summaryrefslogtreecommitdiffstats
path: root/devtools/client
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client')
-rw-r--r--devtools/client/aboutdebugging/aboutdebugging.css199
-rw-r--r--devtools/client/aboutdebugging/aboutdebugging.xhtml22
-rw-r--r--devtools/client/aboutdebugging/components/aboutdebugging.js111
-rw-r--r--devtools/client/aboutdebugging/components/addons/controls.js97
-rw-r--r--devtools/client/aboutdebugging/components/addons/install-error.js26
-rw-r--r--devtools/client/aboutdebugging/components/addons/moz.build10
-rw-r--r--devtools/client/aboutdebugging/components/addons/panel.js146
-rw-r--r--devtools/client/aboutdebugging/components/addons/target.js84
-rw-r--r--devtools/client/aboutdebugging/components/moz.build17
-rw-r--r--devtools/client/aboutdebugging/components/panel-header.js24
-rw-r--r--devtools/client/aboutdebugging/components/panel-menu-entry.js48
-rw-r--r--devtools/client/aboutdebugging/components/panel-menu.js41
-rw-r--r--devtools/client/aboutdebugging/components/tabs/moz.build8
-rw-r--r--devtools/client/aboutdebugging/components/tabs/panel.js98
-rw-r--r--devtools/client/aboutdebugging/components/tabs/target.js53
-rw-r--r--devtools/client/aboutdebugging/components/target-list.js56
-rw-r--r--devtools/client/aboutdebugging/components/workers/moz.build9
-rw-r--r--devtools/client/aboutdebugging/components/workers/panel.js193
-rw-r--r--devtools/client/aboutdebugging/components/workers/service-worker-target.js231
-rw-r--r--devtools/client/aboutdebugging/components/workers/target.js57
-rw-r--r--devtools/client/aboutdebugging/initializer.js67
-rw-r--r--devtools/client/aboutdebugging/modules/addon.js23
-rw-r--r--devtools/client/aboutdebugging/modules/moz.build8
-rw-r--r--devtools/client/aboutdebugging/modules/worker.js77
-rw-r--r--devtools/client/aboutdebugging/moz.build14
-rw-r--r--devtools/client/aboutdebugging/test/.eslintrc.js26
-rw-r--r--devtools/client/aboutdebugging/test/addons/bad/manifest.json1
-rw-r--r--devtools/client/aboutdebugging/test/addons/bug1273184.xpibin0 -> 4246 bytes
-rw-r--r--devtools/client/aboutdebugging/test/addons/test-devtools-webextension-nobg/manifest.json10
-rw-r--r--devtools/client/aboutdebugging/test/addons/test-devtools-webextension/bg.js20
-rw-r--r--devtools/client/aboutdebugging/test/addons/test-devtools-webextension/manifest.json17
-rw-r--r--devtools/client/aboutdebugging/test/addons/test-devtools-webextension/popup.html10
-rw-r--r--devtools/client/aboutdebugging/test/addons/test-devtools-webextension/popup.js13
-rw-r--r--devtools/client/aboutdebugging/test/addons/unpacked/bootstrap.js22
-rw-r--r--devtools/client/aboutdebugging/test/addons/unpacked/install.rdf26
-rw-r--r--devtools/client/aboutdebugging/test/browser.ini44
-rw-r--r--devtools/client/aboutdebugging/test/browser_addons_debug_bootstrapped.js83
-rw-r--r--devtools/client/aboutdebugging/test/browser_addons_debug_webextension.js74
-rw-r--r--devtools/client/aboutdebugging/test/browser_addons_debug_webextension_inspector.js82
-rw-r--r--devtools/client/aboutdebugging/test/browser_addons_debug_webextension_nobg.js84
-rw-r--r--devtools/client/aboutdebugging/test/browser_addons_debug_webextension_popup.js189
-rw-r--r--devtools/client/aboutdebugging/test/browser_addons_debugging_initial_state.js73
-rw-r--r--devtools/client/aboutdebugging/test/browser_addons_install.js51
-rw-r--r--devtools/client/aboutdebugging/test/browser_addons_reload.js207
-rw-r--r--devtools/client/aboutdebugging/test/browser_addons_toggle_debug.js65
-rw-r--r--devtools/client/aboutdebugging/test/browser_page_not_found.js37
-rw-r--r--devtools/client/aboutdebugging/test/browser_service_workers.js51
-rw-r--r--devtools/client/aboutdebugging/test/browser_service_workers_not_compatible.js60
-rw-r--r--devtools/client/aboutdebugging/test/browser_service_workers_push.js105
-rw-r--r--devtools/client/aboutdebugging/test/browser_service_workers_push_service.js122
-rw-r--r--devtools/client/aboutdebugging/test/browser_service_workers_start.js97
-rw-r--r--devtools/client/aboutdebugging/test/browser_service_workers_status.js72
-rw-r--r--devtools/client/aboutdebugging/test/browser_service_workers_timeout.js92
-rw-r--r--devtools/client/aboutdebugging/test/browser_service_workers_unregister.js77
-rw-r--r--devtools/client/aboutdebugging/test/browser_tabs.js59
-rw-r--r--devtools/client/aboutdebugging/test/head.js367
-rw-r--r--devtools/client/aboutdebugging/test/service-workers/delay-sw.html22
-rw-r--r--devtools/client/aboutdebugging/test/service-workers/delay-sw.js17
-rw-r--r--devtools/client/aboutdebugging/test/service-workers/empty-sw.html22
-rw-r--r--devtools/client/aboutdebugging/test/service-workers/empty-sw.js1
-rw-r--r--devtools/client/aboutdebugging/test/service-workers/push-sw.html32
-rw-r--r--devtools/client/aboutdebugging/test/service-workers/push-sw.js33
-rw-r--r--devtools/client/animationinspector/animation-controller.js390
-rw-r--r--devtools/client/animationinspector/animation-inspector.xhtml32
-rw-r--r--devtools/client/animationinspector/animation-panel.js347
-rw-r--r--devtools/client/animationinspector/components/animation-details.js222
-rw-r--r--devtools/client/animationinspector/components/animation-target-node.js80
-rw-r--r--devtools/client/animationinspector/components/animation-time-block.js719
-rw-r--r--devtools/client/animationinspector/components/animation-timeline.js502
-rw-r--r--devtools/client/animationinspector/components/keyframes.js81
-rw-r--r--devtools/client/animationinspector/components/moz.build12
-rw-r--r--devtools/client/animationinspector/components/rate-selector.js105
-rw-r--r--devtools/client/animationinspector/moz.build16
-rw-r--r--devtools/client/animationinspector/test/.eslintrc.js6
-rw-r--r--devtools/client/animationinspector/test/browser.ini71
-rw-r--r--devtools/client/animationinspector/test/browser_animation_animated_properties_displayed.js91
-rw-r--r--devtools/client/animationinspector/test/browser_animation_click_selects_animation.js44
-rw-r--r--devtools/client/animationinspector/test/browser_animation_controller_exposes_document_currentTime.js43
-rw-r--r--devtools/client/animationinspector/test/browser_animation_empty_on_invalid_nodes.js42
-rw-r--r--devtools/client/animationinspector/test/browser_animation_keyframe_click_to_set_time.js52
-rw-r--r--devtools/client/animationinspector/test/browser_animation_keyframe_markers.js74
-rw-r--r--devtools/client/animationinspector/test/browser_animation_mutations_with_same_names.js31
-rw-r--r--devtools/client/animationinspector/test/browser_animation_panel_exists.js23
-rw-r--r--devtools/client/animationinspector/test/browser_animation_participate_in_inspector_update.js46
-rw-r--r--devtools/client/animationinspector/test/browser_animation_playerFronts_are_refreshed.js36
-rw-r--r--devtools/client/animationinspector/test/browser_animation_playerWidgets_appear_on_panel_init.js41
-rw-r--r--devtools/client/animationinspector/test/browser_animation_playerWidgets_target_nodes.js33
-rw-r--r--devtools/client/animationinspector/test/browser_animation_pseudo_elements.js49
-rw-r--r--devtools/client/animationinspector/test/browser_animation_refresh_on_added_animation.js47
-rw-r--r--devtools/client/animationinspector/test/browser_animation_refresh_on_removed_animation.js50
-rw-r--r--devtools/client/animationinspector/test/browser_animation_refresh_when_active.js53
-rw-r--r--devtools/client/animationinspector/test/browser_animation_running_on_compositor.js57
-rw-r--r--devtools/client/animationinspector/test/browser_animation_same_nb_of_playerWidgets_and_playerFronts.js23
-rw-r--r--devtools/client/animationinspector/test/browser_animation_shows_player_on_valid_node.js21
-rw-r--r--devtools/client/animationinspector/test/browser_animation_spacebar_toggles_animations.js49
-rw-r--r--devtools/client/animationinspector/test/browser_animation_spacebar_toggles_node_animations.js45
-rw-r--r--devtools/client/animationinspector/test/browser_animation_target_highlight_select.js73
-rw-r--r--devtools/client/animationinspector/test/browser_animation_target_highlighter_lock.js54
-rw-r--r--devtools/client/animationinspector/test/browser_animation_timeline_currentTime.js48
-rw-r--r--devtools/client/animationinspector/test/browser_animation_timeline_header.js59
-rw-r--r--devtools/client/animationinspector/test/browser_animation_timeline_iterationStart.js71
-rw-r--r--devtools/client/animationinspector/test/browser_animation_timeline_pause_button_01.js34
-rw-r--r--devtools/client/animationinspector/test/browser_animation_timeline_pause_button_02.js48
-rw-r--r--devtools/client/animationinspector/test/browser_animation_timeline_pause_button_03.js60
-rw-r--r--devtools/client/animationinspector/test/browser_animation_timeline_rate_selector.js56
-rw-r--r--devtools/client/animationinspector/test/browser_animation_timeline_rewind_button.js51
-rw-r--r--devtools/client/animationinspector/test/browser_animation_timeline_scrubber_exists.js20
-rw-r--r--devtools/client/animationinspector/test/browser_animation_timeline_scrubber_movable.js70
-rw-r--r--devtools/client/animationinspector/test/browser_animation_timeline_scrubber_moves.js28
-rw-r--r--devtools/client/animationinspector/test/browser_animation_timeline_setCurrentTime.js88
-rw-r--r--devtools/client/animationinspector/test/browser_animation_timeline_shows_delay.js96
-rw-r--r--devtools/client/animationinspector/test/browser_animation_timeline_shows_endDelay.js78
-rw-r--r--devtools/client/animationinspector/test/browser_animation_timeline_shows_iterations.js47
-rw-r--r--devtools/client/animationinspector/test/browser_animation_timeline_shows_name_label.js46
-rw-r--r--devtools/client/animationinspector/test/browser_animation_timeline_shows_time_info.js50
-rw-r--r--devtools/client/animationinspector/test/browser_animation_timeline_takes_rate_into_account.js81
-rw-r--r--devtools/client/animationinspector/test/browser_animation_timeline_ui.js43
-rw-r--r--devtools/client/animationinspector/test/browser_animation_toggle_button_resets_on_navigate.js31
-rw-r--r--devtools/client/animationinspector/test/browser_animation_toggle_button_toggles_animations.js32
-rw-r--r--devtools/client/animationinspector/test/browser_animation_toolbar_exists.js36
-rw-r--r--devtools/client/animationinspector/test/browser_animation_ui_updates_when_animation_data_changes.js53
-rw-r--r--devtools/client/animationinspector/test/doc_body_animation.html23
-rw-r--r--devtools/client/animationinspector/test/doc_end_delay.html69
-rw-r--r--devtools/client/animationinspector/test/doc_frame_script.js122
-rw-r--r--devtools/client/animationinspector/test/doc_keyframes.html55
-rw-r--r--devtools/client/animationinspector/test/doc_modify_playbackRate.html32
-rw-r--r--devtools/client/animationinspector/test/doc_multiple_animation_types.html61
-rw-r--r--devtools/client/animationinspector/test/doc_negative_animation.html66
-rw-r--r--devtools/client/animationinspector/test/doc_pseudo_elements.html61
-rw-r--r--devtools/client/animationinspector/test/doc_script_animation.html71
-rw-r--r--devtools/client/animationinspector/test/doc_simple_animation.html147
-rw-r--r--devtools/client/animationinspector/test/doc_timing_combination_animation.html35
-rw-r--r--devtools/client/animationinspector/test/head.js426
-rw-r--r--devtools/client/animationinspector/test/unit/.eslintrc.js6
-rw-r--r--devtools/client/animationinspector/test/unit/test_findOptimalTimeInterval.js81
-rw-r--r--devtools/client/animationinspector/test/unit/test_formatStopwatchTime.js62
-rw-r--r--devtools/client/animationinspector/test/unit/test_getCssPropertyName.js27
-rw-r--r--devtools/client/animationinspector/test/unit/test_timeScale.js207
-rw-r--r--devtools/client/animationinspector/test/unit/test_timeScale_dimensions.js54
-rw-r--r--devtools/client/animationinspector/test/unit/xpcshell.ini12
-rw-r--r--devtools/client/animationinspector/utils.js275
-rw-r--r--devtools/client/canvasdebugger/callslist.js526
-rw-r--r--devtools/client/canvasdebugger/canvasdebugger.js341
-rw-r--r--devtools/client/canvasdebugger/canvasdebugger.xul135
-rw-r--r--devtools/client/canvasdebugger/moz.build10
-rw-r--r--devtools/client/canvasdebugger/panel.js76
-rw-r--r--devtools/client/canvasdebugger/snapshotslist.js495
-rw-r--r--devtools/client/canvasdebugger/test/.eslintrc.js6
-rw-r--r--devtools/client/canvasdebugger/test/browser.ini61
-rw-r--r--devtools/client/canvasdebugger/test/browser_canvas-actor-test-01.js17
-rw-r--r--devtools/client/canvasdebugger/test/browser_canvas-actor-test-02.js78
-rw-r--r--devtools/client/canvasdebugger/test/browser_canvas-actor-test-03.js75
-rw-r--r--devtools/client/canvasdebugger/test/browser_canvas-actor-test-04.js85
-rw-r--r--devtools/client/canvasdebugger/test/browser_canvas-actor-test-05.js50
-rw-r--r--devtools/client/canvasdebugger/test/browser_canvas-actor-test-06.js100
-rw-r--r--devtools/client/canvasdebugger/test/browser_canvas-actor-test-07.js94
-rw-r--r--devtools/client/canvasdebugger/test/browser_canvas-actor-test-08.js36
-rw-r--r--devtools/client/canvasdebugger/test/browser_canvas-actor-test-09.js36
-rw-r--r--devtools/client/canvasdebugger/test/browser_canvas-actor-test-10.js107
-rw-r--r--devtools/client/canvasdebugger/test/browser_canvas-actor-test-11.js138
-rw-r--r--devtools/client/canvasdebugger/test/browser_canvas-actor-test-12.js29
-rw-r--r--devtools/client/canvasdebugger/test/browser_canvas-frontend-call-highlight.js41
-rw-r--r--devtools/client/canvasdebugger/test/browser_canvas-frontend-call-list.js70
-rw-r--r--devtools/client/canvasdebugger/test/browser_canvas-frontend-call-search.js72
-rw-r--r--devtools/client/canvasdebugger/test/browser_canvas-frontend-call-stack-01.js82
-rw-r--r--devtools/client/canvasdebugger/test/browser_canvas-frontend-call-stack-02.js57
-rw-r--r--devtools/client/canvasdebugger/test/browser_canvas-frontend-call-stack-03.js65
-rw-r--r--devtools/client/canvasdebugger/test/browser_canvas-frontend-clear.js43
-rw-r--r--devtools/client/canvasdebugger/test/browser_canvas-frontend-img-screenshots.js34
-rw-r--r--devtools/client/canvasdebugger/test/browser_canvas-frontend-img-thumbnails-01.js65
-rw-r--r--devtools/client/canvasdebugger/test/browser_canvas-frontend-img-thumbnails-02.js67
-rw-r--r--devtools/client/canvasdebugger/test/browser_canvas-frontend-open.js41
-rw-r--r--devtools/client/canvasdebugger/test/browser_canvas-frontend-record-01.js60
-rw-r--r--devtools/client/canvasdebugger/test/browser_canvas-frontend-record-02.js73
-rw-r--r--devtools/client/canvasdebugger/test/browser_canvas-frontend-record-03.js37
-rw-r--r--devtools/client/canvasdebugger/test/browser_canvas-frontend-record-04.js34
-rw-r--r--devtools/client/canvasdebugger/test/browser_canvas-frontend-reload-01.js55
-rw-r--r--devtools/client/canvasdebugger/test/browser_canvas-frontend-reload-02.js70
-rw-r--r--devtools/client/canvasdebugger/test/browser_canvas-frontend-slider-01.js39
-rw-r--r--devtools/client/canvasdebugger/test/browser_canvas-frontend-slider-02.js97
-rw-r--r--devtools/client/canvasdebugger/test/browser_canvas-frontend-snapshot-select-01.js93
-rw-r--r--devtools/client/canvasdebugger/test/browser_canvas-frontend-snapshot-select-02.js30
-rw-r--r--devtools/client/canvasdebugger/test/browser_canvas-frontend-stepping.js76
-rw-r--r--devtools/client/canvasdebugger/test/browser_canvas-frontend-stop-01.js36
-rw-r--r--devtools/client/canvasdebugger/test/browser_canvas-frontend-stop-02.js35
-rw-r--r--devtools/client/canvasdebugger/test/browser_canvas-frontend-stop-03.js36
-rw-r--r--devtools/client/canvasdebugger/test/browser_profiling-canvas.js45
-rw-r--r--devtools/client/canvasdebugger/test/browser_profiling-webgl.js91
-rw-r--r--devtools/client/canvasdebugger/test/doc_no-canvas.html14
-rw-r--r--devtools/client/canvasdebugger/test/doc_raf-begin.html36
-rw-r--r--devtools/client/canvasdebugger/test/doc_raf-no-canvas.html18
-rw-r--r--devtools/client/canvasdebugger/test/doc_settimeout.html37
-rw-r--r--devtools/client/canvasdebugger/test/doc_simple-canvas-bitmasks.html34
-rw-r--r--devtools/client/canvasdebugger/test/doc_simple-canvas-deep-stack.html46
-rw-r--r--devtools/client/canvasdebugger/test/doc_simple-canvas-transparent.html37
-rw-r--r--devtools/client/canvasdebugger/test/doc_simple-canvas.html37
-rw-r--r--devtools/client/canvasdebugger/test/doc_webgl-bindings.html61
-rw-r--r--devtools/client/canvasdebugger/test/doc_webgl-drawArrays.html187
-rw-r--r--devtools/client/canvasdebugger/test/doc_webgl-drawElements.html225
-rw-r--r--devtools/client/canvasdebugger/test/doc_webgl-enum.html34
-rw-r--r--devtools/client/canvasdebugger/test/head.js305
-rw-r--r--devtools/client/commandline/commandline.css85
-rw-r--r--devtools/client/commandline/commandlineoutput.xhtml17
-rw-r--r--devtools/client/commandline/commandlinetooltip.xhtml18
-rw-r--r--devtools/client/commandline/moz.build5
-rw-r--r--devtools/client/commandline/test/.eslintrc.js10
-rw-r--r--devtools/client/commandline/test/browser.ini124
-rw-r--r--devtools/client/commandline/test/browser_cmd_addon.js195
-rw-r--r--devtools/client/commandline/test/browser_cmd_appcache_invalid.js134
-rw-r--r--devtools/client/commandline/test/browser_cmd_appcache_invalid_appcache.appcache55
-rw-r--r--devtools/client/commandline/test/browser_cmd_appcache_invalid_appcache.appcache^headers^2
-rw-r--r--devtools/client/commandline/test/browser_cmd_appcache_invalid_index.html14
-rw-r--r--devtools/client/commandline/test/browser_cmd_appcache_invalid_page1.html14
-rw-r--r--devtools/client/commandline/test/browser_cmd_appcache_invalid_page2.html14
-rw-r--r--devtools/client/commandline/test/browser_cmd_appcache_invalid_page3.html14
-rw-r--r--devtools/client/commandline/test/browser_cmd_appcache_invalid_page3.html^headers^2
-rw-r--r--devtools/client/commandline/test/browser_cmd_appcache_valid.js173
-rw-r--r--devtools/client/commandline/test/browser_cmd_appcache_valid_appcache.appcache5
-rw-r--r--devtools/client/commandline/test/browser_cmd_appcache_valid_appcache.appcache^headers^2
-rw-r--r--devtools/client/commandline/test/browser_cmd_appcache_valid_index.html13
-rw-r--r--devtools/client/commandline/test/browser_cmd_appcache_valid_page1.html13
-rw-r--r--devtools/client/commandline/test/browser_cmd_appcache_valid_page2.html13
-rw-r--r--devtools/client/commandline/test/browser_cmd_appcache_valid_page3.html13
-rw-r--r--devtools/client/commandline/test/browser_cmd_calllog.js119
-rw-r--r--devtools/client/commandline/test/browser_cmd_calllog_chrome.js116
-rw-r--r--devtools/client/commandline/test/browser_cmd_commands.js63
-rw-r--r--devtools/client/commandline/test/browser_cmd_cookie.html19
-rw-r--r--devtools/client/commandline/test/browser_cmd_cookie.js170
-rw-r--r--devtools/client/commandline/test/browser_cmd_cookie_host.js41
-rw-r--r--devtools/client/commandline/test/browser_cmd_csscoverage_oneshot.js318
-rw-r--r--devtools/client/commandline/test/browser_cmd_csscoverage_page1.html85
-rw-r--r--devtools/client/commandline/test/browser_cmd_csscoverage_page2.html59
-rw-r--r--devtools/client/commandline/test/browser_cmd_csscoverage_page3.html52
-rw-r--r--devtools/client/commandline/test/browser_cmd_csscoverage_sheetA.css22
-rw-r--r--devtools/client/commandline/test/browser_cmd_csscoverage_sheetB.css20
-rw-r--r--devtools/client/commandline/test/browser_cmd_csscoverage_sheetC.css20
-rw-r--r--devtools/client/commandline/test/browser_cmd_csscoverage_sheetD.css20
-rw-r--r--devtools/client/commandline/test/browser_cmd_csscoverage_startstop.js465
-rw-r--r--devtools/client/commandline/test/browser_cmd_csscoverage_util.js24
-rw-r--r--devtools/client/commandline/test/browser_cmd_folder.js58
-rw-r--r--devtools/client/commandline/test/browser_cmd_highlight_01.js90
-rw-r--r--devtools/client/commandline/test/browser_cmd_highlight_02.js45
-rw-r--r--devtools/client/commandline/test/browser_cmd_highlight_03.js131
-rw-r--r--devtools/client/commandline/test/browser_cmd_highlight_04.js94
-rw-r--r--devtools/client/commandline/test/browser_cmd_inject.html8
-rw-r--r--devtools/client/commandline/test/browser_cmd_inject.js69
-rw-r--r--devtools/client/commandline/test/browser_cmd_jsb.js103
-rw-r--r--devtools/client/commandline/test/browser_cmd_jsb_script.jsi2
-rw-r--r--devtools/client/commandline/test/browser_cmd_listen.js80
-rw-r--r--devtools/client/commandline/test/browser_cmd_measure.js53
-rw-r--r--devtools/client/commandline/test/browser_cmd_media.html28
-rw-r--r--devtools/client/commandline/test/browser_cmd_media.js88
-rw-r--r--devtools/client/commandline/test/browser_cmd_pagemod_export.html25
-rw-r--r--devtools/client/commandline/test/browser_cmd_pagemod_export.js417
-rw-r--r--devtools/client/commandline/test/browser_cmd_paintflashing.js60
-rw-r--r--devtools/client/commandline/test/browser_cmd_pref1.js154
-rw-r--r--devtools/client/commandline/test/browser_cmd_pref2.js105
-rw-r--r--devtools/client/commandline/test/browser_cmd_pref3.js113
-rw-r--r--devtools/client/commandline/test/browser_cmd_qsa.js33
-rw-r--r--devtools/client/commandline/test/browser_cmd_restart.js61
-rw-r--r--devtools/client/commandline/test/browser_cmd_rulers.js53
-rw-r--r--devtools/client/commandline/test/browser_cmd_screenshot.html18
-rw-r--r--devtools/client/commandline/test/browser_cmd_screenshot.js374
-rw-r--r--devtools/client/commandline/test/browser_cmd_settings.js124
-rw-r--r--devtools/client/commandline/test/browser_gcli_async.js110
-rw-r--r--devtools/client/commandline/test/browser_gcli_canon.js286
-rw-r--r--devtools/client/commandline/test/browser_gcli_cli1.js528
-rw-r--r--devtools/client/commandline/test/browser_gcli_cli2.js788
-rw-r--r--devtools/client/commandline/test/browser_gcli_completion1.js277
-rw-r--r--devtools/client/commandline/test/browser_gcli_completion2.js263
-rw-r--r--devtools/client/commandline/test/browser_gcli_context.js239
-rw-r--r--devtools/client/commandline/test/browser_gcli_date.js358
-rw-r--r--devtools/client/commandline/test/browser_gcli_exec.js656
-rw-r--r--devtools/client/commandline/test/browser_gcli_fail.js73
-rw-r--r--devtools/client/commandline/test/browser_gcli_file.js821
-rw-r--r--devtools/client/commandline/test/browser_gcli_fileparser.js46
-rw-r--r--devtools/client/commandline/test/browser_gcli_filesystem.js66
-rw-r--r--devtools/client/commandline/test/browser_gcli_focus.js67
-rw-r--r--devtools/client/commandline/test/browser_gcli_history.js72
-rw-r--r--devtools/client/commandline/test/browser_gcli_incomplete.js439
-rw-r--r--devtools/client/commandline/test/browser_gcli_inputter.js97
-rw-r--r--devtools/client/commandline/test/browser_gcli_intro.js71
-rw-r--r--devtools/client/commandline/test/browser_gcli_js.js570
-rw-r--r--devtools/client/commandline/test/browser_gcli_keyboard1.js72
-rw-r--r--devtools/client/commandline/test/browser_gcli_keyboard2.js121
-rw-r--r--devtools/client/commandline/test/browser_gcli_keyboard3.js119
-rw-r--r--devtools/client/commandline/test/browser_gcli_keyboard4.js189
-rw-r--r--devtools/client/commandline/test/browser_gcli_keyboard5.js57
-rw-r--r--devtools/client/commandline/test/browser_gcli_keyboard6.js65
-rw-r--r--devtools/client/commandline/test/browser_gcli_menu.js51
-rw-r--r--devtools/client/commandline/test/browser_gcli_node.js317
-rw-r--r--devtools/client/commandline/test/browser_gcli_pref1.js166
-rw-r--r--devtools/client/commandline/test/browser_gcli_pref2.js119
-rw-r--r--devtools/client/commandline/test/browser_gcli_remotews.js485
-rw-r--r--devtools/client/commandline/test/browser_gcli_remotexhr.js485
-rw-r--r--devtools/client/commandline/test/browser_gcli_resource.js154
-rw-r--r--devtools/client/commandline/test/browser_gcli_short.js248
-rw-r--r--devtools/client/commandline/test/browser_gcli_spell.js72
-rw-r--r--devtools/client/commandline/test/browser_gcli_split.js67
-rw-r--r--devtools/client/commandline/test/browser_gcli_string.js270
-rw-r--r--devtools/client/commandline/test/browser_gcli_tokenize.js290
-rw-r--r--devtools/client/commandline/test/browser_gcli_tooltip.js132
-rw-r--r--devtools/client/commandline/test/browser_gcli_types.js118
-rw-r--r--devtools/client/commandline/test/browser_gcli_union.js173
-rw-r--r--devtools/client/commandline/test/browser_gcli_url.js107
-rw-r--r--devtools/client/commandline/test/head.js40
-rw-r--r--devtools/client/commandline/test/helpers.js1341
-rw-r--r--devtools/client/commandline/test/mockCommands.js794
-rw-r--r--devtools/client/debugger/content/actions/breakpoints.js191
-rw-r--r--devtools/client/debugger/content/actions/event-listeners.js118
-rw-r--r--devtools/client/debugger/content/actions/moz.build10
-rw-r--r--devtools/client/debugger/content/actions/sources.js280
-rw-r--r--devtools/client/debugger/content/constants.js25
-rw-r--r--devtools/client/debugger/content/globalActions.js18
-rw-r--r--devtools/client/debugger/content/moz.build17
-rw-r--r--devtools/client/debugger/content/queries.js70
-rw-r--r--devtools/client/debugger/content/reducers/async-requests.js31
-rw-r--r--devtools/client/debugger/content/reducers/breakpoints.js153
-rw-r--r--devtools/client/debugger/content/reducers/event-listeners.js37
-rw-r--r--devtools/client/debugger/content/reducers/index.js16
-rw-r--r--devtools/client/debugger/content/reducers/moz.build12
-rw-r--r--devtools/client/debugger/content/reducers/sources.js128
-rw-r--r--devtools/client/debugger/content/utils.js88
-rw-r--r--devtools/client/debugger/content/views/event-listeners-view.js295
-rw-r--r--devtools/client/debugger/content/views/moz.build9
-rw-r--r--devtools/client/debugger/content/views/sources-view.js1370
-rw-r--r--devtools/client/debugger/debugger-commands.js633
-rw-r--r--devtools/client/debugger/debugger-controller.js1276
-rw-r--r--devtools/client/debugger/debugger-view.js982
-rw-r--r--devtools/client/debugger/debugger.css69
-rw-r--r--devtools/client/debugger/debugger.xul474
-rw-r--r--devtools/client/debugger/moz.build20
-rw-r--r--devtools/client/debugger/new/bundle.js58335
-rw-r--r--devtools/client/debugger/new/images/Icons.js46
-rw-r--r--devtools/client/debugger/new/images/Svg.js43
-rw-r--r--devtools/client/debugger/new/images/angle-brackets.svg9
-rw-r--r--devtools/client/debugger/new/images/arrow.svg6
-rw-r--r--devtools/client/debugger/new/images/blackBox.svg9
-rw-r--r--devtools/client/debugger/new/images/breakpoint.svg6
-rw-r--r--devtools/client/debugger/new/images/close.svg7
-rw-r--r--devtools/client/debugger/new/images/disableBreakpoints.svg8
-rw-r--r--devtools/client/debugger/new/images/domain.svg7
-rw-r--r--devtools/client/debugger/new/images/favicon.png0
-rw-r--r--devtools/client/debugger/new/images/file.svg7
-rw-r--r--devtools/client/debugger/new/images/folder.svg6
-rw-r--r--devtools/client/debugger/new/images/globe.svg10
-rw-r--r--devtools/client/debugger/new/images/magnifying-glass.svg4
-rw-r--r--devtools/client/debugger/new/images/pause-circle.svg10
-rw-r--r--devtools/client/debugger/new/images/pause-exceptions.svg7
-rw-r--r--devtools/client/debugger/new/images/pause.svg8
-rw-r--r--devtools/client/debugger/new/images/play.svg6
-rw-r--r--devtools/client/debugger/new/images/plus.svg6
-rw-r--r--devtools/client/debugger/new/images/prettyPrint.svg6
-rw-r--r--devtools/client/debugger/new/images/resume.svg6
-rw-r--r--devtools/client/debugger/new/images/sad-face.svg9
-rw-r--r--devtools/client/debugger/new/images/settings.svg6
-rw-r--r--devtools/client/debugger/new/images/stepIn.svg8
-rw-r--r--devtools/client/debugger/new/images/stepOut.svg8
-rw-r--r--devtools/client/debugger/new/images/stepOver.svg9
-rw-r--r--devtools/client/debugger/new/images/subSettings.svg6
-rw-r--r--devtools/client/debugger/new/images/toggle-breakpoints.svg8
-rw-r--r--devtools/client/debugger/new/images/worker.svg6
-rw-r--r--devtools/client/debugger/new/index.html31
-rw-r--r--devtools/client/debugger/new/moz.build12
-rw-r--r--devtools/client/debugger/new/panel.js77
-rw-r--r--devtools/client/debugger/new/pretty-print-worker.js5904
-rw-r--r--devtools/client/debugger/new/source-map-worker.js5831
-rw-r--r--devtools/client/debugger/new/styles.css1724
-rw-r--r--devtools/client/debugger/new/test/mochitest/.eslintrc80
-rw-r--r--devtools/client/debugger/new/test/mochitest/browser.ini60
-rw-r--r--devtools/client/debugger/new/test/mochitest/browser_dbg-breaking-from-console.js31
-rw-r--r--devtools/client/debugger/new/test/mochitest/browser_dbg-breaking.js32
-rw-r--r--devtools/client/debugger/new/test/mochitest/browser_dbg-breakpoints-cond.js50
-rw-r--r--devtools/client/debugger/new/test/mochitest/browser_dbg-breakpoints.js101
-rw-r--r--devtools/client/debugger/new/test/mochitest/browser_dbg-call-stack.js62
-rw-r--r--devtools/client/debugger/new/test/mochitest/browser_dbg-chrome-create.js72
-rw-r--r--devtools/client/debugger/new/test/mochitest/browser_dbg-chrome-debugging.js88
-rw-r--r--devtools/client/debugger/new/test/mochitest/browser_dbg-console.js34
-rw-r--r--devtools/client/debugger/new/test/mochitest/browser_dbg-debugger-buttons.js54
-rw-r--r--devtools/client/debugger/new/test/mochitest/browser_dbg-editor-gutter.js64
-rw-r--r--devtools/client/debugger/new/test/mochitest/browser_dbg-editor-highlight.js46
-rw-r--r--devtools/client/debugger/new/test/mochitest/browser_dbg-editor-mode.js14
-rw-r--r--devtools/client/debugger/new/test/mochitest/browser_dbg-editor-select.js54
-rw-r--r--devtools/client/debugger/new/test/mochitest/browser_dbg-iframes.js26
-rw-r--r--devtools/client/debugger/new/test/mochitest/browser_dbg-navigation.js47
-rw-r--r--devtools/client/debugger/new/test/mochitest/browser_dbg-pause-exceptions.js46
-rw-r--r--devtools/client/debugger/new/test/mochitest/browser_dbg-pretty-print-paused.js22
-rw-r--r--devtools/client/debugger/new/test/mochitest/browser_dbg-pretty-print.js31
-rw-r--r--devtools/client/debugger/new/test/mochitest/browser_dbg-scopes.js27
-rw-r--r--devtools/client/debugger/new/test/mochitest/browser_dbg-searching.js28
-rw-r--r--devtools/client/debugger/new/test/mochitest/browser_dbg-sourcemaps-bogus.js23
-rw-r--r--devtools/client/debugger/new/test/mochitest/browser_dbg-sourcemaps.js44
-rw-r--r--devtools/client/debugger/new/test/mochitest/browser_dbg-sources.js58
-rw-r--r--devtools/client/debugger/new/test/mochitest/browser_dbg_keyboard-shortcuts.js46
-rw-r--r--devtools/client/debugger/new/test/mochitest/examples/README.md7
-rw-r--r--devtools/client/debugger/new/test/mochitest/examples/bogus-map.js8
-rw-r--r--devtools/client/debugger/new/test/mochitest/examples/bundle.js96
-rw-r--r--devtools/client/debugger/new/test/mochitest/examples/bundle.js.map1
-rw-r--r--devtools/client/debugger/new/test/mochitest/examples/doc-debugger-statements.html27
-rw-r--r--devtools/client/debugger/new/test/mochitest/examples/doc-exceptions.html7
-rw-r--r--devtools/client/debugger/new/test/mochitest/examples/doc-frames.html17
-rw-r--r--devtools/client/debugger/new/test/mochitest/examples/doc-iframes.html17
-rw-r--r--devtools/client/debugger/new/test/mochitest/examples/doc-minified.html14
-rw-r--r--devtools/client/debugger/new/test/mochitest/examples/doc-script-switching.html18
-rw-r--r--devtools/client/debugger/new/test/mochitest/examples/doc-scripts.html21
-rw-r--r--devtools/client/debugger/new/test/mochitest/examples/doc-sourcemap-bogus.html13
-rw-r--r--devtools/client/debugger/new/test/mochitest/examples/doc-sourcemaps.html13
-rw-r--r--devtools/client/debugger/new/test/mochitest/examples/doc-sources.html23
-rw-r--r--devtools/client/debugger/new/test/mochitest/examples/entry.js16
-rw-r--r--devtools/client/debugger/new/test/mochitest/examples/exceptions.js19
-rw-r--r--devtools/client/debugger/new/test/mochitest/examples/frames.js24
-rw-r--r--devtools/client/debugger/new/test/mochitest/examples/long.js76
-rw-r--r--devtools/client/debugger/new/test/mochitest/examples/math.min.js3
-rw-r--r--devtools/client/debugger/new/test/mochitest/examples/nested/nested-source.js3
-rw-r--r--devtools/client/debugger/new/test/mochitest/examples/opts.js3
-rw-r--r--devtools/client/debugger/new/test/mochitest/examples/output.js5
-rw-r--r--devtools/client/debugger/new/test/mochitest/examples/script-switching-01.js6
-rw-r--r--devtools/client/debugger/new/test/mochitest/examples/script-switching-02.js13
-rw-r--r--devtools/client/debugger/new/test/mochitest/examples/simple1.js31
-rw-r--r--devtools/client/debugger/new/test/mochitest/examples/simple2.js6
-rw-r--r--devtools/client/debugger/new/test/mochitest/examples/times2.js3
-rw-r--r--devtools/client/debugger/new/test/mochitest/examples/webpack.config.js8
-rw-r--r--devtools/client/debugger/new/test/mochitest/head.js684
-rw-r--r--devtools/client/debugger/panel.js180
-rw-r--r--devtools/client/debugger/test/.eslintrc.js6
-rw-r--r--devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon3/lib/main.js13
-rw-r--r--devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon3/package.json9
-rw-r--r--devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon4/bootstrap.js36
-rw-r--r--devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon4/chrome.manifest1
-rw-r--r--devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon4/install.rdf19
-rw-r--r--devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon4/test.jsm6
-rw-r--r--devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon4/test.xul8
-rw-r--r--devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon4/test2.jsm6
-rw-r--r--devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon4/test2.xul8
-rw-r--r--devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon4/testxul.js4
-rw-r--r--devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon4/testxul2.js4
-rw-r--r--devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon5/bootstrap.js23
-rw-r--r--devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon5/chrome.manifest1
-rw-r--r--devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon5/install.rdf20
-rw-r--r--devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon5/test.jsm6
-rw-r--r--devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon5/test.xul8
-rw-r--r--devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon5/test2.jsm6
-rw-r--r--devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon5/test2.xul8
-rw-r--r--devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon5/testxul.js4
-rw-r--r--devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon5/testxul2.js4
-rw-r--r--devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon_webext_contentscript/manifest.json18
-rw-r--r--devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon_webext_contentscript/webext-content-script.js1
-rw-r--r--devtools/client/debugger/test/mochitest/addon-webext-contentscript.xpibin0 -> 4648 bytes
-rw-r--r--devtools/client/debugger/test/mochitest/addon1.xpibin0 -> 5577 bytes
-rw-r--r--devtools/client/debugger/test/mochitest/addon2.xpibin0 -> 5578 bytes
-rw-r--r--devtools/client/debugger/test/mochitest/addon3.xpibin0 -> 12718 bytes
-rw-r--r--devtools/client/debugger/test/mochitest/addon4.xpibin0 -> 7340 bytes
-rw-r--r--devtools/client/debugger/test/mochitest/addon5.xpibin0 -> 7224 bytes
-rw-r--r--devtools/client/debugger/test/mochitest/browser.ini317
-rw-r--r--devtools/client/debugger/test/mochitest/browser2.ini460
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_WorkerActor.attach.js62
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_WorkerActor.attachThread.js100
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_aaa_run_first_leaktest.js33
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_addon-console.js47
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_addon-modules-unpacked.js67
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_addon-modules.js66
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_addon-panels.js49
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_addon-sources.js42
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_addon-workers-dbg-enabled.js41
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_addonactor.js95
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_auto-pretty-print-01.js117
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_auto-pretty-print-02.js126
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_auto-pretty-print-03.js58
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_bfcache.js95
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_blackboxing-01.js57
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_blackboxing-02.js60
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_blackboxing-03.js65
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_blackboxing-04.js65
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_blackboxing-05.js74
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_blackboxing-06.js61
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_blackboxing-07.js53
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_breadcrumbs-access.js98
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_break-in-anon.js40
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_break-on-dom-01.js58
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_break-on-dom-02.js135
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_break-on-dom-03.js102
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_break-on-dom-04.js101
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_break-on-dom-05.js128
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_break-on-dom-06.js130
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_break-on-dom-07.js106
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_break-on-dom-08.js61
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_break-on-dom-event-01.js225
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_break-on-dom-event-02.js105
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_break-on-dom-event-03.js97
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_break-on-next-console.js61
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_break-on-next.js103
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_break-unselected.js48
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-actual-location.js55
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-actual-location2.js88
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-break-on-last-line-of-script-on-reload.js116
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-button-01.js55
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-button-02.js64
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-condition-thrown-message.js107
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-contextmenu-add.js84
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-contextmenu.js252
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-disabled-reload.js124
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-editor.js241
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-eval.js47
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-highlight.js90
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-new-script.js92
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-other-tabs.js41
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-pane.js238
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-reload.js39
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_bug-896139.js48
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_chrome-create.js64
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_chrome-debugging.js102
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_clean-exit-window.js86
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_clean-exit.js44
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_closure-inspection.js153
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_cmd-blackbox.js117
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_cmd-break.js225
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_cmd-dbg.js102
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_conditional-breakpoints-01.js218
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_conditional-breakpoints-02.js219
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_conditional-breakpoints-03.js78
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_conditional-breakpoints-04.js52
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_conditional-breakpoints-05.js141
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_console-eval.js41
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_console-named-eval.js42
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_controller-evaluate-01.js106
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_controller-evaluate-02.js78
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_debugger-statement.js87
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_editor-contextmenu.js68
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_editor-mode.js97
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_event-listeners-01.js147
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_event-listeners-02.js123
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_event-listeners-03.js82
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_event-listeners-04.js55
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_file-reload.js72
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_function-display-name.js68
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_global-method-override.js26
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_globalactor.js61
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_hide-toolbar-buttons.js34
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_host-layout.js166
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_iframes.js72
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_instruments-pane-collapse.js167
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_instruments-pane-collapse_keyboard.js40
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_interrupts.js123
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_jump-to-function-definition.js50
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_listaddons.js112
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_listtabs-01.js98
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_listtabs-02.js219
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_listtabs-03.js61
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_listworkers.js59
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_location-changes-01-simple.js60
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_location-changes-02-blank.js57
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_location-changes-03-new.js59
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_location-changes-04-breakpoint.js165
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_multiple-windows.js165
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_navigation.js75
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_no-dangling-breakpoints.js25
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_no-page-sources.js54
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_on-pause-highlight.js86
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_on-pause-raise.js120
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_optimized-out-vars.js50
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_panel-size.js88
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_parser-01.js33
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_parser-02.js30
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_parser-03.js79
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_parser-04.js58
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_parser-05.js45
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_parser-06.js80
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_parser-07.js57
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_parser-08.js291
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_parser-09.js292
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_parser-10.js129
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_parser-11.js41
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_parser-computed-name.js32
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_parser-function-defaults.js31
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_parser-spread-expression.js32
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_parser-template-strings.js29
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_pause-exceptions-01.js246
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_pause-exceptions-02.js204
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_pause-no-step.js94
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_pause-resume.js91
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_pause-warning.js109
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_paused-keybindings.js50
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_post-page.js53
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-01.js52
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-02.js41
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-03.js40
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-04.js50
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-05.js66
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-06.js80
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-07.js62
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-08.js99
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-09.js92
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-10.js48
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-11.js65
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-12.js51
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-13.js53
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-on-paused.js69
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_progress-listener-bug.js89
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_promises-allocation-stack.js87
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_promises-chrome-allocation-stack.js100
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_promises-fulfillment-stack.js106
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_promises-rejection-stack.js106
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_reload-preferred-script-02.js50
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_reload-preferred-script-03.js62
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_reload-same-script.js88
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_scripts-switching-01.js162
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_scripts-switching-02.js163
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_scripts-switching-03.js63
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_search-autofill-identifier.js138
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_search-basic-01.js330
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_search-basic-02.js129
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_search-basic-03.js123
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_search-basic-04.js132
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_search-global-01.js278
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_search-global-02.js203
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_search-global-03.js110
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_search-global-04.js98
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_search-global-05.js160
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_search-global-06.js125
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_search-popup-jank.js128
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_search-sources-01.js232
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_search-sources-02.js281
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_search-sources-03.js103
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_search-symbols.js472
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_searchbox-help-popup-01.js64
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_searchbox-help-popup-02.js90
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_searchbox-parse.js126
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_server-conditional-bp-01.js218
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_server-conditional-bp-02.js214
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_server-conditional-bp-03.js73
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_server-conditional-bp-04.js46
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_server-conditional-bp-05.js134
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_source-maps-01.js170
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_source-maps-02.js153
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_source-maps-03.js88
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_source-maps-04.js187
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_sources-bookmarklet.js53
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_sources-cache.js147
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_sources-contextmenu-01.js57
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_sources-contextmenu-02.js75
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_sources-eval-01.js44
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_sources-eval-02.js55
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_sources-iframe-reload.js35
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_sources-keybindings.js40
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_sources-labels.js172
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_sources-large.js80
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_sources-sorting.js141
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_sources-webext-contentscript.js63
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_split-console-keypress.js108
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_split-console-paused-reload.js67
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_stack-01.js49
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_stack-02.js115
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_stack-03.js64
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_stack-04.js58
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_stack-05.js102
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_stack-06.js92
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_stack-07.js113
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_stack-contextmenu-01.js58
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_stack-contextmenu-02.js58
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_step-out.js91
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_tabactor-01.js65
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_tabactor-02.js79
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_terminate-on-tab-close.js34
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_variables-view-01.js132
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_variables-view-02.js227
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_variables-view-03.js157
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_variables-view-04.js156
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_variables-view-05.js234
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_variables-view-06.js125
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_variables-view-07.js69
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_variables-view-08.js61
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_variables-view-accessibility.js557
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_variables-view-data.js611
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_variables-view-edit-cancel.js58
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_variables-view-edit-click.js58
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_variables-view-edit-getset-01.js300
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_variables-view-edit-getset-02.js107
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_variables-view-edit-value.js91
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_variables-view-edit-watch.js510
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_variables-view-filter-01.js241
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_variables-view-filter-02.js249
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_variables-view-filter-03.js178
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_variables-view-filter-04.js243
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_variables-view-filter-05.js254
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_variables-view-filter-pref.js85
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_variables-view-filter-searchbox.js150
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_variables-view-frame-parameters-01.js270
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_variables-view-frame-parameters-02.js552
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_variables-view-frame-parameters-03.js157
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_variables-view-frame-with.js212
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_variables-view-frozen-sealed-nonext.js93
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_variables-view-hide-non-enums.js111
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_variables-view-large-array-buffer.js253
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_variables-view-map-set.js117
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_variables-view-override-01.js240
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_variables-view-override-02.js75
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-01.js67
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-02.js53
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-03.js49
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-04.js38
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-05.js57
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-06.js83
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-07.js70
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-08.js75
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-09.js39
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-10.js67
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-11.js84
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-12.js77
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-13.js68
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-14.js55
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-15.js39
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-16.js77
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-17.js80
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_variables-view-reexpand-01.js211
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_variables-view-reexpand-02.js226
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_variables-view-reexpand-03.js120
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_variables-view-webidl.js262
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_watch-expressions-01.js227
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_watch-expressions-02.js383
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_worker-console-01.js21
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_worker-console-02.js58
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_worker-console-03.js46
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_worker-source-map.js89
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg_worker-window.js61
-rw-r--r--devtools/client/debugger/test/mochitest/code_WorkerActor.attach-worker1.js5
-rw-r--r--devtools/client/debugger/test/mochitest/code_WorkerActor.attach-worker2.js5
-rw-r--r--devtools/client/debugger/test/mochitest/code_WorkerActor.attachThread-worker.js16
-rw-r--r--devtools/client/debugger/test/mochitest/code_binary_search.coffee18
-rw-r--r--devtools/client/debugger/test/mochitest/code_binary_search.js29
-rw-r--r--devtools/client/debugger/test/mochitest/code_binary_search.map10
-rw-r--r--devtools/client/debugger/test/mochitest/code_blackboxing_blackboxme.js9
-rw-r--r--devtools/client/debugger/test/mochitest/code_blackboxing_one.js4
-rw-r--r--devtools/client/debugger/test/mochitest/code_blackboxing_three.js4
-rw-r--r--devtools/client/debugger/test/mochitest/code_blackboxing_two.js4
-rw-r--r--devtools/client/debugger/test/mochitest/code_blackboxing_unblackbox.min.js1
-rw-r--r--devtools/client/debugger/test/mochitest/code_breakpoints-break-on-last-line-of-script-on-reload.js6
-rw-r--r--devtools/client/debugger/test/mochitest/code_breakpoints-other-tabs.js4
-rw-r--r--devtools/client/debugger/test/mochitest/code_bug-896139.js8
-rw-r--r--devtools/client/debugger/test/mochitest/code_frame-script.js106
-rw-r--r--devtools/client/debugger/test/mochitest/code_function-jump-01.js6
-rw-r--r--devtools/client/debugger/test/mochitest/code_function-search-01.js42
-rw-r--r--devtools/client/debugger/test/mochitest/code_function-search-02.js21
-rw-r--r--devtools/client/debugger/test/mochitest/code_function-search-03.js32
-rw-r--r--devtools/client/debugger/test/mochitest/code_listworkers-worker1.js3
-rw-r--r--devtools/client/debugger/test/mochitest/code_listworkers-worker2.js3
-rw-r--r--devtools/client/debugger/test/mochitest/code_location-changes.js7
-rw-r--r--devtools/client/debugger/test/mochitest/code_math.js45
-rw-r--r--devtools/client/debugger/test/mochitest/code_math.map8
-rw-r--r--devtools/client/debugger/test/mochitest/code_math.min.js2
-rw-r--r--devtools/client/debugger/test/mochitest/code_math_bogus_map.js4
-rw-r--r--devtools/client/debugger/test/mochitest/code_same-line-functions.js1
-rw-r--r--devtools/client/debugger/test/mochitest/code_script-eval.js14
-rw-r--r--devtools/client/debugger/test/mochitest/code_script-switching-01.js6
-rw-r--r--devtools/client/debugger/test/mochitest/code_script-switching-02.js13
-rw-r--r--devtools/client/debugger/test/mochitest/code_test-editor-mode6
-rw-r--r--devtools/client/debugger/test/mochitest/code_ugly-2.js1
-rw-r--r--devtools/client/debugger/test/mochitest/code_ugly-3.js1
-rw-r--r--devtools/client/debugger/test/mochitest/code_ugly-4.js25
-rw-r--r--devtools/client/debugger/test/mochitest/code_ugly-5.js14
-rw-r--r--devtools/client/debugger/test/mochitest/code_ugly-6.js5
-rw-r--r--devtools/client/debugger/test/mochitest/code_ugly-7.js5
-rw-r--r--devtools/client/debugger/test/mochitest/code_ugly-83
-rw-r--r--devtools/client/debugger/test/mochitest/code_ugly-8^headers^1
-rw-r--r--devtools/client/debugger/test/mochitest/code_ugly.js3
-rw-r--r--devtools/client/debugger/test/mochitest/code_worker-source-map.coffee22
-rw-r--r--devtools/client/debugger/test/mochitest/code_worker-source-map.js35
-rw-r--r--devtools/client/debugger/test/mochitest/code_worker-source-map.js.map10
-rw-r--r--devtools/client/debugger/test/mochitest/code_workeractor-worker.js5
-rw-r--r--devtools/client/debugger/test/mochitest/doc_WorkerActor.attach-tab1.html8
-rw-r--r--devtools/client/debugger/test/mochitest/doc_WorkerActor.attach-tab2.html8
-rw-r--r--devtools/client/debugger/test/mochitest/doc_WorkerActor.attachThread-tab.html8
-rw-r--r--devtools/client/debugger/test/mochitest/doc_auto-pretty-print-01.html14
-rw-r--r--devtools/client/debugger/test/mochitest/doc_auto-pretty-print-02.html14
-rw-r--r--devtools/client/debugger/test/mochitest/doc_binary_search.html15
-rw-r--r--devtools/client/debugger/test/mochitest/doc_blackboxing.html26
-rw-r--r--devtools/client/debugger/test/mochitest/doc_blackboxing_unblackbox.html11
-rw-r--r--devtools/client/debugger/test/mochitest/doc_breakpoint-move.html25
-rw-r--r--devtools/client/debugger/test/mochitest/doc_breakpoints-break-on-last-line-of-script-on-reload.html8
-rw-r--r--devtools/client/debugger/test/mochitest/doc_breakpoints-other-tabs.html8
-rw-r--r--devtools/client/debugger/test/mochitest/doc_breakpoints-reload.html13
-rw-r--r--devtools/client/debugger/test/mochitest/doc_bug-896139.html18
-rw-r--r--devtools/client/debugger/test/mochitest/doc_closure-optimized-out.html34
-rw-r--r--devtools/client/debugger/test/mochitest/doc_closures.html32
-rw-r--r--devtools/client/debugger/test/mochitest/doc_cmd-break.html22
-rw-r--r--devtools/client/debugger/test/mochitest/doc_cmd-dbg.html40
-rw-r--r--devtools/client/debugger/test/mochitest/doc_conditional-breakpoints.html35
-rw-r--r--devtools/client/debugger/test/mochitest/doc_domnode-variables.html24
-rw-r--r--devtools/client/debugger/test/mochitest/doc_editor-mode.html20
-rw-r--r--devtools/client/debugger/test/mochitest/doc_empty-tab-01.html14
-rw-r--r--devtools/client/debugger/test/mochitest/doc_empty-tab-02.html14
-rw-r--r--devtools/client/debugger/test/mochitest/doc_event-listeners-01.html43
-rw-r--r--devtools/client/debugger/test/mochitest/doc_event-listeners-02.html53
-rw-r--r--devtools/client/debugger/test/mochitest/doc_event-listeners-03.html63
-rw-r--r--devtools/client/debugger/test/mochitest/doc_event-listeners-04.html23
-rw-r--r--devtools/client/debugger/test/mochitest/doc_frame-parameters.html37
-rw-r--r--devtools/client/debugger/test/mochitest/doc_function-display-name.html31
-rw-r--r--devtools/client/debugger/test/mochitest/doc_function-jump.html17
-rw-r--r--devtools/client/debugger/test/mochitest/doc_function-search.html30
-rw-r--r--devtools/client/debugger/test/mochitest/doc_global-method-override.html16
-rw-r--r--devtools/client/debugger/test/mochitest/doc_iframes.html15
-rw-r--r--devtools/client/debugger/test/mochitest/doc_included-script.html22
-rw-r--r--devtools/client/debugger/test/mochitest/doc_inline-debugger-statement.html21
-rw-r--r--devtools/client/debugger/test/mochitest/doc_inline-script.html25
-rw-r--r--devtools/client/debugger/test/mochitest/doc_large-array-buffer.html32
-rw-r--r--devtools/client/debugger/test/mochitest/doc_listworkers-tab.html8
-rw-r--r--devtools/client/debugger/test/mochitest/doc_map-set.html42
-rw-r--r--devtools/client/debugger/test/mochitest/doc_minified.html14
-rw-r--r--devtools/client/debugger/test/mochitest/doc_minified_bogus_map.html14
-rw-r--r--devtools/client/debugger/test/mochitest/doc_native-event-handler.html22
-rw-r--r--devtools/client/debugger/test/mochitest/doc_no-page-sources.html11
-rw-r--r--devtools/client/debugger/test/mochitest/doc_pause-exceptions.html35
-rw-r--r--devtools/client/debugger/test/mochitest/doc_pretty-print-2.html15
-rw-r--r--devtools/client/debugger/test/mochitest/doc_pretty-print-3.html8
-rw-r--r--devtools/client/debugger/test/mochitest/doc_pretty-print-on-paused.html14
-rw-r--r--devtools/client/debugger/test/mochitest/doc_pretty-print.html8
-rw-r--r--devtools/client/debugger/test/mochitest/doc_promise-get-allocation-stack.html24
-rw-r--r--devtools/client/debugger/test/mochitest/doc_promise-get-fulfillment-stack.html24
-rw-r--r--devtools/client/debugger/test/mochitest/doc_promise-get-rejection-stack.html24
-rw-r--r--devtools/client/debugger/test/mochitest/doc_promise.html30
-rw-r--r--devtools/client/debugger/test/mochitest/doc_proxy.html39
-rw-r--r--devtools/client/debugger/test/mochitest/doc_random-javascript.html15
-rw-r--r--devtools/client/debugger/test/mochitest/doc_recursion-stack.html35
-rw-r--r--devtools/client/debugger/test/mochitest/doc_scope-variable-2.html30
-rw-r--r--devtools/client/debugger/test/mochitest/doc_scope-variable-3.html23
-rw-r--r--devtools/client/debugger/test/mochitest/doc_scope-variable-4.html25
-rw-r--r--devtools/client/debugger/test/mochitest/doc_scope-variable.html25
-rw-r--r--devtools/client/debugger/test/mochitest/doc_script-bookmarklet.html14
-rw-r--r--devtools/client/debugger/test/mochitest/doc_script-eval.html16
-rw-r--r--devtools/client/debugger/test/mochitest/doc_script-switching-01.html18
-rw-r--r--devtools/client/debugger/test/mochitest/doc_script-switching-02.html18
-rw-r--r--devtools/client/debugger/test/mochitest/doc_script_webext_contentscript.html13
-rw-r--r--devtools/client/debugger/test/mochitest/doc_split-console-paused-reload.html22
-rw-r--r--devtools/client/debugger/test/mochitest/doc_step-many-statements.html50
-rw-r--r--devtools/client/debugger/test/mochitest/doc_step-out.html42
-rw-r--r--devtools/client/debugger/test/mochitest/doc_terminate-on-tab-close.html20
-rw-r--r--devtools/client/debugger/test/mochitest/doc_watch-expression-button.html31
-rw-r--r--devtools/client/debugger/test/mochitest/doc_watch-expressions.html29
-rw-r--r--devtools/client/debugger/test/mochitest/doc_whitespace-property-names.html29
-rw-r--r--devtools/client/debugger/test/mochitest/doc_with-frame.html29
-rw-r--r--devtools/client/debugger/test/mochitest/doc_worker-source-map.html18
-rw-r--r--devtools/client/debugger/test/mochitest/head.js1351
-rw-r--r--devtools/client/debugger/test/mochitest/sjs_post-page.sjs16
-rw-r--r--devtools/client/debugger/test/mochitest/sjs_random-javascript.sjs11
-rw-r--r--devtools/client/debugger/test/mochitest/testactors.js33
-rw-r--r--devtools/client/debugger/utils.js378
-rw-r--r--devtools/client/debugger/views/filter-view.js925
-rw-r--r--devtools/client/debugger/views/global-search-view.js756
-rw-r--r--devtools/client/debugger/views/options-view.js215
-rw-r--r--devtools/client/debugger/views/stack-frames-classic-view.js141
-rw-r--r--devtools/client/debugger/views/stack-frames-view.js283
-rw-r--r--devtools/client/debugger/views/toolbar-view.js287
-rw-r--r--devtools/client/debugger/views/variable-bubble-view.js321
-rw-r--r--devtools/client/debugger/views/watch-expressions-view.js303
-rw-r--r--devtools/client/debugger/views/workers-view.js55
-rw-r--r--devtools/client/definitions.js511
-rw-r--r--devtools/client/devtools-startup.js215
-rw-r--r--devtools/client/devtools-startup.manifest3
-rw-r--r--devtools/client/dom/.eslintrc.js17
-rw-r--r--devtools/client/dom/content/actions/filter.js21
-rw-r--r--devtools/client/dom/content/actions/grips.js54
-rw-r--r--devtools/client/dom/content/actions/moz.build9
-rw-r--r--devtools/client/dom/content/components/dom-tree.js91
-rw-r--r--devtools/client/dom/content/components/main-frame.js63
-rw-r--r--devtools/client/dom/content/components/main-toolbar.js66
-rw-r--r--devtools/client/dom/content/components/moz.build10
-rw-r--r--devtools/client/dom/content/constants.js9
-rw-r--r--devtools/client/dom/content/dom-decorator.js50
-rw-r--r--devtools/client/dom/content/dom-view.css118
-rw-r--r--devtools/client/dom/content/dom-view.js65
-rw-r--r--devtools/client/dom/content/grip-provider.js97
-rw-r--r--devtools/client/dom/content/moz.build19
-rw-r--r--devtools/client/dom/content/reducers/filter.js29
-rw-r--r--devtools/client/dom/content/reducers/grips.js123
-rw-r--r--devtools/client/dom/content/reducers/index.js14
-rw-r--r--devtools/client/dom/content/reducers/moz.build10
-rw-r--r--devtools/client/dom/content/utils.js27
-rw-r--r--devtools/client/dom/dom-panel.js241
-rw-r--r--devtools/client/dom/dom.html21
-rw-r--r--devtools/client/dom/main.js26
-rw-r--r--devtools/client/dom/moz.build14
-rw-r--r--devtools/client/dom/test/.eslintrc.js6
-rw-r--r--devtools/client/dom/test/browser.ini12
-rw-r--r--devtools/client/dom/test/browser_dom_array.js40
-rw-r--r--devtools/client/dom/test/browser_dom_basic.js24
-rw-r--r--devtools/client/dom/test/browser_dom_refresh.js25
-rw-r--r--devtools/client/dom/test/head.js239
-rw-r--r--devtools/client/dom/test/page_array.html19
-rw-r--r--devtools/client/dom/test/page_basic.html15
-rw-r--r--devtools/client/framework/ToolboxProcess.jsm291
-rw-r--r--devtools/client/framework/about-devtools-toolbox.js61
-rw-r--r--devtools/client/framework/attach-thread.js115
-rw-r--r--devtools/client/framework/browser-menus.js390
-rw-r--r--devtools/client/framework/connect/connect.css112
-rw-r--r--devtools/client/framework/connect/connect.js236
-rw-r--r--devtools/client/framework/connect/connect.xhtml52
-rw-r--r--devtools/client/framework/dev-edition-promo/dev-edition-logo.pngbin0 -> 6764 bytes
-rw-r--r--devtools/client/framework/dev-edition-promo/dev-edition-promo.css94
-rw-r--r--devtools/client/framework/dev-edition-promo/dev-edition-promo.xul36
-rw-r--r--devtools/client/framework/devtools-browser.js758
-rw-r--r--devtools/client/framework/devtools.js534
-rw-r--r--devtools/client/framework/gDevTools.jsm162
-rw-r--r--devtools/client/framework/location-store.js103
-rw-r--r--devtools/client/framework/menu-item.js65
-rw-r--r--devtools/client/framework/menu.js173
-rw-r--r--devtools/client/framework/moz.build33
-rw-r--r--devtools/client/framework/options-panel.css107
-rw-r--r--devtools/client/framework/selection.js247
-rw-r--r--devtools/client/framework/sidebar.js592
-rw-r--r--devtools/client/framework/source-map-service.js209
-rw-r--r--devtools/client/framework/source-map-util.js20
-rw-r--r--devtools/client/framework/source-map-worker.js220
-rw-r--r--devtools/client/framework/source-map.js84
-rw-r--r--devtools/client/framework/target-from-url.js120
-rw-r--r--devtools/client/framework/target.js825
-rw-r--r--devtools/client/framework/test/.eslintrc.js6
-rw-r--r--devtools/client/framework/test/browser.ini95
-rw-r--r--devtools/client/framework/test/browser_browser_toolbox.js65
-rw-r--r--devtools/client/framework/test/browser_browser_toolbox_debugger.js131
-rw-r--r--devtools/client/framework/test/browser_devtools_api.js264
-rw-r--r--devtools/client/framework/test/browser_devtools_api_destroy.js71
-rw-r--r--devtools/client/framework/test/browser_dynamic_tool_enabling.js41
-rw-r--r--devtools/client/framework/test/browser_ignore_toolbox_network_requests.js33
-rw-r--r--devtools/client/framework/test/browser_keybindings_01.js115
-rw-r--r--devtools/client/framework/test/browser_keybindings_02.js65
-rw-r--r--devtools/client/framework/test/browser_keybindings_03.js53
-rw-r--r--devtools/client/framework/test/browser_menu_api.js181
-rw-r--r--devtools/client/framework/test/browser_new_activation_workflow.js69
-rw-r--r--devtools/client/framework/test/browser_source_map-01.js115
-rw-r--r--devtools/client/framework/test/browser_source_map-02.js113
-rw-r--r--devtools/client/framework/test/browser_target_events.js56
-rw-r--r--devtools/client/framework/test/browser_target_from_url.js133
-rw-r--r--devtools/client/framework/test/browser_target_remote.js25
-rw-r--r--devtools/client/framework/test/browser_target_support.js74
-rw-r--r--devtools/client/framework/test/browser_toolbox_custom_host.js57
-rw-r--r--devtools/client/framework/test/browser_toolbox_dynamic_registration.js105
-rw-r--r--devtools/client/framework/test/browser_toolbox_getpanelwhenready.js36
-rw-r--r--devtools/client/framework/test/browser_toolbox_highlight.js81
-rw-r--r--devtools/client/framework/test/browser_toolbox_hosts.js139
-rw-r--r--devtools/client/framework/test/browser_toolbox_hosts_size.js69
-rw-r--r--devtools/client/framework/test/browser_toolbox_hosts_telemetry.js50
-rw-r--r--devtools/client/framework/test/browser_toolbox_keyboard_navigation.js81
-rw-r--r--devtools/client/framework/test/browser_toolbox_minimize.js106
-rw-r--r--devtools/client/framework/test/browser_toolbox_options.js297
-rw-r--r--devtools/client/framework/test/browser_toolbox_options_disable_buttons.js163
-rw-r--r--devtools/client/framework/test/browser_toolbox_options_disable_cache-01.js34
-rw-r--r--devtools/client/framework/test/browser_toolbox_options_disable_cache-02.js47
-rw-r--r--devtools/client/framework/test/browser_toolbox_options_disable_cache.sjs28
-rw-r--r--devtools/client/framework/test/browser_toolbox_options_disable_js.html46
-rw-r--r--devtools/client/framework/test/browser_toolbox_options_disable_js.js119
-rw-r--r--devtools/client/framework/test/browser_toolbox_options_disable_js_iframe.html33
-rw-r--r--devtools/client/framework/test/browser_toolbox_options_enable_serviceworkers_testing.html10
-rw-r--r--devtools/client/framework/test/browser_toolbox_options_enable_serviceworkers_testing.js126
-rw-r--r--devtools/client/framework/test/browser_toolbox_options_enable_serviceworkers_testing_frame_script.js46
-rw-r--r--devtools/client/framework/test/browser_toolbox_races.js81
-rw-r--r--devtools/client/framework/test/browser_toolbox_raise.js78
-rw-r--r--devtools/client/framework/test/browser_toolbox_ready.js21
-rw-r--r--devtools/client/framework/test/browser_toolbox_remoteness_change.js43
-rw-r--r--devtools/client/framework/test/browser_toolbox_select_event.js101
-rw-r--r--devtools/client/framework/test/browser_toolbox_selected_tool_unavailable.js48
-rw-r--r--devtools/client/framework/test/browser_toolbox_sidebar.js181
-rw-r--r--devtools/client/framework/test/browser_toolbox_sidebar_events.js93
-rw-r--r--devtools/client/framework/test/browser_toolbox_sidebar_existing_tabs.js78
-rw-r--r--devtools/client/framework/test/browser_toolbox_sidebar_overflow_menu.js80
-rw-r--r--devtools/client/framework/test/browser_toolbox_sidebar_tool.xul18
-rw-r--r--devtools/client/framework/test/browser_toolbox_split_console.js85
-rw-r--r--devtools/client/framework/test/browser_toolbox_tabsswitch_shortcuts.js68
-rw-r--r--devtools/client/framework/test/browser_toolbox_target.js60
-rw-r--r--devtools/client/framework/test/browser_toolbox_textbox_context_menu.js55
-rw-r--r--devtools/client/framework/test/browser_toolbox_theme_registration.js102
-rw-r--r--devtools/client/framework/test/browser_toolbox_toggle.js108
-rw-r--r--devtools/client/framework/test/browser_toolbox_tool_ready.js51
-rw-r--r--devtools/client/framework/test/browser_toolbox_tool_remote_reopen.js135
-rw-r--r--devtools/client/framework/test/browser_toolbox_transport_events.js108
-rw-r--r--devtools/client/framework/test/browser_toolbox_view_source_01.js46
-rw-r--r--devtools/client/framework/test/browser_toolbox_view_source_02.js54
-rw-r--r--devtools/client/framework/test/browser_toolbox_view_source_03.js40
-rw-r--r--devtools/client/framework/test/browser_toolbox_view_source_04.js39
-rw-r--r--devtools/client/framework/test/browser_toolbox_window_reload_target.js100
-rw-r--r--devtools/client/framework/test/browser_toolbox_window_shortcuts.js84
-rw-r--r--devtools/client/framework/test/browser_toolbox_window_title_changes.js108
-rw-r--r--devtools/client/framework/test/browser_toolbox_window_title_changes_page.html10
-rw-r--r--devtools/client/framework/test/browser_toolbox_window_title_frame_select.js94
-rw-r--r--devtools/client/framework/test/browser_toolbox_window_title_frame_select_page.html11
-rw-r--r--devtools/client/framework/test/browser_toolbox_zoom.js67
-rw-r--r--devtools/client/framework/test/browser_two_tabs.js149
-rw-r--r--devtools/client/framework/test/code_binary_search.coffee18
-rw-r--r--devtools/client/framework/test/code_binary_search.js29
-rw-r--r--devtools/client/framework/test/code_binary_search.map10
-rw-r--r--devtools/client/framework/test/code_math.js9
-rw-r--r--devtools/client/framework/test/code_ugly.js3
-rw-r--r--devtools/client/framework/test/doc_empty-tab-01.html14
-rw-r--r--devtools/client/framework/test/doc_theme.css3
-rw-r--r--devtools/client/framework/test/doc_viewsource.html13
-rw-r--r--devtools/client/framework/test/head.js148
-rw-r--r--devtools/client/framework/test/helper_disable_cache.js128
-rw-r--r--devtools/client/framework/test/serviceworker.js6
-rw-r--r--devtools/client/framework/test/shared-head.js596
-rw-r--r--devtools/client/framework/test/shared-redux-head.js85
-rw-r--r--devtools/client/framework/toolbox-highlighter-utils.js324
-rw-r--r--devtools/client/framework/toolbox-host-manager.js244
-rw-r--r--devtools/client/framework/toolbox-hosts.js425
-rw-r--r--devtools/client/framework/toolbox-init.js73
-rw-r--r--devtools/client/framework/toolbox-options.js431
-rw-r--r--devtools/client/framework/toolbox-options.xhtml201
-rw-r--r--devtools/client/framework/toolbox-process-window.js230
-rw-r--r--devtools/client/framework/toolbox-process-window.xul47
-rw-r--r--devtools/client/framework/toolbox-window.xul47
-rw-r--r--devtools/client/framework/toolbox.js2417
-rw-r--r--devtools/client/framework/toolbox.xul83
-rw-r--r--devtools/client/inspector/.eslintrc.js15
-rw-r--r--devtools/client/inspector/breadcrumbs.js921
-rw-r--r--devtools/client/inspector/components/box-model.js841
-rw-r--r--devtools/client/inspector/components/inspector-tab-panel.css15
-rw-r--r--devtools/client/inspector/components/inspector-tab-panel.js67
-rw-r--r--devtools/client/inspector/components/moz.build13
-rw-r--r--devtools/client/inspector/components/test/.eslintrc.js6
-rw-r--r--devtools/client/inspector/components/test/browser.ini29
-rw-r--r--devtools/client/inspector/components/test/browser_boxmodel.js168
-rw-r--r--devtools/client/inspector/components/test/browser_boxmodel_editablemodel.js194
-rw-r--r--devtools/client/inspector/components/test/browser_boxmodel_editablemodel_allproperties.js146
-rw-r--r--devtools/client/inspector/components/test/browser_boxmodel_editablemodel_bluronclick.js74
-rw-r--r--devtools/client/inspector/components/test/browser_boxmodel_editablemodel_border.js52
-rw-r--r--devtools/client/inspector/components/test/browser_boxmodel_editablemodel_stylerules.js113
-rw-r--r--devtools/client/inspector/components/test/browser_boxmodel_guides.js56
-rw-r--r--devtools/client/inspector/components/test/browser_boxmodel_rotate-labels-on-sides.js49
-rw-r--r--devtools/client/inspector/components/test/browser_boxmodel_sync.js44
-rw-r--r--devtools/client/inspector/components/test/browser_boxmodel_tooltips.js126
-rw-r--r--devtools/client/inspector/components/test/browser_boxmodel_update-after-navigation.js91
-rw-r--r--devtools/client/inspector/components/test/browser_boxmodel_update-after-reload.js40
-rw-r--r--devtools/client/inspector/components/test/browser_boxmodel_update-in-iframes.js101
-rw-r--r--devtools/client/inspector/components/test/doc_boxmodel_iframe1.html3
-rw-r--r--devtools/client/inspector/components/test/doc_boxmodel_iframe2.html3
-rw-r--r--devtools/client/inspector/components/test/head.js87
-rw-r--r--devtools/client/inspector/computed/computed.js1522
-rw-r--r--devtools/client/inspector/computed/moz.build11
-rw-r--r--devtools/client/inspector/computed/test/.eslintrc.js6
-rw-r--r--devtools/client/inspector/computed/test/browser.ini41
-rw-r--r--devtools/client/inspector/computed/test/browser_computed_browser-styles.js52
-rw-r--r--devtools/client/inspector/computed/test/browser_computed_cycle_color.js71
-rw-r--r--devtools/client/inspector/computed/test/browser_computed_getNodeInfo.js178
-rw-r--r--devtools/client/inspector/computed/test/browser_computed_keybindings_01.js83
-rw-r--r--devtools/client/inspector/computed/test/browser_computed_keybindings_02.js66
-rw-r--r--devtools/client/inspector/computed/test/browser_computed_matched-selectors-toggle.js104
-rw-r--r--devtools/client/inspector/computed/test/browser_computed_matched-selectors_01.js40
-rw-r--r--devtools/client/inspector/computed/test/browser_computed_matched-selectors_02.js41
-rw-r--r--devtools/client/inspector/computed/test/browser_computed_media-queries.js36
-rw-r--r--devtools/client/inspector/computed/test/browser_computed_no-results-placeholder.js70
-rw-r--r--devtools/client/inspector/computed/test/browser_computed_original-source-link.js73
-rw-r--r--devtools/client/inspector/computed/test/browser_computed_pseudo-element_01.js39
-rw-r--r--devtools/client/inspector/computed/test/browser_computed_refresh-on-style-change_01.js30
-rw-r--r--devtools/client/inspector/computed/test/browser_computed_search-filter.js66
-rw-r--r--devtools/client/inspector/computed/test/browser_computed_search-filter_clear.js71
-rw-r--r--devtools/client/inspector/computed/test/browser_computed_search-filter_context-menu.js84
-rw-r--r--devtools/client/inspector/computed/test/browser_computed_search-filter_escape-keypress.js75
-rw-r--r--devtools/client/inspector/computed/test/browser_computed_search-filter_noproperties.js61
-rw-r--r--devtools/client/inspector/computed/test/browser_computed_select-and-copy-styles.js118
-rw-r--r--devtools/client/inspector/computed/test/browser_computed_style-editor-link.js142
-rw-r--r--devtools/client/inspector/computed/test/doc_matched_selectors.html28
-rw-r--r--devtools/client/inspector/computed/test/doc_media_queries.html21
-rw-r--r--devtools/client/inspector/computed/test/doc_pseudoelement.html131
-rw-r--r--devtools/client/inspector/computed/test/doc_sourcemaps.css7
-rw-r--r--devtools/client/inspector/computed/test/doc_sourcemaps.css.map7
-rw-r--r--devtools/client/inspector/computed/test/doc_sourcemaps.html11
-rw-r--r--devtools/client/inspector/computed/test/doc_sourcemaps.scss10
-rw-r--r--devtools/client/inspector/computed/test/head.js157
-rw-r--r--devtools/client/inspector/fonts/fonts.js250
-rw-r--r--devtools/client/inspector/fonts/moz.build11
-rw-r--r--devtools/client/inspector/fonts/test/.eslintrc.js6
-rw-r--r--devtools/client/inspector/fonts/test/OstrichLicense.txt41
-rw-r--r--devtools/client/inspector/fonts/test/browser.ini20
-rw-r--r--devtools/client/inspector/fonts/test/browser_fontinspector.html52
-rw-r--r--devtools/client/inspector/fonts/test/browser_fontinspector.js108
-rw-r--r--devtools/client/inspector/fonts/test/browser_fontinspector_edit-previews-show-all.js44
-rw-r--r--devtools/client/inspector/fonts/test/browser_fontinspector_edit-previews.js60
-rw-r--r--devtools/client/inspector/fonts/test/browser_fontinspector_theme-change.js55
-rw-r--r--devtools/client/inspector/fonts/test/head.js86
-rwxr-xr-xdevtools/client/inspector/fonts/test/ostrich-black.ttfbin0 -> 12872 bytes
-rwxr-xr-xdevtools/client/inspector/fonts/test/ostrich-regular.ttfbin0 -> 12476 bytes
-rw-r--r--devtools/client/inspector/fonts/test/test_iframe.html11
-rw-r--r--devtools/client/inspector/inspector-commands.js114
-rw-r--r--devtools/client/inspector/inspector-search.js549
-rw-r--r--devtools/client/inspector/inspector.js1936
-rw-r--r--devtools/client/inspector/inspector.xhtml231
-rw-r--r--devtools/client/inspector/layout/actions/index.js5
-rw-r--r--devtools/client/inspector/layout/actions/moz.build5
-rw-r--r--devtools/client/inspector/layout/components/Accordion.css42
-rw-r--r--devtools/client/inspector/layout/components/Accordion.js82
-rw-r--r--devtools/client/inspector/layout/components/App.js35
-rw-r--r--devtools/client/inspector/layout/components/Grid.js30
-rw-r--r--devtools/client/inspector/layout/components/moz.build12
-rw-r--r--devtools/client/inspector/layout/layout.js55
-rw-r--r--devtools/client/inspector/layout/moz.build18
-rw-r--r--devtools/client/inspector/layout/reducers/grids.js21
-rw-r--r--devtools/client/inspector/layout/reducers/index.js7
-rw-r--r--devtools/client/inspector/layout/reducers/moz.build10
-rw-r--r--devtools/client/inspector/layout/store.js33
-rw-r--r--devtools/client/inspector/layout/types.js5
-rw-r--r--devtools/client/inspector/layout/utils/l10n.js15
-rw-r--r--devtools/client/inspector/layout/utils/moz.build9
-rw-r--r--devtools/client/inspector/markup/markup.js1878
-rw-r--r--devtools/client/inspector/markup/markup.xhtml105
-rw-r--r--devtools/client/inspector/markup/moz.build16
-rw-r--r--devtools/client/inspector/markup/test/.eslintrc.js6
-rw-r--r--devtools/client/inspector/markup/test/actor_events_form.js62
-rw-r--r--devtools/client/inspector/markup/test/browser.ini155
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_accessibility_focus_blur.js59
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_accessibility_navigation.js277
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_accessibility_navigation_after_edit.js126
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_accessibility_semantics.js100
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_anonymous_01.js44
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_anonymous_02.js31
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_anonymous_03.js34
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_anonymous_04.js37
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_copy_image_data.js67
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_css_completion_style_attribute_01.js76
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_css_completion_style_attribute_02.js106
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_css_completion_style_attribute_03.js54
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_dragdrop_autoscroll_01.js51
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_dragdrop_autoscroll_02.js49
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_dragdrop_distance.js49
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_dragdrop_dragRootNode.js22
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_dragdrop_draggable.js63
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_dragdrop_escapeKeyPress.js34
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_dragdrop_invalidNodes.js48
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_dragdrop_reorder.js109
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_dragdrop_tooltip.js35
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_events-overflow.js91
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_events-windowed-host.js61
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_events1.js149
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_events2.js163
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_events3.js161
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_events_form.js61
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_events_jquery_1.0.js237
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_events_jquery_1.1.js271
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_events_jquery_1.11.1.js196
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_events_jquery_1.2.js191
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_events_jquery_1.3.js224
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_events_jquery_1.4.js287
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_events_jquery_1.6.js388
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_events_jquery_1.7.js234
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_events_jquery_2.1.1.js196
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_html_edit_01.js84
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_html_edit_02.js119
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_html_edit_03.js200
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_image_tooltip.js60
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_image_tooltip_mutations.js83
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_keybindings_01.js49
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_keybindings_02.js32
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_keybindings_03.js50
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_keybindings_04.js58
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_keybindings_delete_attributes.js63
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_keybindings_scrolltonode.js87
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_links_01.js128
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_links_02.js38
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_links_03.js38
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_links_04.js116
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_links_05.js69
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_links_06.js53
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_links_07.js109
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_load_01.js71
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_mutation_01.js340
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_mutation_02.js159
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_navigation.js147
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_node_names.js28
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_node_names_namespaced.js43
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_node_not_displayed_01.js35
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_node_not_displayed_02.js150
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_pagesize_01.js86
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_pagesize_02.js47
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_remove_xul_attributes.js28
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_search_01.js51
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_tag_edit_01.js68
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_tag_edit_02.js44
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_tag_edit_03.js51
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_tag_edit_04-backspace.js59
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_tag_edit_04-delete.js59
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_tag_edit_05.js77
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_tag_edit_06.js85
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_tag_edit_07.js135
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_tag_edit_08.js132
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_tag_edit_09.js71
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_tag_edit_10.js34
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_tag_edit_11.js38
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_tag_edit_12.js98
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_tag_edit_13-other.js38
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_tag_edit_long-classname.js41
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_textcontent_display.js89
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_textcontent_edit_01.js84
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_textcontent_edit_02.js116
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_toggle_01.js58
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_toggle_02.js49
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_toggle_03.js35
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_update-on-navigtion.js44
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_void_elements_html.js44
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_void_elements_xhtml.js28
-rw-r--r--devtools/client/inspector/markup/test/browser_markup_whitespace.js66
-rw-r--r--devtools/client/inspector/markup/test/doc_markup_anonymous.html34
-rw-r--r--devtools/client/inspector/markup/test/doc_markup_dragdrop.html23
-rw-r--r--devtools/client/inspector/markup/test/doc_markup_dragdrop_autoscroll_01.html87
-rw-r--r--devtools/client/inspector/markup/test/doc_markup_dragdrop_autoscroll_02.html40
-rw-r--r--devtools/client/inspector/markup/test/doc_markup_edit.html48
-rw-r--r--devtools/client/inspector/markup/test/doc_markup_events-overflow.html19
-rw-r--r--devtools/client/inspector/markup/test/doc_markup_events1.html113
-rw-r--r--devtools/client/inspector/markup/test/doc_markup_events2.html111
-rw-r--r--devtools/client/inspector/markup/test/doc_markup_events3.html115
-rw-r--r--devtools/client/inspector/markup/test/doc_markup_events_form.html11
-rw-r--r--devtools/client/inspector/markup/test/doc_markup_events_jquery.html67
-rw-r--r--devtools/client/inspector/markup/test/doc_markup_flashing.html15
-rw-r--r--devtools/client/inspector/markup/test/doc_markup_html_mixed_case.html12
-rw-r--r--devtools/client/inspector/markup/test/doc_markup_image_and_canvas.html24
-rw-r--r--devtools/client/inspector/markup/test/doc_markup_image_and_canvas_2.html25
-rw-r--r--devtools/client/inspector/markup/test/doc_markup_links.html42
-rw-r--r--devtools/client/inspector/markup/test/doc_markup_mutation.html42
-rw-r--r--devtools/client/inspector/markup/test/doc_markup_navigation.html28
-rw-r--r--devtools/client/inspector/markup/test/doc_markup_not_displayed.html18
-rw-r--r--devtools/client/inspector/markup/test/doc_markup_pagesize_01.html32
-rw-r--r--devtools/client/inspector/markup/test/doc_markup_pagesize_02.html33
-rw-r--r--devtools/client/inspector/markup/test/doc_markup_search.html11
-rw-r--r--devtools/client/inspector/markup/test/doc_markup_svg_attributes.html8
-rw-r--r--devtools/client/inspector/markup/test/doc_markup_toggle.html28
-rw-r--r--devtools/client/inspector/markup/test/doc_markup_tooltip.pngbin0 -> 1095 bytes
-rw-r--r--devtools/client/inspector/markup/test/doc_markup_void_elements.html18
-rw-r--r--devtools/client/inspector/markup/test/doc_markup_void_elements.xhtml21
-rw-r--r--devtools/client/inspector/markup/test/doc_markup_whitespace.html25
-rw-r--r--devtools/client/inspector/markup/test/doc_markup_xul.xul9
-rw-r--r--devtools/client/inspector/markup/test/head.js653
-rw-r--r--devtools/client/inspector/markup/test/helper_attributes_test_runner.js160
-rw-r--r--devtools/client/inspector/markup/test/helper_events_test_runner.js111
-rw-r--r--devtools/client/inspector/markup/test/helper_markup_accessibility_navigation.js70
-rw-r--r--devtools/client/inspector/markup/test/helper_outerhtml_test_runner.js82
-rw-r--r--devtools/client/inspector/markup/test/helper_style_attr_test_runner.js132
-rw-r--r--devtools/client/inspector/markup/test/lib_jquery_1.0.js1814
-rw-r--r--devtools/client/inspector/markup/test/lib_jquery_1.1.js2172
-rw-r--r--devtools/client/inspector/markup/test/lib_jquery_1.11.1_min.js4
-rw-r--r--devtools/client/inspector/markup/test/lib_jquery_1.2_min.js32
-rw-r--r--devtools/client/inspector/markup/test/lib_jquery_1.3_min.js19
-rw-r--r--devtools/client/inspector/markup/test/lib_jquery_1.4_min.js151
-rw-r--r--devtools/client/inspector/markup/test/lib_jquery_1.6_min.js16
-rw-r--r--devtools/client/inspector/markup/test/lib_jquery_1.7_min.js4
-rw-r--r--devtools/client/inspector/markup/test/lib_jquery_2.1.1_min.js4
-rw-r--r--devtools/client/inspector/markup/utils.js135
-rw-r--r--devtools/client/inspector/markup/views/element-container.js193
-rw-r--r--devtools/client/inspector/markup/views/element-editor.js560
-rw-r--r--devtools/client/inspector/markup/views/html-editor.js180
-rw-r--r--devtools/client/inspector/markup/views/markup-container.js720
-rw-r--r--devtools/client/inspector/markup/views/moz.build17
-rw-r--r--devtools/client/inspector/markup/views/read-only-container.js33
-rw-r--r--devtools/client/inspector/markup/views/read-only-editor.js43
-rw-r--r--devtools/client/inspector/markup/views/root-container.js55
-rw-r--r--devtools/client/inspector/markup/views/text-container.js40
-rw-r--r--devtools/client/inspector/markup/views/text-editor.js109
-rw-r--r--devtools/client/inspector/moz.build23
-rw-r--r--devtools/client/inspector/panel.js19
-rw-r--r--devtools/client/inspector/rules/models/element-style.js412
-rw-r--r--devtools/client/inspector/rules/models/moz.build11
-rw-r--r--devtools/client/inspector/rules/models/rule.js686
-rw-r--r--devtools/client/inspector/rules/models/text-property.js215
-rw-r--r--devtools/client/inspector/rules/moz.build16
-rw-r--r--devtools/client/inspector/rules/rules.js1673
-rw-r--r--devtools/client/inspector/rules/test/.eslintrc.js6
-rw-r--r--devtools/client/inspector/rules/test/browser.ini221
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_add-property-and-reselect.js44
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_add-property-cancel_01.js44
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_add-property-cancel_02.js34
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_add-property-cancel_03.js43
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_add-property-commented.js47
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_add-property-svg.js22
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_add-property_01.js32
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_add-property_02.js65
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_add-rule-and-property.js30
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_add-rule-button-state.js51
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_add-rule-edit-selector.js55
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_add-rule-iframes.js57
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_add-rule-namespace-elements.js41
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_add-rule-pseudo-class.js82
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_add-rule-then-property-edit-selector.js80
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_add-rule-with-menu.js42
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_add-rule.js47
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_authored.js49
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_authored_color.js67
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_authored_override.js53
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_blob_stylesheet.js20
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_colorUnit.js65
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_colorpicker-and-image-tooltip_01.js63
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_colorpicker-and-image-tooltip_02.js66
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_colorpicker-appears-on-swatch-click.js51
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_colorpicker-commit-on-ENTER.js61
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_colorpicker-edit-gradient.js77
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_colorpicker-hides-on-tooltip.js46
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_colorpicker-multiple-changes.js124
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_colorpicker-release-outside-frame.js67
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_colorpicker-revert-on-ESC.js109
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_colorpicker-swatch-displayed.js73
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_completion-existing-property_01.js139
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_completion-existing-property_02.js123
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_completion-new-property_01.js102
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_completion-new-property_02.js129
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_completion-new-property_03.js47
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_completion-new-property_04.js73
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_completion-new-property_multiline.js131
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_completion-popup-hidden-after-navigation.js41
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_computed-lists_01.js47
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_computed-lists_02.js74
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_content_01.js51
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_content_02.js60
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_context-menu-show-mdn-docs-01.js96
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_context-menu-show-mdn-docs-02.js61
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_context-menu-show-mdn-docs-03.js118
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_copy_styles.js307
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_css-docs-tooltip_closes-on-escape.js51
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_cssom.js22
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_cubicbezier-appears-on-swatch-click.js70
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_cubicbezier-commit-on-ENTER.js66
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_cubicbezier-revert-on-ESC.js100
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_custom.js72
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_cycle-angle.js93
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_cycle-color.js120
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-display-grid-property.js49
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-property-cancel.js46
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-property-click.js61
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-property-commit.js92
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-property-computed.js89
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-property-increments.js280
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-property-order.js89
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-property-remove_01.js67
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-property-remove_02.js67
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-property-remove_03.js83
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-property_01.js93
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-property_02.js133
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-property_03.js50
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-property_04.js85
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-property_05.js77
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-property_06.js52
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-property_07.js50
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-property_08.js57
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-property_09.js69
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-selector-click-on-scrollbar.js88
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-selector-click.js63
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-selector-commit.js117
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-selector_01.js62
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-selector_02.js88
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-selector_03.js48
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-selector_04.js69
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-selector_05.js78
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-selector_06.js76
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-selector_07.js62
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-selector_08.js71
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-selector_09.js110
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-selector_10.js64
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-selector_11.js69
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_01.js107
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_02.js65
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_03.js69
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_04.js62
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_editable-field-focus_01.js94
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_editable-field-focus_02.js84
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_eyedropper.js123
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_filtereditor-appears-on-swatch-click.js34
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_filtereditor-commit-on-ENTER.js45
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_filtereditor-revert-on-ESC.js118
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_grid-highlighter-on-navigate.js41
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_grid-highlighter-on-reload.js53
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_grid-toggle_01.js64
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_grid-toggle_02.js73
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_grid-toggle_03.js96
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_guessIndentation.js47
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_inherited-properties_01.js47
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_inherited-properties_02.js34
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_inherited-properties_03.js40
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_inline-source-map.js26
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_invalid-source-map.js44
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_invalid.js33
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_keybindings.js49
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_keyframeLineNumbers.js25
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_keyframes-rule_01.js106
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_keyframes-rule_02.js92
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_lineNumbers.js29
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_livepreview.js72
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_mark_overridden_01.js56
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_mark_overridden_02.js45
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_mark_overridden_03.js41
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_mark_overridden_04.js36
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_mark_overridden_05.js33
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_mark_overridden_06.js60
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_mark_overridden_07.js72
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_mathml-element.js53
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_media-queries.js26
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_multiple-properties-duplicates.js68
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_multiple-properties-priority.js47
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_multiple-properties-unfinished_01.js62
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_multiple-properties-unfinished_02.js71
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_multiple_properties_01.js53
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_multiple_properties_02.js54
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_original-source-link.js85
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_pseudo-element_01.js260
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_pseudo-element_02.js29
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_pseudo_lock_options.js131
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_refresh-no-flicker.js39
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_refresh-on-attribute-change_01.js61
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_refresh-on-attribute-change_02.js153
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_refresh-on-style-change.js38
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_01.js156
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_02.js93
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_03.js49
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_04.js63
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_expander.js92
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_search-filter-overridden-property.js74
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_search-filter_01.js91
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_search-filter_02.js32
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_search-filter_03.js39
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_search-filter_04.js76
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_search-filter_05.js33
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_search-filter_06.js27
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_search-filter_07.js62
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_search-filter_08.js53
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_search-filter_09.js73
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_search-filter_10.js84
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_search-filter_context-menu.js83
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_search-filter_escape-keypress.js65
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_select-and-copy-styles.js171
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_selector-highlighter-on-navigate.js38
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_selector-highlighter_01.js35
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_selector-highlighter_02.js78
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_selector-highlighter_03.js78
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_selector-highlighter_04.js53
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_selector_highlight.js144
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_strict-search-filter-computed-list_01.js182
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_strict-search-filter_01.js130
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_strict-search-filter_02.js34
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_strict-search-filter_03.js44
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_style-editor-link.js203
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_urls-clickable.js70
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_user-agent-styles-uneditable.js58
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_user-agent-styles.js183
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_user-property-reset.js90
-rw-r--r--devtools/client/inspector/rules/test/doc_author-sheet.html39
-rw-r--r--devtools/client/inspector/rules/test/doc_blob_stylesheet.html39
-rw-r--r--devtools/client/inspector/rules/test/doc_content_stylesheet.html35
-rw-r--r--devtools/client/inspector/rules/test/doc_content_stylesheet_imported.css5
-rw-r--r--devtools/client/inspector/rules/test/doc_content_stylesheet_imported2.css3
-rw-r--r--devtools/client/inspector/rules/test/doc_content_stylesheet_linked.css3
-rw-r--r--devtools/client/inspector/rules/test/doc_content_stylesheet_script.css5
-rw-r--r--devtools/client/inspector/rules/test/doc_copystyles.css11
-rw-r--r--devtools/client/inspector/rules/test/doc_copystyles.html11
-rw-r--r--devtools/client/inspector/rules/test/doc_cssom.html22
-rw-r--r--devtools/client/inspector/rules/test/doc_custom.html33
-rw-r--r--devtools/client/inspector/rules/test/doc_filter.html13
-rw-r--r--devtools/client/inspector/rules/test/doc_frame_script.js113
-rw-r--r--devtools/client/inspector/rules/test/doc_inline_sourcemap.html18
-rw-r--r--devtools/client/inspector/rules/test/doc_invalid_sourcemap.css3
-rw-r--r--devtools/client/inspector/rules/test/doc_invalid_sourcemap.html11
-rw-r--r--devtools/client/inspector/rules/test/doc_keyframeLineNumbers.html45
-rw-r--r--devtools/client/inspector/rules/test/doc_keyframeanimation.css84
-rw-r--r--devtools/client/inspector/rules/test/doc_keyframeanimation.html13
-rw-r--r--devtools/client/inspector/rules/test/doc_media_queries.html24
-rw-r--r--devtools/client/inspector/rules/test/doc_pseudoelement.html131
-rw-r--r--devtools/client/inspector/rules/test/doc_ruleLineNumbers.html19
-rw-r--r--devtools/client/inspector/rules/test/doc_sourcemaps.css7
-rw-r--r--devtools/client/inspector/rules/test/doc_sourcemaps.css.map7
-rw-r--r--devtools/client/inspector/rules/test/doc_sourcemaps.html11
-rw-r--r--devtools/client/inspector/rules/test/doc_sourcemaps.scss10
-rw-r--r--devtools/client/inspector/rules/test/doc_style_editor_link.css3
-rw-r--r--devtools/client/inspector/rules/test/doc_test_image.pngbin0 -> 580 bytes
-rw-r--r--devtools/client/inspector/rules/test/doc_urls_clickable.css9
-rw-r--r--devtools/client/inspector/rules/test/doc_urls_clickable.html30
-rw-r--r--devtools/client/inspector/rules/test/head.js840
-rw-r--r--devtools/client/inspector/rules/views/moz.build8
-rw-r--r--devtools/client/inspector/rules/views/rule-editor.js620
-rw-r--r--devtools/client/inspector/rules/views/text-property-editor.js880
-rw-r--r--devtools/client/inspector/shared/dom-node-preview.js352
-rw-r--r--devtools/client/inspector/shared/highlighters-overlay.js315
-rw-r--r--devtools/client/inspector/shared/moz.build16
-rw-r--r--devtools/client/inspector/shared/node-types.js17
-rw-r--r--devtools/client/inspector/shared/style-inspector-menu.js510
-rw-r--r--devtools/client/inspector/shared/test/.eslintrc.js6
-rw-r--r--devtools/client/inspector/shared/test/browser.ini41
-rw-r--r--devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-color_01.js118
-rw-r--r--devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-color_02.js99
-rw-r--r--devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-urls.js109
-rw-r--r--devtools/client/inspector/shared/test/browser_styleinspector_csslogic-content-stylesheets.js82
-rw-r--r--devtools/client/inspector/shared/test/browser_styleinspector_output-parser.js341
-rw-r--r--devtools/client/inspector/shared/test/browser_styleinspector_refresh_when_active.js43
-rw-r--r--devtools/client/inspector/shared/test/browser_styleinspector_tooltip-background-image.js125
-rw-r--r--devtools/client/inspector/shared/test/browser_styleinspector_tooltip-closes-on-new-selection.js73
-rw-r--r--devtools/client/inspector/shared/test/browser_styleinspector_tooltip-longhand-fontfamily.js120
-rw-r--r--devtools/client/inspector/shared/test/browser_styleinspector_tooltip-multiple-background-images.js63
-rw-r--r--devtools/client/inspector/shared/test/browser_styleinspector_tooltip-shorthand-fontfamily.js58
-rw-r--r--devtools/client/inspector/shared/test/browser_styleinspector_tooltip-size.js86
-rw-r--r--devtools/client/inspector/shared/test/browser_styleinspector_transform-highlighter-01.js48
-rw-r--r--devtools/client/inspector/shared/test/browser_styleinspector_transform-highlighter-02.js57
-rw-r--r--devtools/client/inspector/shared/test/browser_styleinspector_transform-highlighter-03.js103
-rw-r--r--devtools/client/inspector/shared/test/browser_styleinspector_transform-highlighter-04.js60
-rw-r--r--devtools/client/inspector/shared/test/doc_author-sheet.html37
-rw-r--r--devtools/client/inspector/shared/test/doc_content_stylesheet.html32
-rw-r--r--devtools/client/inspector/shared/test/doc_content_stylesheet.xul9
-rw-r--r--devtools/client/inspector/shared/test/doc_content_stylesheet_imported.css5
-rw-r--r--devtools/client/inspector/shared/test/doc_content_stylesheet_imported2.css3
-rw-r--r--devtools/client/inspector/shared/test/doc_content_stylesheet_linked.css3
-rw-r--r--devtools/client/inspector/shared/test/doc_content_stylesheet_script.css5
-rw-r--r--devtools/client/inspector/shared/test/doc_content_stylesheet_xul.css3
-rw-r--r--devtools/client/inspector/shared/test/doc_frame_script.js115
-rw-r--r--devtools/client/inspector/shared/test/head.js557
-rw-r--r--devtools/client/inspector/shared/tooltips-overlay.js319
-rw-r--r--devtools/client/inspector/shared/utils.js161
-rw-r--r--devtools/client/inspector/test/.eslintrc.js6
-rw-r--r--devtools/client/inspector/test/browser.ini172
-rw-r--r--devtools/client/inspector/test/browser_inspector_addNode_01.js22
-rw-r--r--devtools/client/inspector/test/browser_inspector_addNode_02.js63
-rw-r--r--devtools/client/inspector/test/browser_inspector_addNode_03.js84
-rw-r--r--devtools/client/inspector/test/browser_inspector_addSidebarTab.js62
-rw-r--r--devtools/client/inspector/test/browser_inspector_breadcrumbs.js132
-rw-r--r--devtools/client/inspector/test/browser_inspector_breadcrumbs_highlight_hover.js47
-rw-r--r--devtools/client/inspector/test/browser_inspector_breadcrumbs_keybinding.js71
-rw-r--r--devtools/client/inspector/test/browser_inspector_breadcrumbs_keyboard_trap.js83
-rw-r--r--devtools/client/inspector/test/browser_inspector_breadcrumbs_mutations.js212
-rw-r--r--devtools/client/inspector/test/browser_inspector_breadcrumbs_namespaced.js55
-rw-r--r--devtools/client/inspector/test/browser_inspector_breadcrumbs_visibility.js110
-rw-r--r--devtools/client/inspector/test/browser_inspector_delete-selected-node-01.js24
-rw-r--r--devtools/client/inspector/test/browser_inspector_delete-selected-node-02.js154
-rw-r--r--devtools/client/inspector/test/browser_inspector_delete-selected-node-03.js27
-rw-r--r--devtools/client/inspector/test/browser_inspector_destroy-after-navigation.js24
-rw-r--r--devtools/client/inspector/test/browser_inspector_destroy-before-ready.js26
-rw-r--r--devtools/client/inspector/test/browser_inspector_expand-collapse.js64
-rw-r--r--devtools/client/inspector/test/browser_inspector_gcli-inspect-command.js118
-rw-r--r--devtools/client/inspector/test/browser_inspector_highlighter-01.js31
-rw-r--r--devtools/client/inspector/test/browser_inspector_highlighter-02.js39
-rw-r--r--devtools/client/inspector/test/browser_inspector_highlighter-03.js70
-rw-r--r--devtools/client/inspector/test/browser_inspector_highlighter-04.js43
-rw-r--r--devtools/client/inspector/test/browser_inspector_highlighter-by-type.js66
-rw-r--r--devtools/client/inspector/test/browser_inspector_highlighter-cancel.js52
-rw-r--r--devtools/client/inspector/test/browser_inspector_highlighter-comments.js105
-rw-r--r--devtools/client/inspector/test/browser_inspector_highlighter-cssgrid_01.js77
-rw-r--r--devtools/client/inspector/test/browser_inspector_highlighter-csstransform_01.js152
-rw-r--r--devtools/client/inspector/test/browser_inspector_highlighter-csstransform_02.js56
-rw-r--r--devtools/client/inspector/test/browser_inspector_highlighter-embed.js30
-rw-r--r--devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-clipboard.js39
-rw-r--r--devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-csp.js30
-rw-r--r--devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-events.js141
-rw-r--r--devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-label.js115
-rw-r--r--devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-show-hide.js42
-rw-r--r--devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-xul.js64
-rw-r--r--devtools/client/inspector/test/browser_inspector_highlighter-geometry_01.js89
-rw-r--r--devtools/client/inspector/test/browser_inspector_highlighter-geometry_02.js116
-rw-r--r--devtools/client/inspector/test/browser_inspector_highlighter-geometry_03.js61
-rw-r--r--devtools/client/inspector/test/browser_inspector_highlighter-geometry_04.js85
-rw-r--r--devtools/client/inspector/test/browser_inspector_highlighter-geometry_05.js119
-rw-r--r--devtools/client/inspector/test/browser_inspector_highlighter-geometry_06.js166
-rw-r--r--devtools/client/inspector/test/browser_inspector_highlighter-hover_01.js41
-rw-r--r--devtools/client/inspector/test/browser_inspector_highlighter-hover_02.js38
-rw-r--r--devtools/client/inspector/test/browser_inspector_highlighter-hover_03.js55
-rw-r--r--devtools/client/inspector/test/browser_inspector_highlighter-iframes_01.js64
-rw-r--r--devtools/client/inspector/test/browser_inspector_highlighter-iframes_02.js59
-rw-r--r--devtools/client/inspector/test/browser_inspector_highlighter-inline.js76
-rw-r--r--devtools/client/inspector/test/browser_inspector_highlighter-keybinding_01.js64
-rw-r--r--devtools/client/inspector/test/browser_inspector_highlighter-keybinding_02.js64
-rw-r--r--devtools/client/inspector/test/browser_inspector_highlighter-keybinding_03.js71
-rw-r--r--devtools/client/inspector/test/browser_inspector_highlighter-keybinding_04.js46
-rw-r--r--devtools/client/inspector/test/browser_inspector_highlighter-measure_01.js88
-rw-r--r--devtools/client/inspector/test/browser_inspector_highlighter-measure_02.js130
-rw-r--r--devtools/client/inspector/test/browser_inspector_highlighter-options.js204
-rw-r--r--devtools/client/inspector/test/browser_inspector_highlighter-preview.js56
-rw-r--r--devtools/client/inspector/test/browser_inspector_highlighter-rect_01.js121
-rw-r--r--devtools/client/inspector/test/browser_inspector_highlighter-rect_02.js37
-rw-r--r--devtools/client/inspector/test/browser_inspector_highlighter-rulers_01.js76
-rw-r--r--devtools/client/inspector/test/browser_inspector_highlighter-rulers_02.js103
-rw-r--r--devtools/client/inspector/test/browser_inspector_highlighter-selector_01.js63
-rw-r--r--devtools/client/inspector/test/browser_inspector_highlighter-selector_02.js61
-rw-r--r--devtools/client/inspector/test/browser_inspector_highlighter-xbl.js39
-rw-r--r--devtools/client/inspector/test/browser_inspector_highlighter-zoom.js72
-rw-r--r--devtools/client/inspector/test/browser_inspector_iframe-navigation.js43
-rw-r--r--devtools/client/inspector/test/browser_inspector_infobar_01.js89
-rw-r--r--devtools/client/inspector/test/browser_inspector_infobar_02.js50
-rw-r--r--devtools/client/inspector/test/browser_inspector_infobar_03.js41
-rw-r--r--devtools/client/inspector/test/browser_inspector_infobar_textnode.js46
-rw-r--r--devtools/client/inspector/test/browser_inspector_initialization.js112
-rw-r--r--devtools/client/inspector/test/browser_inspector_inspect-object-element.js18
-rw-r--r--devtools/client/inspector/test/browser_inspector_invalidate.js35
-rw-r--r--devtools/client/inspector/test/browser_inspector_keyboard-shortcuts-copy-outerhtml.js52
-rw-r--r--devtools/client/inspector/test/browser_inspector_keyboard-shortcuts.js48
-rw-r--r--devtools/client/inspector/test/browser_inspector_menu-01-sensitivity.js278
-rw-r--r--devtools/client/inspector/test/browser_inspector_menu-02-copy-items.js49
-rw-r--r--devtools/client/inspector/test/browser_inspector_menu-03-paste-items-svg.js42
-rw-r--r--devtools/client/inspector/test/browser_inspector_menu-03-paste-items.js128
-rw-r--r--devtools/client/inspector/test/browser_inspector_menu-04-use-in-console.js61
-rw-r--r--devtools/client/inspector/test/browser_inspector_menu-05-attribute-items.js79
-rw-r--r--devtools/client/inspector/test/browser_inspector_menu-06-other.js95
-rw-r--r--devtools/client/inspector/test/browser_inspector_navigate_to_errors.js50
-rw-r--r--devtools/client/inspector/test/browser_inspector_navigation.js43
-rw-r--r--devtools/client/inspector/test/browser_inspector_open_on_neterror.js37
-rw-r--r--devtools/client/inspector/test/browser_inspector_pane-toggle-01.js27
-rw-r--r--devtools/client/inspector/test/browser_inspector_pane-toggle-02.js43
-rw-r--r--devtools/client/inspector/test/browser_inspector_pane-toggle-03.js38
-rw-r--r--devtools/client/inspector/test/browser_inspector_pane-toggle-05.js33
-rw-r--r--devtools/client/inspector/test/browser_inspector_picker-stop-on-destroy.js30
-rw-r--r--devtools/client/inspector/test/browser_inspector_picker-stop-on-tool-change.js27
-rw-r--r--devtools/client/inspector/test/browser_inspector_portrait_mode.js78
-rw-r--r--devtools/client/inspector/test/browser_inspector_pseudoclass-lock.js160
-rw-r--r--devtools/client/inspector/test/browser_inspector_pseudoclass-menu.js46
-rw-r--r--devtools/client/inspector/test/browser_inspector_reload-01.js32
-rw-r--r--devtools/client/inspector/test/browser_inspector_reload-02.js48
-rw-r--r--devtools/client/inspector/test/browser_inspector_remove-iframe-during-load.js48
-rw-r--r--devtools/client/inspector/test/browser_inspector_search-01.js96
-rw-r--r--devtools/client/inspector/test/browser_inspector_search-02.js169
-rw-r--r--devtools/client/inspector/test/browser_inspector_search-03.js250
-rw-r--r--devtools/client/inspector/test/browser_inspector_search-04.js112
-rw-r--r--devtools/client/inspector/test/browser_inspector_search-05.js93
-rw-r--r--devtools/client/inspector/test/browser_inspector_search-06.js87
-rw-r--r--devtools/client/inspector/test/browser_inspector_search-07.js49
-rw-r--r--devtools/client/inspector/test/browser_inspector_search-08.js64
-rw-r--r--devtools/client/inspector/test/browser_inspector_search-clear.js52
-rw-r--r--devtools/client/inspector/test/browser_inspector_search-filter_context-menu.js82
-rw-r--r--devtools/client/inspector/test/browser_inspector_search-label.js33
-rw-r--r--devtools/client/inspector/test/browser_inspector_search-navigation.js76
-rw-r--r--devtools/client/inspector/test/browser_inspector_search-reserved.js132
-rw-r--r--devtools/client/inspector/test/browser_inspector_search-selection.js62
-rw-r--r--devtools/client/inspector/test/browser_inspector_search-sidebar.js74
-rw-r--r--devtools/client/inspector/test/browser_inspector_search-suggests-ids-and-classes.js84
-rw-r--r--devtools/client/inspector/test/browser_inspector_search_keyboard_trap.js94
-rw-r--r--devtools/client/inspector/test/browser_inspector_select-docshell.js86
-rw-r--r--devtools/client/inspector/test/browser_inspector_select-last-selected.js95
-rw-r--r--devtools/client/inspector/test/browser_inspector_sidebarstate.js38
-rw-r--r--devtools/client/inspector/test/browser_inspector_switch-to-inspector-on-pick.js39
-rw-r--r--devtools/client/inspector/test/browser_inspector_textbox-menu.js90
-rw-r--r--devtools/client/inspector/test/doc_inspector_add_node.html22
-rw-r--r--devtools/client/inspector/test/doc_inspector_breadcrumbs.html75
-rw-r--r--devtools/client/inspector/test/doc_inspector_breadcrumbs_visibility.html22
-rw-r--r--devtools/client/inspector/test/doc_inspector_csp.html10
-rw-r--r--devtools/client/inspector/test/doc_inspector_csp.html^headers^2
-rw-r--r--devtools/client/inspector/test/doc_inspector_delete-selected-node-01.html4
-rw-r--r--devtools/client/inspector/test/doc_inspector_delete-selected-node-02.html20
-rw-r--r--devtools/client/inspector/test/doc_inspector_embed.html6
-rw-r--r--devtools/client/inspector/test/doc_inspector_gcli-inspect-command.html25
-rw-r--r--devtools/client/inspector/test/doc_inspector_highlight_after_transition.html26
-rw-r--r--devtools/client/inspector/test/doc_inspector_highlighter-comments.html19
-rw-r--r--devtools/client/inspector/test/doc_inspector_highlighter-geometry_01.html90
-rw-r--r--devtools/client/inspector/test/doc_inspector_highlighter-geometry_02.html120
-rw-r--r--devtools/client/inspector/test/doc_inspector_highlighter.html40
-rw-r--r--devtools/client/inspector/test/doc_inspector_highlighter_csstransform.html25
-rw-r--r--devtools/client/inspector/test/doc_inspector_highlighter_dom.html20
-rw-r--r--devtools/client/inspector/test/doc_inspector_highlighter_inline.html36
-rw-r--r--devtools/client/inspector/test/doc_inspector_highlighter_rect.html22
-rw-r--r--devtools/client/inspector/test/doc_inspector_highlighter_rect_iframe.html15
-rw-r--r--devtools/client/inspector/test/doc_inspector_highlighter_xbl.xul9
-rw-r--r--devtools/client/inspector/test/doc_inspector_infobar.html43
-rw-r--r--devtools/client/inspector/test/doc_inspector_infobar_01.html44
-rw-r--r--devtools/client/inspector/test/doc_inspector_infobar_02.html34
-rw-r--r--devtools/client/inspector/test/doc_inspector_infobar_03.html14
-rw-r--r--devtools/client/inspector/test/doc_inspector_infobar_textnode.html14
-rw-r--r--devtools/client/inspector/test/doc_inspector_long-divs.html104
-rw-r--r--devtools/client/inspector/test/doc_inspector_menu.html29
-rw-r--r--devtools/client/inspector/test/doc_inspector_outerhtml.html11
-rw-r--r--devtools/client/inspector/test/doc_inspector_remove-iframe-during-load.html45
-rw-r--r--devtools/client/inspector/test/doc_inspector_search-reserved.html11
-rw-r--r--devtools/client/inspector/test/doc_inspector_search-suggestions.html27
-rw-r--r--devtools/client/inspector/test/doc_inspector_search-svg.html16
-rw-r--r--devtools/client/inspector/test/doc_inspector_search.html26
-rw-r--r--devtools/client/inspector/test/doc_inspector_select-last-selected-01.html21
-rw-r--r--devtools/client/inspector/test/doc_inspector_select-last-selected-02.html10
-rw-r--r--devtools/client/inspector/test/doc_inspector_svg.svg3
-rw-r--r--devtools/client/inspector/test/head.js732
-rw-r--r--devtools/client/inspector/test/shared-head.js186
-rw-r--r--devtools/client/inspector/toolsidebar.js325
-rw-r--r--devtools/client/jar.mn351
-rw-r--r--devtools/client/jsonview/.eslintrc.js11
-rw-r--r--devtools/client/jsonview/components/headers-panel.js79
-rw-r--r--devtools/client/jsonview/components/headers.js105
-rw-r--r--devtools/client/jsonview/components/json-panel.js194
-rw-r--r--devtools/client/jsonview/components/main-tabbed-area.js89
-rw-r--r--devtools/client/jsonview/components/moz.build18
-rw-r--r--devtools/client/jsonview/components/reps/moz.build9
-rw-r--r--devtools/client/jsonview/components/reps/toolbar.js58
-rw-r--r--devtools/client/jsonview/components/search-box.js55
-rw-r--r--devtools/client/jsonview/components/text-panel.js95
-rw-r--r--devtools/client/jsonview/converter-child.js345
-rw-r--r--devtools/client/jsonview/converter-observer.js97
-rw-r--r--devtools/client/jsonview/converter-sniffer.js106
-rw-r--r--devtools/client/jsonview/css/general.css46
-rw-r--r--devtools/client/jsonview/css/headers-panel.css78
-rw-r--r--devtools/client/jsonview/css/json-panel.css16
-rw-r--r--devtools/client/jsonview/css/main.css59
-rw-r--r--devtools/client/jsonview/css/moz.build16
-rw-r--r--devtools/client/jsonview/css/search-box.css24
-rw-r--r--devtools/client/jsonview/css/search.svg22
-rw-r--r--devtools/client/jsonview/css/text-panel.css26
-rw-r--r--devtools/client/jsonview/css/toolbar.css92
-rw-r--r--devtools/client/jsonview/json-viewer.js112
-rw-r--r--devtools/client/jsonview/lib/moz.build9
-rw-r--r--devtools/client/jsonview/lib/require.js2076
-rw-r--r--devtools/client/jsonview/main.js62
-rw-r--r--devtools/client/jsonview/moz.build23
-rw-r--r--devtools/client/jsonview/test/.eslintrc.js6
-rw-r--r--devtools/client/jsonview/test/array_json.json1
-rw-r--r--devtools/client/jsonview/test/array_json.json^headers^1
-rw-r--r--devtools/client/jsonview/test/browser.ini28
-rw-r--r--devtools/client/jsonview/test/browser_jsonview_copy_headers.js35
-rw-r--r--devtools/client/jsonview/test/browser_jsonview_copy_json.js31
-rw-r--r--devtools/client/jsonview/test/browser_jsonview_copy_rawdata.js53
-rw-r--r--devtools/client/jsonview/test/browser_jsonview_filter.js28
-rw-r--r--devtools/client/jsonview/test/browser_jsonview_invalid_json.js20
-rw-r--r--devtools/client/jsonview/test/browser_jsonview_save_json.js38
-rw-r--r--devtools/client/jsonview/test/browser_jsonview_valid_json.js33
-rw-r--r--devtools/client/jsonview/test/doc_frame_script.js98
-rw-r--r--devtools/client/jsonview/test/head.js145
-rw-r--r--devtools/client/jsonview/test/invalid_json.json1
-rw-r--r--devtools/client/jsonview/test/invalid_json.json^headers^1
-rw-r--r--devtools/client/jsonview/test/simple_json.json1
-rw-r--r--devtools/client/jsonview/test/simple_json.json^headers^1
-rw-r--r--devtools/client/jsonview/test/valid_json.json6
-rw-r--r--devtools/client/jsonview/test/valid_json.json^headers^1
-rw-r--r--devtools/client/jsonview/utils.js101
-rw-r--r--devtools/client/jsonview/viewer-config.js39
-rw-r--r--devtools/client/locales/en-US/VariablesView.dtd12
-rw-r--r--devtools/client/locales/en-US/aboutdebugging.dtd5
-rw-r--r--devtools/client/locales/en-US/aboutdebugging.properties105
-rw-r--r--devtools/client/locales/en-US/animationinspector.properties173
-rw-r--r--devtools/client/locales/en-US/app-manager.properties29
-rw-r--r--devtools/client/locales/en-US/appcacheutils.properties119
-rw-r--r--devtools/client/locales/en-US/boxmodel.properties37
-rw-r--r--devtools/client/locales/en-US/canvasdebugger.dtd45
-rw-r--r--devtools/client/locales/en-US/canvasdebugger.properties70
-rw-r--r--devtools/client/locales/en-US/components.properties19
-rw-r--r--devtools/client/locales/en-US/connection-screen.dtd30
-rw-r--r--devtools/client/locales/en-US/connection-screen.properties9
-rw-r--r--devtools/client/locales/en-US/debugger.dtd212
-rw-r--r--devtools/client/locales/en-US/debugger.properties383
-rw-r--r--devtools/client/locales/en-US/device.properties20
-rw-r--r--devtools/client/locales/en-US/dom.properties19
-rw-r--r--devtools/client/locales/en-US/eyedropper.properties14
-rw-r--r--devtools/client/locales/en-US/filterwidget.properties61
-rw-r--r--devtools/client/locales/en-US/font-inspector.properties29
-rw-r--r--devtools/client/locales/en-US/graphs.properties24
-rw-r--r--devtools/client/locales/en-US/har.properties22
-rw-r--r--devtools/client/locales/en-US/inspector.properties364
-rw-r--r--devtools/client/locales/en-US/jit-optimizations.properties35
-rw-r--r--devtools/client/locales/en-US/jsonview.properties49
-rw-r--r--devtools/client/locales/en-US/layout.properties15
-rw-r--r--devtools/client/locales/en-US/markers.properties174
-rw-r--r--devtools/client/locales/en-US/memory.properties446
-rw-r--r--devtools/client/locales/en-US/menus.properties67
-rw-r--r--devtools/client/locales/en-US/netmonitor.properties747
-rw-r--r--devtools/client/locales/en-US/performance.dtd137
-rw-r--r--devtools/client/locales/en-US/performance.properties160
-rw-r--r--devtools/client/locales/en-US/projecteditor.properties88
-rw-r--r--devtools/client/locales/en-US/responsive.properties81
-rw-r--r--devtools/client/locales/en-US/responsiveUI.properties69
-rw-r--r--devtools/client/locales/en-US/scratchpad.dtd155
-rw-r--r--devtools/client/locales/en-US/scratchpad.properties105
-rw-r--r--devtools/client/locales/en-US/shadereditor.dtd32
-rw-r--r--devtools/client/locales/en-US/shadereditor.properties22
-rw-r--r--devtools/client/locales/en-US/shared.properties11
-rw-r--r--devtools/client/locales/en-US/sourceeditor.dtd19
-rw-r--r--devtools/client/locales/en-US/sourceeditor.properties139
-rw-r--r--devtools/client/locales/en-US/startup.properties262
-rw-r--r--devtools/client/locales/en-US/storage.dtd11
-rw-r--r--devtools/client/locales/en-US/storage.properties97
-rw-r--r--devtools/client/locales/en-US/styleeditor.dtd67
-rw-r--r--devtools/client/locales/en-US/styleeditor.properties56
-rw-r--r--devtools/client/locales/en-US/toolbox.dtd220
-rw-r--r--devtools/client/locales/en-US/toolbox.properties160
-rw-r--r--devtools/client/locales/en-US/webConsole.dtd101
-rw-r--r--devtools/client/locales/en-US/webaudioeditor.dtd53
-rw-r--r--devtools/client/locales/en-US/webaudioeditor.properties20
-rw-r--r--devtools/client/locales/en-US/webconsole.properties203
-rw-r--r--devtools/client/locales/en-US/webide.dtd218
-rw-r--r--devtools/client/locales/en-US/webide.properties92
-rw-r--r--devtools/client/locales/jar.mn8
-rw-r--r--devtools/client/locales/l10n.ini12
-rw-r--r--devtools/client/locales/moz.build7
-rw-r--r--devtools/client/memory/actions/allocations.js20
-rw-r--r--devtools/client/memory/actions/census-display.js34
-rw-r--r--devtools/client/memory/actions/diffing.js201
-rw-r--r--devtools/client/memory/actions/filter.js35
-rw-r--r--devtools/client/memory/actions/io.js97
-rw-r--r--devtools/client/memory/actions/label-display.js38
-rw-r--r--devtools/client/memory/actions/moz.build19
-rw-r--r--devtools/client/memory/actions/refresh.js44
-rw-r--r--devtools/client/memory/actions/sizes.js13
-rw-r--r--devtools/client/memory/actions/snapshot.js865
-rw-r--r--devtools/client/memory/actions/task-cache.js99
-rw-r--r--devtools/client/memory/actions/tree-map-display.js37
-rw-r--r--devtools/client/memory/actions/view.js67
-rw-r--r--devtools/client/memory/app.js322
-rw-r--r--devtools/client/memory/components/census-header.js72
-rw-r--r--devtools/client/memory/components/census-tree-item.js134
-rw-r--r--devtools/client/memory/components/census.js79
-rw-r--r--devtools/client/memory/components/dominator-tree-header.js44
-rw-r--r--devtools/client/memory/components/dominator-tree-item.js142
-rw-r--r--devtools/client/memory/components/dominator-tree.js216
-rw-r--r--devtools/client/memory/components/heap.js455
-rw-r--r--devtools/client/memory/components/individuals-header.js44
-rw-r--r--devtools/client/memory/components/individuals.js61
-rw-r--r--devtools/client/memory/components/list.js35
-rw-r--r--devtools/client/memory/components/moz.build25
-rw-r--r--devtools/client/memory/components/shortest-paths.js184
-rw-r--r--devtools/client/memory/components/snapshot-list-item.js114
-rw-r--r--devtools/client/memory/components/toolbar.js300
-rw-r--r--devtools/client/memory/components/tree-map.js71
-rw-r--r--devtools/client/memory/components/tree-map/canvas-utils.js134
-rw-r--r--devtools/client/memory/components/tree-map/color-coarse-type.js70
-rw-r--r--devtools/client/memory/components/tree-map/drag-zoom.js316
-rw-r--r--devtools/client/memory/components/tree-map/draw.js295
-rw-r--r--devtools/client/memory/components/tree-map/moz.build12
-rw-r--r--devtools/client/memory/components/tree-map/start.js32
-rw-r--r--devtools/client/memory/constants.js342
-rw-r--r--devtools/client/memory/dominator-tree-lazy-children.js58
-rw-r--r--devtools/client/memory/initializer.js67
-rw-r--r--devtools/client/memory/memory.xhtml42
-rw-r--r--devtools/client/memory/models.js519
-rw-r--r--devtools/client/memory/moz.build29
-rw-r--r--devtools/client/memory/panel.js75
-rw-r--r--devtools/client/memory/reducers.js16
-rw-r--r--devtools/client/memory/reducers/allocations.js42
-rw-r--r--devtools/client/memory/reducers/census-display.js21
-rw-r--r--devtools/client/memory/reducers/diffing.js146
-rw-r--r--devtools/client/memory/reducers/errors.js17
-rw-r--r--devtools/client/memory/reducers/filter.js14
-rw-r--r--devtools/client/memory/reducers/individuals.js73
-rw-r--r--devtools/client/memory/reducers/label-display.js19
-rw-r--r--devtools/client/memory/reducers/moz.build18
-rw-r--r--devtools/client/memory/reducers/sizes.js18
-rw-r--r--devtools/client/memory/reducers/snapshots.js459
-rw-r--r--devtools/client/memory/reducers/tree-map-display.js19
-rw-r--r--devtools/client/memory/reducers/view.js49
-rw-r--r--devtools/client/memory/store.js33
-rw-r--r--devtools/client/memory/telemetry.js91
-rw-r--r--devtools/client/memory/test/browser/.eslintrc.js6
-rw-r--r--devtools/client/memory/test/browser/browser.ini31
-rw-r--r--devtools/client/memory/test/browser/browser_memory_allocationStackDisplay_01.js39
-rw-r--r--devtools/client/memory/test/browser/browser_memory_clear_snapshots.js36
-rw-r--r--devtools/client/memory/test/browser/browser_memory_diff_01.js74
-rw-r--r--devtools/client/memory/test/browser/browser_memory_displays_01.js41
-rw-r--r--devtools/client/memory/test/browser/browser_memory_dominator_trees_01.js147
-rw-r--r--devtools/client/memory/test/browser/browser_memory_dominator_trees_02.js64
-rw-r--r--devtools/client/memory/test/browser/browser_memory_filter_01.js81
-rw-r--r--devtools/client/memory/test/browser/browser_memory_individuals_01.js67
-rw-r--r--devtools/client/memory/test/browser/browser_memory_keyboard-snapshot-list.js99
-rw-r--r--devtools/client/memory/test/browser/browser_memory_keyboard.js107
-rw-r--r--devtools/client/memory/test/browser/browser_memory_no_allocation_stacks.js39
-rw-r--r--devtools/client/memory/test/browser/browser_memory_no_auto_expand.js37
-rw-r--r--devtools/client/memory/test/browser/browser_memory_percents_01.js47
-rw-r--r--devtools/client/memory/test/browser/browser_memory_refresh_does_not_leak.js108
-rw-r--r--devtools/client/memory/test/browser/browser_memory_simple_01.js40
-rw-r--r--devtools/client/memory/test/browser/browser_memory_transferHeapSnapshot_e10s_01.js28
-rw-r--r--devtools/client/memory/test/browser/browser_memory_tree_map-01.js102
-rw-r--r--devtools/client/memory/test/browser/browser_memory_tree_map-02.js161
-rw-r--r--devtools/client/memory/test/browser/doc_big_tree.html15
-rw-r--r--devtools/client/memory/test/browser/doc_empty.html9
-rw-r--r--devtools/client/memory/test/browser/doc_steady_allocation.html16
-rw-r--r--devtools/client/memory/test/browser/head.js248
-rw-r--r--devtools/client/memory/test/chrome/chrome.ini20
-rw-r--r--devtools/client/memory/test/chrome/head.js335
-rw-r--r--devtools/client/memory/test/chrome/test_CensusTreeItem_01.html65
-rw-r--r--devtools/client/memory/test/chrome/test_DominatorTreeItem_01.html45
-rw-r--r--devtools/client/memory/test/chrome/test_DominatorTree_01.html50
-rw-r--r--devtools/client/memory/test/chrome/test_DominatorTree_02.html50
-rw-r--r--devtools/client/memory/test/chrome/test_DominatorTree_03.html75
-rw-r--r--devtools/client/memory/test/chrome/test_Heap_01.html50
-rw-r--r--devtools/client/memory/test/chrome/test_Heap_02.html78
-rw-r--r--devtools/client/memory/test/chrome/test_Heap_03.html74
-rw-r--r--devtools/client/memory/test/chrome/test_Heap_04.html121
-rw-r--r--devtools/client/memory/test/chrome/test_Heap_05.html132
-rw-r--r--devtools/client/memory/test/chrome/test_List_01.html74
-rw-r--r--devtools/client/memory/test/chrome/test_ShortestPaths_01.html112
-rw-r--r--devtools/client/memory/test/chrome/test_ShortestPaths_02.html45
-rw-r--r--devtools/client/memory/test/chrome/test_SnapshotListItem_01.html53
-rw-r--r--devtools/client/memory/test/chrome/test_Toolbar_01.html47
-rw-r--r--devtools/client/memory/test/chrome/test_TreeMap_01.html44
-rw-r--r--devtools/client/memory/test/unit/.eslintrc.js6
-rw-r--r--devtools/client/memory/test/unit/head.js128
-rw-r--r--devtools/client/memory/test/unit/test_action-clear-snapshots_01.js38
-rw-r--r--devtools/client/memory/test/unit/test_action-clear-snapshots_02.js47
-rw-r--r--devtools/client/memory/test/unit/test_action-clear-snapshots_03.js46
-rw-r--r--devtools/client/memory/test/unit/test_action-clear-snapshots_04.js49
-rw-r--r--devtools/client/memory/test/unit/test_action-clear-snapshots_05.js47
-rw-r--r--devtools/client/memory/test/unit/test_action-clear-snapshots_06.js65
-rw-r--r--devtools/client/memory/test/unit/test_action-export-snapshot.js39
-rw-r--r--devtools/client/memory/test/unit/test_action-filter-01.js23
-rw-r--r--devtools/client/memory/test/unit/test_action-filter-02.js74
-rw-r--r--devtools/client/memory/test/unit/test_action-filter-03.js52
-rw-r--r--devtools/client/memory/test/unit/test_action-import-snapshot-and-census.js98
-rw-r--r--devtools/client/memory/test/unit/test_action-import-snapshot-dominator-tree.js84
-rw-r--r--devtools/client/memory/test/unit/test_action-select-snapshot.js37
-rw-r--r--devtools/client/memory/test/unit/test_action-set-display-and-refresh-01.js118
-rw-r--r--devtools/client/memory/test/unit/test_action-set-display-and-refresh-02.js53
-rw-r--r--devtools/client/memory/test/unit/test_action-set-display.js55
-rw-r--r--devtools/client/memory/test/unit/test_action-take-census.js59
-rw-r--r--devtools/client/memory/test/unit/test_action-take-snapshot-and-census.js58
-rw-r--r--devtools/client/memory/test/unit/test_action-take-snapshot.js54
-rw-r--r--devtools/client/memory/test/unit/test_action-toggle-inverted-and-refresh-01.js82
-rw-r--r--devtools/client/memory/test/unit/test_action-toggle-inverted-and-refresh-02.js58
-rw-r--r--devtools/client/memory/test/unit/test_action-toggle-inverted.js28
-rw-r--r--devtools/client/memory/test/unit/test_action-toggle-recording-allocations.js42
-rw-r--r--devtools/client/memory/test/unit/test_action_diffing_01.js29
-rw-r--r--devtools/client/memory/test/unit/test_action_diffing_02.js46
-rw-r--r--devtools/client/memory/test/unit/test_action_diffing_03.js104
-rw-r--r--devtools/client/memory/test/unit/test_action_diffing_04.js78
-rw-r--r--devtools/client/memory/test/unit/test_action_diffing_05.js112
-rw-r--r--devtools/client/memory/test/unit/test_dominator_trees_01.js61
-rw-r--r--devtools/client/memory/test/unit/test_dominator_trees_02.js64
-rw-r--r--devtools/client/memory/test/unit/test_dominator_trees_03.js61
-rw-r--r--devtools/client/memory/test/unit/test_dominator_trees_04.js69
-rw-r--r--devtools/client/memory/test/unit/test_dominator_trees_05.js59
-rw-r--r--devtools/client/memory/test/unit/test_dominator_trees_06.js127
-rw-r--r--devtools/client/memory/test/unit/test_dominator_trees_07.js146
-rw-r--r--devtools/client/memory/test/unit/test_dominator_trees_08.js81
-rw-r--r--devtools/client/memory/test/unit/test_dominator_trees_09.js78
-rw-r--r--devtools/client/memory/test/unit/test_dominator_trees_10.js74
-rw-r--r--devtools/client/memory/test/unit/test_individuals_01.js76
-rw-r--r--devtools/client/memory/test/unit/test_individuals_02.js88
-rw-r--r--devtools/client/memory/test/unit/test_individuals_03.js106
-rw-r--r--devtools/client/memory/test/unit/test_individuals_04.js89
-rw-r--r--devtools/client/memory/test/unit/test_individuals_05.js82
-rw-r--r--devtools/client/memory/test/unit/test_individuals_06.js84
-rw-r--r--devtools/client/memory/test/unit/test_pop_view_01.js81
-rw-r--r--devtools/client/memory/test/unit/test_tree-map-01.js57
-rw-r--r--devtools/client/memory/test/unit/test_tree-map-02.js81
-rw-r--r--devtools/client/memory/test/unit/test_utils-get-snapshot-totals.js72
-rw-r--r--devtools/client/memory/test/unit/test_utils.js70
-rw-r--r--devtools/client/memory/test/unit/xpcshell.ini56
-rw-r--r--devtools/client/memory/utils.js529
-rw-r--r--devtools/client/menus.js195
-rw-r--r--devtools/client/moz.build54
-rw-r--r--devtools/client/netmonitor/.eslintrc.js15
-rw-r--r--devtools/client/netmonitor/actions/filters.js57
-rw-r--r--devtools/client/netmonitor/actions/index.js9
-rw-r--r--devtools/client/netmonitor/actions/moz.build10
-rw-r--r--devtools/client/netmonitor/actions/sidebar.js49
-rw-r--r--devtools/client/netmonitor/components/filter-buttons.js49
-rw-r--r--devtools/client/netmonitor/components/moz.build10
-rw-r--r--devtools/client/netmonitor/components/search-box.js24
-rw-r--r--devtools/client/netmonitor/components/toggle-button.js69
-rw-r--r--devtools/client/netmonitor/constants.js19
-rw-r--r--devtools/client/netmonitor/custom-request-view.js216
-rw-r--r--devtools/client/netmonitor/events.js86
-rw-r--r--devtools/client/netmonitor/filter-predicates.js129
-rw-r--r--devtools/client/netmonitor/har/har-automation.js273
-rw-r--r--devtools/client/netmonitor/har/har-builder.js491
-rw-r--r--devtools/client/netmonitor/har/har-collector.js462
-rw-r--r--devtools/client/netmonitor/har/har-exporter.js187
-rw-r--r--devtools/client/netmonitor/har/har-utils.js189
-rw-r--r--devtools/client/netmonitor/har/moz.build15
-rw-r--r--devtools/client/netmonitor/har/test/.eslintrc.js6
-rw-r--r--devtools/client/netmonitor/har/test/browser.ini12
-rw-r--r--devtools/client/netmonitor/har/test/browser_net_har_copy_all_as_har.js49
-rw-r--r--devtools/client/netmonitor/har/test/browser_net_har_post_data.js44
-rw-r--r--devtools/client/netmonitor/har/test/browser_net_har_throttle_upload.js75
-rw-r--r--devtools/client/netmonitor/har/test/head.js14
-rw-r--r--devtools/client/netmonitor/har/test/html_har_post-data-test-page.html39
-rw-r--r--devtools/client/netmonitor/har/toolbox-overlay.js85
-rw-r--r--devtools/client/netmonitor/l10n.js9
-rw-r--r--devtools/client/netmonitor/moz.build31
-rw-r--r--devtools/client/netmonitor/netmonitor-controller.js816
-rw-r--r--devtools/client/netmonitor/netmonitor-view.js1230
-rw-r--r--devtools/client/netmonitor/netmonitor.xul741
-rw-r--r--devtools/client/netmonitor/panel.js77
-rw-r--r--devtools/client/netmonitor/performance-statistics-view.js265
-rw-r--r--devtools/client/netmonitor/prefs.js14
-rw-r--r--devtools/client/netmonitor/reducers/filters.js80
-rw-r--r--devtools/client/netmonitor/reducers/index.js13
-rw-r--r--devtools/client/netmonitor/reducers/moz.build10
-rw-r--r--devtools/client/netmonitor/reducers/sidebar.js43
-rw-r--r--devtools/client/netmonitor/request-list-context-menu.js357
-rw-r--r--devtools/client/netmonitor/request-utils.js185
-rw-r--r--devtools/client/netmonitor/requests-menu-view.js1649
-rw-r--r--devtools/client/netmonitor/selectors/index.js8
-rw-r--r--devtools/client/netmonitor/selectors/moz.build8
-rw-r--r--devtools/client/netmonitor/sort-predicates.js92
-rw-r--r--devtools/client/netmonitor/store.js13
-rw-r--r--devtools/client/netmonitor/test/.eslintrc.js6
-rw-r--r--devtools/client/netmonitor/test/browser.ini156
-rw-r--r--devtools/client/netmonitor/test/browser_net_aaa_leaktest.js28
-rw-r--r--devtools/client/netmonitor/test/browser_net_accessibility-01.js87
-rw-r--r--devtools/client/netmonitor/test/browser_net_accessibility-02.js130
-rw-r--r--devtools/client/netmonitor/test/browser_net_api-calls.js39
-rw-r--r--devtools/client/netmonitor/test/browser_net_autoscroll.js75
-rw-r--r--devtools/client/netmonitor/test/browser_net_brotli.js91
-rw-r--r--devtools/client/netmonitor/test/browser_net_cached-status.js111
-rw-r--r--devtools/client/netmonitor/test/browser_net_cause.js147
-rw-r--r--devtools/client/netmonitor/test/browser_net_cause_redirect.js57
-rw-r--r--devtools/client/netmonitor/test/browser_net_charts-01.js73
-rw-r--r--devtools/client/netmonitor/test/browser_net_charts-02.js49
-rw-r--r--devtools/client/netmonitor/test/browser_net_charts-03.js106
-rw-r--r--devtools/client/netmonitor/test/browser_net_charts-04.js75
-rw-r--r--devtools/client/netmonitor/test/browser_net_charts-05.js61
-rw-r--r--devtools/client/netmonitor/test/browser_net_charts-06.js47
-rw-r--r--devtools/client/netmonitor/test/browser_net_charts-07.js63
-rw-r--r--devtools/client/netmonitor/test/browser_net_clear.js77
-rw-r--r--devtools/client/netmonitor/test/browser_net_complex-params.js195
-rw-r--r--devtools/client/netmonitor/test/browser_net_content-type.js255
-rw-r--r--devtools/client/netmonitor/test/browser_net_copy_as_curl.js88
-rw-r--r--devtools/client/netmonitor/test/browser_net_copy_headers.js72
-rw-r--r--devtools/client/netmonitor/test/browser_net_copy_image_as_data_uri.js35
-rw-r--r--devtools/client/netmonitor/test/browser_net_copy_params.js98
-rw-r--r--devtools/client/netmonitor/test/browser_net_copy_response.js35
-rw-r--r--devtools/client/netmonitor/test/browser_net_copy_svg_image_as_data_uri.js37
-rw-r--r--devtools/client/netmonitor/test/browser_net_copy_url.js31
-rw-r--r--devtools/client/netmonitor/test/browser_net_cors_requests.js33
-rw-r--r--devtools/client/netmonitor/test/browser_net_curl-utils.js228
-rw-r--r--devtools/client/netmonitor/test/browser_net_cyrillic-01.js45
-rw-r--r--devtools/client/netmonitor/test/browser_net_cyrillic-02.js44
-rw-r--r--devtools/client/netmonitor/test/browser_net_details-no-duplicated-content.js172
-rw-r--r--devtools/client/netmonitor/test/browser_net_filter-01.js264
-rw-r--r--devtools/client/netmonitor/test/browser_net_filter-02.js200
-rw-r--r--devtools/client/netmonitor/test/browser_net_filter-03.js185
-rw-r--r--devtools/client/netmonitor/test/browser_net_filter-04.js63
-rw-r--r--devtools/client/netmonitor/test/browser_net_footer-summary.js75
-rw-r--r--devtools/client/netmonitor/test/browser_net_frame.js221
-rw-r--r--devtools/client/netmonitor/test/browser_net_html-preview.js62
-rw-r--r--devtools/client/netmonitor/test/browser_net_icon-preview.js71
-rw-r--r--devtools/client/netmonitor/test/browser_net_image-tooltip.js101
-rw-r--r--devtools/client/netmonitor/test/browser_net_json-long.js98
-rw-r--r--devtools/client/netmonitor/test/browser_net_json-malformed.js77
-rw-r--r--devtools/client/netmonitor/test/browser_net_json_custom_mime.js90
-rw-r--r--devtools/client/netmonitor/test/browser_net_json_text_mime.js90
-rw-r--r--devtools/client/netmonitor/test/browser_net_jsonp.js111
-rw-r--r--devtools/client/netmonitor/test/browser_net_large-response.js55
-rw-r--r--devtools/client/netmonitor/test/browser_net_leak_on_tab_close.js17
-rw-r--r--devtools/client/netmonitor/test/browser_net_open_request_in_tab.js37
-rw-r--r--devtools/client/netmonitor/test/browser_net_page-nav.js69
-rw-r--r--devtools/client/netmonitor/test/browser_net_pane-collapse.js72
-rw-r--r--devtools/client/netmonitor/test/browser_net_pane-toggle.js74
-rw-r--r--devtools/client/netmonitor/test/browser_net_persistent_logs.js51
-rw-r--r--devtools/client/netmonitor/test/browser_net_post-data-01.js166
-rw-r--r--devtools/client/netmonitor/test/browser_net_post-data-02.js73
-rw-r--r--devtools/client/netmonitor/test/browser_net_post-data-03.js98
-rw-r--r--devtools/client/netmonitor/test/browser_net_post-data-04.js74
-rw-r--r--devtools/client/netmonitor/test/browser_net_prefs-and-l10n.js54
-rw-r--r--devtools/client/netmonitor/test/browser_net_prefs-reload.js215
-rw-r--r--devtools/client/netmonitor/test/browser_net_raw_headers.js70
-rw-r--r--devtools/client/netmonitor/test/browser_net_reload-button.js25
-rw-r--r--devtools/client/netmonitor/test/browser_net_reload-markers.js35
-rw-r--r--devtools/client/netmonitor/test/browser_net_req-resp-bodies.js68
-rw-r--r--devtools/client/netmonitor/test/browser_net_resend.js174
-rw-r--r--devtools/client/netmonitor/test/browser_net_resend_cors.js80
-rw-r--r--devtools/client/netmonitor/test/browser_net_resend_headers.js67
-rw-r--r--devtools/client/netmonitor/test/browser_net_security-details.js102
-rw-r--r--devtools/client/netmonitor/test/browser_net_security-error.js70
-rw-r--r--devtools/client/netmonitor/test/browser_net_security-icon-click.js57
-rw-r--r--devtools/client/netmonitor/test/browser_net_security-redirect.js38
-rw-r--r--devtools/client/netmonitor/test/browser_net_security-state.js119
-rw-r--r--devtools/client/netmonitor/test/browser_net_security-tab-deselect.js46
-rw-r--r--devtools/client/netmonitor/test/browser_net_security-tab-visibility.js121
-rw-r--r--devtools/client/netmonitor/test/browser_net_security-warnings.js56
-rw-r--r--devtools/client/netmonitor/test/browser_net_send-beacon-other-tab.js34
-rw-r--r--devtools/client/netmonitor/test/browser_net_send-beacon.js31
-rw-r--r--devtools/client/netmonitor/test/browser_net_service-worker-status.js87
-rw-r--r--devtools/client/netmonitor/test/browser_net_simple-init.js93
-rw-r--r--devtools/client/netmonitor/test/browser_net_simple-request-data.js247
-rw-r--r--devtools/client/netmonitor/test/browser_net_simple-request-details.js261
-rw-r--r--devtools/client/netmonitor/test/browser_net_simple-request.js72
-rw-r--r--devtools/client/netmonitor/test/browser_net_sort-01.js230
-rw-r--r--devtools/client/netmonitor/test/browser_net_sort-02.js272
-rw-r--r--devtools/client/netmonitor/test/browser_net_sort-03.js209
-rw-r--r--devtools/client/netmonitor/test/browser_net_statistics-01.js63
-rw-r--r--devtools/client/netmonitor/test/browser_net_statistics-02.js42
-rw-r--r--devtools/client/netmonitor/test/browser_net_statistics-03.js45
-rw-r--r--devtools/client/netmonitor/test/browser_net_status-codes.js213
-rw-r--r--devtools/client/netmonitor/test/browser_net_streaming-response.js69
-rw-r--r--devtools/client/netmonitor/test/browser_net_throttle.js57
-rw-r--r--devtools/client/netmonitor/test/browser_net_timeline_ticks.js142
-rw-r--r--devtools/client/netmonitor/test/browser_net_timing-division.js61
-rw-r--r--devtools/client/netmonitor/test/browser_net_truncate.js44
-rw-r--r--devtools/client/netmonitor/test/dropmarker.svg6
-rw-r--r--devtools/client/netmonitor/test/head.js518
-rw-r--r--devtools/client/netmonitor/test/html_api-calls-test-page.html46
-rw-r--r--devtools/client/netmonitor/test/html_brotli-test-page.html38
-rw-r--r--devtools/client/netmonitor/test/html_cause-test-page.html48
-rw-r--r--devtools/client/netmonitor/test/html_content-type-test-page.html48
-rw-r--r--devtools/client/netmonitor/test/html_content-type-without-cache-test-page.html52
-rw-r--r--devtools/client/netmonitor/test/html_copy-as-curl.html30
-rw-r--r--devtools/client/netmonitor/test/html_cors-test-page.html31
-rw-r--r--devtools/client/netmonitor/test/html_curl-utils.html102
-rw-r--r--devtools/client/netmonitor/test/html_custom-get-page.html44
-rw-r--r--devtools/client/netmonitor/test/html_cyrillic-test-page.html39
-rw-r--r--devtools/client/netmonitor/test/html_filter-test-page.html60
-rw-r--r--devtools/client/netmonitor/test/html_frame-subdocument.html48
-rw-r--r--devtools/client/netmonitor/test/html_frame-test-page.html49
-rw-r--r--devtools/client/netmonitor/test/html_image-tooltip-test-page.html26
-rw-r--r--devtools/client/netmonitor/test/html_infinite-get-page.html41
-rw-r--r--devtools/client/netmonitor/test/html_json-custom-mime-test-page.html38
-rw-r--r--devtools/client/netmonitor/test/html_json-long-test-page.html38
-rw-r--r--devtools/client/netmonitor/test/html_json-malformed-test-page.html38
-rw-r--r--devtools/client/netmonitor/test/html_json-text-mime-test-page.html38
-rw-r--r--devtools/client/netmonitor/test/html_jsonp-test-page.html40
-rw-r--r--devtools/client/netmonitor/test/html_navigate-test-page.html18
-rw-r--r--devtools/client/netmonitor/test/html_params-test-page.html67
-rw-r--r--devtools/client/netmonitor/test/html_post-data-test-page.html77
-rw-r--r--devtools/client/netmonitor/test/html_post-json-test-page.html39
-rw-r--r--devtools/client/netmonitor/test/html_post-raw-test-page.html40
-rw-r--r--devtools/client/netmonitor/test/html_post-raw-with-headers-test-page.html45
-rw-r--r--devtools/client/netmonitor/test/html_send-beacon.html23
-rw-r--r--devtools/client/netmonitor/test/html_simple-test-page.html18
-rw-r--r--devtools/client/netmonitor/test/html_single-get-page.html36
-rw-r--r--devtools/client/netmonitor/test/html_sorting-test-page.html18
-rw-r--r--devtools/client/netmonitor/test/html_statistics-test-page.html40
-rw-r--r--devtools/client/netmonitor/test/html_status-codes-test-page.html55
-rw-r--r--devtools/client/netmonitor/test/service-workers/status-codes-service-worker.js15
-rw-r--r--devtools/client/netmonitor/test/service-workers/status-codes.html59
-rw-r--r--devtools/client/netmonitor/test/sjs_content-type-test-server.sjs273
-rw-r--r--devtools/client/netmonitor/test/sjs_cors-test-server.sjs17
-rw-r--r--devtools/client/netmonitor/test/sjs_hsts-test-server.sjs22
-rw-r--r--devtools/client/netmonitor/test/sjs_https-redirect-test-server.sjs19
-rw-r--r--devtools/client/netmonitor/test/sjs_simple-test-server.sjs17
-rw-r--r--devtools/client/netmonitor/test/sjs_sorting-test-server.sjs26
-rw-r--r--devtools/client/netmonitor/test/sjs_status-codes-test-server.sjs56
-rw-r--r--devtools/client/netmonitor/test/sjs_truncate-test-server.sjs18
-rw-r--r--devtools/client/netmonitor/test/test-image.pngbin0 -> 580 bytes
-rw-r--r--devtools/client/netmonitor/toolbar-view.js77
-rw-r--r--devtools/client/package.json21
-rw-r--r--devtools/client/performance/components/jit-optimizations-item.js175
-rw-r--r--devtools/client/performance/components/jit-optimizations.js248
-rw-r--r--devtools/client/performance/components/moz.build19
-rw-r--r--devtools/client/performance/components/recording-button.js37
-rw-r--r--devtools/client/performance/components/recording-controls.js54
-rw-r--r--devtools/client/performance/components/recording-list-item.js49
-rw-r--r--devtools/client/performance/components/recording-list.js23
-rw-r--r--devtools/client/performance/components/test/chrome.ini5
-rw-r--r--devtools/client/performance/components/test/head.js187
-rw-r--r--devtools/client/performance/components/test/test_jit_optimizations_01.html70
-rw-r--r--devtools/client/performance/components/waterfall-header.js69
-rw-r--r--devtools/client/performance/components/waterfall-tree-row.js107
-rw-r--r--devtools/client/performance/components/waterfall-tree.js167
-rw-r--r--devtools/client/performance/components/waterfall.js36
-rw-r--r--devtools/client/performance/docs/markers.md189
-rw-r--r--devtools/client/performance/events.js108
-rw-r--r--devtools/client/performance/legacy/actors.js263
-rw-r--r--devtools/client/performance/legacy/compatibility.js66
-rw-r--r--devtools/client/performance/legacy/front.js484
-rw-r--r--devtools/client/performance/legacy/moz.build12
-rw-r--r--devtools/client/performance/legacy/recording.js174
-rw-r--r--devtools/client/performance/modules/categories.js128
-rw-r--r--devtools/client/performance/modules/constants.js11
-rw-r--r--devtools/client/performance/modules/global.js36
-rw-r--r--devtools/client/performance/modules/io.js171
-rw-r--r--devtools/client/performance/modules/logic/frame-utils.js478
-rw-r--r--devtools/client/performance/modules/logic/jit.js342
-rw-r--r--devtools/client/performance/modules/logic/moz.build12
-rw-r--r--devtools/client/performance/modules/logic/telemetry.js122
-rw-r--r--devtools/client/performance/modules/logic/tree-model.js556
-rw-r--r--devtools/client/performance/modules/logic/waterfall-utils.js167
-rw-r--r--devtools/client/performance/modules/marker-blueprint-utils.js104
-rw-r--r--devtools/client/performance/modules/marker-dom-utils.js257
-rw-r--r--devtools/client/performance/modules/marker-formatters.js199
-rw-r--r--devtools/client/performance/modules/markers.js170
-rw-r--r--devtools/client/performance/modules/moz.build22
-rw-r--r--devtools/client/performance/modules/utils.js21
-rw-r--r--devtools/client/performance/modules/waterfall-ticks.js98
-rw-r--r--devtools/client/performance/modules/widgets/graphs.js514
-rw-r--r--devtools/client/performance/modules/widgets/marker-details.js164
-rw-r--r--devtools/client/performance/modules/widgets/markers-overview.js243
-rw-r--r--devtools/client/performance/modules/widgets/moz.build11
-rw-r--r--devtools/client/performance/modules/widgets/tree-view.js406
-rw-r--r--devtools/client/performance/moz.build19
-rw-r--r--devtools/client/performance/panel.js100
-rw-r--r--devtools/client/performance/performance-controller.js595
-rw-r--r--devtools/client/performance/performance-view.js411
-rw-r--r--devtools/client/performance/performance.xul368
-rw-r--r--devtools/client/performance/test/.eslintrc.js6
-rw-r--r--devtools/client/performance/test/browser.ini124
-rw-r--r--devtools/client/performance/test/browser_aaa-run-first-leaktest.js28
-rw-r--r--devtools/client/performance/test/browser_perf-button-states.js76
-rw-r--r--devtools/client/performance/test/browser_perf-calltree-js-categories.js60
-rw-r--r--devtools/client/performance/test/browser_perf-calltree-js-columns.js66
-rw-r--r--devtools/client/performance/test/browser_perf-calltree-js-events.js58
-rw-r--r--devtools/client/performance/test/browser_perf-calltree-memory-columns.js69
-rw-r--r--devtools/client/performance/test/browser_perf-console-record-01.js43
-rw-r--r--devtools/client/performance/test/browser_perf-console-record-02.js70
-rw-r--r--devtools/client/performance/test/browser_perf-console-record-03.js58
-rw-r--r--devtools/client/performance/test/browser_perf-console-record-04.js58
-rw-r--r--devtools/client/performance/test/browser_perf-console-record-05.js92
-rw-r--r--devtools/client/performance/test/browser_perf-console-record-06.js96
-rw-r--r--devtools/client/performance/test/browser_perf-console-record-07.js170
-rw-r--r--devtools/client/performance/test/browser_perf-console-record-08.js268
-rw-r--r--devtools/client/performance/test/browser_perf-console-record-09.js64
-rw-r--r--devtools/client/performance/test/browser_perf-details-01-toggle.js67
-rw-r--r--devtools/client/performance/test/browser_perf-details-02-utility-fun.js59
-rw-r--r--devtools/client/performance/test/browser_perf-details-03-without-allocations.js127
-rw-r--r--devtools/client/performance/test/browser_perf-details-04-toolbar-buttons.js145
-rw-r--r--devtools/client/performance/test/browser_perf-details-05-preserve-view.js50
-rw-r--r--devtools/client/performance/test/browser_perf-details-06-rerender-on-selection.js79
-rw-r--r--devtools/client/performance/test/browser_perf-details-07-bleed-events.js48
-rw-r--r--devtools/client/performance/test/browser_perf-details-render-00-waterfall.js40
-rw-r--r--devtools/client/performance/test/browser_perf-details-render-01-js-calltree.js40
-rw-r--r--devtools/client/performance/test/browser_perf-details-render-02-js-flamegraph.js40
-rw-r--r--devtools/client/performance/test/browser_perf-details-render-03-memory-calltree.js44
-rw-r--r--devtools/client/performance/test/browser_perf-details-render-04-memory-flamegraph.js45
-rw-r--r--devtools/client/performance/test/browser_perf-docload.js43
-rw-r--r--devtools/client/performance/test/browser_perf-gc-snap.js146
-rw-r--r--devtools/client/performance/test/browser_perf-highlighted.js48
-rw-r--r--devtools/client/performance/test/browser_perf-loading-01.js52
-rw-r--r--devtools/client/performance/test/browser_perf-loading-02.js82
-rw-r--r--devtools/client/performance/test/browser_perf-marker-details.js146
-rw-r--r--devtools/client/performance/test/browser_perf-options-01-toggle-throw.js31
-rw-r--r--devtools/client/performance/test/browser_perf-options-02-toggle-throw-alt.js38
-rw-r--r--devtools/client/performance/test/browser_perf-options-03-toggle-meta.js38
-rw-r--r--devtools/client/performance/test/browser_perf-options-enable-framerate-01.js52
-rw-r--r--devtools/client/performance/test/browser_perf-options-enable-framerate-02.js43
-rw-r--r--devtools/client/performance/test/browser_perf-options-enable-memory-01.js58
-rw-r--r--devtools/client/performance/test/browser_perf-options-enable-memory-02.js49
-rw-r--r--devtools/client/performance/test/browser_perf-options-flatten-tree-recursion-01.js73
-rw-r--r--devtools/client/performance/test/browser_perf-options-flatten-tree-recursion-02.js86
-rw-r--r--devtools/client/performance/test/browser_perf-options-invert-call-tree-01.js43
-rw-r--r--devtools/client/performance/test/browser_perf-options-invert-call-tree-02.js45
-rw-r--r--devtools/client/performance/test/browser_perf-options-invert-flame-graph-01.js43
-rw-r--r--devtools/client/performance/test/browser_perf-options-invert-flame-graph-02.js46
-rw-r--r--devtools/client/performance/test/browser_perf-options-propagate-allocations.js36
-rw-r--r--devtools/client/performance/test/browser_perf-options-propagate-profiler.js32
-rw-r--r--devtools/client/performance/test/browser_perf-options-show-idle-blocks-01.js43
-rw-r--r--devtools/client/performance/test/browser_perf-options-show-idle-blocks-02.js45
-rw-r--r--devtools/client/performance/test/browser_perf-options-show-jit-optimizations.js260
-rw-r--r--devtools/client/performance/test/browser_perf-options-show-platform-data-01.js43
-rw-r--r--devtools/client/performance/test/browser_perf-options-show-platform-data-02.js43
-rw-r--r--devtools/client/performance/test/browser_perf-overview-render-01.js34
-rw-r--r--devtools/client/performance/test/browser_perf-overview-render-02.js91
-rw-r--r--devtools/client/performance/test/browser_perf-overview-render-03.js76
-rw-r--r--devtools/client/performance/test/browser_perf-overview-render-04.js74
-rw-r--r--devtools/client/performance/test/browser_perf-overview-selection-01.js71
-rw-r--r--devtools/client/performance/test/browser_perf-overview-selection-02.js73
-rw-r--r--devtools/client/performance/test/browser_perf-overview-selection-03.js82
-rw-r--r--devtools/client/performance/test/browser_perf-overview-time-interval.js73
-rw-r--r--devtools/client/performance/test/browser_perf-private-browsing.js114
-rw-r--r--devtools/client/performance/test/browser_perf-range-changed-render.js81
-rw-r--r--devtools/client/performance/test/browser_perf-recording-notices-01.js45
-rw-r--r--devtools/client/performance/test/browser_perf-recording-notices-02.js65
-rw-r--r--devtools/client/performance/test/browser_perf-recording-notices-03.js135
-rw-r--r--devtools/client/performance/test/browser_perf-recording-notices-04.js66
-rw-r--r--devtools/client/performance/test/browser_perf-recording-notices-05.js54
-rw-r--r--devtools/client/performance/test/browser_perf-recording-selected-01.js45
-rw-r--r--devtools/client/performance/test/browser_perf-recording-selected-02.js58
-rw-r--r--devtools/client/performance/test/browser_perf-recording-selected-03.js44
-rw-r--r--devtools/client/performance/test/browser_perf-recording-selected-04.js59
-rw-r--r--devtools/client/performance/test/browser_perf-recordings-clear-01.js54
-rw-r--r--devtools/client/performance/test/browser_perf-recordings-clear-02.js69
-rw-r--r--devtools/client/performance/test/browser_perf-recordings-io-01.js94
-rw-r--r--devtools/client/performance/test/browser_perf-recordings-io-02.js26
-rw-r--r--devtools/client/performance/test/browser_perf-recordings-io-03.js56
-rw-r--r--devtools/client/performance/test/browser_perf-recordings-io-04.js178
-rw-r--r--devtools/client/performance/test/browser_perf-recordings-io-05.js43
-rw-r--r--devtools/client/performance/test/browser_perf-recordings-io-06.js142
-rw-r--r--devtools/client/performance/test/browser_perf-refresh.js36
-rw-r--r--devtools/client/performance/test/browser_perf-states.js102
-rw-r--r--devtools/client/performance/test/browser_perf-telemetry-01.js53
-rw-r--r--devtools/client/performance/test/browser_perf-telemetry-02.js48
-rw-r--r--devtools/client/performance/test/browser_perf-telemetry-03.js56
-rw-r--r--devtools/client/performance/test/browser_perf-telemetry-04.js50
-rw-r--r--devtools/client/performance/test/browser_perf-theme-toggle.js78
-rw-r--r--devtools/client/performance/test/browser_perf-tree-abstract-01.js154
-rw-r--r--devtools/client/performance/test/browser_perf-tree-abstract-02.js138
-rw-r--r--devtools/client/performance/test/browser_perf-tree-abstract-03.js151
-rw-r--r--devtools/client/performance/test/browser_perf-tree-abstract-04.js35
-rw-r--r--devtools/client/performance/test/browser_perf-tree-abstract-05.js103
-rw-r--r--devtools/client/performance/test/browser_perf-tree-view-01.js65
-rw-r--r--devtools/client/performance/test/browser_perf-tree-view-02.js148
-rw-r--r--devtools/client/performance/test/browser_perf-tree-view-03.js79
-rw-r--r--devtools/client/performance/test/browser_perf-tree-view-04.js78
-rw-r--r--devtools/client/performance/test/browser_perf-tree-view-05.js36
-rw-r--r--devtools/client/performance/test/browser_perf-tree-view-06.js52
-rw-r--r--devtools/client/performance/test/browser_perf-tree-view-07.js40
-rw-r--r--devtools/client/performance/test/browser_perf-tree-view-08.js109
-rw-r--r--devtools/client/performance/test/browser_perf-tree-view-09.js59
-rw-r--r--devtools/client/performance/test/browser_perf-tree-view-10.js160
-rw-r--r--devtools/client/performance/test/browser_perf-tree-view-11.js154
-rw-r--r--devtools/client/performance/test/browser_perf-ui-recording.js39
-rw-r--r--devtools/client/performance/test/browser_timeline-filters-01.js119
-rw-r--r--devtools/client/performance/test/browser_timeline-filters-02.js48
-rw-r--r--devtools/client/performance/test/browser_timeline-waterfall-background.js41
-rw-r--r--devtools/client/performance/test/browser_timeline-waterfall-generic.js105
-rw-r--r--devtools/client/performance/test/browser_timeline-waterfall-rerender.js76
-rw-r--r--devtools/client/performance/test/browser_timeline-waterfall-sidebar.js77
-rw-r--r--devtools/client/performance/test/browser_timeline-waterfall-workers.js97
-rw-r--r--devtools/client/performance/test/doc_allocs.html26
-rw-r--r--devtools/client/performance/test/doc_innerHTML.html21
-rw-r--r--devtools/client/performance/test/doc_markers.html38
-rw-r--r--devtools/client/performance/test/doc_simple-test.html27
-rw-r--r--devtools/client/performance/test/doc_worker.html29
-rw-r--r--devtools/client/performance/test/head.js93
-rw-r--r--devtools/client/performance/test/helpers/actions.js155
-rw-r--r--devtools/client/performance/test/helpers/dom-utils.js30
-rw-r--r--devtools/client/performance/test/helpers/event-utils.js114
-rw-r--r--devtools/client/performance/test/helpers/input-utils.js75
-rw-r--r--devtools/client/performance/test/helpers/moz.build20
-rw-r--r--devtools/client/performance/test/helpers/panel-utils.js106
-rw-r--r--devtools/client/performance/test/helpers/prefs.js72
-rw-r--r--devtools/client/performance/test/helpers/profiler-mm-utils.js117
-rw-r--r--devtools/client/performance/test/helpers/recording-utils.js54
-rw-r--r--devtools/client/performance/test/helpers/synth-utils.js99
-rw-r--r--devtools/client/performance/test/helpers/tab-utils.js85
-rw-r--r--devtools/client/performance/test/helpers/urls.js6
-rw-r--r--devtools/client/performance/test/helpers/wait-utils.js61
-rw-r--r--devtools/client/performance/test/js_simpleWorker.js6
-rw-r--r--devtools/client/performance/test/moz.build8
-rw-r--r--devtools/client/performance/test/unit/.eslintrc.js6
-rw-r--r--devtools/client/performance/test/unit/head.js46
-rw-r--r--devtools/client/performance/test/unit/test_frame-utils-01.js133
-rw-r--r--devtools/client/performance/test/unit/test_frame-utils-02.js59
-rw-r--r--devtools/client/performance/test/unit/test_jit-graph-data.js209
-rw-r--r--devtools/client/performance/test/unit/test_jit-model-01.js120
-rw-r--r--devtools/client/performance/test/unit/test_jit-model-02.js149
-rw-r--r--devtools/client/performance/test/unit/test_marker-blueprint.js29
-rw-r--r--devtools/client/performance/test/unit/test_marker-utils.js115
-rw-r--r--devtools/client/performance/test/unit/test_perf-utils-allocations-to-samples.js96
-rw-r--r--devtools/client/performance/test/unit/test_profiler-categories.js38
-rw-r--r--devtools/client/performance/test/unit/test_tree-model-01.js160
-rw-r--r--devtools/client/performance/test/unit/test_tree-model-02.js62
-rw-r--r--devtools/client/performance/test/unit/test_tree-model-03.js95
-rw-r--r--devtools/client/performance/test/unit/test_tree-model-04.js91
-rw-r--r--devtools/client/performance/test/unit/test_tree-model-05.js82
-rw-r--r--devtools/client/performance/test/unit/test_tree-model-06.js176
-rw-r--r--devtools/client/performance/test/unit/test_tree-model-07.js101
-rw-r--r--devtools/client/performance/test/unit/test_tree-model-08.js99
-rw-r--r--devtools/client/performance/test/unit/test_tree-model-09.js84
-rw-r--r--devtools/client/performance/test/unit/test_tree-model-10.js153
-rw-r--r--devtools/client/performance/test/unit/test_tree-model-11.js90
-rw-r--r--devtools/client/performance/test/unit/test_tree-model-12.js94
-rw-r--r--devtools/client/performance/test/unit/test_tree-model-13.js86
-rw-r--r--devtools/client/performance/test/unit/test_tree-model-allocations-01.js95
-rw-r--r--devtools/client/performance/test/unit/test_tree-model-allocations-02.js105
-rw-r--r--devtools/client/performance/test/unit/test_waterfall-utils-collapse-01.js71
-rw-r--r--devtools/client/performance/test/unit/test_waterfall-utils-collapse-02.js82
-rw-r--r--devtools/client/performance/test/unit/test_waterfall-utils-collapse-03.js64
-rw-r--r--devtools/client/performance/test/unit/test_waterfall-utils-collapse-04.js103
-rw-r--r--devtools/client/performance/test/unit/test_waterfall-utils-collapse-05.js164
-rw-r--r--devtools/client/performance/test/unit/xpcshell.ini36
-rw-r--r--devtools/client/performance/views/details-abstract-subview.js194
-rw-r--r--devtools/client/performance/views/details-js-call-tree.js193
-rw-r--r--devtools/client/performance/views/details-js-flamegraph.js125
-rw-r--r--devtools/client/performance/views/details-memory-call-tree.js130
-rw-r--r--devtools/client/performance/views/details-memory-flamegraph.js121
-rw-r--r--devtools/client/performance/views/details-waterfall.js252
-rw-r--r--devtools/client/performance/views/details.js263
-rw-r--r--devtools/client/performance/views/overview.js423
-rw-r--r--devtools/client/performance/views/recordings.js202
-rw-r--r--devtools/client/performance/views/toolbar.js160
-rw-r--r--devtools/client/preferences/devtools.js366
-rw-r--r--devtools/client/preferences/moz.build9
-rw-r--r--devtools/client/projecteditor/chrome/content/projecteditor-loader.js176
-rw-r--r--devtools/client/projecteditor/chrome/content/projecteditor-loader.xul26
-rw-r--r--devtools/client/projecteditor/chrome/content/projecteditor-test.xul18
-rw-r--r--devtools/client/projecteditor/chrome/content/projecteditor.xul87
-rw-r--r--devtools/client/projecteditor/lib/editors.js303
-rw-r--r--devtools/client/projecteditor/lib/helpers/event.js86
-rw-r--r--devtools/client/projecteditor/lib/helpers/file-picker.js116
-rw-r--r--devtools/client/projecteditor/lib/helpers/l10n.js26
-rw-r--r--devtools/client/projecteditor/lib/helpers/moz.build12
-rw-r--r--devtools/client/projecteditor/lib/helpers/prompts.js33
-rw-r--r--devtools/client/projecteditor/lib/helpers/readdir.js89
-rw-r--r--devtools/client/projecteditor/lib/moz.build19
-rw-r--r--devtools/client/projecteditor/lib/plugins/app-manager/app-project-editor.js56
-rw-r--r--devtools/client/projecteditor/lib/plugins/app-manager/moz.build10
-rw-r--r--devtools/client/projecteditor/lib/plugins/app-manager/plugin.js77
-rw-r--r--devtools/client/projecteditor/lib/plugins/core.js83
-rw-r--r--devtools/client/projecteditor/lib/plugins/delete/delete.js67
-rw-r--r--devtools/client/projecteditor/lib/plugins/delete/moz.build9
-rw-r--r--devtools/client/projecteditor/lib/plugins/dirty/dirty.js47
-rw-r--r--devtools/client/projecteditor/lib/plugins/dirty/moz.build9
-rw-r--r--devtools/client/projecteditor/lib/plugins/image-view/image-editor.js50
-rw-r--r--devtools/client/projecteditor/lib/plugins/image-view/moz.build10
-rw-r--r--devtools/client/projecteditor/lib/plugins/image-view/plugin.js28
-rw-r--r--devtools/client/projecteditor/lib/plugins/logging/logging.js29
-rw-r--r--devtools/client/projecteditor/lib/plugins/logging/moz.build9
-rw-r--r--devtools/client/projecteditor/lib/plugins/moz.build21
-rw-r--r--devtools/client/projecteditor/lib/plugins/new/moz.build9
-rw-r--r--devtools/client/projecteditor/lib/plugins/new/new.js80
-rw-r--r--devtools/client/projecteditor/lib/plugins/rename/moz.build9
-rw-r--r--devtools/client/projecteditor/lib/plugins/rename/rename.js74
-rw-r--r--devtools/client/projecteditor/lib/plugins/save/moz.build9
-rw-r--r--devtools/client/projecteditor/lib/plugins/save/save.js93
-rw-r--r--devtools/client/projecteditor/lib/plugins/status-bar/moz.build9
-rw-r--r--devtools/client/projecteditor/lib/plugins/status-bar/plugin.js105
-rw-r--r--devtools/client/projecteditor/lib/project.js246
-rw-r--r--devtools/client/projecteditor/lib/projecteditor.js816
-rw-r--r--devtools/client/projecteditor/lib/shells.js243
-rw-r--r--devtools/client/projecteditor/lib/stores/base.js58
-rw-r--r--devtools/client/projecteditor/lib/stores/local.js215
-rw-r--r--devtools/client/projecteditor/lib/stores/moz.build11
-rw-r--r--devtools/client/projecteditor/lib/stores/resource.js398
-rw-r--r--devtools/client/projecteditor/lib/tree.js593
-rw-r--r--devtools/client/projecteditor/moz.build9
-rw-r--r--devtools/client/projecteditor/test/.eslintrc.js6
-rw-r--r--devtools/client/projecteditor/test/browser.ini31
-rw-r--r--devtools/client/projecteditor/test/browser_projecteditor_app_options.js87
-rw-r--r--devtools/client/projecteditor/test/browser_projecteditor_confirm_unsaved.js60
-rw-r--r--devtools/client/projecteditor/test/browser_projecteditor_contextmenu_01.js27
-rw-r--r--devtools/client/projecteditor/test/browser_projecteditor_contextmenu_02.js66
-rw-r--r--devtools/client/projecteditor/test/browser_projecteditor_delete_file.js85
-rw-r--r--devtools/client/projecteditor/test/browser_projecteditor_editing_01.js70
-rw-r--r--devtools/client/projecteditor/test/browser_projecteditor_editors_image.js74
-rw-r--r--devtools/client/projecteditor/test/browser_projecteditor_external_change.js84
-rw-r--r--devtools/client/projecteditor/test/browser_projecteditor_immediate_destroy.js93
-rw-r--r--devtools/client/projecteditor/test/browser_projecteditor_init.js18
-rw-r--r--devtools/client/projecteditor/test/browser_projecteditor_menubar_01.js28
-rw-r--r--devtools/client/projecteditor/test/browser_projecteditor_menubar_02.js123
-rw-r--r--devtools/client/projecteditor/test/browser_projecteditor_new_file.js13
-rw-r--r--devtools/client/projecteditor/test/browser_projecteditor_rename_file_01.js19
-rw-r--r--devtools/client/projecteditor/test/browser_projecteditor_rename_file_02.js26
-rw-r--r--devtools/client/projecteditor/test/browser_projecteditor_saveall.js64
-rw-r--r--devtools/client/projecteditor/test/browser_projecteditor_stores.js16
-rw-r--r--devtools/client/projecteditor/test/browser_projecteditor_tree_selection_01.js98
-rw-r--r--devtools/client/projecteditor/test/browser_projecteditor_tree_selection_02.js76
-rw-r--r--devtools/client/projecteditor/test/head.js391
-rw-r--r--devtools/client/projecteditor/test/helper_edits.js53
-rw-r--r--devtools/client/projecteditor/test/helper_homepage.html1
-rw-r--r--devtools/client/responsive.html/actions/devices.js138
-rw-r--r--devtools/client/responsive.html/actions/display-pixel-ratio.js23
-rw-r--r--devtools/client/responsive.html/actions/index.js77
-rw-r--r--devtools/client/responsive.html/actions/location.js22
-rw-r--r--devtools/client/responsive.html/actions/moz.build16
-rw-r--r--devtools/client/responsive.html/actions/network-throttling.js21
-rw-r--r--devtools/client/responsive.html/actions/screenshot.js82
-rw-r--r--devtools/client/responsive.html/actions/touch-simulation.js22
-rw-r--r--devtools/client/responsive.html/actions/viewports.js81
-rw-r--r--devtools/client/responsive.html/app.js209
-rw-r--r--devtools/client/responsive.html/browser/moz.build11
-rw-r--r--devtools/client/responsive.html/browser/swap.js309
-rw-r--r--devtools/client/responsive.html/browser/tunnel.js619
-rw-r--r--devtools/client/responsive.html/browser/web-navigation.js179
-rw-r--r--devtools/client/responsive.html/components/browser.js149
-rw-r--r--devtools/client/responsive.html/components/device-modal.js181
-rw-r--r--devtools/client/responsive.html/components/device-selector.js122
-rw-r--r--devtools/client/responsive.html/components/dpr-selector.js131
-rw-r--r--devtools/client/responsive.html/components/global-toolbar.js101
-rw-r--r--devtools/client/responsive.html/components/moz.build19
-rw-r--r--devtools/client/responsive.html/components/network-throttling-selector.js92
-rw-r--r--devtools/client/responsive.html/components/resizable-viewport.js195
-rw-r--r--devtools/client/responsive.html/components/viewport-dimension.js173
-rw-r--r--devtools/client/responsive.html/components/viewport-toolbar.js55
-rw-r--r--devtools/client/responsive.html/components/viewport.js114
-rw-r--r--devtools/client/responsive.html/components/viewports.js70
-rw-r--r--devtools/client/responsive.html/constants.js8
-rw-r--r--devtools/client/responsive.html/docs/browser-swap.md146
-rw-r--r--devtools/client/responsive.html/images/close.svg6
-rw-r--r--devtools/client/responsive.html/images/grippers.svg6
-rw-r--r--devtools/client/responsive.html/images/moz.build14
-rw-r--r--devtools/client/responsive.html/images/rotate-viewport.svg6
-rw-r--r--devtools/client/responsive.html/images/screenshot.svg7
-rw-r--r--devtools/client/responsive.html/images/select-arrow.svg37
-rw-r--r--devtools/client/responsive.html/images/touch-events.svg6
-rw-r--r--devtools/client/responsive.html/index.css521
-rw-r--r--devtools/client/responsive.html/index.js166
-rw-r--r--devtools/client/responsive.html/index.xhtml19
-rw-r--r--devtools/client/responsive.html/manager.js597
-rw-r--r--devtools/client/responsive.html/moz.build28
-rw-r--r--devtools/client/responsive.html/reducers.js13
-rw-r--r--devtools/client/responsive.html/reducers/devices.js86
-rw-r--r--devtools/client/responsive.html/reducers/display-pixel-ratio.js26
-rw-r--r--devtools/client/responsive.html/reducers/location.js25
-rw-r--r--devtools/client/responsive.html/reducers/moz.build15
-rw-r--r--devtools/client/responsive.html/reducers/network-throttling.js33
-rw-r--r--devtools/client/responsive.html/reducers/screenshot.js31
-rw-r--r--devtools/client/responsive.html/reducers/touch-simulation.js31
-rw-r--r--devtools/client/responsive.html/reducers/viewports.js118
-rw-r--r--devtools/client/responsive.html/responsive-ua.css6
-rw-r--r--devtools/client/responsive.html/store.js33
-rw-r--r--devtools/client/responsive.html/test/browser/.eslintrc.js6
-rw-r--r--devtools/client/responsive.html/test/browser/browser.ini44
-rw-r--r--devtools/client/responsive.html/test/browser/browser_device_change.js95
-rw-r--r--devtools/client/responsive.html/test/browser/browser_device_modal_error.js35
-rw-r--r--devtools/client/responsive.html/test/browser/browser_device_modal_exit.js45
-rw-r--r--devtools/client/responsive.html/test/browser/browser_device_modal_submit.js146
-rw-r--r--devtools/client/responsive.html/test/browser/browser_device_width.js66
-rw-r--r--devtools/client/responsive.html/test/browser/browser_dpr_change.js140
-rw-r--r--devtools/client/responsive.html/test/browser/browser_exit_button.js70
-rw-r--r--devtools/client/responsive.html/test/browser/browser_frame_script_active.js46
-rw-r--r--devtools/client/responsive.html/test/browser/browser_menu_item_01.js62
-rw-r--r--devtools/client/responsive.html/test/browser/browser_menu_item_02.js49
-rw-r--r--devtools/client/responsive.html/test/browser/browser_mouse_resize.js27
-rw-r--r--devtools/client/responsive.html/test/browser/browser_navigation.js98
-rw-r--r--devtools/client/responsive.html/test/browser/browser_network_throttling.js56
-rw-r--r--devtools/client/responsive.html/test/browser/browser_page_state.js76
-rw-r--r--devtools/client/responsive.html/test/browser/browser_permission_doorhanger.js52
-rw-r--r--devtools/client/responsive.html/test/browser/browser_resize_cmd.js148
-rw-r--r--devtools/client/responsive.html/test/browser/browser_screenshot_button.js59
-rw-r--r--devtools/client/responsive.html/test/browser/browser_tab_close.js43
-rw-r--r--devtools/client/responsive.html/test/browser/browser_tab_remoteness_change.js45
-rw-r--r--devtools/client/responsive.html/test/browser/browser_toolbox_computed_view.js63
-rw-r--r--devtools/client/responsive.html/test/browser/browser_toolbox_rule_view.js78
-rw-r--r--devtools/client/responsive.html/test/browser/browser_toolbox_swap_browsers.js128
-rw-r--r--devtools/client/responsive.html/test/browser/browser_touch_device.js77
-rw-r--r--devtools/client/responsive.html/test/browser/browser_touch_simulation.js228
-rw-r--r--devtools/client/responsive.html/test/browser/browser_viewport_basics.js30
-rw-r--r--devtools/client/responsive.html/test/browser/browser_window_close.js25
-rw-r--r--devtools/client/responsive.html/test/browser/devices.json651
-rw-r--r--devtools/client/responsive.html/test/browser/doc_page_state.html16
-rw-r--r--devtools/client/responsive.html/test/browser/geolocation.html13
-rw-r--r--devtools/client/responsive.html/test/browser/head.js401
-rw-r--r--devtools/client/responsive.html/test/browser/touch.html86
-rw-r--r--devtools/client/responsive.html/test/unit/.eslintrc.js6
-rw-r--r--devtools/client/responsive.html/test/unit/head.js21
-rw-r--r--devtools/client/responsive.html/test/unit/test_add_device.js35
-rw-r--r--devtools/client/responsive.html/test/unit/test_add_device_type.js22
-rw-r--r--devtools/client/responsive.html/test/unit/test_add_viewport.js23
-rw-r--r--devtools/client/responsive.html/test/unit/test_change_device.js42
-rw-r--r--devtools/client/responsive.html/test/unit/test_change_display_pixel_ratio.js22
-rw-r--r--devtools/client/responsive.html/test/unit/test_change_location.js22
-rw-r--r--devtools/client/responsive.html/test/unit/test_change_network_throttling.js27
-rw-r--r--devtools/client/responsive.html/test/unit/test_change_pixel_ratio.js22
-rw-r--r--devtools/client/responsive.html/test/unit/test_resize_viewport.js21
-rw-r--r--devtools/client/responsive.html/test/unit/test_rotate_viewport.js25
-rw-r--r--devtools/client/responsive.html/test/unit/test_update_device_displayed.js37
-rw-r--r--devtools/client/responsive.html/test/unit/test_update_touch_simulation_enabled.js23
-rw-r--r--devtools/client/responsive.html/test/unit/xpcshell.ini18
-rw-r--r--devtools/client/responsive.html/types.js164
-rw-r--r--devtools/client/responsive.html/utils/e10s.js103
-rw-r--r--devtools/client/responsive.html/utils/enum.js21
-rw-r--r--devtools/client/responsive.html/utils/l10n.js16
-rw-r--r--devtools/client/responsive.html/utils/message.js38
-rw-r--r--devtools/client/responsive.html/utils/moz.build12
-rw-r--r--devtools/client/responsivedesign/moz.build11
-rw-r--r--devtools/client/responsivedesign/resize-commands.js96
-rw-r--r--devtools/client/responsivedesign/responsivedesign-child.js195
-rw-r--r--devtools/client/responsivedesign/responsivedesign.jsm1193
-rw-r--r--devtools/client/responsivedesign/test/.eslintrc.js10
-rw-r--r--devtools/client/responsivedesign/test/browser.ini21
-rw-r--r--devtools/client/responsivedesign/test/browser_responsive_cmd.js143
-rw-r--r--devtools/client/responsivedesign/test/browser_responsive_devicewidth.js68
-rw-r--r--devtools/client/responsivedesign/test/browser_responsivecomputedview.js67
-rw-r--r--devtools/client/responsivedesign/test/browser_responsiveruleview.js95
-rw-r--r--devtools/client/responsivedesign/test/browser_responsiveui.js250
-rw-r--r--devtools/client/responsivedesign/test/browser_responsiveui_customuseragent.js56
-rw-r--r--devtools/client/responsivedesign/test/browser_responsiveui_touch.js148
-rw-r--r--devtools/client/responsivedesign/test/browser_responsiveui_window_close.js25
-rw-r--r--devtools/client/responsivedesign/test/browser_responsiveuiaddcustompreset.js121
-rw-r--r--devtools/client/responsivedesign/test/head.js302
-rw-r--r--devtools/client/responsivedesign/test/touch.html85
-rw-r--r--devtools/client/scratchpad/moz.build13
-rw-r--r--devtools/client/scratchpad/scratchpad-commands.js22
-rw-r--r--devtools/client/scratchpad/scratchpad-manager.jsm185
-rw-r--r--devtools/client/scratchpad/scratchpad-panel.js56
-rw-r--r--devtools/client/scratchpad/scratchpad.js2480
-rw-r--r--devtools/client/scratchpad/scratchpad.xul412
-rw-r--r--devtools/client/scratchpad/test/.eslintrc.js6
-rw-r--r--devtools/client/scratchpad/test/NS_ERROR_ILLEGAL_INPUT.txt2
-rw-r--r--devtools/client/scratchpad/test/browser.ini46
-rw-r--r--devtools/client/scratchpad/test/browser_scratchpad_autocomplete.js66
-rw-r--r--devtools/client/scratchpad/test/browser_scratchpad_browser_last_window_closing.js79
-rw-r--r--devtools/client/scratchpad/test/browser_scratchpad_chrome_context_pref.js50
-rw-r--r--devtools/client/scratchpad/test/browser_scratchpad_close_toolbox.js38
-rw-r--r--devtools/client/scratchpad/test/browser_scratchpad_confirm_close.js230
-rw-r--r--devtools/client/scratchpad/test/browser_scratchpad_contexts.js149
-rw-r--r--devtools/client/scratchpad/test/browser_scratchpad_disable_view_menu_items.js66
-rw-r--r--devtools/client/scratchpad/test/browser_scratchpad_display_non_error_exceptions.js110
-rw-r--r--devtools/client/scratchpad/test/browser_scratchpad_display_outputs_errors.js72
-rw-r--r--devtools/client/scratchpad/test/browser_scratchpad_edit_ui_updates.js206
-rw-r--r--devtools/client/scratchpad/test/browser_scratchpad_eval_func.js86
-rw-r--r--devtools/client/scratchpad/test/browser_scratchpad_execute_print.js116
-rw-r--r--devtools/client/scratchpad/test/browser_scratchpad_falsy.js69
-rw-r--r--devtools/client/scratchpad/test/browser_scratchpad_files.js119
-rw-r--r--devtools/client/scratchpad/test/browser_scratchpad_goto_line_ui.js43
-rw-r--r--devtools/client/scratchpad/test/browser_scratchpad_help_key.js59
-rw-r--r--devtools/client/scratchpad/test/browser_scratchpad_initialization.js50
-rw-r--r--devtools/client/scratchpad/test/browser_scratchpad_inspect.js55
-rw-r--r--devtools/client/scratchpad/test/browser_scratchpad_inspect_primitives.js61
-rw-r--r--devtools/client/scratchpad/test/browser_scratchpad_long_string.js30
-rw-r--r--devtools/client/scratchpad/test/browser_scratchpad_modeline.js87
-rw-r--r--devtools/client/scratchpad/test/browser_scratchpad_open.js101
-rw-r--r--devtools/client/scratchpad/test/browser_scratchpad_open_error_console.js39
-rw-r--r--devtools/client/scratchpad/test/browser_scratchpad_pprint-02.js40
-rw-r--r--devtools/client/scratchpad/test/browser_scratchpad_pprint.js29
-rw-r--r--devtools/client/scratchpad/test/browser_scratchpad_pprint_error_goto_line.js78
-rw-r--r--devtools/client/scratchpad/test/browser_scratchpad_recent_files.js350
-rw-r--r--devtools/client/scratchpad/test/browser_scratchpad_reload_and_run.js76
-rw-r--r--devtools/client/scratchpad/test/browser_scratchpad_remember_view_options.js65
-rw-r--r--devtools/client/scratchpad/test/browser_scratchpad_reset_undo.js155
-rw-r--r--devtools/client/scratchpad/test/browser_scratchpad_restore.js96
-rw-r--r--devtools/client/scratchpad/test/browser_scratchpad_revert_to_saved.js134
-rw-r--r--devtools/client/scratchpad/test/browser_scratchpad_run_error_goto_line.js60
-rw-r--r--devtools/client/scratchpad/test/browser_scratchpad_tab.js75
-rw-r--r--devtools/client/scratchpad/test/browser_scratchpad_tab_switch.js103
-rw-r--r--devtools/client/scratchpad/test/browser_scratchpad_throw_output.js52
-rw-r--r--devtools/client/scratchpad/test/browser_scratchpad_ui.js74
-rw-r--r--devtools/client/scratchpad/test/browser_scratchpad_unsaved.js119
-rw-r--r--devtools/client/scratchpad/test/browser_scratchpad_wrong_window_focus.js93
-rw-r--r--devtools/client/scratchpad/test/head.js221
-rw-r--r--devtools/client/shadereditor/moz.build10
-rw-r--r--devtools/client/shadereditor/panel.js76
-rw-r--r--devtools/client/shadereditor/shadereditor.js633
-rw-r--r--devtools/client/shadereditor/shadereditor.xul70
-rw-r--r--devtools/client/shadereditor/test/.eslintrc.js6
-rw-r--r--devtools/client/shadereditor/test/browser.ini47
-rw-r--r--devtools/client/shadereditor/test/browser_se_aaa_run_first_leaktest.js17
-rw-r--r--devtools/client/shadereditor/test/browser_se_bfcache.js60
-rw-r--r--devtools/client/shadereditor/test/browser_se_editors-contents.js30
-rw-r--r--devtools/client/shadereditor/test/browser_se_editors-error-gutter.js156
-rw-r--r--devtools/client/shadereditor/test/browser_se_editors-error-tooltip.js56
-rw-r--r--devtools/client/shadereditor/test/browser_se_editors-lazy-init.js34
-rw-r--r--devtools/client/shadereditor/test/browser_se_first-run.js43
-rw-r--r--devtools/client/shadereditor/test/browser_se_navigation.js71
-rw-r--r--devtools/client/shadereditor/test/browser_se_programs-blackbox-01.js169
-rw-r--r--devtools/client/shadereditor/test/browser_se_programs-blackbox-02.js63
-rw-r--r--devtools/client/shadereditor/test/browser_se_programs-cache.js41
-rw-r--r--devtools/client/shadereditor/test/browser_se_programs-highlight-01.js93
-rw-r--r--devtools/client/shadereditor/test/browser_se_programs-highlight-02.js49
-rw-r--r--devtools/client/shadereditor/test/browser_se_programs-list.js87
-rw-r--r--devtools/client/shadereditor/test/browser_se_shaders-edit-01.js73
-rw-r--r--devtools/client/shadereditor/test/browser_se_shaders-edit-02.js74
-rw-r--r--devtools/client/shadereditor/test/browser_se_shaders-edit-03.js85
-rw-r--r--devtools/client/shadereditor/test/browser_webgl-actor-test-01.js16
-rw-r--r--devtools/client/shadereditor/test/browser_webgl-actor-test-02.js21
-rw-r--r--devtools/client/shadereditor/test/browser_webgl-actor-test-03.js26
-rw-r--r--devtools/client/shadereditor/test/browser_webgl-actor-test-04.js27
-rw-r--r--devtools/client/shadereditor/test/browser_webgl-actor-test-05.js27
-rw-r--r--devtools/client/shadereditor/test/browser_webgl-actor-test-06.js64
-rw-r--r--devtools/client/shadereditor/test/browser_webgl-actor-test-07.js61
-rw-r--r--devtools/client/shadereditor/test/browser_webgl-actor-test-08.js37
-rw-r--r--devtools/client/shadereditor/test/browser_webgl-actor-test-09.js89
-rw-r--r--devtools/client/shadereditor/test/browser_webgl-actor-test-10.js44
-rw-r--r--devtools/client/shadereditor/test/browser_webgl-actor-test-11.js25
-rw-r--r--devtools/client/shadereditor/test/browser_webgl-actor-test-12.js27
-rw-r--r--devtools/client/shadereditor/test/browser_webgl-actor-test-13.js67
-rw-r--r--devtools/client/shadereditor/test/browser_webgl-actor-test-14.js46
-rw-r--r--devtools/client/shadereditor/test/browser_webgl-actor-test-15.js133
-rw-r--r--devtools/client/shadereditor/test/browser_webgl-actor-test-16.js141
-rw-r--r--devtools/client/shadereditor/test/browser_webgl-actor-test-17.js46
-rw-r--r--devtools/client/shadereditor/test/browser_webgl-actor-test-18.js31
-rw-r--r--devtools/client/shadereditor/test/doc_blended-geometry.html136
-rw-r--r--devtools/client/shadereditor/test/doc_multiple-contexts.html112
-rw-r--r--devtools/client/shadereditor/test/doc_overlapping-geometry.html120
-rw-r--r--devtools/client/shadereditor/test/doc_shader-order.html83
-rw-r--r--devtools/client/shadereditor/test/doc_simple-canvas.html125
-rw-r--r--devtools/client/shadereditor/test/head.js292
-rw-r--r--devtools/client/shared/AppCacheUtils.jsm631
-rw-r--r--devtools/client/shared/DOMHelpers.jsm166
-rw-r--r--devtools/client/shared/Jsbeautify.jsm16
-rw-r--r--devtools/client/shared/SplitView.jsm312
-rw-r--r--devtools/client/shared/autocomplete-popup.js599
-rw-r--r--devtools/client/shared/browser-loader.js235
-rw-r--r--devtools/client/shared/components/.eslintrc.js7
-rw-r--r--devtools/client/shared/components/frame.js239
-rw-r--r--devtools/client/shared/components/h-split-box.js154
-rw-r--r--devtools/client/shared/components/moz.build27
-rw-r--r--devtools/client/shared/components/notification-box.css95
-rw-r--r--devtools/client/shared/components/notification-box.js263
-rw-r--r--devtools/client/shared/components/reps/array.js186
-rw-r--r--devtools/client/shared/components/reps/attribute.js70
-rw-r--r--devtools/client/shared/components/reps/caption.js31
-rw-r--r--devtools/client/shared/components/reps/comment-node.js60
-rw-r--r--devtools/client/shared/components/reps/date-time.js70
-rw-r--r--devtools/client/shared/components/reps/document.js78
-rw-r--r--devtools/client/shared/components/reps/element-node.js114
-rw-r--r--devtools/client/shared/components/reps/event.js81
-rw-r--r--devtools/client/shared/components/reps/function.js73
-rw-r--r--devtools/client/shared/components/reps/grip-array.js198
-rw-r--r--devtools/client/shared/components/reps/grip-map.js193
-rw-r--r--devtools/client/shared/components/reps/grip.js247
-rw-r--r--devtools/client/shared/components/reps/infinity.js41
-rw-r--r--devtools/client/shared/components/reps/long-string.js71
-rw-r--r--devtools/client/shared/components/reps/moz.build40
-rw-r--r--devtools/client/shared/components/reps/nan.js41
-rw-r--r--devtools/client/shared/components/reps/null.js46
-rw-r--r--devtools/client/shared/components/reps/number.js51
-rw-r--r--devtools/client/shared/components/reps/object-with-text.js76
-rw-r--r--devtools/client/shared/components/reps/object-with-url.js76
-rw-r--r--devtools/client/shared/components/reps/object.js171
-rw-r--r--devtools/client/shared/components/reps/promise.js111
-rw-r--r--devtools/client/shared/components/reps/prop-rep.js70
-rw-r--r--devtools/client/shared/components/reps/regexp.js63
-rw-r--r--devtools/client/shared/components/reps/rep-utils.js160
-rw-r--r--devtools/client/shared/components/reps/rep.js144
-rw-r--r--devtools/client/shared/components/reps/reps.css174
-rw-r--r--devtools/client/shared/components/reps/string.js69
-rw-r--r--devtools/client/shared/components/reps/stylesheet.js77
-rw-r--r--devtools/client/shared/components/reps/symbol.js48
-rw-r--r--devtools/client/shared/components/reps/text-node.js94
-rw-r--r--devtools/client/shared/components/reps/undefined.js46
-rw-r--r--devtools/client/shared/components/reps/window.js73
-rw-r--r--devtools/client/shared/components/search-box.js110
-rw-r--r--devtools/client/shared/components/sidebar-toggle.css32
-rw-r--r--devtools/client/shared/components/sidebar-toggle.js66
-rw-r--r--devtools/client/shared/components/splitter/draggable.js54
-rw-r--r--devtools/client/shared/components/splitter/moz.build11
-rw-r--r--devtools/client/shared/components/splitter/split-box.css88
-rw-r--r--devtools/client/shared/components/splitter/split-box.js205
-rw-r--r--devtools/client/shared/components/stack-trace.js68
-rw-r--r--devtools/client/shared/components/tabs/moz.build12
-rw-r--r--devtools/client/shared/components/tabs/tabbar.css53
-rw-r--r--devtools/client/shared/components/tabs/tabbar.js204
-rw-r--r--devtools/client/shared/components/tabs/tabs.css183
-rw-r--r--devtools/client/shared/components/tabs/tabs.js369
-rw-r--r--devtools/client/shared/components/test/browser/.eslintrc.js6
-rw-r--r--devtools/client/shared/components/test/browser/browser.ini7
-rw-r--r--devtools/client/shared/components/test/browser/browser_notification_box_basic.js36
-rw-r--r--devtools/client/shared/components/test/mochitest/.eslintrc.js6
-rw-r--r--devtools/client/shared/components/test/mochitest/chrome.ini51
-rw-r--r--devtools/client/shared/components/test/mochitest/head.js217
-rw-r--r--devtools/client/shared/components/test/mochitest/test_HSplitBox_01.html126
-rw-r--r--devtools/client/shared/components/test/mochitest/test_frame_01.html309
-rw-r--r--devtools/client/shared/components/test/mochitest/test_notification_box_01.html108
-rw-r--r--devtools/client/shared/components/test/mochitest/test_notification_box_02.html70
-rw-r--r--devtools/client/shared/components/test/mochitest/test_notification_box_03.html84
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_array.html259
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_attribute.html56
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_comment-node.html80
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_date-time.html79
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_document.html56
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_element-node.html341
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_event.html300
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_function.html206
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_grip-array.html707
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_grip-map.html405
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_grip.html887
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_infinity.html73
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_long-string.html125
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_nan.html48
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_null.html44
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_number.html97
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_object-with-text.html54
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_object-with-url.html60
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_object.html225
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_promise.html333
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_regexp.html51
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_string.html79
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_stylesheet.html54
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_symbol.html77
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_text-node.html115
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_undefined.html47
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_window.html58
-rw-r--r--devtools/client/shared/components/test/mochitest/test_sidebar_toggle.html56
-rw-r--r--devtools/client/shared/components/test/mochitest/test_stack-trace.html102
-rw-r--r--devtools/client/shared/components/test/mochitest/test_tabs_accessibility.html79
-rw-r--r--devtools/client/shared/components/test/mochitest/test_tabs_menu.html81
-rw-r--r--devtools/client/shared/components/test/mochitest/test_tree_01.html64
-rw-r--r--devtools/client/shared/components/test/mochitest/test_tree_02.html45
-rw-r--r--devtools/client/shared/components/test/mochitest/test_tree_03.html46
-rw-r--r--devtools/client/shared/components/test/mochitest/test_tree_04.html128
-rw-r--r--devtools/client/shared/components/test/mochitest/test_tree_05.html83
-rw-r--r--devtools/client/shared/components/test/mochitest/test_tree_06.html320
-rw-r--r--devtools/client/shared/components/test/mochitest/test_tree_07.html64
-rw-r--r--devtools/client/shared/components/test/mochitest/test_tree_08.html51
-rw-r--r--devtools/client/shared/components/test/mochitest/test_tree_09.html77
-rw-r--r--devtools/client/shared/components/test/mochitest/test_tree_10.html52
-rw-r--r--devtools/client/shared/components/test/mochitest/test_tree_11.html92
-rw-r--r--devtools/client/shared/components/tree.js773
-rw-r--r--devtools/client/shared/components/tree/label-cell.js66
-rw-r--r--devtools/client/shared/components/tree/moz.build14
-rw-r--r--devtools/client/shared/components/tree/object-provider.js90
-rw-r--r--devtools/client/shared/components/tree/tree-cell.js101
-rw-r--r--devtools/client/shared/components/tree/tree-header.js100
-rw-r--r--devtools/client/shared/components/tree/tree-row.js184
-rw-r--r--devtools/client/shared/components/tree/tree-view.css157
-rw-r--r--devtools/client/shared/components/tree/tree-view.js352
-rw-r--r--devtools/client/shared/css-angle.js345
-rw-r--r--devtools/client/shared/css-reload.js142
-rw-r--r--devtools/client/shared/curl.js401
-rw-r--r--devtools/client/shared/demangle.js64
-rw-r--r--devtools/client/shared/developer-toolbar.js1397
-rw-r--r--devtools/client/shared/devices.js88
-rw-r--r--devtools/client/shared/devtools-file-watcher.js78
-rw-r--r--devtools/client/shared/doorhanger.js164
-rw-r--r--devtools/client/shared/file-watcher-worker.js81
-rw-r--r--devtools/client/shared/file-watcher.js28
-rw-r--r--devtools/client/shared/frame-script-utils.js206
-rw-r--r--devtools/client/shared/getjson.js76
-rw-r--r--devtools/client/shared/inplace-editor.js1566
-rw-r--r--devtools/client/shared/key-shortcuts.js251
-rw-r--r--devtools/client/shared/keycodes.js146
-rw-r--r--devtools/client/shared/moz.build54
-rw-r--r--devtools/client/shared/network-throttling-profiles.js68
-rw-r--r--devtools/client/shared/node-attribute-parser.js294
-rw-r--r--devtools/client/shared/options-view.js186
-rw-r--r--devtools/client/shared/output-parser.js695
-rw-r--r--devtools/client/shared/poller.js114
-rw-r--r--devtools/client/shared/prefs.js178
-rw-r--r--devtools/client/shared/redux/create-store.js51
-rw-r--r--devtools/client/shared/redux/middleware/history.js23
-rw-r--r--devtools/client/shared/redux/middleware/log.js17
-rw-r--r--devtools/client/shared/redux/middleware/moz.build16
-rw-r--r--devtools/client/shared/redux/middleware/promise.js54
-rw-r--r--devtools/client/shared/redux/middleware/task.js42
-rw-r--r--devtools/client/shared/redux/middleware/test/.eslintrc.js17
-rw-r--r--devtools/client/shared/redux/middleware/test/head.js27
-rw-r--r--devtools/client/shared/redux/middleware/test/test_middleware-task-01.js56
-rw-r--r--devtools/client/shared/redux/middleware/test/test_middleware-task-02.js67
-rw-r--r--devtools/client/shared/redux/middleware/test/test_middleware-task-03.js42
-rw-r--r--devtools/client/shared/redux/middleware/test/xpcshell.ini10
-rw-r--r--devtools/client/shared/redux/middleware/thunk.js19
-rw-r--r--devtools/client/shared/redux/middleware/wait-service.js64
-rw-r--r--devtools/client/shared/redux/moz.build14
-rw-r--r--devtools/client/shared/redux/non-react-subscriber.js153
-rw-r--r--devtools/client/shared/scroll.js52
-rw-r--r--devtools/client/shared/shim/Services.js620
-rw-r--r--devtools/client/shared/shim/moz.build13
-rw-r--r--devtools/client/shared/shim/test/.eslintrc.js6
-rw-r--r--devtools/client/shared/shim/test/file_service_wm.html20
-rw-r--r--devtools/client/shared/shim/test/mochitest.ini10
-rw-r--r--devtools/client/shared/shim/test/prefs-wrapper.js80
-rw-r--r--devtools/client/shared/shim/test/test_service_appinfo.html29
-rw-r--r--devtools/client/shared/shim/test/test_service_focus.html78
-rw-r--r--devtools/client/shared/shim/test/test_service_prefs.html244
-rw-r--r--devtools/client/shared/shim/test/test_service_prefs_defaults.html71
-rw-r--r--devtools/client/shared/shim/test/test_service_wm.html36
-rw-r--r--devtools/client/shared/source-utils.js328
-rw-r--r--devtools/client/shared/splitview.css83
-rw-r--r--devtools/client/shared/suggestion-picker.js176
-rw-r--r--devtools/client/shared/telemetry.js341
-rw-r--r--devtools/client/shared/test/.eslintrc.js9
-rw-r--r--devtools/client/shared/test/browser.ini188
-rw-r--r--devtools/client/shared/test/browser_css_angle.js176
-rw-r--r--devtools/client/shared/test/browser_css_color.js137
-rw-r--r--devtools/client/shared/test/browser_cubic-bezier-01.js38
-rw-r--r--devtools/client/shared/test/browser_cubic-bezier-02.js200
-rw-r--r--devtools/client/shared/test/browser_cubic-bezier-03.js68
-rw-r--r--devtools/client/shared/test/browser_cubic-bezier-04.js50
-rw-r--r--devtools/client/shared/test/browser_cubic-bezier-05.js48
-rw-r--r--devtools/client/shared/test/browser_cubic-bezier-06.js79
-rw-r--r--devtools/client/shared/test/browser_devices.js57
-rw-r--r--devtools/client/shared/test/browser_devices.json23
-rw-r--r--devtools/client/shared/test/browser_filter-editor-01.js114
-rw-r--r--devtools/client/shared/test/browser_filter-editor-02.js107
-rw-r--r--devtools/client/shared/test/browser_filter-editor-03.js65
-rw-r--r--devtools/client/shared/test/browser_filter-editor-04.js87
-rw-r--r--devtools/client/shared/test/browser_filter-editor-05.js148
-rw-r--r--devtools/client/shared/test/browser_filter-editor-06.js71
-rw-r--r--devtools/client/shared/test/browser_filter-editor-07.js27
-rw-r--r--devtools/client/shared/test/browser_filter-editor-08.js84
-rw-r--r--devtools/client/shared/test/browser_filter-editor-09.js125
-rw-r--r--devtools/client/shared/test/browser_filter-editor-10.js87
-rw-r--r--devtools/client/shared/test/browser_filter-presets-01.js99
-rw-r--r--devtools/client/shared/test/browser_filter-presets-02.js45
-rw-r--r--devtools/client/shared/test/browser_filter-presets-03.js40
-rw-r--r--devtools/client/shared/test/browser_flame-graph-01.js61
-rw-r--r--devtools/client/shared/test/browser_flame-graph-02.js44
-rw-r--r--devtools/client/shared/test/browser_flame-graph-03a.js138
-rw-r--r--devtools/client/shared/test/browser_flame-graph-03b.js92
-rw-r--r--devtools/client/shared/test/browser_flame-graph-03c.js155
-rw-r--r--devtools/client/shared/test/browser_flame-graph-04.js90
-rw-r--r--devtools/client/shared/test/browser_flame-graph-05.js113
-rw-r--r--devtools/client/shared/test/browser_flame-graph-utils-01.js256
-rw-r--r--devtools/client/shared/test/browser_flame-graph-utils-02.js130
-rw-r--r--devtools/client/shared/test/browser_flame-graph-utils-03.js136
-rw-r--r--devtools/client/shared/test/browser_flame-graph-utils-04.js188
-rw-r--r--devtools/client/shared/test/browser_flame-graph-utils-05.js48
-rw-r--r--devtools/client/shared/test/browser_flame-graph-utils-06.js117
-rw-r--r--devtools/client/shared/test/browser_flame-graph-utils-hash.js24
-rw-r--r--devtools/client/shared/test/browser_graphs-01.js70
-rw-r--r--devtools/client/shared/test/browser_graphs-02.js107
-rw-r--r--devtools/client/shared/test/browser_graphs-03.js111
-rw-r--r--devtools/client/shared/test/browser_graphs-04.js69
-rw-r--r--devtools/client/shared/test/browser_graphs-05.js154
-rw-r--r--devtools/client/shared/test/browser_graphs-06.js112
-rw-r--r--devtools/client/shared/test/browser_graphs-07a.js232
-rw-r--r--devtools/client/shared/test/browser_graphs-07b.js88
-rw-r--r--devtools/client/shared/test/browser_graphs-07c.js139
-rw-r--r--devtools/client/shared/test/browser_graphs-07d.js71
-rw-r--r--devtools/client/shared/test/browser_graphs-07e.js127
-rw-r--r--devtools/client/shared/test/browser_graphs-08.js88
-rw-r--r--devtools/client/shared/test/browser_graphs-09a.js104
-rw-r--r--devtools/client/shared/test/browser_graphs-09b.js82
-rw-r--r--devtools/client/shared/test/browser_graphs-09c.js38
-rw-r--r--devtools/client/shared/test/browser_graphs-09d.js39
-rw-r--r--devtools/client/shared/test/browser_graphs-09e.js84
-rw-r--r--devtools/client/shared/test/browser_graphs-09f.js53
-rw-r--r--devtools/client/shared/test/browser_graphs-10a.js162
-rw-r--r--devtools/client/shared/test/browser_graphs-10b.js71
-rw-r--r--devtools/client/shared/test/browser_graphs-10c.js109
-rw-r--r--devtools/client/shared/test/browser_graphs-11a.js60
-rw-r--r--devtools/client/shared/test/browser_graphs-11b.js133
-rw-r--r--devtools/client/shared/test/browser_graphs-12.js157
-rw-r--r--devtools/client/shared/test/browser_graphs-13.js44
-rw-r--r--devtools/client/shared/test/browser_graphs-14.js111
-rw-r--r--devtools/client/shared/test/browser_graphs-15.js49
-rw-r--r--devtools/client/shared/test/browser_graphs-16.js45
-rw-r--r--devtools/client/shared/test/browser_html_tooltip-01.js88
-rw-r--r--devtools/client/shared/test/browser_html_tooltip-02.js174
-rw-r--r--devtools/client/shared/test/browser_html_tooltip-03.js155
-rw-r--r--devtools/client/shared/test/browser_html_tooltip-04.js110
-rw-r--r--devtools/client/shared/test/browser_html_tooltip-05.js109
-rw-r--r--devtools/client/shared/test/browser_html_tooltip_arrow-01.js108
-rw-r--r--devtools/client/shared/test/browser_html_tooltip_arrow-02.js100
-rw-r--r--devtools/client/shared/test/browser_html_tooltip_consecutive-show.js71
-rw-r--r--devtools/client/shared/test/browser_html_tooltip_hover.js65
-rw-r--r--devtools/client/shared/test/browser_html_tooltip_offset.js99
-rw-r--r--devtools/client/shared/test/browser_html_tooltip_rtl.js140
-rw-r--r--devtools/client/shared/test/browser_html_tooltip_variable-height.js75
-rw-r--r--devtools/client/shared/test/browser_html_tooltip_width-auto.js61
-rw-r--r--devtools/client/shared/test/browser_html_tooltip_xul-wrapper.js80
-rw-r--r--devtools/client/shared/test/browser_inplace-editor-01.js150
-rw-r--r--devtools/client/shared/test/browser_inplace-editor-02.js71
-rw-r--r--devtools/client/shared/test/browser_inplace-editor_autocomplete_01.js75
-rw-r--r--devtools/client/shared/test/browser_inplace-editor_autocomplete_02.js80
-rw-r--r--devtools/client/shared/test/browser_inplace-editor_autocomplete_offset.js119
-rw-r--r--devtools/client/shared/test/browser_inplace-editor_maxwidth.js114
-rw-r--r--devtools/client/shared/test/browser_key_shortcuts.js425
-rw-r--r--devtools/client/shared/test/browser_keycodes.js12
-rw-r--r--devtools/client/shared/test/browser_layoutHelpers-getBoxQuads.html65
-rw-r--r--devtools/client/shared/test/browser_layoutHelpers-getBoxQuads.js219
-rw-r--r--devtools/client/shared/test/browser_layoutHelpers.html24
-rw-r--r--devtools/client/shared/test/browser_layoutHelpers.js93
-rw-r--r--devtools/client/shared/test/browser_mdn-docs-01.js168
-rw-r--r--devtools/client/shared/test/browser_mdn-docs-02.js128
-rw-r--r--devtools/client/shared/test/browser_mdn-docs-03.js277
-rw-r--r--devtools/client/shared/test/browser_num-l10n.js27
-rw-r--r--devtools/client/shared/test/browser_options-view-01.js110
-rw-r--r--devtools/client/shared/test/browser_outputparser.js292
-rw-r--r--devtools/client/shared/test/browser_poller.js136
-rw-r--r--devtools/client/shared/test/browser_prefs-01.js44
-rw-r--r--devtools/client/shared/test/browser_prefs-02.js45
-rw-r--r--devtools/client/shared/test/browser_require_raw.js20
-rw-r--r--devtools/client/shared/test/browser_spectrum.js114
-rw-r--r--devtools/client/shared/test/browser_tableWidget_basic.js390
-rw-r--r--devtools/client/shared/test/browser_tableWidget_keyboard_interaction.js194
-rw-r--r--devtools/client/shared/test/browser_tableWidget_mouse_interaction.js317
-rw-r--r--devtools/client/shared/test/browser_telemetry_button_eyedropper.js52
-rw-r--r--devtools/client/shared/test/browser_telemetry_button_paintflashing.js89
-rw-r--r--devtools/client/shared/test/browser_telemetry_button_responsive.js95
-rw-r--r--devtools/client/shared/test/browser_telemetry_button_scratchpad.js127
-rw-r--r--devtools/client/shared/test/browser_telemetry_sidebar.js84
-rw-r--r--devtools/client/shared/test/browser_telemetry_toolbox.js22
-rw-r--r--devtools/client/shared/test/browser_telemetry_toolboxtabs_canvasdebugger.js29
-rw-r--r--devtools/client/shared/test/browser_telemetry_toolboxtabs_inspector.js22
-rw-r--r--devtools/client/shared/test/browser_telemetry_toolboxtabs_jsdebugger.js22
-rw-r--r--devtools/client/shared/test/browser_telemetry_toolboxtabs_jsprofiler.js22
-rw-r--r--devtools/client/shared/test/browser_telemetry_toolboxtabs_netmonitor.js23
-rw-r--r--devtools/client/shared/test/browser_telemetry_toolboxtabs_options.js22
-rw-r--r--devtools/client/shared/test/browser_telemetry_toolboxtabs_shadereditor.js37
-rw-r--r--devtools/client/shared/test/browser_telemetry_toolboxtabs_storage.js28
-rw-r--r--devtools/client/shared/test/browser_telemetry_toolboxtabs_styleeditor.js23
-rw-r--r--devtools/client/shared/test/browser_telemetry_toolboxtabs_webaudioeditor.js29
-rw-r--r--devtools/client/shared/test/browser_telemetry_toolboxtabs_webconsole.js22
-rw-r--r--devtools/client/shared/test/browser_templater_basic.html13
-rw-r--r--devtools/client/shared/test/browser_templater_basic.js286
-rw-r--r--devtools/client/shared/test/browser_theme.js98
-rw-r--r--devtools/client/shared/test/browser_theme_switching.js53
-rw-r--r--devtools/client/shared/test/browser_toolbar_basic.html40
-rw-r--r--devtools/client/shared/test/browser_toolbar_basic.js60
-rw-r--r--devtools/client/shared/test/browser_toolbar_tooltip.js111
-rw-r--r--devtools/client/shared/test/browser_toolbar_webconsole_errors_count.html33
-rw-r--r--devtools/client/shared/test/browser_toolbar_webconsole_errors_count.js256
-rw-r--r--devtools/client/shared/test/browser_treeWidget_basic.js267
-rw-r--r--devtools/client/shared/test/browser_treeWidget_keyboard_interaction.js228
-rw-r--r--devtools/client/shared/test/browser_treeWidget_mouse_interaction.js135
-rw-r--r--devtools/client/shared/test/doc_options-view.xul26
-rw-r--r--devtools/client/shared/test/head.js346
-rw-r--r--devtools/client/shared/test/helper_color_data.js175
-rw-r--r--devtools/client/shared/test/helper_html_tooltip.js96
-rw-r--r--devtools/client/shared/test/helper_inplace_editor.js115
-rw-r--r--devtools/client/shared/test/html-mdn-css-basic-testing.html21
-rw-r--r--devtools/client/shared/test/html-mdn-css-no-summary-or-syntax.html12
-rw-r--r--devtools/client/shared/test/html-mdn-css-no-summary.html21
-rw-r--r--devtools/client/shared/test/html-mdn-css-no-syntax.html17
-rw-r--r--devtools/client/shared/test/html-mdn-css-syntax-old-style.html23
-rw-r--r--devtools/client/shared/test/leakhunt.js165
-rw-r--r--devtools/client/shared/test/test-actor-registry.js97
-rw-r--r--devtools/client/shared/test/test-actor.js1138
-rw-r--r--devtools/client/shared/test/unit/.eslintrc.js6
-rw-r--r--devtools/client/shared/test/unit/test_VariablesView_filtering-without-controller.js36
-rw-r--r--devtools/client/shared/test/unit/test_VariablesView_getString_promise.js76
-rw-r--r--devtools/client/shared/test/unit/test_advanceValidate.js31
-rw-r--r--devtools/client/shared/test/unit/test_attribute-parsing-01.js73
-rw-r--r--devtools/client/shared/test/unit/test_attribute-parsing-02.js134
-rw-r--r--devtools/client/shared/test/unit/test_bezierCanvas.js117
-rw-r--r--devtools/client/shared/test/unit/test_cssAngle.js33
-rw-r--r--devtools/client/shared/test/unit/test_cssColor-01.js76
-rw-r--r--devtools/client/shared/test/unit/test_cssColor-02.js45
-rw-r--r--devtools/client/shared/test/unit/test_cssColor-03.js61
-rw-r--r--devtools/client/shared/test/unit/test_cssColorDatabase.js63
-rw-r--r--devtools/client/shared/test/unit/test_cubicBezier.js146
-rw-r--r--devtools/client/shared/test/unit/test_escapeCSSComment.js40
-rw-r--r--devtools/client/shared/test/unit/test_parseDeclarations.js439
-rw-r--r--devtools/client/shared/test/unit/test_parsePseudoClassesAndAttributes.js213
-rw-r--r--devtools/client/shared/test/unit/test_parseSingleValue.js93
-rw-r--r--devtools/client/shared/test/unit/test_rewriteDeclarations.js529
-rw-r--r--devtools/client/shared/test/unit/test_source-utils.js181
-rw-r--r--devtools/client/shared/test/unit/test_suggestion-picker.js149
-rw-r--r--devtools/client/shared/test/unit/test_undoStack.js98
-rw-r--r--devtools/client/shared/test/unit/xpcshell.ini30
-rw-r--r--devtools/client/shared/theme-switching.js185
-rw-r--r--devtools/client/shared/theme.js84
-rw-r--r--devtools/client/shared/undo.js192
-rw-r--r--devtools/client/shared/vendor/D3_LICENSE26
-rw-r--r--devtools/client/shared/vendor/DAGRE_D3_LICENSE19
-rw-r--r--devtools/client/shared/vendor/REACT_REDUX_LICENSE21
-rw-r--r--devtools/client/shared/vendor/REACT_REDUX_UPGRADING9
-rw-r--r--devtools/client/shared/vendor/REACT_UPGRADING54
-rw-r--r--devtools/client/shared/vendor/REACT_VIRTUALIZED_UPGRADING14
-rw-r--r--devtools/client/shared/vendor/REDUX_LICENSE21
-rw-r--r--devtools/client/shared/vendor/REDUX_UPGRADING10
-rw-r--r--devtools/client/shared/vendor/RESELECT_LICENSE21
-rw-r--r--devtools/client/shared/vendor/RESELECT_UPGRADING7
-rw-r--r--devtools/client/shared/vendor/d3.js9275
-rw-r--r--devtools/client/shared/vendor/dagre-d3.js4560
-rw-r--r--devtools/client/shared/vendor/immutable.js4997
-rwxr-xr-xdevtools/client/shared/vendor/jsol.js97
-rw-r--r--devtools/client/shared/vendor/moz.build29
-rw-r--r--devtools/client/shared/vendor/react-addons-shallow-compare.js9
-rw-r--r--devtools/client/shared/vendor/react-dev.js20763
-rw-r--r--devtools/client/shared/vendor/react-dom.js42
-rw-r--r--devtools/client/shared/vendor/react-proxy.js1909
-rw-r--r--devtools/client/shared/vendor/react-redux.js724
-rw-r--r--devtools/client/shared/vendor/react-virtualized.js4296
-rw-r--r--devtools/client/shared/vendor/react.js20763
-rw-r--r--devtools/client/shared/vendor/redux.js775
-rw-r--r--devtools/client/shared/vendor/reselect.js136
-rw-r--r--devtools/client/shared/vendor/seamless-immutable.js392
-rw-r--r--devtools/client/shared/view-source.js185
-rw-r--r--devtools/client/shared/webgl-utils.js55
-rw-r--r--devtools/client/shared/widgets/AbstractTreeItem.jsm661
-rw-r--r--devtools/client/shared/widgets/BarGraphWidget.js498
-rw-r--r--devtools/client/shared/widgets/BreadcrumbsWidget.jsm250
-rw-r--r--devtools/client/shared/widgets/Chart.jsm449
-rw-r--r--devtools/client/shared/widgets/CubicBezierPresets.js64
-rw-r--r--devtools/client/shared/widgets/CubicBezierWidget.js897
-rw-r--r--devtools/client/shared/widgets/FastListWidget.js249
-rw-r--r--devtools/client/shared/widgets/FilterWidget.js1073
-rw-r--r--devtools/client/shared/widgets/FlameGraph.js1462
-rw-r--r--devtools/client/shared/widgets/Graphs.js1424
-rw-r--r--devtools/client/shared/widgets/GraphsWorker.js103
-rw-r--r--devtools/client/shared/widgets/LineGraphWidget.js402
-rw-r--r--devtools/client/shared/widgets/MdnDocsWidget.js510
-rw-r--r--devtools/client/shared/widgets/MountainGraphWidget.js195
-rw-r--r--devtools/client/shared/widgets/SideMenuWidget.jsm725
-rw-r--r--devtools/client/shared/widgets/SimpleListWidget.jsm255
-rw-r--r--devtools/client/shared/widgets/Spectrum.js336
-rw-r--r--devtools/client/shared/widgets/TableWidget.js1817
-rw-r--r--devtools/client/shared/widgets/TreeWidget.js605
-rw-r--r--devtools/client/shared/widgets/VariablesView.jsm4182
-rw-r--r--devtools/client/shared/widgets/VariablesView.xul18
-rw-r--r--devtools/client/shared/widgets/VariablesViewController.jsm858
-rw-r--r--devtools/client/shared/widgets/cubic-bezier.css216
-rw-r--r--devtools/client/shared/widgets/filter-widget.css238
-rw-r--r--devtools/client/shared/widgets/graphs-frame.xhtml26
-rw-r--r--devtools/client/shared/widgets/mdn-docs.css39
-rw-r--r--devtools/client/shared/widgets/moz.build34
-rw-r--r--devtools/client/shared/widgets/spectrum.css155
-rw-r--r--devtools/client/shared/widgets/tooltip/CssDocsTooltip.js93
-rw-r--r--devtools/client/shared/widgets/tooltip/EventTooltipHelper.js313
-rw-r--r--devtools/client/shared/widgets/tooltip/HTMLTooltip.js638
-rw-r--r--devtools/client/shared/widgets/tooltip/ImageTooltipHelper.js131
-rw-r--r--devtools/client/shared/widgets/tooltip/SwatchBasedEditorTooltip.js209
-rw-r--r--devtools/client/shared/widgets/tooltip/SwatchColorPickerTooltip.js182
-rw-r--r--devtools/client/shared/widgets/tooltip/SwatchCubicBezierTooltip.js102
-rw-r--r--devtools/client/shared/widgets/tooltip/SwatchFilterTooltip.js116
-rw-r--r--devtools/client/shared/widgets/tooltip/Tooltip.js410
-rw-r--r--devtools/client/shared/widgets/tooltip/TooltipToggle.js182
-rw-r--r--devtools/client/shared/widgets/tooltip/VariableContentHelper.js89
-rw-r--r--devtools/client/shared/widgets/tooltip/moz.build19
-rw-r--r--devtools/client/shared/widgets/view-helpers.js1625
-rw-r--r--devtools/client/shared/widgets/widgets.css109
-rw-r--r--devtools/client/shared/zoom-keys.js85
-rw-r--r--devtools/client/shims/gDevTools.jsm35
-rw-r--r--devtools/client/shims/moz.build18
-rw-r--r--devtools/client/sourceeditor/.eslintrc.js15
-rw-r--r--devtools/client/sourceeditor/autocomplete.js405
-rw-r--r--devtools/client/sourceeditor/codemirror/LICENSE23
-rw-r--r--devtools/client/sourceeditor/codemirror/README114
-rw-r--r--devtools/client/sourceeditor/codemirror/addon/comment/comment.js203
-rw-r--r--devtools/client/sourceeditor/codemirror/addon/comment/continuecomment.js85
-rw-r--r--devtools/client/sourceeditor/codemirror/addon/dialog/dialog.css32
-rw-r--r--devtools/client/sourceeditor/codemirror/addon/dialog/dialog.js157
-rw-r--r--devtools/client/sourceeditor/codemirror/addon/edit/closebrackets.js195
-rw-r--r--devtools/client/sourceeditor/codemirror/addon/edit/closetag.js169
-rw-r--r--devtools/client/sourceeditor/codemirror/addon/edit/continuelist.js51
-rw-r--r--devtools/client/sourceeditor/codemirror/addon/edit/matchbrackets.js120
-rw-r--r--devtools/client/sourceeditor/codemirror/addon/edit/matchtags.js66
-rw-r--r--devtools/client/sourceeditor/codemirror/addon/edit/trailingspace.js27
-rw-r--r--devtools/client/sourceeditor/codemirror/addon/fold/brace-fold.js105
-rw-r--r--devtools/client/sourceeditor/codemirror/addon/fold/comment-fold.js59
-rw-r--r--devtools/client/sourceeditor/codemirror/addon/fold/foldcode.js150
-rw-r--r--devtools/client/sourceeditor/codemirror/addon/fold/foldgutter.css20
-rw-r--r--devtools/client/sourceeditor/codemirror/addon/fold/foldgutter.js146
-rw-r--r--devtools/client/sourceeditor/codemirror/addon/fold/indent-fold.js44
-rw-r--r--devtools/client/sourceeditor/codemirror/addon/fold/markdown-fold.js49
-rw-r--r--devtools/client/sourceeditor/codemirror/addon/fold/xml-fold.js182
-rw-r--r--devtools/client/sourceeditor/codemirror/addon/hint/show-hint.js437
-rw-r--r--devtools/client/sourceeditor/codemirror/addon/search/match-highlighter.js146
-rw-r--r--devtools/client/sourceeditor/codemirror/addon/search/search.js246
-rw-r--r--devtools/client/sourceeditor/codemirror/addon/search/searchcursor.js189
-rw-r--r--devtools/client/sourceeditor/codemirror/addon/selection/active-line.js74
-rw-r--r--devtools/client/sourceeditor/codemirror/addon/selection/mark-selection.js118
-rw-r--r--devtools/client/sourceeditor/codemirror/addon/tern/tern.css87
-rw-r--r--devtools/client/sourceeditor/codemirror/addon/tern/tern.js701
-rw-r--r--devtools/client/sourceeditor/codemirror/codemirror.bundle.js20152
-rw-r--r--devtools/client/sourceeditor/codemirror/keymap/emacs.js412
-rw-r--r--devtools/client/sourceeditor/codemirror/keymap/sublime.js580
-rw-r--r--devtools/client/sourceeditor/codemirror/keymap/vim.js5065
-rw-r--r--devtools/client/sourceeditor/codemirror/lib/codemirror.css347
-rw-r--r--devtools/client/sourceeditor/codemirror/lib/codemirror.js8922
-rw-r--r--devtools/client/sourceeditor/codemirror/mode/clike/clike.js786
-rw-r--r--devtools/client/sourceeditor/codemirror/mode/css/css.js825
-rw-r--r--devtools/client/sourceeditor/codemirror/mode/htmlmixed/htmlmixed.js152
-rw-r--r--devtools/client/sourceeditor/codemirror/mode/javascript/javascript.js748
-rw-r--r--devtools/client/sourceeditor/codemirror/mode/wasm/wasm.js203
-rw-r--r--devtools/client/sourceeditor/codemirror/mode/xml/xml.js394
-rw-r--r--devtools/client/sourceeditor/codemirror/mozilla.css263
-rw-r--r--devtools/client/sourceeditor/css-autocompleter.js1214
-rw-r--r--devtools/client/sourceeditor/debugger.js336
-rw-r--r--devtools/client/sourceeditor/editor.js1410
-rw-r--r--devtools/client/sourceeditor/moz.build18
-rw-r--r--devtools/client/sourceeditor/tern/README13
-rw-r--r--devtools/client/sourceeditor/tern/browser.js2921
-rwxr-xr-xdevtools/client/sourceeditor/tern/comment.js87
-rwxr-xr-xdevtools/client/sourceeditor/tern/condense.js304
-rwxr-xr-xdevtools/client/sourceeditor/tern/def.js656
-rw-r--r--devtools/client/sourceeditor/tern/ecma5.js950
-rwxr-xr-xdevtools/client/sourceeditor/tern/infer.js2119
-rw-r--r--devtools/client/sourceeditor/tern/moz.build18
-rwxr-xr-xdevtools/client/sourceeditor/tern/signal.js51
-rwxr-xr-xdevtools/client/sourceeditor/tern/tern.js1056
-rw-r--r--devtools/client/sourceeditor/tern/tests/unit/head_tern.js3
-rw-r--r--devtools/client/sourceeditor/tern/tests/unit/test_autocompletion.js26
-rw-r--r--devtools/client/sourceeditor/tern/tests/unit/test_import_tern.js16
-rw-r--r--devtools/client/sourceeditor/tern/tests/unit/xpcshell.ini8
-rw-r--r--devtools/client/sourceeditor/test/.eslintrc.js6
-rw-r--r--devtools/client/sourceeditor/test/browser.ini48
-rw-r--r--devtools/client/sourceeditor/test/browser_codemirror.js18
-rw-r--r--devtools/client/sourceeditor/test/browser_css_autocompletion.js145
-rw-r--r--devtools/client/sourceeditor/test/browser_css_getInfo.js176
-rw-r--r--devtools/client/sourceeditor/test/browser_css_statemachine.js109
-rw-r--r--devtools/client/sourceeditor/test/browser_detectindent.js102
-rw-r--r--devtools/client/sourceeditor/test/browser_editor_addons.js34
-rw-r--r--devtools/client/sourceeditor/test/browser_editor_autocomplete_basic.js59
-rw-r--r--devtools/client/sourceeditor/test/browser_editor_autocomplete_events.js126
-rw-r--r--devtools/client/sourceeditor/test/browser_editor_autocomplete_js.js45
-rw-r--r--devtools/client/sourceeditor/test/browser_editor_basic.js62
-rw-r--r--devtools/client/sourceeditor/test/browser_editor_cursor.js44
-rw-r--r--devtools/client/sourceeditor/test/browser_editor_find_again.js215
-rw-r--r--devtools/client/sourceeditor/test/browser_editor_goto_line.js131
-rw-r--r--devtools/client/sourceeditor/test/browser_editor_history.js32
-rw-r--r--devtools/client/sourceeditor/test/browser_editor_markers.js39
-rw-r--r--devtools/client/sourceeditor/test/browser_editor_movelines.js63
-rw-r--r--devtools/client/sourceeditor/test/browser_editor_prefs.js121
-rw-r--r--devtools/client/sourceeditor/test/browser_editor_script_injection.js27
-rw-r--r--devtools/client/sourceeditor/test/browser_vimemacs.js17
-rw-r--r--devtools/client/sourceeditor/test/cm_mode_ruby.js285
-rw-r--r--devtools/client/sourceeditor/test/cm_script_injection_test.js8
-rw-r--r--devtools/client/sourceeditor/test/codemirror/codemirror.html210
-rw-r--r--devtools/client/sourceeditor/test/codemirror/comment_test.js100
-rw-r--r--devtools/client/sourceeditor/test/codemirror/doc_test.js371
-rw-r--r--devtools/client/sourceeditor/test/codemirror/driver.js138
-rw-r--r--devtools/client/sourceeditor/test/codemirror/emacs_test.js147
-rw-r--r--devtools/client/sourceeditor/test/codemirror/mode/javascript/test.js210
-rw-r--r--devtools/client/sourceeditor/test/codemirror/mode_test.css23
-rw-r--r--devtools/client/sourceeditor/test/codemirror/mode_test.js192
-rw-r--r--devtools/client/sourceeditor/test/codemirror/multi_test.js285
-rw-r--r--devtools/client/sourceeditor/test/codemirror/search_test.js62
-rw-r--r--devtools/client/sourceeditor/test/codemirror/sublime_test.js307
-rw-r--r--devtools/client/sourceeditor/test/codemirror/test.js2151
-rw-r--r--devtools/client/sourceeditor/test/codemirror/vim_test.js4011
-rw-r--r--devtools/client/sourceeditor/test/codemirror/vimemacs.html212
-rw-r--r--devtools/client/sourceeditor/test/css_autocompletion_tests.json39
-rw-r--r--devtools/client/sourceeditor/test/css_statemachine_testcases.css121
-rw-r--r--devtools/client/sourceeditor/test/css_statemachine_tests.json84
-rw-r--r--devtools/client/sourceeditor/test/head.js163
-rw-r--r--devtools/client/sourceeditor/test/helper_codemirror_runner.js38
-rw-r--r--devtools/client/storage/moz.build12
-rw-r--r--devtools/client/storage/panel.js87
-rw-r--r--devtools/client/storage/storage.xul58
-rw-r--r--devtools/client/storage/test/.eslintrc.js6
-rw-r--r--devtools/client/storage/test/browser.ini44
-rw-r--r--devtools/client/storage/test/browser_storage_basic.js118
-rw-r--r--devtools/client/storage/test/browser_storage_cache_delete.js46
-rw-r--r--devtools/client/storage/test/browser_storage_cache_error.js19
-rw-r--r--devtools/client/storage/test/browser_storage_cookies_delete_all.js74
-rw-r--r--devtools/client/storage/test/browser_storage_cookies_domain.js21
-rw-r--r--devtools/client/storage/test/browser_storage_cookies_edit.js22
-rw-r--r--devtools/client/storage/test/browser_storage_cookies_edit_keyboard.js23
-rw-r--r--devtools/client/storage/test/browser_storage_cookies_tab_navigation.js24
-rw-r--r--devtools/client/storage/test/browser_storage_delete.js56
-rw-r--r--devtools/client/storage/test/browser_storage_delete_all.js90
-rw-r--r--devtools/client/storage/test/browser_storage_delete_tree.js67
-rw-r--r--devtools/client/storage/test/browser_storage_dynamic_updates.js213
-rw-r--r--devtools/client/storage/test/browser_storage_empty_objectstores.js77
-rw-r--r--devtools/client/storage/test/browser_storage_indexeddb_delete.js47
-rw-r--r--devtools/client/storage/test/browser_storage_indexeddb_delete_blocked.js58
-rw-r--r--devtools/client/storage/test/browser_storage_localstorage_edit.js24
-rw-r--r--devtools/client/storage/test/browser_storage_localstorage_error.js24
-rw-r--r--devtools/client/storage/test/browser_storage_overflow.js41
-rw-r--r--devtools/client/storage/test/browser_storage_search.js87
-rw-r--r--devtools/client/storage/test/browser_storage_search_keyboard_trap.js15
-rw-r--r--devtools/client/storage/test/browser_storage_sessionstorage_edit.js24
-rw-r--r--devtools/client/storage/test/browser_storage_sidebar.js125
-rw-r--r--devtools/client/storage/test/browser_storage_sidebar_update.js41
-rw-r--r--devtools/client/storage/test/browser_storage_values.js165
-rw-r--r--devtools/client/storage/test/head.js840
-rw-r--r--devtools/client/storage/test/storage-cache-error.html20
-rw-r--r--devtools/client/storage/test/storage-complex-values.html123
-rw-r--r--devtools/client/storage/test/storage-cookies.html24
-rw-r--r--devtools/client/storage/test/storage-empty-objectstores.html62
-rw-r--r--devtools/client/storage/test/storage-idb-delete-blocked.html52
-rw-r--r--devtools/client/storage/test/storage-listings.html126
-rw-r--r--devtools/client/storage/test/storage-localstorage.html23
-rw-r--r--devtools/client/storage/test/storage-overflow.html19
-rw-r--r--devtools/client/storage/test/storage-search.html23
-rw-r--r--devtools/client/storage/test/storage-secured-iframe.html91
-rw-r--r--devtools/client/storage/test/storage-sessionstorage.html23
-rw-r--r--devtools/client/storage/test/storage-unsecured-iframe.html19
-rw-r--r--devtools/client/storage/test/storage-updates.html64
-rw-r--r--devtools/client/storage/ui.js1073
-rw-r--r--devtools/client/styleeditor/StyleEditorUI.jsm1029
-rw-r--r--devtools/client/styleeditor/StyleEditorUtil.jsm234
-rw-r--r--devtools/client/styleeditor/StyleSheetEditor.jsm886
-rw-r--r--devtools/client/styleeditor/moz.build16
-rw-r--r--devtools/client/styleeditor/styleeditor-commands.js72
-rw-r--r--devtools/client/styleeditor/styleeditor-panel.js158
-rw-r--r--devtools/client/styleeditor/styleeditor.xul220
-rw-r--r--devtools/client/styleeditor/test/.eslintrc.js6
-rw-r--r--devtools/client/styleeditor/test/autocomplete.html23
-rw-r--r--devtools/client/styleeditor/test/browser.ini107
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_autocomplete-disabled.js26
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_autocomplete.js231
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_bom.js34
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_bug_740541_iframes.js76
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_bug_851132_middle_click.js56
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_bug_870339.js45
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_cmd_edit.html53
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_cmd_edit.js215
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_enabled.js56
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_fetch-from-cache.js40
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_filesave.js99
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_highlight-selector.js48
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_import.js55
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_import_rule.js25
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_init.js45
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_inline_friendly_names.js88
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_loading.js36
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_loading_with_containers.js63
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_media_sidebar.js143
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_media_sidebar_links.js144
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_media_sidebar_sourcemaps.js71
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_missing_stylesheet.js30
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_navigate.js32
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_new.js113
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_nostyle.js28
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_opentab.js121
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_pretty.js68
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_private_perwindowpb.js85
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_reload.js34
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_scroll.js91
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_selectstylesheet.js26
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_sourcemap_large.js33
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_sourcemap_watching.js162
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_sourcemaps.js149
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_sourcemaps_inline.js85
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_sv_keynav.js65
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_sv_resize.js48
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_sync.js72
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_syncAddProperty.js45
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_syncAddRule.js31
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_syncAlreadyOpen.js50
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_syncEditSelector.js39
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_syncIntoRuleView.js48
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_transition_rule.js51
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_xul.js22
-rw-r--r--devtools/client/styleeditor/test/doc_long.css403
-rw-r--r--devtools/client/styleeditor/test/doc_uncached.css16
-rw-r--r--devtools/client/styleeditor/test/doc_uncached.html10
-rw-r--r--devtools/client/styleeditor/test/doc_xulpage.xul7
-rw-r--r--devtools/client/styleeditor/test/four.html25
-rw-r--r--devtools/client/styleeditor/test/head.js121
-rw-r--r--devtools/client/styleeditor/test/import.css10
-rw-r--r--devtools/client/styleeditor/test/import.html11
-rw-r--r--devtools/client/styleeditor/test/import2.css10
-rw-r--r--devtools/client/styleeditor/test/inline-1.html19
-rw-r--r--devtools/client/styleeditor/test/inline-2.html19
-rw-r--r--devtools/client/styleeditor/test/longload.html29
-rw-r--r--devtools/client/styleeditor/test/media-rules-sourcemaps.html12
-rw-r--r--devtools/client/styleeditor/test/media-rules.css29
-rw-r--r--devtools/client/styleeditor/test/media-rules.html13
-rw-r--r--devtools/client/styleeditor/test/media-small.css5
-rw-r--r--devtools/client/styleeditor/test/media.html11
-rw-r--r--devtools/client/styleeditor/test/minified.html15
-rw-r--r--devtools/client/styleeditor/test/missing.html11
-rw-r--r--devtools/client/styleeditor/test/nostyle.html5
-rw-r--r--devtools/client/styleeditor/test/pretty.css2
-rw-r--r--devtools/client/styleeditor/test/resources_inpage.jsi12
-rw-r--r--devtools/client/styleeditor/test/resources_inpage1.css11
-rw-r--r--devtools/client/styleeditor/test/resources_inpage2.css11
-rw-r--r--devtools/client/styleeditor/test/simple.css9
-rw-r--r--devtools/client/styleeditor/test/simple.css.gzbin0 -> 166 bytes
-rw-r--r--devtools/client/styleeditor/test/simple.css.gz^headers^4
-rw-r--r--devtools/client/styleeditor/test/simple.gz.html23
-rw-r--r--devtools/client/styleeditor/test/simple.html24
-rw-r--r--devtools/client/styleeditor/test/sourcemap-css/contained.css4
-rw-r--r--devtools/client/styleeditor/test/sourcemap-css/media-rules.css8
-rw-r--r--devtools/client/styleeditor/test/sourcemap-css/media-rules.css.map6
-rw-r--r--devtools/client/styleeditor/test/sourcemap-css/sourcemaps.css7
-rw-r--r--devtools/client/styleeditor/test/sourcemap-css/sourcemaps.css.map6
-rw-r--r--devtools/client/styleeditor/test/sourcemap-css/test-bootstrap-scss.css4513
-rw-r--r--devtools/client/styleeditor/test/sourcemap-css/test-stylus.css7
-rw-r--r--devtools/client/styleeditor/test/sourcemap-sass/media-rules.scss11
-rw-r--r--devtools/client/styleeditor/test/sourcemap-sass/sourcemaps.scss10
-rw-r--r--devtools/client/styleeditor/test/sourcemap-styl/test-stylus.styl7
-rw-r--r--devtools/client/styleeditor/test/sourcemaps-inline.html17
-rw-r--r--devtools/client/styleeditor/test/sourcemaps-large.html11
-rw-r--r--devtools/client/styleeditor/test/sourcemaps-watching.html11
-rw-r--r--devtools/client/styleeditor/test/sourcemaps.html13
-rw-r--r--devtools/client/styleeditor/test/sync.html20
-rw-r--r--devtools/client/styleeditor/test/test_private.css3
-rw-r--r--devtools/client/styleeditor/test/test_private.html7
-rw-r--r--devtools/client/styleeditor/test/utf-16.cssbin0 -> 156 bytes
-rw-r--r--devtools/client/styleeditor/utils.js40
-rw-r--r--devtools/client/themes/animationinspector.css623
-rw-r--r--devtools/client/themes/audio/moz.build9
-rw-r--r--devtools/client/themes/audio/shutter.wavbin0 -> 25744 bytes
-rw-r--r--devtools/client/themes/boxmodel.css258
-rw-r--r--devtools/client/themes/canvasdebugger.css353
-rw-r--r--devtools/client/themes/commandline.css172
-rw-r--r--devtools/client/themes/commandline.inc.css217
-rw-r--r--devtools/client/themes/common.css791
-rw-r--r--devtools/client/themes/components-frame.css53
-rw-r--r--devtools/client/themes/components-h-split-box.css24
-rw-r--r--devtools/client/themes/computed.css237
-rw-r--r--devtools/client/themes/dark-theme.css348
-rw-r--r--devtools/client/themes/debugger.css670
-rw-r--r--devtools/client/themes/devtools-browser.css26
-rw-r--r--devtools/client/themes/dom.css9
-rw-r--r--devtools/client/themes/firebug-theme.css235
-rw-r--r--devtools/client/themes/floating-scrollbars-dark-theme.css59
-rw-r--r--devtools/client/themes/floating-scrollbars-responsive-design.css47
-rw-r--r--devtools/client/themes/fonts.css128
-rw-r--r--devtools/client/themes/images/add.svg6
-rw-r--r--devtools/client/themes/images/alerticon-warning.pngbin0 -> 613 bytes
-rw-r--r--devtools/client/themes/images/alerticon-warning@2x.pngbin0 -> 432 bytes
-rw-r--r--devtools/client/themes/images/angle-swatch.svg17
-rw-r--r--devtools/client/themes/images/animation-fast-track.svg8
-rw-r--r--devtools/client/themes/images/arrow-e.pngbin0 -> 168 bytes
-rw-r--r--devtools/client/themes/images/arrow-e@2x.pngbin0 -> 417 bytes
-rw-r--r--devtools/client/themes/images/breadcrumbs-scrollbutton.pngbin0 -> 260 bytes
-rw-r--r--devtools/client/themes/images/breadcrumbs-scrollbutton@2x.pngbin0 -> 627 bytes
-rw-r--r--devtools/client/themes/images/breakpoint.svg45
-rw-r--r--devtools/client/themes/images/clear.svg7
-rw-r--r--devtools/client/themes/images/close.svg6
-rw-r--r--devtools/client/themes/images/command-console.svg7
-rw-r--r--devtools/client/themes/images/command-eyedropper.svg7
-rw-r--r--devtools/client/themes/images/command-frames.svg6
-rw-r--r--devtools/client/themes/images/command-measure.svg7
-rwxr-xr-xdevtools/client/themes/images/command-noautohide.svg6
-rw-r--r--devtools/client/themes/images/command-paintflashing.svg7
-rw-r--r--devtools/client/themes/images/command-pick.svg9
-rw-r--r--devtools/client/themes/images/command-responsivemode.svg9
-rw-r--r--devtools/client/themes/images/command-rulers.svg7
-rw-r--r--devtools/client/themes/images/command-screenshot.svg7
-rw-r--r--devtools/client/themes/images/commandline-icon.svg42
-rw-r--r--devtools/client/themes/images/controls.pngbin0 -> 1630 bytes
-rw-r--r--devtools/client/themes/images/controls@2x.pngbin0 -> 2045 bytes
-rw-r--r--devtools/client/themes/images/cubic-bezier-swatch.pngbin0 -> 1184 bytes
-rw-r--r--devtools/client/themes/images/cubic-bezier-swatch@2x.pngbin0 -> 1661 bytes
-rw-r--r--devtools/client/themes/images/debugger-step-in.svg6
-rw-r--r--devtools/client/themes/images/debugger-step-out.svg6
-rw-r--r--devtools/client/themes/images/debugger-step-over.pngbin0 -> 306 bytes
-rw-r--r--devtools/client/themes/images/debugger-step-over.svg7
-rw-r--r--devtools/client/themes/images/debugger-step-over@2x.pngbin0 -> 472 bytes
-rw-r--r--devtools/client/themes/images/debugger-toggleBreakpoints.svg6
-rw-r--r--devtools/client/themes/images/debugging-addons.svg6
-rw-r--r--devtools/client/themes/images/debugging-devices.svg7
-rw-r--r--devtools/client/themes/images/debugging-tabs.svg3
-rw-r--r--devtools/client/themes/images/debugging-workers.svg11
-rw-r--r--devtools/client/themes/images/diff.svg9
-rw-r--r--devtools/client/themes/images/dock-bottom.svg6
-rw-r--r--devtools/client/themes/images/dock-side.svg3
-rw-r--r--devtools/client/themes/images/dock-undock.svg8
-rw-r--r--devtools/client/themes/images/dropmarker.svg6
-rw-r--r--devtools/client/themes/images/editor-error.pngbin0 -> 3794 bytes
-rwxr-xr-xdevtools/client/themes/images/emojis/emoji-command-pick.svg7
-rw-r--r--devtools/client/themes/images/emojis/emoji-tool-canvas.svg11
-rw-r--r--devtools/client/themes/images/emojis/emoji-tool-debugger.svg11
-rw-r--r--devtools/client/themes/images/emojis/emoji-tool-dom.svg11
-rw-r--r--devtools/client/themes/images/emojis/emoji-tool-inspector.svg13
-rw-r--r--devtools/client/themes/images/emojis/emoji-tool-memory.svg9
-rw-r--r--devtools/client/themes/images/emojis/emoji-tool-network.svg8
-rw-r--r--devtools/client/themes/images/emojis/emoji-tool-profiler.svg11
-rw-r--r--devtools/client/themes/images/emojis/emoji-tool-scratchpad.svg10
-rw-r--r--devtools/client/themes/images/emojis/emoji-tool-shadereditor.svg96
-rw-r--r--devtools/client/themes/images/emojis/emoji-tool-storage.svg8
-rw-r--r--devtools/client/themes/images/emojis/emoji-tool-styleeditor.svg11
-rw-r--r--devtools/client/themes/images/emojis/emoji-tool-webaudio.svg12
-rw-r--r--devtools/client/themes/images/emojis/emoji-tool-webconsole.svg8
-rw-r--r--devtools/client/themes/images/fast-forward.svg6
-rw-r--r--devtools/client/themes/images/filetypes/dir-close.svg6
-rw-r--r--devtools/client/themes/images/filetypes/dir-open.svg7
-rw-r--r--devtools/client/themes/images/filetypes/globe.svg7
-rw-r--r--devtools/client/themes/images/filter-swatch.svg12
-rw-r--r--devtools/client/themes/images/filter.svg16
-rw-r--r--devtools/client/themes/images/filters.svg31
-rw-r--r--devtools/client/themes/images/firebug/arrow-down.svg6
-rw-r--r--devtools/client/themes/images/firebug/arrow-up.svg6
-rw-r--r--devtools/client/themes/images/firebug/breadcrumbs-divider.svg18
-rw-r--r--devtools/client/themes/images/firebug/breakpoint.svg13
-rw-r--r--devtools/client/themes/images/firebug/close.svg25
-rw-r--r--devtools/client/themes/images/firebug/command-console.svg31
-rw-r--r--devtools/client/themes/images/firebug/command-eyedropper.svg38
-rw-r--r--devtools/client/themes/images/firebug/command-frames.svg25
-rw-r--r--devtools/client/themes/images/firebug/command-measure.svg26
-rw-r--r--devtools/client/themes/images/firebug/command-noautohide.svg47
-rw-r--r--devtools/client/themes/images/firebug/command-paintflashing.svg38
-rw-r--r--devtools/client/themes/images/firebug/command-pick.svg20
-rw-r--r--devtools/client/themes/images/firebug/command-responsivemode.svg39
-rw-r--r--devtools/client/themes/images/firebug/command-rulers.svg20
-rw-r--r--devtools/client/themes/images/firebug/command-scratchpad.svg38
-rw-r--r--devtools/client/themes/images/firebug/command-screenshot.svg39
-rw-r--r--devtools/client/themes/images/firebug/commandline-icon.svg26
-rw-r--r--devtools/client/themes/images/firebug/debugger-blackbox.svg30
-rw-r--r--devtools/client/themes/images/firebug/debugger-prettyprint.svg18
-rw-r--r--devtools/client/themes/images/firebug/debugger-step-in.svg26
-rw-r--r--devtools/client/themes/images/firebug/debugger-step-out.svg26
-rw-r--r--devtools/client/themes/images/firebug/debugger-step-over.svg24
-rw-r--r--devtools/client/themes/images/firebug/debugger-toggleBreakpoints.svg13
-rw-r--r--devtools/client/themes/images/firebug/disable.svg6
-rw-r--r--devtools/client/themes/images/firebug/dock-bottom.svg25
-rw-r--r--devtools/client/themes/images/firebug/dock-side.svg25
-rw-r--r--devtools/client/themes/images/firebug/dock-undock.svg27
-rw-r--r--devtools/client/themes/images/firebug/moz.build11
-rw-r--r--devtools/client/themes/images/firebug/pane-collapse.svg29
-rw-r--r--devtools/client/themes/images/firebug/pane-expand.svg29
-rw-r--r--devtools/client/themes/images/firebug/pause.svg31
-rw-r--r--devtools/client/themes/images/firebug/play.svg18
-rw-r--r--devtools/client/themes/images/firebug/read-only.svg34
-rw-r--r--devtools/client/themes/images/firebug/rewind.svg18
-rw-r--r--devtools/client/themes/images/firebug/spinner.pngbin0 -> 6125 bytes
-rw-r--r--devtools/client/themes/images/firebug/tool-debugger-paused.svg14
-rw-r--r--devtools/client/themes/images/firebug/tool-options.svg18
-rw-r--r--devtools/client/themes/images/firebug/twisty-closed-firebug.svg14
-rw-r--r--devtools/client/themes/images/firebug/twisty-open-firebug.svg14
-rw-r--r--devtools/client/themes/images/geometry-editor.svg7
-rw-r--r--devtools/client/themes/images/globe.svg8
-rw-r--r--devtools/client/themes/images/grid.svg6
-rw-r--r--devtools/client/themes/images/import.svg8
-rw-r--r--devtools/client/themes/images/item-arrow-dark-ltr.svg7
-rw-r--r--devtools/client/themes/images/item-arrow-dark-rtl.svg7
-rw-r--r--devtools/client/themes/images/item-arrow-ltr.svg7
-rwxr-xr-xdevtools/client/themes/images/item-arrow-rtl.svg7
-rw-r--r--devtools/client/themes/images/item-toggle.svg7
-rw-r--r--devtools/client/themes/images/magnifying-glass.pngbin0 -> 192 bytes
-rw-r--r--devtools/client/themes/images/magnifying-glass@2x.pngbin0 -> 449 bytes
-rw-r--r--devtools/client/themes/images/noise.pngbin0 -> 2118 bytes
-rw-r--r--devtools/client/themes/images/pane-collapse.svg7
-rw-r--r--devtools/client/themes/images/pane-expand.svg7
-rw-r--r--devtools/client/themes/images/pause.svg6
-rw-r--r--devtools/client/themes/images/performance-icons.svg42
-rw-r--r--devtools/client/themes/images/play.svg6
-rw-r--r--devtools/client/themes/images/power.svg7
-rw-r--r--devtools/client/themes/images/profiler-stopwatch.svg11
-rw-r--r--devtools/client/themes/images/pseudo-class.svg7
-rw-r--r--devtools/client/themes/images/reload.svg6
-rw-r--r--devtools/client/themes/images/responsivemode/responsive-horizontal-resizer.pngbin0 -> 102 bytes
-rw-r--r--devtools/client/themes/images/responsivemode/responsive-horizontal-resizer@2x.pngbin0 -> 129 bytes
-rw-r--r--devtools/client/themes/images/responsivemode/responsive-se-resizer.pngbin0 -> 129 bytes
-rw-r--r--devtools/client/themes/images/responsivemode/responsive-se-resizer@2x.pngbin0 -> 205 bytes
-rw-r--r--devtools/client/themes/images/responsivemode/responsive-vertical-resizer.pngbin0 -> 105 bytes
-rw-r--r--devtools/client/themes/images/responsivemode/responsive-vertical-resizer@2x.pngbin0 -> 141 bytes
-rw-r--r--devtools/client/themes/images/responsivemode/responsiveui-home.pngbin0 -> 276 bytes
-rw-r--r--devtools/client/themes/images/responsivemode/responsiveui-rotate.pngbin0 -> 245 bytes
-rw-r--r--devtools/client/themes/images/responsivemode/responsiveui-rotate@2x.pngbin0 -> 438 bytes
-rw-r--r--devtools/client/themes/images/responsivemode/responsiveui-screenshot.pngbin0 -> 303 bytes
-rw-r--r--devtools/client/themes/images/responsivemode/responsiveui-screenshot@2x.pngbin0 -> 531 bytes
-rw-r--r--devtools/client/themes/images/responsivemode/responsiveui-touch.pngbin0 -> 470 bytes
-rw-r--r--devtools/client/themes/images/responsivemode/responsiveui-touch@2x.pngbin0 -> 927 bytes
-rw-r--r--devtools/client/themes/images/rewind.svg6
-rw-r--r--devtools/client/themes/images/search-clear-dark.svg15
-rw-r--r--devtools/client/themes/images/search-clear-failed.svg15
-rw-r--r--devtools/client/themes/images/search-clear-light.svg15
-rw-r--r--devtools/client/themes/images/search.svg6
-rw-r--r--devtools/client/themes/images/security-state-broken.svg9
-rw-r--r--devtools/client/themes/images/security-state-insecure.svg38
-rw-r--r--devtools/client/themes/images/security-state-secure.svg27
-rw-r--r--devtools/client/themes/images/security-state-weak.svg31
-rw-r--r--devtools/client/themes/images/sort-arrows.svg12
-rw-r--r--devtools/client/themes/images/toggle-tools.pngbin0 -> 883 bytes
-rw-r--r--devtools/client/themes/images/toggle-tools@2x.pngbin0 -> 1834 bytes
-rw-r--r--devtools/client/themes/images/tool-canvas.svg9
-rw-r--r--devtools/client/themes/images/tool-debugger-paused.svg6
-rw-r--r--devtools/client/themes/images/tool-debugger.svg6
-rw-r--r--devtools/client/themes/images/tool-dom.svg6
-rw-r--r--devtools/client/themes/images/tool-inspector.svg7
-rw-r--r--devtools/client/themes/images/tool-memory-active.svg10
-rw-r--r--devtools/client/themes/images/tool-memory.svg10
-rw-r--r--devtools/client/themes/images/tool-network.svg9
-rw-r--r--devtools/client/themes/images/tool-options.svg7
-rw-r--r--devtools/client/themes/images/tool-profiler-active.svg9
-rw-r--r--devtools/client/themes/images/tool-profiler.svg9
-rw-r--r--devtools/client/themes/images/tool-scratchpad.svg7
-rw-r--r--devtools/client/themes/images/tool-shadereditor.svg12
-rw-r--r--devtools/client/themes/images/tool-storage.svg7
-rw-r--r--devtools/client/themes/images/tool-styleeditor.svg6
-rw-r--r--devtools/client/themes/images/tool-webaudio.svg6
-rw-r--r--devtools/client/themes/images/tool-webconsole.svg7
-rw-r--r--devtools/client/themes/images/tracer-icon.pngbin0 -> 290 bytes
-rw-r--r--devtools/client/themes/images/tracer-icon@2x.pngbin0 -> 469 bytes
-rw-r--r--devtools/client/themes/images/vview-delete.pngbin0 -> 136 bytes
-rw-r--r--devtools/client/themes/images/vview-delete@2x.pngbin0 -> 168 bytes
-rw-r--r--devtools/client/themes/images/vview-edit.pngbin0 -> 160 bytes
-rw-r--r--devtools/client/themes/images/vview-edit@2x.pngbin0 -> 302 bytes
-rw-r--r--devtools/client/themes/images/vview-lock.pngbin0 -> 177 bytes
-rw-r--r--devtools/client/themes/images/vview-lock@2x.pngbin0 -> 272 bytes
-rw-r--r--devtools/client/themes/images/vview-open-inspector.pngbin0 -> 98 bytes
-rw-r--r--devtools/client/themes/images/vview-open-inspector@2x.pngbin0 -> 116 bytes
-rw-r--r--devtools/client/themes/images/webconsole.svg101
-rw-r--r--devtools/client/themes/inspector.css216
-rw-r--r--devtools/client/themes/jit-optimizations.css108
-rw-r--r--devtools/client/themes/layout.css14
-rw-r--r--devtools/client/themes/light-theme.css338
-rw-r--r--devtools/client/themes/markup.css351
-rw-r--r--devtools/client/themes/memory.css637
-rw-r--r--devtools/client/themes/moz.build16
-rw-r--r--devtools/client/themes/netmonitor.css975
-rw-r--r--devtools/client/themes/performance.css794
-rw-r--r--devtools/client/themes/projecteditor/projecteditor.css184
-rw-r--r--devtools/client/themes/responsivedesign.inc.css355
-rw-r--r--devtools/client/themes/rules.css561
-rw-r--r--devtools/client/themes/scratchpad.css12
-rw-r--r--devtools/client/themes/shadereditor.css109
-rw-r--r--devtools/client/themes/shims/common.css10
-rw-r--r--devtools/client/themes/shims/jar.mn6
-rw-r--r--devtools/client/themes/shims/moz.build8
-rw-r--r--devtools/client/themes/splitters.css80
-rw-r--r--devtools/client/themes/splitview.css75
-rw-r--r--devtools/client/themes/storage.css49
-rw-r--r--devtools/client/themes/styleeditor.css445
-rw-r--r--devtools/client/themes/toolbars.css216
-rw-r--r--devtools/client/themes/toolbox.css408
-rw-r--r--devtools/client/themes/tooltip/arrow-horizontal-dark.pngbin0 -> 1418 bytes
-rw-r--r--devtools/client/themes/tooltip/arrow-horizontal-dark@2x.pngbin0 -> 1796 bytes
-rw-r--r--devtools/client/themes/tooltip/arrow-horizontal-light.pngbin0 -> 1434 bytes
-rw-r--r--devtools/client/themes/tooltip/arrow-horizontal-light@2x.pngbin0 -> 1870 bytes
-rw-r--r--devtools/client/themes/tooltip/arrow-vertical-dark.pngbin0 -> 1401 bytes
-rw-r--r--devtools/client/themes/tooltip/arrow-vertical-dark@2x.pngbin0 -> 1866 bytes
-rw-r--r--devtools/client/themes/tooltip/arrow-vertical-light.pngbin0 -> 1377 bytes
-rw-r--r--devtools/client/themes/tooltip/arrow-vertical-light@2x.pngbin0 -> 1752 bytes
-rw-r--r--devtools/client/themes/tooltips.css456
-rw-r--r--devtools/client/themes/variables.css203
-rw-r--r--devtools/client/themes/webaudioeditor.css195
-rw-r--r--devtools/client/themes/webconsole.css793
-rw-r--r--devtools/client/themes/widgets.css1621
-rw-r--r--devtools/client/webaudioeditor/controller.js232
-rw-r--r--devtools/client/webaudioeditor/includes.js110
-rw-r--r--devtools/client/webaudioeditor/models.js288
-rw-r--r--devtools/client/webaudioeditor/moz.build10
-rw-r--r--devtools/client/webaudioeditor/panel.js71
-rw-r--r--devtools/client/webaudioeditor/test/.eslintrc.js6
-rw-r--r--devtools/client/webaudioeditor/test/440hz_sine.oggbin0 -> 11822 bytes
-rw-r--r--devtools/client/webaudioeditor/test/browser.ini77
-rw-r--r--devtools/client/webaudioeditor/test/browser_audionode-actor-add-automation-event.js52
-rw-r--r--devtools/client/webaudioeditor/test/browser_audionode-actor-bypass.js36
-rw-r--r--devtools/client/webaudioeditor/test/browser_audionode-actor-bypassable.js38
-rw-r--r--devtools/client/webaudioeditor/test/browser_audionode-actor-connectnode-disconnect.js39
-rw-r--r--devtools/client/webaudioeditor/test/browser_audionode-actor-connectparam.js32
-rw-r--r--devtools/client/webaudioeditor/test/browser_audionode-actor-get-automation-data-01.js53
-rw-r--r--devtools/client/webaudioeditor/test/browser_audionode-actor-get-automation-data-02.js42
-rw-r--r--devtools/client/webaudioeditor/test/browser_audionode-actor-get-automation-data-03.js34
-rw-r--r--devtools/client/webaudioeditor/test/browser_audionode-actor-get-param-flags.js47
-rw-r--r--devtools/client/webaudioeditor/test/browser_audionode-actor-get-params-01.js49
-rw-r--r--devtools/client/webaudioeditor/test/browser_audionode-actor-get-params-02.js52
-rw-r--r--devtools/client/webaudioeditor/test/browser_audionode-actor-get-set-param.js47
-rw-r--r--devtools/client/webaudioeditor/test/browser_audionode-actor-source.js27
-rw-r--r--devtools/client/webaudioeditor/test/browser_audionode-actor-type.js28
-rw-r--r--devtools/client/webaudioeditor/test/browser_callwatcher-01.js26
-rw-r--r--devtools/client/webaudioeditor/test/browser_callwatcher-02.js44
-rw-r--r--devtools/client/webaudioeditor/test/browser_wa_automation-view-01.js57
-rw-r--r--devtools/client/webaudioeditor/test/browser_wa_automation-view-02.js55
-rw-r--r--devtools/client/webaudioeditor/test/browser_wa_controller-01.js28
-rw-r--r--devtools/client/webaudioeditor/test/browser_wa_destroy-node-01.js59
-rw-r--r--devtools/client/webaudioeditor/test/browser_wa_first-run.js49
-rw-r--r--devtools/client/webaudioeditor/test/browser_wa_graph-click.js49
-rw-r--r--devtools/client/webaudioeditor/test/browser_wa_graph-markers.js61
-rw-r--r--devtools/client/webaudioeditor/test/browser_wa_graph-render-01.js44
-rw-r--r--devtools/client/webaudioeditor/test/browser_wa_graph-render-02.js48
-rw-r--r--devtools/client/webaudioeditor/test/browser_wa_graph-render-03.js34
-rw-r--r--devtools/client/webaudioeditor/test/browser_wa_graph-render-04.js37
-rw-r--r--devtools/client/webaudioeditor/test/browser_wa_graph-render-05.js28
-rw-r--r--devtools/client/webaudioeditor/test/browser_wa_graph-render-06.js25
-rw-r--r--devtools/client/webaudioeditor/test/browser_wa_graph-selected.js49
-rw-r--r--devtools/client/webaudioeditor/test/browser_wa_graph-zoom.js43
-rw-r--r--devtools/client/webaudioeditor/test/browser_wa_inspector-bypass-01.js61
-rw-r--r--devtools/client/webaudioeditor/test/browser_wa_inspector-toggle.js60
-rw-r--r--devtools/client/webaudioeditor/test/browser_wa_inspector-width.js57
-rw-r--r--devtools/client/webaudioeditor/test/browser_wa_inspector.js46
-rw-r--r--devtools/client/webaudioeditor/test/browser_wa_navigate.js44
-rw-r--r--devtools/client/webaudioeditor/test/browser_wa_properties-view-edit-01.js65
-rw-r--r--devtools/client/webaudioeditor/test/browser_wa_properties-view-edit-02.js44
-rw-r--r--devtools/client/webaudioeditor/test/browser_wa_properties-view-media-nodes.js76
-rw-r--r--devtools/client/webaudioeditor/test/browser_wa_properties-view-params-objects.js46
-rw-r--r--devtools/client/webaudioeditor/test/browser_wa_properties-view-params.js43
-rw-r--r--devtools/client/webaudioeditor/test/browser_wa_properties-view.js42
-rw-r--r--devtools/client/webaudioeditor/test/browser_wa_reset-01.js67
-rw-r--r--devtools/client/webaudioeditor/test/browser_wa_reset-02.js37
-rw-r--r--devtools/client/webaudioeditor/test/browser_wa_reset-03.js48
-rw-r--r--devtools/client/webaudioeditor/test/browser_wa_reset-04.js66
-rw-r--r--devtools/client/webaudioeditor/test/browser_webaudio-actor-automation-event.js52
-rw-r--r--devtools/client/webaudioeditor/test/browser_webaudio-actor-connect-param.js25
-rw-r--r--devtools/client/webaudioeditor/test/browser_webaudio-actor-destroy-node.js41
-rw-r--r--devtools/client/webaudioeditor/test/browser_webaudio-actor-simple.js30
-rw-r--r--devtools/client/webaudioeditor/test/doc_automation.html30
-rw-r--r--devtools/client/webaudioeditor/test/doc_buffer-and-array.html56
-rw-r--r--devtools/client/webaudioeditor/test/doc_bug_1112378.html57
-rw-r--r--devtools/client/webaudioeditor/test/doc_bug_1125817.html23
-rw-r--r--devtools/client/webaudioeditor/test/doc_bug_1130901.html22
-rw-r--r--devtools/client/webaudioeditor/test/doc_bug_1141261.html25
-rw-r--r--devtools/client/webaudioeditor/test/doc_complex-context.html44
-rw-r--r--devtools/client/webaudioeditor/test/doc_connect-multi-param.html32
-rw-r--r--devtools/client/webaudioeditor/test/doc_connect-param.html28
-rw-r--r--devtools/client/webaudioeditor/test/doc_destroy-nodes.html36
-rw-r--r--devtools/client/webaudioeditor/test/doc_iframe-context.html14
-rw-r--r--devtools/client/webaudioeditor/test/doc_media-node-creation.html29
-rw-r--r--devtools/client/webaudioeditor/test/doc_simple-context.html33
-rw-r--r--devtools/client/webaudioeditor/test/doc_simple-node-creation.html28
-rw-r--r--devtools/client/webaudioeditor/test/head.js556
-rw-r--r--devtools/client/webaudioeditor/views/automation.js159
-rw-r--r--devtools/client/webaudioeditor/views/context.js314
-rw-r--r--devtools/client/webaudioeditor/views/inspector.js189
-rw-r--r--devtools/client/webaudioeditor/views/properties.js163
-rw-r--r--devtools/client/webaudioeditor/views/utils.js103
-rw-r--r--devtools/client/webaudioeditor/webaudioeditor.xul141
-rw-r--r--devtools/client/webconsole/.babelrc3
-rw-r--r--devtools/client/webconsole/console-commands.js103
-rw-r--r--devtools/client/webconsole/console-output.js3638
-rw-r--r--devtools/client/webconsole/hudservice.js718
-rw-r--r--devtools/client/webconsole/jsterm.js1766
-rw-r--r--devtools/client/webconsole/moz.build22
-rw-r--r--devtools/client/webconsole/net/.eslintrc.js20
-rw-r--r--devtools/client/webconsole/net/components/cookies-tab.js75
-rw-r--r--devtools/client/webconsole/net/components/headers-tab.js79
-rw-r--r--devtools/client/webconsole/net/components/moz.build25
-rw-r--r--devtools/client/webconsole/net/components/net-info-body.css112
-rw-r--r--devtools/client/webconsole/net/components/net-info-body.js179
-rw-r--r--devtools/client/webconsole/net/components/net-info-group-list.js47
-rw-r--r--devtools/client/webconsole/net/components/net-info-group.css80
-rw-r--r--devtools/client/webconsole/net/components/net-info-group.js80
-rw-r--r--devtools/client/webconsole/net/components/net-info-params.css23
-rw-r--r--devtools/client/webconsole/net/components/net-info-params.js58
-rw-r--r--devtools/client/webconsole/net/components/params-tab.js41
-rw-r--r--devtools/client/webconsole/net/components/post-tab.js279
-rw-r--r--devtools/client/webconsole/net/components/response-tab.css21
-rw-r--r--devtools/client/webconsole/net/components/response-tab.js277
-rw-r--r--devtools/client/webconsole/net/components/size-limit.css15
-rw-r--r--devtools/client/webconsole/net/components/size-limit.js62
-rw-r--r--devtools/client/webconsole/net/components/spinner.js26
-rw-r--r--devtools/client/webconsole/net/components/stacktrace-tab.js29
-rw-r--r--devtools/client/webconsole/net/data-provider.js66
-rw-r--r--devtools/client/webconsole/net/main.js98
-rw-r--r--devtools/client/webconsole/net/moz.build19
-rw-r--r--devtools/client/webconsole/net/net-request.css35
-rw-r--r--devtools/client/webconsole/net/net-request.js323
-rw-r--r--devtools/client/webconsole/net/test/mochitest/.eslintrc.js6
-rw-r--r--devtools/client/webconsole/net/test/mochitest/browser.ini22
-rw-r--r--devtools/client/webconsole/net/test/mochitest/browser_net_basic.js33
-rw-r--r--devtools/client/webconsole/net/test/mochitest/browser_net_cookies.js54
-rw-r--r--devtools/client/webconsole/net/test/mochitest/browser_net_headers.js40
-rw-r--r--devtools/client/webconsole/net/test/mochitest/browser_net_params.js69
-rw-r--r--devtools/client/webconsole/net/test/mochitest/browser_net_post.js88
-rw-r--r--devtools/client/webconsole/net/test/mochitest/browser_net_response.js86
-rw-r--r--devtools/client/webconsole/net/test/mochitest/head.js209
-rw-r--r--devtools/client/webconsole/net/test/mochitest/page_basic.html14
-rw-r--r--devtools/client/webconsole/net/test/mochitest/test-cookies.json1
-rw-r--r--devtools/client/webconsole/net/test/mochitest/test-cookies.json^headers^2
-rw-r--r--devtools/client/webconsole/net/test/mochitest/test.json1
-rw-r--r--devtools/client/webconsole/net/test/mochitest/test.json^headers^1
-rw-r--r--devtools/client/webconsole/net/test/mochitest/test.txt1
-rw-r--r--devtools/client/webconsole/net/test/mochitest/test.xml1
-rw-r--r--devtools/client/webconsole/net/test/mochitest/test.xml^headers^1
-rw-r--r--devtools/client/webconsole/net/test/unit/.eslintrc.js6
-rw-r--r--devtools/client/webconsole/net/test/unit/test_json-utils.js45
-rw-r--r--devtools/client/webconsole/net/test/unit/test_net-utils.js77
-rw-r--r--devtools/client/webconsole/net/test/unit/xpcshell.ini9
-rw-r--r--devtools/client/webconsole/net/utils/events.js21
-rw-r--r--devtools/client/webconsole/net/utils/json.js234
-rw-r--r--devtools/client/webconsole/net/utils/moz.build11
-rw-r--r--devtools/client/webconsole/net/utils/net.js134
-rw-r--r--devtools/client/webconsole/new-console-output/actions/enhancers.js20
-rw-r--r--devtools/client/webconsole/new-console-output/actions/filters.js55
-rw-r--r--devtools/client/webconsole/new-console-output/actions/index.js18
-rw-r--r--devtools/client/webconsole/new-console-output/actions/messages.js100
-rw-r--r--devtools/client/webconsole/new-console-output/actions/moz.build12
-rw-r--r--devtools/client/webconsole/new-console-output/actions/ui.js27
-rw-r--r--devtools/client/webconsole/new-console-output/components/collapse-button.js50
-rw-r--r--devtools/client/webconsole/new-console-output/components/console-output.js125
-rw-r--r--devtools/client/webconsole/new-console-output/components/console-table.js202
-rw-r--r--devtools/client/webconsole/new-console-output/components/filter-bar.js170
-rw-r--r--devtools/client/webconsole/new-console-output/components/filter-button.js46
-rw-r--r--devtools/client/webconsole/new-console-output/components/grip-message-body.js102
-rw-r--r--devtools/client/webconsole/new-console-output/components/message-container.js92
-rw-r--r--devtools/client/webconsole/new-console-output/components/message-icon.js32
-rw-r--r--devtools/client/webconsole/new-console-output/components/message-indent.js37
-rw-r--r--devtools/client/webconsole/new-console-output/components/message-repeat.js36
-rw-r--r--devtools/client/webconsole/new-console-output/components/message-types/console-api-call.js132
-rw-r--r--devtools/client/webconsole/new-console-output/components/message-types/console-command.js57
-rw-r--r--devtools/client/webconsole/new-console-output/components/message-types/default-renderer.js22
-rw-r--r--devtools/client/webconsole/new-console-output/components/message-types/evaluation-result.js64
-rw-r--r--devtools/client/webconsole/new-console-output/components/message-types/moz.build13
-rw-r--r--devtools/client/webconsole/new-console-output/components/message-types/network-event-message.js63
-rw-r--r--devtools/client/webconsole/new-console-output/components/message-types/page-error.js69
-rw-r--r--devtools/client/webconsole/new-console-output/components/message.js176
-rw-r--r--devtools/client/webconsole/new-console-output/components/moz.build23
-rw-r--r--devtools/client/webconsole/new-console-output/components/variables-view-link.js34
-rw-r--r--devtools/client/webconsole/new-console-output/constants.js81
-rw-r--r--devtools/client/webconsole/new-console-output/main.js23
-rw-r--r--devtools/client/webconsole/new-console-output/moz.build21
-rw-r--r--devtools/client/webconsole/new-console-output/new-console-output-wrapper.js134
-rw-r--r--devtools/client/webconsole/new-console-output/reducers/filters.js39
-rw-r--r--devtools/client/webconsole/new-console-output/reducers/index.js18
-rw-r--r--devtools/client/webconsole/new-console-output/reducers/messages.js135
-rw-r--r--devtools/client/webconsole/new-console-output/reducers/moz.build12
-rw-r--r--devtools/client/webconsole/new-console-output/reducers/prefs.js18
-rw-r--r--devtools/client/webconsole/new-console-output/reducers/ui.js39
-rw-r--r--devtools/client/webconsole/new-console-output/selectors/filters.js12
-rw-r--r--devtools/client/webconsole/new-console-output/selectors/messages.js168
-rw-r--r--devtools/client/webconsole/new-console-output/selectors/moz.build11
-rw-r--r--devtools/client/webconsole/new-console-output/selectors/prefs.js12
-rw-r--r--devtools/client/webconsole/new-console-output/selectors/ui.js20
-rw-r--r--devtools/client/webconsole/new-console-output/store.js74
-rw-r--r--devtools/client/webconsole/new-console-output/test/.eslintrc.js5
-rw-r--r--devtools/client/webconsole/new-console-output/test/chrome/chrome.ini7
-rw-r--r--devtools/client/webconsole/new-console-output/test/chrome/head.js16
-rw-r--r--devtools/client/webconsole/new-console-output/test/chrome/test_render_perf.html90
-rw-r--r--devtools/client/webconsole/new-console-output/test/components/console-api-call.test.js230
-rw-r--r--devtools/client/webconsole/new-console-output/test/components/evaluation-result.test.js84
-rw-r--r--devtools/client/webconsole/new-console-output/test/components/filter-bar.test.js96
-rw-r--r--devtools/client/webconsole/new-console-output/test/components/filter-button.test.js34
-rw-r--r--devtools/client/webconsole/new-console-output/test/components/message-container.test.js54
-rw-r--r--devtools/client/webconsole/new-console-output/test/components/message-icon.test.js23
-rw-r--r--devtools/client/webconsole/new-console-output/test/components/message-repeat.test.js25
-rw-r--r--devtools/client/webconsole/new-console-output/test/components/network-event-message.test.js74
-rw-r--r--devtools/client/webconsole/new-console-output/test/components/page-error.test.js126
-rw-r--r--devtools/client/webconsole/new-console-output/test/fixtures/L10n.js27
-rw-r--r--devtools/client/webconsole/new-console-output/test/fixtures/LocalizationHelper.js10
-rw-r--r--devtools/client/webconsole/new-console-output/test/fixtures/ObjectClient.js9
-rw-r--r--devtools/client/webconsole/new-console-output/test/fixtures/PluralForm.js18
-rw-r--r--devtools/client/webconsole/new-console-output/test/fixtures/Services.js27
-rw-r--r--devtools/client/webconsole/new-console-output/test/fixtures/WebConsoleUtils.js14
-rw-r--r--devtools/client/webconsole/new-console-output/test/fixtures/moz.build9
-rw-r--r--devtools/client/webconsole/new-console-output/test/fixtures/serviceContainer.js17
-rw-r--r--devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/browser.ini18
-rw-r--r--devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/browser_webconsole_update_stubs_console_api.js56
-rw-r--r--devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/browser_webconsole_update_stubs_evaluation_result.js32
-rw-r--r--devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/browser_webconsole_update_stubs_network_event.js47
-rw-r--r--devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/browser_webconsole_update_stubs_page_error.js48
-rw-r--r--devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/head.js192
-rw-r--r--devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/moz.build8
-rw-r--r--devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/stub-snippets.js148
-rw-r--r--devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html11
-rw-r--r--devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-network-event.html11
-rw-r--r--devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js0
-rw-r--r--devtools/client/webconsole/new-console-output/test/fixtures/stubs/consoleApi.js1482
-rw-r--r--devtools/client/webconsole/new-console-output/test/fixtures/stubs/evaluationResult.js182
-rw-r--r--devtools/client/webconsole/new-console-output/test/fixtures/stubs/index.js29
-rw-r--r--devtools/client/webconsole/new-console-output/test/fixtures/stubs/moz.build11
-rw-r--r--devtools/client/webconsole/new-console-output/test/fixtures/stubs/networkEvent.js189
-rw-r--r--devtools/client/webconsole/new-console-output/test/fixtures/stubs/pageError.js102
-rw-r--r--devtools/client/webconsole/new-console-output/test/helpers.js67
-rw-r--r--devtools/client/webconsole/new-console-output/test/mochitest/browser.ini21
-rw-r--r--devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_batching.js51
-rw-r--r--devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_console_group.js91
-rw-r--r--devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_console_table.js173
-rw-r--r--devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_filters.js72
-rw-r--r--devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_init.js35
-rw-r--r--devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_input_focus.js57
-rw-r--r--devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_keyboard_accessibility.js71
-rw-r--r--devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_observer_notifications.js47
-rw-r--r--devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_vview_close_on_esc_key.js46
-rw-r--r--devtools/client/webconsole/new-console-output/test/mochitest/head.js137
-rw-r--r--devtools/client/webconsole/new-console-output/test/mochitest/test-batching.html28
-rw-r--r--devtools/client/webconsole/new-console-output/test/mochitest/test-console-filters.html17
-rw-r--r--devtools/client/webconsole/new-console-output/test/mochitest/test-console-group.html28
-rw-r--r--devtools/client/webconsole/new-console-output/test/mochitest/test-console-table.html19
-rw-r--r--devtools/client/webconsole/new-console-output/test/mochitest/test-console.html18
-rw-r--r--devtools/client/webconsole/new-console-output/test/moz.build17
-rw-r--r--devtools/client/webconsole/new-console-output/test/requireHelper.js38
-rw-r--r--devtools/client/webconsole/new-console-output/test/store/filters.test.js215
-rw-r--r--devtools/client/webconsole/new-console-output/test/store/messages.test.js353
-rw-r--r--devtools/client/webconsole/new-console-output/test/utils/getRepeatId.test.js41
-rw-r--r--devtools/client/webconsole/new-console-output/types.js53
-rw-r--r--devtools/client/webconsole/new-console-output/utils/id-generator.js22
-rw-r--r--devtools/client/webconsole/new-console-output/utils/messages.js283
-rw-r--r--devtools/client/webconsole/new-console-output/utils/moz.build10
-rw-r--r--devtools/client/webconsole/new-console-output/utils/variables-view.js20
-rw-r--r--devtools/client/webconsole/package.json20
-rw-r--r--devtools/client/webconsole/panel.js118
-rw-r--r--devtools/client/webconsole/test/.eslintrc.js6
-rw-r--r--devtools/client/webconsole/test/browser.ini396
-rw-r--r--devtools/client/webconsole/test/browser_bug1045902_console_csp_ignore_reflected_xss_message.js52
-rw-r--r--devtools/client/webconsole/test/browser_bug664688_sandbox_update_after_navigation.js92
-rw-r--r--devtools/client/webconsole/test/browser_bug_638949_copy_link_location.js107
-rw-r--r--devtools/client/webconsole/test/browser_bug_862916_console_dir_and_filter_off.js31
-rw-r--r--devtools/client/webconsole/test/browser_bug_865288_repeat_different_objects.js63
-rw-r--r--devtools/client/webconsole/test/browser_bug_865871_variables_view_close_on_esc_key.js75
-rw-r--r--devtools/client/webconsole/test/browser_bug_869003_inspect_cross_domain_object.js77
-rw-r--r--devtools/client/webconsole/test/browser_bug_871156_ctrlw_close_tab.js78
-rw-r--r--devtools/client/webconsole/test/browser_cached_messages.js59
-rw-r--r--devtools/client/webconsole/test/browser_console.js160
-rw-r--r--devtools/client/webconsole/test/browser_console_addonsdk_loader_exception.js92
-rw-r--r--devtools/client/webconsole/test/browser_console_clear_method.js41
-rw-r--r--devtools/client/webconsole/test/browser_console_clear_on_reload.js86
-rw-r--r--devtools/client/webconsole/test/browser_console_click_focus.js59
-rw-r--r--devtools/client/webconsole/test/browser_console_consolejsm_output.js285
-rw-r--r--devtools/client/webconsole/test/browser_console_copy_command.js76
-rw-r--r--devtools/client/webconsole/test/browser_console_copy_entire_message_context_menu.js97
-rw-r--r--devtools/client/webconsole/test/browser_console_dead_objects.js88
-rw-r--r--devtools/client/webconsole/test/browser_console_error_source_click.js79
-rw-r--r--devtools/client/webconsole/test/browser_console_filters.js60
-rw-r--r--devtools/client/webconsole/test/browser_console_hide_jsterm_when_devtools_chrome_enabled_false.js114
-rw-r--r--devtools/client/webconsole/test/browser_console_history_persist.js119
-rw-r--r--devtools/client/webconsole/test/browser_console_iframe_messages.js114
-rw-r--r--devtools/client/webconsole/test/browser_console_keyboard_accessibility.js89
-rw-r--r--devtools/client/webconsole/test/browser_console_log_inspectable_object.js52
-rw-r--r--devtools/client/webconsole/test/browser_console_native_getters.js101
-rw-r--r--devtools/client/webconsole/test/browser_console_navigation_marker.js81
-rw-r--r--devtools/client/webconsole/test/browser_console_netlogging.js38
-rw-r--r--devtools/client/webconsole/test/browser_console_nsiconsolemessage.js85
-rw-r--r--devtools/client/webconsole/test/browser_console_open_or_focus.js46
-rw-r--r--devtools/client/webconsole/test/browser_console_optimized_out_vars.js91
-rw-r--r--devtools/client/webconsole/test/browser_console_private_browsing.js192
-rw-r--r--devtools/client/webconsole/test/browser_console_server_logging.js74
-rw-r--r--devtools/client/webconsole/test/browser_console_variables_view.js204
-rw-r--r--devtools/client/webconsole/test/browser_console_variables_view_dom_nodes.js59
-rw-r--r--devtools/client/webconsole/test/browser_console_variables_view_dont_sort_non_sortable_classes_properties.js135
-rw-r--r--devtools/client/webconsole/test/browser_console_variables_view_filter.js80
-rw-r--r--devtools/client/webconsole/test/browser_console_variables_view_highlighter.js97
-rw-r--r--devtools/client/webconsole/test/browser_console_variables_view_special_names.js38
-rw-r--r--devtools/client/webconsole/test/browser_console_variables_view_while_debugging.js109
-rw-r--r--devtools/client/webconsole/test/browser_console_variables_view_while_debugging_and_inspecting.js112
-rw-r--r--devtools/client/webconsole/test/browser_eval_in_debugger_stackframe.js157
-rw-r--r--devtools/client/webconsole/test/browser_eval_in_debugger_stackframe2.js71
-rw-r--r--devtools/client/webconsole/test/browser_jsterm_inspect.js47
-rw-r--r--devtools/client/webconsole/test/browser_longstring_hang.js57
-rw-r--r--devtools/client/webconsole/test/browser_netmonitor_shows_reqs_in_webconsole.js74
-rw-r--r--devtools/client/webconsole/test/browser_output_breaks_after_console_dir_uninspectable.js47
-rw-r--r--devtools/client/webconsole/test/browser_output_longstring_expand.js85
-rw-r--r--devtools/client/webconsole/test/browser_repeated_messages_accuracy.js178
-rw-r--r--devtools/client/webconsole/test/browser_result_format_as_string.js40
-rw-r--r--devtools/client/webconsole/test/browser_warn_user_about_replaced_api.js86
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_allow_mixedcontent_securityerrors.js69
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_assert.js56
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_autocomplete-properties-with-non-alphanumeric-names.js47
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_autocomplete_accessibility.js60
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_autocomplete_and_selfxss.js130
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_autocomplete_crossdomain_iframe.js64
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_autocomplete_in_debugger_stackframe.js245
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_autocomplete_popup_close_on_tab_switch.js27
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_block_mixedcontent_securityerrors.js110
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_1006027_message_timestamps_incorrect.js45
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_1010953_cspro.js55
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_1050691_click_function_to_source.js60
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_1247459_violation.js40
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_578437_page_reload.js41
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_579412_input_focus.js20
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_580001_closing_after_completion.js47
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_580030_errors_after_page_reload.js50
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_580454_timestamp_l10n.js26
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_582201_duplicate_errors.js49
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_583816_No_input_and_Tab_key_pressed.js35
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_585237_line_limit.js89
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_585956_console_trace.js70
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_585991_autocomplete_keys.js367
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_585991_autocomplete_popup.js123
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_586388_select_all.js84
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_587617_output_copy.js106
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_588342_document_focus.js36
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_588730_text_node_insertion.js53
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_588967_input_expansion.js44
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_589162_css_filter.js39
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_592442_closing_brackets.js29
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_593003_iframe_wrong_hud.js68
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_594497_history_arrow_keys.js155
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_595223_file_uri.js64
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_595350_multiple_windows_and_tabs.js100
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_595934_message_categories.js211
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_597103_deactivateHUDForContext_unfocused_window.js97
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_597136_external_script_errors.js33
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_597136_network_requests_from_chrome.js52
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_597460_filter_scroll.js80
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_597756_reopen_closed_tab.js70
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_599725_response_headers.js67
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_600183_charset.js59
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_601177_log_levels.js76
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_601352_scroll.js84
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_601667_filter_buttons.js267
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_603750_websocket.js37
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_611795.js67
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_613013_console_api_iframe.js26
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_613280_jsterm_copy.js64
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_613642_maintain_scroll.js119
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_613642_prune_scroll.js82
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_614793_jsterm_scroll.js54
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_618078_network_exceptions.js36
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_621644_jsterm_dollar.js47
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_622303_persistent_filters.js149
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_623749_ctrl_a_select_all_winnt.js32
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_630733_response_redirect_headers.js120
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_632275_getters_document_width.js47
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_632347_iterators_generators.js84
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_632817.js217
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_642108_pruneTest.js81
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_644419_log_limits.js235
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_646025_console_file_location.js57
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_651501_document_body_autocomplete.js102
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_653531_highlighter_console_helper.js109
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_658368_time_methods.js67
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_659907_console_dir.js36
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_660806_history_nav.js54
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_664131_console_group.js79
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_686937_autocomplete_JSTerm_helpers.js75
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_704295.js41
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_734061_No_input_change_and_Tab_key_pressed.js35
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_737873_mixedcontent.js63
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_752559_ineffective_iframe_sandbox_warning.js83
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_762593_insecure_passwords_about_blank_web_console_warning.js32
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_762593_insecure_passwords_web_console_warning.js62
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_764572_output_open_url.js142
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_766001_JS_Console_in_Debugger.js88
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_770099_violation.js35
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_782653_CSS_links_in_Style_Editor.js140
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_804845_ctrl_key_nav.js227
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_817834_add_edited_input_to_history.js57
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_837351_securityerrors.js42
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_922212_console_dirxml.js48
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_cached_autocomplete.js114
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_cd_iframe.js115
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_certificate_messages.js81
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_chrome.js38
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_clear_method.js131
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_clickable_urls.js103
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_closure_inspection.js100
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_column_numbers.js46
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_completion.js106
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_console_api_stackframe.js85
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_console_custom_styles.js81
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_console_extras.js43
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_console_logging_api.js102
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_console_logging_workers_api.js39
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_console_trace_async.js75
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_console_trace_duplicates.js50
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_context_menu_open_in_var_view.js51
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_context_menu_store_as_global.js66
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_count.js77
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_dont_navigate_on_doubleclick.js56
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_exception_stackframe.js104
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_execution_scope.js37
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_expandable_timestamps.js57
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_filter_buttons_contextmenu.js95
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_for_of.js32
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_history.js62
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_hpkp_invalid-headers.js126
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_hsts_invalid-headers.js92
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_input_field_focus_on_panel_select.js34
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_inspect-parsed-documents.js35
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_js_input_expansion.js55
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_jsterm.js195
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_live_filtering_of_message_types.js56
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_live_filtering_on_search_strings.js96
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_log_file_filter.js83
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_message_node_id.js28
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_multiline_input.js70
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_netlogging.js139
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_netlogging_basic.js44
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_netlogging_panel.js30
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_netlogging_reset_filter.js95
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_notifications.js77
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_open-links-without-callback.js54
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_output_01.js122
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_output_02.js183
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_output_03.js168
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_output_04.js129
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_output_05.js177
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_output_06.js283
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_output_copy_newlines.js72
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_output_dom_elements_01.js122
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_output_dom_elements_02.js66
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_output_dom_elements_03.js70
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_output_dom_elements_04.js113
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_output_dom_elements_05.js47
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_output_events.js54
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_output_order.js47
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_output_regexp.js35
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_output_table.js199
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_promise.js35
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_property_provider.js46
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_reflow.js33
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_scratchpad_panel_link.js76
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_script_errordoc_urls.js67
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_show_subresource_security_errors.js39
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_shows_reqs_in_netmonitor.js73
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_split.js268
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_split_escape_key.js158
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_split_focus.js66
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_split_persist.js119
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_start_netmon_first.js38
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_strict_mode_errors.js83
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_trackingprotection_errors.js54
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_view_source.js52
-rw-r--r--devtools/client/webconsole/test/head.js1844
-rw-r--r--devtools/client/webconsole/test/test-autocomplete-in-stackframe.html50
-rw-r--r--devtools/client/webconsole/test/test-bug-585956-console-trace.html27
-rw-r--r--devtools/client/webconsole/test/test-bug-593003-iframe-wrong-hud-iframe.html13
-rw-r--r--devtools/client/webconsole/test/test-bug-593003-iframe-wrong-hud.html14
-rw-r--r--devtools/client/webconsole/test/test-bug-595934-canvas-css.html17
-rw-r--r--devtools/client/webconsole/test/test-bug-595934-canvas-css.js10
-rw-r--r--devtools/client/webconsole/test/test-bug-595934-css-loader.css10
-rw-r--r--devtools/client/webconsole/test/test-bug-595934-css-loader.css^headers^1
-rw-r--r--devtools/client/webconsole/test/test-bug-595934-css-loader.html13
-rw-r--r--devtools/client/webconsole/test/test-bug-595934-css-parser.css10
-rw-r--r--devtools/client/webconsole/test/test-bug-595934-css-parser.html14
-rw-r--r--devtools/client/webconsole/test/test-bug-595934-empty-getelementbyid.html16
-rw-r--r--devtools/client/webconsole/test/test-bug-595934-empty-getelementbyid.js8
-rw-r--r--devtools/client/webconsole/test/test-bug-595934-html.html16
-rw-r--r--devtools/client/webconsole/test/test-bug-595934-image.html15
-rw-r--r--devtools/client/webconsole/test/test-bug-595934-image.jpgbin0 -> 2532 bytes
-rw-r--r--devtools/client/webconsole/test/test-bug-595934-imagemap.html17
-rw-r--r--devtools/client/webconsole/test/test-bug-595934-malformedxml-external.html19
-rw-r--r--devtools/client/webconsole/test/test-bug-595934-malformedxml-external.xml8
-rw-r--r--devtools/client/webconsole/test/test-bug-595934-malformedxml.xhtml10
-rw-r--r--devtools/client/webconsole/test/test-bug-595934-svg.xhtml17
-rw-r--r--devtools/client/webconsole/test/test-bug-595934-workers.html18
-rw-r--r--devtools/client/webconsole/test/test-bug-595934-workers.js14
-rw-r--r--devtools/client/webconsole/test/test-bug-597136-external-script-errors.html25
-rw-r--r--devtools/client/webconsole/test/test-bug-597136-external-script-errors.js9
-rw-r--r--devtools/client/webconsole/test/test-bug-597756-reopen-closed-tab.html18
-rw-r--r--devtools/client/webconsole/test/test-bug-599725-response-headers.sjs25
-rw-r--r--devtools/client/webconsole/test/test-bug-600183-charset.html9
-rw-r--r--devtools/client/webconsole/test/test-bug-600183-charset.html^headers^1
-rw-r--r--devtools/client/webconsole/test/test-bug-601177-log-levels.html20
-rw-r--r--devtools/client/webconsole/test/test-bug-601177-log-levels.js8
-rw-r--r--devtools/client/webconsole/test/test-bug-603750-websocket.html14
-rw-r--r--devtools/client/webconsole/test/test-bug-603750-websocket.js20
-rw-r--r--devtools/client/webconsole/test/test-bug-609872-cd-iframe-child.html13
-rw-r--r--devtools/client/webconsole/test/test-bug-609872-cd-iframe-parent.html14
-rw-r--r--devtools/client/webconsole/test/test-bug-613013-console-api-iframe.html21
-rw-r--r--devtools/client/webconsole/test/test-bug-618078-network-exceptions.html24
-rw-r--r--devtools/client/webconsole/test/test-bug-621644-jsterm-dollar.html23
-rw-r--r--devtools/client/webconsole/test/test-bug-630733-response-redirect-headers.sjs16
-rw-r--r--devtools/client/webconsole/test/test-bug-632275-getters.html20
-rw-r--r--devtools/client/webconsole/test/test-bug-632347-iterators-generators.html56
-rw-r--r--devtools/client/webconsole/test/test-bug-644419-log-limits.html21
-rw-r--r--devtools/client/webconsole/test/test-bug-646025-console-file-location.html12
-rw-r--r--devtools/client/webconsole/test/test-bug-658368-time-methods.html24
-rw-r--r--devtools/client/webconsole/test/test-bug-737873-mixedcontent.html15
-rw-r--r--devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning-inner.html13
-rw-r--r--devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning-nested1.html14
-rw-r--r--devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning-nested2.html14
-rw-r--r--devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning0.html13
-rw-r--r--devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning1.html13
-rw-r--r--devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning2.html13
-rw-r--r--devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning3.html14
-rw-r--r--devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning4.html14
-rw-r--r--devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning5.html14
-rw-r--r--devtools/client/webconsole/test/test-bug-762593-insecure-passwords-about-blank-web-console-warning.html28
-rw-r--r--devtools/client/webconsole/test/test-bug-762593-insecure-passwords-web-console-warning.html16
-rw-r--r--devtools/client/webconsole/test/test-bug-766001-console-log.js10
-rw-r--r--devtools/client/webconsole/test/test-bug-766001-js-console-links.html14
-rw-r--r--devtools/client/webconsole/test/test-bug-766001-js-errors.js8
-rw-r--r--devtools/client/webconsole/test/test-bug-782653-css-errors-1.css10
-rw-r--r--devtools/client/webconsole/test/test-bug-782653-css-errors-2.css10
-rw-r--r--devtools/client/webconsole/test/test-bug-782653-css-errors.html14
-rw-r--r--devtools/client/webconsole/test/test-bug-837351-security-errors.html15
-rw-r--r--devtools/client/webconsole/test/test-bug-859170-longstring-hang.html23
-rw-r--r--devtools/client/webconsole/test/test-bug-869003-iframe.html20
-rw-r--r--devtools/client/webconsole/test/test-bug-869003-top-window.html14
-rw-r--r--devtools/client/webconsole/test/test-bug-952277-highlight-nodes-in-vview.html15
-rw-r--r--devtools/client/webconsole/test/test-bug-989025-iframe-parent.html13
-rw-r--r--devtools/client/webconsole/test/test-bug_1050691_click_function_to_source.html11
-rw-r--r--devtools/client/webconsole/test/test-bug_1050691_click_function_to_source.js10
-rw-r--r--devtools/client/webconsole/test/test-bug_923281_console_log_filter.html12
-rw-r--r--devtools/client/webconsole/test/test-bug_923281_test1.js7
-rw-r--r--devtools/client/webconsole/test/test-bug_923281_test2.js8
-rw-r--r--devtools/client/webconsole/test/test-bug_939783_console_trace_duplicates.html35
-rw-r--r--devtools/client/webconsole/test/test-certificate-messages.html22
-rw-r--r--devtools/client/webconsole/test/test-closure-optimized-out.html34
-rw-r--r--devtools/client/webconsole/test/test-closures.html26
-rw-r--r--devtools/client/webconsole/test/test-console-api-stackframe.html32
-rw-r--r--devtools/client/webconsole/test/test-console-assert.html23
-rw-r--r--devtools/client/webconsole/test/test-console-clear.html16
-rw-r--r--devtools/client/webconsole/test/test-console-column.html17
-rw-r--r--devtools/client/webconsole/test/test-console-count-external-file.js11
-rw-r--r--devtools/client/webconsole/test/test-console-count.html56
-rw-r--r--devtools/client/webconsole/test/test-console-extras.html18
-rw-r--r--devtools/client/webconsole/test/test-console-output-02.html66
-rw-r--r--devtools/client/webconsole/test/test-console-output-03.html30
-rw-r--r--devtools/client/webconsole/test/test-console-output-04.html77
-rw-r--r--devtools/client/webconsole/test/test-console-output-dom-elements.html91
-rw-r--r--devtools/client/webconsole/test/test-console-output-events.html42
-rw-r--r--devtools/client/webconsole/test/test-console-output-regexp.html23
-rw-r--r--devtools/client/webconsole/test/test-console-replaced-api.html12
-rw-r--r--devtools/client/webconsole/test/test-console-server-logging-array.sjs32
-rw-r--r--devtools/client/webconsole/test/test-console-server-logging.sjs32
-rw-r--r--devtools/client/webconsole/test/test-console-table.html63
-rw-r--r--devtools/client/webconsole/test/test-console-trace-async.html24
-rw-r--r--devtools/client/webconsole/test/test-console-workers.html13
-rw-r--r--devtools/client/webconsole/test/test-console.html34
-rw-r--r--devtools/client/webconsole/test/test-consoleiframes.html13
-rw-r--r--devtools/client/webconsole/test/test-cu-reporterror.js4
-rw-r--r--devtools/client/webconsole/test/test-data.json1
-rw-r--r--devtools/client/webconsole/test/test-data.json^headers^1
-rw-r--r--devtools/client/webconsole/test/test-duplicate-error.html21
-rw-r--r--devtools/client/webconsole/test/test-encoding-ISO-8859-1.html7
-rw-r--r--devtools/client/webconsole/test/test-error.html21
-rw-r--r--devtools/client/webconsole/test/test-eval-in-stackframe.html39
-rw-r--r--devtools/client/webconsole/test/test-exception-stackframe.html43
-rw-r--r--devtools/client/webconsole/test/test-file-location.js12
-rw-r--r--devtools/client/webconsole/test/test-filter.html11
-rw-r--r--devtools/client/webconsole/test/test-for-of.html8
-rw-r--r--devtools/client/webconsole/test/test-iframe-762593-insecure-form-action.html15
-rw-r--r--devtools/client/webconsole/test/test-iframe-762593-insecure-frame.html15
-rw-r--r--devtools/client/webconsole/test/test-iframe1.html10
-rw-r--r--devtools/client/webconsole/test/test-iframe2.html11
-rw-r--r--devtools/client/webconsole/test/test-iframe3.html11
-rw-r--r--devtools/client/webconsole/test/test-image.pngbin0 -> 580 bytes
-rw-r--r--devtools/client/webconsole/test/test-mixedcontent-securityerrors.html21
-rw-r--r--devtools/client/webconsole/test/test-mutation.html16
-rw-r--r--devtools/client/webconsole/test/test-network-request.html40
-rw-r--r--devtools/client/webconsole/test/test-network.html11
-rw-r--r--devtools/client/webconsole/test/test-observe-http-ajax.html17
-rw-r--r--devtools/client/webconsole/test/test-own-console.html24
-rw-r--r--devtools/client/webconsole/test/test-property-provider.html14
-rw-r--r--devtools/client/webconsole/test/test-repeated-messages.html53
-rw-r--r--devtools/client/webconsole/test/test-result-format-as-string.html25
-rw-r--r--devtools/client/webconsole/test/test-trackingprotection-securityerrors.html12
-rw-r--r--devtools/client/webconsole/test/test-webconsole-error-observer.html25
-rw-r--r--devtools/client/webconsole/test/test_bug1045902_console_csp_ignore_reflected_xss_message.html10
-rw-r--r--devtools/client/webconsole/test/test_bug1045902_console_csp_ignore_reflected_xss_message.html^headers^1
-rw-r--r--devtools/client/webconsole/test/test_bug1092055_shouldwarn.html15
-rw-r--r--devtools/client/webconsole/test/test_bug1092055_shouldwarn.js2
-rw-r--r--devtools/client/webconsole/test/test_bug1092055_shouldwarn.js^headers^1
-rw-r--r--devtools/client/webconsole/test/test_bug_1010953_cspro.html20
-rw-r--r--devtools/client/webconsole/test/test_bug_1010953_cspro.html^headers^2
-rw-r--r--devtools/client/webconsole/test/test_bug_1247459_violation.html15
-rw-r--r--devtools/client/webconsole/test/test_bug_770099_violation.html13
-rw-r--r--devtools/client/webconsole/test/test_bug_770099_violation.html^headers^1
-rw-r--r--devtools/client/webconsole/test/test_hpkp-invalid-headers.sjs53
-rw-r--r--devtools/client/webconsole/test/test_hsts-invalid-headers.sjs39
-rw-r--r--devtools/client/webconsole/test/testscript.js2
-rw-r--r--devtools/client/webconsole/utils.js395
-rw-r--r--devtools/client/webconsole/webconsole.js3658
-rw-r--r--devtools/client/webconsole/webconsole.xul214
-rw-r--r--devtools/client/webide/components/moz.build10
-rw-r--r--devtools/client/webide/components/webideCli.js58
-rw-r--r--devtools/client/webide/components/webideComponents.manifest4
-rw-r--r--devtools/client/webide/content/addons.js135
-rw-r--r--devtools/client/webide/content/addons.xhtml31
-rw-r--r--devtools/client/webide/content/details.js139
-rw-r--r--devtools/client/webide/content/details.xhtml54
-rw-r--r--devtools/client/webide/content/devicepreferences.js81
-rw-r--r--devtools/client/webide/content/devicepreferences.xhtml49
-rw-r--r--devtools/client/webide/content/devicesettings.js81
-rw-r--r--devtools/client/webide/content/devicesettings.xhtml50
-rw-r--r--devtools/client/webide/content/jar.mn38
-rw-r--r--devtools/client/webide/content/logs.js70
-rw-r--r--devtools/client/webide/content/logs.xhtml33
-rw-r--r--devtools/client/webide/content/monitor.js741
-rw-r--r--devtools/client/webide/content/monitor.xhtml31
-rw-r--r--devtools/client/webide/content/moz.build7
-rw-r--r--devtools/client/webide/content/newapp.js175
-rw-r--r--devtools/client/webide/content/newapp.xul33
-rw-r--r--devtools/client/webide/content/permissionstable.js78
-rw-r--r--devtools/client/webide/content/permissionstable.xhtml36
-rw-r--r--devtools/client/webide/content/prefs.js108
-rw-r--r--devtools/client/webide/content/prefs.xhtml112
-rw-r--r--devtools/client/webide/content/project-listing.js42
-rw-r--r--devtools/client/webide/content/project-listing.xhtml35
-rw-r--r--devtools/client/webide/content/project-panel.js11
-rw-r--r--devtools/client/webide/content/runtime-listing.js66
-rw-r--r--devtools/client/webide/content/runtime-listing.xhtml45
-rw-r--r--devtools/client/webide/content/runtime-panel.js11
-rw-r--r--devtools/client/webide/content/runtimedetails.js153
-rw-r--r--devtools/client/webide/content/runtimedetails.xhtml46
-rw-r--r--devtools/client/webide/content/simulator.js352
-rw-r--r--devtools/client/webide/content/simulator.xhtml99
-rw-r--r--devtools/client/webide/content/webide.js1157
-rw-r--r--devtools/client/webide/content/webide.xul178
-rw-r--r--devtools/client/webide/content/wifi-auth.js44
-rw-r--r--devtools/client/webide/content/wifi-auth.xhtml45
-rw-r--r--devtools/client/webide/modules/addons.js197
-rw-r--r--devtools/client/webide/modules/app-manager.js850
-rw-r--r--devtools/client/webide/modules/app-projects.js235
-rw-r--r--devtools/client/webide/modules/app-validator.js292
-rw-r--r--devtools/client/webide/modules/build.js199
-rw-r--r--devtools/client/webide/modules/config-view.js373
-rw-r--r--devtools/client/webide/modules/moz.build21
-rw-r--r--devtools/client/webide/modules/project-list.js375
-rw-r--r--devtools/client/webide/modules/runtime-list.js207
-rw-r--r--devtools/client/webide/modules/runtimes.js673
-rw-r--r--devtools/client/webide/modules/simulator-process.js325
-rw-r--r--devtools/client/webide/modules/simulators.js368
-rw-r--r--devtools/client/webide/modules/tab-store.js178
-rw-r--r--devtools/client/webide/modules/utils.js68
-rw-r--r--devtools/client/webide/moz.build23
-rw-r--r--devtools/client/webide/test/.eslintrc.js6
-rw-r--r--devtools/client/webide/test/addons/adbhelper-linux.xpibin0 -> 1293 bytes
-rw-r--r--devtools/client/webide/test/addons/adbhelper-linux64.xpibin0 -> 1293 bytes
-rw-r--r--devtools/client/webide/test/addons/adbhelper-mac64.xpibin0 -> 1293 bytes
-rw-r--r--devtools/client/webide/test/addons/adbhelper-win32.xpibin0 -> 1293 bytes
-rw-r--r--devtools/client/webide/test/addons/fxdt-adapters-linux32.xpibin0 -> 1156 bytes
-rw-r--r--devtools/client/webide/test/addons/fxdt-adapters-linux64.xpibin0 -> 1156 bytes
-rw-r--r--devtools/client/webide/test/addons/fxdt-adapters-mac64.xpibin0 -> 1156 bytes
-rw-r--r--devtools/client/webide/test/addons/fxdt-adapters-win32.xpibin0 -> 1156 bytes
-rw-r--r--devtools/client/webide/test/addons/fxos_1_0_simulator-linux.xpibin0 -> 5046 bytes
-rw-r--r--devtools/client/webide/test/addons/fxos_1_0_simulator-linux64.xpibin0 -> 5046 bytes
-rw-r--r--devtools/client/webide/test/addons/fxos_1_0_simulator-mac64.xpibin0 -> 5044 bytes
-rw-r--r--devtools/client/webide/test/addons/fxos_1_0_simulator-win32.xpibin0 -> 5046 bytes
-rw-r--r--devtools/client/webide/test/addons/fxos_2_0_simulator-linux.xpibin0 -> 5046 bytes
-rw-r--r--devtools/client/webide/test/addons/fxos_2_0_simulator-linux64.xpibin0 -> 5046 bytes
-rw-r--r--devtools/client/webide/test/addons/fxos_2_0_simulator-mac64.xpibin0 -> 5043 bytes
-rw-r--r--devtools/client/webide/test/addons/fxos_2_0_simulator-win32.xpibin0 -> 5045 bytes
-rw-r--r--devtools/client/webide/test/addons/fxos_3_0_simulator-linux.xpibin0 -> 5045 bytes
-rw-r--r--devtools/client/webide/test/addons/fxos_3_0_simulator-linux64.xpibin0 -> 5048 bytes
-rw-r--r--devtools/client/webide/test/addons/fxos_3_0_simulator-mac64.xpibin0 -> 5048 bytes
-rw-r--r--devtools/client/webide/test/addons/fxos_3_0_simulator-win32.xpibin0 -> 5044 bytes
-rw-r--r--devtools/client/webide/test/addons/fxos_3_0_tv_simulator-linux.xpibin0 -> 5052 bytes
-rw-r--r--devtools/client/webide/test/addons/fxos_3_0_tv_simulator-linux64.xpibin0 -> 5055 bytes
-rw-r--r--devtools/client/webide/test/addons/fxos_3_0_tv_simulator-mac64.xpibin0 -> 5051 bytes
-rw-r--r--devtools/client/webide/test/addons/fxos_3_0_tv_simulator-win32.xpibin0 -> 5051 bytes
-rw-r--r--devtools/client/webide/test/addons/simulators.json4
-rw-r--r--devtools/client/webide/test/app.zipbin0 -> 480 bytes
-rw-r--r--devtools/client/webide/test/app/index.html6
-rw-r--r--devtools/client/webide/test/app/manifest.webapp5
-rw-r--r--devtools/client/webide/test/browser.ini12
-rw-r--r--devtools/client/webide/test/browser_tabs.js84
-rw-r--r--devtools/client/webide/test/browser_widget.js15
-rw-r--r--devtools/client/webide/test/build_app1/package.json5
-rw-r--r--devtools/client/webide/test/build_app2/manifest.webapp1
-rw-r--r--devtools/client/webide/test/build_app2/package.json10
-rw-r--r--devtools/client/webide/test/build_app2/stage/empty-directory0
-rw-r--r--devtools/client/webide/test/build_app_windows1/package.json5
-rw-r--r--devtools/client/webide/test/build_app_windows2/manifest.webapp1
-rw-r--r--devtools/client/webide/test/build_app_windows2/package.json10
-rw-r--r--devtools/client/webide/test/build_app_windows2/stage/empty-directory0
-rw-r--r--devtools/client/webide/test/chrome.ini71
-rw-r--r--devtools/client/webide/test/device_front_shared.js219
-rw-r--r--devtools/client/webide/test/doc_tabs.html15
-rw-r--r--devtools/client/webide/test/head.js248
-rw-r--r--devtools/client/webide/test/hosted_app.manifest3
-rw-r--r--devtools/client/webide/test/templates.json14
-rw-r--r--devtools/client/webide/test/test_addons.html176
-rw-r--r--devtools/client/webide/test/test_app_validator.html205
-rw-r--r--devtools/client/webide/test/test_autoconnect_runtime.html94
-rw-r--r--devtools/client/webide/test/test_autoselect_project.html110
-rw-r--r--devtools/client/webide/test/test_basic.html55
-rw-r--r--devtools/client/webide/test/test_build.html128
-rw-r--r--devtools/client/webide/test/test_device_permissions.html81
-rw-r--r--devtools/client/webide/test/test_device_preferences.html87
-rw-r--r--devtools/client/webide/test/test_device_runtime.html81
-rw-r--r--devtools/client/webide/test/test_device_settings.html87
-rw-r--r--devtools/client/webide/test/test_duplicate_import.html77
-rw-r--r--devtools/client/webide/test/test_fullscreenToolbox.html67
-rw-r--r--devtools/client/webide/test/test_import.html82
-rw-r--r--devtools/client/webide/test/test_manifestUpdate.html98
-rw-r--r--devtools/client/webide/test/test_newapp.html46
-rw-r--r--devtools/client/webide/test/test_runtime.html203
-rw-r--r--devtools/client/webide/test/test_simulators.html426
-rw-r--r--devtools/client/webide/test/test_telemetry.html325
-rw-r--r--devtools/client/webide/test/test_toolbox.html93
-rw-r--r--devtools/client/webide/test/test_zoom.html77
-rw-r--r--devtools/client/webide/test/validator/no-name-or-icon/home.html0
-rw-r--r--devtools/client/webide/test/validator/no-name-or-icon/manifest.webapp3
-rw-r--r--devtools/client/webide/test/validator/non-absolute-path/manifest.webapp7
-rw-r--r--devtools/client/webide/test/validator/valid/alsoValid/manifest.webapp7
-rw-r--r--devtools/client/webide/test/validator/valid/home.html0
-rw-r--r--devtools/client/webide/test/validator/valid/icon.png0
-rw-r--r--devtools/client/webide/test/validator/valid/manifest.webapp7
-rw-r--r--devtools/client/webide/test/validator/wrong-launch-path/icon.png0
-rw-r--r--devtools/client/webide/test/validator/wrong-launch-path/manifest.webapp7
-rw-r--r--devtools/client/webide/themes/addons.css79
-rw-r--r--devtools/client/webide/themes/config-view.css80
-rw-r--r--devtools/client/webide/themes/deck.css91
-rw-r--r--devtools/client/webide/themes/default-app-icon.pngbin0 -> 5208 bytes
-rw-r--r--devtools/client/webide/themes/details.css138
-rw-r--r--devtools/client/webide/themes/icons.pngbin0 -> 35353 bytes
-rw-r--r--devtools/client/webide/themes/jar.mn24
-rw-r--r--devtools/client/webide/themes/logs.css18
-rw-r--r--devtools/client/webide/themes/monitor.css86
-rw-r--r--devtools/client/webide/themes/moz.build7
-rw-r--r--devtools/client/webide/themes/newapp.css54
-rw-r--r--devtools/client/webide/themes/noise.pngbin0 -> 6216 bytes
-rw-r--r--devtools/client/webide/themes/panel-listing.css150
-rw-r--r--devtools/client/webide/themes/permissionstable.css23
-rw-r--r--devtools/client/webide/themes/rocket.svg12
-rw-r--r--devtools/client/webide/themes/runtimedetails.css25
-rw-r--r--devtools/client/webide/themes/simulator.css41
-rw-r--r--devtools/client/webide/themes/throbber.svg22
-rw-r--r--devtools/client/webide/themes/webide.css149
-rw-r--r--devtools/client/webide/themes/wifi-auth.css64
-rw-r--r--devtools/client/webide/webide-prefs.js35
-rw-r--r--devtools/client/webpack.config.js39
4269 files changed, 630469 insertions, 0 deletions
diff --git a/devtools/client/aboutdebugging/aboutdebugging.css b/devtools/client/aboutdebugging/aboutdebugging.css
new file mode 100644
index 000000000..5079c4928
--- /dev/null
+++ b/devtools/client/aboutdebugging/aboutdebugging.css
@@ -0,0 +1,199 @@
+/* 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/. */
+
+html, body {
+ height: 100%;
+ width: 100%;
+}
+
+h2, h3, h4 {
+ margin-bottom: 10px;
+}
+
+button {
+ padding-left: 20px;
+ padding-right: 20px;
+ min-width: 100px;
+ margin: 0 4px;
+}
+
+/* Category panels */
+
+.category {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+}
+
+.category-name {
+ cursor: default;
+}
+
+.app {
+ height: 100%;
+ width: 100%;
+ display: flex;
+ flex-direction: row;
+}
+
+.main-content {
+ flex: 1;
+}
+
+.panel {
+ max-width: 800px;
+}
+
+/* Targets */
+
+.targets {
+ margin-bottom: 35px;
+}
+
+.target-list {
+ margin: 0;
+ padding: 0;
+}
+
+.target-container {
+ margin-top: 5px;
+ min-height: 34px;
+ display: flex;
+ flex-direction: row;
+ align-items: start;
+}
+
+.target-icon {
+ height: 24px;
+ margin: 0 5px 0 0;
+}
+
+.target-icon:not([src]) {
+ display: none;
+}
+
+.inverted-icons .target-icon {
+ filter: invert(30%);
+}
+
+.target {
+ flex: 1;
+ margin-top: 2px;
+ /* This is silly: https://bugzilla.mozilla.org/show_bug.cgi?id=1086218#c4. */
+ min-width: 0;
+}
+
+.target-details {
+ margin: 0;
+ padding: 0;
+ list-style-type: none
+}
+
+.target-detail {
+ display: flex;
+ font-size: 12px;
+ margin-top: 7px;
+ margin-bottom: 7px;
+}
+
+.target-detail a {
+ cursor: pointer;
+ white-space: nowrap;
+}
+
+.target-detail strong {
+ white-space: nowrap;
+}
+
+.target-detail span {
+ /* Truncate items that are too long (e.g. URLs that would break the UI). */
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+}
+
+.target-detail > :not(:first-child) {
+ margin-left: 8px;
+}
+
+.target-status {
+ box-sizing: border-box;
+ display: inline-block;
+
+ min-width: 50px;
+ margin: 4px 5px 0 0;
+ padding: 2px;
+
+ border-width: 1px;
+ border-style: solid;
+
+ font-size: 0.6em;
+ text-align: center;
+}
+
+.target-status-stopped {
+ border-color: grey;
+ background-color: lightgrey;
+}
+
+.target-status-running {
+ border-color: limegreen;
+ background-color: palegreen;
+}
+
+.target-name {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.addons-controls {
+ display: flex;
+ flex-direction: row;
+}
+
+.addons-install-error {
+ background-color: #f3b0b0;
+ padding: 5px 10px;
+ margin: 5px 4px 5px 0px;
+}
+
+.service-worker-disabled .warning,
+.addons-install-error .warning {
+ background-image: url(chrome://devtools/skin/images/alerticon-warning.png);
+ background-size: 13px 12px;
+ margin-right: 10px;
+ display: inline-block;
+ width: 13px;
+ height: 12px;
+}
+
+@media (min-resolution: 1.1dppx) {
+ .service-worker-disabled .warning,
+ .addons-install-error .warning {
+ background-image: url(chrome://devtools/skin/images/alerticon-warning@2x.png);
+ }
+}
+
+.addons-options {
+ flex: 1;
+}
+
+.addons-debugging-label {
+ display: inline-block;
+ margin-inline-end: 1ch;
+}
+
+.error-page {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex-direction: column;
+ width: 100%;
+ height: 100%;
+}
+
+.error-page .error-page-details {
+ color: gray;
+}
diff --git a/devtools/client/aboutdebugging/aboutdebugging.xhtml b/devtools/client/aboutdebugging/aboutdebugging.xhtml
new file mode 100644
index 000000000..95f74b2b9
--- /dev/null
+++ b/devtools/client/aboutdebugging/aboutdebugging.xhtml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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/. -->
+
+<!DOCTYPE html [
+<!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd"> %htmlDTD;
+<!ENTITY % toolboxDTD SYSTEM "chrome://devtools/locale/toolbox.dtd"> %toolboxDTD;
+<!ENTITY % aboutdebuggingDTD SYSTEM "chrome://devtools/locale/aboutdebugging.dtd"> %aboutdebuggingDTD;
+]>
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <title>&aboutDebugging.fullTitle;</title>
+ <link rel="stylesheet" href="chrome://global/skin/in-content/common.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://devtools/content/aboutdebugging/aboutdebugging.css" type="text/css"/>
+ <script type="application/javascript" src="resource://devtools/client/shared/vendor/react.js"></script>
+ <script type="application/javascript;version=1.8" src="chrome://devtools/content/aboutdebugging/initializer.js"></script>
+ </head>
+ <body id="body">
+ </body>
+</html>
diff --git a/devtools/client/aboutdebugging/components/aboutdebugging.js b/devtools/client/aboutdebugging/components/aboutdebugging.js
new file mode 100644
index 000000000..601574dcb
--- /dev/null
+++ b/devtools/client/aboutdebugging/components/aboutdebugging.js
@@ -0,0 +1,111 @@
+/* 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/. */
+
+/* eslint-env browser */
+
+"use strict";
+
+const { createFactory, createClass, DOM: dom, PropTypes } =
+ require("devtools/client/shared/vendor/react");
+const Services = require("Services");
+
+const PanelMenu = createFactory(require("./panel-menu"));
+
+loader.lazyGetter(this, "AddonsPanel",
+ () => createFactory(require("./addons/panel")));
+loader.lazyGetter(this, "TabsPanel",
+ () => createFactory(require("./tabs/panel")));
+loader.lazyGetter(this, "WorkersPanel",
+ () => createFactory(require("./workers/panel")));
+
+loader.lazyRequireGetter(this, "DebuggerClient",
+ "devtools/shared/client/main", true);
+loader.lazyRequireGetter(this, "Telemetry",
+ "devtools/client/shared/telemetry");
+
+const Strings = Services.strings.createBundle(
+ "chrome://devtools/locale/aboutdebugging.properties");
+
+const panels = [{
+ id: "addons",
+ name: Strings.GetStringFromName("addons"),
+ icon: "chrome://devtools/skin/images/debugging-addons.svg",
+ component: AddonsPanel
+}, {
+ id: "tabs",
+ name: Strings.GetStringFromName("tabs"),
+ icon: "chrome://devtools/skin/images/debugging-tabs.svg",
+ component: TabsPanel
+}, {
+ id: "workers",
+ name: Strings.GetStringFromName("workers"),
+ icon: "chrome://devtools/skin/images/debugging-workers.svg",
+ component: WorkersPanel
+}];
+
+const defaultPanelId = "addons";
+
+module.exports = createClass({
+ displayName: "AboutDebuggingApp",
+
+ propTypes: {
+ client: PropTypes.instanceOf(DebuggerClient).isRequired,
+ telemetry: PropTypes.instanceOf(Telemetry).isRequired
+ },
+
+ getInitialState() {
+ return {
+ selectedPanelId: defaultPanelId
+ };
+ },
+
+ componentDidMount() {
+ window.addEventListener("hashchange", this.onHashChange);
+ this.onHashChange();
+ this.props.telemetry.toolOpened("aboutdebugging");
+ },
+
+ componentWillUnmount() {
+ window.removeEventListener("hashchange", this.onHashChange);
+ this.props.telemetry.toolClosed("aboutdebugging");
+ this.props.telemetry.destroy();
+ },
+
+ onHashChange() {
+ this.setState({
+ selectedPanelId: window.location.hash.substr(1) || defaultPanelId
+ });
+ },
+
+ selectPanel(panelId) {
+ window.location.hash = "#" + panelId;
+ },
+
+ render() {
+ let { client } = this.props;
+ let { selectedPanelId } = this.state;
+ let selectPanel = this.selectPanel;
+ let selectedPanel = panels.find(p => p.id == selectedPanelId);
+ let panel;
+
+ if (selectedPanel) {
+ panel = selectedPanel.component({ client, id: selectedPanel.id });
+ } else {
+ panel = (
+ dom.div({ className: "error-page" },
+ dom.h1({ className: "header-name" },
+ Strings.GetStringFromName("pageNotFound")
+ ),
+ dom.h4({ className: "error-page-details" },
+ Strings.formatStringFromName("doesNotExist", [selectedPanelId], 1))
+ )
+ );
+ }
+
+ return dom.div({ className: "app" },
+ PanelMenu({ panels, selectedPanelId, selectPanel }),
+ dom.div({ className: "main-content" }, panel)
+ );
+ }
+});
diff --git a/devtools/client/aboutdebugging/components/addons/controls.js b/devtools/client/aboutdebugging/components/addons/controls.js
new file mode 100644
index 000000000..7f985528c
--- /dev/null
+++ b/devtools/client/aboutdebugging/components/addons/controls.js
@@ -0,0 +1,97 @@
+/* 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/. */
+
+/* eslint-env browser */
+/* globals AddonManager */
+
+"use strict";
+
+loader.lazyImporter(this, "AddonManager",
+ "resource://gre/modules/AddonManager.jsm");
+
+const { Cc, Ci } = require("chrome");
+const { createFactory, createClass, DOM: dom, PropTypes } =
+ require("devtools/client/shared/vendor/react");
+const Services = require("Services");
+const AddonsInstallError = createFactory(require("./install-error"));
+
+const Strings = Services.strings.createBundle(
+ "chrome://devtools/locale/aboutdebugging.properties");
+
+const MORE_INFO_URL = "https://developer.mozilla.org/docs/Tools" +
+ "/about:debugging#Enabling_add-on_debugging";
+
+module.exports = createClass({
+ displayName: "AddonsControls",
+
+ propTypes: {
+ debugDisabled: PropTypes.bool
+ },
+
+ getInitialState() {
+ return {
+ installError: null,
+ };
+ },
+
+ onEnableAddonDebuggingChange(event) {
+ let enabled = event.target.checked;
+ Services.prefs.setBoolPref("devtools.chrome.enabled", enabled);
+ Services.prefs.setBoolPref("devtools.debugger.remote-enabled", enabled);
+ },
+
+ loadAddonFromFile() {
+ this.setState({ installError: null });
+ let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ fp.init(window,
+ Strings.GetStringFromName("selectAddonFromFile2"),
+ Ci.nsIFilePicker.modeOpen);
+ let res = fp.show();
+ if (res == Ci.nsIFilePicker.returnCancel || !fp.file) {
+ return;
+ }
+ let file = fp.file;
+ // AddonManager.installTemporaryAddon accepts either
+ // addon directory or final xpi file.
+ if (!file.isDirectory() && !file.leafName.endsWith(".xpi")) {
+ file = file.parent;
+ }
+
+ AddonManager.installTemporaryAddon(file)
+ .catch(e => {
+ console.error(e);
+ this.setState({ installError: e.message });
+ });
+ },
+
+ render() {
+ let { debugDisabled } = this.props;
+
+ return dom.div({ className: "addons-top" },
+ dom.div({ className: "addons-controls" },
+ dom.div({ className: "addons-options toggle-container-with-text" },
+ dom.input({
+ id: "enable-addon-debugging",
+ type: "checkbox",
+ checked: !debugDisabled,
+ onChange: this.onEnableAddonDebuggingChange,
+ }),
+ dom.label({
+ className: "addons-debugging-label",
+ htmlFor: "enable-addon-debugging",
+ title: Strings.GetStringFromName("addonDebugging.tooltip")
+ }, Strings.GetStringFromName("addonDebugging.label")),
+ "(",
+ dom.a({ href: MORE_INFO_URL, target: "_blank" },
+ Strings.GetStringFromName("moreInfo")),
+ ")"
+ ),
+ dom.button({
+ id: "load-addon-from-file",
+ onClick: this.loadAddonFromFile,
+ }, Strings.GetStringFromName("loadTemporaryAddon"))
+ ),
+ AddonsInstallError({ error: this.state.installError }));
+ }
+});
diff --git a/devtools/client/aboutdebugging/components/addons/install-error.js b/devtools/client/aboutdebugging/components/addons/install-error.js
new file mode 100644
index 000000000..aea1c4f09
--- /dev/null
+++ b/devtools/client/aboutdebugging/components/addons/install-error.js
@@ -0,0 +1,26 @@
+/* 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/. */
+
+/* eslint-env browser */
+"use strict";
+
+const { createClass, DOM: dom, PropTypes } = require("devtools/client/shared/vendor/react");
+
+module.exports = createClass({
+ displayName: "AddonsInstallError",
+
+ propTypes: {
+ error: PropTypes.string
+ },
+
+ render() {
+ if (!this.props.error) {
+ return null;
+ }
+ let text = `There was an error during installation: ${this.props.error}`;
+ return dom.div({ className: "addons-install-error" },
+ dom.div({ className: "warning" }),
+ dom.span({}, text));
+ }
+});
diff --git a/devtools/client/aboutdebugging/components/addons/moz.build b/devtools/client/aboutdebugging/components/addons/moz.build
new file mode 100644
index 000000000..378554f78
--- /dev/null
+++ b/devtools/client/aboutdebugging/components/addons/moz.build
@@ -0,0 +1,10 @@
+# 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/.
+
+DevToolsModules(
+ 'controls.js',
+ 'install-error.js',
+ 'panel.js',
+ 'target.js',
+)
diff --git a/devtools/client/aboutdebugging/components/addons/panel.js b/devtools/client/aboutdebugging/components/addons/panel.js
new file mode 100644
index 000000000..425a10a8d
--- /dev/null
+++ b/devtools/client/aboutdebugging/components/addons/panel.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 { AddonManager } = require("resource://gre/modules/AddonManager.jsm");
+const { createFactory, createClass, DOM: dom, PropTypes } =
+ require("devtools/client/shared/vendor/react");
+const Services = require("Services");
+
+const AddonsControls = createFactory(require("./controls"));
+const AddonTarget = createFactory(require("./target"));
+const PanelHeader = createFactory(require("../panel-header"));
+const TargetList = createFactory(require("../target-list"));
+
+loader.lazyRequireGetter(this, "DebuggerClient",
+ "devtools/shared/client/main", true);
+
+const Strings = Services.strings.createBundle(
+ "chrome://devtools/locale/aboutdebugging.properties");
+
+const ExtensionIcon = "chrome://mozapps/skin/extensions/extensionGeneric.svg";
+const CHROME_ENABLED_PREF = "devtools.chrome.enabled";
+const REMOTE_ENABLED_PREF = "devtools.debugger.remote-enabled";
+
+module.exports = createClass({
+ displayName: "AddonsPanel",
+
+ propTypes: {
+ client: PropTypes.instanceOf(DebuggerClient).isRequired,
+ id: PropTypes.string.isRequired
+ },
+
+ getInitialState() {
+ return {
+ extensions: [],
+ debugDisabled: false,
+ };
+ },
+
+ componentDidMount() {
+ AddonManager.addAddonListener(this);
+
+ Services.prefs.addObserver(CHROME_ENABLED_PREF,
+ this.updateDebugStatus, false);
+ Services.prefs.addObserver(REMOTE_ENABLED_PREF,
+ this.updateDebugStatus, false);
+
+ this.updateDebugStatus();
+ this.updateAddonsList();
+ },
+
+ componentWillUnmount() {
+ AddonManager.removeAddonListener(this);
+ Services.prefs.removeObserver(CHROME_ENABLED_PREF,
+ this.updateDebugStatus);
+ Services.prefs.removeObserver(REMOTE_ENABLED_PREF,
+ this.updateDebugStatus);
+ },
+
+ updateDebugStatus() {
+ let debugDisabled =
+ !Services.prefs.getBoolPref(CHROME_ENABLED_PREF) ||
+ !Services.prefs.getBoolPref(REMOTE_ENABLED_PREF);
+
+ this.setState({ debugDisabled });
+ },
+
+ updateAddonsList() {
+ this.props.client.listAddons()
+ .then(({addons}) => {
+ let extensions = addons.filter(addon => addon.debuggable).map(addon => {
+ return {
+ name: addon.name,
+ icon: addon.iconURL || ExtensionIcon,
+ addonID: addon.id,
+ addonActor: addon.actor,
+ temporarilyInstalled: addon.temporarilyInstalled
+ };
+ });
+
+ this.setState({ extensions });
+ }, error => {
+ throw new Error("Client error while listing addons: " + error);
+ });
+ },
+
+ /**
+ * Mandatory callback as AddonManager listener.
+ */
+ onInstalled() {
+ this.updateAddonsList();
+ },
+
+ /**
+ * Mandatory callback as AddonManager listener.
+ */
+ onUninstalled() {
+ this.updateAddonsList();
+ },
+
+ /**
+ * Mandatory callback as AddonManager listener.
+ */
+ onEnabled() {
+ this.updateAddonsList();
+ },
+
+ /**
+ * Mandatory callback as AddonManager listener.
+ */
+ onDisabled() {
+ this.updateAddonsList();
+ },
+
+ render() {
+ let { client, id } = this.props;
+ let { debugDisabled, extensions: targets } = this.state;
+ let name = Strings.GetStringFromName("extensions");
+ let targetClass = AddonTarget;
+
+ return dom.div({
+ id: id + "-panel",
+ className: "panel",
+ role: "tabpanel",
+ "aria-labelledby": id + "-header"
+ },
+ PanelHeader({
+ id: id + "-header",
+ name: Strings.GetStringFromName("addons")
+ }),
+ AddonsControls({ debugDisabled }),
+ dom.div({ id: "addons" },
+ TargetList({
+ id: "extensions",
+ name,
+ targets,
+ client,
+ debugDisabled,
+ targetClass,
+ sort: true
+ })
+ ));
+ }
+});
diff --git a/devtools/client/aboutdebugging/components/addons/target.js b/devtools/client/aboutdebugging/components/addons/target.js
new file mode 100644
index 000000000..c21499650
--- /dev/null
+++ b/devtools/client/aboutdebugging/components/addons/target.js
@@ -0,0 +1,84 @@
+/* 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/. */
+
+/* eslint-env browser */
+
+"use strict";
+
+const { createClass, DOM: dom, PropTypes } =
+ require("devtools/client/shared/vendor/react");
+const { debugAddon } = require("../../modules/addon");
+const Services = require("Services");
+
+loader.lazyImporter(this, "BrowserToolboxProcess",
+ "resource://devtools/client/framework/ToolboxProcess.jsm");
+
+loader.lazyRequireGetter(this, "DebuggerClient",
+ "devtools/shared/client/main", true);
+
+const Strings = Services.strings.createBundle(
+ "chrome://devtools/locale/aboutdebugging.properties");
+
+module.exports = createClass({
+ displayName: "AddonTarget",
+
+ propTypes: {
+ client: PropTypes.instanceOf(DebuggerClient).isRequired,
+ debugDisabled: PropTypes.bool,
+ target: PropTypes.shape({
+ addonActor: PropTypes.string.isRequired,
+ addonID: PropTypes.string.isRequired,
+ icon: PropTypes.string,
+ name: PropTypes.string.isRequired,
+ temporarilyInstalled: PropTypes.bool
+ }).isRequired
+ },
+
+ debug() {
+ let { target } = this.props;
+ debugAddon(target.addonID);
+ },
+
+ reload() {
+ let { client, target } = this.props;
+ // This function sometimes returns a partial promise that only
+ // implements then().
+ client.request({
+ to: target.addonActor,
+ type: "reload"
+ }).then(() => {}, error => {
+ throw new Error(
+ "Error reloading addon " + target.addonID + ": " + error);
+ });
+ },
+
+ render() {
+ let { target, debugDisabled } = this.props;
+ // Only temporarily installed add-ons can be reloaded.
+ const canBeReloaded = target.temporarilyInstalled;
+
+ return dom.li({ className: "target-container" },
+ dom.img({
+ className: "target-icon",
+ role: "presentation",
+ src: target.icon
+ }),
+ dom.div({ className: "target" },
+ dom.div({ className: "target-name", title: target.name }, target.name)
+ ),
+ dom.button({
+ className: "debug-button",
+ onClick: this.debug,
+ disabled: debugDisabled,
+ }, Strings.GetStringFromName("debug")),
+ dom.button({
+ className: "reload-button",
+ onClick: this.reload,
+ disabled: !canBeReloaded,
+ title: !canBeReloaded ?
+ Strings.GetStringFromName("reloadDisabledTooltip") : ""
+ }, Strings.GetStringFromName("reload"))
+ );
+ }
+});
diff --git a/devtools/client/aboutdebugging/components/moz.build b/devtools/client/aboutdebugging/components/moz.build
new file mode 100644
index 000000000..829979dcc
--- /dev/null
+++ b/devtools/client/aboutdebugging/components/moz.build
@@ -0,0 +1,17 @@
+# 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/.
+
+DIRS += [
+ 'addons',
+ 'tabs',
+ 'workers',
+]
+
+DevToolsModules(
+ 'aboutdebugging.js',
+ 'panel-header.js',
+ 'panel-menu-entry.js',
+ 'panel-menu.js',
+ 'target-list.js',
+)
diff --git a/devtools/client/aboutdebugging/components/panel-header.js b/devtools/client/aboutdebugging/components/panel-header.js
new file mode 100644
index 000000000..5629018f7
--- /dev/null
+++ b/devtools/client/aboutdebugging/components/panel-header.js
@@ -0,0 +1,24 @@
+/* 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 { createClass, DOM: dom, PropTypes } =
+ require("devtools/client/shared/vendor/react");
+
+module.exports = createClass({
+ displayName: "PanelHeader",
+
+ propTypes: {
+ id: PropTypes.string.isRequired,
+ name: PropTypes.string.isRequired
+ },
+
+ render() {
+ let { name, id } = this.props;
+
+ return dom.div({ className: "header" },
+ dom.h1({ id, className: "header-name" }, name));
+ },
+});
diff --git a/devtools/client/aboutdebugging/components/panel-menu-entry.js b/devtools/client/aboutdebugging/components/panel-menu-entry.js
new file mode 100644
index 000000000..1af02d435
--- /dev/null
+++ b/devtools/client/aboutdebugging/components/panel-menu-entry.js
@@ -0,0 +1,48 @@
+/* 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 { createClass, DOM: dom, PropTypes } =
+ require("devtools/client/shared/vendor/react");
+
+module.exports = createClass({
+ displayName: "PanelMenuEntry",
+
+ propTypes: {
+ icon: PropTypes.string.isRequired,
+ id: PropTypes.string.isRequired,
+ name: PropTypes.string.isRequired,
+ selected: PropTypes.bool,
+ selectPanel: PropTypes.func.isRequired
+ },
+
+ onClick() {
+ this.props.selectPanel(this.props.id);
+ },
+
+ onKeyDown(event) {
+ if ([" ", "Enter"].includes(event.key)) {
+ this.props.selectPanel(this.props.id);
+ }
+ },
+
+ render() {
+ let { id, name, icon, selected } = this.props;
+
+ // Here .category, .category-icon, .category-name classnames are used to
+ // apply common styles defined.
+ let className = "category" + (selected ? " selected" : "");
+ return dom.div({
+ "aria-selected": selected,
+ "aria-controls": id + "-panel",
+ className,
+ onClick: this.onClick,
+ onKeyDown: this.onKeyDown,
+ tabIndex: "0",
+ role: "tab" },
+ dom.img({ className: "category-icon", src: icon, role: "presentation" }),
+ dom.div({ className: "category-name" }, name));
+ }
+});
diff --git a/devtools/client/aboutdebugging/components/panel-menu.js b/devtools/client/aboutdebugging/components/panel-menu.js
new file mode 100644
index 000000000..b24493d78
--- /dev/null
+++ b/devtools/client/aboutdebugging/components/panel-menu.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 { createClass, createFactory, DOM: dom, PropTypes } =
+ require("devtools/client/shared/vendor/react");
+const PanelMenuEntry = createFactory(require("./panel-menu-entry"));
+
+module.exports = createClass({
+ displayName: "PanelMenu",
+
+ propTypes: {
+ panels: PropTypes.arrayOf(PropTypes.shape({
+ id: PropTypes.string.isRequired,
+ name: PropTypes.string.isRequired,
+ icon: PropTypes.string.isRequired,
+ component: PropTypes.func.isRequired
+ })).isRequired,
+ selectPanel: PropTypes.func.isRequired,
+ selectedPanelId: PropTypes.string
+ },
+
+ render() {
+ let { panels, selectedPanelId, selectPanel } = this.props;
+ let panelLinks = panels.map(({ id, name, icon }) => {
+ let selected = id == selectedPanelId;
+ return PanelMenuEntry({
+ id,
+ name,
+ icon,
+ selected,
+ selectPanel
+ });
+ });
+
+ // "categories" id used for styling purposes
+ return dom.div({ id: "categories", role: "tablist" }, panelLinks);
+ },
+});
diff --git a/devtools/client/aboutdebugging/components/tabs/moz.build b/devtools/client/aboutdebugging/components/tabs/moz.build
new file mode 100644
index 000000000..ee6a89e37
--- /dev/null
+++ b/devtools/client/aboutdebugging/components/tabs/moz.build
@@ -0,0 +1,8 @@
+# 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/.
+
+DevToolsModules(
+ 'panel.js',
+ 'target.js',
+)
diff --git a/devtools/client/aboutdebugging/components/tabs/panel.js b/devtools/client/aboutdebugging/components/tabs/panel.js
new file mode 100644
index 000000000..e280ce7f1
--- /dev/null
+++ b/devtools/client/aboutdebugging/components/tabs/panel.js
@@ -0,0 +1,98 @@
+/* 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/. */
+
+/* eslint-env browser */
+
+"use strict";
+
+const { createClass, createFactory, DOM: dom, PropTypes } =
+ require("devtools/client/shared/vendor/react");
+const Services = require("Services");
+
+const PanelHeader = createFactory(require("../panel-header"));
+const TargetList = createFactory(require("../target-list"));
+const TabTarget = createFactory(require("./target"));
+
+loader.lazyRequireGetter(this, "DebuggerClient",
+ "devtools/shared/client/main", true);
+
+const Strings = Services.strings.createBundle(
+ "chrome://devtools/locale/aboutdebugging.properties");
+
+module.exports = createClass({
+ displayName: "TabsPanel",
+
+ propTypes: {
+ client: PropTypes.instanceOf(DebuggerClient).isRequired,
+ id: PropTypes.string.isRequired
+ },
+
+ getInitialState() {
+ return {
+ tabs: []
+ };
+ },
+
+ componentDidMount() {
+ let { client } = this.props;
+ client.addListener("tabListChanged", this.update);
+ this.update();
+ },
+
+ componentWillUnmount() {
+ let { client } = this.props;
+ client.removeListener("tabListChanged", this.update);
+ },
+
+ update() {
+ this.props.client.mainRoot.listTabs().then(({ tabs }) => {
+ // Filter out closed tabs (represented as `null`).
+ tabs = tabs.filter(tab => !!tab);
+ tabs.forEach(tab => {
+ // FIXME Also try to fetch low-res favicon. But we should use actor
+ // support for this to get the high-res one (bug 1061654).
+ let url = new URL(tab.url);
+ if (url.protocol.startsWith("http")) {
+ let prePath = url.origin;
+ let idx = url.pathname.lastIndexOf("/");
+ if (idx === -1) {
+ prePath += url.pathname;
+ } else {
+ prePath += url.pathname.substr(0, idx);
+ }
+ tab.icon = prePath + "/favicon.ico";
+ } else {
+ tab.icon = "chrome://devtools/skin/images/globe.svg";
+ }
+ });
+ this.setState({ tabs });
+ });
+ },
+
+ render() {
+ let { client, id } = this.props;
+ let { tabs } = this.state;
+
+ return dom.div({
+ id: id + "-panel",
+ className: "panel",
+ role: "tabpanel",
+ "aria-labelledby": id + "-header"
+ },
+ PanelHeader({
+ id: id + "-header",
+ name: Strings.GetStringFromName("tabs")
+ }),
+ dom.div({},
+ TargetList({
+ client,
+ id: "tabs",
+ name: Strings.GetStringFromName("tabs"),
+ sort: false,
+ targetClass: TabTarget,
+ targets: tabs
+ })
+ ));
+ }
+});
diff --git a/devtools/client/aboutdebugging/components/tabs/target.js b/devtools/client/aboutdebugging/components/tabs/target.js
new file mode 100644
index 000000000..d946f8f61
--- /dev/null
+++ b/devtools/client/aboutdebugging/components/tabs/target.js
@@ -0,0 +1,53 @@
+/* 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/. */
+
+/* eslint-env browser */
+
+"use strict";
+
+const { createClass, DOM: dom, PropTypes } =
+ require("devtools/client/shared/vendor/react");
+const Services = require("Services");
+
+const Strings = Services.strings.createBundle(
+ "chrome://devtools/locale/aboutdebugging.properties");
+
+module.exports = createClass({
+ displayName: "TabTarget",
+
+ propTypes: {
+ target: PropTypes.shape({
+ icon: PropTypes.string,
+ outerWindowID: PropTypes.number.isRequired,
+ title: PropTypes.string,
+ url: PropTypes.string.isRequired
+ }).isRequired
+ },
+
+ debug() {
+ let { target } = this.props;
+ window.open("about:devtools-toolbox?type=tab&id=" + target.outerWindowID);
+ },
+
+ render() {
+ let { target } = this.props;
+
+ return dom.div({ className: "target-container" },
+ dom.img({
+ className: "target-icon",
+ role: "presentation",
+ src: target.icon
+ }),
+ dom.div({ className: "target" },
+ // If the title is empty, display the url instead.
+ dom.div({ className: "target-name", title: target.url },
+ target.title || target.url)
+ ),
+ dom.button({
+ className: "debug-button",
+ onClick: this.debug,
+ }, Strings.GetStringFromName("debug"))
+ );
+ }
+});
diff --git a/devtools/client/aboutdebugging/components/target-list.js b/devtools/client/aboutdebugging/components/target-list.js
new file mode 100644
index 000000000..e2d5669e7
--- /dev/null
+++ b/devtools/client/aboutdebugging/components/target-list.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/. */
+
+"use strict";
+
+const { createClass, DOM: dom, PropTypes } =
+ require("devtools/client/shared/vendor/react");
+const Services = require("Services");
+
+loader.lazyRequireGetter(this, "DebuggerClient",
+ "devtools/shared/client/main", true);
+
+const Strings = Services.strings.createBundle(
+ "chrome://devtools/locale/aboutdebugging.properties");
+
+const LocaleCompare = (a, b) => {
+ return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
+};
+
+module.exports = createClass({
+ displayName: "TargetList",
+
+ propTypes: {
+ client: PropTypes.instanceOf(DebuggerClient).isRequired,
+ debugDisabled: PropTypes.bool,
+ error: PropTypes.node,
+ id: PropTypes.string.isRequired,
+ name: PropTypes.string,
+ sort: PropTypes.bool,
+ targetClass: PropTypes.func.isRequired,
+ targets: PropTypes.arrayOf(PropTypes.object).isRequired
+ },
+
+ render() {
+ let { client, debugDisabled, error, targetClass, targets, sort } = this.props;
+ if (sort) {
+ targets = targets.sort(LocaleCompare);
+ }
+ targets = targets.map(target => {
+ return targetClass({ client, target, debugDisabled });
+ });
+
+ let content = "";
+ if (error) {
+ content = error;
+ } else if (targets.length > 0) {
+ content = dom.ul({ className: "target-list" }, targets);
+ } else {
+ content = dom.p(null, Strings.GetStringFromName("nothing"));
+ }
+
+ return dom.div({ id: this.props.id, className: "targets" },
+ dom.h2(null, this.props.name), content);
+ },
+});
diff --git a/devtools/client/aboutdebugging/components/workers/moz.build b/devtools/client/aboutdebugging/components/workers/moz.build
new file mode 100644
index 000000000..ff33a5b28
--- /dev/null
+++ b/devtools/client/aboutdebugging/components/workers/moz.build
@@ -0,0 +1,9 @@
+# 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/.
+
+DevToolsModules(
+ 'panel.js',
+ 'service-worker-target.js',
+ 'target.js',
+)
diff --git a/devtools/client/aboutdebugging/components/workers/panel.js b/devtools/client/aboutdebugging/components/workers/panel.js
new file mode 100644
index 000000000..b1bab2b99
--- /dev/null
+++ b/devtools/client/aboutdebugging/components/workers/panel.js
@@ -0,0 +1,193 @@
+/* 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/. */
+/* globals window */
+"use strict";
+
+loader.lazyImporter(this, "PrivateBrowsingUtils",
+ "resource://gre/modules/PrivateBrowsingUtils.jsm");
+
+const { Ci } = require("chrome");
+const { createClass, createFactory, DOM: dom, PropTypes } =
+ require("devtools/client/shared/vendor/react");
+const { getWorkerForms } = require("../../modules/worker");
+const Services = require("Services");
+
+const PanelHeader = createFactory(require("../panel-header"));
+const TargetList = createFactory(require("../target-list"));
+const WorkerTarget = createFactory(require("./target"));
+const ServiceWorkerTarget = createFactory(require("./service-worker-target"));
+
+loader.lazyImporter(this, "PrivateBrowsingUtils",
+ "resource://gre/modules/PrivateBrowsingUtils.jsm");
+
+loader.lazyRequireGetter(this, "DebuggerClient",
+ "devtools/shared/client/main", true);
+
+const Strings = Services.strings.createBundle(
+ "chrome://devtools/locale/aboutdebugging.properties");
+
+const WorkerIcon = "chrome://devtools/skin/images/debugging-workers.svg";
+const MORE_INFO_URL = "https://developer.mozilla.org/en-US/docs/Tools/about%3Adebugging";
+
+module.exports = createClass({
+ displayName: "WorkersPanel",
+
+ propTypes: {
+ client: PropTypes.instanceOf(DebuggerClient).isRequired,
+ id: PropTypes.string.isRequired
+ },
+
+ getInitialState() {
+ return {
+ workers: {
+ service: [],
+ shared: [],
+ other: []
+ }
+ };
+ },
+
+ componentDidMount() {
+ let client = this.props.client;
+ client.addListener("workerListChanged", this.update);
+ client.addListener("serviceWorkerRegistrationListChanged", this.update);
+ client.addListener("processListChanged", this.update);
+ client.addListener("registration-changed", this.update);
+
+ this.update();
+ },
+
+ componentWillUnmount() {
+ let client = this.props.client;
+ client.removeListener("processListChanged", this.update);
+ client.removeListener("serviceWorkerRegistrationListChanged", this.update);
+ client.removeListener("workerListChanged", this.update);
+ client.removeListener("registration-changed", this.update);
+ },
+
+ update() {
+ let workers = this.getInitialState().workers;
+
+ getWorkerForms(this.props.client).then(forms => {
+ forms.registrations.forEach(form => {
+ workers.service.push({
+ icon: WorkerIcon,
+ name: form.url,
+ url: form.url,
+ scope: form.scope,
+ registrationActor: form.actor,
+ active: form.active
+ });
+ });
+
+ forms.workers.forEach(form => {
+ let worker = {
+ icon: WorkerIcon,
+ name: form.url,
+ url: form.url,
+ workerActor: form.actor
+ };
+ switch (form.type) {
+ case Ci.nsIWorkerDebugger.TYPE_SERVICE:
+ let registration = this.getRegistrationForWorker(form, workers.service);
+ if (registration) {
+ // XXX: Race, sometimes a ServiceWorkerRegistrationInfo doesn't
+ // have a scriptSpec, but its associated WorkerDebugger does.
+ if (!registration.url) {
+ registration.name = registration.url = form.url;
+ }
+ registration.workerActor = form.actor;
+ } else {
+ // If a service worker registration could not be found, this means we are in
+ // e10s, and registrations are not forwarded to other processes until they
+ // reach the activated state. Augment the worker as a registration worker to
+ // display it in aboutdebugging.
+ worker.scope = form.scope;
+ worker.active = false;
+ workers.service.push(worker);
+ }
+ break;
+ case Ci.nsIWorkerDebugger.TYPE_SHARED:
+ workers.shared.push(worker);
+ break;
+ default:
+ workers.other.push(worker);
+ }
+ });
+
+ // XXX: Filter out the service worker registrations for which we couldn't
+ // find the scriptSpec.
+ workers.service = workers.service.filter(reg => !!reg.url);
+
+ this.setState({ workers });
+ });
+ },
+
+ getRegistrationForWorker(form, registrations) {
+ for (let registration of registrations) {
+ if (registration.scope === form.scope) {
+ return registration;
+ }
+ }
+ return null;
+ },
+
+ render() {
+ let { client, id } = this.props;
+ let { workers } = this.state;
+
+ let isWindowPrivate = PrivateBrowsingUtils.isContentWindowPrivate(window);
+ let isPrivateBrowsingMode = PrivateBrowsingUtils.permanentPrivateBrowsing;
+ let isServiceWorkerDisabled = !Services.prefs
+ .getBoolPref("dom.serviceWorkers.enabled");
+ let errorMsg = isWindowPrivate || isPrivateBrowsingMode ||
+ isServiceWorkerDisabled ?
+ dom.p({ className: "service-worker-disabled" },
+ dom.div({ className: "warning" }),
+ Strings.GetStringFromName("configurationIsNotCompatible"),
+ " (",
+ dom.a({ href: MORE_INFO_URL, target: "_blank" },
+ Strings.GetStringFromName("moreInfo")),
+ ")"
+ ) : "";
+
+ return dom.div({
+ id: id + "-panel",
+ className: "panel",
+ role: "tabpanel",
+ "aria-labelledby": id + "-header"
+ },
+ PanelHeader({
+ id: id + "-header",
+ name: Strings.GetStringFromName("workers")
+ }),
+ dom.div({ id: "workers", className: "inverted-icons" },
+ TargetList({
+ client,
+ error: errorMsg,
+ id: "service-workers",
+ name: Strings.GetStringFromName("serviceWorkers"),
+ sort: true,
+ targetClass: ServiceWorkerTarget,
+ targets: workers.service
+ }),
+ TargetList({
+ client,
+ id: "shared-workers",
+ name: Strings.GetStringFromName("sharedWorkers"),
+ sort: true,
+ targetClass: WorkerTarget,
+ targets: workers.shared
+ }),
+ TargetList({
+ client,
+ id: "other-workers",
+ name: Strings.GetStringFromName("otherWorkers"),
+ sort: true,
+ targetClass: WorkerTarget,
+ targets: workers.other
+ })
+ ));
+ }
+});
diff --git a/devtools/client/aboutdebugging/components/workers/service-worker-target.js b/devtools/client/aboutdebugging/components/workers/service-worker-target.js
new file mode 100644
index 000000000..d46f6f20f
--- /dev/null
+++ b/devtools/client/aboutdebugging/components/workers/service-worker-target.js
@@ -0,0 +1,231 @@
+/* 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/. */
+
+/* eslint-env browser */
+
+"use strict";
+
+const { createClass, DOM: dom, PropTypes } =
+ require("devtools/client/shared/vendor/react");
+const { debugWorker } = require("../../modules/worker");
+const Services = require("Services");
+
+loader.lazyRequireGetter(this, "DebuggerClient",
+ "devtools/shared/client/main", true);
+
+const Strings = Services.strings.createBundle(
+ "chrome://devtools/locale/aboutdebugging.properties");
+
+module.exports = createClass({
+ displayName: "ServiceWorkerTarget",
+
+ propTypes: {
+ client: PropTypes.instanceOf(DebuggerClient).isRequired,
+ debugDisabled: PropTypes.bool,
+ target: PropTypes.shape({
+ active: PropTypes.bool,
+ icon: PropTypes.string,
+ name: PropTypes.string.isRequired,
+ url: PropTypes.string,
+ scope: PropTypes.string.isRequired,
+ // registrationActor can be missing in e10s.
+ registrationActor: PropTypes.string,
+ workerActor: PropTypes.string
+ }).isRequired
+ },
+
+ getInitialState() {
+ return {
+ pushSubscription: null
+ };
+ },
+
+ componentDidMount() {
+ let { client } = this.props;
+ client.addListener("push-subscription-modified", this.onPushSubscriptionModified);
+ this.updatePushSubscription();
+ },
+
+ componentDidUpdate(oldProps, oldState) {
+ let wasActive = oldProps.target.active;
+ if (!wasActive && this.isActive()) {
+ // While the service worker isn't active, any calls to `updatePushSubscription`
+ // won't succeed. If we just became active, make sure we didn't miss a push
+ // subscription change by updating it now.
+ this.updatePushSubscription();
+ }
+ },
+
+ componentWillUnmount() {
+ let { client } = this.props;
+ client.removeListener("push-subscription-modified", this.onPushSubscriptionModified);
+ },
+
+ debug() {
+ if (!this.isRunning()) {
+ // If the worker is not running, we can't debug it.
+ return;
+ }
+
+ let { client, target } = this.props;
+ debugWorker(client, target.workerActor);
+ },
+
+ push() {
+ if (!this.isActive() || !this.isRunning()) {
+ // If the worker is not running, we can't push to it.
+ // If the worker is not active, the registration might be unavailable and the
+ // push will not succeed.
+ return;
+ }
+
+ let { client, target } = this.props;
+ client.request({
+ to: target.workerActor,
+ type: "push"
+ });
+ },
+
+ start() {
+ if (!this.isActive() || this.isRunning()) {
+ // If the worker is not active or if it is already running, we can't start it.
+ return;
+ }
+
+ let { client, target } = this.props;
+ client.request({
+ to: target.registrationActor,
+ type: "start"
+ });
+ },
+
+ unregister() {
+ let { client, target } = this.props;
+ client.request({
+ to: target.registrationActor,
+ type: "unregister"
+ });
+ },
+
+ onPushSubscriptionModified(type, data) {
+ let { target } = this.props;
+ if (data.from === target.registrationActor) {
+ this.updatePushSubscription();
+ }
+ },
+
+ updatePushSubscription() {
+ if (!this.props.target.registrationActor) {
+ // A valid registrationActor is needed to retrieve the push subscription.
+ return;
+ }
+
+ let { client, target } = this.props;
+ client.request({
+ to: target.registrationActor,
+ type: "getPushSubscription"
+ }, ({ subscription }) => {
+ this.setState({ pushSubscription: subscription });
+ });
+ },
+
+ isRunning() {
+ // We know the target is running if it has a worker actor.
+ return !!this.props.target.workerActor;
+ },
+
+ isActive() {
+ return this.props.target.active;
+ },
+
+ getServiceWorkerStatus() {
+ if (this.isActive() && this.isRunning()) {
+ return "running";
+ } else if (this.isActive()) {
+ return "stopped";
+ }
+ // We cannot get service worker registrations unless the registration is in
+ // ACTIVE state. Unable to know the actual state ("installing", "waiting"), we
+ // display a custom state "registering" for now. See Bug 1153292.
+ return "registering";
+ },
+
+ renderButtons() {
+ let pushButton = dom.button({
+ className: "push-button",
+ onClick: this.push
+ }, Strings.GetStringFromName("push"));
+
+ let debugButton = dom.button({
+ className: "debug-button",
+ onClick: this.debug,
+ disabled: this.props.debugDisabled
+ }, Strings.GetStringFromName("debug"));
+
+ let startButton = dom.button({
+ className: "start-button",
+ onClick: this.start,
+ }, Strings.GetStringFromName("start"));
+
+ if (this.isRunning()) {
+ if (this.isActive()) {
+ return [pushButton, debugButton];
+ }
+ // Only debug button is available if the service worker is not active.
+ return debugButton;
+ }
+ return startButton;
+ },
+
+ renderUnregisterLink() {
+ if (!this.isActive()) {
+ // If not active, there might be no registrationActor available.
+ return null;
+ }
+
+ return dom.a({
+ onClick: this.unregister,
+ className: "unregister-link"
+ }, Strings.GetStringFromName("unregister"));
+ },
+
+ render() {
+ let { target } = this.props;
+ let { pushSubscription } = this.state;
+ let status = this.getServiceWorkerStatus();
+
+ return dom.div({ className: "target-container" },
+ dom.img({
+ className: "target-icon",
+ role: "presentation",
+ src: target.icon
+ }),
+ dom.span({ className: `target-status target-status-${status}` },
+ Strings.GetStringFromName(status)),
+ dom.div({ className: "target" },
+ dom.div({ className: "target-name", title: target.name }, target.name),
+ dom.ul({ className: "target-details" },
+ (pushSubscription ?
+ dom.li({ className: "target-detail" },
+ dom.strong(null, Strings.GetStringFromName("pushService")),
+ dom.span({
+ className: "service-worker-push-url",
+ title: pushSubscription.endpoint
+ }, pushSubscription.endpoint)) :
+ null
+ ),
+ dom.li({ className: "target-detail" },
+ dom.strong(null, Strings.GetStringFromName("scope")),
+ dom.span({
+ className: "service-worker-scope",
+ title: target.scope
+ }, target.scope),
+ this.renderUnregisterLink()
+ )
+ )
+ ),
+ this.renderButtons()
+ );
+ }
+});
diff --git a/devtools/client/aboutdebugging/components/workers/target.js b/devtools/client/aboutdebugging/components/workers/target.js
new file mode 100644
index 000000000..c1f6420ac
--- /dev/null
+++ b/devtools/client/aboutdebugging/components/workers/target.js
@@ -0,0 +1,57 @@
+/* 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/. */
+
+/* eslint-env browser */
+
+"use strict";
+
+const { createClass, DOM: dom, PropTypes } =
+ require("devtools/client/shared/vendor/react");
+const { debugWorker } = require("../../modules/worker");
+const Services = require("Services");
+
+loader.lazyRequireGetter(this, "DebuggerClient",
+ "devtools/shared/client/main", true);
+
+const Strings = Services.strings.createBundle(
+ "chrome://devtools/locale/aboutdebugging.properties");
+
+module.exports = createClass({
+ displayName: "WorkerTarget",
+
+ propTypes: {
+ client: PropTypes.instanceOf(DebuggerClient).isRequired,
+ debugDisabled: PropTypes.bool,
+ target: PropTypes.shape({
+ icon: PropTypes.string,
+ name: PropTypes.string.isRequired,
+ workerActor: PropTypes.string
+ }).isRequired
+ },
+
+ debug() {
+ let { client, target } = this.props;
+ debugWorker(client, target.workerActor);
+ },
+
+ render() {
+ let { target, debugDisabled } = this.props;
+
+ return dom.li({ className: "target-container" },
+ dom.img({
+ className: "target-icon",
+ role: "presentation",
+ src: target.icon
+ }),
+ dom.div({ className: "target" },
+ dom.div({ className: "target-name", title: target.name }, target.name)
+ ),
+ dom.button({
+ className: "debug-button",
+ onClick: this.debug,
+ disabled: debugDisabled
+ }, Strings.GetStringFromName("debug"))
+ );
+ }
+});
diff --git a/devtools/client/aboutdebugging/initializer.js b/devtools/client/aboutdebugging/initializer.js
new file mode 100644
index 000000000..f1b91f14d
--- /dev/null
+++ b/devtools/client/aboutdebugging/initializer.js
@@ -0,0 +1,67 @@
+/* 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/. */
+
+/* eslint-env browser */
+/* globals DebuggerClient, DebuggerServer, Telemetry */
+
+"use strict";
+
+const { loader } = Components.utils.import(
+ "resource://devtools/shared/Loader.jsm", {});
+const { BrowserLoader } = Components.utils.import(
+ "resource://devtools/client/shared/browser-loader.js", {});
+
+loader.lazyRequireGetter(this, "DebuggerClient",
+ "devtools/shared/client/main", true);
+loader.lazyRequireGetter(this, "DebuggerServer",
+ "devtools/server/main", true);
+loader.lazyRequireGetter(this, "Telemetry",
+ "devtools/client/shared/telemetry");
+
+const { require } = BrowserLoader({
+ baseURI: "resource://devtools/client/aboutdebugging/",
+ window
+});
+
+const { createFactory } = require("devtools/client/shared/vendor/react");
+const { render, unmountComponentAtNode } = require("devtools/client/shared/vendor/react-dom");
+
+const AboutDebuggingApp = createFactory(require("./components/aboutdebugging"));
+
+var AboutDebugging = {
+ init() {
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+ DebuggerServer.allowChromeProcess = true;
+
+ this.client = new DebuggerClient(DebuggerServer.connectPipe());
+
+ this.client.connect().then(() => {
+ let client = this.client;
+ let telemetry = new Telemetry();
+
+ render(AboutDebuggingApp({ client, telemetry }),
+ document.querySelector("#body"));
+ });
+ },
+
+ destroy() {
+ unmountComponentAtNode(document.querySelector("#body"));
+
+ this.client.close();
+ this.client = null;
+ },
+};
+
+window.addEventListener("DOMContentLoaded", function load() {
+ window.removeEventListener("DOMContentLoaded", load);
+ AboutDebugging.init();
+});
+
+window.addEventListener("unload", function unload() {
+ window.removeEventListener("unload", unload);
+ AboutDebugging.destroy();
+});
diff --git a/devtools/client/aboutdebugging/modules/addon.js b/devtools/client/aboutdebugging/modules/addon.js
new file mode 100644
index 000000000..d800462b9
--- /dev/null
+++ b/devtools/client/aboutdebugging/modules/addon.js
@@ -0,0 +1,23 @@
+/* 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";
+
+loader.lazyImporter(this, "BrowserToolboxProcess",
+ "resource://devtools/client/framework/ToolboxProcess.jsm");
+
+let toolbox = null;
+
+exports.debugAddon = function (addonID) {
+ if (toolbox) {
+ toolbox.close();
+ }
+
+ toolbox = BrowserToolboxProcess.init({
+ addonID,
+ onClose: () => {
+ toolbox = null;
+ }
+ });
+};
diff --git a/devtools/client/aboutdebugging/modules/moz.build b/devtools/client/aboutdebugging/modules/moz.build
new file mode 100644
index 000000000..de840f957
--- /dev/null
+++ b/devtools/client/aboutdebugging/modules/moz.build
@@ -0,0 +1,8 @@
+# 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/.
+
+DevToolsModules(
+ 'addon.js',
+ 'worker.js',
+)
diff --git a/devtools/client/aboutdebugging/modules/worker.js b/devtools/client/aboutdebugging/modules/worker.js
new file mode 100644
index 000000000..1623088c6
--- /dev/null
+++ b/devtools/client/aboutdebugging/modules/worker.js
@@ -0,0 +1,77 @@
+/* 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 { Task } = require("devtools/shared/task");
+
+loader.lazyRequireGetter(this, "gDevTools",
+ "devtools/client/framework/devtools", true);
+loader.lazyRequireGetter(this, "TargetFactory",
+ "devtools/client/framework/target", true);
+loader.lazyRequireGetter(this, "Toolbox",
+ "devtools/client/framework/toolbox", true);
+
+/**
+ * Open a window-hosted toolbox to debug the worker associated to the provided
+ * worker actor.
+ *
+ * @param {DebuggerClient} client
+ * @param {Object} workerActor
+ * worker actor form to debug
+ */
+exports.debugWorker = function (client, workerActor) {
+ client.attachWorker(workerActor, (response, workerClient) => {
+ let workerTarget = TargetFactory.forWorker(workerClient);
+ gDevTools.showToolbox(workerTarget, "jsdebugger", Toolbox.HostType.WINDOW)
+ .then(toolbox => {
+ toolbox.once("destroy", () => workerClient.detach());
+ });
+ });
+};
+
+/**
+ * Retrieve all service worker registrations as well as workers from the parent
+ * and child processes.
+ *
+ * @param {DebuggerClient} client
+ * @return {Object}
+ * - {Array} registrations
+ * Array of ServiceWorkerRegistrationActor forms
+ * - {Array} workers
+ * Array of WorkerActor forms
+ */
+exports.getWorkerForms = Task.async(function* (client) {
+ let registrations = [];
+ let workers = [];
+
+ try {
+ // List service worker registrations
+ ({ registrations } =
+ yield client.mainRoot.listServiceWorkerRegistrations());
+
+ // List workers from the Parent process
+ ({ workers } = yield client.mainRoot.listWorkers());
+
+ // And then from the Child processes
+ let { processes } = yield client.mainRoot.listProcesses();
+ for (let process of processes) {
+ // Ignore parent process
+ if (process.parent) {
+ continue;
+ }
+ let { form } = yield client.getProcess(process.id);
+ let processActor = form.actor;
+ let response = yield client.request({
+ to: processActor,
+ type: "listWorkers"
+ });
+ workers = workers.concat(response.workers);
+ }
+ } catch (e) {
+ // Something went wrong, maybe our client is disconnected?
+ }
+
+ return { registrations, workers };
+});
diff --git a/devtools/client/aboutdebugging/moz.build b/devtools/client/aboutdebugging/moz.build
new file mode 100644
index 000000000..fd180ba46
--- /dev/null
+++ b/devtools/client/aboutdebugging/moz.build
@@ -0,0 +1,14 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DIRS += [
+ 'components',
+ 'modules',
+]
+
+BROWSER_CHROME_MANIFESTS += [
+ 'test/browser.ini'
+]
diff --git a/devtools/client/aboutdebugging/test/.eslintrc.js b/devtools/client/aboutdebugging/test/.eslintrc.js
new file mode 100644
index 000000000..8c4bee0ef
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/.eslintrc.js
@@ -0,0 +1,26 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../../.eslintrc.mochitests.js",
+ // All globals made available in aboutdebugging head.js file.
+ "globals": {
+ "AddonManager": true,
+ "addTab": true,
+ "assertHasTarget": true,
+ "CHROME_ROOT": true,
+ "changeAboutDebuggingHash": true,
+ "closeAboutDebugging": true,
+ "getServiceWorkerList": true,
+ "getSupportsFile": true,
+ "installAddon": true,
+ "openAboutDebugging": true,
+ "openPanel": true,
+ "removeTab": true,
+ "uninstallAddon": true,
+ "unregisterServiceWorker": true,
+ "waitForInitialAddonList": true,
+ "waitForMutation": true,
+ "waitForServiceWorkerRegistered": true
+ }
+};
diff --git a/devtools/client/aboutdebugging/test/addons/bad/manifest.json b/devtools/client/aboutdebugging/test/addons/bad/manifest.json
new file mode 100644
index 000000000..4ab10b4de
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/addons/bad/manifest.json
@@ -0,0 +1 @@
+this is not valid json
diff --git a/devtools/client/aboutdebugging/test/addons/bug1273184.xpi b/devtools/client/aboutdebugging/test/addons/bug1273184.xpi
new file mode 100644
index 000000000..e1c42376e
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/addons/bug1273184.xpi
Binary files differ
diff --git a/devtools/client/aboutdebugging/test/addons/test-devtools-webextension-nobg/manifest.json b/devtools/client/aboutdebugging/test/addons/test-devtools-webextension-nobg/manifest.json
new file mode 100644
index 000000000..289d8b918
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/addons/test-devtools-webextension-nobg/manifest.json
@@ -0,0 +1,10 @@
+{
+ "manifest_version": 2,
+ "name": "test-devtools-webextension-nobg",
+ "version": "1.0",
+ "applications": {
+ "gecko": {
+ "id": "test-devtools-webextension-nobg@mozilla.org"
+ }
+ }
+}
diff --git a/devtools/client/aboutdebugging/test/addons/test-devtools-webextension/bg.js b/devtools/client/aboutdebugging/test/addons/test-devtools-webextension/bg.js
new file mode 100644
index 000000000..7ab93c46a
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/addons/test-devtools-webextension/bg.js
@@ -0,0 +1,20 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* eslint-env browser */
+/* global browser */
+
+"use strict";
+
+document.body.innerText = "Background Page Body Test Content";
+
+// This function are called from the webconsole test:
+// browser_addons_debug_webextension.js
+
+function myWebExtensionAddonFunction() { // eslint-disable-line no-unused-vars
+ console.log("Background page function called", browser.runtime.getManifest());
+}
+
+function myWebExtensionShowPopup() { // eslint-disable-line no-unused-vars
+ console.log("readyForOpenPopup");
+}
diff --git a/devtools/client/aboutdebugging/test/addons/test-devtools-webextension/manifest.json b/devtools/client/aboutdebugging/test/addons/test-devtools-webextension/manifest.json
new file mode 100644
index 000000000..f224e5dcf
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/addons/test-devtools-webextension/manifest.json
@@ -0,0 +1,17 @@
+{
+ "manifest_version": 2,
+ "name": "test-devtools-webextension",
+ "version": "1.0",
+ "applications": {
+ "gecko": {
+ "id": "test-devtools-webextension@mozilla.org"
+ }
+ },
+ "background": {
+ "scripts": ["bg.js"]
+ },
+ "browser_action": {
+ "default_title": "WebExtension Popup Debugging",
+ "default_popup": "popup.html"
+ }
+}
diff --git a/devtools/client/aboutdebugging/test/addons/test-devtools-webextension/popup.html b/devtools/client/aboutdebugging/test/addons/test-devtools-webextension/popup.html
new file mode 100644
index 000000000..4e3f7aba2
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/addons/test-devtools-webextension/popup.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <script src="popup.js"></script>
+ </head>
+ <body>
+ Background Page Body Test Content
+ </body>
+</html>
diff --git a/devtools/client/aboutdebugging/test/addons/test-devtools-webextension/popup.js b/devtools/client/aboutdebugging/test/addons/test-devtools-webextension/popup.js
new file mode 100644
index 000000000..035375682
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/addons/test-devtools-webextension/popup.js
@@ -0,0 +1,13 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* eslint-env browser */
+/* global browser */
+
+"use strict";
+
+// This function is called from the webconsole test:
+// browser_addons_debug_webextension.js
+function myWebExtensionPopupAddonFunction() { // eslint-disable-line no-unused-vars
+ console.log("Popup page function called", browser.runtime.getManifest());
+}
diff --git a/devtools/client/aboutdebugging/test/addons/unpacked/bootstrap.js b/devtools/client/aboutdebugging/test/addons/unpacked/bootstrap.js
new file mode 100644
index 000000000..d96e31e5e
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/addons/unpacked/bootstrap.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* eslint-env browser */
+/* exported startup, shutdown, install, uninstall */
+
+"use strict";
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+// This function is called from the webconsole test:
+// browser_addons_debug_bootstrapped.js
+function myBootstrapAddonFunction() { // eslint-disable-line no-unused-vars
+ Services.obs.notifyObservers(null, "addon-console-works", null);
+}
+
+function startup() {
+ Services.obs.notifyObservers(null, "test-devtools", null);
+}
+function shutdown() {}
+function install() {}
+function uninstall() {}
diff --git a/devtools/client/aboutdebugging/test/addons/unpacked/install.rdf b/devtools/client/aboutdebugging/test/addons/unpacked/install.rdf
new file mode 100644
index 000000000..91c7474cc
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/addons/unpacked/install.rdf
@@ -0,0 +1,26 @@
+<?xml version="1.0"?>
+<!--
+# 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/.
+-->
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+ <Description about="urn:mozilla:install-manifest"
+ em:id="test-devtools@mozilla.org"
+ em:name="test-devtools"
+ em:version="1.0"
+ em:type="2"
+ em:creator="Mozilla">
+
+ <em:bootstrap>true</em:bootstrap>
+ <em:targetApplication>
+ <Description>
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>44.0a1</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+</RDF>
diff --git a/devtools/client/aboutdebugging/test/browser.ini b/devtools/client/aboutdebugging/test/browser.ini
new file mode 100644
index 000000000..90ed59d21
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser.ini
@@ -0,0 +1,44 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ head.js
+ addons/unpacked/bootstrap.js
+ addons/unpacked/install.rdf
+ addons/bad/manifest.json
+ addons/bug1273184.xpi
+ addons/test-devtools-webextension/*
+ addons/test-devtools-webextension-nobg/*
+ service-workers/delay-sw.html
+ service-workers/delay-sw.js
+ service-workers/empty-sw.html
+ service-workers/empty-sw.js
+ service-workers/push-sw.html
+ service-workers/push-sw.js
+ !/devtools/client/framework/test/shared-head.js
+
+[browser_addons_debug_bootstrapped.js]
+[browser_addons_debug_webextension.js]
+tags = webextensions
+[browser_addons_debug_webextension_inspector.js]
+tags = webextensions
+[browser_addons_debug_webextension_nobg.js]
+tags = webextensions
+[browser_addons_debug_webextension_popup.js]
+tags = webextensions
+[browser_addons_debugging_initial_state.js]
+[browser_addons_install.js]
+[browser_addons_reload.js]
+[browser_addons_toggle_debug.js]
+[browser_page_not_found.js]
+[browser_service_workers.js]
+[browser_service_workers_not_compatible.js]
+[browser_service_workers_push.js]
+[browser_service_workers_push_service.js]
+[browser_service_workers_start.js]
+[browser_service_workers_status.js]
+[browser_service_workers_timeout.js]
+skip-if = true # Bug 1232931
+[browser_service_workers_unregister.js]
+[browser_tabs.js]
+skip-if = true # Bug 1304941
diff --git a/devtools/client/aboutdebugging/test/browser_addons_debug_bootstrapped.js b/devtools/client/aboutdebugging/test/browser_addons_debug_bootstrapped.js
new file mode 100644
index 000000000..982c4c726
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser_addons_debug_bootstrapped.js
@@ -0,0 +1,83 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Avoid test timeouts that can occur while waiting for the "addon-console-works" message.
+requestLongerTimeout(2);
+
+const ADDON_ID = "test-devtools@mozilla.org";
+const ADDON_NAME = "test-devtools";
+
+const { BrowserToolboxProcess } = Cu.import("resource://devtools/client/framework/ToolboxProcess.jsm", {});
+
+add_task(function* () {
+ yield new Promise(resolve => {
+ let options = {"set": [
+ // Force enabling of addons debugging
+ ["devtools.chrome.enabled", true],
+ ["devtools.debugger.remote-enabled", true],
+ // Disable security prompt
+ ["devtools.debugger.prompt-connection", false],
+ // Enable Browser toolbox test script execution via env variable
+ ["devtools.browser-toolbox.allow-unsafe-script", true],
+ ]};
+ SpecialPowers.pushPrefEnv(options, resolve);
+ });
+
+ let { tab, document } = yield openAboutDebugging("addons");
+ yield waitForInitialAddonList(document);
+ yield installAddon({
+ document,
+ path: "addons/unpacked/install.rdf",
+ name: ADDON_NAME,
+ });
+
+ // Retrieve the DEBUG button for the addon
+ let names = [...document.querySelectorAll("#addons .target-name")];
+ let name = names.filter(element => element.textContent === ADDON_NAME)[0];
+ ok(name, "Found the addon in the list");
+ let targetElement = name.parentNode.parentNode;
+ let debugBtn = targetElement.querySelector(".debug-button");
+ ok(debugBtn, "Found its debug button");
+
+ // Wait for a notification sent by a script evaluated the test addon via
+ // the web console.
+ let onCustomMessage = new Promise(done => {
+ Services.obs.addObserver(function listener() {
+ Services.obs.removeObserver(listener, "addon-console-works");
+ done();
+ }, "addon-console-works", false);
+ });
+
+ // Be careful, this JS function is going to be executed in the addon toolbox,
+ // which lives in another process. So do not try to use any scope variable!
+ let env = Cc["@mozilla.org/process/environment;1"]
+ .getService(Ci.nsIEnvironment);
+ let testScript = function () {
+ /* eslint-disable no-undef */
+ toolbox.selectTool("webconsole")
+ .then(console => {
+ let { jsterm } = console.hud;
+ return jsterm.execute("myBootstrapAddonFunction()");
+ })
+ .then(() => toolbox.destroy());
+ /* eslint-enable no-undef */
+ };
+ env.set("MOZ_TOOLBOX_TEST_SCRIPT", "new " + testScript);
+ registerCleanupFunction(() => {
+ env.set("MOZ_TOOLBOX_TEST_SCRIPT", "");
+ });
+
+ let onToolboxClose = BrowserToolboxProcess.once("close");
+
+ debugBtn.click();
+
+ yield onCustomMessage;
+ ok(true, "Received the notification message from the bootstrap.js function");
+
+ yield onToolboxClose;
+ ok(true, "Addon toolbox closed");
+
+ yield uninstallAddon({document, id: ADDON_ID, name: ADDON_NAME});
+ yield closeAboutDebugging(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser_addons_debug_webextension.js b/devtools/client/aboutdebugging/test/browser_addons_debug_webextension.js
new file mode 100644
index 000000000..5082c1f3f
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser_addons_debug_webextension.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Avoid test timeouts that can occur while waiting for the "addon-console-works" message.
+requestLongerTimeout(2);
+
+const ADDON_ID = "test-devtools-webextension@mozilla.org";
+const ADDON_NAME = "test-devtools-webextension";
+const ADDON_MANIFEST_PATH = "addons/test-devtools-webextension/manifest.json";
+
+const {
+ BrowserToolboxProcess
+} = Cu.import("resource://devtools/client/framework/ToolboxProcess.jsm", {});
+
+/**
+ * This test file ensures that the webextension addon developer toolbox:
+ * - when the debug button is clicked on a webextension, the opened toolbox
+ * has a working webconsole with the background page as default target;
+ */
+add_task(function* testWebExtensionsToolboxWebConsole() {
+ let {
+ tab, document, debugBtn,
+ } = yield setupTestAboutDebuggingWebExtension(ADDON_NAME, ADDON_MANIFEST_PATH);
+
+ // Wait for a notification sent by a script evaluated the test addon via
+ // the web console.
+ let onCustomMessage = new Promise(done => {
+ Services.obs.addObserver(function listener(message, topic) {
+ let apiMessage = message.wrappedJSObject;
+ if (!apiMessage.originAttributes ||
+ apiMessage.originAttributes.addonId != ADDON_ID) {
+ return;
+ }
+ Services.obs.removeObserver(listener, "console-api-log-event");
+ done(apiMessage.arguments);
+ }, "console-api-log-event", false);
+ });
+
+ // Be careful, this JS function is going to be executed in the addon toolbox,
+ // which lives in another process. So do not try to use any scope variable!
+ let env = Cc["@mozilla.org/process/environment;1"]
+ .getService(Ci.nsIEnvironment);
+ let testScript = function () {
+ /* eslint-disable no-undef */
+ toolbox.selectTool("webconsole")
+ .then(console => {
+ let { jsterm } = console.hud;
+ return jsterm.execute("myWebExtensionAddonFunction()");
+ })
+ .then(() => toolbox.destroy());
+ /* eslint-enable no-undef */
+ };
+ env.set("MOZ_TOOLBOX_TEST_SCRIPT", "new " + testScript);
+ registerCleanupFunction(() => {
+ env.set("MOZ_TOOLBOX_TEST_SCRIPT", "");
+ });
+
+ let onToolboxClose = BrowserToolboxProcess.once("close");
+
+ debugBtn.click();
+
+ let args = yield onCustomMessage;
+ ok(true, "Received console message from the background page function as expected");
+ is(args[0], "Background page function called", "Got the expected console message");
+ is(args[1] && args[1].name, ADDON_NAME,
+ "Got the expected manifest from WebExtension API");
+
+ yield onToolboxClose;
+ ok(true, "Addon toolbox closed");
+
+ yield uninstallAddon({document, id: ADDON_ID, name: ADDON_NAME});
+ yield closeAboutDebugging(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser_addons_debug_webextension_inspector.js b/devtools/client/aboutdebugging/test/browser_addons_debug_webextension_inspector.js
new file mode 100644
index 000000000..3adc918d8
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser_addons_debug_webextension_inspector.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Avoid test timeouts that can occur while waiting for the "addon-console-works" message.
+requestLongerTimeout(2);
+
+const ADDON_ID = "test-devtools-webextension@mozilla.org";
+const ADDON_NAME = "test-devtools-webextension";
+const ADDON_PATH = "addons/test-devtools-webextension/manifest.json";
+
+const {
+ BrowserToolboxProcess
+} = Cu.import("resource://devtools/client/framework/ToolboxProcess.jsm", {});
+
+/**
+ * This test file ensures that the webextension addon developer toolbox:
+ * - the webextension developer toolbox has a working Inspector panel, with the
+ * background page as default target;
+ */
+add_task(function* testWebExtensionsToolboxInspector() {
+ let {
+ tab, document, debugBtn,
+ } = yield setupTestAboutDebuggingWebExtension(ADDON_NAME, ADDON_PATH);
+
+ // Be careful, this JS function is going to be executed in the addon toolbox,
+ // which lives in another process. So do not try to use any scope variable!
+ let env = Cc["@mozilla.org/process/environment;1"]
+ .getService(Ci.nsIEnvironment);
+ let testScript = function () {
+ /* eslint-disable no-undef */
+ toolbox.selectTool("inspector")
+ .then(inspector => {
+ return inspector.walker.querySelector(inspector.walker.rootNode, "body");
+ })
+ .then((nodeActor) => {
+ if (!nodeActor) {
+ throw new Error("nodeActor not found");
+ }
+
+ dump("Got a nodeActor\n");
+
+ if (!(nodeActor.inlineTextChild)) {
+ throw new Error("inlineTextChild not found");
+ }
+
+ dump("Got a nodeActor with an inline text child\n");
+
+ let expectedValue = "Background Page Body Test Content";
+ let actualValue = nodeActor.inlineTextChild._form.nodeValue;
+
+ if (String(actualValue).trim() !== String(expectedValue).trim()) {
+ throw new Error(
+ `mismatched inlineTextchild value: "${actualValue}" !== "${expectedValue}"`
+ );
+ }
+
+ dump("Got the expected inline text content in the selected node\n");
+ return Promise.resolve();
+ })
+ .then(() => toolbox.destroy())
+ .catch((error) => {
+ dump("Error while running code in the browser toolbox process:\n");
+ dump(error + "\n");
+ dump("stack:\n" + error.stack + "\n");
+ });
+ /* eslint-enable no-undef */
+ };
+ env.set("MOZ_TOOLBOX_TEST_SCRIPT", "new " + testScript);
+ registerCleanupFunction(() => {
+ env.set("MOZ_TOOLBOX_TEST_SCRIPT", "");
+ });
+
+ let onToolboxClose = BrowserToolboxProcess.once("close");
+ debugBtn.click();
+ yield onToolboxClose;
+
+ ok(true, "Addon toolbox closed");
+
+ yield uninstallAddon({document, id: ADDON_ID, name: ADDON_NAME});
+ yield closeAboutDebugging(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser_addons_debug_webextension_nobg.js b/devtools/client/aboutdebugging/test/browser_addons_debug_webextension_nobg.js
new file mode 100644
index 000000000..0e731fc5d
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser_addons_debug_webextension_nobg.js
@@ -0,0 +1,84 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Avoid test timeouts that can occur while waiting for the "addon-console-works" message.
+requestLongerTimeout(2);
+
+const ADDON_NOBG_ID = "test-devtools-webextension-nobg@mozilla.org";
+const ADDON_NOBG_NAME = "test-devtools-webextension-nobg";
+const ADDON_NOBG_PATH = "addons/test-devtools-webextension-nobg/manifest.json";
+
+const {
+ BrowserToolboxProcess
+} = Cu.import("resource://devtools/client/framework/ToolboxProcess.jsm", {});
+
+/**
+ * This test file ensures that the webextension addon developer toolbox:
+ * - the webextension developer toolbox is connected to a fallback page when the
+ * background page is not available (and in the fallback page document body contains
+ * the expected message, which warns the user that the current page is not a real
+ * webextension context);
+ */
+add_task(function* testWebExtensionsToolboxNoBackgroundPage() {
+ let {
+ tab, document, debugBtn,
+ } = yield setupTestAboutDebuggingWebExtension(ADDON_NOBG_NAME, ADDON_NOBG_PATH);
+
+ // Be careful, this JS function is going to be executed in the addon toolbox,
+ // which lives in another process. So do not try to use any scope variable!
+ let env = Cc["@mozilla.org/process/environment;1"]
+ .getService(Ci.nsIEnvironment);
+ let testScript = function () {
+ /* eslint-disable no-undef */
+ toolbox.selectTool("inspector")
+ .then(inspector => {
+ return inspector.walker.querySelector(inspector.walker.rootNode, "body");
+ })
+ .then((nodeActor) => {
+ if (!nodeActor) {
+ throw new Error("nodeActor not found");
+ }
+
+ dump("Got a nodeActor\n");
+
+ if (!(nodeActor.inlineTextChild)) {
+ throw new Error("inlineTextChild not found");
+ }
+
+ dump("Got a nodeActor with an inline text child\n");
+
+ let expectedValue = "Your addon does not have any document opened yet.";
+ let actualValue = nodeActor.inlineTextChild._form.nodeValue;
+
+ if (actualValue !== expectedValue) {
+ throw new Error(
+ `mismatched inlineTextchild value: "${actualValue}" !== "${expectedValue}"`
+ );
+ }
+
+ dump("Got the expected inline text content in the selected node\n");
+ return Promise.resolve();
+ })
+ .then(() => toolbox.destroy())
+ .catch((error) => {
+ dump("Error while running code in the browser toolbox process:\n");
+ dump(error + "\n");
+ dump("stack:\n" + error.stack + "\n");
+ });
+ /* eslint-enable no-undef */
+ };
+ env.set("MOZ_TOOLBOX_TEST_SCRIPT", "new " + testScript);
+ registerCleanupFunction(() => {
+ env.set("MOZ_TOOLBOX_TEST_SCRIPT", "");
+ });
+
+ let onToolboxClose = BrowserToolboxProcess.once("close");
+ debugBtn.click();
+ yield onToolboxClose;
+
+ ok(true, "Addon toolbox closed");
+
+ yield uninstallAddon({document, id: ADDON_NOBG_ID, name: ADDON_NOBG_NAME});
+ yield closeAboutDebugging(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser_addons_debug_webextension_popup.js b/devtools/client/aboutdebugging/test/browser_addons_debug_webextension_popup.js
new file mode 100644
index 000000000..d2cb8031e
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser_addons_debug_webextension_popup.js
@@ -0,0 +1,189 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Avoid test timeouts that can occur while waiting for the "addon-console-works" message.
+requestLongerTimeout(2);
+
+const ADDON_ID = "test-devtools-webextension@mozilla.org";
+const ADDON_NAME = "test-devtools-webextension";
+const ADDON_MANIFEST_PATH = "addons/test-devtools-webextension/manifest.json";
+
+const {
+ BrowserToolboxProcess
+} = Cu.import("resource://devtools/client/framework/ToolboxProcess.jsm", {});
+
+/**
+ * This test file ensures that the webextension addon developer toolbox:
+ * - when the debug button is clicked on a webextension, the opened toolbox
+ * has a working webconsole with the background page as default target;
+ * - the webextension developer toolbox has a working Inspector panel, with the
+ * background page as default target;
+ * - the webextension developer toolbox is connected to a fallback page when the
+ * background page is not available (and in the fallback page document body contains
+ * the expected message, which warns the user that the current page is not a real
+ * webextension context);
+ * - the webextension developer toolbox has a frame list menu and the noautohide toolbar
+ * toggle button, and they can be used to switch the current target to the extension
+ * popup page.
+ */
+
+/**
+ * Returns the widget id for an extension with the passed id.
+ */
+function makeWidgetId(id) {
+ id = id.toLowerCase();
+ return id.replace(/[^a-z0-9_-]/g, "_");
+}
+
+add_task(function* testWebExtensionsToolboxSwitchToPopup() {
+ let {
+ tab, document, debugBtn,
+ } = yield setupTestAboutDebuggingWebExtension(ADDON_NAME, ADDON_MANIFEST_PATH);
+
+ let onReadyForOpenPopup = new Promise(done => {
+ Services.obs.addObserver(function listener(message, topic) {
+ let apiMessage = message.wrappedJSObject;
+ if (!apiMessage.originAttributes ||
+ apiMessage.originAttributes.addonId != ADDON_ID) {
+ return;
+ }
+
+ if (apiMessage.arguments[0] == "readyForOpenPopup") {
+ Services.obs.removeObserver(listener, "console-api-log-event");
+ done();
+ }
+ }, "console-api-log-event", false);
+ });
+
+ // Be careful, this JS function is going to be executed in the addon toolbox,
+ // which lives in another process. So do not try to use any scope variable!
+ let env = Cc["@mozilla.org/process/environment;1"]
+ .getService(Ci.nsIEnvironment);
+ let testScript = function () {
+ /* eslint-disable no-undef */
+
+ let jsterm;
+ let popupFramePromise;
+
+ toolbox.selectTool("webconsole")
+ .then(console => {
+ dump(`Clicking the noautohide button\n`);
+ toolbox.doc.getElementById("command-button-noautohide").click();
+ dump(`Clicked the noautohide button\n`);
+
+ popupFramePromise = new Promise(resolve => {
+ let listener = (event, data) => {
+ if (data.frames.some(({url}) => url && url.endsWith("popup.html"))) {
+ toolbox.target.off("frame-update", listener);
+ resolve();
+ }
+ };
+ toolbox.target.on("frame-update", listener);
+ });
+
+ let waitForFrameListUpdate = new Promise((done) => {
+ toolbox.target.once("frame-update", () => {
+ done(console);
+ });
+ });
+
+ jsterm = console.hud.jsterm;
+ jsterm.execute("myWebExtensionShowPopup()");
+
+ // Wait the initial frame update (which list the background page).
+ return waitForFrameListUpdate;
+ })
+ .then((console) => {
+ // Wait the new frame update (once the extension popup has been opened).
+ return popupFramePromise;
+ })
+ .then(() => {
+ dump(`Clicking the frame list button\n`);
+ let btn = toolbox.doc.getElementById("command-button-frames");
+ let menu = toolbox.showFramesMenu({target: btn});
+ dump(`Clicked the frame list button\n`);
+ return menu.once("open").then(() => {
+ return menu;
+ });
+ })
+ .then(frameMenu => {
+ let frames = frameMenu.items;
+
+ if (frames.length != 2) {
+ throw Error(`Number of frames found is wrong: ${frames.length} != 2`);
+ }
+
+ let popupFrameBtn = frames.filter((frame) => {
+ return frame.label.endsWith("popup.html");
+ }).pop();
+
+ if (!popupFrameBtn) {
+ throw Error("Extension Popup frame not found in the listed frames");
+ }
+
+ let waitForNavigated = toolbox.target.once("navigate");
+
+ popupFrameBtn.click();
+
+ return waitForNavigated;
+ })
+ .then(() => {
+ return jsterm.execute("myWebExtensionPopupAddonFunction()");
+ })
+ .then(() => toolbox.destroy())
+ .catch((error) => {
+ dump("Error while running code in the browser toolbox process:\n");
+ dump(error + "\n");
+ dump("stack:\n" + error.stack + "\n");
+ });
+ /* eslint-enable no-undef */
+ };
+ env.set("MOZ_TOOLBOX_TEST_SCRIPT", "new " + testScript);
+ registerCleanupFunction(() => {
+ env.set("MOZ_TOOLBOX_TEST_SCRIPT", "");
+ });
+
+ // Wait for a notification sent by a script evaluated the test addon via
+ // the web console.
+ let onPopupCustomMessage = new Promise(done => {
+ Services.obs.addObserver(function listener(message, topic) {
+ let apiMessage = message.wrappedJSObject;
+ if (!apiMessage.originAttributes ||
+ apiMessage.originAttributes.addonId != ADDON_ID) {
+ return;
+ }
+
+ if (apiMessage.arguments[0] == "Popup page function called") {
+ Services.obs.removeObserver(listener, "console-api-log-event");
+ done(apiMessage.arguments);
+ }
+ }, "console-api-log-event", false);
+ });
+
+ let onToolboxClose = BrowserToolboxProcess.once("close");
+
+ debugBtn.click();
+
+ yield onReadyForOpenPopup;
+
+ let browserActionId = makeWidgetId(ADDON_ID) + "-browser-action";
+ let browserActionEl = window.document.getElementById(browserActionId);
+
+ ok(browserActionEl, "Got the browserAction button from the browser UI");
+ browserActionEl.click();
+ info("Clicked on the browserAction button");
+
+ let args = yield onPopupCustomMessage;
+ ok(true, "Received console message from the popup page function as expected");
+ is(args[0], "Popup page function called", "Got the expected console message");
+ is(args[1] && args[1].name, ADDON_NAME,
+ "Got the expected manifest from WebExtension API");
+
+ yield onToolboxClose;
+
+ ok(true, "Addon toolbox closed");
+
+ yield uninstallAddon({document, id: ADDON_ID, name: ADDON_NAME});
+ yield closeAboutDebugging(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser_addons_debugging_initial_state.js b/devtools/client/aboutdebugging/test/browser_addons_debugging_initial_state.js
new file mode 100644
index 000000000..8a63d0061
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser_addons_debugging_initial_state.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that addons debugging controls are properly enabled/disabled depending
+// on the values of the relevant preferences:
+// - devtools.chrome.enabled
+// - devtools.debugger.remote-enabled
+
+const ADDON_ID = "test-devtools@mozilla.org";
+const ADDON_NAME = "test-devtools";
+
+const TEST_DATA = [
+ {
+ chromeEnabled: false,
+ debuggerRemoteEnable: false,
+ expected: false,
+ }, {
+ chromeEnabled: false,
+ debuggerRemoteEnable: true,
+ expected: false,
+ }, {
+ chromeEnabled: true,
+ debuggerRemoteEnable: false,
+ expected: false,
+ }, {
+ chromeEnabled: true,
+ debuggerRemoteEnable: true,
+ expected: true,
+ }
+];
+
+add_task(function* () {
+ for (let testData of TEST_DATA) {
+ yield testCheckboxState(testData);
+ }
+});
+
+function* testCheckboxState(testData) {
+ info("Set preferences as defined by the current test data.");
+ yield new Promise(resolve => {
+ let options = {"set": [
+ ["devtools.chrome.enabled", testData.chromeEnabled],
+ ["devtools.debugger.remote-enabled", testData.debuggerRemoteEnable],
+ ]};
+ SpecialPowers.pushPrefEnv(options, resolve);
+ });
+
+ let { tab, document } = yield openAboutDebugging("addons");
+ yield waitForInitialAddonList(document);
+
+ info("Install a test addon.");
+ yield installAddon({
+ document,
+ path: "addons/unpacked/install.rdf",
+ name: ADDON_NAME,
+ });
+
+ info("Test checkbox checked state.");
+ let addonDebugCheckbox = document.querySelector("#enable-addon-debugging");
+ is(addonDebugCheckbox.checked, testData.expected,
+ "Addons debugging checkbox should be in expected state.");
+
+ info("Test debug buttons disabled state.");
+ let debugButtons = [...document.querySelectorAll("#addons .debug-button")];
+ ok(debugButtons.every(b => b.disabled != testData.expected),
+ "Debug buttons should be in the expected state");
+
+ info("Uninstall test addon installed earlier.");
+ yield uninstallAddon({document, id: ADDON_ID, name: ADDON_NAME});
+
+ yield closeAboutDebugging(tab);
+}
diff --git a/devtools/client/aboutdebugging/test/browser_addons_install.js b/devtools/client/aboutdebugging/test/browser_addons_install.js
new file mode 100644
index 000000000..4c3a97c9f
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser_addons_install.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const ADDON_ID = "test-devtools@mozilla.org";
+const ADDON_NAME = "test-devtools";
+
+add_task(function* () {
+ let { tab, document } = yield openAboutDebugging("addons");
+ yield waitForInitialAddonList(document);
+
+ // Install this add-on, and verify that it appears in the about:debugging UI
+ yield installAddon({
+ document,
+ path: "addons/unpacked/install.rdf",
+ name: ADDON_NAME,
+ });
+
+ // Install the add-on, and verify that it disappears in the about:debugging UI
+ yield uninstallAddon({document, id: ADDON_ID, name: ADDON_NAME});
+
+ yield closeAboutDebugging(tab);
+});
+
+add_task(function* () {
+ let { tab, document } = yield openAboutDebugging("addons");
+ yield waitForInitialAddonList(document);
+
+ // Start an observer that looks for the install error before
+ // actually doing the install
+ let top = document.querySelector(".addons-top");
+ let promise = waitForMutation(top, { childList: true });
+
+ // Mock the file picker to select a test addon
+ let MockFilePicker = SpecialPowers.MockFilePicker;
+ MockFilePicker.init(null);
+ let file = getSupportsFile("addons/bad/manifest.json");
+ MockFilePicker.returnFiles = [file.file];
+
+ // Trigger the file picker by clicking on the button
+ document.getElementById("load-addon-from-file").click();
+
+ // Now wait for the install error to appear.
+ yield promise;
+
+ // And check that it really is there.
+ let err = document.querySelector(".addons-install-error");
+ isnot(err, null, "Addon install error message appeared");
+
+ yield closeAboutDebugging(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser_addons_reload.js b/devtools/client/aboutdebugging/test/browser_addons_reload.js
new file mode 100644
index 000000000..506495a60
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser_addons_reload.js
@@ -0,0 +1,207 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const ADDON_ID = "test-devtools@mozilla.org";
+const ADDON_NAME = "test-devtools";
+
+/**
+ * Returns a promise that resolves when the given add-on event is fired. The
+ * resolved value is an array of arguments passed for the event.
+ */
+function promiseAddonEvent(event) {
+ return new Promise(resolve => {
+ let listener = {
+ [event]: function (...args) {
+ AddonManager.removeAddonListener(listener);
+ resolve(args);
+ }
+ };
+
+ AddonManager.addAddonListener(listener);
+ });
+}
+
+function* tearDownAddon(addon) {
+ const onUninstalled = promiseAddonEvent("onUninstalled");
+ addon.uninstall();
+ const [uninstalledAddon] = yield onUninstalled;
+ is(uninstalledAddon.id, addon.id,
+ `Add-on was uninstalled: ${uninstalledAddon.id}`);
+}
+
+function getReloadButton(document, addonName) {
+ const names = [...document.querySelectorAll("#addons .target-name")];
+ const name = names.filter(element => element.textContent === addonName)[0];
+ ok(name, `Found ${addonName} add-on in the list`);
+ const targetElement = name.parentNode.parentNode;
+ const reloadButton = targetElement.querySelector(".reload-button");
+ info(`Found reload button for ${addonName}`);
+ return reloadButton;
+}
+
+function installAddonWithManager(filePath) {
+ return new Promise((resolve, reject) => {
+ AddonManager.getInstallForFile(filePath, install => {
+ if (!install) {
+ throw new Error(`An install was not created for ${filePath}`);
+ }
+ install.addListener({
+ onDownloadFailed: reject,
+ onDownloadCancelled: reject,
+ onInstallFailed: reject,
+ onInstallCancelled: reject,
+ onInstallEnded: resolve
+ });
+ install.install();
+ });
+ });
+}
+
+function getAddonByID(addonId) {
+ return new Promise(resolve => {
+ AddonManager.getAddonByID(addonId, addon => resolve(addon));
+ });
+}
+
+/**
+ * Creates a web extension from scratch in a temporary location.
+ * The object must be removed when you're finished working with it.
+ */
+class TempWebExt {
+ constructor(addonId) {
+ this.addonId = addonId;
+ this.tmpDir = FileUtils.getDir("TmpD", ["browser_addons_reload"]);
+ if (!this.tmpDir.exists()) {
+ this.tmpDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+ }
+ this.sourceDir = this.tmpDir.clone();
+ this.sourceDir.append(this.addonId);
+ if (!this.sourceDir.exists()) {
+ this.sourceDir.create(Ci.nsIFile.DIRECTORY_TYPE,
+ FileUtils.PERMS_DIRECTORY);
+ }
+ }
+
+ writeManifest(manifestData) {
+ const manifest = this.sourceDir.clone();
+ manifest.append("manifest.json");
+ if (manifest.exists()) {
+ manifest.remove(true);
+ }
+ const fos = Cc["@mozilla.org/network/file-output-stream;1"]
+ .createInstance(Ci.nsIFileOutputStream);
+ fos.init(manifest,
+ FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE |
+ FileUtils.MODE_TRUNCATE,
+ FileUtils.PERMS_FILE, 0);
+
+ const manifestString = JSON.stringify(manifestData);
+ fos.write(manifestString, manifestString.length);
+ fos.close();
+ }
+
+ remove() {
+ return this.tmpDir.remove(true);
+ }
+}
+
+add_task(function* reloadButtonReloadsAddon() {
+ const { tab, document } = yield openAboutDebugging("addons");
+ yield waitForInitialAddonList(document);
+ yield installAddon({
+ document,
+ path: "addons/unpacked/install.rdf",
+ name: ADDON_NAME,
+ });
+
+ const reloadButton = getReloadButton(document, ADDON_NAME);
+ is(reloadButton.disabled, false, "Reload button should not be disabled");
+ is(reloadButton.title, "", "Reload button should not have a tooltip");
+ const onInstalled = promiseAddonEvent("onInstalled");
+
+ const onBootstrapInstallCalled = new Promise(done => {
+ Services.obs.addObserver(function listener() {
+ Services.obs.removeObserver(listener, ADDON_NAME, false);
+ info("Add-on was re-installed: " + ADDON_NAME);
+ done();
+ }, ADDON_NAME, false);
+ });
+
+ reloadButton.click();
+
+ const [reloadedAddon] = yield onInstalled;
+ is(reloadedAddon.name, ADDON_NAME,
+ "Add-on was reloaded: " + reloadedAddon.name);
+
+ yield onBootstrapInstallCalled;
+ yield tearDownAddon(reloadedAddon);
+ yield closeAboutDebugging(tab);
+});
+
+add_task(function* reloadButtonRefreshesMetadata() {
+ const { tab, document } = yield openAboutDebugging("addons");
+ yield waitForInitialAddonList(document);
+
+ const manifestBase = {
+ "manifest_version": 2,
+ "name": "Temporary web extension",
+ "version": "1.0",
+ "applications": {
+ "gecko": {
+ "id": ADDON_ID
+ }
+ }
+ };
+
+ const tempExt = new TempWebExt(ADDON_ID);
+ tempExt.writeManifest(manifestBase);
+
+ const onAddonListUpdated = waitForMutation(getAddonList(document),
+ { childList: true });
+ const onInstalled = promiseAddonEvent("onInstalled");
+ yield AddonManager.installTemporaryAddon(tempExt.sourceDir);
+ const [addon] = yield onInstalled;
+ info(`addon installed: ${addon.id}`);
+ yield onAddonListUpdated;
+
+ const newName = "Temporary web extension (updated)";
+ tempExt.writeManifest(Object.assign({}, manifestBase, {name: newName}));
+
+ // Wait for the add-on list to be updated with the reloaded name.
+ const onReInstall = promiseAddonEvent("onInstalled");
+ const onAddonReloaded = waitForContentMutation(getAddonList(document));
+
+ const reloadButton = getReloadButton(document, manifestBase.name);
+ reloadButton.click();
+
+ yield onAddonReloaded;
+ const [reloadedAddon] = yield onReInstall;
+ // Make sure the name was updated correctly.
+ const allAddons = [...document.querySelectorAll("#addons .target-name")]
+ .map(element => element.textContent);
+ const nameWasUpdated = allAddons.some(name => name === newName);
+ ok(nameWasUpdated, `New name appeared in reloaded add-ons: ${allAddons}`);
+
+ yield tearDownAddon(reloadedAddon);
+ tempExt.remove();
+ yield closeAboutDebugging(tab);
+});
+
+add_task(function* onlyTempInstalledAddonsCanBeReloaded() {
+ const { tab, document } = yield openAboutDebugging("addons");
+ yield waitForInitialAddonList(document);
+ const onAddonListUpdated = waitForMutation(getAddonList(document),
+ { childList: true });
+ yield installAddonWithManager(getSupportsFile("addons/bug1273184.xpi").file);
+ yield onAddonListUpdated;
+ const addon = yield getAddonByID("bug1273184@tests");
+
+ const reloadButton = getReloadButton(document, addon.name);
+ ok(reloadButton, "Reload button exists");
+ is(reloadButton.disabled, true, "Reload button should be disabled");
+ ok(reloadButton.title, "Disabled reload button should have a tooltip");
+
+ yield tearDownAddon(addon);
+ yield closeAboutDebugging(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser_addons_toggle_debug.js b/devtools/client/aboutdebugging/test/browser_addons_toggle_debug.js
new file mode 100644
index 000000000..1f67cac5b
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser_addons_toggle_debug.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that individual Debug buttons are disabled when "Addons debugging"
+// is disabled.
+// Test that the buttons are updated dynamically if the preference changes.
+
+const ADDON_ID = "test-devtools@mozilla.org";
+const ADDON_NAME = "test-devtools";
+
+add_task(function* () {
+ info("Turn off addon debugging.");
+ yield new Promise(resolve => {
+ let options = {"set": [
+ ["devtools.chrome.enabled", false],
+ ["devtools.debugger.remote-enabled", false],
+ ]};
+ SpecialPowers.pushPrefEnv(options, resolve);
+ });
+
+ let { tab, document } = yield openAboutDebugging("addons");
+ yield waitForInitialAddonList(document);
+
+ info("Install a test addon.");
+ yield installAddon({
+ document,
+ path: "addons/unpacked/install.rdf",
+ name: ADDON_NAME,
+ });
+
+ let addonDebugCheckbox = document.querySelector("#enable-addon-debugging");
+ ok(!addonDebugCheckbox.checked, "Addons debugging should be disabled.");
+
+ info("Check all debug buttons are disabled.");
+ let debugButtons = [...document.querySelectorAll("#addons .debug-button")];
+ ok(debugButtons.every(b => b.disabled), "Debug buttons should be disabled");
+
+ info("Click on 'Enable addons debugging' checkbox.");
+ let addonsContainer = document.getElementById("addons");
+ let onAddonsMutation = waitForMutation(addonsContainer,
+ { subtree: true, attributes: true });
+ addonDebugCheckbox.click();
+ yield onAddonsMutation;
+
+ info("Check all debug buttons are enabled.");
+ ok(addonDebugCheckbox.checked, "Addons debugging should be enabled.");
+ debugButtons = [...document.querySelectorAll("#addons .debug-button")];
+ ok(debugButtons.every(b => !b.disabled), "Debug buttons should be enabled");
+
+ info("Click again on 'Enable addons debugging' checkbox.");
+ onAddonsMutation = waitForMutation(addonsContainer,
+ { subtree: true, attributes: true });
+ addonDebugCheckbox.click();
+ yield onAddonsMutation;
+
+ info("Check all debug buttons are disabled again.");
+ debugButtons = [...document.querySelectorAll("#addons .debug-button")];
+ ok(debugButtons.every(b => b.disabled), "Debug buttons should be disabled");
+
+ info("Uninstall addon installed earlier.");
+ yield uninstallAddon({document, id: ADDON_ID, name: ADDON_NAME});
+
+ yield closeAboutDebugging(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser_page_not_found.js b/devtools/client/aboutdebugging/test/browser_page_not_found.js
new file mode 100644
index 000000000..107bc8b91
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser_page_not_found.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that navigating to a about:debugging#invalid-hash should show up an
+// error page.
+// Every url navigating including #invalid-hash should be kept in history and
+// navigate back as expected.
+add_task(function* () {
+ let { tab, document } = yield openAboutDebugging("invalid-hash");
+ let element = document.querySelector(".header-name");
+ is(element.textContent, "Page not found", "Show error page");
+
+ yield openPanel(document, "addons-panel");
+ yield waitForInitialAddonList(document);
+ element = document.querySelector(".header-name");
+ is(element.textContent, "Add-ons", "Show Addons");
+
+ yield changeAboutDebuggingHash(document, "invalid-hash");
+ element = document.querySelector(".header-name");
+ is(element.textContent, "Page not found", "Show error page");
+
+ gBrowser.goBack();
+ yield waitForMutation(
+ document.querySelector(".main-content"), {childList: true});
+ yield waitForInitialAddonList(document);
+ element = document.querySelector(".header-name");
+ is(element.textContent, "Add-ons", "Show Addons");
+
+ gBrowser.goBack();
+ yield waitForMutation(
+ document.querySelector(".main-content"), {childList: true});
+ element = document.querySelector(".header-name");
+ is(element.textContent, "Page not found", "Show error page");
+
+ yield closeAboutDebugging(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser_service_workers.js b/devtools/client/aboutdebugging/test/browser_service_workers.js
new file mode 100644
index 000000000..74e4efb3e
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser_service_workers.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Service workers can't be loaded from chrome://,
+// but http:// is ok with dom.serviceWorkers.testing.enabled turned on.
+const SERVICE_WORKER = URL_ROOT + "service-workers/empty-sw.js";
+const TAB_URL = URL_ROOT + "service-workers/empty-sw.html";
+
+add_task(function* () {
+ yield new Promise(done => {
+ let options = {"set": [
+ // Accept workers from mochitest's http.
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.openWindow.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ]};
+ SpecialPowers.pushPrefEnv(options, done);
+ });
+
+ let { tab, document } = yield openAboutDebugging("workers");
+
+ let swTab = yield addTab(TAB_URL);
+
+ let serviceWorkersElement = getServiceWorkerList(document);
+
+ yield waitForMutation(serviceWorkersElement, { childList: true });
+
+ // Check that the service worker appears in the UI
+ let names = [...document.querySelectorAll("#service-workers .target-name")];
+ names = names.map(element => element.textContent);
+ ok(names.includes(SERVICE_WORKER),
+ "The service worker url appears in the list: " + names);
+
+ try {
+ yield unregisterServiceWorker(swTab, serviceWorkersElement);
+ ok(true, "Service worker registration unregistered");
+ } catch (e) {
+ ok(false, "SW not unregistered; " + e);
+ }
+
+ // Check that the service worker disappeared from the UI
+ names = [...document.querySelectorAll("#service-workers .target-name")];
+ names = names.map(element => element.textContent);
+ ok(!names.includes(SERVICE_WORKER),
+ "The service worker url is no longer in the list: " + names);
+
+ yield removeTab(swTab);
+ yield closeAboutDebugging(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser_service_workers_not_compatible.js b/devtools/client/aboutdebugging/test/browser_service_workers_not_compatible.js
new file mode 100644
index 000000000..6221230b5
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser_service_workers_not_compatible.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that Service Worker section should show warning message in
+// about:debugging if any of following conditions is met:
+// 1. service worker is disabled
+// 2. the about:debugging pannel is openned in private browsing mode
+// 3. the about:debugging pannel is openned in private content window
+
+var imgClass = ".service-worker-disabled .warning";
+
+add_task(function* () {
+ yield new Promise(done => {
+ info("disable service workers");
+ let options = {"set": [
+ ["dom.serviceWorkers.enabled", false],
+ ]};
+ SpecialPowers.pushPrefEnv(options, done);
+ });
+
+ let { tab, document } = yield openAboutDebugging("workers");
+ // Check that the warning img appears in the UI
+ let img = document.querySelector(imgClass);
+ ok(img, "warning message is rendered");
+
+ yield closeAboutDebugging(tab);
+});
+
+add_task(function* () {
+ yield new Promise(done => {
+ info("set private browsing mode as default");
+ let options = {"set": [
+ ["browser.privatebrowsing.autostart", true],
+ ]};
+ SpecialPowers.pushPrefEnv(options, done);
+ });
+
+ let { tab, document } = yield openAboutDebugging("workers");
+ // Check that the warning img appears in the UI
+ let img = document.querySelector(imgClass);
+ ok(img, "warning message is rendered");
+
+ yield closeAboutDebugging(tab);
+});
+
+add_task(function* () {
+ info("Opening a new private window");
+ let win = OpenBrowserWindow({private: true});
+ yield waitForDelayedStartupFinished(win);
+
+ let { tab, document } = yield openAboutDebugging("workers", win);
+ // Check that the warning img appears in the UI
+ let img = document.querySelector(imgClass);
+ ok(img, "warning message is rendered");
+
+ yield closeAboutDebugging(tab);
+ win.close();
+});
diff --git a/devtools/client/aboutdebugging/test/browser_service_workers_push.js b/devtools/client/aboutdebugging/test/browser_service_workers_push.js
new file mode 100644
index 000000000..ff7789458
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser_service_workers_push.js
@@ -0,0 +1,105 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* global sendAsyncMessage */
+
+"use strict";
+
+// Test that clicking on the Push button next to a Service Worker works as
+// intended in about:debugging.
+// It should trigger a "push" notification in the worker.
+
+// Service workers can't be loaded from chrome://, but http:// is ok with
+// dom.serviceWorkers.testing.enabled turned on.
+const SERVICE_WORKER = URL_ROOT + "service-workers/push-sw.js";
+const TAB_URL = URL_ROOT + "service-workers/push-sw.html";
+
+add_task(function* () {
+ info("Turn on workers via mochitest http.");
+ yield new Promise(done => {
+ let options = { "set": [
+ // Accept workers from mochitest's http.
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.openWindow.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ]};
+ SpecialPowers.pushPrefEnv(options, done);
+ });
+
+ let { tab, document } = yield openAboutDebugging("workers");
+
+ // Listen for mutations in the service-workers list.
+ let serviceWorkersElement = getServiceWorkerList(document);
+ let onMutation = waitForMutation(serviceWorkersElement, { childList: true });
+
+ // Open a tab that registers a push service worker.
+ let swTab = yield addTab(TAB_URL);
+
+ info("Make the test page notify us when the service worker sends a message.");
+
+ yield ContentTask.spawn(swTab.linkedBrowser, {}, function () {
+ let win = content.wrappedJSObject;
+ win.navigator.serviceWorker.addEventListener("message", function (event) {
+ sendAsyncMessage(event.data);
+ }, false);
+ });
+
+ // Expect the service worker to claim the test window when activating.
+ let mm = swTab.linkedBrowser.messageManager;
+ let onClaimed = new Promise(done => {
+ mm.addMessageListener("sw-claimed", function listener() {
+ mm.removeMessageListener("sw-claimed", listener);
+ done();
+ });
+ });
+
+ // Wait for the service-workers list to update.
+ yield onMutation;
+
+ // Check that the service worker appears in the UI.
+ assertHasTarget(true, document, "service-workers", SERVICE_WORKER);
+
+ info("Ensure that the registration resolved before trying to interact with " +
+ "the service worker.");
+ yield waitForServiceWorkerRegistered(swTab);
+ ok(true, "Service worker registration resolved");
+
+ yield waitForServiceWorkerActivation(SERVICE_WORKER, document);
+
+ // Retrieve the Push button for the worker.
+ let names = [...document.querySelectorAll("#service-workers .target-name")];
+ let name = names.filter(element => element.textContent === SERVICE_WORKER)[0];
+ ok(name, "Found the service worker in the list");
+
+ let targetElement = name.parentNode.parentNode;
+
+ let pushBtn = targetElement.querySelector(".push-button");
+ ok(pushBtn, "Found its push button");
+
+ info("Wait for the service worker to claim the test window before " +
+ "proceeding.");
+ yield onClaimed;
+
+ info("Click on the Push button and wait for the service worker to receive " +
+ "a push notification");
+ let onPushNotification = new Promise(done => {
+ mm.addMessageListener("sw-pushed", function listener() {
+ mm.removeMessageListener("sw-pushed", listener);
+ done();
+ });
+ });
+ pushBtn.click();
+ yield onPushNotification;
+ ok(true, "Service worker received a push notification");
+
+ // Finally, unregister the service worker itself.
+ try {
+ yield unregisterServiceWorker(swTab, serviceWorkersElement);
+ ok(true, "Service worker registration unregistered");
+ } catch (e) {
+ ok(false, "SW not unregistered; " + e);
+ }
+
+ yield removeTab(swTab);
+ yield closeAboutDebugging(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser_service_workers_push_service.js b/devtools/client/aboutdebugging/test/browser_service_workers_push_service.js
new file mode 100644
index 000000000..732380a4c
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser_service_workers_push_service.js
@@ -0,0 +1,122 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that a Service Worker registration's Push Service subscription appears
+// in about:debugging if it exists, and disappears when unregistered.
+
+// Service workers can't be loaded from chrome://, but http:// is ok with
+// dom.serviceWorkers.testing.enabled turned on.
+const SERVICE_WORKER = URL_ROOT + "service-workers/push-sw.js";
+const TAB_URL = URL_ROOT + "service-workers/push-sw.html";
+
+const FAKE_ENDPOINT = "https://fake/endpoint";
+
+const PushService = Cc["@mozilla.org/push/Service;1"]
+ .getService(Ci.nsIPushService).wrappedJSObject;
+
+add_task(function* () {
+ info("Turn on workers via mochitest http.");
+ yield SpecialPowers.pushPrefEnv({
+ "set": [
+ // Accept workers from mochitest's http.
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.openWindow.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ // Enable the push service.
+ ["dom.push.enabled", true],
+ ["dom.push.connection.enabled", true],
+ ]
+ });
+
+ info("Mock the push service");
+ PushService.service = {
+ _registrations: new Map(),
+ _notify(scope) {
+ Services.obs.notifyObservers(
+ null,
+ PushService.subscriptionModifiedTopic,
+ scope);
+ },
+ init() {},
+ register(pageRecord) {
+ let registration = {
+ endpoint: FAKE_ENDPOINT
+ };
+ this._registrations.set(pageRecord.scope, registration);
+ this._notify(pageRecord.scope);
+ return Promise.resolve(registration);
+ },
+ registration(pageRecord) {
+ return Promise.resolve(this._registrations.get(pageRecord.scope));
+ },
+ unregister(pageRecord) {
+ let deleted = this._registrations.delete(pageRecord.scope);
+ if (deleted) {
+ this._notify(pageRecord.scope);
+ }
+ return Promise.resolve(deleted);
+ },
+ };
+
+ let { tab, document } = yield openAboutDebugging("workers");
+
+ // Listen for mutations in the service-workers list.
+ let serviceWorkersElement = document.getElementById("service-workers");
+ let onMutation = waitForMutation(serviceWorkersElement, { childList: true });
+
+ // Open a tab that registers a push service worker.
+ let swTab = yield addTab(TAB_URL);
+
+ // Wait for the service-workers list to update.
+ yield onMutation;
+
+ // Check that the service worker appears in the UI.
+ assertHasTarget(true, document, "service-workers", SERVICE_WORKER);
+
+ yield waitForServiceWorkerActivation(SERVICE_WORKER, document);
+
+ // Wait for the service worker details to update.
+ let names = [...document.querySelectorAll("#service-workers .target-name")];
+ let name = names.filter(element => element.textContent === SERVICE_WORKER)[0];
+ ok(name, "Found the service worker in the list");
+
+ let targetContainer = name.parentNode.parentNode;
+ let targetDetailsElement = targetContainer.querySelector(".target-details");
+
+ // Retrieve the push subscription endpoint URL, and verify it looks good.
+ let pushURL = targetContainer.querySelector(".service-worker-push-url");
+ if (!pushURL) {
+ yield waitForMutation(targetDetailsElement, { childList: true });
+ pushURL = targetContainer.querySelector(".service-worker-push-url");
+ }
+
+ ok(pushURL, "Found the push service URL in the service worker details");
+ is(pushURL.textContent, FAKE_ENDPOINT, "The push service URL looks correct");
+
+ // Unsubscribe from the push service.
+ ContentTask.spawn(swTab.linkedBrowser, {}, function () {
+ let win = content.wrappedJSObject;
+ return win.sub.unsubscribe();
+ });
+
+ // Wait for the service worker details to update again.
+ yield waitForMutation(targetDetailsElement, { childList: true });
+ ok(!targetContainer.querySelector(".service-worker-push-url"),
+ "The push service URL should be removed");
+
+ // Finally, unregister the service worker itself.
+ try {
+ yield unregisterServiceWorker(swTab, serviceWorkersElement);
+ ok(true, "Service worker registration unregistered");
+ } catch (e) {
+ ok(false, "SW not unregistered; " + e);
+ }
+
+ info("Unmock the push service");
+ PushService.service = null;
+
+ yield removeTab(swTab);
+ yield closeAboutDebugging(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser_service_workers_start.js b/devtools/client/aboutdebugging/test/browser_service_workers_start.js
new file mode 100644
index 000000000..8c15d97c4
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser_service_workers_start.js
@@ -0,0 +1,97 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that clicking on the Start button next to a Service Worker works as
+// intended in about:debugging.
+// It should cause a worker to start running in a child process.
+
+// Service workers can't be loaded from chrome://, but http:// is ok with
+// dom.serviceWorkers.testing.enabled turned on.
+const SERVICE_WORKER = URL_ROOT + "service-workers/empty-sw.js";
+const TAB_URL = URL_ROOT + "service-workers/empty-sw.html";
+
+const SW_TIMEOUT = 1000;
+
+add_task(function* () {
+ info("Turn on workers via mochitest http.");
+ yield new Promise(done => {
+ let options = { "set": [
+ // Accept workers from mochitest's http.
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.openWindow.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ // Reduce the timeout to accelerate service worker freezing
+ ["dom.serviceWorkers.idle_timeout", SW_TIMEOUT],
+ ["dom.serviceWorkers.idle_extended_timeout", SW_TIMEOUT],
+ ]};
+ SpecialPowers.pushPrefEnv(options, done);
+ });
+
+ let { tab, document } = yield openAboutDebugging("workers");
+
+ // Listen for mutations in the service-workers list.
+ let serviceWorkersElement = getServiceWorkerList(document);
+ let onMutation = waitForMutation(serviceWorkersElement, { childList: true });
+
+ // Open a tab that registers an empty service worker.
+ let swTab = yield addTab(TAB_URL);
+
+ // Wait for the service-workers list to update.
+ yield onMutation;
+
+ // Check that the service worker appears in the UI.
+ assertHasTarget(true, document, "service-workers", SERVICE_WORKER);
+
+ info("Ensure that the registration resolved before trying to interact with " +
+ "the service worker.");
+ yield waitForServiceWorkerRegistered(swTab);
+ ok(true, "Service worker registration resolved");
+
+ yield waitForServiceWorkerActivation(SERVICE_WORKER, document);
+
+ // Retrieve the Target element corresponding to the service worker.
+ let names = [...document.querySelectorAll("#service-workers .target-name")];
+ let name = names.filter(element => element.textContent === SERVICE_WORKER)[0];
+ ok(name, "Found the service worker in the list");
+ let targetElement = name.parentNode.parentNode;
+
+ // The service worker may already be killed with the low 1s timeout
+ if (!targetElement.querySelector(".start-button")) {
+ // Check that there is a Debug button but not a Start button.
+ ok(targetElement.querySelector(".debug-button"), "Found its debug button");
+
+ // Wait for the service worker to be killed due to inactivity.
+ yield waitForMutation(targetElement, { childList: true });
+ } else {
+ // Check that there is no Debug button when the SW is already shut down.
+ ok(!targetElement.querySelector(".debug-button"), "No debug button when " +
+ "the worker is already killed");
+ }
+
+ // We should now have a Start button but no Debug button.
+ let startBtn = targetElement.querySelector(".start-button");
+ ok(startBtn, "Found its start button");
+ ok(!targetElement.querySelector(".debug-button"), "No debug button");
+
+ // Click on the Start button and wait for the service worker to be back.
+ let onStarted = waitForMutation(targetElement, { childList: true });
+ startBtn.click();
+ yield onStarted;
+
+ // Check that we have a Debug button but not a Start button again.
+ ok(targetElement.querySelector(".debug-button"), "Found its debug button");
+ ok(!targetElement.querySelector(".start-button"), "No start button");
+
+ // Finally, unregister the service worker itself.
+ try {
+ yield unregisterServiceWorker(swTab, serviceWorkersElement);
+ ok(true, "Service worker registration unregistered");
+ } catch (e) {
+ ok(false, "SW not unregistered; " + e);
+ }
+
+ yield removeTab(swTab);
+ yield closeAboutDebugging(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser_service_workers_status.js b/devtools/client/aboutdebugging/test/browser_service_workers_status.js
new file mode 100644
index 000000000..dff384ee5
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser_service_workers_status.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Service workers can't be loaded from chrome://,
+// but http:// is ok with dom.serviceWorkers.testing.enabled turned on.
+const SERVICE_WORKER = URL_ROOT + "service-workers/delay-sw.js";
+const TAB_URL = URL_ROOT + "service-workers/delay-sw.html";
+const SW_TIMEOUT = 2000;
+
+requestLongerTimeout(2);
+
+add_task(function* () {
+ yield SpecialPowers.pushPrefEnv({
+ "set": [
+ // Accept workers from mochitest's http.
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.openWindow.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ // Reduce the timeout to expose issues when service worker
+ // freezing is broken
+ ["dom.serviceWorkers.idle_timeout", SW_TIMEOUT],
+ ["dom.serviceWorkers.idle_extended_timeout", SW_TIMEOUT],
+ ["dom.ipc.processCount", 1],
+ ]
+ });
+
+ let { tab, document } = yield openAboutDebugging("workers");
+
+ // Listen for mutations in the service-workers list.
+ let serviceWorkersElement = getServiceWorkerList(document);
+ let onMutation = waitForMutation(serviceWorkersElement, { childList: true });
+
+ let swTab = yield addTab(TAB_URL);
+
+ info("Make the test page notify us when the service worker sends a message.");
+
+ // Wait for the service-workers list to update.
+ yield onMutation;
+
+ // Check that the service worker appears in the UI
+ let names = [...document.querySelectorAll("#service-workers .target-name")];
+ let name = names.filter(element => element.textContent === SERVICE_WORKER)[0];
+ ok(name, "Found the service worker in the list");
+
+ let targetElement = name.parentNode.parentNode;
+ let status = targetElement.querySelector(".target-status");
+ is(status.textContent, "Registering", "Service worker is currently registering");
+
+ yield waitForMutation(serviceWorkersElement, { childList: true, subtree: true });
+ is(status.textContent, "Running", "Service worker is currently running");
+
+ yield waitForMutation(serviceWorkersElement, { attributes: true, subtree: true });
+ is(status.textContent, "Stopped", "Service worker is currently stopped");
+
+ try {
+ yield unregisterServiceWorker(swTab, serviceWorkersElement);
+ ok(true, "Service worker unregistered");
+ } catch (e) {
+ ok(false, "Service worker not unregistered; " + e);
+ }
+
+ // Check that the service worker disappeared from the UI
+ names = [...document.querySelectorAll("#service-workers .target-name")];
+ names = names.map(element => element.textContent);
+ ok(!names.includes(SERVICE_WORKER),
+ "The service worker url is no longer in the list: " + names);
+
+ yield removeTab(swTab);
+ yield closeAboutDebugging(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser_service_workers_timeout.js b/devtools/client/aboutdebugging/test/browser_service_workers_timeout.js
new file mode 100644
index 000000000..94e064029
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser_service_workers_timeout.js
@@ -0,0 +1,92 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Service workers can't be loaded from chrome://,
+// but http:// is ok with dom.serviceWorkers.testing.enabled turned on.
+const SERVICE_WORKER = URL_ROOT + "service-workers/empty-sw.js";
+const TAB_URL = URL_ROOT + "service-workers/empty-sw.html";
+
+const SW_TIMEOUT = 1000;
+
+add_task(function* () {
+ yield new Promise(done => {
+ let options = {"set": [
+ // Accept workers from mochitest's http.
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.openWindow.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ // Reduce the timeout to expose issues when service worker
+ // freezing is broken
+ ["dom.serviceWorkers.idle_timeout", SW_TIMEOUT],
+ ["dom.serviceWorkers.idle_extended_timeout", SW_TIMEOUT],
+ ]};
+ SpecialPowers.pushPrefEnv(options, done);
+ });
+
+ let { tab, document } = yield openAboutDebugging("workers");
+
+ let swTab = yield addTab(TAB_URL);
+
+ let serviceWorkersElement = getServiceWorkerList(document);
+ yield waitForMutation(serviceWorkersElement, { childList: true });
+
+ assertHasTarget(true, document, "service-workers", SERVICE_WORKER);
+
+ // Ensure that the registration resolved before trying to connect to the sw
+ yield waitForServiceWorkerRegistered(swTab);
+ ok(true, "Service worker registration resolved");
+
+ // Retrieve the DEBUG button for the worker
+ let names = [...document.querySelectorAll("#service-workers .target-name")];
+ let name = names.filter(element => element.textContent === SERVICE_WORKER)[0];
+ ok(name, "Found the service worker in the list");
+ let targetElement = name.parentNode.parentNode;
+ let debugBtn = targetElement.querySelector(".debug-button");
+ ok(debugBtn, "Found its debug button");
+
+ // Click on it and wait for the toolbox to be ready
+ let onToolboxReady = new Promise(done => {
+ gDevTools.once("toolbox-ready", function (e, toolbox) {
+ done(toolbox);
+ });
+ });
+ debugBtn.click();
+
+ let toolbox = yield onToolboxReady;
+
+ // Wait for more than the regular timeout,
+ // so that if the worker freezing doesn't work,
+ // it will be destroyed and removed from the list
+ yield new Promise(done => {
+ setTimeout(done, SW_TIMEOUT * 2);
+ });
+
+ assertHasTarget(true, document, "service-workers", SERVICE_WORKER);
+ ok(targetElement.querySelector(".debug-button"),
+ "The debug button is still there");
+
+ yield toolbox.destroy();
+ toolbox = null;
+
+ // Now ensure that the worker is correctly destroyed
+ // after we destroy the toolbox.
+ // The DEBUG button should disappear once the worker is destroyed.
+ yield waitForMutation(targetElement, { childList: true });
+ ok(!targetElement.querySelector(".debug-button"),
+ "The debug button was removed when the worker was killed");
+
+ // Finally, unregister the service worker itself.
+ try {
+ yield unregisterServiceWorker(swTab, serviceWorkersElement);
+ ok(true, "Service worker registration unregistered");
+ } catch (e) {
+ ok(false, "SW not unregistered; " + e);
+ }
+
+ assertHasTarget(false, document, "service-workers", SERVICE_WORKER);
+
+ yield removeTab(swTab);
+ yield closeAboutDebugging(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser_service_workers_unregister.js b/devtools/client/aboutdebugging/test/browser_service_workers_unregister.js
new file mode 100644
index 000000000..b6076ea07
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser_service_workers_unregister.js
@@ -0,0 +1,77 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that clicking on the unregister link in the Service Worker details works
+// as intended in about:debugging.
+// It should unregister the service worker, which should trigger an update of
+// the displayed list of service workers.
+
+// Service workers can't be loaded from chrome://, but http:// is ok with
+// dom.serviceWorkers.testing.enabled turned on.
+const SCOPE = URL_ROOT + "service-workers/";
+const SERVICE_WORKER = SCOPE + "empty-sw.js";
+const TAB_URL = SCOPE + "empty-sw.html";
+
+add_task(function* () {
+ info("Turn on workers via mochitest http.");
+ yield new Promise(done => {
+ let options = { "set": [
+ // Accept workers from mochitest's http.
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.openWindow.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ["dom.ipc.processCount", 1],
+ ]};
+ SpecialPowers.pushPrefEnv(options, done);
+ });
+
+ let { tab, document } = yield openAboutDebugging("workers");
+
+ // Listen for mutations in the service-workers list.
+ let serviceWorkersElement = getServiceWorkerList(document);
+ let onMutation = waitForMutation(serviceWorkersElement, { childList: true });
+
+ // Open a tab that registers an empty service worker.
+ let swTab = yield addTab(TAB_URL);
+
+ // Wait for the service workers-list to update.
+ yield onMutation;
+
+ // Check that the service worker appears in the UI.
+ assertHasTarget(true, document, "service-workers", SERVICE_WORKER);
+
+ yield waitForServiceWorkerActivation(SERVICE_WORKER, document);
+
+ info("Ensure that the registration resolved before trying to interact with " +
+ "the service worker.");
+ yield waitForServiceWorkerRegistered(swTab);
+ ok(true, "Service worker registration resolved");
+
+ let targets = document.querySelectorAll("#service-workers .target");
+ is(targets.length, 1, "One service worker is now displayed.");
+
+ let target = targets[0];
+ let name = target.querySelector(".target-name");
+ is(name.textContent, SERVICE_WORKER, "Found the service worker in the list");
+
+ info("Check the scope displayed scope is correct");
+ let scope = target.querySelector(".service-worker-scope");
+ is(scope.textContent, SCOPE,
+ "The expected scope is displayed in the service worker info.");
+
+ info("Unregister the service worker via the unregister link.");
+ let unregisterLink = target.querySelector(".unregister-link");
+ ok(unregisterLink, "Found the unregister link");
+
+ onMutation = waitForMutation(serviceWorkersElement, { childList: true });
+ unregisterLink.click();
+ yield onMutation;
+
+ is(document.querySelector("#service-workers .target"), null,
+ "No service worker displayed anymore.");
+
+ yield removeTab(swTab);
+ yield closeAboutDebugging(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser_tabs.js b/devtools/client/aboutdebugging/test/browser_tabs.js
new file mode 100644
index 000000000..8cdeef17d
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser_tabs.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TAB_URL = "data:text/html,<title>foo</title>";
+
+add_task(function* setup() {
+ yield SpecialPowers.pushPrefEnv({
+ set: [["dom.ipc.processCount", 1]]
+ });
+});
+
+add_task(function* () {
+ let { tab, document } = yield openAboutDebugging("tabs");
+
+ // Wait for initial tabs list which may be empty
+ let tabsElement = getTabList(document);
+ if (tabsElement.querySelectorAll(".target-name").length == 0) {
+ yield waitForMutation(tabsElement, { childList: true });
+ }
+ // Refresh tabsElement to get the .target-list element
+ tabsElement = getTabList(document);
+
+ let names = [...tabsElement.querySelectorAll(".target-name")];
+ let initialTabCount = names.length;
+
+ // Open a new tab in background and wait for its addition in the UI
+ let onNewTab = waitForMutation(tabsElement, { childList: true });
+ let newTab = yield addTab(TAB_URL, { background: true });
+ yield onNewTab;
+
+ // Check that the new tab appears in the UI, but with an empty name
+ let newNames = [...tabsElement.querySelectorAll(".target-name")];
+ newNames = newNames.filter(node => !names.includes(node));
+ is(newNames.length, 1, "A new tab appeared in the list");
+ let newTabTarget = newNames[0];
+
+ // Then wait for title update, but on slow test runner, the title may already
+ // be set to the expected value
+ if (newTabTarget.textContent != "foo") {
+ yield waitForContentMutation(newTabTarget);
+ }
+
+ // Check that the new tab appears in the UI
+ is(newTabTarget.textContent, "foo", "The tab title got updated");
+ is(newTabTarget.title, TAB_URL, "The tab tooltip is the url");
+
+ // Finally, close the tab
+ let onTabsUpdate = waitForMutation(tabsElement, { childList: true });
+ yield removeTab(newTab);
+ yield onTabsUpdate;
+
+ // Check that the tab disappeared from the UI
+ names = [...tabsElement.querySelectorAll("#tabs .target-name")];
+ is(names.length, initialTabCount, "The tab disappeared from the UI");
+
+ yield closeAboutDebugging(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/head.js b/devtools/client/aboutdebugging/test/head.js
new file mode 100644
index 000000000..001d36e34
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/head.js
@@ -0,0 +1,367 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* eslint-env browser */
+/* exported openAboutDebugging, changeAboutDebuggingHash, closeAboutDebugging,
+ installAddon, uninstallAddon, waitForMutation, waitForContentMutation, assertHasTarget,
+ getServiceWorkerList, getTabList, openPanel, waitForInitialAddonList,
+ waitForServiceWorkerRegistered, unregisterServiceWorker,
+ waitForDelayedStartupFinished, setupTestAboutDebuggingWebExtension,
+ waitForServiceWorkerActivation */
+/* import-globals-from ../../framework/test/shared-head.js */
+
+"use strict";
+
+// Load the shared-head file first.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js",
+ this);
+
+const { AddonManager } = Cu.import("resource://gre/modules/AddonManager.jsm", {});
+const { Management } = Cu.import("resource://gre/modules/Extension.jsm", {});
+
+flags.testing = true;
+registerCleanupFunction(() => {
+ flags.testing = false;
+});
+
+function* openAboutDebugging(page, win) {
+ info("opening about:debugging");
+ let url = "about:debugging";
+ if (page) {
+ url += "#" + page;
+ }
+
+ let tab = yield addTab(url, { window: win });
+ let browser = tab.linkedBrowser;
+ let document = browser.contentDocument;
+
+ if (!document.querySelector(".app")) {
+ yield waitForMutation(document.body, { childList: true });
+ }
+
+ return { tab, document };
+}
+
+/**
+ * Change url hash for current about:debugging tab, return a promise after
+ * new content is loaded.
+ * @param {DOMDocument} document container document from current tab
+ * @param {String} hash hash for about:debugging
+ * @return {Promise}
+ */
+function changeAboutDebuggingHash(document, hash) {
+ info(`Opening about:debugging#${hash}`);
+ window.openUILinkIn(`about:debugging#${hash}`, "current");
+ return waitForMutation(
+ document.querySelector(".main-content"), {childList: true});
+}
+
+function openPanel(document, panelId) {
+ info(`Opening ${panelId} panel`);
+ document.querySelector(`[aria-controls="${panelId}"]`).click();
+ return waitForMutation(
+ document.querySelector(".main-content"), {childList: true});
+}
+
+function closeAboutDebugging(tab) {
+ info("Closing about:debugging");
+ return removeTab(tab);
+}
+
+function getSupportsFile(path) {
+ let cr = Cc["@mozilla.org/chrome/chrome-registry;1"]
+ .getService(Ci.nsIChromeRegistry);
+ let uri = Services.io.newURI(CHROME_URL_ROOT + path, null, null);
+ let fileurl = cr.convertChromeURL(uri);
+ return fileurl.QueryInterface(Ci.nsIFileURL);
+}
+
+/**
+ * Depending on whether there are addons installed, return either a target list
+ * element or its container.
+ * @param {DOMDocument} document #addons section container document
+ * @return {DOMNode} target list or container element
+ */
+function getAddonList(document) {
+ return document.querySelector("#addons .target-list") ||
+ document.querySelector("#addons .targets");
+}
+
+/**
+ * Depending on whether there are service workers installed, return either a
+ * target list element or its container.
+ * @param {DOMDocument} document #service-workers section container document
+ * @return {DOMNode} target list or container element
+ */
+function getServiceWorkerList(document) {
+ return document.querySelector("#service-workers .target-list") ||
+ document.querySelector("#service-workers.targets");
+}
+
+/**
+ * Depending on whether there are tabs opened, return either a
+ * target list element or its container.
+ * @param {DOMDocument} document #tabs section container document
+ * @return {DOMNode} target list or container element
+ */
+function getTabList(document) {
+ return document.querySelector("#tabs .target-list") ||
+ document.querySelector("#tabs.targets");
+}
+
+function* installAddon({document, path, name, isWebExtension}) {
+ // Mock the file picker to select a test addon
+ let MockFilePicker = SpecialPowers.MockFilePicker;
+ MockFilePicker.init(null);
+ let file = getSupportsFile(path);
+ MockFilePicker.returnFiles = [file.file];
+
+ let addonList = getAddonList(document);
+ let addonListMutation = waitForMutation(addonList, { childList: true });
+
+ let onAddonInstalled;
+
+ if (isWebExtension) {
+ onAddonInstalled = new Promise(done => {
+ Management.on("startup", function listener(event, extension) {
+ if (extension.name != name) {
+ return;
+ }
+
+ Management.off("startup", listener);
+ done();
+ });
+ });
+ } else {
+ // Wait for a "test-devtools" message sent by the addon's bootstrap.js file
+ onAddonInstalled = new Promise(done => {
+ Services.obs.addObserver(function listener() {
+ Services.obs.removeObserver(listener, "test-devtools");
+
+ done();
+ }, "test-devtools", false);
+ });
+ }
+ // Trigger the file picker by clicking on the button
+ document.getElementById("load-addon-from-file").click();
+
+ yield onAddonInstalled;
+ ok(true, "Addon installed and running its bootstrap.js file");
+
+ // Check that the addon appears in the UI
+ yield addonListMutation;
+ let names = [...addonList.querySelectorAll(".target-name")];
+ names = names.map(element => element.textContent);
+ ok(names.includes(name),
+ "The addon name appears in the list of addons: " + names);
+}
+
+function* uninstallAddon({document, id, name}) {
+ let addonList = getAddonList(document);
+ let addonListMutation = waitForMutation(addonList, { childList: true });
+
+ // Now uninstall this addon
+ yield new Promise(done => {
+ AddonManager.getAddonByID(id, addon => {
+ let listener = {
+ onUninstalled: function (uninstalledAddon) {
+ if (uninstalledAddon != addon) {
+ return;
+ }
+ AddonManager.removeAddonListener(listener);
+
+ done();
+ }
+ };
+ AddonManager.addAddonListener(listener);
+ addon.uninstall();
+ });
+ });
+
+ // Ensure that the UI removes the addon from the list
+ yield addonListMutation;
+ let names = [...addonList.querySelectorAll(".target-name")];
+ names = names.map(element => element.textContent);
+ ok(!names.includes(name),
+ "After uninstall, the addon name disappears from the list of addons: "
+ + names);
+}
+
+/**
+ * Returns a promise that will resolve when the add-on list has been updated.
+ *
+ * @param {Node} document
+ * @return {Promise}
+ */
+function waitForInitialAddonList(document) {
+ const addonListContainer = getAddonList(document);
+ let addonCount = addonListContainer.querySelectorAll(".target");
+ addonCount = addonCount ? [...addonCount].length : -1;
+ info("Waiting for add-ons to load. Current add-on count: " + addonCount);
+
+ // This relies on the network speed of the actor responding to the
+ // listAddons() request and also the speed of openAboutDebugging().
+ let result;
+ if (addonCount > 0) {
+ info("Actually, the add-ons have already loaded");
+ result = Promise.resolve();
+ } else {
+ result = waitForMutation(addonListContainer, { childList: true });
+ }
+ return result;
+}
+
+/**
+ * Returns a promise that will resolve after receiving a mutation matching the
+ * provided mutation options on the provided target.
+ * @param {Node} target
+ * @param {Object} mutationOptions
+ * @return {Promise}
+ */
+function waitForMutation(target, mutationOptions) {
+ return new Promise(resolve => {
+ let observer = new MutationObserver(() => {
+ observer.disconnect();
+ resolve();
+ });
+ observer.observe(target, mutationOptions);
+ });
+}
+
+/**
+ * Returns a promise that will resolve after receiving a mutation in the subtree of the
+ * provided target. Depending on the current React implementation, a text change might be
+ * observable as a childList mutation or a characterData mutation.
+ *
+ * @param {Node} target
+ * @return {Promise}
+ */
+function waitForContentMutation(target) {
+ return waitForMutation(target, {
+ characterData: true,
+ childList: true,
+ subtree: true
+ });
+}
+
+/**
+ * Checks if an about:debugging TargetList element contains a Target element
+ * corresponding to the specified name.
+ * @param {Boolean} expected
+ * @param {Document} document
+ * @param {String} type
+ * @param {String} name
+ */
+function assertHasTarget(expected, document, type, name) {
+ let names = [...document.querySelectorAll("#" + type + " .target-name")];
+ names = names.map(element => element.textContent);
+ is(names.includes(name), expected,
+ "The " + type + " url appears in the list: " + names);
+}
+
+/**
+ * Returns a promise that will resolve after the service worker in the page
+ * has successfully registered itself.
+ * @param {Tab} tab
+ * @return {Promise} Resolves when the service worker is registered.
+ */
+function waitForServiceWorkerRegistered(tab) {
+ return ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ // Retrieve the `sw` promise created in the html page.
+ let { sw } = content.wrappedJSObject;
+ yield sw;
+ });
+}
+
+/**
+ * Asks the service worker within the test page to unregister, and returns a
+ * promise that will resolve when it has successfully unregistered itself and the
+ * about:debugging UI has fully processed this update.
+ *
+ * @param {Tab} tab
+ * @param {Node} serviceWorkersElement
+ * @return {Promise} Resolves when the service worker is unregistered.
+ */
+function* unregisterServiceWorker(tab, serviceWorkersElement) {
+ let onMutation = waitForMutation(serviceWorkersElement, { childList: true });
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ // Retrieve the `sw` promise created in the html page
+ let { sw } = content.wrappedJSObject;
+ let registration = yield sw;
+ yield registration.unregister();
+ });
+ return onMutation;
+}
+
+/**
+ * Waits for the creation of a new window, usually used with create private
+ * browsing window.
+ * Returns a promise that will resolve when the window is successfully created.
+ * @param {window} win
+ */
+function waitForDelayedStartupFinished(win) {
+ return new Promise(function (resolve) {
+ Services.obs.addObserver(function observer(subject, topic) {
+ if (win == subject) {
+ Services.obs.removeObserver(observer, topic);
+ resolve();
+ }
+ }, "browser-delayed-startup-finished", false);
+ });
+}
+
+/**
+ * open the about:debugging page and install an addon
+ */
+function* setupTestAboutDebuggingWebExtension(name, path) {
+ yield new Promise(resolve => {
+ let options = {"set": [
+ // Force enabling of addons debugging
+ ["devtools.chrome.enabled", true],
+ ["devtools.debugger.remote-enabled", true],
+ // Disable security prompt
+ ["devtools.debugger.prompt-connection", false],
+ // Enable Browser toolbox test script execution via env variable
+ ["devtools.browser-toolbox.allow-unsafe-script", true],
+ ]};
+ SpecialPowers.pushPrefEnv(options, resolve);
+ });
+
+ let { tab, document } = yield openAboutDebugging("addons");
+ yield waitForInitialAddonList(document);
+
+ yield installAddon({
+ document,
+ path,
+ name,
+ isWebExtension: true,
+ });
+
+ // Retrieve the DEBUG button for the addon
+ let names = [...document.querySelectorAll("#addons .target-name")];
+ let nameEl = names.filter(element => element.textContent === name)[0];
+ ok(name, "Found the addon in the list");
+ let targetElement = nameEl.parentNode.parentNode;
+ let debugBtn = targetElement.querySelector(".debug-button");
+ ok(debugBtn, "Found its debug button");
+
+ return { tab, document, debugBtn };
+}
+
+/**
+ * Wait for aboutdebugging to be notified about the activation of the service worker
+ * corresponding to the provided service worker url.
+ */
+function* waitForServiceWorkerActivation(swUrl, document) {
+ let serviceWorkersElement = getServiceWorkerList(document);
+ let names = serviceWorkersElement.querySelectorAll(".target-name");
+ let name = [...names].filter(element => element.textContent === swUrl)[0];
+
+ let targetElement = name.parentNode.parentNode;
+ let targetStatus = targetElement.querySelector(".target-status");
+ while (targetStatus.textContent === "Registering") {
+ // Wait for the status to leave the "registering" stage.
+ yield waitForMutation(serviceWorkersElement, { childList: true, subtree: true });
+ }
+}
diff --git a/devtools/client/aboutdebugging/test/service-workers/delay-sw.html b/devtools/client/aboutdebugging/test/service-workers/delay-sw.html
new file mode 100644
index 000000000..545830eba
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/service-workers/delay-sw.html
@@ -0,0 +1,22 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>Service worker test</title>
+</head>
+<body>
+<script type="text/javascript">
+"use strict";
+
+var sw = navigator.serviceWorker.register("delay-sw.js");
+sw.then(
+ function () {
+ dump("SW registered\n");
+ },
+ function (e) {
+ dump("SW not registered: " + e + "\n");
+ }
+);
+</script>
+</body>
+</html>
diff --git a/devtools/client/aboutdebugging/test/service-workers/delay-sw.js b/devtools/client/aboutdebugging/test/service-workers/delay-sw.js
new file mode 100644
index 000000000..3f16c5058
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/service-workers/delay-sw.js
@@ -0,0 +1,17 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* eslint-env worker */
+
+"use strict";
+
+function wait(ms) {
+ return new Promise(resolve => {
+ setTimeout(resolve, ms);
+ });
+}
+
+// Wait for one second to switch from installing to installed.
+self.addEventListener("install", function (event) {
+ event.waitUntil(wait(1000));
+});
diff --git a/devtools/client/aboutdebugging/test/service-workers/empty-sw.html b/devtools/client/aboutdebugging/test/service-workers/empty-sw.html
new file mode 100644
index 000000000..a94c2b9ff
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/service-workers/empty-sw.html
@@ -0,0 +1,22 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>Service worker test</title>
+</head>
+<body>
+<script type="text/javascript">
+"use strict";
+
+var sw = navigator.serviceWorker.register("empty-sw.js");
+sw.then(
+ function () {
+ dump("SW registered\n");
+ },
+ function (e) {
+ dump("SW not registered: " + e + "\n");
+ }
+);
+</script>
+</body>
+</html>
diff --git a/devtools/client/aboutdebugging/test/service-workers/empty-sw.js b/devtools/client/aboutdebugging/test/service-workers/empty-sw.js
new file mode 100644
index 000000000..1e7226402
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/service-workers/empty-sw.js
@@ -0,0 +1 @@
+// Empty, just test registering.
diff --git a/devtools/client/aboutdebugging/test/service-workers/push-sw.html b/devtools/client/aboutdebugging/test/service-workers/push-sw.html
new file mode 100644
index 000000000..7db01f091
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/service-workers/push-sw.html
@@ -0,0 +1,32 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>Service worker push test</title>
+</head>
+<body>
+<script type="text/javascript">
+"use strict";
+SpecialPowers.addPermission("desktop-notification", true, document);
+var sw = navigator.serviceWorker.register("push-sw.js");
+var sub = null;
+sw.then(
+ function (registration) {
+ dump("SW registered\n");
+ registration.pushManager.subscribe().then(
+ function (subscription) {
+ sub = subscription;
+ dump("SW subscribed to push: " + sub.endpoint + "\n");
+ },
+ function (error) {
+ dump("SW not subscribed to push: " + error + "\n");
+ }
+ );
+ },
+ function (error) {
+ dump("SW not registered: " + error + "\n");
+ }
+);
+</script>
+</body>
+</html>
diff --git a/devtools/client/aboutdebugging/test/service-workers/push-sw.js b/devtools/client/aboutdebugging/test/service-workers/push-sw.js
new file mode 100644
index 000000000..b5006eedb
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/service-workers/push-sw.js
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* eslint-env worker */
+/* global clients */
+
+"use strict";
+
+// Send a message to all controlled windows.
+function postMessage(message) {
+ return clients.matchAll().then(function (clientlist) {
+ clientlist.forEach(function (client) {
+ client.postMessage(message);
+ });
+ });
+}
+
+// Don't wait for the next page load to become the active service worker.
+self.addEventListener("install", function (event) {
+ event.waitUntil(self.skipWaiting());
+});
+
+// Claim control over the currently open test page when activating.
+self.addEventListener("activate", function (event) {
+ event.waitUntil(self.clients.claim().then(function () {
+ return postMessage("sw-claimed");
+ }));
+});
+
+// Forward all "push" events to the controlled window.
+self.addEventListener("push", function (event) {
+ event.waitUntil(postMessage("sw-pushed"));
+});
diff --git a/devtools/client/animationinspector/animation-controller.js b/devtools/client/animationinspector/animation-controller.js
new file mode 100644
index 000000000..03c6e0e95
--- /dev/null
+++ b/devtools/client/animationinspector/animation-controller.js
@@ -0,0 +1,390 @@
+/* -*- 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/. */
+
+/* animation-panel.js is loaded in the same scope but we don't use
+ import-globals-from to avoid infinite loops since animation-panel.js already
+ imports globals from animation-controller.js */
+/* globals AnimationsPanel */
+/* eslint no-unused-vars: [2, {"vars": "local", "args": "none"}] */
+
+"use strict";
+
+var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+var { loader, require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+var { Task } = require("devtools/shared/task");
+
+loader.lazyRequireGetter(this, "promise");
+loader.lazyRequireGetter(this, "EventEmitter", "devtools/shared/event-emitter");
+loader.lazyRequireGetter(this, "AnimationsFront", "devtools/shared/fronts/animation", true);
+
+const { LocalizationHelper } = require("devtools/shared/l10n");
+const L10N =
+ new LocalizationHelper("devtools/client/locales/animationinspector.properties");
+
+// Global toolbox/inspector, set when startup is called.
+var gToolbox, gInspector;
+
+/**
+ * Startup the animationinspector controller and view, called by the sidebar
+ * widget when loading/unloading the iframe into the tab.
+ */
+var startup = Task.async(function* (inspector) {
+ gInspector = inspector;
+ gToolbox = inspector.toolbox;
+
+ // Don't assume that AnimationsPanel is defined here, it's in another file.
+ if (!typeof AnimationsPanel === "undefined") {
+ throw new Error("AnimationsPanel was not loaded in the " +
+ "animationinspector window");
+ }
+
+ // Startup first initalizes the controller and then the panel, in sequence.
+ // If you want to know when everything's ready, do:
+ // AnimationsPanel.once(AnimationsPanel.PANEL_INITIALIZED)
+ yield AnimationsController.initialize();
+ yield AnimationsPanel.initialize();
+});
+
+/**
+ * Shutdown the animationinspector controller and view, called by the sidebar
+ * widget when loading/unloading the iframe into the tab.
+ */
+var shutdown = Task.async(function* () {
+ yield AnimationsController.destroy();
+ // Don't assume that AnimationsPanel is defined here, it's in another file.
+ if (typeof AnimationsPanel !== "undefined") {
+ yield AnimationsPanel.destroy();
+ }
+ gToolbox = gInspector = null;
+});
+
+// This is what makes the sidebar widget able to load/unload the panel.
+function setPanel(panel) {
+ return startup(panel).catch(e => console.error(e));
+}
+function destroy() {
+ return shutdown().catch(e => console.error(e));
+}
+
+/**
+ * Get all the server-side capabilities (traits) so the UI knows whether or not
+ * features should be enabled/disabled.
+ * @param {Target} target The current toolbox target.
+ * @return {Object} An object with boolean properties.
+ */
+var getServerTraits = Task.async(function* (target) {
+ let config = [
+ { name: "hasToggleAll", actor: "animations",
+ method: "toggleAll" },
+ { name: "hasToggleSeveral", actor: "animations",
+ method: "toggleSeveral" },
+ { name: "hasSetCurrentTime", actor: "animationplayer",
+ method: "setCurrentTime" },
+ { name: "hasMutationEvents", actor: "animations",
+ method: "stopAnimationPlayerUpdates" },
+ { name: "hasSetPlaybackRate", actor: "animationplayer",
+ method: "setPlaybackRate" },
+ { name: "hasSetPlaybackRates", actor: "animations",
+ method: "setPlaybackRates" },
+ { name: "hasTargetNode", actor: "domwalker",
+ method: "getNodeFromActor" },
+ { name: "hasSetCurrentTimes", actor: "animations",
+ method: "setCurrentTimes" },
+ { name: "hasGetFrames", actor: "animationplayer",
+ method: "getFrames" },
+ { name: "hasGetProperties", actor: "animationplayer",
+ method: "getProperties" },
+ { name: "hasSetWalkerActor", actor: "animations",
+ method: "setWalkerActor" },
+ ];
+
+ let traits = {};
+ for (let {name, actor, method} of config) {
+ traits[name] = yield target.actorHasMethod(actor, method);
+ }
+
+ return traits;
+});
+
+/**
+ * The animationinspector controller's job is to retrieve AnimationPlayerFronts
+ * from the server. It is also responsible for keeping the list of players up to
+ * date when the node selection changes in the inspector, as well as making sure
+ * no updates are done when the animationinspector sidebar panel is not visible.
+ *
+ * AnimationPlayerFronts are available in AnimationsController.animationPlayers.
+ *
+ * Usage example:
+ *
+ * AnimationsController.on(AnimationsController.PLAYERS_UPDATED_EVENT,
+ * onPlayers);
+ * function onPlayers() {
+ * for (let player of AnimationsController.animationPlayers) {
+ * // do something with player
+ * }
+ * }
+ */
+var AnimationsController = {
+ PLAYERS_UPDATED_EVENT: "players-updated",
+ ALL_ANIMATIONS_TOGGLED_EVENT: "all-animations-toggled",
+
+ initialize: Task.async(function* () {
+ if (this.initialized) {
+ yield this.initialized;
+ return;
+ }
+
+ let resolver;
+ this.initialized = new Promise(resolve => {
+ resolver = resolve;
+ });
+
+ this.onPanelVisibilityChange = this.onPanelVisibilityChange.bind(this);
+ this.onNewNodeFront = this.onNewNodeFront.bind(this);
+ this.onAnimationMutations = this.onAnimationMutations.bind(this);
+
+ let target = gInspector.target;
+ this.animationsFront = new AnimationsFront(target.client, target.form);
+
+ // Expose actor capabilities.
+ this.traits = yield getServerTraits(target);
+
+ if (this.destroyed) {
+ console.warn("Could not fully initialize the AnimationsController");
+ return;
+ }
+
+ // Let the AnimationsActor know what WalkerActor we're using. This will
+ // come in handy later to return references to DOM Nodes.
+ if (this.traits.hasSetWalkerActor) {
+ yield this.animationsFront.setWalkerActor(gInspector.walker);
+ }
+
+ this.startListeners();
+ yield this.onNewNodeFront();
+
+ resolver();
+ }),
+
+ destroy: Task.async(function* () {
+ if (!this.initialized) {
+ return;
+ }
+
+ if (this.destroyed) {
+ yield this.destroyed;
+ return;
+ }
+
+ let resolver;
+ this.destroyed = new Promise(resolve => {
+ resolver = resolve;
+ });
+
+ this.stopListeners();
+ this.destroyAnimationPlayers();
+ this.nodeFront = null;
+
+ if (this.animationsFront) {
+ this.animationsFront.destroy();
+ this.animationsFront = null;
+ }
+ resolver();
+ }),
+
+ startListeners: function () {
+ // Re-create the list of players when a new node is selected, except if the
+ // sidebar isn't visible.
+ gInspector.selection.on("new-node-front", this.onNewNodeFront);
+ gInspector.sidebar.on("select", this.onPanelVisibilityChange);
+ gToolbox.on("select", this.onPanelVisibilityChange);
+ },
+
+ stopListeners: function () {
+ gInspector.selection.off("new-node-front", this.onNewNodeFront);
+ gInspector.sidebar.off("select", this.onPanelVisibilityChange);
+ gToolbox.off("select", this.onPanelVisibilityChange);
+ if (this.isListeningToMutations) {
+ this.animationsFront.off("mutations", this.onAnimationMutations);
+ }
+ },
+
+ isPanelVisible: function () {
+ return gToolbox.currentToolId === "inspector" &&
+ gInspector.sidebar &&
+ gInspector.sidebar.getCurrentTabID() == "animationinspector";
+ },
+
+ onPanelVisibilityChange: Task.async(function* () {
+ if (this.isPanelVisible()) {
+ this.onNewNodeFront();
+ }
+ }),
+
+ onNewNodeFront: Task.async(function* () {
+ // Ignore if the panel isn't visible or the node selection hasn't changed.
+ if (!this.isPanelVisible() ||
+ this.nodeFront === gInspector.selection.nodeFront) {
+ return;
+ }
+
+ this.nodeFront = gInspector.selection.nodeFront;
+ let done = gInspector.updating("animationscontroller");
+
+ if (!gInspector.selection.isConnected() ||
+ !gInspector.selection.isElementNode()) {
+ this.destroyAnimationPlayers();
+ this.emit(this.PLAYERS_UPDATED_EVENT);
+ done();
+ return;
+ }
+
+ yield this.refreshAnimationPlayers(this.nodeFront);
+ this.emit(this.PLAYERS_UPDATED_EVENT, this.animationPlayers);
+
+ done();
+ }),
+
+ /**
+ * Toggle (pause/play) all animations in the current target.
+ */
+ toggleAll: function () {
+ if (!this.traits.hasToggleAll) {
+ return promise.resolve();
+ }
+
+ return this.animationsFront.toggleAll()
+ .then(() => this.emit(this.ALL_ANIMATIONS_TOGGLED_EVENT, this))
+ .catch(e => console.error(e));
+ },
+
+ /**
+ * Similar to toggleAll except that it only plays/pauses the currently known
+ * animations (those listed in this.animationPlayers).
+ * @param {Boolean} shouldPause True if the animations should be paused, false
+ * if they should be played.
+ * @return {Promise} Resolves when the playState has been changed.
+ */
+ toggleCurrentAnimations: Task.async(function* (shouldPause) {
+ if (this.traits.hasToggleSeveral) {
+ yield this.animationsFront.toggleSeveral(this.animationPlayers,
+ shouldPause);
+ } else {
+ // Fall back to pausing/playing the players one by one, which is bound to
+ // introduce some de-synchronization.
+ for (let player of this.animationPlayers) {
+ if (shouldPause) {
+ yield player.pause();
+ } else {
+ yield player.play();
+ }
+ }
+ }
+ }),
+
+ /**
+ * Set all known animations' currentTimes to the provided time.
+ * @param {Number} time.
+ * @param {Boolean} shouldPause Should the animations be paused too.
+ * @return {Promise} Resolves when the current time has been set.
+ */
+ setCurrentTimeAll: Task.async(function* (time, shouldPause) {
+ if (this.traits.hasSetCurrentTimes) {
+ yield this.animationsFront.setCurrentTimes(this.animationPlayers, time,
+ shouldPause);
+ } else {
+ // Fall back to pausing and setting the current time on each player, one
+ // by one, which is bound to introduce some de-synchronization.
+ for (let animation of this.animationPlayers) {
+ if (shouldPause) {
+ yield animation.pause();
+ }
+ yield animation.setCurrentTime(time);
+ }
+ }
+ }),
+
+ /**
+ * Set all known animations' playback rates to the provided rate.
+ * @param {Number} rate.
+ * @return {Promise} Resolves when the rate has been set.
+ */
+ setPlaybackRateAll: Task.async(function* (rate) {
+ if (this.traits.hasSetPlaybackRates) {
+ // If the backend can set all playback rates at the same time, use that.
+ yield this.animationsFront.setPlaybackRates(this.animationPlayers, rate);
+ } else if (this.traits.hasSetPlaybackRate) {
+ // Otherwise, fall back to setting each rate individually.
+ for (let animation of this.animationPlayers) {
+ yield animation.setPlaybackRate(rate);
+ }
+ }
+ }),
+
+ // AnimationPlayerFront objects are managed by this controller. They are
+ // retrieved when refreshAnimationPlayers is called, stored in the
+ // animationPlayers array, and destroyed when refreshAnimationPlayers is
+ // called again.
+ animationPlayers: [],
+
+ refreshAnimationPlayers: Task.async(function* (nodeFront) {
+ this.destroyAnimationPlayers();
+
+ this.animationPlayers = yield this.animationsFront
+ .getAnimationPlayersForNode(nodeFront);
+
+ // Start listening for animation mutations only after the first method call
+ // otherwise events won't be sent.
+ if (!this.isListeningToMutations && this.traits.hasMutationEvents) {
+ this.animationsFront.on("mutations", this.onAnimationMutations);
+ this.isListeningToMutations = true;
+ }
+ }),
+
+ onAnimationMutations: function (changes) {
+ // Insert new players into this.animationPlayers when new animations are
+ // added.
+ for (let {type, player} of changes) {
+ if (type === "added") {
+ this.animationPlayers.push(player);
+ }
+
+ if (type === "removed") {
+ let index = this.animationPlayers.indexOf(player);
+ this.animationPlayers.splice(index, 1);
+ }
+ }
+
+ // Let the UI know the list has been updated.
+ this.emit(this.PLAYERS_UPDATED_EVENT, this.animationPlayers);
+ },
+
+ /**
+ * Get the latest known current time of document.timeline.
+ * This value is sent along with all AnimationPlayerActors' states, but it
+ * isn't updated after that, so this function loops over all know animations
+ * to find the highest value.
+ * @return {Number|Boolean} False is returned if this server version doesn't
+ * provide document's current time.
+ */
+ get documentCurrentTime() {
+ let time = 0;
+ for (let {state} of this.animationPlayers) {
+ if (!state.documentCurrentTime) {
+ return false;
+ }
+ time = Math.max(time, state.documentCurrentTime);
+ }
+ return time;
+ },
+
+ destroyAnimationPlayers: function () {
+ this.animationPlayers = [];
+ }
+};
+
+EventEmitter.decorate(AnimationsController);
diff --git a/devtools/client/animationinspector/animation-inspector.xhtml b/devtools/client/animationinspector/animation-inspector.xhtml
new file mode 100644
index 000000000..26115be31
--- /dev/null
+++ b/devtools/client/animationinspector/animation-inspector.xhtml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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/. -->
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <link rel="stylesheet" href="chrome://devtools/skin/animationinspector.css" type="text/css"/>
+ <script type="application/javascript;version=1.8" src="chrome://devtools/content/shared/theme-switching.js"/>
+ </head>
+ <body class="theme-sidebar devtools-monospace" role="application" empty="true">
+ <div id="global-toolbar" class="theme-toolbar">
+ <span id="all-animations-label" class="label"></span>
+ <button id="toggle-all" standalone="true" class="devtools-button pause-button"></button>
+ </div>
+ <div id="timeline-toolbar" class="theme-toolbar">
+ <button id="rewind-timeline" standalone="true" class="devtools-button"></button>
+ <button id="pause-resume-timeline" standalone="true" class="devtools-button pause-button paused"></button>
+ <span id="timeline-rate" standalone="true" class="devtools-button"></span>
+ <span id="timeline-current-time" class="label"></span>
+ </div>
+ <div id="players"></div>
+ <div id="error-message">
+ <p id="error-type"></p>
+ <p id="error-hint"></p>
+ <button id="element-picker" standalone="true" class="devtools-button"></button>
+ </div>
+ <script type="application/javascript;version=1.8" src="animation-controller.js"></script>
+ <script type="application/javascript;version=1.8" src="animation-panel.js"></script>
+ </body>
+</html>
diff --git a/devtools/client/animationinspector/animation-panel.js b/devtools/client/animationinspector/animation-panel.js
new file mode 100644
index 000000000..25fd84b87
--- /dev/null
+++ b/devtools/client/animationinspector/animation-panel.js
@@ -0,0 +1,347 @@
+/* -*- 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/. */
+
+/* import-globals-from animation-controller.js */
+/* globals document */
+
+"use strict";
+
+const {AnimationsTimeline} = require("devtools/client/animationinspector/components/animation-timeline");
+const {RateSelector} = require("devtools/client/animationinspector/components/rate-selector");
+const {formatStopwatchTime} = require("devtools/client/animationinspector/utils");
+const {KeyCodes} = require("devtools/client/shared/keycodes");
+
+var $ = (selector, target = document) => target.querySelector(selector);
+
+/**
+ * The main animations panel UI.
+ */
+var AnimationsPanel = {
+ UI_UPDATED_EVENT: "ui-updated",
+ PANEL_INITIALIZED: "panel-initialized",
+
+ initialize: Task.async(function* () {
+ if (AnimationsController.destroyed) {
+ console.warn("Could not initialize the animation-panel, controller " +
+ "was destroyed");
+ return;
+ }
+ if (this.initialized) {
+ yield this.initialized;
+ return;
+ }
+
+ let resolver;
+ this.initialized = new Promise(resolve => {
+ resolver = resolve;
+ });
+
+ this.playersEl = $("#players");
+ this.errorMessageEl = $("#error-message");
+ this.pickerButtonEl = $("#element-picker");
+ this.toggleAllButtonEl = $("#toggle-all");
+ this.playTimelineButtonEl = $("#pause-resume-timeline");
+ this.rewindTimelineButtonEl = $("#rewind-timeline");
+ this.timelineCurrentTimeEl = $("#timeline-current-time");
+ this.rateSelectorEl = $("#timeline-rate");
+
+ this.rewindTimelineButtonEl.setAttribute("title",
+ L10N.getStr("timeline.rewindButtonTooltip"));
+
+ $("#all-animations-label").textContent = L10N.getStr("panel.allAnimations");
+
+ // If the server doesn't support toggling all animations at once, hide the
+ // whole global toolbar.
+ if (!AnimationsController.traits.hasToggleAll) {
+ $("#global-toolbar").style.display = "none";
+ }
+
+ // Binding functions that need to be called in scope.
+ for (let functionName of ["onKeyDown", "onPickerStarted",
+ "onPickerStopped", "refreshAnimationsUI", "onToggleAllClicked",
+ "onTabNavigated", "onTimelineDataChanged", "onTimelinePlayClicked",
+ "onTimelineRewindClicked", "onRateChanged"]) {
+ this[functionName] = this[functionName].bind(this);
+ }
+ let hUtils = gToolbox.highlighterUtils;
+ this.togglePicker = hUtils.togglePicker.bind(hUtils);
+
+ this.animationsTimelineComponent = new AnimationsTimeline(gInspector,
+ AnimationsController.traits);
+ this.animationsTimelineComponent.init(this.playersEl);
+
+ if (AnimationsController.traits.hasSetPlaybackRate) {
+ this.rateSelectorComponent = new RateSelector();
+ this.rateSelectorComponent.init(this.rateSelectorEl);
+ }
+
+ this.startListeners();
+
+ yield this.refreshAnimationsUI();
+
+ resolver();
+ this.emit(this.PANEL_INITIALIZED);
+ }),
+
+ destroy: Task.async(function* () {
+ if (!this.initialized) {
+ return;
+ }
+
+ if (this.destroyed) {
+ yield this.destroyed;
+ return;
+ }
+
+ let resolver;
+ this.destroyed = new Promise(resolve => {
+ resolver = resolve;
+ });
+
+ this.stopListeners();
+
+ this.animationsTimelineComponent.destroy();
+ this.animationsTimelineComponent = null;
+
+ if (this.rateSelectorComponent) {
+ this.rateSelectorComponent.destroy();
+ this.rateSelectorComponent = null;
+ }
+
+ this.playersEl = this.errorMessageEl = null;
+ this.toggleAllButtonEl = this.pickerButtonEl = null;
+ this.playTimelineButtonEl = this.rewindTimelineButtonEl = null;
+ this.timelineCurrentTimeEl = this.rateSelectorEl = null;
+
+ resolver();
+ }),
+
+ startListeners: function () {
+ AnimationsController.on(AnimationsController.PLAYERS_UPDATED_EVENT,
+ this.refreshAnimationsUI);
+
+ this.pickerButtonEl.addEventListener("click", this.togglePicker);
+ gToolbox.on("picker-started", this.onPickerStarted);
+ gToolbox.on("picker-stopped", this.onPickerStopped);
+
+ this.toggleAllButtonEl.addEventListener("click", this.onToggleAllClicked);
+ this.playTimelineButtonEl.addEventListener(
+ "click", this.onTimelinePlayClicked);
+ this.rewindTimelineButtonEl.addEventListener(
+ "click", this.onTimelineRewindClicked);
+
+ document.addEventListener("keydown", this.onKeyDown, false);
+
+ gToolbox.target.on("navigate", this.onTabNavigated);
+
+ this.animationsTimelineComponent.on("timeline-data-changed",
+ this.onTimelineDataChanged);
+
+ if (this.rateSelectorComponent) {
+ this.rateSelectorComponent.on("rate-changed", this.onRateChanged);
+ }
+ },
+
+ stopListeners: function () {
+ AnimationsController.off(AnimationsController.PLAYERS_UPDATED_EVENT,
+ this.refreshAnimationsUI);
+
+ this.pickerButtonEl.removeEventListener("click", this.togglePicker);
+ gToolbox.off("picker-started", this.onPickerStarted);
+ gToolbox.off("picker-stopped", this.onPickerStopped);
+
+ this.toggleAllButtonEl.removeEventListener("click",
+ this.onToggleAllClicked);
+ this.playTimelineButtonEl.removeEventListener("click",
+ this.onTimelinePlayClicked);
+ this.rewindTimelineButtonEl.removeEventListener("click",
+ this.onTimelineRewindClicked);
+
+ document.removeEventListener("keydown", this.onKeyDown, false);
+
+ gToolbox.target.off("navigate", this.onTabNavigated);
+
+ this.animationsTimelineComponent.off("timeline-data-changed",
+ this.onTimelineDataChanged);
+
+ if (this.rateSelectorComponent) {
+ this.rateSelectorComponent.off("rate-changed", this.onRateChanged);
+ }
+ },
+
+ onKeyDown: function (event) {
+ // If the space key is pressed, it should toggle the play state of
+ // the animations displayed in the panel, or of all the animations on
+ // the page if the selected node does not have any animation on it.
+ if (event.keyCode === KeyCodes.DOM_VK_SPACE) {
+ if (AnimationsController.animationPlayers.length > 0) {
+ this.playPauseTimeline().catch(ex => console.error(ex));
+ } else {
+ this.toggleAll().catch(ex => console.error(ex));
+ }
+ event.preventDefault();
+ }
+ },
+
+ togglePlayers: function (isVisible) {
+ if (isVisible) {
+ document.body.removeAttribute("empty");
+ document.body.setAttribute("timeline", "true");
+ } else {
+ document.body.setAttribute("empty", "true");
+ document.body.removeAttribute("timeline");
+ $("#error-type").textContent = L10N.getStr("panel.invalidElementSelected");
+ $("#error-hint").textContent = L10N.getStr("panel.selectElement");
+ }
+ },
+
+ onPickerStarted: function () {
+ this.pickerButtonEl.setAttribute("checked", "true");
+ },
+
+ onPickerStopped: function () {
+ this.pickerButtonEl.removeAttribute("checked");
+ },
+
+ onToggleAllClicked: function () {
+ this.toggleAll().catch(ex => console.error(ex));
+ },
+
+ /**
+ * Toggle (pause/play) all animations in the current target
+ * and update the UI the toggleAll button.
+ */
+ toggleAll: Task.async(function* () {
+ this.toggleAllButtonEl.classList.toggle("paused");
+ yield AnimationsController.toggleAll();
+ }),
+
+ onTimelinePlayClicked: function () {
+ this.playPauseTimeline().catch(ex => console.error(ex));
+ },
+
+ /**
+ * Depending on the state of the timeline either pause or play the animations
+ * displayed in it.
+ * If the animations are finished, this will play them from the start again.
+ * If the animations are playing, this will pause them.
+ * If the animations are paused, this will resume them.
+ *
+ * @return {Promise} Resolves when the playState is changed and the UI
+ * is refreshed
+ */
+ playPauseTimeline: function () {
+ return AnimationsController
+ .toggleCurrentAnimations(this.timelineData.isMoving)
+ .then(() => this.refreshAnimationsStateAndUI());
+ },
+
+ onTimelineRewindClicked: function () {
+ this.rewindTimeline().catch(ex => console.error(ex));
+ },
+
+ /**
+ * Reset the startTime of all current animations shown in the timeline and
+ * pause them.
+ *
+ * @return {Promise} Resolves when currentTime is set and the UI is refreshed
+ */
+ rewindTimeline: function () {
+ return AnimationsController
+ .setCurrentTimeAll(0, true)
+ .then(() => this.refreshAnimationsStateAndUI());
+ },
+
+ /**
+ * Set the playback rate of all current animations shown in the timeline to
+ * the value of this.rateSelectorEl.
+ */
+ onRateChanged: function (e, rate) {
+ AnimationsController.setPlaybackRateAll(rate)
+ .then(() => this.refreshAnimationsStateAndUI())
+ .catch(ex => console.error(ex));
+ },
+
+ onTabNavigated: function () {
+ this.toggleAllButtonEl.classList.remove("paused");
+ },
+
+ onTimelineDataChanged: function (e, data) {
+ this.timelineData = data;
+ let {isMoving, isUserDrag, time} = data;
+
+ this.playTimelineButtonEl.classList.toggle("paused", !isMoving);
+
+ let l10nPlayProperty = isMoving ? "timeline.resumedButtonTooltip" :
+ "timeline.pausedButtonTooltip";
+
+ this.playTimelineButtonEl.setAttribute("title",
+ L10N.getStr(l10nPlayProperty));
+
+ // If the timeline data changed as a result of the user dragging the
+ // scrubber, then pause all animations and set their currentTimes.
+ // (Note that we want server-side requests to be sequenced, so we only do
+ // this after the previous currentTime setting was done).
+ if (isUserDrag && !this.setCurrentTimeAllPromise) {
+ this.setCurrentTimeAllPromise =
+ AnimationsController.setCurrentTimeAll(time, true)
+ .catch(error => console.error(error))
+ .then(() => {
+ this.setCurrentTimeAllPromise = null;
+ });
+ }
+
+ this.displayTimelineCurrentTime();
+ },
+
+ displayTimelineCurrentTime: function () {
+ let {time} = this.timelineData;
+ this.timelineCurrentTimeEl.textContent = formatStopwatchTime(time);
+ },
+
+ /**
+ * Make sure all known animations have their states up to date (which is
+ * useful after the playState or currentTime has been changed and in case the
+ * animations aren't auto-refreshing), and then refresh the UI.
+ */
+ refreshAnimationsStateAndUI: Task.async(function* () {
+ for (let player of AnimationsController.animationPlayers) {
+ yield player.refreshState();
+ }
+ yield this.refreshAnimationsUI();
+ }),
+
+ /**
+ * Refresh the list of animations UI. This will empty the panel and re-render
+ * the various components again.
+ */
+ refreshAnimationsUI: Task.async(function* () {
+ // Empty the whole panel first.
+ this.togglePlayers(true);
+
+ // Re-render the timeline component.
+ this.animationsTimelineComponent.render(
+ AnimationsController.animationPlayers,
+ AnimationsController.documentCurrentTime);
+
+ // Re-render the rate selector component.
+ if (this.rateSelectorComponent) {
+ this.rateSelectorComponent.render(AnimationsController.animationPlayers);
+ }
+
+ // If there are no players to show, show the error message instead and
+ // return.
+ if (!AnimationsController.animationPlayers.length) {
+ this.togglePlayers(false);
+ this.emit(this.UI_UPDATED_EVENT);
+ return;
+ }
+
+ this.emit(this.UI_UPDATED_EVENT);
+ })
+};
+
+EventEmitter.decorate(AnimationsPanel);
diff --git a/devtools/client/animationinspector/components/animation-details.js b/devtools/client/animationinspector/components/animation-details.js
new file mode 100644
index 000000000..c042ccac0
--- /dev/null
+++ b/devtools/client/animationinspector/components/animation-details.js
@@ -0,0 +1,222 @@
+/* -*- 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 {Task} = require("devtools/shared/task");
+const EventEmitter = require("devtools/shared/event-emitter");
+const {createNode, TimeScale} = require("devtools/client/animationinspector/utils");
+const {Keyframes} = require("devtools/client/animationinspector/components/keyframes");
+
+/**
+ * UI component responsible for displaying detailed information for a given
+ * animation.
+ * This includes information about timing, easing, keyframes, animated
+ * properties.
+ *
+ * @param {Object} serverTraits The list of server-side capabilities.
+ */
+function AnimationDetails(serverTraits) {
+ EventEmitter.decorate(this);
+
+ this.onFrameSelected = this.onFrameSelected.bind(this);
+
+ this.keyframeComponents = [];
+ this.serverTraits = serverTraits;
+}
+
+exports.AnimationDetails = AnimationDetails;
+
+AnimationDetails.prototype = {
+ // These are part of frame objects but are not animated properties. This
+ // array is used to skip them.
+ NON_PROPERTIES: ["easing", "composite", "computedOffset", "offset"],
+
+ init: function (containerEl) {
+ this.containerEl = containerEl;
+ },
+
+ destroy: function () {
+ this.unrender();
+ this.containerEl = null;
+ this.serverTraits = null;
+ },
+
+ unrender: function () {
+ for (let component of this.keyframeComponents) {
+ component.off("frame-selected", this.onFrameSelected);
+ component.destroy();
+ }
+ this.keyframeComponents = [];
+
+ while (this.containerEl.firstChild) {
+ this.containerEl.firstChild.remove();
+ }
+ },
+
+ getPerfDataForProperty: function (animation, propertyName) {
+ let warning = "";
+ let className = "";
+ if (animation.state.propertyState) {
+ let isRunningOnCompositor;
+ for (let propState of animation.state.propertyState) {
+ if (propState.property == propertyName) {
+ isRunningOnCompositor = propState.runningOnCompositor;
+ if (typeof propState.warning != "undefined") {
+ warning = propState.warning;
+ }
+ break;
+ }
+ }
+ if (isRunningOnCompositor && warning == "") {
+ className = "oncompositor";
+ } else if (!isRunningOnCompositor && warning != "") {
+ className = "warning";
+ }
+ }
+ return {className, warning};
+ },
+
+ /**
+ * Get a list of the tracks of the animation actor
+ * @return {Object} A list of tracks, one per animated property, each
+ * with a list of keyframes
+ */
+ getTracks: Task.async(function* () {
+ let tracks = {};
+
+ /*
+ * getFrames is a AnimationPlayorActor method that returns data about the
+ * keyframes of the animation.
+ * In FF48, the data it returns change, and will hold only longhand
+ * properties ( e.g. borderLeftWidth ), which does not match what we
+ * want to display in the animation detail.
+ * A new AnimationPlayerActor function, getProperties, is introduced,
+ * that returns the animated css properties of the animation and their
+ * keyframes values.
+ * If the animation actor has the getProperties function, we use it, and if
+ * not, we fall back to getFrames, which then returns values we used to
+ * handle.
+ */
+ if (this.serverTraits.hasGetProperties) {
+ let properties = yield this.animation.getProperties();
+ for (let {name, values} of properties) {
+ if (!tracks[name]) {
+ tracks[name] = [];
+ }
+
+ for (let {value, offset} of values) {
+ tracks[name].push({value, offset});
+ }
+ }
+ } else {
+ let frames = yield this.animation.getFrames();
+ for (let frame of frames) {
+ for (let name in frame) {
+ if (this.NON_PROPERTIES.indexOf(name) != -1) {
+ continue;
+ }
+
+ if (!tracks[name]) {
+ tracks[name] = [];
+ }
+
+ tracks[name].push({
+ value: frame[name],
+ offset: frame.computedOffset
+ });
+ }
+ }
+ }
+
+ return tracks;
+ }),
+
+ render: Task.async(function* (animation) {
+ this.unrender();
+
+ if (!animation) {
+ return;
+ }
+ this.animation = animation;
+
+ // We might have been destroyed in the meantime, or the component might
+ // have been re-rendered.
+ if (!this.containerEl || this.animation !== animation) {
+ return;
+ }
+
+ // Build an element for each animated property track.
+ this.tracks = yield this.getTracks(animation, this.serverTraits);
+
+ // Useful for tests to know when the keyframes have been retrieved.
+ this.emit("keyframes-retrieved");
+
+ for (let propertyName in this.tracks) {
+ let line = createNode({
+ parent: this.containerEl,
+ attributes: {"class": "property"}
+ });
+ let {warning, className} =
+ this.getPerfDataForProperty(animation, propertyName);
+ createNode({
+ // text-overflow doesn't work in flex items, so we need a second level
+ // of container to actually have an ellipsis on the name.
+ // See bug 972664.
+ parent: createNode({
+ parent: line,
+ attributes: {"class": "name"}
+ }),
+ textContent: getCssPropertyName(propertyName),
+ attributes: {"title": warning,
+ "class": className}
+ });
+
+ // Add the keyframes diagram for this property.
+ let framesWrapperEl = createNode({
+ parent: line,
+ attributes: {"class": "track-container"}
+ });
+
+ let framesEl = createNode({
+ parent: framesWrapperEl,
+ attributes: {"class": "frames"}
+ });
+
+ // Scale the list of keyframes according to the current time scale.
+ let {x, w} = TimeScale.getAnimationDimensions(animation);
+ framesEl.style.left = `${x}%`;
+ framesEl.style.width = `${w}%`;
+
+ let keyframesComponent = new Keyframes();
+ keyframesComponent.init(framesEl);
+ keyframesComponent.render({
+ keyframes: this.tracks[propertyName],
+ propertyName: propertyName,
+ animation: animation
+ });
+ keyframesComponent.on("frame-selected", this.onFrameSelected);
+
+ this.keyframeComponents.push(keyframesComponent);
+ }
+ }),
+
+ onFrameSelected: function (e, args) {
+ // Relay the event up, it's needed in parents too.
+ this.emit(e, args);
+ }
+};
+
+/**
+ * Turn propertyName into property-name.
+ * @param {String} jsPropertyName A camelcased CSS property name. Typically
+ * something that comes out of computed styles. E.g. borderBottomColor
+ * @return {String} The corresponding CSS property name: border-bottom-color
+ */
+function getCssPropertyName(jsPropertyName) {
+ return jsPropertyName.replace(/[A-Z]/g, "-$&").toLowerCase();
+}
+exports.getCssPropertyName = getCssPropertyName;
diff --git a/devtools/client/animationinspector/components/animation-target-node.js b/devtools/client/animationinspector/components/animation-target-node.js
new file mode 100644
index 000000000..c300e9ce7
--- /dev/null
+++ b/devtools/client/animationinspector/components/animation-target-node.js
@@ -0,0 +1,80 @@
+/* -*- 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 {Task} = require("devtools/shared/task");
+const EventEmitter = require("devtools/shared/event-emitter");
+const {DomNodePreview} = require("devtools/client/inspector/shared/dom-node-preview");
+
+// Map dom node fronts by animation fronts so we don't have to get them from the
+// walker every time the timeline is refreshed.
+var nodeFronts = new WeakMap();
+
+/**
+ * UI component responsible for displaying a preview of the target dom node of
+ * a given animation.
+ * Accepts the same parameters as the DomNodePreview component. See
+ * devtools/client/inspector/shared/dom-node-preview.js for documentation.
+ */
+function AnimationTargetNode(inspector, options) {
+ this.inspector = inspector;
+ this.previewer = new DomNodePreview(inspector, options);
+ EventEmitter.decorate(this);
+}
+
+exports.AnimationTargetNode = AnimationTargetNode;
+
+AnimationTargetNode.prototype = {
+ init: function (containerEl) {
+ this.previewer.init(containerEl);
+ this.isDestroyed = false;
+ },
+
+ destroy: function () {
+ this.previewer.destroy();
+ this.inspector = null;
+ this.isDestroyed = true;
+ },
+
+ render: Task.async(function* (playerFront) {
+ // Get the nodeFront from the cache if it was stored previously.
+ let nodeFront = nodeFronts.get(playerFront);
+
+ // Try and get it from the playerFront directly next.
+ if (!nodeFront) {
+ nodeFront = playerFront.animationTargetNodeFront;
+ }
+
+ // Finally, get it from the walkerActor if it wasn't found.
+ if (!nodeFront) {
+ try {
+ nodeFront = yield this.inspector.walker.getNodeFromActor(
+ playerFront.actorID, ["node"]);
+ } catch (e) {
+ // If an error occured while getting the nodeFront and if it can't be
+ // attributed to the panel having been destroyed in the meantime, this
+ // error needs to be logged and render needs to stop.
+ if (!this.isDestroyed) {
+ console.error(e);
+ }
+ return;
+ }
+
+ // In all cases, if by now the panel doesn't exist anymore, we need to
+ // stop rendering too.
+ if (this.isDestroyed) {
+ return;
+ }
+ }
+
+ // Add the nodeFront to the cache.
+ nodeFronts.set(playerFront, nodeFront);
+
+ this.previewer.render(nodeFront);
+ this.emit("target-retrieved");
+ })
+};
diff --git a/devtools/client/animationinspector/components/animation-time-block.js b/devtools/client/animationinspector/components/animation-time-block.js
new file mode 100644
index 000000000..51392da8f
--- /dev/null
+++ b/devtools/client/animationinspector/components/animation-time-block.js
@@ -0,0 +1,719 @@
+/* -*- 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 EventEmitter = require("devtools/shared/event-emitter");
+const {createNode, TimeScale} = require("devtools/client/animationinspector/utils");
+
+const { LocalizationHelper } = require("devtools/shared/l10n");
+const L10N =
+ new LocalizationHelper("devtools/client/locales/animationinspector.properties");
+
+// In the createPathSegments function, an animation duration is divided by
+// DURATION_RESOLUTION in order to draw the way the animation progresses.
+// But depending on the timing-function, we may be not able to make the graph
+// smoothly progress if this resolution is not high enough.
+// So, if the difference of animation progress between 2 divisions is more than
+// MIN_PROGRESS_THRESHOLD, then createPathSegments re-divides
+// by DURATION_RESOLUTION.
+// DURATION_RESOLUTION shoud be integer and more than 2.
+const DURATION_RESOLUTION = 4;
+// MIN_PROGRESS_THRESHOLD shoud be between more than 0 to 1.
+const MIN_PROGRESS_THRESHOLD = 0.1;
+// Show max 10 iterations for infinite animations
+// to give users a clue that the animation does repeat.
+const MAX_INFINITE_ANIMATIONS_ITERATIONS = 10;
+// SVG namespace
+const SVG_NS = "http://www.w3.org/2000/svg";
+
+/**
+ * UI component responsible for displaying a single animation timeline, which
+ * basically looks like a rectangle that shows the delay and iterations.
+ */
+function AnimationTimeBlock() {
+ EventEmitter.decorate(this);
+ this.onClick = this.onClick.bind(this);
+}
+
+exports.AnimationTimeBlock = AnimationTimeBlock;
+
+AnimationTimeBlock.prototype = {
+ init: function (containerEl) {
+ this.containerEl = containerEl;
+ this.containerEl.addEventListener("click", this.onClick);
+ },
+
+ destroy: function () {
+ this.containerEl.removeEventListener("click", this.onClick);
+ this.unrender();
+ this.containerEl = null;
+ this.animation = null;
+ },
+
+ unrender: function () {
+ while (this.containerEl.firstChild) {
+ this.containerEl.firstChild.remove();
+ }
+ },
+
+ render: function (animation) {
+ this.unrender();
+
+ this.animation = animation;
+ let {state} = this.animation;
+
+ // Create a container element to hold the delay and iterations.
+ // It is positioned according to its delay (divided by the playbackrate),
+ // and its width is according to its duration (divided by the playbackrate).
+ const {x, delayX, delayW, endDelayX, endDelayW} =
+ TimeScale.getAnimationDimensions(animation);
+
+ // Animation summary graph element.
+ const summaryEl = createNode({
+ parent: this.containerEl,
+ namespace: "http://www.w3.org/2000/svg",
+ nodeType: "svg",
+ attributes: {
+ "class": "summary",
+ "preserveAspectRatio": "none",
+ "style": `left: ${ x - (state.delay > 0 ? delayW : 0) }%`
+ }
+ });
+
+ // Total displayed duration
+ const totalDisplayedDuration = state.playbackRate * TimeScale.getDuration();
+
+ // Calculate stroke height in viewBox to display stroke of path.
+ const strokeHeightForViewBox = 0.5 / this.containerEl.clientHeight;
+
+ // Set viewBox
+ summaryEl.setAttribute("viewBox",
+ `${ state.delay < 0 ? state.delay : 0 }
+ -${ 1 + strokeHeightForViewBox }
+ ${ totalDisplayedDuration }
+ ${ 1 + strokeHeightForViewBox * 2 }`);
+
+ // Get a helper function that returns the path segment of timing-function.
+ const segmentHelper = getSegmentHelper(state, this.win);
+
+ // Minimum segment duration is the duration of one pixel.
+ const minSegmentDuration =
+ totalDisplayedDuration / this.containerEl.clientWidth;
+ // Minimum progress threshold.
+ let minProgressThreshold = MIN_PROGRESS_THRESHOLD;
+ // If the easing is step function,
+ // minProgressThreshold should be changed by the steps.
+ const stepFunction = state.easing.match(/steps\((\d+)/);
+ if (stepFunction) {
+ minProgressThreshold = 1 / (parseInt(stepFunction[1], 10) + 1);
+ }
+
+ // Starting time of main iteration.
+ let mainIterationStartTime = 0;
+ let iterationStart = state.iterationStart;
+ let iterationCount = state.iterationCount ? state.iterationCount : Infinity;
+
+ // Append delay.
+ if (state.delay > 0) {
+ renderDelay(summaryEl, state, segmentHelper);
+ mainIterationStartTime = state.delay;
+ } else {
+ const negativeDelayCount = -state.delay / state.duration;
+ // Move to forward the starting point for negative delay.
+ iterationStart += negativeDelayCount;
+ // Consume iteration count by negative delay.
+ if (iterationCount !== Infinity) {
+ iterationCount -= negativeDelayCount;
+ }
+ }
+
+ // Append 1st section of iterations,
+ // This section is only useful in cases where iterationStart has decimals.
+ // e.g.
+ // if { iterationStart: 0.25, iterations: 3 }, firstSectionCount is 0.75.
+ const firstSectionCount =
+ iterationStart % 1 === 0
+ ? 0 : Math.min(iterationCount, 1) - iterationStart % 1;
+ if (firstSectionCount) {
+ renderFirstIteration(summaryEl, state, mainIterationStartTime,
+ firstSectionCount, minSegmentDuration,
+ minProgressThreshold, segmentHelper);
+ }
+
+ if (iterationCount === Infinity) {
+ // If the animation repeats infinitely,
+ // we fill the remaining area with iteration paths.
+ renderInfinity(summaryEl, state, mainIterationStartTime,
+ firstSectionCount, totalDisplayedDuration,
+ minSegmentDuration, minProgressThreshold, segmentHelper);
+ } else {
+ // Otherwise, we show remaining iterations, endDelay and fill.
+
+ // Append forwards fill-mode.
+ if (state.fill === "both" || state.fill === "forwards") {
+ renderForwardsFill(summaryEl, state, mainIterationStartTime,
+ iterationCount, totalDisplayedDuration,
+ segmentHelper);
+ }
+
+ // Append middle section of iterations.
+ // e.g.
+ // if { iterationStart: 0.25, iterations: 3 }, middleSectionCount is 2.
+ const middleSectionCount =
+ Math.floor(iterationCount - firstSectionCount);
+ renderMiddleIterations(summaryEl, state, mainIterationStartTime,
+ firstSectionCount, middleSectionCount,
+ minSegmentDuration, minProgressThreshold,
+ segmentHelper);
+
+ // Append last section of iterations, if there is remaining iteration.
+ // e.g.
+ // if { iterationStart: 0.25, iterations: 3 }, lastSectionCount is 0.25.
+ const lastSectionCount =
+ iterationCount - middleSectionCount - firstSectionCount;
+ if (lastSectionCount) {
+ renderLastIteration(summaryEl, state, mainIterationStartTime,
+ firstSectionCount, middleSectionCount,
+ lastSectionCount, minSegmentDuration,
+ minProgressThreshold, segmentHelper);
+ }
+
+ // Append endDelay.
+ if (state.endDelay > 0) {
+ renderEndDelay(summaryEl, state,
+ mainIterationStartTime, iterationCount, segmentHelper);
+ }
+ }
+
+ // Append negative delay (which overlap the animation).
+ if (state.delay < 0) {
+ segmentHelper.animation.effect.timing.fill = "both";
+ segmentHelper.asOriginalBehavior = false;
+ renderNegativeDelayHiddenProgress(summaryEl, state, minSegmentDuration,
+ minProgressThreshold, segmentHelper);
+ }
+ // Append negative endDelay (which overlap the animation).
+ if (state.iterationCount && state.endDelay < 0) {
+ if (segmentHelper.asOriginalBehavior) {
+ segmentHelper.animation.effect.timing.fill = "both";
+ segmentHelper.asOriginalBehavior = false;
+ }
+ renderNegativeEndDelayHiddenProgress(summaryEl, state,
+ minSegmentDuration,
+ minProgressThreshold,
+ segmentHelper);
+ }
+
+ // The animation name is displayed over the animation.
+ createNode({
+ parent: createNode({
+ parent: this.containerEl,
+ attributes: {
+ "class": "name",
+ "title": this.getTooltipText(state)
+ },
+ }),
+ textContent: state.name
+ });
+
+ // Delay.
+ if (state.delay) {
+ // Negative delays need to start at 0.
+ createNode({
+ parent: this.containerEl,
+ attributes: {
+ "class": "delay"
+ + (state.delay < 0 ? " negative" : " positive")
+ + (state.fill === "both" ||
+ state.fill === "backwards" ? " fill" : ""),
+ "style": `left:${ delayX }%; width:${ delayW }%;`
+ }
+ });
+ }
+
+ // endDelay
+ if (state.iterationCount && state.endDelay) {
+ createNode({
+ parent: this.containerEl,
+ attributes: {
+ "class": "end-delay"
+ + (state.endDelay < 0 ? " negative" : " positive")
+ + (state.fill === "both" ||
+ state.fill === "forwards" ? " fill" : ""),
+ "style": `left:${ endDelayX }%; width:${ endDelayW }%;`
+ }
+ });
+ }
+ },
+
+ getTooltipText: function (state) {
+ let getTime = time => L10N.getFormatStr("player.timeLabel",
+ L10N.numberWithDecimals(time / 1000, 2));
+
+ let text = "";
+
+ // Adding the name.
+ text += getFormattedAnimationTitle({state});
+ text += "\n";
+
+ // Adding the delay.
+ if (state.delay) {
+ text += L10N.getStr("player.animationDelayLabel") + " ";
+ text += getTime(state.delay);
+ text += "\n";
+ }
+
+ // Adding the duration.
+ text += L10N.getStr("player.animationDurationLabel") + " ";
+ text += getTime(state.duration);
+ text += "\n";
+
+ // Adding the endDelay.
+ if (state.endDelay) {
+ text += L10N.getStr("player.animationEndDelayLabel") + " ";
+ text += getTime(state.endDelay);
+ text += "\n";
+ }
+
+ // Adding the iteration count (the infinite symbol, or an integer).
+ if (state.iterationCount !== 1) {
+ text += L10N.getStr("player.animationIterationCountLabel") + " ";
+ text += state.iterationCount ||
+ L10N.getStr("player.infiniteIterationCountText");
+ text += "\n";
+ }
+
+ // Adding the iteration start.
+ if (state.iterationStart !== 0) {
+ let iterationStartTime = state.iterationStart * state.duration / 1000;
+ text += L10N.getFormatStr("player.animationIterationStartLabel",
+ state.iterationStart,
+ L10N.numberWithDecimals(iterationStartTime, 2));
+ text += "\n";
+ }
+
+ // Adding the easing.
+ if (state.easing) {
+ text += L10N.getStr("player.animationEasingLabel") + " ";
+ text += state.easing;
+ text += "\n";
+ }
+
+ // Adding the fill mode.
+ if (state.fill) {
+ text += L10N.getStr("player.animationFillLabel") + " ";
+ text += state.fill;
+ text += "\n";
+ }
+
+ // Adding the direction mode.
+ if (state.direction) {
+ text += L10N.getStr("player.animationDirectionLabel") + " ";
+ text += state.direction;
+ text += "\n";
+ }
+
+ // Adding the playback rate if it's different than 1.
+ if (state.playbackRate !== 1) {
+ text += L10N.getStr("player.animationRateLabel") + " ";
+ text += state.playbackRate;
+ text += "\n";
+ }
+
+ // Adding a note that the animation is running on the compositor thread if
+ // needed.
+ if (state.propertyState) {
+ if (state.propertyState
+ .every(propState => propState.runningOnCompositor)) {
+ text += L10N.getStr("player.allPropertiesOnCompositorTooltip");
+ } else if (state.propertyState
+ .some(propState => propState.runningOnCompositor)) {
+ text += L10N.getStr("player.somePropertiesOnCompositorTooltip");
+ }
+ } else if (state.isRunningOnCompositor) {
+ text += L10N.getStr("player.runningOnCompositorTooltip");
+ }
+
+ return text;
+ },
+
+ onClick: function (e) {
+ e.stopPropagation();
+ this.emit("selected", this.animation);
+ },
+
+ get win() {
+ return this.containerEl.ownerDocument.defaultView;
+ }
+};
+
+/**
+ * Get a formatted title for this animation. This will be either:
+ * "some-name", "some-name : CSS Transition", "some-name : CSS Animation",
+ * "some-name : Script Animation", or "Script Animation", depending
+ * if the server provides the type, what type it is and if the animation
+ * has a name
+ * @param {AnimationPlayerFront} animation
+ */
+function getFormattedAnimationTitle({state}) {
+ // Older servers don't send a type, and only know about
+ // CSSAnimations and CSSTransitions, so it's safe to use
+ // just the name.
+ if (!state.type) {
+ return state.name;
+ }
+
+ // Script-generated animations may not have a name.
+ if (state.type === "scriptanimation" && !state.name) {
+ return L10N.getStr("timeline.scriptanimation.unnamedLabel");
+ }
+
+ return L10N.getFormatStr(`timeline.${state.type}.nameLabel`, state.name);
+}
+
+/**
+ * Render delay section.
+ * @param {Element} parentEl - Parent element of this appended path element.
+ * @param {Object} state - State of animation.
+ * @param {Object} segmentHelper - The object returned by getSegmentHelper.
+ */
+function renderDelay(parentEl, state, segmentHelper) {
+ const startSegment = segmentHelper.getSegment(0);
+ const endSegment = { x: state.delay, y: startSegment.y };
+ appendPathElement(parentEl, [startSegment, endSegment], "delay-path");
+}
+
+/**
+ * Render first iteration section.
+ * @param {Element} parentEl - Parent element of this appended path element.
+ * @param {Object} state - State of animation.
+ * @param {Number} mainIterationStartTime - Starting time of main iteration.
+ * @param {Number} firstSectionCount - Iteration count of first section.
+ * @param {Number} minSegmentDuration - Minimum segment duration.
+ * @param {Number} minProgressThreshold - Minimum progress threshold.
+ * @param {Object} segmentHelper - The object returned by getSegmentHelper.
+ */
+function renderFirstIteration(parentEl, state, mainIterationStartTime,
+ firstSectionCount, minSegmentDuration,
+ minProgressThreshold, segmentHelper) {
+ const startTime = mainIterationStartTime;
+ const endTime = startTime + firstSectionCount * state.duration;
+ const segments =
+ createPathSegments(startTime, endTime, minSegmentDuration,
+ minProgressThreshold, segmentHelper);
+ appendPathElement(parentEl, segments, "iteration-path");
+}
+
+/**
+ * Render middle iterations section.
+ * @param {Element} parentEl - Parent element of this appended path element.
+ * @param {Object} state - State of animation.
+ * @param {Number} mainIterationStartTime - Starting time of main iteration.
+ * @param {Number} firstSectionCount - Iteration count of first section.
+ * @param {Number} middleSectionCount - Iteration count of middle section.
+ * @param {Number} minSegmentDuration - Minimum segment duration.
+ * @param {Number} minProgressThreshold - Minimum progress threshold.
+ * @param {Object} segmentHelper - The object returned by getSegmentHelper.
+ */
+function renderMiddleIterations(parentEl, state, mainIterationStartTime,
+ firstSectionCount, middleSectionCount,
+ minSegmentDuration, minProgressThreshold,
+ segmentHelper) {
+ const offset = mainIterationStartTime + firstSectionCount * state.duration;
+ for (let i = 0; i < middleSectionCount; i++) {
+ // Get the path segments of each iteration.
+ const startTime = offset + i * state.duration;
+ const endTime = startTime + state.duration;
+ const segments =
+ createPathSegments(startTime, endTime, minSegmentDuration,
+ minProgressThreshold, segmentHelper);
+ appendPathElement(parentEl, segments, "iteration-path");
+ }
+}
+
+/**
+ * Render last iteration section.
+ * @param {Element} parentEl - Parent element of this appended path element.
+ * @param {Object} state - State of animation.
+ * @param {Number} mainIterationStartTime - Starting time of main iteration.
+ * @param {Number} firstSectionCount - Iteration count of first section.
+ * @param {Number} middleSectionCount - Iteration count of middle section.
+ * @param {Number} lastSectionCount - Iteration count of last section.
+ * @param {Number} minSegmentDuration - Minimum segment duration.
+ * @param {Number} minProgressThreshold - Minimum progress threshold.
+ * @param {Object} segmentHelper - The object returned by getSegmentHelper.
+ */
+function renderLastIteration(parentEl, state, mainIterationStartTime,
+ firstSectionCount, middleSectionCount,
+ lastSectionCount, minSegmentDuration,
+ minProgressThreshold, segmentHelper) {
+ const startTime = mainIterationStartTime +
+ (firstSectionCount + middleSectionCount) * state.duration;
+ const endTime = startTime + lastSectionCount * state.duration;
+ const segments =
+ createPathSegments(startTime, endTime, minSegmentDuration,
+ minProgressThreshold, segmentHelper);
+ appendPathElement(parentEl, segments, "iteration-path");
+}
+
+/**
+ * Render Infinity iterations.
+ * @param {Element} parentEl - Parent element of this appended path element.
+ * @param {Object} state - State of animation.
+ * @param {Number} mainIterationStartTime - Starting time of main iteration.
+ * @param {Number} firstSectionCount - Iteration count of first section.
+ * @param {Number} totalDuration - Displayed max duration.
+ * @param {Number} minSegmentDuration - Minimum segment duration.
+ * @param {Number} minProgressThreshold - Minimum progress threshold.
+ * @param {Object} segmentHelper - The object returned by getSegmentHelper.
+ */
+function renderInfinity(parentEl, state, mainIterationStartTime,
+ firstSectionCount, totalDuration, minSegmentDuration,
+ minProgressThreshold, segmentHelper) {
+ // Calculate the number of iterations to display,
+ // with a maximum of MAX_INFINITE_ANIMATIONS_ITERATIONS
+ let uncappedInfinityIterationCount =
+ (totalDuration - firstSectionCount * state.duration) / state.duration;
+ // If there is a small floating point error resulting in, e.g. 1.0000001
+ // ceil will give us 2 so round first.
+ uncappedInfinityIterationCount =
+ parseFloat(uncappedInfinityIterationCount.toPrecision(6));
+ const infinityIterationCount =
+ Math.min(MAX_INFINITE_ANIMATIONS_ITERATIONS,
+ Math.ceil(uncappedInfinityIterationCount));
+
+ // Append first full iteration path.
+ const firstStartTime =
+ mainIterationStartTime + firstSectionCount * state.duration;
+ const firstEndTime = firstStartTime + state.duration;
+ const firstSegments =
+ createPathSegments(firstStartTime, firstEndTime, minSegmentDuration,
+ minProgressThreshold, segmentHelper);
+ appendPathElement(parentEl, firstSegments, "iteration-path infinity");
+
+ // Append other iterations. We can copy first segments.
+ const isAlternate = state.direction.match(/alternate/);
+ for (let i = 1; i < infinityIterationCount; i++) {
+ const startTime = firstStartTime + i * state.duration;
+ let segments;
+ if (isAlternate && i % 2) {
+ // Copy as reverse.
+ segments = firstSegments.map(segment => {
+ return { x: firstEndTime - segment.x + startTime, y: segment.y };
+ });
+ } else {
+ // Copy as is.
+ segments = firstSegments.map(segment => {
+ return { x: segment.x - firstStartTime + startTime, y: segment.y };
+ });
+ }
+ appendPathElement(parentEl, segments, "iteration-path infinity copied");
+ }
+}
+
+/**
+ * Render endDelay section.
+ * @param {Element} parentEl - Parent element of this appended path element.
+ * @param {Object} state - State of animation.
+ * @param {Number} mainIterationStartTime - Starting time of main iteration.
+ * @param {Number} iterationCount - Whole iteration count.
+ * @param {Object} segmentHelper - The object returned by getSegmentHelper.
+ */
+function renderEndDelay(parentEl, state,
+ mainIterationStartTime, iterationCount, segmentHelper) {
+ const startTime = mainIterationStartTime + iterationCount * state.duration;
+ const startSegment = segmentHelper.getSegment(startTime);
+ const endSegment = { x: startTime + state.endDelay, y: startSegment.y };
+ appendPathElement(parentEl, [startSegment, endSegment], "enddelay-path");
+}
+
+/**
+ * Render forwards fill section.
+ * @param {Element} parentEl - Parent element of this appended path element.
+ * @param {Object} state - State of animation.
+ * @param {Number} mainIterationStartTime - Starting time of main iteration.
+ * @param {Number} iterationCount - Whole iteration count.
+ * @param {Number} totalDuration - Displayed max duration.
+ * @param {Object} segmentHelper - The object returned by getSegmentHelper.
+ */
+function renderForwardsFill(parentEl, state, mainIterationStartTime,
+ iterationCount, totalDuration, segmentHelper) {
+ const startTime = mainIterationStartTime + iterationCount * state.duration +
+ (state.endDelay > 0 ? state.endDelay : 0);
+ const startSegment = segmentHelper.getSegment(startTime);
+ const endSegment = { x: totalDuration, y: startSegment.y };
+ appendPathElement(parentEl, [startSegment, endSegment], "fill-forwards-path");
+}
+
+/**
+ * Render hidden progress of negative delay.
+ * @param {Element} parentEl - Parent element of this appended path element.
+ * @param {Object} state - State of animation.
+ * @param {Number} minSegmentDuration - Minimum segment duration.
+ * @param {Number} minProgressThreshold - Minimum progress threshold.
+ * @param {Object} segmentHelper - The object returned by getSegmentHelper.
+ */
+function renderNegativeDelayHiddenProgress(parentEl, state, minSegmentDuration,
+ minProgressThreshold,
+ segmentHelper) {
+ const startTime = state.delay;
+ const endTime = 0;
+ const segments =
+ createPathSegments(startTime, endTime, minSegmentDuration,
+ minProgressThreshold, segmentHelper);
+ appendPathElement(parentEl, segments, "delay-path negative");
+}
+
+/**
+ * Render hidden progress of negative endDelay.
+ * @param {Element} parentEl - Parent element of this appended path element.
+ * @param {Object} state - State of animation.
+ * @param {Number} minSegmentDuration - Minimum segment duration.
+ * @param {Number} minProgressThreshold - Minimum progress threshold.
+ * @param {Object} segmentHelper - The object returned by getSegmentHelper.
+ */
+function renderNegativeEndDelayHiddenProgress(parentEl, state,
+ minSegmentDuration,
+ minProgressThreshold,
+ segmentHelper) {
+ const endTime = state.delay + state.iterationCount * state.duration;
+ const startTime = endTime + state.endDelay;
+ const segments =
+ createPathSegments(startTime, endTime, minSegmentDuration,
+ minProgressThreshold, segmentHelper);
+ appendPathElement(parentEl, segments, "enddelay-path negative");
+}
+
+/**
+ * Get a helper function which returns the segment coord from given time.
+ * @param {Object} state - animation state
+ * @param {Object} win - window object
+ * @return {Object} A segmentHelper object that has the following properties:
+ * - animation: The script animation used to get the progress
+ * - endTime: The end time of the animation
+ * - asOriginalBehavior: The spec is that the progress of animation is changed
+ * if the time of setCurrentTime is during the endDelay.
+ * Likewise, in case the time is less than 0.
+ * If this flag is true, we prevent the time
+ * to make the same animation behavior as the original.
+ * - getSegment: Helper function that, given a time,
+ * will calculate the progress through the dummy animation.
+ */
+function getSegmentHelper(state, win) {
+ // Create a dummy Animation timing data as the
+ // state object we're being passed in.
+ const timing = Object.assign({}, state, {
+ iterations: state.iterationCount ? state.iterationCount : Infinity
+ });
+
+ // Create a dummy Animation with the given timing.
+ const dummyAnimation =
+ new win.Animation(new win.KeyframeEffect(null, null, timing), null);
+
+ // Returns segment helper object.
+ return {
+ animation: dummyAnimation,
+ endTime: dummyAnimation.effect.getComputedTiming().endTime,
+ asOriginalBehavior: true,
+ getSegment: function (time) {
+ if (this.asOriginalBehavior) {
+ // If the given time is less than 0, returned progress is 0.
+ if (time < 0) {
+ return { x: time, y: 0 };
+ }
+ // Avoid to apply over endTime.
+ this.animation.currentTime = time < this.endTime ? time : this.endTime;
+ } else {
+ this.animation.currentTime = time;
+ }
+ const progress = this.animation.effect.getComputedTiming().progress;
+ return { x: time, y: Math.max(progress, 0) };
+ }
+ };
+}
+
+/**
+ * Create the path segments from given parameters.
+ * @param {Number} startTime - Starting time of animation.
+ * @param {Number} endTime - Ending time of animation.
+ * @param {Number} minSegmentDuration - Minimum segment duration.
+ * @param {Number} minProgressThreshold - Minimum progress threshold.
+ * @param {Object} segmentHelper - The object of getSegmentHelper.
+ * @return {Array} path segments -
+ * [{x: {Number} time, y: {Number} progress}, ...]
+ */
+function createPathSegments(startTime, endTime, minSegmentDuration,
+ minProgressThreshold, segmentHelper) {
+ // If the duration is too short, early return.
+ if (endTime - startTime < minSegmentDuration) {
+ return [segmentHelper.getSegment(startTime),
+ segmentHelper.getSegment(endTime)];
+ }
+
+ // Otherwise, start creating segments.
+ let pathSegments = [];
+
+ // Append the segment for the startTime position.
+ const startTimeSegment = segmentHelper.getSegment(startTime);
+ pathSegments.push(startTimeSegment);
+ let previousSegment = startTimeSegment;
+
+ // Split the duration in equal intervals, and iterate over them.
+ // See the definition of DURATION_RESOLUTION for more information about this.
+ const interval = (endTime - startTime) / DURATION_RESOLUTION;
+ for (let index = 1; index <= DURATION_RESOLUTION; index++) {
+ // Create a segment for this interval.
+ const currentSegment =
+ segmentHelper.getSegment(startTime + index * interval);
+
+ // If the distance between the Y coordinate (the animation's progress) of
+ // the previous segment and the Y coordinate of the current segment is too
+ // large, then recurse with a smaller duration to get more details
+ // in the graph.
+ if (Math.abs(currentSegment.y - previousSegment.y) > minProgressThreshold) {
+ // Divide the current interval (excluding start and end bounds
+ // by adding/subtracting 1ms).
+ pathSegments = pathSegments.concat(
+ createPathSegments(previousSegment.x + 1, currentSegment.x - 1,
+ minSegmentDuration, minProgressThreshold,
+ segmentHelper));
+ }
+
+ pathSegments.push(currentSegment);
+ previousSegment = currentSegment;
+ }
+
+ return pathSegments;
+}
+
+/**
+ * Append path element.
+ * @param {Element} parentEl - Parent element of this appended path element.
+ * @param {Array} pathSegments - Path segments. Please see createPathSegments.
+ * @param {String} cls - Class name.
+ * @return {Element} path element.
+ */
+function appendPathElement(parentEl, pathSegments, cls) {
+ // Create path string.
+ let path = `M${ pathSegments[0].x },0`;
+ pathSegments.forEach(pathSegment => {
+ path += ` L${ pathSegment.x },${ pathSegment.y }`;
+ });
+ path += ` L${ pathSegments[pathSegments.length - 1].x },0 Z`;
+ // Append and return the path element.
+ return createNode({
+ parent: parentEl,
+ namespace: SVG_NS,
+ nodeType: "path",
+ attributes: {
+ "d": path,
+ "class": cls,
+ "vector-effect": "non-scaling-stroke",
+ "transform": "scale(1, -1)"
+ }
+ });
+}
diff --git a/devtools/client/animationinspector/components/animation-timeline.js b/devtools/client/animationinspector/components/animation-timeline.js
new file mode 100644
index 000000000..49995d729
--- /dev/null
+++ b/devtools/client/animationinspector/components/animation-timeline.js
@@ -0,0 +1,502 @@
+/* -*- 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 EventEmitter = require("devtools/shared/event-emitter");
+const {
+ createNode,
+ findOptimalTimeInterval,
+ TimeScale
+} = require("devtools/client/animationinspector/utils");
+const {AnimationDetails} = require("devtools/client/animationinspector/components/animation-details");
+const {AnimationTargetNode} = require("devtools/client/animationinspector/components/animation-target-node");
+const {AnimationTimeBlock} = require("devtools/client/animationinspector/components/animation-time-block");
+
+// The minimum spacing between 2 time graduation headers in the timeline (px).
+const TIME_GRADUATION_MIN_SPACING = 40;
+// When the container window is resized, the timeline background gets refreshed,
+// but only after a timer, and the timer is reset if the window is continuously
+// resized.
+const TIMELINE_BACKGROUND_RESIZE_DEBOUNCE_TIMER = 50;
+
+/**
+ * UI component responsible for displaying a timeline for animations.
+ * The timeline is essentially a graph with time along the x axis and animations
+ * along the y axis.
+ * The time is represented with a graduation header at the top and a current
+ * time play head.
+ * Animations are organized by lines, with a left margin containing the preview
+ * of the target DOM element the animation applies to.
+ * The current time play head can be moved by clicking/dragging in the header.
+ * when this happens, the component emits "current-data-changed" events with the
+ * new time and state of the timeline.
+ *
+ * @param {InspectorPanel} inspector.
+ * @param {Object} serverTraits The list of server-side capabilities.
+ */
+function AnimationsTimeline(inspector, serverTraits) {
+ this.animations = [];
+ this.targetNodes = [];
+ this.timeBlocks = [];
+ this.details = [];
+ this.inspector = inspector;
+ this.serverTraits = serverTraits;
+
+ this.onAnimationStateChanged = this.onAnimationStateChanged.bind(this);
+ this.onScrubberMouseDown = this.onScrubberMouseDown.bind(this);
+ this.onScrubberMouseUp = this.onScrubberMouseUp.bind(this);
+ this.onScrubberMouseOut = this.onScrubberMouseOut.bind(this);
+ this.onScrubberMouseMove = this.onScrubberMouseMove.bind(this);
+ this.onAnimationSelected = this.onAnimationSelected.bind(this);
+ this.onWindowResize = this.onWindowResize.bind(this);
+ this.onFrameSelected = this.onFrameSelected.bind(this);
+
+ EventEmitter.decorate(this);
+}
+
+exports.AnimationsTimeline = AnimationsTimeline;
+
+AnimationsTimeline.prototype = {
+ init: function (containerEl) {
+ this.win = containerEl.ownerDocument.defaultView;
+
+ this.rootWrapperEl = createNode({
+ parent: containerEl,
+ attributes: {
+ "class": "animation-timeline"
+ }
+ });
+
+ let scrubberContainer = createNode({
+ parent: this.rootWrapperEl,
+ attributes: {"class": "scrubber-wrapper"}
+ });
+
+ this.scrubberEl = createNode({
+ parent: scrubberContainer,
+ attributes: {
+ "class": "scrubber"
+ }
+ });
+
+ this.scrubberHandleEl = createNode({
+ parent: this.scrubberEl,
+ attributes: {
+ "class": "scrubber-handle"
+ }
+ });
+ this.scrubberHandleEl.addEventListener("mousedown",
+ this.onScrubberMouseDown);
+
+ this.headerWrapper = createNode({
+ parent: this.rootWrapperEl,
+ attributes: {
+ "class": "header-wrapper"
+ }
+ });
+
+ this.timeHeaderEl = createNode({
+ parent: this.headerWrapper,
+ attributes: {
+ "class": "time-header track-container"
+ }
+ });
+
+ this.timeHeaderEl.addEventListener("mousedown",
+ this.onScrubberMouseDown);
+
+ this.timeTickEl = createNode({
+ parent: this.rootWrapperEl,
+ attributes: {
+ "class": "time-body track-container"
+ }
+ });
+
+ this.animationsEl = createNode({
+ parent: this.rootWrapperEl,
+ nodeType: "ul",
+ attributes: {
+ "class": "animations"
+ }
+ });
+
+ this.win.addEventListener("resize",
+ this.onWindowResize);
+ },
+
+ destroy: function () {
+ this.stopAnimatingScrubber();
+ this.unrender();
+
+ this.win.removeEventListener("resize",
+ this.onWindowResize);
+ this.timeHeaderEl.removeEventListener("mousedown",
+ this.onScrubberMouseDown);
+ this.scrubberHandleEl.removeEventListener("mousedown",
+ this.onScrubberMouseDown);
+
+ this.rootWrapperEl.remove();
+ this.animations = [];
+
+ this.rootWrapperEl = null;
+ this.timeHeaderEl = null;
+ this.animationsEl = null;
+ this.scrubberEl = null;
+ this.scrubberHandleEl = null;
+ this.win = null;
+ this.inspector = null;
+ this.serverTraits = null;
+ },
+
+ /**
+ * Destroy sub-components that have been created and stored on this instance.
+ * @param {String} name An array of components will be expected in this[name]
+ * @param {Array} handlers An option list of event handlers information that
+ * should be used to remove these handlers.
+ */
+ destroySubComponents: function (name, handlers = []) {
+ for (let component of this[name]) {
+ for (let {event, fn} of handlers) {
+ component.off(event, fn);
+ }
+ component.destroy();
+ }
+ this[name] = [];
+ },
+
+ unrender: function () {
+ for (let animation of this.animations) {
+ animation.off("changed", this.onAnimationStateChanged);
+ }
+ this.stopAnimatingScrubber();
+ TimeScale.reset();
+ this.destroySubComponents("targetNodes");
+ this.destroySubComponents("timeBlocks");
+ this.destroySubComponents("details", [{
+ event: "frame-selected",
+ fn: this.onFrameSelected
+ }]);
+ this.animationsEl.innerHTML = "";
+ },
+
+ onWindowResize: function () {
+ // Don't do anything if the root element has a width of 0
+ if (this.rootWrapperEl.offsetWidth === 0) {
+ return;
+ }
+
+ if (this.windowResizeTimer) {
+ this.win.clearTimeout(this.windowResizeTimer);
+ }
+
+ this.windowResizeTimer = this.win.setTimeout(() => {
+ this.drawHeaderAndBackground();
+ }, TIMELINE_BACKGROUND_RESIZE_DEBOUNCE_TIMER);
+ },
+
+ onAnimationSelected: function (e, animation) {
+ let index = this.animations.indexOf(animation);
+ if (index === -1) {
+ return;
+ }
+
+ let el = this.rootWrapperEl;
+ let animationEl = el.querySelectorAll(".animation")[index];
+ let propsEl = el.querySelectorAll(".animated-properties")[index];
+
+ // Toggle the selected state on this animation.
+ animationEl.classList.toggle("selected");
+ propsEl.classList.toggle("selected");
+
+ // Render the details component for this animation if it was shown.
+ if (animationEl.classList.contains("selected")) {
+ this.details[index].render(animation);
+ this.emit("animation-selected", animation);
+ } else {
+ this.emit("animation-unselected", animation);
+ }
+ },
+
+ /**
+ * When a frame gets selected, move the scrubber to the corresponding position
+ */
+ onFrameSelected: function (e, {x}) {
+ this.moveScrubberTo(x, true);
+ },
+
+ onScrubberMouseDown: function (e) {
+ this.moveScrubberTo(e.pageX);
+ this.win.addEventListener("mouseup", this.onScrubberMouseUp);
+ this.win.addEventListener("mouseout", this.onScrubberMouseOut);
+ this.win.addEventListener("mousemove", this.onScrubberMouseMove);
+
+ // Prevent text selection while dragging.
+ e.preventDefault();
+ },
+
+ onScrubberMouseUp: function () {
+ this.cancelTimeHeaderDragging();
+ },
+
+ onScrubberMouseOut: function (e) {
+ // Check that mouseout happened on the window itself, and if yes, cancel
+ // the dragging.
+ if (!this.win.document.contains(e.relatedTarget)) {
+ this.cancelTimeHeaderDragging();
+ }
+ },
+
+ cancelTimeHeaderDragging: function () {
+ this.win.removeEventListener("mouseup", this.onScrubberMouseUp);
+ this.win.removeEventListener("mouseout", this.onScrubberMouseOut);
+ this.win.removeEventListener("mousemove", this.onScrubberMouseMove);
+ },
+
+ onScrubberMouseMove: function (e) {
+ this.moveScrubberTo(e.pageX);
+ },
+
+ moveScrubberTo: function (pageX, noOffset) {
+ this.stopAnimatingScrubber();
+
+ // The offset needs to be in % and relative to the timeline's area (so we
+ // subtract the scrubber's left offset, which is equal to the sidebar's
+ // width).
+ let offset = pageX;
+ if (!noOffset) {
+ offset -= this.timeHeaderEl.offsetLeft;
+ }
+ offset = offset * 100 / this.timeHeaderEl.offsetWidth;
+ if (offset < 0) {
+ offset = 0;
+ }
+
+ this.scrubberEl.style.left = offset + "%";
+
+ let time = TimeScale.distanceToRelativeTime(offset);
+
+ this.emit("timeline-data-changed", {
+ isPaused: true,
+ isMoving: false,
+ isUserDrag: true,
+ time: time
+ });
+ },
+
+ getCompositorStatusClassName: function (state) {
+ let className = state.isRunningOnCompositor
+ ? " fast-track"
+ : "";
+
+ if (state.isRunningOnCompositor && state.propertyState) {
+ className +=
+ state.propertyState.some(propState => !propState.runningOnCompositor)
+ ? " some-properties"
+ : " all-properties";
+ }
+
+ return className;
+ },
+
+ render: function (animations, documentCurrentTime) {
+ this.unrender();
+
+ this.animations = animations;
+ if (!this.animations.length) {
+ return;
+ }
+
+ // Loop first to set the time scale for all current animations.
+ for (let {state} of animations) {
+ TimeScale.addAnimation(state);
+ }
+
+ this.drawHeaderAndBackground();
+
+ for (let animation of this.animations) {
+ animation.on("changed", this.onAnimationStateChanged);
+ // Each line contains the target animated node and the animation time
+ // block.
+ let animationEl = createNode({
+ parent: this.animationsEl,
+ nodeType: "li",
+ attributes: {
+ "class": "animation " +
+ animation.state.type +
+ this.getCompositorStatusClassName(animation.state)
+ }
+ });
+
+ // Right below the line is a hidden-by-default line for displaying the
+ // inline keyframes.
+ let detailsEl = createNode({
+ parent: this.animationsEl,
+ nodeType: "li",
+ attributes: {
+ "class": "animated-properties " + animation.state.type
+ }
+ });
+
+ let details = new AnimationDetails(this.serverTraits);
+ details.init(detailsEl);
+ details.on("frame-selected", this.onFrameSelected);
+ this.details.push(details);
+
+ // Left sidebar for the animated node.
+ let animatedNodeEl = createNode({
+ parent: animationEl,
+ attributes: {
+ "class": "target"
+ }
+ });
+
+ // Draw the animated node target.
+ let targetNode = new AnimationTargetNode(this.inspector, {compact: true});
+ targetNode.init(animatedNodeEl);
+ targetNode.render(animation);
+ this.targetNodes.push(targetNode);
+
+ // Right-hand part contains the timeline itself (called time-block here).
+ let timeBlockEl = createNode({
+ parent: animationEl,
+ attributes: {
+ "class": "time-block track-container"
+ }
+ });
+
+ // Draw the animation time block.
+ let timeBlock = new AnimationTimeBlock();
+ timeBlock.init(timeBlockEl);
+ timeBlock.render(animation);
+ this.timeBlocks.push(timeBlock);
+
+ timeBlock.on("selected", this.onAnimationSelected);
+ }
+
+ // Use the document's current time to position the scrubber (if the server
+ // doesn't provide it, hide the scrubber entirely).
+ // Note that because the currentTime was sent via the protocol, some time
+ // may have gone by since then, and so the scrubber might be a bit late.
+ if (!documentCurrentTime) {
+ this.scrubberEl.style.display = "none";
+ } else {
+ this.scrubberEl.style.display = "block";
+ this.startAnimatingScrubber(this.wasRewound()
+ ? TimeScale.minStartTime
+ : documentCurrentTime);
+ }
+ },
+
+ isAtLeastOneAnimationPlaying: function () {
+ return this.animations.some(({state}) => state.playState === "running");
+ },
+
+ wasRewound: function () {
+ return !this.isAtLeastOneAnimationPlaying() &&
+ this.animations.every(({state}) => state.currentTime === 0);
+ },
+
+ hasInfiniteAnimations: function () {
+ return this.animations.some(({state}) => !state.iterationCount);
+ },
+
+ startAnimatingScrubber: function (time) {
+ let isOutOfBounds = time < TimeScale.minStartTime ||
+ time > TimeScale.maxEndTime;
+ let isAllPaused = !this.isAtLeastOneAnimationPlaying();
+ let hasInfinite = this.hasInfiniteAnimations();
+
+ let x = TimeScale.startTimeToDistance(time);
+ if (x > 100 && !hasInfinite) {
+ x = 100;
+ }
+ this.scrubberEl.style.left = x + "%";
+
+ // Only stop the scrubber if it's out of bounds or all animations have been
+ // paused, but not if at least an animation is infinite.
+ if (isAllPaused || (isOutOfBounds && !hasInfinite)) {
+ this.stopAnimatingScrubber();
+ this.emit("timeline-data-changed", {
+ isPaused: !this.isAtLeastOneAnimationPlaying(),
+ isMoving: false,
+ isUserDrag: false,
+ time: TimeScale.distanceToRelativeTime(x)
+ });
+ return;
+ }
+
+ this.emit("timeline-data-changed", {
+ isPaused: false,
+ isMoving: true,
+ isUserDrag: false,
+ time: TimeScale.distanceToRelativeTime(x)
+ });
+
+ let now = this.win.performance.now();
+ this.rafID = this.win.requestAnimationFrame(() => {
+ if (!this.rafID) {
+ // In case the scrubber was stopped in the meantime.
+ return;
+ }
+ this.startAnimatingScrubber(time + this.win.performance.now() - now);
+ });
+ },
+
+ stopAnimatingScrubber: function () {
+ if (this.rafID) {
+ this.win.cancelAnimationFrame(this.rafID);
+ this.rafID = null;
+ }
+ },
+
+ onAnimationStateChanged: function () {
+ // For now, simply re-render the component. The animation front's state has
+ // already been updated.
+ this.render(this.animations);
+ },
+
+ drawHeaderAndBackground: function () {
+ let width = this.timeHeaderEl.offsetWidth;
+ let animationDuration = TimeScale.maxEndTime - TimeScale.minStartTime;
+ let minTimeInterval = TIME_GRADUATION_MIN_SPACING *
+ animationDuration / width;
+ let intervalLength = findOptimalTimeInterval(minTimeInterval);
+ let intervalWidth = intervalLength * width / animationDuration;
+
+ // And the time graduation header.
+ this.timeHeaderEl.innerHTML = "";
+ this.timeTickEl.innerHTML = "";
+
+ for (let i = 0; i <= width / intervalWidth; i++) {
+ let pos = 100 * i * intervalWidth / width;
+
+ // This element is the header of time tick for displaying animation
+ // duration time.
+ createNode({
+ parent: this.timeHeaderEl,
+ nodeType: "span",
+ attributes: {
+ "class": "header-item",
+ "style": `left:${pos}%`
+ },
+ textContent: TimeScale.formatTime(TimeScale.distanceToRelativeTime(pos))
+ });
+
+ // This element is displayed as a vertical line separator corresponding
+ // the header of time tick for indicating time slice for animation
+ // iterations.
+ createNode({
+ parent: this.timeTickEl,
+ nodeType: "span",
+ attributes: {
+ "class": "time-tick",
+ "style": `left:${pos}%`
+ }
+ });
+ }
+ }
+};
diff --git a/devtools/client/animationinspector/components/keyframes.js b/devtools/client/animationinspector/components/keyframes.js
new file mode 100644
index 000000000..a017935a3
--- /dev/null
+++ b/devtools/client/animationinspector/components/keyframes.js
@@ -0,0 +1,81 @@
+/* -*- 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 EventEmitter = require("devtools/shared/event-emitter");
+const {createNode} = require("devtools/client/animationinspector/utils");
+
+/**
+ * UI component responsible for displaying a list of keyframes.
+ */
+function Keyframes() {
+ EventEmitter.decorate(this);
+ this.onClick = this.onClick.bind(this);
+}
+
+exports.Keyframes = Keyframes;
+
+Keyframes.prototype = {
+ init: function (containerEl) {
+ this.containerEl = containerEl;
+
+ this.keyframesEl = createNode({
+ parent: this.containerEl,
+ attributes: {"class": "keyframes"}
+ });
+
+ this.containerEl.addEventListener("click", this.onClick);
+ },
+
+ destroy: function () {
+ this.containerEl.removeEventListener("click", this.onClick);
+ this.keyframesEl.remove();
+ this.containerEl = this.keyframesEl = this.animation = null;
+ },
+
+ render: function ({keyframes, propertyName, animation}) {
+ this.keyframes = keyframes;
+ this.propertyName = propertyName;
+ this.animation = animation;
+
+ let iterationStartOffset =
+ animation.state.iterationStart % 1 == 0
+ ? 0
+ : 1 - animation.state.iterationStart % 1;
+
+ this.keyframesEl.classList.add(animation.state.type);
+ for (let frame of this.keyframes) {
+ let offset = frame.offset + iterationStartOffset;
+ createNode({
+ parent: this.keyframesEl,
+ attributes: {
+ "class": "frame",
+ "style": `left:${offset * 100}%;`,
+ "data-offset": frame.offset,
+ "data-property": propertyName,
+ "title": frame.value
+ }
+ });
+ }
+ },
+
+ onClick: function (e) {
+ // If the click happened on a frame, tell our parent about it.
+ if (!e.target.classList.contains("frame")) {
+ return;
+ }
+
+ e.stopPropagation();
+ this.emit("frame-selected", {
+ animation: this.animation,
+ propertyName: this.propertyName,
+ offset: parseFloat(e.target.dataset.offset),
+ value: e.target.getAttribute("title"),
+ x: e.target.offsetLeft + e.target.closest(".frames").offsetLeft
+ });
+ }
+};
diff --git a/devtools/client/animationinspector/components/moz.build b/devtools/client/animationinspector/components/moz.build
new file mode 100644
index 000000000..2265f8c28
--- /dev/null
+++ b/devtools/client/animationinspector/components/moz.build
@@ -0,0 +1,12 @@
+# 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/.
+
+DevToolsModules(
+ 'animation-details.js',
+ 'animation-target-node.js',
+ 'animation-time-block.js',
+ 'animation-timeline.js',
+ 'keyframes.js',
+ 'rate-selector.js'
+)
diff --git a/devtools/client/animationinspector/components/rate-selector.js b/devtools/client/animationinspector/components/rate-selector.js
new file mode 100644
index 000000000..e46664e6a
--- /dev/null
+++ b/devtools/client/animationinspector/components/rate-selector.js
@@ -0,0 +1,105 @@
+/* -*- 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 EventEmitter = require("devtools/shared/event-emitter");
+const {createNode} = require("devtools/client/animationinspector/utils");
+const { LocalizationHelper } = require("devtools/shared/l10n");
+const L10N =
+ new LocalizationHelper("devtools/client/locales/animationinspector.properties");
+
+// List of playback rate presets displayed in the timeline toolbar.
+const PLAYBACK_RATES = [.1, .25, .5, 1, 2, 5, 10];
+
+/**
+ * UI component responsible for displaying a playback rate selector UI.
+ * The rendering logic is such that a predefined list of rates is generated.
+ * If *all* animations passed to render share the same rate, then that rate is
+ * selected in the <select> element, otherwise, the empty value is selected.
+ * If the rate that all animations share isn't part of the list of predefined
+ * rates, than that rate is added to the list.
+ */
+function RateSelector() {
+ this.onRateChanged = this.onRateChanged.bind(this);
+ EventEmitter.decorate(this);
+}
+
+exports.RateSelector = RateSelector;
+
+RateSelector.prototype = {
+ init: function (containerEl) {
+ this.selectEl = createNode({
+ parent: containerEl,
+ nodeType: "select",
+ attributes: {
+ "class": "devtools-button",
+ "title": L10N.getStr("timeline.rateSelectorTooltip")
+ }
+ });
+
+ this.selectEl.addEventListener("change", this.onRateChanged);
+ },
+
+ destroy: function () {
+ this.selectEl.removeEventListener("change", this.onRateChanged);
+ this.selectEl.remove();
+ this.selectEl = null;
+ },
+
+ getAnimationsRates: function (animations) {
+ return sortedUnique(animations.map(a => a.state.playbackRate));
+ },
+
+ getAllRates: function (animations) {
+ let animationsRates = this.getAnimationsRates(animations);
+ if (animationsRates.length > 1) {
+ return PLAYBACK_RATES;
+ }
+
+ return sortedUnique(PLAYBACK_RATES.concat(animationsRates));
+ },
+
+ render: function (animations) {
+ let allRates = this.getAnimationsRates(animations);
+ let hasOneRate = allRates.length === 1;
+
+ this.selectEl.innerHTML = "";
+
+ if (!hasOneRate) {
+ // When the animations displayed have mixed playback rates, we can't
+ // select any of the predefined ones, instead, insert an empty rate.
+ createNode({
+ parent: this.selectEl,
+ nodeType: "option",
+ attributes: {value: "", selector: "true"},
+ textContent: "-"
+ });
+ }
+ for (let rate of this.getAllRates(animations)) {
+ let option = createNode({
+ parent: this.selectEl,
+ nodeType: "option",
+ attributes: {value: rate},
+ textContent: L10N.getFormatStr("player.playbackRateLabel", rate)
+ });
+
+ // If there's only one rate and this is the option for it, select it.
+ if (hasOneRate && rate === allRates[0]) {
+ option.setAttribute("selected", "true");
+ }
+ }
+ },
+
+ onRateChanged: function () {
+ let rate = parseFloat(this.selectEl.value);
+ if (!isNaN(rate)) {
+ this.emit("rate-changed", rate);
+ }
+ }
+};
+
+let sortedUnique = arr => [...new Set(arr)].sort((a, b) => a > b);
diff --git a/devtools/client/animationinspector/moz.build b/devtools/client/animationinspector/moz.build
new file mode 100644
index 000000000..60527da7d
--- /dev/null
+++ b/devtools/client/animationinspector/moz.build
@@ -0,0 +1,16 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
+XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
+
+DIRS += [
+ 'components'
+]
+
+DevToolsModules(
+ 'utils.js',
+)
diff --git a/devtools/client/animationinspector/test/.eslintrc.js b/devtools/client/animationinspector/test/.eslintrc.js
new file mode 100644
index 000000000..8d15a76d9
--- /dev/null
+++ b/devtools/client/animationinspector/test/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../../.eslintrc.mochitests.js"
+};
diff --git a/devtools/client/animationinspector/test/browser.ini b/devtools/client/animationinspector/test/browser.ini
new file mode 100644
index 000000000..08bce344d
--- /dev/null
+++ b/devtools/client/animationinspector/test/browser.ini
@@ -0,0 +1,71 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ doc_body_animation.html
+ doc_end_delay.html
+ doc_frame_script.js
+ doc_keyframes.html
+ doc_modify_playbackRate.html
+ doc_negative_animation.html
+ doc_pseudo_elements.html
+ doc_script_animation.html
+ doc_simple_animation.html
+ doc_multiple_animation_types.html
+ doc_timing_combination_animation.html
+ head.js
+ !/devtools/client/commandline/test/helpers.js
+ !/devtools/client/framework/test/shared-head.js
+ !/devtools/client/inspector/test/head.js
+ !/devtools/client/inspector/test/shared-head.js
+ !/devtools/client/shared/test/test-actor-registry.js
+ !/devtools/client/shared/test/test-actor.js
+
+[browser_animation_animated_properties_displayed.js]
+[browser_animation_click_selects_animation.js]
+[browser_animation_controller_exposes_document_currentTime.js]
+skip-if = os == "linux" && !debug # Bug 1234567
+[browser_animation_empty_on_invalid_nodes.js]
+[browser_animation_keyframe_click_to_set_time.js]
+[browser_animation_keyframe_markers.js]
+[browser_animation_mutations_with_same_names.js]
+[browser_animation_panel_exists.js]
+[browser_animation_participate_in_inspector_update.js]
+[browser_animation_playerFronts_are_refreshed.js]
+[browser_animation_playerWidgets_appear_on_panel_init.js]
+[browser_animation_playerWidgets_target_nodes.js]
+[browser_animation_pseudo_elements.js]
+[browser_animation_refresh_on_added_animation.js]
+[browser_animation_refresh_on_removed_animation.js]
+skip-if = os == "linux" && !debug # Bug 1227792
+[browser_animation_refresh_when_active.js]
+[browser_animation_running_on_compositor.js]
+[browser_animation_same_nb_of_playerWidgets_and_playerFronts.js]
+[browser_animation_shows_player_on_valid_node.js]
+[browser_animation_spacebar_toggles_animations.js]
+[browser_animation_spacebar_toggles_node_animations.js]
+[browser_animation_target_highlight_select.js]
+[browser_animation_target_highlighter_lock.js]
+[browser_animation_timeline_currentTime.js]
+[browser_animation_timeline_header.js]
+[browser_animation_timeline_iterationStart.js]
+[browser_animation_timeline_pause_button_01.js]
+[browser_animation_timeline_pause_button_02.js]
+[browser_animation_timeline_pause_button_03.js]
+[browser_animation_timeline_rate_selector.js]
+[browser_animation_timeline_rewind_button.js]
+[browser_animation_timeline_scrubber_exists.js]
+[browser_animation_timeline_scrubber_movable.js]
+[browser_animation_timeline_scrubber_moves.js]
+[browser_animation_timeline_setCurrentTime.js]
+[browser_animation_timeline_shows_delay.js]
+[browser_animation_timeline_shows_endDelay.js]
+[browser_animation_timeline_shows_iterations.js]
+[browser_animation_timeline_shows_name_label.js]
+[browser_animation_timeline_shows_time_info.js]
+[browser_animation_timeline_takes_rate_into_account.js]
+[browser_animation_timeline_ui.js]
+[browser_animation_toggle_button_resets_on_navigate.js]
+[browser_animation_toggle_button_toggles_animations.js]
+[browser_animation_toolbar_exists.js]
+[browser_animation_ui_updates_when_animation_data_changes.js]
diff --git a/devtools/client/animationinspector/test/browser_animation_animated_properties_displayed.js b/devtools/client/animationinspector/test/browser_animation_animated_properties_displayed.js
new file mode 100644
index 000000000..214a33bd4
--- /dev/null
+++ b/devtools/client/animationinspector/test/browser_animation_animated_properties_displayed.js
@@ -0,0 +1,91 @@
+/* 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 LAYOUT_ERRORS_L10N =
+ new LocalizationHelper("toolkit/locales/layout_errors.properties");
+
+// Test that when an animation is selected, its list of animated properties is
+// displayed below it.
+
+const EXPECTED_PROPERTIES = [
+ "background-attachment",
+ "background-clip",
+ "background-color",
+ "background-image",
+ "background-origin",
+ "background-position-x",
+ "background-position-y",
+ "background-repeat",
+ "background-size",
+ "border-bottom-left-radius",
+ "border-bottom-right-radius",
+ "border-top-left-radius",
+ "border-top-right-radius",
+ "filter",
+ "height",
+ "transform",
+ "width"
+].sort();
+
+add_task(function* () {
+ yield addTab(URL_ROOT + "doc_keyframes.html");
+ let {panel} = yield openAnimationInspector();
+ let timeline = panel.animationsTimelineComponent;
+ let propertiesList = timeline.rootWrapperEl
+ .querySelector(".animated-properties");
+
+ ok(!isNodeVisible(propertiesList),
+ "The list of properties panel is hidden by default");
+
+ info("Click to select the animation");
+ yield clickOnAnimation(panel, 0);
+
+ ok(isNodeVisible(propertiesList),
+ "The list of properties panel is shown");
+ ok(propertiesList.querySelectorAll(".property").length,
+ "The list of properties panel actually contains properties");
+ ok(hasExpectedProperties(propertiesList),
+ "The list of properties panel contains the right properties");
+
+ ok(hasExpectedWarnings(propertiesList),
+ "The list of properties panel contains the right warnings");
+
+ info("Click to unselect the animation");
+ yield clickOnAnimation(panel, 0, true);
+
+ ok(!isNodeVisible(propertiesList),
+ "The list of properties panel is hidden again");
+});
+
+function hasExpectedProperties(containerEl) {
+ let names = [...containerEl.querySelectorAll(".property .name")]
+ .map(n => n.textContent)
+ .sort();
+
+ if (names.length !== EXPECTED_PROPERTIES.length) {
+ return false;
+ }
+
+ for (let i = 0; i < names.length; i++) {
+ if (names[i] !== EXPECTED_PROPERTIES[i]) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+function hasExpectedWarnings(containerEl) {
+ let warnings = [...containerEl.querySelectorAll(".warning")];
+ for (let warning of warnings) {
+ let warningID =
+ "CompositorAnimationWarningTransformWithGeometricProperties";
+ if (warning.getAttribute("title") == LAYOUT_ERRORS_L10N.getStr(warningID)) {
+ return true;
+ }
+ }
+ return false;
+}
diff --git a/devtools/client/animationinspector/test/browser_animation_click_selects_animation.js b/devtools/client/animationinspector/test/browser_animation_click_selects_animation.js
new file mode 100644
index 000000000..d6d393d5a
--- /dev/null
+++ b/devtools/client/animationinspector/test/browser_animation_click_selects_animation.js
@@ -0,0 +1,44 @@
+/* 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";
+
+// Check that animations displayed in the timeline can be selected by clicking
+// them, and that this emits the right events and adds the right classes.
+
+add_task(function* () {
+ yield addTab(URL_ROOT + "doc_simple_animation.html");
+ let {panel} = yield openAnimationInspector();
+ let timeline = panel.animationsTimelineComponent;
+
+ let selected = timeline.rootWrapperEl.querySelectorAll(".animation.selected");
+ ok(!selected.length, "There are no animations selected by default");
+
+ info("Click on the first animation, expect the right event and right class");
+ let animation0 = yield clickOnAnimation(panel, 0);
+ is(animation0, timeline.animations[0],
+ "The selected event was emitted with the right animation");
+ ok(isTimeBlockSelected(timeline, 0),
+ "The time block has the right selected class");
+
+ info("Click on the second animation, expect it to be selected too");
+ let animation1 = yield clickOnAnimation(panel, 1);
+ is(animation1, timeline.animations[1],
+ "The selected event was emitted with the right animation");
+ ok(isTimeBlockSelected(timeline, 1),
+ "The second time block has the right selected class");
+
+ info("Click again on the first animation and check if it unselects");
+ yield clickOnAnimation(panel, 0, true);
+ ok(!isTimeBlockSelected(timeline, 0),
+ "The first time block has been unselected");
+});
+
+function isTimeBlockSelected(timeline, index) {
+ let animation = timeline.rootWrapperEl.querySelectorAll(".animation")[index];
+ let animatedProperties = timeline.rootWrapperEl.querySelectorAll(
+ ".animated-properties")[index];
+ return animation.classList.contains("selected") &&
+ animatedProperties.classList.contains("selected");
+}
diff --git a/devtools/client/animationinspector/test/browser_animation_controller_exposes_document_currentTime.js b/devtools/client/animationinspector/test/browser_animation_controller_exposes_document_currentTime.js
new file mode 100644
index 000000000..ae970a426
--- /dev/null
+++ b/devtools/client/animationinspector/test/browser_animation_controller_exposes_document_currentTime.js
@@ -0,0 +1,43 @@
+/* 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";
+
+// Test that the controller provides the document.timeline currentTime (at least
+// the last known version since new animations were added).
+
+add_task(function* () {
+ yield addTab(URL_ROOT + "doc_simple_animation.html");
+ let {panel, controller} = yield openAnimationInspector();
+
+ ok(controller.documentCurrentTime, "The documentCurrentTime getter exists");
+ checkDocumentTimeIsCorrect(controller);
+ let time1 = controller.documentCurrentTime;
+
+ yield startNewAnimation(controller, panel);
+ checkDocumentTimeIsCorrect(controller);
+ let time2 = controller.documentCurrentTime;
+ ok(time2 > time1, "The new documentCurrentTime is higher than the old one");
+});
+
+function checkDocumentTimeIsCorrect(controller) {
+ let time = 0;
+ for (let {state} of controller.animationPlayers) {
+ time = Math.max(time, state.documentCurrentTime);
+ }
+ is(controller.documentCurrentTime, time,
+ "The documentCurrentTime is correct");
+}
+
+function* startNewAnimation(controller, panel) {
+ info("Add a new animation to the page and check the time again");
+ let onPlayerAdded = controller.once(controller.PLAYERS_UPDATED_EVENT);
+ yield executeInContent("devtools:test:setAttribute", {
+ selector: ".still",
+ attributeName: "class",
+ attributeValue: "ball still short"
+ });
+ yield onPlayerAdded;
+ yield waitForAllAnimationTargets(panel);
+}
diff --git a/devtools/client/animationinspector/test/browser_animation_empty_on_invalid_nodes.js b/devtools/client/animationinspector/test/browser_animation_empty_on_invalid_nodes.js
new file mode 100644
index 000000000..9fda89a9a
--- /dev/null
+++ b/devtools/client/animationinspector/test/browser_animation_empty_on_invalid_nodes.js
@@ -0,0 +1,42 @@
+/* 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";
+
+requestLongerTimeout(2);
+
+// Test that the panel shows no animation data for invalid or not animated nodes
+
+add_task(function* () {
+ yield addTab(URL_ROOT + "doc_simple_animation.html");
+ let {inspector, panel, window} = yield openAnimationInspector();
+ let {document} = window;
+
+ info("Select node .still and check that the panel is empty");
+ let stillNode = yield getNodeFront(".still", inspector);
+ let onUpdated = panel.once(panel.UI_UPDATED_EVENT);
+ yield selectNodeAndWaitForAnimations(stillNode, inspector);
+ yield onUpdated;
+
+ is(panel.animationsTimelineComponent.animations.length, 0,
+ "No animation players stored in the timeline component for a still node");
+ is(panel.animationsTimelineComponent.animationsEl.childNodes.length, 0,
+ "No animation displayed in the timeline component for a still node");
+ is(document.querySelector("#error-type").textContent,
+ ANIMATION_L10N.getStr("panel.invalidElementSelected"),
+ "The correct error message is displayed");
+
+ info("Select the comment text node and check that the panel is empty");
+ let commentNode = yield inspector.walker.previousSibling(stillNode);
+ onUpdated = panel.once(panel.UI_UPDATED_EVENT);
+ yield selectNodeAndWaitForAnimations(commentNode, inspector);
+ yield onUpdated;
+
+ is(panel.animationsTimelineComponent.animations.length, 0,
+ "No animation players stored in the timeline component for a text node");
+ is(panel.animationsTimelineComponent.animationsEl.childNodes.length, 0,
+ "No animation displayed in the timeline component for a text node");
+ is(document.querySelector("#error-type").textContent,
+ ANIMATION_L10N.getStr("panel.invalidElementSelected"),
+ "The correct error message is displayed");
+});
diff --git a/devtools/client/animationinspector/test/browser_animation_keyframe_click_to_set_time.js b/devtools/client/animationinspector/test/browser_animation_keyframe_click_to_set_time.js
new file mode 100644
index 000000000..ba700b7a5
--- /dev/null
+++ b/devtools/client/animationinspector/test/browser_animation_keyframe_click_to_set_time.js
@@ -0,0 +1,52 @@
+/* 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";
+
+// Test that animated properties' keyframes can be clicked, and that doing so
+// sets the current time in the timeline.
+
+add_task(function* () {
+ yield addTab(URL_ROOT + "doc_keyframes.html");
+ let {panel} = yield openAnimationInspector();
+ let timeline = panel.animationsTimelineComponent;
+ let {scrubberEl} = timeline;
+
+ // XXX: The scrollbar is placed in the timeline in such a way that it causes
+ // the animations to be slightly offset with the header when it appears.
+ // So for now, let's hide the scrollbar. Bug 1229340 should fix this.
+ timeline.animationsEl.style.overflow = "hidden";
+
+ info("Expand the animation");
+ yield clickOnAnimation(panel, 0);
+
+ info("Click on the first keyframe of the first animated property");
+ yield clickKeyframe(panel, 0, "background-color", 0);
+
+ info("Make sure the scrubber stopped moving and is at the right position");
+ yield assertScrubberMoving(panel, false);
+ checkScrubberPos(scrubberEl, 0);
+
+ info("Click on a keyframe in the middle");
+ yield clickKeyframe(panel, 0, "transform", 2);
+
+ info("Make sure the scrubber is at the right position");
+ checkScrubberPos(scrubberEl, 50);
+});
+
+function* clickKeyframe(panel, animIndex, property, index) {
+ let keyframeComponent = getKeyframeComponent(panel, animIndex, property);
+ let keyframeEl = getKeyframeEl(panel, animIndex, property, index);
+
+ let onSelect = keyframeComponent.once("frame-selected");
+ EventUtils.sendMouseEvent({type: "click"}, keyframeEl,
+ keyframeEl.ownerDocument.defaultView);
+ yield onSelect;
+}
+
+function checkScrubberPos(scrubberEl, pos) {
+ let newPos = Math.round(parseFloat(scrubberEl.style.left));
+ let expectedPos = Math.round(pos);
+ is(newPos, expectedPos, `The scrubber is at ${pos}%`);
+}
diff --git a/devtools/client/animationinspector/test/browser_animation_keyframe_markers.js b/devtools/client/animationinspector/test/browser_animation_keyframe_markers.js
new file mode 100644
index 000000000..789c0efb6
--- /dev/null
+++ b/devtools/client/animationinspector/test/browser_animation_keyframe_markers.js
@@ -0,0 +1,74 @@
+/* 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";
+
+// Test that when an animation is selected and its list of properties is shown,
+// there are keyframes markers next to each property being animated.
+
+const EXPECTED_PROPERTIES = [
+ "backgroundColor",
+ "backgroundPosition",
+ "backgroundSize",
+ "borderBottomLeftRadius",
+ "borderBottomRightRadius",
+ "borderTopLeftRadius",
+ "borderTopRightRadius",
+ "filter",
+ "height",
+ "transform",
+ "width"
+];
+
+add_task(function* () {
+ yield addTab(URL_ROOT + "doc_keyframes.html");
+ let {panel} = yield openAnimationInspector();
+ let timeline = panel.animationsTimelineComponent;
+
+ info("Expand the animation");
+ yield clickOnAnimation(panel, 0);
+
+ ok(timeline.rootWrapperEl.querySelectorAll(".frames .keyframes").length,
+ "There are container elements for displaying keyframes");
+
+ let data = yield getExpectedKeyframesData(timeline.animations[0]);
+ for (let propertyName in data) {
+ info("Check the keyframe markers for " + propertyName);
+ let widthMarkerSelector = ".frame[data-property=" + propertyName + "]";
+ let markers = timeline.rootWrapperEl.querySelectorAll(widthMarkerSelector);
+
+ is(markers.length, data[propertyName].length,
+ "The right number of keyframes was found for " + propertyName);
+
+ let offsets = [...markers].map(m => parseFloat(m.dataset.offset));
+ let values = [...markers].map(m => m.dataset.value);
+ for (let i = 0; i < markers.length; i++) {
+ is(markers[i].dataset.offset, offsets[i],
+ "Marker " + i + " for " + propertyName + " has the right offset");
+ is(markers[i].dataset.value, values[i],
+ "Marker " + i + " for " + propertyName + " has the right value");
+ }
+ }
+});
+
+function* getExpectedKeyframesData(animation) {
+ // We're testing the UI state here, so it's fine to get the list of expected
+ // properties from the animation actor.
+ let properties = yield animation.getProperties();
+ let data = {};
+
+ for (let expectedProperty of EXPECTED_PROPERTIES) {
+ data[expectedProperty] = [];
+ for (let {name, values} of properties) {
+ if (name !== expectedProperty) {
+ continue;
+ }
+ for (let {offset, value} of values) {
+ data[expectedProperty].push({offset, value});
+ }
+ }
+ }
+
+ return data;
+}
diff --git a/devtools/client/animationinspector/test/browser_animation_mutations_with_same_names.js b/devtools/client/animationinspector/test/browser_animation_mutations_with_same_names.js
new file mode 100644
index 000000000..1ae19c277
--- /dev/null
+++ b/devtools/client/animationinspector/test/browser_animation_mutations_with_same_names.js
@@ -0,0 +1,31 @@
+/* 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";
+
+// Check that when animations are added later (through animation mutations) and
+// if these animations have the same names, then all of them are still being
+// displayed (which should be true as long as these animations apply to
+// different nodes).
+
+add_task(function* () {
+ yield addTab(URL_ROOT + "doc_negative_animation.html");
+ let {controller, panel} = yield openAnimationInspector();
+
+ info("Wait until all animations have been added " +
+ "(they're added with setTimeout)");
+ while (controller.animationPlayers.length < 3) {
+ yield controller.once(controller.PLAYERS_UPDATED_EVENT);
+ }
+ yield waitForAllAnimationTargets(panel);
+
+ is(panel.animationsTimelineComponent.animations.length, 3,
+ "The timeline shows 3 animations too");
+
+ // Reduce the known nodeFronts to a set to make them unique.
+ let nodeFronts = new Set(panel.animationsTimelineComponent
+ .targetNodes.map(n => n.previewer.nodeFront));
+ is(nodeFronts.size, 3,
+ "The animations are applied to 3 different node fronts");
+});
diff --git a/devtools/client/animationinspector/test/browser_animation_panel_exists.js b/devtools/client/animationinspector/test/browser_animation_panel_exists.js
new file mode 100644
index 000000000..1f12605a5
--- /dev/null
+++ b/devtools/client/animationinspector/test/browser_animation_panel_exists.js
@@ -0,0 +1,23 @@
+/* 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";
+
+// Test that the animation panel sidebar exists
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8,welcome to the animation panel");
+ let {panel, controller} = yield openAnimationInspector();
+
+ ok(controller,
+ "The animation controller exists");
+ ok(controller.animationsFront,
+ "The animation controller has been initialized");
+ ok(panel,
+ "The animation panel exists");
+ ok(panel.playersEl,
+ "The animation panel has been initialized");
+ ok(panel.animationsTimelineComponent,
+ "The animation panel has been initialized");
+});
diff --git a/devtools/client/animationinspector/test/browser_animation_participate_in_inspector_update.js b/devtools/client/animationinspector/test/browser_animation_participate_in_inspector_update.js
new file mode 100644
index 000000000..fec529568
--- /dev/null
+++ b/devtools/client/animationinspector/test/browser_animation_participate_in_inspector_update.js
@@ -0,0 +1,46 @@
+/* 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";
+
+requestLongerTimeout(2);
+
+// Test that the update of the animation panel participate in the
+// inspector-updated event. This means that the test verifies that the
+// inspector-updated event is emitted *after* the animation panel is ready.
+
+add_task(function* () {
+ yield addTab(URL_ROOT + "doc_simple_animation.html");
+ let {inspector, panel, controller} = yield openAnimationInspector();
+
+ info("Listen for the players-updated, ui-updated and " +
+ "inspector-updated events");
+ let receivedEvents = [];
+ controller.once(controller.PLAYERS_UPDATED_EVENT, () => {
+ receivedEvents.push(controller.PLAYERS_UPDATED_EVENT);
+ });
+ panel.once(panel.UI_UPDATED_EVENT, () => {
+ receivedEvents.push(panel.UI_UPDATED_EVENT);
+ });
+ inspector.once("inspector-updated", () => {
+ receivedEvents.push("inspector-updated");
+ });
+
+ info("Selecting an animated node");
+ let node = yield getNodeFront(".animated", inspector);
+ yield selectNodeAndWaitForAnimations(node, inspector);
+
+ info("Check that all events were received");
+ // Only assert that the inspector-updated event is last, the order of the
+ // first 2 events is irrelevant.
+
+ is(receivedEvents.length, 3, "3 events were received");
+ is(receivedEvents[2], "inspector-updated",
+ "The third event received was the inspector-updated event");
+
+ ok(receivedEvents.indexOf(controller.PLAYERS_UPDATED_EVENT) !== -1,
+ "The players-updated event was received");
+ ok(receivedEvents.indexOf(panel.UI_UPDATED_EVENT) !== -1,
+ "The ui-updated event was received");
+});
diff --git a/devtools/client/animationinspector/test/browser_animation_playerFronts_are_refreshed.js b/devtools/client/animationinspector/test/browser_animation_playerFronts_are_refreshed.js
new file mode 100644
index 000000000..7144adf6c
--- /dev/null
+++ b/devtools/client/animationinspector/test/browser_animation_playerFronts_are_refreshed.js
@@ -0,0 +1,36 @@
+/* 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";
+
+requestLongerTimeout(2);
+
+// Check that the AnimationPlayerFront objects lifecycle is managed by the
+// AnimationController.
+
+add_task(function* () {
+ yield addTab(URL_ROOT + "doc_simple_animation.html");
+ let {controller, inspector} = yield openAnimationInspector();
+
+ info("Selecting an animated node");
+ // selectNode waits for the inspector-updated event before resolving, which
+ // means the controller.PLAYERS_UPDATED_EVENT event has been emitted before
+ // and players are ready.
+ yield selectNodeAndWaitForAnimations(".animated", inspector);
+
+ is(controller.animationPlayers.length, 1,
+ "One AnimationPlayerFront has been created");
+
+ info("Selecting a node with mutliple animations");
+ yield selectNodeAndWaitForAnimations(".multi", inspector);
+
+ is(controller.animationPlayers.length, 2,
+ "2 AnimationPlayerFronts have been created");
+
+ info("Selecting a node with no animations");
+ yield selectNodeAndWaitForAnimations(".still", inspector);
+
+ is(controller.animationPlayers.length, 0,
+ "There are no more AnimationPlayerFront objects");
+});
diff --git a/devtools/client/animationinspector/test/browser_animation_playerWidgets_appear_on_panel_init.js b/devtools/client/animationinspector/test/browser_animation_playerWidgets_appear_on_panel_init.js
new file mode 100644
index 000000000..271b26df3
--- /dev/null
+++ b/devtools/client/animationinspector/test/browser_animation_playerWidgets_appear_on_panel_init.js
@@ -0,0 +1,41 @@
+/* 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";
+
+// Test that player widgets are displayed right when the animation panel is
+// initialized, if the selected node (<body> by default) is animated.
+
+const { ANIMATION_TYPES } = require("devtools/server/actors/animation");
+
+add_task(function* () {
+ yield addTab(URL_ROOT + "doc_multiple_animation_types.html");
+
+ let {panel} = yield openAnimationInspector();
+ is(panel.animationsTimelineComponent.animations.length, 3,
+ "Three animations are handled by the timeline after init");
+ assertAnimationsDisplayed(panel, 3,
+ "Three animations are displayed after init");
+ is(
+ panel.animationsTimelineComponent
+ .animationsEl
+ .querySelectorAll(`.animation.${ANIMATION_TYPES.SCRIPT_ANIMATION}`)
+ .length,
+ 1,
+ "One script-generated animation is displayed");
+ is(
+ panel.animationsTimelineComponent
+ .animationsEl
+ .querySelectorAll(`.animation.${ANIMATION_TYPES.CSS_ANIMATION}`)
+ .length,
+ 1,
+ "One CSS animation is displayed");
+ is(
+ panel.animationsTimelineComponent
+ .animationsEl
+ .querySelectorAll(`.animation.${ANIMATION_TYPES.CSS_TRANSITION}`)
+ .length,
+ 1,
+ "One CSS transition is displayed");
+});
diff --git a/devtools/client/animationinspector/test/browser_animation_playerWidgets_target_nodes.js b/devtools/client/animationinspector/test/browser_animation_playerWidgets_target_nodes.js
new file mode 100644
index 000000000..1fbaa7ae3
--- /dev/null
+++ b/devtools/client/animationinspector/test/browser_animation_playerWidgets_target_nodes.js
@@ -0,0 +1,33 @@
+/* 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";
+
+requestLongerTimeout(2);
+
+// Test that player widgets display information about target nodes
+
+add_task(function* () {
+ yield addTab(URL_ROOT + "doc_simple_animation.html");
+ let {inspector, panel} = yield openAnimationInspector();
+
+ info("Select the simple animated node");
+ yield selectNodeAndWaitForAnimations(".animated", inspector);
+
+ let targetNodeComponent = panel.animationsTimelineComponent.targetNodes[0];
+ let {previewer} = targetNodeComponent;
+
+ // Make sure to wait for the target-retrieved event if the nodeFront hasn't
+ // yet been retrieved by the TargetNodeComponent.
+ if (!previewer.nodeFront) {
+ yield targetNodeComponent.once("target-retrieved");
+ }
+
+ is(previewer.el.textContent, "div#.ball.animated",
+ "The target element's content is correct");
+
+ let highlighterEl = previewer.el.querySelector(".node-highlighter");
+ ok(highlighterEl,
+ "The icon to highlight the target element in the page exists");
+});
diff --git a/devtools/client/animationinspector/test/browser_animation_pseudo_elements.js b/devtools/client/animationinspector/test/browser_animation_pseudo_elements.js
new file mode 100644
index 000000000..38b2f10af
--- /dev/null
+++ b/devtools/client/animationinspector/test/browser_animation_pseudo_elements.js
@@ -0,0 +1,49 @@
+/* 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";
+
+// Test that animated pseudo-elements do show in the timeline.
+
+add_task(function* () {
+ yield addTab(URL_ROOT + "doc_pseudo_elements.html");
+ let {inspector, panel} = yield openAnimationInspector();
+ let timeline = panel.animationsTimelineComponent;
+
+ info("With <body> selected by default check the content of the timeline");
+ is(timeline.timeBlocks.length, 3, "There are 3 animations in the timeline");
+
+ let getTargetNodeText = index => {
+ let el = timeline.targetNodes[index].previewer.previewEl;
+ return [...el.childNodes]
+ .map(n => n.style.display === "none" ? "" : n.textContent)
+ .join("");
+ };
+
+ is(getTargetNodeText(0), "body", "The first animated node is <body>");
+ is(getTargetNodeText(1), "::before", "The second animated node is ::before");
+ is(getTargetNodeText(2), "::after", "The third animated node is ::after");
+
+ info("Getting the before and after nodeFronts");
+ let bodyContainer = yield getContainerForSelector("body", inspector);
+ let getBodyChildNodeFront = index => {
+ return bodyContainer.elt.children[1].childNodes[index].container.node;
+ };
+ let beforeNode = getBodyChildNodeFront(0);
+ let afterNode = getBodyChildNodeFront(1);
+
+ info("Select the ::before pseudo-element in the inspector");
+ yield selectNode(beforeNode, inspector);
+ is(timeline.timeBlocks.length, 1, "There is 1 animation in the timeline");
+ is(timeline.targetNodes[0].previewer.nodeFront,
+ inspector.selection.nodeFront,
+ "The right node front is displayed in the timeline");
+
+ info("Select the ::after pseudo-element in the inspector");
+ yield selectNode(afterNode, inspector);
+ is(timeline.timeBlocks.length, 1, "There is 1 animation in the timeline");
+ is(timeline.targetNodes[0].previewer.nodeFront,
+ inspector.selection.nodeFront,
+ "The right node front is displayed in the timeline");
+});
diff --git a/devtools/client/animationinspector/test/browser_animation_refresh_on_added_animation.js b/devtools/client/animationinspector/test/browser_animation_refresh_on_added_animation.js
new file mode 100644
index 000000000..0bc652476
--- /dev/null
+++ b/devtools/client/animationinspector/test/browser_animation_refresh_on_added_animation.js
@@ -0,0 +1,47 @@
+/* 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";
+
+requestLongerTimeout(2);
+
+// Test that the panel content refreshes when new animations are added.
+
+add_task(function* () {
+ yield addTab(URL_ROOT + "doc_simple_animation.html");
+ let {inspector, panel} = yield openAnimationInspector();
+
+ info("Select a non animated node");
+ yield selectNodeAndWaitForAnimations(".still", inspector);
+
+ assertAnimationsDisplayed(panel, 0);
+
+ info("Start an animation on the node");
+ yield changeElementAndWait({
+ selector: ".still",
+ attributeName: "class",
+ attributeValue: "ball animated"
+ }, panel, inspector);
+
+ assertAnimationsDisplayed(panel, 1);
+
+ info("Remove the animation class on the node");
+ yield changeElementAndWait({
+ selector: ".ball.animated",
+ attributeName: "class",
+ attributeValue: "ball still"
+ }, panel, inspector);
+
+ assertAnimationsDisplayed(panel, 0);
+});
+
+function* changeElementAndWait(options, panel, inspector) {
+ let onPanelUpdated = panel.once(panel.UI_UPDATED_EVENT);
+ let onInspectorUpdated = inspector.once("inspector-updated");
+
+ yield executeInContent("devtools:test:setAttribute", options);
+
+ yield promise.all([
+ onInspectorUpdated, onPanelUpdated, waitForAllAnimationTargets(panel)]);
+}
diff --git a/devtools/client/animationinspector/test/browser_animation_refresh_on_removed_animation.js b/devtools/client/animationinspector/test/browser_animation_refresh_on_removed_animation.js
new file mode 100644
index 000000000..011d4a086
--- /dev/null
+++ b/devtools/client/animationinspector/test/browser_animation_refresh_on_removed_animation.js
@@ -0,0 +1,50 @@
+/* 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";
+
+requestLongerTimeout(2);
+
+// Test that the panel content refreshes when animations are removed.
+
+add_task(function* () {
+ yield addTab(URL_ROOT + "doc_simple_animation.html");
+
+ let {inspector, panel} = yield openAnimationInspector();
+ yield testRefreshOnRemove(inspector, panel);
+});
+
+function* testRefreshOnRemove(inspector, panel) {
+ info("Select a animated node");
+ yield selectNodeAndWaitForAnimations(".animated", inspector);
+
+ assertAnimationsDisplayed(panel, 1);
+
+ info("Listen to the next UI update event");
+ let onPanelUpdated = panel.once(panel.UI_UPDATED_EVENT);
+
+ info("Remove the animation on the node by removing the class");
+ yield executeInContent("devtools:test:setAttribute", {
+ selector: ".animated",
+ attributeName: "class",
+ attributeValue: "ball still test-node"
+ });
+
+ yield onPanelUpdated;
+ ok(true, "The panel update event was fired");
+
+ assertAnimationsDisplayed(panel, 0);
+
+ info("Add an finite animation on the node again, and wait for it to appear");
+ onPanelUpdated = panel.once(panel.UI_UPDATED_EVENT);
+ yield executeInContent("devtools:test:setAttribute", {
+ selector: ".test-node",
+ attributeName: "class",
+ attributeValue: "ball short test-node"
+ });
+ yield onPanelUpdated;
+ yield waitForAllAnimationTargets(panel);
+
+ assertAnimationsDisplayed(panel, 1);
+}
diff --git a/devtools/client/animationinspector/test/browser_animation_refresh_when_active.js b/devtools/client/animationinspector/test/browser_animation_refresh_when_active.js
new file mode 100644
index 000000000..6fb244b1e
--- /dev/null
+++ b/devtools/client/animationinspector/test/browser_animation_refresh_when_active.js
@@ -0,0 +1,53 @@
+/* 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";
+
+requestLongerTimeout(2);
+
+// Test that the panel only refreshes when it is visible in the sidebar.
+
+add_task(function* () {
+ yield addTab(URL_ROOT + "doc_simple_animation.html");
+
+ let {inspector, panel} = yield openAnimationInspector();
+ yield testRefresh(inspector, panel);
+});
+
+function* testRefresh(inspector, panel) {
+ info("Select a non animated node");
+ yield selectNodeAndWaitForAnimations(".still", inspector);
+
+ info("Switch to the rule-view panel");
+ inspector.sidebar.select("ruleview");
+
+ info("Select the animated node now");
+ yield selectNodeAndWaitForAnimations(".animated", inspector);
+
+ assertAnimationsDisplayed(panel, 0,
+ "The panel doesn't show the animation data while inactive");
+
+ info("Switch to the animation panel");
+ inspector.sidebar.select("animationinspector");
+ yield panel.once(panel.UI_UPDATED_EVENT);
+
+ assertAnimationsDisplayed(panel, 1,
+ "The panel shows the animation data after selecting it");
+
+ info("Switch again to the rule-view");
+ inspector.sidebar.select("ruleview");
+
+ info("Select the non animated node again");
+ yield selectNodeAndWaitForAnimations(".still", inspector);
+
+ assertAnimationsDisplayed(panel, 1,
+ "The panel still shows the previous animation data since it is inactive");
+
+ info("Switch to the animation panel again");
+ inspector.sidebar.select("animationinspector");
+ yield panel.once(panel.UI_UPDATED_EVENT);
+
+ assertAnimationsDisplayed(panel, 0,
+ "The panel is now empty after refreshing");
+}
diff --git a/devtools/client/animationinspector/test/browser_animation_running_on_compositor.js b/devtools/client/animationinspector/test/browser_animation_running_on_compositor.js
new file mode 100644
index 000000000..b23479b6c
--- /dev/null
+++ b/devtools/client/animationinspector/test/browser_animation_running_on_compositor.js
@@ -0,0 +1,57 @@
+/* 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";
+
+requestLongerTimeout(2);
+
+// Test that when animations displayed in the timeline are running on the
+// compositor, they get a special icon and information in the tooltip.
+
+add_task(function* () {
+ yield addTab(URL_ROOT + "doc_simple_animation.html");
+ let {inspector, panel} = yield openAnimationInspector();
+ let timeline = panel.animationsTimelineComponent;
+
+ info("Select a test node we know has an animation running on the compositor");
+ yield selectNodeAndWaitForAnimations(".animated", inspector);
+
+ let animationEl = timeline.animationsEl.querySelector(".animation");
+ ok(animationEl.classList.contains("fast-track"),
+ "The animation element has the fast-track css class");
+ ok(hasTooltip(animationEl,
+ ANIMATION_L10N.getStr("player.allPropertiesOnCompositorTooltip")),
+ "The animation element has the right tooltip content");
+
+ info("Select a node we know doesn't have an animation on the compositor");
+ yield selectNodeAndWaitForAnimations(".no-compositor", inspector);
+
+ animationEl = timeline.animationsEl.querySelector(".animation");
+ ok(!animationEl.classList.contains("fast-track"),
+ "The animation element does not have the fast-track css class");
+ ok(!hasTooltip(animationEl,
+ ANIMATION_L10N.getStr("player.allPropertiesOnCompositorTooltip")),
+ "The animation element does not have oncompositor tooltip content");
+ ok(!hasTooltip(animationEl,
+ ANIMATION_L10N.getStr("player.somePropertiesOnCompositorTooltip")),
+ "The animation element does not have oncompositor tooltip content");
+
+ info("Select a node we know has animation on the compositor and not on the" +
+ " compositor");
+ yield selectNodeAndWaitForAnimations(".compositor-notall", inspector);
+
+ animationEl = timeline.animationsEl.querySelector(".animation");
+ ok(animationEl.classList.contains("fast-track"),
+ "The animation element has the fast-track css class");
+ ok(hasTooltip(animationEl,
+ ANIMATION_L10N.getStr("player.somePropertiesOnCompositorTooltip")),
+ "The animation element has the right tooltip content");
+});
+
+function hasTooltip(animationEl, expected) {
+ let el = animationEl.querySelector(".name");
+ let tooltip = el.getAttribute("title");
+
+ return tooltip.indexOf(expected) !== -1;
+}
diff --git a/devtools/client/animationinspector/test/browser_animation_same_nb_of_playerWidgets_and_playerFronts.js b/devtools/client/animationinspector/test/browser_animation_same_nb_of_playerWidgets_and_playerFronts.js
new file mode 100644
index 000000000..a3aa8974c
--- /dev/null
+++ b/devtools/client/animationinspector/test/browser_animation_same_nb_of_playerWidgets_and_playerFronts.js
@@ -0,0 +1,23 @@
+/* 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";
+
+requestLongerTimeout(2);
+
+// Check that when playerFronts are updated, the same number of playerWidgets
+// are created in the panel.
+
+add_task(function* () {
+ yield addTab(URL_ROOT + "doc_simple_animation.html");
+ let {inspector, panel, controller} = yield openAnimationInspector();
+ let timeline = panel.animationsTimelineComponent;
+
+ info("Selecting the test animated node again");
+ yield selectNodeAndWaitForAnimations(".multi", inspector);
+
+ is(controller.animationPlayers.length,
+ timeline.animationsEl.querySelectorAll(".animation").length,
+ "As many timeline elements were created as there are playerFronts");
+});
diff --git a/devtools/client/animationinspector/test/browser_animation_shows_player_on_valid_node.js b/devtools/client/animationinspector/test/browser_animation_shows_player_on_valid_node.js
new file mode 100644
index 000000000..57e6a68fb
--- /dev/null
+++ b/devtools/client/animationinspector/test/browser_animation_shows_player_on_valid_node.js
@@ -0,0 +1,21 @@
+/* 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";
+
+requestLongerTimeout(2);
+
+// Test that the panel shows an animation player when an animated node is
+// selected.
+
+add_task(function* () {
+ yield addTab(URL_ROOT + "doc_simple_animation.html");
+ let {inspector, panel} = yield openAnimationInspector();
+
+ info("Select node .animated and check that the panel is not empty");
+ let node = yield getNodeFront(".animated", inspector);
+ yield selectNodeAndWaitForAnimations(node, inspector);
+
+ assertAnimationsDisplayed(panel, 1);
+});
diff --git a/devtools/client/animationinspector/test/browser_animation_spacebar_toggles_animations.js b/devtools/client/animationinspector/test/browser_animation_spacebar_toggles_animations.js
new file mode 100644
index 000000000..799ecc28d
--- /dev/null
+++ b/devtools/client/animationinspector/test/browser_animation_spacebar_toggles_animations.js
@@ -0,0 +1,49 @@
+/* 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";
+
+add_task(function* setup() {
+ yield SpecialPowers.pushPrefEnv({
+ set: [["dom.ipc.processCount", 1]]
+ });
+});
+
+// Test that the spacebar key press toggles the toggleAll button state
+// when a node with no animation is selected.
+// This test doesn't need to test if animations actually pause/resume
+// because there's an other test that does this :
+// browser_animation_toggle_button_toggles_animation.js
+
+add_task(function* () {
+ yield addTab(URL_ROOT + "doc_simple_animation.html");
+ let {panel, inspector, window, controller} = yield openAnimationInspector();
+ let {toggleAllButtonEl} = panel;
+
+ // select a node without animations
+ yield selectNodeAndWaitForAnimations(".still", inspector);
+
+ // ensure the focus is on the animation panel
+ window.focus();
+
+ info("Simulate spacebar stroke and check toggleAll button" +
+ " is in paused state");
+
+ // sending the key will lead to a ALL_ANIMATIONS_TOGGLED_EVENT
+ let onToggled = once(controller, controller.ALL_ANIMATIONS_TOGGLED_EVENT);
+ EventUtils.sendKey("SPACE", window);
+ yield onToggled;
+ ok(toggleAllButtonEl.classList.contains("paused"),
+ "The toggle all button is in its paused state");
+
+ info("Simulate spacebar stroke and check toggleAll button" +
+ " is in playing state");
+
+ // sending the key will lead to a ALL_ANIMATIONS_TOGGLED_EVENT
+ onToggled = once(controller, controller.ALL_ANIMATIONS_TOGGLED_EVENT);
+ EventUtils.sendKey("SPACE", window);
+ yield onToggled;
+ ok(!toggleAllButtonEl.classList.contains("paused"),
+ "The toggle all button is in its playing state again");
+});
diff --git a/devtools/client/animationinspector/test/browser_animation_spacebar_toggles_node_animations.js b/devtools/client/animationinspector/test/browser_animation_spacebar_toggles_node_animations.js
new file mode 100644
index 000000000..634d4bc49
--- /dev/null
+++ b/devtools/client/animationinspector/test/browser_animation_spacebar_toggles_node_animations.js
@@ -0,0 +1,45 @@
+/* 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";
+
+// Test that the spacebar key press toggles the play/resume button state.
+// This test doesn't need to test if animations actually pause/resume
+// because there's an other test that does this.
+// There are animations in the test page and since, by default, the <body> node
+// is selected, animations will be displayed in the timeline, so the timeline
+// play/resume button will be displayed
+
+requestLongerTimeout(2);
+
+add_task(function* () {
+ requestLongerTimeout(2);
+
+ yield addTab(URL_ROOT + "doc_simple_animation.html");
+ let {panel, window} = yield openAnimationInspector();
+ let {playTimelineButtonEl} = panel;
+
+ // ensure the focus is on the animation panel
+ window.focus();
+
+ info("Simulate spacebar stroke and check playResume button" +
+ " is in paused state");
+
+ // sending the key will lead to a UI_UPDATE_EVENT
+ let onUpdated = panel.once(panel.UI_UPDATED_EVENT);
+ EventUtils.sendKey("SPACE", window);
+ yield onUpdated;
+ ok(playTimelineButtonEl.classList.contains("paused"),
+ "The play/resume button is in its paused state");
+
+ info("Simulate spacebar stroke and check playResume button" +
+ " is in playing state");
+
+ // sending the key will lead to a UI_UPDATE_EVENT
+ onUpdated = panel.once(panel.UI_UPDATED_EVENT);
+ EventUtils.sendKey("SPACE", window);
+ yield onUpdated;
+ ok(!playTimelineButtonEl.classList.contains("paused"),
+ "The play/resume button is in its play state again");
+});
diff --git a/devtools/client/animationinspector/test/browser_animation_target_highlight_select.js b/devtools/client/animationinspector/test/browser_animation_target_highlight_select.js
new file mode 100644
index 000000000..de14e6aca
--- /dev/null
+++ b/devtools/client/animationinspector/test/browser_animation_target_highlight_select.js
@@ -0,0 +1,73 @@
+/* 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";
+
+requestLongerTimeout(2);
+
+// Test that the DOM element targets displayed in animation player widgets can
+// be used to highlight elements in the DOM and select them in the inspector.
+
+add_task(function* () {
+ yield addTab(URL_ROOT + "doc_simple_animation.html");
+
+ let {toolbox, inspector, panel} = yield openAnimationInspector();
+
+ info("Select the simple animated node");
+ let onPanelUpdated = panel.once(panel.UI_UPDATED_EVENT);
+ yield selectNodeAndWaitForAnimations(".animated", inspector);
+ yield onPanelUpdated;
+
+ let targets = yield waitForAllAnimationTargets(panel);
+ // Arbitrary select the first one
+ let targetNodeComponent = targets[0];
+
+ info("Retrieve the part of the widget that highlights the node on hover");
+ let highlightingEl = targetNodeComponent.previewer.previewEl;
+
+ info("Listen to node-highlight event and mouse over the widget");
+ let onHighlight = toolbox.once("node-highlight");
+ EventUtils.synthesizeMouse(highlightingEl, 10, 5, {type: "mouseover"},
+ highlightingEl.ownerDocument.defaultView);
+ let nodeFront = yield onHighlight;
+
+ // Do not forget to mouseout, otherwise we get random mouseover event
+ // when selecting another node, which triggers some requests in animation
+ // inspector.
+ EventUtils.synthesizeMouse(highlightingEl, 10, 5, {type: "mouseout"},
+ highlightingEl.ownerDocument.defaultView);
+
+ ok(true, "The node-highlight event was fired");
+ is(targetNodeComponent.previewer.nodeFront, nodeFront,
+ "The highlighted node is the one stored on the animation widget");
+ is(nodeFront.tagName, "DIV",
+ "The highlighted node has the correct tagName");
+ is(nodeFront.attributes[0].name, "class",
+ "The highlighted node has the correct attributes");
+ is(nodeFront.attributes[0].value, "ball animated",
+ "The highlighted node has the correct class");
+
+ info("Select the body node in order to have the list of all animations");
+ onPanelUpdated = panel.once(panel.UI_UPDATED_EVENT);
+ yield selectNodeAndWaitForAnimations("body", inspector);
+ yield onPanelUpdated;
+
+ targets = yield waitForAllAnimationTargets(panel);
+ targetNodeComponent = targets[0];
+
+ info("Click on the first animated node component and wait for the " +
+ "selection to change");
+ let onSelection = inspector.selection.once("new-node-front");
+ onPanelUpdated = panel.once(panel.UI_UPDATED_EVENT);
+ let nodeEl = targetNodeComponent.previewer.previewEl;
+ EventUtils.sendMouseEvent({type: "click"}, nodeEl,
+ nodeEl.ownerDocument.defaultView);
+ yield onSelection;
+
+ is(inspector.selection.nodeFront, targetNodeComponent.previewer.nodeFront,
+ "The selected node is the one stored on the animation widget");
+
+ yield onPanelUpdated;
+ yield waitForAllAnimationTargets(panel);
+});
diff --git a/devtools/client/animationinspector/test/browser_animation_target_highlighter_lock.js b/devtools/client/animationinspector/test/browser_animation_target_highlighter_lock.js
new file mode 100644
index 000000000..b5e952679
--- /dev/null
+++ b/devtools/client/animationinspector/test/browser_animation_target_highlighter_lock.js
@@ -0,0 +1,54 @@
+/* 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";
+
+requestLongerTimeout(2);
+
+// Test that the DOM element targets displayed in animation player widgets can
+// be used to highlight elements in the DOM and select them in the inspector.
+
+add_task(function* () {
+ yield addTab(URL_ROOT + "doc_simple_animation.html");
+ let {panel} = yield openAnimationInspector();
+
+ let targets = panel.animationsTimelineComponent.targetNodes;
+
+ info("Click on the highlighter icon for the first animated node");
+ let domNodePreview1 = targets[0].previewer;
+ yield lockHighlighterOn(domNodePreview1);
+ ok(domNodePreview1.highlightNodeEl.classList.contains("selected"),
+ "The highlighter icon is selected");
+
+ info("Click on the highlighter icon for the second animated node");
+ let domNodePreview2 = targets[1].previewer;
+ yield lockHighlighterOn(domNodePreview2);
+ ok(domNodePreview2.highlightNodeEl.classList.contains("selected"),
+ "The highlighter icon is selected");
+ ok(!domNodePreview1.highlightNodeEl.classList.contains("selected"),
+ "The highlighter icon for the first node is unselected");
+
+ info("Click again to unhighlight");
+ yield unlockHighlighterOn(domNodePreview2);
+ ok(!domNodePreview2.highlightNodeEl.classList.contains("selected"),
+ "The highlighter icon for the second node is unselected");
+});
+
+function* lockHighlighterOn(domNodePreview) {
+ let onLocked = domNodePreview.once("target-highlighter-locked");
+ clickOnHighlighterIcon(domNodePreview);
+ yield onLocked;
+}
+
+function* unlockHighlighterOn(domNodePreview) {
+ let onUnlocked = domNodePreview.once("target-highlighter-unlocked");
+ clickOnHighlighterIcon(domNodePreview);
+ yield onUnlocked;
+}
+
+function clickOnHighlighterIcon(domNodePreview) {
+ let lockEl = domNodePreview.highlightNodeEl;
+ EventUtils.sendMouseEvent({type: "click"}, lockEl,
+ lockEl.ownerDocument.defaultView);
+}
diff --git a/devtools/client/animationinspector/test/browser_animation_timeline_currentTime.js b/devtools/client/animationinspector/test/browser_animation_timeline_currentTime.js
new file mode 100644
index 000000000..d5caaff28
--- /dev/null
+++ b/devtools/client/animationinspector/test/browser_animation_timeline_currentTime.js
@@ -0,0 +1,48 @@
+/* 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";
+
+requestLongerTimeout(2);
+
+// Check that the timeline toolbar displays the current time, and that it
+// changes when animations are playing, gets back to 0 when animations are
+// rewound, and stops when animations are paused.
+
+add_task(function* () {
+ yield addTab(URL_ROOT + "doc_simple_animation.html");
+
+ let {panel} = yield openAnimationInspector();
+ let label = panel.timelineCurrentTimeEl;
+ ok(label, "The current time label exists");
+
+ // On page load animations are playing so the time shoud change, although we
+ // don't want to test the exact value of the time displayed, just that it
+ // actually changes.
+ info("Make sure the time displayed actually changes");
+ yield isCurrentTimeLabelChanging(panel, true);
+
+ info("Pause the animations and check that the time stops changing");
+ yield clickTimelinePlayPauseButton(panel);
+ yield isCurrentTimeLabelChanging(panel, false);
+
+ info("Rewind the animations and check that the time stops changing");
+ yield clickTimelineRewindButton(panel);
+ yield isCurrentTimeLabelChanging(panel, false);
+ is(label.textContent, "00:00.000");
+});
+
+function* isCurrentTimeLabelChanging(panel, isChanging) {
+ let label = panel.timelineCurrentTimeEl;
+
+ let time1 = label.textContent;
+ yield new Promise(r => setTimeout(r, 200));
+ let time2 = label.textContent;
+
+ if (isChanging) {
+ ok(time1 !== time2, "The text displayed in the label changes with time");
+ } else {
+ is(time1, time2, "The text displayed in the label doesn't change");
+ }
+}
diff --git a/devtools/client/animationinspector/test/browser_animation_timeline_header.js b/devtools/client/animationinspector/test/browser_animation_timeline_header.js
new file mode 100644
index 000000000..3a0a0412a
--- /dev/null
+++ b/devtools/client/animationinspector/test/browser_animation_timeline_header.js
@@ -0,0 +1,59 @@
+/* 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";
+
+requestLongerTimeout(2);
+
+// Check that the timeline shows correct time graduations in the header.
+
+const {findOptimalTimeInterval, TimeScale} = require("devtools/client/animationinspector/utils");
+
+// Should be kept in sync with TIME_GRADUATION_MIN_SPACING in
+// animation-timeline.js
+const TIME_GRADUATION_MIN_SPACING = 40;
+
+add_task(function* () {
+ yield addTab(URL_ROOT + "doc_simple_animation.html");
+
+ // System scrollbar is enabled by default on our testing envionment and it
+ // would shrink width of inspector and affect number of time-ticks causing
+ // unexpected results. So, we set it wider to avoid this kind of edge case.
+ yield pushPref("devtools.toolsidebar-width.inspector", 350);
+
+ let {panel} = yield openAnimationInspector();
+
+ let timeline = panel.animationsTimelineComponent;
+ let headerEl = timeline.timeHeaderEl;
+
+ info("Find out how many time graduations should there be");
+ let width = headerEl.offsetWidth;
+
+ let animationDuration = TimeScale.maxEndTime - TimeScale.minStartTime;
+ let minTimeInterval = TIME_GRADUATION_MIN_SPACING * animationDuration / width;
+
+ // Note that findOptimalTimeInterval is tested separately in xpcshell test
+ // test_findOptimalTimeInterval.js, so we assume that it works here.
+ let interval = findOptimalTimeInterval(minTimeInterval);
+ let nb = Math.ceil(animationDuration / interval);
+
+ is(headerEl.querySelectorAll(".header-item").length, nb,
+ "The expected number of time ticks were found");
+
+ info("Make sure graduations are evenly distributed and show the right times");
+ [...headerEl.querySelectorAll(".time-tick")].forEach((tick, i) => {
+ let left = parseFloat(tick.style.left);
+ let expectedPos = i * interval * 100 / animationDuration;
+ is(Math.round(left), Math.round(expectedPos),
+ `Graduation ${i} is positioned correctly`);
+
+ // Note that the distancetoRelativeTime and formatTime functions are tested
+ // separately in xpcshell test test_timeScale.js, so we assume that they
+ // work here.
+ let formattedTime = TimeScale.formatTime(
+ TimeScale.distanceToRelativeTime(expectedPos, width));
+ is(tick.textContent, formattedTime,
+ `Graduation ${i} has the right text content`);
+ });
+});
diff --git a/devtools/client/animationinspector/test/browser_animation_timeline_iterationStart.js b/devtools/client/animationinspector/test/browser_animation_timeline_iterationStart.js
new file mode 100644
index 000000000..c05f15d27
--- /dev/null
+++ b/devtools/client/animationinspector/test/browser_animation_timeline_iterationStart.js
@@ -0,0 +1,71 @@
+/* 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";
+
+// Check that the iteration start is displayed correctly in time blocks.
+
+add_task(function* () {
+ yield addTab(URL_ROOT + "doc_script_animation.html");
+ let {panel} = yield openAnimationInspector();
+ let timelineComponent = panel.animationsTimelineComponent;
+ let timeBlockComponents = timelineComponent.timeBlocks;
+ let detailsComponents = timelineComponent.details;
+
+ for (let i = 0; i < timeBlockComponents.length; i++) {
+ info(`Expand time block ${i} so its keyframes are visible`);
+ yield clickOnAnimation(panel, i);
+
+ info(`Check the state of time block ${i}`);
+ let {containerEl, animation: {state}} = timeBlockComponents[i];
+
+ checkAnimationTooltip(containerEl, state);
+ checkProgressAtStartingTime(containerEl, state);
+
+ // Get the first set of keyframes (there's only one animated property
+ // anyway), and the first frame element from there, we're only interested in
+ // its offset.
+ let keyframeComponent = detailsComponents[i].keyframeComponents[0];
+ let frameEl = keyframeComponent.keyframesEl.querySelector(".frame");
+ checkKeyframeOffset(containerEl, frameEl, state);
+ }
+});
+
+function checkAnimationTooltip(el, {iterationStart, duration}) {
+ info("Check an animation's iterationStart data in its tooltip");
+ let title = el.querySelector(".name").getAttribute("title");
+
+ let iterationStartTime = iterationStart * duration / 1000;
+ let iterationStartTimeString = iterationStartTime.toLocaleString(undefined, {
+ maximumFractionDigits: 2,
+ minimumFractionDigits: 2
+ }).replace(".", "\\.");
+ let iterationStartString = iterationStart.toString().replace(".", "\\.");
+
+ let regex = new RegExp("Iteration start: " + iterationStartString +
+ " \\(" + iterationStartTimeString + "s\\)");
+ ok(title.match(regex), "The tooltip shows the expected iteration start");
+}
+
+function checkProgressAtStartingTime(el, { iterationStart }) {
+ info("Check the progress of starting time");
+ const pathEl = el.querySelector(".iteration-path");
+ const pathSegList = pathEl.pathSegList;
+ const pathSeg = pathSegList.getItem(1);
+ const progress = pathSeg.y;
+ is(progress, iterationStart % 1,
+ `The progress at starting point should be ${ iterationStart % 1 }`);
+}
+
+function checkKeyframeOffset(timeBlockEl, frameEl, {iterationStart}) {
+ info("Check that the first keyframe is offset correctly");
+
+ let start = getIterationStartFromLeft(frameEl);
+ is(start, iterationStart % 1, "The frame offset for iteration start");
+}
+
+function getIterationStartFromLeft(el) {
+ let left = 100 - parseFloat(/(\d+)%/.exec(el.style.left)[1]);
+ return left / 100;
+}
diff --git a/devtools/client/animationinspector/test/browser_animation_timeline_pause_button_01.js b/devtools/client/animationinspector/test/browser_animation_timeline_pause_button_01.js
new file mode 100644
index 000000000..a3a2b4c61
--- /dev/null
+++ b/devtools/client/animationinspector/test/browser_animation_timeline_pause_button_01.js
@@ -0,0 +1,34 @@
+/* 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";
+
+requestLongerTimeout(2);
+
+// Check that the timeline toolbar contains a pause button and that this pause button can
+// be clicked. Check that when it is, the button changes state and the scrubber stops and
+// resumes.
+
+add_task(function* () {
+ yield addTab(URL_ROOT + "doc_simple_animation.html");
+
+ let {panel} = yield openAnimationInspector();
+ let btn = panel.playTimelineButtonEl;
+
+ ok(btn, "The play/pause button exists");
+ ok(!btn.classList.contains("paused"), "The play/pause button is in its playing state");
+
+ info("Click on the button to pause all timeline animations");
+ yield clickTimelinePlayPauseButton(panel);
+
+ ok(btn.classList.contains("paused"), "The play/pause button is in its paused state");
+ yield assertScrubberMoving(panel, false);
+
+ info("Click again on the button to play all timeline animations");
+ yield clickTimelinePlayPauseButton(panel);
+
+ ok(!btn.classList.contains("paused"),
+ "The play/pause button is in its playing state again");
+ yield assertScrubberMoving(panel, true);
+});
diff --git a/devtools/client/animationinspector/test/browser_animation_timeline_pause_button_02.js b/devtools/client/animationinspector/test/browser_animation_timeline_pause_button_02.js
new file mode 100644
index 000000000..1c440dd88
--- /dev/null
+++ b/devtools/client/animationinspector/test/browser_animation_timeline_pause_button_02.js
@@ -0,0 +1,48 @@
+/* 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";
+
+requestLongerTimeout(2);
+
+// Checks that the play/pause button goes to the right state when the scrubber has reached
+// the end of the timeline but there are infinite animations playing.
+
+add_task(function* () {
+ yield addTab(URL_ROOT + "doc_simple_animation.html");
+
+ let {panel, inspector} = yield openAnimationInspector();
+ let timeline = panel.animationsTimelineComponent;
+ let btn = panel.playTimelineButtonEl;
+
+ info("Select an infinite animation and wait for the scrubber to reach the end");
+ yield selectNodeAndWaitForAnimations(".multi", inspector);
+ yield waitForOutOfBoundScrubber(timeline);
+
+ ok(!btn.classList.contains("paused"),
+ "The button is in its playing state still, animations are infinite.");
+ yield assertScrubberMoving(panel, true);
+
+ info("Click on the button after the scrubber has moved out of bounds");
+ yield clickTimelinePlayPauseButton(panel);
+
+ ok(btn.classList.contains("paused"),
+ "The button can be paused after the scrubber has moved out of bounds");
+ yield assertScrubberMoving(panel, false);
+});
+
+function waitForOutOfBoundScrubber({win, scrubberEl}) {
+ return new Promise(resolve => {
+ function check() {
+ let pos = scrubberEl.getBoxQuads()[0].bounds.right;
+ let width = win.document.documentElement.offsetWidth;
+ if (pos >= width) {
+ setTimeout(resolve, 50);
+ } else {
+ setTimeout(check, 50);
+ }
+ }
+ check();
+ });
+}
diff --git a/devtools/client/animationinspector/test/browser_animation_timeline_pause_button_03.js b/devtools/client/animationinspector/test/browser_animation_timeline_pause_button_03.js
new file mode 100644
index 000000000..5c6e324ed
--- /dev/null
+++ b/devtools/client/animationinspector/test/browser_animation_timeline_pause_button_03.js
@@ -0,0 +1,60 @@
+/* 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";
+
+requestLongerTimeout(2);
+
+// Also checks that the button goes to the right state when the scrubber has
+// reached the end of the timeline: continues to be in playing mode for infinite
+// animations, goes to paused mode otherwise.
+// And test that clicking the button once the scrubber has reached the end of
+// the timeline does the right thing.
+
+add_task(function* () {
+ yield addTab(URL_ROOT + "doc_simple_animation.html");
+
+ let {panel, controller, inspector} = yield openAnimationInspector();
+ let btn = panel.playTimelineButtonEl;
+
+ // For a finite animation, once the scrubber reaches the end of the timeline, the pause
+ // button should go back to paused mode.
+ info("Select a finite animation and wait for the animation to complete");
+ yield selectNodeAndWaitForAnimations(".negative-delay", inspector);
+
+ let onButtonPaused = waitForButtonPaused(btn);
+ let onTimelineUpdated = controller.once(controller.PLAYERS_UPDATED_EVENT);
+ // The page is reloaded to avoid missing the animation.
+ yield reloadTab(inspector);
+ yield onTimelineUpdated;
+ yield onButtonPaused;
+
+ ok(btn.classList.contains("paused"),
+ "The button is in paused state once finite animations are done");
+ yield assertScrubberMoving(panel, false);
+
+ info("Click again on the button to play the animation from the start again");
+ yield clickTimelinePlayPauseButton(panel);
+
+ ok(!btn.classList.contains("paused"),
+ "Clicking the button once finite animations are done should restart them");
+ yield assertScrubberMoving(panel, true);
+});
+
+function waitForButtonPaused(btn) {
+ return new Promise(resolve => {
+ let observer = new btn.ownerDocument.defaultView.MutationObserver(mutations => {
+ for (let mutation of mutations) {
+ if (mutation.type === "attributes" &&
+ mutation.attributeName === "class" &&
+ !mutation.oldValue.includes("paused") &&
+ btn.classList.contains("paused")) {
+ observer.disconnect();
+ resolve();
+ }
+ }
+ });
+ observer.observe(btn, { attributes: true, attributeOldValue: true });
+ });
+}
diff --git a/devtools/client/animationinspector/test/browser_animation_timeline_rate_selector.js b/devtools/client/animationinspector/test/browser_animation_timeline_rate_selector.js
new file mode 100644
index 000000000..37ac20de0
--- /dev/null
+++ b/devtools/client/animationinspector/test/browser_animation_timeline_rate_selector.js
@@ -0,0 +1,56 @@
+/* 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";
+
+requestLongerTimeout(2);
+
+// Check that the timeline toolbar contains a playback rate selector UI and that
+// it can be used to change the playback rate of animations in the timeline.
+// Also check that it displays the rate of the current animations in case they
+// all have the same rate, or that it displays the empty value in case they
+// have mixed rates.
+
+add_task(function* () {
+ yield addTab(URL_ROOT + "doc_simple_animation.html");
+
+ let {panel, controller, inspector, toolbox} = yield openAnimationInspector();
+
+ // In this test, we disable the highlighter on purpose because of the way
+ // events are simulated to select an option in the playbackRate <select>.
+ // Indeed, this may cause mousemove events to be triggered on the nodes that
+ // are underneath the <select>, and these are AnimationTargetNode instances.
+ // Simulating mouse events on them will cause the highlighter to emit requests
+ // and this might cause the test to fail if they happen after it has ended.
+ disableHighlighter(toolbox);
+
+ let select = panel.rateSelectorEl.firstChild;
+
+ ok(select, "The rate selector exists");
+
+ info("Change all of the current animations' rates to 0.5");
+ yield changeTimelinePlaybackRate(panel, .5);
+ checkAllAnimationsRatesChanged(controller, select, .5);
+
+ info("Select just one animated node and change its rate only");
+ yield selectNodeAndWaitForAnimations(".animated", inspector);
+
+ yield changeTimelinePlaybackRate(panel, 2);
+ checkAllAnimationsRatesChanged(controller, select, 2);
+
+ info("Select the <body> again, it should now have mixed-rates animations");
+ yield selectNodeAndWaitForAnimations("body", inspector);
+
+ is(select.value, "", "The selected rate is empty");
+
+ info("Change the rate for these mixed-rate animations");
+ yield changeTimelinePlaybackRate(panel, 1);
+ checkAllAnimationsRatesChanged(controller, select, 1);
+});
+
+function checkAllAnimationsRatesChanged({animationPlayers}, select, rate) {
+ ok(animationPlayers.every(({state}) => state.playbackRate === rate),
+ "All animations' rates have been set to " + rate);
+ is(select.value, rate, "The right value is displayed in the select");
+}
diff --git a/devtools/client/animationinspector/test/browser_animation_timeline_rewind_button.js b/devtools/client/animationinspector/test/browser_animation_timeline_rewind_button.js
new file mode 100644
index 000000000..c4dcbd161
--- /dev/null
+++ b/devtools/client/animationinspector/test/browser_animation_timeline_rewind_button.js
@@ -0,0 +1,51 @@
+/* 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";
+
+requestLongerTimeout(2);
+
+// Check that the timeline toolbar contains a rewind button and that it can be
+// clicked. Check that when it is, the current animations displayed in the
+// timeline get their playstates changed to paused, and their currentTimes
+// reset to 0, and that the scrubber stops moving and is positioned to the
+// start.
+
+add_task(function* () {
+ yield addTab(URL_ROOT + "doc_simple_animation.html");
+
+ let {panel, controller} = yield openAnimationInspector();
+ let players = controller.animationPlayers;
+ let btn = panel.rewindTimelineButtonEl;
+
+ ok(btn, "The rewind button exists");
+
+ info("Click on the button to rewind all timeline animations");
+ yield clickTimelineRewindButton(panel);
+
+ info("Check that the scrubber has stopped moving");
+ yield assertScrubberMoving(panel, false);
+
+ ok(players.every(({state}) => state.currentTime === 0),
+ "All animations' currentTimes have been set to 0");
+ ok(players.every(({state}) => state.playState === "paused"),
+ "All animations have been paused");
+
+ info("Play the animations again");
+ yield clickTimelinePlayPauseButton(panel);
+
+ info("And pause them after a short while");
+ yield new Promise(r => setTimeout(r, 200));
+
+ info("Check that rewinding when animations are paused works too");
+ yield clickTimelineRewindButton(panel);
+
+ info("Check that the scrubber has stopped moving");
+ yield assertScrubberMoving(panel, false);
+
+ ok(players.every(({state}) => state.currentTime === 0),
+ "All animations' currentTimes have been set to 0");
+ ok(players.every(({state}) => state.playState === "paused"),
+ "All animations have been paused");
+});
diff --git a/devtools/client/animationinspector/test/browser_animation_timeline_scrubber_exists.js b/devtools/client/animationinspector/test/browser_animation_timeline_scrubber_exists.js
new file mode 100644
index 000000000..9fa22e007
--- /dev/null
+++ b/devtools/client/animationinspector/test/browser_animation_timeline_scrubber_exists.js
@@ -0,0 +1,20 @@
+/* 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";
+
+requestLongerTimeout(2);
+
+// Check that the timeline does have a scrubber element.
+
+add_task(function* () {
+ yield addTab(URL_ROOT + "doc_simple_animation.html");
+ let {panel} = yield openAnimationInspector();
+
+ let timeline = panel.animationsTimelineComponent;
+ let scrubberEl = timeline.scrubberEl;
+
+ ok(scrubberEl, "The scrubber element exists");
+ ok(scrubberEl.classList.contains("scrubber"), "It has the right classname");
+});
diff --git a/devtools/client/animationinspector/test/browser_animation_timeline_scrubber_movable.js b/devtools/client/animationinspector/test/browser_animation_timeline_scrubber_movable.js
new file mode 100644
index 000000000..a690dd78e
--- /dev/null
+++ b/devtools/client/animationinspector/test/browser_animation_timeline_scrubber_movable.js
@@ -0,0 +1,70 @@
+/* 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";
+
+requestLongerTimeout(2);
+
+// Check that the scrubber in the timeline can be moved by clicking & dragging
+// in the header area.
+// Also check that doing so changes the timeline's play/pause button to paused
+// state.
+// Finally, also check that the scrubber can be moved using the scrubber handle.
+
+add_task(function* () {
+ yield addTab(URL_ROOT + "doc_simple_animation.html");
+
+ let {panel} = yield openAnimationInspector();
+ let timeline = panel.animationsTimelineComponent;
+ let {win, timeHeaderEl, scrubberEl, scrubberHandleEl} = timeline;
+ let playTimelineButtonEl = panel.playTimelineButtonEl;
+
+ ok(!playTimelineButtonEl.classList.contains("paused"),
+ "The timeline play button is in its playing state by default");
+
+ info("Mousedown in the header to move the scrubber");
+ yield synthesizeInHeaderAndWaitForChange(timeline, 50, 1, "mousedown");
+ checkScrubberIsAt(scrubberEl, timeHeaderEl, 50);
+
+ ok(playTimelineButtonEl.classList.contains("paused"),
+ "The timeline play button is in its paused state after mousedown");
+
+ info("Continue moving the mouse and verify that the scrubber tracks it");
+ yield synthesizeInHeaderAndWaitForChange(timeline, 100, 1, "mousemove");
+ checkScrubberIsAt(scrubberEl, timeHeaderEl, 100);
+
+ ok(playTimelineButtonEl.classList.contains("paused"),
+ "The timeline play button is in its paused state after mousemove");
+
+ info("Release the mouse and move again and verify that the scrubber stays");
+ EventUtils.synthesizeMouse(timeHeaderEl, 100, 1, {type: "mouseup"}, win);
+ EventUtils.synthesizeMouse(timeHeaderEl, 200, 1, {type: "mousemove"}, win);
+ checkScrubberIsAt(scrubberEl, timeHeaderEl, 100);
+
+ info("Try to drag the scrubber handle and check that the scrubber moves");
+ let onDataChanged = timeline.once("timeline-data-changed");
+ EventUtils.synthesizeMouse(scrubberHandleEl, 1, 20, {type: "mousedown"}, win);
+ EventUtils.synthesizeMouse(timeHeaderEl, 0, 0, {type: "mousemove"}, win);
+ EventUtils.synthesizeMouse(timeHeaderEl, 0, 0, {type: "mouseup"}, win);
+ yield onDataChanged;
+
+ checkScrubberIsAt(scrubberEl, timeHeaderEl, 0);
+});
+
+function* synthesizeInHeaderAndWaitForChange(timeline, x, y, type) {
+ let onDataChanged = timeline.once("timeline-data-changed");
+ EventUtils.synthesizeMouse(timeline.timeHeaderEl, x, y, {type}, timeline.win);
+ yield onDataChanged;
+}
+
+function getPositionPercentage(pos, headerEl) {
+ return pos * 100 / headerEl.offsetWidth;
+}
+
+function checkScrubberIsAt(scrubberEl, timeHeaderEl, pos) {
+ let newPos = Math.round(parseFloat(scrubberEl.style.left));
+ let expectedPos = Math.round(getPositionPercentage(pos, timeHeaderEl));
+ is(newPos, expectedPos,
+ `The scrubber is at position ${pos} (${expectedPos}%)`);
+}
diff --git a/devtools/client/animationinspector/test/browser_animation_timeline_scrubber_moves.js b/devtools/client/animationinspector/test/browser_animation_timeline_scrubber_moves.js
new file mode 100644
index 000000000..494c581a4
--- /dev/null
+++ b/devtools/client/animationinspector/test/browser_animation_timeline_scrubber_moves.js
@@ -0,0 +1,28 @@
+/* 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";
+
+requestLongerTimeout(2);
+
+// Check that the scrubber in the timeline moves when animations are playing.
+// The animations in the test page last for a very long time, so the test just
+// measures the position of the scrubber once, then waits for some time to pass
+// and measures its position again.
+
+add_task(function* () {
+ yield addTab(URL_ROOT + "doc_simple_animation.html");
+ let {panel} = yield openAnimationInspector();
+
+ let timeline = panel.animationsTimelineComponent;
+ let scrubberEl = timeline.scrubberEl;
+ let startPos = scrubberEl.getBoundingClientRect().left;
+
+ info("Wait for some time to check that the scrubber moves");
+ yield new Promise(r => setTimeout(r, 2000));
+
+ let endPos = scrubberEl.getBoundingClientRect().left;
+
+ ok(endPos > startPos, "The scrubber has moved");
+});
diff --git a/devtools/client/animationinspector/test/browser_animation_timeline_setCurrentTime.js b/devtools/client/animationinspector/test/browser_animation_timeline_setCurrentTime.js
new file mode 100644
index 000000000..efc32c001
--- /dev/null
+++ b/devtools/client/animationinspector/test/browser_animation_timeline_setCurrentTime.js
@@ -0,0 +1,88 @@
+/* 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";
+
+requestLongerTimeout(2);
+
+// Animation.currentTime ignores neagtive delay and positive/negative endDelay
+// during fill-mode, even if they are set.
+// For example, when the animation timing is
+// { duration: 1000, iterations: 1, endDelay: -500, easing: linear },
+// the animation progress is 0.5 at 700ms because the progress stops as 0.5 at
+// 500ms in original animation. However, if you set as
+// animation.currentTime = 700 manually, the progress will be 0.7.
+// So we modify setCurrentTime method since
+// AnimationInspector should re-produce same as original animation.
+// In these tests,
+// we confirm the behavior of setCurrentTime by delay and endDelay.
+
+add_task(function* () {
+ yield addTab(URL_ROOT + "doc_timing_combination_animation.html");
+ const { panel, controller } = yield openAnimationInspector();
+
+ yield clickTimelinePlayPauseButton(panel);
+
+ const timelineComponent = panel.animationsTimelineComponent;
+ const timeBlockComponents = timelineComponent.timeBlocks;
+
+ // Test -5000ms.
+ let time = -5000;
+ yield controller.setCurrentTimeAll(time, true);
+ for (let i = 0; i < timeBlockComponents.length; i++) {
+ yield timeBlockComponents[i].animation.refreshState();
+ const state = yield timeBlockComponents[i].animation.state;
+ info(`Check the state at ${ time }ms with `
+ + `delay:${ state.delay } and endDelay:${ state.endDelay }`);
+ is(state.currentTime, 0,
+ `The currentTime should be 0 at setCurrentTime(${ time })`);
+ }
+
+ // Test 10000ms.
+ time = 10000;
+ yield controller.setCurrentTimeAll(time, true);
+ for (let i = 0; i < timeBlockComponents.length; i++) {
+ yield timeBlockComponents[i].animation.refreshState();
+ const state = yield timeBlockComponents[i].animation.state;
+ info(`Check the state at ${ time }ms with `
+ + `delay:${ state.delay } and endDelay:${ state.endDelay }`);
+ const expected = state.delay < 0 ? 0 : time;
+ is(state.currentTime, expected,
+ `The currentTime should be ${ expected } at setCurrentTime(${ time }).`
+ + ` delay: ${ state.delay } and endDelay: ${ state.endDelay }`);
+ }
+
+ // Test 60000ms.
+ time = 60000;
+ yield controller.setCurrentTimeAll(time, true);
+ for (let i = 0; i < timeBlockComponents.length; i++) {
+ yield timeBlockComponents[i].animation.refreshState();
+ const state = yield timeBlockComponents[i].animation.state;
+ info(`Check the state at ${ time }ms with `
+ + `delay:${ state.delay } and endDelay:${ state.endDelay }`);
+ const expected = state.delay < 0 ? time + state.delay : time;
+ is(state.currentTime, expected,
+ `The currentTime should be ${ expected } at setCurrentTime(${ time }).`
+ + ` delay: ${ state.delay } and endDelay: ${ state.endDelay }`);
+ }
+
+ // Test 150000ms.
+ time = 150000;
+ yield controller.setCurrentTimeAll(time, true);
+ for (let i = 0; i < timeBlockComponents.length; i++) {
+ yield timeBlockComponents[i].animation.refreshState();
+ const state = yield timeBlockComponents[i].animation.state;
+ info(`Check the state at ${ time }ms with `
+ + `delay:${ state.delay } and endDelay:${ state.endDelay }`);
+ const currentTime = state.delay < 0 ? time + state.delay : time;
+ const endTime =
+ state.delay + state.iterationCount * state.duration + state.endDelay;
+ const expected =
+ state.endDelay < 0 && state.fill === "both" && currentTime > endTime
+ ? endTime : currentTime;
+ is(state.currentTime, expected,
+ `The currentTime should be ${ expected } at setCurrentTime(${ time }).`
+ + ` delay: ${ state.delay } and endDelay: ${ state.endDelay }`);
+ }
+});
diff --git a/devtools/client/animationinspector/test/browser_animation_timeline_shows_delay.js b/devtools/client/animationinspector/test/browser_animation_timeline_shows_delay.js
new file mode 100644
index 000000000..8c9b0653d
--- /dev/null
+++ b/devtools/client/animationinspector/test/browser_animation_timeline_shows_delay.js
@@ -0,0 +1,96 @@
+/* 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";
+
+requestLongerTimeout(2);
+
+// Check that animation delay is visualized in the timeline when the animation
+// is delayed.
+// Also check that negative delays do not overflow the UI, and are shown like
+// positive delays.
+
+add_task(function* () {
+ yield addTab(URL_ROOT + "doc_simple_animation.html");
+ let {inspector, panel} = yield openAnimationInspector();
+
+ info("Selecting a delayed animated node");
+ yield selectNodeAndWaitForAnimations(".delayed", inspector);
+ let timelineEl = panel.animationsTimelineComponent.rootWrapperEl;
+ checkDelayAndName(timelineEl, true);
+ let animationEl = timelineEl.querySelector(".animation");
+ let state = panel.animationsTimelineComponent.timeBlocks[0].animation.state;
+ checkPath(animationEl, state);
+
+ info("Selecting a no-delay animated node");
+ yield selectNodeAndWaitForAnimations(".animated", inspector);
+ checkDelayAndName(timelineEl, false);
+ animationEl = timelineEl.querySelector(".animation");
+ state = panel.animationsTimelineComponent.timeBlocks[0].animation.state;
+ checkPath(animationEl, state);
+
+ info("Selecting a negative-delay animated node");
+ yield selectNodeAndWaitForAnimations(".negative-delay", inspector);
+ checkDelayAndName(timelineEl, true);
+ animationEl = timelineEl.querySelector(".animation");
+ state = panel.animationsTimelineComponent.timeBlocks[0].animation.state;
+ checkPath(animationEl, state);
+});
+
+function checkDelayAndName(timelineEl, hasDelay) {
+ let delay = timelineEl.querySelector(".delay");
+
+ is(!!delay, hasDelay, "The timeline " +
+ (hasDelay ? "contains" : "does not contain") +
+ " a delay element, as expected");
+
+ if (hasDelay) {
+ let targetNode = timelineEl.querySelector(".target");
+
+ // Check that the delay element does not cause the timeline to overflow.
+ let delayLeft = Math.round(delay.getBoundingClientRect().x);
+ let sidebarWidth = Math.round(targetNode.getBoundingClientRect().width);
+ ok(delayLeft >= sidebarWidth,
+ "The delay element isn't displayed over the sidebar");
+ }
+}
+
+function checkPath(animationEl, state) {
+ // Check existance of delay path.
+ const delayPathEl = animationEl.querySelector(".delay-path");
+ if (!state.iterationCount && state.delay < 0) {
+ // Infinity
+ ok(!delayPathEl, "The delay path for Infinity should not exist");
+ return;
+ }
+ if (state.delay === 0) {
+ ok(!delayPathEl, "The delay path for zero delay should not exist");
+ return;
+ }
+ ok(delayPathEl, "The delay path should exist");
+
+ // Check delay path coordinates.
+ const pathSegList = delayPathEl.pathSegList;
+ const startingPathSeg = pathSegList.getItem(0);
+ const endingPathSeg = pathSegList.getItem(pathSegList.numberOfItems - 2);
+ if (state.delay < 0) {
+ ok(delayPathEl.classList.contains("negative"),
+ "The delay path should have 'negative' class");
+ const startingX = state.delay;
+ const endingX = 0;
+ is(startingPathSeg.x, startingX,
+ `The x of starting point should be ${ startingX }`);
+ is(endingPathSeg.x, endingX,
+ `The x of ending point should be ${ endingX }`);
+ } else {
+ ok(!delayPathEl.classList.contains("negative"),
+ "The delay path should not have 'negative' class");
+ const startingX = 0;
+ const endingX = state.delay;
+ is(startingPathSeg.x, startingX,
+ `The x of starting point should be ${ startingX }`);
+ is(endingPathSeg.x, endingX,
+ `The x of ending point should be ${ endingX }`);
+ }
+}
diff --git a/devtools/client/animationinspector/test/browser_animation_timeline_shows_endDelay.js b/devtools/client/animationinspector/test/browser_animation_timeline_shows_endDelay.js
new file mode 100644
index 000000000..0aa5c16c0
--- /dev/null
+++ b/devtools/client/animationinspector/test/browser_animation_timeline_shows_endDelay.js
@@ -0,0 +1,78 @@
+/* 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";
+
+requestLongerTimeout(2);
+
+// Check that animation endDelay is visualized in the timeline when the
+// animation is delayed.
+// Also check that negative endDelays do not overflow the UI, and are shown
+// like positive endDelays.
+
+add_task(function* () {
+ yield addTab(URL_ROOT + "doc_end_delay.html");
+ let {inspector, panel} = yield openAnimationInspector();
+
+ let selectors = ["#target1", "#target2", "#target3", "#target4"];
+ for (let i = 0; i < selectors.length; i++) {
+ let selector = selectors[i];
+ yield selectNode(selector, inspector);
+ let timelineEl = panel.animationsTimelineComponent.rootWrapperEl;
+ let animationEl = timelineEl.querySelector(".animation");
+ checkEndDelayAndName(animationEl);
+ const state =
+ panel.animationsTimelineComponent.timeBlocks[0].animation.state;
+ checkPath(animationEl, state);
+ }
+});
+
+function checkEndDelayAndName(animationEl) {
+ let endDelay = animationEl.querySelector(".end-delay");
+ let name = animationEl.querySelector(".name");
+ let targetNode = animationEl.querySelector(".target");
+
+ // Check that the endDelay element does not cause the timeline to overflow.
+ let endDelayLeft = Math.round(endDelay.getBoundingClientRect().x);
+ let sidebarWidth = Math.round(targetNode.getBoundingClientRect().width);
+ ok(endDelayLeft >= sidebarWidth,
+ "The endDelay element isn't displayed over the sidebar");
+
+ // Check that the endDelay is not displayed on top of the name.
+ let endDelayRight = Math.round(endDelay.getBoundingClientRect().right);
+ let nameLeft = Math.round(name.getBoundingClientRect().left);
+ ok(endDelayRight >= nameLeft,
+ "The endDelay element does not span over the name element");
+}
+
+function checkPath(animationEl, state) {
+ // Check existance of enddelay path.
+ const endDelayPathEl = animationEl.querySelector(".enddelay-path");
+ ok(endDelayPathEl, "The endDelay path should exist");
+
+ // Check enddelay path coordinates.
+ const pathSegList = endDelayPathEl.pathSegList;
+ const startingPathSeg = pathSegList.getItem(0);
+ const endingPathSeg = pathSegList.getItem(pathSegList.numberOfItems - 2);
+ if (state.endDelay < 0) {
+ ok(endDelayPathEl.classList.contains("negative"),
+ "The endDelay path should have 'negative' class");
+ const endingX = state.delay + state.iterationCount * state.duration;
+ const startingX = endingX + state.endDelay;
+ is(startingPathSeg.x, startingX,
+ `The x of starting point should be ${ startingX }`);
+ is(endingPathSeg.x, endingX,
+ `The x of ending point should be ${ endingX }`);
+ } else {
+ ok(!endDelayPathEl.classList.contains("negative"),
+ "The endDelay path should not have 'negative' class");
+ const startingX =
+ state.delay + state.iterationCount * state.duration;
+ const endingX = startingX + state.endDelay;
+ is(startingPathSeg.x, startingX,
+ `The x of starting point should be ${ startingX }`);
+ is(endingPathSeg.x, endingX,
+ `The x of ending point should be ${ endingX }`);
+ }
+}
diff --git a/devtools/client/animationinspector/test/browser_animation_timeline_shows_iterations.js b/devtools/client/animationinspector/test/browser_animation_timeline_shows_iterations.js
new file mode 100644
index 000000000..08e5a2620
--- /dev/null
+++ b/devtools/client/animationinspector/test/browser_animation_timeline_shows_iterations.js
@@ -0,0 +1,47 @@
+/* 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";
+
+requestLongerTimeout(2);
+
+// Check that the timeline is displays as many iteration elements as there are
+// iterations in an animation.
+
+add_task(function* () {
+ yield addTab(URL_ROOT + "doc_simple_animation.html");
+ let {inspector, panel} = yield openAnimationInspector();
+
+ info("Selecting the test node");
+ yield selectNodeAndWaitForAnimations(".delayed", inspector);
+
+ info("Getting the animation element from the panel");
+ const timelineComponent = panel.animationsTimelineComponent;
+ const timelineEl = timelineComponent.rootWrapperEl;
+ let animation = timelineEl.querySelector(".time-block");
+ // Get iteration count from summary graph path.
+ let iterationCount = getIterationCount(animation);
+
+ is(iterationCount, 10,
+ "The animation timeline contains the right number of iterations");
+ ok(!animation.querySelector(".infinity"),
+ "The summary graph does not have any elements "
+ + " that have infinity class");
+
+ info("Selecting another test node with an infinite animation");
+ yield selectNodeAndWaitForAnimations(".animated", inspector);
+
+ info("Getting the animation element from the panel again");
+ animation = timelineEl.querySelector(".time-block");
+ iterationCount = getIterationCount(animation);
+
+ is(iterationCount, 1,
+ "The animation timeline contains one iteration");
+ ok(animation.querySelector(".infinity"),
+ "The summary graph has an element that has infinity class");
+});
+
+function getIterationCount(timeblockEl) {
+ return timeblockEl.querySelectorAll(".iteration-path").length;
+}
diff --git a/devtools/client/animationinspector/test/browser_animation_timeline_shows_name_label.js b/devtools/client/animationinspector/test/browser_animation_timeline_shows_name_label.js
new file mode 100644
index 000000000..e5778c943
--- /dev/null
+++ b/devtools/client/animationinspector/test/browser_animation_timeline_shows_name_label.js
@@ -0,0 +1,46 @@
+/* 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";
+
+requestLongerTimeout(2);
+
+// Check the text content and width of name label.
+
+add_task(function* () {
+ yield addTab(URL_ROOT + "doc_simple_animation.html");
+ let {inspector, panel} = yield openAnimationInspector();
+
+ info("Selecting 'simple-animation' animation which is running on compositor");
+ yield selectNodeAndWaitForAnimations(".animated", inspector);
+ checkNameLabel(panel.animationsTimelineComponent.rootWrapperEl, "simple-animation");
+
+ info("Selecting 'no-compositor' animation which is not running on compositor");
+ yield selectNodeAndWaitForAnimations(".no-compositor", inspector);
+ checkNameLabel(panel.animationsTimelineComponent.rootWrapperEl, "no-compositor");
+});
+
+function checkNameLabel(rootWrapperEl, expectedLabelContent) {
+ const timeblockEl = rootWrapperEl.querySelector(".time-block");
+ const labelEl = rootWrapperEl.querySelector(".name div");
+ is(labelEl.textContent, expectedLabelContent,
+ `Text content of labelEl sould be ${ expectedLabelContent }`);
+
+ // Expand timeblockEl to avoid max-width of the label.
+ timeblockEl.style.width = "10000px";
+ const originalLabelWidth = labelEl.clientWidth;
+ ok(originalLabelWidth < timeblockEl.clientWidth / 2,
+ "Label width should be less than 50%");
+
+ // Set timeblockEl width to double of original label width.
+ timeblockEl.style.width = `${ originalLabelWidth * 2 }px`;
+ is(labelEl.clientWidth + labelEl.offsetLeft, originalLabelWidth,
+ `Label width + offsetLeft should be ${ originalLabelWidth }px`);
+
+ // Shrink timeblockEl to enable max-width.
+ timeblockEl.style.width = `${ originalLabelWidth }px`;
+ is(labelEl.clientWidth + labelEl.offsetLeft,
+ Math.round(timeblockEl.clientWidth / 2),
+ "Label width + offsetLeft should be half of timeblockEl");
+}
diff --git a/devtools/client/animationinspector/test/browser_animation_timeline_shows_time_info.js b/devtools/client/animationinspector/test/browser_animation_timeline_shows_time_info.js
new file mode 100644
index 000000000..f330e880e
--- /dev/null
+++ b/devtools/client/animationinspector/test/browser_animation_timeline_shows_time_info.js
@@ -0,0 +1,50 @@
+/* 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";
+
+requestLongerTimeout(2);
+
+// Check that the timeline displays animations' duration, delay iteration
+// counts and iteration start in tooltips.
+
+add_task(function* () {
+ yield addTab(URL_ROOT + "doc_simple_animation.html");
+ let {panel, controller} = yield openAnimationInspector();
+
+ info("Getting the animation element from the panel");
+ let timelineEl = panel.animationsTimelineComponent.rootWrapperEl;
+ let timeBlockNameEls = timelineEl.querySelectorAll(".time-block .name");
+
+ // Verify that each time-block's name element has a tooltip that looks sort of
+ // ok. We don't need to test the actual content.
+ [...timeBlockNameEls].forEach((el, i) => {
+ ok(el.hasAttribute("title"), "The tooltip is defined for animation " + i);
+
+ let title = el.getAttribute("title");
+ if (controller.animationPlayers[i].state.delay) {
+ ok(title.match(/Delay: [\d.-]+s/), "The tooltip shows the delay");
+ }
+ ok(title.match(/Duration: [\d.]+s/), "The tooltip shows the duration");
+ if (controller.animationPlayers[i].state.endDelay) {
+ ok(title.match(/End delay: [\d.-]+s/), "The tooltip shows the endDelay");
+ }
+ if (controller.animationPlayers[i].state.iterationCount !== 1) {
+ ok(title.match(/Repeats: /), "The tooltip shows the iterations");
+ } else {
+ ok(!title.match(/Repeats: /), "The tooltip doesn't show the iterations");
+ }
+ if (controller.animationPlayers[i].state.easing) {
+ ok(title.match(/Easing: /), "The tooltip shows the easing");
+ }
+ if (controller.animationPlayers[i].state.fill) {
+ ok(title.match(/Fill: /), "The tooltip shows the fill");
+ }
+ if (controller.animationPlayers[i].state.direction) {
+ ok(title.match(/Direction: /), "The tooltip shows the direction");
+ }
+ ok(!title.match(/Iteration start:/),
+ "The tooltip doesn't show the iteration start");
+ });
+});
diff --git a/devtools/client/animationinspector/test/browser_animation_timeline_takes_rate_into_account.js b/devtools/client/animationinspector/test/browser_animation_timeline_takes_rate_into_account.js
new file mode 100644
index 000000000..42309203a
--- /dev/null
+++ b/devtools/client/animationinspector/test/browser_animation_timeline_takes_rate_into_account.js
@@ -0,0 +1,81 @@
+/* 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";
+
+// Check that if an animation has had its playbackRate changed via the DOM, then
+// the timeline UI shows the right delay and duration.
+// Indeed, the header in the timeline UI always shows the unaltered time,
+// because there might be multiple animations displayed at the same time, some
+// of which may have a different rate than others. Those that have had their
+// rate changed have a delay = delay/rate and a duration = duration/rate.
+
+add_task(function* () {
+ yield addTab(URL_ROOT + "doc_modify_playbackRate.html");
+
+ let {panel} = yield openAnimationInspector();
+
+ let timelineEl = panel.animationsTimelineComponent.rootWrapperEl;
+
+ let timeBlocks = timelineEl.querySelectorAll(".time-block");
+ is(timeBlocks.length, 2, "2 animations are displayed");
+
+ info("The first animation has its rate set to 1, let's measure it");
+
+ let el = timeBlocks[0];
+ let duration = getDuration(el.querySelector("path"));
+ let delay = parseInt(el.querySelector(".delay").style.width, 10);
+
+ info("The second animation has its rate set to 2, so should be shorter");
+
+ let el2 = timeBlocks[1];
+ let duration2 = getDuration(el2.querySelector("path"));
+ let delay2 = parseInt(el2.querySelector(".delay").style.width, 10);
+
+ // The width are calculated by the animation-inspector dynamically depending
+ // on the size of the panel, and therefore depends on the test machine/OS.
+ // Let's not try to be too precise here and compare numbers.
+ let durationDelta = (2 * duration2) - duration;
+ ok(durationDelta <= 1, "The duration width is correct");
+ let delayDelta = (2 * delay2) - delay;
+ ok(delayDelta <= 1, "The delay width is correct");
+});
+
+function getDuration(pathEl) {
+ const pathSegList = pathEl.pathSegList;
+ // Find the index of starting iterations.
+ let startingIterationIndex = 0;
+ const firstPathSeg = pathSegList.getItem(1);
+ for (let i = 2, n = pathSegList.numberOfItems - 2; i < n; i++) {
+ // Changing point of the progress acceleration is the time.
+ const pathSeg = pathSegList.getItem(i);
+ if (firstPathSeg.y != pathSeg.y) {
+ startingIterationIndex = i;
+ break;
+ }
+ }
+ // Find the index of ending iterations.
+ let endingIterationIndex = 0;
+ let previousPathSegment = pathSegList.getItem(startingIterationIndex);
+ for (let i = startingIterationIndex + 1, n = pathSegList.numberOfItems - 2;
+ i < n; i++) {
+ // Find forwards fill-mode.
+ const pathSeg = pathSegList.getItem(i);
+ if (previousPathSegment.y == pathSeg.y) {
+ endingIterationIndex = i;
+ break;
+ }
+ previousPathSegment = pathSeg;
+ }
+ if (endingIterationIndex) {
+ // Not forwards fill-mode
+ endingIterationIndex = pathSegList.numberOfItems - 2;
+ }
+ // Return the distance of starting and ending
+ const startingIterationPathSegment =
+ pathSegList.getItem(startingIterationIndex);
+ const endingIterationPathSegment =
+ pathSegList.getItem(startingIterationIndex);
+ return endingIterationPathSegment.x - startingIterationPathSegment.x;
+}
diff --git a/devtools/client/animationinspector/test/browser_animation_timeline_ui.js b/devtools/client/animationinspector/test/browser_animation_timeline_ui.js
new file mode 100644
index 000000000..43c148482
--- /dev/null
+++ b/devtools/client/animationinspector/test/browser_animation_timeline_ui.js
@@ -0,0 +1,43 @@
+/* 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";
+
+requestLongerTimeout(2);
+
+// Check that the timeline contains the right elements.
+
+add_task(function* () {
+ yield addTab(URL_ROOT + "doc_simple_animation.html");
+ let {panel} = yield openAnimationInspector();
+
+ let timeline = panel.animationsTimelineComponent;
+ let el = timeline.rootWrapperEl;
+
+ ok(el.querySelector(".time-header"),
+ "The header element is in the DOM of the timeline");
+ ok(el.querySelectorAll(".time-header .header-item").length,
+ "The header has some time graduations");
+
+ ok(el.querySelector(".animations"),
+ "The animations container is in the DOM of the timeline");
+ is(el.querySelectorAll(".animations .animation").length,
+ timeline.animations.length,
+ "The number of animations displayed matches the number of animations");
+
+ for (let i = 0; i < timeline.animations.length; i++) {
+ let animation = timeline.animations[i];
+ let animationEl = el.querySelectorAll(".animations .animation")[i];
+
+ ok(animationEl.querySelector(".target"),
+ "The animated node target element is in the DOM");
+ ok(animationEl.querySelector(".time-block"),
+ "The timeline element is in the DOM");
+ is(animationEl.querySelector(".name").textContent,
+ animation.state.name,
+ "The name on the timeline is correct");
+ ok(animationEl.querySelector("svg path"),
+ "The timeline has svg and path element as summary graph");
+ }
+});
diff --git a/devtools/client/animationinspector/test/browser_animation_toggle_button_resets_on_navigate.js b/devtools/client/animationinspector/test/browser_animation_toggle_button_resets_on_navigate.js
new file mode 100644
index 000000000..d9a92b905
--- /dev/null
+++ b/devtools/client/animationinspector/test/browser_animation_toggle_button_resets_on_navigate.js
@@ -0,0 +1,31 @@
+/* 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";
+
+requestLongerTimeout(2);
+
+// Test that a page navigation resets the state of the global toggle button.
+
+add_task(function* () {
+ yield addTab(URL_ROOT + "doc_simple_animation.html");
+ let {inspector, panel} = yield openAnimationInspector();
+
+ info("Select the non-animated test node");
+ yield selectNodeAndWaitForAnimations(".still", inspector);
+
+ ok(!panel.toggleAllButtonEl.classList.contains("paused"),
+ "The toggle button is in its running state by default");
+
+ info("Toggle all animations, so that they pause");
+ yield panel.toggleAll();
+ ok(panel.toggleAllButtonEl.classList.contains("paused"),
+ "The toggle button now is in its paused state");
+
+ info("Reloading the page");
+ yield reloadTab(inspector);
+
+ ok(!panel.toggleAllButtonEl.classList.contains("paused"),
+ "The toggle button is back in its running state");
+});
diff --git a/devtools/client/animationinspector/test/browser_animation_toggle_button_toggles_animations.js b/devtools/client/animationinspector/test/browser_animation_toggle_button_toggles_animations.js
new file mode 100644
index 000000000..4d55e0433
--- /dev/null
+++ b/devtools/client/animationinspector/test/browser_animation_toggle_button_toggles_animations.js
@@ -0,0 +1,32 @@
+/* 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";
+
+requestLongerTimeout(2);
+
+// Test that the main toggle button actually toggles animations.
+// This test doesn't need to be extra careful about checking that *all*
+// animations have been paused (including inside iframes) because there's an
+// actor test in /devtools/server/tests/browser/ that does this.
+
+add_task(function* () {
+ yield addTab(URL_ROOT + "doc_simple_animation.html");
+ let {panel} = yield openAnimationInspector();
+
+ info("Click the toggle button");
+ yield panel.toggleAll();
+ yield checkState("paused");
+
+ info("Click again the toggle button");
+ yield panel.toggleAll();
+ yield checkState("running");
+});
+
+function* checkState(state) {
+ for (let selector of [".animated", ".multi", ".long"]) {
+ let playState = yield getAnimationPlayerState(selector);
+ is(playState, state, "The animation on node " + selector + " is " + state);
+ }
+}
diff --git a/devtools/client/animationinspector/test/browser_animation_toolbar_exists.js b/devtools/client/animationinspector/test/browser_animation_toolbar_exists.js
new file mode 100644
index 000000000..aa8b69e02
--- /dev/null
+++ b/devtools/client/animationinspector/test/browser_animation_toolbar_exists.js
@@ -0,0 +1,36 @@
+/* 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";
+
+requestLongerTimeout(2);
+
+// Test that the animation panel has a top toolbar that contains the play/pause
+// button and that is displayed at all times.
+// Also test that this toolbar gets replaced by the timeline toolbar when there
+// are animations to be displayed.
+
+add_task(function* () {
+ yield addTab(URL_ROOT + "doc_simple_animation.html");
+ let {inspector, window} = yield openAnimationInspector();
+ let doc = window.document;
+ let toolbar = doc.querySelector("#global-toolbar");
+
+ ok(toolbar, "The panel contains the toolbar element with the new UI");
+ ok(!isNodeVisible(toolbar),
+ "The toolbar is hidden while there are animations");
+
+ let timelineToolbar = doc.querySelector("#timeline-toolbar");
+ ok(timelineToolbar, "The panel contains a timeline toolbar element");
+ ok(isNodeVisible(timelineToolbar),
+ "The timeline toolbar is visible when there are animations");
+
+ info("Select a node that has no animations");
+ yield selectNodeAndWaitForAnimations(".still", inspector);
+
+ ok(isNodeVisible(toolbar),
+ "The toolbar is shown when there are no animations");
+ ok(!isNodeVisible(timelineToolbar),
+ "The timeline toolbar is hidden when there are no animations");
+});
diff --git a/devtools/client/animationinspector/test/browser_animation_ui_updates_when_animation_data_changes.js b/devtools/client/animationinspector/test/browser_animation_ui_updates_when_animation_data_changes.js
new file mode 100644
index 000000000..aa71fd9af
--- /dev/null
+++ b/devtools/client/animationinspector/test/browser_animation_ui_updates_when_animation_data_changes.js
@@ -0,0 +1,53 @@
+/* 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";
+
+requestLongerTimeout(2);
+
+// Verify that if the animation's duration, iterations or delay change in
+// content, then the widget reflects the changes.
+
+add_task(function* () {
+ yield addTab(URL_ROOT + "doc_simple_animation.html");
+ let {panel, controller, inspector} = yield openAnimationInspector();
+
+ info("Select the test node");
+ yield selectNodeAndWaitForAnimations(".animated", inspector);
+
+ let animation = controller.animationPlayers[0];
+ yield setStyle(animation, panel, "animationDuration", "5.5s");
+ yield setStyle(animation, panel, "animationIterationCount", "300");
+ yield setStyle(animation, panel, "animationDelay", "45s");
+
+ let animationsEl = panel.animationsTimelineComponent.animationsEl;
+ let timeBlockEl = animationsEl.querySelector(".time-block");
+
+ // 45s delay + (300 * 5.5)s duration
+ let expectedTotalDuration = 1695 * 1000;
+
+ // XXX: the nb and size of each iteration cannot be tested easily (displayed
+ // using a linear-gradient background and capped at 2px wide). They should
+ // be tested in bug 1173761.
+ let delayWidth = parseFloat(timeBlockEl.querySelector(".delay").style.width);
+ is(Math.round(delayWidth * expectedTotalDuration / 100), 45 * 1000,
+ "The timeline has the right delay");
+});
+
+function* setStyle(animation, panel, name, value) {
+ info("Change the animation style via the content DOM. Setting " +
+ name + " to " + value);
+
+ let onAnimationChanged = once(animation, "changed");
+ yield executeInContent("devtools:test:setStyle", {
+ selector: ".animated",
+ propertyName: name,
+ propertyValue: value
+ });
+ yield onAnimationChanged;
+
+ // Also wait for the target node previews to be loaded if the panel got
+ // refreshed as a result of this animation mutation.
+ yield waitForAllAnimationTargets(panel);
+}
diff --git a/devtools/client/animationinspector/test/doc_body_animation.html b/devtools/client/animationinspector/test/doc_body_animation.html
new file mode 100644
index 000000000..3813ea09c
--- /dev/null
+++ b/devtools/client/animationinspector/test/doc_body_animation.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <style>
+ body {
+ background-color: white;
+ color: black;
+ animation: change-background-color 3s infinite alternate;
+ }
+
+ @keyframes change-background-color {
+ to {
+ background-color: black;
+ color: white;
+ }
+ }
+ </style>
+</head>
+<body>
+ <h1>Animated body element</h1>
+</body>
+</html>
diff --git a/devtools/client/animationinspector/test/doc_end_delay.html b/devtools/client/animationinspector/test/doc_end_delay.html
new file mode 100644
index 000000000..02018bc8a
--- /dev/null
+++ b/devtools/client/animationinspector/test/doc_end_delay.html
@@ -0,0 +1,69 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <style>
+ .target {
+ width: 50px;
+ height: 50px;
+ background: blue;
+ }
+ </style>
+</head>
+<body>
+ <div id="target1" class="target"></div>
+ <div id="target2" class="target"></div>
+ <div id="target3" class="target"></div>
+ <div id="target4" class="target"></div>
+ <script>
+ /* globals KeyframeEffect, Animation */
+ "use strict";
+
+ let animations = [{
+ id: "target1",
+ frames: [{ opacity: 0, offset: 0 }, { opacity: 1, offset: 1 }],
+ timing: {
+ id: "endDelay_animation1",
+ duration: 1000000,
+ endDelay: 500000,
+ fill: "none"
+ }
+ }, {
+ id: "target2",
+ frames: [{ opacity: 0, offset: 0 }, { opacity: 1, offset: 1 }],
+ timing: {
+ id: "endDelay_animation2",
+ duration: 1000000,
+ endDelay: -500000,
+ fill: "none"
+ }
+ }, {
+ id: "target3",
+ frames: [{ opacity: 0, offset: 0 }, { opacity: 1, offset: 1 }],
+ timing: {
+ id: "endDelay_animation3",
+ duration: 1000000,
+ endDelay: -1500000,
+ fill: "forwards"
+ }
+ }, {
+ id: "target4",
+ frames: [{ opacity: 0, offset: 0 }, { opacity: 1, offset: 1 }],
+ timing: {
+ id: "endDelay_animation4",
+ duration: 100000,
+ delay: 100000,
+ endDelay: -1500000,
+ fill: "forwards"
+ }
+ }];
+
+ for (let {id, frames, timing} of animations) {
+ let effect = new KeyframeEffect(document.getElementById(id),
+ frames, timing);
+ let animation = new Animation(effect, document.timeline);
+ animation.play();
+ }
+ </script>
+</body>
+</html>
diff --git a/devtools/client/animationinspector/test/doc_frame_script.js b/devtools/client/animationinspector/test/doc_frame_script.js
new file mode 100644
index 000000000..6846c9b29
--- /dev/null
+++ b/devtools/client/animationinspector/test/doc_frame_script.js
@@ -0,0 +1,122 @@
+/* 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/ */
+/* globals addMessageListener, sendAsyncMessage */
+
+"use strict";
+
+// A helper frame-script for brower/devtools/animationinspector tests.
+
+/**
+ * Toggle (play or pause) one of the animation players of a given node.
+ * @param {Object} data
+ * - {String} selector The CSS selector to get the node (can be a "super"
+ * selector).
+ * - {Number} animationIndex The index of the node's animationPlayers to play
+ * or pause
+ * - {Boolean} pause True to pause the animation, false to play.
+ */
+addMessageListener("Test:ToggleAnimationPlayer", function (msg) {
+ let {selector, animationIndex, pause} = msg.data;
+ let node = superQuerySelector(selector);
+ if (!node) {
+ return;
+ }
+
+ let animation = node.getAnimations()[animationIndex];
+ if (pause) {
+ animation.pause();
+ } else {
+ animation.play();
+ }
+
+ sendAsyncMessage("Test:ToggleAnimationPlayer");
+});
+
+/**
+ * Change the currentTime of one of the animation players of a given node.
+ * @param {Object} data
+ * - {String} selector The CSS selector to get the node (can be a "super"
+ * selector).
+ * - {Number} animationIndex The index of the node's animationPlayers to change.
+ * - {Number} currentTime The current time to set.
+ */
+addMessageListener("Test:SetAnimationPlayerCurrentTime", function (msg) {
+ let {selector, animationIndex, currentTime} = msg.data;
+ let node = superQuerySelector(selector);
+ if (!node) {
+ return;
+ }
+
+ let animation = node.getAnimations()[animationIndex];
+ animation.currentTime = currentTime;
+
+ sendAsyncMessage("Test:SetAnimationPlayerCurrentTime");
+});
+
+/**
+ * Change the playbackRate of one of the animation players of a given node.
+ * @param {Object} data
+ * - {String} selector The CSS selector to get the node (can be a "super"
+ * selector).
+ * - {Number} animationIndex The index of the node's animationPlayers to change.
+ * - {Number} playbackRate The rate to set.
+ */
+addMessageListener("Test:SetAnimationPlayerPlaybackRate", function (msg) {
+ let {selector, animationIndex, playbackRate} = msg.data;
+ let node = superQuerySelector(selector);
+ if (!node) {
+ return;
+ }
+
+ let player = node.getAnimations()[animationIndex];
+ player.playbackRate = playbackRate;
+
+ sendAsyncMessage("Test:SetAnimationPlayerPlaybackRate");
+});
+
+/**
+ * Get the current playState of an animation player on a given node.
+ * @param {Object} data
+ * - {String} selector The CSS selector to get the node (can be a "super"
+ * selector).
+ * - {Number} animationIndex The index of the node's animationPlayers to check
+ */
+addMessageListener("Test:GetAnimationPlayerState", function (msg) {
+ let {selector, animationIndex} = msg.data;
+ let node = superQuerySelector(selector);
+ if (!node) {
+ return;
+ }
+
+ let animation = node.getAnimations()[animationIndex];
+ animation.ready.then(() => {
+ sendAsyncMessage("Test:GetAnimationPlayerState", animation.playState);
+ });
+});
+
+/**
+ * Like document.querySelector but can go into iframes too.
+ * ".container iframe || .sub-container div" will first try to find the node
+ * matched by ".container iframe" in the root document, then try to get the
+ * content document inside it, and then try to match ".sub-container div" inside
+ * this document.
+ * Any selector coming before the || separator *MUST* match a frame node.
+ * @param {String} superSelector.
+ * @return {DOMNode} The node, or null if not found.
+ */
+function superQuerySelector(superSelector, root = content.document) {
+ let frameIndex = superSelector.indexOf("||");
+ if (frameIndex === -1) {
+ return root.querySelector(superSelector);
+ }
+
+ let rootSelector = superSelector.substring(0, frameIndex).trim();
+ let childSelector = superSelector.substring(frameIndex + 2).trim();
+ root = root.querySelector(rootSelector);
+ if (!root || !root.contentWindow) {
+ return null;
+ }
+
+ return superQuerySelector(childSelector, root.contentWindow.document);
+}
diff --git a/devtools/client/animationinspector/test/doc_keyframes.html b/devtools/client/animationinspector/test/doc_keyframes.html
new file mode 100644
index 000000000..7671e09e3
--- /dev/null
+++ b/devtools/client/animationinspector/test/doc_keyframes.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <title>Yay! Keyframes!</title>
+ <style>
+ div {
+ animation: wow 100s forwards;
+ }
+ @keyframes wow {
+ 0% {
+ width: 100px;
+ height: 100px;
+ border-radius: 0px;
+ background: #f06;
+ }
+ 10% {
+ border-radius: 2px;
+ }
+ 20% {
+ transform: rotate(13deg);
+ }
+ 30% {
+ background: gold;
+ }
+ 40% {
+ filter: blur(40px);
+ }
+ 50% {
+ transform: rotate(720deg) translateX(300px) skew(-13deg);
+ }
+ 60% {
+ width: 200px;
+ height: 200px;
+ }
+ 70% {
+ border-radius: 10px;
+ }
+ 80% {
+ background: #333;
+ }
+ 90% {
+ border-radius: 50%;
+ }
+ 100% {
+ width: 500px;
+ height: 500px;
+ }
+ }
+ </style>
+</head>
+<body>
+ <div></div>
+</body>
+</html>
diff --git a/devtools/client/animationinspector/test/doc_modify_playbackRate.html b/devtools/client/animationinspector/test/doc_modify_playbackRate.html
new file mode 100644
index 000000000..7b83f1c38
--- /dev/null
+++ b/devtools/client/animationinspector/test/doc_modify_playbackRate.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <style>
+ div {
+ width: 50px;
+ height: 50px;
+ background: blue;
+ animation: move 20s 20s linear;
+ animation-fill-mode: forwards;
+ }
+
+ @keyframes move {
+ to {
+ margin-left: 200px;
+ }
+ }
+ </style>
+</head>
+<body>
+ <div></div>
+ <div class="rate"></div>
+ <script>
+ "use strict";
+
+ var el = document.querySelector(".rate");
+ var ani = el.getAnimations()[0];
+ ani.playbackRate = 2;
+ </script>
+</body>
+</html>
diff --git a/devtools/client/animationinspector/test/doc_multiple_animation_types.html b/devtools/client/animationinspector/test/doc_multiple_animation_types.html
new file mode 100644
index 000000000..318f14d0a
--- /dev/null
+++ b/devtools/client/animationinspector/test/doc_multiple_animation_types.html
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <style>
+ .ball {
+ width: 80px;
+ height: 80px;
+ border-radius: 50%;
+ }
+
+ .script-animation {
+ background: #f06;
+ }
+
+ .css-transition {
+ background: #006;
+ transition: background-color 20s;
+ }
+
+ .css-animation {
+ background: #a06;
+ animation: flash 10s forwards;
+ }
+
+ @keyframes flash {
+ 0% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0;
+ }
+ 100% {
+ opacity: 1;
+ }
+ }
+ </style>
+</head>
+<body>
+ <div class="ball script-animation"></div>
+ <div class="ball css-animation"></div>
+ <div class="ball css-transition"></div>
+
+ <script>
+ /* globals KeyframeEffect, Animation */
+ "use strict";
+
+ setTimeout(function () {
+ document.querySelector(".css-transition").style.backgroundColor = "yellow";
+ }, 0);
+
+ let effect = new KeyframeEffect(
+ document.querySelector(".script-animation"), [
+ {opacity: 1, offset: 0},
+ {opacity: .1, offset: 1}
+ ], { duration: 10000, fill: "forwards" });
+ let animation = new Animation(effect, document.timeline);
+ animation.play();
+ </script>
+</body>
+</html>
diff --git a/devtools/client/animationinspector/test/doc_negative_animation.html b/devtools/client/animationinspector/test/doc_negative_animation.html
new file mode 100644
index 000000000..ea412025b
--- /dev/null
+++ b/devtools/client/animationinspector/test/doc_negative_animation.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <style>
+ html, body {
+ margin: 0;
+ height: 100%;
+ overflow: hidden;
+ }
+
+ div {
+ position: absolute;
+ top: 0;
+ left: -500px;
+ height: 20px;
+ width: 500px;
+ color: red;
+ background: linear-gradient(to left, currentColor, currentColor 2px, transparent);
+ }
+
+ .zero {
+ color: blue;
+ top: 20px;
+ }
+
+ .positive {
+ color: green;
+ top: 40px;
+ }
+
+ .negative.move { animation: 5s -1s move linear forwards; }
+ .zero.move { animation: 5s 0s move linear forwards; }
+ .positive.move { animation: 5s 1s move linear forwards; }
+
+ @keyframes move {
+ to {
+ transform: translateX(500px);
+ }
+ }
+ </style>
+</head>
+<body>
+ <div class="negative"></div>
+ <div class="zero"></div>
+ <div class="positive"></div>
+ <script>
+ "use strict";
+
+ var negative = document.querySelector(".negative");
+ var zero = document.querySelector(".zero");
+ var positive = document.querySelector(".positive");
+
+ // The non-delayed animation starts now.
+ zero.classList.add("move");
+ // The negative-delayed animation starts in 1 second.
+ setTimeout(function () {
+ negative.classList.add("move");
+ }, 1000);
+ // The positive-delayed animation starts in 200 ms.
+ setTimeout(function () {
+ positive.classList.add("move");
+ }, 200);
+ </script>
+</body>
+</html>
diff --git a/devtools/client/animationinspector/test/doc_pseudo_elements.html b/devtools/client/animationinspector/test/doc_pseudo_elements.html
new file mode 100644
index 000000000..587608b19
--- /dev/null
+++ b/devtools/client/animationinspector/test/doc_pseudo_elements.html
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="UTF-8">
+ <title>Animated pseudo elements</title>
+ <style>
+ html, body {
+ margin: 0;
+ height: 100%;
+ width: 100%;
+ overflow: hidden;
+ display: flex;
+ justify-content: center;
+ align-items: flex-end;
+ }
+
+ body {
+ animation: color 2s linear infinite;
+ background: #333;
+ }
+
+ @keyframes color {
+ to {
+ filter: hue-rotate(360deg);
+ }
+ }
+
+ body::before,
+ body::after {
+ content: "";
+ flex-grow: 1;
+ height: 100%;
+ animation: grow 1s linear infinite alternate;
+ }
+
+ body::before {
+ background: hsl(120, 80%, 80%);
+ }
+ body::after {
+ background: hsl(240, 80%, 80%);
+ animation-delay: -.5s;
+ }
+
+ @keyframes grow {
+ 0% {height: 100%; animation-timing-function: ease-in-out;}
+ 10% {height: 80%; animation-timing-function: ease-in-out;}
+ 20% {height: 60%; animation-timing-function: ease-in-out;}
+ 30% {height: 70%; animation-timing-function: ease-in-out;}
+ 40% {height: 50%; animation-timing-function: ease-in-out;}
+ 50% {height: 30%; animation-timing-function: ease-in-out;}
+ 60% {height: 80%; animation-timing-function: ease-in-out;}
+ 70% {height: 90%; animation-timing-function: ease-in-out;}
+ 80% {height: 70%; animation-timing-function: ease-in-out;}
+ 90% {height: 60%; animation-timing-function: ease-in-out;}
+ 100% {height: 100%; animation-timing-function: ease-in-out;}
+ }
+ </style>
+ </head>
+ <body>
+ </body>
+</html> \ No newline at end of file
diff --git a/devtools/client/animationinspector/test/doc_script_animation.html b/devtools/client/animationinspector/test/doc_script_animation.html
new file mode 100644
index 000000000..b7839622e
--- /dev/null
+++ b/devtools/client/animationinspector/test/doc_script_animation.html
@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <style>
+ #target1 {
+ width: 50px;
+ height: 50px;
+ background: red;
+ }
+
+ #target2 {
+ width: 50px;
+ height: 50px;
+ background: green;
+ }
+
+ #target3 {
+ width: 50px;
+ height: 50px;
+ background: blue;
+ }
+ </style>
+</head>
+<body>
+ <div id="target1"></div>
+ <div id="target2"></div>
+ <div id="target3"></div>
+
+ <script>
+ /* globals KeyframeEffect, Animation */
+ "use strict";
+
+ let animations = [{
+ id: "target1",
+ frames: [{ opacity: 0, offset: 0 }, { opacity: 1, offset: 1 }],
+ timing: {
+ duration: 100,
+ iterations: 2,
+ iterationStart: 0.25,
+ fill: "both"
+ }
+ }, {
+ id: "target2",
+ frames: [{ opacity: 0, offset: 0 }, { opacity: 1, offset: 1 }],
+ timing: {
+ duration: 100,
+ iterations: 1,
+ iterationStart: 0.25,
+ fill: "both"
+ }
+ }, {
+ id: "target3",
+ frames: [{ opacity: 0, offset: 0 }, { opacity: 1, offset: 1 }],
+ timing: {
+ duration: 100,
+ iterations: 1.5,
+ iterationStart: 2.5,
+ fill: "both"
+ }
+ }];
+
+ for (let {id, frames, timing} of animations) {
+ let effect = new KeyframeEffect(document.getElementById(id),
+ frames, timing);
+ let animation = new Animation(effect, document.timeline);
+ animation.play();
+ }
+ </script>
+</body>
+</html>
diff --git a/devtools/client/animationinspector/test/doc_simple_animation.html b/devtools/client/animationinspector/test/doc_simple_animation.html
new file mode 100644
index 000000000..fc65a5744
--- /dev/null
+++ b/devtools/client/animationinspector/test/doc_simple_animation.html
@@ -0,0 +1,147 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <style>
+ .ball {
+ width: 80px;
+ height: 80px;
+ border-radius: 50%;
+ background: #f06;
+
+ position: absolute;
+ }
+
+ .still {
+ top: 0;
+ left: 10px;
+ }
+
+ .animated {
+ top: 100px;
+ left: 10px;
+
+ animation: simple-animation 2s infinite alternate;
+ }
+
+ .multi {
+ top: 200px;
+ left: 10px;
+
+ animation: simple-animation 2s infinite alternate,
+ other-animation 5s infinite alternate;
+ }
+
+ .delayed {
+ top: 300px;
+ left: 10px;
+ background: rebeccapurple;
+
+ animation: simple-animation 3s 60s 10;
+ }
+
+ .multi-finite {
+ top: 400px;
+ left: 10px;
+ background: yellow;
+
+ animation: simple-animation 3s,
+ other-animation 4s;
+ }
+
+ .short {
+ top: 500px;
+ left: 10px;
+ background: red;
+
+ animation: simple-animation 2s;
+ }
+
+ .long {
+ top: 600px;
+ left: 10px;
+ background: blue;
+
+ animation: simple-animation 120s;
+ }
+
+ .negative-delay {
+ top: 700px;
+ left: 10px;
+ background: gray;
+
+ animation: simple-animation 15s -10s;
+ animation-fill-mode: forwards;
+ }
+
+ .no-compositor {
+ top: 0;
+ right: 10px;
+ background: gold;
+
+ animation: no-compositor 10s cubic-bezier(.57,-0.02,1,.31) forwards;
+ }
+
+ .compositor-notall {
+ animation: compositor-notall 2s infinite;
+ }
+
+ @keyframes simple-animation {
+ 100% {
+ transform: translateX(300px);
+ }
+ }
+
+ @keyframes other-animation {
+ 100% {
+ background: blue;
+ }
+ }
+
+ @keyframes no-compositor {
+ 100% {
+ margin-right: 600px;
+ }
+ }
+
+ @keyframes compositor-notall {
+ from {
+ opacity: 0;
+ width: 0px;
+ transform: translate(0px);
+ }
+ to {
+ opacity: 1;
+ width: 100px;
+ transform: translate(100px);
+ }
+ }
+ </style>
+</head>
+<body>
+ <!-- Comment node -->
+ <div class="ball still"></div>
+ <div class="ball animated"></div>
+ <div class="ball multi"></div>
+ <div class="ball delayed"></div>
+ <div class="ball multi-finite"></div>
+ <div class="ball short"></div>
+ <div class="ball long"></div>
+ <div class="ball negative-delay"></div>
+ <div class="ball no-compositor"></div>
+ <div class="ball" id="endDelayed"></div>
+ <div class="ball compositor-notall"></div>
+ <script>
+ /* globals KeyframeEffect, Animation */
+ "use strict";
+
+ var el = document.getElementById("endDelayed");
+ let effect = new KeyframeEffect(el, [
+ { opacity: 0, offset: 0 },
+ { opacity: 1, offset: 1 }
+ ], { duration: 1000000, endDelay: 500000, fill: "none" });
+ let animation = new Animation(effect, document.timeline);
+ animation.play();
+ </script>
+</body>
+</html>
diff --git a/devtools/client/animationinspector/test/doc_timing_combination_animation.html b/devtools/client/animationinspector/test/doc_timing_combination_animation.html
new file mode 100644
index 000000000..8b39af015
--- /dev/null
+++ b/devtools/client/animationinspector/test/doc_timing_combination_animation.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <style>
+ div {
+ display: inline-block;
+ width: 100px;
+ height: 100px;
+ background-color: lime;
+ }
+ </style>
+ </head>
+ <body>
+ <script>
+ "use strict";
+
+ const delayList = [0, 50000, -50000];
+ const endDelayList = [0, 50000, -50000];
+
+ delayList.forEach(delay => {
+ endDelayList.forEach(endDelay => {
+ const el = document.createElement("div");
+ document.body.appendChild(el);
+ el.animate({ opacity: [0, 1] },
+ { duration: 200000,
+ iterations: 1,
+ fill: "both",
+ delay: delay,
+ endDelay: endDelay });
+ });
+ });
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/animationinspector/test/head.js b/devtools/client/animationinspector/test/head.js
new file mode 100644
index 000000000..554a36430
--- /dev/null
+++ b/devtools/client/animationinspector/test/head.js
@@ -0,0 +1,426 @@
+/* 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/ */
+/* eslint no-unused-vars: [2, {"vars": "local", "args": "none"}] */
+
+"use strict";
+
+/* import-globals-from ../../inspector/test/head.js */
+// Import the inspector's head.js first (which itself imports shared-head.js).
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/inspector/test/head.js",
+ this);
+
+const FRAME_SCRIPT_URL = CHROME_URL_ROOT + "doc_frame_script.js";
+const COMMON_FRAME_SCRIPT_URL = "chrome://devtools/content/shared/frame-script-utils.js";
+const TAB_NAME = "animationinspector";
+const ANIMATION_L10N =
+ new LocalizationHelper("devtools/client/locales/animationinspector.properties");
+
+// Auto clean-up when a test ends
+registerCleanupFunction(function* () {
+ yield closeAnimationInspector();
+
+ while (gBrowser.tabs.length > 1) {
+ gBrowser.removeCurrentTab();
+ }
+});
+
+// Clean-up all prefs that might have been changed during a test run
+// (safer here because if the test fails, then the pref is never reverted)
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("devtools.debugger.log");
+});
+
+// WebAnimations API is not enabled by default in all release channels yet, see
+// Bug 1264101.
+function enableWebAnimationsAPI() {
+ return new Promise(resolve => {
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.animations-api.core.enabled", true]
+ ]}, resolve);
+ });
+}
+
+/**
+ * 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 tab object when the url is loaded
+ */
+var _addTab = addTab;
+addTab = function (url) {
+ return enableWebAnimationsAPI().then(() => _addTab(url)).then(tab => {
+ let browser = tab.linkedBrowser;
+ info("Loading the helper frame script " + FRAME_SCRIPT_URL);
+ browser.messageManager.loadFrameScript(FRAME_SCRIPT_URL, false);
+ info("Loading the helper frame script " + COMMON_FRAME_SCRIPT_URL);
+ browser.messageManager.loadFrameScript(COMMON_FRAME_SCRIPT_URL, false);
+ return tab;
+ });
+};
+
+/**
+ * Reload the current tab location.
+ * @param {InspectorPanel} inspector The instance of InspectorPanel currently
+ * loaded in the toolbox
+ */
+function* reloadTab(inspector) {
+ let onNewRoot = inspector.once("new-root");
+ yield executeInContent("devtools:test:reload", {}, {}, false);
+ yield onNewRoot;
+ yield inspector.once("inspector-updated");
+}
+
+/*
+ * Set the inspector's current selection to a node or to the first match of the
+ * given css selector and wait for the animations to be displayed
+ * @param {String|NodeFront}
+ * data The node to select
+ * @param {InspectorPanel} inspector
+ * The instance of InspectorPanel currently
+ * loaded in the toolbox
+ * @param {String} reason
+ * Defaults to "test" which instructs the inspector not
+ * to highlight the node upon selection
+ * @return {Promise} Resolves when the inspector is updated with the new node
+ and animations of its subtree are properly displayed.
+ */
+var selectNodeAndWaitForAnimations = Task.async(
+ function* (data, inspector, reason = "test") {
+ yield selectNode(data, inspector, reason);
+
+ // We want to make sure the rest of the test waits for the animations to
+ // be properly displayed (wait for all target DOM nodes to be previewed).
+ let {AnimationsPanel} = inspector.sidebar.getWindowForTab(TAB_NAME);
+ yield waitForAllAnimationTargets(AnimationsPanel);
+ }
+);
+
+/**
+ * Check if there are the expected number of animations being displayed in the
+ * panel right now.
+ * @param {AnimationsPanel} panel
+ * @param {Number} nbAnimations The expected number of animations.
+ * @param {String} msg An optional string to be used as the assertion message.
+ */
+function assertAnimationsDisplayed(panel, nbAnimations, msg = "") {
+ msg = msg || `There are ${nbAnimations} animations in the panel`;
+ is(panel.animationsTimelineComponent
+ .animationsEl
+ .querySelectorAll(".animation").length, nbAnimations, msg);
+}
+
+/**
+ * Takes an Inspector panel that was just created, and waits
+ * for a "inspector-updated" event as well as the animation inspector
+ * sidebar to be ready. Returns a promise once these are completed.
+ *
+ * @param {InspectorPanel} inspector
+ * @return {Promise}
+ */
+var waitForAnimationInspectorReady = Task.async(function* (inspector) {
+ let win = inspector.sidebar.getWindowForTab(TAB_NAME);
+ let updated = inspector.once("inspector-updated");
+
+ // In e10s, if we wait for underlying toolbox actors to
+ // load (by setting DevToolsUtils.testing to true), we miss the
+ // "animationinspector-ready" event on the sidebar, so check to see if the
+ // iframe is already loaded.
+ let tabReady = win.document.readyState === "complete" ?
+ promise.resolve() :
+ inspector.sidebar.once("animationinspector-ready");
+
+ return promise.all([updated, tabReady]);
+});
+
+/**
+ * Open the toolbox, with the inspector tool visible and the animationinspector
+ * sidebar selected.
+ * @return a promise that resolves when the inspector is ready.
+ */
+var openAnimationInspector = Task.async(function* () {
+ let {inspector, toolbox} = yield openInspectorSidebarTab(TAB_NAME);
+
+ info("Waiting for the inspector and sidebar to be ready");
+ yield waitForAnimationInspectorReady(inspector);
+
+ let win = inspector.sidebar.getWindowForTab(TAB_NAME);
+ let {AnimationsController, AnimationsPanel} = win;
+
+ info("Waiting for the animation controller and panel to be ready");
+ if (AnimationsPanel.initialized) {
+ yield AnimationsPanel.initialized;
+ } else {
+ yield AnimationsPanel.once(AnimationsPanel.PANEL_INITIALIZED);
+ }
+
+ // Make sure we wait for all animations to be loaded (especially their target
+ // nodes to be lazily displayed). This is safe to do even if there are no
+ // animations displayed.
+ yield waitForAllAnimationTargets(AnimationsPanel);
+
+ return {
+ toolbox: toolbox,
+ inspector: inspector,
+ controller: AnimationsController,
+ panel: AnimationsPanel,
+ window: win
+ };
+});
+
+/**
+ * Close the toolbox.
+ * @return a promise that resolves when the toolbox has closed.
+ */
+var closeAnimationInspector = Task.async(function* () {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ yield gDevTools.closeToolbox(target);
+});
+
+/**
+ * Wait for a content -> chrome message on the message manager (the window
+ * messagemanager is used).
+ * @param {String} name The message name
+ * @return {Promise} A promise that resolves to the response data when the
+ * message has been received
+ */
+function waitForContentMessage(name) {
+ info("Expecting message " + name + " from content");
+
+ let mm = gBrowser.selectedBrowser.messageManager;
+
+ return new Promise(resolve => {
+ mm.addMessageListener(name, function onMessage(msg) {
+ mm.removeMessageListener(name, onMessage);
+ resolve(msg.data);
+ });
+ });
+}
+
+/**
+ * Send an async message to the frame script (chrome -> content) and wait for a
+ * response message with the same name (content -> chrome).
+ * @param {String} name The message name. Should be one of the messages defined
+ * in doc_frame_script.js
+ * @param {Object} data Optional data to send along
+ * @param {Object} objects Optional CPOW objects to send along
+ * @param {Boolean} expectResponse If set to false, don't wait for a response
+ * with the same name from the content script. Defaults to true.
+ * @return {Promise} Resolves to the response data if a response is expected,
+ * immediately resolves otherwise
+ */
+function executeInContent(name, data = {}, objects = {},
+ expectResponse = true) {
+ info("Sending message " + name + " to content");
+ let mm = gBrowser.selectedBrowser.messageManager;
+
+ mm.sendAsyncMessage(name, data, objects);
+ if (expectResponse) {
+ return waitForContentMessage(name);
+ }
+
+ return promise.resolve();
+}
+
+/**
+ * Get the current playState of an animation player on a given node.
+ */
+var getAnimationPlayerState = Task.async(function* (selector,
+ animationIndex = 0) {
+ let playState = yield executeInContent("Test:GetAnimationPlayerState",
+ {selector, animationIndex});
+ return playState;
+});
+
+/**
+ * Is the given node visible in the page (rendered in the frame tree).
+ * @param {DOMNode}
+ * @return {Boolean}
+ */
+function isNodeVisible(node) {
+ return !!node.getClientRects().length;
+}
+
+/**
+ * Wait for all AnimationTargetNode instances to be fully loaded
+ * (fetched their related actor and rendered), and return them.
+ * @param {AnimationsPanel} panel
+ * @return {Array} all AnimationTargetNode instances
+ */
+var waitForAllAnimationTargets = Task.async(function* (panel) {
+ let targets = panel.animationsTimelineComponent.targetNodes;
+ yield promise.all(targets.map(t => {
+ if (!t.previewer.nodeFront) {
+ return t.once("target-retrieved");
+ }
+ return false;
+ }));
+ return targets;
+});
+
+/**
+ * Check the scrubber element in the timeline is moving.
+ * @param {AnimationPanel} panel
+ * @param {Boolean} isMoving
+ */
+function* assertScrubberMoving(panel, isMoving) {
+ let timeline = panel.animationsTimelineComponent;
+
+ if (isMoving) {
+ // If we expect the scrubber to move, just wait for a couple of
+ // timeline-data-changed events and compare times.
+ let {time: time1} = yield timeline.once("timeline-data-changed");
+ let {time: time2} = yield timeline.once("timeline-data-changed");
+ ok(time2 > time1, "The scrubber is moving");
+ } else {
+ // If instead we expect the scrubber to remain at its position, just wait
+ // for some time and make sure timeline-data-changed isn't emitted.
+ let hasMoved = false;
+ timeline.once("timeline-data-changed", () => {
+ hasMoved = true;
+ });
+ yield new Promise(r => setTimeout(r, 500));
+ ok(!hasMoved, "The scrubber is not moving");
+ }
+}
+
+/**
+ * Click the play/pause button in the timeline toolbar and wait for animations
+ * to update.
+ * @param {AnimationsPanel} panel
+ */
+function* clickTimelinePlayPauseButton(panel) {
+ let onUiUpdated = panel.once(panel.UI_UPDATED_EVENT);
+
+ let btn = panel.playTimelineButtonEl;
+ let win = btn.ownerDocument.defaultView;
+ EventUtils.sendMouseEvent({type: "click"}, btn, win);
+
+ yield onUiUpdated;
+ yield waitForAllAnimationTargets(panel);
+}
+
+/**
+ * Click the rewind button in the timeline toolbar and wait for animations to
+ * update.
+ * @param {AnimationsPanel} panel
+ */
+function* clickTimelineRewindButton(panel) {
+ let onUiUpdated = panel.once(panel.UI_UPDATED_EVENT);
+
+ let btn = panel.rewindTimelineButtonEl;
+ let win = btn.ownerDocument.defaultView;
+ EventUtils.sendMouseEvent({type: "click"}, btn, win);
+
+ yield onUiUpdated;
+ yield waitForAllAnimationTargets(panel);
+}
+
+/**
+ * Select a rate inside the playback rate selector in the timeline toolbar and
+ * wait for animations to update.
+ * @param {AnimationsPanel} panel
+ * @param {Number} rate The new rate value to be selected
+ */
+function* changeTimelinePlaybackRate(panel, rate) {
+ let onUiUpdated = panel.once(panel.UI_UPDATED_EVENT);
+
+ let select = panel.rateSelectorEl.firstChild;
+ let win = select.ownerDocument.defaultView;
+
+ // Get the right option.
+ let option = [...select.options].filter(o => o.value === rate + "")[0];
+ if (!option) {
+ ok(false,
+ "Could not find an option for rate " + rate + " in the rate selector. " +
+ "Values are: " + [...select.options].map(o => o.value));
+ return;
+ }
+
+ // Simulate the right events to select the option in the drop-down.
+ EventUtils.synthesizeMouseAtCenter(select, {type: "mousedown"}, win);
+ EventUtils.synthesizeMouseAtCenter(option, {type: "mouseup"}, win);
+
+ yield onUiUpdated;
+ yield waitForAllAnimationTargets(panel);
+
+ // Simulate a mousemove outside of the rate selector area to avoid subsequent
+ // tests from failing because of unwanted mouseover events.
+ EventUtils.synthesizeMouseAtCenter(
+ win.document.querySelector("#timeline-toolbar"), {type: "mousemove"}, win);
+}
+
+/**
+ * Prevent the toolbox common highlighter from making backend requests.
+ * @param {Toolbox} toolbox
+ */
+function disableHighlighter(toolbox) {
+ toolbox._highlighter = {
+ showBoxModel: () => new Promise(r => r()),
+ hideBoxModel: () => new Promise(r => r()),
+ pick: () => new Promise(r => r()),
+ cancelPick: () => new Promise(r => r()),
+ destroy: () => {},
+ traits: {}
+ };
+}
+
+/**
+ * Click on an animation in the timeline to select/unselect it.
+ * @param {AnimationsPanel} panel The panel instance.
+ * @param {Number} index The index of the animation to click on.
+ * @param {Boolean} shouldClose Set to true if clicking should close the
+ * animation.
+ * @return {Promise} resolves to the animation whose state has changed.
+ */
+function* clickOnAnimation(panel, index, shouldClose) {
+ let timeline = panel.animationsTimelineComponent;
+
+ // Expect a selection event.
+ let onSelectionChanged = timeline.once(shouldClose
+ ? "animation-unselected"
+ : "animation-selected");
+
+ // If we're opening the animation, also wait for the keyframes-retrieved
+ // event.
+ let onReady = shouldClose
+ ? Promise.resolve()
+ : timeline.details[index].once("keyframes-retrieved");
+
+ info("Click on animation " + index + " in the timeline");
+ let timeBlock = timeline.rootWrapperEl.querySelectorAll(".time-block")[index];
+ EventUtils.sendMouseEvent({type: "click"}, timeBlock,
+ timeBlock.ownerDocument.defaultView);
+
+ yield onReady;
+ return yield onSelectionChanged;
+}
+
+/**
+ * Get an instance of the Keyframes component from the timeline.
+ * @param {AnimationsPanel} panel The panel instance.
+ * @param {Number} animationIndex The index of the animation in the timeline.
+ * @param {String} propertyName The name of the animated property.
+ * @return {Keyframes} The Keyframes component instance.
+ */
+function getKeyframeComponent(panel, animationIndex, propertyName) {
+ let timeline = panel.animationsTimelineComponent;
+ let detailsComponent = timeline.details[animationIndex];
+ return detailsComponent.keyframeComponents
+ .find(c => c.propertyName === propertyName);
+}
+
+/**
+ * Get a keyframe element from the timeline.
+ * @param {AnimationsPanel} panel The panel instance.
+ * @param {Number} animationIndex The index of the animation in the timeline.
+ * @param {String} propertyName The name of the animated property.
+ * @param {Index} keyframeIndex The index of the keyframe.
+ * @return {DOMNode} The keyframe element.
+ */
+function getKeyframeEl(panel, animationIndex, propertyName, keyframeIndex) {
+ let keyframeComponent = getKeyframeComponent(panel, animationIndex,
+ propertyName);
+ return keyframeComponent.keyframesEl
+ .querySelectorAll(".frame")[keyframeIndex];
+}
diff --git a/devtools/client/animationinspector/test/unit/.eslintrc.js b/devtools/client/animationinspector/test/unit/.eslintrc.js
new file mode 100644
index 000000000..59adf410a
--- /dev/null
+++ b/devtools/client/animationinspector/test/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/client/animationinspector/test/unit/test_findOptimalTimeInterval.js b/devtools/client/animationinspector/test/unit/test_findOptimalTimeInterval.js
new file mode 100644
index 000000000..64451bfdf
--- /dev/null
+++ b/devtools/client/animationinspector/test/unit/test_findOptimalTimeInterval.js
@@ -0,0 +1,81 @@
+/* -*- 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/ */
+/* eslint no-eval:0 */
+
+"use strict";
+
+var Cu = Components.utils;
+const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const {findOptimalTimeInterval} = require("devtools/client/animationinspector/utils");
+
+// This test array contains objects that are used to test the
+// findOptimalTimeInterval function. Each object should have the following
+// properties:
+// - desc: an optional string that will be printed out
+// - minTimeInterval: a number that represents the minimum time in ms
+// that should be displayed in one interval
+// - expectedInterval: a number that you expect the findOptimalTimeInterval
+// function to return as a result.
+// Optionally you can pass a string where `interval` is the calculated
+// interval, this string will be eval'd and tested to be truthy.
+const TEST_DATA = [{
+ desc: "With no minTimeInterval, expect the interval to be 0",
+ minTimeInterval: null,
+ expectedInterval: 0
+}, {
+ desc: "With a minTimeInterval of 0 ms, expect the interval to be 0",
+ minTimeInterval: 0,
+ expectedInterval: 0
+}, {
+ desc: "With a minInterval of 1ms, expect the interval to be the 1ms too",
+ minTimeInterval: 1,
+ expectedInterval: 1
+}, {
+ desc: "With a very small minTimeInterval, expect the interval to be 1ms",
+ minTimeInterval: 1e-31,
+ expectedInterval: 1
+}, {
+ desc: "With a minInterval of 2.5ms, expect the interval to be 2.5ms too",
+ minTimeInterval: 2.5,
+ expectedInterval: 2.5
+}, {
+ desc: "With a minInterval of 5ms, expect the interval to be 5ms too",
+ minTimeInterval: 5,
+ expectedInterval: 5
+}, {
+ desc: "With a minInterval of 7ms, expect the interval to be the next " +
+ "multiple of 5",
+ minTimeInterval: 7,
+ expectedInterval: 10
+}, {
+ minTimeInterval: 20,
+ expectedInterval: 25
+}, {
+ minTimeInterval: 33,
+ expectedInterval: 50
+}, {
+ minTimeInterval: 987,
+ expectedInterval: 1000
+}, {
+ minTimeInterval: 1234,
+ expectedInterval: 2500
+}, {
+ minTimeInterval: 9800,
+ expectedInterval: 10000
+}];
+
+function run_test() {
+ for (let {minTimeInterval, desc, expectedInterval} of TEST_DATA) {
+ do_print(`Testing minTimeInterval: ${minTimeInterval}.
+ Expecting ${expectedInterval}.`);
+
+ let interval = findOptimalTimeInterval(minTimeInterval);
+ if (typeof expectedInterval == "string") {
+ ok(eval(expectedInterval), desc);
+ } else {
+ equal(interval, expectedInterval, desc);
+ }
+ }
+}
diff --git a/devtools/client/animationinspector/test/unit/test_formatStopwatchTime.js b/devtools/client/animationinspector/test/unit/test_formatStopwatchTime.js
new file mode 100644
index 000000000..12584a2a4
--- /dev/null
+++ b/devtools/client/animationinspector/test/unit/test_formatStopwatchTime.js
@@ -0,0 +1,62 @@
+/* -*- 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";
+
+var Cu = Components.utils;
+const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const {formatStopwatchTime} = require("devtools/client/animationinspector/utils");
+
+const TEST_DATA = [{
+ desc: "Formatting 0",
+ time: 0,
+ expected: "00:00.000"
+}, {
+ desc: "Formatting null",
+ time: null,
+ expected: "00:00.000"
+}, {
+ desc: "Formatting undefined",
+ time: undefined,
+ expected: "00:00.000"
+}, {
+ desc: "Formatting a small number of ms",
+ time: 13,
+ expected: "00:00.013"
+}, {
+ desc: "Formatting a slightly larger number of ms",
+ time: 500,
+ expected: "00:00.500"
+}, {
+ desc: "Formatting 1 second",
+ time: 1000,
+ expected: "00:01.000"
+}, {
+ desc: "Formatting a number of seconds",
+ time: 1532,
+ expected: "00:01.532"
+}, {
+ desc: "Formatting a big number of seconds",
+ time: 58450,
+ expected: "00:58.450"
+}, {
+ desc: "Formatting 1 minute",
+ time: 60000,
+ expected: "01:00.000"
+}, {
+ desc: "Formatting a number of minutes",
+ time: 263567,
+ expected: "04:23.567"
+}, {
+ desc: "Formatting a large number of minutes",
+ time: 1000 * 60 * 60 * 3,
+ expected: "180:00.000"
+}];
+
+function run_test() {
+ for (let {desc, time, expected} of TEST_DATA) {
+ equal(formatStopwatchTime(time), expected, desc);
+ }
+}
diff --git a/devtools/client/animationinspector/test/unit/test_getCssPropertyName.js b/devtools/client/animationinspector/test/unit/test_getCssPropertyName.js
new file mode 100644
index 000000000..21470d5fb
--- /dev/null
+++ b/devtools/client/animationinspector/test/unit/test_getCssPropertyName.js
@@ -0,0 +1,27 @@
+/* -*- 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";
+
+var Cu = Components.utils;
+const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const {getCssPropertyName} = require("devtools/client/animationinspector/components/animation-details");
+
+const TEST_DATA = [{
+ jsName: "alllowercase",
+ cssName: "alllowercase"
+}, {
+ jsName: "borderWidth",
+ cssName: "border-width"
+}, {
+ jsName: "borderTopRightRadius",
+ cssName: "border-top-right-radius"
+}];
+
+function run_test() {
+ for (let {jsName, cssName} of TEST_DATA) {
+ equal(getCssPropertyName(jsName), cssName);
+ }
+}
diff --git a/devtools/client/animationinspector/test/unit/test_timeScale.js b/devtools/client/animationinspector/test/unit/test_timeScale.js
new file mode 100644
index 000000000..9ee4b8a59
--- /dev/null
+++ b/devtools/client/animationinspector/test/unit/test_timeScale.js
@@ -0,0 +1,207 @@
+/* -*- 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";
+
+var Cu = Components.utils;
+const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const {TimeScale} = require("devtools/client/animationinspector/utils");
+const TEST_ANIMATIONS = [{
+ desc: "Testing a few standard animations",
+ animations: [{
+ previousStartTime: 500,
+ delay: 0,
+ duration: 1000,
+ iterationCount: 1,
+ playbackRate: 1
+ }, {
+ previousStartTime: 400,
+ delay: 100,
+ duration: 10,
+ iterationCount: 100,
+ playbackRate: 1
+ }, {
+ previousStartTime: 50,
+ delay: 1000,
+ duration: 100,
+ iterationCount: 20,
+ playbackRate: 1
+ }],
+ expectedMinStart: 50,
+ expectedMaxEnd: 3050
+}, {
+ desc: "Testing a single negative-delay animation",
+ animations: [{
+ previousStartTime: 100,
+ delay: -100,
+ duration: 100,
+ iterationCount: 1,
+ playbackRate: 1
+ }],
+ expectedMinStart: 0,
+ expectedMaxEnd: 100
+}, {
+ desc: "Testing a single negative-delay animation with a different rate",
+ animations: [{
+ previousStartTime: 3500,
+ delay: -1000,
+ duration: 10000,
+ iterationCount: 2,
+ playbackRate: 2
+ }],
+ expectedMinStart: 3000,
+ expectedMaxEnd: 13000
+}];
+
+const TEST_STARTTIME_TO_DISTANCE = [{
+ time: 50,
+ expectedDistance: 0
+}, {
+ time: 50,
+ expectedDistance: 0
+}, {
+ time: 3050,
+ expectedDistance: 100
+}, {
+ time: 1550,
+ expectedDistance: 50
+}];
+
+const TEST_DURATION_TO_DISTANCE = [{
+ time: 3000,
+ expectedDistance: 100
+}, {
+ time: 0,
+ expectedDistance: 0
+}];
+
+const TEST_DISTANCE_TO_TIME = [{
+ distance: 100,
+ expectedTime: 3050
+}, {
+ distance: 0,
+ expectedTime: 50
+}, {
+ distance: 25,
+ expectedTime: 800
+}];
+
+const TEST_DISTANCE_TO_RELATIVE_TIME = [{
+ distance: 100,
+ expectedTime: 3000
+}, {
+ distance: 0,
+ expectedTime: 0
+}, {
+ distance: 25,
+ expectedTime: 750
+}];
+
+const TEST_FORMAT_TIME_MS = [{
+ time: 0,
+ expectedFormattedTime: "0ms"
+}, {
+ time: 3540.341,
+ expectedFormattedTime: "3540ms"
+}, {
+ time: 1.99,
+ expectedFormattedTime: "2ms"
+}, {
+ time: 4000,
+ expectedFormattedTime: "4000ms"
+}];
+
+const TEST_FORMAT_TIME_S = [{
+ time: 0,
+ expectedFormattedTime: "0.0s"
+}, {
+ time: 3540.341,
+ expectedFormattedTime: "3.5s"
+}, {
+ time: 1.99,
+ expectedFormattedTime: "0.0s"
+}, {
+ time: 4000,
+ expectedFormattedTime: "4.0s"
+}, {
+ time: 102540,
+ expectedFormattedTime: "102.5s"
+}, {
+ time: 102940,
+ expectedFormattedTime: "102.9s"
+}];
+
+function run_test() {
+ do_print("Check the default min/max range values");
+ equal(TimeScale.minStartTime, Infinity);
+ equal(TimeScale.maxEndTime, 0);
+
+ for (let {desc, animations, expectedMinStart, expectedMaxEnd} of
+ TEST_ANIMATIONS) {
+ do_print("Test adding a few animations: " + desc);
+ for (let state of animations) {
+ TimeScale.addAnimation(state);
+ }
+
+ do_print("Checking the time scale range");
+ equal(TimeScale.minStartTime, expectedMinStart);
+ equal(TimeScale.maxEndTime, expectedMaxEnd);
+
+ do_print("Test reseting the animations");
+ TimeScale.reset();
+ equal(TimeScale.minStartTime, Infinity);
+ equal(TimeScale.maxEndTime, 0);
+ }
+
+ do_print("Add a set of animations again");
+ for (let state of TEST_ANIMATIONS[0].animations) {
+ TimeScale.addAnimation(state);
+ }
+
+ do_print("Test converting start times to distances");
+ for (let {time, expectedDistance} of TEST_STARTTIME_TO_DISTANCE) {
+ let distance = TimeScale.startTimeToDistance(time);
+ equal(distance, expectedDistance);
+ }
+
+ do_print("Test converting durations to distances");
+ for (let {time, expectedDistance} of TEST_DURATION_TO_DISTANCE) {
+ let distance = TimeScale.durationToDistance(time);
+ equal(distance, expectedDistance);
+ }
+
+ do_print("Test converting distances to times");
+ for (let {distance, expectedTime} of TEST_DISTANCE_TO_TIME) {
+ let time = TimeScale.distanceToTime(distance);
+ equal(time, expectedTime);
+ }
+
+ do_print("Test converting distances to relative times");
+ for (let {distance, expectedTime} of TEST_DISTANCE_TO_RELATIVE_TIME) {
+ let time = TimeScale.distanceToRelativeTime(distance);
+ equal(time, expectedTime);
+ }
+
+ do_print("Test formatting times (millis)");
+ for (let {time, expectedFormattedTime} of TEST_FORMAT_TIME_MS) {
+ let formattedTime = TimeScale.formatTime(time);
+ equal(formattedTime, expectedFormattedTime);
+ }
+
+ // Add 1 more animation to increase the range and test more time formatting
+ // cases.
+ TimeScale.addAnimation({
+ startTime: 3000,
+ duration: 5000,
+ delay: 0,
+ iterationCount: 1
+ });
+
+ do_print("Test formatting times (seconds)");
+ for (let {time, expectedFormattedTime} of TEST_FORMAT_TIME_S) {
+ let formattedTime = TimeScale.formatTime(time);
+ equal(formattedTime, expectedFormattedTime);
+ }
+}
diff --git a/devtools/client/animationinspector/test/unit/test_timeScale_dimensions.js b/devtools/client/animationinspector/test/unit/test_timeScale_dimensions.js
new file mode 100644
index 000000000..f6d80e60b
--- /dev/null
+++ b/devtools/client/animationinspector/test/unit/test_timeScale_dimensions.js
@@ -0,0 +1,54 @@
+/* -*- 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.
+ https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const Cu = Components.utils;
+const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const {TimeScale} = require("devtools/client/animationinspector/utils");
+
+const TEST_ENDDELAY_X = [{
+ desc: "Testing positive-endDelay animations",
+ animations: [{
+ previousStartTime: 0,
+ duration: 500,
+ playbackRate: 1,
+ iterationCount: 3,
+ delay: 500,
+ endDelay: 500
+ }],
+ expectedEndDelayX: 80
+}, {
+ desc: "Testing negative-endDelay animations",
+ animations: [{
+ previousStartTime: 0,
+ duration: 500,
+ playbackRate: 1,
+ iterationCount: 9,
+ delay: 500,
+ endDelay: -500
+ }],
+ expectedEndDelayX: 90
+}];
+
+function run_test() {
+ do_print("Test calculating endDelayX");
+
+ // Be independent of possible prior tests
+ TimeScale.reset();
+
+ for (let {desc, animations, expectedEndDelayX} of TEST_ENDDELAY_X) {
+ do_print(`Adding animations: ${desc}`);
+
+ for (let state of animations) {
+ TimeScale.addAnimation(state);
+
+ let {endDelayX} = TimeScale.getAnimationDimensions({state});
+ equal(endDelayX, expectedEndDelayX);
+
+ TimeScale.reset();
+ }
+ }
+}
diff --git a/devtools/client/animationinspector/test/unit/xpcshell.ini b/devtools/client/animationinspector/test/unit/xpcshell.ini
new file mode 100644
index 000000000..c88e01cf9
--- /dev/null
+++ b/devtools/client/animationinspector/test/unit/xpcshell.ini
@@ -0,0 +1,12 @@
+[DEFAULT]
+tags = devtools
+head =
+tail =
+firefox-appdir = browser
+skip-if = toolkit == 'android'
+
+[test_findOptimalTimeInterval.js]
+[test_formatStopwatchTime.js]
+[test_getCssPropertyName.js]
+[test_timeScale.js]
+[test_timeScale_dimensions.js]
diff --git a/devtools/client/animationinspector/utils.js b/devtools/client/animationinspector/utils.js
new file mode 100644
index 000000000..4b6891ac1
--- /dev/null
+++ b/devtools/client/animationinspector/utils.js
@@ -0,0 +1,275 @@
+/* -*- 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";
+
+loader.lazyRequireGetter(this, "EventEmitter", "devtools/shared/event-emitter");
+
+const { LocalizationHelper } = require("devtools/shared/l10n");
+const L10N =
+ new LocalizationHelper("devtools/client/locales/animationinspector.properties");
+
+// How many times, maximum, can we loop before we find the optimal time
+// interval in the timeline graph.
+const OPTIMAL_TIME_INTERVAL_MAX_ITERS = 100;
+// Time graduations should be multiple of one of these number.
+const OPTIMAL_TIME_INTERVAL_MULTIPLES = [1, 2.5, 5];
+
+const MILLIS_TIME_FORMAT_MAX_DURATION = 4000;
+
+/**
+ * DOM node creation helper function.
+ * @param {Object} Options to customize the node to be created.
+ * - nodeType {String} Optional, defaults to "div",
+ * - attributes {Object} Optional attributes object like
+ * {attrName1:value1, attrName2: value2, ...}
+ * - parent {DOMNode} Mandatory node to append the newly created node to.
+ * - textContent {String} Optional text for the node.
+ * - namespace {String} Optional namespace
+ * @return {DOMNode} The newly created node.
+ */
+function createNode(options) {
+ if (!options.parent) {
+ throw new Error("Missing parent DOMNode to create new node");
+ }
+
+ let type = options.nodeType || "div";
+ let node =
+ options.namespace
+ ? options.parent.ownerDocument.createElementNS(options.namespace, type)
+ : options.parent.ownerDocument.createElement(type);
+
+ for (let name in options.attributes || {}) {
+ let value = options.attributes[name];
+ node.setAttribute(name, value);
+ }
+
+ if (options.textContent) {
+ node.textContent = options.textContent;
+ }
+
+ options.parent.appendChild(node);
+ return node;
+}
+
+exports.createNode = createNode;
+
+/**
+ * Find the optimal interval between time graduations in the animation timeline
+ * graph based on a minimum time interval
+ * @param {Number} minTimeInterval Minimum time in ms in one interval
+ * @return {Number} The optimal interval time in ms
+ */
+function findOptimalTimeInterval(minTimeInterval) {
+ let numIters = 0;
+ let multiplier = 1;
+
+ if (!minTimeInterval) {
+ return 0;
+ }
+
+ let interval;
+ while (true) {
+ for (let i = 0; i < OPTIMAL_TIME_INTERVAL_MULTIPLES.length; i++) {
+ interval = OPTIMAL_TIME_INTERVAL_MULTIPLES[i] * multiplier;
+ if (minTimeInterval <= interval) {
+ return interval;
+ }
+ }
+ if (++numIters > OPTIMAL_TIME_INTERVAL_MAX_ITERS) {
+ return interval;
+ }
+ multiplier *= 10;
+ }
+}
+
+exports.findOptimalTimeInterval = findOptimalTimeInterval;
+
+/**
+ * Format a timestamp (in ms) as a mm:ss.mmm string.
+ * @param {Number} time
+ * @return {String}
+ */
+function formatStopwatchTime(time) {
+ // Format falsy values as 0
+ if (!time) {
+ return "00:00.000";
+ }
+
+ let milliseconds = parseInt(time % 1000, 10);
+ let seconds = parseInt((time / 1000) % 60, 10);
+ let minutes = parseInt((time / (1000 * 60)), 10);
+
+ let pad = (nb, max) => {
+ if (nb < max) {
+ return new Array((max + "").length - (nb + "").length + 1).join("0") + nb;
+ }
+ return nb;
+ };
+
+ minutes = pad(minutes, 10);
+ seconds = pad(seconds, 10);
+ milliseconds = pad(milliseconds, 100);
+
+ return `${minutes}:${seconds}.${milliseconds}`;
+}
+
+exports.formatStopwatchTime = formatStopwatchTime;
+
+/**
+ * The TimeScale helper object is used to know which size should something be
+ * displayed with in the animation panel, depending on the animations that are
+ * currently displayed.
+ * If there are 5 animations displayed, and the first one starts at 10000ms and
+ * the last one ends at 20000ms, then this helper can be used to convert any
+ * time in this range to a distance in pixels.
+ *
+ * For the helper to know how to convert, it needs to know all the animations.
+ * Whenever a new animation is added to the panel, addAnimation(state) should be
+ * called. reset() can be called to start over.
+ */
+var TimeScale = {
+ minStartTime: Infinity,
+ maxEndTime: 0,
+
+ /**
+ * Add a new animation to time scale.
+ * @param {Object} state A PlayerFront.state object.
+ */
+ addAnimation: function (state) {
+ let {previousStartTime, delay, duration, endDelay,
+ iterationCount, playbackRate} = state;
+
+ endDelay = typeof endDelay === "undefined" ? 0 : endDelay;
+ let toRate = v => v / playbackRate;
+ let minZero = v => Math.max(v, 0);
+ let rateRelativeDuration =
+ toRate(duration * (!iterationCount ? 1 : iterationCount));
+ // Negative-delayed animations have their startTimes set such that we would
+ // be displaying the delay outside the time window if we didn't take it into
+ // account here.
+ let relevantDelay = delay < 0 ? toRate(delay) : 0;
+ previousStartTime = previousStartTime || 0;
+
+ let startTime = toRate(minZero(delay)) +
+ rateRelativeDuration +
+ endDelay;
+ this.minStartTime = Math.min(
+ this.minStartTime,
+ previousStartTime +
+ relevantDelay +
+ Math.min(startTime, 0)
+ );
+ let length = toRate(delay) +
+ rateRelativeDuration +
+ toRate(minZero(endDelay));
+ let endTime = previousStartTime + length;
+ this.maxEndTime = Math.max(this.maxEndTime, endTime);
+ },
+
+ /**
+ * Reset the current time scale.
+ */
+ reset: function () {
+ this.minStartTime = Infinity;
+ this.maxEndTime = 0;
+ },
+
+ /**
+ * Convert a startTime to a distance in %, in the current time scale.
+ * @param {Number} time
+ * @return {Number}
+ */
+ startTimeToDistance: function (time) {
+ time -= this.minStartTime;
+ return this.durationToDistance(time);
+ },
+
+ /**
+ * Convert a duration to a distance in %, in the current time scale.
+ * @param {Number} time
+ * @return {Number}
+ */
+ durationToDistance: function (duration) {
+ return duration * 100 / this.getDuration();
+ },
+
+ /**
+ * Convert a distance in % to a time, in the current time scale.
+ * @param {Number} distance
+ * @return {Number}
+ */
+ distanceToTime: function (distance) {
+ return this.minStartTime + (this.getDuration() * distance / 100);
+ },
+
+ /**
+ * Convert a distance in % to a time, in the current time scale.
+ * The time will be relative to the current minimum start time.
+ * @param {Number} distance
+ * @return {Number}
+ */
+ distanceToRelativeTime: function (distance) {
+ let time = this.distanceToTime(distance);
+ return time - this.minStartTime;
+ },
+
+ /**
+ * Depending on the time scale, format the given time as milliseconds or
+ * seconds.
+ * @param {Number} time
+ * @return {String} The formatted time string.
+ */
+ formatTime: function (time) {
+ // Format in milliseconds if the total duration is short enough.
+ if (this.getDuration() <= MILLIS_TIME_FORMAT_MAX_DURATION) {
+ return L10N.getFormatStr("timeline.timeGraduationLabel", time.toFixed(0));
+ }
+
+ // Otherwise format in seconds.
+ return L10N.getFormatStr("player.timeLabel", (time / 1000).toFixed(1));
+ },
+
+ getDuration: function () {
+ return this.maxEndTime - this.minStartTime;
+ },
+
+ /**
+ * Given an animation, get the various dimensions (in %) useful to draw the
+ * animation in the timeline.
+ */
+ getAnimationDimensions: function ({state}) {
+ let start = state.previousStartTime || 0;
+ let duration = state.duration;
+ let rate = state.playbackRate;
+ let count = state.iterationCount;
+ let delay = state.delay || 0;
+ let endDelay = state.endDelay || 0;
+
+ // The start position.
+ let x = this.startTimeToDistance(start + (delay / rate));
+ // The width for a single iteration.
+ let w = this.durationToDistance(duration / rate);
+ // The width for all iterations.
+ let iterationW = w * (count || 1);
+ // The start position of the delay.
+ let delayX = delay < 0 ? x : this.startTimeToDistance(start);
+ // The width of the delay.
+ let delayW = this.durationToDistance(Math.abs(delay) / rate);
+ // The width of the delay if it is negative, 0 otherwise.
+ let negativeDelayW = delay < 0 ? delayW : 0;
+ // The width of the endDelay.
+ let endDelayW = this.durationToDistance(Math.abs(endDelay) / rate);
+ // The start position of the endDelay.
+ let endDelayX = endDelay < 0 ? x + iterationW - endDelayW
+ : x + iterationW;
+
+ return {x, w, iterationW, delayX, delayW, negativeDelayW,
+ endDelayX, endDelayW};
+ }
+};
+
+exports.TimeScale = TimeScale;
diff --git a/devtools/client/canvasdebugger/callslist.js b/devtools/client/canvasdebugger/callslist.js
new file mode 100644
index 000000000..a6fd132c0
--- /dev/null
+++ b/devtools/client/canvasdebugger/callslist.js
@@ -0,0 +1,526 @@
+/* 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/. */
+/* import-globals-from canvasdebugger.js */
+/* globals window, document */
+"use strict";
+
+/**
+ * Functions handling details about a single recorded animation frame snapshot
+ * (the calls list, rendering preview, thumbnails filmstrip etc.).
+ */
+var CallsListView = Heritage.extend(WidgetMethods, {
+ /**
+ * Initialization function, called when the tool is started.
+ */
+ initialize: function () {
+ this.widget = new SideMenuWidget($("#calls-list"));
+ this._slider = $("#calls-slider");
+ this._searchbox = $("#calls-searchbox");
+ this._filmstrip = $("#snapshot-filmstrip");
+
+ this._onSelect = this._onSelect.bind(this);
+ this._onSlideMouseDown = this._onSlideMouseDown.bind(this);
+ this._onSlideMouseUp = this._onSlideMouseUp.bind(this);
+ this._onSlide = this._onSlide.bind(this);
+ this._onSearch = this._onSearch.bind(this);
+ this._onScroll = this._onScroll.bind(this);
+ this._onExpand = this._onExpand.bind(this);
+ this._onStackFileClick = this._onStackFileClick.bind(this);
+ this._onThumbnailClick = this._onThumbnailClick.bind(this);
+
+ this.widget.addEventListener("select", this._onSelect, false);
+ this._slider.addEventListener("mousedown", this._onSlideMouseDown, false);
+ this._slider.addEventListener("mouseup", this._onSlideMouseUp, false);
+ this._slider.addEventListener("change", this._onSlide, false);
+ this._searchbox.addEventListener("input", this._onSearch, false);
+ this._filmstrip.addEventListener("wheel", this._onScroll, false);
+ },
+
+ /**
+ * Destruction function, called when the tool is closed.
+ */
+ destroy: function () {
+ this.widget.removeEventListener("select", this._onSelect, false);
+ this._slider.removeEventListener("mousedown", this._onSlideMouseDown, false);
+ this._slider.removeEventListener("mouseup", this._onSlideMouseUp, false);
+ this._slider.removeEventListener("change", this._onSlide, false);
+ this._searchbox.removeEventListener("input", this._onSearch, false);
+ this._filmstrip.removeEventListener("wheel", this._onScroll, false);
+ },
+
+ /**
+ * Populates this container with a list of function calls.
+ *
+ * @param array functionCalls
+ * A list of function call actors received from the backend.
+ */
+ showCalls: function (functionCalls) {
+ this.empty();
+
+ for (let i = 0, len = functionCalls.length; i < len; i++) {
+ let call = functionCalls[i];
+
+ let view = document.createElement("vbox");
+ view.className = "call-item-view devtools-monospace";
+ view.setAttribute("flex", "1");
+
+ let contents = document.createElement("hbox");
+ contents.className = "call-item-contents";
+ contents.setAttribute("align", "center");
+ contents.addEventListener("dblclick", this._onExpand);
+ view.appendChild(contents);
+
+ let index = document.createElement("label");
+ index.className = "plain call-item-index";
+ index.setAttribute("flex", "1");
+ index.setAttribute("value", i + 1);
+
+ let gutter = document.createElement("hbox");
+ gutter.className = "call-item-gutter";
+ gutter.appendChild(index);
+ contents.appendChild(gutter);
+
+ if (call.callerPreview) {
+ let context = document.createElement("label");
+ context.className = "plain call-item-context";
+ context.setAttribute("value", call.callerPreview);
+ contents.appendChild(context);
+
+ let separator = document.createElement("label");
+ separator.className = "plain call-item-separator";
+ separator.setAttribute("value", ".");
+ contents.appendChild(separator);
+ }
+
+ let name = document.createElement("label");
+ name.className = "plain call-item-name";
+ name.setAttribute("value", call.name);
+ contents.appendChild(name);
+
+ let argsPreview = document.createElement("label");
+ argsPreview.className = "plain call-item-args";
+ argsPreview.setAttribute("crop", "end");
+ argsPreview.setAttribute("flex", "100");
+ // Getters and setters are displayed differently from regular methods.
+ if (call.type == CallWatcherFront.METHOD_FUNCTION) {
+ argsPreview.setAttribute("value", "(" + call.argsPreview + ")");
+ } else {
+ argsPreview.setAttribute("value", " = " + call.argsPreview);
+ }
+ contents.appendChild(argsPreview);
+
+ let location = document.createElement("label");
+ location.className = "plain call-item-location";
+ location.setAttribute("value", getFileName(call.file) + ":" + call.line);
+ location.setAttribute("crop", "start");
+ location.setAttribute("flex", "1");
+ location.addEventListener("mousedown", this._onExpand);
+ contents.appendChild(location);
+
+ // Append a function call item to this container.
+ this.push([view], {
+ staged: true,
+ attachment: {
+ actor: call
+ }
+ });
+
+ // Highlight certain calls that are probably more interesting than
+ // everything else, making it easier to quickly glance over them.
+ if (CanvasFront.DRAW_CALLS.has(call.name)) {
+ view.setAttribute("draw-call", "");
+ }
+ if (CanvasFront.INTERESTING_CALLS.has(call.name)) {
+ view.setAttribute("interesting-call", "");
+ }
+ }
+
+ // Flushes all the prepared function call items into this container.
+ this.commit();
+ window.emit(EVENTS.CALL_LIST_POPULATED);
+
+ // Resetting the function selection slider's value (shown in this
+ // container's toolbar) would trigger a selection event, which should be
+ // ignored in this case.
+ this._ignoreSliderChanges = true;
+ this._slider.value = 0;
+ this._slider.max = functionCalls.length - 1;
+ this._ignoreSliderChanges = false;
+ },
+
+ /**
+ * Displays an image in the rendering preview of this container, generated
+ * for the specified draw call in the recorded animation frame snapshot.
+ *
+ * @param array screenshot
+ * A single "snapshot-image" instance received from the backend.
+ */
+ showScreenshot: function (screenshot) {
+ let { index, width, height, scaling, flipped, pixels } = screenshot;
+
+ let screenshotNode = $("#screenshot-image");
+ screenshotNode.setAttribute("flipped", flipped);
+ drawBackground("screenshot-rendering", width, height, pixels);
+
+ let dimensionsNode = $("#screenshot-dimensions");
+ let actualWidth = (width / scaling) | 0;
+ let actualHeight = (height / scaling) | 0;
+ dimensionsNode.setAttribute("value",
+ SHARED_L10N.getFormatStr("dimensions", actualWidth, actualHeight));
+
+ window.emit(EVENTS.CALL_SCREENSHOT_DISPLAYED);
+ },
+
+ /**
+ * Populates this container's footer with a list of thumbnails, one generated
+ * for each draw call in the recorded animation frame snapshot.
+ *
+ * @param array thumbnails
+ * An array of "snapshot-image" instances received from the backend.
+ */
+ showThumbnails: function (thumbnails) {
+ while (this._filmstrip.hasChildNodes()) {
+ this._filmstrip.firstChild.remove();
+ }
+ for (let thumbnail of thumbnails) {
+ this.appendThumbnail(thumbnail);
+ }
+
+ window.emit(EVENTS.THUMBNAILS_DISPLAYED);
+ },
+
+ /**
+ * Displays an image in the thumbnails list of this container, generated
+ * for the specified draw call in the recorded animation frame snapshot.
+ *
+ * @param array thumbnail
+ * A single "snapshot-image" instance received from the backend.
+ */
+ appendThumbnail: function (thumbnail) {
+ let { index, width, height, flipped, pixels } = thumbnail;
+
+ let thumbnailNode = document.createElementNS(HTML_NS, "canvas");
+ thumbnailNode.setAttribute("flipped", flipped);
+ thumbnailNode.width = Math.max(CanvasFront.THUMBNAIL_SIZE, width);
+ thumbnailNode.height = Math.max(CanvasFront.THUMBNAIL_SIZE, height);
+ drawImage(thumbnailNode, width, height, pixels, { centered: true });
+
+ thumbnailNode.className = "filmstrip-thumbnail";
+ thumbnailNode.onmousedown = e => this._onThumbnailClick(e, index);
+ thumbnailNode.setAttribute("index", index);
+ this._filmstrip.appendChild(thumbnailNode);
+ },
+
+ /**
+ * Sets the currently highlighted thumbnail in this container.
+ * A screenshot will always correlate to a thumbnail in the filmstrip,
+ * both being identified by the same 'index' of the context function call.
+ *
+ * @param number index
+ * The context function call's index.
+ */
+ set highlightedThumbnail(index) {
+ let currHighlightedThumbnail = $(".filmstrip-thumbnail[index='" + index + "']");
+ if (currHighlightedThumbnail == null) {
+ return;
+ }
+
+ let prevIndex = this._highlightedThumbnailIndex;
+ let prevHighlightedThumbnail = $(".filmstrip-thumbnail[index='" + prevIndex + "']");
+ if (prevHighlightedThumbnail) {
+ prevHighlightedThumbnail.removeAttribute("highlighted");
+ }
+
+ currHighlightedThumbnail.setAttribute("highlighted", "");
+ currHighlightedThumbnail.scrollIntoView();
+ this._highlightedThumbnailIndex = index;
+ },
+
+ /**
+ * Gets the currently highlighted thumbnail in this container.
+ * @return number
+ */
+ get highlightedThumbnail() {
+ return this._highlightedThumbnailIndex;
+ },
+
+ /**
+ * The select listener for this container.
+ */
+ _onSelect: function ({ detail: callItem }) {
+ if (!callItem) {
+ return;
+ }
+
+ // Some of the stepping buttons don't make sense specifically while the
+ // last function call is selected.
+ if (this.selectedIndex == this.itemCount - 1) {
+ $("#resume").setAttribute("disabled", "true");
+ $("#step-over").setAttribute("disabled", "true");
+ $("#step-out").setAttribute("disabled", "true");
+ } else {
+ $("#resume").removeAttribute("disabled");
+ $("#step-over").removeAttribute("disabled");
+ $("#step-out").removeAttribute("disabled");
+ }
+
+ // Correlate the currently selected item with the function selection
+ // slider's value. Avoid triggering a redundant selection event.
+ this._ignoreSliderChanges = true;
+ this._slider.value = this.selectedIndex;
+ this._ignoreSliderChanges = false;
+
+ // Can't generate screenshots for function call actors loaded from disk.
+ // XXX: Bug 984844.
+ if (callItem.attachment.actor.isLoadedFromDisk) {
+ return;
+ }
+
+ // To keep continuous selection buttery smooth (for example, while pressing
+ // the DOWN key or moving the slider), only display the screenshot after
+ // any kind of user input stops.
+ setConditionalTimeout("screenshot-display", SCREENSHOT_DISPLAY_DELAY, () => {
+ return !this._isSliding;
+ }, () => {
+ let frameSnapshot = SnapshotsListView.selectedItem.attachment.actor;
+ let functionCall = callItem.attachment.actor;
+ frameSnapshot.generateScreenshotFor(functionCall).then(screenshot => {
+ this.showScreenshot(screenshot);
+ this.highlightedThumbnail = screenshot.index;
+ }).catch(e => console.error(e));
+ });
+ },
+
+ /**
+ * The mousedown listener for the call selection slider.
+ */
+ _onSlideMouseDown: function () {
+ this._isSliding = true;
+ },
+
+ /**
+ * The mouseup listener for the call selection slider.
+ */
+ _onSlideMouseUp: function () {
+ this._isSliding = false;
+ },
+
+ /**
+ * The change listener for the call selection slider.
+ */
+ _onSlide: function () {
+ // Avoid performing any operations when programatically changing the value.
+ if (this._ignoreSliderChanges) {
+ return;
+ }
+ let selectedFunctionCallIndex = this.selectedIndex = this._slider.value;
+
+ // While sliding, immediately show the most relevant thumbnail for a
+ // function call, for a nice diff-like animation effect between draws.
+ let thumbnails = SnapshotsListView.selectedItem.attachment.thumbnails;
+ let thumbnail = getThumbnailForCall(thumbnails, selectedFunctionCallIndex);
+
+ // Avoid drawing and highlighting if the selected function call has the
+ // same thumbnail as the last one.
+ if (thumbnail.index == this.highlightedThumbnail) {
+ return;
+ }
+ // If a thumbnail wasn't found (e.g. the backend avoids creating thumbnails
+ // when rendering offscreen), simply defer to the first available one.
+ if (thumbnail.index == -1) {
+ thumbnail = thumbnails[0];
+ }
+
+ let { index, width, height, flipped, pixels } = thumbnail;
+ this.highlightedThumbnail = index;
+
+ let screenshotNode = $("#screenshot-image");
+ screenshotNode.setAttribute("flipped", flipped);
+ drawBackground("screenshot-rendering", width, height, pixels);
+ },
+
+ /**
+ * The input listener for the calls searchbox.
+ */
+ _onSearch: function (e) {
+ let lowerCaseSearchToken = this._searchbox.value.toLowerCase();
+
+ this.filterContents(e => {
+ let call = e.attachment.actor;
+ let name = call.name.toLowerCase();
+ let file = call.file.toLowerCase();
+ let line = call.line.toString().toLowerCase();
+ let args = call.argsPreview.toLowerCase();
+
+ return name.includes(lowerCaseSearchToken) ||
+ file.includes(lowerCaseSearchToken) ||
+ line.includes(lowerCaseSearchToken) ||
+ args.includes(lowerCaseSearchToken);
+ });
+ },
+
+ /**
+ * The wheel listener for the filmstrip that contains all the thumbnails.
+ */
+ _onScroll: function (e) {
+ this._filmstrip.scrollLeft += e.deltaX;
+ },
+
+ /**
+ * The click/dblclick listener for an item or location url in this container.
+ * When expanding an item, it's corresponding call stack will be displayed.
+ */
+ _onExpand: function (e) {
+ let callItem = this.getItemForElement(e.target);
+ let view = $(".call-item-view", callItem.target);
+
+ // If the call stack nodes were already created, simply re-show them
+ // or jump to the corresponding file and line in the Debugger if a
+ // location link was clicked.
+ if (view.hasAttribute("call-stack-populated")) {
+ let isExpanded = view.getAttribute("call-stack-expanded") == "true";
+
+ // If clicking on the location, jump to the Debugger.
+ if (e.target.classList.contains("call-item-location")) {
+ let { file, line } = callItem.attachment.actor;
+ this._viewSourceInDebugger(file, line);
+ return;
+ }
+ // Otherwise hide the call stack.
+ else {
+ view.setAttribute("call-stack-expanded", !isExpanded);
+ $(".call-item-stack", view).hidden = isExpanded;
+ return;
+ }
+ }
+
+ let list = document.createElement("vbox");
+ list.className = "call-item-stack";
+ view.setAttribute("call-stack-populated", "");
+ view.setAttribute("call-stack-expanded", "true");
+ view.appendChild(list);
+
+ /**
+ * Creates a function call nodes in this container for a stack.
+ */
+ let display = stack => {
+ for (let i = 1; i < stack.length; i++) {
+ let call = stack[i];
+
+ let contents = document.createElement("hbox");
+ contents.className = "call-item-stack-fn";
+ contents.style.paddingInlineStart = (i * STACK_FUNC_INDENTATION) + "px";
+
+ let name = document.createElement("label");
+ name.className = "plain call-item-stack-fn-name";
+ name.setAttribute("value", "↳ " + call.name + "()");
+ contents.appendChild(name);
+
+ let spacer = document.createElement("spacer");
+ spacer.setAttribute("flex", "100");
+ contents.appendChild(spacer);
+
+ let location = document.createElement("label");
+ location.className = "plain call-item-stack-fn-location";
+ location.setAttribute("value", getFileName(call.file) + ":" + call.line);
+ location.setAttribute("crop", "start");
+ location.setAttribute("flex", "1");
+ location.addEventListener("mousedown", e => this._onStackFileClick(e, call));
+ contents.appendChild(location);
+
+ list.appendChild(contents);
+ }
+
+ window.emit(EVENTS.CALL_STACK_DISPLAYED);
+ };
+
+ // If this animation snapshot is loaded from disk, there are no corresponding
+ // backend actors available and the data is immediately available.
+ let functionCall = callItem.attachment.actor;
+ if (functionCall.isLoadedFromDisk) {
+ display(functionCall.stack);
+ }
+ // ..otherwise we need to request the function call stack from the backend.
+ else {
+ callItem.attachment.actor.getDetails().then(fn => display(fn.stack));
+ }
+ },
+
+ /**
+ * The click listener for a location link in the call stack.
+ *
+ * @param string file
+ * The url of the source owning the function.
+ * @param number line
+ * The line of the respective function.
+ */
+ _onStackFileClick: function (e, { file, line }) {
+ this._viewSourceInDebugger(file, line);
+ },
+
+ /**
+ * The click listener for a thumbnail in the filmstrip.
+ *
+ * @param number index
+ * The function index in the recorded animation frame snapshot.
+ */
+ _onThumbnailClick: function (e, index) {
+ this.selectedIndex = index;
+ },
+
+ /**
+ * The click listener for the "resume" button in this container's toolbar.
+ */
+ _onResume: function () {
+ // Jump to the next draw call in the recorded animation frame snapshot.
+ let drawCall = getNextDrawCall(this.items, this.selectedItem);
+ if (drawCall) {
+ this.selectedItem = drawCall;
+ return;
+ }
+
+ // If there are no more draw calls, just jump to the last context call.
+ this._onStepOut();
+ },
+
+ /**
+ * The click listener for the "step over" button in this container's toolbar.
+ */
+ _onStepOver: function () {
+ this.selectedIndex++;
+ },
+
+ /**
+ * The click listener for the "step in" button in this container's toolbar.
+ */
+ _onStepIn: function () {
+ if (this.selectedIndex == -1) {
+ this._onResume();
+ return;
+ }
+ let callItem = this.selectedItem;
+ let { file, line } = callItem.attachment.actor;
+ this._viewSourceInDebugger(file, line);
+ },
+
+ /**
+ * The click listener for the "step out" button in this container's toolbar.
+ */
+ _onStepOut: function () {
+ this.selectedIndex = this.itemCount - 1;
+ },
+
+ /**
+ * Opens the specified file and line in the debugger. Falls back to Firefox's View Source.
+ */
+ _viewSourceInDebugger: function (file, line) {
+ gToolbox.viewSourceInDebugger(file, line).then(success => {
+ if (success) {
+ window.emit(EVENTS.SOURCE_SHOWN_IN_JS_DEBUGGER);
+ } else {
+ window.emit(EVENTS.SOURCE_NOT_FOUND_IN_JS_DEBUGGER);
+ }
+ });
+ }
+});
diff --git a/devtools/client/canvasdebugger/canvasdebugger.js b/devtools/client/canvasdebugger/canvasdebugger.js
new file mode 100644
index 000000000..c46cc6d0c
--- /dev/null
+++ b/devtools/client/canvasdebugger/canvasdebugger.js
@@ -0,0 +1,341 @@
+/* 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 { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
+const { SideMenuWidget } = require("resource://devtools/client/shared/widgets/SideMenuWidget.jsm");
+const promise = require("promise");
+const Services = require("Services");
+const EventEmitter = require("devtools/shared/event-emitter");
+const { CallWatcherFront } = require("devtools/shared/fronts/call-watcher");
+const { CanvasFront } = require("devtools/shared/fronts/canvas");
+const DevToolsUtils = require("devtools/shared/DevToolsUtils");
+const flags = require("devtools/shared/flags");
+const { LocalizationHelper } = require("devtools/shared/l10n");
+const { PluralForm } = require("devtools/shared/plural-form");
+const { Heritage, WidgetMethods, setNamedTimeout, clearNamedTimeout,
+ setConditionalTimeout } = require("devtools/client/shared/widgets/view-helpers");
+
+const CANVAS_ACTOR_RECORDING_ATTEMPT = flags.testing ? 500 : 5000;
+
+const { Task } = require("devtools/shared/task");
+
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "NetworkHelper", function () {
+ return require("devtools/shared/webconsole/network-helper");
+});
+
+// The panel's window global is an EventEmitter firing the following events:
+const EVENTS = {
+ // When the UI is reset from tab navigation.
+ UI_RESET: "CanvasDebugger:UIReset",
+
+ // When all the animation frame snapshots are removed by the user.
+ SNAPSHOTS_LIST_CLEARED: "CanvasDebugger:SnapshotsListCleared",
+
+ // When an animation frame snapshot starts/finishes being recorded, and
+ // whether it was completed succesfully or cancelled.
+ SNAPSHOT_RECORDING_STARTED: "CanvasDebugger:SnapshotRecordingStarted",
+ SNAPSHOT_RECORDING_FINISHED: "CanvasDebugger:SnapshotRecordingFinished",
+ SNAPSHOT_RECORDING_COMPLETED: "CanvasDebugger:SnapshotRecordingCompleted",
+ SNAPSHOT_RECORDING_CANCELLED: "CanvasDebugger:SnapshotRecordingCancelled",
+
+ // When an animation frame snapshot was selected and all its data displayed.
+ SNAPSHOT_RECORDING_SELECTED: "CanvasDebugger:SnapshotRecordingSelected",
+
+ // After all the function calls associated with an animation frame snapshot
+ // are displayed in the UI.
+ CALL_LIST_POPULATED: "CanvasDebugger:CallListPopulated",
+
+ // After the stack associated with a call in an animation frame snapshot
+ // is displayed in the UI.
+ CALL_STACK_DISPLAYED: "CanvasDebugger:CallStackDisplayed",
+
+ // After a screenshot associated with a call in an animation frame snapshot
+ // is displayed in the UI.
+ CALL_SCREENSHOT_DISPLAYED: "CanvasDebugger:ScreenshotDisplayed",
+
+ // After all the thumbnails associated with an animation frame snapshot
+ // are displayed in the UI.
+ THUMBNAILS_DISPLAYED: "CanvasDebugger:ThumbnailsDisplayed",
+
+ // When a source is shown in the JavaScript Debugger at a specific location.
+ SOURCE_SHOWN_IN_JS_DEBUGGER: "CanvasDebugger:SourceShownInJsDebugger",
+ SOURCE_NOT_FOUND_IN_JS_DEBUGGER: "CanvasDebugger:SourceNotFoundInJsDebugger"
+};
+XPCOMUtils.defineConstant(this, "EVENTS", EVENTS);
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const STRINGS_URI = "devtools/client/locales/canvasdebugger.properties";
+const SHARED_STRINGS_URI = "devtools/client/locales/shared.properties";
+
+const SNAPSHOT_START_RECORDING_DELAY = 10; // ms
+const SNAPSHOT_DATA_EXPORT_MAX_BLOCK = 1000; // ms
+const SNAPSHOT_DATA_DISPLAY_DELAY = 10; // ms
+const SCREENSHOT_DISPLAY_DELAY = 100; // ms
+const STACK_FUNC_INDENTATION = 14; // px
+
+// This identifier string is simply used to tentatively ascertain whether or not
+// a JSON loaded from disk is actually something generated by this tool or not.
+// It isn't, of course, a definitive verification, but a Good Enoughâ„¢
+// approximation before continuing the import. Don't localize this.
+const CALLS_LIST_SERIALIZER_IDENTIFIER = "Recorded Animation Frame Snapshot";
+const CALLS_LIST_SERIALIZER_VERSION = 1;
+const CALLS_LIST_SLOW_SAVE_DELAY = 100; // ms
+
+/**
+ * The current target and the Canvas front, set by this tool's host.
+ */
+var gToolbox, gTarget, gFront;
+
+/**
+ * Initializes the canvas debugger controller and views.
+ */
+function startupCanvasDebugger() {
+ return promise.all([
+ EventsHandler.initialize(),
+ SnapshotsListView.initialize(),
+ CallsListView.initialize()
+ ]);
+}
+
+/**
+ * Destroys the canvas debugger controller and views.
+ */
+function shutdownCanvasDebugger() {
+ return promise.all([
+ EventsHandler.destroy(),
+ SnapshotsListView.destroy(),
+ CallsListView.destroy()
+ ]);
+}
+
+/**
+ * Functions handling target-related lifetime events.
+ */
+var EventsHandler = {
+ /**
+ * Listen for events emitted by the current tab target.
+ */
+ initialize: function () {
+ // Make sure the backend is prepared to handle <canvas> contexts.
+ // Since actors are created lazily on the first request to them, we need to send an
+ // early request to ensure the CallWatcherActor is running and watching for new window
+ // globals.
+ gFront.setup({ reload: false });
+
+ this._onTabNavigated = this._onTabNavigated.bind(this);
+ gTarget.on("will-navigate", this._onTabNavigated);
+ gTarget.on("navigate", this._onTabNavigated);
+ },
+
+ /**
+ * Remove events emitted by the current tab target.
+ */
+ destroy: function () {
+ gTarget.off("will-navigate", this._onTabNavigated);
+ gTarget.off("navigate", this._onTabNavigated);
+ },
+
+ /**
+ * Called for each location change in the debugged tab.
+ */
+ _onTabNavigated: function (event) {
+ if (event != "will-navigate") {
+ return;
+ }
+
+ // Reset UI.
+ SnapshotsListView.empty();
+ CallsListView.empty();
+
+ $("#record-snapshot").removeAttribute("checked");
+ $("#record-snapshot").removeAttribute("disabled");
+ $("#record-snapshot").hidden = false;
+
+ $("#reload-notice").hidden = true;
+ $("#empty-notice").hidden = false;
+ $("#waiting-notice").hidden = true;
+
+ $("#debugging-pane-contents").hidden = true;
+ $("#screenshot-container").hidden = true;
+ $("#snapshot-filmstrip").hidden = true;
+
+ window.emit(EVENTS.UI_RESET);
+ }
+};
+
+/**
+ * Localization convenience methods.
+ */
+var L10N = new LocalizationHelper(STRINGS_URI);
+var SHARED_L10N = new LocalizationHelper(SHARED_STRINGS_URI);
+
+/**
+ * Convenient way of emitting events from the panel window.
+ */
+EventEmitter.decorate(this);
+
+/**
+ * DOM query helpers.
+ */
+var $ = (selector, target = document) => target.querySelector(selector);
+var $all = (selector, target = document) => target.querySelectorAll(selector);
+
+/**
+ * Gets the fileName part of a string which happens to be an URL.
+ */
+function getFileName(url) {
+ try {
+ let { fileName } = NetworkHelper.nsIURL(url);
+ return fileName || "/";
+ } catch (e) {
+ // This doesn't look like a url, or nsIURL can't handle it.
+ return "";
+ }
+}
+
+/**
+ * Gets an image data object containing a buffer large enough to hold
+ * width * height pixels.
+ *
+ * This method avoids allocating memory and tries to reuse a common buffer
+ * as much as possible.
+ *
+ * @param number w
+ * The desired image data storage width.
+ * @param number h
+ * The desired image data storage height.
+ * @return ImageData
+ * The requested image data buffer.
+ */
+function getImageDataStorage(ctx, w, h) {
+ let storage = getImageDataStorage.cache;
+ if (storage && storage.width == w && storage.height == h) {
+ return storage;
+ }
+ return getImageDataStorage.cache = ctx.createImageData(w, h);
+}
+
+// The cache used in the `getImageDataStorage` function.
+getImageDataStorage.cache = null;
+
+/**
+ * Draws image data into a canvas.
+ *
+ * This method makes absolutely no assumptions about the canvas element
+ * dimensions, or pre-existing rendering. It's a dumb proxy that copies pixels.
+ *
+ * @param HTMLCanvasElement canvas
+ * The canvas element to put the image data into.
+ * @param number width
+ * The image data width.
+ * @param number height
+ * The image data height.
+ * @param array pixels
+ * An array buffer view of the image data.
+ * @param object options
+ * Additional options supported by this operation:
+ * - centered: specifies whether the image data should be centered
+ * when copied in the canvas; this is useful when the
+ * supplied pixels don't completely cover the canvas.
+ */
+function drawImage(canvas, width, height, pixels, options = {}) {
+ let ctx = canvas.getContext("2d");
+
+ // FrameSnapshot actors return "snapshot-image" type instances with just an
+ // empty pixel array if the source image is completely transparent.
+ if (pixels.length <= 1) {
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ return;
+ }
+
+ let imageData = getImageDataStorage(ctx, width, height);
+ imageData.data.set(pixels);
+
+ if (options.centered) {
+ let left = (canvas.width - width) / 2;
+ let top = (canvas.height - height) / 2;
+ ctx.putImageData(imageData, left, top);
+ } else {
+ ctx.putImageData(imageData, 0, 0);
+ }
+}
+
+/**
+ * Draws image data into a canvas, and sets that as the rendering source for
+ * an element with the specified id as the -moz-element background image.
+ *
+ * @param string id
+ * The id of the -moz-element background image.
+ * @param number width
+ * The image data width.
+ * @param number height
+ * The image data height.
+ * @param array pixels
+ * An array buffer view of the image data.
+ */
+function drawBackground(id, width, height, pixels) {
+ let canvas = document.createElementNS(HTML_NS, "canvas");
+ canvas.width = width;
+ canvas.height = height;
+
+ drawImage(canvas, width, height, pixels);
+ document.mozSetImageElement(id, canvas);
+
+ // Used in tests. Not emitting an event because this shouldn't be "interesting".
+ if (window._onMozSetImageElement) {
+ window._onMozSetImageElement(pixels);
+ }
+}
+
+/**
+ * Iterates forward to find the next draw call in a snapshot.
+ */
+function getNextDrawCall(calls, call) {
+ for (let i = calls.indexOf(call) + 1, len = calls.length; i < len; i++) {
+ let nextCall = calls[i];
+ let name = nextCall.attachment.actor.name;
+ if (CanvasFront.DRAW_CALLS.has(name)) {
+ return nextCall;
+ }
+ }
+ return null;
+}
+
+/**
+ * Iterates backwards to find the most recent screenshot for a function call
+ * in a snapshot loaded from disk.
+ */
+function getScreenshotFromCallLoadedFromDisk(calls, call) {
+ for (let i = calls.indexOf(call); i >= 0; i--) {
+ let prevCall = calls[i];
+ let screenshot = prevCall.screenshot;
+ if (screenshot) {
+ return screenshot;
+ }
+ }
+ return CanvasFront.INVALID_SNAPSHOT_IMAGE;
+}
+
+/**
+ * Iterates backwards to find the most recent thumbnail for a function call.
+ */
+function getThumbnailForCall(thumbnails, index) {
+ for (let i = thumbnails.length - 1; i >= 0; i--) {
+ let thumbnail = thumbnails[i];
+ if (thumbnail.index <= index) {
+ return thumbnail;
+ }
+ }
+ return CanvasFront.INVALID_SNAPSHOT_IMAGE;
+}
diff --git a/devtools/client/canvasdebugger/canvasdebugger.xul b/devtools/client/canvasdebugger/canvasdebugger.xul
new file mode 100644
index 000000000..f3003cbbe
--- /dev/null
+++ b/devtools/client/canvasdebugger/canvasdebugger.xul
@@ -0,0 +1,135 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/content/shared/widgets/widgets.css" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/skin/widgets.css" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/skin/canvasdebugger.css" type="text/css"?>
+<!DOCTYPE window [
+ <!ENTITY % canvasDebuggerDTD SYSTEM "chrome://devtools/locale/canvasdebugger.dtd">
+ %canvasDebuggerDTD;
+]>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script src="chrome://devtools/content/shared/theme-switching.js"/>
+ <script type="application/javascript" src="canvasdebugger.js"/>
+ <script type="application/javascript" src="callslist.js"/>
+ <script type="application/javascript" src="snapshotslist.js"/>
+
+ <hbox class="theme-body" flex="1">
+ <vbox id="snapshots-pane">
+ <toolbar id="snapshots-toolbar"
+ class="devtools-toolbar">
+ <hbox id="snapshots-controls">
+ <toolbarbutton id="clear-snapshots"
+ class="devtools-toolbarbutton devtools-clear-icon"
+ oncommand="SnapshotsListView._onClearButtonClick()"
+ tooltiptext="&canvasDebuggerUI.clearSnapshots;"/>
+ <toolbarbutton id="record-snapshot"
+ class="devtools-toolbarbutton"
+ oncommand="SnapshotsListView._onRecordButtonClick()"
+ tooltiptext="&canvasDebuggerUI.recordSnapshot.tooltip;"
+ hidden="true"/>
+ <toolbarbutton id="import-snapshot"
+ class="devtools-toolbarbutton"
+ oncommand="SnapshotsListView._onImportButtonClick()"
+ tooltiptext="&canvasDebuggerUI.importSnapshot;"/>
+ </hbox>
+ </toolbar>
+ <vbox id="snapshots-list" flex="1"/>
+ </vbox>
+
+ <vbox id="debugging-pane" class="devtools-main-content" flex="1">
+ <hbox id="reload-notice"
+ class="notice-container"
+ align="center"
+ pack="center"
+ flex="1">
+ <button id="reload-notice-button"
+ class="devtools-toolbarbutton"
+ standalone="true"
+ label="&canvasDebuggerUI.reloadNotice1;"
+ oncommand="gFront.setup({ reload: true })"/>
+ <label id="reload-notice-label"
+ class="plain"
+ value="&canvasDebuggerUI.reloadNotice2;"/>
+ </hbox>
+
+ <hbox id="empty-notice"
+ class="notice-container"
+ align="center"
+ pack="center"
+ flex="1"
+ hidden="true">
+ <label value="&canvasDebuggerUI.emptyNotice1;"/>
+ <button id="canvas-debugging-empty-notice-button"
+ class="devtools-toolbarbutton"
+ standalone="true"
+ oncommand="SnapshotsListView._onRecordButtonClick()"/>
+ <label value="&canvasDebuggerUI.emptyNotice2;"/>
+ </hbox>
+
+ <hbox id="waiting-notice"
+ class="notice-container devtools-throbber"
+ align="center"
+ pack="center"
+ flex="1"
+ hidden="true">
+ <label id="requests-menu-waiting-notice-label"
+ class="plain"
+ value="&canvasDebuggerUI.waitingNotice;"/>
+ </hbox>
+
+ <box id="debugging-pane-contents"
+ class="devtools-responsive-container"
+ flex="1"
+ hidden="true">
+ <vbox id="calls-list-container" flex="1">
+ <toolbar id="debugging-toolbar"
+ class="devtools-toolbar">
+ <hbox id="debugging-controls"
+ class="devtools-toolbarbutton-group">
+ <toolbarbutton id="resume"
+ class="devtools-toolbarbutton"
+ oncommand="CallsListView._onResume()"/>
+ <toolbarbutton id="step-over"
+ class="devtools-toolbarbutton"
+ oncommand="CallsListView._onStepOver()"/>
+ <toolbarbutton id="step-in"
+ class="devtools-toolbarbutton"
+ oncommand="CallsListView._onStepIn()"/>
+ <toolbarbutton id="step-out"
+ class="devtools-toolbarbutton"
+ oncommand="CallsListView._onStepOut()"/>
+ </hbox>
+ <toolbarbutton id="debugging-toolbar-sizer-button"
+ class="devtools-toolbarbutton"
+ label=""/>
+ <scale id="calls-slider"
+ movetoclick="true"
+ flex="100"/>
+ <textbox id="calls-searchbox"
+ class="devtools-filterinput"
+ placeholder="&canvasDebuggerUI.searchboxPlaceholder;"
+ type="search"
+ flex="1"/>
+ </toolbar>
+ <vbox id="calls-list" flex="1"/>
+ </vbox>
+
+ <splitter class="devtools-side-splitter"/>
+
+ <vbox id="screenshot-container"
+ hidden="true">
+ <vbox id="screenshot-image" flex="1"/>
+ <label id="screenshot-dimensions" class="plain"/>
+ </vbox>
+ </box>
+
+ <hbox id="snapshot-filmstrip"
+ hidden="true"/>
+ </vbox>
+
+ </hbox>
+</window>
diff --git a/devtools/client/canvasdebugger/moz.build b/devtools/client/canvasdebugger/moz.build
new file mode 100644
index 000000000..684fabc22
--- /dev/null
+++ b/devtools/client/canvasdebugger/moz.build
@@ -0,0 +1,10 @@
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'panel.js'
+)
+
+BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
diff --git a/devtools/client/canvasdebugger/panel.js b/devtools/client/canvasdebugger/panel.js
new file mode 100644
index 000000000..4535886c7
--- /dev/null
+++ b/devtools/client/canvasdebugger/panel.js
@@ -0,0 +1,76 @@
+/* -*- 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 { Cc, Ci, Cu, Cr } = require("chrome");
+const promise = require("promise");
+const EventEmitter = require("devtools/shared/event-emitter");
+const { CanvasFront } = require("devtools/shared/fronts/canvas");
+const DevToolsUtils = require("devtools/shared/DevToolsUtils");
+
+function CanvasDebuggerPanel(iframeWindow, toolbox) {
+ this.panelWin = iframeWindow;
+ this._toolbox = toolbox;
+ this._destroyer = null;
+
+ EventEmitter.decorate(this);
+}
+
+exports.CanvasDebuggerPanel = CanvasDebuggerPanel;
+
+CanvasDebuggerPanel.prototype = {
+ /**
+ * Open is effectively an asynchronous constructor.
+ *
+ * @return object
+ * A promise that is resolved when the Canvas Debugger completes opening.
+ */
+ open: function () {
+ let targetPromise;
+
+ // Local debugging needs to make the target remote.
+ if (!this.target.isRemote) {
+ targetPromise = this.target.makeRemote();
+ } else {
+ targetPromise = promise.resolve(this.target);
+ }
+
+ return targetPromise
+ .then(() => {
+ this.panelWin.gToolbox = this._toolbox;
+ this.panelWin.gTarget = this.target;
+ this.panelWin.gFront = new CanvasFront(this.target.client, this.target.form);
+ return this.panelWin.startupCanvasDebugger();
+ })
+ .then(() => {
+ this.isReady = true;
+ this.emit("ready");
+ return this;
+ })
+ .then(null, function onError(aReason) {
+ DevToolsUtils.reportException("CanvasDebuggerPanel.prototype.open", aReason);
+ });
+ },
+
+ // DevToolPanel API
+
+ get target() {
+ return this._toolbox.target;
+ },
+
+ destroy: function () {
+ // Make sure this panel is not already destroyed.
+ if (this._destroyer) {
+ return this._destroyer;
+ }
+
+ return this._destroyer = this.panelWin.shutdownCanvasDebugger().then(() => {
+ // Destroy front to ensure packet handler is removed from client
+ this.panelWin.gFront.destroy();
+ this.emit("destroyed");
+ });
+ }
+};
diff --git a/devtools/client/canvasdebugger/snapshotslist.js b/devtools/client/canvasdebugger/snapshotslist.js
new file mode 100644
index 000000000..da3b4a7eb
--- /dev/null
+++ b/devtools/client/canvasdebugger/snapshotslist.js
@@ -0,0 +1,495 @@
+/* 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/. */
+/* import-globals-from canvasdebugger.js */
+/* globals window, document */
+"use strict";
+
+/**
+ * Functions handling the recorded animation frame snapshots UI.
+ */
+var SnapshotsListView = Heritage.extend(WidgetMethods, {
+ /**
+ * Initialization function, called when the tool is started.
+ */
+ initialize: function () {
+ this.widget = new SideMenuWidget($("#snapshots-list"), {
+ showArrows: true
+ });
+
+ this._onSelect = this._onSelect.bind(this);
+ this._onClearButtonClick = this._onClearButtonClick.bind(this);
+ this._onRecordButtonClick = this._onRecordButtonClick.bind(this);
+ this._onImportButtonClick = this._onImportButtonClick.bind(this);
+ this._onSaveButtonClick = this._onSaveButtonClick.bind(this);
+ this._onRecordSuccess = this._onRecordSuccess.bind(this);
+ this._onRecordFailure = this._onRecordFailure.bind(this);
+ this._stopRecordingAnimation = this._stopRecordingAnimation.bind(this);
+
+ window.on(EVENTS.SNAPSHOT_RECORDING_FINISHED, this._enableRecordButton);
+ this.emptyText = L10N.getStr("noSnapshotsText");
+ this.widget.addEventListener("select", this._onSelect, false);
+ },
+
+ /**
+ * Destruction function, called when the tool is closed.
+ */
+ destroy: function () {
+ clearNamedTimeout("canvas-actor-recording");
+ window.off(EVENTS.SNAPSHOT_RECORDING_FINISHED, this._enableRecordButton);
+ this.widget.removeEventListener("select", this._onSelect, false);
+ },
+
+ /**
+ * Adds a snapshot entry to this container.
+ *
+ * @return object
+ * The newly inserted item.
+ */
+ addSnapshot: function () {
+ let contents = document.createElement("hbox");
+ contents.className = "snapshot-item";
+
+ let thumbnail = document.createElementNS(HTML_NS, "canvas");
+ thumbnail.className = "snapshot-item-thumbnail";
+ thumbnail.width = CanvasFront.THUMBNAIL_SIZE;
+ thumbnail.height = CanvasFront.THUMBNAIL_SIZE;
+
+ let title = document.createElement("label");
+ title.className = "plain snapshot-item-title";
+ title.setAttribute("value",
+ L10N.getFormatStr("snapshotsList.itemLabel", this.itemCount + 1));
+
+ let calls = document.createElement("label");
+ calls.className = "plain snapshot-item-calls";
+ calls.setAttribute("value",
+ L10N.getStr("snapshotsList.loadingLabel"));
+
+ let save = document.createElement("label");
+ save.className = "plain snapshot-item-save";
+ save.addEventListener("click", this._onSaveButtonClick, false);
+
+ let spacer = document.createElement("spacer");
+ spacer.setAttribute("flex", "1");
+
+ let footer = document.createElement("hbox");
+ footer.className = "snapshot-item-footer";
+ footer.appendChild(save);
+
+ let details = document.createElement("vbox");
+ details.className = "snapshot-item-details";
+ details.appendChild(title);
+ details.appendChild(calls);
+ details.appendChild(spacer);
+ details.appendChild(footer);
+
+ contents.appendChild(thumbnail);
+ contents.appendChild(details);
+
+ // Append a recorded snapshot item to this container.
+ return this.push([contents], {
+ attachment: {
+ // The snapshot and function call actors, along with the thumbnails
+ // will be available as soon as recording finishes.
+ actor: null,
+ calls: null,
+ thumbnails: null,
+ screenshot: null
+ }
+ });
+ },
+
+ /**
+ * Removes the last snapshot added, in the event no requestAnimationFrame loop was found.
+ */
+ removeLastSnapshot: function () {
+ this.removeAt(this.itemCount - 1);
+ // If this is the only item, revert back to the empty notice
+ if (this.itemCount === 0) {
+ $("#empty-notice").hidden = false;
+ $("#waiting-notice").hidden = true;
+ }
+ },
+
+ /**
+ * Customizes a shapshot in this container.
+ *
+ * @param Item snapshotItem
+ * An item inserted via `SnapshotsListView.addSnapshot`.
+ * @param object snapshotActor
+ * The frame snapshot actor received from the backend.
+ * @param object snapshotOverview
+ * Additional data about the snapshot received from the backend.
+ */
+ customizeSnapshot: function (snapshotItem, snapshotActor, snapshotOverview) {
+ // Make sure the function call actors are stored on the item,
+ // to be used when populating the CallsListView.
+ snapshotItem.attachment.actor = snapshotActor;
+ let functionCalls = snapshotItem.attachment.calls = snapshotOverview.calls;
+ let thumbnails = snapshotItem.attachment.thumbnails = snapshotOverview.thumbnails;
+ let screenshot = snapshotItem.attachment.screenshot = snapshotOverview.screenshot;
+
+ let lastThumbnail = thumbnails[thumbnails.length - 1];
+ let { width, height, flipped, pixels } = lastThumbnail;
+
+ let thumbnailNode = $(".snapshot-item-thumbnail", snapshotItem.target);
+ thumbnailNode.setAttribute("flipped", flipped);
+ drawImage(thumbnailNode, width, height, pixels, { centered: true });
+
+ let callsNode = $(".snapshot-item-calls", snapshotItem.target);
+ let drawCalls = functionCalls.filter(e => CanvasFront.DRAW_CALLS.has(e.name));
+
+ let drawCallsStr = PluralForm.get(drawCalls.length,
+ L10N.getStr("snapshotsList.drawCallsLabel"));
+ let funcCallsStr = PluralForm.get(functionCalls.length,
+ L10N.getStr("snapshotsList.functionCallsLabel"));
+
+ callsNode.setAttribute("value",
+ drawCallsStr.replace("#1", drawCalls.length) + ", " +
+ funcCallsStr.replace("#1", functionCalls.length));
+
+ let saveNode = $(".snapshot-item-save", snapshotItem.target);
+ saveNode.setAttribute("disabled", !!snapshotItem.isLoadedFromDisk);
+ saveNode.setAttribute("value", snapshotItem.isLoadedFromDisk
+ ? L10N.getStr("snapshotsList.loadedLabel")
+ : L10N.getStr("snapshotsList.saveLabel"));
+
+ // Make sure there's always a selected item available.
+ if (!this.selectedItem) {
+ this.selectedIndex = 0;
+ }
+ },
+
+ /**
+ * The select listener for this container.
+ */
+ _onSelect: function ({ detail: snapshotItem }) {
+ // Check to ensure the attachment has an actor, like
+ // an in-progress recording.
+ if (!snapshotItem || !snapshotItem.attachment.actor) {
+ return;
+ }
+ let { calls, thumbnails, screenshot } = snapshotItem.attachment;
+
+ $("#reload-notice").hidden = true;
+ $("#empty-notice").hidden = true;
+ $("#waiting-notice").hidden = false;
+
+ $("#debugging-pane-contents").hidden = true;
+ $("#screenshot-container").hidden = true;
+ $("#snapshot-filmstrip").hidden = true;
+
+ Task.spawn(function* () {
+ // Wait for a few milliseconds between presenting the function calls,
+ // screenshot and thumbnails, to allow each component being
+ // sequentially drawn. This gives the illusion of snappiness.
+
+ yield DevToolsUtils.waitForTime(SNAPSHOT_DATA_DISPLAY_DELAY);
+ CallsListView.showCalls(calls);
+ $("#debugging-pane-contents").hidden = false;
+ $("#waiting-notice").hidden = true;
+
+ yield DevToolsUtils.waitForTime(SNAPSHOT_DATA_DISPLAY_DELAY);
+ CallsListView.showThumbnails(thumbnails);
+ $("#snapshot-filmstrip").hidden = false;
+
+ yield DevToolsUtils.waitForTime(SNAPSHOT_DATA_DISPLAY_DELAY);
+ CallsListView.showScreenshot(screenshot);
+ $("#screenshot-container").hidden = false;
+
+ window.emit(EVENTS.SNAPSHOT_RECORDING_SELECTED);
+ });
+ },
+
+ /**
+ * The click listener for the "clear" button in this container.
+ */
+ _onClearButtonClick: function () {
+ Task.spawn(function* () {
+ SnapshotsListView.empty();
+ CallsListView.empty();
+
+ $("#reload-notice").hidden = true;
+ $("#empty-notice").hidden = true;
+ $("#waiting-notice").hidden = true;
+
+ if (yield gFront.isInitialized()) {
+ $("#empty-notice").hidden = false;
+ } else {
+ $("#reload-notice").hidden = false;
+ }
+
+ $("#debugging-pane-contents").hidden = true;
+ $("#screenshot-container").hidden = true;
+ $("#snapshot-filmstrip").hidden = true;
+
+ window.emit(EVENTS.SNAPSHOTS_LIST_CLEARED);
+ });
+ },
+
+ /**
+ * The click listener for the "record" button in this container.
+ */
+ _onRecordButtonClick: function () {
+ this._disableRecordButton();
+
+ if (this._recording) {
+ this._stopRecordingAnimation();
+ return;
+ }
+
+ // Insert a "dummy" snapshot item in the view, to hint that recording
+ // has now started. However, wait for a few milliseconds before actually
+ // starting the recording, since that might block rendering and prevent
+ // the dummy snapshot item from being drawn.
+ this.addSnapshot();
+
+ // If this is the first item, immediately show the "Loading…" notice.
+ if (this.itemCount == 1) {
+ $("#empty-notice").hidden = true;
+ $("#waiting-notice").hidden = false;
+ }
+
+ this._recordAnimation();
+ },
+
+ /**
+ * Makes the record button able to be clicked again.
+ */
+ _enableRecordButton: function () {
+ $("#record-snapshot").removeAttribute("disabled");
+ },
+
+ /**
+ * Makes the record button unable to be clicked.
+ */
+ _disableRecordButton: function () {
+ $("#record-snapshot").setAttribute("disabled", true);
+ },
+
+ /**
+ * Begins recording an animation.
+ */
+ _recordAnimation: Task.async(function* () {
+ if (this._recording) {
+ return;
+ }
+ this._recording = true;
+ $("#record-snapshot").setAttribute("checked", "true");
+
+ setNamedTimeout("canvas-actor-recording", CANVAS_ACTOR_RECORDING_ATTEMPT, this._stopRecordingAnimation);
+
+ yield DevToolsUtils.waitForTime(SNAPSHOT_START_RECORDING_DELAY);
+ window.emit(EVENTS.SNAPSHOT_RECORDING_STARTED);
+
+ gFront.recordAnimationFrame().then(snapshot => {
+ if (snapshot) {
+ this._onRecordSuccess(snapshot);
+ } else {
+ this._onRecordFailure();
+ }
+ });
+
+ // Wait another delay before reenabling the button to stop the recording
+ // if a recording is not found.
+ yield DevToolsUtils.waitForTime(SNAPSHOT_START_RECORDING_DELAY);
+ this._enableRecordButton();
+ }),
+
+ /**
+ * Stops recording animation. Called when a click on the stopwatch occurs during a recording,
+ * or if a recording times out.
+ */
+ _stopRecordingAnimation: Task.async(function* () {
+ clearNamedTimeout("canvas-actor-recording");
+ let actorCanStop = yield gTarget.actorHasMethod("canvas", "stopRecordingAnimationFrame");
+
+ if (actorCanStop) {
+ yield gFront.stopRecordingAnimationFrame();
+ }
+ // If actor does not have the method to stop recording (Fx39+),
+ // manually call the record failure method. This will call a connection failure
+ // on disconnect as a result of `gFront.recordAnimationFrame()` never resolving,
+ // but this is better than it hanging when there is no requestAnimationFrame anyway.
+ else {
+ this._onRecordFailure();
+ }
+
+ this._recording = false;
+ $("#record-snapshot").removeAttribute("checked");
+ this._enableRecordButton();
+ }),
+
+ /**
+ * Resolves from the front's recordAnimationFrame to setup the interface with the screenshots.
+ */
+ _onRecordSuccess: Task.async(function* (snapshotActor) {
+ // Clear bail-out case if frame found in CANVAS_ACTOR_RECORDING_ATTEMPT milliseconds
+ clearNamedTimeout("canvas-actor-recording");
+ let snapshotItem = this.getItemAtIndex(this.itemCount - 1);
+ let snapshotOverview = yield snapshotActor.getOverview();
+ this.customizeSnapshot(snapshotItem, snapshotActor, snapshotOverview);
+
+ this._recording = false;
+ $("#record-snapshot").removeAttribute("checked");
+
+ window.emit(EVENTS.SNAPSHOT_RECORDING_COMPLETED);
+ window.emit(EVENTS.SNAPSHOT_RECORDING_FINISHED);
+ }),
+
+ /**
+ * Called as a reject from the front's recordAnimationFrame.
+ */
+ _onRecordFailure: function () {
+ clearNamedTimeout("canvas-actor-recording");
+ showNotification(gToolbox, "canvas-debugger-timeout", L10N.getStr("recordingTimeoutFailure"));
+ window.emit(EVENTS.SNAPSHOT_RECORDING_CANCELLED);
+ window.emit(EVENTS.SNAPSHOT_RECORDING_FINISHED);
+ this.removeLastSnapshot();
+ },
+
+ /**
+ * The click listener for the "import" button in this container.
+ */
+ _onImportButtonClick: function () {
+ let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ fp.init(window, L10N.getStr("snapshotsList.saveDialogTitle"), Ci.nsIFilePicker.modeOpen);
+ fp.appendFilter(L10N.getStr("snapshotsList.saveDialogJSONFilter"), "*.json");
+ fp.appendFilter(L10N.getStr("snapshotsList.saveDialogAllFilter"), "*.*");
+
+ if (fp.show() != Ci.nsIFilePicker.returnOK) {
+ return;
+ }
+
+ let channel = NetUtil.newChannel({
+ uri: NetUtil.newURI(fp.file), loadUsingSystemPrincipal: true});
+ channel.contentType = "text/plain";
+
+ NetUtil.asyncFetch(channel, (inputStream, status) => {
+ if (!Components.isSuccessCode(status)) {
+ console.error("Could not import recorded animation frame snapshot file.");
+ return;
+ }
+ try {
+ let string = NetUtil.readInputStreamToString(inputStream, inputStream.available());
+ var data = JSON.parse(string);
+ } catch (e) {
+ console.error("Could not read animation frame snapshot file.");
+ return;
+ }
+ if (data.fileType != CALLS_LIST_SERIALIZER_IDENTIFIER) {
+ console.error("Unrecognized animation frame snapshot file.");
+ return;
+ }
+
+ // Add a `isLoadedFromDisk` flag on everything to avoid sending invalid
+ // requests to the backend, since we're not dealing with actors anymore.
+ let snapshotItem = this.addSnapshot();
+ snapshotItem.isLoadedFromDisk = true;
+ data.calls.forEach(e => e.isLoadedFromDisk = true);
+
+ this.customizeSnapshot(snapshotItem, data.calls, data);
+ });
+ },
+
+ /**
+ * The click listener for the "save" button of each item in this container.
+ */
+ _onSaveButtonClick: function (e) {
+ let snapshotItem = this.getItemForElement(e.target);
+
+ let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ fp.init(window, L10N.getStr("snapshotsList.saveDialogTitle"), Ci.nsIFilePicker.modeSave);
+ fp.appendFilter(L10N.getStr("snapshotsList.saveDialogJSONFilter"), "*.json");
+ fp.appendFilter(L10N.getStr("snapshotsList.saveDialogAllFilter"), "*.*");
+ fp.defaultString = "snapshot.json";
+
+ // Start serializing all the function call actors for the specified snapshot,
+ // while the nsIFilePicker dialog is being opened. Snappy.
+ let serialized = Task.spawn(function* () {
+ let data = {
+ fileType: CALLS_LIST_SERIALIZER_IDENTIFIER,
+ version: CALLS_LIST_SERIALIZER_VERSION,
+ calls: [],
+ thumbnails: [],
+ screenshot: null
+ };
+ let functionCalls = snapshotItem.attachment.calls;
+ let thumbnails = snapshotItem.attachment.thumbnails;
+ let screenshot = snapshotItem.attachment.screenshot;
+
+ // Prepare all the function calls for serialization.
+ yield DevToolsUtils.yieldingEach(functionCalls, (call, i) => {
+ let { type, name, file, line, timestamp, argsPreview, callerPreview } = call;
+ return call.getDetails().then(({ stack }) => {
+ data.calls[i] = {
+ type: type,
+ name: name,
+ file: file,
+ line: line,
+ stack: stack,
+ timestamp: timestamp,
+ argsPreview: argsPreview,
+ callerPreview: callerPreview
+ };
+ });
+ });
+
+ // Prepare all the thumbnails for serialization.
+ yield DevToolsUtils.yieldingEach(thumbnails, (thumbnail, i) => {
+ let { index, width, height, flipped, pixels } = thumbnail;
+ data.thumbnails.push({ index, width, height, flipped, pixels });
+ });
+
+ // Prepare the screenshot for serialization.
+ let { index, width, height, flipped, pixels } = screenshot;
+ data.screenshot = { index, width, height, flipped, pixels };
+
+ let string = JSON.stringify(data);
+ let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
+ createInstance(Ci.nsIScriptableUnicodeConverter);
+
+ converter.charset = "UTF-8";
+ return converter.convertToInputStream(string);
+ });
+
+ // Open the nsIFilePicker and wait for the function call actors to finish
+ // being serialized, in order to save the generated JSON data to disk.
+ fp.open({ done: result => {
+ if (result == Ci.nsIFilePicker.returnCancel) {
+ return;
+ }
+ let footer = $(".snapshot-item-footer", snapshotItem.target);
+ let save = $(".snapshot-item-save", snapshotItem.target);
+
+ // Show a throbber and a "Saving…" label if serializing isn't immediate.
+ setNamedTimeout("call-list-save", CALLS_LIST_SLOW_SAVE_DELAY, () => {
+ footer.classList.add("devtools-throbber");
+ save.setAttribute("disabled", "true");
+ save.setAttribute("value", L10N.getStr("snapshotsList.savingLabel"));
+ });
+
+ serialized.then(inputStream => {
+ let outputStream = FileUtils.openSafeFileOutputStream(fp.file);
+
+ NetUtil.asyncCopy(inputStream, outputStream, status => {
+ if (!Components.isSuccessCode(status)) {
+ console.error("Could not save recorded animation frame snapshot file.");
+ }
+ clearNamedTimeout("call-list-save");
+ footer.classList.remove("devtools-throbber");
+ save.removeAttribute("disabled");
+ save.setAttribute("value", L10N.getStr("snapshotsList.saveLabel"));
+ });
+ });
+ }});
+ }
+});
+
+function showNotification(toolbox, name, message) {
+ let notificationBox = toolbox.getNotificationBox();
+ let notification = notificationBox.getNotificationWithValue(name);
+ if (!notification) {
+ notificationBox.appendNotification(message, name, "", notificationBox.PRIORITY_WARNING_HIGH);
+ }
+}
diff --git a/devtools/client/canvasdebugger/test/.eslintrc.js b/devtools/client/canvasdebugger/test/.eslintrc.js
new file mode 100644
index 000000000..8d15a76d9
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../../.eslintrc.mochitests.js"
+};
diff --git a/devtools/client/canvasdebugger/test/browser.ini b/devtools/client/canvasdebugger/test/browser.ini
new file mode 100644
index 000000000..65c81c32f
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser.ini
@@ -0,0 +1,61 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ doc_raf-begin.html
+ doc_settimeout.html
+ doc_no-canvas.html
+ doc_raf-no-canvas.html
+ doc_simple-canvas.html
+ doc_simple-canvas-bitmasks.html
+ doc_simple-canvas-deep-stack.html
+ doc_simple-canvas-transparent.html
+ doc_webgl-bindings.html
+ doc_webgl-enum.html
+ doc_webgl-drawArrays.html
+ doc_webgl-drawElements.html
+ head.js
+
+[browser_canvas-actor-test-01.js]
+[browser_canvas-actor-test-02.js]
+[browser_canvas-actor-test-03.js]
+[browser_canvas-actor-test-04.js]
+[browser_canvas-actor-test-05.js]
+[browser_canvas-actor-test-06.js]
+[browser_canvas-actor-test-07.js]
+[browser_canvas-actor-test-08.js]
+[browser_canvas-actor-test-09.js]
+subsuite = gpu
+[browser_canvas-actor-test-10.js]
+subsuite = gpu
+[browser_canvas-actor-test-11.js]
+subsuite = gpu
+[browser_canvas-actor-test-12.js]
+[browser_canvas-frontend-call-highlight.js]
+[browser_canvas-frontend-call-list.js]
+[browser_canvas-frontend-call-search.js]
+[browser_canvas-frontend-call-stack-01.js]
+[browser_canvas-frontend-call-stack-02.js]
+[browser_canvas-frontend-call-stack-03.js]
+[browser_canvas-frontend-clear.js]
+[browser_canvas-frontend-img-screenshots.js]
+[browser_canvas-frontend-img-thumbnails-01.js]
+[browser_canvas-frontend-img-thumbnails-02.js]
+[browser_canvas-frontend-open.js]
+[browser_canvas-frontend-record-01.js]
+[browser_canvas-frontend-record-02.js]
+[browser_canvas-frontend-record-03.js]
+[browser_canvas-frontend-record-04.js]
+[browser_canvas-frontend-reload-01.js]
+[browser_canvas-frontend-reload-02.js]
+[browser_canvas-frontend-slider-01.js]
+[browser_canvas-frontend-slider-02.js]
+[browser_canvas-frontend-snapshot-select-01.js]
+[browser_canvas-frontend-snapshot-select-02.js]
+[browser_canvas-frontend-stepping.js]
+[browser_canvas-frontend-stop-01.js]
+[browser_canvas-frontend-stop-02.js]
+[browser_canvas-frontend-stop-03.js]
+[browser_profiling-canvas.js]
+[browser_profiling-webgl.js]
+subsuite = gpu
diff --git a/devtools/client/canvasdebugger/test/browser_canvas-actor-test-01.js b/devtools/client/canvasdebugger/test/browser_canvas-actor-test-01.js
new file mode 100644
index 000000000..9b6ee4e4f
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-actor-test-01.js
@@ -0,0 +1,17 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the canvas debugger leaks on initialization and sudden destruction.
+ * You can also use this initialization format as a template for other tests.
+ */
+
+function* ifTestingSupported() {
+ let { target, front } = yield initCallWatcherBackend(SIMPLE_CANVAS_URL);
+
+ ok(target, "Should have a target available.");
+ ok(front, "Should have a protocol front available.");
+
+ yield removeTab(target.tab);
+ finish();
+}
diff --git a/devtools/client/canvasdebugger/test/browser_canvas-actor-test-02.js b/devtools/client/canvasdebugger/test/browser_canvas-actor-test-02.js
new file mode 100644
index 000000000..eb8a8f5f7
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-actor-test-02.js
@@ -0,0 +1,78 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if functions calls are recorded and stored for a canvas context,
+ * and that their stack is successfully retrieved.
+ */
+
+function* ifTestingSupported() {
+ let { target, front } = yield initCallWatcherBackend(SIMPLE_CANVAS_URL);
+
+ let navigated = once(target, "navigate");
+
+ yield front.setup({
+ tracedGlobals: ["CanvasRenderingContext2D", "WebGLRenderingContext"],
+ startRecording: true,
+ performReload: true,
+ storeCalls: true
+ });
+ ok(true, "The front was setup up successfully.");
+
+ yield navigated;
+ ok(true, "Target automatically navigated when the front was set up.");
+
+ // Allow the content to execute some functions.
+ yield waitForTick();
+
+ let functionCalls = yield front.pauseRecording();
+ ok(functionCalls,
+ "An array of function call actors was sent after reloading.");
+ ok(functionCalls.length > 0,
+ "There's at least one function call actor available.");
+
+ is(functionCalls[0].type, CallWatcherFront.METHOD_FUNCTION,
+ "The called function is correctly identified as a method.");
+ is(functionCalls[0].name, "clearRect",
+ "The called function's name is correct.");
+ is(functionCalls[0].file, SIMPLE_CANVAS_URL,
+ "The called function's file is correct.");
+ is(functionCalls[0].line, 25,
+ "The called function's line is correct.");
+
+ is(functionCalls[0].callerPreview, "Object",
+ "The called function's caller preview is correct.");
+ is(functionCalls[0].argsPreview, "0, 0, 128, 128",
+ "The called function's args preview is correct.");
+
+ let details = yield functionCalls[1].getDetails();
+ ok(details,
+ "The first called function has some details available.");
+
+ is(details.stack.length, 3,
+ "The called function's stack depth is correct.");
+
+ is(details.stack[0].name, "fillStyle",
+ "The called function's stack is correct (1.1).");
+ is(details.stack[0].file, SIMPLE_CANVAS_URL,
+ "The called function's stack is correct (1.2).");
+ is(details.stack[0].line, 20,
+ "The called function's stack is correct (1.3).");
+
+ is(details.stack[1].name, "drawRect",
+ "The called function's stack is correct (2.1).");
+ is(details.stack[1].file, SIMPLE_CANVAS_URL,
+ "The called function's stack is correct (2.2).");
+ is(details.stack[1].line, 26,
+ "The called function's stack is correct (2.3).");
+
+ is(details.stack[2].name, "drawScene",
+ "The called function's stack is correct (3.1).");
+ is(details.stack[2].file, SIMPLE_CANVAS_URL,
+ "The called function's stack is correct (3.2).");
+ is(details.stack[2].line, 33,
+ "The called function's stack is correct (3.3).");
+
+ yield removeTab(target.tab);
+ finish();
+}
diff --git a/devtools/client/canvasdebugger/test/browser_canvas-actor-test-03.js b/devtools/client/canvasdebugger/test/browser_canvas-actor-test-03.js
new file mode 100644
index 000000000..8a8a63780
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-actor-test-03.js
@@ -0,0 +1,75 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if functions inside a single animation frame are recorded and stored
+ * for a canvas context.
+ */
+
+function* ifTestingSupported() {
+ let { target, front } = yield initCanvasDebuggerBackend(SIMPLE_CANVAS_URL);
+
+ let navigated = once(target, "navigate");
+
+ yield front.setup({ reload: true });
+ ok(true, "The front was setup up successfully.");
+
+ yield navigated;
+ ok(true, "Target automatically navigated when the front was set up.");
+
+ let snapshotActor = yield front.recordAnimationFrame();
+ ok(snapshotActor,
+ "A snapshot actor was sent after recording.");
+
+ let animationOverview = yield snapshotActor.getOverview();
+ ok(snapshotActor,
+ "An animation overview could be retrieved after recording.");
+
+ let functionCalls = animationOverview.calls;
+ ok(functionCalls,
+ "An array of function call actors was sent after recording.");
+ is(functionCalls.length, 8,
+ "The number of function call actors is correct.");
+
+ is(functionCalls[0].type, CallWatcherFront.METHOD_FUNCTION,
+ "The first called function is correctly identified as a method.");
+ is(functionCalls[0].name, "clearRect",
+ "The first called function's name is correct.");
+ is(functionCalls[0].file, SIMPLE_CANVAS_URL,
+ "The first called function's file is correct.");
+ is(functionCalls[0].line, 25,
+ "The first called function's line is correct.");
+ is(functionCalls[0].argsPreview, "0, 0, 128, 128",
+ "The first called function's args preview is correct.");
+ is(functionCalls[0].callerPreview, "Object",
+ "The first called function's caller preview is correct.");
+
+ is(functionCalls[6].type, CallWatcherFront.METHOD_FUNCTION,
+ "The penultimate called function is correctly identified as a method.");
+ is(functionCalls[6].name, "fillRect",
+ "The penultimate called function's name is correct.");
+ is(functionCalls[6].file, SIMPLE_CANVAS_URL,
+ "The penultimate called function's file is correct.");
+ is(functionCalls[6].line, 21,
+ "The penultimate called function's line is correct.");
+ is(functionCalls[6].argsPreview, "10, 10, 55, 50",
+ "The penultimate called function's args preview is correct.");
+ is(functionCalls[6].callerPreview, "Object",
+ "The penultimate called function's caller preview is correct.");
+
+ is(functionCalls[7].type, CallWatcherFront.METHOD_FUNCTION,
+ "The last called function is correctly identified as a method.");
+ is(functionCalls[7].name, "requestAnimationFrame",
+ "The last called function's name is correct.");
+ is(functionCalls[7].file, SIMPLE_CANVAS_URL,
+ "The last called function's file is correct.");
+ is(functionCalls[7].line, 30,
+ "The last called function's line is correct.");
+ ok(functionCalls[7].argsPreview.includes("Function"),
+ "The last called function's args preview is correct.");
+ is(functionCalls[7].callerPreview, "Object",
+ "The last called function's caller preview is correct.");
+
+ yield removeTab(target.tab);
+ finish();
+}
diff --git a/devtools/client/canvasdebugger/test/browser_canvas-actor-test-04.js b/devtools/client/canvasdebugger/test/browser_canvas-actor-test-04.js
new file mode 100644
index 000000000..d3c7d7661
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-actor-test-04.js
@@ -0,0 +1,85 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if draw calls inside a single animation frame generate and retrieve
+ * the correct thumbnails.
+ */
+
+function* ifTestingSupported() {
+ let { target, front } = yield initCanvasDebuggerBackend(SIMPLE_CANVAS_URL);
+
+ let navigated = once(target, "navigate");
+
+ yield front.setup({ reload: true });
+ ok(true, "The front was setup up successfully.");
+
+ yield navigated;
+ ok(true, "Target automatically navigated when the front was set up.");
+
+ let snapshotActor = yield front.recordAnimationFrame();
+ ok(snapshotActor,
+ "A snapshot actor was sent after recording.");
+
+ let animationOverview = yield snapshotActor.getOverview();
+ ok(animationOverview,
+ "An animation overview could be retrieved after recording.");
+
+ let thumbnails = animationOverview.thumbnails;
+ ok(thumbnails,
+ "An array of thumbnails was sent after recording.");
+ is(thumbnails.length, 4,
+ "The number of thumbnails is correct.");
+
+ is(thumbnails[0].index, 0,
+ "The first thumbnail's index is correct.");
+ is(thumbnails[0].width, 50,
+ "The first thumbnail's width is correct.");
+ is(thumbnails[0].height, 50,
+ "The first thumbnail's height is correct.");
+ is(thumbnails[0].flipped, false,
+ "The first thumbnail's flipped flag is correct.");
+ is([].find.call(Uint32(thumbnails[0].pixels), e => e > 0), undefined,
+ "The first thumbnail's pixels seem to be completely transparent.");
+
+ is(thumbnails[1].index, 2,
+ "The second thumbnail's index is correct.");
+ is(thumbnails[1].width, 50,
+ "The second thumbnail's width is correct.");
+ is(thumbnails[1].height, 50,
+ "The second thumbnail's height is correct.");
+ is(thumbnails[1].flipped, false,
+ "The second thumbnail's flipped flag is correct.");
+ is([].find.call(Uint32(thumbnails[1].pixels), e => e > 0), 4290822336,
+ "The second thumbnail's pixels seem to not be completely transparent.");
+
+ is(thumbnails[2].index, 4,
+ "The third thumbnail's index is correct.");
+ is(thumbnails[2].width, 50,
+ "The third thumbnail's width is correct.");
+ is(thumbnails[2].height, 50,
+ "The third thumbnail's height is correct.");
+ is(thumbnails[2].flipped, false,
+ "The third thumbnail's flipped flag is correct.");
+ is([].find.call(Uint32(thumbnails[2].pixels), e => e > 0), 4290822336,
+ "The third thumbnail's pixels seem to not be completely transparent.");
+
+ is(thumbnails[3].index, 6,
+ "The fourth thumbnail's index is correct.");
+ is(thumbnails[3].width, 50,
+ "The fourth thumbnail's width is correct.");
+ is(thumbnails[3].height, 50,
+ "The fourth thumbnail's height is correct.");
+ is(thumbnails[3].flipped, false,
+ "The fourth thumbnail's flipped flag is correct.");
+ is([].find.call(Uint32(thumbnails[3].pixels), e => e > 0), 4290822336,
+ "The fourth thumbnail's pixels seem to not be completely transparent.");
+
+ yield removeTab(target.tab);
+ finish();
+}
+
+function Uint32(src) {
+ let charView = new Uint8Array(src);
+ return new Uint32Array(charView.buffer);
+}
diff --git a/devtools/client/canvasdebugger/test/browser_canvas-actor-test-05.js b/devtools/client/canvasdebugger/test/browser_canvas-actor-test-05.js
new file mode 100644
index 000000000..e13dab9a4
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-actor-test-05.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if draw calls inside a single animation frame generate and retrieve
+ * the correct "end result" screenshot.
+ */
+
+function* ifTestingSupported() {
+ let { target, front } = yield initCanvasDebuggerBackend(SIMPLE_CANVAS_URL);
+
+ let navigated = once(target, "navigate");
+
+ yield front.setup({ reload: true });
+ ok(true, "The front was setup up successfully.");
+
+ yield navigated;
+ ok(true, "Target automatically navigated when the front was set up.");
+
+ let snapshotActor = yield front.recordAnimationFrame();
+ ok(snapshotActor,
+ "A snapshot actor was sent after recording.");
+
+ let animationOverview = yield snapshotActor.getOverview();
+ ok(snapshotActor,
+ "An animation overview could be retrieved after recording.");
+
+ let screenshot = animationOverview.screenshot;
+ ok(screenshot,
+ "A screenshot was sent after recording.");
+
+ is(screenshot.index, 6,
+ "The screenshot's index is correct.");
+ is(screenshot.width, 128,
+ "The screenshot's width is correct.");
+ is(screenshot.height, 128,
+ "The screenshot's height is correct.");
+ is(screenshot.flipped, false,
+ "The screenshot's flipped flag is correct.");
+ is([].find.call(Uint32(screenshot.pixels), e => e > 0), 4290822336,
+ "The screenshot's pixels seem to not be completely transparent.");
+
+ yield removeTab(target.tab);
+ finish();
+}
+
+function Uint32(src) {
+ let charView = new Uint8Array(src);
+ return new Uint32Array(charView.buffer);
+}
diff --git a/devtools/client/canvasdebugger/test/browser_canvas-actor-test-06.js b/devtools/client/canvasdebugger/test/browser_canvas-actor-test-06.js
new file mode 100644
index 000000000..511db6667
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-actor-test-06.js
@@ -0,0 +1,100 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if screenshots for arbitrary draw calls are generated properly.
+ */
+
+function* ifTestingSupported() {
+ let { target, front } = yield initCanvasDebuggerBackend(SIMPLE_CANVAS_TRANSPARENT_URL);
+
+ let navigated = once(target, "navigate");
+
+ yield front.setup({ reload: true });
+ ok(true, "The front was setup up successfully.");
+
+ yield navigated;
+ ok(true, "Target automatically navigated when the front was set up.");
+
+ let snapshotActor = yield front.recordAnimationFrame();
+ let animationOverview = yield snapshotActor.getOverview();
+
+ let functionCalls = animationOverview.calls;
+ ok(functionCalls,
+ "An array of function call actors was sent after recording.");
+ is(functionCalls.length, 8,
+ "The number of function call actors is correct.");
+
+ is(functionCalls[0].name, "clearRect",
+ "The first called function's name is correct.");
+ is(functionCalls[2].name, "fillRect",
+ "The second called function's name is correct.");
+ is(functionCalls[4].name, "fillRect",
+ "The third called function's name is correct.");
+ is(functionCalls[6].name, "fillRect",
+ "The fourth called function's name is correct.");
+
+ let firstDrawCallScreenshot = yield snapshotActor.generateScreenshotFor(functionCalls[0]);
+ let secondDrawCallScreenshot = yield snapshotActor.generateScreenshotFor(functionCalls[2]);
+ let thirdDrawCallScreenshot = yield snapshotActor.generateScreenshotFor(functionCalls[4]);
+ let fourthDrawCallScreenshot = yield snapshotActor.generateScreenshotFor(functionCalls[6]);
+
+ ok(firstDrawCallScreenshot,
+ "The first draw call has a screenshot attached.");
+ is(firstDrawCallScreenshot.index, 0,
+ "The first draw call has the correct screenshot index.");
+ is(firstDrawCallScreenshot.width, 128,
+ "The first draw call has the correct screenshot width.");
+ is(firstDrawCallScreenshot.height, 128,
+ "The first draw call has the correct screenshot height.");
+ is([].find.call(Uint32(firstDrawCallScreenshot.pixels), e => e > 0), undefined,
+ "The first draw call's screenshot's pixels seems to be completely transparent.");
+
+ ok(secondDrawCallScreenshot,
+ "The second draw call has a screenshot attached.");
+ is(secondDrawCallScreenshot.index, 2,
+ "The second draw call has the correct screenshot index.");
+ is(secondDrawCallScreenshot.width, 128,
+ "The second draw call has the correct screenshot width.");
+ is(secondDrawCallScreenshot.height, 128,
+ "The second draw call has the correct screenshot height.");
+ is([].find.call(Uint32(firstDrawCallScreenshot.pixels), e => e > 0), undefined,
+ "The second draw call's screenshot's pixels seems to be completely transparent.");
+
+ ok(thirdDrawCallScreenshot,
+ "The third draw call has a screenshot attached.");
+ is(thirdDrawCallScreenshot.index, 4,
+ "The third draw call has the correct screenshot index.");
+ is(thirdDrawCallScreenshot.width, 128,
+ "The third draw call has the correct screenshot width.");
+ is(thirdDrawCallScreenshot.height, 128,
+ "The third draw call has the correct screenshot height.");
+ is([].find.call(Uint32(thirdDrawCallScreenshot.pixels), e => e > 0), 2160001024,
+ "The third draw call's screenshot's pixels seems to not be completely transparent.");
+
+ ok(fourthDrawCallScreenshot,
+ "The fourth draw call has a screenshot attached.");
+ is(fourthDrawCallScreenshot.index, 6,
+ "The fourth draw call has the correct screenshot index.");
+ is(fourthDrawCallScreenshot.width, 128,
+ "The fourth draw call has the correct screenshot width.");
+ is(fourthDrawCallScreenshot.height, 128,
+ "The fourth draw call has the correct screenshot height.");
+ is([].find.call(Uint32(fourthDrawCallScreenshot.pixels), e => e > 0), 2147483839,
+ "The fourth draw call's screenshot's pixels seems to not be completely transparent.");
+
+ isnot(firstDrawCallScreenshot.pixels, secondDrawCallScreenshot.pixels,
+ "The screenshots taken on consecutive draw calls are different (1).");
+ isnot(secondDrawCallScreenshot.pixels, thirdDrawCallScreenshot.pixels,
+ "The screenshots taken on consecutive draw calls are different (2).");
+ isnot(thirdDrawCallScreenshot.pixels, fourthDrawCallScreenshot.pixels,
+ "The screenshots taken on consecutive draw calls are different (3).");
+
+ yield removeTab(target.tab);
+ finish();
+}
+
+function Uint32(src) {
+ let charView = new Uint8Array(src);
+ return new Uint32Array(charView.buffer);
+}
diff --git a/devtools/client/canvasdebugger/test/browser_canvas-actor-test-07.js b/devtools/client/canvasdebugger/test/browser_canvas-actor-test-07.js
new file mode 100644
index 000000000..8e6c8c25a
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-actor-test-07.js
@@ -0,0 +1,94 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if screenshots for non-draw calls can still be retrieved properly,
+ * by deferring the the most recent previous draw-call.
+ */
+
+function* ifTestingSupported() {
+ let { target, front } = yield initCanvasDebuggerBackend(SIMPLE_CANVAS_URL);
+
+ let navigated = once(target, "navigate");
+
+ yield front.setup({ reload: true });
+ ok(true, "The front was setup up successfully.");
+
+ yield navigated;
+ ok(true, "Target automatically navigated when the front was set up.");
+
+ let snapshotActor = yield front.recordAnimationFrame();
+ let animationOverview = yield snapshotActor.getOverview();
+
+ let functionCalls = animationOverview.calls;
+ ok(functionCalls,
+ "An array of function call actors was sent after recording.");
+ is(functionCalls.length, 8,
+ "The number of function call actors is correct.");
+
+ let firstNonDrawCall = yield functionCalls[1].getDetails();
+ let secondNonDrawCall = yield functionCalls[3].getDetails();
+ let lastNonDrawCall = yield functionCalls[7].getDetails();
+
+ is(firstNonDrawCall.name, "fillStyle",
+ "The first non-draw function's name is correct.");
+ is(secondNonDrawCall.name, "fillStyle",
+ "The second non-draw function's name is correct.");
+ is(lastNonDrawCall.name, "requestAnimationFrame",
+ "The last non-draw function's name is correct.");
+
+ let firstScreenshot = yield snapshotActor.generateScreenshotFor(functionCalls[1]);
+ let secondScreenshot = yield snapshotActor.generateScreenshotFor(functionCalls[3]);
+ let lastScreenshot = yield snapshotActor.generateScreenshotFor(functionCalls[7]);
+
+ ok(firstScreenshot,
+ "A screenshot was successfully retrieved for the first non-draw function.");
+ ok(secondScreenshot,
+ "A screenshot was successfully retrieved for the second non-draw function.");
+ ok(lastScreenshot,
+ "A screenshot was successfully retrieved for the last non-draw function.");
+
+ let firstActualScreenshot = yield snapshotActor.generateScreenshotFor(functionCalls[0]);
+ ok(sameArray(firstScreenshot.pixels, firstActualScreenshot.pixels),
+ "The screenshot for the first non-draw function is correct.");
+ is(firstScreenshot.width, 128,
+ "The screenshot for the first non-draw function has the correct width.");
+ is(firstScreenshot.height, 128,
+ "The screenshot for the first non-draw function has the correct height.");
+
+ let secondActualScreenshot = yield snapshotActor.generateScreenshotFor(functionCalls[2]);
+ ok(sameArray(secondScreenshot.pixels, secondActualScreenshot.pixels),
+ "The screenshot for the second non-draw function is correct.");
+ is(secondScreenshot.width, 128,
+ "The screenshot for the second non-draw function has the correct width.");
+ is(secondScreenshot.height, 128,
+ "The screenshot for the second non-draw function has the correct height.");
+
+ let lastActualScreenshot = yield snapshotActor.generateScreenshotFor(functionCalls[6]);
+ ok(sameArray(lastScreenshot.pixels, lastActualScreenshot.pixels),
+ "The screenshot for the last non-draw function is correct.");
+ is(lastScreenshot.width, 128,
+ "The screenshot for the last non-draw function has the correct width.");
+ is(lastScreenshot.height, 128,
+ "The screenshot for the last non-draw function has the correct height.");
+
+ ok(!sameArray(firstScreenshot.pixels, secondScreenshot.pixels),
+ "The screenshots taken on consecutive draw calls are different (1).");
+ ok(!sameArray(secondScreenshot.pixels, lastScreenshot.pixels),
+ "The screenshots taken on consecutive draw calls are different (2).");
+
+ yield removeTab(target.tab);
+ finish();
+}
+
+function sameArray(a, b) {
+ if (a.length != b.length) {
+ return false;
+ }
+ for (let i = 0; i < a.length; i++) {
+ if (a[i] !== b[i]) {
+ return false;
+ }
+ }
+ return true;
+}
diff --git a/devtools/client/canvasdebugger/test/browser_canvas-actor-test-08.js b/devtools/client/canvasdebugger/test/browser_canvas-actor-test-08.js
new file mode 100644
index 000000000..f3aeda1a9
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-actor-test-08.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that integers used in arguments are not cast to their constant, enum value
+ * forms if the method's signature does not expect an enum. Bug 999687.
+ */
+
+function* ifTestingSupported() {
+ let { target, front } = yield initCanvasDebuggerBackend(SIMPLE_BITMASKS_URL);
+
+ let navigated = once(target, "navigate");
+
+ yield front.setup({ reload: true });
+ ok(true, "The front was setup up successfully.");
+
+ yield navigated;
+ ok(true, "Target automatically navigated when the front was set up.");
+
+ let snapshotActor = yield front.recordAnimationFrame();
+ let animationOverview = yield snapshotActor.getOverview();
+ let functionCalls = animationOverview.calls;
+
+ is(functionCalls[0].name, "clearRect",
+ "The first called function's name is correct.");
+ is(functionCalls[0].argsPreview, "0, 0, 4, 4",
+ "The first called function's args preview is not cast to enums.");
+
+ is(functionCalls[2].name, "fillRect",
+ "The fillRect called function's name is correct.");
+ is(functionCalls[2].argsPreview, "0, 0, 1, 1",
+ "The fillRect called function's args preview is not casted to enums.");
+
+ yield removeTab(target.tab);
+ finish();
+}
diff --git a/devtools/client/canvasdebugger/test/browser_canvas-actor-test-09.js b/devtools/client/canvasdebugger/test/browser_canvas-actor-test-09.js
new file mode 100644
index 000000000..d123e3319
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-actor-test-09.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that integers used in arguments are not cast to their constant, enum value
+ * forms if the method's signature does not expect an enum. Bug 999687.
+ */
+
+function* ifTestingSupported() {
+ let { target, front } = yield initCanvasDebuggerBackend(WEBGL_ENUM_URL);
+
+ let navigated = once(target, "navigate");
+
+ yield front.setup({ reload: true });
+ ok(true, "The front was setup up successfully.");
+
+ yield navigated;
+ ok(true, "Target automatically navigated when the front was set up.");
+
+ let snapshotActor = yield front.recordAnimationFrame();
+ let animationOverview = yield snapshotActor.getOverview();
+ let functionCalls = animationOverview.calls;
+
+ is(functionCalls[0].name, "clear",
+ "The function's name is correct.");
+ is(functionCalls[0].argsPreview, "DEPTH_BUFFER_BIT | STENCIL_BUFFER_BIT | COLOR_BUFFER_BIT",
+ "The bits passed into `gl.clear` have been cast to their enum values.");
+
+ is(functionCalls[1].name, "bindTexture",
+ "The function's name is correct.");
+ is(functionCalls[1].argsPreview, "TEXTURE_2D, null",
+ "The bits passed into `gl.bindTexture` have been cast to their enum values.");
+
+ yield removeTab(target.tab);
+ finish();
+}
diff --git a/devtools/client/canvasdebugger/test/browser_canvas-actor-test-10.js b/devtools/client/canvasdebugger/test/browser_canvas-actor-test-10.js
new file mode 100644
index 000000000..672ef9662
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-actor-test-10.js
@@ -0,0 +1,107 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the correct framebuffer, renderbuffer and textures are re-bound
+ * after generating screenshots using the actor.
+ */
+
+function* ifTestingSupported() {
+ let { target, front } = yield initCanvasDebuggerBackend(WEBGL_BINDINGS_URL);
+ loadFrameScripts();
+
+ let navigated = once(target, "navigate");
+
+ yield front.setup({ reload: true });
+ ok(true, "The front was setup up successfully.");
+
+ yield navigated;
+ ok(true, "Target automatically navigated when the front was set up.");
+
+ let snapshotActor = yield front.recordAnimationFrame();
+ let animationOverview = yield snapshotActor.getOverview();
+ let functionCalls = animationOverview.calls;
+
+ let firstScreenshot = yield snapshotActor.generateScreenshotFor(functionCalls[0]);
+ is(firstScreenshot.index, -1,
+ "The first screenshot didn't encounter any draw call.");
+ is(firstScreenshot.scaling, 0.25,
+ "The first screenshot has the correct scaling.");
+ is(firstScreenshot.width, CanvasFront.WEBGL_SCREENSHOT_MAX_HEIGHT,
+ "The first screenshot has the correct width.");
+ is(firstScreenshot.height, CanvasFront.WEBGL_SCREENSHOT_MAX_HEIGHT,
+ "The first screenshot has the correct height.");
+ is(firstScreenshot.flipped, true,
+ "The first screenshot has the correct 'flipped' flag.");
+ is(firstScreenshot.pixels.length, 0,
+ "The first screenshot should be empty.");
+
+ is((yield evalInDebuggee("gl.getParameter(gl.FRAMEBUFFER_BINDING) === customFramebuffer")),
+ true,
+ "The debuggee's gl context framebuffer wasn't changed.");
+ is((yield evalInDebuggee("gl.getParameter(gl.RENDERBUFFER_BINDING) === customRenderbuffer")),
+ true,
+ "The debuggee's gl context renderbuffer wasn't changed.");
+ is((yield evalInDebuggee("gl.getParameter(gl.TEXTURE_BINDING_2D) === customTexture")),
+ true,
+ "The debuggee's gl context texture binding wasn't changed.");
+ is((yield evalInDebuggee("gl.getParameter(gl.VIEWPORT)[0]")),
+ 128,
+ "The debuggee's gl context viewport's left coord. wasn't changed.");
+ is((yield evalInDebuggee("gl.getParameter(gl.VIEWPORT)[1]")),
+ 256,
+ "The debuggee's gl context viewport's left coord. wasn't changed.");
+ is((yield evalInDebuggee("gl.getParameter(gl.VIEWPORT)[2]")),
+ 384,
+ "The debuggee's gl context viewport's left coord. wasn't changed.");
+ is((yield evalInDebuggee("gl.getParameter(gl.VIEWPORT)[3]")),
+ 512,
+ "The debuggee's gl context viewport's left coord. wasn't changed.");
+
+ let secondScreenshot = yield snapshotActor.generateScreenshotFor(functionCalls[1]);
+ is(secondScreenshot.index, 1,
+ "The second screenshot has the correct index.");
+ is(secondScreenshot.width, CanvasFront.WEBGL_SCREENSHOT_MAX_HEIGHT,
+ "The second screenshot has the correct width.");
+ is(secondScreenshot.height, CanvasFront.WEBGL_SCREENSHOT_MAX_HEIGHT,
+ "The second screenshot has the correct height.");
+ is(secondScreenshot.scaling, 0.25,
+ "The second screenshot has the correct scaling.");
+ is(secondScreenshot.flipped, true,
+ "The second screenshot has the correct 'flipped' flag.");
+ is(secondScreenshot.pixels.length, Math.pow(CanvasFront.WEBGL_SCREENSHOT_MAX_HEIGHT, 2) * 4,
+ "The second screenshot should not be empty.");
+ is(secondScreenshot.pixels[0], 0,
+ "The second screenshot has the correct red component.");
+ is(secondScreenshot.pixels[1], 0,
+ "The second screenshot has the correct green component.");
+ is(secondScreenshot.pixels[2], 255,
+ "The second screenshot has the correct blue component.");
+ is(secondScreenshot.pixels[3], 255,
+ "The second screenshot has the correct alpha component.");
+
+ is((yield evalInDebuggee("gl.getParameter(gl.FRAMEBUFFER_BINDING) === customFramebuffer")),
+ true,
+ "The debuggee's gl context framebuffer still wasn't changed.");
+ is((yield evalInDebuggee("gl.getParameter(gl.RENDERBUFFER_BINDING) === customRenderbuffer")),
+ true,
+ "The debuggee's gl context renderbuffer still wasn't changed.");
+ is((yield evalInDebuggee("gl.getParameter(gl.TEXTURE_BINDING_2D) === customTexture")),
+ true,
+ "The debuggee's gl context texture binding still wasn't changed.");
+ is((yield evalInDebuggee("gl.getParameter(gl.VIEWPORT)[0]")),
+ 128,
+ "The debuggee's gl context viewport's left coord. still wasn't changed.");
+ is((yield evalInDebuggee("gl.getParameter(gl.VIEWPORT)[1]")),
+ 256,
+ "The debuggee's gl context viewport's left coord. still wasn't changed.");
+ is((yield evalInDebuggee("gl.getParameter(gl.VIEWPORT)[2]")),
+ 384,
+ "The debuggee's gl context viewport's left coord. still wasn't changed.");
+ is((yield evalInDebuggee("gl.getParameter(gl.VIEWPORT)[3]")),
+ 512,
+ "The debuggee's gl context viewport's left coord. still wasn't changed.");
+
+ yield removeTab(target.tab);
+ finish();
+}
diff --git a/devtools/client/canvasdebugger/test/browser_canvas-actor-test-11.js b/devtools/client/canvasdebugger/test/browser_canvas-actor-test-11.js
new file mode 100644
index 000000000..a1e5010b6
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-actor-test-11.js
@@ -0,0 +1,138 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that loops using setTimeout are recorded and stored
+ * for a canvas context, and that the generated screenshots are correct.
+ */
+
+function* ifTestingSupported() {
+ let { target, front } = yield initCanvasDebuggerBackend(SET_TIMEOUT_URL);
+
+ let navigated = once(target, "navigate");
+
+ yield front.setup({ reload: true });
+ ok(true, "The front was setup up successfully.");
+
+ yield navigated;
+ ok(true, "Target automatically navigated when the front was set up.");
+
+ let snapshotActor = yield front.recordAnimationFrame();
+ ok(snapshotActor,
+ "A snapshot actor was sent after recording.");
+
+ let animationOverview = yield snapshotActor.getOverview();
+ ok(snapshotActor,
+ "An animation overview could be retrieved after recording.");
+
+ let functionCalls = animationOverview.calls;
+ ok(functionCalls,
+ "An array of function call actors was sent after recording.");
+ is(functionCalls.length, 8,
+ "The number of function call actors is correct.");
+
+ is(functionCalls[0].type, CallWatcherFront.METHOD_FUNCTION,
+ "The first called function is correctly identified as a method.");
+ is(functionCalls[0].name, "clearRect",
+ "The first called function's name is correct.");
+ is(functionCalls[0].file, SET_TIMEOUT_URL,
+ "The first called function's file is correct.");
+ is(functionCalls[0].line, 25,
+ "The first called function's line is correct.");
+ is(functionCalls[0].argsPreview, "0, 0, 128, 128",
+ "The first called function's args preview is correct.");
+ is(functionCalls[0].callerPreview, "Object",
+ "The first called function's caller preview is correct.");
+
+ is(functionCalls[6].type, CallWatcherFront.METHOD_FUNCTION,
+ "The penultimate called function is correctly identified as a method.");
+ is(functionCalls[6].name, "fillRect",
+ "The penultimate called function's name is correct.");
+ is(functionCalls[6].file, SET_TIMEOUT_URL,
+ "The penultimate called function's file is correct.");
+ is(functionCalls[6].line, 21,
+ "The penultimate called function's line is correct.");
+ is(functionCalls[6].argsPreview, "10, 10, 55, 50",
+ "The penultimate called function's args preview is correct.");
+ is(functionCalls[6].callerPreview, "Object",
+ "The penultimate called function's caller preview is correct.");
+
+ is(functionCalls[7].type, CallWatcherFront.METHOD_FUNCTION,
+ "The last called function is correctly identified as a method.");
+ is(functionCalls[7].name, "setTimeout",
+ "The last called function's name is correct.");
+ is(functionCalls[7].file, SET_TIMEOUT_URL,
+ "The last called function's file is correct.");
+ is(functionCalls[7].line, 30,
+ "The last called function's line is correct.");
+ ok(functionCalls[7].argsPreview.includes("Function"),
+ "The last called function's args preview is correct.");
+ is(functionCalls[7].callerPreview, "Object",
+ "The last called function's caller preview is correct.");
+
+ let firstNonDrawCall = yield functionCalls[1].getDetails();
+ let secondNonDrawCall = yield functionCalls[3].getDetails();
+ let lastNonDrawCall = yield functionCalls[7].getDetails();
+
+ is(firstNonDrawCall.name, "fillStyle",
+ "The first non-draw function's name is correct.");
+ is(secondNonDrawCall.name, "fillStyle",
+ "The second non-draw function's name is correct.");
+ is(lastNonDrawCall.name, "setTimeout",
+ "The last non-draw function's name is correct.");
+
+ let firstScreenshot = yield snapshotActor.generateScreenshotFor(functionCalls[1]);
+ let secondScreenshot = yield snapshotActor.generateScreenshotFor(functionCalls[3]);
+ let lastScreenshot = yield snapshotActor.generateScreenshotFor(functionCalls[7]);
+
+ ok(firstScreenshot,
+ "A screenshot was successfully retrieved for the first non-draw function.");
+ ok(secondScreenshot,
+ "A screenshot was successfully retrieved for the second non-draw function.");
+ ok(lastScreenshot,
+ "A screenshot was successfully retrieved for the last non-draw function.");
+
+ let firstActualScreenshot = yield snapshotActor.generateScreenshotFor(functionCalls[0]);
+ ok(sameArray(firstScreenshot.pixels, firstActualScreenshot.pixels),
+ "The screenshot for the first non-draw function is correct.");
+ is(firstScreenshot.width, 128,
+ "The screenshot for the first non-draw function has the correct width.");
+ is(firstScreenshot.height, 128,
+ "The screenshot for the first non-draw function has the correct height.");
+
+ let secondActualScreenshot = yield snapshotActor.generateScreenshotFor(functionCalls[2]);
+ ok(sameArray(secondScreenshot.pixels, secondActualScreenshot.pixels),
+ "The screenshot for the second non-draw function is correct.");
+ is(secondScreenshot.width, 128,
+ "The screenshot for the second non-draw function has the correct width.");
+ is(secondScreenshot.height, 128,
+ "The screenshot for the second non-draw function has the correct height.");
+
+ let lastActualScreenshot = yield snapshotActor.generateScreenshotFor(functionCalls[6]);
+ ok(sameArray(lastScreenshot.pixels, lastActualScreenshot.pixels),
+ "The screenshot for the last non-draw function is correct.");
+ is(lastScreenshot.width, 128,
+ "The screenshot for the last non-draw function has the correct width.");
+ is(lastScreenshot.height, 128,
+ "The screenshot for the last non-draw function has the correct height.");
+
+ ok(!sameArray(firstScreenshot.pixels, secondScreenshot.pixels),
+ "The screenshots taken on consecutive draw calls are different (1).");
+ ok(!sameArray(secondScreenshot.pixels, lastScreenshot.pixels),
+ "The screenshots taken on consecutive draw calls are different (2).");
+
+ yield removeTab(target.tab);
+ finish();
+}
+
+function sameArray(a, b) {
+ if (a.length != b.length) {
+ return false;
+ }
+ for (let i = 0; i < a.length; i++) {
+ if (a[i] !== b[i]) {
+ return false;
+ }
+ }
+ return true;
+}
diff --git a/devtools/client/canvasdebugger/test/browser_canvas-actor-test-12.js b/devtools/client/canvasdebugger/test/browser_canvas-actor-test-12.js
new file mode 100644
index 000000000..86e51931e
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-actor-test-12.js
@@ -0,0 +1,29 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the recording can be disabled via stopRecordingAnimationFrame
+ * in the event no rAF loop is found.
+ */
+
+function* ifTestingSupported() {
+ let { target, front } = yield initCanvasDebuggerBackend(NO_CANVAS_URL);
+ loadFrameScripts();
+
+ let navigated = once(target, "navigate");
+
+ yield front.setup({ reload: true });
+ ok(true, "The front was setup up successfully.");
+
+ yield navigated;
+ ok(true, "Target automatically navigated when the front was set up.");
+
+ let startRecording = front.recordAnimationFrame();
+ yield front.stopRecordingAnimationFrame();
+
+ ok(!(yield startRecording),
+ "recordAnimationFrame() does not return a SnapshotActor when cancelled.");
+
+ yield removeTab(target.tab);
+ finish();
+}
diff --git a/devtools/client/canvasdebugger/test/browser_canvas-frontend-call-highlight.js b/devtools/client/canvasdebugger/test/browser_canvas-frontend-call-highlight.js
new file mode 100644
index 000000000..2270f0ccf
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-call-highlight.js
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if certain function calls are properly highlighted in the UI.
+ */
+
+function* ifTestingSupported() {
+ let { target, panel } = yield initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL);
+ let { window, $, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin;
+
+ yield reload(target);
+
+ let recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
+ let callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED);
+ SnapshotsListView._onRecordButtonClick();
+ yield promise.all([recordingFinished, callListPopulated]);
+
+ is(CallsListView.itemCount, 8,
+ "All the function calls should now be displayed in the UI.");
+
+ is($(".call-item-view", CallsListView.getItemAtIndex(0).target).hasAttribute("draw-call"), true,
+ "The first item's node should have a draw-call attribute.");
+ is($(".call-item-view", CallsListView.getItemAtIndex(1).target).hasAttribute("draw-call"), false,
+ "The second item's node should not have a draw-call attribute.");
+ is($(".call-item-view", CallsListView.getItemAtIndex(2).target).hasAttribute("draw-call"), true,
+ "The third item's node should have a draw-call attribute.");
+ is($(".call-item-view", CallsListView.getItemAtIndex(3).target).hasAttribute("draw-call"), false,
+ "The fourth item's node should not have a draw-call attribute.");
+ is($(".call-item-view", CallsListView.getItemAtIndex(4).target).hasAttribute("draw-call"), true,
+ "The fifth item's node should have a draw-call attribute.");
+ is($(".call-item-view", CallsListView.getItemAtIndex(5).target).hasAttribute("draw-call"), false,
+ "The sixth item's node should not have a draw-call attribute.");
+ is($(".call-item-view", CallsListView.getItemAtIndex(6).target).hasAttribute("draw-call"), true,
+ "The seventh item's node should have a draw-call attribute.");
+ is($(".call-item-view", CallsListView.getItemAtIndex(7).target).hasAttribute("draw-call"), false,
+ "The eigth item's node should not have a draw-call attribute.");
+
+ yield teardown(panel);
+ finish();
+}
diff --git a/devtools/client/canvasdebugger/test/browser_canvas-frontend-call-list.js b/devtools/client/canvasdebugger/test/browser_canvas-frontend-call-list.js
new file mode 100644
index 000000000..5f9ce876f
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-call-list.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if all the function calls associated with an animation frame snapshot
+ * are properly displayed in the UI.
+ */
+
+function* ifTestingSupported() {
+ let { target, panel } = yield initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL);
+ let { window, $, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin;
+
+ yield reload(target);
+
+ let recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
+ let callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED);
+ SnapshotsListView._onRecordButtonClick();
+ yield promise.all([recordingFinished, callListPopulated]);
+
+ is(CallsListView.itemCount, 8,
+ "All the function calls should now be displayed in the UI.");
+
+ testItem(CallsListView.getItemAtIndex(0),
+ "1", "Object", "clearRect", "(0, 0, 128, 128)", "doc_simple-canvas.html:25");
+
+ testItem(CallsListView.getItemAtIndex(1),
+ "2", "Object", "fillStyle", " = rgb(192, 192, 192)", "doc_simple-canvas.html:20");
+ testItem(CallsListView.getItemAtIndex(2),
+ "3", "Object", "fillRect", "(0, 0, 128, 128)", "doc_simple-canvas.html:21");
+
+ testItem(CallsListView.getItemAtIndex(3),
+ "4", "Object", "fillStyle", " = rgba(0, 0, 192, 0.5)", "doc_simple-canvas.html:20");
+ testItem(CallsListView.getItemAtIndex(4),
+ "5", "Object", "fillRect", "(30, 30, 55, 50)", "doc_simple-canvas.html:21");
+
+ testItem(CallsListView.getItemAtIndex(5),
+ "6", "Object", "fillStyle", " = rgba(192, 0, 0, 0.5)", "doc_simple-canvas.html:20");
+ testItem(CallsListView.getItemAtIndex(6),
+ "7", "Object", "fillRect", "(10, 10, 55, 50)", "doc_simple-canvas.html:21");
+
+ testItem(CallsListView.getItemAtIndex(7),
+ "8", "", "requestAnimationFrame", "(Function)", "doc_simple-canvas.html:30");
+
+ function testItem(item, index, context, name, args, location) {
+ let i = CallsListView.indexOfItem(item);
+ is(i, index - 1,
+ "The item at index " + index + " is correctly displayed in the UI.");
+
+ is($(".call-item-index", item.target).getAttribute("value"), index,
+ "The item's gutter label has the correct text.");
+
+ if (context) {
+ is($(".call-item-context", item.target).getAttribute("value"), context,
+ "The item's context label has the correct text.");
+ } else {
+ is($(".call-item-context", item.target) + "", "[object XULElement]",
+ "The item's context label should not be available.");
+ }
+
+ is($(".call-item-name", item.target).getAttribute("value"), name,
+ "The item's name label has the correct text.");
+ is($(".call-item-args", item.target).getAttribute("value"), args,
+ "The item's args label has the correct text.");
+ is($(".call-item-location", item.target).getAttribute("value"), location,
+ "The item's location label has the correct text.");
+ }
+
+ yield teardown(panel);
+ finish();
+}
diff --git a/devtools/client/canvasdebugger/test/browser_canvas-frontend-call-search.js b/devtools/client/canvasdebugger/test/browser_canvas-frontend-call-search.js
new file mode 100644
index 000000000..e865df391
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-call-search.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if filtering the items in the call list works properly.
+ */
+
+function* ifTestingSupported() {
+ let { target, panel } = yield initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL);
+ let { window, $, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin;
+ let searchbox = $("#calls-searchbox");
+
+ yield reload(target);
+
+ let firstRecordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
+ let callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED);
+ SnapshotsListView._onRecordButtonClick();
+ yield promise.all([firstRecordingFinished, callListPopulated]);
+
+ is(searchbox.value, "",
+ "The searchbox should be initially empty.");
+ is(CallsListView.visibleItems.length, 8,
+ "All the items should be initially visible in the calls list.");
+
+ searchbox.focus();
+ EventUtils.sendString("clear", window);
+
+ is(searchbox.value, "clear",
+ "The searchbox should now contain the 'clear' string.");
+ is(CallsListView.visibleItems.length, 1,
+ "Only one item should now be visible in the calls list.");
+
+ is(CallsListView.visibleItems[0].attachment.actor.type, CallWatcherFront.METHOD_FUNCTION,
+ "The visible item's type has the expected value.");
+ is(CallsListView.visibleItems[0].attachment.actor.name, "clearRect",
+ "The visible item's name has the expected value.");
+ is(CallsListView.visibleItems[0].attachment.actor.file, SIMPLE_CANVAS_URL,
+ "The visible item's file has the expected value.");
+ is(CallsListView.visibleItems[0].attachment.actor.line, 25,
+ "The visible item's line has the expected value.");
+ is(CallsListView.visibleItems[0].attachment.actor.argsPreview, "0, 0, 128, 128",
+ "The visible item's args have the expected value.");
+ is(CallsListView.visibleItems[0].attachment.actor.callerPreview, "Object",
+ "The visible item's caller has the expected value.");
+
+ let secondRecordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
+ callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED);
+
+ SnapshotsListView._onRecordButtonClick();
+ yield secondRecordingFinished;
+
+ SnapshotsListView.selectedIndex = 1;
+ yield callListPopulated;
+
+ is(searchbox.value, "clear",
+ "The searchbox should still contain the 'clear' string.");
+ is(CallsListView.visibleItems.length, 1,
+ "Only one item should still be visible in the calls list.");
+
+ for (let i = 0; i < 5; i++) {
+ searchbox.focus();
+ EventUtils.sendKey("BACK_SPACE", window);
+ }
+
+ is(searchbox.value, "",
+ "The searchbox should now be emptied.");
+ is(CallsListView.visibleItems.length, 8,
+ "All the items should be initially visible again in the calls list.");
+
+ yield teardown(panel);
+ finish();
+}
diff --git a/devtools/client/canvasdebugger/test/browser_canvas-frontend-call-stack-01.js b/devtools/client/canvasdebugger/test/browser_canvas-frontend-call-stack-01.js
new file mode 100644
index 000000000..964683c84
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-call-stack-01.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the a function call's stack is properly displayed in the UI.
+ */
+
+// Force the old debugger UI since it's directly used (see Bug 1301705)
+Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false);
+registerCleanupFunction(function* () {
+ Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend");
+});
+
+function* ifTestingSupported() {
+ let { target, panel } = yield initCanvasDebuggerFrontend(SIMPLE_CANVAS_DEEP_STACK_URL);
+ let { window, $, $all, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin;
+
+ yield reload(target);
+
+ let recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
+ let callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED);
+ SnapshotsListView._onRecordButtonClick();
+ yield promise.all([recordingFinished, callListPopulated]);
+
+ let callItem = CallsListView.getItemAtIndex(2);
+ let locationLink = $(".call-item-location", callItem.target);
+
+ is($(".call-item-stack", callItem.target), null,
+ "There should be no stack container available yet for the draw call.");
+
+ let callStackDisplayed = once(window, EVENTS.CALL_STACK_DISPLAYED);
+ EventUtils.sendMouseEvent({ type: "mousedown" }, locationLink, window);
+ yield callStackDisplayed;
+
+ isnot($(".call-item-stack", callItem.target), null,
+ "There should be a stack container available now for the draw call.");
+ // We may have more than 4 functions, depending on whether async
+ // stacks are available.
+ ok($all(".call-item-stack-fn", callItem.target).length >= 4,
+ "There should be at least 4 functions on the stack for the draw call.");
+
+ ok($all(".call-item-stack-fn-name", callItem.target)[0].getAttribute("value")
+ .includes("C()"),
+ "The first function on the stack has the correct name.");
+ ok($all(".call-item-stack-fn-name", callItem.target)[1].getAttribute("value")
+ .includes("B()"),
+ "The second function on the stack has the correct name.");
+ ok($all(".call-item-stack-fn-name", callItem.target)[2].getAttribute("value")
+ .includes("A()"),
+ "The third function on the stack has the correct name.");
+ ok($all(".call-item-stack-fn-name", callItem.target)[3].getAttribute("value")
+ .includes("drawRect()"),
+ "The fourth function on the stack has the correct name.");
+
+ is($all(".call-item-stack-fn-location", callItem.target)[0].getAttribute("value"),
+ "doc_simple-canvas-deep-stack.html:26",
+ "The first function on the stack has the correct location.");
+ is($all(".call-item-stack-fn-location", callItem.target)[1].getAttribute("value"),
+ "doc_simple-canvas-deep-stack.html:28",
+ "The second function on the stack has the correct location.");
+ is($all(".call-item-stack-fn-location", callItem.target)[2].getAttribute("value"),
+ "doc_simple-canvas-deep-stack.html:30",
+ "The third function on the stack has the correct location.");
+ is($all(".call-item-stack-fn-location", callItem.target)[3].getAttribute("value"),
+ "doc_simple-canvas-deep-stack.html:35",
+ "The fourth function on the stack has the correct location.");
+
+ let jumpedToSource = once(window, EVENTS.SOURCE_SHOWN_IN_JS_DEBUGGER);
+ EventUtils.sendMouseEvent({ type: "mousedown" }, $(".call-item-stack-fn-location", callItem.target));
+ yield jumpedToSource;
+
+ let toolbox = yield gDevTools.getToolbox(target);
+ let { panelWin: { DebuggerView: view } } = toolbox.getPanel("jsdebugger");
+
+ is(view.Sources.selectedValue, getSourceActor(view.Sources, SIMPLE_CANVAS_DEEP_STACK_URL),
+ "The expected source was shown in the debugger.");
+ is(view.editor.getCursor().line, 25,
+ "The expected source line is highlighted in the debugger.");
+
+ yield teardown(panel);
+ finish();
+}
diff --git a/devtools/client/canvasdebugger/test/browser_canvas-frontend-call-stack-02.js b/devtools/client/canvasdebugger/test/browser_canvas-frontend-call-stack-02.js
new file mode 100644
index 000000000..9b5c65839
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-call-stack-02.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the a function call's stack is properly displayed in the UI
+ * and jumping to source in the debugger for the topmost call item works.
+ */
+
+// Force the old debugger UI since it's directly used (see Bug 1301705)
+Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false);
+registerCleanupFunction(function* () {
+ Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend");
+});
+
+function* ifTestingSupported() {
+ let { target, panel } = yield initCanvasDebuggerFrontend(SIMPLE_CANVAS_DEEP_STACK_URL);
+ let { window, $, $all, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin;
+
+ yield reload(target);
+
+ let recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
+ let callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED);
+ SnapshotsListView._onRecordButtonClick();
+ yield promise.all([recordingFinished, callListPopulated]);
+
+ let callItem = CallsListView.getItemAtIndex(2);
+ let locationLink = $(".call-item-location", callItem.target);
+
+ is($(".call-item-stack", callItem.target), null,
+ "There should be no stack container available yet for the draw call.");
+
+ let callStackDisplayed = once(window, EVENTS.CALL_STACK_DISPLAYED);
+ EventUtils.sendMouseEvent({ type: "mousedown" }, locationLink, window);
+ yield callStackDisplayed;
+
+ isnot($(".call-item-stack", callItem.target), null,
+ "There should be a stack container available now for the draw call.");
+ // We may have more than 4 functions, depending on whether async
+ // stacks are available.
+ ok($all(".call-item-stack-fn", callItem.target).length >= 4,
+ "There should be at least 4 functions on the stack for the draw call.");
+
+ let jumpedToSource = once(window, EVENTS.SOURCE_SHOWN_IN_JS_DEBUGGER);
+ EventUtils.sendMouseEvent({ type: "mousedown" }, $(".call-item-location", callItem.target));
+ yield jumpedToSource;
+
+ let toolbox = yield gDevTools.getToolbox(target);
+ let { panelWin: { DebuggerView: view } } = toolbox.getPanel("jsdebugger");
+
+ is(view.Sources.selectedValue, getSourceActor(view.Sources, SIMPLE_CANVAS_DEEP_STACK_URL),
+ "The expected source was shown in the debugger.");
+ is(view.editor.getCursor().line, 23,
+ "The expected source line is highlighted in the debugger.");
+
+ yield teardown(panel);
+ finish();
+}
diff --git a/devtools/client/canvasdebugger/test/browser_canvas-frontend-call-stack-03.js b/devtools/client/canvasdebugger/test/browser_canvas-frontend-call-stack-03.js
new file mode 100644
index 000000000..24780c566
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-call-stack-03.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the a function call's stack can be shown/hidden by double-clicking
+ * on a function call item.
+ */
+
+function* ifTestingSupported() {
+ let { target, panel } = yield initCanvasDebuggerFrontend(SIMPLE_CANVAS_DEEP_STACK_URL);
+ let { window, $, $all, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin;
+
+ yield reload(target);
+
+ let recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
+ let callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED);
+ SnapshotsListView._onRecordButtonClick();
+ yield promise.all([recordingFinished, callListPopulated]);
+
+ let callItem = CallsListView.getItemAtIndex(2);
+ let view = $(".call-item-view", callItem.target);
+ let contents = $(".call-item-contents", callItem.target);
+
+ is(view.hasAttribute("call-stack-populated"), false,
+ "The call item's view should not have the stack populated yet.");
+ is(view.hasAttribute("call-stack-expanded"), false,
+ "The call item's view should not have the stack populated yet.");
+ is($(".call-item-stack", callItem.target), null,
+ "There should be no stack container available yet for the draw call.");
+
+ let callStackDisplayed = once(window, EVENTS.CALL_STACK_DISPLAYED);
+ EventUtils.sendMouseEvent({ type: "dblclick" }, contents, window);
+ yield callStackDisplayed;
+
+ is(view.hasAttribute("call-stack-populated"), true,
+ "The call item's view should have the stack populated now.");
+ is(view.getAttribute("call-stack-expanded"), "true",
+ "The call item's view should have the stack expanded now.");
+ isnot($(".call-item-stack", callItem.target), null,
+ "There should be a stack container available now for the draw call.");
+ is($(".call-item-stack", callItem.target).hidden, false,
+ "The stack container should now be visible.");
+ // We may have more than 4 functions, depending on whether async
+ // stacks are available.
+ ok($all(".call-item-stack-fn", callItem.target).length >= 4,
+ "There should be at least 4 functions on the stack for the draw call.");
+
+ EventUtils.sendMouseEvent({ type: "dblclick" }, contents, window);
+
+ is(view.hasAttribute("call-stack-populated"), true,
+ "The call item's view should still have the stack populated.");
+ is(view.getAttribute("call-stack-expanded"), "false",
+ "The call item's view should not have the stack expanded anymore.");
+ isnot($(".call-item-stack", callItem.target), null,
+ "There should still be a stack container available for the draw call.");
+ is($(".call-item-stack", callItem.target).hidden, true,
+ "The stack container should now be hidden.");
+ // We may have more than 4 functions, depending on whether async
+ // stacks are available.
+ ok($all(".call-item-stack-fn", callItem.target).length >= 4,
+ "There should still be at least 4 functions on the stack for the draw call.");
+
+ yield teardown(panel);
+ finish();
+}
diff --git a/devtools/client/canvasdebugger/test/browser_canvas-frontend-clear.js b/devtools/client/canvasdebugger/test/browser_canvas-frontend-clear.js
new file mode 100644
index 000000000..c80082046
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-clear.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if clearing the snapshots list works as expected.
+ */
+
+function* ifTestingSupported() {
+ let { target, panel } = yield initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL);
+ let { window, EVENTS, SnapshotsListView } = panel.panelWin;
+
+ yield reload(target);
+
+ let firstRecordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
+ SnapshotsListView._onRecordButtonClick();
+
+ yield firstRecordingFinished;
+ ok(true, "Finished recording a snapshot of the animation loop.");
+
+ is(SnapshotsListView.itemCount, 1,
+ "There should be one item available in the snapshots list.");
+
+ let secondRecordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
+ SnapshotsListView._onRecordButtonClick();
+
+ yield secondRecordingFinished;
+ ok(true, "Finished recording another snapshot of the animation loop.");
+
+ is(SnapshotsListView.itemCount, 2,
+ "There should be two items available in the snapshots list.");
+
+ let clearingFinished = once(window, EVENTS.SNAPSHOTS_LIST_CLEARED);
+ SnapshotsListView._onClearButtonClick();
+
+ yield clearingFinished;
+ ok(true, "Finished recording all snapshots.");
+
+ is(SnapshotsListView.itemCount, 0,
+ "There should be no items available in the snapshots list.");
+
+ yield teardown(panel);
+ finish();
+}
diff --git a/devtools/client/canvasdebugger/test/browser_canvas-frontend-img-screenshots.js b/devtools/client/canvasdebugger/test/browser_canvas-frontend-img-screenshots.js
new file mode 100644
index 000000000..e96543e10
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-img-screenshots.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if screenshots are properly displayed in the UI.
+ */
+
+function* ifTestingSupported() {
+ let { target, panel } = yield initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL);
+ let { window, $, EVENTS, SnapshotsListView } = panel.panelWin;
+
+ yield reload(target);
+
+ let recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
+ let callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED);
+ let screenshotDisplayed = once(window, EVENTS.CALL_SCREENSHOT_DISPLAYED);
+ SnapshotsListView._onRecordButtonClick();
+ yield promise.all([recordingFinished, callListPopulated, screenshotDisplayed]);
+
+ is($("#screenshot-container").hidden, false,
+ "The screenshot container should now be visible.");
+
+ is($("#screenshot-dimensions").getAttribute("value"), "128" + "\u00D7" + "128",
+ "The screenshot dimensions label has the expected value.");
+
+ is($("#screenshot-image").getAttribute("flipped"), "false",
+ "The screenshot element should not be flipped vertically.");
+
+ ok(window.getComputedStyle($("#screenshot-image")).backgroundImage.includes("#screenshot-rendering"),
+ "The screenshot element should have an offscreen canvas element as a background.");
+
+ yield teardown(panel);
+ finish();
+}
diff --git a/devtools/client/canvasdebugger/test/browser_canvas-frontend-img-thumbnails-01.js b/devtools/client/canvasdebugger/test/browser_canvas-frontend-img-thumbnails-01.js
new file mode 100644
index 000000000..41e8f7383
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-img-thumbnails-01.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if thumbnails are properly displayed in the UI.
+ */
+
+function* ifTestingSupported() {
+ let { target, panel } = yield initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL);
+ let { window, $, $all, EVENTS, SnapshotsListView } = panel.panelWin;
+
+ yield reload(target);
+
+ let recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
+ let callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED);
+ let thumbnailsDisplayed = once(window, EVENTS.THUMBNAILS_DISPLAYED);
+ SnapshotsListView._onRecordButtonClick();
+ yield promise.all([recordingFinished, callListPopulated, thumbnailsDisplayed]);
+
+ is($all(".filmstrip-thumbnail").length, 4,
+ "There should be 4 thumbnails displayed in the UI.");
+
+ let firstThumbnail = $(".filmstrip-thumbnail[index='0']");
+ ok(firstThumbnail,
+ "The first thumbnail element should be for the function call at index 0.");
+ is(firstThumbnail.width, 50,
+ "The first thumbnail's width is correct.");
+ is(firstThumbnail.height, 50,
+ "The first thumbnail's height is correct.");
+ is(firstThumbnail.getAttribute("flipped"), "false",
+ "The first thumbnail should not be flipped vertically.");
+
+ let secondThumbnail = $(".filmstrip-thumbnail[index='2']");
+ ok(secondThumbnail,
+ "The second thumbnail element should be for the function call at index 2.");
+ is(secondThumbnail.width, 50,
+ "The second thumbnail's width is correct.");
+ is(secondThumbnail.height, 50,
+ "The second thumbnail's height is correct.");
+ is(secondThumbnail.getAttribute("flipped"), "false",
+ "The second thumbnail should not be flipped vertically.");
+
+ let thirdThumbnail = $(".filmstrip-thumbnail[index='4']");
+ ok(thirdThumbnail,
+ "The third thumbnail element should be for the function call at index 4.");
+ is(thirdThumbnail.width, 50,
+ "The third thumbnail's width is correct.");
+ is(thirdThumbnail.height, 50,
+ "The third thumbnail's height is correct.");
+ is(thirdThumbnail.getAttribute("flipped"), "false",
+ "The third thumbnail should not be flipped vertically.");
+
+ let fourthThumbnail = $(".filmstrip-thumbnail[index='6']");
+ ok(fourthThumbnail,
+ "The fourth thumbnail element should be for the function call at index 6.");
+ is(fourthThumbnail.width, 50,
+ "The fourth thumbnail's width is correct.");
+ is(fourthThumbnail.height, 50,
+ "The fourth thumbnail's height is correct.");
+ is(fourthThumbnail.getAttribute("flipped"), "false",
+ "The fourth thumbnail should not be flipped vertically.");
+
+ yield teardown(panel);
+ finish();
+}
diff --git a/devtools/client/canvasdebugger/test/browser_canvas-frontend-img-thumbnails-02.js b/devtools/client/canvasdebugger/test/browser_canvas-frontend-img-thumbnails-02.js
new file mode 100644
index 000000000..798bc090b
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-img-thumbnails-02.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if thumbnails are correctly linked with other UI elements like
+ * function call items and their respective screenshots.
+ */
+
+function* ifTestingSupported() {
+ let { target, panel } = yield initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL);
+ let { window, $, $all, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin;
+
+ yield reload(target);
+
+ let recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
+ let callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED);
+ let thumbnailsDisplayed = once(window, EVENTS.THUMBNAILS_DISPLAYED);
+ let screenshotDisplayed = once(window, EVENTS.CALL_SCREENSHOT_DISPLAYED);
+ SnapshotsListView._onRecordButtonClick();
+ yield promise.all([
+ recordingFinished,
+ callListPopulated,
+ thumbnailsDisplayed,
+ screenshotDisplayed
+ ]);
+
+ is($all(".filmstrip-thumbnail[highlighted]").length, 0,
+ "There should be no highlighted thumbnail available yet.");
+ is(CallsListView.selectedIndex, -1,
+ "There should be no selected item in the calls list view.");
+
+ EventUtils.sendMouseEvent({ type: "mousedown" }, $all(".filmstrip-thumbnail")[0], window);
+ yield once(window, EVENTS.CALL_SCREENSHOT_DISPLAYED);
+ info("The first draw call was selected, by clicking the first thumbnail.");
+
+ isnot($(".filmstrip-thumbnail[highlighted][index='0']"), null,
+ "There should be a highlighted thumbnail available now, for the first draw call.");
+ is($all(".filmstrip-thumbnail[highlighted]").length, 1,
+ "There should be only one highlighted thumbnail available now.");
+ is(CallsListView.selectedIndex, 0,
+ "The first draw call should be selected in the calls list view.");
+
+ EventUtils.sendMouseEvent({ type: "mousedown" }, $all(".call-item-view")[1], window);
+ yield once(window, EVENTS.CALL_SCREENSHOT_DISPLAYED);
+ info("The second context call was selected, by clicking the second call item.");
+
+ isnot($(".filmstrip-thumbnail[highlighted][index='0']"), null,
+ "There should be a highlighted thumbnail available, for the first draw call.");
+ is($all(".filmstrip-thumbnail[highlighted]").length, 1,
+ "There should be only one highlighted thumbnail available.");
+ is(CallsListView.selectedIndex, 1,
+ "The second draw call should be selected in the calls list view.");
+
+ EventUtils.sendMouseEvent({ type: "mousedown" }, $all(".call-item-view")[2], window);
+ yield once(window, EVENTS.CALL_SCREENSHOT_DISPLAYED);
+ info("The second draw call was selected, by clicking the third call item.");
+
+ isnot($(".filmstrip-thumbnail[highlighted][index='2']"), null,
+ "There should be a highlighted thumbnail available, for the second draw call.");
+ is($all(".filmstrip-thumbnail[highlighted]").length, 1,
+ "There should be only one highlighted thumbnail available.");
+ is(CallsListView.selectedIndex, 2,
+ "The second draw call should be selected in the calls list view.");
+
+ yield teardown(panel);
+ finish();
+}
diff --git a/devtools/client/canvasdebugger/test/browser_canvas-frontend-open.js b/devtools/client/canvasdebugger/test/browser_canvas-frontend-open.js
new file mode 100644
index 000000000..59c4d4cfb
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-open.js
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the frontend UI is properly configured when opening the tool.
+ */
+
+function* ifTestingSupported() {
+ let { target, panel } = yield initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL);
+ let { $ } = panel.panelWin;
+
+ is($("#snapshots-pane").hasAttribute("hidden"), false,
+ "The snapshots pane should initially be visible.");
+ is($("#debugging-pane").hasAttribute("hidden"), false,
+ "The debugging pane should initially be visible.");
+
+ is($("#record-snapshot").getAttribute("hidden"), "true",
+ "The 'record snapshot' button should initially be hidden.");
+ is($("#import-snapshot").hasAttribute("hidden"), false,
+ "The 'import snapshot' button should initially be visible.");
+ is($("#clear-snapshots").hasAttribute("hidden"), false,
+ "The 'clear snapshots' button should initially be visible.");
+
+ is($("#reload-notice").hasAttribute("hidden"), false,
+ "The reload notice should initially be visible.");
+ is($("#empty-notice").getAttribute("hidden"), "true",
+ "The empty notice should initially be hidden.");
+ is($("#waiting-notice").getAttribute("hidden"), "true",
+ "The waiting notice should initially be hidden.");
+
+ is($("#screenshot-container").getAttribute("hidden"), "true",
+ "The screenshot container should initially be hidden.");
+ is($("#snapshot-filmstrip").getAttribute("hidden"), "true",
+ "The snapshot filmstrip should initially be hidden.");
+
+ is($("#debugging-pane-contents").getAttribute("hidden"), "true",
+ "The rest of the UI should initially be hidden.");
+
+ yield teardown(panel);
+ finish();
+}
diff --git a/devtools/client/canvasdebugger/test/browser_canvas-frontend-record-01.js b/devtools/client/canvasdebugger/test/browser_canvas-frontend-record-01.js
new file mode 100644
index 000000000..cd0358d3c
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-record-01.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests whether the frontend behaves correctly while reording a snapshot.
+ */
+
+function* ifTestingSupported() {
+ let { target, panel } = yield initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL);
+ let { window, EVENTS, $, SnapshotsListView } = panel.panelWin;
+
+ yield reload(target);
+
+ is($("#record-snapshot").hasAttribute("checked"), false,
+ "The 'record snapshot' button should initially be unchecked.");
+ is($("#record-snapshot").hasAttribute("disabled"), false,
+ "The 'record snapshot' button should initially be enabled.");
+ is($("#record-snapshot").hasAttribute("hidden"), false,
+ "The 'record snapshot' button should now be visible.");
+
+ is(SnapshotsListView.itemCount, 0,
+ "There should be no items available in the snapshots list view.");
+ is(SnapshotsListView.selectedIndex, -1,
+ "There should be no selected item in the snapshots list view.");
+
+ let recordingStarted = once(window, EVENTS.SNAPSHOT_RECORDING_STARTED);
+ let recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
+ SnapshotsListView._onRecordButtonClick();
+
+ yield recordingStarted;
+ ok(true, "Started recording a snapshot of the animation loop.");
+
+ is($("#record-snapshot").getAttribute("checked"), "true",
+ "The 'record snapshot' button should now be checked.");
+ is($("#record-snapshot").hasAttribute("hidden"), false,
+ "The 'record snapshot' button should still be visible.");
+
+ is(SnapshotsListView.itemCount, 1,
+ "There should be one item available in the snapshots list view now.");
+ is(SnapshotsListView.selectedIndex, -1,
+ "There should be no selected item in the snapshots list view yet.");
+
+ yield recordingFinished;
+ ok(true, "Finished recording a snapshot of the animation loop.");
+
+ is($("#record-snapshot").hasAttribute("checked"), false,
+ "The 'record snapshot' button should now be unchecked.");
+ is($("#record-snapshot").hasAttribute("disabled"), false,
+ "The 'record snapshot' button should now be re-enabled.");
+ is($("#record-snapshot").hasAttribute("hidden"), false,
+ "The 'record snapshot' button should still be visible.");
+
+ is(SnapshotsListView.itemCount, 1,
+ "There should still be only one item available in the snapshots list view.");
+ is(SnapshotsListView.selectedIndex, 0,
+ "There should be one selected item in the snapshots list view now.");
+
+ yield teardown(panel);
+ finish();
+}
diff --git a/devtools/client/canvasdebugger/test/browser_canvas-frontend-record-02.js b/devtools/client/canvasdebugger/test/browser_canvas-frontend-record-02.js
new file mode 100644
index 000000000..aee63a574
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-record-02.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests whether the frontend displays a placeholder snapshot while recording.
+ */
+
+function* ifTestingSupported() {
+ let { target, panel } = yield initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL);
+ let { window, EVENTS, L10N, $, SnapshotsListView } = panel.panelWin;
+
+ yield reload(target);
+
+ let recordingStarted = once(window, EVENTS.SNAPSHOT_RECORDING_STARTED);
+ let recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
+ let recordingSelected = once(window, EVENTS.SNAPSHOT_RECORDING_SELECTED);
+ SnapshotsListView._onRecordButtonClick();
+
+ yield recordingStarted;
+ ok(true, "Started recording a snapshot of the animation loop.");
+
+ let item = SnapshotsListView.getItemAtIndex(0);
+
+ is($(".snapshot-item-title", item.target).getAttribute("value"),
+ L10N.getFormatStr("snapshotsList.itemLabel", 1),
+ "The placeholder item's title label is correct.");
+
+ is($(".snapshot-item-calls", item.target).getAttribute("value"),
+ L10N.getStr("snapshotsList.loadingLabel"),
+ "The placeholder item's calls label is correct.");
+
+ is($(".snapshot-item-save", item.target).getAttribute("value"), "",
+ "The placeholder item's save label should not have a value yet.");
+
+ is($("#reload-notice").getAttribute("hidden"), "true",
+ "The reload notice should now be hidden.");
+ is($("#empty-notice").getAttribute("hidden"), "true",
+ "The empty notice should now be hidden.");
+ is($("#waiting-notice").hasAttribute("hidden"), false,
+ "The waiting notice should now be visible.");
+
+ is($("#screenshot-container").getAttribute("hidden"), "true",
+ "The screenshot container should still be hidden.");
+ is($("#snapshot-filmstrip").getAttribute("hidden"), "true",
+ "The snapshot filmstrip should still be hidden.");
+
+ is($("#debugging-pane-contents").getAttribute("hidden"), "true",
+ "The rest of the UI should still be hidden.");
+
+ yield recordingFinished;
+ ok(true, "Finished recording a snapshot of the animation loop.");
+
+ yield recordingSelected;
+ ok(true, "Finished selecting a snapshot of the animation loop.");
+
+ is($("#reload-notice").getAttribute("hidden"), "true",
+ "The reload notice should now be hidden.");
+ is($("#empty-notice").getAttribute("hidden"), "true",
+ "The empty notice should now be hidden.");
+ is($("#waiting-notice").getAttribute("hidden"), "true",
+ "The waiting notice should now be hidden.");
+
+ is($("#screenshot-container").hasAttribute("hidden"), false,
+ "The screenshot container should now be visible.");
+ is($("#snapshot-filmstrip").hasAttribute("hidden"), false,
+ "The snapshot filmstrip should now be visible.");
+
+ is($("#debugging-pane-contents").hasAttribute("hidden"), false,
+ "The rest of the UI should now be visible.");
+
+ yield teardown(panel);
+ finish();
+}
diff --git a/devtools/client/canvasdebugger/test/browser_canvas-frontend-record-03.js b/devtools/client/canvasdebugger/test/browser_canvas-frontend-record-03.js
new file mode 100644
index 000000000..c3638610e
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-record-03.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests whether the frontend displays the correct info for a snapshot
+ * after finishing recording.
+ */
+
+function* ifTestingSupported() {
+ let { target, panel } = yield initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL);
+ let { window, EVENTS, $, SnapshotsListView } = panel.panelWin;
+
+ yield reload(target);
+
+ let recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
+ SnapshotsListView._onRecordButtonClick();
+
+ yield recordingFinished;
+ ok(true, "Finished recording a snapshot of the animation loop.");
+
+ let item = SnapshotsListView.getItemAtIndex(0);
+
+ is(SnapshotsListView.selectedItem, item,
+ "The first item should now be selected in the snapshots list view (1).");
+ is(SnapshotsListView.selectedIndex, 0,
+ "The first item should now be selected in the snapshots list view (2).");
+
+ is($(".snapshot-item-calls", item.target).getAttribute("value"), "4 draws, 8 calls",
+ "The placeholder item's calls label is correct.");
+ is($(".snapshot-item-save", item.target).getAttribute("value"), "Save",
+ "The placeholder item's save label is correct.");
+ is($(".snapshot-item-save", item.target).getAttribute("disabled"), "false",
+ "The placeholder item's save label should be clickable.");
+
+ yield teardown(panel);
+ finish();
+}
diff --git a/devtools/client/canvasdebugger/test/browser_canvas-frontend-record-04.js b/devtools/client/canvasdebugger/test/browser_canvas-frontend-record-04.js
new file mode 100644
index 000000000..fde8501e6
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-record-04.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Bug 1122766
+ * Tests that the canvas actor correctly returns from recordAnimationFrame
+ * in the scenario where a loop starts with rAF and has rAF in the beginning
+ * of its loop, when the recording starts before the rAFs start.
+ */
+
+function* ifTestingSupported() {
+ let { target, panel } = yield initCanvasDebuggerFrontend(RAF_BEGIN_URL);
+ let { window, EVENTS, gFront, SnapshotsListView } = panel.panelWin;
+ loadFrameScripts();
+
+ yield reload(target);
+
+ let recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
+ SnapshotsListView._onRecordButtonClick();
+
+ // Wait until after the recording started to trigger the content.
+ // Use the gFront method rather than the SNAPSHOT_RECORDING_STARTED event
+ // which triggers before the underlying actor call
+ yield waitUntil(function* () { return !(yield gFront.isRecording()); });
+
+ // Start animation in content
+ evalInDebuggee("start();");
+
+ yield recordingFinished;
+ ok(true, "Finished recording a snapshot of the animation loop.");
+
+ yield removeTab(target.tab);
+ finish();
+}
diff --git a/devtools/client/canvasdebugger/test/browser_canvas-frontend-reload-01.js b/devtools/client/canvasdebugger/test/browser_canvas-frontend-reload-01.js
new file mode 100644
index 000000000..cf353aa27
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-reload-01.js
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the frontend UI is properly reconfigured after reloading.
+ */
+
+function* ifTestingSupported() {
+ let { target, panel } = yield initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL);
+ let { window, $, EVENTS } = panel.panelWin;
+
+ let reset = once(window, EVENTS.UI_RESET);
+ let navigated = reload(target);
+
+ yield reset;
+ ok(true, "The UI was reset after the refresh button was clicked.");
+
+ yield navigated;
+ ok(true, "The target finished reloading.");
+
+ is($("#snapshots-pane").hasAttribute("hidden"), false,
+ "The snapshots pane should still be visible.");
+ is($("#debugging-pane").hasAttribute("hidden"), false,
+ "The debugging pane should still be visible.");
+
+ is($("#record-snapshot").hasAttribute("checked"), false,
+ "The 'record snapshot' button should not be checked.");
+ is($("#record-snapshot").hasAttribute("disabled"), false,
+ "The 'record snapshot' button should not be disabled.");
+
+ is($("#record-snapshot").hasAttribute("hidden"), false,
+ "The 'record snapshot' button should now be visible.");
+ is($("#import-snapshot").hasAttribute("hidden"), false,
+ "The 'import snapshot' button should still be visible.");
+ is($("#clear-snapshots").hasAttribute("hidden"), false,
+ "The 'clear snapshots' button should still be visible.");
+
+ is($("#reload-notice").getAttribute("hidden"), "true",
+ "The reload notice should now be hidden.");
+ is($("#empty-notice").hasAttribute("hidden"), false,
+ "The empty notice should now be visible.");
+ is($("#waiting-notice").getAttribute("hidden"), "true",
+ "The waiting notice should now be hidden.");
+
+ is($("#snapshot-filmstrip").getAttribute("hidden"), "true",
+ "The snapshot filmstrip should still be hidden.");
+ is($("#screenshot-container").getAttribute("hidden"), "true",
+ "The screenshot container should still be hidden.");
+
+ is($("#debugging-pane-contents").getAttribute("hidden"), "true",
+ "The rest of the UI should still be hidden.");
+
+ yield teardown(panel);
+ finish();
+}
diff --git a/devtools/client/canvasdebugger/test/browser_canvas-frontend-reload-02.js b/devtools/client/canvasdebugger/test/browser_canvas-frontend-reload-02.js
new file mode 100644
index 000000000..2747fd13f
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-reload-02.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the frontend UI is properly reconfigured after reloading.
+ */
+
+function* ifTestingSupported() {
+ let { target, panel } = yield initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL);
+ let { window, $, $all, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin;
+
+ is(SnapshotsListView.itemCount, 0,
+ "There should be no snapshots initially displayed in the UI.");
+ is(CallsListView.itemCount, 0,
+ "There should be no function calls initially displayed in the UI.");
+
+ is($("#screenshot-container").hidden, true,
+ "The screenshot should not be initially displayed in the UI.");
+ is($("#snapshot-filmstrip").hidden, true,
+ "There should be no thumbnails initially displayed in the UI (1).");
+ is($all(".filmstrip-thumbnail").length, 0,
+ "There should be no thumbnails initially displayed in the UI (2).");
+
+ yield reload(target);
+
+ let recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
+ let callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED);
+ let thumbnailsDisplayed = once(window, EVENTS.THUMBNAILS_DISPLAYED);
+ let screenshotDisplayed = once(window, EVENTS.CALL_SCREENSHOT_DISPLAYED);
+ SnapshotsListView._onRecordButtonClick();
+ yield promise.all([
+ recordingFinished,
+ callListPopulated,
+ thumbnailsDisplayed,
+ screenshotDisplayed
+ ]);
+
+ is(SnapshotsListView.itemCount, 1,
+ "There should be one snapshot displayed in the UI.");
+ is(CallsListView.itemCount, 8,
+ "All the function calls should now be displayed in the UI.");
+
+ is($("#screenshot-container").hidden, false,
+ "The screenshot should now be displayed in the UI.");
+ is($("#snapshot-filmstrip").hidden, false,
+ "All the thumbnails should now be displayed in the UI (1).");
+ is($all(".filmstrip-thumbnail").length, 4,
+ "All the thumbnails should now be displayed in the UI (2).");
+
+ let reset = once(window, EVENTS.UI_RESET);
+ let navigated = reload(target);
+
+ yield reset;
+ ok(true, "The UI was reset after the refresh button was clicked.");
+
+ is(SnapshotsListView.itemCount, 0,
+ "There should be no snapshots displayed in the UI after navigating.");
+ is(CallsListView.itemCount, 0,
+ "There should be no function calls displayed in the UI after navigating.");
+ is($("#snapshot-filmstrip").hidden, true,
+ "There should be no thumbnails displayed in the UI after navigating.");
+ is($("#screenshot-container").hidden, true,
+ "The screenshot should not be displayed in the UI after navigating.");
+
+ yield navigated;
+ ok(true, "The target finished reloading.");
+
+ yield teardown(panel);
+ finish();
+}
diff --git a/devtools/client/canvasdebugger/test/browser_canvas-frontend-slider-01.js b/devtools/client/canvasdebugger/test/browser_canvas-frontend-slider-01.js
new file mode 100644
index 000000000..cdce00bd1
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-slider-01.js
@@ -0,0 +1,39 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the slider in the calls list view works as advertised.
+ */
+
+function* ifTestingSupported() {
+ let { target, panel } = yield initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL);
+ let { window, $, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin;
+
+ yield reload(target);
+
+ let recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
+ let callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED);
+ SnapshotsListView._onRecordButtonClick();
+ yield promise.all([recordingFinished, callListPopulated]);
+
+ is(CallsListView.selectedIndex, -1,
+ "No item in the function calls list should be initially selected.");
+
+ is($("#calls-slider").value, 0,
+ "The slider should be moved all the way to the start.");
+ is($("#calls-slider").min, 0,
+ "The slider minimum value should be 0.");
+ is($("#calls-slider").max, 7,
+ "The slider maximum value should be 7.");
+
+ CallsListView.selectedIndex = 1;
+ is($("#calls-slider").value, 1,
+ "The slider should be changed according to the current selection.");
+
+ $("#calls-slider").value = 2;
+ is(CallsListView.selectedIndex, 2,
+ "The calls selection should be changed according to the current slider value.");
+
+ yield teardown(panel);
+ finish();
+}
diff --git a/devtools/client/canvasdebugger/test/browser_canvas-frontend-slider-02.js b/devtools/client/canvasdebugger/test/browser_canvas-frontend-slider-02.js
new file mode 100644
index 000000000..5074ab206
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-slider-02.js
@@ -0,0 +1,97 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the slider in the calls list view works as advertised.
+ */
+
+function* ifTestingSupported() {
+ let { target, panel } = yield initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL);
+ let { window, $, EVENTS, gFront, SnapshotsListView, CallsListView } = panel.panelWin;
+
+ yield reload(target);
+
+ let recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
+ let callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED);
+ let thumbnailsDisplayed = once(window, EVENTS.THUMBNAILS_DISPLAYED);
+ SnapshotsListView._onRecordButtonClick();
+ yield promise.all([recordingFinished, callListPopulated, thumbnailsDisplayed]);
+
+ let firstSnapshot = SnapshotsListView.getItemAtIndex(0);
+ let firstSnapshotOverview = yield firstSnapshot.attachment.actor.getOverview();
+
+ let thumbnails = firstSnapshotOverview.thumbnails;
+ is(thumbnails.length, 4,
+ "There should be 4 thumbnails cached for the snapshot item.");
+
+ let thumbnailImageElementSet = waitForMozSetImageElement(window);
+ $("#calls-slider").value = 1;
+ let thumbnailPixels = yield thumbnailImageElementSet;
+
+ ok(sameArray(thumbnailPixels, thumbnails[0].pixels),
+ "The screenshot element should have a thumbnail as an immediate background.");
+
+ yield once(window, EVENTS.CALL_SCREENSHOT_DISPLAYED);
+ ok(true, "The full-sized screenshot was displayed for the item at index 1.");
+
+ thumbnailImageElementSet = waitForMozSetImageElement(window);
+ $("#calls-slider").value = 2;
+ thumbnailPixels = yield thumbnailImageElementSet;
+
+ ok(sameArray(thumbnailPixels, thumbnails[1].pixels),
+ "The screenshot element should have a thumbnail as an immediate background.");
+
+ yield once(window, EVENTS.CALL_SCREENSHOT_DISPLAYED);
+ ok(true, "The full-sized screenshot was displayed for the item at index 2.");
+
+ thumbnailImageElementSet = waitForMozSetImageElement(window);
+ $("#calls-slider").value = 7;
+ thumbnailPixels = yield thumbnailImageElementSet;
+
+ ok(sameArray(thumbnailPixels, thumbnails[3].pixels),
+ "The screenshot element should have a thumbnail as an immediate background.");
+
+ yield once(window, EVENTS.CALL_SCREENSHOT_DISPLAYED);
+ ok(true, "The full-sized screenshot was displayed for the item at index 7.");
+
+ thumbnailImageElementSet = waitForMozSetImageElement(window);
+ $("#calls-slider").value = 4;
+ thumbnailPixels = yield thumbnailImageElementSet;
+
+ ok(sameArray(thumbnailPixels, thumbnails[2].pixels),
+ "The screenshot element should have a thumbnail as an immediate background.");
+
+ yield once(window, EVENTS.CALL_SCREENSHOT_DISPLAYED);
+ ok(true, "The full-sized screenshot was displayed for the item at index 4.");
+
+ thumbnailImageElementSet = waitForMozSetImageElement(window);
+ $("#calls-slider").value = 0;
+ thumbnailPixels = yield thumbnailImageElementSet;
+
+ ok(sameArray(thumbnailPixels, thumbnails[0].pixels),
+ "The screenshot element should have a thumbnail as an immediate background.");
+
+ yield once(window, EVENTS.CALL_SCREENSHOT_DISPLAYED);
+ ok(true, "The full-sized screenshot was displayed for the item at index 0.");
+
+ yield teardown(panel);
+ finish();
+}
+
+function waitForMozSetImageElement(panel) {
+ let deferred = promise.defer();
+ panel._onMozSetImageElement = deferred.resolve;
+ return deferred.promise;
+}
+
+function sameArray(a, b) {
+ if (a.length != b.length) {
+ return false;
+ }
+ for (let i = 0; i < a.length; i++) {
+ if (a[i] !== b[i]) {
+ return false;
+ }
+ }
+ return true;
+}
diff --git a/devtools/client/canvasdebugger/test/browser_canvas-frontend-snapshot-select-01.js b/devtools/client/canvasdebugger/test/browser_canvas-frontend-snapshot-select-01.js
new file mode 100644
index 000000000..4dc275282
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-snapshot-select-01.js
@@ -0,0 +1,93 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if selecting snapshots in the frontend displays the appropriate data
+ * respective to their recorded animation frame.
+ */
+
+function* ifTestingSupported() {
+ let { target, panel } = yield initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL);
+ let { window, $, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin;
+
+ yield reload(target);
+
+ yield recordAndWaitForFirstSnapshot();
+ info("First snapshot recorded.");
+
+ is(SnapshotsListView.selectedIndex, 0,
+ "A snapshot should be automatically selected after first recording.");
+ is(CallsListView.selectedIndex, -1,
+ "There should be no call item automatically selected in the snapshot.");
+
+ yield recordAndWaitForAnotherSnapshot();
+ info("Second snapshot recorded.");
+
+ is(SnapshotsListView.selectedIndex, 0,
+ "A snapshot should not be automatically selected after another recording.");
+ is(CallsListView.selectedIndex, -1,
+ "There should still be no call item automatically selected in the snapshot.");
+
+ let secondSnapshotTarget = SnapshotsListView.getItemAtIndex(1).target;
+ let snapshotSelected = waitForSnapshotSelection();
+ EventUtils.sendMouseEvent({ type: "mousedown" }, secondSnapshotTarget, window);
+
+ yield snapshotSelected;
+ info("Second snapshot selected.");
+
+ is(SnapshotsListView.selectedIndex, 1,
+ "The second snapshot should now be selected.");
+ is(CallsListView.selectedIndex, -1,
+ "There should still be no call item automatically selected in the snapshot.");
+
+ let firstDrawCallContents = $(".call-item-contents", CallsListView.getItemAtIndex(2).target);
+ let screenshotDisplayed = once(window, EVENTS.CALL_SCREENSHOT_DISPLAYED);
+ EventUtils.sendMouseEvent({ type: "mousedown" }, firstDrawCallContents, window);
+
+ yield screenshotDisplayed;
+ info("First draw call in the second snapshot selected.");
+
+ is(SnapshotsListView.selectedIndex, 1,
+ "The second snapshot should still be selected.");
+ is(CallsListView.selectedIndex, 2,
+ "The first draw call should now be selected in the snapshot.");
+
+ let firstSnapshotTarget = SnapshotsListView.getItemAtIndex(0).target;
+ snapshotSelected = waitForSnapshotSelection();
+ EventUtils.sendMouseEvent({ type: "mousedown" }, firstSnapshotTarget, window);
+
+ yield snapshotSelected;
+ info("First snapshot re-selected.");
+
+ is(SnapshotsListView.selectedIndex, 0,
+ "The first snapshot should now be re-selected.");
+ is(CallsListView.selectedIndex, -1,
+ "There should still be no call item automatically selected in the snapshot.");
+
+ function recordAndWaitForFirstSnapshot() {
+ let recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
+ let snapshotSelected = waitForSnapshotSelection();
+ SnapshotsListView._onRecordButtonClick();
+ return promise.all([recordingFinished, snapshotSelected]);
+ }
+
+ function recordAndWaitForAnotherSnapshot() {
+ let recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
+ SnapshotsListView._onRecordButtonClick();
+ return recordingFinished;
+ }
+
+ function waitForSnapshotSelection() {
+ let callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED);
+ let thumbnailsDisplayed = once(window, EVENTS.THUMBNAILS_DISPLAYED);
+ let screenshotDisplayed = once(window, EVENTS.CALL_SCREENSHOT_DISPLAYED);
+ return promise.all([
+ callListPopulated,
+ thumbnailsDisplayed,
+ screenshotDisplayed
+ ]);
+ }
+
+ yield teardown(panel);
+ finish();
+}
diff --git a/devtools/client/canvasdebugger/test/browser_canvas-frontend-snapshot-select-02.js b/devtools/client/canvasdebugger/test/browser_canvas-frontend-snapshot-select-02.js
new file mode 100644
index 000000000..27a03fb51
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-snapshot-select-02.js
@@ -0,0 +1,30 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if selecting snapshots in the frontend displays the appropriate data
+ * respective to their recorded animation frame.
+ */
+
+function* ifTestingSupported() {
+ let { target, panel } = yield initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL);
+ let { window, $, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin;
+
+ yield reload(target);
+
+ SnapshotsListView._onRecordButtonClick();
+ let snapshotTarget = SnapshotsListView.getItemAtIndex(0).target;
+
+ EventUtils.sendMouseEvent({ type: "mousedown" }, snapshotTarget, window);
+ EventUtils.sendMouseEvent({ type: "mousedown" }, snapshotTarget, window);
+ EventUtils.sendMouseEvent({ type: "mousedown" }, snapshotTarget, window);
+
+ ok(true, "clicking in-progress snapshot does not fail");
+
+ let finished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
+ SnapshotsListView._onRecordButtonClick();
+ yield finished;
+
+ yield teardown(panel);
+ finish();
+}
diff --git a/devtools/client/canvasdebugger/test/browser_canvas-frontend-stepping.js b/devtools/client/canvasdebugger/test/browser_canvas-frontend-stepping.js
new file mode 100644
index 000000000..d76449b91
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-stepping.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the stepping buttons in the call list toolbar work as advertised.
+ */
+
+function* ifTestingSupported() {
+ let { target, panel } = yield initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL);
+ let { window, $, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin;
+
+ yield reload(target);
+
+ let recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
+ let callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED);
+ SnapshotsListView._onRecordButtonClick();
+ yield promise.all([recordingFinished, callListPopulated]);
+
+ checkSteppingButtons(1, 1, 1, 1);
+ is(CallsListView.selectedIndex, -1,
+ "There should be no selected item in the calls list view initially.");
+
+ CallsListView._onResume();
+ checkSteppingButtons(1, 1, 1, 1);
+ is(CallsListView.selectedIndex, 0,
+ "The first draw call should now be selected.");
+
+ CallsListView._onResume();
+ checkSteppingButtons(1, 1, 1, 1);
+ is(CallsListView.selectedIndex, 2,
+ "The second draw call should now be selected.");
+
+ CallsListView._onStepOver();
+ checkSteppingButtons(1, 1, 1, 1);
+ is(CallsListView.selectedIndex, 3,
+ "The next context call should now be selected.");
+
+ CallsListView._onStepOut();
+ checkSteppingButtons(0, 0, 1, 0);
+ is(CallsListView.selectedIndex, 7,
+ "The last context call should now be selected.");
+
+ function checkSteppingButtons(resume, stepOver, stepIn, stepOut) {
+ if (!resume) {
+ is($("#resume").getAttribute("disabled"), "true",
+ "The resume button doesn't have the expected disabled state.");
+ } else {
+ is($("#resume").hasAttribute("disabled"), false,
+ "The resume button doesn't have the expected enabled state.");
+ }
+ if (!stepOver) {
+ is($("#step-over").getAttribute("disabled"), "true",
+ "The stepOver button doesn't have the expected disabled state.");
+ } else {
+ is($("#step-over").hasAttribute("disabled"), false,
+ "The stepOver button doesn't have the expected enabled state.");
+ }
+ if (!stepIn) {
+ is($("#step-in").getAttribute("disabled"), "true",
+ "The stepIn button doesn't have the expected disabled state.");
+ } else {
+ is($("#step-in").hasAttribute("disabled"), false,
+ "The stepIn button doesn't have the expected enabled state.");
+ }
+ if (!stepOut) {
+ is($("#step-out").getAttribute("disabled"), "true",
+ "The stepOut button doesn't have the expected disabled state.");
+ } else {
+ is($("#step-out").hasAttribute("disabled"), false,
+ "The stepOut button doesn't have the expected enabled state.");
+ }
+ }
+
+ yield teardown(panel);
+ finish();
+}
diff --git a/devtools/client/canvasdebugger/test/browser_canvas-frontend-stop-01.js b/devtools/client/canvasdebugger/test/browser_canvas-frontend-stop-01.js
new file mode 100644
index 000000000..3a74e4b44
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-stop-01.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that you can stop a recording that does not have a rAF cycle.
+ */
+
+function* ifTestingSupported() {
+ let { target, panel } = yield initCanvasDebuggerFrontend(NO_CANVAS_URL);
+ let { window, EVENTS, $, SnapshotsListView } = panel.panelWin;
+
+ yield reload(target);
+
+ let recordingStarted = once(window, EVENTS.SNAPSHOT_RECORDING_STARTED);
+ SnapshotsListView._onRecordButtonClick();
+
+ yield recordingStarted;
+
+ is($("#empty-notice").hidden, true, "Empty notice not shown");
+ is($("#waiting-notice").hidden, false, "Waiting notice shown");
+
+ let recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
+ let recordingCancelled = once(window, EVENTS.SNAPSHOT_RECORDING_CANCELLED);
+ SnapshotsListView._onRecordButtonClick();
+
+ yield promise.all([recordingFinished, recordingCancelled]);
+
+ ok(true, "Recording stopped and was considered failed.");
+
+ is(SnapshotsListView.itemCount, 0, "No snapshots in the list.");
+ is($("#empty-notice").hidden, false, "Empty notice shown");
+ is($("#waiting-notice").hidden, true, "Waiting notice not shown");
+
+ yield teardown(panel);
+ finish();
+}
diff --git a/devtools/client/canvasdebugger/test/browser_canvas-frontend-stop-02.js b/devtools/client/canvasdebugger/test/browser_canvas-frontend-stop-02.js
new file mode 100644
index 000000000..b062fbc5e
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-stop-02.js
@@ -0,0 +1,35 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that a recording that does not have a rAF cycle fails after timeout.
+ */
+
+function* ifTestingSupported() {
+ let { target, panel } = yield initCanvasDebuggerFrontend(NO_CANVAS_URL);
+ let { window, EVENTS, $, SnapshotsListView } = panel.panelWin;
+
+ yield reload(target);
+
+ let recordingStarted = once(window, EVENTS.SNAPSHOT_RECORDING_STARTED);
+ SnapshotsListView._onRecordButtonClick();
+
+ yield recordingStarted;
+
+ is($("#empty-notice").hidden, true, "Empty notice not shown");
+ is($("#waiting-notice").hidden, false, "Waiting notice shown");
+
+ let recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
+ let recordingCancelled = once(window, EVENTS.SNAPSHOT_RECORDING_CANCELLED);
+
+ yield promise.all([recordingFinished, recordingCancelled]);
+
+ ok(true, "Recording stopped and was considered failed.");
+
+ is(SnapshotsListView.itemCount, 0, "No snapshots in the list.");
+ is($("#empty-notice").hidden, false, "Empty notice shown");
+ is($("#waiting-notice").hidden, true, "Waiting notice not shown");
+
+ yield teardown(panel);
+ finish();
+}
diff --git a/devtools/client/canvasdebugger/test/browser_canvas-frontend-stop-03.js b/devtools/client/canvasdebugger/test/browser_canvas-frontend-stop-03.js
new file mode 100644
index 000000000..70948311d
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-stop-03.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that a recording that has a rAF cycle, but no draw calls, fails
+ * after timeout.
+ */
+
+function* ifTestingSupported() {
+ let { target, panel } = yield initCanvasDebuggerFrontend(RAF_NO_CANVAS_URL);
+ let { window, EVENTS, $, SnapshotsListView } = panel.panelWin;
+
+ yield reload(target);
+
+ let recordingStarted = once(window, EVENTS.SNAPSHOT_RECORDING_STARTED);
+ SnapshotsListView._onRecordButtonClick();
+
+ yield recordingStarted;
+
+ is($("#empty-notice").hidden, true, "Empty notice not shown");
+ is($("#waiting-notice").hidden, false, "Waiting notice shown");
+
+ let recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
+ let recordingCancelled = once(window, EVENTS.SNAPSHOT_RECORDING_CANCELLED);
+
+ yield promise.all([recordingFinished, recordingCancelled]);
+
+ ok(true, "Recording stopped and was considered failed.");
+
+ is(SnapshotsListView.itemCount, 0, "No snapshots in the list.");
+ is($("#empty-notice").hidden, false, "Empty notice shown");
+ is($("#waiting-notice").hidden, true, "Waiting notice not shown");
+
+ yield teardown(panel);
+ finish();
+}
diff --git a/devtools/client/canvasdebugger/test/browser_profiling-canvas.js b/devtools/client/canvasdebugger/test/browser_profiling-canvas.js
new file mode 100644
index 000000000..ede8a4dbf
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_profiling-canvas.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if functions inside a single animation frame are recorded and stored
+ * for a canvas context profiling.
+ */
+
+function* ifTestingSupported() {
+ let currentTime = window.performance.now();
+ let { target, front } = yield initCanvasDebuggerBackend(SIMPLE_CANVAS_URL);
+
+ let navigated = once(target, "navigate");
+
+ yield front.setup({ reload: true });
+ ok(true, "The front was setup up successfully.");
+
+ yield navigated;
+ ok(true, "Target automatically navigated when the front was set up.");
+
+ let snapshotActor = yield front.recordAnimationFrame();
+ ok(snapshotActor,
+ "A snapshot actor was sent after recording.");
+
+ let animationOverview = yield snapshotActor.getOverview();
+ ok(animationOverview,
+ "An animation overview could be retrieved after recording.");
+
+ let functionCalls = animationOverview.calls;
+ ok(functionCalls,
+ "An array of function call actors was sent after recording.");
+ is(functionCalls.length, 8,
+ "The number of function call actors is correct.");
+
+ info("Check the timestamps of function calls");
+
+ for (let i = 0; i < functionCalls.length - 1; i += 2) {
+ ok(functionCalls[i].timestamp > 0, "The timestamp of the called function is larger than 0.");
+ ok(functionCalls[i].timestamp < currentTime, "The timestamp has been minus the frame start time.");
+ ok(functionCalls[i + 1].timestamp > functionCalls[i].timestamp, "The timestamp of the called function is correct.");
+ }
+
+ yield removeTab(target.tab);
+ finish();
+}
diff --git a/devtools/client/canvasdebugger/test/browser_profiling-webgl.js b/devtools/client/canvasdebugger/test/browser_profiling-webgl.js
new file mode 100644
index 000000000..83009317f
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_profiling-webgl.js
@@ -0,0 +1,91 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if functions inside a single animation frame are recorded and stored
+ * for a canvas context profiling.
+ */
+
+function* ifTestingSupported() {
+ let currentTime = window.performance.now();
+ info("Start to estimate WebGL drawArrays function.");
+ var { target, front } = yield initCanvasDebuggerBackend(WEBGL_DRAW_ARRAYS);
+
+ let navigated = once(target, "navigate");
+
+ yield front.setup({ reload: true });
+ ok(true, "The front was setup up successfully.");
+
+ yield navigated;
+ ok(true, "Target automatically navigated when the front was set up.");
+
+ let snapshotActor = yield front.recordAnimationFrame();
+ ok(snapshotActor,
+ "A snapshot actor was sent after recording.");
+
+ let animationOverview = yield snapshotActor.getOverview();
+ ok(animationOverview,
+ "An animation overview could be retrieved after recording.");
+
+ let functionCalls = animationOverview.calls;
+ ok(functionCalls,
+ "An array of function call actors was sent after recording.");
+
+ testFunctionCallTimestamp(functionCalls, currentTime);
+
+ info("Check triangle and vertex counts in drawArrays()");
+ is(animationOverview.primitive.tris, 5, "The count of triangles is correct.");
+ is(animationOverview.primitive.vertices, 26, "The count of vertices is correct.");
+ is(animationOverview.primitive.points, 4, "The count of points is correct.");
+ is(animationOverview.primitive.lines, 8, "The count of lines is correct.");
+
+ yield removeTab(target.tab);
+
+ info("Start to estimate WebGL drawElements function.");
+ var { target, front } = yield initCanvasDebuggerBackend(WEBGL_DRAW_ELEMENTS);
+
+ navigated = once(target, "navigate");
+
+ yield front.setup({ reload: true });
+ ok(true, "The front was setup up successfully.");
+
+ yield navigated;
+ ok(true, "Target automatically navigated when the front was set up.");
+
+ snapshotActor = yield front.recordAnimationFrame();
+ ok(snapshotActor,
+ "A snapshot actor was sent after recording.");
+
+ animationOverview = yield snapshotActor.getOverview();
+ ok(animationOverview,
+ "An animation overview could be retrieved after recording.");
+
+ functionCalls = animationOverview.calls;
+ ok(functionCalls,
+ "An array of function call actors was sent after recording.");
+
+ testFunctionCallTimestamp(functionCalls, currentTime);
+
+ info("Check triangle and vertex counts in drawElements()");
+ is(animationOverview.primitive.tris, 5, "The count of triangles is correct.");
+ is(animationOverview.primitive.vertices, 26, "The count of vertices is correct.");
+ is(animationOverview.primitive.points, 4, "The count of points is correct.");
+ is(animationOverview.primitive.lines, 8, "The count of lines is correct.");
+
+ yield removeTab(target.tab);
+ finish();
+}
+
+function testFunctionCallTimestamp(functionCalls, currentTime) {
+
+ info("Check the timestamps of function calls");
+
+ for ( let i = 0; i < functionCalls.length-1; i += 2 ) {
+ ok( functionCalls[i].timestamp > 0, "The timestamp of the called function is larger than 0." );
+ ok( functionCalls[i].timestamp < currentTime, "The timestamp has been minus the frame start time." );
+ ok( functionCalls[i+1].timestamp > functionCalls[i].timestamp, "The timestamp of the called function is correct." );
+ }
+
+ yield removeTab(target.tab);
+ finish();
+}
diff --git a/devtools/client/canvasdebugger/test/doc_no-canvas.html b/devtools/client/canvasdebugger/test/doc_no-canvas.html
new file mode 100644
index 000000000..a5934e3e7
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/doc_no-canvas.html
@@ -0,0 +1,14 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Canvas inspector test page</title>
+ </head>
+
+ <body>
+ </body>
+
+</html>
diff --git a/devtools/client/canvasdebugger/test/doc_raf-begin.html b/devtools/client/canvasdebugger/test/doc_raf-begin.html
new file mode 100644
index 000000000..8727f8306
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/doc_raf-begin.html
@@ -0,0 +1,36 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Canvas inspector test page</title>
+ </head>
+
+ <body>
+ <canvas width="128" height="128"></canvas>
+
+ <script type="text/javascript;version=1.8">
+ "use strict";
+
+ var ctx = document.querySelector("canvas").getContext("2d");
+
+ function drawRect(fill, size) {
+ ctx.fillStyle = fill;
+ ctx.fillRect(size[0], size[1], size[2], size[3]);
+ }
+
+ function drawScene() {
+ window.requestAnimationFrame(drawScene);
+ ctx.clearRect(0, 0, 128, 128);
+ drawRect("rgb(192, 192, 192)", [0, 0, 128, 128]);
+ drawRect("rgba(0, 0, 192, 0.5)", [30, 30, 55, 50]);
+ drawRect("rgba(192, 0, 0, 0.5)", [10, 10, 55, 50]);
+ }
+
+ function start () { window.requestAnimationFrame(drawScene); }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/canvasdebugger/test/doc_raf-no-canvas.html b/devtools/client/canvasdebugger/test/doc_raf-no-canvas.html
new file mode 100644
index 000000000..fa937623c
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/doc_raf-no-canvas.html
@@ -0,0 +1,18 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Canvas inspector test page</title>
+ </head>
+
+ <body>
+ <script>
+ function render () { window.requestAnimationFrame(render); }
+ window.requestAnimationFrame(render);
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/canvasdebugger/test/doc_settimeout.html b/devtools/client/canvasdebugger/test/doc_settimeout.html
new file mode 100644
index 000000000..57cfbdab0
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/doc_settimeout.html
@@ -0,0 +1,37 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Canvas inspector test page</title>
+ </head>
+
+ <body>
+ <canvas width="128" height="128"></canvas>
+
+ <script type="text/javascript;version=1.8">
+ "use strict";
+
+ var ctx = document.querySelector("canvas").getContext("2d");
+
+ function drawRect(fill, size) {
+ ctx.fillStyle = fill;
+ ctx.fillRect(size[0], size[1], size[2], size[3]);
+ }
+
+ function drawScene() {
+ ctx.clearRect(0, 0, 128, 128);
+ drawRect("rgb(192, 192, 192)", [0, 0, 128, 128]);
+ drawRect("rgba(0, 0, 192, 0.5)", [30, 30, 55, 50]);
+ drawRect("rgba(192, 0, 0, 0.5)", [10, 10, 55, 50]);
+
+ window.setTimeout(drawScene, 50);
+ }
+
+ drawScene();
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/canvasdebugger/test/doc_simple-canvas-bitmasks.html b/devtools/client/canvasdebugger/test/doc_simple-canvas-bitmasks.html
new file mode 100644
index 000000000..bd5f67a6a
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/doc_simple-canvas-bitmasks.html
@@ -0,0 +1,34 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Canvas inspector test page</title>
+ </head>
+
+ <body>
+ <canvas width="128" height="128"></canvas>
+
+ <script type="text/javascript;version=1.8">
+ "use strict";
+
+ var ctx = document.querySelector("canvas").getContext("2d");
+
+ function drawRect(fill, size) {
+ ctx.fillStyle = fill;
+ ctx.fillRect(size[0], size[1], size[2], size[3]);
+ }
+
+ function drawScene() {
+ ctx.clearRect(0, 0, 4, 4);
+ drawRect("rgb(192, 192, 192)", [0, 0, 1, 1]);
+ window.requestAnimationFrame(drawScene);
+ }
+
+ drawScene();
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/canvasdebugger/test/doc_simple-canvas-deep-stack.html b/devtools/client/canvasdebugger/test/doc_simple-canvas-deep-stack.html
new file mode 100644
index 000000000..f5ecc45d6
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/doc_simple-canvas-deep-stack.html
@@ -0,0 +1,46 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Canvas inspector test page</title>
+ </head>
+
+ <body>
+ <canvas width="128" height="128"></canvas>
+
+ <script type="text/javascript;version=1.8">
+ "use strict";
+
+ var ctx = document.querySelector("canvas").getContext("2d");
+
+ function drawRect(fill, size) {
+ function A() {
+ function B() {
+ function C() {
+ ctx.fillStyle = fill;
+ ctx.fillRect(size[0], size[1], size[2], size[3]);
+ }
+ C();
+ }
+ B();
+ }
+ A();
+ }
+
+ function drawScene() {
+ ctx.clearRect(0, 0, 128, 128);
+ drawRect("rgb(192, 192, 192)", [0, 0, 128, 128]);
+ drawRect("rgba(0, 0, 192, 0.5)", [30, 30, 55, 50]);
+ drawRect("rgba(192, 0, 0, 0.5)", [10, 10, 55, 50]);
+
+ window.requestAnimationFrame(drawScene);
+ }
+
+ drawScene();
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/canvasdebugger/test/doc_simple-canvas-transparent.html b/devtools/client/canvasdebugger/test/doc_simple-canvas-transparent.html
new file mode 100644
index 000000000..f8daf1e24
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/doc_simple-canvas-transparent.html
@@ -0,0 +1,37 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Canvas inspector test page</title>
+ </head>
+
+ <body>
+ <canvas width="128" height="128"></canvas>
+
+ <script type="text/javascript;version=1.8">
+ "use strict";
+
+ var ctx = document.querySelector("canvas").getContext("2d");
+
+ function drawRect(fill, size) {
+ ctx.fillStyle = fill;
+ ctx.fillRect(size[0], size[1], size[2], size[3]);
+ }
+
+ function drawScene() {
+ ctx.clearRect(0, 0, 128, 128);
+ drawRect("rgba(255, 255, 255, 0)", [0, 0, 128, 128]);
+ drawRect("rgba(0, 0, 192, 0.5)", [30, 30, 55, 50]);
+ drawRect("rgba(192, 0, 0, 0.5)", [10, 10, 55, 50]);
+
+ window.requestAnimationFrame(drawScene);
+ }
+
+ drawScene();
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/canvasdebugger/test/doc_simple-canvas.html b/devtools/client/canvasdebugger/test/doc_simple-canvas.html
new file mode 100644
index 000000000..4fe6b587a
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/doc_simple-canvas.html
@@ -0,0 +1,37 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Canvas inspector test page</title>
+ </head>
+
+ <body>
+ <canvas width="128" height="128"></canvas>
+
+ <script type="text/javascript;version=1.8">
+ "use strict";
+
+ var ctx = document.querySelector("canvas").getContext("2d");
+
+ function drawRect(fill, size) {
+ ctx.fillStyle = fill;
+ ctx.fillRect(size[0], size[1], size[2], size[3]);
+ }
+
+ function drawScene() {
+ ctx.clearRect(0, 0, 128, 128);
+ drawRect("rgb(192, 192, 192)", [0, 0, 128, 128]);
+ drawRect("rgba(0, 0, 192, 0.5)", [30, 30, 55, 50]);
+ drawRect("rgba(192, 0, 0, 0.5)", [10, 10, 55, 50]);
+
+ window.requestAnimationFrame(drawScene);
+ }
+
+ drawScene();
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/canvasdebugger/test/doc_webgl-bindings.html b/devtools/client/canvasdebugger/test/doc_webgl-bindings.html
new file mode 100644
index 000000000..eb1405359
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/doc_webgl-bindings.html
@@ -0,0 +1,61 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>WebGL editor test page</title>
+ </head>
+
+ <body>
+ <canvas id="canvas" width="1024" height="1024"></canvas>
+
+ <script type="text/javascript;version=1.8">
+ "use strict";
+
+ let canvas, gl;
+ let customFramebuffer;
+ let customRenderbuffer;
+ let customTexture;
+
+ window.onload = function() {
+ canvas = document.querySelector("canvas");
+ gl = canvas.getContext("webgl", { preserveDrawingBuffer: true });
+ gl.clearColor(1.0, 0.0, 0.0, 1.0);
+ gl.clear(gl.COLOR_BUFFER_BIT);
+
+ customFramebuffer = gl.createFramebuffer();
+ gl.bindFramebuffer(gl.FRAMEBUFFER, customFramebuffer);
+
+ customRenderbuffer = gl.createRenderbuffer();
+ gl.bindRenderbuffer(gl.RENDERBUFFER, customRenderbuffer);
+ gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, 1024, 1024);
+
+ customTexture = gl.createTexture();
+ gl.bindTexture(gl.TEXTURE_2D, customTexture);
+ 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, 1024, 1024, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
+
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, customTexture, 0);
+ gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, customRenderbuffer);
+
+ gl.viewport(128, 256, 384, 512);
+ gl.clearColor(0.0, 1.0, 0.0, 1.0);
+ gl.clear(gl.COLOR_BUFFER_BIT);
+
+ drawScene();
+ }
+
+ function drawScene() {
+ gl.clearColor(0.0, 0.0, 1.0, 1.0);
+ gl.clear(gl.COLOR_BUFFER_BIT);
+ window.requestAnimationFrame(drawScene);
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/canvasdebugger/test/doc_webgl-drawArrays.html b/devtools/client/canvasdebugger/test/doc_webgl-drawArrays.html
new file mode 100644
index 000000000..7a6aea907
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/doc_webgl-drawArrays.html
@@ -0,0 +1,187 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>WebGL editor test page</title>
+ </head>
+
+ <body>
+ <canvas id="canvas" width="128" height="128"></canvas>
+ <script id="shader-fs" type="x-shader/x-fragment">
+ precision mediump float;
+ uniform vec4 mtrColor;
+
+ void main(void) {
+ gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0) * mtrColor;
+ }
+ </script>
+ <script id="shader-vs" type="x-shader/x-vertex">
+ attribute vec3 aVertexPosition;
+
+ void main(void) {
+ gl_PointSize = 5.0;
+ gl_Position = vec4(aVertexPosition, 1.0);
+ }
+ </script>
+ <script type="text/javascript;version=1.8">
+ "use strict";
+
+ let canvas, gl, shaderProgram;
+ let triangleVertexPositionBuffer, squareVertexPositionBuffer;
+
+ window.onload = function() {
+ canvas = document.querySelector("canvas");
+ gl = canvas.getContext("webgl", { preserveDrawingBuffer: true });
+ gl.viewportWidth = canvas.width;
+ gl.viewportHeight = canvas.height;
+
+ initShaders();
+ initBuffers();
+
+ gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight);
+ gl.clearColor(0.0, 0.0, 0.0, 1.0);
+ gl.disable(gl.DEPTH_TEST);
+ drawScene();
+ }
+
+ function getShader(gl, id) {
+ var shaderScript = document.getElementById(id);
+ if (!shaderScript) {
+ return null;
+ }
+
+ var str = "";
+ var k = shaderScript.firstChild;
+ while (k) {
+ if (k.nodeType == 3) {
+ str += k.textContent;
+ }
+ k = k.nextSibling;
+ }
+
+ var shader;
+ if (shaderScript.type == "x-shader/x-fragment") {
+ shader = gl.createShader(gl.FRAGMENT_SHADER);
+ } else if (shaderScript.type == "x-shader/x-vertex") {
+ shader = gl.createShader(gl.VERTEX_SHADER);
+ } else {
+ return null;
+ }
+
+ gl.shaderSource(shader, str);
+ gl.compileShader(shader);
+
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
+ alert(gl.getShaderInfoLog(shader));
+ return null;
+ }
+
+ return shader;
+ }
+
+ function initShaders() {
+ var fragmentShader = getShader(gl, "shader-fs");
+ var vertexShader = getShader(gl, "shader-vs");
+
+ shaderProgram = gl.createProgram();
+ gl.attachShader(shaderProgram, vertexShader);
+ gl.attachShader(shaderProgram, fragmentShader);
+ gl.linkProgram(shaderProgram);
+
+ if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
+ alert("Could not initialise shaders");
+ }
+
+ gl.useProgram(shaderProgram);
+
+ shaderProgram.vertexPositionAttribute = gl.getAttribLocation(shaderProgram, "aVertexPosition");
+ shaderProgram.pMaterialColor = gl.getUniformLocation(shaderProgram, "mtrColor");
+ gl.enableVertexAttribArray(shaderProgram.vertexPositionAttribute);
+ }
+
+ function initBuffers() {
+ // Create triangle vertex/index buffer
+ triangleVertexPositionBuffer = gl.createBuffer();
+ gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer);
+ var vertices = [
+ 0.0, 0.5, 0.0,
+ -0.5, -0.5, 0.0,
+ 0.5, -0.5, 0.0
+ ];
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
+ triangleVertexPositionBuffer.itemSize = 3;
+ triangleVertexPositionBuffer.numItems = 3;
+
+ // Create square vertex/index buffer
+ squareVertexPositionBuffer = gl.createBuffer();
+ gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);
+ vertices = [
+ 0.8, 0.8, 0.0,
+ -0.8, 0.8, 0.0,
+ 0.8, -0.8, 0.0,
+ -0.8, -0.8, 0.0
+ ];
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
+ squareVertexPositionBuffer.itemSize = 3;
+ squareVertexPositionBuffer.numItems = 4;
+ }
+
+ function drawScene() {
+ gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT);
+
+ // DrawArrays
+ // --------------
+ // draw square - triangle strip
+ gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);
+ gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, squareVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
+ gl.uniform4f(shaderProgram.pMaterialColor, 1, 1, 1, 1);
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, squareVertexPositionBuffer.numItems);
+
+ // draw square - triangle fan
+ gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);
+ gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, squareVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
+ gl.uniform4f(shaderProgram.pMaterialColor, 0, 1, 0, 1);
+ gl.drawArrays(gl.TRIANGLE_FAN, 0, squareVertexPositionBuffer.numItems);
+
+ // draw triangle
+ gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer);
+ gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, triangleVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
+ gl.uniform4f(shaderProgram.pMaterialColor, 1, 0, 0, 1);
+ gl.drawArrays(gl.TRIANGLES, 0, triangleVertexPositionBuffer.numItems);
+
+ // draw points
+ gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);
+ gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, squareVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
+ gl.uniform4f(shaderProgram.pMaterialColor, 1, 0, 1, 1);
+ gl.drawArrays(gl.POINTS, 0, squareVertexPositionBuffer.numItems);
+
+ // draw lines
+ gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);
+ gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, squareVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
+ gl.uniform4f(shaderProgram.pMaterialColor, 0, 0, 1, 1);
+ gl.lineWidth(8.0);
+ gl.drawArrays(gl.LINES, 0, squareVertexPositionBuffer.numItems);
+
+ // draw line strip
+ gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);
+ gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, squareVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
+ gl.uniform4f(shaderProgram.pMaterialColor, 0.9, 0.6, 0, 1);
+ gl.lineWidth(3.0);
+ gl.drawArrays(gl.LINE_STRIP, 0, squareVertexPositionBuffer.numItems);
+
+ // draw line loop
+ gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer);
+ gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, triangleVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
+ gl.uniform4f(shaderProgram.pMaterialColor, 0, 1, 1, 1);
+ gl.lineWidth(3.0);
+ gl.drawArrays(gl.LINE_LOOP, 0, triangleVertexPositionBuffer.numItems);
+
+ window.requestAnimationFrame(drawScene);
+ }
+ </script>
+ </body>
+
+</html> \ No newline at end of file
diff --git a/devtools/client/canvasdebugger/test/doc_webgl-drawElements.html b/devtools/client/canvasdebugger/test/doc_webgl-drawElements.html
new file mode 100644
index 000000000..a8ba4a3e8
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/doc_webgl-drawElements.html
@@ -0,0 +1,225 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>WebGL editor test page</title>
+ </head>
+
+ <body>
+ <canvas id="canvas" width="128" height="128"></canvas>
+ <script id="shader-fs" type="x-shader/x-fragment">
+ precision mediump float;
+ uniform vec4 mtrColor;
+
+ void main(void) {
+ gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0) * mtrColor;
+ }
+ </script>
+ <script id="shader-vs" type="x-shader/x-vertex">
+ attribute vec3 aVertexPosition;
+
+ void main(void) {
+ gl_PointSize = 5.0;
+ gl_Position = vec4(aVertexPosition, 1.0);
+ }
+ </script>
+ <script type="text/javascript;version=1.8">
+ "use strict";
+
+ let canvas, gl, shaderProgram;
+ let triangleVertexPositionBuffer, squareVertexPositionBuffer;
+ let triangleIndexBuffer;
+ let squareIndexBuffer, squareStripIndexBuffer, squareFanIndexBuffer
+
+ window.onload = function() {
+ canvas = document.querySelector("canvas");
+ gl = canvas.getContext("webgl", { preserveDrawingBuffer: true });
+ gl.viewportWidth = canvas.width;
+ gl.viewportHeight = canvas.height;
+
+ initShaders();
+ initBuffers();
+
+ gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight);
+ gl.clearColor(0.0, 0.0, 0.0, 1.0);
+ gl.disable(gl.DEPTH_TEST);
+ drawScene();
+ }
+
+ function getShader(gl, id) {
+ var shaderScript = document.getElementById(id);
+ if (!shaderScript) {
+ return null;
+ }
+
+ var str = "";
+ var k = shaderScript.firstChild;
+ while (k) {
+ if (k.nodeType == 3) {
+ str += k.textContent;
+ }
+ k = k.nextSibling;
+ }
+
+ var shader;
+ if (shaderScript.type == "x-shader/x-fragment") {
+ shader = gl.createShader(gl.FRAGMENT_SHADER);
+ } else if (shaderScript.type == "x-shader/x-vertex") {
+ shader = gl.createShader(gl.VERTEX_SHADER);
+ } else {
+ return null;
+ }
+
+ gl.shaderSource(shader, str);
+ gl.compileShader(shader);
+
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
+ alert(gl.getShaderInfoLog(shader));
+ return null;
+ }
+
+ return shader;
+ }
+
+ function initShaders() {
+ var fragmentShader = getShader(gl, "shader-fs");
+ var vertexShader = getShader(gl, "shader-vs");
+
+ shaderProgram = gl.createProgram();
+ gl.attachShader(shaderProgram, vertexShader);
+ gl.attachShader(shaderProgram, fragmentShader);
+ gl.linkProgram(shaderProgram);
+
+ if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
+ alert("Could not initialise shaders");
+ }
+
+ gl.useProgram(shaderProgram);
+
+ shaderProgram.vertexPositionAttribute = gl.getAttribLocation(shaderProgram, "aVertexPosition");
+ shaderProgram.pMaterialColor = gl.getUniformLocation(shaderProgram, "mtrColor");
+ gl.enableVertexAttribArray(shaderProgram.vertexPositionAttribute);
+ }
+
+ function initBuffers() {
+ // Create triangle vertex/index buffer
+ triangleVertexPositionBuffer = gl.createBuffer();
+ gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer);
+ var vertices = [
+ 0.0, 0.5, 0.0,
+ -0.5, -0.5, 0.0,
+ 0.5, -0.5, 0.0
+ ];
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
+ triangleVertexPositionBuffer.itemSize = 3;
+ triangleVertexPositionBuffer.numItems = 3;
+
+ triangleIndexBuffer = gl.createBuffer();
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, triangleIndexBuffer);
+ var indices = [
+ 0, 1, 2
+ ];
+ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);
+ triangleIndexBuffer.itemSize = 1;
+ triangleIndexBuffer.numItems = 3;
+
+ // Create square vertex/index buffer
+ squareVertexPositionBuffer = gl.createBuffer();
+ gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);
+ vertices = [
+ 0.8, 0.8, 0.0,
+ -0.8, 0.8, 0.0,
+ 0.8, -0.8, 0.0,
+ -0.8, -0.8, 0.0
+ ];
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
+ squareVertexPositionBuffer.itemSize = 3;
+ squareVertexPositionBuffer.numItems = 4;
+
+ squareIndexBuffer = gl.createBuffer();
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, squareIndexBuffer);
+ indices = [
+ 0, 1, 2,
+ 1, 3, 2
+ ];
+ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);
+ squareIndexBuffer.itemSize = 1;
+ squareIndexBuffer.numItems = 6;
+
+ squareStripIndexBuffer = gl.createBuffer();
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, squareStripIndexBuffer);
+ indices = [
+ 0, 1, 2, 3
+ ];
+ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);
+ squareStripIndexBuffer.itemSize = 1;
+ squareStripIndexBuffer.numItems = 4;
+
+ }
+
+ function drawScene() {
+ gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT);
+
+ // DrawElements
+ // --------------
+ // draw triangle
+ gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);
+ gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, squareVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
+ gl.uniform4f(shaderProgram.pMaterialColor, 1, 1, 1, 1);
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, squareIndexBuffer);
+ gl.drawElements(gl.TRIANGLES, squareIndexBuffer.numItems, gl.UNSIGNED_SHORT, 0);
+
+ // draw square - triangle strip
+ gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);
+ gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, squareVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
+ gl.uniform4f(shaderProgram.pMaterialColor, 0, 1, 0, 1);
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, squareStripIndexBuffer);
+ gl.drawElements(gl.TRIANGLE_FAN, squareStripIndexBuffer.numItems, gl.UNSIGNED_SHORT, 0);
+
+ // draw square - triangle fan
+ gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer);
+ gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, triangleVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
+ gl.uniform4f(shaderProgram.pMaterialColor, 1, 0, 0, 1);
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, triangleIndexBuffer);
+ gl.drawElements(gl.TRIANGLE_FAN, triangleIndexBuffer.numItems, gl.UNSIGNED_SHORT, 0);
+
+ // draw points
+ gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);
+ gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, squareVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
+ gl.uniform4f(shaderProgram.pMaterialColor, 1, 0, 1, 1);
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, squareStripIndexBuffer);
+ gl.drawElements(gl.POINTS, squareStripIndexBuffer.numItems, gl.UNSIGNED_SHORT, 0);
+
+ // draw lines
+ gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);
+ gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, squareVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
+ gl.uniform4f(shaderProgram.pMaterialColor, 0, 0, 1, 1);
+ gl.lineWidth(8.0);
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, squareStripIndexBuffer);
+ gl.drawElements(gl.LINES, squareStripIndexBuffer.numItems, gl.UNSIGNED_SHORT, 0);
+
+ // draw line strip
+ gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);
+ gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, squareVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
+ gl.uniform4f(shaderProgram.pMaterialColor, 0.9, 0.6, 0, 1);
+ gl.lineWidth(3.0);
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, squareStripIndexBuffer);
+ gl.drawElements(gl.LINE_STRIP, squareStripIndexBuffer.numItems, gl.UNSIGNED_SHORT, 0);
+
+ // draw line loop
+ gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer);
+ gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, triangleVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
+ gl.uniform4f(shaderProgram.pMaterialColor, 0, 1, 1, 1);
+ gl.lineWidth(3.0);
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, triangleIndexBuffer);
+ gl.drawElements(gl.LINE_LOOP, triangleIndexBuffer.numItems, gl.UNSIGNED_SHORT, 0);
+
+ window.requestAnimationFrame(drawScene);
+ }
+ </script>
+ </body>
+
+</html> \ No newline at end of file
diff --git a/devtools/client/canvasdebugger/test/doc_webgl-enum.html b/devtools/client/canvasdebugger/test/doc_webgl-enum.html
new file mode 100644
index 000000000..f7f4d6d1e
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/doc_webgl-enum.html
@@ -0,0 +1,34 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>WebGL editor test page</title>
+ </head>
+
+ <body>
+ <canvas id="canvas" width="128" height="128"></canvas>
+
+ <script type="text/javascript;version=1.8">
+ "use strict";
+
+ let canvas, gl;
+
+ window.onload = function() {
+ canvas = document.querySelector("canvas");
+ gl = canvas.getContext("webgl", { preserveDrawingBuffer: true });
+ gl.clearColor(0.0, 0.0, 0.0, 1.0);
+ drawScene();
+ }
+
+ function drawScene() {
+ gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT);
+ gl.bindTexture(gl.TEXTURE_2D, null);
+ window.requestAnimationFrame(drawScene);
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/canvasdebugger/test/head.js b/devtools/client/canvasdebugger/test/head.js
new file mode 100644
index 000000000..a718551ce
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/head.js
@@ -0,0 +1,305 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+var { generateUUID } = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
+var { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+
+var Services = require("Services");
+var promise = require("promise");
+var { gDevTools } = require("devtools/client/framework/devtools");
+var { DebuggerClient } = require("devtools/shared/client/main");
+var { DebuggerServer } = require("devtools/server/main");
+var { CallWatcherFront } = require("devtools/shared/fronts/call-watcher");
+var { CanvasFront } = require("devtools/shared/fronts/canvas");
+var { setTimeout } = require("sdk/timers");
+var DevToolsUtils = require("devtools/shared/DevToolsUtils");
+var flags = require("devtools/shared/flags");
+var { TargetFactory } = require("devtools/client/framework/target");
+var { Toolbox } = require("devtools/client/framework/toolbox");
+var { isWebGLSupported } = require("devtools/client/shared/webgl-utils");
+var mm = null;
+
+const FRAME_SCRIPT_UTILS_URL = "chrome://devtools/content/shared/frame-script-utils.js";
+const EXAMPLE_URL = "http://example.com/browser/devtools/client/canvasdebugger/test/";
+const SET_TIMEOUT_URL = EXAMPLE_URL + "doc_settimeout.html";
+const NO_CANVAS_URL = EXAMPLE_URL + "doc_no-canvas.html";
+const RAF_NO_CANVAS_URL = EXAMPLE_URL + "doc_raf-no-canvas.html";
+const SIMPLE_CANVAS_URL = EXAMPLE_URL + "doc_simple-canvas.html";
+const SIMPLE_BITMASKS_URL = EXAMPLE_URL + "doc_simple-canvas-bitmasks.html";
+const SIMPLE_CANVAS_TRANSPARENT_URL = EXAMPLE_URL + "doc_simple-canvas-transparent.html";
+const SIMPLE_CANVAS_DEEP_STACK_URL = EXAMPLE_URL + "doc_simple-canvas-deep-stack.html";
+const WEBGL_ENUM_URL = EXAMPLE_URL + "doc_webgl-enum.html";
+const WEBGL_BINDINGS_URL = EXAMPLE_URL + "doc_webgl-bindings.html";
+const WEBGL_DRAW_ARRAYS = EXAMPLE_URL + "doc_webgl-drawArrays.html";
+const WEBGL_DRAW_ELEMENTS = EXAMPLE_URL + "doc_webgl-drawElements.html";
+const RAF_BEGIN_URL = EXAMPLE_URL + "doc_raf-begin.html";
+
+// Disable logging for all the tests. Both the debugger server and frontend will
+// be affected by this pref.
+var gEnableLogging = Services.prefs.getBoolPref("devtools.debugger.log");
+Services.prefs.setBoolPref("devtools.debugger.log", false);
+
+// All tests are asynchronous.
+waitForExplicitFinish();
+
+var gToolEnabled = Services.prefs.getBoolPref("devtools.canvasdebugger.enabled");
+
+flags.testing = true;
+
+registerCleanupFunction(() => {
+ info("finish() was called, cleaning up...");
+ flags.testing = false;
+ Services.prefs.setBoolPref("devtools.debugger.log", gEnableLogging);
+ Services.prefs.setBoolPref("devtools.canvasdebugger.enabled", gToolEnabled);
+
+ // Some of yhese tests use a lot of memory due to GL contexts, so force a GC
+ // to help fragmentation.
+ info("Forcing GC after canvas debugger test.");
+ Cu.forceGC();
+});
+
+/**
+ * Call manually in tests that use frame script utils after initializing
+ * the shader editor. Call after init but before navigating to different pages.
+ */
+function loadFrameScripts() {
+ mm = gBrowser.selectedBrowser.messageManager;
+ mm.loadFrameScript(FRAME_SCRIPT_UTILS_URL, false);
+}
+
+function addTab(aUrl, aWindow) {
+ info("Adding tab: " + aUrl);
+
+ let deferred = promise.defer();
+ let targetWindow = aWindow || window;
+ let targetBrowser = targetWindow.gBrowser;
+
+ targetWindow.focus();
+ let tab = targetBrowser.selectedTab = targetBrowser.addTab(aUrl);
+ let linkedBrowser = tab.linkedBrowser;
+
+ BrowserTestUtils.browserLoaded(linkedBrowser)
+ .then(function () {
+ info("Tab added and finished loading: " + aUrl);
+ deferred.resolve(tab);
+ });
+
+ return deferred.promise;
+}
+
+function removeTab(aTab, aWindow) {
+ info("Removing tab.");
+
+ let deferred = promise.defer();
+ let targetWindow = aWindow || window;
+ let targetBrowser = targetWindow.gBrowser;
+ let tabContainer = targetBrowser.tabContainer;
+
+ tabContainer.addEventListener("TabClose", function onClose(aEvent) {
+ tabContainer.removeEventListener("TabClose", onClose, false);
+ info("Tab removed and finished closing.");
+ deferred.resolve();
+ }, false);
+
+ targetBrowser.removeTab(aTab);
+ return deferred.promise;
+}
+
+function handleError(aError) {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ finish();
+}
+
+var gRequiresWebGL = false;
+
+function ifTestingSupported() {
+ ok(false, "You need to define a 'ifTestingSupported' function.");
+ finish();
+}
+
+function ifTestingUnsupported() {
+ todo(false, "Skipping test because some required functionality isn't supported.");
+ finish();
+}
+
+function test() {
+ let generator = isTestingSupported() ? ifTestingSupported : ifTestingUnsupported;
+ Task.spawn(generator).then(null, handleError);
+}
+
+function createCanvas() {
+ return document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
+}
+
+function isTestingSupported() {
+ if (!gRequiresWebGL) {
+ info("This test does not require WebGL support.");
+ return true;
+ }
+
+ let supported = isWebGLSupported(document);
+
+ info("This test requires WebGL support.");
+ info("Apparently, WebGL is" + (supported ? "" : " not") + " supported.");
+ return supported;
+}
+
+function once(aTarget, aEventName, aUseCapture = false) {
+ info("Waiting for event: '" + aEventName + "' on " + aTarget + ".");
+
+ let deferred = promise.defer();
+
+ for (let [add, remove] of [
+ ["on", "off"], // Use event emitter before DOM events for consistency
+ ["addEventListener", "removeEventListener"],
+ ["addListener", "removeListener"]
+ ]) {
+ if ((add in aTarget) && (remove in aTarget)) {
+ aTarget[add](aEventName, function onEvent(...aArgs) {
+ info("Got event: '" + aEventName + "' on " + aTarget + ".");
+ aTarget[remove](aEventName, onEvent, aUseCapture);
+ deferred.resolve(...aArgs);
+ }, aUseCapture);
+ break;
+ }
+ }
+
+ return deferred.promise;
+}
+
+function waitForTick() {
+ let deferred = promise.defer();
+ executeSoon(deferred.resolve);
+ return deferred.promise;
+}
+
+function navigateInHistory(aTarget, aDirection, aWaitForTargetEvent = "navigate") {
+ executeSoon(() => content.history[aDirection]());
+ return once(aTarget, aWaitForTargetEvent);
+}
+
+function navigate(aTarget, aUrl, aWaitForTargetEvent = "navigate") {
+ executeSoon(() => aTarget.activeTab.navigateTo(aUrl));
+ return once(aTarget, aWaitForTargetEvent);
+}
+
+function reload(aTarget, aWaitForTargetEvent = "navigate") {
+ executeSoon(() => aTarget.activeTab.reload());
+ return once(aTarget, aWaitForTargetEvent);
+}
+
+function initServer() {
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+}
+
+function initCallWatcherBackend(aUrl) {
+ info("Initializing a call watcher front.");
+ initServer();
+
+ return Task.spawn(function* () {
+ let tab = yield addTab(aUrl);
+ let target = TargetFactory.forTab(tab);
+
+ yield target.makeRemote();
+
+ let front = new CallWatcherFront(target.client, target.form);
+ return { target, front };
+ });
+}
+
+function initCanvasDebuggerBackend(aUrl) {
+ info("Initializing a canvas debugger front.");
+ initServer();
+
+ return Task.spawn(function* () {
+ let tab = yield addTab(aUrl);
+ let target = TargetFactory.forTab(tab);
+
+ yield target.makeRemote();
+
+ let front = new CanvasFront(target.client, target.form);
+ return { target, front };
+ });
+}
+
+function initCanvasDebuggerFrontend(aUrl) {
+ info("Initializing a canvas debugger pane.");
+
+ return Task.spawn(function* () {
+ let tab = yield addTab(aUrl);
+ let target = TargetFactory.forTab(tab);
+
+ yield target.makeRemote();
+
+ Services.prefs.setBoolPref("devtools.canvasdebugger.enabled", true);
+ let toolbox = yield gDevTools.showToolbox(target, "canvasdebugger");
+ let panel = toolbox.getCurrentPanel();
+ return { target, panel };
+ });
+}
+
+function teardown({target}) {
+ info("Destroying the specified canvas debugger.");
+
+ let {tab} = target;
+ return gDevTools.closeToolbox(target).then(() => {
+ removeTab(tab);
+ });
+}
+
+/**
+ * Takes a string `script` and evaluates it directly in the content
+ * in potentially a different process.
+ */
+function evalInDebuggee(script) {
+ let deferred = promise.defer();
+
+ if (!mm) {
+ throw new Error("`loadFrameScripts()` must be called when using MessageManager.");
+ }
+
+ let id = generateUUID().toString();
+ mm.sendAsyncMessage("devtools:test:eval", { script: script, id: id });
+ mm.addMessageListener("devtools:test:eval:response", handler);
+
+ function handler({ data }) {
+ if (id !== data.id) {
+ return;
+ }
+
+ mm.removeMessageListener("devtools:test:eval:response", handler);
+ deferred.resolve(data.value);
+ }
+
+ return deferred.promise;
+}
+
+function getSourceActor(aSources, aURL) {
+ let item = aSources.getItemForAttachment(a => a.source.url === aURL);
+ return item ? item.value : null;
+}
+
+/**
+ * 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 (yield predicate()) {
+ return Promise.resolve(true);
+ }
+ let deferred = Promise.defer();
+ setTimeout(function () {
+ waitUntil(predicate).then(() => deferred.resolve(true));
+ }, interval);
+ return deferred.promise;
+}
diff --git a/devtools/client/commandline/commandline.css b/devtools/client/commandline/commandline.css
new file mode 100644
index 000000000..aed2d0e64
--- /dev/null
+++ b/devtools/client/commandline/commandline.css
@@ -0,0 +1,85 @@
+/* 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/. */
+
+.gcli-help-name {
+ text-align: end;
+}
+
+.gcli-out-shortcut,
+.gcli-help-synopsis {
+ cursor: pointer;
+ display: inline-block;
+}
+
+.gcli-out-shortcut:before,
+.gcli-help-synopsis:before {
+ content: '\bb';
+}
+
+.gcli-menu-template {
+ white-space: nowrap;
+ width: 290px;
+ display: flex;
+}
+
+.gcli-menu-names {
+ white-space: nowrap;
+ flex-grow: 0;
+ flex-shrink: 0;
+}
+
+.gcli-menu-descs {
+ flex-grow: 1;
+ flex-shrink: 1;
+}
+
+.gcli-menu-name,
+.gcli-menu-desc {
+ white-space: nowrap;
+}
+
+.gcli-menu-name {
+ padding-inline-end: 10px;
+}
+
+.gcli-menu-desc {
+ text-overflow: ellipsis;
+ overflow: hidden;
+}
+
+.gcli-menu-name,
+.gcli-out-shortcut,
+.gcli-help-synopsis {
+ direction: ltr;
+}
+
+.gcli-cookielist-list {
+ list-style-type: none;
+ padding-left: 0;
+}
+
+.gcli-cookielist-detail {
+ padding-left: 20px;
+ padding-bottom: 10px;
+}
+
+.gcli-appcache-list {
+ list-style-type: none;
+ padding-left: 0;
+}
+
+.gcli-appcache-detail {
+ padding-left: 20px;
+ padding-bottom: 10px;
+}
+
+.gcli-row-out .nowrap {
+ white-space: nowrap;
+}
+
+.gcli-mdn-url {
+ text-decoration: underline;
+ cursor: pointer;
+}
+
diff --git a/devtools/client/commandline/commandlineoutput.xhtml b/devtools/client/commandline/commandlineoutput.xhtml
new file mode 100644
index 000000000..c8674c838
--- /dev/null
+++ b/devtools/client/commandline/commandlineoutput.xhtml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+
+<!-- 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/. -->
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <link rel="stylesheet" href="chrome://devtools/content/commandline/commandline.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://devtools/skin/commandline.css" type="text/css"/>
+</head>
+<body class="gcli-body">
+<div id="gcli-output-root"></div>
+</body>
+</html>
diff --git a/devtools/client/commandline/commandlinetooltip.xhtml b/devtools/client/commandline/commandlinetooltip.xhtml
new file mode 100644
index 000000000..74c831022
--- /dev/null
+++ b/devtools/client/commandline/commandlinetooltip.xhtml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+
+<!-- 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/. -->
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <link rel="stylesheet" href="chrome://devtools/content/commandline/commandline.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://devtools/skin/commandline.css" type="text/css"/>
+</head>
+<body class="gcli-body">
+<div id="gcli-tooltip-root"></div>
+<div id="gcli-tooltip-connector"></div>
+</body>
+</html>
diff --git a/devtools/client/commandline/moz.build b/devtools/client/commandline/moz.build
new file mode 100644
index 000000000..22fc46624
--- /dev/null
+++ b/devtools/client/commandline/moz.build
@@ -0,0 +1,5 @@
+# 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/.
+
+BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
diff --git a/devtools/client/commandline/test/.eslintrc.js b/devtools/client/commandline/test/.eslintrc.js
new file mode 100644
index 000000000..815b63a0e
--- /dev/null
+++ b/devtools/client/commandline/test/.eslintrc.js
@@ -0,0 +1,10 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../../.eslintrc.mochitests.js",
+ "globals": {
+ "helpers": true,
+ "assert": true
+ }
+};
diff --git a/devtools/client/commandline/test/browser.ini b/devtools/client/commandline/test/browser.ini
new file mode 100644
index 000000000..7ba549418
--- /dev/null
+++ b/devtools/client/commandline/test/browser.ini
@@ -0,0 +1,124 @@
+[DEFAULT]
+skip-if = e10s # Bug 1034511
+tags = devtools
+subsuite = devtools
+support-files =
+ head.js
+ helpers.js
+ mockCommands.js
+
+[browser_cmd_addon.js]
+[browser_cmd_calllog.js]
+skip-if = true # Bug 845831
+[browser_cmd_calllog_chrome.js]
+skip-if = true # Bug 845831
+[browser_cmd_appcache_invalid.js]
+support-files =
+ browser_cmd_appcache_invalid_appcache.appcache
+ browser_cmd_appcache_invalid_appcache.appcache^headers^
+ browser_cmd_appcache_invalid_index.html
+ browser_cmd_appcache_invalid_page1.html
+ browser_cmd_appcache_invalid_page2.html
+ browser_cmd_appcache_invalid_page3.html
+ browser_cmd_appcache_invalid_page3.html^headers^
+[browser_cmd_appcache_valid.js]
+support-files =
+ browser_cmd_appcache_valid_appcache.appcache
+ browser_cmd_appcache_valid_appcache.appcache^headers^
+ browser_cmd_appcache_valid_index.html
+ browser_cmd_appcache_valid_page1.html
+ browser_cmd_appcache_valid_page2.html
+ browser_cmd_appcache_valid_page3.html
+[browser_cmd_commands.js]
+[browser_cmd_cookie.js]
+support-files =
+ browser_cmd_cookie.html
+[browser_cmd_cookie_host.js]
+support-files =
+ browser_cmd_cookie.html
+[browser_cmd_csscoverage_oneshot.js]
+support-files =
+ browser_cmd_csscoverage_page1.html
+ browser_cmd_csscoverage_page2.html
+ browser_cmd_csscoverage_page3.html
+ browser_cmd_csscoverage_sheetA.css
+ browser_cmd_csscoverage_sheetB.css
+ browser_cmd_csscoverage_sheetC.css
+ browser_cmd_csscoverage_sheetD.css
+[browser_cmd_csscoverage_startstop.js]
+support-files =
+ browser_cmd_csscoverage_page1.html
+ browser_cmd_csscoverage_page2.html
+ browser_cmd_csscoverage_page3.html
+ browser_cmd_csscoverage_sheetA.css
+ browser_cmd_csscoverage_sheetB.css
+ browser_cmd_csscoverage_sheetC.css
+ browser_cmd_csscoverage_sheetD.css
+[browser_cmd_folder.js]
+skip-if = (e10s && debug) # Bug 1034511 (docShell leaks on debug)
+[browser_cmd_highlight_01.js]
+[browser_cmd_highlight_02.js]
+[browser_cmd_highlight_03.js]
+[browser_cmd_highlight_04.js]
+[browser_cmd_inject.js]
+support-files =
+ browser_cmd_inject.html
+[browser_cmd_csscoverage_util.js]
+skip-if = (e10s && debug) # Bug 1034511 (docShell leaks on debug)
+[browser_cmd_jsb.js]
+support-files =
+ browser_cmd_jsb_script.jsi
+[browser_cmd_listen.js]
+[browser_cmd_measure.js]
+[browser_cmd_media.js]
+support-files =
+ browser_cmd_media.html
+[browser_cmd_pagemod_export.js]
+support-files =
+ browser_cmd_pagemod_export.html
+[browser_cmd_paintflashing.js]
+[browser_cmd_pref1.js]
+[browser_cmd_pref2.js]
+[browser_cmd_pref3.js]
+[browser_cmd_qsa.js]
+[browser_cmd_restart.js]
+[browser_cmd_rulers.js]
+[browser_cmd_screenshot.js]
+subsuite = clipboard
+support-files =
+ browser_cmd_screenshot.html
+[browser_cmd_settings.js]
+[browser_gcli_async.js]
+[browser_gcli_canon.js]
+[browser_gcli_cli1.js]
+[browser_gcli_cli2.js]
+[browser_gcli_completion1.js]
+[browser_gcli_completion2.js]
+[browser_gcli_date.js]
+skip-if = true # Bug 934098
+[browser_gcli_exec.js]
+[browser_gcli_fail.js]
+[browser_gcli_file.js]
+[browser_gcli_focus.js]
+[browser_gcli_history.js]
+[browser_gcli_incomplete.js]
+[browser_gcli_inputter.js]
+skip-if = true # Bug 1093205 - Test does not run in Firefox due to missing terminal bug
+[browser_gcli_intro.js]
+[browser_gcli_js.js]
+[browser_gcli_keyboard1.js]
+[browser_gcli_keyboard2.js]
+[browser_gcli_keyboard3.js]
+[browser_gcli_keyboard4.js]
+[browser_gcli_keyboard5.js]
+[browser_gcli_menu.js]
+[browser_gcli_node.js]
+[browser_gcli_resource.js]
+[browser_gcli_short.js]
+[browser_gcli_spell.js]
+[browser_gcli_split.js]
+[browser_gcli_tokenize.js]
+[browser_gcli_tooltip.js]
+skip-if = true # Bug 1093205 - Test does not run in Firefox due to missing terminal
+[browser_gcli_types.js]
+[browser_gcli_union.js]
diff --git a/devtools/client/commandline/test/browser_cmd_addon.js b/devtools/client/commandline/test/browser_cmd_addon.js
new file mode 100644
index 000000000..e8cea2e06
--- /dev/null
+++ b/devtools/client/commandline/test/browser_cmd_addon.js
@@ -0,0 +1,195 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the addon commands works as they should
+
+function test() {
+ return Task.spawn(spawnTest).then(finish, helpers.handleError);
+}
+
+function* spawnTest() {
+ let options = yield helpers.openTab("about:blank");
+ yield helpers.openToolbar(options);
+
+ yield helpers.audit(options, [
+ {
+ setup: "addon list dictionary",
+ check: {
+ input: "addon list dictionary",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVVVVVVVV",
+ status: "VALID"
+ },
+ exec: {
+ output: "There are no add-ons of that type installed."
+ }
+ },
+ {
+ setup: "addon list extension",
+ check: {
+ input: "addon list extension",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVVVVVVV",
+ status: "VALID"
+ },
+ exec: {
+ output: [/The following/, /Mochitest/, /Special Powers/]
+ }
+ },
+ {
+ setup: "addon list locale",
+ check: {
+ input: "addon list locale",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVVVV",
+ status: "VALID"
+ },
+ exec: {
+ output: "There are no add-ons of that type installed."
+ }
+ },
+ {
+ setup: "addon list plugin",
+ check: {
+ input: "addon list plugin",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVVVV",
+ status: "VALID"
+ },
+ exec: {
+ output: [/Test Plug-in/, /Second Test Plug-in/]
+ }
+ },
+ {
+ setup: "addon list theme",
+ check: {
+ input: "addon list theme",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVVV",
+ status: "VALID"
+ },
+ exec: {
+ output: [/following themes/, /Default/]
+ }
+ },
+ {
+ setup: "addon list all",
+ check: {
+ input: "addon list all",
+ hints: "",
+ markup: "VVVVVVVVVVVVVV",
+ status: "VALID"
+ },
+ exec: {
+ output: [/The following/, /Default/, /Mochitest/, /Test Plug-in/,
+ /Second Test Plug-in/, /Special Powers/]
+ }
+ },
+ {
+ setup: "addon disable Test_Plug-in_1.0.0.0",
+ check: {
+ input: "addon disable Test_Plug-in_1.0.0.0",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV",
+ status: "VALID"
+ },
+ exec: {
+ output: "Test Plug-in 1.0.0.0 disabled."
+ }
+ },
+ {
+ setup: "addon disable WRONG",
+ check: {
+ input: "addon disable WRONG",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVEEEEE",
+ status: "ERROR"
+ }
+ },
+ {
+ setup: "addon enable Test_Plug-in_1.0.0.0",
+ check: {
+ input: "addon enable Test_Plug-in_1.0.0.0",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV",
+ status: "VALID",
+ args: {
+ command: { name: "addon enable" },
+ addon: {
+ value: function (addon) {
+ is(addon.name, "Test Plug-in", "test plugin name");
+ },
+ status: "VALID"
+ }
+ }
+ },
+ exec: {
+ output: "Test Plug-in 1.0.0.0 enabled."
+ }
+ },
+ {
+ setup: "addon ctp Test_Plug-in_1.0.0.0",
+ check: {
+ input: "addon ctp Test_Plug-in_1.0.0.0",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVVVVVVVVVVVVVVVVV",
+ status: "VALID",
+ args: {
+ command: { name: "addon ctp" },
+ addon: {
+ value: function (addon) {
+ is(addon.name, "Test Plug-in", "test plugin name");
+ },
+ status: "VALID"
+ }
+ }
+ },
+ exec: {
+ output: "Test Plug-in 1.0.0.0 set to click-to-play."
+ }
+ },
+ {
+ setup: "addon ctp OpenH264_Video_Codec_provided_by_Cisco_Systems,_Inc._null",
+ check: {
+ input: "addon ctp OpenH264_Video_Codec_provided_by_Cisco_Systems,_Inc._null",
+ hints: "",
+ status: "VALID",
+ args: {
+ command: { name: "addon ctp" },
+ addon: {
+ value: function (addon) {
+ is(addon.name, "OpenH264 Video Codec provided by Cisco Systems, Inc.", "openh264");
+ },
+ status: "VALID"
+ }
+ }
+ },
+ exec: {
+ output: "OpenH264 Video Codec provided by Cisco Systems, Inc. null cannot be set to click-to-play."
+ }
+ },
+ {
+ setup: "addon ctp Mochitest_1.0",
+ check: {
+ input: "addon ctp Mochitest_1.0",
+ hints: "",
+ status: "VALID",
+ args: {
+ command: { name: "addon ctp" },
+ addon: {
+ value: function (addon) {
+ is(addon.name, "Mochitest", "mochitest");
+ },
+ status: "VALID"
+ }
+ }
+ },
+ exec: {
+ output: "Mochitest 1.0 cannot be set to click-to-play because it is not a plugin."
+ }
+ }
+ ]);
+
+ yield helpers.closeToolbar(options);
+ yield helpers.closeTab(options);
+}
diff --git a/devtools/client/commandline/test/browser_cmd_appcache_invalid.js b/devtools/client/commandline/test/browser_cmd_appcache_invalid.js
new file mode 100644
index 000000000..df87bbc5a
--- /dev/null
+++ b/devtools/client/commandline/test/browser_cmd_appcache_invalid.js
@@ -0,0 +1,134 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the appcache validate works as they should with an invalid
+// manifest.
+
+const TEST_URI = "http://sub1.test1.example.com/browser/devtools/client/commandline/" +
+ "test/browser_cmd_appcache_invalid_index.html";
+
+function test() {
+ return Task.spawn(spawnTest).then(finish, helpers.handleError);
+}
+
+function* spawnTest() {
+ let lines = [
+ "Manifest has a character encoding of ISO-8859-1. Manifests must have the " +
+ "utf-8 character encoding.",
+ "The first line of the manifest must be \u201cCACHE MANIFEST\u201d at line 1.",
+ "\u201cCACHE MANIFEST\u201d is only valid on the first line but was found at line 3.",
+ "images/sound-icon.png points to a resource that is not available at line 9.",
+ "images/background.png points to a resource that is not available at line 10.",
+ "/checking.cgi points to a resource that is not available at line 13.",
+ "Asterisk (*) incorrectly used in the NETWORK section at line 14. If a line " +
+ "in the NETWORK section contains only a single asterisk character, then any " +
+ "URI not listed in the manifest will be treated as if the URI was listed in " +
+ "the NETWORK section. Otherwise such URIs will be treated as unavailable. " +
+ "Other uses of the * character are prohibited",
+ "../rel.html points to a resource that is not available at line 17.",
+ "../../rel.html points to a resource that is not available at line 18.",
+ "../../../rel.html points to a resource that is not available at line 19.",
+ "../../../../rel.html points to a resource that is not available at line 20.",
+ "../../../../../rel.html points to a resource that is not available at line 21.",
+ "/../ is not a valid URI prefix at line 22.",
+ "/test.css points to a resource that is not available at line 23.",
+ "/test.js points to a resource that is not available at line 24.",
+ "test.png points to a resource that is not available at line 25.",
+ "/main/features.js points to a resource that is not available at line 27.",
+ "/main/settings/index.css points to a resource that is not available at line 28.",
+ "http://example.com/scene.jpg points to a resource that is not available at line 29.",
+ "/section1/blockedbyfallback.html points to a resource that is not available at line 30.",
+ "http://example.com/images/world.jpg points to a resource that is not available at line 31.",
+ "/section2/blockedbyfallback.html points to a resource that is not available at line 32.",
+ "/main/home points to a resource that is not available at line 34.",
+ "main/app.js points to a resource that is not available at line 35.",
+ "/settings/home points to a resource that is not available at line 37.",
+ "/settings/app.js points to a resource that is not available at line 38.",
+ "The file http://sub1.test1.example.com/browser/devtools/client/" +
+ "commandline/test/browser_cmd_appcache_invalid_page3.html was modified " +
+ "after http://sub1.test1.example.com/browser/devtools/client/" +
+ "commandline/test/browser_cmd_appcache_invalid_appcache.appcache. Unless " +
+ "the text in the manifest file is changed the cached version will be used " +
+ "instead at line 39.",
+ "browser_cmd_appcache_invalid_page3.html has cache-control set to no-store. " +
+ "This will prevent the application cache from storing the file at line 39.",
+ "http://example.com/logo.png points to a resource that is not available at line 40.",
+ "http://example.com/check.png points to a resource that is not available at line 41.",
+ "Spaces in URIs need to be replaced with % at line 42.",
+ "http://example.com/cr oss.png points to a resource that is not available at line 42.",
+ "Asterisk (*) incorrectly used in the CACHE section at line 43. If a line " +
+ "in the NETWORK section contains only a single asterisk character, then " +
+ "any URI not listed in the manifest will be treated as if the URI was " +
+ "listed in the NETWORK section. Otherwise such URIs will be treated as " +
+ "unavailable. Other uses of the * character are prohibited",
+ "The SETTINGS section may only contain a single value, \u201cprefer-online\u201d or \u201cfast\u201d at line 47.",
+ "FALLBACK section line 50 (/section1/ /offline1.html) prevents caching of " +
+ "line 30 (/section1/blockedbyfallback.html) in the CACHE section.",
+ "/offline1.html points to a resource that is not available at line 50.",
+ "FALLBACK section line 51 (/section2/ offline2.html) prevents caching of " +
+ "line 32 (/section2/blockedbyfallback.html) in the CACHE section.",
+ "offline2.html points to a resource that is not available at line 51.",
+ "Only two URIs separated by spaces are allowed in the FALLBACK section at line 52.",
+ "Asterisk (*) incorrectly used in the FALLBACK section at line 53. URIs " +
+ "in the FALLBACK section simply need to match a prefix of the request URI.",
+ "offline3.html points to a resource that is not available at line 53.",
+ "Invalid section name (BLAH) at line 55.",
+ "Only two URIs separated by spaces are allowed in the FALLBACK section at line 55."
+ ];
+
+ let options = yield helpers.openTab(TEST_URI);
+ info("window open");
+
+ // Wait for site to be cached.
+ yield helpers.listenOnce(gBrowser.contentWindow.applicationCache, "error");
+ info("applicationCache error happened");
+
+ yield helpers.openToolbar(options);
+ info("toolbar open");
+
+ // Pages containing an appcache the notification bar gives options to allow
+ // or deny permission for the app to save data offline. Let's click Allow.
+ let notificationID = "offline-app-requested-sub1.test1.example.com";
+ let notification =
+ PopupNotifications.getNotification(notificationID, gBrowser.selectedBrowser);
+
+ if (notification) {
+ info("Authorizing offline storage.");
+ notification.mainAction.callback();
+ } else {
+ info("No notification box is available.");
+ }
+
+ info("Site now cached, running tests.");
+ yield helpers.audit(options, [
+ {
+ setup: "appcache validate",
+ check: {
+ input: "appcache validate",
+ markup: "VVVVVVVVVVVVVVVVV",
+ status: "VALID",
+ args: {}
+ },
+ exec: {
+ output: lines.map(getRegexForString)
+ },
+ },
+ ]);
+
+ yield helpers.closeToolbar(options);
+ yield helpers.closeTab(options);
+}
+
+/**
+ * Creates a regular expression that matches a string. This greatly simplifies
+ * matching and debugging long strings.
+ *
+ * @param {String} text
+ * Text to convert
+ * @return {RegExp}
+ * Regular expression matching text
+ */
+function getRegexForString(str) {
+ str = str.replace(/(\.|\\|\/|\(|\)|\[|\]|\*|\+|\?|\$|\^|\|)/g, "\\$1");
+ return new RegExp(str);
+}
diff --git a/devtools/client/commandline/test/browser_cmd_appcache_invalid_appcache.appcache b/devtools/client/commandline/test/browser_cmd_appcache_invalid_appcache.appcache
new file mode 100644
index 000000000..75b5d7bad
--- /dev/null
+++ b/devtools/client/commandline/test/browser_cmd_appcache_invalid_appcache.appcache
@@ -0,0 +1,55 @@
+# some comment
+
+CACHE MANIFEST
+# the above is a required line
+# this is a comment
+# spaces are ignored
+# blank lines are ignored
+
+images/sound-icon.png
+images/background.png
+
+NETWORK:
+/checking.cgi
+/checking.*
+
+CACHE:
+../rel.html
+../../rel.html
+../../../rel.html
+../../../../rel.html
+../../../../../rel.html
+/../invalid.html
+/test.css
+/test.js
+test.png
+browser_cmd_appcache_invalid_index.html
+/main/features.js
+/main/settings/index.css
+http://example.com/scene.jpg
+/section1/blockedbyfallback.html
+http://example.com/images/world.jpg
+/section2/blockedbyfallback.html
+browser_cmd_appcache_invalid_page1.html
+/main/home
+main/app.js
+browser_cmd_appcache_invalid_page2.html
+/settings/home
+/settings/app.js
+browser_cmd_appcache_invalid_page3.html
+http://example.com/logo.png
+http://example.com/check.png
+http://example.com/cr oss.png
+/checking*.png
+
+SETTINGS:
+prefer-online
+fast
+
+FALLBACK:
+/section1/ /offline1.html
+/section2/ offline2.html
+dadsdsd
+* offline3.html
+
+BLAH:
diff --git a/devtools/client/commandline/test/browser_cmd_appcache_invalid_appcache.appcache^headers^ b/devtools/client/commandline/test/browser_cmd_appcache_invalid_appcache.appcache^headers^
new file mode 100644
index 000000000..af95ed1f5
--- /dev/null
+++ b/devtools/client/commandline/test/browser_cmd_appcache_invalid_appcache.appcache^headers^
@@ -0,0 +1,2 @@
+Content-Type: text/cache-manifest; charset=ISO-8859-1
+Last-Modified: Tue, 23 Apr 9998 11:41:13 GMT
diff --git a/devtools/client/commandline/test/browser_cmd_appcache_invalid_index.html b/devtools/client/commandline/test/browser_cmd_appcache_invalid_index.html
new file mode 100644
index 000000000..67f9aa675
--- /dev/null
+++ b/devtools/client/commandline/test/browser_cmd_appcache_invalid_index.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html manifest="browser_cmd_appcache_invalid_appcache.appcache">
+ <head>
+ <meta charset="UTF-8">
+ <head>
+ <body>
+ <h1>Example index.html</h1>
+ <br />
+ <a href="browser_cmd_appcache_invalid_index.html">Home</a> |
+ <a href="browser_cmd_appcache_invalid_page1.html">Page 1</a> |
+ <a href="browser_cmd_appcache_invalid_page2.html">Page 2</a> |
+ <a href="browser_cmd_appcache_invalid_page3.html">Page 3</a>
+ </body>
+</html>
diff --git a/devtools/client/commandline/test/browser_cmd_appcache_invalid_page1.html b/devtools/client/commandline/test/browser_cmd_appcache_invalid_page1.html
new file mode 100644
index 000000000..5ff36f102
--- /dev/null
+++ b/devtools/client/commandline/test/browser_cmd_appcache_invalid_page1.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html manifest="browser_cmd_appcache_invalid_appcache.appcache">
+ <head>
+ <meta charset="UTF-8">
+ <head>
+ <body>
+ <h1>Example page1.html</h1>
+ <br />
+ <a href="browser_cmd_appcache_invalid_index.html">Home</a> |
+ <a href="browser_cmd_appcache_invalid_page1.html">Page 1</a> |
+ <a href="browser_cmd_appcache_invalid_page2.html">Page 2</a> |
+ <a href="browser_cmd_appcache_invalid_page3.html">Page 3</a>
+ </body>
+</html>
diff --git a/devtools/client/commandline/test/browser_cmd_appcache_invalid_page2.html b/devtools/client/commandline/test/browser_cmd_appcache_invalid_page2.html
new file mode 100644
index 000000000..7d4a0c44d
--- /dev/null
+++ b/devtools/client/commandline/test/browser_cmd_appcache_invalid_page2.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html manifest="browser_cmd_appcache_invalid_appcache.appcache">
+ <head>
+ <meta charset="UTF-8">
+ <head>
+ <body>
+ <h1>Example page2.html</h1>
+ <br />
+ <a href="browser_cmd_appcache_invalid_index.html">Home</a> |
+ <a href="browser_cmd_appcache_invalid_page1.html">Page 1</a> |
+ <a href="browser_cmd_appcache_invalid_page2.html">Page 2</a> |
+ <a href="browser_cmd_appcache_invalid_page3.html">Page 3</a>
+ </body>
+</html>
diff --git a/devtools/client/commandline/test/browser_cmd_appcache_invalid_page3.html b/devtools/client/commandline/test/browser_cmd_appcache_invalid_page3.html
new file mode 100644
index 000000000..6777e59f8
--- /dev/null
+++ b/devtools/client/commandline/test/browser_cmd_appcache_invalid_page3.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html manifest="browser_cmd_appcache_invalid_appcache.appcache">
+ <head>
+ <meta charset="UTF-8">
+ <head>
+ <body>
+ <h1>Example page3.html</h1>
+ <br />
+ <a href="browser_cmd_appcache_invalid_index.html">Home</a> |
+ <a href="browser_cmd_appcache_invalid_page1.html">Page 1</a> |
+ <a href="browser_cmd_appcache_invalid_page2.html">Page 2</a> |
+ <a href="browser_cmd_appcache_invalid_page3.html">Page 3</a>
+ </body>
+</html>
diff --git a/devtools/client/commandline/test/browser_cmd_appcache_invalid_page3.html^headers^ b/devtools/client/commandline/test/browser_cmd_appcache_invalid_page3.html^headers^
new file mode 100644
index 000000000..177130b43
--- /dev/null
+++ b/devtools/client/commandline/test/browser_cmd_appcache_invalid_page3.html^headers^
@@ -0,0 +1,2 @@
+Cache-Control: no-store, no-cache
+Last-Modified: Tue, 23 Apr 9999 11:41:13 GMT
diff --git a/devtools/client/commandline/test/browser_cmd_appcache_valid.js b/devtools/client/commandline/test/browser_cmd_appcache_valid.js
new file mode 100644
index 000000000..83aa9ca8f
--- /dev/null
+++ b/devtools/client/commandline/test/browser_cmd_appcache_valid.js
@@ -0,0 +1,173 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the appcache commands works as they should
+
+const TEST_URI = "http://sub1.test2.example.com/browser/devtools/client/" +
+ "commandline/test/browser_cmd_appcache_valid_index.html";
+
+function test() {
+ return Task.spawn(spawnTest).then(finish, helpers.handleError);
+}
+
+function* spawnTest() {
+ let options = yield helpers.openTab(TEST_URI);
+
+ info("adding cache listener.");
+ // Wait for site to be cached.
+ yield helpers.listenOnce(gBrowser.contentWindow.applicationCache, "cached");
+
+ yield helpers.openToolbar(options);
+
+ // Pages containing an appcache the notification bar gives options to allow
+ // or deny permission for the app to save data offline. Let's click Allow.
+ let notificationID = "offline-app-requested-sub1.test2.example.com";
+ let notification = PopupNotifications.getNotification(notificationID, gBrowser.selectedBrowser);
+
+ if (notification) {
+ info("Authorizing offline storage.");
+ notification.mainAction.callback();
+ } else {
+ info("No notification box is available.");
+ }
+
+ info("Site now cached, running tests.");
+ yield helpers.audit(options, [
+ {
+ setup: "appcache",
+ check: {
+ input: "appcache",
+ markup: "IIIIIIII",
+ status: "ERROR",
+ args: {}
+ },
+ },
+
+ {
+ setup: function () {
+ Services.prefs.setBoolPref("browser.cache.disk.enable", false);
+ return helpers.setInput(options, "appcache list", 13);
+ },
+ check: {
+ input: "appcache list",
+ markup: "VVVVVVVVVVVVV",
+ status: "VALID",
+ args: {},
+ },
+ exec: {
+ output: [ /cache is disabled/ ]
+ },
+ post: function (output) {
+ Services.prefs.setBoolPref("browser.cache.disk.enable", true);
+ }
+ },
+
+ {
+ setup: "appcache list",
+ check: {
+ input: "appcache list",
+ markup: "VVVVVVVVVVVVV",
+ status: "VALID",
+ args: {},
+ },
+ exec: {
+ output: [ /index/, /page1/, /page2/, /page3/ ]
+ },
+ },
+
+ {
+ setup: "appcache list page",
+ check: {
+ input: "appcache list page",
+ markup: "VVVVVVVVVVVVVVVVVV",
+ status: "VALID",
+ args: {
+ search: { value: "page" },
+ }
+ },
+ exec: {
+ output: [ /page1/, /page2/, /page3/ ]
+ },
+ post: function (output, text) {
+ ok(!text.includes("index"), "index is not contained in output");
+ }
+ },
+
+ {
+ setup: "appcache validate",
+ check: {
+ input: "appcache validate",
+ markup: "VVVVVVVVVVVVVVVVV",
+ status: "VALID",
+ args: {}
+ },
+ exec: {
+ output: [ /successfully/ ]
+ },
+ },
+
+ {
+ setup: "appcache validate " + TEST_URI,
+ check: {
+ input: "appcache validate " + TEST_URI,
+ // appcache validate http://sub1.test2.example.com/browser/devtools/client/commandline/test/browser_cmd_appcache_valid_index.html
+ markup: "VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV",
+ status: "VALID",
+ args: {
+ uri: {
+ value: TEST_URI
+ },
+ }
+ },
+ exec: {
+ output: [ /successfully/ ]
+ },
+ },
+
+ {
+ setup: "appcache clear",
+ check: {
+ input: "appcache clear",
+ markup: "VVVVVVVVVVVVVV",
+ status: "VALID",
+ args: {},
+ },
+ exec: {
+ output: [ /successfully/ ]
+ },
+ },
+
+ {
+ setup: "appcache list",
+ check: {
+ input: "appcache list",
+ markup: "VVVVVVVVVVVVV",
+ status: "VALID",
+ args: {},
+ },
+ exec: {
+ output: [ /no results/ ]
+ },
+ post: function (output, text) {
+ ok(!text.includes("index"), "index is not contained in output");
+ ok(!text.includes("page1"), "page1 is not contained in output");
+ ok(!text.includes("page2"), "page1 is not contained in output");
+ ok(!text.includes("page3"), "page1 is not contained in output");
+ }
+ },
+
+ {
+ setup: "appcache viewentry --key " + TEST_URI,
+ check: {
+ input: "appcache viewentry --key " + TEST_URI,
+ // appcache viewentry --key http://sub1.test2.example.com/browser/devtools/client/commandline/test/browser_cmd_appcache_valid_index.html
+ markup: "VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV",
+ status: "VALID",
+ args: {}
+ },
+ },
+ ]);
+
+ yield helpers.closeToolbar(options);
+ yield helpers.closeTab(options);
+}
diff --git a/devtools/client/commandline/test/browser_cmd_appcache_valid_appcache.appcache b/devtools/client/commandline/test/browser_cmd_appcache_valid_appcache.appcache
new file mode 100644
index 000000000..4f62825e9
--- /dev/null
+++ b/devtools/client/commandline/test/browser_cmd_appcache_valid_appcache.appcache
@@ -0,0 +1,5 @@
+CACHE MANIFEST
+browser_cmd_appcache_valid_index.html
+browser_cmd_appcache_valid_page1.html
+browser_cmd_appcache_valid_page2.html
+browser_cmd_appcache_valid_page3.html
diff --git a/devtools/client/commandline/test/browser_cmd_appcache_valid_appcache.appcache^headers^ b/devtools/client/commandline/test/browser_cmd_appcache_valid_appcache.appcache^headers^
new file mode 100644
index 000000000..d1a0abd3f
--- /dev/null
+++ b/devtools/client/commandline/test/browser_cmd_appcache_valid_appcache.appcache^headers^
@@ -0,0 +1,2 @@
+Content-Type: text/cache-manifest
+Last-Modified: Tue, 23 Apr 9998 11:41:13 GMT
diff --git a/devtools/client/commandline/test/browser_cmd_appcache_valid_index.html b/devtools/client/commandline/test/browser_cmd_appcache_valid_index.html
new file mode 100644
index 000000000..1ab3f3e31
--- /dev/null
+++ b/devtools/client/commandline/test/browser_cmd_appcache_valid_index.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html manifest="browser_cmd_appcache_valid_appcache.appcache">
+ <head>
+ <meta charset="UTF-8">
+ <head>
+ <body>
+ <h1>Example index.html</h1>
+ <a href="browser_cmd_appcache_valid_index.html">Home</a> |
+ <a href="browser_cmd_appcache_valid_page1.html">Page 1</a> |
+ <a href="browser_cmd_appcache_valid_page2.html">Page 2</a> |
+ <a href="browser_cmd_appcache_valid_page3.html">Page 3</a>
+ </body>
+</html>
diff --git a/devtools/client/commandline/test/browser_cmd_appcache_valid_page1.html b/devtools/client/commandline/test/browser_cmd_appcache_valid_page1.html
new file mode 100644
index 000000000..e0bb429e7
--- /dev/null
+++ b/devtools/client/commandline/test/browser_cmd_appcache_valid_page1.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html manifest="browser_cmd_appcache_valid_appcache.appcache">
+ <head>
+ <meta charset="UTF-8">
+ <head>
+ <body>
+ <h1>Example page1.html</h1>
+ <a href="browser_cmd_appcache_valid_index.html">Home</a> |
+ <a href="browser_cmd_appcache_valid_page1.html">Page 1</a> |
+ <a href="browser_cmd_appcache_valid_page2.html">Page 2</a> |
+ <a href="browser_cmd_appcache_valid_page3.html">Page 3</a>
+ </body>
+</html>
diff --git a/devtools/client/commandline/test/browser_cmd_appcache_valid_page2.html b/devtools/client/commandline/test/browser_cmd_appcache_valid_page2.html
new file mode 100644
index 000000000..1ce36b319
--- /dev/null
+++ b/devtools/client/commandline/test/browser_cmd_appcache_valid_page2.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html manifest="browser_cmd_appcache_valid_appcache.appcache">
+ <head>
+ <meta charset="UTF-8">
+ <head>
+ <body>
+ <h1>Example page2.html</h1>
+ <a href="browser_cmd_appcache_valid_index.html">Home</a> |
+ <a href="browser_cmd_appcache_valid_page1.html">Page 1</a> |
+ <a href="browser_cmd_appcache_valid_page2.html">Page 2</a> |
+ <a href="browser_cmd_appcache_valid_page3.html">Page 3</a>
+ </body>
+</html>
diff --git a/devtools/client/commandline/test/browser_cmd_appcache_valid_page3.html b/devtools/client/commandline/test/browser_cmd_appcache_valid_page3.html
new file mode 100644
index 000000000..074ff7d41
--- /dev/null
+++ b/devtools/client/commandline/test/browser_cmd_appcache_valid_page3.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html manifest="browser_cmd_appcache_valid_appcache.appcache">
+ <head>
+ <meta charset="UTF-8">
+ <head>
+ <body>
+ <h1>Example page3.html</h1>
+ <a href="browser_cmd_appcache_valid_index.html">Home</a> |
+ <a href="browser_cmd_appcache_valid_page1.html">Page 1</a> |
+ <a href="browser_cmd_appcache_valid_page2.html">Page 2</a> |
+ <a href="browser_cmd_appcache_valid_page3.html">Page 3</a>
+ </body>
+</html>
diff --git a/devtools/client/commandline/test/browser_cmd_calllog.js b/devtools/client/commandline/test/browser_cmd_calllog.js
new file mode 100644
index 000000000..ebe10165f
--- /dev/null
+++ b/devtools/client/commandline/test/browser_cmd_calllog.js
@@ -0,0 +1,119 @@
+/* Any copyright is dedicated to the Public Domain.
+* http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the calllog commands works as they should
+
+const TEST_URI = "data:text/html;charset=utf-8,gcli-calllog";
+
+var tests = {};
+
+function test() {
+ return Task.spawn(function* () {
+ let options = yield helpers.openTab(TEST_URI);
+ yield helpers.openToolbar(options);
+
+ yield helpers.runTests(options, tests);
+
+ yield helpers.closeToolbar(options);
+ yield helpers.closeTab(options);
+ }).then(finish, helpers.handleError);
+}
+
+tests.testCallLogStatus = function (options) {
+ return helpers.audit(options, [
+ {
+ setup: "calllog",
+ check: {
+ input: "calllog",
+ hints: "",
+ markup: "IIIIIII",
+ status: "ERROR"
+ }
+ },
+ {
+ setup: "calllog start",
+ check: {
+ input: "calllog start",
+ hints: "",
+ markup: "VVVVVVVVVVVVV",
+ status: "VALID"
+ }
+ },
+ {
+ setup: "calllog stop",
+ check: {
+ input: "calllog stop",
+ hints: "",
+ markup: "VVVVVVVVVVVV",
+ status: "VALID"
+ }
+ },
+ ]);
+};
+
+tests.testCallLogExec = function (options) {
+ var deferred = promise.defer();
+
+ var onWebConsoleOpen = function (subject) {
+ Services.obs.removeObserver(onWebConsoleOpen, "web-console-created");
+
+ subject.QueryInterface(Ci.nsISupportsString);
+ let hud = HUDService.getHudReferenceById(subject.data);
+ ok(hud, "console open");
+
+ helpers.audit(options, [
+ {
+ setup: "calllog stop",
+ exec: {
+ output: /Stopped call logging/,
+ }
+ },
+ {
+ setup: "console clear",
+ exec: {
+ output: "",
+ },
+ post: function () {
+ let labels = hud.outputNode.querySelectorAll(".webconsole-msg-output");
+ is(labels.length, 0, "no output in console");
+ }
+ },
+ {
+ setup: "console close",
+ exec: {
+ output: "",
+ }
+ },
+ ]).then(function () {
+ deferred.resolve();
+ });
+ };
+ Services.obs.addObserver(onWebConsoleOpen, "web-console-created", false);
+
+ helpers.audit(options, [
+ {
+ setup: "calllog stop",
+ exec: {
+ output: /No call logging/,
+ }
+ },
+ {
+ name: "calllog start",
+ setup: function () {
+ // This test wants to be in a different event
+ var deferred = promise.defer();
+ executeSoon(function () {
+ helpers.setInput(options, "calllog start").then(() => {
+ deferred.resolve();
+ });
+ });
+ return deferred.promise;
+ },
+ exec: {
+ output: /Call logging started/,
+ },
+ },
+ ]);
+
+ return deferred.promise;
+};
diff --git a/devtools/client/commandline/test/browser_cmd_calllog_chrome.js b/devtools/client/commandline/test/browser_cmd_calllog_chrome.js
new file mode 100644
index 000000000..81d2dfd54
--- /dev/null
+++ b/devtools/client/commandline/test/browser_cmd_calllog_chrome.js
@@ -0,0 +1,116 @@
+/* Any copyright is dedicated to the Public Domain.
+* http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the calllog commands works as they should
+
+const TEST_URI = "data:text/html;charset=utf-8,cmd-calllog-chrome";
+
+var tests = {};
+
+function test() {
+ return Task.spawn(function* () {
+ let options = yield helpers.openTab(TEST_URI);
+ yield helpers.openToolbar(options);
+
+ yield helpers.runTests(options, tests);
+
+ yield helpers.closeToolbar(options);
+ yield helpers.closeTab(options);
+ }).then(finish, helpers.handleError);
+}
+
+tests.testCallLogStatus = function (options) {
+ return helpers.audit(options, [
+ {
+ setup: "calllog",
+ check: {
+ status: "ERROR",
+ emptyParameters: [ " " ]
+ }
+ },
+ {
+ setup: "calllog chromestop",
+ check: {
+ status: "VALID",
+ emptyParameters: [ " " ]
+ }
+ },
+ {
+ setup: "calllog chromestart content-variable window",
+ check: {
+ status: "VALID",
+ emptyParameters: [ " " ]
+ }
+ },
+ {
+ setup: "calllog chromestart javascript \"({a1: function() {this.a2()},a2: function() {}});\"",
+ check: {
+ status: "VALID",
+ emptyParameters: [ " " ]
+ }
+ },
+ ]);
+};
+
+tests.testCallLogExec = function (options) {
+ let deferred = promise.defer();
+
+ function onWebConsoleOpen(subject) {
+ Services.obs.removeObserver(onWebConsoleOpen, "web-console-created");
+
+ subject.QueryInterface(Ci.nsISupportsString);
+ let hud = HUDService.getHudReferenceById(subject.data);
+ ok(hud, "console open");
+
+ helpers.audit(options, [
+ {
+ setup: "calllog chromestop",
+ exec: {
+ output: /Stopped call logging/,
+ }
+ },
+ {
+ setup: "calllog chromestart javascript XXX",
+ exec: {
+ output: /following exception/,
+ }
+ },
+ {
+ setup: "console clear",
+ exec: {
+ output: "",
+ },
+ post: function () {
+ let labels = hud.jsterm.outputNode.querySelectorAll(".webconsole-msg-output");
+ is(labels.length, 0, "no output in console");
+ }
+ },
+ {
+ setup: "console close",
+ exec: {
+ output: "",
+ },
+ },
+ ]).then(function () {
+ deferred.resolve();
+ });
+ }
+ Services.obs.addObserver(onWebConsoleOpen, "web-console-created", false);
+
+ helpers.audit(options, [
+ {
+ setup: "calllog chromestop",
+ exec: {
+ output: /No call logging/
+ }
+ },
+ {
+ setup: "calllog chromestart javascript \"({a1: function() {this.a2()},a2: function() {}});\"",
+ exec: {
+ output: /Call logging started/,
+ }
+ },
+ ]);
+
+ return deferred.promise;
+};
diff --git a/devtools/client/commandline/test/browser_cmd_commands.js b/devtools/client/commandline/test/browser_cmd_commands.js
new file mode 100644
index 000000000..6c69034ec
--- /dev/null
+++ b/devtools/client/commandline/test/browser_cmd_commands.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test various GCLI commands
+
+const TEST_URI = "data:text/html;charset=utf-8,gcli-commands";
+const HUDService = require("devtools/client/webconsole/hudservice");
+
+// Use the old webconsole since pprint isn't working on new one (Bug 1304794)
+Services.prefs.setBoolPref("devtools.webconsole.new-frontend-enabled", false);
+registerCleanupFunction(function* () {
+ Services.prefs.clearUserPref("devtools.webconsole.new-frontend-enabled");
+});
+
+function test() {
+ return Task.spawn(spawnTest).then(finish, helpers.handleError);
+}
+
+function* spawnTest() {
+ let options = yield helpers.openTab(TEST_URI);
+ yield helpers.openToolbar(options);
+
+ let subjectPromise = helpers.observeOnce("web-console-created");
+
+ helpers.audit(options, [
+ {
+ setup: "console open",
+ exec: { }
+ }
+ ]);
+
+ let subject = yield subjectPromise;
+
+ subject.QueryInterface(Ci.nsISupportsString);
+ let hud = HUDService.getHudReferenceById(subject.data);
+ ok(hud, "console open");
+
+ let msg = yield hud.jsterm.execute("pprint(window)");
+
+ ok(msg, "output for pprint(window)");
+
+ yield helpers.audit(options, [
+ {
+ setup: "console clear",
+ exec: { output: "" }
+ }
+ ]);
+
+ let labels = hud.outputNode.querySelectorAll(".message");
+ is(labels.length, 0, "no output in console");
+
+ yield helpers.audit(options, [
+ {
+ setup: "console close",
+ exec: { output: "" }
+ }
+ ]);
+
+ ok(!HUDService.getHudReferenceById(hud.hudId), "console closed");
+
+ yield helpers.closeToolbar(options);
+ yield helpers.closeTab(options);
+}
diff --git a/devtools/client/commandline/test/browser_cmd_cookie.html b/devtools/client/commandline/test/browser_cmd_cookie.html
new file mode 100644
index 000000000..7688d034a
--- /dev/null
+++ b/devtools/client/commandline/test/browser_cmd_cookie.html
@@ -0,0 +1,19 @@
+<!doctype html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <title>GCLI cookie command test</title>
+</head>
+<body>
+
+ <p>Cookie test</p>
+ <p id=result></p>
+ <script type="text/javascript">
+ document.cookie = "zap=zep";
+ document.cookie = "zip=zop";
+ document.cookie = "zig=zag; domain=.mochi.test";
+ document.getElementById("result").innerHTML = document.cookie;
+ </script>
+
+</body>
+</html>
diff --git a/devtools/client/commandline/test/browser_cmd_cookie.js b/devtools/client/commandline/test/browser_cmd_cookie.js
new file mode 100644
index 000000000..37a8205cb
--- /dev/null
+++ b/devtools/client/commandline/test/browser_cmd_cookie.js
@@ -0,0 +1,170 @@
+/* Any copyright is dedicated to the Public Domain.
+* http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the cookie commands works as they should
+
+const TEST_URI = "http://example.com/browser/devtools/client/commandline/" +
+ "test/browser_cmd_cookie.html";
+
+function test() {
+ helpers.addTabWithToolbar(TEST_URI, function (options) {
+ return helpers.audit(options, [
+ {
+ setup: "cookie",
+ check: {
+ input: "cookie",
+ hints: " list",
+ markup: "IIIIII",
+ status: "ERROR"
+ },
+ },
+ {
+ setup: "cookie lis",
+ check: {
+ input: "cookie lis",
+ hints: "t",
+ markup: "IIIIIIVIII",
+ status: "ERROR"
+ },
+ },
+ {
+ setup: "cookie list",
+ check: {
+ input: "cookie list",
+ hints: "",
+ markup: "VVVVVVVVVVV",
+ status: "VALID"
+ },
+ },
+ {
+ setup: "cookie remove",
+ check: {
+ input: "cookie remove",
+ hints: " <name>",
+ markup: "VVVVVVVVVVVVV",
+ status: "ERROR"
+ },
+ },
+ {
+ setup: "cookie set",
+ check: {
+ input: "cookie set",
+ hints: " <name> <value> [options]",
+ markup: "VVVVVVVVVV",
+ status: "ERROR"
+ },
+ },
+ {
+ setup: "cookie set fruit",
+ check: {
+ input: "cookie set fruit",
+ hints: " <value> [options]",
+ markup: "VVVVVVVVVVVVVVVV",
+ status: "ERROR"
+ },
+ },
+ {
+ setup: "cookie set fruit ban",
+ check: {
+ input: "cookie set fruit ban",
+ hints: " [options]",
+ markup: "VVVVVVVVVVVVVVVVVVVV",
+ status: "VALID",
+ args: {
+ name: { value: "fruit" },
+ value: { value: "ban" },
+ secure: { value: false },
+ }
+ },
+ },
+ {
+ setup: 'cookie set fruit ban --path ""',
+ check: {
+ input: 'cookie set fruit ban --path ""',
+ hints: " [options]",
+ markup: "VVVVVVVVVVVVVVVVVVVVVVVVVVVVVV",
+ status: "VALID",
+ args: {
+ name: { value: "fruit" },
+ value: { value: "ban" },
+ path: { value: "" },
+ secure: { value: false },
+ }
+ },
+ },
+ {
+ setup: "cookie list",
+ exec: {
+ output: [ /zap=zep/, /zip=zop/, /Edit/ ]
+ }
+ },
+ {
+ setup: "cookie set zup banana",
+ check: {
+ args: {
+ name: { value: "zup" },
+ value: { value: "banana" },
+ }
+ },
+ exec: {
+ output: ""
+ }
+ },
+ {
+ setup: "cookie list",
+ exec: {
+ output: [ /zap=zep/, /zip=zop/, /zup=banana/, /Edit/ ]
+ }
+ },
+ {
+ setup: "cookie remove zip",
+ exec: { },
+ },
+ {
+ setup: "cookie list",
+ exec: {
+ output: [ /zap=zep/, /zup=banana/, /Edit/ ]
+ },
+ post: function (output, text) {
+ ok(!text.includes("zip"), "");
+ ok(!text.includes("zop"), "");
+ }
+ },
+ {
+ setup: "cookie remove zap",
+ exec: { },
+ },
+ {
+ setup: "cookie list",
+ exec: {
+ output: [ /zup=banana/, /Edit/ ]
+ },
+ post: function (output, text) {
+ ok(!text.includes("zap"), "");
+ ok(!text.includes("zep"), "");
+ ok(!text.includes("zip"), "");
+ ok(!text.includes("zop"), "");
+ }
+ },
+ {
+ setup: "cookie remove zup",
+ exec: { }
+ },
+ {
+ setup: "cookie list",
+ exec: {
+ output: "No cookies found for host example.com"
+ },
+ post: function (output, text) {
+ ok(!text.includes("zap"), "");
+ ok(!text.includes("zep"), "");
+ ok(!text.includes("zip"), "");
+ ok(!text.includes("zop"), "");
+ ok(!text.includes("zup"), "");
+ ok(!text.includes("banana"), "");
+ ok(!text.includes("Edit"), "");
+ }
+ },
+ ]);
+ }).then(finish, helpers.handleError);
+}
diff --git a/devtools/client/commandline/test/browser_cmd_cookie_host.js b/devtools/client/commandline/test/browser_cmd_cookie_host.js
new file mode 100644
index 000000000..c20294027
--- /dev/null
+++ b/devtools/client/commandline/test/browser_cmd_cookie_host.js
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the cookie command works for host with a port specified
+
+const TEST_URI = "http://mochi.test:8888/browser/devtools/client/commandline/" +
+ "test/browser_cmd_cookie.html";
+
+function test() {
+ helpers.addTabWithToolbar(TEST_URI, function (options) {
+ return helpers.audit(options, [
+ {
+ setup: "cookie list",
+ exec: {
+ output: [ /zap=zep/, /zip=zop/, /zig=zag/ ],
+ }
+ },
+ {
+ setup: "cookie set zup banana",
+ check: {
+ args: {
+ name: { value: "zup" },
+ value: { value: "banana" },
+ }
+ },
+ exec: {
+ output: ""
+ }
+ },
+ {
+ setup: "cookie list",
+ exec: {
+ output: [ /zap=zep/, /zip=zop/, /zig=zag/, /zup=banana/, /Edit/ ]
+ }
+ }
+ ]);
+ }).then(finish, helpers.handleError);
+}
+
diff --git a/devtools/client/commandline/test/browser_cmd_csscoverage_oneshot.js b/devtools/client/commandline/test/browser_cmd_csscoverage_oneshot.js
new file mode 100644
index 000000000..3394045f5
--- /dev/null
+++ b/devtools/client/commandline/test/browser_cmd_csscoverage_oneshot.js
@@ -0,0 +1,318 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the addon commands works as they should
+
+const csscoverage = require("devtools/shared/fronts/csscoverage");
+
+const PAGE_1 = TEST_BASE_HTTPS + "browser_cmd_csscoverage_page1.html";
+const PAGE_2 = TEST_BASE_HTTPS + "browser_cmd_csscoverage_page2.html";
+const PAGE_3 = TEST_BASE_HTTPS + "browser_cmd_csscoverage_page3.html";
+
+const SHEET_A = TEST_BASE_HTTPS + "browser_cmd_csscoverage_sheetA.css";
+const SHEET_B = TEST_BASE_HTTPS + "browser_cmd_csscoverage_sheetB.css";
+const SHEET_C = TEST_BASE_HTTPS + "browser_cmd_csscoverage_sheetC.css";
+const SHEET_D = TEST_BASE_HTTPS + "browser_cmd_csscoverage_sheetD.css";
+
+add_task(function* () {
+ let options = yield helpers.openTab(PAGE_3);
+ yield helpers.openToolbar(options);
+
+ let usage = yield csscoverage.getUsage(options.target);
+
+ yield navigate(usage, options);
+ yield checkPages(usage);
+ yield checkEditorReport(usage);
+ // usage.createPageReport is not supported for usage.oneshot data as of
+ // bug 1035300 because the page report assumed we have preload data which
+ // oneshot can't gather. The ideal solution is to have a special no-preload
+ // mode for the page report, but since oneshot isn't needed for the UI to
+ // function, we're currently not supporting page report for oneshot data
+ // yield checkPageReport(usage);
+
+ yield helpers.closeToolbar(options);
+ yield helpers.closeTab(options);
+});
+
+/**
+ * Just check current page
+ */
+function* navigate(usage, options) {
+ ok(!usage.isRunning(), "csscoverage is not running");
+
+ yield usage.oneshot();
+
+ ok(!usage.isRunning(), "csscoverage is still not running");
+}
+
+/**
+ * Check the expected pages have been visited
+ */
+function* checkPages(usage) {
+ let expectedVisited = [ PAGE_3 ];
+ let actualVisited = yield usage._testOnlyVisitedPages();
+ isEqualJson(actualVisited, expectedVisited, "Visited");
+}
+
+/**
+ * Check that createEditorReport returns the expected JSON
+ */
+function* checkEditorReport(usage) {
+ // Page1
+ let expectedPage1 = { reports: [] };
+ let actualPage1 = yield usage.createEditorReport(PAGE_1 + " \u2192 <style> index 0");
+ isEqualJson(actualPage1, expectedPage1, "Page1");
+
+ // Page2
+ let expectedPage2 = { reports: [] };
+ let actualPage2 = yield usage.createEditorReport(PAGE_2 + " \u2192 <style> index 0");
+ isEqualJson(actualPage2, expectedPage2, "Page2");
+
+ // Page3a
+ let expectedPage3a = {
+ reports: [
+ {
+ selectorText: ".page3-test2",
+ start: { line: 9, column: 5 },
+ }
+ ]
+ };
+ let actualPage3a = yield usage.createEditorReport(PAGE_3 + " \u2192 <style> index 0");
+ isEqualJson(actualPage3a, expectedPage3a, "Page3a");
+
+ // Page3b
+ let expectedPage3b = {
+ reports: [
+ {
+ selectorText: ".page3-test3",
+ start: { line: 3, column: 5 },
+ }
+ ]
+ };
+ let actualPage3b = yield usage.createEditorReport(PAGE_3 + " \u2192 <style> index 1");
+ isEqualJson(actualPage3b, expectedPage3b, "Page3b");
+
+ // SheetA
+ let expectedSheetA = {
+ reports: [
+ {
+ selectorText: ".sheetA-test2",
+ start: { line: 8, column: 1 },
+ },
+ {
+ selectorText: ".sheetA-test3",
+ start: { line: 12, column: 1 },
+ },
+ {
+ selectorText: ".sheetA-test4",
+ start: { line: 16, column: 1 },
+ }
+ ]
+ };
+ let actualSheetA = yield usage.createEditorReport(SHEET_A);
+ isEqualJson(actualSheetA, expectedSheetA, "SheetA");
+
+ // SheetB
+ let expectedSheetB = {
+ reports: [
+ {
+ selectorText: ".sheetB-test2",
+ start: { line: 6, column: 1 },
+ },
+ {
+ selectorText: ".sheetB-test3",
+ start: { line: 10, column: 1 },
+ },
+ {
+ selectorText: ".sheetB-test4",
+ start: { line: 14, column: 1 },
+ }
+ ]
+ };
+ let actualSheetB = yield usage.createEditorReport(SHEET_B);
+ isEqualJson(actualSheetB, expectedSheetB, "SheetB");
+
+ // SheetC
+ let expectedSheetC = {
+ reports: [
+ {
+ selectorText: ".sheetC-test2",
+ start: { line: 6, column: 1 },
+ },
+ {
+ selectorText: ".sheetC-test3",
+ start: { line: 10, column: 1 },
+ },
+ {
+ selectorText: ".sheetC-test4",
+ start: { line: 14, column: 1 },
+ }
+ ]
+ };
+ let actualSheetC = yield usage.createEditorReport(SHEET_C);
+ isEqualJson(actualSheetC, expectedSheetC, "SheetC");
+
+ // SheetD
+ let expectedSheetD = {
+ reports: [
+ {
+ selectorText: ".sheetD-test2",
+ start: { line: 6, column: 1 },
+ },
+ {
+ selectorText: ".sheetD-test3",
+ start: { line: 10, column: 1 },
+ },
+ {
+ selectorText: ".sheetD-test4",
+ start: { line: 14, column: 1 },
+ }
+ ]
+ };
+ let actualSheetD = yield usage.createEditorReport(SHEET_D);
+ isEqualJson(actualSheetD, expectedSheetD, "SheetD");
+}
+
+/**
+ * Check that checkPageReport returns the expected JSON
+ */
+function* checkPageReport(usage) {
+ let actualReport = yield usage.createPageReport();
+
+ // Quick check on trivial things. See doc comment for checkRuleProperties
+ actualReport.preload.forEach(page => page.rules.forEach(checkRuleProperties));
+ actualReport.unused.forEach(page => page.rules.forEach(checkRuleProperties));
+
+ // Check the summary
+ let expectedSummary = { "used": 23, "unused": 9, "preload": 0 };
+ isEqualJson(actualReport.summary, expectedSummary, "summary");
+
+ // Check the preload header
+ isEqualJson(actualReport.preload.length, 0, "preload length");
+
+ // Check the unused header
+ isEqualJson(actualReport.unused.length, 6, "unused length");
+
+ // Check the unused rules
+ isEqualJson(actualReport.unused[0].url, PAGE_3 + " \u2192 <style> index 0", "unused url 0");
+ let expectedUnusedRules0 = [
+ {
+ "url": PAGE_3 + " \u2192 <style> index 0",
+ "start": { "line": 9, "column": 5 },
+ "selectorText": ".page3-test2"
+ }
+ ];
+ isEqualJson(actualReport.unused[0].rules, expectedUnusedRules0, "unused rules 0");
+
+ isEqualJson(actualReport.unused[1].url, PAGE_3 + " \u2192 <style> index 1", "unused url 1");
+ let expectedUnusedRules1 = [
+ {
+ "url": PAGE_3 + " \u2192 <style> index 1",
+ "start": { "line": 3, "column": 5 },
+ "selectorText": ".page3-test3"
+ }
+ ];
+ isEqualJson(actualReport.unused[1].rules, expectedUnusedRules1, "unused rules 1");
+
+ isEqualJson(actualReport.unused[2].url, SHEET_A, "unused url 2");
+ let expectedUnusedRules2 = [
+ {
+ "url": SHEET_A,
+ "start": { "line": 8, "column": 1 },
+ "selectorText": ".sheetA-test2"
+ },
+ {
+ "url": SHEET_A,
+ "start": { "line": 12, "column": 1 },
+ "selectorText": ".sheetA-test3"
+ },
+ {
+ "url": SHEET_A,
+ "start": { "line": 16, "column": 1 },
+ "selectorText": ".sheetA-test4"
+ }
+ ];
+ isEqualJson(actualReport.unused[2].rules, expectedUnusedRules2, "unused rules 2");
+
+ isEqualJson(actualReport.unused[3].url, SHEET_B, "unused url 3");
+ let expectedUnusedRules3 = [
+ {
+ "url": SHEET_B,
+ "start": { "line": 6, "column": 1 },
+ "selectorText": ".sheetB-test2"
+ },
+ {
+ "url": SHEET_B,
+ "start": { "line": 10, "column": 1 },
+ "selectorText": ".sheetB-test3"
+ },
+ {
+ "url": SHEET_B,
+ "start": { "line": 14, "column": 1 },
+ "selectorText": ".sheetB-test4"
+ }
+ ];
+ isEqualJson(actualReport.unused[3].rules, expectedUnusedRules3, "unused rules 3");
+
+ isEqualJson(actualReport.unused[4].url, SHEET_D, "unused url 4");
+ let expectedUnusedRules4 = [
+ {
+ "url": SHEET_D,
+ "start": { "line": 6, "column": 1 },
+ "selectorText": ".sheetD-test2"
+ },
+ {
+ "url": SHEET_D,
+ "start": { "line": 10, "column": 1 },
+ "selectorText": ".sheetD-test3"
+ },
+ {
+ "url": SHEET_D,
+ "start": { "line": 14, "column": 1 },
+ "selectorText": ".sheetD-test4"
+ }
+ ];
+ isEqualJson(actualReport.unused[4].rules, expectedUnusedRules4, "unused rules 4");
+
+ isEqualJson(actualReport.unused[5].url, SHEET_C, "unused url 5");
+ let expectedUnusedRules5 = [
+ {
+ "url": SHEET_C,
+ "start": { "line": 6, "column": 1 },
+ "selectorText": ".sheetC-test2"
+ },
+ {
+ "url": SHEET_C,
+ "start": { "line": 10, "column": 1 },
+ "selectorText": ".sheetC-test3"
+ },
+ {
+ "url": SHEET_C,
+ "start": { "line": 14, "column": 1 },
+ "selectorText": ".sheetC-test4"
+ }
+ ];
+ isEqualJson(actualReport.unused[5].rules, expectedUnusedRules5, "unused rules 5");
+}
+
+/**
+ * We do basic tests on the shortUrl and formattedCssText because they are
+ * very derivative, and so make for fragile tests, and having done those quick
+ * existence checks we remove them so the JSON check later can ignore them
+ */
+function checkRuleProperties(rule, index) {
+ is(typeof rule.shortUrl, "string", "typeof rule.shortUrl for " + index);
+ is(rule.shortUrl.indexOf("http://"), -1, "http not in rule.shortUrl for" + index);
+ delete rule.shortUrl;
+
+ is(typeof rule.formattedCssText, "string", "typeof rule.formattedCssText for " + index);
+ ok(rule.formattedCssText.indexOf("{") > 0, "{ in rule.formattedCssText for " + index);
+ delete rule.formattedCssText;
+}
+
+/**
+ * Utility to compare JSON structures
+ */
+function isEqualJson(o1, o2, msg) {
+ is(JSON.stringify(o1), JSON.stringify(o2), msg);
+}
diff --git a/devtools/client/commandline/test/browser_cmd_csscoverage_page1.html b/devtools/client/commandline/test/browser_cmd_csscoverage_page1.html
new file mode 100644
index 000000000..b137ac1e7
--- /dev/null
+++ b/devtools/client/commandline/test/browser_cmd_csscoverage_page1.html
@@ -0,0 +1,85 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <!--
+ First page of the css coverage test.
+ * Contains page2 in an iframe
+ * Forwards to page2 on a timeout
+ -->
+ <title>Page 1</title>
+ <style>
+ @import url(browser_cmd_csscoverage_sheetD.css);
+ /* This should match below */
+ .page1-test1 {
+ color: #011;
+ }
+ /* This should not match below */
+ .page1-test2 {
+ color: #012;
+ }
+ /* This would match if the mouse was in the right place */
+ .page1-test3:hover {
+ color: #013;
+ }
+ /* This can't match because it's illegal */
+ .page1-test4:broken {
+ color: #014;
+ }
+ /* This doesn't match until the event fires */
+ .page1-test5 {
+ color: #015;
+ }
+ /* TODO: include examples of all CSS rules in
+ https://developer.mozilla.org/en-US/docs/Web/API/CSSRule
+ and include tests for rules nested in media rules, etc */
+
+ /* We're not testing unparable CSS right now */
+ </style>
+ <link rel="stylesheet" type="text/css" href="browser_cmd_csscoverage_sheetA.css">
+ <link rel="stylesheet" type="text/css" href="browser_cmd_csscoverage_sheetB.css">
+ <script type="application/javascript;version=1.8">
+ /* How quickly do we rush through this? */
+ let delay = 500;
+ window.addEventListener("load", () => {
+ dump('TEST-INFO | load from browser_cmd_csscoverage_page1.html\n');
+ setTimeout(() => {
+ dump('TEST-INFO | timeout from browser_cmd_csscoverage_page1.html\n');
+ /* This adds <div class=page1-test5></div> */
+ let parent = document.querySelector("#page1-test5-holder");
+ let child = document.createElement("div");
+ child.classList.add("page1-test5");
+ parent.appendChild(child);
+
+ /* Then navigate to the next step */
+ window.location.href = "browser_cmd_csscoverage_page3.html"
+ }, delay);
+ });
+ </script>
+</head>
+<body>
+
+<h2>Page 1</h2>
+
+<div class=page1-test1>.page1-test1</div>
+<div class=page1-test3>.page1-test3</div>
+
+<div id=page1-test5-holder></div>
+
+<div class=sheetA-test1>.sheetA-test1</div>
+<div class=sheetA-test3>.sheetA-test3</div>
+<div class=sheetB-test1>.sheetB-test1</div>
+<div class=sheetB-test3>.sheetB-test3</div>
+<div class=sheetC-test1>.sheetC-test1</div>
+<div class=sheetC-test3>.sheetC-test3</div>
+<div class=sheetD-test1>.sheetD-test1</div>
+<div class=sheetD-test3>.sheetD-test3</div>
+
+<iframe src=browser_cmd_csscoverage_page2.html></iframe>
+
+<p>
+ <a href="browser_cmd_csscoverage_page3.html">Page 3</a>
+</p>
+
+</body>
+</html>
diff --git a/devtools/client/commandline/test/browser_cmd_csscoverage_page2.html b/devtools/client/commandline/test/browser_cmd_csscoverage_page2.html
new file mode 100644
index 000000000..13fa8697c
--- /dev/null
+++ b/devtools/client/commandline/test/browser_cmd_csscoverage_page2.html
@@ -0,0 +1,59 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Page 2</title>
+ <style>
+ @import url(browser_cmd_csscoverage_sheetD.css);
+
+ /* This should match below */
+ .page2-test1 {
+ color: #021;
+ }
+ /* This should not match below */
+ .page2-test2 {
+ color: #022;
+ }
+ /* This doesn't match until the event fires */
+ .page2-test3 {
+ color: #023;
+ }
+ </style>
+
+ <link rel="stylesheet" type="text/css" href="browser_cmd_csscoverage_sheetA.css">
+ <link rel="stylesheet" type="text/css" href="browser_cmd_csscoverage_sheetB.css">
+ <script type="application/javascript;version=1.8">
+ /* How quickly do we rush through this? */
+ let delay = 500;
+ window.addEventListener("load", () => {
+ dump('TEST-INFO | load from browser_cmd_csscoverage_page2.html\n');
+ setTimeout(() => {
+ dump('TEST-INFO | timeout from browser_cmd_csscoverage_page2.html\n');
+ /* This adds <div class=page2-test3></div> */
+ let parent = document.querySelector("#page2-test3-holder");
+ let child = document.createElement("div");
+ child.classList.add("page2-test3");
+ parent.appendChild(child);
+ }, delay);
+ });
+ </script>
+</head>
+<body>
+
+<h2>Page 2</h2>
+
+<div class=page2-test1>.page2-test1</div>
+
+<div id=page2-test3-holder></div>
+
+<div class=sheetA-test1>.sheetA-test1</div>
+<div class=sheetA-test4>.sheetA-test4</div>
+<div class=sheetB-test1>.sheetB-test1</div>
+<div class=sheetB-test4>.sheetB-test4</div>
+<div class=sheetC-test1>.sheetC-test1</div>
+<div class=sheetC-test4>.sheetC-test4</div>
+<div class=sheetD-test1>.sheetD-test1</div>
+<div class=sheetD-test4>.sheetD-test4</div>
+
+</body>
+</html>
diff --git a/devtools/client/commandline/test/browser_cmd_csscoverage_page3.html b/devtools/client/commandline/test/browser_cmd_csscoverage_page3.html
new file mode 100644
index 000000000..4dc91d5b2
--- /dev/null
+++ b/devtools/client/commandline/test/browser_cmd_csscoverage_page3.html
@@ -0,0 +1,52 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Page 3</title>
+ <style>
+ @import url(browser_cmd_csscoverage_sheetD.css);
+
+ /* This should match below */
+ .page3-test1 {
+ color: #031;
+ }
+ /* This should not match below */
+ .page3-test2 {
+ color: #032;
+ }
+ </style>
+ <style>
+ /* This also should not match below, but in a second inline sheet */
+ .page3-test3 {
+ color: #033;
+ }
+ </style>
+ <link rel="stylesheet" type="text/css" href="browser_cmd_csscoverage_sheetA.css">
+ <link rel="stylesheet" type="text/css" href="browser_cmd_csscoverage_sheetB.css">
+ <script type="application/javascript;version=1.8">
+ window.addEventListener("load", () => {
+ dump('TEST-INFO | load from browser_cmd_csscoverage_page3.html\n');
+ });
+ </script>
+</head>
+<body>
+
+<h2>Page 3</h2>
+
+<div class=page3-test1>.page3-test1</div>
+
+<div class=sheetA-test1>.sheetA-test1</div>
+<div class=sheetA-test5>.sheetA-test5</div>
+<div class=sheetB-test1>.sheetB-test1</div>
+<div class=sheetB-test5>.sheetB-test5</div>
+<div class=sheetC-test1>.sheetC-test1</div>
+<div class=sheetC-test5>.sheetC-test5</div>
+<div class=sheetD-test1>.sheetD-test1</div>
+<div class=sheetD-test5>.sheetD-test5</div>
+
+<p>
+ <a href="browser_cmd_csscoverage_page1.html">Page 1</a>
+</p>
+
+</body>
+</html>
diff --git a/devtools/client/commandline/test/browser_cmd_csscoverage_sheetA.css b/devtools/client/commandline/test/browser_cmd_csscoverage_sheetA.css
new file mode 100644
index 000000000..1a3bac926
--- /dev/null
+++ b/devtools/client/commandline/test/browser_cmd_csscoverage_sheetA.css
@@ -0,0 +1,22 @@
+@import url(browser_cmd_csscoverage_sheetC.css);
+
+/* This should match in page 1, 2 and 3 */
+.sheetA-test1 {
+ color: #0A1;
+}
+/* This should not match anywhere */
+.sheetA-test2 {
+ color: #0A2;
+}
+/* This should match in page 1 only */
+.sheetA-test3 {
+ color: #0A3;
+}
+/* This should match in page 2 only */
+.sheetA-test4 {
+ color: #0A4;
+}
+/* This should match in page 3 only */
+.sheetA-test5 {
+ color: #0A5;
+}
diff --git a/devtools/client/commandline/test/browser_cmd_csscoverage_sheetB.css b/devtools/client/commandline/test/browser_cmd_csscoverage_sheetB.css
new file mode 100644
index 000000000..9335bd60d
--- /dev/null
+++ b/devtools/client/commandline/test/browser_cmd_csscoverage_sheetB.css
@@ -0,0 +1,20 @@
+/* This should match in page 1, 2 and 3 */
+.sheetB-test1 {
+ color: #0B1;
+}
+/* This should not match anywhere */
+.sheetB-test2 {
+ color: #0B2;
+}
+/* This should match in page 1 only */
+.sheetB-test3 {
+ color: #0B3;
+}
+/* This should match in page 2 only */
+.sheetB-test4 {
+ color: #0B4;
+}
+/* This should match in page 3 only */
+.sheetB-test5 {
+ color: #0B5;
+}
diff --git a/devtools/client/commandline/test/browser_cmd_csscoverage_sheetC.css b/devtools/client/commandline/test/browser_cmd_csscoverage_sheetC.css
new file mode 100644
index 000000000..8c899ead9
--- /dev/null
+++ b/devtools/client/commandline/test/browser_cmd_csscoverage_sheetC.css
@@ -0,0 +1,20 @@
+/* This should match in page 1, 2 and 3 */
+.sheetC-test1 {
+ color: #0C1;
+}
+/* This should not match anywhere */
+.sheetC-test2 {
+ color: #0C2;
+}
+/* This should match in page 1 only */
+.sheetC-test3 {
+ color: #0C3;
+}
+/* This should match in page 2 only */
+.sheetC-test4 {
+ color: #0C4;
+}
+/* This should match in page 3 only */
+.sheetC-test5 {
+ color: #0C5;
+}
diff --git a/devtools/client/commandline/test/browser_cmd_csscoverage_sheetD.css b/devtools/client/commandline/test/browser_cmd_csscoverage_sheetD.css
new file mode 100644
index 000000000..60ebb314a
--- /dev/null
+++ b/devtools/client/commandline/test/browser_cmd_csscoverage_sheetD.css
@@ -0,0 +1,20 @@
+/* This should match in page 1, 2 and 3 */
+.sheetD-test1 {
+ color: #0D1;
+}
+/* This should not match anywhere */
+.sheetD-test2 {
+ color: #0D2;
+}
+/* This should match in page 1 only */
+.sheetD-test3 {
+ color: #0D3;
+}
+/* This should match in page 2 only */
+.sheetD-test4 {
+ color: #0D4;
+}
+/* This should match in page 3 only */
+.sheetD-test5 {
+ color: #0D5;
+}
diff --git a/devtools/client/commandline/test/browser_cmd_csscoverage_startstop.js b/devtools/client/commandline/test/browser_cmd_csscoverage_startstop.js
new file mode 100644
index 000000000..2bdb86d86
--- /dev/null
+++ b/devtools/client/commandline/test/browser_cmd_csscoverage_startstop.js
@@ -0,0 +1,465 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the addon commands works as they should
+
+const csscoverage = require("devtools/shared/fronts/csscoverage");
+
+const PAGE_1 = TEST_BASE_HTTPS + "browser_cmd_csscoverage_page1.html";
+const PAGE_2 = TEST_BASE_HTTPS + "browser_cmd_csscoverage_page2.html";
+const PAGE_3 = TEST_BASE_HTTPS + "browser_cmd_csscoverage_page3.html";
+
+const SHEET_A = TEST_BASE_HTTPS + "browser_cmd_csscoverage_sheetA.css";
+const SHEET_B = TEST_BASE_HTTPS + "browser_cmd_csscoverage_sheetB.css";
+const SHEET_C = TEST_BASE_HTTPS + "browser_cmd_csscoverage_sheetC.css";
+const SHEET_D = TEST_BASE_HTTPS + "browser_cmd_csscoverage_sheetD.css";
+
+add_task(function* () {
+ let options = yield helpers.openTab("about:blank");
+ yield helpers.openToolbar(options);
+
+ let usage = yield csscoverage.getUsage(options.target);
+
+ yield navigate(usage, options);
+ yield checkPages(usage);
+ yield checkEditorReport(usage);
+ yield checkPageReport(usage);
+
+ yield helpers.closeToolbar(options);
+ yield helpers.closeTab(options);
+});
+
+/**
+ * Visit all the pages in the test
+ */
+function* navigate(usage, options) {
+ yield usage.start(options.chromeWindow, options.target);
+
+ ok(usage.isRunning(), "csscoverage is running");
+
+ // Load page 1.
+ options.browser.loadURI(PAGE_1);
+ // And wait until page 1 and page 2 (an iframe inside page 1) are both loaded.
+ yield Promise.all([
+ BrowserTestUtils.browserLoaded(options.browser, false, PAGE_1),
+ BrowserTestUtils.browserLoaded(options.browser, true, PAGE_2)
+ ]);
+ is(options.browser.currentURI.spec, PAGE_1, "page 1 loaded");
+
+ // page 2 has JS that navigates to page 3 after a timeout.
+ yield BrowserTestUtils.browserLoaded(options.browser, false, PAGE_3);
+ is(options.browser.currentURI.spec, PAGE_3, "page 3 loaded");
+
+ let toolboxReady = gDevTools.once("toolbox-ready");
+
+ yield usage.stop();
+
+ ok(!usage.isRunning(), "csscoverage not is running");
+
+ yield toolboxReady;
+}
+
+/**
+ * Check the expected pages have been visited
+ */
+function* checkPages(usage) {
+ // 'load' event order. '' is for the initial location
+ let expectedVisited = [ "", PAGE_2, PAGE_1, PAGE_3 ];
+ let actualVisited = yield usage._testOnlyVisitedPages();
+ isEqualJson(actualVisited, expectedVisited, "Visited");
+}
+
+/**
+ * Check that createEditorReport returns the expected JSON
+ */
+function* checkEditorReport(usage) {
+ // Page1
+ let expectedPage1 = {
+ reports: [
+ {
+ selectorText: ".page1-test2",
+ start: { line: 8, column: 5 },
+ }
+ ]
+ };
+ let actualPage1 = yield usage.createEditorReport(PAGE_1 + " \u2192 <style> index 0");
+ isEqualJson(actualPage1, expectedPage1, "Page1");
+
+ // Page2
+ let expectedPage2 = {
+ reports: [
+ {
+ selectorText: ".page2-test2",
+ start: { line: 9, column: 5 },
+ },
+ ]
+ };
+ let actualPage2 = yield usage.createEditorReport(PAGE_2 + " \u2192 <style> index 0");
+ isEqualJson(actualPage2, expectedPage2, "Page2");
+
+ // Page3a
+ let expectedPage3a = {
+ reports: [
+ {
+ selectorText: ".page3-test2",
+ start: { line: 9, column: 5 },
+ }
+ ]
+ };
+ let actualPage3a = yield usage.createEditorReport(PAGE_3 + " \u2192 <style> index 0");
+ isEqualJson(actualPage3a, expectedPage3a, "Page3a");
+
+ // Page3b
+ let expectedPage3b = {
+ reports: [
+ {
+ selectorText: ".page3-test3",
+ start: { line: 3, column: 5 },
+ }
+ ]
+ };
+ let actualPage3b = yield usage.createEditorReport(PAGE_3 + " \u2192 <style> index 1");
+ isEqualJson(actualPage3b, expectedPage3b, "Page3b");
+
+ // SheetA
+ let expectedSheetA = {
+ reports: [
+ {
+ selectorText: ".sheetA-test2",
+ start: { line: 8, column: 1 },
+ }
+ ]
+ };
+ let actualSheetA = yield usage.createEditorReport(SHEET_A);
+ isEqualJson(actualSheetA, expectedSheetA, "SheetA");
+
+ // SheetB
+ let expectedSheetB = {
+ reports: [
+ {
+ selectorText: ".sheetB-test2",
+ start: { line: 6, column: 1 },
+ }
+ ]
+ };
+ let actualSheetB = yield usage.createEditorReport(SHEET_B);
+ isEqualJson(actualSheetB, expectedSheetB, "SheetB");
+
+ // SheetC
+ let expectedSheetC = {
+ reports: [
+ {
+ selectorText: ".sheetC-test2",
+ start: { line: 6, column: 1 },
+ }
+ ]
+ };
+ let actualSheetC = yield usage.createEditorReport(SHEET_C);
+ isEqualJson(actualSheetC, expectedSheetC, "SheetC");
+
+ // SheetD
+ let expectedSheetD = {
+ reports: [
+ {
+ selectorText: ".sheetD-test2",
+ start: { line: 6, column: 1 },
+ }
+ ]
+ };
+ let actualSheetD = yield usage.createEditorReport(SHEET_D);
+ isEqualJson(actualSheetD, expectedSheetD, "SheetD");
+}
+
+/**
+ * Check that checkPageReport returns the expected JSON
+ */
+function* checkPageReport(usage) {
+ let actualReport = yield usage.createPageReport();
+
+ // Quick check on trivial things. See doc comment for checkRuleProperties
+ actualReport.preload.forEach(page => page.rules.forEach(checkRuleProperties));
+ actualReport.unused.forEach(page => page.rules.forEach(checkRuleProperties));
+
+ // Check the summary
+ let expectedSummary = { "used": 92, "unused": 22, "preload": 28 };
+ isEqualJson(actualReport.summary, expectedSummary, "summary");
+
+ checkPageReportPreload(actualReport);
+ checkPageReportUnused(actualReport);
+}
+
+/**
+ * Check that checkPageReport returns the expected preload JSON
+ */
+function checkPageReportPreload(actualReport) {
+ // Check the preload header
+ isEqualJson(actualReport.preload.length, 3, "preload length");
+
+ // Check the preload rules
+ isEqualJson(actualReport.preload[0].url, PAGE_2, "preload url 0");
+ let expectedPreloadRules0 = [
+ // TODO: This is already pre-loaded, we should note this
+ {
+ url: PAGE_2 + " \u2192 <style> index 0",
+ start: { line: 5, column: 5 },
+ selectorText: ".page2-test1"
+ },
+ {
+ url: SHEET_A,
+ start: { line: 4, column: 1 },
+ selectorText: ".sheetA-test1"
+ },
+ {
+ url: SHEET_A,
+ start: { line: 16, column: 1 },
+ selectorText: ".sheetA-test4"
+ },
+ {
+ url: SHEET_B,
+ start: { line: 2, column: 1 },
+ selectorText: ".sheetB-test1"
+ },
+ {
+ url: SHEET_B,
+ start: { line: 14, column: 1 },
+ selectorText: ".sheetB-test4"
+ },
+ {
+ url: SHEET_D,
+ start: { line: 2, column: 1 },
+ selectorText: ".sheetD-test1"
+ },
+ {
+ url: SHEET_D,
+ start: { line: 14, column: 1 },
+ selectorText: ".sheetD-test4"
+ },
+ {
+ url: SHEET_C,
+ start: { line: 2, column: 1 },
+ selectorText: ".sheetC-test1"
+ },
+ {
+ url: SHEET_C,
+ start: { line: 14, column: 1 },
+ selectorText: ".sheetC-test4"
+ }
+ ];
+ isEqualJson(actualReport.preload[0].rules, expectedPreloadRules0, "preload rules 0");
+
+ isEqualJson(actualReport.preload[1].url, PAGE_1, "preload url 1");
+ let expectedPreloadRules1 = [
+ {
+ url: SHEET_A,
+ start: { line: 4, column: 1 },
+ selectorText: ".sheetA-test1"
+ },
+ {
+ url: SHEET_A,
+ start: { line: 12, column: 1 },
+ selectorText: ".sheetA-test3"
+ },
+ {
+ url: SHEET_B,
+ start: { line: 2, column: 1 },
+ selectorText: ".sheetB-test1"
+ },
+ {
+ url: SHEET_B,
+ start: { line: 10, column: 1 },
+ selectorText: ".sheetB-test3"
+ },
+ {
+ url: SHEET_D,
+ start: { line: 2, column: 1 },
+ selectorText: ".sheetD-test1"
+ },
+ {
+ url: SHEET_D,
+ start: { line: 10, column: 1 },
+ selectorText: ".sheetD-test3"
+ },
+ {
+ url: SHEET_C,
+ start: { line: 2, column: 1 },
+ selectorText: ".sheetC-test1"
+ },
+ {
+ url: SHEET_C,
+ start: { line: 10, column: 1 },
+ selectorText: ".sheetC-test3"
+ },
+ {
+ url: PAGE_1 + " \u2192 <style> index 0",
+ start: { line: 4, column: 5 },
+ selectorText: ".page1-test1"
+ },
+ {
+ url: PAGE_1 + " \u2192 <style> index 0",
+ start: { line: 12, column: 5 },
+ selectorText: ".page1-test3:hover"
+ }
+ ];
+ isEqualJson(actualReport.preload[1].rules, expectedPreloadRules1, "preload rules 1");
+
+ isEqualJson(actualReport.preload[2].url, PAGE_3, "preload url 2");
+ let expectedPreloadRules2 = [
+ {
+ url: SHEET_A,
+ start: { line: 4, column: 1 },
+ selectorText: ".sheetA-test1"
+ },
+ {
+ url: SHEET_A,
+ start: { line: 20, column: 1 },
+ selectorText: ".sheetA-test5"
+ },
+ {
+ url: SHEET_B,
+ start: { line: 2, column: 1 },
+ selectorText: ".sheetB-test1"
+ },
+ {
+ url: SHEET_B,
+ start: { line: 18, column: 1 },
+ selectorText: ".sheetB-test5"
+ },
+ {
+ url: SHEET_D,
+ start: { line: 2, column: 1 },
+ selectorText: ".sheetD-test1"
+ },
+ {
+ url: SHEET_D,
+ start: { line: 18, column: 1 },
+ selectorText: ".sheetD-test5"
+ },
+ {
+ url: SHEET_C,
+ start: { line: 2, column: 1 },
+ selectorText: ".sheetC-test1"
+ },
+ {
+ url: SHEET_C,
+ start: { line: 18, column: 1 },
+ selectorText: ".sheetC-test5"
+ },
+ {
+ url: PAGE_3 + " \u2192 <style> index 0",
+ start: { line: 5, column: 5 },
+ selectorText: ".page3-test1"
+ },
+ ];
+ isEqualJson(actualReport.preload[2].rules, expectedPreloadRules2, "preload rules 2");
+}
+
+/**
+ * Check that checkPageReport returns the expected unused JSON
+ */
+function checkPageReportUnused(actualReport) {
+ // Check the unused header
+ isEqualJson(actualReport.unused.length, 8, "unused length");
+
+ // Check the unused rules
+ isEqualJson(actualReport.unused[0].url, PAGE_2 + " \u2192 <style> index 0", "unused url 0");
+ let expectedUnusedRules0 = [
+ {
+ url: PAGE_2 + " \u2192 <style> index 0",
+ start: { line: 9, column: 5 },
+ selectorText: ".page2-test2"
+ }
+ ];
+ isEqualJson(actualReport.unused[0].rules, expectedUnusedRules0, "unused rules 0");
+
+ isEqualJson(actualReport.unused[1].url, SHEET_A, "unused url 1");
+ let expectedUnusedRules1 = [
+ {
+ url: SHEET_A,
+ start: { line: 8, column: 1 },
+ selectorText: ".sheetA-test2"
+ }
+ ];
+ isEqualJson(actualReport.unused[1].rules, expectedUnusedRules1, "unused rules 1");
+
+ isEqualJson(actualReport.unused[2].url, SHEET_B, "unused url 2");
+ let expectedUnusedRules2 = [
+ {
+ url: SHEET_B,
+ start: { line: 6, column: 1 },
+ selectorText: ".sheetB-test2"
+ }
+ ];
+ isEqualJson(actualReport.unused[2].rules, expectedUnusedRules2, "unused rules 2");
+
+ isEqualJson(actualReport.unused[3].url, SHEET_D, "unused url 3");
+ let expectedUnusedRules3 = [
+ {
+ url: SHEET_D,
+ start: { line: 6, column: 1 },
+ selectorText: ".sheetD-test2"
+ }
+ ];
+ isEqualJson(actualReport.unused[3].rules, expectedUnusedRules3, "unused rules 3");
+
+ isEqualJson(actualReport.unused[4].url, SHEET_C, "unused url 4");
+ let expectedUnusedRules4 = [
+ {
+ url: SHEET_C,
+ start: { line: 6, column: 1 },
+ selectorText: ".sheetC-test2"
+ }
+ ];
+ isEqualJson(actualReport.unused[4].rules, expectedUnusedRules4, "unused rules 4");
+
+ isEqualJson(actualReport.unused[5].url, PAGE_1 + " \u2192 <style> index 0", "unused url 5");
+ let expectedUnusedRules5 = [
+ {
+ url: PAGE_1 + " \u2192 <style> index 0",
+ start: { line: 8, column: 5 },
+ selectorText: ".page1-test2"
+ }
+ ];
+ isEqualJson(actualReport.unused[5].rules, expectedUnusedRules5, "unused rules 5");
+
+ isEqualJson(actualReport.unused[6].url, PAGE_3 + " \u2192 <style> index 0", "unused url 6");
+ let expectedUnusedRules6 = [
+ {
+ url: PAGE_3 + " \u2192 <style> index 0",
+ start: { line: 9, column: 5 },
+ selectorText: ".page3-test2"
+ }
+ ];
+ isEqualJson(actualReport.unused[6].rules, expectedUnusedRules6, "unused rules 6");
+
+ isEqualJson(actualReport.unused[7].url, PAGE_3 + " \u2192 <style> index 1", "unused url 7");
+ let expectedUnusedRules7 = [
+ {
+ url: PAGE_3 + " \u2192 <style> index 1",
+ start: { line: 3, column: 5 },
+ selectorText: ".page3-test3"
+ }
+ ];
+ isEqualJson(actualReport.unused[7].rules, expectedUnusedRules7, "unused rules 7");
+}
+
+/**
+ * We do basic tests on the shortUrl and formattedCssText because they are
+ * very derivative, and so make for fragile tests, and having done those quick
+ * existence checks we remove them so the JSON check later can ignore them
+ */
+function checkRuleProperties(rule, index) {
+ is(typeof rule.shortUrl, "string", "typeof rule.shortUrl for " + index);
+ is(rule.shortUrl.indexOf("http://"), -1, "http not in rule.shortUrl for" + index);
+ delete rule.shortUrl;
+
+ is(typeof rule.formattedCssText, "string", "typeof rule.formattedCssText for " + index);
+ ok(rule.formattedCssText.indexOf("{") > 0, "{ in rule.formattedCssText for " + index);
+ delete rule.formattedCssText;
+}
+
+/**
+ * Utility to compare JSON structures
+ */
+function isEqualJson(o1, o2, msg) {
+ is(JSON.stringify(o1), JSON.stringify(o2), msg);
+}
diff --git a/devtools/client/commandline/test/browser_cmd_csscoverage_util.js b/devtools/client/commandline/test/browser_cmd_csscoverage_util.js
new file mode 100644
index 000000000..264967a6b
--- /dev/null
+++ b/devtools/client/commandline/test/browser_cmd_csscoverage_util.js
@@ -0,0 +1,24 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the addon commands works as they should
+
+const csscoverage = require("devtools/server/actors/csscoverage");
+
+add_task(function* () {
+ testDeconstructRuleId();
+});
+
+function testDeconstructRuleId() {
+ // This is the easy case
+ let rule = csscoverage.deconstructRuleId("http://thing/blah|10|20");
+ is(rule.url, "http://thing/blah", "1 url");
+ is(rule.line, 10, "1 line");
+ is(rule.column, 20, "1 column");
+
+ // This is the harder case with a URL containing a '|'
+ rule = csscoverage.deconstructRuleId("http://thing/blah?q=a|b|11|22");
+ is(rule.url, "http://thing/blah?q=a|b", "2 url");
+ is(rule.line, 11, "2 line");
+ is(rule.column, 22, "2 column");
+}
diff --git a/devtools/client/commandline/test/browser_cmd_folder.js b/devtools/client/commandline/test/browser_cmd_folder.js
new file mode 100644
index 000000000..8592d4a78
--- /dev/null
+++ b/devtools/client/commandline/test/browser_cmd_folder.js
@@ -0,0 +1,58 @@
+/* Any copyright is dedicated to the Public Domain.
+* http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the folder commands works as they should
+
+const TEST_URI = "data:text/html;charset=utf-8,cmd-folder";
+
+function test() {
+ helpers.addTabWithToolbar(TEST_URI, function (options) {
+ return helpers.audit(options, [
+ {
+ setup: "folder",
+ check: {
+ input: "folder",
+ hints: " open",
+ markup: "IIIIII",
+ status: "ERROR"
+ },
+ },
+ {
+ setup: "folder open",
+ check: {
+ input: "folder open",
+ hints: " [path]",
+ markup: "VVVVVVVVVVV",
+ status: "VALID"
+ }
+ },
+ {
+ setup: "folder open ~",
+ check: {
+ input: "folder open ~",
+ hints: "",
+ markup: "VVVVVVVVVVVVV",
+ status: "VALID"
+ }
+ },
+ {
+ setup: "folder openprofile",
+ check: {
+ input: "folder openprofile",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVVVVV",
+ status: "VALID"
+ }
+ },
+ {
+ setup: "folder openprofile WRONG",
+ check: {
+ input: "folder openprofile WRONG",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVVVVVVEEEEE",
+ status: "ERROR"
+ }
+ }
+ ]);
+ }).then(finish, helpers.handleError);
+}
diff --git a/devtools/client/commandline/test/browser_cmd_highlight_01.js b/devtools/client/commandline/test/browser_cmd_highlight_01.js
new file mode 100644
index 000000000..1d9c151d9
--- /dev/null
+++ b/devtools/client/commandline/test/browser_cmd_highlight_01.js
@@ -0,0 +1,90 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* eslint key-spacing: 0 */
+
+// Tests the various highlight command parameters and options
+
+// Creating a test page with many elements to test the --showall option
+var TEST_PAGE = "data:text/html;charset=utf-8,<body><ul>";
+for (let i = 0; i < 101; i++) {
+ TEST_PAGE += "<li class='item'>" + i + "</li>";
+}
+TEST_PAGE += "</ul></body>";
+
+function test() {
+ return Task.spawn(spawnTest).then(finish, helpers.handleError);
+}
+
+function* spawnTest() {
+ let options = yield helpers.openTab(TEST_PAGE);
+ yield helpers.openToolbar(options);
+
+ yield helpers.audit(options, [
+ {
+ setup: "highlight",
+ check: {
+ input: "highlight",
+ hints: " [selector] [options]",
+ markup: "VVVVVVVVV",
+ status: "VALID"
+ },
+ exec: {
+ output: "0 nodes highlighted"
+ }
+ },
+ {
+ setup: "highlight bo",
+ check: {
+ input: "highlight bo",
+ hints: " [options]",
+ markup: "VVVVVVVVVVII",
+ status: "ERROR"
+ },
+ exec: {
+ output: "Error: No matches"
+ }
+ },
+ {
+ setup: "highlight body",
+ check: {
+ input: "highlight body",
+ hints: " [options]",
+ markup: "VVVVVVVVVVVVVV",
+ status: "VALID"
+ },
+ exec: {
+ output: "1 node highlighted"
+ }
+ },
+ {
+ setup: "highlight body --hideguides",
+ check: {
+ input: "highlight body --hideguides",
+ hints: " [options]",
+ markup: "VVVVVVVVVVVVVVVVVVVVVVVVVVV",
+ status: "VALID"
+ },
+ exec: {
+ output: "1 node highlighted"
+ }
+ },
+ {
+ setup: "highlight body --showinfobar",
+ check: {
+ input: "highlight body --showinfobar",
+ hints: " [options]",
+ markup: "VVVVVVVVVVVVVVVVVVVVVVVVVVVV",
+ status: "VALID"
+ },
+ exec: {
+ output: "1 node highlighted"
+ }
+ }
+ ]);
+
+ yield helpers.closeToolbar(options);
+ yield helpers.closeTab(options);
+}
diff --git a/devtools/client/commandline/test/browser_cmd_highlight_02.js b/devtools/client/commandline/test/browser_cmd_highlight_02.js
new file mode 100644
index 000000000..d5a361f43
--- /dev/null
+++ b/devtools/client/commandline/test/browser_cmd_highlight_02.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the highlight command actually creates a highlighter
+
+const TEST_PAGE = "data:text/html;charset=utf-8,<div></div>";
+
+function test() {
+ return Task.spawn(function* () {
+ let options = yield helpers.openTab(TEST_PAGE);
+ yield helpers.openToolbar(options);
+
+ info("highlighting the body node");
+ yield runCommand("highlight body", options);
+ is(yield getHighlighterNumber(), 1, "The highlighter element exists for body");
+
+ info("highlighting the div node");
+ yield runCommand("highlight div", options);
+ is(yield getHighlighterNumber(), 1, "The highlighter element exists for div");
+
+ info("highlighting the body node again, asking to keep the div");
+ yield runCommand("highlight body --keep", options);
+ is(yield getHighlighterNumber(), 2, "2 highlighter elements have been created");
+
+ info("unhighlighting all nodes");
+ yield runCommand("unhighlight", options);
+ is(yield getHighlighterNumber(), 0, "All highlighters have been removed");
+
+ yield helpers.closeToolbar(options);
+ yield helpers.closeTab(options);
+ }).then(finish, helpers.handleError);
+}
+
+function getHighlighterNumber() {
+ return ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+ return require("devtools/shared/gcli/commands/highlight").highlighters.length;
+ });
+}
+
+function* runCommand(cmd, options) {
+ yield helpers.audit(options, [{ setup: cmd, exec: {} }]);
+}
diff --git a/devtools/client/commandline/test/browser_cmd_highlight_03.js b/devtools/client/commandline/test/browser_cmd_highlight_03.js
new file mode 100644
index 000000000..75b28af61
--- /dev/null
+++ b/devtools/client/commandline/test/browser_cmd_highlight_03.js
@@ -0,0 +1,131 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* eslint key-spacing: 0 */
+
+// Tests the various highlight command parameters and options that doesn't
+// involve nodes at all.
+
+var TEST_PAGE = "data:text/html;charset=utf-8,";
+
+function test() {
+ return Task.spawn(spawnTest).then(finish, helpers.handleError);
+}
+
+function* spawnTest() {
+ let options = yield helpers.openTab(TEST_PAGE);
+ yield helpers.openToolbar(options);
+
+ yield helpers.audit(options, [
+ {
+ setup: "highlight body --hide",
+ check: {
+ input: "highlight body --hide",
+ hints: "guides [options]",
+ markup: "VVVVVVVVVVVVVVVIIIIII",
+ status: "ERROR"
+ },
+ exec: {
+ output: "Error: Too many arguments"
+ }
+ },
+ {
+ setup: "highlight body --show",
+ check: {
+ input: "highlight body --show",
+ hints: "infobar [options]",
+ markup: "VVVVVVVVVVVVVVVIIIIII",
+ status: "ERROR"
+ },
+ exec: {
+ output: "Error: Too many arguments"
+ }
+ },
+ {
+ setup: "highlight body --showa",
+ check: {
+ input: "highlight body --showa",
+ hints: "ll [options]",
+ markup: "VVVVVVVVVVVVVVVIIIIIII",
+ status: "ERROR"
+ },
+ exec: {
+ output: "Error: Too many arguments"
+ }
+ },
+ {
+ setup: "highlight body --r",
+ check: {
+ input: "highlight body --r",
+ hints: "egion [options]",
+ markup: "VVVVVVVVVVVVVVVIII",
+ status: "ERROR"
+ },
+ exec: {
+ output: "Error: Too many arguments"
+ }
+ },
+ {
+ setup: "highlight body --region",
+ check: {
+ input: "highlight body --region",
+ hints: " <selection> [options]",
+ markup: "VVVVVVVVVVVVVVVIIIIIIII",
+ status: "ERROR"
+ },
+ exec: {
+ output: "Error: Value required for \u2018region\u2019."
+ }
+ },
+ {
+ setup: "highlight body --fi",
+ check: {
+ input: "highlight body --fi",
+ hints: "ll [options]",
+ markup: "VVVVVVVVVVVVVVVIIII",
+ status: "ERROR"
+ },
+ exec: {
+ output: "Error: Too many arguments"
+ }
+ },
+ {
+ setup: "highlight body --fill",
+ check: {
+ input: "highlight body --fill",
+ hints: " <string> [options]",
+ markup: "VVVVVVVVVVVVVVVIIIIII",
+ status: "ERROR"
+ },
+ exec: {
+ output: "Error: Value required for \u2018fill\u2019."
+ }
+ },
+ {
+ setup: "highlight body --ke",
+ check: {
+ input: "highlight body --ke",
+ hints: "ep [options]",
+ markup: "VVVVVVVVVVVVVVVIIII",
+ status: "ERROR"
+ },
+ exec: {
+ output: "Error: Too many arguments"
+ }
+ },
+ {
+ setup: "unhighlight",
+ check: {
+ input: "unhighlight",
+ hints: "",
+ markup: "VVVVVVVVVVV",
+ status: "VALID"
+ }
+ }
+ ]);
+
+ yield helpers.closeToolbar(options);
+ yield helpers.closeTab(options);
+}
diff --git a/devtools/client/commandline/test/browser_cmd_highlight_04.js b/devtools/client/commandline/test/browser_cmd_highlight_04.js
new file mode 100644
index 000000000..108308d8f
--- /dev/null
+++ b/devtools/client/commandline/test/browser_cmd_highlight_04.js
@@ -0,0 +1,94 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* eslint key-spacing: 0 */
+
+// Tests the various highlight command parameters and options
+
+// Creating a test page with many elements to test the --showall option
+var TEST_PAGE = "data:text/html;charset=utf-8,<body><ul>";
+for (let i = 0; i < 101; i++) {
+ TEST_PAGE += "<li class='item'>" + i + "</li>";
+}
+TEST_PAGE += "</ul></body>";
+
+function test() {
+ return Task.spawn(spawnTest).then(finish, helpers.handleError);
+}
+
+function* spawnTest() {
+ let options = yield helpers.openTab(TEST_PAGE);
+ yield helpers.openToolbar(options);
+
+ yield helpers.audit(options, [
+ {
+ setup: "highlight body --showall",
+ check: {
+ input: "highlight body --showall",
+ hints: " [options]",
+ markup: "VVVVVVVVVVVVVVVVVVVVVVVV",
+ status: "VALID"
+ },
+ exec: {
+ output: "1 node highlighted"
+ }
+ },
+ {
+ setup: "highlight body --keep",
+ check: {
+ input: "highlight body --keep",
+ hints: " [options]",
+ markup: "VVVVVVVVVVVVVVVVVVVVV",
+ status: "VALID"
+ },
+ exec: {
+ output: "1 node highlighted"
+ }
+ },
+ {
+ setup: "highlight body --hideguides --showinfobar --showall --region " +
+ "content --fill red --keep",
+ check: {
+ input: "highlight body --hideguides --showinfobar --showall --region " +
+ "content --fill red --keep",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV" +
+ "VVVVVVVVVVVVVVVVVVVVVVVVV",
+ status: "VALID"
+ },
+ exec: {
+ output: "1 node highlighted"
+ }
+ },
+ {
+ setup: "highlight .item",
+ check: {
+ input: "highlight .item",
+ hints: " [options]",
+ markup: "VVVVVVVVVVVVVVV",
+ status: "VALID"
+ },
+ exec: {
+ output: "101 nodes matched, but only 100 nodes highlighted. Use " +
+ "\u2018--showall\u2019 to show all"
+ }
+ },
+ {
+ setup: "highlight .item --showall",
+ check: {
+ input: "highlight .item --showall",
+ hints: " [options]",
+ markup: "VVVVVVVVVVVVVVVVVVVVVVVVV",
+ status: "VALID"
+ },
+ exec: {
+ output: "101 nodes highlighted"
+ }
+ }
+ ]);
+
+ yield helpers.closeToolbar(options);
+ yield helpers.closeTab(options);
+}
diff --git a/devtools/client/commandline/test/browser_cmd_inject.html b/devtools/client/commandline/test/browser_cmd_inject.html
new file mode 100644
index 000000000..ea84be393
--- /dev/null
+++ b/devtools/client/commandline/test/browser_cmd_inject.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <head>
+ <body>
+ </body>
+</html>
diff --git a/devtools/client/commandline/test/browser_cmd_inject.js b/devtools/client/commandline/test/browser_cmd_inject.js
new file mode 100644
index 000000000..ec618bb77
--- /dev/null
+++ b/devtools/client/commandline/test/browser_cmd_inject.js
@@ -0,0 +1,69 @@
+/* Any copyright is dedicated to the Public Domain.
+* http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the inject commands works as they should
+
+const TEST_URI = "http://example.com/browser/devtools/client/commandline/" +
+ "test/browser_cmd_inject.html";
+
+function test() {
+ helpers.addTabWithToolbar(TEST_URI, function (options) {
+ return helpers.audit(options, [
+ {
+ setup: "inject",
+ check: {
+ input: "inject",
+ markup: "VVVVVV",
+ hints: " <library>",
+ status: "ERROR"
+ },
+ },
+ {
+ setup: "inject j",
+ check: {
+ input: "inject j",
+ markup: "VVVVVVVI",
+ hints: "Query",
+ status: "ERROR"
+ },
+ },
+ {
+ setup: "inject notauri",
+ check: {
+ input: "inject notauri",
+ hints: " -> http://notauri/",
+ markup: "VVVVVVVIIIIIII",
+ status: "ERROR",
+ args: {
+ library: {
+ value: undefined,
+ status: "INCOMPLETE"
+ }
+ }
+ }
+ },
+ {
+ setup: "inject http://example.com/browser/devtools/client/commandline/test/browser_cmd_inject.js",
+ check: {
+ input: "inject http://example.com/browser/devtools/client/commandline/test/browser_cmd_inject.js",
+ markup: "VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV",
+ hints: "",
+ status: "VALID",
+ args: {
+ library: {
+ value: function (library) {
+ is(library.type, "url", "inject type name");
+ is(library.url.origin, "http://example.com", "inject url hostname");
+ ok(library.url.pathname.indexOf("_inject.js") != -1, "inject url path");
+ },
+ status: "VALID"
+ }
+ }
+ },
+ exec: {
+ output: [ /http:\/\/example.com\/browser\/devtools\/client\/commandline\/test\/browser_cmd_inject.js loaded/ ]
+ }
+ }
+ ]);
+ }).then(finish, helpers.handleError);
+}
diff --git a/devtools/client/commandline/test/browser_cmd_jsb.js b/devtools/client/commandline/test/browser_cmd_jsb.js
new file mode 100644
index 000000000..c27cf151c
--- /dev/null
+++ b/devtools/client/commandline/test/browser_cmd_jsb.js
@@ -0,0 +1,103 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the jsb command works as it should
+
+const TEST_URI = "http://example.com/browser/devtools/client/commandline/" +
+ "test/browser_cmd_jsb_script.jsi";
+
+function test() {
+ return Task.spawn(testTask).then(finish, helpers.handleError);
+}
+
+function* testTask() {
+ let options = yield helpers.openTab("about:blank");
+ yield helpers.openToolbar(options);
+
+ let notifyPromise = wwNotifyOnce();
+
+ helpers.audit(options, [
+ {
+ setup: "jsb",
+ check: {
+ input: "jsb",
+ hints: " <url> [options]",
+ markup: "VVV",
+ status: "ERROR"
+ }
+ },
+ {
+ setup: "jsb " + TEST_URI,
+ // Should result in a new scratchpad window
+ exec: {
+ output: "",
+ error: false
+ }
+ }
+ ]);
+
+ let { subject } = yield notifyPromise;
+ let scratchpadWin = subject.QueryInterface(Ci.nsIDOMWindow);
+ yield helpers.listenOnce(scratchpadWin, "load");
+
+ let scratchpad = scratchpadWin.Scratchpad;
+
+ yield observeOnce(scratchpad);
+
+ let result = scratchpad.getText();
+ result = result.replace(/[\r\n]]*/g, "\n");
+ let correct = "function somefunc() {\n" +
+ " if (true) // Some comment\n" +
+ " doSomething();\n" +
+ " for (let n = 0; n < 500; n++) {\n" +
+ " if (n % 2 == 1) {\n" +
+ " console.log(n);\n" +
+ " console.log(n + 1);\n" +
+ " }\n" +
+ " }\n" +
+ "}";
+ is(result, correct, "JS has been correctly prettified");
+
+ if (scratchpadWin) {
+ scratchpadWin.close();
+ scratchpadWin = null;
+ }
+
+ yield helpers.closeToolbar(options);
+ yield helpers.closeTab(options);
+}
+
+/**
+ * A wrapper for calling Services.ww.[un]registerNotification using promises.
+ * @return a promise that resolves when the notification service first notifies
+ * with topic == "domwindowopened".
+ * The value of the promise is { subject: subject, topic: topic, data: data }
+ */
+function wwNotifyOnce() {
+ return new Promise(resolve => {
+ let onNotify = (subject, topic, data) => {
+ if (topic == "domwindowopened") {
+ Services.ww.unregisterNotification(onNotify);
+ resolve({ subject: subject, topic: topic, data: data });
+ }
+ };
+
+ Services.ww.registerNotification(onNotify);
+ });
+}
+
+/**
+ * YET ANOTHER WRAPPER for a place where we are using events as poor-man's
+ * promises. Perhaps this should be promoted to scratchpad?
+ */
+function observeOnce(scratchpad) {
+ return new Promise(resolve => {
+ let observer = {
+ onReady: function () {
+ scratchpad.removeObserver(observer);
+ resolve();
+ },
+ };
+ scratchpad.addObserver(observer);
+ });
+}
diff --git a/devtools/client/commandline/test/browser_cmd_jsb_script.jsi b/devtools/client/commandline/test/browser_cmd_jsb_script.jsi
new file mode 100644
index 000000000..dcaac807c
--- /dev/null
+++ b/devtools/client/commandline/test/browser_cmd_jsb_script.jsi
@@ -0,0 +1,2 @@
+function somefunc(){if (true) // Some comment
+doSomething();for(let n=0;n<500;n++){if(n%2==1){console.log(n);console.log(n+1);}}}
diff --git a/devtools/client/commandline/test/browser_cmd_listen.js b/devtools/client/commandline/test/browser_cmd_listen.js
new file mode 100644
index 000000000..91746c2aa
--- /dev/null
+++ b/devtools/client/commandline/test/browser_cmd_listen.js
@@ -0,0 +1,80 @@
+/* Any copyright is dedicated to the Public Domain.
+* http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the listen/unlisten commands work as they should.
+
+const TEST_URI = "http://example.com/browser/devtools/client/commandline/" +
+ "test/browser_cmd_cookie.html";
+
+function test() {
+ return Task.spawn(testTask).then(finish, helpers.handleError);
+}
+
+var tests = {
+ testInput: function (options) {
+ return helpers.audit(options, [
+ {
+ setup: "listen",
+ check: {
+ input: "listen",
+ markup: "VVVVVV",
+ status: "VALID"
+ },
+ },
+ {
+ setup: "unlisten",
+ check: {
+ input: "unlisten",
+ markup: "VVVVVVVV",
+ status: "VALID"
+ },
+ exec: {
+ output: "All TCP ports closed"
+ }
+ },
+ {
+ setup: function () {
+ return helpers.setInput(options, "listen");
+ },
+ check: {
+ input: "listen",
+ hints: " [port] [protocol]",
+ markup: "VVVVVV",
+ status: "VALID"
+ },
+ exec: {
+ output: "Listening on port " + Services.prefs
+ .getIntPref("devtools.debugger.remote-port")
+ }
+ },
+ {
+ setup: function () {
+ return helpers.setInput(options, "listen 8000");
+ },
+ exec: {
+ output: "Listening on port 8000"
+ }
+ },
+ {
+ setup: function () {
+ return helpers.setInput(options, "unlisten");
+ },
+ exec: {
+ output: "All TCP ports closed"
+ }
+ }
+ ]);
+ },
+};
+
+function* testTask() {
+ Services.prefs.setBoolPref("devtools.debugger.remote-enabled", true);
+ let options = yield helpers.openTab(TEST_URI);
+ yield helpers.openToolbar(options);
+
+ yield helpers.runTests(options, tests);
+
+ yield helpers.closeToolbar(options);
+ yield helpers.closeTab(options);
+ Services.prefs.clearUserPref("devtools.debugger.remote-enabled");
+}
diff --git a/devtools/client/commandline/test/browser_cmd_measure.js b/devtools/client/commandline/test/browser_cmd_measure.js
new file mode 100644
index 000000000..7dd9bd9c9
--- /dev/null
+++ b/devtools/client/commandline/test/browser_cmd_measure.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests the highlight command, ensure no invalid arguments are given
+
+const TEST_PAGE = "data:text/html;charset=utf-8,foo";
+
+function test() {
+ return Task.spawn(spawnTest).then(finish, helpers.handleError);
+}
+
+function* spawnTest() {
+ let options = yield helpers.openTab(TEST_PAGE);
+ yield helpers.openToolbar(options);
+
+ yield helpers.audit(options, [
+ {
+ setup: "measure",
+ check: {
+ input: "measure",
+ markup: "VVVVVVV",
+ status: "VALID"
+ }
+ },
+ {
+ setup: "measure on",
+ check: {
+ input: "measure on",
+ markup: "VVVVVVVVEE",
+ status: "ERROR"
+ },
+ exec: {
+ output: "Error: Too many arguments"
+ }
+ },
+ {
+ setup: "measure --visible",
+ check: {
+ input: "measure --visible",
+ markup: "VVVVVVVVEEEEEEEEE",
+ status: "ERROR"
+ },
+ exec: {
+ output: "Error: Too many arguments"
+ }
+ }
+ ]);
+
+ yield helpers.closeToolbar(options);
+ yield helpers.closeTab(options);
+}
diff --git a/devtools/client/commandline/test/browser_cmd_media.html b/devtools/client/commandline/test/browser_cmd_media.html
new file mode 100644
index 000000000..9bc1e7aeb
--- /dev/null
+++ b/devtools/client/commandline/test/browser_cmd_media.html
@@ -0,0 +1,28 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>GCLI Test for Bug 819930</title>
+ <style>
+ @media braille {
+ body {
+ background-color: yellow;
+ }
+ }
+
+ @media embossed {
+ body {
+ background-color: indigo;
+ }
+ }
+
+ @media screen {
+ body {
+ background-color: white;
+ }
+ }
+ </style>
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/devtools/client/commandline/test/browser_cmd_media.js b/devtools/client/commandline/test/browser_cmd_media.js
new file mode 100644
index 000000000..559370add
--- /dev/null
+++ b/devtools/client/commandline/test/browser_cmd_media.js
@@ -0,0 +1,88 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that screenshot command works properly
+const TEST_URI = "http://example.com/browser/devtools/client/commandline/" +
+ "test/browser_cmd_media.html";
+var tests = {
+ testInput: function (options) {
+ return helpers.audit(options, [
+ {
+ setup: "media emulate braille",
+ check: {
+ input: "media emulate braille",
+ markup: "VVVVVVVVVVVVVVVVVVVVV",
+ status: "VALID",
+ args: {
+ type: { value: "braille"},
+ }
+ },
+ },
+ {
+ setup: "media reset",
+ check: {
+ input: "media reset",
+ markup: "VVVVVVVVVVV",
+ status: "VALID",
+ args: {
+ }
+ },
+ },
+ ]);
+ },
+
+ testEmulateMedia: function (options) {
+ return helpers.audit(options, [
+ {
+ setup: "media emulate braille",
+ check: {
+ args: {
+ type: { value: "braille"}
+ }
+ },
+ exec: {
+ output: ""
+ },
+ post: Task.async(function* () {
+ yield ContentTask.spawn(options.browser, {}, function* () {
+ let color = content.getComputedStyle(content.document.body).backgroundColor;
+ is(color, "rgb(255, 255, 0)", "media correctly emulated");
+ });
+ })
+ }
+ ]);
+ },
+
+ testEndMediaEmulation: function (options) {
+ return helpers.audit(options, [
+ {
+ setup: function () {
+ let mDV = options.browser.markupDocumentViewer;
+ mDV.emulateMedium("embossed");
+ return helpers.setInput(options, "media reset");
+ },
+ exec: {
+ output: ""
+ },
+ post: Task.async(function* () {
+ yield ContentTask.spawn(options.browser, {}, function* () {
+ let color = content.getComputedStyle(content.document.body).backgroundColor;
+ is(color, "rgb(255, 255, 255)", "media reset");
+ });
+ })
+ }
+ ]);
+ }
+};
+
+function test() {
+ return Task.spawn(function* () {
+ let options = yield helpers.openTab(TEST_URI);
+ yield helpers.openToolbar(options);
+
+ yield helpers.runTests(options, tests);
+
+ yield helpers.closeToolbar(options);
+ yield helpers.closeTab(options);
+ }).then(finish, helpers.handleError);
+}
diff --git a/devtools/client/commandline/test/browser_cmd_pagemod_export.html b/devtools/client/commandline/test/browser_cmd_pagemod_export.html
new file mode 100644
index 000000000..a7d28828c
--- /dev/null
+++ b/devtools/client/commandline/test/browser_cmd_pagemod_export.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <title>GCLI inspect command test</title>
+</head>
+<body>
+
+ <!-- This is a list of 0 h1 elements -->
+
+ <!-- This is a list of 1 div elements -->
+ <div>Hello, I'm a div</div>
+
+ <!-- This is a list of 2 span elements -->
+ <span>Hello, I'm a span</span>
+ <span>And me</span>
+
+ <!-- This is a collection of various things that match only once -->
+ <p class="someclass">.someclass</p>
+ <p id="someid">#someid</p>
+ <button disabled>button[disabled]</button>
+ <p><strong>p&gt;strong</strong></p>
+
+</body>
+</html>
diff --git a/devtools/client/commandline/test/browser_cmd_pagemod_export.js b/devtools/client/commandline/test/browser_cmd_pagemod_export.js
new file mode 100644
index 000000000..c1053a065
--- /dev/null
+++ b/devtools/client/commandline/test/browser_cmd_pagemod_export.js
@@ -0,0 +1,417 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the inspect command works as it should
+
+const TEST_URI = "http://example.com/browser/devtools/client/commandline/" +
+ "test/browser_cmd_pagemod_export.html";
+
+function test() {
+ return Task.spawn(spawnTest).then(finish, helpers.handleError);
+}
+
+function* spawnTest() {
+ let options = yield helpers.openTab(TEST_URI);
+ yield helpers.openToolbar(options);
+
+ function getHTML() {
+ return ContentTask.spawn(options.browser, {}, function* () {
+ return content.document.documentElement.innerHTML;
+ });
+ }
+
+ const initialHtml = yield getHTML();
+
+ function resetContent() {
+ return ContentTask.spawn(options.browser, initialHtml, function* (html) {
+ content.document.documentElement.innerHTML = html;
+ });
+ }
+
+ // Test exporting HTML
+ yield ContentTask.spawn(options.browser, {}, function* () {
+ content.wrappedJSObject.oldOpen = content.open;
+ content.wrappedJSObject.openURL = "";
+ content.wrappedJSObject.open = function (url) {
+ // The URL is a data: URL that contains the document source
+ content.wrappedJSObject.openURL = decodeURIComponent(url);
+ };
+ });
+
+ yield helpers.audit(options, [
+ {
+ setup: "export html",
+ skipIf: true,
+ check: {
+ input: "export html",
+ hints: " [destination]",
+ markup: "VVVVVVVVVVV",
+ status: "VALID",
+ },
+ exec: {
+ output: ""
+ },
+ post: Task.async(function* () {
+ yield ContentTask.spawn(options.browser, {}, function* () {
+ let openURL = content.wrappedJSObject.openURL;
+ isnot(openURL.indexOf('<html lang="en">'), -1, "export html works: <html>");
+ isnot(openURL.indexOf("<title>GCLI"), -1, "export html works: <title>");
+ isnot(openURL.indexOf('<p id="someid">#'), -1, "export html works: <p>");
+ });
+ })
+ },
+ {
+ setup: "export html stdout",
+ check: {
+ input: "export html stdout",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVVVVV",
+ status: "VALID",
+ args: {
+ destination: { value: "stdout" }
+ },
+ },
+ exec: {
+ output: [
+ /<html lang="en">/,
+ /<title>GCLI/,
+ /<p id="someid">#/
+ ]
+ }
+ }
+ ]);
+
+ yield ContentTask.spawn(options.browser, {}, function* () {
+ content.wrappedJSObject.open = content.wrappedJSObject.oldOpen;
+ delete content.wrappedJSObject.openURL;
+ delete content.wrappedJSObject.oldOpen;
+ });
+
+ // Test 'pagemod replace'
+ yield helpers.audit(options, [
+ {
+ setup: "pagemod replace",
+ check: {
+ input: "pagemod replace",
+ hints: " <search> <replace> [ignoreCase] [selector] [root] [attrOnly] [contentOnly] [attributes]",
+ markup: "VVVVVVVVVVVVVVV",
+ status: "ERROR"
+ }
+ },
+ {
+ setup: "pagemod replace some foo",
+ check: {
+ input: "pagemod replace some foo",
+ hints: " [ignoreCase] [selector] [root] [attrOnly] [contentOnly] [attributes]",
+ markup: "VVVVVVVVVVVVVVVVVVVVVVVV",
+ status: "VALID"
+ }
+ },
+ {
+ setup: "pagemod replace some foo true",
+ check: {
+ input: "pagemod replace some foo true",
+ hints: " [selector] [root] [attrOnly] [contentOnly] [attributes]",
+ markup: "VVVVVVVVVVVVVVVVVVVVVVVVVVVVV",
+ status: "VALID"
+ }
+ },
+ {
+ setup: "pagemod replace some foo true --attrOnly",
+ check: {
+ input: "pagemod replace some foo true --attrOnly",
+ hints: " [selector] [root] [contentOnly] [attributes]",
+ markup: "VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV",
+ status: "VALID"
+ }
+ },
+ {
+ setup: "pagemod replace sOme foOBar",
+ exec: {
+ output: /^[^:]+: 13\. [^:]+: 0\. [^:]+: 0\.\s*$/
+ },
+ post: Task.async(function* () {
+ let html = yield getHTML();
+ is(html, initialHtml, "no change in the page");
+ })
+ },
+ {
+ setup: "pagemod replace sOme foOBar true",
+ exec: {
+ output: /^[^:]+: 13\. [^:]+: 2\. [^:]+: 2\.\s*$/
+ },
+ post: Task.async(function* () {
+ let html = yield getHTML();
+
+ isnot(html.indexOf('<p class="foOBarclass">.foOBarclass'), -1,
+ ".someclass changed to .foOBarclass");
+ isnot(html.indexOf('<p id="foOBarid">#foOBarid'), -1,
+ "#someid changed to #foOBarid");
+
+ yield resetContent();
+ })
+ },
+ {
+ setup: "pagemod replace some foobar --contentOnly",
+ exec: {
+ output: /^[^:]+: 13\. [^:]+: 2\. [^:]+: 0\.\s*$/
+ },
+ post: Task.async(function* () {
+ let html = yield getHTML();
+
+ isnot(html.indexOf('<p class="someclass">.foobarclass'), -1,
+ ".someclass changed to .foobarclass (content only)");
+ isnot(html.indexOf('<p id="someid">#foobarid'), -1,
+ "#someid changed to #foobarid (content only)");
+
+ yield resetContent();
+ })
+ },
+ {
+ setup: "pagemod replace some foobar --attrOnly",
+ exec: {
+ output: /^[^:]+: 13\. [^:]+: 0\. [^:]+: 2\.\s*$/
+ },
+ post: Task.async(function* () {
+ let html = yield getHTML();
+
+ isnot(html.indexOf('<p class="foobarclass">.someclass'), -1,
+ ".someclass changed to .foobarclass (attr only)");
+ isnot(html.indexOf('<p id="foobarid">#someid'), -1,
+ "#someid changed to #foobarid (attr only)");
+
+ yield resetContent();
+ })
+ },
+ {
+ setup: "pagemod replace some foobar --root head",
+ exec: {
+ output: /^[^:]+: 2\. [^:]+: 0\. [^:]+: 0\.\s*$/
+ },
+ post: Task.async(function* () {
+ let html = yield getHTML();
+ is(html, initialHtml, "nothing changed");
+ })
+ },
+ {
+ setup: "pagemod replace some foobar --selector .someclass,div,span",
+ exec: {
+ output: /^[^:]+: 4\. [^:]+: 1\. [^:]+: 1\.\s*$/
+ },
+ post: Task.async(function* () {
+ let html = yield getHTML();
+
+ isnot(html.indexOf('<p class="foobarclass">.foobarclass'), -1,
+ ".someclass changed to .foobarclass");
+ isnot(html.indexOf('<p id="someid">#someid'), -1,
+ "#someid did not change");
+
+ yield resetContent();
+ })
+ },
+ ]);
+
+ // Test 'pagemod remove element'
+ yield helpers.audit(options, [
+ {
+ setup: "pagemod remove",
+ check: {
+ input: "pagemod remove",
+ hints: " attribute",
+ markup: "IIIIIIIVIIIIII",
+ status: "ERROR"
+ },
+ },
+ {
+ setup: "pagemod remove element",
+ check: {
+ input: "pagemod remove element",
+ hints: " <search> [root] [stripOnly] [ifEmptyOnly]",
+ markup: "VVVVVVVVVVVVVVVVVVVVVV",
+ status: "ERROR"
+ },
+ },
+ {
+ setup: "pagemod remove element foo",
+ check: {
+ input: "pagemod remove element foo",
+ hints: " [root] [stripOnly] [ifEmptyOnly]",
+ markup: "VVVVVVVVVVVVVVVVVVVVVVVVVV",
+ status: "VALID"
+ },
+ },
+ {
+ setup: "pagemod remove element p",
+ exec: {
+ output: /^[^:]+: 3\. [^:]+: 3\.\s*$/
+ },
+ post: Task.async(function* () {
+ let html = yield getHTML();
+
+ is(html.indexOf('<p class="someclass">'), -1, "p.someclass removed");
+ is(html.indexOf('<p id="someid">'), -1, "p#someid removed");
+ is(html.indexOf("<p><strong>"), -1, "<p> wrapping <strong> removed");
+ isnot(html.indexOf("<span>"), -1, "<span> not removed");
+
+ yield resetContent();
+ })
+ },
+ {
+ setup: "pagemod remove element p head",
+ exec: {
+ output: /^[^:]+: 0\. [^:]+: 0\.\s*$/
+ },
+ post: Task.async(function* () {
+ let html = yield getHTML();
+ is(html, initialHtml, "nothing changed in the page");
+ })
+ },
+ {
+ setup: "pagemod remove element p --ifEmptyOnly",
+ exec: {
+ output: /^[^:]+: 3\. [^:]+: 0\.\s*$/
+ },
+ post: Task.async(function* () {
+ let html = yield getHTML();
+ is(html, initialHtml, "nothing changed in the page");
+ })
+ },
+ {
+ setup: "pagemod remove element meta,title --ifEmptyOnly",
+ exec: {
+ output: /^[^:]+: 2\. [^:]+: 1\.\s*$/
+ },
+ post: Task.async(function* () {
+ let html = yield getHTML();
+
+ is(html.indexOf("<meta charset="), -1, "<meta> removed");
+ isnot(html.indexOf("<title>"), -1, "<title> not removed");
+
+ yield resetContent();
+ })
+ },
+ {
+ setup: "pagemod remove element p --stripOnly",
+ exec: {
+ output: /^[^:]+: 3\. [^:]+: 3\.\s*$/
+ },
+ post: Task.async(function* () {
+ let html = yield getHTML();
+
+ is(html.indexOf('<p class="someclass">'), -1, "p.someclass removed");
+ is(html.indexOf('<p id="someid">'), -1, "p#someid removed");
+ is(html.indexOf("<p><strong>"), -1, "<p> wrapping <strong> removed");
+ isnot(html.indexOf(".someclass"), -1, ".someclass still exists");
+ isnot(html.indexOf("#someid"), -1, "#someid still exists");
+ isnot(html.indexOf("<strong>p"), -1, "<strong> still exists");
+
+ yield resetContent();
+ })
+ },
+ ]);
+
+ // Test 'pagemod remove attribute'
+ yield helpers.audit(options, [
+ {
+ setup: "pagemod remove attribute",
+ check: {
+ input: "pagemod remove attribute",
+ hints: " <searchAttributes> <searchElements> [root] [ignoreCase]",
+ markup: "VVVVVVVVVVVVVVVVVVVVVVVV",
+ status: "ERROR",
+ args: {
+ searchAttributes: { value: undefined, status: "INCOMPLETE" },
+ searchElements: { value: undefined, status: "INCOMPLETE" },
+ // root: { value: undefined }, // 'root' is a node which is remote
+ // so we can't see the value in tests
+ ignoreCase: { value: false },
+ }
+ },
+ },
+ {
+ setup: "pagemod remove attribute foo bar",
+ check: {
+ input: "pagemod remove attribute foo bar",
+ hints: " [root] [ignoreCase]",
+ markup: "VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV",
+ status: "VALID",
+ args: {
+ searchAttributes: { value: "foo" },
+ searchElements: { value: "bar" },
+ // root: { value: undefined }, // 'root' is a node which is remote
+ // so we can't see the value in tests
+ ignoreCase: { value: false },
+ }
+ },
+ post: function () {
+ return new Promise(resolve => {
+ executeSoon(resolve);
+ });
+ }
+ },
+ {
+ setup: "pagemod remove attribute foo bar",
+ exec: {
+ output: /^[^:]+: 0\. [^:]+: 0\.\s*$/
+ },
+ post: Task.async(function* () {
+ let html = yield getHTML();
+ is(html, initialHtml, "nothing changed in the page");
+ })
+ },
+ {
+ setup: "pagemod remove attribute foo p",
+ exec: {
+ output: /^[^:]+: 3\. [^:]+: 0\.\s*$/
+ },
+ post: Task.async(function* () {
+ let html = yield getHTML();
+ is(html, initialHtml, "nothing changed in the page");
+ })
+ },
+ {
+ setup: "pagemod remove attribute id p,span",
+ exec: {
+ output: /^[^:]+: 5\. [^:]+: 1\.\s*$/
+ },
+ post: Task.async(function* () {
+ let html = yield getHTML();
+
+ is(html.indexOf('<p id="someid">#someid'), -1, "p#someid attribute removed");
+ isnot(html.indexOf("<p>#someid"), -1, "p with someid content still exists");
+
+ yield resetContent();
+ })
+ },
+ {
+ setup: "pagemod remove attribute Class p",
+ exec: {
+ output: /^[^:]+: 3\. [^:]+: 0\.\s*$/
+ },
+ post: Task.async(function* () {
+ let html = yield getHTML();
+ is(html, initialHtml, "nothing changed in the page");
+ })
+ },
+ {
+ setup: "pagemod remove attribute Class p --ignoreCase",
+ exec: {
+ output: /^[^:]+: 3\. [^:]+: 1\.\s*$/
+ },
+ post: Task.async(function* () {
+ let html = yield getHTML();
+
+ is(html.indexOf('<p class="someclass">.someclass'), -1,
+ "p.someclass attribute removed");
+ isnot(html.indexOf("<p>.someclass"), -1,
+ "p with someclass content still exists");
+
+ yield resetContent();
+ })
+ },
+ ]);
+
+ // Shutdown
+ yield helpers.closeToolbar(options);
+ yield helpers.closeTab(options);
+}
diff --git a/devtools/client/commandline/test/browser_cmd_paintflashing.js b/devtools/client/commandline/test/browser_cmd_paintflashing.js
new file mode 100644
index 000000000..09d5edc59
--- /dev/null
+++ b/devtools/client/commandline/test/browser_cmd_paintflashing.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+* http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the paintflashing command correctly sets its state.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/commandline/" +
+ "test/browser_cmd_cookie.html";
+
+function test() {
+ return Task.spawn(testTask).then(finish, helpers.handleError);
+}
+
+var tests = {
+ testInput: function (options) {
+ let toggleCommand = options.requisition.system.commands.get("paintflashing toggle");
+
+ let _tab = options.tab;
+
+ let actions = [
+ {
+ command: "paintflashing on",
+ isChecked: true,
+ label: "checked after on"
+ },
+ {
+ command: "paintflashing off",
+ isChecked: false,
+ label: "unchecked after off"
+ },
+ {
+ command: "paintflashing toggle",
+ isChecked: true,
+ label: "checked after toggle"
+ },
+ {
+ command: "paintflashing toggle",
+ isChecked: false,
+ label: "unchecked after toggle"
+ }
+ ];
+
+ return helpers.audit(options, actions.map(spec => ({
+ setup: spec.command,
+ exec: {},
+ post: () => is(toggleCommand.state.isChecked({_tab}), spec.isChecked, spec.label)
+ })));
+ },
+};
+
+function* testTask() {
+ let options = yield helpers.openTab(TEST_URI);
+ yield helpers.openToolbar(options);
+
+ yield helpers.runTests(options, tests);
+
+ yield helpers.closeToolbar(options);
+ yield helpers.closeTab(options);
+}
diff --git a/devtools/client/commandline/test/browser_cmd_pref1.js b/devtools/client/commandline/test/browser_cmd_pref1.js
new file mode 100644
index 000000000..4dc34f7ae
--- /dev/null
+++ b/devtools/client/commandline/test/browser_cmd_pref1.js
@@ -0,0 +1,154 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the pref commands work
+
+var prefBranch = Cc["@mozilla.org/preferences-service;1"]
+ .getService(Ci.nsIPrefService).getBranch(null)
+ .QueryInterface(Ci.nsIPrefBranch2);
+
+const TEST_URI = "data:text/html;charset=utf-8,gcli-pref1";
+
+function test() {
+ return Task.spawn(spawnTest).then(finish, helpers.handleError);
+}
+
+function* spawnTest() {
+ let options = yield helpers.openTab(TEST_URI);
+ yield helpers.openToolbar(options);
+
+ let netmonEnabledOrig = prefBranch.getBoolPref("devtools.netmonitor.enabled");
+ info("originally: devtools.netmonitor.enabled = " + netmonEnabledOrig);
+
+ yield helpers.audit(options, [
+ {
+ setup: "pref",
+ check: {
+ input: "pref",
+ hints: " reset",
+ markup: "IIII",
+ status: "ERROR"
+ },
+ },
+ {
+ setup: "pref s",
+ check: {
+ input: "pref s",
+ hints: "et",
+ markup: "IIIIVI",
+ status: "ERROR"
+ },
+ },
+ {
+ setup: "pref sh",
+ check: {
+ input: "pref sh",
+ hints: "ow",
+ markup: "IIIIVII",
+ status: "ERROR"
+ },
+ },
+ {
+ setup: "pref show ",
+ check: {
+ input: "pref show ",
+ markup: "VVVVVVVVVV",
+ status: "ERROR"
+ },
+ },
+ {
+ setup: "pref show usetexttospeech",
+ check: {
+ input: "pref show usetexttospeech",
+ hints: " -> accessibility.usetexttospeech",
+ markup: "VVVVVVVVVVIIIIIIIIIIIIIII",
+ status: "ERROR"
+ },
+ },
+ {
+ setup: "pref show devtools.netmoni",
+ check: {
+ input: "pref show devtools.netmoni",
+ hints: "tor.enabled",
+ markup: "VVVVVVVVVVIIIIIIIIIIIIIIII",
+ status: "ERROR",
+ tooltipState: "true:importantFieldFlag",
+ args: {
+ setting: { value: undefined, status: "INCOMPLETE" },
+ }
+ },
+ },
+ {
+ setup: "pref reset devtools.netmonitor.enabled",
+ check: {
+ input: "pref reset devtools.netmonitor.enabled",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV",
+ status: "VALID"
+ },
+ },
+ {
+ setup: "pref show devtools.netmonitor.enabled 4",
+ check: {
+ input: "pref show devtools.netmonitor.enabled 4",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVE",
+ status: "ERROR"
+ },
+ },
+ {
+ setup: "pref set devtools.netmonitor.enabled 4",
+ check: {
+ input: "pref set devtools.netmonitor.enabled 4",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVE",
+ status: "ERROR",
+ args: {
+ setting: { arg: " devtools.netmonitor.enabled" },
+ value: { status: "ERROR", message: "Can\u2019t use \u20184\u2019." },
+ }
+ },
+ },
+ {
+ setup: "pref set devtools.editor.tabsize 4",
+ check: {
+ input: "pref set devtools.editor.tabsize 4",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV",
+ status: "VALID",
+ args: {
+ setting: { arg: " devtools.editor.tabsize" },
+ value: { value: 4 },
+ }
+ },
+ },
+ {
+ setup: "pref list",
+ check: {
+ input: "pref list",
+ hints: " -> pref set",
+ markup: "IIIIVIIII",
+ status: "ERROR"
+ },
+ },
+ {
+ setup: "pref show devtools.netmonitor.enabled",
+ check: {
+ args: {
+ setting: {
+ value: options.requisition.system.settings.get("devtools.netmonitor.enabled")
+ }
+ },
+ },
+ exec: {
+ output: "devtools.netmonitor.enabled: " + netmonEnabledOrig,
+ },
+ post: function () {
+ prefBranch.setBoolPref("devtools.netmonitor.enabled", netmonEnabledOrig);
+ }
+ },
+ ]);
+
+ yield helpers.closeToolbar(options);
+ yield helpers.closeTab(options);
+}
diff --git a/devtools/client/commandline/test/browser_cmd_pref2.js b/devtools/client/commandline/test/browser_cmd_pref2.js
new file mode 100644
index 000000000..44619517e
--- /dev/null
+++ b/devtools/client/commandline/test/browser_cmd_pref2.js
@@ -0,0 +1,105 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the pref commands work
+
+var prefBranch = Cc["@mozilla.org/preferences-service;1"]
+ .getService(Ci.nsIPrefService).getBranch(null)
+ .QueryInterface(Ci.nsIPrefBranch2);
+
+const TEST_URI = "data:text/html;charset=utf-8,gcli-pref2";
+
+function test() {
+ return Task.spawn(spawnTest).then(finish, helpers.handleError);
+}
+
+function* spawnTest() {
+ let options = yield helpers.openTab(TEST_URI);
+ yield helpers.openToolbar(options);
+
+ let tabSizeOrig = prefBranch.getIntPref("devtools.editor.tabsize");
+ info("originally: devtools.editor.tabsize = " + tabSizeOrig);
+
+ yield helpers.audit(options, [
+ {
+ setup: "pref show devtools.editor.tabsize",
+ check: {
+ args: {
+ setting: {
+ value: options.requisition.system.settings.get("devtools.editor.tabsize")
+ }
+ },
+ },
+ exec: {
+ output: "devtools.editor.tabsize: " + tabSizeOrig,
+ },
+ },
+ {
+ setup: "pref set devtools.editor.tabsize 20",
+ check: {
+ args: {
+ setting: {
+ value: options.requisition.system.settings.get("devtools.editor.tabsize")
+ },
+ value: { value: 20 }
+ },
+ },
+ exec: {
+ output: "",
+ },
+ post: function () {
+ is(prefBranch.getIntPref("devtools.editor.tabsize"), 20,
+ "devtools.editor.tabsize is 20");
+ }
+ },
+ {
+ setup: "pref show devtools.editor.tabsize",
+ check: {
+ args: {
+ setting: {
+ value: options.requisition.system.settings.get("devtools.editor.tabsize")
+ }
+ },
+ },
+ exec: {
+ output: "devtools.editor.tabsize: 20",
+ }
+ },
+ {
+ setup: "pref set devtools.editor.tabsize 1",
+ check: {
+ args: {
+ setting: {
+ value: options.requisition.system.settings.get("devtools.editor.tabsize")
+ },
+ value: { value: 1 }
+ },
+ },
+ exec: {
+ output: "",
+ },
+ },
+ {
+ setup: "pref show devtools.editor.tabsize",
+ check: {
+ args: {
+ setting: {
+ value: options.requisition.system.settings.get("devtools.editor.tabsize")
+ }
+ },
+ },
+ exec: {
+ output: "devtools.editor.tabsize: 1",
+ },
+ post: function () {
+ is(prefBranch.getIntPref("devtools.editor.tabsize"), 1,
+ "devtools.editor.tabsize is 1");
+ }
+ },
+ ]);
+
+ prefBranch.setIntPref("devtools.editor.tabsize", tabSizeOrig);
+
+ yield helpers.closeToolbar(options);
+ yield helpers.closeTab(options);
+}
diff --git a/devtools/client/commandline/test/browser_cmd_pref3.js b/devtools/client/commandline/test/browser_cmd_pref3.js
new file mode 100644
index 000000000..8ce5ffe5a
--- /dev/null
+++ b/devtools/client/commandline/test/browser_cmd_pref3.js
@@ -0,0 +1,113 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the pref commands work
+
+var prefBranch = Cc["@mozilla.org/preferences-service;1"]
+ .getService(Ci.nsIPrefService).getBranch(null)
+ .QueryInterface(Ci.nsIPrefBranch2);
+
+var supportsString = Cc["@mozilla.org/supports-string;1"]
+ .createInstance(Ci.nsISupportsString);
+
+const TEST_URI = "data:text/html;charset=utf-8,gcli-pref3";
+
+function test() {
+ return Task.spawn(spawnTest).then(finish, helpers.handleError);
+}
+
+function* spawnTest() {
+ let options = yield helpers.openTab(TEST_URI);
+ yield helpers.openToolbar(options);
+
+ let remoteHostOrig = prefBranch.getComplexValue("devtools.debugger.remote-host",
+ Ci.nsISupportsString).data;
+ info("originally: devtools.debugger.remote-host = " + remoteHostOrig);
+
+ yield helpers.audit(options, [
+ {
+ setup: "pref show devtools.debugger.remote-host",
+ check: {
+ args: {
+ setting: {
+ value: options.requisition.system.settings.get("devtools.debugger.remote-host")
+ }
+ },
+ },
+ exec: {
+ output: new RegExp("^devtools\.debugger\.remote-host: " + remoteHostOrig + "$"),
+ },
+ },
+ {
+ setup: "pref set devtools.debugger.remote-host e.com",
+ check: {
+ args: {
+ setting: {
+ value: options.requisition.system.settings.get("devtools.debugger.remote-host")
+ },
+ value: { value: "e.com" }
+ },
+ },
+ exec: {
+ output: "",
+ },
+ },
+ {
+ setup: "pref show devtools.debugger.remote-host",
+ check: {
+ args: {
+ setting: {
+ value: options.requisition.system.settings.get("devtools.debugger.remote-host")
+ }
+ },
+ },
+ exec: {
+ output: new RegExp("^devtools\.debugger\.remote-host: e.com$"),
+ },
+ post: function () {
+ var ecom = prefBranch.getComplexValue("devtools.debugger.remote-host",
+ Ci.nsISupportsString).data;
+ is(ecom, "e.com", "devtools.debugger.remote-host is e.com");
+ }
+ },
+ {
+ setup: "pref set devtools.debugger.remote-host moz.foo",
+ check: {
+ args: {
+ setting: {
+ value: options.requisition.system.settings.get("devtools.debugger.remote-host")
+ },
+ value: { value: "moz.foo" }
+ },
+ },
+ exec: {
+ output: "",
+ },
+ },
+ {
+ setup: "pref show devtools.debugger.remote-host",
+ check: {
+ args: {
+ setting: {
+ value: options.requisition.system.settings.get("devtools.debugger.remote-host")
+ }
+ },
+ },
+ exec: {
+ output: new RegExp("^devtools\.debugger\.remote-host: moz.foo$"),
+ },
+ post: function () {
+ var mozfoo = prefBranch.getComplexValue("devtools.debugger.remote-host",
+ Ci.nsISupportsString).data;
+ is(mozfoo, "moz.foo", "devtools.debugger.remote-host is moz.foo");
+ }
+ },
+ ]);
+
+ supportsString.data = remoteHostOrig;
+ prefBranch.setComplexValue("devtools.debugger.remote-host",
+ Ci.nsISupportsString, supportsString);
+
+ yield helpers.closeToolbar(options);
+ yield helpers.closeTab(options);
+}
diff --git a/devtools/client/commandline/test/browser_cmd_qsa.js b/devtools/client/commandline/test/browser_cmd_qsa.js
new file mode 100644
index 000000000..551d47739
--- /dev/null
+++ b/devtools/client/commandline/test/browser_cmd_qsa.js
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+* http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the qsa commands work as they should.
+
+const TEST_URI = "data:text/html;charset=utf-8,<body></body>";
+
+function test() {
+ helpers.addTabWithToolbar(TEST_URI, function (options) {
+ return helpers.audit(options, [
+ {
+ setup: "qsa",
+ check: {
+ input: "qsa",
+ hints: " [query]",
+ markup: "VVV",
+ status: "VALID"
+ }
+ },
+ {
+ setup: "qsa body",
+ check: {
+ input: "qsa body",
+ hints: "",
+ markup: "VVVVVVVV",
+ status: "VALID"
+ }
+ }
+ ]);
+ }).then(finish, helpers.handleError);
+}
diff --git a/devtools/client/commandline/test/browser_cmd_restart.js b/devtools/client/commandline/test/browser_cmd_restart.js
new file mode 100644
index 000000000..cf4c1a5d5
--- /dev/null
+++ b/devtools/client/commandline/test/browser_cmd_restart.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that restart command works properly (input wise)
+
+const TEST_URI = "data:text/html;charset=utf-8,gcli-command-restart";
+
+function test() {
+ helpers.addTabWithToolbar(TEST_URI, function (options) {
+ return helpers.audit(options, [
+ {
+ setup: "restart",
+ check: {
+ input: "restart",
+ markup: "VVVVVVV",
+ status: "VALID",
+ args: {
+ nocache: { value: false },
+ safemode: { value: false },
+ }
+ },
+ },
+ {
+ setup: "restart --nocache",
+ check: {
+ input: "restart --nocache",
+ markup: "VVVVVVVVVVVVVVVVV",
+ status: "VALID",
+ args: {
+ nocache: { value: true },
+ safemode: { value: false },
+ }
+ },
+ },
+ {
+ setup: "restart --safemode",
+ check: {
+ input: "restart --safemode",
+ markup: "VVVVVVVVVVVVVVVVVV",
+ status: "VALID",
+ args: {
+ nocache: { value: false },
+ safemode: { value: true },
+ }
+ },
+ },
+ {
+ setup: "restart --safemode --nocache",
+ check: {
+ input: "restart --safemode --nocache",
+ markup: "VVVVVVVVVVVVVVVVVVVVVVVVVVVV",
+ status: "VALID",
+ args: {
+ nocache: { value: true },
+ safemode: { value: true },
+ }
+ },
+ },
+ ]);
+ }).then(finish, helpers.handleError);
+}
diff --git a/devtools/client/commandline/test/browser_cmd_rulers.js b/devtools/client/commandline/test/browser_cmd_rulers.js
new file mode 100644
index 000000000..8fc099257
--- /dev/null
+++ b/devtools/client/commandline/test/browser_cmd_rulers.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests the various highlight command parameters and options
+
+var TEST_PAGE = "data:text/html;charset=utf-8,foo";
+
+function test() {
+ return Task.spawn(spawnTest).then(finish, helpers.handleError);
+}
+
+function* spawnTest() {
+ let options = yield helpers.openTab(TEST_PAGE);
+ yield helpers.openToolbar(options);
+
+ yield helpers.audit(options, [
+ {
+ setup: "rulers",
+ check: {
+ input: "rulers",
+ markup: "VVVVVV",
+ status: "VALID"
+ }
+ },
+ {
+ setup: "rulers on",
+ check: {
+ input: "rulers on",
+ markup: "VVVVVVVEE",
+ status: "ERROR"
+ },
+ exec: {
+ output: "Error: Too many arguments"
+ }
+ },
+ {
+ setup: "rulers --visible",
+ check: {
+ input: "rulers --visible",
+ markup: "VVVVVVVEEEEEEEEE",
+ status: "ERROR"
+ },
+ exec: {
+ output: "Error: Too many arguments"
+ }
+ }
+ ]);
+
+ yield helpers.closeToolbar(options);
+ yield helpers.closeTab(options);
+}
diff --git a/devtools/client/commandline/test/browser_cmd_screenshot.html b/devtools/client/commandline/test/browser_cmd_screenshot.html
new file mode 100644
index 000000000..d86966188
--- /dev/null
+++ b/devtools/client/commandline/test/browser_cmd_screenshot.html
@@ -0,0 +1,18 @@
+<html>
+ <head>
+ <style>
+ img {
+ height: 100px;
+ width: 100px;
+ }
+ .overflow {
+ overflow: scroll;
+ height: 200%;
+ width: 200%;
+ }
+ </style>
+ </head>
+ <body>
+ <img id="testImage" ></img>
+ </body>
+</html>
diff --git a/devtools/client/commandline/test/browser_cmd_screenshot.js b/devtools/client/commandline/test/browser_cmd_screenshot.js
new file mode 100644
index 000000000..e7f3d0587
--- /dev/null
+++ b/devtools/client/commandline/test/browser_cmd_screenshot.js
@@ -0,0 +1,374 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* global helpers, btoa, whenDelayedStartupFinished, OpenBrowserWindow */
+
+// Test that screenshot command works properly
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/commandline/" +
+ "test/browser_cmd_screenshot.html";
+
+var FileUtils = (Cu.import("resource://gre/modules/FileUtils.jsm", {})).FileUtils;
+
+function test() {
+ // This test gets bombarded by a cascade of GCs and often takes 50s so lets be
+ // safe and give the test 90s to run.
+ requestLongerTimeout(3);
+
+ return Task.spawn(spawnTest).then(finish, helpers.handleError);
+}
+
+function* spawnTest() {
+ waitForExplicitFinish();
+
+ info("RUN TEST: non-private window");
+ let normWin = yield addWindow({ private: false });
+ yield addTabWithToolbarRunTests(normWin);
+ normWin.close();
+
+ info("RUN TEST: private window");
+ let pbWin = yield addWindow({ private: true });
+ yield addTabWithToolbarRunTests(pbWin);
+ pbWin.close();
+}
+
+function* addTabWithToolbarRunTests(win) {
+ let options = yield helpers.openTab(TEST_URI, { chromeWindow: win });
+ let browser = options.browser;
+ yield helpers.openToolbar(options);
+
+ // Test input status
+ yield helpers.audit(options, [
+ {
+ setup: "screenshot",
+ check: {
+ input: "screenshot",
+ markup: "VVVVVVVVVV",
+ status: "VALID",
+ args: {
+ }
+ },
+ },
+ {
+ setup: "screenshot abc.png",
+ check: {
+ input: "screenshot abc.png",
+ markup: "VVVVVVVVVVVVVVVVVV",
+ status: "VALID",
+ args: {
+ filename: { value: "abc.png"},
+ }
+ },
+ },
+ {
+ setup: "screenshot --fullpage",
+ check: {
+ input: "screenshot --fullpage",
+ markup: "VVVVVVVVVVVVVVVVVVVVV",
+ status: "VALID",
+ args: {
+ fullpage: { value: true},
+ }
+ },
+ },
+ {
+ setup: "screenshot abc --delay 5",
+ check: {
+ input: "screenshot abc --delay 5",
+ markup: "VVVVVVVVVVVVVVVVVVVVVVVV",
+ status: "VALID",
+ args: {
+ filename: { value: "abc"},
+ delay: { value: 5 },
+ }
+ },
+ },
+ {
+ setup: "screenshot --selector img#testImage",
+ check: {
+ input: "screenshot --selector img#testImage",
+ markup: "VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV",
+ status: "VALID",
+ },
+ },
+ ]);
+
+ // Test capture to file
+ let file = FileUtils.getFile("TmpD", [ "TestScreenshotFile.png" ]);
+
+ yield helpers.audit(options, [
+ {
+ setup: "screenshot " + file.path,
+ check: {
+ args: {
+ filename: { value: "" + file.path },
+ fullpage: { value: false },
+ clipboard: { value: false },
+ },
+ },
+ exec: {
+ output: new RegExp("^Saved to "),
+ },
+ post: function () {
+ // Bug 849168: screenshot command tests fail in try but not locally
+ // ok(file.exists(), "Screenshot file exists");
+
+ if (file.exists()) {
+ file.remove(false);
+ }
+ }
+ },
+ ]);
+
+ // Test capture to clipboard
+ yield helpers.audit(options, [
+ {
+ setup: "screenshot --clipboard",
+ check: {
+ args: {
+ clipboard: { value: true },
+ },
+ },
+ exec: {
+ output: new RegExp("^Copied to clipboard.$"),
+ },
+ post: Task.async(function* () {
+ let imgSize1 = yield getImageSizeFromClipboard();
+ yield ContentTask.spawn(browser, imgSize1, function* (imgSize) {
+ Assert.equal(imgSize.width, content.innerWidth,
+ "Image width matches window size");
+ Assert.equal(imgSize.height, content.innerHeight,
+ "Image height matches window size");
+ });
+ })
+ },
+ {
+ setup: "screenshot --fullpage --clipboard",
+ check: {
+ args: {
+ fullpage: { value: true },
+ clipboard: { value: true },
+ },
+ },
+ exec: {
+ output: new RegExp("^Copied to clipboard.$"),
+ },
+ post: Task.async(function* () {
+ let imgSize1 = yield getImageSizeFromClipboard();
+ yield ContentTask.spawn(browser, imgSize1, function* (imgSize) {
+ Assert.equal(imgSize.width,
+ content.innerWidth + content.scrollMaxX - content.scrollMinX,
+ "Image width matches page size");
+ Assert.equal(imgSize.height,
+ content.innerHeight + content.scrollMaxY - content.scrollMinY,
+ "Image height matches page size");
+ });
+ })
+ },
+ {
+ setup: "screenshot --selector img#testImage --clipboard",
+ check: {
+ args: {
+ clipboard: { value: true },
+ },
+ },
+ exec: {
+ output: new RegExp("^Copied to clipboard.$"),
+ },
+ post: Task.async(function* () {
+ let imgSize1 = yield getImageSizeFromClipboard();
+ yield ContentTask.spawn(browser, imgSize1, function* (imgSize) {
+ let img = content.document.querySelector("img#testImage");
+ Assert.equal(imgSize.width, img.clientWidth,
+ "Image width matches element size");
+ Assert.equal(imgSize.height, img.clientHeight,
+ "Image height matches element size");
+ });
+ })
+ },
+ ]);
+
+ // Trigger scrollbars by forcing document to overflow
+ // This only affects results on OSes with scrollbars that reduce document size
+ // (non-floating scrollbars). With default OS settings, this means Windows
+ // and Linux are affected, but Mac is not. For Mac to exhibit this behavior,
+ // change System Preferences -> General -> Show scroll bars to Always.
+ yield ContentTask.spawn(browser, {}, function* () {
+ content.document.body.classList.add("overflow");
+ });
+
+ let scrollbarSize = yield ContentTask.spawn(browser, {}, function* () {
+ const winUtils = content.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ let scrollbarHeight = {};
+ let scrollbarWidth = {};
+ winUtils.getScrollbarSize(true, scrollbarWidth, scrollbarHeight);
+ return {
+ width: scrollbarWidth.value,
+ height: scrollbarHeight.value,
+ };
+ });
+
+ info(`Scrollbar size: ${scrollbarSize.width}x${scrollbarSize.height}`);
+
+ // Test capture to clipboard in presence of scrollbars
+ yield helpers.audit(options, [
+ {
+ setup: "screenshot --clipboard",
+ check: {
+ args: {
+ clipboard: { value: true },
+ },
+ },
+ exec: {
+ output: new RegExp("^Copied to clipboard.$"),
+ },
+ post: Task.async(function* () {
+ let imgSize1 = yield getImageSizeFromClipboard();
+ imgSize1.scrollbarWidth = scrollbarSize.width;
+ imgSize1.scrollbarHeight = scrollbarSize.height;
+ yield ContentTask.spawn(browser, imgSize1, function* (imgSize) {
+ Assert.equal(imgSize.width, content.innerWidth - imgSize.scrollbarWidth,
+ "Image width matches window size minus scrollbar size");
+ Assert.equal(imgSize.height, content.innerHeight - imgSize.scrollbarHeight,
+ "Image height matches window size minus scrollbar size");
+ });
+ })
+ },
+ {
+ setup: "screenshot --fullpage --clipboard",
+ check: {
+ args: {
+ fullpage: { value: true },
+ clipboard: { value: true },
+ },
+ },
+ exec: {
+ output: new RegExp("^Copied to clipboard.$"),
+ },
+ post: Task.async(function* () {
+ let imgSize1 = yield getImageSizeFromClipboard();
+ imgSize1.scrollbarWidth = scrollbarSize.width;
+ imgSize1.scrollbarHeight = scrollbarSize.height;
+ yield ContentTask.spawn(browser, imgSize1, function* (imgSize) {
+ Assert.equal(imgSize.width,
+ (content.innerWidth + content.scrollMaxX -
+ content.scrollMinX) - imgSize.scrollbarWidth,
+ "Image width matches page size minus scrollbar size");
+ Assert.equal(imgSize.height,
+ (content.innerHeight + content.scrollMaxY -
+ content.scrollMinY) - imgSize.scrollbarHeight,
+ "Image height matches page size minus scrollbar size");
+ });
+ })
+ },
+ {
+ setup: "screenshot --selector img#testImage --clipboard",
+ check: {
+ args: {
+ clipboard: { value: true },
+ },
+ },
+ exec: {
+ output: new RegExp("^Copied to clipboard.$"),
+ },
+ post: Task.async(function* () {
+ let imgSize1 = yield getImageSizeFromClipboard();
+ yield ContentTask.spawn(browser, imgSize1, function* (imgSize) {
+ let img = content.document.querySelector("img#testImage");
+ Assert.equal(imgSize.width, img.clientWidth,
+ "Image width matches element size");
+ Assert.equal(imgSize.height, img.clientHeight,
+ "Image height matches element size");
+ });
+ })
+ },
+ ]);
+
+ yield helpers.closeToolbar(options);
+ yield helpers.closeTab(options);
+}
+
+function addWindow(windowOptions) {
+ return new Promise(resolve => {
+ let win = OpenBrowserWindow(windowOptions);
+
+ // This feels hacky, we should refactor it
+ whenDelayedStartupFinished(win, () => {
+ // Would like to get rid of this executeSoon, but without it the url
+ // (TEST_URI) provided in addTabWithToolbarRunTests hasn't loaded
+ executeSoon(() => {
+ resolve(win);
+ });
+ });
+ });
+}
+
+let getImageSizeFromClipboard = Task.async(function* () {
+ let clipid = Ci.nsIClipboard;
+ let clip = Cc["@mozilla.org/widget/clipboard;1"].getService(clipid);
+ let trans = Cc["@mozilla.org/widget/transferable;1"]
+ .createInstance(Ci.nsITransferable);
+ let flavor = "image/png";
+ trans.init(null);
+ trans.addDataFlavor(flavor);
+
+ clip.getData(trans, clipid.kGlobalClipboard);
+ let data = new Object();
+ let dataLength = new Object();
+ trans.getTransferData(flavor, data, dataLength);
+
+ ok(data.value, "screenshot exists");
+ ok(dataLength.value > 0, "screenshot has length");
+
+ let image = data.value;
+ let dataURI = `data:${flavor};base64,`;
+
+ // Due to the differences in how images could be stored in the clipboard the
+ // checks below are needed. The clipboard could already provide the image as
+ // byte streams, but also as pointer, or as image container. If it's not
+ // possible obtain a byte stream, the function returns `null`.
+ if (image instanceof Ci.nsISupportsInterfacePointer) {
+ image = image.data;
+ }
+
+ if (image instanceof Ci.imgIContainer) {
+ image = Cc["@mozilla.org/image/tools;1"]
+ .getService(Ci.imgITools)
+ .encodeImage(image, flavor);
+ }
+
+ if (image instanceof Ci.nsIInputStream) {
+ let binaryStream = Cc["@mozilla.org/binaryinputstream;1"]
+ .createInstance(Ci.nsIBinaryInputStream);
+ binaryStream.setInputStream(image);
+ let rawData = binaryStream.readBytes(binaryStream.available());
+ let charCodes = Array.from(rawData, c => c.charCodeAt(0) & 0xff);
+ let encodedData = String.fromCharCode(...charCodes);
+ encodedData = btoa(encodedData);
+ dataURI = dataURI + encodedData;
+ } else {
+ throw new Error("Unable to read image data");
+ }
+
+ let img = document.createElementNS("http://www.w3.org/1999/xhtml", "img");
+
+ let loaded = new Promise(resolve => {
+ img.addEventListener("load", function onLoad() {
+ img.removeEventListener("load", onLoad);
+ resolve();
+ });
+ });
+
+ img.src = dataURI;
+ document.documentElement.appendChild(img);
+ yield loaded;
+ img.remove();
+
+ return {
+ width: img.width,
+ height: img.height,
+ };
+});
diff --git a/devtools/client/commandline/test/browser_cmd_settings.js b/devtools/client/commandline/test/browser_cmd_settings.js
new file mode 100644
index 000000000..051d81f2f
--- /dev/null
+++ b/devtools/client/commandline/test/browser_cmd_settings.js
@@ -0,0 +1,124 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the pref commands work
+
+var prefBranch = Cc["@mozilla.org/preferences-service;1"]
+ .getService(Ci.nsIPrefService).getBranch(null)
+ .QueryInterface(Ci.nsIPrefBranch2);
+
+var supportsString = Cc["@mozilla.org/supports-string;1"]
+ .createInstance(Ci.nsISupportsString);
+
+const TEST_URI = "data:text/html;charset=utf-8,gcli-settings";
+
+function test() {
+ return Task.spawn(spawnTest).then(finish, helpers.handleError);
+}
+
+function* spawnTest() {
+ // Setup
+ let options = yield helpers.openTab(TEST_URI);
+
+ const { createSystem } = require("gcli/system");
+ const system = createSystem({ location: "server" });
+
+ const gcliInit = require("devtools/shared/gcli/commands/index");
+ gcliInit.addAllItemsByModule(system);
+ yield system.load();
+
+ let settings = system.settings;
+
+ let hideIntroEnabled = settings.get("devtools.gcli.hideIntro");
+ let tabSize = settings.get("devtools.editor.tabsize");
+ let remoteHost = settings.get("devtools.debugger.remote-host");
+
+ let hideIntroOrig = prefBranch.getBoolPref("devtools.gcli.hideIntro");
+ let tabSizeOrig = prefBranch.getIntPref("devtools.editor.tabsize");
+ let remoteHostOrig = prefBranch.getComplexValue(
+ "devtools.debugger.remote-host",
+ Components.interfaces.nsISupportsString).data;
+
+ info("originally: devtools.gcli.hideIntro = " + hideIntroOrig);
+ info("originally: devtools.editor.tabsize = " + tabSizeOrig);
+ info("originally: devtools.debugger.remote-host = " + remoteHostOrig);
+
+ // Actual tests
+ is(hideIntroEnabled.value, hideIntroOrig, "hideIntroEnabled default");
+ is(tabSize.value, tabSizeOrig, "tabSize default");
+ is(remoteHost.value, remoteHostOrig, "remoteHost default");
+
+ hideIntroEnabled.setDefault();
+ tabSize.setDefault();
+ remoteHost.setDefault();
+
+ let hideIntroEnabledDefault = hideIntroEnabled.value;
+ let tabSizeDefault = tabSize.value;
+ let remoteHostDefault = remoteHost.value;
+
+ hideIntroEnabled.value = false;
+ tabSize.value = 42;
+ remoteHost.value = "example.com";
+
+ is(hideIntroEnabled.value, false, "hideIntroEnabled basic");
+ is(tabSize.value, 42, "tabSize basic");
+ is(remoteHost.value, "example.com", "remoteHost basic");
+
+ function hideIntroEnabledCheck(ev) {
+ is(ev.setting, hideIntroEnabled, "hideIntroEnabled event setting");
+ is(ev.value, true, "hideIntroEnabled event value");
+ is(ev.setting.value, true, "hideIntroEnabled event setting value");
+ }
+ hideIntroEnabled.onChange.add(hideIntroEnabledCheck);
+ hideIntroEnabled.value = true;
+ is(hideIntroEnabled.value, true, "hideIntroEnabled change");
+
+ function tabSizeCheck(ev) {
+ is(ev.setting, tabSize, "tabSize event setting");
+ is(ev.value, 1, "tabSize event value");
+ is(ev.setting.value, 1, "tabSize event setting value");
+ }
+ tabSize.onChange.add(tabSizeCheck);
+ tabSize.value = 1;
+ is(tabSize.value, 1, "tabSize change");
+
+ function remoteHostCheck(ev) {
+ is(ev.setting, remoteHost, "remoteHost event setting");
+ is(ev.value, "y.com", "remoteHost event value");
+ is(ev.setting.value, "y.com", "remoteHost event setting value");
+ }
+ remoteHost.onChange.add(remoteHostCheck);
+ remoteHost.value = "y.com";
+ is(remoteHost.value, "y.com", "remoteHost change");
+
+ hideIntroEnabled.onChange.remove(hideIntroEnabledCheck);
+ tabSize.onChange.remove(tabSizeCheck);
+ remoteHost.onChange.remove(remoteHostCheck);
+
+ function remoteHostReCheck(ev) {
+ is(ev.setting, remoteHost, "remoteHost event reset");
+ is(ev.value, null, "remoteHost event revalue");
+ is(ev.setting.value, null, "remoteHost event setting revalue");
+ }
+ remoteHost.onChange.add(remoteHostReCheck);
+
+ hideIntroEnabled.setDefault();
+ tabSize.setDefault();
+ remoteHost.setDefault();
+
+ remoteHost.onChange.remove(remoteHostReCheck);
+
+ is(hideIntroEnabled.value, hideIntroEnabledDefault, "hideIntroEnabled reset");
+ is(tabSize.value, tabSizeDefault, "tabSize reset");
+ is(remoteHost.value, remoteHostDefault, "remoteHost reset");
+
+ // Cleanup
+ prefBranch.setBoolPref("devtools.gcli.hideIntro", hideIntroOrig);
+ prefBranch.setIntPref("devtools.editor.tabsize", tabSizeOrig);
+ supportsString.data = remoteHostOrig;
+ prefBranch.setComplexValue("devtools.debugger.remote-host",
+ Components.interfaces.nsISupportsString,
+ supportsString);
+
+ yield helpers.closeTab(options);
+}
diff --git a/devtools/client/commandline/test/browser_gcli_async.js b/devtools/client/commandline/test/browser_gcli_async.js
new file mode 100644
index 000000000..4ce564d8a
--- /dev/null
+++ b/devtools/client/commandline/test/browser_gcli_async.js
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2012, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+"use strict";
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
+
+const exports = {};
+
+function test() {
+ helpers.runTestModule(exports, "browser_gcli_async.js");
+}
+
+// var helpers = require('./helpers');
+
+exports.testBasic = function (options) {
+ return helpers.audit(options, [
+ {
+ setup: "tsslo",
+ check: {
+ input: "tsslo",
+ hints: "w",
+ markup: "IIIII",
+ cursor: 5,
+ current: "__command",
+ status: "ERROR",
+ predictions: ["tsslow"],
+ unassigned: [ ]
+ }
+ },
+ {
+ setup: "tsslo<TAB>",
+ check: {
+ input: "tsslow ",
+ hints: "Shalom",
+ markup: "VVVVVVV",
+ cursor: 7,
+ current: "hello",
+ status: "ERROR",
+ predictions: [
+ "Shalom", "Namasté", "Hallo", "Dydd-da", "Chào", "Hej",
+ "Saluton", "Sawubona"
+ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "tsslow" },
+ hello: {
+ arg: "",
+ status: "INCOMPLETE"
+ },
+ }
+ }
+ },
+ {
+ setup: "tsslow S",
+ check: {
+ input: "tsslow S",
+ hints: "halom",
+ markup: "VVVVVVVI",
+ cursor: 8,
+ current: "hello",
+ status: "ERROR",
+ predictions: [ "Shalom", "Saluton", "Sawubona", "Namasté" ],
+ unassigned: [ ],
+ args: {
+ command: { name: "tsslow" },
+ hello: {
+ arg: " S",
+ status: "INCOMPLETE"
+ },
+ }
+ }
+ },
+ {
+ setup: "tsslow S<TAB>",
+ check: {
+ input: "tsslow Shalom ",
+ hints: "",
+ markup: "VVVVVVVVVVVVVV",
+ cursor: 14,
+ current: "hello",
+ status: "VALID",
+ predictions: [ "Shalom" ],
+ unassigned: [ ],
+ args: {
+ command: { name: "tsslow" },
+ hello: {
+ arg: " Shalom ",
+ status: "VALID",
+ message: ""
+ },
+ }
+ }
+ }
+ ]);
+};
diff --git a/devtools/client/commandline/test/browser_gcli_canon.js b/devtools/client/commandline/test/browser_gcli_canon.js
new file mode 100644
index 000000000..807244505
--- /dev/null
+++ b/devtools/client/commandline/test/browser_gcli_canon.js
@@ -0,0 +1,286 @@
+/*
+ * Copyright 2012, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+"use strict";
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
+
+const exports = {};
+
+function test() {
+ helpers.runTestModule(exports, "browser_gcli_canon.js");
+}
+
+// var assert = require('../testharness/assert');
+// var helpers = require('./helpers');
+var Commands = require("gcli/commands/commands").Commands;
+
+var startCount;
+var events;
+
+var commandsChange = function (ev) {
+ events++;
+};
+
+exports.setup = function (options) {
+ startCount = options.requisition.system.commands.getAll().length;
+ events = 0;
+};
+
+exports.shutdown = function (options) {
+ startCount = undefined;
+ events = undefined;
+};
+
+exports.testAddRemove1 = function (options) {
+ var commands = options.requisition.system.commands;
+
+ return helpers.audit(options, [
+ {
+ name: "testadd add",
+ setup: function () {
+ commands.onCommandsChange.add(commandsChange);
+
+ commands.add({
+ name: "testadd",
+ exec: function () {
+ return 1;
+ }
+ });
+
+ assert.is(commands.getAll().length,
+ startCount + 1,
+ "add command success");
+ assert.is(events, 1, "add event");
+
+ return helpers.setInput(options, "testadd");
+ },
+ check: {
+ input: "testadd",
+ hints: "",
+ markup: "VVVVVVV",
+ cursor: 7,
+ current: "__command",
+ status: "VALID",
+ predictions: [ ],
+ unassigned: [ ],
+ args: { }
+ },
+ exec: {
+ output: /^1$/
+ }
+ },
+ {
+ name: "testadd alter",
+ setup: function () {
+ commands.add({
+ name: "testadd",
+ exec: function () {
+ return 2;
+ }
+ });
+
+ assert.is(commands.getAll().length,
+ startCount + 1,
+ "read command success");
+ assert.is(events, 2, "read event");
+
+ return helpers.setInput(options, "testadd");
+ },
+ check: {
+ input: "testadd",
+ hints: "",
+ markup: "VVVVVVV",
+ },
+ exec: {
+ output: "2"
+ }
+ },
+ {
+ name: "testadd remove",
+ setup: function () {
+ commands.remove("testadd");
+
+ assert.is(commands.getAll().length,
+ startCount,
+ "remove command success");
+ assert.is(events, 3, "remove event");
+
+ return helpers.setInput(options, "testadd");
+ },
+ check: {
+ typed: "testadd",
+ cursor: 7,
+ current: "__command",
+ status: "ERROR",
+ unassigned: [ ],
+ }
+ }
+ ]);
+};
+
+exports.testAddRemove2 = function (options) {
+ var commands = options.requisition.system.commands;
+
+ commands.add({
+ name: "testadd",
+ exec: function () {
+ return 3;
+ }
+ });
+
+ assert.is(commands.getAll().length,
+ startCount + 1,
+ "rereadd command success");
+ assert.is(events, 4, "rereadd event");
+
+ return helpers.audit(options, [
+ {
+ setup: "testadd",
+ exec: {
+ output: /^3$/
+ },
+ post: function () {
+ commands.remove({
+ name: "testadd"
+ });
+
+ assert.is(commands.getAll().length,
+ startCount,
+ "reremove command success");
+ assert.is(events, 5, "reremove event");
+ }
+ },
+ {
+ setup: "testadd",
+ check: {
+ typed: "testadd",
+ status: "ERROR"
+ }
+ }
+ ]);
+};
+
+exports.testAddRemove3 = function (options) {
+ var commands = options.requisition.system.commands;
+
+ commands.remove({ name: "nonexistant" });
+ assert.is(commands.getAll().length,
+ startCount,
+ "nonexistant1 command success");
+ assert.is(events, 5, "nonexistant1 event");
+
+ commands.remove("nonexistant");
+ assert.is(commands.getAll().length,
+ startCount,
+ "nonexistant2 command success");
+ assert.is(events, 5, "nonexistant2 event");
+
+ commands.onCommandsChange.remove(commandsChange);
+};
+
+exports.testAltCommands = function (options) {
+ var commands = options.requisition.system.commands;
+ var altCommands = new Commands(options.requisition.system.types);
+
+ var tss = {
+ name: "tss",
+ params: [
+ { name: "str", type: "string" },
+ { name: "num", type: "number" },
+ { name: "opt", type: { name: "selection", data: [ "1", "2", "3" ] } },
+ ],
+ customProp1: "localValue",
+ customProp2: true,
+ customProp3: 42,
+ exec: function (args, context) {
+ return context.commandName + ":" +
+ args.str + ":" + args.num + ":" + args.opt;
+ }
+ };
+ altCommands.add(tss);
+
+ var commandSpecs = altCommands.getCommandSpecs();
+ assert.is(JSON.stringify(commandSpecs),
+ '[{"item":"command","name":"tss","params":[' +
+ '{"name":"str","type":"string"},' +
+ '{"name":"num","type":"number"},' +
+ '{"name":"opt","type":{"name":"selection","data":["1","2","3"]}}' +
+ '],"isParent":false}]',
+ "JSON.stringify(commandSpecs)");
+
+ var customProps = [ "customProp1", "customProp2", "customProp3", ];
+ var commandSpecs2 = altCommands.getCommandSpecs(customProps);
+ assert.is(JSON.stringify(commandSpecs2),
+ "[{" +
+ '"item":"command",' +
+ '"name":"tss",' +
+ '"params":[' +
+ '{"name":"str","type":"string"},' +
+ '{"name":"num","type":"number"},' +
+ '{"name":"opt","type":{"name":"selection","data":["1","2","3"]}}' +
+ "]," +
+ '"isParent":false,' +
+ '"customProp1":"localValue",' +
+ '"customProp2":true,' +
+ '"customProp3":42' +
+ "}]",
+ "JSON.stringify(commandSpecs)");
+
+ var remoter = function (args, context) {
+ assert.is(context.commandName, "tss", "commandName is tss");
+
+ var cmd = altCommands.get(context.commandName);
+ return cmd.exec(args, context);
+ };
+
+ commands.addProxyCommands(commandSpecs, remoter, "proxy", "test");
+
+ var parent = commands.get("proxy");
+ assert.is(parent.name, "proxy", "Parent command called proxy");
+
+ var child = commands.get("proxy tss");
+ assert.is(child.name, "proxy tss", "child command called proxy tss");
+
+ return helpers.audit(options, [
+ {
+ setup: "proxy tss foo 6 3",
+ check: {
+ input: "proxy tss foo 6 3",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVVVV",
+ cursor: 17,
+ status: "VALID",
+ args: {
+ str: { value: "foo", status: "VALID" },
+ num: { value: 6, status: "VALID" },
+ opt: { value: "3", status: "VALID" }
+ }
+ },
+ exec: {
+ output: "tss:foo:6:3"
+ },
+ post: function () {
+ commands.remove("proxy");
+ commands.remove("proxy tss");
+
+ assert.is(commands.get("proxy"), undefined, "remove proxy");
+ assert.is(commands.get("proxy tss"), undefined, "remove proxy tss");
+ }
+ }
+ ]);
+};
diff --git a/devtools/client/commandline/test/browser_gcli_cli1.js b/devtools/client/commandline/test/browser_gcli_cli1.js
new file mode 100644
index 000000000..5c7d75ead
--- /dev/null
+++ b/devtools/client/commandline/test/browser_gcli_cli1.js
@@ -0,0 +1,528 @@
+/*
+ * Copyright 2012, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+"use strict";
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
+
+const exports = {};
+
+function test() {
+ helpers.runTestModule(exports, "browser_gcli_cli1.js");
+}
+
+// var assert = require('../testharness/assert');
+// var helpers = require('./helpers');
+
+exports.testBlank = function (options) {
+ return helpers.audit(options, [
+ {
+ setup: "",
+ check: {
+ input: "",
+ hints: "",
+ markup: "",
+ cursor: 0,
+ current: "__command",
+ status: "ERROR"
+ },
+ post: function () {
+ assert.is(options.requisition.commandAssignment.value, undefined);
+ }
+ },
+ {
+ setup: " ",
+ check: {
+ input: " ",
+ hints: "",
+ markup: "V",
+ cursor: 1,
+ current: "__command",
+ status: "ERROR"
+ },
+ post: function () {
+ assert.is(options.requisition.commandAssignment.value, undefined);
+ }
+ },
+ {
+ name: "| ",
+ setup: function () {
+ return helpers.setInput(options, " ", 0);
+ },
+ check: {
+ input: " ",
+ hints: "",
+ markup: "V",
+ cursor: 0,
+ current: "__command",
+ status: "ERROR"
+ },
+ post: function () {
+ assert.is(options.requisition.commandAssignment.value, undefined);
+ }
+ }
+ ]);
+};
+
+exports.testDelete = function (options) {
+ return helpers.audit(options, [
+ {
+ setup: "x<BACKSPACE>",
+ check: {
+ input: "",
+ hints: "",
+ markup: "",
+ cursor: 0,
+ current: "__command",
+ status: "ERROR"
+ },
+ post: function () {
+ assert.is(options.requisition.commandAssignment.value, undefined);
+ }
+ }
+ ]);
+};
+
+exports.testIncompleteMultiMatch = function (options) {
+ return helpers.audit(options, [
+ {
+ setup: "tsn ex",
+ check: {
+ input: "tsn ex",
+ hints: "t",
+ markup: "IIIVII",
+ cursor: 6,
+ current: "__command",
+ status: "ERROR",
+ predictionsContains: [
+ "tsn ext", "tsn exte", "tsn exten", "tsn extend"
+ ]
+ }
+ }
+ ]);
+};
+
+exports.testIncompleteSingleMatch = function (options) {
+ return helpers.audit(options, [
+ {
+ setup: "tselar",
+ check: {
+ input: "tselar",
+ hints: "r",
+ markup: "IIIIII",
+ cursor: 6,
+ current: "__command",
+ status: "ERROR",
+ predictions: [ "tselarr" ],
+ unassigned: [ ]
+ }
+ }
+ ]);
+};
+
+exports.testTsv = function (options) {
+ return helpers.audit(options, [
+ {
+ setup: "tsv",
+ check: {
+ input: "tsv",
+ hints: " <optionType> <optionValue>",
+ markup: "VVV",
+ cursor: 3,
+ current: "__command",
+ status: "ERROR",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "tsv" },
+ optionType: { arg: "", status: "INCOMPLETE" },
+ optionValue: { arg: "", status: "INCOMPLETE" }
+ }
+ }
+ },
+ {
+ setup: "tsv ",
+ check: {
+ input: "tsv ",
+ hints: "option1 <optionValue>",
+ markup: "VVVV",
+ cursor: 4,
+ current: "optionType",
+ status: "ERROR",
+ predictions: [ "option1", "option2", "option3" ],
+ unassigned: [ ],
+ tooltipState: "true:importantFieldFlag",
+ args: {
+ command: { name: "tsv" },
+ optionType: { arg: "", status: "INCOMPLETE" },
+ optionValue: { arg: "", status: "INCOMPLETE" }
+ }
+ }
+ },
+ {
+ name: "ts|v",
+ setup: function () {
+ return helpers.setInput(options, "tsv ", 2);
+ },
+ check: {
+ input: "tsv ",
+ hints: "<optionType> <optionValue>",
+ markup: "VVVV",
+ cursor: 2,
+ current: "__command",
+ status: "ERROR",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "tsv" },
+ optionType: { arg: "", status: "INCOMPLETE" },
+ optionValue: { arg: "", status: "INCOMPLETE" }
+ }
+ }
+ },
+ {
+ setup: "tsv o",
+ check: {
+ input: "tsv o",
+ hints: "ption1 <optionValue>",
+ markup: "VVVVI",
+ cursor: 5,
+ current: "optionType",
+ status: "ERROR",
+ predictions: [ "option1", "option2", "option3" ],
+ unassigned: [ ],
+ tooltipState: "true:importantFieldFlag",
+ args: {
+ command: { name: "tsv" },
+ optionType: {
+ value: undefined,
+ arg: " o",
+ status: "INCOMPLETE",
+ message: "Value required for \u2018optionType\u2019."
+ },
+ optionValue: {
+ value: undefined,
+ arg: "",
+ status: "INCOMPLETE",
+ message: "Value required for \u2018optionValue\u2019."
+ }
+ }
+ }
+ },
+ {
+ setup: "tsv option",
+ check: {
+ input: "tsv option",
+ hints: "1 <optionValue>",
+ markup: "VVVVIIIIII",
+ cursor: 10,
+ current: "optionType",
+ status: "ERROR",
+ predictions: [ "option1", "option2", "option3" ],
+ unassigned: [ ],
+ tooltipState: "true:importantFieldFlag",
+ args: {
+ command: { name: "tsv" },
+ optionType: {
+ value: undefined,
+ arg: " option",
+ status: "INCOMPLETE",
+ message: "Value required for \u2018optionType\u2019."
+ },
+ optionValue: {
+ value: undefined,
+ arg: "",
+ status: "INCOMPLETE",
+ message: "Value required for \u2018optionValue\u2019."
+ }
+ }
+ }
+ },
+ {
+ name: "|tsv option",
+ setup: function () {
+ return helpers.setInput(options, "tsv option", 0);
+ },
+ check: {
+ input: "tsv option",
+ hints: " <optionValue>",
+ markup: "VVVVEEEEEE",
+ cursor: 0,
+ current: "__command",
+ status: "ERROR",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "tsv" },
+ optionType: {
+ value: undefined,
+ arg: " option",
+ status: "INCOMPLETE",
+ message: "Value required for \u2018optionType\u2019."
+ },
+ optionValue: {
+ value: undefined,
+ arg: "",
+ status: "INCOMPLETE",
+ message: "Value required for \u2018optionValue\u2019."
+ }
+ }
+ }
+ },
+ {
+ setup: "tsv option ",
+ check: {
+ input: "tsv option ",
+ hints: "<optionValue>",
+ markup: "VVVVEEEEEEV",
+ cursor: 11,
+ current: "optionValue",
+ status: "ERROR",
+ predictions: [ ],
+ unassigned: [ ],
+ tooltipState: "false:default",
+ args: {
+ command: { name: "tsv" },
+ optionType: {
+ value: undefined,
+ arg: " option ",
+ status: "ERROR",
+ message: "Can\u2019t use \u2018option\u2019."
+ },
+ optionValue: {
+ value: undefined,
+ arg: "",
+ status: "INCOMPLETE",
+ message: "Value required for \u2018optionValue\u2019."
+ }
+ }
+ }
+ },
+ {
+ setup: "tsv option1",
+ check: {
+ input: "tsv option1",
+ hints: " <optionValue>",
+ markup: "VVVVVVVVVVV",
+ cursor: 11,
+ current: "optionType",
+ status: "ERROR",
+ predictions: [ "option1" ],
+ unassigned: [ ],
+ tooltipState: "true:importantFieldFlag",
+ args: {
+ command: { name: "tsv" },
+ optionType: {
+ value: "string",
+ arg: " option1",
+ status: "VALID",
+ message: ""
+ },
+ optionValue: {
+ value: undefined,
+ arg: "",
+ status: "INCOMPLETE",
+ message: "Value required for \u2018optionValue\u2019."
+ }
+ }
+ }
+ },
+ {
+ setup: "tsv option1 ",
+ check: {
+ input: "tsv option1 ",
+ hints: "<optionValue>",
+ markup: "VVVVVVVVVVVV",
+ cursor: 12,
+ current: "optionValue",
+ status: "ERROR",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "tsv" },
+ optionType: {
+ value: "string",
+ arg: " option1 ",
+ status: "VALID",
+ message: ""
+ },
+ optionValue: {
+ value: undefined,
+ arg: "",
+ status: "INCOMPLETE",
+ message: "Value required for \u2018optionValue\u2019."
+ }
+ }
+ }
+ },
+ {
+ setup: "tsv option2",
+ check: {
+ input: "tsv option2",
+ hints: " <optionValue>",
+ markup: "VVVVVVVVVVV",
+ cursor: 11,
+ current: "optionType",
+ status: "ERROR",
+ predictions: [ "option2" ],
+ unassigned: [ ],
+ tooltipState: "true:importantFieldFlag",
+ args: {
+ command: { name: "tsv" },
+ optionType: {
+ value: "number",
+ arg: " option2",
+ status: "VALID",
+ message: ""
+ },
+ optionValue: {
+ value: undefined,
+ arg: "",
+ status: "INCOMPLETE",
+ message: "Value required for \u2018optionValue\u2019."
+ }
+ }
+ }
+ }
+ ]);
+};
+
+exports.testTsvValues = function (options) {
+ return helpers.audit(options, [
+ {
+ setup: "tsv option1 6",
+ check: {
+ input: "tsv option1 6",
+ hints: "",
+ markup: "VVVVVVVVVVVVV",
+ cursor: 13,
+ current: "optionValue",
+ status: "VALID",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "tsv" },
+ optionType: {
+ value: "string",
+ arg: " option1",
+ status: "VALID",
+ message: ""
+ },
+ optionValue: {
+ arg: " 6",
+ status: "VALID",
+ message: ""
+ }
+ }
+ }
+ },
+ {
+ setup: "tsv option2 6",
+ check: {
+ input: "tsv option2 6",
+ hints: "",
+ markup: "VVVVVVVVVVVVV",
+ cursor: 13,
+ current: "optionValue",
+ status: "VALID",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "tsv" },
+ optionType: {
+ value: "number",
+ arg: " option2",
+ status: "VALID",
+ message: ""
+ },
+ optionValue: {
+ arg: " 6",
+ status: "VALID",
+ message: ""
+ }
+ }
+ }
+ },
+ // Delegated remote types can't transfer value types so we only test for
+ // the value of 'value' when we're local
+ {
+ skipIf: options.isRemote,
+ setup: "tsv option1 6",
+ check: {
+ args: {
+ optionValue: { value: "6" }
+ }
+ }
+ },
+ {
+ skipIf: options.isRemote,
+ setup: "tsv option2 6",
+ check: {
+ args: {
+ optionValue: { value: 6 }
+ }
+ }
+ }
+ ]);
+};
+
+exports.testInvalid = function (options) {
+ return helpers.audit(options, [
+ {
+ setup: "zxjq",
+ check: {
+ input: "zxjq",
+ hints: "",
+ markup: "EEEE",
+ cursor: 4,
+ current: "__command",
+ status: "ERROR",
+ predictions: [ ],
+ unassigned: [ ],
+ tooltipState: "true:isError"
+ }
+ },
+ {
+ setup: "zxjq ",
+ check: {
+ input: "zxjq ",
+ hints: "",
+ markup: "EEEEV",
+ cursor: 5,
+ current: "__command",
+ status: "ERROR",
+ predictions: [ ],
+ unassigned: [ ],
+ tooltipState: "true:isError"
+ }
+ },
+ {
+ setup: "zxjq one",
+ check: {
+ input: "zxjq one",
+ hints: "",
+ markup: "EEEEVEEE",
+ cursor: 8,
+ current: "__unassigned",
+ status: "ERROR",
+ predictions: [ ],
+ unassigned: [ " one" ],
+ tooltipState: "true:isError"
+ }
+ }
+ ]);
+};
diff --git a/devtools/client/commandline/test/browser_gcli_cli2.js b/devtools/client/commandline/test/browser_gcli_cli2.js
new file mode 100644
index 000000000..7ae5174d3
--- /dev/null
+++ b/devtools/client/commandline/test/browser_gcli_cli2.js
@@ -0,0 +1,788 @@
+/*
+ * Copyright 2012, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+"use strict";
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
+
+const exports = {};
+
+function test() {
+ helpers.runTestModule(exports, "browser_gcli_cli2.js");
+}
+
+// var helpers = require('./helpers');
+
+exports.testSingleString = function (options) {
+ return helpers.audit(options, [
+ {
+ setup: "tsr",
+ check: {
+ input: "tsr",
+ hints: " <text>",
+ markup: "VVV",
+ cursor: 3,
+ current: "__command",
+ status: "ERROR",
+ unassigned: [ ],
+ args: {
+ command: { name: "tsr" },
+ text: {
+ value: undefined,
+ arg: "",
+ status: "INCOMPLETE",
+ message: "Value required for \u2018text\u2019."
+ }
+ }
+ }
+ },
+ {
+ setup: "tsr ",
+ check: {
+ input: "tsr ",
+ hints: "<text>",
+ markup: "VVVV",
+ cursor: 4,
+ current: "text",
+ status: "ERROR",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "tsr" },
+ text: {
+ value: undefined,
+ arg: "",
+ status: "INCOMPLETE",
+ message: "Value required for \u2018text\u2019."
+ }
+ }
+ }
+ },
+ {
+ setup: "tsr h",
+ check: {
+ input: "tsr h",
+ hints: "",
+ markup: "VVVVV",
+ cursor: 5,
+ current: "text",
+ status: "VALID",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "tsr" },
+ text: {
+ value: "h",
+ arg: " h",
+ status: "VALID",
+ message: ""
+ }
+ }
+ }
+ },
+ {
+ setup: 'tsr "h h"',
+ check: {
+ input: 'tsr "h h"',
+ hints: "",
+ markup: "VVVVVVVVV",
+ cursor: 9,
+ current: "text",
+ status: "VALID",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "tsr" },
+ text: {
+ value: "h h",
+ arg: ' "h h"',
+ status: "VALID",
+ message: ""
+ }
+ }
+ }
+ },
+ {
+ setup: "tsr h h h",
+ check: {
+ input: "tsr h h h",
+ hints: "",
+ markup: "VVVVVVVVV",
+ cursor: 9,
+ current: "text",
+ status: "VALID",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "tsr" },
+ text: {
+ value: "h h h",
+ arg: " h h h",
+ status: "VALID",
+ message: ""
+ }
+ }
+ }
+ }
+ ]);
+};
+
+exports.testSingleNumber = function (options) {
+ return helpers.audit(options, [
+ {
+ setup: "tsu",
+ check: {
+ input: "tsu",
+ hints: " <num>",
+ markup: "VVV",
+ cursor: 3,
+ current: "__command",
+ status: "ERROR",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "tsu" },
+ num: {
+ value: undefined,
+ arg: "",
+ status: "INCOMPLETE",
+ message: "Value required for \u2018num\u2019."
+ }
+ }
+ }
+ },
+ {
+ setup: "tsu ",
+ check: {
+ input: "tsu ",
+ hints: "<num>",
+ markup: "VVVV",
+ cursor: 4,
+ current: "num",
+ status: "ERROR",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "tsu" },
+ num: {
+ value: undefined,
+ arg: "",
+ status: "INCOMPLETE",
+ message: "Value required for \u2018num\u2019."
+ }
+ }
+ }
+ },
+ {
+ setup: "tsu 1",
+ check: {
+ input: "tsu 1",
+ hints: "",
+ markup: "VVVVV",
+ cursor: 5,
+ current: "num",
+ status: "VALID",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "tsu" },
+ num: { value: 1, arg: " 1", status: "VALID", message: "" }
+ }
+ }
+ },
+ {
+ setup: "tsu x",
+ check: {
+ input: "tsu x",
+ hints: "",
+ markup: "VVVVE",
+ cursor: 5,
+ current: "num",
+ status: "ERROR",
+ predictions: [ ],
+ unassigned: [ ],
+ tooltipState: "true:isError",
+ args: {
+ command: { name: "tsu" },
+ num: {
+ value: undefined,
+ arg: " x",
+ status: "ERROR",
+ message: "Can\u2019t convert \u201cx\u201d to a number."
+ }
+ }
+ }
+ },
+ {
+ setup: "tsu 1.5",
+ check: {
+ input: "tsu 1.5",
+ hints: "",
+ markup: "VVVVEEE",
+ cursor: 7,
+ current: "num",
+ status: "ERROR",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "tsu" },
+ num: {
+ value: undefined,
+ arg: " 1.5",
+ status: "ERROR",
+ message: "Can\u2019t convert \u201c1.5\u201d to an integer."
+ }
+ }
+ }
+ }
+ ]);
+};
+
+exports.testSingleFloat = function (options) {
+ return helpers.audit(options, [
+ {
+ setup: "tsf",
+ check: {
+ input: "tsf",
+ hints: " <num>",
+ markup: "VVV",
+ cursor: 3,
+ current: "__command",
+ status: "ERROR",
+ error: "",
+ unassigned: [ ],
+ args: {
+ command: { name: "tsf" },
+ num: {
+ value: undefined,
+ arg: "",
+ status: "INCOMPLETE",
+ message: "Value required for \u2018num\u2019."
+ }
+ }
+ }
+ },
+ {
+ setup: "tsf 1",
+ check: {
+ input: "tsf 1",
+ hints: "",
+ markup: "VVVVV",
+ cursor: 5,
+ current: "num",
+ status: "VALID",
+ error: "",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "tsf" },
+ num: { value: 1, arg: " 1", status: "VALID", message: "" }
+ }
+ }
+ },
+ {
+ setup: "tsf 1.",
+ check: {
+ input: "tsf 1.",
+ hints: "",
+ markup: "VVVVVV",
+ cursor: 6,
+ current: "num",
+ status: "VALID",
+ error: "",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "tsf" },
+ num: { value: 1, arg: " 1.", status: "VALID", message: "" }
+ }
+ }
+ },
+ {
+ setup: "tsf 1.5",
+ check: {
+ input: "tsf 1.5",
+ hints: "",
+ markup: "VVVVVVV",
+ cursor: 7,
+ current: "num",
+ status: "VALID",
+ error: "",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "tsf" },
+ num: { value: 1.5, arg: " 1.5", status: "VALID", message: "" }
+ }
+ }
+ },
+ {
+ setup: "tsf 1.5x",
+ check: {
+ input: "tsf 1.5x",
+ hints: "",
+ markup: "VVVVVVVV",
+ cursor: 8,
+ current: "num",
+ status: "VALID",
+ error: "",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "tsf" },
+ num: { value: 1.5, arg: " 1.5x", status: "VALID", message: "" }
+ }
+ }
+ },
+ {
+ name: "tsf x (cursor=4)",
+ setup: function () {
+ return helpers.setInput(options, "tsf x", 4);
+ },
+ check: {
+ input: "tsf x",
+ hints: "",
+ markup: "VVVVE",
+ cursor: 4,
+ current: "num",
+ status: "ERROR",
+ error: "Can\u2019t convert \u201cx\u201d to a number.",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "tsf" },
+ num: {
+ value: undefined,
+ arg: " x",
+ status: "ERROR",
+ message: "Can\u2019t convert \u201cx\u201d to a number."
+ }
+ }
+ }
+ }
+ ]);
+};
+
+exports.testElementWeb = function (options) {
+ return helpers.audit(options, [
+ {
+ setup: "tse #gcli-root",
+ check: {
+ input: "tse #gcli-root",
+ hints: " [options]",
+ markup: "VVVVVVVVVVVVVV",
+ cursor: 14,
+ current: "node",
+ status: "VALID",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "tse" },
+ node: {
+ arg: " #gcli-root",
+ status: "VALID",
+ message: ""
+ },
+ nodes: { arg: "", status: "VALID", message: "" },
+ nodes2: { arg: "", status: "VALID", message: "" },
+ }
+ }
+ }
+ ]);
+};
+
+exports.testElement = function (options) {
+ return helpers.audit(options, [
+ {
+ setup: "tse",
+ check: {
+ input: "tse",
+ hints: " <node> [options]",
+ markup: "VVV",
+ cursor: 3,
+ current: "__command",
+ status: "ERROR",
+ predictions: [ "tse", "tselarr" ],
+ unassigned: [ ],
+ args: {
+ command: { name: "tse" },
+ node: { arg: "", status: "INCOMPLETE" },
+ nodes: { arg: "", status: "VALID", message: "" },
+ nodes2: { arg: "", status: "VALID", message: "" },
+ }
+ }
+ },
+ {
+ setup: "tse #gcli-nomatch",
+ check: {
+ input: "tse #gcli-nomatch",
+ hints: " [options]",
+ markup: "VVVVIIIIIIIIIIIII",
+ cursor: 17,
+ current: "node",
+ status: "ERROR",
+ predictions: [ ],
+ unassigned: [ ],
+ outputState: "false:default",
+ tooltipState: "true:isError",
+ args: {
+ command: { name: "tse" },
+ node: {
+ value: undefined,
+ arg: " #gcli-nomatch",
+ // This is somewhat debatable because this input can't be corrected
+ // simply by typing so it's and error rather than incomplete,
+ // however without digging into the CSS engine we can't tell that
+ // so we default to incomplete
+ status: "INCOMPLETE",
+ message: "No matches"
+ },
+ nodes: { arg: "", status: "VALID", message: "" },
+ nodes2: { arg: "", status: "VALID", message: "" },
+ }
+ }
+ },
+ {
+ setup: "tse #",
+ check: {
+ input: "tse #",
+ hints: " [options]",
+ markup: "VVVVE",
+ cursor: 5,
+ current: "node",
+ status: "ERROR",
+ predictions: [ ],
+ unassigned: [ ],
+ tooltipState: "true:isError",
+ args: {
+ command: { name: "tse" },
+ node: {
+ value: undefined,
+ arg: " #",
+ status: "ERROR",
+ message: "Syntax error in CSS query"
+ },
+ nodes: { arg: "", status: "VALID", message: "" },
+ nodes2: { arg: "", status: "VALID", message: "" },
+ }
+ }
+ },
+ {
+ setup: "tse .",
+ check: {
+ input: "tse .",
+ hints: " [options]",
+ markup: "VVVVE",
+ cursor: 5,
+ current: "node",
+ status: "ERROR",
+ predictions: [ ],
+ unassigned: [ ],
+ tooltipState: "true:isError",
+ args: {
+ command: { name: "tse" },
+ node: {
+ value: undefined,
+ arg: " .",
+ status: "ERROR",
+ message: "Syntax error in CSS query"
+ },
+ nodes: { arg: "", status: "VALID", message: "" },
+ nodes2: { arg: "", status: "VALID", message: "" },
+ }
+ }
+ },
+ {
+ setup: "tse *",
+ check: {
+ input: "tse *",
+ hints: " [options]",
+ markup: "VVVVE",
+ cursor: 5,
+ current: "node",
+ status: "ERROR",
+ predictions: [ ],
+ unassigned: [ ],
+ tooltipState: "true:isError",
+ args: {
+ command: { name: "tse" },
+ node: {
+ value: undefined,
+ arg: " *",
+ status: "ERROR",
+ message: /^Too many matches \([0-9]*\)/
+ },
+ nodes: { arg: "", status: "VALID", message: "" },
+ nodes2: { arg: "", status: "VALID", message: "" },
+ }
+ }
+ }
+ ]);
+};
+
+exports.testNestedCommand = function (options) {
+ return helpers.audit(options, [
+ {
+ setup: "tsn",
+ check: {
+ input: "tsn",
+ hints: " deep down nested cmd",
+ markup: "III",
+ cursor: 3,
+ current: "__command",
+ status: "ERROR",
+ predictionsInclude: [
+ "tsn deep", "tsn deep down", "tsn deep down nested",
+ "tsn deep down nested cmd", "tsn dif"
+ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "tsn" }
+ }
+ }
+ },
+ {
+ setup: "tsn ",
+ check: {
+ input: "tsn ",
+ hints: " deep down nested cmd",
+ markup: "IIIV",
+ cursor: 4,
+ current: "__command",
+ status: "ERROR",
+ unassigned: [ ]
+ }
+ },
+ {
+ skipIf: options.isPhantomjs, // PhantomJS gets predictions wrong
+ setup: "tsn x",
+ check: {
+ input: "tsn x",
+ hints: " -> tsn ext",
+ markup: "IIIVI",
+ cursor: 5,
+ current: "__command",
+ status: "ERROR",
+ predictions: [ "tsn ext" ],
+ unassigned: [ ]
+ }
+ },
+ {
+ setup: "tsn dif",
+ check: {
+ input: "tsn dif",
+ hints: " <text>",
+ markup: "VVVVVVV",
+ cursor: 7,
+ current: "__command",
+ status: "ERROR",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "tsn dif" },
+ text: {
+ value: undefined,
+ arg: "",
+ status: "INCOMPLETE",
+ message: "Value required for \u2018text\u2019."
+ }
+ }
+ }
+ },
+ {
+ setup: "tsn dif ",
+ check: {
+ input: "tsn dif ",
+ hints: "<text>",
+ markup: "VVVVVVVV",
+ cursor: 8,
+ current: "text",
+ status: "ERROR",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "tsn dif" },
+ text: {
+ value: undefined,
+ arg: "",
+ status: "INCOMPLETE",
+ message: "Value required for \u2018text\u2019."
+ }
+ }
+ }
+ },
+ {
+ setup: "tsn dif x",
+ check: {
+ input: "tsn dif x",
+ hints: "",
+ markup: "VVVVVVVVV",
+ cursor: 9,
+ current: "text",
+ status: "VALID",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "tsn dif" },
+ text: { value: "x", arg: " x", status: "VALID", message: "" }
+ }
+ }
+ },
+ {
+ setup: "tsn ext",
+ check: {
+ input: "tsn ext",
+ hints: " <text>",
+ markup: "VVVVVVV",
+ cursor: 7,
+ current: "__command",
+ status: "ERROR",
+ predictions: [ "tsn ext", "tsn exte", "tsn exten", "tsn extend" ],
+ unassigned: [ ],
+ args: {
+ command: { name: "tsn ext" },
+ text: {
+ value: undefined,
+ arg: "",
+ status: "INCOMPLETE",
+ message: "Value required for \u2018text\u2019."
+ }
+ }
+ }
+ },
+ {
+ setup: "tsn exte",
+ check: {
+ input: "tsn exte",
+ hints: " <text>",
+ markup: "VVVVVVVV",
+ cursor: 8,
+ current: "__command",
+ status: "ERROR",
+ predictions: [ "tsn exte", "tsn exten", "tsn extend" ],
+ unassigned: [ ],
+ args: {
+ command: { name: "tsn exte" },
+ text: {
+ value: undefined,
+ arg: "",
+ status: "INCOMPLETE",
+ message: "Value required for \u2018text\u2019."
+ }
+ }
+ }
+ },
+ {
+ setup: "tsn exten",
+ check: {
+ input: "tsn exten",
+ hints: " <text>",
+ markup: "VVVVVVVVV",
+ cursor: 9,
+ current: "__command",
+ status: "ERROR",
+ predictions: [ "tsn exten", "tsn extend" ],
+ unassigned: [ ],
+ args: {
+ command: { name: "tsn exten" },
+ text: {
+ value: undefined,
+ arg: "",
+ status: "INCOMPLETE",
+ message: "Value required for \u2018text\u2019."
+ }
+ }
+ }
+ },
+ {
+ setup: "tsn extend",
+ check: {
+ input: "tsn extend",
+ hints: " <text>",
+ markup: "VVVVVVVVVV",
+ cursor: 10,
+ current: "__command",
+ status: "ERROR",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "tsn extend" },
+ text: {
+ value: undefined,
+ arg: "",
+ status: "INCOMPLETE",
+ message: "Value required for \u2018text\u2019."
+ }
+ }
+ }
+ },
+ {
+ setup: "ts ",
+ check: {
+ input: "ts ",
+ hints: "",
+ markup: "EEV",
+ cursor: 3,
+ current: "__command",
+ status: "ERROR",
+ predictions: [ ],
+ unassigned: [ ],
+ tooltipState: "true:isError"
+ }
+ },
+ ]);
+};
+
+// From Bug 664203
+exports.testDeeplyNested = function (options) {
+ return helpers.audit(options, [
+ {
+ setup: "tsn deep down nested",
+ check: {
+ input: "tsn deep down nested",
+ hints: " cmd",
+ markup: "IIIVIIIIVIIIIVIIIIII",
+ cursor: 20,
+ current: "__command",
+ status: "ERROR",
+ predictions: [ "tsn deep down nested cmd" ],
+ unassigned: [ ],
+ outputState: "false:default",
+ tooltipState: "false:default",
+ args: {
+ command: { name: "tsn deep down nested" },
+ }
+ }
+ },
+ {
+ setup: "tsn deep down nested cmd",
+ check: {
+ input: "tsn deep down nested cmd",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVVVVVVVVVVV",
+ cursor: 24,
+ current: "__command",
+ status: "VALID",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "tsn deep down nested cmd" },
+ }
+ }
+ }
+ ]);
+};
diff --git a/devtools/client/commandline/test/browser_gcli_completion1.js b/devtools/client/commandline/test/browser_gcli_completion1.js
new file mode 100644
index 000000000..274f831bc
--- /dev/null
+++ b/devtools/client/commandline/test/browser_gcli_completion1.js
@@ -0,0 +1,277 @@
+/*
+ * Copyright 2012, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+"use strict";
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
+
+const exports = {};
+
+function test() {
+ helpers.runTestModule(exports, "browser_gcli_completion1.js");
+}
+
+// var helpers = require('./helpers');
+
+exports.testActivate = function (options) {
+ return helpers.audit(options, [
+ {
+ setup: "",
+ check: {
+ hints: ""
+ }
+ },
+ {
+ setup: " ",
+ check: {
+ hints: ""
+ }
+ },
+ {
+ setup: "tsr",
+ check: {
+ hints: " <text>"
+ }
+ },
+ {
+ setup: "tsr ",
+ check: {
+ hints: "<text>"
+ }
+ },
+ {
+ setup: "tsr b",
+ check: {
+ hints: ""
+ }
+ },
+ {
+ setup: "tsb",
+ check: {
+ hints: " [toggle]"
+ }
+ },
+ {
+ setup: "tsm",
+ check: {
+ hints: " <abc> <txt> <num>"
+ }
+ },
+ {
+ setup: "tsm ",
+ check: {
+ hints: "a <txt> <num>"
+ }
+ },
+ {
+ setup: "tsm a",
+ check: {
+ hints: " <txt> <num>"
+ }
+ },
+ {
+ setup: "tsm a ",
+ check: {
+ hints: "<txt> <num>"
+ }
+ },
+ {
+ setup: "tsm a ",
+ check: {
+ hints: "<txt> <num>"
+ }
+ },
+ {
+ setup: "tsm a d",
+ check: {
+ hints: " <num>"
+ }
+ },
+ {
+ setup: 'tsm a "d d"',
+ check: {
+ hints: " <num>"
+ }
+ },
+ {
+ setup: 'tsm a "d ',
+ check: {
+ hints: " <num>"
+ }
+ },
+ {
+ setup: 'tsm a "d d" ',
+ check: {
+ hints: "<num>"
+ }
+ },
+ {
+ setup: 'tsm a "d d ',
+ check: {
+ hints: " <num>"
+ }
+ },
+ {
+ setup: "tsm d r",
+ check: {
+ hints: " <num>"
+ }
+ },
+ {
+ setup: "tsm a d ",
+ check: {
+ hints: "<num>"
+ }
+ },
+ {
+ setup: "tsm a d 4",
+ check: {
+ hints: ""
+ }
+ },
+ {
+ setup: "tsg",
+ check: {
+ hints: " <solo> [options]"
+ }
+ },
+ {
+ setup: "tsg ",
+ check: {
+ hints: "aaa [options]"
+ }
+ },
+ {
+ setup: "tsg a",
+ check: {
+ hints: "aa [options]"
+ }
+ },
+ {
+ setup: "tsg b",
+ check: {
+ hints: "bb [options]"
+ }
+ },
+ {
+ skipIf: options.isPhantomjs, // PhantomJS gets predictions wrong
+ setup: "tsg d",
+ check: {
+ hints: " [options] -> ccc"
+ }
+ },
+ {
+ setup: "tsg aa",
+ check: {
+ hints: "a [options]"
+ }
+ },
+ {
+ setup: "tsg aaa",
+ check: {
+ hints: " [options]"
+ }
+ },
+ {
+ setup: "tsg aaa ",
+ check: {
+ hints: "[options]"
+ }
+ },
+ {
+ setup: "tsg aaa d",
+ check: {
+ hints: " [options]"
+ }
+ },
+ {
+ setup: "tsg aaa dddddd",
+ check: {
+ hints: " [options]"
+ }
+ },
+ {
+ setup: "tsg aaa dddddd ",
+ check: {
+ hints: "[options]"
+ }
+ },
+ {
+ setup: 'tsg aaa "d',
+ check: {
+ hints: " [options]"
+ }
+ },
+ {
+ setup: 'tsg aaa "d d',
+ check: {
+ hints: " [options]"
+ }
+ },
+ {
+ setup: 'tsg aaa "d d"',
+ check: {
+ hints: " [options]"
+ }
+ },
+ {
+ setup: "tsn ex ",
+ check: {
+ hints: ""
+ }
+ },
+ {
+ setup: "selarr",
+ check: {
+ hints: " -> tselarr"
+ }
+ },
+ {
+ setup: "tselar 1",
+ check: {
+ hints: ""
+ }
+ },
+ {
+ name: "tselar |1",
+ setup: function () {
+ return helpers.setInput(options, "tselar 1", 7);
+ },
+ check: {
+ hints: ""
+ }
+ },
+ {
+ name: "tselar| 1",
+ setup: function () {
+ return helpers.setInput(options, "tselar 1", 6);
+ },
+ check: {
+ hints: " -> tselarr"
+ }
+ },
+ {
+ name: "tsela|r 1",
+ setup: function () {
+ return helpers.setInput(options, "tselar 1", 5);
+ },
+ check: {
+ hints: " -> tselarr"
+ }
+ },
+ ]);
+};
diff --git a/devtools/client/commandline/test/browser_gcli_completion2.js b/devtools/client/commandline/test/browser_gcli_completion2.js
new file mode 100644
index 000000000..309070b1f
--- /dev/null
+++ b/devtools/client/commandline/test/browser_gcli_completion2.js
@@ -0,0 +1,263 @@
+/*
+ * Copyright 2012, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+"use strict";
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
+
+const exports = {};
+
+function test() {
+ helpers.runTestModule(exports, "browser_gcli_completion2.js");
+}
+
+// var helpers = require('./helpers');
+
+exports.testLong = function (options) {
+ return helpers.audit(options, [
+ {
+ setup: "tslong --sel",
+ check: {
+ input: "tslong --sel",
+ hints: " <selection> <msg> [options]",
+ markup: "VVVVVVVIIIII"
+ }
+ },
+ {
+ setup: "tslong --sel<TAB>",
+ check: {
+ input: "tslong --sel ",
+ hints: "space <msg> [options]",
+ markup: "VVVVVVVIIIIIV"
+ }
+ },
+ {
+ setup: "tslong --sel ",
+ check: {
+ input: "tslong --sel ",
+ hints: "space <msg> [options]",
+ markup: "VVVVVVVIIIIIV"
+ }
+ },
+ {
+ setup: "tslong --sel s",
+ check: {
+ input: "tslong --sel s",
+ hints: "pace <msg> [options]",
+ markup: "VVVVVVVIIIIIVI"
+ }
+ },
+ {
+ setup: "tslong --num ",
+ check: {
+ input: "tslong --num ",
+ hints: "<number> <msg> [options]",
+ markup: "VVVVVVVIIIIIV"
+ }
+ },
+ {
+ setup: "tslong --num 42",
+ check: {
+ input: "tslong --num 42",
+ hints: " <msg> [options]",
+ markup: "VVVVVVVVVVVVVVV"
+ }
+ },
+ {
+ setup: "tslong --num 42 ",
+ check: {
+ input: "tslong --num 42 ",
+ hints: "<msg> [options]",
+ markup: "VVVVVVVVVVVVVVVV"
+ }
+ },
+ {
+ setup: "tslong --num 42 --se",
+ check: {
+ input: "tslong --num 42 --se",
+ hints: "l <msg> [options]",
+ markup: "VVVVVVVVVVVVVVVVIIII"
+ }
+ },
+ {
+ setup: "tslong --num 42 --se<TAB>",
+ check: {
+ input: "tslong --num 42 --sel ",
+ hints: "space <msg> [options]",
+ markup: "VVVVVVVVVVVVVVVVIIIIIV"
+ }
+ },
+ {
+ setup: "tslong --num 42 --se<TAB><TAB>",
+ check: {
+ input: "tslong --num 42 --sel space ",
+ hints: "<msg> [options]",
+ markup: "VVVVVVVVVVVVVVVVVVVVVVVVVVVV"
+ }
+ },
+ {
+ setup: "tslong --num 42 --sel ",
+ check: {
+ input: "tslong --num 42 --sel ",
+ hints: "space <msg> [options]",
+ markup: "VVVVVVVVVVVVVVVVIIIIIV"
+ }
+ },
+ {
+ setup: "tslong --num 42 --sel space ",
+ check: {
+ input: "tslong --num 42 --sel space ",
+ hints: "<msg> [options]",
+ markup: "VVVVVVVVVVVVVVVVVVVVVVVVVVVV"
+ }
+ }
+ ]);
+};
+
+exports.testNoTab = function (options) {
+ return helpers.audit(options, [
+ {
+ setup: "tss<TAB>",
+ check: {
+ input: "tss ",
+ markup: "VVVV",
+ hints: ""
+ }
+ },
+ {
+ setup: "tss<TAB><TAB>",
+ check: {
+ input: "tss ",
+ markup: "VVVV",
+ hints: ""
+ }
+ },
+ {
+ setup: "xxxx",
+ check: {
+ input: "xxxx",
+ markup: "EEEE",
+ hints: ""
+ }
+ },
+ {
+ name: "<TAB>",
+ setup: function () {
+ // Doing it this way avoids clearing the input buffer
+ return helpers.pressTab(options);
+ },
+ check: {
+ input: "xxxx",
+ markup: "EEEE",
+ hints: ""
+ }
+ }
+ ]);
+};
+
+exports.testOutstanding = function (options) {
+ // See bug 779800
+ /*
+ return helpers.audit(options, [
+ {
+ setup: 'tsg --txt1 ddd ',
+ check: {
+ input: 'tsg --txt1 ddd ',
+ hints: 'aaa [options]',
+ markup: 'VVVVVVVVVVVVVVV'
+ }
+ },
+ ]);
+ */
+};
+
+exports.testCompleteIntoOptional = function (options) {
+ // From bug 779816
+ return helpers.audit(options, [
+ {
+ setup: "tso ",
+ check: {
+ typed: "tso ",
+ hints: "[text]",
+ markup: "VVVV",
+ status: "VALID"
+ }
+ },
+ {
+ setup: "tso<TAB>",
+ check: {
+ typed: "tso ",
+ hints: "[text]",
+ markup: "VVVV",
+ status: "VALID"
+ }
+ }
+ ]);
+};
+
+exports.testSpaceComplete = function (options) {
+ return helpers.audit(options, [
+ {
+ setup: "tslong --sel2 wit",
+ check: {
+ input: "tslong --sel2 wit",
+ hints: "h space <msg> [options]",
+ markup: "VVVVVVVIIIIIIVIII",
+ cursor: 17,
+ current: "sel2",
+ status: "ERROR",
+ tooltipState: "true:importantFieldFlag",
+ args: {
+ command: { name: "tslong" },
+ msg: { status: "INCOMPLETE" },
+ num: { status: "VALID" },
+ sel: { status: "VALID" },
+ bool: { value: false, status: "VALID" },
+ num2: { status: "VALID" },
+ bool2: { value: false, status: "VALID" },
+ sel2: { arg: " --sel2 wit", status: "INCOMPLETE" }
+ }
+ }
+ },
+ {
+ setup: "tslong --sel2 wit<TAB>",
+ check: {
+ input: "tslong --sel2 'with space' ",
+ hints: "<msg> [options]",
+ markup: "VVVVVVVVVVVVVVVVVVVVVVVVVVV",
+ cursor: 27,
+ current: "sel2",
+ status: "ERROR",
+ tooltipState: "true:importantFieldFlag",
+ args: {
+ command: { name: "tslong" },
+ msg: { status: "INCOMPLETE" },
+ num: { status: "VALID" },
+ sel: { status: "VALID" },
+ bool: { value: false, status: "VALID" },
+ num2: { status: "VALID" },
+ bool2: { value: false, status: "VALID" },
+ sel2: {
+ value: "with space",
+ arg: " --sel2 'with space' ",
+ status: "VALID"
+ }
+ }
+ }
+ }
+ ]);
+};
diff --git a/devtools/client/commandline/test/browser_gcli_context.js b/devtools/client/commandline/test/browser_gcli_context.js
new file mode 100644
index 000000000..499c074aa
--- /dev/null
+++ b/devtools/client/commandline/test/browser_gcli_context.js
@@ -0,0 +1,239 @@
+/*
+ * Copyright 2012, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+"use strict";
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
+
+const exports = {};
+
+function test() {
+ helpers.runTestModule(exports, "browser_gcli_context.js");
+}
+
+// var helpers = require('./helpers');
+
+exports.testBaseline = function (options) {
+ return helpers.audit(options, [
+ // These 3 establish a baseline for comparison when we have used the
+ // context command
+ {
+ setup: "ext",
+ check: {
+ input: "ext",
+ hints: " -> context",
+ markup: "III",
+ message: "",
+ predictions: [ "context", "tsn ext", "tsn exte", "tsn exten", "tsn extend" ],
+ unassigned: [ ],
+ }
+ },
+ {
+ setup: "ext test",
+ check: {
+ input: "ext test",
+ hints: "",
+ markup: "IIIVEEEE",
+ status: "ERROR",
+ message: "Too many arguments",
+ unassigned: [ " test" ],
+ }
+ },
+ {
+ setup: "tsn",
+ check: {
+ input: "tsn",
+ hints: " deep down nested cmd",
+ markup: "III",
+ cursor: 3,
+ current: "__command",
+ status: "ERROR",
+ predictionsContains: [ "tsn deep down nested cmd", "tsn ext", "tsn exte" ],
+ args: {
+ command: { name: "tsn" },
+ }
+ }
+ }
+ ]);
+};
+
+exports.testContext = function (options) {
+ return helpers.audit(options, [
+ // Use the 'tsn' context
+ {
+ setup: "context tsn",
+ check: {
+ input: "context tsn",
+ hints: " deep down nested cmd",
+ markup: "VVVVVVVVVVV",
+ message: "",
+ predictionsContains: [ "tsn deep down nested cmd", "tsn ext", "tsn exte" ],
+ args: {
+ command: { name: "context" },
+ prefix: {
+ value: options.requisition.system.commands.get("tsn"),
+ status: "VALID",
+ message: ""
+ }
+ }
+ },
+ exec: {
+ output: "Using tsn as a command prefix"
+ }
+ },
+ // For comparison with earlier
+ {
+ setup: "ext",
+ check: {
+ input: "ext",
+ hints: " <text>",
+ markup: "VVV",
+ predictions: [ "tsn ext", "tsn exte", "tsn exten", "tsn extend" ],
+ args: {
+ command: { name: "tsn ext" },
+ text: {
+ value: undefined,
+ arg: "",
+ status: "INCOMPLETE"
+ }
+ }
+ }
+ },
+ {
+ setup: "ext test",
+ check: {
+ input: "ext test",
+ hints: "",
+ markup: "VVVVVVVV",
+ args: {
+ command: { name: "tsn ext" },
+ text: {
+ value: "test",
+ arg: " test",
+ status: "VALID",
+ message: ""
+ }
+ }
+ },
+ exec: {
+ output: "Exec: tsnExt text=test"
+ }
+ },
+ {
+ setup: "tsn",
+ check: {
+ input: "tsn",
+ hints: " deep down nested cmd",
+ markup: "III",
+ message: "",
+ predictionsContains: [ "tsn deep down nested cmd", "tsn ext", "tsn exte" ],
+ args: {
+ command: { name: "tsn" },
+ }
+ }
+ },
+ // Does it actually work?
+ {
+ setup: "tsb true",
+ check: {
+ input: "tsb true",
+ hints: "",
+ markup: "VVVVVVVV",
+ options: [ "true" ],
+ message: "",
+ predictions: [ "true" ],
+ unassigned: [ ],
+ args: {
+ command: { name: "tsb" },
+ toggle: { value: true, arg: " true", status: "VALID", message: "" }
+ }
+ }
+ },
+ {
+ // Bug 866710 - GCLI should allow argument merging for non-string parameters
+ setup: "context tsn ext",
+ skip: true
+ },
+ {
+ setup: 'context "tsn ext"',
+ check: {
+ input: 'context "tsn ext"',
+ hints: "",
+ markup: "VVVVVVVVVVVVVVVVV",
+ message: "",
+ predictions: [ "tsn ext", "tsn exte", "tsn exten", "tsn extend" ],
+ unassigned: [ ],
+ args: {
+ command: { name: "context" },
+ prefix: {
+ value: options.requisition.system.commands.get("tsn ext"),
+ status: "VALID",
+ message: ""
+ }
+ }
+ },
+ exec: {
+ output: "Can't use 'tsn ext' as a prefix because it is not a parent command.",
+ error: true
+ }
+ },
+ /*
+ {
+ setup: 'context "tsn deep"',
+ check: {
+ input: 'context "tsn deep"',
+ hints: '',
+ markup: 'VVVVVVVVVVVVVVVVVV',
+ status: 'ERROR',
+ message: '',
+ predictions: [ 'tsn deep' ],
+ unassigned: [ ],
+ args: {
+ command: { name: 'context' },
+ prefix: {
+ value: options.requisition.system.commands.get('tsn deep'),
+ status: 'VALID',
+ message: ''
+ }
+ }
+ },
+ exec: {
+ output: ''
+ }
+ },
+ */
+ {
+ setup: "context",
+ check: {
+ input: "context",
+ hints: " [prefix]",
+ markup: "VVVVVVV",
+ status: "VALID",
+ unassigned: [ ],
+ args: {
+ command: { name: "context" },
+ prefix: { value: undefined, arg: "", status: "VALID", message: "" },
+ }
+ },
+ exec: {
+ output: "Command prefix is unset",
+ type: "string",
+ error: false
+ }
+ }
+ ]);
+};
diff --git a/devtools/client/commandline/test/browser_gcli_date.js b/devtools/client/commandline/test/browser_gcli_date.js
new file mode 100644
index 000000000..9d50aebcb
--- /dev/null
+++ b/devtools/client/commandline/test/browser_gcli_date.js
@@ -0,0 +1,358 @@
+/*
+ * Copyright 2012, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+"use strict";
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
+
+const exports = {};
+
+function test() {
+ helpers.runTestModule(exports, "browser_gcli_date.js");
+}
+
+// var assert = require('../testharness/assert');
+// var helpers = require('./helpers');
+
+var Status = require("gcli/types/types").Status;
+
+exports.testParse = function (options) {
+ var date = options.requisition.system.types.createType("date");
+ return date.parseString("now").then(function (conversion) {
+ // Date comparison - these 2 dates may not be the same, but how close is
+ // close enough? If this test takes more than 30secs to run the it will
+ // probably time out, so we'll assume that these 2 values must be within
+ // 1 min of each other
+ var gap = new Date().getTime() - conversion.value.getTime();
+ assert.ok(gap < 60000, "now is less than a minute away");
+
+ assert.is(conversion.getStatus(), Status.VALID, "now parse");
+ });
+};
+
+exports.testMaxMin = function (options) {
+ var max = new Date();
+ var min = new Date();
+ var types = options.requisition.system.types;
+ var date = types.createType({ name: "date", max: max, min: min });
+ assert.is(date.getMax(), max, "max setup");
+
+ var incremented = date.nudge(min, 1);
+ assert.is(incremented, max, "incremented");
+};
+
+exports.testIncrement = function (options) {
+ var date = options.requisition.system.types.createType("date");
+ return date.parseString("now").then(function (conversion) {
+ var plusOne = date.nudge(conversion.value, 1);
+ var minusOne = date.nudge(plusOne, -1);
+
+ // See comments in testParse
+ var gap = new Date().getTime() - minusOne.getTime();
+ assert.ok(gap < 60000, "now is less than a minute away");
+ });
+};
+
+exports.testInput = function (options) {
+ return helpers.audit(options, [
+ {
+ setup: "tsdate 2001-01-01 1980-01-03",
+ check: {
+ input: "tsdate 2001-01-01 1980-01-03",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVVVVVVVVVVVVVVV",
+ status: "VALID",
+ message: "",
+ args: {
+ command: { name: "tsdate" },
+ d1: {
+ value: function (d1) {
+ assert.is(d1.getFullYear(), 2001, "d1 year");
+ assert.is(d1.getMonth(), 0, "d1 month");
+ assert.is(d1.getDate(), 1, "d1 date");
+ assert.is(d1.getHours(), 0, "d1 hours");
+ assert.is(d1.getMinutes(), 0, "d1 minutes");
+ assert.is(d1.getSeconds(), 0, "d1 seconds");
+ assert.is(d1.getMilliseconds(), 0, "d1 millis");
+ },
+ arg: " 2001-01-01",
+ status: "VALID",
+ message: ""
+ },
+ d2: {
+ value: function (d2) {
+ assert.is(d2.getFullYear(), 1980, "d2 year");
+ assert.is(d2.getMonth(), 0, "d2 month");
+ assert.is(d2.getDate(), 3, "d2 date");
+ assert.is(d2.getHours(), 0, "d2 hours");
+ assert.is(d2.getMinutes(), 0, "d2 minutes");
+ assert.is(d2.getSeconds(), 0, "d2 seconds");
+ assert.is(d2.getMilliseconds(), 0, "d2 millis");
+ },
+ arg: " 1980-01-03",
+ status: "VALID",
+ message: ""
+ },
+ }
+ },
+ exec: {
+ output: [ /^Exec: tsdate/, /2001/, /1980/ ],
+ type: "testCommandOutput",
+ error: false
+ }
+ },
+ {
+ setup: "tsdate 2001/01/01 1980/01/03",
+ check: {
+ input: "tsdate 2001/01/01 1980/01/03",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVVVVVVVVVVVVVVV",
+ status: "VALID",
+ message: "",
+ args: {
+ command: { name: "tsdate" },
+ d1: {
+ value: function (d1) {
+ assert.is(d1.getFullYear(), 2001, "d1 year");
+ assert.is(d1.getMonth(), 0, "d1 month");
+ assert.is(d1.getDate(), 1, "d1 date");
+ assert.is(d1.getHours(), 0, "d1 hours");
+ assert.is(d1.getMinutes(), 0, "d1 minutes");
+ assert.is(d1.getSeconds(), 0, "d1 seconds");
+ assert.is(d1.getMilliseconds(), 0, "d1 millis");
+ },
+ arg: " 2001/01/01",
+ status: "VALID",
+ message: ""
+ },
+ d2: {
+ value: function (d2) {
+ assert.is(d2.getFullYear(), 1980, "d2 year");
+ assert.is(d2.getMonth(), 0, "d2 month");
+ assert.is(d2.getDate(), 3, "d2 date");
+ assert.is(d2.getHours(), 0, "d2 hours");
+ assert.is(d2.getMinutes(), 0, "d2 minutes");
+ assert.is(d2.getSeconds(), 0, "d2 seconds");
+ assert.is(d2.getMilliseconds(), 0, "d2 millis");
+ },
+ arg: " 1980/01/03",
+ status: "VALID",
+ message: ""
+ },
+ }
+ },
+ exec: {
+ output: [ /^Exec: tsdate/, /2001/, /1980/ ],
+ type: "testCommandOutput",
+ error: false
+ }
+ },
+ {
+ setup: "tsdate now today",
+ check: {
+ input: "tsdate now today",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVVV",
+ status: "VALID",
+ message: "",
+ args: {
+ command: { name: "tsdate" },
+ d1: {
+ value: function (d1) {
+ // How long should we allow between d1 and now? Mochitest will
+ // time out after 30 secs, so that seems like a decent upper
+ // limit, although 30 ms should probably do it. I don't think
+ // reducing the limit from 30 secs will find any extra bugs
+ assert.ok(d1.getTime() - new Date().getTime() < 30 * 1000,
+ "d1 time");
+ },
+ arg: " now",
+ status: "VALID",
+ message: ""
+ },
+ d2: {
+ value: function (d2) {
+ // See comment for d1 above
+ assert.ok(d2.getTime() - new Date().getTime() < 30 * 1000,
+ "d2 time");
+ },
+ arg: " today",
+ status: "VALID",
+ message: ""
+ },
+ }
+ },
+ exec: {
+ output: [ /^Exec: tsdate/, new Date().getFullYear() ],
+ type: "testCommandOutput",
+ error: false
+ }
+ },
+ {
+ setup: "tsdate yesterday tomorrow",
+ check: {
+ input: "tsdate yesterday tomorrow",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVVVVVVVVVVVV",
+ status: "VALID",
+ message: "",
+ args: {
+ command: { name: "tsdate" },
+ d1: {
+ value: function (d1) {
+ var compare = new Date().getTime() - (24 * 60 * 60 * 1000);
+ // See comment for d1 in the test for 'tsdate now today'
+ assert.ok(d1.getTime() - compare < 30 * 1000,
+ "d1 time");
+ },
+ arg: " yesterday",
+ status: "VALID",
+ message: ""
+ },
+ d2: {
+ value: function (d2) {
+ var compare = new Date().getTime() + (24 * 60 * 60 * 1000);
+ // See comment for d1 in the test for 'tsdate now today'
+ assert.ok(d2.getTime() - compare < 30 * 1000,
+ "d2 time");
+ },
+ arg: " tomorrow",
+ status: "VALID",
+ message: ""
+ },
+ }
+ },
+ exec: {
+ output: [ /^Exec: tsdate/, new Date().getFullYear() ],
+ type: "testCommandOutput",
+ error: false
+ }
+ }
+ ]);
+};
+
+exports.testIncrDecr = function (options) {
+ return helpers.audit(options, [
+ {
+ // createRequisitionAutomator doesn't fake UP/DOWN well enough
+ skipRemainingIf: options.isNode,
+ setup: "tsdate 2001-01-01<UP>",
+ check: {
+ input: "tsdate 2001-01-02",
+ hints: " <d2>",
+ markup: "VVVVVVVVVVVVVVVVV",
+ status: "ERROR",
+ message: "",
+ args: {
+ command: { name: "tsdate" },
+ d1: {
+ value: function (d1) {
+ assert.is(d1.getFullYear(), 2001, "d1 year");
+ assert.is(d1.getMonth(), 0, "d1 month");
+ assert.is(d1.getDate(), 2, "d1 date");
+ assert.is(d1.getHours(), 0, "d1 hours");
+ assert.is(d1.getMinutes(), 0, "d1 minutes");
+ assert.is(d1.getSeconds(), 0, "d1 seconds");
+ assert.is(d1.getMilliseconds(), 0, "d1 millis");
+ },
+ arg: " 2001-01-02",
+ status: "VALID",
+ message: ""
+ },
+ d2: {
+ value: undefined,
+ status: "INCOMPLETE"
+ },
+ }
+ }
+ },
+ {
+ // Check wrapping on decrement
+ setup: "tsdate 2001-02-01<DOWN>",
+ check: {
+ input: "tsdate 2001-01-31",
+ hints: " <d2>",
+ markup: "VVVVVVVVVVVVVVVVV",
+ status: "ERROR",
+ message: "",
+ args: {
+ command: { name: "tsdate" },
+ d1: {
+ value: function (d1) {
+ assert.is(d1.getFullYear(), 2001, "d1 year");
+ assert.is(d1.getMonth(), 0, "d1 month");
+ assert.is(d1.getDate(), 31, "d1 date");
+ assert.is(d1.getHours(), 0, "d1 hours");
+ assert.is(d1.getMinutes(), 0, "d1 minutes");
+ assert.is(d1.getSeconds(), 0, "d1 seconds");
+ assert.is(d1.getMilliseconds(), 0, "d1 millis");
+ },
+ arg: " 2001-01-31",
+ status: "VALID",
+ message: ""
+ },
+ d2: {
+ value: undefined,
+ status: "INCOMPLETE"
+ },
+ }
+ }
+ },
+ {
+ // Check 'max' value capping on increment
+ setup: 'tsdate 2001-02-01 "27 feb 2000"<UP>',
+ check: {
+ input: 'tsdate 2001-02-01 "2000-02-28"',
+ hints: "",
+ markup: "VVVVVVVVVVVVVVVVVVVVVVVVVVVVVV",
+ status: "VALID",
+ message: "",
+ args: {
+ command: { name: "tsdate" },
+ d1: {
+ value: function (d1) {
+ assert.is(d1.getFullYear(), 2001, "d1 year");
+ assert.is(d1.getMonth(), 1, "d1 month");
+ assert.is(d1.getDate(), 1, "d1 date");
+ assert.is(d1.getHours(), 0, "d1 hours");
+ assert.is(d1.getMinutes(), 0, "d1 minutes");
+ assert.is(d1.getSeconds(), 0, "d1 seconds");
+ assert.is(d1.getMilliseconds(), 0, "d1 millis");
+ },
+ arg: " 2001-02-01",
+ status: "VALID",
+ message: ""
+ },
+ d2: {
+ value: function (d2) {
+ assert.is(d2.getFullYear(), 2000, "d2 year");
+ assert.is(d2.getMonth(), 1, "d2 month");
+ assert.is(d2.getDate(), 28, "d2 date");
+ assert.is(d2.getHours(), 0, "d2 hours");
+ assert.is(d2.getMinutes(), 0, "d2 minutes");
+ assert.is(d2.getSeconds(), 0, "d2 seconds");
+ assert.is(d2.getMilliseconds(), 0, "d2 millis");
+ },
+ arg: ' "2000-02-28"',
+ status: "VALID",
+ message: ""
+ },
+ }
+ }
+ }
+ ]);
+};
diff --git a/devtools/client/commandline/test/browser_gcli_exec.js b/devtools/client/commandline/test/browser_gcli_exec.js
new file mode 100644
index 000000000..ef3b9fffe
--- /dev/null
+++ b/devtools/client/commandline/test/browser_gcli_exec.js
@@ -0,0 +1,656 @@
+/*
+ * Copyright 2012, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+"use strict";
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
+
+const exports = {};
+
+function test() {
+ helpers.runTestModule(exports, "browser_gcli_exec.js");
+}
+
+// var assert = require('../testharness/assert');
+// var helpers = require('./helpers');
+
+exports.testParamGroup = function (options) {
+ var tsg = options.requisition.system.commands.get("tsg");
+
+ assert.is(tsg.params[0].groupName, null, "tsg param 0 group null");
+ assert.is(tsg.params[1].groupName, "First", "tsg param 1 group First");
+ assert.is(tsg.params[2].groupName, "First", "tsg param 2 group First");
+ assert.is(tsg.params[3].groupName, "Second", "tsg param 3 group Second");
+ assert.is(tsg.params[4].groupName, "Second", "tsg param 4 group Second");
+};
+
+exports.testWithHelpers = function (options) {
+ return helpers.audit(options, [
+ {
+ setup: "tss",
+ check: {
+ input: "tss",
+ hints: "",
+ markup: "VVV",
+ cursor: 3,
+ current: "__command",
+ status: "VALID",
+ unassigned: [ ],
+ args: {
+ command: { name: "tss" },
+ }
+ },
+ exec: {
+ output: /^Exec: tss/,
+ }
+ },
+ {
+ setup: "tsv option1 10",
+ check: {
+ input: "tsv option1 10",
+ hints: "",
+ markup: "VVVVVVVVVVVVVV",
+ cursor: 14,
+ current: "optionValue",
+ status: "VALID",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "tsv" },
+ optionType: {
+ value: "string",
+ arg: " option1",
+ status: "VALID",
+ message: ""
+ },
+ optionValue: {
+ arg: " 10",
+ status: "VALID",
+ message: ""
+ }
+ }
+ },
+ exec: {
+ output: "Exec: tsv optionType=option1 optionValue=10"
+ }
+ },
+ {
+ setup: "tsv option2 10",
+ check: {
+ input: "tsv option2 10",
+ hints: "",
+ markup: "VVVVVVVVVVVVVV",
+ cursor: 14,
+ current: "optionValue",
+ status: "VALID",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "tsv" },
+ optionType: {
+ value: "number",
+ arg: " option2",
+ status: "VALID",
+ message: ""
+ },
+ optionValue: {
+ arg: " 10",
+ status: "VALID",
+ message: ""
+ }
+ }
+ },
+ exec: {
+ output: "Exec: tsv optionType=option2 optionValue=10"
+ }
+ },
+ // Delegated remote types can't transfer value types so we only test for
+ // the value of optionValue when we're local
+ {
+ skipIf: options.isRemote,
+ setup: "tsv option1 10",
+ check: {
+ args: { optionValue: { value: "10" } }
+ },
+ exec: {
+ output: "Exec: tsv optionType=option1 optionValue=10"
+ }
+ },
+ {
+ skipIf: options.isRemote,
+ setup: "tsv option2 10",
+ check: {
+ args: { optionValue: { value: 10 } }
+ },
+ exec: {
+ output: "Exec: tsv optionType=option2 optionValue=10"
+ }
+ }
+ ]);
+};
+
+exports.testExecText = function (options) {
+ return helpers.audit(options, [
+ {
+ setup: "tsr fred",
+ check: {
+ input: "tsr fred",
+ hints: "",
+ markup: "VVVVVVVV",
+ cursor: 8,
+ current: "text",
+ status: "VALID",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "tsr" },
+ text: {
+ value: "fred",
+ arg: " fred",
+ status: "VALID",
+ message: ""
+ }
+ }
+ },
+ exec: {
+ output: "Exec: tsr text=fred"
+ }
+ },
+ {
+ setup: "tsr fred bloggs",
+ check: {
+ input: "tsr fred bloggs",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVV",
+ cursor: 15,
+ current: "text",
+ status: "VALID",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "tsr" },
+ text: {
+ value: "fred bloggs",
+ arg: " fred bloggs",
+ status: "VALID",
+ message: ""
+ }
+ }
+ },
+ exec: {
+ output: "Exec: tsr text=fred\\ bloggs"
+ }
+ },
+ {
+ setup: 'tsr "fred bloggs"',
+ check: {
+ input: 'tsr "fred bloggs"',
+ hints: "",
+ markup: "VVVVVVVVVVVVVVVVV",
+ cursor: 17,
+ current: "text",
+ status: "VALID",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "tsr" },
+ text: {
+ value: "fred bloggs",
+ arg: ' "fred bloggs"',
+ status: "VALID",
+ message: ""
+ }
+ }
+ },
+ exec: {
+ output: "Exec: tsr text=fred\\ bloggs"
+ }
+ },
+ {
+ setup: 'tsr "fred bloggs',
+ check: {
+ input: 'tsr "fred bloggs',
+ hints: "",
+ markup: "VVVVVVVVVVVVVVVV",
+ cursor: 16,
+ current: "text",
+ status: "VALID",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "tsr" },
+ text: {
+ value: "fred bloggs",
+ arg: ' "fred bloggs',
+ status: "VALID",
+ message: ""
+ }
+ }
+ },
+ exec: {
+ output: "Exec: tsr text=fred\\ bloggs"
+ }
+ }
+ ]);
+};
+
+exports.testExecBoolean = function (options) {
+ return helpers.audit(options, [
+ {
+ setup: "tsb",
+ check: {
+ input: "tsb",
+ hints: " [toggle]",
+ markup: "VVV",
+ cursor: 3,
+ current: "__command",
+ status: "VALID",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "tsb" },
+ toggle: {
+ value: false,
+ arg: "",
+ status: "VALID",
+ message: ""
+ }
+ }
+ },
+ exec: {
+ output: "Exec: tsb toggle=false"
+ }
+ },
+ {
+ setup: "tsb --toggle",
+ check: {
+ input: "tsb --toggle",
+ hints: "",
+ markup: "VVVVVVVVVVVV",
+ cursor: 12,
+ current: "toggle",
+ status: "VALID",
+ predictions: [ ],
+ unassigned: [ ],
+ outputState: "false:default",
+ args: {
+ command: { name: "tsb" },
+ toggle: {
+ value: true,
+ arg: " --toggle",
+ status: "VALID",
+ message: ""
+ }
+ }
+ },
+ exec: {
+ output: "Exec: tsb toggle=true"
+ }
+ }
+ ]);
+};
+
+exports.testExecNumber = function (options) {
+ return helpers.audit(options, [
+ {
+ setup: "tsu 10",
+ check: {
+ input: "tsu 10",
+ hints: "",
+ markup: "VVVVVV",
+ cursor: 6,
+ current: "num",
+ status: "VALID",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "tsu" },
+ num: { value: 10, arg: " 10", status: "VALID", message: "" }
+ }
+ },
+ exec: {
+ output: "Exec: tsu num=10"
+ }
+ },
+ {
+ setup: "tsu --num 10",
+ check: {
+ input: "tsu --num 10",
+ hints: "",
+ markup: "VVVVVVVVVVVV",
+ cursor: 12,
+ current: "num",
+ status: "VALID",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "tsu" },
+ num: { value: 10, arg: " --num 10", status: "VALID", message: "" }
+ }
+ },
+ exec: {
+ output: "Exec: tsu num=10"
+ }
+ }
+ ]);
+};
+
+exports.testExecScript = function (options) {
+ return helpers.audit(options, [
+ {
+ // Bug 704829 - Enable GCLI Javascript parameters
+ // The answer to this should be 2
+ setup: "tsj { 1 + 1 }",
+ check: {
+ input: "tsj { 1 + 1 }",
+ hints: "",
+ markup: "VVVVVVVVVVVVV",
+ cursor: 13,
+ current: "javascript",
+ status: "VALID",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "tsj" },
+ javascript: {
+ arg: " { 1 + 1 }",
+ status: "VALID",
+ message: ""
+ }
+ }
+ },
+ exec: {
+ output: "Exec: tsj javascript=1 + 1"
+ }
+ }
+ ]);
+};
+
+exports.testExecNode = function (options) {
+ return helpers.audit(options, [
+ {
+ skipIf: options.isRemote,
+ setup: "tse :root",
+ check: {
+ input: "tse :root",
+ hints: " [options]",
+ markup: "VVVVVVVVV",
+ cursor: 9,
+ current: "node",
+ status: "VALID",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "tse" },
+ node: {
+ arg: " :root",
+ status: "VALID",
+ message: ""
+ },
+ nodes: {
+ arg: "",
+ status: "VALID",
+ message: ""
+ },
+ nodes2: {
+ arg: "",
+ status: "VALID",
+ message: ""
+ }
+ }
+ },
+ exec: {
+ output: /^Exec: tse/
+ },
+ post: function (output) {
+ assert.is(output.data.args.node, ":root", "node should be :root");
+ assert.is(output.data.args.nodes, "Error", "nodes should be Error");
+ assert.is(output.data.args.nodes2, "Error", "nodes2 should be Error");
+ }
+ }
+ ]);
+};
+
+exports.testExecSubCommand = function (options) {
+ return helpers.audit(options, [
+ {
+ setup: "tsn dif fred",
+ check: {
+ input: "tsn dif fred",
+ hints: "",
+ markup: "VVVVVVVVVVVV",
+ cursor: 12,
+ current: "text",
+ status: "VALID",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "tsn dif" },
+ text: { value: "fred", arg: " fred", status: "VALID", message: "" }
+ }
+ },
+ exec: {
+ output: "Exec: tsnDif text=fred"
+ }
+ },
+ {
+ setup: "tsn exten fred",
+ check: {
+ input: "tsn exten fred",
+ hints: "",
+ markup: "VVVVVVVVVVVVVV",
+ cursor: 14,
+ current: "text",
+ status: "VALID",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "tsn exten" },
+ text: { value: "fred", arg: " fred", status: "VALID", message: "" },
+ }
+ },
+ exec: {
+ output: "Exec: tsnExten text=fred"
+ }
+ },
+ {
+ setup: "tsn extend fred",
+ check: {
+ input: "tsn extend fred",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVV",
+ cursor: 15,
+ current: "text",
+ status: "VALID",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "tsn extend" },
+ text: { value: "fred", arg: " fred", status: "VALID", message: "" },
+ }
+ },
+ exec: {
+ output: "Exec: tsnExtend text=fred"
+ }
+ }
+ ]);
+};
+
+exports.testExecArray = function (options) {
+ return helpers.audit(options, [
+ {
+ setup: "tselarr 1",
+ check: {
+ input: "tselarr 1",
+ hints: "",
+ markup: "VVVVVVVVV",
+ cursor: 9,
+ current: "num",
+ status: "VALID",
+ predictions: ["1"],
+ unassigned: [ ],
+ outputState: "false:default",
+ args: {
+ command: { name: "tselarr" },
+ num: { value: "1", arg: " 1", status: "VALID", message: "" },
+ arr: { /* value:,*/ arg: "{}", status: "VALID", message: "" },
+ }
+ },
+ exec: {
+ output: "Exec: tselarr num=1 arr="
+ }
+ },
+ {
+ setup: "tselarr 1 a",
+ check: {
+ input: "tselarr 1 a",
+ hints: "",
+ markup: "VVVVVVVVVVV",
+ cursor: 11,
+ current: "arr",
+ status: "VALID",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "tselarr" },
+ num: { value: "1", arg: " 1", status: "VALID", message: "" },
+ arr: { /* value:a,*/ arg: "{ a}", status: "VALID", message: "" },
+ }
+ },
+ exec: {
+ output: "Exec: tselarr num=1 arr=a"
+ }
+ },
+ {
+ setup: "tselarr 1 a b",
+ check: {
+ input: "tselarr 1 a b",
+ hints: "",
+ markup: "VVVVVVVVVVVVV",
+ cursor: 13,
+ current: "arr",
+ status: "VALID",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "tselarr" },
+ num: { value: "1", arg: " 1", status: "VALID", message: "" },
+ arr: { /* value:a,b,*/ arg: "{ a, b}", status: "VALID", message: "" },
+ }
+ },
+ exec: {
+ output: "Exec: tselarr num=1 arr=a b"
+ }
+ }
+ ]);
+};
+
+exports.testExecMultiple = function (options) {
+ return helpers.audit(options, [
+ {
+ setup: "tsm a 10 10",
+ check: {
+ input: "tsm a 10 10",
+ hints: "",
+ markup: "VVVVVVVVVVV",
+ cursor: 11,
+ current: "num",
+ status: "VALID",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "tsm" },
+ abc: { value: "a", arg: " a", status: "VALID", message: "" },
+ txt: { value: "10", arg: " 10", status: "VALID", message: "" },
+ num: { value: 10, arg: " 10", status: "VALID", message: "" },
+ }
+ },
+ exec: {
+ output: "Exec: tsm abc=a txt=10 num=10"
+ }
+ }
+ ]);
+};
+
+exports.testExecDefaults = function (options) {
+ return helpers.audit(options, [
+ {
+ // Bug 707009 - GCLI doesn't always fill in default parameters properly
+ setup: "tsg aaa",
+ check: {
+ input: "tsg aaa",
+ hints: " [options]",
+ markup: "VVVVVVV",
+ cursor: 7,
+ current: "solo",
+ status: "VALID",
+ predictions: ["aaa"],
+ unassigned: [ ],
+ args: {
+ command: { name: "tsg" },
+ solo: { value: "aaa", arg: " aaa", status: "VALID", message: "" },
+ txt1: { value: undefined, arg: "", status: "VALID", message: "" },
+ bool: { value: false, arg: "", status: "VALID", message: "" },
+ txt2: { value: undefined, arg: "", status: "VALID", message: "" },
+ num: { value: undefined, arg: "", status: "VALID", message: "" },
+ }
+ },
+ exec: {
+ output: "Exec: tsg solo=aaa txt1= bool=false txt2=d num=42"
+ }
+ }
+ ]);
+};
+
+exports.testNested = function (options) {
+ var commands = options.requisition.system.commands;
+ commands.add({
+ name: "nestorama",
+ exec: function (args, context) {
+ return context.updateExec("tsb").then(function (tsbOutput) {
+ return context.updateExec("tsu 6").then(function (tsuOutput) {
+ return JSON.stringify({
+ tsb: tsbOutput.data,
+ tsu: tsuOutput.data
+ });
+ });
+ });
+ }
+ });
+
+ return helpers.audit(options, [
+ {
+ setup: "nestorama",
+ exec: {
+ output:
+ "{" +
+ '"tsb":{' +
+ '"name":"tsb",' +
+ '"args":{"toggle":"false"}' +
+ "}," +
+ '"tsu":{' +
+ '"name":"tsu",' +
+ '"args":{"num":"6"}' +
+ "}" +
+ "}"
+ },
+ post: function () {
+ commands.remove("nestorama");
+ }
+ }
+ ]);
+};
diff --git a/devtools/client/commandline/test/browser_gcli_fail.js b/devtools/client/commandline/test/browser_gcli_fail.js
new file mode 100644
index 000000000..6a9b5e9f5
--- /dev/null
+++ b/devtools/client/commandline/test/browser_gcli_fail.js
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2012, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+"use strict";
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
+
+const exports = {};
+
+function test() {
+ helpers.runTestModule(exports, "browser_gcli_fail.js");
+}
+
+// var helpers = require('./helpers');
+
+exports.testBasic = function (options) {
+ return helpers.audit(options, [
+ {
+ setup: "tsfail reject",
+ exec: {
+ output: "rejected promise",
+ type: "error",
+ error: true
+ }
+ },
+ {
+ setup: "tsfail rejecttyped",
+ exec: {
+ output: "54",
+ type: "number",
+ error: true
+ }
+ },
+ {
+ setup: "tsfail throwerror",
+ exec: {
+ output: /thrown error$/,
+ type: "error",
+ error: true
+ }
+ },
+ {
+ setup: "tsfail throwstring",
+ exec: {
+ output: "thrown string",
+ type: "error",
+ error: true
+ }
+ },
+ {
+ setup: "tsfail noerror",
+ exec: {
+ output: "no error",
+ type: "string",
+ error: false
+ }
+ }
+ ]);
+};
diff --git a/devtools/client/commandline/test/browser_gcli_file.js b/devtools/client/commandline/test/browser_gcli_file.js
new file mode 100644
index 000000000..4b24ed570
--- /dev/null
+++ b/devtools/client/commandline/test/browser_gcli_file.js
@@ -0,0 +1,821 @@
+/*
+ * Copyright 2012, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+"use strict";
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
+
+const exports = {};
+
+function test() {
+ helpers.runTestModule(exports, "browser_gcli_file.js");
+}
+
+// var helpers = require('./helpers');
+
+var local = false;
+
+exports.testBasic = function (options) {
+ return helpers.audit(options, [
+ {
+ skipRemainingIf: options.isFirefox, // No file implementation in Firefox
+ setup: "tsfile open /",
+ check: {
+ input: "tsfile open /",
+ hints: "",
+ markup: "VVVVVVVVVVVVI",
+ cursor: 13,
+ current: "p1",
+ status: "ERROR",
+ message: "'/' is not a file",
+ args: {
+ command: { name: "tsfile open" },
+ p1: {
+ value: undefined,
+ arg: " /",
+ status: "INCOMPLETE",
+ message: "'/' is not a file"
+ }
+ }
+ }
+ },
+ {
+ setup: "tsfile open /zxcv",
+ check: {
+ input: "tsfile open /zxcv",
+ // hints: ' -> /etc/',
+ markup: "VVVVVVVVVVVVIIIII",
+ cursor: 17,
+ current: "p1",
+ status: "ERROR",
+ message: "'/zxcv' doesn't exist",
+ args: {
+ command: { name: "tsfile open" },
+ p1: {
+ value: undefined,
+ arg: " /zxcv",
+ status: "INCOMPLETE",
+ message: "'/zxcv' doesn't exist"
+ }
+ }
+ }
+ },
+ {
+ skipIf: !local,
+ setup: "tsfile open /mach_kernel",
+ check: {
+ input: "tsfile open /mach_kernel",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVVVVVVVVVVV",
+ cursor: 24,
+ current: "p1",
+ status: "VALID",
+ message: "",
+ args: {
+ command: { name: "tsfile open" },
+ p1: {
+ value: "/mach_kernel",
+ arg: " /mach_kernel",
+ status: "VALID",
+ message: ""
+ }
+ }
+ }
+ },
+ {
+ setup: "tsfile saveas /",
+ check: {
+ input: "tsfile saveas /",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVI",
+ cursor: 15,
+ current: "p1",
+ status: "ERROR",
+ message: "'/' already exists",
+ args: {
+ command: { name: "tsfile saveas" },
+ p1: {
+ value: undefined,
+ arg: " /",
+ status: "INCOMPLETE",
+ message: "'/' already exists"
+ }
+ }
+ }
+ },
+ {
+ setup: "tsfile saveas /zxcv",
+ check: {
+ input: "tsfile saveas /zxcv",
+ // hints: ' -> /etc/',
+ markup: "VVVVVVVVVVVVVVVVVVV",
+ cursor: 19,
+ current: "p1",
+ status: "VALID",
+ message: "",
+ args: {
+ command: { name: "tsfile saveas" },
+ p1: {
+ value: "/zxcv",
+ arg: " /zxcv",
+ status: "VALID",
+ message: ""
+ }
+ }
+ }
+ },
+ {
+ skipIf: !local,
+ setup: "tsfile saveas /mach_kernel",
+ check: {
+ input: "tsfile saveas /mach_kernel",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVIIIIIIIIIIII",
+ cursor: 26,
+ current: "p1",
+ status: "ERROR",
+ message: "'/mach_kernel' already exists",
+ args: {
+ command: { name: "tsfile saveas" },
+ p1: {
+ value: undefined,
+ arg: " /mach_kernel",
+ status: "INCOMPLETE",
+ message: "'/mach_kernel' already exists"
+ }
+ }
+ }
+ },
+ {
+ setup: "tsfile save /",
+ check: {
+ input: "tsfile save /",
+ hints: "",
+ markup: "VVVVVVVVVVVVI",
+ cursor: 13,
+ current: "p1",
+ status: "ERROR",
+ message: "'/' is not a file",
+ args: {
+ command: { name: "tsfile save" },
+ p1: {
+ value: undefined,
+ arg: " /",
+ status: "INCOMPLETE",
+ message: "'/' is not a file"
+ }
+ }
+ }
+ },
+ {
+ setup: "tsfile save /zxcv",
+ check: {
+ input: "tsfile save /zxcv",
+ // hints: ' -> /etc/',
+ markup: "VVVVVVVVVVVVVVVVV",
+ cursor: 17,
+ current: "p1",
+ status: "VALID",
+ message: "",
+ args: {
+ command: { name: "tsfile save" },
+ p1: {
+ value: "/zxcv",
+ arg: " /zxcv",
+ status: "VALID",
+ message: ""
+ }
+ }
+ }
+ },
+ {
+ skipIf: !local,
+ setup: "tsfile save /mach_kernel",
+ check: {
+ input: "tsfile save /mach_kernel",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVVVVVVVVVVV",
+ cursor: 24,
+ current: "p1",
+ status: "VALID",
+ message: "",
+ args: {
+ command: { name: "tsfile save" },
+ p1: {
+ value: "/mach_kernel",
+ arg: " /mach_kernel",
+ status: "VALID",
+ message: ""
+ }
+ }
+ }
+ },
+ {
+ setup: "tsfile cd /",
+ check: {
+ input: "tsfile cd /",
+ hints: "",
+ markup: "VVVVVVVVVVV",
+ cursor: 11,
+ current: "p1",
+ status: "VALID",
+ message: "",
+ args: {
+ command: { name: "tsfile cd" },
+ p1: {
+ value: "/",
+ arg: " /",
+ status: "VALID",
+ message: ""
+ }
+ }
+ }
+ },
+ {
+ setup: "tsfile cd /zxcv",
+ check: {
+ input: "tsfile cd /zxcv",
+ // hints: ' -> /dev/',
+ markup: "VVVVVVVVVVIIIII",
+ cursor: 15,
+ current: "p1",
+ status: "ERROR",
+ message: "'/zxcv' doesn't exist",
+ args: {
+ command: { name: "tsfile cd" },
+ p1: {
+ value: undefined,
+ arg: " /zxcv",
+ status: "INCOMPLETE",
+ message: "'/zxcv' doesn't exist"
+ }
+ }
+ }
+ },
+ {
+ skipIf: true || !local,
+ setup: "tsfile cd /etc/passwd",
+ check: {
+ input: "tsfile cd /etc/passwd",
+ hints: " -> /etc/pam.d/",
+ markup: "VVVVVVVVVVIIIIIIIIIII",
+ cursor: 21,
+ current: "p1",
+ status: "ERROR",
+ message: "'/etc/passwd' is not a directory",
+ args: {
+ command: { name: "tsfile cd" },
+ p1: {
+ value: undefined,
+ arg: " /etc/passwd",
+ status: "INCOMPLETE",
+ message: "'/etc/passwd' is not a directory"
+ }
+ }
+ }
+ },
+ {
+ setup: "tsfile mkdir /",
+ check: {
+ input: "tsfile mkdir /",
+ hints: "",
+ markup: "VVVVVVVVVVVVVI",
+ cursor: 14,
+ current: "p1",
+ status: "ERROR",
+ message: "" / " already exists",
+ args: {
+ command: { name: "tsfile mkdir" },
+ p1: {
+ value: undefined,
+ arg: " /",
+ status: "INCOMPLETE",
+ message: "'/' already exists"
+ }
+ }
+ }
+ },
+ {
+ setup: "tsfile mkdir /zxcv",
+ check: {
+ input: "tsfile mkdir /zxcv",
+ // hints: ' -> /dev/',
+ markup: "VVVVVVVVVVVVVVVVVV",
+ cursor: 18,
+ current: "p1",
+ status: "VALID",
+ message: "",
+ args: {
+ command: { name: "tsfile mkdir" },
+ p1: {
+ value: "/zxcv",
+ arg: " /zxcv",
+ status: "VALID",
+ message: ""
+ }
+ }
+ }
+ },
+ {
+ skipIf: !local,
+ setup: "tsfile mkdir /mach_kernel",
+ check: {
+ input: "tsfile mkdir /mach_kernel",
+ hints: "",
+ markup: "VVVVVVVVVVVVVIIIIIIIIIIII",
+ cursor: 25,
+ current: "p1",
+ status: "ERROR",
+ message: "'/mach_kernel' already exists",
+ args: {
+ command: { name: "tsfile mkdir" },
+ p1: {
+ value: undefined,
+ arg: " /mach_kernel",
+ status: "INCOMPLETE",
+ message: "'/mach_kernel' already exists"
+ }
+ }
+ }
+ },
+ {
+ setup: "tsfile rm /",
+ check: {
+ input: "tsfile rm /",
+ hints: "",
+ markup: "VVVVVVVVVVV",
+ cursor: 11,
+ current: "p1",
+ status: "VALID",
+ message: "",
+ args: {
+ command: { name: "tsfile rm" },
+ p1: {
+ value: "/",
+ arg: " /",
+ status: "VALID",
+ message: ""
+ }
+ }
+ }
+ },
+ {
+ setup: "tsfile rm /zxcv",
+ check: {
+ input: "tsfile rm /zxcv",
+ // hints: ' -> /etc/',
+ markup: "VVVVVVVVVVIIIII",
+ cursor: 15,
+ current: "p1",
+ status: "ERROR",
+ message: "'/zxcv' doesn't exist",
+ args: {
+ command: { name: "tsfile rm" },
+ p1: {
+ value: undefined,
+ arg: " /zxcv",
+ status: "INCOMPLETE",
+ message: "'/zxcv' doesn't exist"
+ }
+ }
+ }
+ },
+ {
+ skipIf: !local,
+ setup: "tsfile rm /mach_kernel",
+ check: {
+ input: "tsfile rm /mach_kernel",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVVVVVVVVV",
+ cursor: 22,
+ current: "p1",
+ status: "VALID",
+ message: "",
+ args: {
+ command: { name: "tsfile rm" },
+ p1: {
+ value: "/mach_kernel",
+ arg: " /mach_kernel",
+ status: "VALID",
+ message: ""
+ }
+ }
+ }
+ }
+ ]);
+};
+
+exports.testFirefoxBasic = function (options) {
+ return helpers.audit(options, [
+ {
+ // These tests are just like the ones above tailored for running in
+ // Firefox
+ skipRemainingIf: true,
+ // skipRemainingIf: !options.isFirefox,
+ skipIf: true,
+ setup: "tsfile open /",
+ check: {
+ input: "tsfile open /",
+ hints: "",
+ markup: "VVVVVVVVVVVVI",
+ cursor: 13,
+ current: "p1",
+ status: "ERROR",
+ message: "'/' is not a file",
+ args: {
+ command: { name: "tsfile open" },
+ p1: {
+ value: undefined,
+ arg: " /",
+ status: "INCOMPLETE",
+ message: "'/' is not a file"
+ }
+ }
+ }
+ },
+ {
+ skipIf: true,
+ setup: "tsfile open /zxcv",
+ check: {
+ input: "tsfile open /zxcv",
+ // hints: ' -> /etc/',
+ markup: "VVVVVVVVVVVVIIIII",
+ cursor: 17,
+ current: "p1",
+ status: "ERROR",
+ message: "'/zxcv' doesn't exist",
+ args: {
+ command: { name: "tsfile open" },
+ p1: {
+ value: undefined,
+ arg: " /zxcv",
+ status: "INCOMPLETE",
+ message: "'/zxcv' doesn't exist"
+ }
+ }
+ }
+ },
+ {
+ skipIf: !local,
+ setup: "tsfile open /mach_kernel",
+ check: {
+ input: "tsfile open /mach_kernel",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVVVVVVVVVVV",
+ cursor: 24,
+ current: "p1",
+ status: "VALID",
+ message: "",
+ args: {
+ command: { name: "tsfile open" },
+ p1: {
+ value: "/mach_kernel",
+ arg: " /mach_kernel",
+ status: "VALID",
+ message: ""
+ }
+ }
+ }
+ },
+ {
+ skipIf: true,
+ setup: "tsfile saveas /",
+ check: {
+ input: "tsfile saveas /",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVI",
+ cursor: 15,
+ current: "p1",
+ status: "ERROR",
+ message: "'/' already exists",
+ args: {
+ command: { name: "tsfile saveas" },
+ p1: {
+ value: undefined,
+ arg: " /",
+ status: "INCOMPLETE",
+ message: "'/' already exists"
+ }
+ }
+ }
+ },
+ {
+ skipIf: true,
+ setup: "tsfile saveas /zxcv",
+ check: {
+ input: "tsfile saveas /zxcv",
+ // hints: ' -> /etc/',
+ markup: "VVVVVVVVVVVVVVVVVVV",
+ cursor: 19,
+ current: "p1",
+ status: "VALID",
+ message: "",
+ args: {
+ command: { name: "tsfile saveas" },
+ p1: {
+ value: "/zxcv",
+ arg: " /zxcv",
+ status: "VALID",
+ message: ""
+ }
+ }
+ }
+ },
+ {
+ skipIf: !local,
+ setup: "tsfile saveas /mach_kernel",
+ check: {
+ input: "tsfile saveas /mach_kernel",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVIIIIIIIIIIII",
+ cursor: 26,
+ current: "p1",
+ status: "ERROR",
+ message: "'/mach_kernel' already exists",
+ args: {
+ command: { name: "tsfile saveas" },
+ p1: {
+ value: undefined,
+ arg: " /mach_kernel",
+ status: "INCOMPLETE",
+ message: "'/mach_kernel' already exists"
+ }
+ }
+ }
+ },
+ {
+ skipIf: true,
+ setup: "tsfile save /",
+ check: {
+ input: "tsfile save /",
+ hints: "",
+ markup: "VVVVVVVVVVVVI",
+ cursor: 13,
+ current: "p1",
+ status: "ERROR",
+ message: "'/' is not a file",
+ args: {
+ command: { name: "tsfile save" },
+ p1: {
+ value: undefined,
+ arg: " /",
+ status: "INCOMPLETE",
+ message: "'/' is not a file"
+ }
+ }
+ }
+ },
+ {
+ skipIf: true,
+ setup: "tsfile save /zxcv",
+ check: {
+ input: "tsfile save /zxcv",
+ // hints: ' -> /etc/',
+ markup: "VVVVVVVVVVVVVVVVV",
+ cursor: 17,
+ current: "p1",
+ status: "VALID",
+ message: "",
+ args: {
+ command: { name: "tsfile save" },
+ p1: {
+ value: "/zxcv",
+ arg: " /zxcv",
+ status: "VALID",
+ message: ""
+ }
+ }
+ }
+ },
+ {
+ skipIf: !local,
+ setup: "tsfile save /mach_kernel",
+ check: {
+ input: "tsfile save /mach_kernel",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVVVVVVVVVVV",
+ cursor: 24,
+ current: "p1",
+ status: "VALID",
+ message: "",
+ args: {
+ command: { name: "tsfile save" },
+ p1: {
+ value: "/mach_kernel",
+ arg: " /mach_kernel",
+ status: "VALID",
+ message: ""
+ }
+ }
+ }
+ },
+ {
+ setup: "tsfile cd /",
+ check: {
+ input: "tsfile cd /",
+ hints: "",
+ markup: "VVVVVVVVVVV",
+ cursor: 11,
+ current: "p1",
+ status: "VALID",
+ message: "",
+ args: {
+ command: { name: "tsfile cd" },
+ p1: {
+ value: "/",
+ arg: " /",
+ status: "VALID",
+ message: ""
+ }
+ }
+ }
+ },
+ {
+ setup: "tsfile cd /zxcv",
+ check: {
+ input: "tsfile cd /zxcv",
+ // hints: ' -> /dev/',
+ // markup: 'VVVVVVVVVVIIIII',
+ cursor: 15,
+ current: "p1",
+ // status: 'ERROR',
+ message: "'/zxcv' doesn't exist",
+ args: {
+ command: { name: "tsfile cd" },
+ p1: {
+ value: undefined,
+ arg: " /zxcv",
+ // status: 'INCOMPLETE',
+ message: "'/zxcv' doesn't exist"
+ }
+ }
+ }
+ },
+ {
+ skipIf: true || !local,
+ setup: "tsfile cd /etc/passwd",
+ check: {
+ input: "tsfile cd /etc/passwd",
+ hints: " -> /etc/pam.d/",
+ markup: "VVVVVVVVVVIIIIIIIIIII",
+ cursor: 21,
+ current: "p1",
+ status: "ERROR",
+ message: "'/etc/passwd' is not a directory",
+ args: {
+ command: { name: "tsfile cd" },
+ p1: {
+ value: undefined,
+ arg: " /etc/passwd",
+ status: "INCOMPLETE",
+ message: "'/etc/passwd' is not a directory"
+ }
+ }
+ }
+ },
+ {
+ setup: "tsfile mkdir /",
+ check: {
+ input: "tsfile mkdir /",
+ hints: "",
+ markup: "VVVVVVVVVVVVVI",
+ cursor: 14,
+ current: "p1",
+ status: "ERROR",
+ message: "" / " already exists",
+ args: {
+ command: { name: "tsfile mkdir" },
+ p1: {
+ value: undefined,
+ arg: " /",
+ status: "INCOMPLETE",
+ message: "'/' already exists"
+ }
+ }
+ }
+ },
+ {
+ setup: "tsfile mkdir /zxcv",
+ check: {
+ input: "tsfile mkdir /zxcv",
+ // hints: ' -> /dev/',
+ markup: "VVVVVVVVVVVVVVVVVV",
+ cursor: 18,
+ current: "p1",
+ status: "VALID",
+ message: "",
+ args: {
+ command: { name: "tsfile mkdir" },
+ p1: {
+ value: "/zxcv",
+ arg: " /zxcv",
+ status: "VALID",
+ message: ""
+ }
+ }
+ }
+ },
+ {
+ skipIf: !local,
+ setup: "tsfile mkdir /mach_kernel",
+ check: {
+ input: "tsfile mkdir /mach_kernel",
+ hints: "",
+ markup: "VVVVVVVVVVVVVIIIIIIIIIIII",
+ cursor: 25,
+ current: "p1",
+ status: "ERROR",
+ message: "'/mach_kernel' already exists",
+ args: {
+ command: { name: "tsfile mkdir" },
+ p1: {
+ value: undefined,
+ arg: " /mach_kernel",
+ status: "INCOMPLETE",
+ message: "'/mach_kernel' already exists"
+ }
+ }
+ }
+ },
+ {
+ skipIf: true,
+ setup: "tsfile rm /",
+ check: {
+ input: "tsfile rm /",
+ hints: "",
+ markup: "VVVVVVVVVVV",
+ cursor: 11,
+ current: "p1",
+ status: "VALID",
+ message: "",
+ args: {
+ command: { name: "tsfile rm" },
+ p1: {
+ value: "/",
+ arg: " /",
+ status: "VALID",
+ message: ""
+ }
+ }
+ }
+ },
+ {
+ skipIf: true,
+ setup: "tsfile rm /zxcv",
+ check: {
+ input: "tsfile rm /zxcv",
+ // hints: ' -> /etc/',
+ markup: "VVVVVVVVVVIIIII",
+ cursor: 15,
+ current: "p1",
+ status: "ERROR",
+ message: "'/zxcv' doesn't exist",
+ args: {
+ command: { name: "tsfile rm" },
+ p1: {
+ value: undefined,
+ arg: " /zxcv",
+ status: "INCOMPLETE",
+ message: "'/zxcv' doesn't exist"
+ }
+ }
+ }
+ },
+ {
+ skipIf: !local,
+ setup: "tsfile rm /mach_kernel",
+ check: {
+ input: "tsfile rm /mach_kernel",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVVVVVVVVV",
+ cursor: 22,
+ current: "p1",
+ status: "VALID",
+ message: "",
+ args: {
+ command: { name: "tsfile rm" },
+ p1: {
+ value: "/mach_kernel",
+ arg: " /mach_kernel",
+ status: "VALID",
+ message: ""
+ }
+ }
+ }
+ }
+ ]);
+};
diff --git a/devtools/client/commandline/test/browser_gcli_fileparser.js b/devtools/client/commandline/test/browser_gcli_fileparser.js
new file mode 100644
index 000000000..3b1c76ad5
--- /dev/null
+++ b/devtools/client/commandline/test/browser_gcli_fileparser.js
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2012, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+"use strict";
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
+
+const exports = {};
+
+function test() {
+ helpers.runTestModule(exports, "browser_gcli_fileparser.js");
+}
+
+// var assert = require('../testharness/assert');
+var fileparser = require("gcli/util/fileparser");
+
+var local = false;
+
+exports.testGetPredictor = function (options) {
+ if (!options.isNode || !local) {
+ assert.log("Skipping tests due to install differences.");
+ return;
+ }
+
+ var opts = { filetype: "file", existing: "yes" };
+ var predictor = fileparser.getPredictor("/usr/locl/bin/nmp", opts);
+ return predictor().then(function (replies) {
+ assert.is(replies[0].name,
+ "/usr/local/bin/npm",
+ "predict npm");
+ });
+};
diff --git a/devtools/client/commandline/test/browser_gcli_filesystem.js b/devtools/client/commandline/test/browser_gcli_filesystem.js
new file mode 100644
index 000000000..19087b74e
--- /dev/null
+++ b/devtools/client/commandline/test/browser_gcli_filesystem.js
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2012, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+"use strict";
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
+
+const exports = {};
+
+function test() {
+ helpers.runTestModule(exports, "browser_gcli_filesystem.js");
+}
+
+// var assert = require('../testharness/assert');
+// var helpers = require('./helpers');
+var filesystem = require("gcli/util/filesystem");
+
+exports.testSplit = function (options) {
+ if (!options.isNode) {
+ return;
+ }
+
+ helpers.arrayIs(filesystem.split("", "/"),
+ [ "." ],
+ "split <blank>");
+
+ helpers.arrayIs(filesystem.split("a", "/"),
+ [ "a" ],
+ "split a");
+
+ helpers.arrayIs(filesystem.split("a/b/c", "/"),
+ [ "a", "b", "c" ],
+ "split a/b/c");
+
+ helpers.arrayIs(filesystem.split("/a/b/c/", "/"),
+ [ "a", "b", "c" ],
+ "split a/b/c");
+
+ helpers.arrayIs(filesystem.split("/a/b///c/", "/"),
+ [ "a", "b", "c" ],
+ "split a/b/c");
+};
+
+exports.testJoin = function (options) {
+ if (!options.isNode) {
+ return;
+ }
+
+ assert.is(filesystem.join("usr", "local", "bin"),
+ "usr/local/bin",
+ "join to usr/local/bin");
+};
diff --git a/devtools/client/commandline/test/browser_gcli_focus.js b/devtools/client/commandline/test/browser_gcli_focus.js
new file mode 100644
index 000000000..3229a6a19
--- /dev/null
+++ b/devtools/client/commandline/test/browser_gcli_focus.js
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2012, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+"use strict";
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
+
+const exports = {};
+
+function test() {
+ helpers.runTestModule(exports, "browser_gcli_focus.js");
+}
+
+// var helpers = require('./helpers');
+
+exports.testBasic = function (options) {
+ return helpers.audit(options, [
+ {
+ name: "exec setup",
+ setup: function () {
+ // Just check that we've got focus, and everything is clear
+ helpers.focusInput(options);
+ return helpers.setInput(options, "echo hi");
+ },
+ check: { },
+ exec: { }
+ },
+ {
+ setup: "tsn deep",
+ check: {
+ input: "tsn deep",
+ hints: " down nested cmd",
+ markup: "IIIVIIII",
+ cursor: 8,
+ status: "ERROR",
+ outputState: "false:default",
+ tooltipState: "false:default"
+ }
+ },
+ {
+ setup: "tsn deep<TAB>",
+ check: {
+ input: "tsn deep down nested cmd ",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVVVVVVVVVVVV",
+ cursor: 25,
+ status: "VALID",
+ outputState: "false:default",
+ tooltipState: "false:default"
+ }
+ }
+ ]);
+};
diff --git a/devtools/client/commandline/test/browser_gcli_history.js b/devtools/client/commandline/test/browser_gcli_history.js
new file mode 100644
index 000000000..e01426031
--- /dev/null
+++ b/devtools/client/commandline/test/browser_gcli_history.js
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2012, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+"use strict";
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
+
+const exports = {};
+
+function test() {
+ helpers.runTestModule(exports, "browser_gcli_history.js");
+}
+
+// var assert = require('../testharness/assert');
+var History = require("gcli/ui/history").History;
+
+exports.testSimpleHistory = function (options) {
+ var history = new History({});
+ history.add("foo");
+ history.add("bar");
+ assert.is(history.backward(), "bar");
+ assert.is(history.backward(), "foo");
+
+ // Adding to the history again moves us back to the start of the history.
+ history.add("quux");
+ assert.is(history.backward(), "quux");
+ assert.is(history.backward(), "bar");
+ assert.is(history.backward(), "foo");
+};
+
+exports.testBackwardsPastIndex = function (options) {
+ var history = new History({});
+ history.add("foo");
+ history.add("bar");
+ assert.is(history.backward(), "bar");
+ assert.is(history.backward(), "foo");
+
+ // Moving backwards past recorded history just keeps giving you the last
+ // item.
+ assert.is(history.backward(), "foo");
+};
+
+exports.testForwardsPastIndex = function (options) {
+ var history = new History({});
+ history.add("foo");
+ history.add("bar");
+ assert.is(history.backward(), "bar");
+ assert.is(history.backward(), "foo");
+
+ // Going forward through the history again.
+ assert.is(history.forward(), "bar");
+
+ // 'Present' time.
+ assert.is(history.forward(), "");
+
+ // Going to the 'future' just keeps giving us the empty string.
+ assert.is(history.forward(), "");
+};
diff --git a/devtools/client/commandline/test/browser_gcli_incomplete.js b/devtools/client/commandline/test/browser_gcli_incomplete.js
new file mode 100644
index 000000000..38424b745
--- /dev/null
+++ b/devtools/client/commandline/test/browser_gcli_incomplete.js
@@ -0,0 +1,439 @@
+/*
+ * Copyright 2012, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+"use strict";
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
+
+const exports = {};
+
+function test() {
+ helpers.runTestModule(exports, "browser_gcli_incomplete.js");
+}
+
+// var assert = require('../testharness/assert');
+// var helpers = require('./helpers');
+
+exports.testBasic = function (options) {
+ return helpers.audit(options, [
+ {
+ setup: "tsu 2 extra",
+ check: {
+ args: {
+ num: { value: 2, type: "Argument" }
+ }
+ },
+ post: function () {
+ var requisition = options.requisition;
+
+ assert.is(requisition._unassigned.length,
+ 1,
+ "single unassigned: tsu 2 extra");
+ assert.is(requisition._unassigned[0].param.type.isIncompleteName,
+ false,
+ "unassigned.isIncompleteName: tsu 2 extra");
+ }
+ },
+ {
+ setup: "tsu",
+ check: {
+ args: {
+ num: { value: undefined, type: "BlankArgument" }
+ }
+ }
+ },
+ {
+ setup: "tsg",
+ check: {
+ args: {
+ solo: { type: "BlankArgument" },
+ txt1: { type: "BlankArgument" },
+ bool: { type: "BlankArgument" },
+ txt2: { type: "BlankArgument" },
+ num: { type: "BlankArgument" }
+ }
+ }
+ }
+ ]);
+};
+
+exports.testCompleted = function (options) {
+ return helpers.audit(options, [
+ {
+ setup: "tsela<TAB>",
+ check: {
+ args: {
+ command: { name: "tselarr", type: "Argument" },
+ num: { type: "BlankArgument" },
+ arr: { type: "ArrayArgument" }
+ }
+ }
+ },
+ {
+ setup: "tsn dif ",
+ check: {
+ input: "tsn dif ",
+ hints: "<text>",
+ markup: "VVVVVVVV",
+ cursor: 8,
+ status: "ERROR",
+ args: {
+ command: { name: "tsn dif", type: "MergedArgument" },
+ text: { type: "BlankArgument", status: "INCOMPLETE" }
+ }
+ }
+ },
+ {
+ setup: "tsn di<TAB>",
+ check: {
+ input: "tsn dif ",
+ hints: "<text>",
+ markup: "VVVVVVVV",
+ cursor: 8,
+ status: "ERROR",
+ args: {
+ command: { name: "tsn dif", type: "Argument" },
+ text: { type: "BlankArgument", status: "INCOMPLETE" }
+ }
+ }
+ },
+ // The above 2 tests take different routes to 'tsn dif '.
+ // The results should be similar. The difference is in args.command.type.
+ {
+ setup: "tsg -",
+ check: {
+ input: "tsg -",
+ hints: "-txt1 <solo> [options]",
+ markup: "VVVVI",
+ cursor: 5,
+ status: "ERROR",
+ args: {
+ solo: { value: undefined, status: "INCOMPLETE" },
+ txt1: { value: undefined, status: "VALID" },
+ bool: { value: false, status: "VALID" },
+ txt2: { value: undefined, status: "VALID" },
+ num: { value: undefined, status: "VALID" }
+ }
+ }
+ },
+ {
+ setup: "tsg -<TAB>",
+ check: {
+ input: "tsg --txt1 ",
+ hints: "<string> <solo> [options]",
+ markup: "VVVVIIIIIIV",
+ cursor: 11,
+ status: "ERROR",
+ args: {
+ solo: { value: undefined, status: "INCOMPLETE" },
+ txt1: { value: undefined, status: "INCOMPLETE" },
+ bool: { value: false, status: "VALID" },
+ txt2: { value: undefined, status: "VALID" },
+ num: { value: undefined, status: "VALID" }
+ }
+ }
+ },
+ {
+ setup: "tsg --txt1 fred",
+ check: {
+ input: "tsg --txt1 fred",
+ hints: " <solo> [options]",
+ markup: "VVVVVVVVVVVVVVV",
+ status: "ERROR",
+ args: {
+ solo: { value: undefined, status: "INCOMPLETE" },
+ txt1: { value: "fred", status: "VALID" },
+ bool: { value: false, status: "VALID" },
+ txt2: { value: undefined, status: "VALID" },
+ num: { value: undefined, status: "VALID" }
+ }
+ }
+ },
+ {
+ setup: "tscook key value --path path --",
+ check: {
+ input: "tscook key value --path path --",
+ hints: "domain [options]",
+ markup: "VVVVVVVVVVVVVVVVVVVVVVVVVVVVVII",
+ status: "ERROR",
+ args: {
+ key: { value: "key", status: "VALID" },
+ value: { value: "value", status: "VALID" },
+ path: { value: "path", status: "VALID" },
+ domain: { value: undefined, status: "VALID" },
+ secure: { value: false, status: "VALID" }
+ }
+ }
+ },
+ {
+ setup: "tscook key value --path path --domain domain --",
+ check: {
+ input: "tscook key value --path path --domain domain --",
+ hints: "secure [options]",
+ markup: "VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVII",
+ status: "ERROR",
+ args: {
+ key: { value: "key", status: "VALID" },
+ value: { value: "value", status: "VALID" },
+ path: { value: "path", status: "VALID" },
+ domain: { value: "domain", status: "VALID" },
+ secure: { value: false, status: "VALID" }
+ }
+ }
+ }
+ ]);
+
+};
+
+exports.testCase = function (options) {
+ return helpers.audit(options, [
+ {
+ setup: "tsg AA",
+ check: {
+ input: "tsg AA",
+ hints: " [options] -> aaa",
+ markup: "VVVVII",
+ status: "ERROR",
+ args: {
+ solo: { value: undefined, text: "AA", status: "INCOMPLETE" },
+ txt1: { value: undefined, status: "VALID" },
+ bool: { value: false, status: "VALID" },
+ txt2: { value: undefined, status: "VALID" },
+ num: { value: undefined, status: "VALID" }
+ }
+ }
+ }
+ ]);
+};
+
+exports.testIncomplete = function (options) {
+ return helpers.audit(options, [
+ {
+ setup: "tsm a a -",
+ check: {
+ args: {
+ abc: { value: "a", type: "Argument" },
+ txt: { value: "a", type: "Argument" },
+ num: { value: undefined, arg: " -", type: "Argument", status: "INCOMPLETE" }
+ }
+ }
+ },
+ {
+ setup: "tsg -",
+ check: {
+ args: {
+ solo: { type: "BlankArgument" },
+ txt1: { type: "BlankArgument" },
+ bool: { type: "BlankArgument" },
+ txt2: { type: "BlankArgument" },
+ num: { type: "BlankArgument" }
+ }
+ },
+ post: function () {
+ var requisition = options.requisition;
+
+ assert.is(requisition._unassigned[0],
+ requisition.getAssignmentAt(5),
+ "unassigned -");
+ assert.is(requisition._unassigned.length,
+ 1,
+ "single unassigned - tsg -");
+ assert.is(requisition._unassigned[0].param.type.isIncompleteName,
+ true,
+ "unassigned.isIncompleteName: tsg -");
+ }
+ }
+ ]);
+};
+
+exports.testRepeated = function (options) {
+ return helpers.audit(options, [
+ {
+ setup: "tscook key value --path jjj --path kkk",
+ check: {
+ input: "tscook key value --path jjj --path kkk",
+ hints: " [options]",
+ markup: "VVVVVVVVVVVVVVVVVVVVVVVVVVVVEEEEEEVEEE",
+ cursor: 38,
+ current: "__unassigned",
+ status: "ERROR",
+ options: [ ],
+ message: "",
+ predictions: [ ],
+ unassigned: [ " --path", " kkk" ],
+ args: {
+ command: { name: "tscook" },
+ key: {
+ value: "key",
+ arg: " key",
+ status: "VALID",
+ message: ""
+ },
+ value: {
+ value: "value",
+ arg: " value",
+ status: "VALID",
+ message: ""
+ },
+ path: {
+ value: "jjj",
+ arg: " --path jjj",
+ status: "VALID",
+ message: ""
+ },
+ domain: {
+ value: undefined,
+ arg: "",
+ status: "VALID",
+ message: ""
+ },
+ secure: {
+ value: false,
+ arg: "",
+ status: "VALID",
+ message: ""
+ },
+ }
+ }
+ }
+ ]);
+};
+
+exports.testHidden = function (options) {
+ return helpers.audit(options, [
+ {
+ setup: "tshidde",
+ check: {
+ input: "tshidde",
+ hints: " -> tse",
+ status: "ERROR"
+ }
+ },
+ {
+ setup: "tshidden",
+ check: {
+ input: "tshidden",
+ hints: " [options]",
+ markup: "VVVVVVVV",
+ status: "VALID",
+ args: {
+ visible: { value: undefined, status: "VALID" },
+ invisiblestring: { value: undefined, status: "VALID" },
+ invisibleboolean: { value: false, status: "VALID" }
+ }
+ }
+ },
+ {
+ setup: "tshidden --vis",
+ check: {
+ input: "tshidden --vis",
+ hints: "ible [options]",
+ markup: "VVVVVVVVVIIIII",
+ status: "ERROR",
+ args: {
+ visible: { value: undefined, status: "VALID" },
+ invisiblestring: { value: undefined, status: "VALID" },
+ invisibleboolean: { value: false, status: "VALID" }
+ }
+ }
+ },
+ {
+ setup: "tshidden --invisiblestrin",
+ check: {
+ input: "tshidden --invisiblestrin",
+ hints: " [options]",
+ markup: "VVVVVVVVVEEEEEEEEEEEEEEEE",
+ status: "ERROR",
+ args: {
+ visible: { value: undefined, status: "VALID" },
+ invisiblestring: { value: undefined, status: "VALID" },
+ invisibleboolean: { value: false, status: "VALID" }
+ }
+ }
+ },
+ {
+ setup: "tshidden --invisiblestring",
+ check: {
+ input: "tshidden --invisiblestring",
+ hints: " <string> [options]",
+ markup: "VVVVVVVVVIIIIIIIIIIIIIIIII",
+ status: "ERROR",
+ args: {
+ visible: { value: undefined, status: "VALID" },
+ invisiblestring: { value: undefined, status: "INCOMPLETE" },
+ invisibleboolean: { value: false, status: "VALID" }
+ }
+ }
+ },
+ {
+ setup: "tshidden --invisiblestring x",
+ check: {
+ input: "tshidden --invisiblestring x",
+ hints: " [options]",
+ markup: "VVVVVVVVVVVVVVVVVVVVVVVVVVVV",
+ status: "VALID",
+ args: {
+ visible: { value: undefined, status: "VALID" },
+ invisiblestring: { value: "x", status: "VALID" },
+ invisibleboolean: { value: false, status: "VALID" }
+ }
+ }
+ },
+ {
+ setup: "tshidden --invisibleboolea",
+ check: {
+ input: "tshidden --invisibleboolea",
+ hints: " [options]",
+ markup: "VVVVVVVVVEEEEEEEEEEEEEEEEE",
+ status: "ERROR",
+ args: {
+ visible: { value: undefined, status: "VALID" },
+ invisiblestring: { value: undefined, status: "VALID" },
+ invisibleboolean: { value: false, status: "VALID" }
+ }
+ }
+ },
+ {
+ setup: "tshidden --invisibleboolean",
+ check: {
+ input: "tshidden --invisibleboolean",
+ hints: " [options]",
+ markup: "VVVVVVVVVVVVVVVVVVVVVVVVVVV",
+ status: "VALID",
+ args: {
+ visible: { value: undefined, status: "VALID" },
+ invisiblestring: { value: undefined, status: "VALID" },
+ invisibleboolean: { value: true, status: "VALID" }
+ }
+ }
+ },
+ {
+ setup: "tshidden --visible xxx",
+ check: {
+ input: "tshidden --visible xxx",
+ markup: "VVVVVVVVVVVVVVVVVVVVVV",
+ status: "VALID",
+ hints: "",
+ args: {
+ visible: { value: "xxx", status: "VALID" },
+ invisiblestring: { value: undefined, status: "VALID" },
+ invisibleboolean: { value: false, status: "VALID" }
+ }
+ }
+ }
+ ]);
+};
diff --git a/devtools/client/commandline/test/browser_gcli_inputter.js b/devtools/client/commandline/test/browser_gcli_inputter.js
new file mode 100644
index 000000000..3fa3e7806
--- /dev/null
+++ b/devtools/client/commandline/test/browser_gcli_inputter.js
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2012, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+"use strict";
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
+
+const exports = {};
+
+function test() {
+ helpers.runTestModule(exports, "browser_gcli_inputter.js");
+}
+
+// var assert = require('../testharness/assert');
+var KeyEvent = require("gcli/util/util").KeyEvent;
+
+var latestEvent;
+var latestData;
+
+var outputted = function (ev) {
+ latestEvent = ev;
+
+ ev.output.promise.then(function () {
+ latestData = ev.output.data;
+ });
+};
+
+
+exports.setup = function (options) {
+ options.requisition.commandOutputManager.onOutput.add(outputted);
+};
+
+exports.shutdown = function (options) {
+ options.requisition.commandOutputManager.onOutput.remove(outputted);
+};
+
+exports.testOutput = function (options) {
+ latestEvent = undefined;
+ latestData = undefined;
+
+ var terminal = options.terminal;
+ if (!terminal) {
+ assert.log("Skipping testInputter.testOutput due to lack of terminal.");
+ return;
+ }
+
+ var focusManager = terminal.focusManager;
+
+ terminal.setInput("tss");
+
+ var ev0 = { keyCode: KeyEvent.DOM_VK_RETURN };
+ terminal.onKeyDown(ev0);
+
+ assert.is(terminal.getInputState().typed,
+ "tss",
+ "terminal should do nothing on RETURN keyDown");
+ assert.is(latestEvent, undefined, "no events this test");
+ assert.is(latestData, undefined, "no data this test");
+
+ var ev1 = { keyCode: KeyEvent.DOM_VK_RETURN };
+ return terminal.handleKeyUp(ev1).then(function () {
+ assert.ok(latestEvent != null, "events this test");
+ assert.is(latestData.name, "tss", "last command is tss");
+
+ assert.is(terminal.getInputState().typed,
+ "",
+ "terminal should exec on RETURN keyUp");
+
+ assert.ok(focusManager._recentOutput, "recent output happened");
+
+ var ev2 = { keyCode: KeyEvent.DOM_VK_F1 };
+ return terminal.handleKeyUp(ev2).then(function () {
+ assert.ok(!focusManager._recentOutput, "no recent output happened post F1");
+ assert.ok(focusManager._helpRequested, "F1 = help");
+
+ var ev3 = { keyCode: KeyEvent.DOM_VK_ESCAPE };
+ return terminal.handleKeyUp(ev3).then(function () {
+ assert.ok(!focusManager._helpRequested, "ESCAPE = anti help");
+ });
+ });
+
+ });
+};
diff --git a/devtools/client/commandline/test/browser_gcli_intro.js b/devtools/client/commandline/test/browser_gcli_intro.js
new file mode 100644
index 000000000..0fe640d4c
--- /dev/null
+++ b/devtools/client/commandline/test/browser_gcli_intro.js
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2012, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+"use strict";
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
+
+const exports = {};
+
+function test() {
+ helpers.runTestModule(exports, "browser_gcli_intro.js");
+}
+
+// var helpers = require('./helpers');
+
+exports.testIntroStatus = function (options) {
+ return helpers.audit(options, [
+ {
+ skipRemainingIf: function commandIntroMissing() {
+ return options.requisition.system.commands.get("intro") == null;
+ },
+ setup: "intro",
+ check: {
+ typed: "intro",
+ markup: "VVVVV",
+ status: "VALID",
+ hints: ""
+ }
+ },
+ {
+ setup: "intro foo",
+ check: {
+ typed: "intro foo",
+ markup: "VVVVVVEEE",
+ status: "ERROR",
+ hints: ""
+ }
+ },
+ {
+ setup: "intro",
+ check: {
+ typed: "intro",
+ markup: "VVVVV",
+ status: "VALID",
+ hints: ""
+ },
+ exec: {
+ output: [
+ /command\s*line/,
+ /help/,
+ /F1/,
+ /Escape/
+ ]
+ }
+ }
+ ]);
+};
diff --git a/devtools/client/commandline/test/browser_gcli_js.js b/devtools/client/commandline/test/browser_gcli_js.js
new file mode 100644
index 000000000..4d36cedd8
--- /dev/null
+++ b/devtools/client/commandline/test/browser_gcli_js.js
@@ -0,0 +1,570 @@
+/*
+ * Copyright 2012, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+"use strict";
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
+
+const exports = {};
+
+function test() {
+ helpers.runTestModule(exports, "browser_gcli_js.js");
+}
+
+// var assert = require('../testharness/assert');
+// var helpers = require('./helpers');
+
+exports.setup = function (options) {
+ if (jsTestDisallowed(options)) {
+ return;
+ }
+
+ // Check that we're not trespassing on 'donteval'
+ var win = options.requisition.environment.window;
+ Object.defineProperty(win, "donteval", {
+ get: function () {
+ assert.ok(false, "donteval should not be used");
+ console.trace();
+ return { cant: "", touch: "", "this": "" };
+ },
+ enumerable: true,
+ configurable: true
+ });
+};
+
+exports.shutdown = function (options) {
+ if (jsTestDisallowed(options)) {
+ return;
+ }
+
+ delete options.requisition.environment.window.donteval;
+};
+
+function jsTestDisallowed(options) {
+ return options.isRemote || // Altering the environment (which isn't remoted)
+ options.isNode ||
+ options.requisition.system.commands.get("{") == null;
+}
+
+exports.testBasic = function (options) {
+ return helpers.audit(options, [
+ {
+ skipRemainingIf: jsTestDisallowed,
+ setup: "{",
+ check: {
+ input: "{",
+ hints: "",
+ markup: "V",
+ cursor: 1,
+ current: "javascript",
+ status: "ERROR",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "{" },
+ javascript: {
+ value: undefined,
+ arg: "{",
+ status: "INCOMPLETE"
+ }
+ }
+ }
+ },
+ {
+ setup: "{ ",
+ check: {
+ input: "{ ",
+ hints: "",
+ markup: "VV",
+ cursor: 2,
+ current: "javascript",
+ status: "ERROR",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "{" },
+ javascript: {
+ value: undefined,
+ arg: "{ ",
+ status: "INCOMPLETE"
+ }
+ }
+ }
+ },
+ {
+ setup: "{ w",
+ check: {
+ input: "{ w",
+ hints: "indow",
+ markup: "VVI",
+ cursor: 3,
+ current: "javascript",
+ status: "ERROR",
+ predictionsContains: [ "window" ],
+ unassigned: [ ],
+ args: {
+ command: { name: "{" },
+ javascript: {
+ value: "w",
+ arg: "{ w",
+ status: "INCOMPLETE"
+ }
+ }
+ }
+ },
+ {
+ setup: "{ windo",
+ check: {
+ input: "{ windo",
+ hints: "w",
+ markup: "VVIIIII",
+ cursor: 7,
+ current: "javascript",
+ status: "ERROR",
+ predictions: [ "window" ],
+ unassigned: [ ],
+ args: {
+ command: { name: "{" },
+ javascript: {
+ value: "windo",
+ arg: "{ windo",
+ status: "INCOMPLETE"
+ }
+ }
+ }
+ },
+ {
+ setup: "{ window",
+ check: {
+ input: "{ window",
+ hints: "",
+ markup: "VVVVVVVV",
+ cursor: 8,
+ current: "javascript",
+ status: "VALID",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "{" },
+ javascript: {
+ value: "window",
+ arg: "{ window",
+ status: "VALID",
+ message: ""
+ }
+ }
+ }
+ },
+ {
+ setup: "{ window.do",
+ check: {
+ input: "{ window.do",
+ hints: "cument",
+ markup: "VVIIIIIIIII",
+ cursor: 11,
+ current: "javascript",
+ status: "ERROR",
+ predictionsContains: [ "window.document" ],
+ unassigned: [ ],
+ args: {
+ command: { name: "{" },
+ javascript: {
+ value: "window.do",
+ arg: "{ window.do",
+ status: "INCOMPLETE"
+ }
+ }
+ }
+ },
+ {
+ setup: "{ window.document.title",
+ check: {
+ input: "{ window.document.title",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVVVVVVVVVV",
+ cursor: 23,
+ current: "javascript",
+ status: "VALID",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "{" },
+ javascript: {
+ value: "window.document.title",
+ arg: "{ window.document.title",
+ status: "VALID",
+ message: ""
+ }
+ }
+ }
+ }
+ ]);
+};
+
+exports.testDocument = function (options) {
+ return helpers.audit(options, [
+ {
+ skipRemainingIf: jsTestDisallowed,
+ setup: "{ docu",
+ check: {
+ input: "{ docu",
+ hints: "ment",
+ markup: "VVIIII",
+ cursor: 6,
+ current: "javascript",
+ status: "ERROR",
+ predictions: [ "document" ],
+ unassigned: [ ],
+ args: {
+ command: { name: "{" },
+ javascript: {
+ value: "docu",
+ arg: "{ docu",
+ status: "INCOMPLETE"
+ }
+ }
+ }
+ },
+ {
+ setup: "{ docu<TAB>",
+ check: {
+ input: "{ document",
+ hints: "",
+ markup: "VVVVVVVVVV",
+ cursor: 10,
+ current: "javascript",
+ status: "VALID",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "{" },
+ javascript: {
+ value: "document",
+ arg: "{ document",
+ status: "VALID",
+ message: ""
+ }
+ }
+ }
+ },
+ {
+ setup: "{ document.titl",
+ check: {
+ input: "{ document.titl",
+ hints: "e",
+ markup: "VVIIIIIIIIIIIII",
+ cursor: 15,
+ current: "javascript",
+ status: "ERROR",
+ predictions: [ "document.title" ],
+ unassigned: [ ],
+ args: {
+ command: { name: "{" },
+ javascript: {
+ value: "document.titl",
+ arg: "{ document.titl",
+ status: "INCOMPLETE"
+ }
+ }
+ }
+ },
+ {
+ setup: "{ document.titl<TAB>",
+ check: {
+ input: "{ document.title ",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVVVV",
+ cursor: 17,
+ current: "javascript",
+ status: "VALID",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "{" },
+ javascript: {
+ value: "document.title",
+ // arg: '{ document.title ',
+ // Node/JSDom gets this wrong and omits the trailing space. Why?
+ status: "VALID",
+ message: ""
+ }
+ }
+ }
+ },
+ {
+ setup: "{ document.title",
+ check: {
+ input: "{ document.title",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVVV",
+ cursor: 16,
+ current: "javascript",
+ status: "VALID",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "{" },
+ javascript: {
+ value: "document.title",
+ arg: "{ document.title",
+ status: "VALID",
+ message: ""
+ }
+ }
+ }
+ }
+ ]);
+};
+
+exports.testDonteval = function (options) {
+ return helpers.audit(options, [
+ {
+ skipRemainingIf: true, // Commented out until we fix non-enumerable props
+ setup: "{ don",
+ check: {
+ input: "{ don",
+ hints: "teval",
+ markup: "VVIII",
+ cursor: 5,
+ current: "javascript",
+ status: "ERROR",
+ predictions: [ "donteval" ],
+ unassigned: [ ],
+ args: {
+ command: { name: "{" },
+ javascript: {
+ value: "don",
+ arg: "{ don",
+ status: "INCOMPLETE"
+ }
+ }
+ }
+ },
+ {
+ setup: "{ donteval",
+ check: {
+ input: "{ donteval",
+ hints: "",
+ markup: "VVVVVVVVVV",
+ cursor: 10,
+ current: "javascript",
+ status: "VALID",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "{" },
+ javascript: {
+ value: "donteval",
+ arg: "{ donteval",
+ status: "VALID",
+ message: ""
+ }
+ }
+ }
+ },
+ /*
+ // This is a controversial test - technically we can tell that it's an error
+ // because 'donteval.' is a syntax error, however donteval is unsafe so we
+ // are playing safe by bailing out early. It's enough of a corner case that
+ // I don't think it warrants fixing
+ {
+ setup: '{ donteval.',
+ check: {
+ input: '{ donteval.',
+ hints: '',
+ markup: 'VVVVVVVVVVV',
+ cursor: 11,
+ current: 'javascript',
+ status: 'VALID',
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: '{' },
+ javascript: {
+ value: 'donteval.',
+ arg: '{ donteval.',
+ status: 'VALID',
+ message: ''
+ }
+ }
+ }
+ },
+ */
+ {
+ setup: "{ donteval.cant",
+ check: {
+ input: "{ donteval.cant",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVV",
+ cursor: 15,
+ current: "javascript",
+ status: "VALID",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "{" },
+ javascript: {
+ value: "donteval.cant",
+ arg: "{ donteval.cant",
+ status: "VALID",
+ message: ""
+ }
+ }
+ }
+ },
+ {
+ setup: "{ donteval.xxx",
+ check: {
+ input: "{ donteval.xxx",
+ hints: "",
+ markup: "VVVVVVVVVVVVVV",
+ cursor: 14,
+ current: "javascript",
+ status: "VALID",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "{" },
+ javascript: {
+ value: "donteval.xxx",
+ arg: "{ donteval.xxx",
+ status: "VALID",
+ message: ""
+ }
+ }
+ }
+ }
+ ]);
+};
+
+exports.testExec = function (options) {
+ return helpers.audit(options, [
+ {
+ skipRemainingIf: jsTestDisallowed,
+ setup: "{ 1+1",
+ check: {
+ input: "{ 1+1",
+ hints: "",
+ markup: "VVVVV",
+ cursor: 5,
+ current: "javascript",
+ status: "VALID",
+ options: [ ],
+ message: "",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ javascript: {
+ value: "1+1",
+ arg: "{ 1+1",
+ status: "VALID",
+ message: ""
+ }
+ }
+ },
+ exec: {
+ output: "2",
+ type: "number",
+ error: false
+ }
+ },
+ {
+ setup: "{ 1+1 }",
+ check: {
+ input: "{ 1+1 }",
+ hints: "",
+ markup: "VVVVVVV",
+ cursor: 7,
+ current: "javascript",
+ status: "VALID",
+ options: [ ],
+ message: "",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ javascript: {
+ value: "1+1",
+ arg: "{ 1+1 }",
+ status: "VALID",
+ message: ""
+ }
+ }
+ },
+ exec: {
+ output: "2",
+ type: "number",
+ error: false
+ }
+ },
+ {
+ setup: '{ "hello"',
+ check: {
+ input: '{ "hello"',
+ hints: "",
+ markup: "VVVVVVVVV",
+ cursor: 9,
+ current: "javascript",
+ status: "VALID",
+ options: [ ],
+ message: "",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ javascript: {
+ value: '"hello"',
+ arg: '{ "hello"',
+ status: "VALID",
+ message: ""
+ }
+ }
+ },
+ exec: {
+ output: "hello",
+ type: "string",
+ error: false
+ }
+ },
+ {
+ setup: '{ "hello" + 1',
+ check: {
+ input: '{ "hello" + 1',
+ hints: "",
+ markup: "VVVVVVVVVVVVV",
+ cursor: 13,
+ current: "javascript",
+ status: "VALID",
+ options: [ ],
+ message: "",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ javascript: {
+ value: '"hello" + 1',
+ arg: '{ "hello" + 1',
+ status: "VALID",
+ message: ""
+ }
+ }
+ },
+ exec: {
+ output: "hello1",
+ type: "string",
+ error: false
+ }
+ }
+ ]);
+};
diff --git a/devtools/client/commandline/test/browser_gcli_keyboard1.js b/devtools/client/commandline/test/browser_gcli_keyboard1.js
new file mode 100644
index 000000000..a71ac81dd
--- /dev/null
+++ b/devtools/client/commandline/test/browser_gcli_keyboard1.js
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2012, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+"use strict";
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
+
+const exports = {};
+
+function test() {
+ helpers.runTestModule(exports, "browser_gcli_keyboard1.js");
+}
+
+var javascript = require("gcli/types/javascript");
+// var helpers = require('./helpers');
+
+exports.testSimple = function (options) {
+ return helpers.audit(options, [
+ {
+ setup: "tsela<TAB>",
+ check: { input: "tselarr ", cursor: 8 }
+ },
+ {
+ setup: "tsn di<TAB>",
+ check: { input: "tsn dif ", cursor: 8 }
+ },
+ {
+ setup: "tsg a<TAB>",
+ check: { input: "tsg aaa ", cursor: 8 }
+ }
+ ]);
+};
+
+exports.testScript = function (options) {
+ return helpers.audit(options, [
+ {
+ skipRemainingIf: options.isRemote ||
+ options.requisition.system.commands.get("{") == null,
+ setup: "{ wind<TAB>",
+ check: { input: "{ window" }
+ },
+ {
+ setup: "{ window.docum<TAB>",
+ check: { input: "{ window.document" }
+ }
+ ]);
+};
+
+exports.testJsdom = function (options) {
+ return helpers.audit(options, [
+ {
+ skipIf: options.isRemote ||
+ options.requisition.system.commands.get("{") == null,
+ setup: "{ window.document.titl<TAB>",
+ check: { input: "{ window.document.title " }
+ }
+ ]);
+};
diff --git a/devtools/client/commandline/test/browser_gcli_keyboard2.js b/devtools/client/commandline/test/browser_gcli_keyboard2.js
new file mode 100644
index 000000000..80bb42867
--- /dev/null
+++ b/devtools/client/commandline/test/browser_gcli_keyboard2.js
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2012, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+"use strict";
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
+
+const exports = {};
+
+function test() {
+ helpers.runTestModule(exports, "browser_gcli_keyboard2.js");
+}
+
+// var helpers = require('./helpers');
+
+exports.testIncr = function (options) {
+ return helpers.audit(options, [
+ /*
+ // We currently refuse to increment/decrement things with a non-valid
+ // status which makes sense for many cases, and is a decent default.
+ // However in theory we could do better, these tests are there for then
+ {
+ setup: 'tsu -70<UP>',
+ check: { input: 'tsu -5' }
+ },
+ {
+ setup: 'tsu -7<UP>',
+ check: { input: 'tsu -5' }
+ },
+ {
+ setup: 'tsu -6<UP>',
+ check: { input: 'tsu -5' }
+ },
+ */
+ {
+ setup: "tsu -5<UP>",
+ check: { input: "tsu -3" }
+ },
+ {
+ setup: "tsu -4<UP>",
+ check: { input: "tsu -3" }
+ },
+ {
+ setup: "tsu -3<UP>",
+ check: { input: "tsu 0" }
+ },
+ {
+ setup: "tsu -2<UP>",
+ check: { input: "tsu 0" }
+ },
+ {
+ setup: "tsu -1<UP>",
+ check: { input: "tsu 0" }
+ },
+ {
+ setup: "tsu 0<UP>",
+ check: { input: "tsu 3" }
+ },
+ {
+ setup: "tsu 1<UP>",
+ check: { input: "tsu 3" }
+ },
+ {
+ setup: "tsu 2<UP>",
+ check: { input: "tsu 3" }
+ },
+ {
+ setup: "tsu 3<UP>",
+ check: { input: "tsu 6" }
+ },
+ {
+ setup: "tsu 4<UP>",
+ check: { input: "tsu 6" }
+ },
+ {
+ setup: "tsu 5<UP>",
+ check: { input: "tsu 6" }
+ },
+ {
+ setup: "tsu 6<UP>",
+ check: { input: "tsu 9" }
+ },
+ {
+ setup: "tsu 7<UP>",
+ check: { input: "tsu 9" }
+ },
+ {
+ setup: "tsu 8<UP>",
+ check: { input: "tsu 9" }
+ },
+ {
+ setup: "tsu 9<UP>",
+ check: { input: "tsu 10" }
+ },
+ {
+ setup: "tsu 10<UP>",
+ check: { input: "tsu 10" }
+ }
+ /*
+ // See notes above
+ {
+ setup: 'tsu 100<UP>',
+ check: { input: 'tsu 10' }
+ }
+ */
+ ]);
+};
diff --git a/devtools/client/commandline/test/browser_gcli_keyboard3.js b/devtools/client/commandline/test/browser_gcli_keyboard3.js
new file mode 100644
index 000000000..f47eab2a8
--- /dev/null
+++ b/devtools/client/commandline/test/browser_gcli_keyboard3.js
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2012, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+"use strict";
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
+
+const exports = {};
+
+function test() {
+ helpers.runTestModule(exports, "browser_gcli_keyboard3.js");
+}
+
+// var helpers = require('./helpers');
+
+exports.testDecr = function (options) {
+ return helpers.audit(options, [
+ /*
+ // See notes at top of testIncr in testKeyboard2.js
+ {
+ setup: 'tsu -70<DOWN>',
+ check: { input: 'tsu -5' }
+ },
+ {
+ setup: 'tsu -7<DOWN>',
+ check: { input: 'tsu -5' }
+ },
+ {
+ setup: 'tsu -6<DOWN>',
+ check: { input: 'tsu -5' }
+ },
+ */
+ {
+ setup: "tsu -5<DOWN>",
+ check: { input: "tsu -5" }
+ },
+ {
+ setup: "tsu -4<DOWN>",
+ check: { input: "tsu -5" }
+ },
+ {
+ setup: "tsu -3<DOWN>",
+ check: { input: "tsu -5" }
+ },
+ {
+ setup: "tsu -2<DOWN>",
+ check: { input: "tsu -3" }
+ },
+ {
+ setup: "tsu -1<DOWN>",
+ check: { input: "tsu -3" }
+ },
+ {
+ setup: "tsu 0<DOWN>",
+ check: { input: "tsu -3" }
+ },
+ {
+ setup: "tsu 1<DOWN>",
+ check: { input: "tsu 0" }
+ },
+ {
+ setup: "tsu 2<DOWN>",
+ check: { input: "tsu 0" }
+ },
+ {
+ setup: "tsu 3<DOWN>",
+ check: { input: "tsu 0" }
+ },
+ {
+ setup: "tsu 4<DOWN>",
+ check: { input: "tsu 3" }
+ },
+ {
+ setup: "tsu 5<DOWN>",
+ check: { input: "tsu 3" }
+ },
+ {
+ setup: "tsu 6<DOWN>",
+ check: { input: "tsu 3" }
+ },
+ {
+ setup: "tsu 7<DOWN>",
+ check: { input: "tsu 6" }
+ },
+ {
+ setup: "tsu 8<DOWN>",
+ check: { input: "tsu 6" }
+ },
+ {
+ setup: "tsu 9<DOWN>",
+ check: { input: "tsu 6" }
+ },
+ {
+ setup: "tsu 10<DOWN>",
+ check: { input: "tsu 9" }
+ }
+ /*
+ // See notes at top of testIncr
+ {
+ setup: 'tsu 100<DOWN>',
+ check: { input: 'tsu 9' }
+ }
+ */
+ ]);
+};
diff --git a/devtools/client/commandline/test/browser_gcli_keyboard4.js b/devtools/client/commandline/test/browser_gcli_keyboard4.js
new file mode 100644
index 000000000..0ee785a87
--- /dev/null
+++ b/devtools/client/commandline/test/browser_gcli_keyboard4.js
@@ -0,0 +1,189 @@
+/*
+ * Copyright 2012, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+"use strict";
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
+
+const exports = {};
+
+function test() {
+ helpers.runTestModule(exports, "browser_gcli_keyboard4.js");
+}
+
+// var helpers = require('./helpers');
+
+exports.testIncrFloat = function (options) {
+ return helpers.audit(options, [
+ /*
+ // See notes at top of testIncr
+ {
+ setup: 'tsf -70<UP>',
+ check: { input: 'tsf -6.5' }
+ },
+ */
+ {
+ setup: "tsf -6.5<UP>",
+ check: { input: "tsf -6" }
+ },
+ {
+ setup: "tsf -6<UP>",
+ check: { input: "tsf -4.5" }
+ },
+ {
+ setup: "tsf -4.5<UP>",
+ check: { input: "tsf -3" }
+ },
+ {
+ setup: "tsf -4<UP>",
+ check: { input: "tsf -3" }
+ },
+ {
+ setup: "tsf -3<UP>",
+ check: { input: "tsf -1.5" }
+ },
+ {
+ setup: "tsf -1.5<UP>",
+ check: { input: "tsf 0" }
+ },
+ {
+ setup: "tsf 0<UP>",
+ check: { input: "tsf 1.5" }
+ },
+ {
+ setup: "tsf 1.5<UP>",
+ check: { input: "tsf 3" }
+ },
+ {
+ setup: "tsf 2<UP>",
+ check: { input: "tsf 3" }
+ },
+ {
+ setup: "tsf 3<UP>",
+ check: { input: "tsf 4.5" }
+ },
+ {
+ setup: "tsf 5<UP>",
+ check: { input: "tsf 6" }
+ }
+ /*
+ // See notes at top of testIncr
+ {
+ setup: 'tsf 100<UP>',
+ check: { input: 'tsf -6.5' }
+ }
+ */
+ ]);
+};
+
+exports.testDecrFloat = function (options) {
+ return helpers.audit(options, [
+ /*
+ // See notes at top of testIncr
+ {
+ setup: 'tsf -70<DOWN>',
+ check: { input: 'tsf 11.5' }
+ },
+ */
+ {
+ setup: "tsf -6.5<DOWN>",
+ check: { input: "tsf -6.5" }
+ },
+ {
+ setup: "tsf -6<DOWN>",
+ check: { input: "tsf -6.5" }
+ },
+ {
+ setup: "tsf -4.5<DOWN>",
+ check: { input: "tsf -6" }
+ },
+ {
+ setup: "tsf -4<DOWN>",
+ check: { input: "tsf -4.5" }
+ },
+ {
+ setup: "tsf -3<DOWN>",
+ check: { input: "tsf -4.5" }
+ },
+ {
+ setup: "tsf -1.5<DOWN>",
+ check: { input: "tsf -3" }
+ },
+ {
+ setup: "tsf 0<DOWN>",
+ check: { input: "tsf -1.5" }
+ },
+ {
+ setup: "tsf 1.5<DOWN>",
+ check: { input: "tsf 0" }
+ },
+ {
+ setup: "tsf 2<DOWN>",
+ check: { input: "tsf 1.5" }
+ },
+ {
+ setup: "tsf 3<DOWN>",
+ check: { input: "tsf 1.5" }
+ },
+ {
+ setup: "tsf 5<DOWN>",
+ check: { input: "tsf 4.5" }
+ }
+ /*
+ // See notes at top of testIncr
+ {
+ setup: 'tsf 100<DOWN>',
+ check: { input: 'tsf 11.5' }
+ }
+ */
+ ]);
+};
+
+exports.testIncrSelection = function (options) {
+ /*
+ // Bug 829516: GCLI up/down navigation over selection is sometimes bizarre
+ return helpers.audit(options, [
+ {
+ setup: 'tselarr <DOWN>',
+ check: { hints: '2' },
+ exec: {}
+ },
+ {
+ setup: 'tselarr <DOWN><DOWN>',
+ check: { hints: '3' },
+ exec: {}
+ },
+ {
+ setup: 'tselarr <DOWN><DOWN><DOWN>',
+ check: { hints: '1' },
+ exec: {}
+ }
+ ]);
+ */
+};
+
+exports.testDecrSelection = function (options) {
+ /*
+ // Bug 829516: GCLI up/down navigation over selection is sometimes bizarre
+ return helpers.audit(options, [
+ {
+ setup: 'tselarr <UP>',
+ check: { hints: '3' }
+ }
+ ]);
+ */
+};
diff --git a/devtools/client/commandline/test/browser_gcli_keyboard5.js b/devtools/client/commandline/test/browser_gcli_keyboard5.js
new file mode 100644
index 000000000..0c2356519
--- /dev/null
+++ b/devtools/client/commandline/test/browser_gcli_keyboard5.js
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2012, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+"use strict";
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
+
+const exports = {};
+
+function test() {
+ helpers.runTestModule(exports, "browser_gcli_keyboard5.js");
+}
+
+// var helpers = require('./helpers');
+
+exports.testCompleteDown = function (options) {
+ return helpers.audit(options, [
+ {
+ setup: "tsn e<DOWN><DOWN><DOWN><DOWN><DOWN><TAB>",
+ check: { input: "tsn exte " }
+ },
+ {
+ setup: "tsn e<DOWN><DOWN><DOWN><DOWN><TAB>",
+ check: { input: "tsn ext " }
+ },
+ {
+ setup: "tsn e<DOWN><DOWN><DOWN><TAB>",
+ check: { input: "tsn extend " }
+ },
+ {
+ setup: "tsn e<DOWN><DOWN><TAB>",
+ check: { input: "tsn exten " }
+ },
+ {
+ setup: "tsn e<DOWN><TAB>",
+ check: { input: "tsn exte " }
+ },
+ {
+ setup: "tsn e<TAB>",
+ check: { input: "tsn ext " }
+ }
+ ]);
+};
diff --git a/devtools/client/commandline/test/browser_gcli_keyboard6.js b/devtools/client/commandline/test/browser_gcli_keyboard6.js
new file mode 100644
index 000000000..f3217f8e0
--- /dev/null
+++ b/devtools/client/commandline/test/browser_gcli_keyboard6.js
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2012, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+"use strict";
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
+
+const exports = {};
+
+function test() {
+ helpers.runTestModule(exports, "browser_gcli_keyboard6.js");
+}
+
+// var helpers = require('./helpers');
+
+exports.testCompleteUp = function (options) {
+ return helpers.audit(options, [
+ {
+ setup: "tsn e<UP><TAB>",
+ check: { input: "tsn extend " }
+ },
+ {
+ setup: "tsn e<UP><UP><TAB>",
+ check: { input: "tsn exten " }
+ },
+ {
+ setup: "tsn e<UP><UP><UP><TAB>",
+ check: { input: "tsn exte " }
+ },
+ {
+ setup: "tsn e<UP><UP><UP><UP><TAB>",
+ check: { input: "tsn ext " }
+ },
+ {
+ setup: "tsn e<UP><UP><UP><UP><UP><TAB>",
+ check: { input: "tsn extend " }
+ },
+ {
+ setup: "tsn e<UP><UP><UP><UP><UP><UP><TAB>",
+ check: { input: "tsn exten " }
+ },
+ {
+ setup: "tsn e<UP><UP><UP><UP><UP><UP><UP><TAB>",
+ check: { input: "tsn exte " }
+ },
+ {
+ setup: "tsn e<UP><UP><UP><UP><UP><UP><UP><UP><TAB>",
+ check: { input: "tsn ext " }
+ }
+ ]);
+};
diff --git a/devtools/client/commandline/test/browser_gcli_menu.js b/devtools/client/commandline/test/browser_gcli_menu.js
new file mode 100644
index 000000000..59a4ec0ec
--- /dev/null
+++ b/devtools/client/commandline/test/browser_gcli_menu.js
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2012, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+"use strict";
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
+
+const exports = {};
+
+function test() {
+ helpers.runTestModule(exports, "browser_gcli_menu.js");
+}
+
+// var helpers = require('./helpers');
+
+exports.testOptions = function (options) {
+ return helpers.audit(options, [
+ {
+ setup: "tslong",
+ check: {
+ input: "tslong",
+ markup: "VVVVVV",
+ status: "ERROR",
+ hints: " <msg> [options]",
+ args: {
+ msg: { value: undefined, status: "INCOMPLETE" },
+ num: { value: undefined, status: "VALID" },
+ sel: { value: undefined, status: "VALID" },
+ bool: { value: false, status: "VALID" },
+ bool2: { value: false, status: "VALID" },
+ sel2: { value: undefined, status: "VALID" },
+ num2: { value: undefined, status: "VALID" }
+ }
+ }
+ }
+ ]);
+};
diff --git a/devtools/client/commandline/test/browser_gcli_node.js b/devtools/client/commandline/test/browser_gcli_node.js
new file mode 100644
index 000000000..3fdf1ef6d
--- /dev/null
+++ b/devtools/client/commandline/test/browser_gcli_node.js
@@ -0,0 +1,317 @@
+/*
+ * Copyright 2012, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+"use strict";
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
+
+const exports = {};
+
+function test() {
+ helpers.runTestModule(exports, "browser_gcli_node.js");
+}
+
+// var assert = require('../testharness/assert');
+// var helpers = require('./helpers');
+
+exports.testNode = function (options) {
+ return helpers.audit(options, [
+ {
+ setup: "tse ",
+ check: {
+ input: "tse ",
+ hints: "<node> [options]",
+ markup: "VVVV",
+ cursor: 4,
+ current: "node",
+ status: "ERROR",
+ args: {
+ command: { name: "tse" },
+ node: { status: "INCOMPLETE" },
+ nodes: { status: "VALID" },
+ nodes2: { status: "VALID" }
+ }
+ }
+ },
+ {
+ setup: "tse :",
+ check: {
+ input: "tse :",
+ hints: " [options]",
+ markup: "VVVVE",
+ cursor: 5,
+ current: "node",
+ status: "ERROR",
+ args: {
+ command: { name: "tse" },
+ node: {
+ arg: " :",
+ status: "ERROR",
+ message: "Syntax error in CSS query"
+ },
+ nodes: { status: "VALID" },
+ nodes2: { status: "VALID" }
+ }
+ }
+ },
+ {
+ setup: "tse #",
+ check: {
+ input: "tse #",
+ hints: " [options]",
+ markup: "VVVVE",
+ cursor: 5,
+ current: "node",
+ status: "ERROR",
+ args: {
+ command: { name: "tse" },
+ node: {
+ value: undefined,
+ arg: " #",
+ status: "ERROR",
+ message: "Syntax error in CSS query"
+ },
+ nodes: { status: "VALID" },
+ nodes2: { status: "VALID" }
+ }
+ }
+ },
+ {
+ setup: "tse .",
+ check: {
+ input: "tse .",
+ hints: " [options]",
+ markup: "VVVVE",
+ cursor: 5,
+ current: "node",
+ status: "ERROR",
+ args: {
+ command: { name: "tse" },
+ node: {
+ value: undefined,
+ arg: " .",
+ status: "ERROR",
+ message: "Syntax error in CSS query"
+ },
+ nodes: { status: "VALID" },
+ nodes2: { status: "VALID" }
+ }
+ }
+ },
+ {
+ setup: "tse *",
+ check: {
+ input: "tse *",
+ hints: " [options]",
+ markup: "VVVVE",
+ cursor: 5,
+ current: "node",
+ status: "ERROR",
+ args: {
+ command: { name: "tse" },
+ node: {
+ value: undefined,
+ arg: " *",
+ status: "ERROR"
+ // message: 'Too many matches (128)'
+ },
+ nodes: { status: "VALID" },
+ nodes2: { status: "VALID" }
+ }
+ }
+ }
+ ]);
+};
+
+exports.testNodeDom = function (options) {
+ return helpers.audit(options, [
+ {
+ setup: "tse :root",
+ check: {
+ input: "tse :root",
+ hints: " [options]",
+ markup: "VVVVVVVVV",
+ cursor: 9,
+ current: "node",
+ status: "VALID",
+ args: {
+ command: { name: "tse" },
+ node: { arg: " :root", status: "VALID" },
+ nodes: { status: "VALID" },
+ nodes2: { status: "VALID" }
+ }
+ }
+ },
+ {
+ setup: "tse :root ",
+ check: {
+ input: "tse :root ",
+ hints: "[options]",
+ markup: "VVVVVVVVVV",
+ cursor: 10,
+ current: "node",
+ status: "VALID",
+ args: {
+ command: { name: "tse" },
+ node: { arg: " :root ", status: "VALID" },
+ nodes: { status: "VALID" },
+ nodes2: { status: "VALID" }
+ }
+ },
+ exec: {
+ },
+ post: function (output) {
+ if (!options.isRemote) {
+ assert.is(output.args.node.tagName, "HTML", ":root tagName");
+ }
+ }
+ },
+ {
+ setup: "tse #gcli-nomatch",
+ check: {
+ input: "tse #gcli-nomatch",
+ hints: " [options]",
+ markup: "VVVVIIIIIIIIIIIII",
+ cursor: 17,
+ current: "node",
+ status: "ERROR",
+ args: {
+ command: { name: "tse" },
+ node: {
+ value: undefined,
+ arg: " #gcli-nomatch",
+ status: "INCOMPLETE",
+ message: "No matches"
+ },
+ nodes: { status: "VALID" },
+ nodes2: { status: "VALID" }
+ }
+ }
+ }
+ ]);
+};
+
+exports.testNodes = function (options) {
+ return helpers.audit(options, [
+ {
+ setup: "tse :root --nodes *",
+ check: {
+ input: "tse :root --nodes *",
+ hints: " [options]",
+ markup: "VVVVVVVVVVVVVVVVVVV",
+ current: "nodes",
+ status: "VALID",
+ args: {
+ command: { name: "tse" },
+ node: { arg: " :root", status: "VALID" },
+ nodes: { arg: " --nodes *", status: "VALID" },
+ nodes2: { status: "VALID" }
+ }
+ },
+ exec: {
+ },
+ post: function (output) {
+ if (!options.isRemote) {
+ assert.is(output.args.node.tagName, "HTML", ":root tagName");
+ assert.ok(output.args.nodes.length > 3, "nodes length");
+ assert.is(output.args.nodes2.length, 0, "nodes2 length");
+ }
+
+ assert.is(output.data.args.node, ":root", "node data");
+ assert.is(output.data.args.nodes, "*", "nodes data");
+ assert.is(output.data.args.nodes2, "Error", "nodes2 data");
+ }
+ },
+ {
+ setup: "tse :root --nodes2 div",
+ check: {
+ input: "tse :root --nodes2 div",
+ hints: " [options]",
+ markup: "VVVVVVVVVVVVVVVVVVVVVV",
+ cursor: 22,
+ current: "nodes2",
+ status: "VALID",
+ args: {
+ command: { name: "tse" },
+ node: { arg: " :root", status: "VALID" },
+ nodes: { status: "VALID" },
+ nodes2: { arg: " --nodes2 div", status: "VALID" }
+ }
+ },
+ exec: {
+ },
+ post: function (output) {
+ if (!options.isRemote) {
+ assert.is(output.args.node.tagName, "HTML", ":root tagName");
+ assert.is(output.args.nodes.length, 0, "nodes length");
+ assert.is(output.args.nodes2.item(0).tagName, "DIV", "div tagName");
+ }
+
+ assert.is(output.data.args.node, ":root", "node data");
+ assert.is(output.data.args.nodes, "Error", "nodes data");
+ assert.is(output.data.args.nodes2, "div", "nodes2 data");
+ }
+ },
+ {
+ setup: "tse --nodes ffff",
+ check: {
+ input: "tse --nodes ffff",
+ hints: " <node> [options]",
+ markup: "VVVVIIIIIIIVIIII",
+ cursor: 16,
+ current: "nodes",
+ status: "ERROR",
+ args: {
+ command: { name: "tse" },
+ node: {
+ value: undefined,
+ arg: "",
+ status: "INCOMPLETE"
+ },
+ nodes: {
+ value: undefined,
+ arg: " --nodes ffff",
+ status: "INCOMPLETE",
+ message: "No matches"
+ },
+ nodes2: { arg: "", status: "VALID", message: "" }
+ }
+ }
+ },
+ {
+ setup: "tse --nodes2 ffff",
+ check: {
+ input: "tse --nodes2 ffff",
+ hints: " <node> [options]",
+ markup: "VVVVVVVVVVVVVVVVV",
+ cursor: 17,
+ current: "nodes2",
+ status: "ERROR",
+ args: {
+ command: { name: "tse" },
+ node: {
+ value: undefined,
+ arg: "",
+ status: "INCOMPLETE"
+ },
+ nodes: { arg: "", status: "VALID", message: "" },
+ nodes2: { arg: " --nodes2 ffff", status: "VALID", message: "" }
+ }
+ }
+ },
+ ]);
+};
diff --git a/devtools/client/commandline/test/browser_gcli_pref1.js b/devtools/client/commandline/test/browser_gcli_pref1.js
new file mode 100644
index 000000000..d4788c610
--- /dev/null
+++ b/devtools/client/commandline/test/browser_gcli_pref1.js
@@ -0,0 +1,166 @@
+/*
+ * Copyright 2012, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+"use strict";
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
+
+const exports = {};
+
+function test() {
+ helpers.runTestModule(exports, "browser_gcli_pref1.js");
+}
+
+// var helpers = require('./helpers');
+
+exports.testPrefShowStatus = function (options) {
+ return helpers.audit(options, [
+ {
+ skipRemainingIf: options.requisition.system.commands.get("pref") == null,
+ setup: "pref s",
+ check: {
+ typed: "pref s",
+ hints: "et",
+ markup: "IIIIVI",
+ status: "ERROR"
+ }
+ },
+ {
+ setup: "pref show",
+ check: {
+ typed: "pref show",
+ hints: " <setting>",
+ markup: "VVVVVVVVV",
+ status: "ERROR"
+ }
+ },
+ {
+ setup: "pref show ",
+ check: {
+ typed: "pref show ",
+ hints: "eagerHelper",
+ markup: "VVVVVVVVVV",
+ status: "ERROR"
+ }
+ },
+ {
+ setup: "pref show tempTBo",
+ check: {
+ typed: "pref show tempTBo",
+ hints: "ol",
+ markup: "VVVVVVVVVVIIIIIII",
+ status: "ERROR"
+ }
+ },
+ {
+ setup: "pref show tempTBool",
+ check: {
+ typed: "pref show tempTBool",
+ markup: "VVVVVVVVVVVVVVVVVVV",
+ status: "VALID",
+ hints: ""
+ }
+ },
+ {
+ setup: "pref show tempTBool 4",
+ check: {
+ typed: "pref show tempTBool 4",
+ markup: "VVVVVVVVVVVVVVVVVVVVE",
+ status: "ERROR",
+ hints: ""
+ }
+ },
+ {
+ setup: "pref show tempNumber 4",
+ check: {
+ typed: "pref show tempNumber 4",
+ markup: "VVVVVVVVVVVVVVVVVVVVVE",
+ status: "ERROR",
+ hints: ""
+ }
+ }
+ ]);
+};
+
+exports.testPrefSetStatus = function (options) {
+ return helpers.audit(options, [
+ {
+ skipRemainingIf: options.requisition.system.commands.get("pref") == null,
+ setup: "pref s",
+ check: {
+ typed: "pref s",
+ hints: "et",
+ markup: "IIIIVI",
+ status: "ERROR"
+ }
+ },
+ {
+ setup: "pref set",
+ check: {
+ typed: "pref set",
+ hints: " <setting> <value>",
+ markup: "VVVVVVVV",
+ status: "ERROR"
+ }
+ },
+ {
+ setup: "pref xxx",
+ check: {
+ typed: "pref xxx",
+ markup: "IIIIVIII",
+ status: "ERROR"
+ }
+ },
+ {
+ setup: "pref set ",
+ check: {
+ typed: "pref set ",
+ hints: "eagerHelper <value>",
+ markup: "VVVVVVVVV",
+ status: "ERROR"
+ }
+ },
+ {
+ setup: "pref set tempTBo",
+ check: {
+ typed: "pref set tempTBo",
+ hints: "ol <value>",
+ markup: "VVVVVVVVVIIIIIII",
+ status: "ERROR"
+ }
+ },
+ {
+ skipIf: options.isRemote,
+ setup: "pref set tempTBool 4",
+ check: {
+ typed: "pref set tempTBool 4",
+ markup: "VVVVVVVVVVVVVVVVVVVE",
+ status: "ERROR",
+ hints: ""
+ }
+ },
+ {
+ setup: "pref set tempNumber 4",
+ check: {
+ typed: "pref set tempNumber 4",
+ markup: "VVVVVVVVVVVVVVVVVVVVV",
+ status: "VALID",
+ hints: ""
+ }
+ }
+ ]);
+};
diff --git a/devtools/client/commandline/test/browser_gcli_pref2.js b/devtools/client/commandline/test/browser_gcli_pref2.js
new file mode 100644
index 000000000..3bbfc5e79
--- /dev/null
+++ b/devtools/client/commandline/test/browser_gcli_pref2.js
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2012, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+"use strict";
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
+
+const exports = {};
+
+function test() {
+ helpers.runTestModule(exports, "browser_gcli_pref2.js");
+}
+
+// var assert = require('../testharness/assert');
+// var helpers = require('./helpers');
+var mockSettings = require("./mockSettings");
+
+exports.testPrefExec = function (options) {
+ if (options.requisition.system.commands.get("pref") == null) {
+ assert.log("Skipping test; missing pref command.");
+ return;
+ }
+
+ if (options.isRemote) {
+ assert.log("Skipping test which assumes local settings.");
+ return;
+ }
+
+ assert.is(mockSettings.tempNumber.value, 42, "set to 42");
+
+ return helpers.audit(options, [
+ {
+ // Delegated remote types can't transfer value types so we only test for
+ // the value of 'value' when we're local
+ skipIf: options.isRemote,
+ setup: "pref set tempNumber 4",
+ check: {
+ setting: { value: mockSettings.tempNumber },
+ args: { value: { value: 4 } }
+ }
+ },
+ {
+ setup: "pref set tempNumber 4",
+ check: {
+ input: "pref set tempNumber 4",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVVVVVVVV",
+ cursor: 21,
+ current: "value",
+ status: "VALID",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "pref set" },
+ setting: {
+ arg: " tempNumber",
+ status: "VALID",
+ message: ""
+ },
+ value: {
+ arg: " 4",
+ status: "VALID",
+ message: ""
+ }
+ }
+ },
+ exec: {
+ output: ""
+ },
+ post: function () {
+ assert.is(mockSettings.tempNumber.value, 4, "set to 4");
+ }
+ },
+ {
+ setup: "pref reset tempNumber",
+ check: {
+ args: {
+ command: { name: "pref reset" },
+ setting: { value: mockSettings.tempNumber }
+ }
+ },
+ exec: {
+ output: ""
+ },
+ post: function () {
+ assert.is(mockSettings.tempNumber.value, 42, "reset to 42");
+ }
+ },
+ {
+ skipRemainingIf: function commandPrefListMissing() {
+ return options.requisition.system.commands.get("pref list") == null;
+ },
+ setup: "pref list tempNum",
+ check: {
+ args: {
+ command: { name: "pref list" },
+ search: { value: "tempNum" }
+ }
+ },
+ exec: {
+ output: /tempNum/
+ }
+ },
+ ]);
+};
diff --git a/devtools/client/commandline/test/browser_gcli_remotews.js b/devtools/client/commandline/test/browser_gcli_remotews.js
new file mode 100644
index 000000000..a4c77b858
--- /dev/null
+++ b/devtools/client/commandline/test/browser_gcli_remotews.js
@@ -0,0 +1,485 @@
+/*
+ * Copyright 2012, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+"use strict";
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
+
+const exports = {};
+
+function test() {
+ helpers.runTestModule(exports, "browser_gcli_remotews.js");
+}
+
+// var assert = require('../testharness/assert');
+// var helpers = require('./helpers');
+
+// testRemoteWs and testRemoteXhr are virtually identical.
+// Changes made here should be made there too.
+// They are kept separate to save adding complexity to the test system and so
+// to help us select the test that are available in different environments
+
+exports.testRemoteWebsocket = function (options) {
+ return helpers.audit(options, [
+ {
+ skipRemainingIf: options.isRemote || options.isNode || options.isFirefox,
+ setup: "remote ",
+ check: {
+ input: "remote ",
+ hints: "",
+ markup: "EEEEEEV",
+ cursor: 7,
+ current: "__command",
+ status: "ERROR",
+ options: [ ],
+ message: "Can't use 'remote'.",
+ predictions: [ ],
+ unassigned: [ ],
+ }
+ },
+ {
+ setup: "connect remote",
+ check: {
+ args: {
+ prefix: { value: "remote" },
+ url: { value: undefined }
+ }
+ },
+ exec: {
+ error: false
+ }
+ },
+ {
+ setup: "disconnect remote",
+ check: {
+ args: {
+ prefix: {
+ value: function (front) {
+ assert.is(front.prefix, "remote", "disconnecting remote");
+ }
+ }
+ }
+ },
+ exec: {
+ output: /^Removed [0-9]* commands.$/,
+ type: "string",
+ error: false
+ }
+ },
+ {
+ setup: "connect remote --method websocket",
+ check: {
+ args: {
+ prefix: { value: "remote" },
+ url: { value: undefined }
+ }
+ },
+ exec: {
+ error: false
+ }
+ },
+ {
+ setup: "disconnect remote",
+ check: {
+ args: {
+ prefix: {
+ value: function (front) {
+ assert.is(front.prefix, "remote", "disconnecting remote");
+ }
+ }
+ }
+ },
+ exec: {
+ output: /^Removed [0-9]* commands.$/,
+ type: "string",
+ error: false
+ }
+ },
+ {
+ setup: "connect remote --method websocket",
+ check: {
+ args: {
+ prefix: { value: "remote" },
+ url: { value: undefined }
+ }
+ },
+ exec: {
+ output: /^Added [0-9]* commands.$/,
+ type: "string",
+ error: false
+ }
+ },
+ {
+ setup: "remote ",
+ check: {
+ input: "remote ",
+ // PhantomJS fails on this. Unsure why
+ // hints: ' {',
+ markup: "IIIIIIV",
+ status: "ERROR",
+ optionsIncludes: [
+ "remote", "remote cd", "remote context", "remote echo",
+ "remote exec", "remote exit", "remote firefox", "remote help",
+ "remote intro", "remote make"
+ ],
+ message: "",
+ predictionsIncludes: [ "remote" ],
+ unassigned: [ ],
+ }
+ },
+ {
+ setup: "remote echo hello world",
+ check: {
+ input: "remote echo hello world",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVVVVVVVVVV",
+ cursor: 23,
+ current: "message",
+ status: "VALID",
+ options: [ ],
+ message: "",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "remote echo" },
+ message: {
+ value: "hello world",
+ arg: " hello world",
+ status: "VALID",
+ message: ""
+ }
+ }
+ },
+ exec: {
+ output: "hello world",
+ type: "string",
+ error: false
+ }
+ },
+ {
+ setup: "remote exec ls",
+ check: {
+ input: "remote exec ls",
+ hints: "",
+ markup: "VVVVVVVVVVVVVV",
+ cursor: 14,
+ current: "command",
+ status: "VALID",
+ options: [ ],
+ message: "",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: {
+ value: "ls",
+ arg: " ls",
+ status: "VALID",
+ message: ""
+ }
+ }
+ },
+ exec: {
+ // output: '', We can't rely on the contents of the FS
+ type: "output",
+ error: false
+ }
+ },
+ {
+ setup: "remote sleep mistake",
+ check: {
+ input: "remote sleep mistake",
+ hints: "",
+ markup: "VVVVVVVVVVVVVEEEEEEE",
+ cursor: 20,
+ current: "length",
+ status: "ERROR",
+ options: [ ],
+ message: 'Can\'t convert "mistake" to a number.',
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "remote sleep" },
+ length: {
+ value: undefined,
+ arg: " mistake",
+ status: "ERROR",
+ message: 'Can\'t convert "mistake" to a number.'
+ }
+ }
+ }
+ },
+ {
+ setup: "remote sleep 1",
+ check: {
+ input: "remote sleep 1",
+ hints: "",
+ markup: "VVVVVVVVVVVVVV",
+ cursor: 14,
+ current: "length",
+ status: "VALID",
+ options: [ ],
+ message: "",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "remote sleep" },
+ length: { value: 1, arg: " 1", status: "VALID", message: "" }
+ }
+ },
+ exec: {
+ output: "Done",
+ type: "string",
+ error: false
+ }
+ },
+ {
+ setup: "remote help ",
+ skipIf: true, // The help command is not remotable
+ check: {
+ input: "remote help ",
+ hints: "[search]",
+ markup: "VVVVVVVVVVVV",
+ cursor: 12,
+ current: "search",
+ status: "VALID",
+ options: [ ],
+ message: "",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "remote help" },
+ search: {
+ value: undefined,
+ arg: "",
+ status: "VALID",
+ message: ""
+ }
+ }
+ },
+ exec: {
+ output: "",
+ type: "string",
+ error: false
+ }
+ },
+ {
+ setup: "remote intro",
+ check: {
+ input: "remote intro",
+ hints: "",
+ markup: "VVVVVVVVVVVV",
+ cursor: 12,
+ current: "__command",
+ status: "VALID",
+ options: [ ],
+ message: "",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "remote intro" }
+ }
+ },
+ exec: {
+ output: [
+ /GCLI is an experiment/,
+ /F1\/Escape/
+ ],
+ type: "intro",
+ error: false
+ }
+ },
+ {
+ setup: "context remote",
+ check: {
+ input: "context remote",
+ // hints: ' {',
+ markup: "VVVVVVVVVVVVVV",
+ cursor: 14,
+ current: "prefix",
+ status: "VALID",
+ optionsContains: [
+ "remote", "remote cd", "remote echo", "remote exec", "remote exit",
+ "remote firefox", "remote help", "remote intro", "remote make"
+ ],
+ message: "",
+ // predictionsContains: [
+ // 'remote', 'remote cd', 'remote echo', 'remote exec', 'remote exit',
+ // 'remote firefox', 'remote help', 'remote intro', 'remote make',
+ // 'remote pref'
+ // ],
+ unassigned: [ ],
+ args: {
+ command: { name: "context" },
+ prefix: {
+ arg: " remote",
+ status: "VALID",
+ message: ""
+ }
+ }
+ },
+ exec: {
+ output: "Using remote as a command prefix",
+ type: "string",
+ error: false
+ }
+ },
+ {
+ setup: "exec ls",
+ check: {
+ input: "exec ls",
+ hints: "",
+ markup: "VVVVVVV",
+ cursor: 7,
+ current: "command",
+ status: "VALID",
+ options: [ ],
+ message: "",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { value: "ls", arg: " ls", status: "VALID", message: "" },
+ }
+ },
+ exec: {
+ // output: '', We can't rely on the contents of the filesystem
+ type: "output",
+ error: false
+ }
+ },
+ {
+ setup: "echo hello world",
+ check: {
+ input: "echo hello world",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVVV",
+ cursor: 16,
+ current: "message",
+ status: "VALID",
+ options: [ ],
+ message: "",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "remote echo" },
+ message: {
+ value: "hello world",
+ arg: " hello world",
+ status: "VALID",
+ message: ""
+ }
+ }
+ },
+ exec: {
+ output: /^hello world$/,
+ type: "string",
+ error: false
+ }
+ },
+ {
+ setup: "context",
+ check: {
+ input: "context",
+ hints: " [prefix]",
+ markup: "VVVVVVV",
+ cursor: 7,
+ current: "__command",
+ status: "VALID",
+ optionsContains: [
+ "remote", "remote cd", "remote echo", "remote exec", "remote exit",
+ "remote firefox", "remote help", "remote intro", "remote make"
+ ],
+ message: "",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "context" },
+ prefix: { value: undefined, arg: "", status: "VALID", message: "" }
+ }
+ },
+ exec: {
+ output: "Command prefix is unset",
+ type: "string",
+ error: false
+ }
+ },
+ {
+ setup: "disconnect ",
+ check: {
+ input: "disconnect ",
+ hints: "remote",
+ markup: "VVVVVVVVVVV",
+ cursor: 11,
+ current: "prefix",
+ status: "ERROR",
+ options: [ "remote" ],
+ message: "",
+ predictions: [ "remote" ],
+ unassigned: [ ],
+ args: {
+ command: { name: "disconnect" },
+ prefix: {
+ value: undefined,
+ arg: "",
+ status: "INCOMPLETE",
+ message: "Value required for 'prefix'."
+ }
+ }
+ }
+ },
+ {
+ setup: "disconnect remote",
+ check: {
+ input: "disconnect remote",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVVVV",
+ status: "VALID",
+ message: "",
+ unassigned: [ ],
+ args: {
+ prefix: {
+ value: function (front) {
+ assert.is(front.prefix, "remote", "disconnecting remote");
+ },
+ arg: " remote",
+ status: "VALID",
+ message: ""
+ }
+ }
+ },
+ exec: {
+ output: /^Removed [0-9]* commands.$/,
+ type: "string",
+ error: false
+ }
+ },
+ {
+ setup: "remote ",
+ check: {
+ input: "remote ",
+ hints: "",
+ markup: "EEEEEEV",
+ cursor: 7,
+ current: "__command",
+ status: "ERROR",
+ options: [ ],
+ message: "Can't use 'remote'.",
+ predictions: [ ],
+ unassigned: [ ],
+ }
+ }
+ ]);
+};
diff --git a/devtools/client/commandline/test/browser_gcli_remotexhr.js b/devtools/client/commandline/test/browser_gcli_remotexhr.js
new file mode 100644
index 000000000..69054d2e2
--- /dev/null
+++ b/devtools/client/commandline/test/browser_gcli_remotexhr.js
@@ -0,0 +1,485 @@
+/*
+ * Copyright 2012, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+"use strict";
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
+
+const exports = {};
+
+function test() {
+ helpers.runTestModule(exports, "browser_gcli_remotexhr.js");
+}
+
+// var assert = require('../testharness/assert');
+// var helpers = require('./helpers');
+
+// testRemoteWs and testRemoteXhr are virtually identical.
+// Changes made here should be made there too.
+// They are kept separate to save adding complexity to the test system and so
+// to help us select the test that are available in different environments
+
+exports.testRemoteXhr = function (options) {
+ return helpers.audit(options, [
+ {
+ skipRemainingIf: options.isRemote || options.isNode || options.isFirefox,
+ setup: "remote ",
+ check: {
+ input: "remote ",
+ hints: "",
+ markup: "EEEEEEV",
+ cursor: 7,
+ current: "__command",
+ status: "ERROR",
+ options: [ ],
+ message: "Can't use 'remote'.",
+ predictions: [ ],
+ unassigned: [ ],
+ }
+ },
+ {
+ setup: "connect remote",
+ check: {
+ args: {
+ prefix: { value: "remote" },
+ url: { value: undefined }
+ }
+ },
+ exec: {
+ error: false
+ }
+ },
+ {
+ setup: "disconnect remote",
+ check: {
+ args: {
+ prefix: {
+ value: function (front) {
+ assert.is(front.prefix, "remote", "disconnecting remote");
+ }
+ }
+ }
+ },
+ exec: {
+ output: /^Removed [0-9]* commands.$/,
+ type: "string",
+ error: false
+ }
+ },
+ {
+ setup: "connect remote --method xhr",
+ check: {
+ args: {
+ prefix: { value: "remote" },
+ url: { value: undefined }
+ }
+ },
+ exec: {
+ error: false
+ }
+ },
+ {
+ setup: "disconnect remote",
+ check: {
+ args: {
+ prefix: {
+ value: function (front) {
+ assert.is(front.prefix, "remote", "disconnecting remote");
+ }
+ }
+ }
+ },
+ exec: {
+ output: /^Removed [0-9]* commands.$/,
+ type: "string",
+ error: false
+ }
+ },
+ {
+ setup: "connect remote --method xhr",
+ check: {
+ args: {
+ prefix: { value: "remote" },
+ url: { value: undefined }
+ }
+ },
+ exec: {
+ output: /^Added [0-9]* commands.$/,
+ type: "string",
+ error: false
+ }
+ },
+ {
+ setup: "remote ",
+ check: {
+ input: "remote ",
+ // PhantomJS fails on this. Unsure why
+ // hints: ' {',
+ markup: "IIIIIIV",
+ status: "ERROR",
+ optionsIncludes: [
+ "remote", "remote cd", "remote context", "remote echo",
+ "remote exec", "remote exit", "remote firefox", "remote help",
+ "remote intro", "remote make"
+ ],
+ message: "",
+ predictionsIncludes: [ "remote" ],
+ unassigned: [ ],
+ }
+ },
+ {
+ setup: "remote echo hello world",
+ check: {
+ input: "remote echo hello world",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVVVVVVVVVV",
+ cursor: 23,
+ current: "message",
+ status: "VALID",
+ options: [ ],
+ message: "",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "remote echo" },
+ message: {
+ value: "hello world",
+ arg: " hello world",
+ status: "VALID",
+ message: ""
+ }
+ }
+ },
+ exec: {
+ output: "hello world",
+ type: "string",
+ error: false
+ }
+ },
+ {
+ setup: "remote exec ls",
+ check: {
+ input: "remote exec ls",
+ hints: "",
+ markup: "VVVVVVVVVVVVVV",
+ cursor: 14,
+ current: "command",
+ status: "VALID",
+ options: [ ],
+ message: "",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: {
+ value: "ls",
+ arg: " ls",
+ status: "VALID",
+ message: ""
+ }
+ }
+ },
+ exec: {
+ // output: '', We can't rely on the contents of the FS
+ type: "output",
+ error: false
+ }
+ },
+ {
+ setup: "remote sleep mistake",
+ check: {
+ input: "remote sleep mistake",
+ hints: "",
+ markup: "VVVVVVVVVVVVVEEEEEEE",
+ cursor: 20,
+ current: "length",
+ status: "ERROR",
+ options: [ ],
+ message: 'Can\'t convert "mistake" to a number.',
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "remote sleep" },
+ length: {
+ value: undefined,
+ arg: " mistake",
+ status: "ERROR",
+ message: 'Can\'t convert "mistake" to a number.'
+ }
+ }
+ }
+ },
+ {
+ setup: "remote sleep 1",
+ check: {
+ input: "remote sleep 1",
+ hints: "",
+ markup: "VVVVVVVVVVVVVV",
+ cursor: 14,
+ current: "length",
+ status: "VALID",
+ options: [ ],
+ message: "",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "remote sleep" },
+ length: { value: 1, arg: " 1", status: "VALID", message: "" }
+ }
+ },
+ exec: {
+ output: "Done",
+ type: "string",
+ error: false
+ }
+ },
+ {
+ setup: "remote help ",
+ skipIf: true, // The help command is not remotable
+ check: {
+ input: "remote help ",
+ hints: "[search]",
+ markup: "VVVVVVVVVVVV",
+ cursor: 12,
+ current: "search",
+ status: "VALID",
+ options: [ ],
+ message: "",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "remote help" },
+ search: {
+ value: undefined,
+ arg: "",
+ status: "VALID",
+ message: ""
+ }
+ }
+ },
+ exec: {
+ output: "",
+ type: "string",
+ error: false
+ }
+ },
+ {
+ setup: "remote intro",
+ check: {
+ input: "remote intro",
+ hints: "",
+ markup: "VVVVVVVVVVVV",
+ cursor: 12,
+ current: "__command",
+ status: "VALID",
+ options: [ ],
+ message: "",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "remote intro" }
+ }
+ },
+ exec: {
+ output: [
+ /GCLI is an experiment/,
+ /F1\/Escape/
+ ],
+ type: "intro",
+ error: false
+ }
+ },
+ {
+ setup: "context remote",
+ check: {
+ input: "context remote",
+ // hints: ' {',
+ markup: "VVVVVVVVVVVVVV",
+ cursor: 14,
+ current: "prefix",
+ status: "VALID",
+ optionsContains: [
+ "remote", "remote cd", "remote echo", "remote exec", "remote exit",
+ "remote firefox", "remote help", "remote intro", "remote make"
+ ],
+ message: "",
+ // predictionsContains: [
+ // 'remote', 'remote cd', 'remote echo', 'remote exec', 'remote exit',
+ // 'remote firefox', 'remote help', 'remote intro', 'remote make',
+ // 'remote pref'
+ // ],
+ unassigned: [ ],
+ args: {
+ command: { name: "context" },
+ prefix: {
+ arg: " remote",
+ status: "VALID",
+ message: ""
+ }
+ }
+ },
+ exec: {
+ output: "Using remote as a command prefix",
+ type: "string",
+ error: false
+ }
+ },
+ {
+ setup: "exec ls",
+ check: {
+ input: "exec ls",
+ hints: "",
+ markup: "VVVVVVV",
+ cursor: 7,
+ current: "command",
+ status: "VALID",
+ options: [ ],
+ message: "",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { value: "ls", arg: " ls", status: "VALID", message: "" },
+ }
+ },
+ exec: {
+ // output: '', We can't rely on the contents of the filesystem
+ type: "output",
+ error: false
+ }
+ },
+ {
+ setup: "echo hello world",
+ check: {
+ input: "echo hello world",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVVV",
+ cursor: 16,
+ current: "message",
+ status: "VALID",
+ options: [ ],
+ message: "",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "remote echo" },
+ message: {
+ value: "hello world",
+ arg: " hello world",
+ status: "VALID",
+ message: ""
+ }
+ }
+ },
+ exec: {
+ output: /^hello world$/,
+ type: "string",
+ error: false
+ }
+ },
+ {
+ setup: "context",
+ check: {
+ input: "context",
+ hints: " [prefix]",
+ markup: "VVVVVVV",
+ cursor: 7,
+ current: "__command",
+ status: "VALID",
+ optionsContains: [
+ "remote", "remote cd", "remote echo", "remote exec", "remote exit",
+ "remote firefox", "remote help", "remote intro", "remote make"
+ ],
+ message: "",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "context" },
+ prefix: { value: undefined, arg: "", status: "VALID", message: "" }
+ }
+ },
+ exec: {
+ output: "Command prefix is unset",
+ type: "string",
+ error: false
+ }
+ },
+ {
+ setup: "disconnect ",
+ check: {
+ input: "disconnect ",
+ hints: "remote",
+ markup: "VVVVVVVVVVV",
+ cursor: 11,
+ current: "prefix",
+ status: "ERROR",
+ options: [ "remote" ],
+ message: "",
+ predictions: [ "remote" ],
+ unassigned: [ ],
+ args: {
+ command: { name: "disconnect" },
+ prefix: {
+ value: undefined,
+ arg: "",
+ status: "INCOMPLETE",
+ message: "Value required for 'prefix'."
+ }
+ }
+ }
+ },
+ {
+ setup: "disconnect remote",
+ check: {
+ input: "disconnect remote",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVVVV",
+ status: "VALID",
+ message: "",
+ unassigned: [ ],
+ args: {
+ prefix: {
+ value: function (front) {
+ assert.is(front.prefix, "remote", "disconnecting remote");
+ },
+ arg: " remote",
+ status: "VALID",
+ message: ""
+ }
+ }
+ },
+ exec: {
+ output: /^Removed [0-9]* commands.$/,
+ type: "string",
+ error: false
+ }
+ },
+ {
+ setup: "remote ",
+ check: {
+ input: "remote ",
+ hints: "",
+ markup: "EEEEEEV",
+ cursor: 7,
+ current: "__command",
+ status: "ERROR",
+ options: [ ],
+ message: "Can't use 'remote'.",
+ predictions: [ ],
+ unassigned: [ ],
+ }
+ }
+ ]);
+};
diff --git a/devtools/client/commandline/test/browser_gcli_resource.js b/devtools/client/commandline/test/browser_gcli_resource.js
new file mode 100644
index 000000000..8ce846bd3
--- /dev/null
+++ b/devtools/client/commandline/test/browser_gcli_resource.js
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2012, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+"use strict";
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
+
+const exports = {};
+
+function test() {
+ helpers.runTestModule(exports, "browser_gcli_resource.js");
+}
+
+// var helpers = require('./helpers');
+// var assert = require('../testharness/assert');
+
+var util = require("gcli/util/util");
+var resource = require("gcli/types/resource");
+var Status = require("gcli/types/types").Status;
+
+exports.testCommand = function (options) {
+ return helpers.audit(options, [
+ {
+ setup: "tsres ",
+ check: {
+ predictionsContains: [ "inline-css" ],
+ }
+ }
+ ]);
+};
+
+exports.testAllPredictions1 = function (options) {
+ if (options.isRemote) {
+ assert.log("Can't directly test remote types locally.");
+ return;
+ }
+
+ var context = options.requisition.conversionContext;
+ var resource = options.requisition.system.types.createType("resource");
+ return resource.getLookup(context).then(function (opts) {
+ assert.ok(opts.length > 1, "have all resources");
+
+ return util.promiseEach(opts, function (prediction) {
+ return checkPrediction(resource, prediction, context);
+ });
+ });
+};
+
+exports.testScriptPredictions = function (options) {
+ if (options.isRemote || options.isNode) {
+ assert.log("Can't directly test remote types locally.");
+ return;
+ }
+
+ var context = options.requisition.conversionContext;
+ var types = options.requisition.system.types;
+ var resource = types.createType({ name: "resource", include: "text/javascript" });
+ return resource.getLookup(context).then(function (opts) {
+ assert.ok(opts.length > 1, "have js resources");
+
+ return util.promiseEach(opts, function (prediction) {
+ return checkPrediction(resource, prediction, context);
+ });
+ });
+};
+
+exports.testStylePredictions = function (options) {
+ if (options.isRemote) {
+ assert.log("Can't directly test remote types locally.");
+ return;
+ }
+
+ var context = options.requisition.conversionContext;
+ var types = options.requisition.system.types;
+ var resource = types.createType({ name: "resource", include: "text/css" });
+ return resource.getLookup(context).then(function (opts) {
+ assert.ok(opts.length >= 1, "have css resources");
+
+ return util.promiseEach(opts, function (prediction) {
+ return checkPrediction(resource, prediction, context);
+ });
+ });
+};
+
+exports.testAllPredictions2 = function (options) {
+ if (options.isRemote) {
+ assert.log("Can't directly test remote types locally.");
+ return;
+ }
+
+ var context = options.requisition.conversionContext;
+ var types = options.requisition.system.types;
+
+ var scriptRes = types.createType({ name: "resource", include: "text/javascript" });
+ return scriptRes.getLookup(context).then(function (scriptOptions) {
+ var styleRes = types.createType({ name: "resource", include: "text/css" });
+ return styleRes.getLookup(context).then(function (styleOptions) {
+ var allRes = types.createType({ name: "resource" });
+ return allRes.getLookup(context).then(function (allOptions) {
+ assert.is(scriptOptions.length + styleOptions.length,
+ allOptions.length,
+ "split");
+ });
+ });
+ });
+};
+
+exports.testAllPredictions3 = function (options) {
+ if (options.isRemote) {
+ assert.log("Can't directly test remote types locally.");
+ return;
+ }
+
+ var context = options.requisition.conversionContext;
+ var types = options.requisition.system.types;
+ var res1 = types.createType({ name: "resource" });
+ return res1.getLookup(context).then(function (options1) {
+ var res2 = types.createType("resource");
+ return res2.getLookup(context).then(function (options2) {
+ assert.is(options1.length, options2.length, "type spec");
+ });
+ });
+};
+
+function checkPrediction(res, prediction, context) {
+ var name = prediction.name;
+ var value = prediction.value;
+
+ return res.parseString(name, context).then(function (conversion) {
+ assert.is(conversion.getStatus(), Status.VALID, "status VALID for " + name);
+ assert.is(conversion.value, value, "value for " + name);
+
+ assert.is(typeof value.loadContents, "function", "resource for " + name);
+ assert.is(typeof value.element, "object", "resource for " + name);
+
+ return Promise.resolve(res.stringify(value, context)).then(function (strung) {
+ assert.is(strung, name, "stringify for " + name);
+ });
+ });
+}
diff --git a/devtools/client/commandline/test/browser_gcli_short.js b/devtools/client/commandline/test/browser_gcli_short.js
new file mode 100644
index 000000000..3322ec0af
--- /dev/null
+++ b/devtools/client/commandline/test/browser_gcli_short.js
@@ -0,0 +1,248 @@
+/*
+ * Copyright 2012, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+"use strict";
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
+
+const exports = {};
+
+function test() {
+ helpers.runTestModule(exports, "browser_gcli_short.js");
+}
+
+// var helpers = require('./helpers');
+
+exports.testBasic = function (options) {
+ return helpers.audit(options, [
+ {
+ setup: "tshidden -v",
+ check: {
+ input: "tshidden -v",
+ hints: " <string>",
+ markup: "VVVVVVVVVII",
+ cursor: 11,
+ current: "visible",
+ status: "ERROR",
+ options: [ ],
+ message: "",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "tshidden" },
+ visible: {
+ value: undefined,
+ arg: " -v",
+ status: "INCOMPLETE"
+ },
+ invisiblestring: {
+ value: undefined,
+ arg: "",
+ status: "VALID",
+ message: ""
+ },
+ invisibleboolean: {
+ value: false,
+ arg: "",
+ status: "VALID",
+ message: ""
+ }
+ }
+ }
+ },
+ {
+ setup: "tshidden -v v",
+ check: {
+ input: "tshidden -v v",
+ hints: "",
+ markup: "VVVVVVVVVVVVV",
+ cursor: 13,
+ current: "visible",
+ status: "VALID",
+ options: [ ],
+ message: "",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "tshidden" },
+ visible: {
+ value: "v",
+ arg: " -v v",
+ status: "VALID",
+ message: ""
+ },
+ invisiblestring: {
+ value: undefined,
+ arg: "",
+ status: "VALID",
+ message: ""
+ },
+ invisibleboolean: {
+ value: false,
+ arg: "",
+ status: "VALID",
+ message: ""
+ }
+ }
+ }
+ },
+ {
+ setup: "tshidden -i i",
+ check: {
+ input: "tshidden -i i",
+ hints: " [options]",
+ markup: "VVVVVVVVVVVVV",
+ cursor: 13,
+ current: "invisiblestring",
+ status: "VALID",
+ options: [ ],
+ message: "",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "tshidden" },
+ visible: {
+ value: undefined,
+ arg: "",
+ status: "VALID",
+ message: ""
+ },
+ invisiblestring: {
+ value: "i",
+ arg: " -i i",
+ status: "VALID",
+ message: ""
+ },
+ invisibleboolean: {
+ value: false,
+ arg: "",
+ status: "VALID",
+ message: ""
+ }
+ }
+ }
+ },
+ {
+ setup: "tshidden -b",
+ check: {
+ input: "tshidden -b",
+ hints: " [options]",
+ markup: "VVVVVVVVVVV",
+ cursor: 11,
+ current: "invisibleboolean",
+ status: "VALID",
+ options: [ ],
+ message: "",
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: "tshidden" },
+ visible: {
+ value: undefined,
+ arg: "",
+ status: "VALID",
+ message: ""
+ },
+ invisiblestring: {
+ value: undefined,
+ arg: "",
+ status: "VALID",
+ message: ""
+ },
+ invisibleboolean: {
+ value: true,
+ arg: " -b",
+ status: "VALID",
+ message: ""
+ }
+ }
+ }
+ },
+ {
+ setup: "tshidden -j",
+ check: {
+ input: "tshidden -j",
+ hints: " [options]",
+ markup: "VVVVVVVVVEE",
+ cursor: 11,
+ current: "__unassigned",
+ status: "ERROR",
+ options: [ ],
+ message: "Can't use '-j'.",
+ predictions: [ ],
+ unassigned: [ " -j" ],
+ args: {
+ command: { name: "tshidden" },
+ visible: {
+ value: undefined,
+ arg: "",
+ status: "VALID",
+ message: ""
+ },
+ invisiblestring: {
+ value: undefined,
+ arg: "",
+ status: "VALID",
+ message: ""
+ },
+ invisibleboolean: {
+ value: false,
+ arg: "",
+ status: "VALID",
+ message: ""
+ }
+ }
+ }
+ },
+ {
+ setup: "tshidden -v jj -b --",
+ check: {
+ input: "tshidden -v jj -b --",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVVVVVEE",
+ cursor: 20,
+ current: "__unassigned",
+ status: "ERROR",
+ options: [ ],
+ message: "Can't use '--'.",
+ predictions: [ ],
+ unassigned: [ " --" ],
+ args: {
+ command: { name: "tshidden" },
+ visible: {
+ value: "jj",
+ arg: " -v jj",
+ status: "VALID",
+ message: ""
+ },
+ invisiblestring: {
+ value: undefined,
+ arg: "",
+ status: "VALID",
+ message: ""
+ },
+ invisibleboolean: {
+ value: true,
+ arg: " -b",
+ status: "VALID",
+ message: ""
+ }
+ }
+ }
+ }
+ ]);
+};
diff --git a/devtools/client/commandline/test/browser_gcli_spell.js b/devtools/client/commandline/test/browser_gcli_spell.js
new file mode 100644
index 000000000..0448ed39b
--- /dev/null
+++ b/devtools/client/commandline/test/browser_gcli_spell.js
@@ -0,0 +1,72 @@
+/*
+ * Copyright (c) 2009 Panagiotis Astithas
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+ * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+ * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+ * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+"use strict";
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
+
+const exports = {};
+
+function test() {
+ helpers.runTestModule(exports, "browser_gcli_spell.js");
+}
+
+// var assert = require('../testharness/assert');
+var spell = require("gcli/util/spell");
+
+exports.testSpellerSimple = function (options) {
+ var alternatives = [
+ "window", "document", "InstallTrigger", "requirejs", "require", "define",
+ "console", "location", "constructor", "getInterface", "external", "sidebar"
+ ];
+
+ assert.is(spell.correct("document", alternatives), "document");
+ assert.is(spell.correct("documen", alternatives), "document");
+ assert.is(spell.correct("ocument", alternatives), "document");
+ assert.is(spell.correct("odcument", alternatives), "document");
+
+ assert.is(spell.correct("=========", alternatives), undefined);
+};
+
+exports.testRank = function (options) {
+ var distances = spell.rank("fred", [ "banana", "fred", "ed", "red", "FRED" ]);
+
+ assert.is(distances.length, 5, "rank length");
+
+ assert.is(distances[0].name, "fred", "fred name #0");
+ assert.is(distances[1].name, "FRED", "FRED name #1");
+ assert.is(distances[2].name, "red", "red name #2");
+ assert.is(distances[3].name, "ed", "ed name #3");
+ assert.is(distances[4].name, "banana", "banana name #4");
+
+ assert.is(distances[0].dist, 0, "fred dist 0");
+ assert.is(distances[1].dist, 4, "FRED dist 4");
+ assert.is(distances[2].dist, 10, "red dist 10");
+ assert.is(distances[3].dist, 20, "ed dist 20");
+ assert.is(distances[4].dist, 100, "banana dist 100");
+};
+
+exports.testRank2 = function (options) {
+ var distances = spell.rank("caps", [ "CAPS", "false" ]);
+ assert.is(JSON.stringify(distances),
+ '[{"name":"CAPS","dist":4},{"name":"false","dist":50}]',
+ 'spell.rank("caps", [ "CAPS", "false" ]');
+};
+
+exports.testDistancePrefix = function (options) {
+ assert.is(spell.distancePrefix("fred", "freddy"), 0, "distancePrefix fred");
+ assert.is(spell.distancePrefix("FRED", "freddy"), 4, "distancePrefix FRED");
+};
diff --git a/devtools/client/commandline/test/browser_gcli_split.js b/devtools/client/commandline/test/browser_gcli_split.js
new file mode 100644
index 000000000..5939f9952
--- /dev/null
+++ b/devtools/client/commandline/test/browser_gcli_split.js
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2012, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+"use strict";
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
+
+const exports = {};
+
+function test() {
+ helpers.runTestModule(exports, "browser_gcli_split.js");
+}
+
+// var assert = require('../testharness/assert');
+
+var cli = require("gcli/cli");
+
+exports.testSplitSimple = function (options) {
+ var args = cli.tokenize("s");
+ options.requisition._split(args);
+ assert.is(args.length, 0);
+ assert.is(options.requisition.commandAssignment.arg.text, "s");
+};
+
+exports.testFlatCommand = function (options) {
+ var args = cli.tokenize("tsv");
+ options.requisition._split(args);
+ assert.is(args.length, 0);
+ assert.is(options.requisition.commandAssignment.value.name, "tsv");
+
+ args = cli.tokenize("tsv a b");
+ options.requisition._split(args);
+ assert.is(options.requisition.commandAssignment.value.name, "tsv");
+ assert.is(args.length, 2);
+ assert.is(args[0].text, "a");
+ assert.is(args[1].text, "b");
+};
+
+exports.testJavascript = function (options) {
+ if (!options.requisition.system.commands.get("{")) {
+ assert.log("Skipping testJavascript because { is not registered");
+ return;
+ }
+
+ var args = cli.tokenize("{");
+ options.requisition._split(args);
+ assert.is(args.length, 1);
+ assert.is(args[0].text, "");
+ assert.is(options.requisition.commandAssignment.arg.text, "");
+ assert.is(options.requisition.commandAssignment.value.name, "{");
+};
+
+// BUG 663081 - add tests for sub commands
diff --git a/devtools/client/commandline/test/browser_gcli_string.js b/devtools/client/commandline/test/browser_gcli_string.js
new file mode 100644
index 000000000..ec964b570
--- /dev/null
+++ b/devtools/client/commandline/test/browser_gcli_string.js
@@ -0,0 +1,270 @@
+/*
+ * Copyright 2012, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+"use strict";
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
+
+const exports = {};
+
+function test() {
+ helpers.runTestModule(exports, "browser_gcli_string.js");
+}
+
+// var helpers = require('./helpers');
+
+exports.testNewLine = function (options) {
+ return helpers.audit(options, [
+ {
+ setup: "echo a\\nb",
+ check: {
+ input: "echo a\\nb",
+ hints: "",
+ markup: "VVVVVVVVV",
+ cursor: 9,
+ current: "message",
+ status: "VALID",
+ args: {
+ command: { name: "echo" },
+ message: {
+ value: "a\nb",
+ arg: " a\\nb",
+ status: "VALID",
+ message: ""
+ }
+ }
+ }
+ }
+ ]);
+};
+
+exports.testTab = function (options) {
+ return helpers.audit(options, [
+ {
+ setup: "echo a\\tb",
+ check: {
+ input: "echo a\\tb",
+ hints: "",
+ markup: "VVVVVVVVV",
+ cursor: 9,
+ current: "message",
+ status: "VALID",
+ args: {
+ command: { name: "echo" },
+ message: {
+ value: "a\tb",
+ arg: " a\\tb",
+ status: "VALID",
+ message: ""
+ }
+ }
+ }
+ }
+ ]);
+};
+
+exports.testEscape = function (options) {
+ return helpers.audit(options, [
+ {
+ // What's typed is actually:
+ // tsrsrsr a\\ b c
+ setup: "tsrsrsr a\\\\ b c",
+ check: {
+ input: "tsrsrsr a\\\\ b c",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVV",
+ status: "VALID",
+ message: "",
+ args: {
+ command: { name: "tsrsrsr" },
+ p1: { value: "a\\", arg: " a\\\\", status: "VALID", message: "" },
+ p2: { value: "b", arg: " b", status: "VALID", message: "" },
+ p3: { value: "c", arg: " c", status: "VALID", message: "" },
+ }
+ }
+ },
+ {
+ // What's typed is actually:
+ // tsrsrsr abc\\ndef asd asd
+ setup: "tsrsrsr abc\\\\ndef asd asd",
+ check: {
+ input: "tsrsrsr abc\\\\ndef asd asd",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVVVVVVVVVVVV",
+ status: "VALID",
+ message: "",
+ args: {
+ command: { name: "tsrsrsr" },
+ p1: {
+ value: "abc\\ndef",
+ arg: " abc\\\\ndef",
+ status: "VALID",
+ message: ""
+ },
+ p2: { value: "asd", arg: " asd", status: "VALID", message: "" },
+ p3: { value: "asd", arg: " asd", status: "VALID", message: "" },
+ }
+ }
+ }
+ ]);
+};
+
+exports.testBlank = function (options) {
+ return helpers.audit(options, [
+ {
+ setup: 'tsrsrsr a "" c',
+ check: {
+ input: 'tsrsrsr a "" c',
+ hints: "",
+ markup: "VVVVVVVVVVVVVV",
+ cursor: 14,
+ current: "p3",
+ status: "ERROR",
+ message: "",
+ args: {
+ command: { name: "tsrsrsr" },
+ p1: {
+ value: "a",
+ arg: " a",
+ status: "VALID",
+ message: ""
+ },
+ p2: {
+ value: undefined,
+ arg: ' ""',
+ status: "INCOMPLETE"
+ },
+ p3: {
+ value: "c",
+ arg: " c",
+ status: "VALID",
+ message: ""
+ }
+ }
+ }
+ },
+ {
+ setup: 'tsrsrsr a b ""',
+ check: {
+ input: 'tsrsrsr a b ""',
+ hints: "",
+ markup: "VVVVVVVVVVVVVV",
+ cursor: 14,
+ current: "p3",
+ status: "VALID",
+ message: "",
+ args: {
+ command: { name: "tsrsrsr" },
+ p1: {
+ value: "a",
+ arg: " a",
+ status:"VALID",
+ message: "" },
+ p2: {
+ value: "b",
+ arg: " b",
+ status: "VALID",
+ message: ""
+ },
+ p3: {
+ value: "",
+ arg: ' ""',
+ status: "VALID",
+ message: ""
+ }
+ }
+ }
+ }
+ ]);
+};
+
+exports.testBlankWithParam = function (options) {
+ return helpers.audit(options, [
+ {
+ setup: "tsrsrsr a --p3",
+ check: {
+ input: "tsrsrsr a --p3",
+ hints: " <string> <p2>",
+ markup: "VVVVVVVVVVVVVVV",
+ cursor: 15,
+ current: "p3",
+ status: "ERROR",
+ message: "",
+ args: {
+ command: { name: "tsrsrsr" },
+ p1: { value: "a", arg: " a", status: "VALID", message: "" },
+ p2: { value: undefined, arg: "", status: "INCOMPLETE" },
+ p3: { value: "", arg: " --p3", status: "VALID", message: "" },
+ }
+ }
+ },
+ {
+ setup: "tsrsrsr a --p3 ",
+ check: {
+ input: "tsrsrsr a --p3 ",
+ hints: "<string> <p2>",
+ markup: "VVVVVVVVVVVVVVVV",
+ cursor: 16,
+ current: "p3",
+ status: "ERROR",
+ message: "",
+ args: {
+ command: { name: "tsrsrsr" },
+ p1: { value: "a", arg: " a", status: "VALID", message: "" },
+ p2: { value: undefined, arg: "", status: "INCOMPLETE" },
+ p3: { value: "", arg: " --p3 ", status: "VALID", message: "" },
+ }
+ }
+ },
+ {
+ setup: 'tsrsrsr a --p3 "',
+ check: {
+ input: 'tsrsrsr a --p3 "',
+ hints: " <p2>",
+ markup: "VVVVVVVVVVVVVVVVV",
+ cursor: 17,
+ current: "p3",
+ status: "ERROR",
+ message: "",
+ args: {
+ command: { name: "tsrsrsr" },
+ p1: { value: "a", arg: " a", status: "VALID", message: "" },
+ p2: { value: undefined, arg: "", status: "INCOMPLETE" },
+ p3: { value: "", arg: ' --p3 "', status: "VALID", message: "" },
+ }
+ }
+ },
+ {
+ setup: 'tsrsrsr a --p3 ""',
+ check: {
+ input: 'tsrsrsr a --p3 ""',
+ hints: " <p2>",
+ markup: "VVVVVVVVVVVVVVVVVV",
+ cursor: 18,
+ current: "p3",
+ status: "ERROR",
+ message: "",
+ args: {
+ command: { name: "tsrsrsr" },
+ p1: { value: "a", arg: " a", status: "VALID", message: "" },
+ p2: { value: undefined, arg: "", status: "INCOMPLETE" },
+ p3: { value: "", arg: ' --p3 ""', status: "VALID", message: "" },
+ }
+ }
+ }
+ ]);
+};
diff --git a/devtools/client/commandline/test/browser_gcli_tokenize.js b/devtools/client/commandline/test/browser_gcli_tokenize.js
new file mode 100644
index 000000000..d66b66e05
--- /dev/null
+++ b/devtools/client/commandline/test/browser_gcli_tokenize.js
@@ -0,0 +1,290 @@
+/*
+ * Copyright 2012, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+"use strict";
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
+
+const exports = {};
+
+function test() {
+ helpers.runTestModule(exports, "browser_gcli_tokenize.js");
+}
+
+// var assert = require('../testharness/assert');
+var cli = require("gcli/cli");
+
+exports.testBlanks = function (options) {
+ var args;
+
+ args = cli.tokenize("");
+ assert.is(args.length, 1);
+ assert.is(args[0].text, "");
+ assert.is(args[0].prefix, "");
+ assert.is(args[0].suffix, "");
+
+ args = cli.tokenize(" ");
+ assert.is(args.length, 1);
+ assert.is(args[0].text, "");
+ assert.is(args[0].prefix, " ");
+ assert.is(args[0].suffix, "");
+};
+
+exports.testTokSimple = function (options) {
+ var args;
+
+ args = cli.tokenize("s");
+ assert.is(args.length, 1);
+ assert.is(args[0].text, "s");
+ assert.is(args[0].prefix, "");
+ assert.is(args[0].suffix, "");
+ assert.is(args[0].type, "Argument");
+
+ args = cli.tokenize("s s");
+ assert.is(args.length, 2);
+ assert.is(args[0].text, "s");
+ assert.is(args[0].prefix, "");
+ assert.is(args[0].suffix, "");
+ assert.is(args[0].type, "Argument");
+ assert.is(args[1].text, "s");
+ assert.is(args[1].prefix, " ");
+ assert.is(args[1].suffix, "");
+ assert.is(args[1].type, "Argument");
+};
+
+exports.testJavascript = function (options) {
+ var args;
+
+ args = cli.tokenize("{x}");
+ assert.is(args.length, 1);
+ assert.is(args[0].text, "x");
+ assert.is(args[0].prefix, "{");
+ assert.is(args[0].suffix, "}");
+ assert.is(args[0].type, "ScriptArgument");
+
+ args = cli.tokenize("{ x }");
+ assert.is(args.length, 1);
+ assert.is(args[0].text, "x");
+ assert.is(args[0].prefix, "{ ");
+ assert.is(args[0].suffix, " }");
+ assert.is(args[0].type, "ScriptArgument");
+
+ args = cli.tokenize("{x} {y}");
+ assert.is(args.length, 2);
+ assert.is(args[0].text, "x");
+ assert.is(args[0].prefix, "{");
+ assert.is(args[0].suffix, "}");
+ assert.is(args[0].type, "ScriptArgument");
+ assert.is(args[1].text, "y");
+ assert.is(args[1].prefix, " {");
+ assert.is(args[1].suffix, "}");
+ assert.is(args[1].type, "ScriptArgument");
+
+ args = cli.tokenize("{x}{y}");
+ assert.is(args.length, 2);
+ assert.is(args[0].text, "x");
+ assert.is(args[0].prefix, "{");
+ assert.is(args[0].suffix, "}");
+ assert.is(args[0].type, "ScriptArgument");
+ assert.is(args[1].text, "y");
+ assert.is(args[1].prefix, "{");
+ assert.is(args[1].suffix, "}");
+ assert.is(args[1].type, "ScriptArgument");
+
+ args = cli.tokenize("{");
+ assert.is(args.length, 1);
+ assert.is(args[0].text, "");
+ assert.is(args[0].prefix, "{");
+ assert.is(args[0].suffix, "");
+ assert.is(args[0].type, "ScriptArgument");
+
+ args = cli.tokenize("{ ");
+ assert.is(args.length, 1);
+ assert.is(args[0].text, "");
+ assert.is(args[0].prefix, "{ ");
+ assert.is(args[0].suffix, "");
+ assert.is(args[0].type, "ScriptArgument");
+
+ args = cli.tokenize("{x");
+ assert.is(args.length, 1);
+ assert.is(args[0].text, "x");
+ assert.is(args[0].prefix, "{");
+ assert.is(args[0].suffix, "");
+ assert.is(args[0].type, "ScriptArgument");
+};
+
+exports.testRegularNesting = function (options) {
+ var args;
+
+ args = cli.tokenize('{"x"}');
+ assert.is(args.length, 1);
+ assert.is(args[0].text, '"x"');
+ assert.is(args[0].prefix, "{");
+ assert.is(args[0].suffix, "}");
+ assert.is(args[0].type, "ScriptArgument");
+
+ args = cli.tokenize("{'x'}");
+ assert.is(args.length, 1);
+ assert.is(args[0].text, "'x'");
+ assert.is(args[0].prefix, "{");
+ assert.is(args[0].suffix, "}");
+ assert.is(args[0].type, "ScriptArgument");
+
+ args = cli.tokenize('"{x}"');
+ assert.is(args.length, 1);
+ assert.is(args[0].text, "{x}");
+ assert.is(args[0].prefix, '"');
+ assert.is(args[0].suffix, '"');
+ assert.is(args[0].type, "Argument");
+
+ args = cli.tokenize("'{x}'");
+ assert.is(args.length, 1);
+ assert.is(args[0].text, "{x}");
+ assert.is(args[0].prefix, "'");
+ assert.is(args[0].suffix, "'");
+ assert.is(args[0].type, "Argument");
+};
+
+exports.testDeepNesting = function (options) {
+ var args;
+
+ args = cli.tokenize("{{}}");
+ assert.is(args.length, 1);
+ assert.is(args[0].text, "{}");
+ assert.is(args[0].prefix, "{");
+ assert.is(args[0].suffix, "}");
+ assert.is(args[0].type, "ScriptArgument");
+
+ args = cli.tokenize("{{x} {y}}");
+ assert.is(args.length, 1);
+ assert.is(args[0].text, "{x} {y}");
+ assert.is(args[0].prefix, "{");
+ assert.is(args[0].suffix, "}");
+ assert.is(args[0].type, "ScriptArgument");
+
+ args = cli.tokenize("{{w} {{{x}}}} {y} {{{z}}}");
+
+ assert.is(args.length, 3);
+
+ assert.is(args[0].text, "{w} {{{x}}}");
+ assert.is(args[0].prefix, "{");
+ assert.is(args[0].suffix, "}");
+ assert.is(args[0].type, "ScriptArgument");
+
+ assert.is(args[1].text, "y");
+ assert.is(args[1].prefix, " {");
+ assert.is(args[1].suffix, "}");
+ assert.is(args[1].type, "ScriptArgument");
+
+ assert.is(args[2].text, "{{z}}");
+ assert.is(args[2].prefix, " {");
+ assert.is(args[2].suffix, "}");
+ assert.is(args[2].type, "ScriptArgument");
+
+ args = cli.tokenize("{{w} {{{x}}} {y} {{{z}}}");
+
+ assert.is(args.length, 1);
+
+ assert.is(args[0].text, "{w} {{{x}}} {y} {{{z}}}");
+ assert.is(args[0].prefix, "{");
+ assert.is(args[0].suffix, "");
+ assert.is(args[0].type, "ScriptArgument");
+};
+
+exports.testStrangeNesting = function (options) {
+ var args;
+
+ // Note: When we get real JS parsing this should break
+ args = cli.tokenize('{"x}"}');
+
+ assert.is(args.length, 2);
+
+ assert.is(args[0].text, '"x');
+ assert.is(args[0].prefix, "{");
+ assert.is(args[0].suffix, "}");
+ assert.is(args[0].type, "ScriptArgument");
+
+ assert.is(args[1].text, "}");
+ assert.is(args[1].prefix, '"');
+ assert.is(args[1].suffix, "");
+ assert.is(args[1].type, "Argument");
+};
+
+exports.testComplex = function (options) {
+ var args;
+
+ args = cli.tokenize(" 1234 '12 34'");
+
+ assert.is(args.length, 2);
+
+ assert.is(args[0].text, "1234");
+ assert.is(args[0].prefix, " ");
+ assert.is(args[0].suffix, "");
+ assert.is(args[0].type, "Argument");
+
+ assert.is(args[1].text, "12 34");
+ assert.is(args[1].prefix, " '");
+ assert.is(args[1].suffix, "'");
+ assert.is(args[1].type, "Argument");
+
+ args = cli.tokenize('12\'34 "12 34" \\'); // 12'34 "12 34" \
+
+ assert.is(args.length, 3);
+
+ assert.is(args[0].text, "12'34");
+ assert.is(args[0].prefix, "");
+ assert.is(args[0].suffix, "");
+ assert.is(args[0].type, "Argument");
+
+ assert.is(args[1].text, "12 34");
+ assert.is(args[1].prefix, ' "');
+ assert.is(args[1].suffix, '"');
+ assert.is(args[1].type, "Argument");
+
+ assert.is(args[2].text, "\\");
+ assert.is(args[2].prefix, " ");
+ assert.is(args[2].suffix, "");
+ assert.is(args[2].type, "Argument");
+};
+
+exports.testPathological = function (options) {
+ var args;
+
+ args = cli.tokenize('a\\ b \\t\\n\\r \\\'x\\\" \'d'); // a_b \t\n\r \'x\" 'd
+
+ assert.is(args.length, 4);
+
+ assert.is(args[0].text, "a\\ b");
+ assert.is(args[0].prefix, "");
+ assert.is(args[0].suffix, "");
+ assert.is(args[0].type, "Argument");
+
+ assert.is(args[1].text, "\\t\\n\\r");
+ assert.is(args[1].prefix, " ");
+ assert.is(args[1].suffix, "");
+ assert.is(args[1].type, "Argument");
+
+ assert.is(args[2].text, '\\\'x\\"');
+ assert.is(args[2].prefix, " ");
+ assert.is(args[2].suffix, "");
+ assert.is(args[2].type, "Argument");
+
+ assert.is(args[3].text, "d");
+ assert.is(args[3].prefix, " '");
+ assert.is(args[3].suffix, "");
+ assert.is(args[3].type, "Argument");
+};
diff --git a/devtools/client/commandline/test/browser_gcli_tooltip.js b/devtools/client/commandline/test/browser_gcli_tooltip.js
new file mode 100644
index 000000000..3582490b0
--- /dev/null
+++ b/devtools/client/commandline/test/browser_gcli_tooltip.js
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2012, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+"use strict";
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
+
+const exports = {};
+
+function test() {
+ helpers.runTestModule(exports, "browser_gcli_tooltip.js");
+}
+
+// var assert = require('../testharness/assert');
+// var helpers = require('./helpers');
+
+exports.testActivate = function (options) {
+ return helpers.audit(options, [
+ {
+ setup: " ",
+ check: {
+ input: " ",
+ hints: "",
+ markup: "V",
+ cursor: 1,
+ current: "__command",
+ status: "ERROR",
+ message: "",
+ unassigned: [ ],
+ outputState: "false:default",
+ tooltipState: "false:default"
+ }
+ },
+ {
+ setup: "tsb ",
+ check: {
+ input: "tsb ",
+ hints: "false",
+ markup: "VVVV",
+ cursor: 4,
+ current: "toggle",
+ status: "VALID",
+ options: [ "false", "true" ],
+ message: "",
+ predictions: [ "false", "true" ],
+ unassigned: [ ],
+ outputState: "false:default",
+ tooltipState: "true:importantFieldFlag"
+ }
+ },
+ {
+ setup: "tsb t",
+ check: {
+ input: "tsb t",
+ hints: "rue",
+ markup: "VVVVI",
+ cursor: 5,
+ current: "toggle",
+ status: "ERROR",
+ options: [ "true" ],
+ message: "",
+ predictions: [ "true" ],
+ unassigned: [ ],
+ outputState: "false:default",
+ tooltipState: "true:importantFieldFlag"
+ }
+ },
+ {
+ setup: "tsb tt",
+ check: {
+ input: "tsb tt",
+ hints: " -> true",
+ markup: "VVVVII",
+ cursor: 6,
+ current: "toggle",
+ status: "ERROR",
+ options: [ "true" ],
+ message: "",
+ predictions: [ "true" ],
+ unassigned: [ ],
+ outputState: "false:default",
+ tooltipState: "true:importantFieldFlag"
+ }
+ },
+ {
+ setup: "wxqy",
+ check: {
+ input: "wxqy",
+ hints: "",
+ markup: "EEEE",
+ cursor: 4,
+ current: "__command",
+ status: "ERROR",
+ options: [ ],
+ message: "Can't use 'wxqy'.",
+ predictions: [ ],
+ unassigned: [ ],
+ outputState: "false:default",
+ tooltipState: "true:isError"
+ }
+ },
+ {
+ setup: "",
+ check: {
+ input: "",
+ hints: "",
+ markup: "",
+ cursor: 0,
+ current: "__command",
+ status: "ERROR",
+ message: "",
+ unassigned: [ ],
+ outputState: "false:default",
+ tooltipState: "false:default"
+ }
+ }
+ ]);
+};
diff --git a/devtools/client/commandline/test/browser_gcli_types.js b/devtools/client/commandline/test/browser_gcli_types.js
new file mode 100644
index 000000000..bbdd73c30
--- /dev/null
+++ b/devtools/client/commandline/test/browser_gcli_types.js
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2012, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+"use strict";
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
+
+const exports = {};
+
+function test() {
+ helpers.runTestModule(exports, "browser_gcli_types.js");
+}
+
+// var assert = require('../testharness/assert');
+var util = require("gcli/util/util");
+
+function forEachType(options, templateTypeSpec, callback) {
+ var types = options.requisition.system.types;
+ return util.promiseEach(types.getTypeNames(), function (name) {
+ var typeSpec = {};
+ util.copyProperties(templateTypeSpec, typeSpec);
+ typeSpec.name = name;
+ typeSpec.requisition = options.requisition;
+
+ // Provide some basic defaults to help selection/delegate/array work
+ if (name === "selection") {
+ typeSpec.data = [ "a", "b" ];
+ }
+ else if (name === "delegate") {
+ typeSpec.delegateType = function () {
+ return "string";
+ };
+ }
+ else if (name === "array") {
+ typeSpec.subtype = "string";
+ }
+ else if (name === "remote") {
+ return;
+ }
+ else if (name === "union") {
+ typeSpec.alternatives = [{ name: "string" }];
+ }
+ else if (options.isRemote) {
+ if (name === "node" || name === "nodelist") {
+ return;
+ }
+ }
+
+ var type = types.createType(typeSpec);
+ var reply = callback(type);
+ return Promise.resolve(reply);
+ });
+}
+
+exports.testDefault = function (options) {
+ return forEachType(options, {}, function (type) {
+ var context = options.requisition.executionContext;
+ var blank = type.getBlank(context).value;
+
+ // boolean and array types are exempt from needing undefined blank values
+ if (type.name === "boolean") {
+ assert.is(blank, false, "blank boolean is false");
+ }
+ else if (type.name === "array") {
+ assert.ok(Array.isArray(blank), "blank array is array");
+ assert.is(blank.length, 0, "blank array is empty");
+ }
+ else if (type.name === "nodelist") {
+ assert.ok(typeof blank.item, "function", "blank.item is function");
+ assert.is(blank.length, 0, "blank nodelist is empty");
+ }
+ else {
+ assert.is(blank, undefined, "default defined for " + type.name);
+ }
+ });
+};
+
+exports.testNullDefault = function (options) {
+ var context = null; // Is this test still valid with a null context?
+
+ return forEachType(options, { defaultValue: null }, function (type) {
+ var reply = type.stringify(null, context);
+ return Promise.resolve(reply).then(function (str) {
+ assert.is(str, "", "stringify(null) for " + type.name);
+ });
+ });
+};
+
+exports.testGetSpec = function (options) {
+ return forEachType(options, {}, function (type) {
+ if (type.name === "param") {
+ return;
+ }
+
+ var spec = type.getSpec("cmd", "param");
+ assert.ok(spec != null, "non null spec for " + type.name);
+
+ var str = JSON.stringify(spec);
+ assert.ok(str != null, "serializable spec for " + type.name);
+
+ var example = options.requisition.system.types.createType(spec);
+ assert.ok(example != null, "creatable spec for " + type.name);
+ });
+};
diff --git a/devtools/client/commandline/test/browser_gcli_union.js b/devtools/client/commandline/test/browser_gcli_union.js
new file mode 100644
index 000000000..cff9395b4
--- /dev/null
+++ b/devtools/client/commandline/test/browser_gcli_union.js
@@ -0,0 +1,173 @@
+/*
+ * Copyright 2012, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+"use strict";
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
+
+const exports = {};
+
+function test() {
+ helpers.runTestModule(exports, "browser_gcli_union.js");
+}
+
+// var assert = require('../testharness/assert');
+// var helpers = require('./helpers');
+
+exports.testDefault = function (options) {
+ return helpers.audit(options, [
+ {
+ setup: "unionc1",
+ check: {
+ input: "unionc1",
+ markup: "VVVVVVV",
+ hints: " <first>",
+ status: "ERROR",
+ args: {
+ first: {
+ value: undefined,
+ arg: "",
+ status: "INCOMPLETE"
+ }
+ }
+ }
+ },
+ {
+ setup: "unionc1 three",
+ check: {
+ input: "unionc1 three",
+ markup: "VVVVVVVVVVVVV",
+ hints: "",
+ status: "VALID",
+ args: {
+ first: {
+ value: function (data) {
+ assert.is(Object.keys(data).length, 2, "union3 Object.keys");
+ assert.is(data.type, "string", "union3 val type");
+ assert.is(data.string, "three", "union3 val string");
+ },
+ arg: " three",
+ status: "VALID"
+ }
+ }
+ },
+ exec: {
+ output: [
+ /"type": ?"string"/,
+ /"string": ?"three"/
+ ]
+ },
+ post: function (output, text) {
+ var data = output.data.first;
+ assert.is(Object.keys(data).length, 2, "union3 Object.keys");
+ assert.is(data.type, "string", "union3 val type");
+ assert.is(data.string, "three", "union3 val string");
+ }
+ },
+ {
+ setup: "unionc1 one",
+ check: {
+ input: "unionc1 one",
+ markup: "VVVVVVVVVVV",
+ hints: "",
+ status: "VALID",
+ args: {
+ first: {
+ value: function (data) {
+ assert.is(Object.keys(data).length, 2, "union1 Object.keys");
+ assert.is(data.type, "selection", "union1 val type");
+ assert.is(data.selection, 1, "union1 val selection");
+ },
+ arg: " one",
+ status: "VALID"
+ }
+ }
+ },
+ exec: {
+ output: [
+ /"type": ?"selection"/,
+ /"selection": ?1/
+ ]
+ },
+ post: function (output, text) {
+ var data = output.data.first;
+ assert.is(Object.keys(data).length, 2, "union1 Object.keys");
+ assert.is(data.type, "selection", "union1 val type");
+ assert.is(data.selection, 1, "union1 val selection");
+ }
+ },
+ {
+ skipIf: options.isPhantomjs, // PhantomJS gets predictions wrong
+ setup: "unionc1 5",
+ check: {
+ input: "unionc1 5",
+ markup: "VVVVVVVVV",
+ hints: " -> two",
+ predictions: [ "two" ],
+ status: "VALID",
+ args: {
+ first: {
+ value: function (data) {
+ assert.is(Object.keys(data).length, 2, "union5 Object.keys");
+ assert.is(data.type, "number", "union5 val type");
+ assert.is(data.number, 5, "union5 val number");
+ },
+ arg: " 5",
+ status: "VALID"
+ }
+ }
+ },
+ exec: {
+ output: [
+ /"type": ?"number"/,
+ /"number": ?5/
+ ]
+ },
+ post: function (output, text) {
+ var data = output.data.first;
+ assert.is(Object.keys(data).length, 2, "union5 Object.keys");
+ assert.is(data.type, "number", "union5 val type");
+ assert.is(data.number, 5, "union5 val number");
+ }
+ },
+ {
+ skipIf: options.isPhantomjs, // PhantomJS URL type is broken
+ setup: "unionc2 on",
+ check: {
+ input: "unionc2 on",
+ hints: "e",
+ markup: "VVVVVVVVII",
+ current: "first",
+ status: "ERROR",
+ predictionsContains: [
+ "one",
+ "http://on/",
+ "https://on/"
+ ],
+ args: {
+ command: { name: "unionc2" },
+ first: {
+ value: undefined,
+ arg: " on",
+ status: "INCOMPLETE",
+ message: "Can\u2019t use \u2018on\u2019."
+ },
+ }
+ }
+ }
+ ]);
+};
diff --git a/devtools/client/commandline/test/browser_gcli_url.js b/devtools/client/commandline/test/browser_gcli_url.js
new file mode 100644
index 000000000..dadda81f8
--- /dev/null
+++ b/devtools/client/commandline/test/browser_gcli_url.js
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2012, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+"use strict";
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
+
+const exports = {};
+
+function test() {
+ helpers.runTestModule(exports, "browser_gcli_url.js");
+}
+
+// var assert = require('../testharness/assert');
+// var helpers = require('./helpers');
+
+exports.testDefault = function (options) {
+ return helpers.audit(options, [
+ {
+ skipRemainingIf: options.isPhantomjs, // PhantomJS URL type is broken
+ setup: "urlc",
+ check: {
+ input: "urlc",
+ markup: "VVVV",
+ hints: " <url>",
+ status: "ERROR",
+ args: {
+ url: {
+ value: undefined,
+ arg: "",
+ status: "INCOMPLETE"
+ }
+ }
+ }
+ },
+ {
+ setup: "urlc example",
+ check: {
+ input: "urlc example",
+ markup: "VVVVVIIIIIII",
+ hints: " -> http://example/",
+ predictions: [
+ "http://example/",
+ "https://example/",
+ "http://localhost:9999/example"
+ ],
+ status: "ERROR",
+ args: {
+ url: {
+ value: undefined,
+ arg: " example",
+ status: "INCOMPLETE"
+ }
+ }
+ },
+ },
+ {
+ setup: "urlc example.com/",
+ check: {
+ input: "urlc example.com/",
+ markup: "VVVVVIIIIIIIIIIII",
+ hints: " -> http://example.com/",
+ status: "ERROR",
+ args: {
+ url: {
+ value: undefined,
+ arg: " example.com/",
+ status: "INCOMPLETE"
+ }
+ }
+ },
+ },
+ {
+ setup: "urlc http://example.com/index?q=a#hash",
+ check: {
+ input: "urlc http://example.com/index?q=a#hash",
+ markup: "VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV",
+ hints: "",
+ status: "VALID",
+ args: {
+ url: {
+ value: function (data) {
+ assert.is(data.hash, "#hash", "url hash");
+ },
+ arg: " http://example.com/index?q=a#hash",
+ status: "VALID"
+ }
+ }
+ },
+ exec: { output: /"url": ?/ }
+ }
+ ]);
+};
diff --git a/devtools/client/commandline/test/head.js b/devtools/client/commandline/test/head.js
new file mode 100644
index 000000000..b6b2e8cab
--- /dev/null
+++ b/devtools/client/commandline/test/head.js
@@ -0,0 +1,40 @@
+/* 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/. */
+
+const TEST_BASE_HTTP = "http://example.com/browser/devtools/client/commandline/test/";
+const TEST_BASE_HTTPS = "https://example.com/browser/devtools/client/commandline/test/";
+
+var { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+var { console } = require("resource://gre/modules/Console.jsm");
+var flags = require("devtools/shared/flags");
+
+// Import the GCLI test helper
+var testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/"));
+Services.scriptloader.loadSubScript(testDir + "/helpers.js", this);
+Services.scriptloader.loadSubScript(testDir + "/mockCommands.js", this);
+
+flags.testing = true;
+SimpleTest.registerCleanupFunction(() => {
+ flags.testing = false;
+});
+
+function whenDelayedStartupFinished(aWindow, aCallback) {
+ Services.obs.addObserver(function observer(aSubject, aTopic) {
+ if (aWindow == aSubject) {
+ Services.obs.removeObserver(observer, aTopic);
+ executeSoon(aCallback);
+ }
+ }, "browser-delayed-startup-finished", false);
+}
+
+/**
+ * Force GC on shutdown, because it seems that GCLI can outrun the garbage
+ * collector in some situations, which causes test failures in later tests
+ * Bug 774619 is an example.
+ */
+registerCleanupFunction(function tearDown() {
+ window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils)
+ .garbageCollect();
+});
diff --git a/devtools/client/commandline/test/helpers.js b/devtools/client/commandline/test/helpers.js
new file mode 100644
index 000000000..d365765a2
--- /dev/null
+++ b/devtools/client/commandline/test/helpers.js
@@ -0,0 +1,1341 @@
+/*
+ * Copyright 2012, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+"use strict";
+
+// A copy of this code exists in firefox mochitests. They should be kept
+// in sync. Hence the exports synonym for non AMD contexts.
+var { helpers, assert } = (function () {
+
+ var helpers = {};
+
+ var { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+ var { TargetFactory } = require("devtools/client/framework/target");
+ var Services = require("Services");
+
+ var assert = { ok: ok, is: is, log: info };
+ var util = require("gcli/util/util");
+ var cli = require("gcli/cli");
+ var KeyEvent = require("gcli/util/util").KeyEvent;
+
+ const { GcliFront } = require("devtools/shared/fronts/gcli");
+
+/**
+ * See notes in helpers.checkOptions()
+ */
+ var createDeveloperToolbarAutomator = function (toolbar) {
+ var automator = {
+ setInput: function (typed) {
+ return toolbar.inputter.setInput(typed);
+ },
+
+ setCursor: function (cursor) {
+ return toolbar.inputter.setCursor(cursor);
+ },
+
+ focus: function () {
+ return toolbar.inputter.focus();
+ },
+
+ fakeKey: function (keyCode) {
+ var fakeEvent = {
+ keyCode: keyCode,
+ preventDefault: function () { },
+ timeStamp: new Date().getTime()
+ };
+
+ toolbar.inputter.onKeyDown(fakeEvent);
+
+ if (keyCode === KeyEvent.DOM_VK_BACK_SPACE) {
+ var input = toolbar.inputter.element;
+ input.value = input.value.slice(0, -1);
+ }
+
+ return toolbar.inputter.handleKeyUp(fakeEvent);
+ },
+
+ getInputState: function () {
+ return toolbar.inputter.getInputState();
+ },
+
+ getCompleterTemplateData: function () {
+ return toolbar.completer._getCompleterTemplateData();
+ },
+
+ getErrorMessage: function () {
+ return toolbar.tooltip.errorEle.textContent;
+ }
+ };
+
+ Object.defineProperty(automator, "focusManager", {
+ get: function () { return toolbar.focusManager; },
+ enumerable: true
+ });
+
+ Object.defineProperty(automator, "field", {
+ get: function () { return toolbar.tooltip.field; },
+ enumerable: true
+ });
+
+ return automator;
+ };
+
+/**
+ * Warning: For use with Firefox Mochitests only.
+ *
+ * Open a new tab at a URL and call a callback on load, and then tidy up when
+ * the callback finishes.
+ * The function will be passed a set of test options, and will usually return a
+ * promise to indicate that the tab can be cleared up. (To be formal, we call
+ * Promise.resolve() on the return value of the callback function)
+ *
+ * The options used by addTab include:
+ * - chromeWindow: XUL window parent of created tab. a.k.a 'window' in mochitest
+ * - tab: The new XUL tab element, as returned by gBrowser.addTab()
+ * - target: The debug target as defined by the devtools framework
+ * - browser: The XUL browser element for the given tab
+ * - isFirefox: Always true. Allows test sharing with GCLI
+ *
+ * Normally addTab will create an options object containing the values as
+ * described above. However these options can be customized by the third
+ * 'options' parameter. This has the ability to customize the value of
+ * chromeWindow or isFirefox, and to add new properties.
+ *
+ * @param url The URL for the new tab
+ * @param callback The function to call on page load
+ * @param options An optional set of options to customize the way the tests run
+ */
+ helpers.addTab = function (url, callback, options) {
+ waitForExplicitFinish();
+
+ options = options || {};
+ options.chromeWindow = options.chromeWindow || window;
+ options.isFirefox = true;
+
+ var tabbrowser = options.chromeWindow.gBrowser;
+ options.tab = tabbrowser.addTab();
+ tabbrowser.selectedTab = options.tab;
+ options.browser = tabbrowser.getBrowserForTab(options.tab);
+ options.target = TargetFactory.forTab(options.tab);
+
+ var loaded = helpers.listenOnce(options.browser, "load", true).then(function (ev) {
+ var reply = callback.call(null, options);
+
+ return Promise.resolve(reply).then(null, function (error) {
+ ok(false, error);
+ }).then(function () {
+ tabbrowser.removeTab(options.tab);
+
+ delete options.target;
+ delete options.browser;
+ delete options.tab;
+
+ delete options.chromeWindow;
+ delete options.isFirefox;
+ });
+ });
+
+ options.browser.contentWindow.location = url;
+ return loaded;
+ };
+
+/**
+ * Open a new tab
+ * @param url Address of the page to open
+ * @param options Object to which we add properties describing the new tab. The
+ * following properties are added:
+ * - chromeWindow
+ * - tab
+ * - browser
+ * - target
+ * @return A promise which resolves to the options object when the 'load' event
+ * happens on the new tab
+ */
+ helpers.openTab = function (url, options) {
+ waitForExplicitFinish();
+
+ options = options || {};
+ options.chromeWindow = options.chromeWindow || window;
+ options.isFirefox = true;
+
+ var tabbrowser = options.chromeWindow.gBrowser;
+ options.tab = tabbrowser.addTab();
+ tabbrowser.selectedTab = options.tab;
+ options.browser = tabbrowser.getBrowserForTab(options.tab);
+ options.target = TargetFactory.forTab(options.tab);
+
+ return helpers.navigate(url, options);
+ };
+
+/**
+ * Undo the effects of |helpers.openTab|
+ * @param options The options object passed to |helpers.openTab|
+ * @return A promise resolved (with undefined) when the tab is closed
+ */
+ helpers.closeTab = function (options) {
+ options.chromeWindow.gBrowser.removeTab(options.tab);
+
+ delete options.target;
+ delete options.browser;
+ delete options.tab;
+
+ delete options.chromeWindow;
+ delete options.isFirefox;
+
+ return Promise.resolve(undefined);
+ };
+
+/**
+ * Open the developer toolbar in a tab
+ * @param options Object to which we add properties describing the developer
+ * toolbar. The following properties are added:
+ * - automator
+ * - requisition
+ * @return A promise which resolves to the options object when the 'load' event
+ * happens on the new tab
+ */
+ helpers.openToolbar = function (options) {
+ options = options || {};
+ options.chromeWindow = options.chromeWindow || window;
+
+ return options.chromeWindow.DeveloperToolbar.show(true).then(function () {
+ var toolbar = options.chromeWindow.DeveloperToolbar;
+ options.automator = createDeveloperToolbarAutomator(toolbar);
+ options.requisition = toolbar.requisition;
+ return options;
+ });
+ };
+
+/**
+ * Navigate the current tab to a URL
+ */
+ helpers.navigate = Task.async(function* (url, options) {
+ options = options || {};
+ options.chromeWindow = options.chromeWindow || window;
+ options.tab = options.tab || options.chromeWindow.gBrowser.selectedTab;
+
+ var tabbrowser = options.chromeWindow.gBrowser;
+ options.browser = tabbrowser.getBrowserForTab(options.tab);
+
+ let onLoaded = BrowserTestUtils.browserLoaded(options.browser);
+ options.browser.loadURI(url);
+ yield onLoaded;
+
+ return options;
+ });
+
+/**
+ * Undo the effects of |helpers.openToolbar|
+ * @param options The options object passed to |helpers.openToolbar|
+ * @return A promise resolved (with undefined) when the toolbar is closed
+ */
+ helpers.closeToolbar = function (options) {
+ return options.chromeWindow.DeveloperToolbar.hide().then(function () {
+ delete options.automator;
+ delete options.requisition;
+ });
+ };
+
+/**
+ * A helper to work with Task.spawn so you can do:
+ * return Task.spawn(realTestFunc).then(finish, helpers.handleError);
+ */
+ helpers.handleError = function (ex) {
+ console.error(ex);
+ ok(false, ex);
+ finish();
+ };
+
+/**
+ * A helper for calling addEventListener and then removeEventListener as soon
+ * as the event is called, passing the results on as a promise
+ * @param element The DOM element to listen on
+ * @param event The name of the event to listen for
+ * @param useCapture Should we use the capturing phase?
+ * @return A promise resolved with the event object when the event first happens
+ */
+ helpers.listenOnce = function (element, event, useCapture) {
+ return new Promise(function (resolve, reject) {
+ var onEvent = function (ev) {
+ element.removeEventListener(event, onEvent, useCapture);
+ resolve(ev);
+ };
+ element.addEventListener(event, onEvent, useCapture);
+ }.bind(this));
+ };
+
+/**
+ * A wrapper for calling Services.obs.[add|remove]Observer using promises.
+ * @param topic The topic parameter to Services.obs.addObserver
+ * @param ownsWeak The ownsWeak parameter to Services.obs.addObserver with a
+ * default value of false
+ * @return a promise that resolves when the ObserverService first notifies us
+ * of the topic. The value of the promise is the first parameter to the observer
+ * function other parameters are dropped.
+ */
+ helpers.observeOnce = function (topic, ownsWeak = false) {
+ return new Promise(function (resolve, reject) {
+ let resolver = function (subject) {
+ Services.obs.removeObserver(resolver, topic);
+ resolve(subject);
+ };
+ Services.obs.addObserver(resolver, topic, ownsWeak);
+ }.bind(this));
+ };
+
+/**
+ * Takes a function that uses a callback as its last parameter, and returns a
+ * new function that returns a promise instead
+ */
+ helpers.promiseify = function (functionWithLastParamCallback, scope) {
+ return function () {
+ let args = [].slice.call(arguments);
+ return new Promise(resolve => {
+ args.push((...results) => {
+ resolve(results.length > 1 ? results : results[0]);
+ });
+ functionWithLastParamCallback.apply(scope, args);
+ });
+ };
+ };
+
+/**
+ * Warning: For use with Firefox Mochitests only.
+ *
+ * As addTab, but that also opens the developer toolbar. In addition a new
+ * 'automator' property is added to the options object which uses the
+ * developer toolbar
+ */
+ helpers.addTabWithToolbar = function (url, callback, options) {
+ return helpers.addTab(url, function (innerOptions) {
+ var win = innerOptions.chromeWindow;
+
+ return win.DeveloperToolbar.show(true).then(function () {
+ var toolbar = win.DeveloperToolbar;
+ innerOptions.automator = createDeveloperToolbarAutomator(toolbar);
+ innerOptions.requisition = toolbar.requisition;
+
+ var reply = callback.call(null, innerOptions);
+
+ return Promise.resolve(reply).then(null, function (error) {
+ ok(false, error);
+ console.error(error);
+ }).then(function () {
+ win.DeveloperToolbar.hide().then(function () {
+ delete innerOptions.automator;
+ });
+ });
+ });
+ }, options);
+ };
+
+/**
+ * Warning: For use with Firefox Mochitests only.
+ *
+ * Run a set of test functions stored in the values of the 'exports' object
+ * functions stored under setup/shutdown will be run at the start/end of the
+ * sequence of tests.
+ * A test will be considered finished when its return value is resolved.
+ * @param options An object to be passed to the test functions
+ * @param tests An object containing named test functions
+ * @return a promise which will be resolved when all tests have been run and
+ * their return values resolved
+ */
+ helpers.runTests = function (options, tests) {
+ var testNames = Object.keys(tests).filter(function (test) {
+ return test != "setup" && test != "shutdown";
+ });
+
+ var recover = function (error) {
+ ok(false, error);
+ console.error(error, error.stack);
+ };
+
+ info("SETUP");
+ var setupDone = (tests.setup != null) ?
+ Promise.resolve(tests.setup(options)) :
+ Promise.resolve();
+
+ var testDone = setupDone.then(function () {
+ return util.promiseEach(testNames, function (testName) {
+ info(testName);
+ var action = tests[testName];
+
+ if (typeof action === "function") {
+ var reply = action.call(tests, options);
+ return Promise.resolve(reply);
+ }
+ else if (Array.isArray(action)) {
+ return helpers.audit(options, action);
+ }
+
+ return Promise.reject("test action '" + testName +
+ "' is not a function or helpers.audit() object");
+ });
+ }, recover);
+
+ return testDone.then(function () {
+ info("SHUTDOWN");
+ return (tests.shutdown != null) ?
+ Promise.resolve(tests.shutdown(options)) :
+ Promise.resolve();
+ }, recover);
+ };
+
+ const MOCK_COMMANDS_URI = "chrome://mochitests/content/browser/devtools/client/commandline/test/mockCommands.js";
+
+ const defer = function () {
+ const deferred = { };
+ deferred.promise = new Promise(function (resolve, reject) {
+ deferred.resolve = resolve;
+ deferred.reject = reject;
+ });
+ return deferred;
+ };
+
+/**
+ * This does several actions associated with running a GCLI test in mochitest
+ * 1. Create a new tab containing basic markup for GCLI tests
+ * 2. Open the developer toolbar
+ * 3. Register the mock commands with the server process
+ * 4. Wait for the proxy commands to be auto-regitstered with the client
+ * 5. Register the mock converters with the client process
+ * 6. Run all the tests
+ * 7. Tear down all the setup
+ */
+ helpers.runTestModule = function (exports, name) {
+ return Task.spawn(function* () {
+ const uri = "data:text/html;charset=utf-8," +
+ "<style>div{color:red;}</style>" +
+ "<div id='gcli-root'>" + name + "</div>";
+
+ const options = yield helpers.openTab(uri);
+ options.isRemote = true;
+
+ yield helpers.openToolbar(options);
+
+ const system = options.requisition.system;
+
+ // Register a one time listener with the local set of commands
+ const addedDeferred = defer();
+ const removedDeferred = defer();
+ let state = "preAdd"; // Then 'postAdd' then 'postRemove'
+
+ system.commands.onCommandsChange.add(function (ev) {
+ if (system.commands.get("tsslow") != null) {
+ if (state === "preAdd") {
+ addedDeferred.resolve();
+ state = "postAdd";
+ }
+ }
+ else {
+ if (state === "postAdd") {
+ removedDeferred.resolve();
+ state = "postRemove";
+ }
+ }
+ });
+
+ // Send a message to add the commands to the content process
+ const front = yield GcliFront.create(options.target);
+ yield front._testOnlyAddItemsByModule(MOCK_COMMANDS_URI);
+
+ // This will cause the local set of commands to be updated with the
+ // command proxies, wait for that to complete.
+ yield addedDeferred.promise;
+
+ // Now we need to add the converters to the local GCLI
+ const converters = mockCommands.items.filter(item => item.item === "converter");
+ system.addItems(converters);
+
+ // Next run the tests
+ yield helpers.runTests(options, exports);
+
+ // Finally undo the mock commands and converters
+ system.removeItems(converters);
+ const removePromise = system.commands.onCommandsChange.once();
+ yield front._testOnlyRemoveItemsByModule(MOCK_COMMANDS_URI);
+ yield removedDeferred.promise;
+
+ // And close everything down
+ yield helpers.closeToolbar(options);
+ yield helpers.closeTab(options);
+ }).then(finish, helpers.handleError);
+ };
+
+/**
+ * Ensure that the options object is setup correctly
+ * options should contain an automator object that looks like this:
+ * {
+ * getInputState: function() { ... },
+ * setCursor: function(cursor) { ... },
+ * getCompleterTemplateData: function() { ... },
+ * focus: function() { ... },
+ * getErrorMessage: function() { ... },
+ * fakeKey: function(keyCode) { ... },
+ * setInput: function(typed) { ... },
+ * focusManager: ...,
+ * field: ...,
+ * }
+ */
+ function checkOptions(options) {
+ if (options == null) {
+ console.trace();
+ throw new Error("Missing options object");
+ }
+ if (options.requisition == null) {
+ console.trace();
+ throw new Error("options.requisition == null");
+ }
+ }
+
+/**
+ * Various functions to return the actual state of the command line
+ */
+ helpers._actual = {
+ input: function (options) {
+ return options.automator.getInputState().typed;
+ },
+
+ hints: function (options) {
+ return options.automator.getCompleterTemplateData().then(function (data) {
+ var emptyParams = data.emptyParameters.join("");
+ return (data.directTabText + emptyParams + data.arrowTabText)
+ .replace(/\u00a0/g, " ")
+ .replace(/\u21E5/, "->")
+ .replace(/ $/, "");
+ });
+ },
+
+ markup: function (options) {
+ var cursor = helpers._actual.cursor(options);
+ var statusMarkup = options.requisition.getInputStatusMarkup(cursor);
+ return statusMarkup.map(function (s) {
+ return new Array(s.string.length + 1).join(s.status.toString()[0]);
+ }).join("");
+ },
+
+ cursor: function (options) {
+ return options.automator.getInputState().cursor.start;
+ },
+
+ current: function (options) {
+ var cursor = helpers._actual.cursor(options);
+ return options.requisition.getAssignmentAt(cursor).param.name;
+ },
+
+ status: function (options) {
+ return options.requisition.status.toString();
+ },
+
+ predictions: function (options) {
+ var cursor = helpers._actual.cursor(options);
+ var assignment = options.requisition.getAssignmentAt(cursor);
+ var context = options.requisition.executionContext;
+ return assignment.getPredictions(context).then(function (predictions) {
+ return predictions.map(function (prediction) {
+ return prediction.name;
+ });
+ });
+ },
+
+ unassigned: function (options) {
+ return options.requisition._unassigned.map(function (assignment) {
+ return assignment.arg.toString();
+ }.bind(this));
+ },
+
+ outputState: function (options) {
+ var outputData = options.automator.focusManager._shouldShowOutput();
+ return outputData.visible + ":" + outputData.reason;
+ },
+
+ tooltipState: function (options) {
+ var tooltipData = options.automator.focusManager._shouldShowTooltip();
+ return tooltipData.visible + ":" + tooltipData.reason;
+ },
+
+ options: function (options) {
+ if (options.automator.field.menu == null) {
+ return [];
+ }
+ return options.automator.field.menu.items.map(function (item) {
+ return item.name.textContent ? item.name.textContent : item.name;
+ });
+ },
+
+ message: function (options) {
+ return options.automator.getErrorMessage();
+ }
+ };
+
+ function shouldOutputUnquoted(value) {
+ var type = typeof value;
+ return value == null || type === "boolean" || type === "number";
+ }
+
+ function outputArray(array) {
+ return (array.length === 0) ?
+ "[ ]" :
+ "[ '" + array.join("', '") + "' ]";
+ }
+
+ helpers._createDebugCheck = function (options) {
+ checkOptions(options);
+ var requisition = options.requisition;
+ var command = requisition.commandAssignment.value;
+ var cursor = helpers._actual.cursor(options);
+ var input = helpers._actual.input(options);
+ var padding = new Array(input.length + 1).join(" ");
+
+ var hintsPromise = helpers._actual.hints(options);
+ var predictionsPromise = helpers._actual.predictions(options);
+
+ return Promise.all([ hintsPromise, predictionsPromise ]).then(function (values) {
+ var hints = values[0];
+ var predictions = values[1];
+ var output = "";
+
+ output += "return helpers.audit(options, [\n";
+ output += " {\n";
+
+ if (cursor === input.length) {
+ output += " setup: '" + input + "',\n";
+ }
+ else {
+ output += " name: '" + input + " (cursor=" + cursor + ")',\n";
+ output += " setup: function() {\n";
+ output += " return helpers.setInput(options, '" + input + "', " + cursor + ");\n";
+ output += " },\n";
+ }
+
+ output += " check: {\n";
+
+ output += " input: '" + input + "',\n";
+ output += " hints: " + padding + "'" + hints + "',\n";
+ output += " markup: '" + helpers._actual.markup(options) + "',\n";
+ output += " cursor: " + cursor + ",\n";
+ output += " current: '" + helpers._actual.current(options) + "',\n";
+ output += " status: '" + helpers._actual.status(options) + "',\n";
+ output += " options: " + outputArray(helpers._actual.options(options)) + ",\n";
+ output += " message: '" + helpers._actual.message(options) + "',\n";
+ output += " predictions: " + outputArray(predictions) + ",\n";
+ output += " unassigned: " + outputArray(requisition._unassigned) + ",\n";
+ output += " outputState: '" + helpers._actual.outputState(options) + "',\n";
+ output += " tooltipState: '" + helpers._actual.tooltipState(options) + "'" +
+ (command ? "," : "") + "\n";
+
+ if (command) {
+ output += " args: {\n";
+ output += " command: { name: '" + command.name + "' },\n";
+
+ requisition.getAssignments().forEach(function (assignment) {
+ output += " " + assignment.param.name + ": { ";
+
+ if (typeof assignment.value === "string") {
+ output += "value: '" + assignment.value + "', ";
+ }
+ else if (shouldOutputUnquoted(assignment.value)) {
+ output += "value: " + assignment.value + ", ";
+ }
+ else {
+ output += "/*value:" + assignment.value + ",*/ ";
+ }
+
+ output += "arg: '" + assignment.arg + "', ";
+ output += "status: '" + assignment.getStatus().toString() + "', ";
+ output += "message: '" + assignment.message + "'";
+ output += " },\n";
+ });
+
+ output += " }\n";
+ }
+
+ output += " },\n";
+ output += " exec: {\n";
+ output += " output: '',\n";
+ output += " type: 'string',\n";
+ output += " error: false\n";
+ output += " }\n";
+ output += " }\n";
+ output += "]);";
+
+ return output;
+ }.bind(this), util.errorHandler);
+ };
+
+/**
+ * Simulate focusing the input field
+ */
+ helpers.focusInput = function (options) {
+ checkOptions(options);
+ options.automator.focus();
+ };
+
+/**
+ * Simulate pressing TAB in the input field
+ */
+ helpers.pressTab = function (options) {
+ checkOptions(options);
+ return helpers.pressKey(options, KeyEvent.DOM_VK_TAB);
+ };
+
+/**
+ * Simulate pressing RETURN in the input field
+ */
+ helpers.pressReturn = function (options) {
+ checkOptions(options);
+ return helpers.pressKey(options, KeyEvent.DOM_VK_RETURN);
+ };
+
+/**
+ * Simulate pressing a key by keyCode in the input field
+ */
+ helpers.pressKey = function (options, keyCode) {
+ checkOptions(options);
+ return options.automator.fakeKey(keyCode);
+ };
+
+/**
+ * A list of special key presses and how to to them, for the benefit of
+ * helpers.setInput
+ */
+ var ACTIONS = {
+ "<TAB>": function (options) {
+ return helpers.pressTab(options);
+ },
+ "<RETURN>": function (options) {
+ return helpers.pressReturn(options);
+ },
+ "<UP>": function (options) {
+ return helpers.pressKey(options, KeyEvent.DOM_VK_UP);
+ },
+ "<DOWN>": function (options) {
+ return helpers.pressKey(options, KeyEvent.DOM_VK_DOWN);
+ },
+ "<BACKSPACE>": function (options) {
+ return helpers.pressKey(options, KeyEvent.DOM_VK_BACK_SPACE);
+ }
+ };
+
+/**
+ * Used in helpers.setInput to cut an input string like 'blah<TAB>foo<UP>' into
+ * an array like [ 'blah', '<TAB>', 'foo', '<UP>' ].
+ * When using this RegExp, you also need to filter out the blank strings.
+ */
+ var CHUNKER = /([^<]*)(<[A-Z]+>)/;
+
+/**
+ * Alter the input to <code>typed</code> optionally leaving the cursor at
+ * <code>cursor</code>.
+ * @return A promise of the number of key-presses to respond
+ */
+ helpers.setInput = function (options, typed, cursor) {
+ checkOptions(options);
+ var inputPromise;
+ var automator = options.automator;
+ // We try to measure average keypress time, but setInput can simulate
+ // several, so we try to keep track of how many
+ var chunkLen = 1;
+
+ // The easy case is a simple string without things like <TAB>
+ if (typed.indexOf("<") === -1) {
+ inputPromise = automator.setInput(typed);
+ }
+ else {
+ // Cut the input up into input strings separated by '<KEY>' tokens. The
+ // CHUNKS RegExp leaves blanks so we filter them out.
+ var chunks = typed.split(CHUNKER).filter(function (s) {
+ return s !== "";
+ });
+ chunkLen = chunks.length + 1;
+
+ // We're working on this in chunks so first clear the input
+ inputPromise = automator.setInput("").then(function () {
+ return util.promiseEach(chunks, function (chunk) {
+ if (chunk.charAt(0) === "<") {
+ var action = ACTIONS[chunk];
+ if (typeof action !== "function") {
+ console.error("Known actions: " + Object.keys(ACTIONS).join());
+ throw new Error('Key action not found "' + chunk + '"');
+ }
+ return action(options);
+ }
+ else {
+ return automator.setInput(automator.getInputState().typed + chunk);
+ }
+ });
+ });
+ }
+
+ return inputPromise.then(function () {
+ if (cursor != null) {
+ automator.setCursor({ start: cursor, end: cursor });
+ }
+
+ if (automator.focusManager) {
+ automator.focusManager.onInputChange();
+ }
+
+ // Firefox testing is noisy and distant, so logging helps
+ if (options.isFirefox) {
+ var cursorStr = (cursor == null ? "" : ", " + cursor);
+ log('setInput("' + typed + '"' + cursorStr + ")");
+ }
+
+ return chunkLen;
+ });
+ };
+
+/**
+ * Helper for helpers.audit() to ensure that all the 'check' properties match.
+ * See helpers.audit for more information.
+ * @param name The name to use in error messages
+ * @param checks See helpers.audit for a list of available checks
+ * @return A promise which resolves to undefined when the checks are complete
+ */
+ helpers._check = function (options, name, checks) {
+ // A test method to check that all args are assigned in some way
+ var requisition = options.requisition;
+ requisition._args.forEach(function (arg) {
+ if (arg.assignment == null) {
+ assert.ok(false, "No assignment for " + arg);
+ }
+ });
+
+ if (checks == null) {
+ return Promise.resolve();
+ }
+
+ var outstanding = [];
+ var suffix = name ? " (for '" + name + "')" : "";
+
+ if (!options.isNode && "input" in checks) {
+ assert.is(helpers._actual.input(options), checks.input, "input" + suffix);
+ }
+
+ if (!options.isNode && "cursor" in checks) {
+ assert.is(helpers._actual.cursor(options), checks.cursor, "cursor" + suffix);
+ }
+
+ if (!options.isNode && "current" in checks) {
+ assert.is(helpers._actual.current(options), checks.current, "current" + suffix);
+ }
+
+ if ("status" in checks) {
+ assert.is(helpers._actual.status(options), checks.status, "status" + suffix);
+ }
+
+ if (!options.isNode && "markup" in checks) {
+ assert.is(helpers._actual.markup(options), checks.markup, "markup" + suffix);
+ }
+
+ if (!options.isNode && "hints" in checks) {
+ var hintCheck = function (actualHints) {
+ assert.is(actualHints, checks.hints, "hints" + suffix);
+ };
+ outstanding.push(helpers._actual.hints(options).then(hintCheck));
+ }
+
+ if (!options.isNode && "predictions" in checks) {
+ var predictionsCheck = function (actualPredictions) {
+ helpers.arrayIs(actualPredictions,
+ checks.predictions,
+ "predictions" + suffix);
+ };
+ outstanding.push(helpers._actual.predictions(options).then(predictionsCheck));
+ }
+
+ if (!options.isNode && "predictionsContains" in checks) {
+ var containsCheck = function (actualPredictions) {
+ checks.predictionsContains.forEach(function (prediction) {
+ var index = actualPredictions.indexOf(prediction);
+ assert.ok(index !== -1,
+ "predictionsContains:" + prediction + suffix);
+ if (index === -1) {
+ log("Actual predictions (" + actualPredictions.length + "): " +
+ actualPredictions.join(", "));
+ }
+ });
+ };
+ outstanding.push(helpers._actual.predictions(options).then(containsCheck));
+ }
+
+ if ("unassigned" in checks) {
+ helpers.arrayIs(helpers._actual.unassigned(options),
+ checks.unassigned,
+ "unassigned" + suffix);
+ }
+
+ /* TODO: Fix this
+ if (!options.isNode && 'tooltipState' in checks) {
+ assert.is(helpers._actual.tooltipState(options),
+ checks.tooltipState,
+ 'tooltipState' + suffix);
+ }
+ */
+
+ if (!options.isNode && "outputState" in checks) {
+ assert.is(helpers._actual.outputState(options),
+ checks.outputState,
+ "outputState" + suffix);
+ }
+
+ if (!options.isNode && "options" in checks) {
+ helpers.arrayIs(helpers._actual.options(options),
+ checks.options,
+ "options" + suffix);
+ }
+
+ if (!options.isNode && "error" in checks) {
+ assert.is(helpers._actual.message(options), checks.error, "error" + suffix);
+ }
+
+ if (checks.args != null) {
+ Object.keys(checks.args).forEach(function (paramName) {
+ var check = checks.args[paramName];
+
+ // We allow an 'argument' called 'command' to be the command itself, but
+ // what if the command has a parameter called 'command' (for example, an
+ // 'exec' command)? We default to using the parameter because checking
+ // the command value is less useful
+ var assignment = requisition.getAssignment(paramName);
+ if (assignment == null && paramName === "command") {
+ assignment = requisition.commandAssignment;
+ }
+
+ if (assignment == null) {
+ assert.ok(false, "Unknown arg: " + paramName + suffix);
+ return;
+ }
+
+ if ("value" in check) {
+ if (typeof check.value === "function") {
+ try {
+ check.value(assignment.value);
+ }
+ catch (ex) {
+ assert.ok(false, "" + ex);
+ }
+ }
+ else {
+ assert.is(assignment.value,
+ check.value,
+ "arg." + paramName + ".value" + suffix);
+ }
+ }
+
+ if ("name" in check) {
+ assert.is(assignment.value.name,
+ check.name,
+ "arg." + paramName + ".name" + suffix);
+ }
+
+ if ("type" in check) {
+ assert.is(assignment.arg.type,
+ check.type,
+ "arg." + paramName + ".type" + suffix);
+ }
+
+ if ("arg" in check) {
+ assert.is(assignment.arg.toString(),
+ check.arg,
+ "arg." + paramName + ".arg" + suffix);
+ }
+
+ if ("status" in check) {
+ assert.is(assignment.getStatus().toString(),
+ check.status,
+ "arg." + paramName + ".status" + suffix);
+ }
+
+ if (!options.isNode && "message" in check) {
+ if (typeof check.message.test === "function") {
+ assert.ok(check.message.test(assignment.message),
+ "arg." + paramName + ".message" + suffix);
+ }
+ else {
+ assert.is(assignment.message,
+ check.message,
+ "arg." + paramName + ".message" + suffix);
+ }
+ }
+ });
+ }
+
+ return Promise.all(outstanding).then(function () {
+ // Ensure the promise resolves to nothing
+ return undefined;
+ });
+ };
+
+/**
+ * Helper for helpers.audit() to ensure that all the 'exec' properties work.
+ * See helpers.audit for more information.
+ * @param name The name to use in error messages
+ * @param expected See helpers.audit for a list of available exec checks
+ * @return A promise which resolves to undefined when the checks are complete
+ */
+ helpers._exec = function (options, name, expected) {
+ var requisition = options.requisition;
+ if (expected == null) {
+ return Promise.resolve({});
+ }
+
+ var origLogErrors = cli.logErrors;
+ if (expected.error) {
+ cli.logErrors = false;
+ }
+
+ try {
+ return requisition.exec({ hidden: true }).then(function (output) {
+ if ("type" in expected) {
+ assert.is(output.type,
+ expected.type,
+ "output.type for: " + name);
+ }
+
+ if ("error" in expected) {
+ assert.is(output.error,
+ expected.error,
+ "output.error for: " + name);
+ }
+
+ if (!("output" in expected)) {
+ return { output: output };
+ }
+
+ var context = requisition.conversionContext;
+ var convertPromise;
+ if (options.isNode) {
+ convertPromise = output.convert("string", context);
+ }
+ else {
+ convertPromise = output.convert("dom", context).then(function (node) {
+ return (node == null) ? "" : node.textContent.trim();
+ });
+ }
+
+ return convertPromise.then(function (textOutput) {
+ var doTest = function (match, against) {
+ // Only log the real textContent if the test fails
+ if (against.match(match) != null) {
+ assert.ok(true, "html output for '" + name + "' " +
+ "should match /" + (match.source || match) + "/");
+ } else {
+ assert.ok(false, "html output for '" + name + "' " +
+ "should match /" + (match.source || match) + "/. " +
+ 'Actual textContent: "' + against + '"');
+ }
+ };
+
+ if (typeof expected.output === "string") {
+ assert.is(textOutput,
+ expected.output,
+ "html output for " + name);
+ }
+ else if (Array.isArray(expected.output)) {
+ expected.output.forEach(function (match) {
+ doTest(match, textOutput);
+ });
+ }
+ else {
+ doTest(expected.output, textOutput);
+ }
+
+ if (expected.error) {
+ cli.logErrors = origLogErrors;
+ }
+ return { output: output, text: textOutput };
+ });
+ }.bind(this)).then(function (data) {
+ if (expected.error) {
+ cli.logErrors = origLogErrors;
+ }
+
+ return data;
+ });
+ }
+ catch (ex) {
+ assert.ok(false, "Failure executing '" + name + "': " + ex);
+ util.errorHandler(ex);
+
+ if (expected.error) {
+ cli.logErrors = origLogErrors;
+ }
+ return Promise.resolve({});
+ }
+ };
+
+/**
+ * Helper to setup the test
+ */
+ helpers._setup = function (options, name, audit) {
+ if (typeof audit.setup === "string") {
+ return helpers.setInput(options, audit.setup);
+ }
+
+ if (typeof audit.setup === "function") {
+ return Promise.resolve(audit.setup.call(audit));
+ }
+
+ return Promise.reject("'setup' property must be a string or a function. Is " + audit.setup);
+ };
+
+/**
+ * Helper to shutdown the test
+ */
+ helpers._post = function (name, audit, data) {
+ if (typeof audit.post === "function") {
+ return Promise.resolve(audit.post.call(audit, data.output, data.text));
+ }
+ return Promise.resolve(audit.post);
+ };
+
+/*
+ * We do some basic response time stats so we can see if we're getting slow
+ */
+ var totalResponseTime = 0;
+ var averageOver = 0;
+ var maxResponseTime = 0;
+ var maxResponseCulprit;
+ var start;
+
+/**
+ * Restart the stats collection process
+ */
+ helpers.resetResponseTimes = function () {
+ start = new Date().getTime();
+ totalResponseTime = 0;
+ averageOver = 0;
+ maxResponseTime = 0;
+ maxResponseCulprit = undefined;
+ };
+
+/**
+ * Expose an average response time in milliseconds
+ */
+ Object.defineProperty(helpers, "averageResponseTime", {
+ get: function () {
+ return averageOver === 0 ?
+ undefined :
+ Math.round(100 * totalResponseTime / averageOver) / 100;
+ },
+ enumerable: true
+ });
+
+/**
+ * Expose a maximum response time in milliseconds
+ */
+ Object.defineProperty(helpers, "maxResponseTime", {
+ get: function () { return Math.round(maxResponseTime * 100) / 100; },
+ enumerable: true
+ });
+
+/**
+ * Expose the name of the test that provided the maximum response time
+ */
+ Object.defineProperty(helpers, "maxResponseCulprit", {
+ get: function () { return maxResponseCulprit; },
+ enumerable: true
+ });
+
+/**
+ * Quick summary of the times
+ */
+ Object.defineProperty(helpers, "timingSummary", {
+ get: function () {
+ var elapsed = (new Date().getTime() - start) / 1000;
+ return "Total " + elapsed + "s, " +
+ "ave response " + helpers.averageResponseTime + "ms, " +
+ "max response " + helpers.maxResponseTime + "ms " +
+ "from '" + helpers.maxResponseCulprit + "'";
+ },
+ enumerable: true
+ });
+
+/**
+ * A way of turning a set of tests into something more declarative, this helps
+ * to allow tests to be asynchronous.
+ * @param audits An array of objects each of which contains:
+ * - setup: string/function to be called to set the test up.
+ * If audit is a string then it is passed to helpers.setInput().
+ * If audit is a function then it is executed. The tests will wait while
+ * tests that return promises complete.
+ * - name: For debugging purposes. If name is undefined, and 'setup'
+ * is a string then the setup value will be used automatically
+ * - skipIf: A function to define if the test should be skipped. Useful for
+ * excluding tests from certain environments (e.g. nodom, firefox, etc).
+ * The name of the test will be used in log messages noting the skip
+ * See helpers.reason for pre-defined skip functions. The skip function must
+ * be synchronous, and will be passed the test options object.
+ * - skipRemainingIf: A function to skip all the remaining audits in this set.
+ * See skipIf for details of how skip functions work.
+ * - check: Check data. Available checks:
+ * - input: The text displayed in the input field
+ * - cursor: The position of the start of the cursor
+ * - status: One of 'VALID', 'ERROR', 'INCOMPLETE'
+ * - hints: The hint text, i.e. a concatenation of the directTabText, the
+ * emptyParameters and the arrowTabText. The text as inserted into the UI
+ * will include NBSP and Unicode RARR characters, these should be
+ * represented using normal space and '->' for the arrow
+ * - markup: What state should the error markup be in. e.g. 'VVVIIIEEE'
+ * - args: Maps of checks to make against the arguments:
+ * - value: i.e. assignment.value (which ignores defaultValue)
+ * - type: Argument/BlankArgument/MergedArgument/etc i.e. what's assigned
+ * Care should be taken with this since it's something of an
+ * implementation detail
+ * - arg: The toString value of the argument
+ * - status: i.e. assignment.getStatus
+ * - message: i.e. assignment.message
+ * - name: For commands - checks assignment.value.name
+ * - exec: Object to indicate we should execute the command and check the
+ * results. Available checks:
+ * - output: A string, RegExp or array of RegExps to compare with the output
+ * If typeof output is a string then the output should be exactly equal
+ * to the given string. If the type of output is a RegExp or array of
+ * RegExps then the output should match all RegExps
+ * - error: If true, then it is expected that this command will fail (that
+ * is, return a rejected promise or throw an exception)
+ * - type: A string documenting the expected type of the return value
+ * - post: Function to be called after the checks have been run, which will be
+ * passed 2 parameters: the first being output data (with type, data, and
+ * error properties), and the second being the converted text version of
+ * the output data
+ */
+ helpers.audit = function (options, audits) {
+ checkOptions(options);
+ var skipReason = null;
+ return util.promiseEach(audits, function (audit) {
+ var name = audit.name;
+ if (name == null && typeof audit.setup === "string") {
+ name = audit.setup;
+ }
+
+ if (assert.testLogging) {
+ log("- START '" + name + "' in " + assert.currentTest);
+ }
+
+ if (audit.skipRemainingIf) {
+ var skipRemainingIf = (typeof audit.skipRemainingIf === "function") ?
+ audit.skipRemainingIf(options) :
+ !!audit.skipRemainingIf;
+ if (skipRemainingIf) {
+ skipReason = audit.skipRemainingIf.name ?
+ "due to " + audit.skipRemainingIf.name :
+ "";
+ assert.log("Skipped " + name + " " + skipReason);
+
+ // Tests need at least one pass, fail or todo. Create a dummy pass
+ assert.ok(true, "Each test requires at least one pass, fail or todo");
+
+ return Promise.resolve(undefined);
+ }
+ }
+
+ if (audit.skipIf) {
+ var skip = (typeof audit.skipIf === "function") ?
+ audit.skipIf(options) :
+ !!audit.skipIf;
+ if (skip) {
+ var reason = audit.skipIf.name ? "due to " + audit.skipIf.name : "";
+ assert.log("Skipped " + name + " " + reason);
+ return Promise.resolve(undefined);
+ }
+ }
+
+ if (skipReason != null) {
+ assert.log("Skipped " + name + " " + skipReason);
+ return Promise.resolve(undefined);
+ }
+
+ var start = new Date().getTime();
+
+ var setupDone = helpers._setup(options, name, audit);
+ return setupDone.then(function (chunkLen) {
+ if (typeof chunkLen !== "number") {
+ chunkLen = 1;
+ }
+
+ // Nasty hack to allow us to auto-skip tests where we're actually testing
+ // a key-sequence (i.e. targeting terminal.js) when there is no terminal
+ if (chunkLen === -1) {
+ assert.log("Skipped " + name + " " + skipReason);
+ return Promise.resolve(undefined);
+ }
+
+ if (assert.currentTest) {
+ var responseTime = (new Date().getTime() - start) / chunkLen;
+ totalResponseTime += responseTime;
+ if (responseTime > maxResponseTime) {
+ maxResponseTime = responseTime;
+ maxResponseCulprit = assert.currentTest + "/" + name;
+ }
+ averageOver++;
+ }
+
+ var checkDone = helpers._check(options, name, audit.check);
+ return checkDone.then(function () {
+ var execDone = helpers._exec(options, name, audit.exec);
+ return execDone.then(function (data) {
+ return helpers._post(name, audit, data).then(function () {
+ if (assert.testLogging) {
+ log("- END '" + name + "' in " + assert.currentTest);
+ }
+ });
+ });
+ });
+ });
+ }).then(function () {
+ return options.automator.setInput("");
+ }, function (ex) {
+ options.automator.setInput("");
+ throw ex;
+ });
+ };
+
+/**
+ * Compare 2 arrays.
+ */
+ helpers.arrayIs = function (actual, expected, message) {
+ assert.ok(Array.isArray(actual), "actual is not an array: " + message);
+ assert.ok(Array.isArray(expected), "expected is not an array: " + message);
+
+ if (!Array.isArray(actual) || !Array.isArray(expected)) {
+ return;
+ }
+
+ assert.is(actual.length, expected.length, "array length: " + message);
+
+ for (var i = 0; i < actual.length && i < expected.length; i++) {
+ assert.is(actual[i], expected[i], "member[" + i + "]: " + message);
+ }
+ };
+
+/**
+ * A quick helper to log to the correct place
+ */
+ function log(message) {
+ if (typeof info === "function") {
+ info(message);
+ }
+ else {
+ console.log(message);
+ }
+ }
+
+ return { helpers: helpers, assert: assert };
+})();
diff --git a/devtools/client/commandline/test/mockCommands.js b/devtools/client/commandline/test/mockCommands.js
new file mode 100644
index 000000000..82cc7e384
--- /dev/null
+++ b/devtools/client/commandline/test/mockCommands.js
@@ -0,0 +1,794 @@
+/*
+ * Copyright 2012, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+"use strict";
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
+
+var mockCommands;
+if (typeof exports !== "undefined") {
+ // If we're being loaded via require();
+ mockCommands = exports;
+}
+else {
+ // If we're being loaded via loadScript in mochitest
+ mockCommands = {};
+}
+
+// We use an alias for exports here because this module is used in Firefox
+// mochitests where we don't have define/require
+
+/**
+ * Registration and de-registration.
+ */
+mockCommands.setup = function (requisition) {
+ requisition.system.addItems(mockCommands.items);
+};
+
+mockCommands.shutdown = function (requisition) {
+ requisition.system.removeItems(mockCommands.items);
+};
+
+function createExec(name) {
+ return function (args, context) {
+ var promises = [];
+
+ Object.keys(args).map(function (argName) {
+ var value = args[argName];
+ var type = this.getParameterByName(argName).type;
+ var promise = Promise.resolve(type.stringify(value, context));
+ promises.push(promise.then(function (str) {
+ return { name: argName, value: str };
+ }.bind(this)));
+ }.bind(this));
+
+ return Promise.all(promises).then(function (data) {
+ var argValues = {};
+ data.forEach(function (entry) { argValues[entry.name] = entry.value; });
+
+ return context.typedData("testCommandOutput", {
+ name: name,
+ args: argValues
+ });
+ }.bind(this));
+ };
+}
+
+mockCommands.items = [
+ {
+ item: "converter",
+ from: "testCommandOutput",
+ to: "dom",
+ exec: function (testCommandOutput, context) {
+ var view = context.createView({
+ data: testCommandOutput,
+ html: "" +
+ "<table>" +
+ "<thead>" +
+ "<tr>" +
+ '<th colspan="3">Exec: ${name}</th>' +
+ "</tr>" +
+ "</thead>" +
+ "<tbody>" +
+ '<tr foreach="key in ${args}">' +
+ "<td> ${key}</td>" +
+ "<td>=</td>" +
+ "<td>${args[key]}</td>" +
+ "</tr>" +
+ "</tbody>" +
+ "</table>",
+ options: {
+ allowEval: true
+ }
+ });
+
+ return view.toDom(context.document);
+ }
+ },
+ {
+ item: "converter",
+ from: "testCommandOutput",
+ to: "string",
+ exec: function (testCommandOutput, context) {
+ var argsOut = Object.keys(testCommandOutput.args).map(function (key) {
+ return key + "=" + testCommandOutput.args[key];
+ }).join(" ");
+ return "Exec: " + testCommandOutput.name + " " + argsOut;
+ }
+ },
+ {
+ item: "type",
+ name: "optionType",
+ parent: "selection",
+ lookup: [
+ {
+ name: "option1",
+ value: "string"
+ },
+ {
+ name: "option2",
+ value: "number"
+ },
+ {
+ name: "option3",
+ value: {
+ name: "selection",
+ lookup: [
+ { name: "one", value: 1 },
+ { name: "two", value: 2 },
+ { name: "three", value: 3 }
+ ]
+ }
+ }
+ ]
+ },
+ {
+ item: "type",
+ name: "optionValue",
+ parent: "delegate",
+ delegateType: function (executionContext) {
+ if (executionContext != null) {
+ var option = executionContext.getArgsObject().optionType;
+ if (option != null) {
+ return option;
+ }
+ }
+ return "blank";
+ }
+ },
+ {
+ item: "command",
+ name: "tsv",
+ params: [
+ { name: "optionType", type: "optionType" },
+ { name: "optionValue", type: "optionValue" }
+ ],
+ exec: createExec("tsv")
+ },
+ {
+ item: "command",
+ name: "tsr",
+ params: [ { name: "text", type: "string" } ],
+ exec: createExec("tsr")
+ },
+ {
+ item: "command",
+ name: "tsrsrsr",
+ params: [
+ { name: "p1", type: "string" },
+ { name: "p2", type: "string" },
+ { name: "p3", type: { name: "string", allowBlank: true} },
+ ],
+ exec: createExec("tsrsrsr")
+ },
+ {
+ item: "command",
+ name: "tso",
+ params: [ { name: "text", type: "string", defaultValue: null } ],
+ exec: createExec("tso")
+ },
+ {
+ item: "command",
+ name: "tse",
+ params: [
+ { name: "node", type: "node" },
+ {
+ group: "options",
+ params: [
+ { name: "nodes", type: { name: "nodelist" } },
+ { name: "nodes2", type: { name: "nodelist", allowEmpty: true } }
+ ]
+ }
+ ],
+ exec: createExec("tse")
+ },
+ {
+ item: "command",
+ name: "tsj",
+ params: [ { name: "javascript", type: "javascript" } ],
+ exec: createExec("tsj")
+ },
+ {
+ item: "command",
+ name: "tsb",
+ params: [ { name: "toggle", type: "boolean" } ],
+ exec: createExec("tsb")
+ },
+ {
+ item: "command",
+ name: "tss",
+ exec: createExec("tss")
+ },
+ {
+ item: "command",
+ name: "tsu",
+ params: [
+ {
+ name: "num",
+ type: {
+ name: "number",
+ max: 10,
+ min: -5,
+ step: 3
+ }
+ }
+ ],
+ exec: createExec("tsu")
+ },
+ {
+ item: "command",
+ name: "tsf",
+ params: [
+ {
+ name: "num",
+ type: {
+ name: "number",
+ allowFloat: true,
+ max: 11.5,
+ min: -6.5,
+ step: 1.5
+ }
+ }
+ ],
+ exec: createExec("tsf")
+ },
+ {
+ item: "command",
+ name: "tsn"
+ },
+ {
+ item: "command",
+ name: "tsn dif",
+ params: [ { name: "text", type: "string", description: "tsn dif text" } ],
+ exec: createExec("tsnDif")
+ },
+ {
+ item: "command",
+ name: "tsn hidden",
+ hidden: true,
+ exec: createExec("tsnHidden")
+ },
+ {
+ item: "command",
+ name: "tsn ext",
+ params: [ { name: "text", type: "string" } ],
+ exec: createExec("tsnExt")
+ },
+ {
+ item: "command",
+ name: "tsn exte",
+ params: [ { name: "text", type: "string" } ],
+ exec: createExec("tsnExte")
+ },
+ {
+ item: "command",
+ name: "tsn exten",
+ params: [ { name: "text", type: "string" } ],
+ exec: createExec("tsnExten")
+ },
+ {
+ item: "command",
+ name: "tsn extend",
+ params: [ { name: "text", type: "string" } ],
+ exec: createExec("tsnExtend")
+ },
+ {
+ item: "command",
+ name: "tsn deep"
+ },
+ {
+ item: "command",
+ name: "tsn deep down"
+ },
+ {
+ item: "command",
+ name: "tsn deep down nested"
+ },
+ {
+ item: "command",
+ name: "tsn deep down nested cmd",
+ exec: createExec("tsnDeepDownNestedCmd")
+ },
+ {
+ item: "command",
+ name: "tshidden",
+ hidden: true,
+ params: [
+ {
+ group: "Options",
+ params: [
+ {
+ name: "visible",
+ type: "string",
+ short: "v",
+ defaultValue: null,
+ description: "visible"
+ },
+ {
+ name: "invisiblestring",
+ type: "string",
+ short: "i",
+ description: "invisiblestring",
+ defaultValue: null,
+ hidden: true
+ },
+ {
+ name: "invisibleboolean",
+ short: "b",
+ type: "boolean",
+ description: "invisibleboolean",
+ hidden: true
+ }
+ ]
+ }
+ ],
+ exec: createExec("tshidden")
+ },
+ {
+ item: "command",
+ name: "tselarr",
+ params: [
+ { name: "num", type: { name: "selection", data: [ "1", "2", "3" ] } },
+ { name: "arr", type: { name: "array", subtype: "string" } }
+ ],
+ exec: createExec("tselarr")
+ },
+ {
+ item: "command",
+ name: "tsm",
+ description: "a 3-param test selection|string|number",
+ params: [
+ { name: "abc", type: { name: "selection", data: [ "a", "b", "c" ] } },
+ { name: "txt", type: "string" },
+ { name: "num", type: { name: "number", max: 42, min: 0 } }
+ ],
+ exec: createExec("tsm")
+ },
+ {
+ item: "command",
+ name: "tsg",
+ description: "a param group test",
+ params: [
+ {
+ name: "solo",
+ type: { name: "selection", data: [ "aaa", "bbb", "ccc" ] },
+ description: "solo param"
+ },
+ {
+ group: "First",
+ params: [
+ {
+ name: "txt1",
+ type: "string",
+ defaultValue: null,
+ description: "txt1 param"
+ },
+ {
+ name: "bool",
+ type: "boolean",
+ description: "bool param"
+ }
+ ]
+ },
+ {
+ name: "txt2",
+ type: "string",
+ defaultValue: "d",
+ description: "txt2 param",
+ option: "Second"
+ },
+ {
+ name: "num",
+ type: { name: "number", min: 40 },
+ defaultValue: 42,
+ description: "num param",
+ option: "Second"
+ }
+ ],
+ exec: createExec("tsg")
+ },
+ {
+ item: "command",
+ name: "tscook",
+ description: "param group test to catch problems with cookie command",
+ params: [
+ {
+ name: "key",
+ type: "string",
+ description: "tscookKeyDesc"
+ },
+ {
+ name: "value",
+ type: "string",
+ description: "tscookValueDesc"
+ },
+ {
+ group: "tscookOptionsDesc",
+ params: [
+ {
+ name: "path",
+ type: "string",
+ defaultValue: "/",
+ description: "tscookPathDesc"
+ },
+ {
+ name: "domain",
+ type: "string",
+ defaultValue: null,
+ description: "tscookDomainDesc"
+ },
+ {
+ name: "secure",
+ type: "boolean",
+ description: "tscookSecureDesc"
+ }
+ ]
+ }
+ ],
+ exec: createExec("tscook")
+ },
+ {
+ item: "command",
+ name: "tslong",
+ description: "long param tests to catch problems with the jsb command",
+ params: [
+ {
+ name: "msg",
+ type: "string",
+ description: "msg Desc"
+ },
+ {
+ group: "Options Desc",
+ params: [
+ {
+ name: "num",
+ short: "n",
+ type: "number",
+ description: "num Desc",
+ defaultValue: 2
+ },
+ {
+ name: "sel",
+ short: "s",
+ type: {
+ name: "selection",
+ lookup: [
+ { name: "space", value: " " },
+ { name: "tab", value: "\t" }
+ ]
+ },
+ description: "sel Desc",
+ defaultValue: " "
+ },
+ {
+ name: "bool",
+ short: "b",
+ type: "boolean",
+ description: "bool Desc"
+ },
+ {
+ name: "num2",
+ short: "m",
+ type: "number",
+ description: "num2 Desc",
+ defaultValue: -1
+ },
+ {
+ name: "bool2",
+ short: "c",
+ type: "boolean",
+ description: "bool2 Desc"
+ },
+ {
+ name: "sel2",
+ short: "t",
+ type: {
+ name: "selection",
+ data: [ "collapse", "basic", "with space", "with two spaces" ]
+ },
+ description: "sel2 Desc",
+ defaultValue: "collapse"
+ }
+ ]
+ }
+ ],
+ exec: createExec("tslong")
+ },
+ {
+ item: "command",
+ name: "tsdate",
+ description: "long param tests to catch problems with the jsb command",
+ params: [
+ {
+ name: "d1",
+ type: "date",
+ },
+ {
+ name: "d2",
+ type: {
+ name: "date",
+ min: "1 jan 2000",
+ max: "28 feb 2000",
+ step: 2
+ }
+ },
+ ],
+ exec: createExec("tsdate")
+ },
+ {
+ item: "command",
+ name: "tsfail",
+ description: "test errors",
+ params: [
+ {
+ name: "method",
+ type: {
+ name: "selection",
+ data: [
+ "reject", "rejecttyped",
+ "throwerror", "throwstring", "throwinpromise",
+ "noerror"
+ ]
+ }
+ }
+ ],
+ exec: function (args, context) {
+ if (args.method === "reject") {
+ return new Promise(function (resolve, reject) {
+ context.environment.window.setTimeout(function () {
+ reject("rejected promise");
+ }, 10);
+ });
+ }
+
+ if (args.method === "rejecttyped") {
+ return new Promise(function (resolve, reject) {
+ context.environment.window.setTimeout(function () {
+ reject(context.typedData("number", 54));
+ }, 10);
+ });
+ }
+
+ if (args.method === "throwinpromise") {
+ return new Promise(function (resolve, reject) {
+ context.environment.window.setTimeout(function () {
+ resolve("should be lost");
+ }, 10);
+ }).then(function () {
+ var t = null;
+ return t.foo;
+ });
+ }
+
+ if (args.method === "throwerror") {
+ throw new Error("thrown error");
+ }
+
+ if (args.method === "throwstring") {
+ throw "thrown string";
+ }
+
+ return "no error";
+ }
+ },
+ {
+ item: "command",
+ name: "tsfile",
+ description: "test file params",
+ },
+ {
+ item: "command",
+ name: "tsfile open",
+ description: "a file param in open mode",
+ params: [
+ {
+ name: "p1",
+ type: {
+ name: "file",
+ filetype: "file",
+ existing: "yes"
+ }
+ }
+ ],
+ exec: createExec("tsfile open")
+ },
+ {
+ item: "command",
+ name: "tsfile saveas",
+ description: "a file param in saveas mode",
+ params: [
+ {
+ name: "p1",
+ type: {
+ name: "file",
+ filetype: "file",
+ existing: "no"
+ }
+ }
+ ],
+ exec: createExec("tsfile saveas")
+ },
+ {
+ item: "command",
+ name: "tsfile save",
+ description: "a file param in save mode",
+ params: [
+ {
+ name: "p1",
+ type: {
+ name: "file",
+ filetype: "file",
+ existing: "maybe"
+ }
+ }
+ ],
+ exec: createExec("tsfile save")
+ },
+ {
+ item: "command",
+ name: "tsfile cd",
+ description: "a file param in cd mode",
+ params: [
+ {
+ name: "p1",
+ type: {
+ name: "file",
+ filetype: "directory",
+ existing: "yes"
+ }
+ }
+ ],
+ exec: createExec("tsfile cd")
+ },
+ {
+ item: "command",
+ name: "tsfile mkdir",
+ description: "a file param in mkdir mode",
+ params: [
+ {
+ name: "p1",
+ type: {
+ name: "file",
+ filetype: "directory",
+ existing: "no"
+ }
+ }
+ ],
+ exec: createExec("tsfile mkdir")
+ },
+ {
+ item: "command",
+ name: "tsfile rm",
+ description: "a file param in rm mode",
+ params: [
+ {
+ name: "p1",
+ type: {
+ name: "file",
+ filetype: "any",
+ existing: "yes"
+ }
+ }
+ ],
+ exec: createExec("tsfile rm")
+ },
+ {
+ item: "command",
+ name: "tsslow",
+ params: [
+ {
+ name: "hello",
+ type: {
+ name: "selection",
+ data: function (context) {
+ return new Promise(function (resolve, reject) {
+ context.environment.window.setTimeout(function () {
+ resolve([
+ "Shalom", "Namasté", "Hallo", "Dydd-da",
+ "Chào", "Hej", "Saluton", "Sawubona"
+ ]);
+ }, 10);
+ });
+ }
+ }
+ }
+ ],
+ exec: function (args, context) {
+ return "Test completed";
+ }
+ },
+ {
+ item: "command",
+ name: "urlc",
+ params: [
+ {
+ name: "url",
+ type: "url"
+ }
+ ],
+ returnType: "json",
+ exec: function (args, context) {
+ return args;
+ }
+ },
+ {
+ item: "command",
+ name: "unionc1",
+ params: [
+ {
+ name: "first",
+ type: {
+ name: "union",
+ alternatives: [
+ {
+ name: "selection",
+ lookup: [
+ { name: "one", value: 1 },
+ { name: "two", value: 2 },
+ ]
+ },
+ "number",
+ { name: "string" }
+ ]
+ }
+ }
+ ],
+ returnType: "json",
+ exec: function (args, context) {
+ return args;
+ }
+ },
+ {
+ item: "command",
+ name: "unionc2",
+ params: [
+ {
+ name: "first",
+ type: {
+ name: "union",
+ alternatives: [
+ {
+ name: "selection",
+ lookup: [
+ { name: "one", value: 1 },
+ { name: "two", value: 2 },
+ ]
+ },
+ {
+ name: "url"
+ }
+ ]
+ }
+ }
+ ],
+ returnType: "json",
+ exec: function (args, context) {
+ return args;
+ }
+ },
+ {
+ item: "command",
+ name: "tsres",
+ params: [
+ {
+ name: "resource",
+ type: "resource"
+ }
+ ],
+ exec: createExec("tsres"),
+ }
+];
diff --git a/devtools/client/debugger/content/actions/breakpoints.js b/devtools/client/debugger/content/actions/breakpoints.js
new file mode 100644
index 000000000..5c5552d78
--- /dev/null
+++ b/devtools/client/debugger/content/actions/breakpoints.js
@@ -0,0 +1,191 @@
+/* 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 constants = require("../constants");
+const promise = require("promise");
+const { asPaused } = require("../utils");
+const { PROMISE } = require("devtools/client/shared/redux/middleware/promise");
+const {
+ getSource, getBreakpoint, getBreakpoints, makeLocationId
+} = require("../queries");
+const { Task } = require("devtools/shared/task");
+
+// Because breakpoints are just simple data structures, we still need
+// a way to lookup the actual client instance to talk to the server.
+// We keep an internal database of clients based off of actor ID.
+const BREAKPOINT_CLIENT_STORE = new Map();
+
+function setBreakpointClient(actor, client) {
+ BREAKPOINT_CLIENT_STORE.set(actor, client);
+}
+
+function getBreakpointClient(actor) {
+ return BREAKPOINT_CLIENT_STORE.get(actor);
+}
+
+function enableBreakpoint(location) {
+ // Enabling is exactly the same as adding. It will use the existing
+ // breakpoint that still stored.
+ return addBreakpoint(location);
+}
+
+function _breakpointExists(state, location) {
+ const currentBp = getBreakpoint(state, location);
+ return currentBp && !currentBp.disabled;
+}
+
+function _getOrCreateBreakpoint(state, location, condition) {
+ return getBreakpoint(state, location) || { location, condition };
+}
+
+function addBreakpoint(location, condition) {
+ return (dispatch, getState) => {
+ if (_breakpointExists(getState(), location)) {
+ return;
+ }
+
+ const bp = _getOrCreateBreakpoint(getState(), location, condition);
+
+ return dispatch({
+ type: constants.ADD_BREAKPOINT,
+ breakpoint: bp,
+ condition: condition,
+ [PROMISE]: Task.spawn(function* () {
+ const sourceClient = gThreadClient.source(
+ getSource(getState(), bp.location.actor)
+ );
+ const [response, bpClient] = yield sourceClient.setBreakpoint({
+ line: bp.location.line,
+ column: bp.location.column,
+ condition: bp.condition
+ });
+ const { isPending, actualLocation } = response;
+
+ // Save the client instance
+ setBreakpointClient(bpClient.actor, bpClient);
+
+ return {
+ text: DebuggerView.editor.getText(
+ (actualLocation ? actualLocation.line : bp.location.line) - 1
+ ).trim(),
+
+ // If the breakpoint response has an "actualLocation" attached, then
+ // the original requested placement for the breakpoint wasn't
+ // accepted.
+ actualLocation: isPending ? null : actualLocation,
+ actor: bpClient.actor
+ };
+ })
+ });
+ };
+}
+
+function disableBreakpoint(location) {
+ return _removeOrDisableBreakpoint(location, true);
+}
+
+function removeBreakpoint(location) {
+ return _removeOrDisableBreakpoint(location);
+}
+
+function _removeOrDisableBreakpoint(location, isDisabled) {
+ return (dispatch, getState) => {
+ let bp = getBreakpoint(getState(), location);
+ if (!bp) {
+ throw new Error("attempt to remove breakpoint that does not exist");
+ }
+ if (bp.loading) {
+ // TODO(jwl): make this wait until the breakpoint is saved if it
+ // is still loading
+ throw new Error("attempt to remove unsaved breakpoint");
+ }
+
+ const bpClient = getBreakpointClient(bp.actor);
+ const action = {
+ type: constants.REMOVE_BREAKPOINT,
+ breakpoint: bp,
+ disabled: isDisabled
+ };
+
+ // If the breakpoint is already disabled, we don't need to remove
+ // it from the server. We just need to dispatch an action
+ // simulating a successful server request to remove it, and it
+ // will be removed completely from the state.
+ if (!bp.disabled) {
+ return dispatch(Object.assign({}, action, {
+ [PROMISE]: bpClient.remove()
+ }));
+ } else {
+ return dispatch(Object.assign({}, action, { status: "done" }));
+ }
+ };
+}
+
+function removeAllBreakpoints() {
+ return (dispatch, getState) => {
+ const breakpoints = getBreakpoints(getState());
+ const activeBreakpoints = breakpoints.filter(bp => !bp.disabled);
+ activeBreakpoints.forEach(bp => removeBreakpoint(bp.location));
+ };
+}
+
+/**
+ * Update the condition of a breakpoint.
+ *
+ * @param object aLocation
+ * @see DebuggerController.Breakpoints.addBreakpoint
+ * @param string aClients
+ * The condition to set on the breakpoint
+ * @return object
+ * A promise that will be resolved with the breakpoint client
+ */
+function setBreakpointCondition(location, condition) {
+ return (dispatch, getState) => {
+ const bp = getBreakpoint(getState(), location);
+ if (!bp) {
+ throw new Error("Breakpoint does not exist at the specified location");
+ }
+ if (bp.loading) {
+ // TODO(jwl): when this function is called, make sure the action
+ // creator waits for the breakpoint to exist
+ throw new Error("breakpoint must be saved");
+ }
+
+ const bpClient = getBreakpointClient(bp.actor);
+ const action = {
+ type: constants.SET_BREAKPOINT_CONDITION,
+ breakpoint: bp,
+ condition: condition
+ };
+
+ // If it's not disabled, we need to update the condition on the
+ // server. Otherwise, just dispatch a non-remote action that
+ // updates the condition locally.
+ if (!bp.disabled) {
+ return dispatch(Object.assign({}, action, {
+ [PROMISE]: Task.spawn(function* () {
+ const newClient = yield bpClient.setCondition(gThreadClient, condition);
+
+ // Remove the old instance and save the new one
+ setBreakpointClient(bpClient.actor, null);
+ setBreakpointClient(newClient.actor, newClient);
+
+ return { actor: newClient.actor };
+ })
+ }));
+ } else {
+ return dispatch(action);
+ }
+ };
+}
+
+module.exports = {
+ enableBreakpoint,
+ addBreakpoint,
+ disableBreakpoint,
+ removeBreakpoint,
+ removeAllBreakpoints,
+ setBreakpointCondition
+};
diff --git a/devtools/client/debugger/content/actions/event-listeners.js b/devtools/client/debugger/content/actions/event-listeners.js
new file mode 100644
index 000000000..4bca557fe
--- /dev/null
+++ b/devtools/client/debugger/content/actions/event-listeners.js
@@ -0,0 +1,118 @@
+/* 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 constants = require("../constants");
+const { asPaused } = require("../utils");
+const { reportException } = require("devtools/shared/DevToolsUtils");
+const { setNamedTimeout } = require("devtools/client/shared/widgets/view-helpers");
+const { Task } = require("devtools/shared/task");
+
+const FETCH_EVENT_LISTENERS_DELAY = 200; // ms
+
+function fetchEventListeners() {
+ return (dispatch, getState) => {
+ // Make sure we"re not sending a batch of closely repeated requests.
+ // This can easily happen whenever new sources are fetched.
+ setNamedTimeout("event-listeners-fetch", FETCH_EVENT_LISTENERS_DELAY, () => {
+ // In case there is still a request of listeners going on (it
+ // takes several RDP round trips right now), make sure we wait
+ // on a currently running request
+ if (getState().eventListeners.fetchingListeners) {
+ dispatch({
+ type: services.WAIT_UNTIL,
+ predicate: action => (
+ action.type === constants.FETCH_EVENT_LISTENERS &&
+ action.status === "done"
+ ),
+ run: dispatch => dispatch(fetchEventListeners())
+ });
+ return;
+ }
+
+ dispatch({
+ type: constants.FETCH_EVENT_LISTENERS,
+ status: "begin"
+ });
+
+ asPaused(gThreadClient, _getListeners).then(listeners => {
+ // Notify that event listeners were fetched and shown in the view,
+ // and callback to resume the active thread if necessary.
+ window.emit(EVENTS.EVENT_LISTENERS_FETCHED);
+
+ dispatch({
+ type: constants.FETCH_EVENT_LISTENERS,
+ status: "done",
+ listeners: listeners
+ });
+ });
+ });
+ };
+}
+
+const _getListeners = Task.async(function* () {
+ const response = yield gThreadClient.eventListeners();
+
+ // Make sure all the listeners are sorted by the event type, since
+ // they"re not guaranteed to be clustered together.
+ response.listeners.sort((a, b) => a.type > b.type ? 1 : -1);
+
+ // Add all the listeners in the debugger view event linsteners container.
+ let fetchedDefinitions = new Map();
+ let listeners = [];
+ for (let listener of response.listeners) {
+ let definitionSite;
+ if (fetchedDefinitions.has(listener.function.actor)) {
+ definitionSite = fetchedDefinitions.get(listener.function.actor);
+ } else if (listener.function.class == "Function") {
+ definitionSite = yield _getDefinitionSite(listener.function);
+ if (!definitionSite) {
+ // We don"t know where this listener comes from so don"t show it in
+ // the UI as breaking on it doesn"t work (bug 942899).
+ continue;
+ }
+
+ fetchedDefinitions.set(listener.function.actor, definitionSite);
+ }
+ listener.function.url = definitionSite;
+ listeners.push(listener);
+ }
+ fetchedDefinitions.clear();
+
+ return listeners;
+});
+
+const _getDefinitionSite = Task.async(function* (aFunction) {
+ const grip = gThreadClient.pauseGrip(aFunction);
+ let response;
+
+ try {
+ response = yield grip.getDefinitionSite();
+ }
+ catch (e) {
+ // Don't make this error fatal, because it would break the entire events pane.
+ reportException("_getDefinitionSite", e);
+ return null;
+ }
+
+ return response.source.url;
+});
+
+function updateEventBreakpoints(eventNames) {
+ return dispatch => {
+ setNamedTimeout("event-breakpoints-update", 0, () => {
+ gThreadClient.pauseOnDOMEvents(eventNames, function () {
+ // Notify that event breakpoints were added/removed on the server.
+ window.emit(EVENTS.EVENT_BREAKPOINTS_UPDATED);
+
+ dispatch({
+ type: constants.UPDATE_EVENT_BREAKPOINTS,
+ eventNames: eventNames
+ });
+ });
+ });
+ };
+}
+
+module.exports = { updateEventBreakpoints, fetchEventListeners };
diff --git a/devtools/client/debugger/content/actions/moz.build b/devtools/client/debugger/content/actions/moz.build
new file mode 100644
index 000000000..13a2dd9ad
--- /dev/null
+++ b/devtools/client/debugger/content/actions/moz.build
@@ -0,0 +1,10 @@
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'breakpoints.js',
+ 'event-listeners.js',
+ 'sources.js'
+)
diff --git a/devtools/client/debugger/content/actions/sources.js b/devtools/client/debugger/content/actions/sources.js
new file mode 100644
index 000000000..d7e0728e7
--- /dev/null
+++ b/devtools/client/debugger/content/actions/sources.js
@@ -0,0 +1,280 @@
+/* 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 constants = require("../constants");
+const promise = require("promise");
+const Services = require("Services");
+const { dumpn } = require("devtools/shared/DevToolsUtils");
+const { PROMISE, HISTOGRAM_ID } = require("devtools/client/shared/redux/middleware/promise");
+const { getSource, getSourceText } = require("../queries");
+const { Task } = require("devtools/shared/task");
+
+const NEW_SOURCE_IGNORED_URLS = ["debugger eval code", "XStringBundle"];
+const FETCH_SOURCE_RESPONSE_DELAY = 200; // ms
+
+function getSourceClient(source) {
+ return gThreadClient.source(source);
+}
+
+/**
+ * Handler for the debugger client's unsolicited newSource notification.
+ */
+function newSource(source) {
+ return dispatch => {
+ // Ignore bogus scripts, e.g. generated from 'clientEvaluate' packets.
+ if (NEW_SOURCE_IGNORED_URLS.indexOf(source.url) != -1) {
+ return;
+ }
+
+ // Signal that a new source has been added.
+ window.emit(EVENTS.NEW_SOURCE);
+
+ return dispatch({
+ type: constants.ADD_SOURCE,
+ source: source
+ });
+ };
+}
+
+function selectSource(source, opts) {
+ return (dispatch, getState) => {
+ if (!gThreadClient) {
+ // No connection, do nothing. This happens when the debugger is
+ // shut down too fast and it tries to display a default source.
+ return;
+ }
+
+ source = getSource(getState(), source.actor);
+
+ // Make sure to start a request to load the source text.
+ dispatch(loadSourceText(source));
+
+ dispatch({
+ type: constants.SELECT_SOURCE,
+ source: source,
+ opts: opts
+ });
+ };
+}
+
+function loadSources() {
+ return {
+ type: constants.LOAD_SOURCES,
+ [PROMISE]: Task.spawn(function* () {
+ const response = yield gThreadClient.getSources();
+
+ // Top-level breakpoints may pause the entire loading process
+ // because scripts are executed as they are loaded, so the
+ // engine may pause in the middle of loading all the sources.
+ // This is relatively harmless, as individual `newSource`
+ // notifications are fired for each script and they will be
+ // added to the UI through that.
+ if (!response.sources) {
+ dumpn(
+ "Error getting sources, probably because a top-level " +
+ "breakpoint was hit while executing them"
+ );
+ return;
+ }
+
+ // Ignore bogus scripts, e.g. generated from 'clientEvaluate' packets.
+ return response.sources.filter(source => {
+ return NEW_SOURCE_IGNORED_URLS.indexOf(source.url) === -1;
+ });
+ })
+ };
+}
+
+/**
+ * Set the black boxed status of the given source.
+ *
+ * @param Object aSource
+ * The source form.
+ * @param bool aBlackBoxFlag
+ * True to black box the source, false to un-black box it.
+ * @returns Promise
+ * A promize that resolves to [aSource, isBlackBoxed] or rejects to
+ * [aSource, error].
+ */
+function blackbox(source, shouldBlackBox) {
+ const client = getSourceClient(source);
+
+ return {
+ type: constants.BLACKBOX,
+ source: source,
+ [PROMISE]: Task.spawn(function* () {
+ yield shouldBlackBox ? client.blackBox() : client.unblackBox();
+ return {
+ isBlackBoxed: shouldBlackBox
+ };
+ })
+ };
+}
+
+/**
+ * Toggle the pretty printing of a source's text. All subsequent calls to
+ * |getText| will return the pretty-toggled text. Nothing will happen for
+ * non-javascript files.
+ *
+ * @param Object aSource
+ * The source form from the RDP.
+ * @returns Promise
+ * A promise that resolves to [aSource, prettyText] or rejects to
+ * [aSource, error].
+ */
+function togglePrettyPrint(source) {
+ return (dispatch, getState) => {
+ const sourceClient = getSourceClient(source);
+ const wantPretty = !source.isPrettyPrinted;
+
+ return dispatch({
+ type: constants.TOGGLE_PRETTY_PRINT,
+ source: source,
+ [PROMISE]: Task.spawn(function* () {
+ let response;
+
+ // Only attempt to pretty print JavaScript sources.
+ const sourceText = getSourceText(getState(), source.actor);
+ const contentType = sourceText ? sourceText.contentType : null;
+ if (!SourceUtils.isJavaScript(source.url, contentType)) {
+ throw new Error("Can't prettify non-javascript files.");
+ }
+
+ if (wantPretty) {
+ response = yield sourceClient.prettyPrint(Prefs.editorTabSize);
+ }
+ else {
+ response = yield sourceClient.disablePrettyPrint();
+ }
+
+ // Remove the cached source AST from the Parser, to avoid getting
+ // wrong locations when searching for functions.
+ DebuggerController.Parser.clearSource(source.url);
+
+ return {
+ isPrettyPrinted: wantPretty,
+ text: response.source,
+ contentType: response.contentType
+ };
+ })
+ });
+ };
+}
+
+function loadSourceText(source) {
+ return (dispatch, getState) => {
+ // Fetch the source text only once.
+ let textInfo = getSourceText(getState(), source.actor);
+ if (textInfo) {
+ // It's already loaded or is loading
+ return promise.resolve(textInfo);
+ }
+
+ const sourceClient = getSourceClient(source);
+
+ return dispatch({
+ type: constants.LOAD_SOURCE_TEXT,
+ source: source,
+ [PROMISE]: Task.spawn(function* () {
+ let transportType = gClient.localTransport ? "_LOCAL" : "_REMOTE";
+ let histogramId = "DEVTOOLS_DEBUGGER_DISPLAY_SOURCE" + transportType + "_MS";
+ let histogram = Services.telemetry.getHistogramById(histogramId);
+ let startTime = Date.now();
+
+ const response = yield sourceClient.source();
+
+ histogram.add(Date.now() - startTime);
+
+ // Automatically pretty print if enabled and the test is
+ // detected to be "minified"
+ if (Prefs.autoPrettyPrint &&
+ !source.isPrettyPrinted &&
+ SourceUtils.isMinified(source.actor, response.source)) {
+ dispatch(togglePrettyPrint(source));
+ }
+
+ return { text: response.source,
+ contentType: response.contentType };
+ })
+ });
+ };
+}
+
+/**
+ * Starts fetching all the sources, silently.
+ *
+ * @param array aUrls
+ * The urls for the sources to fetch. If fetching a source's text
+ * takes too long, it will be discarded.
+ * @return object
+ * A promise that is resolved after source texts have been fetched.
+ */
+function getTextForSources(actors) {
+ return (dispatch, getState) => {
+ let deferred = promise.defer();
+ let pending = new Set(actors);
+ let fetched = [];
+
+ // Can't use promise.all, because if one fetch operation is rejected, then
+ // everything is considered rejected, thus no other subsequent source will
+ // be getting fetched. We don't want that. Something like Q's allSettled
+ // would work like a charm here.
+
+ // Try to fetch as many sources as possible.
+ for (let actor of actors) {
+ let source = getSource(getState(), actor);
+ dispatch(loadSourceText(source)).then(({ text, contentType }) => {
+ onFetch([source, text, contentType]);
+ }, err => {
+ onError(source, err);
+ });
+ }
+
+ setTimeout(onTimeout, FETCH_SOURCE_RESPONSE_DELAY);
+
+ /* Called if fetching a source takes too long. */
+ function onTimeout() {
+ pending = new Set();
+ maybeFinish();
+ }
+
+ /* Called if fetching a source finishes successfully. */
+ function onFetch([aSource, aText, aContentType]) {
+ // If fetching the source has previously timed out, discard it this time.
+ if (!pending.has(aSource.actor)) {
+ return;
+ }
+ pending.delete(aSource.actor);
+ fetched.push([aSource.actor, aText, aContentType]);
+ maybeFinish();
+ }
+
+ /* Called if fetching a source failed because of an error. */
+ function onError([aSource, aError]) {
+ pending.delete(aSource.actor);
+ maybeFinish();
+ }
+
+ /* Called every time something interesting happens while fetching sources. */
+ function maybeFinish() {
+ if (pending.size == 0) {
+ // Sort the fetched sources alphabetically by their url.
+ deferred.resolve(fetched.sort(([aFirst], [aSecond]) => aFirst > aSecond));
+ }
+ }
+
+ return deferred.promise;
+ };
+}
+
+module.exports = {
+ newSource,
+ selectSource,
+ loadSources,
+ blackbox,
+ togglePrettyPrint,
+ loadSourceText,
+ getTextForSources
+};
diff --git a/devtools/client/debugger/content/constants.js b/devtools/client/debugger/content/constants.js
new file mode 100644
index 000000000..0099477b7
--- /dev/null
+++ b/devtools/client/debugger/content/constants.js
@@ -0,0 +1,25 @@
+/* -*- 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";
+
+exports.UPDATE_EVENT_BREAKPOINTS = "UPDATE_EVENT_BREAKPOINTS";
+exports.FETCH_EVENT_LISTENERS = "FETCH_EVENT_LISTENERS";
+
+exports.TOGGLE_PRETTY_PRINT = "TOGGLE_PRETTY_PRINT";
+exports.BLACKBOX = "BLACKBOX";
+
+exports.ADD_BREAKPOINT = "ADD_BREAKPOINT";
+exports.REMOVE_BREAKPOINT = "REMOVE_BREAKPOINT";
+exports.ENABLE_BREAKPOINT = "ENABLE_BREAKPOINT";
+exports.DISABLE_BREAKPOINT = "DISABLE_BREAKPOINT";
+exports.SET_BREAKPOINT_CONDITION = "SET_BREAKPOINT_CONDITION";
+
+exports.ADD_SOURCE = "ADD_SOURCE";
+exports.LOAD_SOURCES = "LOAD_SOURCES";
+exports.LOAD_SOURCE_TEXT = "LOAD_SOURCE_TEXT";
+exports.SELECT_SOURCE = "SELECT_SOURCE";
+exports.UNLOAD = "UNLOAD";
+exports.RELOAD = "RELOAD";
diff --git a/devtools/client/debugger/content/globalActions.js b/devtools/client/debugger/content/globalActions.js
new file mode 100644
index 000000000..3f02be36e
--- /dev/null
+++ b/devtools/client/debugger/content/globalActions.js
@@ -0,0 +1,18 @@
+/* -*- 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 constants = require("./constants");
+
+// Fired when the page is being unloaded, for example when it's being
+// navigated away from.
+function unload() {
+ return {
+ type: constants.UNLOAD
+ };
+}
+
+module.exports = { unload };
diff --git a/devtools/client/debugger/content/moz.build b/devtools/client/debugger/content/moz.build
new file mode 100644
index 000000000..fcca58e65
--- /dev/null
+++ b/devtools/client/debugger/content/moz.build
@@ -0,0 +1,17 @@
+# vim: set filetype=python:
+# 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/.
+
+DIRS += [
+ 'actions',
+ 'reducers',
+ 'views',
+]
+
+DevToolsModules(
+ 'constants.js',
+ 'globalActions.js',
+ 'queries.js',
+ 'utils.js'
+)
diff --git a/devtools/client/debugger/content/queries.js b/devtools/client/debugger/content/queries.js
new file mode 100644
index 000000000..3a0c54b88
--- /dev/null
+++ b/devtools/client/debugger/content/queries.js
@@ -0,0 +1,70 @@
+
+function getSource(state, actor) {
+ return state.sources.sources[actor];
+}
+
+function getSources(state) {
+ return state.sources.sources;
+}
+
+function getSourceCount(state) {
+ return Object.keys(state.sources.sources).length;
+}
+
+function getSourceByURL(state, url) {
+ for (let k in state.sources.sources) {
+ const source = state.sources.sources[k];
+ if (source.url === url) {
+ return source;
+ }
+ }
+}
+
+function getSourceByActor(state, actor) {
+ for (let k in state.sources.sources) {
+ const source = state.sources.sources[k];
+ if (source.actor === actor) {
+ return source;
+ }
+ }
+}
+
+function getSelectedSource(state) {
+ return state.sources.sources[state.sources.selectedSource];
+}
+
+function getSelectedSourceOpts(state) {
+ return state.sources.selectedSourceOpts;
+}
+
+function getSourceText(state, actor) {
+ return state.sources.sourcesText[actor];
+}
+
+function getBreakpoints(state) {
+ return Object.keys(state.breakpoints.breakpoints).map(k => {
+ return state.breakpoints.breakpoints[k];
+ });
+}
+
+function getBreakpoint(state, location) {
+ return state.breakpoints.breakpoints[makeLocationId(location)];
+}
+
+function makeLocationId(location) {
+ return location.actor + ":" + location.line.toString();
+}
+
+module.exports = {
+ getSource,
+ getSources,
+ getSourceCount,
+ getSourceByURL,
+ getSourceByActor,
+ getSelectedSource,
+ getSelectedSourceOpts,
+ getSourceText,
+ getBreakpoint,
+ getBreakpoints,
+ makeLocationId
+};
diff --git a/devtools/client/debugger/content/reducers/async-requests.js b/devtools/client/debugger/content/reducers/async-requests.js
new file mode 100644
index 000000000..206e1cf60
--- /dev/null
+++ b/devtools/client/debugger/content/reducers/async-requests.js
@@ -0,0 +1,31 @@
+/* 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 constants = require("../constants");
+const initialState = [];
+
+function update(state = initialState, action, emitChange) {
+ const { seqId } = action;
+
+ if (action.type === constants.UNLOAD) {
+ return initialState;
+ }
+ else if (seqId) {
+ let newState;
+ if (action.status === "start") {
+ newState = [...state, seqId];
+ }
+ else if (action.status === "error" || action.status === "done") {
+ newState = state.filter(id => id !== seqId);
+ }
+
+ emitChange("open-requests", newState);
+ return newState;
+ }
+
+ return state;
+}
+
+module.exports = update;
diff --git a/devtools/client/debugger/content/reducers/breakpoints.js b/devtools/client/debugger/content/reducers/breakpoints.js
new file mode 100644
index 000000000..7e42098e8
--- /dev/null
+++ b/devtools/client/debugger/content/reducers/breakpoints.js
@@ -0,0 +1,153 @@
+/* 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 constants = require("../constants");
+const Immutable = require("devtools/client/shared/vendor/seamless-immutable");
+const { mergeIn, setIn, deleteIn } = require("../utils");
+const { makeLocationId } = require("../queries");
+
+const initialState = Immutable({
+ breakpoints: {}
+});
+
+// Return the first argument that is a string, or null if nothing is a
+// string.
+function firstString(...args) {
+ for (var arg of args) {
+ if (typeof arg === "string") {
+ return arg;
+ }
+ }
+ return null;
+}
+
+function update(state = initialState, action, emitChange) {
+ switch (action.type) {
+ case constants.ADD_BREAKPOINT: {
+ const id = makeLocationId(action.breakpoint.location);
+
+ if (action.status === "start") {
+ const existingBp = state.breakpoints[id];
+ const bp = existingBp || Immutable(action.breakpoint);
+
+ state = setIn(state, ["breakpoints", id], bp.merge({
+ disabled: false,
+ loading: true,
+ // We want to do an OR here, but we can't because we need
+ // empty strings to be truthy, i.e. an empty string is a valid
+ // condition.
+ condition: firstString(action.condition, bp.condition)
+ }));
+
+ emitChange(existingBp ? "breakpoint-enabled" : "breakpoint-added",
+ state.breakpoints[id]);
+ return state;
+ }
+ else if (action.status === "done") {
+ const { actor, text } = action.value;
+ let { actualLocation } = action.value;
+
+ // If the breakpoint moved, update the map
+ if (actualLocation) {
+ // XXX Bug 1227417: The `setBreakpoint` RDP request rdp
+ // request returns an `actualLocation` field that doesn't
+ // conform to the regular { actor, line } location shape, but
+ // it has a `source` field. We should fix that.
+ actualLocation = { actor: actualLocation.source.actor,
+ line: actualLocation.line };
+
+ state = deleteIn(state, ["breakpoints", id]);
+
+ const movedId = makeLocationId(actualLocation);
+ const currentBp = state.breakpoints[movedId] || Immutable(action.breakpoint);
+ const prevLocation = action.breakpoint.location;
+ const newBp = currentBp.merge({ location: actualLocation });
+ state = setIn(state, ["breakpoints", movedId], newBp);
+
+ emitChange("breakpoint-moved", {
+ breakpoint: newBp,
+ prevLocation: prevLocation
+ });
+ }
+
+ const finalLocation = (
+ actualLocation ? actualLocation : action.breakpoint.location
+ );
+ const finalLocationId = makeLocationId(finalLocation);
+ state = mergeIn(state, ["breakpoints", finalLocationId], {
+ disabled: false,
+ loading: false,
+ actor: actor,
+ text: text
+ });
+ emitChange("breakpoint-updated", state.breakpoints[finalLocationId]);
+ return state;
+ }
+ else if (action.status === "error") {
+ // Remove the optimistic update
+ emitChange("breakpoint-removed", state.breakpoints[id]);
+ return deleteIn(state, ["breakpoints", id]);
+ }
+ break;
+ }
+
+ case constants.REMOVE_BREAKPOINT: {
+ if (action.status === "done") {
+ const id = makeLocationId(action.breakpoint.location);
+ const bp = state.breakpoints[id];
+
+ if (action.disabled) {
+ state = mergeIn(state, ["breakpoints", id],
+ { loading: false, disabled: true });
+ emitChange("breakpoint-disabled", state.breakpoints[id]);
+ return state;
+ }
+
+ state = deleteIn(state, ["breakpoints", id]);
+ emitChange("breakpoint-removed", bp);
+ return state;
+ }
+ break;
+ }
+
+ case constants.SET_BREAKPOINT_CONDITION: {
+ const id = makeLocationId(action.breakpoint.location);
+ const bp = state.breakpoints[id];
+ emitChange("breakpoint-condition-updated", bp);
+
+ if (!action.status) {
+ // No status means that it wasn't a remote request. Just update
+ // the condition locally.
+ return mergeIn(state, ["breakpoints", id], {
+ condition: action.condition
+ });
+ }
+ else if (action.status === "start") {
+ return mergeIn(state, ["breakpoints", id], {
+ loading: true,
+ condition: action.condition
+ });
+ }
+ else if (action.status === "done") {
+ return mergeIn(state, ["breakpoints", id], {
+ loading: false,
+ condition: action.condition,
+ // Setting a condition creates a new breakpoint client as of
+ // now, so we need to update the actor
+ actor: action.value.actor
+ });
+ }
+ else if (action.status === "error") {
+ emitChange("breakpoint-removed", bp);
+ return deleteIn(state, ["breakpoints", id]);
+ }
+
+ break;
+ }}
+
+ return state;
+}
+
+module.exports = update;
diff --git a/devtools/client/debugger/content/reducers/event-listeners.js b/devtools/client/debugger/content/reducers/event-listeners.js
new file mode 100644
index 000000000..fdd3da99d
--- /dev/null
+++ b/devtools/client/debugger/content/reducers/event-listeners.js
@@ -0,0 +1,37 @@
+/* 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 constants = require("../constants");
+
+const FETCH_EVENT_LISTENERS_DELAY = 200; // ms
+
+const initialState = {
+ activeEventNames: [],
+ listeners: [],
+ fetchingListeners: false,
+};
+
+function update(state = initialState, action, emit) {
+ switch (action.type) {
+ case constants.UPDATE_EVENT_BREAKPOINTS:
+ state.activeEventNames = action.eventNames;
+ emit("activeEventNames", state.activeEventNames);
+ break;
+ case constants.FETCH_EVENT_LISTENERS:
+ if (action.status === "begin") {
+ state.fetchingListeners = true;
+ }
+ else if (action.status === "done") {
+ state.fetchingListeners = false;
+ state.listeners = action.listeners;
+ emit("event-listeners", state.listeners);
+ }
+ break;
+ }
+
+ return state;
+}
+
+module.exports = update;
diff --git a/devtools/client/debugger/content/reducers/index.js b/devtools/client/debugger/content/reducers/index.js
new file mode 100644
index 000000000..27f2059f9
--- /dev/null
+++ b/devtools/client/debugger/content/reducers/index.js
@@ -0,0 +1,16 @@
+/* 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 eventListeners = require("./event-listeners");
+const sources = require("./sources");
+const breakpoints = require("./breakpoints");
+const asyncRequests = require("./async-requests");
+
+module.exports = {
+ eventListeners,
+ sources,
+ breakpoints,
+ asyncRequests
+};
diff --git a/devtools/client/debugger/content/reducers/moz.build b/devtools/client/debugger/content/reducers/moz.build
new file mode 100644
index 000000000..0433a099c
--- /dev/null
+++ b/devtools/client/debugger/content/reducers/moz.build
@@ -0,0 +1,12 @@
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'async-requests.js',
+ 'breakpoints.js',
+ 'event-listeners.js',
+ 'index.js',
+ 'sources.js'
+)
diff --git a/devtools/client/debugger/content/reducers/sources.js b/devtools/client/debugger/content/reducers/sources.js
new file mode 100644
index 000000000..963a52fb5
--- /dev/null
+++ b/devtools/client/debugger/content/reducers/sources.js
@@ -0,0 +1,128 @@
+/* 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 constants = require("../constants");
+const Immutable = require("devtools/client/shared/vendor/seamless-immutable");
+const { mergeIn, setIn } = require("../utils");
+
+const initialState = Immutable({
+ sources: {},
+ selectedSource: null,
+ selectedSourceOpts: null,
+ sourcesText: {}
+});
+
+function update(state = initialState, action, emitChange) {
+ switch (action.type) {
+ case constants.ADD_SOURCE:
+ emitChange("source", action.source);
+ return mergeIn(state, ["sources", action.source.actor], action.source);
+
+ case constants.LOAD_SOURCES:
+ if (action.status === "done") {
+ const sources = action.value;
+ if (!sources) {
+ return state;
+ }
+ const sourcesByActor = {};
+ sources.forEach(source => {
+ if (!state.sources[source.actor]) {
+ emitChange("source", source);
+ }
+ sourcesByActor[source.actor] = source;
+ });
+ return mergeIn(state, ["sources"], state.sources.merge(sourcesByActor));
+ }
+ break;
+
+ case constants.SELECT_SOURCE:
+ emitChange("source-selected", action.source);
+ return state.merge({
+ selectedSource: action.source.actor,
+ selectedSourceOpts: action.opts
+ });
+
+ case constants.LOAD_SOURCE_TEXT: {
+ const s = _updateText(state, action);
+ emitChange("source-text-loaded", s.sources[action.source.actor]);
+ return s;
+ }
+
+ case constants.BLACKBOX:
+ if (action.status === "done") {
+ const s = mergeIn(state,
+ ["sources", action.source.actor, "isBlackBoxed"],
+ action.value.isBlackBoxed);
+ emitChange("blackboxed", s.sources[action.source.actor]);
+ return s;
+ }
+ break;
+
+ case constants.TOGGLE_PRETTY_PRINT:
+ let s = state;
+ if (action.status === "error") {
+ s = mergeIn(state, ["sourcesText", action.source.actor], {
+ loading: false
+ });
+
+ // If it errored, just display the source as it was before, but
+ // only if there is existing text already. If auto-prettifying
+ // is on, the original text may still be coming in and we don't
+ // have it yet. If we try to set empty text we confuse the
+ // editor because it thinks it's already displaying the source's
+ // text and won't load the text when it actually comes in.
+ if (s.sourcesText[action.source.actor].text != null) {
+ emitChange("prettyprinted", s.sources[action.source.actor]);
+ }
+ }
+ else {
+ s = _updateText(state, action);
+ // Don't do this yet, the progress bar is still imperatively shown
+ // from the source view. We will fix in the next iteration.
+ // emitChange('source-text-loaded', s.sources[action.source.actor]);
+
+ if (action.status === "done") {
+ s = mergeIn(s,
+ ["sources", action.source.actor, "isPrettyPrinted"],
+ action.value.isPrettyPrinted);
+ emitChange("prettyprinted", s.sources[action.source.actor]);
+ }
+ }
+ return s;
+
+ case constants.UNLOAD:
+ // Reset the entire state to just the initial state, a blank state
+ // if you will.
+ return initialState;
+ }
+
+ return state;
+}
+
+function _updateText(state, action) {
+ const { source } = action;
+
+ if (action.status === "start") {
+ // Merge this in, don't set it. That way the previous value is
+ // still stored here, and we can retrieve it if whatever we're
+ // doing fails.
+ return mergeIn(state, ["sourcesText", source.actor], {
+ loading: true
+ });
+ }
+ else if (action.status === "error") {
+ return setIn(state, ["sourcesText", source.actor], {
+ error: action.error
+ });
+ }
+ else {
+ return setIn(state, ["sourcesText", source.actor], {
+ text: action.value.text,
+ contentType: action.value.contentType
+ });
+ }
+}
+
+module.exports = update;
diff --git a/devtools/client/debugger/content/utils.js b/devtools/client/debugger/content/utils.js
new file mode 100644
index 000000000..59993e9b4
--- /dev/null
+++ b/devtools/client/debugger/content/utils.js
@@ -0,0 +1,88 @@
+/* -*- 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 { reportException } = require("devtools/shared/DevToolsUtils");
+const { Task } = require("devtools/shared/task");
+
+function asPaused(client, func) {
+ if (client.state != "paused") {
+ return Task.spawn(function* () {
+ yield client.interrupt();
+ let result;
+
+ try {
+ result = yield func();
+ }
+ catch (e) {
+ // Try to put the debugger back in a working state by resuming
+ // it
+ yield client.resume();
+ throw e;
+ }
+
+ yield client.resume();
+ return result;
+ });
+ } else {
+ return func();
+ }
+}
+
+function handleError(err) {
+ reportException("promise", err.toString());
+}
+
+function onReducerEvents(controller, listeners, thisContext) {
+ Object.keys(listeners).forEach(name => {
+ const listener = listeners[name];
+ controller.onChange(name, payload => {
+ listener.call(thisContext, payload);
+ });
+ });
+}
+
+function _getIn(destObj, path) {
+ return path.reduce(function (acc, name) {
+ return acc[name];
+ }, destObj);
+}
+
+function mergeIn(destObj, path, value) {
+ path = [...path];
+ path.reverse();
+ var obj = path.reduce(function (acc, name) {
+ return { [name]: acc };
+ }, value);
+
+ return destObj.merge(obj, { deep: true });
+}
+
+function setIn(destObj, path, value) {
+ destObj = mergeIn(destObj, path, null);
+ return mergeIn(destObj, path, value);
+}
+
+function updateIn(destObj, path, fn) {
+ return setIn(destObj, path, fn(_getIn(destObj, path)));
+}
+
+function deleteIn(destObj, path) {
+ const objPath = path.slice(0, -1);
+ const propName = path[path.length - 1];
+ const obj = _getIn(destObj, objPath);
+ return setIn(destObj, objPath, obj.without(propName));
+}
+
+module.exports = {
+ asPaused,
+ handleError,
+ onReducerEvents,
+ mergeIn,
+ setIn,
+ updateIn,
+ deleteIn
+};
diff --git a/devtools/client/debugger/content/views/event-listeners-view.js b/devtools/client/debugger/content/views/event-listeners-view.js
new file mode 100644
index 000000000..993d6506e
--- /dev/null
+++ b/devtools/client/debugger/content/views/event-listeners-view.js
@@ -0,0 +1,295 @@
+/* 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";
+
+/* import-globals-from ../../debugger-controller.js */
+
+const actions = require("../actions/event-listeners");
+const { bindActionCreators } = require("devtools/client/shared/vendor/redux");
+const { Heritage, WidgetMethods } = require("devtools/client/shared/widgets/view-helpers");
+const { SideMenuWidget } = require("resource://devtools/client/shared/widgets/SideMenuWidget.jsm");
+
+/**
+ * Functions handling the event listeners UI.
+ */
+function EventListenersView(controller) {
+ dumpn("EventListenersView was instantiated");
+
+ this.actions = bindActionCreators(actions, controller.dispatch);
+ this.getState = () => controller.getState().eventListeners;
+
+ this._onCheck = this._onCheck.bind(this);
+ this._onClick = this._onClick.bind(this);
+
+ controller.onChange("event-listeners", this.renderListeners.bind(this));
+}
+
+EventListenersView.prototype = Heritage.extend(WidgetMethods, {
+ /**
+ * Initialization function, called when the debugger is started.
+ */
+ initialize: function () {
+ dumpn("Initializing the EventListenersView");
+
+ this.widget = new SideMenuWidget(document.getElementById("event-listeners"), {
+ showItemCheckboxes: true,
+ showGroupCheckboxes: true
+ });
+
+ this.emptyText = L10N.getStr("noEventListenersText");
+ this._eventCheckboxTooltip = L10N.getStr("eventCheckboxTooltip");
+ this._onSelectorString = " " + L10N.getStr("eventOnSelector") + " ";
+ this._inSourceString = " " + L10N.getStr("eventInSource") + " ";
+ this._inNativeCodeString = L10N.getStr("eventNative");
+
+ this.widget.addEventListener("check", this._onCheck, false);
+ this.widget.addEventListener("click", this._onClick, false);
+ },
+
+ /**
+ * Destruction function, called when the debugger is closed.
+ */
+ destroy: function () {
+ dumpn("Destroying the EventListenersView");
+
+ this.widget.removeEventListener("check", this._onCheck, false);
+ this.widget.removeEventListener("click", this._onClick, false);
+ },
+
+ renderListeners: function (listeners) {
+ listeners.forEach(listener => {
+ this.addListener(listener, { staged: true });
+ });
+
+ // Flushes all the prepared events into the event listeners container.
+ this.commit();
+ },
+
+ /**
+ * Adds an event to this event listeners container.
+ *
+ * @param object aListener
+ * The listener object coming from the active thread.
+ * @param object aOptions [optional]
+ * Additional options for adding the source. Supported options:
+ * - staged: true to stage the item to be appended later
+ */
+ addListener: function (aListener, aOptions = {}) {
+ let { node: { selector }, function: { url }, type } = aListener;
+ if (!type) return;
+
+ // Some listener objects may be added from plugins, thus getting
+ // translated to native code.
+ if (!url) {
+ url = this._inNativeCodeString;
+ }
+
+ // If an event item for this listener's url and type was already added,
+ // avoid polluting the view and simply increase the "targets" count.
+ let eventItem = this.getItemForPredicate(aItem =>
+ aItem.attachment.url == url &&
+ aItem.attachment.type == type);
+
+ if (eventItem) {
+ let { selectors, view: { targets } } = eventItem.attachment;
+ if (selectors.indexOf(selector) == -1) {
+ selectors.push(selector);
+ targets.setAttribute("value", L10N.getFormatStr("eventNodes", selectors.length));
+ }
+ return;
+ }
+
+ // There's no easy way of grouping event types into higher-level groups,
+ // so we need to do this by hand.
+ let is = (...args) => args.indexOf(type) != -1;
+ let has = str => type.includes(str);
+ let starts = str => type.startsWith(str);
+ let group;
+
+ if (starts("animation")) {
+ group = L10N.getStr("animationEvents");
+ } else if (starts("audio")) {
+ group = L10N.getStr("audioEvents");
+ } else if (is("levelchange")) {
+ group = L10N.getStr("batteryEvents");
+ } else if (is("cut", "copy", "paste")) {
+ group = L10N.getStr("clipboardEvents");
+ } else if (starts("composition")) {
+ group = L10N.getStr("compositionEvents");
+ } else if (starts("device")) {
+ group = L10N.getStr("deviceEvents");
+ } else if (is("fullscreenchange", "fullscreenerror", "orientationchange",
+ "overflow", "resize", "scroll", "underflow", "zoom")) {
+ group = L10N.getStr("displayEvents");
+ } else if (starts("drag") || starts("drop")) {
+ group = L10N.getStr("dragAndDropEvents");
+ } else if (starts("gamepad")) {
+ group = L10N.getStr("gamepadEvents");
+ } else if (is("canplay", "canplaythrough", "durationchange", "emptied",
+ "ended", "loadeddata", "loadedmetadata", "pause", "play", "playing",
+ "ratechange", "seeked", "seeking", "stalled", "suspend", "timeupdate",
+ "volumechange", "waiting")) {
+ group = L10N.getStr("mediaEvents");
+ } else if (is("blocked", "complete", "success", "upgradeneeded", "versionchange")) {
+ group = L10N.getStr("indexedDBEvents");
+ } else if (is("blur", "change", "focus", "focusin", "focusout", "invalid",
+ "reset", "select", "submit")) {
+ group = L10N.getStr("interactionEvents");
+ } else if (starts("key") || is("input")) {
+ group = L10N.getStr("keyboardEvents");
+ } else if (starts("mouse") || has("click") || is("contextmenu", "show", "wheel")) {
+ group = L10N.getStr("mouseEvents");
+ } else if (starts("DOM")) {
+ group = L10N.getStr("mutationEvents");
+ } else if (is("abort", "error", "hashchange", "load", "loadend", "loadstart",
+ "pagehide", "pageshow", "progress", "timeout", "unload", "uploadprogress",
+ "visibilitychange")) {
+ group = L10N.getStr("navigationEvents");
+ } else if (is("pointerlockchange", "pointerlockerror")) {
+ group = L10N.getStr("pointerLockEvents");
+ } else if (is("compassneedscalibration", "userproximity")) {
+ group = L10N.getStr("sensorEvents");
+ } else if (starts("storage")) {
+ group = L10N.getStr("storageEvents");
+ } else if (is("beginEvent", "endEvent", "repeatEvent")) {
+ group = L10N.getStr("timeEvents");
+ } else if (starts("touch")) {
+ group = L10N.getStr("touchEvents");
+ } else {
+ group = L10N.getStr("otherEvents");
+ }
+
+ // Create the element node for the event listener item.
+ const itemView = this._createItemView(type, selector, url);
+
+ // Event breakpoints survive target navigations. Make sure the newly
+ // inserted event item is correctly checked.
+ const activeEventNames = this.getState().activeEventNames;
+ const checkboxState = activeEventNames.indexOf(type) != -1;
+
+ // Append an event listener item to this container.
+ this.push([itemView.container], {
+ staged: aOptions.staged, /* stage the item to be appended later? */
+ attachment: {
+ url: url,
+ type: type,
+ view: itemView,
+ selectors: [selector],
+ group: group,
+ checkboxState: checkboxState,
+ checkboxTooltip: this._eventCheckboxTooltip
+ }
+ });
+ },
+
+ /**
+ * Gets all the event types known to this container.
+ *
+ * @return array
+ * List of event types, for example ["load", "click"...]
+ */
+ getAllEvents: function () {
+ return this.attachments.map(e => e.type);
+ },
+
+ /**
+ * Gets the checked event types in this container.
+ *
+ * @return array
+ * List of event types, for example ["load", "click"...]
+ */
+ getCheckedEvents: function () {
+ return this.attachments.filter(e => e.checkboxState).map(e => e.type);
+ },
+
+ /**
+ * Customization function for creating an item's UI.
+ *
+ * @param string aType
+ * The event type, for example "click".
+ * @param string aSelector
+ * The target element's selector.
+ * @param string url
+ * The source url in which the event listener is located.
+ * @return object
+ * An object containing the event listener view nodes.
+ */
+ _createItemView: function (aType, aSelector, aUrl) {
+ let container = document.createElement("hbox");
+ container.className = "dbg-event-listener";
+
+ let eventType = document.createElement("label");
+ eventType.className = "plain dbg-event-listener-type";
+ eventType.setAttribute("value", aType);
+ container.appendChild(eventType);
+
+ let typeSeparator = document.createElement("label");
+ typeSeparator.className = "plain dbg-event-listener-separator";
+ typeSeparator.setAttribute("value", this._onSelectorString);
+ container.appendChild(typeSeparator);
+
+ let eventTargets = document.createElement("label");
+ eventTargets.className = "plain dbg-event-listener-targets";
+ eventTargets.setAttribute("value", aSelector);
+ container.appendChild(eventTargets);
+
+ let selectorSeparator = document.createElement("label");
+ selectorSeparator.className = "plain dbg-event-listener-separator";
+ selectorSeparator.setAttribute("value", this._inSourceString);
+ container.appendChild(selectorSeparator);
+
+ let eventLocation = document.createElement("label");
+ eventLocation.className = "plain dbg-event-listener-location";
+ eventLocation.setAttribute("value", SourceUtils.getSourceLabel(aUrl));
+ eventLocation.setAttribute("flex", "1");
+ eventLocation.setAttribute("crop", "center");
+ container.appendChild(eventLocation);
+
+ return {
+ container: container,
+ type: eventType,
+ targets: eventTargets,
+ location: eventLocation
+ };
+ },
+
+ /**
+ * The check listener for the event listeners container.
+ */
+ _onCheck: function ({ detail: { description, checked }, target }) {
+ if (description == "item") {
+ this.getItemForElement(target).attachment.checkboxState = checked;
+
+ this.actions.updateEventBreakpoints(this.getCheckedEvents());
+ return;
+ }
+
+ // Check all the event items in this group.
+ this.items
+ .filter(e => e.attachment.group == description)
+ .forEach(e => this.callMethod("checkItem", e.target, checked));
+ },
+
+ /**
+ * The select listener for the event listeners container.
+ */
+ _onClick: function ({ target }) {
+ // Changing the checkbox state is handled by the _onCheck event. Avoid
+ // handling that again in this click event, so pass in "noSiblings"
+ // when retrieving the target's item, to ignore the checkbox.
+ let eventItem = this.getItemForElement(target, { noSiblings: true });
+ if (eventItem) {
+ let newState = eventItem.attachment.checkboxState ^= 1;
+ this.callMethod("checkItem", eventItem.target, newState);
+ }
+ },
+
+ _eventCheckboxTooltip: "",
+ _onSelectorString: "",
+ _inSourceString: "",
+ _inNativeCodeString: ""
+});
+
+module.exports = EventListenersView;
diff --git a/devtools/client/debugger/content/views/moz.build b/devtools/client/debugger/content/views/moz.build
new file mode 100644
index 000000000..de1ecc184
--- /dev/null
+++ b/devtools/client/debugger/content/views/moz.build
@@ -0,0 +1,9 @@
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'event-listeners-view.js',
+ 'sources-view.js'
+)
diff --git a/devtools/client/debugger/content/views/sources-view.js b/devtools/client/debugger/content/views/sources-view.js
new file mode 100644
index 000000000..bb68afcf4
--- /dev/null
+++ b/devtools/client/debugger/content/views/sources-view.js
@@ -0,0 +1,1370 @@
+/* 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";
+
+/* import-globals-from ../../debugger-controller.js */
+
+const utils = require("../utils");
+const {
+ getSelectedSource,
+ getSourceByURL,
+ getBreakpoint,
+ getBreakpoints,
+ makeLocationId
+} = require("../queries");
+const actions = Object.assign(
+ {},
+ require("../actions/sources"),
+ require("../actions/breakpoints")
+);
+const { bindActionCreators } = require("devtools/client/shared/vendor/redux");
+const {
+ Heritage,
+ WidgetMethods,
+ setNamedTimeout
+} = require("devtools/client/shared/widgets/view-helpers");
+const { Task } = require("devtools/shared/task");
+const { SideMenuWidget } = require("resource://devtools/client/shared/widgets/SideMenuWidget.jsm");
+const { gDevTools } = require("devtools/client/framework/devtools");
+const {KeyCodes} = require("devtools/client/shared/keycodes");
+
+const NEW_SOURCE_DISPLAY_DELAY = 200; // ms
+const FUNCTION_SEARCH_POPUP_POSITION = "topcenter bottomleft";
+const BREAKPOINT_LINE_TOOLTIP_MAX_LENGTH = 1000; // chars
+const BREAKPOINT_CONDITIONAL_POPUP_POSITION = "before_start";
+const BREAKPOINT_CONDITIONAL_POPUP_OFFSET_X = 7; // px
+const BREAKPOINT_CONDITIONAL_POPUP_OFFSET_Y = -3; // px
+
+/**
+ * Functions handling the sources UI.
+ */
+function SourcesView(controller, DebuggerView) {
+ dumpn("SourcesView was instantiated");
+
+ utils.onReducerEvents(controller, {
+ "source": this.renderSource,
+ "blackboxed": this.renderBlackBoxed,
+ "prettyprinted": this.updateToolbarButtonsState,
+ "source-selected": this.renderSourceSelected,
+ "breakpoint-updated": bp => this.renderBreakpoint(bp),
+ "breakpoint-enabled": bp => this.renderBreakpoint(bp),
+ "breakpoint-disabled": bp => this.renderBreakpoint(bp),
+ "breakpoint-removed": bp => this.renderBreakpoint(bp, true),
+ }, this);
+
+ this.getState = controller.getState;
+ this.actions = bindActionCreators(actions, controller.dispatch);
+ this.DebuggerView = DebuggerView;
+ this.Parser = DebuggerController.Parser;
+
+ this.togglePrettyPrint = this.togglePrettyPrint.bind(this);
+ this.toggleBlackBoxing = this.toggleBlackBoxing.bind(this);
+ this.toggleBreakpoints = this.toggleBreakpoints.bind(this);
+
+ this._onEditorCursorActivity = this._onEditorCursorActivity.bind(this);
+ this._onMouseDown = this._onMouseDown.bind(this);
+ this._onSourceSelect = this._onSourceSelect.bind(this);
+ this._onStopBlackBoxing = this._onStopBlackBoxing.bind(this);
+ this._onBreakpointRemoved = this._onBreakpointRemoved.bind(this);
+ this._onBreakpointClick = this._onBreakpointClick.bind(this);
+ this._onBreakpointCheckboxClick = this._onBreakpointCheckboxClick.bind(this);
+ this._onConditionalPopupShowing = this._onConditionalPopupShowing.bind(this);
+ this._onConditionalPopupShown = this._onConditionalPopupShown.bind(this);
+ this._onConditionalPopupHiding = this._onConditionalPopupHiding.bind(this);
+ this._onConditionalTextboxKeyPress = this._onConditionalTextboxKeyPress.bind(this);
+ this._onEditorContextMenuOpen = this._onEditorContextMenuOpen.bind(this);
+ this._onCopyUrlCommand = this._onCopyUrlCommand.bind(this);
+ this._onNewTabCommand = this._onNewTabCommand.bind(this);
+ this._onConditionalPopupHidden = this._onConditionalPopupHidden.bind(this);
+}
+
+SourcesView.prototype = Heritage.extend(WidgetMethods, {
+ /**
+ * Initialization function, called when the debugger is started.
+ */
+ initialize: function (isWorker) {
+ dumpn("Initializing the SourcesView");
+
+ this.widget = new SideMenuWidget(document.getElementById("sources"), {
+ contextMenu: document.getElementById("debuggerSourcesContextMenu"),
+ showArrows: true
+ });
+
+ this._preferredSourceURL = null;
+ this._unnamedSourceIndex = 0;
+ this.emptyText = L10N.getStr("noSourcesText");
+ this._blackBoxCheckboxTooltip = L10N.getStr("blackBoxCheckboxTooltip");
+
+ this._commandset = document.getElementById("debuggerCommands");
+ this._popupset = document.getElementById("debuggerPopupset");
+ this._cmPopup = document.getElementById("sourceEditorContextMenu");
+ this._cbPanel = document.getElementById("conditional-breakpoint-panel");
+ this._cbTextbox = document.getElementById("conditional-breakpoint-panel-textbox");
+ this._blackBoxButton = document.getElementById("black-box");
+ this._stopBlackBoxButton = document.getElementById("black-boxed-message-button");
+ this._prettyPrintButton = document.getElementById("pretty-print");
+ this._toggleBreakpointsButton = document.getElementById("toggle-breakpoints");
+ this._newTabMenuItem = document.getElementById("debugger-sources-context-newtab");
+ this._copyUrlMenuItem = document.getElementById("debugger-sources-context-copyurl");
+
+ this._noResultsFoundToolTip = new Tooltip(document);
+ this._noResultsFoundToolTip.defaultPosition = FUNCTION_SEARCH_POPUP_POSITION;
+
+ // We don't show the pretty print button if debugger a worker
+ // because it simply doesn't work yet. (bug 1273730)
+ if (Prefs.prettyPrintEnabled && !isWorker) {
+ this._prettyPrintButton.removeAttribute("hidden");
+ }
+
+ this._editorContainer = document.getElementById("editor");
+ this._editorContainer.addEventListener("mousedown", this._onMouseDown, false);
+
+ this.widget.addEventListener("select", this._onSourceSelect, false);
+
+ this._stopBlackBoxButton.addEventListener("click", this._onStopBlackBoxing, false);
+ this._cbPanel.addEventListener("popupshowing", this._onConditionalPopupShowing, false);
+ this._cbPanel.addEventListener("popupshown", this._onConditionalPopupShown, false);
+ this._cbPanel.addEventListener("popuphiding", this._onConditionalPopupHiding, false);
+ this._cbPanel.addEventListener("popuphidden", this._onConditionalPopupHidden, false);
+ this._cbTextbox.addEventListener("keypress", this._onConditionalTextboxKeyPress, false);
+ this._copyUrlMenuItem.addEventListener("command", this._onCopyUrlCommand, false);
+ this._newTabMenuItem.addEventListener("command", this._onNewTabCommand, false);
+
+ this._cbPanel.hidden = true;
+ this.allowFocusOnRightClick = true;
+ this.autoFocusOnSelection = false;
+ this.autoFocusOnFirstItem = false;
+
+ // Sort the contents by the displayed label.
+ this.sortContents((aFirst, aSecond) => {
+ return +(aFirst.attachment.label.toLowerCase() >
+ aSecond.attachment.label.toLowerCase());
+ });
+
+ // Sort known source groups towards the end of the list
+ this.widget.groupSortPredicate = function (a, b) {
+ if ((a in KNOWN_SOURCE_GROUPS) == (b in KNOWN_SOURCE_GROUPS)) {
+ return a.localeCompare(b);
+ }
+ return (a in KNOWN_SOURCE_GROUPS) ? 1 : -1;
+ };
+
+ this.DebuggerView.editor.on("popupOpen", this._onEditorContextMenuOpen);
+
+ this._addCommands();
+ },
+
+ /**
+ * Destruction function, called when the debugger is closed.
+ */
+ destroy: function () {
+ dumpn("Destroying the SourcesView");
+
+ this.widget.removeEventListener("select", this._onSourceSelect, false);
+ this._stopBlackBoxButton.removeEventListener("click", this._onStopBlackBoxing, false);
+ this._cbPanel.removeEventListener("popupshowing", this._onConditionalPopupShowing, false);
+ this._cbPanel.removeEventListener("popupshown", this._onConditionalPopupShown, false);
+ this._cbPanel.removeEventListener("popuphiding", this._onConditionalPopupHiding, false);
+ this._cbPanel.removeEventListener("popuphidden", this._onConditionalPopupHidden, false);
+ this._cbTextbox.removeEventListener("keypress", this._onConditionalTextboxKeyPress, false);
+ this._copyUrlMenuItem.removeEventListener("command", this._onCopyUrlCommand, false);
+ this._newTabMenuItem.removeEventListener("command", this._onNewTabCommand, false);
+ this.DebuggerView.editor.off("popupOpen", this._onEditorContextMenuOpen, false);
+ },
+
+ empty: function () {
+ WidgetMethods.empty.call(this);
+ this._unnamedSourceIndex = 0;
+ this._selectedBreakpoint = null;
+ },
+
+ /**
+ * Add commands that XUL can fire.
+ */
+ _addCommands: function () {
+ XULUtils.addCommands(this._commandset, {
+ addBreakpointCommand: e => this._onCmdAddBreakpoint(e),
+ addConditionalBreakpointCommand: e => this._onCmdAddConditionalBreakpoint(e),
+ blackBoxCommand: () => this.toggleBlackBoxing(),
+ unBlackBoxButton: () => this._onStopBlackBoxing(),
+ prettyPrintCommand: () => this.togglePrettyPrint(),
+ toggleBreakpointsCommand: () =>this.toggleBreakpoints(),
+ nextSourceCommand: () => this.selectNextItem(),
+ prevSourceCommand: () => this.selectPrevItem()
+ });
+ },
+
+ /**
+ * Sets the preferred location to be selected in this sources container.
+ * @param string aUrl
+ */
+ set preferredSource(aUrl) {
+ this._preferredValue = aUrl;
+
+ // Selects the element with the specified value in this sources container,
+ // if already inserted.
+ if (this.containsValue(aUrl)) {
+ this.selectedValue = aUrl;
+ }
+ },
+
+ sourcesDidUpdate: function () {
+ if (!getSelectedSource(this.getState())) {
+ let url = this._preferredSourceURL;
+ let source = url && getSourceByURL(this.getState(), url);
+ if (source) {
+ this.actions.selectSource(source);
+ }
+ else {
+ setNamedTimeout("new-source", NEW_SOURCE_DISPLAY_DELAY, () => {
+ if (!getSelectedSource(this.getState()) && this.itemCount > 0) {
+ this.actions.selectSource(this.getItemAtIndex(0).attachment.source);
+ }
+ });
+ }
+ }
+ },
+
+ renderSource: function (source) {
+ this.addSource(source, { staged: false });
+ for (let bp of getBreakpoints(this.getState())) {
+ if (bp.location.actor === source.actor) {
+ this.renderBreakpoint(bp);
+ }
+ }
+ this.sourcesDidUpdate();
+ },
+
+ /**
+ * Adds a source to this sources container.
+ *
+ * @param object aSource
+ * The source object coming from the active thread.
+ * @param object aOptions [optional]
+ * Additional options for adding the source. Supported options:
+ * - staged: true to stage the item to be appended later
+ */
+ addSource: function (aSource, aOptions = {}) {
+ if (!aSource.url && !aOptions.force) {
+ // We don't show any unnamed eval scripts yet (see bug 1124106)
+ return;
+ }
+
+ let { label, group, unicodeUrl } = this._parseUrl(aSource);
+
+ let contents = document.createElement("label");
+ contents.className = "plain dbg-source-item";
+ contents.setAttribute("value", label);
+ contents.setAttribute("crop", "start");
+ contents.setAttribute("flex", "1");
+ contents.setAttribute("tooltiptext", unicodeUrl);
+
+ if (aSource.introductionType === "wasm") {
+ const wasm = document.createElement("box");
+ wasm.className = "dbg-wasm-item";
+ const icon = document.createElement("box");
+ icon.setAttribute("tooltiptext", L10N.getStr("experimental"));
+ icon.className = "icon";
+ wasm.appendChild(icon);
+ wasm.appendChild(contents);
+
+ contents = wasm;
+ }
+
+ // If the source is blackboxed, apply the appropriate style.
+ if (gThreadClient.source(aSource).isBlackBoxed) {
+ contents.classList.add("black-boxed");
+ }
+
+ // Append a source item to this container.
+ this.push([contents, aSource.actor], {
+ staged: aOptions.staged, /* stage the item to be appended later? */
+ attachment: {
+ label: label,
+ group: group,
+ checkboxState: !aSource.isBlackBoxed,
+ checkboxTooltip: this._blackBoxCheckboxTooltip,
+ source: aSource
+ }
+ });
+ },
+
+ _parseUrl: function (aSource) {
+ let fullUrl = aSource.url;
+ let url, unicodeUrl, label, group;
+
+ if (!fullUrl) {
+ unicodeUrl = "SCRIPT" + this._unnamedSourceIndex++;
+ label = unicodeUrl;
+ group = L10N.getStr("anonymousSourcesLabel");
+ }
+ else {
+ let url = fullUrl.split(" -> ").pop();
+ label = aSource.addonPath ? aSource.addonPath : SourceUtils.getSourceLabel(url);
+ group = aSource.addonID ? aSource.addonID : SourceUtils.getSourceGroup(url);
+ unicodeUrl = NetworkHelper.convertToUnicode(unescape(fullUrl));
+ }
+
+ return {
+ label: label,
+ group: group,
+ unicodeUrl: unicodeUrl
+ };
+ },
+
+ renderBreakpoint: function (breakpoint, removed) {
+ if (removed) {
+ // Be defensive about the breakpoint not existing.
+ if (this._getBreakpoint(breakpoint)) {
+ this._removeBreakpoint(breakpoint);
+ }
+ }
+ else {
+ if (this._getBreakpoint(breakpoint)) {
+ this._updateBreakpointStatus(breakpoint);
+ }
+ else {
+ this._addBreakpoint(breakpoint);
+ }
+ }
+ },
+
+ /**
+ * Adds a breakpoint to this sources container.
+ *
+ * @param object aBreakpointClient
+ * See Breakpoints.prototype._showBreakpoint
+ * @param object aOptions [optional]
+ * @see DebuggerController.Breakpoints.addBreakpoint
+ */
+ _addBreakpoint: function (breakpoint, options = {}) {
+ let disabled = breakpoint.disabled;
+ let location = breakpoint.location;
+
+ // Get the source item to which the breakpoint should be attached.
+ let sourceItem = this.getItemByValue(location.actor);
+ if (!sourceItem) {
+ return;
+ }
+
+ // Create the element node and menu popup for the breakpoint item.
+ let breakpointArgs = Heritage.extend(breakpoint.asMutable(), options);
+ let breakpointView = this._createBreakpointView.call(this, breakpointArgs);
+ let contextMenu = this._createContextMenu.call(this, breakpointArgs);
+
+ // Append a breakpoint child item to the corresponding source item.
+ sourceItem.append(breakpointView.container, {
+ attachment: Heritage.extend(breakpointArgs, {
+ actor: location.actor,
+ line: location.line,
+ view: breakpointView,
+ popup: contextMenu
+ }),
+ attributes: [
+ ["contextmenu", contextMenu.menupopupId]
+ ],
+ // Make sure that when the breakpoint item is removed, the corresponding
+ // menupopup and commandset are also destroyed.
+ finalize: this._onBreakpointRemoved
+ });
+
+ if (typeof breakpoint.condition === "string") {
+ this.highlightBreakpoint(breakpoint.location, {
+ openPopup: true,
+ noEditorUpdate: true
+ });
+ }
+
+ window.emit(EVENTS.BREAKPOINT_SHOWN_IN_PANE);
+ },
+
+ /**
+ * Removes a breakpoint from this sources container.
+ * It does not also remove the breakpoint from the controller. Be careful.
+ *
+ * @param object aLocation
+ * @see DebuggerController.Breakpoints.addBreakpoint
+ */
+ _removeBreakpoint: function (breakpoint) {
+ // When a parent source item is removed, all the child breakpoint items are
+ // also automagically removed.
+ let sourceItem = this.getItemByValue(breakpoint.location.actor);
+ if (!sourceItem) {
+ return;
+ }
+
+ // Clear the breakpoint view.
+ sourceItem.remove(this._getBreakpoint(breakpoint));
+
+ if (this._selectedBreakpoint &&
+ (queries.makeLocationId(this._selectedBreakpoint.location) ===
+ queries.makeLocationId(breakpoint.location))) {
+ this._selectedBreakpoint = null;
+ }
+
+ window.emit(EVENTS.BREAKPOINT_HIDDEN_IN_PANE);
+ },
+
+ _getBreakpoint: function (bp) {
+ return this.getItemForPredicate(item => {
+ return item.attachment.actor === bp.location.actor &&
+ item.attachment.line === bp.location.line;
+ });
+ },
+
+ /**
+ * Updates a breakpoint.
+ *
+ * @param object breakpoint
+ */
+ _updateBreakpointStatus: function (breakpoint) {
+ let location = breakpoint.location;
+ let breakpointItem = this._getBreakpoint(getBreakpoint(this.getState(), location));
+ if (!breakpointItem) {
+ return promise.reject(new Error("No breakpoint found."));
+ }
+
+ // Breakpoint will now be enabled.
+ let attachment = breakpointItem.attachment;
+
+ // Update the corresponding menu items to reflect the enabled state.
+ let prefix = "bp-cMenu-"; // "breakpoints context menu"
+ let identifier = makeLocationId(location);
+ let enableSelfId = prefix + "enableSelf-" + identifier + "-menuitem";
+ let disableSelfId = prefix + "disableSelf-" + identifier + "-menuitem";
+ let enableSelf = document.getElementById(enableSelfId);
+ let disableSelf = document.getElementById(disableSelfId);
+
+ if (breakpoint.disabled) {
+ enableSelf.removeAttribute("hidden");
+ disableSelf.setAttribute("hidden", true);
+ attachment.view.checkbox.removeAttribute("checked");
+ }
+ else {
+ enableSelf.setAttribute("hidden", true);
+ disableSelf.removeAttribute("hidden");
+ attachment.view.checkbox.setAttribute("checked", "true");
+
+ // Update the breakpoint toggle button checked state.
+ this._toggleBreakpointsButton.removeAttribute("checked");
+ }
+
+ },
+
+ /**
+ * Highlights a breakpoint in this sources container.
+ *
+ * @param object aLocation
+ * @see DebuggerController.Breakpoints.addBreakpoint
+ * @param object aOptions [optional]
+ * An object containing some of the following boolean properties:
+ * - openPopup: tells if the expression popup should be shown.
+ * - noEditorUpdate: tells if you want to skip editor updates.
+ */
+ highlightBreakpoint: function (aLocation, aOptions = {}) {
+ let breakpoint = getBreakpoint(this.getState(), aLocation);
+ if (!breakpoint) {
+ return;
+ }
+
+ // Breakpoint will now be selected.
+ this._selectBreakpoint(breakpoint);
+
+ // Update the editor location if necessary.
+ if (!aOptions.noEditorUpdate) {
+ this.DebuggerView.setEditorLocation(aLocation.actor, aLocation.line, { noDebug: true });
+ }
+
+ // If the breakpoint requires a new conditional expression, display
+ // the panel to input the corresponding expression.
+ if (aOptions.openPopup) {
+ return this._openConditionalPopup();
+ } else {
+ return this._hideConditionalPopup();
+ }
+ },
+
+ /**
+ * Highlight the breakpoint on the current currently focused line/column
+ * if it exists.
+ */
+ highlightBreakpointAtCursor: function () {
+ let actor = this.selectedValue;
+ let line = this.DebuggerView.editor.getCursor().line + 1;
+
+ let location = { actor: actor, line: line };
+ this.highlightBreakpoint(location, { noEditorUpdate: true });
+ },
+
+ /**
+ * Unhighlights the current breakpoint in this sources container.
+ */
+ unhighlightBreakpoint: function () {
+ this._hideConditionalPopup();
+ this._unselectBreakpoint();
+ },
+
+ /**
+ * Display the message thrown on breakpoint condition
+ */
+ showBreakpointConditionThrownMessage: function (aLocation, aMessage = "") {
+ let breakpointItem = this._getBreakpoint(getBreakpoint(this.getState(), aLocation));
+ if (!breakpointItem) {
+ return;
+ }
+ let attachment = breakpointItem.attachment;
+ attachment.view.container.classList.add("dbg-breakpoint-condition-thrown");
+ attachment.view.message.setAttribute("value", aMessage);
+ },
+
+ /**
+ * Update the checked/unchecked and enabled/disabled states of the buttons in
+ * the sources toolbar based on the currently selected source's state.
+ */
+ updateToolbarButtonsState: function (source) {
+ if (source.isBlackBoxed) {
+ this._blackBoxButton.setAttribute("checked", true);
+ this._prettyPrintButton.setAttribute("checked", true);
+ } else {
+ this._blackBoxButton.removeAttribute("checked");
+ this._prettyPrintButton.removeAttribute("checked");
+ }
+
+ if (source.isPrettyPrinted) {
+ this._prettyPrintButton.setAttribute("checked", true);
+ } else {
+ this._prettyPrintButton.removeAttribute("checked");
+ }
+ },
+
+ /**
+ * Toggle the pretty printing of the selected source.
+ */
+ togglePrettyPrint: function () {
+ if (this._prettyPrintButton.hasAttribute("disabled")) {
+ return;
+ }
+
+ this.DebuggerView.showProgressBar();
+ const source = getSelectedSource(this.getState());
+ const sourceClient = gThreadClient.source(source);
+ const shouldPrettyPrint = !source.isPrettyPrinted;
+
+ // This is only here to give immediate feedback,
+ // `renderPrettyPrinted` will set the final status of the buttons
+ if (shouldPrettyPrint) {
+ this._prettyPrintButton.setAttribute("checked", true);
+ } else {
+ this._prettyPrintButton.removeAttribute("checked");
+ }
+
+ this.actions.togglePrettyPrint(source);
+ },
+
+ /**
+ * Toggle the black boxed state of the selected source.
+ */
+ toggleBlackBoxing: Task.async(function* () {
+ const source = getSelectedSource(this.getState());
+ const shouldBlackBox = !source.isBlackBoxed;
+
+ // Be optimistic that the (un-)black boxing will succeed, so
+ // enable/disable the pretty print button and check/uncheck the
+ // black box button immediately.
+ if (shouldBlackBox) {
+ this._prettyPrintButton.setAttribute("disabled", true);
+ this._blackBoxButton.setAttribute("checked", true);
+ } else {
+ this._prettyPrintButton.removeAttribute("disabled");
+ this._blackBoxButton.removeAttribute("checked");
+ }
+
+ this.actions.blackbox(source, shouldBlackBox);
+ }),
+
+ renderBlackBoxed: function (source) {
+ const sourceItem = this.getItemByValue(source.actor);
+ sourceItem.prebuiltNode.classList.toggle(
+ "black-boxed",
+ source.isBlackBoxed
+ );
+
+ if (getSelectedSource(this.getState()).actor === source.actor) {
+ this.updateToolbarButtonsState(source);
+ }
+ },
+
+ /**
+ * Toggles all breakpoints enabled/disabled.
+ */
+ toggleBreakpoints: function () {
+ let breakpoints = getBreakpoints(this.getState());
+ let hasBreakpoints = breakpoints.length > 0;
+ let hasEnabledBreakpoints = breakpoints.some(bp => !bp.disabled);
+
+ if (hasBreakpoints && hasEnabledBreakpoints) {
+ this._toggleBreakpointsButton.setAttribute("checked", true);
+ this._onDisableAll();
+ } else {
+ this._toggleBreakpointsButton.removeAttribute("checked");
+ this._onEnableAll();
+ }
+ },
+
+ hidePrettyPrinting: function () {
+ this._prettyPrintButton.style.display = "none";
+
+ if (this._blackBoxButton.style.display === "none") {
+ let sep = document.querySelector("#sources-toolbar .devtools-separator");
+ sep.style.display = "none";
+ }
+ },
+
+ hideBlackBoxing: function () {
+ this._blackBoxButton.style.display = "none";
+
+ if (this._prettyPrintButton.style.display === "none") {
+ let sep = document.querySelector("#sources-toolbar .devtools-separator");
+ sep.style.display = "none";
+ }
+ },
+
+ getDisplayURL: function (source) {
+ if (!source.url) {
+ return this.getItemByValue(source.actor).attachment.label;
+ }
+ return NetworkHelper.convertToUnicode(unescape(source.url));
+ },
+
+ /**
+ * Marks a breakpoint as selected in this sources container.
+ *
+ * @param object aItem
+ * The breakpoint item to select.
+ */
+ _selectBreakpoint: function (bp) {
+ if (this._selectedBreakpoint === bp) {
+ return;
+ }
+ this._unselectBreakpoint();
+ this._selectedBreakpoint = bp;
+
+ const item = this._getBreakpoint(bp);
+ item.target.classList.add("selected");
+
+ // Ensure the currently selected breakpoint is visible.
+ this.widget.ensureElementIsVisible(item.target);
+ },
+
+ /**
+ * Marks the current breakpoint as unselected in this sources container.
+ */
+ _unselectBreakpoint: function () {
+ if (!this._selectedBreakpoint) {
+ return;
+ }
+
+ const item = this._getBreakpoint(this._selectedBreakpoint);
+ item.target.classList.remove("selected");
+
+ this._selectedBreakpoint = null;
+ },
+
+ /**
+ * Opens a conditional breakpoint's expression input popup.
+ */
+ _openConditionalPopup: function () {
+ let breakpointItem = this._getBreakpoint(this._selectedBreakpoint);
+ let attachment = breakpointItem.attachment;
+ // Check if this is an enabled conditional breakpoint, and if so,
+ // retrieve the current conditional epression.
+ let bp = getBreakpoint(this.getState(), attachment);
+ let expr = (bp ? (bp.condition || "") : "");
+ let cbPanel = this._cbPanel;
+
+ // Update the conditional expression textbox. If no expression was
+ // previously set, revert to using an empty string by default.
+ this._cbTextbox.value = expr;
+
+ function openPopup() {
+ // Show the conditional expression panel. The popup arrow should be pointing
+ // at the line number node in the breakpoint item view.
+ cbPanel.hidden = false;
+ cbPanel.openPopup(breakpointItem.attachment.view.lineNumber,
+ BREAKPOINT_CONDITIONAL_POPUP_POSITION,
+ BREAKPOINT_CONDITIONAL_POPUP_OFFSET_X,
+ BREAKPOINT_CONDITIONAL_POPUP_OFFSET_Y);
+
+ cbPanel.removeEventListener("popuphidden", openPopup, false);
+ }
+
+ // Wait until the other cb panel is closed
+ if (!this._cbPanel.hidden) {
+ this._cbPanel.addEventListener("popuphidden", openPopup, false);
+ } else {
+ openPopup();
+ }
+ },
+
+ /**
+ * Hides a conditional breakpoint's expression input popup.
+ */
+ _hideConditionalPopup: function () {
+ // Sometimes this._cbPanel doesn't have hidePopup method which doesn't
+ // break anything but simply outputs an exception to the console.
+ if (this._cbPanel.hidePopup) {
+ this._cbPanel.hidePopup();
+ }
+ },
+
+ /**
+ * Customization function for creating a breakpoint item's UI.
+ *
+ * @param object aOptions
+ * A couple of options or flags supported by this operation:
+ * - location: the breakpoint's source location and line number
+ * - disabled: the breakpoint's disabled state, boolean
+ * - text: the breakpoint's line text to be displayed
+ * - message: thrown string when the breakpoint condition throws
+ * @return object
+ * An object containing the breakpoint container, checkbox,
+ * line number and line text nodes.
+ */
+ _createBreakpointView: function (aOptions) {
+ let { location, disabled, text, message } = aOptions;
+ let identifier = makeLocationId(location);
+
+ let checkbox = document.createElement("checkbox");
+ if (!disabled) {
+ checkbox.setAttribute("checked", true);
+ }
+ checkbox.className = "dbg-breakpoint-checkbox";
+
+ let lineNumberNode = document.createElement("label");
+ lineNumberNode.className = "plain dbg-breakpoint-line";
+ lineNumberNode.setAttribute("value", location.line);
+
+ let lineTextNode = document.createElement("label");
+ lineTextNode.className = "plain dbg-breakpoint-text";
+ lineTextNode.setAttribute("value", text);
+ lineTextNode.setAttribute("crop", "end");
+ lineTextNode.setAttribute("flex", "1");
+
+ let tooltip = text ? text.substr(0, BREAKPOINT_LINE_TOOLTIP_MAX_LENGTH) : "";
+ lineTextNode.setAttribute("tooltiptext", tooltip);
+
+ let thrownNode = document.createElement("label");
+ thrownNode.className = "plain dbg-breakpoint-condition-thrown-message dbg-breakpoint-text";
+ thrownNode.setAttribute("value", message);
+ thrownNode.setAttribute("crop", "end");
+ thrownNode.setAttribute("flex", "1");
+
+ let bpLineContainer = document.createElement("hbox");
+ bpLineContainer.className = "plain dbg-breakpoint-line-container";
+ bpLineContainer.setAttribute("flex", "1");
+
+ bpLineContainer.appendChild(lineNumberNode);
+ bpLineContainer.appendChild(lineTextNode);
+
+ let bpDetailContainer = document.createElement("vbox");
+ bpDetailContainer.className = "plain dbg-breakpoint-detail-container";
+ bpDetailContainer.setAttribute("flex", "1");
+
+ bpDetailContainer.appendChild(bpLineContainer);
+ bpDetailContainer.appendChild(thrownNode);
+
+ let container = document.createElement("hbox");
+ container.id = "breakpoint-" + identifier;
+ container.className = "dbg-breakpoint side-menu-widget-item-other";
+ container.classList.add("devtools-monospace");
+ container.setAttribute("align", "center");
+ container.setAttribute("flex", "1");
+
+ container.addEventListener("click", this._onBreakpointClick, false);
+ checkbox.addEventListener("click", this._onBreakpointCheckboxClick, false);
+
+ container.appendChild(checkbox);
+ container.appendChild(bpDetailContainer);
+
+ return {
+ container: container,
+ checkbox: checkbox,
+ lineNumber: lineNumberNode,
+ lineText: lineTextNode,
+ message: thrownNode
+ };
+ },
+
+ /**
+ * Creates a context menu for a breakpoint element.
+ *
+ * @param object aOptions
+ * A couple of options or flags supported by this operation:
+ * - location: the breakpoint's source location and line number
+ * - disabled: the breakpoint's disabled state, boolean
+ * @return object
+ * An object containing the breakpoint commandset and menu popup ids.
+ */
+ _createContextMenu: function (aOptions) {
+ let { location, disabled } = aOptions;
+ let identifier = makeLocationId(location);
+
+ let commandset = document.createElement("commandset");
+ let menupopup = document.createElement("menupopup");
+ commandset.id = "bp-cSet-" + identifier;
+ menupopup.id = "bp-mPop-" + identifier;
+
+ createMenuItem.call(this, "enableSelf", !disabled);
+ createMenuItem.call(this, "disableSelf", disabled);
+ createMenuItem.call(this, "deleteSelf");
+ createMenuSeparator();
+ createMenuItem.call(this, "setConditional");
+ createMenuSeparator();
+ createMenuItem.call(this, "enableOthers");
+ createMenuItem.call(this, "disableOthers");
+ createMenuItem.call(this, "deleteOthers");
+ createMenuSeparator();
+ createMenuItem.call(this, "enableAll");
+ createMenuItem.call(this, "disableAll");
+ createMenuSeparator();
+ createMenuItem.call(this, "deleteAll");
+
+ this._popupset.appendChild(menupopup);
+ this._commandset.appendChild(commandset);
+
+ return {
+ commandsetId: commandset.id,
+ menupopupId: menupopup.id
+ };
+
+ /**
+ * Creates a menu item specified by a name with the appropriate attributes
+ * (label and handler).
+ *
+ * @param string aName
+ * A global identifier for the menu item.
+ * @param boolean aHiddenFlag
+ * True if this menuitem should be hidden.
+ */
+ function createMenuItem(aName, aHiddenFlag) {
+ let menuitem = document.createElement("menuitem");
+ let command = document.createElement("command");
+
+ let prefix = "bp-cMenu-"; // "breakpoints context menu"
+ let commandId = prefix + aName + "-" + identifier + "-command";
+ let menuitemId = prefix + aName + "-" + identifier + "-menuitem";
+
+ let label = L10N.getStr("breakpointMenuItem." + aName);
+ let func = "_on" + aName.charAt(0).toUpperCase() + aName.slice(1);
+
+ command.id = commandId;
+ command.setAttribute("label", label);
+ command.addEventListener("command", () => this[func](location), false);
+
+ menuitem.id = menuitemId;
+ menuitem.setAttribute("command", commandId);
+ aHiddenFlag && menuitem.setAttribute("hidden", "true");
+
+ commandset.appendChild(command);
+ menupopup.appendChild(menuitem);
+ }
+
+ /**
+ * Creates a simple menu separator element and appends it to the current
+ * menupopup hierarchy.
+ */
+ function createMenuSeparator() {
+ let menuseparator = document.createElement("menuseparator");
+ menupopup.appendChild(menuseparator);
+ }
+ },
+
+ /**
+ * Copy the source url from the currently selected item.
+ */
+ _onCopyUrlCommand: function () {
+ let selected = this.selectedItem && this.selectedItem.attachment;
+ if (!selected) {
+ return;
+ }
+ clipboardHelper.copyString(selected.source.url);
+ },
+
+ /**
+ * Opens selected item source in a new tab.
+ */
+ _onNewTabCommand: function () {
+ let win = Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
+ let selected = this.selectedItem.attachment;
+ win.openUILinkIn(selected.source.url, "tab", { relatedToCurrent: true });
+ },
+
+ /**
+ * Function called each time a breakpoint item is removed.
+ *
+ * @param object aItem
+ * The corresponding item.
+ */
+ _onBreakpointRemoved: function (aItem) {
+ dumpn("Finalizing breakpoint item: " + aItem.stringify());
+
+ // Destroy the context menu for the breakpoint.
+ let contextMenu = aItem.attachment.popup;
+ document.getElementById(contextMenu.commandsetId).remove();
+ document.getElementById(contextMenu.menupopupId).remove();
+ },
+
+ _onMouseDown: function (e) {
+ this.hideNoResultsTooltip();
+
+ if (!e.metaKey) {
+ return;
+ }
+
+ let editor = this.DebuggerView.editor;
+ let identifier = this._findIdentifier(e.clientX, e.clientY);
+
+ if (!identifier) {
+ return;
+ }
+
+ let foundDefinitions = this._getFunctionDefinitions(identifier);
+
+ if (!foundDefinitions || !foundDefinitions.definitions) {
+ return;
+ }
+
+ this._showFunctionDefinitionResults(identifier, foundDefinitions.definitions, editor);
+ },
+
+ /**
+ * Searches for function definition of a function in a given source file
+ */
+
+ _findDefinition: function (parsedSource, aName) {
+ let functionDefinitions = parsedSource.getNamedFunctionDefinitions(aName);
+
+ let resultList = [];
+
+ if (!functionDefinitions || !functionDefinitions.length || !functionDefinitions[0].length) {
+ return {
+ definitions: resultList
+ };
+ }
+
+ // functionDefinitions is a list with an object full of metadata,
+ // extract the data and use to construct a more useful, less
+ // cluttered, contextual list
+ for (let i = 0; i < functionDefinitions.length; i++) {
+ let functionDefinition = {
+ source: functionDefinitions[i].sourceUrl,
+ startLine: functionDefinitions[i][0].functionLocation.start.line,
+ startColumn: functionDefinitions[i][0].functionLocation.start.column,
+ name: functionDefinitions[i][0].functionName
+ };
+
+ resultList.push(functionDefinition);
+ }
+
+ return {
+ definitions: resultList
+ };
+ },
+
+ /**
+ * Searches for an identifier underneath the specified position in the
+ * source editor.
+ *
+ * @param number x, y
+ * The left/top coordinates where to look for an identifier.
+ */
+ _findIdentifier: function (x, y) {
+ let parsedSource = SourceUtils.parseSource(this.DebuggerView, this.Parser);
+ let identifierInfo = SourceUtils.findIdentifier(this.DebuggerView.editor, parsedSource, x, y);
+
+ // Not hovering over an identifier
+ if (!identifierInfo) {
+ return;
+ }
+
+ return identifierInfo;
+ },
+
+ /**
+ * The selection listener for the source editor.
+ */
+ _onEditorCursorActivity: function (e) {
+ let editor = this.DebuggerView.editor;
+ let start = editor.getCursor("start").line + 1;
+ let end = editor.getCursor().line + 1;
+ let source = getSelectedSource(this.getState());
+
+ if (source) {
+ let location = { actor: source.actor, line: start };
+ if (getBreakpoint(this.getState(), location) && start == end) {
+ this.highlightBreakpoint(location, { noEditorUpdate: true });
+ } else {
+ this.unhighlightBreakpoint();
+ }
+ }
+ },
+
+ /*
+ * Uses function definition data to perform actions in different
+ * cases of how many locations were found: zero, one, or multiple definitions
+ */
+ _showFunctionDefinitionResults: function (aHoveredFunction, aDefinitionList, aEditor) {
+ let definitions = aDefinitionList;
+ let hoveredFunction = aHoveredFunction;
+
+ // show a popup saying no results were found
+ if (definitions.length == 0) {
+ this._noResultsFoundToolTip.setTextContent({
+ messages: [L10N.getStr("noMatchingStringsText")]
+ });
+
+ this._markedIdentifier = aEditor.markText(
+ { line: hoveredFunction.location.start.line - 1, ch: hoveredFunction.location.start.column },
+ { line: hoveredFunction.location.end.line - 1, ch: hoveredFunction.location.end.column });
+
+ this._noResultsFoundToolTip.show(this._markedIdentifier.anchor);
+
+ } else if (definitions.length == 1) {
+ this.DebuggerView.setEditorLocation(definitions[0].source, definitions[0].startLine);
+ } else {
+ // TODO: multiple definitions found, do something else
+ this.DebuggerView.setEditorLocation(definitions[0].source, definitions[0].startLine);
+ }
+ },
+
+ /**
+ * Hides the tooltip and clear marked text popup.
+ */
+ hideNoResultsTooltip: function () {
+ this._noResultsFoundToolTip.hide();
+ if (this._markedIdentifier) {
+ this._markedIdentifier.clear();
+ this._markedIdentifier = null;
+ }
+ },
+
+ /*
+ * Gets the definition locations from function metadata
+ */
+ _getFunctionDefinitions: function (aIdentifierInfo) {
+ let parsedSource = SourceUtils.parseSource(this.DebuggerView, this.Parser);
+ let definition_info = this._findDefinition(parsedSource, aIdentifierInfo.name);
+
+ // Did not find any definitions for the identifier
+ if (!definition_info) {
+ return;
+ }
+
+ return definition_info;
+ },
+
+ /**
+ * The select listener for the sources container.
+ */
+ _onSourceSelect: function ({ detail: sourceItem }) {
+ if (!sourceItem) {
+ return;
+ }
+
+ const { source } = sourceItem.attachment;
+ this.actions.selectSource(source);
+ },
+
+ renderSourceSelected: function (source) {
+ if (source.url) {
+ this._preferredSourceURL = source.url;
+ }
+ this.updateToolbarButtonsState(source);
+ this._selectItem(this.getItemByValue(source.actor));
+ },
+
+ /**
+ * The click listener for the "stop black boxing" button.
+ */
+ _onStopBlackBoxing: Task.async(function* () {
+ this.actions.blackbox(getSelectedSource(this.getState()), false);
+ }),
+
+ /**
+ * The source editor's contextmenu handler.
+ * - Toggles "Add Conditional Breakpoint" and "Edit Conditional Breakpoint" items
+ */
+ _onEditorContextMenuOpen: function (message, ev, popup) {
+ let actor = this.selectedValue;
+ let line = this.DebuggerView.editor.getCursor().line + 1;
+ let location = { actor, line };
+
+ let breakpoint = getBreakpoint(this.getState(), location);
+ let addConditionalBreakpointMenuItem = popup.querySelector("#se-dbg-cMenu-addConditionalBreakpoint");
+ let editConditionalBreakpointMenuItem = popup.querySelector("#se-dbg-cMenu-editConditionalBreakpoint");
+
+ if (breakpoint && !!breakpoint.condition) {
+ editConditionalBreakpointMenuItem.removeAttribute("hidden");
+ addConditionalBreakpointMenuItem.setAttribute("hidden", true);
+ }
+ else {
+ addConditionalBreakpointMenuItem.removeAttribute("hidden");
+ editConditionalBreakpointMenuItem.setAttribute("hidden", true);
+ }
+ },
+
+ /**
+ * The click listener for a breakpoint container.
+ */
+ _onBreakpointClick: function (e) {
+ let sourceItem = this.getItemForElement(e.target);
+ let breakpointItem = this.getItemForElement.call(sourceItem, e.target);
+ let attachment = breakpointItem.attachment;
+ let bp = getBreakpoint(this.getState(), attachment);
+ if (bp) {
+ this.highlightBreakpoint(bp.location, {
+ openPopup: bp.condition && e.button == 0
+ });
+ } else {
+ this.highlightBreakpoint(bp.location);
+ }
+ },
+
+ /**
+ * The click listener for a breakpoint checkbox.
+ */
+ _onBreakpointCheckboxClick: function (e) {
+ let sourceItem = this.getItemForElement(e.target);
+ let breakpointItem = this.getItemForElement.call(sourceItem, e.target);
+ let bp = getBreakpoint(this.getState(), breakpointItem.attachment);
+
+ if (bp.disabled) {
+ this.actions.enableBreakpoint(bp.location);
+ }
+ else {
+ this.actions.disableBreakpoint(bp.location);
+ }
+
+ // Don't update the editor location (avoid propagating into _onBreakpointClick).
+ e.preventDefault();
+ e.stopPropagation();
+ },
+
+ /**
+ * The popup showing listener for the breakpoints conditional expression panel.
+ */
+ _onConditionalPopupShowing: function () {
+ this._conditionalPopupVisible = true; // Used in tests.
+ },
+
+ /**
+ * The popup shown listener for the breakpoints conditional expression panel.
+ */
+ _onConditionalPopupShown: function () {
+ this._cbTextbox.focus();
+ this._cbTextbox.select();
+ window.emit(EVENTS.CONDITIONAL_BREAKPOINT_POPUP_SHOWN);
+ },
+
+ /**
+ * The popup hiding listener for the breakpoints conditional expression panel.
+ */
+ _onConditionalPopupHiding: function () {
+ this._conditionalPopupVisible = false; // Used in tests.
+
+ // Check if this is an enabled conditional breakpoint, and if so,
+ // save the current conditional expression.
+ let bp = this._selectedBreakpoint;
+ if (bp) {
+ let condition = this._cbTextbox.value;
+ this.actions.setBreakpointCondition(bp.location, condition);
+ }
+ },
+
+ /**
+ * The popup hidden listener for the breakpoints conditional expression panel.
+ */
+ _onConditionalPopupHidden: function () {
+ this._cbPanel.hidden = true;
+ window.emit(EVENTS.CONDITIONAL_BREAKPOINT_POPUP_HIDDEN);
+ },
+
+ /**
+ * The keypress listener for the breakpoints conditional expression textbox.
+ */
+ _onConditionalTextboxKeyPress: function (e) {
+ if (e.keyCode == KeyCodes.DOM_VK_RETURN) {
+ this._hideConditionalPopup();
+ }
+ },
+
+ /**
+ * Called when the add breakpoint key sequence was pressed.
+ */
+ _onCmdAddBreakpoint: function (e) {
+ let actor = this.selectedValue;
+ let line = (this.DebuggerView.clickedLine ?
+ this.DebuggerView.clickedLine + 1 :
+ this.DebuggerView.editor.getCursor().line + 1);
+ let location = { actor, line };
+ let bp = getBreakpoint(this.getState(), location);
+
+ // If a breakpoint already existed, remove it now.
+ if (bp) {
+ this.actions.removeBreakpoint(bp.location);
+ }
+ // No breakpoint existed at the required location, add one now.
+ else {
+ this.actions.addBreakpoint(location);
+ }
+ },
+
+ /**
+ * Called when the add conditional breakpoint key sequence was pressed.
+ */
+ _onCmdAddConditionalBreakpoint: function (e) {
+ let actor = this.selectedValue;
+ let line = (this.DebuggerView.clickedLine ?
+ this.DebuggerView.clickedLine + 1 :
+ this.DebuggerView.editor.getCursor().line + 1);
+
+ let location = { actor, line };
+ let bp = getBreakpoint(this.getState(), location);
+
+ // If a breakpoint already existed or wasn't a conditional, morph it now.
+ if (bp) {
+ this.highlightBreakpoint(bp.location, { openPopup: true });
+ }
+ // No breakpoint existed at the required location, add one now.
+ else {
+ this.actions.addBreakpoint(location, "");
+ }
+ },
+
+ getOtherBreakpoints: function (location) {
+ const bps = getBreakpoints(this.getState());
+ if (location) {
+ return bps.filter(bp => {
+ return (bp.location.actor !== location.actor ||
+ bp.location.line !== location.line);
+ });
+ }
+ return bps;
+ },
+
+ /**
+ * Function invoked on the "setConditional" menuitem command.
+ *
+ * @param object aLocation
+ * @see DebuggerController.Breakpoints.addBreakpoint
+ */
+ _onSetConditional: function (aLocation) {
+ // Highlight the breakpoint and show a conditional expression popup.
+ this.highlightBreakpoint(aLocation, { openPopup: true });
+ },
+
+ /**
+ * Function invoked on the "enableSelf" menuitem command.
+ *
+ * @param object aLocation
+ * @see DebuggerController.Breakpoints.addBreakpoint
+ */
+ _onEnableSelf: function (aLocation) {
+ // Enable the breakpoint, in this container and the controller store.
+ this.actions.enableBreakpoint(aLocation);
+ },
+
+ /**
+ * Function invoked on the "disableSelf" menuitem command.
+ *
+ * @param object aLocation
+ * @see DebuggerController.Breakpoints.addBreakpoint
+ */
+ _onDisableSelf: function (aLocation) {
+ const bp = getBreakpoint(this.getState(), aLocation);
+ if (!bp.disabled) {
+ this.actions.disableBreakpoint(aLocation);
+ }
+ },
+
+ /**
+ * Function invoked on the "deleteSelf" menuitem command.
+ *
+ * @param object aLocation
+ * @see DebuggerController.Breakpoints.addBreakpoint
+ */
+ _onDeleteSelf: function (aLocation) {
+ this.actions.removeBreakpoint(aLocation);
+ },
+
+ /**
+ * Function invoked on the "enableOthers" menuitem command.
+ *
+ * @param object aLocation
+ * @see DebuggerController.Breakpoints.addBreakpoint
+ */
+ _onEnableOthers: function (aLocation) {
+ let other = this.getOtherBreakpoints(aLocation);
+ // TODO(jwl): batch these and interrupt the thread for all of them
+ other.forEach(bp => this._onEnableSelf(bp.location));
+ },
+
+ /**
+ * Function invoked on the "disableOthers" menuitem command.
+ *
+ * @param object aLocation
+ * @see DebuggerController.Breakpoints.addBreakpoint
+ */
+ _onDisableOthers: function (aLocation) {
+ let other = this.getOtherBreakpoints(aLocation);
+ other.forEach(bp => this._onDisableSelf(bp.location));
+ },
+
+ /**
+ * Function invoked on the "deleteOthers" menuitem command.
+ *
+ * @param object aLocation
+ * @see DebuggerController.Breakpoints.addBreakpoint
+ */
+ _onDeleteOthers: function (aLocation) {
+ let other = this.getOtherBreakpoints(aLocation);
+ other.forEach(bp => this._onDeleteSelf(bp.location));
+ },
+
+ /**
+ * Function invoked on the "enableAll" menuitem command.
+ */
+ _onEnableAll: function () {
+ this._onEnableOthers(undefined);
+ },
+
+ /**
+ * Function invoked on the "disableAll" menuitem command.
+ */
+ _onDisableAll: function () {
+ this._onDisableOthers(undefined);
+ },
+
+ /**
+ * Function invoked on the "deleteAll" menuitem command.
+ */
+ _onDeleteAll: function () {
+ this._onDeleteOthers(undefined);
+ },
+
+ _commandset: null,
+ _popupset: null,
+ _cmPopup: null,
+ _cbPanel: null,
+ _cbTextbox: null,
+ _selectedBreakpointItem: null,
+ _conditionalPopupVisible: false,
+ _noResultsFoundToolTip: null,
+ _markedIdentifier: null,
+ _selectedBreakpoint: null,
+ _conditionalPopupVisible: false
+});
+
+module.exports = SourcesView;
diff --git a/devtools/client/debugger/debugger-commands.js b/devtools/client/debugger/debugger-commands.js
new file mode 100644
index 000000000..3229646f8
--- /dev/null
+++ b/devtools/client/debugger/debugger-commands.js
@@ -0,0 +1,633 @@
+/* -*- 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 { Cc, Ci, Cu } = require("chrome");
+const l10n = require("gcli/l10n");
+loader.lazyRequireGetter(this, "gDevTools",
+ "devtools/client/framework/devtools", true);
+
+/**
+ * The commands and converters that are exported to GCLI
+ */
+exports.items = [];
+
+/**
+ * Utility to get access to the current breakpoint list.
+ *
+ * @param DebuggerPanel dbg
+ * The debugger panel.
+ * @return array
+ * An array of objects, one for each breakpoint, where each breakpoint
+ * object has the following properties:
+ * - url: the URL of the source file.
+ * - label: a unique string identifier designed to be user visible.
+ * - lineNumber: the line number of the breakpoint in the source file.
+ * - lineText: the text of the line at the breakpoint.
+ * - truncatedLineText: lineText truncated to MAX_LINE_TEXT_LENGTH.
+ */
+function getAllBreakpoints(dbg) {
+ let breakpoints = [];
+ let sources = dbg._view.Sources;
+ let { trimUrlLength: trim } = dbg.panelWin.SourceUtils;
+
+ for (let source of sources) {
+ for (let { attachment: breakpoint } of source) {
+ breakpoints.push({
+ url: source.attachment.source.url,
+ label: source.attachment.label + ":" + breakpoint.line,
+ lineNumber: breakpoint.line,
+ lineText: breakpoint.text,
+ truncatedLineText: trim(breakpoint.text, MAX_LINE_TEXT_LENGTH, "end")
+ });
+ }
+ }
+
+ return breakpoints;
+}
+
+function getAllSources(dbg) {
+ if (!dbg) {
+ return [];
+ }
+
+ let items = dbg._view.Sources.items;
+ return items
+ .filter(item => !!item.attachment.source.url)
+ .map(item => ({
+ name: item.attachment.source.url,
+ value: item.attachment.source.actor
+ }));
+}
+
+/**
+ * 'break' command
+ */
+exports.items.push({
+ name: "break",
+ description: l10n.lookup("breakDesc"),
+ manual: l10n.lookup("breakManual")
+});
+
+/**
+ * 'break list' command
+ */
+exports.items.push({
+ name: "break list",
+ item: "command",
+ runAt: "client",
+ description: l10n.lookup("breaklistDesc"),
+ returnType: "breakpoints",
+ exec: function (args, context) {
+ let dbg = getPanel(context, "jsdebugger", { ensureOpened: true });
+ return dbg.then(getAllBreakpoints);
+ }
+});
+
+exports.items.push({
+ item: "converter",
+ from: "breakpoints",
+ to: "view",
+ exec: function (breakpoints, context) {
+ let dbg = getPanel(context, "jsdebugger");
+ if (dbg && breakpoints.length) {
+ return context.createView({
+ html: breakListHtml,
+ data: {
+ breakpoints: breakpoints,
+ onclick: context.update,
+ ondblclick: context.updateExec
+ }
+ });
+ } else {
+ return context.createView({
+ html: "<p>${message}</p>",
+ data: { message: l10n.lookup("breaklistNone") }
+ });
+ }
+ }
+});
+
+var breakListHtml = "" +
+ "<table>" +
+ " <thead>" +
+ " <th>Source</th>" +
+ " <th>Line</th>" +
+ " <th>Actions</th>" +
+ " </thead>" +
+ " <tbody>" +
+ " <tr foreach='breakpoint in ${breakpoints}'>" +
+ " <td class='gcli-breakpoint-label'>${breakpoint.label}</td>" +
+ " <td class='gcli-breakpoint-lineText'>" +
+ " ${breakpoint.truncatedLineText}" +
+ " </td>" +
+ " <td>" +
+ " <span class='gcli-out-shortcut'" +
+ " data-command='break del ${breakpoint.label}'" +
+ " onclick='${onclick}'" +
+ " ondblclick='${ondblclick}'>" +
+ " " + l10n.lookup("breaklistOutRemove") + "</span>" +
+ " </td>" +
+ " </tr>" +
+ " </tbody>" +
+ "</table>" +
+ "";
+
+var MAX_LINE_TEXT_LENGTH = 30;
+var MAX_LABEL_LENGTH = 20;
+
+/**
+ * 'break add' command
+ */
+exports.items.push({
+ name: "break add",
+ description: l10n.lookup("breakaddDesc"),
+ manual: l10n.lookup("breakaddManual")
+});
+
+/**
+ * 'break add line' command
+ */
+exports.items.push({
+ item: "command",
+ runAt: "client",
+ name: "break add line",
+ description: l10n.lookup("breakaddlineDesc"),
+ params: [
+ {
+ name: "file",
+ type: {
+ name: "selection",
+ lookup: function (context) {
+ return getAllSources(getPanel(context, "jsdebugger"));
+ }
+ },
+ description: l10n.lookup("breakaddlineFileDesc")
+ },
+ {
+ name: "line",
+ type: { name: "number", min: 1, step: 10 },
+ description: l10n.lookup("breakaddlineLineDesc")
+ }
+ ],
+ returnType: "string",
+ exec: function (args, context) {
+ let dbg = getPanel(context, "jsdebugger");
+ if (!dbg) {
+ return l10n.lookup("debuggerStopped");
+ }
+
+ let deferred = context.defer();
+ let item = dbg._view.Sources.getItemForAttachment(a => {
+ return a.source && a.source.actor === args.file;
+ });
+ let position = { actor: item.value, line: args.line };
+
+ dbg.addBreakpoint(position).then(() => {
+ deferred.resolve(l10n.lookup("breakaddAdded"));
+ }, aError => {
+ deferred.resolve(l10n.lookupFormat("breakaddFailed", [aError]));
+ });
+
+ return deferred.promise;
+ }
+});
+
+/**
+ * 'break del' command
+ */
+exports.items.push({
+ item: "command",
+ runAt: "client",
+ name: "break del",
+ description: l10n.lookup("breakdelDesc"),
+ params: [
+ {
+ name: "breakpoint",
+ type: {
+ name: "selection",
+ lookup: function (context) {
+ let dbg = getPanel(context, "jsdebugger");
+ if (!dbg) {
+ return [];
+ }
+ return getAllBreakpoints(dbg).map(breakpoint => ({
+ name: breakpoint.label,
+ value: breakpoint,
+ description: breakpoint.truncatedLineText
+ }));
+ }
+ },
+ description: l10n.lookup("breakdelBreakidDesc")
+ }
+ ],
+ returnType: "string",
+ exec: function (args, context) {
+ let dbg = getPanel(context, "jsdebugger");
+ if (!dbg) {
+ return l10n.lookup("debuggerStopped");
+ }
+
+ let source = dbg._view.Sources.getItemForAttachment(a => {
+ return a.source && a.source.url === args.breakpoint.url;
+ });
+
+ let deferred = context.defer();
+ let position = { actor: source.attachment.source.actor,
+ line: args.breakpoint.lineNumber };
+
+ dbg.removeBreakpoint(position).then(() => {
+ deferred.resolve(l10n.lookup("breakdelRemoved"));
+ }, () => {
+ deferred.resolve(l10n.lookup("breakNotFound"));
+ });
+
+ return deferred.promise;
+ }
+});
+
+/**
+ * 'dbg' command
+ */
+exports.items.push({
+ name: "dbg",
+ description: l10n.lookup("dbgDesc"),
+ manual: l10n.lookup("dbgManual")
+});
+
+/**
+ * 'dbg open' command
+ */
+exports.items.push({
+ item: "command",
+ runAt: "client",
+ name: "dbg open",
+ description: l10n.lookup("dbgOpen"),
+ params: [],
+ exec: function (args, context) {
+ let target = context.environment.target;
+ return gDevTools.showToolbox(target, "jsdebugger").then(() => null);
+ }
+});
+
+/**
+ * 'dbg close' command
+ */
+exports.items.push({
+ item: "command",
+ runAt: "client",
+ name: "dbg close",
+ description: l10n.lookup("dbgClose"),
+ params: [],
+ exec: function (args, context) {
+ if (!getPanel(context, "jsdebugger")) {
+ return;
+ }
+ let target = context.environment.target;
+ return gDevTools.closeToolbox(target).then(() => null);
+ }
+});
+
+/**
+ * 'dbg interrupt' command
+ */
+exports.items.push({
+ item: "command",
+ runAt: "client",
+ name: "dbg interrupt",
+ description: l10n.lookup("dbgInterrupt"),
+ params: [],
+ exec: function (args, context) {
+ let dbg = getPanel(context, "jsdebugger");
+ if (!dbg) {
+ return l10n.lookup("debuggerStopped");
+ }
+
+ let controller = dbg._controller;
+ let thread = controller.activeThread;
+ if (!thread.paused) {
+ thread.interrupt();
+ }
+ }
+});
+
+/**
+ * 'dbg continue' command
+ */
+exports.items.push({
+ item: "command",
+ runAt: "client",
+ name: "dbg continue",
+ description: l10n.lookup("dbgContinue"),
+ params: [],
+ exec: function (args, context) {
+ let dbg = getPanel(context, "jsdebugger");
+ if (!dbg) {
+ return l10n.lookup("debuggerStopped");
+ }
+
+ let controller = dbg._controller;
+ let thread = controller.activeThread;
+ if (thread.paused) {
+ thread.resume();
+ }
+ }
+});
+
+/**
+ * 'dbg step' command
+ */
+exports.items.push({
+ item: "command",
+ runAt: "client",
+ name: "dbg step",
+ description: l10n.lookup("dbgStepDesc"),
+ manual: l10n.lookup("dbgStepManual")
+});
+
+/**
+ * 'dbg step over' command
+ */
+exports.items.push({
+ item: "command",
+ runAt: "client",
+ name: "dbg step over",
+ description: l10n.lookup("dbgStepOverDesc"),
+ params: [],
+ exec: function (args, context) {
+ let dbg = getPanel(context, "jsdebugger");
+ if (!dbg) {
+ return l10n.lookup("debuggerStopped");
+ }
+
+ let controller = dbg._controller;
+ let thread = controller.activeThread;
+ if (thread.paused) {
+ thread.stepOver();
+ }
+ }
+});
+
+/**
+ * 'dbg step in' command
+ */
+exports.items.push({
+ item: "command",
+ runAt: "client",
+ name: "dbg step in",
+ description: l10n.lookup("dbgStepInDesc"),
+ params: [],
+ exec: function (args, context) {
+ let dbg = getPanel(context, "jsdebugger");
+ if (!dbg) {
+ return l10n.lookup("debuggerStopped");
+ }
+
+ let controller = dbg._controller;
+ let thread = controller.activeThread;
+ if (thread.paused) {
+ thread.stepIn();
+ }
+ }
+});
+
+/**
+ * 'dbg step over' command
+ */
+exports.items.push({
+ item: "command",
+ runAt: "client",
+ name: "dbg step out",
+ description: l10n.lookup("dbgStepOutDesc"),
+ params: [],
+ exec: function (args, context) {
+ let dbg = getPanel(context, "jsdebugger");
+ if (!dbg) {
+ return l10n.lookup("debuggerStopped");
+ }
+
+ let controller = dbg._controller;
+ let thread = controller.activeThread;
+ if (thread.paused) {
+ thread.stepOut();
+ }
+ }
+});
+
+/**
+ * 'dbg list' command
+ */
+exports.items.push({
+ item: "command",
+ runAt: "client",
+ name: "dbg list",
+ description: l10n.lookup("dbgListSourcesDesc"),
+ params: [],
+ returnType: "dom",
+ exec: function (args, context) {
+ let dbg = getPanel(context, "jsdebugger");
+ if (!dbg) {
+ return l10n.lookup("debuggerClosed");
+ }
+
+ let sources = getAllSources(dbg);
+ let doc = context.environment.chromeDocument;
+ let div = createXHTMLElement(doc, "div");
+ let ol = createXHTMLElement(doc, "ol");
+
+ sources.forEach(source => {
+ let li = createXHTMLElement(doc, "li");
+ li.textContent = source.name;
+ ol.appendChild(li);
+ });
+ div.appendChild(ol);
+
+ return div;
+ }
+});
+
+/**
+ * Define the 'dbg blackbox' and 'dbg unblackbox' commands.
+ */
+[
+ {
+ name: "blackbox",
+ clientMethod: "blackBox",
+ l10nPrefix: "dbgBlackBox"
+ },
+ {
+ name: "unblackbox",
+ clientMethod: "unblackBox",
+ l10nPrefix: "dbgUnBlackBox"
+ }
+].forEach(function (cmd) {
+ const lookup = function (id) {
+ return l10n.lookup(cmd.l10nPrefix + id);
+ };
+
+ exports.items.push({
+ item: "command",
+ runAt: "client",
+ name: "dbg " + cmd.name,
+ description: lookup("Desc"),
+ params: [
+ {
+ name: "source",
+ type: {
+ name: "selection",
+ lookup: function (context) {
+ return getAllSources(getPanel(context, "jsdebugger"));
+ }
+ },
+ description: lookup("SourceDesc"),
+ defaultValue: null
+ },
+ {
+ name: "glob",
+ type: "string",
+ description: lookup("GlobDesc"),
+ defaultValue: null
+ },
+ {
+ name: "invert",
+ type: "boolean",
+ description: lookup("InvertDesc")
+ }
+ ],
+ returnType: "dom",
+ exec: function (args, context) {
+ const dbg = getPanel(context, "jsdebugger");
+ const doc = context.environment.chromeDocument;
+ if (!dbg) {
+ throw new Error(l10n.lookup("debuggerClosed"));
+ }
+
+ const { promise, resolve, reject } = context.defer();
+ const { activeThread } = dbg._controller;
+ const globRegExp = args.glob ? globToRegExp(args.glob) : null;
+
+ // Filter the sources down to those that we will need to black box.
+
+ function shouldBlackBox(source) {
+ var value = globRegExp && globRegExp.test(source.url)
+ || args.source && source.actor == args.source;
+ return args.invert ? !value : value;
+ }
+
+ const toBlackBox = [];
+ for (let {attachment: {source}} of dbg._view.Sources.items) {
+ if (shouldBlackBox(source)) {
+ toBlackBox.push(source);
+ }
+ }
+
+ // If we aren't black boxing any sources, bail out now.
+
+ if (toBlackBox.length === 0) {
+ const empty = createXHTMLElement(doc, "div");
+ empty.textContent = lookup("EmptyDesc");
+ return void resolve(empty);
+ }
+
+ // Send the black box request to each source we are black boxing. As we
+ // get responses, accumulate the results in `blackBoxed`.
+
+ const blackBoxed = [];
+
+ for (let source of toBlackBox) {
+ dbg.blackbox(source, cmd.clientMethod === "blackBox").then(() => {
+ blackBoxed.push(source.url);
+ }, err => {
+ blackBoxed.push(lookup("ErrorDesc") + " " + source.url);
+ }).then(() => {
+ if (toBlackBox.length === blackBoxed.length) {
+ displayResults();
+ }
+ });
+ }
+
+ // List the results for the user.
+
+ function displayResults() {
+ const results = doc.createElement("div");
+ results.textContent = lookup("NonEmptyDesc");
+
+ const list = createXHTMLElement(doc, "ul");
+ results.appendChild(list);
+
+ for (let result of blackBoxed) {
+ const item = createXHTMLElement(doc, "li");
+ item.textContent = result;
+ list.appendChild(item);
+ }
+ resolve(results);
+ }
+
+ return promise;
+ }
+ });
+});
+
+/**
+ * A helper to create xhtml namespaced elements.
+ */
+function createXHTMLElement(document, tagname) {
+ return document.createElementNS("http://www.w3.org/1999/xhtml", tagname);
+}
+
+/**
+ * A helper to go from a command context to a debugger panel.
+ */
+function getPanel(context, id, options = {}) {
+ if (!context) {
+ return undefined;
+ }
+
+ let target = context.environment.target;
+
+ if (options.ensureOpened) {
+ return gDevTools.showToolbox(target, id).then(toolbox => {
+ return toolbox.getPanel(id);
+ });
+ } else {
+ let toolbox = gDevTools.getToolbox(target);
+ if (toolbox) {
+ return toolbox.getPanel(id);
+ } else {
+ return undefined;
+ }
+ }
+}
+
+/**
+ * Converts a glob to a regular expression.
+ */
+function globToRegExp(glob) {
+ const reStr = glob
+ // Escape existing regular expression syntax.
+ .replace(/\\/g, "\\\\")
+ .replace(/\//g, "\\/")
+ .replace(/\^/g, "\\^")
+ .replace(/\$/g, "\\$")
+ .replace(/\+/g, "\\+")
+ .replace(/\?/g, "\\?")
+ .replace(/\./g, "\\.")
+ .replace(/\(/g, "\\(")
+ .replace(/\)/g, "\\)")
+ .replace(/\=/g, "\\=")
+ .replace(/\!/g, "\\!")
+ .replace(/\|/g, "\\|")
+ .replace(/\{/g, "\\{")
+ .replace(/\}/g, "\\}")
+ .replace(/\,/g, "\\,")
+ .replace(/\[/g, "\\[")
+ .replace(/\]/g, "\\]")
+ .replace(/\-/g, "\\-")
+ // Turn * into the match everything wildcard.
+ .replace(/\*/g, ".*");
+ return new RegExp("^" + reStr + "$");
+}
diff --git a/devtools/client/debugger/debugger-controller.js b/devtools/client/debugger/debugger-controller.js
new file mode 100644
index 000000000..ce6a467bc
--- /dev/null
+++ b/devtools/client/debugger/debugger-controller.js
@@ -0,0 +1,1276 @@
+/* -*- 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";
+
+var { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+const DBG_STRINGS_URI = "devtools/client/locales/debugger.properties";
+const NEW_SOURCE_IGNORED_URLS = ["debugger eval code", "XStringBundle"];
+const NEW_SOURCE_DISPLAY_DELAY = 200; // ms
+const FETCH_SOURCE_RESPONSE_DELAY = 200; // ms
+const FRAME_STEP_CLEAR_DELAY = 100; // ms
+const CALL_STACK_PAGE_SIZE = 25; // frames
+
+// The panel's window global is an EventEmitter firing the following events:
+const EVENTS = {
+ // When the debugger's source editor instance finishes loading or unloading.
+ EDITOR_LOADED: "Debugger:EditorLoaded",
+ EDITOR_UNLOADED: "Debugger:EditorUnloaded",
+
+ // When new sources are received from the debugger server.
+ NEW_SOURCE: "Debugger:NewSource",
+ SOURCES_ADDED: "Debugger:SourcesAdded",
+
+ // When a source is shown in the source editor.
+ SOURCE_SHOWN: "Debugger:EditorSourceShown",
+ SOURCE_ERROR_SHOWN: "Debugger:EditorSourceErrorShown",
+
+ // When the editor has shown a source and set the line / column position
+ EDITOR_LOCATION_SET: "Debugger:EditorLocationSet",
+
+ // When scopes, variables, properties and watch expressions are fetched and
+ // displayed in the variables view.
+ FETCHED_SCOPES: "Debugger:FetchedScopes",
+ FETCHED_VARIABLES: "Debugger:FetchedVariables",
+ FETCHED_PROPERTIES: "Debugger:FetchedProperties",
+ FETCHED_BUBBLE_PROPERTIES: "Debugger:FetchedBubbleProperties",
+ FETCHED_WATCH_EXPRESSIONS: "Debugger:FetchedWatchExpressions",
+
+ // When a breakpoint has been added or removed on the debugger server.
+ BREAKPOINT_ADDED: "Debugger:BreakpointAdded",
+ BREAKPOINT_REMOVED: "Debugger:BreakpointRemoved",
+ BREAKPOINT_CLICKED: "Debugger:BreakpointClicked",
+
+ // When a breakpoint has been shown or hidden in the source editor
+ // or the pane.
+ BREAKPOINT_SHOWN_IN_EDITOR: "Debugger:BreakpointShownInEditor",
+ BREAKPOINT_SHOWN_IN_PANE: "Debugger:BreakpointShownInPane",
+ BREAKPOINT_HIDDEN_IN_EDITOR: "Debugger:BreakpointHiddenInEditor",
+ BREAKPOINT_HIDDEN_IN_PANE: "Debugger:BreakpointHiddenInPane",
+
+ // When a conditional breakpoint's popup is shown/hidden.
+ CONDITIONAL_BREAKPOINT_POPUP_SHOWN: "Debugger:ConditionalBreakpointPopupShown",
+ CONDITIONAL_BREAKPOINT_POPUP_HIDDEN: "Debugger:ConditionalBreakpointPopupHidden",
+
+ // When event listeners are fetched or event breakpoints are updated.
+ EVENT_LISTENERS_FETCHED: "Debugger:EventListenersFetched",
+ EVENT_BREAKPOINTS_UPDATED: "Debugger:EventBreakpointsUpdated",
+
+ // When a file search was performed.
+ FILE_SEARCH_MATCH_FOUND: "Debugger:FileSearch:MatchFound",
+ FILE_SEARCH_MATCH_NOT_FOUND: "Debugger:FileSearch:MatchNotFound",
+
+ // When a function search was performed.
+ FUNCTION_SEARCH_MATCH_FOUND: "Debugger:FunctionSearch:MatchFound",
+ FUNCTION_SEARCH_MATCH_NOT_FOUND: "Debugger:FunctionSearch:MatchNotFound",
+
+ // When a global text search was performed.
+ GLOBAL_SEARCH_MATCH_FOUND: "Debugger:GlobalSearch:MatchFound",
+ GLOBAL_SEARCH_MATCH_NOT_FOUND: "Debugger:GlobalSearch:MatchNotFound",
+
+ // After the the StackFrames object has been filled with frames
+ AFTER_FRAMES_REFILLED: "Debugger:AfterFramesRefilled",
+
+ // After the stackframes are cleared and debugger won't pause anymore.
+ AFTER_FRAMES_CLEARED: "Debugger:AfterFramesCleared",
+
+ // When the options popup is showing or hiding.
+ OPTIONS_POPUP_SHOWING: "Debugger:OptionsPopupShowing",
+ OPTIONS_POPUP_HIDDEN: "Debugger:OptionsPopupHidden",
+
+ // When the widgets layout has been changed.
+ LAYOUT_CHANGED: "Debugger:LayoutChanged",
+
+ // When a worker has been selected.
+ WORKER_SELECTED: "Debugger::WorkerSelected"
+};
+
+// Descriptions for what a stack frame represents after the debugger pauses.
+const FRAME_TYPE = {
+ NORMAL: 0,
+ CONDITIONAL_BREAKPOINT_EVAL: 1,
+ WATCH_EXPRESSIONS_EVAL: 2,
+ PUBLIC_CLIENT_EVAL: 3
+};
+
+const { BrowserLoader } = Cu.import("resource://devtools/client/shared/browser-loader.js", {});
+const { require } = BrowserLoader({
+ baseURI: "resource://devtools/client/debugger/",
+ window,
+});
+const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineConstant(this, "require", require);
+const { SimpleListWidget } = require("resource://devtools/client/shared/widgets/SimpleListWidget.jsm");
+const { BreadcrumbsWidget } = require("resource://devtools/client/shared/widgets/BreadcrumbsWidget.jsm");
+const { SideMenuWidget } = require("resource://devtools/client/shared/widgets/SideMenuWidget.jsm");
+const { VariablesView } = require("resource://devtools/client/shared/widgets/VariablesView.jsm");
+const { VariablesViewController, StackFrameUtils } = require("resource://devtools/client/shared/widgets/VariablesViewController.jsm");
+const EventEmitter = require("devtools/shared/event-emitter");
+const { gDevTools } = require("devtools/client/framework/devtools");
+const { ViewHelpers, Heritage, WidgetMethods, setNamedTimeout,
+ clearNamedTimeout } = require("devtools/client/shared/widgets/view-helpers");
+
+// React
+const React = require("devtools/client/shared/vendor/react");
+const ReactDOM = require("devtools/client/shared/vendor/react-dom");
+const { Provider } = require("devtools/client/shared/vendor/react-redux");
+
+// Used to create the Redux store
+const createStore = require("devtools/client/shared/redux/create-store")({
+ getTargetClient: () => DebuggerController.client,
+ log: false
+});
+const {
+ makeStateBroadcaster,
+ enhanceStoreWithBroadcaster,
+ combineBroadcastingReducers
+} = require("devtools/client/shared/redux/non-react-subscriber");
+const { bindActionCreators } = require("devtools/client/shared/vendor/redux");
+const reducers = require("./content/reducers/index");
+const { onReducerEvents } = require("./content/utils");
+
+const waitUntilService = require("devtools/client/shared/redux/middleware/wait-service");
+var services = {
+ WAIT_UNTIL: waitUntilService.NAME
+};
+
+var Services = require("Services");
+var {TargetFactory} = require("devtools/client/framework/target");
+var {Toolbox} = require("devtools/client/framework/toolbox");
+var DevToolsUtils = require("devtools/shared/DevToolsUtils");
+var promise = require("devtools/shared/deprecated-sync-thenables");
+var Editor = require("devtools/client/sourceeditor/editor");
+var DebuggerEditor = require("devtools/client/sourceeditor/debugger");
+var Tooltip = require("devtools/client/shared/widgets/tooltip/Tooltip");
+var FastListWidget = require("devtools/client/shared/widgets/FastListWidget");
+var {LocalizationHelper, ELLIPSIS} = require("devtools/shared/l10n");
+var {PrefsHelper} = require("devtools/client/shared/prefs");
+var {Task} = require("devtools/shared/task");
+
+XPCOMUtils.defineConstant(this, "EVENTS", EVENTS);
+
+XPCOMUtils.defineLazyModuleGetter(this, "Parser",
+ "resource://devtools/shared/Parser.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "ShortcutUtils",
+ "resource://gre/modules/ShortcutUtils.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "clipboardHelper",
+ "@mozilla.org/widget/clipboardhelper;1", "nsIClipboardHelper");
+
+Object.defineProperty(this, "NetworkHelper", {
+ get: function () {
+ return require("devtools/shared/webconsole/network-helper");
+ },
+ configurable: true,
+ enumerable: true
+});
+
+/**
+ * Localization convenience methods.
+ */
+var L10N = new LocalizationHelper(DBG_STRINGS_URI);
+
+/**
+ * Object defining the debugger controller components.
+ */
+var DebuggerController = {
+ /**
+ * Initializes the debugger controller.
+ */
+ initialize: function () {
+ dumpn("Initializing the DebuggerController");
+
+ this.startupDebugger = this.startupDebugger.bind(this);
+ this.shutdownDebugger = this.shutdownDebugger.bind(this);
+ this._onNavigate = this._onNavigate.bind(this);
+ this._onWillNavigate = this._onWillNavigate.bind(this);
+ this._onTabDetached = this._onTabDetached.bind(this);
+
+ const broadcaster = makeStateBroadcaster(() => !!this.activeThread);
+ const reducer = combineBroadcastingReducers(
+ reducers,
+ broadcaster.emitChange
+ );
+ // TODO: Bug 1228867, clean this up and probably abstract it out
+ // better.
+ //
+ // We only want to process async event that are appropriate for
+ // this page. The devtools are open across page reloads, so async
+ // requests from the last page might bleed through if reloading
+ // fast enough. We check to make sure the async action is part of
+ // a current request, and ignore it if not.
+ let store = createStore((state, action) => {
+ if (action.seqId &&
+ (action.status === "done" || action.status === "error") &&
+ state && state.asyncRequests.indexOf(action.seqId) === -1) {
+ return state;
+ }
+ return reducer(state, action);
+ });
+ store = enhanceStoreWithBroadcaster(store, broadcaster);
+
+ // This controller right now acts as the store that's globally
+ // available, so just copy the Redux API onto it.
+ Object.keys(store).forEach(name => {
+ this[name] = store[name];
+ });
+ },
+
+ /**
+ * Initializes the view.
+ *
+ * @return object
+ * A promise that is resolved when the debugger finishes startup.
+ */
+ startupDebugger: Task.async(function* () {
+ if (this._startup) {
+ return;
+ }
+
+ yield DebuggerView.initialize(this._target.isWorkerTarget);
+ this._startup = true;
+ }),
+
+ /**
+ * Destroys the view and disconnects the debugger client from the server.
+ *
+ * @return object
+ * A promise that is resolved when the debugger finishes shutdown.
+ */
+ shutdownDebugger: Task.async(function* () {
+ if (this._shutdown) {
+ return;
+ }
+
+ DebuggerView.destroy();
+ this.StackFrames.disconnect();
+ this.ThreadState.disconnect();
+ if (this._target.isTabActor) {
+ this.Workers.disconnect();
+ }
+
+ this.disconnect();
+
+ this._shutdown = true;
+ }),
+
+ /**
+ * Initiates remote debugging based on the current target, wiring event
+ * handlers as necessary.
+ *
+ * @return object
+ * A promise that is resolved when the debugger finishes connecting.
+ */
+ connect: Task.async(function* () {
+ let target = this._target;
+
+ let { client } = target;
+ target.on("close", this._onTabDetached);
+ target.on("navigate", this._onNavigate);
+ target.on("will-navigate", this._onWillNavigate);
+ this.client = client;
+ this.activeThread = this._toolbox.threadClient;
+
+ // Disable asm.js so that we can set breakpoints and other things
+ // on asm.js scripts
+ yield this.reconfigureThread({ observeAsmJS: true });
+ yield this.connectThread();
+
+ // We need to call this to sync the state of the resume
+ // button in the toolbar with the state of the thread.
+ this.ThreadState._update();
+
+ this._hideUnsupportedFeatures();
+ }),
+
+ connectThread: function () {
+ const { newSource, fetchEventListeners } = bindActionCreators(actions, this.dispatch);
+
+ // TODO: bug 806775, update the globals list using aPacket.hostAnnotations
+ // from bug 801084.
+ // this.client.addListener("newGlobal", ...);
+
+ this.activeThread.addListener("newSource", (event, packet) => {
+ newSource(packet.source);
+
+ // Make sure the events listeners are up to date.
+ if (DebuggerView.instrumentsPaneTab == "events-tab") {
+ fetchEventListeners();
+ }
+ });
+
+ if (this._target.isTabActor) {
+ this.Workers.connect();
+ }
+ this.ThreadState.connect();
+ this.StackFrames.connect();
+
+ // Load all of the sources. Note that the server will actually
+ // emit individual `newSource` notifications, which trigger
+ // separate actions, so this won't do anything other than force
+ // the server to traverse sources.
+ this.dispatch(actions.loadSources()).then(() => {
+ // If the engine is already paused, update the UI to represent the
+ // paused state
+ if (this.activeThread) {
+ const pausedPacket = this.activeThread.getLastPausePacket();
+ DebuggerView.Toolbar.toggleResumeButtonState(
+ this.activeThread.state,
+ !!pausedPacket
+ );
+ if (pausedPacket) {
+ this.StackFrames._onPaused("paused", pausedPacket);
+ }
+ }
+ });
+ },
+
+ /**
+ * Disconnects the debugger client and removes event handlers as necessary.
+ */
+ disconnect: function () {
+ // Return early if the client didn't even have a chance to instantiate.
+ if (!this.client) {
+ return;
+ }
+
+ this.client.removeListener("newGlobal");
+ this.activeThread.removeListener("newSource");
+ this.activeThread.removeListener("blackboxchange");
+
+ this._connected = false;
+ this.client = null;
+ this.activeThread = null;
+ },
+
+ _hideUnsupportedFeatures: function () {
+ if (this.client.mainRoot.traits.noPrettyPrinting) {
+ DebuggerView.Sources.hidePrettyPrinting();
+ }
+
+ if (this.client.mainRoot.traits.noBlackBoxing) {
+ DebuggerView.Sources.hideBlackBoxing();
+ }
+ },
+
+ _onWillNavigate: function (opts = {}) {
+ // Reset UI.
+ DebuggerView.handleTabNavigation();
+ if (!opts.noUnload) {
+ this.dispatch(actions.unload());
+ }
+
+ // Discard all the cached parsed sources *before* the target
+ // starts navigating. Sources may be fetched during navigation, in
+ // which case we don't want to hang on to the old source contents.
+ DebuggerController.Parser.clearCache();
+ SourceUtils.clearCache();
+
+ // Prevent performing any actions that were scheduled before
+ // navigation.
+ clearNamedTimeout("new-source");
+ clearNamedTimeout("event-breakpoints-update");
+ clearNamedTimeout("event-listeners-fetch");
+ },
+
+ _onNavigate: function () {
+ this.ThreadState.handleTabNavigation();
+ this.StackFrames.handleTabNavigation();
+ },
+
+ /**
+ * Called when the debugged tab is closed.
+ */
+ _onTabDetached: function () {
+ this.shutdownDebugger();
+ },
+
+ /**
+ * Warn if resuming execution produced a wrongOrder error.
+ */
+ _ensureResumptionOrder: function (aResponse) {
+ if (aResponse.error == "wrongOrder") {
+ DebuggerView.Toolbar.showResumeWarning(aResponse.lastPausedUrl);
+ }
+ },
+
+ /**
+ * Detach and reattach to the thread actor with useSourceMaps true, blow
+ * away old sources and get them again.
+ */
+ reconfigureThread: function (opts) {
+ const deferred = promise.defer();
+ this.activeThread.reconfigure(
+ opts,
+ aResponse => {
+ if (aResponse.error) {
+ deferred.reject(aResponse.error);
+ return;
+ }
+
+ if (("useSourceMaps" in opts) || ("autoBlackBox" in opts)) {
+ // Reset the view and fetch all the sources again.
+ DebuggerView.handleTabNavigation();
+ this.dispatch(actions.unload());
+ this.dispatch(actions.loadSources());
+
+ // Update the stack frame list.
+ if (this.activeThread.paused) {
+ this.activeThread._clearFrames();
+ this.activeThread.fillFrames(CALL_STACK_PAGE_SIZE);
+ }
+ }
+
+ deferred.resolve();
+ }
+ );
+ return deferred.promise;
+ },
+
+ waitForSourcesLoaded: function () {
+ const deferred = promise.defer();
+ this.dispatch({
+ type: services.WAIT_UNTIL,
+ predicate: action => (action.type === constants.LOAD_SOURCES &&
+ action.status === "done"),
+ run: deferred.resolve
+ });
+ return deferred.promise;
+ },
+
+ waitForSourceShown: function (name) {
+ const deferred = promise.defer();
+ window.on(EVENTS.SOURCE_SHOWN, function onShown(_, source) {
+ if (source.url.includes(name)) {
+ window.off(EVENTS.SOURCE_SHOWN, onShown);
+ deferred.resolve();
+ }
+ });
+ return deferred.promise;
+ },
+
+ _startup: false,
+ _shutdown: false,
+ _connected: false,
+ client: null,
+ activeThread: null
+};
+
+function Workers() {
+ this._workerForms = Object.create(null);
+ this._onWorkerListChanged = this._onWorkerListChanged.bind(this);
+ this._onWorkerSelect = this._onWorkerSelect.bind(this);
+}
+
+Workers.prototype = {
+ get _tabClient() {
+ return DebuggerController._target.activeTab;
+ },
+
+ connect: function () {
+ if (!Prefs.workersEnabled) {
+ return;
+ }
+
+ this._updateWorkerList();
+ this._tabClient.addListener("workerListChanged", this._onWorkerListChanged);
+ },
+
+ disconnect: function () {
+ this._tabClient.removeListener("workerListChanged", this._onWorkerListChanged);
+ },
+
+ _updateWorkerList: function () {
+ if (!this._tabClient.listWorkers) {
+ return;
+ }
+
+ this._tabClient.listWorkers((response) => {
+ let workerForms = Object.create(null);
+ for (let worker of response.workers) {
+ workerForms[worker.actor] = worker;
+ }
+
+ for (let workerActor in this._workerForms) {
+ if (!(workerActor in workerForms)) {
+ DebuggerView.Workers.removeWorker(this._workerForms[workerActor]);
+ delete this._workerForms[workerActor];
+ }
+ }
+
+ for (let workerActor in workerForms) {
+ if (!(workerActor in this._workerForms)) {
+ let workerForm = workerForms[workerActor];
+ this._workerForms[workerActor] = workerForm;
+ DebuggerView.Workers.addWorker(workerForm);
+ }
+ }
+ });
+ },
+
+ _onWorkerListChanged: function () {
+ this._updateWorkerList();
+ },
+
+ _onWorkerSelect: function (workerForm) {
+ DebuggerController.client.attachWorker(workerForm.actor, (response, workerClient) => {
+ let toolbox = gDevTools.showToolbox(TargetFactory.forWorker(workerClient),
+ "jsdebugger", Toolbox.HostType.WINDOW);
+ window.emit(EVENTS.WORKER_SELECTED, toolbox);
+ });
+ }
+};
+
+/**
+ * ThreadState keeps the UI up to date with the state of the
+ * thread (paused/attached/etc.).
+ */
+function ThreadState() {
+ this._update = this._update.bind(this);
+ this.interruptedByResumeButton = false;
+}
+
+ThreadState.prototype = {
+ get activeThread() {
+ return DebuggerController.activeThread;
+ },
+
+ /**
+ * Connect to the current thread client.
+ */
+ connect: function () {
+ dumpn("ThreadState is connecting...");
+ this.activeThread.addListener("paused", this._update);
+ this.activeThread.addListener("resumed", this._update);
+ },
+
+ /**
+ * Disconnect from the client.
+ */
+ disconnect: function () {
+ if (!this.activeThread) {
+ return;
+ }
+ dumpn("ThreadState is disconnecting...");
+ this.activeThread.removeListener("paused", this._update);
+ this.activeThread.removeListener("resumed", this._update);
+ },
+
+ /**
+ * Handles any initialization on a tab navigation event issued by the client.
+ */
+ handleTabNavigation: function () {
+ if (!this.activeThread) {
+ return;
+ }
+ dumpn("Handling tab navigation in the ThreadState");
+ this._update();
+ },
+
+ /**
+ * Update the UI after a thread state change.
+ */
+ _update: function (aEvent, aPacket) {
+ if (aEvent == "paused") {
+ if (aPacket.why.type == "interrupted" &&
+ this.interruptedByResumeButton) {
+ // Interrupt requests suppressed by default, but if this is an
+ // explicit interrupt by the pause button we want to emit it.
+ gTarget.emit("thread-paused", aPacket);
+ } else if (aPacket.why.type == "breakpointConditionThrown" && aPacket.why.message) {
+ let where = aPacket.frame.where;
+ let aLocation = {
+ line: where.line,
+ column: where.column,
+ actor: where.source ? where.source.actor : null
+ };
+ DebuggerView.Sources.showBreakpointConditionThrownMessage(
+ aLocation,
+ aPacket.why.message
+ );
+ }
+ }
+
+ this.interruptedByResumeButton = false;
+ DebuggerView.Toolbar.toggleResumeButtonState(
+ this.activeThread.state,
+ aPacket ? aPacket.frame : false
+ );
+ }
+};
+
+/**
+ * Keeps the stack frame list up-to-date, using the thread client's
+ * stack frame cache.
+ */
+function StackFrames() {
+ this._onPaused = this._onPaused.bind(this);
+ this._onResumed = this._onResumed.bind(this);
+ this._onFrames = this._onFrames.bind(this);
+ this._onFramesCleared = this._onFramesCleared.bind(this);
+ this._onBlackBoxChange = this._onBlackBoxChange.bind(this);
+ this._onPrettyPrintChange = this._onPrettyPrintChange.bind(this);
+ this._afterFramesCleared = this._afterFramesCleared.bind(this);
+ this.evaluate = this.evaluate.bind(this);
+}
+
+StackFrames.prototype = {
+ get activeThread() {
+ return DebuggerController.activeThread;
+ },
+
+ currentFrameDepth: -1,
+ _currentFrameDescription: FRAME_TYPE.NORMAL,
+ _syncedWatchExpressions: null,
+ _currentWatchExpressions: null,
+ _currentBreakpointLocation: null,
+ _currentEvaluation: null,
+ _currentException: null,
+ _currentReturnedValue: null,
+
+ /**
+ * Connect to the current thread client.
+ */
+ connect: function () {
+ dumpn("StackFrames is connecting...");
+ this.activeThread.addListener("paused", this._onPaused);
+ this.activeThread.addListener("resumed", this._onResumed);
+ this.activeThread.addListener("framesadded", this._onFrames);
+ this.activeThread.addListener("framescleared", this._onFramesCleared);
+ this.activeThread.addListener("blackboxchange", this._onBlackBoxChange);
+ this.activeThread.addListener("prettyprintchange", this._onPrettyPrintChange);
+ this.handleTabNavigation();
+ },
+
+ /**
+ * Disconnect from the client.
+ */
+ disconnect: function () {
+ if (!this.activeThread) {
+ return;
+ }
+ dumpn("StackFrames is disconnecting...");
+ this.activeThread.removeListener("paused", this._onPaused);
+ this.activeThread.removeListener("resumed", this._onResumed);
+ this.activeThread.removeListener("framesadded", this._onFrames);
+ this.activeThread.removeListener("framescleared", this._onFramesCleared);
+ this.activeThread.removeListener("blackboxchange", this._onBlackBoxChange);
+ this.activeThread.removeListener("prettyprintchange", this._onPrettyPrintChange);
+ clearNamedTimeout("frames-cleared");
+ },
+
+ /**
+ * Handles any initialization on a tab navigation event issued by the client.
+ */
+ handleTabNavigation: function () {
+ dumpn("Handling tab navigation in the StackFrames");
+ // Nothing to do here yet.
+ },
+
+ /**
+ * Handler for the thread client's paused notification.
+ *
+ * @param string aEvent
+ * The name of the notification ("paused" in this case).
+ * @param object aPacket
+ * The response packet.
+ */
+ _onPaused: function (aEvent, aPacket) {
+ switch (aPacket.why.type) {
+ // If paused by a breakpoint, store the breakpoint location.
+ case "breakpoint":
+ this._currentBreakpointLocation = aPacket.frame.where;
+ break;
+ case "breakpointConditionThrown":
+ this._currentBreakpointLocation = aPacket.frame.where;
+ this._conditionThrowMessage = aPacket.why.message;
+ break;
+ // If paused by a client evaluation, store the evaluated value.
+ case "clientEvaluated":
+ this._currentEvaluation = aPacket.why.frameFinished;
+ break;
+ // If paused by an exception, store the exception value.
+ case "exception":
+ this._currentException = aPacket.why.exception;
+ break;
+ // If paused while stepping out of a frame, store the returned value or
+ // thrown exception.
+ case "resumeLimit":
+ if (!aPacket.why.frameFinished) {
+ break;
+ } else if (aPacket.why.frameFinished.throw) {
+ this._currentException = aPacket.why.frameFinished.throw;
+ } else if (aPacket.why.frameFinished.return) {
+ this._currentReturnedValue = aPacket.why.frameFinished.return;
+ }
+ break;
+ // If paused by an explicit interrupt, which are generated by the slow
+ // script dialog and internal events such as setting breakpoints, ignore
+ // the event to avoid UI flicker.
+ case "interrupted":
+ if (!aPacket.why.onNext) {
+ return;
+ }
+ break;
+ }
+
+ this.activeThread.fillFrames(CALL_STACK_PAGE_SIZE);
+ // Focus the editor, but don't steal focus from the split console.
+ if (!DebuggerController._toolbox.isSplitConsoleFocused()) {
+ DebuggerView.editor.focus();
+ }
+ },
+
+ /**
+ * Handler for the thread client's resumed notification.
+ */
+ _onResumed: function () {
+ // Prepare the watch expression evaluation string for the next pause.
+ if (this._currentFrameDescription != FRAME_TYPE.WATCH_EXPRESSIONS_EVAL) {
+ this._currentWatchExpressions = this._syncedWatchExpressions;
+ }
+ },
+
+ /**
+ * Handler for the thread client's framesadded notification.
+ */
+ _onFrames: Task.async(function* () {
+ // Ignore useless notifications.
+ if (!this.activeThread || !this.activeThread.cachedFrames.length) {
+ return;
+ }
+ if (this._currentFrameDescription != FRAME_TYPE.NORMAL &&
+ this._currentFrameDescription != FRAME_TYPE.PUBLIC_CLIENT_EVAL) {
+ return;
+ }
+
+ // TODO: remove all of this deprecated code: Bug 990137.
+ yield this._handleConditionalBreakpoint();
+
+ // TODO: handle all of this server-side: Bug 832470, comment 14.
+ yield this._handleWatchExpressions();
+
+ // Make sure the debugger view panes are visible, then refill the frames.
+ DebuggerView.showInstrumentsPane();
+ this._refillFrames();
+
+ // No additional processing is necessary for this stack frame.
+ if (this._currentFrameDescription != FRAME_TYPE.NORMAL) {
+ this._currentFrameDescription = FRAME_TYPE.NORMAL;
+ }
+ }),
+
+ /**
+ * Fill the StackFrames view with the frames we have in the cache, compressing
+ * frames which have black boxed sources into single frames.
+ */
+ _refillFrames: function () {
+ // Make sure all the previous stackframes are removed before re-adding them.
+ DebuggerView.StackFrames.empty();
+
+ for (let frame of this.activeThread.cachedFrames) {
+ let { depth, source, where: { line, column } } = frame;
+
+ let isBlackBoxed = source ? this.activeThread.source(source).isBlackBoxed : false;
+ DebuggerView.StackFrames.addFrame(frame, line, column, depth, isBlackBoxed);
+ }
+
+ DebuggerView.StackFrames.selectedDepth = Math.max(this.currentFrameDepth, 0);
+ DebuggerView.StackFrames.dirty = this.activeThread.moreFrames;
+
+ DebuggerView.StackFrames.addCopyContextMenu();
+
+ window.emit(EVENTS.AFTER_FRAMES_REFILLED);
+ },
+
+ /**
+ * Handler for the thread client's framescleared notification.
+ */
+ _onFramesCleared: function () {
+ switch (this._currentFrameDescription) {
+ case FRAME_TYPE.NORMAL:
+ this._currentEvaluation = null;
+ this._currentException = null;
+ this._currentReturnedValue = null;
+ break;
+ case FRAME_TYPE.CONDITIONAL_BREAKPOINT_EVAL:
+ this._currentBreakpointLocation = null;
+ break;
+ case FRAME_TYPE.WATCH_EXPRESSIONS_EVAL:
+ this._currentWatchExpressions = null;
+ break;
+ }
+
+ // After each frame step (in, over, out), framescleared is fired, which
+ // forces the UI to be emptied and rebuilt on framesadded. Most of the times
+ // this is not necessary, and will result in a brief redraw flicker.
+ // To avoid it, invalidate the UI only after a short time if necessary.
+ setNamedTimeout("frames-cleared", FRAME_STEP_CLEAR_DELAY, this._afterFramesCleared);
+ },
+
+ /**
+ * Handler for the debugger's blackboxchange notification.
+ */
+ _onBlackBoxChange: function () {
+ if (this.activeThread.state == "paused") {
+ // Hack to avoid selecting the topmost frame after blackboxing a source.
+ this.currentFrameDepth = NaN;
+ this._refillFrames();
+ }
+ },
+
+ /**
+ * Handler for the debugger's prettyprintchange notification.
+ */
+ _onPrettyPrintChange: function () {
+ if (this.activeThread.state != "paused") {
+ return;
+ }
+ // Makes sure the selected source remains selected
+ // after the fillFrames is called.
+ const source = DebuggerView.Sources.selectedValue;
+
+ this.activeThread.fillFrames(CALL_STACK_PAGE_SIZE, () => {
+ DebuggerView.Sources.selectedValue = source;
+ });
+ },
+
+ /**
+ * Called soon after the thread client's framescleared notification.
+ */
+ _afterFramesCleared: function () {
+ // Ignore useless notifications.
+ if (this.activeThread.cachedFrames.length) {
+ return;
+ }
+ DebuggerView.editor.clearDebugLocation();
+ DebuggerView.StackFrames.empty();
+ DebuggerView.Sources.unhighlightBreakpoint();
+ DebuggerView.WatchExpressions.toggleContents(true);
+ DebuggerView.Variables.empty(0);
+
+ window.emit(EVENTS.AFTER_FRAMES_CLEARED);
+ },
+
+ /**
+ * Marks the stack frame at the specified depth as selected and updates the
+ * properties view with the stack frame's data.
+ *
+ * @param number aDepth
+ * The depth of the frame in the stack.
+ */
+ selectFrame: function (aDepth) {
+ // Make sure the frame at the specified depth exists first.
+ let frame = this.activeThread.cachedFrames[this.currentFrameDepth = aDepth];
+ if (!frame) {
+ return;
+ }
+
+ // Check if the frame does not represent the evaluation of debuggee code.
+ let { environment, where, source } = frame;
+ if (!environment) {
+ return;
+ }
+
+ // Don't change the editor's location if the execution was paused by a
+ // public client evaluation. This is useful for adding overlays on
+ // top of the editor, like a variable inspection popup.
+ let isClientEval = this._currentFrameDescription == FRAME_TYPE.PUBLIC_CLIENT_EVAL;
+ let isPopupShown = DebuggerView.VariableBubble.contentsShown();
+ if (!isClientEval && !isPopupShown) {
+ // Move the editor's caret to the proper url and line.
+ DebuggerView.setEditorLocation(source.actor, where.line);
+ } else {
+ // Highlight the line where the execution is paused in the editor.
+ DebuggerView.setEditorLocation(source.actor, where.line, { noCaret: true });
+ }
+
+ // Highlight the breakpoint at the line and column if it exists.
+ DebuggerView.Sources.highlightBreakpointAtCursor();
+
+ // Don't display the watch expressions textbox inputs in the pane.
+ DebuggerView.WatchExpressions.toggleContents(false);
+
+ // Start recording any added variables or properties in any scope and
+ // clear existing scopes to create each one dynamically.
+ DebuggerView.Variables.empty();
+
+ // If watch expressions evaluation results are available, create a scope
+ // to contain all the values.
+ if (this._syncedWatchExpressions && aDepth == 0) {
+ let label = L10N.getStr("watchExpressionsScopeLabel");
+ let scope = DebuggerView.Variables.addScope(label,
+ "variables-view-watch-expressions");
+
+ // Customize the scope for holding watch expressions evaluations.
+ scope.descriptorTooltip = false;
+ scope.contextMenuId = "debuggerWatchExpressionsContextMenu";
+ scope.separatorStr = L10N.getStr("watchExpressionsSeparatorLabel2");
+ scope.switch = DebuggerView.WatchExpressions.switchExpression;
+ scope.delete = DebuggerView.WatchExpressions.deleteExpression;
+
+ // The evaluation hasn't thrown, so fetch and add the returned results.
+ this._fetchWatchExpressions(scope, this._currentEvaluation.return);
+
+ // The watch expressions scope is always automatically expanded.
+ scope.expand();
+ }
+
+ do {
+ // Create a scope to contain all the inspected variables in the
+ // current environment.
+ let label = StackFrameUtils.getScopeLabel(environment);
+ let scope = DebuggerView.Variables.addScope(label);
+ let innermost = environment == frame.environment;
+
+ // Handle special additions to the innermost scope.
+ if (innermost) {
+ this._insertScopeFrameReferences(scope, frame);
+ }
+
+ // Handle the expansion of the scope, lazily populating it with the
+ // variables in the current environment.
+ DebuggerView.Variables.controller.addExpander(scope, environment);
+
+ // The innermost scope is always automatically expanded, because it
+ // contains the variables in the current stack frame which are likely to
+ // be inspected. The previously expanded scopes are also reexpanded here.
+ if (innermost || DebuggerView.Variables.wasExpanded(scope)) {
+ scope.expand();
+ }
+ } while ((environment = environment.parent));
+
+ // Signal that scope environments have been shown.
+ window.emit(EVENTS.FETCHED_SCOPES);
+ },
+
+ /**
+ * Loads more stack frames from the debugger server cache.
+ */
+ addMoreFrames: function () {
+ this.activeThread.fillFrames(
+ this.activeThread.cachedFrames.length + CALL_STACK_PAGE_SIZE);
+ },
+
+ /**
+ * Evaluate an expression in the context of the selected frame.
+ *
+ * @param string aExpression
+ * The expression to evaluate.
+ * @param object aOptions [optional]
+ * Additional options for this client evaluation:
+ * - depth: the frame depth used for evaluation, 0 being the topmost.
+ * - meta: some meta-description for what this evaluation represents.
+ * @return object
+ * A promise that is resolved when the evaluation finishes,
+ * or rejected if there was no stack frame available or some
+ * other error occurred.
+ */
+ evaluate: function (aExpression, aOptions = {}) {
+ let depth = "depth" in aOptions ? aOptions.depth : this.currentFrameDepth;
+ let frame = this.activeThread.cachedFrames[depth];
+ if (frame == null) {
+ return promise.reject(new Error("No stack frame available."));
+ }
+
+ let deferred = promise.defer();
+
+ this.activeThread.addOneTimeListener("paused", (aEvent, aPacket) => {
+ let { type, frameFinished } = aPacket.why;
+ if (type == "clientEvaluated") {
+ deferred.resolve(frameFinished);
+ } else {
+ deferred.reject(new Error("Active thread paused unexpectedly."));
+ }
+ });
+
+ let meta = "meta" in aOptions ? aOptions.meta : FRAME_TYPE.PUBLIC_CLIENT_EVAL;
+ this._currentFrameDescription = meta;
+ this.activeThread.eval(frame.actor, aExpression);
+
+ return deferred.promise;
+ },
+
+ /**
+ * Add nodes for special frame references in the innermost scope.
+ *
+ * @param Scope aScope
+ * The scope where the references will be placed into.
+ * @param object aFrame
+ * The frame to get some references from.
+ */
+ _insertScopeFrameReferences: function (aScope, aFrame) {
+ // Add any thrown exception.
+ if (this._currentException) {
+ let excRef = aScope.addItem("<exception>", { value: this._currentException },
+ { internalItem: true });
+ DebuggerView.Variables.controller.addExpander(excRef, this._currentException);
+ }
+ // Add any returned value.
+ if (this._currentReturnedValue) {
+ let retRef = aScope.addItem("<return>",
+ { value: this._currentReturnedValue },
+ { internalItem: true });
+ DebuggerView.Variables.controller.addExpander(retRef, this._currentReturnedValue);
+ }
+ // Add "this".
+ if (aFrame.this) {
+ let thisRef = aScope.addItem("this", { value: aFrame.this });
+ DebuggerView.Variables.controller.addExpander(thisRef, aFrame.this);
+ }
+ },
+
+ /**
+ * Handles conditional breakpoints when the debugger pauses and the
+ * stackframes are received.
+ *
+ * We moved conditional breakpoint handling to the server, but
+ * need to support it in the client for a while until most of the
+ * server code in production is updated with it.
+ * TODO: remove all of this deprecated code: Bug 990137.
+ *
+ * @return object
+ * A promise that is resolved after a potential breakpoint's
+ * conditional expression is evaluated. If there's no breakpoint
+ * where the debugger is paused, the promise is resolved immediately.
+ */
+ _handleConditionalBreakpoint: Task.async(function* () {
+ if (gClient.mainRoot.traits.conditionalBreakpoints) {
+ return;
+ }
+ let breakLocation = this._currentBreakpointLocation;
+ if (!breakLocation) {
+ return;
+ }
+
+ let bp = queries.getBreakpoint(DebuggerController.getState(), {
+ actor: breakLocation.source.actor,
+ line: breakLocation.line
+ });
+ let conditionalExpression = bp.condition;
+ if (!conditionalExpression) {
+ return;
+ }
+
+ // Evaluating the current breakpoint's conditional expression will
+ // cause the stack frames to be cleared and active thread to pause,
+ // sending a 'clientEvaluated' packed and adding the frames again.
+ let evaluationOptions = { depth: 0, meta: FRAME_TYPE.CONDITIONAL_BREAKPOINT_EVAL };
+ yield this.evaluate(conditionalExpression, evaluationOptions);
+ this._currentFrameDescription = FRAME_TYPE.NORMAL;
+
+ // If the breakpoint's conditional expression evaluation is falsy
+ // and there is no exception, automatically resume execution.
+ if (!this._currentEvaluation.throw &&
+ VariablesView.isFalsy({ value: this._currentEvaluation.return })) {
+ this.activeThread.resume(DebuggerController._ensureResumptionOrder);
+ }
+ }),
+
+ /**
+ * Handles watch expressions when the debugger pauses and the stackframes
+ * are received.
+ *
+ * @return object
+ * A promise that is resolved after the potential watch expressions
+ * are evaluated. If there are no watch expressions where the debugger
+ * is paused, the promise is resolved immediately.
+ */
+ _handleWatchExpressions: Task.async(function* () {
+ // Ignore useless notifications.
+ if (!this.activeThread || !this.activeThread.cachedFrames.length) {
+ return;
+ }
+
+ let watchExpressions = this._currentWatchExpressions;
+ if (!watchExpressions) {
+ return;
+ }
+
+ // Evaluation causes the stack frames to be cleared and active thread to
+ // pause, sending a 'clientEvaluated' packet and adding the frames again.
+ let evaluationOptions = { depth: 0, meta: FRAME_TYPE.WATCH_EXPRESSIONS_EVAL };
+ yield this.evaluate(watchExpressions, evaluationOptions);
+ this._currentFrameDescription = FRAME_TYPE.NORMAL;
+
+ // If an error was thrown during the evaluation of the watch expressions
+ // or the evaluation was terminated from the slow script dialog, then at
+ // least one expression evaluation could not be performed. So remove the
+ // most recent watch expression and try again.
+ if (this._currentEvaluation.throw || this._currentEvaluation.terminated) {
+ DebuggerView.WatchExpressions.removeAt(0);
+ yield DebuggerController.StackFrames.syncWatchExpressions();
+ }
+ }),
+
+ /**
+ * Adds the watch expressions evaluation results to a scope in the view.
+ *
+ * @param Scope aScope
+ * The scope where the watch expressions will be placed into.
+ * @param object aExp
+ * The grip of the evaluation results.
+ */
+ _fetchWatchExpressions: function (aScope, aExp) {
+ // Fetch the expressions only once.
+ if (aScope._fetched) {
+ return;
+ }
+ aScope._fetched = true;
+
+ // Add nodes for every watch expression in scope.
+ this.activeThread.pauseGrip(aExp).getPrototypeAndProperties(aResponse => {
+ let ownProperties = aResponse.ownProperties;
+ let totalExpressions = DebuggerView.WatchExpressions.itemCount;
+
+ for (let i = 0; i < totalExpressions; i++) {
+ let name = DebuggerView.WatchExpressions.getString(i);
+ let expVal = ownProperties[i].value;
+ let expRef = aScope.addItem(name, ownProperties[i]);
+ DebuggerView.Variables.controller.addExpander(expRef, expVal);
+
+ // Revert some of the custom watch expressions scope presentation flags,
+ // so that they don't propagate to child items.
+ expRef.switch = null;
+ expRef.delete = null;
+ expRef.descriptorTooltip = true;
+ expRef.separatorStr = L10N.getStr("variablesSeparatorLabel");
+ }
+
+ // Signal that watch expressions have been fetched.
+ window.emit(EVENTS.FETCHED_WATCH_EXPRESSIONS);
+ });
+ },
+
+ /**
+ * Updates a list of watch expressions to evaluate on each pause.
+ * TODO: handle all of this server-side: Bug 832470, comment 14.
+ */
+ syncWatchExpressions: function () {
+ let list = DebuggerView.WatchExpressions.getAllStrings();
+
+ // Sanity check all watch expressions before syncing them. To avoid
+ // having the whole watch expressions array throw because of a single
+ // faulty expression, simply convert it to a string describing the error.
+ // There's no other information necessary to be offered in such cases.
+ let sanitizedExpressions = list.map(aString => {
+ // Reflect.parse throws when it encounters a syntax error.
+ try {
+ Parser.reflectionAPI.parse(aString);
+ return aString; // Watch expression can be executed safely.
+ } catch (e) {
+ return "\"" + e.name + ": " + e.message + "\""; // Syntax error.
+ }
+ });
+
+ if (!sanitizedExpressions.length) {
+ this._currentWatchExpressions = null;
+ this._syncedWatchExpressions = null;
+ } else {
+ this._syncedWatchExpressions =
+ this._currentWatchExpressions = "[" +
+ sanitizedExpressions.map(aString =>
+ "eval(\"" +
+ "try {" +
+ // Make sure all quotes are escaped in the expression's syntax,
+ // and add a newline after the statement to avoid comments
+ // breaking the code integrity inside the eval block.
+ aString.replace(/\\/g, "\\\\").replace(/"/g, "\\$&") + "\" + " + "'\\n'" + " + \"" +
+ "} catch (e) {" +
+ "e.name + ': ' + e.message;" + // TODO: Bug 812765, 812764.
+ "}" +
+ "\")"
+ ).join(",") +
+ "]";
+ }
+
+ this.currentFrameDepth = -1;
+ return this._onFrames();
+ }
+};
+
+/**
+ * Shortcuts for accessing various debugger preferences.
+ */
+var Prefs = new PrefsHelper("devtools", {
+ workersAndSourcesWidth: ["Int", "debugger.ui.panes-workers-and-sources-width"],
+ instrumentsWidth: ["Int", "debugger.ui.panes-instruments-width"],
+ panesVisibleOnStartup: ["Bool", "debugger.ui.panes-visible-on-startup"],
+ variablesSortingEnabled: ["Bool", "debugger.ui.variables-sorting-enabled"],
+ variablesOnlyEnumVisible: ["Bool", "debugger.ui.variables-only-enum-visible"],
+ variablesSearchboxVisible: ["Bool", "debugger.ui.variables-searchbox-visible"],
+ pauseOnExceptions: ["Bool", "debugger.pause-on-exceptions"],
+ ignoreCaughtExceptions: ["Bool", "debugger.ignore-caught-exceptions"],
+ sourceMapsEnabled: ["Bool", "debugger.source-maps-enabled"],
+ prettyPrintEnabled: ["Bool", "debugger.pretty-print-enabled"],
+ autoPrettyPrint: ["Bool", "debugger.auto-pretty-print"],
+ workersEnabled: ["Bool", "debugger.workers"],
+ editorTabSize: ["Int", "editor.tabsize"],
+ autoBlackBox: ["Bool", "debugger.auto-black-box"],
+});
+
+/**
+ * Convenient way of emitting events from the panel window.
+ */
+EventEmitter.decorate(this);
+
+/**
+ * Preliminary setup for the DebuggerController object.
+ */
+DebuggerController.initialize();
+DebuggerController.Parser = new Parser();
+DebuggerController.Workers = new Workers();
+DebuggerController.ThreadState = new ThreadState();
+DebuggerController.StackFrames = new StackFrames();
+
+/**
+ * Export some properties to the global scope for easier access.
+ */
+Object.defineProperties(window, {
+ "gTarget": {
+ get: function () {
+ return DebuggerController._target;
+ },
+ configurable: true
+ },
+ "gHostType": {
+ get: function () {
+ return DebuggerView._hostType;
+ },
+ configurable: true
+ },
+ "gClient": {
+ get: function () {
+ return DebuggerController.client;
+ },
+ configurable: true
+ },
+ "gThreadClient": {
+ get: function () {
+ return DebuggerController.activeThread;
+ },
+ configurable: true
+ },
+ "gCallStackPageSize": {
+ get: function () {
+ return CALL_STACK_PAGE_SIZE;
+ },
+ configurable: true
+ }
+});
+
+/**
+ * Helper method for debugging.
+ * @param string
+ */
+function dumpn(str) {
+ if (wantLogging) {
+ dump("DBG-FRONTEND: " + str + "\n");
+ }
+}
+
+var wantLogging = Services.prefs.getBoolPref("devtools.debugger.log");
diff --git a/devtools/client/debugger/debugger-view.js b/devtools/client/debugger/debugger-view.js
new file mode 100644
index 000000000..b6a5850ff
--- /dev/null
+++ b/devtools/client/debugger/debugger-view.js
@@ -0,0 +1,982 @@
+/* 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 SOURCE_URL_DEFAULT_MAX_LENGTH = 64; // chars
+const STACK_FRAMES_SOURCE_URL_MAX_LENGTH = 15; // chars
+const STACK_FRAMES_SOURCE_URL_TRIM_SECTION = "center";
+const STACK_FRAMES_SCROLL_DELAY = 100; // ms
+const BREAKPOINT_SMALL_WINDOW_WIDTH = 850; // px
+const RESULTS_PANEL_POPUP_POSITION = "before_end";
+const RESULTS_PANEL_MAX_RESULTS = 10;
+const FILE_SEARCH_ACTION_MAX_DELAY = 300; // ms
+const GLOBAL_SEARCH_EXPAND_MAX_RESULTS = 50;
+const GLOBAL_SEARCH_LINE_MAX_LENGTH = 300; // chars
+const GLOBAL_SEARCH_ACTION_MAX_DELAY = 1500; // ms
+const FUNCTION_SEARCH_ACTION_MAX_DELAY = 400; // ms
+const SEARCH_GLOBAL_FLAG = "!";
+const SEARCH_FUNCTION_FLAG = "@";
+const SEARCH_TOKEN_FLAG = "#";
+const SEARCH_LINE_FLAG = ":";
+const SEARCH_VARIABLE_FLAG = "*";
+const SEARCH_AUTOFILL = [SEARCH_GLOBAL_FLAG, SEARCH_FUNCTION_FLAG, SEARCH_TOKEN_FLAG];
+const TOOLBAR_ORDER_POPUP_POSITION = "topcenter bottomleft";
+const RESIZE_REFRESH_RATE = 50; // ms
+
+const EventListenersView = require("./content/views/event-listeners-view");
+const SourcesView = require("./content/views/sources-view");
+var actions = Object.assign(
+ {},
+ require("./content/globalActions"),
+ require("./content/actions/breakpoints"),
+ require("./content/actions/sources"),
+ require("./content/actions/event-listeners")
+);
+var queries = require("./content/queries");
+var constants = require("./content/constants");
+
+/**
+ * Object defining the debugger view components.
+ */
+var DebuggerView = {
+
+ /**
+ * This is attached so tests can change it without needing to load an
+ * actual large file in automation
+ */
+ LARGE_FILE_SIZE: 1048576, // 1 MB in bytes
+
+ /**
+ * Initializes the debugger view.
+ *
+ * @return object
+ * A promise that is resolved when the view finishes initializing.
+ */
+ initialize: function (isWorker) {
+ if (this._startup) {
+ return this._startup;
+ }
+ const deferred = promise.defer();
+ this._startup = deferred.promise;
+
+ this._initializePanes();
+ this._initializeEditor(deferred.resolve);
+ this.Toolbar.initialize();
+ this.Options.initialize();
+ this.Filtering.initialize();
+ this.StackFrames.initialize();
+ this.StackFramesClassicList.initialize();
+ this.Workers.initialize();
+ this.Sources.initialize(isWorker);
+ this.VariableBubble.initialize();
+ this.WatchExpressions.initialize();
+ this.EventListeners.initialize();
+ this.GlobalSearch.initialize();
+ this._initializeVariablesView();
+
+ this._editorSource = {};
+ this._editorDocuments = {};
+
+ this.editor.on("cursorActivity", this.Sources._onEditorCursorActivity);
+
+ this.controller = DebuggerController;
+ const getState = this.controller.getState;
+
+ onReducerEvents(this.controller, {
+ "source-text-loaded": this.renderSourceText,
+ "source-selected": this.renderSourceText,
+ "blackboxed": this.renderBlackBoxed,
+ "prettyprinted": this.renderPrettyPrinted,
+ "breakpoint-added": this.addEditorBreakpoint,
+ "breakpoint-enabled": this.addEditorBreakpoint,
+ "breakpoint-disabled": this.removeEditorBreakpoint,
+ "breakpoint-removed": this.removeEditorBreakpoint,
+ "breakpoint-condition-updated": this.renderEditorBreakpointCondition,
+ "breakpoint-moved": ({ breakpoint, prevLocation }) => {
+ const selectedSource = queries.getSelectedSource(getState());
+ const { location } = breakpoint;
+
+ if (selectedSource &&
+ selectedSource.actor === location.actor) {
+ this.editor.moveBreakpoint(prevLocation.line - 1,
+ location.line - 1);
+ }
+ }
+ }, this);
+
+ return deferred.promise;
+ },
+
+ /**
+ * Destroys the debugger view.
+ *
+ * @return object
+ * A promise that is resolved when the view finishes destroying.
+ */
+ destroy: function () {
+ if (this._hasShutdown) {
+ return;
+ }
+ this._hasShutdown = true;
+
+ window.removeEventListener("resize", this._onResize, false);
+ this.editor.off("cursorActivity", this.Sources._onEditorCursorActivity);
+
+ this.Toolbar.destroy();
+ this.Options.destroy();
+ this.Filtering.destroy();
+ this.StackFrames.destroy();
+ this.StackFramesClassicList.destroy();
+ this.Sources.destroy();
+ this.VariableBubble.destroy();
+ this.WatchExpressions.destroy();
+ this.EventListeners.destroy();
+ this.GlobalSearch.destroy();
+ this._destroyPanes();
+
+ this.editor.destroy();
+ this.editor = null;
+
+ this.controller.dispatch(actions.removeAllBreakpoints());
+ },
+
+ /**
+ * Initializes the UI for all the displayed panes.
+ */
+ _initializePanes: function () {
+ dumpn("Initializing the DebuggerView panes");
+
+ this._body = document.getElementById("body");
+ this._editorDeck = document.getElementById("editor-deck");
+ this._workersAndSourcesPane = document.getElementById("workers-and-sources-pane");
+ this._instrumentsPane = document.getElementById("instruments-pane");
+ this._instrumentsPaneToggleButton = document.getElementById("instruments-pane-toggle");
+
+ this.showEditor = this.showEditor.bind(this);
+ this.showBlackBoxMessage = this.showBlackBoxMessage.bind(this);
+ this.showProgressBar = this.showProgressBar.bind(this);
+
+ this._onTabSelect = this._onInstrumentsPaneTabSelect.bind(this);
+ this._instrumentsPane.tabpanels.addEventListener("select", this._onTabSelect);
+
+ this._collapsePaneString = L10N.getStr("collapsePanes");
+ this._expandPaneString = L10N.getStr("expandPanes");
+
+ this._workersAndSourcesPane.setAttribute("width", Prefs.workersAndSourcesWidth);
+ this._instrumentsPane.setAttribute("width", Prefs.instrumentsWidth);
+ this.toggleInstrumentsPane({ visible: Prefs.panesVisibleOnStartup });
+
+ this.updateLayoutMode();
+
+ this._onResize = this._onResize.bind(this);
+ window.addEventListener("resize", this._onResize, false);
+ },
+
+ /**
+ * Destroys the UI for all the displayed panes.
+ */
+ _destroyPanes: function () {
+ dumpn("Destroying the DebuggerView panes");
+
+ if (gHostType != "side") {
+ Prefs.workersAndSourcesWidth = this._workersAndSourcesPane.getAttribute("width");
+ Prefs.instrumentsWidth = this._instrumentsPane.getAttribute("width");
+ }
+
+ this._workersAndSourcesPane = null;
+ this._instrumentsPane = null;
+ this._instrumentsPaneToggleButton = null;
+ },
+
+ /**
+ * Initializes the VariablesView instance and attaches a controller.
+ */
+ _initializeVariablesView: function () {
+ this.Variables = new VariablesView(document.getElementById("variables"), {
+ searchPlaceholder: L10N.getStr("emptyVariablesFilterText"),
+ emptyText: L10N.getStr("emptyVariablesText"),
+ onlyEnumVisible: Prefs.variablesOnlyEnumVisible,
+ searchEnabled: Prefs.variablesSearchboxVisible,
+ eval: (variable, value) => {
+ let string = variable.evaluationMacro(variable, value);
+ DebuggerController.StackFrames.evaluate(string);
+ },
+ lazyEmpty: true
+ });
+
+ // Attach the current toolbox to the VView so it can link DOMNodes to
+ // the inspector/highlighter
+ this.Variables.toolbox = DebuggerController._toolbox;
+
+ // Attach a controller that handles interfacing with the debugger protocol.
+ VariablesViewController.attach(this.Variables, {
+ getEnvironmentClient: aObject => gThreadClient.environment(aObject),
+ getObjectClient: aObject => {
+ return gThreadClient.pauseGrip(aObject);
+ }
+ });
+
+ // Relay events from the VariablesView.
+ this.Variables.on("fetched", (aEvent, aType) => {
+ switch (aType) {
+ case "scopes":
+ window.emit(EVENTS.FETCHED_SCOPES);
+ break;
+ case "variables":
+ window.emit(EVENTS.FETCHED_VARIABLES);
+ break;
+ case "properties":
+ window.emit(EVENTS.FETCHED_PROPERTIES);
+ break;
+ }
+ });
+ },
+
+ /**
+ * Initializes the Editor instance.
+ *
+ * @param function aCallback
+ * Called after the editor finishes initializing.
+ */
+ _initializeEditor: function (callback) {
+ dumpn("Initializing the DebuggerView editor");
+
+ let extraKeys = {};
+ bindKey("_doTokenSearch", "tokenSearchKey");
+ bindKey("_doGlobalSearch", "globalSearchKey", { alt: true });
+ bindKey("_doFunctionSearch", "functionSearchKey");
+ extraKeys[Editor.keyFor("jumpToLine")] = false;
+ extraKeys["Esc"] = false;
+
+ function bindKey(func, key, modifiers = {}) {
+ key = document.getElementById(key).getAttribute("key");
+ let shortcut = Editor.accel(key, modifiers);
+ extraKeys[shortcut] = () => DebuggerView.Filtering[func]();
+ }
+
+ let gutters = ["breakpoints"];
+
+ this.editor = new Editor({
+ mode: Editor.modes.text,
+ readOnly: true,
+ lineNumbers: true,
+ showAnnotationRuler: true,
+ gutters: gutters,
+ extraKeys: extraKeys,
+ contextMenu: "sourceEditorContextMenu",
+ enableCodeFolding: false
+ });
+
+ this.editor.appendTo(document.getElementById("editor")).then(() => {
+ this.editor.extend(DebuggerEditor);
+ this._loadingText = L10N.getStr("loadingText");
+ callback();
+ });
+
+ this.editor.on("gutterClick", (ev, line, button) => {
+ // A right-click shouldn't do anything but keep track of where
+ // it was clicked.
+ if (button == 2) {
+ this.clickedLine = line;
+ }
+ else {
+ const source = queries.getSelectedSource(this.controller.getState());
+ if (source) {
+ const location = { actor: source.actor, line: line + 1 };
+ if (this.editor.hasBreakpoint(line)) {
+ this.controller.dispatch(actions.removeBreakpoint(location));
+ } else {
+ this.controller.dispatch(actions.addBreakpoint(location));
+ }
+ }
+ }
+ });
+
+ this.editor.on("cursorActivity", () => {
+ this.clickedLine = null;
+ });
+ },
+
+ updateEditorBreakpoints: function (source) {
+ const breakpoints = queries.getBreakpoints(this.controller.getState());
+ const sources = queries.getSources(this.controller.getState());
+
+ for (let bp of breakpoints) {
+ if (sources[bp.location.actor] && !bp.disabled) {
+ this.addEditorBreakpoint(bp);
+ }
+ else {
+ this.removeEditorBreakpoint(bp);
+ }
+ }
+ },
+
+ addEditorBreakpoint: function (breakpoint) {
+ const { location, condition } = breakpoint;
+ const source = queries.getSelectedSource(this.controller.getState());
+
+ if (source &&
+ source.actor === location.actor &&
+ !breakpoint.disabled) {
+ this.editor.addBreakpoint(location.line - 1, condition);
+ }
+ },
+
+ removeEditorBreakpoint: function (breakpoint) {
+ const { location } = breakpoint;
+ const source = queries.getSelectedSource(this.controller.getState());
+
+ if (source && source.actor === location.actor) {
+ this.editor.removeBreakpoint(location.line - 1);
+ this.editor.removeBreakpointCondition(location.line - 1);
+ }
+ },
+
+ renderEditorBreakpointCondition: function (breakpoint) {
+ const { location, condition, disabled } = breakpoint;
+ const source = queries.getSelectedSource(this.controller.getState());
+
+ if (source && source.actor === location.actor && !disabled) {
+ if (condition) {
+ this.editor.setBreakpointCondition(location.line - 1);
+ } else {
+ this.editor.removeBreakpointCondition(location.line - 1);
+ }
+ }
+ },
+
+ /**
+ * Display the source editor.
+ */
+ showEditor: function () {
+ this._editorDeck.selectedIndex = 0;
+ },
+
+ /**
+ * Display the black box message.
+ */
+ showBlackBoxMessage: function () {
+ this._editorDeck.selectedIndex = 1;
+ },
+
+ /**
+ * Display the progress bar.
+ */
+ showProgressBar: function () {
+ this._editorDeck.selectedIndex = 2;
+ },
+
+ /**
+ * Sets the currently displayed text contents in the source editor.
+ * This resets the mode and undo stack.
+ *
+ * @param string documentKey
+ * Key to get the correct editor document
+ *
+ * @param string aTextContent
+ * The source text content.
+ *
+ * @param boolean shouldUpdateText
+ Forces a text and mode reset
+ */
+ _setEditorText: function (documentKey, aTextContent = "", shouldUpdateText = false) {
+ const isNew = this._setEditorDocument(documentKey);
+
+ this.editor.clearDebugLocation();
+ this.editor.clearHistory();
+ this.editor.removeBreakpoints();
+
+ // Only set editor's text and mode if it is a new document
+ if (isNew || shouldUpdateText) {
+ this.editor.setMode(Editor.modes.text);
+ this.editor.setText(aTextContent);
+ }
+ },
+
+ /**
+ * Sets the proper editor mode (JS or HTML) according to the specified
+ * content type, or by determining the type from the url or text content.
+ *
+ * @param string aUrl
+ * The source url.
+ * @param string aContentType [optional]
+ * The source content type.
+ * @param string aTextContent [optional]
+ * The source text content.
+ */
+ _setEditorMode: function (aUrl, aContentType = "", aTextContent = "") {
+ // Use JS mode for files with .js and .jsm extensions.
+ if (SourceUtils.isJavaScript(aUrl, aContentType)) {
+ return void this.editor.setMode(Editor.modes.js);
+ }
+
+ if (aContentType === "text/wasm") {
+ return void this.editor.setMode(Editor.modes.text);
+ }
+
+ // Use HTML mode for files in which the first non whitespace character is
+ // &lt;, regardless of extension.
+ if (aTextContent.match(/^\s*</)) {
+ return void this.editor.setMode(Editor.modes.html);
+ }
+
+ // Unknown language, use text.
+ this.editor.setMode(Editor.modes.text);
+ },
+
+ /**
+ * Sets the editor's displayed document.
+ * If there isn't a document for the source, create one
+ *
+ * @param string key - key used to access the editor document cache
+ *
+ * @return boolean isNew - was the document just created
+ */
+ _setEditorDocument: function (key) {
+ let isNew;
+
+ if (!this._editorDocuments[key]) {
+ isNew = true;
+ this._editorDocuments[key] = this.editor.createDocument();
+ } else {
+ isNew = false;
+ }
+
+ const doc = this._editorDocuments[key];
+ this.editor.replaceDocument(doc);
+ return isNew;
+ },
+
+ renderBlackBoxed: function (source) {
+ this._renderSourceText(
+ source,
+ queries.getSourceText(this.controller.getState(), source.actor)
+ );
+ },
+
+ renderPrettyPrinted: function (source) {
+ this._renderSourceText(
+ source,
+ queries.getSourceText(this.controller.getState(), source.actor)
+ );
+ },
+
+ renderSourceText: function (source) {
+ this._renderSourceText(
+ source,
+ queries.getSourceText(this.controller.getState(), source.actor),
+ queries.getSelectedSourceOpts(this.controller.getState())
+ );
+ },
+
+ _renderSourceText: function (source, textInfo, opts = {}) {
+ const selectedSource = queries.getSelectedSource(this.controller.getState());
+
+ // Exit early if we're attempting to render an unselected source
+ if (!selectedSource || selectedSource.actor !== source.actor) {
+ return;
+ }
+
+ if (source.isBlackBoxed) {
+ this.showBlackBoxMessage();
+ setTimeout(() => {
+ window.emit(EVENTS.SOURCE_SHOWN, source);
+ }, 0);
+ return;
+ }
+ else {
+ this.showEditor();
+ }
+
+ if (textInfo.loading) {
+ // TODO: bug 1228866, we need to update `_editorSource` here but
+ // still make the editor be updated when the full text comes
+ // through somehow.
+ this._setEditorText("loading", L10N.getStr("loadingText"));
+ return;
+ }
+ else if (textInfo.error) {
+ let msg = L10N.getFormatStr("errorLoadingText2", textInfo.error);
+ this._setEditorText("error", msg);
+ console.error(new Error(msg));
+ dumpn(msg);
+
+ this.showEditor();
+ window.emit(EVENTS.SOURCE_ERROR_SHOWN, source);
+ return;
+ }
+
+ // If the line is not specified, default to the current frame's position,
+ // if available and the frame's url corresponds to the requested url.
+ if (!("line" in opts)) {
+ let cachedFrames = DebuggerController.activeThread.cachedFrames;
+ let currentDepth = DebuggerController.StackFrames.currentFrameDepth;
+ let frame = cachedFrames[currentDepth];
+ if (frame && frame.source.actor == source.actor) {
+ opts.line = frame.where.line;
+ }
+ }
+
+ if (this._editorSource.actor === source.actor &&
+ this._editorSource.prettyPrinted === source.isPrettyPrinted &&
+ this._editorSource.blackboxed === source.isBlackBoxed) {
+ this.updateEditorPosition(opts);
+ return;
+ }
+
+ let { text, contentType } = textInfo;
+ let shouldUpdateText = this._editorSource.prettyPrinted != source.isPrettyPrinted;
+ this._setEditorText(source.actor, text, shouldUpdateText);
+
+ this._editorSource.actor = source.actor;
+ this._editorSource.prettyPrinted = source.isPrettyPrinted;
+ this._editorSource.blackboxed = source.isBlackBoxed;
+ this._editorSource.prettyPrinted = source.isPrettyPrinted;
+
+ this._setEditorMode(source.url, contentType, text);
+ this.updateEditorBreakpoints(source);
+
+ setTimeout(() => {
+ window.emit(EVENTS.SOURCE_SHOWN, source);
+ }, 0);
+
+ this.updateEditorPosition(opts);
+ },
+
+ updateEditorPosition: function (opts) {
+ let line = opts.line || 0;
+
+ // Line numbers in the source editor should start from 1. If
+ // invalid or not specified, then don't do anything.
+ if (line < 1) {
+ window.emit(EVENTS.EDITOR_LOCATION_SET);
+ return;
+ }
+
+ if (opts.charOffset) {
+ line += this.editor.getPosition(opts.charOffset).line;
+ }
+ if (opts.lineOffset) {
+ line += opts.lineOffset;
+ }
+ if (opts.moveCursor) {
+ let location = { line: line - 1, ch: opts.columnOffset || 0 };
+ this.editor.setCursor(location);
+ }
+ if (!opts.noDebug) {
+ this.editor.setDebugLocation(line - 1);
+ }
+ window.emit(EVENTS.EDITOR_LOCATION_SET);
+ },
+
+ /**
+ * Update the source editor's current caret and debug location based on
+ * a requested url and line.
+ *
+ * @param string aActor
+ * The target actor id.
+ * @param number aLine [optional]
+ * The target line in the source.
+ * @param object aFlags [optional]
+ * Additional options for showing the source. Supported options:
+ * - charOffset: character offset for the caret or debug location
+ * - lineOffset: line offset for the caret or debug location
+ * - columnOffset: column offset for the caret or debug location
+ * - noCaret: don't set the caret location at the specified line
+ * - noDebug: don't set the debug location at the specified line
+ * - align: string specifying whether to align the specified line
+ * at the "top", "center" or "bottom" of the editor
+ * - force: boolean forcing all text to be reshown in the editor
+ * @return object
+ * A promise that is resolved after the source text has been set.
+ */
+ setEditorLocation: function (aActor, aLine, aFlags = {}) {
+ // Avoid trying to set a source for a url that isn't known yet.
+ if (!this.Sources.containsValue(aActor)) {
+ throw new Error("Unknown source for the specified URL.");
+ }
+
+ let sourceItem = this.Sources.getItemByValue(aActor);
+ let source = sourceItem.attachment.source;
+
+ // Make sure the requested source client is shown in the editor,
+ // then update the source editor's caret position and debug
+ // location.
+ this.controller.dispatch(actions.selectSource(source, {
+ line: aLine,
+ charOffset: aFlags.charOffset,
+ lineOffset: aFlags.lineOffset,
+ columnOffset: aFlags.columnOffset,
+ moveCursor: !aFlags.noCaret,
+ noDebug: aFlags.noDebug,
+ forceUpdate: aFlags.force
+ }));
+ },
+
+ /**
+ * Gets the visibility state of the instruments pane.
+ * @return boolean
+ */
+ get instrumentsPaneHidden() {
+ return this._instrumentsPane.classList.contains("pane-collapsed");
+ },
+
+ /**
+ * Gets the currently selected tab in the instruments pane.
+ * @return string
+ */
+ get instrumentsPaneTab() {
+ return this._instrumentsPane.selectedTab.id;
+ },
+
+ /**
+ * Sets the instruments pane hidden or visible.
+ *
+ * @param object aFlags
+ * An object containing some of the following properties:
+ * - visible: true if the pane should be shown, false to hide
+ * - animated: true to display an animation on toggle
+ * - delayed: true to wait a few cycles before toggle
+ * - callback: a function to invoke when the toggle finishes
+ * @param number aTabIndex [optional]
+ * The index of the intended selected tab in the details pane.
+ */
+ toggleInstrumentsPane: function (aFlags, aTabIndex) {
+ let pane = this._instrumentsPane;
+ let button = this._instrumentsPaneToggleButton;
+
+ ViewHelpers.togglePane(aFlags, pane);
+
+ if (aFlags.visible) {
+ button.classList.remove("pane-collapsed");
+ button.setAttribute("tooltiptext", this._collapsePaneString);
+ } else {
+ button.classList.add("pane-collapsed");
+ button.setAttribute("tooltiptext", this._expandPaneString);
+ }
+
+ if (aTabIndex !== undefined) {
+ pane.selectedIndex = aTabIndex;
+ }
+ },
+
+ /**
+ * Sets the instruments pane visible after a short period of time.
+ *
+ * @param function aCallback
+ * A function to invoke when the toggle finishes.
+ */
+ showInstrumentsPane: function (aCallback) {
+ DebuggerView.toggleInstrumentsPane({
+ visible: true,
+ animated: true,
+ delayed: true,
+ callback: aCallback
+ }, 0);
+ },
+
+ /**
+ * Handles a tab selection event on the instruments pane.
+ */
+ _onInstrumentsPaneTabSelect: function () {
+ if (this._instrumentsPane.selectedTab.id == "events-tab") {
+ this.controller.dispatch(actions.fetchEventListeners());
+ }
+ },
+
+ /**
+ * Handles a host change event issued by the parent toolbox.
+ *
+ * @param string aType
+ * The host type, either "bottom", "side" or "window".
+ */
+ handleHostChanged: function (hostType) {
+ this._hostType = hostType;
+ this.updateLayoutMode();
+ },
+
+ /**
+ * Resize handler for this container's window.
+ */
+ _onResize: function (evt) {
+ // Allow requests to settle down first.
+ setNamedTimeout(
+ "resize-events", RESIZE_REFRESH_RATE, () => this.updateLayoutMode());
+ },
+
+ /**
+ * Set the layout to "vertical" or "horizontal" depending on the host type.
+ */
+ updateLayoutMode: function () {
+ if (this._isSmallWindowHost() || this._hostType == "side") {
+ this._setLayoutMode("vertical");
+ } else {
+ this._setLayoutMode("horizontal");
+ }
+ },
+
+ /**
+ * Check if the current host is in window mode and is
+ * too small for horizontal layout
+ */
+ _isSmallWindowHost: function () {
+ if (this._hostType != "window") {
+ return false;
+ }
+
+ return window.outerWidth <= BREAKPOINT_SMALL_WINDOW_WIDTH;
+ },
+
+ /**
+ * Enter the provided layoutMode. Do nothing if the layout is the same as the current one.
+ * @param {String} layoutMode new layout ("vertical" or "horizontal")
+ */
+ _setLayoutMode: function (layoutMode) {
+ if (this._body.getAttribute("layout") == layoutMode) {
+ return;
+ }
+
+ if (layoutMode == "vertical") {
+ this._enterVerticalLayout();
+ } else {
+ this._enterHorizontalLayout();
+ }
+
+ this._body.setAttribute("layout", layoutMode);
+ window.emit(EVENTS.LAYOUT_CHANGED, layoutMode);
+ },
+
+ /**
+ * Switches the debugger widgets to a vertical layout.
+ */
+ _enterVerticalLayout: function () {
+ let vertContainer = document.getElementById("vertical-layout-panes-container");
+
+ // Move the soruces and instruments panes in a different container.
+ let splitter = document.getElementById("sources-and-instruments-splitter");
+ vertContainer.insertBefore(this._workersAndSourcesPane, splitter);
+ vertContainer.appendChild(this._instrumentsPane);
+
+ // Make sure the vertical layout container's height doesn't repeatedly
+ // grow or shrink based on the displayed sources, variables etc.
+ vertContainer.setAttribute("height",
+ vertContainer.getBoundingClientRect().height);
+ },
+
+ /**
+ * Switches the debugger widgets to a horizontal layout.
+ */
+ _enterHorizontalLayout: function () {
+ let normContainer = document.getElementById("debugger-widgets");
+ let editorPane = document.getElementById("editor-and-instruments-pane");
+
+ // The sources and instruments pane need to be inserted at their
+ // previous locations in their normal container.
+ let splitter = document.getElementById("sources-and-editor-splitter");
+ normContainer.insertBefore(this._workersAndSourcesPane, splitter);
+ editorPane.appendChild(this._instrumentsPane);
+
+ // Revert to the preferred sources and instruments widths, because
+ // they flexed in the vertical layout.
+ this._workersAndSourcesPane.setAttribute("width", Prefs.workersAndSourcesWidth);
+ this._instrumentsPane.setAttribute("width", Prefs.instrumentsWidth);
+ },
+
+ /**
+ * Handles any initialization on a tab navigation event issued by the client.
+ */
+ handleTabNavigation: function () {
+ dumpn("Handling tab navigation in the DebuggerView");
+ this.Filtering.clearSearch();
+ this.GlobalSearch.clearView();
+ this.StackFrames.empty();
+ this.Sources.empty();
+ this.Variables.empty();
+ this.EventListeners.empty();
+
+ if (this.editor) {
+ this.editor.setMode(Editor.modes.text);
+ this.editor.setText("");
+ this.editor.clearHistory();
+ this._editorSource = {};
+ this._editorDocuments = {};
+ }
+ },
+
+ Toolbar: null,
+ Options: null,
+ Filtering: null,
+ GlobalSearch: null,
+ StackFrames: null,
+ Sources: null,
+ Variables: null,
+ VariableBubble: null,
+ WatchExpressions: null,
+ EventListeners: null,
+ editor: null,
+ _loadingText: "",
+ _body: null,
+ _editorDeck: null,
+ _workersAndSourcesPane: null,
+ _instrumentsPane: null,
+ _instrumentsPaneToggleButton: null,
+ _collapsePaneString: "",
+ _expandPaneString: ""
+};
+
+/**
+ * A custom items container, used for displaying views like the
+ * FilteredSources, FilteredFunctions etc., inheriting the generic WidgetMethods.
+ */
+function ResultsPanelContainer() {
+}
+
+ResultsPanelContainer.prototype = Heritage.extend(WidgetMethods, {
+ /**
+ * Sets the anchor node for this container panel.
+ * @param nsIDOMNode aNode
+ */
+ set anchor(aNode) {
+ this._anchor = aNode;
+
+ // If the anchor node is not null, create a panel to attach to the anchor
+ // when showing the popup.
+ if (aNode) {
+ if (!this._panel) {
+ this._panel = document.createElement("panel");
+ this._panel.id = "results-panel";
+ this._panel.setAttribute("level", "top");
+ this._panel.setAttribute("noautofocus", "true");
+ this._panel.setAttribute("consumeoutsideclicks", "false");
+ document.documentElement.appendChild(this._panel);
+ }
+ if (!this.widget) {
+ this.widget = new SimpleListWidget(this._panel);
+ this.autoFocusOnFirstItem = false;
+ this.autoFocusOnSelection = false;
+ this.maintainSelectionVisible = false;
+ }
+ }
+ // Cleanup the anchor and remove the previously created panel.
+ else {
+ this._panel.remove();
+ this._panel = null;
+ this.widget = null;
+ }
+ },
+
+ /**
+ * Gets the anchor node for this container panel.
+ * @return nsIDOMNode
+ */
+ get anchor() {
+ return this._anchor;
+ },
+
+ /**
+ * Sets the container panel hidden or visible. It's hidden by default.
+ * @param boolean aFlag
+ */
+ set hidden(aFlag) {
+ if (aFlag) {
+ this._panel.hidden = true;
+ this._panel.hidePopup();
+ } else {
+ this._panel.hidden = false;
+ this._panel.openPopup(this._anchor, this.position, this.left, this.top);
+ }
+ },
+
+ /**
+ * Gets this container's visibility state.
+ * @return boolean
+ */
+ get hidden() {
+ return this._panel.state == "closed" ||
+ this._panel.state == "hiding";
+ },
+
+ /**
+ * Removes all items from this container and hides it.
+ */
+ clearView: function () {
+ this.hidden = true;
+ this.empty();
+ },
+
+ /**
+ * Selects the next found item in this container.
+ * Does not change the currently focused node.
+ */
+ selectNext: function () {
+ let nextIndex = this.selectedIndex + 1;
+ if (nextIndex >= this.itemCount) {
+ nextIndex = 0;
+ }
+ this.selectedItem = this.getItemAtIndex(nextIndex);
+ },
+
+ /**
+ * Selects the previously found item in this container.
+ * Does not change the currently focused node.
+ */
+ selectPrev: function () {
+ let prevIndex = this.selectedIndex - 1;
+ if (prevIndex < 0) {
+ prevIndex = this.itemCount - 1;
+ }
+ this.selectedItem = this.getItemAtIndex(prevIndex);
+ },
+
+ /**
+ * Customization function for creating an item's UI.
+ *
+ * @param string aLabel
+ * The item's label string.
+ * @param string aBeforeLabel
+ * An optional string shown before the label.
+ * @param string aBelowLabel
+ * An optional string shown underneath the label.
+ */
+ _createItemView: function (aLabel, aBelowLabel, aBeforeLabel) {
+ let container = document.createElement("vbox");
+ container.className = "results-panel-item";
+
+ let firstRowLabels = document.createElement("hbox");
+ let secondRowLabels = document.createElement("hbox");
+
+ if (aBeforeLabel) {
+ let beforeLabelNode = document.createElement("label");
+ beforeLabelNode.className = "plain results-panel-item-label-before";
+ beforeLabelNode.setAttribute("value", aBeforeLabel);
+ firstRowLabels.appendChild(beforeLabelNode);
+ }
+
+ let labelNode = document.createElement("label");
+ labelNode.className = "plain results-panel-item-label";
+ labelNode.setAttribute("value", aLabel);
+ firstRowLabels.appendChild(labelNode);
+
+ if (aBelowLabel) {
+ let belowLabelNode = document.createElement("label");
+ belowLabelNode.className = "plain results-panel-item-label-below";
+ belowLabelNode.setAttribute("value", aBelowLabel);
+ secondRowLabels.appendChild(belowLabelNode);
+ }
+
+ container.appendChild(firstRowLabels);
+ container.appendChild(secondRowLabels);
+
+ return container;
+ },
+
+ _anchor: null,
+ _panel: null,
+ position: RESULTS_PANEL_POPUP_POSITION,
+ left: 0,
+ top: 0
+});
+
+DebuggerView.EventListeners = new EventListenersView(DebuggerController);
+DebuggerView.Sources = new SourcesView(DebuggerController, DebuggerView);
diff --git a/devtools/client/debugger/debugger.css b/devtools/client/debugger/debugger.css
new file mode 100644
index 000000000..9898e6bf0
--- /dev/null
+++ b/devtools/client/debugger/debugger.css
@@ -0,0 +1,69 @@
+/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */
+
+/* Side pane views */
+
+#workers-pane > tabpanels > tabpanel,
+#sources-pane > tabpanels > tabpanel,
+#instruments-pane > tabpanels > tabpanel {
+ -moz-box-orient: vertical;
+}
+
+/* Toolbar controls */
+
+.devtools-toolbarbutton:not([label]) > .toolbarbutton-text {
+ display: none;
+}
+
+/* Horizontal vs. vertical layout */
+
+#body[layout=vertical] #debugger-widgets {
+ -moz-box-orient: vertical;
+}
+
+#body[layout=vertical] #workers-and-sources-pane {
+ -moz-box-flex: 1;
+}
+
+#body[layout=vertical] #instruments-pane {
+ -moz-box-flex: 2;
+}
+
+#body[layout=vertical] #instruments-pane-toggle {
+ display: none;
+}
+
+#body[layout=vertical] #sources-and-editor-splitter,
+#body[layout=vertical] #editor-and-instruments-splitter {
+ display: none;
+}
+
+#body[layout=horizontal] #vertical-layout-splitter,
+#body[layout=horizontal] #vertical-layout-panes-container {
+ display: none;
+}
+
+#body[layout=vertical] #stackframes {
+ visibility: hidden;
+}
+
+#source-progress-container {
+ display: flex;
+ flex-flow: column;
+ justify-content: center;
+}
+
+#source-progress {
+ flex: none;
+}
+
+#redux-devtools * {
+ display: block;
+}
+
+#redux-devtools span {
+ display: inline
+}
diff --git a/devtools/client/debugger/debugger.xul b/devtools/client/debugger/debugger.xul
new file mode 100644
index 000000000..5a22cf7f8
--- /dev/null
+++ b/devtools/client/debugger/debugger.xul
@@ -0,0 +1,474 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/content/shared/widgets/widgets.css" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/content/debugger/debugger.css" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/skin/widgets.css" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/skin/debugger.css" type="text/css"?>
+<!DOCTYPE window [
+ <!ENTITY % debuggerDTD SYSTEM "chrome://devtools/locale/debugger.dtd">
+ %debuggerDTD;
+]>
+<?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ macanimationtype="document"
+ fullscreenbutton="true"
+ screenX="4" screenY="4"
+ width="960" height="480"
+ persist="screenX screenY width height sizemode">
+
+ <script type="application/javascript;version=1.8"
+ src="chrome://devtools/content/shared/theme-switching.js"/>
+ <script type="text/javascript" src="chrome://global/content/globalOverlay.js"/>
+ <script type="text/javascript" src="debugger-controller.js"/>
+ <script type="text/javascript" src="debugger-view.js"/>
+ <script type="text/javascript" src="utils.js"/>
+ <script type="text/javascript" src="views/workers-view.js"/>
+ <script type="text/javascript" src="views/variable-bubble-view.js"/>
+ <script type="text/javascript" src="views/watch-expressions-view.js"/>
+ <script type="text/javascript" src="views/global-search-view.js"/>
+ <script type="text/javascript" src="views/toolbar-view.js"/>
+ <script type="text/javascript" src="views/options-view.js"/>
+ <script type="text/javascript" src="views/stack-frames-view.js"/>
+ <script type="text/javascript" src="views/stack-frames-classic-view.js"/>
+ <script type="text/javascript" src="views/filter-view.js"/>
+
+ <commandset id="editMenuCommands"/>
+
+ <commandset id="debuggerCommands"></commandset>
+
+ <popupset id="debuggerPopupset">
+ <menupopup id="sourceEditorContextMenu"
+ onpopupshowing="goUpdateGlobalEditMenuItems()">
+ <menuitem id="se-dbg-cMenu-addBreakpoint"
+ label="&debuggerUI.seMenuBreak;"
+ key="addBreakpointKey"
+ command="addBreakpointCommand"/>
+ <menuitem id="se-dbg-cMenu-addConditionalBreakpoint"
+ label="&debuggerUI.seMenuCondBreak;"
+ key="addConditionalBreakpointKey"
+ command="addConditionalBreakpointCommand"/>
+ <menuitem id="se-dbg-cMenu-editConditionalBreakpoint"
+ label="&debuggerUI.seEditMenuCondBreak;"
+ key="addConditionalBreakpointKey"
+ command="addConditionalBreakpointCommand"/>
+ <menuitem id="se-dbg-cMenu-addAsWatch"
+ label="&debuggerUI.seMenuAddWatch;"
+ key="addWatchExpressionKey"
+ command="addWatchExpressionCommand"/>
+ <menuseparator/>
+ <menuitem id="cMenu_copy"/>
+ <menuseparator/>
+ <menuitem id="cMenu_selectAll"/>
+ <menuseparator/>
+ <menuitem id="se-dbg-cMenu-findFile"
+ label="&debuggerUI.searchFile;"
+ accesskey="&debuggerUI.searchFile.accesskey;"
+ key="fileSearchKey"
+ command="fileSearchCommand"/>
+ <menuitem id="se-dbg-cMenu-findGlobal"
+ label="&debuggerUI.searchGlobal;"
+ accesskey="&debuggerUI.searchGlobal.accesskey;"
+ key="globalSearchKey"
+ command="globalSearchCommand"/>
+ <menuitem id="se-dbg-cMenu-findFunction"
+ label="&debuggerUI.searchFunction;"
+ accesskey="&debuggerUI.searchFunction.accesskey;"
+ key="functionSearchKey"
+ command="functionSearchCommand"/>
+ <menuseparator/>
+ <menuitem id="se-dbg-cMenu-findToken"
+ label="&debuggerUI.searchToken;"
+ accesskey="&debuggerUI.searchToken.accesskey;"
+ key="tokenSearchKey"
+ command="tokenSearchCommand"/>
+ <menuitem id="se-dbg-cMenu-findLine"
+ label="&debuggerUI.searchGoToLine;"
+ accesskey="&debuggerUI.searchGoToLine.accesskey;"
+ key="lineSearchKey"
+ command="lineSearchCommand"/>
+ <menuseparator/>
+ <menuitem id="se-dbg-cMenu-findVariable"
+ label="&debuggerUI.searchVariable;"
+ accesskey="&debuggerUI.searchVariable.accesskey;"
+ key="variableSearchKey"
+ command="variableSearchCommand"/>
+ <menuitem id="se-dbg-cMenu-focusVariables"
+ label="&debuggerUI.focusVariables;"
+ accesskey="&debuggerUI.focusVariables.accesskey;"
+ key="variablesFocusKey"
+ command="variablesFocusCommand"/>
+ <menuitem id="se-dbg-cMenu-prettyPrint"
+ label="&debuggerUI.sources.prettyPrint;"
+ command="prettyPrintCommand"/>
+ </menupopup>
+ <menupopup id="debuggerWatchExpressionsContextMenu">
+ <menuitem id="add-watch-expression"
+ label="&debuggerUI.addWatch;"
+ accesskey="&debuggerUI.addWatch.accesskey;"
+ key="addWatchExpressionKey"
+ command="addWatchExpressionCommand"/>
+ <menuitem id="removeAll-watch-expression"
+ label="&debuggerUI.removeAllWatch;"
+ accesskey="&debuggerUI.removeAllWatch.accesskey;"
+ key="removeAllWatchExpressionsKey"
+ command="removeAllWatchExpressionsCommand"/>
+ </menupopup>
+ <menupopup id="debuggerPrefsContextMenu"
+ position="before_end"
+ onpopupshowing="DebuggerView.Options._onPopupShowing()"
+ onpopuphiding="DebuggerView.Options._onPopupHiding()"
+ onpopuphidden="DebuggerView.Options._onPopupHidden()">
+ <menuitem id="auto-pretty-print"
+ type="checkbox"
+ label="&debuggerUI.autoPrettyPrint;"
+ accesskey="&debuggerUI.autoPrettyPrint.accesskey;"
+ command="toggleAutoPrettyPrint"/>
+ <menuitem id="pause-on-exceptions"
+ type="checkbox"
+ label="&debuggerUI.pauseExceptions;"
+ accesskey="&debuggerUI.pauseExceptions.accesskey;"
+ command="togglePauseOnExceptions"/>
+ <menuitem id="ignore-caught-exceptions"
+ type="checkbox"
+ label="&debuggerUI.ignoreCaughtExceptions;"
+ accesskey="&debuggerUI.ignoreCaughtExceptions.accesskey;"
+ command="toggleIgnoreCaughtExceptions"/>
+ <menuitem id="show-panes-on-startup"
+ type="checkbox"
+ label="&debuggerUI.showPanesOnInit;"
+ accesskey="&debuggerUI.showPanesOnInit.accesskey;"
+ command="toggleShowPanesOnStartup"/>
+ <menuitem id="show-vars-only-enum"
+ type="checkbox"
+ label="&debuggerUI.showOnlyEnum;"
+ accesskey="&debuggerUI.showOnlyEnum.accesskey;"
+ command="toggleShowOnlyEnum"/>
+ <menuitem id="show-vars-filter-box"
+ type="checkbox"
+ label="&debuggerUI.showVarsFilter;"
+ accesskey="&debuggerUI.showVarsFilter.accesskey;"
+ command="toggleShowVariablesFilterBox"/>
+ <menuitem id="show-original-source"
+ type="checkbox"
+ label="&debuggerUI.showOriginalSource;"
+ accesskey="&debuggerUI.showOriginalSource.accesskey;"
+ command="toggleShowOriginalSource"/>
+ <menuitem id="auto-black-box"
+ type="checkbox"
+ label="&debuggerUI.autoBlackBox;"
+ accesskey="&debuggerUI.autoBlackBox.accesskey;"
+ command="toggleAutoBlackBox"/>
+ </menupopup>
+ </popupset>
+
+ <popupset id="debuggerSourcesPopupset">
+ <menupopup id="debuggerSourcesContextMenu">
+ <menuitem id="debugger-sources-context-newtab"
+ label="&debuggerUI.context.newTab;"
+ accesskey="&debuggerUI.context.newTab.accesskey;"/>
+ <menuitem id="debugger-sources-context-copyurl"
+ label="&debuggerUI.context.copyUrl;"
+ accesskey="&debuggerUI.context.copyUrl.accesskey;"/>
+ </menupopup>
+ </popupset>
+
+ <keyset id="debuggerKeys">
+ <key id="nextSourceKey"
+ keycode="VK_DOWN"
+ modifiers="accel alt"
+ command="nextSourceCommand"/>
+ <key id="prevSourceKey"
+ keycode="VK_UP"
+ modifiers="accel alt"
+ command="prevSourceCommand"/>
+ <key id="resumeKey"
+ keycode="&debuggerUI.stepping.resume1;"
+ command="resumeCommand"/>
+ <key id="stepOverKey"
+ keycode="&debuggerUI.stepping.stepOver1;"
+ command="stepOverCommand"/>
+ <key id="stepInKey"
+ keycode="&debuggerUI.stepping.stepIn1;"
+ command="stepInCommand"/>
+ <key id="stepOutKey"
+ keycode="&debuggerUI.stepping.stepOut1;"
+ modifiers="shift"
+ command="stepOutCommand"/>
+ <key id="fileSearchKey"
+ key="&debuggerUI.searchFile.key;"
+ modifiers="accel"
+ command="fileSearchCommand"/>
+ <key id="fileSearchKey"
+ key="&debuggerUI.searchFile.altkey;"
+ modifiers="accel"
+ command="fileSearchCommand"/>
+ <key id="globalSearchKey"
+ key="&debuggerUI.searchGlobal.key;"
+ modifiers="accel alt"
+ command="globalSearchCommand"/>
+ <key id="functionSearchKey"
+ key="&debuggerUI.searchFunction.key;"
+ modifiers="accel"
+ command="functionSearchCommand"/>
+ <key id="tokenSearchKey"
+ key="&debuggerUI.searchToken.key;"
+ modifiers="accel"
+ command="tokenSearchCommand"/>
+ <key id="lineSearchKey"
+ key="&debuggerUI.searchGoToLine.key;"
+ modifiers="accel"
+ command="lineSearchCommand"/>
+ <key id="variableSearchKey"
+ key="&debuggerUI.searchVariable.key;"
+ modifiers="accel alt"
+ command="variableSearchCommand"/>
+ <key id="variablesFocusKey"
+ key="&debuggerUI.focusVariables.key;"
+ modifiers="accel shift"
+ command="variablesFocusCommand"/>
+ <key id="addBreakpointKey"
+ key="&debuggerUI.seMenuBreak.key;"
+ modifiers="accel"
+ command="addBreakpointCommand"/>
+ <key id="addConditionalBreakpointKey"
+ key="&debuggerUI.seMenuCondBreak.key;"
+ modifiers="accel shift"
+ command="addConditionalBreakpointCommand"/>
+ <key id="addWatchExpressionKey"
+ key="&debuggerUI.seMenuAddWatch.key;"
+ modifiers="accel shift"
+ command="addWatchExpressionCommand"/>
+ <key id="removeAllWatchExpressionsKey"
+ key="&debuggerUI.removeAllWatch.key;"
+ modifiers="accel alt"
+ command="removeAllWatchExpressionsCommand"/>
+ <key id="debuggerSourcesCopyUrl"
+ key="&debuggerUI.context.copyUrl.key;"
+ modifiers="accel"
+ oncommand="DebuggerView.Sources._onCopyUrlCommand()"/>
+ </keyset>
+
+ <vbox id="body"
+ class="theme-body"
+ layout="horizontal"
+ flex="1">
+ <toolbar id="debugger-toolbar"
+ class="devtools-toolbar">
+ <hbox id="debugger-controls"
+ class="devtools-toolbarbutton-group">
+ <toolbarbutton id="resume"
+ class="devtools-toolbarbutton"
+ tabindex="0"/>
+ <toolbarbutton id="step-over"
+ class="devtools-toolbarbutton"
+ tabindex="0"/>
+ <toolbarbutton id="step-in"
+ class="devtools-toolbarbutton"
+ tabindex="0"/>
+ <toolbarbutton id="step-out"
+ class="devtools-toolbarbutton"
+ tabindex="0"/>
+ </hbox>
+ <vbox id="stackframes" flex="1"/>
+ <textbox id="searchbox"
+ class="devtools-searchinput" type="search"/>
+ <toolbarbutton id="instruments-pane-toggle"
+ class="devtools-toolbarbutton"
+ tooltiptext="&debuggerUI.panesButton.tooltip;"
+ tabindex="0"/>
+ <toolbarbutton id="debugger-options"
+ class="devtools-toolbarbutton devtools-option-toolbarbutton"
+ tooltiptext="&debuggerUI.optsButton.tooltip;"
+ popup="debuggerPrefsContextMenu"
+ tabindex="0"/>
+ </toolbar>
+ <vbox id="globalsearch" orient="vertical" hidden="true"/>
+ <splitter class="devtools-horizontal-splitter" hidden="true"/>
+ <hbox id="debugger-widgets" flex="1">
+ <vbox id="workers-and-sources-pane">
+ <tabbox id="workers-pane"
+ class="devtools-sidebar-tabs"
+ flex="0"
+ hidden="true">
+ <tabs>
+ <tab id="workers-tab"
+ crop="end"
+ label="&debuggerUI.tabs.workers;"/>
+ </tabs>
+ <tabpanels flex="1">
+ <tabpanel>
+ <vbox id="workers" flex="1"/>
+ </tabpanel>
+ </tabpanels>
+ </tabbox>
+ <splitter id="workers-splitter"
+ class="devtools-horizontal-splitter"
+ hidden="true" />
+ <tabbox id="sources-pane"
+ class="devtools-sidebar-tabs"
+ flex="1">
+ <tabs>
+ <tab id="sources-tab"
+ crop="end"
+ label="&debuggerUI.tabs.sources;"/>
+ <tab id="callstack-tab"
+ crop="end"
+ label="&debuggerUI.tabs.callstack;"/>
+ </tabs>
+ <tabpanels flex="1">
+ <tabpanel id="sources-tabpanel">
+ <vbox id="sources" flex="1"/>
+ <toolbar id="sources-toolbar" class="devtools-toolbar">
+ <hbox id="sources-controls"
+ class="devtools-toolbarbutton-group">
+ <toolbarbutton id="black-box"
+ class="devtools-toolbarbutton"
+ tooltiptext="&debuggerUI.sources.blackBoxTooltip;"
+ command="blackBoxCommand"/>
+ <toolbarbutton id="pretty-print"
+ class="devtools-toolbarbutton"
+ tooltiptext="&debuggerUI.sources.prettyPrint;"
+ command="prettyPrintCommand"
+ hidden="true"/>
+ </hbox>
+ <vbox class="devtools-separator"/>
+ <toolbarbutton id="toggle-breakpoints"
+ class="devtools-toolbarbutton"
+ tooltiptext="&debuggerUI.sources.toggleBreakpoints;"
+ command="toggleBreakpointsCommand"/>
+ </toolbar>
+ </tabpanel>
+ <tabpanel id="callstack-tabpanel">
+ <vbox id="callstack-list" flex="1"/>
+ </tabpanel>
+ </tabpanels>
+ </tabbox>
+ </vbox>
+ <splitter id="sources-and-editor-splitter"
+ class="devtools-side-splitter"/>
+ <vbox id="debugger-content" flex="1">
+ <hbox id="editor-and-instruments-pane" flex="1">
+ <deck id="editor-deck" flex="1" class="devtools-main-content">
+ <vbox id="editor"/>
+ <vbox id="black-boxed-message"
+ align="center"
+ pack="center">
+ <description id="black-boxed-message-label">
+ &debuggerUI.blackBoxMessage.label;
+ </description>
+ <button id="black-boxed-message-button"
+ class="devtools-toolbarbutton"
+ label="&debuggerUI.blackBoxMessage.unBlackBoxButton;"
+ command="unBlackBoxCommand"/>
+ </vbox>
+ <html:div id="source-progress-container"
+ align="center">
+ <html:div id="hbox">
+ <html:progress id="source-progress"></html:progress>
+ </html:div>
+ </html:div>
+ </deck>
+ <splitter id="editor-and-instruments-splitter"
+ class="devtools-side-splitter"/>
+ <tabbox id="instruments-pane"
+ class="devtools-sidebar-tabs"
+ hidden="true">
+ <tabs>
+ <tab id="variables-tab"
+ crop="end"
+ label="&debuggerUI.tabs.variables;"/>
+ <tab id="events-tab"
+ crop="end"
+ label="&debuggerUI.tabs.events;"/>
+ </tabs>
+ <tabpanels flex="1">
+ <tabpanel id="variables-tabpanel">
+ <vbox id="expressions"/>
+ <splitter class="devtools-horizontal-splitter"/>
+ <vbox id="variables" flex="1"/>
+ </tabpanel>
+ <tabpanel id="events-tabpanel">
+ <vbox id="event-listeners" flex="1"/>
+ </tabpanel>
+ </tabpanels>
+ </tabbox>
+ </hbox>
+ </vbox>
+ <splitter id="vertical-layout-splitter"
+ class="devtools-horizontal-splitter"/>
+ <hbox id="vertical-layout-panes-container">
+ <splitter id="sources-and-instruments-splitter"
+ class="devtools-side-splitter"/>
+ <!-- The sources-pane and instruments-pane will be moved in this
+ container if the toolbox's host requires it. -->
+ </hbox>
+ </hbox>
+ </vbox>
+
+ <panel id="searchbox-help-panel"
+ level="top"
+ type="arrow"
+ position="before_start"
+ noautofocus="true"
+ consumeoutsideclicks="false">
+ <vbox>
+ <hbox>
+ <label id="filter-label"/>
+ </hbox>
+ <label id="searchbox-panel-operators"
+ value="&debuggerUI.searchPanelOperators;"/>
+ <hbox align="center">
+ <button id="global-operator-button"
+ class="searchbox-panel-operator-button devtools-monospace"
+ command="globalSearchCommand"/>
+ <label id="global-operator-label"
+ class="plain searchbox-panel-operator-label"/>
+ </hbox>
+ <hbox align="center">
+ <button id="function-operator-button"
+ class="searchbox-panel-operator-button devtools-monospace"
+ command="functionSearchCommand"/>
+ <label id="function-operator-label"
+ class="plain searchbox-panel-operator-label"/>
+ </hbox>
+ <hbox align="center">
+ <button id="token-operator-button"
+ class="searchbox-panel-operator-button devtools-monospace"
+ command="tokenSearchCommand"/>
+ <label id="token-operator-label"
+ class="plain searchbox-panel-operator-label"/>
+ </hbox>
+ <hbox align="center">
+ <button id="line-operator-button"
+ class="searchbox-panel-operator-button devtools-monospace"
+ command="lineSearchCommand"/>
+ <label id="line-operator-label"
+ class="plain searchbox-panel-operator-label"/>
+ </hbox>
+ <hbox align="center">
+ <button id="variable-operator-button"
+ class="searchbox-panel-operator-button devtools-monospace"
+ command="variableSearchCommand"/>
+ <label id="variable-operator-label"
+ class="plain searchbox-panel-operator-label"/>
+ </hbox>
+ </vbox>
+ </panel>
+
+ <panel id="conditional-breakpoint-panel"
+ level="top"
+ type="arrow"
+ noautofocus="true"
+ consumeoutsideclicks="false">
+ <vbox>
+ <label id="conditional-breakpoint-panel-description"
+ value="&debuggerUI.condBreakPanelTitle;"/>
+ <textbox id="conditional-breakpoint-panel-textbox"/>
+ </vbox>
+ </panel>
+</window>
diff --git a/devtools/client/debugger/moz.build b/devtools/client/debugger/moz.build
new file mode 100644
index 000000000..9719ec002
--- /dev/null
+++ b/devtools/client/debugger/moz.build
@@ -0,0 +1,20 @@
+# vim: set filetype=python:
+# 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/.
+
+DIRS += [
+ 'content',
+ 'new'
+]
+
+DevToolsModules(
+ 'debugger-commands.js',
+ 'panel.js'
+)
+
+BROWSER_CHROME_MANIFESTS += [
+ 'new/test/mochitest/browser.ini',
+ 'test/mochitest/browser.ini',
+ 'test/mochitest/browser2.ini'
+]
diff --git a/devtools/client/debugger/new/bundle.js b/devtools/client/debugger/new/bundle.js
new file mode 100644
index 000000000..cbbd15a44
--- /dev/null
+++ b/devtools/client/debugger/new/bundle.js
@@ -0,0 +1,58335 @@
+// Generated from: 8175aacaec380ecf859183ad62bee2a9aef180d2 Disable searching test because it's timing out on try on certain platforms for some reason
+
+var Debugger =
+/******/ (function(modules) { // webpackBootstrap
+/******/ // The module cache
+/******/ var installedModules = {};
+/******/
+/******/ // The require function
+/******/ function __webpack_require__(moduleId) {
+/******/
+/******/ // Check if module is in cache
+/******/ if(installedModules[moduleId])
+/******/ return installedModules[moduleId].exports;
+/******/
+/******/ // Create a new module (and put it into the cache)
+/******/ var module = installedModules[moduleId] = {
+/******/ exports: {},
+/******/ id: moduleId,
+/******/ loaded: false
+/******/ };
+/******/
+/******/ // Execute the module function
+/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
+/******/
+/******/ // Flag the module as loaded
+/******/ module.loaded = true;
+/******/
+/******/ // Return the exports of the module
+/******/ return module.exports;
+/******/ }
+/******/
+/******/
+/******/ // expose the modules object (__webpack_modules__)
+/******/ __webpack_require__.m = modules;
+/******/
+/******/ // expose the module cache
+/******/ __webpack_require__.c = installedModules;
+/******/
+/******/ // __webpack_public_path__
+/******/ __webpack_require__.p = "/public/build";
+/******/
+/******/ // Load entry module and return exports
+/******/ return __webpack_require__(0);
+/******/ })
+/************************************************************************/
+/******/ ([
+/* 0 */
+/***/ function(module, exports, __webpack_require__) {
+
+ module.exports = __webpack_require__(1);
+
+
+/***/ },
+/* 1 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var React = __webpack_require__(2);
+
+ var _require = __webpack_require__(3);
+
+ var bindActionCreators = _require.bindActionCreators;
+ var combineReducers = _require.combineReducers;
+
+ var ReactDOM = __webpack_require__(16);
+
+ var _require2 = __webpack_require__(18);
+
+ var _require2$client = _require2.client;
+ var getClient = _require2$client.getClient;
+ var firefox = _require2$client.firefox;
+ var renderRoot = _require2.renderRoot;
+ var bootstrap = _require2.bootstrap;
+
+ var _require3 = __webpack_require__(89);
+
+ var getValue = _require3.getValue;
+ var isFirefoxPanel = _require3.isFirefoxPanel;
+
+
+ var configureStore = __webpack_require__(238);
+
+ var reducers = __webpack_require__(249);
+ var selectors = __webpack_require__(259);
+
+ var App = __webpack_require__(260);
+
+ var createStore = configureStore({
+ log: getValue("logging.actions"),
+ makeThunkArgs: (args, state) => {
+ return Object.assign({}, args, { client: getClient(state) });
+ }
+ });
+
+ var store = createStore(combineReducers(reducers));
+ var actions = bindActionCreators(__webpack_require__(262), store.dispatch);
+
+ if (!isFirefoxPanel()) {
+ L10N.setBundle(__webpack_require__(458));
+ }
+
+ window.appStore = store;
+
+ // Expose the bound actions so external things can do things like
+ // selecting a source.
+ window.actions = {
+ selectSource: actions.selectSource,
+ selectSourceURL: actions.selectSourceURL
+ };
+
+ function unmountRoot() {
+ var mount = document.querySelector("#mount");
+ ReactDOM.unmountComponentAtNode(mount);
+ }
+
+ if (isFirefoxPanel()) {
+ (function () {
+ var sourceMap = __webpack_require__(264);
+ var prettyPrint = __webpack_require__(276);
+
+ module.exports = {
+ bootstrap: (_ref) => {
+ var threadClient = _ref.threadClient;
+ var tabTarget = _ref.tabTarget;
+ var toolbox = _ref.toolbox;
+ var L10N = _ref.L10N;
+
+ // TODO (jlast) remove when the panel has L10N
+ if (L10N) {
+ window.L10N = L10N;
+ } else {
+ window.L10N = __webpack_require__(459);
+ window.L10N.setBundle(__webpack_require__(458));
+ }
+
+ firefox.setThreadClient(threadClient);
+ firefox.setTabTarget(tabTarget);
+ renderRoot(React, ReactDOM, App, store);
+ return firefox.initPage(actions);
+ },
+ destroy: () => {
+ unmountRoot();
+ sourceMap.destroyWorker();
+ prettyPrint.destroyWorker();
+ },
+ store: store,
+ actions: actions,
+ selectors: selectors,
+ client: firefox.clientCommands
+ };
+ })();
+ } else {
+ bootstrap(React, ReactDOM, App, actions, store);
+ }
+
+/***/ },
+/* 2 */
+/***/ function(module, exports) {
+
+ module.exports = devtoolsRequire("devtools/client/shared/vendor/react");
+
+/***/ },
+/* 3 */
+/***/ function(module, exports, __webpack_require__) {
+
+ 'use strict';
+
+ exports.__esModule = true;
+ exports.compose = exports.applyMiddleware = exports.bindActionCreators = exports.combineReducers = exports.createStore = undefined;
+
+ var _createStore = __webpack_require__(4);
+
+ var _createStore2 = _interopRequireDefault(_createStore);
+
+ var _combineReducers = __webpack_require__(11);
+
+ var _combineReducers2 = _interopRequireDefault(_combineReducers);
+
+ var _bindActionCreators = __webpack_require__(13);
+
+ var _bindActionCreators2 = _interopRequireDefault(_bindActionCreators);
+
+ var _applyMiddleware = __webpack_require__(14);
+
+ var _applyMiddleware2 = _interopRequireDefault(_applyMiddleware);
+
+ var _compose = __webpack_require__(15);
+
+ var _compose2 = _interopRequireDefault(_compose);
+
+ var _warning = __webpack_require__(12);
+
+ var _warning2 = _interopRequireDefault(_warning);
+
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
+
+ /*
+ * This is a dummy function to check if the function name has been altered by minification.
+ * If the function has been minified and NODE_ENV !== 'production', warn the user.
+ */
+ function isCrushed() {}
+
+ if (false) {
+ (0, _warning2["default"])('You are currently using minified code outside of NODE_ENV === \'production\'. ' + 'This means that you are running a slower development build of Redux. ' + 'You can use loose-envify (https://github.com/zertosh/loose-envify) for browserify ' + 'or DefinePlugin for webpack (http://stackoverflow.com/questions/30030031) ' + 'to ensure you have the correct code for your production build.');
+ }
+
+ exports.createStore = _createStore2["default"];
+ exports.combineReducers = _combineReducers2["default"];
+ exports.bindActionCreators = _bindActionCreators2["default"];
+ exports.applyMiddleware = _applyMiddleware2["default"];
+ exports.compose = _compose2["default"];
+
+/***/ },
+/* 4 */
+/***/ function(module, exports, __webpack_require__) {
+
+ 'use strict';
+
+ exports.__esModule = true;
+ exports.ActionTypes = undefined;
+ exports["default"] = createStore;
+
+ var _isPlainObject = __webpack_require__(5);
+
+ var _isPlainObject2 = _interopRequireDefault(_isPlainObject);
+
+ var _symbolObservable = __webpack_require__(9);
+
+ var _symbolObservable2 = _interopRequireDefault(_symbolObservable);
+
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
+
+ /**
+ * These are private action types reserved by Redux.
+ * For any unknown actions, you must return the current state.
+ * If the current state is undefined, you must return the initial state.
+ * Do not reference these action types directly in your code.
+ */
+ var ActionTypes = exports.ActionTypes = {
+ INIT: '@@redux/INIT'
+ };
+
+ /**
+ * Creates a Redux store that holds the state tree.
+ * The only way to change the data in the store is to call `dispatch()` on it.
+ *
+ * There should only be a single store in your app. To specify how different
+ * parts of the state tree respond to actions, you may combine several reducers
+ * into a single reducer function by using `combineReducers`.
+ *
+ * @param {Function} reducer A function that returns the next state tree, given
+ * the current state tree and the action to handle.
+ *
+ * @param {any} [initialState] The initial state. You may optionally specify it
+ * to hydrate the state from the server in universal apps, or to restore a
+ * previously serialized user session.
+ * If you use `combineReducers` to produce the root reducer function, this must be
+ * an object with the same shape as `combineReducers` keys.
+ *
+ * @param {Function} enhancer The store enhancer. You may optionally specify it
+ * to enhance the store with third-party capabilities such as middleware,
+ * time travel, persistence, etc. The only store enhancer that ships with Redux
+ * is `applyMiddleware()`.
+ *
+ * @returns {Store} A Redux store that lets you read the state, dispatch actions
+ * and subscribe to changes.
+ */
+ function createStore(reducer, initialState, enhancer) {
+ var _ref2;
+
+ if (typeof initialState === 'function' && typeof enhancer === 'undefined') {
+ enhancer = initialState;
+ initialState = undefined;
+ }
+
+ if (typeof enhancer !== 'undefined') {
+ if (typeof enhancer !== 'function') {
+ throw new Error('Expected the enhancer to be a function.');
+ }
+
+ return enhancer(createStore)(reducer, initialState);
+ }
+
+ if (typeof reducer !== 'function') {
+ throw new Error('Expected the reducer to be a function.');
+ }
+
+ var currentReducer = reducer;
+ var currentState = initialState;
+ var currentListeners = [];
+ var nextListeners = currentListeners;
+ var isDispatching = false;
+
+ function ensureCanMutateNextListeners() {
+ if (nextListeners === currentListeners) {
+ nextListeners = currentListeners.slice();
+ }
+ }
+
+ /**
+ * Reads the state tree managed by the store.
+ *
+ * @returns {any} The current state tree of your application.
+ */
+ function getState() {
+ return currentState;
+ }
+
+ /**
+ * Adds a change listener. It will be called any time an action is dispatched,
+ * and some part of the state tree may potentially have changed. You may then
+ * call `getState()` to read the current state tree inside the callback.
+ *
+ * You may call `dispatch()` from a change listener, with the following
+ * caveats:
+ *
+ * 1. The subscriptions are snapshotted just before every `dispatch()` call.
+ * If you subscribe or unsubscribe while the listeners are being invoked, this
+ * will not have any effect on the `dispatch()` that is currently in progress.
+ * However, the next `dispatch()` call, whether nested or not, will use a more
+ * recent snapshot of the subscription list.
+ *
+ * 2. The listener should not expect to see all state changes, as the state
+ * might have been updated multiple times during a nested `dispatch()` before
+ * the listener is called. It is, however, guaranteed that all subscribers
+ * registered before the `dispatch()` started will be called with the latest
+ * state by the time it exits.
+ *
+ * @param {Function} listener A callback to be invoked on every dispatch.
+ * @returns {Function} A function to remove this change listener.
+ */
+ function subscribe(listener) {
+ if (typeof listener !== 'function') {
+ throw new Error('Expected listener to be a function.');
+ }
+
+ var isSubscribed = true;
+
+ ensureCanMutateNextListeners();
+ nextListeners.push(listener);
+
+ return function unsubscribe() {
+ if (!isSubscribed) {
+ return;
+ }
+
+ isSubscribed = false;
+
+ ensureCanMutateNextListeners();
+ var index = nextListeners.indexOf(listener);
+ nextListeners.splice(index, 1);
+ };
+ }
+
+ /**
+ * Dispatches an action. It is the only way to trigger a state change.
+ *
+ * The `reducer` function, used to create the store, will be called with the
+ * current state tree and the given `action`. Its return value will
+ * be considered the **next** state of the tree, and the change listeners
+ * will be notified.
+ *
+ * The base implementation only supports plain object actions. If you want to
+ * dispatch a Promise, an Observable, a thunk, or something else, you need to
+ * wrap your store creating function into the corresponding middleware. For
+ * example, see the documentation for the `redux-thunk` package. Even the
+ * middleware will eventually dispatch plain object actions using this method.
+ *
+ * @param {Object} action A plain object representing “what changedâ€. It is
+ * a good idea to keep actions serializable so you can record and replay user
+ * sessions, or use the time travelling `redux-devtools`. An action must have
+ * a `type` property which may not be `undefined`. It is a good idea to use
+ * string constants for action types.
+ *
+ * @returns {Object} For convenience, the same action object you dispatched.
+ *
+ * Note that, if you use a custom middleware, it may wrap `dispatch()` to
+ * return something else (for example, a Promise you can await).
+ */
+ function dispatch(action) {
+ if (!(0, _isPlainObject2["default"])(action)) {
+ throw new Error('Actions must be plain objects. ' + 'Use custom middleware for async actions.');
+ }
+
+ if (typeof action.type === 'undefined') {
+ throw new Error('Actions may not have an undefined "type" property. ' + 'Have you misspelled a constant?');
+ }
+
+ if (isDispatching) {
+ throw new Error('Reducers may not dispatch actions.');
+ }
+
+ try {
+ isDispatching = true;
+ currentState = currentReducer(currentState, action);
+ } finally {
+ isDispatching = false;
+ }
+
+ var listeners = currentListeners = nextListeners;
+ for (var i = 0; i < listeners.length; i++) {
+ listeners[i]();
+ }
+
+ return action;
+ }
+
+ /**
+ * Replaces the reducer currently used by the store to calculate the state.
+ *
+ * You might need this if your app implements code splitting and you want to
+ * load some of the reducers dynamically. You might also need this if you
+ * implement a hot reloading mechanism for Redux.
+ *
+ * @param {Function} nextReducer The reducer for the store to use instead.
+ * @returns {void}
+ */
+ function replaceReducer(nextReducer) {
+ if (typeof nextReducer !== 'function') {
+ throw new Error('Expected the nextReducer to be a function.');
+ }
+
+ currentReducer = nextReducer;
+ dispatch({ type: ActionTypes.INIT });
+ }
+
+ /**
+ * Interoperability point for observable/reactive libraries.
+ * @returns {observable} A minimal observable of state changes.
+ * For more information, see the observable proposal:
+ * https://github.com/zenparsing/es-observable
+ */
+ function observable() {
+ var _ref;
+
+ var outerSubscribe = subscribe;
+ return _ref = {
+ /**
+ * The minimal observable subscription method.
+ * @param {Object} observer Any object that can be used as an observer.
+ * The observer object should have a `next` method.
+ * @returns {subscription} An object with an `unsubscribe` method that can
+ * be used to unsubscribe the observable from the store, and prevent further
+ * emission of values from the observable.
+ */
+
+ subscribe: function subscribe(observer) {
+ if (typeof observer !== 'object') {
+ throw new TypeError('Expected the observer to be an object.');
+ }
+
+ function observeState() {
+ if (observer.next) {
+ observer.next(getState());
+ }
+ }
+
+ observeState();
+ var unsubscribe = outerSubscribe(observeState);
+ return { unsubscribe: unsubscribe };
+ }
+ }, _ref[_symbolObservable2["default"]] = function () {
+ return this;
+ }, _ref;
+ }
+
+ // When a store is created, an "INIT" action is dispatched so that every
+ // reducer returns their initial state. This effectively populates
+ // the initial state tree.
+ dispatch({ type: ActionTypes.INIT });
+
+ return _ref2 = {
+ dispatch: dispatch,
+ subscribe: subscribe,
+ getState: getState,
+ replaceReducer: replaceReducer
+ }, _ref2[_symbolObservable2["default"]] = observable, _ref2;
+ }
+
+/***/ },
+/* 5 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var getPrototype = __webpack_require__(6),
+ isObjectLike = __webpack_require__(8);
+
+ /** `Object#toString` result references. */
+ var objectTag = '[object Object]';
+
+ /** Used for built-in method references. */
+ var funcProto = Function.prototype,
+ objectProto = Object.prototype;
+
+ /** Used to resolve the decompiled source of functions. */
+ var funcToString = funcProto.toString;
+
+ /** Used to check objects for own properties. */
+ var hasOwnProperty = objectProto.hasOwnProperty;
+
+ /** Used to infer the `Object` constructor. */
+ var objectCtorString = funcToString.call(Object);
+
+ /**
+ * Used to resolve the
+ * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring)
+ * of values.
+ */
+ var objectToString = objectProto.toString;
+
+ /**
+ * Checks if `value` is a plain object, that is, an object created by the
+ * `Object` constructor or one with a `[[Prototype]]` of `null`.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.8.0
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is a plain object, else `false`.
+ * @example
+ *
+ * function Foo() {
+ * this.a = 1;
+ * }
+ *
+ * _.isPlainObject(new Foo);
+ * // => false
+ *
+ * _.isPlainObject([1, 2, 3]);
+ * // => false
+ *
+ * _.isPlainObject({ 'x': 0, 'y': 0 });
+ * // => true
+ *
+ * _.isPlainObject(Object.create(null));
+ * // => true
+ */
+ function isPlainObject(value) {
+ if (!isObjectLike(value) || objectToString.call(value) != objectTag) {
+ return false;
+ }
+ var proto = getPrototype(value);
+ if (proto === null) {
+ return true;
+ }
+ var Ctor = hasOwnProperty.call(proto, 'constructor') && proto.constructor;
+ return (typeof Ctor == 'function' &&
+ Ctor instanceof Ctor && funcToString.call(Ctor) == objectCtorString);
+ }
+
+ module.exports = isPlainObject;
+
+
+/***/ },
+/* 6 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var overArg = __webpack_require__(7);
+
+ /** Built-in value references. */
+ var getPrototype = overArg(Object.getPrototypeOf, Object);
+
+ module.exports = getPrototype;
+
+
+/***/ },
+/* 7 */
+/***/ function(module, exports) {
+
+ /**
+ * Creates a unary function that invokes `func` with its argument transformed.
+ *
+ * @private
+ * @param {Function} func The function to wrap.
+ * @param {Function} transform The argument transform.
+ * @returns {Function} Returns the new function.
+ */
+ function overArg(func, transform) {
+ return function(arg) {
+ return func(transform(arg));
+ };
+ }
+
+ module.exports = overArg;
+
+
+/***/ },
+/* 8 */
+/***/ function(module, exports) {
+
+ /**
+ * Checks if `value` is object-like. A value is object-like if it's not `null`
+ * and has a `typeof` result of "object".
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is object-like, else `false`.
+ * @example
+ *
+ * _.isObjectLike({});
+ * // => true
+ *
+ * _.isObjectLike([1, 2, 3]);
+ * // => true
+ *
+ * _.isObjectLike(_.noop);
+ * // => false
+ *
+ * _.isObjectLike(null);
+ * // => false
+ */
+ function isObjectLike(value) {
+ return value != null && typeof value == 'object';
+ }
+
+ module.exports = isObjectLike;
+
+
+/***/ },
+/* 9 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /* WEBPACK VAR INJECTION */(function(global) {/* global window */
+ 'use strict';
+
+ module.exports = __webpack_require__(10)(global || window || this);
+
+ /* WEBPACK VAR INJECTION */}.call(exports, (function() { return this; }())))
+
+/***/ },
+/* 10 */
+/***/ function(module, exports) {
+
+ 'use strict';
+
+ module.exports = function symbolObservablePonyfill(root) {
+ var result;
+ var Symbol = root.Symbol;
+
+ if (typeof Symbol === 'function') {
+ if (Symbol.observable) {
+ result = Symbol.observable;
+ } else {
+ result = Symbol('observable');
+ Symbol.observable = result;
+ }
+ } else {
+ result = '@@observable';
+ }
+
+ return result;
+ };
+
+
+/***/ },
+/* 11 */
+/***/ function(module, exports, __webpack_require__) {
+
+ 'use strict';
+
+ exports.__esModule = true;
+ exports["default"] = combineReducers;
+
+ var _createStore = __webpack_require__(4);
+
+ var _isPlainObject = __webpack_require__(5);
+
+ var _isPlainObject2 = _interopRequireDefault(_isPlainObject);
+
+ var _warning = __webpack_require__(12);
+
+ var _warning2 = _interopRequireDefault(_warning);
+
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
+
+ function getUndefinedStateErrorMessage(key, action) {
+ var actionType = action && action.type;
+ var actionName = actionType && '"' + actionType.toString() + '"' || 'an action';
+
+ return 'Given action ' + actionName + ', reducer "' + key + '" returned undefined. ' + 'To ignore an action, you must explicitly return the previous state.';
+ }
+
+ function getUnexpectedStateShapeWarningMessage(inputState, reducers, action) {
+ var reducerKeys = Object.keys(reducers);
+ var argumentName = action && action.type === _createStore.ActionTypes.INIT ? 'initialState argument passed to createStore' : 'previous state received by the reducer';
+
+ if (reducerKeys.length === 0) {
+ return 'Store does not have a valid reducer. Make sure the argument passed ' + 'to combineReducers is an object whose values are reducers.';
+ }
+
+ if (!(0, _isPlainObject2["default"])(inputState)) {
+ return 'The ' + argumentName + ' has unexpected type of "' + {}.toString.call(inputState).match(/\s([a-z|A-Z]+)/)[1] + '". Expected argument to be an object with the following ' + ('keys: "' + reducerKeys.join('", "') + '"');
+ }
+
+ var unexpectedKeys = Object.keys(inputState).filter(function (key) {
+ return !reducers.hasOwnProperty(key);
+ });
+
+ if (unexpectedKeys.length > 0) {
+ return 'Unexpected ' + (unexpectedKeys.length > 1 ? 'keys' : 'key') + ' ' + ('"' + unexpectedKeys.join('", "') + '" found in ' + argumentName + '. ') + 'Expected to find one of the known reducer keys instead: ' + ('"' + reducerKeys.join('", "') + '". Unexpected keys will be ignored.');
+ }
+ }
+
+ function assertReducerSanity(reducers) {
+ Object.keys(reducers).forEach(function (key) {
+ var reducer = reducers[key];
+ var initialState = reducer(undefined, { type: _createStore.ActionTypes.INIT });
+
+ if (typeof initialState === 'undefined') {
+ throw new Error('Reducer "' + key + '" returned undefined during initialization. ' + 'If the state passed to the reducer is undefined, you must ' + 'explicitly return the initial state. The initial state may ' + 'not be undefined.');
+ }
+
+ var type = '@@redux/PROBE_UNKNOWN_ACTION_' + Math.random().toString(36).substring(7).split('').join('.');
+ if (typeof reducer(undefined, { type: type }) === 'undefined') {
+ throw new Error('Reducer "' + key + '" returned undefined when probed with a random type. ' + ('Don\'t try to handle ' + _createStore.ActionTypes.INIT + ' or other actions in "redux/*" ') + 'namespace. They are considered private. Instead, you must return the ' + 'current state for any unknown actions, unless it is undefined, ' + 'in which case you must return the initial state, regardless of the ' + 'action type. The initial state may not be undefined.');
+ }
+ });
+ }
+
+ /**
+ * Turns an object whose values are different reducer functions, into a single
+ * reducer function. It will call every child reducer, and gather their results
+ * into a single state object, whose keys correspond to the keys of the passed
+ * reducer functions.
+ *
+ * @param {Object} reducers An object whose values correspond to different
+ * reducer functions that need to be combined into one. One handy way to obtain
+ * it is to use ES6 `import * as reducers` syntax. The reducers may never return
+ * undefined for any action. Instead, they should return their initial state
+ * if the state passed to them was undefined, and the current state for any
+ * unrecognized action.
+ *
+ * @returns {Function} A reducer function that invokes every reducer inside the
+ * passed object, and builds a state object with the same shape.
+ */
+ function combineReducers(reducers) {
+ var reducerKeys = Object.keys(reducers);
+ var finalReducers = {};
+ for (var i = 0; i < reducerKeys.length; i++) {
+ var key = reducerKeys[i];
+ if (typeof reducers[key] === 'function') {
+ finalReducers[key] = reducers[key];
+ }
+ }
+ var finalReducerKeys = Object.keys(finalReducers);
+
+ var sanityError;
+ try {
+ assertReducerSanity(finalReducers);
+ } catch (e) {
+ sanityError = e;
+ }
+
+ return function combination() {
+ var state = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0];
+ var action = arguments[1];
+
+ if (sanityError) {
+ throw sanityError;
+ }
+
+ if (false) {
+ var warningMessage = getUnexpectedStateShapeWarningMessage(state, finalReducers, action);
+ if (warningMessage) {
+ (0, _warning2["default"])(warningMessage);
+ }
+ }
+
+ var hasChanged = false;
+ var nextState = {};
+ for (var i = 0; i < finalReducerKeys.length; i++) {
+ var key = finalReducerKeys[i];
+ var reducer = finalReducers[key];
+ var previousStateForKey = state[key];
+ var nextStateForKey = reducer(previousStateForKey, action);
+ if (typeof nextStateForKey === 'undefined') {
+ var errorMessage = getUndefinedStateErrorMessage(key, action);
+ throw new Error(errorMessage);
+ }
+ nextState[key] = nextStateForKey;
+ hasChanged = hasChanged || nextStateForKey !== previousStateForKey;
+ }
+ return hasChanged ? nextState : state;
+ };
+ }
+
+/***/ },
+/* 12 */
+/***/ function(module, exports) {
+
+ 'use strict';
+
+ exports.__esModule = true;
+ exports["default"] = warning;
+ /**
+ * Prints a warning in the console if it exists.
+ *
+ * @param {String} message The warning message.
+ * @returns {void}
+ */
+ function warning(message) {
+ /* eslint-disable no-console */
+ if (typeof console !== 'undefined' && typeof console.error === 'function') {
+ console.error(message);
+ }
+ /* eslint-enable no-console */
+ try {
+ // This error was thrown as a convenience so that if you enable
+ // "break on all exceptions" in your console,
+ // it would pause the execution at this line.
+ throw new Error(message);
+ /* eslint-disable no-empty */
+ } catch (e) {}
+ /* eslint-enable no-empty */
+ }
+
+/***/ },
+/* 13 */
+/***/ function(module, exports) {
+
+ 'use strict';
+
+ exports.__esModule = true;
+ exports["default"] = bindActionCreators;
+ function bindActionCreator(actionCreator, dispatch) {
+ return function () {
+ return dispatch(actionCreator.apply(undefined, arguments));
+ };
+ }
+
+ /**
+ * Turns an object whose values are action creators, into an object with the
+ * same keys, but with every function wrapped into a `dispatch` call so they
+ * may be invoked directly. This is just a convenience method, as you can call
+ * `store.dispatch(MyActionCreators.doSomething())` yourself just fine.
+ *
+ * For convenience, you can also pass a single function as the first argument,
+ * and get a function in return.
+ *
+ * @param {Function|Object} actionCreators An object whose values are action
+ * creator functions. One handy way to obtain it is to use ES6 `import * as`
+ * syntax. You may also pass a single function.
+ *
+ * @param {Function} dispatch The `dispatch` function available on your Redux
+ * store.
+ *
+ * @returns {Function|Object} The object mimicking the original object, but with
+ * every action creator wrapped into the `dispatch` call. If you passed a
+ * function as `actionCreators`, the return value will also be a single
+ * function.
+ */
+ function bindActionCreators(actionCreators, dispatch) {
+ if (typeof actionCreators === 'function') {
+ return bindActionCreator(actionCreators, dispatch);
+ }
+
+ if (typeof actionCreators !== 'object' || actionCreators === null) {
+ throw new Error('bindActionCreators expected an object or a function, instead received ' + (actionCreators === null ? 'null' : typeof actionCreators) + '. ' + 'Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?');
+ }
+
+ var keys = Object.keys(actionCreators);
+ var boundActionCreators = {};
+ for (var i = 0; i < keys.length; i++) {
+ var key = keys[i];
+ var actionCreator = actionCreators[key];
+ if (typeof actionCreator === 'function') {
+ boundActionCreators[key] = bindActionCreator(actionCreator, dispatch);
+ }
+ }
+ return boundActionCreators;
+ }
+
+/***/ },
+/* 14 */
+/***/ function(module, exports, __webpack_require__) {
+
+ 'use strict';
+
+ exports.__esModule = true;
+
+ var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
+
+ exports["default"] = applyMiddleware;
+
+ var _compose = __webpack_require__(15);
+
+ var _compose2 = _interopRequireDefault(_compose);
+
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
+
+ /**
+ * Creates a store enhancer that applies middleware to the dispatch method
+ * of the Redux store. This is handy for a variety of tasks, such as expressing
+ * asynchronous actions in a concise manner, or logging every action payload.
+ *
+ * See `redux-thunk` package as an example of the Redux middleware.
+ *
+ * Because middleware is potentially asynchronous, this should be the first
+ * store enhancer in the composition chain.
+ *
+ * Note that each middleware will be given the `dispatch` and `getState` functions
+ * as named arguments.
+ *
+ * @param {...Function} middlewares The middleware chain to be applied.
+ * @returns {Function} A store enhancer applying the middleware.
+ */
+ function applyMiddleware() {
+ for (var _len = arguments.length, middlewares = Array(_len), _key = 0; _key < _len; _key++) {
+ middlewares[_key] = arguments[_key];
+ }
+
+ return function (createStore) {
+ return function (reducer, initialState, enhancer) {
+ var store = createStore(reducer, initialState, enhancer);
+ var _dispatch = store.dispatch;
+ var chain = [];
+
+ var middlewareAPI = {
+ getState: store.getState,
+ dispatch: function dispatch(action) {
+ return _dispatch(action);
+ }
+ };
+ chain = middlewares.map(function (middleware) {
+ return middleware(middlewareAPI);
+ });
+ _dispatch = _compose2["default"].apply(undefined, chain)(store.dispatch);
+
+ return _extends({}, store, {
+ dispatch: _dispatch
+ });
+ };
+ };
+ }
+
+/***/ },
+/* 15 */
+/***/ function(module, exports) {
+
+ "use strict";
+
+ exports.__esModule = true;
+ exports["default"] = compose;
+ /**
+ * Composes single-argument functions from right to left. The rightmost
+ * function can take multiple arguments as it provides the signature for
+ * the resulting composite function.
+ *
+ * @param {...Function} funcs The functions to compose.
+ * @returns {Function} A function obtained by composing the argument functions
+ * from right to left. For example, compose(f, g, h) is identical to doing
+ * (...args) => f(g(h(...args))).
+ */
+
+ function compose() {
+ for (var _len = arguments.length, funcs = Array(_len), _key = 0; _key < _len; _key++) {
+ funcs[_key] = arguments[_key];
+ }
+
+ if (funcs.length === 0) {
+ return function (arg) {
+ return arg;
+ };
+ } else {
+ var _ret = function () {
+ var last = funcs[funcs.length - 1];
+ var rest = funcs.slice(0, -1);
+ return {
+ v: function v() {
+ return rest.reduceRight(function (composed, f) {
+ return f(composed);
+ }, last.apply(undefined, arguments));
+ }
+ };
+ }();
+
+ if (typeof _ret === "object") return _ret.v;
+ }
+ }
+
+/***/ },
+/* 16 */
+/***/ function(module, exports, __webpack_require__) {
+
+ 'use strict';
+
+ module.exports = __webpack_require__(17);
+
+
+/***/ },
+/* 17 */
+/***/ function(module, exports) {
+
+ module.exports = devtoolsRequire("devtools/client/shared/vendor/react-dom");
+
+/***/ },
+/* 18 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /* global window, document, DebuggerConfig */
+
+ var _require = __webpack_require__(3);
+
+ var bindActionCreators = _require.bindActionCreators;
+ var combineReducers = _require.combineReducers;
+
+ var _require2 = __webpack_require__(19);
+
+ var Provider = _require2.Provider;
+
+ var _require3 = __webpack_require__(28);
+
+ var DevToolsUtils = _require3.DevToolsUtils;
+ var AppConstants = _require3.AppConstants;
+
+ var _require4 = __webpack_require__(88);
+
+ var injectGlobals = _require4.injectGlobals;
+ var debugGlobal = _require4.debugGlobal;
+
+ var _require5 = __webpack_require__(89);
+
+ var setConfig = _require5.setConfig;
+ var isEnabled = _require5.isEnabled;
+ var getValue = _require5.getValue;
+ var isDevelopment = _require5.isDevelopment;
+
+
+ setConfig(({"environment":"firefox-panel","baseWorkerURL":"resource://devtools/client/debugger/new/","logging":false,"clientLogging":false,"features":{"tabs":true,"sourceMaps":true,"prettyPrint":true}}));
+
+ // Set various flags before requiring app code.
+ if (isEnabled("logging.client")) {
+ DevToolsUtils.dumpn.wantLogging = true;
+ }
+
+ var client = __webpack_require__(141);
+ var getClient = client.getClient;
+ var connectClients = client.connectClients;
+ var startDebugging = client.startDebugging;
+
+
+ var Root = __webpack_require__(210);
+
+ // Using this static variable allows webpack to know at compile-time
+ // to avoid this require and not include it at all in the output.
+ if (false) {
+ var theme = getValue("theme");
+ switch (theme) {
+ case "dark":
+ require("./lib/themes/dark-theme.css");break;
+ case "light":
+ require("./lib/themes/light-theme.css");break;
+ case "firebug":
+ require("./lib/themes/firebug-theme.css");break;
+ }
+ document.body.parentNode.classList.add(`theme-${ theme }`);
+
+ window.L10N = require("./utils/L10N");
+ }
+
+ function initApp() {
+ var configureStore = __webpack_require__(216);
+ var reducers = __webpack_require__(226);
+ var LandingPage = __webpack_require__(231);
+
+ var createStore = configureStore({
+ log: getValue("logging.actions"),
+ makeThunkArgs: (args, state) => {
+ return Object.assign({}, args, { client: getClient(state) });
+ }
+ });
+
+ var store = createStore(combineReducers(reducers));
+ var actions = bindActionCreators(__webpack_require__(236), store.dispatch);
+
+ if (isDevelopment()) {
+ AppConstants.DEBUG_JS_MODULES = true;
+ injectGlobals({ store });
+ }
+
+ return { store, actions, LandingPage };
+ }
+
+ function renderRoot(_React, _ReactDOM, component, _store) {
+ var mount = document.querySelector("#mount");
+
+ // bail in test environments that do not have a mount
+ if (!mount) {
+ return;
+ }
+
+ _ReactDOM.render(_React.createElement(Provider, { store: _store }, Root(component)), mount);
+ }
+
+ function getTargetFromQuery() {
+ var href = window.location.href;
+ var nodeMatch = href.match(/ws=([^&#]*)/);
+ var firefoxMatch = href.match(/firefox-tab=([^&#]*)/);
+ var chromeMatch = href.match(/chrome-tab=([^&#]*)/);
+
+ if (nodeMatch) {
+ return { type: "node", param: nodeMatch[1] };
+ } else if (firefoxMatch) {
+ return { type: "firefox", param: firefoxMatch[1] };
+ } else if (chromeMatch) {
+ return { type: "chrome", param: chromeMatch[1] };
+ }
+
+ return null;
+ }
+
+ function bootstrap(React, ReactDOM, App, appActions, appStore) {
+ var connTarget = getTargetFromQuery();
+ if (connTarget) {
+ startDebugging(connTarget, appActions).then(tabs => {
+ renderRoot(React, ReactDOM, App, appStore);
+ });
+ } else {
+ (function () {
+ var _initApp = initApp();
+
+ var store = _initApp.store;
+ var actions = _initApp.actions;
+ var LandingPage = _initApp.LandingPage;
+
+ renderRoot(React, ReactDOM, LandingPage, store);
+ connectClients(tabs => actions.newTabs(tabs));
+ })();
+ }
+ }
+
+ module.exports = {
+ bootstrap,
+ renderRoot,
+ debugGlobal,
+ client
+ };
+
+/***/ },
+/* 19 */
+/***/ function(module, exports, __webpack_require__) {
+
+ 'use strict';
+
+ exports.__esModule = true;
+ exports.connect = exports.Provider = undefined;
+
+ var _Provider = __webpack_require__(20);
+
+ var _Provider2 = _interopRequireDefault(_Provider);
+
+ var _connect = __webpack_require__(23);
+
+ var _connect2 = _interopRequireDefault(_connect);
+
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
+
+ exports.Provider = _Provider2["default"];
+ exports.connect = _connect2["default"];
+
+/***/ },
+/* 20 */
+/***/ function(module, exports, __webpack_require__) {
+
+ 'use strict';
+
+ exports.__esModule = true;
+ exports["default"] = undefined;
+
+ var _react = __webpack_require__(2);
+
+ var _storeShape = __webpack_require__(21);
+
+ var _storeShape2 = _interopRequireDefault(_storeShape);
+
+ var _warning = __webpack_require__(22);
+
+ var _warning2 = _interopRequireDefault(_warning);
+
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
+
+ function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+ function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
+
+ function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
+
+ var didWarnAboutReceivingStore = false;
+ function warnAboutReceivingStore() {
+ if (didWarnAboutReceivingStore) {
+ return;
+ }
+ didWarnAboutReceivingStore = true;
+
+ (0, _warning2["default"])('<Provider> does not support changing `store` on the fly. ' + 'It is most likely that you see this error because you updated to ' + 'Redux 2.x and React Redux 2.x which no longer hot reload reducers ' + 'automatically. See https://github.com/reactjs/react-redux/releases/' + 'tag/v2.0.0 for the migration instructions.');
+ }
+
+ var Provider = function (_Component) {
+ _inherits(Provider, _Component);
+
+ Provider.prototype.getChildContext = function getChildContext() {
+ return { store: this.store };
+ };
+
+ function Provider(props, context) {
+ _classCallCheck(this, Provider);
+
+ var _this = _possibleConstructorReturn(this, _Component.call(this, props, context));
+
+ _this.store = props.store;
+ return _this;
+ }
+
+ Provider.prototype.render = function render() {
+ var children = this.props.children;
+
+ return _react.Children.only(children);
+ };
+
+ return Provider;
+ }(_react.Component);
+
+ exports["default"] = Provider;
+
+ if (false) {
+ Provider.prototype.componentWillReceiveProps = function (nextProps) {
+ var store = this.store;
+ var nextStore = nextProps.store;
+
+ if (store !== nextStore) {
+ warnAboutReceivingStore();
+ }
+ };
+ }
+
+ Provider.propTypes = {
+ store: _storeShape2["default"].isRequired,
+ children: _react.PropTypes.element.isRequired
+ };
+ Provider.childContextTypes = {
+ store: _storeShape2["default"].isRequired
+ };
+
+/***/ },
+/* 21 */
+/***/ function(module, exports, __webpack_require__) {
+
+ 'use strict';
+
+ exports.__esModule = true;
+
+ var _react = __webpack_require__(2);
+
+ exports["default"] = _react.PropTypes.shape({
+ subscribe: _react.PropTypes.func.isRequired,
+ dispatch: _react.PropTypes.func.isRequired,
+ getState: _react.PropTypes.func.isRequired
+ });
+
+/***/ },
+/* 22 */
+/***/ function(module, exports) {
+
+ 'use strict';
+
+ exports.__esModule = true;
+ exports["default"] = warning;
+ /**
+ * Prints a warning in the console if it exists.
+ *
+ * @param {String} message The warning message.
+ * @returns {void}
+ */
+ function warning(message) {
+ /* eslint-disable no-console */
+ if (typeof console !== 'undefined' && typeof console.error === 'function') {
+ console.error(message);
+ }
+ /* eslint-enable no-console */
+ try {
+ // This error was thrown as a convenience so that you can use this stack
+ // to find the callsite that caused this warning to fire.
+ throw new Error(message);
+ /* eslint-disable no-empty */
+ } catch (e) {}
+ /* eslint-enable no-empty */
+ }
+
+/***/ },
+/* 23 */
+/***/ function(module, exports, __webpack_require__) {
+
+ 'use strict';
+
+ var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
+
+ exports.__esModule = true;
+ exports["default"] = connect;
+
+ var _react = __webpack_require__(2);
+
+ var _storeShape = __webpack_require__(21);
+
+ var _storeShape2 = _interopRequireDefault(_storeShape);
+
+ var _shallowEqual = __webpack_require__(24);
+
+ var _shallowEqual2 = _interopRequireDefault(_shallowEqual);
+
+ var _wrapActionCreators = __webpack_require__(25);
+
+ var _wrapActionCreators2 = _interopRequireDefault(_wrapActionCreators);
+
+ var _warning = __webpack_require__(22);
+
+ var _warning2 = _interopRequireDefault(_warning);
+
+ var _isPlainObject = __webpack_require__(5);
+
+ var _isPlainObject2 = _interopRequireDefault(_isPlainObject);
+
+ var _hoistNonReactStatics = __webpack_require__(26);
+
+ var _hoistNonReactStatics2 = _interopRequireDefault(_hoistNonReactStatics);
+
+ var _invariant = __webpack_require__(27);
+
+ var _invariant2 = _interopRequireDefault(_invariant);
+
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
+
+ function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+ function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
+
+ function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
+
+ var defaultMapStateToProps = function defaultMapStateToProps(state) {
+ return {};
+ }; // eslint-disable-line no-unused-vars
+ var defaultMapDispatchToProps = function defaultMapDispatchToProps(dispatch) {
+ return { dispatch: dispatch };
+ };
+ var defaultMergeProps = function defaultMergeProps(stateProps, dispatchProps, parentProps) {
+ return _extends({}, parentProps, stateProps, dispatchProps);
+ };
+
+ function getDisplayName(WrappedComponent) {
+ return WrappedComponent.displayName || WrappedComponent.name || 'Component';
+ }
+
+ var errorObject = { value: null };
+ function tryCatch(fn, ctx) {
+ try {
+ return fn.apply(ctx);
+ } catch (e) {
+ errorObject.value = e;
+ return errorObject;
+ }
+ }
+
+ // Helps track hot reloading.
+ var nextVersion = 0;
+
+ function connect(mapStateToProps, mapDispatchToProps, mergeProps) {
+ var options = arguments.length <= 3 || arguments[3] === undefined ? {} : arguments[3];
+
+ var shouldSubscribe = Boolean(mapStateToProps);
+ var mapState = mapStateToProps || defaultMapStateToProps;
+
+ var mapDispatch = undefined;
+ if (typeof mapDispatchToProps === 'function') {
+ mapDispatch = mapDispatchToProps;
+ } else if (!mapDispatchToProps) {
+ mapDispatch = defaultMapDispatchToProps;
+ } else {
+ mapDispatch = (0, _wrapActionCreators2["default"])(mapDispatchToProps);
+ }
+
+ var finalMergeProps = mergeProps || defaultMergeProps;
+ var _options$pure = options.pure;
+ var pure = _options$pure === undefined ? true : _options$pure;
+ var _options$withRef = options.withRef;
+ var withRef = _options$withRef === undefined ? false : _options$withRef;
+
+ var checkMergedEquals = pure && finalMergeProps !== defaultMergeProps;
+
+ // Helps track hot reloading.
+ var version = nextVersion++;
+
+ return function wrapWithConnect(WrappedComponent) {
+ var connectDisplayName = 'Connect(' + getDisplayName(WrappedComponent) + ')';
+
+ function checkStateShape(props, methodName) {
+ if (!(0, _isPlainObject2["default"])(props)) {
+ (0, _warning2["default"])(methodName + '() in ' + connectDisplayName + ' must return a plain object. ' + ('Instead received ' + props + '.'));
+ }
+ }
+
+ function computeMergedProps(stateProps, dispatchProps, parentProps) {
+ var mergedProps = finalMergeProps(stateProps, dispatchProps, parentProps);
+ if (false) {
+ checkStateShape(mergedProps, 'mergeProps');
+ }
+ return mergedProps;
+ }
+
+ var Connect = function (_Component) {
+ _inherits(Connect, _Component);
+
+ Connect.prototype.shouldComponentUpdate = function shouldComponentUpdate() {
+ return !pure || this.haveOwnPropsChanged || this.hasStoreStateChanged;
+ };
+
+ function Connect(props, context) {
+ _classCallCheck(this, Connect);
+
+ var _this = _possibleConstructorReturn(this, _Component.call(this, props, context));
+
+ _this.version = version;
+ _this.store = props.store || context.store;
+
+ (0, _invariant2["default"])(_this.store, 'Could not find "store" in either the context or ' + ('props of "' + connectDisplayName + '". ') + 'Either wrap the root component in a <Provider>, ' + ('or explicitly pass "store" as a prop to "' + connectDisplayName + '".'));
+
+ var storeState = _this.store.getState();
+ _this.state = { storeState: storeState };
+ _this.clearCache();
+ return _this;
+ }
+
+ Connect.prototype.computeStateProps = function computeStateProps(store, props) {
+ if (!this.finalMapStateToProps) {
+ return this.configureFinalMapState(store, props);
+ }
+
+ var state = store.getState();
+ var stateProps = this.doStatePropsDependOnOwnProps ? this.finalMapStateToProps(state, props) : this.finalMapStateToProps(state);
+
+ if (false) {
+ checkStateShape(stateProps, 'mapStateToProps');
+ }
+ return stateProps;
+ };
+
+ Connect.prototype.configureFinalMapState = function configureFinalMapState(store, props) {
+ var mappedState = mapState(store.getState(), props);
+ var isFactory = typeof mappedState === 'function';
+
+ this.finalMapStateToProps = isFactory ? mappedState : mapState;
+ this.doStatePropsDependOnOwnProps = this.finalMapStateToProps.length !== 1;
+
+ if (isFactory) {
+ return this.computeStateProps(store, props);
+ }
+
+ if (false) {
+ checkStateShape(mappedState, 'mapStateToProps');
+ }
+ return mappedState;
+ };
+
+ Connect.prototype.computeDispatchProps = function computeDispatchProps(store, props) {
+ if (!this.finalMapDispatchToProps) {
+ return this.configureFinalMapDispatch(store, props);
+ }
+
+ var dispatch = store.dispatch;
+
+ var dispatchProps = this.doDispatchPropsDependOnOwnProps ? this.finalMapDispatchToProps(dispatch, props) : this.finalMapDispatchToProps(dispatch);
+
+ if (false) {
+ checkStateShape(dispatchProps, 'mapDispatchToProps');
+ }
+ return dispatchProps;
+ };
+
+ Connect.prototype.configureFinalMapDispatch = function configureFinalMapDispatch(store, props) {
+ var mappedDispatch = mapDispatch(store.dispatch, props);
+ var isFactory = typeof mappedDispatch === 'function';
+
+ this.finalMapDispatchToProps = isFactory ? mappedDispatch : mapDispatch;
+ this.doDispatchPropsDependOnOwnProps = this.finalMapDispatchToProps.length !== 1;
+
+ if (isFactory) {
+ return this.computeDispatchProps(store, props);
+ }
+
+ if (false) {
+ checkStateShape(mappedDispatch, 'mapDispatchToProps');
+ }
+ return mappedDispatch;
+ };
+
+ Connect.prototype.updateStatePropsIfNeeded = function updateStatePropsIfNeeded() {
+ var nextStateProps = this.computeStateProps(this.store, this.props);
+ if (this.stateProps && (0, _shallowEqual2["default"])(nextStateProps, this.stateProps)) {
+ return false;
+ }
+
+ this.stateProps = nextStateProps;
+ return true;
+ };
+
+ Connect.prototype.updateDispatchPropsIfNeeded = function updateDispatchPropsIfNeeded() {
+ var nextDispatchProps = this.computeDispatchProps(this.store, this.props);
+ if (this.dispatchProps && (0, _shallowEqual2["default"])(nextDispatchProps, this.dispatchProps)) {
+ return false;
+ }
+
+ this.dispatchProps = nextDispatchProps;
+ return true;
+ };
+
+ Connect.prototype.updateMergedPropsIfNeeded = function updateMergedPropsIfNeeded() {
+ var nextMergedProps = computeMergedProps(this.stateProps, this.dispatchProps, this.props);
+ if (this.mergedProps && checkMergedEquals && (0, _shallowEqual2["default"])(nextMergedProps, this.mergedProps)) {
+ return false;
+ }
+
+ this.mergedProps = nextMergedProps;
+ return true;
+ };
+
+ Connect.prototype.isSubscribed = function isSubscribed() {
+ return typeof this.unsubscribe === 'function';
+ };
+
+ Connect.prototype.trySubscribe = function trySubscribe() {
+ if (shouldSubscribe && !this.unsubscribe) {
+ this.unsubscribe = this.store.subscribe(this.handleChange.bind(this));
+ this.handleChange();
+ }
+ };
+
+ Connect.prototype.tryUnsubscribe = function tryUnsubscribe() {
+ if (this.unsubscribe) {
+ this.unsubscribe();
+ this.unsubscribe = null;
+ }
+ };
+
+ Connect.prototype.componentDidMount = function componentDidMount() {
+ this.trySubscribe();
+ };
+
+ Connect.prototype.componentWillReceiveProps = function componentWillReceiveProps(nextProps) {
+ if (!pure || !(0, _shallowEqual2["default"])(nextProps, this.props)) {
+ this.haveOwnPropsChanged = true;
+ }
+ };
+
+ Connect.prototype.componentWillUnmount = function componentWillUnmount() {
+ this.tryUnsubscribe();
+ this.clearCache();
+ };
+
+ Connect.prototype.clearCache = function clearCache() {
+ this.dispatchProps = null;
+ this.stateProps = null;
+ this.mergedProps = null;
+ this.haveOwnPropsChanged = true;
+ this.hasStoreStateChanged = true;
+ this.haveStatePropsBeenPrecalculated = false;
+ this.statePropsPrecalculationError = null;
+ this.renderedElement = null;
+ this.finalMapDispatchToProps = null;
+ this.finalMapStateToProps = null;
+ };
+
+ Connect.prototype.handleChange = function handleChange() {
+ if (!this.unsubscribe) {
+ return;
+ }
+
+ var storeState = this.store.getState();
+ var prevStoreState = this.state.storeState;
+ if (pure && prevStoreState === storeState) {
+ return;
+ }
+
+ if (pure && !this.doStatePropsDependOnOwnProps) {
+ var haveStatePropsChanged = tryCatch(this.updateStatePropsIfNeeded, this);
+ if (!haveStatePropsChanged) {
+ return;
+ }
+ if (haveStatePropsChanged === errorObject) {
+ this.statePropsPrecalculationError = errorObject.value;
+ }
+ this.haveStatePropsBeenPrecalculated = true;
+ }
+
+ this.hasStoreStateChanged = true;
+ this.setState({ storeState: storeState });
+ };
+
+ Connect.prototype.getWrappedInstance = function getWrappedInstance() {
+ (0, _invariant2["default"])(withRef, 'To access the wrapped instance, you need to specify ' + '{ withRef: true } as the fourth argument of the connect() call.');
+
+ return this.refs.wrappedInstance;
+ };
+
+ Connect.prototype.render = function render() {
+ var haveOwnPropsChanged = this.haveOwnPropsChanged;
+ var hasStoreStateChanged = this.hasStoreStateChanged;
+ var haveStatePropsBeenPrecalculated = this.haveStatePropsBeenPrecalculated;
+ var statePropsPrecalculationError = this.statePropsPrecalculationError;
+ var renderedElement = this.renderedElement;
+
+ this.haveOwnPropsChanged = false;
+ this.hasStoreStateChanged = false;
+ this.haveStatePropsBeenPrecalculated = false;
+ this.statePropsPrecalculationError = null;
+
+ if (statePropsPrecalculationError) {
+ throw statePropsPrecalculationError;
+ }
+
+ var shouldUpdateStateProps = true;
+ var shouldUpdateDispatchProps = true;
+ if (pure && renderedElement) {
+ shouldUpdateStateProps = hasStoreStateChanged || haveOwnPropsChanged && this.doStatePropsDependOnOwnProps;
+ shouldUpdateDispatchProps = haveOwnPropsChanged && this.doDispatchPropsDependOnOwnProps;
+ }
+
+ var haveStatePropsChanged = false;
+ var haveDispatchPropsChanged = false;
+ if (haveStatePropsBeenPrecalculated) {
+ haveStatePropsChanged = true;
+ } else if (shouldUpdateStateProps) {
+ haveStatePropsChanged = this.updateStatePropsIfNeeded();
+ }
+ if (shouldUpdateDispatchProps) {
+ haveDispatchPropsChanged = this.updateDispatchPropsIfNeeded();
+ }
+
+ var haveMergedPropsChanged = true;
+ if (haveStatePropsChanged || haveDispatchPropsChanged || haveOwnPropsChanged) {
+ haveMergedPropsChanged = this.updateMergedPropsIfNeeded();
+ } else {
+ haveMergedPropsChanged = false;
+ }
+
+ if (!haveMergedPropsChanged && renderedElement) {
+ return renderedElement;
+ }
+
+ if (withRef) {
+ this.renderedElement = (0, _react.createElement)(WrappedComponent, _extends({}, this.mergedProps, {
+ ref: 'wrappedInstance'
+ }));
+ } else {
+ this.renderedElement = (0, _react.createElement)(WrappedComponent, this.mergedProps);
+ }
+
+ return this.renderedElement;
+ };
+
+ return Connect;
+ }(_react.Component);
+
+ Connect.displayName = connectDisplayName;
+ Connect.WrappedComponent = WrappedComponent;
+ Connect.contextTypes = {
+ store: _storeShape2["default"]
+ };
+ Connect.propTypes = {
+ store: _storeShape2["default"]
+ };
+
+ if (false) {
+ Connect.prototype.componentWillUpdate = function componentWillUpdate() {
+ if (this.version === version) {
+ return;
+ }
+
+ // We are hot reloading!
+ this.version = version;
+ this.trySubscribe();
+ this.clearCache();
+ };
+ }
+
+ return (0, _hoistNonReactStatics2["default"])(Connect, WrappedComponent);
+ };
+ }
+
+/***/ },
+/* 24 */
+/***/ function(module, exports) {
+
+ "use strict";
+
+ exports.__esModule = true;
+ exports["default"] = shallowEqual;
+ function shallowEqual(objA, objB) {
+ if (objA === objB) {
+ return true;
+ }
+
+ var keysA = Object.keys(objA);
+ var keysB = Object.keys(objB);
+
+ if (keysA.length !== keysB.length) {
+ return false;
+ }
+
+ // Test for A's keys different from B.
+ var hasOwn = Object.prototype.hasOwnProperty;
+ for (var i = 0; i < keysA.length; i++) {
+ if (!hasOwn.call(objB, keysA[i]) || objA[keysA[i]] !== objB[keysA[i]]) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+/***/ },
+/* 25 */
+/***/ function(module, exports, __webpack_require__) {
+
+ 'use strict';
+
+ exports.__esModule = true;
+ exports["default"] = wrapActionCreators;
+
+ var _redux = __webpack_require__(3);
+
+ function wrapActionCreators(actionCreators) {
+ return function (dispatch) {
+ return (0, _redux.bindActionCreators)(actionCreators, dispatch);
+ };
+ }
+
+/***/ },
+/* 26 */
+/***/ function(module, exports) {
+
+ /**
+ * Copyright 2015, Yahoo! Inc.
+ * Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
+ */
+ 'use strict';
+
+ var REACT_STATICS = {
+ childContextTypes: true,
+ contextTypes: true,
+ defaultProps: true,
+ displayName: true,
+ getDefaultProps: true,
+ mixins: true,
+ propTypes: true,
+ type: true
+ };
+
+ var KNOWN_STATICS = {
+ name: true,
+ length: true,
+ prototype: true,
+ caller: true,
+ arguments: true,
+ arity: true
+ };
+
+ var isGetOwnPropertySymbolsAvailable = typeof Object.getOwnPropertySymbols === 'function';
+
+ module.exports = function hoistNonReactStatics(targetComponent, sourceComponent, customStatics) {
+ if (typeof sourceComponent !== 'string') { // don't hoist over string (html) components
+ var keys = Object.getOwnPropertyNames(sourceComponent);
+
+ /* istanbul ignore else */
+ if (isGetOwnPropertySymbolsAvailable) {
+ keys = keys.concat(Object.getOwnPropertySymbols(sourceComponent));
+ }
+
+ for (var i = 0; i < keys.length; ++i) {
+ if (!REACT_STATICS[keys[i]] && !KNOWN_STATICS[keys[i]] && (!customStatics || !customStatics[keys[i]])) {
+ try {
+ targetComponent[keys[i]] = sourceComponent[keys[i]];
+ } catch (error) {
+
+ }
+ }
+ }
+ }
+
+ return targetComponent;
+ };
+
+
+/***/ },
+/* 27 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+ 'use strict';
+
+ /**
+ * Use invariant() to assert state which your program assumes to be true.
+ *
+ * Provide sprintf-style format (only %s is supported) and arguments
+ * to provide information about what broke and what you were
+ * expecting.
+ *
+ * The invariant message will be stripped in production, but the invariant
+ * will remain to ensure logic does not differ in production.
+ */
+
+ var invariant = function(condition, format, a, b, c, d, e, f) {
+ if (false) {
+ if (format === undefined) {
+ throw new Error('invariant requires an error message argument');
+ }
+ }
+
+ if (!condition) {
+ var error;
+ if (format === undefined) {
+ error = new Error(
+ 'Minified exception occurred; use the non-minified dev environment ' +
+ 'for the full error message and additional helpful warnings.'
+ );
+ } else {
+ var args = [a, b, c, d, e, f];
+ var argIndex = 0;
+ error = new Error(
+ format.replace(/%s/g, function() { return args[argIndex++]; })
+ );
+ error.name = 'Invariant Violation';
+ }
+
+ error.framesToPop = 1; // we don't care about invariant's own frame
+ throw error;
+ }
+ };
+
+ module.exports = invariant;
+
+
+/***/ },
+/* 28 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var _require = __webpack_require__(29);
+
+ var KeyShortcuts = _require.KeyShortcuts;
+
+ var _require2 = __webpack_require__(67);
+
+ var DebuggerTransport = _require2.DebuggerTransport;
+
+ var _require3 = __webpack_require__(79);
+
+ var DebuggerClient = _require3.DebuggerClient;
+
+ var PrefsHelper = __webpack_require__(83).PrefsHelper;
+
+ var _require4 = __webpack_require__(84);
+
+ var TargetFactory = _require4.TargetFactory;
+
+ var DevToolsUtils = __webpack_require__(68);
+ var AppConstants = __webpack_require__(70);
+ var EventEmitter = __webpack_require__(60);
+ var WebsocketTransport = __webpack_require__(85);
+ var Menu = __webpack_require__(86);
+ var MenuItem = __webpack_require__(87);
+
+ module.exports = {
+ KeyShortcuts,
+ PrefsHelper,
+ DebuggerClient,
+ DebuggerTransport,
+ TargetFactory,
+ DevToolsUtils,
+ AppConstants,
+ EventEmitter,
+ WebsocketTransport,
+ Menu,
+ MenuItem
+ };
+
+/***/ },
+/* 29 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }();
+
+ /* 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 _require = __webpack_require__(30);
+
+ var appinfo = _require.Services.appinfo;
+
+ var EventEmitter = __webpack_require__(60);
+ var isOSX = appinfo.OS === "Darwin";
+ "use strict";
+
+ // List of electron keys mapped to DOM API (DOM_VK_*) key code
+ var ElectronKeysMapping = {
+ "F1": "DOM_VK_F1",
+ "F2": "DOM_VK_F2",
+ "F3": "DOM_VK_F3",
+ "F4": "DOM_VK_F4",
+ "F5": "DOM_VK_F5",
+ "F6": "DOM_VK_F6",
+ "F7": "DOM_VK_F7",
+ "F8": "DOM_VK_F8",
+ "F9": "DOM_VK_F9",
+ "F10": "DOM_VK_F10",
+ "F11": "DOM_VK_F11",
+ "F12": "DOM_VK_F12",
+ "F13": "DOM_VK_F13",
+ "F14": "DOM_VK_F14",
+ "F15": "DOM_VK_F15",
+ "F16": "DOM_VK_F16",
+ "F17": "DOM_VK_F17",
+ "F18": "DOM_VK_F18",
+ "F19": "DOM_VK_F19",
+ "F20": "DOM_VK_F20",
+ "F21": "DOM_VK_F21",
+ "F22": "DOM_VK_F22",
+ "F23": "DOM_VK_F23",
+ "F24": "DOM_VK_F24",
+ "Space": "DOM_VK_SPACE",
+ "Backspace": "DOM_VK_BACK_SPACE",
+ "Delete": "DOM_VK_DELETE",
+ "Insert": "DOM_VK_INSERT",
+ "Return": "DOM_VK_RETURN",
+ "Enter": "DOM_VK_RETURN",
+ "Up": "DOM_VK_UP",
+ "Down": "DOM_VK_DOWN",
+ "Left": "DOM_VK_LEFT",
+ "Right": "DOM_VK_RIGHT",
+ "Home": "DOM_VK_HOME",
+ "End": "DOM_VK_END",
+ "PageUp": "DOM_VK_PAGE_UP",
+ "PageDown": "DOM_VK_PAGE_DOWN",
+ "Escape": "DOM_VK_ESCAPE",
+ "Esc": "DOM_VK_ESCAPE",
+ "Tab": "DOM_VK_TAB",
+ "VolumeUp": "DOM_VK_VOLUME_UP",
+ "VolumeDown": "DOM_VK_VOLUME_DOWN",
+ "VolumeMute": "DOM_VK_VOLUME_MUTE",
+ "PrintScreen": "DOM_VK_PRINTSCREEN"
+ };
+
+ /**
+ * Helper to listen for keyboard events decribed in .properties file.
+ *
+ * let shortcuts = new KeyShortcuts({
+ * window
+ * });
+ * shortcuts.on("Ctrl+F", event => {
+ * // `event` is the KeyboardEvent which relates to the key shortcuts
+ * });
+ *
+ * @param DOMWindow window
+ * The window object of the document to listen events from.
+ * @param DOMElement target
+ * Optional DOM Element on which we should listen events from.
+ * If omitted, we listen for all events fired on `window`.
+ */
+ function KeyShortcuts(_ref) {
+ var window = _ref.window;
+ var target = _ref.target;
+
+ this.window = window;
+ this.target = target || window;
+ this.keys = new Map();
+ this.eventEmitter = new EventEmitter();
+ this.target.addEventListener("keydown", this);
+ }
+
+ /*
+ * Parse an electron-like key string and return a normalized object which
+ * allow efficient match on DOM key event. The normalized object matches DOM
+ * API.
+ *
+ * @param DOMWindow window
+ * Any DOM Window object, just to fetch its `KeyboardEvent` object
+ * @param String str
+ * The shortcut string to parse, following this document:
+ * https://github.com/electron/electron/blob/master/docs/api/accelerator.md
+ */
+ KeyShortcuts.parseElectronKey = function (window, str) {
+ var modifiers = str.split("+");
+ var key = modifiers.pop();
+
+ var shortcut = {
+ ctrl: false,
+ meta: false,
+ alt: false,
+ shift: false,
+ // Set for character keys
+ key: undefined,
+ // Set for non-character keys
+ keyCode: undefined
+ };
+ for (var mod of modifiers) {
+ if (mod === "Alt") {
+ shortcut.alt = true;
+ } else if (["Command", "Cmd"].includes(mod)) {
+ shortcut.meta = true;
+ } else if (["CommandOrControl", "CmdOrCtrl"].includes(mod)) {
+ if (isOSX) {
+ shortcut.meta = true;
+ } else {
+ shortcut.ctrl = true;
+ }
+ } else if (["Control", "Ctrl"].includes(mod)) {
+ shortcut.ctrl = true;
+ } else if (mod === "Shift") {
+ shortcut.shift = true;
+ } else {
+ console.error("Unsupported modifier:", mod, "from key:", str);
+ return null;
+ }
+ }
+
+ // Plus is a special case. It's a character key and shouldn't be matched
+ // against a keycode as it is only accessible via Shift/Capslock
+ if (key === "Plus") {
+ key = "+";
+ }
+
+ if (typeof key === "string" && key.length === 1) {
+ // Match any single character
+ shortcut.key = key.toLowerCase();
+ } else if (key in ElectronKeysMapping) {
+ // Maps the others manually to DOM API DOM_VK_*
+ key = ElectronKeysMapping[key];
+ shortcut.keyCode = window.KeyboardEvent[key];
+ // Used only to stringify the shortcut
+ shortcut.keyCodeString = key;
+ shortcut.key = key;
+ } else {
+ console.error("Unsupported key:", key);
+ return null;
+ }
+
+ return shortcut;
+ };
+
+ KeyShortcuts.stringify = function (shortcut) {
+ var list = [];
+ if (shortcut.alt) {
+ list.push("Alt");
+ }
+ if (shortcut.ctrl) {
+ list.push("Ctrl");
+ }
+ if (shortcut.meta) {
+ list.push("Cmd");
+ }
+ if (shortcut.shift) {
+ list.push("Shift");
+ }
+ var key = void 0;
+ if (shortcut.key) {
+ key = shortcut.key.toUpperCase();
+ } else {
+ key = shortcut.keyCodeString;
+ }
+ list.push(key);
+ return list.join("+");
+ };
+
+ KeyShortcuts.prototype = {
+ destroy() {
+ this.target.removeEventListener("keydown", this);
+ this.keys.clear();
+ },
+
+ doesEventMatchShortcut(event, shortcut) {
+ if (shortcut.meta != event.metaKey) {
+ return false;
+ }
+ if (shortcut.ctrl != event.ctrlKey) {
+ return false;
+ }
+ if (shortcut.alt != event.altKey) {
+ return false;
+ }
+ // Shift is a special modifier, it may implicitely be required if the
+ // expected key is a special character accessible via shift.
+ if (shortcut.shift != event.shiftKey && event.key && event.key.match(/[a-zA-Z]/)) {
+ return false;
+ }
+ if (shortcut.keyCode) {
+ return event.keyCode == shortcut.keyCode;
+ } else if (event.key in ElectronKeysMapping) {
+ return ElectronKeysMapping[event.key] === shortcut.key;
+ }
+
+ // get the key from the keyCode if key is not provided.
+ var key = event.key || String.fromCharCode(event.keyCode);
+
+ // For character keys, we match if the final character is the expected one.
+ // But for digits we also accept indirect match to please azerty keyboard,
+ // which requires Shift to be pressed to get digits.
+ return key.toLowerCase() == shortcut.key || shortcut.key.match(/[0-9]/) && event.keyCode == shortcut.key.charCodeAt(0);
+ },
+
+ handleEvent(event) {
+ for (var _ref2 of this.keys) {
+ var _ref3 = _slicedToArray(_ref2, 2);
+
+ var key = _ref3[0];
+ var shortcut = _ref3[1];
+
+ if (this.doesEventMatchShortcut(event, shortcut)) {
+ this.eventEmitter.emit(key, event);
+ }
+ }
+ },
+
+ on(key, listener) {
+ if (typeof listener !== "function") {
+ throw new Error("KeyShortcuts.on() expects a function as " + "second argument");
+ }
+ if (!this.keys.has(key)) {
+ var shortcut = KeyShortcuts.parseElectronKey(this.window, key);
+ // The key string is wrong and we were unable to compute the key shortcut
+ if (!shortcut) {
+ return;
+ }
+ this.keys.set(key, shortcut);
+ }
+ this.eventEmitter.on(key, listener);
+ },
+
+ off(key, listener) {
+ this.eventEmitter.off(key, listener);
+ }
+ };
+ exports.KeyShortcuts = KeyShortcuts;
+
+/***/ },
+/* 30 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var Services = __webpack_require__(31);
+ var SplitBox = __webpack_require__(32);
+ // const SplitBoxCSS = require("./client/shared/components/splitter/SplitBox.css")
+ var rep = __webpack_require__(34).Rep;
+ // const repCSS = require("./client/shared/components/reps/reps.css");
+ var Grip = __webpack_require__(44).Grip;
+ var sprintf = __webpack_require__(59).sprintf;
+
+ module.exports = {
+ Services,
+ SplitBox,
+ // SplitBoxCSS,
+ rep,
+ // repCSS,
+ Grip,
+ sprintf
+ };
+
+/***/ },
+/* 31 */
+/***/ function(module, exports) {
+
+ module.exports = devtoolsRequire("Services");
+
+/***/ },
+/* 32 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var React = __webpack_require__(2);
+ var ReactDOM = __webpack_require__(16);
+ var Draggable = React.createFactory(__webpack_require__(33));
+ var dom = React.DOM;
+ var PropTypes = React.PropTypes;
+
+ /**
+ * This component represents a Splitter. The splitter supports vertical
+ * as well as horizontal mode.
+ */
+
+ var SplitBox = React.createClass({
+
+ propTypes: {
+ // Custom class name. You can use more names separated by a space.
+ className: PropTypes.string,
+ // Initial size of controlled panel.
+ initialSize: PropTypes.any,
+ // Optional initial width of controlled panel.
+ initialWidth: PropTypes.number,
+ // Optional initial height of controlled panel.
+ initialHeight: PropTypes.number,
+ // Left/top panel
+ startPanel: PropTypes.any,
+ // Min panel size.
+ minSize: PropTypes.any,
+ // Max panel size.
+ maxSize: PropTypes.any,
+ // Right/bottom panel
+ endPanel: PropTypes.any,
+ // True if the right/bottom panel should be controlled.
+ endPanelControl: PropTypes.bool,
+ // Size of the splitter handle bar.
+ splitterSize: PropTypes.number,
+ // True if the splitter bar is vertical (default is vertical).
+ vert: PropTypes.bool,
+ // Optional style properties passed into the splitbox
+ style: PropTypes.object
+ },
+
+ displayName: "SplitBox",
+
+ getDefaultProps() {
+ return {
+ splitterSize: 5,
+ vert: true,
+ endPanelControl: false
+ };
+ },
+
+ /**
+ * The state stores the current orientation (vertical or horizontal)
+ * and the current size (width/height). All these values can change
+ * during the component's life time.
+ */
+ getInitialState() {
+ return {
+ vert: this.props.vert,
+ width: this.props.initialWidth || this.props.initialSize,
+ height: this.props.initialHeight || this.props.initialSize
+ };
+ },
+
+ // Dragging Events
+
+ /**
+ * Set 'resizing' cursor on entire document during splitter dragging.
+ * This avoids cursor-flickering that happens when the mouse leaves
+ * the splitter bar area (happens frequently).
+ */
+ onStartMove() {
+ var splitBox = ReactDOM.findDOMNode(this);
+ var doc = splitBox.ownerDocument;
+ var defaultCursor = doc.documentElement.style.cursor;
+ doc.documentElement.style.cursor = this.state.vert ? "ew-resize" : "ns-resize";
+
+ splitBox.classList.add("dragging");
+
+ this.setState({
+ defaultCursor: defaultCursor
+ });
+ },
+
+ onStopMove() {
+ var splitBox = ReactDOM.findDOMNode(this);
+ var doc = splitBox.ownerDocument;
+ doc.documentElement.style.cursor = this.state.defaultCursor;
+
+ splitBox.classList.remove("dragging");
+ },
+
+ screenX() {
+ var borderWidth = (window.outerWidth - window.innerWidth) / 2;
+ return window.screenX + borderWidth;
+ },
+
+ screenY() {
+ var borderHeignt = window.outerHeight - window.innerHeight;
+ return window.screenY + borderHeignt;
+ },
+
+ /**
+ * Adjust size of the controlled panel. Depending on the current
+ * orientation we either remember the width or height of
+ * the splitter box.
+ */
+ onMove(x, y) {
+ var node = ReactDOM.findDOMNode(this);
+ var doc = node.ownerDocument;
+ var win = doc.defaultView;
+
+ var size = void 0;
+ var endPanelControl = this.props.endPanelControl;
+
+
+ if (this.state.vert) {
+ // Switch the control flag in case of RTL. Note that RTL
+ // has impact on vertical splitter only.
+ var dir = win.getComputedStyle(doc.documentElement).direction;
+ if (dir == "rtl") {
+ endPanelControl = !endPanelControl;
+ }
+
+ var innerOffset = x - this.screenX();
+ size = endPanelControl ? node.offsetLeft + node.offsetWidth - innerOffset : innerOffset - node.offsetLeft;
+
+ this.setState({
+ width: size
+ });
+ } else {
+ var _innerOffset = y - this.screenY();
+ size = endPanelControl ? node.offsetTop + node.offsetHeight - _innerOffset : _innerOffset - node.offsetTop;
+
+ this.setState({
+ height: size
+ });
+ }
+ },
+
+ // Rendering
+
+ render() {
+ var vert = this.state.vert;
+ var _props = this.props;
+ var startPanel = _props.startPanel;
+ var endPanel = _props.endPanel;
+ var endPanelControl = _props.endPanelControl;
+ var minSize = _props.minSize;
+ var maxSize = _props.maxSize;
+ var splitterSize = _props.splitterSize;
+
+
+ var style = Object.assign({}, this.props.style);
+
+ // Calculate class names list.
+ var classNames = ["split-box"];
+ classNames.push(vert ? "vert" : "horz");
+ if (this.props.className) {
+ classNames = classNames.concat(this.props.className.split(" "));
+ }
+
+ var leftPanelStyle = void 0;
+ var rightPanelStyle = void 0;
+
+ // Set proper size for panels depending on the current state.
+ if (vert) {
+ leftPanelStyle = {
+ maxWidth: endPanelControl ? null : maxSize,
+ minWidth: endPanelControl ? null : minSize,
+ width: endPanelControl ? null : this.state.width
+ };
+ rightPanelStyle = {
+ maxWidth: endPanelControl ? maxSize : null,
+ minWidth: endPanelControl ? minSize : null,
+ width: endPanelControl ? this.state.width : null
+ };
+ } else {
+ leftPanelStyle = {
+ maxHeight: endPanelControl ? null : maxSize,
+ minHeight: endPanelControl ? null : minSize,
+ height: endPanelControl ? null : this.state.height
+ };
+ rightPanelStyle = {
+ maxHeight: endPanelControl ? maxSize : null,
+ minHeight: endPanelControl ? minSize : null,
+ height: endPanelControl ? this.state.height : null
+ };
+ }
+
+ // Calculate splitter size
+ var splitterStyle = {
+ flex: "0 0 " + splitterSize + "px"
+ };
+
+ return dom.div({
+ className: classNames.join(" "),
+ style: style }, startPanel ? dom.div({
+ className: endPanelControl ? "uncontrolled" : "controlled",
+ style: leftPanelStyle }, startPanel) : null, Draggable({
+ className: "splitter",
+ style: splitterStyle,
+ onStart: this.onStartMove,
+ onStop: this.onStopMove,
+ onMove: this.onMove
+ }), endPanel ? dom.div({
+ className: endPanelControl ? "controlled" : "uncontrolled",
+ style: rightPanelStyle }, endPanel) : null);
+ }
+ });
+
+ module.exports = SplitBox;
+
+/***/ },
+/* 33 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /* 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 React = __webpack_require__(2);
+ var ReactDOM = __webpack_require__(16);
+ var dom = React.DOM;
+ var PropTypes = React.PropTypes;
+
+
+ var Draggable = React.createClass({
+ displayName: "Draggable",
+
+ propTypes: {
+ onMove: PropTypes.func.isRequired,
+ onStart: PropTypes.func,
+ onStop: PropTypes.func,
+ style: PropTypes.object,
+ className: PropTypes.string
+ },
+
+ startDragging(ev) {
+ ev.preventDefault();
+ var doc = ReactDOM.findDOMNode(this).ownerDocument;
+ doc.addEventListener("mousemove", this.onMove);
+ doc.addEventListener("mouseup", this.onUp);
+ this.props.onStart && this.props.onStart();
+ },
+
+ onMove(ev) {
+ ev.preventDefault();
+ // Use screen coordinates so, moving mouse over iframes
+ // doesn't mangle (relative) coordinates.
+ this.props.onMove(ev.screenX, ev.screenY);
+ },
+
+ onUp(ev) {
+ ev.preventDefault();
+ var doc = ReactDOM.findDOMNode(this).ownerDocument;
+ doc.removeEventListener("mousemove", this.onMove);
+ doc.removeEventListener("mouseup", this.onUp);
+ this.props.onStop && this.props.onStop();
+ },
+
+ render() {
+ return dom.div({
+ style: this.props.style,
+ className: this.props.className,
+ onMouseDown: this.startDragging
+ });
+ }
+ });
+
+ module.exports = Draggable;
+
+/***/ },
+/* 34 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_RESULT__;/* -*- 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";
+
+ // Make this available to both AMD and CJS environments
+
+ !(__WEBPACK_AMD_DEFINE_RESULT__ = function (require, exports, module) {
+ // Dependencies
+ var React = __webpack_require__(2);
+
+ var _require = __webpack_require__(35);
+
+ var isGrip = _require.isGrip;
+
+ // Load all existing rep templates
+
+ var _require2 = __webpack_require__(36);
+
+ var Undefined = _require2.Undefined;
+
+ var _require3 = __webpack_require__(37);
+
+ var Null = _require3.Null;
+
+ var _require4 = __webpack_require__(38);
+
+ var StringRep = _require4.StringRep;
+
+ var _require5 = __webpack_require__(39);
+
+ var Number = _require5.Number;
+
+ var _require6 = __webpack_require__(40);
+
+ var ArrayRep = _require6.ArrayRep;
+
+ var _require7 = __webpack_require__(42);
+
+ var Obj = _require7.Obj;
+
+ var _require8 = __webpack_require__(45);
+
+ var SymbolRep = _require8.SymbolRep;
+
+ // DOM types (grips)
+
+ var _require9 = __webpack_require__(46);
+
+ var Attribute = _require9.Attribute;
+
+ var _require10 = __webpack_require__(47);
+
+ var DateTime = _require10.DateTime;
+
+ var _require11 = __webpack_require__(48);
+
+ var Document = _require11.Document;
+
+ var _require12 = __webpack_require__(49);
+
+ var Event = _require12.Event;
+
+ var _require13 = __webpack_require__(50);
+
+ var Func = _require13.Func;
+
+ var _require14 = __webpack_require__(51);
+
+ var RegExp = _require14.RegExp;
+
+ var _require15 = __webpack_require__(52);
+
+ var StyleSheet = _require15.StyleSheet;
+
+ var _require16 = __webpack_require__(53);
+
+ var TextNode = _require16.TextNode;
+
+ var _require17 = __webpack_require__(54);
+
+ var Window = _require17.Window;
+
+ var _require18 = __webpack_require__(55);
+
+ var ObjectWithText = _require18.ObjectWithText;
+
+ var _require19 = __webpack_require__(56);
+
+ var ObjectWithURL = _require19.ObjectWithURL;
+
+ var _require20 = __webpack_require__(57);
+
+ var GripArray = _require20.GripArray;
+
+ var _require21 = __webpack_require__(58);
+
+ var GripMap = _require21.GripMap;
+
+ var _require22 = __webpack_require__(44);
+
+ var Grip = _require22.Grip;
+
+ // List of all registered template.
+ // XXX there should be a way for extensions to register a new
+ // or modify an existing rep.
+
+ var reps = [RegExp, StyleSheet, Event, DateTime, TextNode, Attribute, Func, ArrayRep, Document, Window, ObjectWithText, ObjectWithURL, GripArray, GripMap, Grip, Undefined, Null, StringRep, Number, SymbolRep];
+
+ /**
+ * Generic rep that is using for rendering native JS types or an object.
+ * The right template used for rendering is picked automatically according
+ * to the current value type. The value must be passed is as 'object'
+ * property.
+ */
+ var Rep = React.createClass({
+ displayName: "Rep",
+
+ propTypes: {
+ object: React.PropTypes.any,
+ defaultRep: React.PropTypes.object,
+ mode: React.PropTypes.string
+ },
+
+ render: function () {
+ var rep = getRep(this.props.object, this.props.defaultRep);
+ return rep(this.props);
+ }
+ });
+
+ // Helpers
+
+ /**
+ * Return a rep object that is responsible for rendering given
+ * object.
+ *
+ * @param object {Object} Object to be rendered in the UI. This
+ * can be generic JS object as well as a grip (handle to a remote
+ * debuggee object).
+ *
+ * @param defaultObject {React.Component} The default template
+ * that should be used to render given object if none is found.
+ */
+ function getRep(object) {
+ var defaultRep = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : Obj;
+
+ var type = typeof object;
+ if (type == "object" && object instanceof String) {
+ type = "string";
+ } else if (type == "object" && object.type === "symbol") {
+ type = "symbol";
+ }
+
+ if (isGrip(object)) {
+ type = object.class;
+ }
+
+ for (var i = 0; i < reps.length; i++) {
+ var rep = reps[i];
+ try {
+ // supportsObject could return weight (not only true/false
+ // but a number), which would allow to priorities templates and
+ // support better extensibility.
+ if (rep.supportsObject(object, type)) {
+ return React.createFactory(rep.rep);
+ }
+ } catch (err) {
+ console.error(err);
+ }
+ }
+
+ return React.createFactory(defaultRep.rep);
+ }
+
+ // Exports from this module
+ exports.Rep = Rep;
+ }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+/***/ },
+/* 35 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_RESULT__;/* globals URLSearchParams */
+ /* -*- 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";
+
+ // Make this available to both AMD and CJS environments
+
+ function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
+
+ !(__WEBPACK_AMD_DEFINE_RESULT__ = function (require, exports, module) {
+ // Dependencies
+ var React = __webpack_require__(2);
+
+ /**
+ * Create React factories for given arguments.
+ * Example:
+ * const { Rep } = createFactories(require("./rep"));
+ */
+ function createFactories(args) {
+ var result = {};
+ for (var p in args) {
+ result[p] = React.createFactory(args[p]);
+ }
+ return result;
+ }
+
+ /**
+ * Returns true if the given object is a grip (see RDP protocol)
+ */
+ function isGrip(object) {
+ return object && object.actor;
+ }
+
+ function escapeNewLines(value) {
+ return value.replace(/\r/gm, "\\r").replace(/\n/gm, "\\n");
+ }
+
+ function cropMultipleLines(text, limit) {
+ return escapeNewLines(cropString(text, limit));
+ }
+
+ function cropString(text, limit, alternativeText) {
+ if (!alternativeText) {
+ alternativeText = "\u2026";
+ }
+
+ // Make sure it's a string.
+ text = text + "";
+
+ // Crop the string only if a limit is actually specified.
+ if (!limit || limit <= 0) {
+ return text;
+ }
+
+ // Set the limit at least to the length of the alternative text
+ // plus one character of the original text.
+ if (limit <= alternativeText.length) {
+ limit = alternativeText.length + 1;
+ }
+
+ var halfLimit = (limit - alternativeText.length) / 2;
+
+ if (text.length > limit) {
+ return text.substr(0, Math.ceil(halfLimit)) + alternativeText + text.substr(text.length - Math.floor(halfLimit));
+ }
+
+ return text;
+ }
+
+ function parseURLParams(url) {
+ url = new URL(url);
+ return parseURLEncodedText(url.searchParams);
+ }
+
+ function parseURLEncodedText(text) {
+ var params = [];
+
+ // In case the text is empty just return the empty parameters
+ if (text == "") {
+ return params;
+ }
+
+ var searchParams = new URLSearchParams(text);
+ var entries = [].concat(_toConsumableArray(searchParams.entries()));
+ return entries.map(entry => {
+ return {
+ name: entry[0],
+ value: entry[1]
+ };
+ });
+ }
+
+ function getFileName(url) {
+ var split = splitURLBase(url);
+ return split.name;
+ }
+
+ function splitURLBase(url) {
+ if (!isDataURL(url)) {
+ return splitURLTrue(url);
+ }
+ return {};
+ }
+
+ function getURLDisplayString(url) {
+ return cropString(url);
+ }
+
+ function isDataURL(url) {
+ return url && url.substr(0, 5) == "data:";
+ }
+
+ function splitURLTrue(url) {
+ var reSplitFile = /(.*?):\/{2,3}([^\/]*)(.*?)([^\/]*?)($|\?.*)/;
+ var m = reSplitFile.exec(url);
+
+ if (!m) {
+ return {
+ name: url,
+ path: url
+ };
+ } else if (m[4] == "" && m[5] == "") {
+ return {
+ protocol: m[1],
+ domain: m[2],
+ path: m[3],
+ name: m[3] != "/" ? m[3] : m[2]
+ };
+ }
+
+ return {
+ protocol: m[1],
+ domain: m[2],
+ path: m[2] + m[3],
+ name: m[4] + m[5]
+ };
+ }
+
+ // Exports from this module
+ exports.createFactories = createFactories;
+ exports.isGrip = isGrip;
+ exports.cropString = cropString;
+ exports.cropMultipleLines = cropMultipleLines;
+ exports.parseURLParams = parseURLParams;
+ exports.parseURLEncodedText = parseURLEncodedText;
+ exports.getFileName = getFileName;
+ exports.getURLDisplayString = getURLDisplayString;
+ }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+/***/ },
+/* 36 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_RESULT__;/* -*- 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";
+
+ // Make this available to both AMD and CJS environments
+
+ !(__WEBPACK_AMD_DEFINE_RESULT__ = function (require, exports, module) {
+ // Dependencies
+ var React = __webpack_require__(2);
+
+ // Shortcuts
+ var span = React.DOM.span;
+
+ /**
+ * Renders undefined value
+ */
+
+ var Undefined = React.createClass({
+ displayName: "UndefinedRep",
+
+ render: function () {
+ return span({ className: "objectBox objectBox-undefined" }, "undefined");
+ }
+ });
+
+ function supportsObject(object, type) {
+ if (object && object.type && object.type == "undefined") {
+ return true;
+ }
+
+ return type == "undefined";
+ }
+
+ // Exports from this module
+
+ exports.Undefined = {
+ rep: Undefined,
+ supportsObject: supportsObject
+ };
+ }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+/***/ },
+/* 37 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_RESULT__;/* -*- 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";
+
+ // Make this available to both AMD and CJS environments
+
+ !(__WEBPACK_AMD_DEFINE_RESULT__ = function (require, exports, module) {
+ // Dependencies
+ var React = __webpack_require__(2);
+
+ // Shortcuts
+ var span = React.DOM.span;
+
+ /**
+ * Renders null value
+ */
+
+ var Null = React.createClass({
+ displayName: "NullRep",
+
+ render: function () {
+ return span({ className: "objectBox objectBox-null" }, "null");
+ }
+ });
+
+ function supportsObject(object, type) {
+ if (object && object.type && object.type == "null") {
+ return true;
+ }
+
+ return object == null;
+ }
+
+ // Exports from this module
+
+ exports.Null = {
+ rep: Null,
+ supportsObject: supportsObject
+ };
+ }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+/***/ },
+/* 38 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_RESULT__;/* -*- 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";
+
+ // Make this available to both AMD and CJS environments
+
+ !(__WEBPACK_AMD_DEFINE_RESULT__ = function (require, exports, module) {
+ // Dependencies
+ var React = __webpack_require__(2);
+
+ var _require = __webpack_require__(35);
+
+ var cropMultipleLines = _require.cropMultipleLines;
+
+ // Shortcuts
+
+ var span = React.DOM.span;
+
+ /**
+ * Renders a string. String value is enclosed within quotes.
+ */
+
+ var StringRep = React.createClass({
+ displayName: "StringRep",
+
+ propTypes: {
+ useQuotes: React.PropTypes.bool
+ },
+
+ getDefaultProps: function () {
+ return {
+ useQuotes: true
+ };
+ },
+
+ render: function () {
+ var text = this.props.object;
+ var member = this.props.member;
+ if (member && member.open) {
+ return span({ className: "objectBox objectBox-string" }, "\"" + text + "\"");
+ }
+
+ var croppedString = this.props.cropLimit ? cropMultipleLines(text, this.props.cropLimit) : cropMultipleLines(text);
+
+ var formattedString = this.props.useQuotes ? "\"" + croppedString + "\"" : croppedString;
+
+ return span({ className: "objectBox objectBox-string" }, formattedString);
+ }
+ });
+
+ function supportsObject(object, type) {
+ return type == "string";
+ }
+
+ // Exports from this module
+
+ exports.StringRep = {
+ rep: StringRep,
+ supportsObject: supportsObject
+ };
+ }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+/***/ },
+/* 39 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_RESULT__;/* -*- 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";
+
+ // Make this available to both AMD and CJS environments
+
+ !(__WEBPACK_AMD_DEFINE_RESULT__ = function (require, exports, module) {
+ // Dependencies
+ var React = __webpack_require__(2);
+
+ // Shortcuts
+ var span = React.DOM.span;
+
+ /**
+ * Renders a number
+ */
+
+ var Number = React.createClass({
+ displayName: "Number",
+
+ stringify: function (object) {
+ var isNegativeZero = Object.is(object, -0) || object.type && object.type == "-0";
+
+ return isNegativeZero ? "-0" : String(object);
+ },
+
+ render: function () {
+ var value = this.props.object;
+
+ return span({ className: "objectBox objectBox-number" }, this.stringify(value));
+ }
+ });
+
+ function supportsObject(object, type) {
+ return type == "boolean" || type == "number" || type == "object" && object.type == "-0";
+ }
+
+ // Exports from this module
+
+ exports.Number = {
+ rep: Number,
+ supportsObject: supportsObject
+ };
+ }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+/***/ },
+/* 40 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_RESULT__;/* -*- 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";
+
+ // Make this available to both AMD and CJS environments
+
+ !(__WEBPACK_AMD_DEFINE_RESULT__ = function (require, exports, module) {
+ // Dependencies
+ var React = __webpack_require__(2);
+
+ var _require = __webpack_require__(35);
+
+ var createFactories = _require.createFactories;
+
+ var _createFactories = createFactories(__webpack_require__(41));
+
+ var Caption = _createFactories.Caption;
+
+ // Shortcuts
+
+ var DOM = React.DOM;
+
+ /**
+ * Renders an array. The array is enclosed by left and right bracket
+ * and the max number of rendered items depends on the current mode.
+ */
+ var ArrayRep = React.createClass({
+ displayName: "ArrayRep",
+
+ getTitle: function (object, context) {
+ return "[" + object.length + "]";
+ },
+
+ arrayIterator: function (array, max) {
+ var items = [];
+ var delim = void 0;
+
+ for (var i = 0; i < array.length && i < max; i++) {
+ try {
+ var value = array[i];
+
+ delim = i == array.length - 1 ? "" : ", ";
+
+ items.push(ItemRep({
+ key: i,
+ object: value,
+ // Hardcode tiny mode to avoid recursive handling.
+ mode: "tiny",
+ delim: delim
+ }));
+ } catch (exc) {
+ items.push(ItemRep({
+ key: i,
+ object: exc,
+ mode: "tiny",
+ delim: delim
+ }));
+ }
+ }
+
+ if (array.length > max) {
+ var objectLink = this.props.objectLink || DOM.span;
+ items.push(Caption({
+ key: "more",
+ object: objectLink({
+ object: this.props.object
+ }, array.length - max + " more…")
+ }));
+ }
+
+ return items;
+ },
+
+ /**
+ * Returns true if the passed object is an array with additional (custom)
+ * properties, otherwise returns false. Custom properties should be
+ * displayed in extra expandable section.
+ *
+ * Example array with a custom property.
+ * let arr = [0, 1];
+ * arr.myProp = "Hello";
+ *
+ * @param {Array} array The array object.
+ */
+ hasSpecialProperties: function (array) {
+ function isInteger(x) {
+ var y = parseInt(x, 10);
+ if (isNaN(y)) {
+ return false;
+ }
+ return x === y.toString();
+ }
+
+ var props = Object.getOwnPropertyNames(array);
+ for (var i = 0; i < props.length; i++) {
+ var p = props[i];
+
+ // Valid indexes are skipped
+ if (isInteger(p)) {
+ continue;
+ }
+
+ // Ignore standard 'length' property, anything else is custom.
+ if (p != "length") {
+ return true;
+ }
+ }
+
+ return false;
+ },
+
+ // Event Handlers
+
+ onToggleProperties: function (event) {},
+
+ onClickBracket: function (event) {},
+
+ render: function () {
+ var mode = this.props.mode || "short";
+ var object = this.props.object;
+ var items = void 0;
+ var brackets = void 0;
+ var needSpace = function (space) {
+ return space ? { left: "[ ", right: " ]" } : { left: "[", right: "]" };
+ };
+
+ if (mode == "tiny") {
+ var isEmpty = object.length === 0;
+ items = DOM.span({ className: "length" }, isEmpty ? "" : object.length);
+ brackets = needSpace(false);
+ } else {
+ var max = mode == "short" ? 3 : 300;
+ items = this.arrayIterator(object, max);
+ brackets = needSpace(items.length > 0);
+ }
+
+ var objectLink = this.props.objectLink || DOM.span;
+
+ return DOM.span({
+ className: "objectBox objectBox-array" }, objectLink({
+ className: "arrayLeftBracket",
+ object: object
+ }, brackets.left), items, objectLink({
+ className: "arrayRightBracket",
+ object: object
+ }, brackets.right), DOM.span({
+ className: "arrayProperties",
+ role: "group" }));
+ }
+ });
+
+ /**
+ * Renders array item. Individual values are separated by a comma.
+ */
+ var ItemRep = React.createFactory(React.createClass({
+ displayName: "ItemRep",
+
+ render: function () {
+ var _createFactories2 = createFactories(__webpack_require__(34));
+
+ var Rep = _createFactories2.Rep;
+
+
+ var object = this.props.object;
+ var delim = this.props.delim;
+ var mode = this.props.mode;
+ return DOM.span({}, Rep({ object: object, mode: mode }), delim);
+ }
+ }));
+
+ function supportsObject(object, type) {
+ return Array.isArray(object) || Object.prototype.toString.call(object) === "[object Arguments]";
+ }
+
+ // Exports from this module
+ exports.ArrayRep = {
+ rep: ArrayRep,
+ supportsObject: supportsObject
+ };
+ }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+/***/ },
+/* 41 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_RESULT__;/* -*- 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";
+
+ // Make this available to both AMD and CJS environments
+
+ !(__WEBPACK_AMD_DEFINE_RESULT__ = function (require, exports, module) {
+ // Dependencies
+ var React = __webpack_require__(2);
+ var DOM = React.DOM;
+
+ /**
+ * Renders a caption. This template is used by other components
+ * that needs to distinguish between a simple text/value and a label.
+ */
+ var Caption = React.createClass({
+ displayName: "Caption",
+
+ render: function () {
+ return DOM.span({ "className": "caption" }, this.props.object);
+ }
+ });
+
+ // Exports from this module
+ exports.Caption = Caption;
+ }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+/***/ },
+/* 42 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_RESULT__;/* -*- 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";
+ // Make this available to both AMD and CJS environments
+
+ !(__WEBPACK_AMD_DEFINE_RESULT__ = function (require, exports, module) {
+ // Dependencies
+ var React = __webpack_require__(2);
+
+ var _require = __webpack_require__(35);
+
+ var createFactories = _require.createFactories;
+
+ var _createFactories = createFactories(__webpack_require__(41));
+
+ var Caption = _createFactories.Caption;
+
+ var _createFactories2 = createFactories(__webpack_require__(43));
+
+ var PropRep = _createFactories2.PropRep;
+ // Shortcuts
+
+ var span = React.DOM.span;
+ /**
+ * Renders an object. An object is represented by a list of its
+ * properties enclosed in curly brackets.
+ */
+
+ var Obj = React.createClass({
+ displayName: "Obj",
+
+ propTypes: {
+ object: React.PropTypes.object,
+ mode: React.PropTypes.string
+ },
+
+ getTitle: function (object) {
+ var className = object && object.class ? object.class : "Object";
+ if (this.props.objectLink) {
+ return this.props.objectLink({
+ object: object
+ }, className);
+ }
+ return className;
+ },
+
+ safePropIterator: function (object, max) {
+ max = typeof max === "undefined" ? 3 : max;
+ try {
+ return this.propIterator(object, max);
+ } catch (err) {
+ console.error(err);
+ }
+ return [];
+ },
+
+ propIterator: function (object, max) {
+ var isInterestingProp = (t, value) => {
+ // Do not pick objects, it could cause recursion.
+ return t == "boolean" || t == "number" || t == "string" && value;
+ };
+
+ // Work around https://bugzilla.mozilla.org/show_bug.cgi?id=945377
+ if (Object.prototype.toString.call(object) === "[object Generator]") {
+ object = Object.getPrototypeOf(object);
+ }
+
+ // Object members with non-empty values are preferred since it gives the
+ // user a better overview of the object.
+ var props = this.getProps(object, max, isInterestingProp);
+
+ if (props.length <= max) {
+ // There are not enough props yet (or at least, not enough props to
+ // be able to know whether we should print "more…" or not).
+ // Let's display also empty members and functions.
+ props = props.concat(this.getProps(object, max, (t, value) => {
+ return !isInterestingProp(t, value);
+ }));
+ }
+
+ if (props.length > max) {
+ props.pop();
+ var objectLink = this.props.objectLink || span;
+
+ props.push(Caption({
+ key: "more",
+ object: objectLink({
+ object: object
+ }, Object.keys(object).length - max + " more…")
+ }));
+ } else if (props.length > 0) {
+ // Remove the last comma.
+ props[props.length - 1] = React.cloneElement(props[props.length - 1], { delim: "" });
+ }
+
+ return props;
+ },
+
+ getProps: function (object, max, filter) {
+ var props = [];
+
+ max = max || 3;
+ if (!object) {
+ return props;
+ }
+
+ // Hardcode tiny mode to avoid recursive handling.
+ var mode = "tiny";
+
+ try {
+ for (var name in object) {
+ if (props.length > max) {
+ return props;
+ }
+
+ var value = void 0;
+ try {
+ value = object[name];
+ } catch (exc) {
+ continue;
+ }
+
+ var t = typeof value;
+ if (filter(t, value)) {
+ props.push(PropRep({
+ key: name,
+ mode: mode,
+ name: name,
+ object: value,
+ equal: ": ",
+ delim: ", "
+ }));
+ }
+ }
+ } catch (err) {
+ console.error(err);
+ }
+
+ return props;
+ },
+
+ render: function () {
+ var object = this.props.object;
+ var props = this.safePropIterator(object);
+ var objectLink = this.props.objectLink || span;
+
+ if (this.props.mode == "tiny" || !props.length) {
+ return span({ className: "objectBox objectBox-object" }, objectLink({ className: "objectTitle" }, this.getTitle(object)));
+ }
+
+ return span({ className: "objectBox objectBox-object" }, this.getTitle(object), objectLink({
+ className: "objectLeftBrace",
+ object: object
+ }, " { "), props, objectLink({
+ className: "objectRightBrace",
+ object: object
+ }, " }"));
+ }
+ });
+ function supportsObject(object, type) {
+ return true;
+ }
+
+ // Exports from this module
+ exports.Obj = {
+ rep: Obj,
+ supportsObject: supportsObject
+ };
+ }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+/***/ },
+/* 43 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_RESULT__;/* -*- 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";
+
+ // Make this available to both AMD and CJS environments
+
+ !(__WEBPACK_AMD_DEFINE_RESULT__ = function (require, exports, module) {
+ var React = __webpack_require__(2);
+
+ var _require = __webpack_require__(35);
+
+ var createFactories = _require.createFactories;
+ var span = React.DOM.span;
+
+ /**
+ * Property for Obj (local JS objects), Grip (remote JS objects)
+ * and GripMap (remote JS maps and weakmaps) reps.
+ * It's used to render object properties.
+ */
+
+ var PropRep = React.createFactory(React.createClass({
+ displayName: "PropRep",
+
+ propTypes: {
+ // Property name.
+ name: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.object]).isRequired,
+ // Equal character rendered between property name and value.
+ equal: React.PropTypes.string,
+ // Delimiter character used to separate individual properties.
+ delim: React.PropTypes.string,
+ mode: React.PropTypes.string
+ },
+
+ render: function () {
+ var _require2 = __webpack_require__(44);
+
+ var Grip = _require2.Grip;
+
+ var _createFactories = createFactories(__webpack_require__(34));
+
+ var Rep = _createFactories.Rep;
+
+
+ var key = void 0;
+ // The key can be a simple string, for plain objects,
+ // or another object for maps and weakmaps.
+ if (typeof this.props.name === "string") {
+ key = span({ "className": "nodeName" }, this.props.name);
+ } else {
+ key = Rep({
+ object: this.props.name,
+ mode: this.props.mode || "tiny",
+ defaultRep: Grip,
+ objectLink: this.props.objectLink
+ });
+ }
+
+ return span({}, key, span({
+ "className": "objectEqual"
+ }, this.props.equal), Rep(this.props), span({
+ "className": "objectComma"
+ }, this.props.delim));
+ }
+ }));
+
+ // Exports from this module
+ exports.PropRep = PropRep;
+ }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+/***/ },
+/* 44 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_RESULT__;/* -*- 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";
+
+ // Make this available to both AMD and CJS environments
+
+ !(__WEBPACK_AMD_DEFINE_RESULT__ = function (require, exports, module) {
+ // ReactJS
+ var React = __webpack_require__(2);
+ // Dependencies
+
+ var _require = __webpack_require__(35);
+
+ var createFactories = _require.createFactories;
+ var isGrip = _require.isGrip;
+
+ var _createFactories = createFactories(__webpack_require__(41));
+
+ var Caption = _createFactories.Caption;
+
+ var _createFactories2 = createFactories(__webpack_require__(43));
+
+ var PropRep = _createFactories2.PropRep;
+ // Shortcuts
+
+ var span = React.DOM.span;
+
+ /**
+ * Renders generic grip. Grip is client representation
+ * of remote JS object and is used as an input object
+ * for this rep component.
+ */
+
+ var GripRep = React.createClass({
+ displayName: "Grip",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired,
+ mode: React.PropTypes.string,
+ isInterestingProp: React.PropTypes.func
+ },
+
+ getTitle: function (object) {
+ if (this.props.objectLink) {
+ return this.props.objectLink({
+ object: object
+ }, object.class);
+ }
+ return object.class || "Object";
+ },
+
+ safePropIterator: function (object, max) {
+ max = typeof max === "undefined" ? 3 : max;
+ try {
+ return this.propIterator(object, max);
+ } catch (err) {
+ console.error(err);
+ }
+ return [];
+ },
+
+ propIterator: function (object, max) {
+ // Property filter. Show only interesting properties to the user.
+ var isInterestingProp = this.props.isInterestingProp || ((type, value) => {
+ return type == "boolean" || type == "number" || type == "string" && value.length != 0;
+ });
+
+ var ownProperties = object.preview ? object.preview.ownProperties : [];
+ var indexes = this.getPropIndexes(ownProperties, max, isInterestingProp);
+ if (indexes.length < max && indexes.length < object.ownPropertyLength) {
+ // There are not enough props yet. Then add uninteresting props to display them.
+ indexes = indexes.concat(this.getPropIndexes(ownProperties, max - indexes.length, (t, value, name) => {
+ return !isInterestingProp(t, value, name);
+ }));
+ }
+
+ var props = this.getProps(ownProperties, indexes);
+ if (props.length < object.ownPropertyLength) {
+ // There are some undisplayed props. Then display "more...".
+ var objectLink = this.props.objectLink || span;
+
+ props.push(Caption({
+ key: "more",
+ object: objectLink({
+ object: object
+ }, (object ? object.ownPropertyLength : 0) - max + " more…")
+ }));
+ } else if (props.length > 0) {
+ // Remove the last comma.
+ // NOTE: do not change comp._store.props directly to update a property,
+ // it should be re-rendered or cloned with changed props
+ var last = props.length - 1;
+ props[last] = React.cloneElement(props[last], {
+ delim: ""
+ });
+ }
+
+ return props;
+ },
+
+ /**
+ * Get props ordered by index.
+ *
+ * @param {Object} ownProperties Props object.
+ * @param {Array} indexes Indexes of props.
+ * @return {Array} Props.
+ */
+ getProps: function (ownProperties, indexes) {
+ var props = [];
+
+ // Make indexes ordered by ascending.
+ indexes.sort(function (a, b) {
+ return a - b;
+ });
+
+ indexes.forEach(i => {
+ var name = Object.keys(ownProperties)[i];
+ var prop = ownProperties[name];
+ var value = prop.value !== undefined ? prop.value : prop;
+ props.push(PropRep(Object.assign({}, this.props, {
+ key: name,
+ mode: "tiny",
+ name: name,
+ object: value,
+ equal: ": ",
+ delim: ", ",
+ defaultRep: Grip
+ })));
+ });
+
+ return props;
+ },
+
+ /**
+ * Get the indexes of props in the object.
+ *
+ * @param {Object} ownProperties Props object.
+ * @param {Number} max The maximum length of indexes array.
+ * @param {Function} filter Filter the props you want.
+ * @return {Array} Indexes of interesting props in the object.
+ */
+ getPropIndexes: function (ownProperties, max, filter) {
+ var indexes = [];
+
+ try {
+ var i = 0;
+ for (var name in ownProperties) {
+ if (indexes.length >= max) {
+ return indexes;
+ }
+
+ var prop = ownProperties[name];
+ var value = prop.value !== undefined ? prop.value : prop;
+
+ // Type is specified in grip's "class" field and for primitive
+ // values use typeof.
+ var type = value.class || typeof value;
+ type = type.toLowerCase();
+
+ if (filter(type, value, name)) {
+ indexes.push(i);
+ }
+ i++;
+ }
+ } catch (err) {
+ console.error(err);
+ }
+
+ return indexes;
+ },
+
+ render: function () {
+ var object = this.props.object;
+ var props = this.safePropIterator(object, this.props.mode == "long" ? 100 : 3);
+
+ var objectLink = this.props.objectLink || span;
+ if (this.props.mode == "tiny" || !props.length) {
+ return span({ className: "objectBox objectBox-object" }, this.getTitle(object), objectLink({
+ className: "objectLeftBrace",
+ object: object
+ }, ""));
+ }
+
+ return span({ className: "objectBox objectBox-object" }, this.getTitle(object), objectLink({
+ className: "objectLeftBrace",
+ object: object
+ }, " { "), props, objectLink({
+ className: "objectRightBrace",
+ object: object
+ }, " }"));
+ }
+ });
+
+ // Registration
+ function supportsObject(object, type) {
+ if (!isGrip(object)) {
+ return false;
+ }
+ return object.preview && object.preview.ownProperties;
+ }
+
+ var Grip = {
+ rep: GripRep,
+ supportsObject: supportsObject
+ };
+
+ // Exports from this module
+ exports.Grip = Grip;
+ }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+/***/ },
+/* 45 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_RESULT__;/* -*- 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";
+
+ // Make this available to both AMD and CJS environments
+
+ !(__WEBPACK_AMD_DEFINE_RESULT__ = function (require, exports, module) {
+ // Dependencies
+ var React = __webpack_require__(2);
+
+ // Shortcuts
+ var span = React.DOM.span;
+
+ /**
+ * Renders a symbol.
+ */
+
+ var SymbolRep = React.createClass({
+ displayName: "SymbolRep",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired
+ },
+
+ render: function () {
+ var object = this.props.object;
+ var name = object.name;
+
+
+ return span({ className: "objectBox objectBox-symbol" }, `Symbol(${ name || "" })`);
+ }
+ });
+
+ function supportsObject(object, type) {
+ return type == "symbol";
+ }
+
+ // Exports from this module
+ exports.SymbolRep = {
+ rep: SymbolRep,
+ supportsObject: supportsObject
+ };
+ }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+/***/ },
+/* 46 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_RESULT__;/* -*- 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";
+
+ // Make this available to both AMD and CJS environments
+
+ !(__WEBPACK_AMD_DEFINE_RESULT__ = function (require, exports, module) {
+ // ReactJS
+ var React = __webpack_require__(2);
+
+ // Reps
+
+ var _require = __webpack_require__(35);
+
+ var createFactories = _require.createFactories;
+ var isGrip = _require.isGrip;
+
+ var _require2 = __webpack_require__(38);
+
+ var StringRep = _require2.StringRep;
+
+ // Shortcuts
+
+ var span = React.DOM.span;
+
+ var _createFactories = createFactories(StringRep);
+
+ var StringRepFactory = _createFactories.rep;
+
+ /**
+ * Renders DOM attribute
+ */
+
+ var Attribute = React.createClass({
+ displayName: "Attr",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired
+ },
+
+ getTitle: function (grip) {
+ return grip.preview.nodeName;
+ },
+
+ render: function () {
+ var grip = this.props.object;
+ var value = grip.preview.value;
+ var objectLink = this.props.objectLink || span;
+
+ return objectLink({ className: "objectLink-Attr" }, span({}, span({ className: "attrTitle" }, this.getTitle(grip)), span({ className: "attrEqual" }, "="), StringRepFactory({ object: value })));
+ }
+ });
+
+ // Registration
+
+ function supportsObject(grip, type) {
+ if (!isGrip(grip)) {
+ return false;
+ }
+
+ return type == "Attr" && grip.preview;
+ }
+
+ exports.Attribute = {
+ rep: Attribute,
+ supportsObject: supportsObject
+ };
+ }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+/***/ },
+/* 47 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_RESULT__;/* -*- 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";
+
+ // Make this available to both AMD and CJS environments
+
+ !(__WEBPACK_AMD_DEFINE_RESULT__ = function (require, exports, module) {
+ // ReactJS
+ var React = __webpack_require__(2);
+
+ // Reps
+
+ var _require = __webpack_require__(35);
+
+ var isGrip = _require.isGrip;
+
+ // Shortcuts
+
+ var span = React.DOM.span;
+
+ /**
+ * Used to render JS built-in Date() object.
+ */
+
+ var DateTime = React.createClass({
+ displayName: "Date",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired
+ },
+
+ getTitle: function (grip) {
+ if (this.props.objectLink) {
+ return this.props.objectLink({
+ object: grip
+ }, grip.class + " ");
+ }
+ return "";
+ },
+
+ render: function () {
+ var grip = this.props.object;
+ return span({ className: "objectBox" }, this.getTitle(grip), span({ className: "Date" }, new Date(grip.preview.timestamp).toISOString()));
+ }
+ });
+
+ // Registration
+
+ function supportsObject(grip, type) {
+ if (!isGrip(grip)) {
+ return false;
+ }
+
+ return type == "Date" && grip.preview;
+ }
+
+ // Exports from this module
+ exports.DateTime = {
+ rep: DateTime,
+ supportsObject: supportsObject
+ };
+ }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+/***/ },
+/* 48 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_RESULT__;/* -*- 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";
+
+ // Make this available to both AMD and CJS environments
+
+ !(__WEBPACK_AMD_DEFINE_RESULT__ = function (require, exports, module) {
+ // ReactJS
+ var React = __webpack_require__(2);
+
+ // Reps
+
+ var _require = __webpack_require__(35);
+
+ var isGrip = _require.isGrip;
+ var getURLDisplayString = _require.getURLDisplayString;
+
+ // Shortcuts
+
+ var span = React.DOM.span;
+
+ /**
+ * Renders DOM document object.
+ */
+
+ var Document = React.createClass({
+ displayName: "Document",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired
+ },
+
+ getLocation: function (grip) {
+ var location = grip.preview.location;
+ return location ? getURLDisplayString(location) : "";
+ },
+
+ getTitle: function (grip) {
+ if (this.props.objectLink) {
+ return span({ className: "objectBox" }, this.props.objectLink({
+ object: grip
+ }, grip.class + " "));
+ }
+ return "";
+ },
+
+ getTooltip: function (doc) {
+ return doc.location.href;
+ },
+
+ render: function () {
+ var grip = this.props.object;
+
+ return span({ className: "objectBox objectBox-object" }, this.getTitle(grip), span({ className: "objectPropValue" }, this.getLocation(grip)));
+ }
+ });
+
+ // Registration
+
+ function supportsObject(object, type) {
+ if (!isGrip(object)) {
+ return false;
+ }
+
+ return object.preview && type == "HTMLDocument";
+ }
+
+ // Exports from this module
+ exports.Document = {
+ rep: Document,
+ supportsObject: supportsObject
+ };
+ }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+/***/ },
+/* 49 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_RESULT__;/* -*- 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";
+
+ // Make this available to both AMD and CJS environments
+
+ !(__WEBPACK_AMD_DEFINE_RESULT__ = function (require, exports, module) {
+ // ReactJS
+ var React = __webpack_require__(2);
+
+ // Reps
+
+ var _require = __webpack_require__(35);
+
+ var createFactories = _require.createFactories;
+ var isGrip = _require.isGrip;
+
+ var _createFactories = createFactories(__webpack_require__(44).Grip);
+
+ var rep = _createFactories.rep;
+
+ /**
+ * Renders DOM event objects.
+ */
+
+ var Event = React.createClass({
+ displayName: "event",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired
+ },
+
+ render: function () {
+ // Use `Object.assign` to keep `this.props` without changes because:
+ // 1. JSON.stringify/JSON.parse is slow.
+ // 2. Immutable.js is planned for the future.
+ var props = Object.assign({}, this.props);
+ props.object = Object.assign({}, this.props.object);
+ props.object.preview = Object.assign({}, this.props.object.preview);
+ props.object.preview.ownProperties = props.object.preview.properties;
+ delete props.object.preview.properties;
+ props.object.ownPropertyLength = Object.keys(props.object.preview.ownProperties).length;
+
+ switch (props.object.class) {
+ case "MouseEvent":
+ props.isInterestingProp = (type, value, name) => {
+ return name == "clientX" || name == "clientY" || name == "layerX" || name == "layerY";
+ };
+ break;
+ case "KeyboardEvent":
+ props.isInterestingProp = (type, value, name) => {
+ return name == "key" || name == "charCode" || name == "keyCode";
+ };
+ break;
+ case "MessageEvent":
+ props.isInterestingProp = (type, value, name) => {
+ return name == "isTrusted" || name == "data";
+ };
+ break;
+ }
+ return rep(props);
+ }
+ });
+
+ // Registration
+
+ function supportsObject(grip, type) {
+ if (!isGrip(grip)) {
+ return false;
+ }
+
+ return grip.preview && grip.preview.kind == "DOMEvent";
+ }
+
+ // Exports from this module
+ exports.Event = {
+ rep: Event,
+ supportsObject: supportsObject
+ };
+ }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+/***/ },
+/* 50 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_RESULT__;/* -*- 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";
+
+ // Make this available to both AMD and CJS environments
+
+ !(__WEBPACK_AMD_DEFINE_RESULT__ = function (require, exports, module) {
+ // ReactJS
+ var React = __webpack_require__(2);
+
+ // Reps
+
+ var _require = __webpack_require__(35);
+
+ var isGrip = _require.isGrip;
+ var cropString = _require.cropString;
+
+ // Shortcuts
+
+ var span = React.DOM.span;
+
+ /**
+ * This component represents a template for Function objects.
+ */
+
+ var Func = React.createClass({
+ displayName: "Func",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired
+ },
+
+ getTitle: function (grip) {
+ if (this.props.objectLink) {
+ return this.props.objectLink({
+ object: grip
+ }, "function ");
+ }
+ return "";
+ },
+
+ summarizeFunction: function (grip) {
+ var name = grip.userDisplayName || grip.displayName || grip.name || "function";
+ return cropString(name + "()", 100);
+ },
+
+ render: function () {
+ var grip = this.props.object;
+
+ return span({ className: "objectBox objectBox-function" }, this.getTitle(grip), this.summarizeFunction(grip));
+ }
+ });
+
+ // Registration
+
+ function supportsObject(grip, type) {
+ if (!isGrip(grip)) {
+ return type == "function";
+ }
+
+ return type == "Function";
+ }
+
+ // Exports from this module
+
+ exports.Func = {
+ rep: Func,
+ supportsObject: supportsObject
+ };
+ }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+/***/ },
+/* 51 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_RESULT__;/* -*- 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";
+
+ // Make this available to both AMD and CJS environments
+
+ !(__WEBPACK_AMD_DEFINE_RESULT__ = function (require, exports, module) {
+ // ReactJS
+ var React = __webpack_require__(2);
+
+ // Reps
+
+ var _require = __webpack_require__(35);
+
+ var isGrip = _require.isGrip;
+
+ // Shortcuts
+
+ var span = React.DOM.span;
+
+ /**
+ * Renders a grip object with regular expression.
+ */
+
+ var RegExp = React.createClass({
+ displayName: "regexp",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired
+ },
+
+ getSource: function (grip) {
+ return grip.displayString;
+ },
+
+ render: function () {
+ var grip = this.props.object;
+ var objectLink = this.props.objectLink || span;
+
+ return span({ className: "objectBox objectBox-regexp" }, objectLink({
+ object: grip,
+ className: "regexpSource"
+ }, this.getSource(grip)));
+ }
+ });
+
+ // Registration
+
+ function supportsObject(object, type) {
+ if (!isGrip(object)) {
+ return false;
+ }
+
+ return type == "RegExp";
+ }
+
+ // Exports from this module
+ exports.RegExp = {
+ rep: RegExp,
+ supportsObject: supportsObject
+ };
+ }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+/***/ },
+/* 52 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_RESULT__;/* -*- 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";
+
+ // Make this available to both AMD and CJS environments
+
+ !(__WEBPACK_AMD_DEFINE_RESULT__ = function (require, exports, module) {
+ // ReactJS
+ var React = __webpack_require__(2);
+
+ // Reps
+
+ var _require = __webpack_require__(35);
+
+ var isGrip = _require.isGrip;
+ var getURLDisplayString = _require.getURLDisplayString;
+
+ // Shortcuts
+
+ var DOM = React.DOM;
+
+ /**
+ * Renders a grip representing CSSStyleSheet
+ */
+ var StyleSheet = React.createClass({
+ displayName: "object",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired
+ },
+
+ getTitle: function (grip) {
+ var title = "StyleSheet ";
+ if (this.props.objectLink) {
+ return DOM.span({ className: "objectBox" }, this.props.objectLink({
+ object: grip
+ }, title));
+ }
+ return title;
+ },
+
+ getLocation: function (grip) {
+ // Embedded stylesheets don't have URL and so, no preview.
+ var url = grip.preview ? grip.preview.url : "";
+ return url ? getURLDisplayString(url) : "";
+ },
+
+ render: function () {
+ var grip = this.props.object;
+
+ return DOM.span({ className: "objectBox objectBox-object" }, this.getTitle(grip), DOM.span({ className: "objectPropValue" }, this.getLocation(grip)));
+ }
+ });
+
+ // Registration
+
+ function supportsObject(object, type) {
+ if (!isGrip(object)) {
+ return false;
+ }
+
+ return type == "CSSStyleSheet";
+ }
+
+ // Exports from this module
+
+ exports.StyleSheet = {
+ rep: StyleSheet,
+ supportsObject: supportsObject
+ };
+ }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+/***/ },
+/* 53 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_RESULT__;/* -*- 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";
+
+ // Make this available to both AMD and CJS environments
+
+ !(__WEBPACK_AMD_DEFINE_RESULT__ = function (require, exports, module) {
+ // ReactJS
+ var React = __webpack_require__(2);
+
+ // Reps
+
+ var _require = __webpack_require__(35);
+
+ var isGrip = _require.isGrip;
+ var cropMultipleLines = _require.cropMultipleLines;
+
+ // Shortcuts
+
+ var DOM = React.DOM;
+
+ /**
+ * Renders DOM #text node.
+ */
+ var TextNode = React.createClass({
+ displayName: "TextNode",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired,
+ mode: React.PropTypes.string
+ },
+
+ getTextContent: function (grip) {
+ return cropMultipleLines(grip.preview.textContent);
+ },
+
+ getTitle: function (grip) {
+ if (this.props.objectLink) {
+ return this.props.objectLink({
+ object: grip
+ }, "#text");
+ }
+ return "";
+ },
+
+ render: function () {
+ var grip = this.props.object;
+ var mode = this.props.mode || "short";
+
+ if (mode == "short" || mode == "tiny") {
+ return DOM.span({ className: "objectBox objectBox-textNode" }, this.getTitle(grip), "\"" + this.getTextContent(grip) + "\"");
+ }
+
+ var objectLink = this.props.objectLink || DOM.span;
+ return DOM.span({ className: "objectBox objectBox-textNode" }, this.getTitle(grip), objectLink({
+ object: grip
+ }, "<"), DOM.span({ className: "nodeTag" }, "TextNode"), " textContent=\"", DOM.span({ className: "nodeValue" }, this.getTextContent(grip)), "\"", objectLink({
+ object: grip
+ }, ">;"));
+ }
+ });
+
+ // Registration
+
+ function supportsObject(grip, type) {
+ if (!isGrip(grip)) {
+ return false;
+ }
+
+ return grip.preview && grip.class == "Text";
+ }
+
+ // Exports from this module
+ exports.TextNode = {
+ rep: TextNode,
+ supportsObject: supportsObject
+ };
+ }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+/***/ },
+/* 54 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_RESULT__;/* -*- 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";
+
+ // Make this available to both AMD and CJS environments
+
+ !(__WEBPACK_AMD_DEFINE_RESULT__ = function (require, exports, module) {
+ // ReactJS
+ var React = __webpack_require__(2);
+
+ // Reps
+
+ var _require = __webpack_require__(35);
+
+ var isGrip = _require.isGrip;
+ var getURLDisplayString = _require.getURLDisplayString;
+
+ // Shortcuts
+
+ var DOM = React.DOM;
+
+ /**
+ * Renders a grip representing a window.
+ */
+ var Window = React.createClass({
+ displayName: "Window",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired,
+ mode: React.PropTypes.string
+ },
+
+ getTitle: function (grip) {
+ if (this.props.objectLink) {
+ return DOM.span({ className: "objectBox" }, this.props.objectLink({
+ object: grip
+ }, grip.class + " "));
+ }
+ return "";
+ },
+
+ getLocation: function (grip) {
+ return getURLDisplayString(grip.preview.url);
+ },
+
+ getDisplayValue: function (grip) {
+ if (this.props.mode === "tiny") {
+ return grip.isGlobal ? "Global" : "Window";
+ } else {
+ return this.getLocation(grip);
+ }
+ },
+
+ render: function () {
+ var grip = this.props.object;
+
+ return DOM.span({ className: "objectBox objectBox-Window" }, this.getTitle(grip), DOM.span({ className: "objectPropValue" }, this.getDisplayValue(grip)));
+ }
+ });
+
+ // Registration
+
+ function supportsObject(object, type) {
+ if (!isGrip(object)) {
+ return false;
+ }
+
+ return object.preview && type == "Window";
+ }
+
+ // Exports from this module
+ exports.Window = {
+ rep: Window,
+ supportsObject: supportsObject
+ };
+ }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+/***/ },
+/* 55 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_RESULT__;/* -*- 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";
+
+ // Make this available to both AMD and CJS environments
+
+ !(__WEBPACK_AMD_DEFINE_RESULT__ = function (require, exports, module) {
+ // ReactJS
+ var React = __webpack_require__(2);
+
+ // Reps
+
+ var _require = __webpack_require__(35);
+
+ var isGrip = _require.isGrip;
+
+ // Shortcuts
+
+ var span = React.DOM.span;
+
+ /**
+ * Renders a grip object with textual data.
+ */
+
+ var ObjectWithText = React.createClass({
+ displayName: "ObjectWithText",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired
+ },
+
+ getTitle: function (grip) {
+ if (this.props.objectLink) {
+ return span({ className: "objectBox" }, this.props.objectLink({
+ object: grip
+ }, this.getType(grip) + " "));
+ }
+ return "";
+ },
+
+ getType: function (grip) {
+ return grip.class;
+ },
+
+ getDescription: function (grip) {
+ return "\"" + grip.preview.text + "\"";
+ },
+
+ render: function () {
+ var grip = this.props.object;
+ return span({ className: "objectBox objectBox-" + this.getType(grip) }, this.getTitle(grip), span({ className: "objectPropValue" }, this.getDescription(grip)));
+ }
+ });
+
+ // Registration
+
+ function supportsObject(grip, type) {
+ if (!isGrip(grip)) {
+ return false;
+ }
+
+ return grip.preview && grip.preview.kind == "ObjectWithText";
+ }
+
+ // Exports from this module
+ exports.ObjectWithText = {
+ rep: ObjectWithText,
+ supportsObject: supportsObject
+ };
+ }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+/***/ },
+/* 56 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_RESULT__;/* -*- 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";
+
+ // Make this available to both AMD and CJS environments
+
+ !(__WEBPACK_AMD_DEFINE_RESULT__ = function (require, exports, module) {
+ // ReactJS
+ var React = __webpack_require__(2);
+
+ // Reps
+
+ var _require = __webpack_require__(35);
+
+ var isGrip = _require.isGrip;
+ var getURLDisplayString = _require.getURLDisplayString;
+
+ // Shortcuts
+
+ var span = React.DOM.span;
+
+ /**
+ * Renders a grip object with URL data.
+ */
+
+ var ObjectWithURL = React.createClass({
+ displayName: "ObjectWithURL",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired
+ },
+
+ getTitle: function (grip) {
+ if (this.props.objectLink) {
+ return span({ className: "objectBox" }, this.props.objectLink({
+ object: grip
+ }, this.getType(grip) + " "));
+ }
+ return "";
+ },
+
+ getType: function (grip) {
+ return grip.class;
+ },
+
+ getDescription: function (grip) {
+ return getURLDisplayString(grip.preview.url);
+ },
+
+ render: function () {
+ var grip = this.props.object;
+ return span({ className: "objectBox objectBox-" + this.getType(grip) }, this.getTitle(grip), span({ className: "objectPropValue" }, this.getDescription(grip)));
+ }
+ });
+
+ // Registration
+
+ function supportsObject(grip, type) {
+ if (!isGrip(grip)) {
+ return false;
+ }
+
+ return grip.preview && grip.preview.kind == "ObjectWithURL";
+ }
+
+ // Exports from this module
+ exports.ObjectWithURL = {
+ rep: ObjectWithURL,
+ supportsObject: supportsObject
+ };
+ }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+/***/ },
+/* 57 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_RESULT__;/* -*- 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";
+
+ // Make this available to both AMD and CJS environments
+
+ !(__WEBPACK_AMD_DEFINE_RESULT__ = function (require, exports, module) {
+ // Dependencies
+ var React = __webpack_require__(2);
+
+ var _require = __webpack_require__(35);
+
+ var createFactories = _require.createFactories;
+ var isGrip = _require.isGrip;
+
+ var _createFactories = createFactories(__webpack_require__(41));
+
+ var Caption = _createFactories.Caption;
+
+ // Shortcuts
+
+ var span = React.DOM.span;
+
+ /**
+ * Renders an array. The array is enclosed by left and right bracket
+ * and the max number of rendered items depends on the current mode.
+ */
+
+ var GripArray = React.createClass({
+ displayName: "GripArray",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired,
+ mode: React.PropTypes.string,
+ provider: React.PropTypes.object
+ },
+
+ getLength: function (grip) {
+ return grip.preview ? grip.preview.length : 0;
+ },
+
+ getTitle: function (object, context) {
+ var objectLink = this.props.objectLink || span;
+ if (this.props.mode != "tiny") {
+ return objectLink({
+ object: object
+ }, object.class + " ");
+ }
+ return "";
+ },
+
+ arrayIterator: function (grip, max) {
+ var items = [];
+
+ if (!grip.preview || !grip.preview.length) {
+ return items;
+ }
+
+ var array = grip.preview.items;
+ if (!array) {
+ return items;
+ }
+
+ var delim = void 0;
+ // number of grip.preview.items is limited to 10, but we may have more
+ // items in grip-array
+ var delimMax = grip.preview.length > array.length ? array.length : array.length - 1;
+ var provider = this.props.provider;
+
+ for (var i = 0; i < array.length && i < max; i++) {
+ try {
+ var itemGrip = array[i];
+ var value = provider ? provider.getValue(itemGrip) : itemGrip;
+
+ delim = i == delimMax ? "" : ", ";
+
+ items.push(GripArrayItem(Object.assign({}, this.props, {
+ key: i,
+ object: value,
+ delim: delim })));
+ } catch (exc) {
+ items.push(GripArrayItem(Object.assign({}, this.props, {
+ object: exc,
+ delim: delim,
+ key: i })));
+ }
+ }
+ if (array.length > max || grip.preview.length > array.length) {
+ var objectLink = this.props.objectLink || span;
+ var leftItemNum = grip.preview.length - max > 0 ? grip.preview.length - max : grip.preview.length - array.length;
+ items.push(Caption({
+ key: "more",
+ object: objectLink({
+ object: this.props.object
+ }, leftItemNum + " more…")
+ }));
+ }
+
+ return items;
+ },
+
+ render: function () {
+ var mode = this.props.mode || "short";
+ var object = this.props.object;
+
+ var items = void 0;
+ var brackets = void 0;
+ var needSpace = function (space) {
+ return space ? { left: "[ ", right: " ]" } : { left: "[", right: "]" };
+ };
+
+ if (mode == "tiny") {
+ var objectLength = this.getLength(object);
+ var isEmpty = objectLength === 0;
+ items = span({ className: "length" }, isEmpty ? "" : objectLength);
+ brackets = needSpace(false);
+ } else {
+ var max = mode == "short" ? 3 : 300;
+ items = this.arrayIterator(object, max);
+ brackets = needSpace(items.length > 0);
+ }
+
+ var objectLink = this.props.objectLink || span;
+ var title = this.getTitle(object);
+
+ return span({
+ className: "objectBox objectBox-array" }, title, objectLink({
+ className: "arrayLeftBracket",
+ object: object
+ }, brackets.left), items, objectLink({
+ className: "arrayRightBracket",
+ object: object
+ }, brackets.right), span({
+ className: "arrayProperties",
+ role: "group" }));
+ }
+ });
+
+ /**
+ * Renders array item. Individual values are separated by
+ * a delimiter (a comma by default).
+ */
+ var GripArrayItem = React.createFactory(React.createClass({
+ displayName: "GripArrayItem",
+
+ propTypes: {
+ delim: React.PropTypes.string
+ },
+
+ render: function () {
+ var _createFactories2 = createFactories(__webpack_require__(34));
+
+ var Rep = _createFactories2.Rep;
+
+
+ return span({}, Rep(Object.assign({}, this.props, {
+ mode: "tiny"
+ })), this.props.delim);
+ }
+ }));
+
+ function supportsObject(grip, type) {
+ if (!isGrip(grip)) {
+ return false;
+ }
+
+ return grip.preview && grip.preview.kind == "ArrayLike";
+ }
+
+ // Exports from this module
+ exports.GripArray = {
+ rep: GripArray,
+ supportsObject: supportsObject
+ };
+ }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+/***/ },
+/* 58 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_RESULT__;/* -*- 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";
+ // Make this available to both AMD and CJS environments
+
+ var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }();
+
+ !(__WEBPACK_AMD_DEFINE_RESULT__ = function (require, exports, module) {
+ // Dependencies
+ var React = __webpack_require__(2);
+
+ var _require = __webpack_require__(35);
+
+ var createFactories = _require.createFactories;
+ var isGrip = _require.isGrip;
+
+ var _createFactories = createFactories(__webpack_require__(41));
+
+ var Caption = _createFactories.Caption;
+
+ var _createFactories2 = createFactories(__webpack_require__(43));
+
+ var PropRep = _createFactories2.PropRep;
+
+ // Shortcuts
+
+ var span = React.DOM.span;
+ /**
+ * Renders an map. A map is represented by a list of its
+ * entries enclosed in curly brackets.
+ */
+
+ var GripMap = React.createClass({
+ displayName: "GripMap",
+
+ propTypes: {
+ object: React.PropTypes.object,
+ mode: React.PropTypes.string
+ },
+
+ getTitle: function (object) {
+ var title = object && object.class ? object.class : "Map";
+ if (this.props.objectLink) {
+ return this.props.objectLink({
+ object: object
+ }, title);
+ }
+ return title;
+ },
+
+ safeEntriesIterator: function (object, max) {
+ max = typeof max === "undefined" ? 3 : max;
+ try {
+ return this.entriesIterator(object, max);
+ } catch (err) {
+ console.error(err);
+ }
+ return [];
+ },
+
+ entriesIterator: function (object, max) {
+ // Entry filter. Show only interesting entries to the user.
+ var isInterestingEntry = this.props.isInterestingEntry || ((type, value) => {
+ return type == "boolean" || type == "number" || type == "string" && value.length != 0;
+ });
+
+ var mapEntries = object.preview && object.preview.entries ? object.preview.entries : [];
+
+ var indexes = this.getEntriesIndexes(mapEntries, max, isInterestingEntry);
+ if (indexes.length < max && indexes.length < mapEntries.length) {
+ // There are not enough entries yet, so we add uninteresting entries.
+ indexes = indexes.concat(this.getEntriesIndexes(mapEntries, max - indexes.length, (t, value, name) => {
+ return !isInterestingEntry(t, value, name);
+ }));
+ }
+
+ var entries = this.getEntries(mapEntries, indexes);
+ if (entries.length < mapEntries.length) {
+ // There are some undisplayed entries. Then display "more…".
+ var objectLink = this.props.objectLink || span;
+
+ entries.push(Caption({
+ key: "more",
+ object: objectLink({
+ object: object
+ }, `${ mapEntries.length - max } more…`)
+ }));
+ }
+
+ return entries;
+ },
+
+ /**
+ * Get entries ordered by index.
+ *
+ * @param {Array} entries Entries array.
+ * @param {Array} indexes Indexes of entries.
+ * @return {Array} Array of PropRep.
+ */
+ getEntries: function (entries, indexes) {
+ // Make indexes ordered by ascending.
+ indexes.sort(function (a, b) {
+ return a - b;
+ });
+
+ return indexes.map((index, i) => {
+ var _entries$index = _slicedToArray(entries[index], 2);
+
+ var key = _entries$index[0];
+ var entryValue = _entries$index[1];
+
+ var value = entryValue.value !== undefined ? entryValue.value : entryValue;
+
+ return PropRep({
+ // key,
+ name: key,
+ equal: ": ",
+ object: value,
+ // Do not add a trailing comma on the last entry
+ // if there won't be a "more..." item.
+ delim: i < indexes.length - 1 || indexes.length < entries.length ? ", " : "",
+ mode: "tiny",
+ objectLink: this.props.objectLink
+ });
+ });
+ },
+
+ /**
+ * Get the indexes of entries in the map.
+ *
+ * @param {Array} entries Entries array.
+ * @param {Number} max The maximum length of indexes array.
+ * @param {Function} filter Filter the entry you want.
+ * @return {Array} Indexes of filtered entries in the map.
+ */
+ getEntriesIndexes: function (entries, max, filter) {
+ return entries.reduce((indexes, _ref, i) => {
+ var _ref2 = _slicedToArray(_ref, 2);
+
+ var key = _ref2[0];
+ var entry = _ref2[1];
+
+ if (indexes.length < max) {
+ var value = entry && entry.value !== undefined ? entry.value : entry;
+ // Type is specified in grip's "class" field and for primitive
+ // values use typeof.
+ var type = (value && value.class ? value.class : typeof value).toLowerCase();
+
+ if (filter(type, value, key)) {
+ indexes.push(i);
+ }
+ }
+
+ return indexes;
+ }, []);
+ },
+
+ render: function () {
+ var object = this.props.object;
+ var props = this.safeEntriesIterator(object, this.props.mode == "long" ? 100 : 3);
+
+ var objectLink = this.props.objectLink || span;
+ if (this.props.mode == "tiny") {
+ return span({ className: "objectBox objectBox-object" }, this.getTitle(object), objectLink({
+ className: "objectLeftBrace",
+ object: object
+ }, ""));
+ }
+
+ return span({ className: "objectBox objectBox-object" }, this.getTitle(object), objectLink({
+ className: "objectLeftBrace",
+ object: object
+ }, " { "), props, objectLink({
+ className: "objectRightBrace",
+ object: object
+ }, " }"));
+ }
+ });
+
+ function supportsObject(grip, type) {
+ if (!isGrip(grip)) {
+ return false;
+ }
+ return grip.preview && grip.preview.kind == "MapLike";
+ }
+
+ // Exports from this module
+ exports.GripMap = {
+ rep: GripMap,
+ supportsObject: supportsObject
+ };
+ }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+/***/ },
+/* 59 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /**
+ * Copyright (c) 2007-2016, Alexandru Marasteanu <hello [at) alexei (dot] ro>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * * Neither the name of this software nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ */
+
+ /* globals window, exports, define */
+
+ (function (window) {
+ 'use strict';
+
+ var re = {
+ not_string: /[^s]/,
+ not_bool: /[^t]/,
+ not_type: /[^T]/,
+ not_primitive: /[^v]/,
+ number: /[diefg]/,
+ numeric_arg: /bcdiefguxX/,
+ json: /[j]/,
+ not_json: /[^j]/,
+ text: /^[^\x25]+/,
+ modulo: /^\x25{2}/,
+ placeholder: /^\x25(?:([1-9]\d*)\$|\(([^\)]+)\))?(\+)?(0|'[^$])?(-)?(\d+)?(?:\.(\d+))?([b-gijosStTuvxX])/,
+ key: /^([a-z_][a-z_\d]*)/i,
+ key_access: /^\.([a-z_][a-z_\d]*)/i,
+ index_access: /^\[(\d+)\]/,
+ sign: /^[\+\-]/
+ };
+
+ function sprintf() {
+ var key = arguments[0],
+ cache = sprintf.cache;
+ if (!(cache[key] && cache.hasOwnProperty(key))) {
+ cache[key] = sprintf.parse(key);
+ }
+ return sprintf.format.call(null, cache[key], arguments);
+ }
+
+ sprintf.format = function (parse_tree, argv) {
+ var cursor = 1,
+ tree_length = parse_tree.length,
+ node_type = '',
+ arg,
+ output = [],
+ i,
+ k,
+ match,
+ pad,
+ pad_character,
+ pad_length,
+ is_positive = true,
+ sign = '';
+ for (i = 0; i < tree_length; i++) {
+ node_type = get_type(parse_tree[i]);
+ if (node_type === 'string') {
+ output[output.length] = parse_tree[i];
+ } else if (node_type === 'array') {
+ match = parse_tree[i]; // convenience purposes only
+ if (match[2]) {
+ // keyword argument
+ arg = argv[cursor];
+ for (k = 0; k < match[2].length; k++) {
+ if (!arg.hasOwnProperty(match[2][k])) {
+ throw new Error(sprintf('[sprintf] property "%s" does not exist', match[2][k]));
+ }
+ arg = arg[match[2][k]];
+ }
+ } else if (match[1]) {
+ // positional argument (explicit)
+ arg = argv[match[1]];
+ } else {
+ // positional argument (implicit)
+ arg = argv[cursor++];
+ }
+
+ if (re.not_type.test(match[8]) && re.not_primitive.test(match[8]) && get_type(arg) == 'function') {
+ arg = arg();
+ }
+
+ if (re.numeric_arg.test(match[8]) && get_type(arg) != 'number' && isNaN(arg)) {
+ throw new TypeError(sprintf("[sprintf] expecting number but found %s", get_type(arg)));
+ }
+
+ if (re.number.test(match[8])) {
+ is_positive = arg >= 0;
+ }
+
+ switch (match[8]) {
+ case 'b':
+ arg = parseInt(arg, 10).toString(2);
+ break;
+ case 'c':
+ arg = String.fromCharCode(parseInt(arg, 10));
+ break;
+ case 'd':
+ case 'i':
+ arg = parseInt(arg, 10);
+ break;
+ case 'j':
+ arg = JSON.stringify(arg, null, match[6] ? parseInt(match[6]) : 0);
+ break;
+ case 'e':
+ arg = match[7] ? parseFloat(arg).toExponential(match[7]) : parseFloat(arg).toExponential();
+ break;
+ case 'f':
+ arg = match[7] ? parseFloat(arg).toFixed(match[7]) : parseFloat(arg);
+ break;
+ case 'g':
+ arg = match[7] ? parseFloat(arg).toPrecision(match[7]) : parseFloat(arg);
+ break;
+ case 'o':
+ arg = arg.toString(8);
+ break;
+ case 's':
+ case 'S':
+ arg = String(arg);
+ arg = match[7] ? arg.substring(0, match[7]) : arg;
+ break;
+ case 't':
+ arg = String(!!arg);
+ arg = match[7] ? arg.substring(0, match[7]) : arg;
+ break;
+ case 'T':
+ arg = get_type(arg);
+ arg = match[7] ? arg.substring(0, match[7]) : arg;
+ break;
+ case 'u':
+ arg = parseInt(arg, 10) >>> 0;
+ break;
+ case 'v':
+ arg = arg.valueOf();
+ arg = match[7] ? arg.substring(0, match[7]) : arg;
+ break;
+ case 'x':
+ arg = parseInt(arg, 10).toString(16);
+ break;
+ case 'X':
+ arg = parseInt(arg, 10).toString(16).toUpperCase();
+ break;
+ }
+ if (re.json.test(match[8])) {
+ output[output.length] = arg;
+ } else {
+ if (re.number.test(match[8]) && (!is_positive || match[3])) {
+ sign = is_positive ? '+' : '-';
+ arg = arg.toString().replace(re.sign, '');
+ } else {
+ sign = '';
+ }
+ pad_character = match[4] ? match[4] === '0' ? '0' : match[4].charAt(1) : ' ';
+ pad_length = match[6] - (sign + arg).length;
+ pad = match[6] ? pad_length > 0 ? str_repeat(pad_character, pad_length) : '' : '';
+ output[output.length] = match[5] ? sign + arg + pad : pad_character === '0' ? sign + pad + arg : pad + sign + arg;
+ }
+ }
+ }
+ return output.join('');
+ };
+
+ sprintf.cache = {};
+
+ sprintf.parse = function (fmt) {
+ var _fmt = fmt,
+ match = [],
+ parse_tree = [],
+ arg_names = 0;
+ while (_fmt) {
+ if ((match = re.text.exec(_fmt)) !== null) {
+ parse_tree[parse_tree.length] = match[0];
+ } else if ((match = re.modulo.exec(_fmt)) !== null) {
+ parse_tree[parse_tree.length] = '%';
+ } else if ((match = re.placeholder.exec(_fmt)) !== null) {
+ if (match[2]) {
+ arg_names |= 1;
+ var field_list = [],
+ replacement_field = match[2],
+ field_match = [];
+ if ((field_match = re.key.exec(replacement_field)) !== null) {
+ field_list[field_list.length] = field_match[1];
+ while ((replacement_field = replacement_field.substring(field_match[0].length)) !== '') {
+ if ((field_match = re.key_access.exec(replacement_field)) !== null) {
+ field_list[field_list.length] = field_match[1];
+ } else if ((field_match = re.index_access.exec(replacement_field)) !== null) {
+ field_list[field_list.length] = field_match[1];
+ } else {
+ throw new SyntaxError("[sprintf] failed to parse named argument key");
+ }
+ }
+ } else {
+ throw new SyntaxError("[sprintf] failed to parse named argument key");
+ }
+ match[2] = field_list;
+ } else {
+ arg_names |= 2;
+ }
+ if (arg_names === 3) {
+ throw new Error("[sprintf] mixing positional and named placeholders is not (yet) supported");
+ }
+ parse_tree[parse_tree.length] = match;
+ } else {
+ throw new SyntaxError("[sprintf] unexpected placeholder");
+ }
+ _fmt = _fmt.substring(match[0].length);
+ }
+ return parse_tree;
+ };
+
+ var vsprintf = function (fmt, argv, _argv) {
+ _argv = (argv || []).slice(0);
+ _argv.splice(0, 0, fmt);
+ return sprintf.apply(null, _argv);
+ };
+
+ /**
+ * helpers
+ */
+ function get_type(variable) {
+ if (typeof variable === 'number') {
+ return 'number';
+ } else if (typeof variable === 'string') {
+ return 'string';
+ } else {
+ return Object.prototype.toString.call(variable).slice(8, -1).toLowerCase();
+ }
+ }
+
+ var preformattedPadding = {
+ '0': ['', '0', '00', '000', '0000', '00000', '000000', '0000000'],
+ ' ': ['', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
+ '_': ['', '_', '__', '___', '____', '_____', '______', '_______']
+ };
+ function str_repeat(input, multiplier) {
+ if (multiplier >= 0 && multiplier <= 7 && preformattedPadding[input]) {
+ return preformattedPadding[input][multiplier];
+ }
+ return Array(multiplier + 1).join(input);
+ }
+
+ /**
+ * export to either browser or node.js
+ */
+ if (true) {
+ exports.sprintf = sprintf;
+ exports.vsprintf = vsprintf;
+ } else {
+ window.sprintf = sprintf;
+ window.vsprintf = vsprintf;
+
+ if (typeof define === 'function' && define.amd) {
+ define(function () {
+ return {
+ sprintf: sprintf,
+ vsprintf: vsprintf
+ };
+ });
+ }
+ }
+ })(typeof window === 'undefined' ? this : window);
+
+/***/ },
+/* 60 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /* 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/. */
+
+ /**
+ * EventEmitter.
+ */
+
+ var EventEmitter = function EventEmitter() {};
+ module.exports = EventEmitter;
+
+ var _require = __webpack_require__(61);
+
+ var Cu = _require.Cu;
+
+ var promise = __webpack_require__(66);
+
+ /**
+ * Decorate an object with event emitter functionality.
+ *
+ * @param Object aObjectToDecorate
+ * Bind all public methods of EventEmitter to
+ * the aObjectToDecorate object.
+ */
+ EventEmitter.decorate = function EventEmitter_decorate(aObjectToDecorate) {
+ var emitter = new EventEmitter();
+ aObjectToDecorate.on = emitter.on.bind(emitter);
+ aObjectToDecorate.off = emitter.off.bind(emitter);
+ aObjectToDecorate.once = emitter.once.bind(emitter);
+ aObjectToDecorate.emit = emitter.emit.bind(emitter);
+ };
+
+ EventEmitter.prototype = {
+ /**
+ * Connect a listener.
+ *
+ * @param string aEvent
+ * The event name to which we're connecting.
+ * @param function aListener
+ * Called when the event is fired.
+ */
+ on: function EventEmitter_on(aEvent, aListener) {
+ if (!this._eventEmitterListeners) this._eventEmitterListeners = new Map();
+ if (!this._eventEmitterListeners.has(aEvent)) {
+ this._eventEmitterListeners.set(aEvent, []);
+ }
+ this._eventEmitterListeners.get(aEvent).push(aListener);
+ },
+
+ /**
+ * Listen for the next time an event is fired.
+ *
+ * @param string aEvent
+ * The event name to which we're connecting.
+ * @param function aListener
+ * (Optional) Called when the event is fired. Will be called at most
+ * one time.
+ * @return promise
+ * A promise which is resolved when the event next happens. The
+ * resolution value of the promise is the first event argument. If
+ * you need access to second or subsequent event arguments (it's rare
+ * that this is needed) then use aListener
+ */
+ once: function EventEmitter_once(aEvent, aListener) {
+ var _this = this;
+
+ var deferred = promise.defer();
+
+ var handler = function (aEvent, aFirstArg) {
+ for (var _len = arguments.length, aRest = Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) {
+ aRest[_key - 2] = arguments[_key];
+ }
+
+ _this.off(aEvent, handler);
+ if (aListener) {
+ aListener.apply(null, [aEvent, aFirstArg].concat(aRest));
+ }
+ deferred.resolve(aFirstArg);
+ };
+
+ handler._originalListener = aListener;
+ this.on(aEvent, handler);
+
+ return deferred.promise;
+ },
+
+ /**
+ * Remove a previously-registered event listener. Works for events
+ * registered with either on or once.
+ *
+ * @param string aEvent
+ * The event name whose listener we're disconnecting.
+ * @param function aListener
+ * The listener to remove.
+ */
+ off: function EventEmitter_off(aEvent, aListener) {
+ if (!this._eventEmitterListeners) return;
+ var listeners = this._eventEmitterListeners.get(aEvent);
+ if (listeners) {
+ this._eventEmitterListeners.set(aEvent, listeners.filter(l => {
+ return l !== aListener && l._originalListener !== aListener;
+ }));
+ }
+ },
+
+ /**
+ * Emit an event. All arguments to this method will
+ * be sent to listener functions.
+ */
+ emit: function EventEmitter_emit(aEvent) {
+ var _this2 = this,
+ _arguments = arguments;
+
+ if (!this._eventEmitterListeners || !this._eventEmitterListeners.has(aEvent)) {
+ return;
+ }
+
+ var originalListeners = this._eventEmitterListeners.get(aEvent);
+
+ var _loop = function (listener) {
+ // If the object was destroyed during event emission, stop
+ // emitting.
+ if (!_this2._eventEmitterListeners) {
+ return "break";
+ }
+
+ // If listeners were removed during emission, make sure the
+ // event handler we're going to fire wasn't removed.
+ if (originalListeners === _this2._eventEmitterListeners.get(aEvent) || _this2._eventEmitterListeners.get(aEvent).some(l => l === listener)) {
+ try {
+ listener.apply(null, _arguments);
+ } catch (ex) {
+ // Prevent a bad listener from interfering with the others.
+ var msg = ex + ": " + ex.stack;
+ //console.error(msg);
+ console.log(msg);
+ }
+ }
+ };
+
+ for (var listener of this._eventEmitterListeners.get(aEvent)) {
+ var _ret = _loop(listener);
+
+ if (_ret === "break") break;
+ }
+ }
+ };
+
+/***/ },
+/* 61 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /*
+ * A sham for https://developer.mozilla.org/en-US/Add-ons/SDK/Low-Level_APIs/chrome
+ */
+
+ var _require = __webpack_require__(62);
+
+ var inDOMUtils = _require.inDOMUtils;
+
+
+ var ourServices = {
+ inIDOMUtils: inDOMUtils,
+ nsIClipboardHelper: {
+ copyString: () => {}
+ },
+ nsIXULChromeRegistry: {
+ isLocaleRTL: () => {
+ return false;
+ }
+ },
+ nsIDOMParser: {}
+ };
+
+ module.exports = {
+ Cc: name => {
+ if (typeof console !== "undefined") {
+ console.log('Cc sham for', name);
+ }
+ return {
+ getService: name => ourServices[name],
+ createInstance: iface => ourServices[iface]
+ };
+ },
+ CC: (name, iface, method) => {
+ if (typeof console !== "undefined") {
+ console.log('CC sham for', name, iface, method);
+ }
+ return {};
+ },
+ Ci: {
+ nsIThread: {
+ "DISPATCH_NORMAL": 0,
+ "DISPATCH_SYNC": 1
+ },
+ nsIDOMNode: typeof HTMLElement !== "undefined" ? HTMLElement : null,
+ nsIFocusManager: {
+ MOVEFOCUS_BACKWARD: 2,
+ MOVEFOCUS_FORWARD: 1
+ },
+ nsIDOMKeyEvent: {},
+ nsIDOMCSSRule: { "UNKNOWN_RULE": 0, "STYLE_RULE": 1, "CHARSET_RULE": 2, "IMPORT_RULE": 3, "MEDIA_RULE": 4, "FONT_FACE_RULE": 5, "PAGE_RULE": 6, "KEYFRAMES_RULE": 7, "KEYFRAME_RULE": 8, "MOZ_KEYFRAMES_RULE": 7, "MOZ_KEYFRAME_RULE": 8, "NAMESPACE_RULE": 10, "COUNTER_STYLE_RULE": 11, "SUPPORTS_RULE": 12, "FONT_FEATURE_VALUES_RULE": 14 },
+ inIDOMUtils: "inIDOMUtils",
+ nsIClipboardHelper: "nsIClipboardHelper",
+ nsIXULChromeRegistry: "nsIXULChromeRegistry"
+ },
+ Cu: {
+ reportError: msg => {
+ typeof console !== "undefined" ? console.error(msg) : dump(msg);
+ },
+ callFunctionWithAsyncStack: fn => fn()
+ },
+ Cr: {},
+ components: {
+ isSuccessCode: () => (returnCode & 0x80000000) === 0
+ }
+ };
+
+/***/ },
+/* 62 */
+/***/ function(module, exports, __webpack_require__) {
+
+ // A sham for inDOMUtils.
+
+ "use strict";
+
+ var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }();
+
+ var _require = __webpack_require__(63);
+
+ var CSSLexer = _require.CSSLexer;
+
+ var _require2 = __webpack_require__(64);
+
+ var cssColors = _require2.cssColors;
+
+ var _require3 = __webpack_require__(65);
+
+ var cssProperties = _require3.cssProperties;
+
+
+ var cssRGBMap;
+
+ // From inIDOMUtils.idl.
+ var EXCLUDE_SHORTHANDS = 1 << 0;
+ var INCLUDE_ALIASES = 1 << 1;
+ var TYPE_LENGTH = 0;
+ var TYPE_PERCENTAGE = 1;
+ var TYPE_COLOR = 2;
+ var TYPE_URL = 3;
+ var TYPE_ANGLE = 4;
+ var TYPE_FREQUENCY = 5;
+ var TYPE_TIME = 6;
+ var TYPE_GRADIENT = 7;
+ var TYPE_TIMING_FUNCTION = 8;
+ var TYPE_IMAGE_RECT = 9;
+ var TYPE_NUMBER = 10;
+
+ function getCSSLexer(text) {
+ return new CSSLexer(text);
+ }
+
+ function rgbToColorName(r, g, b) {
+ if (!cssRGBMap) {
+ cssRGBMap = new Map();
+ for (var name in cssColors) {
+ cssRGBMap.set(JSON.stringify(cssColors[name]), name);
+ }
+ }
+ var value = cssRGBMap.get(JSON.stringify([r, g, b]));
+ if (!value) {
+ throw new Error("no such color");
+ }
+ return value;
+ }
+
+ // Taken from dom/tests/mochitest/ajax/mochikit/MochiKit/Color.js
+ function _hslValue(n1, n2, hue) {
+ if (hue > 6.0) {
+ hue -= 6.0;
+ } else if (hue < 0.0) {
+ hue += 6.0;
+ }
+ var val;
+ if (hue < 1.0) {
+ val = n1 + (n2 - n1) * hue;
+ } else if (hue < 3.0) {
+ val = n2;
+ } else if (hue < 4.0) {
+ val = n1 + (n2 - n1) * (4.0 - hue);
+ } else {
+ val = n1;
+ }
+ return val;
+ }
+
+ // Taken from dom/tests/mochitest/ajax/mochikit/MochiKit/Color.js
+ // and then modified.
+ function hslToRGB(_ref) {
+ var _ref2 = _slicedToArray(_ref, 3);
+
+ var hue = _ref2[0];
+ var saturation = _ref2[1];
+ var lightness = _ref2[2];
+
+ var red;
+ var green;
+ var blue;
+ if (saturation === 0) {
+ red = lightness;
+ green = lightness;
+ blue = lightness;
+ } else {
+ var m2;
+ if (lightness <= 0.5) {
+ m2 = lightness * (1.0 + saturation);
+ } else {
+ m2 = lightness + saturation - lightness * saturation;
+ }
+ var m1 = 2.0 * lightness - m2;
+ var f = _hslValue;
+ var h6 = hue * 6.0;
+ red = f(m1, m2, h6 + 2);
+ green = f(m1, m2, h6);
+ blue = f(m1, m2, h6 - 2);
+ }
+ return [red, green, blue];
+ }
+
+ function colorToRGBA(name) {
+ name = name.trim().toLowerCase();
+ if (name in cssColors) {
+ return cssColors[name];
+ }
+
+ if (name === "transparent") {
+ return [0, 0, 0, 0];
+ }
+
+ var lexer = getCSSLexer(name);
+
+ var getToken = function () {
+ while (true) {
+ var token = lexer.nextToken();
+ if (!token || token.tokenType !== "comment" || token.tokenType !== "whitespace") {
+ return token;
+ }
+ }
+ };
+
+ var requireComma = function (token) {
+ if (token.tokenType !== "symbol" || token.text !== ",") {
+ return null;
+ }
+ return getToken();
+ };
+
+ var func = getToken();
+ if (!func || func.tokenType !== "function") {
+ return null;
+ }
+ var alpha = false;
+ if (func.text === "rgb" || func.text === "hsl") {
+ // Nothing.
+ } else if (func.text === "rgba" || func.text === "hsla") {
+ alpha = true;
+ } else {
+ return null;
+ }
+
+ var vals = [];
+ for (var i = 0; i < 3; ++i) {
+ var token = getToken();
+ if (i > 0) {
+ token = requireComma(token);
+ }
+ if (token.tokenType !== "number" || !token.isInteger) {
+ return null;
+ }
+ var num = token.number;
+ if (num < 0) {
+ num = 0;
+ } else if (num > 255) {
+ num = 255;
+ }
+ vals.push(num);
+ }
+
+ if (func.text === "hsl" || func.text === "hsla") {
+ vals = hslToRGB(vals);
+ }
+
+ if (alpha) {
+ var _token = requireComma(getToken());
+ if (_token.tokenType !== "number") {
+ return null;
+ }
+ var _num = _token.number;
+ if (_num < 0) {
+ _num = 0;
+ } else if (_num > 1) {
+ _num = 1;
+ }
+ vals.push(_num);
+ } else {
+ vals.push(1);
+ }
+
+ var parenToken = getToken();
+ if (!parenToken || parenToken.tokenType !== "symbol" || parenToken.text !== ")") {
+ return null;
+ }
+ if (getToken() !== null) {
+ return null;
+ }
+
+ return vals;
+ }
+
+ function isValidCSSColor(name) {
+ return colorToRGBA(name) !== null;
+ }
+
+ function isVariable(name) {
+ return name.startsWith("--");
+ }
+
+ function cssPropertyIsShorthand(name) {
+ if (isVariable(name)) {
+ return false;
+ }
+ if (!(name in cssProperties)) {
+ throw Error("unknown property " + name);
+ }
+ return !!cssProperties[name].subproperties;
+ }
+
+ function getSubpropertiesForCSSProperty(name) {
+ if (isVariable(name)) {
+ return [name];
+ }
+ if (!(name in cssProperties)) {
+ throw Error("unknown property " + name);
+ }
+ if ("subproperties" in cssProperties[name]) {
+ return cssProperties[name].subproperties.slice();
+ }
+ return [name];
+ }
+
+ function getCSSValuesForProperty(name) {
+ if (isVariable(name)) {
+ return ["initial", "inherit", "unset"];
+ }
+ if (!(name in cssProperties)) {
+ throw Error("unknown property " + name);
+ }
+ return cssProperties[name].values.slice();
+ }
+
+ function getCSSPropertyNames(flags) {
+ var names = Object.keys(cssProperties);
+ if ((flags & EXCLUDE_SHORTHANDS) !== 0) {
+ names = names.filter(name => cssProperties[name].subproperties);
+ }
+ if ((flags & INCLUDE_ALIASES) === 0) {
+ names = names.filter(name => !cssProperties[name].alias);
+ }
+ return names;
+ }
+
+ function cssPropertySupportsType(name, type) {
+ if (isVariable(name)) {
+ return false;
+ }
+ if (!(name in cssProperties)) {
+ throw Error("unknown property " + name);
+ }
+ return (cssProperties[name].supports & 1 << type) !== 0;
+ }
+
+ function isInheritedProperty(name) {
+ if (isVariable(name)) {
+ return true;
+ }
+ if (!(name in cssProperties)) {
+ return false;
+ }
+ return cssProperties[name].inherited;
+ }
+
+ function cssPropertyIsValid(name, value) {
+ if (isVariable(name)) {
+ return true;
+ }
+ if (!(name in cssProperties)) {
+ return false;
+ }
+ var elt = document.createElement("div");
+ elt.style = name + ":" + value;
+ return elt.style.length > 0;
+ }
+
+ exports.inDOMUtils = {
+ getCSSLexer,
+ rgbToColorName,
+ colorToRGBA,
+ isValidCSSColor,
+ cssPropertyIsShorthand,
+ getSubpropertiesForCSSProperty,
+ getCSSValuesForProperty,
+ getCSSPropertyNames,
+ cssPropertySupportsType,
+ isInheritedProperty,
+ cssPropertyIsValid,
+
+ // Constants.
+ EXCLUDE_SHORTHANDS,
+ INCLUDE_ALIASES,
+ TYPE_LENGTH,
+ TYPE_PERCENTAGE,
+ TYPE_COLOR,
+ TYPE_URL,
+ TYPE_ANGLE,
+ TYPE_FREQUENCY,
+ TYPE_TIME,
+ TYPE_GRADIENT,
+ TYPE_TIMING_FUNCTION,
+ TYPE_IMAGE_RECT,
+ TYPE_NUMBER
+ };
+
+/***/ },
+/* 63 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;"use strict";
+
+ (function (root, factory) {
+ // Universal Module Definition (UMD) to support AMD, CommonJS/Node.js,
+ // Rhino, and plain browser loading.
+ if (true) {
+ !(__WEBPACK_AMD_DEFINE_ARRAY__ = [exports], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+ } else if (typeof exports !== 'undefined') {
+ factory(exports);
+ } else {
+ factory(root);
+ }
+ })(this, function (exports) {
+
+ function between(num, first, last) {
+ return num >= first && num <= last;
+ }
+ function digit(code) {
+ return between(code, 0x30, 0x39);
+ }
+ function hexdigit(code) {
+ return digit(code) || between(code, 0x41, 0x46) || between(code, 0x61, 0x66);
+ }
+ function uppercaseletter(code) {
+ return between(code, 0x41, 0x5a);
+ }
+ function lowercaseletter(code) {
+ return between(code, 0x61, 0x7a);
+ }
+ function letter(code) {
+ return uppercaseletter(code) || lowercaseletter(code);
+ }
+ function nonascii(code) {
+ return code >= 0x80;
+ }
+ function namestartchar(code) {
+ return letter(code) || nonascii(code) || code == 0x5f;
+ }
+ function namechar(code) {
+ return namestartchar(code) || digit(code) || code == 0x2d;
+ }
+ function nonprintable(code) {
+ return between(code, 0, 8) || code == 0xb || between(code, 0xe, 0x1f) || code == 0x7f;
+ }
+ function newline(code) {
+ return code == 0xa;
+ }
+ function whitespace(code) {
+ return newline(code) || code == 9 || code == 0x20;
+ }
+
+ var maximumallowedcodepoint = 0x10ffff;
+
+ var InvalidCharacterError = function (message) {
+ this.message = message;
+ };
+ InvalidCharacterError.prototype = new Error();
+ InvalidCharacterError.prototype.name = 'InvalidCharacterError';
+
+ function stringFromCode(code) {
+ if (code <= 0xffff) return String.fromCharCode(code);
+ // Otherwise, encode astral char as surrogate pair.
+ code -= Math.pow(2, 20);
+ var lead = Math.floor(code / Math.pow(2, 10)) + 0xd800;
+ var trail = code % Math.pow(2, 10) + 0xdc00;
+ return String.fromCharCode(lead) + String.fromCharCode(trail);
+ }
+
+ function* tokenize(str, options) {
+ if (options === undefined) {
+ options = {};
+ }
+ if (options.loc === undefined) {
+ options.loc = false;
+ }
+ if (options.offsets === undefined) {
+ options.offsets = false;
+ }
+ if (options.keepComments === undefined) {
+ options.keepComments = false;
+ }
+ if (options.startOffset === undefined) {
+ options.startOffset = 0;
+ }
+
+ var i = options.startOffset - 1;
+ var code;
+
+ // Line number information.
+ var line = 0;
+ var column = 0;
+ // The only use of lastLineLength is in reconsume().
+ var lastLineLength = 0;
+ var incrLineno = function () {
+ line += 1;
+ lastLineLength = column;
+ column = 0;
+ };
+ var locStart = { line: line, column: column };
+ var offsetStart = i;
+
+ var codepoint = function (i) {
+ if (i >= str.length) {
+ return -1;
+ }
+ return str.charCodeAt(i);
+ };
+ var next = function (num) {
+ if (num === undefined) num = 1;
+ if (num > 3) throw "Spec Error: no more than three codepoints of lookahead.";
+
+ var rcode;
+ for (var offset = i + 1; num-- > 0; ++offset) {
+ rcode = codepoint(offset);
+ if (rcode === 0xd && codepoint(offset + 1) === 0xa) {
+ ++offset;
+ rcode = 0xa;
+ } else if (rcode === 0xd || rcode === 0xc) {
+ rcode = 0xa;
+ } else if (rcode === 0x0) {
+ rcode = 0xfffd;
+ }
+ }
+
+ return rcode;
+ };
+ var consume = function (num) {
+ if (num === undefined) num = 1;
+ while (num-- > 0) {
+ ++i;
+ code = codepoint(i);
+ if (code === 0xd && codepoint(i + 1) === 0xa) {
+ ++i;
+ code = 0xa;
+ } else if (code === 0xd || code === 0xc) {
+ code = 0xa;
+ } else if (code === 0x0) {
+ code = 0xfffd;
+ }
+ if (newline(code)) incrLineno();else column++;
+ }
+ return true;
+ };
+ var reconsume = function () {
+ i -= 1; // This is ok even in the \r\n case.
+ if (newline(code)) {
+ line -= 1;
+ column = lastLineLength;
+ } else {
+ column -= 1;
+ }
+ return true;
+ };
+ var eof = function (codepoint) {
+ if (codepoint === undefined) codepoint = code;
+ return codepoint == -1;
+ };
+ var donothing = function () {};
+ var parseerror = function () {
+ console.log("Parse error at index " + i + ", processing codepoint 0x" + code.toString(16) + ".");return true;
+ };
+
+ var consumeAToken = function () {
+ consume();
+ if (!options.keepComments) {
+ while (code == 0x2f && next() == 0x2a) {
+ consumeAComment();
+ consume();
+ }
+ }
+ locStart.line = line;
+ locStart.column = column;
+ offsetStart = i;
+ if (whitespace(code)) {
+ while (whitespace(next())) {
+ consume();
+ }return new WhitespaceToken();
+ } else if (code == 0x2f && next() == 0x2a) return consumeAComment();else if (code == 0x22) return consumeAStringToken();else if (code == 0x23) {
+ if (namechar(next()) || areAValidEscape(next(1), next(2))) {
+ var token = new HashToken();
+ if (wouldStartAnIdentifier(next(1), next(2), next(3))) {
+ token.type = "id";
+ token.tokenType = "id";
+ }
+ token.value = consumeAName();
+ token.text = token.value;
+ return token;
+ } else {
+ return new DelimToken(code);
+ }
+ } else if (code == 0x24) {
+ if (next() == 0x3d) {
+ consume();
+ return new SuffixMatchToken();
+ } else {
+ return new DelimToken(code);
+ }
+ } else if (code == 0x27) return consumeAStringToken();else if (code == 0x28) return new OpenParenToken();else if (code == 0x29) return new CloseParenToken();else if (code == 0x2a) {
+ if (next() == 0x3d) {
+ consume();
+ return new SubstringMatchToken();
+ } else {
+ return new DelimToken(code);
+ }
+ } else if (code == 0x2b) {
+ if (startsWithANumber()) {
+ reconsume();
+ return consumeANumericToken();
+ } else {
+ return new DelimToken(code);
+ }
+ } else if (code == 0x2c) return new CommaToken();else if (code == 0x2d) {
+ if (startsWithANumber()) {
+ reconsume();
+ return consumeANumericToken();
+ } else if (next(1) == 0x2d && next(2) == 0x3e) {
+ consume(2);
+ return new CDCToken();
+ } else if (startsWithAnIdentifier()) {
+ reconsume();
+ return consumeAnIdentlikeToken();
+ } else {
+ return new DelimToken(code);
+ }
+ } else if (code == 0x2e) {
+ if (startsWithANumber()) {
+ reconsume();
+ return consumeANumericToken();
+ } else {
+ return new DelimToken(code);
+ }
+ } else if (code == 0x3a) return new ColonToken();else if (code == 0x3b) return new SemicolonToken();else if (code == 0x3c) {
+ if (next(1) == 0x21 && next(2) == 0x2d && next(3) == 0x2d) {
+ consume(3);
+ return new CDOToken();
+ } else {
+ return new DelimToken(code);
+ }
+ } else if (code == 0x40) {
+ if (wouldStartAnIdentifier(next(1), next(2), next(3))) {
+ return new AtKeywordToken(consumeAName());
+ } else {
+ return new DelimToken(code);
+ }
+ } else if (code == 0x5b) return new OpenSquareToken();else if (code == 0x5c) {
+ if (startsWithAValidEscape()) {
+ reconsume();
+ return consumeAnIdentlikeToken();
+ } else {
+ parseerror();
+ return new DelimToken(code);
+ }
+ } else if (code == 0x5d) return new CloseSquareToken();else if (code == 0x5e) {
+ if (next() == 0x3d) {
+ consume();
+ return new PrefixMatchToken();
+ } else {
+ return new DelimToken(code);
+ }
+ } else if (code == 0x7b) return new OpenCurlyToken();else if (code == 0x7c) {
+ if (next() == 0x3d) {
+ consume();
+ return new DashMatchToken();
+ // } else if(next() == 0x7c) {
+ // consume();
+ // return new ColumnToken();
+ } else {
+ return new DelimToken(code);
+ }
+ } else if (code == 0x7d) return new CloseCurlyToken();else if (code == 0x7e) {
+ if (next() == 0x3d) {
+ consume();
+ return new IncludeMatchToken();
+ } else {
+ return new DelimToken(code);
+ }
+ } else if (digit(code)) {
+ reconsume();
+ return consumeANumericToken();
+ } else if (namestartchar(code)) {
+ reconsume();
+ return consumeAnIdentlikeToken();
+ } else if (eof()) return new EOFToken();else return new DelimToken(code);
+ };
+
+ var consumeAComment = function () {
+ consume();
+ var comment = "";
+ while (true) {
+ consume();
+ if (code == 0x2a && next() == 0x2f) {
+ consume();
+ break;
+ } else if (eof()) {
+ break;
+ }
+ comment += stringFromCode(code);
+ }
+ return new CommentToken(comment);
+ };
+
+ var consumeANumericToken = function () {
+ var num = consumeANumber();
+ var token;
+ if (wouldStartAnIdentifier(next(1), next(2), next(3))) {
+ token = new DimensionToken();
+ token.value = num.value;
+ token.repr = num.repr;
+ token.type = num.type;
+ token.unit = consumeAName();
+ token.text = token.unit;
+ } else if (next() == 0x25) {
+ consume();
+ token = new PercentageToken();
+ token.value = num.value;
+ token.repr = num.repr;
+ } else {
+ var token = new NumberToken();
+ token.value = num.value;
+ token.repr = num.repr;
+ token.type = num.type;
+ }
+ token.number = token.value;
+ token.isInteger = token.type === "integer";
+ // FIXME hasSign
+ return token;
+ };
+
+ var consumeAnIdentlikeToken = function () {
+ var str = consumeAName();
+ if (str.toLowerCase() == "url" && next() == 0x28) {
+ consume();
+ while (whitespace(next(1)) && whitespace(next(2))) {
+ consume();
+ }if (next() == 0x22 || next() == 0x27 || whitespace(next()) && (next(2) == 0x22 || next(2) == 0x27)) {
+ while (whitespace(next())) {
+ consume();
+ }consume();
+ var _str = consumeAStringToken();
+ while (whitespace(next())) {
+ consume();
+ } // The closing paren.
+ consume();
+ return new URLToken(_str.text);
+ } else {
+ return consumeAURLToken();
+ }
+ } else if (next() == 0x28) {
+ consume();
+ return new FunctionToken(str);
+ } else {
+ return new IdentToken(str);
+ }
+ };
+
+ var consumeAStringToken = function (endingCodePoint) {
+ if (endingCodePoint === undefined) endingCodePoint = code;
+ var string = "";
+ while (consume()) {
+ if (code == endingCodePoint || eof()) {
+ return new StringToken(string);
+ } else if (newline(code)) {
+ reconsume();
+ return new BadStringToken(string);
+ } else if (code == 0x5c) {
+ if (eof(next())) {
+ donothing();
+ } else if (newline(next())) {
+ consume();
+ } else {
+ string += stringFromCode(consumeEscape());
+ }
+ } else {
+ string += stringFromCode(code);
+ }
+ }
+ };
+
+ var consumeAURLToken = function () {
+ var token = new URLToken("");
+ while (whitespace(next())) {
+ consume();
+ }if (eof(next())) return token;
+ while (consume()) {
+ if (code == 0x29 || eof()) {
+ break;
+ } else if (whitespace(code)) {
+ while (whitespace(next())) {
+ consume();
+ }if (next() == 0x29 || eof(next())) {
+ consume();
+ break;
+ } else {
+ consumeTheRemnantsOfABadURL();
+ return new BadURLToken();
+ }
+ } else if (code == 0x22 || code == 0x27 || code == 0x28 || nonprintable(code)) {
+ parseerror();
+ consumeTheRemnantsOfABadURL();
+ return new BadURLToken();
+ } else if (code == 0x5c) {
+ if (startsWithAValidEscape()) {
+ token.value += stringFromCode(consumeEscape());
+ } else {
+ parseerror();
+ consumeTheRemnantsOfABadURL();
+ return new BadURLToken();
+ }
+ } else {
+ token.value += stringFromCode(code);
+ }
+ }
+ token.text = token.value;
+ return token;
+ };
+
+ var consumeEscape = function () {
+ // Assume the the current character is the \
+ // and the next code point is not a newline.
+ consume();
+ if (hexdigit(code)) {
+ // Consume 1-6 hex digits
+ var digits = [code];
+ for (var total = 0; total < 5; total++) {
+ if (hexdigit(next())) {
+ consume();
+ digits.push(code);
+ } else {
+ break;
+ }
+ }
+ if (whitespace(next())) consume();
+ var value = parseInt(digits.map(function (x) {
+ return String.fromCharCode(x);
+ }).join(''), 16);
+ if (value > maximumallowedcodepoint) value = 0xfffd;
+ return value;
+ } else if (eof()) {
+ return 0xfffd;
+ } else {
+ return code;
+ }
+ };
+
+ var areAValidEscape = function (c1, c2) {
+ if (c1 != 0x5c) return false;
+ if (newline(c2)) return false;
+ return true;
+ };
+ var startsWithAValidEscape = function () {
+ return areAValidEscape(code, next());
+ };
+
+ var wouldStartAnIdentifier = function (c1, c2, c3) {
+ if (c1 == 0x2d) {
+ return namestartchar(c2) || c2 == 0x2d || areAValidEscape(c2, c3);
+ } else if (namestartchar(c1)) {
+ return true;
+ } else if (c1 == 0x5c) {
+ return areAValidEscape(c1, c2);
+ } else {
+ return false;
+ }
+ };
+ var startsWithAnIdentifier = function () {
+ return wouldStartAnIdentifier(code, next(1), next(2));
+ };
+
+ var wouldStartANumber = function (c1, c2, c3) {
+ if (c1 == 0x2b || c1 == 0x2d) {
+ if (digit(c2)) return true;
+ if (c2 == 0x2e && digit(c3)) return true;
+ return false;
+ } else if (c1 == 0x2e) {
+ if (digit(c2)) return true;
+ return false;
+ } else if (digit(c1)) {
+ return true;
+ } else {
+ return false;
+ }
+ };
+ var startsWithANumber = function () {
+ return wouldStartANumber(code, next(1), next(2));
+ };
+
+ var consumeAName = function () {
+ var result = "";
+ while (consume()) {
+ if (namechar(code)) {
+ result += stringFromCode(code);
+ } else if (startsWithAValidEscape()) {
+ result += stringFromCode(consumeEscape());
+ } else {
+ reconsume();
+ return result;
+ }
+ }
+ };
+
+ var consumeANumber = function () {
+ var repr = [];
+ var type = "integer";
+ if (next() == 0x2b || next() == 0x2d) {
+ consume();
+ repr += stringFromCode(code);
+ }
+ while (digit(next())) {
+ consume();
+ repr += stringFromCode(code);
+ }
+ if (next(1) == 0x2e && digit(next(2))) {
+ consume();
+ repr += stringFromCode(code);
+ consume();
+ repr += stringFromCode(code);
+ type = "number";
+ while (digit(next())) {
+ consume();
+ repr += stringFromCode(code);
+ }
+ }
+ var c1 = next(1),
+ c2 = next(2),
+ c3 = next(3);
+ if ((c1 == 0x45 || c1 == 0x65) && digit(c2)) {
+ consume();
+ repr += stringFromCode(code);
+ consume();
+ repr += stringFromCode(code);
+ type = "number";
+ while (digit(next())) {
+ consume();
+ repr += stringFromCode(code);
+ }
+ } else if ((c1 == 0x45 || c1 == 0x65) && (c2 == 0x2b || c2 == 0x2d) && digit(c3)) {
+ consume();
+ repr += stringFromCode(code);
+ consume();
+ repr += stringFromCode(code);
+ consume();
+ repr += stringFromCode(code);
+ type = "number";
+ while (digit(next())) {
+ consume();
+ repr += stringFromCode(code);
+ }
+ }
+ var value = convertAStringToANumber(repr);
+ return { type: type, value: value, repr: repr };
+ };
+
+ var convertAStringToANumber = function (string) {
+ // CSS's number rules are identical to JS, afaik.
+ return +string;
+ };
+
+ var consumeTheRemnantsOfABadURL = function () {
+ while (consume()) {
+ if (code == 0x2d || eof()) {
+ return;
+ } else if (startsWithAValidEscape()) {
+ consumeEscape();
+ donothing();
+ } else {
+ donothing();
+ }
+ }
+ };
+
+ var iterationCount = 0;
+ while (!eof(next())) {
+ var token = consumeAToken();
+ if (options.loc) {
+ token.loc = {};
+ token.loc.start = { line: locStart.line, column: locStart.column };
+ token.loc.end = { line: line, column: column };
+ }
+ if (options.offsets) {
+ token.startOffset = offsetStart;
+ token.endOffset = i + 1;
+ }
+ yield token;
+ iterationCount++;
+ if (iterationCount > str.length * 2) return "I'm infinite-looping!";
+ }
+ }
+
+ function CSSParserToken() {
+ throw "Abstract Base Class";
+ }
+ CSSParserToken.prototype.toJSON = function () {
+ return { token: this.tokenType };
+ };
+ CSSParserToken.prototype.toString = function () {
+ return this.tokenType;
+ };
+ CSSParserToken.prototype.toSource = function () {
+ return '' + this;
+ };
+
+ function BadStringToken(text) {
+ this.text = text;
+ return this;
+ }
+ BadStringToken.prototype = Object.create(CSSParserToken.prototype);
+ BadStringToken.prototype.tokenType = "bad_string";
+
+ function BadURLToken() {
+ return this;
+ }
+ BadURLToken.prototype = Object.create(CSSParserToken.prototype);
+ BadURLToken.prototype.tokenType = "bad_url";
+
+ function WhitespaceToken() {
+ return this;
+ }
+ WhitespaceToken.prototype = Object.create(CSSParserToken.prototype);
+ WhitespaceToken.prototype.tokenType = "whitespace";
+ WhitespaceToken.prototype.toString = function () {
+ return "WS";
+ };
+ WhitespaceToken.prototype.toSource = function () {
+ return " ";
+ };
+
+ function CDOToken() {
+ return this;
+ }
+ CDOToken.prototype = Object.create(CSSParserToken.prototype);
+ CDOToken.prototype.tokenType = "htmlcomment";
+ CDOToken.prototype.toSource = function () {
+ return "<!--";
+ };
+
+ function CDCToken() {
+ return this;
+ }
+ CDCToken.prototype = Object.create(CSSParserToken.prototype);
+ CDCToken.prototype.tokenType = "htmlcomment";
+ CDCToken.prototype.toSource = function () {
+ return "-->";
+ };
+
+ function ColonToken() {
+ return this;
+ }
+ ColonToken.prototype = Object.create(CSSParserToken.prototype);
+ ColonToken.prototype.tokenType = "symbol";
+ ColonToken.prototype.text = ":";
+
+ function SemicolonToken() {
+ return this;
+ }
+ SemicolonToken.prototype = Object.create(CSSParserToken.prototype);
+ SemicolonToken.prototype.tokenType = "symbol";
+ SemicolonToken.prototype.text = ";";
+
+ function CommaToken() {
+ return this;
+ }
+ CommaToken.prototype = Object.create(CSSParserToken.prototype);
+ CommaToken.prototype.tokenType = "symbol";
+ CommaToken.prototype.text = ",";
+
+ function GroupingToken() {
+ throw "Abstract Base Class";
+ }
+ GroupingToken.prototype = Object.create(CSSParserToken.prototype);
+
+ function OpenCurlyToken() {
+ this.value = "{";this.mirror = "}";return this;
+ }
+ OpenCurlyToken.prototype = Object.create(GroupingToken.prototype);
+ OpenCurlyToken.prototype.tokenType = "symbol";
+ OpenCurlyToken.prototype.text = "{";
+
+ function CloseCurlyToken() {
+ this.value = "}";this.mirror = "{";return this;
+ }
+ CloseCurlyToken.prototype = Object.create(GroupingToken.prototype);
+ CloseCurlyToken.prototype.tokenType = "symbol";
+ CloseCurlyToken.prototype.text = "}";
+
+ function OpenSquareToken() {
+ this.value = "[";this.mirror = "]";return this;
+ }
+ OpenSquareToken.prototype = Object.create(GroupingToken.prototype);
+ OpenSquareToken.prototype.tokenType = "symbol";
+ OpenSquareToken.prototype.text = "[";
+
+ function CloseSquareToken() {
+ this.value = "]";this.mirror = "[";return this;
+ }
+ CloseSquareToken.prototype = Object.create(GroupingToken.prototype);
+ CloseSquareToken.prototype.tokenType = "symbol";
+ CloseSquareToken.prototype.text = "]";
+
+ function OpenParenToken() {
+ this.value = "(";this.mirror = ")";return this;
+ }
+ OpenParenToken.prototype = Object.create(GroupingToken.prototype);
+ OpenParenToken.prototype.tokenType = "symbol";
+ OpenParenToken.prototype.text = "(";
+
+ function CloseParenToken() {
+ this.value = ")";this.mirror = "(";return this;
+ }
+ CloseParenToken.prototype = Object.create(GroupingToken.prototype);
+ CloseParenToken.prototype.tokenType = "symbol";
+ CloseParenToken.prototype.text = ")";
+
+ function IncludeMatchToken() {
+ return this;
+ }
+ IncludeMatchToken.prototype = Object.create(CSSParserToken.prototype);
+ IncludeMatchToken.prototype.tokenType = "includes";
+
+ function DashMatchToken() {
+ return this;
+ }
+ DashMatchToken.prototype = Object.create(CSSParserToken.prototype);
+ DashMatchToken.prototype.tokenType = "dashmatch";
+
+ function PrefixMatchToken() {
+ return this;
+ }
+ PrefixMatchToken.prototype = Object.create(CSSParserToken.prototype);
+ PrefixMatchToken.prototype.tokenType = "beginsmatch";
+
+ function SuffixMatchToken() {
+ return this;
+ }
+ SuffixMatchToken.prototype = Object.create(CSSParserToken.prototype);
+ SuffixMatchToken.prototype.tokenType = "endsmatch";
+
+ function SubstringMatchToken() {
+ return this;
+ }
+ SubstringMatchToken.prototype = Object.create(CSSParserToken.prototype);
+ SubstringMatchToken.prototype.tokenType = "containsmatch";
+
+ function ColumnToken() {
+ return this;
+ }
+ ColumnToken.prototype = Object.create(CSSParserToken.prototype);
+ ColumnToken.prototype.tokenType = "||";
+
+ function EOFToken() {
+ return this;
+ }
+ EOFToken.prototype = Object.create(CSSParserToken.prototype);
+ EOFToken.prototype.tokenType = "EOF";
+ EOFToken.prototype.toSource = function () {
+ return "";
+ };
+
+ function DelimToken(code) {
+ this.value = stringFromCode(code);
+ this.text = this.value;
+ return this;
+ }
+ DelimToken.prototype = Object.create(CSSParserToken.prototype);
+ DelimToken.prototype.tokenType = "symbol";
+ DelimToken.prototype.toString = function () {
+ return "DELIM(" + this.value + ")";
+ };
+ DelimToken.prototype.toJSON = function () {
+ var json = this.constructor.prototype.constructor.prototype.toJSON.call(this);
+ json.value = this.value;
+ return json;
+ };
+ DelimToken.prototype.toSource = function () {
+ if (this.value == "\\") return "\\\n";else return this.value;
+ };
+
+ function StringValuedToken() {
+ throw "Abstract Base Class";
+ }
+ StringValuedToken.prototype = Object.create(CSSParserToken.prototype);
+ StringValuedToken.prototype.ASCIIMatch = function (str) {
+ return this.value.toLowerCase() == str.toLowerCase();
+ };
+ StringValuedToken.prototype.toJSON = function () {
+ var json = this.constructor.prototype.constructor.prototype.toJSON.call(this);
+ json.value = this.value;
+ return json;
+ };
+
+ function IdentToken(val) {
+ this.value = val;
+ this.text = val;
+ }
+ IdentToken.prototype = Object.create(StringValuedToken.prototype);
+ IdentToken.prototype.tokenType = "ident";
+ IdentToken.prototype.toString = function () {
+ return "IDENT(" + this.value + ")";
+ };
+ IdentToken.prototype.toSource = function () {
+ return escapeIdent(this.value);
+ };
+
+ function FunctionToken(val) {
+ this.value = val;
+ this.text = val;
+ this.mirror = ")";
+ }
+ FunctionToken.prototype = Object.create(StringValuedToken.prototype);
+ FunctionToken.prototype.tokenType = "function";
+ FunctionToken.prototype.toString = function () {
+ return "FUNCTION(" + this.value + ")";
+ };
+ FunctionToken.prototype.toSource = function () {
+ return escapeIdent(this.value) + "(";
+ };
+
+ function AtKeywordToken(val) {
+ this.value = val;
+ this.text = val;
+ }
+ AtKeywordToken.prototype = Object.create(StringValuedToken.prototype);
+ AtKeywordToken.prototype.tokenType = "at";
+ AtKeywordToken.prototype.toString = function () {
+ return "AT(" + this.value + ")";
+ };
+ AtKeywordToken.prototype.toSource = function () {
+ return "@" + escapeIdent(this.value);
+ };
+
+ function HashToken(val) {
+ this.value = val;
+ this.text = val;
+ this.type = "unrestricted";
+ }
+ HashToken.prototype = Object.create(StringValuedToken.prototype);
+ HashToken.prototype.tokenType = "hash";
+ HashToken.prototype.toString = function () {
+ return "HASH(" + this.value + ")";
+ };
+ HashToken.prototype.toJSON = function () {
+ var json = this.constructor.prototype.constructor.prototype.toJSON.call(this);
+ json.value = this.value;
+ json.type = this.type;
+ return json;
+ };
+ HashToken.prototype.toSource = function () {
+ if (this.type == "id") {
+ return "#" + escapeIdent(this.value);
+ } else {
+ return "#" + escapeHash(this.value);
+ }
+ };
+
+ function StringToken(val) {
+ this.value = val;
+ this.text = val;
+ }
+ StringToken.prototype = Object.create(StringValuedToken.prototype);
+ StringToken.prototype.tokenType = "string";
+ StringToken.prototype.toString = function () {
+ return '"' + escapeString(this.value) + '"';
+ };
+
+ function CommentToken(val) {
+ this.value = val;
+ }
+ CommentToken.prototype = Object.create(StringValuedToken.prototype);
+ CommentToken.prototype.tokenType = "comment";
+ CommentToken.prototype.toString = function () {
+ return '/*' + this.value + '*/';
+ };
+ CommentToken.prototype.toSource = CommentToken.prototype.toString;
+
+ function URLToken(val) {
+ this.value = val;
+ this.text = val;
+ }
+ URLToken.prototype = Object.create(StringValuedToken.prototype);
+ URLToken.prototype.tokenType = "url";
+ URLToken.prototype.toString = function () {
+ return "URL(" + this.value + ")";
+ };
+ URLToken.prototype.toSource = function () {
+ return 'url("' + escapeString(this.value) + '")';
+ };
+
+ function NumberToken() {
+ this.value = null;
+ this.type = "integer";
+ this.repr = "";
+ }
+ NumberToken.prototype = Object.create(CSSParserToken.prototype);
+ NumberToken.prototype.tokenType = "number";
+ NumberToken.prototype.toString = function () {
+ if (this.type == "integer") return "INT(" + this.value + ")";
+ return "NUMBER(" + this.value + ")";
+ };
+ NumberToken.prototype.toJSON = function () {
+ var json = this.constructor.prototype.constructor.prototype.toJSON.call(this);
+ json.value = this.value;
+ json.type = this.type;
+ json.repr = this.repr;
+ return json;
+ };
+ NumberToken.prototype.toSource = function () {
+ return this.repr;
+ };
+
+ function PercentageToken() {
+ this.value = null;
+ this.repr = "";
+ }
+ PercentageToken.prototype = Object.create(CSSParserToken.prototype);
+ PercentageToken.prototype.tokenType = "percentage";
+ PercentageToken.prototype.toString = function () {
+ return "PERCENTAGE(" + this.value + ")";
+ };
+ PercentageToken.prototype.toJSON = function () {
+ var json = this.constructor.prototype.constructor.prototype.toJSON.call(this);
+ json.value = this.value;
+ json.repr = this.repr;
+ return json;
+ };
+ PercentageToken.prototype.toSource = function () {
+ return this.repr + "%";
+ };
+
+ function DimensionToken() {
+ this.value = null;
+ this.type = "integer";
+ this.repr = "";
+ this.unit = "";
+ }
+ DimensionToken.prototype = Object.create(CSSParserToken.prototype);
+ DimensionToken.prototype.tokenType = "dimension";
+ DimensionToken.prototype.toString = function () {
+ return "DIM(" + this.value + "," + this.unit + ")";
+ };
+ DimensionToken.prototype.toJSON = function () {
+ var json = this.constructor.prototype.constructor.prototype.toJSON.call(this);
+ json.value = this.value;
+ json.type = this.type;
+ json.repr = this.repr;
+ json.unit = this.unit;
+ return json;
+ };
+ DimensionToken.prototype.toSource = function () {
+ var source = this.repr;
+ var unit = escapeIdent(this.unit);
+ if (unit[0].toLowerCase() == "e" && (unit[1] == "-" || between(unit.charCodeAt(1), 0x30, 0x39))) {
+ // Unit is ambiguous with scinot
+ // Remove the leading "e", replace with escape.
+ unit = "\\65 " + unit.slice(1, unit.length);
+ }
+ return source + unit;
+ };
+
+ function escapeIdent(string) {
+ string = '' + string;
+ var result = '';
+ var firstcode = string.charCodeAt(0);
+ for (var i = 0; i < string.length; i++) {
+ var code = string.charCodeAt(i);
+ if (code === 0x0) {
+ throw new InvalidCharacterError('Invalid character: the input contains U+0000.');
+ }
+
+ if (between(code, 0x1, 0x1f) || code == 0x7f || i === 0 && between(code, 0x30, 0x39) || i == 1 && between(code, 0x30, 0x39) && firstcode == 0x2d) {
+ result += '\\' + code.toString(16) + ' ';
+ } else if (code >= 0x80 || code == 0x2d || code == 0x5f || between(code, 0x30, 0x39) || between(code, 0x41, 0x5a) || between(code, 0x61, 0x7a)) {
+ result += string[i];
+ } else {
+ result += '\\' + string[i];
+ }
+ }
+ return result;
+ }
+
+ function escapeHash(string) {
+ // Escapes the contents of "unrestricted"-type hash tokens.
+ // Won't preserve the ID-ness of "id"-type hash tokens;
+ // use escapeIdent() for that.
+ string = '' + string;
+ var result = '';
+ for (var i = 0; i < string.length; i++) {
+ var code = string.charCodeAt(i);
+ if (code === 0x0) {
+ throw new InvalidCharacterError('Invalid character: the input contains U+0000.');
+ }
+
+ if (code >= 0x80 || code == 0x2d || code == 0x5f || between(code, 0x30, 0x39) || between(code, 0x41, 0x5a) || between(code, 0x61, 0x7a)) {
+ result += string[i];
+ } else {
+ result += '\\' + code.toString(16) + ' ';
+ }
+ }
+ return result;
+ }
+
+ function escapeString(string) {
+ string = '' + string;
+ var result = '';
+ for (var i = 0; i < string.length; i++) {
+ var code = string.charCodeAt(i);
+
+ if (code === 0x0) {
+ throw new InvalidCharacterError('Invalid character: the input contains U+0000.');
+ }
+
+ if (between(code, 0x1, 0x1f) || code == 0x7f) {
+ result += '\\' + code.toString(16) + ' ';
+ } else if (code == 0x22 || code == 0x5c) {
+ result += '\\' + string[i];
+ } else {
+ result += string[i];
+ }
+ }
+ return result;
+ }
+
+ // Exportation.
+ exports.tokenize = tokenize;
+ exports.IdentToken = IdentToken;
+ exports.FunctionToken = FunctionToken;
+ exports.AtKeywordToken = AtKeywordToken;
+ exports.HashToken = HashToken;
+ exports.StringToken = StringToken;
+ exports.BadStringToken = BadStringToken;
+ exports.URLToken = URLToken;
+ exports.BadURLToken = BadURLToken;
+ exports.DelimToken = DelimToken;
+ exports.NumberToken = NumberToken;
+ exports.PercentageToken = PercentageToken;
+ exports.DimensionToken = DimensionToken;
+ exports.IncludeMatchToken = IncludeMatchToken;
+ exports.DashMatchToken = DashMatchToken;
+ exports.PrefixMatchToken = PrefixMatchToken;
+ exports.SuffixMatchToken = SuffixMatchToken;
+ exports.SubstringMatchToken = SubstringMatchToken;
+ exports.ColumnToken = ColumnToken;
+ exports.WhitespaceToken = WhitespaceToken;
+ exports.CDOToken = CDOToken;
+ exports.CDCToken = CDCToken;
+ exports.ColonToken = ColonToken;
+ exports.SemicolonToken = SemicolonToken;
+ exports.CommaToken = CommaToken;
+ exports.OpenParenToken = OpenParenToken;
+ exports.CloseParenToken = CloseParenToken;
+ exports.OpenSquareToken = OpenSquareToken;
+ exports.CloseSquareToken = CloseSquareToken;
+ exports.OpenCurlyToken = OpenCurlyToken;
+ exports.CloseCurlyToken = CloseCurlyToken;
+ exports.EOFToken = EOFToken;
+ exports.CSSParserToken = CSSParserToken;
+ exports.GroupingToken = GroupingToken;
+
+ function TokenStream(tokens) {
+ // Assume that tokens is a iterator.
+ this.tokens = tokens;
+ this.token = undefined;
+ this.stored = [];
+ }
+ TokenStream.prototype.consume = function (num) {
+ if (num === undefined) num = 1;
+ while (num-- > 0) {
+ if (this.stored.length > 0) {
+ this.token = this.stored.shift();
+ } else {
+ var n = this.tokens.next();
+ while (!n.done && n.value instanceof CommentToken) {
+ n = this.tokens.next();
+ }
+ if (n.done) {
+ this.token = new EOFToken();
+ break;
+ }
+ this.token = n.value;
+ }
+ }
+ //console.log(this.i, this.token);
+ return true;
+ };
+ TokenStream.prototype.next = function () {
+ if (this.stored.length === 0) {
+ var n = this.tokens.next();
+ while (!n.done && n.value instanceof CommentToken) {
+ n = this.tokens.next();
+ }
+ if (n.done) return new EOFToken();
+ this.stored.push(n.value);
+ }
+ return this.stored[0];
+ };
+ TokenStream.prototype.reconsume = function () {
+ this.stored.unshift(this.token);
+ };
+
+ function parseerror(s, msg) {
+ console.log("Parse error at token " + s.i + ": " + s.token + ".\n" + msg);
+ return true;
+ }
+ function donothing() {
+ return true;
+ }
+
+ function consumeAListOfRules(s, topLevel) {
+ var rules = [];
+ var rule;
+ while (s.consume()) {
+ if (s.token instanceof WhitespaceToken) {
+ continue;
+ } else if (s.token instanceof EOFToken) {
+ return rules;
+ } else if (s.token instanceof CDOToken || s.token instanceof CDCToken) {
+ if (topLevel == "top-level") continue;
+ s.reconsume();
+ if (rule = consumeAQualifiedRule(s)) rules.push(rule);
+ } else if (s.token instanceof AtKeywordToken) {
+ s.reconsume();
+ if (rule = consumeAnAtRule(s)) rules.push(rule);
+ } else {
+ s.reconsume();
+ if (rule = consumeAQualifiedRule(s)) rules.push(rule);
+ }
+ }
+ }
+
+ function consumeAnAtRule(s) {
+ s.consume();
+ var rule = new AtRule(s.token.value);
+ while (s.consume()) {
+ if (s.token instanceof SemicolonToken || s.token instanceof EOFToken) {
+ return rule;
+ } else if (s.token instanceof OpenCurlyToken) {
+ rule.value = consumeASimpleBlock(s);
+ return rule;
+ } else {
+ s.reconsume();
+ rule.prelude.push(consumeAComponentValue(s));
+ }
+ }
+ }
+
+ function consumeAQualifiedRule(s) {
+ var rule = new QualifiedRule();
+ while (s.consume()) {
+ if (s.token instanceof EOFToken) {
+ parseerror(s, "Hit EOF when trying to parse the prelude of a qualified rule.");
+ return;
+ } else if (s.token instanceof OpenCurlyToken) {
+ rule.value = consumeASimpleBlock(s);
+ return rule;
+ } else {
+ s.reconsume();
+ rule.prelude.push(consumeAComponentValue(s));
+ }
+ }
+ }
+
+ function consumeAListOfDeclarations(s) {
+ var decls = [];
+ while (s.consume()) {
+ if (s.token instanceof WhitespaceToken || s.token instanceof SemicolonToken) {
+ donothing();
+ } else if (s.token instanceof EOFToken) {
+ return decls;
+ } else if (s.token instanceof AtKeywordToken) {
+ s.reconsume();
+ decls.push(consumeAnAtRule(s));
+ } else if (s.token instanceof IdentToken) {
+ var temp = [s.token];
+ while (!(s.next() instanceof SemicolonToken || s.next() instanceof EOFToken)) {
+ temp.push(consumeAComponentValue(s));
+ }var decl;
+ if (decl = consumeADeclaration(new TokenStream(temp))) decls.push(decl);
+ } else {
+ parseerror(s);
+ s.reconsume();
+ while (!(s.next() instanceof SemicolonToken || s.next() instanceof EOFToken)) {
+ consumeAComponentValue(s);
+ }
+ }
+ }
+ }
+
+ function consumeADeclaration(s) {
+ // Assumes that the next input token will be an ident token.
+ s.consume();
+ var decl = new Declaration(s.token.value);
+ while (s.next() instanceof WhitespaceToken) {
+ s.consume();
+ }if (!(s.next() instanceof ColonToken)) {
+ parseerror(s);
+ return;
+ } else {
+ s.consume();
+ }
+ while (!(s.next() instanceof EOFToken)) {
+ decl.value.push(consumeAComponentValue(s));
+ }
+ var foundImportant = false;
+ for (var i = decl.value.length - 1; i >= 0; i--) {
+ if (decl.value[i] instanceof WhitespaceToken) {
+ continue;
+ } else if (decl.value[i] instanceof IdentToken && decl.value[i].ASCIIMatch("important")) {
+ foundImportant = true;
+ } else if (foundImportant && decl.value[i] instanceof DelimToken && decl.value[i].value == "!") {
+ decl.value.splice(i, decl.value.length);
+ decl.important = true;
+ break;
+ } else {
+ break;
+ }
+ }
+ return decl;
+ }
+
+ function consumeAComponentValue(s) {
+ s.consume();
+ if (s.token instanceof OpenCurlyToken || s.token instanceof OpenSquareToken || s.token instanceof OpenParenToken) return consumeASimpleBlock(s);
+ if (s.token instanceof FunctionToken) return consumeAFunction(s);
+ return s.token;
+ }
+
+ function consumeASimpleBlock(s) {
+ var mirror = s.token.mirror;
+ var block = new SimpleBlock(s.token.value);
+ block.startToken = s.token;
+ while (s.consume()) {
+ if (s.token instanceof EOFToken || s.token instanceof GroupingToken && s.token.value == mirror) return block;else {
+ s.reconsume();
+ block.value.push(consumeAComponentValue(s));
+ }
+ }
+ }
+
+ function consumeAFunction(s) {
+ var func = new Func(s.token.value);
+ while (s.consume()) {
+ if (s.token instanceof EOFToken || s.token instanceof CloseParenToken) return func;else {
+ s.reconsume();
+ func.value.push(consumeAComponentValue(s));
+ }
+ }
+ }
+
+ function normalizeInput(input) {
+ if (typeof input == "string") return new TokenStream(tokenize(input));
+ if (input instanceof TokenStream) return input;
+ if (typeof input.next == "function") return new TokenStream(input);
+ if (input.length !== undefined) return new TokenStream(input[Symbol.iterator]());else throw SyntaxError(input);
+ }
+
+ function parseAStylesheet(s) {
+ s = normalizeInput(s);
+ var sheet = new Stylesheet();
+ sheet.value = consumeAListOfRules(s, "top-level");
+ return sheet;
+ }
+
+ function parseAListOfRules(s) {
+ s = normalizeInput(s);
+ return consumeAListOfRules(s);
+ }
+
+ function parseARule(s) {
+ s = normalizeInput(s);
+ while (s.next() instanceof WhitespaceToken) {
+ s.consume();
+ }if (s.next() instanceof EOFToken) throw SyntaxError();
+ var rule;
+ var startToken = s.next();
+ if (startToken instanceof AtKeywordToken) {
+ rule = consumeAnAtRule(s);
+ } else {
+ rule = consumeAQualifiedRule(s);
+ if (!rule) throw SyntaxError();
+ }
+ rule.startToken = startToken;
+ rule.endToken = s.token;
+ return rule;
+ }
+
+ function parseADeclaration(s) {
+ s = normalizeInput(s);
+ while (s.next() instanceof WhitespaceToken) {
+ s.consume();
+ }if (!(s.next() instanceof IdentToken)) throw SyntaxError();
+ var decl = consumeADeclaration(s);
+ if (decl) return decl;else throw SyntaxError();
+ }
+
+ function parseAListOfDeclarations(s) {
+ s = normalizeInput(s);
+ return consumeAListOfDeclarations(s);
+ }
+
+ function parseAComponentValue(s) {
+ s = normalizeInput(s);
+ while (s.next() instanceof WhitespaceToken) {
+ s.consume();
+ }if (s.next() instanceof EOFToken) throw SyntaxError();
+ var val = consumeAComponentValue(s);
+ if (!val) throw SyntaxError();
+ while (s.next() instanceof WhitespaceToken) {
+ s.consume();
+ }if (s.next() instanceof EOFToken) return val;
+ throw SyntaxError();
+ }
+
+ function parseAListOfComponentValues(s) {
+ s = normalizeInput(s);
+ var vals = [];
+ while (true) {
+ var val = consumeAComponentValue(s);
+ if (val instanceof EOFToken) return vals;else vals.push(val);
+ }
+ }
+
+ function parseACommaSeparatedListOfComponentValues(s) {
+ s = normalizeInput(s);
+ var listOfCVLs = [];
+ while (true) {
+ var vals = [];
+ while (true) {
+ var val = consumeAComponentValue(s);
+ if (val instanceof EOFToken) {
+ listOfCVLs.push(vals);
+ return listOfCVLs;
+ } else if (val instanceof CommaToken) {
+ listOfCVLs.push(vals);
+ break;
+ } else {
+ vals.push(val);
+ }
+ }
+ }
+ }
+
+ function CSSParserRule() {
+ throw "Abstract Base Class";
+ }
+ CSSParserRule.prototype.toString = function (indent) {
+ return JSON.stringify(this, null, indent);
+ };
+ CSSParserRule.prototype.toJSON = function () {
+ return { type: this.type, value: this.value };
+ };
+
+ function Stylesheet() {
+ this.value = [];
+ return this;
+ }
+ Stylesheet.prototype = Object.create(CSSParserRule.prototype);
+ Stylesheet.prototype.type = "STYLESHEET";
+
+ function AtRule(name) {
+ this.name = name;
+ this.prelude = [];
+ this.value = null;
+ return this;
+ }
+ AtRule.prototype = Object.create(CSSParserRule.prototype);
+ AtRule.prototype.type = "AT-RULE";
+ AtRule.prototype.toJSON = function () {
+ var json = this.constructor.prototype.constructor.prototype.toJSON.call(this);
+ json.name = this.name;
+ json.prelude = this.prelude;
+ return json;
+ };
+
+ function QualifiedRule() {
+ this.prelude = [];
+ this.value = [];
+ return this;
+ }
+ QualifiedRule.prototype = Object.create(CSSParserRule.prototype);
+ QualifiedRule.prototype.type = "QUALIFIED-RULE";
+ QualifiedRule.prototype.toJSON = function () {
+ var json = this.constructor.prototype.constructor.prototype.toJSON.call(this);
+ json.prelude = this.prelude;
+ return json;
+ };
+
+ function Declaration(name) {
+ this.name = name;
+ this.value = [];
+ this.important = false;
+ return this;
+ }
+ Declaration.prototype = Object.create(CSSParserRule.prototype);
+ Declaration.prototype.type = "DECLARATION";
+ Declaration.prototype.toJSON = function () {
+ var json = this.constructor.prototype.constructor.prototype.toJSON.call(this);
+ json.name = this.name;
+ json.important = this.important;
+ return json;
+ };
+
+ function SimpleBlock(type) {
+ this.name = type;
+ this.value = [];
+ return this;
+ }
+ SimpleBlock.prototype = Object.create(CSSParserRule.prototype);
+ SimpleBlock.prototype.type = "BLOCK";
+ SimpleBlock.prototype.toJSON = function () {
+ var json = this.constructor.prototype.constructor.prototype.toJSON.call(this);
+ json.name = this.name;
+ return json;
+ };
+
+ function Func(name) {
+ this.name = name;
+ this.value = [];
+ return this;
+ }
+ Func.prototype = Object.create(CSSParserRule.prototype);
+ Func.prototype.type = "FUNCTION";
+ Func.prototype.toJSON = function () {
+ var json = this.constructor.prototype.constructor.prototype.toJSON.call(this);
+ json.name = this.name;
+ return json;
+ };
+
+ function CSSLexer(text) {
+ this.stream = tokenize(text, {
+ loc: true,
+ offsets: true,
+ keepComments: true
+ });
+ this.lineNumber = 0;
+ this.columnNumber = 0;
+ return this;
+ }
+
+ CSSLexer.prototype.performEOFFixup = function (input, preserveBackslash) {
+ // Just lie for now.
+ return "";
+ };
+
+ CSSLexer.prototype.nextToken = function () {
+ if (!this.stream) {
+ return null;
+ }
+ var v = this.stream.next();
+ if (v.done || v.value.tokenType === "EOF") {
+ this.stream = null;
+ return null;
+ }
+ this.lineNumber = v.value.loc.start.line;
+ this.columnNumber = v.value.loc.start.column;
+ return v.value;
+ };
+
+ // Exportation.
+ exports.CSSParserRule = CSSParserRule;
+ exports.Stylesheet = Stylesheet;
+ exports.AtRule = AtRule;
+ exports.QualifiedRule = QualifiedRule;
+ exports.Declaration = Declaration;
+ exports.SimpleBlock = SimpleBlock;
+ exports.Func = Func;
+ exports.parseAStylesheet = parseAStylesheet;
+ exports.parseAListOfRules = parseAListOfRules;
+ exports.parseARule = parseARule;
+ exports.parseADeclaration = parseADeclaration;
+ exports.parseAListOfDeclarations = parseAListOfDeclarations;
+ exports.parseAComponentValue = parseAComponentValue;
+ exports.parseAListOfComponentValues = parseAListOfComponentValues;
+ exports.parseACommaSeparatedListOfComponentValues = parseACommaSeparatedListOfComponentValues;
+ exports.CSSLexer = CSSLexer;
+ });
+
+/***/ },
+/* 64 */
+/***/ function(module, exports) {
+
+ // auto-generated from nsColorNameList.h
+ var cssColors = {
+ aliceblue: [240, 248, 255],
+ antiquewhite: [250, 235, 215],
+ aqua: [0, 255, 255],
+ aquamarine: [127, 255, 212],
+ azure: [240, 255, 255],
+ beige: [245, 245, 220],
+ bisque: [255, 228, 196],
+ black: [0, 0, 0],
+ blanchedalmond: [255, 235, 205],
+ blue: [0, 0, 255],
+ blueviolet: [138, 43, 226],
+ brown: [165, 42, 42],
+ burlywood: [222, 184, 135],
+ cadetblue: [95, 158, 160],
+ chartreuse: [127, 255, 0],
+ chocolate: [210, 105, 30],
+ coral: [255, 127, 80],
+ cornflowerblue: [100, 149, 237],
+ cornsilk: [255, 248, 220],
+ crimson: [220, 20, 60],
+ cyan: [0, 255, 255],
+ darkblue: [0, 0, 139],
+ darkcyan: [0, 139, 139],
+ darkgoldenrod: [184, 134, 11],
+ darkgray: [169, 169, 169],
+ darkgreen: [0, 100, 0],
+ darkgrey: [169, 169, 169],
+ darkkhaki: [189, 183, 107],
+ darkmagenta: [139, 0, 139],
+ darkolivegreen: [85, 107, 47],
+ darkorange: [255, 140, 0],
+ darkorchid: [153, 50, 204],
+ darkred: [139, 0, 0],
+ darksalmon: [233, 150, 122],
+ darkseagreen: [143, 188, 143],
+ darkslateblue: [72, 61, 139],
+ darkslategray: [47, 79, 79],
+ darkslategrey: [47, 79, 79],
+ darkturquoise: [0, 206, 209],
+ darkviolet: [148, 0, 211],
+ deeppink: [255, 20, 147],
+ deepskyblue: [0, 191, 255],
+ dimgray: [105, 105, 105],
+ dimgrey: [105, 105, 105],
+ dodgerblue: [30, 144, 255],
+ firebrick: [178, 34, 34],
+ floralwhite: [255, 250, 240],
+ forestgreen: [34, 139, 34],
+ fuchsia: [255, 0, 255],
+ gainsboro: [220, 220, 220],
+ ghostwhite: [248, 248, 255],
+ gold: [255, 215, 0],
+ goldenrod: [218, 165, 32],
+ gray: [128, 128, 128],
+ grey: [128, 128, 128],
+ green: [0, 128, 0],
+ greenyellow: [173, 255, 47],
+ honeydew: [240, 255, 240],
+ hotpink: [255, 105, 180],
+ indianred: [205, 92, 92],
+ indigo: [75, 0, 130],
+ ivory: [255, 255, 240],
+ khaki: [240, 230, 140],
+ lavender: [230, 230, 250],
+ lavenderblush: [255, 240, 245],
+ lawngreen: [124, 252, 0],
+ lemonchiffon: [255, 250, 205],
+ lightblue: [173, 216, 230],
+ lightcoral: [240, 128, 128],
+ lightcyan: [224, 255, 255],
+ lightgoldenrodyellow: [250, 250, 210],
+ lightgray: [211, 211, 211],
+ lightgreen: [144, 238, 144],
+ lightgrey: [211, 211, 211],
+ lightpink: [255, 182, 193],
+ lightsalmon: [255, 160, 122],
+ lightseagreen: [32, 178, 170],
+ lightskyblue: [135, 206, 250],
+ lightslategray: [119, 136, 153],
+ lightslategrey: [119, 136, 153],
+ lightsteelblue: [176, 196, 222],
+ lightyellow: [255, 255, 224],
+ lime: [0, 255, 0],
+ limegreen: [50, 205, 50],
+ linen: [250, 240, 230],
+ magenta: [255, 0, 255],
+ maroon: [128, 0, 0],
+ mediumaquamarine: [102, 205, 170],
+ mediumblue: [0, 0, 205],
+ mediumorchid: [186, 85, 211],
+ mediumpurple: [147, 112, 219],
+ mediumseagreen: [60, 179, 113],
+ mediumslateblue: [123, 104, 238],
+ mediumspringgreen: [0, 250, 154],
+ mediumturquoise: [72, 209, 204],
+ mediumvioletred: [199, 21, 133],
+ midnightblue: [25, 25, 112],
+ mintcream: [245, 255, 250],
+ mistyrose: [255, 228, 225],
+ moccasin: [255, 228, 181],
+ navajowhite: [255, 222, 173],
+ navy: [0, 0, 128],
+ oldlace: [253, 245, 230],
+ olive: [128, 128, 0],
+ olivedrab: [107, 142, 35],
+ orange: [255, 165, 0],
+ orangered: [255, 69, 0],
+ orchid: [218, 112, 214],
+ palegoldenrod: [238, 232, 170],
+ palegreen: [152, 251, 152],
+ paleturquoise: [175, 238, 238],
+ palevioletred: [219, 112, 147],
+ papayawhip: [255, 239, 213],
+ peachpuff: [255, 218, 185],
+ peru: [205, 133, 63],
+ pink: [255, 192, 203],
+ plum: [221, 160, 221],
+ powderblue: [176, 224, 230],
+ purple: [128, 0, 128],
+ rebeccapurple: [102, 51, 153],
+ red: [255, 0, 0],
+ rosybrown: [188, 143, 143],
+ royalblue: [65, 105, 225],
+ saddlebrown: [139, 69, 19],
+ salmon: [250, 128, 114],
+ sandybrown: [244, 164, 96],
+ seagreen: [46, 139, 87],
+ seashell: [255, 245, 238],
+ sienna: [160, 82, 45],
+ silver: [192, 192, 192],
+ skyblue: [135, 206, 235],
+ slateblue: [106, 90, 205],
+ slategray: [112, 128, 144],
+ slategrey: [112, 128, 144],
+ snow: [255, 250, 250],
+ springgreen: [0, 255, 127],
+ steelblue: [70, 130, 180],
+ tan: [210, 180, 140],
+ teal: [0, 128, 128],
+ thistle: [216, 191, 216],
+ tomato: [255, 99, 71],
+ turquoise: [64, 224, 208],
+ violet: [238, 130, 238],
+ wheat: [245, 222, 179],
+ white: [255, 255, 255],
+ whitesmoke: [245, 245, 245],
+ yellow: [255, 255, 0],
+ yellowgreen: [154, 205, 50]
+ };
+ module.exports = { cssColors };
+
+/***/ },
+/* 65 */
+/***/ function(module, exports) {
+
+ // auto-generated by means you would rather not know
+ var cssProperties={"-moz-appearance":{inherited:false,supports:0,values:["-moz-gtk-info-bar","-moz-mac-disclosure-button-closed","-moz-mac-disclosure-button-open","-moz-mac-fullscreen-button","-moz-mac-help-button","-moz-mac-vibrancy-dark","-moz-mac-vibrancy-light","-moz-win-borderless-glass","-moz-win-browsertabbar-toolbox","-moz-win-communications-toolbox","-moz-win-exclude-glass","-moz-win-glass","-moz-win-media-toolbox","-moz-window-button-box","-moz-window-button-box-maximized","-moz-window-button-close","-moz-window-button-maximize","-moz-window-button-minimize","-moz-window-button-restore","-moz-window-frame-bottom","-moz-window-frame-left","-moz-window-frame-right","-moz-window-titlebar","-moz-window-titlebar-maximized","button","button-arrow-down","button-arrow-next","button-arrow-previous","button-arrow-up","button-bevel","button-focus","caret","checkbox","checkbox-container","checkbox-label","checkmenuitem","dialog","dualbutton","groupbox","inherit","initial","listbox","listitem","menuarrow","menubar","menucheckbox","menuimage","menuitem","menuitemtext","menulist","menulist-button","menulist-text","menulist-textfield","menupopup","menuradio","menuseparator","meterbar","meterchunk","none","number-input","progressbar","progressbar-vertical","progresschunk","progresschunk-vertical","radio","radio-container","radio-label","radiomenuitem","range","range-thumb","resizer","resizerpanel","scale-horizontal","scale-vertical","scalethumb-horizontal","scalethumb-vertical","scalethumbend","scalethumbstart","scalethumbtick","scrollbar","scrollbar-small","scrollbarbutton-down","scrollbarbutton-left","scrollbarbutton-right","scrollbarbutton-up","scrollbarthumb-horizontal","scrollbarthumb-vertical","scrollbartrack-horizontal","scrollbartrack-vertical","searchfield","separator","spinner","spinner-downbutton","spinner-textfield","spinner-upbutton","splitter","statusbar","statusbarpanel","tab","tab-scroll-arrow-back","tab-scroll-arrow-forward","tabpanel","tabpanels","textfield","textfield-multiline","toolbar","toolbarbutton","toolbarbutton-dropdown","toolbargripper","toolbox","tooltip","treeheader","treeheadercell","treeheadersortarrow","treeitem","treeline","treetwisty","treetwistyopen","treeview","unset","window"]},"-moz-outline-radius-topleft":{inherited:false,supports:3,values:["inherit","initial","unset"]},"-moz-outline-radius-topright":{inherited:false,supports:3,values:["inherit","initial","unset"]},"-moz-outline-radius-bottomright":{inherited:false,supports:3,values:["inherit","initial","unset"]},"-moz-outline-radius-bottomleft":{inherited:false,supports:3,values:["inherit","initial","unset"]},"-moz-tab-size":{inherited:true,supports:1024,values:["inherit","initial","unset"]},"animation-delay":{inherited:false,supports:64,values:["inherit","initial","unset"]},"animation-direction":{inherited:false,supports:0,values:["alternate","alternate-reverse","inherit","initial","normal","reverse","unset"]},"animation-duration":{inherited:false,supports:64,values:["inherit","initial","unset"]},"animation-fill-mode":{inherited:false,supports:0,values:["backwards","both","forwards","inherit","initial","none","unset"]},"animation-iteration-count":{inherited:false,supports:1024,values:["infinite","inherit","initial","unset"]},"animation-name":{inherited:false,supports:0,values:["inherit","initial","none","unset"]},"animation-play-state":{inherited:false,supports:0,values:["inherit","initial","paused","running","unset"]},"animation-timing-function":{inherited:false,supports:256,values:["cubic-bezier","ease","ease-in","ease-in-out","ease-out","inherit","initial","linear","step-end","step-start","steps","unset"]},"background-attachment":{inherited:false,supports:0,values:["fixed","inherit","initial","local","scroll","unset"]},"background-clip":{inherited:false,supports:0,values:["border-box","content-box","inherit","initial","padding-box","unset"]},"background-color":{inherited:false,supports:4,values:["aliceblue","antiquewhite","aqua","aquamarine","azure","beige","bisque","black","blanchedalmond","blue","blueviolet","brown","burlywood","cadetblue","chartreuse","chocolate","coral","cornflowerblue","cornsilk","crimson","currentColor","cyan","darkblue","darkcyan","darkgoldenrod","darkgray","darkgreen","darkgrey","darkkhaki","darkmagenta","darkolivegreen","darkorange","darkorchid","darkred","darksalmon","darkseagreen","darkslateblue","darkslategray","darkslategrey","darkturquoise","darkviolet","deeppink","deepskyblue","dimgray","dimgrey","dodgerblue","firebrick","floralwhite","forestgreen","fuchsia","gainsboro","ghostwhite","gold","goldenrod","gray","grey","green","greenyellow","honeydew","hotpink","hsl","hsla","indianred","indigo","inherit","initial","ivory","khaki","lavender","lavenderblush","lawngreen","lemonchiffon","lightblue","lightcoral","lightcyan","lightgoldenrodyellow","lightgray","lightgreen","lightgrey","lightpink","lightsalmon","lightseagreen","lightskyblue","lightslategray","lightslategrey","lightsteelblue","lightyellow","lime","limegreen","linen","magenta","maroon","mediumaquamarine","mediumblue","mediumorchid","mediumpurple","mediumseagreen","mediumslateblue","mediumspringgreen","mediumturquoise","mediumvioletred","midnightblue","mintcream","mistyrose","moccasin","navajowhite","navy","oldlace","olive","olivedrab","orange","orangered","orchid","palegoldenrod","palegreen","paleturquoise","palevioletred","papayawhip","peachpuff","peru","pink","plum","powderblue","purple","rebeccapurple","red","rgb","rgba","rosybrown","royalblue","saddlebrown","salmon","sandybrown","seagreen","seashell","sienna","silver","skyblue","slateblue","slategray","slategrey","snow","springgreen","steelblue","tan","teal","thistle","tomato","transparent","turquoise","unset","violet","wheat","white","whitesmoke","yellow","yellowgreen"]},"background-image":{inherited:false,supports:648,values:["-moz-element","-moz-image-rect","-moz-linear-gradient","-moz-radial-gradient","-moz-repeating-linear-gradient","-moz-repeating-radial-gradient","inherit","initial","linear-gradient","none","radial-gradient","repeating-linear-gradient","repeating-radial-gradient","unset","url"]},"background-blend-mode":{inherited:false,supports:0,values:["color","color-burn","color-dodge","darken","difference","exclusion","hard-light","hue","inherit","initial","lighten","luminosity","multiply","normal","overlay","saturation","screen","soft-light","unset"]},"background-origin":{inherited:false,supports:0,values:["border-box","content-box","inherit","initial","padding-box","unset"]},"background-position":{inherited:false,supports:3,values:["inherit","initial","unset"]},"background-repeat":{inherited:false,supports:0,values:["inherit","initial","no-repeat","repeat","repeat-x","repeat-y","unset"]},"background-size":{inherited:false,supports:3,values:["inherit","initial","unset"]},"-moz-binding":{inherited:false,supports:8,values:["inherit","initial","none","unset","url"]},"block-size":{inherited:false,supports:3,values:["-moz-calc","auto","calc","inherit","initial","unset"]},"border-block-end-color":{inherited:false,supports:4,values:["-moz-use-text-color","aliceblue","antiquewhite","aqua","aquamarine","azure","beige","bisque","black","blanchedalmond","blue","blueviolet","brown","burlywood","cadetblue","chartreuse","chocolate","coral","cornflowerblue","cornsilk","crimson","currentColor","cyan","darkblue","darkcyan","darkgoldenrod","darkgray","darkgreen","darkgrey","darkkhaki","darkmagenta","darkolivegreen","darkorange","darkorchid","darkred","darksalmon","darkseagreen","darkslateblue","darkslategray","darkslategrey","darkturquoise","darkviolet","deeppink","deepskyblue","dimgray","dimgrey","dodgerblue","firebrick","floralwhite","forestgreen","fuchsia","gainsboro","ghostwhite","gold","goldenrod","gray","grey","green","greenyellow","honeydew","hotpink","hsl","hsla","indianred","indigo","inherit","initial","ivory","khaki","lavender","lavenderblush","lawngreen","lemonchiffon","lightblue","lightcoral","lightcyan","lightgoldenrodyellow","lightgray","lightgreen","lightgrey","lightpink","lightsalmon","lightseagreen","lightskyblue","lightslategray","lightslategrey","lightsteelblue","lightyellow","lime","limegreen","linen","magenta","maroon","mediumaquamarine","mediumblue","mediumorchid","mediumpurple","mediumseagreen","mediumslateblue","mediumspringgreen","mediumturquoise","mediumvioletred","midnightblue","mintcream","mistyrose","moccasin","navajowhite","navy","oldlace","olive","olivedrab","orange","orangered","orchid","palegoldenrod","palegreen","paleturquoise","palevioletred","papayawhip","peachpuff","peru","pink","plum","powderblue","purple","rebeccapurple","red","rgb","rgba","rosybrown","royalblue","saddlebrown","salmon","sandybrown","seagreen","seashell","sienna","silver","skyblue","slateblue","slategray","slategrey","snow","springgreen","steelblue","tan","teal","thistle","tomato","transparent","turquoise","unset","violet","wheat","white","whitesmoke","yellow","yellowgreen"]},"border-block-end-style":{inherited:false,supports:0,values:["dashed","dotted","double","groove","hidden","inherit","initial","inset","none","outset","ridge","solid","unset"]},"border-block-end-width":{inherited:false,supports:1,values:["-moz-calc","calc","inherit","initial","medium","thick","thin","unset"]},"border-block-start-color":{inherited:false,supports:4,values:["-moz-use-text-color","aliceblue","antiquewhite","aqua","aquamarine","azure","beige","bisque","black","blanchedalmond","blue","blueviolet","brown","burlywood","cadetblue","chartreuse","chocolate","coral","cornflowerblue","cornsilk","crimson","currentColor","cyan","darkblue","darkcyan","darkgoldenrod","darkgray","darkgreen","darkgrey","darkkhaki","darkmagenta","darkolivegreen","darkorange","darkorchid","darkred","darksalmon","darkseagreen","darkslateblue","darkslategray","darkslategrey","darkturquoise","darkviolet","deeppink","deepskyblue","dimgray","dimgrey","dodgerblue","firebrick","floralwhite","forestgreen","fuchsia","gainsboro","ghostwhite","gold","goldenrod","gray","grey","green","greenyellow","honeydew","hotpink","hsl","hsla","indianred","indigo","inherit","initial","ivory","khaki","lavender","lavenderblush","lawngreen","lemonchiffon","lightblue","lightcoral","lightcyan","lightgoldenrodyellow","lightgray","lightgreen","lightgrey","lightpink","lightsalmon","lightseagreen","lightskyblue","lightslategray","lightslategrey","lightsteelblue","lightyellow","lime","limegreen","linen","magenta","maroon","mediumaquamarine","mediumblue","mediumorchid","mediumpurple","mediumseagreen","mediumslateblue","mediumspringgreen","mediumturquoise","mediumvioletred","midnightblue","mintcream","mistyrose","moccasin","navajowhite","navy","oldlace","olive","olivedrab","orange","orangered","orchid","palegoldenrod","palegreen","paleturquoise","palevioletred","papayawhip","peachpuff","peru","pink","plum","powderblue","purple","rebeccapurple","red","rgb","rgba","rosybrown","royalblue","saddlebrown","salmon","sandybrown","seagreen","seashell","sienna","silver","skyblue","slateblue","slategray","slategrey","snow","springgreen","steelblue","tan","teal","thistle","tomato","transparent","turquoise","unset","violet","wheat","white","whitesmoke","yellow","yellowgreen"]},"border-block-start-style":{inherited:false,supports:0,values:["dashed","dotted","double","groove","hidden","inherit","initial","inset","none","outset","ridge","solid","unset"]},"border-block-start-width":{inherited:false,supports:1,values:["-moz-calc","calc","inherit","initial","medium","thick","thin","unset"]},"border-bottom-color":{inherited:false,supports:4,values:["-moz-use-text-color","aliceblue","antiquewhite","aqua","aquamarine","azure","beige","bisque","black","blanchedalmond","blue","blueviolet","brown","burlywood","cadetblue","chartreuse","chocolate","coral","cornflowerblue","cornsilk","crimson","currentColor","cyan","darkblue","darkcyan","darkgoldenrod","darkgray","darkgreen","darkgrey","darkkhaki","darkmagenta","darkolivegreen","darkorange","darkorchid","darkred","darksalmon","darkseagreen","darkslateblue","darkslategray","darkslategrey","darkturquoise","darkviolet","deeppink","deepskyblue","dimgray","dimgrey","dodgerblue","firebrick","floralwhite","forestgreen","fuchsia","gainsboro","ghostwhite","gold","goldenrod","gray","grey","green","greenyellow","honeydew","hotpink","hsl","hsla","indianred","indigo","inherit","initial","ivory","khaki","lavender","lavenderblush","lawngreen","lemonchiffon","lightblue","lightcoral","lightcyan","lightgoldenrodyellow","lightgray","lightgreen","lightgrey","lightpink","lightsalmon","lightseagreen","lightskyblue","lightslategray","lightslategrey","lightsteelblue","lightyellow","lime","limegreen","linen","magenta","maroon","mediumaquamarine","mediumblue","mediumorchid","mediumpurple","mediumseagreen","mediumslateblue","mediumspringgreen","mediumturquoise","mediumvioletred","midnightblue","mintcream","mistyrose","moccasin","navajowhite","navy","oldlace","olive","olivedrab","orange","orangered","orchid","palegoldenrod","palegreen","paleturquoise","palevioletred","papayawhip","peachpuff","peru","pink","plum","powderblue","purple","rebeccapurple","red","rgb","rgba","rosybrown","royalblue","saddlebrown","salmon","sandybrown","seagreen","seashell","sienna","silver","skyblue","slateblue","slategray","slategrey","snow","springgreen","steelblue","tan","teal","thistle","tomato","transparent","turquoise","unset","violet","wheat","white","whitesmoke","yellow","yellowgreen"]},"-moz-border-bottom-colors":{inherited:false,supports:4,values:["inherit","initial","unset"]},"border-bottom-style":{inherited:false,supports:0,values:["dashed","dotted","double","groove","hidden","inherit","initial","inset","none","outset","ridge","solid","unset"]},"border-bottom-width":{inherited:false,supports:1,values:["-moz-calc","calc","inherit","initial","medium","thick","thin","unset"]},"border-collapse":{inherited:true,supports:0,values:["collapse","inherit","initial","separate","unset"]},"border-image-source":{inherited:false,supports:648,values:["-moz-element","-moz-image-rect","-moz-linear-gradient","-moz-radial-gradient","-moz-repeating-linear-gradient","-moz-repeating-radial-gradient","inherit","initial","linear-gradient","none","radial-gradient","repeating-linear-gradient","repeating-radial-gradient","unset","url"]},"border-image-slice":{inherited:false,supports:1026,values:["inherit","initial","unset"]},"border-image-width":{inherited:false,supports:1027,values:["inherit","initial","unset"]},"border-image-outset":{inherited:false,supports:1025,values:["inherit","initial","unset"]},"border-image-repeat":{inherited:false,supports:0,values:["inherit","initial","unset"]},"border-inline-end-color":{inherited:false,supports:4,values:["-moz-use-text-color","aliceblue","antiquewhite","aqua","aquamarine","azure","beige","bisque","black","blanchedalmond","blue","blueviolet","brown","burlywood","cadetblue","chartreuse","chocolate","coral","cornflowerblue","cornsilk","crimson","currentColor","cyan","darkblue","darkcyan","darkgoldenrod","darkgray","darkgreen","darkgrey","darkkhaki","darkmagenta","darkolivegreen","darkorange","darkorchid","darkred","darksalmon","darkseagreen","darkslateblue","darkslategray","darkslategrey","darkturquoise","darkviolet","deeppink","deepskyblue","dimgray","dimgrey","dodgerblue","firebrick","floralwhite","forestgreen","fuchsia","gainsboro","ghostwhite","gold","goldenrod","gray","grey","green","greenyellow","honeydew","hotpink","hsl","hsla","indianred","indigo","inherit","initial","ivory","khaki","lavender","lavenderblush","lawngreen","lemonchiffon","lightblue","lightcoral","lightcyan","lightgoldenrodyellow","lightgray","lightgreen","lightgrey","lightpink","lightsalmon","lightseagreen","lightskyblue","lightslategray","lightslategrey","lightsteelblue","lightyellow","lime","limegreen","linen","magenta","maroon","mediumaquamarine","mediumblue","mediumorchid","mediumpurple","mediumseagreen","mediumslateblue","mediumspringgreen","mediumturquoise","mediumvioletred","midnightblue","mintcream","mistyrose","moccasin","navajowhite","navy","oldlace","olive","olivedrab","orange","orangered","orchid","palegoldenrod","palegreen","paleturquoise","palevioletred","papayawhip","peachpuff","peru","pink","plum","powderblue","purple","rebeccapurple","red","rgb","rgba","rosybrown","royalblue","saddlebrown","salmon","sandybrown","seagreen","seashell","sienna","silver","skyblue","slateblue","slategray","slategrey","snow","springgreen","steelblue","tan","teal","thistle","tomato","transparent","turquoise","unset","violet","wheat","white","whitesmoke","yellow","yellowgreen"]},"border-inline-end-style":{inherited:false,supports:0,values:["dashed","dotted","double","groove","hidden","inherit","initial","inset","none","outset","ridge","solid","unset"]},"border-inline-end-width":{inherited:false,supports:1,values:["-moz-calc","calc","inherit","initial","medium","thick","thin","unset"]},"border-inline-start-color":{inherited:false,supports:4,values:["-moz-use-text-color","aliceblue","antiquewhite","aqua","aquamarine","azure","beige","bisque","black","blanchedalmond","blue","blueviolet","brown","burlywood","cadetblue","chartreuse","chocolate","coral","cornflowerblue","cornsilk","crimson","currentColor","cyan","darkblue","darkcyan","darkgoldenrod","darkgray","darkgreen","darkgrey","darkkhaki","darkmagenta","darkolivegreen","darkorange","darkorchid","darkred","darksalmon","darkseagreen","darkslateblue","darkslategray","darkslategrey","darkturquoise","darkviolet","deeppink","deepskyblue","dimgray","dimgrey","dodgerblue","firebrick","floralwhite","forestgreen","fuchsia","gainsboro","ghostwhite","gold","goldenrod","gray","grey","green","greenyellow","honeydew","hotpink","hsl","hsla","indianred","indigo","inherit","initial","ivory","khaki","lavender","lavenderblush","lawngreen","lemonchiffon","lightblue","lightcoral","lightcyan","lightgoldenrodyellow","lightgray","lightgreen","lightgrey","lightpink","lightsalmon","lightseagreen","lightskyblue","lightslategray","lightslategrey","lightsteelblue","lightyellow","lime","limegreen","linen","magenta","maroon","mediumaquamarine","mediumblue","mediumorchid","mediumpurple","mediumseagreen","mediumslateblue","mediumspringgreen","mediumturquoise","mediumvioletred","midnightblue","mintcream","mistyrose","moccasin","navajowhite","navy","oldlace","olive","olivedrab","orange","orangered","orchid","palegoldenrod","palegreen","paleturquoise","palevioletred","papayawhip","peachpuff","peru","pink","plum","powderblue","purple","rebeccapurple","red","rgb","rgba","rosybrown","royalblue","saddlebrown","salmon","sandybrown","seagreen","seashell","sienna","silver","skyblue","slateblue","slategray","slategrey","snow","springgreen","steelblue","tan","teal","thistle","tomato","transparent","turquoise","unset","violet","wheat","white","whitesmoke","yellow","yellowgreen"]},"border-inline-start-style":{inherited:false,supports:0,values:["dashed","dotted","double","groove","hidden","inherit","initial","inset","none","outset","ridge","solid","unset"]},"border-inline-start-width":{inherited:false,supports:1,values:["-moz-calc","calc","inherit","initial","medium","thick","thin","unset"]},"border-left-color":{inherited:false,supports:4,values:["-moz-use-text-color","aliceblue","antiquewhite","aqua","aquamarine","azure","beige","bisque","black","blanchedalmond","blue","blueviolet","brown","burlywood","cadetblue","chartreuse","chocolate","coral","cornflowerblue","cornsilk","crimson","currentColor","cyan","darkblue","darkcyan","darkgoldenrod","darkgray","darkgreen","darkgrey","darkkhaki","darkmagenta","darkolivegreen","darkorange","darkorchid","darkred","darksalmon","darkseagreen","darkslateblue","darkslategray","darkslategrey","darkturquoise","darkviolet","deeppink","deepskyblue","dimgray","dimgrey","dodgerblue","firebrick","floralwhite","forestgreen","fuchsia","gainsboro","ghostwhite","gold","goldenrod","gray","grey","green","greenyellow","honeydew","hotpink","hsl","hsla","indianred","indigo","inherit","initial","ivory","khaki","lavender","lavenderblush","lawngreen","lemonchiffon","lightblue","lightcoral","lightcyan","lightgoldenrodyellow","lightgray","lightgreen","lightgrey","lightpink","lightsalmon","lightseagreen","lightskyblue","lightslategray","lightslategrey","lightsteelblue","lightyellow","lime","limegreen","linen","magenta","maroon","mediumaquamarine","mediumblue","mediumorchid","mediumpurple","mediumseagreen","mediumslateblue","mediumspringgreen","mediumturquoise","mediumvioletred","midnightblue","mintcream","mistyrose","moccasin","navajowhite","navy","oldlace","olive","olivedrab","orange","orangered","orchid","palegoldenrod","palegreen","paleturquoise","palevioletred","papayawhip","peachpuff","peru","pink","plum","powderblue","purple","rebeccapurple","red","rgb","rgba","rosybrown","royalblue","saddlebrown","salmon","sandybrown","seagreen","seashell","sienna","silver","skyblue","slateblue","slategray","slategrey","snow","springgreen","steelblue","tan","teal","thistle","tomato","transparent","turquoise","unset","violet","wheat","white","whitesmoke","yellow","yellowgreen"]},"-moz-border-left-colors":{inherited:false,supports:4,values:["inherit","initial","unset"]},"border-left-style":{inherited:false,supports:0,values:["dashed","dotted","double","groove","hidden","inherit","initial","inset","none","outset","ridge","solid","unset"]},"border-left-width":{inherited:false,supports:1,values:["-moz-calc","calc","inherit","initial","medium","thick","thin","unset"]},"border-right-color":{inherited:false,supports:4,values:["-moz-use-text-color","aliceblue","antiquewhite","aqua","aquamarine","azure","beige","bisque","black","blanchedalmond","blue","blueviolet","brown","burlywood","cadetblue","chartreuse","chocolate","coral","cornflowerblue","cornsilk","crimson","currentColor","cyan","darkblue","darkcyan","darkgoldenrod","darkgray","darkgreen","darkgrey","darkkhaki","darkmagenta","darkolivegreen","darkorange","darkorchid","darkred","darksalmon","darkseagreen","darkslateblue","darkslategray","darkslategrey","darkturquoise","darkviolet","deeppink","deepskyblue","dimgray","dimgrey","dodgerblue","firebrick","floralwhite","forestgreen","fuchsia","gainsboro","ghostwhite","gold","goldenrod","gray","grey","green","greenyellow","honeydew","hotpink","hsl","hsla","indianred","indigo","inherit","initial","ivory","khaki","lavender","lavenderblush","lawngreen","lemonchiffon","lightblue","lightcoral","lightcyan","lightgoldenrodyellow","lightgray","lightgreen","lightgrey","lightpink","lightsalmon","lightseagreen","lightskyblue","lightslategray","lightslategrey","lightsteelblue","lightyellow","lime","limegreen","linen","magenta","maroon","mediumaquamarine","mediumblue","mediumorchid","mediumpurple","mediumseagreen","mediumslateblue","mediumspringgreen","mediumturquoise","mediumvioletred","midnightblue","mintcream","mistyrose","moccasin","navajowhite","navy","oldlace","olive","olivedrab","orange","orangered","orchid","palegoldenrod","palegreen","paleturquoise","palevioletred","papayawhip","peachpuff","peru","pink","plum","powderblue","purple","rebeccapurple","red","rgb","rgba","rosybrown","royalblue","saddlebrown","salmon","sandybrown","seagreen","seashell","sienna","silver","skyblue","slateblue","slategray","slategrey","snow","springgreen","steelblue","tan","teal","thistle","tomato","transparent","turquoise","unset","violet","wheat","white","whitesmoke","yellow","yellowgreen"]},"-moz-border-right-colors":{inherited:false,supports:4,values:["inherit","initial","unset"]},"border-right-style":{inherited:false,supports:0,values:["dashed","dotted","double","groove","hidden","inherit","initial","inset","none","outset","ridge","solid","unset"]},"border-right-width":{inherited:false,supports:1,values:["-moz-calc","calc","inherit","initial","medium","thick","thin","unset"]},"border-spacing":{inherited:true,supports:1,values:["inherit","initial","unset"]},"border-top-color":{inherited:false,supports:4,values:["-moz-use-text-color","aliceblue","antiquewhite","aqua","aquamarine","azure","beige","bisque","black","blanchedalmond","blue","blueviolet","brown","burlywood","cadetblue","chartreuse","chocolate","coral","cornflowerblue","cornsilk","crimson","currentColor","cyan","darkblue","darkcyan","darkgoldenrod","darkgray","darkgreen","darkgrey","darkkhaki","darkmagenta","darkolivegreen","darkorange","darkorchid","darkred","darksalmon","darkseagreen","darkslateblue","darkslategray","darkslategrey","darkturquoise","darkviolet","deeppink","deepskyblue","dimgray","dimgrey","dodgerblue","firebrick","floralwhite","forestgreen","fuchsia","gainsboro","ghostwhite","gold","goldenrod","gray","grey","green","greenyellow","honeydew","hotpink","hsl","hsla","indianred","indigo","inherit","initial","ivory","khaki","lavender","lavenderblush","lawngreen","lemonchiffon","lightblue","lightcoral","lightcyan","lightgoldenrodyellow","lightgray","lightgreen","lightgrey","lightpink","lightsalmon","lightseagreen","lightskyblue","lightslategray","lightslategrey","lightsteelblue","lightyellow","lime","limegreen","linen","magenta","maroon","mediumaquamarine","mediumblue","mediumorchid","mediumpurple","mediumseagreen","mediumslateblue","mediumspringgreen","mediumturquoise","mediumvioletred","midnightblue","mintcream","mistyrose","moccasin","navajowhite","navy","oldlace","olive","olivedrab","orange","orangered","orchid","palegoldenrod","palegreen","paleturquoise","palevioletred","papayawhip","peachpuff","peru","pink","plum","powderblue","purple","rebeccapurple","red","rgb","rgba","rosybrown","royalblue","saddlebrown","salmon","sandybrown","seagreen","seashell","sienna","silver","skyblue","slateblue","slategray","slategrey","snow","springgreen","steelblue","tan","teal","thistle","tomato","transparent","turquoise","unset","violet","wheat","white","whitesmoke","yellow","yellowgreen"]},"-moz-border-top-colors":{inherited:false,supports:4,values:["inherit","initial","unset"]},"border-top-style":{inherited:false,supports:0,values:["dashed","dotted","double","groove","hidden","inherit","initial","inset","none","outset","ridge","solid","unset"]},"border-top-width":{inherited:false,supports:1,values:["-moz-calc","calc","inherit","initial","medium","thick","thin","unset"]},"border-top-left-radius":{inherited:false,supports:3,values:["inherit","initial","unset"]},"border-top-right-radius":{inherited:false,supports:3,values:["inherit","initial","unset"]},"border-bottom-right-radius":{inherited:false,supports:3,values:["inherit","initial","unset"]},"border-bottom-left-radius":{inherited:false,supports:3,values:["inherit","initial","unset"]},"bottom":{inherited:false,supports:3,values:["-moz-calc","auto","calc","inherit","initial","unset"]},"box-decoration-break":{inherited:false,supports:0,values:["clone","inherit","initial","slice","unset"]},"box-shadow":{inherited:false,supports:5,values:["inherit","initial","unset"]},"box-sizing":{inherited:false,supports:0,values:["border-box","content-box","inherit","initial","padding-box","unset"]},"caption-side":{inherited:true,supports:0,values:["bottom","bottom-outside","inherit","initial","left","right","top","top-outside","unset"]},"clear":{inherited:false,supports:0,values:["both","inherit","initial","inline-end","inline-start","left","none","right","unset"]},"clip":{inherited:false,supports:0,values:["inherit","initial","unset"]},"color":{inherited:true,supports:4,values:["aliceblue","antiquewhite","aqua","aquamarine","azure","beige","bisque","black","blanchedalmond","blue","blueviolet","brown","burlywood","cadetblue","chartreuse","chocolate","coral","cornflowerblue","cornsilk","crimson","currentColor","cyan","darkblue","darkcyan","darkgoldenrod","darkgray","darkgreen","darkgrey","darkkhaki","darkmagenta","darkolivegreen","darkorange","darkorchid","darkred","darksalmon","darkseagreen","darkslateblue","darkslategray","darkslategrey","darkturquoise","darkviolet","deeppink","deepskyblue","dimgray","dimgrey","dodgerblue","firebrick","floralwhite","forestgreen","fuchsia","gainsboro","ghostwhite","gold","goldenrod","gray","grey","green","greenyellow","honeydew","hotpink","hsl","hsla","indianred","indigo","inherit","initial","ivory","khaki","lavender","lavenderblush","lawngreen","lemonchiffon","lightblue","lightcoral","lightcyan","lightgoldenrodyellow","lightgray","lightgreen","lightgrey","lightpink","lightsalmon","lightseagreen","lightskyblue","lightslategray","lightslategrey","lightsteelblue","lightyellow","lime","limegreen","linen","magenta","maroon","mediumaquamarine","mediumblue","mediumorchid","mediumpurple","mediumseagreen","mediumslateblue","mediumspringgreen","mediumturquoise","mediumvioletred","midnightblue","mintcream","mistyrose","moccasin","navajowhite","navy","oldlace","olive","olivedrab","orange","orangered","orchid","palegoldenrod","palegreen","paleturquoise","palevioletred","papayawhip","peachpuff","peru","pink","plum","powderblue","purple","rebeccapurple","red","rgb","rgba","rosybrown","royalblue","saddlebrown","salmon","sandybrown","seagreen","seashell","sienna","silver","skyblue","slateblue","slategray","slategrey","snow","springgreen","steelblue","tan","teal","thistle","tomato","transparent","turquoise","unset","violet","wheat","white","whitesmoke","yellow","yellowgreen"]},"-moz-column-count":{inherited:false,supports:1024,values:["auto","inherit","initial","unset"]},"-moz-column-fill":{inherited:false,supports:0,values:["auto","balance","inherit","initial","unset"]},"-moz-column-width":{inherited:false,supports:1,values:["-moz-calc","auto","calc","inherit","initial","unset"]},"-moz-column-gap":{inherited:false,supports:1,values:["-moz-calc","calc","inherit","initial","normal","unset"]},"-moz-column-rule-color":{inherited:false,supports:4,values:["-moz-use-text-color","aliceblue","antiquewhite","aqua","aquamarine","azure","beige","bisque","black","blanchedalmond","blue","blueviolet","brown","burlywood","cadetblue","chartreuse","chocolate","coral","cornflowerblue","cornsilk","crimson","currentColor","cyan","darkblue","darkcyan","darkgoldenrod","darkgray","darkgreen","darkgrey","darkkhaki","darkmagenta","darkolivegreen","darkorange","darkorchid","darkred","darksalmon","darkseagreen","darkslateblue","darkslategray","darkslategrey","darkturquoise","darkviolet","deeppink","deepskyblue","dimgray","dimgrey","dodgerblue","firebrick","floralwhite","forestgreen","fuchsia","gainsboro","ghostwhite","gold","goldenrod","gray","grey","green","greenyellow","honeydew","hotpink","hsl","hsla","indianred","indigo","inherit","initial","ivory","khaki","lavender","lavenderblush","lawngreen","lemonchiffon","lightblue","lightcoral","lightcyan","lightgoldenrodyellow","lightgray","lightgreen","lightgrey","lightpink","lightsalmon","lightseagreen","lightskyblue","lightslategray","lightslategrey","lightsteelblue","lightyellow","lime","limegreen","linen","magenta","maroon","mediumaquamarine","mediumblue","mediumorchid","mediumpurple","mediumseagreen","mediumslateblue","mediumspringgreen","mediumturquoise","mediumvioletred","midnightblue","mintcream","mistyrose","moccasin","navajowhite","navy","oldlace","olive","olivedrab","orange","orangered","orchid","palegoldenrod","palegreen","paleturquoise","palevioletred","papayawhip","peachpuff","peru","pink","plum","powderblue","purple","rebeccapurple","red","rgb","rgba","rosybrown","royalblue","saddlebrown","salmon","sandybrown","seagreen","seashell","sienna","silver","skyblue","slateblue","slategray","slategrey","snow","springgreen","steelblue","tan","teal","thistle","tomato","transparent","turquoise","unset","violet","wheat","white","whitesmoke","yellow","yellowgreen"]},"-moz-column-rule-style":{inherited:false,supports:0,values:["dashed","dotted","double","groove","hidden","inherit","initial","inset","none","outset","ridge","solid","unset"]},"-moz-column-rule-width":{inherited:false,supports:1,values:["-moz-calc","calc","inherit","initial","medium","thick","thin","unset"]},"contain":{inherited:false,supports:0,values:["inherit","initial","layout","none","paint","strict","style","unset"]},"content":{inherited:false,supports:8,values:["inherit","initial","unset"]},"-moz-control-character-visibility":{inherited:true,supports:0,values:["hidden","inherit","initial","unset","visible"]},"counter-increment":{inherited:false,supports:0,values:["inherit","initial","unset"]},"counter-reset":{inherited:false,supports:0,values:["inherit","initial","unset"]},"cursor":{inherited:true,supports:8,values:["inherit","initial","unset"]},"direction":{inherited:true,supports:0,values:["inherit","initial","ltr","rtl","unset"]},"display":{inherited:false,supports:0,values:["-moz-box","-moz-deck","-moz-grid","-moz-grid-group","-moz-grid-line","-moz-groupbox","-moz-inline-box","-moz-inline-grid","-moz-inline-stack","-moz-popup","-moz-stack","block","contents","flex","grid","inherit","initial","inline","inline-block","inline-flex","inline-grid","inline-table","list-item","none","ruby","ruby-base","ruby-base-container","ruby-text","ruby-text-container","table","table-caption","table-cell","table-column","table-column-group","table-footer-group","table-header-group","table-row","table-row-group","unset"]},"empty-cells":{inherited:true,supports:0,values:["hide","inherit","initial","show","unset"]},"align-content":{inherited:false,supports:0,values:["inherit","initial","unset"]},"align-items":{inherited:false,supports:0,values:["inherit","initial","unset"]},"align-self":{inherited:false,supports:0,values:["inherit","initial","unset"]},"flex-basis":{inherited:false,supports:3,values:["-moz-available","-moz-calc","-moz-fit-content","-moz-max-content","-moz-min-content","auto","calc","inherit","initial","unset"]},"flex-direction":{inherited:false,supports:0,values:["column","column-reverse","inherit","initial","row","row-reverse","unset"]},"flex-grow":{inherited:false,supports:1024,values:["inherit","initial","unset"]},"flex-shrink":{inherited:false,supports:1024,values:["inherit","initial","unset"]},"flex-wrap":{inherited:false,supports:0,values:["inherit","initial","nowrap","unset","wrap","wrap-reverse"]},"order":{inherited:false,supports:1024,values:["inherit","initial","unset"]},"justify-content":{inherited:false,supports:0,values:["inherit","initial","unset"]},"justify-items":{inherited:false,supports:0,values:["inherit","initial","unset"]},"justify-self":{inherited:false,supports:0,values:["inherit","initial","unset"]},"float":{inherited:false,supports:0,values:["inherit","initial","inline-end","inline-start","left","none","right","unset"]},"-moz-float-edge":{inherited:false,supports:0,values:["content-box","inherit","initial","margin-box","unset"]},"font-family":{inherited:true,supports:0,values:["inherit","initial","unset"]},"font-feature-settings":{inherited:true,supports:0,values:["inherit","initial","unset"]},"font-kerning":{inherited:true,supports:0,values:["auto","inherit","initial","none","normal","unset"]},"font-language-override":{inherited:true,supports:0,values:["inherit","initial","normal","unset"]},"font-size":{inherited:true,supports:3,values:["-moz-calc","calc","inherit","initial","large","larger","medium","small","smaller","unset","x-large","x-small","xx-large","xx-small"]},"font-size-adjust":{inherited:true,supports:1024,values:["inherit","initial","none","unset"]},"font-stretch":{inherited:true,supports:0,values:["condensed","expanded","extra-condensed","extra-expanded","inherit","initial","normal","semi-condensed","semi-expanded","ultra-condensed","ultra-expanded","unset"]},"font-style":{inherited:true,supports:0,values:["inherit","initial","italic","normal","oblique","unset"]},"font-synthesis":{inherited:true,supports:0,values:["inherit","initial","unset"]},"font-variant-alternates":{inherited:true,supports:0,values:["inherit","initial","unset"]},"font-variant-caps":{inherited:true,supports:0,values:["all-petite-caps","all-small-caps","inherit","initial","normal","petite-caps","small-caps","titling-caps","unicase","unset"]},"font-variant-east-asian":{inherited:true,supports:0,values:["inherit","initial","unset"]},"font-variant-ligatures":{inherited:true,supports:0,values:["inherit","initial","unset"]},"font-variant-numeric":{inherited:true,supports:0,values:["inherit","initial","unset"]},"font-variant-position":{inherited:true,supports:0,values:["inherit","initial","normal","sub","super","unset"]},"font-weight":{inherited:true,supports:1024,values:["inherit","initial","unset"]},"-moz-force-broken-image-icon":{inherited:false,supports:1024,values:["inherit","initial","unset"]},"grid-auto-flow":{inherited:false,supports:0,values:["inherit","initial","unset"]},"grid-auto-columns":{inherited:false,supports:3,values:["inherit","initial","unset"]},"grid-auto-rows":{inherited:false,supports:3,values:["inherit","initial","unset"]},"grid-template-areas":{inherited:false,supports:0,values:["inherit","initial","unset"]},"grid-template-columns":{inherited:false,supports:3,values:["inherit","initial","unset"]},"grid-template-rows":{inherited:false,supports:3,values:["inherit","initial","unset"]},"grid-column-start":{inherited:false,supports:1024,values:["inherit","initial","unset"]},"grid-column-end":{inherited:false,supports:1024,values:["inherit","initial","unset"]},"grid-row-start":{inherited:false,supports:1024,values:["inherit","initial","unset"]},"grid-row-end":{inherited:false,supports:1024,values:["inherit","initial","unset"]},"grid-column-gap":{inherited:false,supports:1,values:["-moz-calc","calc","inherit","initial","unset"]},"grid-row-gap":{inherited:false,supports:1,values:["-moz-calc","calc","inherit","initial","unset"]},"height":{inherited:false,supports:3,values:["-moz-available","-moz-calc","-moz-fit-content","-moz-max-content","-moz-min-content","auto","calc","inherit","initial","unset"]},"image-orientation":{inherited:true,supports:16,values:["inherit","initial","unset"]},"-moz-image-region":{inherited:true,supports:0,values:["inherit","initial","unset"]},"ime-mode":{inherited:false,supports:0,values:["active","auto","disabled","inactive","inherit","initial","normal","unset"]},"inline-size":{inherited:false,supports:3,values:["-moz-available","-moz-calc","-moz-fit-content","-moz-max-content","-moz-min-content","auto","calc","inherit","initial","unset"]},"left":{inherited:false,supports:3,values:["-moz-calc","auto","calc","inherit","initial","unset"]},"letter-spacing":{inherited:true,supports:1,values:["-moz-calc","calc","inherit","initial","normal","unset"]},"line-height":{inherited:true,supports:1027,values:["-moz-block-height","inherit","initial","normal","unset"]},"list-style-image":{inherited:true,supports:8,values:["inherit","initial","none","unset","url"]},"list-style-position":{inherited:true,supports:0,values:["inherit","initial","inside","outside","unset"]},"list-style-type":{inherited:true,supports:0,values:["inherit","initial","unset"]},"margin-block-end":{inherited:false,supports:3,values:["-moz-calc","auto","calc","inherit","initial","unset"]},"margin-block-start":{inherited:false,supports:3,values:["-moz-calc","auto","calc","inherit","initial","unset"]},"margin-bottom":{inherited:false,supports:3,values:["-moz-calc","auto","calc","inherit","initial","unset"]},"margin-inline-end":{inherited:false,supports:3,values:["-moz-calc","auto","calc","inherit","initial","unset"]},"margin-inline-start":{inherited:false,supports:3,values:["-moz-calc","auto","calc","inherit","initial","unset"]},"margin-left":{inherited:false,supports:3,values:["-moz-calc","auto","calc","inherit","initial","unset"]},"margin-right":{inherited:false,supports:3,values:["-moz-calc","auto","calc","inherit","initial","unset"]},"margin-top":{inherited:false,supports:3,values:["-moz-calc","auto","calc","inherit","initial","unset"]},"marker-offset":{inherited:false,supports:1,values:["-moz-calc","auto","calc","inherit","initial","unset"]},"max-block-size":{inherited:false,supports:3,values:["-moz-calc","calc","inherit","initial","none","unset"]},"max-height":{inherited:false,supports:3,values:["-moz-available","-moz-calc","-moz-fit-content","-moz-max-content","-moz-min-content","calc","inherit","initial","none","unset"]},"max-inline-size":{inherited:false,supports:3,values:["-moz-available","-moz-calc","-moz-fit-content","-moz-max-content","-moz-min-content","calc","inherit","initial","none","unset"]},"max-width":{inherited:false,supports:3,values:["-moz-available","-moz-calc","-moz-fit-content","-moz-max-content","-moz-min-content","calc","inherit","initial","none","unset"]},"min-height":{inherited:false,supports:3,values:["-moz-available","-moz-calc","-moz-fit-content","-moz-max-content","-moz-min-content","auto","calc","inherit","initial","unset"]},"min-block-size":{inherited:false,supports:3,values:["-moz-calc","auto","calc","inherit","initial","unset"]},"min-inline-size":{inherited:false,supports:3,values:["-moz-available","-moz-calc","-moz-fit-content","-moz-max-content","-moz-min-content","auto","calc","inherit","initial","unset"]},"min-width":{inherited:false,supports:3,values:["-moz-available","-moz-calc","-moz-fit-content","-moz-max-content","-moz-min-content","auto","calc","inherit","initial","unset"]},"mix-blend-mode":{inherited:false,supports:0,values:["color","color-burn","color-dodge","darken","difference","exclusion","hard-light","hue","inherit","initial","lighten","luminosity","multiply","normal","overlay","saturation","screen","soft-light","unset"]},"isolation":{inherited:false,supports:0,values:["auto","inherit","initial","isolate","unset"]},"object-fit":{inherited:false,supports:0,values:["contain","cover","fill","inherit","initial","none","scale-down","unset"]},"object-position":{inherited:false,supports:3,values:["inherit","initial","unset"]},"offset-block-end":{inherited:false,supports:3,values:["-moz-calc","auto","calc","inherit","initial","unset"]},"offset-block-start":{inherited:false,supports:3,values:["-moz-calc","auto","calc","inherit","initial","unset"]},"offset-inline-end":{inherited:false,supports:3,values:["-moz-calc","auto","calc","inherit","initial","unset"]},"offset-inline-start":{inherited:false,supports:3,values:["-moz-calc","auto","calc","inherit","initial","unset"]},"opacity":{inherited:false,supports:1024,values:["inherit","initial","unset"]},"-moz-orient":{inherited:false,supports:0,values:["block","horizontal","inherit","initial","inline","unset","vertical"]},"outline-color":{inherited:false,supports:4,values:["-moz-use-text-color","aliceblue","antiquewhite","aqua","aquamarine","azure","beige","bisque","black","blanchedalmond","blue","blueviolet","brown","burlywood","cadetblue","chartreuse","chocolate","coral","cornflowerblue","cornsilk","crimson","currentColor","cyan","darkblue","darkcyan","darkgoldenrod","darkgray","darkgreen","darkgrey","darkkhaki","darkmagenta","darkolivegreen","darkorange","darkorchid","darkred","darksalmon","darkseagreen","darkslateblue","darkslategray","darkslategrey","darkturquoise","darkviolet","deeppink","deepskyblue","dimgray","dimgrey","dodgerblue","firebrick","floralwhite","forestgreen","fuchsia","gainsboro","ghostwhite","gold","goldenrod","gray","grey","green","greenyellow","honeydew","hotpink","hsl","hsla","indianred","indigo","inherit","initial","ivory","khaki","lavender","lavenderblush","lawngreen","lemonchiffon","lightblue","lightcoral","lightcyan","lightgoldenrodyellow","lightgray","lightgreen","lightgrey","lightpink","lightsalmon","lightseagreen","lightskyblue","lightslategray","lightslategrey","lightsteelblue","lightyellow","lime","limegreen","linen","magenta","maroon","mediumaquamarine","mediumblue","mediumorchid","mediumpurple","mediumseagreen","mediumslateblue","mediumspringgreen","mediumturquoise","mediumvioletred","midnightblue","mintcream","mistyrose","moccasin","navajowhite","navy","oldlace","olive","olivedrab","orange","orangered","orchid","palegoldenrod","palegreen","paleturquoise","palevioletred","papayawhip","peachpuff","peru","pink","plum","powderblue","purple","rebeccapurple","red","rgb","rgba","rosybrown","royalblue","saddlebrown","salmon","sandybrown","seagreen","seashell","sienna","silver","skyblue","slateblue","slategray","slategrey","snow","springgreen","steelblue","tan","teal","thistle","tomato","transparent","turquoise","unset","violet","wheat","white","whitesmoke","yellow","yellowgreen"]},"outline-style":{inherited:false,supports:0,values:["auto","dashed","dotted","double","groove","inherit","initial","inset","none","outset","ridge","solid","unset"]},"outline-width":{inherited:false,supports:1,values:["-moz-calc","calc","inherit","initial","medium","thick","thin","unset"]},"outline-offset":{inherited:false,supports:1,values:["-moz-calc","calc","inherit","initial","unset"]},"overflow-x":{inherited:false,supports:0,values:["-moz-hidden-unscrollable","auto","hidden","inherit","initial","scroll","unset","visible"]},"overflow-y":{inherited:false,supports:0,values:["-moz-hidden-unscrollable","auto","hidden","inherit","initial","scroll","unset","visible"]},"padding-block-end":{inherited:false,supports:3,values:["-moz-calc","calc","inherit","initial","unset"]},"padding-block-start":{inherited:false,supports:3,values:["-moz-calc","calc","inherit","initial","unset"]},"padding-bottom":{inherited:false,supports:3,values:["-moz-calc","calc","inherit","initial","unset"]},"padding-inline-end":{inherited:false,supports:3,values:["-moz-calc","calc","inherit","initial","unset"]},"padding-inline-start":{inherited:false,supports:3,values:["-moz-calc","calc","inherit","initial","unset"]},"padding-left":{inherited:false,supports:3,values:["-moz-calc","calc","inherit","initial","unset"]},"padding-right":{inherited:false,supports:3,values:["-moz-calc","calc","inherit","initial","unset"]},"padding-top":{inherited:false,supports:3,values:["-moz-calc","calc","inherit","initial","unset"]},"page-break-after":{inherited:false,supports:0,values:["always","auto","avoid","inherit","initial","left","right","unset"]},"page-break-before":{inherited:false,supports:0,values:["always","auto","avoid","inherit","initial","left","right","unset"]},"page-break-inside":{inherited:false,supports:0,values:["auto","avoid","inherit","initial","unset"]},"paint-order":{inherited:true,supports:0,values:["inherit","initial","unset"]},"pointer-events":{inherited:true,supports:0,values:["all","auto","fill","inherit","initial","none","painted","stroke","unset","visible","visiblefill","visiblepainted","visiblestroke"]},"position":{inherited:false,supports:0,values:["absolute","fixed","inherit","initial","relative","static","sticky","unset"]},"quotes":{inherited:true,supports:0,values:["inherit","initial","unset"]},"resize":{inherited:false,supports:0,values:["both","horizontal","inherit","initial","none","unset","vertical"]},"right":{inherited:false,supports:3,values:["-moz-calc","auto","calc","inherit","initial","unset"]},"ruby-align":{inherited:true,supports:0,values:["center","inherit","initial","space-around","space-between","start","unset"]},"ruby-position":{inherited:true,supports:0,values:["inherit","initial","over","under","unset"]},"scroll-behavior":{inherited:false,supports:0,values:["auto","inherit","initial","smooth","unset"]},"scroll-snap-coordinate":{inherited:false,supports:3,values:["inherit","initial","unset"]},"scroll-snap-destination":{inherited:false,supports:3,values:["inherit","initial","unset"]},"scroll-snap-points-x":{inherited:false,supports:0,values:["inherit","initial","unset"]},"scroll-snap-points-y":{inherited:false,supports:0,values:["inherit","initial","unset"]},"scroll-snap-type-x":{inherited:false,supports:0,values:["inherit","initial","mandatory","none","proximity","unset"]},"scroll-snap-type-y":{inherited:false,supports:0,values:["inherit","initial","mandatory","none","proximity","unset"]},"table-layout":{inherited:false,supports:0,values:["auto","fixed","inherit","initial","unset"]},"text-align":{inherited:true,supports:0,values:["-moz-center","-moz-left","-moz-right","center","end","inherit","initial","justify","left","right","start","unset"]},"-moz-text-align-last":{inherited:true,supports:0,values:["auto","center","end","inherit","initial","justify","left","right","start","unset"]},"text-decoration-color":{inherited:false,supports:4,values:["-moz-use-text-color","aliceblue","antiquewhite","aqua","aquamarine","azure","beige","bisque","black","blanchedalmond","blue","blueviolet","brown","burlywood","cadetblue","chartreuse","chocolate","coral","cornflowerblue","cornsilk","crimson","currentColor","cyan","darkblue","darkcyan","darkgoldenrod","darkgray","darkgreen","darkgrey","darkkhaki","darkmagenta","darkolivegreen","darkorange","darkorchid","darkred","darksalmon","darkseagreen","darkslateblue","darkslategray","darkslategrey","darkturquoise","darkviolet","deeppink","deepskyblue","dimgray","dimgrey","dodgerblue","firebrick","floralwhite","forestgreen","fuchsia","gainsboro","ghostwhite","gold","goldenrod","gray","grey","green","greenyellow","honeydew","hotpink","hsl","hsla","indianred","indigo","inherit","initial","ivory","khaki","lavender","lavenderblush","lawngreen","lemonchiffon","lightblue","lightcoral","lightcyan","lightgoldenrodyellow","lightgray","lightgreen","lightgrey","lightpink","lightsalmon","lightseagreen","lightskyblue","lightslategray","lightslategrey","lightsteelblue","lightyellow","lime","limegreen","linen","magenta","maroon","mediumaquamarine","mediumblue","mediumorchid","mediumpurple","mediumseagreen","mediumslateblue","mediumspringgreen","mediumturquoise","mediumvioletred","midnightblue","mintcream","mistyrose","moccasin","navajowhite","navy","oldlace","olive","olivedrab","orange","orangered","orchid","palegoldenrod","palegreen","paleturquoise","palevioletred","papayawhip","peachpuff","peru","pink","plum","powderblue","purple","rebeccapurple","red","rgb","rgba","rosybrown","royalblue","saddlebrown","salmon","sandybrown","seagreen","seashell","sienna","silver","skyblue","slateblue","slategray","slategrey","snow","springgreen","steelblue","tan","teal","thistle","tomato","transparent","turquoise","unset","violet","wheat","white","whitesmoke","yellow","yellowgreen"]},"text-decoration-line":{inherited:false,supports:0,values:["inherit","initial","unset"]},"text-decoration-style":{inherited:false,supports:0,values:["-moz-none","dashed","dotted","double","inherit","initial","solid","unset","wavy"]},"text-indent":{inherited:true,supports:3,values:["-moz-calc","calc","inherit","initial","unset"]},"text-orientation":{inherited:true,supports:0,values:["inherit","initial","mixed","sideways","sideways-right","unset","upright"]},"text-overflow":{inherited:false,supports:0,values:["inherit","initial","unset"]},"text-shadow":{inherited:true,supports:5,values:["inherit","initial","unset"]},"-moz-text-size-adjust":{inherited:true,supports:0,values:["auto","inherit","initial","none","unset"]},"text-transform":{inherited:true,supports:0,values:["capitalize","full-width","inherit","initial","lowercase","none","unset","uppercase"]},"transform":{inherited:false,supports:0,values:["inherit","initial","unset"]},"transform-box":{inherited:false,supports:0,values:["border-box","fill-box","inherit","initial","unset","view-box"]},"transform-origin":{inherited:false,supports:3,values:["inherit","initial","unset"]},"perspective-origin":{inherited:false,supports:3,values:["inherit","initial","unset"]},"perspective":{inherited:false,supports:1,values:["inherit","initial","none","unset"]},"transform-style":{inherited:false,supports:0,values:["flat","inherit","initial","preserve-3d","unset"]},"backface-visibility":{inherited:false,supports:0,values:["hidden","inherit","initial","unset","visible"]},"top":{inherited:false,supports:3,values:["-moz-calc","auto","calc","inherit","initial","unset"]},"transition-delay":{inherited:false,supports:64,values:["inherit","initial","unset"]},"transition-duration":{inherited:false,supports:64,values:["inherit","initial","unset"]},"transition-property":{inherited:false,supports:0,values:["all","inherit","initial","none","unset"]},"transition-timing-function":{inherited:false,supports:256,values:["cubic-bezier","ease","ease-in","ease-in-out","ease-out","inherit","initial","linear","step-end","step-start","steps","unset"]},"unicode-bidi":{inherited:false,supports:0,values:["-moz-isolate","-moz-isolate-override","-moz-plaintext","bidi-override","embed","inherit","initial","normal","unset"]},"-moz-user-focus":{inherited:true,supports:0,values:["ignore","inherit","initial","none","normal","select-after","select-all","select-before","select-menu","select-same","unset"]},"-moz-user-input":{inherited:true,supports:0,values:["auto","disabled","enabled","inherit","initial","none","unset"]},"-moz-user-modify":{inherited:true,supports:0,values:["inherit","initial","read-only","read-write","unset","write-only"]},"-moz-user-select":{inherited:false,supports:0,values:["-moz-all","-moz-none","-moz-text","all","auto","element","elements","inherit","initial","none","text","toggle","tri-state","unset"]},"vertical-align":{inherited:false,supports:3,values:["-moz-calc","-moz-middle-with-baseline","baseline","bottom","calc","inherit","initial","middle","sub","super","text-bottom","text-top","top","unset"]},"visibility":{inherited:true,supports:0,values:["collapse","hidden","inherit","initial","unset","visible"]},"white-space":{inherited:true,supports:0,values:["-moz-pre-space","inherit","initial","normal","nowrap","pre","pre-line","pre-wrap","unset"]},"width":{inherited:false,supports:3,values:["-moz-available","-moz-calc","-moz-fit-content","-moz-max-content","-moz-min-content","auto","calc","inherit","initial","unset"]},"-moz-window-dragging":{inherited:true,supports:0,values:["drag","inherit","initial","no-drag","unset"]},"word-break":{inherited:true,supports:0,values:["break-all","inherit","initial","keep-all","normal","unset"]},"word-spacing":{inherited:true,supports:3,values:["-moz-calc","calc","inherit","initial","normal","unset"]},"word-wrap":{inherited:true,supports:0,values:["break-word","inherit","initial","normal","unset"]},"hyphens":{inherited:true,supports:0,values:["auto","inherit","initial","manual","none","unset"]},"writing-mode":{inherited:true,supports:0,values:["horizontal-tb","inherit","initial","lr","lr-tb","rl","rl-tb","sideways-lr","sideways-rl","tb","tb-rl","unset","vertical-lr","vertical-rl"]},"z-index":{inherited:false,supports:1024,values:["auto","inherit","initial","unset"]},"-moz-box-align":{inherited:false,supports:0,values:["baseline","center","end","inherit","initial","start","stretch","unset"]},"-moz-box-direction":{inherited:false,supports:0,values:["inherit","initial","normal","reverse","unset"]},"-moz-box-flex":{inherited:false,supports:1024,values:["inherit","initial","unset"]},"-moz-box-orient":{inherited:false,supports:0,values:["block-axis","horizontal","inherit","initial","inline-axis","unset","vertical"]},"-moz-box-pack":{inherited:false,supports:0,values:["center","end","inherit","initial","justify","start","unset"]},"-moz-box-ordinal-group":{inherited:false,supports:1024,values:["inherit","initial","unset"]},"-moz-stack-sizing":{inherited:false,supports:0,values:["ignore","inherit","initial","stretch-to-fit","unset"]},"clip-path":{inherited:false,supports:8,values:["inherit","initial","unset"]},"clip-rule":{inherited:true,supports:0,values:["evenodd","inherit","initial","nonzero","unset"]},"color-interpolation":{inherited:true,supports:0,values:["auto","inherit","initial","linearrgb","srgb","unset"]},"color-interpolation-filters":{inherited:true,supports:0,values:["auto","inherit","initial","linearrgb","srgb","unset"]},"dominant-baseline":{inherited:false,supports:0,values:["alphabetic","auto","central","hanging","ideographic","inherit","initial","mathematical","middle","no-change","reset-size","text-after-edge","text-before-edge","unset","use-script"]},"fill":{inherited:true,supports:12,values:["inherit","initial","unset"]},"fill-opacity":{inherited:true,supports:1024,values:["inherit","initial","unset"]},"fill-rule":{inherited:true,supports:0,values:["evenodd","inherit","initial","nonzero","unset"]},"filter":{inherited:false,supports:8,values:["inherit","initial","unset"]},"flood-color":{inherited:false,supports:4,values:["aliceblue","antiquewhite","aqua","aquamarine","azure","beige","bisque","black","blanchedalmond","blue","blueviolet","brown","burlywood","cadetblue","chartreuse","chocolate","coral","cornflowerblue","cornsilk","crimson","currentColor","cyan","darkblue","darkcyan","darkgoldenrod","darkgray","darkgreen","darkgrey","darkkhaki","darkmagenta","darkolivegreen","darkorange","darkorchid","darkred","darksalmon","darkseagreen","darkslateblue","darkslategray","darkslategrey","darkturquoise","darkviolet","deeppink","deepskyblue","dimgray","dimgrey","dodgerblue","firebrick","floralwhite","forestgreen","fuchsia","gainsboro","ghostwhite","gold","goldenrod","gray","grey","green","greenyellow","honeydew","hotpink","hsl","hsla","indianred","indigo","inherit","initial","ivory","khaki","lavender","lavenderblush","lawngreen","lemonchiffon","lightblue","lightcoral","lightcyan","lightgoldenrodyellow","lightgray","lightgreen","lightgrey","lightpink","lightsalmon","lightseagreen","lightskyblue","lightslategray","lightslategrey","lightsteelblue","lightyellow","lime","limegreen","linen","magenta","maroon","mediumaquamarine","mediumblue","mediumorchid","mediumpurple","mediumseagreen","mediumslateblue","mediumspringgreen","mediumturquoise","mediumvioletred","midnightblue","mintcream","mistyrose","moccasin","navajowhite","navy","oldlace","olive","olivedrab","orange","orangered","orchid","palegoldenrod","palegreen","paleturquoise","palevioletred","papayawhip","peachpuff","peru","pink","plum","powderblue","purple","rebeccapurple","red","rgb","rgba","rosybrown","royalblue","saddlebrown","salmon","sandybrown","seagreen","seashell","sienna","silver","skyblue","slateblue","slategray","slategrey","snow","springgreen","steelblue","tan","teal","thistle","tomato","transparent","turquoise","unset","violet","wheat","white","whitesmoke","yellow","yellowgreen"]},"flood-opacity":{inherited:false,supports:1024,values:["inherit","initial","unset"]},"image-rendering":{inherited:true,supports:0,values:["-moz-crisp-edges","auto","inherit","initial","optimizequality","optimizespeed","unset"]},"lighting-color":{inherited:false,supports:4,values:["aliceblue","antiquewhite","aqua","aquamarine","azure","beige","bisque","black","blanchedalmond","blue","blueviolet","brown","burlywood","cadetblue","chartreuse","chocolate","coral","cornflowerblue","cornsilk","crimson","currentColor","cyan","darkblue","darkcyan","darkgoldenrod","darkgray","darkgreen","darkgrey","darkkhaki","darkmagenta","darkolivegreen","darkorange","darkorchid","darkred","darksalmon","darkseagreen","darkslateblue","darkslategray","darkslategrey","darkturquoise","darkviolet","deeppink","deepskyblue","dimgray","dimgrey","dodgerblue","firebrick","floralwhite","forestgreen","fuchsia","gainsboro","ghostwhite","gold","goldenrod","gray","grey","green","greenyellow","honeydew","hotpink","hsl","hsla","indianred","indigo","inherit","initial","ivory","khaki","lavender","lavenderblush","lawngreen","lemonchiffon","lightblue","lightcoral","lightcyan","lightgoldenrodyellow","lightgray","lightgreen","lightgrey","lightpink","lightsalmon","lightseagreen","lightskyblue","lightslategray","lightslategrey","lightsteelblue","lightyellow","lime","limegreen","linen","magenta","maroon","mediumaquamarine","mediumblue","mediumorchid","mediumpurple","mediumseagreen","mediumslateblue","mediumspringgreen","mediumturquoise","mediumvioletred","midnightblue","mintcream","mistyrose","moccasin","navajowhite","navy","oldlace","olive","olivedrab","orange","orangered","orchid","palegoldenrod","palegreen","paleturquoise","palevioletred","papayawhip","peachpuff","peru","pink","plum","powderblue","purple","rebeccapurple","red","rgb","rgba","rosybrown","royalblue","saddlebrown","salmon","sandybrown","seagreen","seashell","sienna","silver","skyblue","slateblue","slategray","slategrey","snow","springgreen","steelblue","tan","teal","thistle","tomato","transparent","turquoise","unset","violet","wheat","white","whitesmoke","yellow","yellowgreen"]},"marker-end":{inherited:true,supports:8,values:["inherit","initial","none","unset","url"]},"marker-mid":{inherited:true,supports:8,values:["inherit","initial","none","unset","url"]},"marker-start":{inherited:true,supports:8,values:["inherit","initial","none","unset","url"]},"mask":{inherited:false,supports:8,values:["inherit","initial","none","unset","url"]},"mask-type":{inherited:false,supports:0,values:["alpha","inherit","initial","luminance","unset"]},"shape-rendering":{inherited:true,supports:0,values:["auto","crispedges","geometricprecision","inherit","initial","optimizespeed","unset"]},"stop-color":{inherited:false,supports:4,values:["aliceblue","antiquewhite","aqua","aquamarine","azure","beige","bisque","black","blanchedalmond","blue","blueviolet","brown","burlywood","cadetblue","chartreuse","chocolate","coral","cornflowerblue","cornsilk","crimson","currentColor","cyan","darkblue","darkcyan","darkgoldenrod","darkgray","darkgreen","darkgrey","darkkhaki","darkmagenta","darkolivegreen","darkorange","darkorchid","darkred","darksalmon","darkseagreen","darkslateblue","darkslategray","darkslategrey","darkturquoise","darkviolet","deeppink","deepskyblue","dimgray","dimgrey","dodgerblue","firebrick","floralwhite","forestgreen","fuchsia","gainsboro","ghostwhite","gold","goldenrod","gray","grey","green","greenyellow","honeydew","hotpink","hsl","hsla","indianred","indigo","inherit","initial","ivory","khaki","lavender","lavenderblush","lawngreen","lemonchiffon","lightblue","lightcoral","lightcyan","lightgoldenrodyellow","lightgray","lightgreen","lightgrey","lightpink","lightsalmon","lightseagreen","lightskyblue","lightslategray","lightslategrey","lightsteelblue","lightyellow","lime","limegreen","linen","magenta","maroon","mediumaquamarine","mediumblue","mediumorchid","mediumpurple","mediumseagreen","mediumslateblue","mediumspringgreen","mediumturquoise","mediumvioletred","midnightblue","mintcream","mistyrose","moccasin","navajowhite","navy","oldlace","olive","olivedrab","orange","orangered","orchid","palegoldenrod","palegreen","paleturquoise","palevioletred","papayawhip","peachpuff","peru","pink","plum","powderblue","purple","rebeccapurple","red","rgb","rgba","rosybrown","royalblue","saddlebrown","salmon","sandybrown","seagreen","seashell","sienna","silver","skyblue","slateblue","slategray","slategrey","snow","springgreen","steelblue","tan","teal","thistle","tomato","transparent","turquoise","unset","violet","wheat","white","whitesmoke","yellow","yellowgreen"]},"stop-opacity":{inherited:false,supports:1024,values:["inherit","initial","unset"]},"stroke":{inherited:true,supports:12,values:["inherit","initial","unset"]},"stroke-dasharray":{inherited:true,supports:1027,values:["inherit","initial","unset"]},"stroke-dashoffset":{inherited:true,supports:1027,values:["inherit","initial","unset"]},"stroke-linecap":{inherited:true,supports:0,values:["butt","inherit","initial","round","square","unset"]},"stroke-linejoin":{inherited:true,supports:0,values:["bevel","inherit","initial","miter","round","unset"]},"stroke-miterlimit":{inherited:true,supports:1024,values:["inherit","initial","unset"]},"stroke-opacity":{inherited:true,supports:1024,values:["inherit","initial","unset"]},"stroke-width":{inherited:true,supports:1027,values:["inherit","initial","unset"]},"text-anchor":{inherited:true,supports:0,values:["end","inherit","initial","middle","start","unset"]},"text-rendering":{inherited:true,supports:0,values:["auto","geometricprecision","inherit","initial","optimizelegibility","optimizespeed","unset"]},"vector-effect":{inherited:false,supports:0,values:["inherit","initial","non-scaling-stroke","none","unset"]},"will-change":{inherited:false,supports:0,values:["inherit","initial","unset"]},"-moz-outline-radius":{subproperties:["-moz-outline-radius-topleft","-moz-outline-radius-topright","-moz-outline-radius-bottomright","-moz-outline-radius-bottomleft"],inherited:false,supports:3,values:["inherit","initial","unset"]},"all":{subproperties:["-moz-appearance","-moz-outline-radius-topleft","-moz-outline-radius-topright","-moz-outline-radius-bottomright","-moz-outline-radius-bottomleft","-moz-tab-size","-x-system-font","animation-delay","animation-direction","animation-duration","animation-fill-mode","animation-iteration-count","animation-name","animation-play-state","animation-timing-function","background-attachment","background-clip","background-color","background-image","background-blend-mode","background-origin","background-position","background-repeat","background-size","-moz-binding","block-size","border-block-end-color","border-block-end-style","border-block-end-width","border-block-start-color","border-block-start-style","border-block-start-width","border-bottom-color","-moz-border-bottom-colors","border-bottom-style","border-bottom-width","border-collapse","border-image-source","border-image-slice","border-image-width","border-image-outset","border-image-repeat","border-inline-end-color","border-inline-end-style","border-inline-end-width","border-inline-start-color","border-inline-start-style","border-inline-start-width","border-left-color","-moz-border-left-colors","border-left-style","border-left-width","border-right-color","-moz-border-right-colors","border-right-style","border-right-width","border-spacing","border-top-color","-moz-border-top-colors","border-top-style","border-top-width","border-top-left-radius","border-top-right-radius","border-bottom-right-radius","border-bottom-left-radius","bottom","box-decoration-break","box-shadow","box-sizing","caption-side","clear","clip","color","-moz-column-count","-moz-column-fill","-moz-column-width","-moz-column-gap","-moz-column-rule-color","-moz-column-rule-style","-moz-column-rule-width","contain","content","-moz-control-character-visibility","counter-increment","counter-reset","cursor","display","empty-cells","align-content","align-items","align-self","flex-basis","flex-direction","flex-grow","flex-shrink","flex-wrap","order","justify-content","justify-items","justify-self","float","-moz-float-edge","font-family","font-feature-settings","font-kerning","font-language-override","font-size","font-size-adjust","-moz-osx-font-smoothing","font-stretch","font-style","font-synthesis","font-variant-alternates","font-variant-caps","font-variant-east-asian","font-variant-ligatures","font-variant-numeric","font-variant-position","font-weight","-moz-force-broken-image-icon","grid-auto-flow","grid-auto-columns","grid-auto-rows","grid-template-areas","grid-template-columns","grid-template-rows","grid-column-start","grid-column-end","grid-row-start","grid-row-end","grid-column-gap","grid-row-gap","height","image-orientation","-moz-image-region","ime-mode","inline-size","left","letter-spacing","line-height","list-style-image","list-style-position","list-style-type","margin-block-end","margin-block-start","margin-bottom","margin-inline-end","margin-inline-start","margin-left","margin-right","margin-top","marker-offset","max-block-size","max-height","max-inline-size","max-width","-moz-min-font-size-ratio","min-height","min-block-size","min-inline-size","min-width","mix-blend-mode","isolation","object-fit","object-position","offset-block-end","offset-block-start","offset-inline-end","offset-inline-start","opacity","-moz-orient","outline-color","outline-style","outline-width","outline-offset","overflow-clip-box","overflow-x","overflow-y","padding-block-end","padding-block-start","padding-bottom","padding-inline-end","padding-inline-start","padding-left","padding-right","padding-top","page-break-after","page-break-before","page-break-inside","paint-order","pointer-events","position","quotes","resize","right","ruby-align","ruby-position","scroll-behavior","scroll-snap-coordinate","scroll-snap-destination","scroll-snap-points-x","scroll-snap-points-y","scroll-snap-type-x","scroll-snap-type-y","table-layout","text-align","-moz-text-align-last","text-combine-upright","text-decoration-color","text-decoration-line","text-decoration-style","text-indent","text-orientation","text-overflow","text-shadow","-moz-text-size-adjust","text-transform","transform","transform-box","transform-origin","perspective-origin","perspective","transform-style","backface-visibility","top","-moz-top-layer","touch-action","transition-delay","transition-duration","transition-property","transition-timing-function","-moz-user-focus","-moz-user-input","-moz-user-modify","-moz-user-select","vertical-align","visibility","white-space","width","-moz-window-dragging","-moz-window-shadow","word-break","word-spacing","word-wrap","hyphens","writing-mode","z-index","-moz-box-align","-moz-box-direction","-moz-box-flex","-moz-box-orient","-moz-box-pack","-moz-box-ordinal-group","-moz-stack-sizing","clip-path","clip-rule","color-interpolation","color-interpolation-filters","dominant-baseline","fill","fill-opacity","fill-rule","filter","flood-color","flood-opacity","image-rendering","lighting-color","marker-end","marker-mid","marker-start","mask","mask-type","shape-rendering","stop-color","stop-opacity","stroke","stroke-dasharray","stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke-width","text-anchor","text-rendering","vector-effect","will-change"],inherited:false,supports:2015,values:["-moz-all","-moz-available","-moz-block-height","-moz-box","-moz-calc","-moz-center","-moz-crisp-edges","-moz-deck","-moz-element","-moz-fit-content","-moz-grid","-moz-grid-group","-moz-grid-line","-moz-groupbox","-moz-gtk-info-bar","-moz-hidden-unscrollable","-moz-image-rect","-moz-inline-box","-moz-inline-grid","-moz-inline-stack","-moz-left","-moz-linear-gradient","-moz-mac-disclosure-button-closed","-moz-mac-disclosure-button-open","-moz-mac-fullscreen-button","-moz-mac-help-button","-moz-mac-vibrancy-dark","-moz-mac-vibrancy-light","-moz-max-content","-moz-middle-with-baseline","-moz-min-content","-moz-none","-moz-popup","-moz-pre-space","-moz-radial-gradient","-moz-repeating-linear-gradient","-moz-repeating-radial-gradient","-moz-right","-moz-stack","-moz-text","-moz-use-text-color","-moz-win-borderless-glass","-moz-win-browsertabbar-toolbox","-moz-win-communications-toolbox","-moz-win-exclude-glass","-moz-win-glass","-moz-win-media-toolbox","-moz-window-button-box","-moz-window-button-box-maximized","-moz-window-button-close","-moz-window-button-maximize","-moz-window-button-minimize","-moz-window-button-restore","-moz-window-frame-bottom","-moz-window-frame-left","-moz-window-frame-right","-moz-window-titlebar","-moz-window-titlebar-maximized","absolute","active","aliceblue","all","all-petite-caps","all-small-caps","alpha","alphabetic","alternate","alternate-reverse","always","antiquewhite","aqua","aquamarine","auto","avoid","azure","backwards","balance","baseline","beige","bevel","bisque","black","blanchedalmond","block","block-axis","blue","blueviolet","border-box","both","bottom","bottom-outside","break-all","break-word","brown","burlywood","butt","button","button-arrow-down","button-arrow-next","button-arrow-previous","button-arrow-up","button-bevel","button-focus","cadetblue","calc","capitalize","caret","center","central","chartreuse","checkbox","checkbox-container","checkbox-label","checkmenuitem","chocolate","clone","collapse","color","color-burn","color-dodge","column","column-reverse","condensed","contain","content-box","contents","coral","cornflowerblue","cornsilk","cover","crimson","crispedges","cubic-bezier","currentColor","cyan","darkblue","darkcyan","darken","darkgoldenrod","darkgray","darkgreen","darkgrey","darkkhaki","darkmagenta","darkolivegreen","darkorange","darkorchid","darkred","darksalmon","darkseagreen","darkslateblue","darkslategray","darkslategrey","darkturquoise","darkviolet","dashed","deeppink","deepskyblue","dialog","difference","dimgray","dimgrey","disabled","dodgerblue","dotted","double","drag","dualbutton","ease","ease-in","ease-in-out","ease-out","element","elements","enabled","end","evenodd","exclusion","expanded","extra-condensed","extra-expanded","fill","fill-box","firebrick","fixed","flat","flex","floralwhite","forestgreen","forwards","fuchsia","full-width","gainsboro","geometricprecision","ghostwhite","gold","goldenrod","gray","grey","green","greenyellow","grid","groove","groupbox","hanging","hard-light","hidden","hide","honeydew","horizontal","horizontal-tb","hotpink","hsl","hsla","hue","ideographic","ignore","inactive","indianred","indigo","infinite","inherit","initial","inline","inline-axis","inline-block","inline-end","inline-flex","inline-grid","inline-start","inline-table","inset","inside","isolate","italic","ivory","justify","keep-all","khaki","large","larger","lavender","lavenderblush","lawngreen","layout","left","lemonchiffon","lightblue","lightcoral","lightcyan","lighten","lightgoldenrodyellow","lightgray","lightgreen","lightgrey","lightpink","lightsalmon","lightseagreen","lightskyblue","lightslategray","lightslategrey","lightsteelblue","lightyellow","lime","limegreen","linear","linear-gradient","linearrgb","linen","list-item","listbox","listitem","local","lowercase","lr","lr-tb","luminance","luminosity","magenta","mandatory","manual","margin-box","maroon","mathematical","medium","mediumaquamarine","mediumblue","mediumorchid","mediumpurple","mediumseagreen","mediumslateblue","mediumspringgreen","mediumturquoise","mediumvioletred","menuarrow","menubar","menucheckbox","menuimage","menuitem","menuitemtext","menulist","menulist-button","menulist-text","menulist-textfield","menupopup","menuradio","menuseparator","meterbar","meterchunk","middle","midnightblue","mintcream","mistyrose","miter","mixed","moccasin","multiply","navajowhite","navy","no-change","no-drag","no-repeat","non-scaling-stroke","none","nonzero","normal","nowrap","number-input","oblique","oldlace","olive","olivedrab","optimizelegibility","optimizequality","optimizespeed","orange","orangered","orchid","outset","outside","over","overlay","padding-box","paint","painted","palegoldenrod","palegreen","paleturquoise","palevioletred","papayawhip","paused","peachpuff","peru","petite-caps","pink","plum","powderblue","pre","pre-line","pre-wrap","preserve-3d","progressbar","progressbar-vertical","progresschunk","progresschunk-vertical","proximity","purple","radial-gradient","radio","radio-container","radio-label","radiomenuitem","range","range-thumb","read-only","read-write","rebeccapurple","red","relative","repeat","repeat-x","repeat-y","repeating-linear-gradient","repeating-radial-gradient","reset-size","resizer","resizerpanel","reverse","rgb","rgba","ridge","right","rl","rl-tb","rosybrown","round","row","row-reverse","royalblue","ruby","ruby-base","ruby-base-container","ruby-text","ruby-text-container","running","saddlebrown","salmon","sandybrown","saturation","scale-down","scale-horizontal","scale-vertical","scalethumb-horizontal","scalethumb-vertical","scalethumbend","scalethumbstart","scalethumbtick","screen","scroll","scrollbar","scrollbar-small","scrollbarbutton-down","scrollbarbutton-left","scrollbarbutton-right","scrollbarbutton-up","scrollbarthumb-horizontal","scrollbarthumb-vertical","scrollbartrack-horizontal","scrollbartrack-vertical","seagreen","searchfield","seashell","select-after","select-all","select-before","select-menu","select-same","semi-condensed","semi-expanded","separate","separator","show","sideways","sideways-lr","sideways-right","sideways-rl","sienna","silver","skyblue","slateblue","slategray","slategrey","slice","small","small-caps","smaller","smooth","snow","soft-light","solid","space-around","space-between","spinner","spinner-downbutton","spinner-textfield","spinner-upbutton","splitter","springgreen","square","srgb","start","static","statusbar","statusbarpanel","steelblue","step-end","step-start","steps","sticky","stretch","stretch-to-fit","strict","stroke","style","sub","super","tab","tab-scroll-arrow-back","tab-scroll-arrow-forward","table","table-caption","table-cell","table-column","table-column-group","table-footer-group","table-header-group","table-row","table-row-group","tabpanel","tabpanels","tan","tb","tb-rl","teal","text","text-after-edge","text-before-edge","text-bottom","text-top","textfield","textfield-multiline","thick","thin","thistle","titling-caps","toggle","tomato","toolbar","toolbarbutton","toolbarbutton-dropdown","toolbargripper","toolbox","tooltip","top","top-outside","transparent","treeheader","treeheadercell","treeheadersortarrow","treeitem","treeline","treetwisty","treetwistyopen","treeview","tri-state","turquoise","ultra-condensed","ultra-expanded","under","unicase","unset","uppercase","upright","url","use-script","vertical","vertical-lr","vertical-rl","view-box","violet","visible","visiblefill","visiblepainted","visiblestroke","wavy","wheat","white","whitesmoke","window","wrap","wrap-reverse","write-only","x-large","x-small","xx-large","xx-small","yellow","yellowgreen"]},"animation":{subproperties:["animation-duration","animation-timing-function","animation-delay","animation-direction","animation-fill-mode","animation-iteration-count","animation-play-state","animation-name"],inherited:false,supports:1344,values:["alternate","alternate-reverse","backwards","both","cubic-bezier","ease","ease-in","ease-in-out","ease-out","forwards","infinite","inherit","initial","linear","none","normal","paused","reverse","running","step-end","step-start","steps","unset"]},"background":{subproperties:["background-color","background-image","background-repeat","background-attachment","background-position","background-clip","background-origin","background-size"],inherited:false,supports:655,values:["-moz-element","-moz-image-rect","-moz-linear-gradient","-moz-radial-gradient","-moz-repeating-linear-gradient","-moz-repeating-radial-gradient","aliceblue","antiquewhite","aqua","aquamarine","azure","beige","bisque","black","blanchedalmond","blue","blueviolet","border-box","brown","burlywood","cadetblue","chartreuse","chocolate","content-box","coral","cornflowerblue","cornsilk","crimson","currentColor","cyan","darkblue","darkcyan","darkgoldenrod","darkgray","darkgreen","darkgrey","darkkhaki","darkmagenta","darkolivegreen","darkorange","darkorchid","darkred","darksalmon","darkseagreen","darkslateblue","darkslategray","darkslategrey","darkturquoise","darkviolet","deeppink","deepskyblue","dimgray","dimgrey","dodgerblue","firebrick","fixed","floralwhite","forestgreen","fuchsia","gainsboro","ghostwhite","gold","goldenrod","gray","grey","green","greenyellow","honeydew","hotpink","hsl","hsla","indianred","indigo","inherit","initial","ivory","khaki","lavender","lavenderblush","lawngreen","lemonchiffon","lightblue","lightcoral","lightcyan","lightgoldenrodyellow","lightgray","lightgreen","lightgrey","lightpink","lightsalmon","lightseagreen","lightskyblue","lightslategray","lightslategrey","lightsteelblue","lightyellow","lime","limegreen","linear-gradient","linen","local","magenta","maroon","mediumaquamarine","mediumblue","mediumorchid","mediumpurple","mediumseagreen","mediumslateblue","mediumspringgreen","mediumturquoise","mediumvioletred","midnightblue","mintcream","mistyrose","moccasin","navajowhite","navy","no-repeat","none","oldlace","olive","olivedrab","orange","orangered","orchid","padding-box","palegoldenrod","palegreen","paleturquoise","palevioletred","papayawhip","peachpuff","peru","pink","plum","powderblue","purple","radial-gradient","rebeccapurple","red","repeat","repeat-x","repeat-y","repeating-linear-gradient","repeating-radial-gradient","rgb","rgba","rosybrown","royalblue","saddlebrown","salmon","sandybrown","scroll","seagreen","seashell","sienna","silver","skyblue","slateblue","slategray","slategrey","snow","springgreen","steelblue","tan","teal","thistle","tomato","transparent","turquoise","unset","url","violet","wheat","white","whitesmoke","yellow","yellowgreen"]},"border":{subproperties:["border-top-width","border-right-width","border-bottom-width","border-left-width","border-top-style","border-right-style","border-bottom-style","border-left-style","border-top-color","border-right-color","border-bottom-color","border-left-color","-moz-border-top-colors","-moz-border-right-colors","-moz-border-bottom-colors","-moz-border-left-colors","border-image-source","border-image-slice","border-image-width","border-image-outset","border-image-repeat"],inherited:false,supports:5,values:["-moz-calc","-moz-element","-moz-image-rect","-moz-linear-gradient","-moz-radial-gradient","-moz-repeating-linear-gradient","-moz-repeating-radial-gradient","-moz-use-text-color","aliceblue","antiquewhite","aqua","aquamarine","azure","beige","bisque","black","blanchedalmond","blue","blueviolet","brown","burlywood","cadetblue","calc","chartreuse","chocolate","coral","cornflowerblue","cornsilk","crimson","currentColor","cyan","darkblue","darkcyan","darkgoldenrod","darkgray","darkgreen","darkgrey","darkkhaki","darkmagenta","darkolivegreen","darkorange","darkorchid","darkred","darksalmon","darkseagreen","darkslateblue","darkslategray","darkslategrey","darkturquoise","darkviolet","dashed","deeppink","deepskyblue","dimgray","dimgrey","dodgerblue","dotted","double","firebrick","floralwhite","forestgreen","fuchsia","gainsboro","ghostwhite","gold","goldenrod","gray","grey","green","greenyellow","groove","hidden","honeydew","hotpink","hsl","hsla","indianred","indigo","inherit","initial","inset","ivory","khaki","lavender","lavenderblush","lawngreen","lemonchiffon","lightblue","lightcoral","lightcyan","lightgoldenrodyellow","lightgray","lightgreen","lightgrey","lightpink","lightsalmon","lightseagreen","lightskyblue","lightslategray","lightslategrey","lightsteelblue","lightyellow","lime","limegreen","linear-gradient","linen","magenta","maroon","medium","mediumaquamarine","mediumblue","mediumorchid","mediumpurple","mediumseagreen","mediumslateblue","mediumspringgreen","mediumturquoise","mediumvioletred","midnightblue","mintcream","mistyrose","moccasin","navajowhite","navy","none","oldlace","olive","olivedrab","orange","orangered","orchid","outset","palegoldenrod","palegreen","paleturquoise","palevioletred","papayawhip","peachpuff","peru","pink","plum","powderblue","purple","radial-gradient","rebeccapurple","red","repeating-linear-gradient","repeating-radial-gradient","rgb","rgba","ridge","rosybrown","royalblue","saddlebrown","salmon","sandybrown","seagreen","seashell","sienna","silver","skyblue","slateblue","slategray","slategrey","snow","solid","springgreen","steelblue","tan","teal","thick","thin","thistle","tomato","transparent","turquoise","unset","url","violet","wheat","white","whitesmoke","yellow","yellowgreen"]},"border-block-end":{subproperties:["border-block-end-width","border-block-end-style","border-block-end-color"],inherited:false,supports:5,values:["-moz-calc","-moz-use-text-color","aliceblue","antiquewhite","aqua","aquamarine","azure","beige","bisque","black","blanchedalmond","blue","blueviolet","brown","burlywood","cadetblue","calc","chartreuse","chocolate","coral","cornflowerblue","cornsilk","crimson","currentColor","cyan","darkblue","darkcyan","darkgoldenrod","darkgray","darkgreen","darkgrey","darkkhaki","darkmagenta","darkolivegreen","darkorange","darkorchid","darkred","darksalmon","darkseagreen","darkslateblue","darkslategray","darkslategrey","darkturquoise","darkviolet","dashed","deeppink","deepskyblue","dimgray","dimgrey","dodgerblue","dotted","double","firebrick","floralwhite","forestgreen","fuchsia","gainsboro","ghostwhite","gold","goldenrod","gray","grey","green","greenyellow","groove","hidden","honeydew","hotpink","hsl","hsla","indianred","indigo","inherit","initial","inset","ivory","khaki","lavender","lavenderblush","lawngreen","lemonchiffon","lightblue","lightcoral","lightcyan","lightgoldenrodyellow","lightgray","lightgreen","lightgrey","lightpink","lightsalmon","lightseagreen","lightskyblue","lightslategray","lightslategrey","lightsteelblue","lightyellow","lime","limegreen","linen","magenta","maroon","medium","mediumaquamarine","mediumblue","mediumorchid","mediumpurple","mediumseagreen","mediumslateblue","mediumspringgreen","mediumturquoise","mediumvioletred","midnightblue","mintcream","mistyrose","moccasin","navajowhite","navy","none","oldlace","olive","olivedrab","orange","orangered","orchid","outset","palegoldenrod","palegreen","paleturquoise","palevioletred","papayawhip","peachpuff","peru","pink","plum","powderblue","purple","rebeccapurple","red","rgb","rgba","ridge","rosybrown","royalblue","saddlebrown","salmon","sandybrown","seagreen","seashell","sienna","silver","skyblue","slateblue","slategray","slategrey","snow","solid","springgreen","steelblue","tan","teal","thick","thin","thistle","tomato","transparent","turquoise","unset","violet","wheat","white","whitesmoke","yellow","yellowgreen"]},"border-block-start":{subproperties:["border-block-start-width","border-block-start-style","border-block-start-color"],inherited:false,supports:5,values:["-moz-calc","-moz-use-text-color","aliceblue","antiquewhite","aqua","aquamarine","azure","beige","bisque","black","blanchedalmond","blue","blueviolet","brown","burlywood","cadetblue","calc","chartreuse","chocolate","coral","cornflowerblue","cornsilk","crimson","currentColor","cyan","darkblue","darkcyan","darkgoldenrod","darkgray","darkgreen","darkgrey","darkkhaki","darkmagenta","darkolivegreen","darkorange","darkorchid","darkred","darksalmon","darkseagreen","darkslateblue","darkslategray","darkslategrey","darkturquoise","darkviolet","dashed","deeppink","deepskyblue","dimgray","dimgrey","dodgerblue","dotted","double","firebrick","floralwhite","forestgreen","fuchsia","gainsboro","ghostwhite","gold","goldenrod","gray","grey","green","greenyellow","groove","hidden","honeydew","hotpink","hsl","hsla","indianred","indigo","inherit","initial","inset","ivory","khaki","lavender","lavenderblush","lawngreen","lemonchiffon","lightblue","lightcoral","lightcyan","lightgoldenrodyellow","lightgray","lightgreen","lightgrey","lightpink","lightsalmon","lightseagreen","lightskyblue","lightslategray","lightslategrey","lightsteelblue","lightyellow","lime","limegreen","linen","magenta","maroon","medium","mediumaquamarine","mediumblue","mediumorchid","mediumpurple","mediumseagreen","mediumslateblue","mediumspringgreen","mediumturquoise","mediumvioletred","midnightblue","mintcream","mistyrose","moccasin","navajowhite","navy","none","oldlace","olive","olivedrab","orange","orangered","orchid","outset","palegoldenrod","palegreen","paleturquoise","palevioletred","papayawhip","peachpuff","peru","pink","plum","powderblue","purple","rebeccapurple","red","rgb","rgba","ridge","rosybrown","royalblue","saddlebrown","salmon","sandybrown","seagreen","seashell","sienna","silver","skyblue","slateblue","slategray","slategrey","snow","solid","springgreen","steelblue","tan","teal","thick","thin","thistle","tomato","transparent","turquoise","unset","violet","wheat","white","whitesmoke","yellow","yellowgreen"]},"border-bottom":{subproperties:["border-bottom-width","border-bottom-style","border-bottom-color"],inherited:false,supports:5,values:["-moz-calc","-moz-use-text-color","aliceblue","antiquewhite","aqua","aquamarine","azure","beige","bisque","black","blanchedalmond","blue","blueviolet","brown","burlywood","cadetblue","calc","chartreuse","chocolate","coral","cornflowerblue","cornsilk","crimson","currentColor","cyan","darkblue","darkcyan","darkgoldenrod","darkgray","darkgreen","darkgrey","darkkhaki","darkmagenta","darkolivegreen","darkorange","darkorchid","darkred","darksalmon","darkseagreen","darkslateblue","darkslategray","darkslategrey","darkturquoise","darkviolet","dashed","deeppink","deepskyblue","dimgray","dimgrey","dodgerblue","dotted","double","firebrick","floralwhite","forestgreen","fuchsia","gainsboro","ghostwhite","gold","goldenrod","gray","grey","green","greenyellow","groove","hidden","honeydew","hotpink","hsl","hsla","indianred","indigo","inherit","initial","inset","ivory","khaki","lavender","lavenderblush","lawngreen","lemonchiffon","lightblue","lightcoral","lightcyan","lightgoldenrodyellow","lightgray","lightgreen","lightgrey","lightpink","lightsalmon","lightseagreen","lightskyblue","lightslategray","lightslategrey","lightsteelblue","lightyellow","lime","limegreen","linen","magenta","maroon","medium","mediumaquamarine","mediumblue","mediumorchid","mediumpurple","mediumseagreen","mediumslateblue","mediumspringgreen","mediumturquoise","mediumvioletred","midnightblue","mintcream","mistyrose","moccasin","navajowhite","navy","none","oldlace","olive","olivedrab","orange","orangered","orchid","outset","palegoldenrod","palegreen","paleturquoise","palevioletred","papayawhip","peachpuff","peru","pink","plum","powderblue","purple","rebeccapurple","red","rgb","rgba","ridge","rosybrown","royalblue","saddlebrown","salmon","sandybrown","seagreen","seashell","sienna","silver","skyblue","slateblue","slategray","slategrey","snow","solid","springgreen","steelblue","tan","teal","thick","thin","thistle","tomato","transparent","turquoise","unset","violet","wheat","white","whitesmoke","yellow","yellowgreen"]},"border-color":{subproperties:["border-top-color","border-right-color","border-bottom-color","border-left-color"],inherited:false,supports:4,values:["-moz-use-text-color","aliceblue","antiquewhite","aqua","aquamarine","azure","beige","bisque","black","blanchedalmond","blue","blueviolet","brown","burlywood","cadetblue","chartreuse","chocolate","coral","cornflowerblue","cornsilk","crimson","currentColor","cyan","darkblue","darkcyan","darkgoldenrod","darkgray","darkgreen","darkgrey","darkkhaki","darkmagenta","darkolivegreen","darkorange","darkorchid","darkred","darksalmon","darkseagreen","darkslateblue","darkslategray","darkslategrey","darkturquoise","darkviolet","deeppink","deepskyblue","dimgray","dimgrey","dodgerblue","firebrick","floralwhite","forestgreen","fuchsia","gainsboro","ghostwhite","gold","goldenrod","gray","grey","green","greenyellow","honeydew","hotpink","hsl","hsla","indianred","indigo","inherit","initial","ivory","khaki","lavender","lavenderblush","lawngreen","lemonchiffon","lightblue","lightcoral","lightcyan","lightgoldenrodyellow","lightgray","lightgreen","lightgrey","lightpink","lightsalmon","lightseagreen","lightskyblue","lightslategray","lightslategrey","lightsteelblue","lightyellow","lime","limegreen","linen","magenta","maroon","mediumaquamarine","mediumblue","mediumorchid","mediumpurple","mediumseagreen","mediumslateblue","mediumspringgreen","mediumturquoise","mediumvioletred","midnightblue","mintcream","mistyrose","moccasin","navajowhite","navy","oldlace","olive","olivedrab","orange","orangered","orchid","palegoldenrod","palegreen","paleturquoise","palevioletred","papayawhip","peachpuff","peru","pink","plum","powderblue","purple","rebeccapurple","red","rgb","rgba","rosybrown","royalblue","saddlebrown","salmon","sandybrown","seagreen","seashell","sienna","silver","skyblue","slateblue","slategray","slategrey","snow","springgreen","steelblue","tan","teal","thistle","tomato","transparent","turquoise","unset","violet","wheat","white","whitesmoke","yellow","yellowgreen"]},"border-image":{subproperties:["border-image-source","border-image-slice","border-image-width","border-image-outset","border-image-repeat"],inherited:false,supports:1675,values:["-moz-element","-moz-image-rect","-moz-linear-gradient","-moz-radial-gradient","-moz-repeating-linear-gradient","-moz-repeating-radial-gradient","inherit","initial","linear-gradient","none","radial-gradient","repeating-linear-gradient","repeating-radial-gradient","unset","url"]},"border-inline-end":{subproperties:["border-inline-end-width","border-inline-end-style","border-inline-end-color"],inherited:false,supports:5,values:["-moz-calc","-moz-use-text-color","aliceblue","antiquewhite","aqua","aquamarine","azure","beige","bisque","black","blanchedalmond","blue","blueviolet","brown","burlywood","cadetblue","calc","chartreuse","chocolate","coral","cornflowerblue","cornsilk","crimson","currentColor","cyan","darkblue","darkcyan","darkgoldenrod","darkgray","darkgreen","darkgrey","darkkhaki","darkmagenta","darkolivegreen","darkorange","darkorchid","darkred","darksalmon","darkseagreen","darkslateblue","darkslategray","darkslategrey","darkturquoise","darkviolet","dashed","deeppink","deepskyblue","dimgray","dimgrey","dodgerblue","dotted","double","firebrick","floralwhite","forestgreen","fuchsia","gainsboro","ghostwhite","gold","goldenrod","gray","grey","green","greenyellow","groove","hidden","honeydew","hotpink","hsl","hsla","indianred","indigo","inherit","initial","inset","ivory","khaki","lavender","lavenderblush","lawngreen","lemonchiffon","lightblue","lightcoral","lightcyan","lightgoldenrodyellow","lightgray","lightgreen","lightgrey","lightpink","lightsalmon","lightseagreen","lightskyblue","lightslategray","lightslategrey","lightsteelblue","lightyellow","lime","limegreen","linen","magenta","maroon","medium","mediumaquamarine","mediumblue","mediumorchid","mediumpurple","mediumseagreen","mediumslateblue","mediumspringgreen","mediumturquoise","mediumvioletred","midnightblue","mintcream","mistyrose","moccasin","navajowhite","navy","none","oldlace","olive","olivedrab","orange","orangered","orchid","outset","palegoldenrod","palegreen","paleturquoise","palevioletred","papayawhip","peachpuff","peru","pink","plum","powderblue","purple","rebeccapurple","red","rgb","rgba","ridge","rosybrown","royalblue","saddlebrown","salmon","sandybrown","seagreen","seashell","sienna","silver","skyblue","slateblue","slategray","slategrey","snow","solid","springgreen","steelblue","tan","teal","thick","thin","thistle","tomato","transparent","turquoise","unset","violet","wheat","white","whitesmoke","yellow","yellowgreen"]},"border-inline-start":{subproperties:["border-inline-start-width","border-inline-start-style","border-inline-start-color"],inherited:false,supports:5,values:["-moz-calc","-moz-use-text-color","aliceblue","antiquewhite","aqua","aquamarine","azure","beige","bisque","black","blanchedalmond","blue","blueviolet","brown","burlywood","cadetblue","calc","chartreuse","chocolate","coral","cornflowerblue","cornsilk","crimson","currentColor","cyan","darkblue","darkcyan","darkgoldenrod","darkgray","darkgreen","darkgrey","darkkhaki","darkmagenta","darkolivegreen","darkorange","darkorchid","darkred","darksalmon","darkseagreen","darkslateblue","darkslategray","darkslategrey","darkturquoise","darkviolet","dashed","deeppink","deepskyblue","dimgray","dimgrey","dodgerblue","dotted","double","firebrick","floralwhite","forestgreen","fuchsia","gainsboro","ghostwhite","gold","goldenrod","gray","grey","green","greenyellow","groove","hidden","honeydew","hotpink","hsl","hsla","indianred","indigo","inherit","initial","inset","ivory","khaki","lavender","lavenderblush","lawngreen","lemonchiffon","lightblue","lightcoral","lightcyan","lightgoldenrodyellow","lightgray","lightgreen","lightgrey","lightpink","lightsalmon","lightseagreen","lightskyblue","lightslategray","lightslategrey","lightsteelblue","lightyellow","lime","limegreen","linen","magenta","maroon","medium","mediumaquamarine","mediumblue","mediumorchid","mediumpurple","mediumseagreen","mediumslateblue","mediumspringgreen","mediumturquoise","mediumvioletred","midnightblue","mintcream","mistyrose","moccasin","navajowhite","navy","none","oldlace","olive","olivedrab","orange","orangered","orchid","outset","palegoldenrod","palegreen","paleturquoise","palevioletred","papayawhip","peachpuff","peru","pink","plum","powderblue","purple","rebeccapurple","red","rgb","rgba","ridge","rosybrown","royalblue","saddlebrown","salmon","sandybrown","seagreen","seashell","sienna","silver","skyblue","slateblue","slategray","slategrey","snow","solid","springgreen","steelblue","tan","teal","thick","thin","thistle","tomato","transparent","turquoise","unset","violet","wheat","white","whitesmoke","yellow","yellowgreen"]},"border-left":{subproperties:["border-left-width","border-left-style","border-left-color"],inherited:false,supports:5,values:["-moz-calc","-moz-use-text-color","aliceblue","antiquewhite","aqua","aquamarine","azure","beige","bisque","black","blanchedalmond","blue","blueviolet","brown","burlywood","cadetblue","calc","chartreuse","chocolate","coral","cornflowerblue","cornsilk","crimson","currentColor","cyan","darkblue","darkcyan","darkgoldenrod","darkgray","darkgreen","darkgrey","darkkhaki","darkmagenta","darkolivegreen","darkorange","darkorchid","darkred","darksalmon","darkseagreen","darkslateblue","darkslategray","darkslategrey","darkturquoise","darkviolet","dashed","deeppink","deepskyblue","dimgray","dimgrey","dodgerblue","dotted","double","firebrick","floralwhite","forestgreen","fuchsia","gainsboro","ghostwhite","gold","goldenrod","gray","grey","green","greenyellow","groove","hidden","honeydew","hotpink","hsl","hsla","indianred","indigo","inherit","initial","inset","ivory","khaki","lavender","lavenderblush","lawngreen","lemonchiffon","lightblue","lightcoral","lightcyan","lightgoldenrodyellow","lightgray","lightgreen","lightgrey","lightpink","lightsalmon","lightseagreen","lightskyblue","lightslategray","lightslategrey","lightsteelblue","lightyellow","lime","limegreen","linen","magenta","maroon","medium","mediumaquamarine","mediumblue","mediumorchid","mediumpurple","mediumseagreen","mediumslateblue","mediumspringgreen","mediumturquoise","mediumvioletred","midnightblue","mintcream","mistyrose","moccasin","navajowhite","navy","none","oldlace","olive","olivedrab","orange","orangered","orchid","outset","palegoldenrod","palegreen","paleturquoise","palevioletred","papayawhip","peachpuff","peru","pink","plum","powderblue","purple","rebeccapurple","red","rgb","rgba","ridge","rosybrown","royalblue","saddlebrown","salmon","sandybrown","seagreen","seashell","sienna","silver","skyblue","slateblue","slategray","slategrey","snow","solid","springgreen","steelblue","tan","teal","thick","thin","thistle","tomato","transparent","turquoise","unset","violet","wheat","white","whitesmoke","yellow","yellowgreen"]},"border-right":{subproperties:["border-right-width","border-right-style","border-right-color"],inherited:false,supports:5,values:["-moz-calc","-moz-use-text-color","aliceblue","antiquewhite","aqua","aquamarine","azure","beige","bisque","black","blanchedalmond","blue","blueviolet","brown","burlywood","cadetblue","calc","chartreuse","chocolate","coral","cornflowerblue","cornsilk","crimson","currentColor","cyan","darkblue","darkcyan","darkgoldenrod","darkgray","darkgreen","darkgrey","darkkhaki","darkmagenta","darkolivegreen","darkorange","darkorchid","darkred","darksalmon","darkseagreen","darkslateblue","darkslategray","darkslategrey","darkturquoise","darkviolet","dashed","deeppink","deepskyblue","dimgray","dimgrey","dodgerblue","dotted","double","firebrick","floralwhite","forestgreen","fuchsia","gainsboro","ghostwhite","gold","goldenrod","gray","grey","green","greenyellow","groove","hidden","honeydew","hotpink","hsl","hsla","indianred","indigo","inherit","initial","inset","ivory","khaki","lavender","lavenderblush","lawngreen","lemonchiffon","lightblue","lightcoral","lightcyan","lightgoldenrodyellow","lightgray","lightgreen","lightgrey","lightpink","lightsalmon","lightseagreen","lightskyblue","lightslategray","lightslategrey","lightsteelblue","lightyellow","lime","limegreen","linen","magenta","maroon","medium","mediumaquamarine","mediumblue","mediumorchid","mediumpurple","mediumseagreen","mediumslateblue","mediumspringgreen","mediumturquoise","mediumvioletred","midnightblue","mintcream","mistyrose","moccasin","navajowhite","navy","none","oldlace","olive","olivedrab","orange","orangered","orchid","outset","palegoldenrod","palegreen","paleturquoise","palevioletred","papayawhip","peachpuff","peru","pink","plum","powderblue","purple","rebeccapurple","red","rgb","rgba","ridge","rosybrown","royalblue","saddlebrown","salmon","sandybrown","seagreen","seashell","sienna","silver","skyblue","slateblue","slategray","slategrey","snow","solid","springgreen","steelblue","tan","teal","thick","thin","thistle","tomato","transparent","turquoise","unset","violet","wheat","white","whitesmoke","yellow","yellowgreen"]},"border-style":{subproperties:["border-top-style","border-right-style","border-bottom-style","border-left-style"],inherited:false,supports:0,values:["dashed","dotted","double","groove","hidden","inherit","initial","inset","none","outset","ridge","solid","unset"]},"border-top":{subproperties:["border-top-width","border-top-style","border-top-color"],inherited:false,supports:5,values:["-moz-calc","-moz-use-text-color","aliceblue","antiquewhite","aqua","aquamarine","azure","beige","bisque","black","blanchedalmond","blue","blueviolet","brown","burlywood","cadetblue","calc","chartreuse","chocolate","coral","cornflowerblue","cornsilk","crimson","currentColor","cyan","darkblue","darkcyan","darkgoldenrod","darkgray","darkgreen","darkgrey","darkkhaki","darkmagenta","darkolivegreen","darkorange","darkorchid","darkred","darksalmon","darkseagreen","darkslateblue","darkslategray","darkslategrey","darkturquoise","darkviolet","dashed","deeppink","deepskyblue","dimgray","dimgrey","dodgerblue","dotted","double","firebrick","floralwhite","forestgreen","fuchsia","gainsboro","ghostwhite","gold","goldenrod","gray","grey","green","greenyellow","groove","hidden","honeydew","hotpink","hsl","hsla","indianred","indigo","inherit","initial","inset","ivory","khaki","lavender","lavenderblush","lawngreen","lemonchiffon","lightblue","lightcoral","lightcyan","lightgoldenrodyellow","lightgray","lightgreen","lightgrey","lightpink","lightsalmon","lightseagreen","lightskyblue","lightslategray","lightslategrey","lightsteelblue","lightyellow","lime","limegreen","linen","magenta","maroon","medium","mediumaquamarine","mediumblue","mediumorchid","mediumpurple","mediumseagreen","mediumslateblue","mediumspringgreen","mediumturquoise","mediumvioletred","midnightblue","mintcream","mistyrose","moccasin","navajowhite","navy","none","oldlace","olive","olivedrab","orange","orangered","orchid","outset","palegoldenrod","palegreen","paleturquoise","palevioletred","papayawhip","peachpuff","peru","pink","plum","powderblue","purple","rebeccapurple","red","rgb","rgba","ridge","rosybrown","royalblue","saddlebrown","salmon","sandybrown","seagreen","seashell","sienna","silver","skyblue","slateblue","slategray","slategrey","snow","solid","springgreen","steelblue","tan","teal","thick","thin","thistle","tomato","transparent","turquoise","unset","violet","wheat","white","whitesmoke","yellow","yellowgreen"]},"border-width":{subproperties:["border-top-width","border-right-width","border-bottom-width","border-left-width"],inherited:false,supports:1,values:["-moz-calc","calc","inherit","initial","medium","thick","thin","unset"]},"border-radius":{subproperties:["border-top-left-radius","border-top-right-radius","border-bottom-right-radius","border-bottom-left-radius"],inherited:false,supports:3,values:["inherit","initial","unset"]},"-moz-columns":{subproperties:["-moz-column-count","-moz-column-width"],inherited:false,supports:1025,values:["-moz-calc","auto","calc","inherit","initial","unset"]},"-moz-column-rule":{subproperties:["-moz-column-rule-width","-moz-column-rule-style","-moz-column-rule-color"],inherited:false,supports:5,values:["-moz-calc","-moz-use-text-color","aliceblue","antiquewhite","aqua","aquamarine","azure","beige","bisque","black","blanchedalmond","blue","blueviolet","brown","burlywood","cadetblue","calc","chartreuse","chocolate","coral","cornflowerblue","cornsilk","crimson","currentColor","cyan","darkblue","darkcyan","darkgoldenrod","darkgray","darkgreen","darkgrey","darkkhaki","darkmagenta","darkolivegreen","darkorange","darkorchid","darkred","darksalmon","darkseagreen","darkslateblue","darkslategray","darkslategrey","darkturquoise","darkviolet","dashed","deeppink","deepskyblue","dimgray","dimgrey","dodgerblue","dotted","double","firebrick","floralwhite","forestgreen","fuchsia","gainsboro","ghostwhite","gold","goldenrod","gray","grey","green","greenyellow","groove","hidden","honeydew","hotpink","hsl","hsla","indianred","indigo","inherit","initial","inset","ivory","khaki","lavender","lavenderblush","lawngreen","lemonchiffon","lightblue","lightcoral","lightcyan","lightgoldenrodyellow","lightgray","lightgreen","lightgrey","lightpink","lightsalmon","lightseagreen","lightskyblue","lightslategray","lightslategrey","lightsteelblue","lightyellow","lime","limegreen","linen","magenta","maroon","medium","mediumaquamarine","mediumblue","mediumorchid","mediumpurple","mediumseagreen","mediumslateblue","mediumspringgreen","mediumturquoise","mediumvioletred","midnightblue","mintcream","mistyrose","moccasin","navajowhite","navy","none","oldlace","olive","olivedrab","orange","orangered","orchid","outset","palegoldenrod","palegreen","paleturquoise","palevioletred","papayawhip","peachpuff","peru","pink","plum","powderblue","purple","rebeccapurple","red","rgb","rgba","ridge","rosybrown","royalblue","saddlebrown","salmon","sandybrown","seagreen","seashell","sienna","silver","skyblue","slateblue","slategray","slategrey","snow","solid","springgreen","steelblue","tan","teal","thick","thin","thistle","tomato","transparent","turquoise","unset","violet","wheat","white","whitesmoke","yellow","yellowgreen"]},"flex":{subproperties:["flex-grow","flex-shrink","flex-basis"],inherited:false,supports:1027,values:["-moz-available","-moz-calc","-moz-fit-content","-moz-max-content","-moz-min-content","auto","calc","inherit","initial","unset"]},"flex-flow":{subproperties:["flex-direction","flex-wrap"],inherited:false,supports:0,values:["column","column-reverse","inherit","initial","nowrap","row","row-reverse","unset","wrap","wrap-reverse"]},"font":{subproperties:["font-family","font-style","font-weight","font-size","line-height","font-size-adjust","font-stretch","-x-system-font","font-feature-settings","font-language-override","font-kerning","font-synthesis","font-variant-alternates","font-variant-caps","font-variant-east-asian","font-variant-ligatures","font-variant-numeric","font-variant-position"],inherited:true,supports:1027,values:["-moz-block-height","-moz-calc","all-petite-caps","all-small-caps","auto","calc","condensed","expanded","extra-condensed","extra-expanded","inherit","initial","italic","large","larger","medium","none","normal","oblique","petite-caps","semi-condensed","semi-expanded","small","small-caps","smaller","sub","super","titling-caps","ultra-condensed","ultra-expanded","unicase","unset","x-large","x-small","xx-large","xx-small"]},"font-variant":{subproperties:["font-variant-alternates","font-variant-caps","font-variant-east-asian","font-variant-ligatures","font-variant-numeric","font-variant-position"],inherited:true,supports:0,values:["all-petite-caps","all-small-caps","inherit","initial","normal","petite-caps","small-caps","sub","super","titling-caps","unicase","unset"]},"grid-template":{subproperties:["grid-template-areas","grid-template-columns","grid-template-rows"],inherited:false,supports:3,values:["inherit","initial","unset"]},"grid":{subproperties:["grid-template-areas","grid-template-columns","grid-template-rows","grid-auto-flow","grid-auto-columns","grid-auto-rows","grid-column-gap","grid-row-gap"],inherited:false,supports:3,values:["-moz-calc","calc","inherit","initial","unset"]},"grid-column":{subproperties:["grid-column-start","grid-column-end"],inherited:false,supports:1024,values:["inherit","initial","unset"]},"grid-row":{subproperties:["grid-row-start","grid-row-end"],inherited:false,supports:1024,values:["inherit","initial","unset"]},"grid-area":{subproperties:["grid-row-start","grid-column-start","grid-row-end","grid-column-end"],inherited:false,supports:1024,values:["inherit","initial","unset"]},"grid-gap":{subproperties:["grid-column-gap","grid-row-gap"],inherited:false,supports:1,values:["-moz-calc","calc","inherit","initial","unset"]},"list-style":{subproperties:["list-style-type","list-style-image","list-style-position"],inherited:true,supports:8,values:["inherit","initial","inside","none","outside","unset","url"]},"margin":{subproperties:["margin-top","margin-right","margin-bottom","margin-left"],inherited:false,supports:3,values:["-moz-calc","auto","calc","inherit","initial","unset"]},"outline":{subproperties:["outline-width","outline-style","outline-color"],inherited:false,supports:5,values:["-moz-calc","-moz-use-text-color","aliceblue","antiquewhite","aqua","aquamarine","auto","azure","beige","bisque","black","blanchedalmond","blue","blueviolet","brown","burlywood","cadetblue","calc","chartreuse","chocolate","coral","cornflowerblue","cornsilk","crimson","currentColor","cyan","darkblue","darkcyan","darkgoldenrod","darkgray","darkgreen","darkgrey","darkkhaki","darkmagenta","darkolivegreen","darkorange","darkorchid","darkred","darksalmon","darkseagreen","darkslateblue","darkslategray","darkslategrey","darkturquoise","darkviolet","dashed","deeppink","deepskyblue","dimgray","dimgrey","dodgerblue","dotted","double","firebrick","floralwhite","forestgreen","fuchsia","gainsboro","ghostwhite","gold","goldenrod","gray","grey","green","greenyellow","groove","honeydew","hotpink","hsl","hsla","indianred","indigo","inherit","initial","inset","ivory","khaki","lavender","lavenderblush","lawngreen","lemonchiffon","lightblue","lightcoral","lightcyan","lightgoldenrodyellow","lightgray","lightgreen","lightgrey","lightpink","lightsalmon","lightseagreen","lightskyblue","lightslategray","lightslategrey","lightsteelblue","lightyellow","lime","limegreen","linen","magenta","maroon","medium","mediumaquamarine","mediumblue","mediumorchid","mediumpurple","mediumseagreen","mediumslateblue","mediumspringgreen","mediumturquoise","mediumvioletred","midnightblue","mintcream","mistyrose","moccasin","navajowhite","navy","none","oldlace","olive","olivedrab","orange","orangered","orchid","outset","palegoldenrod","palegreen","paleturquoise","palevioletred","papayawhip","peachpuff","peru","pink","plum","powderblue","purple","rebeccapurple","red","rgb","rgba","ridge","rosybrown","royalblue","saddlebrown","salmon","sandybrown","seagreen","seashell","sienna","silver","skyblue","slateblue","slategray","slategrey","snow","solid","springgreen","steelblue","tan","teal","thick","thin","thistle","tomato","transparent","turquoise","unset","violet","wheat","white","whitesmoke","yellow","yellowgreen"]},"overflow":{subproperties:["overflow-x","overflow-y"],inherited:false,supports:0,values:["-moz-hidden-unscrollable","auto","hidden","inherit","initial","scroll","unset","visible"]},"padding":{subproperties:["padding-top","padding-right","padding-bottom","padding-left"],inherited:false,supports:3,values:["-moz-calc","calc","inherit","initial","unset"]},"scroll-snap-type":{subproperties:["scroll-snap-type-x","scroll-snap-type-y"],inherited:false,supports:0,values:["inherit","initial","mandatory","none","proximity","unset"]},"text-decoration":{subproperties:["text-decoration-color","text-decoration-line","text-decoration-style"],inherited:false,supports:4,values:["-moz-none","-moz-use-text-color","aliceblue","antiquewhite","aqua","aquamarine","azure","beige","bisque","black","blanchedalmond","blue","blueviolet","brown","burlywood","cadetblue","chartreuse","chocolate","coral","cornflowerblue","cornsilk","crimson","currentColor","cyan","darkblue","darkcyan","darkgoldenrod","darkgray","darkgreen","darkgrey","darkkhaki","darkmagenta","darkolivegreen","darkorange","darkorchid","darkred","darksalmon","darkseagreen","darkslateblue","darkslategray","darkslategrey","darkturquoise","darkviolet","dashed","deeppink","deepskyblue","dimgray","dimgrey","dodgerblue","dotted","double","firebrick","floralwhite","forestgreen","fuchsia","gainsboro","ghostwhite","gold","goldenrod","gray","grey","green","greenyellow","honeydew","hotpink","hsl","hsla","indianred","indigo","inherit","initial","ivory","khaki","lavender","lavenderblush","lawngreen","lemonchiffon","lightblue","lightcoral","lightcyan","lightgoldenrodyellow","lightgray","lightgreen","lightgrey","lightpink","lightsalmon","lightseagreen","lightskyblue","lightslategray","lightslategrey","lightsteelblue","lightyellow","lime","limegreen","linen","magenta","maroon","mediumaquamarine","mediumblue","mediumorchid","mediumpurple","mediumseagreen","mediumslateblue","mediumspringgreen","mediumturquoise","mediumvioletred","midnightblue","mintcream","mistyrose","moccasin","navajowhite","navy","oldlace","olive","olivedrab","orange","orangered","orchid","palegoldenrod","palegreen","paleturquoise","palevioletred","papayawhip","peachpuff","peru","pink","plum","powderblue","purple","rebeccapurple","red","rgb","rgba","rosybrown","royalblue","saddlebrown","salmon","sandybrown","seagreen","seashell","sienna","silver","skyblue","slateblue","slategray","slategrey","snow","solid","springgreen","steelblue","tan","teal","thistle","tomato","transparent","turquoise","unset","violet","wavy","wheat","white","whitesmoke","yellow","yellowgreen"]},"transition":{subproperties:["transition-property","transition-duration","transition-timing-function","transition-delay"],inherited:false,supports:320,values:["all","cubic-bezier","ease","ease-in","ease-in-out","ease-out","inherit","initial","linear","none","step-end","step-start","steps","unset"]},"marker":{subproperties:["marker-start","marker-mid","marker-end"],inherited:true,supports:8,values:["inherit","initial","none","unset","url"]},"-moz-transform":{alias:true,subproperties:["transform"],inherited:false,supports:0,values:["inherit","initial","unset"]},"-moz-transform-origin":{alias:true,inherited:false,supports:3,values:["inherit","initial","unset"]},"-moz-perspective-origin":{alias:true,inherited:false,supports:3,values:["inherit","initial","unset"]},"-moz-perspective":{alias:true,inherited:false,supports:1,values:["inherit","initial","none","unset"]},"-moz-transform-style":{alias:true,inherited:false,supports:0,values:["flat","inherit","initial","preserve-3d","unset"]},"-moz-backface-visibility":{alias:true,inherited:false,supports:0,values:["hidden","inherit","initial","unset","visible"]},"-moz-border-image":{alias:true,subproperties:["border-image-source","border-image-slice","border-image-width","border-image-outset","border-image-repeat"],inherited:false,supports:1675,values:["-moz-element","-moz-image-rect","-moz-linear-gradient","-moz-radial-gradient","-moz-repeating-linear-gradient","-moz-repeating-radial-gradient","inherit","initial","linear-gradient","none","radial-gradient","repeating-linear-gradient","repeating-radial-gradient","unset","url"]},"-moz-transition":{alias:true,subproperties:["transition-property","transition-duration","transition-timing-function","transition-delay"],inherited:false,supports:320,values:["all","cubic-bezier","ease","ease-in","ease-in-out","ease-out","inherit","initial","linear","none","step-end","step-start","steps","unset"]},"-moz-transition-delay":{alias:true,inherited:false,supports:64,values:["inherit","initial","unset"]},"-moz-transition-duration":{alias:true,inherited:false,supports:64,values:["inherit","initial","unset"]},"-moz-transition-property":{alias:true,inherited:false,supports:0,values:["all","inherit","initial","none","unset"]},"-moz-transition-timing-function":{alias:true,inherited:false,supports:256,values:["cubic-bezier","ease","ease-in","ease-in-out","ease-out","inherit","initial","linear","step-end","step-start","steps","unset"]},"-moz-animation":{alias:true,subproperties:["animation-duration","animation-timing-function","animation-delay","animation-direction","animation-fill-mode","animation-iteration-count","animation-play-state","animation-name"],inherited:false,supports:1344,values:["alternate","alternate-reverse","backwards","both","cubic-bezier","ease","ease-in","ease-in-out","ease-out","forwards","infinite","inherit","initial","linear","none","normal","paused","reverse","running","step-end","step-start","steps","unset"]},"-moz-animation-delay":{alias:true,inherited:false,supports:64,values:["inherit","initial","unset"]},"-moz-animation-direction":{alias:true,inherited:false,supports:0,values:["alternate","alternate-reverse","inherit","initial","normal","reverse","unset"]},"-moz-animation-duration":{alias:true,inherited:false,supports:64,values:["inherit","initial","unset"]},"-moz-animation-fill-mode":{alias:true,inherited:false,supports:0,values:["backwards","both","forwards","inherit","initial","none","unset"]},"-moz-animation-iteration-count":{alias:true,inherited:false,supports:1024,values:["infinite","inherit","initial","unset"]},"-moz-animation-name":{alias:true,inherited:false,supports:0,values:["inherit","initial","none","unset"]},"-moz-animation-play-state":{alias:true,inherited:false,supports:0,values:["inherit","initial","paused","running","unset"]},"-moz-animation-timing-function":{alias:true,inherited:false,supports:256,values:["cubic-bezier","ease","ease-in","ease-in-out","ease-out","inherit","initial","linear","step-end","step-start","steps","unset"]},"-moz-box-sizing":{alias:true,inherited:false,supports:0,values:["border-box","content-box","inherit","initial","padding-box","unset"]},"-moz-font-feature-settings":{alias:true,inherited:true,supports:0,values:["inherit","initial","unset"]},"-moz-font-language-override":{alias:true,inherited:true,supports:0,values:["inherit","initial","normal","unset"]},"-moz-padding-end":{alias:true,inherited:false,supports:3,values:["-moz-calc","calc","inherit","initial","unset"]},"-moz-padding-start":{alias:true,inherited:false,supports:3,values:["-moz-calc","calc","inherit","initial","unset"]},"-moz-margin-end":{alias:true,inherited:false,supports:3,values:["-moz-calc","auto","calc","inherit","initial","unset"]},"-moz-margin-start":{alias:true,inherited:false,supports:3,values:["-moz-calc","auto","calc","inherit","initial","unset"]},"-moz-border-end":{alias:true,subproperties:["border-inline-end-width","border-inline-end-style","border-inline-end-color"],inherited:false,supports:5,values:["-moz-calc","-moz-use-text-color","aliceblue","antiquewhite","aqua","aquamarine","azure","beige","bisque","black","blanchedalmond","blue","blueviolet","brown","burlywood","cadetblue","calc","chartreuse","chocolate","coral","cornflowerblue","cornsilk","crimson","currentColor","cyan","darkblue","darkcyan","darkgoldenrod","darkgray","darkgreen","darkgrey","darkkhaki","darkmagenta","darkolivegreen","darkorange","darkorchid","darkred","darksalmon","darkseagreen","darkslateblue","darkslategray","darkslategrey","darkturquoise","darkviolet","dashed","deeppink","deepskyblue","dimgray","dimgrey","dodgerblue","dotted","double","firebrick","floralwhite","forestgreen","fuchsia","gainsboro","ghostwhite","gold","goldenrod","gray","grey","green","greenyellow","groove","hidden","honeydew","hotpink","hsl","hsla","indianred","indigo","inherit","initial","inset","ivory","khaki","lavender","lavenderblush","lawngreen","lemonchiffon","lightblue","lightcoral","lightcyan","lightgoldenrodyellow","lightgray","lightgreen","lightgrey","lightpink","lightsalmon","lightseagreen","lightskyblue","lightslategray","lightslategrey","lightsteelblue","lightyellow","lime","limegreen","linen","magenta","maroon","medium","mediumaquamarine","mediumblue","mediumorchid","mediumpurple","mediumseagreen","mediumslateblue","mediumspringgreen","mediumturquoise","mediumvioletred","midnightblue","mintcream","mistyrose","moccasin","navajowhite","navy","none","oldlace","olive","olivedrab","orange","orangered","orchid","outset","palegoldenrod","palegreen","paleturquoise","palevioletred","papayawhip","peachpuff","peru","pink","plum","powderblue","purple","rebeccapurple","red","rgb","rgba","ridge","rosybrown","royalblue","saddlebrown","salmon","sandybrown","seagreen","seashell","sienna","silver","skyblue","slateblue","slategray","slategrey","snow","solid","springgreen","steelblue","tan","teal","thick","thin","thistle","tomato","transparent","turquoise","unset","violet","wheat","white","whitesmoke","yellow","yellowgreen"]},"-moz-border-end-color":{alias:true,inherited:false,supports:4,values:["-moz-use-text-color","aliceblue","antiquewhite","aqua","aquamarine","azure","beige","bisque","black","blanchedalmond","blue","blueviolet","brown","burlywood","cadetblue","chartreuse","chocolate","coral","cornflowerblue","cornsilk","crimson","currentColor","cyan","darkblue","darkcyan","darkgoldenrod","darkgray","darkgreen","darkgrey","darkkhaki","darkmagenta","darkolivegreen","darkorange","darkorchid","darkred","darksalmon","darkseagreen","darkslateblue","darkslategray","darkslategrey","darkturquoise","darkviolet","deeppink","deepskyblue","dimgray","dimgrey","dodgerblue","firebrick","floralwhite","forestgreen","fuchsia","gainsboro","ghostwhite","gold","goldenrod","gray","grey","green","greenyellow","honeydew","hotpink","hsl","hsla","indianred","indigo","inherit","initial","ivory","khaki","lavender","lavenderblush","lawngreen","lemonchiffon","lightblue","lightcoral","lightcyan","lightgoldenrodyellow","lightgray","lightgreen","lightgrey","lightpink","lightsalmon","lightseagreen","lightskyblue","lightslategray","lightslategrey","lightsteelblue","lightyellow","lime","limegreen","linen","magenta","maroon","mediumaquamarine","mediumblue","mediumorchid","mediumpurple","mediumseagreen","mediumslateblue","mediumspringgreen","mediumturquoise","mediumvioletred","midnightblue","mintcream","mistyrose","moccasin","navajowhite","navy","oldlace","olive","olivedrab","orange","orangered","orchid","palegoldenrod","palegreen","paleturquoise","palevioletred","papayawhip","peachpuff","peru","pink","plum","powderblue","purple","rebeccapurple","red","rgb","rgba","rosybrown","royalblue","saddlebrown","salmon","sandybrown","seagreen","seashell","sienna","silver","skyblue","slateblue","slategray","slategrey","snow","springgreen","steelblue","tan","teal","thistle","tomato","transparent","turquoise","unset","violet","wheat","white","whitesmoke","yellow","yellowgreen"]},"-moz-border-end-style":{alias:true,inherited:false,supports:0,values:["dashed","dotted","double","groove","hidden","inherit","initial","inset","none","outset","ridge","solid","unset"]},"-moz-border-end-width":{alias:true,inherited:false,supports:1,values:["-moz-calc","calc","inherit","initial","medium","thick","thin","unset"]},"-moz-border-start":{alias:true,subproperties:["border-inline-start-width","border-inline-start-style","border-inline-start-color"],inherited:false,supports:5,values:["-moz-calc","-moz-use-text-color","aliceblue","antiquewhite","aqua","aquamarine","azure","beige","bisque","black","blanchedalmond","blue","blueviolet","brown","burlywood","cadetblue","calc","chartreuse","chocolate","coral","cornflowerblue","cornsilk","crimson","currentColor","cyan","darkblue","darkcyan","darkgoldenrod","darkgray","darkgreen","darkgrey","darkkhaki","darkmagenta","darkolivegreen","darkorange","darkorchid","darkred","darksalmon","darkseagreen","darkslateblue","darkslategray","darkslategrey","darkturquoise","darkviolet","dashed","deeppink","deepskyblue","dimgray","dimgrey","dodgerblue","dotted","double","firebrick","floralwhite","forestgreen","fuchsia","gainsboro","ghostwhite","gold","goldenrod","gray","grey","green","greenyellow","groove","hidden","honeydew","hotpink","hsl","hsla","indianred","indigo","inherit","initial","inset","ivory","khaki","lavender","lavenderblush","lawngreen","lemonchiffon","lightblue","lightcoral","lightcyan","lightgoldenrodyellow","lightgray","lightgreen","lightgrey","lightpink","lightsalmon","lightseagreen","lightskyblue","lightslategray","lightslategrey","lightsteelblue","lightyellow","lime","limegreen","linen","magenta","maroon","medium","mediumaquamarine","mediumblue","mediumorchid","mediumpurple","mediumseagreen","mediumslateblue","mediumspringgreen","mediumturquoise","mediumvioletred","midnightblue","mintcream","mistyrose","moccasin","navajowhite","navy","none","oldlace","olive","olivedrab","orange","orangered","orchid","outset","palegoldenrod","palegreen","paleturquoise","palevioletred","papayawhip","peachpuff","peru","pink","plum","powderblue","purple","rebeccapurple","red","rgb","rgba","ridge","rosybrown","royalblue","saddlebrown","salmon","sandybrown","seagreen","seashell","sienna","silver","skyblue","slateblue","slategray","slategrey","snow","solid","springgreen","steelblue","tan","teal","thick","thin","thistle","tomato","transparent","turquoise","unset","violet","wheat","white","whitesmoke","yellow","yellowgreen"]},"-moz-border-start-color":{alias:true,inherited:false,supports:4,values:["-moz-use-text-color","aliceblue","antiquewhite","aqua","aquamarine","azure","beige","bisque","black","blanchedalmond","blue","blueviolet","brown","burlywood","cadetblue","chartreuse","chocolate","coral","cornflowerblue","cornsilk","crimson","currentColor","cyan","darkblue","darkcyan","darkgoldenrod","darkgray","darkgreen","darkgrey","darkkhaki","darkmagenta","darkolivegreen","darkorange","darkorchid","darkred","darksalmon","darkseagreen","darkslateblue","darkslategray","darkslategrey","darkturquoise","darkviolet","deeppink","deepskyblue","dimgray","dimgrey","dodgerblue","firebrick","floralwhite","forestgreen","fuchsia","gainsboro","ghostwhite","gold","goldenrod","gray","grey","green","greenyellow","honeydew","hotpink","hsl","hsla","indianred","indigo","inherit","initial","ivory","khaki","lavender","lavenderblush","lawngreen","lemonchiffon","lightblue","lightcoral","lightcyan","lightgoldenrodyellow","lightgray","lightgreen","lightgrey","lightpink","lightsalmon","lightseagreen","lightskyblue","lightslategray","lightslategrey","lightsteelblue","lightyellow","lime","limegreen","linen","magenta","maroon","mediumaquamarine","mediumblue","mediumorchid","mediumpurple","mediumseagreen","mediumslateblue","mediumspringgreen","mediumturquoise","mediumvioletred","midnightblue","mintcream","mistyrose","moccasin","navajowhite","navy","oldlace","olive","olivedrab","orange","orangered","orchid","palegoldenrod","palegreen","paleturquoise","palevioletred","papayawhip","peachpuff","peru","pink","plum","powderblue","purple","rebeccapurple","red","rgb","rgba","rosybrown","royalblue","saddlebrown","salmon","sandybrown","seagreen","seashell","sienna","silver","skyblue","slateblue","slategray","slategrey","snow","springgreen","steelblue","tan","teal","thistle","tomato","transparent","turquoise","unset","violet","wheat","white","whitesmoke","yellow","yellowgreen"]},"-moz-border-start-style":{alias:true,inherited:false,supports:0,values:["dashed","dotted","double","groove","hidden","inherit","initial","inset","none","outset","ridge","solid","unset"]},"-moz-border-start-width":{alias:true,inherited:false,supports:1,values:["-moz-calc","calc","inherit","initial","medium","thick","thin","unset"]},"-moz-hyphens":{alias:true,inherited:true,supports:0,values:["auto","inherit","initial","manual","none","unset"]},"-webkit-animation":{alias:true,subproperties:["animation-duration","animation-timing-function","animation-delay","animation-direction","animation-fill-mode","animation-iteration-count","animation-play-state","animation-name"],inherited:false,supports:1344,values:["alternate","alternate-reverse","backwards","both","cubic-bezier","ease","ease-in","ease-in-out","ease-out","forwards","infinite","inherit","initial","linear","none","normal","paused","reverse","running","step-end","step-start","steps","unset"]},"-webkit-animation-delay":{alias:true,inherited:false,supports:64,values:["inherit","initial","unset"]},"-webkit-animation-direction":{alias:true,inherited:false,supports:0,values:["alternate","alternate-reverse","inherit","initial","normal","reverse","unset"]},"-webkit-animation-duration":{alias:true,inherited:false,supports:64,values:["inherit","initial","unset"]},"-webkit-animation-fill-mode":{alias:true,inherited:false,supports:0,values:["backwards","both","forwards","inherit","initial","none","unset"]},"-webkit-animation-iteration-count":{alias:true,inherited:false,supports:1024,values:["infinite","inherit","initial","unset"]},"-webkit-animation-name":{alias:true,inherited:false,supports:0,values:["inherit","initial","none","unset"]},"-webkit-animation-play-state":{alias:true,inherited:false,supports:0,values:["inherit","initial","paused","running","unset"]},"-webkit-animation-timing-function":{alias:true,inherited:false,supports:256,values:["cubic-bezier","ease","ease-in","ease-in-out","ease-out","inherit","initial","linear","step-end","step-start","steps","unset"]},"-webkit-text-size-adjust":{alias:true,inherited:true,supports:0,values:["auto","inherit","initial","none","unset"]},"-webkit-transform":{alias:true,inherited:false,supports:0,values:["inherit","initial","unset"]},"-webkit-transform-origin":{alias:true,inherited:false,supports:3,values:["inherit","initial","unset"]},"-webkit-transform-style":{alias:true,inherited:false,supports:0,values:["flat","inherit","initial","preserve-3d","unset"]},"-webkit-backface-visibility":{alias:true,inherited:false,supports:0,values:["hidden","inherit","initial","unset","visible"]},"-webkit-perspective":{alias:true,inherited:false,supports:1,values:["inherit","initial","none","unset"]},"-webkit-perspective-origin":{alias:true,inherited:false,supports:3,values:["inherit","initial","unset"]},"-webkit-transition":{alias:true,subproperties:["transition-property","transition-duration","transition-timing-function","transition-delay"],inherited:false,supports:320,values:["all","cubic-bezier","ease","ease-in","ease-in-out","ease-out","inherit","initial","linear","none","step-end","step-start","steps","unset"]},"-webkit-transition-delay":{alias:true,inherited:false,supports:64,values:["inherit","initial","unset"]},"-webkit-transition-duration":{alias:true,inherited:false,supports:64,values:["inherit","initial","unset"]},"-webkit-transition-property":{alias:true,inherited:false,supports:0,values:["all","inherit","initial","none","unset"]},"-webkit-transition-timing-function":{alias:true,inherited:false,supports:256,values:["cubic-bezier","ease","ease-in","ease-in-out","ease-out","inherit","initial","linear","step-end","step-start","steps","unset"]},"-webkit-border-radius":{alias:true,subproperties:["border-top-left-radius","border-top-right-radius","border-bottom-right-radius","border-bottom-left-radius"],inherited:false,supports:3,values:["inherit","initial","unset"]},"-webkit-border-top-left-radius":{alias:true,inherited:false,supports:3,values:["inherit","initial","unset"]},"-webkit-border-top-right-radius":{alias:true,inherited:false,supports:3,values:["inherit","initial","unset"]},"-webkit-border-bottom-left-radius":{alias:true,inherited:false,supports:3,values:["inherit","initial","unset"]},"-webkit-border-bottom-right-radius":{alias:true,inherited:false,supports:3,values:["inherit","initial","unset"]},"-webkit-appearance":{alias:true,inherited:false,supports:0,values:["-moz-gtk-info-bar","-moz-mac-disclosure-button-closed","-moz-mac-disclosure-button-open","-moz-mac-fullscreen-button","-moz-mac-help-button","-moz-mac-vibrancy-dark","-moz-mac-vibrancy-light","-moz-win-borderless-glass","-moz-win-browsertabbar-toolbox","-moz-win-communications-toolbox","-moz-win-exclude-glass","-moz-win-glass","-moz-win-media-toolbox","-moz-window-button-box","-moz-window-button-box-maximized","-moz-window-button-close","-moz-window-button-maximize","-moz-window-button-minimize","-moz-window-button-restore","-moz-window-frame-bottom","-moz-window-frame-left","-moz-window-frame-right","-moz-window-titlebar","-moz-window-titlebar-maximized","button","button-arrow-down","button-arrow-next","button-arrow-previous","button-arrow-up","button-bevel","button-focus","caret","checkbox","checkbox-container","checkbox-label","checkmenuitem","dialog","dualbutton","groupbox","inherit","initial","listbox","listitem","menuarrow","menubar","menucheckbox","menuimage","menuitem","menuitemtext","menulist","menulist-button","menulist-text","menulist-textfield","menupopup","menuradio","menuseparator","meterbar","meterchunk","none","number-input","progressbar","progressbar-vertical","progresschunk","progresschunk-vertical","radio","radio-container","radio-label","radiomenuitem","range","range-thumb","resizer","resizerpanel","scale-horizontal","scale-vertical","scalethumb-horizontal","scalethumb-vertical","scalethumbend","scalethumbstart","scalethumbtick","scrollbar","scrollbar-small","scrollbarbutton-down","scrollbarbutton-left","scrollbarbutton-right","scrollbarbutton-up","scrollbarthumb-horizontal","scrollbarthumb-vertical","scrollbartrack-horizontal","scrollbartrack-vertical","searchfield","separator","spinner","spinner-downbutton","spinner-textfield","spinner-upbutton","splitter","statusbar","statusbarpanel","tab","tab-scroll-arrow-back","tab-scroll-arrow-forward","tabpanel","tabpanels","textfield","textfield-multiline","toolbar","toolbarbutton","toolbarbutton-dropdown","toolbargripper","toolbox","tooltip","treeheader","treeheadercell","treeheadersortarrow","treeitem","treeline","treetwisty","treetwistyopen","treeview","unset","window"]},"-webkit-background-clip":{alias:true,inherited:false,supports:0,values:["border-box","content-box","inherit","initial","padding-box","unset"]},"-webkit-background-origin":{alias:true,inherited:false,supports:0,values:["border-box","content-box","inherit","initial","padding-box","unset"]},"-webkit-background-size":{alias:true,inherited:false,supports:3,values:["inherit","initial","unset"]},"-webkit-border-image":{alias:true,subproperties:["border-image-source","border-image-slice","border-image-width","border-image-outset","border-image-repeat"],inherited:false,supports:1675,values:["-moz-element","-moz-image-rect","-moz-linear-gradient","-moz-radial-gradient","-moz-repeating-linear-gradient","-moz-repeating-radial-gradient","inherit","initial","linear-gradient","none","radial-gradient","repeating-linear-gradient","repeating-radial-gradient","unset","url"]},"-webkit-border-image-outset":{alias:true,inherited:false,supports:1025,values:["inherit","initial","unset"]},"-webkit-border-image-repeat":{alias:true,inherited:false,supports:0,values:["inherit","initial","unset"]},"-webkit-border-image-slice":{alias:true,inherited:false,supports:1026,values:["inherit","initial","unset"]},"-webkit-border-image-source":{alias:true,inherited:false,supports:648,values:["-moz-element","-moz-image-rect","-moz-linear-gradient","-moz-radial-gradient","-moz-repeating-linear-gradient","-moz-repeating-radial-gradient","inherit","initial","linear-gradient","none","radial-gradient","repeating-linear-gradient","repeating-radial-gradient","unset","url"]},"-webkit-border-image-width":{alias:true,inherited:false,supports:1027,values:["inherit","initial","unset"]},"-webkit-box-shadow":{alias:true,inherited:false,supports:5,values:["inherit","initial","unset"]},"-webkit-box-sizing":{alias:true,inherited:false,supports:0,values:["border-box","content-box","inherit","initial","padding-box","unset"]},"-webkit-box-flex":{alias:true,inherited:false,supports:1024,values:["inherit","initial","unset"]},"-webkit-box-ordinal-group":{alias:true,inherited:false,supports:1024,values:["inherit","initial","unset"]},"-webkit-box-align":{alias:true,inherited:false,supports:0,values:["inherit","initial","unset"]},"-webkit-box-pack":{alias:true,inherited:false,supports:0,values:["inherit","initial","unset"]},"-webkit-user-select":{alias:true,inherited:false,supports:0,values:["-moz-all","-moz-none","-moz-text","all","auto","element","elements","inherit","initial","none","text","toggle","tri-state","unset"]}};module.exports={cssProperties};
+
+/***/ },
+/* 66 */
+/***/ function(module, exports) {
+
+ /*
+ * A sham for https://dxr.mozilla.org/mozilla-central/source/toolkit/modules/Promise.jsm
+ */
+
+ /**
+ * Promise.jsm is mostly the Promise web API with a `defer` method. Just drop this in here,
+ * and use the native web API (although building with webpack/babel, it may replace this
+ * with it's own version if we want to target environments that do not have `Promise`.
+ */
+
+ var p = typeof window != "undefined" ? window.Promise : Promise;
+ p.defer = function defer() {
+ var resolve, reject;
+ var promise = new Promise(function () {
+ resolve = arguments[0];
+ reject = arguments[1];
+ });
+ return {
+ resolve: resolve,
+ reject: reject,
+ promise: promise
+ };
+ };
+
+ module.exports = p;
+
+/***/ },
+/* 67 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /* eslint-env browser */
+ /* -*- 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/. */
+
+ // TODO: Get rid of this code once the marionette server loads transport.js as
+ // an SDK module (see bug 1000814)
+
+ "use strict";
+
+ var DevToolsUtils = __webpack_require__(68);
+ var dumpn = DevToolsUtils.dumpn;
+ var dumpv = DevToolsUtils.dumpv;
+
+ var StreamUtils = __webpack_require__(74);
+
+ var _require = __webpack_require__(75);
+
+ var Packet = _require.Packet;
+ var JSONPacket = _require.JSONPacket;
+ var BulkPacket = _require.BulkPacket;
+
+ var promise = __webpack_require__(66);
+ var EventEmitter = __webpack_require__(60);
+ var utf8 = __webpack_require__(78);
+
+ var PACKET_HEADER_MAX = 200;
+
+ /**
+ * An adapter that handles data transfers between the debugger client and
+ * server. It can work with both nsIPipe and nsIServerSocket transports so
+ * long as the properly created input and output streams are specified.
+ * (However, for intra-process connections, LocalDebuggerTransport, below,
+ * is more efficient than using an nsIPipe pair with DebuggerTransport.)
+ *
+ * @param input nsIAsyncInputStream
+ * The input stream.
+ * @param output nsIAsyncOutputStream
+ * The output stream.
+ *
+ * Given a DebuggerTransport instance dt:
+ * 1) Set dt.hooks to a packet handler object (described below).
+ * 2) Call dt.ready() to begin watching for input packets.
+ * 3) Call dt.send() / dt.startBulkSend() to send packets.
+ * 4) Call dt.close() to close the connection, and disengage from the event
+ * loop.
+ *
+ * A packet handler is an object with the following methods:
+ *
+ * - onPacket(packet) - called when we have received a complete packet.
+ * |packet| is the parsed form of the packet --- a JavaScript value, not
+ * a JSON-syntax string.
+ *
+ * - onBulkPacket(packet) - called when we have switched to bulk packet
+ * receiving mode. |packet| is an object containing:
+ * * actor: Name of actor that will receive the packet
+ * * type: Name of actor's method that should be called on receipt
+ * * length: Size of the data to be read
+ * * stream: This input stream should only be used directly if you can ensure
+ * that you will read exactly |length| bytes and will not close the
+ * stream when reading is complete
+ * * done: If you use the stream directly (instead of |copyTo| below), you
+ * must signal completion by resolving / rejecting this deferred.
+ * If it's rejected, the transport will be closed. If an Error is
+ * supplied as a rejection value, it will be logged via |dumpn|.
+ * If you do use |copyTo|, resolving is taken care of for you when
+ * copying completes.
+ * * copyTo: A helper function for getting your data out of the stream that
+ * meets the stream handling requirements above, and has the
+ * following signature:
+ * @param output nsIAsyncOutputStream
+ * The stream to copy to.
+ * @return Promise
+ * The promise is resolved when copying completes or rejected if any
+ * (unexpected) errors occur.
+ * This object also emits "progress" events for each chunk that is
+ * copied. See stream-utils.js.
+ *
+ * - onClosed(reason) - called when the connection is closed. |reason| is
+ * an optional nsresult or object, typically passed when the transport is
+ * closed due to some error in a underlying stream.
+ *
+ * See ./packets.js and the Remote Debugging Protocol specification for more
+ * details on the format of these packets.
+ */
+ function DebuggerTransport(socket) {
+ EventEmitter.decorate(this);
+
+ this._socket = socket;
+
+ // The current incoming (possibly partial) header, which will determine which
+ // type of Packet |_incoming| below will become.
+ this._incomingHeader = "";
+ // The current incoming Packet object
+ this._incoming = null;
+ // A queue of outgoing Packet objects
+ this._outgoing = [];
+
+ this.hooks = null;
+ this.active = false;
+
+ this._incomingEnabled = true;
+ this._outgoingEnabled = true;
+
+ this.close = this.close.bind(this);
+ }
+
+ DebuggerTransport.prototype = {
+ /**
+ * Transmit an object as a JSON packet.
+ *
+ * This method returns immediately, without waiting for the entire
+ * packet to be transmitted, registering event handlers as needed to
+ * transmit the entire packet. Packets are transmitted in the order
+ * they are passed to this method.
+ */
+ send: function (object) {
+ this.emit("send", object);
+
+ var packet = new JSONPacket(this);
+ packet.object = object;
+ this._outgoing.push(packet);
+ this._flushOutgoing();
+ },
+
+ /**
+ * Transmit streaming data via a bulk packet.
+ *
+ * This method initiates the bulk send process by queuing up the header data.
+ * The caller receives eventual access to a stream for writing.
+ *
+ * N.B.: Do *not* attempt to close the stream handed to you, as it will
+ * continue to be used by this transport afterwards. Most users should
+ * instead use the provided |copyFrom| function instead.
+ *
+ * @param header Object
+ * This is modeled after the format of JSON packets above, but does not
+ * actually contain the data, but is instead just a routing header:
+ * * actor: Name of actor that will receive the packet
+ * * type: Name of actor's method that should be called on receipt
+ * * length: Size of the data to be sent
+ * @return Promise
+ * The promise will be resolved when you are allowed to write to the
+ * stream with an object containing:
+ * * stream: This output stream should only be used directly if
+ * you can ensure that you will write exactly |length|
+ * bytes and will not close the stream when writing is
+ * complete
+ * * done: If you use the stream directly (instead of |copyFrom|
+ * below), you must signal completion by resolving /
+ * rejecting this deferred. If it's rejected, the
+ * transport will be closed. If an Error is supplied as
+ * a rejection value, it will be logged via |dumpn|. If
+ * you do use |copyFrom|, resolving is taken care of for
+ * you when copying completes.
+ * * copyFrom: A helper function for getting your data onto the
+ * stream that meets the stream handling requirements
+ * above, and has the following signature:
+ * @param input nsIAsyncInputStream
+ * The stream to copy from.
+ * @return Promise
+ * The promise is resolved when copying completes or
+ * rejected if any (unexpected) errors occur.
+ * This object also emits "progress" events for each chunk
+ * that is copied. See stream-utils.js.
+ */
+ startBulkSend: function (header) {
+ this.emit("startBulkSend", header);
+
+ var packet = new BulkPacket(this);
+ packet.header = header;
+ this._outgoing.push(packet);
+ this._flushOutgoing();
+ return packet.streamReadyForWriting;
+ },
+
+ /**
+ * Close the transport.
+ * @param reason nsresult / object (optional)
+ * The status code or error message that corresponds to the reason for
+ * closing the transport (likely because a stream closed or failed).
+ */
+ close: function (reason) {
+ this.emit("onClosed", reason);
+
+ this.active = false;
+ this._socket.close();
+ this._destroyIncoming();
+ this._destroyAllOutgoing();
+ if (this.hooks) {
+ this.hooks.onClosed(reason);
+ this.hooks = null;
+ }
+ if (reason) {
+ dumpn("Transport closed: " + DevToolsUtils.safeErrorString(reason));
+ } else {
+ dumpn("Transport closed.");
+ }
+ },
+
+ /**
+ * The currently outgoing packet (at the top of the queue).
+ */
+ get _currentOutgoing() {
+ return this._outgoing[0];
+ },
+
+ /**
+ * Flush data to the outgoing stream. Waits until the output stream notifies
+ * us that it is ready to be written to (via onOutputStreamReady).
+ */
+ _flushOutgoing: function () {
+ if (!this._outgoingEnabled || this._outgoing.length === 0) {
+ return;
+ }
+
+ // If the top of the packet queue has nothing more to send, remove it.
+ if (this._currentOutgoing.done) {
+ this._finishCurrentOutgoing();
+ }
+
+ if (this._outgoing.length > 0) {
+ setTimeout(this.onOutputStreamReady.bind(this), 0);
+ }
+ },
+
+ /**
+ * Pause this transport's attempts to write to the output stream. This is
+ * used when we've temporarily handed off our output stream for writing bulk
+ * data.
+ */
+ pauseOutgoing: function () {
+ this._outgoingEnabled = false;
+ },
+
+ /**
+ * Resume this transport's attempts to write to the output stream.
+ */
+ resumeOutgoing: function () {
+ this._outgoingEnabled = true;
+ this._flushOutgoing();
+ },
+
+ // nsIOutputStreamCallback
+ /**
+ * This is called when the output stream is ready for more data to be written.
+ * The current outgoing packet will attempt to write some amount of data, but
+ * may not complete.
+ */
+ onOutputStreamReady: DevToolsUtils.makeInfallible(function () {
+ if (!this._outgoingEnabled || this._outgoing.length === 0) {
+ return;
+ }
+
+ try {
+ this._currentOutgoing.write({
+ write: data => {
+ var count = data.length;
+ this._socket.send(data);
+ return count;
+ }
+ });
+ } catch (e) {
+ if (e.result != Cr.NS_BASE_STREAM_WOULD_BLOCK) {
+ this.close(e.result);
+ return;
+ } else {
+ throw e;
+ }
+ }
+
+ this._flushOutgoing();
+ }, "DebuggerTransport.prototype.onOutputStreamReady"),
+
+ /**
+ * Remove the current outgoing packet from the queue upon completion.
+ */
+ _finishCurrentOutgoing: function () {
+ if (this._currentOutgoing) {
+ this._currentOutgoing.destroy();
+ this._outgoing.shift();
+ }
+ },
+
+ /**
+ * Clear the entire outgoing queue.
+ */
+ _destroyAllOutgoing: function () {
+ for (var packet of this._outgoing) {
+ packet.destroy();
+ }
+ this._outgoing = [];
+ },
+
+ /**
+ * Initialize the input stream for reading. Once this method has been called,
+ * we watch for packets on the input stream, and pass them to the appropriate
+ * handlers via this.hooks.
+ */
+ ready: function () {
+ this.active = true;
+ this._waitForIncoming();
+ },
+
+ /**
+ * Asks the input stream to notify us (via onInputStreamReady) when it is
+ * ready for reading.
+ */
+ _waitForIncoming: function () {
+ if (this._incomingEnabled && !this._socket.onmessage) {
+ this._socket.onmessage = this.onInputStreamReady.bind(this);
+ }
+ },
+
+ /**
+ * Pause this transport's attempts to read from the input stream. This is
+ * used when we've temporarily handed off our input stream for reading bulk
+ * data.
+ */
+ pauseIncoming: function () {
+ this._incomingEnabled = false;
+ },
+
+ /**
+ * Resume this transport's attempts to read from the input stream.
+ */
+ resumeIncoming: function () {
+ this._incomingEnabled = true;
+ this._flushIncoming();
+ this._waitForIncoming();
+ },
+
+ // nsIInputStreamCallback
+ /**
+ * Called when the stream is either readable or closed.
+ */
+ onInputStreamReady: DevToolsUtils.makeInfallible(function (event) {
+ var data = event.data;
+ // TODO: ws-tcp-proxy decodes utf-8, but the transport expects to see the
+ // encoded bytes. Simplest step is to re-encode for now.
+ data = utf8.encode(data);
+ var stream = {
+ available() {
+ return data.length;
+ },
+ readBytes(count) {
+ var result = data.slice(0, count);
+ data = data.slice(count);
+ return result;
+ }
+ };
+
+ try {
+ while (data && this._incomingEnabled && this._processIncoming(stream, stream.available())) {}
+ this._waitForIncoming();
+ } catch (e) {
+ if (e.result != Cr.NS_BASE_STREAM_WOULD_BLOCK) {
+ this.close(e.result);
+ } else {
+ throw e;
+ }
+ }
+ }, "DebuggerTransport.prototype.onInputStreamReady"),
+
+ /**
+ * Process the incoming data. Will create a new currently incoming Packet if
+ * needed. Tells the incoming Packet to read as much data as it can, but
+ * reading may not complete. The Packet signals that its data is ready for
+ * delivery by calling one of this transport's _on*Ready methods (see
+ * ./packets.js and the _on*Ready methods below).
+ * @return boolean
+ * Whether incoming stream processing should continue for any
+ * remaining data.
+ */
+ _processIncoming: function (stream, count) {
+ dumpv("Data available: " + count);
+
+ if (!count) {
+ dumpv("Nothing to read, skipping");
+ return false;
+ }
+
+ try {
+ if (!this._incoming) {
+ dumpv("Creating a new packet from incoming");
+
+ if (!this._readHeader(stream)) {
+ return false; // Not enough data to read packet type
+ }
+
+ // Attempt to create a new Packet by trying to parse each possible
+ // header pattern.
+ this._incoming = Packet.fromHeader(this._incomingHeader, this);
+ if (!this._incoming) {
+ throw new Error("No packet types for header: " + this._incomingHeader);
+ }
+ }
+
+ if (!this._incoming.done) {
+ // We have an incomplete packet, keep reading it.
+ dumpv("Existing packet incomplete, keep reading");
+ this._incoming.read(stream);
+ }
+ } catch (e) {
+ var msg = "Error reading incoming packet: (" + e + " - " + e.stack + ")";
+ dumpn(msg);
+
+ // Now in an invalid state, shut down the transport.
+ this.close();
+ return false;
+ }
+
+ if (!this._incoming.done) {
+ // Still not complete, we'll wait for more data.
+ dumpv("Packet not done, wait for more");
+ return true;
+ }
+
+ // Ready for next packet
+ this._flushIncoming();
+ return true;
+ },
+
+ /**
+ * Read as far as we can into the incoming data, attempting to build up a
+ * complete packet header (which terminates with ":"). We'll only read up to
+ * PACKET_HEADER_MAX characters.
+ * @return boolean
+ * True if we now have a complete header.
+ */
+ _readHeader: function (stream) {
+ var amountToRead = PACKET_HEADER_MAX - this._incomingHeader.length;
+ this._incomingHeader += StreamUtils.delimitedRead(stream, ":", amountToRead);
+ if (dumpv.wantVerbose) {
+ dumpv("Header read: " + this._incomingHeader);
+ }
+
+ if (this._incomingHeader.endsWith(":")) {
+ if (dumpv.wantVerbose) {
+ dumpv("Found packet header successfully: " + this._incomingHeader);
+ }
+ return true;
+ }
+
+ if (this._incomingHeader.length >= PACKET_HEADER_MAX) {
+ throw new Error("Failed to parse packet header!");
+ }
+
+ // Not enough data yet.
+ return false;
+ },
+
+ /**
+ * If the incoming packet is done, log it as needed and clear the buffer.
+ */
+ _flushIncoming: function () {
+ if (!this._incoming.done) {
+ return;
+ }
+ if (dumpn.wantLogging) {
+ dumpn("Got: " + this._incoming);
+ }
+ this._destroyIncoming();
+ },
+
+ /**
+ * Handler triggered by an incoming JSONPacket completing it's |read| method.
+ * Delivers the packet to this.hooks.onPacket.
+ */
+ _onJSONObjectReady: function (object) {
+ DevToolsUtils.executeSoon(DevToolsUtils.makeInfallible(() => {
+ // Ensure the transport is still alive by the time this runs.
+ if (this.active) {
+ this.emit("onPacket", object);
+ this.hooks.onPacket(object);
+ }
+ }, "DebuggerTransport instance's this.hooks.onPacket"));
+ },
+
+ /**
+ * Handler triggered by an incoming BulkPacket entering the |read| phase for
+ * the stream portion of the packet. Delivers info about the incoming
+ * streaming data to this.hooks.onBulkPacket. See the main comment on the
+ * transport at the top of this file for more details.
+ */
+ _onBulkReadReady: function () {
+ for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
+ args[_key] = arguments[_key];
+ }
+
+ DevToolsUtils.executeSoon(DevToolsUtils.makeInfallible(() => {
+ // Ensure the transport is still alive by the time this runs.
+ if (this.active) {
+ var _hooks;
+
+ this.emit.apply(this, ["onBulkPacket"].concat(args));
+ (_hooks = this.hooks).onBulkPacket.apply(_hooks, args);
+ }
+ }, "DebuggerTransport instance's this.hooks.onBulkPacket"));
+ },
+
+ /**
+ * Remove all handlers and references related to the current incoming packet,
+ * either because it is now complete or because the transport is closing.
+ */
+ _destroyIncoming: function () {
+ if (this._incoming) {
+ this._incoming.destroy();
+ }
+ this._incomingHeader = "";
+ this._incoming = null;
+ }
+
+ };
+
+ exports.DebuggerTransport = DebuggerTransport;
+
+ /**
+ * An adapter that handles data transfers between the debugger client and
+ * server when they both run in the same process. It presents the same API as
+ * DebuggerTransport, but instead of transmitting serialized messages across a
+ * connection it merely calls the packet dispatcher of the other side.
+ *
+ * @param other LocalDebuggerTransport
+ * The other endpoint for this debugger connection.
+ *
+ * @see DebuggerTransport
+ */
+ function LocalDebuggerTransport(other) {
+ EventEmitter.decorate(this);
+
+ this.other = other;
+ this.hooks = null;
+
+ /*
+ * A packet number, shared between this and this.other. This isn't used
+ * by the protocol at all, but it makes the packet traces a lot easier to
+ * follow.
+ */
+ this._serial = this.other ? this.other._serial : { count: 0 };
+ this.close = this.close.bind(this);
+ }
+
+ LocalDebuggerTransport.prototype = {
+ /**
+ * Transmit a message by directly calling the onPacket handler of the other
+ * endpoint.
+ */
+ send: function (packet) {
+ this.emit("send", packet);
+
+ var serial = this._serial.count++;
+ if (dumpn.wantLogging) {
+ /* Check 'from' first, as 'echo' packets have both. */
+ if (packet.from) {
+ dumpn("Packet " + serial + " sent from " + uneval(packet.from));
+ } else if (packet.to) {
+ dumpn("Packet " + serial + " sent to " + uneval(packet.to));
+ }
+ }
+ this._deepFreeze(packet);
+ var other = this.other;
+ if (other) {
+ DevToolsUtils.executeSoon(DevToolsUtils.makeInfallible(() => {
+ // Avoid the cost of JSON.stringify() when logging is disabled.
+ if (dumpn.wantLogging) {
+ dumpn("Received packet " + serial + ": " + JSON.stringify(packet, null, 2));
+ }
+ if (other.hooks) {
+ other.emit("onPacket", packet);
+ other.hooks.onPacket(packet);
+ }
+ }, "LocalDebuggerTransport instance's this.other.hooks.onPacket"));
+ }
+ },
+
+ /**
+ * Send a streaming bulk packet directly to the onBulkPacket handler of the
+ * other endpoint.
+ *
+ * This case is much simpler than the full DebuggerTransport, since there is
+ * no primary stream we have to worry about managing while we hand it off to
+ * others temporarily. Instead, we can just make a single use pipe and be
+ * done with it.
+ */
+ startBulkSend: function (_ref) {
+ var actor = _ref.actor;
+ var type = _ref.type;
+ var length = _ref.length;
+
+ this.emit("startBulkSend", { actor, type, length });
+
+ var serial = this._serial.count++;
+
+ dumpn("Sent bulk packet " + serial + " for actor " + actor);
+ if (!this.other) {
+ return;
+ }
+
+ var pipe = new Pipe(true, true, 0, 0, null);
+
+ DevToolsUtils.executeSoon(DevToolsUtils.makeInfallible(() => {
+ dumpn("Received bulk packet " + serial);
+ if (!this.other.hooks) {
+ return;
+ }
+
+ // Receiver
+ var deferred = promise.defer();
+ var packet = {
+ actor: actor,
+ type: type,
+ length: length,
+ copyTo: output => {
+ var copying = StreamUtils.copyStream(pipe.inputStream, output, length);
+ deferred.resolve(copying);
+ return copying;
+ },
+ stream: pipe.inputStream,
+ done: deferred
+ };
+
+ this.other.emit("onBulkPacket", packet);
+ this.other.hooks.onBulkPacket(packet);
+
+ // Await the result of reading from the stream
+ deferred.promise.then(() => pipe.inputStream.close(), this.close);
+ }, "LocalDebuggerTransport instance's this.other.hooks.onBulkPacket"));
+
+ // Sender
+ var sendDeferred = promise.defer();
+
+ // The remote transport is not capable of resolving immediately here, so we
+ // shouldn't be able to either.
+ DevToolsUtils.executeSoon(() => {
+ var copyDeferred = promise.defer();
+
+ sendDeferred.resolve({
+ copyFrom: input => {
+ var copying = StreamUtils.copyStream(input, pipe.outputStream, length);
+ copyDeferred.resolve(copying);
+ return copying;
+ },
+ stream: pipe.outputStream,
+ done: copyDeferred
+ });
+
+ // Await the result of writing to the stream
+ copyDeferred.promise.then(() => pipe.outputStream.close(), this.close);
+ });
+
+ return sendDeferred.promise;
+ },
+
+ /**
+ * Close the transport.
+ */
+ close: function () {
+ this.emit("close");
+
+ if (this.other) {
+ // Remove the reference to the other endpoint before calling close(), to
+ // avoid infinite recursion.
+ var other = this.other;
+ this.other = null;
+ other.close();
+ }
+ if (this.hooks) {
+ try {
+ this.hooks.onClosed();
+ } catch (ex) {
+ console.error(ex);
+ }
+ this.hooks = null;
+ }
+ },
+
+ /**
+ * An empty method for emulating the DebuggerTransport API.
+ */
+ ready: function () {},
+
+ /**
+ * Helper function that makes an object fully immutable.
+ */
+ _deepFreeze: function (object) {
+ Object.freeze(object);
+ for (var prop in object) {
+ // Freeze the properties that are objects, not on the prototype, and not
+ // already frozen. Note that this might leave an unfrozen reference
+ // somewhere in the object if there is an already frozen object containing
+ // an unfrozen object.
+ if (object.hasOwnProperty(prop) && typeof object === "object" && !Object.isFrozen(object)) {
+ this._deepFreeze(o[prop]);
+ }
+ }
+ }
+ };
+
+ exports.LocalDebuggerTransport = LocalDebuggerTransport;
+
+ /**
+ * A transport for the debugging protocol that uses nsIMessageSenders to
+ * exchange packets with servers running in child processes.
+ *
+ * In the parent process, |sender| should be the nsIMessageSender for the
+ * child process. In a child process, |sender| should be the child process
+ * message manager, which sends packets to the parent.
+ *
+ * |prefix| is a string included in the message names, to distinguish
+ * multiple servers running in the same child process.
+ *
+ * This transport exchanges messages named 'debug:<prefix>:packet', where
+ * <prefix> is |prefix|, whose data is the protocol packet.
+ */
+ function ChildDebuggerTransport(sender, prefix) {
+ EventEmitter.decorate(this);
+
+ this._sender = sender.QueryInterface(Ci.nsIMessageSender);
+ this._messageName = "debug:" + prefix + ":packet";
+ }
+
+ /*
+ * To avoid confusion, we use 'message' to mean something that
+ * nsIMessageSender conveys, and 'packet' to mean a remote debugging
+ * protocol packet.
+ */
+ ChildDebuggerTransport.prototype = {
+ constructor: ChildDebuggerTransport,
+
+ hooks: null,
+
+ ready: function () {
+ this._sender.addMessageListener(this._messageName, this);
+ },
+
+ close: function () {
+ this._sender.removeMessageListener(this._messageName, this);
+ this.emit("onClosed");
+ this.hooks.onClosed();
+ },
+
+ receiveMessage: function (_ref2) {
+ var data = _ref2.data;
+
+ this.emit("onPacket", data);
+ this.hooks.onPacket(data);
+ },
+
+ send: function (packet) {
+ this.emit("send", packet);
+ this._sender.sendAsyncMessage(this._messageName, packet);
+ },
+
+ startBulkSend: function () {
+ throw new Error("Can't send bulk data to child processes.");
+ }
+ };
+
+ exports.ChildDebuggerTransport = ChildDebuggerTransport;
+
+ // WorkerDebuggerTransport is defined differently depending on whether we are
+ // on the main thread or a worker thread. In the former case, we are required
+ // by the devtools loader, and isWorker will be false. Otherwise, we are
+ // required by the worker loader, and isWorker will be true.
+ //
+ // Each worker debugger supports only a single connection to the main thread.
+ // However, its theoretically possible for multiple servers to connect to the
+ // same worker. Consequently, each transport has a connection id, to allow
+ // messages from multiple connections to be multiplexed on a single channel.
+
+ if (typeof WorkerGlobalScope === 'undefined') {
+ // i.e. not in a worker
+ (function () {
+ // Main thread
+ /**
+ * A transport that uses a WorkerDebugger to send packets from the main
+ * thread to a worker thread.
+ */
+ function WorkerDebuggerTransport(dbg, id) {
+ this._dbg = dbg;
+ this._id = id;
+ this.onMessage = this._onMessage.bind(this);
+ }
+
+ WorkerDebuggerTransport.prototype = {
+ constructor: WorkerDebuggerTransport,
+
+ ready: function () {
+ this._dbg.addListener(this);
+ },
+
+ close: function () {
+ this._dbg.removeListener(this);
+ if (this.hooks) {
+ this.hooks.onClosed();
+ }
+ },
+
+ send: function (packet) {
+ this._dbg.postMessage(JSON.stringify({
+ type: "message",
+ id: this._id,
+ message: packet
+ }));
+ },
+
+ startBulkSend: function () {
+ throw new Error("Can't send bulk data from worker threads!");
+ },
+
+ _onMessage: function (message) {
+ var packet = JSON.parse(message);
+ if (packet.type !== "message" || packet.id !== this._id) {
+ return;
+ }
+
+ if (this.hooks) {
+ this.hooks.onPacket(packet.message);
+ }
+ }
+ };
+
+ exports.WorkerDebuggerTransport = WorkerDebuggerTransport;
+ }).call(this);
+ } else {
+ (function () {
+ // Worker thread
+ /*
+ * A transport that uses a WorkerDebuggerGlobalScope to send packets from a
+ * worker thread to the main thread.
+ */
+ function WorkerDebuggerTransport(scope, id) {
+ this._scope = scope;
+ this._id = id;
+ this._onMessage = this._onMessage.bind(this);
+ }
+
+ WorkerDebuggerTransport.prototype = {
+ constructor: WorkerDebuggerTransport,
+
+ ready: function () {
+ this._scope.addEventListener("message", this._onMessage);
+ },
+
+ close: function () {
+ this._scope.removeEventListener("message", this._onMessage);
+ if (this.hooks) {
+ this.hooks.onClosed();
+ }
+ },
+
+ send: function (packet) {
+ this._scope.postMessage(JSON.stringify({
+ type: "message",
+ id: this._id,
+ message: packet
+ }));
+ },
+
+ startBulkSend: function () {
+ throw new Error("Can't send bulk data from worker threads!");
+ },
+
+ _onMessage: function (event) {
+ var packet = JSON.parse(event.data);
+ if (packet.type !== "message" || packet.id !== this._id) {
+ return;
+ }
+
+ if (this.hooks) {
+ this.hooks.onPacket(packet.message);
+ }
+ }
+ };
+
+ exports.WorkerDebuggerTransport = WorkerDebuggerTransport;
+ }).call(this);
+ }
+
+/***/ },
+/* 68 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var _this = this;
+
+ function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
+
+ /* 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/. */
+
+ /* General utilities used throughout devtools. */
+ var _require = __webpack_require__(61);
+
+ var Ci = _require.Ci;
+ var Cu = _require.Cu;
+ var Cc = _require.Cc;
+ var components = _require.components;
+
+ var _require2 = __webpack_require__(30);
+
+ var Services = _require2.Services;
+
+ var promise = __webpack_require__(66);
+
+ var _require3 = __webpack_require__(69);
+
+ var FileUtils = _require3.FileUtils;
+
+ /**
+ * Turn the error |aError| into a string, without fail.
+ */
+
+ exports.safeErrorString = function safeErrorString(aError) {
+ try {
+ var errorString = aError.toString();
+ if (typeof errorString == "string") {
+ // Attempt to attach a stack to |errorString|. If it throws an error, or
+ // isn't a string, don't use it.
+ try {
+ if (aError.stack) {
+ var stack = aError.stack.toString();
+ if (typeof stack == "string") {
+ errorString += "\nStack: " + stack;
+ }
+ }
+ } catch (ee) {}
+
+ // Append additional line and column number information to the output,
+ // since it might not be part of the stringified error.
+ if (typeof aError.lineNumber == "number" && typeof aError.columnNumber == "number") {
+ errorString += "Line: " + aError.lineNumber + ", column: " + aError.columnNumber;
+ }
+
+ return errorString;
+ }
+ } catch (ee) {}
+
+ // We failed to find a good error description, so do the next best thing.
+ return Object.prototype.toString.call(aError);
+ };
+
+ /**
+ * Report that |aWho| threw an exception, |aException|.
+ */
+ exports.reportException = function reportException(aWho, aException) {
+ var msg = aWho + " threw an exception: " + exports.safeErrorString(aException);
+
+ console.log(msg);
+
+ // if (Cu && console.error) {
+ // /*
+ // * Note that the xpcshell test harness registers an observer for
+ // * console messages, so when we're running tests, this will cause
+ // * the test to quit.
+ // */
+ // console.error(msg);
+ // }
+ };
+
+ /**
+ * Given a handler function that may throw, return an infallible handler
+ * function that calls the fallible handler, and logs any exceptions it
+ * throws.
+ *
+ * @param aHandler function
+ * A handler function, which may throw.
+ * @param aName string
+ * A name for aHandler, for use in error messages. If omitted, we use
+ * aHandler.name.
+ *
+ * (SpiderMonkey does generate good names for anonymous functions, but we
+ * don't have a way to get at them from JavaScript at the moment.)
+ */
+ exports.makeInfallible = function makeInfallible(aHandler, aName) {
+ if (!aName) aName = aHandler.name;
+
+ return function () /* arguments */{
+ // try {
+ return aHandler.apply(this, arguments);
+ // } catch (ex) {
+ // let who = "Handler function";
+ // if (aName) {
+ // who += " " + aName;
+ // }
+ // return exports.reportException(who, ex);
+ // }
+ };
+ };
+
+ /**
+ * Waits for the next tick in the event loop to execute a callback.
+ */
+ exports.executeSoon = function executeSoon(aFn) {
+ setTimeout(aFn, 0);
+ };
+
+ /**
+ * Waits for the next tick in the event loop.
+ *
+ * @return Promise
+ * A promise that is resolved after the next tick in the event loop.
+ */
+ exports.waitForTick = function waitForTick() {
+ var deferred = promise.defer();
+ exports.executeSoon(deferred.resolve);
+ return deferred.promise;
+ };
+
+ /**
+ * Waits for the specified amount of time to pass.
+ *
+ * @param number aDelay
+ * The amount of time to wait, in milliseconds.
+ * @return Promise
+ * A promise that is resolved after the specified amount of time passes.
+ */
+ exports.waitForTime = function waitForTime(aDelay) {
+ var deferred = promise.defer();
+ setTimeout(deferred.resolve, aDelay);
+ return deferred.promise;
+ };
+
+ /**
+ * Like Array.prototype.forEach, but doesn't cause jankiness when iterating over
+ * very large arrays by yielding to the browser and continuing execution on the
+ * next tick.
+ *
+ * @param Array aArray
+ * The array being iterated over.
+ * @param Function aFn
+ * The function called on each item in the array. If a promise is
+ * returned by this function, iterating over the array will be paused
+ * until the respective promise is resolved.
+ * @returns Promise
+ * A promise that is resolved once the whole array has been iterated
+ * over, and all promises returned by the aFn callback are resolved.
+ */
+ exports.yieldingEach = function yieldingEach(aArray, aFn) {
+ var deferred = promise.defer();
+
+ var i = 0;
+ var len = aArray.length;
+ var outstanding = [deferred.promise];
+
+ (function loop() {
+ var start = Date.now();
+
+ while (i < len) {
+ // Don't block the main thread for longer than 16 ms at a time. To
+ // maintain 60fps, you have to render every frame in at least 16ms; we
+ // aren't including time spent in non-JS here, but this is Good
+ // Enough(tm).
+ if (Date.now() - start > 16) {
+ exports.executeSoon(loop);
+ return;
+ }
+
+ try {
+ outstanding.push(aFn(aArray[i], i++));
+ } catch (e) {
+ deferred.reject(e);
+ return;
+ }
+ }
+
+ deferred.resolve();
+ })();
+
+ return promise.all(outstanding);
+ };
+
+ /**
+ * Like XPCOMUtils.defineLazyGetter, but with a |this| sensitive getter that
+ * allows the lazy getter to be defined on a prototype and work correctly with
+ * instances.
+ *
+ * @param Object aObject
+ * The prototype object to define the lazy getter on.
+ * @param String aKey
+ * The key to define the lazy getter on.
+ * @param Function aCallback
+ * The callback that will be called to determine the value. Will be
+ * called with the |this| value of the current instance.
+ */
+ exports.defineLazyPrototypeGetter = function defineLazyPrototypeGetter(aObject, aKey, aCallback) {
+ Object.defineProperty(aObject, aKey, {
+ configurable: true,
+ get: function () {
+ var value = aCallback.call(this);
+
+ Object.defineProperty(this, aKey, {
+ configurable: true,
+ writable: true,
+ value: value
+ });
+
+ return value;
+ }
+ });
+ };
+
+ /**
+ * Safely get the property value from a Debugger.Object for a given key. Walks
+ * the prototype chain until the property is found.
+ *
+ * @param Debugger.Object aObject
+ * The Debugger.Object to get the value from.
+ * @param String aKey
+ * The key to look for.
+ * @return Any
+ */
+ exports.getProperty = function getProperty(aObj, aKey) {
+ var root = aObj;
+ try {
+ do {
+ var desc = aObj.getOwnPropertyDescriptor(aKey);
+ if (desc) {
+ if ("value" in desc) {
+ return desc.value;
+ }
+ // Call the getter if it's safe.
+ return exports.hasSafeGetter(desc) ? desc.get.call(root).return : undefined;
+ }
+ aObj = aObj.proto;
+ } while (aObj);
+ } catch (e) {
+ // If anything goes wrong report the error and return undefined.
+ exports.reportException("getProperty", e);
+ }
+ return undefined;
+ };
+
+ /**
+ * Determines if a descriptor has a getter which doesn't call into JavaScript.
+ *
+ * @param Object aDesc
+ * The descriptor to check for a safe getter.
+ * @return Boolean
+ * Whether a safe getter was found.
+ */
+ exports.hasSafeGetter = function hasSafeGetter(aDesc) {
+ // Scripted functions that are CCWs will not appear scripted until after
+ // unwrapping.
+ try {
+ var fn = aDesc.get.unwrap();
+ return fn && fn.callable && fn.class == "Function" && fn.script === undefined;
+ } catch (e) {
+ // Avoid exception 'Object in compartment marked as invisible to Debugger'
+ return false;
+ }
+ };
+
+ /**
+ * Check if it is safe to read properties and execute methods from the given JS
+ * object. Safety is defined as being protected from unintended code execution
+ * from content scripts (or cross-compartment code).
+ *
+ * See bugs 945920 and 946752 for discussion.
+ *
+ * @type Object aObj
+ * The object to check.
+ * @return Boolean
+ * True if it is safe to read properties from aObj, or false otherwise.
+ */
+ exports.isSafeJSObject = function isSafeJSObject(aObj) {
+ // If we are running on a worker thread, Cu is not available. In this case,
+ // we always return false, just to be on the safe side.
+ if (isWorker) {
+ return false;
+ }
+
+ if (Cu.getGlobalForObject(aObj) == Cu.getGlobalForObject(exports.isSafeJSObject)) {
+ return true; // aObj is not a cross-compartment wrapper.
+ }
+
+ var principal = Cu.getObjectPrincipal(aObj);
+ if (Services.scriptSecurityManager.isSystemPrincipal(principal)) {
+ return true; // allow chrome objects
+ }
+
+ return Cu.isXrayWrapper(aObj);
+ };
+
+ exports.dumpn = function dumpn(str) {
+ if (exports.dumpn.wantLogging) {
+ console.log("DBG-SERVER: " + str + "\n");
+ }
+ };
+
+ // We want wantLogging to be writable. The exports object is frozen by the
+ // loader, so define it on dumpn instead.
+ exports.dumpn.wantLogging = false;
+
+ /**
+ * A verbose logger for low-level tracing.
+ */
+ exports.dumpv = function (msg) {
+ if (exports.dumpv.wantVerbose) {
+ exports.dumpn(msg);
+ }
+ };
+
+ // We want wantLogging to be writable. The exports object is frozen by the
+ // loader, so define it on dumpn instead.
+ exports.dumpv.wantVerbose = false;
+
+ /**
+ * Utility function for updating an object with the properties of
+ * other objects.
+ *
+ * @param aTarget Object
+ * The object being updated.
+ * @param aNewAttrs Object
+ * The rest params are objects to update aTarget with. You
+ * can pass as many as you like.
+ */
+ exports.update = function update(aTarget) {
+ for (var _len = arguments.length, aArgs = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
+ aArgs[_key - 1] = arguments[_key];
+ }
+
+ for (var attrs of aArgs) {
+ for (var key in attrs) {
+ var desc = Object.getOwnPropertyDescriptor(attrs, key);
+
+ if (desc) {
+ Object.defineProperty(aTarget, key, desc);
+ }
+ }
+ }
+
+ return aTarget;
+ };
+
+ /**
+ * Utility function for getting the values from an object as an array
+ *
+ * @param aObject Object
+ * The object to iterate over
+ */
+ exports.values = function values(aObject) {
+ return Object.keys(aObject).map(k => aObject[k]);
+ };
+
+ /**
+ * Defines a getter on a specified object that will be created upon first use.
+ *
+ * @param aObject
+ * The object to define the lazy getter on.
+ * @param aName
+ * The name of the getter to define on aObject.
+ * @param aLambda
+ * A function that returns what the getter should return. This will
+ * only ever be called once.
+ */
+ exports.defineLazyGetter = function defineLazyGetter(aObject, aName, aLambda) {
+ Object.defineProperty(aObject, aName, {
+ get: function () {
+ delete aObject[aName];
+ return aObject[aName] = aLambda.apply(aObject);
+ },
+ configurable: true,
+ enumerable: true
+ });
+ };
+
+ // DEPRECATED: use DevToolsUtils.assert(condition, message) instead!
+ var haveLoggedDeprecationMessage = false;
+ exports.dbg_assert = function dbg_assert(cond, e) {
+ if (!haveLoggedDeprecationMessage) {
+ haveLoggedDeprecationMessage = true;
+ var deprecationMessage = "DevToolsUtils.dbg_assert is deprecated! Use DevToolsUtils.assert instead!" + Error().stack;
+ console.log(deprecationMessage);
+ if (typeof console === "object" && console && console.warn) {
+ console.warn(deprecationMessage);
+ }
+ }
+
+ if (!cond) {
+ return e;
+ }
+ };
+
+ var _require4 = __webpack_require__(70);
+
+ var AppConstants = _require4.AppConstants;
+
+ /**
+ * No operation. The empty function.
+ */
+
+ exports.noop = function () {};
+
+ function reallyAssert(condition, message) {
+ if (!condition) {
+ var err = new Error("Assertion failure: " + message);
+ exports.reportException("DevToolsUtils.assert", err);
+ throw err;
+ }
+ }
+
+ /**
+ * DevToolsUtils.assert(condition, message)
+ *
+ * @param Boolean condition
+ * @param String message
+ *
+ * Assertions are enabled when any of the following are true:
+ * - This is a DEBUG_JS_MODULES build
+ * - This is a DEBUG build
+ * - DevToolsUtils.testing is set to true
+ *
+ * If assertions are enabled, then `condition` is checked and if false-y, the
+ * assertion failure is logged and then an error is thrown.
+ *
+ * If assertions are not enabled, then this function is a no-op.
+ *
+ * This is an improvement over `dbg_assert`, which doesn't actually cause any
+ * fatal behavior, and is therefore much easier to accidentally ignore.
+ */
+ Object.defineProperty(exports, "assert", {
+ get: () => AppConstants.DEBUG || AppConstants.DEBUG_JS_MODULES || _this.testing ? reallyAssert : exports.noop
+ });
+
+ /**
+ * Defines a getter on a specified object for a module. The module will not
+ * be imported until first use.
+ *
+ * @param aObject
+ * The object to define the lazy getter on.
+ * @param aName
+ * The name of the getter to define on aObject for the module.
+ * @param aResource
+ * The URL used to obtain the module.
+ * @param aSymbol
+ * The name of the symbol exported by the module.
+ * This parameter is optional and defaults to aName.
+ */
+ exports.defineLazyModuleGetter = function defineLazyModuleGetter(aObject, aName, aResource, aSymbol) {
+ this.defineLazyGetter(aObject, aName, function XPCU_moduleLambda() {
+ var temp = {};
+ Cu.import(aResource, temp);
+ return temp[aSymbol || aName];
+ });
+ };
+
+ var _require5 = __webpack_require__(71);
+
+ var NetUtil = _require5.NetUtil;
+
+ var _require6 = __webpack_require__(72);
+
+ var TextDecoder = _require6.TextDecoder;
+ var OS = _require6.OS;
+
+
+ var NetworkHelper = __webpack_require__(73);
+
+ /**
+ * Performs a request to load the desired URL and returns a promise.
+ *
+ * @param aURL String
+ * The URL we will request.
+ * @param aOptions Object
+ * An object with the following optional properties:
+ * - loadFromCache: if false, will bypass the cache and
+ * always load fresh from the network (default: true)
+ * - policy: the nsIContentPolicy type to apply when fetching the URL
+ * - window: the window to get the loadGroup from
+ * - charset: the charset to use if the channel doesn't provide one
+ * @returns Promise that resolves with an object with the following members on
+ * success:
+ * - content: the document at that URL, as a string,
+ * - contentType: the content type of the document
+ *
+ * If an error occurs, the promise is rejected with that error.
+ *
+ * XXX: It may be better to use nsITraceableChannel to get to the sources
+ * without relying on caching when we can (not for eval, etc.):
+ * http://www.softwareishard.com/blog/firebug/nsitraceablechannel-intercept-http-traffic/
+ */
+ function mainThreadFetch(aURL) {
+ var aOptions = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : { loadFromCache: true,
+ policy: Ci.nsIContentPolicy.TYPE_OTHER,
+ window: null,
+ charset: null };
+
+ // Create a channel.
+ var url = aURL.split(" -> ").pop();
+ var channel = void 0;
+ try {
+ channel = newChannelForURL(url, aOptions);
+ } catch (ex) {
+ return promise.reject(ex);
+ }
+
+ // Set the channel options.
+ channel.loadFlags = aOptions.loadFromCache ? channel.LOAD_FROM_CACHE : channel.LOAD_BYPASS_CACHE;
+
+ if (aOptions.window) {
+ // Respect private browsing.
+ channel.loadGroup = aOptions.window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation).QueryInterface(Ci.nsIDocumentLoader).loadGroup;
+ }
+
+ var deferred = promise.defer();
+ var onResponse = (stream, status, request) => {
+ if (!components.isSuccessCode(status)) {
+ deferred.reject(new Error(`Failed to fetch ${ url }. Code ${ status }.`));
+ return;
+ }
+
+ try {
+ // We cannot use NetUtil to do the charset conversion as if charset
+ // information is not available and our default guess is wrong the method
+ // might fail and we lose the stream data. This means we can't fall back
+ // to using the locale default encoding (bug 1181345).
+
+ // Read and decode the data according to the locale default encoding.
+ var available = stream.available();
+ var source = NetUtil.readInputStreamToString(stream, available);
+ stream.close();
+
+ // If the channel or the caller has correct charset information, the
+ // content will be decoded correctly. If we have to fall back to UTF-8 and
+ // the guess is wrong, the conversion fails and convertToUnicode returns
+ // the input unmodified. Essentially we try to decode the data as UTF-8
+ // and if that fails, we use the locale specific default encoding. This is
+ // the best we can do if the source does not provide charset info.
+ var charset = channel.contentCharset || aOptions.charset || "UTF-8";
+ var unicodeSource = NetworkHelper.convertToUnicode(source, charset);
+
+ deferred.resolve({
+ content: unicodeSource,
+ contentType: request.contentType
+ });
+ } catch (ex) {
+ var uri = request.originalURI;
+ if (ex.name === "NS_BASE_STREAM_CLOSED" && uri instanceof Ci.nsIFileURL) {
+ // Empty files cause NS_BASE_STREAM_CLOSED exception. Use OS.File to
+ // differentiate between empty files and other errors (bug 1170864).
+ // This can be removed when bug 982654 is fixed.
+
+ uri.QueryInterface(Ci.nsIFileURL);
+ var result = OS.File.read(uri.file.path).then(bytes => {
+ // Convert the bytearray to a String.
+ var decoder = new TextDecoder();
+ var content = decoder.decode(bytes);
+
+ // We can't detect the contentType without opening a channel
+ // and that failed already. This is the best we can do here.
+ return {
+ content,
+ contentType: "text/plain"
+ };
+ });
+
+ deferred.resolve(result);
+ } else {
+ deferred.reject(ex);
+ }
+ }
+ };
+
+ // Open the channel
+ try {
+ NetUtil.asyncFetch(channel, onResponse);
+ } catch (ex) {
+ return promise.reject(ex);
+ }
+
+ return deferred.promise;
+ }
+
+ /**
+ * Opens a channel for given URL. Tries a bit harder than NetUtil.newChannel.
+ *
+ * @param {String} url - The URL to open a channel for.
+ * @param {Object} options - The options object passed to @method fetch.
+ * @return {nsIChannel} - The newly created channel. Throws on failure.
+ */
+ function newChannelForURL(url, _ref) {
+ var policy = _ref.policy;
+
+ var channelOptions = {
+ contentPolicyType: policy,
+ loadUsingSystemPrincipal: true,
+ uri: url
+ };
+
+ try {
+ return NetUtil.newChannel(channelOptions);
+ } catch (e) {
+ // In the xpcshell tests, the script url is the absolute path of the test
+ // file, which will make a malformed URI error be thrown. Add the file
+ // scheme to see if it helps.
+ channelOptions.uri = "file://" + url;
+
+ return NetUtil.newChannel(channelOptions);
+ }
+ }
+
+ // Fetch is defined differently depending on whether we are on the main thread
+ // or a worker thread.
+ if (typeof WorkerGlobalScope === "undefined") {
+ // i.e. not in a worker
+ exports.fetch = mainThreadFetch;
+ } else {
+ // Services is not available in worker threads, nor is there any other way
+ // to fetch a URL. We need to enlist the help from the main thread here, by
+ // issuing an rpc request, to fetch the URL on our behalf.
+ exports.fetch = function (url, options) {
+ return rpc("fetch", url, options);
+ };
+ }
+
+ /**
+ * Returns a promise that is resolved or rejected when all promises have settled
+ * (resolved or rejected).
+ *
+ * This differs from Promise.all, which will reject immediately after the first
+ * rejection, instead of waiting for the remaining promises to settle.
+ *
+ * @param values
+ * Iterable of promises that may be pending, resolved, or rejected. When
+ * when all promises have settled (resolved or rejected), the returned
+ * promise will be resolved or rejected as well.
+ *
+ * @return A new promise that is fulfilled when all values have settled
+ * (resolved or rejected). Its resolution value will be an array of all
+ * resolved values in the given order, or undefined if values is an
+ * empty array. The reject reason will be forwarded from the first
+ * promise in the list of given promises to be rejected.
+ */
+ exports.settleAll = values => {
+ if (values === null || typeof values[Symbol.iterator] != "function") {
+ throw new Error("settleAll() expects an iterable.");
+ }
+
+ var deferred = promise.defer();
+
+ values = Array.isArray(values) ? values : [].concat(_toConsumableArray(values));
+ var countdown = values.length;
+ var resolutionValues = new Array(countdown);
+ var rejectionValue = void 0;
+ var rejectionOccurred = false;
+
+ if (!countdown) {
+ deferred.resolve(resolutionValues);
+ return deferred.promise;
+ }
+
+ function checkForCompletion() {
+ if (--countdown > 0) {
+ return;
+ }
+ if (!rejectionOccurred) {
+ deferred.resolve(resolutionValues);
+ } else {
+ deferred.reject(rejectionValue);
+ }
+ }
+
+ var _loop = function (i) {
+ var index = i;
+ var value = values[i];
+ var resolver = result => {
+ resolutionValues[index] = result;
+ checkForCompletion();
+ };
+ var rejecter = error => {
+ if (!rejectionOccurred) {
+ rejectionValue = error;
+ rejectionOccurred = true;
+ }
+ checkForCompletion();
+ };
+
+ if (value && typeof value.then == "function") {
+ value.then(resolver, rejecter);
+ } else {
+ // Given value is not a promise, forward it as a resolution value.
+ resolver(value);
+ }
+ };
+
+ for (var i = 0; i < values.length; i++) {
+ _loop(i);
+ }
+
+ return deferred.promise;
+ };
+
+ /**
+ * When the testing flag is set, various behaviors may be altered from
+ * production mode, typically to enable easier testing or enhanced debugging.
+ */
+ var testing = false;
+ Object.defineProperty(exports, "testing", {
+ get: function () {
+ return testing;
+ },
+ set: function (state) {
+ testing = state;
+ }
+ });
+
+ /**
+ * Open the file at the given path for reading.
+ *
+ * @param {String} filePath
+ *
+ * @returns Promise<nsIInputStream>
+ */
+ exports.openFileStream = function (filePath) {
+ return new Promise((resolve, reject) => {
+ var uri = NetUtil.newURI(new FileUtils.File(filePath));
+ NetUtil.asyncFetch({ uri, loadUsingSystemPrincipal: true }, (stream, result) => {
+ if (!components.isSuccessCode(result)) {
+ reject(new Error(`Could not open "${ filePath }": result = ${ result }`));
+ return;
+ }
+
+ resolve(stream);
+ });
+ });
+ };
+
+ exports.isGenerator = function (fn) {
+ if (typeof fn !== "function") {
+ return false;
+ }
+ var proto = Object.getPrototypeOf(fn);
+ if (!proto) {
+ return false;
+ }
+ var ctor = proto.constructor;
+ if (!ctor) {
+ return false;
+ }
+ return ctor.name == "GeneratorFunction";
+ };
+
+ exports.isPromise = function (p) {
+ return p && typeof p.then === "function";
+ };
+
+ /**
+ * Return true if `thing` is a SavedFrame, false otherwise.
+ */
+ exports.isSavedFrame = function (thing) {
+ return Object.prototype.toString.call(thing) === "[object SavedFrame]";
+ };
+
+/***/ },
+/* 69 */
+/***/ function(module, exports) {
+
+ /*
+ * A sham for https://dxr.mozilla.org/mozilla-central/source/toolkit/modules/FileUtils.jsm
+ */
+
+/***/ },
+/* 70 */
+/***/ function(module, exports) {
+
+ /*
+ * A sham for https://dxr.mozilla.org/mozilla-central/source/toolkit/modules/AppConstants.jsm
+ */
+
+ module.exports = { AppConstants: {} };
+
+/***/ },
+/* 71 */
+/***/ function(module, exports) {
+
+ /*
+ * A sham for https://dxr.mozilla.org/mozilla-central/source/netwerk/base/NetUtil.jsm
+ */
+
+/***/ },
+/* 72 */
+/***/ function(module, exports) {
+
+ /*
+ * A sham for https://dxr.mozilla.org/mozilla-central/source/toolkit/components/osfile/osfile.jsm
+ */
+
+/***/ },
+/* 73 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /* vim:set ts=2 sw=2 sts=2 et: */
+ /*
+ * Software License Agreement (BSD License)
+ *
+ * Copyright (c) 2007, Parakey Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use of this software in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above
+ * copyright notice, this list of conditions and the
+ * following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the
+ * following disclaimer in the documentation and/or other
+ * materials provided with the distribution.
+ *
+ * * Neither the name of Parakey Inc. nor the names of its
+ * contributors may be used to endorse or promote products
+ * derived from this software without specific prior
+ * written permission of Parakey Inc.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+ * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
+ * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
+ * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+ /*
+ * Creator:
+ * Joe Hewitt
+ * Contributors
+ * John J. Barton (IBM Almaden)
+ * Jan Odvarko (Mozilla Corp.)
+ * Max Stepanov (Aptana Inc.)
+ * Rob Campbell (Mozilla Corp.)
+ * Hans Hillen (Paciello Group, Mozilla)
+ * Curtis Bartley (Mozilla Corp.)
+ * Mike Collins (IBM Almaden)
+ * Kevin Decker
+ * Mike Ratcliffe (Comartis AG)
+ * Hernan Rodríguez Colmeiro
+ * Austin Andrews
+ * Christoph Dorn
+ * Steven Roussey (AppCenter Inc, Network54)
+ * Mihai Sucan (Mozilla Corp.)
+ */
+
+ "use strict";
+
+ var _require = __webpack_require__(61);
+
+ var components = _require.components;
+ var Cc = _require.Cc;
+ var Ci = _require.Ci;
+ var Cu = _require.Cu;
+
+ var _require2 = __webpack_require__(71);
+
+ var NetUtil = _require2.NetUtil;
+
+ var DevToolsUtils = __webpack_require__(68);
+
+ // The cache used in the `nsIURL` function.
+ var gNSURLStore = new Map();
+
+ /**
+ * Helper object for networking stuff.
+ *
+ * Most of the following functions have been taken from the Firebug source. They
+ * have been modified to match the Firefox coding rules.
+ */
+ var NetworkHelper = {
+ /**
+ * Converts aText with a given aCharset to unicode.
+ *
+ * @param string aText
+ * Text to convert.
+ * @param string aCharset
+ * Charset to convert the text to.
+ * @returns string
+ * Converted text.
+ */
+ convertToUnicode: function NH_convertToUnicode(aText, aCharset) {
+ var conv = Cc("@mozilla.org/intl/scriptableunicodeconverter").createInstance(Ci.nsIScriptableUnicodeConverter);
+ try {
+ conv.charset = aCharset || "UTF-8";
+ return conv.ConvertToUnicode(aText);
+ } catch (ex) {
+ return aText;
+ }
+ },
+
+ /**
+ * Reads all available bytes from aStream and converts them to aCharset.
+ *
+ * @param nsIInputStream aStream
+ * @param string aCharset
+ * @returns string
+ * UTF-16 encoded string based on the content of aStream and aCharset.
+ */
+ readAndConvertFromStream: function NH_readAndConvertFromStream(aStream, aCharset) {
+ var text = null;
+ try {
+ text = NetUtil.readInputStreamToString(aStream, aStream.available());
+ return this.convertToUnicode(text, aCharset);
+ } catch (err) {
+ return text;
+ }
+ },
+
+ /**
+ * Reads the posted text from aRequest.
+ *
+ * @param nsIHttpChannel aRequest
+ * @param string aCharset
+ * The content document charset, used when reading the POSTed data.
+ * @returns string or null
+ * Returns the posted string if it was possible to read from aRequest
+ * otherwise null.
+ */
+ readPostTextFromRequest: function NH_readPostTextFromRequest(aRequest, aCharset) {
+ if (aRequest instanceof Ci.nsIUploadChannel) {
+ var iStream = aRequest.uploadStream;
+
+ var isSeekableStream = false;
+ if (iStream instanceof Ci.nsISeekableStream) {
+ isSeekableStream = true;
+ }
+
+ var prevOffset = void 0;
+ if (isSeekableStream) {
+ prevOffset = iStream.tell();
+ iStream.seek(Ci.nsISeekableStream.NS_SEEK_SET, 0);
+ }
+
+ // Read data from the stream.
+ var text = this.readAndConvertFromStream(iStream, aCharset);
+
+ // Seek locks the file, so seek to the beginning only if necko hasn't
+ // read it yet, since necko doesn't seek to 0 before reading (at lest
+ // not till 459384 is fixed).
+ if (isSeekableStream && prevOffset == 0) {
+ iStream.seek(Ci.nsISeekableStream.NS_SEEK_SET, 0);
+ }
+ return text;
+ }
+ return null;
+ },
+
+ /**
+ * Reads the posted text from the page's cache.
+ *
+ * @param nsIDocShell aDocShell
+ * @param string aCharset
+ * @returns string or null
+ * Returns the posted string if it was possible to read from
+ * aDocShell otherwise null.
+ */
+ readPostTextFromPage: function NH_readPostTextFromPage(aDocShell, aCharset) {
+ var webNav = aDocShell.QueryInterface(Ci.nsIWebNavigation);
+ return this.readPostTextFromPageViaWebNav(webNav, aCharset);
+ },
+
+ /**
+ * Reads the posted text from the page's cache, given an nsIWebNavigation
+ * object.
+ *
+ * @param nsIWebNavigation aWebNav
+ * @param string aCharset
+ * @returns string or null
+ * Returns the posted string if it was possible to read from
+ * aWebNav, otherwise null.
+ */
+ readPostTextFromPageViaWebNav: function NH_readPostTextFromPageViaWebNav(aWebNav, aCharset) {
+ if (aWebNav instanceof Ci.nsIWebPageDescriptor) {
+ var descriptor = aWebNav.currentDescriptor;
+
+ if (descriptor instanceof Ci.nsISHEntry && descriptor.postData && descriptor instanceof Ci.nsISeekableStream) {
+ descriptor.seek(NS_SEEK_SET, 0);
+
+ return this.readAndConvertFromStream(descriptor, aCharset);
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Gets the web appId that is associated with aRequest.
+ *
+ * @param nsIHttpChannel aRequest
+ * @returns number|null
+ * The appId for the given request, if available.
+ */
+ getAppIdForRequest: function NH_getAppIdForRequest(aRequest) {
+ try {
+ return this.getRequestLoadContext(aRequest).appId;
+ } catch (ex) {
+ // request loadContent is not always available.
+ }
+ return null;
+ },
+
+ /**
+ * Gets the topFrameElement that is associated with aRequest. This
+ * works in single-process and multiprocess contexts. It may cross
+ * the content/chrome boundary.
+ *
+ * @param nsIHttpChannel aRequest
+ * @returns nsIDOMElement|null
+ * The top frame element for the given request.
+ */
+ getTopFrameForRequest: function NH_getTopFrameForRequest(aRequest) {
+ try {
+ return this.getRequestLoadContext(aRequest).topFrameElement;
+ } catch (ex) {
+ // request loadContent is not always available.
+ }
+ return null;
+ },
+
+ /**
+ * Gets the nsIDOMWindow that is associated with aRequest.
+ *
+ * @param nsIHttpChannel aRequest
+ * @returns nsIDOMWindow or null
+ */
+ getWindowForRequest: function NH_getWindowForRequest(aRequest) {
+ try {
+ return this.getRequestLoadContext(aRequest).associatedWindow;
+ } catch (ex) {
+ // TODO: bug 802246 - getWindowForRequest() throws on b2g: there is no
+ // associatedWindow property.
+ }
+ return null;
+ },
+
+ /**
+ * Gets the nsILoadContext that is associated with aRequest.
+ *
+ * @param nsIHttpChannel aRequest
+ * @returns nsILoadContext or null
+ */
+ getRequestLoadContext: function NH_getRequestLoadContext(aRequest) {
+ try {
+ return aRequest.notificationCallbacks.getInterface(Ci.nsILoadContext);
+ } catch (ex) {}
+
+ try {
+ return aRequest.loadGroup.notificationCallbacks.getInterface(Ci.nsILoadContext);
+ } catch (ex) {}
+
+ return null;
+ },
+
+ /**
+ * Determines whether the request has been made for the top level document.
+ *
+ * @param nsIHttpChannel aRequest
+ * @returns Boolean True if the request represents the top level document.
+ */
+ isTopLevelLoad: function (aRequest) {
+ if (aRequest instanceof Ci.nsIChannel) {
+ var loadInfo = aRequest.loadInfo;
+ if (loadInfo && loadInfo.parentOuterWindowID == loadInfo.outerWindowID) {
+ return aRequest.loadFlags & Ci.nsIChannel.LOAD_DOCUMENT_URI;
+ }
+ }
+
+ return false;
+ },
+
+ /**
+ * Loads the content of aUrl from the cache.
+ *
+ * @param string aUrl
+ * URL to load the cached content for.
+ * @param string aCharset
+ * Assumed charset of the cached content. Used if there is no charset
+ * on the channel directly.
+ * @param function aCallback
+ * Callback that is called with the loaded cached content if available
+ * or null if something failed while getting the cached content.
+ */
+ loadFromCache: function NH_loadFromCache(aUrl, aCharset, aCallback) {
+ var channel = NetUtil.newChannel({ uri: aUrl, loadUsingSystemPrincipal: true });
+
+ // Ensure that we only read from the cache and not the server.
+ channel.loadFlags = Ci.nsIRequest.LOAD_FROM_CACHE | Ci.nsICachingChannel.LOAD_ONLY_FROM_CACHE | Ci.nsICachingChannel.LOAD_BYPASS_LOCAL_CACHE_IF_BUSY;
+
+ NetUtil.asyncFetch(channel, (aInputStream, aStatusCode, aRequest) => {
+ if (!components.isSuccessCode(aStatusCode)) {
+ aCallback(null);
+ return;
+ }
+
+ // Try to get the encoding from the channel. If there is none, then use
+ // the passed assumed aCharset.
+ var aChannel = aRequest.QueryInterface(Ci.nsIChannel);
+ var contentCharset = aChannel.contentCharset || aCharset;
+
+ // Read the content of the stream using contentCharset as encoding.
+ aCallback(this.readAndConvertFromStream(aInputStream, contentCharset));
+ });
+ },
+
+ /**
+ * Parse a raw Cookie header value.
+ *
+ * @param string aHeader
+ * The raw Cookie header value.
+ * @return array
+ * Array holding an object for each cookie. Each object holds the
+ * following properties: name and value.
+ */
+ parseCookieHeader: function NH_parseCookieHeader(aHeader) {
+ var cookies = aHeader.split(";");
+ var result = [];
+
+ cookies.forEach(function (aCookie) {
+ var equal = aCookie.indexOf("=");
+ var name = aCookie.substr(0, equal);
+ var value = aCookie.substr(equal + 1);
+ result.push({ name: unescape(name.trim()),
+ value: unescape(value.trim()) });
+ });
+
+ return result;
+ },
+
+ /**
+ * Parse a raw Set-Cookie header value.
+ *
+ * @param string aHeader
+ * The raw Set-Cookie header value.
+ * @return array
+ * Array holding an object for each cookie. Each object holds the
+ * following properties: name, value, secure (boolean), httpOnly
+ * (boolean), path, domain and expires (ISO date string).
+ */
+ parseSetCookieHeader: function NH_parseSetCookieHeader(aHeader) {
+ var rawCookies = aHeader.split(/\r\n|\n|\r/);
+ var cookies = [];
+
+ rawCookies.forEach(function (aCookie) {
+ var equal = aCookie.indexOf("=");
+ var name = unescape(aCookie.substr(0, equal).trim());
+ var parts = aCookie.substr(equal + 1).split(";");
+ var value = unescape(parts.shift().trim());
+
+ var cookie = { name: name, value: value };
+
+ parts.forEach(function (aPart) {
+ var part = aPart.trim();
+ if (part.toLowerCase() == "secure") {
+ cookie.secure = true;
+ } else if (part.toLowerCase() == "httponly") {
+ cookie.httpOnly = true;
+ } else if (part.indexOf("=") > -1) {
+ var pair = part.split("=");
+ pair[0] = pair[0].toLowerCase();
+ if (pair[0] == "path" || pair[0] == "domain") {
+ cookie[pair[0]] = pair[1];
+ } else if (pair[0] == "expires") {
+ try {
+ pair[1] = pair[1].replace(/-/g, ' ');
+ cookie.expires = new Date(pair[1]).toISOString();
+ } catch (ex) {}
+ }
+ }
+ });
+
+ cookies.push(cookie);
+ });
+
+ return cookies;
+ },
+
+ // This is a list of all the mime category maps jviereck could find in the
+ // firebug code base.
+ mimeCategoryMap: {
+ "text/plain": "txt",
+ "text/html": "html",
+ "text/xml": "xml",
+ "text/xsl": "txt",
+ "text/xul": "txt",
+ "text/css": "css",
+ "text/sgml": "txt",
+ "text/rtf": "txt",
+ "text/x-setext": "txt",
+ "text/richtext": "txt",
+ "text/javascript": "js",
+ "text/jscript": "txt",
+ "text/tab-separated-values": "txt",
+ "text/rdf": "txt",
+ "text/xif": "txt",
+ "text/ecmascript": "js",
+ "text/vnd.curl": "txt",
+ "text/x-json": "json",
+ "text/x-js": "txt",
+ "text/js": "txt",
+ "text/vbscript": "txt",
+ "view-source": "txt",
+ "view-fragment": "txt",
+ "application/xml": "xml",
+ "application/xhtml+xml": "xml",
+ "application/atom+xml": "xml",
+ "application/rss+xml": "xml",
+ "application/vnd.mozilla.maybe.feed": "xml",
+ "application/vnd.mozilla.xul+xml": "xml",
+ "application/javascript": "js",
+ "application/x-javascript": "js",
+ "application/x-httpd-php": "txt",
+ "application/rdf+xml": "xml",
+ "application/ecmascript": "js",
+ "application/http-index-format": "txt",
+ "application/json": "json",
+ "application/x-js": "txt",
+ "multipart/mixed": "txt",
+ "multipart/x-mixed-replace": "txt",
+ "image/svg+xml": "svg",
+ "application/octet-stream": "bin",
+ "image/jpeg": "image",
+ "image/jpg": "image",
+ "image/gif": "image",
+ "image/png": "image",
+ "image/bmp": "image",
+ "application/x-shockwave-flash": "flash",
+ "video/x-flv": "flash",
+ "audio/mpeg3": "media",
+ "audio/x-mpeg-3": "media",
+ "video/mpeg": "media",
+ "video/x-mpeg": "media",
+ "audio/ogg": "media",
+ "application/ogg": "media",
+ "application/x-ogg": "media",
+ "application/x-midi": "media",
+ "audio/midi": "media",
+ "audio/x-mid": "media",
+ "audio/x-midi": "media",
+ "music/crescendo": "media",
+ "audio/wav": "media",
+ "audio/x-wav": "media",
+ "text/json": "json",
+ "application/x-json": "json",
+ "application/json-rpc": "json",
+ "application/x-web-app-manifest+json": "json",
+ "application/manifest+json": "json"
+ },
+
+ /**
+ * Check if the given MIME type is a text-only MIME type.
+ *
+ * @param string aMimeType
+ * @return boolean
+ */
+ isTextMimeType: function NH_isTextMimeType(aMimeType) {
+ if (aMimeType.indexOf("text/") == 0) {
+ return true;
+ }
+
+ // XML and JSON often come with custom MIME types, so in addition to the
+ // standard "application/xml" and "application/json", we also look for
+ // variants like "application/x-bigcorp+xml". For JSON we allow "+json" and
+ // "-json" as suffixes.
+ if (/^application\/\w+(?:[\.-]\w+)*(?:\+xml|[-+]json)$/.test(aMimeType)) {
+ return true;
+ }
+
+ var category = this.mimeCategoryMap[aMimeType] || null;
+ switch (category) {
+ case "txt":
+ case "js":
+ case "json":
+ case "css":
+ case "html":
+ case "svg":
+ case "xml":
+ return true;
+
+ default:
+ return false;
+ }
+ },
+
+ /**
+ * Takes a securityInfo object of nsIRequest, the nsIRequest itself and
+ * extracts security information from them.
+ *
+ * @param object securityInfo
+ * The securityInfo object of a request. If null channel is assumed
+ * to be insecure.
+ * @param object httpActivity
+ * The httpActivity object for the request with at least members
+ * { private, hostname }.
+ *
+ * @return object
+ * Returns an object containing following members:
+ * - state: The security of the connection used to fetch this
+ * request. Has one of following string values:
+ * * "insecure": the connection was not secure (only http)
+ * * "weak": the connection has minor security issues
+ * * "broken": secure connection failed (e.g. expired cert)
+ * * "secure": the connection was properly secured.
+ * If state == broken:
+ * - errorMessage: full error message from nsITransportSecurityInfo.
+ * If state == secure:
+ * - protocolVersion: one of TLSv1, TLSv1.1, TLSv1.2.
+ * - cipherSuite: the cipher suite used in this connection.
+ * - cert: information about certificate used in this connection.
+ * See parseCertificateInfo for the contents.
+ * - hsts: true if host uses Strict Transport Security, false otherwise
+ * - hpkp: true if host uses Public Key Pinning, false otherwise
+ * If state == weak: Same as state == secure and
+ * - weaknessReasons: list of reasons that cause the request to be
+ * considered weak. See getReasonsForWeakness.
+ */
+ parseSecurityInfo: function NH_parseSecurityInfo(securityInfo, httpActivity) {
+ var info = {
+ state: "insecure"
+ };
+
+ // The request did not contain any security info.
+ if (!securityInfo) {
+ return info;
+ }
+
+ /**
+ * Different scenarios to consider here and how they are handled:
+ * - request is HTTP, the connection is not secure
+ * => securityInfo is null
+ * => state === "insecure"
+ *
+ * - request is HTTPS, the connection is secure
+ * => .securityState has STATE_IS_SECURE flag
+ * => state === "secure"
+ *
+ * - request is HTTPS, the connection has security issues
+ * => .securityState has STATE_IS_INSECURE flag
+ * => .errorCode is an NSS error code.
+ * => state === "broken"
+ *
+ * - request is HTTPS, the connection was terminated before the security
+ * could be validated
+ * => .securityState has STATE_IS_INSECURE flag
+ * => .errorCode is NOT an NSS error code.
+ * => .errorMessage is not available.
+ * => state === "insecure"
+ *
+ * - request is HTTPS but it uses a weak cipher or old protocol, see
+ * http://hg.mozilla.org/mozilla-central/annotate/def6ed9d1c1a/
+ * security/manager/ssl/nsNSSCallbacks.cpp#l1233
+ * - request is mixed content (which makes no sense whatsoever)
+ * => .securityState has STATE_IS_BROKEN flag
+ * => .errorCode is NOT an NSS error code
+ * => .errorMessage is not available
+ * => state === "weak"
+ */
+
+ securityInfo.QueryInterface(Ci.nsITransportSecurityInfo);
+ securityInfo.QueryInterface(Ci.nsISSLStatusProvider);
+
+ var wpl = Ci.nsIWebProgressListener;
+ var NSSErrorsService = Cc['@mozilla.org/nss_errors_service;1'].getService(Ci.nsINSSErrorsService);
+ var SSLStatus = securityInfo.SSLStatus;
+ if (!NSSErrorsService.isNSSErrorCode(securityInfo.errorCode)) {
+ var state = securityInfo.securityState;
+
+ var uri = null;
+ if (httpActivity.channel && httpActivity.channel.URI) {
+ uri = httpActivity.channel.URI;
+ }
+ if (uri && !uri.schemeIs("https") && !uri.schemeIs("wss")) {
+ // it is not enough to look at the transport security info - schemes other than
+ // https and wss are subject to downgrade/etc at the scheme level and should
+ // always be considered insecure
+ info.state = "insecure";
+ } else if (state & wpl.STATE_IS_SECURE) {
+ // The connection is secure if the scheme is sufficient
+ info.state = "secure";
+ } else if (state & wpl.STATE_IS_BROKEN) {
+ // The connection is not secure, there was no error but there's some
+ // minor security issues.
+ info.state = "weak";
+ info.weaknessReasons = this.getReasonsForWeakness(state);
+ } else if (state & wpl.STATE_IS_INSECURE) {
+ // This was most likely an https request that was aborted before
+ // validation. Return info as info.state = insecure.
+ return info;
+ } else {
+ DevToolsUtils.reportException("NetworkHelper.parseSecurityInfo", "Security state " + state + " has no known STATE_IS_* flags.");
+ return info;
+ }
+
+ // Cipher suite.
+ info.cipherSuite = SSLStatus.cipherName;
+
+ // Protocol version.
+ info.protocolVersion = this.formatSecurityProtocol(SSLStatus.protocolVersion);
+
+ // Certificate.
+ info.cert = this.parseCertificateInfo(SSLStatus.serverCert);
+
+ // HSTS and HPKP if available.
+ if (httpActivity.hostname) {
+ var sss = Cc("@mozilla.org/ssservice;1").getService(Ci.nsISiteSecurityService);
+
+ // SiteSecurityService uses different storage if the channel is
+ // private. Thus we must give isSecureHost correct flags or we
+ // might get incorrect results.
+ var flags = httpActivity.private ? Ci.nsISocketProvider.NO_PERMANENT_STORAGE : 0;
+
+ var host = httpActivity.hostname;
+
+ info.hsts = sss.isSecureHost(sss.HEADER_HSTS, host, flags);
+ info.hpkp = sss.isSecureHost(sss.HEADER_HPKP, host, flags);
+ } else {
+ DevToolsUtils.reportException("NetworkHelper.parseSecurityInfo", "Could not get HSTS/HPKP status as hostname is not available.");
+ info.hsts = false;
+ info.hpkp = false;
+ }
+ } else {
+ // The connection failed.
+ info.state = "broken";
+ info.errorMessage = securityInfo.errorMessage;
+ }
+
+ return info;
+ },
+
+ /**
+ * Takes an nsIX509Cert and returns an object with certificate information.
+ *
+ * @param nsIX509Cert cert
+ * The certificate to extract the information from.
+ * @return object
+ * An object with following format:
+ * {
+ * subject: { commonName, organization, organizationalUnit },
+ * issuer: { commonName, organization, organizationUnit },
+ * validity: { start, end },
+ * fingerprint: { sha1, sha256 }
+ * }
+ */
+ parseCertificateInfo: function NH_parseCertifificateInfo(cert) {
+ var info = {};
+ if (cert) {
+ info.subject = {
+ commonName: cert.commonName,
+ organization: cert.organization,
+ organizationalUnit: cert.organizationalUnit
+ };
+
+ info.issuer = {
+ commonName: cert.issuerCommonName,
+ organization: cert.issuerOrganization,
+ organizationUnit: cert.issuerOrganizationUnit
+ };
+
+ info.validity = {
+ start: cert.validity.notBeforeLocalDay,
+ end: cert.validity.notAfterLocalDay
+ };
+
+ info.fingerprint = {
+ sha1: cert.sha1Fingerprint,
+ sha256: cert.sha256Fingerprint
+ };
+ } else {
+ DevToolsUtils.reportException("NetworkHelper.parseCertificateInfo", "Secure connection established without certificate.");
+ }
+
+ return info;
+ },
+
+ /**
+ * Takes protocolVersion of SSLStatus object and returns human readable
+ * description.
+ *
+ * @param Number version
+ * One of nsISSLStatus version constants.
+ * @return string
+ * One of TLSv1, TLSv1.1, TLSv1.2 if @param version is valid,
+ * Unknown otherwise.
+ */
+ formatSecurityProtocol: function NH_formatSecurityProtocol(version) {
+ switch (version) {
+ case Ci.nsISSLStatus.TLS_VERSION_1:
+ return "TLSv1";
+ case Ci.nsISSLStatus.TLS_VERSION_1_1:
+ return "TLSv1.1";
+ case Ci.nsISSLStatus.TLS_VERSION_1_2:
+ return "TLSv1.2";
+ default:
+ DevToolsUtils.reportException("NetworkHelper.formatSecurityProtocol", "protocolVersion " + version + " is unknown.");
+ return "Unknown";
+ }
+ },
+
+ /**
+ * Takes the securityState bitfield and returns reasons for weak connection
+ * as an array of strings.
+ *
+ * @param Number state
+ * nsITransportSecurityInfo.securityState.
+ *
+ * @return Array[String]
+ * List of weakness reasons. A subset of { cipher } where
+ * * cipher: The cipher suite is consireded to be weak (RC4).
+ */
+ getReasonsForWeakness: function NH_getReasonsForWeakness(state) {
+ var wpl = Ci.nsIWebProgressListener;
+
+ // If there's non-fatal security issues the request has STATE_IS_BROKEN
+ // flag set. See http://hg.mozilla.org/mozilla-central/file/44344099d119
+ // /security/manager/ssl/nsNSSCallbacks.cpp#l1233
+ var reasons = [];
+
+ if (state & wpl.STATE_IS_BROKEN) {
+ var isCipher = state & wpl.STATE_USES_WEAK_CRYPTO;
+
+ if (isCipher) {
+ reasons.push("cipher");
+ }
+
+ if (!isCipher) {
+ DevToolsUtils.reportException("NetworkHelper.getReasonsForWeakness", "STATE_IS_BROKEN without a known reason. Full state was: " + state);
+ }
+ }
+
+ return reasons;
+ },
+
+ /**
+ * Parse a url's query string into its components
+ *
+ * @param string aQueryString
+ * The query part of a url
+ * @return array
+ * Array of query params {name, value}
+ */
+ parseQueryString: function (aQueryString) {
+ // Make sure there's at least one param available.
+ // Be careful here, params don't necessarily need to have values, so
+ // no need to verify the existence of a "=".
+ if (!aQueryString) {
+ return;
+ }
+
+ // Turn the params string into an array containing { name: value } tuples.
+ var paramsArray = aQueryString.replace(/^[?&]/, "").split("&").map(e => {
+ var param = e.split("=");
+ return {
+ name: param[0] ? NetworkHelper.convertToUnicode(unescape(param[0])) : "",
+ value: param[1] ? NetworkHelper.convertToUnicode(unescape(param[1])) : ""
+ };
+ });
+
+ return paramsArray;
+ },
+
+ /**
+ * Helper for getting an nsIURL instance out of a string.
+ */
+ nsIURL: function (aUrl) {
+ var aStore = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : gNSURLStore;
+
+ if (aStore.has(aUrl)) {
+ return aStore.get(aUrl);
+ }
+
+ var uri = Services.io.newURI(aUrl, null, null).QueryInterface(Ci.nsIURL);
+ aStore.set(aUrl, uri);
+ return uri;
+ }
+ };
+
+ for (var prop of Object.getOwnPropertyNames(NetworkHelper)) {
+ exports[prop] = NetworkHelper[prop];
+ }
+
+/***/ },
+/* 74 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /* 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 _require = __webpack_require__(61);
+
+ var Ci = _require.Ci;
+ var Cc = _require.Cc;
+ var Cr = _require.Cr;
+ var CC = _require.CC;
+
+ var _require2 = __webpack_require__(30);
+
+ var Services = _require2.Services;
+
+ var _require3 = __webpack_require__(68);
+
+ var dumpv = _require3.dumpv;
+
+ var EventEmitter = __webpack_require__(60);
+ var promise = __webpack_require__(66);
+
+ var IOUtil = Cc("@mozilla.org/io-util;1").getService(Ci.nsIIOUtil);
+
+ var ScriptableInputStream = CC("@mozilla.org/scriptableinputstream;1", "nsIScriptableInputStream", "init");
+
+ var BUFFER_SIZE = 0x8000;
+
+ /**
+ * This helper function (and its companion object) are used by bulk senders and
+ * receivers to read and write data in and out of other streams. Functions that
+ * make use of this tool are passed to callers when it is time to read or write
+ * bulk data. It is highly recommended to use these copier functions instead of
+ * the stream directly because the copier enforces the agreed upon length.
+ * Since bulk mode reuses an existing stream, the sender and receiver must write
+ * and read exactly the agreed upon amount of data, or else the entire transport
+ * will be left in a invalid state. Additionally, other methods of stream
+ * copying (such as NetUtil.asyncCopy) close the streams involved, which would
+ * terminate the debugging transport, and so it is avoided here.
+ *
+ * Overall, this *works*, but clearly the optimal solution would be able to just
+ * use the streams directly. If it were possible to fully implement
+ * nsIInputStream / nsIOutputStream in JS, wrapper streams could be created to
+ * enforce the length and avoid closing, and consumers could use familiar stream
+ * utilities like NetUtil.asyncCopy.
+ *
+ * The function takes two async streams and copies a precise number of bytes
+ * from one to the other. Copying begins immediately, but may complete at some
+ * future time depending on data size. Use the returned promise to know when
+ * it's complete.
+ *
+ * @param input nsIAsyncInputStream
+ * The stream to copy from.
+ * @param output nsIAsyncOutputStream
+ * The stream to copy to.
+ * @param length Integer
+ * The amount of data that needs to be copied.
+ * @return Promise
+ * The promise is resolved when copying completes or rejected if any
+ * (unexpected) errors occur.
+ */
+ function copyStream(input, output, length) {
+ var copier = new StreamCopier(input, output, length);
+ return copier.copy();
+ }
+
+ function StreamCopier(input, output, length) {
+ EventEmitter.decorate(this);
+ this._id = StreamCopier._nextId++;
+ this.input = input;
+ // Save off the base output stream, since we know it's async as we've required
+ this.baseAsyncOutput = output;
+ if (IOUtil.outputStreamIsBuffered(output)) {
+ this.output = output;
+ } else {
+ this.output = Cc("@mozilla.org/network/buffered-output-stream;1").createInstance(Ci.nsIBufferedOutputStream);
+ this.output.init(output, BUFFER_SIZE);
+ }
+ this._length = length;
+ this._amountLeft = length;
+ this._deferred = promise.defer();
+
+ this._copy = this._copy.bind(this);
+ this._flush = this._flush.bind(this);
+ this._destroy = this._destroy.bind(this);
+
+ // Copy promise's then method up to this object.
+ // Allows the copier to offer a promise interface for the simple succeed or
+ // fail scenarios, but also emit events (due to the EventEmitter) for other
+ // states, like progress.
+ this.then = this._deferred.promise.then.bind(this._deferred.promise);
+ this.then(this._destroy, this._destroy);
+
+ // Stream ready callback starts as |_copy|, but may switch to |_flush| at end
+ // if flushing would block the output stream.
+ this._streamReadyCallback = this._copy;
+ }
+ StreamCopier._nextId = 0;
+
+ StreamCopier.prototype = {
+
+ copy: function () {
+ // Dispatch to the next tick so that it's possible to attach a progress
+ // event listener, even for extremely fast copies (like when testing).
+ Services.tm.currentThread.dispatch(() => {
+ try {
+ this._copy();
+ } catch (e) {
+ this._deferred.reject(e);
+ }
+ }, 0);
+ return this;
+ },
+
+ _copy: function () {
+ var bytesAvailable = this.input.available();
+ var amountToCopy = Math.min(bytesAvailable, this._amountLeft);
+ this._debug("Trying to copy: " + amountToCopy);
+
+ var bytesCopied = void 0;
+ try {
+ bytesCopied = this.output.writeFrom(this.input, amountToCopy);
+ } catch (e) {
+ if (e.result == Cr.NS_BASE_STREAM_WOULD_BLOCK) {
+ this._debug("Base stream would block, will retry");
+ this._debug("Waiting for output stream");
+ this.baseAsyncOutput.asyncWait(this, 0, 0, Services.tm.currentThread);
+ return;
+ } else {
+ throw e;
+ }
+ }
+
+ this._amountLeft -= bytesCopied;
+ this._debug("Copied: " + bytesCopied + ", Left: " + this._amountLeft);
+ this._emitProgress();
+
+ if (this._amountLeft === 0) {
+ this._debug("Copy done!");
+ this._flush();
+ return;
+ }
+
+ this._debug("Waiting for input stream");
+ this.input.asyncWait(this, 0, 0, Services.tm.currentThread);
+ },
+
+ _emitProgress: function () {
+ this.emit("progress", {
+ bytesSent: this._length - this._amountLeft,
+ totalBytes: this._length
+ });
+ },
+
+ _flush: function () {
+ try {
+ this.output.flush();
+ } catch (e) {
+ if (e.result == Cr.NS_BASE_STREAM_WOULD_BLOCK || e.result == Cr.NS_ERROR_FAILURE) {
+ this._debug("Flush would block, will retry");
+ this._streamReadyCallback = this._flush;
+ this._debug("Waiting for output stream");
+ this.baseAsyncOutput.asyncWait(this, 0, 0, Services.tm.currentThread);
+ return;
+ } else {
+ throw e;
+ }
+ }
+ this._deferred.resolve();
+ },
+
+ _destroy: function () {
+ this._destroy = null;
+ this._copy = null;
+ this._flush = null;
+ this.input = null;
+ this.output = null;
+ },
+
+ // nsIInputStreamCallback
+ onInputStreamReady: function () {
+ this._streamReadyCallback();
+ },
+
+ // nsIOutputStreamCallback
+ onOutputStreamReady: function () {
+ this._streamReadyCallback();
+ },
+
+ _debug: function (msg) {
+ // Prefix logs with the copier ID, which makes logs much easier to
+ // understand when several copiers are running simultaneously
+ dumpv("Copier: " + this._id + " " + msg);
+ }
+
+ };
+
+ /**
+ * Read from a stream, one byte at a time, up to the next |delimiter|
+ * character, but stopping if we've read |count| without finding it. Reading
+ * also terminates early if there are less than |count| bytes available on the
+ * stream. In that case, we only read as many bytes as the stream currently has
+ * to offer.
+ * TODO: This implementation could be removed if bug 984651 is fixed, which
+ * provides a native version of the same idea.
+ * @param stream nsIInputStream
+ * The input stream to read from.
+ * @param delimiter string
+ * The character we're trying to find.
+ * @param count integer
+ * The max number of characters to read while searching.
+ * @return string
+ * The data collected. If the delimiter was found, this string will
+ * end with it.
+ */
+ function delimitedRead(stream, delimiter, count) {
+ dumpv("Starting delimited read for " + delimiter + " up to " + count + " bytes");
+
+ var scriptableStream = void 0;
+ if (stream.readBytes) {
+ scriptableStream = stream;
+ } else {
+ scriptableStream = new ScriptableInputStream(stream);
+ }
+
+ var data = "";
+
+ // Don't exceed what's available on the stream
+ count = Math.min(count, stream.available());
+
+ if (count <= 0) {
+ return data;
+ }
+
+ var char = void 0;
+ while (char !== delimiter && count > 0) {
+ char = scriptableStream.readBytes(1);
+ count--;
+ data += char;
+ }
+
+ return data;
+ }
+
+ module.exports = {
+ copyStream: copyStream,
+ delimitedRead: delimitedRead
+ };
+
+/***/ },
+/* 75 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /* 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";
+
+ /**
+ * Packets contain read / write functionality for the different packet types
+ * supported by the debugging protocol, so that a transport can focus on
+ * delivery and queue management without worrying too much about the specific
+ * packet types.
+ *
+ * They are intended to be "one use only", so a new packet should be
+ * instantiated for each incoming or outgoing packet.
+ *
+ * A complete Packet type should expose at least the following:
+ * * read(stream, scriptableStream)
+ * Called when the input stream has data to read
+ * * write(stream)
+ * Called when the output stream is ready to write
+ * * get done()
+ * Returns true once the packet is done being read / written
+ * * destroy()
+ * Called to clean up at the end of use
+ */
+
+ var _require = __webpack_require__(61);
+
+ var Cc = _require.Cc;
+ var Ci = _require.Ci;
+ var Cu = _require.Cu;
+
+ var DevToolsUtils = __webpack_require__(68);
+ var dumpn = DevToolsUtils.dumpn;
+ var dumpv = DevToolsUtils.dumpv;
+
+ var StreamUtils = __webpack_require__(74);
+ var promise = __webpack_require__(66);
+
+ /*DevToolsUtils.defineLazyGetter(this, "unicodeConverter", () => {
+ const unicodeConverter = Cc("@mozilla.org/intl/scriptableunicodeconverter")
+ .createInstance(Ci.nsIScriptableUnicodeConverter);
+ unicodeConverter.charset = "UTF-8";
+ return unicodeConverter;
+ });*/
+ var utf8 = __webpack_require__(76);
+
+ // The transport's previous check ensured the header length did not exceed 20
+ // characters. Here, we opt for the somewhat smaller, but still large limit of
+ // 1 TiB.
+ var PACKET_LENGTH_MAX = Math.pow(2, 40);
+
+ /**
+ * A generic Packet processing object (extended by two subtypes below).
+ */
+ function Packet(transport) {
+ this._transport = transport;
+ this._length = 0;
+ }
+
+ /**
+ * Attempt to initialize a new Packet based on the incoming packet header we've
+ * received so far. We try each of the types in succession, trying JSON packets
+ * first since they are much more common.
+ * @param header string
+ * The packet header string to attempt parsing.
+ * @param transport DebuggerTransport
+ * The transport instance that will own the packet.
+ * @return Packet
+ * The parsed packet of the matching type, or null if no types matched.
+ */
+ Packet.fromHeader = function (header, transport) {
+ return JSONPacket.fromHeader(header, transport) || BulkPacket.fromHeader(header, transport);
+ };
+
+ Packet.prototype = {
+
+ get length() {
+ return this._length;
+ },
+
+ set length(length) {
+ if (length > PACKET_LENGTH_MAX) {
+ throw Error("Packet length " + length + " exceeds the max length of " + PACKET_LENGTH_MAX);
+ }
+ this._length = length;
+ },
+
+ destroy: function () {
+ this._transport = null;
+ }
+
+ };
+
+ exports.Packet = Packet;
+
+ /**
+ * With a JSON packet (the typical packet type sent via the transport), data is
+ * transferred as a JSON packet serialized into a string, with the string length
+ * prepended to the packet, followed by a colon ([length]:[packet]). The
+ * contents of the JSON packet are specified in the Remote Debugging Protocol
+ * specification.
+ * @param transport DebuggerTransport
+ * The transport instance that will own the packet.
+ */
+ function JSONPacket(transport) {
+ Packet.call(this, transport);
+ this._data = "";
+ this._done = false;
+ }
+
+ /**
+ * Attempt to initialize a new JSONPacket based on the incoming packet header
+ * we've received so far.
+ * @param header string
+ * The packet header string to attempt parsing.
+ * @param transport DebuggerTransport
+ * The transport instance that will own the packet.
+ * @return JSONPacket
+ * The parsed packet, or null if it's not a match.
+ */
+ JSONPacket.fromHeader = function (header, transport) {
+ var match = this.HEADER_PATTERN.exec(header);
+
+ if (!match) {
+ return null;
+ }
+
+ dumpv("Header matches JSON packet");
+ var packet = new JSONPacket(transport);
+ packet.length = +match[1];
+ return packet;
+ };
+
+ JSONPacket.HEADER_PATTERN = /^(\d+):$/;
+
+ JSONPacket.prototype = Object.create(Packet.prototype);
+
+ Object.defineProperty(JSONPacket.prototype, "object", {
+ /**
+ * Gets the object (not the serialized string) being read or written.
+ */
+ get: function () {
+ return this._object;
+ },
+
+ /**
+ * Sets the object to be sent when write() is called.
+ */
+ set: function (object) {
+ this._object = object;
+ var data = JSON.stringify(object);
+ this._data = data;
+ this.length = this._data.length;
+ }
+ });
+
+ JSONPacket.prototype.read = function (stream, scriptableStream) {
+ dumpv("Reading JSON packet");
+
+ // Read in more packet data.
+ this._readData(stream, scriptableStream);
+
+ if (!this.done) {
+ // Don't have a complete packet yet.
+ return;
+ }
+
+ var json = this._data;
+ try {
+ json = utf8.decode(json);
+ this._object = JSON.parse(json);
+ } catch (e) {
+ var msg = "Error parsing incoming packet: " + json + " (" + e + " - " + e.stack + ")";
+ if (console.error) {
+ console.error(msg);
+ }
+ dumpn(msg);
+ return;
+ }
+
+ this._transport._onJSONObjectReady(this._object);
+ };
+
+ JSONPacket.prototype._readData = function (stream, scriptableStream) {
+ if (!scriptableStream) {
+ scriptableStream = stream;
+ }
+ if (dumpv.wantVerbose) {
+ dumpv("Reading JSON data: _l: " + this.length + " dL: " + this._data.length + " sA: " + stream.available());
+ }
+ var bytesToRead = Math.min(this.length - this._data.length, stream.available());
+ this._data += scriptableStream.readBytes(bytesToRead);
+ this._done = this._data.length === this.length;
+ };
+
+ JSONPacket.prototype.write = function (stream) {
+ dumpv("Writing JSON packet");
+
+ if (this._outgoing === undefined) {
+ // Format the serialized packet to a buffer
+ this._outgoing = this.length + ":" + this._data;
+ }
+
+ var written = stream.write(this._outgoing, this._outgoing.length);
+ this._outgoing = this._outgoing.slice(written);
+ this._done = !this._outgoing.length;
+ };
+
+ Object.defineProperty(JSONPacket.prototype, "done", {
+ get: function () {
+ return this._done;
+ }
+ });
+
+ JSONPacket.prototype.toString = function () {
+ return JSON.stringify(this._object, null, 2);
+ };
+
+ exports.JSONPacket = JSONPacket;
+
+ /**
+ * With a bulk packet, data is transferred by temporarily handing over the
+ * transport's input or output stream to the application layer for writing data
+ * directly. This can be much faster for large data sets, and avoids various
+ * stages of copies and data duplication inherent in the JSON packet type. The
+ * bulk packet looks like:
+ *
+ * bulk [actor] [type] [length]:[data]
+ *
+ * The interpretation of the data portion depends on the kind of actor and the
+ * packet's type. See the Remote Debugging Protocol Stream Transport spec for
+ * more details.
+ * @param transport DebuggerTransport
+ * The transport instance that will own the packet.
+ */
+ function BulkPacket(transport) {
+ Packet.call(this, transport);
+ this._done = false;
+ this._readyForWriting = promise.defer();
+ }
+
+ /**
+ * Attempt to initialize a new BulkPacket based on the incoming packet header
+ * we've received so far.
+ * @param header string
+ * The packet header string to attempt parsing.
+ * @param transport DebuggerTransport
+ * The transport instance that will own the packet.
+ * @return BulkPacket
+ * The parsed packet, or null if it's not a match.
+ */
+ BulkPacket.fromHeader = function (header, transport) {
+ var match = this.HEADER_PATTERN.exec(header);
+
+ if (!match) {
+ return null;
+ }
+
+ dumpv("Header matches bulk packet");
+ var packet = new BulkPacket(transport);
+ packet.header = {
+ actor: match[1],
+ type: match[2],
+ length: +match[3]
+ };
+ return packet;
+ };
+
+ BulkPacket.HEADER_PATTERN = /^bulk ([^: ]+) ([^: ]+) (\d+):$/;
+
+ BulkPacket.prototype = Object.create(Packet.prototype);
+
+ BulkPacket.prototype.read = function (stream) {
+ dumpv("Reading bulk packet, handing off input stream");
+
+ // Temporarily pause monitoring of the input stream
+ this._transport.pauseIncoming();
+
+ var deferred = promise.defer();
+
+ this._transport._onBulkReadReady({
+ actor: this.actor,
+ type: this.type,
+ length: this.length,
+ copyTo: output => {
+ dumpv("CT length: " + this.length);
+ var copying = StreamUtils.copyStream(stream, output, this.length);
+ deferred.resolve(copying);
+ return copying;
+ },
+ stream: stream,
+ done: deferred
+ });
+
+ // Await the result of reading from the stream
+ deferred.promise.then(() => {
+ dumpv("onReadDone called, ending bulk mode");
+ this._done = true;
+ this._transport.resumeIncoming();
+ }, this._transport.close);
+
+ // Ensure this is only done once
+ this.read = () => {
+ throw new Error("Tried to read() a BulkPacket's stream multiple times.");
+ };
+ };
+
+ BulkPacket.prototype.write = function (stream) {
+ dumpv("Writing bulk packet");
+
+ if (this._outgoingHeader === undefined) {
+ dumpv("Serializing bulk packet header");
+ // Format the serialized packet header to a buffer
+ this._outgoingHeader = "bulk " + this.actor + " " + this.type + " " + this.length + ":";
+ }
+
+ // Write the header, or whatever's left of it to write.
+ if (this._outgoingHeader.length) {
+ dumpv("Writing bulk packet header");
+ var written = stream.write(this._outgoingHeader, this._outgoingHeader.length);
+ this._outgoingHeader = this._outgoingHeader.slice(written);
+ return;
+ }
+
+ dumpv("Handing off output stream");
+
+ // Temporarily pause the monitoring of the output stream
+ this._transport.pauseOutgoing();
+
+ var deferred = promise.defer();
+
+ this._readyForWriting.resolve({
+ copyFrom: input => {
+ dumpv("CF length: " + this.length);
+ var copying = StreamUtils.copyStream(input, stream, this.length);
+ deferred.resolve(copying);
+ return copying;
+ },
+ stream: stream,
+ done: deferred
+ });
+
+ // Await the result of writing to the stream
+ deferred.promise.then(() => {
+ dumpv("onWriteDone called, ending bulk mode");
+ this._done = true;
+ this._transport.resumeOutgoing();
+ }, this._transport.close);
+
+ // Ensure this is only done once
+ this.write = () => {
+ throw new Error("Tried to write() a BulkPacket's stream multiple times.");
+ };
+ };
+
+ Object.defineProperty(BulkPacket.prototype, "streamReadyForWriting", {
+ get: function () {
+ return this._readyForWriting.promise;
+ }
+ });
+
+ Object.defineProperty(BulkPacket.prototype, "header", {
+ get: function () {
+ return {
+ actor: this.actor,
+ type: this.type,
+ length: this.length
+ };
+ },
+
+ set: function (header) {
+ this.actor = header.actor;
+ this.type = header.type;
+ this.length = header.length;
+ }
+ });
+
+ Object.defineProperty(BulkPacket.prototype, "done", {
+ get: function () {
+ return this._done;
+ }
+ });
+
+ BulkPacket.prototype.toString = function () {
+ return "Bulk: " + JSON.stringify(this.header, null, 2);
+ };
+
+ exports.BulkPacket = BulkPacket;
+
+ /**
+ * RawPacket is used to test the transport's error handling of malformed
+ * packets, by writing data directly onto the stream.
+ * @param transport DebuggerTransport
+ * The transport instance that will own the packet.
+ * @param data string
+ * The raw string to send out onto the stream.
+ */
+ function RawPacket(transport, data) {
+ Packet.call(this, transport);
+ this._data = data;
+ this.length = data.length;
+ this._done = false;
+ }
+
+ RawPacket.prototype = Object.create(Packet.prototype);
+
+ RawPacket.prototype.read = function (stream) {
+ // This hasn't yet been needed for testing.
+ throw Error("Not implmented.");
+ };
+
+ RawPacket.prototype.write = function (stream) {
+ var written = stream.write(this._data, this._data.length);
+ this._data = this._data.slice(written);
+ this._done = !this._data.length;
+ };
+
+ Object.defineProperty(RawPacket.prototype, "done", {
+ get: function () {
+ return this._done;
+ }
+ });
+
+ exports.RawPacket = RawPacket;
+
+/***/ },
+/* 76 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_RESULT__;/* WEBPACK VAR INJECTION */(function(module, global) {/*! https://mths.be/utf8js v2.0.0 by @mathias */
+ ;(function (root) {
+
+ // Detect free variables `exports`
+ var freeExports = typeof exports == 'object' && exports;
+
+ // Detect free variable `module`
+ var freeModule = typeof module == 'object' && module && module.exports == freeExports && module;
+
+ // Detect free variable `global`, from Node.js or Browserified code,
+ // and use it as `root`
+ var freeGlobal = typeof global == 'object' && global;
+ if (freeGlobal.global === freeGlobal || freeGlobal.window === freeGlobal) {
+ root = freeGlobal;
+ }
+
+ /*--------------------------------------------------------------------------*/
+
+ var stringFromCharCode = String.fromCharCode;
+
+ // Taken from https://mths.be/punycode
+ function ucs2decode(string) {
+ var output = [];
+ var counter = 0;
+ var length = string.length;
+ var value;
+ var extra;
+ while (counter < length) {
+ value = string.charCodeAt(counter++);
+ if (value >= 0xD800 && value <= 0xDBFF && counter < length) {
+ // high surrogate, and there is a next character
+ extra = string.charCodeAt(counter++);
+ if ((extra & 0xFC00) == 0xDC00) {
+ // low surrogate
+ output.push(((value & 0x3FF) << 10) + (extra & 0x3FF) + 0x10000);
+ } else {
+ // unmatched surrogate; only append this code unit, in case the next
+ // code unit is the high surrogate of a surrogate pair
+ output.push(value);
+ counter--;
+ }
+ } else {
+ output.push(value);
+ }
+ }
+ return output;
+ }
+
+ // Taken from https://mths.be/punycode
+ function ucs2encode(array) {
+ var length = array.length;
+ var index = -1;
+ var value;
+ var output = '';
+ while (++index < length) {
+ value = array[index];
+ if (value > 0xFFFF) {
+ value -= 0x10000;
+ output += stringFromCharCode(value >>> 10 & 0x3FF | 0xD800);
+ value = 0xDC00 | value & 0x3FF;
+ }
+ output += stringFromCharCode(value);
+ }
+ return output;
+ }
+
+ function checkScalarValue(codePoint) {
+ if (codePoint >= 0xD800 && codePoint <= 0xDFFF) {
+ throw Error('Lone surrogate U+' + codePoint.toString(16).toUpperCase() + ' is not a scalar value');
+ }
+ }
+ /*--------------------------------------------------------------------------*/
+
+ function createByte(codePoint, shift) {
+ return stringFromCharCode(codePoint >> shift & 0x3F | 0x80);
+ }
+
+ function encodeCodePoint(codePoint) {
+ if ((codePoint & 0xFFFFFF80) == 0) {
+ // 1-byte sequence
+ return stringFromCharCode(codePoint);
+ }
+ var symbol = '';
+ if ((codePoint & 0xFFFFF800) == 0) {
+ // 2-byte sequence
+ symbol = stringFromCharCode(codePoint >> 6 & 0x1F | 0xC0);
+ } else if ((codePoint & 0xFFFF0000) == 0) {
+ // 3-byte sequence
+ checkScalarValue(codePoint);
+ symbol = stringFromCharCode(codePoint >> 12 & 0x0F | 0xE0);
+ symbol += createByte(codePoint, 6);
+ } else if ((codePoint & 0xFFE00000) == 0) {
+ // 4-byte sequence
+ symbol = stringFromCharCode(codePoint >> 18 & 0x07 | 0xF0);
+ symbol += createByte(codePoint, 12);
+ symbol += createByte(codePoint, 6);
+ }
+ symbol += stringFromCharCode(codePoint & 0x3F | 0x80);
+ return symbol;
+ }
+
+ function utf8encode(string) {
+ var codePoints = ucs2decode(string);
+ var length = codePoints.length;
+ var index = -1;
+ var codePoint;
+ var byteString = '';
+ while (++index < length) {
+ codePoint = codePoints[index];
+ byteString += encodeCodePoint(codePoint);
+ }
+ return byteString;
+ }
+
+ /*--------------------------------------------------------------------------*/
+
+ function readContinuationByte() {
+ if (byteIndex >= byteCount) {
+ throw Error('Invalid byte index');
+ }
+
+ var continuationByte = byteArray[byteIndex] & 0xFF;
+ byteIndex++;
+
+ if ((continuationByte & 0xC0) == 0x80) {
+ return continuationByte & 0x3F;
+ }
+
+ // If we end up here, it’s not a continuation byte
+ throw Error('Invalid continuation byte');
+ }
+
+ function decodeSymbol() {
+ var byte1;
+ var byte2;
+ var byte3;
+ var byte4;
+ var codePoint;
+
+ if (byteIndex > byteCount) {
+ throw Error('Invalid byte index');
+ }
+
+ if (byteIndex == byteCount) {
+ return false;
+ }
+
+ // Read first byte
+ byte1 = byteArray[byteIndex] & 0xFF;
+ byteIndex++;
+
+ // 1-byte sequence (no continuation bytes)
+ if ((byte1 & 0x80) == 0) {
+ return byte1;
+ }
+
+ // 2-byte sequence
+ if ((byte1 & 0xE0) == 0xC0) {
+ var byte2 = readContinuationByte();
+ codePoint = (byte1 & 0x1F) << 6 | byte2;
+ if (codePoint >= 0x80) {
+ return codePoint;
+ } else {
+ throw Error('Invalid continuation byte');
+ }
+ }
+
+ // 3-byte sequence (may include unpaired surrogates)
+ if ((byte1 & 0xF0) == 0xE0) {
+ byte2 = readContinuationByte();
+ byte3 = readContinuationByte();
+ codePoint = (byte1 & 0x0F) << 12 | byte2 << 6 | byte3;
+ if (codePoint >= 0x0800) {
+ checkScalarValue(codePoint);
+ return codePoint;
+ } else {
+ throw Error('Invalid continuation byte');
+ }
+ }
+
+ // 4-byte sequence
+ if ((byte1 & 0xF8) == 0xF0) {
+ byte2 = readContinuationByte();
+ byte3 = readContinuationByte();
+ byte4 = readContinuationByte();
+ codePoint = (byte1 & 0x0F) << 0x12 | byte2 << 0x0C | byte3 << 0x06 | byte4;
+ if (codePoint >= 0x010000 && codePoint <= 0x10FFFF) {
+ return codePoint;
+ }
+ }
+
+ throw Error('Invalid UTF-8 detected');
+ }
+
+ var byteArray;
+ var byteCount;
+ var byteIndex;
+ function utf8decode(byteString) {
+ byteArray = ucs2decode(byteString);
+ byteCount = byteArray.length;
+ byteIndex = 0;
+ var codePoints = [];
+ var tmp;
+ while ((tmp = decodeSymbol()) !== false) {
+ codePoints.push(tmp);
+ }
+ return ucs2encode(codePoints);
+ }
+
+ /*--------------------------------------------------------------------------*/
+
+ var utf8 = {
+ 'version': '2.0.0',
+ 'encode': utf8encode,
+ 'decode': utf8decode
+ };
+
+ // Some AMD build optimizers, like r.js, check for specific condition patterns
+ // like the following:
+ if (true) {
+ !(__WEBPACK_AMD_DEFINE_RESULT__ = function () {
+ return utf8;
+ }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+ } else if (freeExports && !freeExports.nodeType) {
+ if (freeModule) {
+ // in Node.js or RingoJS v0.8.0+
+ freeModule.exports = utf8;
+ } else {
+ // in Narwhal or RingoJS v0.7.0-
+ var object = {};
+ var hasOwnProperty = object.hasOwnProperty;
+ for (var key in utf8) {
+ hasOwnProperty.call(utf8, key) && (freeExports[key] = utf8[key]);
+ }
+ }
+ } else {
+ // in Rhino or a web browser
+ root.utf8 = utf8;
+ }
+ })(this);
+ /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(77)(module), (function() { return this; }())))
+
+/***/ },
+/* 77 */
+/***/ function(module, exports) {
+
+ module.exports = function(module) {
+ if(!module.webpackPolyfill) {
+ module.deprecate = function() {};
+ module.paths = [];
+ // module.parent = undefined by default
+ module.children = [];
+ module.webpackPolyfill = 1;
+ }
+ return module;
+ }
+
+
+/***/ },
+/* 78 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_RESULT__;/* WEBPACK VAR INJECTION */(function(module, global) {/*! https://mths.be/utf8js v2.0.0 by @mathias */
+ ;(function (root) {
+
+ // Detect free variables `exports`
+ var freeExports = typeof exports == 'object' && exports;
+
+ // Detect free variable `module`
+ var freeModule = typeof module == 'object' && module && module.exports == freeExports && module;
+
+ // Detect free variable `global`, from Node.js or Browserified code,
+ // and use it as `root`
+ var freeGlobal = typeof global == 'object' && global;
+ if (freeGlobal.global === freeGlobal || freeGlobal.window === freeGlobal) {
+ root = freeGlobal;
+ }
+
+ /*--------------------------------------------------------------------------*/
+
+ var stringFromCharCode = String.fromCharCode;
+
+ // Taken from https://mths.be/punycode
+ function ucs2decode(string) {
+ var output = [];
+ var counter = 0;
+ var length = string.length;
+ var value;
+ var extra;
+ while (counter < length) {
+ value = string.charCodeAt(counter++);
+ if (value >= 0xD800 && value <= 0xDBFF && counter < length) {
+ // high surrogate, and there is a next character
+ extra = string.charCodeAt(counter++);
+ if ((extra & 0xFC00) == 0xDC00) {
+ // low surrogate
+ output.push(((value & 0x3FF) << 10) + (extra & 0x3FF) + 0x10000);
+ } else {
+ // unmatched surrogate; only append this code unit, in case the next
+ // code unit is the high surrogate of a surrogate pair
+ output.push(value);
+ counter--;
+ }
+ } else {
+ output.push(value);
+ }
+ }
+ return output;
+ }
+
+ // Taken from https://mths.be/punycode
+ function ucs2encode(array) {
+ var length = array.length;
+ var index = -1;
+ var value;
+ var output = '';
+ while (++index < length) {
+ value = array[index];
+ if (value > 0xFFFF) {
+ value -= 0x10000;
+ output += stringFromCharCode(value >>> 10 & 0x3FF | 0xD800);
+ value = 0xDC00 | value & 0x3FF;
+ }
+ output += stringFromCharCode(value);
+ }
+ return output;
+ }
+
+ function checkScalarValue(codePoint) {
+ if (codePoint >= 0xD800 && codePoint <= 0xDFFF) {
+ throw Error('Lone surrogate U+' + codePoint.toString(16).toUpperCase() + ' is not a scalar value');
+ }
+ }
+ /*--------------------------------------------------------------------------*/
+
+ function createByte(codePoint, shift) {
+ return stringFromCharCode(codePoint >> shift & 0x3F | 0x80);
+ }
+
+ function encodeCodePoint(codePoint) {
+ if ((codePoint & 0xFFFFFF80) == 0) {
+ // 1-byte sequence
+ return stringFromCharCode(codePoint);
+ }
+ var symbol = '';
+ if ((codePoint & 0xFFFFF800) == 0) {
+ // 2-byte sequence
+ symbol = stringFromCharCode(codePoint >> 6 & 0x1F | 0xC0);
+ } else if ((codePoint & 0xFFFF0000) == 0) {
+ // 3-byte sequence
+ checkScalarValue(codePoint);
+ symbol = stringFromCharCode(codePoint >> 12 & 0x0F | 0xE0);
+ symbol += createByte(codePoint, 6);
+ } else if ((codePoint & 0xFFE00000) == 0) {
+ // 4-byte sequence
+ symbol = stringFromCharCode(codePoint >> 18 & 0x07 | 0xF0);
+ symbol += createByte(codePoint, 12);
+ symbol += createByte(codePoint, 6);
+ }
+ symbol += stringFromCharCode(codePoint & 0x3F | 0x80);
+ return symbol;
+ }
+
+ function utf8encode(string) {
+ var codePoints = ucs2decode(string);
+ var length = codePoints.length;
+ var index = -1;
+ var codePoint;
+ var byteString = '';
+ while (++index < length) {
+ codePoint = codePoints[index];
+ byteString += encodeCodePoint(codePoint);
+ }
+ return byteString;
+ }
+
+ /*--------------------------------------------------------------------------*/
+
+ function readContinuationByte() {
+ if (byteIndex >= byteCount) {
+ throw Error('Invalid byte index');
+ }
+
+ var continuationByte = byteArray[byteIndex] & 0xFF;
+ byteIndex++;
+
+ if ((continuationByte & 0xC0) == 0x80) {
+ return continuationByte & 0x3F;
+ }
+
+ // If we end up here, it’s not a continuation byte
+ throw Error('Invalid continuation byte');
+ }
+
+ function decodeSymbol() {
+ var byte1;
+ var byte2;
+ var byte3;
+ var byte4;
+ var codePoint;
+
+ if (byteIndex > byteCount) {
+ throw Error('Invalid byte index');
+ }
+
+ if (byteIndex == byteCount) {
+ return false;
+ }
+
+ // Read first byte
+ byte1 = byteArray[byteIndex] & 0xFF;
+ byteIndex++;
+
+ // 1-byte sequence (no continuation bytes)
+ if ((byte1 & 0x80) == 0) {
+ return byte1;
+ }
+
+ // 2-byte sequence
+ if ((byte1 & 0xE0) == 0xC0) {
+ var byte2 = readContinuationByte();
+ codePoint = (byte1 & 0x1F) << 6 | byte2;
+ if (codePoint >= 0x80) {
+ return codePoint;
+ } else {
+ throw Error('Invalid continuation byte');
+ }
+ }
+
+ // 3-byte sequence (may include unpaired surrogates)
+ if ((byte1 & 0xF0) == 0xE0) {
+ byte2 = readContinuationByte();
+ byte3 = readContinuationByte();
+ codePoint = (byte1 & 0x0F) << 12 | byte2 << 6 | byte3;
+ if (codePoint >= 0x0800) {
+ checkScalarValue(codePoint);
+ return codePoint;
+ } else {
+ throw Error('Invalid continuation byte');
+ }
+ }
+
+ // 4-byte sequence
+ if ((byte1 & 0xF8) == 0xF0) {
+ byte2 = readContinuationByte();
+ byte3 = readContinuationByte();
+ byte4 = readContinuationByte();
+ codePoint = (byte1 & 0x0F) << 0x12 | byte2 << 0x0C | byte3 << 0x06 | byte4;
+ if (codePoint >= 0x010000 && codePoint <= 0x10FFFF) {
+ return codePoint;
+ }
+ }
+
+ throw Error('Invalid UTF-8 detected');
+ }
+
+ var byteArray;
+ var byteCount;
+ var byteIndex;
+ function utf8decode(byteString) {
+ byteArray = ucs2decode(byteString);
+ byteCount = byteArray.length;
+ byteIndex = 0;
+ var codePoints = [];
+ var tmp;
+ while ((tmp = decodeSymbol()) !== false) {
+ codePoints.push(tmp);
+ }
+ return ucs2encode(codePoints);
+ }
+
+ /*--------------------------------------------------------------------------*/
+
+ var utf8 = {
+ 'version': '2.0.0',
+ 'encode': utf8encode,
+ 'decode': utf8decode
+ };
+
+ // Some AMD build optimizers, like r.js, check for specific condition patterns
+ // like the following:
+ if (true) {
+ !(__WEBPACK_AMD_DEFINE_RESULT__ = function () {
+ return utf8;
+ }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+ } else if (freeExports && !freeExports.nodeType) {
+ if (freeModule) {
+ // in Node.js or RingoJS v0.8.0+
+ freeModule.exports = utf8;
+ } else {
+ // in Narwhal or RingoJS v0.7.0-
+ var object = {};
+ var hasOwnProperty = object.hasOwnProperty;
+ for (var key in utf8) {
+ hasOwnProperty.call(utf8, key) && (freeExports[key] = utf8[key]);
+ }
+ }
+ } else {
+ // in Rhino or a web browser
+ root.utf8 = utf8;
+ }
+ })(this);
+ /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(77)(module), (function() { return this; }())))
+
+/***/ },
+/* 79 */
+/***/ function(module, exports, __webpack_require__) {
+
+ function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
+
+ /* -*- 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/. */
+
+ var _require = __webpack_require__(61);
+
+ var Ci = _require.Ci;
+ var Cu = _require.Cu;
+ var components = _require.components;
+
+ var _require2 = __webpack_require__(30);
+
+ var Services = _require2.Services;
+
+ var DevToolsUtils = __webpack_require__(68);
+
+ // WARNING I swapped the sync one for the async one here
+ // const promise = require("resource://devtools/shared/deprecated-sync-thenables.js", {}).Promise;
+ var promise = __webpack_require__(66);
+
+ var events = __webpack_require__(80);
+
+ var _require3 = __webpack_require__(82);
+
+ var WebConsoleClient = _require3.WebConsoleClient;
+ /* const { DebuggerSocket } = require("../shared/security/socket");*/
+ /* const Authentication = require("../shared/security/auth");*/
+
+ var noop = () => {};
+
+ /**
+ * TODO: Get rid of this API in favor of EventTarget (bug 1042642)
+ *
+ * Add simple event notification to a prototype object. Any object that has
+ * some use for event notifications or the observer pattern in general can be
+ * augmented with the necessary facilities by passing its prototype to this
+ * function.
+ *
+ * @param aProto object
+ * The prototype object that will be modified.
+ */
+ function eventSource(aProto) {
+ /**
+ * Add a listener to the event source for a given event.
+ *
+ * @param aName string
+ * The event to listen for.
+ * @param aListener function
+ * Called when the event is fired. If the same listener
+ * is added more than once, it will be called once per
+ * addListener call.
+ */
+ aProto.addListener = function (aName, aListener) {
+ if (typeof aListener != "function") {
+ throw TypeError("Listeners must be functions.");
+ }
+
+ if (!this._listeners) {
+ this._listeners = {};
+ }
+
+ this._getListeners(aName).push(aListener);
+ };
+
+ /**
+ * Add a listener to the event source for a given event. The
+ * listener will be removed after it is called for the first time.
+ *
+ * @param aName string
+ * The event to listen for.
+ * @param aListener function
+ * Called when the event is fired.
+ */
+ aProto.addOneTimeListener = function (aName, aListener) {
+ var _this = this;
+
+ var l = function () {
+ for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
+ args[_key] = arguments[_key];
+ }
+
+ _this.removeListener(aName, l);
+ aListener.apply(null, args);
+ };
+ this.addListener(aName, l);
+ };
+
+ /**
+ * Remove a listener from the event source previously added with
+ * addListener().
+ *
+ * @param aName string
+ * The event name used during addListener to add the listener.
+ * @param aListener function
+ * The callback to remove. If addListener was called multiple
+ * times, all instances will be removed.
+ */
+ aProto.removeListener = function (aName, aListener) {
+ if (!this._listeners || aListener && !this._listeners[aName]) {
+ return;
+ }
+
+ if (!aListener) {
+ this._listeners[aName] = [];
+ } else {
+ this._listeners[aName] = this._listeners[aName].filter(function (l) {
+ return l != aListener;
+ });
+ }
+ };
+
+ /**
+ * Returns the listeners for the specified event name. If none are defined it
+ * initializes an empty list and returns that.
+ *
+ * @param aName string
+ * The event name.
+ */
+ aProto._getListeners = function (aName) {
+ if (aName in this._listeners) {
+ return this._listeners[aName];
+ }
+ this._listeners[aName] = [];
+ return this._listeners[aName];
+ };
+
+ /**
+ * Notify listeners of an event.
+ *
+ * @param aName string
+ * The event to fire.
+ * @param arguments
+ * All arguments will be passed along to the listeners,
+ * including the name argument.
+ */
+ aProto.emit = function () {
+ if (!this._listeners) {
+ return;
+ }
+
+ var name = arguments[0];
+ var listeners = this._getListeners(name).slice(0);
+
+ for (var listener of listeners) {
+ try {
+ listener.apply(null, arguments);
+ } catch (e) {
+ // Prevent a bad listener from interfering with the others.
+ DevToolsUtils.reportException("notify event '" + name + "'", e);
+ }
+ }
+ };
+ }
+
+ /**
+ * Set of protocol messages that affect thread state, and the
+ * state the actor is in after each message.
+ */
+ var ThreadStateTypes = {
+ "paused": "paused",
+ "resumed": "attached",
+ "detached": "detached"
+ };
+
+ /**
+ * Set of protocol messages that are sent by the server without a prior request
+ * by the client.
+ */
+ var UnsolicitedNotifications = {
+ "consoleAPICall": "consoleAPICall",
+ "eventNotification": "eventNotification",
+ "fileActivity": "fileActivity",
+ "lastPrivateContextExited": "lastPrivateContextExited",
+ "logMessage": "logMessage",
+ "networkEvent": "networkEvent",
+ "networkEventUpdate": "networkEventUpdate",
+ "newGlobal": "newGlobal",
+ "newScript": "newScript",
+ "tabDetached": "tabDetached",
+ "tabListChanged": "tabListChanged",
+ "reflowActivity": "reflowActivity",
+ "addonListChanged": "addonListChanged",
+ "workerListChanged": "workerListChanged",
+ "serviceWorkerRegistrationListChanged": "serviceWorkerRegistrationList",
+ "tabNavigated": "tabNavigated",
+ "frameUpdate": "frameUpdate",
+ "pageError": "pageError",
+ "documentLoad": "documentLoad",
+ "enteredFrame": "enteredFrame",
+ "exitedFrame": "exitedFrame",
+ "appOpen": "appOpen",
+ "appClose": "appClose",
+ "appInstall": "appInstall",
+ "appUninstall": "appUninstall",
+ "evaluationResult": "evaluationResult",
+ "newSource": "newSource",
+ "updatedSource": "updatedSource"
+ };
+
+ /**
+ * Set of pause types that are sent by the server and not as an immediate
+ * response to a client request.
+ */
+ var UnsolicitedPauses = {
+ "resumeLimit": "resumeLimit",
+ "debuggerStatement": "debuggerStatement",
+ "breakpoint": "breakpoint",
+ "DOMEvent": "DOMEvent",
+ "watchpoint": "watchpoint",
+ "exception": "exception"
+ };
+
+ /**
+ * Creates a client for the remote debugging protocol server. This client
+ * provides the means to communicate with the server and exchange the messages
+ * required by the protocol in a traditional JavaScript API.
+ */
+ var DebuggerClient = exports.DebuggerClient = function (aTransport) {
+ this._transport = aTransport;
+ this._transport.hooks = this;
+
+ // Map actor ID to client instance for each actor type.
+ this._clients = new Map();
+
+ this._pendingRequests = new Map();
+ this._activeRequests = new Map();
+ this._eventsEnabled = true;
+
+ this.traits = {};
+
+ this.request = this.request.bind(this);
+ this.localTransport = this._transport.onOutputStreamReady === undefined;
+
+ /*
+ * As the first thing on the connection, expect a greeting packet from
+ * the connection's root actor.
+ */
+ this.mainRoot = null;
+ this.expectReply("root", aPacket => {
+ this.mainRoot = new RootClient(this, aPacket);
+ this.emit("connected", aPacket.applicationType, aPacket.traits);
+ });
+ };
+
+ /**
+ * A declarative helper for defining methods that send requests to the server.
+ *
+ * @param aPacketSkeleton
+ * The form of the packet to send. Can specify fields to be filled from
+ * the parameters by using the |args| function.
+ * @param telemetry
+ * The unique suffix of the telemetry histogram id.
+ * @param before
+ * The function to call before sending the packet. Is passed the packet,
+ * and the return value is used as the new packet. The |this| context is
+ * the instance of the client object we are defining a method for.
+ * @param after
+ * The function to call after the response is received. It is passed the
+ * response, and the return value is considered the new response that
+ * will be passed to the callback. The |this| context is the instance of
+ * the client object we are defining a method for.
+ * @return Request
+ * The `Request` object that is a Promise object and resolves once
+ * we receive the response. (See request method for more details)
+ */
+ DebuggerClient.requester = function (aPacketSkeleton) {
+ var config = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
+ var telemetry = config.telemetry;
+ var before = config.before;
+ var after = config.after;
+
+ return DevToolsUtils.makeInfallible(function () {
+ for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
+ args[_key2] = arguments[_key2];
+ }
+
+ var histogram = void 0,
+ startTime = void 0;
+ if (telemetry) {
+ var transportType = this._transport.onOutputStreamReady === undefined ? "LOCAL_" : "REMOTE_";
+ var histogramId = "DEVTOOLS_DEBUGGER_RDP_" + transportType + telemetry + "_MS";
+ histogram = Services.telemetry.getHistogramById(histogramId);
+ startTime = +new Date();
+ }
+ var outgoingPacket = {
+ to: aPacketSkeleton.to || this.actor
+ };
+
+ var maxPosition = -1;
+ for (var k of Object.keys(aPacketSkeleton)) {
+ if (aPacketSkeleton[k] instanceof DebuggerClient.Argument) {
+ var position = aPacketSkeleton[k].position;
+
+ outgoingPacket[k] = aPacketSkeleton[k].getArgument(args);
+ maxPosition = Math.max(position, maxPosition);
+ } else {
+ outgoingPacket[k] = aPacketSkeleton[k];
+ }
+ }
+
+ if (before) {
+ outgoingPacket = before.call(this, outgoingPacket);
+ }
+
+ return this.request(outgoingPacket, DevToolsUtils.makeInfallible(aResponse => {
+ if (after) {
+ var _aResponse = aResponse;
+ var from = _aResponse.from;
+
+ aResponse = after.call(this, aResponse);
+ if (!aResponse.from) {
+ aResponse.from = from;
+ }
+ }
+
+ // The callback is always the last parameter.
+ var thisCallback = args[maxPosition + 1];
+ if (thisCallback) {
+ thisCallback(aResponse);
+ }
+
+ if (histogram) {
+ histogram.add(+new Date() - startTime);
+ }
+ }, "DebuggerClient.requester request callback"));
+ }, "DebuggerClient.requester");
+ };
+
+ function args(aPos) {
+ return new DebuggerClient.Argument(aPos);
+ }
+
+ DebuggerClient.Argument = function (aPosition) {
+ this.position = aPosition;
+ };
+
+ DebuggerClient.Argument.prototype.getArgument = function (aParams) {
+ if (!(this.position in aParams)) {
+ throw new Error("Bad index into params: " + this.position);
+ }
+ return aParams[this.position];
+ };
+
+ // Expose these to save callers the trouble of importing DebuggerSocket
+ DebuggerClient.socketConnect = function (options) {
+ // Defined here instead of just copying the function to allow lazy-load
+ return DebuggerSocket.connect(options);
+ };
+ DevToolsUtils.defineLazyGetter(DebuggerClient, "Authenticators", () => {
+ return Authentication.Authenticators;
+ });
+ DevToolsUtils.defineLazyGetter(DebuggerClient, "AuthenticationResult", () => {
+ return Authentication.AuthenticationResult;
+ });
+
+ DebuggerClient.prototype = {
+ /**
+ * Connect to the server and start exchanging protocol messages.
+ *
+ * @param aOnConnected function
+ * If specified, will be called when the greeting packet is
+ * received from the debugging server.
+ *
+ * @return Promise
+ * Resolves once connected with an array whose first element
+ * is the application type, by default "browser", and the second
+ * element is the traits object (help figure out the features
+ * and behaviors of the server we connect to. See RootActor).
+ */
+ connect: function (aOnConnected) {
+ return Promise.race([new Promise((resolve, reject) => {
+ this.emit("connect");
+
+ // Also emit the event on the |DebuggerClient| object (not on the instance),
+ // so it's possible to track all instances.
+ events.emit(DebuggerClient, "connect", this);
+
+ this.addOneTimeListener("connected", (aName, aApplicationType, aTraits) => {
+ this.traits = aTraits;
+ if (aOnConnected) {
+ aOnConnected(aApplicationType, aTraits);
+ }
+ resolve([aApplicationType, aTraits]);
+ });
+
+ this._transport.ready();
+ }), new Promise((resolve, reject) => {
+ setTimeout(() => reject(new Error("Connect timeout error")), 6000);
+ })]);
+ },
+
+ /**
+ * Shut down communication with the debugging server.
+ *
+ * @param aOnClosed function
+ * If specified, will be called when the debugging connection
+ * has been closed.
+ */
+ close: function (aOnClosed) {
+ // Disable detach event notifications, because event handlers will be in a
+ // cleared scope by the time they run.
+ this._eventsEnabled = false;
+
+ var cleanup = () => {
+ this._transport.close();
+ this._transport = null;
+ };
+
+ // If the connection is already closed,
+ // there is no need to detach client
+ // as we won't be able to send any message.
+ if (this._closed) {
+ cleanup();
+ if (aOnClosed) {
+ aOnClosed();
+ }
+ return;
+ }
+
+ if (aOnClosed) {
+ this.addOneTimeListener("closed", function (aEvent) {
+ aOnClosed();
+ });
+ }
+
+ // Call each client's `detach` method by calling
+ // lastly registered ones first to give a chance
+ // to detach child clients first.
+ var clients = [].concat(_toConsumableArray(this._clients.values()));
+ this._clients.clear();
+ var detachClients = () => {
+ var client = clients.pop();
+ if (!client) {
+ // All clients detached.
+ cleanup();
+ return;
+ }
+ if (client.detach) {
+ client.detach(detachClients);
+ return;
+ }
+ detachClients();
+ };
+ detachClients();
+ },
+
+ /*
+ * This function exists only to preserve DebuggerClient's interface;
+ * new code should say 'client.mainRoot.listTabs()'.
+ */
+ listTabs: function (aOnResponse) {
+ return this.mainRoot.listTabs(aOnResponse);
+ },
+
+ /*
+ * This function exists only to preserve DebuggerClient's interface;
+ * new code should say 'client.mainRoot.listAddons()'.
+ */
+ listAddons: function (aOnResponse) {
+ return this.mainRoot.listAddons(aOnResponse);
+ },
+
+ getTab: function (aFilter) {
+ return this.mainRoot.getTab(aFilter);
+ },
+
+ /**
+ * Attach to a tab actor.
+ *
+ * @param string aTabActor
+ * The actor ID for the tab to attach.
+ * @param function aOnResponse
+ * Called with the response packet and a TabClient
+ * (which will be undefined on error).
+ */
+ attachTab: function (aTabActor) {
+ var _this2 = this;
+
+ var aOnResponse = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : noop;
+
+ if (this._clients.has(aTabActor)) {
+ var _ret = function () {
+ var cachedTab = _this2._clients.get(aTabActor);
+ var cachedResponse = {
+ cacheDisabled: cachedTab.cacheDisabled,
+ javascriptEnabled: cachedTab.javascriptEnabled,
+ traits: cachedTab.traits
+ };
+ DevToolsUtils.executeSoon(() => aOnResponse(cachedResponse, cachedTab));
+ return {
+ v: promise.resolve([cachedResponse, cachedTab])
+ };
+ }();
+
+ if (typeof _ret === "object") return _ret.v;
+ }
+
+ var packet = {
+ to: aTabActor,
+ type: "attach"
+ };
+ return this.request(packet).then(aResponse => {
+ var tabClient = void 0;
+ if (!aResponse.error) {
+ tabClient = new TabClient(this, aResponse);
+ this.registerClient(tabClient);
+ }
+ aOnResponse(aResponse, tabClient);
+ return [aResponse, tabClient];
+ });
+ },
+
+ attachWorker: function DC_attachWorker(aWorkerActor) {
+ var aOnResponse = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : noop;
+
+ var workerClient = this._clients.get(aWorkerActor);
+ if (workerClient !== undefined) {
+ var _ret2 = function () {
+ var response = {
+ from: workerClient.actor,
+ type: "attached",
+ url: workerClient.url
+ };
+ DevToolsUtils.executeSoon(() => aOnResponse(response, workerClient));
+ return {
+ v: promise.resolve([response, workerClient])
+ };
+ }();
+
+ if (typeof _ret2 === "object") return _ret2.v;
+ }
+
+ return this.request({ to: aWorkerActor, type: "attach" }).then(aResponse => {
+ if (aResponse.error) {
+ aOnResponse(aResponse, null);
+ return [aResponse, null];
+ }
+
+ var workerClient = new WorkerClient(this, aResponse);
+ this.registerClient(workerClient);
+ aOnResponse(aResponse, workerClient);
+ return [aResponse, workerClient];
+ });
+ },
+
+ /**
+ * Attach to an addon actor.
+ *
+ * @param string aAddonActor
+ * The actor ID for the addon to attach.
+ * @param function aOnResponse
+ * Called with the response packet and a AddonClient
+ * (which will be undefined on error).
+ */
+ attachAddon: function DC_attachAddon(aAddonActor) {
+ var aOnResponse = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : noop;
+
+ var packet = {
+ to: aAddonActor,
+ type: "attach"
+ };
+ return this.request(packet).then(aResponse => {
+ var addonClient = void 0;
+ if (!aResponse.error) {
+ addonClient = new AddonClient(this, aAddonActor);
+ this.registerClient(addonClient);
+ this.activeAddon = addonClient;
+ }
+ aOnResponse(aResponse, addonClient);
+ return [aResponse, addonClient];
+ });
+ },
+
+ /**
+ * Attach to a Web Console actor.
+ *
+ * @param string aConsoleActor
+ * The ID for the console actor to attach to.
+ * @param array aListeners
+ * The console listeners you want to start.
+ * @param function aOnResponse
+ * Called with the response packet and a WebConsoleClient
+ * instance (which will be undefined on error).
+ */
+ attachConsole: function (aConsoleActor, aListeners) {
+ var aOnResponse = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : noop;
+
+ var packet = {
+ to: aConsoleActor,
+ type: "startListeners",
+ listeners: aListeners
+ };
+
+ return this.request(packet).then(aResponse => {
+ var consoleClient = void 0;
+ if (!aResponse.error) {
+ if (this._clients.has(aConsoleActor)) {
+ consoleClient = this._clients.get(aConsoleActor);
+ } else {
+ consoleClient = new WebConsoleClient(this, aResponse);
+ this.registerClient(consoleClient);
+ }
+ }
+ aOnResponse(aResponse, consoleClient);
+ return [aResponse, consoleClient];
+ });
+ },
+
+ /**
+ * Attach to a global-scoped thread actor for chrome debugging.
+ *
+ * @param string aThreadActor
+ * The actor ID for the thread to attach.
+ * @param function aOnResponse
+ * Called with the response packet and a ThreadClient
+ * (which will be undefined on error).
+ * @param object aOptions
+ * Configuration options.
+ * - useSourceMaps: whether to use source maps or not.
+ */
+ attachThread: function (aThreadActor) {
+ var _this3 = this;
+
+ var aOnResponse = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : noop;
+ var aOptions = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
+
+ if (this._clients.has(aThreadActor)) {
+ var _ret3 = function () {
+ var client = _this3._clients.get(aThreadActor);
+ DevToolsUtils.executeSoon(() => aOnResponse({}, client));
+ return {
+ v: promise.resolve([{}, client])
+ };
+ }();
+
+ if (typeof _ret3 === "object") return _ret3.v;
+ }
+
+ var packet = {
+ to: aThreadActor,
+ type: "attach",
+ options: aOptions
+ };
+ return this.request(packet).then(aResponse => {
+ if (!aResponse.error) {
+ var threadClient = new ThreadClient(this, aThreadActor);
+ this.registerClient(threadClient);
+ }
+ aOnResponse(aResponse, threadClient);
+ return [aResponse, threadClient];
+ });
+ },
+
+ /**
+ * Attach to a trace actor.
+ *
+ * @param string aTraceActor
+ * The actor ID for the tracer to attach.
+ * @param function aOnResponse
+ * Called with the response packet and a TraceClient
+ * (which will be undefined on error).
+ */
+ attachTracer: function (aTraceActor) {
+ var _this4 = this;
+
+ var aOnResponse = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : noop;
+
+ if (this._clients.has(aTraceActor)) {
+ var _ret4 = function () {
+ var client = _this4._clients.get(aTraceActor);
+ DevToolsUtils.executeSoon(() => aOnResponse({}, client));
+ return {
+ v: promise.resolve([{}, client])
+ };
+ }();
+
+ if (typeof _ret4 === "object") return _ret4.v;
+ }
+
+ var packet = {
+ to: aTraceActor,
+ type: "attach"
+ };
+ return this.request(packet).then(aResponse => {
+ if (!aResponse.error) {
+ var traceClient = new TraceClient(this, aTraceActor);
+ this.registerClient(traceClient);
+ }
+ aOnResponse(aResponse, traceClient);
+ return [aResponse, traceClient];
+ });
+ },
+
+ /**
+ * Fetch the ChromeActor for the main process or ChildProcessActor for a
+ * a given child process ID.
+ *
+ * @param number aId
+ * The ID for the process to attach (returned by `listProcesses`).
+ * Connected to the main process if omitted, or is 0.
+ */
+ getProcess: function (aId) {
+ var packet = {
+ to: "root",
+ type: "getProcess"
+ };
+ if (typeof aId == "number") {
+ packet.id = aId;
+ }
+ return this.request(packet);
+ },
+
+ /**
+ * Release an object actor.
+ *
+ * @param string aActor
+ * The actor ID to send the request to.
+ * @param aOnResponse function
+ * If specified, will be called with the response packet when
+ * debugging server responds.
+ */
+ release: DebuggerClient.requester({
+ to: args(0),
+ type: "release"
+ }, {
+ telemetry: "RELEASE"
+ }),
+
+ /**
+ * Send a request to the debugging server.
+ *
+ * @param aRequest object
+ * A JSON packet to send to the debugging server.
+ * @param aOnResponse function
+ * If specified, will be called with the JSON response packet when
+ * debugging server responds.
+ * @return Request
+ * This object emits a number of events to allow you to respond to
+ * different parts of the request lifecycle.
+ * It is also a Promise object, with a `then` method, that is resolved
+ * whenever a JSON or a Bulk response is received; and is rejected
+ * if the response is an error.
+ * Note: This return value can be ignored if you are using JSON alone,
+ * because the callback provided in |aOnResponse| will be bound to the
+ * "json-reply" event automatically.
+ *
+ * Events emitted:
+ * * json-reply: The server replied with a JSON packet, which is
+ * passed as event data.
+ * * bulk-reply: The server replied with bulk data, which you can read
+ * using the event data object containing:
+ * * actor: Name of actor that received the packet
+ * * type: Name of actor's method that was called on receipt
+ * * length: Size of the data to be read
+ * * stream: This input stream should only be used directly if you
+ * can ensure that you will read exactly |length| bytes
+ * and will not close the stream when reading is complete
+ * * done: If you use the stream directly (instead of |copyTo|
+ * below), you must signal completion by resolving /
+ * rejecting this deferred. If it's rejected, the
+ * transport will be closed. If an Error is supplied as a
+ * rejection value, it will be logged via |dumpn|. If you
+ * do use |copyTo|, resolving is taken care of for you
+ * when copying completes.
+ * * copyTo: A helper function for getting your data out of the
+ * stream that meets the stream handling requirements
+ * above, and has the following signature:
+ * @param output nsIAsyncOutputStream
+ * The stream to copy to.
+ * @return Promise
+ * The promise is resolved when copying completes or
+ * rejected if any (unexpected) errors occur.
+ * This object also emits "progress" events for each chunk
+ * that is copied. See stream-utils.js.
+ */
+ request: function (aRequest, aOnResponse) {
+ if (!this.mainRoot) {
+ throw Error("Have not yet received a hello packet from the server.");
+ }
+ var type = aRequest.type || "";
+ if (!aRequest.to) {
+ throw Error("'" + type + "' request packet has no destination.");
+ }
+ if (this._closed) {
+ var msg = "'" + type + "' request packet to " + "'" + aRequest.to + "' " + "can't be sent as the connection is closed.";
+ var resp = { error: "connectionClosed", message: msg };
+ if (aOnResponse) {
+ aOnResponse(resp);
+ }
+ return promise.reject(resp);
+ }
+
+ var request = new Request(aRequest);
+ request.format = "json";
+ request.stack = components.stack;
+ if (aOnResponse) {
+ request.on("json-reply", aOnResponse);
+ }
+
+ this._sendOrQueueRequest(request);
+
+ // Implement a Promise like API on the returned object
+ // that resolves/rejects on request response
+ var deferred = promise.defer();
+ function listenerJson(resp) {
+ request.off("json-reply", listenerJson);
+ request.off("bulk-reply", listenerBulk);
+ if (resp.error) {
+ deferred.reject(resp);
+ } else {
+ deferred.resolve(resp);
+ }
+ }
+ function listenerBulk(resp) {
+ request.off("json-reply", listenerJson);
+ request.off("bulk-reply", listenerBulk);
+ deferred.resolve(resp);
+ }
+ request.on("json-reply", listenerJson);
+ request.on("bulk-reply", listenerBulk);
+ request.then = deferred.promise.then.bind(deferred.promise);
+
+ return request;
+ },
+
+ /**
+ * Transmit streaming data via a bulk request.
+ *
+ * This method initiates the bulk send process by queuing up the header data.
+ * The caller receives eventual access to a stream for writing.
+ *
+ * Since this opens up more options for how the server might respond (it could
+ * send back either JSON or bulk data), and the returned Request object emits
+ * events for different stages of the request process that you may want to
+ * react to.
+ *
+ * @param request Object
+ * This is modeled after the format of JSON packets above, but does not
+ * actually contain the data, but is instead just a routing header:
+ * * actor: Name of actor that will receive the packet
+ * * type: Name of actor's method that should be called on receipt
+ * * length: Size of the data to be sent
+ * @return Request
+ * This object emits a number of events to allow you to respond to
+ * different parts of the request lifecycle.
+ *
+ * Events emitted:
+ * * bulk-send-ready: Ready to send bulk data to the server, using the
+ * event data object containing:
+ * * stream: This output stream should only be used directly if
+ * you can ensure that you will write exactly |length|
+ * bytes and will not close the stream when writing is
+ * complete
+ * * done: If you use the stream directly (instead of |copyFrom|
+ * below), you must signal completion by resolving /
+ * rejecting this deferred. If it's rejected, the
+ * transport will be closed. If an Error is supplied as
+ * a rejection value, it will be logged via |dumpn|. If
+ * you do use |copyFrom|, resolving is taken care of for
+ * you when copying completes.
+ * * copyFrom: A helper function for getting your data onto the
+ * stream that meets the stream handling requirements
+ * above, and has the following signature:
+ * @param input nsIAsyncInputStream
+ * The stream to copy from.
+ * @return Promise
+ * The promise is resolved when copying completes or
+ * rejected if any (unexpected) errors occur.
+ * This object also emits "progress" events for each chunk
+ * that is copied. See stream-utils.js.
+ * * json-reply: The server replied with a JSON packet, which is
+ * passed as event data.
+ * * bulk-reply: The server replied with bulk data, which you can read
+ * using the event data object containing:
+ * * actor: Name of actor that received the packet
+ * * type: Name of actor's method that was called on receipt
+ * * length: Size of the data to be read
+ * * stream: This input stream should only be used directly if you
+ * can ensure that you will read exactly |length| bytes
+ * and will not close the stream when reading is complete
+ * * done: If you use the stream directly (instead of |copyTo|
+ * below), you must signal completion by resolving /
+ * rejecting this deferred. If it's rejected, the
+ * transport will be closed. If an Error is supplied as a
+ * rejection value, it will be logged via |dumpn|. If you
+ * do use |copyTo|, resolving is taken care of for you
+ * when copying completes.
+ * * copyTo: A helper function for getting your data out of the
+ * stream that meets the stream handling requirements
+ * above, and has the following signature:
+ * @param output nsIAsyncOutputStream
+ * The stream to copy to.
+ * @return Promise
+ * The promise is resolved when copying completes or
+ * rejected if any (unexpected) errors occur.
+ * This object also emits "progress" events for each chunk
+ * that is copied. See stream-utils.js.
+ */
+ startBulkRequest: function (request) {
+ if (!this.traits.bulk) {
+ throw Error("Server doesn't support bulk transfers");
+ }
+ if (!this.mainRoot) {
+ throw Error("Have not yet received a hello packet from the server.");
+ }
+ if (!request.type) {
+ throw Error("Bulk packet is missing the required 'type' field.");
+ }
+ if (!request.actor) {
+ throw Error("'" + request.type + "' bulk packet has no destination.");
+ }
+ if (!request.length) {
+ throw Error("'" + request.type + "' bulk packet has no length.");
+ }
+
+ request = new Request(request);
+ request.format = "bulk";
+
+ this._sendOrQueueRequest(request);
+
+ return request;
+ },
+
+ /**
+ * If a new request can be sent immediately, do so. Otherwise, queue it.
+ */
+ _sendOrQueueRequest(request) {
+ var actor = request.actor;
+ if (!this._activeRequests.has(actor)) {
+ this._sendRequest(request);
+ } else {
+ this._queueRequest(request);
+ }
+ },
+
+ /**
+ * Send a request.
+ * @throws Error if there is already an active request in flight for the same
+ * actor.
+ */
+ _sendRequest(request) {
+ var actor = request.actor;
+ this.expectReply(actor, request);
+
+ if (request.format === "json") {
+ this._transport.send(request.request);
+ return false;
+ }
+
+ this._transport.startBulkSend(request.request).then(function () {
+ for (var _len3 = arguments.length, args = Array(_len3), _key3 = 0; _key3 < _len3; _key3++) {
+ args[_key3] = arguments[_key3];
+ }
+
+ request.emit.apply(request, ["bulk-send-ready"].concat(args));
+ });
+ },
+
+ /**
+ * Queue a request to be sent later. Queues are only drained when an in
+ * flight request to a given actor completes.
+ */
+ _queueRequest(request) {
+ var actor = request.actor;
+ var queue = this._pendingRequests.get(actor) || [];
+ queue.push(request);
+ this._pendingRequests.set(actor, queue);
+ },
+
+ /**
+ * Attempt the next request to a given actor (if any).
+ */
+ _attemptNextRequest(actor) {
+ if (this._activeRequests.has(actor)) {
+ return;
+ }
+ var queue = this._pendingRequests.get(actor);
+ if (!queue) {
+ return;
+ }
+ var request = queue.shift();
+ if (queue.length === 0) {
+ this._pendingRequests.delete(actor);
+ }
+ this._sendRequest(request);
+ },
+
+ /**
+ * Arrange to hand the next reply from |aActor| to the handler bound to
+ * |aRequest|.
+ *
+ * DebuggerClient.prototype.request / startBulkRequest usually takes care of
+ * establishing the handler for a given request, but in rare cases (well,
+ * greetings from new root actors, is the only case at the moment) we must be
+ * prepared for a "reply" that doesn't correspond to any request we sent.
+ */
+ expectReply: function (aActor, aRequest) {
+ if (this._activeRequests.has(aActor)) {
+ throw Error("clashing handlers for next reply from " + uneval(aActor));
+ }
+
+ // If a handler is passed directly (as it is with the handler for the root
+ // actor greeting), create a dummy request to bind this to.
+ if (typeof aRequest === "function") {
+ var handler = aRequest;
+ aRequest = new Request();
+ aRequest.on("json-reply", handler);
+ }
+
+ this._activeRequests.set(aActor, aRequest);
+ },
+
+ // Transport hooks.
+
+ /**
+ * Called by DebuggerTransport to dispatch incoming packets as appropriate.
+ *
+ * @param aPacket object
+ * The incoming packet.
+ */
+ onPacket: function (aPacket) {
+ if (!aPacket.from) {
+ DevToolsUtils.reportException("onPacket", new Error("Server did not specify an actor, dropping packet: " + JSON.stringify(aPacket)));
+ return;
+ }
+
+ // If we have a registered Front for this actor, let it handle the packet
+ // and skip all the rest of this unpleasantness.
+ var front = this.getActor(aPacket.from);
+ if (front) {
+ front.onPacket(aPacket);
+ return;
+ }
+
+ if (this._clients.has(aPacket.from) && aPacket.type) {
+ var client = this._clients.get(aPacket.from);
+ var type = aPacket.type;
+ if (client.events.indexOf(type) != -1) {
+ client.emit(type, aPacket);
+ // we ignore the rest, as the client is expected to handle this packet.
+ return;
+ }
+ }
+
+ var activeRequest = void 0;
+ // See if we have a handler function waiting for a reply from this
+ // actor. (Don't count unsolicited notifications or pauses as
+ // replies.)
+ if (this._activeRequests.has(aPacket.from) && !(aPacket.type in UnsolicitedNotifications) && !(aPacket.type == ThreadStateTypes.paused && aPacket.why.type in UnsolicitedPauses)) {
+ activeRequest = this._activeRequests.get(aPacket.from);
+ this._activeRequests.delete(aPacket.from);
+ }
+
+ // If there is a subsequent request for the same actor, hand it off to the
+ // transport. Delivery of packets on the other end is always async, even
+ // in the local transport case.
+ this._attemptNextRequest(aPacket.from);
+
+ // Packets that indicate thread state changes get special treatment.
+ if (aPacket.type in ThreadStateTypes && this._clients.has(aPacket.from) && typeof this._clients.get(aPacket.from)._onThreadState == "function") {
+ this._clients.get(aPacket.from)._onThreadState(aPacket);
+ }
+
+ // TODO: Bug 1151156 - Remove once Gecko 40 is on b2g-stable.
+ if (!this.traits.noNeedToFakeResumptionOnNavigation) {
+ // On navigation the server resumes, so the client must resume as well.
+ // We achieve that by generating a fake resumption packet that triggers
+ // the client's thread state change listeners.
+ if (aPacket.type == UnsolicitedNotifications.tabNavigated && this._clients.has(aPacket.from) && this._clients.get(aPacket.from).thread) {
+ var thread = this._clients.get(aPacket.from).thread;
+ var resumption = { from: thread._actor, type: "resumed" };
+ thread._onThreadState(resumption);
+ }
+ }
+
+ // Only try to notify listeners on events, not responses to requests
+ // that lack a packet type.
+ if (aPacket.type) {
+ this.emit(aPacket.type, aPacket);
+ }
+
+ if (activeRequest) {
+ var emitReply = () => activeRequest.emit("json-reply", aPacket);
+ if (activeRequest.stack) {
+ Cu.callFunctionWithAsyncStack(emitReply, activeRequest.stack, "DevTools RDP");
+ } else {
+ emitReply();
+ }
+ }
+ },
+
+ /**
+ * Called by the DebuggerTransport to dispatch incoming bulk packets as
+ * appropriate.
+ *
+ * @param packet object
+ * The incoming packet, which contains:
+ * * actor: Name of actor that will receive the packet
+ * * type: Name of actor's method that should be called on receipt
+ * * length: Size of the data to be read
+ * * stream: This input stream should only be used directly if you can
+ * ensure that you will read exactly |length| bytes and will
+ * not close the stream when reading is complete
+ * * done: If you use the stream directly (instead of |copyTo|
+ * below), you must signal completion by resolving /
+ * rejecting this deferred. If it's rejected, the transport
+ * will be closed. If an Error is supplied as a rejection
+ * value, it will be logged via |dumpn|. If you do use
+ * |copyTo|, resolving is taken care of for you when copying
+ * completes.
+ * * copyTo: A helper function for getting your data out of the stream
+ * that meets the stream handling requirements above, and has
+ * the following signature:
+ * @param output nsIAsyncOutputStream
+ * The stream to copy to.
+ * @return Promise
+ * The promise is resolved when copying completes or rejected
+ * if any (unexpected) errors occur.
+ * This object also emits "progress" events for each chunk
+ * that is copied. See stream-utils.js.
+ */
+ onBulkPacket: function (packet) {
+ var actor = packet.actor;
+ var type = packet.type;
+ var length = packet.length;
+
+
+ if (!actor) {
+ DevToolsUtils.reportException("onBulkPacket", new Error("Server did not specify an actor, dropping bulk packet: " + JSON.stringify(packet)));
+ return;
+ }
+
+ // See if we have a handler function waiting for a reply from this
+ // actor.
+ if (!this._activeRequests.has(actor)) {
+ return;
+ }
+
+ var activeRequest = this._activeRequests.get(actor);
+ this._activeRequests.delete(actor);
+
+ // If there is a subsequent request for the same actor, hand it off to the
+ // transport. Delivery of packets on the other end is always async, even
+ // in the local transport case.
+ this._attemptNextRequest(actor);
+
+ activeRequest.emit("bulk-reply", packet);
+ },
+
+ /**
+ * Called by DebuggerTransport when the underlying stream is closed.
+ *
+ * @param aStatus nsresult
+ * The status code that corresponds to the reason for closing
+ * the stream.
+ */
+ onClosed: function (aStatus) {
+ this._closed = true;
+ this.emit("closed");
+
+ // Reject all pending and active requests
+ var reject = function (type, request, actor) {
+ // Server can send packets on its own and client only pass a callback
+ // to expectReply, so that there is no request object.
+ var msg = void 0;
+ if (request.request) {
+ msg = "'" + request.request.type + "' " + type + " request packet" + " to '" + actor + "' " + "can't be sent as the connection just closed.";
+ } else {
+ msg = "server side packet from '" + actor + "' can't be received " + "as the connection just closed.";
+ }
+ var packet = { error: "connectionClosed", message: msg };
+ request.emit("json-reply", packet);
+ };
+
+ var pendingRequests = new Map(this._pendingRequests);
+ this._pendingRequests.clear();
+ pendingRequests.forEach((list, actor) => {
+ list.forEach(request => reject("pending", request, actor));
+ });
+ var activeRequests = new Map(this._activeRequests);
+ this._activeRequests.clear();
+ activeRequests.forEach(reject.bind(null, "active"));
+
+ // The |_pools| array on the client-side currently is used only by
+ // protocol.js to store active fronts, mirroring the actor pools found in
+ // the server. So, read all usages of "pool" as "protocol.js front".
+ //
+ // In the normal case where we shutdown cleanly, the toolbox tells each tool
+ // to close, and they each call |destroy| on any fronts they were using.
+ // When |destroy| or |cleanup| is called on a protocol.js front, it also
+ // removes itself from the |_pools| array. Once the toolbox has shutdown,
+ // the connection is closed, and we reach here. All fronts (should have
+ // been) |destroy|ed, so |_pools| should empty.
+ //
+ // If the connection instead aborts unexpectedly, we may end up here with
+ // all fronts used during the life of the connection. So, we call |cleanup|
+ // on them clear their state, reject pending requests, and remove themselves
+ // from |_pools|. This saves the toolbox from hanging indefinitely, in case
+ // it waits for some server response before shutdown that will now never
+ // arrive.
+ for (var pool of this._pools) {
+ pool.cleanup();
+ }
+ },
+
+ registerClient: function (client) {
+ var actorID = client.actor;
+ if (!actorID) {
+ throw new Error("DebuggerServer.registerClient expects " + "a client instance with an `actor` attribute.");
+ }
+ if (!Array.isArray(client.events)) {
+ throw new Error("DebuggerServer.registerClient expects " + "a client instance with an `events` attribute " + "that is an array.");
+ }
+ if (client.events.length > 0 && typeof client.emit != "function") {
+ throw new Error("DebuggerServer.registerClient expects " + "a client instance with non-empty `events` array to" + "have an `emit` function.");
+ }
+ if (this._clients.has(actorID)) {
+ throw new Error("DebuggerServer.registerClient already registered " + "a client for this actor.");
+ }
+ this._clients.set(actorID, client);
+ },
+
+ unregisterClient: function (client) {
+ var actorID = client.actor;
+ if (!actorID) {
+ throw new Error("DebuggerServer.unregisterClient expects " + "a Client instance with a `actor` attribute.");
+ }
+ this._clients.delete(actorID);
+ },
+
+ /**
+ * Actor lifetime management, echos the server's actor pools.
+ */
+ __pools: null,
+ get _pools() {
+ if (this.__pools) {
+ return this.__pools;
+ }
+ this.__pools = new Set();
+ return this.__pools;
+ },
+
+ addActorPool: function (pool) {
+ this._pools.add(pool);
+ },
+ removeActorPool: function (pool) {
+ this._pools.delete(pool);
+ },
+ getActor: function (actorID) {
+ var pool = this.poolFor(actorID);
+ return pool ? pool.get(actorID) : null;
+ },
+
+ poolFor: function (actorID) {
+ for (var pool of this._pools) {
+ if (pool.has(actorID)) return pool;
+ }
+ return null;
+ },
+
+ /**
+ * Currently attached addon.
+ */
+ activeAddon: null
+ };
+
+ eventSource(DebuggerClient.prototype);
+
+ function Request(request) {
+ this.request = request;
+ }
+
+ Request.prototype = {
+
+ on: function (type, listener) {
+ events.on(this, type, listener);
+ },
+
+ off: function (type, listener) {
+ events.off(this, type, listener);
+ },
+
+ once: function (type, listener) {
+ events.once(this, type, listener);
+ },
+
+ emit: function (type) {
+ for (var _len4 = arguments.length, args = Array(_len4 > 1 ? _len4 - 1 : 0), _key4 = 1; _key4 < _len4; _key4++) {
+ args[_key4 - 1] = arguments[_key4];
+ }
+
+ events.emit.apply(events, [this, type].concat(args));
+ },
+
+ get actor() {
+ return this.request.to || this.request.actor;
+ }
+
+ };
+
+ /**
+ * Creates a tab client for the remote debugging protocol server. This client
+ * is a front to the tab actor created in the server side, hiding the protocol
+ * details in a traditional JavaScript API.
+ *
+ * @param aClient DebuggerClient
+ * The debugger client parent.
+ * @param aForm object
+ * The protocol form for this tab.
+ */
+ function TabClient(aClient, aForm) {
+ this.client = aClient;
+ this._actor = aForm.from;
+ this._threadActor = aForm.threadActor;
+ this.javascriptEnabled = aForm.javascriptEnabled;
+ this.cacheDisabled = aForm.cacheDisabled;
+ this.thread = null;
+ this.request = this.client.request;
+ this.traits = aForm.traits || {};
+ this.events = ["workerListChanged"];
+ }
+
+ TabClient.prototype = {
+ get actor() {
+ return this._actor;
+ },
+ get _transport() {
+ return this.client._transport;
+ },
+
+ /**
+ * Attach to a thread actor.
+ *
+ * @param object aOptions
+ * Configuration options.
+ * - useSourceMaps: whether to use source maps or not.
+ * @param function aOnResponse
+ * Called with the response packet and a ThreadClient
+ * (which will be undefined on error).
+ */
+ attachThread: function () {
+ var aOptions = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
+ var aOnResponse = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : noop;
+
+ if (this.thread) {
+ DevToolsUtils.executeSoon(() => aOnResponse({}, this.thread));
+ return promise.resolve([{}, this.thread]);
+ }
+
+ var packet = {
+ to: this._threadActor,
+ type: "attach",
+ options: aOptions
+ };
+ return this.request(packet).then(aResponse => {
+ if (!aResponse.error) {
+ this.thread = new ThreadClient(this, this._threadActor);
+ this.client.registerClient(this.thread);
+ }
+ aOnResponse(aResponse, this.thread);
+ return [aResponse, this.thread];
+ });
+ },
+
+ /**
+ * Detach the client from the tab actor.
+ *
+ * @param function aOnResponse
+ * Called with the response packet.
+ */
+ detach: DebuggerClient.requester({
+ type: "detach"
+ }, {
+ before: function (aPacket) {
+ if (this.thread) {
+ this.thread.detach();
+ }
+ return aPacket;
+ },
+ after: function (aResponse) {
+ this.client.unregisterClient(this);
+ return aResponse;
+ },
+ telemetry: "TABDETACH"
+ }),
+
+ /**
+ * Bring the window to the front.
+ */
+ focus: DebuggerClient.requester({
+ type: "focus"
+ }, {}),
+
+ /**
+ * Reload the page in this tab.
+ *
+ * @param [optional] object options
+ * An object with a `force` property indicating whether or not
+ * this reload should skip the cache
+ */
+ reload: function () {
+ var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : { force: false };
+
+ return this._reload(options);
+ },
+ _reload: DebuggerClient.requester({
+ type: "reload",
+ options: args(0)
+ }, {
+ telemetry: "RELOAD"
+ }),
+
+ /**
+ * Navigate to another URL.
+ *
+ * @param string url
+ * The URL to navigate to.
+ */
+ navigateTo: DebuggerClient.requester({
+ type: "navigateTo",
+ url: args(0)
+ }, {
+ telemetry: "NAVIGATETO"
+ }),
+
+ /**
+ * Reconfigure the tab actor.
+ *
+ * @param object aOptions
+ * A dictionary object of the new options to use in the tab actor.
+ * @param function aOnResponse
+ * Called with the response packet.
+ */
+ reconfigure: DebuggerClient.requester({
+ type: "reconfigure",
+ options: args(0)
+ }, {
+ telemetry: "RECONFIGURETAB"
+ }),
+
+ listWorkers: DebuggerClient.requester({
+ type: "listWorkers"
+ }, {
+ telemetry: "LISTWORKERS"
+ }),
+
+ attachWorker: function (aWorkerActor, aOnResponse) {
+ this.client.attachWorker(aWorkerActor, aOnResponse);
+ },
+
+ /**
+ * Resolve a location ({ url, line, column }) to its current
+ * source mapping location.
+ *
+ * @param {String} arg[0].url
+ * @param {Number} arg[0].line
+ * @param {Number?} arg[0].column
+ */
+ resolveLocation: DebuggerClient.requester({
+ type: "resolveLocation",
+ location: args(0)
+ })
+ };
+
+ eventSource(TabClient.prototype);
+
+ function WorkerClient(aClient, aForm) {
+ this.client = aClient;
+ this._actor = aForm.from;
+ this._isClosed = false;
+ this._url = aForm.url;
+
+ this._onClose = this._onClose.bind(this);
+
+ this.addListener("close", this._onClose);
+
+ this.traits = {};
+ }
+
+ WorkerClient.prototype = {
+ get _transport() {
+ return this.client._transport;
+ },
+
+ get request() {
+ return this.client.request;
+ },
+
+ get actor() {
+ return this._actor;
+ },
+
+ get url() {
+ return this._url;
+ },
+
+ get isClosed() {
+ return this._isClosed;
+ },
+
+ detach: DebuggerClient.requester({ type: "detach" }, {
+ after: function (aResponse) {
+ if (this.thread) {
+ this.client.unregisterClient(this.thread);
+ }
+ this.client.unregisterClient(this);
+ return aResponse;
+ },
+
+ telemetry: "WORKERDETACH"
+ }),
+
+ attachThread: function () {
+ var _this5 = this;
+
+ var aOptions = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
+ var aOnResponse = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : noop;
+
+ if (this.thread) {
+ var _ret5 = function () {
+ var response = [{
+ type: "connected",
+ threadActor: _this5.thread._actor,
+ consoleActor: _this5.consoleActor
+ }, _this5.thread];
+ DevToolsUtils.executeSoon(() => aOnResponse(response));
+ return {
+ v: response
+ };
+ }();
+
+ if (typeof _ret5 === "object") return _ret5.v;
+ }
+
+ // The connect call on server doesn't attach the thread as of version 44.
+ return this.request({
+ to: this._actor,
+ type: "connect",
+ options: aOptions
+ }).then(connectReponse => {
+ if (connectReponse.error) {
+ aOnResponse(connectReponse, null);
+ return [connectResponse, null];
+ }
+
+ return this.request({
+ to: connectReponse.threadActor,
+ type: "attach",
+ options: aOptions
+ }).then(attachResponse => {
+ if (attachResponse.error) {
+ aOnResponse(attachResponse, null);
+ }
+
+ this.thread = new ThreadClient(this, connectReponse.threadActor);
+ this.consoleActor = connectReponse.consoleActor;
+ this.client.registerClient(this.thread);
+
+ aOnResponse(connectReponse, this.thread);
+ return [connectResponse, this.thread];
+ });
+ });
+ },
+
+ _onClose: function () {
+ this.removeListener("close", this._onClose);
+
+ if (this.thread) {
+ this.client.unregisterClient(this.thread);
+ }
+ this.client.unregisterClient(this);
+ this._isClosed = true;
+ },
+
+ reconfigure: function () {
+ return Promise.resolve();
+ },
+
+ events: ["close"]
+ };
+
+ eventSource(WorkerClient.prototype);
+
+ function AddonClient(aClient, aActor) {
+ this._client = aClient;
+ this._actor = aActor;
+ this.request = this._client.request;
+ this.events = [];
+ }
+
+ AddonClient.prototype = {
+ get actor() {
+ return this._actor;
+ },
+ get _transport() {
+ return this._client._transport;
+ },
+
+ /**
+ * Detach the client from the addon actor.
+ *
+ * @param function aOnResponse
+ * Called with the response packet.
+ */
+ detach: DebuggerClient.requester({
+ type: "detach"
+ }, {
+ after: function (aResponse) {
+ if (this._client.activeAddon === this) {
+ this._client.activeAddon = null;
+ }
+ this._client.unregisterClient(this);
+ return aResponse;
+ },
+ telemetry: "ADDONDETACH"
+ })
+ };
+
+ /**
+ * A RootClient object represents a root actor on the server. Each
+ * DebuggerClient keeps a RootClient instance representing the root actor
+ * for the initial connection; DebuggerClient's 'listTabs' and
+ * 'listChildProcesses' methods forward to that root actor.
+ *
+ * @param aClient object
+ * The client connection to which this actor belongs.
+ * @param aGreeting string
+ * The greeting packet from the root actor we're to represent.
+ *
+ * Properties of a RootClient instance:
+ *
+ * @property actor string
+ * The name of this child's root actor.
+ * @property applicationType string
+ * The application type, as given in the root actor's greeting packet.
+ * @property traits object
+ * The traits object, as given in the root actor's greeting packet.
+ */
+ function RootClient(aClient, aGreeting) {
+ this._client = aClient;
+ this.actor = aGreeting.from;
+ this.applicationType = aGreeting.applicationType;
+ this.traits = aGreeting.traits;
+ }
+ exports.RootClient = RootClient;
+
+ RootClient.prototype = {
+ constructor: RootClient,
+
+ /**
+ * List the open tabs.
+ *
+ * @param function aOnResponse
+ * Called with the response packet.
+ */
+ listTabs: DebuggerClient.requester({ type: "listTabs" }, { telemetry: "LISTTABS" }),
+
+ /**
+ * List the installed addons.
+ *
+ * @param function aOnResponse
+ * Called with the response packet.
+ */
+ listAddons: DebuggerClient.requester({ type: "listAddons" }, { telemetry: "LISTADDONS" }),
+
+ /**
+ * List the registered workers.
+ *
+ * @param function aOnResponse
+ * Called with the response packet.
+ */
+ listWorkers: DebuggerClient.requester({ type: "listWorkers" }, { telemetry: "LISTWORKERS" }),
+
+ /**
+ * List the registered service workers.
+ *
+ * @param function aOnResponse
+ * Called with the response packet.
+ */
+ listServiceWorkerRegistrations: DebuggerClient.requester({ type: "listServiceWorkerRegistrations" }, { telemetry: "LISTSERVICEWORKERREGISTRATIONS" }),
+
+ /**
+ * List the running processes.
+ *
+ * @param function aOnResponse
+ * Called with the response packet.
+ */
+ listProcesses: DebuggerClient.requester({ type: "listProcesses" }, { telemetry: "LISTPROCESSES" }),
+
+ /**
+ * Fetch the TabActor for the currently selected tab, or for a specific
+ * tab given as first parameter.
+ *
+ * @param [optional] object aFilter
+ * A dictionary object with following optional attributes:
+ * - outerWindowID: used to match tabs in parent process
+ * - tabId: used to match tabs in child processes
+ * - tab: a reference to xul:tab element
+ * If nothing is specified, returns the actor for the currently
+ * selected tab.
+ */
+ getTab: function (aFilter) {
+ var packet = {
+ to: this.actor,
+ type: "getTab"
+ };
+
+ if (aFilter) {
+ if (typeof aFilter.outerWindowID == "number") {
+ packet.outerWindowID = aFilter.outerWindowID;
+ } else if (typeof aFilter.tabId == "number") {
+ packet.tabId = aFilter.tabId;
+ } else if ("tab" in aFilter) {
+ var browser = aFilter.tab.linkedBrowser;
+ if (browser.frameLoader.tabParent) {
+ // Tabs in child process
+ packet.tabId = browser.frameLoader.tabParent.tabId;
+ } else {
+ // Tabs in parent process
+ var windowUtils = browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+ packet.outerWindowID = windowUtils.outerWindowID;
+ }
+ } else {
+ // Throw if a filter object have been passed but without
+ // any clearly idenfified filter.
+ throw new Error("Unsupported argument given to getTab request");
+ }
+ }
+
+ return this.request(packet);
+ },
+
+ /**
+ * Description of protocol's actors and methods.
+ *
+ * @param function aOnResponse
+ * Called with the response packet.
+ */
+ protocolDescription: DebuggerClient.requester({ type: "protocolDescription" }, { telemetry: "PROTOCOLDESCRIPTION" }),
+
+ /*
+ * Methods constructed by DebuggerClient.requester require these forwards
+ * on their 'this'.
+ */
+ get _transport() {
+ return this._client._transport;
+ },
+ get request() {
+ return this._client.request;
+ }
+ };
+
+ /**
+ * Creates a thread client for the remote debugging protocol server. This client
+ * is a front to the thread actor created in the server side, hiding the
+ * protocol details in a traditional JavaScript API.
+ *
+ * @param aClient DebuggerClient|TabClient
+ * The parent of the thread (tab for tab-scoped debuggers, DebuggerClient
+ * for chrome debuggers).
+ * @param aActor string
+ * The actor ID for this thread.
+ */
+ function ThreadClient(aClient, aActor) {
+ this._parent = aClient;
+ this.client = aClient instanceof DebuggerClient ? aClient : aClient.client;
+ this._actor = aActor;
+ this._frameCache = [];
+ this._scriptCache = {};
+ this._pauseGrips = {};
+ this._threadGrips = {};
+ this.request = this.client.request;
+ }
+
+ ThreadClient.prototype = {
+ _state: "paused",
+ get state() {
+ return this._state;
+ },
+ get paused() {
+ return this._state === "paused";
+ },
+
+ _pauseOnExceptions: false,
+ _ignoreCaughtExceptions: false,
+ _pauseOnDOMEvents: null,
+
+ _actor: null,
+ get actor() {
+ return this._actor;
+ },
+
+ get _transport() {
+ return this.client._transport;
+ },
+
+ _assertPaused: function (aCommand) {
+ if (!this.paused) {
+ throw Error(aCommand + " command sent while not paused. Currently " + this._state);
+ }
+ },
+
+ /**
+ * Resume a paused thread. If the optional aLimit parameter is present, then
+ * the thread will also pause when that limit is reached.
+ *
+ * @param [optional] object aLimit
+ * An object with a type property set to the appropriate limit (next,
+ * step, or finish) per the remote debugging protocol specification.
+ * Use null to specify no limit.
+ * @param function aOnResponse
+ * Called with the response packet.
+ */
+ _doResume: DebuggerClient.requester({
+ type: "resume",
+ resumeLimit: args(0)
+ }, {
+ before: function (aPacket) {
+ this._assertPaused("resume");
+
+ // Put the client in a tentative "resuming" state so we can prevent
+ // further requests that should only be sent in the paused state.
+ this._state = "resuming";
+
+ if (this._pauseOnExceptions) {
+ aPacket.pauseOnExceptions = this._pauseOnExceptions;
+ }
+ if (this._ignoreCaughtExceptions) {
+ aPacket.ignoreCaughtExceptions = this._ignoreCaughtExceptions;
+ }
+ if (this._pauseOnDOMEvents) {
+ aPacket.pauseOnDOMEvents = this._pauseOnDOMEvents;
+ }
+ return aPacket;
+ },
+ after: function (aResponse) {
+ if (aResponse.error) {
+ // There was an error resuming, back to paused state.
+ this._state = "paused";
+ }
+ return aResponse;
+ },
+ telemetry: "RESUME"
+ }),
+
+ /**
+ * Reconfigure the thread actor.
+ *
+ * @param object aOptions
+ * A dictionary object of the new options to use in the thread actor.
+ * @param function aOnResponse
+ * Called with the response packet.
+ */
+ reconfigure: DebuggerClient.requester({
+ type: "reconfigure",
+ options: args(0)
+ }, {
+ telemetry: "RECONFIGURETHREAD"
+ }),
+
+ /**
+ * Resume a paused thread.
+ */
+ resume: function (aOnResponse) {
+ return this._doResume(null, aOnResponse);
+ },
+
+ /**
+ * Resume then pause without stepping.
+ *
+ * @param function aOnResponse
+ * Called with the response packet.
+ */
+ resumeThenPause: function (aOnResponse) {
+ return this._doResume({ type: "break" }, aOnResponse);
+ },
+
+ /**
+ * Step over a function call.
+ *
+ * @param function aOnResponse
+ * Called with the response packet.
+ */
+ stepOver: function (aOnResponse) {
+ return this._doResume({ type: "next" }, aOnResponse);
+ },
+
+ /**
+ * Step into a function call.
+ *
+ * @param function aOnResponse
+ * Called with the response packet.
+ */
+ stepIn: function (aOnResponse) {
+ return this._doResume({ type: "step" }, aOnResponse);
+ },
+
+ /**
+ * Step out of a function call.
+ *
+ * @param function aOnResponse
+ * Called with the response packet.
+ */
+ stepOut: function (aOnResponse) {
+ return this._doResume({ type: "finish" }, aOnResponse);
+ },
+
+ /**
+ * Immediately interrupt a running thread.
+ *
+ * @param function aOnResponse
+ * Called with the response packet.
+ */
+ interrupt: function (aOnResponse) {
+ return this._doInterrupt(null, aOnResponse);
+ },
+
+ /**
+ * Pause execution right before the next JavaScript bytecode is executed.
+ *
+ * @param function aOnResponse
+ * Called with the response packet.
+ */
+ breakOnNext: function (aOnResponse) {
+ return this._doInterrupt("onNext", aOnResponse);
+ },
+
+ /**
+ * Interrupt a running thread.
+ *
+ * @param function aOnResponse
+ * Called with the response packet.
+ */
+ _doInterrupt: DebuggerClient.requester({
+ type: "interrupt",
+ when: args(0)
+ }, {
+ telemetry: "INTERRUPT"
+ }),
+
+ /**
+ * Enable or disable pausing when an exception is thrown.
+ *
+ * @param boolean aFlag
+ * Enables pausing if true, disables otherwise.
+ * @param function aOnResponse
+ * Called with the response packet.
+ */
+ pauseOnExceptions: function (aPauseOnExceptions, aIgnoreCaughtExceptions) {
+ var aOnResponse = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : noop;
+
+ this._pauseOnExceptions = aPauseOnExceptions;
+ this._ignoreCaughtExceptions = aIgnoreCaughtExceptions;
+
+ // Otherwise send the flag using a standard resume request.
+ if (!this.paused) {
+ return this.interrupt(aResponse => {
+ if (aResponse.error) {
+ // Can't continue if pausing failed.
+ aOnResponse(aResponse);
+ return aResponse;
+ }
+ return this.resume(aOnResponse);
+ });
+ }
+
+ aOnResponse();
+ return promise.resolve();
+ },
+
+ /**
+ * Enable pausing when the specified DOM events are triggered. Disabling
+ * pausing on an event can be realized by calling this method with the updated
+ * array of events that doesn't contain it.
+ *
+ * @param array|string events
+ * An array of strings, representing the DOM event types to pause on,
+ * or "*" to pause on all DOM events. Pass an empty array to
+ * completely disable pausing on DOM events.
+ * @param function onResponse
+ * Called with the response packet in a future turn of the event loop.
+ */
+ pauseOnDOMEvents: function (events) {
+ var onResponse = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : noop;
+
+ this._pauseOnDOMEvents = events;
+ // If the debuggee is paused, the value of the array will be communicated in
+ // the next resumption. Otherwise we have to force a pause in order to send
+ // the array.
+ if (this.paused) {
+ DevToolsUtils.executeSoon(() => onResponse({}));
+ return {};
+ }
+ return this.interrupt(response => {
+ // Can't continue if pausing failed.
+ if (response.error) {
+ onResponse(response);
+ return response;
+ }
+ return this.resume(onResponse);
+ });
+ },
+
+ /**
+ * Send a clientEvaluate packet to the debuggee. Response
+ * will be a resume packet.
+ *
+ * @param string aFrame
+ * The actor ID of the frame where the evaluation should take place.
+ * @param string aExpression
+ * The expression that will be evaluated in the scope of the frame
+ * above.
+ * @param function aOnResponse
+ * Called with the response packet.
+ */
+ eval: DebuggerClient.requester({
+ type: "clientEvaluate",
+ frame: args(0),
+ expression: args(1)
+ }, {
+ before: function (aPacket) {
+ this._assertPaused("eval");
+ // Put the client in a tentative "resuming" state so we can prevent
+ // further requests that should only be sent in the paused state.
+ this._state = "resuming";
+ return aPacket;
+ },
+ after: function (aResponse) {
+ if (aResponse.error) {
+ // There was an error resuming, back to paused state.
+ this._state = "paused";
+ }
+ return aResponse;
+ },
+ telemetry: "CLIENTEVALUATE"
+ }),
+
+ /**
+ * Detach from the thread actor.
+ *
+ * @param function aOnResponse
+ * Called with the response packet.
+ */
+ detach: DebuggerClient.requester({
+ type: "detach"
+ }, {
+ after: function (aResponse) {
+ this.client.unregisterClient(this);
+ this._parent.thread = null;
+ return aResponse;
+ },
+ telemetry: "THREADDETACH"
+ }),
+
+ /**
+ * Release multiple thread-lifetime object actors. If any pause-lifetime
+ * actors are included in the request, a |notReleasable| error will return,
+ * but all the thread-lifetime ones will have been released.
+ *
+ * @param array actors
+ * An array with actor IDs to release.
+ */
+ releaseMany: DebuggerClient.requester({
+ type: "releaseMany",
+ actors: args(0)
+ }, {
+ telemetry: "RELEASEMANY"
+ }),
+
+ /**
+ * Promote multiple pause-lifetime object actors to thread-lifetime ones.
+ *
+ * @param array actors
+ * An array with actor IDs to promote.
+ */
+ threadGrips: DebuggerClient.requester({
+ type: "threadGrips",
+ actors: args(0)
+ }, {
+ telemetry: "THREADGRIPS"
+ }),
+
+ /**
+ * Return the event listeners defined on the page.
+ *
+ * @param aOnResponse Function
+ * Called with the thread's response.
+ */
+ eventListeners: DebuggerClient.requester({
+ type: "eventListeners"
+ }, {
+ telemetry: "EVENTLISTENERS"
+ }),
+
+ /**
+ * Request the loaded sources for the current thread.
+ *
+ * @param aOnResponse Function
+ * Called with the thread's response.
+ */
+ getSources: DebuggerClient.requester({
+ type: "sources"
+ }, {
+ telemetry: "SOURCES"
+ }),
+
+ /**
+ * Clear the thread's source script cache. A scriptscleared event
+ * will be sent.
+ */
+ _clearScripts: function () {
+ if (Object.keys(this._scriptCache).length > 0) {
+ this._scriptCache = {};
+ this.emit("scriptscleared");
+ }
+ },
+
+ /**
+ * Request frames from the callstack for the current thread.
+ *
+ * @param aStart integer
+ * The number of the youngest stack frame to return (the youngest
+ * frame is 0).
+ * @param aCount integer
+ * The maximum number of frames to return, or null to return all
+ * frames.
+ * @param aOnResponse function
+ * Called with the thread's response.
+ */
+ getFrames: DebuggerClient.requester({
+ type: "frames",
+ start: args(0),
+ count: args(1)
+ }, {
+ telemetry: "FRAMES"
+ }),
+
+ /**
+ * An array of cached frames. Clients can observe the framesadded and
+ * framescleared event to keep up to date on changes to this cache,
+ * and can fill it using the fillFrames method.
+ */
+ get cachedFrames() {
+ return this._frameCache;
+ },
+
+ /**
+ * true if there are more stack frames available on the server.
+ */
+ get moreFrames() {
+ return this.paused && (!this._frameCache || this._frameCache.length == 0 || !this._frameCache[this._frameCache.length - 1].oldest);
+ },
+
+ /**
+ * Ensure that at least aTotal stack frames have been loaded in the
+ * ThreadClient's stack frame cache. A framesadded event will be
+ * sent when the stack frame cache is updated.
+ *
+ * @param aTotal number
+ * The minimum number of stack frames to be included.
+ * @param aCallback function
+ * Optional callback function called when frames have been loaded
+ * @returns true if a framesadded notification should be expected.
+ */
+ fillFrames: function (aTotal) {
+ var aCallback = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : noop;
+
+ this._assertPaused("fillFrames");
+ if (this._frameCache.length >= aTotal) {
+ return false;
+ }
+
+ var numFrames = this._frameCache.length;
+
+ this.getFrames(numFrames, aTotal - numFrames, aResponse => {
+ if (aResponse.error) {
+ aCallback(aResponse);
+ return;
+ }
+
+ var threadGrips = DevToolsUtils.values(this._threadGrips);
+
+ for (var i in aResponse.frames) {
+ var frame = aResponse.frames[i];
+ if (!frame.where.source) {
+ // Older servers use urls instead, so we need to resolve
+ // them to source actors
+ for (var grip of threadGrips) {
+ if (grip instanceof SourceClient && grip.url === frame.url) {
+ frame.where.source = grip._form;
+ }
+ }
+ }
+
+ this._frameCache[frame.depth] = frame;
+ }
+
+ // If we got as many frames as we asked for, there might be more
+ // frames available.
+ this.emit("framesadded");
+
+ aCallback(aResponse);
+ });
+
+ return true;
+ },
+
+ /**
+ * Clear the thread's stack frame cache. A framescleared event
+ * will be sent.
+ */
+ _clearFrames: function () {
+ if (this._frameCache.length > 0) {
+ this._frameCache = [];
+ this.emit("framescleared");
+ }
+ },
+
+ /**
+ * Return a ObjectClient object for the given object grip.
+ *
+ * @param aGrip object
+ * A pause-lifetime object grip returned by the protocol.
+ */
+ pauseGrip: function (aGrip) {
+ if (aGrip.actor in this._pauseGrips) {
+ return this._pauseGrips[aGrip.actor];
+ }
+
+ var client = new ObjectClient(this.client, aGrip);
+ this._pauseGrips[aGrip.actor] = client;
+ return client;
+ },
+
+ /**
+ * Get or create a long string client, checking the grip client cache if it
+ * already exists.
+ *
+ * @param aGrip Object
+ * The long string grip returned by the protocol.
+ * @param aGripCacheName String
+ * The property name of the grip client cache to check for existing
+ * clients in.
+ */
+ _longString: function (aGrip, aGripCacheName) {
+ if (aGrip.actor in this[aGripCacheName]) {
+ return this[aGripCacheName][aGrip.actor];
+ }
+
+ var client = new LongStringClient(this.client, aGrip);
+ this[aGripCacheName][aGrip.actor] = client;
+ return client;
+ },
+
+ /**
+ * Return an instance of LongStringClient for the given long string grip that
+ * is scoped to the current pause.
+ *
+ * @param aGrip Object
+ * The long string grip returned by the protocol.
+ */
+ pauseLongString: function (aGrip) {
+ return this._longString(aGrip, "_pauseGrips");
+ },
+
+ /**
+ * Return an instance of LongStringClient for the given long string grip that
+ * is scoped to the thread lifetime.
+ *
+ * @param aGrip Object
+ * The long string grip returned by the protocol.
+ */
+ threadLongString: function (aGrip) {
+ return this._longString(aGrip, "_threadGrips");
+ },
+
+ /**
+ * Clear and invalidate all the grip clients from the given cache.
+ *
+ * @param aGripCacheName
+ * The property name of the grip cache we want to clear.
+ */
+ _clearObjectClients: function (aGripCacheName) {
+ for (var id in this[aGripCacheName]) {
+ this[aGripCacheName][id].valid = false;
+ }
+ this[aGripCacheName] = {};
+ },
+
+ /**
+ * Invalidate pause-lifetime grip clients and clear the list of current grip
+ * clients.
+ */
+ _clearPauseGrips: function () {
+ this._clearObjectClients("_pauseGrips");
+ },
+
+ /**
+ * Invalidate thread-lifetime grip clients and clear the list of current grip
+ * clients.
+ */
+ _clearThreadGrips: function () {
+ this._clearObjectClients("_threadGrips");
+ },
+
+ /**
+ * Handle thread state change by doing necessary cleanup and notifying all
+ * registered listeners.
+ */
+ _onThreadState: function (aPacket) {
+ this._state = ThreadStateTypes[aPacket.type];
+ // The debugger UI may not be initialized yet so we want to keep
+ // the packet around so it knows what to pause state to display
+ // when it's initialized
+ this._lastPausePacket = aPacket.type === "resumed" ? null : aPacket;
+ this._clearFrames();
+ this._clearPauseGrips();
+ aPacket.type === ThreadStateTypes.detached && this._clearThreadGrips();
+ this.client._eventsEnabled && this.emit(aPacket.type, aPacket);
+ },
+
+ getLastPausePacket: function () {
+ return this._lastPausePacket;
+ },
+
+ /**
+ * Return an EnvironmentClient instance for the given environment actor form.
+ */
+ environment: function (aForm) {
+ return new EnvironmentClient(this.client, aForm);
+ },
+
+ /**
+ * Return an instance of SourceClient for the given source actor form.
+ */
+ source: function (aForm) {
+ if (aForm.actor in this._threadGrips) {
+ return this._threadGrips[aForm.actor];
+ }
+
+ return this._threadGrips[aForm.actor] = new SourceClient(this, aForm);
+ },
+
+ /**
+ * Request the prototype and own properties of mutlipleObjects.
+ *
+ * @param aOnResponse function
+ * Called with the request's response.
+ * @param actors [string]
+ * List of actor ID of the queried objects.
+ */
+ getPrototypesAndProperties: DebuggerClient.requester({
+ type: "prototypesAndProperties",
+ actors: args(0)
+ }, {
+ telemetry: "PROTOTYPESANDPROPERTIES"
+ }),
+
+ events: ["newSource"]
+ };
+
+ eventSource(ThreadClient.prototype);
+
+ /**
+ * Creates a tracing profiler client for the remote debugging protocol
+ * server. This client is a front to the trace actor created on the
+ * server side, hiding the protocol details in a traditional
+ * JavaScript API.
+ *
+ * @param aClient DebuggerClient
+ * The debugger client parent.
+ * @param aActor string
+ * The actor ID for this thread.
+ */
+ function TraceClient(aClient, aActor) {
+ this._client = aClient;
+ this._actor = aActor;
+ this._activeTraces = new Set();
+ this._waitingPackets = new Map();
+ this._expectedPacket = 0;
+ this.request = this._client.request;
+ this.events = [];
+ }
+
+ TraceClient.prototype = {
+ get actor() {
+ return this._actor;
+ },
+ get tracing() {
+ return this._activeTraces.size > 0;
+ },
+
+ get _transport() {
+ return this._client._transport;
+ },
+
+ /**
+ * Detach from the trace actor.
+ */
+ detach: DebuggerClient.requester({
+ type: "detach"
+ }, {
+ after: function (aResponse) {
+ this._client.unregisterClient(this);
+ return aResponse;
+ },
+ telemetry: "TRACERDETACH"
+ }),
+
+ /**
+ * Start a new trace.
+ *
+ * @param aTrace [string]
+ * An array of trace types to be recorded by the new trace.
+ *
+ * @param aName string
+ * The name of the new trace.
+ *
+ * @param aOnResponse function
+ * Called with the request's response.
+ */
+ startTrace: DebuggerClient.requester({
+ type: "startTrace",
+ name: args(1),
+ trace: args(0)
+ }, {
+ after: function (aResponse) {
+ if (aResponse.error) {
+ return aResponse;
+ }
+
+ if (!this.tracing) {
+ this._waitingPackets.clear();
+ this._expectedPacket = 0;
+ }
+ this._activeTraces.add(aResponse.name);
+
+ return aResponse;
+ },
+ telemetry: "STARTTRACE"
+ }),
+
+ /**
+ * End a trace. If a name is provided, stop the named
+ * trace. Otherwise, stop the most recently started trace.
+ *
+ * @param aName string
+ * The name of the trace to stop.
+ *
+ * @param aOnResponse function
+ * Called with the request's response.
+ */
+ stopTrace: DebuggerClient.requester({
+ type: "stopTrace",
+ name: args(0)
+ }, {
+ after: function (aResponse) {
+ if (aResponse.error) {
+ return aResponse;
+ }
+
+ this._activeTraces.delete(aResponse.name);
+
+ return aResponse;
+ },
+ telemetry: "STOPTRACE"
+ })
+ };
+
+ /**
+ * Grip clients are used to retrieve information about the relevant object.
+ *
+ * @param aClient DebuggerClient
+ * The debugger client parent.
+ * @param aGrip object
+ * A pause-lifetime object grip returned by the protocol.
+ */
+ function ObjectClient(aClient, aGrip) {
+ this._grip = aGrip;
+ this._client = aClient;
+ this.request = this._client.request;
+ }
+ exports.ObjectClient = ObjectClient;
+
+ ObjectClient.prototype = {
+ get actor() {
+ return this._grip.actor;
+ },
+ get _transport() {
+ return this._client._transport;
+ },
+
+ valid: true,
+
+ get isFrozen() {
+ return this._grip.frozen;
+ },
+ get isSealed() {
+ return this._grip.sealed;
+ },
+ get isExtensible() {
+ return this._grip.extensible;
+ },
+
+ getDefinitionSite: DebuggerClient.requester({
+ type: "definitionSite"
+ }, {
+ before: function (aPacket) {
+ if (this._grip.class != "Function") {
+ throw new Error("getDefinitionSite is only valid for function grips.");
+ }
+ return aPacket;
+ }
+ }),
+
+ /**
+ * Request the names of a function's formal parameters.
+ *
+ * @param aOnResponse function
+ * Called with an object of the form:
+ * { parameterNames:[<parameterName>, ...] }
+ * where each <parameterName> is the name of a parameter.
+ */
+ getParameterNames: DebuggerClient.requester({
+ type: "parameterNames"
+ }, {
+ before: function (aPacket) {
+ if (this._grip.class !== "Function") {
+ throw new Error("getParameterNames is only valid for function grips.");
+ }
+ return aPacket;
+ },
+ telemetry: "PARAMETERNAMES"
+ }),
+
+ /**
+ * Request the names of the properties defined on the object and not its
+ * prototype.
+ *
+ * @param aOnResponse function Called with the request's response.
+ */
+ getOwnPropertyNames: DebuggerClient.requester({
+ type: "ownPropertyNames"
+ }, {
+ telemetry: "OWNPROPERTYNAMES"
+ }),
+
+ /**
+ * Request the prototype and own properties of the object.
+ *
+ * @param aOnResponse function Called with the request's response.
+ */
+ getPrototypeAndProperties: DebuggerClient.requester({
+ type: "prototypeAndProperties"
+ }, {
+ telemetry: "PROTOTYPEANDPROPERTIES"
+ }),
+
+ /**
+ * Request a PropertyIteratorClient instance to ease listing
+ * properties for this object.
+ *
+ * @param options Object
+ * A dictionary object with various boolean attributes:
+ * - ignoreSafeGetters Boolean
+ * If true, do not iterate over safe getters.
+ * - ignoreIndexedProperties Boolean
+ * If true, filters out Array items.
+ * e.g. properties names between `0` and `object.length`.
+ * - ignoreNonIndexedProperties Boolean
+ * If true, filters out items that aren't array items
+ * e.g. properties names that are not a number between `0`
+ * and `object.length`.
+ * - sort Boolean
+ * If true, the iterator will sort the properties by name
+ * before dispatching them.
+ * @param aOnResponse function Called with the client instance.
+ */
+ enumProperties: DebuggerClient.requester({
+ type: "enumProperties",
+ options: args(0)
+ }, {
+ after: function (aResponse) {
+ if (aResponse.iterator) {
+ return { iterator: new PropertyIteratorClient(this._client, aResponse.iterator) };
+ }
+ return aResponse;
+ },
+ telemetry: "ENUMPROPERTIES"
+ }),
+
+ /**
+ * Request a PropertyIteratorClient instance to enumerate entries in a
+ * Map/Set-like object.
+ *
+ * @param aOnResponse function Called with the request's response.
+ */
+ enumEntries: DebuggerClient.requester({
+ type: "enumEntries"
+ }, {
+ before: function (packet) {
+ if (!["Map", "WeakMap", "Set", "WeakSet"].includes(this._grip.class)) {
+ throw new Error("enumEntries is only valid for Map/Set-like grips.");
+ }
+ return packet;
+ },
+ after: function (response) {
+ if (response.iterator) {
+ return {
+ iterator: new PropertyIteratorClient(this._client, response.iterator)
+ };
+ }
+ return response;
+ }
+ }),
+
+ /**
+ * Request the property descriptor of the object's specified property.
+ *
+ * @param aName string The name of the requested property.
+ * @param aOnResponse function Called with the request's response.
+ */
+ getProperty: DebuggerClient.requester({
+ type: "property",
+ name: args(0)
+ }, {
+ telemetry: "PROPERTY"
+ }),
+
+ /**
+ * Request the prototype of the object.
+ *
+ * @param aOnResponse function Called with the request's response.
+ */
+ getPrototype: DebuggerClient.requester({
+ type: "prototype"
+ }, {
+ telemetry: "PROTOTYPE"
+ }),
+
+ /**
+ * Request the display string of the object.
+ *
+ * @param aOnResponse function Called with the request's response.
+ */
+ getDisplayString: DebuggerClient.requester({
+ type: "displayString"
+ }, {
+ telemetry: "DISPLAYSTRING"
+ }),
+
+ /**
+ * Request the scope of the object.
+ *
+ * @param aOnResponse function Called with the request's response.
+ */
+ getScope: DebuggerClient.requester({
+ type: "scope"
+ }, {
+ before: function (aPacket) {
+ if (this._grip.class !== "Function") {
+ throw new Error("scope is only valid for function grips.");
+ }
+ return aPacket;
+ },
+ telemetry: "SCOPE"
+ }),
+
+ /**
+ * Request the promises directly depending on the current promise.
+ */
+ getDependentPromises: DebuggerClient.requester({
+ type: "dependentPromises"
+ }, {
+ before: function (aPacket) {
+ if (this._grip.class !== "Promise") {
+ throw new Error("getDependentPromises is only valid for promise " + "grips.");
+ }
+ return aPacket;
+ }
+ }),
+
+ /**
+ * Request the stack to the promise's allocation point.
+ */
+ getPromiseAllocationStack: DebuggerClient.requester({
+ type: "allocationStack"
+ }, {
+ before: function (aPacket) {
+ if (this._grip.class !== "Promise") {
+ throw new Error("getAllocationStack is only valid for promise grips.");
+ }
+ return aPacket;
+ }
+ }),
+
+ /**
+ * Request the stack to the promise's fulfillment point.
+ */
+ getPromiseFulfillmentStack: DebuggerClient.requester({
+ type: "fulfillmentStack"
+ }, {
+ before: function (packet) {
+ if (this._grip.class !== "Promise") {
+ throw new Error("getPromiseFulfillmentStack is only valid for " + "promise grips.");
+ }
+ return packet;
+ }
+ }),
+
+ /**
+ * Request the stack to the promise's rejection point.
+ */
+ getPromiseRejectionStack: DebuggerClient.requester({
+ type: "rejectionStack"
+ }, {
+ before: function (packet) {
+ if (this._grip.class !== "Promise") {
+ throw new Error("getPromiseRejectionStack is only valid for " + "promise grips.");
+ }
+ return packet;
+ }
+ })
+ };
+
+ /**
+ * A PropertyIteratorClient provides a way to access to property names and
+ * values of an object efficiently, slice by slice.
+ * Note that the properties can be sorted in the backend,
+ * this is controled while creating the PropertyIteratorClient
+ * from ObjectClient.enumProperties.
+ *
+ * @param aClient DebuggerClient
+ * The debugger client parent.
+ * @param aGrip Object
+ * A PropertyIteratorActor grip returned by the protocol via
+ * TabActor.enumProperties request.
+ */
+ function PropertyIteratorClient(aClient, aGrip) {
+ this._grip = aGrip;
+ this._client = aClient;
+ this.request = this._client.request;
+ }
+
+ PropertyIteratorClient.prototype = {
+ get actor() {
+ return this._grip.actor;
+ },
+
+ /**
+ * Get the total number of properties available in the iterator.
+ */
+ get count() {
+ return this._grip.count;
+ },
+
+ /**
+ * Get one or more property names that correspond to the positions in the
+ * indexes parameter.
+ *
+ * @param indexes Array
+ * An array of property indexes.
+ * @param aCallback Function
+ * The function called when we receive the property names.
+ */
+ names: DebuggerClient.requester({
+ type: "names",
+ indexes: args(0)
+ }, {}),
+
+ /**
+ * Get a set of following property value(s).
+ *
+ * @param start Number
+ * The index of the first property to fetch.
+ * @param count Number
+ * The number of properties to fetch.
+ * @param aCallback Function
+ * The function called when we receive the property values.
+ */
+ slice: DebuggerClient.requester({
+ type: "slice",
+ start: args(0),
+ count: args(1)
+ }, {}),
+
+ /**
+ * Get all the property values.
+ *
+ * @param aCallback Function
+ * The function called when we receive the property values.
+ */
+ all: DebuggerClient.requester({
+ type: "all"
+ }, {})
+ };
+
+ /**
+ * A LongStringClient provides a way to access "very long" strings from the
+ * debugger server.
+ *
+ * @param aClient DebuggerClient
+ * The debugger client parent.
+ * @param aGrip Object
+ * A pause-lifetime long string grip returned by the protocol.
+ */
+ function LongStringClient(aClient, aGrip) {
+ this._grip = aGrip;
+ this._client = aClient;
+ this.request = this._client.request;
+ }
+ exports.LongStringClient = LongStringClient;
+
+ LongStringClient.prototype = {
+ get actor() {
+ return this._grip.actor;
+ },
+ get length() {
+ return this._grip.length;
+ },
+ get initial() {
+ return this._grip.initial;
+ },
+ get _transport() {
+ return this._client._transport;
+ },
+
+ valid: true,
+
+ /**
+ * Get the substring of this LongString from aStart to aEnd.
+ *
+ * @param aStart Number
+ * The starting index.
+ * @param aEnd Number
+ * The ending index.
+ * @param aCallback Function
+ * The function called when we receive the substring.
+ */
+ substring: DebuggerClient.requester({
+ type: "substring",
+ start: args(0),
+ end: args(1)
+ }, {
+ telemetry: "SUBSTRING"
+ })
+ };
+
+ /**
+ * A SourceClient provides a way to access the source text of a script.
+ *
+ * @param aClient ThreadClient
+ * The thread client parent.
+ * @param aForm Object
+ * The form sent across the remote debugging protocol.
+ */
+ function SourceClient(aClient, aForm) {
+ this._form = aForm;
+ this._isBlackBoxed = aForm.isBlackBoxed;
+ this._isPrettyPrinted = aForm.isPrettyPrinted;
+ this._activeThread = aClient;
+ this._client = aClient.client;
+ }
+
+ SourceClient.prototype = {
+ get _transport() {
+ return this._client._transport;
+ },
+ get isBlackBoxed() {
+ return this._isBlackBoxed;
+ },
+ get isPrettyPrinted() {
+ return this._isPrettyPrinted;
+ },
+ get actor() {
+ return this._form.actor;
+ },
+ get request() {
+ return this._client.request;
+ },
+ get url() {
+ return this._form.url;
+ },
+
+ /**
+ * Black box this SourceClient's source.
+ *
+ * @param aCallback Function
+ * The callback function called when we receive the response from the server.
+ */
+ blackBox: DebuggerClient.requester({
+ type: "blackbox"
+ }, {
+ telemetry: "BLACKBOX",
+ after: function (aResponse) {
+ if (!aResponse.error) {
+ this._isBlackBoxed = true;
+ if (this._activeThread) {
+ this._activeThread.emit("blackboxchange", this);
+ }
+ }
+ return aResponse;
+ }
+ }),
+
+ /**
+ * Un-black box this SourceClient's source.
+ *
+ * @param aCallback Function
+ * The callback function called when we receive the response from the server.
+ */
+ unblackBox: DebuggerClient.requester({
+ type: "unblackbox"
+ }, {
+ telemetry: "UNBLACKBOX",
+ after: function (aResponse) {
+ if (!aResponse.error) {
+ this._isBlackBoxed = false;
+ if (this._activeThread) {
+ this._activeThread.emit("blackboxchange", this);
+ }
+ }
+ return aResponse;
+ }
+ }),
+
+ /**
+ * Get Executable Lines from a source
+ *
+ * @param aCallback Function
+ * The callback function called when we receive the response from the server.
+ */
+ getExecutableLines: function () {
+ var cb = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : noop;
+
+ var packet = {
+ to: this._form.actor,
+ type: "getExecutableLines"
+ };
+
+ return this._client.request(packet).then(res => {
+ cb(res.lines);
+ return res.lines;
+ });
+ },
+
+ /**
+ * Get a long string grip for this SourceClient's source.
+ */
+ source: function () {
+ var aCallback = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : noop;
+
+ var packet = {
+ to: this._form.actor,
+ type: "source"
+ };
+ return this._client.request(packet).then(aResponse => {
+ return this._onSourceResponse(aResponse, aCallback);
+ });
+ },
+
+ /**
+ * Pretty print this source's text.
+ */
+ prettyPrint: function (aIndent) {
+ var aCallback = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : noop;
+
+ var packet = {
+ to: this._form.actor,
+ type: "prettyPrint",
+ indent: aIndent
+ };
+ return this._client.request(packet).then(aResponse => {
+ if (!aResponse.error) {
+ this._isPrettyPrinted = true;
+ this._activeThread._clearFrames();
+ this._activeThread.emit("prettyprintchange", this);
+ }
+ return this._onSourceResponse(aResponse, aCallback);
+ });
+ },
+
+ /**
+ * Stop pretty printing this source's text.
+ */
+ disablePrettyPrint: function () {
+ var aCallback = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : noop;
+
+ var packet = {
+ to: this._form.actor,
+ type: "disablePrettyPrint"
+ };
+ return this._client.request(packet).then(aResponse => {
+ if (!aResponse.error) {
+ this._isPrettyPrinted = false;
+ this._activeThread._clearFrames();
+ this._activeThread.emit("prettyprintchange", this);
+ }
+ return this._onSourceResponse(aResponse, aCallback);
+ });
+ },
+
+ _onSourceResponse: function (aResponse, aCallback) {
+ if (aResponse.error) {
+ aCallback(aResponse);
+ return aResponse;
+ }
+
+ if (typeof aResponse.source === "string") {
+ aCallback(aResponse);
+ return aResponse;
+ }
+
+ var contentType = aResponse.contentType;
+ var source = aResponse.source;
+
+ var longString = this._activeThread.threadLongString(source);
+ return longString.substring(0, longString.length).then(function (aResponse) {
+ if (aResponse.error) {
+ aCallback(aResponse);
+ return aReponse;
+ }
+
+ var response = {
+ source: aResponse.substring,
+ contentType: contentType
+ };
+ aCallback(response);
+ return response;
+ });
+ },
+
+ /**
+ * Request to set a breakpoint in the specified location.
+ *
+ * @param object aLocation
+ * The location and condition of the breakpoint in
+ * the form of { line[, column, condition] }.
+ * @param function aOnResponse
+ * Called with the thread's response.
+ */
+ setBreakpoint: function (_ref) {
+ var line = _ref.line;
+ var column = _ref.column;
+ var condition = _ref.condition;
+ var noSliding = _ref.noSliding;
+ var aOnResponse = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : noop;
+
+ // A helper function that sets the breakpoint.
+ var doSetBreakpoint = aCallback => {
+ var root = this._client.mainRoot;
+ var location = {
+ line: line,
+ column: column
+ };
+
+ var packet = {
+ to: this.actor,
+ type: "setBreakpoint",
+ location: location,
+ condition: condition,
+ noSliding: noSliding
+ };
+
+ // Backwards compatibility: send the breakpoint request to the
+ // thread if the server doesn't support Debugger.Source actors.
+ if (!root.traits.debuggerSourceActors) {
+ packet.to = this._activeThread.actor;
+ packet.location.url = this.url;
+ }
+
+ return this._client.request(packet).then(aResponse => {
+ // Ignoring errors, since the user may be setting a breakpoint in a
+ // dead script that will reappear on a page reload.
+ var bpClient = void 0;
+ if (aResponse.actor) {
+ bpClient = new BreakpointClient(this._client, this, aResponse.actor, location, root.traits.conditionalBreakpoints ? condition : undefined);
+ }
+ aOnResponse(aResponse, bpClient);
+ if (aCallback) {
+ aCallback();
+ }
+ return [aResponse, bpClient];
+ });
+ };
+
+ // If the debuggee is paused, just set the breakpoint.
+ if (this._activeThread.paused) {
+ return doSetBreakpoint();
+ }
+ // Otherwise, force a pause in order to set the breakpoint.
+ return this._activeThread.interrupt().then(aResponse => {
+ if (aResponse.error) {
+ // Can't set the breakpoint if pausing failed.
+ aOnResponse(aResponse);
+ return aResponse;
+ }
+
+ var type = aResponse.type;
+ var why = aResponse.why;
+
+ var cleanUp = type == "paused" && why.type == "interrupted" ? () => this._activeThread.resume() : noop;
+
+ return doSetBreakpoint(cleanUp);
+ });
+ }
+ };
+
+ /**
+ * Breakpoint clients are used to remove breakpoints that are no longer used.
+ *
+ * @param aClient DebuggerClient
+ * The debugger client parent.
+ * @param aSourceClient SourceClient
+ * The source where this breakpoint exists
+ * @param aActor string
+ * The actor ID for this breakpoint.
+ * @param aLocation object
+ * The location of the breakpoint. This is an object with two properties:
+ * url and line.
+ * @param aCondition string
+ * The conditional expression of the breakpoint
+ */
+ function BreakpointClient(aClient, aSourceClient, aActor, aLocation, aCondition) {
+ this._client = aClient;
+ this._actor = aActor;
+ this.location = aLocation;
+ this.location.actor = aSourceClient.actor;
+ this.location.url = aSourceClient.url;
+ this.source = aSourceClient;
+ this.request = this._client.request;
+
+ // The condition property should only exist if it's a truthy value
+ if (aCondition) {
+ this.condition = aCondition;
+ }
+ }
+
+ BreakpointClient.prototype = {
+
+ _actor: null,
+ get actor() {
+ return this._actor;
+ },
+ get _transport() {
+ return this._client._transport;
+ },
+
+ /**
+ * Remove the breakpoint from the server.
+ */
+ remove: DebuggerClient.requester({
+ type: "delete"
+ }, {
+ telemetry: "DELETE"
+ }),
+
+ /**
+ * Determines if this breakpoint has a condition
+ */
+ hasCondition: function () {
+ var root = this._client.mainRoot;
+ // XXX bug 990137: We will remove support for client-side handling of
+ // conditional breakpoints
+ if (root.traits.conditionalBreakpoints) {
+ return "condition" in this;
+ } else {
+ return "conditionalExpression" in this;
+ }
+ },
+
+ /**
+ * Get the condition of this breakpoint. Currently we have to
+ * support locally emulated conditional breakpoints until the
+ * debugger servers are updated (see bug 990137). We used a
+ * different property when moving it server-side to ensure that we
+ * are testing the right code.
+ */
+ getCondition: function () {
+ var root = this._client.mainRoot;
+ if (root.traits.conditionalBreakpoints) {
+ return this.condition;
+ } else {
+ return this.conditionalExpression;
+ }
+ },
+
+ /**
+ * Set the condition of this breakpoint
+ */
+ setCondition: function (gThreadClient, aCondition, noSliding) {
+ var _this6 = this;
+
+ var root = this._client.mainRoot;
+ var deferred = promise.defer();
+
+ if (root.traits.conditionalBreakpoints) {
+ (function () {
+ var info = {
+ line: _this6.location.line,
+ column: _this6.location.column,
+ condition: aCondition,
+ noSliding
+ };
+
+ // Remove the current breakpoint and add a new one with the
+ // condition.
+ _this6.remove(aResponse => {
+ if (aResponse && aResponse.error) {
+ deferred.reject(aResponse);
+ return;
+ }
+
+ _this6.source.setBreakpoint(info, (aResponse, aNewBreakpoint) => {
+ if (aResponse && aResponse.error) {
+ deferred.reject(aResponse);
+ } else {
+ deferred.resolve(aNewBreakpoint);
+ }
+ });
+ });
+ })();
+ } else {
+ // The property shouldn't even exist if the condition is blank
+ if (aCondition === "") {
+ delete this.conditionalExpression;
+ } else {
+ this.conditionalExpression = aCondition;
+ }
+ deferred.resolve(this);
+ }
+
+ return deferred.promise;
+ }
+ };
+
+ eventSource(BreakpointClient.prototype);
+
+ /**
+ * Environment clients are used to manipulate the lexical environment actors.
+ *
+ * @param aClient DebuggerClient
+ * The debugger client parent.
+ * @param aForm Object
+ * The form sent across the remote debugging protocol.
+ */
+ function EnvironmentClient(aClient, aForm) {
+ this._client = aClient;
+ this._form = aForm;
+ this.request = this._client.request;
+ }
+ exports.EnvironmentClient = EnvironmentClient;
+
+ EnvironmentClient.prototype = {
+
+ get actor() {
+ return this._form.actor;
+ },
+ get _transport() {
+ return this._client._transport;
+ },
+
+ /**
+ * Fetches the bindings introduced by this lexical environment.
+ */
+ getBindings: DebuggerClient.requester({
+ type: "bindings"
+ }, {
+ telemetry: "BINDINGS"
+ }),
+
+ /**
+ * Changes the value of the identifier whose name is name (a string) to that
+ * represented by value (a grip).
+ */
+ assign: DebuggerClient.requester({
+ type: "assign",
+ name: args(0),
+ value: args(1)
+ }, {
+ telemetry: "ASSIGN"
+ })
+ };
+
+ eventSource(EnvironmentClient.prototype);
+
+/***/ },
+/* 80 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /* WEBPACK VAR INJECTION */(function(module) {/* 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";
+
+ module.metadata = {
+ "stability": "unstable"
+ };
+
+ var UNCAUGHT_ERROR = 'An error event was emitted for which there was no listener.';
+ var BAD_LISTENER = 'The event listener must be a function.';
+
+ var _require = __webpack_require__(81);
+
+ var ns = _require.ns;
+
+
+ var event = ns();
+
+ var EVENT_TYPE_PATTERN = /^on([A-Z]\w+$)/;
+ exports.EVENT_TYPE_PATTERN = EVENT_TYPE_PATTERN;
+
+ // Utility function to access given event `target` object's event listeners for
+ // the specific event `type`. If listeners for this type does not exists they
+ // will be created.
+ var observers = function observers(target, type) {
+ if (!target) throw TypeError("Event target must be an object");
+ var listeners = event(target);
+ return type in listeners ? listeners[type] : listeners[type] = [];
+ };
+
+ /**
+ * Registers an event `listener` that is called every time events of
+ * specified `type` is emitted on the given event `target`.
+ * @param {Object} target
+ * Event target object.
+ * @param {String} type
+ * The type of event.
+ * @param {Function} listener
+ * The listener function that processes the event.
+ */
+ function on(target, type, listener) {
+ if (typeof listener !== 'function') throw new Error(BAD_LISTENER);
+
+ var listeners = observers(target, type);
+ if (!~listeners.indexOf(listener)) listeners.push(listener);
+ }
+ exports.on = on;
+
+ var onceWeakMap = new WeakMap();
+
+ /**
+ * Registers an event `listener` that is called only the next time an event
+ * of the specified `type` is emitted on the given event `target`.
+ * @param {Object} target
+ * Event target object.
+ * @param {String} type
+ * The type of the event.
+ * @param {Function} listener
+ * The listener function that processes the event.
+ */
+ function once(target, type, listener) {
+ var replacement = function observer() {
+ off(target, type, observer);
+ onceWeakMap.delete(listener);
+
+ for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
+ args[_key] = arguments[_key];
+ }
+
+ listener.apply(target, args);
+ };
+ onceWeakMap.set(listener, replacement);
+ on(target, type, replacement);
+ }
+ exports.once = once;
+
+ /**
+ * Execute each of the listeners in order with the supplied arguments.
+ * All the exceptions that are thrown by listeners during the emit
+ * are caught and can be handled by listeners of 'error' event. Thrown
+ * exceptions are passed as an argument to an 'error' event listener.
+ * If no 'error' listener is registered exception will be logged into an
+ * error console.
+ * @param {Object} target
+ * Event target object.
+ * @param {String} type
+ * The type of event.
+ * @params {Object|Number|String|Boolean} args
+ * Arguments that will be passed to listeners.
+ */
+ function emit(target, type) {
+ for (var _len2 = arguments.length, args = Array(_len2 > 2 ? _len2 - 2 : 0), _key2 = 2; _key2 < _len2; _key2++) {
+ args[_key2 - 2] = arguments[_key2];
+ }
+
+ emitOnObject.apply(undefined, [target, type, target].concat(args));
+ }
+ exports.emit = emit;
+
+ /**
+ * A variant of emit that allows setting the this property for event listeners
+ */
+ function emitOnObject(target, type, thisArg) {
+ var all = observers(target, '*').length;
+ var state = observers(target, type);
+ var listeners = state.slice();
+ var count = listeners.length;
+ var index = 0;
+
+ // If error event and there are no handlers (explicit or catch-all)
+ // then print error message to the console.
+
+ for (var _len3 = arguments.length, args = Array(_len3 > 3 ? _len3 - 3 : 0), _key3 = 3; _key3 < _len3; _key3++) {
+ args[_key3 - 3] = arguments[_key3];
+ }
+
+ if (count === 0 && type === 'error' && all === 0) console.exception(args[0]);
+ while (index < count) {
+ try {
+ var listener = listeners[index];
+ // Dispatch only if listener is still registered.
+ if (~state.indexOf(listener)) listener.apply(thisArg, args);
+ } catch (error) {
+ // If exception is not thrown by a error listener and error listener is
+ // registered emit `error` event. Otherwise dump exception to the console.
+ if (type !== 'error') emit(target, 'error', error);else console.exception(error);
+ }
+ index++;
+ }
+ // Also emit on `"*"` so that one could listen for all events.
+ if (type !== '*') emit.apply(undefined, [target, '*', type].concat(args));
+ }
+ exports.emitOnObject = emitOnObject;
+
+ /**
+ * Removes an event `listener` for the given event `type` on the given event
+ * `target`. If no `listener` is passed removes all listeners of the given
+ * `type`. If `type` is not passed removes all the listeners of the given
+ * event `target`.
+ * @param {Object} target
+ * The event target object.
+ * @param {String} type
+ * The type of event.
+ * @param {Function} listener
+ * The listener function that processes the event.
+ */
+ function off(target, type, listener) {
+ var length = arguments.length;
+ if (length === 3) {
+ if (onceWeakMap.has(listener)) {
+ listener = onceWeakMap.get(listener);
+ onceWeakMap.delete(listener);
+ }
+
+ var listeners = observers(target, type);
+ var index = listeners.indexOf(listener);
+ if (~index) listeners.splice(index, 1);
+ } else if (length === 2) {
+ observers(target, type).splice(0);
+ } else if (length === 1) {
+ (function () {
+ var listeners = event(target);
+ Object.keys(listeners).forEach(type => delete listeners[type]);
+ })();
+ }
+ }
+ exports.off = off;
+
+ /**
+ * Returns a number of event listeners registered for the given event `type`
+ * on the given event `target`.
+ */
+ function count(target, type) {
+ return observers(target, type).length;
+ }
+ exports.count = count;
+
+ /**
+ * Registers listeners on the given event `target` from the given `listeners`
+ * dictionary. Iterates over the listeners and if property name matches name
+ * pattern `onEventType` and property is a function, then registers it as
+ * an `eventType` listener on `target`.
+ *
+ * @param {Object} target
+ * The type of event.
+ * @param {Object} listeners
+ * Dictionary of listeners.
+ */
+ function setListeners(target, listeners) {
+ Object.keys(listeners || {}).forEach(key => {
+ var match = EVENT_TYPE_PATTERN.exec(key);
+ var type = match && match[1].toLowerCase();
+ if (!type) return;
+
+ var listener = listeners[key];
+ if (typeof listener === 'function') on(target, type, listener);
+ });
+ }
+ exports.setListeners = setListeners;
+ /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(77)(module)))
+
+/***/ },
+/* 81 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /* WEBPACK VAR INJECTION */(function(module) {/* 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";
+
+ module.metadata = {
+ "stability": "unstable"
+ };
+
+ var create = Object.create;
+ var prototypeOf = Object.getPrototypeOf;
+
+ /**
+ * Returns a new namespace, function that may can be used to access an
+ * namespaced object of the argument argument. Namespaced object are associated
+ * with owner objects via weak references. Namespaced objects inherit from the
+ * owners ancestor namespaced object. If owner's ancestor is `null` then
+ * namespaced object inherits from given `prototype`. Namespaces can be used
+ * to define internal APIs that can be shared via enclosing `namespace`
+ * function.
+ * @examples
+ * const internals = ns();
+ * internals(object).secret = secret;
+ */
+ function ns() {
+ var map = new WeakMap();
+ return function namespace(target) {
+ if (!target) // If `target` is not an object return `target` itself.
+ return target;
+ // If target has no namespaced object yet, create one that inherits from
+ // the target prototype's namespaced object.
+ if (!map.has(target)) map.set(target, create(namespace(prototypeOf(target) || null)));
+
+ return map.get(target);
+ };
+ };
+
+ // `Namespace` is a e4x function in the scope, so we export the function also as
+ // `ns` as alias to avoid clashing.
+ exports.ns = ns;
+ exports.Namespace = ns;
+ /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(77)(module)))
+
+/***/ },
+/* 82 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
+ /* vim: set 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";
+
+ var _require = __webpack_require__(61);
+
+ var Cc = _require.Cc;
+ var Ci = _require.Ci;
+ var Cu = _require.Cu;
+
+ var DevToolsUtils = __webpack_require__(68);
+ var EventEmitter = __webpack_require__(60);
+ var promise = __webpack_require__(66);
+
+ var _require2 = __webpack_require__(79);
+
+ var LongStringClient = _require2.LongStringClient;
+
+ /**
+ * A WebConsoleClient is used as a front end for the WebConsoleActor that is
+ * created on the server, hiding implementation details.
+ *
+ * @param object aDebuggerClient
+ * The DebuggerClient instance we live for.
+ * @param object aResponse
+ * The response packet received from the "startListeners" request sent to
+ * the WebConsoleActor.
+ */
+
+ function WebConsoleClient(aDebuggerClient, aResponse) {
+ this._actor = aResponse.from;
+ this._client = aDebuggerClient;
+ this._longStrings = {};
+ this.traits = aResponse.traits || {};
+ this.events = [];
+ this._networkRequests = new Map();
+
+ this.pendingEvaluationResults = new Map();
+ this.onEvaluationResult = this.onEvaluationResult.bind(this);
+ this.onNetworkEvent = this._onNetworkEvent.bind(this);
+ this.onNetworkEventUpdate = this._onNetworkEventUpdate.bind(this);
+
+ this._client.addListener("evaluationResult", this.onEvaluationResult);
+ this._client.addListener("networkEvent", this.onNetworkEvent);
+ this._client.addListener("networkEventUpdate", this.onNetworkEventUpdate);
+ EventEmitter.decorate(this);
+ }
+
+ exports.WebConsoleClient = WebConsoleClient;
+
+ WebConsoleClient.prototype = {
+ _longStrings: null,
+ traits: null,
+
+ /**
+ * Holds the network requests currently displayed by the Web Console. Each key
+ * represents the connection ID and the value is network request information.
+ * @private
+ * @type object
+ */
+ _networkRequests: null,
+
+ getNetworkRequest(actorId) {
+ return this._networkRequests.get(actorId);
+ },
+
+ hasNetworkRequest(actorId) {
+ return this._networkRequests.has(actorId);
+ },
+
+ removeNetworkRequest(actorId) {
+ this._networkRequests.delete(actorId);
+ },
+
+ getNetworkEvents() {
+ return this._networkRequests.values();
+ },
+
+ get actor() {
+ return this._actor;
+ },
+
+ /**
+ * The "networkEvent" message type handler. We redirect any message to
+ * the UI for displaying.
+ *
+ * @private
+ * @param string type
+ * Message type.
+ * @param object packet
+ * The message received from the server.
+ */
+ _onNetworkEvent: function (type, packet) {
+ if (packet.from == this._actor) {
+ var actor = packet.eventActor;
+ var networkInfo = {
+ _type: "NetworkEvent",
+ timeStamp: actor.timeStamp,
+ node: null,
+ actor: actor.actor,
+ discardRequestBody: true,
+ discardResponseBody: true,
+ startedDateTime: actor.startedDateTime,
+ request: {
+ url: actor.url,
+ method: actor.method
+ },
+ isXHR: actor.isXHR,
+ response: {},
+ timings: {},
+ updates: [], // track the list of network event updates
+ private: actor.private,
+ fromCache: actor.fromCache
+ };
+ this._networkRequests.set(actor.actor, networkInfo);
+
+ this.emit("networkEvent", networkInfo);
+ }
+ },
+
+ /**
+ * The "networkEventUpdate" message type handler. We redirect any message to
+ * the UI for displaying.
+ *
+ * @private
+ * @param string type
+ * Message type.
+ * @param object packet
+ * The message received from the server.
+ */
+ _onNetworkEventUpdate: function (type, packet) {
+ var networkInfo = this.getNetworkRequest(packet.from);
+ if (!networkInfo) {
+ return;
+ }
+
+ networkInfo.updates.push(packet.updateType);
+
+ switch (packet.updateType) {
+ case "requestHeaders":
+ networkInfo.request.headersSize = packet.headersSize;
+ break;
+ case "requestPostData":
+ networkInfo.discardRequestBody = packet.discardRequestBody;
+ networkInfo.request.bodySize = packet.dataSize;
+ break;
+ case "responseStart":
+ networkInfo.response.httpVersion = packet.response.httpVersion;
+ networkInfo.response.status = packet.response.status;
+ networkInfo.response.statusText = packet.response.statusText;
+ networkInfo.response.headersSize = packet.response.headersSize;
+ networkInfo.response.remoteAddress = packet.response.remoteAddress;
+ networkInfo.response.remotePort = packet.response.remotePort;
+ networkInfo.discardResponseBody = packet.response.discardResponseBody;
+ break;
+ case "responseContent":
+ networkInfo.response.content = {
+ mimeType: packet.mimeType
+ };
+ networkInfo.response.bodySize = packet.contentSize;
+ networkInfo.response.transferredSize = packet.transferredSize;
+ networkInfo.discardResponseBody = packet.discardResponseBody;
+ break;
+ case "eventTimings":
+ networkInfo.totalTime = packet.totalTime;
+ break;
+ case "securityInfo":
+ networkInfo.securityInfo = packet.state;
+ break;
+ }
+
+ this.emit("networkEventUpdate", {
+ packet: packet,
+ networkInfo
+ });
+ },
+
+ /**
+ * Retrieve the cached messages from the server.
+ *
+ * @see this.CACHED_MESSAGES
+ * @param array types
+ * The array of message types you want from the server. See
+ * this.CACHED_MESSAGES for known types.
+ * @param function aOnResponse
+ * The function invoked when the response is received.
+ */
+ getCachedMessages: function WCC_getCachedMessages(types, aOnResponse) {
+ var packet = {
+ to: this._actor,
+ type: "getCachedMessages",
+ messageTypes: types
+ };
+ this._client.request(packet, aOnResponse);
+ },
+
+ /**
+ * Inspect the properties of an object.
+ *
+ * @param string aActor
+ * The WebConsoleObjectActor ID to send the request to.
+ * @param function aOnResponse
+ * The function invoked when the response is received.
+ */
+ inspectObjectProperties: function WCC_inspectObjectProperties(aActor, aOnResponse) {
+ var packet = {
+ to: aActor,
+ type: "inspectProperties"
+ };
+ this._client.request(packet, aOnResponse);
+ },
+
+ /**
+ * Evaluate a JavaScript expression.
+ *
+ * @param string aString
+ * The code you want to evaluate.
+ * @param function aOnResponse
+ * The function invoked when the response is received.
+ * @param object [aOptions={}]
+ * Options for evaluation:
+ *
+ * - bindObjectActor: an ObjectActor ID. The OA holds a reference to
+ * a Debugger.Object that wraps a content object. This option allows
+ * you to bind |_self| to the D.O of the given OA, during string
+ * evaluation.
+ *
+ * See: Debugger.Object.executeInGlobalWithBindings() for information
+ * about bindings.
+ *
+ * Use case: the variable view needs to update objects and it does so
+ * by knowing the ObjectActor it inspects and binding |_self| to the
+ * D.O of the OA. As such, variable view sends strings like these for
+ * eval:
+ * _self["prop"] = value;
+ *
+ * - frameActor: a FrameActor ID. The FA holds a reference to
+ * a Debugger.Frame. This option allows you to evaluate the string in
+ * the frame of the given FA.
+ *
+ * - url: the url to evaluate the script as. Defaults to
+ * "debugger eval code".
+ *
+ * - selectedNodeActor: the NodeActor ID of the current selection in the
+ * Inspector, if such a selection exists. This is used by helper functions
+ * that can reference the currently selected node in the Inspector, like
+ * $0.
+ */
+ evaluateJS: function WCC_evaluateJS(aString, aOnResponse) {
+ var aOptions = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
+
+ var packet = {
+ to: this._actor,
+ type: "evaluateJS",
+ text: aString,
+ bindObjectActor: aOptions.bindObjectActor,
+ frameActor: aOptions.frameActor,
+ url: aOptions.url,
+ selectedNodeActor: aOptions.selectedNodeActor,
+ selectedObjectActor: aOptions.selectedObjectActor
+ };
+ this._client.request(packet, aOnResponse);
+ },
+
+ /**
+ * Evaluate a JavaScript expression asynchronously.
+ * See evaluateJS for parameter and response information.
+ */
+ evaluateJSAsync: function (aString, aOnResponse) {
+ var aOptions = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
+
+ // Pre-37 servers don't support async evaluation.
+ if (!this.traits.evaluateJSAsync) {
+ this.evaluateJS(aString, aOnResponse, aOptions);
+ return;
+ }
+
+ var packet = {
+ to: this._actor,
+ type: "evaluateJSAsync",
+ text: aString,
+ bindObjectActor: aOptions.bindObjectActor,
+ frameActor: aOptions.frameActor,
+ url: aOptions.url,
+ selectedNodeActor: aOptions.selectedNodeActor,
+ selectedObjectActor: aOptions.selectedObjectActor
+ };
+
+ this._client.request(packet, response => {
+ // Null check this in case the client has been detached while waiting
+ // for a response.
+ if (this.pendingEvaluationResults) {
+ this.pendingEvaluationResults.set(response.resultID, aOnResponse);
+ }
+ });
+ },
+
+ /**
+ * Handler for the actors's unsolicited evaluationResult packet.
+ */
+ onEvaluationResult: function (aNotification, aPacket) {
+ // The client on the main thread can receive notification packets from
+ // multiple webconsole actors: the one on the main thread and the ones
+ // on worker threads. So make sure we should be handling this request.
+ if (aPacket.from !== this._actor) {
+ return;
+ }
+
+ // Find the associated callback based on this ID, and fire it.
+ // In a sync evaluation, this would have already been called in
+ // direct response to the client.request function.
+ var onResponse = this.pendingEvaluationResults.get(aPacket.resultID);
+ if (onResponse) {
+ onResponse(aPacket);
+ this.pendingEvaluationResults.delete(aPacket.resultID);
+ } else {
+ DevToolsUtils.reportException("onEvaluationResult", "No response handler for an evaluateJSAsync result (resultID: " + aPacket.resultID + ")");
+ }
+ },
+
+ /**
+ * Autocomplete a JavaScript expression.
+ *
+ * @param string aString
+ * The code you want to autocomplete.
+ * @param number aCursor
+ * Cursor location inside the string. Index starts from 0.
+ * @param function aOnResponse
+ * The function invoked when the response is received.
+ * @param string aFrameActor
+ * The id of the frame actor that made the call.
+ */
+ autocomplete: function WCC_autocomplete(aString, aCursor, aOnResponse, aFrameActor) {
+ var packet = {
+ to: this._actor,
+ type: "autocomplete",
+ text: aString,
+ cursor: aCursor,
+ frameActor: aFrameActor
+ };
+ this._client.request(packet, aOnResponse);
+ },
+
+ /**
+ * Clear the cache of messages (page errors and console API calls).
+ */
+ clearMessagesCache: function WCC_clearMessagesCache() {
+ var packet = {
+ to: this._actor,
+ type: "clearMessagesCache"
+ };
+ this._client.request(packet);
+ },
+
+ /**
+ * Get Web Console-related preferences on the server.
+ *
+ * @param array aPreferences
+ * An array with the preferences you want to retrieve.
+ * @param function [aOnResponse]
+ * Optional function to invoke when the response is received.
+ */
+ getPreferences: function WCC_getPreferences(aPreferences, aOnResponse) {
+ var packet = {
+ to: this._actor,
+ type: "getPreferences",
+ preferences: aPreferences
+ };
+ this._client.request(packet, aOnResponse);
+ },
+
+ /**
+ * Set Web Console-related preferences on the server.
+ *
+ * @param object aPreferences
+ * An object with the preferences you want to change.
+ * @param function [aOnResponse]
+ * Optional function to invoke when the response is received.
+ */
+ setPreferences: function WCC_setPreferences(aPreferences, aOnResponse) {
+ var packet = {
+ to: this._actor,
+ type: "setPreferences",
+ preferences: aPreferences
+ };
+ this._client.request(packet, aOnResponse);
+ },
+
+ /**
+ * Retrieve the request headers from the given NetworkEventActor.
+ *
+ * @param string aActor
+ * The NetworkEventActor ID.
+ * @param function aOnResponse
+ * The function invoked when the response is received.
+ */
+ getRequestHeaders: function WCC_getRequestHeaders(aActor, aOnResponse) {
+ var packet = {
+ to: aActor,
+ type: "getRequestHeaders"
+ };
+ this._client.request(packet, aOnResponse);
+ },
+
+ /**
+ * Retrieve the request cookies from the given NetworkEventActor.
+ *
+ * @param string aActor
+ * The NetworkEventActor ID.
+ * @param function aOnResponse
+ * The function invoked when the response is received.
+ */
+ getRequestCookies: function WCC_getRequestCookies(aActor, aOnResponse) {
+ var packet = {
+ to: aActor,
+ type: "getRequestCookies"
+ };
+ this._client.request(packet, aOnResponse);
+ },
+
+ /**
+ * Retrieve the request post data from the given NetworkEventActor.
+ *
+ * @param string aActor
+ * The NetworkEventActor ID.
+ * @param function aOnResponse
+ * The function invoked when the response is received.
+ */
+ getRequestPostData: function WCC_getRequestPostData(aActor, aOnResponse) {
+ var packet = {
+ to: aActor,
+ type: "getRequestPostData"
+ };
+ this._client.request(packet, aOnResponse);
+ },
+
+ /**
+ * Retrieve the response headers from the given NetworkEventActor.
+ *
+ * @param string aActor
+ * The NetworkEventActor ID.
+ * @param function aOnResponse
+ * The function invoked when the response is received.
+ */
+ getResponseHeaders: function WCC_getResponseHeaders(aActor, aOnResponse) {
+ var packet = {
+ to: aActor,
+ type: "getResponseHeaders"
+ };
+ this._client.request(packet, aOnResponse);
+ },
+
+ /**
+ * Retrieve the response cookies from the given NetworkEventActor.
+ *
+ * @param string aActor
+ * The NetworkEventActor ID.
+ * @param function aOnResponse
+ * The function invoked when the response is received.
+ */
+ getResponseCookies: function WCC_getResponseCookies(aActor, aOnResponse) {
+ var packet = {
+ to: aActor,
+ type: "getResponseCookies"
+ };
+ this._client.request(packet, aOnResponse);
+ },
+
+ /**
+ * Retrieve the response content from the given NetworkEventActor.
+ *
+ * @param string aActor
+ * The NetworkEventActor ID.
+ * @param function aOnResponse
+ * The function invoked when the response is received.
+ */
+ getResponseContent: function WCC_getResponseContent(aActor, aOnResponse) {
+ var packet = {
+ to: aActor,
+ type: "getResponseContent"
+ };
+ this._client.request(packet, aOnResponse);
+ },
+
+ /**
+ * Retrieve the timing information for the given NetworkEventActor.
+ *
+ * @param string aActor
+ * The NetworkEventActor ID.
+ * @param function aOnResponse
+ * The function invoked when the response is received.
+ */
+ getEventTimings: function WCC_getEventTimings(aActor, aOnResponse) {
+ var packet = {
+ to: aActor,
+ type: "getEventTimings"
+ };
+ this._client.request(packet, aOnResponse);
+ },
+
+ /**
+ * Retrieve the security information for the given NetworkEventActor.
+ *
+ * @param string aActor
+ * The NetworkEventActor ID.
+ * @param function aOnResponse
+ * The function invoked when the response is received.
+ */
+ getSecurityInfo: function WCC_getSecurityInfo(aActor, aOnResponse) {
+ var packet = {
+ to: aActor,
+ type: "getSecurityInfo"
+ };
+ this._client.request(packet, aOnResponse);
+ },
+
+ /**
+ * Send a HTTP request with the given data.
+ *
+ * @param string aData
+ * The details of the HTTP request.
+ * @param function aOnResponse
+ * The function invoked when the response is received.
+ */
+ sendHTTPRequest: function WCC_sendHTTPRequest(aData, aOnResponse) {
+ var packet = {
+ to: this._actor,
+ type: "sendHTTPRequest",
+ request: aData
+ };
+ this._client.request(packet, aOnResponse);
+ },
+
+ /**
+ * Start the given Web Console listeners.
+ *
+ * @see this.LISTENERS
+ * @param array aListeners
+ * Array of listeners you want to start. See this.LISTENERS for
+ * known listeners.
+ * @param function aOnResponse
+ * Function to invoke when the server response is received.
+ */
+ startListeners: function WCC_startListeners(aListeners, aOnResponse) {
+ var packet = {
+ to: this._actor,
+ type: "startListeners",
+ listeners: aListeners
+ };
+ this._client.request(packet, aOnResponse);
+ },
+
+ /**
+ * Stop the given Web Console listeners.
+ *
+ * @see this.LISTENERS
+ * @param array aListeners
+ * Array of listeners you want to stop. See this.LISTENERS for
+ * known listeners.
+ * @param function aOnResponse
+ * Function to invoke when the server response is received.
+ */
+ stopListeners: function WCC_stopListeners(aListeners, aOnResponse) {
+ var packet = {
+ to: this._actor,
+ type: "stopListeners",
+ listeners: aListeners
+ };
+ this._client.request(packet, aOnResponse);
+ },
+
+ /**
+ * Return an instance of LongStringClient for the given long string grip.
+ *
+ * @param object aGrip
+ * The long string grip returned by the protocol.
+ * @return object
+ * The LongStringClient for the given long string grip.
+ */
+ longString: function WCC_longString(aGrip) {
+ if (aGrip.actor in this._longStrings) {
+ return this._longStrings[aGrip.actor];
+ }
+
+ var client = new LongStringClient(this._client, aGrip);
+ this._longStrings[aGrip.actor] = client;
+ return client;
+ },
+
+ /**
+ * Close the WebConsoleClient. This stops all the listeners on the server and
+ * detaches from the console actor.
+ *
+ * @param function aOnResponse
+ * Function to invoke when the server response is received.
+ */
+ detach: function WCC_detach(aOnResponse) {
+ this._client.removeListener("evaluationResult", this.onEvaluationResult);
+ this._client.removeListener("networkEvent", this.onNetworkEvent);
+ this._client.removeListener("networkEventUpdate", this.onNetworkEventUpdate);
+ this.stopListeners(null, aOnResponse);
+ this._longStrings = null;
+ this._client = null;
+ this.pendingEvaluationResults.clear();
+ this.pendingEvaluationResults = null;
+ this.clearNetworkRequests();
+ this._networkRequests = null;
+ },
+
+ clearNetworkRequests: function () {
+ this._networkRequests.clear();
+ },
+
+ /**
+ * Fetches the full text of a LongString.
+ *
+ * @param object | string stringGrip
+ * The long string grip containing the corresponding actor.
+ * If you pass in a plain string (by accident or because you're lazy),
+ * then a promise of the same string is simply returned.
+ * @return object Promise
+ * A promise that is resolved when the full string contents
+ * are available, or rejected if something goes wrong.
+ */
+ getString: function (stringGrip) {
+ // Make sure this is a long string.
+ if (typeof stringGrip != "object" || stringGrip.type != "longString") {
+ return promise.resolve(stringGrip); // Go home string, you're drunk.
+ }
+
+ // Fetch the long string only once.
+ if (stringGrip._fullText) {
+ return stringGrip._fullText.promise;
+ }
+
+ var deferred = stringGrip._fullText = promise.defer();
+ var actor = stringGrip.actor;
+ var initial = stringGrip.initial;
+ var length = stringGrip.length;
+
+ var longStringClient = this.longString(stringGrip);
+
+ longStringClient.substring(initial.length, length, aResponse => {
+ if (aResponse.error) {
+ DevToolsUtils.reportException("getString", aResponse.error + ": " + aResponse.message);
+
+ deferred.reject(aResponse);
+ return;
+ }
+ deferred.resolve(initial + aResponse.substring);
+ });
+
+ return deferred.promise;
+ }
+ };
+
+/***/ },
+/* 83 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /* 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/. */
+
+ const { Services } = __webpack_require__(30);
+ const EventEmitter = __webpack_require__(60);
+
+ /**
+ * Shortcuts for lazily accessing and setting various preferences.
+ * Usage:
+ * let prefs = new Prefs("root.path.to.branch", {
+ * myIntPref: ["Int", "leaf.path.to.my-int-pref"],
+ * myCharPref: ["Char", "leaf.path.to.my-char-pref"],
+ * myJsonPref: ["Json", "leaf.path.to.my-json-pref"],
+ * myFloatPref: ["Float", "leaf.path.to.my-float-pref"]
+ * ...
+ * });
+ *
+ * Get/set:
+ * prefs.myCharPref = "foo";
+ * let aux = prefs.myCharPref;
+ *
+ * Observe:
+ * prefs.registerObserver();
+ * prefs.on("pref-changed", (prefName, prefValue) => {
+ * ...
+ * });
+ *
+ * @param string prefsRoot
+ * The root path to the required preferences branch.
+ * @param object prefsBlueprint
+ * An object containing { accessorName: [prefType, prefName] } keys.
+ */
+ function PrefsHelper(prefsRoot = "", prefsBlueprint = {}) {
+ EventEmitter.decorate(this);
+
+ let cache = new Map();
+
+ for (let accessorName in prefsBlueprint) {
+ let [prefType, prefName] = prefsBlueprint[accessorName];
+ map(this, cache, accessorName, prefType, prefsRoot, prefName);
+ }
+
+ let observer = makeObserver(this, cache, prefsRoot, prefsBlueprint);
+ this.registerObserver = () => observer.register();
+ this.unregisterObserver = () => observer.unregister();
+ }
+
+ /**
+ * Helper method for getting a pref value.
+ *
+ * @param Map cache
+ * @param string prefType
+ * @param string prefsRoot
+ * @param string prefName
+ * @return any
+ */
+ function get(cache, prefType, prefsRoot, prefName) {
+ let cachedPref = cache.get(prefName);
+ if (cachedPref !== undefined) {
+ return cachedPref;
+ }
+ let value = Services.prefs["get" + prefType + "Pref"](
+ [prefsRoot, prefName].join(".")
+ );
+ cache.set(prefName, value);
+ return value;
+ }
+
+ /**
+ * Helper method for setting a pref value.
+ *
+ * @param Map cache
+ * @param string prefType
+ * @param string prefsRoot
+ * @param string prefName
+ * @param any value
+ */
+ function set(cache, prefType, prefsRoot, prefName, value) {
+ Services.prefs["set" + prefType + "Pref"](
+ [prefsRoot, prefName].join("."),
+ value
+ );
+ cache.set(prefName, value);
+ }
+
+ /**
+ * Maps a property name to a pref, defining lazy getters and setters.
+ * Supported types are "Bool", "Char", "Int", "Float" (sugar around "Char"
+ * type and casting), and "Json" (which is basically just sugar for "Char"
+ * using the standard JSON serializer).
+ *
+ * @param PrefsHelper self
+ * @param Map cache
+ * @param string accessorName
+ * @param string prefType
+ * @param string prefsRoot
+ * @param string prefName
+ * @param array serializer [optional]
+ */
+ function map(self, cache, accessorName, prefType, prefsRoot, prefName,
+ serializer = { in: e => e, out: e => e }) {
+ if (prefName in self) {
+ throw new Error(`Can't use ${prefName} because it overrides a property` +
+ "on the instance.");
+ }
+ if (prefType == "Json") {
+ map(self, cache, accessorName, "Char", prefsRoot, prefName, {
+ in: JSON.parse,
+ out: JSON.stringify
+ });
+ return;
+ }
+ if (prefType == "Float") {
+ map(self, cache, accessorName, "Char", prefsRoot, prefName, {
+ in: Number.parseFloat,
+ out: (n) => n + ""
+ });
+ return;
+ }
+
+ Object.defineProperty(self, accessorName, {
+ get: () => serializer.in(get(cache, prefType, prefsRoot, prefName)),
+ set: (e) => set(cache, prefType, prefsRoot, prefName, serializer.out(e))
+ });
+ }
+
+ /**
+ * Finds the accessor for the provided pref, based on the blueprint object
+ * used in the constructor.
+ *
+ * @param PrefsHelper self
+ * @param object prefsBlueprint
+ * @return string
+ */
+ function accessorNameForPref(somePrefName, prefsBlueprint) {
+ for (let accessorName in prefsBlueprint) {
+ let [, prefName] = prefsBlueprint[accessorName];
+ if (somePrefName == prefName) {
+ return accessorName;
+ }
+ }
+ return "";
+ }
+
+ /**
+ * Creates a pref observer for `self`.
+ *
+ * @param PrefsHelper self
+ * @param Map cache
+ * @param string prefsRoot
+ * @param object prefsBlueprint
+ * @return object
+ */
+ function makeObserver(self, cache, prefsRoot, prefsBlueprint) {
+ return {
+ register: function() {
+ this._branch = Services.prefs.getBranch(prefsRoot + ".");
+ this._branch.addObserver("", this, false);
+ },
+ unregister: function() {
+ this._branch.removeObserver("", this);
+ },
+ observe: function(subject, topic, prefName) {
+ // If this particular pref isn't handled by the blueprint object,
+ // even though it's in the specified branch, ignore it.
+ let accessorName = accessorNameForPref(prefName, prefsBlueprint);
+ if (!(accessorName in self)) {
+ return;
+ }
+ cache.delete(prefName);
+ self.emit("pref-changed", accessorName, self[accessorName]);
+ }
+ };
+ }
+
+ exports.PrefsHelper = PrefsHelper;
+
+
+/***/ },
+/* 84 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /* 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 promise = __webpack_require__(66);
+ var EventEmitter = __webpack_require__(60);
+
+ /* const { DebuggerServer } = require("../../server/main");*/
+
+ var _require = __webpack_require__(79);
+
+ var DebuggerClient = _require.DebuggerClient;
+
+
+ var targets = new WeakMap();
+ var promiseTargets = new WeakMap();
+
+ /**
+ * Functions for creating Targets
+ */
+ exports.TargetFactory = {
+ /**
+ * Construct a Target
+ * @param {XULTab} tab
+ * The tab to use in creating a new target.
+ *
+ * @return A target object
+ */
+ forTab: function (tab) {
+ var target = targets.get(tab);
+ if (target == null) {
+ target = new TabTarget(tab);
+ targets.set(tab, target);
+ }
+ return target;
+ },
+
+ /**
+ * Return a promise of a Target for a remote tab.
+ * @param {Object} options
+ * The options object has the following properties:
+ * {
+ * form: the remote protocol form of a tab,
+ * client: a DebuggerClient instance
+ * (caller owns this and is responsible for closing),
+ * chrome: true if the remote target is the whole process
+ * }
+ *
+ * @return A promise of a target object
+ */
+ forRemoteTab: function (options) {
+ var targetPromise = promiseTargets.get(options);
+ if (targetPromise == null) {
+ (function () {
+ var target = new TabTarget(options);
+ targetPromise = target.makeRemote().then(() => target);
+ promiseTargets.set(options, targetPromise);
+ })();
+ }
+ return targetPromise;
+ },
+
+ forWorker: function (workerClient) {
+ var target = targets.get(workerClient);
+ if (target == null) {
+ target = new WorkerTarget(workerClient);
+ targets.set(workerClient, target);
+ }
+ return target;
+ },
+
+ /**
+ * Creating a target for a tab that is being closed is a problem because it
+ * allows a leak as a result of coming after the close event which normally
+ * clears things up. This function allows us to ask if there is a known
+ * target for a tab without creating a target
+ * @return true/false
+ */
+ isKnownTab: function (tab) {
+ return targets.has(tab);
+ }
+ };
+
+ /**
+ * A Target represents something that we can debug. Targets are generally
+ * read-only. Any changes that you wish to make to a target should be done via
+ * a Tool that attaches to the target. i.e. a Target is just a pointer saying
+ * "the thing to debug is over there".
+ *
+ * Providing a generalized abstraction of a web-page or web-browser (available
+ * either locally or remotely) is beyond the scope of this class (and maybe
+ * also beyond the scope of this universe) However Target does attempt to
+ * abstract some common events and read-only properties common to many Tools.
+ *
+ * Supported read-only properties:
+ * - name, isRemote, url
+ *
+ * Target extends EventEmitter and provides support for the following events:
+ * - close: The target window has been closed. All tools attached to this
+ * target should close. This event is not currently cancelable.
+ * - navigate: The target window has navigated to a different URL
+ *
+ * Optional events:
+ * - will-navigate: The target window will navigate to a different URL
+ * - hidden: The target is not visible anymore (for TargetTab, another tab is
+ * selected)
+ * - visible: The target is visible (for TargetTab, tab is selected)
+ *
+ * Comparing Targets: 2 instances of a Target object can point at the same
+ * thing, so t1 !== t2 and t1 != t2 even when they represent the same object.
+ * To compare to targets use 't1.equals(t2)'.
+ */
+
+ /**
+ * A TabTarget represents a page living in a browser tab. Generally these will
+ * be web pages served over http(s), but they don't have to be.
+ */
+ function TabTarget(tab) {
+ EventEmitter.decorate(this);
+ this.destroy = this.destroy.bind(this);
+ this._handleThreadState = this._handleThreadState.bind(this);
+ this.on("thread-resumed", this._handleThreadState);
+ this.on("thread-paused", this._handleThreadState);
+ this.activeTab = this.activeConsole = null;
+ // Only real tabs need initialization here. Placeholder objects for remote
+ // targets will be initialized after a makeRemote method call.
+ if (tab && !["client", "form", "chrome"].every(tab.hasOwnProperty, tab)) {
+ this._tab = tab;
+ this._setupListeners();
+ } else {
+ this._form = tab.form;
+ this._client = tab.client;
+ this._chrome = tab.chrome;
+ }
+ // Default isTabActor to true if not explicitly specified
+ if (typeof tab.isTabActor == "boolean") {
+ this._isTabActor = tab.isTabActor;
+ } else {
+ this._isTabActor = true;
+ }
+ }
+
+ TabTarget.prototype = {
+ _webProgressListener: null,
+
+ /**
+ * Returns a promise for the protocol description from the root actor. Used
+ * internally with `target.actorHasMethod`. Takes advantage of caching if
+ * definition was fetched previously with the corresponding actor information.
+ * Actors are lazily loaded, so not only must the tool using a specific actor
+ * be in use, the actors are only registered after invoking a method (for
+ * performance reasons, added in bug 988237), so to use these actor detection
+ * methods, one must already be communicating with a specific actor of that
+ * type.
+ *
+ * Must be a remote target.
+ *
+ * @return {Promise}
+ * {
+ * "category": "actor",
+ * "typeName": "longstractor",
+ * "methods": [{
+ * "name": "substring",
+ * "request": {
+ * "type": "substring",
+ * "start": {
+ * "_arg": 0,
+ * "type": "primitive"
+ * },
+ * "end": {
+ * "_arg": 1,
+ * "type": "primitive"
+ * }
+ * },
+ * "response": {
+ * "substring": {
+ * "_retval": "primitive"
+ * }
+ * }
+ * }],
+ * "events": {}
+ * }
+ */
+ getActorDescription: function (actorName) {
+ if (!this.client) {
+ throw new Error("TabTarget#getActorDescription() can only be called on " + "remote tabs.");
+ }
+
+ var deferred = promise.defer();
+
+ if (this._protocolDescription && this._protocolDescription.types[actorName]) {
+ deferred.resolve(this._protocolDescription.types[actorName]);
+ } else {
+ this.client.mainRoot.protocolDescription(description => {
+ this._protocolDescription = description;
+ deferred.resolve(description.types[actorName]);
+ });
+ }
+
+ return deferred.promise;
+ },
+
+ /**
+ * Returns a boolean indicating whether or not the specific actor
+ * type exists. Must be a remote target.
+ *
+ * @param {String} actorName
+ * @return {Boolean}
+ */
+ hasActor: function (actorName) {
+ if (!this.client) {
+ throw new Error("TabTarget#hasActor() can only be called on remote " + "tabs.");
+ }
+ if (this.form) {
+ return !!this.form[actorName + "Actor"];
+ }
+ return false;
+ },
+
+ /**
+ * Queries the protocol description to see if an actor has
+ * an available method. The actor must already be lazily-loaded (read
+ * the restrictions in the `getActorDescription` comments),
+ * so this is for use inside of tool. Returns a promise that
+ * resolves to a boolean. Must be a remote target.
+ *
+ * @param {String} actorName
+ * @param {String} methodName
+ * @return {Promise}
+ */
+ actorHasMethod: function (actorName, methodName) {
+ if (!this.client) {
+ throw new Error("TabTarget#actorHasMethod() can only be called on " + "remote tabs.");
+ }
+ return this.getActorDescription(actorName).then(desc => {
+ if (desc && desc.methods) {
+ return !!desc.methods.find(method => method.name === methodName);
+ }
+ return false;
+ });
+ },
+
+ /**
+ * Returns a trait from the root actor.
+ *
+ * @param {String} traitName
+ * @return {Mixed}
+ */
+ getTrait: function (traitName) {
+ if (!this.client) {
+ throw new Error("TabTarget#getTrait() can only be called on remote " + "tabs.");
+ }
+
+ // If the targeted actor exposes traits and has a defined value for this
+ // traits, override the root actor traits
+ if (this.form.traits && traitName in this.form.traits) {
+ return this.form.traits[traitName];
+ }
+
+ return this.client.traits[traitName];
+ },
+
+ get tab() {
+ return this._tab;
+ },
+
+ get form() {
+ return this._form;
+ },
+
+ // Get a promise of the root form returned by a listTabs request. This promise
+ // is cached.
+ get root() {
+ if (!this._root) {
+ this._root = this._getRoot();
+ }
+ return this._root;
+ },
+
+ _getRoot: function () {
+ return new Promise((resolve, reject) => {
+ this.client.listTabs(response => {
+ if (response.error) {
+ reject(new Error(response.error + ": " + response.message));
+ return;
+ }
+
+ resolve(response);
+ });
+ });
+ },
+
+ get client() {
+ return this._client;
+ },
+
+ // Tells us if we are debugging content document
+ // or if we are debugging chrome stuff.
+ // Allows to controls which features are available against
+ // a chrome or a content document.
+ get chrome() {
+ return this._chrome;
+ },
+
+ // Tells us if the related actor implements TabActor interface
+ // and requires to call `attach` request before being used
+ // and `detach` during cleanup
+ get isTabActor() {
+ return this._isTabActor;
+ },
+
+ get window() {
+ // XXX - this is a footgun for e10s - there .contentWindow will be null,
+ // and even though .contentWindowAsCPOW *might* work, it will not work
+ // in all contexts. Consumers of .window need to be refactored to not
+ // rely on this.
+ // if (Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) {
+ // console.error("The .window getter on devtools' |target| object isn't " +
+ // "e10s friendly!\n" + Error().stack);
+ // }
+ // Be extra careful here, since this may be called by HS_getHudByWindow
+ // during shutdown.
+ if (this._tab && this._tab.linkedBrowser) {
+ return this._tab.linkedBrowser.contentWindow;
+ }
+ return null;
+ },
+
+ get name() {
+ if (this._tab && this._tab.linkedBrowser.contentDocument) {
+ return this._tab.linkedBrowser.contentDocument.title;
+ }
+ if (this.isAddon) {
+ return this._form.name;
+ }
+ return this._form.title;
+ },
+
+ get url() {
+ return this._tab ? this._tab.linkedBrowser.currentURI.spec : this._form.url;
+ },
+
+ get isRemote() {
+ return !this.isLocalTab;
+ },
+
+ get isAddon() {
+ return !!(this._form && this._form.actor && this._form.actor.match(/conn\d+\.addon\d+/));
+ },
+
+ get isLocalTab() {
+ return !!this._tab;
+ },
+
+ get isMultiProcess() {
+ return !this.window;
+ },
+
+ get isThreadPaused() {
+ return !!this._isThreadPaused;
+ },
+
+ /**
+ * Adds remote protocol capabilities to the target, so that it can be used
+ * for tools that support the Remote Debugging Protocol even for local
+ * connections.
+ */
+ makeRemote: function () {
+ if (this._remote) {
+ return this._remote.promise;
+ }
+
+ this._remote = promise.defer();
+
+ if (this.isLocalTab) {
+ // Since a remote protocol connection will be made, let's start the
+ // DebuggerServer here, once and for all tools.
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+
+ this._client = new DebuggerClient(DebuggerServer.connectPipe());
+ // A local TabTarget will never perform chrome debugging.
+ this._chrome = false;
+ }
+
+ this._setupRemoteListeners();
+
+ var attachTab = () => {
+ this._client.attachTab(this._form.actor, (response, tabClient) => {
+ if (!tabClient) {
+ this._remote.reject("Unable to attach to the tab");
+ return;
+ }
+ this.activeTab = tabClient;
+ this.threadActor = response.threadActor;
+ attachConsole();
+ });
+ };
+
+ var onConsoleAttached = (response, consoleClient) => {
+ if (!consoleClient) {
+ this._remote.reject("Unable to attach to the console");
+ return;
+ }
+ this.activeConsole = consoleClient;
+ this._remote.resolve(null);
+ };
+
+ var attachConsole = () => {
+ this._client.attachConsole(this._form.consoleActor, ["NetworkActivity"], onConsoleAttached);
+ };
+
+ if (this.isLocalTab) {
+ this._client.connect(() => {
+ this._client.getTab({ tab: this.tab }).then(response => {
+ this._form = response.tab;
+ attachTab();
+ });
+ });
+ } else if (this.isTabActor) {
+ // In the remote debugging case, the protocol connection will have been
+ // already initialized in the connection screen code.
+ attachTab();
+ } else {
+ // AddonActor and chrome debugging on RootActor doesn't inherits from
+ // TabActor and doesn't need to be attached.
+ attachConsole();
+ }
+
+ return this._remote.promise;
+ },
+
+ /**
+ * Listen to the different events.
+ */
+ _setupListeners: function () {
+ this._webProgressListener = new TabWebProgressListener(this);
+ this.tab.linkedBrowser.addProgressListener(this._webProgressListener);
+ this.tab.addEventListener("TabClose", this);
+ this.tab.parentNode.addEventListener("TabSelect", this);
+ this.tab.ownerDocument.defaultView.addEventListener("unload", this);
+ },
+
+ /**
+ * Teardown event listeners.
+ */
+ _teardownListeners: function () {
+ if (this._webProgressListener) {
+ this._webProgressListener.destroy();
+ }
+
+ this._tab.ownerDocument.defaultView.removeEventListener("unload", this);
+ this._tab.removeEventListener("TabClose", this);
+ this._tab.parentNode.removeEventListener("TabSelect", this);
+ },
+
+ /**
+ * Setup listeners for remote debugging, updating existing ones as necessary.
+ */
+ _setupRemoteListeners: function () {
+ this.client.addListener("closed", this.destroy);
+
+ this._onTabDetached = (aType, aPacket) => {
+ // We have to filter message to ensure that this detach is for this tab
+ if (aPacket.from == this._form.actor) {
+ this.destroy();
+ }
+ };
+ this.client.addListener("tabDetached", this._onTabDetached);
+
+ this._onTabNavigated = (aType, aPacket) => {
+ var event = Object.create(null);
+ event.url = aPacket.url;
+ event.title = aPacket.title;
+ event.nativeConsoleAPI = aPacket.nativeConsoleAPI;
+ event.isFrameSwitching = aPacket.isFrameSwitching;
+ // Send any stored event payload (DOMWindow or nsIRequest) for backwards
+ // compatibility with non-remotable tools.
+ if (aPacket.state == "start") {
+ event._navPayload = this._navRequest;
+ this.emit("will-navigate", event);
+ this._navRequest = null;
+ } else {
+ event._navPayload = this._navWindow;
+ this.emit("navigate", event);
+ this._navWindow = null;
+ }
+ };
+ this.client.addListener("tabNavigated", this._onTabNavigated);
+
+ this._onFrameUpdate = (aType, aPacket) => {
+ this.emit("frame-update", aPacket);
+ };
+ this.client.addListener("frameUpdate", this._onFrameUpdate);
+ },
+
+ /**
+ * Teardown listeners for remote debugging.
+ */
+ _teardownRemoteListeners: function () {
+ this.client.removeListener("closed", this.destroy);
+ this.client.removeListener("tabNavigated", this._onTabNavigated);
+ this.client.removeListener("tabDetached", this._onTabDetached);
+ this.client.removeListener("frameUpdate", this._onFrameUpdate);
+ },
+
+ /**
+ * Handle tabs events.
+ */
+ handleEvent: function (event) {
+ switch (event.type) {
+ case "TabClose":
+ case "unload":
+ this.destroy();
+ break;
+ case "TabSelect":
+ if (this.tab.selected) {
+ this.emit("visible", event);
+ } else {
+ this.emit("hidden", event);
+ }
+ break;
+ }
+ },
+
+ /**
+ * Handle script status.
+ */
+ _handleThreadState: function (event) {
+ switch (event) {
+ case "thread-resumed":
+ this._isThreadPaused = false;
+ break;
+ case "thread-paused":
+ this._isThreadPaused = true;
+ break;
+ }
+ },
+
+ /**
+ * Target is not alive anymore.
+ */
+ destroy: function () {
+ // If several things call destroy then we give them all the same
+ // destruction promise so we're sure to destroy only once
+ if (this._destroyer) {
+ return this._destroyer.promise;
+ }
+
+ this._destroyer = promise.defer();
+
+ // Before taking any action, notify listeners that destruction is imminent.
+ this.emit("close");
+
+ // First of all, do cleanup tasks that pertain to both remoted and
+ // non-remoted targets.
+ this.off("thread-resumed", this._handleThreadState);
+ this.off("thread-paused", this._handleThreadState);
+
+ if (this._tab) {
+ this._teardownListeners();
+ }
+
+ var cleanupAndResolve = () => {
+ this._cleanup();
+ this._destroyer.resolve(null);
+ };
+ // If this target was not remoted, the promise will be resolved before the
+ // function returns.
+ if (this._tab && !this._client) {
+ cleanupAndResolve();
+ } else if (this._client) {
+ // If, on the other hand, this target was remoted, the promise will be
+ // resolved after the remote connection is closed.
+ this._teardownRemoteListeners();
+
+ if (this.isLocalTab) {
+ // We started with a local tab and created the client ourselves, so we
+ // should close it.
+ this._client.close(cleanupAndResolve);
+ } else if (this.activeTab) {
+ // The client was handed to us, so we are not responsible for closing
+ // it. We just need to detach from the tab, if already attached.
+ // |detach| may fail if the connection is already dead, so proceed with
+ // cleanup directly after this.
+ this.activeTab.detach();
+ cleanupAndResolve();
+ } else {
+ cleanupAndResolve();
+ }
+ }
+
+ return this._destroyer.promise;
+ },
+
+ /**
+ * Clean up references to what this target points to.
+ */
+ _cleanup: function () {
+ if (this._tab) {
+ targets.delete(this._tab);
+ } else {
+ promiseTargets.delete(this._form);
+ }
+ this.activeTab = null;
+ this.activeConsole = null;
+ this._client = null;
+ this._tab = null;
+ this._form = null;
+ this._remote = null;
+ },
+
+ toString: function () {
+ var id = this._tab ? this._tab : this._form && this._form.actor;
+ return `TabTarget:${ id }`;
+ }
+ };
+
+ function WorkerTarget(workerClient) {
+ EventEmitter.decorate(this);
+ this._workerClient = workerClient;
+ }
+
+ /**
+ * A WorkerTarget represents a worker. Unlike TabTarget, which can represent
+ * either a local or remote tab, WorkerTarget always represents a remote worker.
+ * Moreover, unlike TabTarget, which is constructed with a placeholder object
+ * for remote tabs (from which a TabClient can then be lazily obtained),
+ * WorkerTarget is constructed with a WorkerClient directly.
+ *
+ * WorkerClient is designed to mimic the interface of TabClient as closely as
+ * possible. This allows us to debug workers as if they were ordinary tabs,
+ * requiring only minimal changes to the rest of the frontend.
+ */
+ WorkerTarget.prototype = {
+ destroy: function () {},
+
+ get isRemote() {
+ return true;
+ },
+
+ get isTabActor() {
+ return true;
+ },
+
+ get url() {
+ return this._workerClient.url;
+ },
+
+ get isWorkerTarget() {
+ return true;
+ },
+
+ get form() {
+ return {
+ consoleActor: this._workerClient.consoleActor
+ };
+ },
+
+ get activeTab() {
+ return this._workerClient;
+ },
+
+ get client() {
+ return this._workerClient.client;
+ },
+
+ destroy: function () {},
+
+ hasActor: function (name) {
+ return false;
+ },
+
+ getTrait: function () {
+ return undefined;
+ },
+
+ makeRemote: function () {
+ return Promise.resolve();
+ }
+ };
+
+/***/ },
+/* 85 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /* 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 EventEmitter = __webpack_require__(60);
+
+ function WebSocketDebuggerTransport(socket) {
+ EventEmitter.decorate(this);
+
+ this.active = false;
+ this.hooks = null;
+ this.socket = socket;
+ }
+
+ WebSocketDebuggerTransport.prototype = {
+ ready() {
+ if (this.active) {
+ return;
+ }
+
+ this.socket.addEventListener("message", this);
+ this.socket.addEventListener("close", this);
+
+ this.active = true;
+ },
+
+ send(object) {
+ this.emit("send", object);
+ if (this.socket) {
+ this.socket.send(JSON.stringify(object));
+ }
+ },
+
+ startBulkSend() {
+ throw new Error("Bulk send is not supported by WebSocket transport");
+ },
+
+ close() {
+ this.emit("close");
+ this.active = false;
+
+ this.socket.removeEventListener("message", this);
+ this.socket.removeEventListener("close", this);
+ this.socket.close();
+ this.socket = null;
+
+ if (this.hooks) {
+ this.hooks.onClosed();
+ this.hooks = null;
+ }
+ },
+
+ handleEvent(event) {
+ switch (event.type) {
+ case "message":
+ this.onMessage(event);
+ break;
+ case "close":
+ this.close();
+ break;
+ }
+ },
+
+ onMessage(_ref) {
+ var data = _ref.data;
+
+ if (typeof data !== "string") {
+ throw new Error("Binary messages are not supported by WebSocket transport");
+ }
+
+ var object = JSON.parse(data);
+ this.emit("packet", object);
+ if (this.hooks) {
+ this.hooks.onPacket(object);
+ }
+ }
+ };
+
+ module.exports = WebSocketDebuggerTransport;
+
+/***/ },
+/* 86 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /* -*- 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";
+
+ var EventEmitter = __webpack_require__(60);
+
+ /**
+ * A partial implementation of the Menu API provided by electron:
+ * https://github.com/electron/electron/blob/master/docs/api/menu.md.
+ *
+ * Extra features:
+ * - Emits an 'open' and 'close' event when the menu is opened/closed
+
+ * @param String id (non standard)
+ * Needed so tests can confirm the XUL implementation is working
+ */
+ function Menu() {
+ var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
+
+ var _ref$id = _ref.id;
+ var id = _ref$id === undefined ? null : _ref$id;
+
+ this.menuitems = [];
+ this.id = id;
+
+ Object.defineProperty(this, "items", {
+ get() {
+ return this.menuitems;
+ }
+ });
+
+ EventEmitter.decorate(this);
+ }
+
+ /**
+ * Add an item to the end of the Menu
+ *
+ * @param {MenuItem} menuItem
+ */
+ Menu.prototype.append = function (menuItem) {
+ this.menuitems.push(menuItem);
+ };
+
+ /**
+ * Add an item to a specified position in the menu
+ *
+ * @param {int} pos
+ * @param {MenuItem} menuItem
+ */
+ Menu.prototype.insert = function (pos, menuItem) {
+ throw Error("Not implemented");
+ };
+
+ /**
+ * Show the Menu at a specified location on the screen
+ *
+ * Missing features:
+ * - browserWindow - BrowserWindow (optional) - Default is null.
+ * - positioningItem Number - (optional) OS X
+ *
+ * @param {int} screenX
+ * @param {int} screenY
+ * @param Toolbox toolbox (non standard)
+ * Needed so we in which window to inject XUL
+ */
+ Menu.prototype.popup = function (screenX, screenY, toolbox) {
+ var doc = toolbox.doc;
+ var popupset = doc.querySelector("popupset");
+ // See bug 1285229, on Windows, opening the same popup multiple times in a
+ // row ends up duplicating the popup. The newly inserted popup doesn't
+ // dismiss the old one. So remove any previously displayed popup before
+ // opening a new one.
+ var popup = popupset.querySelector("menupopup[menu-api=\"true\"]");
+ if (popup) {
+ popup.hidePopup();
+ }
+
+ popup = this.createPopup(doc);
+ popup.setAttribute("menu-api", "true");
+
+ if (this.id) {
+ popup.id = this.id;
+ }
+ this._createMenuItems(popup);
+
+ // Remove the menu from the DOM once it's hidden.
+ popup.addEventListener("popuphidden", e => {
+ if (e.target === popup) {
+ popup.remove();
+ this.emit("close", popup);
+ }
+ });
+
+ popup.addEventListener("popupshown", e => {
+ if (e.target === popup) {
+ this.emit("open", popup);
+ }
+ });
+
+ popupset.appendChild(popup);
+ popup.openPopupAtScreen(screenX, screenY, true);
+ };
+
+ Menu.prototype.createPopup = function (doc) {
+ return doc.createElement("menupopup");
+ };
+
+ Menu.prototype._createMenuItems = function (parent) {
+ var doc = parent.ownerDocument;
+ this.menuitems.forEach(item => {
+ if (!item.visible) {
+ return;
+ }
+
+ if (item.submenu) {
+ var menupopup = doc.createElement("menupopup");
+ item.submenu._createMenuItems(menupopup);
+
+ var menu = doc.createElement("menu");
+ menu.appendChild(menupopup);
+ menu.setAttribute("label", item.label);
+ if (item.disabled) {
+ menu.setAttribute("disabled", "true");
+ }
+ if (item.accesskey) {
+ menu.setAttribute("accesskey", item.accesskey);
+ }
+ if (item.id) {
+ menu.id = item.id;
+ }
+ parent.appendChild(menu);
+ } else if (item.type === "separator") {
+ var menusep = doc.createElement("menuseparator");
+ parent.appendChild(menusep);
+ } else {
+ var menuitem = doc.createElement("menuitem");
+ menuitem.setAttribute("label", item.label);
+ menuitem.textContent = item.label;
+ menuitem.addEventListener("command", () => item.click());
+
+ if (item.type === "checkbox") {
+ menuitem.setAttribute("type", "checkbox");
+ }
+ if (item.type === "radio") {
+ menuitem.setAttribute("type", "radio");
+ }
+ if (item.disabled) {
+ menuitem.setAttribute("disabled", "true");
+ }
+ if (item.checked) {
+ menuitem.setAttribute("checked", "true");
+ }
+ if (item.accesskey) {
+ menuitem.setAttribute("accesskey", item.accesskey);
+ }
+ if (item.id) {
+ menuitem.id = item.id;
+ }
+
+ parent.appendChild(menuitem);
+ }
+ });
+ };
+
+ Menu.setApplicationMenu = () => {
+ throw Error("Not implemented");
+ };
+
+ Menu.sendActionToFirstResponder = () => {
+ throw Error("Not implemented");
+ };
+
+ Menu.buildFromTemplate = () => {
+ throw Error("Not implemented");
+ };
+
+ module.exports = Menu;
+
+/***/ },
+/* 87 */
+/***/ function(module, exports) {
+
+ /* -*- 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";
+
+ /**
+ * A partial implementation of the MenuItem API provided by electron:
+ * https://github.com/electron/electron/blob/master/docs/api/menu-item.md.
+ *
+ * Missing features:
+ * - id String - Unique within a single menu. If defined then it can be used
+ * as a reference to this item by the position attribute.
+ * - role String - Define the action of the menu item; when specified the
+ * click property will be ignored
+ * - sublabel String
+ * - accelerator Accelerator
+ * - icon NativeImage
+ * - position String - This field allows fine-grained definition of the
+ * specific location within a given menu.
+ *
+ * Implemented features:
+ * @param Object options
+ * Function click
+ * Will be called with click(menuItem, browserWindow) when the menu item
+ * is clicked
+ * String type
+ * Can be normal, separator, submenu, checkbox or radio
+ * String label
+ * Boolean enabled
+ * If false, the menu item will be greyed out and unclickable.
+ * Boolean checked
+ * Should only be specified for checkbox or radio type menu items.
+ * Menu submenu
+ * Should be specified for submenu type menu items. If submenu is specified,
+ * the type: 'submenu' can be omitted. If the value is not a Menu then it
+ * will be automatically converted to one using Menu.buildFromTemplate.
+ * Boolean visible
+ * If false, the menu item will be entirely hidden.
+ */
+
+ function MenuItem() {
+ var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
+
+ var _ref$accesskey = _ref.accesskey;
+ var accesskey = _ref$accesskey === undefined ? null : _ref$accesskey;
+ var _ref$checked = _ref.checked;
+ var checked = _ref$checked === undefined ? false : _ref$checked;
+ var _ref$click = _ref.click;
+ var click = _ref$click === undefined ? () => {} : _ref$click;
+ var _ref$disabled = _ref.disabled;
+ var disabled = _ref$disabled === undefined ? false : _ref$disabled;
+ var _ref$label = _ref.label;
+ var label = _ref$label === undefined ? "" : _ref$label;
+ var _ref$id = _ref.id;
+ var id = _ref$id === undefined ? null : _ref$id;
+ var _ref$submenu = _ref.submenu;
+ var submenu = _ref$submenu === undefined ? null : _ref$submenu;
+ var _ref$type = _ref.type;
+ var type = _ref$type === undefined ? "normal" : _ref$type;
+ var _ref$visible = _ref.visible;
+ var visible = _ref$visible === undefined ? true : _ref$visible;
+
+ this.accesskey = accesskey;
+ this.checked = checked;
+ this.click = click;
+ this.disabled = disabled;
+ this.id = id;
+ this.label = label;
+ this.submenu = submenu;
+ this.type = type;
+ this.visible = visible;
+ }
+
+ module.exports = MenuItem;
+
+/***/ },
+/* 88 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var _require = __webpack_require__(89);
+
+ var isDevelopment = _require.isDevelopment;
+ var isTesting = _require.isTesting;
+
+
+ function debugGlobal(field, value) {
+ if (isDevelopment() || isTesting()) {
+ window[field] = value;
+ }
+ }
+
+ function injectGlobals(_ref) {
+ var store = _ref.store;
+
+ debugGlobal("store", store);
+ debugGlobal("injectDebuggee", __webpack_require__(140));
+ debugGlobal("serializeStore", () => {
+ return JSON.parse(JSON.stringify(store.getState()));
+ });
+ }
+
+ module.exports = {
+ debugGlobal,
+ injectGlobals
+ };
+
+/***/ },
+/* 89 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var feature = __webpack_require__(90);
+
+ module.exports = feature;
+
+/***/ },
+/* 90 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var pick = __webpack_require__(91);
+ var config = void 0;
+
+ var flag = __webpack_require__(139);
+
+ /**
+ * Gets a config value for a given key
+ * e.g "chrome.webSocketPort"
+ */
+ function getValue(key) {
+ return pick(config, key);
+ }
+
+ function isEnabled(key) {
+ return config.features[key];
+ }
+
+ function isDevelopment() {
+ if (isFirefoxPanel()) {
+ // Default to production if compiling for the Firefox panel
+ return ("production") === "development";
+ }
+ return ("production") !== "production";
+ }
+
+ function isTesting() {
+ return flag.testing;
+ }
+
+ function isFirefoxPanel() {
+ return ("firefox-panel") == "firefox-panel";
+ }
+
+ function isFirefox() {
+ return (/firefox/i.test(navigator.userAgent)
+ );
+ }
+
+ function setConfig(value) {
+ config = value;
+ }
+
+ function getConfig() {
+ return config;
+ }
+
+ module.exports = {
+ isEnabled,
+ getValue,
+ isDevelopment,
+ isTesting,
+ isFirefoxPanel,
+ isFirefox,
+ getConfig,
+ setConfig
+ };
+
+/***/ },
+/* 91 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var baseGet = __webpack_require__(92);
+
+ /**
+ * Gets the value at `path` of `object`. If the resolved value is
+ * `undefined`, the `defaultValue` is returned in its place.
+ *
+ * @static
+ * @memberOf _
+ * @since 3.7.0
+ * @category Object
+ * @param {Object} object The object to query.
+ * @param {Array|string} path The path of the property to get.
+ * @param {*} [defaultValue] The value returned for `undefined` resolved values.
+ * @returns {*} Returns the resolved value.
+ * @example
+ *
+ * var object = { 'a': [{ 'b': { 'c': 3 } }] };
+ *
+ * _.get(object, 'a[0].b.c');
+ * // => 3
+ *
+ * _.get(object, ['a', '0', 'b', 'c']);
+ * // => 3
+ *
+ * _.get(object, 'a.b.c', 'default');
+ * // => 'default'
+ */
+ function get(object, path, defaultValue) {
+ var result = object == null ? undefined : baseGet(object, path);
+ return result === undefined ? defaultValue : result;
+ }
+
+ module.exports = get;
+
+
+/***/ },
+/* 92 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var castPath = __webpack_require__(93),
+ isKey = __webpack_require__(137),
+ toKey = __webpack_require__(138);
+
+ /**
+ * The base implementation of `_.get` without support for default values.
+ *
+ * @private
+ * @param {Object} object The object to query.
+ * @param {Array|string} path The path of the property to get.
+ * @returns {*} Returns the resolved value.
+ */
+ function baseGet(object, path) {
+ path = isKey(path, object) ? [path] : castPath(path);
+
+ var index = 0,
+ length = path.length;
+
+ while (object != null && index < length) {
+ object = object[toKey(path[index++])];
+ }
+ return (index && index == length) ? object : undefined;
+ }
+
+ module.exports = baseGet;
+
+
+/***/ },
+/* 93 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var isArray = __webpack_require__(94),
+ stringToPath = __webpack_require__(95);
+
+ /**
+ * Casts `value` to a path array if it's not one.
+ *
+ * @private
+ * @param {*} value The value to inspect.
+ * @returns {Array} Returns the cast property path array.
+ */
+ function castPath(value) {
+ return isArray(value) ? value : stringToPath(value);
+ }
+
+ module.exports = castPath;
+
+
+/***/ },
+/* 94 */
+/***/ function(module, exports) {
+
+ /**
+ * Checks if `value` is classified as an `Array` object.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.1.0
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is an array, else `false`.
+ * @example
+ *
+ * _.isArray([1, 2, 3]);
+ * // => true
+ *
+ * _.isArray(document.body.children);
+ * // => false
+ *
+ * _.isArray('abc');
+ * // => false
+ *
+ * _.isArray(_.noop);
+ * // => false
+ */
+ var isArray = Array.isArray;
+
+ module.exports = isArray;
+
+
+/***/ },
+/* 95 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var memoizeCapped = __webpack_require__(96),
+ toString = __webpack_require__(132);
+
+ /** Used to match property names within property paths. */
+ var reLeadingDot = /^\./,
+ rePropName = /[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g;
+
+ /** Used to match backslashes in property paths. */
+ var reEscapeChar = /\\(\\)?/g;
+
+ /**
+ * Converts `string` to a property path array.
+ *
+ * @private
+ * @param {string} string The string to convert.
+ * @returns {Array} Returns the property path array.
+ */
+ var stringToPath = memoizeCapped(function(string) {
+ string = toString(string);
+
+ var result = [];
+ if (reLeadingDot.test(string)) {
+ result.push('');
+ }
+ string.replace(rePropName, function(match, number, quote, string) {
+ result.push(quote ? string.replace(reEscapeChar, '$1') : (number || match));
+ });
+ return result;
+ });
+
+ module.exports = stringToPath;
+
+
+/***/ },
+/* 96 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var memoize = __webpack_require__(97);
+
+ /** Used as the maximum memoize cache size. */
+ var MAX_MEMOIZE_SIZE = 500;
+
+ /**
+ * A specialized version of `_.memoize` which clears the memoized function's
+ * cache when it exceeds `MAX_MEMOIZE_SIZE`.
+ *
+ * @private
+ * @param {Function} func The function to have its output memoized.
+ * @returns {Function} Returns the new memoized function.
+ */
+ function memoizeCapped(func) {
+ var result = memoize(func, function(key) {
+ if (cache.size === MAX_MEMOIZE_SIZE) {
+ cache.clear();
+ }
+ return key;
+ });
+
+ var cache = result.cache;
+ return result;
+ }
+
+ module.exports = memoizeCapped;
+
+
+/***/ },
+/* 97 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var MapCache = __webpack_require__(98);
+
+ /** Error message constants. */
+ var FUNC_ERROR_TEXT = 'Expected a function';
+
+ /**
+ * Creates a function that memoizes the result of `func`. If `resolver` is
+ * provided, it determines the cache key for storing the result based on the
+ * arguments provided to the memoized function. By default, the first argument
+ * provided to the memoized function is used as the map cache key. The `func`
+ * is invoked with the `this` binding of the memoized function.
+ *
+ * **Note:** The cache is exposed as the `cache` property on the memoized
+ * function. Its creation may be customized by replacing the `_.memoize.Cache`
+ * constructor with one whose instances implement the
+ * [`Map`](http://ecma-international.org/ecma-262/7.0/#sec-properties-of-the-map-prototype-object)
+ * method interface of `delete`, `get`, `has`, and `set`.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.1.0
+ * @category Function
+ * @param {Function} func The function to have its output memoized.
+ * @param {Function} [resolver] The function to resolve the cache key.
+ * @returns {Function} Returns the new memoized function.
+ * @example
+ *
+ * var object = { 'a': 1, 'b': 2 };
+ * var other = { 'c': 3, 'd': 4 };
+ *
+ * var values = _.memoize(_.values);
+ * values(object);
+ * // => [1, 2]
+ *
+ * values(other);
+ * // => [3, 4]
+ *
+ * object.a = 2;
+ * values(object);
+ * // => [1, 2]
+ *
+ * // Modify the result cache.
+ * values.cache.set(object, ['a', 'b']);
+ * values(object);
+ * // => ['a', 'b']
+ *
+ * // Replace `_.memoize.Cache`.
+ * _.memoize.Cache = WeakMap;
+ */
+ function memoize(func, resolver) {
+ if (typeof func != 'function' || (resolver && typeof resolver != 'function')) {
+ throw new TypeError(FUNC_ERROR_TEXT);
+ }
+ var memoized = function() {
+ var args = arguments,
+ key = resolver ? resolver.apply(this, args) : args[0],
+ cache = memoized.cache;
+
+ if (cache.has(key)) {
+ return cache.get(key);
+ }
+ var result = func.apply(this, args);
+ memoized.cache = cache.set(key, result) || cache;
+ return result;
+ };
+ memoized.cache = new (memoize.Cache || MapCache);
+ return memoized;
+ }
+
+ // Expose `MapCache`.
+ memoize.Cache = MapCache;
+
+ module.exports = memoize;
+
+
+/***/ },
+/* 98 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var mapCacheClear = __webpack_require__(99),
+ mapCacheDelete = __webpack_require__(126),
+ mapCacheGet = __webpack_require__(129),
+ mapCacheHas = __webpack_require__(130),
+ mapCacheSet = __webpack_require__(131);
+
+ /**
+ * Creates a map cache object to store key-value pairs.
+ *
+ * @private
+ * @constructor
+ * @param {Array} [entries] The key-value pairs to cache.
+ */
+ function MapCache(entries) {
+ var index = -1,
+ length = entries ? entries.length : 0;
+
+ this.clear();
+ while (++index < length) {
+ var entry = entries[index];
+ this.set(entry[0], entry[1]);
+ }
+ }
+
+ // Add methods to `MapCache`.
+ MapCache.prototype.clear = mapCacheClear;
+ MapCache.prototype['delete'] = mapCacheDelete;
+ MapCache.prototype.get = mapCacheGet;
+ MapCache.prototype.has = mapCacheHas;
+ MapCache.prototype.set = mapCacheSet;
+
+ module.exports = MapCache;
+
+
+/***/ },
+/* 99 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var Hash = __webpack_require__(100),
+ ListCache = __webpack_require__(117),
+ Map = __webpack_require__(125);
+
+ /**
+ * Removes all key-value entries from the map.
+ *
+ * @private
+ * @name clear
+ * @memberOf MapCache
+ */
+ function mapCacheClear() {
+ this.size = 0;
+ this.__data__ = {
+ 'hash': new Hash,
+ 'map': new (Map || ListCache),
+ 'string': new Hash
+ };
+ }
+
+ module.exports = mapCacheClear;
+
+
+/***/ },
+/* 100 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var hashClear = __webpack_require__(101),
+ hashDelete = __webpack_require__(113),
+ hashGet = __webpack_require__(114),
+ hashHas = __webpack_require__(115),
+ hashSet = __webpack_require__(116);
+
+ /**
+ * Creates a hash object.
+ *
+ * @private
+ * @constructor
+ * @param {Array} [entries] The key-value pairs to cache.
+ */
+ function Hash(entries) {
+ var index = -1,
+ length = entries ? entries.length : 0;
+
+ this.clear();
+ while (++index < length) {
+ var entry = entries[index];
+ this.set(entry[0], entry[1]);
+ }
+ }
+
+ // Add methods to `Hash`.
+ Hash.prototype.clear = hashClear;
+ Hash.prototype['delete'] = hashDelete;
+ Hash.prototype.get = hashGet;
+ Hash.prototype.has = hashHas;
+ Hash.prototype.set = hashSet;
+
+ module.exports = Hash;
+
+
+/***/ },
+/* 101 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var nativeCreate = __webpack_require__(102);
+
+ /**
+ * Removes all key-value entries from the hash.
+ *
+ * @private
+ * @name clear
+ * @memberOf Hash
+ */
+ function hashClear() {
+ this.__data__ = nativeCreate ? nativeCreate(null) : {};
+ this.size = 0;
+ }
+
+ module.exports = hashClear;
+
+
+/***/ },
+/* 102 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var getNative = __webpack_require__(103);
+
+ /* Built-in method references that are verified to be native. */
+ var nativeCreate = getNative(Object, 'create');
+
+ module.exports = nativeCreate;
+
+
+/***/ },
+/* 103 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var baseIsNative = __webpack_require__(104),
+ getValue = __webpack_require__(112);
+
+ /**
+ * Gets the native function at `key` of `object`.
+ *
+ * @private
+ * @param {Object} object The object to query.
+ * @param {string} key The key of the method to get.
+ * @returns {*} Returns the function if it's native, else `undefined`.
+ */
+ function getNative(object, key) {
+ var value = getValue(object, key);
+ return baseIsNative(value) ? value : undefined;
+ }
+
+ module.exports = getNative;
+
+
+/***/ },
+/* 104 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var isFunction = __webpack_require__(105),
+ isMasked = __webpack_require__(107),
+ isObject = __webpack_require__(106),
+ toSource = __webpack_require__(111);
+
+ /**
+ * Used to match `RegExp`
+ * [syntax characters](http://ecma-international.org/ecma-262/7.0/#sec-patterns).
+ */
+ var reRegExpChar = /[\\^$.*+?()[\]{}|]/g;
+
+ /** Used to detect host constructors (Safari). */
+ var reIsHostCtor = /^\[object .+?Constructor\]$/;
+
+ /** Used for built-in method references. */
+ var funcProto = Function.prototype,
+ objectProto = Object.prototype;
+
+ /** Used to resolve the decompiled source of functions. */
+ var funcToString = funcProto.toString;
+
+ /** Used to check objects for own properties. */
+ var hasOwnProperty = objectProto.hasOwnProperty;
+
+ /** Used to detect if a method is native. */
+ var reIsNative = RegExp('^' +
+ funcToString.call(hasOwnProperty).replace(reRegExpChar, '\\$&')
+ .replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g, '$1.*?') + '$'
+ );
+
+ /**
+ * The base implementation of `_.isNative` without bad shim checks.
+ *
+ * @private
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is a native function,
+ * else `false`.
+ */
+ function baseIsNative(value) {
+ if (!isObject(value) || isMasked(value)) {
+ return false;
+ }
+ var pattern = isFunction(value) ? reIsNative : reIsHostCtor;
+ return pattern.test(toSource(value));
+ }
+
+ module.exports = baseIsNative;
+
+
+/***/ },
+/* 105 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var isObject = __webpack_require__(106);
+
+ /** `Object#toString` result references. */
+ var funcTag = '[object Function]',
+ genTag = '[object GeneratorFunction]',
+ proxyTag = '[object Proxy]';
+
+ /** Used for built-in method references. */
+ var objectProto = Object.prototype;
+
+ /**
+ * Used to resolve the
+ * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring)
+ * of values.
+ */
+ var objectToString = objectProto.toString;
+
+ /**
+ * Checks if `value` is classified as a `Function` object.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.1.0
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is a function, else `false`.
+ * @example
+ *
+ * _.isFunction(_);
+ * // => true
+ *
+ * _.isFunction(/abc/);
+ * // => false
+ */
+ function isFunction(value) {
+ // The use of `Object#toString` avoids issues with the `typeof` operator
+ // in Safari 9 which returns 'object' for typed array and other constructors.
+ var tag = isObject(value) ? objectToString.call(value) : '';
+ return tag == funcTag || tag == genTag || tag == proxyTag;
+ }
+
+ module.exports = isFunction;
+
+
+/***/ },
+/* 106 */
+/***/ function(module, exports) {
+
+ /**
+ * Checks if `value` is the
+ * [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types)
+ * of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`)
+ *
+ * @static
+ * @memberOf _
+ * @since 0.1.0
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is an object, else `false`.
+ * @example
+ *
+ * _.isObject({});
+ * // => true
+ *
+ * _.isObject([1, 2, 3]);
+ * // => true
+ *
+ * _.isObject(_.noop);
+ * // => true
+ *
+ * _.isObject(null);
+ * // => false
+ */
+ function isObject(value) {
+ var type = typeof value;
+ return value != null && (type == 'object' || type == 'function');
+ }
+
+ module.exports = isObject;
+
+
+/***/ },
+/* 107 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var coreJsData = __webpack_require__(108);
+
+ /** Used to detect methods masquerading as native. */
+ var maskSrcKey = (function() {
+ var uid = /[^.]+$/.exec(coreJsData && coreJsData.keys && coreJsData.keys.IE_PROTO || '');
+ return uid ? ('Symbol(src)_1.' + uid) : '';
+ }());
+
+ /**
+ * Checks if `func` has its source masked.
+ *
+ * @private
+ * @param {Function} func The function to check.
+ * @returns {boolean} Returns `true` if `func` is masked, else `false`.
+ */
+ function isMasked(func) {
+ return !!maskSrcKey && (maskSrcKey in func);
+ }
+
+ module.exports = isMasked;
+
+
+/***/ },
+/* 108 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var root = __webpack_require__(109);
+
+ /** Used to detect overreaching core-js shims. */
+ var coreJsData = root['__core-js_shared__'];
+
+ module.exports = coreJsData;
+
+
+/***/ },
+/* 109 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var freeGlobal = __webpack_require__(110);
+
+ /** Detect free variable `self`. */
+ var freeSelf = typeof self == 'object' && self && self.Object === Object && self;
+
+ /** Used as a reference to the global object. */
+ var root = freeGlobal || freeSelf || Function('return this')();
+
+ module.exports = root;
+
+
+/***/ },
+/* 110 */
+/***/ function(module, exports) {
+
+ /* WEBPACK VAR INJECTION */(function(global) {/** Detect free variable `global` from Node.js. */
+ var freeGlobal = typeof global == 'object' && global && global.Object === Object && global;
+
+ module.exports = freeGlobal;
+
+ /* WEBPACK VAR INJECTION */}.call(exports, (function() { return this; }())))
+
+/***/ },
+/* 111 */
+/***/ function(module, exports) {
+
+ /** Used for built-in method references. */
+ var funcProto = Function.prototype;
+
+ /** Used to resolve the decompiled source of functions. */
+ var funcToString = funcProto.toString;
+
+ /**
+ * Converts `func` to its source code.
+ *
+ * @private
+ * @param {Function} func The function to process.
+ * @returns {string} Returns the source code.
+ */
+ function toSource(func) {
+ if (func != null) {
+ try {
+ return funcToString.call(func);
+ } catch (e) {}
+ try {
+ return (func + '');
+ } catch (e) {}
+ }
+ return '';
+ }
+
+ module.exports = toSource;
+
+
+/***/ },
+/* 112 */
+/***/ function(module, exports) {
+
+ /**
+ * Gets the value at `key` of `object`.
+ *
+ * @private
+ * @param {Object} [object] The object to query.
+ * @param {string} key The key of the property to get.
+ * @returns {*} Returns the property value.
+ */
+ function getValue(object, key) {
+ return object == null ? undefined : object[key];
+ }
+
+ module.exports = getValue;
+
+
+/***/ },
+/* 113 */
+/***/ function(module, exports) {
+
+ /**
+ * Removes `key` and its value from the hash.
+ *
+ * @private
+ * @name delete
+ * @memberOf Hash
+ * @param {Object} hash The hash to modify.
+ * @param {string} key The key of the value to remove.
+ * @returns {boolean} Returns `true` if the entry was removed, else `false`.
+ */
+ function hashDelete(key) {
+ var result = this.has(key) && delete this.__data__[key];
+ this.size -= result ? 1 : 0;
+ return result;
+ }
+
+ module.exports = hashDelete;
+
+
+/***/ },
+/* 114 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var nativeCreate = __webpack_require__(102);
+
+ /** Used to stand-in for `undefined` hash values. */
+ var HASH_UNDEFINED = '__lodash_hash_undefined__';
+
+ /** Used for built-in method references. */
+ var objectProto = Object.prototype;
+
+ /** Used to check objects for own properties. */
+ var hasOwnProperty = objectProto.hasOwnProperty;
+
+ /**
+ * Gets the hash value for `key`.
+ *
+ * @private
+ * @name get
+ * @memberOf Hash
+ * @param {string} key The key of the value to get.
+ * @returns {*} Returns the entry value.
+ */
+ function hashGet(key) {
+ var data = this.__data__;
+ if (nativeCreate) {
+ var result = data[key];
+ return result === HASH_UNDEFINED ? undefined : result;
+ }
+ return hasOwnProperty.call(data, key) ? data[key] : undefined;
+ }
+
+ module.exports = hashGet;
+
+
+/***/ },
+/* 115 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var nativeCreate = __webpack_require__(102);
+
+ /** Used for built-in method references. */
+ var objectProto = Object.prototype;
+
+ /** Used to check objects for own properties. */
+ var hasOwnProperty = objectProto.hasOwnProperty;
+
+ /**
+ * Checks if a hash value for `key` exists.
+ *
+ * @private
+ * @name has
+ * @memberOf Hash
+ * @param {string} key The key of the entry to check.
+ * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.
+ */
+ function hashHas(key) {
+ var data = this.__data__;
+ return nativeCreate ? data[key] !== undefined : hasOwnProperty.call(data, key);
+ }
+
+ module.exports = hashHas;
+
+
+/***/ },
+/* 116 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var nativeCreate = __webpack_require__(102);
+
+ /** Used to stand-in for `undefined` hash values. */
+ var HASH_UNDEFINED = '__lodash_hash_undefined__';
+
+ /**
+ * Sets the hash `key` to `value`.
+ *
+ * @private
+ * @name set
+ * @memberOf Hash
+ * @param {string} key The key of the value to set.
+ * @param {*} value The value to set.
+ * @returns {Object} Returns the hash instance.
+ */
+ function hashSet(key, value) {
+ var data = this.__data__;
+ this.size += this.has(key) ? 0 : 1;
+ data[key] = (nativeCreate && value === undefined) ? HASH_UNDEFINED : value;
+ return this;
+ }
+
+ module.exports = hashSet;
+
+
+/***/ },
+/* 117 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var listCacheClear = __webpack_require__(118),
+ listCacheDelete = __webpack_require__(119),
+ listCacheGet = __webpack_require__(122),
+ listCacheHas = __webpack_require__(123),
+ listCacheSet = __webpack_require__(124);
+
+ /**
+ * Creates an list cache object.
+ *
+ * @private
+ * @constructor
+ * @param {Array} [entries] The key-value pairs to cache.
+ */
+ function ListCache(entries) {
+ var index = -1,
+ length = entries ? entries.length : 0;
+
+ this.clear();
+ while (++index < length) {
+ var entry = entries[index];
+ this.set(entry[0], entry[1]);
+ }
+ }
+
+ // Add methods to `ListCache`.
+ ListCache.prototype.clear = listCacheClear;
+ ListCache.prototype['delete'] = listCacheDelete;
+ ListCache.prototype.get = listCacheGet;
+ ListCache.prototype.has = listCacheHas;
+ ListCache.prototype.set = listCacheSet;
+
+ module.exports = ListCache;
+
+
+/***/ },
+/* 118 */
+/***/ function(module, exports) {
+
+ /**
+ * Removes all key-value entries from the list cache.
+ *
+ * @private
+ * @name clear
+ * @memberOf ListCache
+ */
+ function listCacheClear() {
+ this.__data__ = [];
+ this.size = 0;
+ }
+
+ module.exports = listCacheClear;
+
+
+/***/ },
+/* 119 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var assocIndexOf = __webpack_require__(120);
+
+ /** Used for built-in method references. */
+ var arrayProto = Array.prototype;
+
+ /** Built-in value references. */
+ var splice = arrayProto.splice;
+
+ /**
+ * Removes `key` and its value from the list cache.
+ *
+ * @private
+ * @name delete
+ * @memberOf ListCache
+ * @param {string} key The key of the value to remove.
+ * @returns {boolean} Returns `true` if the entry was removed, else `false`.
+ */
+ function listCacheDelete(key) {
+ var data = this.__data__,
+ index = assocIndexOf(data, key);
+
+ if (index < 0) {
+ return false;
+ }
+ var lastIndex = data.length - 1;
+ if (index == lastIndex) {
+ data.pop();
+ } else {
+ splice.call(data, index, 1);
+ }
+ --this.size;
+ return true;
+ }
+
+ module.exports = listCacheDelete;
+
+
+/***/ },
+/* 120 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var eq = __webpack_require__(121);
+
+ /**
+ * Gets the index at which the `key` is found in `array` of key-value pairs.
+ *
+ * @private
+ * @param {Array} array The array to inspect.
+ * @param {*} key The key to search for.
+ * @returns {number} Returns the index of the matched value, else `-1`.
+ */
+ function assocIndexOf(array, key) {
+ var length = array.length;
+ while (length--) {
+ if (eq(array[length][0], key)) {
+ return length;
+ }
+ }
+ return -1;
+ }
+
+ module.exports = assocIndexOf;
+
+
+/***/ },
+/* 121 */
+/***/ function(module, exports) {
+
+ /**
+ * Performs a
+ * [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)
+ * comparison between two values to determine if they are equivalent.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Lang
+ * @param {*} value The value to compare.
+ * @param {*} other The other value to compare.
+ * @returns {boolean} Returns `true` if the values are equivalent, else `false`.
+ * @example
+ *
+ * var object = { 'a': 1 };
+ * var other = { 'a': 1 };
+ *
+ * _.eq(object, object);
+ * // => true
+ *
+ * _.eq(object, other);
+ * // => false
+ *
+ * _.eq('a', 'a');
+ * // => true
+ *
+ * _.eq('a', Object('a'));
+ * // => false
+ *
+ * _.eq(NaN, NaN);
+ * // => true
+ */
+ function eq(value, other) {
+ return value === other || (value !== value && other !== other);
+ }
+
+ module.exports = eq;
+
+
+/***/ },
+/* 122 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var assocIndexOf = __webpack_require__(120);
+
+ /**
+ * Gets the list cache value for `key`.
+ *
+ * @private
+ * @name get
+ * @memberOf ListCache
+ * @param {string} key The key of the value to get.
+ * @returns {*} Returns the entry value.
+ */
+ function listCacheGet(key) {
+ var data = this.__data__,
+ index = assocIndexOf(data, key);
+
+ return index < 0 ? undefined : data[index][1];
+ }
+
+ module.exports = listCacheGet;
+
+
+/***/ },
+/* 123 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var assocIndexOf = __webpack_require__(120);
+
+ /**
+ * Checks if a list cache value for `key` exists.
+ *
+ * @private
+ * @name has
+ * @memberOf ListCache
+ * @param {string} key The key of the entry to check.
+ * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.
+ */
+ function listCacheHas(key) {
+ return assocIndexOf(this.__data__, key) > -1;
+ }
+
+ module.exports = listCacheHas;
+
+
+/***/ },
+/* 124 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var assocIndexOf = __webpack_require__(120);
+
+ /**
+ * Sets the list cache `key` to `value`.
+ *
+ * @private
+ * @name set
+ * @memberOf ListCache
+ * @param {string} key The key of the value to set.
+ * @param {*} value The value to set.
+ * @returns {Object} Returns the list cache instance.
+ */
+ function listCacheSet(key, value) {
+ var data = this.__data__,
+ index = assocIndexOf(data, key);
+
+ if (index < 0) {
+ ++this.size;
+ data.push([key, value]);
+ } else {
+ data[index][1] = value;
+ }
+ return this;
+ }
+
+ module.exports = listCacheSet;
+
+
+/***/ },
+/* 125 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var getNative = __webpack_require__(103),
+ root = __webpack_require__(109);
+
+ /* Built-in method references that are verified to be native. */
+ var Map = getNative(root, 'Map');
+
+ module.exports = Map;
+
+
+/***/ },
+/* 126 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var getMapData = __webpack_require__(127);
+
+ /**
+ * Removes `key` and its value from the map.
+ *
+ * @private
+ * @name delete
+ * @memberOf MapCache
+ * @param {string} key The key of the value to remove.
+ * @returns {boolean} Returns `true` if the entry was removed, else `false`.
+ */
+ function mapCacheDelete(key) {
+ var result = getMapData(this, key)['delete'](key);
+ this.size -= result ? 1 : 0;
+ return result;
+ }
+
+ module.exports = mapCacheDelete;
+
+
+/***/ },
+/* 127 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var isKeyable = __webpack_require__(128);
+
+ /**
+ * Gets the data for `map`.
+ *
+ * @private
+ * @param {Object} map The map to query.
+ * @param {string} key The reference key.
+ * @returns {*} Returns the map data.
+ */
+ function getMapData(map, key) {
+ var data = map.__data__;
+ return isKeyable(key)
+ ? data[typeof key == 'string' ? 'string' : 'hash']
+ : data.map;
+ }
+
+ module.exports = getMapData;
+
+
+/***/ },
+/* 128 */
+/***/ function(module, exports) {
+
+ /**
+ * Checks if `value` is suitable for use as unique object key.
+ *
+ * @private
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is suitable, else `false`.
+ */
+ function isKeyable(value) {
+ var type = typeof value;
+ return (type == 'string' || type == 'number' || type == 'symbol' || type == 'boolean')
+ ? (value !== '__proto__')
+ : (value === null);
+ }
+
+ module.exports = isKeyable;
+
+
+/***/ },
+/* 129 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var getMapData = __webpack_require__(127);
+
+ /**
+ * Gets the map value for `key`.
+ *
+ * @private
+ * @name get
+ * @memberOf MapCache
+ * @param {string} key The key of the value to get.
+ * @returns {*} Returns the entry value.
+ */
+ function mapCacheGet(key) {
+ return getMapData(this, key).get(key);
+ }
+
+ module.exports = mapCacheGet;
+
+
+/***/ },
+/* 130 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var getMapData = __webpack_require__(127);
+
+ /**
+ * Checks if a map value for `key` exists.
+ *
+ * @private
+ * @name has
+ * @memberOf MapCache
+ * @param {string} key The key of the entry to check.
+ * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.
+ */
+ function mapCacheHas(key) {
+ return getMapData(this, key).has(key);
+ }
+
+ module.exports = mapCacheHas;
+
+
+/***/ },
+/* 131 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var getMapData = __webpack_require__(127);
+
+ /**
+ * Sets the map `key` to `value`.
+ *
+ * @private
+ * @name set
+ * @memberOf MapCache
+ * @param {string} key The key of the value to set.
+ * @param {*} value The value to set.
+ * @returns {Object} Returns the map cache instance.
+ */
+ function mapCacheSet(key, value) {
+ var data = getMapData(this, key),
+ size = data.size;
+
+ data.set(key, value);
+ this.size += data.size == size ? 0 : 1;
+ return this;
+ }
+
+ module.exports = mapCacheSet;
+
+
+/***/ },
+/* 132 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var baseToString = __webpack_require__(133);
+
+ /**
+ * Converts `value` to a string. An empty string is returned for `null`
+ * and `undefined` values. The sign of `-0` is preserved.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Lang
+ * @param {*} value The value to convert.
+ * @returns {string} Returns the converted string.
+ * @example
+ *
+ * _.toString(null);
+ * // => ''
+ *
+ * _.toString(-0);
+ * // => '-0'
+ *
+ * _.toString([1, 2, 3]);
+ * // => '1,2,3'
+ */
+ function toString(value) {
+ return value == null ? '' : baseToString(value);
+ }
+
+ module.exports = toString;
+
+
+/***/ },
+/* 133 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var Symbol = __webpack_require__(134),
+ arrayMap = __webpack_require__(135),
+ isArray = __webpack_require__(94),
+ isSymbol = __webpack_require__(136);
+
+ /** Used as references for various `Number` constants. */
+ var INFINITY = 1 / 0;
+
+ /** Used to convert symbols to primitives and strings. */
+ var symbolProto = Symbol ? Symbol.prototype : undefined,
+ symbolToString = symbolProto ? symbolProto.toString : undefined;
+
+ /**
+ * The base implementation of `_.toString` which doesn't convert nullish
+ * values to empty strings.
+ *
+ * @private
+ * @param {*} value The value to process.
+ * @returns {string} Returns the string.
+ */
+ function baseToString(value) {
+ // Exit early for strings to avoid a performance hit in some environments.
+ if (typeof value == 'string') {
+ return value;
+ }
+ if (isArray(value)) {
+ // Recursively convert values (susceptible to call stack limits).
+ return arrayMap(value, baseToString) + '';
+ }
+ if (isSymbol(value)) {
+ return symbolToString ? symbolToString.call(value) : '';
+ }
+ var result = (value + '');
+ return (result == '0' && (1 / value) == -INFINITY) ? '-0' : result;
+ }
+
+ module.exports = baseToString;
+
+
+/***/ },
+/* 134 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var root = __webpack_require__(109);
+
+ /** Built-in value references. */
+ var Symbol = root.Symbol;
+
+ module.exports = Symbol;
+
+
+/***/ },
+/* 135 */
+/***/ function(module, exports) {
+
+ /**
+ * A specialized version of `_.map` for arrays without support for iteratee
+ * shorthands.
+ *
+ * @private
+ * @param {Array} [array] The array to iterate over.
+ * @param {Function} iteratee The function invoked per iteration.
+ * @returns {Array} Returns the new mapped array.
+ */
+ function arrayMap(array, iteratee) {
+ var index = -1,
+ length = array ? array.length : 0,
+ result = Array(length);
+
+ while (++index < length) {
+ result[index] = iteratee(array[index], index, array);
+ }
+ return result;
+ }
+
+ module.exports = arrayMap;
+
+
+/***/ },
+/* 136 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var isObjectLike = __webpack_require__(8);
+
+ /** `Object#toString` result references. */
+ var symbolTag = '[object Symbol]';
+
+ /** Used for built-in method references. */
+ var objectProto = Object.prototype;
+
+ /**
+ * Used to resolve the
+ * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring)
+ * of values.
+ */
+ var objectToString = objectProto.toString;
+
+ /**
+ * Checks if `value` is classified as a `Symbol` primitive or object.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is a symbol, else `false`.
+ * @example
+ *
+ * _.isSymbol(Symbol.iterator);
+ * // => true
+ *
+ * _.isSymbol('abc');
+ * // => false
+ */
+ function isSymbol(value) {
+ return typeof value == 'symbol' ||
+ (isObjectLike(value) && objectToString.call(value) == symbolTag);
+ }
+
+ module.exports = isSymbol;
+
+
+/***/ },
+/* 137 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var isArray = __webpack_require__(94),
+ isSymbol = __webpack_require__(136);
+
+ /** Used to match property names within property paths. */
+ var reIsDeepProp = /\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,
+ reIsPlainProp = /^\w*$/;
+
+ /**
+ * Checks if `value` is a property name and not a property path.
+ *
+ * @private
+ * @param {*} value The value to check.
+ * @param {Object} [object] The object to query keys on.
+ * @returns {boolean} Returns `true` if `value` is a property name, else `false`.
+ */
+ function isKey(value, object) {
+ if (isArray(value)) {
+ return false;
+ }
+ var type = typeof value;
+ if (type == 'number' || type == 'symbol' || type == 'boolean' ||
+ value == null || isSymbol(value)) {
+ return true;
+ }
+ return reIsPlainProp.test(value) || !reIsDeepProp.test(value) ||
+ (object != null && value in Object(object));
+ }
+
+ module.exports = isKey;
+
+
+/***/ },
+/* 138 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var isSymbol = __webpack_require__(136);
+
+ /** Used as references for various `Number` constants. */
+ var INFINITY = 1 / 0;
+
+ /**
+ * Converts `value` to a string key if it's not a string or symbol.
+ *
+ * @private
+ * @param {*} value The value to inspect.
+ * @returns {string|symbol} Returns the key.
+ */
+ function toKey(value) {
+ if (typeof value == 'string' || isSymbol(value)) {
+ return value;
+ }
+ var result = (value + '');
+ return (result == '0' && (1 / value) == -INFINITY) ? '-0' : result;
+ }
+
+ module.exports = toKey;
+
+
+/***/ },
+/* 139 */
+/***/ function(module, exports) {
+
+ module.exports = devtoolsRequire("devtools/shared/flags");
+
+/***/ },
+/* 140 */
+/***/ function(module, exports) {
+
+ function Debuggee() {
+ function $(selector) {
+ var element = document.querySelector(selector);
+ console.log("$", selector, element);
+
+ if (!element) {
+ throw new Error("Element not found, try changing the selector");
+ }
+ return element;
+ }
+
+ function mouseEvent(eventType) {
+ return new MouseEvent(eventType, {
+ "view": window,
+ "bubbles": true,
+ "cancelable": true
+ });
+ }
+
+ var specialKeysMap = {
+ "{enter}": 13
+ };
+
+ // Special character examples {enter}, {esc}, {leftarrow} ..
+ function isSpecialCharacter(text) {
+ return text.match(/^\{.*\}$/);
+ }
+
+ function keyInfo(key, eventType) {
+ var charCodeAt = void 0;
+
+ if (key.length > 1) {
+ charCodeAt = specialKeysMap[key];
+ } else {
+ charCodeAt = key.toUpperCase().charCodeAt(0);
+ }
+
+ return {
+ charCode: eventType == "keypress" ? 0 : charCodeAt,
+ keyCode: charCodeAt,
+ which: charCodeAt
+ };
+ }
+
+ function keyEvent(eventType, key) {
+ var event = new Event(eventType, {
+ bubbles: true,
+ cancelable: false,
+ view: window
+ });
+
+ var _keyInfo = keyInfo(key, eventType);
+
+ var charCode = _keyInfo.charCode;
+ var keyCode = _keyInfo.keyCode;
+ var which = _keyInfo.which;
+
+
+ return Object.assign(event, {
+ charCode: charCode,
+ keyCode: keyCode,
+ which: which,
+ detail: 0,
+ layerX: 0,
+ layerY: 0,
+ pageX: 0,
+ pageY: 0
+ });
+ }
+
+ function sendKey(element, key) {
+ element.dispatchEvent(keyEvent("keydown", key));
+ element.dispatchEvent(keyEvent("keypress", key));
+ if (key.length == 1) {
+ element.value += key;
+ }
+ element.dispatchEvent(keyEvent("keyup", key));
+ }
+
+ function click(selector) {
+ var element = $(selector);
+ console.log("click", selector);
+ element.dispatchEvent(mouseEvent("click"));
+ }
+
+ function dblclick(selector) {
+ var element = $(selector);
+ console.log("dblclick", selector);
+ element.dispatchEvent(mouseEvent("dblclick"));
+ }
+
+ function type(selector, text) {
+ var element = $(selector);
+ console.log("type", selector, text);
+ element.select();
+
+ if (isSpecialCharacter(text)) {
+ sendKey(element, text);
+ } else {
+ var chars = text.split("");
+ chars.forEach(char => sendKey(element, char));
+ }
+ }
+
+ return {
+ click,
+ dblclick,
+ type
+ };
+ }
+
+ var debuggeeStatement = `window.dbg = (${ Debuggee })()`;
+ var injectedDebuggee = void 0;
+
+ function injectDebuggee() {
+ if (injectedDebuggee) {
+ return Promise.resolve(injectedDebuggee);
+ }
+
+ return window.client.debuggeeCommand(debuggeeStatement).then(result => {
+ injectedDebuggee = result;
+ });
+ }
+
+ module.exports = injectDebuggee;
+
+/***/ },
+/* 141 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var _require = __webpack_require__(142);
+
+ var Task = _require.Task;
+
+ var firefox = __webpack_require__(143);
+ var chrome = __webpack_require__(205);
+
+ var _require2 = __webpack_require__(88);
+
+ var debugGlobal = _require2.debugGlobal;
+
+
+ var clientType = void 0;
+ function getClient() {
+ if (clientType === "chrome") {
+ return chrome.clientCommands;
+ }
+
+ return firefox.clientCommands;
+ }
+
+ function startDebugging(connTarget, actions) {
+ if (connTarget.type === "node") {
+ return startDebuggingNode(connTarget.param, actions);
+ }
+
+ var target = connTarget.type === "chrome" ? chrome : firefox;
+ return startDebuggingTab(target, connTarget.param, actions);
+ }
+
+ function startDebuggingNode(url, actions) {
+ clientType = "chrome";
+ return chrome.connectNode(`ws://${ url }`).then(() => {
+ chrome.initPage(actions);
+ });
+ }
+
+ function startDebuggingTab(targetEnv, tabId, actions) {
+ return Task.spawn(function* () {
+ var tabs = yield targetEnv.connectClient();
+ var tab = tabs.find(t => t.id.indexOf(tabId) !== -1);
+ yield targetEnv.connectTab(tab.tab);
+ targetEnv.initPage(actions);
+
+ clientType = targetEnv === firefox ? "firefox" : "chrome";
+ debugGlobal("client", targetEnv.clientCommands);
+
+ return tabs;
+ });
+ }
+
+ function connectClients(onConnect) {
+ firefox.connectClient().then(onConnect);
+ chrome.connectClient().then(onConnect);
+ }
+
+ module.exports = {
+ getClient,
+ connectClients,
+ startDebugging,
+ firefox,
+ chrome
+ };
+
+/***/ },
+/* 142 */
+/***/ function(module, exports) {
+
+ /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+ /* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
+ /* 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/. */
+
+ /**
+ * This object provides the public module functions.
+ */
+ var Task = {
+ // XXX: Not sure if this works in all cases...
+ async: function (task) {
+ return function () {
+ return Task.spawn(task, this, arguments);
+ };
+ },
+
+ /**
+ * Creates and starts a new task.
+ * @param task A generator function
+ * @return A promise, resolved when the task terminates
+ */
+ spawn: function (task, scope, args) {
+ return new Promise(function (resolve, reject) {
+ var iterator = task.apply(scope, args);
+
+ var callNext = lastValue => {
+ var iteration = iterator.next(lastValue);
+ Promise.resolve(iteration.value).then(value => {
+ if (iteration.done) {
+ resolve(value);
+ } else {
+ callNext(value);
+ }
+ }).catch(error => {
+ reject(error);
+ iterator.throw(error);
+ });
+ };
+
+ callNext(undefined);
+ });
+ }
+ };
+
+ module.exports = { Task };
+
+/***/ },
+/* 143 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var _require = __webpack_require__(28);
+
+ var DebuggerClient = _require.DebuggerClient;
+ var DebuggerTransport = _require.DebuggerTransport;
+ var TargetFactory = _require.TargetFactory;
+ var WebsocketTransport = _require.WebsocketTransport;
+
+ var defer = __webpack_require__(144);
+
+ var _require2 = __webpack_require__(89);
+
+ var getValue = _require2.getValue;
+
+ var _require3 = __webpack_require__(145);
+
+ var Tab = _require3.Tab;
+
+ var _require4 = __webpack_require__(202);
+
+ var setupCommands = _require4.setupCommands;
+ var clientCommands = _require4.clientCommands;
+
+ var _require5 = __webpack_require__(203);
+
+ var setupEvents = _require5.setupEvents;
+ var clientEvents = _require5.clientEvents;
+
+ var _require6 = __webpack_require__(204);
+
+ var createSource = _require6.createSource;
+
+
+ var debuggerClient = null;
+ var threadClient = null;
+ var tabTarget = null;
+
+ function getThreadClient() {
+ return threadClient;
+ }
+
+ function setThreadClient(client) {
+ threadClient = client;
+ }
+
+ function getTabTarget() {
+ return tabTarget;
+ }
+
+ function setTabTarget(target) {
+ tabTarget = target;
+ }
+
+ function lookupTabTarget(tab) {
+ var options = { client: debuggerClient, form: tab, chrome: false };
+ return TargetFactory.forRemoteTab(options);
+ }
+
+ function createTabs(tabs) {
+ return tabs.map(tab => {
+ return Tab({
+ title: tab.title,
+ url: tab.url,
+ id: tab.actor,
+ tab,
+ browser: "firefox"
+ });
+ });
+ }
+
+ function connectClient() {
+ var deferred = defer();
+ var useProxy = !getValue("firefox.webSocketConnection");
+ var portPref = useProxy ? "firefox.proxyPort" : "firefox.webSocketPort";
+ var webSocketPort = getValue(portPref);
+
+ var socket = new WebSocket(`ws://${ document.location.hostname }:${ webSocketPort }`);
+ var transport = useProxy ? new DebuggerTransport(socket) : new WebsocketTransport(socket);
+ debuggerClient = new DebuggerClient(transport);
+
+ debuggerClient.connect().then(() => {
+ return debuggerClient.listTabs().then(response => {
+ deferred.resolve(createTabs(response.tabs));
+ });
+ }).catch(err => {
+ console.log(err);
+ deferred.resolve([]);
+ });
+
+ return deferred.promise;
+ }
+
+ function connectTab(tab) {
+ return new Promise((resolve, reject) => {
+ window.addEventListener("beforeunload", () => {
+ getTabTarget() && getTabTarget().destroy();
+ });
+
+ lookupTabTarget(tab).then(target => {
+ tabTarget = target;
+ target.activeTab.attachThread({}, (res, _threadClient) => {
+ threadClient = _threadClient;
+ threadClient.resume();
+ resolve();
+ });
+ });
+ });
+ }
+
+ function initPage(actions) {
+ tabTarget = getTabTarget();
+ threadClient = getThreadClient();
+
+ setupCommands({ threadClient, tabTarget, debuggerClient });
+
+ tabTarget.on("will-navigate", actions.willNavigate);
+ tabTarget.on("navigate", actions.navigated);
+
+ // Listen to all the requested events.
+ setupEvents({ threadClient, actions });
+ Object.keys(clientEvents).forEach(eventName => {
+ threadClient.addListener(eventName, clientEvents[eventName]);
+ });
+
+ // In Firefox, we need to initially request all of the sources. This
+ // usually fires off individual `newSource` notifications as the
+ // debugger finds them, but there may be existing sources already in
+ // the debugger (if it's paused already, or if loading the page from
+ // bfcache) so explicity fire `newSource` events for all returned
+ // sources.
+ return threadClient.getSources().then((_ref) => {
+ var sources = _ref.sources;
+
+ actions.newSources(sources.map(createSource));
+
+ // If the threadClient is already paused, make sure to show a
+ // paused state.
+ var pausedPacket = threadClient.getLastPausePacket();
+ if (pausedPacket) {
+ clientEvents.paused(null, pausedPacket);
+ }
+ });
+ }
+
+ module.exports = {
+ connectClient,
+ connectTab,
+ clientCommands,
+ getThreadClient,
+ setThreadClient,
+ getTabTarget,
+ setTabTarget,
+ initPage
+ };
+
+/***/ },
+/* 144 */
+/***/ function(module, exports) {
+
+ module.exports = function defer() {
+ var resolve = void 0,
+ reject = void 0;
+ var promise = new Promise(function () {
+ resolve = arguments[0];
+ reject = arguments[1];
+ });
+ return {
+ resolve: resolve,
+ reject: reject,
+ promise: promise
+ };
+ };
+
+/***/ },
+/* 145 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var t = __webpack_require__(146);
+
+ var Tab = t.struct({
+ title: t.String,
+ url: t.String,
+ id: t.String,
+ tab: t.Object,
+ browser: t.enums.of(["chrome", "firefox"])
+ }, "Tab");
+
+ var SourceText = t.struct({
+ text: t.String,
+ contentType: t.String
+ });
+
+ var Source = t.struct({
+ id: t.String,
+ url: t.union([t.String, t.Nil]),
+ isPrettyPrinted: t.Boolean,
+ sourceMapURL: t.union([t.String, t.Nil])
+ }, "Source");
+
+ var Location = t.struct({
+ sourceId: t.String,
+ line: t.Number,
+ column: t.union([t.Number, t.Nil])
+ }, "Location");
+
+ var Breakpoint = t.struct({
+ id: t.String,
+ loading: t.Boolean,
+ disabled: t.Boolean,
+ text: t.String,
+ condition: t.union([t.String, t.Nil])
+ });
+
+ var BreakpointResult = t.struct({
+ id: t.String,
+ actualLocation: Location
+ });
+
+ var Frame = t.struct({
+ id: t.String,
+ displayName: t.String,
+ location: Location,
+ this: t.union([t.Object, t.Nil]),
+ scope: t.union([t.Object, t.Nil])
+ }, "Frame");
+
+ module.exports = {
+ Tab,
+ Source,
+ SourceText,
+ Location,
+ Breakpoint,
+ BreakpointResult,
+ Frame
+ };
+
+/***/ },
+/* 146 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /*! @preserve
+ *
+ * tcomb.js - Type checking and DDD for JavaScript
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2014-2016 Giulio Canti
+ *
+ */
+
+ // core
+ var t = __webpack_require__(147);
+
+ // types
+ t.Any = __webpack_require__(153);
+ t.Array = __webpack_require__(161);
+ t.Boolean = __webpack_require__(162);
+ t.Date = __webpack_require__(164);
+ t.Error = __webpack_require__(165);
+ t.Function = __webpack_require__(166);
+ t.Nil = __webpack_require__(167);
+ t.Number = __webpack_require__(168);
+ t.Integer = __webpack_require__(170);
+ t.IntegerT = t.Integer;
+ t.Object = __webpack_require__(176);
+ t.RegExp = __webpack_require__(177);
+ t.String = __webpack_require__(178);
+ t.Type = __webpack_require__(179);
+ t.TypeT = t.Type;
+
+ // short alias are deprecated
+ t.Arr = t.Array;
+ t.Bool = t.Boolean;
+ t.Dat = t.Date;
+ t.Err = t.Error;
+ t.Func = t.Function;
+ t.Num = t.Number;
+ t.Obj = t.Object;
+ t.Re = t.RegExp;
+ t.Str = t.String;
+
+ // combinators
+ t.dict = __webpack_require__(180);
+ t.declare = __webpack_require__(181);
+ t.enums = __webpack_require__(184);
+ t.irreducible = __webpack_require__(154);
+ t.list = __webpack_require__(185);
+ t.maybe = __webpack_require__(186);
+ t.refinement = __webpack_require__(171);
+ t.struct = __webpack_require__(188);
+ t.tuple = __webpack_require__(194);
+ t.union = __webpack_require__(195);
+ t.func = __webpack_require__(196);
+ t.intersection = __webpack_require__(197);
+ t.subtype = t.refinement;
+ t.inter = __webpack_require__(198); // IE8 alias
+ t['interface'] = t.inter;
+
+ // functions
+ t.assert = t;
+ t.update = __webpack_require__(200);
+ t.mixin = __webpack_require__(182);
+ t.isType = __webpack_require__(158);
+ t.is = __webpack_require__(175);
+ t.getTypeName = __webpack_require__(157);
+ t.match = __webpack_require__(201);
+
+ module.exports = t;
+
+
+/***/ },
+/* 147 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var isFunction = __webpack_require__(148);
+ var isNil = __webpack_require__(149);
+ var fail = __webpack_require__(150);
+ var stringify = __webpack_require__(151);
+
+ function assert(guard, message) {
+ if (guard !== true) {
+ if (isFunction(message)) { // handle lazy messages
+ message = message();
+ }
+ else if (isNil(message)) { // use a default message
+ message = 'Assert failed (turn on "Pause on exceptions" in your Source panel)';
+ }
+ assert.fail(message);
+ }
+ }
+
+ assert.fail = fail;
+ assert.stringify = stringify;
+
+ module.exports = assert;
+
+/***/ },
+/* 148 */
+/***/ function(module, exports) {
+
+ module.exports = function isFunction(x) {
+ return typeof x === 'function';
+ };
+
+/***/ },
+/* 149 */
+/***/ function(module, exports) {
+
+ module.exports = function isNil(x) {
+ return x === null || x === void 0;
+ };
+
+/***/ },
+/* 150 */
+/***/ function(module, exports) {
+
+ module.exports = function fail(message) {
+ throw new TypeError('[tcomb] ' + message);
+ };
+
+/***/ },
+/* 151 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var getFunctionName = __webpack_require__(152);
+
+ function replacer(key, value) {
+ if (typeof value === 'function') {
+ return getFunctionName(value);
+ }
+ return value;
+ }
+
+ module.exports = function stringify(x) {
+ try { // handle "Converting circular structure to JSON" error
+ return JSON.stringify(x, replacer, 2);
+ }
+ catch (e) {
+ return String(x);
+ }
+ };
+
+/***/ },
+/* 152 */
+/***/ function(module, exports) {
+
+ module.exports = function getFunctionName(f) {
+ return f.displayName || f.name || '<function' + f.length + '>';
+ };
+
+/***/ },
+/* 153 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var irreducible = __webpack_require__(154);
+
+ module.exports = irreducible('Any', function () { return true; });
+
+
+/***/ },
+/* 154 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var assert = __webpack_require__(147);
+ var isString = __webpack_require__(155);
+ var isFunction = __webpack_require__(148);
+ var forbidNewOperator = __webpack_require__(156);
+
+ module.exports = function irreducible(name, predicate) {
+
+ if (false) {
+ assert(isString(name), function () { return 'Invalid argument name ' + assert.stringify(name) + ' supplied to irreducible(name, predicate) (expected a string)'; });
+ assert(isFunction(predicate), 'Invalid argument predicate ' + assert.stringify(predicate) + ' supplied to irreducible(name, predicate) (expected a function)');
+ }
+
+ function Irreducible(value, path) {
+
+ if (false) {
+ forbidNewOperator(this, Irreducible);
+ path = path || [name];
+ assert(predicate(value), function () { return 'Invalid value ' + assert.stringify(value) + ' supplied to ' + path.join('/'); });
+ }
+
+ return value;
+ }
+
+ Irreducible.meta = {
+ kind: 'irreducible',
+ name: name,
+ predicate: predicate,
+ identity: true
+ };
+
+ Irreducible.displayName = name;
+
+ Irreducible.is = predicate;
+
+ return Irreducible;
+ };
+
+
+/***/ },
+/* 155 */
+/***/ function(module, exports) {
+
+ module.exports = function isString(x) {
+ return typeof x === 'string';
+ };
+
+/***/ },
+/* 156 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var assert = __webpack_require__(147);
+ var getTypeName = __webpack_require__(157);
+
+ module.exports = function forbidNewOperator(x, type) {
+ assert(!(x instanceof type), function () { return 'Cannot use the new operator to instantiate the type ' + getTypeName(type); });
+ };
+
+/***/ },
+/* 157 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var isType = __webpack_require__(158);
+ var getFunctionName = __webpack_require__(152);
+
+ module.exports = function getTypeName(ctor) {
+ if (isType(ctor)) {
+ return ctor.displayName;
+ }
+ return getFunctionName(ctor);
+ };
+
+/***/ },
+/* 158 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var isFunction = __webpack_require__(148);
+ var isObject = __webpack_require__(159);
+
+ module.exports = function isType(x) {
+ return isFunction(x) && isObject(x.meta);
+ };
+
+/***/ },
+/* 159 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var isNil = __webpack_require__(149);
+ var isArray = __webpack_require__(160);
+
+ module.exports = function isObject(x) {
+ return !isNil(x) && typeof x === 'object' && !isArray(x);
+ };
+
+/***/ },
+/* 160 */
+/***/ function(module, exports) {
+
+ module.exports = function isArray(x) {
+ return Array.isArray ? Array.isArray(x) : x instanceof Array;
+ };
+
+/***/ },
+/* 161 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var irreducible = __webpack_require__(154);
+ var isArray = __webpack_require__(160);
+
+ module.exports = irreducible('Array', isArray);
+
+
+/***/ },
+/* 162 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var irreducible = __webpack_require__(154);
+ var isBoolean = __webpack_require__(163);
+
+ module.exports = irreducible('Boolean', isBoolean);
+
+
+/***/ },
+/* 163 */
+/***/ function(module, exports) {
+
+ module.exports = function isBoolean(x) {
+ return x === true || x === false;
+ };
+
+/***/ },
+/* 164 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var irreducible = __webpack_require__(154);
+
+ module.exports = irreducible('Date', function (x) { return x instanceof Date; });
+
+
+/***/ },
+/* 165 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var irreducible = __webpack_require__(154);
+
+ module.exports = irreducible('Error', function (x) { return x instanceof Error; });
+
+
+/***/ },
+/* 166 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var irreducible = __webpack_require__(154);
+ var isFunction = __webpack_require__(148);
+
+ module.exports = irreducible('Function', isFunction);
+
+
+/***/ },
+/* 167 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var irreducible = __webpack_require__(154);
+ var isNil = __webpack_require__(149);
+
+ module.exports = irreducible('Nil', isNil);
+
+
+/***/ },
+/* 168 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var irreducible = __webpack_require__(154);
+ var isNumber = __webpack_require__(169);
+
+ module.exports = irreducible('Number', isNumber);
+
+
+/***/ },
+/* 169 */
+/***/ function(module, exports) {
+
+ module.exports = function isNumber(x) {
+ return typeof x === 'number' && isFinite(x) && !isNaN(x);
+ };
+
+/***/ },
+/* 170 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var refinement = __webpack_require__(171);
+ var Number = __webpack_require__(168);
+
+ module.exports = refinement(Number, function (x) { return x % 1 === 0; }, 'Integer');
+
+
+/***/ },
+/* 171 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var assert = __webpack_require__(147);
+ var isTypeName = __webpack_require__(172);
+ var isFunction = __webpack_require__(148);
+ var forbidNewOperator = __webpack_require__(156);
+ var isIdentity = __webpack_require__(173);
+ var create = __webpack_require__(174);
+ var is = __webpack_require__(175);
+ var getTypeName = __webpack_require__(157);
+ var getFunctionName = __webpack_require__(152);
+
+ function getDefaultName(type, predicate) {
+ return '{' + getTypeName(type) + ' | ' + getFunctionName(predicate) + '}';
+ }
+
+ function refinement(type, predicate, name) {
+
+ if (false) {
+ assert(isFunction(type), function () { return 'Invalid argument type ' + assert.stringify(type) + ' supplied to refinement(type, predicate, [name]) combinator (expected a type)'; });
+ assert(isFunction(predicate), function () { return 'Invalid argument predicate supplied to refinement(type, predicate, [name]) combinator (expected a function)'; });
+ assert(isTypeName(name), function () { return 'Invalid argument name ' + assert.stringify(name) + ' supplied to refinement(type, predicate, [name]) combinator (expected a string)'; });
+ }
+
+ var displayName = name || getDefaultName(type, predicate);
+ var identity = isIdentity(type);
+
+ function Refinement(value, path) {
+
+ if (false) {
+ if (identity) {
+ forbidNewOperator(this, Refinement);
+ }
+ path = path || [displayName];
+ }
+
+ var x = create(type, value, path);
+
+ if (false) {
+ assert(predicate(x), function () { return 'Invalid value ' + assert.stringify(value) + ' supplied to ' + path.join('/'); });
+ }
+
+ return x;
+ }
+
+ Refinement.meta = {
+ kind: 'subtype',
+ type: type,
+ predicate: predicate,
+ name: name,
+ identity: identity
+ };
+
+ Refinement.displayName = displayName;
+
+ Refinement.is = function (x) {
+ return is(x, type) && predicate(x);
+ };
+
+ Refinement.update = function (instance, patch) {
+ return Refinement(assert.update(instance, patch));
+ };
+
+ return Refinement;
+ }
+
+ refinement.getDefaultName = getDefaultName;
+ module.exports = refinement;
+
+
+/***/ },
+/* 172 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var isNil = __webpack_require__(149);
+ var isString = __webpack_require__(155);
+
+ module.exports = function isTypeName(name) {
+ return isNil(name) || isString(name);
+ };
+
+/***/ },
+/* 173 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var assert = __webpack_require__(147);
+ var Boolean = __webpack_require__(162);
+ var isType = __webpack_require__(158);
+ var getTypeName = __webpack_require__(157);
+
+ // return true if the type constructor behaves like the identity function
+ module.exports = function isIdentity(type) {
+ if (isType(type)) {
+ if (false) {
+ assert(Boolean.is(type.meta.identity), function () { return 'Invalid meta identity ' + assert.stringify(type.meta.identity) + ' supplied to type ' + getTypeName(type); });
+ }
+ return type.meta.identity;
+ }
+ // for tcomb the other constructors, like ES6 classes, are identity-like
+ return true;
+ };
+
+/***/ },
+/* 174 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var isType = __webpack_require__(158);
+ var getFunctionName = __webpack_require__(152);
+ var assert = __webpack_require__(147);
+ var stringify = __webpack_require__(151);
+
+ // creates an instance of a type, handling the optional new operator
+ module.exports = function create(type, value, path) {
+ if (isType(type)) {
+ return !type.meta.identity && typeof value === 'object' && value !== null ? new type(value, path): type(value, path);
+ }
+
+ if (false) {
+ // here type should be a class constructor and value some instance, just check membership and return the value
+ path = path || [getFunctionName(type)];
+ assert(value instanceof type, function () { return 'Invalid value ' + stringify(value) + ' supplied to ' + path.join('/'); });
+ }
+
+ return value;
+ };
+
+/***/ },
+/* 175 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var isType = __webpack_require__(158);
+
+ // returns true if x is an instance of type
+ module.exports = function is(x, type) {
+ if (isType(type)) {
+ return type.is(x);
+ }
+ return x instanceof type; // type should be a class constructor
+ };
+
+
+/***/ },
+/* 176 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var irreducible = __webpack_require__(154);
+ var isObject = __webpack_require__(159);
+
+ module.exports = irreducible('Object', isObject);
+
+
+/***/ },
+/* 177 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var irreducible = __webpack_require__(154);
+
+ module.exports = irreducible('RegExp', function (x) { return x instanceof RegExp; });
+
+
+/***/ },
+/* 178 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var irreducible = __webpack_require__(154);
+ var isString = __webpack_require__(155);
+
+ module.exports = irreducible('String', isString);
+
+
+/***/ },
+/* 179 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var irreducible = __webpack_require__(154);
+ var isType = __webpack_require__(158);
+
+ module.exports = irreducible('Type', isType);
+
+/***/ },
+/* 180 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var assert = __webpack_require__(147);
+ var isTypeName = __webpack_require__(172);
+ var isFunction = __webpack_require__(148);
+ var getTypeName = __webpack_require__(157);
+ var isIdentity = __webpack_require__(173);
+ var isObject = __webpack_require__(159);
+ var create = __webpack_require__(174);
+ var is = __webpack_require__(175);
+
+ function getDefaultName(domain, codomain) {
+ return '{[key: ' + getTypeName(domain) + ']: ' + getTypeName(codomain) + '}';
+ }
+
+ function dict(domain, codomain, name) {
+
+ if (false) {
+ assert(isFunction(domain), function () { return 'Invalid argument domain ' + assert.stringify(domain) + ' supplied to dict(domain, codomain, [name]) combinator (expected a type)'; });
+ assert(isFunction(codomain), function () { return 'Invalid argument codomain ' + assert.stringify(codomain) + ' supplied to dict(domain, codomain, [name]) combinator (expected a type)'; });
+ assert(isTypeName(name), function () { return 'Invalid argument name ' + assert.stringify(name) + ' supplied to dict(domain, codomain, [name]) combinator (expected a string)'; });
+ }
+
+ var displayName = name || getDefaultName(domain, codomain);
+ var domainNameCache = getTypeName(domain);
+ var codomainNameCache = getTypeName(codomain);
+ var identity = isIdentity(domain) && isIdentity(codomain);
+
+ function Dict(value, path) {
+
+ if (true) {
+ if (identity) {
+ return value; // just trust the input if elements must not be hydrated
+ }
+ }
+
+ if (false) {
+ path = path || [displayName];
+ assert(isObject(value), function () { return 'Invalid value ' + assert.stringify(value) + ' supplied to ' + path.join('/'); });
+ }
+
+ var idempotent = true; // will remain true if I can reutilise the input
+ var ret = {}; // make a temporary copy, will be discarded if idempotent remains true
+ for (var k in value) {
+ if (value.hasOwnProperty(k)) {
+ k = create(domain, k, ( false ? path.concat(domainNameCache) : null ));
+ var actual = value[k];
+ var instance = create(codomain, actual, ( false ? path.concat(k + ': ' + codomainNameCache) : null ));
+ idempotent = idempotent && ( actual === instance );
+ ret[k] = instance;
+ }
+ }
+
+ if (idempotent) { // implements idempotency
+ ret = value;
+ }
+
+ if (false) {
+ Object.freeze(ret);
+ }
+
+ return ret;
+ }
+
+ Dict.meta = {
+ kind: 'dict',
+ domain: domain,
+ codomain: codomain,
+ name: name,
+ identity: identity
+ };
+
+ Dict.displayName = displayName;
+
+ Dict.is = function (x) {
+ if (!isObject(x)) {
+ return false;
+ }
+ for (var k in x) {
+ if (x.hasOwnProperty(k)) {
+ if (!is(k, domain) || !is(x[k], codomain)) {
+ return false;
+ }
+ }
+ }
+ return true;
+ };
+
+ Dict.update = function (instance, patch) {
+ return Dict(assert.update(instance, patch));
+ };
+
+ return Dict;
+ }
+
+ dict.getDefaultName = getDefaultName;
+ module.exports = dict;
+
+
+/***/ },
+/* 181 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var assert = __webpack_require__(147);
+ var isTypeName = __webpack_require__(172);
+ var isType = __webpack_require__(158);
+ var isNil = __webpack_require__(149);
+ var mixin = __webpack_require__(182);
+ var getTypeName = __webpack_require__(157);
+ var isUnion = __webpack_require__(183);
+
+ // All the .declare-d types should be clearly different from each other thus they should have
+ // different names when a name was not explicitly provided.
+ var nextDeclareUniqueId = 1;
+
+ module.exports = function declare(name) {
+ if (false) {
+ assert(isTypeName(name), function () { return 'Invalid argument name ' + name + ' supplied to declare([name]) (expected a string)'; });
+ }
+
+ var type;
+
+ function Declare(value, path) {
+ if (false) {
+ assert(!isNil(type), function () { return 'Type declared but not defined, don\'t forget to call .define on every declared type'; });
+ if (isUnion(type)) {
+ assert(type.dispatch === Declare.dispatch, function () { return 'Please define the custom ' + name + '.dispatch function before calling ' + name + '.define()'; });
+ }
+ }
+ return type(value, path);
+ }
+
+ Declare.define = function (spec) {
+ if (false) {
+ assert(isType(spec), function () { return 'Invalid argument type ' + assert.stringify(spec) + ' supplied to define(type) (expected a type)'; });
+ assert(isNil(type), function () { return 'Declare.define(type) can only be invoked once'; });
+ assert(isNil(spec.meta.name) && Object.keys(spec.prototype).length === 0, function () { return 'Invalid argument type ' + assert.stringify(spec) + ' supplied to define(type) (expected a fresh, unnamed type)'; });
+ }
+
+ if (isUnion(spec) && Declare.hasOwnProperty('dispatch')) {
+ spec.dispatch = Declare.dispatch;
+ }
+ type = spec;
+ mixin(Declare, type, true); // true because it overwrites Declare.displayName
+ if (name) {
+ type.displayName = Declare.displayName = name;
+ Declare.meta.name = name;
+ }
+ Declare.meta.identity = type.meta.identity;
+ Declare.prototype = type.prototype;
+ return Declare;
+ };
+
+ Declare.displayName = name || ( getTypeName(Declare) + "$" + nextDeclareUniqueId++ );
+ // in general I can't say if this type will be an identity, for safety setting to false
+ Declare.meta = { identity: false };
+ Declare.prototype = null;
+ return Declare;
+ };
+
+
+/***/ },
+/* 182 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var isNil = __webpack_require__(149);
+ var assert = __webpack_require__(147);
+
+ // safe mixin, cannot override props unless specified
+ module.exports = function mixin(target, source, overwrite) {
+ if (isNil(source)) { return target; }
+ for (var k in source) {
+ if (source.hasOwnProperty(k)) {
+ if (overwrite !== true) {
+ if (false) {
+ assert(!target.hasOwnProperty(k) || target[k] === source[k], function () { return 'Invalid call to mixin(target, source, [overwrite]): cannot overwrite property "' + k + '" of target object'; });
+ }
+ }
+ target[k] = source[k];
+ }
+ }
+ return target;
+ };
+
+/***/ },
+/* 183 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var isType = __webpack_require__(158);
+
+ module.exports = function isUnion(x) {
+ return isType(x) && ( x.meta.kind === 'union' );
+ };
+
+/***/ },
+/* 184 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var assert = __webpack_require__(147);
+ var isTypeName = __webpack_require__(172);
+ var forbidNewOperator = __webpack_require__(156);
+ var isString = __webpack_require__(155);
+ var isObject = __webpack_require__(159);
+
+ function getDefaultName(map) {
+ return Object.keys(map).map(function (k) { return assert.stringify(k); }).join(' | ');
+ }
+
+ function enums(map, name) {
+
+ if (false) {
+ assert(isObject(map), function () { return 'Invalid argument map ' + assert.stringify(map) + ' supplied to enums(map, [name]) combinator (expected a dictionary of String -> String | Number)'; });
+ assert(isTypeName(name), function () { return 'Invalid argument name ' + assert.stringify(name) + ' supplied to enums(map, [name]) combinator (expected a string)'; });
+ }
+
+ var displayName = name || getDefaultName(map);
+
+ function Enums(value, path) {
+
+ if (false) {
+ forbidNewOperator(this, Enums);
+ path = path || [displayName];
+ assert(Enums.is(value), function () { return 'Invalid value ' + assert.stringify(value) + ' supplied to ' + path.join('/') + ' (expected one of ' + assert.stringify(Object.keys(map)) + ')'; });
+ }
+
+ return value;
+ }
+
+ Enums.meta = {
+ kind: 'enums',
+ map: map,
+ name: name,
+ identity: true
+ };
+
+ Enums.displayName = displayName;
+
+ Enums.is = function (x) {
+ return map.hasOwnProperty(x);
+ };
+
+ return Enums;
+ }
+
+ enums.of = function (keys, name) {
+ keys = isString(keys) ? keys.split(' ') : keys;
+ var value = {};
+ keys.forEach(function (k) {
+ value[k] = k;
+ });
+ return enums(value, name);
+ };
+
+ enums.getDefaultName = getDefaultName;
+ module.exports = enums;
+
+
+
+/***/ },
+/* 185 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var assert = __webpack_require__(147);
+ var isTypeName = __webpack_require__(172);
+ var isFunction = __webpack_require__(148);
+ var getTypeName = __webpack_require__(157);
+ var isIdentity = __webpack_require__(173);
+ var create = __webpack_require__(174);
+ var is = __webpack_require__(175);
+ var isArray = __webpack_require__(160);
+
+ function getDefaultName(type) {
+ return 'Array<' + getTypeName(type) + '>';
+ }
+
+ function list(type, name) {
+
+ if (false) {
+ assert(isFunction(type), function () { return 'Invalid argument type ' + assert.stringify(type) + ' supplied to list(type, [name]) combinator (expected a type)'; });
+ assert(isTypeName(name), function () { return 'Invalid argument name ' + assert.stringify(name) + ' supplied to list(type, [name]) combinator (expected a string)'; });
+ }
+
+ var displayName = name || getDefaultName(type);
+ var typeNameCache = getTypeName(type);
+ var identity = isIdentity(type); // the list is identity iif type is identity
+
+ function List(value, path) {
+
+ if (true) {
+ if (identity) {
+ return value; // just trust the input if elements must not be hydrated
+ }
+ }
+
+ if (false) {
+ path = path || [displayName];
+ assert(isArray(value), function () { return 'Invalid value ' + assert.stringify(value) + ' supplied to ' + path.join('/') + ' (expected an array of ' + typeNameCache + ')'; });
+ }
+
+ var idempotent = true; // will remain true if I can reutilise the input
+ var ret = []; // make a temporary copy, will be discarded if idempotent remains true
+ for (var i = 0, len = value.length; i < len; i++ ) {
+ var actual = value[i];
+ var instance = create(type, actual, ( false ? path.concat(i + ': ' + typeNameCache) : null ));
+ idempotent = idempotent && ( actual === instance );
+ ret.push(instance);
+ }
+
+ if (idempotent) { // implements idempotency
+ ret = value;
+ }
+
+ if (false) {
+ Object.freeze(ret);
+ }
+
+ return ret;
+ }
+
+ List.meta = {
+ kind: 'list',
+ type: type,
+ name: name,
+ identity: identity
+ };
+
+ List.displayName = displayName;
+
+ List.is = function (x) {
+ return isArray(x) && x.every(function (e) {
+ return is(e, type);
+ });
+ };
+
+ List.update = function (instance, patch) {
+ return List(assert.update(instance, patch));
+ };
+
+ return List;
+ }
+
+ list.getDefaultName = getDefaultName;
+ module.exports = list;
+
+
+/***/ },
+/* 186 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var assert = __webpack_require__(147);
+ var isTypeName = __webpack_require__(172);
+ var isFunction = __webpack_require__(148);
+ var isMaybe = __webpack_require__(187);
+ var isIdentity = __webpack_require__(173);
+ var Any = __webpack_require__(153);
+ var create = __webpack_require__(174);
+ var Nil = __webpack_require__(167);
+ var forbidNewOperator = __webpack_require__(156);
+ var is = __webpack_require__(175);
+ var getTypeName = __webpack_require__(157);
+
+ function getDefaultName(type) {
+ return '?' + getTypeName(type);
+ }
+
+ function maybe(type, name) {
+
+ if (isMaybe(type) || type === Any || type === Nil) { // makes the combinator idempotent and handle Any, Nil
+ return type;
+ }
+
+ if (false) {
+ assert(isFunction(type), function () { return 'Invalid argument type ' + assert.stringify(type) + ' supplied to maybe(type, [name]) combinator (expected a type)'; });
+ assert(isTypeName(name), function () { return 'Invalid argument name ' + assert.stringify(name) + ' supplied to maybe(type, [name]) combinator (expected a string)'; });
+ }
+
+ var displayName = name || getDefaultName(type);
+ var identity = isIdentity(type);
+
+ function Maybe(value, path) {
+ if (false) {
+ if (identity) {
+ forbidNewOperator(this, Maybe);
+ }
+ }
+ return Nil.is(value) ? value : create(type, value, path);
+ }
+
+ Maybe.meta = {
+ kind: 'maybe',
+ type: type,
+ name: name,
+ identity: identity
+ };
+
+ Maybe.displayName = displayName;
+
+ Maybe.is = function (x) {
+ return Nil.is(x) || is(x, type);
+ };
+
+ return Maybe;
+ }
+
+ maybe.getDefaultName = getDefaultName;
+ module.exports = maybe;
+
+
+/***/ },
+/* 187 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var isType = __webpack_require__(158);
+
+ module.exports = function isMaybe(x) {
+ return isType(x) && ( x.meta.kind === 'maybe' );
+ };
+
+/***/ },
+/* 188 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var assert = __webpack_require__(147);
+ var isTypeName = __webpack_require__(172);
+ var String = __webpack_require__(178);
+ var Function = __webpack_require__(166);
+ var isBoolean = __webpack_require__(163);
+ var isObject = __webpack_require__(159);
+ var isNil = __webpack_require__(149);
+ var create = __webpack_require__(174);
+ var getTypeName = __webpack_require__(157);
+ var dict = __webpack_require__(180);
+ var getDefaultInterfaceName = __webpack_require__(189);
+ var extend = __webpack_require__(190);
+
+ function getDefaultName(props) {
+ return 'Struct' + getDefaultInterfaceName(props);
+ }
+
+ function extendStruct(mixins, name) {
+ return extend(struct, mixins, name);
+ }
+
+ function getOptions(options) {
+ if (!isObject(options)) {
+ options = isNil(options) ? {} : { name: options };
+ }
+ if (!options.hasOwnProperty('strict')) {
+ options.strict = struct.strict;
+ }
+ if (!options.hasOwnProperty('defaultProps')) {
+ options.defaultProps = {};
+ }
+ return options;
+ }
+
+ function struct(props, options) {
+
+ options = getOptions(options);
+ var name = options.name;
+ var strict = options.strict;
+ var defaultProps = options.defaultProps;
+
+ if (false) {
+ assert(dict(String, Function).is(props), function () { return 'Invalid argument props ' + assert.stringify(props) + ' supplied to struct(props, [options]) combinator (expected a dictionary String -> Type)'; });
+ assert(isTypeName(name), function () { return 'Invalid argument name ' + assert.stringify(name) + ' supplied to struct(props, [options]) combinator (expected a string)'; });
+ assert(isBoolean(strict), function () { return 'Invalid argument strict ' + assert.stringify(strict) + ' supplied to struct(props, [options]) combinator (expected a boolean)'; });
+ assert(isObject(defaultProps), function () { return 'Invalid argument defaultProps ' + assert.stringify(defaultProps) + ' supplied to struct(props, [options]) combinator (expected an object)'; });
+ }
+
+ var displayName = name || getDefaultName(props);
+
+ function Struct(value, path) {
+
+ if (Struct.is(value)) { // implements idempotency
+ return value;
+ }
+
+ if (false) {
+ path = path || [displayName];
+ assert(isObject(value), function () { return 'Invalid value ' + assert.stringify(value) + ' supplied to ' + path.join('/') + ' (expected an object)'; });
+ // strictness
+ if (strict) {
+ for (k in value) {
+ if (value.hasOwnProperty(k)) {
+ assert(props.hasOwnProperty(k), function () { return 'Invalid additional prop "' + k + '" supplied to ' + path.join('/'); });
+ }
+ }
+ }
+ }
+
+ if (!(this instanceof Struct)) { // `new` is optional
+ return new Struct(value, path);
+ }
+
+ for (var k in props) {
+ if (props.hasOwnProperty(k)) {
+ var expected = props[k];
+ var actual = value[k];
+ // apply defaults
+ if (actual === undefined) {
+ actual = defaultProps[k];
+ }
+ this[k] = create(expected, actual, ( false ? path.concat(k + ': ' + getTypeName(expected)) : null ));
+ }
+ }
+
+ if (false) {
+ Object.freeze(this);
+ }
+
+ }
+
+ Struct.meta = {
+ kind: 'struct',
+ props: props,
+ name: name,
+ identity: false,
+ strict: strict,
+ defaultProps: defaultProps
+ };
+
+ Struct.displayName = displayName;
+
+ Struct.is = function (x) {
+ return x instanceof Struct;
+ };
+
+ Struct.update = function (instance, patch) {
+ return new Struct(assert.update(instance, patch));
+ };
+
+ Struct.extend = function (xs, name) {
+ return extendStruct([Struct].concat(xs), name);
+ };
+
+ return Struct;
+ }
+
+ struct.strict = false;
+ struct.getOptions = getOptions;
+ struct.getDefaultName = getDefaultName;
+ struct.extend = extendStruct;
+ module.exports = struct;
+
+
+/***/ },
+/* 189 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var getTypeName = __webpack_require__(157);
+
+ function getDefaultInterfaceName(props) {
+ return '{' + Object.keys(props).map(function (prop) {
+ return prop + ': ' + getTypeName(props[prop]);
+ }).join(', ') + '}';
+ }
+
+ module.exports = getDefaultInterfaceName;
+
+
+/***/ },
+/* 190 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var assert = __webpack_require__(147);
+ var isFunction = __webpack_require__(148);
+ var isArray = __webpack_require__(160);
+ var mixin = __webpack_require__(182);
+ var isStruct = __webpack_require__(191);
+ var isInterface = __webpack_require__(192);
+ var isObject = __webpack_require__(159);
+ var refinement = __webpack_require__(171);
+ var decompose = __webpack_require__(193);
+
+ function compose(predicates, unrefinedType) {
+ return predicates.reduce(function (type, predicate) {
+ return refinement(type, predicate);
+ }, unrefinedType);
+ }
+
+ function getProps(type) {
+ return isObject(type) ? type : type.meta.props;
+ }
+
+ function getDefaultProps(type) {
+ return isObject(type) ? null : type.meta.defaultProps;
+ }
+
+ function pushAll(arr, elements) {
+ Array.prototype.push.apply(arr, elements);
+ }
+
+ function extend(combinator, mixins, options) {
+ if (false) {
+ assert(isFunction(combinator), function () { return 'Invalid argument combinator supplied to extend(combinator, mixins, options), expected a function'; });
+ assert(isArray(mixins), function () { return 'Invalid argument mixins supplied to extend(combinator, mixins, options), expected an array'; });
+ }
+ var props = {};
+ var prototype = {};
+ var predicates = [];
+ var defaultProps = {};
+ mixins.forEach(function (x, i) {
+ var decomposition = decompose(x);
+ var unrefinedType = decomposition.unrefinedType;
+ if (false) {
+ assert(isObject(unrefinedType) || isStruct(unrefinedType) || isInterface(unrefinedType), function () { return 'Invalid argument mixins[' + i + '] supplied to extend(combinator, mixins, options), expected an object, struct, interface or a refinement (of struct or interface)'; });
+ }
+ pushAll(predicates, decomposition.predicates);
+ mixin(props, getProps(unrefinedType));
+ mixin(prototype, unrefinedType.prototype);
+ mixin(defaultProps, getDefaultProps(unrefinedType), true);
+ });
+ options = combinator.getOptions(options);
+ options.defaultProps = mixin(defaultProps, options.defaultProps, true);
+ var result = compose(predicates, combinator(props, options));
+ mixin(result.prototype, prototype);
+ return result;
+ }
+
+ module.exports = extend;
+
+/***/ },
+/* 191 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var isType = __webpack_require__(158);
+
+ module.exports = function isStruct(x) {
+ return isType(x) && ( x.meta.kind === 'struct' );
+ };
+
+/***/ },
+/* 192 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var isType = __webpack_require__(158);
+
+ module.exports = function isInterface(x) {
+ return isType(x) && ( x.meta.kind === 'interface' );
+ };
+
+/***/ },
+/* 193 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var isType = __webpack_require__(158);
+
+ function isRefinement(type) {
+ return isType(type) && type.meta.kind === 'subtype';
+ }
+
+ function getPredicates(type) {
+ return isRefinement(type) ?
+ [type.meta.predicate].concat(getPredicates(type.meta.type)) :
+ [];
+ }
+
+ function getUnrefinedType(type) {
+ return isRefinement(type) ?
+ getUnrefinedType(type.meta.type) :
+ type;
+ }
+
+ function decompose(type) {
+ return {
+ predicates: getPredicates(type),
+ unrefinedType: getUnrefinedType(type)
+ };
+ }
+
+ module.exports = decompose;
+
+/***/ },
+/* 194 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var assert = __webpack_require__(147);
+ var isTypeName = __webpack_require__(172);
+ var isFunction = __webpack_require__(148);
+ var getTypeName = __webpack_require__(157);
+ var isIdentity = __webpack_require__(173);
+ var isArray = __webpack_require__(160);
+ var create = __webpack_require__(174);
+ var is = __webpack_require__(175);
+
+ function getDefaultName(types) {
+ return '[' + types.map(getTypeName).join(', ') + ']';
+ }
+
+ function tuple(types, name) {
+
+ if (false) {
+ assert(isArray(types) && types.every(isFunction), function () { return 'Invalid argument types ' + assert.stringify(types) + ' supplied to tuple(types, [name]) combinator (expected an array of types)'; });
+ assert(isTypeName(name), function () { return 'Invalid argument name ' + assert.stringify(name) + ' supplied to tuple(types, [name]) combinator (expected a string)'; });
+ }
+
+ var displayName = name || getDefaultName(types);
+ var identity = types.every(isIdentity);
+
+ function Tuple(value, path) {
+
+ if (true) {
+ if (identity) {
+ return value;
+ }
+ }
+
+ if (false) {
+ path = path || [displayName];
+ assert(isArray(value) && value.length === types.length, function () { return 'Invalid value ' + assert.stringify(value) + ' supplied to ' + path.join('/') + ' (expected an array of length ' + types.length + ')'; });
+ }
+
+ var idempotent = true;
+ var ret = [];
+ for (var i = 0, len = types.length; i < len; i++) {
+ var expected = types[i];
+ var actual = value[i];
+ var instance = create(expected, actual, ( false ? path.concat(i + ': ' + getTypeName(expected)) : null ));
+ idempotent = idempotent && ( actual === instance );
+ ret.push(instance);
+ }
+
+ if (idempotent) { // implements idempotency
+ ret = value;
+ }
+
+ if (false) {
+ Object.freeze(ret);
+ }
+
+ return ret;
+ }
+
+ Tuple.meta = {
+ kind: 'tuple',
+ types: types,
+ name: name,
+ identity: identity
+ };
+
+ Tuple.displayName = displayName;
+
+ Tuple.is = function (x) {
+ return isArray(x) &&
+ x.length === types.length &&
+ types.every(function (type, i) {
+ return is(x[i], type);
+ });
+ };
+
+ Tuple.update = function (instance, patch) {
+ return Tuple(assert.update(instance, patch));
+ };
+
+ return Tuple;
+ }
+
+ tuple.getDefaultName = getDefaultName;
+ module.exports = tuple;
+
+/***/ },
+/* 195 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var assert = __webpack_require__(147);
+ var isTypeName = __webpack_require__(172);
+ var isFunction = __webpack_require__(148);
+ var getTypeName = __webpack_require__(157);
+ var isIdentity = __webpack_require__(173);
+ var isArray = __webpack_require__(160);
+ var create = __webpack_require__(174);
+ var is = __webpack_require__(175);
+ var forbidNewOperator = __webpack_require__(156);
+ var isUnion = __webpack_require__(183);
+ var isNil = __webpack_require__(149);
+
+ function getDefaultName(types) {
+ return types.map(getTypeName).join(' | ');
+ }
+
+ function union(types, name) {
+
+ if (false) {
+ assert(isArray(types) && types.every(isFunction) && types.length >= 2, function () { return 'Invalid argument types ' + assert.stringify(types) + ' supplied to union(types, [name]) combinator (expected an array of at least 2 types)'; });
+ assert(isTypeName(name), function () { return 'Invalid argument name ' + assert.stringify(name) + ' supplied to union(types, [name]) combinator (expected a string)'; });
+ }
+
+ var displayName = name || getDefaultName(types);
+ var identity = types.every(isIdentity);
+
+ function Union(value, path) {
+
+ if (true) {
+ if (identity) {
+ return value;
+ }
+ }
+
+ var type = Union.dispatch(value);
+ if (!type && Union.is(value)) {
+ return value;
+ }
+
+ if (false) {
+ if (identity) {
+ forbidNewOperator(this, Union);
+ }
+ path = path || [displayName];
+ assert(isFunction(type), function () { return 'Invalid value ' + assert.stringify(value) + ' supplied to ' + path.join('/') + ' (no constructor returned by dispatch)'; });
+ path[path.length - 1] += '(' + getTypeName(type) + ')';
+ }
+
+ return create(type, value, path);
+ }
+
+ Union.meta = {
+ kind: 'union',
+ types: types,
+ name: name,
+ identity: identity
+ };
+
+ Union.displayName = displayName;
+
+ Union.is = function (x) {
+ return types.some(function (type) {
+ return is(x, type);
+ });
+ };
+
+ Union.dispatch = function (x) { // default dispatch implementation
+ for (var i = 0, len = types.length; i < len; i++ ) {
+ var type = types[i];
+ if (isUnion(type)) { // handle union of unions
+ var t = type.dispatch(x);
+ if (!isNil(t)) {
+ return t;
+ }
+ }
+ else if (is(x, type)) {
+ return type;
+ }
+ }
+ };
+
+ Union.update = function (instance, patch) {
+ return Union(assert.update(instance, patch));
+ };
+
+ return Union;
+ }
+
+ union.getDefaultName = getDefaultName;
+ module.exports = union;
+
+
+
+/***/ },
+/* 196 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var assert = __webpack_require__(147);
+ var isTypeName = __webpack_require__(172);
+ var FunctionType = __webpack_require__(166);
+ var isArray = __webpack_require__(160);
+ var list = __webpack_require__(185);
+ var isObject = __webpack_require__(159);
+ var create = __webpack_require__(174);
+ var isNil = __webpack_require__(149);
+ var isBoolean = __webpack_require__(163);
+ var tuple = __webpack_require__(194);
+ var getFunctionName = __webpack_require__(152);
+ var getTypeName = __webpack_require__(157);
+ var isType = __webpack_require__(158);
+
+ function getDefaultName(domain, codomain) {
+ return '(' + domain.map(getTypeName).join(', ') + ') => ' + getTypeName(codomain);
+ }
+
+ function isInstrumented(f) {
+ return FunctionType.is(f) && isObject(f.instrumentation);
+ }
+
+ function getOptionalArgumentsIndex(types) {
+ var end = types.length;
+ var areAllMaybes = false;
+ for (var i = end - 1; i >= 0; i--) {
+ var type = types[i];
+ if (!isType(type) || type.meta.kind !== 'maybe') {
+ return (i + 1);
+ } else {
+ areAllMaybes = true;
+ }
+ }
+ return areAllMaybes ? 0 : end;
+ }
+
+ function func(domain, codomain, name) {
+
+ domain = isArray(domain) ? domain : [domain]; // handle handy syntax for unary functions
+
+ if (false) {
+ assert(list(FunctionType).is(domain), function () { return 'Invalid argument domain ' + assert.stringify(domain) + ' supplied to func(domain, codomain, [name]) combinator (expected an array of types)'; });
+ assert(FunctionType.is(codomain), function () { return 'Invalid argument codomain ' + assert.stringify(codomain) + ' supplied to func(domain, codomain, [name]) combinator (expected a type)'; });
+ assert(isTypeName(name), function () { return 'Invalid argument name ' + assert.stringify(name) + ' supplied to func(domain, codomain, [name]) combinator (expected a string)'; });
+ }
+
+ var displayName = name || getDefaultName(domain, codomain);
+ var domainLength = domain.length;
+ var optionalArgumentsIndex = getOptionalArgumentsIndex(domain);
+
+ function FuncType(value, path) {
+
+ if (!isInstrumented(value)) { // automatically instrument the function
+ return FuncType.of(value);
+ }
+
+ if (false) {
+ path = path || [displayName];
+ assert(FuncType.is(value), function () { return 'Invalid value ' + assert.stringify(value) + ' supplied to ' + path.join('/'); });
+ }
+
+ return value;
+ }
+
+ FuncType.meta = {
+ kind: 'func',
+ domain: domain,
+ codomain: codomain,
+ name: name,
+ identity: true
+ };
+
+ FuncType.displayName = displayName;
+
+ FuncType.is = function (x) {
+ return isInstrumented(x) &&
+ x.instrumentation.domain.length === domainLength &&
+ x.instrumentation.domain.every(function (type, i) {
+ return type === domain[i];
+ }) &&
+ x.instrumentation.codomain === codomain;
+ };
+
+ FuncType.of = function (f, curried) {
+
+ if (false) {
+ assert(FunctionType.is(f), function () { return 'Invalid argument f supplied to func.of ' + displayName + ' (expected a function)'; });
+ assert(isNil(curried) || isBoolean(curried), function () { return 'Invalid argument curried ' + assert.stringify(curried) + ' supplied to func.of ' + displayName + ' (expected a boolean)'; });
+ }
+
+ if (FuncType.is(f)) { // makes FuncType.of idempotent
+ return f;
+ }
+
+ function fn() {
+ var args = Array.prototype.slice.call(arguments);
+ var argsLength = args.length;
+
+ if (false) {
+ // type-check arguments
+ var tupleLength = curried ? argsLength : Math.max(argsLength, optionalArgumentsIndex);
+ tuple(domain.slice(0, tupleLength), 'arguments of function ' + displayName)(args);
+ }
+
+ if (curried && argsLength < domainLength) {
+ if (false) {
+ assert(argsLength > 0, 'Invalid arguments.length = 0 for curried function ' + displayName);
+ }
+ var g = Function.prototype.bind.apply(f, [this].concat(args));
+ var newDomain = func(domain.slice(argsLength), codomain);
+ return newDomain.of(g, true);
+ }
+ else {
+ return create(codomain, f.apply(this, args));
+ }
+ }
+
+ fn.instrumentation = {
+ domain: domain,
+ codomain: codomain,
+ f: f
+ };
+
+ fn.displayName = getFunctionName(f);
+
+ return fn;
+
+ };
+
+ return FuncType;
+
+ }
+
+ func.getDefaultName = getDefaultName;
+ func.getOptionalArgumentsIndex = getOptionalArgumentsIndex;
+ module.exports = func;
+
+
+/***/ },
+/* 197 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var assert = __webpack_require__(147);
+ var isTypeName = __webpack_require__(172);
+ var isFunction = __webpack_require__(148);
+ var isArray = __webpack_require__(160);
+ var forbidNewOperator = __webpack_require__(173);
+ var is = __webpack_require__(175);
+ var getTypeName = __webpack_require__(157);
+ var isIdentity = __webpack_require__(173);
+
+ function getDefaultName(types) {
+ return types.map(getTypeName).join(' & ');
+ }
+
+ function intersection(types, name) {
+
+ if (false) {
+ assert(isArray(types) && types.every(isFunction) && types.length >= 2, function () { return 'Invalid argument types ' + assert.stringify(types) + ' supplied to intersection(types, [name]) combinator (expected an array of at least 2 types)'; });
+ assert(isTypeName(name), function () { return 'Invalid argument name ' + assert.stringify(name) + ' supplied to intersection(types, [name]) combinator (expected a string)'; });
+ }
+
+ var displayName = name || getDefaultName(types);
+ var identity = types.every(isIdentity);
+
+ function Intersection(value, path) {
+
+ if (false) {
+ if (identity) {
+ forbidNewOperator(this, Intersection);
+ }
+ path = path || [displayName];
+ assert(Intersection.is(value), function () { return 'Invalid value ' + assert.stringify(value) + ' supplied to ' + path.join('/'); });
+ }
+
+ return value;
+ }
+
+ Intersection.meta = {
+ kind: 'intersection',
+ types: types,
+ name: name,
+ identity: identity
+ };
+
+ Intersection.displayName = displayName;
+
+ Intersection.is = function (x) {
+ return types.every(function (type) {
+ return is(x, type);
+ });
+ };
+
+ Intersection.update = function (instance, patch) {
+ return Intersection(assert.update(instance, patch));
+ };
+
+ return Intersection;
+ }
+
+ intersection.getDefaultName = getDefaultName;
+ module.exports = intersection;
+
+
+
+/***/ },
+/* 198 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var assert = __webpack_require__(147);
+ var isTypeName = __webpack_require__(172);
+ var String = __webpack_require__(178);
+ var Function = __webpack_require__(166);
+ var isBoolean = __webpack_require__(163);
+ var isObject = __webpack_require__(159);
+ var isNil = __webpack_require__(149);
+ var create = __webpack_require__(174);
+ var getTypeName = __webpack_require__(157);
+ var dict = __webpack_require__(180);
+ var getDefaultInterfaceName = __webpack_require__(189);
+ var isIdentity = __webpack_require__(173);
+ var is = __webpack_require__(175);
+ var extend = __webpack_require__(190);
+ var assign = __webpack_require__(199);
+
+ function extendInterface(mixins, name) {
+ return extend(inter, mixins, name);
+ }
+
+ function getOptions(options) {
+ if (!isObject(options)) {
+ options = isNil(options) ? {} : { name: options };
+ }
+ if (!options.hasOwnProperty('strict')) {
+ options.strict = inter.strict;
+ }
+ return options;
+ }
+
+ function inter(props, options) {
+
+ options = getOptions(options);
+ var name = options.name;
+ var strict = options.strict;
+
+ if (false) {
+ assert(dict(String, Function).is(props), function () { return 'Invalid argument props ' + assert.stringify(props) + ' supplied to interface(props, [options]) combinator (expected a dictionary String -> Type)'; });
+ assert(isTypeName(name), function () { return 'Invalid argument name ' + assert.stringify(name) + ' supplied to interface(props, [options]) combinator (expected a string)'; });
+ assert(isBoolean(strict), function () { return 'Invalid argument strict ' + assert.stringify(strict) + ' supplied to struct(props, [options]) combinator (expected a boolean)'; });
+ }
+
+ var displayName = name || getDefaultInterfaceName(props);
+ var identity = Object.keys(props).map(function (prop) { return props[prop]; }).every(isIdentity);
+
+ function Interface(value, path) {
+
+ if (true) {
+ if (identity) {
+ return value; // just trust the input if elements must not be hydrated
+ }
+ }
+
+ if (false) {
+ path = path || [displayName];
+ assert(!isNil(value), function () { return 'Invalid value ' + value + ' supplied to ' + path.join('/'); });
+ // strictness
+ if (strict) {
+ for (var k in value) {
+ assert(props.hasOwnProperty(k), function () { return 'Invalid additional prop "' + k + '" supplied to ' + path.join('/'); });
+ }
+ }
+ }
+
+ var idempotent = true;
+ var ret = identity ? {} : assign({}, value);
+ for (var prop in props) {
+ var expected = props[prop];
+ var actual = value[prop];
+ var instance = create(expected, actual, ( false ? path.concat(prop + ': ' + getTypeName(expected)) : null ));
+ idempotent = idempotent && ( actual === instance );
+ ret[prop] = instance;
+ }
+
+ if (idempotent) { // implements idempotency
+ ret = value;
+ }
+
+ if (false) {
+ Object.freeze(ret);
+ }
+
+ return ret;
+
+ }
+
+ Interface.meta = {
+ kind: 'interface',
+ props: props,
+ name: name,
+ identity: identity,
+ strict: strict
+ };
+
+ Interface.displayName = displayName;
+
+ Interface.is = function (x) {
+ if (isNil(x)) {
+ return false;
+ }
+ if (strict) {
+ for (var k in x) {
+ if (!props.hasOwnProperty(k)) {
+ return false;
+ }
+ }
+ }
+ for (var prop in props) {
+ if (!is(x[prop], props[prop])) {
+ return false;
+ }
+ }
+ return true;
+ };
+
+ Interface.update = function (instance, patch) {
+ return Interface(assert.update(instance, patch));
+ };
+
+ Interface.extend = function (xs, name) {
+ return extendInterface([Interface].concat(xs), name);
+ };
+
+ return Interface;
+ }
+
+ inter.strict = false;
+ inter.getOptions = getOptions;
+ inter.getDefaultName = getDefaultInterfaceName;
+ inter.extend = extendInterface;
+ module.exports = inter;
+
+
+/***/ },
+/* 199 */
+/***/ function(module, exports) {
+
+ function assign(x, y) {
+ for (var k in y) {
+ x[k] = y[k];
+ }
+ return x;
+ }
+
+ module.exports = assign;
+
+/***/ },
+/* 200 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var assert = __webpack_require__(147);
+ var isObject = __webpack_require__(159);
+ var isFunction = __webpack_require__(148);
+ var isArray = __webpack_require__(160);
+ var isNumber = __webpack_require__(169);
+ var assign = __webpack_require__(199);
+
+ function getShallowCopy(x) {
+ if (isObject(x)) {
+ if (x instanceof Date || x instanceof RegExp) {
+ return x;
+ }
+ return assign({}, x);
+ }
+ if (isArray(x)) {
+ return x.concat();
+ }
+ return x;
+ }
+
+ function isCommand(k) {
+ return update.commands.hasOwnProperty(k);
+ }
+
+ function getCommand(k) {
+ return update.commands[k];
+ }
+
+ function update(instance, patch) {
+
+ if (false) {
+ assert(isObject(patch), function () { return 'Invalid argument patch ' + assert.stringify(patch) + ' supplied to function update(instance, patch): expected an object containing commands'; });
+ }
+
+ var value = instance;
+ var isChanged = false;
+ var newValue;
+ for (var k in patch) {
+ if (patch.hasOwnProperty(k)) {
+ if (isCommand(k)) {
+ newValue = getCommand(k)(patch[k], value);
+ if (newValue !== instance) {
+ isChanged = true;
+ value = newValue;
+ } else {
+ value = instance;
+ }
+ }
+ else {
+ if (value === instance) {
+ value = getShallowCopy(instance);
+ }
+ newValue = update(value[k], patch[k]);
+ isChanged = isChanged || ( newValue !== value[k] );
+ value[k] = newValue;
+ }
+ }
+ }
+ return isChanged ? value : instance;
+ }
+
+ // built-in commands
+
+ function $apply(f, value) {
+ if (false) {
+ assert(isFunction(f), 'Invalid argument f supplied to immutability helper { $apply: f } (expected a function)');
+ }
+ return f(value);
+ }
+
+ function $push(elements, arr) {
+ if (false) {
+ assert(isArray(elements), 'Invalid argument elements supplied to immutability helper { $push: elements } (expected an array)');
+ assert(isArray(arr), 'Invalid value supplied to immutability helper $push (expected an array)');
+ }
+ if (elements.length > 0) {
+ return arr.concat(elements);
+ }
+ return arr;
+ }
+
+ function $remove(keys, obj) {
+ if (false) {
+ assert(isArray(keys), 'Invalid argument keys supplied to immutability helper { $remove: keys } (expected an array)');
+ assert(isObject(obj), 'Invalid value supplied to immutability helper $remove (expected an object)');
+ }
+ if (keys.length > 0) {
+ obj = getShallowCopy(obj);
+ for (var i = 0, len = keys.length; i < len; i++ ) {
+ delete obj[keys[i]];
+ }
+ }
+ return obj;
+ }
+
+ function $set(value) {
+ return value;
+ }
+
+ function $splice(splices, arr) {
+ if (false) {
+ assert(isArray(splices) && splices.every(isArray), 'Invalid argument splices supplied to immutability helper { $splice: splices } (expected an array of arrays)');
+ assert(isArray(arr), 'Invalid value supplied to immutability helper $splice (expected an array)');
+ }
+ if (splices.length > 0) {
+ arr = getShallowCopy(arr);
+ return splices.reduce(function (acc, splice) {
+ acc.splice.apply(acc, splice);
+ return acc;
+ }, arr);
+ }
+ return arr;
+ }
+
+ function $swap(config, arr) {
+ if (false) {
+ assert(isObject(config), 'Invalid argument config supplied to immutability helper { $swap: config } (expected an object)');
+ assert(isNumber(config.from), 'Invalid argument config.from supplied to immutability helper { $swap: config } (expected a number)');
+ assert(isNumber(config.to), 'Invalid argument config.to supplied to immutability helper { $swap: config } (expected a number)');
+ assert(isArray(arr), 'Invalid value supplied to immutability helper $swap (expected an array)');
+ }
+ if (config.from !== config.to) {
+ arr = getShallowCopy(arr);
+ var element = arr[config.to];
+ arr[config.to] = arr[config.from];
+ arr[config.from] = element;
+ }
+ return arr;
+ }
+
+ function $unshift(elements, arr) {
+ if (false) {
+ assert(isArray(elements), 'Invalid argument elements supplied to immutability helper {$unshift: elements} (expected an array)');
+ assert(isArray(arr), 'Invalid value supplied to immutability helper $unshift (expected an array)');
+ }
+ if (elements.length > 0) {
+ return elements.concat(arr);
+ }
+ return arr;
+ }
+
+ function $merge(whatToMerge, value) {
+ var isChanged = false;
+ var result = getShallowCopy(value);
+ for (var k in whatToMerge) {
+ if (whatToMerge.hasOwnProperty(k)) {
+ result[k] = whatToMerge[k];
+ isChanged = isChanged || ( result[k] !== value[k] );
+ }
+ }
+ return isChanged ? result : value;
+ }
+
+ update.commands = {
+ $apply: $apply,
+ $push: $push,
+ $remove: $remove,
+ $set: $set,
+ $splice: $splice,
+ $swap: $swap,
+ $unshift: $unshift,
+ $merge: $merge
+ };
+
+ module.exports = update;
+
+
+/***/ },
+/* 201 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var assert = __webpack_require__(147);
+ var isFunction = __webpack_require__(148);
+ var isType = __webpack_require__(158);
+ var Any = __webpack_require__(153);
+
+ module.exports = function match(x) {
+ var type, guard, f, count;
+ for (var i = 1, len = arguments.length; i < len; ) {
+ type = arguments[i];
+ guard = arguments[i + 1];
+ f = arguments[i + 2];
+
+ if (isFunction(f) && !isType(f)) {
+ i = i + 3;
+ }
+ else {
+ f = guard;
+ guard = Any.is;
+ i = i + 2;
+ }
+
+ if (false) {
+ count = (count || 0) + 1;
+ assert(isType(type), function () { return 'Invalid type in clause #' + count; });
+ assert(isFunction(guard), function () { return 'Invalid guard in clause #' + count; });
+ assert(isFunction(f), function () { return 'Invalid block in clause #' + count; });
+ }
+
+ if (type.is(x) && guard(x)) {
+ return f(x);
+ }
+ }
+ assert.fail('Match error');
+ };
+
+
+/***/ },
+/* 202 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var _require = __webpack_require__(145);
+
+ var BreakpointResult = _require.BreakpointResult;
+ var Location = _require.Location;
+
+ var defer = __webpack_require__(144);
+
+ var bpClients = void 0;
+ var threadClient = void 0;
+ var tabTarget = void 0;
+ var debuggerClient = void 0;
+
+ function setupCommands(dependencies) {
+ threadClient = dependencies.threadClient;
+ tabTarget = dependencies.tabTarget;
+ debuggerClient = dependencies.debuggerClient;
+ bpClients = {};
+ }
+
+ function resume() {
+ return new Promise(resolve => {
+ threadClient.resume(resolve);
+ });
+ }
+
+ function stepIn() {
+ return new Promise(resolve => {
+ threadClient.stepIn(resolve);
+ });
+ }
+
+ function stepOver() {
+ return new Promise(resolve => {
+ threadClient.stepOver(resolve);
+ });
+ }
+
+ function stepOut() {
+ return new Promise(resolve => {
+ threadClient.stepOut(resolve);
+ });
+ }
+
+ function breakOnNext() {
+ return threadClient.breakOnNext();
+ }
+
+ function sourceContents(sourceId) {
+ var sourceClient = threadClient.source({ actor: sourceId });
+ return sourceClient.source();
+ }
+
+ function setBreakpoint(location, condition, noSliding) {
+ var sourceClient = threadClient.source({ actor: location.sourceId });
+
+ return sourceClient.setBreakpoint({
+ line: location.line,
+ column: location.column,
+ condition,
+ noSliding
+ }).then(res => onNewBreakpoint(location, res));
+ }
+
+ function onNewBreakpoint(location, res) {
+ var bpClient = res[1];
+ var actualLocation = res[0].actualLocation;
+ bpClients[bpClient.actor] = bpClient;
+
+ // Firefox only returns `actualLocation` if it actually changed,
+ // but we want it always to exist. Format `actualLocation` if it
+ // exists, otherwise use `location`.
+ actualLocation = actualLocation ? {
+ sourceId: actualLocation.source.actor,
+ line: actualLocation.line,
+ column: actualLocation.column
+ } : location;
+
+ return BreakpointResult({
+ id: bpClient.actor,
+ actualLocation: Location(actualLocation)
+ });
+ }
+
+ function removeBreakpoint(breakpointId) {
+ var bpClient = bpClients[breakpointId];
+ bpClients[breakpointId] = null;
+ return bpClient.remove();
+ }
+
+ function setBreakpointCondition(breakpointId, location, condition, noSliding) {
+ var bpClient = bpClients[breakpointId];
+ bpClients[breakpointId] = null;
+
+ return bpClient.setCondition(threadClient, condition, noSliding).then(_bpClient => onNewBreakpoint(location, [{}, _bpClient]));
+ }
+
+ function evaluate(script, _ref) {
+ var frameId = _ref.frameId;
+
+ var deferred = defer();
+ tabTarget.activeConsole.evaluateJS(script, result => {
+ deferred.resolve(result);
+ }, { frameActor: frameId });
+
+ return deferred.promise;
+ }
+
+ function debuggeeCommand(script) {
+ tabTarget.activeConsole.evaluateJS(script, () => {});
+
+ var consoleActor = tabTarget.form.consoleActor;
+ var request = debuggerClient._activeRequests.get(consoleActor);
+ request.emit("json-reply", {});
+ debuggerClient._activeRequests.delete(consoleActor);
+
+ return Promise.resolve();
+ }
+
+ function navigate(url) {
+ return tabTarget.activeTab.navigateTo(url);
+ }
+
+ function reload() {
+ return tabTarget.activeTab.reload();
+ }
+
+ function getProperties(grip) {
+ var objClient = threadClient.pauseGrip(grip);
+ return objClient.getPrototypeAndProperties();
+ }
+
+ function pauseOnExceptions(shouldPauseOnExceptions, shouldIgnoreCaughtExceptions) {
+ return threadClient.pauseOnExceptions(shouldPauseOnExceptions, shouldIgnoreCaughtExceptions);
+ }
+
+ function prettyPrint(sourceId, indentSize) {
+ var sourceClient = threadClient.source({ actor: sourceId });
+ return sourceClient.prettyPrint(indentSize);
+ }
+
+ function disablePrettyPrint(sourceId) {
+ var sourceClient = threadClient.source({ actor: sourceId });
+ return sourceClient.disablePrettyPrint();
+ }
+
+ var clientCommands = {
+ resume,
+ stepIn,
+ stepOut,
+ stepOver,
+ breakOnNext,
+ sourceContents,
+ setBreakpoint,
+ removeBreakpoint,
+ setBreakpointCondition,
+ evaluate,
+ debuggeeCommand,
+ navigate,
+ reload,
+ getProperties,
+ pauseOnExceptions,
+ prettyPrint,
+ disablePrettyPrint
+ };
+
+ module.exports = {
+ setupCommands,
+ clientCommands
+ };
+
+/***/ },
+/* 203 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var paused = (() => {
+ var _ref = _asyncToGenerator(function* (_, packet) {
+ // If paused by an explicit interrupt, which are generated by the
+ // slow script dialog and internal events such as setting
+ // breakpoints, ignore the event.
+ if (packet.why.type === "interrupted" && !packet.why.onNext) {
+ return;
+ }
+
+ // Eagerly fetch the frames
+ var response = yield threadClient.getFrames(0, CALL_STACK_PAGE_SIZE);
+ var frames = response.frames.map(createFrame);
+
+ var pause = Object.assign({}, packet, {
+ frame: createFrame(packet.frame),
+ frames: frames
+ });
+
+ actions.paused(pause);
+ });
+
+ return function paused(_x, _x2) {
+ return _ref.apply(this, arguments);
+ };
+ })();
+
+ function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; }
+
+ var _require = __webpack_require__(204);
+
+ var createFrame = _require.createFrame;
+ var createSource = _require.createSource;
+
+
+ var CALL_STACK_PAGE_SIZE = 1000;
+
+ var threadClient = void 0;
+ var actions = void 0;
+
+ function setupEvents(dependencies) {
+ threadClient = dependencies.threadClient;
+ actions = dependencies.actions;
+ }
+
+ function resumed(_, packet) {
+ actions.resumed(packet);
+ }
+
+ function newSource(_, _ref2) {
+ var source = _ref2.source;
+
+ actions.newSource(createSource(source));
+ }
+
+ var clientEvents = {
+ paused,
+ resumed,
+ newSource
+ };
+
+ module.exports = {
+ setupEvents,
+ clientEvents
+ };
+
+/***/ },
+/* 204 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var _require = __webpack_require__(145);
+
+ var Source = _require.Source;
+ var Frame = _require.Frame;
+ var Location = _require.Location;
+
+
+ function createFrame(frame) {
+ var title = void 0;
+ if (frame.type == "call") {
+ var c = frame.callee;
+ title = c.name || c.userDisplayName || c.displayName || "(anonymous)";
+ } else {
+ title = "(" + frame.type + ")";
+ }
+
+ return Frame({
+ id: frame.actor,
+ displayName: title,
+ location: Location({
+ sourceId: frame.where.source.actor,
+ line: frame.where.line,
+ column: frame.where.column
+ }),
+ this: frame.this,
+ scope: frame.environment
+ });
+ }
+
+ function createSource(source) {
+ return Source({
+ id: source.actor,
+ url: source.url,
+ isPrettyPrinted: false,
+ sourceMapURL: source.sourceMapURL
+ });
+ }
+
+ module.exports = { createFrame, createSource };
+
+/***/ },
+/* 205 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /* eslint-disable */
+
+ var _require = __webpack_require__(206);
+
+ var connect = _require.connect;
+
+ var defer = __webpack_require__(144);
+
+ var _require2 = __webpack_require__(145);
+
+ var Tab = _require2.Tab;
+
+ var _require3 = __webpack_require__(89);
+
+ var isEnabled = _require3.isEnabled;
+ var getValue = _require3.getValue;
+
+ var networkRequest = __webpack_require__(207);
+
+ var _require4 = __webpack_require__(208);
+
+ var setupCommands = _require4.setupCommands;
+ var clientCommands = _require4.clientCommands;
+
+ var _require5 = __webpack_require__(209);
+
+ var setupEvents = _require5.setupEvents;
+ var clientEvents = _require5.clientEvents;
+ var pageEvents = _require5.pageEvents;
+
+ // TODO: figure out a way to avoid patching native prototypes.
+ // Unfortunately the Chrome client requires it to work.
+
+ Array.prototype.peekLast = function () {
+ return this[this.length - 1];
+ };
+
+ var connection = void 0;
+
+ function createTabs(tabs) {
+
+ return tabs.filter(tab => {
+ var isPage = tab.type == "page";
+ return isPage;
+ }).map(tab => {
+ return Tab({
+ title: tab.title,
+ url: tab.url,
+ id: tab.id,
+ tab,
+ browser: "chrome"
+ });
+ });
+ }
+
+ function connectClient() {
+ var deferred = defer();
+
+ if (!getValue("chrome.debug")) {
+ return deferred.resolve(createTabs([]));
+ }
+
+ var webSocketPort = getValue("chrome.webSocketPort");
+ var url = `http://localhost:${ webSocketPort }/json/list`;
+ networkRequest(url).then(res => {
+ deferred.resolve(createTabs(JSON.parse(res.content)));
+ }).catch(err => {
+ console.log(err);
+ deferred.reject();
+ });
+
+ return deferred.promise;
+ }
+
+ function connectTab(tab) {
+ return connect(tab.webSocketDebuggerUrl).then(conn => {
+ connection = conn;
+ });
+ }
+
+ function connectNode(url) {
+ return connect(url).then(conn => {
+ connection = conn;
+ });
+ }
+
+ function initPage(actions) {
+ var agents = connection._agents;
+
+ setupCommands({ agents: agents });
+ setupEvents({ actions, agents });
+
+ agents.Debugger.enable();
+ agents.Debugger.setPauseOnExceptions("none");
+ agents.Debugger.setAsyncCallStackDepth(0);
+
+ agents.Runtime.enable();
+ agents.Runtime.run();
+
+ agents.Page.enable();
+
+ connection.registerDispatcher("Debugger", clientEvents);
+ connection.registerDispatcher("Page", pageEvents);
+ }
+
+ module.exports = {
+ connectClient,
+ clientCommands,
+ connectNode,
+ connectTab,
+ initPage
+ };
+
+/***/ },
+/* 206 */
+/***/ function(module, exports) {
+
+ module.exports = {};
+
+/***/ },
+/* 207 */
+/***/ function(module, exports) {
+
+ function networkRequest(url, opts) {
+ return new Promise((resolve, reject) => {
+ var req = new XMLHttpRequest();
+
+ req.addEventListener("readystatechange", () => {
+ if (req.readyState === XMLHttpRequest.DONE) {
+ if (req.status === 200) {
+ resolve({ content: req.responseText });
+ } else {
+ resolve(req.statusText);
+ }
+ }
+ });
+
+ // Not working yet.
+ // if (!opts.loadFromCache) {
+ // req.channel.loadFlags = (
+ // Components.interfaces.nsIRequest.LOAD_BYPASS_CACHE |
+ // Components.interfaces.nsIRequest.INHIBIT_CACHING |
+ // Components.interfaces.nsIRequest.LOAD_ANONYMOUS
+ // );
+ // }
+
+ req.open("GET", url);
+ req.send();
+ });
+ }
+
+ module.exports = networkRequest;
+
+/***/ },
+/* 208 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var _require = __webpack_require__(145);
+
+ var BreakpointResult = _require.BreakpointResult;
+ var Location = _require.Location;
+
+
+ var debuggerAgent = void 0;
+ var runtimeAgent = void 0;
+ var pageAgent = void 0;
+
+ function setupCommands(_ref) {
+ var agents = _ref.agents;
+
+ debuggerAgent = agents.Debugger;
+ runtimeAgent = agents.Runtime;
+ pageAgent = agents.Page;
+ }
+
+ function resume() {
+ return debuggerAgent.resume();
+ }
+
+ function stepIn() {
+ return debuggerAgent.stepInto();
+ }
+
+ function stepOver() {
+ return debuggerAgent.stepOver();
+ }
+
+ function stepOut() {
+ return debuggerAgent.stepOut();
+ }
+
+ function pauseOnExceptions(toggle) {
+ var state = toggle ? "uncaught" : "none";
+ return debuggerAgent.setPauseOnExceptions(state);
+ }
+
+ function breakOnNext() {
+ return debuggerAgent.pause();
+ }
+
+ function sourceContents(sourceId) {
+ return debuggerAgent.getScriptSource(sourceId, (err, contents) => ({
+ source: contents,
+ contentType: null
+ }));
+ }
+
+ function setBreakpoint(location, condition) {
+ return new Promise((resolve, reject) => {
+ return debuggerAgent.setBreakpoint({
+ scriptId: location.sourceId,
+ lineNumber: location.line - 1,
+ columnNumber: location.column
+ }, (err, breakpointId, actualLocation) => {
+ if (err) {
+ reject(err);
+ return;
+ }
+
+ actualLocation = actualLocation ? {
+ sourceId: actualLocation.scriptId,
+ line: actualLocation.lineNumber + 1,
+ column: actualLocation.columnNumber
+ } : location;
+
+ resolve(BreakpointResult({
+ id: breakpointId,
+ actualLocation: Location(actualLocation)
+ }));
+ });
+ });
+ }
+
+ function removeBreakpoint(breakpointId) {
+ // TODO: resolve promise when request is completed.
+ return new Promise((resolve, reject) => {
+ resolve(debuggerAgent.removeBreakpoint(breakpointId));
+ });
+ }
+
+ function evaluate(script) {
+ return runtimeAgent.evaluate(script, (_, result) => {
+ return result;
+ });
+ }
+
+ function debuggeeCommand(script) {
+ evaluate(script);
+ return Promise.resolve();
+ }
+
+ function navigate(url) {
+ return pageAgent.navigate(url, (_, result) => {
+ return result;
+ });
+ }
+
+ var clientCommands = {
+ resume,
+ stepIn,
+ stepOut,
+ stepOver,
+ pauseOnExceptions,
+ breakOnNext,
+ sourceContents,
+ setBreakpoint,
+ removeBreakpoint,
+ evaluate,
+ debuggeeCommand,
+ navigate
+ };
+
+ module.exports = {
+ setupCommands,
+ clientCommands
+ };
+
+/***/ },
+/* 209 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var paused = (() => {
+ var _ref = _asyncToGenerator(function* (callFrames, reason, data, hitBreakpoints, asyncStackTrace) {
+ var frames = callFrames.map(function (frame) {
+ return Frame({
+ id: frame.callFrameId,
+ displayName: frame.functionName,
+ location: Location({
+ sourceId: frame.location.scriptId,
+ line: frame.location.lineNumber + 1,
+ column: frame.location.columnNumber
+ })
+ });
+ });
+
+ var frame = frames[0];
+ var why = Object.assign({}, {
+ type: reason
+ }, data);
+
+ pageAgent.setOverlayMessage("Paused in debugger.html");
+
+ yield actions.paused({ frame, why, frames });
+ });
+
+ return function paused(_x, _x2, _x3, _x4, _x5) {
+ return _ref.apply(this, arguments);
+ };
+ })();
+
+ function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; }
+
+ var _require = __webpack_require__(145);
+
+ var Source = _require.Source;
+ var Location = _require.Location;
+ var Frame = _require.Frame;
+
+
+ var actions = void 0;
+ var pageAgent = void 0;
+
+ function setupEvents(dependencies) {
+ actions = dependencies.actions;
+ pageAgent = dependencies.agents.Page;
+ }
+
+ // Debugger Events
+ function scriptParsed(scriptId, url, startLine, startColumn, endLine, endColumn, executionContextId, hash, isContentScript, isInternalScript, isLiveEdit, sourceMapURL, hasSourceURL, deprecatedCommentWasUsed) {
+ if (isContentScript) {
+ return;
+ }
+
+ actions.newSource(Source({
+ id: scriptId,
+ url,
+ sourceMapURL,
+ isPrettyPrinted: false
+ }));
+ }
+
+ function scriptFailedToParse() {}
+
+ function resumed() {
+ pageAgent.setOverlayMessage(undefined);
+ actions.resumed();
+ }
+
+ function globalObjectCleared() {}
+
+ // Page Events
+ function frameNavigated(frame) {
+ actions.navigate();
+ }
+
+ function frameStartedLoading() {
+ actions.willNavigate();
+ }
+
+ function domContentEventFired() {}
+
+ function loadEventFired() {}
+
+ function frameStoppedLoading() {}
+
+ var clientEvents = {
+ scriptParsed,
+ scriptFailedToParse,
+ paused,
+ resumed,
+ globalObjectCleared
+ };
+
+ var pageEvents = {
+ frameNavigated,
+ frameStartedLoading,
+ domContentEventFired,
+ loadEventFired,
+ frameStoppedLoading
+ };
+
+ module.exports = {
+ setupEvents,
+ pageEvents,
+ clientEvents
+ };
+
+/***/ },
+/* 210 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var React = __webpack_require__(2);
+ var dom = React.DOM;
+
+ var classnames = __webpack_require__(211);
+
+ var _require = __webpack_require__(89);
+
+ var getValue = _require.getValue;
+ var isDevelopment = _require.isDevelopment;
+
+
+ __webpack_require__(212);
+
+ function themeClass() {
+ var theme = getValue("theme");
+ return `theme-${ theme }`;
+ }
+
+ module.exports = function (component) {
+ return dom.div({
+ className: classnames("theme-body", { [themeClass()]: isDevelopment() }),
+ style: { flex: 1 }
+ }, React.createElement(component));
+ };
+
+/***/ },
+/* 211 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/*!
+ Copyright (c) 2016 Jed Watson.
+ Licensed under the MIT License (MIT), see
+ http://jedwatson.github.io/classnames
+ */
+ /* global define */
+
+ (function () {
+ 'use strict';
+
+ var hasOwn = {}.hasOwnProperty;
+
+ function classNames () {
+ var classes = [];
+
+ for (var i = 0; i < arguments.length; i++) {
+ var arg = arguments[i];
+ if (!arg) continue;
+
+ var argType = typeof arg;
+
+ if (argType === 'string' || argType === 'number') {
+ classes.push(arg);
+ } else if (Array.isArray(arg)) {
+ classes.push(classNames.apply(null, arg));
+ } else if (argType === 'object') {
+ for (var key in arg) {
+ if (hasOwn.call(arg, key) && arg[key]) {
+ classes.push(key);
+ }
+ }
+ }
+ }
+
+ return classes.join(' ');
+ }
+
+ if (typeof module !== 'undefined' && module.exports) {
+ module.exports = classNames;
+ } else if (true) {
+ // register as 'classnames', consistent with npm package name
+ !(__WEBPACK_AMD_DEFINE_ARRAY__ = [], __WEBPACK_AMD_DEFINE_RESULT__ = function () {
+ return classNames;
+ }.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+ } else {
+ window.classNames = classNames;
+ }
+ }());
+
+
+/***/ },
+/* 212 */
+/***/ function(module, exports) {
+
+ // removed by extract-text-webpack-plugin
+
+/***/ },
+/* 213 */,
+/* 214 */,
+/* 215 */,
+/* 216 */
+/***/ function(module, exports, __webpack_require__) {
+
+
+
+ /* 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/. */
+ /* global window */
+
+ var _require = __webpack_require__(3);
+
+ var createStore = _require.createStore;
+ var applyMiddleware = _require.applyMiddleware;
+
+ var _require2 = __webpack_require__(217);
+
+ var waitUntilService = _require2.waitUntilService;
+
+ var _require3 = __webpack_require__(218);
+
+ var log = _require3.log;
+
+ var _require4 = __webpack_require__(219);
+
+ var history = _require4.history;
+
+ var _require5 = __webpack_require__(220);
+
+ var promise = _require5.promise;
+
+ var _require6 = __webpack_require__(225);
+
+ var thunk = _require6.thunk;
+
+
+ /**
+ * This creates a dispatcher with all the standard middleware in place
+ * that all code requires. It can also be optionally configured in
+ * various ways, such as logging and recording.
+ *
+ * @param {object} opts:
+ * - log: log all dispatched actions to console
+ * - history: an array to store every action in. Should only be
+ * used in tests.
+ * - middleware: array of middleware to be included in the redux store
+ */
+ var configureStore = function () {
+ var opts = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
+
+ var middleware = [thunk(opts.makeThunkArgs), promise,
+
+ // Order is important: services must go last as they always
+ // operate on "already transformed" actions. Actions going through
+ // them shouldn't have any special fields like promises, they
+ // should just be normal JSON objects.
+ waitUntilService];
+
+ if (opts.history) {
+ middleware.push(history(opts.history));
+ }
+
+ if (opts.middleware) {
+ opts.middleware.forEach(fn => middleware.push(fn));
+ }
+
+ if (opts.log) {
+ middleware.push(log);
+ }
+
+ // Hook in the redux devtools browser extension if it exists
+ var devtoolsExt = typeof window === "object" && window.devToolsExtension ? window.devToolsExtension() : f => f;
+
+ return applyMiddleware.apply(undefined, middleware)(devtoolsExt(createStore));
+ };
+
+ module.exports = configureStore;
+
+/***/ },
+/* 217 */
+/***/ function(module, exports) {
+
+ /* 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";
+
+ /**
+ * A middleware which acts like a service, because it is stateful
+ * and "long-running" in the background. It provides the ability
+ * for actions to install a function to be run once when a specific
+ * condition is met by an action coming through the system. Think of
+ * it as a thunk that blocks until the condition is met. Example:
+ *
+ * ```js
+ * const services = { WAIT_UNTIL: require('wait-service').NAME };
+ *
+ * { type: services.WAIT_UNTIL,
+ * predicate: action => action.type === constants.ADD_ITEM,
+ * run: (dispatch, getState, action) => {
+ * // Do anything here. You only need to accept the arguments
+ * // if you need them. `action` is the action that satisfied
+ * // the predicate.
+ * }
+ * }
+ * ```
+ */
+
+ var NAME = exports.NAME = "@@service/waitUntil";
+
+ function waitUntilService(_ref) {
+ var dispatch = _ref.dispatch;
+ var getState = _ref.getState;
+
+ var pending = [];
+
+ function checkPending(action) {
+ var readyRequests = [];
+ var stillPending = [];
+
+ // Find the pending requests whose predicates are satisfied with
+ // this action. Wait to run the requests until after we update the
+ // pending queue because the request handler may synchronously
+ // dispatch again and run this service (that use case is
+ // completely valid).
+ for (var request of pending) {
+ if (request.predicate(action)) {
+ readyRequests.push(request);
+ } else {
+ stillPending.push(request);
+ }
+ }
+
+ pending = stillPending;
+ for (var _request of readyRequests) {
+ _request.run(dispatch, getState, action);
+ }
+ }
+
+ return next => action => {
+ if (action.type === NAME) {
+ pending.push(action);
+ return null;
+ }
+ var result = next(action);
+ checkPending(action);
+ return result;
+ };
+ }
+ exports.waitUntilService = waitUntilService;
+
+/***/ },
+/* 218 */
+/***/ function(module, exports) {
+
+ /**
+ * A middleware that logs all actions coming through the system
+ * to the console.
+ */
+ function log(_ref) {
+ var dispatch = _ref.dispatch;
+ var getState = _ref.getState;
+
+ return next => action => {
+ var actionText = JSON.stringify(action, null, 2);
+ var truncatedActionText = actionText.slice(0, 1000) + "...";
+ console.log(`[DISPATCH ${ action.type }]`, action, truncatedActionText);
+ next(action);
+ };
+ }
+
+ exports.log = log;
+
+/***/ },
+/* 219 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /* 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 _require = __webpack_require__(89);
+
+ var isDevelopment = _require.isDevelopment;
+
+ /**
+ * A middleware that stores every action coming through the store in the passed
+ * in logging object. Should only be used for tests, as it collects all
+ * action information, which will cause memory bloat.
+ */
+
+ exports.history = function () {
+ var log = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
+ return (_ref) => {
+ var dispatch = _ref.dispatch;
+ var getState = _ref.getState;
+
+ if (isDevelopment()) {
+ console.warn("Using history middleware stores all actions in state for " + "testing and devtools is not currently running in test " + "mode. Be sure this is intentional.");
+ }
+ return next => action => {
+ log.push(action);
+ next(action);
+ };
+ };
+ };
+
+/***/ },
+/* 220 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /* 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 defer = __webpack_require__(144);
+
+ var _require = __webpack_require__(221);
+
+ var entries = _require.entries;
+ var toObject = _require.toObject;
+
+ var _require2 = __webpack_require__(223);
+
+ var executeSoon = _require2.executeSoon;
+
+
+ var PROMISE = exports.PROMISE = "@@dispatch/promise";
+ var seqIdVal = 1;
+
+ function seqIdGen() {
+ return seqIdVal++;
+ }
+
+ function promiseMiddleware(_ref) {
+ var dispatch = _ref.dispatch;
+ var getState = _ref.getState;
+
+ return next => action => {
+ if (!(PROMISE in action)) {
+ return next(action);
+ }
+
+ var promiseInst = action[PROMISE];
+ var seqId = seqIdGen().toString();
+
+ // Create a new action that doesn't have the promise field and has
+ // the `seqId` field that represents the sequence id
+ action = Object.assign(toObject(entries(action).filter(pair => pair[0] !== PROMISE)), { seqId });
+
+ dispatch(Object.assign({}, action, { status: "start" }));
+
+ // Return the promise so action creators can still compose if they
+ // want to.
+ var deferred = defer();
+ promiseInst.then(value => {
+ executeSoon(() => {
+ dispatch(Object.assign({}, action, {
+ status: "done",
+ value: value
+ }));
+ deferred.resolve(value);
+ });
+ }, error => {
+ executeSoon(() => {
+ dispatch(Object.assign({}, action, {
+ status: "error",
+ error: error.message || error
+ }));
+ deferred.reject(error);
+ });
+ });
+ return deferred.promise;
+ };
+ }
+
+ exports.promise = promiseMiddleware;
+
+/***/ },
+/* 221 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }();
+
+ function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
+
+ /* -*- 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/. */
+
+ var co = __webpack_require__(222);
+
+ function asPaused(client, func) {
+ if (client.state != "paused") {
+ return co(function* () {
+ yield client.interrupt();
+ var result = void 0;
+
+ try {
+ result = yield func();
+ } catch (e) {
+ // Try to put the debugger back in a working state by resuming
+ // it
+ yield client.resume();
+ throw e;
+ }
+
+ yield client.resume();
+ return result;
+ });
+ }
+ return func();
+ }
+
+ function handleError(err) {
+ console.log("ERROR: ", err);
+ }
+
+ function promisify(context, method) {
+ for (var _len = arguments.length, args = Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) {
+ args[_key - 2] = arguments[_key];
+ }
+
+ return new Promise((resolve, reject) => {
+ args.push(response => {
+ if (response.error) {
+ reject(response);
+ } else {
+ resolve(response);
+ }
+ });
+ method.apply(context, args);
+ });
+ }
+
+ function truncateStr(str, size) {
+ if (str.length > size) {
+ return str.slice(0, size) + "...";
+ }
+ return str;
+ }
+
+ function endTruncateStr(str, size) {
+ if (str.length > size) {
+ return "..." + str.slice(str.length - size);
+ }
+ return str;
+ }
+
+ var msgId = 1;
+ function workerTask(worker, method) {
+ return function () {
+ for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
+ args[_key2] = arguments[_key2];
+ }
+
+ return new Promise((resolve, reject) => {
+ var id = msgId++;
+ worker.postMessage({ id, method, args });
+
+ var listener = (_ref) => {
+ var result = _ref.data;
+
+ if (result.id !== id) {
+ return;
+ }
+
+ worker.removeEventListener("message", listener);
+ if (result.error) {
+ reject(result.error);
+ } else {
+ resolve(result.response);
+ }
+ };
+
+ worker.addEventListener("message", listener);
+ });
+ };
+ }
+
+ /**
+ * Interleaves two arrays element by element, returning the combined array, like
+ * a zip. In the case of arrays with different sizes, undefined values will be
+ * interleaved at the end along with the extra values of the larger array.
+ *
+ * @param Array a
+ * @param Array b
+ * @returns Array
+ * The combined array, in the form [a1, b1, a2, b2, ...]
+ */
+ function zip(a, b) {
+ if (!b) {
+ return a;
+ }
+ if (!a) {
+ return b;
+ }
+ var pairs = [];
+ for (var i = 0, aLength = a.length, bLength = b.length; i < aLength || i < bLength; i++) {
+ pairs.push([a[i], b[i]]);
+ }
+ return pairs;
+ }
+
+ /**
+ * Converts an object into an array with 2-element arrays as key/value
+ * pairs of the object. `{ foo: 1, bar: 2}` would become
+ * `[[foo, 1], [bar 2]]` (order not guaranteed);
+ *
+ * @param object obj
+ * @returns array
+ */
+ function entries(obj) {
+ return Object.keys(obj).map(k => [k, obj[k]]);
+ }
+
+ function mapObject(obj, iteratee) {
+ return toObject(entries(obj).map((_ref2) => {
+ var _ref3 = _slicedToArray(_ref2, 2);
+
+ var key = _ref3[0];
+ var value = _ref3[1];
+
+ return [key, iteratee(key, value)];
+ }));
+ }
+
+ /**
+ * Takes an array of 2-element arrays as key/values pairs and
+ * constructs an object using them.
+ */
+ function toObject(arr) {
+ var obj = {};
+ for (var pair of arr) {
+ obj[pair[0]] = pair[1];
+ }
+ return obj;
+ }
+
+ /**
+ * Composes the given functions into a single function, which will
+ * apply the results of each function right-to-left, starting with
+ * applying the given arguments to the right-most function.
+ * `compose(foo, bar, baz)` === `args => foo(bar(baz(args)`
+ *
+ * @param ...function funcs
+ * @returns function
+ */
+ function compose() {
+ for (var _len3 = arguments.length, funcs = Array(_len3), _key3 = 0; _key3 < _len3; _key3++) {
+ funcs[_key3] = arguments[_key3];
+ }
+
+ return function () {
+ for (var _len4 = arguments.length, args = Array(_len4), _key4 = 0; _key4 < _len4; _key4++) {
+ args[_key4] = arguments[_key4];
+ }
+
+ var initialValue = funcs[funcs.length - 1].apply(null, args);
+ var leftFuncs = funcs.slice(0, -1);
+ return leftFuncs.reduceRight((composed, f) => f(composed), initialValue);
+ };
+ }
+
+ function updateObj(obj, fields) {
+ return Object.assign({}, obj, fields);
+ }
+
+ function throttle(func, ms) {
+ var timeout = void 0,
+ _this = void 0;
+ return function () {
+ for (var _len5 = arguments.length, args = Array(_len5), _key5 = 0; _key5 < _len5; _key5++) {
+ args[_key5] = arguments[_key5];
+ }
+
+ _this = this;
+ if (!timeout) {
+ timeout = setTimeout(() => {
+ func.apply.apply(func, [_this].concat(_toConsumableArray(args)));
+ timeout = null;
+ }, ms);
+ }
+ };
+ }
+
+ module.exports = {
+ asPaused,
+ handleError,
+ promisify,
+ truncateStr,
+ endTruncateStr,
+ workerTask,
+ zip,
+ entries,
+ toObject,
+ mapObject,
+ compose,
+ updateObj,
+ throttle
+ };
+
+/***/ },
+/* 222 */
+/***/ function(module, exports) {
+
+
+ /**
+ * slice() reference.
+ */
+
+ var slice = Array.prototype.slice;
+
+ /**
+ * Expose `co`.
+ */
+
+ module.exports = co['default'] = co.co = co;
+
+ /**
+ * Wrap the given generator `fn` into a
+ * function that returns a promise.
+ * This is a separate function so that
+ * every `co()` call doesn't create a new,
+ * unnecessary closure.
+ *
+ * @param {GeneratorFunction} fn
+ * @return {Function}
+ * @api public
+ */
+
+ co.wrap = function (fn) {
+ createPromise.__generatorFunction__ = fn;
+ return createPromise;
+ function createPromise() {
+ return co.call(this, fn.apply(this, arguments));
+ }
+ };
+
+ /**
+ * Execute the generator function or a generator
+ * and return a promise.
+ *
+ * @param {Function} fn
+ * @return {Promise}
+ * @api public
+ */
+
+ function co(gen) {
+ var ctx = this;
+ var args = slice.call(arguments, 1)
+
+ // we wrap everything in a promise to avoid promise chaining,
+ // which leads to memory leak errors.
+ // see https://github.com/tj/co/issues/180
+ return new Promise(function(resolve, reject) {
+ if (typeof gen === 'function') gen = gen.apply(ctx, args);
+ if (!gen || typeof gen.next !== 'function') return resolve(gen);
+
+ onFulfilled();
+
+ /**
+ * @param {Mixed} res
+ * @return {Promise}
+ * @api private
+ */
+
+ function onFulfilled(res) {
+ var ret;
+ try {
+ ret = gen.next(res);
+ } catch (e) {
+ return reject(e);
+ }
+ next(ret);
+ }
+
+ /**
+ * @param {Error} err
+ * @return {Promise}
+ * @api private
+ */
+
+ function onRejected(err) {
+ var ret;
+ try {
+ ret = gen.throw(err);
+ } catch (e) {
+ return reject(e);
+ }
+ next(ret);
+ }
+
+ /**
+ * Get the next value in the generator,
+ * return a promise.
+ *
+ * @param {Object} ret
+ * @return {Promise}
+ * @api private
+ */
+
+ function next(ret) {
+ if (ret.done) return resolve(ret.value);
+ var value = toPromise.call(ctx, ret.value);
+ if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
+ return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
+ + 'but the following object was passed: "' + String(ret.value) + '"'));
+ }
+ });
+ }
+
+ /**
+ * Convert a `yield`ed value into a promise.
+ *
+ * @param {Mixed} obj
+ * @return {Promise}
+ * @api private
+ */
+
+ function toPromise(obj) {
+ if (!obj) return obj;
+ if (isPromise(obj)) return obj;
+ if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
+ if ('function' == typeof obj) return thunkToPromise.call(this, obj);
+ if (Array.isArray(obj)) return arrayToPromise.call(this, obj);
+ if (isObject(obj)) return objectToPromise.call(this, obj);
+ return obj;
+ }
+
+ /**
+ * Convert a thunk to a promise.
+ *
+ * @param {Function}
+ * @return {Promise}
+ * @api private
+ */
+
+ function thunkToPromise(fn) {
+ var ctx = this;
+ return new Promise(function (resolve, reject) {
+ fn.call(ctx, function (err, res) {
+ if (err) return reject(err);
+ if (arguments.length > 2) res = slice.call(arguments, 1);
+ resolve(res);
+ });
+ });
+ }
+
+ /**
+ * Convert an array of "yieldables" to a promise.
+ * Uses `Promise.all()` internally.
+ *
+ * @param {Array} obj
+ * @return {Promise}
+ * @api private
+ */
+
+ function arrayToPromise(obj) {
+ return Promise.all(obj.map(toPromise, this));
+ }
+
+ /**
+ * Convert an object of "yieldables" to a promise.
+ * Uses `Promise.all()` internally.
+ *
+ * @param {Object} obj
+ * @return {Promise}
+ * @api private
+ */
+
+ function objectToPromise(obj){
+ var results = new obj.constructor();
+ var keys = Object.keys(obj);
+ var promises = [];
+ for (var i = 0; i < keys.length; i++) {
+ var key = keys[i];
+ var promise = toPromise.call(this, obj[key]);
+ if (promise && isPromise(promise)) defer(promise, key);
+ else results[key] = obj[key];
+ }
+ return Promise.all(promises).then(function () {
+ return results;
+ });
+
+ function defer(promise, key) {
+ // predefine the key in the result
+ results[key] = undefined;
+ promises.push(promise.then(function (res) {
+ results[key] = res;
+ }));
+ }
+ }
+
+ /**
+ * Check if `obj` is a promise.
+ *
+ * @param {Object} obj
+ * @return {Boolean}
+ * @api private
+ */
+
+ function isPromise(obj) {
+ return 'function' == typeof obj.then;
+ }
+
+ /**
+ * Check if `obj` is a generator.
+ *
+ * @param {Mixed} obj
+ * @return {Boolean}
+ * @api private
+ */
+
+ function isGenerator(obj) {
+ return 'function' == typeof obj.next && 'function' == typeof obj.throw;
+ }
+
+ /**
+ * Check if `obj` is a generator function.
+ *
+ * @param {Mixed} obj
+ * @return {Boolean}
+ * @api private
+ */
+ function isGeneratorFunction(obj) {
+ var constructor = obj.constructor;
+ if (!constructor) return false;
+ if ('GeneratorFunction' === constructor.name || 'GeneratorFunction' === constructor.displayName) return true;
+ return isGenerator(constructor.prototype);
+ }
+
+ /**
+ * Check for plain object.
+ *
+ * @param {Mixed} val
+ * @return {Boolean}
+ * @api private
+ */
+
+ function isObject(val) {
+ return Object == val.constructor;
+ }
+
+
+/***/ },
+/* 223 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var assert = __webpack_require__(224);
+
+ function reportException(who, exception) {
+ var msg = who + " threw an exception: ";
+ console.error(msg, exception);
+ }
+
+ function executeSoon(fn) {
+ setTimeout(fn, 0);
+ }
+
+ module.exports = {
+ reportException,
+ executeSoon,
+ assert
+ };
+
+/***/ },
+/* 224 */
+/***/ function(module, exports) {
+
+ function assert(condition, message) {
+ if (!condition) {
+ throw new Error("Assertion failure: " + message);
+ }
+ }
+
+ module.exports = assert;
+
+/***/ },
+/* 225 */
+/***/ function(module, exports) {
+
+
+ /**
+ * A middleware that allows thunks (functions) to be dispatched. If
+ * it's a thunk, it is called with an argument that contains
+ * `dispatch`, `getState`, and any additional args passed in via the
+ * middleware constructure. This allows the action to create multiple
+ * actions (most likely asynchronously).
+ */
+ function thunk(makeArgs) {
+ return (_ref) => {
+ var dispatch = _ref.dispatch;
+ var getState = _ref.getState;
+
+ var args = { dispatch, getState };
+
+ return next => action => {
+ return typeof action === "function" ? action(makeArgs ? makeArgs(args, getState()) : args) : next(action);
+ };
+ };
+ }
+ exports.thunk = thunk;
+
+/***/ },
+/* 226 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var tabs = __webpack_require__(227);
+
+ module.exports = {
+ tabs
+ };
+
+/***/ },
+/* 227 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /* 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 constants = __webpack_require__(228);
+ var Immutable = __webpack_require__(229);
+ var fromJS = __webpack_require__(230);
+
+ var initialState = fromJS({
+ tabs: {},
+ selectedTab: null
+ });
+
+ function update() {
+ var state = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : initialState;
+ var action = arguments[1];
+
+ switch (action.type) {
+ case constants.ADD_TABS:
+ var tabs = action.value;
+ if (!tabs) {
+ return state;
+ }
+
+ return state.mergeIn(["tabs"], Immutable.Map(tabs.map(tab => {
+ tab = Object.assign({}, tab, { id: getTabId(tab) });
+ return [tab.id, Immutable.Map(tab)];
+ })));
+ case constants.SELECT_TAB:
+ var tab = state.getIn(["tabs", action.id]);
+ return state.setIn(["selectedTab"], tab);
+ }
+
+ return state;
+ }
+
+ function getTabId(tab) {
+ var id = tab.id;
+ var isFirefox = tab.browser == "firefox";
+
+ // NOTE: we're getting the last part of the actor because
+ // we want to ignore the connection id
+ if (isFirefox) {
+ id = tab.id.split(".").pop();
+ }
+
+ return id;
+ }
+
+ module.exports = update;
+
+/***/ },
+/* 228 */
+/***/ function(module, exports) {
+
+ /* -*- 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/. */
+
+ exports.ADD_TABS = "ADD_TABS";
+ exports.SELECT_TAB = "SELECT_TAB";
+
+/***/ },
+/* 229 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /**
+ * Copyright (c) 2014-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+ (function (global, factory) {
+ true ? module.exports = factory() :
+ typeof define === 'function' && define.amd ? define(factory) :
+ (global.Immutable = factory());
+ }(this, function () { 'use strict';var SLICE$0 = Array.prototype.slice;
+
+ function createClass(ctor, superClass) {
+ if (superClass) {
+ ctor.prototype = Object.create(superClass.prototype);
+ }
+ ctor.prototype.constructor = ctor;
+ }
+
+ function Iterable(value) {
+ return isIterable(value) ? value : Seq(value);
+ }
+
+
+ createClass(KeyedIterable, Iterable);
+ function KeyedIterable(value) {
+ return isKeyed(value) ? value : KeyedSeq(value);
+ }
+
+
+ createClass(IndexedIterable, Iterable);
+ function IndexedIterable(value) {
+ return isIndexed(value) ? value : IndexedSeq(value);
+ }
+
+
+ createClass(SetIterable, Iterable);
+ function SetIterable(value) {
+ return isIterable(value) && !isAssociative(value) ? value : SetSeq(value);
+ }
+
+
+
+ function isIterable(maybeIterable) {
+ return !!(maybeIterable && maybeIterable[IS_ITERABLE_SENTINEL]);
+ }
+
+ function isKeyed(maybeKeyed) {
+ return !!(maybeKeyed && maybeKeyed[IS_KEYED_SENTINEL]);
+ }
+
+ function isIndexed(maybeIndexed) {
+ return !!(maybeIndexed && maybeIndexed[IS_INDEXED_SENTINEL]);
+ }
+
+ function isAssociative(maybeAssociative) {
+ return isKeyed(maybeAssociative) || isIndexed(maybeAssociative);
+ }
+
+ function isOrdered(maybeOrdered) {
+ return !!(maybeOrdered && maybeOrdered[IS_ORDERED_SENTINEL]);
+ }
+
+ Iterable.isIterable = isIterable;
+ Iterable.isKeyed = isKeyed;
+ Iterable.isIndexed = isIndexed;
+ Iterable.isAssociative = isAssociative;
+ Iterable.isOrdered = isOrdered;
+
+ Iterable.Keyed = KeyedIterable;
+ Iterable.Indexed = IndexedIterable;
+ Iterable.Set = SetIterable;
+
+
+ var IS_ITERABLE_SENTINEL = '@@__IMMUTABLE_ITERABLE__@@';
+ var IS_KEYED_SENTINEL = '@@__IMMUTABLE_KEYED__@@';
+ var IS_INDEXED_SENTINEL = '@@__IMMUTABLE_INDEXED__@@';
+ var IS_ORDERED_SENTINEL = '@@__IMMUTABLE_ORDERED__@@';
+
+ // Used for setting prototype methods that IE8 chokes on.
+ var DELETE = 'delete';
+
+ // Constants describing the size of trie nodes.
+ var SHIFT = 5; // Resulted in best performance after ______?
+ var SIZE = 1 << SHIFT;
+ var MASK = SIZE - 1;
+
+ // A consistent shared value representing "not set" which equals nothing other
+ // than itself, and nothing that could be provided externally.
+ var NOT_SET = {};
+
+ // Boolean references, Rough equivalent of `bool &`.
+ var CHANGE_LENGTH = { value: false };
+ var DID_ALTER = { value: false };
+
+ function MakeRef(ref) {
+ ref.value = false;
+ return ref;
+ }
+
+ function SetRef(ref) {
+ ref && (ref.value = true);
+ }
+
+ // A function which returns a value representing an "owner" for transient writes
+ // to tries. The return value will only ever equal itself, and will not equal
+ // the return of any subsequent call of this function.
+ function OwnerID() {}
+
+ // http://jsperf.com/copy-array-inline
+ function arrCopy(arr, offset) {
+ offset = offset || 0;
+ var len = Math.max(0, arr.length - offset);
+ var newArr = new Array(len);
+ for (var ii = 0; ii < len; ii++) {
+ newArr[ii] = arr[ii + offset];
+ }
+ return newArr;
+ }
+
+ function ensureSize(iter) {
+ if (iter.size === undefined) {
+ iter.size = iter.__iterate(returnTrue);
+ }
+ return iter.size;
+ }
+
+ function wrapIndex(iter, index) {
+ // This implements "is array index" which the ECMAString spec defines as:
+ //
+ // A String property name P is an array index if and only if
+ // ToString(ToUint32(P)) is equal to P and ToUint32(P) is not equal
+ // to 2^32−1.
+ //
+ // http://www.ecma-international.org/ecma-262/6.0/#sec-array-exotic-objects
+ if (typeof index !== 'number') {
+ var uint32Index = index >>> 0; // N >>> 0 is shorthand for ToUint32
+ if ('' + uint32Index !== index || uint32Index === 4294967295) {
+ return NaN;
+ }
+ index = uint32Index;
+ }
+ return index < 0 ? ensureSize(iter) + index : index;
+ }
+
+ function returnTrue() {
+ return true;
+ }
+
+ function wholeSlice(begin, end, size) {
+ return (begin === 0 || (size !== undefined && begin <= -size)) &&
+ (end === undefined || (size !== undefined && end >= size));
+ }
+
+ function resolveBegin(begin, size) {
+ return resolveIndex(begin, size, 0);
+ }
+
+ function resolveEnd(end, size) {
+ return resolveIndex(end, size, size);
+ }
+
+ function resolveIndex(index, size, defaultIndex) {
+ return index === undefined ?
+ defaultIndex :
+ index < 0 ?
+ Math.max(0, size + index) :
+ size === undefined ?
+ index :
+ Math.min(size, index);
+ }
+
+ /* global Symbol */
+
+ var ITERATE_KEYS = 0;
+ var ITERATE_VALUES = 1;
+ var ITERATE_ENTRIES = 2;
+
+ var REAL_ITERATOR_SYMBOL = typeof Symbol === 'function' && Symbol.iterator;
+ var FAUX_ITERATOR_SYMBOL = '@@iterator';
+
+ var ITERATOR_SYMBOL = REAL_ITERATOR_SYMBOL || FAUX_ITERATOR_SYMBOL;
+
+
+ function Iterator(next) {
+ this.next = next;
+ }
+
+ Iterator.prototype.toString = function() {
+ return '[Iterator]';
+ };
+
+
+ Iterator.KEYS = ITERATE_KEYS;
+ Iterator.VALUES = ITERATE_VALUES;
+ Iterator.ENTRIES = ITERATE_ENTRIES;
+
+ Iterator.prototype.inspect =
+ Iterator.prototype.toSource = function () { return this.toString(); }
+ Iterator.prototype[ITERATOR_SYMBOL] = function () {
+ return this;
+ };
+
+
+ function iteratorValue(type, k, v, iteratorResult) {
+ var value = type === 0 ? k : type === 1 ? v : [k, v];
+ iteratorResult ? (iteratorResult.value = value) : (iteratorResult = {
+ value: value, done: false
+ });
+ return iteratorResult;
+ }
+
+ function iteratorDone() {
+ return { value: undefined, done: true };
+ }
+
+ function hasIterator(maybeIterable) {
+ return !!getIteratorFn(maybeIterable);
+ }
+
+ function isIterator(maybeIterator) {
+ return maybeIterator && typeof maybeIterator.next === 'function';
+ }
+
+ function getIterator(iterable) {
+ var iteratorFn = getIteratorFn(iterable);
+ return iteratorFn && iteratorFn.call(iterable);
+ }
+
+ function getIteratorFn(iterable) {
+ var iteratorFn = iterable && (
+ (REAL_ITERATOR_SYMBOL && iterable[REAL_ITERATOR_SYMBOL]) ||
+ iterable[FAUX_ITERATOR_SYMBOL]
+ );
+ if (typeof iteratorFn === 'function') {
+ return iteratorFn;
+ }
+ }
+
+ function isArrayLike(value) {
+ return value && typeof value.length === 'number';
+ }
+
+ createClass(Seq, Iterable);
+ function Seq(value) {
+ return value === null || value === undefined ? emptySequence() :
+ isIterable(value) ? value.toSeq() : seqFromValue(value);
+ }
+
+ Seq.of = function(/*...values*/) {
+ return Seq(arguments);
+ };
+
+ Seq.prototype.toSeq = function() {
+ return this;
+ };
+
+ Seq.prototype.toString = function() {
+ return this.__toString('Seq {', '}');
+ };
+
+ Seq.prototype.cacheResult = function() {
+ if (!this._cache && this.__iterateUncached) {
+ this._cache = this.entrySeq().toArray();
+ this.size = this._cache.length;
+ }
+ return this;
+ };
+
+ // abstract __iterateUncached(fn, reverse)
+
+ Seq.prototype.__iterate = function(fn, reverse) {
+ return seqIterate(this, fn, reverse, true);
+ };
+
+ // abstract __iteratorUncached(type, reverse)
+
+ Seq.prototype.__iterator = function(type, reverse) {
+ return seqIterator(this, type, reverse, true);
+ };
+
+
+
+ createClass(KeyedSeq, Seq);
+ function KeyedSeq(value) {
+ return value === null || value === undefined ?
+ emptySequence().toKeyedSeq() :
+ isIterable(value) ?
+ (isKeyed(value) ? value.toSeq() : value.fromEntrySeq()) :
+ keyedSeqFromValue(value);
+ }
+
+ KeyedSeq.prototype.toKeyedSeq = function() {
+ return this;
+ };
+
+
+
+ createClass(IndexedSeq, Seq);
+ function IndexedSeq(value) {
+ return value === null || value === undefined ? emptySequence() :
+ !isIterable(value) ? indexedSeqFromValue(value) :
+ isKeyed(value) ? value.entrySeq() : value.toIndexedSeq();
+ }
+
+ IndexedSeq.of = function(/*...values*/) {
+ return IndexedSeq(arguments);
+ };
+
+ IndexedSeq.prototype.toIndexedSeq = function() {
+ return this;
+ };
+
+ IndexedSeq.prototype.toString = function() {
+ return this.__toString('Seq [', ']');
+ };
+
+ IndexedSeq.prototype.__iterate = function(fn, reverse) {
+ return seqIterate(this, fn, reverse, false);
+ };
+
+ IndexedSeq.prototype.__iterator = function(type, reverse) {
+ return seqIterator(this, type, reverse, false);
+ };
+
+
+
+ createClass(SetSeq, Seq);
+ function SetSeq(value) {
+ return (
+ value === null || value === undefined ? emptySequence() :
+ !isIterable(value) ? indexedSeqFromValue(value) :
+ isKeyed(value) ? value.entrySeq() : value
+ ).toSetSeq();
+ }
+
+ SetSeq.of = function(/*...values*/) {
+ return SetSeq(arguments);
+ };
+
+ SetSeq.prototype.toSetSeq = function() {
+ return this;
+ };
+
+
+
+ Seq.isSeq = isSeq;
+ Seq.Keyed = KeyedSeq;
+ Seq.Set = SetSeq;
+ Seq.Indexed = IndexedSeq;
+
+ var IS_SEQ_SENTINEL = '@@__IMMUTABLE_SEQ__@@';
+
+ Seq.prototype[IS_SEQ_SENTINEL] = true;
+
+
+
+ createClass(ArraySeq, IndexedSeq);
+ function ArraySeq(array) {
+ this._array = array;
+ this.size = array.length;
+ }
+
+ ArraySeq.prototype.get = function(index, notSetValue) {
+ return this.has(index) ? this._array[wrapIndex(this, index)] : notSetValue;
+ };
+
+ ArraySeq.prototype.__iterate = function(fn, reverse) {
+ var array = this._array;
+ var maxIndex = array.length - 1;
+ for (var ii = 0; ii <= maxIndex; ii++) {
+ if (fn(array[reverse ? maxIndex - ii : ii], ii, this) === false) {
+ return ii + 1;
+ }
+ }
+ return ii;
+ };
+
+ ArraySeq.prototype.__iterator = function(type, reverse) {
+ var array = this._array;
+ var maxIndex = array.length - 1;
+ var ii = 0;
+ return new Iterator(function()
+ {return ii > maxIndex ?
+ iteratorDone() :
+ iteratorValue(type, ii, array[reverse ? maxIndex - ii++ : ii++])}
+ );
+ };
+
+
+
+ createClass(ObjectSeq, KeyedSeq);
+ function ObjectSeq(object) {
+ var keys = Object.keys(object);
+ this._object = object;
+ this._keys = keys;
+ this.size = keys.length;
+ }
+
+ ObjectSeq.prototype.get = function(key, notSetValue) {
+ if (notSetValue !== undefined && !this.has(key)) {
+ return notSetValue;
+ }
+ return this._object[key];
+ };
+
+ ObjectSeq.prototype.has = function(key) {
+ return this._object.hasOwnProperty(key);
+ };
+
+ ObjectSeq.prototype.__iterate = function(fn, reverse) {
+ var object = this._object;
+ var keys = this._keys;
+ var maxIndex = keys.length - 1;
+ for (var ii = 0; ii <= maxIndex; ii++) {
+ var key = keys[reverse ? maxIndex - ii : ii];
+ if (fn(object[key], key, this) === false) {
+ return ii + 1;
+ }
+ }
+ return ii;
+ };
+
+ ObjectSeq.prototype.__iterator = function(type, reverse) {
+ var object = this._object;
+ var keys = this._keys;
+ var maxIndex = keys.length - 1;
+ var ii = 0;
+ return new Iterator(function() {
+ var key = keys[reverse ? maxIndex - ii : ii];
+ return ii++ > maxIndex ?
+ iteratorDone() :
+ iteratorValue(type, key, object[key]);
+ });
+ };
+
+ ObjectSeq.prototype[IS_ORDERED_SENTINEL] = true;
+
+
+ createClass(IterableSeq, IndexedSeq);
+ function IterableSeq(iterable) {
+ this._iterable = iterable;
+ this.size = iterable.length || iterable.size;
+ }
+
+ IterableSeq.prototype.__iterateUncached = function(fn, reverse) {
+ if (reverse) {
+ return this.cacheResult().__iterate(fn, reverse);
+ }
+ var iterable = this._iterable;
+ var iterator = getIterator(iterable);
+ var iterations = 0;
+ if (isIterator(iterator)) {
+ var step;
+ while (!(step = iterator.next()).done) {
+ if (fn(step.value, iterations++, this) === false) {
+ break;
+ }
+ }
+ }
+ return iterations;
+ };
+
+ IterableSeq.prototype.__iteratorUncached = function(type, reverse) {
+ if (reverse) {
+ return this.cacheResult().__iterator(type, reverse);
+ }
+ var iterable = this._iterable;
+ var iterator = getIterator(iterable);
+ if (!isIterator(iterator)) {
+ return new Iterator(iteratorDone);
+ }
+ var iterations = 0;
+ return new Iterator(function() {
+ var step = iterator.next();
+ return step.done ? step : iteratorValue(type, iterations++, step.value);
+ });
+ };
+
+
+
+ createClass(IteratorSeq, IndexedSeq);
+ function IteratorSeq(iterator) {
+ this._iterator = iterator;
+ this._iteratorCache = [];
+ }
+
+ IteratorSeq.prototype.__iterateUncached = function(fn, reverse) {
+ if (reverse) {
+ return this.cacheResult().__iterate(fn, reverse);
+ }
+ var iterator = this._iterator;
+ var cache = this._iteratorCache;
+ var iterations = 0;
+ while (iterations < cache.length) {
+ if (fn(cache[iterations], iterations++, this) === false) {
+ return iterations;
+ }
+ }
+ var step;
+ while (!(step = iterator.next()).done) {
+ var val = step.value;
+ cache[iterations] = val;
+ if (fn(val, iterations++, this) === false) {
+ break;
+ }
+ }
+ return iterations;
+ };
+
+ IteratorSeq.prototype.__iteratorUncached = function(type, reverse) {
+ if (reverse) {
+ return this.cacheResult().__iterator(type, reverse);
+ }
+ var iterator = this._iterator;
+ var cache = this._iteratorCache;
+ var iterations = 0;
+ return new Iterator(function() {
+ if (iterations >= cache.length) {
+ var step = iterator.next();
+ if (step.done) {
+ return step;
+ }
+ cache[iterations] = step.value;
+ }
+ return iteratorValue(type, iterations, cache[iterations++]);
+ });
+ };
+
+
+
+
+ // # pragma Helper functions
+
+ function isSeq(maybeSeq) {
+ return !!(maybeSeq && maybeSeq[IS_SEQ_SENTINEL]);
+ }
+
+ var EMPTY_SEQ;
+
+ function emptySequence() {
+ return EMPTY_SEQ || (EMPTY_SEQ = new ArraySeq([]));
+ }
+
+ function keyedSeqFromValue(value) {
+ var seq =
+ Array.isArray(value) ? new ArraySeq(value).fromEntrySeq() :
+ isIterator(value) ? new IteratorSeq(value).fromEntrySeq() :
+ hasIterator(value) ? new IterableSeq(value).fromEntrySeq() :
+ typeof value === 'object' ? new ObjectSeq(value) :
+ undefined;
+ if (!seq) {
+ throw new TypeError(
+ 'Expected Array or iterable object of [k, v] entries, '+
+ 'or keyed object: ' + value
+ );
+ }
+ return seq;
+ }
+
+ function indexedSeqFromValue(value) {
+ var seq = maybeIndexedSeqFromValue(value);
+ if (!seq) {
+ throw new TypeError(
+ 'Expected Array or iterable object of values: ' + value
+ );
+ }
+ return seq;
+ }
+
+ function seqFromValue(value) {
+ var seq = maybeIndexedSeqFromValue(value) ||
+ (typeof value === 'object' && new ObjectSeq(value));
+ if (!seq) {
+ throw new TypeError(
+ 'Expected Array or iterable object of values, or keyed object: ' + value
+ );
+ }
+ return seq;
+ }
+
+ function maybeIndexedSeqFromValue(value) {
+ return (
+ isArrayLike(value) ? new ArraySeq(value) :
+ isIterator(value) ? new IteratorSeq(value) :
+ hasIterator(value) ? new IterableSeq(value) :
+ undefined
+ );
+ }
+
+ function seqIterate(seq, fn, reverse, useKeys) {
+ var cache = seq._cache;
+ if (cache) {
+ var maxIndex = cache.length - 1;
+ for (var ii = 0; ii <= maxIndex; ii++) {
+ var entry = cache[reverse ? maxIndex - ii : ii];
+ if (fn(entry[1], useKeys ? entry[0] : ii, seq) === false) {
+ return ii + 1;
+ }
+ }
+ return ii;
+ }
+ return seq.__iterateUncached(fn, reverse);
+ }
+
+ function seqIterator(seq, type, reverse, useKeys) {
+ var cache = seq._cache;
+ if (cache) {
+ var maxIndex = cache.length - 1;
+ var ii = 0;
+ return new Iterator(function() {
+ var entry = cache[reverse ? maxIndex - ii : ii];
+ return ii++ > maxIndex ?
+ iteratorDone() :
+ iteratorValue(type, useKeys ? entry[0] : ii - 1, entry[1]);
+ });
+ }
+ return seq.__iteratorUncached(type, reverse);
+ }
+
+ function fromJS(json, converter) {
+ return converter ?
+ fromJSWith(converter, json, '', {'': json}) :
+ fromJSDefault(json);
+ }
+
+ function fromJSWith(converter, json, key, parentJSON) {
+ if (Array.isArray(json)) {
+ return converter.call(parentJSON, key, IndexedSeq(json).map(function(v, k) {return fromJSWith(converter, v, k, json)}));
+ }
+ if (isPlainObj(json)) {
+ return converter.call(parentJSON, key, KeyedSeq(json).map(function(v, k) {return fromJSWith(converter, v, k, json)}));
+ }
+ return json;
+ }
+
+ function fromJSDefault(json) {
+ if (Array.isArray(json)) {
+ return IndexedSeq(json).map(fromJSDefault).toList();
+ }
+ if (isPlainObj(json)) {
+ return KeyedSeq(json).map(fromJSDefault).toMap();
+ }
+ return json;
+ }
+
+ function isPlainObj(value) {
+ return value && (value.constructor === Object || value.constructor === undefined);
+ }
+
+ /**
+ * An extension of the "same-value" algorithm as [described for use by ES6 Map
+ * and Set](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map#Key_equality)
+ *
+ * NaN is considered the same as NaN, however -0 and 0 are considered the same
+ * value, which is different from the algorithm described by
+ * [`Object.is`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is).
+ *
+ * This is extended further to allow Objects to describe the values they
+ * represent, by way of `valueOf` or `equals` (and `hashCode`).
+ *
+ * Note: because of this extension, the key equality of Immutable.Map and the
+ * value equality of Immutable.Set will differ from ES6 Map and Set.
+ *
+ * ### Defining custom values
+ *
+ * The easiest way to describe the value an object represents is by implementing
+ * `valueOf`. For example, `Date` represents a value by returning a unix
+ * timestamp for `valueOf`:
+ *
+ * var date1 = new Date(1234567890000); // Fri Feb 13 2009 ...
+ * var date2 = new Date(1234567890000);
+ * date1.valueOf(); // 1234567890000
+ * assert( date1 !== date2 );
+ * assert( Immutable.is( date1, date2 ) );
+ *
+ * Note: overriding `valueOf` may have other implications if you use this object
+ * where JavaScript expects a primitive, such as implicit string coercion.
+ *
+ * For more complex types, especially collections, implementing `valueOf` may
+ * not be performant. An alternative is to implement `equals` and `hashCode`.
+ *
+ * `equals` takes another object, presumably of similar type, and returns true
+ * if the it is equal. Equality is symmetrical, so the same result should be
+ * returned if this and the argument are flipped.
+ *
+ * assert( a.equals(b) === b.equals(a) );
+ *
+ * `hashCode` returns a 32bit integer number representing the object which will
+ * be used to determine how to store the value object in a Map or Set. You must
+ * provide both or neither methods, one must not exist without the other.
+ *
+ * Also, an important relationship between these methods must be upheld: if two
+ * values are equal, they *must* return the same hashCode. If the values are not
+ * equal, they might have the same hashCode; this is called a hash collision,
+ * and while undesirable for performance reasons, it is acceptable.
+ *
+ * if (a.equals(b)) {
+ * assert( a.hashCode() === b.hashCode() );
+ * }
+ *
+ * All Immutable collections implement `equals` and `hashCode`.
+ *
+ */
+ function is(valueA, valueB) {
+ if (valueA === valueB || (valueA !== valueA && valueB !== valueB)) {
+ return true;
+ }
+ if (!valueA || !valueB) {
+ return false;
+ }
+ if (typeof valueA.valueOf === 'function' &&
+ typeof valueB.valueOf === 'function') {
+ valueA = valueA.valueOf();
+ valueB = valueB.valueOf();
+ if (valueA === valueB || (valueA !== valueA && valueB !== valueB)) {
+ return true;
+ }
+ if (!valueA || !valueB) {
+ return false;
+ }
+ }
+ if (typeof valueA.equals === 'function' &&
+ typeof valueB.equals === 'function' &&
+ valueA.equals(valueB)) {
+ return true;
+ }
+ return false;
+ }
+
+ function deepEqual(a, b) {
+ if (a === b) {
+ return true;
+ }
+
+ if (
+ !isIterable(b) ||
+ a.size !== undefined && b.size !== undefined && a.size !== b.size ||
+ a.__hash !== undefined && b.__hash !== undefined && a.__hash !== b.__hash ||
+ isKeyed(a) !== isKeyed(b) ||
+ isIndexed(a) !== isIndexed(b) ||
+ isOrdered(a) !== isOrdered(b)
+ ) {
+ return false;
+ }
+
+ if (a.size === 0 && b.size === 0) {
+ return true;
+ }
+
+ var notAssociative = !isAssociative(a);
+
+ if (isOrdered(a)) {
+ var entries = a.entries();
+ return b.every(function(v, k) {
+ var entry = entries.next().value;
+ return entry && is(entry[1], v) && (notAssociative || is(entry[0], k));
+ }) && entries.next().done;
+ }
+
+ var flipped = false;
+
+ if (a.size === undefined) {
+ if (b.size === undefined) {
+ if (typeof a.cacheResult === 'function') {
+ a.cacheResult();
+ }
+ } else {
+ flipped = true;
+ var _ = a;
+ a = b;
+ b = _;
+ }
+ }
+
+ var allEqual = true;
+ var bSize = b.__iterate(function(v, k) {
+ if (notAssociative ? !a.has(v) :
+ flipped ? !is(v, a.get(k, NOT_SET)) : !is(a.get(k, NOT_SET), v)) {
+ allEqual = false;
+ return false;
+ }
+ });
+
+ return allEqual && a.size === bSize;
+ }
+
+ createClass(Repeat, IndexedSeq);
+
+ function Repeat(value, times) {
+ if (!(this instanceof Repeat)) {
+ return new Repeat(value, times);
+ }
+ this._value = value;
+ this.size = times === undefined ? Infinity : Math.max(0, times);
+ if (this.size === 0) {
+ if (EMPTY_REPEAT) {
+ return EMPTY_REPEAT;
+ }
+ EMPTY_REPEAT = this;
+ }
+ }
+
+ Repeat.prototype.toString = function() {
+ if (this.size === 0) {
+ return 'Repeat []';
+ }
+ return 'Repeat [ ' + this._value + ' ' + this.size + ' times ]';
+ };
+
+ Repeat.prototype.get = function(index, notSetValue) {
+ return this.has(index) ? this._value : notSetValue;
+ };
+
+ Repeat.prototype.includes = function(searchValue) {
+ return is(this._value, searchValue);
+ };
+
+ Repeat.prototype.slice = function(begin, end) {
+ var size = this.size;
+ return wholeSlice(begin, end, size) ? this :
+ new Repeat(this._value, resolveEnd(end, size) - resolveBegin(begin, size));
+ };
+
+ Repeat.prototype.reverse = function() {
+ return this;
+ };
+
+ Repeat.prototype.indexOf = function(searchValue) {
+ if (is(this._value, searchValue)) {
+ return 0;
+ }
+ return -1;
+ };
+
+ Repeat.prototype.lastIndexOf = function(searchValue) {
+ if (is(this._value, searchValue)) {
+ return this.size;
+ }
+ return -1;
+ };
+
+ Repeat.prototype.__iterate = function(fn, reverse) {
+ for (var ii = 0; ii < this.size; ii++) {
+ if (fn(this._value, ii, this) === false) {
+ return ii + 1;
+ }
+ }
+ return ii;
+ };
+
+ Repeat.prototype.__iterator = function(type, reverse) {var this$0 = this;
+ var ii = 0;
+ return new Iterator(function()
+ {return ii < this$0.size ? iteratorValue(type, ii++, this$0._value) : iteratorDone()}
+ );
+ };
+
+ Repeat.prototype.equals = function(other) {
+ return other instanceof Repeat ?
+ is(this._value, other._value) :
+ deepEqual(other);
+ };
+
+
+ var EMPTY_REPEAT;
+
+ function invariant(condition, error) {
+ if (!condition) throw new Error(error);
+ }
+
+ createClass(Range, IndexedSeq);
+
+ function Range(start, end, step) {
+ if (!(this instanceof Range)) {
+ return new Range(start, end, step);
+ }
+ invariant(step !== 0, 'Cannot step a Range by 0');
+ start = start || 0;
+ if (end === undefined) {
+ end = Infinity;
+ }
+ step = step === undefined ? 1 : Math.abs(step);
+ if (end < start) {
+ step = -step;
+ }
+ this._start = start;
+ this._end = end;
+ this._step = step;
+ this.size = Math.max(0, Math.ceil((end - start) / step - 1) + 1);
+ if (this.size === 0) {
+ if (EMPTY_RANGE) {
+ return EMPTY_RANGE;
+ }
+ EMPTY_RANGE = this;
+ }
+ }
+
+ Range.prototype.toString = function() {
+ if (this.size === 0) {
+ return 'Range []';
+ }
+ return 'Range [ ' +
+ this._start + '...' + this._end +
+ (this._step !== 1 ? ' by ' + this._step : '') +
+ ' ]';
+ };
+
+ Range.prototype.get = function(index, notSetValue) {
+ return this.has(index) ?
+ this._start + wrapIndex(this, index) * this._step :
+ notSetValue;
+ };
+
+ Range.prototype.includes = function(searchValue) {
+ var possibleIndex = (searchValue - this._start) / this._step;
+ return possibleIndex >= 0 &&
+ possibleIndex < this.size &&
+ possibleIndex === Math.floor(possibleIndex);
+ };
+
+ Range.prototype.slice = function(begin, end) {
+ if (wholeSlice(begin, end, this.size)) {
+ return this;
+ }
+ begin = resolveBegin(begin, this.size);
+ end = resolveEnd(end, this.size);
+ if (end <= begin) {
+ return new Range(0, 0);
+ }
+ return new Range(this.get(begin, this._end), this.get(end, this._end), this._step);
+ };
+
+ Range.prototype.indexOf = function(searchValue) {
+ var offsetValue = searchValue - this._start;
+ if (offsetValue % this._step === 0) {
+ var index = offsetValue / this._step;
+ if (index >= 0 && index < this.size) {
+ return index
+ }
+ }
+ return -1;
+ };
+
+ Range.prototype.lastIndexOf = function(searchValue) {
+ return this.indexOf(searchValue);
+ };
+
+ Range.prototype.__iterate = function(fn, reverse) {
+ var maxIndex = this.size - 1;
+ var step = this._step;
+ var value = reverse ? this._start + maxIndex * step : this._start;
+ for (var ii = 0; ii <= maxIndex; ii++) {
+ if (fn(value, ii, this) === false) {
+ return ii + 1;
+ }
+ value += reverse ? -step : step;
+ }
+ return ii;
+ };
+
+ Range.prototype.__iterator = function(type, reverse) {
+ var maxIndex = this.size - 1;
+ var step = this._step;
+ var value = reverse ? this._start + maxIndex * step : this._start;
+ var ii = 0;
+ return new Iterator(function() {
+ var v = value;
+ value += reverse ? -step : step;
+ return ii > maxIndex ? iteratorDone() : iteratorValue(type, ii++, v);
+ });
+ };
+
+ Range.prototype.equals = function(other) {
+ return other instanceof Range ?
+ this._start === other._start &&
+ this._end === other._end &&
+ this._step === other._step :
+ deepEqual(this, other);
+ };
+
+
+ var EMPTY_RANGE;
+
+ createClass(Collection, Iterable);
+ function Collection() {
+ throw TypeError('Abstract');
+ }
+
+
+ createClass(KeyedCollection, Collection);function KeyedCollection() {}
+
+ createClass(IndexedCollection, Collection);function IndexedCollection() {}
+
+ createClass(SetCollection, Collection);function SetCollection() {}
+
+
+ Collection.Keyed = KeyedCollection;
+ Collection.Indexed = IndexedCollection;
+ Collection.Set = SetCollection;
+
+ var imul =
+ typeof Math.imul === 'function' && Math.imul(0xffffffff, 2) === -2 ?
+ Math.imul :
+ function imul(a, b) {
+ a = a | 0; // int
+ b = b | 0; // int
+ var c = a & 0xffff;
+ var d = b & 0xffff;
+ // Shift by 0 fixes the sign on the high part.
+ return (c * d) + ((((a >>> 16) * d + c * (b >>> 16)) << 16) >>> 0) | 0; // int
+ };
+
+ // v8 has an optimization for storing 31-bit signed numbers.
+ // Values which have either 00 or 11 as the high order bits qualify.
+ // This function drops the highest order bit in a signed number, maintaining
+ // the sign bit.
+ function smi(i32) {
+ return ((i32 >>> 1) & 0x40000000) | (i32 & 0xBFFFFFFF);
+ }
+
+ function hash(o) {
+ if (o === false || o === null || o === undefined) {
+ return 0;
+ }
+ if (typeof o.valueOf === 'function') {
+ o = o.valueOf();
+ if (o === false || o === null || o === undefined) {
+ return 0;
+ }
+ }
+ if (o === true) {
+ return 1;
+ }
+ var type = typeof o;
+ if (type === 'number') {
+ if (o !== o || o === Infinity) {
+ return 0;
+ }
+ var h = o | 0;
+ if (h !== o) {
+ h ^= o * 0xFFFFFFFF;
+ }
+ while (o > 0xFFFFFFFF) {
+ o /= 0xFFFFFFFF;
+ h ^= o;
+ }
+ return smi(h);
+ }
+ if (type === 'string') {
+ return o.length > STRING_HASH_CACHE_MIN_STRLEN ? cachedHashString(o) : hashString(o);
+ }
+ if (typeof o.hashCode === 'function') {
+ return o.hashCode();
+ }
+ if (type === 'object') {
+ return hashJSObj(o);
+ }
+ if (typeof o.toString === 'function') {
+ return hashString(o.toString());
+ }
+ throw new Error('Value type ' + type + ' cannot be hashed.');
+ }
+
+ function cachedHashString(string) {
+ var hash = stringHashCache[string];
+ if (hash === undefined) {
+ hash = hashString(string);
+ if (STRING_HASH_CACHE_SIZE === STRING_HASH_CACHE_MAX_SIZE) {
+ STRING_HASH_CACHE_SIZE = 0;
+ stringHashCache = {};
+ }
+ STRING_HASH_CACHE_SIZE++;
+ stringHashCache[string] = hash;
+ }
+ return hash;
+ }
+
+ // http://jsperf.com/hashing-strings
+ function hashString(string) {
+ // This is the hash from JVM
+ // The hash code for a string is computed as
+ // s[0] * 31 ^ (n - 1) + s[1] * 31 ^ (n - 2) + ... + s[n - 1],
+ // where s[i] is the ith character of the string and n is the length of
+ // the string. We "mod" the result to make it between 0 (inclusive) and 2^31
+ // (exclusive) by dropping high bits.
+ var hash = 0;
+ for (var ii = 0; ii < string.length; ii++) {
+ hash = 31 * hash + string.charCodeAt(ii) | 0;
+ }
+ return smi(hash);
+ }
+
+ function hashJSObj(obj) {
+ var hash;
+ if (usingWeakMap) {
+ hash = weakMap.get(obj);
+ if (hash !== undefined) {
+ return hash;
+ }
+ }
+
+ hash = obj[UID_HASH_KEY];
+ if (hash !== undefined) {
+ return hash;
+ }
+
+ if (!canDefineProperty) {
+ hash = obj.propertyIsEnumerable && obj.propertyIsEnumerable[UID_HASH_KEY];
+ if (hash !== undefined) {
+ return hash;
+ }
+
+ hash = getIENodeHash(obj);
+ if (hash !== undefined) {
+ return hash;
+ }
+ }
+
+ hash = ++objHashUID;
+ if (objHashUID & 0x40000000) {
+ objHashUID = 0;
+ }
+
+ if (usingWeakMap) {
+ weakMap.set(obj, hash);
+ } else if (isExtensible !== undefined && isExtensible(obj) === false) {
+ throw new Error('Non-extensible objects are not allowed as keys.');
+ } else if (canDefineProperty) {
+ Object.defineProperty(obj, UID_HASH_KEY, {
+ 'enumerable': false,
+ 'configurable': false,
+ 'writable': false,
+ 'value': hash
+ });
+ } else if (obj.propertyIsEnumerable !== undefined &&
+ obj.propertyIsEnumerable === obj.constructor.prototype.propertyIsEnumerable) {
+ // Since we can't define a non-enumerable property on the object
+ // we'll hijack one of the less-used non-enumerable properties to
+ // save our hash on it. Since this is a function it will not show up in
+ // `JSON.stringify` which is what we want.
+ obj.propertyIsEnumerable = function() {
+ return this.constructor.prototype.propertyIsEnumerable.apply(this, arguments);
+ };
+ obj.propertyIsEnumerable[UID_HASH_KEY] = hash;
+ } else if (obj.nodeType !== undefined) {
+ // At this point we couldn't get the IE `uniqueID` to use as a hash
+ // and we couldn't use a non-enumerable property to exploit the
+ // dontEnum bug so we simply add the `UID_HASH_KEY` on the node
+ // itself.
+ obj[UID_HASH_KEY] = hash;
+ } else {
+ throw new Error('Unable to set a non-enumerable property on object.');
+ }
+
+ return hash;
+ }
+
+ // Get references to ES5 object methods.
+ var isExtensible = Object.isExtensible;
+
+ // True if Object.defineProperty works as expected. IE8 fails this test.
+ var canDefineProperty = (function() {
+ try {
+ Object.defineProperty({}, '@', {});
+ return true;
+ } catch (e) {
+ return false;
+ }
+ }());
+
+ // IE has a `uniqueID` property on DOM nodes. We can construct the hash from it
+ // and avoid memory leaks from the IE cloneNode bug.
+ function getIENodeHash(node) {
+ if (node && node.nodeType > 0) {
+ switch (node.nodeType) {
+ case 1: // Element
+ return node.uniqueID;
+ case 9: // Document
+ return node.documentElement && node.documentElement.uniqueID;
+ }
+ }
+ }
+
+ // If possible, use a WeakMap.
+ var usingWeakMap = typeof WeakMap === 'function';
+ var weakMap;
+ if (usingWeakMap) {
+ weakMap = new WeakMap();
+ }
+
+ var objHashUID = 0;
+
+ var UID_HASH_KEY = '__immutablehash__';
+ if (typeof Symbol === 'function') {
+ UID_HASH_KEY = Symbol(UID_HASH_KEY);
+ }
+
+ var STRING_HASH_CACHE_MIN_STRLEN = 16;
+ var STRING_HASH_CACHE_MAX_SIZE = 255;
+ var STRING_HASH_CACHE_SIZE = 0;
+ var stringHashCache = {};
+
+ function assertNotInfinite(size) {
+ invariant(
+ size !== Infinity,
+ 'Cannot perform this action with an infinite size.'
+ );
+ }
+
+ createClass(Map, KeyedCollection);
+
+ // @pragma Construction
+
+ function Map(value) {
+ return value === null || value === undefined ? emptyMap() :
+ isMap(value) && !isOrdered(value) ? value :
+ emptyMap().withMutations(function(map ) {
+ var iter = KeyedIterable(value);
+ assertNotInfinite(iter.size);
+ iter.forEach(function(v, k) {return map.set(k, v)});
+ });
+ }
+
+ Map.of = function() {var keyValues = SLICE$0.call(arguments, 0);
+ return emptyMap().withMutations(function(map ) {
+ for (var i = 0; i < keyValues.length; i += 2) {
+ if (i + 1 >= keyValues.length) {
+ throw new Error('Missing value for key: ' + keyValues[i]);
+ }
+ map.set(keyValues[i], keyValues[i + 1]);
+ }
+ });
+ };
+
+ Map.prototype.toString = function() {
+ return this.__toString('Map {', '}');
+ };
+
+ // @pragma Access
+
+ Map.prototype.get = function(k, notSetValue) {
+ return this._root ?
+ this._root.get(0, undefined, k, notSetValue) :
+ notSetValue;
+ };
+
+ // @pragma Modification
+
+ Map.prototype.set = function(k, v) {
+ return updateMap(this, k, v);
+ };
+
+ Map.prototype.setIn = function(keyPath, v) {
+ return this.updateIn(keyPath, NOT_SET, function() {return v});
+ };
+
+ Map.prototype.remove = function(k) {
+ return updateMap(this, k, NOT_SET);
+ };
+
+ Map.prototype.deleteIn = function(keyPath) {
+ return this.updateIn(keyPath, function() {return NOT_SET});
+ };
+
+ Map.prototype.update = function(k, notSetValue, updater) {
+ return arguments.length === 1 ?
+ k(this) :
+ this.updateIn([k], notSetValue, updater);
+ };
+
+ Map.prototype.updateIn = function(keyPath, notSetValue, updater) {
+ if (!updater) {
+ updater = notSetValue;
+ notSetValue = undefined;
+ }
+ var updatedValue = updateInDeepMap(
+ this,
+ forceIterator(keyPath),
+ notSetValue,
+ updater
+ );
+ return updatedValue === NOT_SET ? undefined : updatedValue;
+ };
+
+ Map.prototype.clear = function() {
+ if (this.size === 0) {
+ return this;
+ }
+ if (this.__ownerID) {
+ this.size = 0;
+ this._root = null;
+ this.__hash = undefined;
+ this.__altered = true;
+ return this;
+ }
+ return emptyMap();
+ };
+
+ // @pragma Composition
+
+ Map.prototype.merge = function(/*...iters*/) {
+ return mergeIntoMapWith(this, undefined, arguments);
+ };
+
+ Map.prototype.mergeWith = function(merger) {var iters = SLICE$0.call(arguments, 1);
+ return mergeIntoMapWith(this, merger, iters);
+ };
+
+ Map.prototype.mergeIn = function(keyPath) {var iters = SLICE$0.call(arguments, 1);
+ return this.updateIn(
+ keyPath,
+ emptyMap(),
+ function(m ) {return typeof m.merge === 'function' ?
+ m.merge.apply(m, iters) :
+ iters[iters.length - 1]}
+ );
+ };
+
+ Map.prototype.mergeDeep = function(/*...iters*/) {
+ return mergeIntoMapWith(this, deepMerger, arguments);
+ };
+
+ Map.prototype.mergeDeepWith = function(merger) {var iters = SLICE$0.call(arguments, 1);
+ return mergeIntoMapWith(this, deepMergerWith(merger), iters);
+ };
+
+ Map.prototype.mergeDeepIn = function(keyPath) {var iters = SLICE$0.call(arguments, 1);
+ return this.updateIn(
+ keyPath,
+ emptyMap(),
+ function(m ) {return typeof m.mergeDeep === 'function' ?
+ m.mergeDeep.apply(m, iters) :
+ iters[iters.length - 1]}
+ );
+ };
+
+ Map.prototype.sort = function(comparator) {
+ // Late binding
+ return OrderedMap(sortFactory(this, comparator));
+ };
+
+ Map.prototype.sortBy = function(mapper, comparator) {
+ // Late binding
+ return OrderedMap(sortFactory(this, comparator, mapper));
+ };
+
+ // @pragma Mutability
+
+ Map.prototype.withMutations = function(fn) {
+ var mutable = this.asMutable();
+ fn(mutable);
+ return mutable.wasAltered() ? mutable.__ensureOwner(this.__ownerID) : this;
+ };
+
+ Map.prototype.asMutable = function() {
+ return this.__ownerID ? this : this.__ensureOwner(new OwnerID());
+ };
+
+ Map.prototype.asImmutable = function() {
+ return this.__ensureOwner();
+ };
+
+ Map.prototype.wasAltered = function() {
+ return this.__altered;
+ };
+
+ Map.prototype.__iterator = function(type, reverse) {
+ return new MapIterator(this, type, reverse);
+ };
+
+ Map.prototype.__iterate = function(fn, reverse) {var this$0 = this;
+ var iterations = 0;
+ this._root && this._root.iterate(function(entry ) {
+ iterations++;
+ return fn(entry[1], entry[0], this$0);
+ }, reverse);
+ return iterations;
+ };
+
+ Map.prototype.__ensureOwner = function(ownerID) {
+ if (ownerID === this.__ownerID) {
+ return this;
+ }
+ if (!ownerID) {
+ this.__ownerID = ownerID;
+ this.__altered = false;
+ return this;
+ }
+ return makeMap(this.size, this._root, ownerID, this.__hash);
+ };
+
+
+ function isMap(maybeMap) {
+ return !!(maybeMap && maybeMap[IS_MAP_SENTINEL]);
+ }
+
+ Map.isMap = isMap;
+
+ var IS_MAP_SENTINEL = '@@__IMMUTABLE_MAP__@@';
+
+ var MapPrototype = Map.prototype;
+ MapPrototype[IS_MAP_SENTINEL] = true;
+ MapPrototype[DELETE] = MapPrototype.remove;
+ MapPrototype.removeIn = MapPrototype.deleteIn;
+
+
+ // #pragma Trie Nodes
+
+
+
+ function ArrayMapNode(ownerID, entries) {
+ this.ownerID = ownerID;
+ this.entries = entries;
+ }
+
+ ArrayMapNode.prototype.get = function(shift, keyHash, key, notSetValue) {
+ var entries = this.entries;
+ for (var ii = 0, len = entries.length; ii < len; ii++) {
+ if (is(key, entries[ii][0])) {
+ return entries[ii][1];
+ }
+ }
+ return notSetValue;
+ };
+
+ ArrayMapNode.prototype.update = function(ownerID, shift, keyHash, key, value, didChangeSize, didAlter) {
+ var removed = value === NOT_SET;
+
+ var entries = this.entries;
+ var idx = 0;
+ for (var len = entries.length; idx < len; idx++) {
+ if (is(key, entries[idx][0])) {
+ break;
+ }
+ }
+ var exists = idx < len;
+
+ if (exists ? entries[idx][1] === value : removed) {
+ return this;
+ }
+
+ SetRef(didAlter);
+ (removed || !exists) && SetRef(didChangeSize);
+
+ if (removed && entries.length === 1) {
+ return; // undefined
+ }
+
+ if (!exists && !removed && entries.length >= MAX_ARRAY_MAP_SIZE) {
+ return createNodes(ownerID, entries, key, value);
+ }
+
+ var isEditable = ownerID && ownerID === this.ownerID;
+ var newEntries = isEditable ? entries : arrCopy(entries);
+
+ if (exists) {
+ if (removed) {
+ idx === len - 1 ? newEntries.pop() : (newEntries[idx] = newEntries.pop());
+ } else {
+ newEntries[idx] = [key, value];
+ }
+ } else {
+ newEntries.push([key, value]);
+ }
+
+ if (isEditable) {
+ this.entries = newEntries;
+ return this;
+ }
+
+ return new ArrayMapNode(ownerID, newEntries);
+ };
+
+
+
+
+ function BitmapIndexedNode(ownerID, bitmap, nodes) {
+ this.ownerID = ownerID;
+ this.bitmap = bitmap;
+ this.nodes = nodes;
+ }
+
+ BitmapIndexedNode.prototype.get = function(shift, keyHash, key, notSetValue) {
+ if (keyHash === undefined) {
+ keyHash = hash(key);
+ }
+ var bit = (1 << ((shift === 0 ? keyHash : keyHash >>> shift) & MASK));
+ var bitmap = this.bitmap;
+ return (bitmap & bit) === 0 ? notSetValue :
+ this.nodes[popCount(bitmap & (bit - 1))].get(shift + SHIFT, keyHash, key, notSetValue);
+ };
+
+ BitmapIndexedNode.prototype.update = function(ownerID, shift, keyHash, key, value, didChangeSize, didAlter) {
+ if (keyHash === undefined) {
+ keyHash = hash(key);
+ }
+ var keyHashFrag = (shift === 0 ? keyHash : keyHash >>> shift) & MASK;
+ var bit = 1 << keyHashFrag;
+ var bitmap = this.bitmap;
+ var exists = (bitmap & bit) !== 0;
+
+ if (!exists && value === NOT_SET) {
+ return this;
+ }
+
+ var idx = popCount(bitmap & (bit - 1));
+ var nodes = this.nodes;
+ var node = exists ? nodes[idx] : undefined;
+ var newNode = updateNode(node, ownerID, shift + SHIFT, keyHash, key, value, didChangeSize, didAlter);
+
+ if (newNode === node) {
+ return this;
+ }
+
+ if (!exists && newNode && nodes.length >= MAX_BITMAP_INDEXED_SIZE) {
+ return expandNodes(ownerID, nodes, bitmap, keyHashFrag, newNode);
+ }
+
+ if (exists && !newNode && nodes.length === 2 && isLeafNode(nodes[idx ^ 1])) {
+ return nodes[idx ^ 1];
+ }
+
+ if (exists && newNode && nodes.length === 1 && isLeafNode(newNode)) {
+ return newNode;
+ }
+
+ var isEditable = ownerID && ownerID === this.ownerID;
+ var newBitmap = exists ? newNode ? bitmap : bitmap ^ bit : bitmap | bit;
+ var newNodes = exists ? newNode ?
+ setIn(nodes, idx, newNode, isEditable) :
+ spliceOut(nodes, idx, isEditable) :
+ spliceIn(nodes, idx, newNode, isEditable);
+
+ if (isEditable) {
+ this.bitmap = newBitmap;
+ this.nodes = newNodes;
+ return this;
+ }
+
+ return new BitmapIndexedNode(ownerID, newBitmap, newNodes);
+ };
+
+
+
+
+ function HashArrayMapNode(ownerID, count, nodes) {
+ this.ownerID = ownerID;
+ this.count = count;
+ this.nodes = nodes;
+ }
+
+ HashArrayMapNode.prototype.get = function(shift, keyHash, key, notSetValue) {
+ if (keyHash === undefined) {
+ keyHash = hash(key);
+ }
+ var idx = (shift === 0 ? keyHash : keyHash >>> shift) & MASK;
+ var node = this.nodes[idx];
+ return node ? node.get(shift + SHIFT, keyHash, key, notSetValue) : notSetValue;
+ };
+
+ HashArrayMapNode.prototype.update = function(ownerID, shift, keyHash, key, value, didChangeSize, didAlter) {
+ if (keyHash === undefined) {
+ keyHash = hash(key);
+ }
+ var idx = (shift === 0 ? keyHash : keyHash >>> shift) & MASK;
+ var removed = value === NOT_SET;
+ var nodes = this.nodes;
+ var node = nodes[idx];
+
+ if (removed && !node) {
+ return this;
+ }
+
+ var newNode = updateNode(node, ownerID, shift + SHIFT, keyHash, key, value, didChangeSize, didAlter);
+ if (newNode === node) {
+ return this;
+ }
+
+ var newCount = this.count;
+ if (!node) {
+ newCount++;
+ } else if (!newNode) {
+ newCount--;
+ if (newCount < MIN_HASH_ARRAY_MAP_SIZE) {
+ return packNodes(ownerID, nodes, newCount, idx);
+ }
+ }
+
+ var isEditable = ownerID && ownerID === this.ownerID;
+ var newNodes = setIn(nodes, idx, newNode, isEditable);
+
+ if (isEditable) {
+ this.count = newCount;
+ this.nodes = newNodes;
+ return this;
+ }
+
+ return new HashArrayMapNode(ownerID, newCount, newNodes);
+ };
+
+
+
+
+ function HashCollisionNode(ownerID, keyHash, entries) {
+ this.ownerID = ownerID;
+ this.keyHash = keyHash;
+ this.entries = entries;
+ }
+
+ HashCollisionNode.prototype.get = function(shift, keyHash, key, notSetValue) {
+ var entries = this.entries;
+ for (var ii = 0, len = entries.length; ii < len; ii++) {
+ if (is(key, entries[ii][0])) {
+ return entries[ii][1];
+ }
+ }
+ return notSetValue;
+ };
+
+ HashCollisionNode.prototype.update = function(ownerID, shift, keyHash, key, value, didChangeSize, didAlter) {
+ if (keyHash === undefined) {
+ keyHash = hash(key);
+ }
+
+ var removed = value === NOT_SET;
+
+ if (keyHash !== this.keyHash) {
+ if (removed) {
+ return this;
+ }
+ SetRef(didAlter);
+ SetRef(didChangeSize);
+ return mergeIntoNode(this, ownerID, shift, keyHash, [key, value]);
+ }
+
+ var entries = this.entries;
+ var idx = 0;
+ for (var len = entries.length; idx < len; idx++) {
+ if (is(key, entries[idx][0])) {
+ break;
+ }
+ }
+ var exists = idx < len;
+
+ if (exists ? entries[idx][1] === value : removed) {
+ return this;
+ }
+
+ SetRef(didAlter);
+ (removed || !exists) && SetRef(didChangeSize);
+
+ if (removed && len === 2) {
+ return new ValueNode(ownerID, this.keyHash, entries[idx ^ 1]);
+ }
+
+ var isEditable = ownerID && ownerID === this.ownerID;
+ var newEntries = isEditable ? entries : arrCopy(entries);
+
+ if (exists) {
+ if (removed) {
+ idx === len - 1 ? newEntries.pop() : (newEntries[idx] = newEntries.pop());
+ } else {
+ newEntries[idx] = [key, value];
+ }
+ } else {
+ newEntries.push([key, value]);
+ }
+
+ if (isEditable) {
+ this.entries = newEntries;
+ return this;
+ }
+
+ return new HashCollisionNode(ownerID, this.keyHash, newEntries);
+ };
+
+
+
+
+ function ValueNode(ownerID, keyHash, entry) {
+ this.ownerID = ownerID;
+ this.keyHash = keyHash;
+ this.entry = entry;
+ }
+
+ ValueNode.prototype.get = function(shift, keyHash, key, notSetValue) {
+ return is(key, this.entry[0]) ? this.entry[1] : notSetValue;
+ };
+
+ ValueNode.prototype.update = function(ownerID, shift, keyHash, key, value, didChangeSize, didAlter) {
+ var removed = value === NOT_SET;
+ var keyMatch = is(key, this.entry[0]);
+ if (keyMatch ? value === this.entry[1] : removed) {
+ return this;
+ }
+
+ SetRef(didAlter);
+
+ if (removed) {
+ SetRef(didChangeSize);
+ return; // undefined
+ }
+
+ if (keyMatch) {
+ if (ownerID && ownerID === this.ownerID) {
+ this.entry[1] = value;
+ return this;
+ }
+ return new ValueNode(ownerID, this.keyHash, [key, value]);
+ }
+
+ SetRef(didChangeSize);
+ return mergeIntoNode(this, ownerID, shift, hash(key), [key, value]);
+ };
+
+
+
+ // #pragma Iterators
+
+ ArrayMapNode.prototype.iterate =
+ HashCollisionNode.prototype.iterate = function (fn, reverse) {
+ var entries = this.entries;
+ for (var ii = 0, maxIndex = entries.length - 1; ii <= maxIndex; ii++) {
+ if (fn(entries[reverse ? maxIndex - ii : ii]) === false) {
+ return false;
+ }
+ }
+ }
+
+ BitmapIndexedNode.prototype.iterate =
+ HashArrayMapNode.prototype.iterate = function (fn, reverse) {
+ var nodes = this.nodes;
+ for (var ii = 0, maxIndex = nodes.length - 1; ii <= maxIndex; ii++) {
+ var node = nodes[reverse ? maxIndex - ii : ii];
+ if (node && node.iterate(fn, reverse) === false) {
+ return false;
+ }
+ }
+ }
+
+ ValueNode.prototype.iterate = function (fn, reverse) {
+ return fn(this.entry);
+ }
+
+ createClass(MapIterator, Iterator);
+
+ function MapIterator(map, type, reverse) {
+ this._type = type;
+ this._reverse = reverse;
+ this._stack = map._root && mapIteratorFrame(map._root);
+ }
+
+ MapIterator.prototype.next = function() {
+ var type = this._type;
+ var stack = this._stack;
+ while (stack) {
+ var node = stack.node;
+ var index = stack.index++;
+ var maxIndex;
+ if (node.entry) {
+ if (index === 0) {
+ return mapIteratorValue(type, node.entry);
+ }
+ } else if (node.entries) {
+ maxIndex = node.entries.length - 1;
+ if (index <= maxIndex) {
+ return mapIteratorValue(type, node.entries[this._reverse ? maxIndex - index : index]);
+ }
+ } else {
+ maxIndex = node.nodes.length - 1;
+ if (index <= maxIndex) {
+ var subNode = node.nodes[this._reverse ? maxIndex - index : index];
+ if (subNode) {
+ if (subNode.entry) {
+ return mapIteratorValue(type, subNode.entry);
+ }
+ stack = this._stack = mapIteratorFrame(subNode, stack);
+ }
+ continue;
+ }
+ }
+ stack = this._stack = this._stack.__prev;
+ }
+ return iteratorDone();
+ };
+
+
+ function mapIteratorValue(type, entry) {
+ return iteratorValue(type, entry[0], entry[1]);
+ }
+
+ function mapIteratorFrame(node, prev) {
+ return {
+ node: node,
+ index: 0,
+ __prev: prev
+ };
+ }
+
+ function makeMap(size, root, ownerID, hash) {
+ var map = Object.create(MapPrototype);
+ map.size = size;
+ map._root = root;
+ map.__ownerID = ownerID;
+ map.__hash = hash;
+ map.__altered = false;
+ return map;
+ }
+
+ var EMPTY_MAP;
+ function emptyMap() {
+ return EMPTY_MAP || (EMPTY_MAP = makeMap(0));
+ }
+
+ function updateMap(map, k, v) {
+ var newRoot;
+ var newSize;
+ if (!map._root) {
+ if (v === NOT_SET) {
+ return map;
+ }
+ newSize = 1;
+ newRoot = new ArrayMapNode(map.__ownerID, [[k, v]]);
+ } else {
+ var didChangeSize = MakeRef(CHANGE_LENGTH);
+ var didAlter = MakeRef(DID_ALTER);
+ newRoot = updateNode(map._root, map.__ownerID, 0, undefined, k, v, didChangeSize, didAlter);
+ if (!didAlter.value) {
+ return map;
+ }
+ newSize = map.size + (didChangeSize.value ? v === NOT_SET ? -1 : 1 : 0);
+ }
+ if (map.__ownerID) {
+ map.size = newSize;
+ map._root = newRoot;
+ map.__hash = undefined;
+ map.__altered = true;
+ return map;
+ }
+ return newRoot ? makeMap(newSize, newRoot) : emptyMap();
+ }
+
+ function updateNode(node, ownerID, shift, keyHash, key, value, didChangeSize, didAlter) {
+ if (!node) {
+ if (value === NOT_SET) {
+ return node;
+ }
+ SetRef(didAlter);
+ SetRef(didChangeSize);
+ return new ValueNode(ownerID, keyHash, [key, value]);
+ }
+ return node.update(ownerID, shift, keyHash, key, value, didChangeSize, didAlter);
+ }
+
+ function isLeafNode(node) {
+ return node.constructor === ValueNode || node.constructor === HashCollisionNode;
+ }
+
+ function mergeIntoNode(node, ownerID, shift, keyHash, entry) {
+ if (node.keyHash === keyHash) {
+ return new HashCollisionNode(ownerID, keyHash, [node.entry, entry]);
+ }
+
+ var idx1 = (shift === 0 ? node.keyHash : node.keyHash >>> shift) & MASK;
+ var idx2 = (shift === 0 ? keyHash : keyHash >>> shift) & MASK;
+
+ var newNode;
+ var nodes = idx1 === idx2 ?
+ [mergeIntoNode(node, ownerID, shift + SHIFT, keyHash, entry)] :
+ ((newNode = new ValueNode(ownerID, keyHash, entry)), idx1 < idx2 ? [node, newNode] : [newNode, node]);
+
+ return new BitmapIndexedNode(ownerID, (1 << idx1) | (1 << idx2), nodes);
+ }
+
+ function createNodes(ownerID, entries, key, value) {
+ if (!ownerID) {
+ ownerID = new OwnerID();
+ }
+ var node = new ValueNode(ownerID, hash(key), [key, value]);
+ for (var ii = 0; ii < entries.length; ii++) {
+ var entry = entries[ii];
+ node = node.update(ownerID, 0, undefined, entry[0], entry[1]);
+ }
+ return node;
+ }
+
+ function packNodes(ownerID, nodes, count, excluding) {
+ var bitmap = 0;
+ var packedII = 0;
+ var packedNodes = new Array(count);
+ for (var ii = 0, bit = 1, len = nodes.length; ii < len; ii++, bit <<= 1) {
+ var node = nodes[ii];
+ if (node !== undefined && ii !== excluding) {
+ bitmap |= bit;
+ packedNodes[packedII++] = node;
+ }
+ }
+ return new BitmapIndexedNode(ownerID, bitmap, packedNodes);
+ }
+
+ function expandNodes(ownerID, nodes, bitmap, including, node) {
+ var count = 0;
+ var expandedNodes = new Array(SIZE);
+ for (var ii = 0; bitmap !== 0; ii++, bitmap >>>= 1) {
+ expandedNodes[ii] = bitmap & 1 ? nodes[count++] : undefined;
+ }
+ expandedNodes[including] = node;
+ return new HashArrayMapNode(ownerID, count + 1, expandedNodes);
+ }
+
+ function mergeIntoMapWith(map, merger, iterables) {
+ var iters = [];
+ for (var ii = 0; ii < iterables.length; ii++) {
+ var value = iterables[ii];
+ var iter = KeyedIterable(value);
+ if (!isIterable(value)) {
+ iter = iter.map(function(v ) {return fromJS(v)});
+ }
+ iters.push(iter);
+ }
+ return mergeIntoCollectionWith(map, merger, iters);
+ }
+
+ function deepMerger(existing, value, key) {
+ return existing && existing.mergeDeep && isIterable(value) ?
+ existing.mergeDeep(value) :
+ is(existing, value) ? existing : value;
+ }
+
+ function deepMergerWith(merger) {
+ return function(existing, value, key) {
+ if (existing && existing.mergeDeepWith && isIterable(value)) {
+ return existing.mergeDeepWith(merger, value);
+ }
+ var nextValue = merger(existing, value, key);
+ return is(existing, nextValue) ? existing : nextValue;
+ };
+ }
+
+ function mergeIntoCollectionWith(collection, merger, iters) {
+ iters = iters.filter(function(x ) {return x.size !== 0});
+ if (iters.length === 0) {
+ return collection;
+ }
+ if (collection.size === 0 && !collection.__ownerID && iters.length === 1) {
+ return collection.constructor(iters[0]);
+ }
+ return collection.withMutations(function(collection ) {
+ var mergeIntoMap = merger ?
+ function(value, key) {
+ collection.update(key, NOT_SET, function(existing )
+ {return existing === NOT_SET ? value : merger(existing, value, key)}
+ );
+ } :
+ function(value, key) {
+ collection.set(key, value);
+ }
+ for (var ii = 0; ii < iters.length; ii++) {
+ iters[ii].forEach(mergeIntoMap);
+ }
+ });
+ }
+
+ function updateInDeepMap(existing, keyPathIter, notSetValue, updater) {
+ var isNotSet = existing === NOT_SET;
+ var step = keyPathIter.next();
+ if (step.done) {
+ var existingValue = isNotSet ? notSetValue : existing;
+ var newValue = updater(existingValue);
+ return newValue === existingValue ? existing : newValue;
+ }
+ invariant(
+ isNotSet || (existing && existing.set),
+ 'invalid keyPath'
+ );
+ var key = step.value;
+ var nextExisting = isNotSet ? NOT_SET : existing.get(key, NOT_SET);
+ var nextUpdated = updateInDeepMap(
+ nextExisting,
+ keyPathIter,
+ notSetValue,
+ updater
+ );
+ return nextUpdated === nextExisting ? existing :
+ nextUpdated === NOT_SET ? existing.remove(key) :
+ (isNotSet ? emptyMap() : existing).set(key, nextUpdated);
+ }
+
+ function popCount(x) {
+ x = x - ((x >> 1) & 0x55555555);
+ x = (x & 0x33333333) + ((x >> 2) & 0x33333333);
+ x = (x + (x >> 4)) & 0x0f0f0f0f;
+ x = x + (x >> 8);
+ x = x + (x >> 16);
+ return x & 0x7f;
+ }
+
+ function setIn(array, idx, val, canEdit) {
+ var newArray = canEdit ? array : arrCopy(array);
+ newArray[idx] = val;
+ return newArray;
+ }
+
+ function spliceIn(array, idx, val, canEdit) {
+ var newLen = array.length + 1;
+ if (canEdit && idx + 1 === newLen) {
+ array[idx] = val;
+ return array;
+ }
+ var newArray = new Array(newLen);
+ var after = 0;
+ for (var ii = 0; ii < newLen; ii++) {
+ if (ii === idx) {
+ newArray[ii] = val;
+ after = -1;
+ } else {
+ newArray[ii] = array[ii + after];
+ }
+ }
+ return newArray;
+ }
+
+ function spliceOut(array, idx, canEdit) {
+ var newLen = array.length - 1;
+ if (canEdit && idx === newLen) {
+ array.pop();
+ return array;
+ }
+ var newArray = new Array(newLen);
+ var after = 0;
+ for (var ii = 0; ii < newLen; ii++) {
+ if (ii === idx) {
+ after = 1;
+ }
+ newArray[ii] = array[ii + after];
+ }
+ return newArray;
+ }
+
+ var MAX_ARRAY_MAP_SIZE = SIZE / 4;
+ var MAX_BITMAP_INDEXED_SIZE = SIZE / 2;
+ var MIN_HASH_ARRAY_MAP_SIZE = SIZE / 4;
+
+ createClass(List, IndexedCollection);
+
+ // @pragma Construction
+
+ function List(value) {
+ var empty = emptyList();
+ if (value === null || value === undefined) {
+ return empty;
+ }
+ if (isList(value)) {
+ return value;
+ }
+ var iter = IndexedIterable(value);
+ var size = iter.size;
+ if (size === 0) {
+ return empty;
+ }
+ assertNotInfinite(size);
+ if (size > 0 && size < SIZE) {
+ return makeList(0, size, SHIFT, null, new VNode(iter.toArray()));
+ }
+ return empty.withMutations(function(list ) {
+ list.setSize(size);
+ iter.forEach(function(v, i) {return list.set(i, v)});
+ });
+ }
+
+ List.of = function(/*...values*/) {
+ return this(arguments);
+ };
+
+ List.prototype.toString = function() {
+ return this.__toString('List [', ']');
+ };
+
+ // @pragma Access
+
+ List.prototype.get = function(index, notSetValue) {
+ index = wrapIndex(this, index);
+ if (index >= 0 && index < this.size) {
+ index += this._origin;
+ var node = listNodeFor(this, index);
+ return node && node.array[index & MASK];
+ }
+ return notSetValue;
+ };
+
+ // @pragma Modification
+
+ List.prototype.set = function(index, value) {
+ return updateList(this, index, value);
+ };
+
+ List.prototype.remove = function(index) {
+ return !this.has(index) ? this :
+ index === 0 ? this.shift() :
+ index === this.size - 1 ? this.pop() :
+ this.splice(index, 1);
+ };
+
+ List.prototype.insert = function(index, value) {
+ return this.splice(index, 0, value);
+ };
+
+ List.prototype.clear = function() {
+ if (this.size === 0) {
+ return this;
+ }
+ if (this.__ownerID) {
+ this.size = this._origin = this._capacity = 0;
+ this._level = SHIFT;
+ this._root = this._tail = null;
+ this.__hash = undefined;
+ this.__altered = true;
+ return this;
+ }
+ return emptyList();
+ };
+
+ List.prototype.push = function(/*...values*/) {
+ var values = arguments;
+ var oldSize = this.size;
+ return this.withMutations(function(list ) {
+ setListBounds(list, 0, oldSize + values.length);
+ for (var ii = 0; ii < values.length; ii++) {
+ list.set(oldSize + ii, values[ii]);
+ }
+ });
+ };
+
+ List.prototype.pop = function() {
+ return setListBounds(this, 0, -1);
+ };
+
+ List.prototype.unshift = function(/*...values*/) {
+ var values = arguments;
+ return this.withMutations(function(list ) {
+ setListBounds(list, -values.length);
+ for (var ii = 0; ii < values.length; ii++) {
+ list.set(ii, values[ii]);
+ }
+ });
+ };
+
+ List.prototype.shift = function() {
+ return setListBounds(this, 1);
+ };
+
+ // @pragma Composition
+
+ List.prototype.merge = function(/*...iters*/) {
+ return mergeIntoListWith(this, undefined, arguments);
+ };
+
+ List.prototype.mergeWith = function(merger) {var iters = SLICE$0.call(arguments, 1);
+ return mergeIntoListWith(this, merger, iters);
+ };
+
+ List.prototype.mergeDeep = function(/*...iters*/) {
+ return mergeIntoListWith(this, deepMerger, arguments);
+ };
+
+ List.prototype.mergeDeepWith = function(merger) {var iters = SLICE$0.call(arguments, 1);
+ return mergeIntoListWith(this, deepMergerWith(merger), iters);
+ };
+
+ List.prototype.setSize = function(size) {
+ return setListBounds(this, 0, size);
+ };
+
+ // @pragma Iteration
+
+ List.prototype.slice = function(begin, end) {
+ var size = this.size;
+ if (wholeSlice(begin, end, size)) {
+ return this;
+ }
+ return setListBounds(
+ this,
+ resolveBegin(begin, size),
+ resolveEnd(end, size)
+ );
+ };
+
+ List.prototype.__iterator = function(type, reverse) {
+ var index = 0;
+ var values = iterateList(this, reverse);
+ return new Iterator(function() {
+ var value = values();
+ return value === DONE ?
+ iteratorDone() :
+ iteratorValue(type, index++, value);
+ });
+ };
+
+ List.prototype.__iterate = function(fn, reverse) {
+ var index = 0;
+ var values = iterateList(this, reverse);
+ var value;
+ while ((value = values()) !== DONE) {
+ if (fn(value, index++, this) === false) {
+ break;
+ }
+ }
+ return index;
+ };
+
+ List.prototype.__ensureOwner = function(ownerID) {
+ if (ownerID === this.__ownerID) {
+ return this;
+ }
+ if (!ownerID) {
+ this.__ownerID = ownerID;
+ return this;
+ }
+ return makeList(this._origin, this._capacity, this._level, this._root, this._tail, ownerID, this.__hash);
+ };
+
+
+ function isList(maybeList) {
+ return !!(maybeList && maybeList[IS_LIST_SENTINEL]);
+ }
+
+ List.isList = isList;
+
+ var IS_LIST_SENTINEL = '@@__IMMUTABLE_LIST__@@';
+
+ var ListPrototype = List.prototype;
+ ListPrototype[IS_LIST_SENTINEL] = true;
+ ListPrototype[DELETE] = ListPrototype.remove;
+ ListPrototype.setIn = MapPrototype.setIn;
+ ListPrototype.deleteIn =
+ ListPrototype.removeIn = MapPrototype.removeIn;
+ ListPrototype.update = MapPrototype.update;
+ ListPrototype.updateIn = MapPrototype.updateIn;
+ ListPrototype.mergeIn = MapPrototype.mergeIn;
+ ListPrototype.mergeDeepIn = MapPrototype.mergeDeepIn;
+ ListPrototype.withMutations = MapPrototype.withMutations;
+ ListPrototype.asMutable = MapPrototype.asMutable;
+ ListPrototype.asImmutable = MapPrototype.asImmutable;
+ ListPrototype.wasAltered = MapPrototype.wasAltered;
+
+
+
+ function VNode(array, ownerID) {
+ this.array = array;
+ this.ownerID = ownerID;
+ }
+
+ // TODO: seems like these methods are very similar
+
+ VNode.prototype.removeBefore = function(ownerID, level, index) {
+ if (index === level ? 1 << level : 0 || this.array.length === 0) {
+ return this;
+ }
+ var originIndex = (index >>> level) & MASK;
+ if (originIndex >= this.array.length) {
+ return new VNode([], ownerID);
+ }
+ var removingFirst = originIndex === 0;
+ var newChild;
+ if (level > 0) {
+ var oldChild = this.array[originIndex];
+ newChild = oldChild && oldChild.removeBefore(ownerID, level - SHIFT, index);
+ if (newChild === oldChild && removingFirst) {
+ return this;
+ }
+ }
+ if (removingFirst && !newChild) {
+ return this;
+ }
+ var editable = editableVNode(this, ownerID);
+ if (!removingFirst) {
+ for (var ii = 0; ii < originIndex; ii++) {
+ editable.array[ii] = undefined;
+ }
+ }
+ if (newChild) {
+ editable.array[originIndex] = newChild;
+ }
+ return editable;
+ };
+
+ VNode.prototype.removeAfter = function(ownerID, level, index) {
+ if (index === (level ? 1 << level : 0) || this.array.length === 0) {
+ return this;
+ }
+ var sizeIndex = ((index - 1) >>> level) & MASK;
+ if (sizeIndex >= this.array.length) {
+ return this;
+ }
+
+ var newChild;
+ if (level > 0) {
+ var oldChild = this.array[sizeIndex];
+ newChild = oldChild && oldChild.removeAfter(ownerID, level - SHIFT, index);
+ if (newChild === oldChild && sizeIndex === this.array.length - 1) {
+ return this;
+ }
+ }
+
+ var editable = editableVNode(this, ownerID);
+ editable.array.splice(sizeIndex + 1);
+ if (newChild) {
+ editable.array[sizeIndex] = newChild;
+ }
+ return editable;
+ };
+
+
+
+ var DONE = {};
+
+ function iterateList(list, reverse) {
+ var left = list._origin;
+ var right = list._capacity;
+ var tailPos = getTailOffset(right);
+ var tail = list._tail;
+
+ return iterateNodeOrLeaf(list._root, list._level, 0);
+
+ function iterateNodeOrLeaf(node, level, offset) {
+ return level === 0 ?
+ iterateLeaf(node, offset) :
+ iterateNode(node, level, offset);
+ }
+
+ function iterateLeaf(node, offset) {
+ var array = offset === tailPos ? tail && tail.array : node && node.array;
+ var from = offset > left ? 0 : left - offset;
+ var to = right - offset;
+ if (to > SIZE) {
+ to = SIZE;
+ }
+ return function() {
+ if (from === to) {
+ return DONE;
+ }
+ var idx = reverse ? --to : from++;
+ return array && array[idx];
+ };
+ }
+
+ function iterateNode(node, level, offset) {
+ var values;
+ var array = node && node.array;
+ var from = offset > left ? 0 : (left - offset) >> level;
+ var to = ((right - offset) >> level) + 1;
+ if (to > SIZE) {
+ to = SIZE;
+ }
+ return function() {
+ do {
+ if (values) {
+ var value = values();
+ if (value !== DONE) {
+ return value;
+ }
+ values = null;
+ }
+ if (from === to) {
+ return DONE;
+ }
+ var idx = reverse ? --to : from++;
+ values = iterateNodeOrLeaf(
+ array && array[idx], level - SHIFT, offset + (idx << level)
+ );
+ } while (true);
+ };
+ }
+ }
+
+ function makeList(origin, capacity, level, root, tail, ownerID, hash) {
+ var list = Object.create(ListPrototype);
+ list.size = capacity - origin;
+ list._origin = origin;
+ list._capacity = capacity;
+ list._level = level;
+ list._root = root;
+ list._tail = tail;
+ list.__ownerID = ownerID;
+ list.__hash = hash;
+ list.__altered = false;
+ return list;
+ }
+
+ var EMPTY_LIST;
+ function emptyList() {
+ return EMPTY_LIST || (EMPTY_LIST = makeList(0, 0, SHIFT));
+ }
+
+ function updateList(list, index, value) {
+ index = wrapIndex(list, index);
+
+ if (index !== index) {
+ return list;
+ }
+
+ if (index >= list.size || index < 0) {
+ return list.withMutations(function(list ) {
+ index < 0 ?
+ setListBounds(list, index).set(0, value) :
+ setListBounds(list, 0, index + 1).set(index, value)
+ });
+ }
+
+ index += list._origin;
+
+ var newTail = list._tail;
+ var newRoot = list._root;
+ var didAlter = MakeRef(DID_ALTER);
+ if (index >= getTailOffset(list._capacity)) {
+ newTail = updateVNode(newTail, list.__ownerID, 0, index, value, didAlter);
+ } else {
+ newRoot = updateVNode(newRoot, list.__ownerID, list._level, index, value, didAlter);
+ }
+
+ if (!didAlter.value) {
+ return list;
+ }
+
+ if (list.__ownerID) {
+ list._root = newRoot;
+ list._tail = newTail;
+ list.__hash = undefined;
+ list.__altered = true;
+ return list;
+ }
+ return makeList(list._origin, list._capacity, list._level, newRoot, newTail);
+ }
+
+ function updateVNode(node, ownerID, level, index, value, didAlter) {
+ var idx = (index >>> level) & MASK;
+ var nodeHas = node && idx < node.array.length;
+ if (!nodeHas && value === undefined) {
+ return node;
+ }
+
+ var newNode;
+
+ if (level > 0) {
+ var lowerNode = node && node.array[idx];
+ var newLowerNode = updateVNode(lowerNode, ownerID, level - SHIFT, index, value, didAlter);
+ if (newLowerNode === lowerNode) {
+ return node;
+ }
+ newNode = editableVNode(node, ownerID);
+ newNode.array[idx] = newLowerNode;
+ return newNode;
+ }
+
+ if (nodeHas && node.array[idx] === value) {
+ return node;
+ }
+
+ SetRef(didAlter);
+
+ newNode = editableVNode(node, ownerID);
+ if (value === undefined && idx === newNode.array.length - 1) {
+ newNode.array.pop();
+ } else {
+ newNode.array[idx] = value;
+ }
+ return newNode;
+ }
+
+ function editableVNode(node, ownerID) {
+ if (ownerID && node && ownerID === node.ownerID) {
+ return node;
+ }
+ return new VNode(node ? node.array.slice() : [], ownerID);
+ }
+
+ function listNodeFor(list, rawIndex) {
+ if (rawIndex >= getTailOffset(list._capacity)) {
+ return list._tail;
+ }
+ if (rawIndex < 1 << (list._level + SHIFT)) {
+ var node = list._root;
+ var level = list._level;
+ while (node && level > 0) {
+ node = node.array[(rawIndex >>> level) & MASK];
+ level -= SHIFT;
+ }
+ return node;
+ }
+ }
+
+ function setListBounds(list, begin, end) {
+ // Sanitize begin & end using this shorthand for ToInt32(argument)
+ // http://www.ecma-international.org/ecma-262/6.0/#sec-toint32
+ if (begin !== undefined) {
+ begin = begin | 0;
+ }
+ if (end !== undefined) {
+ end = end | 0;
+ }
+ var owner = list.__ownerID || new OwnerID();
+ var oldOrigin = list._origin;
+ var oldCapacity = list._capacity;
+ var newOrigin = oldOrigin + begin;
+ var newCapacity = end === undefined ? oldCapacity : end < 0 ? oldCapacity + end : oldOrigin + end;
+ if (newOrigin === oldOrigin && newCapacity === oldCapacity) {
+ return list;
+ }
+
+ // If it's going to end after it starts, it's empty.
+ if (newOrigin >= newCapacity) {
+ return list.clear();
+ }
+
+ var newLevel = list._level;
+ var newRoot = list._root;
+
+ // New origin might need creating a higher root.
+ var offsetShift = 0;
+ while (newOrigin + offsetShift < 0) {
+ newRoot = new VNode(newRoot && newRoot.array.length ? [undefined, newRoot] : [], owner);
+ newLevel += SHIFT;
+ offsetShift += 1 << newLevel;
+ }
+ if (offsetShift) {
+ newOrigin += offsetShift;
+ oldOrigin += offsetShift;
+ newCapacity += offsetShift;
+ oldCapacity += offsetShift;
+ }
+
+ var oldTailOffset = getTailOffset(oldCapacity);
+ var newTailOffset = getTailOffset(newCapacity);
+
+ // New size might need creating a higher root.
+ while (newTailOffset >= 1 << (newLevel + SHIFT)) {
+ newRoot = new VNode(newRoot && newRoot.array.length ? [newRoot] : [], owner);
+ newLevel += SHIFT;
+ }
+
+ // Locate or create the new tail.
+ var oldTail = list._tail;
+ var newTail = newTailOffset < oldTailOffset ?
+ listNodeFor(list, newCapacity - 1) :
+ newTailOffset > oldTailOffset ? new VNode([], owner) : oldTail;
+
+ // Merge Tail into tree.
+ if (oldTail && newTailOffset > oldTailOffset && newOrigin < oldCapacity && oldTail.array.length) {
+ newRoot = editableVNode(newRoot, owner);
+ var node = newRoot;
+ for (var level = newLevel; level > SHIFT; level -= SHIFT) {
+ var idx = (oldTailOffset >>> level) & MASK;
+ node = node.array[idx] = editableVNode(node.array[idx], owner);
+ }
+ node.array[(oldTailOffset >>> SHIFT) & MASK] = oldTail;
+ }
+
+ // If the size has been reduced, there's a chance the tail needs to be trimmed.
+ if (newCapacity < oldCapacity) {
+ newTail = newTail && newTail.removeAfter(owner, 0, newCapacity);
+ }
+
+ // If the new origin is within the tail, then we do not need a root.
+ if (newOrigin >= newTailOffset) {
+ newOrigin -= newTailOffset;
+ newCapacity -= newTailOffset;
+ newLevel = SHIFT;
+ newRoot = null;
+ newTail = newTail && newTail.removeBefore(owner, 0, newOrigin);
+
+ // Otherwise, if the root has been trimmed, garbage collect.
+ } else if (newOrigin > oldOrigin || newTailOffset < oldTailOffset) {
+ offsetShift = 0;
+
+ // Identify the new top root node of the subtree of the old root.
+ while (newRoot) {
+ var beginIndex = (newOrigin >>> newLevel) & MASK;
+ if (beginIndex !== (newTailOffset >>> newLevel) & MASK) {
+ break;
+ }
+ if (beginIndex) {
+ offsetShift += (1 << newLevel) * beginIndex;
+ }
+ newLevel -= SHIFT;
+ newRoot = newRoot.array[beginIndex];
+ }
+
+ // Trim the new sides of the new root.
+ if (newRoot && newOrigin > oldOrigin) {
+ newRoot = newRoot.removeBefore(owner, newLevel, newOrigin - offsetShift);
+ }
+ if (newRoot && newTailOffset < oldTailOffset) {
+ newRoot = newRoot.removeAfter(owner, newLevel, newTailOffset - offsetShift);
+ }
+ if (offsetShift) {
+ newOrigin -= offsetShift;
+ newCapacity -= offsetShift;
+ }
+ }
+
+ if (list.__ownerID) {
+ list.size = newCapacity - newOrigin;
+ list._origin = newOrigin;
+ list._capacity = newCapacity;
+ list._level = newLevel;
+ list._root = newRoot;
+ list._tail = newTail;
+ list.__hash = undefined;
+ list.__altered = true;
+ return list;
+ }
+ return makeList(newOrigin, newCapacity, newLevel, newRoot, newTail);
+ }
+
+ function mergeIntoListWith(list, merger, iterables) {
+ var iters = [];
+ var maxSize = 0;
+ for (var ii = 0; ii < iterables.length; ii++) {
+ var value = iterables[ii];
+ var iter = IndexedIterable(value);
+ if (iter.size > maxSize) {
+ maxSize = iter.size;
+ }
+ if (!isIterable(value)) {
+ iter = iter.map(function(v ) {return fromJS(v)});
+ }
+ iters.push(iter);
+ }
+ if (maxSize > list.size) {
+ list = list.setSize(maxSize);
+ }
+ return mergeIntoCollectionWith(list, merger, iters);
+ }
+
+ function getTailOffset(size) {
+ return size < SIZE ? 0 : (((size - 1) >>> SHIFT) << SHIFT);
+ }
+
+ createClass(OrderedMap, Map);
+
+ // @pragma Construction
+
+ function OrderedMap(value) {
+ return value === null || value === undefined ? emptyOrderedMap() :
+ isOrderedMap(value) ? value :
+ emptyOrderedMap().withMutations(function(map ) {
+ var iter = KeyedIterable(value);
+ assertNotInfinite(iter.size);
+ iter.forEach(function(v, k) {return map.set(k, v)});
+ });
+ }
+
+ OrderedMap.of = function(/*...values*/) {
+ return this(arguments);
+ };
+
+ OrderedMap.prototype.toString = function() {
+ return this.__toString('OrderedMap {', '}');
+ };
+
+ // @pragma Access
+
+ OrderedMap.prototype.get = function(k, notSetValue) {
+ var index = this._map.get(k);
+ return index !== undefined ? this._list.get(index)[1] : notSetValue;
+ };
+
+ // @pragma Modification
+
+ OrderedMap.prototype.clear = function() {
+ if (this.size === 0) {
+ return this;
+ }
+ if (this.__ownerID) {
+ this.size = 0;
+ this._map.clear();
+ this._list.clear();
+ return this;
+ }
+ return emptyOrderedMap();
+ };
+
+ OrderedMap.prototype.set = function(k, v) {
+ return updateOrderedMap(this, k, v);
+ };
+
+ OrderedMap.prototype.remove = function(k) {
+ return updateOrderedMap(this, k, NOT_SET);
+ };
+
+ OrderedMap.prototype.wasAltered = function() {
+ return this._map.wasAltered() || this._list.wasAltered();
+ };
+
+ OrderedMap.prototype.__iterate = function(fn, reverse) {var this$0 = this;
+ return this._list.__iterate(
+ function(entry ) {return entry && fn(entry[1], entry[0], this$0)},
+ reverse
+ );
+ };
+
+ OrderedMap.prototype.__iterator = function(type, reverse) {
+ return this._list.fromEntrySeq().__iterator(type, reverse);
+ };
+
+ OrderedMap.prototype.__ensureOwner = function(ownerID) {
+ if (ownerID === this.__ownerID) {
+ return this;
+ }
+ var newMap = this._map.__ensureOwner(ownerID);
+ var newList = this._list.__ensureOwner(ownerID);
+ if (!ownerID) {
+ this.__ownerID = ownerID;
+ this._map = newMap;
+ this._list = newList;
+ return this;
+ }
+ return makeOrderedMap(newMap, newList, ownerID, this.__hash);
+ };
+
+
+ function isOrderedMap(maybeOrderedMap) {
+ return isMap(maybeOrderedMap) && isOrdered(maybeOrderedMap);
+ }
+
+ OrderedMap.isOrderedMap = isOrderedMap;
+
+ OrderedMap.prototype[IS_ORDERED_SENTINEL] = true;
+ OrderedMap.prototype[DELETE] = OrderedMap.prototype.remove;
+
+
+
+ function makeOrderedMap(map, list, ownerID, hash) {
+ var omap = Object.create(OrderedMap.prototype);
+ omap.size = map ? map.size : 0;
+ omap._map = map;
+ omap._list = list;
+ omap.__ownerID = ownerID;
+ omap.__hash = hash;
+ return omap;
+ }
+
+ var EMPTY_ORDERED_MAP;
+ function emptyOrderedMap() {
+ return EMPTY_ORDERED_MAP || (EMPTY_ORDERED_MAP = makeOrderedMap(emptyMap(), emptyList()));
+ }
+
+ function updateOrderedMap(omap, k, v) {
+ var map = omap._map;
+ var list = omap._list;
+ var i = map.get(k);
+ var has = i !== undefined;
+ var newMap;
+ var newList;
+ if (v === NOT_SET) { // removed
+ if (!has) {
+ return omap;
+ }
+ if (list.size >= SIZE && list.size >= map.size * 2) {
+ newList = list.filter(function(entry, idx) {return entry !== undefined && i !== idx});
+ newMap = newList.toKeyedSeq().map(function(entry ) {return entry[0]}).flip().toMap();
+ if (omap.__ownerID) {
+ newMap.__ownerID = newList.__ownerID = omap.__ownerID;
+ }
+ } else {
+ newMap = map.remove(k);
+ newList = i === list.size - 1 ? list.pop() : list.set(i, undefined);
+ }
+ } else {
+ if (has) {
+ if (v === list.get(i)[1]) {
+ return omap;
+ }
+ newMap = map;
+ newList = list.set(i, [k, v]);
+ } else {
+ newMap = map.set(k, list.size);
+ newList = list.set(list.size, [k, v]);
+ }
+ }
+ if (omap.__ownerID) {
+ omap.size = newMap.size;
+ omap._map = newMap;
+ omap._list = newList;
+ omap.__hash = undefined;
+ return omap;
+ }
+ return makeOrderedMap(newMap, newList);
+ }
+
+ createClass(ToKeyedSequence, KeyedSeq);
+ function ToKeyedSequence(indexed, useKeys) {
+ this._iter = indexed;
+ this._useKeys = useKeys;
+ this.size = indexed.size;
+ }
+
+ ToKeyedSequence.prototype.get = function(key, notSetValue) {
+ return this._iter.get(key, notSetValue);
+ };
+
+ ToKeyedSequence.prototype.has = function(key) {
+ return this._iter.has(key);
+ };
+
+ ToKeyedSequence.prototype.valueSeq = function() {
+ return this._iter.valueSeq();
+ };
+
+ ToKeyedSequence.prototype.reverse = function() {var this$0 = this;
+ var reversedSequence = reverseFactory(this, true);
+ if (!this._useKeys) {
+ reversedSequence.valueSeq = function() {return this$0._iter.toSeq().reverse()};
+ }
+ return reversedSequence;
+ };
+
+ ToKeyedSequence.prototype.map = function(mapper, context) {var this$0 = this;
+ var mappedSequence = mapFactory(this, mapper, context);
+ if (!this._useKeys) {
+ mappedSequence.valueSeq = function() {return this$0._iter.toSeq().map(mapper, context)};
+ }
+ return mappedSequence;
+ };
+
+ ToKeyedSequence.prototype.__iterate = function(fn, reverse) {var this$0 = this;
+ var ii;
+ return this._iter.__iterate(
+ this._useKeys ?
+ function(v, k) {return fn(v, k, this$0)} :
+ ((ii = reverse ? resolveSize(this) : 0),
+ function(v ) {return fn(v, reverse ? --ii : ii++, this$0)}),
+ reverse
+ );
+ };
+
+ ToKeyedSequence.prototype.__iterator = function(type, reverse) {
+ if (this._useKeys) {
+ return this._iter.__iterator(type, reverse);
+ }
+ var iterator = this._iter.__iterator(ITERATE_VALUES, reverse);
+ var ii = reverse ? resolveSize(this) : 0;
+ return new Iterator(function() {
+ var step = iterator.next();
+ return step.done ? step :
+ iteratorValue(type, reverse ? --ii : ii++, step.value, step);
+ });
+ };
+
+ ToKeyedSequence.prototype[IS_ORDERED_SENTINEL] = true;
+
+
+ createClass(ToIndexedSequence, IndexedSeq);
+ function ToIndexedSequence(iter) {
+ this._iter = iter;
+ this.size = iter.size;
+ }
+
+ ToIndexedSequence.prototype.includes = function(value) {
+ return this._iter.includes(value);
+ };
+
+ ToIndexedSequence.prototype.__iterate = function(fn, reverse) {var this$0 = this;
+ var iterations = 0;
+ return this._iter.__iterate(function(v ) {return fn(v, iterations++, this$0)}, reverse);
+ };
+
+ ToIndexedSequence.prototype.__iterator = function(type, reverse) {
+ var iterator = this._iter.__iterator(ITERATE_VALUES, reverse);
+ var iterations = 0;
+ return new Iterator(function() {
+ var step = iterator.next();
+ return step.done ? step :
+ iteratorValue(type, iterations++, step.value, step)
+ });
+ };
+
+
+
+ createClass(ToSetSequence, SetSeq);
+ function ToSetSequence(iter) {
+ this._iter = iter;
+ this.size = iter.size;
+ }
+
+ ToSetSequence.prototype.has = function(key) {
+ return this._iter.includes(key);
+ };
+
+ ToSetSequence.prototype.__iterate = function(fn, reverse) {var this$0 = this;
+ return this._iter.__iterate(function(v ) {return fn(v, v, this$0)}, reverse);
+ };
+
+ ToSetSequence.prototype.__iterator = function(type, reverse) {
+ var iterator = this._iter.__iterator(ITERATE_VALUES, reverse);
+ return new Iterator(function() {
+ var step = iterator.next();
+ return step.done ? step :
+ iteratorValue(type, step.value, step.value, step);
+ });
+ };
+
+
+
+ createClass(FromEntriesSequence, KeyedSeq);
+ function FromEntriesSequence(entries) {
+ this._iter = entries;
+ this.size = entries.size;
+ }
+
+ FromEntriesSequence.prototype.entrySeq = function() {
+ return this._iter.toSeq();
+ };
+
+ FromEntriesSequence.prototype.__iterate = function(fn, reverse) {var this$0 = this;
+ return this._iter.__iterate(function(entry ) {
+ // Check if entry exists first so array access doesn't throw for holes
+ // in the parent iteration.
+ if (entry) {
+ validateEntry(entry);
+ var indexedIterable = isIterable(entry);
+ return fn(
+ indexedIterable ? entry.get(1) : entry[1],
+ indexedIterable ? entry.get(0) : entry[0],
+ this$0
+ );
+ }
+ }, reverse);
+ };
+
+ FromEntriesSequence.prototype.__iterator = function(type, reverse) {
+ var iterator = this._iter.__iterator(ITERATE_VALUES, reverse);
+ return new Iterator(function() {
+ while (true) {
+ var step = iterator.next();
+ if (step.done) {
+ return step;
+ }
+ var entry = step.value;
+ // Check if entry exists first so array access doesn't throw for holes
+ // in the parent iteration.
+ if (entry) {
+ validateEntry(entry);
+ var indexedIterable = isIterable(entry);
+ return iteratorValue(
+ type,
+ indexedIterable ? entry.get(0) : entry[0],
+ indexedIterable ? entry.get(1) : entry[1],
+ step
+ );
+ }
+ }
+ });
+ };
+
+
+ ToIndexedSequence.prototype.cacheResult =
+ ToKeyedSequence.prototype.cacheResult =
+ ToSetSequence.prototype.cacheResult =
+ FromEntriesSequence.prototype.cacheResult =
+ cacheResultThrough;
+
+
+ function flipFactory(iterable) {
+ var flipSequence = makeSequence(iterable);
+ flipSequence._iter = iterable;
+ flipSequence.size = iterable.size;
+ flipSequence.flip = function() {return iterable};
+ flipSequence.reverse = function () {
+ var reversedSequence = iterable.reverse.apply(this); // super.reverse()
+ reversedSequence.flip = function() {return iterable.reverse()};
+ return reversedSequence;
+ };
+ flipSequence.has = function(key ) {return iterable.includes(key)};
+ flipSequence.includes = function(key ) {return iterable.has(key)};
+ flipSequence.cacheResult = cacheResultThrough;
+ flipSequence.__iterateUncached = function (fn, reverse) {var this$0 = this;
+ return iterable.__iterate(function(v, k) {return fn(k, v, this$0) !== false}, reverse);
+ }
+ flipSequence.__iteratorUncached = function(type, reverse) {
+ if (type === ITERATE_ENTRIES) {
+ var iterator = iterable.__iterator(type, reverse);
+ return new Iterator(function() {
+ var step = iterator.next();
+ if (!step.done) {
+ var k = step.value[0];
+ step.value[0] = step.value[1];
+ step.value[1] = k;
+ }
+ return step;
+ });
+ }
+ return iterable.__iterator(
+ type === ITERATE_VALUES ? ITERATE_KEYS : ITERATE_VALUES,
+ reverse
+ );
+ }
+ return flipSequence;
+ }
+
+
+ function mapFactory(iterable, mapper, context) {
+ var mappedSequence = makeSequence(iterable);
+ mappedSequence.size = iterable.size;
+ mappedSequence.has = function(key ) {return iterable.has(key)};
+ mappedSequence.get = function(key, notSetValue) {
+ var v = iterable.get(key, NOT_SET);
+ return v === NOT_SET ?
+ notSetValue :
+ mapper.call(context, v, key, iterable);
+ };
+ mappedSequence.__iterateUncached = function (fn, reverse) {var this$0 = this;
+ return iterable.__iterate(
+ function(v, k, c) {return fn(mapper.call(context, v, k, c), k, this$0) !== false},
+ reverse
+ );
+ }
+ mappedSequence.__iteratorUncached = function (type, reverse) {
+ var iterator = iterable.__iterator(ITERATE_ENTRIES, reverse);
+ return new Iterator(function() {
+ var step = iterator.next();
+ if (step.done) {
+ return step;
+ }
+ var entry = step.value;
+ var key = entry[0];
+ return iteratorValue(
+ type,
+ key,
+ mapper.call(context, entry[1], key, iterable),
+ step
+ );
+ });
+ }
+ return mappedSequence;
+ }
+
+
+ function reverseFactory(iterable, useKeys) {
+ var reversedSequence = makeSequence(iterable);
+ reversedSequence._iter = iterable;
+ reversedSequence.size = iterable.size;
+ reversedSequence.reverse = function() {return iterable};
+ if (iterable.flip) {
+ reversedSequence.flip = function () {
+ var flipSequence = flipFactory(iterable);
+ flipSequence.reverse = function() {return iterable.flip()};
+ return flipSequence;
+ };
+ }
+ reversedSequence.get = function(key, notSetValue)
+ {return iterable.get(useKeys ? key : -1 - key, notSetValue)};
+ reversedSequence.has = function(key )
+ {return iterable.has(useKeys ? key : -1 - key)};
+ reversedSequence.includes = function(value ) {return iterable.includes(value)};
+ reversedSequence.cacheResult = cacheResultThrough;
+ reversedSequence.__iterate = function (fn, reverse) {var this$0 = this;
+ return iterable.__iterate(function(v, k) {return fn(v, k, this$0)}, !reverse);
+ };
+ reversedSequence.__iterator =
+ function(type, reverse) {return iterable.__iterator(type, !reverse)};
+ return reversedSequence;
+ }
+
+
+ function filterFactory(iterable, predicate, context, useKeys) {
+ var filterSequence = makeSequence(iterable);
+ if (useKeys) {
+ filterSequence.has = function(key ) {
+ var v = iterable.get(key, NOT_SET);
+ return v !== NOT_SET && !!predicate.call(context, v, key, iterable);
+ };
+ filterSequence.get = function(key, notSetValue) {
+ var v = iterable.get(key, NOT_SET);
+ return v !== NOT_SET && predicate.call(context, v, key, iterable) ?
+ v : notSetValue;
+ };
+ }
+ filterSequence.__iterateUncached = function (fn, reverse) {var this$0 = this;
+ var iterations = 0;
+ iterable.__iterate(function(v, k, c) {
+ if (predicate.call(context, v, k, c)) {
+ iterations++;
+ return fn(v, useKeys ? k : iterations - 1, this$0);
+ }
+ }, reverse);
+ return iterations;
+ };
+ filterSequence.__iteratorUncached = function (type, reverse) {
+ var iterator = iterable.__iterator(ITERATE_ENTRIES, reverse);
+ var iterations = 0;
+ return new Iterator(function() {
+ while (true) {
+ var step = iterator.next();
+ if (step.done) {
+ return step;
+ }
+ var entry = step.value;
+ var key = entry[0];
+ var value = entry[1];
+ if (predicate.call(context, value, key, iterable)) {
+ return iteratorValue(type, useKeys ? key : iterations++, value, step);
+ }
+ }
+ });
+ }
+ return filterSequence;
+ }
+
+
+ function countByFactory(iterable, grouper, context) {
+ var groups = Map().asMutable();
+ iterable.__iterate(function(v, k) {
+ groups.update(
+ grouper.call(context, v, k, iterable),
+ 0,
+ function(a ) {return a + 1}
+ );
+ });
+ return groups.asImmutable();
+ }
+
+
+ function groupByFactory(iterable, grouper, context) {
+ var isKeyedIter = isKeyed(iterable);
+ var groups = (isOrdered(iterable) ? OrderedMap() : Map()).asMutable();
+ iterable.__iterate(function(v, k) {
+ groups.update(
+ grouper.call(context, v, k, iterable),
+ function(a ) {return (a = a || [], a.push(isKeyedIter ? [k, v] : v), a)}
+ );
+ });
+ var coerce = iterableClass(iterable);
+ return groups.map(function(arr ) {return reify(iterable, coerce(arr))});
+ }
+
+
+ function sliceFactory(iterable, begin, end, useKeys) {
+ var originalSize = iterable.size;
+
+ // Sanitize begin & end using this shorthand for ToInt32(argument)
+ // http://www.ecma-international.org/ecma-262/6.0/#sec-toint32
+ if (begin !== undefined) {
+ begin = begin | 0;
+ }
+ if (end !== undefined) {
+ if (end === Infinity) {
+ end = originalSize;
+ } else {
+ end = end | 0;
+ }
+ }
+
+ if (wholeSlice(begin, end, originalSize)) {
+ return iterable;
+ }
+
+ var resolvedBegin = resolveBegin(begin, originalSize);
+ var resolvedEnd = resolveEnd(end, originalSize);
+
+ // begin or end will be NaN if they were provided as negative numbers and
+ // this iterable's size is unknown. In that case, cache first so there is
+ // a known size and these do not resolve to NaN.
+ if (resolvedBegin !== resolvedBegin || resolvedEnd !== resolvedEnd) {
+ return sliceFactory(iterable.toSeq().cacheResult(), begin, end, useKeys);
+ }
+
+ // Note: resolvedEnd is undefined when the original sequence's length is
+ // unknown and this slice did not supply an end and should contain all
+ // elements after resolvedBegin.
+ // In that case, resolvedSize will be NaN and sliceSize will remain undefined.
+ var resolvedSize = resolvedEnd - resolvedBegin;
+ var sliceSize;
+ if (resolvedSize === resolvedSize) {
+ sliceSize = resolvedSize < 0 ? 0 : resolvedSize;
+ }
+
+ var sliceSeq = makeSequence(iterable);
+
+ // If iterable.size is undefined, the size of the realized sliceSeq is
+ // unknown at this point unless the number of items to slice is 0
+ sliceSeq.size = sliceSize === 0 ? sliceSize : iterable.size && sliceSize || undefined;
+
+ if (!useKeys && isSeq(iterable) && sliceSize >= 0) {
+ sliceSeq.get = function (index, notSetValue) {
+ index = wrapIndex(this, index);
+ return index >= 0 && index < sliceSize ?
+ iterable.get(index + resolvedBegin, notSetValue) :
+ notSetValue;
+ }
+ }
+
+ sliceSeq.__iterateUncached = function(fn, reverse) {var this$0 = this;
+ if (sliceSize === 0) {
+ return 0;
+ }
+ if (reverse) {
+ return this.cacheResult().__iterate(fn, reverse);
+ }
+ var skipped = 0;
+ var isSkipping = true;
+ var iterations = 0;
+ iterable.__iterate(function(v, k) {
+ if (!(isSkipping && (isSkipping = skipped++ < resolvedBegin))) {
+ iterations++;
+ return fn(v, useKeys ? k : iterations - 1, this$0) !== false &&
+ iterations !== sliceSize;
+ }
+ });
+ return iterations;
+ };
+
+ sliceSeq.__iteratorUncached = function(type, reverse) {
+ if (sliceSize !== 0 && reverse) {
+ return this.cacheResult().__iterator(type, reverse);
+ }
+ // Don't bother instantiating parent iterator if taking 0.
+ var iterator = sliceSize !== 0 && iterable.__iterator(type, reverse);
+ var skipped = 0;
+ var iterations = 0;
+ return new Iterator(function() {
+ while (skipped++ < resolvedBegin) {
+ iterator.next();
+ }
+ if (++iterations > sliceSize) {
+ return iteratorDone();
+ }
+ var step = iterator.next();
+ if (useKeys || type === ITERATE_VALUES) {
+ return step;
+ } else if (type === ITERATE_KEYS) {
+ return iteratorValue(type, iterations - 1, undefined, step);
+ } else {
+ return iteratorValue(type, iterations - 1, step.value[1], step);
+ }
+ });
+ }
+
+ return sliceSeq;
+ }
+
+
+ function takeWhileFactory(iterable, predicate, context) {
+ var takeSequence = makeSequence(iterable);
+ takeSequence.__iterateUncached = function(fn, reverse) {var this$0 = this;
+ if (reverse) {
+ return this.cacheResult().__iterate(fn, reverse);
+ }
+ var iterations = 0;
+ iterable.__iterate(function(v, k, c)
+ {return predicate.call(context, v, k, c) && ++iterations && fn(v, k, this$0)}
+ );
+ return iterations;
+ };
+ takeSequence.__iteratorUncached = function(type, reverse) {var this$0 = this;
+ if (reverse) {
+ return this.cacheResult().__iterator(type, reverse);
+ }
+ var iterator = iterable.__iterator(ITERATE_ENTRIES, reverse);
+ var iterating = true;
+ return new Iterator(function() {
+ if (!iterating) {
+ return iteratorDone();
+ }
+ var step = iterator.next();
+ if (step.done) {
+ return step;
+ }
+ var entry = step.value;
+ var k = entry[0];
+ var v = entry[1];
+ if (!predicate.call(context, v, k, this$0)) {
+ iterating = false;
+ return iteratorDone();
+ }
+ return type === ITERATE_ENTRIES ? step :
+ iteratorValue(type, k, v, step);
+ });
+ };
+ return takeSequence;
+ }
+
+
+ function skipWhileFactory(iterable, predicate, context, useKeys) {
+ var skipSequence = makeSequence(iterable);
+ skipSequence.__iterateUncached = function (fn, reverse) {var this$0 = this;
+ if (reverse) {
+ return this.cacheResult().__iterate(fn, reverse);
+ }
+ var isSkipping = true;
+ var iterations = 0;
+ iterable.__iterate(function(v, k, c) {
+ if (!(isSkipping && (isSkipping = predicate.call(context, v, k, c)))) {
+ iterations++;
+ return fn(v, useKeys ? k : iterations - 1, this$0);
+ }
+ });
+ return iterations;
+ };
+ skipSequence.__iteratorUncached = function(type, reverse) {var this$0 = this;
+ if (reverse) {
+ return this.cacheResult().__iterator(type, reverse);
+ }
+ var iterator = iterable.__iterator(ITERATE_ENTRIES, reverse);
+ var skipping = true;
+ var iterations = 0;
+ return new Iterator(function() {
+ var step, k, v;
+ do {
+ step = iterator.next();
+ if (step.done) {
+ if (useKeys || type === ITERATE_VALUES) {
+ return step;
+ } else if (type === ITERATE_KEYS) {
+ return iteratorValue(type, iterations++, undefined, step);
+ } else {
+ return iteratorValue(type, iterations++, step.value[1], step);
+ }
+ }
+ var entry = step.value;
+ k = entry[0];
+ v = entry[1];
+ skipping && (skipping = predicate.call(context, v, k, this$0));
+ } while (skipping);
+ return type === ITERATE_ENTRIES ? step :
+ iteratorValue(type, k, v, step);
+ });
+ };
+ return skipSequence;
+ }
+
+
+ function concatFactory(iterable, values) {
+ var isKeyedIterable = isKeyed(iterable);
+ var iters = [iterable].concat(values).map(function(v ) {
+ if (!isIterable(v)) {
+ v = isKeyedIterable ?
+ keyedSeqFromValue(v) :
+ indexedSeqFromValue(Array.isArray(v) ? v : [v]);
+ } else if (isKeyedIterable) {
+ v = KeyedIterable(v);
+ }
+ return v;
+ }).filter(function(v ) {return v.size !== 0});
+
+ if (iters.length === 0) {
+ return iterable;
+ }
+
+ if (iters.length === 1) {
+ var singleton = iters[0];
+ if (singleton === iterable ||
+ isKeyedIterable && isKeyed(singleton) ||
+ isIndexed(iterable) && isIndexed(singleton)) {
+ return singleton;
+ }
+ }
+
+ var concatSeq = new ArraySeq(iters);
+ if (isKeyedIterable) {
+ concatSeq = concatSeq.toKeyedSeq();
+ } else if (!isIndexed(iterable)) {
+ concatSeq = concatSeq.toSetSeq();
+ }
+ concatSeq = concatSeq.flatten(true);
+ concatSeq.size = iters.reduce(
+ function(sum, seq) {
+ if (sum !== undefined) {
+ var size = seq.size;
+ if (size !== undefined) {
+ return sum + size;
+ }
+ }
+ },
+ 0
+ );
+ return concatSeq;
+ }
+
+
+ function flattenFactory(iterable, depth, useKeys) {
+ var flatSequence = makeSequence(iterable);
+ flatSequence.__iterateUncached = function(fn, reverse) {
+ var iterations = 0;
+ var stopped = false;
+ function flatDeep(iter, currentDepth) {var this$0 = this;
+ iter.__iterate(function(v, k) {
+ if ((!depth || currentDepth < depth) && isIterable(v)) {
+ flatDeep(v, currentDepth + 1);
+ } else if (fn(v, useKeys ? k : iterations++, this$0) === false) {
+ stopped = true;
+ }
+ return !stopped;
+ }, reverse);
+ }
+ flatDeep(iterable, 0);
+ return iterations;
+ }
+ flatSequence.__iteratorUncached = function(type, reverse) {
+ var iterator = iterable.__iterator(type, reverse);
+ var stack = [];
+ var iterations = 0;
+ return new Iterator(function() {
+ while (iterator) {
+ var step = iterator.next();
+ if (step.done !== false) {
+ iterator = stack.pop();
+ continue;
+ }
+ var v = step.value;
+ if (type === ITERATE_ENTRIES) {
+ v = v[1];
+ }
+ if ((!depth || stack.length < depth) && isIterable(v)) {
+ stack.push(iterator);
+ iterator = v.__iterator(type, reverse);
+ } else {
+ return useKeys ? step : iteratorValue(type, iterations++, v, step);
+ }
+ }
+ return iteratorDone();
+ });
+ }
+ return flatSequence;
+ }
+
+
+ function flatMapFactory(iterable, mapper, context) {
+ var coerce = iterableClass(iterable);
+ return iterable.toSeq().map(
+ function(v, k) {return coerce(mapper.call(context, v, k, iterable))}
+ ).flatten(true);
+ }
+
+
+ function interposeFactory(iterable, separator) {
+ var interposedSequence = makeSequence(iterable);
+ interposedSequence.size = iterable.size && iterable.size * 2 -1;
+ interposedSequence.__iterateUncached = function(fn, reverse) {var this$0 = this;
+ var iterations = 0;
+ iterable.__iterate(function(v, k)
+ {return (!iterations || fn(separator, iterations++, this$0) !== false) &&
+ fn(v, iterations++, this$0) !== false},
+ reverse
+ );
+ return iterations;
+ };
+ interposedSequence.__iteratorUncached = function(type, reverse) {
+ var iterator = iterable.__iterator(ITERATE_VALUES, reverse);
+ var iterations = 0;
+ var step;
+ return new Iterator(function() {
+ if (!step || iterations % 2) {
+ step = iterator.next();
+ if (step.done) {
+ return step;
+ }
+ }
+ return iterations % 2 ?
+ iteratorValue(type, iterations++, separator) :
+ iteratorValue(type, iterations++, step.value, step);
+ });
+ };
+ return interposedSequence;
+ }
+
+
+ function sortFactory(iterable, comparator, mapper) {
+ if (!comparator) {
+ comparator = defaultComparator;
+ }
+ var isKeyedIterable = isKeyed(iterable);
+ var index = 0;
+ var entries = iterable.toSeq().map(
+ function(v, k) {return [k, v, index++, mapper ? mapper(v, k, iterable) : v]}
+ ).toArray();
+ entries.sort(function(a, b) {return comparator(a[3], b[3]) || a[2] - b[2]}).forEach(
+ isKeyedIterable ?
+ function(v, i) { entries[i].length = 2; } :
+ function(v, i) { entries[i] = v[1]; }
+ );
+ return isKeyedIterable ? KeyedSeq(entries) :
+ isIndexed(iterable) ? IndexedSeq(entries) :
+ SetSeq(entries);
+ }
+
+
+ function maxFactory(iterable, comparator, mapper) {
+ if (!comparator) {
+ comparator = defaultComparator;
+ }
+ if (mapper) {
+ var entry = iterable.toSeq()
+ .map(function(v, k) {return [v, mapper(v, k, iterable)]})
+ .reduce(function(a, b) {return maxCompare(comparator, a[1], b[1]) ? b : a});
+ return entry && entry[0];
+ } else {
+ return iterable.reduce(function(a, b) {return maxCompare(comparator, a, b) ? b : a});
+ }
+ }
+
+ function maxCompare(comparator, a, b) {
+ var comp = comparator(b, a);
+ // b is considered the new max if the comparator declares them equal, but
+ // they are not equal and b is in fact a nullish value.
+ return (comp === 0 && b !== a && (b === undefined || b === null || b !== b)) || comp > 0;
+ }
+
+
+ function zipWithFactory(keyIter, zipper, iters) {
+ var zipSequence = makeSequence(keyIter);
+ zipSequence.size = new ArraySeq(iters).map(function(i ) {return i.size}).min();
+ // Note: this a generic base implementation of __iterate in terms of
+ // __iterator which may be more generically useful in the future.
+ zipSequence.__iterate = function(fn, reverse) {
+ /* generic:
+ var iterator = this.__iterator(ITERATE_ENTRIES, reverse);
+ var step;
+ var iterations = 0;
+ while (!(step = iterator.next()).done) {
+ iterations++;
+ if (fn(step.value[1], step.value[0], this) === false) {
+ break;
+ }
+ }
+ return iterations;
+ */
+ // indexed:
+ var iterator = this.__iterator(ITERATE_VALUES, reverse);
+ var step;
+ var iterations = 0;
+ while (!(step = iterator.next()).done) {
+ if (fn(step.value, iterations++, this) === false) {
+ break;
+ }
+ }
+ return iterations;
+ };
+ zipSequence.__iteratorUncached = function(type, reverse) {
+ var iterators = iters.map(function(i )
+ {return (i = Iterable(i), getIterator(reverse ? i.reverse() : i))}
+ );
+ var iterations = 0;
+ var isDone = false;
+ return new Iterator(function() {
+ var steps;
+ if (!isDone) {
+ steps = iterators.map(function(i ) {return i.next()});
+ isDone = steps.some(function(s ) {return s.done});
+ }
+ if (isDone) {
+ return iteratorDone();
+ }
+ return iteratorValue(
+ type,
+ iterations++,
+ zipper.apply(null, steps.map(function(s ) {return s.value}))
+ );
+ });
+ };
+ return zipSequence
+ }
+
+
+ // #pragma Helper Functions
+
+ function reify(iter, seq) {
+ return isSeq(iter) ? seq : iter.constructor(seq);
+ }
+
+ function validateEntry(entry) {
+ if (entry !== Object(entry)) {
+ throw new TypeError('Expected [K, V] tuple: ' + entry);
+ }
+ }
+
+ function resolveSize(iter) {
+ assertNotInfinite(iter.size);
+ return ensureSize(iter);
+ }
+
+ function iterableClass(iterable) {
+ return isKeyed(iterable) ? KeyedIterable :
+ isIndexed(iterable) ? IndexedIterable :
+ SetIterable;
+ }
+
+ function makeSequence(iterable) {
+ return Object.create(
+ (
+ isKeyed(iterable) ? KeyedSeq :
+ isIndexed(iterable) ? IndexedSeq :
+ SetSeq
+ ).prototype
+ );
+ }
+
+ function cacheResultThrough() {
+ if (this._iter.cacheResult) {
+ this._iter.cacheResult();
+ this.size = this._iter.size;
+ return this;
+ } else {
+ return Seq.prototype.cacheResult.call(this);
+ }
+ }
+
+ function defaultComparator(a, b) {
+ return a > b ? 1 : a < b ? -1 : 0;
+ }
+
+ function forceIterator(keyPath) {
+ var iter = getIterator(keyPath);
+ if (!iter) {
+ // Array might not be iterable in this environment, so we need a fallback
+ // to our wrapped type.
+ if (!isArrayLike(keyPath)) {
+ throw new TypeError('Expected iterable or array-like: ' + keyPath);
+ }
+ iter = getIterator(Iterable(keyPath));
+ }
+ return iter;
+ }
+
+ createClass(Record, KeyedCollection);
+
+ function Record(defaultValues, name) {
+ var hasInitialized;
+
+ var RecordType = function Record(values) {
+ if (values instanceof RecordType) {
+ return values;
+ }
+ if (!(this instanceof RecordType)) {
+ return new RecordType(values);
+ }
+ if (!hasInitialized) {
+ hasInitialized = true;
+ var keys = Object.keys(defaultValues);
+ setProps(RecordTypePrototype, keys);
+ RecordTypePrototype.size = keys.length;
+ RecordTypePrototype._name = name;
+ RecordTypePrototype._keys = keys;
+ RecordTypePrototype._defaultValues = defaultValues;
+ }
+ this._map = Map(values);
+ };
+
+ var RecordTypePrototype = RecordType.prototype = Object.create(RecordPrototype);
+ RecordTypePrototype.constructor = RecordType;
+
+ return RecordType;
+ }
+
+ Record.prototype.toString = function() {
+ return this.__toString(recordName(this) + ' {', '}');
+ };
+
+ // @pragma Access
+
+ Record.prototype.has = function(k) {
+ return this._defaultValues.hasOwnProperty(k);
+ };
+
+ Record.prototype.get = function(k, notSetValue) {
+ if (!this.has(k)) {
+ return notSetValue;
+ }
+ var defaultVal = this._defaultValues[k];
+ return this._map ? this._map.get(k, defaultVal) : defaultVal;
+ };
+
+ // @pragma Modification
+
+ Record.prototype.clear = function() {
+ if (this.__ownerID) {
+ this._map && this._map.clear();
+ return this;
+ }
+ var RecordType = this.constructor;
+ return RecordType._empty || (RecordType._empty = makeRecord(this, emptyMap()));
+ };
+
+ Record.prototype.set = function(k, v) {
+ if (!this.has(k)) {
+ throw new Error('Cannot set unknown key "' + k + '" on ' + recordName(this));
+ }
+ if (this._map && !this._map.has(k)) {
+ var defaultVal = this._defaultValues[k];
+ if (v === defaultVal) {
+ return this;
+ }
+ }
+ var newMap = this._map && this._map.set(k, v);
+ if (this.__ownerID || newMap === this._map) {
+ return this;
+ }
+ return makeRecord(this, newMap);
+ };
+
+ Record.prototype.remove = function(k) {
+ if (!this.has(k)) {
+ return this;
+ }
+ var newMap = this._map && this._map.remove(k);
+ if (this.__ownerID || newMap === this._map) {
+ return this;
+ }
+ return makeRecord(this, newMap);
+ };
+
+ Record.prototype.wasAltered = function() {
+ return this._map.wasAltered();
+ };
+
+ Record.prototype.__iterator = function(type, reverse) {var this$0 = this;
+ return KeyedIterable(this._defaultValues).map(function(_, k) {return this$0.get(k)}).__iterator(type, reverse);
+ };
+
+ Record.prototype.__iterate = function(fn, reverse) {var this$0 = this;
+ return KeyedIterable(this._defaultValues).map(function(_, k) {return this$0.get(k)}).__iterate(fn, reverse);
+ };
+
+ Record.prototype.__ensureOwner = function(ownerID) {
+ if (ownerID === this.__ownerID) {
+ return this;
+ }
+ var newMap = this._map && this._map.__ensureOwner(ownerID);
+ if (!ownerID) {
+ this.__ownerID = ownerID;
+ this._map = newMap;
+ return this;
+ }
+ return makeRecord(this, newMap, ownerID);
+ };
+
+
+ var RecordPrototype = Record.prototype;
+ RecordPrototype[DELETE] = RecordPrototype.remove;
+ RecordPrototype.deleteIn =
+ RecordPrototype.removeIn = MapPrototype.removeIn;
+ RecordPrototype.merge = MapPrototype.merge;
+ RecordPrototype.mergeWith = MapPrototype.mergeWith;
+ RecordPrototype.mergeIn = MapPrototype.mergeIn;
+ RecordPrototype.mergeDeep = MapPrototype.mergeDeep;
+ RecordPrototype.mergeDeepWith = MapPrototype.mergeDeepWith;
+ RecordPrototype.mergeDeepIn = MapPrototype.mergeDeepIn;
+ RecordPrototype.setIn = MapPrototype.setIn;
+ RecordPrototype.update = MapPrototype.update;
+ RecordPrototype.updateIn = MapPrototype.updateIn;
+ RecordPrototype.withMutations = MapPrototype.withMutations;
+ RecordPrototype.asMutable = MapPrototype.asMutable;
+ RecordPrototype.asImmutable = MapPrototype.asImmutable;
+
+
+ function makeRecord(likeRecord, map, ownerID) {
+ var record = Object.create(Object.getPrototypeOf(likeRecord));
+ record._map = map;
+ record.__ownerID = ownerID;
+ return record;
+ }
+
+ function recordName(record) {
+ return record._name || record.constructor.name || 'Record';
+ }
+
+ function setProps(prototype, names) {
+ try {
+ names.forEach(setProp.bind(undefined, prototype));
+ } catch (error) {
+ // Object.defineProperty failed. Probably IE8.
+ }
+ }
+
+ function setProp(prototype, name) {
+ Object.defineProperty(prototype, name, {
+ get: function() {
+ return this.get(name);
+ },
+ set: function(value) {
+ invariant(this.__ownerID, 'Cannot set on an immutable record.');
+ this.set(name, value);
+ }
+ });
+ }
+
+ createClass(Set, SetCollection);
+
+ // @pragma Construction
+
+ function Set(value) {
+ return value === null || value === undefined ? emptySet() :
+ isSet(value) && !isOrdered(value) ? value :
+ emptySet().withMutations(function(set ) {
+ var iter = SetIterable(value);
+ assertNotInfinite(iter.size);
+ iter.forEach(function(v ) {return set.add(v)});
+ });
+ }
+
+ Set.of = function(/*...values*/) {
+ return this(arguments);
+ };
+
+ Set.fromKeys = function(value) {
+ return this(KeyedIterable(value).keySeq());
+ };
+
+ Set.prototype.toString = function() {
+ return this.__toString('Set {', '}');
+ };
+
+ // @pragma Access
+
+ Set.prototype.has = function(value) {
+ return this._map.has(value);
+ };
+
+ // @pragma Modification
+
+ Set.prototype.add = function(value) {
+ return updateSet(this, this._map.set(value, true));
+ };
+
+ Set.prototype.remove = function(value) {
+ return updateSet(this, this._map.remove(value));
+ };
+
+ Set.prototype.clear = function() {
+ return updateSet(this, this._map.clear());
+ };
+
+ // @pragma Composition
+
+ Set.prototype.union = function() {var iters = SLICE$0.call(arguments, 0);
+ iters = iters.filter(function(x ) {return x.size !== 0});
+ if (iters.length === 0) {
+ return this;
+ }
+ if (this.size === 0 && !this.__ownerID && iters.length === 1) {
+ return this.constructor(iters[0]);
+ }
+ return this.withMutations(function(set ) {
+ for (var ii = 0; ii < iters.length; ii++) {
+ SetIterable(iters[ii]).forEach(function(value ) {return set.add(value)});
+ }
+ });
+ };
+
+ Set.prototype.intersect = function() {var iters = SLICE$0.call(arguments, 0);
+ if (iters.length === 0) {
+ return this;
+ }
+ iters = iters.map(function(iter ) {return SetIterable(iter)});
+ var originalSet = this;
+ return this.withMutations(function(set ) {
+ originalSet.forEach(function(value ) {
+ if (!iters.every(function(iter ) {return iter.includes(value)})) {
+ set.remove(value);
+ }
+ });
+ });
+ };
+
+ Set.prototype.subtract = function() {var iters = SLICE$0.call(arguments, 0);
+ if (iters.length === 0) {
+ return this;
+ }
+ iters = iters.map(function(iter ) {return SetIterable(iter)});
+ var originalSet = this;
+ return this.withMutations(function(set ) {
+ originalSet.forEach(function(value ) {
+ if (iters.some(function(iter ) {return iter.includes(value)})) {
+ set.remove(value);
+ }
+ });
+ });
+ };
+
+ Set.prototype.merge = function() {
+ return this.union.apply(this, arguments);
+ };
+
+ Set.prototype.mergeWith = function(merger) {var iters = SLICE$0.call(arguments, 1);
+ return this.union.apply(this, iters);
+ };
+
+ Set.prototype.sort = function(comparator) {
+ // Late binding
+ return OrderedSet(sortFactory(this, comparator));
+ };
+
+ Set.prototype.sortBy = function(mapper, comparator) {
+ // Late binding
+ return OrderedSet(sortFactory(this, comparator, mapper));
+ };
+
+ Set.prototype.wasAltered = function() {
+ return this._map.wasAltered();
+ };
+
+ Set.prototype.__iterate = function(fn, reverse) {var this$0 = this;
+ return this._map.__iterate(function(_, k) {return fn(k, k, this$0)}, reverse);
+ };
+
+ Set.prototype.__iterator = function(type, reverse) {
+ return this._map.map(function(_, k) {return k}).__iterator(type, reverse);
+ };
+
+ Set.prototype.__ensureOwner = function(ownerID) {
+ if (ownerID === this.__ownerID) {
+ return this;
+ }
+ var newMap = this._map.__ensureOwner(ownerID);
+ if (!ownerID) {
+ this.__ownerID = ownerID;
+ this._map = newMap;
+ return this;
+ }
+ return this.__make(newMap, ownerID);
+ };
+
+
+ function isSet(maybeSet) {
+ return !!(maybeSet && maybeSet[IS_SET_SENTINEL]);
+ }
+
+ Set.isSet = isSet;
+
+ var IS_SET_SENTINEL = '@@__IMMUTABLE_SET__@@';
+
+ var SetPrototype = Set.prototype;
+ SetPrototype[IS_SET_SENTINEL] = true;
+ SetPrototype[DELETE] = SetPrototype.remove;
+ SetPrototype.mergeDeep = SetPrototype.merge;
+ SetPrototype.mergeDeepWith = SetPrototype.mergeWith;
+ SetPrototype.withMutations = MapPrototype.withMutations;
+ SetPrototype.asMutable = MapPrototype.asMutable;
+ SetPrototype.asImmutable = MapPrototype.asImmutable;
+
+ SetPrototype.__empty = emptySet;
+ SetPrototype.__make = makeSet;
+
+ function updateSet(set, newMap) {
+ if (set.__ownerID) {
+ set.size = newMap.size;
+ set._map = newMap;
+ return set;
+ }
+ return newMap === set._map ? set :
+ newMap.size === 0 ? set.__empty() :
+ set.__make(newMap);
+ }
+
+ function makeSet(map, ownerID) {
+ var set = Object.create(SetPrototype);
+ set.size = map ? map.size : 0;
+ set._map = map;
+ set.__ownerID = ownerID;
+ return set;
+ }
+
+ var EMPTY_SET;
+ function emptySet() {
+ return EMPTY_SET || (EMPTY_SET = makeSet(emptyMap()));
+ }
+
+ createClass(OrderedSet, Set);
+
+ // @pragma Construction
+
+ function OrderedSet(value) {
+ return value === null || value === undefined ? emptyOrderedSet() :
+ isOrderedSet(value) ? value :
+ emptyOrderedSet().withMutations(function(set ) {
+ var iter = SetIterable(value);
+ assertNotInfinite(iter.size);
+ iter.forEach(function(v ) {return set.add(v)});
+ });
+ }
+
+ OrderedSet.of = function(/*...values*/) {
+ return this(arguments);
+ };
+
+ OrderedSet.fromKeys = function(value) {
+ return this(KeyedIterable(value).keySeq());
+ };
+
+ OrderedSet.prototype.toString = function() {
+ return this.__toString('OrderedSet {', '}');
+ };
+
+
+ function isOrderedSet(maybeOrderedSet) {
+ return isSet(maybeOrderedSet) && isOrdered(maybeOrderedSet);
+ }
+
+ OrderedSet.isOrderedSet = isOrderedSet;
+
+ var OrderedSetPrototype = OrderedSet.prototype;
+ OrderedSetPrototype[IS_ORDERED_SENTINEL] = true;
+
+ OrderedSetPrototype.__empty = emptyOrderedSet;
+ OrderedSetPrototype.__make = makeOrderedSet;
+
+ function makeOrderedSet(map, ownerID) {
+ var set = Object.create(OrderedSetPrototype);
+ set.size = map ? map.size : 0;
+ set._map = map;
+ set.__ownerID = ownerID;
+ return set;
+ }
+
+ var EMPTY_ORDERED_SET;
+ function emptyOrderedSet() {
+ return EMPTY_ORDERED_SET || (EMPTY_ORDERED_SET = makeOrderedSet(emptyOrderedMap()));
+ }
+
+ createClass(Stack, IndexedCollection);
+
+ // @pragma Construction
+
+ function Stack(value) {
+ return value === null || value === undefined ? emptyStack() :
+ isStack(value) ? value :
+ emptyStack().unshiftAll(value);
+ }
+
+ Stack.of = function(/*...values*/) {
+ return this(arguments);
+ };
+
+ Stack.prototype.toString = function() {
+ return this.__toString('Stack [', ']');
+ };
+
+ // @pragma Access
+
+ Stack.prototype.get = function(index, notSetValue) {
+ var head = this._head;
+ index = wrapIndex(this, index);
+ while (head && index--) {
+ head = head.next;
+ }
+ return head ? head.value : notSetValue;
+ };
+
+ Stack.prototype.peek = function() {
+ return this._head && this._head.value;
+ };
+
+ // @pragma Modification
+
+ Stack.prototype.push = function(/*...values*/) {
+ if (arguments.length === 0) {
+ return this;
+ }
+ var newSize = this.size + arguments.length;
+ var head = this._head;
+ for (var ii = arguments.length - 1; ii >= 0; ii--) {
+ head = {
+ value: arguments[ii],
+ next: head
+ };
+ }
+ if (this.__ownerID) {
+ this.size = newSize;
+ this._head = head;
+ this.__hash = undefined;
+ this.__altered = true;
+ return this;
+ }
+ return makeStack(newSize, head);
+ };
+
+ Stack.prototype.pushAll = function(iter) {
+ iter = IndexedIterable(iter);
+ if (iter.size === 0) {
+ return this;
+ }
+ assertNotInfinite(iter.size);
+ var newSize = this.size;
+ var head = this._head;
+ iter.reverse().forEach(function(value ) {
+ newSize++;
+ head = {
+ value: value,
+ next: head
+ };
+ });
+ if (this.__ownerID) {
+ this.size = newSize;
+ this._head = head;
+ this.__hash = undefined;
+ this.__altered = true;
+ return this;
+ }
+ return makeStack(newSize, head);
+ };
+
+ Stack.prototype.pop = function() {
+ return this.slice(1);
+ };
+
+ Stack.prototype.unshift = function(/*...values*/) {
+ return this.push.apply(this, arguments);
+ };
+
+ Stack.prototype.unshiftAll = function(iter) {
+ return this.pushAll(iter);
+ };
+
+ Stack.prototype.shift = function() {
+ return this.pop.apply(this, arguments);
+ };
+
+ Stack.prototype.clear = function() {
+ if (this.size === 0) {
+ return this;
+ }
+ if (this.__ownerID) {
+ this.size = 0;
+ this._head = undefined;
+ this.__hash = undefined;
+ this.__altered = true;
+ return this;
+ }
+ return emptyStack();
+ };
+
+ Stack.prototype.slice = function(begin, end) {
+ if (wholeSlice(begin, end, this.size)) {
+ return this;
+ }
+ var resolvedBegin = resolveBegin(begin, this.size);
+ var resolvedEnd = resolveEnd(end, this.size);
+ if (resolvedEnd !== this.size) {
+ // super.slice(begin, end);
+ return IndexedCollection.prototype.slice.call(this, begin, end);
+ }
+ var newSize = this.size - resolvedBegin;
+ var head = this._head;
+ while (resolvedBegin--) {
+ head = head.next;
+ }
+ if (this.__ownerID) {
+ this.size = newSize;
+ this._head = head;
+ this.__hash = undefined;
+ this.__altered = true;
+ return this;
+ }
+ return makeStack(newSize, head);
+ };
+
+ // @pragma Mutability
+
+ Stack.prototype.__ensureOwner = function(ownerID) {
+ if (ownerID === this.__ownerID) {
+ return this;
+ }
+ if (!ownerID) {
+ this.__ownerID = ownerID;
+ this.__altered = false;
+ return this;
+ }
+ return makeStack(this.size, this._head, ownerID, this.__hash);
+ };
+
+ // @pragma Iteration
+
+ Stack.prototype.__iterate = function(fn, reverse) {
+ if (reverse) {
+ return this.reverse().__iterate(fn);
+ }
+ var iterations = 0;
+ var node = this._head;
+ while (node) {
+ if (fn(node.value, iterations++, this) === false) {
+ break;
+ }
+ node = node.next;
+ }
+ return iterations;
+ };
+
+ Stack.prototype.__iterator = function(type, reverse) {
+ if (reverse) {
+ return this.reverse().__iterator(type);
+ }
+ var iterations = 0;
+ var node = this._head;
+ return new Iterator(function() {
+ if (node) {
+ var value = node.value;
+ node = node.next;
+ return iteratorValue(type, iterations++, value);
+ }
+ return iteratorDone();
+ });
+ };
+
+
+ function isStack(maybeStack) {
+ return !!(maybeStack && maybeStack[IS_STACK_SENTINEL]);
+ }
+
+ Stack.isStack = isStack;
+
+ var IS_STACK_SENTINEL = '@@__IMMUTABLE_STACK__@@';
+
+ var StackPrototype = Stack.prototype;
+ StackPrototype[IS_STACK_SENTINEL] = true;
+ StackPrototype.withMutations = MapPrototype.withMutations;
+ StackPrototype.asMutable = MapPrototype.asMutable;
+ StackPrototype.asImmutable = MapPrototype.asImmutable;
+ StackPrototype.wasAltered = MapPrototype.wasAltered;
+
+
+ function makeStack(size, head, ownerID, hash) {
+ var map = Object.create(StackPrototype);
+ map.size = size;
+ map._head = head;
+ map.__ownerID = ownerID;
+ map.__hash = hash;
+ map.__altered = false;
+ return map;
+ }
+
+ var EMPTY_STACK;
+ function emptyStack() {
+ return EMPTY_STACK || (EMPTY_STACK = makeStack(0));
+ }
+
+ /**
+ * Contributes additional methods to a constructor
+ */
+ function mixin(ctor, methods) {
+ var keyCopier = function(key ) { ctor.prototype[key] = methods[key]; };
+ Object.keys(methods).forEach(keyCopier);
+ Object.getOwnPropertySymbols &&
+ Object.getOwnPropertySymbols(methods).forEach(keyCopier);
+ return ctor;
+ }
+
+ Iterable.Iterator = Iterator;
+
+ mixin(Iterable, {
+
+ // ### Conversion to other types
+
+ toArray: function() {
+ assertNotInfinite(this.size);
+ var array = new Array(this.size || 0);
+ this.valueSeq().__iterate(function(v, i) { array[i] = v; });
+ return array;
+ },
+
+ toIndexedSeq: function() {
+ return new ToIndexedSequence(this);
+ },
+
+ toJS: function() {
+ return this.toSeq().map(
+ function(value ) {return value && typeof value.toJS === 'function' ? value.toJS() : value}
+ ).__toJS();
+ },
+
+ toJSON: function() {
+ return this.toSeq().map(
+ function(value ) {return value && typeof value.toJSON === 'function' ? value.toJSON() : value}
+ ).__toJS();
+ },
+
+ toKeyedSeq: function() {
+ return new ToKeyedSequence(this, true);
+ },
+
+ toMap: function() {
+ // Use Late Binding here to solve the circular dependency.
+ return Map(this.toKeyedSeq());
+ },
+
+ toObject: function() {
+ assertNotInfinite(this.size);
+ var object = {};
+ this.__iterate(function(v, k) { object[k] = v; });
+ return object;
+ },
+
+ toOrderedMap: function() {
+ // Use Late Binding here to solve the circular dependency.
+ return OrderedMap(this.toKeyedSeq());
+ },
+
+ toOrderedSet: function() {
+ // Use Late Binding here to solve the circular dependency.
+ return OrderedSet(isKeyed(this) ? this.valueSeq() : this);
+ },
+
+ toSet: function() {
+ // Use Late Binding here to solve the circular dependency.
+ return Set(isKeyed(this) ? this.valueSeq() : this);
+ },
+
+ toSetSeq: function() {
+ return new ToSetSequence(this);
+ },
+
+ toSeq: function() {
+ return isIndexed(this) ? this.toIndexedSeq() :
+ isKeyed(this) ? this.toKeyedSeq() :
+ this.toSetSeq();
+ },
+
+ toStack: function() {
+ // Use Late Binding here to solve the circular dependency.
+ return Stack(isKeyed(this) ? this.valueSeq() : this);
+ },
+
+ toList: function() {
+ // Use Late Binding here to solve the circular dependency.
+ return List(isKeyed(this) ? this.valueSeq() : this);
+ },
+
+
+ // ### Common JavaScript methods and properties
+
+ toString: function() {
+ return '[Iterable]';
+ },
+
+ __toString: function(head, tail) {
+ if (this.size === 0) {
+ return head + tail;
+ }
+ return head + ' ' + this.toSeq().map(this.__toStringMapper).join(', ') + ' ' + tail;
+ },
+
+
+ // ### ES6 Collection methods (ES6 Array and Map)
+
+ concat: function() {var values = SLICE$0.call(arguments, 0);
+ return reify(this, concatFactory(this, values));
+ },
+
+ includes: function(searchValue) {
+ return this.some(function(value ) {return is(value, searchValue)});
+ },
+
+ entries: function() {
+ return this.__iterator(ITERATE_ENTRIES);
+ },
+
+ every: function(predicate, context) {
+ assertNotInfinite(this.size);
+ var returnValue = true;
+ this.__iterate(function(v, k, c) {
+ if (!predicate.call(context, v, k, c)) {
+ returnValue = false;
+ return false;
+ }
+ });
+ return returnValue;
+ },
+
+ filter: function(predicate, context) {
+ return reify(this, filterFactory(this, predicate, context, true));
+ },
+
+ find: function(predicate, context, notSetValue) {
+ var entry = this.findEntry(predicate, context);
+ return entry ? entry[1] : notSetValue;
+ },
+
+ forEach: function(sideEffect, context) {
+ assertNotInfinite(this.size);
+ return this.__iterate(context ? sideEffect.bind(context) : sideEffect);
+ },
+
+ join: function(separator) {
+ assertNotInfinite(this.size);
+ separator = separator !== undefined ? '' + separator : ',';
+ var joined = '';
+ var isFirst = true;
+ this.__iterate(function(v ) {
+ isFirst ? (isFirst = false) : (joined += separator);
+ joined += v !== null && v !== undefined ? v.toString() : '';
+ });
+ return joined;
+ },
+
+ keys: function() {
+ return this.__iterator(ITERATE_KEYS);
+ },
+
+ map: function(mapper, context) {
+ return reify(this, mapFactory(this, mapper, context));
+ },
+
+ reduce: function(reducer, initialReduction, context) {
+ assertNotInfinite(this.size);
+ var reduction;
+ var useFirst;
+ if (arguments.length < 2) {
+ useFirst = true;
+ } else {
+ reduction = initialReduction;
+ }
+ this.__iterate(function(v, k, c) {
+ if (useFirst) {
+ useFirst = false;
+ reduction = v;
+ } else {
+ reduction = reducer.call(context, reduction, v, k, c);
+ }
+ });
+ return reduction;
+ },
+
+ reduceRight: function(reducer, initialReduction, context) {
+ var reversed = this.toKeyedSeq().reverse();
+ return reversed.reduce.apply(reversed, arguments);
+ },
+
+ reverse: function() {
+ return reify(this, reverseFactory(this, true));
+ },
+
+ slice: function(begin, end) {
+ return reify(this, sliceFactory(this, begin, end, true));
+ },
+
+ some: function(predicate, context) {
+ return !this.every(not(predicate), context);
+ },
+
+ sort: function(comparator) {
+ return reify(this, sortFactory(this, comparator));
+ },
+
+ values: function() {
+ return this.__iterator(ITERATE_VALUES);
+ },
+
+
+ // ### More sequential methods
+
+ butLast: function() {
+ return this.slice(0, -1);
+ },
+
+ isEmpty: function() {
+ return this.size !== undefined ? this.size === 0 : !this.some(function() {return true});
+ },
+
+ count: function(predicate, context) {
+ return ensureSize(
+ predicate ? this.toSeq().filter(predicate, context) : this
+ );
+ },
+
+ countBy: function(grouper, context) {
+ return countByFactory(this, grouper, context);
+ },
+
+ equals: function(other) {
+ return deepEqual(this, other);
+ },
+
+ entrySeq: function() {
+ var iterable = this;
+ if (iterable._cache) {
+ // We cache as an entries array, so we can just return the cache!
+ return new ArraySeq(iterable._cache);
+ }
+ var entriesSequence = iterable.toSeq().map(entryMapper).toIndexedSeq();
+ entriesSequence.fromEntrySeq = function() {return iterable.toSeq()};
+ return entriesSequence;
+ },
+
+ filterNot: function(predicate, context) {
+ return this.filter(not(predicate), context);
+ },
+
+ findEntry: function(predicate, context, notSetValue) {
+ var found = notSetValue;
+ this.__iterate(function(v, k, c) {
+ if (predicate.call(context, v, k, c)) {
+ found = [k, v];
+ return false;
+ }
+ });
+ return found;
+ },
+
+ findKey: function(predicate, context) {
+ var entry = this.findEntry(predicate, context);
+ return entry && entry[0];
+ },
+
+ findLast: function(predicate, context, notSetValue) {
+ return this.toKeyedSeq().reverse().find(predicate, context, notSetValue);
+ },
+
+ findLastEntry: function(predicate, context, notSetValue) {
+ return this.toKeyedSeq().reverse().findEntry(predicate, context, notSetValue);
+ },
+
+ findLastKey: function(predicate, context) {
+ return this.toKeyedSeq().reverse().findKey(predicate, context);
+ },
+
+ first: function() {
+ return this.find(returnTrue);
+ },
+
+ flatMap: function(mapper, context) {
+ return reify(this, flatMapFactory(this, mapper, context));
+ },
+
+ flatten: function(depth) {
+ return reify(this, flattenFactory(this, depth, true));
+ },
+
+ fromEntrySeq: function() {
+ return new FromEntriesSequence(this);
+ },
+
+ get: function(searchKey, notSetValue) {
+ return this.find(function(_, key) {return is(key, searchKey)}, undefined, notSetValue);
+ },
+
+ getIn: function(searchKeyPath, notSetValue) {
+ var nested = this;
+ // Note: in an ES6 environment, we would prefer:
+ // for (var key of searchKeyPath) {
+ var iter = forceIterator(searchKeyPath);
+ var step;
+ while (!(step = iter.next()).done) {
+ var key = step.value;
+ nested = nested && nested.get ? nested.get(key, NOT_SET) : NOT_SET;
+ if (nested === NOT_SET) {
+ return notSetValue;
+ }
+ }
+ return nested;
+ },
+
+ groupBy: function(grouper, context) {
+ return groupByFactory(this, grouper, context);
+ },
+
+ has: function(searchKey) {
+ return this.get(searchKey, NOT_SET) !== NOT_SET;
+ },
+
+ hasIn: function(searchKeyPath) {
+ return this.getIn(searchKeyPath, NOT_SET) !== NOT_SET;
+ },
+
+ isSubset: function(iter) {
+ iter = typeof iter.includes === 'function' ? iter : Iterable(iter);
+ return this.every(function(value ) {return iter.includes(value)});
+ },
+
+ isSuperset: function(iter) {
+ iter = typeof iter.isSubset === 'function' ? iter : Iterable(iter);
+ return iter.isSubset(this);
+ },
+
+ keyOf: function(searchValue) {
+ return this.findKey(function(value ) {return is(value, searchValue)});
+ },
+
+ keySeq: function() {
+ return this.toSeq().map(keyMapper).toIndexedSeq();
+ },
+
+ last: function() {
+ return this.toSeq().reverse().first();
+ },
+
+ lastKeyOf: function(searchValue) {
+ return this.toKeyedSeq().reverse().keyOf(searchValue);
+ },
+
+ max: function(comparator) {
+ return maxFactory(this, comparator);
+ },
+
+ maxBy: function(mapper, comparator) {
+ return maxFactory(this, comparator, mapper);
+ },
+
+ min: function(comparator) {
+ return maxFactory(this, comparator ? neg(comparator) : defaultNegComparator);
+ },
+
+ minBy: function(mapper, comparator) {
+ return maxFactory(this, comparator ? neg(comparator) : defaultNegComparator, mapper);
+ },
+
+ rest: function() {
+ return this.slice(1);
+ },
+
+ skip: function(amount) {
+ return this.slice(Math.max(0, amount));
+ },
+
+ skipLast: function(amount) {
+ return reify(this, this.toSeq().reverse().skip(amount).reverse());
+ },
+
+ skipWhile: function(predicate, context) {
+ return reify(this, skipWhileFactory(this, predicate, context, true));
+ },
+
+ skipUntil: function(predicate, context) {
+ return this.skipWhile(not(predicate), context);
+ },
+
+ sortBy: function(mapper, comparator) {
+ return reify(this, sortFactory(this, comparator, mapper));
+ },
+
+ take: function(amount) {
+ return this.slice(0, Math.max(0, amount));
+ },
+
+ takeLast: function(amount) {
+ return reify(this, this.toSeq().reverse().take(amount).reverse());
+ },
+
+ takeWhile: function(predicate, context) {
+ return reify(this, takeWhileFactory(this, predicate, context));
+ },
+
+ takeUntil: function(predicate, context) {
+ return this.takeWhile(not(predicate), context);
+ },
+
+ valueSeq: function() {
+ return this.toIndexedSeq();
+ },
+
+
+ // ### Hashable Object
+
+ hashCode: function() {
+ return this.__hash || (this.__hash = hashIterable(this));
+ }
+
+
+ // ### Internal
+
+ // abstract __iterate(fn, reverse)
+
+ // abstract __iterator(type, reverse)
+ });
+
+ // var IS_ITERABLE_SENTINEL = '@@__IMMUTABLE_ITERABLE__@@';
+ // var IS_KEYED_SENTINEL = '@@__IMMUTABLE_KEYED__@@';
+ // var IS_INDEXED_SENTINEL = '@@__IMMUTABLE_INDEXED__@@';
+ // var IS_ORDERED_SENTINEL = '@@__IMMUTABLE_ORDERED__@@';
+
+ var IterablePrototype = Iterable.prototype;
+ IterablePrototype[IS_ITERABLE_SENTINEL] = true;
+ IterablePrototype[ITERATOR_SYMBOL] = IterablePrototype.values;
+ IterablePrototype.__toJS = IterablePrototype.toArray;
+ IterablePrototype.__toStringMapper = quoteString;
+ IterablePrototype.inspect =
+ IterablePrototype.toSource = function() { return this.toString(); };
+ IterablePrototype.chain = IterablePrototype.flatMap;
+ IterablePrototype.contains = IterablePrototype.includes;
+
+ mixin(KeyedIterable, {
+
+ // ### More sequential methods
+
+ flip: function() {
+ return reify(this, flipFactory(this));
+ },
+
+ mapEntries: function(mapper, context) {var this$0 = this;
+ var iterations = 0;
+ return reify(this,
+ this.toSeq().map(
+ function(v, k) {return mapper.call(context, [k, v], iterations++, this$0)}
+ ).fromEntrySeq()
+ );
+ },
+
+ mapKeys: function(mapper, context) {var this$0 = this;
+ return reify(this,
+ this.toSeq().flip().map(
+ function(k, v) {return mapper.call(context, k, v, this$0)}
+ ).flip()
+ );
+ }
+
+ });
+
+ var KeyedIterablePrototype = KeyedIterable.prototype;
+ KeyedIterablePrototype[IS_KEYED_SENTINEL] = true;
+ KeyedIterablePrototype[ITERATOR_SYMBOL] = IterablePrototype.entries;
+ KeyedIterablePrototype.__toJS = IterablePrototype.toObject;
+ KeyedIterablePrototype.__toStringMapper = function(v, k) {return JSON.stringify(k) + ': ' + quoteString(v)};
+
+
+
+ mixin(IndexedIterable, {
+
+ // ### Conversion to other types
+
+ toKeyedSeq: function() {
+ return new ToKeyedSequence(this, false);
+ },
+
+
+ // ### ES6 Collection methods (ES6 Array and Map)
+
+ filter: function(predicate, context) {
+ return reify(this, filterFactory(this, predicate, context, false));
+ },
+
+ findIndex: function(predicate, context) {
+ var entry = this.findEntry(predicate, context);
+ return entry ? entry[0] : -1;
+ },
+
+ indexOf: function(searchValue) {
+ var key = this.keyOf(searchValue);
+ return key === undefined ? -1 : key;
+ },
+
+ lastIndexOf: function(searchValue) {
+ var key = this.lastKeyOf(searchValue);
+ return key === undefined ? -1 : key;
+ },
+
+ reverse: function() {
+ return reify(this, reverseFactory(this, false));
+ },
+
+ slice: function(begin, end) {
+ return reify(this, sliceFactory(this, begin, end, false));
+ },
+
+ splice: function(index, removeNum /*, ...values*/) {
+ var numArgs = arguments.length;
+ removeNum = Math.max(removeNum | 0, 0);
+ if (numArgs === 0 || (numArgs === 2 && !removeNum)) {
+ return this;
+ }
+ // If index is negative, it should resolve relative to the size of the
+ // collection. However size may be expensive to compute if not cached, so
+ // only call count() if the number is in fact negative.
+ index = resolveBegin(index, index < 0 ? this.count() : this.size);
+ var spliced = this.slice(0, index);
+ return reify(
+ this,
+ numArgs === 1 ?
+ spliced :
+ spliced.concat(arrCopy(arguments, 2), this.slice(index + removeNum))
+ );
+ },
+
+
+ // ### More collection methods
+
+ findLastIndex: function(predicate, context) {
+ var entry = this.findLastEntry(predicate, context);
+ return entry ? entry[0] : -1;
+ },
+
+ first: function() {
+ return this.get(0);
+ },
+
+ flatten: function(depth) {
+ return reify(this, flattenFactory(this, depth, false));
+ },
+
+ get: function(index, notSetValue) {
+ index = wrapIndex(this, index);
+ return (index < 0 || (this.size === Infinity ||
+ (this.size !== undefined && index > this.size))) ?
+ notSetValue :
+ this.find(function(_, key) {return key === index}, undefined, notSetValue);
+ },
+
+ has: function(index) {
+ index = wrapIndex(this, index);
+ return index >= 0 && (this.size !== undefined ?
+ this.size === Infinity || index < this.size :
+ this.indexOf(index) !== -1
+ );
+ },
+
+ interpose: function(separator) {
+ return reify(this, interposeFactory(this, separator));
+ },
+
+ interleave: function(/*...iterables*/) {
+ var iterables = [this].concat(arrCopy(arguments));
+ var zipped = zipWithFactory(this.toSeq(), IndexedSeq.of, iterables);
+ var interleaved = zipped.flatten(true);
+ if (zipped.size) {
+ interleaved.size = zipped.size * iterables.length;
+ }
+ return reify(this, interleaved);
+ },
+
+ keySeq: function() {
+ return Range(0, this.size);
+ },
+
+ last: function() {
+ return this.get(-1);
+ },
+
+ skipWhile: function(predicate, context) {
+ return reify(this, skipWhileFactory(this, predicate, context, false));
+ },
+
+ zip: function(/*, ...iterables */) {
+ var iterables = [this].concat(arrCopy(arguments));
+ return reify(this, zipWithFactory(this, defaultZipper, iterables));
+ },
+
+ zipWith: function(zipper/*, ...iterables */) {
+ var iterables = arrCopy(arguments);
+ iterables[0] = this;
+ return reify(this, zipWithFactory(this, zipper, iterables));
+ }
+
+ });
+
+ IndexedIterable.prototype[IS_INDEXED_SENTINEL] = true;
+ IndexedIterable.prototype[IS_ORDERED_SENTINEL] = true;
+
+
+
+ mixin(SetIterable, {
+
+ // ### ES6 Collection methods (ES6 Array and Map)
+
+ get: function(value, notSetValue) {
+ return this.has(value) ? value : notSetValue;
+ },
+
+ includes: function(value) {
+ return this.has(value);
+ },
+
+
+ // ### More sequential methods
+
+ keySeq: function() {
+ return this.valueSeq();
+ }
+
+ });
+
+ SetIterable.prototype.has = IterablePrototype.includes;
+ SetIterable.prototype.contains = SetIterable.prototype.includes;
+
+
+ // Mixin subclasses
+
+ mixin(KeyedSeq, KeyedIterable.prototype);
+ mixin(IndexedSeq, IndexedIterable.prototype);
+ mixin(SetSeq, SetIterable.prototype);
+
+ mixin(KeyedCollection, KeyedIterable.prototype);
+ mixin(IndexedCollection, IndexedIterable.prototype);
+ mixin(SetCollection, SetIterable.prototype);
+
+
+ // #pragma Helper functions
+
+ function keyMapper(v, k) {
+ return k;
+ }
+
+ function entryMapper(v, k) {
+ return [k, v];
+ }
+
+ function not(predicate) {
+ return function() {
+ return !predicate.apply(this, arguments);
+ }
+ }
+
+ function neg(predicate) {
+ return function() {
+ return -predicate.apply(this, arguments);
+ }
+ }
+
+ function quoteString(value) {
+ return typeof value === 'string' ? JSON.stringify(value) : String(value);
+ }
+
+ function defaultZipper() {
+ return arrCopy(arguments);
+ }
+
+ function defaultNegComparator(a, b) {
+ return a < b ? 1 : a > b ? -1 : 0;
+ }
+
+ function hashIterable(iterable) {
+ if (iterable.size === Infinity) {
+ return 0;
+ }
+ var ordered = isOrdered(iterable);
+ var keyed = isKeyed(iterable);
+ var h = ordered ? 1 : 0;
+ var size = iterable.__iterate(
+ keyed ?
+ ordered ?
+ function(v, k) { h = 31 * h + hashMerge(hash(v), hash(k)) | 0; } :
+ function(v, k) { h = h + hashMerge(hash(v), hash(k)) | 0; } :
+ ordered ?
+ function(v ) { h = 31 * h + hash(v) | 0; } :
+ function(v ) { h = h + hash(v) | 0; }
+ );
+ return murmurHashOfSize(size, h);
+ }
+
+ function murmurHashOfSize(size, h) {
+ h = imul(h, 0xCC9E2D51);
+ h = imul(h << 15 | h >>> -15, 0x1B873593);
+ h = imul(h << 13 | h >>> -13, 5);
+ h = (h + 0xE6546B64 | 0) ^ size;
+ h = imul(h ^ h >>> 16, 0x85EBCA6B);
+ h = imul(h ^ h >>> 13, 0xC2B2AE35);
+ h = smi(h ^ h >>> 16);
+ return h;
+ }
+
+ function hashMerge(a, b) {
+ return a ^ b + 0x9E3779B9 + (a << 6) + (a >> 2) | 0; // int
+ }
+
+ var Immutable = {
+
+ Iterable: Iterable,
+
+ Seq: Seq,
+ Collection: Collection,
+ Map: Map,
+ OrderedMap: OrderedMap,
+ List: List,
+ Stack: Stack,
+ Set: Set,
+ OrderedSet: OrderedSet,
+
+ Record: Record,
+ Range: Range,
+ Repeat: Repeat,
+
+ is: is,
+ fromJS: fromJS
+
+ };
+
+ return Immutable;
+
+ }));
+
+/***/ },
+/* 230 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var Immutable = __webpack_require__(229);
+
+ // When our app state is fully types, we should be able to get rid of
+ // this function. This is only temporarily necessary to support
+ // converting typed objects to immutable.js, which usually happens in
+ // reducers.
+ function fromJS(value) {
+ if (Array.isArray(value)) {
+ return Immutable.Seq(value).map(fromJS).toList();
+ }
+ if (value && value.constructor.meta) {
+ // This adds support for tcomb objects which are native JS objects
+ // but are not "plain", so the above checks fail. Since they
+ // behave the same we can use the same constructors, but we need
+ // special checks for them.
+ var kind = value.constructor.meta.kind;
+ if (kind === "struct") {
+ return Immutable.Seq(value).map(fromJS).toMap();
+ } else if (kind === "list") {
+ return Immutable.Seq(value).map(fromJS).toList();
+ }
+ }
+
+ // If it's a primitive type, just return the value. Note `==` check
+ // for null, which is intentionally used to match either `null` or
+ // `undefined`.
+ if (value == null || typeof value !== "object") {
+ return value;
+ }
+
+ // Otherwise, treat it like an object. We can't reliably detect if
+ // it's a plain object because we might be objects from other JS
+ // contexts so `Object !== Object`.
+ return Immutable.Seq(value).map(fromJS).toMap();
+ }
+
+ module.exports = fromJS;
+
+/***/ },
+/* 231 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var React = __webpack_require__(2);
+
+ var _require = __webpack_require__(19);
+
+ var connect = _require.connect;
+
+ var classnames = __webpack_require__(211);
+
+ var _require2 = __webpack_require__(232);
+
+ var getTabs = _require2.getTabs;
+
+
+ __webpack_require__(233);
+ var dom = React.DOM;
+
+ var ImPropTypes = __webpack_require__(235);
+
+ var githubUrl = "https://github.com/devtools-html/debugger.html/blob/master";
+
+ function getTabsByBrowser(tabs, browser) {
+ return tabs.valueSeq().filter(tab => tab.get("browser") == browser);
+ }
+
+ function firstTimeMessage(title, urlPart) {
+ return dom.div({ className: "footer-note" }, `First time connecting to ${ title }? Checkout out the `, dom.a({ href: `${ githubUrl }/CONTRIBUTING.md#${ urlPart }` }, "docs"), ".");
+ }
+
+ var LandingPage = React.createClass({
+ propTypes: {
+ tabs: ImPropTypes.map.isRequired
+ },
+
+ displayName: "LandingPage",
+
+ getInitialState() {
+ return {
+ selectedPane: "Firefox"
+ };
+ },
+
+ renderTabs(tabTitle, tabs, paramName) {
+ if (!tabs || tabs.count() == 0) {
+ return dom.div({}, "");
+ }
+
+ return dom.div({ className: "tab-group" }, dom.ul({ className: "tab-list" }, tabs.valueSeq().map(tab => dom.li({ "className": "tab",
+ "key": tab.get("id"),
+ "onClick": () => {
+ window.location = "/?" + paramName + "=" + tab.get("id");
+ } }, dom.div({ className: "tab-title" }, tab.get("title")), dom.div({ className: "tab-url" }, tab.get("url"))))));
+ },
+
+ renderFirefoxPanel() {
+ var targets = getTabsByBrowser(this.props.tabs, "firefox");
+ return dom.div({ className: "center" }, this.renderTabs("", targets, "firefox-tab"), firstTimeMessage("Firefox", "firefox"));
+ },
+
+ renderChromePanel() {
+ var targets = getTabsByBrowser(this.props.tabs, "chrome");
+ return dom.div({ className: "center" }, this.renderTabs("", targets, "chrome-tab"), firstTimeMessage("Chrome", "chrome"));
+ },
+
+ renderNodePanel() {
+ return dom.div({ className: "center" }, dom.div({ className: "center-message" }, dom.a({
+ href: `/?ws=${ document.location.hostname }:9229/node`
+ }, "Connect to Node")), firstTimeMessage("Node", "nodejs"));
+ },
+
+ renderPanel() {
+ var panels = {
+ Firefox: this.renderFirefoxPanel,
+ Chrome: this.renderChromePanel,
+ Node: this.renderNodePanel
+ };
+
+ return dom.div({
+ className: "panel"
+ }, dom.div({ className: "title" }, dom.h2({}, this.state.selectedPane)), panels[this.state.selectedPane]());
+ },
+
+ renderSidebar() {
+ return dom.div({
+ className: "sidebar"
+ }, dom.h1({}, "Debugger"), dom.ul({}, ["Firefox", "Chrome", "Node"].map(title => dom.li({
+ className: classnames({
+ selected: title == this.state.selectedPane
+ }),
+ key: title,
+
+ onClick: () => this.setState({ selectedPane: title })
+ }, dom.a({}, title)))));
+ },
+
+ render() {
+ return dom.div({
+ className: "landing-page"
+ }, this.renderSidebar(), this.renderPanel());
+ }
+ });
+
+ module.exports = connect(state => ({ tabs: getTabs(state) }))(LandingPage);
+
+/***/ },
+/* 232 */
+/***/ function(module, exports) {
+
+ function getTabs(state) {
+ return state.tabs.get("tabs");
+ }
+
+ function getSelectedTab(state) {
+ return state.tabs.get("selectedTab");
+ }
+
+ module.exports = {
+ getTabs,
+ getSelectedTab
+ };
+
+/***/ },
+/* 233 */
+/***/ function(module, exports) {
+
+ // removed by extract-text-webpack-plugin
+
+/***/ },
+/* 234 */,
+/* 235 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /**
+ * This is a straight rip-off of the React.js ReactPropTypes.js proptype validators,
+ * modified to make it possible to validate Immutable.js data.
+ * ImmutableTypes.listOf is patterned after React.PropTypes.arrayOf, but for Immutable.List
+ * ImmutableTypes.shape is based on React.PropTypes.shape, but for any Immutable.Iterable
+ */
+ "use strict";
+
+ var Immutable = __webpack_require__(229);
+
+ var ANONYMOUS = "<<anonymous>>";
+
+ var ImmutablePropTypes = {
+ listOf: createListOfTypeChecker,
+ mapOf: createMapOfTypeChecker,
+ orderedMapOf: createOrderedMapOfTypeChecker,
+ setOf: createSetOfTypeChecker,
+ orderedSetOf: createOrderedSetOfTypeChecker,
+ stackOf: createStackOfTypeChecker,
+ iterableOf: createIterableOfTypeChecker,
+ recordOf: createRecordOfTypeChecker,
+ shape: createShapeChecker,
+ contains: createShapeChecker,
+ mapContains: createMapContainsChecker,
+ // Primitive Types
+ list: createImmutableTypeChecker("List", Immutable.List.isList),
+ map: createImmutableTypeChecker("Map", Immutable.Map.isMap),
+ orderedMap: createImmutableTypeChecker("OrderedMap", Immutable.OrderedMap.isOrderedMap),
+ set: createImmutableTypeChecker("Set", Immutable.Set.isSet),
+ orderedSet: createImmutableTypeChecker("OrderedSet", Immutable.OrderedSet.isOrderedSet),
+ stack: createImmutableTypeChecker("Stack", Immutable.Stack.isStack),
+ seq: createImmutableTypeChecker("Seq", Immutable.Seq.isSeq),
+ record: createImmutableTypeChecker("Record", function (isRecord) {
+ return isRecord instanceof Immutable.Record;
+ }),
+ iterable: createImmutableTypeChecker("Iterable", Immutable.Iterable.isIterable)
+ };
+
+ function getPropType(propValue) {
+ var propType = typeof propValue;
+ if (Array.isArray(propValue)) {
+ return "array";
+ }
+ if (propValue instanceof RegExp) {
+ // Old webkits (at least until Android 4.0) return 'function' rather than
+ // 'object' for typeof a RegExp. We'll normalize this here so that /bla/
+ // passes PropTypes.object.
+ return "object";
+ }
+ if (propValue instanceof Immutable.Iterable) {
+ return "Immutable." + propValue.toSource().split(" ")[0];
+ }
+ return propType;
+ }
+
+ function createChainableTypeChecker(validate) {
+ function checkType(isRequired, props, propName, componentName, location, propFullName) {
+ propFullName = propFullName || propName;
+ componentName = componentName || ANONYMOUS;
+ if (props[propName] == null) {
+ var locationName = location;
+ if (isRequired) {
+ return new Error("Required " + locationName + " `" + propFullName + "` was not specified in " + ("`" + componentName + "`."));
+ }
+ } else {
+ return validate(props, propName, componentName, location, propFullName);
+ }
+ }
+
+ var chainedCheckType = checkType.bind(null, false);
+ chainedCheckType.isRequired = checkType.bind(null, true);
+
+ return chainedCheckType;
+ }
+
+ function createImmutableTypeChecker(immutableClassName, immutableClassTypeValidator) {
+ function validate(props, propName, componentName, location, propFullName) {
+ var propValue = props[propName];
+ if (!immutableClassTypeValidator(propValue)) {
+ var propType = getPropType(propValue);
+ return new Error("Invalid " + location + " `" + propFullName + "` of type `" + propType + "` " + ("supplied to `" + componentName + "`, expected `" + immutableClassName + "`."));
+ }
+ return null;
+ }
+ return createChainableTypeChecker(validate);
+ }
+
+ function createIterableTypeChecker(typeChecker, immutableClassName, immutableClassTypeValidator) {
+
+ function validate(props, propName, componentName, location, propFullName) {
+ var propValue = props[propName];
+ if (!immutableClassTypeValidator(propValue)) {
+ var locationName = location;
+ var propType = getPropType(propValue);
+ return new Error("Invalid " + locationName + " `" + propFullName + "` of type " + ("`" + propType + "` supplied to `" + componentName + "`, expected an Immutable.js " + immutableClassName + "."));
+ }
+
+ if (typeof typeChecker !== "function") {
+ return new Error("Invalid typeChecker supplied to `" + componentName + "` " + ("for propType `" + propFullName + "`, expected a function."));
+ }
+
+ var propValues = propValue.toArray();
+ for (var i = 0, len = propValues.length; i < len; i++) {
+ var error = typeChecker(propValues, i, componentName, location, "" + propFullName + "[" + i + "]");
+ if (error instanceof Error) {
+ return error;
+ }
+ }
+ }
+ return createChainableTypeChecker(validate);
+ }
+
+ function createListOfTypeChecker(typeChecker) {
+ return createIterableTypeChecker(typeChecker, "List", Immutable.List.isList);
+ }
+
+ function createMapOfTypeChecker(typeChecker) {
+ return createIterableTypeChecker(typeChecker, "Map", Immutable.Map.isMap);
+ }
+
+ function createOrderedMapOfTypeChecker(typeChecker) {
+ return createIterableTypeChecker(typeChecker, "OrderedMap", Immutable.OrderedMap.isOrderedMap);
+ }
+
+ function createSetOfTypeChecker(typeChecker) {
+ return createIterableTypeChecker(typeChecker, "Set", Immutable.Set.isSet);
+ }
+
+ function createOrderedSetOfTypeChecker(typeChecker) {
+ return createIterableTypeChecker(typeChecker, "OrderedSet", Immutable.OrderedSet.isOrderedSet);
+ }
+
+ function createStackOfTypeChecker(typeChecker) {
+ return createIterableTypeChecker(typeChecker, "Stack", Immutable.Stack.isStack);
+ }
+
+ function createIterableOfTypeChecker(typeChecker) {
+ return createIterableTypeChecker(typeChecker, "Iterable", Immutable.Iterable.isIterable);
+ }
+
+ function createRecordOfTypeChecker(recordKeys) {
+ function validate(props, propName, componentName, location, propFullName) {
+ var propValue = props[propName];
+ if (!(propValue instanceof Immutable.Record)) {
+ var propType = getPropType(propValue);
+ var locationName = location;
+ return new Error("Invalid " + locationName + " `" + propFullName + "` of type `" + propType + "` " + ("supplied to `" + componentName + "`, expected an Immutable.js Record."));
+ }
+ for (var key in recordKeys) {
+ var checker = recordKeys[key];
+ if (!checker) {
+ continue;
+ }
+ var mutablePropValue = propValue.toObject();
+ var error = checker(mutablePropValue, key, componentName, location, "" + propFullName + "." + key);
+ if (error) {
+ return error;
+ }
+ }
+ }
+ return createChainableTypeChecker(validate);
+ }
+
+ // there is some irony in the fact that shapeTypes is a standard hash and not an immutable collection
+ function createShapeTypeChecker(shapeTypes) {
+ var immutableClassName = arguments[1] === undefined ? "Iterable" : arguments[1];
+ var immutableClassTypeValidator = arguments[2] === undefined ? Immutable.Iterable.isIterable : arguments[2];
+
+ function validate(props, propName, componentName, location, propFullName) {
+ var propValue = props[propName];
+ if (!immutableClassTypeValidator(propValue)) {
+ var propType = getPropType(propValue);
+ var locationName = location;
+ return new Error("Invalid " + locationName + " `" + propFullName + "` of type `" + propType + "` " + ("supplied to `" + componentName + "`, expected an Immutable.js " + immutableClassName + "."));
+ }
+ var mutablePropValue = propValue.toObject();
+ for (var key in shapeTypes) {
+ var checker = shapeTypes[key];
+ if (!checker) {
+ continue;
+ }
+ var error = checker(mutablePropValue, key, componentName, location, "" + propFullName + "." + key);
+ if (error) {
+ return error;
+ }
+ }
+ }
+ return createChainableTypeChecker(validate);
+ }
+
+ function createShapeChecker(shapeTypes) {
+ return createShapeTypeChecker(shapeTypes);
+ }
+
+ function createMapContainsChecker(shapeTypes) {
+ return createShapeTypeChecker(shapeTypes, "Map", Immutable.Map.isMap);
+ }
+
+ module.exports = ImmutablePropTypes;
+
+/***/ },
+/* 236 */
+/***/ function(module, exports, __webpack_require__) {
+
+
+ var tabs = __webpack_require__(237);
+
+ module.exports = Object.assign({}, tabs);
+
+/***/ },
+/* 237 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /* 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/. */
+ /* global window */
+
+ /**
+ * Redux actions for the pause state
+ * @module actions/tabs
+ */
+
+ var constants = __webpack_require__(228);
+
+ /**
+ * @typedef {Object} TabAction
+ * @memberof actions/tabs
+ * @static
+ * @property {number} type The type of Action
+ * @property {number} value The payload of the Action
+ */
+
+ /**
+ * @memberof actions/tabs
+ * @static
+ * @param {Array} tabs
+ * @returns {TabAction} with type constants.ADD_TABS and tabs as value
+ */
+ function newTabs(tabs) {
+ return {
+ type: constants.ADD_TABS,
+ value: tabs
+ };
+ }
+
+ /**
+ * @memberof actions/tabs
+ * @static
+ * @param {String} $0.id Unique ID of the tab to select
+ * @returns {TabAction}
+ */
+ function selectTab(_ref) {
+ var id = _ref.id;
+
+ return {
+ type: constants.SELECT_TAB,
+ id: id
+ };
+ }
+
+ module.exports = {
+ newTabs,
+ selectTab
+ };
+
+/***/ },
+/* 238 */
+/***/ function(module, exports, __webpack_require__) {
+
+
+
+ /* 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/. */
+ /* global window */
+
+ /**
+ * Redux store utils
+ * @module utils/create-store
+ */
+
+ var _require = __webpack_require__(3);
+
+ var createStore = _require.createStore;
+ var applyMiddleware = _require.applyMiddleware;
+
+ var _require2 = __webpack_require__(239);
+
+ var waitUntilService = _require2.waitUntilService;
+
+ var _require3 = __webpack_require__(240);
+
+ var log = _require3.log;
+
+ var _require4 = __webpack_require__(241);
+
+ var history = _require4.history;
+
+ var _require5 = __webpack_require__(242);
+
+ var promise = _require5.promise;
+
+ var _require6 = __webpack_require__(248);
+
+ var thunk = _require6.thunk;
+
+ /**
+ * @memberof utils/create-store
+ * @static
+ */
+
+ /**
+ * This creates a dispatcher with all the standard middleware in place
+ * that all code requires. It can also be optionally configured in
+ * various ways, such as logging and recording.
+ *
+ * @param {object} opts:
+ * - log: log all dispatched actions to console
+ * - history: an array to store every action in. Should only be
+ * used in tests.
+ * - middleware: array of middleware to be included in the redux store
+ * @memberof utils/create-store
+ * @static
+ */
+ var configureStore = function () {
+ var opts = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
+
+ var middleware = [thunk(opts.makeThunkArgs), promise,
+
+ // Order is important: services must go last as they always
+ // operate on "already transformed" actions. Actions going through
+ // them shouldn't have any special fields like promises, they
+ // should just be normal JSON objects.
+ waitUntilService];
+
+ if (opts.history) {
+ middleware.push(history(opts.history));
+ }
+
+ if (opts.middleware) {
+ opts.middleware.forEach(fn => middleware.push(fn));
+ }
+
+ if (opts.log) {
+ middleware.push(log);
+ }
+
+ // Hook in the redux devtools browser extension if it exists
+ var devtoolsExt = typeof window === "object" && window.devToolsExtension ? window.devToolsExtension() : f => f;
+
+ return applyMiddleware.apply(undefined, middleware)(devtoolsExt(createStore));
+ };
+
+ module.exports = configureStore;
+
+/***/ },
+/* 239 */
+/***/ function(module, exports) {
+
+ /* 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/. */
+
+ /**
+ * A middleware which acts like a service, because it is stateful
+ * and "long-running" in the background. It provides the ability
+ * for actions to install a function to be run once when a specific
+ * condition is met by an action coming through the system. Think of
+ * it as a thunk that blocks until the condition is met. Example:
+ *
+ * ```js
+ * const services = { WAIT_UNTIL: require('wait-service').NAME };
+ *
+ * { type: services.WAIT_UNTIL,
+ * predicate: action => action.type === constants.ADD_ITEM,
+ * run: (dispatch, getState, action) => {
+ * // Do anything here. You only need to accept the arguments
+ * // if you need them. `action` is the action that satisfied
+ * // the predicate.
+ * }
+ * }
+ * ```
+ */
+ var NAME = exports.NAME = "@@service/waitUntil";
+
+ function waitUntilService(_ref) {
+ var dispatch = _ref.dispatch;
+ var getState = _ref.getState;
+
+ var pending = [];
+
+ function checkPending(action) {
+ var readyRequests = [];
+ var stillPending = [];
+
+ // Find the pending requests whose predicates are satisfied with
+ // this action. Wait to run the requests until after we update the
+ // pending queue because the request handler may synchronously
+ // dispatch again and run this service (that use case is
+ // completely valid).
+ for (var request of pending) {
+ if (request.predicate(action)) {
+ readyRequests.push(request);
+ } else {
+ stillPending.push(request);
+ }
+ }
+
+ pending = stillPending;
+ for (var _request of readyRequests) {
+ _request.run(dispatch, getState, action);
+ }
+ }
+
+ return next => action => {
+ if (action.type === NAME) {
+ pending.push(action);
+ return null;
+ }
+ var result = next(action);
+ checkPending(action);
+ return result;
+ };
+ }
+ exports.waitUntilService = waitUntilService;
+
+/***/ },
+/* 240 */
+/***/ function(module, exports) {
+
+ /**
+ * A middleware that logs all actions coming through the system
+ * to the console.
+ */
+ function log(_ref) {
+ var dispatch = _ref.dispatch;
+ var getState = _ref.getState;
+
+ return next => action => {
+ var actionText = JSON.stringify(action, null, 2);
+ var truncatedActionText = actionText.slice(0, 1000) + "...";
+ console.log(`[DISPATCH ${ action.type }]`, action, truncatedActionText);
+ next(action);
+ };
+ }
+
+ exports.log = log;
+
+/***/ },
+/* 241 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /* 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 _require = __webpack_require__(89);
+
+ var isDevelopment = _require.isDevelopment;
+
+ /**
+ * A middleware that stores every action coming through the store in the passed
+ * in logging object. Should only be used for tests, as it collects all
+ * action information, which will cause memory bloat.
+ */
+
+ exports.history = function () {
+ var log = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
+ return (_ref) => {
+ var dispatch = _ref.dispatch;
+ var getState = _ref.getState;
+
+ if (isDevelopment()) {
+ console.warn("Using history middleware stores all actions in state for " + "testing and devtools is not currently running in test " + "mode. Be sure this is intentional.");
+ }
+ return next => action => {
+ log.push(action);
+ next(action);
+ };
+ };
+ };
+
+/***/ },
+/* 242 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /* 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 defer = __webpack_require__(243);
+
+ var _require = __webpack_require__(244);
+
+ var entries = _require.entries;
+ var toObject = _require.toObject;
+
+ var _require2 = __webpack_require__(246);
+
+ var executeSoon = _require2.executeSoon;
+
+
+ var PROMISE = exports.PROMISE = "@@dispatch/promise";
+ var seqIdVal = 1;
+
+ function seqIdGen() {
+ return seqIdVal++;
+ }
+
+ function promiseMiddleware(_ref) {
+ var dispatch = _ref.dispatch;
+ var getState = _ref.getState;
+
+ return next => action => {
+ if (!(PROMISE in action)) {
+ return next(action);
+ }
+
+ var promiseInst = action[PROMISE];
+ var seqId = seqIdGen().toString();
+
+ // Create a new action that doesn't have the promise field and has
+ // the `seqId` field that represents the sequence id
+ action = Object.assign(toObject(entries(action).filter(pair => pair[0] !== PROMISE)), { seqId });
+
+ dispatch(Object.assign({}, action, { status: "start" }));
+
+ // Return the promise so action creators can still compose if they
+ // want to.
+ var deferred = defer();
+ promiseInst.then(value => {
+ executeSoon(() => {
+ dispatch(Object.assign({}, action, {
+ status: "done",
+ value: value
+ }));
+ deferred.resolve(value);
+ });
+ }, error => {
+ executeSoon(() => {
+ dispatch(Object.assign({}, action, {
+ status: "error",
+ error: error.message || error
+ }));
+ deferred.reject(error);
+ });
+ });
+ return deferred.promise;
+ };
+ }
+
+ exports.promise = promiseMiddleware;
+
+/***/ },
+/* 243 */
+/***/ function(module, exports) {
+
+
+
+ function defer() {
+ var resolve = void 0;
+ var reject = void 0;
+ var promise = new Promise(function (innerResolve, innerReject) {
+ resolve = innerResolve;
+ reject = innerReject;
+ });
+ return {
+ resolve: resolve,
+ reject: reject,
+ promise: promise
+ };
+ } /* flow */
+
+ module.exports = defer;
+
+/***/ },
+/* 244 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }();
+
+ function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
+
+ /* -*- 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/. */
+
+ /**
+ * Utils for utils, by utils
+ * @module utils/utils
+ */
+
+ var co = __webpack_require__(245);
+
+ /**
+ * @memberof utils/utils
+ * @static
+ */
+ function asPaused(client, func) {
+ if (client.state != "paused") {
+ return co(function* () {
+ yield client.interrupt();
+ var result = void 0;
+
+ try {
+ result = yield func();
+ } catch (e) {
+ // Try to put the debugger back in a working state by resuming
+ // it
+ yield client.resume();
+ throw e;
+ }
+
+ yield client.resume();
+ return result;
+ });
+ }
+ return func();
+ }
+
+ /**
+ * @memberof utils/utils
+ * @static
+ */
+ function handleError(err) {
+ console.log("ERROR: ", err);
+ }
+
+ /**
+ * @memberof utils/utils
+ * @static
+ */
+ function promisify(context, method) {
+ for (var _len = arguments.length, args = Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) {
+ args[_key - 2] = arguments[_key];
+ }
+
+ return new Promise((resolve, reject) => {
+ args.push(response => {
+ if (response.error) {
+ reject(response);
+ } else {
+ resolve(response);
+ }
+ });
+ method.apply(context, args);
+ });
+ }
+
+ /**
+ * @memberof utils/utils
+ * @static
+ */
+ function truncateStr(str, size) {
+ if (str.length > size) {
+ return str.slice(0, size) + "...";
+ }
+ return str;
+ }
+
+ /**
+ * @memberof utils/utils
+ * @static
+ */
+ function endTruncateStr(str, size) {
+ if (str.length > size) {
+ return "..." + str.slice(str.length - size);
+ }
+ return str;
+ }
+
+ var msgId = 1;
+ /**
+ * @memberof utils/utils
+ * @static
+ */
+ function workerTask(worker, method) {
+ return function () {
+ for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
+ args[_key2] = arguments[_key2];
+ }
+
+ return new Promise((resolve, reject) => {
+ var id = msgId++;
+ worker.postMessage({ id, method, args });
+
+ var listener = (_ref) => {
+ var result = _ref.data;
+
+ if (result.id !== id) {
+ return;
+ }
+
+ worker.removeEventListener("message", listener);
+ if (result.error) {
+ reject(result.error);
+ } else {
+ resolve(result.response);
+ }
+ };
+
+ worker.addEventListener("message", listener);
+ });
+ };
+ }
+
+ /**
+ * Interleaves two arrays element by element, returning the combined array, like
+ * a zip. In the case of arrays with different sizes, undefined values will be
+ * interleaved at the end along with the extra values of the larger array.
+ *
+ * @param Array a
+ * @param Array b
+ * @returns Array
+ * The combined array, in the form [a1, b1, a2, b2, ...]
+ * @memberof utils/utils
+ * @static
+ */
+ function zip(a, b) {
+ if (!b) {
+ return a;
+ }
+ if (!a) {
+ return b;
+ }
+ var pairs = [];
+ for (var i = 0, aLength = a.length, bLength = b.length; i < aLength || i < bLength; i++) {
+ pairs.push([a[i], b[i]]);
+ }
+ return pairs;
+ }
+
+ /**
+ * Converts an object into an array with 2-element arrays as key/value
+ * pairs of the object. `{ foo: 1, bar: 2}` would become
+ * `[[foo, 1], [bar 2]]` (order not guaranteed);
+ *
+ * @returns array
+ * @memberof utils/utils
+ * @static
+ */
+ function entries(obj) {
+ return Object.keys(obj).map(k => [k, obj[k]]);
+ }
+
+ /**
+ * @memberof utils/utils
+ * @static
+ */
+ function mapObject(obj, iteratee) {
+ return toObject(entries(obj).map((_ref2) => {
+ var _ref3 = _slicedToArray(_ref2, 2);
+
+ var key = _ref3[0];
+ var value = _ref3[1];
+
+ return [key, iteratee(key, value)];
+ }));
+ }
+
+ /**
+ * Takes an array of 2-element arrays as key/values pairs and
+ * constructs an object using them.
+ * @memberof utils/utils
+ * @static
+ */
+ function toObject(arr) {
+ var obj = {};
+ for (var pair of arr) {
+ obj[pair[0]] = pair[1];
+ }
+ return obj;
+ }
+
+ /**
+ * Composes the given functions into a single function, which will
+ * apply the results of each function right-to-left, starting with
+ * applying the given arguments to the right-most function.
+ * `compose(foo, bar, baz)` === `args => foo(bar(baz(args)`
+ *
+ * @param ...function funcs
+ * @returns function
+ * @memberof utils/utils
+ * @static
+ */
+ function compose() {
+ for (var _len3 = arguments.length, funcs = Array(_len3), _key3 = 0; _key3 < _len3; _key3++) {
+ funcs[_key3] = arguments[_key3];
+ }
+
+ return function () {
+ for (var _len4 = arguments.length, args = Array(_len4), _key4 = 0; _key4 < _len4; _key4++) {
+ args[_key4] = arguments[_key4];
+ }
+
+ var initialValue = funcs[funcs.length - 1].apply(null, args);
+ var leftFuncs = funcs.slice(0, -1);
+ return leftFuncs.reduceRight((composed, f) => f(composed), initialValue);
+ };
+ }
+
+ /**
+ * @memberof utils/utils
+ * @static
+ */
+ function updateObj(obj, fields) {
+ return Object.assign({}, obj, fields);
+ }
+
+ /**
+ * @memberof utils/utils
+ * @static
+ */
+ function throttle(func, ms) {
+ var timeout = void 0,
+ _this = void 0;
+ return function () {
+ for (var _len5 = arguments.length, args = Array(_len5), _key5 = 0; _key5 < _len5; _key5++) {
+ args[_key5] = arguments[_key5];
+ }
+
+ _this = this;
+ if (!timeout) {
+ timeout = setTimeout(() => {
+ func.apply.apply(func, [_this].concat(_toConsumableArray(args)));
+ timeout = null;
+ }, ms);
+ }
+ };
+ }
+
+ module.exports = {
+ asPaused,
+ handleError,
+ promisify,
+ truncateStr,
+ endTruncateStr,
+ workerTask,
+ zip,
+ entries,
+ toObject,
+ mapObject,
+ compose,
+ updateObj,
+ throttle
+ };
+
+/***/ },
+/* 245 */
+/***/ function(module, exports) {
+
+
+ /**
+ * slice() reference.
+ */
+
+ var slice = Array.prototype.slice;
+
+ /**
+ * Expose `co`.
+ */
+
+ module.exports = co['default'] = co.co = co;
+
+ /**
+ * Wrap the given generator `fn` into a
+ * function that returns a promise.
+ * This is a separate function so that
+ * every `co()` call doesn't create a new,
+ * unnecessary closure.
+ *
+ * @param {GeneratorFunction} fn
+ * @return {Function}
+ * @api public
+ */
+
+ co.wrap = function (fn) {
+ createPromise.__generatorFunction__ = fn;
+ return createPromise;
+ function createPromise() {
+ return co.call(this, fn.apply(this, arguments));
+ }
+ };
+
+ /**
+ * Execute the generator function or a generator
+ * and return a promise.
+ *
+ * @param {Function} fn
+ * @return {Promise}
+ * @api public
+ */
+
+ function co(gen) {
+ var ctx = this;
+ var args = slice.call(arguments, 1)
+
+ // we wrap everything in a promise to avoid promise chaining,
+ // which leads to memory leak errors.
+ // see https://github.com/tj/co/issues/180
+ return new Promise(function(resolve, reject) {
+ if (typeof gen === 'function') gen = gen.apply(ctx, args);
+ if (!gen || typeof gen.next !== 'function') return resolve(gen);
+
+ onFulfilled();
+
+ /**
+ * @param {Mixed} res
+ * @return {Promise}
+ * @api private
+ */
+
+ function onFulfilled(res) {
+ var ret;
+ try {
+ ret = gen.next(res);
+ } catch (e) {
+ return reject(e);
+ }
+ next(ret);
+ }
+
+ /**
+ * @param {Error} err
+ * @return {Promise}
+ * @api private
+ */
+
+ function onRejected(err) {
+ var ret;
+ try {
+ ret = gen.throw(err);
+ } catch (e) {
+ return reject(e);
+ }
+ next(ret);
+ }
+
+ /**
+ * Get the next value in the generator,
+ * return a promise.
+ *
+ * @param {Object} ret
+ * @return {Promise}
+ * @api private
+ */
+
+ function next(ret) {
+ if (ret.done) return resolve(ret.value);
+ var value = toPromise.call(ctx, ret.value);
+ if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
+ return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
+ + 'but the following object was passed: "' + String(ret.value) + '"'));
+ }
+ });
+ }
+
+ /**
+ * Convert a `yield`ed value into a promise.
+ *
+ * @param {Mixed} obj
+ * @return {Promise}
+ * @api private
+ */
+
+ function toPromise(obj) {
+ if (!obj) return obj;
+ if (isPromise(obj)) return obj;
+ if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
+ if ('function' == typeof obj) return thunkToPromise.call(this, obj);
+ if (Array.isArray(obj)) return arrayToPromise.call(this, obj);
+ if (isObject(obj)) return objectToPromise.call(this, obj);
+ return obj;
+ }
+
+ /**
+ * Convert a thunk to a promise.
+ *
+ * @param {Function}
+ * @return {Promise}
+ * @api private
+ */
+
+ function thunkToPromise(fn) {
+ var ctx = this;
+ return new Promise(function (resolve, reject) {
+ fn.call(ctx, function (err, res) {
+ if (err) return reject(err);
+ if (arguments.length > 2) res = slice.call(arguments, 1);
+ resolve(res);
+ });
+ });
+ }
+
+ /**
+ * Convert an array of "yieldables" to a promise.
+ * Uses `Promise.all()` internally.
+ *
+ * @param {Array} obj
+ * @return {Promise}
+ * @api private
+ */
+
+ function arrayToPromise(obj) {
+ return Promise.all(obj.map(toPromise, this));
+ }
+
+ /**
+ * Convert an object of "yieldables" to a promise.
+ * Uses `Promise.all()` internally.
+ *
+ * @param {Object} obj
+ * @return {Promise}
+ * @api private
+ */
+
+ function objectToPromise(obj){
+ var results = new obj.constructor();
+ var keys = Object.keys(obj);
+ var promises = [];
+ for (var i = 0; i < keys.length; i++) {
+ var key = keys[i];
+ var promise = toPromise.call(this, obj[key]);
+ if (promise && isPromise(promise)) defer(promise, key);
+ else results[key] = obj[key];
+ }
+ return Promise.all(promises).then(function () {
+ return results;
+ });
+
+ function defer(promise, key) {
+ // predefine the key in the result
+ results[key] = undefined;
+ promises.push(promise.then(function (res) {
+ results[key] = res;
+ }));
+ }
+ }
+
+ /**
+ * Check if `obj` is a promise.
+ *
+ * @param {Object} obj
+ * @return {Boolean}
+ * @api private
+ */
+
+ function isPromise(obj) {
+ return 'function' == typeof obj.then;
+ }
+
+ /**
+ * Check if `obj` is a generator.
+ *
+ * @param {Mixed} obj
+ * @return {Boolean}
+ * @api private
+ */
+
+ function isGenerator(obj) {
+ return 'function' == typeof obj.next && 'function' == typeof obj.throw;
+ }
+
+ /**
+ * Check if `obj` is a generator function.
+ *
+ * @param {Mixed} obj
+ * @return {Boolean}
+ * @api private
+ */
+ function isGeneratorFunction(obj) {
+ var constructor = obj.constructor;
+ if (!constructor) return false;
+ if ('GeneratorFunction' === constructor.name || 'GeneratorFunction' === constructor.displayName) return true;
+ return isGenerator(constructor.prototype);
+ }
+
+ /**
+ * Check for plain object.
+ *
+ * @param {Mixed} val
+ * @return {Boolean}
+ * @api private
+ */
+
+ function isObject(val) {
+ return Object == val.constructor;
+ }
+
+
+/***/ },
+/* 246 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var assert = __webpack_require__(247);
+
+ function reportException(who, exception) {
+ var msg = who + " threw an exception: ";
+ console.error(msg, exception);
+ }
+
+ function executeSoon(fn) {
+ setTimeout(fn, 0);
+ }
+
+ module.exports = {
+ reportException,
+ executeSoon,
+ assert
+ };
+
+/***/ },
+/* 247 */
+/***/ function(module, exports) {
+
+ function assert(condition, message) {
+ if (!condition) {
+ throw new Error("Assertion failure: " + message);
+ }
+ }
+
+ module.exports = assert;
+
+/***/ },
+/* 248 */
+/***/ function(module, exports) {
+
+
+ /**
+ * A middleware that allows thunks (functions) to be dispatched. If
+ * it's a thunk, it is called with an argument that contains
+ * `dispatch`, `getState`, and any additional args passed in via the
+ * middleware constructure. This allows the action to create multiple
+ * actions (most likely asynchronously).
+ */
+ function thunk(makeArgs) {
+ return (_ref) => {
+ var dispatch = _ref.dispatch;
+ var getState = _ref.getState;
+
+ var args = { dispatch, getState };
+
+ return next => action => {
+ return typeof action === "function" ? action(makeArgs ? makeArgs(args, getState()) : args) : next(action);
+ };
+ };
+ }
+ exports.thunk = thunk;
+
+/***/ },
+/* 249 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /* 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 eventListeners = __webpack_require__(250);
+ var sources = __webpack_require__(252);
+ var breakpoints = __webpack_require__(255);
+ var asyncRequests = __webpack_require__(256);
+ var pause = __webpack_require__(257);
+ var ui = __webpack_require__(258);
+
+ module.exports = {
+ eventListeners,
+ sources: sources.update,
+ breakpoints: breakpoints.update,
+ pause: pause.update,
+ asyncRequests,
+ ui: ui.update
+ };
+
+/***/ },
+/* 250 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /* 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 constants = __webpack_require__(251);
+
+ var initialState = {
+ activeEventNames: [],
+ listeners: [],
+ fetchingListeners: false
+ };
+
+ function update() {
+ var state = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : initialState;
+ var action = arguments[1];
+ var emit = arguments[2];
+
+ switch (action.type) {
+ case constants.UPDATE_EVENT_BREAKPOINTS:
+ state.activeEventNames = action.eventNames;
+ emit("activeEventNames", state.activeEventNames);
+ break;
+ case constants.FETCH_EVENT_LISTENERS:
+ if (action.status === "begin") {
+ state.fetchingListeners = true;
+ } else if (action.status === "done") {
+ state.fetchingListeners = false;
+ state.listeners = action.listeners;
+ emit("event-listeners", state.listeners);
+ }
+ break;
+ case constants.NAVIGATE:
+ return initialState;
+ }
+
+ return state;
+ }
+
+ module.exports = update;
+
+/***/ },
+/* 251 */
+/***/ function(module, exports) {
+
+
+
+ /* -*- 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/. */
+
+ exports.UPDATE_EVENT_BREAKPOINTS = "UPDATE_EVENT_BREAKPOINTS";
+ exports.FETCH_EVENT_LISTENERS = "FETCH_EVENT_LISTENERS";
+
+ exports.TOGGLE_PRETTY_PRINT = "TOGGLE_PRETTY_PRINT";
+ exports.BLACKBOX = "BLACKBOX";
+
+ exports.ADD_BREAKPOINT = "ADD_BREAKPOINT";
+ exports.REMOVE_BREAKPOINT = "REMOVE_BREAKPOINT";
+ exports.ENABLE_BREAKPOINT = "ENABLE_BREAKPOINT";
+ exports.DISABLE_BREAKPOINT = "DISABLE_BREAKPOINT";
+ exports.SET_BREAKPOINT_CONDITION = "SET_BREAKPOINT_CONDITION";
+ exports.TOGGLE_BREAKPOINTS = "TOGGLE_BREAKPOINTS";
+
+ exports.ADD_SOURCE = "ADD_SOURCE";
+ exports.ADD_SOURCES = "ADD_SOURCES";
+ exports.LOAD_SOURCE_TEXT = "LOAD_SOURCE_TEXT";
+ exports.SELECT_SOURCE = "SELECT_SOURCE";
+ exports.SELECT_SOURCE_URL = "SELECT_SOURCE_URL";
+ exports.CLOSE_TAB = "CLOSE_TAB";
+ exports.NAVIGATE = "NAVIGATE";
+ exports.RELOAD = "RELOAD";
+
+ exports.ADD_TABS = "ADD_TABS";
+ exports.SELECT_TAB = "SELECT_TAB";
+
+ exports.BREAK_ON_NEXT = "BREAK_ON_NEXT";
+ exports.RESUME = "RESUME";
+ exports.PAUSED = "PAUSED";
+ exports.PAUSE_ON_EXCEPTIONS = "PAUSE_ON_EXCEPTIONS";
+ exports.COMMAND = "COMMAND";
+ exports.SELECT_FRAME = "SELECT_FRAME";
+ exports.LOAD_OBJECT_PROPERTIES = "LOAD_OBJECT_PROPERTIES";
+ exports.ADD_EXPRESSION = "ADD_EXPRESSION";
+ exports.EVALUATE_EXPRESSION = "EVALUATE_EXPRESSION";
+ exports.UPDATE_EXPRESSION = "UPDATE_EXPRESSION";
+ exports.DELETE_EXPRESSION = "DELETE_EXPRESSION";
+
+ exports.TOGGLE_FILE_SEARCH = "TOGGLE_FILE_SEARCH";
+
+/***/ },
+/* 252 */
+/***/ function(module, exports, __webpack_require__) {
+
+
+ /* 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/. */
+
+ /**
+ * Sources reducer
+ * @module reducers/sources
+ */
+
+ var fromJS = __webpack_require__(253);
+ var I = __webpack_require__(229);
+ var makeRecord = __webpack_require__(254);
+
+ var State = makeRecord({
+ sources: I.Map(),
+ selectedLocation: undefined,
+ pendingSelectedLocation: undefined,
+ sourcesText: I.Map(),
+ tabs: I.List([])
+ });
+
+ function update() {
+ var state = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : State();
+ var action = arguments[1];
+
+ switch (action.type) {
+ case "ADD_SOURCE":
+ {
+ var _source = action.source;
+ return state.mergeIn(["sources", action.source.id], _source);
+ }
+
+ case "SELECT_SOURCE":
+ return state.set("selectedLocation", {
+ sourceId: action.source.id,
+ line: action.line
+ }).set("pendingSelectedLocation", null).merge({
+ tabs: updateTabList(state, fromJS(action.source), action.tabIndex)
+ });
+
+ case "SELECT_SOURCE_URL":
+ return state.set("pendingSelectedLocation", {
+ url: action.url,
+ line: action.line
+ });
+
+ case "CLOSE_TAB":
+ return state.merge({ tabs: removeSourceFromTabList(state, action.id) }).set("selectedLocation", {
+ sourceId: getNewSelectedSourceId(state, action.id)
+ });
+
+ case "LOAD_SOURCE_TEXT":
+ return _updateText(state, action);
+
+ case "BLACKBOX":
+ if (action.status === "done") {
+ return state.setIn(["sources", action.source.id, "isBlackBoxed"], action.value.isBlackBoxed);
+ }
+ break;
+
+ case "TOGGLE_PRETTY_PRINT":
+ return _updateText(state, action);
+
+ case "NAVIGATE":
+ var source = getSelectedSource({ sources: state });
+ var _url = source && source.get("url");
+ return State().set("pendingSelectedLocation", { url: _url });
+ }
+
+ return state;
+ }
+
+ // TODO: Action is coerced to `any` unfortunately because how we type
+ // asynchronous actions is wrong. The `value` may be null for the
+ // "start" and "error" states but we don't type it like that. We need
+ // to rethink how we type async actions.
+ function _updateText(state, action) {
+ var source = action.source;
+ var sourceText = action.value;
+
+ if (action.status === "start") {
+ // Merge this in, don't set it. That way the previous value is
+ // still stored here, and we can retrieve it if whatever we're
+ // doing fails.
+ return state.mergeIn(["sourcesText", source.id], {
+ loading: true
+ });
+ }
+
+ if (action.status === "error") {
+ return state.setIn(["sourcesText", source.id], I.Map({
+ error: action.error
+ }));
+ }
+
+ return state.setIn(["sourcesText", source.id], I.Map({
+ text: sourceText.text,
+ contentType: sourceText.contentType
+ }));
+ }
+
+ function removeSourceFromTabList(state, id) {
+ return state.tabs.filter(tab => tab.get("id") != id);
+ }
+
+ /**
+ * Adds the new source to the tab list if it is not already there
+ * @memberof reducers/sources
+ * @static
+ */
+ function updateTabList(state, source, tabIndex) {
+ var tabs = state.get("tabs");
+ var sourceIndex = tabs.indexOf(source);
+ var includesSource = !!tabs.find(t => t.get("id") == source.get("id"));
+
+ if (includesSource) {
+ if (tabIndex != undefined) {
+ return tabs.delete(sourceIndex).insert(tabIndex, source);
+ }
+
+ return tabs;
+ }
+
+ return tabs.insert(0, source);
+ }
+
+ /**
+ * Gets the next tab to select when a tab closes.
+ * @memberof reducers/sources
+ * @static
+ */
+ function getNewSelectedSourceId(state, id) {
+ var tabs = state.get("tabs");
+ var selectedSource = getSelectedSource({ sources: state });
+
+ if (!selectedSource) {
+ return undefined;
+ } else if (selectedSource.get("id") != id) {
+ // If we're not closing the selected tab return the selected tab
+ return selectedSource.get("id");
+ }
+
+ var tabIndex = tabs.findIndex(tab => tab.get("id") == id);
+ var numTabs = tabs.count();
+
+ if (numTabs == 1) {
+ return undefined;
+ }
+
+ // if we're closing the last tab, select the penultimate tab
+ if (tabIndex + 1 == numTabs) {
+ return tabs.get(tabIndex - 1).get("id");
+ }
+
+ // return the next tab
+ return tabs.get(tabIndex + 1).get("id");
+ }
+
+ // Selectors
+
+ // Unfortunately, it's really hard to make these functions accept just
+ // the state that we care about and still type it with Flow. The
+ // problem is that we want to re-export all selectors from a single
+ // module for the UI, and all of those selectors should take the
+ // top-level app state, so we'd have to "wrap" them to automatically
+ // pick off the piece of state we're interested in. It's impossible
+ // (right now) to type those wrapped functions.
+
+
+ function getSource(state, id) {
+ return state.sources.sources.get(id);
+ }
+
+ function getSourceByURL(state, url) {
+ return state.sources.sources.find(source => source.get("url") == url);
+ }
+
+ function getSourceById(state, id) {
+ return state.sources.sources.find(source => source.get("id") == id);
+ }
+
+ function getSources(state) {
+ return state.sources.sources;
+ }
+
+ function getSourceText(state, id) {
+ return state.sources.sourcesText.get(id);
+ }
+
+ function getSourceTabs(state) {
+ return state.sources.tabs;
+ }
+
+ function getSelectedSource(state) {
+ if (state.sources.selectedLocation) {
+ return getSource(state, state.sources.selectedLocation.sourceId);
+ }
+ return undefined;
+ }
+
+ function getSelectedLocation(state) {
+ return state.sources.selectedLocation;
+ }
+
+ function getPendingSelectedLocation(state) {
+ return state.sources.pendingSelectedLocation;
+ }
+
+ function getPrettySource(state, id) {
+ var source = getSource(state, id);
+ if (!source) {
+ return;
+ }
+
+ return getSourceByURL(state, source.get("url") + ":formatted");
+ }
+
+ module.exports = {
+ State,
+ update,
+ getSource,
+ getSourceByURL,
+ getSourceById,
+ getSources,
+ getSourceText,
+ getSourceTabs,
+ getSelectedSource,
+ getSelectedLocation,
+ getPendingSelectedLocation,
+ getPrettySource
+ };
+
+/***/ },
+/* 253 */
+/***/ function(module, exports, __webpack_require__) {
+
+
+
+ /**
+ * Immutable JS conversion utils
+ * @deprecated
+ * @module utils/fromJS
+ */
+
+ var Immutable = __webpack_require__(229);
+
+ /**
+ * When our app state is fully typed, we should be able to get rid of
+ * this function. This is only temporarily necessary to support
+ * converting typed objects to immutable.js, which usually happens in
+ * reducers.
+ *
+ * @memberof utils/fromJS
+ * @static
+ */
+ function fromJS(value) {
+ if (Array.isArray(value)) {
+ return Immutable.Seq(value).map(fromJS).toList();
+ }
+ if (value && value.constructor.meta) {
+ // This adds support for tcomb objects which are native JS objects
+ // but are not "plain", so the above checks fail. Since they
+ // behave the same we can use the same constructors, but we need
+ // special checks for them.
+ var kind = value.constructor.meta.kind;
+ if (kind === "struct") {
+ return Immutable.Seq(value).map(fromJS).toMap();
+ } else if (kind === "list") {
+ return Immutable.Seq(value).map(fromJS).toList();
+ }
+ }
+
+ // If it's a primitive type, just return the value. Note `==` check
+ // for null, which is intentionally used to match either `null` or
+ // `undefined`.
+ if (value == null || typeof value !== "object") {
+ return value;
+ }
+
+ // Otherwise, treat it like an object. We can't reliably detect if
+ // it's a plain object because we might be objects from other JS
+ // contexts so `Object !== Object`.
+ return Immutable.Seq(value).map(fromJS).toMap();
+ }
+
+ module.exports = fromJS;
+
+/***/ },
+/* 254 */
+/***/ function(module, exports, __webpack_require__) {
+
+
+
+ /**
+ * When Flow 0.29 is released (very soon), we can use this Record type
+ * instead of the builtin immutable.js Record type. This is better
+ * because all the fields are actually typed, unlike the builtin one.
+ * This depends on a performance fix that will go out in 0.29 though;
+ * @module utils/makeRecord
+ */
+
+ var I = __webpack_require__(229);
+
+ /**
+ * @memberof utils/makeRecord
+ * @static
+ */
+
+
+ /**
+ * Make an immutable record type
+ *
+ * @param spec - the keys and their default values
+ * @return a state record factory function
+ * @memberof utils/makeRecord
+ * @static
+ */
+ function makeRecord(spec) {
+ return I.Record(spec);
+ }
+
+ module.exports = makeRecord;
+
+/***/ },
+/* 255 */
+/***/ function(module, exports, __webpack_require__) {
+
+
+ /* 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/. */
+
+ /**
+ * Breakpoints reducer
+ * @module reducers/breakpoints
+ */
+
+ var fromJS = __webpack_require__(253);
+
+ var _require = __webpack_require__(244);
+
+ var updateObj = _require.updateObj;
+
+ var I = __webpack_require__(229);
+ var makeRecord = __webpack_require__(254);
+
+ var State = makeRecord({
+ breakpoints: I.Map(),
+ breakpointsDisabled: false
+ });
+
+ // Return the first argument that is a string, or null if nothing is a
+ // string.
+ function firstString() {
+ for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
+ args[_key] = arguments[_key];
+ }
+
+ for (var arg of args) {
+ if (typeof arg === "string") {
+ return arg;
+ }
+ }
+ return null;
+ }
+
+ function locationMoved(location, newLocation) {
+ return location.line !== newLocation.line || location.column != null && location.column !== newLocation.column;
+ }
+
+ function makeLocationId(location) {
+ return location.sourceId + ":" + location.line.toString();
+ }
+
+ function update() {
+ var state = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : State();
+ var action = arguments[1];
+
+ switch (action.type) {
+ case "ADD_BREAKPOINT":
+ {
+ var id = makeLocationId(action.breakpoint.location);
+
+ if (action.status === "start") {
+ var bp = state.breakpoints.get(id) || action.breakpoint;
+
+ return state.setIn(["breakpoints", id], updateObj(bp, {
+ disabled: false,
+ loading: true,
+ // We want to do an OR here, but we can't because we need
+ // empty strings to be truthy, i.e. an empty string is a valid
+ // condition.
+ condition: firstString(action.condition, bp.condition)
+ }));
+ } else if (action.status === "done") {
+ var _action$value = action.value;
+ var breakpointId = _action$value.id;
+ var text = _action$value.text;
+
+ var location = action.breakpoint.location;
+ var actualLocation = action.value.actualLocation;
+
+ // If the breakpoint moved, update the map
+
+ if (locationMoved(location, actualLocation)) {
+ state = state.deleteIn(["breakpoints", id]);
+
+ var movedId = makeLocationId(actualLocation);
+ var currentBp = state.breakpoints.get(movedId) || fromJS(action.breakpoint);
+ var newBp = updateObj(currentBp, { location: actualLocation });
+ state = state.setIn(["breakpoints", movedId], newBp);
+ location = actualLocation;
+ }
+
+ var locationId = makeLocationId(location);
+ var _bp = state.breakpoints.get(locationId);
+ return state.setIn(["breakpoints", locationId], updateObj(_bp, {
+ id: breakpointId,
+ disabled: false,
+ loading: false,
+ text: text
+ }));
+ } else if (action.status === "error") {
+ // Remove the optimistic update
+ return state.deleteIn(["breakpoints", id]);
+ }
+ break;
+ }
+
+ case "REMOVE_BREAKPOINT":
+ {
+ if (action.status === "done") {
+ var _id = makeLocationId(action.breakpoint.location);
+
+ if (action.disabled) {
+ var _bp2 = state.breakpoints.get(_id);
+ return state.setIn(["breakpoints", _id], updateObj(_bp2, {
+ loading: false, disabled: true
+ }));
+ }
+
+ return state.deleteIn(["breakpoints", _id]);
+ }
+ break;
+ }
+
+ case "TOGGLE_BREAKPOINTS":
+ {
+ if (action.status === "start") {
+ return state.set("breakpointsDisabled", action.shouldDisableBreakpoints);
+ }
+ break;
+ }
+
+ case "SET_BREAKPOINT_CONDITION":
+ {
+ var _id2 = makeLocationId(action.breakpoint.location);
+
+ if (action.status === "start") {
+ var _bp3 = state.breakpoints.get(_id2);
+ return state.setIn(["breakpoints", _id2], updateObj(_bp3, {
+ loading: true,
+ condition: action.condition
+ }));
+ } else if (action.status === "done") {
+ var _bp4 = state.breakpoints.get(_id2);
+ return state.setIn(["breakpoints", _id2], updateObj(_bp4, {
+ id: action.value.id,
+ loading: false
+ }));
+ } else if (action.status === "error") {
+ return state.deleteIn(["breakpoints", _id2]);
+ }
+
+ break;
+ }}
+
+ return state;
+ }
+
+ // Selectors
+
+ function getBreakpoint(state, location) {
+ return state.breakpoints.breakpoints.get(makeLocationId(location));
+ }
+
+ function getBreakpoints(state) {
+ return state.breakpoints.breakpoints;
+ }
+
+ function getBreakpointsForSource(state, sourceId) {
+ return state.breakpoints.breakpoints.filter(bp => {
+ return bp.location.sourceId === sourceId;
+ });
+ }
+
+ function getBreakpointsDisabled(state) {
+ return state.breakpoints.get("breakpointsDisabled");
+ }
+
+ function getBreakpointsLoading(state) {
+ var breakpoints = getBreakpoints(state);
+ var isLoading = !!breakpoints.valueSeq().filter(bp => bp.loading).first();
+
+ return breakpoints.size > 0 && isLoading;
+ }
+
+ module.exports = {
+ State,
+ update,
+ makeLocationId,
+ getBreakpoint,
+ getBreakpoints,
+ getBreakpointsForSource,
+ getBreakpointsDisabled,
+ getBreakpointsLoading
+ };
+
+/***/ },
+/* 256 */
+/***/ function(module, exports, __webpack_require__) {
+
+ function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
+
+ /* 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 constants = __webpack_require__(251);
+ var initialState = [];
+
+ function update() {
+ var state = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : initialState;
+ var action = arguments[1];
+ var seqId = action.seqId;
+
+
+ if (action.type === constants.NAVIGATE) {
+ return initialState;
+ } else if (seqId) {
+ var newState = void 0;
+ if (action.status === "start") {
+ newState = [].concat(_toConsumableArray(state), [seqId]);
+ } else if (action.status === "error" || action.status === "done") {
+ newState = state.filter(id => id !== seqId);
+ }
+
+ return newState;
+ }
+
+ return state;
+ }
+
+ module.exports = update;
+
+/***/ },
+/* 257 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /* 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 constants = __webpack_require__(251);
+ var fromJS = __webpack_require__(253);
+
+ var initialState = fromJS({
+ pause: null,
+ isWaitingOnBreak: false,
+ frames: null,
+ selectedFrameId: null,
+ loadedObjects: {},
+ shouldPauseOnExceptions: false,
+ shouldIgnoreCaughtExceptions: false,
+ expressions: []
+ });
+
+ function update() {
+ var state = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : initialState;
+ var action = arguments[1];
+ var emit = arguments[2];
+
+ switch (action.type) {
+ case constants.PAUSED:
+ {
+ var selectedFrameId = action.selectedFrameId;
+ var frames = action.frames;
+ var pauseInfo = action.pauseInfo;
+
+ pauseInfo.isInterrupted = pauseInfo.why.type === "interrupted";
+
+ return state.merge({
+ isWaitingOnBreak: false,
+ pause: fromJS(pauseInfo),
+ selectedFrameId,
+ frames
+ });
+ }
+
+ case constants.RESUME:
+ return state.merge({
+ pause: null,
+ frames: null,
+ selectedFrameId: null,
+ loadedObjects: {}
+ });
+
+ case constants.TOGGLE_PRETTY_PRINT:
+ if (action.status == "done") {
+ var _frames = action.value.frames;
+ var pause = state.get("pause");
+ if (pause) {
+ pause = pause.set("frame", fromJS(_frames[0]));
+ }
+
+ return state.merge({ pause, frames: _frames });
+ }
+
+ break;
+ case constants.BREAK_ON_NEXT:
+ return state.set("isWaitingOnBreak", true);
+
+ case constants.LOADED_FRAMES:
+ if (action.status == "done") {
+ return state.set("frames", action.value.frames);
+ }
+
+ break;
+ case constants.SELECT_FRAME:
+ return state.set("selectedFrameId", action.frame.id);
+
+ case constants.LOAD_OBJECT_PROPERTIES:
+ if (action.status === "done") {
+ var ownProperties = action.value.ownProperties;
+ var prototype = action.value.prototype;
+
+ return state.setIn(["loadedObjects", action.objectId], { ownProperties, prototype });
+ }
+ break;
+
+ case constants.NAVIGATE:
+ return initialState;
+
+ case constants.PAUSE_ON_EXCEPTIONS:
+ var shouldPauseOnExceptions = action.shouldPauseOnExceptions;
+ var shouldIgnoreCaughtExceptions = action.shouldIgnoreCaughtExceptions;
+
+ return state.merge({
+ shouldPauseOnExceptions,
+ shouldIgnoreCaughtExceptions
+ });
+
+ case constants.ADD_EXPRESSION:
+ return state.setIn(["expressions", action.id], { id: action.id,
+ input: action.input,
+ value: action.value,
+ updating: false });
+
+ case constants.EVALUATE_EXPRESSION:
+ if (action.status === "done") {
+ return state.mergeIn(["expressions", action.id], { id: action.id,
+ input: action.input,
+ value: action.value,
+ updating: false });
+ }
+ break;
+
+ case constants.UPDATE_EXPRESSION:
+ return state.mergeIn(["expressions", action.id], { id: action.id,
+ input: action.input,
+ updating: true });
+
+ case constants.DELETE_EXPRESSION:
+ return state.deleteIn(["expressions", action.id]);
+ }
+
+ return state;
+ }
+
+ function getPause(state) {
+ return state.pause.get("pause");
+ }
+
+ function getLoadedObjects(state) {
+ return state.pause.get("loadedObjects");
+ }
+
+ function getExpressions(state) {
+ return state.pause.get("expressions");
+ }
+
+ function getIsWaitingOnBreak(state) {
+ return state.pause.get("isWaitingOnBreak");
+ }
+
+ function getShouldPauseOnExceptions(state) {
+ return state.pause.get("shouldPauseOnExceptions");
+ }
+
+ function getShouldIgnoreCaughtExceptions(state) {
+ return state.pause.get("shouldIgnoreCaughtExceptions");
+ }
+
+ function getFrames(state) {
+ return state.pause.get("frames");
+ }
+
+ function getSelectedFrame(state) {
+ var selectedFrameId = state.pause.get("selectedFrameId");
+ var frames = state.pause.get("frames");
+ return frames && frames.find(frame => frame.id == selectedFrameId);
+ }
+
+ module.exports = {
+ initialState,
+ update,
+ getPause,
+ getLoadedObjects,
+ getExpressions,
+ getIsWaitingOnBreak,
+ getShouldPauseOnExceptions,
+ getShouldIgnoreCaughtExceptions,
+ getFrames,
+ getSelectedFrame
+ };
+
+/***/ },
+/* 258 */
+/***/ function(module, exports, __webpack_require__) {
+
+
+
+ /**
+ * UI reducer
+ * @module reducers/ui
+ */
+
+ var constants = __webpack_require__(251);
+ var makeRecord = __webpack_require__(254);
+
+ var State = makeRecord({
+ searchOn: false
+ });
+
+ function update() {
+ var state = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : State();
+ var action = arguments[1];
+
+ switch (action.type) {
+ case constants.TOGGLE_FILE_SEARCH:
+ {
+ return state.set("searchOn", action.searchOn);
+ }
+ default:
+ {
+ return state;
+ }
+ }
+ }
+
+ // NOTE: we'd like to have the app state fully typed
+ // https://github.com/devtools-html/debugger.html/blob/master/public/js/reducers/sources.js#L179-L185
+
+
+ function getFileSearchState(state) {
+ return state.ui.get("searchOn");
+ }
+
+ module.exports = {
+ State,
+ update,
+ getFileSearchState
+ };
+
+/***/ },
+/* 259 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var sources = __webpack_require__(252);
+ var pause = __webpack_require__(257);
+ var breakpoints = __webpack_require__(255);
+ var ui = __webpack_require__(258);
+
+ /**
+ * @param object - location
+ */
+
+ module.exports = {
+ getSource: sources.getSource,
+ getSourceByURL: sources.getSourceByURL,
+ getSourceById: sources.getSourceById,
+ getSources: sources.getSources,
+ getSourceText: sources.getSourceText,
+ getSourceTabs: sources.getSourceTabs,
+ getSelectedSource: sources.getSelectedSource,
+ getSelectedLocation: sources.getSelectedLocation,
+ getPendingSelectedLocation: sources.getPendingSelectedLocation,
+ getPrettySource: sources.getPrettySource,
+
+ getBreakpoint: breakpoints.getBreakpoint,
+ getBreakpoints: breakpoints.getBreakpoints,
+ getBreakpointsForSource: breakpoints.getBreakpointsForSource,
+ getBreakpointsDisabled: breakpoints.getBreakpointsDisabled,
+ getBreakpointsLoading: breakpoints.getBreakpointsLoading,
+
+ getPause: pause.getPause,
+ getLoadedObjects: pause.getLoadedObjects,
+ getExpressions: pause.getExpressions,
+ getExpressionInputVisibility: pause.getExpressionInputVisibility,
+ getIsWaitingOnBreak: pause.getIsWaitingOnBreak,
+ getShouldPauseOnExceptions: pause.getShouldPauseOnExceptions,
+ getShouldIgnoreCaughtExceptions: pause.getShouldIgnoreCaughtExceptions,
+ getFrames: pause.getFrames,
+ getSelectedFrame: pause.getSelectedFrame,
+
+ getFileSearchState: ui.getFileSearchState
+ };
+
+/***/ },
+/* 260 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var React = __webpack_require__(2);
+ var dom = React.DOM;
+ var PropTypes = React.PropTypes;
+ var createFactory = React.createFactory;
+
+ var _require = __webpack_require__(19);
+
+ var connect = _require.connect;
+
+ var _require2 = __webpack_require__(3);
+
+ var bindActionCreators = _require2.bindActionCreators;
+
+ var _require3 = __webpack_require__(261);
+
+ var cmdString = _require3.cmdString;
+
+ var actions = __webpack_require__(262);
+
+ var _require4 = __webpack_require__(259);
+
+ var getSources = _require4.getSources;
+ var getSelectedSource = _require4.getSelectedSource;
+
+ var _require5 = __webpack_require__(28);
+
+ var KeyShortcuts = _require5.KeyShortcuts;
+
+ var shortcuts = new KeyShortcuts({ window });
+
+ __webpack_require__(283);
+ __webpack_require__(286);
+ __webpack_require__(288);
+ __webpack_require__(290);
+
+ var _require6 = __webpack_require__(30);
+
+ var SplitBox = _require6.SplitBox;
+
+ SplitBox = createFactory(SplitBox);
+
+ var SourceSearch = createFactory(__webpack_require__(292));
+ var Sources = createFactory(__webpack_require__(339));
+ var Editor = createFactory(__webpack_require__(400));
+ var RightSidebar = createFactory(__webpack_require__(416));
+ var SourceTabs = createFactory(__webpack_require__(452));
+
+ var App = React.createClass({
+ propTypes: {
+ sources: PropTypes.object,
+ selectSource: PropTypes.func,
+ selectedSource: PropTypes.object
+ },
+
+ displayName: "App",
+
+ getChildContext() {
+ return { shortcuts };
+ },
+
+ renderWelcomeBox() {
+ return dom.div({ className: "welcomebox" }, L10N.getFormatStr("welcome.search", cmdString() + "+P"));
+ },
+
+ renderCenterPane() {
+ return dom.div({ className: "center-pane" }, dom.div({ className: "editor-container" }, SourceTabs(), Editor(), !this.props.selectedSource ? this.renderWelcomeBox() : null, SourceSearch()));
+ },
+
+ render: function () {
+ return dom.div({ className: "debugger" }, SplitBox({
+ style: { width: "100vw" },
+ initialSize: "300px",
+ minSize: 10,
+ maxSize: "50%",
+ splitterSize: 1,
+ startPanel: Sources({ sources: this.props.sources }),
+ endPanel: SplitBox({
+ initialSize: "300px",
+ minSize: 10,
+ maxSize: "80%",
+ splitterSize: 1,
+ endPanelControl: true,
+ startPanel: this.renderCenterPane(this.props),
+ endPanel: RightSidebar()
+ })
+ }));
+ }
+ });
+
+ App.childContextTypes = {
+ shortcuts: PropTypes.object
+ };
+
+ module.exports = connect(state => ({ sources: getSources(state),
+ selectedSource: getSelectedSource(state)
+ }), dispatch => bindActionCreators(actions, dispatch))(App);
+
+/***/ },
+/* 261 */
+/***/ function(module, exports, __webpack_require__) {
+
+
+
+ /**
+ * Utils for keyboard command strings
+ * @module utils/text
+ */
+
+ var _require = __webpack_require__(30);
+
+ var appinfo = _require.Services.appinfo;
+
+ /**
+ * @memberof utils/text
+ * @static
+ */
+
+ function cmdString() {
+ return appinfo.OS === "Darwin" ? "⌘" : "Ctrl";
+ }
+
+ module.exports = {
+ cmdString
+ };
+
+/***/ },
+/* 262 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var breakpoints = __webpack_require__(263);
+ var eventListeners = __webpack_require__(271);
+ var sources = __webpack_require__(273);
+ var pause = __webpack_require__(280);
+ var navigation = __webpack_require__(281);
+ var ui = __webpack_require__(282);
+
+ module.exports = Object.assign(navigation, breakpoints, eventListeners, sources, pause, ui);
+
+/***/ },
+/* 263 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }();
+
+ function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; }
+
+ /* 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/. */
+
+ /**
+ * Redux actions for breakpoints
+ * @module actions/breakpoints
+ */
+
+ var constants = __webpack_require__(251);
+
+ var _require = __webpack_require__(242);
+
+ var PROMISE = _require.PROMISE;
+
+ var _require2 = __webpack_require__(259);
+
+ var getBreakpoint = _require2.getBreakpoint;
+ var getBreakpoints = _require2.getBreakpoints;
+ var getSource = _require2.getSource;
+
+ var _require3 = __webpack_require__(264);
+
+ var getOriginalLocation = _require3.getOriginalLocation;
+ var getGeneratedLocation = _require3.getGeneratedLocation;
+ var isOriginalId = _require3.isOriginalId;
+
+
+ function _breakpointExists(state, location) {
+ var currentBp = getBreakpoint(state, location);
+ return currentBp && !currentBp.disabled;
+ }
+
+ function _getOrCreateBreakpoint(state, location, condition) {
+ return getBreakpoint(state, location) || { location, condition, text: "" };
+ }
+
+ /**
+ * Enabling a breakpoint calls {@link addBreakpoint}
+ * which will reuse the existing breakpoint information that is stored.
+ *
+ * @memberof actions/breakpoints
+ * @static
+ */
+ function enableBreakpoint(location) {
+ return addBreakpoint(location);
+ }
+
+ /**
+ * Add a new or enable an existing breakpoint
+ *
+ * @memberof actions/breakpoints
+ * @static
+ * @param {String} $1.condition Conditional breakpoint condition value
+ * @param {Function} $1.getTextForLine Get the text to represent the line
+ */
+ function addBreakpoint(location) {
+ var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
+
+ var condition = _ref.condition;
+ var getTextForLine = _ref.getTextForLine;
+
+ return (_ref2) => {
+ var dispatch = _ref2.dispatch;
+ var getState = _ref2.getState;
+ var client = _ref2.client;
+
+ if (_breakpointExists(getState(), location)) {
+ return Promise.resolve();
+ }
+
+ var bp = _getOrCreateBreakpoint(getState(), location, condition);
+
+ return dispatch({
+ type: constants.ADD_BREAKPOINT,
+ breakpoint: bp,
+ condition: condition,
+ [PROMISE]: _asyncToGenerator(function* () {
+ if (isOriginalId(bp.location.sourceId)) {
+ var source = getSource(getState(), bp.location.sourceId);
+ location = yield getGeneratedLocation(bp.location, source.toJS());
+ }
+
+ var _ref4 = yield client.setBreakpoint(location, bp.condition, isOriginalId(bp.location.sourceId));
+
+ var id = _ref4.id;
+ var actualLocation = _ref4.actualLocation;
+
+
+ actualLocation = yield getOriginalLocation(actualLocation);
+
+ // If this breakpoint is being re-enabled, it already has a
+ // text snippet.
+ var text = bp.text;
+ if (!text) {
+ text = getTextForLine ? getTextForLine(actualLocation.line) : "";
+ }
+
+ return { id, actualLocation, text };
+ })()
+ });
+ };
+ }
+
+ /**
+ * Disable a single breakpoint
+ *
+ * @memberof actions/breakpoints
+ * @static
+ */
+ function disableBreakpoint(location) {
+ return _removeOrDisableBreakpoint(location, true);
+ }
+
+ /**
+ * Remove a single breakpoint
+ *
+ * @memberof actions/breakpoints
+ * @static
+ */
+ function removeBreakpoint(location) {
+ return _removeOrDisableBreakpoint(location);
+ }
+
+ function _removeOrDisableBreakpoint(location, isDisabled) {
+ return (_ref5) => {
+ var dispatch = _ref5.dispatch;
+ var getState = _ref5.getState;
+ var client = _ref5.client;
+
+ var bp = getBreakpoint(getState(), location);
+ if (!bp) {
+ throw new Error("attempt to remove breakpoint that does not exist");
+ }
+ if (bp.loading) {
+ // TODO(jwl): make this wait until the breakpoint is saved if it
+ // is still loading
+ throw new Error("attempt to remove unsaved breakpoint");
+ }
+
+ var action = {
+ type: constants.REMOVE_BREAKPOINT,
+ breakpoint: bp,
+ disabled: isDisabled
+ };
+
+ // If the breakpoint is already disabled, we don't need to remove
+ // it from the server. We just need to dispatch an action
+ // simulating a successful server request to remove it, and it
+ // will be removed completely from the state.
+ if (!bp.disabled) {
+ return dispatch(Object.assign({}, action, {
+ [PROMISE]: client.removeBreakpoint(bp.id)
+ }));
+ }
+ return dispatch(Object.assign({}, action, { status: "done" }));
+ };
+ }
+
+ /**
+ * Toggle All Breakpoints
+ *
+ * @memberof actions/breakpoints
+ * @static
+ */
+ function toggleAllBreakpoints(shouldDisableBreakpoints) {
+ return (_ref6) => {
+ var dispatch = _ref6.dispatch;
+ var getState = _ref6.getState;
+
+ var breakpoints = getBreakpoints(getState());
+ return dispatch({
+ type: constants.TOGGLE_BREAKPOINTS,
+ shouldDisableBreakpoints,
+ [PROMISE]: _asyncToGenerator(function* () {
+ for (var _ref8 of breakpoints) {
+ var _ref9 = _slicedToArray(_ref8, 2);
+
+ var breakpoint = _ref9[1];
+
+ if (shouldDisableBreakpoints) {
+ yield dispatch(disableBreakpoint(breakpoint.location));
+ } else {
+ yield dispatch(enableBreakpoint(breakpoint.location));
+ }
+ }
+ })()
+ });
+ };
+ }
+
+ /**
+ * Update the condition of a breakpoint.
+ *
+ * @throws {Error} "not implemented"
+ * @memberof actions/breakpoints
+ * @static
+ * @param {Location} location
+ * @see DebuggerController.Breakpoints.addBreakpoint
+ * @param {string} condition
+ * The condition to set on the breakpoint
+ */
+ function setBreakpointCondition(location) {
+ var _ref10 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
+
+ var condition = _ref10.condition;
+ var getTextForLine = _ref10.getTextForLine;
+
+ // location: Location, condition: string, { getTextForLine }) {
+ return (_ref11) => {
+ var dispatch = _ref11.dispatch;
+ var getState = _ref11.getState;
+ var client = _ref11.client;
+
+ var bp = getBreakpoint(getState(), location);
+ if (!bp) {
+ return dispatch(addBreakpoint(location, { condition, getTextForLine }));
+ }
+
+ if (bp.loading) {
+ // TODO(jwl): when this function is called, make sure the action
+ // creator waits for the breakpoint to exist
+ throw new Error("breakpoint must be saved");
+ }
+
+ return dispatch({
+ type: constants.SET_BREAKPOINT_CONDITION,
+ breakpoint: bp,
+ condition: condition,
+ [PROMISE]: client.setBreakpointCondition(bp.id, location, condition, isOriginalId(bp.location.sourceId))
+ });
+ };
+ }
+
+ module.exports = {
+ enableBreakpoint,
+ addBreakpoint,
+ disableBreakpoint,
+ removeBreakpoint,
+ toggleAllBreakpoints,
+ setBreakpointCondition
+ };
+
+/***/ },
+/* 264 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var _require = __webpack_require__(89);
+
+ var getValue = _require.getValue;
+ var isEnabled = _require.isEnabled;
+
+ var _require2 = __webpack_require__(244);
+
+ var workerTask = _require2.workerTask;
+
+ var _require3 = __webpack_require__(265);
+
+ var originalToGeneratedId = _require3.originalToGeneratedId;
+ var generatedToOriginalId = _require3.generatedToOriginalId;
+ var isGeneratedId = _require3.isGeneratedId;
+ var isOriginalId = _require3.isOriginalId;
+
+ var _require4 = __webpack_require__(270);
+
+ var prefs = _require4.prefs;
+
+
+ var sourceMapWorker = void 0;
+ function restartWorker() {
+ if (sourceMapWorker) {
+ sourceMapWorker.terminate();
+ }
+ sourceMapWorker = new Worker(getValue("baseWorkerURL") + "source-map-worker.js");
+
+ if (isEnabled("sourceMaps")) {
+ sourceMapWorker.postMessage({ id: 0, method: "enableSourceMaps" });
+ }
+ }
+ restartWorker();
+
+ function destroyWorker() {
+ if (sourceMapWorker) {
+ sourceMapWorker.terminate();
+ sourceMapWorker = null;
+ }
+ }
+
+ function shouldSourceMap() {
+ return isEnabled("sourceMaps") && prefs.clientSourceMapsEnabled;
+ }
+
+ var getOriginalURLs = workerTask(sourceMapWorker, "getOriginalURLs");
+ var getGeneratedLocation = workerTask(sourceMapWorker, "getGeneratedLocation");
+ var getOriginalLocation = workerTask(sourceMapWorker, "getOriginalLocation");
+ var getOriginalSourceText = workerTask(sourceMapWorker, "getOriginalSourceText");
+ var applySourceMap = workerTask(sourceMapWorker, "applySourceMap");
+ var clearSourceMaps = workerTask(sourceMapWorker, "clearSourceMaps");
+
+ module.exports = {
+ originalToGeneratedId,
+ generatedToOriginalId,
+ isGeneratedId,
+ isOriginalId,
+
+ getOriginalURLs,
+ getGeneratedLocation,
+ getOriginalLocation,
+ getOriginalSourceText,
+ applySourceMap,
+ clearSourceMaps,
+ destroyWorker,
+ shouldSourceMap
+ };
+
+/***/ },
+/* 265 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var md5 = __webpack_require__(266);
+
+ function originalToGeneratedId(originalId) {
+ var match = originalId.match(/(.*)\/originalSource/);
+ return match ? match[1] : "";
+ }
+
+ function generatedToOriginalId(generatedId, url) {
+ return generatedId + "/originalSource-" + md5(url);
+ }
+
+ function isOriginalId(id) {
+ return !!id.match(/\/originalSource/);
+ }
+
+ function isGeneratedId(id) {
+ return !isOriginalId(id);
+ }
+
+ module.exports = {
+ originalToGeneratedId, generatedToOriginalId, isOriginalId, isGeneratedId
+ };
+
+/***/ },
+/* 266 */
+/***/ function(module, exports, __webpack_require__) {
+
+ (function(){
+ var crypt = __webpack_require__(267),
+ utf8 = __webpack_require__(268).utf8,
+ isBuffer = __webpack_require__(269),
+ bin = __webpack_require__(268).bin,
+
+ // The core
+ md5 = function (message, options) {
+ // Convert to byte array
+ if (message.constructor == String)
+ if (options && options.encoding === 'binary')
+ message = bin.stringToBytes(message);
+ else
+ message = utf8.stringToBytes(message);
+ else if (isBuffer(message))
+ message = Array.prototype.slice.call(message, 0);
+ else if (!Array.isArray(message))
+ message = message.toString();
+ // else, assume byte array already
+
+ var m = crypt.bytesToWords(message),
+ l = message.length * 8,
+ a = 1732584193,
+ b = -271733879,
+ c = -1732584194,
+ d = 271733878;
+
+ // Swap endian
+ for (var i = 0; i < m.length; i++) {
+ m[i] = ((m[i] << 8) | (m[i] >>> 24)) & 0x00FF00FF |
+ ((m[i] << 24) | (m[i] >>> 8)) & 0xFF00FF00;
+ }
+
+ // Padding
+ m[l >>> 5] |= 0x80 << (l % 32);
+ m[(((l + 64) >>> 9) << 4) + 14] = l;
+
+ // Method shortcuts
+ var FF = md5._ff,
+ GG = md5._gg,
+ HH = md5._hh,
+ II = md5._ii;
+
+ for (var i = 0; i < m.length; i += 16) {
+
+ var aa = a,
+ bb = b,
+ cc = c,
+ dd = d;
+
+ a = FF(a, b, c, d, m[i+ 0], 7, -680876936);
+ d = FF(d, a, b, c, m[i+ 1], 12, -389564586);
+ c = FF(c, d, a, b, m[i+ 2], 17, 606105819);
+ b = FF(b, c, d, a, m[i+ 3], 22, -1044525330);
+ a = FF(a, b, c, d, m[i+ 4], 7, -176418897);
+ d = FF(d, a, b, c, m[i+ 5], 12, 1200080426);
+ c = FF(c, d, a, b, m[i+ 6], 17, -1473231341);
+ b = FF(b, c, d, a, m[i+ 7], 22, -45705983);
+ a = FF(a, b, c, d, m[i+ 8], 7, 1770035416);
+ d = FF(d, a, b, c, m[i+ 9], 12, -1958414417);
+ c = FF(c, d, a, b, m[i+10], 17, -42063);
+ b = FF(b, c, d, a, m[i+11], 22, -1990404162);
+ a = FF(a, b, c, d, m[i+12], 7, 1804603682);
+ d = FF(d, a, b, c, m[i+13], 12, -40341101);
+ c = FF(c, d, a, b, m[i+14], 17, -1502002290);
+ b = FF(b, c, d, a, m[i+15], 22, 1236535329);
+
+ a = GG(a, b, c, d, m[i+ 1], 5, -165796510);
+ d = GG(d, a, b, c, m[i+ 6], 9, -1069501632);
+ c = GG(c, d, a, b, m[i+11], 14, 643717713);
+ b = GG(b, c, d, a, m[i+ 0], 20, -373897302);
+ a = GG(a, b, c, d, m[i+ 5], 5, -701558691);
+ d = GG(d, a, b, c, m[i+10], 9, 38016083);
+ c = GG(c, d, a, b, m[i+15], 14, -660478335);
+ b = GG(b, c, d, a, m[i+ 4], 20, -405537848);
+ a = GG(a, b, c, d, m[i+ 9], 5, 568446438);
+ d = GG(d, a, b, c, m[i+14], 9, -1019803690);
+ c = GG(c, d, a, b, m[i+ 3], 14, -187363961);
+ b = GG(b, c, d, a, m[i+ 8], 20, 1163531501);
+ a = GG(a, b, c, d, m[i+13], 5, -1444681467);
+ d = GG(d, a, b, c, m[i+ 2], 9, -51403784);
+ c = GG(c, d, a, b, m[i+ 7], 14, 1735328473);
+ b = GG(b, c, d, a, m[i+12], 20, -1926607734);
+
+ a = HH(a, b, c, d, m[i+ 5], 4, -378558);
+ d = HH(d, a, b, c, m[i+ 8], 11, -2022574463);
+ c = HH(c, d, a, b, m[i+11], 16, 1839030562);
+ b = HH(b, c, d, a, m[i+14], 23, -35309556);
+ a = HH(a, b, c, d, m[i+ 1], 4, -1530992060);
+ d = HH(d, a, b, c, m[i+ 4], 11, 1272893353);
+ c = HH(c, d, a, b, m[i+ 7], 16, -155497632);
+ b = HH(b, c, d, a, m[i+10], 23, -1094730640);
+ a = HH(a, b, c, d, m[i+13], 4, 681279174);
+ d = HH(d, a, b, c, m[i+ 0], 11, -358537222);
+ c = HH(c, d, a, b, m[i+ 3], 16, -722521979);
+ b = HH(b, c, d, a, m[i+ 6], 23, 76029189);
+ a = HH(a, b, c, d, m[i+ 9], 4, -640364487);
+ d = HH(d, a, b, c, m[i+12], 11, -421815835);
+ c = HH(c, d, a, b, m[i+15], 16, 530742520);
+ b = HH(b, c, d, a, m[i+ 2], 23, -995338651);
+
+ a = II(a, b, c, d, m[i+ 0], 6, -198630844);
+ d = II(d, a, b, c, m[i+ 7], 10, 1126891415);
+ c = II(c, d, a, b, m[i+14], 15, -1416354905);
+ b = II(b, c, d, a, m[i+ 5], 21, -57434055);
+ a = II(a, b, c, d, m[i+12], 6, 1700485571);
+ d = II(d, a, b, c, m[i+ 3], 10, -1894986606);
+ c = II(c, d, a, b, m[i+10], 15, -1051523);
+ b = II(b, c, d, a, m[i+ 1], 21, -2054922799);
+ a = II(a, b, c, d, m[i+ 8], 6, 1873313359);
+ d = II(d, a, b, c, m[i+15], 10, -30611744);
+ c = II(c, d, a, b, m[i+ 6], 15, -1560198380);
+ b = II(b, c, d, a, m[i+13], 21, 1309151649);
+ a = II(a, b, c, d, m[i+ 4], 6, -145523070);
+ d = II(d, a, b, c, m[i+11], 10, -1120210379);
+ c = II(c, d, a, b, m[i+ 2], 15, 718787259);
+ b = II(b, c, d, a, m[i+ 9], 21, -343485551);
+
+ a = (a + aa) >>> 0;
+ b = (b + bb) >>> 0;
+ c = (c + cc) >>> 0;
+ d = (d + dd) >>> 0;
+ }
+
+ return crypt.endian([a, b, c, d]);
+ };
+
+ // Auxiliary functions
+ md5._ff = function (a, b, c, d, x, s, t) {
+ var n = a + (b & c | ~b & d) + (x >>> 0) + t;
+ return ((n << s) | (n >>> (32 - s))) + b;
+ };
+ md5._gg = function (a, b, c, d, x, s, t) {
+ var n = a + (b & d | c & ~d) + (x >>> 0) + t;
+ return ((n << s) | (n >>> (32 - s))) + b;
+ };
+ md5._hh = function (a, b, c, d, x, s, t) {
+ var n = a + (b ^ c ^ d) + (x >>> 0) + t;
+ return ((n << s) | (n >>> (32 - s))) + b;
+ };
+ md5._ii = function (a, b, c, d, x, s, t) {
+ var n = a + (c ^ (b | ~d)) + (x >>> 0) + t;
+ return ((n << s) | (n >>> (32 - s))) + b;
+ };
+
+ // Package private blocksize
+ md5._blocksize = 16;
+ md5._digestsize = 16;
+
+ module.exports = function (message, options) {
+ if (message === undefined || message === null)
+ throw new Error('Illegal argument ' + message);
+
+ var digestbytes = crypt.wordsToBytes(md5(message, options));
+ return options && options.asBytes ? digestbytes :
+ options && options.asString ? bin.bytesToString(digestbytes) :
+ crypt.bytesToHex(digestbytes);
+ };
+
+ })();
+
+
+/***/ },
+/* 267 */
+/***/ function(module, exports) {
+
+ (function() {
+ var base64map
+ = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/',
+
+ crypt = {
+ // Bit-wise rotation left
+ rotl: function(n, b) {
+ return (n << b) | (n >>> (32 - b));
+ },
+
+ // Bit-wise rotation right
+ rotr: function(n, b) {
+ return (n << (32 - b)) | (n >>> b);
+ },
+
+ // Swap big-endian to little-endian and vice versa
+ endian: function(n) {
+ // If number given, swap endian
+ if (n.constructor == Number) {
+ return crypt.rotl(n, 8) & 0x00FF00FF | crypt.rotl(n, 24) & 0xFF00FF00;
+ }
+
+ // Else, assume array and swap all items
+ for (var i = 0; i < n.length; i++)
+ n[i] = crypt.endian(n[i]);
+ return n;
+ },
+
+ // Generate an array of any length of random bytes
+ randomBytes: function(n) {
+ for (var bytes = []; n > 0; n--)
+ bytes.push(Math.floor(Math.random() * 256));
+ return bytes;
+ },
+
+ // Convert a byte array to big-endian 32-bit words
+ bytesToWords: function(bytes) {
+ for (var words = [], i = 0, b = 0; i < bytes.length; i++, b += 8)
+ words[b >>> 5] |= bytes[i] << (24 - b % 32);
+ return words;
+ },
+
+ // Convert big-endian 32-bit words to a byte array
+ wordsToBytes: function(words) {
+ for (var bytes = [], b = 0; b < words.length * 32; b += 8)
+ bytes.push((words[b >>> 5] >>> (24 - b % 32)) & 0xFF);
+ return bytes;
+ },
+
+ // Convert a byte array to a hex string
+ bytesToHex: function(bytes) {
+ for (var hex = [], i = 0; i < bytes.length; i++) {
+ hex.push((bytes[i] >>> 4).toString(16));
+ hex.push((bytes[i] & 0xF).toString(16));
+ }
+ return hex.join('');
+ },
+
+ // Convert a hex string to a byte array
+ hexToBytes: function(hex) {
+ for (var bytes = [], c = 0; c < hex.length; c += 2)
+ bytes.push(parseInt(hex.substr(c, 2), 16));
+ return bytes;
+ },
+
+ // Convert a byte array to a base-64 string
+ bytesToBase64: function(bytes) {
+ for (var base64 = [], i = 0; i < bytes.length; i += 3) {
+ var triplet = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2];
+ for (var j = 0; j < 4; j++)
+ if (i * 8 + j * 6 <= bytes.length * 8)
+ base64.push(base64map.charAt((triplet >>> 6 * (3 - j)) & 0x3F));
+ else
+ base64.push('=');
+ }
+ return base64.join('');
+ },
+
+ // Convert a base-64 string to a byte array
+ base64ToBytes: function(base64) {
+ // Remove non-base-64 characters
+ base64 = base64.replace(/[^A-Z0-9+\/]/ig, '');
+
+ for (var bytes = [], i = 0, imod4 = 0; i < base64.length;
+ imod4 = ++i % 4) {
+ if (imod4 == 0) continue;
+ bytes.push(((base64map.indexOf(base64.charAt(i - 1))
+ & (Math.pow(2, -2 * imod4 + 8) - 1)) << (imod4 * 2))
+ | (base64map.indexOf(base64.charAt(i)) >>> (6 - imod4 * 2)));
+ }
+ return bytes;
+ }
+ };
+
+ module.exports = crypt;
+ })();
+
+
+/***/ },
+/* 268 */
+/***/ function(module, exports) {
+
+ var charenc = {
+ // UTF-8 encoding
+ utf8: {
+ // Convert a string to a byte array
+ stringToBytes: function(str) {
+ return charenc.bin.stringToBytes(unescape(encodeURIComponent(str)));
+ },
+
+ // Convert a byte array to a string
+ bytesToString: function(bytes) {
+ return decodeURIComponent(escape(charenc.bin.bytesToString(bytes)));
+ }
+ },
+
+ // Binary encoding
+ bin: {
+ // Convert a string to a byte array
+ stringToBytes: function(str) {
+ for (var bytes = [], i = 0; i < str.length; i++)
+ bytes.push(str.charCodeAt(i) & 0xFF);
+ return bytes;
+ },
+
+ // Convert a byte array to a string
+ bytesToString: function(bytes) {
+ for (var str = [], i = 0; i < bytes.length; i++)
+ str.push(String.fromCharCode(bytes[i]));
+ return str.join('');
+ }
+ }
+ };
+
+ module.exports = charenc;
+
+
+/***/ },
+/* 269 */
+/***/ function(module, exports) {
+
+ /*!
+ * Determine if an object is a Buffer
+ *
+ * @author Feross Aboukhadijeh <feross@feross.org> <http://feross.org>
+ * @license MIT
+ */
+
+ // The _isBuffer check is for Safari 5-7 support, because it's missing
+ // Object.prototype.constructor. Remove this eventually
+ module.exports = function (obj) {
+ return obj != null && (isBuffer(obj) || isSlowBuffer(obj) || !!obj._isBuffer)
+ }
+
+ function isBuffer (obj) {
+ return !!obj.constructor && typeof obj.constructor.isBuffer === 'function' && obj.constructor.isBuffer(obj)
+ }
+
+ // For Node v0.10 support. Remove this eventually.
+ function isSlowBuffer (obj) {
+ return typeof obj.readFloatLE === 'function' && typeof obj.slice === 'function' && isBuffer(obj.slice(0, 0))
+ }
+
+
+/***/ },
+/* 270 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var { PrefsHelper } = __webpack_require__(28);
+ const { Services: { pref }} = __webpack_require__(30);
+ const { isDevelopment } = __webpack_require__(89);
+
+ if (isDevelopment()) {
+ pref("devtools.debugger.client-source-maps-enabled", true);
+ }
+
+ const prefs = new PrefsHelper("devtools", {
+ clientSourceMapsEnabled: ["Bool", "debugger.client-source-maps-enabled"],
+ });
+
+ module.exports = { prefs };
+
+
+/***/ },
+/* 271 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /* 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/. */
+ /* global window gThreadClient setNamedTimeout services EVENTS */
+ /* eslint no-shadow: 0 */
+
+ /**
+ * Redux actions for the event listeners state
+ * @module actions/event-listeners
+ */
+
+ var constants = __webpack_require__(251);
+
+ var _require = __webpack_require__(244);
+
+ var asPaused = _require.asPaused;
+
+ var _require2 = __webpack_require__(246);
+
+ var reportException = _require2.reportException;
+
+ var _require3 = __webpack_require__(272);
+
+ var Task = _require3.Task;
+
+ // delay is in ms
+
+ var FETCH_EVENT_LISTENERS_DELAY = 200;
+
+ /**
+ * @memberof actions/event-listeners
+ * @static
+ */
+ function fetchEventListeners() {
+ return (dispatch, getState) => {
+ // Make sure we"re not sending a batch of closely repeated requests.
+ // This can easily happen whenever new sources are fetched.
+ setNamedTimeout("event-listeners-fetch", FETCH_EVENT_LISTENERS_DELAY, () => {
+ // In case there is still a request of listeners going on (it
+ // takes several RDP round trips right now), make sure we wait
+ // on a currently running request
+ if (getState().eventListeners.fetchingListeners) {
+ dispatch({
+ type: services.WAIT_UNTIL,
+ predicate: action => action.type === constants.FETCH_EVENT_LISTENERS && action.status === "done",
+ run: dispatch => dispatch(fetchEventListeners())
+ });
+ return;
+ }
+
+ dispatch({
+ type: constants.FETCH_EVENT_LISTENERS,
+ status: "begin"
+ });
+
+ asPaused(gThreadClient, _getListeners).then(listeners => {
+ // Notify that event listeners were fetched and shown in the view,
+ // and callback to resume the active thread if necessary.
+ window.emit(EVENTS.EVENT_LISTENERS_FETCHED);
+
+ dispatch({
+ type: constants.FETCH_EVENT_LISTENERS,
+ status: "done",
+ listeners: listeners
+ });
+ });
+ });
+ };
+ }
+
+ var _getListeners = Task.async(function* () {
+ var response = yield gThreadClient.eventListeners();
+
+ // Make sure all the listeners are sorted by the event type, since
+ // they"re not guaranteed to be clustered together.
+ response.listeners.sort((a, b) => a.type > b.type ? 1 : -1);
+
+ // Add all the listeners in the debugger view event linsteners container.
+ var fetchedDefinitions = new Map();
+ var listeners = [];
+ for (var listener of response.listeners) {
+ var definitionSite = void 0;
+ if (fetchedDefinitions.has(listener.function.actor)) {
+ definitionSite = fetchedDefinitions.get(listener.function.actor);
+ } else if (listener.function.class == "Function") {
+ definitionSite = yield _getDefinitionSite(listener.function);
+ if (!definitionSite) {
+ // We don"t know where this listener comes from so don"t show it in
+ // the UI as breaking on it doesn"t work (bug 942899).
+ continue;
+ }
+
+ fetchedDefinitions.set(listener.function.actor, definitionSite);
+ }
+ listener.function.url = definitionSite;
+ listeners.push(listener);
+ }
+ fetchedDefinitions.clear();
+
+ return listeners;
+ });
+
+ var _getDefinitionSite = Task.async(function* (func) {
+ var grip = gThreadClient.pauseGrip(func);
+ var response = void 0;
+
+ try {
+ response = yield grip.getDefinitionSite();
+ } catch (e) {
+ // Don't make this error fatal, it would break the entire events pane.
+ reportException("_getDefinitionSite", e);
+ return null;
+ }
+
+ return response.source.url;
+ });
+
+ /**
+ * @memberof actions/event-listeners
+ * @static
+ * @param {string} eventNames
+ */
+ function updateEventBreakpoints(eventNames) {
+ return dispatch => {
+ setNamedTimeout("event-breakpoints-update", 0, () => {
+ gThreadClient.pauseOnDOMEvents(eventNames, function () {
+ // Notify that event breakpoints were added/removed on the server.
+ window.emit(EVENTS.EVENT_BREAKPOINTS_UPDATED);
+
+ dispatch({
+ type: constants.UPDATE_EVENT_BREAKPOINTS,
+ eventNames: eventNames
+ });
+ });
+ });
+ };
+ }
+
+ module.exports = { updateEventBreakpoints, fetchEventListeners };
+
+/***/ },
+/* 272 */
+/***/ function(module, exports) {
+
+ /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+ /* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
+ /* 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/. */
+
+ /**
+ * This object provides the public module functions.
+ */
+ var Task = {
+ // XXX: Not sure if this works in all cases...
+ async: function (task) {
+ return function () {
+ return Task.spawn(task, this, arguments);
+ };
+ },
+
+ /**
+ * Creates and starts a new task.
+ * @param task A generator function
+ * @return A promise, resolved when the task terminates
+ */
+ spawn: function (task, scope, args) {
+ return new Promise(function (resolve, reject) {
+ var iterator = task.apply(scope, args);
+
+ var callNext = lastValue => {
+ var iteration = iterator.next(lastValue);
+ Promise.resolve(iteration.value).then(value => {
+ if (iteration.done) {
+ resolve(value);
+ } else {
+ callNext(value);
+ }
+ }).catch(error => {
+ reject(error);
+ iterator.throw(error);
+ });
+ };
+
+ callNext(undefined);
+ });
+ }
+ };
+
+ module.exports = { Task };
+
+/***/ },
+/* 273 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }();
+
+ function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; }
+
+ /* 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/. */
+
+ /**
+ * Redux actions for the sources state
+ * @module actions/sources
+ */
+
+ var defer = __webpack_require__(243);
+
+ var _require = __webpack_require__(242);
+
+ var PROMISE = _require.PROMISE;
+
+ var assert = __webpack_require__(247);
+
+ var _require2 = __webpack_require__(274);
+
+ var updateFrameLocations = _require2.updateFrameLocations;
+
+ var _require3 = __webpack_require__(264);
+
+ var getOriginalURLs = _require3.getOriginalURLs;
+ var getOriginalSourceText = _require3.getOriginalSourceText;
+ var generatedToOriginalId = _require3.generatedToOriginalId;
+ var isOriginalId = _require3.isOriginalId;
+ var isGeneratedId = _require3.isGeneratedId;
+ var applySourceMap = _require3.applySourceMap;
+ var shouldSourceMap = _require3.shouldSourceMap;
+
+ var _require4 = __webpack_require__(276);
+
+ var prettyPrint = _require4.prettyPrint;
+
+
+ var constants = __webpack_require__(251);
+
+ var _require5 = __webpack_require__(89);
+
+ var isEnabled = _require5.isEnabled;
+
+ var _require6 = __webpack_require__(279);
+
+ var removeDocument = _require6.removeDocument;
+
+ var _require7 = __webpack_require__(259);
+
+ var getSource = _require7.getSource;
+ var getSourceByURL = _require7.getSourceByURL;
+ var getSourceText = _require7.getSourceText;
+ var getPendingSelectedLocation = _require7.getPendingSelectedLocation;
+ var getFrames = _require7.getFrames;
+
+
+ /**
+ * Handler for the debugger client's unsolicited newSource notification.
+ * @memberof actions/sources
+ * @static
+ */
+ function newSource(source) {
+ return (_ref) => {
+ var dispatch = _ref.dispatch;
+ var getState = _ref.getState;
+
+ if (shouldSourceMap()) {
+ dispatch(loadSourceMap(source));
+ }
+
+ dispatch({
+ type: constants.ADD_SOURCE,
+ source
+ });
+
+ // If a request has been made to show this source, go ahead and
+ // select it.
+ var pendingLocation = getPendingSelectedLocation(getState());
+ if (pendingLocation && pendingLocation.url === source.url) {
+ dispatch(selectSource(source.id, { line: pendingLocation.line }));
+ }
+ };
+ }
+
+ function newSources(sources) {
+ return (_ref2) => {
+ var dispatch = _ref2.dispatch;
+ var getState = _ref2.getState;
+
+ sources.filter(source => !getSource(getState(), source.id)).forEach(source => dispatch(newSource(source)));
+ };
+ }
+
+ /**
+ * @memberof actions/sources
+ * @static
+ */
+ function loadSourceMap(generatedSource) {
+ return (() => {
+ var _ref3 = _asyncToGenerator(function* (_ref4) {
+ var dispatch = _ref4.dispatch;
+ var getState = _ref4.getState;
+
+ var urls = yield getOriginalURLs(generatedSource);
+ if (!urls) {
+ // If this source doesn't have a sourcemap, do nothing.
+ return;
+ }
+
+ var originalSources = urls.map(function (originalUrl) {
+ return {
+ url: originalUrl,
+ id: generatedToOriginalId(generatedSource.id, originalUrl),
+ isPrettyPrinted: false
+ };
+ });
+
+ originalSources.forEach(function (s) {
+ return dispatch(newSource(s));
+ });
+ });
+
+ return function (_x) {
+ return _ref3.apply(this, arguments);
+ };
+ })();
+ }
+
+ /**
+ * Deterministically select a source that has a given URL. This will
+ * work regardless of the connection status or if the source exists
+ * yet. This exists mostly for external things to interact with the
+ * debugger.
+ *
+ * @memberof actions/sources
+ * @static
+ */
+ function selectSourceURL(url) {
+ var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
+
+ return (_ref5) => {
+ var dispatch = _ref5.dispatch;
+ var getState = _ref5.getState;
+
+ var source = getSourceByURL(getState(), url);
+ if (source) {
+ dispatch(selectSource(source.get("id"), options));
+ } else {
+ dispatch({
+ type: constants.SELECT_SOURCE_URL,
+ url: url,
+ tabIndex: options.tabIndex,
+ line: options.line
+ });
+ }
+ };
+ }
+
+ /**
+ * @memberof actions/sources
+ * @static
+ */
+ function selectSource(id) {
+ var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
+
+ return (_ref6) => {
+ var dispatch = _ref6.dispatch;
+ var getState = _ref6.getState;
+ var client = _ref6.client;
+
+ if (!client) {
+ // No connection, do nothing. This happens when the debugger is
+ // shut down too fast and it tries to display a default source.
+ return;
+ }
+
+ var source = getSource(getState(), id).toJS();
+
+ // Make sure to start a request to load the source text.
+ dispatch(loadSourceText(source));
+
+ dispatch({ type: constants.TOGGLE_FILE_SEARCH, searchOn: false });
+
+ dispatch({
+ type: constants.SELECT_SOURCE,
+ source: source,
+ tabIndex: options.tabIndex,
+ line: options.line
+ });
+ };
+ }
+
+ /**
+ * @memberof actions/sources
+ * @static
+ */
+ function closeTab(id) {
+ removeDocument(id);
+ return {
+ type: constants.CLOSE_TAB,
+ id: id
+ };
+ }
+
+ /**
+ * Toggle the pretty printing of a source's text. All subsequent calls to
+ * |getText| will return the pretty-toggled text. Nothing will happen for
+ * non-javascript files.
+ *
+ * @memberof actions/sources
+ * @static
+ * @param string id The source form from the RDP.
+ * @returns Promise
+ * A promise that resolves to [aSource, prettyText] or rejects to
+ * [aSource, error].
+ */
+ function togglePrettyPrint(sourceId) {
+ return (_ref7) => {
+ var dispatch = _ref7.dispatch;
+ var getState = _ref7.getState;
+ var client = _ref7.client;
+
+ var source = getSource(getState(), sourceId).toJS();
+ var sourceText = getSourceText(getState(), sourceId).toJS();
+
+ if (!isEnabled("prettyPrint") || sourceText.loading) {
+ return {};
+ }
+
+ assert(isGeneratedId(sourceId), "Pretty-printing only allowed on generated sources");
+
+ var url = source.url + ":formatted";
+ var id = generatedToOriginalId(source.id, url);
+ var originalSource = { url, id, isPrettyPrinted: false };
+ dispatch({
+ type: constants.ADD_SOURCE,
+ source: originalSource
+ });
+
+ return dispatch({
+ type: constants.TOGGLE_PRETTY_PRINT,
+ source: originalSource,
+ [PROMISE]: _asyncToGenerator(function* () {
+ var _ref9 = yield prettyPrint({
+ source, sourceText, url
+ });
+
+ var code = _ref9.code;
+ var mappings = _ref9.mappings;
+
+ yield applySourceMap(source.id, url, code, mappings);
+
+ var frames = yield updateFrameLocations(getFrames(getState()));
+ dispatch(selectSource(originalSource.id));
+
+ return {
+ text: code,
+ contentType: "text/javascript",
+ frames
+ };
+ })()
+ });
+ };
+ }
+
+ /**
+ * @memberof actions/sources
+ * @static
+ */
+ function loadSourceText(source) {
+ return (_ref10) => {
+ var dispatch = _ref10.dispatch;
+ var getState = _ref10.getState;
+ var client = _ref10.client;
+
+ // Fetch the source text only once.
+ var textInfo = getSourceText(getState(), source.id);
+ if (textInfo) {
+ // It's already loaded or is loading
+ return Promise.resolve(textInfo);
+ }
+
+ return dispatch({
+ type: constants.LOAD_SOURCE_TEXT,
+ source: source,
+ [PROMISE]: _asyncToGenerator(function* () {
+ if (isOriginalId(source.id)) {
+ return yield getOriginalSourceText(source);
+ }
+
+ var response = yield client.sourceContents(source.id);
+ return {
+ text: response.source,
+ contentType: response.contentType || "text/javascript"
+ };
+
+ // Automatically pretty print if enabled and the test is
+ // detected to be "minified"
+ // if (Prefs.autoPrettyPrint &&
+ // !source.isPrettyPrinted &&
+ // SourceUtils.isMinified(source.id, response.source)) {
+ // dispatch(togglePrettyPrint(source));
+ // }
+ })()
+ });
+ };
+ }
+
+ // delay is in ms
+ var FETCH_SOURCE_RESPONSE_DELAY = 200;
+
+ /**
+ * Starts fetching all the sources, silently.
+ *
+ * @memberof actions/sources
+ * @static
+ * @param array actors
+ * The urls for the sources to fetch. If fetching a source's text
+ * takes too long, it will be discarded.
+ * @returns {Promise}
+ * A promise that is resolved after source texts have been fetched.
+ */
+ function getTextForSources(actors) {
+ return (_ref12) => {
+ var dispatch = _ref12.dispatch;
+ var getState = _ref12.getState;
+
+ var deferred = defer();
+ var pending = new Set(actors);
+
+ var fetched = [];
+
+ // Can't use promise.all, because if one fetch operation is rejected, then
+ // everything is considered rejected, thus no other subsequent source will
+ // be getting fetched. We don't want that. Something like Q's allSettled
+ // would work like a charm here.
+
+ // Try to fetch as many sources as possible.
+
+ var _loop = function (actor) {
+ var source = getSource(getState(), actor);
+ dispatch(loadSourceText(source)).then((_ref21) => {
+ var text = _ref21.text;
+ var contentType = _ref21.contentType;
+
+ onFetch([source, text, contentType]);
+ }, err => {
+ onError(source, err);
+ });
+ };
+
+ for (var actor of actors) {
+ _loop(actor);
+ }
+
+ setTimeout(onTimeout, FETCH_SOURCE_RESPONSE_DELAY);
+
+ /* Called if fetching a source takes too long. */
+ function onTimeout() {
+ pending = new Set();
+ maybeFinish();
+ }
+
+ /* Called if fetching a source finishes successfully. */
+ function onFetch(_ref13) {
+ var _ref14 = _slicedToArray(_ref13, 3);
+
+ var aSource = _ref14[0];
+ var aText = _ref14[1];
+ var aContentType = _ref14[2];
+
+ // If fetching the source has previously timed out, discard it this time.
+ if (!pending.has(aSource.actor)) {
+ return;
+ }
+ pending.delete(aSource.actor);
+ fetched.push([aSource.actor, aText, aContentType]);
+ maybeFinish();
+ }
+
+ /* Called if fetching a source failed because of an error. */
+ function onError(_ref15) {
+ var _ref16 = _slicedToArray(_ref15, 2);
+
+ var aSource = _ref16[0];
+ var aError = _ref16[1];
+
+ pending.delete(aSource.actor);
+ maybeFinish();
+ }
+
+ /* Called every time something interesting
+ * happens while fetching sources.
+ */
+ function maybeFinish() {
+ if (pending.size == 0) {
+ // Sort the fetched sources alphabetically by their url.
+ if (deferred) {
+ deferred.resolve(fetched.sort((_ref17, _ref18) => {
+ var _ref20 = _slicedToArray(_ref17, 1);
+
+ var aFirst = _ref20[0];
+
+ var _ref19 = _slicedToArray(_ref18, 1);
+
+ var aSecond = _ref19[0];
+ return aFirst > aSecond ? -1 : 1;
+ }));
+ }
+ }
+ }
+
+ return deferred.promise;
+ };
+ }
+
+ module.exports = {
+ newSource,
+ newSources,
+ selectSource,
+ selectSourceURL,
+ closeTab,
+ togglePrettyPrint,
+ loadSourceText,
+ getTextForSources
+ };
+
+/***/ },
+/* 274 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var _require = __webpack_require__(275);
+
+ var Frame = _require.Frame;
+
+ var _require2 = __webpack_require__(264);
+
+ var getOriginalLocation = _require2.getOriginalLocation;
+
+
+ function updateFrameLocations(frames) {
+ if (!frames) {
+ return Promise.resolve(frames);
+ }
+ return Promise.all(frames.map(frame => {
+ return getOriginalLocation(frame.location).then(loc => {
+ return Frame.update(frame, {
+ $merge: { location: loc }
+ });
+ });
+ }));
+ }
+
+ module.exports = {
+ updateFrameLocations
+ };
+
+/***/ },
+/* 275 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var t = __webpack_require__(146);
+
+ var Location = t.struct({
+ sourceId: t.String,
+ line: t.Number,
+ column: t.union([t.Number, t.Nil])
+ }, "Location");
+
+ var Frame = t.struct({
+ id: t.String,
+ displayName: t.String,
+ location: Location,
+ this: t.union([t.Object, t.Nil]),
+ scope: t.union([t.Object, t.Nil])
+ }, "Frame");
+
+ module.exports = {
+ Frame
+ };
+
+/***/ },
+/* 276 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var prettyPrint = (() => {
+ var _ref = _asyncToGenerator(function* (_ref2) {
+ var source = _ref2.source;
+ var sourceText = _ref2.sourceText;
+ var url = _ref2.url;
+
+ var contentType = sourceText ? sourceText.contentType : null;
+ var indent = 2;
+
+ assert(isJavaScript(source.url, contentType), "Can't prettify non-javascript files.");
+
+ return yield _prettyPrint({
+ url,
+ indent,
+ source: sourceText.text
+ });
+ });
+
+ return function prettyPrint(_x) {
+ return _ref.apply(this, arguments);
+ };
+ })();
+
+ function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; }
+
+ var _require = __webpack_require__(89);
+
+ var getValue = _require.getValue;
+
+ var _require2 = __webpack_require__(244);
+
+ var workerTask = _require2.workerTask;
+
+ var _require3 = __webpack_require__(277);
+
+ var isJavaScript = _require3.isJavaScript;
+
+ var assert = __webpack_require__(247);
+
+ var prettyPrintWorker = new Worker(getValue("baseWorkerURL") + "pretty-print-worker.js");
+
+ function destroyWorker() {
+ prettyPrintWorker.terminate();
+ prettyPrintWorker = null;
+ }
+
+ var _prettyPrint = workerTask(prettyPrintWorker, "prettyPrint");
+
+ module.exports = {
+ prettyPrint,
+ destroyWorker
+ };
+
+/***/ },
+/* 277 */
+/***/ function(module, exports, __webpack_require__) {
+
+
+
+ /**
+ * Utils for working with Source URLs
+ * @module utils/source
+ */
+
+ var _require = __webpack_require__(244);
+
+ var endTruncateStr = _require.endTruncateStr;
+
+ var _require2 = __webpack_require__(278);
+
+ var basename = _require2.basename;
+
+
+ /**
+ * Trims the query part or reference identifier of a url string, if necessary.
+ *
+ * @memberof utils/source
+ * @static
+ */
+ function trimUrlQuery(url) {
+ var length = url.length;
+ var q1 = url.indexOf("?");
+ var q2 = url.indexOf("&");
+ var q3 = url.indexOf("#");
+ var q = Math.min(q1 != -1 ? q1 : length, q2 != -1 ? q2 : length, q3 != -1 ? q3 : length);
+
+ return url.slice(0, q);
+ }
+
+ /**
+ * Returns true if the specified url and/or content type are specific to
+ * javascript files.
+ *
+ * @return boolean
+ * True if the source is likely javascript.
+ *
+ * @memberof utils/source
+ * @static
+ */
+ function isJavaScript(url) {
+ var contentType = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "";
+
+ return url && /\.(jsm|js)?$/.test(trimUrlQuery(url)) || contentType.includes("javascript");
+ }
+
+ /**
+ * @memberof utils/source
+ * @static
+ */
+ function isPretty(source) {
+ return source.url ? /formatted$/.test(source.url) : false;
+ }
+
+ /**
+ * Show a source url's filename.
+ * If the source does not have a url, use the source id.
+ *
+ * @memberof utils/source
+ * @static
+ */
+ function getFilename(source) {
+ var url = source.url;
+ var id = source.id;
+
+ if (!url) {
+ var sourceId = id.split("/")[1];
+ return `SOURCE${ sourceId }`;
+ }
+
+ var name = basename(source.url || "") || "(index)";
+ return endTruncateStr(name, 50);
+ }
+
+ module.exports = {
+ isJavaScript,
+ isPretty,
+ getFilename
+ };
+
+/***/ },
+/* 278 */
+/***/ function(module, exports) {
+
+ function basename(path) {
+ return path.split("/").pop();
+ }
+
+ function dirname(path) {
+ var idx = path.lastIndexOf("/");
+ return path.slice(0, idx);
+ }
+
+ function isURL(str) {
+ return str.indexOf("://") !== -1;
+ }
+
+ function isAbsolute(str) {
+ return str[0] === "/";
+ }
+
+ function join(base, dir) {
+ return base + "/" + dir;
+ }
+
+ module.exports = {
+ basename, dirname, isURL, isAbsolute, join
+ };
+
+/***/ },
+/* 279 */
+/***/ function(module, exports) {
+
+ var sourceDocs = {};
+
+ function getDocument(key) {
+ return sourceDocs[key];
+ }
+
+ function setDocument(key, doc) {
+ sourceDocs[key] = doc;
+ }
+
+ function removeDocument(key) {
+ delete sourceDocs[key];
+ }
+
+ function clearDocuments() {
+ sourceDocs = {};
+ }
+
+ module.exports = {
+ getDocument,
+ setDocument,
+ removeDocument,
+ clearDocuments
+ };
+
+/***/ },
+/* 280 */
+/***/ function(module, exports, __webpack_require__) {
+
+ function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; }
+
+ var constants = __webpack_require__(251);
+
+ var _require = __webpack_require__(273);
+
+ var selectSource = _require.selectSource;
+
+ var _require2 = __webpack_require__(242);
+
+ var PROMISE = _require2.PROMISE;
+
+ var _require3 = __webpack_require__(259);
+
+ var getExpressions = _require3.getExpressions;
+ var getSelectedFrame = _require3.getSelectedFrame;
+
+ var _require4 = __webpack_require__(274);
+
+ var updateFrameLocations = _require4.updateFrameLocations;
+
+ /**
+ * Redux actions for the pause state
+ * @module actions/pause
+ */
+
+ /**
+ * Debugger has just resumed
+ *
+ * @memberof actions/pause
+ * @static
+ */
+
+ function resumed() {
+ return (_ref) => {
+ var dispatch = _ref.dispatch;
+ var client = _ref.client;
+
+ return dispatch({
+ type: constants.RESUME,
+ value: undefined
+ });
+ };
+ }
+
+ /**
+ * Debugger has just paused
+ *
+ * @param {object} pauseInfo
+ * @memberof actions/pause
+ * @static
+ */
+ function paused(pauseInfo) {
+ return (() => {
+ var _ref2 = _asyncToGenerator(function* (_ref3) {
+ var dispatch = _ref3.dispatch;
+ var getState = _ref3.getState;
+ var client = _ref3.client;
+ var frames = pauseInfo.frames;
+ var why = pauseInfo.why;
+
+ frames = yield updateFrameLocations(frames);
+ var frame = frames[0];
+
+ dispatch({
+ type: constants.PAUSED,
+ pauseInfo: { why, frame },
+ frames: frames,
+ selectedFrameId: frame.id
+ });
+
+ dispatch(evaluateExpressions());
+
+ dispatch(selectSource(frame.location.sourceId, { line: frame.location.line }));
+ });
+
+ return function (_x) {
+ return _ref2.apply(this, arguments);
+ };
+ })();
+ }
+
+ /**
+ *
+ * @memberof actions/pause
+ * @static
+ */
+ function pauseOnExceptions(shouldPauseOnExceptions, shouldIgnoreCaughtExceptions) {
+ return (_ref4) => {
+ var dispatch = _ref4.dispatch;
+ var client = _ref4.client;
+
+ dispatch({
+ type: constants.PAUSE_ON_EXCEPTIONS,
+ shouldPauseOnExceptions,
+ shouldIgnoreCaughtExceptions,
+ [PROMISE]: client.pauseOnExceptions(shouldPauseOnExceptions, shouldIgnoreCaughtExceptions)
+ });
+ };
+ }
+
+ /**
+ * Debugger commands like stepOver, stepIn, stepUp
+ *
+ * @param string $0.type
+ * @memberof actions/pause
+ * @static
+ */
+ function command(_ref5) {
+ var type = _ref5.type;
+
+ return (_ref6) => {
+ var dispatch = _ref6.dispatch;
+ var client = _ref6.client;
+
+ // execute debugger thread command e.g. stepIn, stepOver
+ client[type]();
+
+ return dispatch({
+ type: constants.COMMAND,
+ value: undefined
+ });
+ };
+ }
+
+ /**
+ * StepIn
+ * @memberof actions/pause
+ * @static
+ * @returns {Function} {@link command}
+ */
+ function stepIn() {
+ return command({ type: "stepIn" });
+ }
+
+ /**
+ * stepOver
+ * @memberof actions/pause
+ * @static
+ * @returns {Function} {@link command}
+ */
+ function stepOver() {
+ return command({ type: "stepOver" });
+ }
+
+ /**
+ * stepOut
+ * @memberof actions/pause
+ * @static
+ * @returns {Function} {@link command}
+ */
+ function stepOut() {
+ return command({ type: "stepOut" });
+ }
+
+ /**
+ * resume
+ * @memberof actions/pause
+ * @static
+ * @returns {Function} {@link command}
+ */
+ function resume() {
+ return command({ type: "resume" });
+ }
+
+ /**
+ * Debugger breakOnNext command.
+ * It's different from the comand action because we also want to
+ * highlight the pause icon.
+ *
+ * @memberof actions/pause
+ * @static
+ */
+ function breakOnNext() {
+ return (_ref7) => {
+ var dispatch = _ref7.dispatch;
+ var client = _ref7.client;
+
+ client.breakOnNext();
+
+ return dispatch({
+ type: constants.BREAK_ON_NEXT,
+ value: true
+ });
+ };
+ }
+
+ /**
+ * Select a frame
+ *
+ * @param frame
+ * @memberof actions/pause
+ * @static
+ */
+ function selectFrame(frame) {
+ return (_ref8) => {
+ var dispatch = _ref8.dispatch;
+
+ dispatch(selectSource(frame.location.sourceId, { line: frame.location.line }));
+ dispatch({
+ type: constants.SELECT_FRAME,
+ frame
+ });
+ };
+ }
+
+ /**
+ * Load an object.
+ *
+ * @param grip
+ * TODO: Right now this if Firefox specific and is not implemented
+ * for Chrome, which is why it takes a grip.
+ * @memberof actions/pause
+ * @static
+ */
+ function loadObjectProperties(grip) {
+ return (_ref9) => {
+ var dispatch = _ref9.dispatch;
+ var client = _ref9.client;
+
+ dispatch({
+ type: constants.LOAD_OBJECT_PROPERTIES,
+ objectId: grip.actor,
+ [PROMISE]: client.getProperties(grip)
+ });
+ };
+ }
+
+ /**
+ * Add expression for debugger to watch
+ *
+ * @param {object} expression
+ * @param {number} expression.id
+ * @memberof actions/pause
+ * @static
+ */
+ function addExpression(expression) {
+ return (_ref10) => {
+ var dispatch = _ref10.dispatch;
+ var getState = _ref10.getState;
+
+ var id = expression.id !== undefined ? parseInt(expression.id, 10) : getExpressions(getState()).toSeq().size++;
+ dispatch({
+ type: constants.ADD_EXPRESSION,
+ id: id,
+ input: expression.input
+ });
+ dispatch(evaluateExpressions());
+ };
+ }
+
+ /**
+ *
+ * @param {object} expression
+ * @param {number} expression.id
+ * @memberof actions/pause
+ * @static
+ */
+ function updateExpression(expression) {
+ return (_ref11) => {
+ var dispatch = _ref11.dispatch;
+
+ dispatch({
+ type: constants.UPDATE_EXPRESSION,
+ id: expression.id,
+ input: expression.input
+ });
+ };
+ }
+
+ /**
+ *
+ * @param {object} expression
+ * @param {number} expression.id
+ * @memberof actions/pause
+ * @static
+ */
+ function deleteExpression(expression) {
+ return (_ref12) => {
+ var dispatch = _ref12.dispatch;
+
+ dispatch({
+ type: constants.DELETE_EXPRESSION,
+ id: expression.id
+ });
+ };
+ }
+
+ /**
+ *
+ * @memberof actions/pause
+ * @static
+ */
+ function evaluateExpressions() {
+ return (() => {
+ var _ref13 = _asyncToGenerator(function* (_ref14) {
+ var dispatch = _ref14.dispatch;
+ var getState = _ref14.getState;
+ var client = _ref14.client;
+
+ var selectedFrame = getSelectedFrame(getState());
+ if (!selectedFrame) {
+ return;
+ }
+
+ var frameId = selectedFrame.id;
+
+ for (var expression of getExpressions(getState())) {
+ yield dispatch({
+ type: constants.EVALUATE_EXPRESSION,
+ id: expression.id,
+ input: expression.input,
+ [PROMISE]: client.evaluate(expression.input, { frameId })
+ });
+ }
+ });
+
+ return function (_x2) {
+ return _ref13.apply(this, arguments);
+ };
+ })();
+ }
+
+ module.exports = {
+ addExpression,
+ updateExpression,
+ deleteExpression,
+ evaluateExpressions,
+ resumed,
+ paused,
+ pauseOnExceptions,
+ command,
+ stepIn,
+ stepOut,
+ stepOver,
+ resume,
+ breakOnNext,
+ selectFrame,
+ loadObjectProperties
+ };
+
+/***/ },
+/* 281 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var constants = __webpack_require__(251);
+
+ var _require = __webpack_require__(264);
+
+ var clearSourceMaps = _require.clearSourceMaps;
+
+ var _require2 = __webpack_require__(279);
+
+ var clearDocuments = _require2.clearDocuments;
+
+ /**
+ * Redux actions for the navigation state
+ * @module actions/navigation
+ */
+
+ /**
+ * @memberof actions/navigation
+ * @static
+ */
+
+ function willNavigate() {
+ clearSourceMaps();
+ clearDocuments();
+
+ return { type: constants.NAVIGATE };
+ }
+
+ /**
+ * @memberof actions/navigation
+ * @static
+ */
+ function navigated() {
+ return (_ref) => {
+ // We need to load all the sources again because they might have
+ // come from bfcache, so we won't get a `newSource` notification.
+ //
+ // TODO: This seems to be buggy on the debugger server side. When
+ // the page is loaded from bfcache, we still get sources from the
+ // *previous* page as well. For now, emulate the current debugger
+ // behavior by not showing sources loaded by bfcache.
+ // return dispatch(sources.loadSources());
+
+ var dispatch = _ref.dispatch;
+ };
+ }
+
+ module.exports = {
+ willNavigate,
+ navigated
+ };
+
+/***/ },
+/* 282 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var constants = __webpack_require__(251);
+
+ function toggleFileSearch(searchOn) {
+ return {
+ type: constants.TOGGLE_FILE_SEARCH,
+ searchOn
+ };
+ }
+
+ module.exports = {
+ toggleFileSearch
+ };
+
+/***/ },
+/* 283 */
+/***/ function(module, exports) {
+
+ // removed by extract-text-webpack-plugin
+
+/***/ },
+/* 284 */,
+/* 285 */,
+/* 286 */
+/***/ function(module, exports) {
+
+ // removed by extract-text-webpack-plugin
+
+/***/ },
+/* 287 */,
+/* 288 */
+/***/ function(module, exports) {
+
+ // removed by extract-text-webpack-plugin
+
+/***/ },
+/* 289 */,
+/* 290 */
+/***/ function(module, exports) {
+
+ // removed by extract-text-webpack-plugin
+
+/***/ },
+/* 291 */,
+/* 292 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var React = __webpack_require__(2);
+ var dom = React.DOM;
+ var PropTypes = React.PropTypes;
+ var createFactory = React.createFactory;
+
+ var _require = __webpack_require__(19);
+
+ var connect = _require.connect;
+
+ var _require2 = __webpack_require__(3);
+
+ var bindActionCreators = _require2.bindActionCreators;
+
+ var actions = __webpack_require__(262);
+
+ var _require3 = __webpack_require__(259);
+
+ var getSources = _require3.getSources;
+ var getSelectedSource = _require3.getSelectedSource;
+ var getFileSearchState = _require3.getFileSearchState;
+
+ var _require4 = __webpack_require__(244);
+
+ var endTruncateStr = _require4.endTruncateStr;
+
+ var _require5 = __webpack_require__(293);
+
+ var parseURL = _require5.parse;
+
+ var _require6 = __webpack_require__(277);
+
+ var isPretty = _require6.isPretty;
+
+
+ __webpack_require__(298);
+
+ var Autocomplete = createFactory(__webpack_require__(300));
+
+ function searchResults(sources) {
+ function getSourcePath(source) {
+ var _parseURL = parseURL(source.get("url"));
+
+ var path = _parseURL.path;
+ var href = _parseURL.href;
+ // for URLs like "about:home" the path is null so we pass the full href
+
+ return endTruncateStr(path || href, 50);
+ }
+
+ return sources.valueSeq().filter(source => !isPretty(source.toJS()) && source.get("url")).map(source => ({
+ value: getSourcePath(source),
+ title: getSourcePath(source).split("/").pop(),
+ subtitle: getSourcePath(source),
+ id: source.get("id")
+ })).toJS();
+ }
+
+ var Search = React.createClass({
+ propTypes: {
+ sources: PropTypes.object,
+ selectSource: PropTypes.func,
+ selectedSource: PropTypes.object,
+ toggleFileSearch: PropTypes.func,
+ searchOn: PropTypes.bool
+ },
+
+ contextTypes: {
+ shortcuts: PropTypes.object
+ },
+
+ displayName: "Search",
+
+ getInitialState() {
+ return {
+ inputValue: ""
+ };
+ },
+
+ componentWillUnmount() {
+ var shortcuts = this.context.shortcuts;
+ shortcuts.off("CmdOrCtrl+P", this.toggle);
+ shortcuts.off("Escape", this.onEscape);
+ },
+
+ componentDidMount() {
+ var shortcuts = this.context.shortcuts;
+ shortcuts.on("CmdOrCtrl+P", this.toggle);
+ shortcuts.on("Escape", this.onEscape);
+ },
+
+ toggle(key, e) {
+ e.preventDefault();
+ this.props.toggleFileSearch(!this.props.searchOn);
+ },
+
+ onEscape(shortcut, e) {
+ if (this.props.searchOn) {
+ e.preventDefault();
+ this.setState({ inputValue: "" });
+ this.props.toggleFileSearch(false);
+ }
+ },
+
+ close() {
+ var inputValue = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : "";
+
+ this.setState({ inputValue });
+ this.props.toggleFileSearch(false);
+ },
+
+ render() {
+ return this.props.searchOn ? dom.div({ className: "search-container" }, Autocomplete({
+ selectItem: result => {
+ this.props.selectSource(result.id);
+ this.setState({ inputValue: "" });
+ this.props.toggleFileSearch(false);
+ },
+ handleClose: this.close,
+ items: searchResults(this.props.sources),
+ inputValue: this.state.inputValue
+ })) : null;
+ }
+
+ });
+
+ module.exports = connect(state => ({
+ sources: getSources(state),
+ selectedSource: getSelectedSource(state),
+ searchOn: getFileSearchState(state)
+ }), dispatch => bindActionCreators(actions, dispatch))(Search);
+
+/***/ },
+/* 293 */
+/***/ function(module, exports, __webpack_require__) {
+
+ // Copyright Joyent, Inc. and other Node contributors.
+ //
+ // Permission is hereby granted, free of charge, to any person obtaining a
+ // copy of this software and associated documentation files (the
+ // "Software"), to deal in the Software without restriction, including
+ // without limitation the rights to use, copy, modify, merge, publish,
+ // distribute, sublicense, and/or sell copies of the Software, and to permit
+ // persons to whom the Software is furnished to do so, subject to the
+ // following conditions:
+ //
+ // The above copyright notice and this permission notice shall be included
+ // in all copies or substantial portions of the Software.
+ //
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+ // NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+ // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+ // USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+ var punycode = __webpack_require__(294);
+
+ exports.parse = urlParse;
+ exports.resolve = urlResolve;
+ exports.resolveObject = urlResolveObject;
+ exports.format = urlFormat;
+
+ exports.Url = Url;
+
+ function Url() {
+ this.protocol = null;
+ this.slashes = null;
+ this.auth = null;
+ this.host = null;
+ this.port = null;
+ this.hostname = null;
+ this.hash = null;
+ this.search = null;
+ this.query = null;
+ this.pathname = null;
+ this.path = null;
+ this.href = null;
+ }
+
+ // Reference: RFC 3986, RFC 1808, RFC 2396
+
+ // define these here so at least they only have to be
+ // compiled once on the first module load.
+ var protocolPattern = /^([a-z0-9.+-]+:)/i,
+ portPattern = /:[0-9]*$/,
+
+ // RFC 2396: characters reserved for delimiting URLs.
+ // We actually just auto-escape these.
+ delims = ['<', '>', '"', '`', ' ', '\r', '\n', '\t'],
+
+ // RFC 2396: characters not allowed for various reasons.
+ unwise = ['{', '}', '|', '\\', '^', '`'].concat(delims),
+
+ // Allowed by RFCs, but cause of XSS attacks. Always escape these.
+ autoEscape = ['\''].concat(unwise),
+ // Characters that are never ever allowed in a hostname.
+ // Note that any invalid chars are also handled, but these
+ // are the ones that are *expected* to be seen, so we fast-path
+ // them.
+ nonHostChars = ['%', '/', '?', ';', '#'].concat(autoEscape),
+ hostEndingChars = ['/', '?', '#'],
+ hostnameMaxLen = 255,
+ hostnamePartPattern = /^[a-z0-9A-Z_-]{0,63}$/,
+ hostnamePartStart = /^([a-z0-9A-Z_-]{0,63})(.*)$/,
+ // protocols that can allow "unsafe" and "unwise" chars.
+ unsafeProtocol = {
+ 'javascript': true,
+ 'javascript:': true
+ },
+ // protocols that never have a hostname.
+ hostlessProtocol = {
+ 'javascript': true,
+ 'javascript:': true
+ },
+ // protocols that always contain a // bit.
+ slashedProtocol = {
+ 'http': true,
+ 'https': true,
+ 'ftp': true,
+ 'gopher': true,
+ 'file': true,
+ 'http:': true,
+ 'https:': true,
+ 'ftp:': true,
+ 'gopher:': true,
+ 'file:': true
+ },
+ querystring = __webpack_require__(295);
+
+ function urlParse(url, parseQueryString, slashesDenoteHost) {
+ if (url && isObject(url) && url instanceof Url) return url;
+
+ var u = new Url;
+ u.parse(url, parseQueryString, slashesDenoteHost);
+ return u;
+ }
+
+ Url.prototype.parse = function(url, parseQueryString, slashesDenoteHost) {
+ if (!isString(url)) {
+ throw new TypeError("Parameter 'url' must be a string, not " + typeof url);
+ }
+
+ var rest = url;
+
+ // trim before proceeding.
+ // This is to support parse stuff like " http://foo.com \n"
+ rest = rest.trim();
+
+ var proto = protocolPattern.exec(rest);
+ if (proto) {
+ proto = proto[0];
+ var lowerProto = proto.toLowerCase();
+ this.protocol = lowerProto;
+ rest = rest.substr(proto.length);
+ }
+
+ // figure out if it's got a host
+ // user@server is *always* interpreted as a hostname, and url
+ // resolution will treat //foo/bar as host=foo,path=bar because that's
+ // how the browser resolves relative URLs.
+ if (slashesDenoteHost || proto || rest.match(/^\/\/[^@\/]+@[^@\/]+/)) {
+ var slashes = rest.substr(0, 2) === '//';
+ if (slashes && !(proto && hostlessProtocol[proto])) {
+ rest = rest.substr(2);
+ this.slashes = true;
+ }
+ }
+
+ if (!hostlessProtocol[proto] &&
+ (slashes || (proto && !slashedProtocol[proto]))) {
+
+ // there's a hostname.
+ // the first instance of /, ?, ;, or # ends the host.
+ //
+ // If there is an @ in the hostname, then non-host chars *are* allowed
+ // to the left of the last @ sign, unless some host-ending character
+ // comes *before* the @-sign.
+ // URLs are obnoxious.
+ //
+ // ex:
+ // http://a@b@c/ => user:a@b host:c
+ // http://a@b?@c => user:a host:c path:/?@c
+
+ // v0.12 TODO(isaacs): This is not quite how Chrome does things.
+ // Review our test case against browsers more comprehensively.
+
+ // find the first instance of any hostEndingChars
+ var hostEnd = -1;
+ for (var i = 0; i < hostEndingChars.length; i++) {
+ var hec = rest.indexOf(hostEndingChars[i]);
+ if (hec !== -1 && (hostEnd === -1 || hec < hostEnd))
+ hostEnd = hec;
+ }
+
+ // at this point, either we have an explicit point where the
+ // auth portion cannot go past, or the last @ char is the decider.
+ var auth, atSign;
+ if (hostEnd === -1) {
+ // atSign can be anywhere.
+ atSign = rest.lastIndexOf('@');
+ } else {
+ // atSign must be in auth portion.
+ // http://a@b/c@d => host:b auth:a path:/c@d
+ atSign = rest.lastIndexOf('@', hostEnd);
+ }
+
+ // Now we have a portion which is definitely the auth.
+ // Pull that off.
+ if (atSign !== -1) {
+ auth = rest.slice(0, atSign);
+ rest = rest.slice(atSign + 1);
+ this.auth = decodeURIComponent(auth);
+ }
+
+ // the host is the remaining to the left of the first non-host char
+ hostEnd = -1;
+ for (var i = 0; i < nonHostChars.length; i++) {
+ var hec = rest.indexOf(nonHostChars[i]);
+ if (hec !== -1 && (hostEnd === -1 || hec < hostEnd))
+ hostEnd = hec;
+ }
+ // if we still have not hit it, then the entire thing is a host.
+ if (hostEnd === -1)
+ hostEnd = rest.length;
+
+ this.host = rest.slice(0, hostEnd);
+ rest = rest.slice(hostEnd);
+
+ // pull out port.
+ this.parseHost();
+
+ // we've indicated that there is a hostname,
+ // so even if it's empty, it has to be present.
+ this.hostname = this.hostname || '';
+
+ // if hostname begins with [ and ends with ]
+ // assume that it's an IPv6 address.
+ var ipv6Hostname = this.hostname[0] === '[' &&
+ this.hostname[this.hostname.length - 1] === ']';
+
+ // validate a little.
+ if (!ipv6Hostname) {
+ var hostparts = this.hostname.split(/\./);
+ for (var i = 0, l = hostparts.length; i < l; i++) {
+ var part = hostparts[i];
+ if (!part) continue;
+ if (!part.match(hostnamePartPattern)) {
+ var newpart = '';
+ for (var j = 0, k = part.length; j < k; j++) {
+ if (part.charCodeAt(j) > 127) {
+ // we replace non-ASCII char with a temporary placeholder
+ // we need this to make sure size of hostname is not
+ // broken by replacing non-ASCII by nothing
+ newpart += 'x';
+ } else {
+ newpart += part[j];
+ }
+ }
+ // we test again with ASCII char only
+ if (!newpart.match(hostnamePartPattern)) {
+ var validParts = hostparts.slice(0, i);
+ var notHost = hostparts.slice(i + 1);
+ var bit = part.match(hostnamePartStart);
+ if (bit) {
+ validParts.push(bit[1]);
+ notHost.unshift(bit[2]);
+ }
+ if (notHost.length) {
+ rest = '/' + notHost.join('.') + rest;
+ }
+ this.hostname = validParts.join('.');
+ break;
+ }
+ }
+ }
+ }
+
+ if (this.hostname.length > hostnameMaxLen) {
+ this.hostname = '';
+ } else {
+ // hostnames are always lower case.
+ this.hostname = this.hostname.toLowerCase();
+ }
+
+ if (!ipv6Hostname) {
+ // IDNA Support: Returns a puny coded representation of "domain".
+ // It only converts the part of the domain name that
+ // has non ASCII characters. I.e. it dosent matter if
+ // you call it with a domain that already is in ASCII.
+ var domainArray = this.hostname.split('.');
+ var newOut = [];
+ for (var i = 0; i < domainArray.length; ++i) {
+ var s = domainArray[i];
+ newOut.push(s.match(/[^A-Za-z0-9_-]/) ?
+ 'xn--' + punycode.encode(s) : s);
+ }
+ this.hostname = newOut.join('.');
+ }
+
+ var p = this.port ? ':' + this.port : '';
+ var h = this.hostname || '';
+ this.host = h + p;
+ this.href += this.host;
+
+ // strip [ and ] from the hostname
+ // the host field still retains them, though
+ if (ipv6Hostname) {
+ this.hostname = this.hostname.substr(1, this.hostname.length - 2);
+ if (rest[0] !== '/') {
+ rest = '/' + rest;
+ }
+ }
+ }
+
+ // now rest is set to the post-host stuff.
+ // chop off any delim chars.
+ if (!unsafeProtocol[lowerProto]) {
+
+ // First, make 100% sure that any "autoEscape" chars get
+ // escaped, even if encodeURIComponent doesn't think they
+ // need to be.
+ for (var i = 0, l = autoEscape.length; i < l; i++) {
+ var ae = autoEscape[i];
+ var esc = encodeURIComponent(ae);
+ if (esc === ae) {
+ esc = escape(ae);
+ }
+ rest = rest.split(ae).join(esc);
+ }
+ }
+
+
+ // chop off from the tail first.
+ var hash = rest.indexOf('#');
+ if (hash !== -1) {
+ // got a fragment string.
+ this.hash = rest.substr(hash);
+ rest = rest.slice(0, hash);
+ }
+ var qm = rest.indexOf('?');
+ if (qm !== -1) {
+ this.search = rest.substr(qm);
+ this.query = rest.substr(qm + 1);
+ if (parseQueryString) {
+ this.query = querystring.parse(this.query);
+ }
+ rest = rest.slice(0, qm);
+ } else if (parseQueryString) {
+ // no query string, but parseQueryString still requested
+ this.search = '';
+ this.query = {};
+ }
+ if (rest) this.pathname = rest;
+ if (slashedProtocol[lowerProto] &&
+ this.hostname && !this.pathname) {
+ this.pathname = '/';
+ }
+
+ //to support http.request
+ if (this.pathname || this.search) {
+ var p = this.pathname || '';
+ var s = this.search || '';
+ this.path = p + s;
+ }
+
+ // finally, reconstruct the href based on what has been validated.
+ this.href = this.format();
+ return this;
+ };
+
+ // format a parsed object into a url string
+ function urlFormat(obj) {
+ // ensure it's an object, and not a string url.
+ // If it's an obj, this is a no-op.
+ // this way, you can call url_format() on strings
+ // to clean up potentially wonky urls.
+ if (isString(obj)) obj = urlParse(obj);
+ if (!(obj instanceof Url)) return Url.prototype.format.call(obj);
+ return obj.format();
+ }
+
+ Url.prototype.format = function() {
+ var auth = this.auth || '';
+ if (auth) {
+ auth = encodeURIComponent(auth);
+ auth = auth.replace(/%3A/i, ':');
+ auth += '@';
+ }
+
+ var protocol = this.protocol || '',
+ pathname = this.pathname || '',
+ hash = this.hash || '',
+ host = false,
+ query = '';
+
+ if (this.host) {
+ host = auth + this.host;
+ } else if (this.hostname) {
+ host = auth + (this.hostname.indexOf(':') === -1 ?
+ this.hostname :
+ '[' + this.hostname + ']');
+ if (this.port) {
+ host += ':' + this.port;
+ }
+ }
+
+ if (this.query &&
+ isObject(this.query) &&
+ Object.keys(this.query).length) {
+ query = querystring.stringify(this.query);
+ }
+
+ var search = this.search || (query && ('?' + query)) || '';
+
+ if (protocol && protocol.substr(-1) !== ':') protocol += ':';
+
+ // only the slashedProtocols get the //. Not mailto:, xmpp:, etc.
+ // unless they had them to begin with.
+ if (this.slashes ||
+ (!protocol || slashedProtocol[protocol]) && host !== false) {
+ host = '//' + (host || '');
+ if (pathname && pathname.charAt(0) !== '/') pathname = '/' + pathname;
+ } else if (!host) {
+ host = '';
+ }
+
+ if (hash && hash.charAt(0) !== '#') hash = '#' + hash;
+ if (search && search.charAt(0) !== '?') search = '?' + search;
+
+ pathname = pathname.replace(/[?#]/g, function(match) {
+ return encodeURIComponent(match);
+ });
+ search = search.replace('#', '%23');
+
+ return protocol + host + pathname + search + hash;
+ };
+
+ function urlResolve(source, relative) {
+ return urlParse(source, false, true).resolve(relative);
+ }
+
+ Url.prototype.resolve = function(relative) {
+ return this.resolveObject(urlParse(relative, false, true)).format();
+ };
+
+ function urlResolveObject(source, relative) {
+ if (!source) return relative;
+ return urlParse(source, false, true).resolveObject(relative);
+ }
+
+ Url.prototype.resolveObject = function(relative) {
+ if (isString(relative)) {
+ var rel = new Url();
+ rel.parse(relative, false, true);
+ relative = rel;
+ }
+
+ var result = new Url();
+ Object.keys(this).forEach(function(k) {
+ result[k] = this[k];
+ }, this);
+
+ // hash is always overridden, no matter what.
+ // even href="" will remove it.
+ result.hash = relative.hash;
+
+ // if the relative url is empty, then there's nothing left to do here.
+ if (relative.href === '') {
+ result.href = result.format();
+ return result;
+ }
+
+ // hrefs like //foo/bar always cut to the protocol.
+ if (relative.slashes && !relative.protocol) {
+ // take everything except the protocol from relative
+ Object.keys(relative).forEach(function(k) {
+ if (k !== 'protocol')
+ result[k] = relative[k];
+ });
+
+ //urlParse appends trailing / to urls like http://www.example.com
+ if (slashedProtocol[result.protocol] &&
+ result.hostname && !result.pathname) {
+ result.path = result.pathname = '/';
+ }
+
+ result.href = result.format();
+ return result;
+ }
+
+ if (relative.protocol && relative.protocol !== result.protocol) {
+ // if it's a known url protocol, then changing
+ // the protocol does weird things
+ // first, if it's not file:, then we MUST have a host,
+ // and if there was a path
+ // to begin with, then we MUST have a path.
+ // if it is file:, then the host is dropped,
+ // because that's known to be hostless.
+ // anything else is assumed to be absolute.
+ if (!slashedProtocol[relative.protocol]) {
+ Object.keys(relative).forEach(function(k) {
+ result[k] = relative[k];
+ });
+ result.href = result.format();
+ return result;
+ }
+
+ result.protocol = relative.protocol;
+ if (!relative.host && !hostlessProtocol[relative.protocol]) {
+ var relPath = (relative.pathname || '').split('/');
+ while (relPath.length && !(relative.host = relPath.shift()));
+ if (!relative.host) relative.host = '';
+ if (!relative.hostname) relative.hostname = '';
+ if (relPath[0] !== '') relPath.unshift('');
+ if (relPath.length < 2) relPath.unshift('');
+ result.pathname = relPath.join('/');
+ } else {
+ result.pathname = relative.pathname;
+ }
+ result.search = relative.search;
+ result.query = relative.query;
+ result.host = relative.host || '';
+ result.auth = relative.auth;
+ result.hostname = relative.hostname || relative.host;
+ result.port = relative.port;
+ // to support http.request
+ if (result.pathname || result.search) {
+ var p = result.pathname || '';
+ var s = result.search || '';
+ result.path = p + s;
+ }
+ result.slashes = result.slashes || relative.slashes;
+ result.href = result.format();
+ return result;
+ }
+
+ var isSourceAbs = (result.pathname && result.pathname.charAt(0) === '/'),
+ isRelAbs = (
+ relative.host ||
+ relative.pathname && relative.pathname.charAt(0) === '/'
+ ),
+ mustEndAbs = (isRelAbs || isSourceAbs ||
+ (result.host && relative.pathname)),
+ removeAllDots = mustEndAbs,
+ srcPath = result.pathname && result.pathname.split('/') || [],
+ relPath = relative.pathname && relative.pathname.split('/') || [],
+ psychotic = result.protocol && !slashedProtocol[result.protocol];
+
+ // if the url is a non-slashed url, then relative
+ // links like ../.. should be able
+ // to crawl up to the hostname, as well. This is strange.
+ // result.protocol has already been set by now.
+ // Later on, put the first path part into the host field.
+ if (psychotic) {
+ result.hostname = '';
+ result.port = null;
+ if (result.host) {
+ if (srcPath[0] === '') srcPath[0] = result.host;
+ else srcPath.unshift(result.host);
+ }
+ result.host = '';
+ if (relative.protocol) {
+ relative.hostname = null;
+ relative.port = null;
+ if (relative.host) {
+ if (relPath[0] === '') relPath[0] = relative.host;
+ else relPath.unshift(relative.host);
+ }
+ relative.host = null;
+ }
+ mustEndAbs = mustEndAbs && (relPath[0] === '' || srcPath[0] === '');
+ }
+
+ if (isRelAbs) {
+ // it's absolute.
+ result.host = (relative.host || relative.host === '') ?
+ relative.host : result.host;
+ result.hostname = (relative.hostname || relative.hostname === '') ?
+ relative.hostname : result.hostname;
+ result.search = relative.search;
+ result.query = relative.query;
+ srcPath = relPath;
+ // fall through to the dot-handling below.
+ } else if (relPath.length) {
+ // it's relative
+ // throw away the existing file, and take the new path instead.
+ if (!srcPath) srcPath = [];
+ srcPath.pop();
+ srcPath = srcPath.concat(relPath);
+ result.search = relative.search;
+ result.query = relative.query;
+ } else if (!isNullOrUndefined(relative.search)) {
+ // just pull out the search.
+ // like href='?foo'.
+ // Put this after the other two cases because it simplifies the booleans
+ if (psychotic) {
+ result.hostname = result.host = srcPath.shift();
+ //occationaly the auth can get stuck only in host
+ //this especialy happens in cases like
+ //url.resolveObject('mailto:local1@domain1', 'local2@domain2')
+ var authInHost = result.host && result.host.indexOf('@') > 0 ?
+ result.host.split('@') : false;
+ if (authInHost) {
+ result.auth = authInHost.shift();
+ result.host = result.hostname = authInHost.shift();
+ }
+ }
+ result.search = relative.search;
+ result.query = relative.query;
+ //to support http.request
+ if (!isNull(result.pathname) || !isNull(result.search)) {
+ result.path = (result.pathname ? result.pathname : '') +
+ (result.search ? result.search : '');
+ }
+ result.href = result.format();
+ return result;
+ }
+
+ if (!srcPath.length) {
+ // no path at all. easy.
+ // we've already handled the other stuff above.
+ result.pathname = null;
+ //to support http.request
+ if (result.search) {
+ result.path = '/' + result.search;
+ } else {
+ result.path = null;
+ }
+ result.href = result.format();
+ return result;
+ }
+
+ // if a url ENDs in . or .., then it must get a trailing slash.
+ // however, if it ends in anything else non-slashy,
+ // then it must NOT get a trailing slash.
+ var last = srcPath.slice(-1)[0];
+ var hasTrailingSlash = (
+ (result.host || relative.host) && (last === '.' || last === '..') ||
+ last === '');
+
+ // strip single dots, resolve double dots to parent dir
+ // if the path tries to go above the root, `up` ends up > 0
+ var up = 0;
+ for (var i = srcPath.length; i >= 0; i--) {
+ last = srcPath[i];
+ if (last == '.') {
+ srcPath.splice(i, 1);
+ } else if (last === '..') {
+ srcPath.splice(i, 1);
+ up++;
+ } else if (up) {
+ srcPath.splice(i, 1);
+ up--;
+ }
+ }
+
+ // if the path is allowed to go above the root, restore leading ..s
+ if (!mustEndAbs && !removeAllDots) {
+ for (; up--; up) {
+ srcPath.unshift('..');
+ }
+ }
+
+ if (mustEndAbs && srcPath[0] !== '' &&
+ (!srcPath[0] || srcPath[0].charAt(0) !== '/')) {
+ srcPath.unshift('');
+ }
+
+ if (hasTrailingSlash && (srcPath.join('/').substr(-1) !== '/')) {
+ srcPath.push('');
+ }
+
+ var isAbsolute = srcPath[0] === '' ||
+ (srcPath[0] && srcPath[0].charAt(0) === '/');
+
+ // put the host back
+ if (psychotic) {
+ result.hostname = result.host = isAbsolute ? '' :
+ srcPath.length ? srcPath.shift() : '';
+ //occationaly the auth can get stuck only in host
+ //this especialy happens in cases like
+ //url.resolveObject('mailto:local1@domain1', 'local2@domain2')
+ var authInHost = result.host && result.host.indexOf('@') > 0 ?
+ result.host.split('@') : false;
+ if (authInHost) {
+ result.auth = authInHost.shift();
+ result.host = result.hostname = authInHost.shift();
+ }
+ }
+
+ mustEndAbs = mustEndAbs || (result.host && srcPath.length);
+
+ if (mustEndAbs && !isAbsolute) {
+ srcPath.unshift('');
+ }
+
+ if (!srcPath.length) {
+ result.pathname = null;
+ result.path = null;
+ } else {
+ result.pathname = srcPath.join('/');
+ }
+
+ //to support request.http
+ if (!isNull(result.pathname) || !isNull(result.search)) {
+ result.path = (result.pathname ? result.pathname : '') +
+ (result.search ? result.search : '');
+ }
+ result.auth = relative.auth || result.auth;
+ result.slashes = result.slashes || relative.slashes;
+ result.href = result.format();
+ return result;
+ };
+
+ Url.prototype.parseHost = function() {
+ var host = this.host;
+ var port = portPattern.exec(host);
+ if (port) {
+ port = port[0];
+ if (port !== ':') {
+ this.port = port.substr(1);
+ }
+ host = host.substr(0, host.length - port.length);
+ }
+ if (host) this.hostname = host;
+ };
+
+ function isString(arg) {
+ return typeof arg === "string";
+ }
+
+ function isObject(arg) {
+ return typeof arg === 'object' && arg !== null;
+ }
+
+ function isNull(arg) {
+ return arg === null;
+ }
+ function isNullOrUndefined(arg) {
+ return arg == null;
+ }
+
+
+/***/ },
+/* 294 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_RESULT__;/* WEBPACK VAR INJECTION */(function(module, global) {/*! https://mths.be/punycode v1.3.2 by @mathias */
+ ;(function(root) {
+
+ /** Detect free variables */
+ var freeExports = typeof exports == 'object' && exports &&
+ !exports.nodeType && exports;
+ var freeModule = typeof module == 'object' && module &&
+ !module.nodeType && module;
+ var freeGlobal = typeof global == 'object' && global;
+ if (
+ freeGlobal.global === freeGlobal ||
+ freeGlobal.window === freeGlobal ||
+ freeGlobal.self === freeGlobal
+ ) {
+ root = freeGlobal;
+ }
+
+ /**
+ * The `punycode` object.
+ * @name punycode
+ * @type Object
+ */
+ var punycode,
+
+ /** Highest positive signed 32-bit float value */
+ maxInt = 2147483647, // aka. 0x7FFFFFFF or 2^31-1
+
+ /** Bootstring parameters */
+ base = 36,
+ tMin = 1,
+ tMax = 26,
+ skew = 38,
+ damp = 700,
+ initialBias = 72,
+ initialN = 128, // 0x80
+ delimiter = '-', // '\x2D'
+
+ /** Regular expressions */
+ regexPunycode = /^xn--/,
+ regexNonASCII = /[^\x20-\x7E]/, // unprintable ASCII chars + non-ASCII chars
+ regexSeparators = /[\x2E\u3002\uFF0E\uFF61]/g, // RFC 3490 separators
+
+ /** Error messages */
+ errors = {
+ 'overflow': 'Overflow: input needs wider integers to process',
+ 'not-basic': 'Illegal input >= 0x80 (not a basic code point)',
+ 'invalid-input': 'Invalid input'
+ },
+
+ /** Convenience shortcuts */
+ baseMinusTMin = base - tMin,
+ floor = Math.floor,
+ stringFromCharCode = String.fromCharCode,
+
+ /** Temporary variable */
+ key;
+
+ /*--------------------------------------------------------------------------*/
+
+ /**
+ * A generic error utility function.
+ * @private
+ * @param {String} type The error type.
+ * @returns {Error} Throws a `RangeError` with the applicable error message.
+ */
+ function error(type) {
+ throw RangeError(errors[type]);
+ }
+
+ /**
+ * A generic `Array#map` utility function.
+ * @private
+ * @param {Array} array The array to iterate over.
+ * @param {Function} callback The function that gets called for every array
+ * item.
+ * @returns {Array} A new array of values returned by the callback function.
+ */
+ function map(array, fn) {
+ var length = array.length;
+ var result = [];
+ while (length--) {
+ result[length] = fn(array[length]);
+ }
+ return result;
+ }
+
+ /**
+ * A simple `Array#map`-like wrapper to work with domain name strings or email
+ * addresses.
+ * @private
+ * @param {String} domain The domain name or email address.
+ * @param {Function} callback The function that gets called for every
+ * character.
+ * @returns {Array} A new string of characters returned by the callback
+ * function.
+ */
+ function mapDomain(string, fn) {
+ var parts = string.split('@');
+ var result = '';
+ if (parts.length > 1) {
+ // In email addresses, only the domain name should be punycoded. Leave
+ // the local part (i.e. everything up to `@`) intact.
+ result = parts[0] + '@';
+ string = parts[1];
+ }
+ // Avoid `split(regex)` for IE8 compatibility. See #17.
+ string = string.replace(regexSeparators, '\x2E');
+ var labels = string.split('.');
+ var encoded = map(labels, fn).join('.');
+ return result + encoded;
+ }
+
+ /**
+ * Creates an array containing the numeric code points of each Unicode
+ * character in the string. While JavaScript uses UCS-2 internally,
+ * this function will convert a pair of surrogate halves (each of which
+ * UCS-2 exposes as separate characters) into a single code point,
+ * matching UTF-16.
+ * @see `punycode.ucs2.encode`
+ * @see <https://mathiasbynens.be/notes/javascript-encoding>
+ * @memberOf punycode.ucs2
+ * @name decode
+ * @param {String} string The Unicode input string (UCS-2).
+ * @returns {Array} The new array of code points.
+ */
+ function ucs2decode(string) {
+ var output = [],
+ counter = 0,
+ length = string.length,
+ value,
+ extra;
+ while (counter < length) {
+ value = string.charCodeAt(counter++);
+ if (value >= 0xD800 && value <= 0xDBFF && counter < length) {
+ // high surrogate, and there is a next character
+ extra = string.charCodeAt(counter++);
+ if ((extra & 0xFC00) == 0xDC00) { // low surrogate
+ output.push(((value & 0x3FF) << 10) + (extra & 0x3FF) + 0x10000);
+ } else {
+ // unmatched surrogate; only append this code unit, in case the next
+ // code unit is the high surrogate of a surrogate pair
+ output.push(value);
+ counter--;
+ }
+ } else {
+ output.push(value);
+ }
+ }
+ return output;
+ }
+
+ /**
+ * Creates a string based on an array of numeric code points.
+ * @see `punycode.ucs2.decode`
+ * @memberOf punycode.ucs2
+ * @name encode
+ * @param {Array} codePoints The array of numeric code points.
+ * @returns {String} The new Unicode string (UCS-2).
+ */
+ function ucs2encode(array) {
+ return map(array, function(value) {
+ var output = '';
+ if (value > 0xFFFF) {
+ value -= 0x10000;
+ output += stringFromCharCode(value >>> 10 & 0x3FF | 0xD800);
+ value = 0xDC00 | value & 0x3FF;
+ }
+ output += stringFromCharCode(value);
+ return output;
+ }).join('');
+ }
+
+ /**
+ * Converts a basic code point into a digit/integer.
+ * @see `digitToBasic()`
+ * @private
+ * @param {Number} codePoint The basic numeric code point value.
+ * @returns {Number} The numeric value of a basic code point (for use in
+ * representing integers) in the range `0` to `base - 1`, or `base` if
+ * the code point does not represent a value.
+ */
+ function basicToDigit(codePoint) {
+ if (codePoint - 48 < 10) {
+ return codePoint - 22;
+ }
+ if (codePoint - 65 < 26) {
+ return codePoint - 65;
+ }
+ if (codePoint - 97 < 26) {
+ return codePoint - 97;
+ }
+ return base;
+ }
+
+ /**
+ * Converts a digit/integer into a basic code point.
+ * @see `basicToDigit()`
+ * @private
+ * @param {Number} digit The numeric value of a basic code point.
+ * @returns {Number} The basic code point whose value (when used for
+ * representing integers) is `digit`, which needs to be in the range
+ * `0` to `base - 1`. If `flag` is non-zero, the uppercase form is
+ * used; else, the lowercase form is used. The behavior is undefined
+ * if `flag` is non-zero and `digit` has no uppercase form.
+ */
+ function digitToBasic(digit, flag) {
+ // 0..25 map to ASCII a..z or A..Z
+ // 26..35 map to ASCII 0..9
+ return digit + 22 + 75 * (digit < 26) - ((flag != 0) << 5);
+ }
+
+ /**
+ * Bias adaptation function as per section 3.4 of RFC 3492.
+ * http://tools.ietf.org/html/rfc3492#section-3.4
+ * @private
+ */
+ function adapt(delta, numPoints, firstTime) {
+ var k = 0;
+ delta = firstTime ? floor(delta / damp) : delta >> 1;
+ delta += floor(delta / numPoints);
+ for (/* no initialization */; delta > baseMinusTMin * tMax >> 1; k += base) {
+ delta = floor(delta / baseMinusTMin);
+ }
+ return floor(k + (baseMinusTMin + 1) * delta / (delta + skew));
+ }
+
+ /**
+ * Converts a Punycode string of ASCII-only symbols to a string of Unicode
+ * symbols.
+ * @memberOf punycode
+ * @param {String} input The Punycode string of ASCII-only symbols.
+ * @returns {String} The resulting string of Unicode symbols.
+ */
+ function decode(input) {
+ // Don't use UCS-2
+ var output = [],
+ inputLength = input.length,
+ out,
+ i = 0,
+ n = initialN,
+ bias = initialBias,
+ basic,
+ j,
+ index,
+ oldi,
+ w,
+ k,
+ digit,
+ t,
+ /** Cached calculation results */
+ baseMinusT;
+
+ // Handle the basic code points: let `basic` be the number of input code
+ // points before the last delimiter, or `0` if there is none, then copy
+ // the first basic code points to the output.
+
+ basic = input.lastIndexOf(delimiter);
+ if (basic < 0) {
+ basic = 0;
+ }
+
+ for (j = 0; j < basic; ++j) {
+ // if it's not a basic code point
+ if (input.charCodeAt(j) >= 0x80) {
+ error('not-basic');
+ }
+ output.push(input.charCodeAt(j));
+ }
+
+ // Main decoding loop: start just after the last delimiter if any basic code
+ // points were copied; start at the beginning otherwise.
+
+ for (index = basic > 0 ? basic + 1 : 0; index < inputLength; /* no final expression */) {
+
+ // `index` is the index of the next character to be consumed.
+ // Decode a generalized variable-length integer into `delta`,
+ // which gets added to `i`. The overflow checking is easier
+ // if we increase `i` as we go, then subtract off its starting
+ // value at the end to obtain `delta`.
+ for (oldi = i, w = 1, k = base; /* no condition */; k += base) {
+
+ if (index >= inputLength) {
+ error('invalid-input');
+ }
+
+ digit = basicToDigit(input.charCodeAt(index++));
+
+ if (digit >= base || digit > floor((maxInt - i) / w)) {
+ error('overflow');
+ }
+
+ i += digit * w;
+ t = k <= bias ? tMin : (k >= bias + tMax ? tMax : k - bias);
+
+ if (digit < t) {
+ break;
+ }
+
+ baseMinusT = base - t;
+ if (w > floor(maxInt / baseMinusT)) {
+ error('overflow');
+ }
+
+ w *= baseMinusT;
+
+ }
+
+ out = output.length + 1;
+ bias = adapt(i - oldi, out, oldi == 0);
+
+ // `i` was supposed to wrap around from `out` to `0`,
+ // incrementing `n` each time, so we'll fix that now:
+ if (floor(i / out) > maxInt - n) {
+ error('overflow');
+ }
+
+ n += floor(i / out);
+ i %= out;
+
+ // Insert `n` at position `i` of the output
+ output.splice(i++, 0, n);
+
+ }
+
+ return ucs2encode(output);
+ }
+
+ /**
+ * Converts a string of Unicode symbols (e.g. a domain name label) to a
+ * Punycode string of ASCII-only symbols.
+ * @memberOf punycode
+ * @param {String} input The string of Unicode symbols.
+ * @returns {String} The resulting Punycode string of ASCII-only symbols.
+ */
+ function encode(input) {
+ var n,
+ delta,
+ handledCPCount,
+ basicLength,
+ bias,
+ j,
+ m,
+ q,
+ k,
+ t,
+ currentValue,
+ output = [],
+ /** `inputLength` will hold the number of code points in `input`. */
+ inputLength,
+ /** Cached calculation results */
+ handledCPCountPlusOne,
+ baseMinusT,
+ qMinusT;
+
+ // Convert the input in UCS-2 to Unicode
+ input = ucs2decode(input);
+
+ // Cache the length
+ inputLength = input.length;
+
+ // Initialize the state
+ n = initialN;
+ delta = 0;
+ bias = initialBias;
+
+ // Handle the basic code points
+ for (j = 0; j < inputLength; ++j) {
+ currentValue = input[j];
+ if (currentValue < 0x80) {
+ output.push(stringFromCharCode(currentValue));
+ }
+ }
+
+ handledCPCount = basicLength = output.length;
+
+ // `handledCPCount` is the number of code points that have been handled;
+ // `basicLength` is the number of basic code points.
+
+ // Finish the basic string - if it is not empty - with a delimiter
+ if (basicLength) {
+ output.push(delimiter);
+ }
+
+ // Main encoding loop:
+ while (handledCPCount < inputLength) {
+
+ // All non-basic code points < n have been handled already. Find the next
+ // larger one:
+ for (m = maxInt, j = 0; j < inputLength; ++j) {
+ currentValue = input[j];
+ if (currentValue >= n && currentValue < m) {
+ m = currentValue;
+ }
+ }
+
+ // Increase `delta` enough to advance the decoder's <n,i> state to <m,0>,
+ // but guard against overflow
+ handledCPCountPlusOne = handledCPCount + 1;
+ if (m - n > floor((maxInt - delta) / handledCPCountPlusOne)) {
+ error('overflow');
+ }
+
+ delta += (m - n) * handledCPCountPlusOne;
+ n = m;
+
+ for (j = 0; j < inputLength; ++j) {
+ currentValue = input[j];
+
+ if (currentValue < n && ++delta > maxInt) {
+ error('overflow');
+ }
+
+ if (currentValue == n) {
+ // Represent delta as a generalized variable-length integer
+ for (q = delta, k = base; /* no condition */; k += base) {
+ t = k <= bias ? tMin : (k >= bias + tMax ? tMax : k - bias);
+ if (q < t) {
+ break;
+ }
+ qMinusT = q - t;
+ baseMinusT = base - t;
+ output.push(
+ stringFromCharCode(digitToBasic(t + qMinusT % baseMinusT, 0))
+ );
+ q = floor(qMinusT / baseMinusT);
+ }
+
+ output.push(stringFromCharCode(digitToBasic(q, 0)));
+ bias = adapt(delta, handledCPCountPlusOne, handledCPCount == basicLength);
+ delta = 0;
+ ++handledCPCount;
+ }
+ }
+
+ ++delta;
+ ++n;
+
+ }
+ return output.join('');
+ }
+
+ /**
+ * Converts a Punycode string representing a domain name or an email address
+ * to Unicode. Only the Punycoded parts of the input will be converted, i.e.
+ * it doesn't matter if you call it on a string that has already been
+ * converted to Unicode.
+ * @memberOf punycode
+ * @param {String} input The Punycoded domain name or email address to
+ * convert to Unicode.
+ * @returns {String} The Unicode representation of the given Punycode
+ * string.
+ */
+ function toUnicode(input) {
+ return mapDomain(input, function(string) {
+ return regexPunycode.test(string)
+ ? decode(string.slice(4).toLowerCase())
+ : string;
+ });
+ }
+
+ /**
+ * Converts a Unicode string representing a domain name or an email address to
+ * Punycode. Only the non-ASCII parts of the domain name will be converted,
+ * i.e. it doesn't matter if you call it with a domain that's already in
+ * ASCII.
+ * @memberOf punycode
+ * @param {String} input The domain name or email address to convert, as a
+ * Unicode string.
+ * @returns {String} The Punycode representation of the given domain name or
+ * email address.
+ */
+ function toASCII(input) {
+ return mapDomain(input, function(string) {
+ return regexNonASCII.test(string)
+ ? 'xn--' + encode(string)
+ : string;
+ });
+ }
+
+ /*--------------------------------------------------------------------------*/
+
+ /** Define the public API */
+ punycode = {
+ /**
+ * A string representing the current Punycode.js version number.
+ * @memberOf punycode
+ * @type String
+ */
+ 'version': '1.3.2',
+ /**
+ * An object of methods to convert from JavaScript's internal character
+ * representation (UCS-2) to Unicode code points, and back.
+ * @see <https://mathiasbynens.be/notes/javascript-encoding>
+ * @memberOf punycode
+ * @type Object
+ */
+ 'ucs2': {
+ 'decode': ucs2decode,
+ 'encode': ucs2encode
+ },
+ 'decode': decode,
+ 'encode': encode,
+ 'toASCII': toASCII,
+ 'toUnicode': toUnicode
+ };
+
+ /** Expose `punycode` */
+ // Some AMD build optimizers, like r.js, check for specific condition patterns
+ // like the following:
+ if (
+ true
+ ) {
+ !(__WEBPACK_AMD_DEFINE_RESULT__ = function() {
+ return punycode;
+ }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+ } else if (freeExports && freeModule) {
+ if (module.exports == freeExports) { // in Node.js or RingoJS v0.8.0+
+ freeModule.exports = punycode;
+ } else { // in Narwhal or RingoJS v0.7.0-
+ for (key in punycode) {
+ punycode.hasOwnProperty(key) && (freeExports[key] = punycode[key]);
+ }
+ }
+ } else { // in Rhino or a web browser
+ root.punycode = punycode;
+ }
+
+ }(this));
+
+ /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(77)(module), (function() { return this; }())))
+
+/***/ },
+/* 295 */
+/***/ function(module, exports, __webpack_require__) {
+
+ 'use strict';
+
+ exports.decode = exports.parse = __webpack_require__(296);
+ exports.encode = exports.stringify = __webpack_require__(297);
+
+
+/***/ },
+/* 296 */
+/***/ function(module, exports) {
+
+ // Copyright Joyent, Inc. and other Node contributors.
+ //
+ // Permission is hereby granted, free of charge, to any person obtaining a
+ // copy of this software and associated documentation files (the
+ // "Software"), to deal in the Software without restriction, including
+ // without limitation the rights to use, copy, modify, merge, publish,
+ // distribute, sublicense, and/or sell copies of the Software, and to permit
+ // persons to whom the Software is furnished to do so, subject to the
+ // following conditions:
+ //
+ // The above copyright notice and this permission notice shall be included
+ // in all copies or substantial portions of the Software.
+ //
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+ // NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+ // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+ // USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+ 'use strict';
+
+ // If obj.hasOwnProperty has been overridden, then calling
+ // obj.hasOwnProperty(prop) will break.
+ // See: https://github.com/joyent/node/issues/1707
+ function hasOwnProperty(obj, prop) {
+ return Object.prototype.hasOwnProperty.call(obj, prop);
+ }
+
+ module.exports = function(qs, sep, eq, options) {
+ sep = sep || '&';
+ eq = eq || '=';
+ var obj = {};
+
+ if (typeof qs !== 'string' || qs.length === 0) {
+ return obj;
+ }
+
+ var regexp = /\+/g;
+ qs = qs.split(sep);
+
+ var maxKeys = 1000;
+ if (options && typeof options.maxKeys === 'number') {
+ maxKeys = options.maxKeys;
+ }
+
+ var len = qs.length;
+ // maxKeys <= 0 means that we should not limit keys count
+ if (maxKeys > 0 && len > maxKeys) {
+ len = maxKeys;
+ }
+
+ for (var i = 0; i < len; ++i) {
+ var x = qs[i].replace(regexp, '%20'),
+ idx = x.indexOf(eq),
+ kstr, vstr, k, v;
+
+ if (idx >= 0) {
+ kstr = x.substr(0, idx);
+ vstr = x.substr(idx + 1);
+ } else {
+ kstr = x;
+ vstr = '';
+ }
+
+ k = decodeURIComponent(kstr);
+ v = decodeURIComponent(vstr);
+
+ if (!hasOwnProperty(obj, k)) {
+ obj[k] = v;
+ } else if (Array.isArray(obj[k])) {
+ obj[k].push(v);
+ } else {
+ obj[k] = [obj[k], v];
+ }
+ }
+
+ return obj;
+ };
+
+
+/***/ },
+/* 297 */
+/***/ function(module, exports) {
+
+ // Copyright Joyent, Inc. and other Node contributors.
+ //
+ // Permission is hereby granted, free of charge, to any person obtaining a
+ // copy of this software and associated documentation files (the
+ // "Software"), to deal in the Software without restriction, including
+ // without limitation the rights to use, copy, modify, merge, publish,
+ // distribute, sublicense, and/or sell copies of the Software, and to permit
+ // persons to whom the Software is furnished to do so, subject to the
+ // following conditions:
+ //
+ // The above copyright notice and this permission notice shall be included
+ // in all copies or substantial portions of the Software.
+ //
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+ // NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+ // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+ // USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+ 'use strict';
+
+ var stringifyPrimitive = function(v) {
+ switch (typeof v) {
+ case 'string':
+ return v;
+
+ case 'boolean':
+ return v ? 'true' : 'false';
+
+ case 'number':
+ return isFinite(v) ? v : '';
+
+ default:
+ return '';
+ }
+ };
+
+ module.exports = function(obj, sep, eq, name) {
+ sep = sep || '&';
+ eq = eq || '=';
+ if (obj === null) {
+ obj = undefined;
+ }
+
+ if (typeof obj === 'object') {
+ return Object.keys(obj).map(function(k) {
+ var ks = encodeURIComponent(stringifyPrimitive(k)) + eq;
+ if (Array.isArray(obj[k])) {
+ return obj[k].map(function(v) {
+ return ks + encodeURIComponent(stringifyPrimitive(v));
+ }).join(sep);
+ } else {
+ return ks + encodeURIComponent(stringifyPrimitive(obj[k]));
+ }
+ }).join(sep);
+
+ }
+
+ if (!name) return '';
+ return encodeURIComponent(stringifyPrimitive(name)) + eq +
+ encodeURIComponent(stringifyPrimitive(obj));
+ };
+
+
+/***/ },
+/* 298 */
+/***/ function(module, exports) {
+
+ // removed by extract-text-webpack-plugin
+
+/***/ },
+/* 299 */,
+/* 300 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var React = __webpack_require__(2);
+ var dom = React.DOM;
+ var PropTypes = React.PropTypes;
+
+ var _require = __webpack_require__(301);
+
+ var filter = _require.filter;
+
+ var classnames = __webpack_require__(211);
+ __webpack_require__(308);
+ var Svg = __webpack_require__(310);
+ var CloseButton = __webpack_require__(336);
+
+ var INITIAL_SELECTED_INDEX = 0;
+
+ var Autocomplete = React.createClass({
+ propTypes: {
+ selectItem: PropTypes.func,
+ items: PropTypes.array,
+ handleClose: PropTypes.func,
+ inputValue: PropTypes.string
+ },
+
+ displayName: "Autocomplete",
+
+ getInitialState() {
+ return {
+ inputValue: this.props.inputValue,
+ selectedIndex: INITIAL_SELECTED_INDEX
+ };
+ },
+
+ componentDidMount() {
+ var endOfInput = this.state.inputValue.length;
+ this.refs.searchInput.focus();
+ this.refs.searchInput.setSelectionRange(endOfInput, endOfInput);
+ },
+
+ componentDidUpdate() {
+ this.scrollList();
+ },
+
+ scrollList() {
+ var resultsEl = this.refs.results;
+ if (!resultsEl || resultsEl.children.length === 0) {
+ return;
+ }
+
+ var resultsHeight = resultsEl.clientHeight;
+ var itemHeight = resultsEl.children[0].clientHeight;
+ var numVisible = resultsHeight / itemHeight;
+ var positionsToScroll = this.state.selectedIndex - numVisible + 1;
+ var itemOffset = resultsHeight % itemHeight;
+ var scroll = positionsToScroll * (itemHeight + 2) + itemOffset;
+
+ resultsEl.scrollTop = Math.max(0, scroll);
+ },
+
+ getSearchResults() {
+ var inputValue = this.state.inputValue;
+
+ if (inputValue == "") {
+ return [];
+ }
+ return filter(this.props.items, this.state.inputValue, {
+ key: "value"
+ });
+ },
+
+ onKeyDown(e) {
+ var searchResults = this.getSearchResults(),
+ resultCount = searchResults.length;
+
+ if (e.key === "ArrowUp") {
+ this.setState({
+ selectedIndex: Math.max(0, this.state.selectedIndex - 1)
+ });
+ e.preventDefault();
+ } else if (e.key === "ArrowDown") {
+ this.setState({
+ selectedIndex: Math.min(resultCount - 1, this.state.selectedIndex + 1)
+ });
+ e.preventDefault();
+ } else if (e.key === "Enter") {
+ if (searchResults.length) {
+ this.props.selectItem(searchResults[this.state.selectedIndex]);
+ } else {
+ this.props.handleClose(this.state.inputValue);
+ }
+ e.preventDefault();
+ } else if (e.key === "Tab") {
+ this.props.handleClose(this.state.inputValue);
+ e.preventDefault();
+ }
+ },
+
+ renderSearchItem(result, index) {
+ return dom.li({
+ onClick: () => this.props.selectItem(result),
+ key: result.value,
+ className: classnames({
+ selected: index === this.state.selectedIndex
+ })
+ }, dom.div({ className: "title" }, result.title), dom.div({ className: "subtitle" }, result.subtitle));
+ },
+
+ renderInput() {
+ return dom.input({
+ ref: "searchInput",
+ value: this.state.inputValue,
+ onChange: e => this.setState({
+ inputValue: e.target.value,
+ selectedIndex: INITIAL_SELECTED_INDEX
+ }),
+ onFocus: e => this.setState({ focused: true }),
+ onBlur: e => this.setState({ focused: false }),
+ onKeyDown: this.onKeyDown,
+ placeholder: L10N.getStr("sourceSearch.search")
+ });
+ },
+
+ renderResults(results) {
+ if (results.length) {
+ return dom.ul({ className: "results", ref: "results" }, results.map(this.renderSearchItem));
+ } else if (this.state.inputValue && !results.length) {
+ return dom.div({ className: "no-result-msg" }, Svg("sad-face"), L10N.getFormatStr("sourceSearch.noResults", this.state.inputValue));
+ }
+ },
+
+ renderSummary(searchResults) {
+ if (searchResults && searchResults.length === 0) {
+ return;
+ }
+
+ var resultCountSummary = "";
+ if (this.state.inputValue) {
+ resultCountSummary = L10N.getFormatStr("sourceSearch.resultsSummary", searchResults.length, this.state.inputValue);
+ }
+ return dom.div({ className: "results-summary" }, resultCountSummary);
+ },
+
+ render() {
+ var searchResults = this.getSearchResults();
+ return dom.div({ className: classnames({
+ autocomplete: true,
+ focused: this.state.focused
+ })
+ }, dom.div({ className: "searchinput-container" }, Svg("magnifying-glass"), this.renderInput(), CloseButton({
+ buttonClass: "big",
+ handleClick: this.props.handleClose
+ })), this.renderSummary(searchResults), this.renderResults(searchResults));
+ }
+ });
+
+ module.exports = Autocomplete;
+
+/***/ },
+/* 301 */
+/***/ function(module, exports, __webpack_require__) {
+
+ (function() {
+ var PathSeparator, filter, legacy_scorer, matcher, prepQueryCache, scorer;
+
+ scorer = __webpack_require__(302);
+
+ legacy_scorer = __webpack_require__(305);
+
+ filter = __webpack_require__(306);
+
+ matcher = __webpack_require__(307);
+
+ PathSeparator = __webpack_require__(303).sep;
+
+ prepQueryCache = null;
+
+ module.exports = {
+ filter: function(candidates, query, options) {
+ if (!((query != null ? query.length : void 0) && (candidates != null ? candidates.length : void 0))) {
+ return [];
+ }
+ return filter(candidates, query, options);
+ },
+ prepQuery: function(query) {
+ return scorer.prepQuery(query);
+ },
+ score: function(string, query, prepQuery, _arg) {
+ var allowErrors, coreQuery, legacy, queryHasSlashes, score, _ref;
+ _ref = _arg != null ? _arg : {}, allowErrors = _ref.allowErrors, legacy = _ref.legacy;
+ if (!((string != null ? string.length : void 0) && (query != null ? query.length : void 0))) {
+ return 0;
+ }
+ if (prepQuery == null) {
+ prepQuery = prepQueryCache && prepQueryCache.query === query ? prepQueryCache : (prepQueryCache = scorer.prepQuery(query));
+ }
+ if (!legacy) {
+ score = scorer.score(string, query, prepQuery, !!allowErrors);
+ } else {
+ queryHasSlashes = prepQuery.depth > 0;
+ coreQuery = prepQuery.core;
+ score = legacy_scorer.score(string, coreQuery, queryHasSlashes);
+ if (!queryHasSlashes) {
+ score = legacy_scorer.basenameScore(string, coreQuery, score);
+ }
+ }
+ return score;
+ },
+ match: function(string, query, prepQuery, _arg) {
+ var allowErrors, baseMatches, matches, query_lw, string_lw, _i, _ref, _results;
+ allowErrors = (_arg != null ? _arg : {}).allowErrors;
+ if (!string) {
+ return [];
+ }
+ if (!query) {
+ return [];
+ }
+ if (string === query) {
+ return (function() {
+ _results = [];
+ for (var _i = 0, _ref = string.length; 0 <= _ref ? _i < _ref : _i > _ref; 0 <= _ref ? _i++ : _i--){ _results.push(_i); }
+ return _results;
+ }).apply(this);
+ }
+ if (prepQuery == null) {
+ prepQuery = prepQueryCache && prepQueryCache.query === query ? prepQueryCache : (prepQueryCache = scorer.prepQuery(query));
+ }
+ if (!(allowErrors || scorer.isMatch(string, prepQuery.core_lw, prepQuery.core_up))) {
+ return [];
+ }
+ string_lw = string.toLowerCase();
+ query_lw = prepQuery.query_lw;
+ matches = matcher.match(string, string_lw, prepQuery);
+ if (matches.length === 0) {
+ return matches;
+ }
+ if (string.indexOf(PathSeparator) > -1) {
+ baseMatches = matcher.basenameMatch(string, string_lw, prepQuery);
+ matches = matcher.mergeMatches(matches, baseMatches);
+ }
+ return matches;
+ }
+ };
+
+ }).call(this);
+
+
+/***/ },
+/* 302 */
+/***/ function(module, exports, __webpack_require__) {
+
+ (function() {
+ var AcronymResult, PathSeparator, Query, basenameScore, coreChars, countDir, doScore, emptyAcronymResult, file_coeff, isMatch, isSeparator, isWordEnd, isWordStart, miss_coeff, opt_char_re, pos_bonus, scoreAcronyms, scoreCharacter, scoreConsecutives, scoreExact, scoreExactMatch, scorePattern, scorePosition, scoreSize, tau_depth, tau_size, truncatedUpperCase, wm;
+
+ PathSeparator = __webpack_require__(303).sep;
+
+ wm = 150;
+
+ pos_bonus = 20;
+
+ tau_depth = 13;
+
+ tau_size = 85;
+
+ file_coeff = 1.2;
+
+ miss_coeff = 0.75;
+
+ opt_char_re = /[ _\-:\/\\]/g;
+
+ exports.coreChars = coreChars = function(query) {
+ return query.replace(opt_char_re, '');
+ };
+
+ exports.score = function(string, query, prepQuery, allowErrors) {
+ var score, string_lw;
+ if (prepQuery == null) {
+ prepQuery = new Query(query);
+ }
+ if (allowErrors == null) {
+ allowErrors = false;
+ }
+ if (!(allowErrors || isMatch(string, prepQuery.core_lw, prepQuery.core_up))) {
+ return 0;
+ }
+ string_lw = string.toLowerCase();
+ score = doScore(string, string_lw, prepQuery);
+ return Math.ceil(basenameScore(string, string_lw, prepQuery, score));
+ };
+
+ Query = (function() {
+ function Query(query) {
+ if (!(query != null ? query.length : void 0)) {
+ return null;
+ }
+ this.query = query;
+ this.query_lw = query.toLowerCase();
+ this.core = coreChars(query);
+ this.core_lw = this.core.toLowerCase();
+ this.core_up = truncatedUpperCase(this.core);
+ this.depth = countDir(query, query.length);
+ }
+
+ return Query;
+
+ })();
+
+ exports.prepQuery = function(query) {
+ return new Query(query);
+ };
+
+ exports.isMatch = isMatch = function(subject, query_lw, query_up) {
+ var i, j, m, n, qj_lw, qj_up, si;
+ m = subject.length;
+ n = query_lw.length;
+ if (!m || n > m) {
+ return false;
+ }
+ i = -1;
+ j = -1;
+ while (++j < n) {
+ qj_lw = query_lw[j];
+ qj_up = query_up[j];
+ while (++i < m) {
+ si = subject[i];
+ if (si === qj_lw || si === qj_up) {
+ break;
+ }
+ }
+ if (i === m) {
+ return false;
+ }
+ }
+ return true;
+ };
+
+ doScore = function(subject, subject_lw, prepQuery) {
+ var acro, acro_score, align, csc_diag, csc_row, csc_score, i, j, m, miss_budget, miss_left, mm, n, pos, query, query_lw, record_miss, score, score_diag, score_row, score_up, si_lw, start, sz;
+ query = prepQuery.query;
+ query_lw = prepQuery.query_lw;
+ m = subject.length;
+ n = query.length;
+ acro = scoreAcronyms(subject, subject_lw, query, query_lw);
+ acro_score = acro.score;
+ if (acro.count === n) {
+ return scoreExact(n, m, acro_score, acro.pos);
+ }
+ pos = subject_lw.indexOf(query_lw);
+ if (pos > -1) {
+ return scoreExactMatch(subject, subject_lw, query, query_lw, pos, n, m);
+ }
+ score_row = new Array(n);
+ csc_row = new Array(n);
+ sz = scoreSize(n, m);
+ miss_budget = Math.ceil(miss_coeff * n) + 5;
+ miss_left = miss_budget;
+ j = -1;
+ while (++j < n) {
+ score_row[j] = 0;
+ csc_row[j] = 0;
+ }
+ i = subject_lw.indexOf(query_lw[0]);
+ if (i > -1) {
+ i--;
+ }
+ mm = subject_lw.lastIndexOf(query_lw[n - 1], m);
+ if (mm > i) {
+ m = mm + 1;
+ }
+ while (++i < m) {
+ score = 0;
+ score_diag = 0;
+ csc_diag = 0;
+ si_lw = subject_lw[i];
+ record_miss = true;
+ j = -1;
+ while (++j < n) {
+ score_up = score_row[j];
+ if (score_up > score) {
+ score = score_up;
+ }
+ csc_score = 0;
+ if (query_lw[j] === si_lw) {
+ start = isWordStart(i, subject, subject_lw);
+ csc_score = csc_diag > 0 ? csc_diag : scoreConsecutives(subject, subject_lw, query, query_lw, i, j, start);
+ align = score_diag + scoreCharacter(i, j, start, acro_score, csc_score);
+ if (align > score) {
+ score = align;
+ miss_left = miss_budget;
+ } else {
+ if (record_miss && --miss_left <= 0) {
+ return score_row[n - 1] * sz;
+ }
+ record_miss = false;
+ }
+ }
+ score_diag = score_up;
+ csc_diag = csc_row[j];
+ csc_row[j] = csc_score;
+ score_row[j] = score;
+ }
+ }
+ return score * sz;
+ };
+
+ exports.isWordStart = isWordStart = function(pos, subject, subject_lw) {
+ var curr_s, prev_s;
+ if (pos === 0) {
+ return true;
+ }
+ curr_s = subject[pos];
+ prev_s = subject[pos - 1];
+ return isSeparator(curr_s) || isSeparator(prev_s) || (curr_s !== subject_lw[pos] && prev_s === subject_lw[pos - 1]);
+ };
+
+ exports.isWordEnd = isWordEnd = function(pos, subject, subject_lw, len) {
+ var curr_s, next_s;
+ if (pos === len - 1) {
+ return true;
+ }
+ curr_s = subject[pos];
+ next_s = subject[pos + 1];
+ return isSeparator(curr_s) || isSeparator(next_s) || (curr_s === subject_lw[pos] && next_s !== subject_lw[pos + 1]);
+ };
+
+ isSeparator = function(c) {
+ return c === ' ' || c === '.' || c === '-' || c === '_' || c === '/' || c === '\\';
+ };
+
+ scorePosition = function(pos) {
+ var sc;
+ if (pos < pos_bonus) {
+ sc = pos_bonus - pos;
+ return 100 + sc * sc;
+ } else {
+ return Math.max(100 + pos_bonus - pos, 0);
+ }
+ };
+
+ scoreSize = function(n, m) {
+ return tau_size / (tau_size + Math.abs(m - n));
+ };
+
+ scoreExact = function(n, m, quality, pos) {
+ return 2 * n * (wm * quality + scorePosition(pos)) * scoreSize(n, m);
+ };
+
+ exports.scorePattern = scorePattern = function(count, len, sameCase, start, end) {
+ var bonus, sz;
+ sz = count;
+ bonus = 6;
+ if (sameCase === count) {
+ bonus += 2;
+ }
+ if (start) {
+ bonus += 3;
+ }
+ if (end) {
+ bonus += 1;
+ }
+ if (count === len) {
+ if (start) {
+ if (sameCase === len) {
+ sz += 2;
+ } else {
+ sz += 1;
+ }
+ }
+ if (end) {
+ bonus += 1;
+ }
+ }
+ return sameCase + sz * (sz + bonus);
+ };
+
+ exports.scoreCharacter = scoreCharacter = function(i, j, start, acro_score, csc_score) {
+ var posBonus;
+ posBonus = scorePosition(i);
+ if (start) {
+ return posBonus + wm * ((acro_score > csc_score ? acro_score : csc_score) + 10);
+ }
+ return posBonus + wm * csc_score;
+ };
+
+ exports.scoreConsecutives = scoreConsecutives = function(subject, subject_lw, query, query_lw, i, j, start) {
+ var k, m, mi, n, nj, sameCase, startPos, sz;
+ m = subject.length;
+ n = query.length;
+ mi = m - i;
+ nj = n - j;
+ k = mi < nj ? mi : nj;
+ startPos = i;
+ sameCase = 0;
+ sz = 0;
+ if (query[j] === subject[i]) {
+ sameCase++;
+ }
+ while (++sz < k && query_lw[++j] === subject_lw[++i]) {
+ if (query[j] === subject[i]) {
+ sameCase++;
+ }
+ }
+ if (sz === 1) {
+ return 1 + 2 * sameCase;
+ }
+ return scorePattern(sz, n, sameCase, start, isWordEnd(i, subject, subject_lw, m));
+ };
+
+ exports.scoreExactMatch = scoreExactMatch = function(subject, subject_lw, query, query_lw, pos, n, m) {
+ var end, i, pos2, sameCase, start;
+ start = isWordStart(pos, subject, subject_lw);
+ if (!start) {
+ pos2 = subject_lw.indexOf(query_lw, pos + 1);
+ if (pos2 > -1) {
+ start = isWordStart(pos2, subject, subject_lw);
+ if (start) {
+ pos = pos2;
+ }
+ }
+ }
+ i = -1;
+ sameCase = 0;
+ while (++i < n) {
+ if (query[pos + i] === subject[i]) {
+ sameCase++;
+ }
+ }
+ end = isWordEnd(pos + n - 1, subject, subject_lw, m);
+ return scoreExact(n, m, scorePattern(n, n, sameCase, start, end), pos);
+ };
+
+ AcronymResult = (function() {
+ function AcronymResult(score, pos, count) {
+ this.score = score;
+ this.pos = pos;
+ this.count = count;
+ }
+
+ return AcronymResult;
+
+ })();
+
+ emptyAcronymResult = new AcronymResult(0, 0.1, 0);
+
+ exports.scoreAcronyms = scoreAcronyms = function(subject, subject_lw, query, query_lw) {
+ var count, i, j, m, n, pos, qj_lw, sameCase, score;
+ m = subject.length;
+ n = query.length;
+ if (!(m > 1 && n > 1)) {
+ return emptyAcronymResult;
+ }
+ count = 0;
+ pos = 0;
+ sameCase = 0;
+ i = -1;
+ j = -1;
+ while (++j < n) {
+ qj_lw = query_lw[j];
+ while (++i < m) {
+ if (qj_lw === subject_lw[i] && isWordStart(i, subject, subject_lw)) {
+ if (query[j] === subject[i]) {
+ sameCase++;
+ }
+ pos += i;
+ count++;
+ break;
+ }
+ }
+ if (i === m) {
+ break;
+ }
+ }
+ if (count < 2) {
+ return emptyAcronymResult;
+ }
+ score = scorePattern(count, n, sameCase, true, false);
+ return new AcronymResult(score, pos / count, count);
+ };
+
+ basenameScore = function(subject, subject_lw, prepQuery, fullPathScore) {
+ var alpha, basePathScore, basePos, depth, end;
+ if (fullPathScore === 0) {
+ return 0;
+ }
+ end = subject.length - 1;
+ while (subject[end] === PathSeparator) {
+ end--;
+ }
+ basePos = subject.lastIndexOf(PathSeparator, end);
+ if (basePos === -1) {
+ return fullPathScore;
+ }
+ depth = prepQuery.depth;
+ while (depth-- > 0) {
+ basePos = subject.lastIndexOf(PathSeparator, basePos - 1);
+ if (basePos === -1) {
+ return fullPathScore;
+ }
+ }
+ basePos++;
+ end++;
+ basePathScore = doScore(subject.slice(basePos, end), subject_lw.slice(basePos, end), prepQuery);
+ alpha = 0.5 * tau_depth / (tau_depth + countDir(subject, end + 1));
+ return alpha * basePathScore + (1 - alpha) * fullPathScore * scoreSize(0, file_coeff * (end - basePos));
+ };
+
+ exports.countDir = countDir = function(path, end) {
+ var count, i;
+ if (end < 1) {
+ return 0;
+ }
+ count = 0;
+ i = -1;
+ while (++i < end && path[i] === PathSeparator) {
+ continue;
+ }
+ while (++i < end) {
+ if (path[i] === PathSeparator) {
+ count++;
+ while (++i < end && path[i] === PathSeparator) {
+ continue;
+ }
+ }
+ }
+ return count;
+ };
+
+ truncatedUpperCase = function(str) {
+ var char, upper, _i, _len;
+ upper = "";
+ for (_i = 0, _len = str.length; _i < _len; _i++) {
+ char = str[_i];
+ upper += char.toUpperCase()[0];
+ }
+ return upper;
+ };
+
+ }).call(this);
+
+
+/***/ },
+/* 303 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /* WEBPACK VAR INJECTION */(function(process) {// Copyright Joyent, Inc. and other Node contributors.
+ //
+ // Permission is hereby granted, free of charge, to any person obtaining a
+ // copy of this software and associated documentation files (the
+ // "Software"), to deal in the Software without restriction, including
+ // without limitation the rights to use, copy, modify, merge, publish,
+ // distribute, sublicense, and/or sell copies of the Software, and to permit
+ // persons to whom the Software is furnished to do so, subject to the
+ // following conditions:
+ //
+ // The above copyright notice and this permission notice shall be included
+ // in all copies or substantial portions of the Software.
+ //
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+ // NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+ // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+ // USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+ // resolves . and .. elements in a path array with directory names there
+ // must be no slashes, empty elements, or device names (c:\) in the array
+ // (so also no leading and trailing slashes - it does not distinguish
+ // relative and absolute paths)
+ function normalizeArray(parts, allowAboveRoot) {
+ // if the path tries to go above the root, `up` ends up > 0
+ var up = 0;
+ for (var i = parts.length - 1; i >= 0; i--) {
+ var last = parts[i];
+ if (last === '.') {
+ parts.splice(i, 1);
+ } else if (last === '..') {
+ parts.splice(i, 1);
+ up++;
+ } else if (up) {
+ parts.splice(i, 1);
+ up--;
+ }
+ }
+
+ // if the path is allowed to go above the root, restore leading ..s
+ if (allowAboveRoot) {
+ for (; up--; up) {
+ parts.unshift('..');
+ }
+ }
+
+ return parts;
+ }
+
+ // Split a filename into [root, dir, basename, ext], unix version
+ // 'root' is just a slash, or nothing.
+ var splitPathRe =
+ /^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/;
+ var splitPath = function(filename) {
+ return splitPathRe.exec(filename).slice(1);
+ };
+
+ // path.resolve([from ...], to)
+ // posix version
+ exports.resolve = function() {
+ var resolvedPath = '',
+ resolvedAbsolute = false;
+
+ for (var i = arguments.length - 1; i >= -1 && !resolvedAbsolute; i--) {
+ var path = (i >= 0) ? arguments[i] : process.cwd();
+
+ // Skip empty and invalid entries
+ if (typeof path !== 'string') {
+ throw new TypeError('Arguments to path.resolve must be strings');
+ } else if (!path) {
+ continue;
+ }
+
+ resolvedPath = path + '/' + resolvedPath;
+ resolvedAbsolute = path.charAt(0) === '/';
+ }
+
+ // At this point the path should be resolved to a full absolute path, but
+ // handle relative paths to be safe (might happen when process.cwd() fails)
+
+ // Normalize the path
+ resolvedPath = normalizeArray(filter(resolvedPath.split('/'), function(p) {
+ return !!p;
+ }), !resolvedAbsolute).join('/');
+
+ return ((resolvedAbsolute ? '/' : '') + resolvedPath) || '.';
+ };
+
+ // path.normalize(path)
+ // posix version
+ exports.normalize = function(path) {
+ var isAbsolute = exports.isAbsolute(path),
+ trailingSlash = substr(path, -1) === '/';
+
+ // Normalize the path
+ path = normalizeArray(filter(path.split('/'), function(p) {
+ return !!p;
+ }), !isAbsolute).join('/');
+
+ if (!path && !isAbsolute) {
+ path = '.';
+ }
+ if (path && trailingSlash) {
+ path += '/';
+ }
+
+ return (isAbsolute ? '/' : '') + path;
+ };
+
+ // posix version
+ exports.isAbsolute = function(path) {
+ return path.charAt(0) === '/';
+ };
+
+ // posix version
+ exports.join = function() {
+ var paths = Array.prototype.slice.call(arguments, 0);
+ return exports.normalize(filter(paths, function(p, index) {
+ if (typeof p !== 'string') {
+ throw new TypeError('Arguments to path.join must be strings');
+ }
+ return p;
+ }).join('/'));
+ };
+
+
+ // path.relative(from, to)
+ // posix version
+ exports.relative = function(from, to) {
+ from = exports.resolve(from).substr(1);
+ to = exports.resolve(to).substr(1);
+
+ function trim(arr) {
+ var start = 0;
+ for (; start < arr.length; start++) {
+ if (arr[start] !== '') break;
+ }
+
+ var end = arr.length - 1;
+ for (; end >= 0; end--) {
+ if (arr[end] !== '') break;
+ }
+
+ if (start > end) return [];
+ return arr.slice(start, end - start + 1);
+ }
+
+ var fromParts = trim(from.split('/'));
+ var toParts = trim(to.split('/'));
+
+ var length = Math.min(fromParts.length, toParts.length);
+ var samePartsLength = length;
+ for (var i = 0; i < length; i++) {
+ if (fromParts[i] !== toParts[i]) {
+ samePartsLength = i;
+ break;
+ }
+ }
+
+ var outputParts = [];
+ for (var i = samePartsLength; i < fromParts.length; i++) {
+ outputParts.push('..');
+ }
+
+ outputParts = outputParts.concat(toParts.slice(samePartsLength));
+
+ return outputParts.join('/');
+ };
+
+ exports.sep = '/';
+ exports.delimiter = ':';
+
+ exports.dirname = function(path) {
+ var result = splitPath(path),
+ root = result[0],
+ dir = result[1];
+
+ if (!root && !dir) {
+ // No dirname whatsoever
+ return '.';
+ }
+
+ if (dir) {
+ // It has a dirname, strip trailing slash
+ dir = dir.substr(0, dir.length - 1);
+ }
+
+ return root + dir;
+ };
+
+
+ exports.basename = function(path, ext) {
+ var f = splitPath(path)[2];
+ // TODO: make this comparison case-insensitive on windows?
+ if (ext && f.substr(-1 * ext.length) === ext) {
+ f = f.substr(0, f.length - ext.length);
+ }
+ return f;
+ };
+
+
+ exports.extname = function(path) {
+ return splitPath(path)[3];
+ };
+
+ function filter (xs, f) {
+ if (xs.filter) return xs.filter(f);
+ var res = [];
+ for (var i = 0; i < xs.length; i++) {
+ if (f(xs[i], i, xs)) res.push(xs[i]);
+ }
+ return res;
+ }
+
+ // String.prototype.substr - negative index don't work in IE8
+ var substr = 'ab'.substr(-1) === 'b'
+ ? function (str, start, len) { return str.substr(start, len) }
+ : function (str, start, len) {
+ if (start < 0) start = str.length + start;
+ return str.substr(start, len);
+ }
+ ;
+
+ /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(304)))
+
+/***/ },
+/* 304 */
+/***/ function(module, exports) {
+
+ // shim for using process in browser
+ var process = module.exports = {};
+
+ // cached from whatever global is present so that test runners that stub it
+ // don't break things. But we need to wrap it in a try catch in case it is
+ // wrapped in strict mode code which doesn't define any globals. It's inside a
+ // function because try/catches deoptimize in certain engines.
+
+ var cachedSetTimeout;
+ var cachedClearTimeout;
+
+ function defaultSetTimout() {
+ throw new Error('setTimeout has not been defined');
+ }
+ function defaultClearTimeout () {
+ throw new Error('clearTimeout has not been defined');
+ }
+ (function () {
+ try {
+ if (typeof setTimeout === 'function') {
+ cachedSetTimeout = setTimeout;
+ } else {
+ cachedSetTimeout = defaultSetTimout;
+ }
+ } catch (e) {
+ cachedSetTimeout = defaultSetTimout;
+ }
+ try {
+ if (typeof clearTimeout === 'function') {
+ cachedClearTimeout = clearTimeout;
+ } else {
+ cachedClearTimeout = defaultClearTimeout;
+ }
+ } catch (e) {
+ cachedClearTimeout = defaultClearTimeout;
+ }
+ } ())
+ function runTimeout(fun) {
+ if (cachedSetTimeout === setTimeout) {
+ //normal enviroments in sane situations
+ return setTimeout(fun, 0);
+ }
+ // if setTimeout wasn't available but was latter defined
+ if ((cachedSetTimeout === defaultSetTimout || !cachedSetTimeout) && setTimeout) {
+ cachedSetTimeout = setTimeout;
+ return setTimeout(fun, 0);
+ }
+ try {
+ // when when somebody has screwed with setTimeout but no I.E. maddness
+ return cachedSetTimeout(fun, 0);
+ } catch(e){
+ try {
+ // When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally
+ return cachedSetTimeout.call(null, fun, 0);
+ } catch(e){
+ // same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error
+ return cachedSetTimeout.call(this, fun, 0);
+ }
+ }
+
+
+ }
+ function runClearTimeout(marker) {
+ if (cachedClearTimeout === clearTimeout) {
+ //normal enviroments in sane situations
+ return clearTimeout(marker);
+ }
+ // if clearTimeout wasn't available but was latter defined
+ if ((cachedClearTimeout === defaultClearTimeout || !cachedClearTimeout) && clearTimeout) {
+ cachedClearTimeout = clearTimeout;
+ return clearTimeout(marker);
+ }
+ try {
+ // when when somebody has screwed with setTimeout but no I.E. maddness
+ return cachedClearTimeout(marker);
+ } catch (e){
+ try {
+ // When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally
+ return cachedClearTimeout.call(null, marker);
+ } catch (e){
+ // same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error.
+ // Some versions of I.E. have different rules for clearTimeout vs setTimeout
+ return cachedClearTimeout.call(this, marker);
+ }
+ }
+
+
+
+ }
+ var queue = [];
+ var draining = false;
+ var currentQueue;
+ var queueIndex = -1;
+
+ function cleanUpNextTick() {
+ if (!draining || !currentQueue) {
+ return;
+ }
+ draining = false;
+ if (currentQueue.length) {
+ queue = currentQueue.concat(queue);
+ } else {
+ queueIndex = -1;
+ }
+ if (queue.length) {
+ drainQueue();
+ }
+ }
+
+ function drainQueue() {
+ if (draining) {
+ return;
+ }
+ var timeout = runTimeout(cleanUpNextTick);
+ draining = true;
+
+ var len = queue.length;
+ while(len) {
+ currentQueue = queue;
+ queue = [];
+ while (++queueIndex < len) {
+ if (currentQueue) {
+ currentQueue[queueIndex].run();
+ }
+ }
+ queueIndex = -1;
+ len = queue.length;
+ }
+ currentQueue = null;
+ draining = false;
+ runClearTimeout(timeout);
+ }
+
+ process.nextTick = function (fun) {
+ var args = new Array(arguments.length - 1);
+ if (arguments.length > 1) {
+ for (var i = 1; i < arguments.length; i++) {
+ args[i - 1] = arguments[i];
+ }
+ }
+ queue.push(new Item(fun, args));
+ if (queue.length === 1 && !draining) {
+ runTimeout(drainQueue);
+ }
+ };
+
+ // v8 likes predictible objects
+ function Item(fun, array) {
+ this.fun = fun;
+ this.array = array;
+ }
+ Item.prototype.run = function () {
+ this.fun.apply(null, this.array);
+ };
+ process.title = 'browser';
+ process.browser = true;
+ process.env = {};
+ process.argv = [];
+ process.version = ''; // empty string to avoid regexp issues
+ process.versions = {};
+
+ function noop() {}
+
+ process.on = noop;
+ process.addListener = noop;
+ process.once = noop;
+ process.off = noop;
+ process.removeListener = noop;
+ process.removeAllListeners = noop;
+ process.emit = noop;
+
+ process.binding = function (name) {
+ throw new Error('process.binding is not supported');
+ };
+
+ process.cwd = function () { return '/' };
+ process.chdir = function (dir) {
+ throw new Error('process.chdir is not supported');
+ };
+ process.umask = function() { return 0; };
+
+
+/***/ },
+/* 305 */
+/***/ function(module, exports, __webpack_require__) {
+
+ (function() {
+ var PathSeparator, queryIsLastPathSegment;
+
+ PathSeparator = __webpack_require__(303).sep;
+
+ exports.basenameScore = function(string, query, score) {
+ var base, depth, index, lastCharacter, segmentCount, slashCount;
+ index = string.length - 1;
+ while (string[index] === PathSeparator) {
+ index--;
+ }
+ slashCount = 0;
+ lastCharacter = index;
+ base = null;
+ while (index >= 0) {
+ if (string[index] === PathSeparator) {
+ slashCount++;
+ if (base == null) {
+ base = string.substring(index + 1, lastCharacter + 1);
+ }
+ } else if (index === 0) {
+ if (lastCharacter < string.length - 1) {
+ if (base == null) {
+ base = string.substring(0, lastCharacter + 1);
+ }
+ } else {
+ if (base == null) {
+ base = string;
+ }
+ }
+ }
+ index--;
+ }
+ if (base === string) {
+ score *= 2;
+ } else if (base) {
+ score += exports.score(base, query);
+ }
+ segmentCount = slashCount + 1;
+ depth = Math.max(1, 10 - segmentCount);
+ score *= depth * 0.01;
+ return score;
+ };
+
+ exports.score = function(string, query) {
+ var character, characterScore, indexInQuery, indexInString, lowerCaseIndex, minIndex, queryLength, queryScore, stringLength, totalCharacterScore, upperCaseIndex, _ref;
+ if (string === query) {
+ return 1;
+ }
+ if (queryIsLastPathSegment(string, query)) {
+ return 1;
+ }
+ totalCharacterScore = 0;
+ queryLength = query.length;
+ stringLength = string.length;
+ indexInQuery = 0;
+ indexInString = 0;
+ while (indexInQuery < queryLength) {
+ character = query[indexInQuery++];
+ lowerCaseIndex = string.indexOf(character.toLowerCase());
+ upperCaseIndex = string.indexOf(character.toUpperCase());
+ minIndex = Math.min(lowerCaseIndex, upperCaseIndex);
+ if (minIndex === -1) {
+ minIndex = Math.max(lowerCaseIndex, upperCaseIndex);
+ }
+ indexInString = minIndex;
+ if (indexInString === -1) {
+ return 0;
+ }
+ characterScore = 0.1;
+ if (string[indexInString] === character) {
+ characterScore += 0.1;
+ }
+ if (indexInString === 0 || string[indexInString - 1] === PathSeparator) {
+ characterScore += 0.8;
+ } else if ((_ref = string[indexInString - 1]) === '-' || _ref === '_' || _ref === ' ') {
+ characterScore += 0.7;
+ }
+ string = string.substring(indexInString + 1, stringLength);
+ totalCharacterScore += characterScore;
+ }
+ queryScore = totalCharacterScore / queryLength;
+ return ((queryScore * (queryLength / stringLength)) + queryScore) / 2;
+ };
+
+ queryIsLastPathSegment = function(string, query) {
+ if (string[string.length - query.length - 1] === PathSeparator) {
+ return string.lastIndexOf(query) === string.length - query.length;
+ }
+ };
+
+ exports.match = function(string, query, stringOffset) {
+ var character, indexInQuery, indexInString, lowerCaseIndex, matches, minIndex, queryLength, stringLength, upperCaseIndex, _i, _ref, _results;
+ if (stringOffset == null) {
+ stringOffset = 0;
+ }
+ if (string === query) {
+ return (function() {
+ _results = [];
+ for (var _i = stringOffset, _ref = stringOffset + string.length; stringOffset <= _ref ? _i < _ref : _i > _ref; stringOffset <= _ref ? _i++ : _i--){ _results.push(_i); }
+ return _results;
+ }).apply(this);
+ }
+ queryLength = query.length;
+ stringLength = string.length;
+ indexInQuery = 0;
+ indexInString = 0;
+ matches = [];
+ while (indexInQuery < queryLength) {
+ character = query[indexInQuery++];
+ lowerCaseIndex = string.indexOf(character.toLowerCase());
+ upperCaseIndex = string.indexOf(character.toUpperCase());
+ minIndex = Math.min(lowerCaseIndex, upperCaseIndex);
+ if (minIndex === -1) {
+ minIndex = Math.max(lowerCaseIndex, upperCaseIndex);
+ }
+ indexInString = minIndex;
+ if (indexInString === -1) {
+ return [];
+ }
+ matches.push(stringOffset + indexInString);
+ stringOffset += indexInString + 1;
+ string = string.substring(indexInString + 1, stringLength);
+ }
+ return matches;
+ };
+
+ }).call(this);
+
+
+/***/ },
+/* 306 */
+/***/ function(module, exports, __webpack_require__) {
+
+ (function() {
+ var PathSeparator, legacy_scorer, pluckCandidates, scorer, sortCandidates;
+
+ scorer = __webpack_require__(302);
+
+ legacy_scorer = __webpack_require__(305);
+
+ pluckCandidates = function(a) {
+ return a.candidate;
+ };
+
+ sortCandidates = function(a, b) {
+ return b.score - a.score;
+ };
+
+ PathSeparator = __webpack_require__(303).sep;
+
+ module.exports = function(candidates, query, _arg) {
+ var allowErrors, bAllowErrors, bKey, candidate, coreQuery, key, legacy, maxInners, maxResults, prepQuery, queryHasSlashes, score, scoredCandidates, spotLeft, string, _i, _j, _len, _len1, _ref;
+ _ref = _arg != null ? _arg : {}, key = _ref.key, maxResults = _ref.maxResults, maxInners = _ref.maxInners, allowErrors = _ref.allowErrors, legacy = _ref.legacy;
+ scoredCandidates = [];
+ spotLeft = (maxInners != null) && maxInners > 0 ? maxInners : candidates.length;
+ bAllowErrors = !!allowErrors;
+ bKey = key != null;
+ prepQuery = scorer.prepQuery(query);
+ if (!legacy) {
+ for (_i = 0, _len = candidates.length; _i < _len; _i++) {
+ candidate = candidates[_i];
+ string = bKey ? candidate[key] : candidate;
+ if (!string) {
+ continue;
+ }
+ score = scorer.score(string, query, prepQuery, bAllowErrors);
+ if (score > 0) {
+ scoredCandidates.push({
+ candidate: candidate,
+ score: score
+ });
+ if (!--spotLeft) {
+ break;
+ }
+ }
+ }
+ } else {
+ queryHasSlashes = prepQuery.depth > 0;
+ coreQuery = prepQuery.core;
+ for (_j = 0, _len1 = candidates.length; _j < _len1; _j++) {
+ candidate = candidates[_j];
+ string = key != null ? candidate[key] : candidate;
+ if (!string) {
+ continue;
+ }
+ score = legacy_scorer.score(string, coreQuery, queryHasSlashes);
+ if (!queryHasSlashes) {
+ score = legacy_scorer.basenameScore(string, coreQuery, score);
+ }
+ if (score > 0) {
+ scoredCandidates.push({
+ candidate: candidate,
+ score: score
+ });
+ }
+ }
+ }
+ scoredCandidates.sort(sortCandidates);
+ candidates = scoredCandidates.map(pluckCandidates);
+ if (maxResults != null) {
+ candidates = candidates.slice(0, maxResults);
+ }
+ return candidates;
+ };
+
+ }).call(this);
+
+
+/***/ },
+/* 307 */
+/***/ function(module, exports, __webpack_require__) {
+
+ (function() {
+ var PathSeparator, scorer;
+
+ PathSeparator = __webpack_require__(303).sep;
+
+ scorer = __webpack_require__(302);
+
+ exports.basenameMatch = function(subject, subject_lw, prepQuery) {
+ var basePos, depth, end;
+ end = subject.length - 1;
+ while (subject[end] === PathSeparator) {
+ end--;
+ }
+ basePos = subject.lastIndexOf(PathSeparator, end);
+ if (basePos === -1) {
+ return [];
+ }
+ depth = prepQuery.depth;
+ while (depth-- > 0) {
+ basePos = subject.lastIndexOf(PathSeparator, basePos - 1);
+ if (basePos === -1) {
+ return [];
+ }
+ }
+ basePos++;
+ end++;
+ return exports.match(subject.slice(basePos, end), subject_lw.slice(basePos, end), prepQuery, basePos);
+ };
+
+ exports.mergeMatches = function(a, b) {
+ var ai, bj, i, j, m, n, out;
+ m = a.length;
+ n = b.length;
+ if (n === 0) {
+ return a.slice();
+ }
+ if (m === 0) {
+ return b.slice();
+ }
+ i = -1;
+ j = 0;
+ bj = b[j];
+ out = [];
+ while (++i < m) {
+ ai = a[i];
+ while (bj <= ai && ++j < n) {
+ if (bj < ai) {
+ out.push(bj);
+ }
+ bj = b[j];
+ }
+ out.push(ai);
+ }
+ while (j < n) {
+ out.push(b[j++]);
+ }
+ return out;
+ };
+
+ exports.match = function(subject, subject_lw, prepQuery, offset) {
+ var DIAGONAL, LEFT, STOP, UP, acro_score, align, backtrack, csc_diag, csc_row, csc_score, i, j, m, matches, move, n, pos, query, query_lw, score, score_diag, score_row, score_up, si_lw, start, trace;
+ if (offset == null) {
+ offset = 0;
+ }
+ query = prepQuery.query;
+ query_lw = prepQuery.query_lw;
+ m = subject.length;
+ n = query.length;
+ acro_score = scorer.scoreAcronyms(subject, subject_lw, query, query_lw).score;
+ score_row = new Array(n);
+ csc_row = new Array(n);
+ STOP = 0;
+ UP = 1;
+ LEFT = 2;
+ DIAGONAL = 3;
+ trace = new Array(m * n);
+ pos = -1;
+ j = -1;
+ while (++j < n) {
+ score_row[j] = 0;
+ csc_row[j] = 0;
+ }
+ i = -1;
+ while (++i < m) {
+ score = 0;
+ score_up = 0;
+ csc_diag = 0;
+ si_lw = subject_lw[i];
+ j = -1;
+ while (++j < n) {
+ csc_score = 0;
+ align = 0;
+ score_diag = score_up;
+ if (query_lw[j] === si_lw) {
+ start = scorer.isWordStart(i, subject, subject_lw);
+ csc_score = csc_diag > 0 ? csc_diag : scorer.scoreConsecutives(subject, subject_lw, query, query_lw, i, j, start);
+ align = score_diag + scorer.scoreCharacter(i, j, start, acro_score, csc_score);
+ }
+ score_up = score_row[j];
+ csc_diag = csc_row[j];
+ if (score > score_up) {
+ move = LEFT;
+ } else {
+ score = score_up;
+ move = UP;
+ }
+ if (align > score) {
+ score = align;
+ move = DIAGONAL;
+ } else {
+ csc_score = 0;
+ }
+ score_row[j] = score;
+ csc_row[j] = csc_score;
+ trace[++pos] = score > 0 ? move : STOP;
+ }
+ }
+ i = m - 1;
+ j = n - 1;
+ pos = i * n + j;
+ backtrack = true;
+ matches = [];
+ while (backtrack && i >= 0 && j >= 0) {
+ switch (trace[pos]) {
+ case UP:
+ i--;
+ pos -= n;
+ break;
+ case LEFT:
+ j--;
+ pos--;
+ break;
+ case DIAGONAL:
+ matches.push(i + offset);
+ j--;
+ i--;
+ pos -= n + 1;
+ break;
+ default:
+ backtrack = false;
+ }
+ }
+ matches.reverse();
+ return matches;
+ };
+
+ }).call(this);
+
+
+/***/ },
+/* 308 */
+/***/ function(module, exports) {
+
+ // removed by extract-text-webpack-plugin
+
+/***/ },
+/* 309 */,
+/* 310 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /**
+ * This file maps the SVG React Components in the public/images directory.
+ */
+ var Svg = __webpack_require__(311);
+ module.exports = Svg;
+
+/***/ },
+/* 311 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var React = __webpack_require__(2);
+ var InlineSVG = __webpack_require__(312);
+
+ var svg = {
+ "angle-brackets": __webpack_require__(313),
+ "arrow": __webpack_require__(314),
+ "blackBox": __webpack_require__(315),
+ "breakpoint": __webpack_require__(316),
+ "close": __webpack_require__(317),
+ "domain": __webpack_require__(318),
+ "file": __webpack_require__(319),
+ "folder": __webpack_require__(320),
+ "globe": __webpack_require__(321),
+ "magnifying-glass": __webpack_require__(322),
+ "pause": __webpack_require__(323),
+ "pause-exceptions": __webpack_require__(324),
+ "plus": __webpack_require__(325),
+ "prettyPrint": __webpack_require__(326),
+ "resume": __webpack_require__(327),
+ "settings": __webpack_require__(328),
+ "stepIn": __webpack_require__(329),
+ "stepOut": __webpack_require__(330),
+ "stepOver": __webpack_require__(331),
+ "subSettings": __webpack_require__(332),
+ "toggleBreakpoints": __webpack_require__(333),
+ "worker": __webpack_require__(334),
+ "sad-face": __webpack_require__(335)
+ };
+
+ module.exports = function (name, props) {
+ // eslint-disable-line
+ if (!svg[name]) {
+ throw new Error("Unknown SVG: " + name);
+ }
+ var className = name;
+ if (props && props.className) {
+ className = `${ name } ${ props.className }`;
+ }
+ if (name === "subSettings") {
+ className = "";
+ }
+ props = Object.assign({}, props, { className, src: svg[name] });
+ return React.createElement(InlineSVG, props);
+ };
+
+/***/ },
+/* 312 */
+/***/ function(module, exports, __webpack_require__) {
+
+ 'use strict';
+
+ Object.defineProperty(exports, '__esModule', {
+ value: true
+ });
+
+ var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
+
+ var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })();
+
+ var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; desc = parent = undefined; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } };
+
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
+
+ function _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; }
+
+ function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }
+
+ function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
+
+ var _react = __webpack_require__(2);
+
+ var _react2 = _interopRequireDefault(_react);
+
+ var DOMParser = typeof window !== 'undefined' && window.DOMParser;
+ var process = process || {};
+ process.env = process.env || {};
+ var parserAvailable = typeof DOMParser !== 'undefined' && DOMParser.prototype != null && DOMParser.prototype.parseFromString != null;
+
+ if ("production" !== process.env.NODE_ENV && !parserAvailable) {
+ console.info('<InlineSVG />: `raw` prop works only when `window.DOMParser` exists.');
+ }
+
+ function isParsable(src) {
+ // kinda naive but meh, ain't gonna use full-blown parser for this
+ return parserAvailable && typeof src === 'string' && src.trim().substr(0, 4) === '<svg';
+ }
+
+ // parse SVG string using `DOMParser`
+ function parseFromSVGString(src) {
+ var parser = new DOMParser();
+ return parser.parseFromString(src, "image/svg+xml");
+ }
+
+ // Transform DOM prop/attr names applicable to `<svg>` element but react-limited
+ function switchSVGAttrToReactProp(propName) {
+ switch (propName) {
+ case 'class':
+ return 'className';
+ default:
+ return propName;
+ }
+ }
+
+ var InlineSVG = (function (_React$Component) {
+ _inherits(InlineSVG, _React$Component);
+
+ _createClass(InlineSVG, null, [{
+ key: 'defaultProps',
+ value: {
+ element: 'i',
+ raw: false,
+ src: ''
+ },
+ enumerable: true
+ }, {
+ key: 'propTypes',
+ value: {
+ src: _react2['default'].PropTypes.string.isRequired,
+ element: _react2['default'].PropTypes.string,
+ raw: _react2['default'].PropTypes.bool
+ },
+ enumerable: true
+ }]);
+
+ function InlineSVG(props) {
+ _classCallCheck(this, InlineSVG);
+
+ _get(Object.getPrototypeOf(InlineSVG.prototype), 'constructor', this).call(this, props);
+ this._extractSVGProps = this._extractSVGProps.bind(this);
+ }
+
+ // Serialize `Attr` objects in `NamedNodeMap`
+
+ _createClass(InlineSVG, [{
+ key: '_serializeAttrs',
+ value: function _serializeAttrs(map) {
+ var ret = {};
+ var prop = undefined;
+ for (var i = 0; i < map.length; i++) {
+ prop = switchSVGAttrToReactProp(map[i].name);
+ ret[prop] = map[i].value;
+ }
+ return ret;
+ }
+
+ // get <svg /> element props
+ }, {
+ key: '_extractSVGProps',
+ value: function _extractSVGProps(src) {
+ var map = parseFromSVGString(src).documentElement.attributes;
+ return map.length > 0 ? this._serializeAttrs(map) : null;
+ }
+
+ // get content inside <svg> element.
+ }, {
+ key: '_stripSVG',
+ value: function _stripSVG(src) {
+ return parseFromSVGString(src).documentElement.innerHTML;
+ }
+ }, {
+ key: 'componentWillReceiveProps',
+ value: function componentWillReceiveProps(_ref) {
+ var children = _ref.children;
+
+ if ("production" !== process.env.NODE_ENV && children != null) {
+ console.info('<InlineSVG />: `children` prop will be ignored.');
+ }
+ }
+ }, {
+ key: 'render',
+ value: function render() {
+ var Element = undefined,
+ __html = undefined,
+ svgProps = undefined;
+ var _props = this.props;
+ var element = _props.element;
+ var raw = _props.raw;
+ var src = _props.src;
+
+ var otherProps = _objectWithoutProperties(_props, ['element', 'raw', 'src']);
+
+ if (raw === true && isParsable(src)) {
+ Element = 'svg';
+ svgProps = this._extractSVGProps(src);
+ __html = this._stripSVG(src);
+ }
+ __html = __html || src;
+ Element = Element || element;
+ svgProps = svgProps || {};
+
+ return _react2['default'].createElement(Element, _extends({}, svgProps, otherProps, { src: null, children: null,
+ dangerouslySetInnerHTML: { __html: __html } }));
+ }
+ }]);
+
+ return InlineSVG;
+ })(_react2['default'].Component);
+
+ exports['default'] = InlineSVG;
+ module.exports = exports['default'];
+
+/***/ },
+/* 313 */
+/***/ function(module, exports) {
+
+ module.exports = "<!-- 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/. --><svg viewBox=\"-1 73 16 11\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\"><g id=\"Shape-Copy-3-+-Shape-Copy-4\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" transform=\"translate(0.000000, 74.000000)\"><path d=\"M0.749321284,4.16081709 L4.43130681,0.242526751 C4.66815444,-0.00952143591 5.06030999,-0.0211407611 5.30721074,0.216574262 C5.55411149,0.454289284 5.56226116,0.851320812 5.32541353,1.103369 L1.95384971,4.69131519 L5.48809879,8.09407556 C5.73499955,8.33179058 5.74314922,8.72882211 5.50630159,8.9808703 C5.26945396,9.23291849 4.87729841,9.24453781 4.63039766,9.00682279 L0.827097345,5.34502101 C0.749816996,5.31670099 0.677016974,5.27216098 0.613753508,5.21125118 C0.427367989,5.03179997 0.377040713,4.7615583 0.465458792,4.53143559 C0.492371834,4.43667624 0.541703274,4.34676528 0.613628034,4.27022448 C0.654709457,4.22650651 0.70046335,4.19002189 0.749321284,4.16081709 Z\" id=\"Shape-Copy-3\" stroke=\"#FFFFFF\" stroke-width=\"0.05\" fill=\"#DDE1E4\"></path><path d=\"M13.7119065,5.44453032 L9.77062746,9.09174784 C9.51677479,9.3266604 9.12476399,9.31089603 8.89504684,9.05653714 C8.66532968,8.80217826 8.68489539,8.40554539 8.93874806,8.17063283 L12.5546008,4.82456128 L9.26827469,1.18571135 C9.03855754,0.931352463 9.05812324,0.534719593 9.31197591,0.299807038 C9.56582858,0.0648944831 9.95783938,0.0806588502 10.1875565,0.335017737 L13.72891,4.25625178 C13.8013755,4.28980469 13.8684335,4.3382578 13.9254821,4.40142604 C14.0883019,4.58171146 14.1258883,4.83347168 14.0435812,5.04846202 C14.0126705,5.15680232 13.9526426,5.2583679 13.8641331,5.34027361 C13.8174417,5.38348136 13.7660763,5.41820853 13.7119065,5.44453032 Z\" id=\"Shape-Copy-4\" stroke=\"#FFFFFF\" stroke-width=\"0.05\" fill=\"#DDE1E4\"></path></g></svg>"
+
+/***/ },
+/* 314 */
+/***/ function(module, exports) {
+
+ module.exports = "<!-- 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/. --><svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" viewBox=\"0 0 16 16\"><path d=\"M8 13.4c-.5 0-.9-.2-1.2-.6L.4 5.2C0 4.7-.1 4.3.2 3.7S1 3 1.6 3h12.8c.6 0 1.2.1 1.4.7.3.6.2 1.1-.2 1.6l-6.4 7.6c-.3.4-.7.5-1.2.5z\"></path></svg>"
+
+/***/ },
+/* 315 */
+/***/ function(module, exports) {
+
+ module.exports = "<!-- 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/. --><svg viewBox=\"0 0 16 16\" xmlns=\"http://www.w3.org/2000/svg\"><g fill-rule=\"evenodd\"><circle cx=\"8\" cy=\"8.5\" r=\"1.5\"></circle><path d=\"M15.498 8.28l-.001-.03v-.002-.004l-.002-.018-.004-.031c0-.002 0-.002 0 0l-.004-.035.006.082c-.037-.296-.133-.501-.28-.661-.4-.522-.915-1.042-1.562-1.604-1.36-1.182-2.74-1.975-4.178-2.309a6.544 6.544 0 0 0-2.755-.042c-.78.153-1.565.462-2.369.91C3.252 5.147 2.207 6 1.252 7.035c-.216.233-.36.398-.499.577-.338.437-.338 1 0 1.437.428.552.941 1.072 1.59 1.635 1.359 1.181 2.739 1.975 4.177 2.308.907.21 1.829.223 2.756.043.78-.153 1.564-.462 2.369-.91 1.097-.612 2.141-1.464 3.097-2.499.217-.235.36-.398.498-.578.12-.128.216-.334.248-.554 0 .01 0 .01-.008.04l.013-.079-.001.011.003-.031.001-.017v.005l.001-.02v.008l.002-.03.001-.05-.001-.044v-.004-.004zm-.954.045v.007l.001.004V8.33v.012l-.001.01v-.005-.005l.002-.015-.001.008c-.002.014-.002.014 0 0l-.007.084c.003-.057-.004-.041-.014-.031-.143.182-.27.327-.468.543-.89.963-1.856 1.752-2.86 2.311-.724.404-1.419.677-2.095.81a5.63 5.63 0 0 1-2.374-.036c-1.273-.295-2.523-1.014-3.774-2.101-.604-.525-1.075-1.001-1.457-1.496-.054-.07-.054-.107 0-.177.117-.152.244-.298.442-.512.89-.963 1.856-1.752 2.86-2.311.724-.404 1.419-.678 2.095-.81a5.631 5.631 0 0 1 2.374.036c1.272.295 2.523 1.014 3.774 2.101.603.524 1.074 1 1.457 1.496.035.041.043.057.046.076 0 .01 0 .01.008.043l-.009-.047.003.02-.002-.013v-.008.016c0-.004 0-.004 0 0v-.004z\"></path></g></svg>"
+
+/***/ },
+/* 316 */
+/***/ function(module, exports) {
+
+ module.exports = "<!-- 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/. --><svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" viewBox=\"0 0 60 12\"><path id=\"base-path\" d=\"M53.9,0H1C0.4,0,0,0.4,0,1v10c0,0.6,0.4,1,1,1h52.9c0.6,0,1.2-0.3,1.5-0.7L60,6l-4.4-5.3C55,0.3,54.5,0,53.9,0z\"></path></svg>"
+
+/***/ },
+/* 317 */
+/***/ function(module, exports) {
+
+ module.exports = "<!-- 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/. --><svg viewBox=\"0 0 6 6\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\"><path d=\"M1.35191454,5.27895256 L5.31214367,1.35518468 C5.50830675,1.16082764 5.50977084,0.844248536 5.3154138,0.648085456 C5.12105677,0.451922377 4.80447766,0.450458288 4.60831458,0.644815324 L0.648085456,4.56858321 C0.451922377,4.76294025 0.450458288,5.07951935 0.644815324,5.27568243 C0.83917236,5.47184551 1.15575146,5.4733096 1.35191454,5.27895256 L1.35191454,5.27895256 Z\" id=\"Line\" stroke=\"none\" fill=\"#696969\" fill-rule=\"evenodd\"></path><path d=\"M5.31214367,4.56858321 L1.35191454,0.644815324 C1.15575146,0.450458288 0.83917236,0.451922377 0.644815324,0.648085456 C0.450458288,0.844248536 0.451922377,1.16082764 0.648085456,1.35518468 L4.60831458,5.27895256 C4.80447766,5.4733096 5.12105677,5.47184551 5.3154138,5.27568243 C5.50977084,5.07951935 5.50830675,4.76294025 5.31214367,4.56858321 L5.31214367,4.56858321 Z\" id=\"Line-Copy-2\" stroke=\"none\" fill=\"#696969\" fill-rule=\"evenodd\"></path></svg>"
+
+/***/ },
+/* 318 */
+/***/ function(module, exports) {
+
+ module.exports = "<!-- 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/. --><svg viewBox=\"0 0 16 16\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M9.05 4.634l-2.144.003-.116.116v1.445l.92.965.492.034.116-.116v-.617L9.13 5.7l.035-.95M12.482 10.38l-1.505-1.462H9.362l-.564.516-.034 1.108.72.768 1.323.034-.117-.116v1.2l.972 1.02.315.034.116-.116v-1.154l.422-.374.034-.927-.117.117h.26l.408-.36V10.5l-.125-.124-.575-.033\"></path><path d=\"M8.47 15.073c-3.088 0-5.6-2.513-5.6-5.602V9.4v-.003c0-.018 0-.018.002-.034l.182-.088.724.587.49.033.497.543-.034.9.317.383h.47l.114.096-.032 1.9.524.553h.105l.025-.338 1.004-.95.054-.474.53-.462v-.888l-.588-.038-1.118-1.155H4.48l-.154-.09V9.01l.155-.1h1.164v-.273l.12-.115.7.033.494-.443.034-.746-.624-.655h-.724v.28l-.11.07H4.64l-.114-.09.025-.64.48-.43v-.244h-.382c-.102 0-.152-.128-.08-.2 1.04-1.01 2.428-1.59 3.903-1.59 1.374 0 2.672.5 3.688 1.39.08.068.03.198-.075.198l-1.144-.034-.81.803.52.523v.16l-.382.388h-.158l-.176-.177v-.16l.076-.074-.252-.252-.37.362.53.53c.072.072.005.194-.096.194l-.752-.005v.844h.783L9.885 8l.16-.143h.16l.62.61v.267l.58.027.003.002V8.76l.18-.03 1.234 1.24.753-.708h.382l.116.108c0 .02.003.016.003.036v.065c0 3.09-2.515 5.603-5.605 5.603M8.47 3C4.904 3 2 5.903 2 9.47c0 3.57 2.903 6.472 6.47 6.472 3.57 0 6.472-2.903 6.472-6.47C14.942 5.9 12.04 3 8.472 3\"></path></svg>"
+
+/***/ },
+/* 319 */
+/***/ function(module, exports) {
+
+ module.exports = "<!-- 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/. --><svg viewBox=\"0 0 16 16\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M4 2v12h9V4.775L9.888 2H4zm0-1h5.888c.246 0 .483.09.666.254l3.112 2.774c.212.19.334.462.334.747V14c0 .552-.448 1-1 1H4c-.552 0-1-.448-1-1V2c0-.552.448-1 1-1z\"></path><path d=\"M9 1.5v4c0 .325.306.564.62.485l4-1c.27-.067.432-.338.365-.606-.067-.27-.338-.432-.606-.365l-4 1L10 5.5v-4c0-.276-.224-.5-.5-.5s-.5.224-.5.5z\"></path></svg>"
+
+/***/ },
+/* 320 */
+/***/ function(module, exports) {
+
+ module.exports = "<!-- 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/. --><svg viewBox=\"0 0 16 16\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M2 5.193v7.652c0 .003-.002 0 .007 0H14v-7.69c0-.003.002 0-.007 0h-7.53v-2.15c0-.002-.004-.005-.01-.005H2.01C2 3 2 3 2 3.005V5.193zm-1 0V3.005C1 2.45 1.444 2 2.01 2h4.442c.558 0 1.01.45 1.01 1.005v1.15h6.53c.557 0 1.008.44 1.008 1v7.69c0 .553-.45 1-1.007 1H2.007c-.556 0-1.007-.44-1.007-1V5.193zM6.08 4.15H2v1h4.46v-1h-.38z\" fill-rule=\"evenodd\"></path></svg>"
+
+/***/ },
+/* 321 */
+/***/ function(module, exports) {
+
+ module.exports = "<!-- 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/. --><svg viewBox=\"14 6 13 12\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\"><g id=\"world\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" transform=\"translate(14.000000, 6.000000)\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M6.35076107,0.354 C3.25095418,0.354 0.729,2.87582735 0.729,5.9758879 C0.729,9.07544113 3.25082735,11.5972685 6.35076107,11.5972685 C9.45044113,11.5972685 11.9723953,9.07544113 11.9723953,5.97576107 C11.9723953,2.87582735 9.45044113,0.354 6.35076107,0.354 L6.35076107,0.354 Z M6.35076107,10.8289121 C3.67445071,10.8289121 1.49722956,8.65181776 1.49722956,5.97576107 C1.49722956,5.9443064 1.49900522,5.91335907 1.49976622,5.88215806 L2.20090094,6.4213266 L2.56313696,6.4213266 L2.97268183,6.8306178 L2.97268183,7.68217686 L3.32324919,8.03287105 L3.73926255,8.03287105 L3.73926255,9.79940584 L4.27386509,10.3361645 L4.4591686,10.3361645 L4.4591686,10.000183 L5.37655417,9.08343163 L5.37655417,8.73400577 L5.85585737,8.25203907 L5.85585737,7.37206934 L5.32518666,7.37206934 L4.28439226,6.33140176 L2.82225748,6.33140176 L2.82225748,5.56938704 L3.96286973,5.56938704 L3.96286973,5.23949352 L4.65068695,5.23949352 L5.11477015,4.77667865 L5.11477015,4.03001076 L4.49087694,3.40662489 L3.75359472,3.40662489 L3.75359472,3.78725175 L2.96228149,3.78725175 L2.96228149,3.28385021 L3.42217919,2.82319151 L3.42217919,2.49786399 L2.97001833,2.49786399 C3.84466106,1.64744643 5.03714814,1.12222956 6.35063424,1.12222956 C7.57292716,1.12222956 8.69020207,1.57730759 9.54442463,2.32587797 L8.46164839,2.32587797 L7.680355,3.10666403 L8.21508437,3.64088607 L7.87238068,3.98257509 L7.7165025,3.82669692 L7.85297518,3.68946324 L7.78930484,3.62566607 L7.78943167,3.62566607 L7.56011699,3.39559038 L7.55986332,3.39571722 L7.49758815,3.33318838 L7.01904595,3.78585658 L7.55910232,4.32654712 L6.8069806,4.32198112 L6.8069806,5.25864535 L7.66716433,5.25864535 L7.6723645,4.72112565 L7.81289584,4.57996014 L8.31819988,5.08653251 L8.31819988,5.41921636 L9.00703176,5.41921636 L9.03366676,5.39321553 L9.03430093,5.39194719 L10.195587,6.55259911 L10.8637451,5.88520206 L11.2018828,5.88520206 C11.2023901,5.9153884 11.2041658,5.94532107 11.2041658,5.97563424 C11.2040389,8.65181776 9.0269446,10.8289121 6.35076107,10.8289121 L6.35076107,10.8289121 Z\" id=\"Shape\" stroke=\"#DDE1E5\" stroke-width=\"0.25\" fill=\"#DDE1E5\"></path><polygon id=\"Shape\" stroke=\"#DDE1E5\" stroke-width=\"0.25\" fill=\"#DDE1E5\" points=\"6.50676608 1.61523076 4.52892694 1.61789426 4.52892694 2.95192735 5.34560683 3.76733891 5.72496536 3.76733891 5.72496536 3.1967157 6.50676608 2.41592965\"></polygon><polygon id=\"Shape\" stroke=\"#DDE1E5\" stroke-width=\"0.25\" fill=\"#DDE1E5\" points=\"9.59959714 6.88718547 8.28623788 5.57268471 8.28623788 5.57002121 6.79607294 5.57002121 6.35101474 6.01469891 6.35101474 6.96201714 6.98429362 7.59466185 8.12909136 7.59466185 8.12909136 8.70343893 8.99434843 9.56882283 9.20971144 9.56882283 9.20971144 8.50329592 9.63029081 8.08271655 9.63029081 7.3026915 9.87025949 7.3026915 10.1711082 7.00082814 10.0558167 6.88718547\"></polygon></g></svg>"
+
+/***/ },
+/* 322 */
+/***/ function(module, exports) {
+
+ module.exports = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\"><path class=\"st0\" d=\"M9 9.3l3.6 3.6\"></path><ellipse fill=\"transparent\" cx=\"5.9\" cy=\"6.2\" rx=\"4.5\" ry=\"4.5\"></ellipse></svg>"
+
+/***/ },
+/* 323 */
+/***/ function(module, exports) {
+
+ module.exports = "<!-- 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/. --><svg viewBox=\"0 0 16 16\" xmlns=\"http://www.w3.org/2000/svg\"><g fill-rule=\"evenodd\"><path d=\"M6.5 12.003l.052-9a.5.5 0 1 0-1-.006l-.052 9a.5.5 0 1 0 1 .006zM13 11.997l-.05-9a.488.488 0 0 0-.477-.497.488.488 0 0 0-.473.503l.05 9a.488.488 0 0 0 .477.497.488.488 0 0 0 .473-.503z\"></path></g></svg>"
+
+/***/ },
+/* 324 */
+/***/ function(module, exports) {
+
+ module.exports = "<!-- 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/. --><svg viewBox=\"0 0 16 16\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M10.483 13.995H5.517l-3.512-3.512V5.516l3.512-3.512h4.966l3.512 3.512v4.967l-3.512 3.512zm4.37-9.042l-3.807-3.805A.503.503 0 0 0 10.691 1H5.309a.503.503 0 0 0-.356.148L1.147 4.953A.502.502 0 0 0 1 5.308v5.383c0 .134.053.262.147.356l3.806 3.806a.503.503 0 0 0 .356.147h5.382a.503.503 0 0 0 .355-.147l3.806-3.806A.502.502 0 0 0 15 10.69V5.308a.502.502 0 0 0-.147-.355z\"></path><path d=\"M10 10.5a.5.5 0 1 0 1 0v-5a.5.5 0 1 0-1 0v5zM5 10.5a.5.5 0 1 0 1 0v-5a.5.5 0 0 0-1 0v5z\"></path></svg>"
+
+/***/ },
+/* 325 */
+/***/ function(module, exports) {
+
+ module.exports = "<!-- 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/. --><svg viewBox=\"0 0 16 16\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M8.5 8.5V14a.5.5 0 1 1-1 0V8.5H2a.5.5 0 0 1 0-1h5.5V2a.5.5 0 0 1 1 0v5.5H14a.5.5 0 1 1 0 1H8.5z\" fill-rule=\"evenodd\"></path></svg>"
+
+/***/ },
+/* 326 */
+/***/ function(module, exports) {
+
+ module.exports = "<!-- 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/. --><svg viewBox=\"0 0 16 16\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M4.525 13.21h-.472c-.574 0-.987-.154-1.24-.463-.253-.31-.38-.882-.38-1.719v-.573c0-.746-.097-1.265-.292-1.557-.196-.293-.51-.44-.945-.44v-.974c.435 0 .75-.146.945-.44.195-.292.293-.811.293-1.556v-.58c0-.833.126-1.404.379-1.712.253-.31.666-.464 1.24-.464h.472v.783h-.179c-.37 0-.628.08-.774.24-.145.159-.218.54-.218 1.141v.383c0 .824-.096 1.432-.287 1.823-.191.39-.516.679-.974.866.458.191.783.482.974.873.191.39.287.998.287 1.823v.382c0 .602.073.982.218 1.142.146.16.404.239.774.239h.18v.783zm9.502-4.752c-.43 0-.744.147-.942.44-.197.292-.296.811-.296 1.557v.573c0 .837-.125 1.41-.376 1.719-.251.309-.664.463-1.237.463h-.478v-.783h.185c.37 0 .628-.08.774-.24.145-.159.218-.539.218-1.14v-.383c0-.825.096-1.433.287-1.823.191-.39.516-.682.974-.873-.458-.187-.783-.476-.974-.866-.191-.391-.287-.999-.287-1.823v-.383c0-.602-.073-.982-.218-1.142-.146-.159-.404-.239-.774-.239h-.185v-.783h.478c.573 0 .986.155 1.237.464.25.308.376.88.376 1.712v.58c0 .673.088 1.174.263 1.503.176.329.5.493.975.493v.974z\" fill-rule=\"evenodd\"></path></svg>"
+
+/***/ },
+/* 327 */
+/***/ function(module, exports) {
+
+ module.exports = "<!-- 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/. --><svg viewBox=\"0 0 16 16\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M6.925 12.5l7.4-5-7.4-5v10zM6 12.5v-10c0-.785.8-1.264 1.415-.848l7.4 5c.58.392.58 1.304 0 1.696l-7.4 5C6.8 13.764 6 13.285 6 12.5z\" fill-rule=\"evenodd\"></path></svg>"
+
+/***/ },
+/* 328 */
+/***/ function(module, exports) {
+
+ module.exports = "<!-- 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/. --><svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" viewBox=\"0 0 33 12\"><path id=\"base-path\" d=\"M27.1,0H1C0.4,0,0,0.4,0,1v10c0,0.6,0.4,1,1,1h26.1 c0.6,0,1.2-0.3,1.5-0.7L33,6l-4.4-5.3C28.2,0.3,27.7,0,27.1,0z\"></path></svg>"
+
+/***/ },
+/* 329 */
+/***/ function(module, exports) {
+
+ module.exports = "<!-- 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/. --><svg viewBox=\"0 0 16 16\" xmlns=\"http://www.w3.org/2000/svg\"><g fill-rule=\"evenodd\"><path d=\"M1.5 14.042h4.095a.5.5 0 0 0 0-1H1.5a.5.5 0 1 0 0 1zM7.983 2a.5.5 0 0 1 .517.5v7.483l3.136-3.326a.5.5 0 1 1 .728.686l-4 4.243a.499.499 0 0 1-.73-.004L3.635 7.343a.5.5 0 0 1 .728-.686L7.5 9.983V3H1.536C1.24 3 1 2.776 1 2.5s.24-.5.536-.5h6.447zM10.5 14.042h4.095a.5.5 0 0 0 0-1H10.5a.5.5 0 1 0 0 1z\"></path></g></svg>"
+
+/***/ },
+/* 330 */
+/***/ function(module, exports) {
+
+ module.exports = "<!-- 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/. --><svg viewBox=\"0 0 16 16\" xmlns=\"http://www.w3.org/2000/svg\"><g fill-rule=\"evenodd\"><path d=\"M5 13.5H1a.5.5 0 1 0 0 1h4a.5.5 0 1 0 0-1zM12 13.5H8a.5.5 0 1 0 0 1h4a.5.5 0 1 0 0-1zM6.11 5.012A.427.427 0 0 1 6.21 5h7.083L9.646 1.354a.5.5 0 1 1 .708-.708l4.5 4.5a.498.498 0 0 1 0 .708l-4.5 4.5a.5.5 0 0 1-.708-.708L13.293 6H6.5v5.5a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .61-.488z\"></path></g></svg>"
+
+/***/ },
+/* 331 */
+/***/ function(module, exports) {
+
+ module.exports = "<!-- 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/. --><svg viewBox=\"0 0 16 16\" xmlns=\"http://www.w3.org/2000/svg\"><g fill-rule=\"evenodd\"><path d=\"M13.297 6.912C12.595 4.39 10.167 2.5 7.398 2.5A5.898 5.898 0 0 0 1.5 8.398a.5.5 0 0 0 1 0A4.898 4.898 0 0 1 7.398 3.5c2.75 0 5.102 2.236 5.102 4.898v.004L8.669 7.029a.5.5 0 0 0-.338.942l4.462 1.598a.5.5 0 0 0 .651-.34.506.506 0 0 0 .02-.043l2-5a.5.5 0 1 0-.928-.372l-1.24 3.098z\"></path><circle cx=\"7\" cy=\"12\" r=\"1\"></circle></g></svg>"
+
+/***/ },
+/* 332 */
+/***/ function(module, exports) {
+
+ module.exports = "<!-- 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/. --><svg viewBox=\"0 0 16 16\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M12.219 7c.345 0 .635.117.869.352.234.234.351.524.351.869 0 .351-.118.652-.356.903-.238.25-.526.376-.864.376-.332 0-.615-.125-.85-.376a1.276 1.276 0 0 1-.351-.903A1.185 1.185 0 0 1 12.218 7zM8.234 7c.345 0 .635.117.87.352.234.234.351.524.351.869 0 .351-.119.652-.356.903-.238.25-.526.376-.865.376-.332 0-.613-.125-.844-.376a1.286 1.286 0 0 1-.347-.903c0-.352.114-.643.342-.874.228-.231.51-.347.85-.347zM4.201 7c.339 0 .627.117.864.352.238.234.357.524.357.869 0 .351-.119.652-.357.903-.237.25-.525.376-.864.376-.338 0-.623-.125-.854-.376A1.286 1.286 0 0 1 3 8.221 1.185 1.185 0 0 1 4.201 7z\" fill-rule=\"evenodd\"></path></svg>"
+
+/***/ },
+/* 333 */
+/***/ function(module, exports) {
+
+ module.exports = "<!-- 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/. --><svg viewBox=\"0 0 16 16\" xmlns=\"http://www.w3.org/2000/svg\"><g fill-rule=\"evenodd\"><path d=\"M3.233 11.25l-.417 1H1.712C.763 12.25 0 11.574 0 10.747V6.503C0 5.675.755 5 1.712 5h4.127l-.417 1H1.597C1.257 6 1 6.225 1 6.503v4.244c0 .277.267.503.597.503h1.636zM7.405 11.27L7 12.306c.865.01 2.212-.024 2.315-.04.112-.016.112-.016.185-.035.075-.02.156-.046.251-.082.152-.056.349-.138.592-.244.415-.182.962-.435 1.612-.744l.138-.066a179.35 179.35 0 0 0 2.255-1.094c1.191-.546 1.191-2.074-.025-2.632l-.737-.34a3547.554 3547.554 0 0 0-3.854-1.78c-.029.11-.065.222-.11.336l-.232.596c.894.408 4.56 2.107 4.56 2.107.458.21.458.596 0 .806L9.197 11.27H7.405zM4.462 14.692l5-12a.5.5 0 1 0-.924-.384l-5 12a.5.5 0 1 0 .924.384z\"></path></g></svg>"
+
+/***/ },
+/* 334 */
+/***/ function(module, exports) {
+
+ module.exports = "<!-- 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/. --><svg viewBox=\"0 0 16 16\" xmlns=\"http://www.w3.org/2000/svg\"><path fill-rule=\"evenodd\" d=\"M8.5 8.793L5.854 6.146l-.04-.035L7.5 4.426c.2-.2.3-.4.3-.6 0-.2-.1-.4-.2-.6l-1-1c-.4-.3-.9-.3-1.2 0l-4.1 4.1c-.2.2-.3.4-.3.6 0 .2.1.4.2.6l1 1c.3.3.9.3 1.2 0l1.71-1.71.036.04L7.793 9.5l-3.647 3.646c-.195.196-.195.512 0 .708.196.195.512.195.708 0L8.5 10.207l3.646 3.647c.196.195.512.195.708 0 .195-.196.195-.512 0-.708L9.207 9.5l2.565-2.565L13.3 8.5c.1.1 2.3 1.1 2.7.7.4-.4-.3-2.7-.5-2.9l-1.1-1.1c.1-.1.2-.4.2-.6 0-.2-.1-.4-.2-.6l-.4-.4c-.3-.3-.8-.3-1.1 0l-1.5-1.4c-.2-.2-.3-.2-.5-.2s-.3.1-.5.2L9.2 3.4c-.2.1-.2.2-.2.4s.1.4.2.5l1.874 1.92L8.5 8.792z\"></path></svg>"
+
+/***/ },
+/* 335 */
+/***/ function(module, exports) {
+
+ module.exports = "<!-- 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/. --><svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" fill=\"#D92215\"><path d=\"M8 14.5c-3.6 0-6.5-2.9-6.5-6.5S4.4 1.5 8 1.5s6.5 2.9 6.5 6.5-2.9 6.5-6.5 6.5zm0-12C5 2.5 2.5 5 2.5 8S5 13.5 8 13.5 13.5 11 13.5 8 11 2.5 8 2.5z\"></path><circle cx=\"5\" cy=\"6\" r=\"1\" transform=\"translate(1 1)\"></circle><circle cx=\"9\" cy=\"6\" r=\"1\" transform=\"translate(1 1)\"></circle><path d=\"M5.5 11c-.1 0-.2 0-.3-.1-.2-.1-.3-.4-.1-.7C6 9 7 8.5 8.1 8.5c1.7.1 2.8 1.7 2.8 1.8.2.2.1.5-.1.7-.2.1-.6 0-.7-.2 0 0-.9-1.3-2-1.3-.7 0-1.4.4-2.1 1.3-.2.2-.4.2-.5.2z\"></path></svg>"
+
+/***/ },
+/* 336 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var React = __webpack_require__(2);
+ var dom = React.DOM;
+ var PropTypes = React.PropTypes;
+
+ var Svg = __webpack_require__(310);
+
+ __webpack_require__(337);
+
+ function CloseButton(_ref) {
+ var handleClick = _ref.handleClick;
+ var buttonClass = _ref.buttonClass;
+
+ return dom.div({
+ className: buttonClass ? "close-btn-" + buttonClass : "close-btn",
+ onClick: handleClick
+ }, Svg("close"));
+ }
+
+ CloseButton.propTypes = {
+ handleClick: PropTypes.func.isRequired
+ };
+
+ module.exports = CloseButton;
+
+/***/ },
+/* 337 */
+/***/ function(module, exports) {
+
+ // removed by extract-text-webpack-plugin
+
+/***/ },
+/* 338 */,
+/* 339 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var React = __webpack_require__(2);
+ var dom = React.DOM;
+ var PropTypes = React.PropTypes;
+
+ var ImPropTypes = __webpack_require__(235);
+
+ var _require = __webpack_require__(3);
+
+ var bindActionCreators = _require.bindActionCreators;
+
+ var _require2 = __webpack_require__(19);
+
+ var connect = _require2.connect;
+
+ var _require3 = __webpack_require__(261);
+
+ var cmdString = _require3.cmdString;
+
+ var SourcesTree = React.createFactory(__webpack_require__(340));
+ var actions = __webpack_require__(262);
+
+ var _require4 = __webpack_require__(259);
+
+ var getSelectedSource = _require4.getSelectedSource;
+ var getSources = _require4.getSources;
+
+
+ __webpack_require__(398);
+
+ var Sources = React.createClass({
+ propTypes: {
+ sources: ImPropTypes.map.isRequired,
+ selectSource: PropTypes.func.isRequired
+ },
+
+ displayName: "Sources",
+
+ render() {
+ var _props = this.props;
+ var sources = _props.sources;
+ var selectSource = _props.selectSource;
+
+
+ return dom.div({ className: "sources-panel" }, dom.div({ className: "sources-header" }, L10N.getStr("sources.header"), dom.span({ className: "sources-header-info" }, L10N.getFormatStr("sources.search", cmdString() + "+P"))), SourcesTree({ sources, selectSource }));
+ }
+ });
+
+ module.exports = connect(state => ({ selectedSource: getSelectedSource(state),
+ sources: getSources(state) }), dispatch => bindActionCreators(actions, dispatch))(Sources);
+
+/***/ },
+/* 340 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var React = __webpack_require__(2);
+ var dom = React.DOM;
+ var PropTypes = React.PropTypes;
+
+ var classnames = __webpack_require__(211);
+ var ImPropTypes = __webpack_require__(235);
+
+ var _require = __webpack_require__(229);
+
+ var Set = _require.Set;
+
+ var _require2 = __webpack_require__(341);
+
+ var nodeHasChildren = _require2.nodeHasChildren;
+ var createParentMap = _require2.createParentMap;
+ var addToTree = _require2.addToTree;
+ var collapseTree = _require2.collapseTree;
+ var createTree = _require2.createTree;
+
+ var ManagedTree = React.createFactory(__webpack_require__(394));
+ var Svg = __webpack_require__(310);
+
+ var _require3 = __webpack_require__(244);
+
+ var throttle = _require3.throttle;
+
+
+ var SourcesTree = React.createClass({
+ propTypes: {
+ sources: ImPropTypes.map.isRequired,
+ selectSource: PropTypes.func.isRequired
+ },
+
+ displayName: "SourcesTree",
+
+ getInitialState() {
+ return createTree(this.props.sources);
+ },
+
+ queueUpdate: throttle(function () {
+ if (!this.isMounted()) {
+ return;
+ }
+
+ this.forceUpdate();
+ }, 50),
+
+ shouldComponentUpdate() {
+ this.queueUpdate();
+ return false;
+ },
+
+ componentWillReceiveProps(nextProps) {
+ if (nextProps.sources === this.props.sources) {
+ return;
+ }
+
+ if (nextProps.sources.size === 0) {
+ this.setState(createTree(nextProps.sources));
+ return;
+ }
+
+ var next = Set(nextProps.sources.valueSeq());
+ var prev = Set(this.props.sources.valueSeq());
+ var newSet = next.subtract(prev);
+
+ var uncollapsedTree = this.state.uncollapsedTree;
+ for (var source of newSet) {
+ addToTree(uncollapsedTree, source);
+ }
+
+ // TODO: recreating the tree every time messes with the expanded
+ // state of ManagedTree, because it depends on item instances
+ // being the same. The result is that if a source is added at a
+ // later time, all expanded state is lost.
+ var sourceTree = newSet.size > 0 ? collapseTree(uncollapsedTree) : this.state.sourceTree;
+
+ this.setState({ uncollapsedTree,
+ sourceTree,
+ parentMap: createParentMap(sourceTree) });
+ },
+
+ focusItem(item) {
+ this.setState({ focusedItem: item });
+ },
+
+ selectItem(item) {
+ if (!nodeHasChildren(item)) {
+ this.props.selectSource(item.contents.get("id"));
+ }
+ },
+
+ getIcon(item, depth) {
+ if (depth === 0) {
+ return Svg("domain");
+ }
+
+ if (!nodeHasChildren(item)) {
+ return Svg("file");
+ }
+
+ return Svg("folder");
+ },
+
+ renderItem(item, depth, focused, _, expanded, _ref) {
+ var setExpanded = _ref.setExpanded;
+
+ var arrow = Svg("arrow", {
+ className: classnames({ expanded: expanded,
+ hidden: !nodeHasChildren(item) }),
+ onClick: e => {
+ e.stopPropagation();
+ setExpanded(item, !expanded);
+ }
+ });
+
+ var icon = this.getIcon(item, depth);
+
+ return dom.div({
+ className: classnames("node", { focused }),
+ style: { paddingLeft: depth * 15 + "px" },
+ key: item.path,
+ onClick: () => this.selectItem(item),
+ onDoubleClick: e => setExpanded(item, !expanded)
+ }, dom.div(null, arrow, icon, item.name));
+ },
+
+ render: function () {
+ var _state = this.state;
+ var focusedItem = _state.focusedItem;
+ var sourceTree = _state.sourceTree;
+ var parentMap = _state.parentMap;
+
+ const isEmpty = sourceTree.contents.length === 0;
+
+ var tree = ManagedTree({
+ key: isEmpty ? "empty" : "full",
+ getParent: item => {
+ return parentMap.get(item);
+ },
+ getChildren: item => {
+ if (nodeHasChildren(item)) {
+ return item.contents;
+ }
+ return [];
+ },
+ getRoots: () => sourceTree.contents,
+ getKey: (item, i) => item.path,
+ itemHeight: 30,
+ autoExpandDepth: 1,
+ onFocus: this.focusItem,
+ renderItem: this.renderItem
+ });
+
+ return dom.div({
+ className: "sources-list",
+ onKeyDown: e => {
+ if (e.keyCode === 13 && focusedItem) {
+ this.selectItem(focusedItem);
+ }
+ }
+ }, tree);
+ }
+ });
+
+ module.exports = SourcesTree;
+
+/***/ },
+/* 341 */
+/***/ function(module, exports, __webpack_require__) {
+
+
+
+ /**
+ * Utils for Sources Tree Component
+ * @module utils/sources-tree
+ */
+
+ var _require = __webpack_require__(293);
+
+ var parse = _require.parse;
+
+ var _require2 = __webpack_require__(246);
+
+ var assert = _require2.assert;
+
+ var _require3 = __webpack_require__(277);
+
+ var isPretty = _require3.isPretty;
+
+ var merge = __webpack_require__(342);
+
+ var IGNORED_URLS = ["debugger eval code", "XStringBundle"];
+
+ /**
+ * Temporary Source type to be used only within this module
+ * TODO: Replace with real Source type definition when refactoring types
+ * @memberof utils/sources-tree
+ * @static
+ */
+
+
+ /**
+ * TODO: createNode is exported so this type could be useful to other modules
+ * @memberof utils/sources-tree
+ * @static
+ */
+
+
+ /**
+ * @memberof utils/sources-tree
+ * @static
+ */
+ function nodeHasChildren(item) {
+ return Array.isArray(item.contents);
+ }
+
+ /**
+ * @memberof utils/sources-tree
+ * @static
+ */
+ function createNode(name, path, contents) {
+ return {
+ name,
+ path,
+ contents: contents || null
+ };
+ }
+
+ /**
+ * @memberof utils/sources-tree
+ * @static
+ */
+ function createParentMap(tree) {
+ var map = new WeakMap();
+
+ function _traverse(subtree) {
+ if (nodeHasChildren(subtree)) {
+ for (var child of subtree.contents) {
+ map.set(child, subtree);
+ _traverse(child);
+ }
+ }
+ }
+
+ // Don't link each top-level path to the "root" node because the
+ // user never sees the root
+ tree.contents.forEach(_traverse);
+ return map;
+ }
+
+ /**
+ * @memberof utils/sources-tree
+ * @static
+ */
+ function getURL(source) {
+ var url = source.get("url");
+ var def = { path: "", group: "" };
+ if (!url) {
+ return def;
+ }
+
+ var _parse = parse(url);
+
+ var pathname = _parse.pathname;
+ var protocol = _parse.protocol;
+ var host = _parse.host;
+ var path = _parse.path;
+
+
+ switch (protocol) {
+ case "javascript:":
+ // Ignore `javascript:` URLs for now
+ return def;
+
+ case "about:":
+ // An about page is a special case
+ return merge(def, {
+ path: "/",
+ group: url
+ });
+
+ case null:
+ if (pathname && pathname.startsWith("/")) {
+ // If it's just a URL like "/foo/bar.js", resolve it to the file
+ // protocol
+ return merge(def, {
+ path: path,
+ group: "file://"
+ });
+ } else if (host === null) {
+ // We don't know what group to put this under, and it's a script
+ // with a weird URL. Just group them all under an anonymous group.
+ return merge(def, {
+ path: url,
+ group: "(no domain)"
+ });
+ }
+ break;
+
+ case "http:":
+ case "https:":
+ return merge(def, {
+ path: pathname,
+ group: host
+ });
+ }
+
+ return merge(def, {
+ path: path,
+ group: protocol ? protocol + "//" : ""
+ });
+ }
+
+ /**
+ * @memberof utils/sources-tree
+ * @static
+ */
+ function addToTree(tree, source) {
+ var url = getURL(source);
+
+ if (IGNORED_URLS.indexOf(url) != -1 || !source.get("url") || isPretty(source.toJS())) {
+ return;
+ }
+
+ url.path = decodeURIComponent(url.path);
+
+ var parts = url.path.split("/").filter(p => p !== "");
+ var isDir = parts.length === 0 || parts[parts.length - 1].indexOf(".") === -1;
+ parts.unshift(url.group);
+
+ var path = "";
+ var subtree = tree;
+
+ for (var i = 0; i < parts.length; i++) {
+ var part = parts[i];
+ var isLastPart = i === parts.length - 1;
+
+ // Currently we assume that we are descending into a node with
+ // children. This will fail if a path has a directory named the
+ // same as another file, like `foo/bar.js/file.js`.
+ //
+ // TODO: Be smarter about this, which we'll probably do when we
+ // are smarter about folders and collapsing empty ones.
+ assert(nodeHasChildren(subtree), `${ subtree.name } should have children`);
+ var children = subtree.contents;
+
+ var index = determineFileSortOrder(children, part, isLastPart);
+
+ if (index >= 0 && children[index].name === part) {
+ // A node with the same name already exists, simply traverse
+ // into it.
+ subtree = children[index];
+ } else {
+ // No node with this name exists, so insert a new one in the
+ // place that is alphabetically sorted.
+ var node = createNode(part, path + "/" + part, []);
+ var where = index === -1 ? children.length : index;
+ children.splice(where, 0, node);
+ subtree = children[where];
+ }
+
+ // Keep track of the children so we can tag each node with them.
+ path = path + "/" + part;
+ }
+
+ // Overwrite the contents of the final node to store the source
+ // there.
+ if (isDir) {
+ subtree.contents.unshift(createNode("(index)", source.get("url"), source));
+ } else {
+ subtree.contents = source;
+ }
+ }
+
+ /**
+ * Look at the nodes in the source tree, and determine the index of where to
+ * insert a new node. The ordering is index -> folder -> file.
+ * @memberof utils/sources-tree
+ * @static
+ */
+ function determineFileSortOrder(nodes, pathPart, isLastPart) {
+ var partIsDir = !isLastPart || pathPart.indexOf(".") === -1;
+
+ return nodes.findIndex(node => {
+ var nodeIsDir = nodeHasChildren(node);
+
+ // The index will always be the first thing, so this pathPart will be
+ // after it.
+ if (node.name === "(index)") {
+ return false;
+ }
+
+ // If both the pathPart and node are the same type, then compare them
+ // alphabetically.
+ if (partIsDir === nodeIsDir) {
+ return node.name.localeCompare(pathPart) >= 0;
+ }
+
+ // If the pathPart and node differ, then stop here if the pathPart is a
+ // directory. Keep on searching if the part is a file, as it needs to be
+ // placed after the directories.
+ return partIsDir;
+ });
+ }
+
+ /**
+ * Take an existing source tree, and return a new one with collapsed nodes.
+ * @memberof utils/sources-tree
+ * @static
+ */
+ function collapseTree(node) {
+ var depth = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
+
+ // Node is a folder.
+ if (nodeHasChildren(node)) {
+ // Node is not a root/domain node, and only contains 1 item.
+ if (depth > 1 && node.contents.length === 1) {
+ var next = node.contents[0];
+ // Do not collapse if the next node is a leaf node.
+ if (nodeHasChildren(next)) {
+ return collapseTree(createNode(`${ node.name }/${ next.name }`, next.path, next.contents), depth + 1);
+ }
+ }
+ // Map the contents.
+ return createNode(node.name, node.path, node.contents.map(next => collapseTree(next, depth + 1)));
+ }
+ // Node is a leaf, not a folder, do not modify it.
+ return node;
+ }
+
+ /**
+ * @memberof utils/sources-tree
+ * @static
+ */
+ function createTree(sources) {
+ var uncollapsedTree = createNode("root", "", []);
+ for (var source of sources.valueSeq()) {
+ addToTree(uncollapsedTree, source);
+ }
+ var sourceTree = collapseTree(uncollapsedTree);
+
+ return { uncollapsedTree,
+ sourceTree,
+ parentMap: createParentMap(sourceTree),
+ focusedItem: null };
+ }
+
+ module.exports = {
+ createNode,
+ nodeHasChildren,
+ createParentMap,
+ addToTree,
+ collapseTree,
+ createTree
+ };
+
+/***/ },
+/* 342 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var baseMerge = __webpack_require__(343),
+ createAssigner = __webpack_require__(384);
+
+ /**
+ * This method is like `_.assign` except that it recursively merges own and
+ * inherited enumerable string keyed properties of source objects into the
+ * destination object. Source properties that resolve to `undefined` are
+ * skipped if a destination value exists. Array and plain object properties
+ * are merged recursively. Other objects and value types are overridden by
+ * assignment. Source objects are applied from left to right. Subsequent
+ * sources overwrite property assignments of previous sources.
+ *
+ * **Note:** This method mutates `object`.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.5.0
+ * @category Object
+ * @param {Object} object The destination object.
+ * @param {...Object} [sources] The source objects.
+ * @returns {Object} Returns `object`.
+ * @example
+ *
+ * var object = {
+ * 'a': [{ 'b': 2 }, { 'd': 4 }]
+ * };
+ *
+ * var other = {
+ * 'a': [{ 'c': 3 }, { 'e': 5 }]
+ * };
+ *
+ * _.merge(object, other);
+ * // => { 'a': [{ 'b': 2, 'c': 3 }, { 'd': 4, 'e': 5 }] }
+ */
+ var merge = createAssigner(function(object, source, srcIndex) {
+ baseMerge(object, source, srcIndex);
+ });
+
+ module.exports = merge;
+
+
+/***/ },
+/* 343 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var Stack = __webpack_require__(344),
+ assignMergeValue = __webpack_require__(350),
+ baseFor = __webpack_require__(353),
+ baseMergeDeep = __webpack_require__(355),
+ isObject = __webpack_require__(106),
+ keysIn = __webpack_require__(378);
+
+ /**
+ * The base implementation of `_.merge` without support for multiple sources.
+ *
+ * @private
+ * @param {Object} object The destination object.
+ * @param {Object} source The source object.
+ * @param {number} srcIndex The index of `source`.
+ * @param {Function} [customizer] The function to customize merged values.
+ * @param {Object} [stack] Tracks traversed source values and their merged
+ * counterparts.
+ */
+ function baseMerge(object, source, srcIndex, customizer, stack) {
+ if (object === source) {
+ return;
+ }
+ baseFor(source, function(srcValue, key) {
+ if (isObject(srcValue)) {
+ stack || (stack = new Stack);
+ baseMergeDeep(object, source, key, srcIndex, baseMerge, customizer, stack);
+ }
+ else {
+ var newValue = customizer
+ ? customizer(object[key], srcValue, (key + ''), object, source, stack)
+ : undefined;
+
+ if (newValue === undefined) {
+ newValue = srcValue;
+ }
+ assignMergeValue(object, key, newValue);
+ }
+ }, keysIn);
+ }
+
+ module.exports = baseMerge;
+
+
+/***/ },
+/* 344 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var ListCache = __webpack_require__(117),
+ stackClear = __webpack_require__(345),
+ stackDelete = __webpack_require__(346),
+ stackGet = __webpack_require__(347),
+ stackHas = __webpack_require__(348),
+ stackSet = __webpack_require__(349);
+
+ /**
+ * Creates a stack cache object to store key-value pairs.
+ *
+ * @private
+ * @constructor
+ * @param {Array} [entries] The key-value pairs to cache.
+ */
+ function Stack(entries) {
+ var data = this.__data__ = new ListCache(entries);
+ this.size = data.size;
+ }
+
+ // Add methods to `Stack`.
+ Stack.prototype.clear = stackClear;
+ Stack.prototype['delete'] = stackDelete;
+ Stack.prototype.get = stackGet;
+ Stack.prototype.has = stackHas;
+ Stack.prototype.set = stackSet;
+
+ module.exports = Stack;
+
+
+/***/ },
+/* 345 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var ListCache = __webpack_require__(117);
+
+ /**
+ * Removes all key-value entries from the stack.
+ *
+ * @private
+ * @name clear
+ * @memberOf Stack
+ */
+ function stackClear() {
+ this.__data__ = new ListCache;
+ this.size = 0;
+ }
+
+ module.exports = stackClear;
+
+
+/***/ },
+/* 346 */
+/***/ function(module, exports) {
+
+ /**
+ * Removes `key` and its value from the stack.
+ *
+ * @private
+ * @name delete
+ * @memberOf Stack
+ * @param {string} key The key of the value to remove.
+ * @returns {boolean} Returns `true` if the entry was removed, else `false`.
+ */
+ function stackDelete(key) {
+ var data = this.__data__,
+ result = data['delete'](key);
+
+ this.size = data.size;
+ return result;
+ }
+
+ module.exports = stackDelete;
+
+
+/***/ },
+/* 347 */
+/***/ function(module, exports) {
+
+ /**
+ * Gets the stack value for `key`.
+ *
+ * @private
+ * @name get
+ * @memberOf Stack
+ * @param {string} key The key of the value to get.
+ * @returns {*} Returns the entry value.
+ */
+ function stackGet(key) {
+ return this.__data__.get(key);
+ }
+
+ module.exports = stackGet;
+
+
+/***/ },
+/* 348 */
+/***/ function(module, exports) {
+
+ /**
+ * Checks if a stack value for `key` exists.
+ *
+ * @private
+ * @name has
+ * @memberOf Stack
+ * @param {string} key The key of the entry to check.
+ * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.
+ */
+ function stackHas(key) {
+ return this.__data__.has(key);
+ }
+
+ module.exports = stackHas;
+
+
+/***/ },
+/* 349 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var ListCache = __webpack_require__(117),
+ Map = __webpack_require__(125),
+ MapCache = __webpack_require__(98);
+
+ /** Used as the size to enable large array optimizations. */
+ var LARGE_ARRAY_SIZE = 200;
+
+ /**
+ * Sets the stack `key` to `value`.
+ *
+ * @private
+ * @name set
+ * @memberOf Stack
+ * @param {string} key The key of the value to set.
+ * @param {*} value The value to set.
+ * @returns {Object} Returns the stack cache instance.
+ */
+ function stackSet(key, value) {
+ var data = this.__data__;
+ if (data instanceof ListCache) {
+ var pairs = data.__data__;
+ if (!Map || (pairs.length < LARGE_ARRAY_SIZE - 1)) {
+ pairs.push([key, value]);
+ this.size = ++data.size;
+ return this;
+ }
+ data = this.__data__ = new MapCache(pairs);
+ }
+ data.set(key, value);
+ this.size = data.size;
+ return this;
+ }
+
+ module.exports = stackSet;
+
+
+/***/ },
+/* 350 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var baseAssignValue = __webpack_require__(351),
+ eq = __webpack_require__(121);
+
+ /**
+ * This function is like `assignValue` except that it doesn't assign
+ * `undefined` values.
+ *
+ * @private
+ * @param {Object} object The object to modify.
+ * @param {string} key The key of the property to assign.
+ * @param {*} value The value to assign.
+ */
+ function assignMergeValue(object, key, value) {
+ if ((value !== undefined && !eq(object[key], value)) ||
+ (value === undefined && !(key in object))) {
+ baseAssignValue(object, key, value);
+ }
+ }
+
+ module.exports = assignMergeValue;
+
+
+/***/ },
+/* 351 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var defineProperty = __webpack_require__(352);
+
+ /**
+ * The base implementation of `assignValue` and `assignMergeValue` without
+ * value checks.
+ *
+ * @private
+ * @param {Object} object The object to modify.
+ * @param {string} key The key of the property to assign.
+ * @param {*} value The value to assign.
+ */
+ function baseAssignValue(object, key, value) {
+ if (key == '__proto__' && defineProperty) {
+ defineProperty(object, key, {
+ 'configurable': true,
+ 'enumerable': true,
+ 'value': value,
+ 'writable': true
+ });
+ } else {
+ object[key] = value;
+ }
+ }
+
+ module.exports = baseAssignValue;
+
+
+/***/ },
+/* 352 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var getNative = __webpack_require__(103);
+
+ var defineProperty = (function() {
+ try {
+ var func = getNative(Object, 'defineProperty');
+ func({}, '', {});
+ return func;
+ } catch (e) {}
+ }());
+
+ module.exports = defineProperty;
+
+
+/***/ },
+/* 353 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var createBaseFor = __webpack_require__(354);
+
+ /**
+ * The base implementation of `baseForOwn` which iterates over `object`
+ * properties returned by `keysFunc` and invokes `iteratee` for each property.
+ * Iteratee functions may exit iteration early by explicitly returning `false`.
+ *
+ * @private
+ * @param {Object} object The object to iterate over.
+ * @param {Function} iteratee The function invoked per iteration.
+ * @param {Function} keysFunc The function to get the keys of `object`.
+ * @returns {Object} Returns `object`.
+ */
+ var baseFor = createBaseFor();
+
+ module.exports = baseFor;
+
+
+/***/ },
+/* 354 */
+/***/ function(module, exports) {
+
+ /**
+ * Creates a base function for methods like `_.forIn` and `_.forOwn`.
+ *
+ * @private
+ * @param {boolean} [fromRight] Specify iterating from right to left.
+ * @returns {Function} Returns the new base function.
+ */
+ function createBaseFor(fromRight) {
+ return function(object, iteratee, keysFunc) {
+ var index = -1,
+ iterable = Object(object),
+ props = keysFunc(object),
+ length = props.length;
+
+ while (length--) {
+ var key = props[fromRight ? length : ++index];
+ if (iteratee(iterable[key], key, iterable) === false) {
+ break;
+ }
+ }
+ return object;
+ };
+ }
+
+ module.exports = createBaseFor;
+
+
+/***/ },
+/* 355 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var assignMergeValue = __webpack_require__(350),
+ cloneBuffer = __webpack_require__(356),
+ cloneTypedArray = __webpack_require__(357),
+ copyArray = __webpack_require__(360),
+ initCloneObject = __webpack_require__(361),
+ isArguments = __webpack_require__(364),
+ isArray = __webpack_require__(94),
+ isArrayLikeObject = __webpack_require__(366),
+ isBuffer = __webpack_require__(369),
+ isFunction = __webpack_require__(105),
+ isObject = __webpack_require__(106),
+ isPlainObject = __webpack_require__(5),
+ isTypedArray = __webpack_require__(371),
+ toPlainObject = __webpack_require__(375);
+
+ /**
+ * A specialized version of `baseMerge` for arrays and objects which performs
+ * deep merges and tracks traversed objects enabling objects with circular
+ * references to be merged.
+ *
+ * @private
+ * @param {Object} object The destination object.
+ * @param {Object} source The source object.
+ * @param {string} key The key of the value to merge.
+ * @param {number} srcIndex The index of `source`.
+ * @param {Function} mergeFunc The function to merge values.
+ * @param {Function} [customizer] The function to customize assigned values.
+ * @param {Object} [stack] Tracks traversed source values and their merged
+ * counterparts.
+ */
+ function baseMergeDeep(object, source, key, srcIndex, mergeFunc, customizer, stack) {
+ var objValue = object[key],
+ srcValue = source[key],
+ stacked = stack.get(srcValue);
+
+ if (stacked) {
+ assignMergeValue(object, key, stacked);
+ return;
+ }
+ var newValue = customizer
+ ? customizer(objValue, srcValue, (key + ''), object, source, stack)
+ : undefined;
+
+ var isCommon = newValue === undefined;
+
+ if (isCommon) {
+ var isArr = isArray(srcValue),
+ isBuff = !isArr && isBuffer(srcValue),
+ isTyped = !isArr && !isBuff && isTypedArray(srcValue);
+
+ newValue = srcValue;
+ if (isArr || isBuff || isTyped) {
+ if (isArray(objValue)) {
+ newValue = objValue;
+ }
+ else if (isArrayLikeObject(objValue)) {
+ newValue = copyArray(objValue);
+ }
+ else if (isBuff) {
+ isCommon = false;
+ newValue = cloneBuffer(srcValue, true);
+ }
+ else if (isTyped) {
+ isCommon = false;
+ newValue = cloneTypedArray(srcValue, true);
+ }
+ else {
+ newValue = [];
+ }
+ }
+ else if (isPlainObject(srcValue) || isArguments(srcValue)) {
+ newValue = objValue;
+ if (isArguments(objValue)) {
+ newValue = toPlainObject(objValue);
+ }
+ else if (!isObject(objValue) || (srcIndex && isFunction(objValue))) {
+ newValue = initCloneObject(srcValue);
+ }
+ }
+ else {
+ isCommon = false;
+ }
+ }
+ if (isCommon) {
+ // Recursively merge objects and arrays (susceptible to call stack limits).
+ stack.set(srcValue, newValue);
+ mergeFunc(newValue, srcValue, srcIndex, customizer, stack);
+ stack['delete'](srcValue);
+ }
+ assignMergeValue(object, key, newValue);
+ }
+
+ module.exports = baseMergeDeep;
+
+
+/***/ },
+/* 356 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /* WEBPACK VAR INJECTION */(function(module) {var root = __webpack_require__(109);
+
+ /** Detect free variable `exports`. */
+ var freeExports = typeof exports == 'object' && exports && !exports.nodeType && exports;
+
+ /** Detect free variable `module`. */
+ var freeModule = freeExports && typeof module == 'object' && module && !module.nodeType && module;
+
+ /** Detect the popular CommonJS extension `module.exports`. */
+ var moduleExports = freeModule && freeModule.exports === freeExports;
+
+ /** Built-in value references. */
+ var Buffer = moduleExports ? root.Buffer : undefined,
+ allocUnsafe = Buffer ? Buffer.allocUnsafe : undefined;
+
+ /**
+ * Creates a clone of `buffer`.
+ *
+ * @private
+ * @param {Buffer} buffer The buffer to clone.
+ * @param {boolean} [isDeep] Specify a deep clone.
+ * @returns {Buffer} Returns the cloned buffer.
+ */
+ function cloneBuffer(buffer, isDeep) {
+ if (isDeep) {
+ return buffer.slice();
+ }
+ var length = buffer.length,
+ result = allocUnsafe ? allocUnsafe(length) : new buffer.constructor(length);
+
+ buffer.copy(result);
+ return result;
+ }
+
+ module.exports = cloneBuffer;
+
+ /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(77)(module)))
+
+/***/ },
+/* 357 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var cloneArrayBuffer = __webpack_require__(358);
+
+ /**
+ * Creates a clone of `typedArray`.
+ *
+ * @private
+ * @param {Object} typedArray The typed array to clone.
+ * @param {boolean} [isDeep] Specify a deep clone.
+ * @returns {Object} Returns the cloned typed array.
+ */
+ function cloneTypedArray(typedArray, isDeep) {
+ var buffer = isDeep ? cloneArrayBuffer(typedArray.buffer) : typedArray.buffer;
+ return new typedArray.constructor(buffer, typedArray.byteOffset, typedArray.length);
+ }
+
+ module.exports = cloneTypedArray;
+
+
+/***/ },
+/* 358 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var Uint8Array = __webpack_require__(359);
+
+ /**
+ * Creates a clone of `arrayBuffer`.
+ *
+ * @private
+ * @param {ArrayBuffer} arrayBuffer The array buffer to clone.
+ * @returns {ArrayBuffer} Returns the cloned array buffer.
+ */
+ function cloneArrayBuffer(arrayBuffer) {
+ var result = new arrayBuffer.constructor(arrayBuffer.byteLength);
+ new Uint8Array(result).set(new Uint8Array(arrayBuffer));
+ return result;
+ }
+
+ module.exports = cloneArrayBuffer;
+
+
+/***/ },
+/* 359 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var root = __webpack_require__(109);
+
+ /** Built-in value references. */
+ var Uint8Array = root.Uint8Array;
+
+ module.exports = Uint8Array;
+
+
+/***/ },
+/* 360 */
+/***/ function(module, exports) {
+
+ /**
+ * Copies the values of `source` to `array`.
+ *
+ * @private
+ * @param {Array} source The array to copy values from.
+ * @param {Array} [array=[]] The array to copy values to.
+ * @returns {Array} Returns `array`.
+ */
+ function copyArray(source, array) {
+ var index = -1,
+ length = source.length;
+
+ array || (array = Array(length));
+ while (++index < length) {
+ array[index] = source[index];
+ }
+ return array;
+ }
+
+ module.exports = copyArray;
+
+
+/***/ },
+/* 361 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var baseCreate = __webpack_require__(362),
+ getPrototype = __webpack_require__(6),
+ isPrototype = __webpack_require__(363);
+
+ /**
+ * Initializes an object clone.
+ *
+ * @private
+ * @param {Object} object The object to clone.
+ * @returns {Object} Returns the initialized clone.
+ */
+ function initCloneObject(object) {
+ return (typeof object.constructor == 'function' && !isPrototype(object))
+ ? baseCreate(getPrototype(object))
+ : {};
+ }
+
+ module.exports = initCloneObject;
+
+
+/***/ },
+/* 362 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var isObject = __webpack_require__(106);
+
+ /** Built-in value references. */
+ var objectCreate = Object.create;
+
+ /**
+ * The base implementation of `_.create` without support for assigning
+ * properties to the created object.
+ *
+ * @private
+ * @param {Object} proto The object to inherit from.
+ * @returns {Object} Returns the new object.
+ */
+ var baseCreate = (function() {
+ function object() {}
+ return function(proto) {
+ if (!isObject(proto)) {
+ return {};
+ }
+ if (objectCreate) {
+ return objectCreate(proto);
+ }
+ object.prototype = proto;
+ var result = new object;
+ object.prototype = undefined;
+ return result;
+ };
+ }());
+
+ module.exports = baseCreate;
+
+
+/***/ },
+/* 363 */
+/***/ function(module, exports) {
+
+ /** Used for built-in method references. */
+ var objectProto = Object.prototype;
+
+ /**
+ * Checks if `value` is likely a prototype object.
+ *
+ * @private
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is a prototype, else `false`.
+ */
+ function isPrototype(value) {
+ var Ctor = value && value.constructor,
+ proto = (typeof Ctor == 'function' && Ctor.prototype) || objectProto;
+
+ return value === proto;
+ }
+
+ module.exports = isPrototype;
+
+
+/***/ },
+/* 364 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var baseIsArguments = __webpack_require__(365),
+ isObjectLike = __webpack_require__(8);
+
+ /** Used for built-in method references. */
+ var objectProto = Object.prototype;
+
+ /** Used to check objects for own properties. */
+ var hasOwnProperty = objectProto.hasOwnProperty;
+
+ /** Built-in value references. */
+ var propertyIsEnumerable = objectProto.propertyIsEnumerable;
+
+ /**
+ * Checks if `value` is likely an `arguments` object.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.1.0
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is an `arguments` object,
+ * else `false`.
+ * @example
+ *
+ * _.isArguments(function() { return arguments; }());
+ * // => true
+ *
+ * _.isArguments([1, 2, 3]);
+ * // => false
+ */
+ var isArguments = baseIsArguments(function() { return arguments; }()) ? baseIsArguments : function(value) {
+ return isObjectLike(value) && hasOwnProperty.call(value, 'callee') &&
+ !propertyIsEnumerable.call(value, 'callee');
+ };
+
+ module.exports = isArguments;
+
+
+/***/ },
+/* 365 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var isObjectLike = __webpack_require__(8);
+
+ /** `Object#toString` result references. */
+ var argsTag = '[object Arguments]';
+
+ /** Used for built-in method references. */
+ var objectProto = Object.prototype;
+
+ /**
+ * Used to resolve the
+ * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring)
+ * of values.
+ */
+ var objectToString = objectProto.toString;
+
+ /**
+ * The base implementation of `_.isArguments`.
+ *
+ * @private
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is an `arguments` object,
+ */
+ function baseIsArguments(value) {
+ return isObjectLike(value) && objectToString.call(value) == argsTag;
+ }
+
+ module.exports = baseIsArguments;
+
+
+/***/ },
+/* 366 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var isArrayLike = __webpack_require__(367),
+ isObjectLike = __webpack_require__(8);
+
+ /**
+ * This method is like `_.isArrayLike` except that it also checks if `value`
+ * is an object.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is an array-like object,
+ * else `false`.
+ * @example
+ *
+ * _.isArrayLikeObject([1, 2, 3]);
+ * // => true
+ *
+ * _.isArrayLikeObject(document.body.children);
+ * // => true
+ *
+ * _.isArrayLikeObject('abc');
+ * // => false
+ *
+ * _.isArrayLikeObject(_.noop);
+ * // => false
+ */
+ function isArrayLikeObject(value) {
+ return isObjectLike(value) && isArrayLike(value);
+ }
+
+ module.exports = isArrayLikeObject;
+
+
+/***/ },
+/* 367 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var isFunction = __webpack_require__(105),
+ isLength = __webpack_require__(368);
+
+ /**
+ * Checks if `value` is array-like. A value is considered array-like if it's
+ * not a function and has a `value.length` that's an integer greater than or
+ * equal to `0` and less than or equal to `Number.MAX_SAFE_INTEGER`.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is array-like, else `false`.
+ * @example
+ *
+ * _.isArrayLike([1, 2, 3]);
+ * // => true
+ *
+ * _.isArrayLike(document.body.children);
+ * // => true
+ *
+ * _.isArrayLike('abc');
+ * // => true
+ *
+ * _.isArrayLike(_.noop);
+ * // => false
+ */
+ function isArrayLike(value) {
+ return value != null && isLength(value.length) && !isFunction(value);
+ }
+
+ module.exports = isArrayLike;
+
+
+/***/ },
+/* 368 */
+/***/ function(module, exports) {
+
+ /** Used as references for various `Number` constants. */
+ var MAX_SAFE_INTEGER = 9007199254740991;
+
+ /**
+ * Checks if `value` is a valid array-like length.
+ *
+ * **Note:** This method is loosely based on
+ * [`ToLength`](http://ecma-international.org/ecma-262/7.0/#sec-tolength).
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is a valid length, else `false`.
+ * @example
+ *
+ * _.isLength(3);
+ * // => true
+ *
+ * _.isLength(Number.MIN_VALUE);
+ * // => false
+ *
+ * _.isLength(Infinity);
+ * // => false
+ *
+ * _.isLength('3');
+ * // => false
+ */
+ function isLength(value) {
+ return typeof value == 'number' &&
+ value > -1 && value % 1 == 0 && value <= MAX_SAFE_INTEGER;
+ }
+
+ module.exports = isLength;
+
+
+/***/ },
+/* 369 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /* WEBPACK VAR INJECTION */(function(module) {var root = __webpack_require__(109),
+ stubFalse = __webpack_require__(370);
+
+ /** Detect free variable `exports`. */
+ var freeExports = typeof exports == 'object' && exports && !exports.nodeType && exports;
+
+ /** Detect free variable `module`. */
+ var freeModule = freeExports && typeof module == 'object' && module && !module.nodeType && module;
+
+ /** Detect the popular CommonJS extension `module.exports`. */
+ var moduleExports = freeModule && freeModule.exports === freeExports;
+
+ /** Built-in value references. */
+ var Buffer = moduleExports ? root.Buffer : undefined;
+
+ /* Built-in method references for those with the same name as other `lodash` methods. */
+ var nativeIsBuffer = Buffer ? Buffer.isBuffer : undefined;
+
+ /**
+ * Checks if `value` is a buffer.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.3.0
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is a buffer, else `false`.
+ * @example
+ *
+ * _.isBuffer(new Buffer(2));
+ * // => true
+ *
+ * _.isBuffer(new Uint8Array(2));
+ * // => false
+ */
+ var isBuffer = nativeIsBuffer || stubFalse;
+
+ module.exports = isBuffer;
+
+ /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(77)(module)))
+
+/***/ },
+/* 370 */
+/***/ function(module, exports) {
+
+ /**
+ * This method returns `false`.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.13.0
+ * @category Util
+ * @returns {boolean} Returns `false`.
+ * @example
+ *
+ * _.times(2, _.stubFalse);
+ * // => [false, false]
+ */
+ function stubFalse() {
+ return false;
+ }
+
+ module.exports = stubFalse;
+
+
+/***/ },
+/* 371 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var baseIsTypedArray = __webpack_require__(372),
+ baseUnary = __webpack_require__(373),
+ nodeUtil = __webpack_require__(374);
+
+ /* Node.js helper references. */
+ var nodeIsTypedArray = nodeUtil && nodeUtil.isTypedArray;
+
+ /**
+ * Checks if `value` is classified as a typed array.
+ *
+ * @static
+ * @memberOf _
+ * @since 3.0.0
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is a typed array, else `false`.
+ * @example
+ *
+ * _.isTypedArray(new Uint8Array);
+ * // => true
+ *
+ * _.isTypedArray([]);
+ * // => false
+ */
+ var isTypedArray = nodeIsTypedArray ? baseUnary(nodeIsTypedArray) : baseIsTypedArray;
+
+ module.exports = isTypedArray;
+
+
+/***/ },
+/* 372 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var isLength = __webpack_require__(368),
+ isObjectLike = __webpack_require__(8);
+
+ /** `Object#toString` result references. */
+ var argsTag = '[object Arguments]',
+ arrayTag = '[object Array]',
+ boolTag = '[object Boolean]',
+ dateTag = '[object Date]',
+ errorTag = '[object Error]',
+ funcTag = '[object Function]',
+ mapTag = '[object Map]',
+ numberTag = '[object Number]',
+ objectTag = '[object Object]',
+ regexpTag = '[object RegExp]',
+ setTag = '[object Set]',
+ stringTag = '[object String]',
+ weakMapTag = '[object WeakMap]';
+
+ var arrayBufferTag = '[object ArrayBuffer]',
+ dataViewTag = '[object DataView]',
+ float32Tag = '[object Float32Array]',
+ float64Tag = '[object Float64Array]',
+ int8Tag = '[object Int8Array]',
+ int16Tag = '[object Int16Array]',
+ int32Tag = '[object Int32Array]',
+ uint8Tag = '[object Uint8Array]',
+ uint8ClampedTag = '[object Uint8ClampedArray]',
+ uint16Tag = '[object Uint16Array]',
+ uint32Tag = '[object Uint32Array]';
+
+ /** Used to identify `toStringTag` values of typed arrays. */
+ var typedArrayTags = {};
+ typedArrayTags[float32Tag] = typedArrayTags[float64Tag] =
+ typedArrayTags[int8Tag] = typedArrayTags[int16Tag] =
+ typedArrayTags[int32Tag] = typedArrayTags[uint8Tag] =
+ typedArrayTags[uint8ClampedTag] = typedArrayTags[uint16Tag] =
+ typedArrayTags[uint32Tag] = true;
+ typedArrayTags[argsTag] = typedArrayTags[arrayTag] =
+ typedArrayTags[arrayBufferTag] = typedArrayTags[boolTag] =
+ typedArrayTags[dataViewTag] = typedArrayTags[dateTag] =
+ typedArrayTags[errorTag] = typedArrayTags[funcTag] =
+ typedArrayTags[mapTag] = typedArrayTags[numberTag] =
+ typedArrayTags[objectTag] = typedArrayTags[regexpTag] =
+ typedArrayTags[setTag] = typedArrayTags[stringTag] =
+ typedArrayTags[weakMapTag] = false;
+
+ /** Used for built-in method references. */
+ var objectProto = Object.prototype;
+
+ /**
+ * Used to resolve the
+ * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring)
+ * of values.
+ */
+ var objectToString = objectProto.toString;
+
+ /**
+ * The base implementation of `_.isTypedArray` without Node.js optimizations.
+ *
+ * @private
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is a typed array, else `false`.
+ */
+ function baseIsTypedArray(value) {
+ return isObjectLike(value) &&
+ isLength(value.length) && !!typedArrayTags[objectToString.call(value)];
+ }
+
+ module.exports = baseIsTypedArray;
+
+
+/***/ },
+/* 373 */
+/***/ function(module, exports) {
+
+ /**
+ * The base implementation of `_.unary` without support for storing metadata.
+ *
+ * @private
+ * @param {Function} func The function to cap arguments for.
+ * @returns {Function} Returns the new capped function.
+ */
+ function baseUnary(func) {
+ return function(value) {
+ return func(value);
+ };
+ }
+
+ module.exports = baseUnary;
+
+
+/***/ },
+/* 374 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /* WEBPACK VAR INJECTION */(function(module) {var freeGlobal = __webpack_require__(110);
+
+ /** Detect free variable `exports`. */
+ var freeExports = typeof exports == 'object' && exports && !exports.nodeType && exports;
+
+ /** Detect free variable `module`. */
+ var freeModule = freeExports && typeof module == 'object' && module && !module.nodeType && module;
+
+ /** Detect the popular CommonJS extension `module.exports`. */
+ var moduleExports = freeModule && freeModule.exports === freeExports;
+
+ /** Detect free variable `process` from Node.js. */
+ var freeProcess = moduleExports && freeGlobal.process;
+
+ /** Used to access faster Node.js helpers. */
+ var nodeUtil = (function() {
+ try {
+ return freeProcess && freeProcess.binding('util');
+ } catch (e) {}
+ }());
+
+ module.exports = nodeUtil;
+
+ /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(77)(module)))
+
+/***/ },
+/* 375 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var copyObject = __webpack_require__(376),
+ keysIn = __webpack_require__(378);
+
+ /**
+ * Converts `value` to a plain object flattening inherited enumerable string
+ * keyed properties of `value` to own properties of the plain object.
+ *
+ * @static
+ * @memberOf _
+ * @since 3.0.0
+ * @category Lang
+ * @param {*} value The value to convert.
+ * @returns {Object} Returns the converted plain object.
+ * @example
+ *
+ * function Foo() {
+ * this.b = 2;
+ * }
+ *
+ * Foo.prototype.c = 3;
+ *
+ * _.assign({ 'a': 1 }, new Foo);
+ * // => { 'a': 1, 'b': 2 }
+ *
+ * _.assign({ 'a': 1 }, _.toPlainObject(new Foo));
+ * // => { 'a': 1, 'b': 2, 'c': 3 }
+ */
+ function toPlainObject(value) {
+ return copyObject(value, keysIn(value));
+ }
+
+ module.exports = toPlainObject;
+
+
+/***/ },
+/* 376 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var assignValue = __webpack_require__(377),
+ baseAssignValue = __webpack_require__(351);
+
+ /**
+ * Copies properties of `source` to `object`.
+ *
+ * @private
+ * @param {Object} source The object to copy properties from.
+ * @param {Array} props The property identifiers to copy.
+ * @param {Object} [object={}] The object to copy properties to.
+ * @param {Function} [customizer] The function to customize copied values.
+ * @returns {Object} Returns `object`.
+ */
+ function copyObject(source, props, object, customizer) {
+ var isNew = !object;
+ object || (object = {});
+
+ var index = -1,
+ length = props.length;
+
+ while (++index < length) {
+ var key = props[index];
+
+ var newValue = customizer
+ ? customizer(object[key], source[key], key, object, source)
+ : undefined;
+
+ if (newValue === undefined) {
+ newValue = source[key];
+ }
+ if (isNew) {
+ baseAssignValue(object, key, newValue);
+ } else {
+ assignValue(object, key, newValue);
+ }
+ }
+ return object;
+ }
+
+ module.exports = copyObject;
+
+
+/***/ },
+/* 377 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var baseAssignValue = __webpack_require__(351),
+ eq = __webpack_require__(121);
+
+ /** Used for built-in method references. */
+ var objectProto = Object.prototype;
+
+ /** Used to check objects for own properties. */
+ var hasOwnProperty = objectProto.hasOwnProperty;
+
+ /**
+ * Assigns `value` to `key` of `object` if the existing value is not equivalent
+ * using [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)
+ * for equality comparisons.
+ *
+ * @private
+ * @param {Object} object The object to modify.
+ * @param {string} key The key of the property to assign.
+ * @param {*} value The value to assign.
+ */
+ function assignValue(object, key, value) {
+ var objValue = object[key];
+ if (!(hasOwnProperty.call(object, key) && eq(objValue, value)) ||
+ (value === undefined && !(key in object))) {
+ baseAssignValue(object, key, value);
+ }
+ }
+
+ module.exports = assignValue;
+
+
+/***/ },
+/* 378 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var arrayLikeKeys = __webpack_require__(379),
+ baseKeysIn = __webpack_require__(382),
+ isArrayLike = __webpack_require__(367);
+
+ /**
+ * Creates an array of the own and inherited enumerable property names of `object`.
+ *
+ * **Note:** Non-object values are coerced to objects.
+ *
+ * @static
+ * @memberOf _
+ * @since 3.0.0
+ * @category Object
+ * @param {Object} object The object to query.
+ * @returns {Array} Returns the array of property names.
+ * @example
+ *
+ * function Foo() {
+ * this.a = 1;
+ * this.b = 2;
+ * }
+ *
+ * Foo.prototype.c = 3;
+ *
+ * _.keysIn(new Foo);
+ * // => ['a', 'b', 'c'] (iteration order is not guaranteed)
+ */
+ function keysIn(object) {
+ return isArrayLike(object) ? arrayLikeKeys(object, true) : baseKeysIn(object);
+ }
+
+ module.exports = keysIn;
+
+
+/***/ },
+/* 379 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var baseTimes = __webpack_require__(380),
+ isArguments = __webpack_require__(364),
+ isArray = __webpack_require__(94),
+ isBuffer = __webpack_require__(369),
+ isIndex = __webpack_require__(381),
+ isTypedArray = __webpack_require__(371);
+
+ /** Used for built-in method references. */
+ var objectProto = Object.prototype;
+
+ /** Used to check objects for own properties. */
+ var hasOwnProperty = objectProto.hasOwnProperty;
+
+ /**
+ * Creates an array of the enumerable property names of the array-like `value`.
+ *
+ * @private
+ * @param {*} value The value to query.
+ * @param {boolean} inherited Specify returning inherited property names.
+ * @returns {Array} Returns the array of property names.
+ */
+ function arrayLikeKeys(value, inherited) {
+ var isArr = isArray(value),
+ isArg = !isArr && isArguments(value),
+ isBuff = !isArr && !isArg && isBuffer(value),
+ isType = !isArr && !isArg && !isBuff && isTypedArray(value),
+ skipIndexes = isArr || isArg || isBuff || isType,
+ result = skipIndexes ? baseTimes(value.length, String) : [],
+ length = result.length;
+
+ for (var key in value) {
+ if ((inherited || hasOwnProperty.call(value, key)) &&
+ !(skipIndexes && (
+ // Safari 9 has enumerable `arguments.length` in strict mode.
+ key == 'length' ||
+ // Node.js 0.10 has enumerable non-index properties on buffers.
+ (isBuff && (key == 'offset' || key == 'parent')) ||
+ // PhantomJS 2 has enumerable non-index properties on typed arrays.
+ (isType && (key == 'buffer' || key == 'byteLength' || key == 'byteOffset')) ||
+ // Skip index properties.
+ isIndex(key, length)
+ ))) {
+ result.push(key);
+ }
+ }
+ return result;
+ }
+
+ module.exports = arrayLikeKeys;
+
+
+/***/ },
+/* 380 */
+/***/ function(module, exports) {
+
+ /**
+ * The base implementation of `_.times` without support for iteratee shorthands
+ * or max array length checks.
+ *
+ * @private
+ * @param {number} n The number of times to invoke `iteratee`.
+ * @param {Function} iteratee The function invoked per iteration.
+ * @returns {Array} Returns the array of results.
+ */
+ function baseTimes(n, iteratee) {
+ var index = -1,
+ result = Array(n);
+
+ while (++index < n) {
+ result[index] = iteratee(index);
+ }
+ return result;
+ }
+
+ module.exports = baseTimes;
+
+
+/***/ },
+/* 381 */
+/***/ function(module, exports) {
+
+ /** Used as references for various `Number` constants. */
+ var MAX_SAFE_INTEGER = 9007199254740991;
+
+ /** Used to detect unsigned integer values. */
+ var reIsUint = /^(?:0|[1-9]\d*)$/;
+
+ /**
+ * Checks if `value` is a valid array-like index.
+ *
+ * @private
+ * @param {*} value The value to check.
+ * @param {number} [length=MAX_SAFE_INTEGER] The upper bounds of a valid index.
+ * @returns {boolean} Returns `true` if `value` is a valid index, else `false`.
+ */
+ function isIndex(value, length) {
+ length = length == null ? MAX_SAFE_INTEGER : length;
+ return !!length &&
+ (typeof value == 'number' || reIsUint.test(value)) &&
+ (value > -1 && value % 1 == 0 && value < length);
+ }
+
+ module.exports = isIndex;
+
+
+/***/ },
+/* 382 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var isObject = __webpack_require__(106),
+ isPrototype = __webpack_require__(363),
+ nativeKeysIn = __webpack_require__(383);
+
+ /** Used for built-in method references. */
+ var objectProto = Object.prototype;
+
+ /** Used to check objects for own properties. */
+ var hasOwnProperty = objectProto.hasOwnProperty;
+
+ /**
+ * The base implementation of `_.keysIn` which doesn't treat sparse arrays as dense.
+ *
+ * @private
+ * @param {Object} object The object to query.
+ * @returns {Array} Returns the array of property names.
+ */
+ function baseKeysIn(object) {
+ if (!isObject(object)) {
+ return nativeKeysIn(object);
+ }
+ var isProto = isPrototype(object),
+ result = [];
+
+ for (var key in object) {
+ if (!(key == 'constructor' && (isProto || !hasOwnProperty.call(object, key)))) {
+ result.push(key);
+ }
+ }
+ return result;
+ }
+
+ module.exports = baseKeysIn;
+
+
+/***/ },
+/* 383 */
+/***/ function(module, exports) {
+
+ /**
+ * This function is like
+ * [`Object.keys`](http://ecma-international.org/ecma-262/7.0/#sec-object.keys)
+ * except that it includes inherited enumerable properties.
+ *
+ * @private
+ * @param {Object} object The object to query.
+ * @returns {Array} Returns the array of property names.
+ */
+ function nativeKeysIn(object) {
+ var result = [];
+ if (object != null) {
+ for (var key in Object(object)) {
+ result.push(key);
+ }
+ }
+ return result;
+ }
+
+ module.exports = nativeKeysIn;
+
+
+/***/ },
+/* 384 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var baseRest = __webpack_require__(385),
+ isIterateeCall = __webpack_require__(393);
+
+ /**
+ * Creates a function like `_.assign`.
+ *
+ * @private
+ * @param {Function} assigner The function to assign values.
+ * @returns {Function} Returns the new assigner function.
+ */
+ function createAssigner(assigner) {
+ return baseRest(function(object, sources) {
+ var index = -1,
+ length = sources.length,
+ customizer = length > 1 ? sources[length - 1] : undefined,
+ guard = length > 2 ? sources[2] : undefined;
+
+ customizer = (assigner.length > 3 && typeof customizer == 'function')
+ ? (length--, customizer)
+ : undefined;
+
+ if (guard && isIterateeCall(sources[0], sources[1], guard)) {
+ customizer = length < 3 ? undefined : customizer;
+ length = 1;
+ }
+ object = Object(object);
+ while (++index < length) {
+ var source = sources[index];
+ if (source) {
+ assigner(object, source, index, customizer);
+ }
+ }
+ return object;
+ });
+ }
+
+ module.exports = createAssigner;
+
+
+/***/ },
+/* 385 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var identity = __webpack_require__(386),
+ overRest = __webpack_require__(387),
+ setToString = __webpack_require__(389);
+
+ /**
+ * The base implementation of `_.rest` which doesn't validate or coerce arguments.
+ *
+ * @private
+ * @param {Function} func The function to apply a rest parameter to.
+ * @param {number} [start=func.length-1] The start position of the rest parameter.
+ * @returns {Function} Returns the new function.
+ */
+ function baseRest(func, start) {
+ return setToString(overRest(func, start, identity), func + '');
+ }
+
+ module.exports = baseRest;
+
+
+/***/ },
+/* 386 */
+/***/ function(module, exports) {
+
+ /**
+ * This method returns the first argument it receives.
+ *
+ * @static
+ * @since 0.1.0
+ * @memberOf _
+ * @category Util
+ * @param {*} value Any value.
+ * @returns {*} Returns `value`.
+ * @example
+ *
+ * var object = { 'a': 1 };
+ *
+ * console.log(_.identity(object) === object);
+ * // => true
+ */
+ function identity(value) {
+ return value;
+ }
+
+ module.exports = identity;
+
+
+/***/ },
+/* 387 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var apply = __webpack_require__(388);
+
+ /* Built-in method references for those with the same name as other `lodash` methods. */
+ var nativeMax = Math.max;
+
+ /**
+ * A specialized version of `baseRest` which transforms the rest array.
+ *
+ * @private
+ * @param {Function} func The function to apply a rest parameter to.
+ * @param {number} [start=func.length-1] The start position of the rest parameter.
+ * @param {Function} transform The rest array transform.
+ * @returns {Function} Returns the new function.
+ */
+ function overRest(func, start, transform) {
+ start = nativeMax(start === undefined ? (func.length - 1) : start, 0);
+ return function() {
+ var args = arguments,
+ index = -1,
+ length = nativeMax(args.length - start, 0),
+ array = Array(length);
+
+ while (++index < length) {
+ array[index] = args[start + index];
+ }
+ index = -1;
+ var otherArgs = Array(start + 1);
+ while (++index < start) {
+ otherArgs[index] = args[index];
+ }
+ otherArgs[start] = transform(array);
+ return apply(func, this, otherArgs);
+ };
+ }
+
+ module.exports = overRest;
+
+
+/***/ },
+/* 388 */
+/***/ function(module, exports) {
+
+ /**
+ * A faster alternative to `Function#apply`, this function invokes `func`
+ * with the `this` binding of `thisArg` and the arguments of `args`.
+ *
+ * @private
+ * @param {Function} func The function to invoke.
+ * @param {*} thisArg The `this` binding of `func`.
+ * @param {Array} args The arguments to invoke `func` with.
+ * @returns {*} Returns the result of `func`.
+ */
+ function apply(func, thisArg, args) {
+ switch (args.length) {
+ case 0: return func.call(thisArg);
+ case 1: return func.call(thisArg, args[0]);
+ case 2: return func.call(thisArg, args[0], args[1]);
+ case 3: return func.call(thisArg, args[0], args[1], args[2]);
+ }
+ return func.apply(thisArg, args);
+ }
+
+ module.exports = apply;
+
+
+/***/ },
+/* 389 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var baseSetToString = __webpack_require__(390),
+ shortOut = __webpack_require__(392);
+
+ /**
+ * Sets the `toString` method of `func` to return `string`.
+ *
+ * @private
+ * @param {Function} func The function to modify.
+ * @param {Function} string The `toString` result.
+ * @returns {Function} Returns `func`.
+ */
+ var setToString = shortOut(baseSetToString);
+
+ module.exports = setToString;
+
+
+/***/ },
+/* 390 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var constant = __webpack_require__(391),
+ defineProperty = __webpack_require__(352),
+ identity = __webpack_require__(386);
+
+ /**
+ * The base implementation of `setToString` without support for hot loop shorting.
+ *
+ * @private
+ * @param {Function} func The function to modify.
+ * @param {Function} string The `toString` result.
+ * @returns {Function} Returns `func`.
+ */
+ var baseSetToString = !defineProperty ? identity : function(func, string) {
+ return defineProperty(func, 'toString', {
+ 'configurable': true,
+ 'enumerable': false,
+ 'value': constant(string),
+ 'writable': true
+ });
+ };
+
+ module.exports = baseSetToString;
+
+
+/***/ },
+/* 391 */
+/***/ function(module, exports) {
+
+ /**
+ * Creates a function that returns `value`.
+ *
+ * @static
+ * @memberOf _
+ * @since 2.4.0
+ * @category Util
+ * @param {*} value The value to return from the new function.
+ * @returns {Function} Returns the new constant function.
+ * @example
+ *
+ * var objects = _.times(2, _.constant({ 'a': 1 }));
+ *
+ * console.log(objects);
+ * // => [{ 'a': 1 }, { 'a': 1 }]
+ *
+ * console.log(objects[0] === objects[1]);
+ * // => true
+ */
+ function constant(value) {
+ return function() {
+ return value;
+ };
+ }
+
+ module.exports = constant;
+
+
+/***/ },
+/* 392 */
+/***/ function(module, exports) {
+
+ /** Used to detect hot functions by number of calls within a span of milliseconds. */
+ var HOT_COUNT = 500,
+ HOT_SPAN = 16;
+
+ /* Built-in method references for those with the same name as other `lodash` methods. */
+ var nativeNow = Date.now;
+
+ /**
+ * Creates a function that'll short out and invoke `identity` instead
+ * of `func` when it's called `HOT_COUNT` or more times in `HOT_SPAN`
+ * milliseconds.
+ *
+ * @private
+ * @param {Function} func The function to restrict.
+ * @returns {Function} Returns the new shortable function.
+ */
+ function shortOut(func) {
+ var count = 0,
+ lastCalled = 0;
+
+ return function() {
+ var stamp = nativeNow(),
+ remaining = HOT_SPAN - (stamp - lastCalled);
+
+ lastCalled = stamp;
+ if (remaining > 0) {
+ if (++count >= HOT_COUNT) {
+ return arguments[0];
+ }
+ } else {
+ count = 0;
+ }
+ return func.apply(undefined, arguments);
+ };
+ }
+
+ module.exports = shortOut;
+
+
+/***/ },
+/* 393 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var eq = __webpack_require__(121),
+ isArrayLike = __webpack_require__(367),
+ isIndex = __webpack_require__(381),
+ isObject = __webpack_require__(106);
+
+ /**
+ * Checks if the given arguments are from an iteratee call.
+ *
+ * @private
+ * @param {*} value The potential iteratee value argument.
+ * @param {*} index The potential iteratee index or key argument.
+ * @param {*} object The potential iteratee object argument.
+ * @returns {boolean} Returns `true` if the arguments are from an iteratee call,
+ * else `false`.
+ */
+ function isIterateeCall(value, index, object) {
+ if (!isObject(object)) {
+ return false;
+ }
+ var type = typeof index;
+ if (type == 'number'
+ ? (isArrayLike(object) && isIndex(index, object.length))
+ : (type == 'string' && index in object)
+ ) {
+ return eq(object[index], value);
+ }
+ return false;
+ }
+
+ module.exports = isIterateeCall;
+
+
+/***/ },
+/* 394 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var React = __webpack_require__(2);
+ var Tree = React.createFactory(__webpack_require__(395));
+ __webpack_require__(396);
+
+ var ManagedTree = React.createClass({
+ propTypes: Tree.propTypes,
+
+ displayName: "ManagedTree",
+
+ getInitialState() {
+ return {
+ expanded: new Set(),
+ focusedItem: null
+ };
+ },
+
+ setExpanded(item, isExpanded) {
+ var expanded = this.state.expanded;
+ var key = this.props.getKey(item);
+ if (isExpanded) {
+ expanded.add(key);
+ } else {
+ expanded.delete(key);
+ }
+ this.setState({ expanded });
+
+ if (isExpanded && this.props.onExpand) {
+ this.props.onExpand(item);
+ } else if (!expanded && this.props.onCollapse) {
+ this.props.onCollapse(item);
+ }
+ },
+
+ focusItem(item) {
+ if (!this.props.disabledFocus && this.state.focusedItem !== item) {
+ this.setState({ focusedItem: item });
+
+ if (this.props.onFocus) {
+ this.props.onFocus(item);
+ }
+ }
+ },
+
+ render() {
+ var _this = this;
+
+ var _state = this.state;
+ var expanded = _state.expanded;
+ var focusedItem = _state.focusedItem;
+
+
+ var props = Object.assign({}, this.props, {
+ isExpanded: item => expanded.has(this.props.getKey(item)),
+ focused: focusedItem,
+
+ onExpand: item => this.setExpanded(item, true),
+ onCollapse: item => this.setExpanded(item, false),
+ onFocus: this.focusItem,
+
+ renderItem: function () {
+ var _props;
+
+ for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
+ args[_key] = arguments[_key];
+ }
+
+ return (_props = _this.props).renderItem.apply(_props, args.concat([{
+ setExpanded: _this.setExpanded
+ }]));
+ }
+ });
+
+ return Tree(props);
+ }
+ });
+
+ module.exports = ManagedTree;
+
+/***/ },
+/* 395 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /* 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/. */
+
+ const { DOM: dom, createClass, createFactory, PropTypes } = __webpack_require__(2);
+ // const { ViewHelpers } =
+ // require("resource://devtools/client/shared/widgets/ViewHelpers.jsm");
+ // let { VirtualScroll } = require("react-virtualized");
+ // VirtualScroll = createFactory(VirtualScroll);
+
+ const AUTO_EXPAND_DEPTH = 0; // depth
+
+ /**
+ * An arrow that displays whether its node is expanded (â–¼) or collapsed
+ * (â–¶). When its node has no children, it is hidden.
+ */
+ const ArrowExpander = createFactory(createClass({
+ displayName: "ArrowExpander",
+
+ shouldComponentUpdate(nextProps, nextState) {
+ return this.props.item !== nextProps.item
+ || this.props.visible !== nextProps.visible
+ || this.props.expanded !== nextProps.expanded;
+ },
+
+ render() {
+ const attrs = {
+ className: "arrow theme-twisty",
+ onClick: this.props.expanded
+ ? () => this.props.onCollapse(this.props.item)
+ : e => this.props.onExpand(this.props.item, e.altKey)
+ };
+
+ if (this.props.expanded) {
+ attrs.className += " open";
+ }
+
+ if (!this.props.visible) {
+ attrs.style = Object.assign({}, this.props.style || {}, {
+ visibility: "hidden"
+ });
+ }
+
+ return dom.div(attrs, this.props.children);
+ }
+ }));
+
+ const TreeNode = createFactory(createClass({
+ displayName: "TreeNode",
+
+ componentDidMount() {
+ if (this.props.focused) {
+ this.refs.button.focus();
+ }
+ },
+
+ componentDidUpdate() {
+ if (this.props.focused) {
+ this.refs.button.focus();
+ }
+ },
+
+ shouldComponentUpdate(nextProps) {
+ return this.props.item !== nextProps.item ||
+ this.props.focused !== nextProps.focused ||
+ this.props.expanded !== nextProps.expanded;
+ },
+
+ render() {
+ const arrow = ArrowExpander({
+ item: this.props.item,
+ expanded: this.props.expanded,
+ visible: this.props.hasChildren,
+ onExpand: this.props.onExpand,
+ onCollapse: this.props.onCollapse,
+ });
+
+ let isOddRow = this.props.index % 2;
+ return dom.div(
+ {
+ className: `tree-node div ${isOddRow ? "tree-node-odd" : ""}`,
+ onFocus: this.props.onFocus,
+ onClick: this.props.onFocus,
+ onBlur: this.props.onBlur,
+ style: {
+ padding: 0,
+ margin: 0
+ }
+ },
+
+ this.props.renderItem(this.props.item,
+ this.props.depth,
+ this.props.focused,
+ arrow,
+ this.props.expanded),
+
+ // XXX: OSX won't focus/blur regular elements even if you set tabindex
+ // unless there is an input/button child.
+ dom.button(this._buttonAttrs)
+ );
+ },
+
+ _buttonAttrs: {
+ ref: "button",
+ style: {
+ opacity: 0,
+ width: "0 !important",
+ height: "0 !important",
+ padding: "0 !important",
+ outline: "none",
+ MozAppearance: "none",
+ // XXX: Despite resetting all of the above properties (and margin), the
+ // button still ends up with ~79px width, so we set a large negative
+ // margin to completely hide it.
+ MozMarginStart: "-1000px !important",
+ }
+ }
+ }));
+
+ /**
+ * Create a function that calls the given function `fn` only once per animation
+ * frame.
+ *
+ * @param {Function} fn
+ * @returns {Function}
+ */
+ function oncePerAnimationFrame(fn) {
+ let animationId = null;
+ let argsToPass = null;
+ return function(...args) {
+ argsToPass = args;
+ if (animationId !== null) {
+ return;
+ }
+
+ animationId = requestAnimationFrame(() => {
+ fn.call(this, ...argsToPass);
+ animationId = null;
+ argsToPass = null;
+ });
+ };
+ }
+
+ const NUMBER_OF_OFFSCREEN_ITEMS = 1;
+
+ /**
+ * A generic tree component. See propTypes for the public API.
+ *
+ * @see `devtools/client/memory/components/test/mochitest/head.js` for usage
+ * @see `devtools/client/memory/components/heap.js` for usage
+ */
+ const Tree = module.exports = createClass({
+ displayName: "Tree",
+
+ propTypes: {
+ // Required props
+
+ // A function to get an item's parent, or null if it is a root.
+ getParent: PropTypes.func.isRequired,
+ // A function to get an item's children.
+ getChildren: PropTypes.func.isRequired,
+ // A function which takes an item and ArrowExpander and returns a
+ // component.
+ renderItem: PropTypes.func.isRequired,
+ // A function which returns the roots of the tree (forest).
+ getRoots: PropTypes.func.isRequired,
+ // A function to get a unique key for the given item.
+ getKey: PropTypes.func.isRequired,
+ // A function to get whether an item is expanded or not. If an item is not
+ // expanded, then it must be collapsed.
+ isExpanded: PropTypes.func.isRequired,
+ // The height of an item in the tree including margin and padding, in
+ // pixels.
+ itemHeight: PropTypes.number.isRequired,
+
+ // Optional props
+
+ // The currently focused item, if any such item exists.
+ focused: PropTypes.any,
+ // Handle when a new item is focused.
+ onFocus: PropTypes.func,
+ // The depth to which we should automatically expand new items.
+ autoExpandDepth: PropTypes.number,
+ // Optional event handlers for when items are expanded or collapsed.
+ onExpand: PropTypes.func,
+ onCollapse: PropTypes.func,
+ },
+
+ getDefaultProps() {
+ return {
+ autoExpandDepth: AUTO_EXPAND_DEPTH,
+ };
+ },
+
+ getInitialState() {
+ return {
+ scroll: 0,
+ height: window.innerHeight,
+ seen: new Set(),
+ };
+ },
+
+ componentDidMount() {
+ window.addEventListener("resize", this._updateHeight);
+ this._autoExpand(this.props);
+ this._updateHeight();
+ },
+
+ componentWillUnmount() {
+ window.removeEventListener("resize", this._updateHeight);
+ },
+
+ componentWillReceiveProps(nextProps) {
+ this._autoExpand(nextProps);
+ this._updateHeight();
+ },
+
+ _autoExpand(props) {
+ if (!props.autoExpandDepth) {
+ return;
+ }
+
+ // Automatically expand the first autoExpandDepth levels for new items. Do
+ // not use the usual DFS infrastructure because we don't want to ignore
+ // collapsed nodes.
+ const autoExpand = (item, currentDepth) => {
+ if (currentDepth >= props.autoExpandDepth ||
+ this.state.seen.has(item)) {
+ return;
+ }
+
+ props.onExpand(item);
+ this.state.seen.add(item);
+
+ const children = props.getChildren(item);
+ const length = children.length;
+ for (let i = 0; i < length; i++) {
+ autoExpand(children[i], currentDepth + 1);
+ }
+ };
+
+ const roots = props.getRoots();
+ const length = roots.length;
+ for (let i = 0; i < length; i++) {
+ autoExpand(roots[i], 0);
+ }
+ },
+
+ render() {
+ const traversal = this._dfsFromRoots();
+
+ // Remove `NUMBER_OF_OFFSCREEN_ITEMS` from `begin` and add `2 *
+ // NUMBER_OF_OFFSCREEN_ITEMS` to `end` so that the top and bottom of the
+ // page are filled with the `NUMBER_OF_OFFSCREEN_ITEMS` previous and next
+ // items respectively, rather than whitespace if the item is not in full
+ // view.
+ // const begin = Math.max(((this.state.scroll / this.props.itemHeight) | 0) - NUMBER_OF_OFFSCREEN_ITEMS, 0);
+ // const end = begin + (2 * NUMBER_OF_OFFSCREEN_ITEMS) + ((this.state.height / this.props.itemHeight) | 0);
+ // const toRender = traversal;
+
+ // const nodes = [
+ // dom.div({
+ // key: "top-spacer",
+ // style: {
+ // padding: 0,
+ // margin: 0,
+ // height: begin * this.props.itemHeight + "px"
+ // }
+ // })
+ // ];
+
+ const renderItem = i => {
+ let { item, depth } = traversal[i];
+ return TreeNode({
+ key: this.props.getKey(item, i),
+ index: i,
+ item: item,
+ depth: depth,
+ renderItem: this.props.renderItem,
+ focused: this.props.focused === item,
+ expanded: this.props.isExpanded(item),
+ hasChildren: !!this.props.getChildren(item).length,
+ onExpand: this._onExpand,
+ onCollapse: this._onCollapse,
+ onFocus: () => this._focus(i, item),
+ });
+ };
+
+ // nodes.push(dom.div({
+ // key: "bottom-spacer",
+ // style: {
+ // padding: 0,
+ // margin: 0,
+ // height: (traversal.length - 1 - end) * this.props.itemHeight + "px"
+ // }
+ // }));
+
+ const style = Object.assign({}, this.props.style || {}, {
+ padding: 0,
+ margin: 0
+ });
+
+ return dom.div(
+ {
+ className: "tree",
+ ref: "tree",
+ onKeyDown: this._onKeyDown,
+ onKeyPress: this._preventArrowKeyScrolling,
+ onKeyUp: this._preventArrowKeyScrolling,
+ // onScroll: this._onScroll,
+ style
+ },
+ // VirtualScroll({
+ // width: this.props.width,
+ // height: this.props.height,
+ // rowsCount: traversal.length,
+ // rowHeight: this.props.itemHeight,
+ // rowRenderer: renderItem
+ // })
+ traversal.map((v, i) => renderItem(i))
+ );
+ },
+
+ _preventArrowKeyScrolling(e) {
+ switch (e.key) {
+ case "ArrowUp":
+ case "ArrowDown":
+ case "ArrowLeft":
+ case "ArrowRight":
+ e.preventDefault();
+ e.stopPropagation();
+ if (e.nativeEvent) {
+ if (e.nativeEvent.preventDefault) {
+ e.nativeEvent.preventDefault();
+ }
+ if (e.nativeEvent.stopPropagation) {
+ e.nativeEvent.stopPropagation();
+ }
+ }
+ }
+ },
+
+ /**
+ * Updates the state's height based on clientHeight.
+ */
+ _updateHeight() {
+ this.setState({
+ height: this.refs.tree.clientHeight
+ });
+ },
+
+ /**
+ * Perform a pre-order depth-first search from item.
+ */
+ _dfs(item, maxDepth = Infinity, traversal = [], _depth = 0) {
+ traversal.push({ item, depth: _depth });
+
+ if (!this.props.isExpanded(item)) {
+ return traversal;
+ }
+
+ const nextDepth = _depth + 1;
+
+ if (nextDepth > maxDepth) {
+ return traversal;
+ }
+
+ const children = this.props.getChildren(item);
+ const length = children.length;
+ for (let i = 0; i < length; i++) {
+ this._dfs(children[i], maxDepth, traversal, nextDepth);
+ }
+
+ return traversal;
+ },
+
+ /**
+ * Perform a pre-order depth-first search over the whole forest.
+ */
+ _dfsFromRoots(maxDepth = Infinity) {
+ const traversal = [];
+
+ const roots = this.props.getRoots();
+ const length = roots.length;
+ for (let i = 0; i < length; i++) {
+ this._dfs(roots[i], maxDepth, traversal);
+ }
+
+ return traversal;
+ },
+
+ /**
+ * Expands current row.
+ *
+ * @param {Object} item
+ * @param {Boolean} expandAllChildren
+ */
+ _onExpand: oncePerAnimationFrame(function(item, expandAllChildren) {
+ if (this.props.onExpand) {
+ this.props.onExpand(item);
+
+ if (expandAllChildren) {
+ const children = this._dfs(item);
+ const length = children.length;
+ for (let i = 0; i < length; i++) {
+ this.props.onExpand(children[i].item);
+ }
+ }
+ }
+ }),
+
+ /**
+ * Collapses current row.
+ *
+ * @param {Object} item
+ */
+ _onCollapse: oncePerAnimationFrame(function(item) {
+ if (this.props.onCollapse) {
+ this.props.onCollapse(item);
+ }
+ }),
+
+ /**
+ * Sets the passed in item to be the focused item.
+ *
+ * @param {Number} index
+ * The index of the item in a full DFS traversal (ignoring collapsed
+ * nodes). Ignored if `item` is undefined.
+ *
+ * @param {Object|undefined} item
+ * The item to be focused, or undefined to focus no item.
+ */
+ _focus(index, item) {
+ if (item !== undefined) {
+ const itemStartPosition = index * this.props.itemHeight;
+ const itemEndPosition = (index + 1) * this.props.itemHeight;
+
+ // Note that if the height of the viewport (this.state.height) is less than
+ // `this.props.itemHeight`, we could accidentally try and scroll both up and
+ // down in a futile attempt to make both the item's start and end positions
+ // visible. Instead, give priority to the start of the item by checking its
+ // position first, and then using an "else if", rather than a separate "if",
+ // for the end position.
+ if (this.state.scroll > itemStartPosition) {
+ this.refs.tree.scrollTop = itemStartPosition;
+ } else if ((this.state.scroll + this.state.height) < itemEndPosition) {
+ this.refs.tree.scrollTop = itemEndPosition - this.state.height;
+ }
+ }
+
+ if (this.props.onFocus) {
+ this.props.onFocus(item);
+ }
+ },
+
+ /**
+ * Sets the state to have no focused item.
+ */
+ _onBlur() {
+ this._focus(0, undefined);
+ },
+
+ /**
+ * Fired on a scroll within the tree's container, updates
+ * the stored position of the view port to handle virtual view rendering.
+ *
+ * @param {Event} e
+ */
+ _onScroll: oncePerAnimationFrame(function(e) {
+ this.setState({
+ scroll: Math.max(this.refs.tree.scrollTop, 0),
+ height: this.refs.tree.clientHeight
+ });
+ }),
+
+ /**
+ * Handles key down events in the tree's container.
+ *
+ * @param {Event} e
+ */
+ _onKeyDown(e) {
+ if (this.props.focused == null) {
+ return;
+ }
+
+ // Allow parent nodes to use navigation arrows with modifiers.
+ if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) {
+ return;
+ }
+
+ this._preventArrowKeyScrolling(e);
+
+ switch (e.key) {
+ case "ArrowUp":
+ this._focusPrevNode();
+ return;
+
+ case "ArrowDown":
+ this._focusNextNode();
+ return;
+
+ case "ArrowLeft":
+ if (this.props.isExpanded(this.props.focused)
+ && this.props.getChildren(this.props.focused).length) {
+ this._onCollapse(this.props.focused);
+ } else {
+ this._focusParentNode();
+ }
+ return;
+
+ case "ArrowRight":
+ if (!this.props.isExpanded(this.props.focused)) {
+ this._onExpand(this.props.focused);
+ } else {
+ this._focusNextNode();
+ }
+ return;
+ }
+ },
+
+ /**
+ * Sets the previous node relative to the currently focused item, to focused.
+ */
+ _focusPrevNode: oncePerAnimationFrame(function() {
+ // Start a depth first search and keep going until we reach the currently
+ // focused node. Focus the previous node in the DFS, if it exists. If it
+ // doesn't exist, we're at the first node already.
+
+ let prev;
+ let prevIndex;
+
+ const traversal = this._dfsFromRoots();
+ const length = traversal.length;
+ for (let i = 0; i < length; i++) {
+ const item = traversal[i].item;
+ if (item === this.props.focused) {
+ break;
+ }
+ prev = item;
+ prevIndex = i;
+ }
+
+ if (prev === undefined) {
+ return;
+ }
+
+ this._focus(prevIndex, prev);
+ }),
+
+ /**
+ * Handles the down arrow key which will focus either the next child
+ * or sibling row.
+ */
+ _focusNextNode: oncePerAnimationFrame(function() {
+ // Start a depth first search and keep going until we reach the currently
+ // focused node. Focus the next node in the DFS, if it exists. If it
+ // doesn't exist, we're at the last node already.
+
+ const traversal = this._dfsFromRoots();
+ const length = traversal.length;
+ let i = 0;
+
+ while (i < length) {
+ if (traversal[i].item === this.props.focused) {
+ break;
+ }
+ i++;
+ }
+
+ if (i + 1 < traversal.length) {
+ this._focus(i + 1, traversal[i + 1].item);
+ }
+ }),
+
+ /**
+ * Handles the left arrow key, going back up to the current rows'
+ * parent row.
+ */
+ _focusParentNode: oncePerAnimationFrame(function() {
+ const parent = this.props.getParent(this.props.focused);
+ if (!parent) {
+ return;
+ }
+
+ const traversal = this._dfsFromRoots();
+ const length = traversal.length;
+ let parentIndex = 0;
+ for (; parentIndex < length; parentIndex++) {
+ if (traversal[parentIndex].item === parent) {
+ break;
+ }
+ }
+
+ this._focus(parentIndex, parent);
+ }),
+ });
+
+
+/***/ },
+/* 396 */
+/***/ function(module, exports) {
+
+ // removed by extract-text-webpack-plugin
+
+/***/ },
+/* 397 */,
+/* 398 */
+/***/ function(module, exports) {
+
+ // removed by extract-text-webpack-plugin
+
+/***/ },
+/* 399 */,
+/* 400 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var React = __webpack_require__(2);
+ var dom = React.DOM;
+ var PropTypes = React.PropTypes;
+ var createFactory = React.createFactory;
+
+
+ var ReactDOM = __webpack_require__(16);
+ var ImPropTypes = __webpack_require__(235);
+
+ var _require = __webpack_require__(3);
+
+ var bindActionCreators = _require.bindActionCreators;
+
+ var _require2 = __webpack_require__(19);
+
+ var connect = _require2.connect;
+
+ var SourceEditor = __webpack_require__(401);
+ var SourceFooter = createFactory(__webpack_require__(402));
+ var EditorSearchBar = createFactory(__webpack_require__(406));
+
+ var _require3 = __webpack_require__(411);
+
+ var renderConditionalPanel = _require3.renderConditionalPanel;
+
+ var _require4 = __webpack_require__(18);
+
+ var debugGlobal = _require4.debugGlobal;
+
+ var _require5 = __webpack_require__(259);
+
+ var getSourceText = _require5.getSourceText;
+ var getBreakpointsForSource = _require5.getBreakpointsForSource;
+ var getSelectedLocation = _require5.getSelectedLocation;
+ var getSelectedFrame = _require5.getSelectedFrame;
+ var getSelectedSource = _require5.getSelectedSource;
+
+ var _require6 = __webpack_require__(255);
+
+ var makeLocationId = _require6.makeLocationId;
+
+ var actions = __webpack_require__(262);
+ var Breakpoint = React.createFactory(__webpack_require__(412));
+
+ var _require7 = __webpack_require__(279);
+
+ var getDocument = _require7.getDocument;
+ var setDocument = _require7.setDocument;
+
+ var _require8 = __webpack_require__(403);
+
+ var shouldShowFooter = _require8.shouldShowFooter;
+
+ var _require9 = __webpack_require__(89);
+
+ var isFirefox = _require9.isFirefox;
+
+ var _require10 = __webpack_require__(413);
+
+ var showMenu = _require10.showMenu;
+
+ var _require11 = __webpack_require__(89);
+
+ var isEnabled = _require11.isEnabled;
+
+
+ __webpack_require__(414);
+
+ function isTextForSource(sourceText) {
+ return !sourceText.get("loading") && !sourceText.get("error");
+ }
+
+ function breakpointAtLine(breakpoints, line) {
+ return breakpoints.find(b => {
+ return b.location.line === line + 1;
+ });
+ }
+
+ function getTextForLine(codeMirror, line) {
+ return codeMirror.getLine(line - 1).trim();
+ }
+
+ /**
+ * Forces the breakpoint gutter to be the same size as the line
+ * numbers gutter. Editor CSS will absolutely position the gutter
+ * beneath the line numbers. This makes it easy to be flexible with
+ * how we overlay breakpoints.
+ */
+ function resizeBreakpointGutter(editor) {
+ var gutters = editor.display.gutters;
+ var lineNumbers = gutters.querySelector(".CodeMirror-linenumbers");
+ var breakpoints = gutters.querySelector(".breakpoints");
+ breakpoints.style.width = lineNumbers.clientWidth + "px";
+ }
+
+ var Editor = React.createClass({
+ propTypes: {
+ breakpoints: ImPropTypes.map.isRequired,
+ selectedLocation: PropTypes.object,
+ selectedSource: ImPropTypes.map,
+ sourceText: PropTypes.object,
+ addBreakpoint: PropTypes.func,
+ removeBreakpoint: PropTypes.func,
+ setBreakpointCondition: PropTypes.func,
+ selectedFrame: PropTypes.object
+ },
+
+ displayName: "Editor",
+
+ onGutterClick(cm, line, gutter, ev) {
+ // ignore right clicks in the gutter
+ if (ev.which === 3) {
+ return;
+ }
+
+ if (this.isCbPanelOpen()) {
+ return this.closeConditionalPanel(line);
+ }
+
+ this.toggleBreakpoint(line);
+ },
+
+ onGutterContextMenu(event) {
+ event.preventDefault();
+ var line = this.editor.codeMirror.lineAtHeight(event.clientY);
+ var bp = breakpointAtLine(this.props.breakpoints, line);
+ this.showGutterMenu(event, line, bp);
+ },
+
+ showConditionalPanel(line) {
+ if (this.isCbPanelOpen()) {
+ return;
+ }
+
+ var _props = this.props;
+ var sourceId = _props.selectedLocation.sourceId;
+ var setBreakpointCondition = _props.setBreakpointCondition;
+ var breakpoints = _props.breakpoints;
+
+
+ var bp = breakpointAtLine(breakpoints, line);
+ var location = { sourceId, line: line + 1 };
+ var condition = bp ? bp.condition : "";
+
+ var setBreakpoint = value => {
+ setBreakpointCondition(location, {
+ condition: value,
+ getTextForLine: l => getTextForLine(this.editor.codeMirror, l)
+ });
+ };
+
+ var panel = renderConditionalPanel({
+ condition,
+ setBreakpoint,
+ closePanel: this.closeConditionalPanel
+ });
+
+ this.cbPanel = this.editor.codeMirror.addLineWidget(line, panel);
+ this.cbPanel.node.querySelector("input").focus();
+ },
+
+ closeConditionalPanel() {
+ this.cbPanel.clear();
+ this.cbPanel = null;
+ },
+
+ isCbPanelOpen() {
+ return !!this.cbPanel;
+ },
+
+ toggleBreakpoint(line) {
+ var bp = breakpointAtLine(this.props.breakpoints, line);
+
+ if (bp && bp.loading) {
+ return;
+ }
+
+ if (bp) {
+ this.props.removeBreakpoint({
+ sourceId: this.props.selectedLocation.sourceId,
+ line: line + 1
+ });
+ } else {
+ this.props.addBreakpoint({ sourceId: this.props.selectedLocation.sourceId,
+ line: line + 1 },
+ // Pass in a function to get line text because the breakpoint
+ // may slide and it needs to compute the value at the new
+ // line.
+ { getTextForLine: l => getTextForLine(this.editor.codeMirror, l) });
+ }
+ },
+
+ clearDebugLine(selectedFrame) {
+ if (selectedFrame) {
+ var line = selectedFrame.location.line;
+ this.editor.codeMirror.removeLineClass(line - 1, "line", "debug-line");
+ }
+ },
+
+ setDebugLine(selectedFrame, selectedLocation) {
+ if (selectedFrame && selectedLocation && selectedFrame.location.sourceId === selectedLocation.sourceId) {
+ var line = selectedFrame.location.line;
+ this.editor.codeMirror.addLineClass(line - 1, "line", "debug-line");
+ }
+ },
+
+ highlightLine() {
+ if (!this.pendingJumpLine) {
+ return;
+ }
+
+ // If the location has changed and a specific line is requested,
+ // move to that line and flash it.
+ var codeMirror = this.editor.codeMirror;
+
+ // Make sure to clean up after ourselves. Not only does this
+ // cancel any existing animation, but it avoids it from
+ // happening ever again (in case CodeMirror re-applies the
+ // class, etc).
+ if (this.lastJumpLine) {
+ codeMirror.removeLineClass(this.lastJumpLine - 1, "line", "highlight-line");
+ }
+
+ var line = this.pendingJumpLine;
+ this.editor.alignLine(line);
+
+ // We only want to do the flashing animation if it's not a debug
+ // line, which has it's own styling.
+ if (!this.props.selectedFrame || this.props.selectedFrame.location.line !== line) {
+ this.editor.codeMirror.addLineClass(line - 1, "line", "highlight-line");
+ }
+
+ this.lastJumpLine = line;
+ this.pendingJumpLine = null;
+ },
+
+ setText(text) {
+ if (!text || !this.editor) {
+ return;
+ }
+
+ this.editor.setText(text);
+ },
+
+ setMode(sourceText) {
+ var contentType = sourceText.get("contentType");
+
+ if (contentType.includes("javascript")) {
+ this.editor.setMode({ name: "javascript" });
+ } else if (contentType === "text/wasm") {
+ this.editor.setMode({ name: "text" });
+ } else if (sourceText.get("text").match(/^\s*</)) {
+ // Use HTML mode for files in which the first non whitespace
+ // character is `<` regardless of extension.
+ this.editor.setMode({ name: "htmlmixed" });
+ } else {
+ this.editor.setMode({ name: "text" });
+ }
+ },
+
+ showGutterMenu(e, line, bp) {
+ var bpLabel = void 0;
+ var cbLabel = void 0;
+ if (!bp) {
+ bpLabel = L10N.getStr("editor.addBreakpoint");
+ cbLabel = L10N.getStr("editor.addConditionalBreakpoint");
+ } else {
+ bpLabel = L10N.getStr("editor.removeBreakpoint");
+ cbLabel = L10N.getStr("editor.editBreakpoint");
+ }
+
+ var toggleBreakpoint = {
+ id: "node-menu-breakpoint",
+ label: bpLabel,
+ accesskey: "B",
+ disabled: false,
+ click: () => {
+ this.toggleBreakpoint(line);
+ if (this.isCbPanelOpen()) {
+ this.closeConditionalPanel();
+ }
+ }
+ };
+
+ var conditionalBreakpoint = {
+ id: "node-menu-conditional-breakpoint",
+ label: cbLabel,
+ accesskey: "C",
+ disabled: false,
+ click: () => this.showConditionalPanel(line)
+ };
+
+ showMenu(e, [toggleBreakpoint, conditionalBreakpoint]);
+ },
+
+ componentDidMount() {
+ this.cbPanel = null;
+
+ this.editor = new SourceEditor({
+ mode: "javascript",
+ readOnly: true,
+ lineNumbers: true,
+ theme: "mozilla",
+ lineWrapping: false,
+ matchBrackets: true,
+ showAnnotationRuler: true,
+ enableCodeFolding: false,
+ gutters: ["breakpoints"],
+ value: " ",
+ extraKeys: {}
+ });
+
+ // disables the default search shortcuts
+ if (isEnabled("editorSearch")) {
+ this.editor._initShortcuts = () => {};
+ }
+
+ this.editor.appendToLocalElement(ReactDOM.findDOMNode(this).querySelector(".editor-mount"));
+
+ this.editor.codeMirror.on("gutterClick", this.onGutterClick);
+
+ if (!isFirefox()) {
+ this.editor.codeMirror.on("gutterContextMenu", (cm, line, eventName, event) => this.onGutterContextMenu(event));
+ } else {
+ this.editor.codeMirror.getWrapperElement().addEventListener("contextmenu", event => this.onGutterContextMenu(event), false);
+ }
+
+ resizeBreakpointGutter(this.editor.codeMirror);
+ debugGlobal("cm", this.editor.codeMirror);
+
+ if (this.props.sourceText) {
+ this.setText(this.props.sourceText.get("text"));
+ }
+ },
+
+ componentWillUnmount() {
+ this.editor.destroy();
+ this.editor = null;
+ },
+
+ componentWillReceiveProps(nextProps) {
+ // This lifecycle method is responsible for updating the editor
+ // text.
+ var sourceText = nextProps.sourceText;
+ var selectedLocation = nextProps.selectedLocation;
+
+ this.clearDebugLine(this.props.selectedFrame);
+
+ if (!sourceText) {
+ this.showMessage("");
+ } else if (!isTextForSource(sourceText)) {
+ this.showMessage(sourceText.get("error") || "Loading...");
+ } else if (this.props.sourceText !== sourceText) {
+ this.showSourceText(sourceText, selectedLocation);
+ }
+
+ this.setDebugLine(nextProps.selectedFrame, selectedLocation);
+ resizeBreakpointGutter(this.editor.codeMirror);
+ },
+
+ showMessage(msg) {
+ this.editor.replaceDocument(this.editor.createDocument());
+ this.setText(msg);
+ this.editor.setMode({ name: "text" });
+ },
+
+ /**
+ * Handle getting the source document or creating a new
+ * document with the correct mode and text.
+ *
+ */
+ showSourceText(sourceText, selectedLocation) {
+ var doc = getDocument(selectedLocation.sourceId);
+ if (doc) {
+ this.editor.replaceDocument(doc);
+ return doc;
+ }
+
+ doc = this.editor.createDocument();
+ setDocument(selectedLocation.sourceId, doc);
+ this.editor.replaceDocument(doc);
+
+ this.setText(sourceText.get("text"));
+ this.setMode(sourceText);
+ },
+
+ componentDidUpdate(prevProps) {
+ // This is in `componentDidUpdate` so helper functions can expect
+ // `this.props` to be the current props. This lifecycle method is
+ // responsible for updating the editor annotations.
+ var selectedLocation = this.props.selectedLocation;
+
+ // If the location is different and a new line is requested,
+ // update the pending jump line. Note that if jumping to a line in
+ // a source where the text hasn't been loaded yet, we will set the
+ // line here but not jump until rendering the actual source.
+
+ if (prevProps.selectedLocation !== selectedLocation) {
+ if (selectedLocation && selectedLocation.line != undefined) {
+ this.pendingJumpLine = selectedLocation.line;
+ } else {
+ this.pendingJumpLine = null;
+ }
+ }
+
+ // Only update and jump around in real source texts. This will
+ // keep the jump state around until the real source text is
+ // loaded.
+ if (this.props.sourceText && isTextForSource(this.props.sourceText)) {
+ this.highlightLine();
+ }
+ },
+
+ renderBreakpoints() {
+ var _props2 = this.props;
+ var breakpoints = _props2.breakpoints;
+ var sourceText = _props2.sourceText;
+
+ var isLoading = sourceText && sourceText.get("loading");
+
+ if (isLoading) {
+ return;
+ }
+
+ return breakpoints.valueSeq().map(bp => {
+ return Breakpoint({
+ key: makeLocationId(bp.location),
+ breakpoint: bp,
+ editor: this.editor && this.editor.codeMirror
+ });
+ });
+ },
+
+ editorHeight() {
+ var selectedSource = this.props.selectedSource;
+
+
+ if (!selectedSource || !shouldShowFooter(selectedSource.toJS())) {
+ return "100%";
+ }
+
+ return "";
+ },
+
+ render() {
+ var sourceText = this.props.sourceText;
+
+
+ return dom.div({ className: "editor-wrapper devtools-monospace" }, EditorSearchBar({
+ editor: this.editor,
+ sourceText
+ }), dom.div({
+ className: "editor-mount",
+ style: { height: this.editorHeight() }
+ }), this.renderBreakpoints(), SourceFooter({ editor: this.editor }));
+ }
+ });
+
+ module.exports = connect(state => {
+ var selectedLocation = getSelectedLocation(state);
+ var sourceId = selectedLocation && selectedLocation.sourceId;
+ var selectedSource = getSelectedSource(state);
+
+ return {
+ selectedLocation,
+ selectedSource,
+ sourceText: getSourceText(state, sourceId),
+ breakpoints: getBreakpointsForSource(state, sourceId),
+ selectedFrame: getSelectedFrame(state)
+ };
+ }, dispatch => bindActionCreators(actions, dispatch))(Editor);
+
+/***/ },
+/* 401 */
+/***/ function(module, exports) {
+
+ module.exports = devtoolsRequire("devtools/client/sourceeditor/editor");
+
+/***/ },
+/* 402 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var React = __webpack_require__(2);
+ var dom = React.DOM;
+ var PropTypes = React.PropTypes;
+
+ var _require = __webpack_require__(19);
+
+ var connect = _require.connect;
+
+ var _require2 = __webpack_require__(3);
+
+ var bindActionCreators = _require2.bindActionCreators;
+
+ var actions = __webpack_require__(262);
+
+ var _require3 = __webpack_require__(259);
+
+ var getSelectedSource = _require3.getSelectedSource;
+ var getSourceText = _require3.getSourceText;
+ var getPrettySource = _require3.getPrettySource;
+
+ var Svg = __webpack_require__(310);
+ var ImPropTypes = __webpack_require__(235);
+ var classnames = __webpack_require__(211);
+
+ var _require4 = __webpack_require__(277);
+
+ var isPretty = _require4.isPretty;
+
+ var _require5 = __webpack_require__(403);
+
+ var shouldShowFooter = _require5.shouldShowFooter;
+ var shouldShowPrettyPrint = _require5.shouldShowPrettyPrint;
+
+
+ __webpack_require__(404);
+
+ function debugBtn(onClick, type) {
+ var className = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : "active";
+ var tooltip = arguments[3];
+
+ className = `${ type } ${ className }`;
+ return dom.span({ onClick, className, key: type }, Svg(type, { title: tooltip }));
+ }
+
+ var SourceFooter = React.createClass({
+ propTypes: {
+ selectedSource: ImPropTypes.map,
+ togglePrettyPrint: PropTypes.func,
+ sourceText: ImPropTypes.map,
+ selectSource: PropTypes.func,
+ prettySource: ImPropTypes.map,
+ editor: PropTypes.object
+ },
+
+ displayName: "SourceFooter",
+
+ onClickPrettyPrint() {
+ this.props.togglePrettyPrint(this.props.selectedSource.get("id"));
+ },
+
+ prettyPrintButton() {
+ var _props = this.props;
+ var selectedSource = _props.selectedSource;
+ var sourceText = _props.sourceText;
+
+ var sourceLoaded = selectedSource && !sourceText.get("loading");
+
+ if (!shouldShowPrettyPrint(selectedSource.toJS())) {
+ return;
+ }
+
+ return debugBtn(this.onClickPrettyPrint, "prettyPrint", classnames({
+ active: sourceLoaded,
+ pretty: isPretty(selectedSource.toJS())
+ }), "Prettify Source");
+ },
+
+ render() {
+ var selectedSource = this.props.selectedSource;
+
+
+ if (!selectedSource || !shouldShowFooter(selectedSource.toJS())) {
+ return null;
+ }
+
+ return dom.div({ className: "source-footer" }, dom.div({ className: "command-bar" }, this.prettyPrintButton()));
+ }
+ });
+
+ module.exports = connect(state => {
+ var selectedSource = getSelectedSource(state);
+ var selectedId = selectedSource && selectedSource.get("id");
+ return {
+ selectedSource,
+ sourceText: getSourceText(state, selectedId),
+ prettySource: getPrettySource(state, selectedId)
+ };
+ }, dispatch => bindActionCreators(actions, dispatch))(SourceFooter);
+
+/***/ },
+/* 403 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var _require = __webpack_require__(277);
+
+ var isPretty = _require.isPretty;
+
+ var _require2 = __webpack_require__(89);
+
+ var isEnabled = _require2.isEnabled;
+
+ var _require3 = __webpack_require__(264);
+
+ var isOriginalId = _require3.isOriginalId;
+
+
+ function shouldShowPrettyPrint(selectedSource) {
+ if (!isEnabled("prettyPrint")) {
+ return false;
+ }
+
+ var _isPretty = isPretty(selectedSource);
+ var isOriginal = isOriginalId(selectedSource.id);
+ var hasSourceMap = selectedSource.sourceMapURL;
+
+ if (_isPretty || isOriginal || hasSourceMap) {
+ return false;
+ }
+
+ return true;
+ }
+
+ function shouldShowFooter(selectedSource) {
+ return shouldShowPrettyPrint(selectedSource);
+ }
+
+ module.exports = {
+ shouldShowPrettyPrint,
+ shouldShowFooter
+ };
+
+/***/ },
+/* 404 */
+/***/ function(module, exports) {
+
+ // removed by extract-text-webpack-plugin
+
+/***/ },
+/* 405 */,
+/* 406 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var React = __webpack_require__(2);
+ var dom = React.DOM;
+ var PropTypes = React.PropTypes;
+
+ var _require = __webpack_require__(16);
+
+ var findDOMNode = _require.findDOMNode;
+
+ var Svg = __webpack_require__(310);
+
+ var _require2 = __webpack_require__(407);
+
+ var find = _require2.find;
+ var findNext = _require2.findNext;
+ var findPrev = _require2.findPrev;
+
+ var classnames = __webpack_require__(211);
+
+ var _require3 = __webpack_require__(408);
+
+ var debounce = _require3.debounce;
+ var escapeRegExp = _require3.escapeRegExp;
+
+ var CloseButton = __webpack_require__(336);
+
+ var _require4 = __webpack_require__(89);
+
+ var isEnabled = _require4.isEnabled;
+
+
+ __webpack_require__(409);
+
+ function countMatches(query, text) {
+ var re = new RegExp(escapeRegExp(query), "g");
+ var match = text.match(re);
+ return match ? match.length : 0;
+ }
+
+ var EditorSearchBar = React.createClass({
+
+ propTypes: {
+ editor: PropTypes.object,
+ sourceText: PropTypes.object
+ },
+
+ displayName: "EditorSearchBar",
+
+ getInitialState() {
+ return {
+ enabled: false,
+ query: "",
+ count: 0,
+ index: 0
+ };
+ },
+
+ contextTypes: {
+ shortcuts: PropTypes.object
+ },
+
+ componentWillUnmount() {
+ var shortcuts = this.context.shortcuts;
+ if (isEnabled("editorSearch")) {
+ shortcuts.off("CmdOrCtrl+F", this.toggleSearch);
+ shortcuts.off("Escape", this.onEscape);
+ }
+ },
+
+ componentDidMount() {
+ var shortcuts = this.context.shortcuts;
+ if (isEnabled("editorSearch")) {
+ shortcuts.on("CmdOrCtrl+F", this.toggleSearch);
+ shortcuts.on("Escape", this.onEscape);
+ }
+ },
+
+ componentWillReceiveProps() {
+ var shortcuts = this.context.shortcuts;
+ shortcuts.on("CmdOrCtrl+Shift+G", (_, e) => this.traverseResultsPrev(e));
+ shortcuts.on("CmdOrCtrl+G", (_, e) => this.traverseResultsNext(e));
+ },
+
+ componentDidUpdate() {
+ if (this.searchInput()) {
+ this.searchInput().focus();
+ }
+ },
+
+ onEscape(shortcut, e) {
+ this.closeSearch(e);
+ },
+
+ closeSearch(e) {
+ if (this.state.enabled) {
+ this.setState({ enabled: false });
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ },
+
+ toggleSearch(shortcut, e) {
+ e.stopPropagation();
+ e.preventDefault();
+
+ this.setState({ enabled: !this.state.enabled });
+
+ if (this.state.enabled) {
+ var node = this.searchInput();
+ if (node) {
+ node.setSelectionRange(0, node.value.length);
+ }
+ }
+ },
+
+ searchInput() {
+ return findDOMNode(this).querySelector("input");
+ },
+
+ onChange(e) {
+ var query = e.target.value;
+
+ var count = countMatches(query, this.props.sourceText.get("text"));
+ this.setState({ query, count, index: 0 });
+
+ this.search(query);
+ },
+
+ traverseResultsPrev(e) {
+ e.stopPropagation();
+ e.preventDefault();
+
+ var ed = this.props.editor;
+ var ctx = { ed, cm: ed.codeMirror };
+ var _state = this.state;
+ var query = _state.query;
+ var index = _state.index;
+ var count = _state.count;
+
+
+ findPrev(ctx, query);
+ var nextIndex = index == 0 ? count - 1 : index - 1;
+ this.setState({ index: nextIndex });
+ },
+
+ traverseResultsNext(e) {
+ e.stopPropagation();
+ e.preventDefault();
+
+ var ed = this.props.editor;
+ var ctx = { ed, cm: ed.codeMirror };
+ var _state2 = this.state;
+ var query = _state2.query;
+ var index = _state2.index;
+ var count = _state2.count;
+
+
+ findNext(ctx, query);
+ var nextIndex = index == count - 1 ? 0 : index + 1;
+ this.setState({ index: nextIndex });
+ },
+
+ onKeyUp(e) {
+ if (e.key != "Enter") {
+ return;
+ }
+
+ if (e.shiftKey) {
+ this.traverseResultsPrev(e);
+ } else {
+ this.traverseResultsNext(e);
+ }
+ },
+
+ search: debounce(function (query) {
+ var ed = this.props.editor;
+ var ctx = { ed, cm: ed.codeMirror };
+
+ find(ctx, query);
+ }, 100),
+
+ renderSummary() {
+ var _state3 = this.state;
+ var count = _state3.count;
+ var index = _state3.index;
+ var query = _state3.query;
+
+
+ if (query.trim() == "") {
+ return dom.div({});
+ } else if (count == 0) {
+ return dom.div({ className: "summary" }, L10N.getStr("editor.noResults"));
+ }
+
+ return dom.div({ className: "summary" }, L10N.getFormatStr("editor.searchResults", index + 1, count));
+ },
+
+ renderSvg() {
+ var _state4 = this.state;
+ var count = _state4.count;
+ var query = _state4.query;
+
+
+ if (count == 0 && query.trim() != "") {
+ return Svg("sad-face");
+ }
+
+ return Svg("magnifying-glass");
+ },
+
+ render() {
+ if (!this.state.enabled) {
+ return dom.div();
+ }
+
+ var _state5 = this.state;
+ var count = _state5.count;
+ var query = _state5.query;
+
+
+ return dom.div({ className: "search-bar" }, this.renderSvg(), dom.input({
+ className: classnames({
+ empty: count == 0 && query.trim() != ""
+ }),
+ onChange: this.onChange,
+ onKeyUp: this.onKeyUp,
+ placeholder: "Search in file...",
+ value: this.state.query,
+ spellCheck: false
+ }), this.renderSummary(), CloseButton({
+ handleClick: this.closeSearch,
+ buttonClass: "big"
+ }));
+ }
+ });
+
+ module.exports = EditorSearchBar;
+
+/***/ },
+/* 407 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var _require = __webpack_require__(408);
+
+ var escapeRegExp = _require.escapeRegExp;
+ /**
+ * These functions implement search within the debugger. Since
+ * search in the debugger is different from other components,
+ * we can't use search.js CodeMirror addon. This is a slightly
+ * modified version of that addon. Depends on searchcursor.js.
+ * @module utils/source-search
+ */
+
+ /**
+ * @memberof utils/source-search
+ * @static
+ */
+
+ function SearchState() {
+ this.posFrom = this.posTo = this.query = null;
+ this.overlay = null;
+ }
+
+ /**
+ * @memberof utils/source-search
+ * @static
+ */
+ function getSearchState(cm) {
+ return cm.state.search || (cm.state.search = new SearchState());
+ }
+
+ /**
+ * @memberof utils/source-search
+ * @static
+ */
+ function getSearchCursor(cm, query, pos) {
+ // If the query string is all lowercase, do a case insensitive search.
+ return cm.getSearchCursor(query, pos, typeof query == "string" && query == query.toLowerCase());
+ }
+
+ /**
+ * Ignore doing outline matches for less than 3 whitespaces
+ *
+ * @memberof utils/source-search
+ * @static
+ */
+ function ignoreWhiteSpace(str) {
+ return (/^\s{0,2}$/.test(str) ? "(?!\s*.*)" : str
+ );
+ }
+
+ /**
+ * This returns a mode object used by CoeMirror's addOverlay function
+ * to parse and style tokens in the file.
+ * The mode object contains a tokenizer function (token) which takes
+ * a character stream as input, advances it past a token, and returns
+ * a style for that token. For more details see
+ * https://codemirror.net/doc/manual.html#modeapi
+ *
+ * @memberof utils/source-search
+ * @static
+ */
+ function searchOverlay(query) {
+ query = new RegExp(escapeRegExp(ignoreWhiteSpace(query)), "g");
+ return {
+ token: function (stream) {
+ query.lastIndex = stream.pos;
+ var match = query.exec(stream.string);
+ if (match && match.index == stream.pos) {
+ stream.pos += match[0].length || 1;
+ return "selecting";
+ } else if (match) {
+ stream.pos = match.index;
+ } else {
+ stream.skipToEnd();
+ }
+ }
+ };
+ }
+
+ /**
+ * @memberof utils/source-search
+ * @static
+ */
+ function startSearch(cm, state, query) {
+ cm.removeOverlay(state.overlay);
+ state.overlay = searchOverlay(query);
+ cm.addOverlay(state.overlay, { opaque: true });
+ }
+
+ /**
+ * If there's a saved search, selects the next results.
+ * Otherwise, creates a new search and selects the first
+ * result.
+ *
+ * @memberof utils/source-search
+ * @static
+ */
+ function doSearch(ctx, rev, query) {
+ var cm = ctx.cm;
+
+ var state = getSearchState(cm);
+
+ if (state.query) {
+ searchNext(ctx, rev);
+ return;
+ }
+
+ cm.operation(function () {
+ if (state.query) {
+ return;
+ }
+ startSearch(cm, state, query);
+ state.query = query;
+ state.posFrom = state.posTo = { line: 0, ch: 0 };
+ searchNext(ctx, rev);
+ });
+ }
+
+ /**
+ * Selects the next result of a saved search.
+ *
+ * @memberof utils/source-search
+ * @static
+ */
+ function searchNext(ctx, rev) {
+ var cm = ctx.cm;
+ var ed = ctx.ed;
+
+ cm.operation(function () {
+ var state = getSearchState(cm);
+ var cursor = getSearchCursor(cm, state.query, rev ? state.posFrom : state.posTo);
+
+ if (!cursor.find(rev)) {
+ cursor = getSearchCursor(cm, state.query, rev ? { line: cm.lastLine(), ch: null } : { line: cm.firstLine(), ch: 0 });
+ if (!cursor.find(rev)) {
+ return;
+ }
+ }
+
+ ed.alignLine(cursor.from().line, "center");
+ cm.setSelection(cursor.from(), cursor.to());
+ state.posFrom = cursor.from();
+ state.posTo = cursor.to();
+ });
+ }
+
+ /**
+ * Clears the currently saved search.
+ *
+ * @memberof utils/source-search
+ * @static
+ */
+ function clearSearch(cm) {
+ var state = getSearchState(cm);
+
+ if (!state.query) {
+ return;
+ }
+ cm.removeOverlay(state.overlay);
+ state.query = null;
+ }
+
+ /**
+ * Starts a new search.
+ *
+ * @memberof utils/source-search
+ * @static
+ */
+ function find(ctx, query) {
+ clearSearch(ctx.cm);
+ doSearch(ctx, false, query);
+ }
+
+ /**
+ * Finds the next item based on the currently saved search.
+ *
+ * @memberof utils/source-search
+ * @static
+ */
+ function findNext(ctx, query) {
+ doSearch(ctx, false, query);
+ }
+
+ /**
+ * Finds the previous item based on the currently saved search.
+ *
+ * @memberof utils/source-search
+ * @static
+ */
+ function findPrev(ctx, query) {
+ doSearch(ctx, true, query);
+ }
+
+ module.exports = { find, findNext, findPrev };
+
+/***/ },
+/* 408 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_RESULT__;/* WEBPACK VAR INJECTION */(function(global, module) {/**
+ * @license
+ * lodash <https://lodash.com/>
+ * Copyright jQuery Foundation and other contributors <https://jquery.org/>
+ * Released under MIT license <https://lodash.com/license>
+ * Based on Underscore.js 1.8.3 <http://underscorejs.org/LICENSE>
+ * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
+ */
+ ;(function() {
+
+ /** Used as a safe reference for `undefined` in pre-ES5 environments. */
+ var undefined;
+
+ /** Used as the semantic version number. */
+ var VERSION = '4.16.4';
+
+ /** Used as the size to enable large array optimizations. */
+ var LARGE_ARRAY_SIZE = 200;
+
+ /** Error message constants. */
+ var CORE_ERROR_TEXT = 'Unsupported core-js use. Try https://github.com/es-shims.',
+ FUNC_ERROR_TEXT = 'Expected a function';
+
+ /** Used to stand-in for `undefined` hash values. */
+ var HASH_UNDEFINED = '__lodash_hash_undefined__';
+
+ /** Used as the maximum memoize cache size. */
+ var MAX_MEMOIZE_SIZE = 500;
+
+ /** Used as the internal argument placeholder. */
+ var PLACEHOLDER = '__lodash_placeholder__';
+
+ /** Used to compose bitmasks for function metadata. */
+ var BIND_FLAG = 1,
+ BIND_KEY_FLAG = 2,
+ CURRY_BOUND_FLAG = 4,
+ CURRY_FLAG = 8,
+ CURRY_RIGHT_FLAG = 16,
+ PARTIAL_FLAG = 32,
+ PARTIAL_RIGHT_FLAG = 64,
+ ARY_FLAG = 128,
+ REARG_FLAG = 256,
+ FLIP_FLAG = 512;
+
+ /** Used to compose bitmasks for comparison styles. */
+ var UNORDERED_COMPARE_FLAG = 1,
+ PARTIAL_COMPARE_FLAG = 2;
+
+ /** Used as default options for `_.truncate`. */
+ var DEFAULT_TRUNC_LENGTH = 30,
+ DEFAULT_TRUNC_OMISSION = '...';
+
+ /** Used to detect hot functions by number of calls within a span of milliseconds. */
+ var HOT_COUNT = 500,
+ HOT_SPAN = 16;
+
+ /** Used to indicate the type of lazy iteratees. */
+ var LAZY_FILTER_FLAG = 1,
+ LAZY_MAP_FLAG = 2,
+ LAZY_WHILE_FLAG = 3;
+
+ /** Used as references for various `Number` constants. */
+ var INFINITY = 1 / 0,
+ MAX_SAFE_INTEGER = 9007199254740991,
+ MAX_INTEGER = 1.7976931348623157e+308,
+ NAN = 0 / 0;
+
+ /** Used as references for the maximum length and index of an array. */
+ var MAX_ARRAY_LENGTH = 4294967295,
+ MAX_ARRAY_INDEX = MAX_ARRAY_LENGTH - 1,
+ HALF_MAX_ARRAY_LENGTH = MAX_ARRAY_LENGTH >>> 1;
+
+ /** Used to associate wrap methods with their bit flags. */
+ var wrapFlags = [
+ ['ary', ARY_FLAG],
+ ['bind', BIND_FLAG],
+ ['bindKey', BIND_KEY_FLAG],
+ ['curry', CURRY_FLAG],
+ ['curryRight', CURRY_RIGHT_FLAG],
+ ['flip', FLIP_FLAG],
+ ['partial', PARTIAL_FLAG],
+ ['partialRight', PARTIAL_RIGHT_FLAG],
+ ['rearg', REARG_FLAG]
+ ];
+
+ /** `Object#toString` result references. */
+ var argsTag = '[object Arguments]',
+ arrayTag = '[object Array]',
+ boolTag = '[object Boolean]',
+ dateTag = '[object Date]',
+ errorTag = '[object Error]',
+ funcTag = '[object Function]',
+ genTag = '[object GeneratorFunction]',
+ mapTag = '[object Map]',
+ numberTag = '[object Number]',
+ objectTag = '[object Object]',
+ promiseTag = '[object Promise]',
+ proxyTag = '[object Proxy]',
+ regexpTag = '[object RegExp]',
+ setTag = '[object Set]',
+ stringTag = '[object String]',
+ symbolTag = '[object Symbol]',
+ weakMapTag = '[object WeakMap]',
+ weakSetTag = '[object WeakSet]';
+
+ var arrayBufferTag = '[object ArrayBuffer]',
+ dataViewTag = '[object DataView]',
+ float32Tag = '[object Float32Array]',
+ float64Tag = '[object Float64Array]',
+ int8Tag = '[object Int8Array]',
+ int16Tag = '[object Int16Array]',
+ int32Tag = '[object Int32Array]',
+ uint8Tag = '[object Uint8Array]',
+ uint8ClampedTag = '[object Uint8ClampedArray]',
+ uint16Tag = '[object Uint16Array]',
+ uint32Tag = '[object Uint32Array]';
+
+ /** Used to match empty string literals in compiled template source. */
+ var reEmptyStringLeading = /\b__p \+= '';/g,
+ reEmptyStringMiddle = /\b(__p \+=) '' \+/g,
+ reEmptyStringTrailing = /(__e\(.*?\)|\b__t\)) \+\n'';/g;
+
+ /** Used to match HTML entities and HTML characters. */
+ var reEscapedHtml = /&(?:amp|lt|gt|quot|#39);/g,
+ reUnescapedHtml = /[&<>"']/g,
+ reHasEscapedHtml = RegExp(reEscapedHtml.source),
+ reHasUnescapedHtml = RegExp(reUnescapedHtml.source);
+
+ /** Used to match template delimiters. */
+ var reEscape = /<%-([\s\S]+?)%>/g,
+ reEvaluate = /<%([\s\S]+?)%>/g,
+ reInterpolate = /<%=([\s\S]+?)%>/g;
+
+ /** Used to match property names within property paths. */
+ var reIsDeepProp = /\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,
+ reIsPlainProp = /^\w*$/,
+ reLeadingDot = /^\./,
+ rePropName = /[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g;
+
+ /**
+ * Used to match `RegExp`
+ * [syntax characters](http://ecma-international.org/ecma-262/7.0/#sec-patterns).
+ */
+ var reRegExpChar = /[\\^$.*+?()[\]{}|]/g,
+ reHasRegExpChar = RegExp(reRegExpChar.source);
+
+ /** Used to match leading and trailing whitespace. */
+ var reTrim = /^\s+|\s+$/g,
+ reTrimStart = /^\s+/,
+ reTrimEnd = /\s+$/;
+
+ /** Used to match wrap detail comments. */
+ var reWrapComment = /\{(?:\n\/\* \[wrapped with .+\] \*\/)?\n?/,
+ reWrapDetails = /\{\n\/\* \[wrapped with (.+)\] \*/,
+ reSplitDetails = /,? & /;
+
+ /** Used to match words composed of alphanumeric characters. */
+ var reAsciiWord = /[^\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f]+/g;
+
+ /** Used to match backslashes in property paths. */
+ var reEscapeChar = /\\(\\)?/g;
+
+ /**
+ * Used to match
+ * [ES template delimiters](http://ecma-international.org/ecma-262/7.0/#sec-template-literal-lexical-components).
+ */
+ var reEsTemplate = /\$\{([^\\}]*(?:\\.[^\\}]*)*)\}/g;
+
+ /** Used to match `RegExp` flags from their coerced string values. */
+ var reFlags = /\w*$/;
+
+ /** Used to detect bad signed hexadecimal string values. */
+ var reIsBadHex = /^[-+]0x[0-9a-f]+$/i;
+
+ /** Used to detect binary string values. */
+ var reIsBinary = /^0b[01]+$/i;
+
+ /** Used to detect host constructors (Safari). */
+ var reIsHostCtor = /^\[object .+?Constructor\]$/;
+
+ /** Used to detect octal string values. */
+ var reIsOctal = /^0o[0-7]+$/i;
+
+ /** Used to detect unsigned integer values. */
+ var reIsUint = /^(?:0|[1-9]\d*)$/;
+
+ /** Used to match Latin Unicode letters (excluding mathematical operators). */
+ var reLatin = /[\xc0-\xd6\xd8-\xf6\xf8-\xff\u0100-\u017f]/g;
+
+ /** Used to ensure capturing order of template delimiters. */
+ var reNoMatch = /($^)/;
+
+ /** Used to match unescaped characters in compiled string literals. */
+ var reUnescapedString = /['\n\r\u2028\u2029\\]/g;
+
+ /** Used to compose unicode character classes. */
+ var rsAstralRange = '\\ud800-\\udfff',
+ rsComboMarksRange = '\\u0300-\\u036f\\ufe20-\\ufe23',
+ rsComboSymbolsRange = '\\u20d0-\\u20f0',
+ rsDingbatRange = '\\u2700-\\u27bf',
+ rsLowerRange = 'a-z\\xdf-\\xf6\\xf8-\\xff',
+ rsMathOpRange = '\\xac\\xb1\\xd7\\xf7',
+ rsNonCharRange = '\\x00-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\xbf',
+ rsPunctuationRange = '\\u2000-\\u206f',
+ rsSpaceRange = ' \\t\\x0b\\f\\xa0\\ufeff\\n\\r\\u2028\\u2029\\u1680\\u180e\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u202f\\u205f\\u3000',
+ rsUpperRange = 'A-Z\\xc0-\\xd6\\xd8-\\xde',
+ rsVarRange = '\\ufe0e\\ufe0f',
+ rsBreakRange = rsMathOpRange + rsNonCharRange + rsPunctuationRange + rsSpaceRange;
+
+ /** Used to compose unicode capture groups. */
+ var rsApos = "['\u2019]",
+ rsAstral = '[' + rsAstralRange + ']',
+ rsBreak = '[' + rsBreakRange + ']',
+ rsCombo = '[' + rsComboMarksRange + rsComboSymbolsRange + ']',
+ rsDigits = '\\d+',
+ rsDingbat = '[' + rsDingbatRange + ']',
+ rsLower = '[' + rsLowerRange + ']',
+ rsMisc = '[^' + rsAstralRange + rsBreakRange + rsDigits + rsDingbatRange + rsLowerRange + rsUpperRange + ']',
+ rsFitz = '\\ud83c[\\udffb-\\udfff]',
+ rsModifier = '(?:' + rsCombo + '|' + rsFitz + ')',
+ rsNonAstral = '[^' + rsAstralRange + ']',
+ rsRegional = '(?:\\ud83c[\\udde6-\\uddff]){2}',
+ rsSurrPair = '[\\ud800-\\udbff][\\udc00-\\udfff]',
+ rsUpper = '[' + rsUpperRange + ']',
+ rsZWJ = '\\u200d';
+
+ /** Used to compose unicode regexes. */
+ var rsLowerMisc = '(?:' + rsLower + '|' + rsMisc + ')',
+ rsUpperMisc = '(?:' + rsUpper + '|' + rsMisc + ')',
+ rsOptLowerContr = '(?:' + rsApos + '(?:d|ll|m|re|s|t|ve))?',
+ rsOptUpperContr = '(?:' + rsApos + '(?:D|LL|M|RE|S|T|VE))?',
+ reOptMod = rsModifier + '?',
+ rsOptVar = '[' + rsVarRange + ']?',
+ rsOptJoin = '(?:' + rsZWJ + '(?:' + [rsNonAstral, rsRegional, rsSurrPair].join('|') + ')' + rsOptVar + reOptMod + ')*',
+ rsSeq = rsOptVar + reOptMod + rsOptJoin,
+ rsEmoji = '(?:' + [rsDingbat, rsRegional, rsSurrPair].join('|') + ')' + rsSeq,
+ rsSymbol = '(?:' + [rsNonAstral + rsCombo + '?', rsCombo, rsRegional, rsSurrPair, rsAstral].join('|') + ')';
+
+ /** Used to match apostrophes. */
+ var reApos = RegExp(rsApos, 'g');
+
+ /**
+ * Used to match [combining diacritical marks](https://en.wikipedia.org/wiki/Combining_Diacritical_Marks) and
+ * [combining diacritical marks for symbols](https://en.wikipedia.org/wiki/Combining_Diacritical_Marks_for_Symbols).
+ */
+ var reComboMark = RegExp(rsCombo, 'g');
+
+ /** Used to match [string symbols](https://mathiasbynens.be/notes/javascript-unicode). */
+ var reUnicode = RegExp(rsFitz + '(?=' + rsFitz + ')|' + rsSymbol + rsSeq, 'g');
+
+ /** Used to match complex or compound words. */
+ var reUnicodeWord = RegExp([
+ rsUpper + '?' + rsLower + '+' + rsOptLowerContr + '(?=' + [rsBreak, rsUpper, '$'].join('|') + ')',
+ rsUpperMisc + '+' + rsOptUpperContr + '(?=' + [rsBreak, rsUpper + rsLowerMisc, '$'].join('|') + ')',
+ rsUpper + '?' + rsLowerMisc + '+' + rsOptLowerContr,
+ rsUpper + '+' + rsOptUpperContr,
+ rsDigits,
+ rsEmoji
+ ].join('|'), 'g');
+
+ /** Used to detect strings with [zero-width joiners or code points from the astral planes](http://eev.ee/blog/2015/09/12/dark-corners-of-unicode/). */
+ var reHasUnicode = RegExp('[' + rsZWJ + rsAstralRange + rsComboMarksRange + rsComboSymbolsRange + rsVarRange + ']');
+
+ /** Used to detect strings that need a more robust regexp to match words. */
+ var reHasUnicodeWord = /[a-z][A-Z]|[A-Z]{2,}[a-z]|[0-9][a-zA-Z]|[a-zA-Z][0-9]|[^a-zA-Z0-9 ]/;
+
+ /** Used to assign default `context` object properties. */
+ var contextProps = [
+ 'Array', 'Buffer', 'DataView', 'Date', 'Error', 'Float32Array', 'Float64Array',
+ 'Function', 'Int8Array', 'Int16Array', 'Int32Array', 'Map', 'Math', 'Object',
+ 'Promise', 'RegExp', 'Set', 'String', 'Symbol', 'TypeError', 'Uint8Array',
+ 'Uint8ClampedArray', 'Uint16Array', 'Uint32Array', 'WeakMap',
+ '_', 'clearTimeout', 'isFinite', 'parseInt', 'setTimeout'
+ ];
+
+ /** Used to make template sourceURLs easier to identify. */
+ var templateCounter = -1;
+
+ /** Used to identify `toStringTag` values of typed arrays. */
+ var typedArrayTags = {};
+ typedArrayTags[float32Tag] = typedArrayTags[float64Tag] =
+ typedArrayTags[int8Tag] = typedArrayTags[int16Tag] =
+ typedArrayTags[int32Tag] = typedArrayTags[uint8Tag] =
+ typedArrayTags[uint8ClampedTag] = typedArrayTags[uint16Tag] =
+ typedArrayTags[uint32Tag] = true;
+ typedArrayTags[argsTag] = typedArrayTags[arrayTag] =
+ typedArrayTags[arrayBufferTag] = typedArrayTags[boolTag] =
+ typedArrayTags[dataViewTag] = typedArrayTags[dateTag] =
+ typedArrayTags[errorTag] = typedArrayTags[funcTag] =
+ typedArrayTags[mapTag] = typedArrayTags[numberTag] =
+ typedArrayTags[objectTag] = typedArrayTags[regexpTag] =
+ typedArrayTags[setTag] = typedArrayTags[stringTag] =
+ typedArrayTags[weakMapTag] = false;
+
+ /** Used to identify `toStringTag` values supported by `_.clone`. */
+ var cloneableTags = {};
+ cloneableTags[argsTag] = cloneableTags[arrayTag] =
+ cloneableTags[arrayBufferTag] = cloneableTags[dataViewTag] =
+ cloneableTags[boolTag] = cloneableTags[dateTag] =
+ cloneableTags[float32Tag] = cloneableTags[float64Tag] =
+ cloneableTags[int8Tag] = cloneableTags[int16Tag] =
+ cloneableTags[int32Tag] = cloneableTags[mapTag] =
+ cloneableTags[numberTag] = cloneableTags[objectTag] =
+ cloneableTags[regexpTag] = cloneableTags[setTag] =
+ cloneableTags[stringTag] = cloneableTags[symbolTag] =
+ cloneableTags[uint8Tag] = cloneableTags[uint8ClampedTag] =
+ cloneableTags[uint16Tag] = cloneableTags[uint32Tag] = true;
+ cloneableTags[errorTag] = cloneableTags[funcTag] =
+ cloneableTags[weakMapTag] = false;
+
+ /** Used to map Latin Unicode letters to basic Latin letters. */
+ var deburredLetters = {
+ // Latin-1 Supplement block.
+ '\xc0': 'A', '\xc1': 'A', '\xc2': 'A', '\xc3': 'A', '\xc4': 'A', '\xc5': 'A',
+ '\xe0': 'a', '\xe1': 'a', '\xe2': 'a', '\xe3': 'a', '\xe4': 'a', '\xe5': 'a',
+ '\xc7': 'C', '\xe7': 'c',
+ '\xd0': 'D', '\xf0': 'd',
+ '\xc8': 'E', '\xc9': 'E', '\xca': 'E', '\xcb': 'E',
+ '\xe8': 'e', '\xe9': 'e', '\xea': 'e', '\xeb': 'e',
+ '\xcc': 'I', '\xcd': 'I', '\xce': 'I', '\xcf': 'I',
+ '\xec': 'i', '\xed': 'i', '\xee': 'i', '\xef': 'i',
+ '\xd1': 'N', '\xf1': 'n',
+ '\xd2': 'O', '\xd3': 'O', '\xd4': 'O', '\xd5': 'O', '\xd6': 'O', '\xd8': 'O',
+ '\xf2': 'o', '\xf3': 'o', '\xf4': 'o', '\xf5': 'o', '\xf6': 'o', '\xf8': 'o',
+ '\xd9': 'U', '\xda': 'U', '\xdb': 'U', '\xdc': 'U',
+ '\xf9': 'u', '\xfa': 'u', '\xfb': 'u', '\xfc': 'u',
+ '\xdd': 'Y', '\xfd': 'y', '\xff': 'y',
+ '\xc6': 'Ae', '\xe6': 'ae',
+ '\xde': 'Th', '\xfe': 'th',
+ '\xdf': 'ss',
+ // Latin Extended-A block.
+ '\u0100': 'A', '\u0102': 'A', '\u0104': 'A',
+ '\u0101': 'a', '\u0103': 'a', '\u0105': 'a',
+ '\u0106': 'C', '\u0108': 'C', '\u010a': 'C', '\u010c': 'C',
+ '\u0107': 'c', '\u0109': 'c', '\u010b': 'c', '\u010d': 'c',
+ '\u010e': 'D', '\u0110': 'D', '\u010f': 'd', '\u0111': 'd',
+ '\u0112': 'E', '\u0114': 'E', '\u0116': 'E', '\u0118': 'E', '\u011a': 'E',
+ '\u0113': 'e', '\u0115': 'e', '\u0117': 'e', '\u0119': 'e', '\u011b': 'e',
+ '\u011c': 'G', '\u011e': 'G', '\u0120': 'G', '\u0122': 'G',
+ '\u011d': 'g', '\u011f': 'g', '\u0121': 'g', '\u0123': 'g',
+ '\u0124': 'H', '\u0126': 'H', '\u0125': 'h', '\u0127': 'h',
+ '\u0128': 'I', '\u012a': 'I', '\u012c': 'I', '\u012e': 'I', '\u0130': 'I',
+ '\u0129': 'i', '\u012b': 'i', '\u012d': 'i', '\u012f': 'i', '\u0131': 'i',
+ '\u0134': 'J', '\u0135': 'j',
+ '\u0136': 'K', '\u0137': 'k', '\u0138': 'k',
+ '\u0139': 'L', '\u013b': 'L', '\u013d': 'L', '\u013f': 'L', '\u0141': 'L',
+ '\u013a': 'l', '\u013c': 'l', '\u013e': 'l', '\u0140': 'l', '\u0142': 'l',
+ '\u0143': 'N', '\u0145': 'N', '\u0147': 'N', '\u014a': 'N',
+ '\u0144': 'n', '\u0146': 'n', '\u0148': 'n', '\u014b': 'n',
+ '\u014c': 'O', '\u014e': 'O', '\u0150': 'O',
+ '\u014d': 'o', '\u014f': 'o', '\u0151': 'o',
+ '\u0154': 'R', '\u0156': 'R', '\u0158': 'R',
+ '\u0155': 'r', '\u0157': 'r', '\u0159': 'r',
+ '\u015a': 'S', '\u015c': 'S', '\u015e': 'S', '\u0160': 'S',
+ '\u015b': 's', '\u015d': 's', '\u015f': 's', '\u0161': 's',
+ '\u0162': 'T', '\u0164': 'T', '\u0166': 'T',
+ '\u0163': 't', '\u0165': 't', '\u0167': 't',
+ '\u0168': 'U', '\u016a': 'U', '\u016c': 'U', '\u016e': 'U', '\u0170': 'U', '\u0172': 'U',
+ '\u0169': 'u', '\u016b': 'u', '\u016d': 'u', '\u016f': 'u', '\u0171': 'u', '\u0173': 'u',
+ '\u0174': 'W', '\u0175': 'w',
+ '\u0176': 'Y', '\u0177': 'y', '\u0178': 'Y',
+ '\u0179': 'Z', '\u017b': 'Z', '\u017d': 'Z',
+ '\u017a': 'z', '\u017c': 'z', '\u017e': 'z',
+ '\u0132': 'IJ', '\u0133': 'ij',
+ '\u0152': 'Oe', '\u0153': 'oe',
+ '\u0149': "'n", '\u017f': 's'
+ };
+
+ /** Used to map characters to HTML entities. */
+ var htmlEscapes = {
+ '&': '&amp;',
+ '<': '&lt;',
+ '>': '&gt;',
+ '"': '&quot;',
+ "'": '&#39;'
+ };
+
+ /** Used to map HTML entities to characters. */
+ var htmlUnescapes = {
+ '&amp;': '&',
+ '&lt;': '<',
+ '&gt;': '>',
+ '&quot;': '"',
+ '&#39;': "'"
+ };
+
+ /** Used to escape characters for inclusion in compiled string literals. */
+ var stringEscapes = {
+ '\\': '\\',
+ "'": "'",
+ '\n': 'n',
+ '\r': 'r',
+ '\u2028': 'u2028',
+ '\u2029': 'u2029'
+ };
+
+ /** Built-in method references without a dependency on `root`. */
+ var freeParseFloat = parseFloat,
+ freeParseInt = parseInt;
+
+ /** Detect free variable `global` from Node.js. */
+ var freeGlobal = typeof global == 'object' && global && global.Object === Object && global;
+
+ /** Detect free variable `self`. */
+ var freeSelf = typeof self == 'object' && self && self.Object === Object && self;
+
+ /** Used as a reference to the global object. */
+ var root = freeGlobal || freeSelf || Function('return this')();
+
+ /** Detect free variable `exports`. */
+ var freeExports = typeof exports == 'object' && exports && !exports.nodeType && exports;
+
+ /** Detect free variable `module`. */
+ var freeModule = freeExports && typeof module == 'object' && module && !module.nodeType && module;
+
+ /** Detect the popular CommonJS extension `module.exports`. */
+ var moduleExports = freeModule && freeModule.exports === freeExports;
+
+ /** Detect free variable `process` from Node.js. */
+ var freeProcess = moduleExports && freeGlobal.process;
+
+ /** Used to access faster Node.js helpers. */
+ var nodeUtil = (function() {
+ try {
+ return freeProcess && freeProcess.binding('util');
+ } catch (e) {}
+ }());
+
+ /* Node.js helper references. */
+ var nodeIsArrayBuffer = nodeUtil && nodeUtil.isArrayBuffer,
+ nodeIsDate = nodeUtil && nodeUtil.isDate,
+ nodeIsMap = nodeUtil && nodeUtil.isMap,
+ nodeIsRegExp = nodeUtil && nodeUtil.isRegExp,
+ nodeIsSet = nodeUtil && nodeUtil.isSet,
+ nodeIsTypedArray = nodeUtil && nodeUtil.isTypedArray;
+
+ /*--------------------------------------------------------------------------*/
+
+ /**
+ * Adds the key-value `pair` to `map`.
+ *
+ * @private
+ * @param {Object} map The map to modify.
+ * @param {Array} pair The key-value pair to add.
+ * @returns {Object} Returns `map`.
+ */
+ function addMapEntry(map, pair) {
+ // Don't return `map.set` because it's not chainable in IE 11.
+ map.set(pair[0], pair[1]);
+ return map;
+ }
+
+ /**
+ * Adds `value` to `set`.
+ *
+ * @private
+ * @param {Object} set The set to modify.
+ * @param {*} value The value to add.
+ * @returns {Object} Returns `set`.
+ */
+ function addSetEntry(set, value) {
+ // Don't return `set.add` because it's not chainable in IE 11.
+ set.add(value);
+ return set;
+ }
+
+ /**
+ * A faster alternative to `Function#apply`, this function invokes `func`
+ * with the `this` binding of `thisArg` and the arguments of `args`.
+ *
+ * @private
+ * @param {Function} func The function to invoke.
+ * @param {*} thisArg The `this` binding of `func`.
+ * @param {Array} args The arguments to invoke `func` with.
+ * @returns {*} Returns the result of `func`.
+ */
+ function apply(func, thisArg, args) {
+ switch (args.length) {
+ case 0: return func.call(thisArg);
+ case 1: return func.call(thisArg, args[0]);
+ case 2: return func.call(thisArg, args[0], args[1]);
+ case 3: return func.call(thisArg, args[0], args[1], args[2]);
+ }
+ return func.apply(thisArg, args);
+ }
+
+ /**
+ * A specialized version of `baseAggregator` for arrays.
+ *
+ * @private
+ * @param {Array} [array] The array to iterate over.
+ * @param {Function} setter The function to set `accumulator` values.
+ * @param {Function} iteratee The iteratee to transform keys.
+ * @param {Object} accumulator The initial aggregated object.
+ * @returns {Function} Returns `accumulator`.
+ */
+ function arrayAggregator(array, setter, iteratee, accumulator) {
+ var index = -1,
+ length = array ? array.length : 0;
+
+ while (++index < length) {
+ var value = array[index];
+ setter(accumulator, value, iteratee(value), array);
+ }
+ return accumulator;
+ }
+
+ /**
+ * A specialized version of `_.forEach` for arrays without support for
+ * iteratee shorthands.
+ *
+ * @private
+ * @param {Array} [array] The array to iterate over.
+ * @param {Function} iteratee The function invoked per iteration.
+ * @returns {Array} Returns `array`.
+ */
+ function arrayEach(array, iteratee) {
+ var index = -1,
+ length = array ? array.length : 0;
+
+ while (++index < length) {
+ if (iteratee(array[index], index, array) === false) {
+ break;
+ }
+ }
+ return array;
+ }
+
+ /**
+ * A specialized version of `_.forEachRight` for arrays without support for
+ * iteratee shorthands.
+ *
+ * @private
+ * @param {Array} [array] The array to iterate over.
+ * @param {Function} iteratee The function invoked per iteration.
+ * @returns {Array} Returns `array`.
+ */
+ function arrayEachRight(array, iteratee) {
+ var length = array ? array.length : 0;
+
+ while (length--) {
+ if (iteratee(array[length], length, array) === false) {
+ break;
+ }
+ }
+ return array;
+ }
+
+ /**
+ * A specialized version of `_.every` for arrays without support for
+ * iteratee shorthands.
+ *
+ * @private
+ * @param {Array} [array] The array to iterate over.
+ * @param {Function} predicate The function invoked per iteration.
+ * @returns {boolean} Returns `true` if all elements pass the predicate check,
+ * else `false`.
+ */
+ function arrayEvery(array, predicate) {
+ var index = -1,
+ length = array ? array.length : 0;
+
+ while (++index < length) {
+ if (!predicate(array[index], index, array)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * A specialized version of `_.filter` for arrays without support for
+ * iteratee shorthands.
+ *
+ * @private
+ * @param {Array} [array] The array to iterate over.
+ * @param {Function} predicate The function invoked per iteration.
+ * @returns {Array} Returns the new filtered array.
+ */
+ function arrayFilter(array, predicate) {
+ var index = -1,
+ length = array ? array.length : 0,
+ resIndex = 0,
+ result = [];
+
+ while (++index < length) {
+ var value = array[index];
+ if (predicate(value, index, array)) {
+ result[resIndex++] = value;
+ }
+ }
+ return result;
+ }
+
+ /**
+ * A specialized version of `_.includes` for arrays without support for
+ * specifying an index to search from.
+ *
+ * @private
+ * @param {Array} [array] The array to inspect.
+ * @param {*} target The value to search for.
+ * @returns {boolean} Returns `true` if `target` is found, else `false`.
+ */
+ function arrayIncludes(array, value) {
+ var length = array ? array.length : 0;
+ return !!length && baseIndexOf(array, value, 0) > -1;
+ }
+
+ /**
+ * This function is like `arrayIncludes` except that it accepts a comparator.
+ *
+ * @private
+ * @param {Array} [array] The array to inspect.
+ * @param {*} target The value to search for.
+ * @param {Function} comparator The comparator invoked per element.
+ * @returns {boolean} Returns `true` if `target` is found, else `false`.
+ */
+ function arrayIncludesWith(array, value, comparator) {
+ var index = -1,
+ length = array ? array.length : 0;
+
+ while (++index < length) {
+ if (comparator(value, array[index])) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * A specialized version of `_.map` for arrays without support for iteratee
+ * shorthands.
+ *
+ * @private
+ * @param {Array} [array] The array to iterate over.
+ * @param {Function} iteratee The function invoked per iteration.
+ * @returns {Array} Returns the new mapped array.
+ */
+ function arrayMap(array, iteratee) {
+ var index = -1,
+ length = array ? array.length : 0,
+ result = Array(length);
+
+ while (++index < length) {
+ result[index] = iteratee(array[index], index, array);
+ }
+ return result;
+ }
+
+ /**
+ * Appends the elements of `values` to `array`.
+ *
+ * @private
+ * @param {Array} array The array to modify.
+ * @param {Array} values The values to append.
+ * @returns {Array} Returns `array`.
+ */
+ function arrayPush(array, values) {
+ var index = -1,
+ length = values.length,
+ offset = array.length;
+
+ while (++index < length) {
+ array[offset + index] = values[index];
+ }
+ return array;
+ }
+
+ /**
+ * A specialized version of `_.reduce` for arrays without support for
+ * iteratee shorthands.
+ *
+ * @private
+ * @param {Array} [array] The array to iterate over.
+ * @param {Function} iteratee The function invoked per iteration.
+ * @param {*} [accumulator] The initial value.
+ * @param {boolean} [initAccum] Specify using the first element of `array` as
+ * the initial value.
+ * @returns {*} Returns the accumulated value.
+ */
+ function arrayReduce(array, iteratee, accumulator, initAccum) {
+ var index = -1,
+ length = array ? array.length : 0;
+
+ if (initAccum && length) {
+ accumulator = array[++index];
+ }
+ while (++index < length) {
+ accumulator = iteratee(accumulator, array[index], index, array);
+ }
+ return accumulator;
+ }
+
+ /**
+ * A specialized version of `_.reduceRight` for arrays without support for
+ * iteratee shorthands.
+ *
+ * @private
+ * @param {Array} [array] The array to iterate over.
+ * @param {Function} iteratee The function invoked per iteration.
+ * @param {*} [accumulator] The initial value.
+ * @param {boolean} [initAccum] Specify using the last element of `array` as
+ * the initial value.
+ * @returns {*} Returns the accumulated value.
+ */
+ function arrayReduceRight(array, iteratee, accumulator, initAccum) {
+ var length = array ? array.length : 0;
+ if (initAccum && length) {
+ accumulator = array[--length];
+ }
+ while (length--) {
+ accumulator = iteratee(accumulator, array[length], length, array);
+ }
+ return accumulator;
+ }
+
+ /**
+ * A specialized version of `_.some` for arrays without support for iteratee
+ * shorthands.
+ *
+ * @private
+ * @param {Array} [array] The array to iterate over.
+ * @param {Function} predicate The function invoked per iteration.
+ * @returns {boolean} Returns `true` if any element passes the predicate check,
+ * else `false`.
+ */
+ function arraySome(array, predicate) {
+ var index = -1,
+ length = array ? array.length : 0;
+
+ while (++index < length) {
+ if (predicate(array[index], index, array)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Gets the size of an ASCII `string`.
+ *
+ * @private
+ * @param {string} string The string inspect.
+ * @returns {number} Returns the string size.
+ */
+ var asciiSize = baseProperty('length');
+
+ /**
+ * Converts an ASCII `string` to an array.
+ *
+ * @private
+ * @param {string} string The string to convert.
+ * @returns {Array} Returns the converted array.
+ */
+ function asciiToArray(string) {
+ return string.split('');
+ }
+
+ /**
+ * Splits an ASCII `string` into an array of its words.
+ *
+ * @private
+ * @param {string} The string to inspect.
+ * @returns {Array} Returns the words of `string`.
+ */
+ function asciiWords(string) {
+ return string.match(reAsciiWord) || [];
+ }
+
+ /**
+ * The base implementation of methods like `_.findKey` and `_.findLastKey`,
+ * without support for iteratee shorthands, which iterates over `collection`
+ * using `eachFunc`.
+ *
+ * @private
+ * @param {Array|Object} collection The collection to inspect.
+ * @param {Function} predicate The function invoked per iteration.
+ * @param {Function} eachFunc The function to iterate over `collection`.
+ * @returns {*} Returns the found element or its key, else `undefined`.
+ */
+ function baseFindKey(collection, predicate, eachFunc) {
+ var result;
+ eachFunc(collection, function(value, key, collection) {
+ if (predicate(value, key, collection)) {
+ result = key;
+ return false;
+ }
+ });
+ return result;
+ }
+
+ /**
+ * The base implementation of `_.findIndex` and `_.findLastIndex` without
+ * support for iteratee shorthands.
+ *
+ * @private
+ * @param {Array} array The array to inspect.
+ * @param {Function} predicate The function invoked per iteration.
+ * @param {number} fromIndex The index to search from.
+ * @param {boolean} [fromRight] Specify iterating from right to left.
+ * @returns {number} Returns the index of the matched value, else `-1`.
+ */
+ function baseFindIndex(array, predicate, fromIndex, fromRight) {
+ var length = array.length,
+ index = fromIndex + (fromRight ? 1 : -1);
+
+ while ((fromRight ? index-- : ++index < length)) {
+ if (predicate(array[index], index, array)) {
+ return index;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * The base implementation of `_.indexOf` without `fromIndex` bounds checks.
+ *
+ * @private
+ * @param {Array} array The array to inspect.
+ * @param {*} value The value to search for.
+ * @param {number} fromIndex The index to search from.
+ * @returns {number} Returns the index of the matched value, else `-1`.
+ */
+ function baseIndexOf(array, value, fromIndex) {
+ return value === value
+ ? strictIndexOf(array, value, fromIndex)
+ : baseFindIndex(array, baseIsNaN, fromIndex);
+ }
+
+ /**
+ * This function is like `baseIndexOf` except that it accepts a comparator.
+ *
+ * @private
+ * @param {Array} array The array to inspect.
+ * @param {*} value The value to search for.
+ * @param {number} fromIndex The index to search from.
+ * @param {Function} comparator The comparator invoked per element.
+ * @returns {number} Returns the index of the matched value, else `-1`.
+ */
+ function baseIndexOfWith(array, value, fromIndex, comparator) {
+ var index = fromIndex - 1,
+ length = array.length;
+
+ while (++index < length) {
+ if (comparator(array[index], value)) {
+ return index;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * The base implementation of `_.isNaN` without support for number objects.
+ *
+ * @private
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is `NaN`, else `false`.
+ */
+ function baseIsNaN(value) {
+ return value !== value;
+ }
+
+ /**
+ * The base implementation of `_.mean` and `_.meanBy` without support for
+ * iteratee shorthands.
+ *
+ * @private
+ * @param {Array} array The array to iterate over.
+ * @param {Function} iteratee The function invoked per iteration.
+ * @returns {number} Returns the mean.
+ */
+ function baseMean(array, iteratee) {
+ var length = array ? array.length : 0;
+ return length ? (baseSum(array, iteratee) / length) : NAN;
+ }
+
+ /**
+ * The base implementation of `_.property` without support for deep paths.
+ *
+ * @private
+ * @param {string} key The key of the property to get.
+ * @returns {Function} Returns the new accessor function.
+ */
+ function baseProperty(key) {
+ return function(object) {
+ return object == null ? undefined : object[key];
+ };
+ }
+
+ /**
+ * The base implementation of `_.propertyOf` without support for deep paths.
+ *
+ * @private
+ * @param {Object} object The object to query.
+ * @returns {Function} Returns the new accessor function.
+ */
+ function basePropertyOf(object) {
+ return function(key) {
+ return object == null ? undefined : object[key];
+ };
+ }
+
+ /**
+ * The base implementation of `_.reduce` and `_.reduceRight`, without support
+ * for iteratee shorthands, which iterates over `collection` using `eachFunc`.
+ *
+ * @private
+ * @param {Array|Object} collection The collection to iterate over.
+ * @param {Function} iteratee The function invoked per iteration.
+ * @param {*} accumulator The initial value.
+ * @param {boolean} initAccum Specify using the first or last element of
+ * `collection` as the initial value.
+ * @param {Function} eachFunc The function to iterate over `collection`.
+ * @returns {*} Returns the accumulated value.
+ */
+ function baseReduce(collection, iteratee, accumulator, initAccum, eachFunc) {
+ eachFunc(collection, function(value, index, collection) {
+ accumulator = initAccum
+ ? (initAccum = false, value)
+ : iteratee(accumulator, value, index, collection);
+ });
+ return accumulator;
+ }
+
+ /**
+ * The base implementation of `_.sortBy` which uses `comparer` to define the
+ * sort order of `array` and replaces criteria objects with their corresponding
+ * values.
+ *
+ * @private
+ * @param {Array} array The array to sort.
+ * @param {Function} comparer The function to define sort order.
+ * @returns {Array} Returns `array`.
+ */
+ function baseSortBy(array, comparer) {
+ var length = array.length;
+
+ array.sort(comparer);
+ while (length--) {
+ array[length] = array[length].value;
+ }
+ return array;
+ }
+
+ /**
+ * The base implementation of `_.sum` and `_.sumBy` without support for
+ * iteratee shorthands.
+ *
+ * @private
+ * @param {Array} array The array to iterate over.
+ * @param {Function} iteratee The function invoked per iteration.
+ * @returns {number} Returns the sum.
+ */
+ function baseSum(array, iteratee) {
+ var result,
+ index = -1,
+ length = array.length;
+
+ while (++index < length) {
+ var current = iteratee(array[index]);
+ if (current !== undefined) {
+ result = result === undefined ? current : (result + current);
+ }
+ }
+ return result;
+ }
+
+ /**
+ * The base implementation of `_.times` without support for iteratee shorthands
+ * or max array length checks.
+ *
+ * @private
+ * @param {number} n The number of times to invoke `iteratee`.
+ * @param {Function} iteratee The function invoked per iteration.
+ * @returns {Array} Returns the array of results.
+ */
+ function baseTimes(n, iteratee) {
+ var index = -1,
+ result = Array(n);
+
+ while (++index < n) {
+ result[index] = iteratee(index);
+ }
+ return result;
+ }
+
+ /**
+ * The base implementation of `_.toPairs` and `_.toPairsIn` which creates an array
+ * of key-value pairs for `object` corresponding to the property names of `props`.
+ *
+ * @private
+ * @param {Object} object The object to query.
+ * @param {Array} props The property names to get values for.
+ * @returns {Object} Returns the key-value pairs.
+ */
+ function baseToPairs(object, props) {
+ return arrayMap(props, function(key) {
+ return [key, object[key]];
+ });
+ }
+
+ /**
+ * The base implementation of `_.unary` without support for storing metadata.
+ *
+ * @private
+ * @param {Function} func The function to cap arguments for.
+ * @returns {Function} Returns the new capped function.
+ */
+ function baseUnary(func) {
+ return function(value) {
+ return func(value);
+ };
+ }
+
+ /**
+ * The base implementation of `_.values` and `_.valuesIn` which creates an
+ * array of `object` property values corresponding to the property names
+ * of `props`.
+ *
+ * @private
+ * @param {Object} object The object to query.
+ * @param {Array} props The property names to get values for.
+ * @returns {Object} Returns the array of property values.
+ */
+ function baseValues(object, props) {
+ return arrayMap(props, function(key) {
+ return object[key];
+ });
+ }
+
+ /**
+ * Checks if a `cache` value for `key` exists.
+ *
+ * @private
+ * @param {Object} cache The cache to query.
+ * @param {string} key The key of the entry to check.
+ * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.
+ */
+ function cacheHas(cache, key) {
+ return cache.has(key);
+ }
+
+ /**
+ * Used by `_.trim` and `_.trimStart` to get the index of the first string symbol
+ * that is not found in the character symbols.
+ *
+ * @private
+ * @param {Array} strSymbols The string symbols to inspect.
+ * @param {Array} chrSymbols The character symbols to find.
+ * @returns {number} Returns the index of the first unmatched string symbol.
+ */
+ function charsStartIndex(strSymbols, chrSymbols) {
+ var index = -1,
+ length = strSymbols.length;
+
+ while (++index < length && baseIndexOf(chrSymbols, strSymbols[index], 0) > -1) {}
+ return index;
+ }
+
+ /**
+ * Used by `_.trim` and `_.trimEnd` to get the index of the last string symbol
+ * that is not found in the character symbols.
+ *
+ * @private
+ * @param {Array} strSymbols The string symbols to inspect.
+ * @param {Array} chrSymbols The character symbols to find.
+ * @returns {number} Returns the index of the last unmatched string symbol.
+ */
+ function charsEndIndex(strSymbols, chrSymbols) {
+ var index = strSymbols.length;
+
+ while (index-- && baseIndexOf(chrSymbols, strSymbols[index], 0) > -1) {}
+ return index;
+ }
+
+ /**
+ * Gets the number of `placeholder` occurrences in `array`.
+ *
+ * @private
+ * @param {Array} array The array to inspect.
+ * @param {*} placeholder The placeholder to search for.
+ * @returns {number} Returns the placeholder count.
+ */
+ function countHolders(array, placeholder) {
+ var length = array.length,
+ result = 0;
+
+ while (length--) {
+ if (array[length] === placeholder) {
+ ++result;
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Used by `_.deburr` to convert Latin-1 Supplement and Latin Extended-A
+ * letters to basic Latin letters.
+ *
+ * @private
+ * @param {string} letter The matched letter to deburr.
+ * @returns {string} Returns the deburred letter.
+ */
+ var deburrLetter = basePropertyOf(deburredLetters);
+
+ /**
+ * Used by `_.escape` to convert characters to HTML entities.
+ *
+ * @private
+ * @param {string} chr The matched character to escape.
+ * @returns {string} Returns the escaped character.
+ */
+ var escapeHtmlChar = basePropertyOf(htmlEscapes);
+
+ /**
+ * Used by `_.template` to escape characters for inclusion in compiled string literals.
+ *
+ * @private
+ * @param {string} chr The matched character to escape.
+ * @returns {string} Returns the escaped character.
+ */
+ function escapeStringChar(chr) {
+ return '\\' + stringEscapes[chr];
+ }
+
+ /**
+ * Gets the value at `key` of `object`.
+ *
+ * @private
+ * @param {Object} [object] The object to query.
+ * @param {string} key The key of the property to get.
+ * @returns {*} Returns the property value.
+ */
+ function getValue(object, key) {
+ return object == null ? undefined : object[key];
+ }
+
+ /**
+ * Checks if `string` contains Unicode symbols.
+ *
+ * @private
+ * @param {string} string The string to inspect.
+ * @returns {boolean} Returns `true` if a symbol is found, else `false`.
+ */
+ function hasUnicode(string) {
+ return reHasUnicode.test(string);
+ }
+
+ /**
+ * Checks if `string` contains a word composed of Unicode symbols.
+ *
+ * @private
+ * @param {string} string The string to inspect.
+ * @returns {boolean} Returns `true` if a word is found, else `false`.
+ */
+ function hasUnicodeWord(string) {
+ return reHasUnicodeWord.test(string);
+ }
+
+ /**
+ * Converts `iterator` to an array.
+ *
+ * @private
+ * @param {Object} iterator The iterator to convert.
+ * @returns {Array} Returns the converted array.
+ */
+ function iteratorToArray(iterator) {
+ var data,
+ result = [];
+
+ while (!(data = iterator.next()).done) {
+ result.push(data.value);
+ }
+ return result;
+ }
+
+ /**
+ * Converts `map` to its key-value pairs.
+ *
+ * @private
+ * @param {Object} map The map to convert.
+ * @returns {Array} Returns the key-value pairs.
+ */
+ function mapToArray(map) {
+ var index = -1,
+ result = Array(map.size);
+
+ map.forEach(function(value, key) {
+ result[++index] = [key, value];
+ });
+ return result;
+ }
+
+ /**
+ * Creates a unary function that invokes `func` with its argument transformed.
+ *
+ * @private
+ * @param {Function} func The function to wrap.
+ * @param {Function} transform The argument transform.
+ * @returns {Function} Returns the new function.
+ */
+ function overArg(func, transform) {
+ return function(arg) {
+ return func(transform(arg));
+ };
+ }
+
+ /**
+ * Replaces all `placeholder` elements in `array` with an internal placeholder
+ * and returns an array of their indexes.
+ *
+ * @private
+ * @param {Array} array The array to modify.
+ * @param {*} placeholder The placeholder to replace.
+ * @returns {Array} Returns the new array of placeholder indexes.
+ */
+ function replaceHolders(array, placeholder) {
+ var index = -1,
+ length = array.length,
+ resIndex = 0,
+ result = [];
+
+ while (++index < length) {
+ var value = array[index];
+ if (value === placeholder || value === PLACEHOLDER) {
+ array[index] = PLACEHOLDER;
+ result[resIndex++] = index;
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Converts `set` to an array of its values.
+ *
+ * @private
+ * @param {Object} set The set to convert.
+ * @returns {Array} Returns the values.
+ */
+ function setToArray(set) {
+ var index = -1,
+ result = Array(set.size);
+
+ set.forEach(function(value) {
+ result[++index] = value;
+ });
+ return result;
+ }
+
+ /**
+ * Converts `set` to its value-value pairs.
+ *
+ * @private
+ * @param {Object} set The set to convert.
+ * @returns {Array} Returns the value-value pairs.
+ */
+ function setToPairs(set) {
+ var index = -1,
+ result = Array(set.size);
+
+ set.forEach(function(value) {
+ result[++index] = [value, value];
+ });
+ return result;
+ }
+
+ /**
+ * A specialized version of `_.indexOf` which performs strict equality
+ * comparisons of values, i.e. `===`.
+ *
+ * @private
+ * @param {Array} array The array to inspect.
+ * @param {*} value The value to search for.
+ * @param {number} fromIndex The index to search from.
+ * @returns {number} Returns the index of the matched value, else `-1`.
+ */
+ function strictIndexOf(array, value, fromIndex) {
+ var index = fromIndex - 1,
+ length = array.length;
+
+ while (++index < length) {
+ if (array[index] === value) {
+ return index;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * A specialized version of `_.lastIndexOf` which performs strict equality
+ * comparisons of values, i.e. `===`.
+ *
+ * @private
+ * @param {Array} array The array to inspect.
+ * @param {*} value The value to search for.
+ * @param {number} fromIndex The index to search from.
+ * @returns {number} Returns the index of the matched value, else `-1`.
+ */
+ function strictLastIndexOf(array, value, fromIndex) {
+ var index = fromIndex + 1;
+ while (index--) {
+ if (array[index] === value) {
+ return index;
+ }
+ }
+ return index;
+ }
+
+ /**
+ * Gets the number of symbols in `string`.
+ *
+ * @private
+ * @param {string} string The string to inspect.
+ * @returns {number} Returns the string size.
+ */
+ function stringSize(string) {
+ return hasUnicode(string)
+ ? unicodeSize(string)
+ : asciiSize(string);
+ }
+
+ /**
+ * Converts `string` to an array.
+ *
+ * @private
+ * @param {string} string The string to convert.
+ * @returns {Array} Returns the converted array.
+ */
+ function stringToArray(string) {
+ return hasUnicode(string)
+ ? unicodeToArray(string)
+ : asciiToArray(string);
+ }
+
+ /**
+ * Used by `_.unescape` to convert HTML entities to characters.
+ *
+ * @private
+ * @param {string} chr The matched character to unescape.
+ * @returns {string} Returns the unescaped character.
+ */
+ var unescapeHtmlChar = basePropertyOf(htmlUnescapes);
+
+ /**
+ * Gets the size of a Unicode `string`.
+ *
+ * @private
+ * @param {string} string The string inspect.
+ * @returns {number} Returns the string size.
+ */
+ function unicodeSize(string) {
+ var result = reUnicode.lastIndex = 0;
+ while (reUnicode.test(string)) {
+ ++result;
+ }
+ return result;
+ }
+
+ /**
+ * Converts a Unicode `string` to an array.
+ *
+ * @private
+ * @param {string} string The string to convert.
+ * @returns {Array} Returns the converted array.
+ */
+ function unicodeToArray(string) {
+ return string.match(reUnicode) || [];
+ }
+
+ /**
+ * Splits a Unicode `string` into an array of its words.
+ *
+ * @private
+ * @param {string} The string to inspect.
+ * @returns {Array} Returns the words of `string`.
+ */
+ function unicodeWords(string) {
+ return string.match(reUnicodeWord) || [];
+ }
+
+ /*--------------------------------------------------------------------------*/
+
+ /**
+ * Create a new pristine `lodash` function using the `context` object.
+ *
+ * @static
+ * @memberOf _
+ * @since 1.1.0
+ * @category Util
+ * @param {Object} [context=root] The context object.
+ * @returns {Function} Returns a new `lodash` function.
+ * @example
+ *
+ * _.mixin({ 'foo': _.constant('foo') });
+ *
+ * var lodash = _.runInContext();
+ * lodash.mixin({ 'bar': lodash.constant('bar') });
+ *
+ * _.isFunction(_.foo);
+ * // => true
+ * _.isFunction(_.bar);
+ * // => false
+ *
+ * lodash.isFunction(lodash.foo);
+ * // => false
+ * lodash.isFunction(lodash.bar);
+ * // => true
+ *
+ * // Create a suped-up `defer` in Node.js.
+ * var defer = _.runInContext({ 'setTimeout': setImmediate }).defer;
+ */
+ var runInContext = (function runInContext(context) {
+ context = context ? _.defaults(root.Object(), context, _.pick(root, contextProps)) : root;
+
+ /** Built-in constructor references. */
+ var Array = context.Array,
+ Date = context.Date,
+ Error = context.Error,
+ Function = context.Function,
+ Math = context.Math,
+ Object = context.Object,
+ RegExp = context.RegExp,
+ String = context.String,
+ TypeError = context.TypeError;
+
+ /** Used for built-in method references. */
+ var arrayProto = Array.prototype,
+ funcProto = Function.prototype,
+ objectProto = Object.prototype;
+
+ /** Used to detect overreaching core-js shims. */
+ var coreJsData = context['__core-js_shared__'];
+
+ /** Used to detect methods masquerading as native. */
+ var maskSrcKey = (function() {
+ var uid = /[^.]+$/.exec(coreJsData && coreJsData.keys && coreJsData.keys.IE_PROTO || '');
+ return uid ? ('Symbol(src)_1.' + uid) : '';
+ }());
+
+ /** Used to resolve the decompiled source of functions. */
+ var funcToString = funcProto.toString;
+
+ /** Used to check objects for own properties. */
+ var hasOwnProperty = objectProto.hasOwnProperty;
+
+ /** Used to generate unique IDs. */
+ var idCounter = 0;
+
+ /** Used to infer the `Object` constructor. */
+ var objectCtorString = funcToString.call(Object);
+
+ /**
+ * Used to resolve the
+ * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring)
+ * of values.
+ */
+ var objectToString = objectProto.toString;
+
+ /** Used to restore the original `_` reference in `_.noConflict`. */
+ var oldDash = root._;
+
+ /** Used to detect if a method is native. */
+ var reIsNative = RegExp('^' +
+ funcToString.call(hasOwnProperty).replace(reRegExpChar, '\\$&')
+ .replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g, '$1.*?') + '$'
+ );
+
+ /** Built-in value references. */
+ var Buffer = moduleExports ? context.Buffer : undefined,
+ Symbol = context.Symbol,
+ Uint8Array = context.Uint8Array,
+ allocUnsafe = Buffer ? Buffer.allocUnsafe : undefined,
+ getPrototype = overArg(Object.getPrototypeOf, Object),
+ iteratorSymbol = Symbol ? Symbol.iterator : undefined,
+ objectCreate = Object.create,
+ propertyIsEnumerable = objectProto.propertyIsEnumerable,
+ splice = arrayProto.splice,
+ spreadableSymbol = Symbol ? Symbol.isConcatSpreadable : undefined;
+
+ var defineProperty = (function() {
+ try {
+ var func = getNative(Object, 'defineProperty');
+ func({}, '', {});
+ return func;
+ } catch (e) {}
+ }());
+
+ /** Mocked built-ins. */
+ var ctxClearTimeout = context.clearTimeout !== root.clearTimeout && context.clearTimeout,
+ ctxNow = Date && Date.now !== root.Date.now && Date.now,
+ ctxSetTimeout = context.setTimeout !== root.setTimeout && context.setTimeout;
+
+ /* Built-in method references for those with the same name as other `lodash` methods. */
+ var nativeCeil = Math.ceil,
+ nativeFloor = Math.floor,
+ nativeGetSymbols = Object.getOwnPropertySymbols,
+ nativeIsBuffer = Buffer ? Buffer.isBuffer : undefined,
+ nativeIsFinite = context.isFinite,
+ nativeJoin = arrayProto.join,
+ nativeKeys = overArg(Object.keys, Object),
+ nativeMax = Math.max,
+ nativeMin = Math.min,
+ nativeNow = Date.now,
+ nativeParseInt = context.parseInt,
+ nativeRandom = Math.random,
+ nativeReverse = arrayProto.reverse;
+
+ /* Built-in method references that are verified to be native. */
+ var DataView = getNative(context, 'DataView'),
+ Map = getNative(context, 'Map'),
+ Promise = getNative(context, 'Promise'),
+ Set = getNative(context, 'Set'),
+ WeakMap = getNative(context, 'WeakMap'),
+ nativeCreate = getNative(Object, 'create');
+
+ /** Used to store function metadata. */
+ var metaMap = WeakMap && new WeakMap;
+
+ /** Used to lookup unminified function names. */
+ var realNames = {};
+
+ /** Used to detect maps, sets, and weakmaps. */
+ var dataViewCtorString = toSource(DataView),
+ mapCtorString = toSource(Map),
+ promiseCtorString = toSource(Promise),
+ setCtorString = toSource(Set),
+ weakMapCtorString = toSource(WeakMap);
+
+ /** Used to convert symbols to primitives and strings. */
+ var symbolProto = Symbol ? Symbol.prototype : undefined,
+ symbolValueOf = symbolProto ? symbolProto.valueOf : undefined,
+ symbolToString = symbolProto ? symbolProto.toString : undefined;
+
+ /*------------------------------------------------------------------------*/
+
+ /**
+ * Creates a `lodash` object which wraps `value` to enable implicit method
+ * chain sequences. Methods that operate on and return arrays, collections,
+ * and functions can be chained together. Methods that retrieve a single value
+ * or may return a primitive value will automatically end the chain sequence
+ * and return the unwrapped value. Otherwise, the value must be unwrapped
+ * with `_#value`.
+ *
+ * Explicit chain sequences, which must be unwrapped with `_#value`, may be
+ * enabled using `_.chain`.
+ *
+ * The execution of chained methods is lazy, that is, it's deferred until
+ * `_#value` is implicitly or explicitly called.
+ *
+ * Lazy evaluation allows several methods to support shortcut fusion.
+ * Shortcut fusion is an optimization to merge iteratee calls; this avoids
+ * the creation of intermediate arrays and can greatly reduce the number of
+ * iteratee executions. Sections of a chain sequence qualify for shortcut
+ * fusion if the section is applied to an array of at least `200` elements
+ * and any iteratees accept only one argument. The heuristic for whether a
+ * section qualifies for shortcut fusion is subject to change.
+ *
+ * Chaining is supported in custom builds as long as the `_#value` method is
+ * directly or indirectly included in the build.
+ *
+ * In addition to lodash methods, wrappers have `Array` and `String` methods.
+ *
+ * The wrapper `Array` methods are:
+ * `concat`, `join`, `pop`, `push`, `shift`, `sort`, `splice`, and `unshift`
+ *
+ * The wrapper `String` methods are:
+ * `replace` and `split`
+ *
+ * The wrapper methods that support shortcut fusion are:
+ * `at`, `compact`, `drop`, `dropRight`, `dropWhile`, `filter`, `find`,
+ * `findLast`, `head`, `initial`, `last`, `map`, `reject`, `reverse`, `slice`,
+ * `tail`, `take`, `takeRight`, `takeRightWhile`, `takeWhile`, and `toArray`
+ *
+ * The chainable wrapper methods are:
+ * `after`, `ary`, `assign`, `assignIn`, `assignInWith`, `assignWith`, `at`,
+ * `before`, `bind`, `bindAll`, `bindKey`, `castArray`, `chain`, `chunk`,
+ * `commit`, `compact`, `concat`, `conforms`, `constant`, `countBy`, `create`,
+ * `curry`, `debounce`, `defaults`, `defaultsDeep`, `defer`, `delay`,
+ * `difference`, `differenceBy`, `differenceWith`, `drop`, `dropRight`,
+ * `dropRightWhile`, `dropWhile`, `extend`, `extendWith`, `fill`, `filter`,
+ * `flatMap`, `flatMapDeep`, `flatMapDepth`, `flatten`, `flattenDeep`,
+ * `flattenDepth`, `flip`, `flow`, `flowRight`, `fromPairs`, `functions`,
+ * `functionsIn`, `groupBy`, `initial`, `intersection`, `intersectionBy`,
+ * `intersectionWith`, `invert`, `invertBy`, `invokeMap`, `iteratee`, `keyBy`,
+ * `keys`, `keysIn`, `map`, `mapKeys`, `mapValues`, `matches`, `matchesProperty`,
+ * `memoize`, `merge`, `mergeWith`, `method`, `methodOf`, `mixin`, `negate`,
+ * `nthArg`, `omit`, `omitBy`, `once`, `orderBy`, `over`, `overArgs`,
+ * `overEvery`, `overSome`, `partial`, `partialRight`, `partition`, `pick`,
+ * `pickBy`, `plant`, `property`, `propertyOf`, `pull`, `pullAll`, `pullAllBy`,
+ * `pullAllWith`, `pullAt`, `push`, `range`, `rangeRight`, `rearg`, `reject`,
+ * `remove`, `rest`, `reverse`, `sampleSize`, `set`, `setWith`, `shuffle`,
+ * `slice`, `sort`, `sortBy`, `splice`, `spread`, `tail`, `take`, `takeRight`,
+ * `takeRightWhile`, `takeWhile`, `tap`, `throttle`, `thru`, `toArray`,
+ * `toPairs`, `toPairsIn`, `toPath`, `toPlainObject`, `transform`, `unary`,
+ * `union`, `unionBy`, `unionWith`, `uniq`, `uniqBy`, `uniqWith`, `unset`,
+ * `unshift`, `unzip`, `unzipWith`, `update`, `updateWith`, `values`,
+ * `valuesIn`, `without`, `wrap`, `xor`, `xorBy`, `xorWith`, `zip`,
+ * `zipObject`, `zipObjectDeep`, and `zipWith`
+ *
+ * The wrapper methods that are **not** chainable by default are:
+ * `add`, `attempt`, `camelCase`, `capitalize`, `ceil`, `clamp`, `clone`,
+ * `cloneDeep`, `cloneDeepWith`, `cloneWith`, `conformsTo`, `deburr`,
+ * `defaultTo`, `divide`, `each`, `eachRight`, `endsWith`, `eq`, `escape`,
+ * `escapeRegExp`, `every`, `find`, `findIndex`, `findKey`, `findLast`,
+ * `findLastIndex`, `findLastKey`, `first`, `floor`, `forEach`, `forEachRight`,
+ * `forIn`, `forInRight`, `forOwn`, `forOwnRight`, `get`, `gt`, `gte`, `has`,
+ * `hasIn`, `head`, `identity`, `includes`, `indexOf`, `inRange`, `invoke`,
+ * `isArguments`, `isArray`, `isArrayBuffer`, `isArrayLike`, `isArrayLikeObject`,
+ * `isBoolean`, `isBuffer`, `isDate`, `isElement`, `isEmpty`, `isEqual`,
+ * `isEqualWith`, `isError`, `isFinite`, `isFunction`, `isInteger`, `isLength`,
+ * `isMap`, `isMatch`, `isMatchWith`, `isNaN`, `isNative`, `isNil`, `isNull`,
+ * `isNumber`, `isObject`, `isObjectLike`, `isPlainObject`, `isRegExp`,
+ * `isSafeInteger`, `isSet`, `isString`, `isUndefined`, `isTypedArray`,
+ * `isWeakMap`, `isWeakSet`, `join`, `kebabCase`, `last`, `lastIndexOf`,
+ * `lowerCase`, `lowerFirst`, `lt`, `lte`, `max`, `maxBy`, `mean`, `meanBy`,
+ * `min`, `minBy`, `multiply`, `noConflict`, `noop`, `now`, `nth`, `pad`,
+ * `padEnd`, `padStart`, `parseInt`, `pop`, `random`, `reduce`, `reduceRight`,
+ * `repeat`, `result`, `round`, `runInContext`, `sample`, `shift`, `size`,
+ * `snakeCase`, `some`, `sortedIndex`, `sortedIndexBy`, `sortedLastIndex`,
+ * `sortedLastIndexBy`, `startCase`, `startsWith`, `stubArray`, `stubFalse`,
+ * `stubObject`, `stubString`, `stubTrue`, `subtract`, `sum`, `sumBy`,
+ * `template`, `times`, `toFinite`, `toInteger`, `toJSON`, `toLength`,
+ * `toLower`, `toNumber`, `toSafeInteger`, `toString`, `toUpper`, `trim`,
+ * `trimEnd`, `trimStart`, `truncate`, `unescape`, `uniqueId`, `upperCase`,
+ * `upperFirst`, `value`, and `words`
+ *
+ * @name _
+ * @constructor
+ * @category Seq
+ * @param {*} value The value to wrap in a `lodash` instance.
+ * @returns {Object} Returns the new `lodash` wrapper instance.
+ * @example
+ *
+ * function square(n) {
+ * return n * n;
+ * }
+ *
+ * var wrapped = _([1, 2, 3]);
+ *
+ * // Returns an unwrapped value.
+ * wrapped.reduce(_.add);
+ * // => 6
+ *
+ * // Returns a wrapped value.
+ * var squares = wrapped.map(square);
+ *
+ * _.isArray(squares);
+ * // => false
+ *
+ * _.isArray(squares.value());
+ * // => true
+ */
+ function lodash(value) {
+ if (isObjectLike(value) && !isArray(value) && !(value instanceof LazyWrapper)) {
+ if (value instanceof LodashWrapper) {
+ return value;
+ }
+ if (hasOwnProperty.call(value, '__wrapped__')) {
+ return wrapperClone(value);
+ }
+ }
+ return new LodashWrapper(value);
+ }
+
+ /**
+ * The base implementation of `_.create` without support for assigning
+ * properties to the created object.
+ *
+ * @private
+ * @param {Object} proto The object to inherit from.
+ * @returns {Object} Returns the new object.
+ */
+ var baseCreate = (function() {
+ function object() {}
+ return function(proto) {
+ if (!isObject(proto)) {
+ return {};
+ }
+ if (objectCreate) {
+ return objectCreate(proto);
+ }
+ object.prototype = proto;
+ var result = new object;
+ object.prototype = undefined;
+ return result;
+ };
+ }());
+
+ /**
+ * The function whose prototype chain sequence wrappers inherit from.
+ *
+ * @private
+ */
+ function baseLodash() {
+ // No operation performed.
+ }
+
+ /**
+ * The base constructor for creating `lodash` wrapper objects.
+ *
+ * @private
+ * @param {*} value The value to wrap.
+ * @param {boolean} [chainAll] Enable explicit method chain sequences.
+ */
+ function LodashWrapper(value, chainAll) {
+ this.__wrapped__ = value;
+ this.__actions__ = [];
+ this.__chain__ = !!chainAll;
+ this.__index__ = 0;
+ this.__values__ = undefined;
+ }
+
+ /**
+ * By default, the template delimiters used by lodash are like those in
+ * embedded Ruby (ERB). Change the following template settings to use
+ * alternative delimiters.
+ *
+ * @static
+ * @memberOf _
+ * @type {Object}
+ */
+ lodash.templateSettings = {
+
+ /**
+ * Used to detect `data` property values to be HTML-escaped.
+ *
+ * @memberOf _.templateSettings
+ * @type {RegExp}
+ */
+ 'escape': reEscape,
+
+ /**
+ * Used to detect code to be evaluated.
+ *
+ * @memberOf _.templateSettings
+ * @type {RegExp}
+ */
+ 'evaluate': reEvaluate,
+
+ /**
+ * Used to detect `data` property values to inject.
+ *
+ * @memberOf _.templateSettings
+ * @type {RegExp}
+ */
+ 'interpolate': reInterpolate,
+
+ /**
+ * Used to reference the data object in the template text.
+ *
+ * @memberOf _.templateSettings
+ * @type {string}
+ */
+ 'variable': '',
+
+ /**
+ * Used to import variables into the compiled template.
+ *
+ * @memberOf _.templateSettings
+ * @type {Object}
+ */
+ 'imports': {
+
+ /**
+ * A reference to the `lodash` function.
+ *
+ * @memberOf _.templateSettings.imports
+ * @type {Function}
+ */
+ '_': lodash
+ }
+ };
+
+ // Ensure wrappers are instances of `baseLodash`.
+ lodash.prototype = baseLodash.prototype;
+ lodash.prototype.constructor = lodash;
+
+ LodashWrapper.prototype = baseCreate(baseLodash.prototype);
+ LodashWrapper.prototype.constructor = LodashWrapper;
+
+ /*------------------------------------------------------------------------*/
+
+ /**
+ * Creates a lazy wrapper object which wraps `value` to enable lazy evaluation.
+ *
+ * @private
+ * @constructor
+ * @param {*} value The value to wrap.
+ */
+ function LazyWrapper(value) {
+ this.__wrapped__ = value;
+ this.__actions__ = [];
+ this.__dir__ = 1;
+ this.__filtered__ = false;
+ this.__iteratees__ = [];
+ this.__takeCount__ = MAX_ARRAY_LENGTH;
+ this.__views__ = [];
+ }
+
+ /**
+ * Creates a clone of the lazy wrapper object.
+ *
+ * @private
+ * @name clone
+ * @memberOf LazyWrapper
+ * @returns {Object} Returns the cloned `LazyWrapper` object.
+ */
+ function lazyClone() {
+ var result = new LazyWrapper(this.__wrapped__);
+ result.__actions__ = copyArray(this.__actions__);
+ result.__dir__ = this.__dir__;
+ result.__filtered__ = this.__filtered__;
+ result.__iteratees__ = copyArray(this.__iteratees__);
+ result.__takeCount__ = this.__takeCount__;
+ result.__views__ = copyArray(this.__views__);
+ return result;
+ }
+
+ /**
+ * Reverses the direction of lazy iteration.
+ *
+ * @private
+ * @name reverse
+ * @memberOf LazyWrapper
+ * @returns {Object} Returns the new reversed `LazyWrapper` object.
+ */
+ function lazyReverse() {
+ if (this.__filtered__) {
+ var result = new LazyWrapper(this);
+ result.__dir__ = -1;
+ result.__filtered__ = true;
+ } else {
+ result = this.clone();
+ result.__dir__ *= -1;
+ }
+ return result;
+ }
+
+ /**
+ * Extracts the unwrapped value from its lazy wrapper.
+ *
+ * @private
+ * @name value
+ * @memberOf LazyWrapper
+ * @returns {*} Returns the unwrapped value.
+ */
+ function lazyValue() {
+ var array = this.__wrapped__.value(),
+ dir = this.__dir__,
+ isArr = isArray(array),
+ isRight = dir < 0,
+ arrLength = isArr ? array.length : 0,
+ view = getView(0, arrLength, this.__views__),
+ start = view.start,
+ end = view.end,
+ length = end - start,
+ index = isRight ? end : (start - 1),
+ iteratees = this.__iteratees__,
+ iterLength = iteratees.length,
+ resIndex = 0,
+ takeCount = nativeMin(length, this.__takeCount__);
+
+ if (!isArr || arrLength < LARGE_ARRAY_SIZE ||
+ (arrLength == length && takeCount == length)) {
+ return baseWrapperValue(array, this.__actions__);
+ }
+ var result = [];
+
+ outer:
+ while (length-- && resIndex < takeCount) {
+ index += dir;
+
+ var iterIndex = -1,
+ value = array[index];
+
+ while (++iterIndex < iterLength) {
+ var data = iteratees[iterIndex],
+ iteratee = data.iteratee,
+ type = data.type,
+ computed = iteratee(value);
+
+ if (type == LAZY_MAP_FLAG) {
+ value = computed;
+ } else if (!computed) {
+ if (type == LAZY_FILTER_FLAG) {
+ continue outer;
+ } else {
+ break outer;
+ }
+ }
+ }
+ result[resIndex++] = value;
+ }
+ return result;
+ }
+
+ // Ensure `LazyWrapper` is an instance of `baseLodash`.
+ LazyWrapper.prototype = baseCreate(baseLodash.prototype);
+ LazyWrapper.prototype.constructor = LazyWrapper;
+
+ /*------------------------------------------------------------------------*/
+
+ /**
+ * Creates a hash object.
+ *
+ * @private
+ * @constructor
+ * @param {Array} [entries] The key-value pairs to cache.
+ */
+ function Hash(entries) {
+ var index = -1,
+ length = entries ? entries.length : 0;
+
+ this.clear();
+ while (++index < length) {
+ var entry = entries[index];
+ this.set(entry[0], entry[1]);
+ }
+ }
+
+ /**
+ * Removes all key-value entries from the hash.
+ *
+ * @private
+ * @name clear
+ * @memberOf Hash
+ */
+ function hashClear() {
+ this.__data__ = nativeCreate ? nativeCreate(null) : {};
+ this.size = 0;
+ }
+
+ /**
+ * Removes `key` and its value from the hash.
+ *
+ * @private
+ * @name delete
+ * @memberOf Hash
+ * @param {Object} hash The hash to modify.
+ * @param {string} key The key of the value to remove.
+ * @returns {boolean} Returns `true` if the entry was removed, else `false`.
+ */
+ function hashDelete(key) {
+ var result = this.has(key) && delete this.__data__[key];
+ this.size -= result ? 1 : 0;
+ return result;
+ }
+
+ /**
+ * Gets the hash value for `key`.
+ *
+ * @private
+ * @name get
+ * @memberOf Hash
+ * @param {string} key The key of the value to get.
+ * @returns {*} Returns the entry value.
+ */
+ function hashGet(key) {
+ var data = this.__data__;
+ if (nativeCreate) {
+ var result = data[key];
+ return result === HASH_UNDEFINED ? undefined : result;
+ }
+ return hasOwnProperty.call(data, key) ? data[key] : undefined;
+ }
+
+ /**
+ * Checks if a hash value for `key` exists.
+ *
+ * @private
+ * @name has
+ * @memberOf Hash
+ * @param {string} key The key of the entry to check.
+ * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.
+ */
+ function hashHas(key) {
+ var data = this.__data__;
+ return nativeCreate ? data[key] !== undefined : hasOwnProperty.call(data, key);
+ }
+
+ /**
+ * Sets the hash `key` to `value`.
+ *
+ * @private
+ * @name set
+ * @memberOf Hash
+ * @param {string} key The key of the value to set.
+ * @param {*} value The value to set.
+ * @returns {Object} Returns the hash instance.
+ */
+ function hashSet(key, value) {
+ var data = this.__data__;
+ this.size += this.has(key) ? 0 : 1;
+ data[key] = (nativeCreate && value === undefined) ? HASH_UNDEFINED : value;
+ return this;
+ }
+
+ // Add methods to `Hash`.
+ Hash.prototype.clear = hashClear;
+ Hash.prototype['delete'] = hashDelete;
+ Hash.prototype.get = hashGet;
+ Hash.prototype.has = hashHas;
+ Hash.prototype.set = hashSet;
+
+ /*------------------------------------------------------------------------*/
+
+ /**
+ * Creates an list cache object.
+ *
+ * @private
+ * @constructor
+ * @param {Array} [entries] The key-value pairs to cache.
+ */
+ function ListCache(entries) {
+ var index = -1,
+ length = entries ? entries.length : 0;
+
+ this.clear();
+ while (++index < length) {
+ var entry = entries[index];
+ this.set(entry[0], entry[1]);
+ }
+ }
+
+ /**
+ * Removes all key-value entries from the list cache.
+ *
+ * @private
+ * @name clear
+ * @memberOf ListCache
+ */
+ function listCacheClear() {
+ this.__data__ = [];
+ this.size = 0;
+ }
+
+ /**
+ * Removes `key` and its value from the list cache.
+ *
+ * @private
+ * @name delete
+ * @memberOf ListCache
+ * @param {string} key The key of the value to remove.
+ * @returns {boolean} Returns `true` if the entry was removed, else `false`.
+ */
+ function listCacheDelete(key) {
+ var data = this.__data__,
+ index = assocIndexOf(data, key);
+
+ if (index < 0) {
+ return false;
+ }
+ var lastIndex = data.length - 1;
+ if (index == lastIndex) {
+ data.pop();
+ } else {
+ splice.call(data, index, 1);
+ }
+ --this.size;
+ return true;
+ }
+
+ /**
+ * Gets the list cache value for `key`.
+ *
+ * @private
+ * @name get
+ * @memberOf ListCache
+ * @param {string} key The key of the value to get.
+ * @returns {*} Returns the entry value.
+ */
+ function listCacheGet(key) {
+ var data = this.__data__,
+ index = assocIndexOf(data, key);
+
+ return index < 0 ? undefined : data[index][1];
+ }
+
+ /**
+ * Checks if a list cache value for `key` exists.
+ *
+ * @private
+ * @name has
+ * @memberOf ListCache
+ * @param {string} key The key of the entry to check.
+ * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.
+ */
+ function listCacheHas(key) {
+ return assocIndexOf(this.__data__, key) > -1;
+ }
+
+ /**
+ * Sets the list cache `key` to `value`.
+ *
+ * @private
+ * @name set
+ * @memberOf ListCache
+ * @param {string} key The key of the value to set.
+ * @param {*} value The value to set.
+ * @returns {Object} Returns the list cache instance.
+ */
+ function listCacheSet(key, value) {
+ var data = this.__data__,
+ index = assocIndexOf(data, key);
+
+ if (index < 0) {
+ ++this.size;
+ data.push([key, value]);
+ } else {
+ data[index][1] = value;
+ }
+ return this;
+ }
+
+ // Add methods to `ListCache`.
+ ListCache.prototype.clear = listCacheClear;
+ ListCache.prototype['delete'] = listCacheDelete;
+ ListCache.prototype.get = listCacheGet;
+ ListCache.prototype.has = listCacheHas;
+ ListCache.prototype.set = listCacheSet;
+
+ /*------------------------------------------------------------------------*/
+
+ /**
+ * Creates a map cache object to store key-value pairs.
+ *
+ * @private
+ * @constructor
+ * @param {Array} [entries] The key-value pairs to cache.
+ */
+ function MapCache(entries) {
+ var index = -1,
+ length = entries ? entries.length : 0;
+
+ this.clear();
+ while (++index < length) {
+ var entry = entries[index];
+ this.set(entry[0], entry[1]);
+ }
+ }
+
+ /**
+ * Removes all key-value entries from the map.
+ *
+ * @private
+ * @name clear
+ * @memberOf MapCache
+ */
+ function mapCacheClear() {
+ this.size = 0;
+ this.__data__ = {
+ 'hash': new Hash,
+ 'map': new (Map || ListCache),
+ 'string': new Hash
+ };
+ }
+
+ /**
+ * Removes `key` and its value from the map.
+ *
+ * @private
+ * @name delete
+ * @memberOf MapCache
+ * @param {string} key The key of the value to remove.
+ * @returns {boolean} Returns `true` if the entry was removed, else `false`.
+ */
+ function mapCacheDelete(key) {
+ var result = getMapData(this, key)['delete'](key);
+ this.size -= result ? 1 : 0;
+ return result;
+ }
+
+ /**
+ * Gets the map value for `key`.
+ *
+ * @private
+ * @name get
+ * @memberOf MapCache
+ * @param {string} key The key of the value to get.
+ * @returns {*} Returns the entry value.
+ */
+ function mapCacheGet(key) {
+ return getMapData(this, key).get(key);
+ }
+
+ /**
+ * Checks if a map value for `key` exists.
+ *
+ * @private
+ * @name has
+ * @memberOf MapCache
+ * @param {string} key The key of the entry to check.
+ * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.
+ */
+ function mapCacheHas(key) {
+ return getMapData(this, key).has(key);
+ }
+
+ /**
+ * Sets the map `key` to `value`.
+ *
+ * @private
+ * @name set
+ * @memberOf MapCache
+ * @param {string} key The key of the value to set.
+ * @param {*} value The value to set.
+ * @returns {Object} Returns the map cache instance.
+ */
+ function mapCacheSet(key, value) {
+ var data = getMapData(this, key),
+ size = data.size;
+
+ data.set(key, value);
+ this.size += data.size == size ? 0 : 1;
+ return this;
+ }
+
+ // Add methods to `MapCache`.
+ MapCache.prototype.clear = mapCacheClear;
+ MapCache.prototype['delete'] = mapCacheDelete;
+ MapCache.prototype.get = mapCacheGet;
+ MapCache.prototype.has = mapCacheHas;
+ MapCache.prototype.set = mapCacheSet;
+
+ /*------------------------------------------------------------------------*/
+
+ /**
+ *
+ * Creates an array cache object to store unique values.
+ *
+ * @private
+ * @constructor
+ * @param {Array} [values] The values to cache.
+ */
+ function SetCache(values) {
+ var index = -1,
+ length = values ? values.length : 0;
+
+ this.__data__ = new MapCache;
+ while (++index < length) {
+ this.add(values[index]);
+ }
+ }
+
+ /**
+ * Adds `value` to the array cache.
+ *
+ * @private
+ * @name add
+ * @memberOf SetCache
+ * @alias push
+ * @param {*} value The value to cache.
+ * @returns {Object} Returns the cache instance.
+ */
+ function setCacheAdd(value) {
+ this.__data__.set(value, HASH_UNDEFINED);
+ return this;
+ }
+
+ /**
+ * Checks if `value` is in the array cache.
+ *
+ * @private
+ * @name has
+ * @memberOf SetCache
+ * @param {*} value The value to search for.
+ * @returns {number} Returns `true` if `value` is found, else `false`.
+ */
+ function setCacheHas(value) {
+ return this.__data__.has(value);
+ }
+
+ // Add methods to `SetCache`.
+ SetCache.prototype.add = SetCache.prototype.push = setCacheAdd;
+ SetCache.prototype.has = setCacheHas;
+
+ /*------------------------------------------------------------------------*/
+
+ /**
+ * Creates a stack cache object to store key-value pairs.
+ *
+ * @private
+ * @constructor
+ * @param {Array} [entries] The key-value pairs to cache.
+ */
+ function Stack(entries) {
+ var data = this.__data__ = new ListCache(entries);
+ this.size = data.size;
+ }
+
+ /**
+ * Removes all key-value entries from the stack.
+ *
+ * @private
+ * @name clear
+ * @memberOf Stack
+ */
+ function stackClear() {
+ this.__data__ = new ListCache;
+ this.size = 0;
+ }
+
+ /**
+ * Removes `key` and its value from the stack.
+ *
+ * @private
+ * @name delete
+ * @memberOf Stack
+ * @param {string} key The key of the value to remove.
+ * @returns {boolean} Returns `true` if the entry was removed, else `false`.
+ */
+ function stackDelete(key) {
+ var data = this.__data__,
+ result = data['delete'](key);
+
+ this.size = data.size;
+ return result;
+ }
+
+ /**
+ * Gets the stack value for `key`.
+ *
+ * @private
+ * @name get
+ * @memberOf Stack
+ * @param {string} key The key of the value to get.
+ * @returns {*} Returns the entry value.
+ */
+ function stackGet(key) {
+ return this.__data__.get(key);
+ }
+
+ /**
+ * Checks if a stack value for `key` exists.
+ *
+ * @private
+ * @name has
+ * @memberOf Stack
+ * @param {string} key The key of the entry to check.
+ * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.
+ */
+ function stackHas(key) {
+ return this.__data__.has(key);
+ }
+
+ /**
+ * Sets the stack `key` to `value`.
+ *
+ * @private
+ * @name set
+ * @memberOf Stack
+ * @param {string} key The key of the value to set.
+ * @param {*} value The value to set.
+ * @returns {Object} Returns the stack cache instance.
+ */
+ function stackSet(key, value) {
+ var data = this.__data__;
+ if (data instanceof ListCache) {
+ var pairs = data.__data__;
+ if (!Map || (pairs.length < LARGE_ARRAY_SIZE - 1)) {
+ pairs.push([key, value]);
+ this.size = ++data.size;
+ return this;
+ }
+ data = this.__data__ = new MapCache(pairs);
+ }
+ data.set(key, value);
+ this.size = data.size;
+ return this;
+ }
+
+ // Add methods to `Stack`.
+ Stack.prototype.clear = stackClear;
+ Stack.prototype['delete'] = stackDelete;
+ Stack.prototype.get = stackGet;
+ Stack.prototype.has = stackHas;
+ Stack.prototype.set = stackSet;
+
+ /*------------------------------------------------------------------------*/
+
+ /**
+ * Creates an array of the enumerable property names of the array-like `value`.
+ *
+ * @private
+ * @param {*} value The value to query.
+ * @param {boolean} inherited Specify returning inherited property names.
+ * @returns {Array} Returns the array of property names.
+ */
+ function arrayLikeKeys(value, inherited) {
+ var isArr = isArray(value),
+ isArg = !isArr && isArguments(value),
+ isBuff = !isArr && !isArg && isBuffer(value),
+ isType = !isArr && !isArg && !isBuff && isTypedArray(value),
+ skipIndexes = isArr || isArg || isBuff || isType,
+ result = skipIndexes ? baseTimes(value.length, String) : [],
+ length = result.length;
+
+ for (var key in value) {
+ if ((inherited || hasOwnProperty.call(value, key)) &&
+ !(skipIndexes && (
+ // Safari 9 has enumerable `arguments.length` in strict mode.
+ key == 'length' ||
+ // Node.js 0.10 has enumerable non-index properties on buffers.
+ (isBuff && (key == 'offset' || key == 'parent')) ||
+ // PhantomJS 2 has enumerable non-index properties on typed arrays.
+ (isType && (key == 'buffer' || key == 'byteLength' || key == 'byteOffset')) ||
+ // Skip index properties.
+ isIndex(key, length)
+ ))) {
+ result.push(key);
+ }
+ }
+ return result;
+ }
+
+ /**
+ * A specialized version of `_.sample` for arrays.
+ *
+ * @private
+ * @param {Array} array The array to sample.
+ * @returns {*} Returns the random element.
+ */
+ function arraySample(array) {
+ var length = array.length;
+ return length ? array[baseRandom(0, length - 1)] : undefined;
+ }
+
+ /**
+ * A specialized version of `_.sampleSize` for arrays.
+ *
+ * @private
+ * @param {Array} array The array to sample.
+ * @param {number} n The number of elements to sample.
+ * @returns {Array} Returns the random elements.
+ */
+ function arraySampleSize(array, n) {
+ return shuffleSelf(copyArray(array), baseClamp(n, 0, array.length));
+ }
+
+ /**
+ * A specialized version of `_.shuffle` for arrays.
+ *
+ * @private
+ * @param {Array} array The array to shuffle.
+ * @returns {Array} Returns the new shuffled array.
+ */
+ function arrayShuffle(array) {
+ return shuffleSelf(copyArray(array));
+ }
+
+ /**
+ * Used by `_.defaults` to customize its `_.assignIn` use.
+ *
+ * @private
+ * @param {*} objValue The destination value.
+ * @param {*} srcValue The source value.
+ * @param {string} key The key of the property to assign.
+ * @param {Object} object The parent object of `objValue`.
+ * @returns {*} Returns the value to assign.
+ */
+ function assignInDefaults(objValue, srcValue, key, object) {
+ if (objValue === undefined ||
+ (eq(objValue, objectProto[key]) && !hasOwnProperty.call(object, key))) {
+ return srcValue;
+ }
+ return objValue;
+ }
+
+ /**
+ * This function is like `assignValue` except that it doesn't assign
+ * `undefined` values.
+ *
+ * @private
+ * @param {Object} object The object to modify.
+ * @param {string} key The key of the property to assign.
+ * @param {*} value The value to assign.
+ */
+ function assignMergeValue(object, key, value) {
+ if ((value !== undefined && !eq(object[key], value)) ||
+ (value === undefined && !(key in object))) {
+ baseAssignValue(object, key, value);
+ }
+ }
+
+ /**
+ * Assigns `value` to `key` of `object` if the existing value is not equivalent
+ * using [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)
+ * for equality comparisons.
+ *
+ * @private
+ * @param {Object} object The object to modify.
+ * @param {string} key The key of the property to assign.
+ * @param {*} value The value to assign.
+ */
+ function assignValue(object, key, value) {
+ var objValue = object[key];
+ if (!(hasOwnProperty.call(object, key) && eq(objValue, value)) ||
+ (value === undefined && !(key in object))) {
+ baseAssignValue(object, key, value);
+ }
+ }
+
+ /**
+ * Gets the index at which the `key` is found in `array` of key-value pairs.
+ *
+ * @private
+ * @param {Array} array The array to inspect.
+ * @param {*} key The key to search for.
+ * @returns {number} Returns the index of the matched value, else `-1`.
+ */
+ function assocIndexOf(array, key) {
+ var length = array.length;
+ while (length--) {
+ if (eq(array[length][0], key)) {
+ return length;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Aggregates elements of `collection` on `accumulator` with keys transformed
+ * by `iteratee` and values set by `setter`.
+ *
+ * @private
+ * @param {Array|Object} collection The collection to iterate over.
+ * @param {Function} setter The function to set `accumulator` values.
+ * @param {Function} iteratee The iteratee to transform keys.
+ * @param {Object} accumulator The initial aggregated object.
+ * @returns {Function} Returns `accumulator`.
+ */
+ function baseAggregator(collection, setter, iteratee, accumulator) {
+ baseEach(collection, function(value, key, collection) {
+ setter(accumulator, value, iteratee(value), collection);
+ });
+ return accumulator;
+ }
+
+ /**
+ * The base implementation of `_.assign` without support for multiple sources
+ * or `customizer` functions.
+ *
+ * @private
+ * @param {Object} object The destination object.
+ * @param {Object} source The source object.
+ * @returns {Object} Returns `object`.
+ */
+ function baseAssign(object, source) {
+ return object && copyObject(source, keys(source), object);
+ }
+
+ /**
+ * The base implementation of `assignValue` and `assignMergeValue` without
+ * value checks.
+ *
+ * @private
+ * @param {Object} object The object to modify.
+ * @param {string} key The key of the property to assign.
+ * @param {*} value The value to assign.
+ */
+ function baseAssignValue(object, key, value) {
+ if (key == '__proto__' && defineProperty) {
+ defineProperty(object, key, {
+ 'configurable': true,
+ 'enumerable': true,
+ 'value': value,
+ 'writable': true
+ });
+ } else {
+ object[key] = value;
+ }
+ }
+
+ /**
+ * The base implementation of `_.at` without support for individual paths.
+ *
+ * @private
+ * @param {Object} object The object to iterate over.
+ * @param {string[]} paths The property paths of elements to pick.
+ * @returns {Array} Returns the picked elements.
+ */
+ function baseAt(object, paths) {
+ var index = -1,
+ isNil = object == null,
+ length = paths.length,
+ result = Array(length);
+
+ while (++index < length) {
+ result[index] = isNil ? undefined : get(object, paths[index]);
+ }
+ return result;
+ }
+
+ /**
+ * The base implementation of `_.clamp` which doesn't coerce arguments.
+ *
+ * @private
+ * @param {number} number The number to clamp.
+ * @param {number} [lower] The lower bound.
+ * @param {number} upper The upper bound.
+ * @returns {number} Returns the clamped number.
+ */
+ function baseClamp(number, lower, upper) {
+ if (number === number) {
+ if (upper !== undefined) {
+ number = number <= upper ? number : upper;
+ }
+ if (lower !== undefined) {
+ number = number >= lower ? number : lower;
+ }
+ }
+ return number;
+ }
+
+ /**
+ * The base implementation of `_.clone` and `_.cloneDeep` which tracks
+ * traversed objects.
+ *
+ * @private
+ * @param {*} value The value to clone.
+ * @param {boolean} [isDeep] Specify a deep clone.
+ * @param {boolean} [isFull] Specify a clone including symbols.
+ * @param {Function} [customizer] The function to customize cloning.
+ * @param {string} [key] The key of `value`.
+ * @param {Object} [object] The parent object of `value`.
+ * @param {Object} [stack] Tracks traversed objects and their clone counterparts.
+ * @returns {*} Returns the cloned value.
+ */
+ function baseClone(value, isDeep, isFull, customizer, key, object, stack) {
+ var result;
+ if (customizer) {
+ result = object ? customizer(value, key, object, stack) : customizer(value);
+ }
+ if (result !== undefined) {
+ return result;
+ }
+ if (!isObject(value)) {
+ return value;
+ }
+ var isArr = isArray(value);
+ if (isArr) {
+ result = initCloneArray(value);
+ if (!isDeep) {
+ return copyArray(value, result);
+ }
+ } else {
+ var tag = getTag(value),
+ isFunc = tag == funcTag || tag == genTag;
+
+ if (isBuffer(value)) {
+ return cloneBuffer(value, isDeep);
+ }
+ if (tag == objectTag || tag == argsTag || (isFunc && !object)) {
+ result = initCloneObject(isFunc ? {} : value);
+ if (!isDeep) {
+ return copySymbols(value, baseAssign(result, value));
+ }
+ } else {
+ if (!cloneableTags[tag]) {
+ return object ? value : {};
+ }
+ result = initCloneByTag(value, tag, baseClone, isDeep);
+ }
+ }
+ // Check for circular references and return its corresponding clone.
+ stack || (stack = new Stack);
+ var stacked = stack.get(value);
+ if (stacked) {
+ return stacked;
+ }
+ stack.set(value, result);
+
+ var props = isArr ? undefined : (isFull ? getAllKeys : keys)(value);
+ arrayEach(props || value, function(subValue, key) {
+ if (props) {
+ key = subValue;
+ subValue = value[key];
+ }
+ // Recursively populate clone (susceptible to call stack limits).
+ assignValue(result, key, baseClone(subValue, isDeep, isFull, customizer, key, value, stack));
+ });
+ return result;
+ }
+
+ /**
+ * The base implementation of `_.conforms` which doesn't clone `source`.
+ *
+ * @private
+ * @param {Object} source The object of property predicates to conform to.
+ * @returns {Function} Returns the new spec function.
+ */
+ function baseConforms(source) {
+ var props = keys(source);
+ return function(object) {
+ return baseConformsTo(object, source, props);
+ };
+ }
+
+ /**
+ * The base implementation of `_.conformsTo` which accepts `props` to check.
+ *
+ * @private
+ * @param {Object} object The object to inspect.
+ * @param {Object} source The object of property predicates to conform to.
+ * @returns {boolean} Returns `true` if `object` conforms, else `false`.
+ */
+ function baseConformsTo(object, source, props) {
+ var length = props.length;
+ if (object == null) {
+ return !length;
+ }
+ object = Object(object);
+ while (length--) {
+ var key = props[length],
+ predicate = source[key],
+ value = object[key];
+
+ if ((value === undefined && !(key in object)) || !predicate(value)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * The base implementation of `_.delay` and `_.defer` which accepts `args`
+ * to provide to `func`.
+ *
+ * @private
+ * @param {Function} func The function to delay.
+ * @param {number} wait The number of milliseconds to delay invocation.
+ * @param {Array} args The arguments to provide to `func`.
+ * @returns {number|Object} Returns the timer id or timeout object.
+ */
+ function baseDelay(func, wait, args) {
+ if (typeof func != 'function') {
+ throw new TypeError(FUNC_ERROR_TEXT);
+ }
+ return setTimeout(function() { func.apply(undefined, args); }, wait);
+ }
+
+ /**
+ * The base implementation of methods like `_.difference` without support
+ * for excluding multiple arrays or iteratee shorthands.
+ *
+ * @private
+ * @param {Array} array The array to inspect.
+ * @param {Array} values The values to exclude.
+ * @param {Function} [iteratee] The iteratee invoked per element.
+ * @param {Function} [comparator] The comparator invoked per element.
+ * @returns {Array} Returns the new array of filtered values.
+ */
+ function baseDifference(array, values, iteratee, comparator) {
+ var index = -1,
+ includes = arrayIncludes,
+ isCommon = true,
+ length = array.length,
+ result = [],
+ valuesLength = values.length;
+
+ if (!length) {
+ return result;
+ }
+ if (iteratee) {
+ values = arrayMap(values, baseUnary(iteratee));
+ }
+ if (comparator) {
+ includes = arrayIncludesWith;
+ isCommon = false;
+ }
+ else if (values.length >= LARGE_ARRAY_SIZE) {
+ includes = cacheHas;
+ isCommon = false;
+ values = new SetCache(values);
+ }
+ outer:
+ while (++index < length) {
+ var value = array[index],
+ computed = iteratee ? iteratee(value) : value;
+
+ value = (comparator || value !== 0) ? value : 0;
+ if (isCommon && computed === computed) {
+ var valuesIndex = valuesLength;
+ while (valuesIndex--) {
+ if (values[valuesIndex] === computed) {
+ continue outer;
+ }
+ }
+ result.push(value);
+ }
+ else if (!includes(values, computed, comparator)) {
+ result.push(value);
+ }
+ }
+ return result;
+ }
+
+ /**
+ * The base implementation of `_.forEach` without support for iteratee shorthands.
+ *
+ * @private
+ * @param {Array|Object} collection The collection to iterate over.
+ * @param {Function} iteratee The function invoked per iteration.
+ * @returns {Array|Object} Returns `collection`.
+ */
+ var baseEach = createBaseEach(baseForOwn);
+
+ /**
+ * The base implementation of `_.forEachRight` without support for iteratee shorthands.
+ *
+ * @private
+ * @param {Array|Object} collection The collection to iterate over.
+ * @param {Function} iteratee The function invoked per iteration.
+ * @returns {Array|Object} Returns `collection`.
+ */
+ var baseEachRight = createBaseEach(baseForOwnRight, true);
+
+ /**
+ * The base implementation of `_.every` without support for iteratee shorthands.
+ *
+ * @private
+ * @param {Array|Object} collection The collection to iterate over.
+ * @param {Function} predicate The function invoked per iteration.
+ * @returns {boolean} Returns `true` if all elements pass the predicate check,
+ * else `false`
+ */
+ function baseEvery(collection, predicate) {
+ var result = true;
+ baseEach(collection, function(value, index, collection) {
+ result = !!predicate(value, index, collection);
+ return result;
+ });
+ return result;
+ }
+
+ /**
+ * The base implementation of methods like `_.max` and `_.min` which accepts a
+ * `comparator` to determine the extremum value.
+ *
+ * @private
+ * @param {Array} array The array to iterate over.
+ * @param {Function} iteratee The iteratee invoked per iteration.
+ * @param {Function} comparator The comparator used to compare values.
+ * @returns {*} Returns the extremum value.
+ */
+ function baseExtremum(array, iteratee, comparator) {
+ var index = -1,
+ length = array.length;
+
+ while (++index < length) {
+ var value = array[index],
+ current = iteratee(value);
+
+ if (current != null && (computed === undefined
+ ? (current === current && !isSymbol(current))
+ : comparator(current, computed)
+ )) {
+ var computed = current,
+ result = value;
+ }
+ }
+ return result;
+ }
+
+ /**
+ * The base implementation of `_.fill` without an iteratee call guard.
+ *
+ * @private
+ * @param {Array} array The array to fill.
+ * @param {*} value The value to fill `array` with.
+ * @param {number} [start=0] The start position.
+ * @param {number} [end=array.length] The end position.
+ * @returns {Array} Returns `array`.
+ */
+ function baseFill(array, value, start, end) {
+ var length = array.length;
+
+ start = toInteger(start);
+ if (start < 0) {
+ start = -start > length ? 0 : (length + start);
+ }
+ end = (end === undefined || end > length) ? length : toInteger(end);
+ if (end < 0) {
+ end += length;
+ }
+ end = start > end ? 0 : toLength(end);
+ while (start < end) {
+ array[start++] = value;
+ }
+ return array;
+ }
+
+ /**
+ * The base implementation of `_.filter` without support for iteratee shorthands.
+ *
+ * @private
+ * @param {Array|Object} collection The collection to iterate over.
+ * @param {Function} predicate The function invoked per iteration.
+ * @returns {Array} Returns the new filtered array.
+ */
+ function baseFilter(collection, predicate) {
+ var result = [];
+ baseEach(collection, function(value, index, collection) {
+ if (predicate(value, index, collection)) {
+ result.push(value);
+ }
+ });
+ return result;
+ }
+
+ /**
+ * The base implementation of `_.flatten` with support for restricting flattening.
+ *
+ * @private
+ * @param {Array} array The array to flatten.
+ * @param {number} depth The maximum recursion depth.
+ * @param {boolean} [predicate=isFlattenable] The function invoked per iteration.
+ * @param {boolean} [isStrict] Restrict to values that pass `predicate` checks.
+ * @param {Array} [result=[]] The initial result value.
+ * @returns {Array} Returns the new flattened array.
+ */
+ function baseFlatten(array, depth, predicate, isStrict, result) {
+ var index = -1,
+ length = array.length;
+
+ predicate || (predicate = isFlattenable);
+ result || (result = []);
+
+ while (++index < length) {
+ var value = array[index];
+ if (depth > 0 && predicate(value)) {
+ if (depth > 1) {
+ // Recursively flatten arrays (susceptible to call stack limits).
+ baseFlatten(value, depth - 1, predicate, isStrict, result);
+ } else {
+ arrayPush(result, value);
+ }
+ } else if (!isStrict) {
+ result[result.length] = value;
+ }
+ }
+ return result;
+ }
+
+ /**
+ * The base implementation of `baseForOwn` which iterates over `object`
+ * properties returned by `keysFunc` and invokes `iteratee` for each property.
+ * Iteratee functions may exit iteration early by explicitly returning `false`.
+ *
+ * @private
+ * @param {Object} object The object to iterate over.
+ * @param {Function} iteratee The function invoked per iteration.
+ * @param {Function} keysFunc The function to get the keys of `object`.
+ * @returns {Object} Returns `object`.
+ */
+ var baseFor = createBaseFor();
+
+ /**
+ * This function is like `baseFor` except that it iterates over properties
+ * in the opposite order.
+ *
+ * @private
+ * @param {Object} object The object to iterate over.
+ * @param {Function} iteratee The function invoked per iteration.
+ * @param {Function} keysFunc The function to get the keys of `object`.
+ * @returns {Object} Returns `object`.
+ */
+ var baseForRight = createBaseFor(true);
+
+ /**
+ * The base implementation of `_.forOwn` without support for iteratee shorthands.
+ *
+ * @private
+ * @param {Object} object The object to iterate over.
+ * @param {Function} iteratee The function invoked per iteration.
+ * @returns {Object} Returns `object`.
+ */
+ function baseForOwn(object, iteratee) {
+ return object && baseFor(object, iteratee, keys);
+ }
+
+ /**
+ * The base implementation of `_.forOwnRight` without support for iteratee shorthands.
+ *
+ * @private
+ * @param {Object} object The object to iterate over.
+ * @param {Function} iteratee The function invoked per iteration.
+ * @returns {Object} Returns `object`.
+ */
+ function baseForOwnRight(object, iteratee) {
+ return object && baseForRight(object, iteratee, keys);
+ }
+
+ /**
+ * The base implementation of `_.functions` which creates an array of
+ * `object` function property names filtered from `props`.
+ *
+ * @private
+ * @param {Object} object The object to inspect.
+ * @param {Array} props The property names to filter.
+ * @returns {Array} Returns the function names.
+ */
+ function baseFunctions(object, props) {
+ return arrayFilter(props, function(key) {
+ return isFunction(object[key]);
+ });
+ }
+
+ /**
+ * The base implementation of `_.get` without support for default values.
+ *
+ * @private
+ * @param {Object} object The object to query.
+ * @param {Array|string} path The path of the property to get.
+ * @returns {*} Returns the resolved value.
+ */
+ function baseGet(object, path) {
+ path = isKey(path, object) ? [path] : castPath(path);
+
+ var index = 0,
+ length = path.length;
+
+ while (object != null && index < length) {
+ object = object[toKey(path[index++])];
+ }
+ return (index && index == length) ? object : undefined;
+ }
+
+ /**
+ * The base implementation of `getAllKeys` and `getAllKeysIn` which uses
+ * `keysFunc` and `symbolsFunc` to get the enumerable property names and
+ * symbols of `object`.
+ *
+ * @private
+ * @param {Object} object The object to query.
+ * @param {Function} keysFunc The function to get the keys of `object`.
+ * @param {Function} symbolsFunc The function to get the symbols of `object`.
+ * @returns {Array} Returns the array of property names and symbols.
+ */
+ function baseGetAllKeys(object, keysFunc, symbolsFunc) {
+ var result = keysFunc(object);
+ return isArray(object) ? result : arrayPush(result, symbolsFunc(object));
+ }
+
+ /**
+ * The base implementation of `getTag`.
+ *
+ * @private
+ * @param {*} value The value to query.
+ * @returns {string} Returns the `toStringTag`.
+ */
+ function baseGetTag(value) {
+ return objectToString.call(value);
+ }
+
+ /**
+ * The base implementation of `_.gt` which doesn't coerce arguments.
+ *
+ * @private
+ * @param {*} value The value to compare.
+ * @param {*} other The other value to compare.
+ * @returns {boolean} Returns `true` if `value` is greater than `other`,
+ * else `false`.
+ */
+ function baseGt(value, other) {
+ return value > other;
+ }
+
+ /**
+ * The base implementation of `_.has` without support for deep paths.
+ *
+ * @private
+ * @param {Object} [object] The object to query.
+ * @param {Array|string} key The key to check.
+ * @returns {boolean} Returns `true` if `key` exists, else `false`.
+ */
+ function baseHas(object, key) {
+ return object != null && hasOwnProperty.call(object, key);
+ }
+
+ /**
+ * The base implementation of `_.hasIn` without support for deep paths.
+ *
+ * @private
+ * @param {Object} [object] The object to query.
+ * @param {Array|string} key The key to check.
+ * @returns {boolean} Returns `true` if `key` exists, else `false`.
+ */
+ function baseHasIn(object, key) {
+ return object != null && key in Object(object);
+ }
+
+ /**
+ * The base implementation of `_.inRange` which doesn't coerce arguments.
+ *
+ * @private
+ * @param {number} number The number to check.
+ * @param {number} start The start of the range.
+ * @param {number} end The end of the range.
+ * @returns {boolean} Returns `true` if `number` is in the range, else `false`.
+ */
+ function baseInRange(number, start, end) {
+ return number >= nativeMin(start, end) && number < nativeMax(start, end);
+ }
+
+ /**
+ * The base implementation of methods like `_.intersection`, without support
+ * for iteratee shorthands, that accepts an array of arrays to inspect.
+ *
+ * @private
+ * @param {Array} arrays The arrays to inspect.
+ * @param {Function} [iteratee] The iteratee invoked per element.
+ * @param {Function} [comparator] The comparator invoked per element.
+ * @returns {Array} Returns the new array of shared values.
+ */
+ function baseIntersection(arrays, iteratee, comparator) {
+ var includes = comparator ? arrayIncludesWith : arrayIncludes,
+ length = arrays[0].length,
+ othLength = arrays.length,
+ othIndex = othLength,
+ caches = Array(othLength),
+ maxLength = Infinity,
+ result = [];
+
+ while (othIndex--) {
+ var array = arrays[othIndex];
+ if (othIndex && iteratee) {
+ array = arrayMap(array, baseUnary(iteratee));
+ }
+ maxLength = nativeMin(array.length, maxLength);
+ caches[othIndex] = !comparator && (iteratee || (length >= 120 && array.length >= 120))
+ ? new SetCache(othIndex && array)
+ : undefined;
+ }
+ array = arrays[0];
+
+ var index = -1,
+ seen = caches[0];
+
+ outer:
+ while (++index < length && result.length < maxLength) {
+ var value = array[index],
+ computed = iteratee ? iteratee(value) : value;
+
+ value = (comparator || value !== 0) ? value : 0;
+ if (!(seen
+ ? cacheHas(seen, computed)
+ : includes(result, computed, comparator)
+ )) {
+ othIndex = othLength;
+ while (--othIndex) {
+ var cache = caches[othIndex];
+ if (!(cache
+ ? cacheHas(cache, computed)
+ : includes(arrays[othIndex], computed, comparator))
+ ) {
+ continue outer;
+ }
+ }
+ if (seen) {
+ seen.push(computed);
+ }
+ result.push(value);
+ }
+ }
+ return result;
+ }
+
+ /**
+ * The base implementation of `_.invert` and `_.invertBy` which inverts
+ * `object` with values transformed by `iteratee` and set by `setter`.
+ *
+ * @private
+ * @param {Object} object The object to iterate over.
+ * @param {Function} setter The function to set `accumulator` values.
+ * @param {Function} iteratee The iteratee to transform values.
+ * @param {Object} accumulator The initial inverted object.
+ * @returns {Function} Returns `accumulator`.
+ */
+ function baseInverter(object, setter, iteratee, accumulator) {
+ baseForOwn(object, function(value, key, object) {
+ setter(accumulator, iteratee(value), key, object);
+ });
+ return accumulator;
+ }
+
+ /**
+ * The base implementation of `_.invoke` without support for individual
+ * method arguments.
+ *
+ * @private
+ * @param {Object} object The object to query.
+ * @param {Array|string} path The path of the method to invoke.
+ * @param {Array} args The arguments to invoke the method with.
+ * @returns {*} Returns the result of the invoked method.
+ */
+ function baseInvoke(object, path, args) {
+ if (!isKey(path, object)) {
+ path = castPath(path);
+ object = parent(object, path);
+ path = last(path);
+ }
+ var func = object == null ? object : object[toKey(path)];
+ return func == null ? undefined : apply(func, object, args);
+ }
+
+ /**
+ * The base implementation of `_.isArguments`.
+ *
+ * @private
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is an `arguments` object,
+ */
+ function baseIsArguments(value) {
+ return isObjectLike(value) && objectToString.call(value) == argsTag;
+ }
+
+ /**
+ * The base implementation of `_.isArrayBuffer` without Node.js optimizations.
+ *
+ * @private
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is an array buffer, else `false`.
+ */
+ function baseIsArrayBuffer(value) {
+ return isObjectLike(value) && objectToString.call(value) == arrayBufferTag;
+ }
+
+ /**
+ * The base implementation of `_.isDate` without Node.js optimizations.
+ *
+ * @private
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is a date object, else `false`.
+ */
+ function baseIsDate(value) {
+ return isObjectLike(value) && objectToString.call(value) == dateTag;
+ }
+
+ /**
+ * The base implementation of `_.isEqual` which supports partial comparisons
+ * and tracks traversed objects.
+ *
+ * @private
+ * @param {*} value The value to compare.
+ * @param {*} other The other value to compare.
+ * @param {Function} [customizer] The function to customize comparisons.
+ * @param {boolean} [bitmask] The bitmask of comparison flags.
+ * The bitmask may be composed of the following flags:
+ * 1 - Unordered comparison
+ * 2 - Partial comparison
+ * @param {Object} [stack] Tracks traversed `value` and `other` objects.
+ * @returns {boolean} Returns `true` if the values are equivalent, else `false`.
+ */
+ function baseIsEqual(value, other, customizer, bitmask, stack) {
+ if (value === other) {
+ return true;
+ }
+ if (value == null || other == null || (!isObject(value) && !isObjectLike(other))) {
+ return value !== value && other !== other;
+ }
+ return baseIsEqualDeep(value, other, baseIsEqual, customizer, bitmask, stack);
+ }
+
+ /**
+ * A specialized version of `baseIsEqual` for arrays and objects which performs
+ * deep comparisons and tracks traversed objects enabling objects with circular
+ * references to be compared.
+ *
+ * @private
+ * @param {Object} object The object to compare.
+ * @param {Object} other The other object to compare.
+ * @param {Function} equalFunc The function to determine equivalents of values.
+ * @param {Function} [customizer] The function to customize comparisons.
+ * @param {number} [bitmask] The bitmask of comparison flags. See `baseIsEqual`
+ * for more details.
+ * @param {Object} [stack] Tracks traversed `object` and `other` objects.
+ * @returns {boolean} Returns `true` if the objects are equivalent, else `false`.
+ */
+ function baseIsEqualDeep(object, other, equalFunc, customizer, bitmask, stack) {
+ var objIsArr = isArray(object),
+ othIsArr = isArray(other),
+ objTag = arrayTag,
+ othTag = arrayTag;
+
+ if (!objIsArr) {
+ objTag = getTag(object);
+ objTag = objTag == argsTag ? objectTag : objTag;
+ }
+ if (!othIsArr) {
+ othTag = getTag(other);
+ othTag = othTag == argsTag ? objectTag : othTag;
+ }
+ var objIsObj = objTag == objectTag,
+ othIsObj = othTag == objectTag,
+ isSameTag = objTag == othTag;
+
+ if (isSameTag && isBuffer(object)) {
+ if (!isBuffer(other)) {
+ return false;
+ }
+ objIsArr = true;
+ objIsObj = false;
+ }
+ if (isSameTag && !objIsObj) {
+ stack || (stack = new Stack);
+ return (objIsArr || isTypedArray(object))
+ ? equalArrays(object, other, equalFunc, customizer, bitmask, stack)
+ : equalByTag(object, other, objTag, equalFunc, customizer, bitmask, stack);
+ }
+ if (!(bitmask & PARTIAL_COMPARE_FLAG)) {
+ var objIsWrapped = objIsObj && hasOwnProperty.call(object, '__wrapped__'),
+ othIsWrapped = othIsObj && hasOwnProperty.call(other, '__wrapped__');
+
+ if (objIsWrapped || othIsWrapped) {
+ var objUnwrapped = objIsWrapped ? object.value() : object,
+ othUnwrapped = othIsWrapped ? other.value() : other;
+
+ stack || (stack = new Stack);
+ return equalFunc(objUnwrapped, othUnwrapped, customizer, bitmask, stack);
+ }
+ }
+ if (!isSameTag) {
+ return false;
+ }
+ stack || (stack = new Stack);
+ return equalObjects(object, other, equalFunc, customizer, bitmask, stack);
+ }
+
+ /**
+ * The base implementation of `_.isMap` without Node.js optimizations.
+ *
+ * @private
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is a map, else `false`.
+ */
+ function baseIsMap(value) {
+ return isObjectLike(value) && getTag(value) == mapTag;
+ }
+
+ /**
+ * The base implementation of `_.isMatch` without support for iteratee shorthands.
+ *
+ * @private
+ * @param {Object} object The object to inspect.
+ * @param {Object} source The object of property values to match.
+ * @param {Array} matchData The property names, values, and compare flags to match.
+ * @param {Function} [customizer] The function to customize comparisons.
+ * @returns {boolean} Returns `true` if `object` is a match, else `false`.
+ */
+ function baseIsMatch(object, source, matchData, customizer) {
+ var index = matchData.length,
+ length = index,
+ noCustomizer = !customizer;
+
+ if (object == null) {
+ return !length;
+ }
+ object = Object(object);
+ while (index--) {
+ var data = matchData[index];
+ if ((noCustomizer && data[2])
+ ? data[1] !== object[data[0]]
+ : !(data[0] in object)
+ ) {
+ return false;
+ }
+ }
+ while (++index < length) {
+ data = matchData[index];
+ var key = data[0],
+ objValue = object[key],
+ srcValue = data[1];
+
+ if (noCustomizer && data[2]) {
+ if (objValue === undefined && !(key in object)) {
+ return false;
+ }
+ } else {
+ var stack = new Stack;
+ if (customizer) {
+ var result = customizer(objValue, srcValue, key, object, source, stack);
+ }
+ if (!(result === undefined
+ ? baseIsEqual(srcValue, objValue, customizer, UNORDERED_COMPARE_FLAG | PARTIAL_COMPARE_FLAG, stack)
+ : result
+ )) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
+ * The base implementation of `_.isNative` without bad shim checks.
+ *
+ * @private
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is a native function,
+ * else `false`.
+ */
+ function baseIsNative(value) {
+ if (!isObject(value) || isMasked(value)) {
+ return false;
+ }
+ var pattern = isFunction(value) ? reIsNative : reIsHostCtor;
+ return pattern.test(toSource(value));
+ }
+
+ /**
+ * The base implementation of `_.isRegExp` without Node.js optimizations.
+ *
+ * @private
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is a regexp, else `false`.
+ */
+ function baseIsRegExp(value) {
+ return isObject(value) && objectToString.call(value) == regexpTag;
+ }
+
+ /**
+ * The base implementation of `_.isSet` without Node.js optimizations.
+ *
+ * @private
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is a set, else `false`.
+ */
+ function baseIsSet(value) {
+ return isObjectLike(value) && getTag(value) == setTag;
+ }
+
+ /**
+ * The base implementation of `_.isTypedArray` without Node.js optimizations.
+ *
+ * @private
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is a typed array, else `false`.
+ */
+ function baseIsTypedArray(value) {
+ return isObjectLike(value) &&
+ isLength(value.length) && !!typedArrayTags[objectToString.call(value)];
+ }
+
+ /**
+ * The base implementation of `_.iteratee`.
+ *
+ * @private
+ * @param {*} [value=_.identity] The value to convert to an iteratee.
+ * @returns {Function} Returns the iteratee.
+ */
+ function baseIteratee(value) {
+ // Don't store the `typeof` result in a variable to avoid a JIT bug in Safari 9.
+ // See https://bugs.webkit.org/show_bug.cgi?id=156034 for more details.
+ if (typeof value == 'function') {
+ return value;
+ }
+ if (value == null) {
+ return identity;
+ }
+ if (typeof value == 'object') {
+ return isArray(value)
+ ? baseMatchesProperty(value[0], value[1])
+ : baseMatches(value);
+ }
+ return property(value);
+ }
+
+ /**
+ * The base implementation of `_.keys` which doesn't treat sparse arrays as dense.
+ *
+ * @private
+ * @param {Object} object The object to query.
+ * @returns {Array} Returns the array of property names.
+ */
+ function baseKeys(object) {
+ if (!isPrototype(object)) {
+ return nativeKeys(object);
+ }
+ var result = [];
+ for (var key in Object(object)) {
+ if (hasOwnProperty.call(object, key) && key != 'constructor') {
+ result.push(key);
+ }
+ }
+ return result;
+ }
+
+ /**
+ * The base implementation of `_.keysIn` which doesn't treat sparse arrays as dense.
+ *
+ * @private
+ * @param {Object} object The object to query.
+ * @returns {Array} Returns the array of property names.
+ */
+ function baseKeysIn(object) {
+ if (!isObject(object)) {
+ return nativeKeysIn(object);
+ }
+ var isProto = isPrototype(object),
+ result = [];
+
+ for (var key in object) {
+ if (!(key == 'constructor' && (isProto || !hasOwnProperty.call(object, key)))) {
+ result.push(key);
+ }
+ }
+ return result;
+ }
+
+ /**
+ * The base implementation of `_.lt` which doesn't coerce arguments.
+ *
+ * @private
+ * @param {*} value The value to compare.
+ * @param {*} other The other value to compare.
+ * @returns {boolean} Returns `true` if `value` is less than `other`,
+ * else `false`.
+ */
+ function baseLt(value, other) {
+ return value < other;
+ }
+
+ /**
+ * The base implementation of `_.map` without support for iteratee shorthands.
+ *
+ * @private
+ * @param {Array|Object} collection The collection to iterate over.
+ * @param {Function} iteratee The function invoked per iteration.
+ * @returns {Array} Returns the new mapped array.
+ */
+ function baseMap(collection, iteratee) {
+ var index = -1,
+ result = isArrayLike(collection) ? Array(collection.length) : [];
+
+ baseEach(collection, function(value, key, collection) {
+ result[++index] = iteratee(value, key, collection);
+ });
+ return result;
+ }
+
+ /**
+ * The base implementation of `_.matches` which doesn't clone `source`.
+ *
+ * @private
+ * @param {Object} source The object of property values to match.
+ * @returns {Function} Returns the new spec function.
+ */
+ function baseMatches(source) {
+ var matchData = getMatchData(source);
+ if (matchData.length == 1 && matchData[0][2]) {
+ return matchesStrictComparable(matchData[0][0], matchData[0][1]);
+ }
+ return function(object) {
+ return object === source || baseIsMatch(object, source, matchData);
+ };
+ }
+
+ /**
+ * The base implementation of `_.matchesProperty` which doesn't clone `srcValue`.
+ *
+ * @private
+ * @param {string} path The path of the property to get.
+ * @param {*} srcValue The value to match.
+ * @returns {Function} Returns the new spec function.
+ */
+ function baseMatchesProperty(path, srcValue) {
+ if (isKey(path) && isStrictComparable(srcValue)) {
+ return matchesStrictComparable(toKey(path), srcValue);
+ }
+ return function(object) {
+ var objValue = get(object, path);
+ return (objValue === undefined && objValue === srcValue)
+ ? hasIn(object, path)
+ : baseIsEqual(srcValue, objValue, undefined, UNORDERED_COMPARE_FLAG | PARTIAL_COMPARE_FLAG);
+ };
+ }
+
+ /**
+ * The base implementation of `_.merge` without support for multiple sources.
+ *
+ * @private
+ * @param {Object} object The destination object.
+ * @param {Object} source The source object.
+ * @param {number} srcIndex The index of `source`.
+ * @param {Function} [customizer] The function to customize merged values.
+ * @param {Object} [stack] Tracks traversed source values and their merged
+ * counterparts.
+ */
+ function baseMerge(object, source, srcIndex, customizer, stack) {
+ if (object === source) {
+ return;
+ }
+ baseFor(source, function(srcValue, key) {
+ if (isObject(srcValue)) {
+ stack || (stack = new Stack);
+ baseMergeDeep(object, source, key, srcIndex, baseMerge, customizer, stack);
+ }
+ else {
+ var newValue = customizer
+ ? customizer(object[key], srcValue, (key + ''), object, source, stack)
+ : undefined;
+
+ if (newValue === undefined) {
+ newValue = srcValue;
+ }
+ assignMergeValue(object, key, newValue);
+ }
+ }, keysIn);
+ }
+
+ /**
+ * A specialized version of `baseMerge` for arrays and objects which performs
+ * deep merges and tracks traversed objects enabling objects with circular
+ * references to be merged.
+ *
+ * @private
+ * @param {Object} object The destination object.
+ * @param {Object} source The source object.
+ * @param {string} key The key of the value to merge.
+ * @param {number} srcIndex The index of `source`.
+ * @param {Function} mergeFunc The function to merge values.
+ * @param {Function} [customizer] The function to customize assigned values.
+ * @param {Object} [stack] Tracks traversed source values and their merged
+ * counterparts.
+ */
+ function baseMergeDeep(object, source, key, srcIndex, mergeFunc, customizer, stack) {
+ var objValue = object[key],
+ srcValue = source[key],
+ stacked = stack.get(srcValue);
+
+ if (stacked) {
+ assignMergeValue(object, key, stacked);
+ return;
+ }
+ var newValue = customizer
+ ? customizer(objValue, srcValue, (key + ''), object, source, stack)
+ : undefined;
+
+ var isCommon = newValue === undefined;
+
+ if (isCommon) {
+ var isArr = isArray(srcValue),
+ isBuff = !isArr && isBuffer(srcValue),
+ isTyped = !isArr && !isBuff && isTypedArray(srcValue);
+
+ newValue = srcValue;
+ if (isArr || isBuff || isTyped) {
+ if (isArray(objValue)) {
+ newValue = objValue;
+ }
+ else if (isArrayLikeObject(objValue)) {
+ newValue = copyArray(objValue);
+ }
+ else if (isBuff) {
+ isCommon = false;
+ newValue = cloneBuffer(srcValue, true);
+ }
+ else if (isTyped) {
+ isCommon = false;
+ newValue = cloneTypedArray(srcValue, true);
+ }
+ else {
+ newValue = [];
+ }
+ }
+ else if (isPlainObject(srcValue) || isArguments(srcValue)) {
+ newValue = objValue;
+ if (isArguments(objValue)) {
+ newValue = toPlainObject(objValue);
+ }
+ else if (!isObject(objValue) || (srcIndex && isFunction(objValue))) {
+ newValue = initCloneObject(srcValue);
+ }
+ }
+ else {
+ isCommon = false;
+ }
+ }
+ if (isCommon) {
+ // Recursively merge objects and arrays (susceptible to call stack limits).
+ stack.set(srcValue, newValue);
+ mergeFunc(newValue, srcValue, srcIndex, customizer, stack);
+ stack['delete'](srcValue);
+ }
+ assignMergeValue(object, key, newValue);
+ }
+
+ /**
+ * The base implementation of `_.nth` which doesn't coerce arguments.
+ *
+ * @private
+ * @param {Array} array The array to query.
+ * @param {number} n The index of the element to return.
+ * @returns {*} Returns the nth element of `array`.
+ */
+ function baseNth(array, n) {
+ var length = array.length;
+ if (!length) {
+ return;
+ }
+ n += n < 0 ? length : 0;
+ return isIndex(n, length) ? array[n] : undefined;
+ }
+
+ /**
+ * The base implementation of `_.orderBy` without param guards.
+ *
+ * @private
+ * @param {Array|Object} collection The collection to iterate over.
+ * @param {Function[]|Object[]|string[]} iteratees The iteratees to sort by.
+ * @param {string[]} orders The sort orders of `iteratees`.
+ * @returns {Array} Returns the new sorted array.
+ */
+ function baseOrderBy(collection, iteratees, orders) {
+ var index = -1;
+ iteratees = arrayMap(iteratees.length ? iteratees : [identity], baseUnary(getIteratee()));
+
+ var result = baseMap(collection, function(value, key, collection) {
+ var criteria = arrayMap(iteratees, function(iteratee) {
+ return iteratee(value);
+ });
+ return { 'criteria': criteria, 'index': ++index, 'value': value };
+ });
+
+ return baseSortBy(result, function(object, other) {
+ return compareMultiple(object, other, orders);
+ });
+ }
+
+ /**
+ * The base implementation of `_.pick` without support for individual
+ * property identifiers.
+ *
+ * @private
+ * @param {Object} object The source object.
+ * @param {string[]} props The property identifiers to pick.
+ * @returns {Object} Returns the new object.
+ */
+ function basePick(object, props) {
+ object = Object(object);
+ return basePickBy(object, props, function(value, key) {
+ return key in object;
+ });
+ }
+
+ /**
+ * The base implementation of `_.pickBy` without support for iteratee shorthands.
+ *
+ * @private
+ * @param {Object} object The source object.
+ * @param {string[]} props The property identifiers to pick from.
+ * @param {Function} predicate The function invoked per property.
+ * @returns {Object} Returns the new object.
+ */
+ function basePickBy(object, props, predicate) {
+ var index = -1,
+ length = props.length,
+ result = {};
+
+ while (++index < length) {
+ var key = props[index],
+ value = object[key];
+
+ if (predicate(value, key)) {
+ baseAssignValue(result, key, value);
+ }
+ }
+ return result;
+ }
+
+ /**
+ * A specialized version of `baseProperty` which supports deep paths.
+ *
+ * @private
+ * @param {Array|string} path The path of the property to get.
+ * @returns {Function} Returns the new accessor function.
+ */
+ function basePropertyDeep(path) {
+ return function(object) {
+ return baseGet(object, path);
+ };
+ }
+
+ /**
+ * The base implementation of `_.pullAllBy` without support for iteratee
+ * shorthands.
+ *
+ * @private
+ * @param {Array} array The array to modify.
+ * @param {Array} values The values to remove.
+ * @param {Function} [iteratee] The iteratee invoked per element.
+ * @param {Function} [comparator] The comparator invoked per element.
+ * @returns {Array} Returns `array`.
+ */
+ function basePullAll(array, values, iteratee, comparator) {
+ var indexOf = comparator ? baseIndexOfWith : baseIndexOf,
+ index = -1,
+ length = values.length,
+ seen = array;
+
+ if (array === values) {
+ values = copyArray(values);
+ }
+ if (iteratee) {
+ seen = arrayMap(array, baseUnary(iteratee));
+ }
+ while (++index < length) {
+ var fromIndex = 0,
+ value = values[index],
+ computed = iteratee ? iteratee(value) : value;
+
+ while ((fromIndex = indexOf(seen, computed, fromIndex, comparator)) > -1) {
+ if (seen !== array) {
+ splice.call(seen, fromIndex, 1);
+ }
+ splice.call(array, fromIndex, 1);
+ }
+ }
+ return array;
+ }
+
+ /**
+ * The base implementation of `_.pullAt` without support for individual
+ * indexes or capturing the removed elements.
+ *
+ * @private
+ * @param {Array} array The array to modify.
+ * @param {number[]} indexes The indexes of elements to remove.
+ * @returns {Array} Returns `array`.
+ */
+ function basePullAt(array, indexes) {
+ var length = array ? indexes.length : 0,
+ lastIndex = length - 1;
+
+ while (length--) {
+ var index = indexes[length];
+ if (length == lastIndex || index !== previous) {
+ var previous = index;
+ if (isIndex(index)) {
+ splice.call(array, index, 1);
+ }
+ else if (!isKey(index, array)) {
+ var path = castPath(index),
+ object = parent(array, path);
+
+ if (object != null) {
+ delete object[toKey(last(path))];
+ }
+ }
+ else {
+ delete array[toKey(index)];
+ }
+ }
+ }
+ return array;
+ }
+
+ /**
+ * The base implementation of `_.random` without support for returning
+ * floating-point numbers.
+ *
+ * @private
+ * @param {number} lower The lower bound.
+ * @param {number} upper The upper bound.
+ * @returns {number} Returns the random number.
+ */
+ function baseRandom(lower, upper) {
+ return lower + nativeFloor(nativeRandom() * (upper - lower + 1));
+ }
+
+ /**
+ * The base implementation of `_.range` and `_.rangeRight` which doesn't
+ * coerce arguments.
+ *
+ * @private
+ * @param {number} start The start of the range.
+ * @param {number} end The end of the range.
+ * @param {number} step The value to increment or decrement by.
+ * @param {boolean} [fromRight] Specify iterating from right to left.
+ * @returns {Array} Returns the range of numbers.
+ */
+ function baseRange(start, end, step, fromRight) {
+ var index = -1,
+ length = nativeMax(nativeCeil((end - start) / (step || 1)), 0),
+ result = Array(length);
+
+ while (length--) {
+ result[fromRight ? length : ++index] = start;
+ start += step;
+ }
+ return result;
+ }
+
+ /**
+ * The base implementation of `_.repeat` which doesn't coerce arguments.
+ *
+ * @private
+ * @param {string} string The string to repeat.
+ * @param {number} n The number of times to repeat the string.
+ * @returns {string} Returns the repeated string.
+ */
+ function baseRepeat(string, n) {
+ var result = '';
+ if (!string || n < 1 || n > MAX_SAFE_INTEGER) {
+ return result;
+ }
+ // Leverage the exponentiation by squaring algorithm for a faster repeat.
+ // See https://en.wikipedia.org/wiki/Exponentiation_by_squaring for more details.
+ do {
+ if (n % 2) {
+ result += string;
+ }
+ n = nativeFloor(n / 2);
+ if (n) {
+ string += string;
+ }
+ } while (n);
+
+ return result;
+ }
+
+ /**
+ * The base implementation of `_.rest` which doesn't validate or coerce arguments.
+ *
+ * @private
+ * @param {Function} func The function to apply a rest parameter to.
+ * @param {number} [start=func.length-1] The start position of the rest parameter.
+ * @returns {Function} Returns the new function.
+ */
+ function baseRest(func, start) {
+ return setToString(overRest(func, start, identity), func + '');
+ }
+
+ /**
+ * The base implementation of `_.sample`.
+ *
+ * @private
+ * @param {Array|Object} collection The collection to sample.
+ * @returns {*} Returns the random element.
+ */
+ function baseSample(collection) {
+ return arraySample(values(collection));
+ }
+
+ /**
+ * The base implementation of `_.sampleSize` without param guards.
+ *
+ * @private
+ * @param {Array|Object} collection The collection to sample.
+ * @param {number} n The number of elements to sample.
+ * @returns {Array} Returns the random elements.
+ */
+ function baseSampleSize(collection, n) {
+ var array = values(collection);
+ return shuffleSelf(array, baseClamp(n, 0, array.length));
+ }
+
+ /**
+ * The base implementation of `_.set`.
+ *
+ * @private
+ * @param {Object} object The object to modify.
+ * @param {Array|string} path The path of the property to set.
+ * @param {*} value The value to set.
+ * @param {Function} [customizer] The function to customize path creation.
+ * @returns {Object} Returns `object`.
+ */
+ function baseSet(object, path, value, customizer) {
+ if (!isObject(object)) {
+ return object;
+ }
+ path = isKey(path, object) ? [path] : castPath(path);
+
+ var index = -1,
+ length = path.length,
+ lastIndex = length - 1,
+ nested = object;
+
+ while (nested != null && ++index < length) {
+ var key = toKey(path[index]),
+ newValue = value;
+
+ if (index != lastIndex) {
+ var objValue = nested[key];
+ newValue = customizer ? customizer(objValue, key, nested) : undefined;
+ if (newValue === undefined) {
+ newValue = isObject(objValue)
+ ? objValue
+ : (isIndex(path[index + 1]) ? [] : {});
+ }
+ }
+ assignValue(nested, key, newValue);
+ nested = nested[key];
+ }
+ return object;
+ }
+
+ /**
+ * The base implementation of `setData` without support for hot loop shorting.
+ *
+ * @private
+ * @param {Function} func The function to associate metadata with.
+ * @param {*} data The metadata.
+ * @returns {Function} Returns `func`.
+ */
+ var baseSetData = !metaMap ? identity : function(func, data) {
+ metaMap.set(func, data);
+ return func;
+ };
+
+ /**
+ * The base implementation of `setToString` without support for hot loop shorting.
+ *
+ * @private
+ * @param {Function} func The function to modify.
+ * @param {Function} string The `toString` result.
+ * @returns {Function} Returns `func`.
+ */
+ var baseSetToString = !defineProperty ? identity : function(func, string) {
+ return defineProperty(func, 'toString', {
+ 'configurable': true,
+ 'enumerable': false,
+ 'value': constant(string),
+ 'writable': true
+ });
+ };
+
+ /**
+ * The base implementation of `_.shuffle`.
+ *
+ * @private
+ * @param {Array|Object} collection The collection to shuffle.
+ * @returns {Array} Returns the new shuffled array.
+ */
+ function baseShuffle(collection) {
+ return shuffleSelf(values(collection));
+ }
+
+ /**
+ * The base implementation of `_.slice` without an iteratee call guard.
+ *
+ * @private
+ * @param {Array} array The array to slice.
+ * @param {number} [start=0] The start position.
+ * @param {number} [end=array.length] The end position.
+ * @returns {Array} Returns the slice of `array`.
+ */
+ function baseSlice(array, start, end) {
+ var index = -1,
+ length = array.length;
+
+ if (start < 0) {
+ start = -start > length ? 0 : (length + start);
+ }
+ end = end > length ? length : end;
+ if (end < 0) {
+ end += length;
+ }
+ length = start > end ? 0 : ((end - start) >>> 0);
+ start >>>= 0;
+
+ var result = Array(length);
+ while (++index < length) {
+ result[index] = array[index + start];
+ }
+ return result;
+ }
+
+ /**
+ * The base implementation of `_.some` without support for iteratee shorthands.
+ *
+ * @private
+ * @param {Array|Object} collection The collection to iterate over.
+ * @param {Function} predicate The function invoked per iteration.
+ * @returns {boolean} Returns `true` if any element passes the predicate check,
+ * else `false`.
+ */
+ function baseSome(collection, predicate) {
+ var result;
+
+ baseEach(collection, function(value, index, collection) {
+ result = predicate(value, index, collection);
+ return !result;
+ });
+ return !!result;
+ }
+
+ /**
+ * The base implementation of `_.sortedIndex` and `_.sortedLastIndex` which
+ * performs a binary search of `array` to determine the index at which `value`
+ * should be inserted into `array` in order to maintain its sort order.
+ *
+ * @private
+ * @param {Array} array The sorted array to inspect.
+ * @param {*} value The value to evaluate.
+ * @param {boolean} [retHighest] Specify returning the highest qualified index.
+ * @returns {number} Returns the index at which `value` should be inserted
+ * into `array`.
+ */
+ function baseSortedIndex(array, value, retHighest) {
+ var low = 0,
+ high = array ? array.length : low;
+
+ if (typeof value == 'number' && value === value && high <= HALF_MAX_ARRAY_LENGTH) {
+ while (low < high) {
+ var mid = (low + high) >>> 1,
+ computed = array[mid];
+
+ if (computed !== null && !isSymbol(computed) &&
+ (retHighest ? (computed <= value) : (computed < value))) {
+ low = mid + 1;
+ } else {
+ high = mid;
+ }
+ }
+ return high;
+ }
+ return baseSortedIndexBy(array, value, identity, retHighest);
+ }
+
+ /**
+ * The base implementation of `_.sortedIndexBy` and `_.sortedLastIndexBy`
+ * which invokes `iteratee` for `value` and each element of `array` to compute
+ * their sort ranking. The iteratee is invoked with one argument; (value).
+ *
+ * @private
+ * @param {Array} array The sorted array to inspect.
+ * @param {*} value The value to evaluate.
+ * @param {Function} iteratee The iteratee invoked per element.
+ * @param {boolean} [retHighest] Specify returning the highest qualified index.
+ * @returns {number} Returns the index at which `value` should be inserted
+ * into `array`.
+ */
+ function baseSortedIndexBy(array, value, iteratee, retHighest) {
+ value = iteratee(value);
+
+ var low = 0,
+ high = array ? array.length : 0,
+ valIsNaN = value !== value,
+ valIsNull = value === null,
+ valIsSymbol = isSymbol(value),
+ valIsUndefined = value === undefined;
+
+ while (low < high) {
+ var mid = nativeFloor((low + high) / 2),
+ computed = iteratee(array[mid]),
+ othIsDefined = computed !== undefined,
+ othIsNull = computed === null,
+ othIsReflexive = computed === computed,
+ othIsSymbol = isSymbol(computed);
+
+ if (valIsNaN) {
+ var setLow = retHighest || othIsReflexive;
+ } else if (valIsUndefined) {
+ setLow = othIsReflexive && (retHighest || othIsDefined);
+ } else if (valIsNull) {
+ setLow = othIsReflexive && othIsDefined && (retHighest || !othIsNull);
+ } else if (valIsSymbol) {
+ setLow = othIsReflexive && othIsDefined && !othIsNull && (retHighest || !othIsSymbol);
+ } else if (othIsNull || othIsSymbol) {
+ setLow = false;
+ } else {
+ setLow = retHighest ? (computed <= value) : (computed < value);
+ }
+ if (setLow) {
+ low = mid + 1;
+ } else {
+ high = mid;
+ }
+ }
+ return nativeMin(high, MAX_ARRAY_INDEX);
+ }
+
+ /**
+ * The base implementation of `_.sortedUniq` and `_.sortedUniqBy` without
+ * support for iteratee shorthands.
+ *
+ * @private
+ * @param {Array} array The array to inspect.
+ * @param {Function} [iteratee] The iteratee invoked per element.
+ * @returns {Array} Returns the new duplicate free array.
+ */
+ function baseSortedUniq(array, iteratee) {
+ var index = -1,
+ length = array.length,
+ resIndex = 0,
+ result = [];
+
+ while (++index < length) {
+ var value = array[index],
+ computed = iteratee ? iteratee(value) : value;
+
+ if (!index || !eq(computed, seen)) {
+ var seen = computed;
+ result[resIndex++] = value === 0 ? 0 : value;
+ }
+ }
+ return result;
+ }
+
+ /**
+ * The base implementation of `_.toNumber` which doesn't ensure correct
+ * conversions of binary, hexadecimal, or octal string values.
+ *
+ * @private
+ * @param {*} value The value to process.
+ * @returns {number} Returns the number.
+ */
+ function baseToNumber(value) {
+ if (typeof value == 'number') {
+ return value;
+ }
+ if (isSymbol(value)) {
+ return NAN;
+ }
+ return +value;
+ }
+
+ /**
+ * The base implementation of `_.toString` which doesn't convert nullish
+ * values to empty strings.
+ *
+ * @private
+ * @param {*} value The value to process.
+ * @returns {string} Returns the string.
+ */
+ function baseToString(value) {
+ // Exit early for strings to avoid a performance hit in some environments.
+ if (typeof value == 'string') {
+ return value;
+ }
+ if (isArray(value)) {
+ // Recursively convert values (susceptible to call stack limits).
+ return arrayMap(value, baseToString) + '';
+ }
+ if (isSymbol(value)) {
+ return symbolToString ? symbolToString.call(value) : '';
+ }
+ var result = (value + '');
+ return (result == '0' && (1 / value) == -INFINITY) ? '-0' : result;
+ }
+
+ /**
+ * The base implementation of `_.uniqBy` without support for iteratee shorthands.
+ *
+ * @private
+ * @param {Array} array The array to inspect.
+ * @param {Function} [iteratee] The iteratee invoked per element.
+ * @param {Function} [comparator] The comparator invoked per element.
+ * @returns {Array} Returns the new duplicate free array.
+ */
+ function baseUniq(array, iteratee, comparator) {
+ var index = -1,
+ includes = arrayIncludes,
+ length = array.length,
+ isCommon = true,
+ result = [],
+ seen = result;
+
+ if (comparator) {
+ isCommon = false;
+ includes = arrayIncludesWith;
+ }
+ else if (length >= LARGE_ARRAY_SIZE) {
+ var set = iteratee ? null : createSet(array);
+ if (set) {
+ return setToArray(set);
+ }
+ isCommon = false;
+ includes = cacheHas;
+ seen = new SetCache;
+ }
+ else {
+ seen = iteratee ? [] : result;
+ }
+ outer:
+ while (++index < length) {
+ var value = array[index],
+ computed = iteratee ? iteratee(value) : value;
+
+ value = (comparator || value !== 0) ? value : 0;
+ if (isCommon && computed === computed) {
+ var seenIndex = seen.length;
+ while (seenIndex--) {
+ if (seen[seenIndex] === computed) {
+ continue outer;
+ }
+ }
+ if (iteratee) {
+ seen.push(computed);
+ }
+ result.push(value);
+ }
+ else if (!includes(seen, computed, comparator)) {
+ if (seen !== result) {
+ seen.push(computed);
+ }
+ result.push(value);
+ }
+ }
+ return result;
+ }
+
+ /**
+ * The base implementation of `_.unset`.
+ *
+ * @private
+ * @param {Object} object The object to modify.
+ * @param {Array|string} path The path of the property to unset.
+ * @returns {boolean} Returns `true` if the property is deleted, else `false`.
+ */
+ function baseUnset(object, path) {
+ path = isKey(path, object) ? [path] : castPath(path);
+ object = parent(object, path);
+
+ var key = toKey(last(path));
+ return !(object != null && hasOwnProperty.call(object, key)) || delete object[key];
+ }
+
+ /**
+ * The base implementation of `_.update`.
+ *
+ * @private
+ * @param {Object} object The object to modify.
+ * @param {Array|string} path The path of the property to update.
+ * @param {Function} updater The function to produce the updated value.
+ * @param {Function} [customizer] The function to customize path creation.
+ * @returns {Object} Returns `object`.
+ */
+ function baseUpdate(object, path, updater, customizer) {
+ return baseSet(object, path, updater(baseGet(object, path)), customizer);
+ }
+
+ /**
+ * The base implementation of methods like `_.dropWhile` and `_.takeWhile`
+ * without support for iteratee shorthands.
+ *
+ * @private
+ * @param {Array} array The array to query.
+ * @param {Function} predicate The function invoked per iteration.
+ * @param {boolean} [isDrop] Specify dropping elements instead of taking them.
+ * @param {boolean} [fromRight] Specify iterating from right to left.
+ * @returns {Array} Returns the slice of `array`.
+ */
+ function baseWhile(array, predicate, isDrop, fromRight) {
+ var length = array.length,
+ index = fromRight ? length : -1;
+
+ while ((fromRight ? index-- : ++index < length) &&
+ predicate(array[index], index, array)) {}
+
+ return isDrop
+ ? baseSlice(array, (fromRight ? 0 : index), (fromRight ? index + 1 : length))
+ : baseSlice(array, (fromRight ? index + 1 : 0), (fromRight ? length : index));
+ }
+
+ /**
+ * The base implementation of `wrapperValue` which returns the result of
+ * performing a sequence of actions on the unwrapped `value`, where each
+ * successive action is supplied the return value of the previous.
+ *
+ * @private
+ * @param {*} value The unwrapped value.
+ * @param {Array} actions Actions to perform to resolve the unwrapped value.
+ * @returns {*} Returns the resolved value.
+ */
+ function baseWrapperValue(value, actions) {
+ var result = value;
+ if (result instanceof LazyWrapper) {
+ result = result.value();
+ }
+ return arrayReduce(actions, function(result, action) {
+ return action.func.apply(action.thisArg, arrayPush([result], action.args));
+ }, result);
+ }
+
+ /**
+ * The base implementation of methods like `_.xor`, without support for
+ * iteratee shorthands, that accepts an array of arrays to inspect.
+ *
+ * @private
+ * @param {Array} arrays The arrays to inspect.
+ * @param {Function} [iteratee] The iteratee invoked per element.
+ * @param {Function} [comparator] The comparator invoked per element.
+ * @returns {Array} Returns the new array of values.
+ */
+ function baseXor(arrays, iteratee, comparator) {
+ var index = -1,
+ length = arrays.length;
+
+ while (++index < length) {
+ var result = result
+ ? arrayPush(
+ baseDifference(result, arrays[index], iteratee, comparator),
+ baseDifference(arrays[index], result, iteratee, comparator)
+ )
+ : arrays[index];
+ }
+ return (result && result.length) ? baseUniq(result, iteratee, comparator) : [];
+ }
+
+ /**
+ * This base implementation of `_.zipObject` which assigns values using `assignFunc`.
+ *
+ * @private
+ * @param {Array} props The property identifiers.
+ * @param {Array} values The property values.
+ * @param {Function} assignFunc The function to assign values.
+ * @returns {Object} Returns the new object.
+ */
+ function baseZipObject(props, values, assignFunc) {
+ var index = -1,
+ length = props.length,
+ valsLength = values.length,
+ result = {};
+
+ while (++index < length) {
+ var value = index < valsLength ? values[index] : undefined;
+ assignFunc(result, props[index], value);
+ }
+ return result;
+ }
+
+ /**
+ * Casts `value` to an empty array if it's not an array like object.
+ *
+ * @private
+ * @param {*} value The value to inspect.
+ * @returns {Array|Object} Returns the cast array-like object.
+ */
+ function castArrayLikeObject(value) {
+ return isArrayLikeObject(value) ? value : [];
+ }
+
+ /**
+ * Casts `value` to `identity` if it's not a function.
+ *
+ * @private
+ * @param {*} value The value to inspect.
+ * @returns {Function} Returns cast function.
+ */
+ function castFunction(value) {
+ return typeof value == 'function' ? value : identity;
+ }
+
+ /**
+ * Casts `value` to a path array if it's not one.
+ *
+ * @private
+ * @param {*} value The value to inspect.
+ * @returns {Array} Returns the cast property path array.
+ */
+ function castPath(value) {
+ return isArray(value) ? value : stringToPath(value);
+ }
+
+ /**
+ * A `baseRest` alias which can be replaced with `identity` by module
+ * replacement plugins.
+ *
+ * @private
+ * @type {Function}
+ * @param {Function} func The function to apply a rest parameter to.
+ * @returns {Function} Returns the new function.
+ */
+ var castRest = baseRest;
+
+ /**
+ * Casts `array` to a slice if it's needed.
+ *
+ * @private
+ * @param {Array} array The array to inspect.
+ * @param {number} start The start position.
+ * @param {number} [end=array.length] The end position.
+ * @returns {Array} Returns the cast slice.
+ */
+ function castSlice(array, start, end) {
+ var length = array.length;
+ end = end === undefined ? length : end;
+ return (!start && end >= length) ? array : baseSlice(array, start, end);
+ }
+
+ /**
+ * A simple wrapper around the global [`clearTimeout`](https://mdn.io/clearTimeout).
+ *
+ * @private
+ * @param {number|Object} id The timer id or timeout object of the timer to clear.
+ */
+ var clearTimeout = ctxClearTimeout || function(id) {
+ return root.clearTimeout(id);
+ };
+
+ /**
+ * Creates a clone of `buffer`.
+ *
+ * @private
+ * @param {Buffer} buffer The buffer to clone.
+ * @param {boolean} [isDeep] Specify a deep clone.
+ * @returns {Buffer} Returns the cloned buffer.
+ */
+ function cloneBuffer(buffer, isDeep) {
+ if (isDeep) {
+ return buffer.slice();
+ }
+ var length = buffer.length,
+ result = allocUnsafe ? allocUnsafe(length) : new buffer.constructor(length);
+
+ buffer.copy(result);
+ return result;
+ }
+
+ /**
+ * Creates a clone of `arrayBuffer`.
+ *
+ * @private
+ * @param {ArrayBuffer} arrayBuffer The array buffer to clone.
+ * @returns {ArrayBuffer} Returns the cloned array buffer.
+ */
+ function cloneArrayBuffer(arrayBuffer) {
+ var result = new arrayBuffer.constructor(arrayBuffer.byteLength);
+ new Uint8Array(result).set(new Uint8Array(arrayBuffer));
+ return result;
+ }
+
+ /**
+ * Creates a clone of `dataView`.
+ *
+ * @private
+ * @param {Object} dataView The data view to clone.
+ * @param {boolean} [isDeep] Specify a deep clone.
+ * @returns {Object} Returns the cloned data view.
+ */
+ function cloneDataView(dataView, isDeep) {
+ var buffer = isDeep ? cloneArrayBuffer(dataView.buffer) : dataView.buffer;
+ return new dataView.constructor(buffer, dataView.byteOffset, dataView.byteLength);
+ }
+
+ /**
+ * Creates a clone of `map`.
+ *
+ * @private
+ * @param {Object} map The map to clone.
+ * @param {Function} cloneFunc The function to clone values.
+ * @param {boolean} [isDeep] Specify a deep clone.
+ * @returns {Object} Returns the cloned map.
+ */
+ function cloneMap(map, isDeep, cloneFunc) {
+ var array = isDeep ? cloneFunc(mapToArray(map), true) : mapToArray(map);
+ return arrayReduce(array, addMapEntry, new map.constructor);
+ }
+
+ /**
+ * Creates a clone of `regexp`.
+ *
+ * @private
+ * @param {Object} regexp The regexp to clone.
+ * @returns {Object} Returns the cloned regexp.
+ */
+ function cloneRegExp(regexp) {
+ var result = new regexp.constructor(regexp.source, reFlags.exec(regexp));
+ result.lastIndex = regexp.lastIndex;
+ return result;
+ }
+
+ /**
+ * Creates a clone of `set`.
+ *
+ * @private
+ * @param {Object} set The set to clone.
+ * @param {Function} cloneFunc The function to clone values.
+ * @param {boolean} [isDeep] Specify a deep clone.
+ * @returns {Object} Returns the cloned set.
+ */
+ function cloneSet(set, isDeep, cloneFunc) {
+ var array = isDeep ? cloneFunc(setToArray(set), true) : setToArray(set);
+ return arrayReduce(array, addSetEntry, new set.constructor);
+ }
+
+ /**
+ * Creates a clone of the `symbol` object.
+ *
+ * @private
+ * @param {Object} symbol The symbol object to clone.
+ * @returns {Object} Returns the cloned symbol object.
+ */
+ function cloneSymbol(symbol) {
+ return symbolValueOf ? Object(symbolValueOf.call(symbol)) : {};
+ }
+
+ /**
+ * Creates a clone of `typedArray`.
+ *
+ * @private
+ * @param {Object} typedArray The typed array to clone.
+ * @param {boolean} [isDeep] Specify a deep clone.
+ * @returns {Object} Returns the cloned typed array.
+ */
+ function cloneTypedArray(typedArray, isDeep) {
+ var buffer = isDeep ? cloneArrayBuffer(typedArray.buffer) : typedArray.buffer;
+ return new typedArray.constructor(buffer, typedArray.byteOffset, typedArray.length);
+ }
+
+ /**
+ * Compares values to sort them in ascending order.
+ *
+ * @private
+ * @param {*} value The value to compare.
+ * @param {*} other The other value to compare.
+ * @returns {number} Returns the sort order indicator for `value`.
+ */
+ function compareAscending(value, other) {
+ if (value !== other) {
+ var valIsDefined = value !== undefined,
+ valIsNull = value === null,
+ valIsReflexive = value === value,
+ valIsSymbol = isSymbol(value);
+
+ var othIsDefined = other !== undefined,
+ othIsNull = other === null,
+ othIsReflexive = other === other,
+ othIsSymbol = isSymbol(other);
+
+ if ((!othIsNull && !othIsSymbol && !valIsSymbol && value > other) ||
+ (valIsSymbol && othIsDefined && othIsReflexive && !othIsNull && !othIsSymbol) ||
+ (valIsNull && othIsDefined && othIsReflexive) ||
+ (!valIsDefined && othIsReflexive) ||
+ !valIsReflexive) {
+ return 1;
+ }
+ if ((!valIsNull && !valIsSymbol && !othIsSymbol && value < other) ||
+ (othIsSymbol && valIsDefined && valIsReflexive && !valIsNull && !valIsSymbol) ||
+ (othIsNull && valIsDefined && valIsReflexive) ||
+ (!othIsDefined && valIsReflexive) ||
+ !othIsReflexive) {
+ return -1;
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * Used by `_.orderBy` to compare multiple properties of a value to another
+ * and stable sort them.
+ *
+ * If `orders` is unspecified, all values are sorted in ascending order. Otherwise,
+ * specify an order of "desc" for descending or "asc" for ascending sort order
+ * of corresponding values.
+ *
+ * @private
+ * @param {Object} object The object to compare.
+ * @param {Object} other The other object to compare.
+ * @param {boolean[]|string[]} orders The order to sort by for each property.
+ * @returns {number} Returns the sort order indicator for `object`.
+ */
+ function compareMultiple(object, other, orders) {
+ var index = -1,
+ objCriteria = object.criteria,
+ othCriteria = other.criteria,
+ length = objCriteria.length,
+ ordersLength = orders.length;
+
+ while (++index < length) {
+ var result = compareAscending(objCriteria[index], othCriteria[index]);
+ if (result) {
+ if (index >= ordersLength) {
+ return result;
+ }
+ var order = orders[index];
+ return result * (order == 'desc' ? -1 : 1);
+ }
+ }
+ // Fixes an `Array#sort` bug in the JS engine embedded in Adobe applications
+ // that causes it, under certain circumstances, to provide the same value for
+ // `object` and `other`. See https://github.com/jashkenas/underscore/pull/1247
+ // for more details.
+ //
+ // This also ensures a stable sort in V8 and other engines.
+ // See https://bugs.chromium.org/p/v8/issues/detail?id=90 for more details.
+ return object.index - other.index;
+ }
+
+ /**
+ * Creates an array that is the composition of partially applied arguments,
+ * placeholders, and provided arguments into a single array of arguments.
+ *
+ * @private
+ * @param {Array} args The provided arguments.
+ * @param {Array} partials The arguments to prepend to those provided.
+ * @param {Array} holders The `partials` placeholder indexes.
+ * @params {boolean} [isCurried] Specify composing for a curried function.
+ * @returns {Array} Returns the new array of composed arguments.
+ */
+ function composeArgs(args, partials, holders, isCurried) {
+ var argsIndex = -1,
+ argsLength = args.length,
+ holdersLength = holders.length,
+ leftIndex = -1,
+ leftLength = partials.length,
+ rangeLength = nativeMax(argsLength - holdersLength, 0),
+ result = Array(leftLength + rangeLength),
+ isUncurried = !isCurried;
+
+ while (++leftIndex < leftLength) {
+ result[leftIndex] = partials[leftIndex];
+ }
+ while (++argsIndex < holdersLength) {
+ if (isUncurried || argsIndex < argsLength) {
+ result[holders[argsIndex]] = args[argsIndex];
+ }
+ }
+ while (rangeLength--) {
+ result[leftIndex++] = args[argsIndex++];
+ }
+ return result;
+ }
+
+ /**
+ * This function is like `composeArgs` except that the arguments composition
+ * is tailored for `_.partialRight`.
+ *
+ * @private
+ * @param {Array} args The provided arguments.
+ * @param {Array} partials The arguments to append to those provided.
+ * @param {Array} holders The `partials` placeholder indexes.
+ * @params {boolean} [isCurried] Specify composing for a curried function.
+ * @returns {Array} Returns the new array of composed arguments.
+ */
+ function composeArgsRight(args, partials, holders, isCurried) {
+ var argsIndex = -1,
+ argsLength = args.length,
+ holdersIndex = -1,
+ holdersLength = holders.length,
+ rightIndex = -1,
+ rightLength = partials.length,
+ rangeLength = nativeMax(argsLength - holdersLength, 0),
+ result = Array(rangeLength + rightLength),
+ isUncurried = !isCurried;
+
+ while (++argsIndex < rangeLength) {
+ result[argsIndex] = args[argsIndex];
+ }
+ var offset = argsIndex;
+ while (++rightIndex < rightLength) {
+ result[offset + rightIndex] = partials[rightIndex];
+ }
+ while (++holdersIndex < holdersLength) {
+ if (isUncurried || argsIndex < argsLength) {
+ result[offset + holders[holdersIndex]] = args[argsIndex++];
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Copies the values of `source` to `array`.
+ *
+ * @private
+ * @param {Array} source The array to copy values from.
+ * @param {Array} [array=[]] The array to copy values to.
+ * @returns {Array} Returns `array`.
+ */
+ function copyArray(source, array) {
+ var index = -1,
+ length = source.length;
+
+ array || (array = Array(length));
+ while (++index < length) {
+ array[index] = source[index];
+ }
+ return array;
+ }
+
+ /**
+ * Copies properties of `source` to `object`.
+ *
+ * @private
+ * @param {Object} source The object to copy properties from.
+ * @param {Array} props The property identifiers to copy.
+ * @param {Object} [object={}] The object to copy properties to.
+ * @param {Function} [customizer] The function to customize copied values.
+ * @returns {Object} Returns `object`.
+ */
+ function copyObject(source, props, object, customizer) {
+ var isNew = !object;
+ object || (object = {});
+
+ var index = -1,
+ length = props.length;
+
+ while (++index < length) {
+ var key = props[index];
+
+ var newValue = customizer
+ ? customizer(object[key], source[key], key, object, source)
+ : undefined;
+
+ if (newValue === undefined) {
+ newValue = source[key];
+ }
+ if (isNew) {
+ baseAssignValue(object, key, newValue);
+ } else {
+ assignValue(object, key, newValue);
+ }
+ }
+ return object;
+ }
+
+ /**
+ * Copies own symbol properties of `source` to `object`.
+ *
+ * @private
+ * @param {Object} source The object to copy symbols from.
+ * @param {Object} [object={}] The object to copy symbols to.
+ * @returns {Object} Returns `object`.
+ */
+ function copySymbols(source, object) {
+ return copyObject(source, getSymbols(source), object);
+ }
+
+ /**
+ * Creates a function like `_.groupBy`.
+ *
+ * @private
+ * @param {Function} setter The function to set accumulator values.
+ * @param {Function} [initializer] The accumulator object initializer.
+ * @returns {Function} Returns the new aggregator function.
+ */
+ function createAggregator(setter, initializer) {
+ return function(collection, iteratee) {
+ var func = isArray(collection) ? arrayAggregator : baseAggregator,
+ accumulator = initializer ? initializer() : {};
+
+ return func(collection, setter, getIteratee(iteratee, 2), accumulator);
+ };
+ }
+
+ /**
+ * Creates a function like `_.assign`.
+ *
+ * @private
+ * @param {Function} assigner The function to assign values.
+ * @returns {Function} Returns the new assigner function.
+ */
+ function createAssigner(assigner) {
+ return baseRest(function(object, sources) {
+ var index = -1,
+ length = sources.length,
+ customizer = length > 1 ? sources[length - 1] : undefined,
+ guard = length > 2 ? sources[2] : undefined;
+
+ customizer = (assigner.length > 3 && typeof customizer == 'function')
+ ? (length--, customizer)
+ : undefined;
+
+ if (guard && isIterateeCall(sources[0], sources[1], guard)) {
+ customizer = length < 3 ? undefined : customizer;
+ length = 1;
+ }
+ object = Object(object);
+ while (++index < length) {
+ var source = sources[index];
+ if (source) {
+ assigner(object, source, index, customizer);
+ }
+ }
+ return object;
+ });
+ }
+
+ /**
+ * Creates a `baseEach` or `baseEachRight` function.
+ *
+ * @private
+ * @param {Function} eachFunc The function to iterate over a collection.
+ * @param {boolean} [fromRight] Specify iterating from right to left.
+ * @returns {Function} Returns the new base function.
+ */
+ function createBaseEach(eachFunc, fromRight) {
+ return function(collection, iteratee) {
+ if (collection == null) {
+ return collection;
+ }
+ if (!isArrayLike(collection)) {
+ return eachFunc(collection, iteratee);
+ }
+ var length = collection.length,
+ index = fromRight ? length : -1,
+ iterable = Object(collection);
+
+ while ((fromRight ? index-- : ++index < length)) {
+ if (iteratee(iterable[index], index, iterable) === false) {
+ break;
+ }
+ }
+ return collection;
+ };
+ }
+
+ /**
+ * Creates a base function for methods like `_.forIn` and `_.forOwn`.
+ *
+ * @private
+ * @param {boolean} [fromRight] Specify iterating from right to left.
+ * @returns {Function} Returns the new base function.
+ */
+ function createBaseFor(fromRight) {
+ return function(object, iteratee, keysFunc) {
+ var index = -1,
+ iterable = Object(object),
+ props = keysFunc(object),
+ length = props.length;
+
+ while (length--) {
+ var key = props[fromRight ? length : ++index];
+ if (iteratee(iterable[key], key, iterable) === false) {
+ break;
+ }
+ }
+ return object;
+ };
+ }
+
+ /**
+ * Creates a function that wraps `func` to invoke it with the optional `this`
+ * binding of `thisArg`.
+ *
+ * @private
+ * @param {Function} func The function to wrap.
+ * @param {number} bitmask The bitmask flags. See `createWrap` for more details.
+ * @param {*} [thisArg] The `this` binding of `func`.
+ * @returns {Function} Returns the new wrapped function.
+ */
+ function createBind(func, bitmask, thisArg) {
+ var isBind = bitmask & BIND_FLAG,
+ Ctor = createCtor(func);
+
+ function wrapper() {
+ var fn = (this && this !== root && this instanceof wrapper) ? Ctor : func;
+ return fn.apply(isBind ? thisArg : this, arguments);
+ }
+ return wrapper;
+ }
+
+ /**
+ * Creates a function like `_.lowerFirst`.
+ *
+ * @private
+ * @param {string} methodName The name of the `String` case method to use.
+ * @returns {Function} Returns the new case function.
+ */
+ function createCaseFirst(methodName) {
+ return function(string) {
+ string = toString(string);
+
+ var strSymbols = hasUnicode(string)
+ ? stringToArray(string)
+ : undefined;
+
+ var chr = strSymbols
+ ? strSymbols[0]
+ : string.charAt(0);
+
+ var trailing = strSymbols
+ ? castSlice(strSymbols, 1).join('')
+ : string.slice(1);
+
+ return chr[methodName]() + trailing;
+ };
+ }
+
+ /**
+ * Creates a function like `_.camelCase`.
+ *
+ * @private
+ * @param {Function} callback The function to combine each word.
+ * @returns {Function} Returns the new compounder function.
+ */
+ function createCompounder(callback) {
+ return function(string) {
+ return arrayReduce(words(deburr(string).replace(reApos, '')), callback, '');
+ };
+ }
+
+ /**
+ * Creates a function that produces an instance of `Ctor` regardless of
+ * whether it was invoked as part of a `new` expression or by `call` or `apply`.
+ *
+ * @private
+ * @param {Function} Ctor The constructor to wrap.
+ * @returns {Function} Returns the new wrapped function.
+ */
+ function createCtor(Ctor) {
+ return function() {
+ // Use a `switch` statement to work with class constructors. See
+ // http://ecma-international.org/ecma-262/7.0/#sec-ecmascript-function-objects-call-thisargument-argumentslist
+ // for more details.
+ var args = arguments;
+ switch (args.length) {
+ case 0: return new Ctor;
+ case 1: return new Ctor(args[0]);
+ case 2: return new Ctor(args[0], args[1]);
+ case 3: return new Ctor(args[0], args[1], args[2]);
+ case 4: return new Ctor(args[0], args[1], args[2], args[3]);
+ case 5: return new Ctor(args[0], args[1], args[2], args[3], args[4]);
+ case 6: return new Ctor(args[0], args[1], args[2], args[3], args[4], args[5]);
+ case 7: return new Ctor(args[0], args[1], args[2], args[3], args[4], args[5], args[6]);
+ }
+ var thisBinding = baseCreate(Ctor.prototype),
+ result = Ctor.apply(thisBinding, args);
+
+ // Mimic the constructor's `return` behavior.
+ // See https://es5.github.io/#x13.2.2 for more details.
+ return isObject(result) ? result : thisBinding;
+ };
+ }
+
+ /**
+ * Creates a function that wraps `func` to enable currying.
+ *
+ * @private
+ * @param {Function} func The function to wrap.
+ * @param {number} bitmask The bitmask flags. See `createWrap` for more details.
+ * @param {number} arity The arity of `func`.
+ * @returns {Function} Returns the new wrapped function.
+ */
+ function createCurry(func, bitmask, arity) {
+ var Ctor = createCtor(func);
+
+ function wrapper() {
+ var length = arguments.length,
+ args = Array(length),
+ index = length,
+ placeholder = getHolder(wrapper);
+
+ while (index--) {
+ args[index] = arguments[index];
+ }
+ var holders = (length < 3 && args[0] !== placeholder && args[length - 1] !== placeholder)
+ ? []
+ : replaceHolders(args, placeholder);
+
+ length -= holders.length;
+ if (length < arity) {
+ return createRecurry(
+ func, bitmask, createHybrid, wrapper.placeholder, undefined,
+ args, holders, undefined, undefined, arity - length);
+ }
+ var fn = (this && this !== root && this instanceof wrapper) ? Ctor : func;
+ return apply(fn, this, args);
+ }
+ return wrapper;
+ }
+
+ /**
+ * Creates a `_.find` or `_.findLast` function.
+ *
+ * @private
+ * @param {Function} findIndexFunc The function to find the collection index.
+ * @returns {Function} Returns the new find function.
+ */
+ function createFind(findIndexFunc) {
+ return function(collection, predicate, fromIndex) {
+ var iterable = Object(collection);
+ if (!isArrayLike(collection)) {
+ var iteratee = getIteratee(predicate, 3);
+ collection = keys(collection);
+ predicate = function(key) { return iteratee(iterable[key], key, iterable); };
+ }
+ var index = findIndexFunc(collection, predicate, fromIndex);
+ return index > -1 ? iterable[iteratee ? collection[index] : index] : undefined;
+ };
+ }
+
+ /**
+ * Creates a `_.flow` or `_.flowRight` function.
+ *
+ * @private
+ * @param {boolean} [fromRight] Specify iterating from right to left.
+ * @returns {Function} Returns the new flow function.
+ */
+ function createFlow(fromRight) {
+ return flatRest(function(funcs) {
+ var length = funcs.length,
+ index = length,
+ prereq = LodashWrapper.prototype.thru;
+
+ if (fromRight) {
+ funcs.reverse();
+ }
+ while (index--) {
+ var func = funcs[index];
+ if (typeof func != 'function') {
+ throw new TypeError(FUNC_ERROR_TEXT);
+ }
+ if (prereq && !wrapper && getFuncName(func) == 'wrapper') {
+ var wrapper = new LodashWrapper([], true);
+ }
+ }
+ index = wrapper ? index : length;
+ while (++index < length) {
+ func = funcs[index];
+
+ var funcName = getFuncName(func),
+ data = funcName == 'wrapper' ? getData(func) : undefined;
+
+ if (data && isLaziable(data[0]) &&
+ data[1] == (ARY_FLAG | CURRY_FLAG | PARTIAL_FLAG | REARG_FLAG) &&
+ !data[4].length && data[9] == 1
+ ) {
+ wrapper = wrapper[getFuncName(data[0])].apply(wrapper, data[3]);
+ } else {
+ wrapper = (func.length == 1 && isLaziable(func))
+ ? wrapper[funcName]()
+ : wrapper.thru(func);
+ }
+ }
+ return function() {
+ var args = arguments,
+ value = args[0];
+
+ if (wrapper && args.length == 1 &&
+ isArray(value) && value.length >= LARGE_ARRAY_SIZE) {
+ return wrapper.plant(value).value();
+ }
+ var index = 0,
+ result = length ? funcs[index].apply(this, args) : value;
+
+ while (++index < length) {
+ result = funcs[index].call(this, result);
+ }
+ return result;
+ };
+ });
+ }
+
+ /**
+ * Creates a function that wraps `func` to invoke it with optional `this`
+ * binding of `thisArg`, partial application, and currying.
+ *
+ * @private
+ * @param {Function|string} func The function or method name to wrap.
+ * @param {number} bitmask The bitmask flags. See `createWrap` for more details.
+ * @param {*} [thisArg] The `this` binding of `func`.
+ * @param {Array} [partials] The arguments to prepend to those provided to
+ * the new function.
+ * @param {Array} [holders] The `partials` placeholder indexes.
+ * @param {Array} [partialsRight] The arguments to append to those provided
+ * to the new function.
+ * @param {Array} [holdersRight] The `partialsRight` placeholder indexes.
+ * @param {Array} [argPos] The argument positions of the new function.
+ * @param {number} [ary] The arity cap of `func`.
+ * @param {number} [arity] The arity of `func`.
+ * @returns {Function} Returns the new wrapped function.
+ */
+ function createHybrid(func, bitmask, thisArg, partials, holders, partialsRight, holdersRight, argPos, ary, arity) {
+ var isAry = bitmask & ARY_FLAG,
+ isBind = bitmask & BIND_FLAG,
+ isBindKey = bitmask & BIND_KEY_FLAG,
+ isCurried = bitmask & (CURRY_FLAG | CURRY_RIGHT_FLAG),
+ isFlip = bitmask & FLIP_FLAG,
+ Ctor = isBindKey ? undefined : createCtor(func);
+
+ function wrapper() {
+ var length = arguments.length,
+ args = Array(length),
+ index = length;
+
+ while (index--) {
+ args[index] = arguments[index];
+ }
+ if (isCurried) {
+ var placeholder = getHolder(wrapper),
+ holdersCount = countHolders(args, placeholder);
+ }
+ if (partials) {
+ args = composeArgs(args, partials, holders, isCurried);
+ }
+ if (partialsRight) {
+ args = composeArgsRight(args, partialsRight, holdersRight, isCurried);
+ }
+ length -= holdersCount;
+ if (isCurried && length < arity) {
+ var newHolders = replaceHolders(args, placeholder);
+ return createRecurry(
+ func, bitmask, createHybrid, wrapper.placeholder, thisArg,
+ args, newHolders, argPos, ary, arity - length
+ );
+ }
+ var thisBinding = isBind ? thisArg : this,
+ fn = isBindKey ? thisBinding[func] : func;
+
+ length = args.length;
+ if (argPos) {
+ args = reorder(args, argPos);
+ } else if (isFlip && length > 1) {
+ args.reverse();
+ }
+ if (isAry && ary < length) {
+ args.length = ary;
+ }
+ if (this && this !== root && this instanceof wrapper) {
+ fn = Ctor || createCtor(fn);
+ }
+ return fn.apply(thisBinding, args);
+ }
+ return wrapper;
+ }
+
+ /**
+ * Creates a function like `_.invertBy`.
+ *
+ * @private
+ * @param {Function} setter The function to set accumulator values.
+ * @param {Function} toIteratee The function to resolve iteratees.
+ * @returns {Function} Returns the new inverter function.
+ */
+ function createInverter(setter, toIteratee) {
+ return function(object, iteratee) {
+ return baseInverter(object, setter, toIteratee(iteratee), {});
+ };
+ }
+
+ /**
+ * Creates a function that performs a mathematical operation on two values.
+ *
+ * @private
+ * @param {Function} operator The function to perform the operation.
+ * @param {number} [defaultValue] The value used for `undefined` arguments.
+ * @returns {Function} Returns the new mathematical operation function.
+ */
+ function createMathOperation(operator, defaultValue) {
+ return function(value, other) {
+ var result;
+ if (value === undefined && other === undefined) {
+ return defaultValue;
+ }
+ if (value !== undefined) {
+ result = value;
+ }
+ if (other !== undefined) {
+ if (result === undefined) {
+ return other;
+ }
+ if (typeof value == 'string' || typeof other == 'string') {
+ value = baseToString(value);
+ other = baseToString(other);
+ } else {
+ value = baseToNumber(value);
+ other = baseToNumber(other);
+ }
+ result = operator(value, other);
+ }
+ return result;
+ };
+ }
+
+ /**
+ * Creates a function like `_.over`.
+ *
+ * @private
+ * @param {Function} arrayFunc The function to iterate over iteratees.
+ * @returns {Function} Returns the new over function.
+ */
+ function createOver(arrayFunc) {
+ return flatRest(function(iteratees) {
+ iteratees = arrayMap(iteratees, baseUnary(getIteratee()));
+ return baseRest(function(args) {
+ var thisArg = this;
+ return arrayFunc(iteratees, function(iteratee) {
+ return apply(iteratee, thisArg, args);
+ });
+ });
+ });
+ }
+
+ /**
+ * Creates the padding for `string` based on `length`. The `chars` string
+ * is truncated if the number of characters exceeds `length`.
+ *
+ * @private
+ * @param {number} length The padding length.
+ * @param {string} [chars=' '] The string used as padding.
+ * @returns {string} Returns the padding for `string`.
+ */
+ function createPadding(length, chars) {
+ chars = chars === undefined ? ' ' : baseToString(chars);
+
+ var charsLength = chars.length;
+ if (charsLength < 2) {
+ return charsLength ? baseRepeat(chars, length) : chars;
+ }
+ var result = baseRepeat(chars, nativeCeil(length / stringSize(chars)));
+ return hasUnicode(chars)
+ ? castSlice(stringToArray(result), 0, length).join('')
+ : result.slice(0, length);
+ }
+
+ /**
+ * Creates a function that wraps `func` to invoke it with the `this` binding
+ * of `thisArg` and `partials` prepended to the arguments it receives.
+ *
+ * @private
+ * @param {Function} func The function to wrap.
+ * @param {number} bitmask The bitmask flags. See `createWrap` for more details.
+ * @param {*} thisArg The `this` binding of `func`.
+ * @param {Array} partials The arguments to prepend to those provided to
+ * the new function.
+ * @returns {Function} Returns the new wrapped function.
+ */
+ function createPartial(func, bitmask, thisArg, partials) {
+ var isBind = bitmask & BIND_FLAG,
+ Ctor = createCtor(func);
+
+ function wrapper() {
+ var argsIndex = -1,
+ argsLength = arguments.length,
+ leftIndex = -1,
+ leftLength = partials.length,
+ args = Array(leftLength + argsLength),
+ fn = (this && this !== root && this instanceof wrapper) ? Ctor : func;
+
+ while (++leftIndex < leftLength) {
+ args[leftIndex] = partials[leftIndex];
+ }
+ while (argsLength--) {
+ args[leftIndex++] = arguments[++argsIndex];
+ }
+ return apply(fn, isBind ? thisArg : this, args);
+ }
+ return wrapper;
+ }
+
+ /**
+ * Creates a `_.range` or `_.rangeRight` function.
+ *
+ * @private
+ * @param {boolean} [fromRight] Specify iterating from right to left.
+ * @returns {Function} Returns the new range function.
+ */
+ function createRange(fromRight) {
+ return function(start, end, step) {
+ if (step && typeof step != 'number' && isIterateeCall(start, end, step)) {
+ end = step = undefined;
+ }
+ // Ensure the sign of `-0` is preserved.
+ start = toFinite(start);
+ if (end === undefined) {
+ end = start;
+ start = 0;
+ } else {
+ end = toFinite(end);
+ }
+ step = step === undefined ? (start < end ? 1 : -1) : toFinite(step);
+ return baseRange(start, end, step, fromRight);
+ };
+ }
+
+ /**
+ * Creates a function that performs a relational operation on two values.
+ *
+ * @private
+ * @param {Function} operator The function to perform the operation.
+ * @returns {Function} Returns the new relational operation function.
+ */
+ function createRelationalOperation(operator) {
+ return function(value, other) {
+ if (!(typeof value == 'string' && typeof other == 'string')) {
+ value = toNumber(value);
+ other = toNumber(other);
+ }
+ return operator(value, other);
+ };
+ }
+
+ /**
+ * Creates a function that wraps `func` to continue currying.
+ *
+ * @private
+ * @param {Function} func The function to wrap.
+ * @param {number} bitmask The bitmask flags. See `createWrap` for more details.
+ * @param {Function} wrapFunc The function to create the `func` wrapper.
+ * @param {*} placeholder The placeholder value.
+ * @param {*} [thisArg] The `this` binding of `func`.
+ * @param {Array} [partials] The arguments to prepend to those provided to
+ * the new function.
+ * @param {Array} [holders] The `partials` placeholder indexes.
+ * @param {Array} [argPos] The argument positions of the new function.
+ * @param {number} [ary] The arity cap of `func`.
+ * @param {number} [arity] The arity of `func`.
+ * @returns {Function} Returns the new wrapped function.
+ */
+ function createRecurry(func, bitmask, wrapFunc, placeholder, thisArg, partials, holders, argPos, ary, arity) {
+ var isCurry = bitmask & CURRY_FLAG,
+ newHolders = isCurry ? holders : undefined,
+ newHoldersRight = isCurry ? undefined : holders,
+ newPartials = isCurry ? partials : undefined,
+ newPartialsRight = isCurry ? undefined : partials;
+
+ bitmask |= (isCurry ? PARTIAL_FLAG : PARTIAL_RIGHT_FLAG);
+ bitmask &= ~(isCurry ? PARTIAL_RIGHT_FLAG : PARTIAL_FLAG);
+
+ if (!(bitmask & CURRY_BOUND_FLAG)) {
+ bitmask &= ~(BIND_FLAG | BIND_KEY_FLAG);
+ }
+ var newData = [
+ func, bitmask, thisArg, newPartials, newHolders, newPartialsRight,
+ newHoldersRight, argPos, ary, arity
+ ];
+
+ var result = wrapFunc.apply(undefined, newData);
+ if (isLaziable(func)) {
+ setData(result, newData);
+ }
+ result.placeholder = placeholder;
+ return setWrapToString(result, func, bitmask);
+ }
+
+ /**
+ * Creates a function like `_.round`.
+ *
+ * @private
+ * @param {string} methodName The name of the `Math` method to use when rounding.
+ * @returns {Function} Returns the new round function.
+ */
+ function createRound(methodName) {
+ var func = Math[methodName];
+ return function(number, precision) {
+ number = toNumber(number);
+ precision = nativeMin(toInteger(precision), 292);
+ if (precision) {
+ // Shift with exponential notation to avoid floating-point issues.
+ // See [MDN](https://mdn.io/round#Examples) for more details.
+ var pair = (toString(number) + 'e').split('e'),
+ value = func(pair[0] + 'e' + (+pair[1] + precision));
+
+ pair = (toString(value) + 'e').split('e');
+ return +(pair[0] + 'e' + (+pair[1] - precision));
+ }
+ return func(number);
+ };
+ }
+
+ /**
+ * Creates a set object of `values`.
+ *
+ * @private
+ * @param {Array} values The values to add to the set.
+ * @returns {Object} Returns the new set.
+ */
+ var createSet = !(Set && (1 / setToArray(new Set([,-0]))[1]) == INFINITY) ? noop : function(values) {
+ return new Set(values);
+ };
+
+ /**
+ * Creates a `_.toPairs` or `_.toPairsIn` function.
+ *
+ * @private
+ * @param {Function} keysFunc The function to get the keys of a given object.
+ * @returns {Function} Returns the new pairs function.
+ */
+ function createToPairs(keysFunc) {
+ return function(object) {
+ var tag = getTag(object);
+ if (tag == mapTag) {
+ return mapToArray(object);
+ }
+ if (tag == setTag) {
+ return setToPairs(object);
+ }
+ return baseToPairs(object, keysFunc(object));
+ };
+ }
+
+ /**
+ * Creates a function that either curries or invokes `func` with optional
+ * `this` binding and partially applied arguments.
+ *
+ * @private
+ * @param {Function|string} func The function or method name to wrap.
+ * @param {number} bitmask The bitmask flags.
+ * The bitmask may be composed of the following flags:
+ * 1 - `_.bind`
+ * 2 - `_.bindKey`
+ * 4 - `_.curry` or `_.curryRight` of a bound function
+ * 8 - `_.curry`
+ * 16 - `_.curryRight`
+ * 32 - `_.partial`
+ * 64 - `_.partialRight`
+ * 128 - `_.rearg`
+ * 256 - `_.ary`
+ * 512 - `_.flip`
+ * @param {*} [thisArg] The `this` binding of `func`.
+ * @param {Array} [partials] The arguments to be partially applied.
+ * @param {Array} [holders] The `partials` placeholder indexes.
+ * @param {Array} [argPos] The argument positions of the new function.
+ * @param {number} [ary] The arity cap of `func`.
+ * @param {number} [arity] The arity of `func`.
+ * @returns {Function} Returns the new wrapped function.
+ */
+ function createWrap(func, bitmask, thisArg, partials, holders, argPos, ary, arity) {
+ var isBindKey = bitmask & BIND_KEY_FLAG;
+ if (!isBindKey && typeof func != 'function') {
+ throw new TypeError(FUNC_ERROR_TEXT);
+ }
+ var length = partials ? partials.length : 0;
+ if (!length) {
+ bitmask &= ~(PARTIAL_FLAG | PARTIAL_RIGHT_FLAG);
+ partials = holders = undefined;
+ }
+ ary = ary === undefined ? ary : nativeMax(toInteger(ary), 0);
+ arity = arity === undefined ? arity : toInteger(arity);
+ length -= holders ? holders.length : 0;
+
+ if (bitmask & PARTIAL_RIGHT_FLAG) {
+ var partialsRight = partials,
+ holdersRight = holders;
+
+ partials = holders = undefined;
+ }
+ var data = isBindKey ? undefined : getData(func);
+
+ var newData = [
+ func, bitmask, thisArg, partials, holders, partialsRight, holdersRight,
+ argPos, ary, arity
+ ];
+
+ if (data) {
+ mergeData(newData, data);
+ }
+ func = newData[0];
+ bitmask = newData[1];
+ thisArg = newData[2];
+ partials = newData[3];
+ holders = newData[4];
+ arity = newData[9] = newData[9] == null
+ ? (isBindKey ? 0 : func.length)
+ : nativeMax(newData[9] - length, 0);
+
+ if (!arity && bitmask & (CURRY_FLAG | CURRY_RIGHT_FLAG)) {
+ bitmask &= ~(CURRY_FLAG | CURRY_RIGHT_FLAG);
+ }
+ if (!bitmask || bitmask == BIND_FLAG) {
+ var result = createBind(func, bitmask, thisArg);
+ } else if (bitmask == CURRY_FLAG || bitmask == CURRY_RIGHT_FLAG) {
+ result = createCurry(func, bitmask, arity);
+ } else if ((bitmask == PARTIAL_FLAG || bitmask == (BIND_FLAG | PARTIAL_FLAG)) && !holders.length) {
+ result = createPartial(func, bitmask, thisArg, partials);
+ } else {
+ result = createHybrid.apply(undefined, newData);
+ }
+ var setter = data ? baseSetData : setData;
+ return setWrapToString(setter(result, newData), func, bitmask);
+ }
+
+ /**
+ * A specialized version of `baseIsEqualDeep` for arrays with support for
+ * partial deep comparisons.
+ *
+ * @private
+ * @param {Array} array The array to compare.
+ * @param {Array} other The other array to compare.
+ * @param {Function} equalFunc The function to determine equivalents of values.
+ * @param {Function} customizer The function to customize comparisons.
+ * @param {number} bitmask The bitmask of comparison flags. See `baseIsEqual`
+ * for more details.
+ * @param {Object} stack Tracks traversed `array` and `other` objects.
+ * @returns {boolean} Returns `true` if the arrays are equivalent, else `false`.
+ */
+ function equalArrays(array, other, equalFunc, customizer, bitmask, stack) {
+ var isPartial = bitmask & PARTIAL_COMPARE_FLAG,
+ arrLength = array.length,
+ othLength = other.length;
+
+ if (arrLength != othLength && !(isPartial && othLength > arrLength)) {
+ return false;
+ }
+ // Assume cyclic values are equal.
+ var stacked = stack.get(array);
+ if (stacked && stack.get(other)) {
+ return stacked == other;
+ }
+ var index = -1,
+ result = true,
+ seen = (bitmask & UNORDERED_COMPARE_FLAG) ? new SetCache : undefined;
+
+ stack.set(array, other);
+ stack.set(other, array);
+
+ // Ignore non-index properties.
+ while (++index < arrLength) {
+ var arrValue = array[index],
+ othValue = other[index];
+
+ if (customizer) {
+ var compared = isPartial
+ ? customizer(othValue, arrValue, index, other, array, stack)
+ : customizer(arrValue, othValue, index, array, other, stack);
+ }
+ if (compared !== undefined) {
+ if (compared) {
+ continue;
+ }
+ result = false;
+ break;
+ }
+ // Recursively compare arrays (susceptible to call stack limits).
+ if (seen) {
+ if (!arraySome(other, function(othValue, othIndex) {
+ if (!cacheHas(seen, othIndex) &&
+ (arrValue === othValue || equalFunc(arrValue, othValue, customizer, bitmask, stack))) {
+ return seen.push(othIndex);
+ }
+ })) {
+ result = false;
+ break;
+ }
+ } else if (!(
+ arrValue === othValue ||
+ equalFunc(arrValue, othValue, customizer, bitmask, stack)
+ )) {
+ result = false;
+ break;
+ }
+ }
+ stack['delete'](array);
+ stack['delete'](other);
+ return result;
+ }
+
+ /**
+ * A specialized version of `baseIsEqualDeep` for comparing objects of
+ * the same `toStringTag`.
+ *
+ * **Note:** This function only supports comparing values with tags of
+ * `Boolean`, `Date`, `Error`, `Number`, `RegExp`, or `String`.
+ *
+ * @private
+ * @param {Object} object The object to compare.
+ * @param {Object} other The other object to compare.
+ * @param {string} tag The `toStringTag` of the objects to compare.
+ * @param {Function} equalFunc The function to determine equivalents of values.
+ * @param {Function} customizer The function to customize comparisons.
+ * @param {number} bitmask The bitmask of comparison flags. See `baseIsEqual`
+ * for more details.
+ * @param {Object} stack Tracks traversed `object` and `other` objects.
+ * @returns {boolean} Returns `true` if the objects are equivalent, else `false`.
+ */
+ function equalByTag(object, other, tag, equalFunc, customizer, bitmask, stack) {
+ switch (tag) {
+ case dataViewTag:
+ if ((object.byteLength != other.byteLength) ||
+ (object.byteOffset != other.byteOffset)) {
+ return false;
+ }
+ object = object.buffer;
+ other = other.buffer;
+
+ case arrayBufferTag:
+ if ((object.byteLength != other.byteLength) ||
+ !equalFunc(new Uint8Array(object), new Uint8Array(other))) {
+ return false;
+ }
+ return true;
+
+ case boolTag:
+ case dateTag:
+ case numberTag:
+ // Coerce booleans to `1` or `0` and dates to milliseconds.
+ // Invalid dates are coerced to `NaN`.
+ return eq(+object, +other);
+
+ case errorTag:
+ return object.name == other.name && object.message == other.message;
+
+ case regexpTag:
+ case stringTag:
+ // Coerce regexes to strings and treat strings, primitives and objects,
+ // as equal. See http://www.ecma-international.org/ecma-262/7.0/#sec-regexp.prototype.tostring
+ // for more details.
+ return object == (other + '');
+
+ case mapTag:
+ var convert = mapToArray;
+
+ case setTag:
+ var isPartial = bitmask & PARTIAL_COMPARE_FLAG;
+ convert || (convert = setToArray);
+
+ if (object.size != other.size && !isPartial) {
+ return false;
+ }
+ // Assume cyclic values are equal.
+ var stacked = stack.get(object);
+ if (stacked) {
+ return stacked == other;
+ }
+ bitmask |= UNORDERED_COMPARE_FLAG;
+
+ // Recursively compare objects (susceptible to call stack limits).
+ stack.set(object, other);
+ var result = equalArrays(convert(object), convert(other), equalFunc, customizer, bitmask, stack);
+ stack['delete'](object);
+ return result;
+
+ case symbolTag:
+ if (symbolValueOf) {
+ return symbolValueOf.call(object) == symbolValueOf.call(other);
+ }
+ }
+ return false;
+ }
+
+ /**
+ * A specialized version of `baseIsEqualDeep` for objects with support for
+ * partial deep comparisons.
+ *
+ * @private
+ * @param {Object} object The object to compare.
+ * @param {Object} other The other object to compare.
+ * @param {Function} equalFunc The function to determine equivalents of values.
+ * @param {Function} customizer The function to customize comparisons.
+ * @param {number} bitmask The bitmask of comparison flags. See `baseIsEqual`
+ * for more details.
+ * @param {Object} stack Tracks traversed `object` and `other` objects.
+ * @returns {boolean} Returns `true` if the objects are equivalent, else `false`.
+ */
+ function equalObjects(object, other, equalFunc, customizer, bitmask, stack) {
+ var isPartial = bitmask & PARTIAL_COMPARE_FLAG,
+ objProps = keys(object),
+ objLength = objProps.length,
+ othProps = keys(other),
+ othLength = othProps.length;
+
+ if (objLength != othLength && !isPartial) {
+ return false;
+ }
+ var index = objLength;
+ while (index--) {
+ var key = objProps[index];
+ if (!(isPartial ? key in other : hasOwnProperty.call(other, key))) {
+ return false;
+ }
+ }
+ // Assume cyclic values are equal.
+ var stacked = stack.get(object);
+ if (stacked && stack.get(other)) {
+ return stacked == other;
+ }
+ var result = true;
+ stack.set(object, other);
+ stack.set(other, object);
+
+ var skipCtor = isPartial;
+ while (++index < objLength) {
+ key = objProps[index];
+ var objValue = object[key],
+ othValue = other[key];
+
+ if (customizer) {
+ var compared = isPartial
+ ? customizer(othValue, objValue, key, other, object, stack)
+ : customizer(objValue, othValue, key, object, other, stack);
+ }
+ // Recursively compare objects (susceptible to call stack limits).
+ if (!(compared === undefined
+ ? (objValue === othValue || equalFunc(objValue, othValue, customizer, bitmask, stack))
+ : compared
+ )) {
+ result = false;
+ break;
+ }
+ skipCtor || (skipCtor = key == 'constructor');
+ }
+ if (result && !skipCtor) {
+ var objCtor = object.constructor,
+ othCtor = other.constructor;
+
+ // Non `Object` object instances with different constructors are not equal.
+ if (objCtor != othCtor &&
+ ('constructor' in object && 'constructor' in other) &&
+ !(typeof objCtor == 'function' && objCtor instanceof objCtor &&
+ typeof othCtor == 'function' && othCtor instanceof othCtor)) {
+ result = false;
+ }
+ }
+ stack['delete'](object);
+ stack['delete'](other);
+ return result;
+ }
+
+ /**
+ * A specialized version of `baseRest` which flattens the rest array.
+ *
+ * @private
+ * @param {Function} func The function to apply a rest parameter to.
+ * @returns {Function} Returns the new function.
+ */
+ function flatRest(func) {
+ return setToString(overRest(func, undefined, flatten), func + '');
+ }
+
+ /**
+ * Creates an array of own enumerable property names and symbols of `object`.
+ *
+ * @private
+ * @param {Object} object The object to query.
+ * @returns {Array} Returns the array of property names and symbols.
+ */
+ function getAllKeys(object) {
+ return baseGetAllKeys(object, keys, getSymbols);
+ }
+
+ /**
+ * Creates an array of own and inherited enumerable property names and
+ * symbols of `object`.
+ *
+ * @private
+ * @param {Object} object The object to query.
+ * @returns {Array} Returns the array of property names and symbols.
+ */
+ function getAllKeysIn(object) {
+ return baseGetAllKeys(object, keysIn, getSymbolsIn);
+ }
+
+ /**
+ * Gets metadata for `func`.
+ *
+ * @private
+ * @param {Function} func The function to query.
+ * @returns {*} Returns the metadata for `func`.
+ */
+ var getData = !metaMap ? noop : function(func) {
+ return metaMap.get(func);
+ };
+
+ /**
+ * Gets the name of `func`.
+ *
+ * @private
+ * @param {Function} func The function to query.
+ * @returns {string} Returns the function name.
+ */
+ function getFuncName(func) {
+ var result = (func.name + ''),
+ array = realNames[result],
+ length = hasOwnProperty.call(realNames, result) ? array.length : 0;
+
+ while (length--) {
+ var data = array[length],
+ otherFunc = data.func;
+ if (otherFunc == null || otherFunc == func) {
+ return data.name;
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Gets the argument placeholder value for `func`.
+ *
+ * @private
+ * @param {Function} func The function to inspect.
+ * @returns {*} Returns the placeholder value.
+ */
+ function getHolder(func) {
+ var object = hasOwnProperty.call(lodash, 'placeholder') ? lodash : func;
+ return object.placeholder;
+ }
+
+ /**
+ * Gets the appropriate "iteratee" function. If `_.iteratee` is customized,
+ * this function returns the custom method, otherwise it returns `baseIteratee`.
+ * If arguments are provided, the chosen function is invoked with them and
+ * its result is returned.
+ *
+ * @private
+ * @param {*} [value] The value to convert to an iteratee.
+ * @param {number} [arity] The arity of the created iteratee.
+ * @returns {Function} Returns the chosen function or its result.
+ */
+ function getIteratee() {
+ var result = lodash.iteratee || iteratee;
+ result = result === iteratee ? baseIteratee : result;
+ return arguments.length ? result(arguments[0], arguments[1]) : result;
+ }
+
+ /**
+ * Gets the data for `map`.
+ *
+ * @private
+ * @param {Object} map The map to query.
+ * @param {string} key The reference key.
+ * @returns {*} Returns the map data.
+ */
+ function getMapData(map, key) {
+ var data = map.__data__;
+ return isKeyable(key)
+ ? data[typeof key == 'string' ? 'string' : 'hash']
+ : data.map;
+ }
+
+ /**
+ * Gets the property names, values, and compare flags of `object`.
+ *
+ * @private
+ * @param {Object} object The object to query.
+ * @returns {Array} Returns the match data of `object`.
+ */
+ function getMatchData(object) {
+ var result = keys(object),
+ length = result.length;
+
+ while (length--) {
+ var key = result[length],
+ value = object[key];
+
+ result[length] = [key, value, isStrictComparable(value)];
+ }
+ return result;
+ }
+
+ /**
+ * Gets the native function at `key` of `object`.
+ *
+ * @private
+ * @param {Object} object The object to query.
+ * @param {string} key The key of the method to get.
+ * @returns {*} Returns the function if it's native, else `undefined`.
+ */
+ function getNative(object, key) {
+ var value = getValue(object, key);
+ return baseIsNative(value) ? value : undefined;
+ }
+
+ /**
+ * Creates an array of the own enumerable symbol properties of `object`.
+ *
+ * @private
+ * @param {Object} object The object to query.
+ * @returns {Array} Returns the array of symbols.
+ */
+ var getSymbols = nativeGetSymbols ? overArg(nativeGetSymbols, Object) : stubArray;
+
+ /**
+ * Creates an array of the own and inherited enumerable symbol properties
+ * of `object`.
+ *
+ * @private
+ * @param {Object} object The object to query.
+ * @returns {Array} Returns the array of symbols.
+ */
+ var getSymbolsIn = !nativeGetSymbols ? stubArray : function(object) {
+ var result = [];
+ while (object) {
+ arrayPush(result, getSymbols(object));
+ object = getPrototype(object);
+ }
+ return result;
+ };
+
+ /**
+ * Gets the `toStringTag` of `value`.
+ *
+ * @private
+ * @param {*} value The value to query.
+ * @returns {string} Returns the `toStringTag`.
+ */
+ var getTag = baseGetTag;
+
+ // Fallback for data views, maps, sets, and weak maps in IE 11 and promises in Node.js < 6.
+ if ((DataView && getTag(new DataView(new ArrayBuffer(1))) != dataViewTag) ||
+ (Map && getTag(new Map) != mapTag) ||
+ (Promise && getTag(Promise.resolve()) != promiseTag) ||
+ (Set && getTag(new Set) != setTag) ||
+ (WeakMap && getTag(new WeakMap) != weakMapTag)) {
+ getTag = function(value) {
+ var result = objectToString.call(value),
+ Ctor = result == objectTag ? value.constructor : undefined,
+ ctorString = Ctor ? toSource(Ctor) : undefined;
+
+ if (ctorString) {
+ switch (ctorString) {
+ case dataViewCtorString: return dataViewTag;
+ case mapCtorString: return mapTag;
+ case promiseCtorString: return promiseTag;
+ case setCtorString: return setTag;
+ case weakMapCtorString: return weakMapTag;
+ }
+ }
+ return result;
+ };
+ }
+
+ /**
+ * Gets the view, applying any `transforms` to the `start` and `end` positions.
+ *
+ * @private
+ * @param {number} start The start of the view.
+ * @param {number} end The end of the view.
+ * @param {Array} transforms The transformations to apply to the view.
+ * @returns {Object} Returns an object containing the `start` and `end`
+ * positions of the view.
+ */
+ function getView(start, end, transforms) {
+ var index = -1,
+ length = transforms.length;
+
+ while (++index < length) {
+ var data = transforms[index],
+ size = data.size;
+
+ switch (data.type) {
+ case 'drop': start += size; break;
+ case 'dropRight': end -= size; break;
+ case 'take': end = nativeMin(end, start + size); break;
+ case 'takeRight': start = nativeMax(start, end - size); break;
+ }
+ }
+ return { 'start': start, 'end': end };
+ }
+
+ /**
+ * Extracts wrapper details from the `source` body comment.
+ *
+ * @private
+ * @param {string} source The source to inspect.
+ * @returns {Array} Returns the wrapper details.
+ */
+ function getWrapDetails(source) {
+ var match = source.match(reWrapDetails);
+ return match ? match[1].split(reSplitDetails) : [];
+ }
+
+ /**
+ * Checks if `path` exists on `object`.
+ *
+ * @private
+ * @param {Object} object The object to query.
+ * @param {Array|string} path The path to check.
+ * @param {Function} hasFunc The function to check properties.
+ * @returns {boolean} Returns `true` if `path` exists, else `false`.
+ */
+ function hasPath(object, path, hasFunc) {
+ path = isKey(path, object) ? [path] : castPath(path);
+
+ var index = -1,
+ length = path.length,
+ result = false;
+
+ while (++index < length) {
+ var key = toKey(path[index]);
+ if (!(result = object != null && hasFunc(object, key))) {
+ break;
+ }
+ object = object[key];
+ }
+ if (result || ++index != length) {
+ return result;
+ }
+ length = object ? object.length : 0;
+ return !!length && isLength(length) && isIndex(key, length) &&
+ (isArray(object) || isArguments(object));
+ }
+
+ /**
+ * Initializes an array clone.
+ *
+ * @private
+ * @param {Array} array The array to clone.
+ * @returns {Array} Returns the initialized clone.
+ */
+ function initCloneArray(array) {
+ var length = array.length,
+ result = array.constructor(length);
+
+ // Add properties assigned by `RegExp#exec`.
+ if (length && typeof array[0] == 'string' && hasOwnProperty.call(array, 'index')) {
+ result.index = array.index;
+ result.input = array.input;
+ }
+ return result;
+ }
+
+ /**
+ * Initializes an object clone.
+ *
+ * @private
+ * @param {Object} object The object to clone.
+ * @returns {Object} Returns the initialized clone.
+ */
+ function initCloneObject(object) {
+ return (typeof object.constructor == 'function' && !isPrototype(object))
+ ? baseCreate(getPrototype(object))
+ : {};
+ }
+
+ /**
+ * Initializes an object clone based on its `toStringTag`.
+ *
+ * **Note:** This function only supports cloning values with tags of
+ * `Boolean`, `Date`, `Error`, `Number`, `RegExp`, or `String`.
+ *
+ * @private
+ * @param {Object} object The object to clone.
+ * @param {string} tag The `toStringTag` of the object to clone.
+ * @param {Function} cloneFunc The function to clone values.
+ * @param {boolean} [isDeep] Specify a deep clone.
+ * @returns {Object} Returns the initialized clone.
+ */
+ function initCloneByTag(object, tag, cloneFunc, isDeep) {
+ var Ctor = object.constructor;
+ switch (tag) {
+ case arrayBufferTag:
+ return cloneArrayBuffer(object);
+
+ case boolTag:
+ case dateTag:
+ return new Ctor(+object);
+
+ case dataViewTag:
+ return cloneDataView(object, isDeep);
+
+ case float32Tag: case float64Tag:
+ case int8Tag: case int16Tag: case int32Tag:
+ case uint8Tag: case uint8ClampedTag: case uint16Tag: case uint32Tag:
+ return cloneTypedArray(object, isDeep);
+
+ case mapTag:
+ return cloneMap(object, isDeep, cloneFunc);
+
+ case numberTag:
+ case stringTag:
+ return new Ctor(object);
+
+ case regexpTag:
+ return cloneRegExp(object);
+
+ case setTag:
+ return cloneSet(object, isDeep, cloneFunc);
+
+ case symbolTag:
+ return cloneSymbol(object);
+ }
+ }
+
+ /**
+ * Inserts wrapper `details` in a comment at the top of the `source` body.
+ *
+ * @private
+ * @param {string} source The source to modify.
+ * @returns {Array} details The details to insert.
+ * @returns {string} Returns the modified source.
+ */
+ function insertWrapDetails(source, details) {
+ var length = details.length;
+ if (!length) {
+ return source;
+ }
+ var lastIndex = length - 1;
+ details[lastIndex] = (length > 1 ? '& ' : '') + details[lastIndex];
+ details = details.join(length > 2 ? ', ' : ' ');
+ return source.replace(reWrapComment, '{\n/* [wrapped with ' + details + '] */\n');
+ }
+
+ /**
+ * Checks if `value` is a flattenable `arguments` object or array.
+ *
+ * @private
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is flattenable, else `false`.
+ */
+ function isFlattenable(value) {
+ return isArray(value) || isArguments(value) ||
+ !!(spreadableSymbol && value && value[spreadableSymbol]);
+ }
+
+ /**
+ * Checks if `value` is a valid array-like index.
+ *
+ * @private
+ * @param {*} value The value to check.
+ * @param {number} [length=MAX_SAFE_INTEGER] The upper bounds of a valid index.
+ * @returns {boolean} Returns `true` if `value` is a valid index, else `false`.
+ */
+ function isIndex(value, length) {
+ length = length == null ? MAX_SAFE_INTEGER : length;
+ return !!length &&
+ (typeof value == 'number' || reIsUint.test(value)) &&
+ (value > -1 && value % 1 == 0 && value < length);
+ }
+
+ /**
+ * Checks if the given arguments are from an iteratee call.
+ *
+ * @private
+ * @param {*} value The potential iteratee value argument.
+ * @param {*} index The potential iteratee index or key argument.
+ * @param {*} object The potential iteratee object argument.
+ * @returns {boolean} Returns `true` if the arguments are from an iteratee call,
+ * else `false`.
+ */
+ function isIterateeCall(value, index, object) {
+ if (!isObject(object)) {
+ return false;
+ }
+ var type = typeof index;
+ if (type == 'number'
+ ? (isArrayLike(object) && isIndex(index, object.length))
+ : (type == 'string' && index in object)
+ ) {
+ return eq(object[index], value);
+ }
+ return false;
+ }
+
+ /**
+ * Checks if `value` is a property name and not a property path.
+ *
+ * @private
+ * @param {*} value The value to check.
+ * @param {Object} [object] The object to query keys on.
+ * @returns {boolean} Returns `true` if `value` is a property name, else `false`.
+ */
+ function isKey(value, object) {
+ if (isArray(value)) {
+ return false;
+ }
+ var type = typeof value;
+ if (type == 'number' || type == 'symbol' || type == 'boolean' ||
+ value == null || isSymbol(value)) {
+ return true;
+ }
+ return reIsPlainProp.test(value) || !reIsDeepProp.test(value) ||
+ (object != null && value in Object(object));
+ }
+
+ /**
+ * Checks if `value` is suitable for use as unique object key.
+ *
+ * @private
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is suitable, else `false`.
+ */
+ function isKeyable(value) {
+ var type = typeof value;
+ return (type == 'string' || type == 'number' || type == 'symbol' || type == 'boolean')
+ ? (value !== '__proto__')
+ : (value === null);
+ }
+
+ /**
+ * Checks if `func` has a lazy counterpart.
+ *
+ * @private
+ * @param {Function} func The function to check.
+ * @returns {boolean} Returns `true` if `func` has a lazy counterpart,
+ * else `false`.
+ */
+ function isLaziable(func) {
+ var funcName = getFuncName(func),
+ other = lodash[funcName];
+
+ if (typeof other != 'function' || !(funcName in LazyWrapper.prototype)) {
+ return false;
+ }
+ if (func === other) {
+ return true;
+ }
+ var data = getData(other);
+ return !!data && func === data[0];
+ }
+
+ /**
+ * Checks if `func` has its source masked.
+ *
+ * @private
+ * @param {Function} func The function to check.
+ * @returns {boolean} Returns `true` if `func` is masked, else `false`.
+ */
+ function isMasked(func) {
+ return !!maskSrcKey && (maskSrcKey in func);
+ }
+
+ /**
+ * Checks if `func` is capable of being masked.
+ *
+ * @private
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `func` is maskable, else `false`.
+ */
+ var isMaskable = coreJsData ? isFunction : stubFalse;
+
+ /**
+ * Checks if `value` is likely a prototype object.
+ *
+ * @private
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is a prototype, else `false`.
+ */
+ function isPrototype(value) {
+ var Ctor = value && value.constructor,
+ proto = (typeof Ctor == 'function' && Ctor.prototype) || objectProto;
+
+ return value === proto;
+ }
+
+ /**
+ * Checks if `value` is suitable for strict equality comparisons, i.e. `===`.
+ *
+ * @private
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` if suitable for strict
+ * equality comparisons, else `false`.
+ */
+ function isStrictComparable(value) {
+ return value === value && !isObject(value);
+ }
+
+ /**
+ * A specialized version of `matchesProperty` for source values suitable
+ * for strict equality comparisons, i.e. `===`.
+ *
+ * @private
+ * @param {string} key The key of the property to get.
+ * @param {*} srcValue The value to match.
+ * @returns {Function} Returns the new spec function.
+ */
+ function matchesStrictComparable(key, srcValue) {
+ return function(object) {
+ if (object == null) {
+ return false;
+ }
+ return object[key] === srcValue &&
+ (srcValue !== undefined || (key in Object(object)));
+ };
+ }
+
+ /**
+ * A specialized version of `_.memoize` which clears the memoized function's
+ * cache when it exceeds `MAX_MEMOIZE_SIZE`.
+ *
+ * @private
+ * @param {Function} func The function to have its output memoized.
+ * @returns {Function} Returns the new memoized function.
+ */
+ function memoizeCapped(func) {
+ var result = memoize(func, function(key) {
+ if (cache.size === MAX_MEMOIZE_SIZE) {
+ cache.clear();
+ }
+ return key;
+ });
+
+ var cache = result.cache;
+ return result;
+ }
+
+ /**
+ * Merges the function metadata of `source` into `data`.
+ *
+ * Merging metadata reduces the number of wrappers used to invoke a function.
+ * This is possible because methods like `_.bind`, `_.curry`, and `_.partial`
+ * may be applied regardless of execution order. Methods like `_.ary` and
+ * `_.rearg` modify function arguments, making the order in which they are
+ * executed important, preventing the merging of metadata. However, we make
+ * an exception for a safe combined case where curried functions have `_.ary`
+ * and or `_.rearg` applied.
+ *
+ * @private
+ * @param {Array} data The destination metadata.
+ * @param {Array} source The source metadata.
+ * @returns {Array} Returns `data`.
+ */
+ function mergeData(data, source) {
+ var bitmask = data[1],
+ srcBitmask = source[1],
+ newBitmask = bitmask | srcBitmask,
+ isCommon = newBitmask < (BIND_FLAG | BIND_KEY_FLAG | ARY_FLAG);
+
+ var isCombo =
+ ((srcBitmask == ARY_FLAG) && (bitmask == CURRY_FLAG)) ||
+ ((srcBitmask == ARY_FLAG) && (bitmask == REARG_FLAG) && (data[7].length <= source[8])) ||
+ ((srcBitmask == (ARY_FLAG | REARG_FLAG)) && (source[7].length <= source[8]) && (bitmask == CURRY_FLAG));
+
+ // Exit early if metadata can't be merged.
+ if (!(isCommon || isCombo)) {
+ return data;
+ }
+ // Use source `thisArg` if available.
+ if (srcBitmask & BIND_FLAG) {
+ data[2] = source[2];
+ // Set when currying a bound function.
+ newBitmask |= bitmask & BIND_FLAG ? 0 : CURRY_BOUND_FLAG;
+ }
+ // Compose partial arguments.
+ var value = source[3];
+ if (value) {
+ var partials = data[3];
+ data[3] = partials ? composeArgs(partials, value, source[4]) : value;
+ data[4] = partials ? replaceHolders(data[3], PLACEHOLDER) : source[4];
+ }
+ // Compose partial right arguments.
+ value = source[5];
+ if (value) {
+ partials = data[5];
+ data[5] = partials ? composeArgsRight(partials, value, source[6]) : value;
+ data[6] = partials ? replaceHolders(data[5], PLACEHOLDER) : source[6];
+ }
+ // Use source `argPos` if available.
+ value = source[7];
+ if (value) {
+ data[7] = value;
+ }
+ // Use source `ary` if it's smaller.
+ if (srcBitmask & ARY_FLAG) {
+ data[8] = data[8] == null ? source[8] : nativeMin(data[8], source[8]);
+ }
+ // Use source `arity` if one is not provided.
+ if (data[9] == null) {
+ data[9] = source[9];
+ }
+ // Use source `func` and merge bitmasks.
+ data[0] = source[0];
+ data[1] = newBitmask;
+
+ return data;
+ }
+
+ /**
+ * Used by `_.defaultsDeep` to customize its `_.merge` use.
+ *
+ * @private
+ * @param {*} objValue The destination value.
+ * @param {*} srcValue The source value.
+ * @param {string} key The key of the property to merge.
+ * @param {Object} object The parent object of `objValue`.
+ * @param {Object} source The parent object of `srcValue`.
+ * @param {Object} [stack] Tracks traversed source values and their merged
+ * counterparts.
+ * @returns {*} Returns the value to assign.
+ */
+ function mergeDefaults(objValue, srcValue, key, object, source, stack) {
+ if (isObject(objValue) && isObject(srcValue)) {
+ // Recursively merge objects and arrays (susceptible to call stack limits).
+ stack.set(srcValue, objValue);
+ baseMerge(objValue, srcValue, undefined, mergeDefaults, stack);
+ stack['delete'](srcValue);
+ }
+ return objValue;
+ }
+
+ /**
+ * This function is like
+ * [`Object.keys`](http://ecma-international.org/ecma-262/7.0/#sec-object.keys)
+ * except that it includes inherited enumerable properties.
+ *
+ * @private
+ * @param {Object} object The object to query.
+ * @returns {Array} Returns the array of property names.
+ */
+ function nativeKeysIn(object) {
+ var result = [];
+ if (object != null) {
+ for (var key in Object(object)) {
+ result.push(key);
+ }
+ }
+ return result;
+ }
+
+ /**
+ * A specialized version of `baseRest` which transforms the rest array.
+ *
+ * @private
+ * @param {Function} func The function to apply a rest parameter to.
+ * @param {number} [start=func.length-1] The start position of the rest parameter.
+ * @param {Function} transform The rest array transform.
+ * @returns {Function} Returns the new function.
+ */
+ function overRest(func, start, transform) {
+ start = nativeMax(start === undefined ? (func.length - 1) : start, 0);
+ return function() {
+ var args = arguments,
+ index = -1,
+ length = nativeMax(args.length - start, 0),
+ array = Array(length);
+
+ while (++index < length) {
+ array[index] = args[start + index];
+ }
+ index = -1;
+ var otherArgs = Array(start + 1);
+ while (++index < start) {
+ otherArgs[index] = args[index];
+ }
+ otherArgs[start] = transform(array);
+ return apply(func, this, otherArgs);
+ };
+ }
+
+ /**
+ * Gets the parent value at `path` of `object`.
+ *
+ * @private
+ * @param {Object} object The object to query.
+ * @param {Array} path The path to get the parent value of.
+ * @returns {*} Returns the parent value.
+ */
+ function parent(object, path) {
+ return path.length == 1 ? object : baseGet(object, baseSlice(path, 0, -1));
+ }
+
+ /**
+ * Reorder `array` according to the specified indexes where the element at
+ * the first index is assigned as the first element, the element at
+ * the second index is assigned as the second element, and so on.
+ *
+ * @private
+ * @param {Array} array The array to reorder.
+ * @param {Array} indexes The arranged array indexes.
+ * @returns {Array} Returns `array`.
+ */
+ function reorder(array, indexes) {
+ var arrLength = array.length,
+ length = nativeMin(indexes.length, arrLength),
+ oldArray = copyArray(array);
+
+ while (length--) {
+ var index = indexes[length];
+ array[length] = isIndex(index, arrLength) ? oldArray[index] : undefined;
+ }
+ return array;
+ }
+
+ /**
+ * Sets metadata for `func`.
+ *
+ * **Note:** If this function becomes hot, i.e. is invoked a lot in a short
+ * period of time, it will trip its breaker and transition to an identity
+ * function to avoid garbage collection pauses in V8. See
+ * [V8 issue 2070](https://bugs.chromium.org/p/v8/issues/detail?id=2070)
+ * for more details.
+ *
+ * @private
+ * @param {Function} func The function to associate metadata with.
+ * @param {*} data The metadata.
+ * @returns {Function} Returns `func`.
+ */
+ var setData = shortOut(baseSetData);
+
+ /**
+ * A simple wrapper around the global [`setTimeout`](https://mdn.io/setTimeout).
+ *
+ * @private
+ * @param {Function} func The function to delay.
+ * @param {number} wait The number of milliseconds to delay invocation.
+ * @returns {number|Object} Returns the timer id or timeout object.
+ */
+ var setTimeout = ctxSetTimeout || function(func, wait) {
+ return root.setTimeout(func, wait);
+ };
+
+ /**
+ * Sets the `toString` method of `func` to return `string`.
+ *
+ * @private
+ * @param {Function} func The function to modify.
+ * @param {Function} string The `toString` result.
+ * @returns {Function} Returns `func`.
+ */
+ var setToString = shortOut(baseSetToString);
+
+ /**
+ * Sets the `toString` method of `wrapper` to mimic the source of `reference`
+ * with wrapper details in a comment at the top of the source body.
+ *
+ * @private
+ * @param {Function} wrapper The function to modify.
+ * @param {Function} reference The reference function.
+ * @param {number} bitmask The bitmask flags. See `createWrap` for more details.
+ * @returns {Function} Returns `wrapper`.
+ */
+ function setWrapToString(wrapper, reference, bitmask) {
+ var source = (reference + '');
+ return setToString(wrapper, insertWrapDetails(source, updateWrapDetails(getWrapDetails(source), bitmask)));
+ }
+
+ /**
+ * Creates a function that'll short out and invoke `identity` instead
+ * of `func` when it's called `HOT_COUNT` or more times in `HOT_SPAN`
+ * milliseconds.
+ *
+ * @private
+ * @param {Function} func The function to restrict.
+ * @returns {Function} Returns the new shortable function.
+ */
+ function shortOut(func) {
+ var count = 0,
+ lastCalled = 0;
+
+ return function() {
+ var stamp = nativeNow(),
+ remaining = HOT_SPAN - (stamp - lastCalled);
+
+ lastCalled = stamp;
+ if (remaining > 0) {
+ if (++count >= HOT_COUNT) {
+ return arguments[0];
+ }
+ } else {
+ count = 0;
+ }
+ return func.apply(undefined, arguments);
+ };
+ }
+
+ /**
+ * A specialized version of `_.shuffle` which mutates and sets the size of `array`.
+ *
+ * @private
+ * @param {Array} array The array to shuffle.
+ * @param {number} [size=array.length] The size of `array`.
+ * @returns {Array} Returns `array`.
+ */
+ function shuffleSelf(array, size) {
+ var index = -1,
+ length = array.length,
+ lastIndex = length - 1;
+
+ size = size === undefined ? length : size;
+ while (++index < size) {
+ var rand = baseRandom(index, lastIndex),
+ value = array[rand];
+
+ array[rand] = array[index];
+ array[index] = value;
+ }
+ array.length = size;
+ return array;
+ }
+
+ /**
+ * Converts `string` to a property path array.
+ *
+ * @private
+ * @param {string} string The string to convert.
+ * @returns {Array} Returns the property path array.
+ */
+ var stringToPath = memoizeCapped(function(string) {
+ string = toString(string);
+
+ var result = [];
+ if (reLeadingDot.test(string)) {
+ result.push('');
+ }
+ string.replace(rePropName, function(match, number, quote, string) {
+ result.push(quote ? string.replace(reEscapeChar, '$1') : (number || match));
+ });
+ return result;
+ });
+
+ /**
+ * Converts `value` to a string key if it's not a string or symbol.
+ *
+ * @private
+ * @param {*} value The value to inspect.
+ * @returns {string|symbol} Returns the key.
+ */
+ function toKey(value) {
+ if (typeof value == 'string' || isSymbol(value)) {
+ return value;
+ }
+ var result = (value + '');
+ return (result == '0' && (1 / value) == -INFINITY) ? '-0' : result;
+ }
+
+ /**
+ * Converts `func` to its source code.
+ *
+ * @private
+ * @param {Function} func The function to process.
+ * @returns {string} Returns the source code.
+ */
+ function toSource(func) {
+ if (func != null) {
+ try {
+ return funcToString.call(func);
+ } catch (e) {}
+ try {
+ return (func + '');
+ } catch (e) {}
+ }
+ return '';
+ }
+
+ /**
+ * Updates wrapper `details` based on `bitmask` flags.
+ *
+ * @private
+ * @returns {Array} details The details to modify.
+ * @param {number} bitmask The bitmask flags. See `createWrap` for more details.
+ * @returns {Array} Returns `details`.
+ */
+ function updateWrapDetails(details, bitmask) {
+ arrayEach(wrapFlags, function(pair) {
+ var value = '_.' + pair[0];
+ if ((bitmask & pair[1]) && !arrayIncludes(details, value)) {
+ details.push(value);
+ }
+ });
+ return details.sort();
+ }
+
+ /**
+ * Creates a clone of `wrapper`.
+ *
+ * @private
+ * @param {Object} wrapper The wrapper to clone.
+ * @returns {Object} Returns the cloned wrapper.
+ */
+ function wrapperClone(wrapper) {
+ if (wrapper instanceof LazyWrapper) {
+ return wrapper.clone();
+ }
+ var result = new LodashWrapper(wrapper.__wrapped__, wrapper.__chain__);
+ result.__actions__ = copyArray(wrapper.__actions__);
+ result.__index__ = wrapper.__index__;
+ result.__values__ = wrapper.__values__;
+ return result;
+ }
+
+ /*------------------------------------------------------------------------*/
+
+ /**
+ * Creates an array of elements split into groups the length of `size`.
+ * If `array` can't be split evenly, the final chunk will be the remaining
+ * elements.
+ *
+ * @static
+ * @memberOf _
+ * @since 3.0.0
+ * @category Array
+ * @param {Array} array The array to process.
+ * @param {number} [size=1] The length of each chunk
+ * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.
+ * @returns {Array} Returns the new array of chunks.
+ * @example
+ *
+ * _.chunk(['a', 'b', 'c', 'd'], 2);
+ * // => [['a', 'b'], ['c', 'd']]
+ *
+ * _.chunk(['a', 'b', 'c', 'd'], 3);
+ * // => [['a', 'b', 'c'], ['d']]
+ */
+ function chunk(array, size, guard) {
+ if ((guard ? isIterateeCall(array, size, guard) : size === undefined)) {
+ size = 1;
+ } else {
+ size = nativeMax(toInteger(size), 0);
+ }
+ var length = array ? array.length : 0;
+ if (!length || size < 1) {
+ return [];
+ }
+ var index = 0,
+ resIndex = 0,
+ result = Array(nativeCeil(length / size));
+
+ while (index < length) {
+ result[resIndex++] = baseSlice(array, index, (index += size));
+ }
+ return result;
+ }
+
+ /**
+ * Creates an array with all falsey values removed. The values `false`, `null`,
+ * `0`, `""`, `undefined`, and `NaN` are falsey.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.1.0
+ * @category Array
+ * @param {Array} array The array to compact.
+ * @returns {Array} Returns the new array of filtered values.
+ * @example
+ *
+ * _.compact([0, 1, false, 2, '', 3]);
+ * // => [1, 2, 3]
+ */
+ function compact(array) {
+ var index = -1,
+ length = array ? array.length : 0,
+ resIndex = 0,
+ result = [];
+
+ while (++index < length) {
+ var value = array[index];
+ if (value) {
+ result[resIndex++] = value;
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Creates a new array concatenating `array` with any additional arrays
+ * and/or values.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Array
+ * @param {Array} array The array to concatenate.
+ * @param {...*} [values] The values to concatenate.
+ * @returns {Array} Returns the new concatenated array.
+ * @example
+ *
+ * var array = [1];
+ * var other = _.concat(array, 2, [3], [[4]]);
+ *
+ * console.log(other);
+ * // => [1, 2, 3, [4]]
+ *
+ * console.log(array);
+ * // => [1]
+ */
+ function concat() {
+ var length = arguments.length;
+ if (!length) {
+ return [];
+ }
+ var args = Array(length - 1),
+ array = arguments[0],
+ index = length;
+
+ while (index--) {
+ args[index - 1] = arguments[index];
+ }
+ return arrayPush(isArray(array) ? copyArray(array) : [array], baseFlatten(args, 1));
+ }
+
+ /**
+ * Creates an array of `array` values not included in the other given arrays
+ * using [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)
+ * for equality comparisons. The order and references of result values are
+ * determined by the first array.
+ *
+ * **Note:** Unlike `_.pullAll`, this method returns a new array.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.1.0
+ * @category Array
+ * @param {Array} array The array to inspect.
+ * @param {...Array} [values] The values to exclude.
+ * @returns {Array} Returns the new array of filtered values.
+ * @see _.without, _.xor
+ * @example
+ *
+ * _.difference([2, 1], [2, 3]);
+ * // => [1]
+ */
+ var difference = baseRest(function(array, values) {
+ return isArrayLikeObject(array)
+ ? baseDifference(array, baseFlatten(values, 1, isArrayLikeObject, true))
+ : [];
+ });
+
+ /**
+ * This method is like `_.difference` except that it accepts `iteratee` which
+ * is invoked for each element of `array` and `values` to generate the criterion
+ * by which they're compared. The order and references of result values are
+ * determined by the first array. The iteratee is invoked with one argument:
+ * (value).
+ *
+ * **Note:** Unlike `_.pullAllBy`, this method returns a new array.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Array
+ * @param {Array} array The array to inspect.
+ * @param {...Array} [values] The values to exclude.
+ * @param {Function} [iteratee=_.identity] The iteratee invoked per element.
+ * @returns {Array} Returns the new array of filtered values.
+ * @example
+ *
+ * _.differenceBy([2.1, 1.2], [2.3, 3.4], Math.floor);
+ * // => [1.2]
+ *
+ * // The `_.property` iteratee shorthand.
+ * _.differenceBy([{ 'x': 2 }, { 'x': 1 }], [{ 'x': 1 }], 'x');
+ * // => [{ 'x': 2 }]
+ */
+ var differenceBy = baseRest(function(array, values) {
+ var iteratee = last(values);
+ if (isArrayLikeObject(iteratee)) {
+ iteratee = undefined;
+ }
+ return isArrayLikeObject(array)
+ ? baseDifference(array, baseFlatten(values, 1, isArrayLikeObject, true), getIteratee(iteratee, 2))
+ : [];
+ });
+
+ /**
+ * This method is like `_.difference` except that it accepts `comparator`
+ * which is invoked to compare elements of `array` to `values`. The order and
+ * references of result values are determined by the first array. The comparator
+ * is invoked with two arguments: (arrVal, othVal).
+ *
+ * **Note:** Unlike `_.pullAllWith`, this method returns a new array.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Array
+ * @param {Array} array The array to inspect.
+ * @param {...Array} [values] The values to exclude.
+ * @param {Function} [comparator] The comparator invoked per element.
+ * @returns {Array} Returns the new array of filtered values.
+ * @example
+ *
+ * var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }];
+ *
+ * _.differenceWith(objects, [{ 'x': 1, 'y': 2 }], _.isEqual);
+ * // => [{ 'x': 2, 'y': 1 }]
+ */
+ var differenceWith = baseRest(function(array, values) {
+ var comparator = last(values);
+ if (isArrayLikeObject(comparator)) {
+ comparator = undefined;
+ }
+ return isArrayLikeObject(array)
+ ? baseDifference(array, baseFlatten(values, 1, isArrayLikeObject, true), undefined, comparator)
+ : [];
+ });
+
+ /**
+ * Creates a slice of `array` with `n` elements dropped from the beginning.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.5.0
+ * @category Array
+ * @param {Array} array The array to query.
+ * @param {number} [n=1] The number of elements to drop.
+ * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.
+ * @returns {Array} Returns the slice of `array`.
+ * @example
+ *
+ * _.drop([1, 2, 3]);
+ * // => [2, 3]
+ *
+ * _.drop([1, 2, 3], 2);
+ * // => [3]
+ *
+ * _.drop([1, 2, 3], 5);
+ * // => []
+ *
+ * _.drop([1, 2, 3], 0);
+ * // => [1, 2, 3]
+ */
+ function drop(array, n, guard) {
+ var length = array ? array.length : 0;
+ if (!length) {
+ return [];
+ }
+ n = (guard || n === undefined) ? 1 : toInteger(n);
+ return baseSlice(array, n < 0 ? 0 : n, length);
+ }
+
+ /**
+ * Creates a slice of `array` with `n` elements dropped from the end.
+ *
+ * @static
+ * @memberOf _
+ * @since 3.0.0
+ * @category Array
+ * @param {Array} array The array to query.
+ * @param {number} [n=1] The number of elements to drop.
+ * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.
+ * @returns {Array} Returns the slice of `array`.
+ * @example
+ *
+ * _.dropRight([1, 2, 3]);
+ * // => [1, 2]
+ *
+ * _.dropRight([1, 2, 3], 2);
+ * // => [1]
+ *
+ * _.dropRight([1, 2, 3], 5);
+ * // => []
+ *
+ * _.dropRight([1, 2, 3], 0);
+ * // => [1, 2, 3]
+ */
+ function dropRight(array, n, guard) {
+ var length = array ? array.length : 0;
+ if (!length) {
+ return [];
+ }
+ n = (guard || n === undefined) ? 1 : toInteger(n);
+ n = length - n;
+ return baseSlice(array, 0, n < 0 ? 0 : n);
+ }
+
+ /**
+ * Creates a slice of `array` excluding elements dropped from the end.
+ * Elements are dropped until `predicate` returns falsey. The predicate is
+ * invoked with three arguments: (value, index, array).
+ *
+ * @static
+ * @memberOf _
+ * @since 3.0.0
+ * @category Array
+ * @param {Array} array The array to query.
+ * @param {Function} [predicate=_.identity] The function invoked per iteration.
+ * @returns {Array} Returns the slice of `array`.
+ * @example
+ *
+ * var users = [
+ * { 'user': 'barney', 'active': true },
+ * { 'user': 'fred', 'active': false },
+ * { 'user': 'pebbles', 'active': false }
+ * ];
+ *
+ * _.dropRightWhile(users, function(o) { return !o.active; });
+ * // => objects for ['barney']
+ *
+ * // The `_.matches` iteratee shorthand.
+ * _.dropRightWhile(users, { 'user': 'pebbles', 'active': false });
+ * // => objects for ['barney', 'fred']
+ *
+ * // The `_.matchesProperty` iteratee shorthand.
+ * _.dropRightWhile(users, ['active', false]);
+ * // => objects for ['barney']
+ *
+ * // The `_.property` iteratee shorthand.
+ * _.dropRightWhile(users, 'active');
+ * // => objects for ['barney', 'fred', 'pebbles']
+ */
+ function dropRightWhile(array, predicate) {
+ return (array && array.length)
+ ? baseWhile(array, getIteratee(predicate, 3), true, true)
+ : [];
+ }
+
+ /**
+ * Creates a slice of `array` excluding elements dropped from the beginning.
+ * Elements are dropped until `predicate` returns falsey. The predicate is
+ * invoked with three arguments: (value, index, array).
+ *
+ * @static
+ * @memberOf _
+ * @since 3.0.0
+ * @category Array
+ * @param {Array} array The array to query.
+ * @param {Function} [predicate=_.identity]
+ * The function invoked per iteration.
+ * @returns {Array} Returns the slice of `array`.
+ * @example
+ *
+ * var users = [
+ * { 'user': 'barney', 'active': false },
+ * { 'user': 'fred', 'active': false },
+ * { 'user': 'pebbles', 'active': true }
+ * ];
+ *
+ * _.dropWhile(users, function(o) { return !o.active; });
+ * // => objects for ['pebbles']
+ *
+ * // The `_.matches` iteratee shorthand.
+ * _.dropWhile(users, { 'user': 'barney', 'active': false });
+ * // => objects for ['fred', 'pebbles']
+ *
+ * // The `_.matchesProperty` iteratee shorthand.
+ * _.dropWhile(users, ['active', false]);
+ * // => objects for ['pebbles']
+ *
+ * // The `_.property` iteratee shorthand.
+ * _.dropWhile(users, 'active');
+ * // => objects for ['barney', 'fred', 'pebbles']
+ */
+ function dropWhile(array, predicate) {
+ return (array && array.length)
+ ? baseWhile(array, getIteratee(predicate, 3), true)
+ : [];
+ }
+
+ /**
+ * Fills elements of `array` with `value` from `start` up to, but not
+ * including, `end`.
+ *
+ * **Note:** This method mutates `array`.
+ *
+ * @static
+ * @memberOf _
+ * @since 3.2.0
+ * @category Array
+ * @param {Array} array The array to fill.
+ * @param {*} value The value to fill `array` with.
+ * @param {number} [start=0] The start position.
+ * @param {number} [end=array.length] The end position.
+ * @returns {Array} Returns `array`.
+ * @example
+ *
+ * var array = [1, 2, 3];
+ *
+ * _.fill(array, 'a');
+ * console.log(array);
+ * // => ['a', 'a', 'a']
+ *
+ * _.fill(Array(3), 2);
+ * // => [2, 2, 2]
+ *
+ * _.fill([4, 6, 8, 10], '*', 1, 3);
+ * // => [4, '*', '*', 10]
+ */
+ function fill(array, value, start, end) {
+ var length = array ? array.length : 0;
+ if (!length) {
+ return [];
+ }
+ if (start && typeof start != 'number' && isIterateeCall(array, value, start)) {
+ start = 0;
+ end = length;
+ }
+ return baseFill(array, value, start, end);
+ }
+
+ /**
+ * This method is like `_.find` except that it returns the index of the first
+ * element `predicate` returns truthy for instead of the element itself.
+ *
+ * @static
+ * @memberOf _
+ * @since 1.1.0
+ * @category Array
+ * @param {Array} array The array to inspect.
+ * @param {Function} [predicate=_.identity]
+ * The function invoked per iteration.
+ * @param {number} [fromIndex=0] The index to search from.
+ * @returns {number} Returns the index of the found element, else `-1`.
+ * @example
+ *
+ * var users = [
+ * { 'user': 'barney', 'active': false },
+ * { 'user': 'fred', 'active': false },
+ * { 'user': 'pebbles', 'active': true }
+ * ];
+ *
+ * _.findIndex(users, function(o) { return o.user == 'barney'; });
+ * // => 0
+ *
+ * // The `_.matches` iteratee shorthand.
+ * _.findIndex(users, { 'user': 'fred', 'active': false });
+ * // => 1
+ *
+ * // The `_.matchesProperty` iteratee shorthand.
+ * _.findIndex(users, ['active', false]);
+ * // => 0
+ *
+ * // The `_.property` iteratee shorthand.
+ * _.findIndex(users, 'active');
+ * // => 2
+ */
+ function findIndex(array, predicate, fromIndex) {
+ var length = array ? array.length : 0;
+ if (!length) {
+ return -1;
+ }
+ var index = fromIndex == null ? 0 : toInteger(fromIndex);
+ if (index < 0) {
+ index = nativeMax(length + index, 0);
+ }
+ return baseFindIndex(array, getIteratee(predicate, 3), index);
+ }
+
+ /**
+ * This method is like `_.findIndex` except that it iterates over elements
+ * of `collection` from right to left.
+ *
+ * @static
+ * @memberOf _
+ * @since 2.0.0
+ * @category Array
+ * @param {Array} array The array to inspect.
+ * @param {Function} [predicate=_.identity]
+ * The function invoked per iteration.
+ * @param {number} [fromIndex=array.length-1] The index to search from.
+ * @returns {number} Returns the index of the found element, else `-1`.
+ * @example
+ *
+ * var users = [
+ * { 'user': 'barney', 'active': true },
+ * { 'user': 'fred', 'active': false },
+ * { 'user': 'pebbles', 'active': false }
+ * ];
+ *
+ * _.findLastIndex(users, function(o) { return o.user == 'pebbles'; });
+ * // => 2
+ *
+ * // The `_.matches` iteratee shorthand.
+ * _.findLastIndex(users, { 'user': 'barney', 'active': true });
+ * // => 0
+ *
+ * // The `_.matchesProperty` iteratee shorthand.
+ * _.findLastIndex(users, ['active', false]);
+ * // => 2
+ *
+ * // The `_.property` iteratee shorthand.
+ * _.findLastIndex(users, 'active');
+ * // => 0
+ */
+ function findLastIndex(array, predicate, fromIndex) {
+ var length = array ? array.length : 0;
+ if (!length) {
+ return -1;
+ }
+ var index = length - 1;
+ if (fromIndex !== undefined) {
+ index = toInteger(fromIndex);
+ index = fromIndex < 0
+ ? nativeMax(length + index, 0)
+ : nativeMin(index, length - 1);
+ }
+ return baseFindIndex(array, getIteratee(predicate, 3), index, true);
+ }
+
+ /**
+ * Flattens `array` a single level deep.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.1.0
+ * @category Array
+ * @param {Array} array The array to flatten.
+ * @returns {Array} Returns the new flattened array.
+ * @example
+ *
+ * _.flatten([1, [2, [3, [4]], 5]]);
+ * // => [1, 2, [3, [4]], 5]
+ */
+ function flatten(array) {
+ var length = array ? array.length : 0;
+ return length ? baseFlatten(array, 1) : [];
+ }
+
+ /**
+ * Recursively flattens `array`.
+ *
+ * @static
+ * @memberOf _
+ * @since 3.0.0
+ * @category Array
+ * @param {Array} array The array to flatten.
+ * @returns {Array} Returns the new flattened array.
+ * @example
+ *
+ * _.flattenDeep([1, [2, [3, [4]], 5]]);
+ * // => [1, 2, 3, 4, 5]
+ */
+ function flattenDeep(array) {
+ var length = array ? array.length : 0;
+ return length ? baseFlatten(array, INFINITY) : [];
+ }
+
+ /**
+ * Recursively flatten `array` up to `depth` times.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.4.0
+ * @category Array
+ * @param {Array} array The array to flatten.
+ * @param {number} [depth=1] The maximum recursion depth.
+ * @returns {Array} Returns the new flattened array.
+ * @example
+ *
+ * var array = [1, [2, [3, [4]], 5]];
+ *
+ * _.flattenDepth(array, 1);
+ * // => [1, 2, [3, [4]], 5]
+ *
+ * _.flattenDepth(array, 2);
+ * // => [1, 2, 3, [4], 5]
+ */
+ function flattenDepth(array, depth) {
+ var length = array ? array.length : 0;
+ if (!length) {
+ return [];
+ }
+ depth = depth === undefined ? 1 : toInteger(depth);
+ return baseFlatten(array, depth);
+ }
+
+ /**
+ * The inverse of `_.toPairs`; this method returns an object composed
+ * from key-value `pairs`.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Array
+ * @param {Array} pairs The key-value pairs.
+ * @returns {Object} Returns the new object.
+ * @example
+ *
+ * _.fromPairs([['a', 1], ['b', 2]]);
+ * // => { 'a': 1, 'b': 2 }
+ */
+ function fromPairs(pairs) {
+ var index = -1,
+ length = pairs ? pairs.length : 0,
+ result = {};
+
+ while (++index < length) {
+ var pair = pairs[index];
+ result[pair[0]] = pair[1];
+ }
+ return result;
+ }
+
+ /**
+ * Gets the first element of `array`.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.1.0
+ * @alias first
+ * @category Array
+ * @param {Array} array The array to query.
+ * @returns {*} Returns the first element of `array`.
+ * @example
+ *
+ * _.head([1, 2, 3]);
+ * // => 1
+ *
+ * _.head([]);
+ * // => undefined
+ */
+ function head(array) {
+ return (array && array.length) ? array[0] : undefined;
+ }
+
+ /**
+ * Gets the index at which the first occurrence of `value` is found in `array`
+ * using [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)
+ * for equality comparisons. If `fromIndex` is negative, it's used as the
+ * offset from the end of `array`.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.1.0
+ * @category Array
+ * @param {Array} array The array to inspect.
+ * @param {*} value The value to search for.
+ * @param {number} [fromIndex=0] The index to search from.
+ * @returns {number} Returns the index of the matched value, else `-1`.
+ * @example
+ *
+ * _.indexOf([1, 2, 1, 2], 2);
+ * // => 1
+ *
+ * // Search from the `fromIndex`.
+ * _.indexOf([1, 2, 1, 2], 2, 2);
+ * // => 3
+ */
+ function indexOf(array, value, fromIndex) {
+ var length = array ? array.length : 0;
+ if (!length) {
+ return -1;
+ }
+ var index = fromIndex == null ? 0 : toInteger(fromIndex);
+ if (index < 0) {
+ index = nativeMax(length + index, 0);
+ }
+ return baseIndexOf(array, value, index);
+ }
+
+ /**
+ * Gets all but the last element of `array`.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.1.0
+ * @category Array
+ * @param {Array} array The array to query.
+ * @returns {Array} Returns the slice of `array`.
+ * @example
+ *
+ * _.initial([1, 2, 3]);
+ * // => [1, 2]
+ */
+ function initial(array) {
+ var length = array ? array.length : 0;
+ return length ? baseSlice(array, 0, -1) : [];
+ }
+
+ /**
+ * Creates an array of unique values that are included in all given arrays
+ * using [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)
+ * for equality comparisons. The order and references of result values are
+ * determined by the first array.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.1.0
+ * @category Array
+ * @param {...Array} [arrays] The arrays to inspect.
+ * @returns {Array} Returns the new array of intersecting values.
+ * @example
+ *
+ * _.intersection([2, 1], [2, 3]);
+ * // => [2]
+ */
+ var intersection = baseRest(function(arrays) {
+ var mapped = arrayMap(arrays, castArrayLikeObject);
+ return (mapped.length && mapped[0] === arrays[0])
+ ? baseIntersection(mapped)
+ : [];
+ });
+
+ /**
+ * This method is like `_.intersection` except that it accepts `iteratee`
+ * which is invoked for each element of each `arrays` to generate the criterion
+ * by which they're compared. The order and references of result values are
+ * determined by the first array. The iteratee is invoked with one argument:
+ * (value).
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Array
+ * @param {...Array} [arrays] The arrays to inspect.
+ * @param {Function} [iteratee=_.identity] The iteratee invoked per element.
+ * @returns {Array} Returns the new array of intersecting values.
+ * @example
+ *
+ * _.intersectionBy([2.1, 1.2], [2.3, 3.4], Math.floor);
+ * // => [2.1]
+ *
+ * // The `_.property` iteratee shorthand.
+ * _.intersectionBy([{ 'x': 1 }], [{ 'x': 2 }, { 'x': 1 }], 'x');
+ * // => [{ 'x': 1 }]
+ */
+ var intersectionBy = baseRest(function(arrays) {
+ var iteratee = last(arrays),
+ mapped = arrayMap(arrays, castArrayLikeObject);
+
+ if (iteratee === last(mapped)) {
+ iteratee = undefined;
+ } else {
+ mapped.pop();
+ }
+ return (mapped.length && mapped[0] === arrays[0])
+ ? baseIntersection(mapped, getIteratee(iteratee, 2))
+ : [];
+ });
+
+ /**
+ * This method is like `_.intersection` except that it accepts `comparator`
+ * which is invoked to compare elements of `arrays`. The order and references
+ * of result values are determined by the first array. The comparator is
+ * invoked with two arguments: (arrVal, othVal).
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Array
+ * @param {...Array} [arrays] The arrays to inspect.
+ * @param {Function} [comparator] The comparator invoked per element.
+ * @returns {Array} Returns the new array of intersecting values.
+ * @example
+ *
+ * var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }];
+ * var others = [{ 'x': 1, 'y': 1 }, { 'x': 1, 'y': 2 }];
+ *
+ * _.intersectionWith(objects, others, _.isEqual);
+ * // => [{ 'x': 1, 'y': 2 }]
+ */
+ var intersectionWith = baseRest(function(arrays) {
+ var comparator = last(arrays),
+ mapped = arrayMap(arrays, castArrayLikeObject);
+
+ if (comparator === last(mapped)) {
+ comparator = undefined;
+ } else {
+ mapped.pop();
+ }
+ return (mapped.length && mapped[0] === arrays[0])
+ ? baseIntersection(mapped, undefined, comparator)
+ : [];
+ });
+
+ /**
+ * Converts all elements in `array` into a string separated by `separator`.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Array
+ * @param {Array} array The array to convert.
+ * @param {string} [separator=','] The element separator.
+ * @returns {string} Returns the joined string.
+ * @example
+ *
+ * _.join(['a', 'b', 'c'], '~');
+ * // => 'a~b~c'
+ */
+ function join(array, separator) {
+ return array ? nativeJoin.call(array, separator) : '';
+ }
+
+ /**
+ * Gets the last element of `array`.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.1.0
+ * @category Array
+ * @param {Array} array The array to query.
+ * @returns {*} Returns the last element of `array`.
+ * @example
+ *
+ * _.last([1, 2, 3]);
+ * // => 3
+ */
+ function last(array) {
+ var length = array ? array.length : 0;
+ return length ? array[length - 1] : undefined;
+ }
+
+ /**
+ * This method is like `_.indexOf` except that it iterates over elements of
+ * `array` from right to left.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.1.0
+ * @category Array
+ * @param {Array} array The array to inspect.
+ * @param {*} value The value to search for.
+ * @param {number} [fromIndex=array.length-1] The index to search from.
+ * @returns {number} Returns the index of the matched value, else `-1`.
+ * @example
+ *
+ * _.lastIndexOf([1, 2, 1, 2], 2);
+ * // => 3
+ *
+ * // Search from the `fromIndex`.
+ * _.lastIndexOf([1, 2, 1, 2], 2, 2);
+ * // => 1
+ */
+ function lastIndexOf(array, value, fromIndex) {
+ var length = array ? array.length : 0;
+ if (!length) {
+ return -1;
+ }
+ var index = length;
+ if (fromIndex !== undefined) {
+ index = toInteger(fromIndex);
+ index = index < 0 ? nativeMax(length + index, 0) : nativeMin(index, length - 1);
+ }
+ return value === value
+ ? strictLastIndexOf(array, value, index)
+ : baseFindIndex(array, baseIsNaN, index, true);
+ }
+
+ /**
+ * Gets the element at index `n` of `array`. If `n` is negative, the nth
+ * element from the end is returned.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.11.0
+ * @category Array
+ * @param {Array} array The array to query.
+ * @param {number} [n=0] The index of the element to return.
+ * @returns {*} Returns the nth element of `array`.
+ * @example
+ *
+ * var array = ['a', 'b', 'c', 'd'];
+ *
+ * _.nth(array, 1);
+ * // => 'b'
+ *
+ * _.nth(array, -2);
+ * // => 'c';
+ */
+ function nth(array, n) {
+ return (array && array.length) ? baseNth(array, toInteger(n)) : undefined;
+ }
+
+ /**
+ * Removes all given values from `array` using
+ * [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)
+ * for equality comparisons.
+ *
+ * **Note:** Unlike `_.without`, this method mutates `array`. Use `_.remove`
+ * to remove elements from an array by predicate.
+ *
+ * @static
+ * @memberOf _
+ * @since 2.0.0
+ * @category Array
+ * @param {Array} array The array to modify.
+ * @param {...*} [values] The values to remove.
+ * @returns {Array} Returns `array`.
+ * @example
+ *
+ * var array = ['a', 'b', 'c', 'a', 'b', 'c'];
+ *
+ * _.pull(array, 'a', 'c');
+ * console.log(array);
+ * // => ['b', 'b']
+ */
+ var pull = baseRest(pullAll);
+
+ /**
+ * This method is like `_.pull` except that it accepts an array of values to remove.
+ *
+ * **Note:** Unlike `_.difference`, this method mutates `array`.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Array
+ * @param {Array} array The array to modify.
+ * @param {Array} values The values to remove.
+ * @returns {Array} Returns `array`.
+ * @example
+ *
+ * var array = ['a', 'b', 'c', 'a', 'b', 'c'];
+ *
+ * _.pullAll(array, ['a', 'c']);
+ * console.log(array);
+ * // => ['b', 'b']
+ */
+ function pullAll(array, values) {
+ return (array && array.length && values && values.length)
+ ? basePullAll(array, values)
+ : array;
+ }
+
+ /**
+ * This method is like `_.pullAll` except that it accepts `iteratee` which is
+ * invoked for each element of `array` and `values` to generate the criterion
+ * by which they're compared. The iteratee is invoked with one argument: (value).
+ *
+ * **Note:** Unlike `_.differenceBy`, this method mutates `array`.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Array
+ * @param {Array} array The array to modify.
+ * @param {Array} values The values to remove.
+ * @param {Function} [iteratee=_.identity]
+ * The iteratee invoked per element.
+ * @returns {Array} Returns `array`.
+ * @example
+ *
+ * var array = [{ 'x': 1 }, { 'x': 2 }, { 'x': 3 }, { 'x': 1 }];
+ *
+ * _.pullAllBy(array, [{ 'x': 1 }, { 'x': 3 }], 'x');
+ * console.log(array);
+ * // => [{ 'x': 2 }]
+ */
+ function pullAllBy(array, values, iteratee) {
+ return (array && array.length && values && values.length)
+ ? basePullAll(array, values, getIteratee(iteratee, 2))
+ : array;
+ }
+
+ /**
+ * This method is like `_.pullAll` except that it accepts `comparator` which
+ * is invoked to compare elements of `array` to `values`. The comparator is
+ * invoked with two arguments: (arrVal, othVal).
+ *
+ * **Note:** Unlike `_.differenceWith`, this method mutates `array`.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.6.0
+ * @category Array
+ * @param {Array} array The array to modify.
+ * @param {Array} values The values to remove.
+ * @param {Function} [comparator] The comparator invoked per element.
+ * @returns {Array} Returns `array`.
+ * @example
+ *
+ * var array = [{ 'x': 1, 'y': 2 }, { 'x': 3, 'y': 4 }, { 'x': 5, 'y': 6 }];
+ *
+ * _.pullAllWith(array, [{ 'x': 3, 'y': 4 }], _.isEqual);
+ * console.log(array);
+ * // => [{ 'x': 1, 'y': 2 }, { 'x': 5, 'y': 6 }]
+ */
+ function pullAllWith(array, values, comparator) {
+ return (array && array.length && values && values.length)
+ ? basePullAll(array, values, undefined, comparator)
+ : array;
+ }
+
+ /**
+ * Removes elements from `array` corresponding to `indexes` and returns an
+ * array of removed elements.
+ *
+ * **Note:** Unlike `_.at`, this method mutates `array`.
+ *
+ * @static
+ * @memberOf _
+ * @since 3.0.0
+ * @category Array
+ * @param {Array} array The array to modify.
+ * @param {...(number|number[])} [indexes] The indexes of elements to remove.
+ * @returns {Array} Returns the new array of removed elements.
+ * @example
+ *
+ * var array = ['a', 'b', 'c', 'd'];
+ * var pulled = _.pullAt(array, [1, 3]);
+ *
+ * console.log(array);
+ * // => ['a', 'c']
+ *
+ * console.log(pulled);
+ * // => ['b', 'd']
+ */
+ var pullAt = flatRest(function(array, indexes) {
+ var length = array ? array.length : 0,
+ result = baseAt(array, indexes);
+
+ basePullAt(array, arrayMap(indexes, function(index) {
+ return isIndex(index, length) ? +index : index;
+ }).sort(compareAscending));
+
+ return result;
+ });
+
+ /**
+ * Removes all elements from `array` that `predicate` returns truthy for
+ * and returns an array of the removed elements. The predicate is invoked
+ * with three arguments: (value, index, array).
+ *
+ * **Note:** Unlike `_.filter`, this method mutates `array`. Use `_.pull`
+ * to pull elements from an array by value.
+ *
+ * @static
+ * @memberOf _
+ * @since 2.0.0
+ * @category Array
+ * @param {Array} array The array to modify.
+ * @param {Function} [predicate=_.identity]
+ * The function invoked per iteration.
+ * @returns {Array} Returns the new array of removed elements.
+ * @example
+ *
+ * var array = [1, 2, 3, 4];
+ * var evens = _.remove(array, function(n) {
+ * return n % 2 == 0;
+ * });
+ *
+ * console.log(array);
+ * // => [1, 3]
+ *
+ * console.log(evens);
+ * // => [2, 4]
+ */
+ function remove(array, predicate) {
+ var result = [];
+ if (!(array && array.length)) {
+ return result;
+ }
+ var index = -1,
+ indexes = [],
+ length = array.length;
+
+ predicate = getIteratee(predicate, 3);
+ while (++index < length) {
+ var value = array[index];
+ if (predicate(value, index, array)) {
+ result.push(value);
+ indexes.push(index);
+ }
+ }
+ basePullAt(array, indexes);
+ return result;
+ }
+
+ /**
+ * Reverses `array` so that the first element becomes the last, the second
+ * element becomes the second to last, and so on.
+ *
+ * **Note:** This method mutates `array` and is based on
+ * [`Array#reverse`](https://mdn.io/Array/reverse).
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Array
+ * @param {Array} array The array to modify.
+ * @returns {Array} Returns `array`.
+ * @example
+ *
+ * var array = [1, 2, 3];
+ *
+ * _.reverse(array);
+ * // => [3, 2, 1]
+ *
+ * console.log(array);
+ * // => [3, 2, 1]
+ */
+ function reverse(array) {
+ return array ? nativeReverse.call(array) : array;
+ }
+
+ /**
+ * Creates a slice of `array` from `start` up to, but not including, `end`.
+ *
+ * **Note:** This method is used instead of
+ * [`Array#slice`](https://mdn.io/Array/slice) to ensure dense arrays are
+ * returned.
+ *
+ * @static
+ * @memberOf _
+ * @since 3.0.0
+ * @category Array
+ * @param {Array} array The array to slice.
+ * @param {number} [start=0] The start position.
+ * @param {number} [end=array.length] The end position.
+ * @returns {Array} Returns the slice of `array`.
+ */
+ function slice(array, start, end) {
+ var length = array ? array.length : 0;
+ if (!length) {
+ return [];
+ }
+ if (end && typeof end != 'number' && isIterateeCall(array, start, end)) {
+ start = 0;
+ end = length;
+ }
+ else {
+ start = start == null ? 0 : toInteger(start);
+ end = end === undefined ? length : toInteger(end);
+ }
+ return baseSlice(array, start, end);
+ }
+
+ /**
+ * Uses a binary search to determine the lowest index at which `value`
+ * should be inserted into `array` in order to maintain its sort order.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.1.0
+ * @category Array
+ * @param {Array} array The sorted array to inspect.
+ * @param {*} value The value to evaluate.
+ * @returns {number} Returns the index at which `value` should be inserted
+ * into `array`.
+ * @example
+ *
+ * _.sortedIndex([30, 50], 40);
+ * // => 1
+ */
+ function sortedIndex(array, value) {
+ return baseSortedIndex(array, value);
+ }
+
+ /**
+ * This method is like `_.sortedIndex` except that it accepts `iteratee`
+ * which is invoked for `value` and each element of `array` to compute their
+ * sort ranking. The iteratee is invoked with one argument: (value).
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Array
+ * @param {Array} array The sorted array to inspect.
+ * @param {*} value The value to evaluate.
+ * @param {Function} [iteratee=_.identity]
+ * The iteratee invoked per element.
+ * @returns {number} Returns the index at which `value` should be inserted
+ * into `array`.
+ * @example
+ *
+ * var objects = [{ 'x': 4 }, { 'x': 5 }];
+ *
+ * _.sortedIndexBy(objects, { 'x': 4 }, function(o) { return o.x; });
+ * // => 0
+ *
+ * // The `_.property` iteratee shorthand.
+ * _.sortedIndexBy(objects, { 'x': 4 }, 'x');
+ * // => 0
+ */
+ function sortedIndexBy(array, value, iteratee) {
+ return baseSortedIndexBy(array, value, getIteratee(iteratee, 2));
+ }
+
+ /**
+ * This method is like `_.indexOf` except that it performs a binary
+ * search on a sorted `array`.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Array
+ * @param {Array} array The array to inspect.
+ * @param {*} value The value to search for.
+ * @returns {number} Returns the index of the matched value, else `-1`.
+ * @example
+ *
+ * _.sortedIndexOf([4, 5, 5, 5, 6], 5);
+ * // => 1
+ */
+ function sortedIndexOf(array, value) {
+ var length = array ? array.length : 0;
+ if (length) {
+ var index = baseSortedIndex(array, value);
+ if (index < length && eq(array[index], value)) {
+ return index;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * This method is like `_.sortedIndex` except that it returns the highest
+ * index at which `value` should be inserted into `array` in order to
+ * maintain its sort order.
+ *
+ * @static
+ * @memberOf _
+ * @since 3.0.0
+ * @category Array
+ * @param {Array} array The sorted array to inspect.
+ * @param {*} value The value to evaluate.
+ * @returns {number} Returns the index at which `value` should be inserted
+ * into `array`.
+ * @example
+ *
+ * _.sortedLastIndex([4, 5, 5, 5, 6], 5);
+ * // => 4
+ */
+ function sortedLastIndex(array, value) {
+ return baseSortedIndex(array, value, true);
+ }
+
+ /**
+ * This method is like `_.sortedLastIndex` except that it accepts `iteratee`
+ * which is invoked for `value` and each element of `array` to compute their
+ * sort ranking. The iteratee is invoked with one argument: (value).
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Array
+ * @param {Array} array The sorted array to inspect.
+ * @param {*} value The value to evaluate.
+ * @param {Function} [iteratee=_.identity]
+ * The iteratee invoked per element.
+ * @returns {number} Returns the index at which `value` should be inserted
+ * into `array`.
+ * @example
+ *
+ * var objects = [{ 'x': 4 }, { 'x': 5 }];
+ *
+ * _.sortedLastIndexBy(objects, { 'x': 4 }, function(o) { return o.x; });
+ * // => 1
+ *
+ * // The `_.property` iteratee shorthand.
+ * _.sortedLastIndexBy(objects, { 'x': 4 }, 'x');
+ * // => 1
+ */
+ function sortedLastIndexBy(array, value, iteratee) {
+ return baseSortedIndexBy(array, value, getIteratee(iteratee, 2), true);
+ }
+
+ /**
+ * This method is like `_.lastIndexOf` except that it performs a binary
+ * search on a sorted `array`.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Array
+ * @param {Array} array The array to inspect.
+ * @param {*} value The value to search for.
+ * @returns {number} Returns the index of the matched value, else `-1`.
+ * @example
+ *
+ * _.sortedLastIndexOf([4, 5, 5, 5, 6], 5);
+ * // => 3
+ */
+ function sortedLastIndexOf(array, value) {
+ var length = array ? array.length : 0;
+ if (length) {
+ var index = baseSortedIndex(array, value, true) - 1;
+ if (eq(array[index], value)) {
+ return index;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * This method is like `_.uniq` except that it's designed and optimized
+ * for sorted arrays.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Array
+ * @param {Array} array The array to inspect.
+ * @returns {Array} Returns the new duplicate free array.
+ * @example
+ *
+ * _.sortedUniq([1, 1, 2]);
+ * // => [1, 2]
+ */
+ function sortedUniq(array) {
+ return (array && array.length)
+ ? baseSortedUniq(array)
+ : [];
+ }
+
+ /**
+ * This method is like `_.uniqBy` except that it's designed and optimized
+ * for sorted arrays.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Array
+ * @param {Array} array The array to inspect.
+ * @param {Function} [iteratee] The iteratee invoked per element.
+ * @returns {Array} Returns the new duplicate free array.
+ * @example
+ *
+ * _.sortedUniqBy([1.1, 1.2, 2.3, 2.4], Math.floor);
+ * // => [1.1, 2.3]
+ */
+ function sortedUniqBy(array, iteratee) {
+ return (array && array.length)
+ ? baseSortedUniq(array, getIteratee(iteratee, 2))
+ : [];
+ }
+
+ /**
+ * Gets all but the first element of `array`.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Array
+ * @param {Array} array The array to query.
+ * @returns {Array} Returns the slice of `array`.
+ * @example
+ *
+ * _.tail([1, 2, 3]);
+ * // => [2, 3]
+ */
+ function tail(array) {
+ var length = array ? array.length : 0;
+ return length ? baseSlice(array, 1, length) : [];
+ }
+
+ /**
+ * Creates a slice of `array` with `n` elements taken from the beginning.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.1.0
+ * @category Array
+ * @param {Array} array The array to query.
+ * @param {number} [n=1] The number of elements to take.
+ * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.
+ * @returns {Array} Returns the slice of `array`.
+ * @example
+ *
+ * _.take([1, 2, 3]);
+ * // => [1]
+ *
+ * _.take([1, 2, 3], 2);
+ * // => [1, 2]
+ *
+ * _.take([1, 2, 3], 5);
+ * // => [1, 2, 3]
+ *
+ * _.take([1, 2, 3], 0);
+ * // => []
+ */
+ function take(array, n, guard) {
+ if (!(array && array.length)) {
+ return [];
+ }
+ n = (guard || n === undefined) ? 1 : toInteger(n);
+ return baseSlice(array, 0, n < 0 ? 0 : n);
+ }
+
+ /**
+ * Creates a slice of `array` with `n` elements taken from the end.
+ *
+ * @static
+ * @memberOf _
+ * @since 3.0.0
+ * @category Array
+ * @param {Array} array The array to query.
+ * @param {number} [n=1] The number of elements to take.
+ * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.
+ * @returns {Array} Returns the slice of `array`.
+ * @example
+ *
+ * _.takeRight([1, 2, 3]);
+ * // => [3]
+ *
+ * _.takeRight([1, 2, 3], 2);
+ * // => [2, 3]
+ *
+ * _.takeRight([1, 2, 3], 5);
+ * // => [1, 2, 3]
+ *
+ * _.takeRight([1, 2, 3], 0);
+ * // => []
+ */
+ function takeRight(array, n, guard) {
+ var length = array ? array.length : 0;
+ if (!length) {
+ return [];
+ }
+ n = (guard || n === undefined) ? 1 : toInteger(n);
+ n = length - n;
+ return baseSlice(array, n < 0 ? 0 : n, length);
+ }
+
+ /**
+ * Creates a slice of `array` with elements taken from the end. Elements are
+ * taken until `predicate` returns falsey. The predicate is invoked with
+ * three arguments: (value, index, array).
+ *
+ * @static
+ * @memberOf _
+ * @since 3.0.0
+ * @category Array
+ * @param {Array} array The array to query.
+ * @param {Function} [predicate=_.identity]
+ * The function invoked per iteration.
+ * @returns {Array} Returns the slice of `array`.
+ * @example
+ *
+ * var users = [
+ * { 'user': 'barney', 'active': true },
+ * { 'user': 'fred', 'active': false },
+ * { 'user': 'pebbles', 'active': false }
+ * ];
+ *
+ * _.takeRightWhile(users, function(o) { return !o.active; });
+ * // => objects for ['fred', 'pebbles']
+ *
+ * // The `_.matches` iteratee shorthand.
+ * _.takeRightWhile(users, { 'user': 'pebbles', 'active': false });
+ * // => objects for ['pebbles']
+ *
+ * // The `_.matchesProperty` iteratee shorthand.
+ * _.takeRightWhile(users, ['active', false]);
+ * // => objects for ['fred', 'pebbles']
+ *
+ * // The `_.property` iteratee shorthand.
+ * _.takeRightWhile(users, 'active');
+ * // => []
+ */
+ function takeRightWhile(array, predicate) {
+ return (array && array.length)
+ ? baseWhile(array, getIteratee(predicate, 3), false, true)
+ : [];
+ }
+
+ /**
+ * Creates a slice of `array` with elements taken from the beginning. Elements
+ * are taken until `predicate` returns falsey. The predicate is invoked with
+ * three arguments: (value, index, array).
+ *
+ * @static
+ * @memberOf _
+ * @since 3.0.0
+ * @category Array
+ * @param {Array} array The array to query.
+ * @param {Function} [predicate=_.identity]
+ * The function invoked per iteration.
+ * @returns {Array} Returns the slice of `array`.
+ * @example
+ *
+ * var users = [
+ * { 'user': 'barney', 'active': false },
+ * { 'user': 'fred', 'active': false},
+ * { 'user': 'pebbles', 'active': true }
+ * ];
+ *
+ * _.takeWhile(users, function(o) { return !o.active; });
+ * // => objects for ['barney', 'fred']
+ *
+ * // The `_.matches` iteratee shorthand.
+ * _.takeWhile(users, { 'user': 'barney', 'active': false });
+ * // => objects for ['barney']
+ *
+ * // The `_.matchesProperty` iteratee shorthand.
+ * _.takeWhile(users, ['active', false]);
+ * // => objects for ['barney', 'fred']
+ *
+ * // The `_.property` iteratee shorthand.
+ * _.takeWhile(users, 'active');
+ * // => []
+ */
+ function takeWhile(array, predicate) {
+ return (array && array.length)
+ ? baseWhile(array, getIteratee(predicate, 3))
+ : [];
+ }
+
+ /**
+ * Creates an array of unique values, in order, from all given arrays using
+ * [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)
+ * for equality comparisons.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.1.0
+ * @category Array
+ * @param {...Array} [arrays] The arrays to inspect.
+ * @returns {Array} Returns the new array of combined values.
+ * @example
+ *
+ * _.union([2], [1, 2]);
+ * // => [2, 1]
+ */
+ var union = baseRest(function(arrays) {
+ return baseUniq(baseFlatten(arrays, 1, isArrayLikeObject, true));
+ });
+
+ /**
+ * This method is like `_.union` except that it accepts `iteratee` which is
+ * invoked for each element of each `arrays` to generate the criterion by
+ * which uniqueness is computed. Result values are chosen from the first
+ * array in which the value occurs. The iteratee is invoked with one argument:
+ * (value).
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Array
+ * @param {...Array} [arrays] The arrays to inspect.
+ * @param {Function} [iteratee=_.identity]
+ * The iteratee invoked per element.
+ * @returns {Array} Returns the new array of combined values.
+ * @example
+ *
+ * _.unionBy([2.1], [1.2, 2.3], Math.floor);
+ * // => [2.1, 1.2]
+ *
+ * // The `_.property` iteratee shorthand.
+ * _.unionBy([{ 'x': 1 }], [{ 'x': 2 }, { 'x': 1 }], 'x');
+ * // => [{ 'x': 1 }, { 'x': 2 }]
+ */
+ var unionBy = baseRest(function(arrays) {
+ var iteratee = last(arrays);
+ if (isArrayLikeObject(iteratee)) {
+ iteratee = undefined;
+ }
+ return baseUniq(baseFlatten(arrays, 1, isArrayLikeObject, true), getIteratee(iteratee, 2));
+ });
+
+ /**
+ * This method is like `_.union` except that it accepts `comparator` which
+ * is invoked to compare elements of `arrays`. Result values are chosen from
+ * the first array in which the value occurs. The comparator is invoked
+ * with two arguments: (arrVal, othVal).
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Array
+ * @param {...Array} [arrays] The arrays to inspect.
+ * @param {Function} [comparator] The comparator invoked per element.
+ * @returns {Array} Returns the new array of combined values.
+ * @example
+ *
+ * var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }];
+ * var others = [{ 'x': 1, 'y': 1 }, { 'x': 1, 'y': 2 }];
+ *
+ * _.unionWith(objects, others, _.isEqual);
+ * // => [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }, { 'x': 1, 'y': 1 }]
+ */
+ var unionWith = baseRest(function(arrays) {
+ var comparator = last(arrays);
+ if (isArrayLikeObject(comparator)) {
+ comparator = undefined;
+ }
+ return baseUniq(baseFlatten(arrays, 1, isArrayLikeObject, true), undefined, comparator);
+ });
+
+ /**
+ * Creates a duplicate-free version of an array, using
+ * [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)
+ * for equality comparisons, in which only the first occurrence of each element
+ * is kept. The order of result values is determined by the order they occur
+ * in the array.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.1.0
+ * @category Array
+ * @param {Array} array The array to inspect.
+ * @returns {Array} Returns the new duplicate free array.
+ * @example
+ *
+ * _.uniq([2, 1, 2]);
+ * // => [2, 1]
+ */
+ function uniq(array) {
+ return (array && array.length)
+ ? baseUniq(array)
+ : [];
+ }
+
+ /**
+ * This method is like `_.uniq` except that it accepts `iteratee` which is
+ * invoked for each element in `array` to generate the criterion by which
+ * uniqueness is computed. The order of result values is determined by the
+ * order they occur in the array. The iteratee is invoked with one argument:
+ * (value).
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Array
+ * @param {Array} array The array to inspect.
+ * @param {Function} [iteratee=_.identity]
+ * The iteratee invoked per element.
+ * @returns {Array} Returns the new duplicate free array.
+ * @example
+ *
+ * _.uniqBy([2.1, 1.2, 2.3], Math.floor);
+ * // => [2.1, 1.2]
+ *
+ * // The `_.property` iteratee shorthand.
+ * _.uniqBy([{ 'x': 1 }, { 'x': 2 }, { 'x': 1 }], 'x');
+ * // => [{ 'x': 1 }, { 'x': 2 }]
+ */
+ function uniqBy(array, iteratee) {
+ return (array && array.length)
+ ? baseUniq(array, getIteratee(iteratee, 2))
+ : [];
+ }
+
+ /**
+ * This method is like `_.uniq` except that it accepts `comparator` which
+ * is invoked to compare elements of `array`. The order of result values is
+ * determined by the order they occur in the array.The comparator is invoked
+ * with two arguments: (arrVal, othVal).
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Array
+ * @param {Array} array The array to inspect.
+ * @param {Function} [comparator] The comparator invoked per element.
+ * @returns {Array} Returns the new duplicate free array.
+ * @example
+ *
+ * var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }, { 'x': 1, 'y': 2 }];
+ *
+ * _.uniqWith(objects, _.isEqual);
+ * // => [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }]
+ */
+ function uniqWith(array, comparator) {
+ return (array && array.length)
+ ? baseUniq(array, undefined, comparator)
+ : [];
+ }
+
+ /**
+ * This method is like `_.zip` except that it accepts an array of grouped
+ * elements and creates an array regrouping the elements to their pre-zip
+ * configuration.
+ *
+ * @static
+ * @memberOf _
+ * @since 1.2.0
+ * @category Array
+ * @param {Array} array The array of grouped elements to process.
+ * @returns {Array} Returns the new array of regrouped elements.
+ * @example
+ *
+ * var zipped = _.zip(['a', 'b'], [1, 2], [true, false]);
+ * // => [['a', 1, true], ['b', 2, false]]
+ *
+ * _.unzip(zipped);
+ * // => [['a', 'b'], [1, 2], [true, false]]
+ */
+ function unzip(array) {
+ if (!(array && array.length)) {
+ return [];
+ }
+ var length = 0;
+ array = arrayFilter(array, function(group) {
+ if (isArrayLikeObject(group)) {
+ length = nativeMax(group.length, length);
+ return true;
+ }
+ });
+ return baseTimes(length, function(index) {
+ return arrayMap(array, baseProperty(index));
+ });
+ }
+
+ /**
+ * This method is like `_.unzip` except that it accepts `iteratee` to specify
+ * how regrouped values should be combined. The iteratee is invoked with the
+ * elements of each group: (...group).
+ *
+ * @static
+ * @memberOf _
+ * @since 3.8.0
+ * @category Array
+ * @param {Array} array The array of grouped elements to process.
+ * @param {Function} [iteratee=_.identity] The function to combine
+ * regrouped values.
+ * @returns {Array} Returns the new array of regrouped elements.
+ * @example
+ *
+ * var zipped = _.zip([1, 2], [10, 20], [100, 200]);
+ * // => [[1, 10, 100], [2, 20, 200]]
+ *
+ * _.unzipWith(zipped, _.add);
+ * // => [3, 30, 300]
+ */
+ function unzipWith(array, iteratee) {
+ if (!(array && array.length)) {
+ return [];
+ }
+ var result = unzip(array);
+ if (iteratee == null) {
+ return result;
+ }
+ return arrayMap(result, function(group) {
+ return apply(iteratee, undefined, group);
+ });
+ }
+
+ /**
+ * Creates an array excluding all given values using
+ * [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)
+ * for equality comparisons.
+ *
+ * **Note:** Unlike `_.pull`, this method returns a new array.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.1.0
+ * @category Array
+ * @param {Array} array The array to inspect.
+ * @param {...*} [values] The values to exclude.
+ * @returns {Array} Returns the new array of filtered values.
+ * @see _.difference, _.xor
+ * @example
+ *
+ * _.without([2, 1, 2, 3], 1, 2);
+ * // => [3]
+ */
+ var without = baseRest(function(array, values) {
+ return isArrayLikeObject(array)
+ ? baseDifference(array, values)
+ : [];
+ });
+
+ /**
+ * Creates an array of unique values that is the
+ * [symmetric difference](https://en.wikipedia.org/wiki/Symmetric_difference)
+ * of the given arrays. The order of result values is determined by the order
+ * they occur in the arrays.
+ *
+ * @static
+ * @memberOf _
+ * @since 2.4.0
+ * @category Array
+ * @param {...Array} [arrays] The arrays to inspect.
+ * @returns {Array} Returns the new array of filtered values.
+ * @see _.difference, _.without
+ * @example
+ *
+ * _.xor([2, 1], [2, 3]);
+ * // => [1, 3]
+ */
+ var xor = baseRest(function(arrays) {
+ return baseXor(arrayFilter(arrays, isArrayLikeObject));
+ });
+
+ /**
+ * This method is like `_.xor` except that it accepts `iteratee` which is
+ * invoked for each element of each `arrays` to generate the criterion by
+ * which by which they're compared. The order of result values is determined
+ * by the order they occur in the arrays. The iteratee is invoked with one
+ * argument: (value).
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Array
+ * @param {...Array} [arrays] The arrays to inspect.
+ * @param {Function} [iteratee=_.identity]
+ * The iteratee invoked per element.
+ * @returns {Array} Returns the new array of filtered values.
+ * @example
+ *
+ * _.xorBy([2.1, 1.2], [2.3, 3.4], Math.floor);
+ * // => [1.2, 3.4]
+ *
+ * // The `_.property` iteratee shorthand.
+ * _.xorBy([{ 'x': 1 }], [{ 'x': 2 }, { 'x': 1 }], 'x');
+ * // => [{ 'x': 2 }]
+ */
+ var xorBy = baseRest(function(arrays) {
+ var iteratee = last(arrays);
+ if (isArrayLikeObject(iteratee)) {
+ iteratee = undefined;
+ }
+ return baseXor(arrayFilter(arrays, isArrayLikeObject), getIteratee(iteratee, 2));
+ });
+
+ /**
+ * This method is like `_.xor` except that it accepts `comparator` which is
+ * invoked to compare elements of `arrays`. The order of result values is
+ * determined by the order they occur in the arrays. The comparator is invoked
+ * with two arguments: (arrVal, othVal).
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Array
+ * @param {...Array} [arrays] The arrays to inspect.
+ * @param {Function} [comparator] The comparator invoked per element.
+ * @returns {Array} Returns the new array of filtered values.
+ * @example
+ *
+ * var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }];
+ * var others = [{ 'x': 1, 'y': 1 }, { 'x': 1, 'y': 2 }];
+ *
+ * _.xorWith(objects, others, _.isEqual);
+ * // => [{ 'x': 2, 'y': 1 }, { 'x': 1, 'y': 1 }]
+ */
+ var xorWith = baseRest(function(arrays) {
+ var comparator = last(arrays);
+ if (isArrayLikeObject(comparator)) {
+ comparator = undefined;
+ }
+ return baseXor(arrayFilter(arrays, isArrayLikeObject), undefined, comparator);
+ });
+
+ /**
+ * Creates an array of grouped elements, the first of which contains the
+ * first elements of the given arrays, the second of which contains the
+ * second elements of the given arrays, and so on.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.1.0
+ * @category Array
+ * @param {...Array} [arrays] The arrays to process.
+ * @returns {Array} Returns the new array of grouped elements.
+ * @example
+ *
+ * _.zip(['a', 'b'], [1, 2], [true, false]);
+ * // => [['a', 1, true], ['b', 2, false]]
+ */
+ var zip = baseRest(unzip);
+
+ /**
+ * This method is like `_.fromPairs` except that it accepts two arrays,
+ * one of property identifiers and one of corresponding values.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.4.0
+ * @category Array
+ * @param {Array} [props=[]] The property identifiers.
+ * @param {Array} [values=[]] The property values.
+ * @returns {Object} Returns the new object.
+ * @example
+ *
+ * _.zipObject(['a', 'b'], [1, 2]);
+ * // => { 'a': 1, 'b': 2 }
+ */
+ function zipObject(props, values) {
+ return baseZipObject(props || [], values || [], assignValue);
+ }
+
+ /**
+ * This method is like `_.zipObject` except that it supports property paths.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.1.0
+ * @category Array
+ * @param {Array} [props=[]] The property identifiers.
+ * @param {Array} [values=[]] The property values.
+ * @returns {Object} Returns the new object.
+ * @example
+ *
+ * _.zipObjectDeep(['a.b[0].c', 'a.b[1].d'], [1, 2]);
+ * // => { 'a': { 'b': [{ 'c': 1 }, { 'd': 2 }] } }
+ */
+ function zipObjectDeep(props, values) {
+ return baseZipObject(props || [], values || [], baseSet);
+ }
+
+ /**
+ * This method is like `_.zip` except that it accepts `iteratee` to specify
+ * how grouped values should be combined. The iteratee is invoked with the
+ * elements of each group: (...group).
+ *
+ * @static
+ * @memberOf _
+ * @since 3.8.0
+ * @category Array
+ * @param {...Array} [arrays] The arrays to process.
+ * @param {Function} [iteratee=_.identity] The function to combine grouped values.
+ * @returns {Array} Returns the new array of grouped elements.
+ * @example
+ *
+ * _.zipWith([1, 2], [10, 20], [100, 200], function(a, b, c) {
+ * return a + b + c;
+ * });
+ * // => [111, 222]
+ */
+ var zipWith = baseRest(function(arrays) {
+ var length = arrays.length,
+ iteratee = length > 1 ? arrays[length - 1] : undefined;
+
+ iteratee = typeof iteratee == 'function' ? (arrays.pop(), iteratee) : undefined;
+ return unzipWith(arrays, iteratee);
+ });
+
+ /*------------------------------------------------------------------------*/
+
+ /**
+ * Creates a `lodash` wrapper instance that wraps `value` with explicit method
+ * chain sequences enabled. The result of such sequences must be unwrapped
+ * with `_#value`.
+ *
+ * @static
+ * @memberOf _
+ * @since 1.3.0
+ * @category Seq
+ * @param {*} value The value to wrap.
+ * @returns {Object} Returns the new `lodash` wrapper instance.
+ * @example
+ *
+ * var users = [
+ * { 'user': 'barney', 'age': 36 },
+ * { 'user': 'fred', 'age': 40 },
+ * { 'user': 'pebbles', 'age': 1 }
+ * ];
+ *
+ * var youngest = _
+ * .chain(users)
+ * .sortBy('age')
+ * .map(function(o) {
+ * return o.user + ' is ' + o.age;
+ * })
+ * .head()
+ * .value();
+ * // => 'pebbles is 1'
+ */
+ function chain(value) {
+ var result = lodash(value);
+ result.__chain__ = true;
+ return result;
+ }
+
+ /**
+ * This method invokes `interceptor` and returns `value`. The interceptor
+ * is invoked with one argument; (value). The purpose of this method is to
+ * "tap into" a method chain sequence in order to modify intermediate results.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.1.0
+ * @category Seq
+ * @param {*} value The value to provide to `interceptor`.
+ * @param {Function} interceptor The function to invoke.
+ * @returns {*} Returns `value`.
+ * @example
+ *
+ * _([1, 2, 3])
+ * .tap(function(array) {
+ * // Mutate input array.
+ * array.pop();
+ * })
+ * .reverse()
+ * .value();
+ * // => [2, 1]
+ */
+ function tap(value, interceptor) {
+ interceptor(value);
+ return value;
+ }
+
+ /**
+ * This method is like `_.tap` except that it returns the result of `interceptor`.
+ * The purpose of this method is to "pass thru" values replacing intermediate
+ * results in a method chain sequence.
+ *
+ * @static
+ * @memberOf _
+ * @since 3.0.0
+ * @category Seq
+ * @param {*} value The value to provide to `interceptor`.
+ * @param {Function} interceptor The function to invoke.
+ * @returns {*} Returns the result of `interceptor`.
+ * @example
+ *
+ * _(' abc ')
+ * .chain()
+ * .trim()
+ * .thru(function(value) {
+ * return [value];
+ * })
+ * .value();
+ * // => ['abc']
+ */
+ function thru(value, interceptor) {
+ return interceptor(value);
+ }
+
+ /**
+ * This method is the wrapper version of `_.at`.
+ *
+ * @name at
+ * @memberOf _
+ * @since 1.0.0
+ * @category Seq
+ * @param {...(string|string[])} [paths] The property paths of elements to pick.
+ * @returns {Object} Returns the new `lodash` wrapper instance.
+ * @example
+ *
+ * var object = { 'a': [{ 'b': { 'c': 3 } }, 4] };
+ *
+ * _(object).at(['a[0].b.c', 'a[1]']).value();
+ * // => [3, 4]
+ */
+ var wrapperAt = flatRest(function(paths) {
+ var length = paths.length,
+ start = length ? paths[0] : 0,
+ value = this.__wrapped__,
+ interceptor = function(object) { return baseAt(object, paths); };
+
+ if (length > 1 || this.__actions__.length ||
+ !(value instanceof LazyWrapper) || !isIndex(start)) {
+ return this.thru(interceptor);
+ }
+ value = value.slice(start, +start + (length ? 1 : 0));
+ value.__actions__.push({
+ 'func': thru,
+ 'args': [interceptor],
+ 'thisArg': undefined
+ });
+ return new LodashWrapper(value, this.__chain__).thru(function(array) {
+ if (length && !array.length) {
+ array.push(undefined);
+ }
+ return array;
+ });
+ });
+
+ /**
+ * Creates a `lodash` wrapper instance with explicit method chain sequences enabled.
+ *
+ * @name chain
+ * @memberOf _
+ * @since 0.1.0
+ * @category Seq
+ * @returns {Object} Returns the new `lodash` wrapper instance.
+ * @example
+ *
+ * var users = [
+ * { 'user': 'barney', 'age': 36 },
+ * { 'user': 'fred', 'age': 40 }
+ * ];
+ *
+ * // A sequence without explicit chaining.
+ * _(users).head();
+ * // => { 'user': 'barney', 'age': 36 }
+ *
+ * // A sequence with explicit chaining.
+ * _(users)
+ * .chain()
+ * .head()
+ * .pick('user')
+ * .value();
+ * // => { 'user': 'barney' }
+ */
+ function wrapperChain() {
+ return chain(this);
+ }
+
+ /**
+ * Executes the chain sequence and returns the wrapped result.
+ *
+ * @name commit
+ * @memberOf _
+ * @since 3.2.0
+ * @category Seq
+ * @returns {Object} Returns the new `lodash` wrapper instance.
+ * @example
+ *
+ * var array = [1, 2];
+ * var wrapped = _(array).push(3);
+ *
+ * console.log(array);
+ * // => [1, 2]
+ *
+ * wrapped = wrapped.commit();
+ * console.log(array);
+ * // => [1, 2, 3]
+ *
+ * wrapped.last();
+ * // => 3
+ *
+ * console.log(array);
+ * // => [1, 2, 3]
+ */
+ function wrapperCommit() {
+ return new LodashWrapper(this.value(), this.__chain__);
+ }
+
+ /**
+ * Gets the next value on a wrapped object following the
+ * [iterator protocol](https://mdn.io/iteration_protocols#iterator).
+ *
+ * @name next
+ * @memberOf _
+ * @since 4.0.0
+ * @category Seq
+ * @returns {Object} Returns the next iterator value.
+ * @example
+ *
+ * var wrapped = _([1, 2]);
+ *
+ * wrapped.next();
+ * // => { 'done': false, 'value': 1 }
+ *
+ * wrapped.next();
+ * // => { 'done': false, 'value': 2 }
+ *
+ * wrapped.next();
+ * // => { 'done': true, 'value': undefined }
+ */
+ function wrapperNext() {
+ if (this.__values__ === undefined) {
+ this.__values__ = toArray(this.value());
+ }
+ var done = this.__index__ >= this.__values__.length,
+ value = done ? undefined : this.__values__[this.__index__++];
+
+ return { 'done': done, 'value': value };
+ }
+
+ /**
+ * Enables the wrapper to be iterable.
+ *
+ * @name Symbol.iterator
+ * @memberOf _
+ * @since 4.0.0
+ * @category Seq
+ * @returns {Object} Returns the wrapper object.
+ * @example
+ *
+ * var wrapped = _([1, 2]);
+ *
+ * wrapped[Symbol.iterator]() === wrapped;
+ * // => true
+ *
+ * Array.from(wrapped);
+ * // => [1, 2]
+ */
+ function wrapperToIterator() {
+ return this;
+ }
+
+ /**
+ * Creates a clone of the chain sequence planting `value` as the wrapped value.
+ *
+ * @name plant
+ * @memberOf _
+ * @since 3.2.0
+ * @category Seq
+ * @param {*} value The value to plant.
+ * @returns {Object} Returns the new `lodash` wrapper instance.
+ * @example
+ *
+ * function square(n) {
+ * return n * n;
+ * }
+ *
+ * var wrapped = _([1, 2]).map(square);
+ * var other = wrapped.plant([3, 4]);
+ *
+ * other.value();
+ * // => [9, 16]
+ *
+ * wrapped.value();
+ * // => [1, 4]
+ */
+ function wrapperPlant(value) {
+ var result,
+ parent = this;
+
+ while (parent instanceof baseLodash) {
+ var clone = wrapperClone(parent);
+ clone.__index__ = 0;
+ clone.__values__ = undefined;
+ if (result) {
+ previous.__wrapped__ = clone;
+ } else {
+ result = clone;
+ }
+ var previous = clone;
+ parent = parent.__wrapped__;
+ }
+ previous.__wrapped__ = value;
+ return result;
+ }
+
+ /**
+ * This method is the wrapper version of `_.reverse`.
+ *
+ * **Note:** This method mutates the wrapped array.
+ *
+ * @name reverse
+ * @memberOf _
+ * @since 0.1.0
+ * @category Seq
+ * @returns {Object} Returns the new `lodash` wrapper instance.
+ * @example
+ *
+ * var array = [1, 2, 3];
+ *
+ * _(array).reverse().value()
+ * // => [3, 2, 1]
+ *
+ * console.log(array);
+ * // => [3, 2, 1]
+ */
+ function wrapperReverse() {
+ var value = this.__wrapped__;
+ if (value instanceof LazyWrapper) {
+ var wrapped = value;
+ if (this.__actions__.length) {
+ wrapped = new LazyWrapper(this);
+ }
+ wrapped = wrapped.reverse();
+ wrapped.__actions__.push({
+ 'func': thru,
+ 'args': [reverse],
+ 'thisArg': undefined
+ });
+ return new LodashWrapper(wrapped, this.__chain__);
+ }
+ return this.thru(reverse);
+ }
+
+ /**
+ * Executes the chain sequence to resolve the unwrapped value.
+ *
+ * @name value
+ * @memberOf _
+ * @since 0.1.0
+ * @alias toJSON, valueOf
+ * @category Seq
+ * @returns {*} Returns the resolved unwrapped value.
+ * @example
+ *
+ * _([1, 2, 3]).value();
+ * // => [1, 2, 3]
+ */
+ function wrapperValue() {
+ return baseWrapperValue(this.__wrapped__, this.__actions__);
+ }
+
+ /*------------------------------------------------------------------------*/
+
+ /**
+ * Creates an object composed of keys generated from the results of running
+ * each element of `collection` thru `iteratee`. The corresponding value of
+ * each key is the number of times the key was returned by `iteratee`. The
+ * iteratee is invoked with one argument: (value).
+ *
+ * @static
+ * @memberOf _
+ * @since 0.5.0
+ * @category Collection
+ * @param {Array|Object} collection The collection to iterate over.
+ * @param {Function} [iteratee=_.identity]
+ * The iteratee to transform keys.
+ * @returns {Object} Returns the composed aggregate object.
+ * @example
+ *
+ * _.countBy([6.1, 4.2, 6.3], Math.floor);
+ * // => { '4': 1, '6': 2 }
+ *
+ * // The `_.property` iteratee shorthand.
+ * _.countBy(['one', 'two', 'three'], 'length');
+ * // => { '3': 2, '5': 1 }
+ */
+ var countBy = createAggregator(function(result, value, key) {
+ if (hasOwnProperty.call(result, key)) {
+ ++result[key];
+ } else {
+ baseAssignValue(result, key, 1);
+ }
+ });
+
+ /**
+ * Checks if `predicate` returns truthy for **all** elements of `collection`.
+ * Iteration is stopped once `predicate` returns falsey. The predicate is
+ * invoked with three arguments: (value, index|key, collection).
+ *
+ * **Note:** This method returns `true` for
+ * [empty collections](https://en.wikipedia.org/wiki/Empty_set) because
+ * [everything is true](https://en.wikipedia.org/wiki/Vacuous_truth) of
+ * elements of empty collections.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.1.0
+ * @category Collection
+ * @param {Array|Object} collection The collection to iterate over.
+ * @param {Function} [predicate=_.identity]
+ * The function invoked per iteration.
+ * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.
+ * @returns {boolean} Returns `true` if all elements pass the predicate check,
+ * else `false`.
+ * @example
+ *
+ * _.every([true, 1, null, 'yes'], Boolean);
+ * // => false
+ *
+ * var users = [
+ * { 'user': 'barney', 'age': 36, 'active': false },
+ * { 'user': 'fred', 'age': 40, 'active': false }
+ * ];
+ *
+ * // The `_.matches` iteratee shorthand.
+ * _.every(users, { 'user': 'barney', 'active': false });
+ * // => false
+ *
+ * // The `_.matchesProperty` iteratee shorthand.
+ * _.every(users, ['active', false]);
+ * // => true
+ *
+ * // The `_.property` iteratee shorthand.
+ * _.every(users, 'active');
+ * // => false
+ */
+ function every(collection, predicate, guard) {
+ var func = isArray(collection) ? arrayEvery : baseEvery;
+ if (guard && isIterateeCall(collection, predicate, guard)) {
+ predicate = undefined;
+ }
+ return func(collection, getIteratee(predicate, 3));
+ }
+
+ /**
+ * Iterates over elements of `collection`, returning an array of all elements
+ * `predicate` returns truthy for. The predicate is invoked with three
+ * arguments: (value, index|key, collection).
+ *
+ * **Note:** Unlike `_.remove`, this method returns a new array.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.1.0
+ * @category Collection
+ * @param {Array|Object} collection The collection to iterate over.
+ * @param {Function} [predicate=_.identity]
+ * The function invoked per iteration.
+ * @returns {Array} Returns the new filtered array.
+ * @see _.reject
+ * @example
+ *
+ * var users = [
+ * { 'user': 'barney', 'age': 36, 'active': true },
+ * { 'user': 'fred', 'age': 40, 'active': false }
+ * ];
+ *
+ * _.filter(users, function(o) { return !o.active; });
+ * // => objects for ['fred']
+ *
+ * // The `_.matches` iteratee shorthand.
+ * _.filter(users, { 'age': 36, 'active': true });
+ * // => objects for ['barney']
+ *
+ * // The `_.matchesProperty` iteratee shorthand.
+ * _.filter(users, ['active', false]);
+ * // => objects for ['fred']
+ *
+ * // The `_.property` iteratee shorthand.
+ * _.filter(users, 'active');
+ * // => objects for ['barney']
+ */
+ function filter(collection, predicate) {
+ var func = isArray(collection) ? arrayFilter : baseFilter;
+ return func(collection, getIteratee(predicate, 3));
+ }
+
+ /**
+ * Iterates over elements of `collection`, returning the first element
+ * `predicate` returns truthy for. The predicate is invoked with three
+ * arguments: (value, index|key, collection).
+ *
+ * @static
+ * @memberOf _
+ * @since 0.1.0
+ * @category Collection
+ * @param {Array|Object} collection The collection to inspect.
+ * @param {Function} [predicate=_.identity]
+ * The function invoked per iteration.
+ * @param {number} [fromIndex=0] The index to search from.
+ * @returns {*} Returns the matched element, else `undefined`.
+ * @example
+ *
+ * var users = [
+ * { 'user': 'barney', 'age': 36, 'active': true },
+ * { 'user': 'fred', 'age': 40, 'active': false },
+ * { 'user': 'pebbles', 'age': 1, 'active': true }
+ * ];
+ *
+ * _.find(users, function(o) { return o.age < 40; });
+ * // => object for 'barney'
+ *
+ * // The `_.matches` iteratee shorthand.
+ * _.find(users, { 'age': 1, 'active': true });
+ * // => object for 'pebbles'
+ *
+ * // The `_.matchesProperty` iteratee shorthand.
+ * _.find(users, ['active', false]);
+ * // => object for 'fred'
+ *
+ * // The `_.property` iteratee shorthand.
+ * _.find(users, 'active');
+ * // => object for 'barney'
+ */
+ var find = createFind(findIndex);
+
+ /**
+ * This method is like `_.find` except that it iterates over elements of
+ * `collection` from right to left.
+ *
+ * @static
+ * @memberOf _
+ * @since 2.0.0
+ * @category Collection
+ * @param {Array|Object} collection The collection to inspect.
+ * @param {Function} [predicate=_.identity]
+ * The function invoked per iteration.
+ * @param {number} [fromIndex=collection.length-1] The index to search from.
+ * @returns {*} Returns the matched element, else `undefined`.
+ * @example
+ *
+ * _.findLast([1, 2, 3, 4], function(n) {
+ * return n % 2 == 1;
+ * });
+ * // => 3
+ */
+ var findLast = createFind(findLastIndex);
+
+ /**
+ * Creates a flattened array of values by running each element in `collection`
+ * thru `iteratee` and flattening the mapped results. The iteratee is invoked
+ * with three arguments: (value, index|key, collection).
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Collection
+ * @param {Array|Object} collection The collection to iterate over.
+ * @param {Function} [iteratee=_.identity]
+ * The function invoked per iteration.
+ * @returns {Array} Returns the new flattened array.
+ * @example
+ *
+ * function duplicate(n) {
+ * return [n, n];
+ * }
+ *
+ * _.flatMap([1, 2], duplicate);
+ * // => [1, 1, 2, 2]
+ */
+ function flatMap(collection, iteratee) {
+ return baseFlatten(map(collection, iteratee), 1);
+ }
+
+ /**
+ * This method is like `_.flatMap` except that it recursively flattens the
+ * mapped results.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.7.0
+ * @category Collection
+ * @param {Array|Object} collection The collection to iterate over.
+ * @param {Function} [iteratee=_.identity]
+ * The function invoked per iteration.
+ * @returns {Array} Returns the new flattened array.
+ * @example
+ *
+ * function duplicate(n) {
+ * return [[[n, n]]];
+ * }
+ *
+ * _.flatMapDeep([1, 2], duplicate);
+ * // => [1, 1, 2, 2]
+ */
+ function flatMapDeep(collection, iteratee) {
+ return baseFlatten(map(collection, iteratee), INFINITY);
+ }
+
+ /**
+ * This method is like `_.flatMap` except that it recursively flattens the
+ * mapped results up to `depth` times.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.7.0
+ * @category Collection
+ * @param {Array|Object} collection The collection to iterate over.
+ * @param {Function} [iteratee=_.identity]
+ * The function invoked per iteration.
+ * @param {number} [depth=1] The maximum recursion depth.
+ * @returns {Array} Returns the new flattened array.
+ * @example
+ *
+ * function duplicate(n) {
+ * return [[[n, n]]];
+ * }
+ *
+ * _.flatMapDepth([1, 2], duplicate, 2);
+ * // => [[1, 1], [2, 2]]
+ */
+ function flatMapDepth(collection, iteratee, depth) {
+ depth = depth === undefined ? 1 : toInteger(depth);
+ return baseFlatten(map(collection, iteratee), depth);
+ }
+
+ /**
+ * Iterates over elements of `collection` and invokes `iteratee` for each element.
+ * The iteratee is invoked with three arguments: (value, index|key, collection).
+ * Iteratee functions may exit iteration early by explicitly returning `false`.
+ *
+ * **Note:** As with other "Collections" methods, objects with a "length"
+ * property are iterated like arrays. To avoid this behavior use `_.forIn`
+ * or `_.forOwn` for object iteration.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.1.0
+ * @alias each
+ * @category Collection
+ * @param {Array|Object} collection The collection to iterate over.
+ * @param {Function} [iteratee=_.identity] The function invoked per iteration.
+ * @returns {Array|Object} Returns `collection`.
+ * @see _.forEachRight
+ * @example
+ *
+ * _.forEach([1, 2], function(value) {
+ * console.log(value);
+ * });
+ * // => Logs `1` then `2`.
+ *
+ * _.forEach({ 'a': 1, 'b': 2 }, function(value, key) {
+ * console.log(key);
+ * });
+ * // => Logs 'a' then 'b' (iteration order is not guaranteed).
+ */
+ function forEach(collection, iteratee) {
+ var func = isArray(collection) ? arrayEach : baseEach;
+ return func(collection, getIteratee(iteratee, 3));
+ }
+
+ /**
+ * This method is like `_.forEach` except that it iterates over elements of
+ * `collection` from right to left.
+ *
+ * @static
+ * @memberOf _
+ * @since 2.0.0
+ * @alias eachRight
+ * @category Collection
+ * @param {Array|Object} collection The collection to iterate over.
+ * @param {Function} [iteratee=_.identity] The function invoked per iteration.
+ * @returns {Array|Object} Returns `collection`.
+ * @see _.forEach
+ * @example
+ *
+ * _.forEachRight([1, 2], function(value) {
+ * console.log(value);
+ * });
+ * // => Logs `2` then `1`.
+ */
+ function forEachRight(collection, iteratee) {
+ var func = isArray(collection) ? arrayEachRight : baseEachRight;
+ return func(collection, getIteratee(iteratee, 3));
+ }
+
+ /**
+ * Creates an object composed of keys generated from the results of running
+ * each element of `collection` thru `iteratee`. The order of grouped values
+ * is determined by the order they occur in `collection`. The corresponding
+ * value of each key is an array of elements responsible for generating the
+ * key. The iteratee is invoked with one argument: (value).
+ *
+ * @static
+ * @memberOf _
+ * @since 0.1.0
+ * @category Collection
+ * @param {Array|Object} collection The collection to iterate over.
+ * @param {Function} [iteratee=_.identity]
+ * The iteratee to transform keys.
+ * @returns {Object} Returns the composed aggregate object.
+ * @example
+ *
+ * _.groupBy([6.1, 4.2, 6.3], Math.floor);
+ * // => { '4': [4.2], '6': [6.1, 6.3] }
+ *
+ * // The `_.property` iteratee shorthand.
+ * _.groupBy(['one', 'two', 'three'], 'length');
+ * // => { '3': ['one', 'two'], '5': ['three'] }
+ */
+ var groupBy = createAggregator(function(result, value, key) {
+ if (hasOwnProperty.call(result, key)) {
+ result[key].push(value);
+ } else {
+ baseAssignValue(result, key, [value]);
+ }
+ });
+
+ /**
+ * Checks if `value` is in `collection`. If `collection` is a string, it's
+ * checked for a substring of `value`, otherwise
+ * [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)
+ * is used for equality comparisons. If `fromIndex` is negative, it's used as
+ * the offset from the end of `collection`.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.1.0
+ * @category Collection
+ * @param {Array|Object|string} collection The collection to inspect.
+ * @param {*} value The value to search for.
+ * @param {number} [fromIndex=0] The index to search from.
+ * @param- {Object} [guard] Enables use as an iteratee for methods like `_.reduce`.
+ * @returns {boolean} Returns `true` if `value` is found, else `false`.
+ * @example
+ *
+ * _.includes([1, 2, 3], 1);
+ * // => true
+ *
+ * _.includes([1, 2, 3], 1, 2);
+ * // => false
+ *
+ * _.includes({ 'a': 1, 'b': 2 }, 1);
+ * // => true
+ *
+ * _.includes('abcd', 'bc');
+ * // => true
+ */
+ function includes(collection, value, fromIndex, guard) {
+ collection = isArrayLike(collection) ? collection : values(collection);
+ fromIndex = (fromIndex && !guard) ? toInteger(fromIndex) : 0;
+
+ var length = collection.length;
+ if (fromIndex < 0) {
+ fromIndex = nativeMax(length + fromIndex, 0);
+ }
+ return isString(collection)
+ ? (fromIndex <= length && collection.indexOf(value, fromIndex) > -1)
+ : (!!length && baseIndexOf(collection, value, fromIndex) > -1);
+ }
+
+ /**
+ * Invokes the method at `path` of each element in `collection`, returning
+ * an array of the results of each invoked method. Any additional arguments
+ * are provided to each invoked method. If `path` is a function, it's invoked
+ * for, and `this` bound to, each element in `collection`.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Collection
+ * @param {Array|Object} collection The collection to iterate over.
+ * @param {Array|Function|string} path The path of the method to invoke or
+ * the function invoked per iteration.
+ * @param {...*} [args] The arguments to invoke each method with.
+ * @returns {Array} Returns the array of results.
+ * @example
+ *
+ * _.invokeMap([[5, 1, 7], [3, 2, 1]], 'sort');
+ * // => [[1, 5, 7], [1, 2, 3]]
+ *
+ * _.invokeMap([123, 456], String.prototype.split, '');
+ * // => [['1', '2', '3'], ['4', '5', '6']]
+ */
+ var invokeMap = baseRest(function(collection, path, args) {
+ var index = -1,
+ isFunc = typeof path == 'function',
+ isProp = isKey(path),
+ result = isArrayLike(collection) ? Array(collection.length) : [];
+
+ baseEach(collection, function(value) {
+ var func = isFunc ? path : ((isProp && value != null) ? value[path] : undefined);
+ result[++index] = func ? apply(func, value, args) : baseInvoke(value, path, args);
+ });
+ return result;
+ });
+
+ /**
+ * Creates an object composed of keys generated from the results of running
+ * each element of `collection` thru `iteratee`. The corresponding value of
+ * each key is the last element responsible for generating the key. The
+ * iteratee is invoked with one argument: (value).
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Collection
+ * @param {Array|Object} collection The collection to iterate over.
+ * @param {Function} [iteratee=_.identity]
+ * The iteratee to transform keys.
+ * @returns {Object} Returns the composed aggregate object.
+ * @example
+ *
+ * var array = [
+ * { 'dir': 'left', 'code': 97 },
+ * { 'dir': 'right', 'code': 100 }
+ * ];
+ *
+ * _.keyBy(array, function(o) {
+ * return String.fromCharCode(o.code);
+ * });
+ * // => { 'a': { 'dir': 'left', 'code': 97 }, 'd': { 'dir': 'right', 'code': 100 } }
+ *
+ * _.keyBy(array, 'dir');
+ * // => { 'left': { 'dir': 'left', 'code': 97 }, 'right': { 'dir': 'right', 'code': 100 } }
+ */
+ var keyBy = createAggregator(function(result, value, key) {
+ baseAssignValue(result, key, value);
+ });
+
+ /**
+ * Creates an array of values by running each element in `collection` thru
+ * `iteratee`. The iteratee is invoked with three arguments:
+ * (value, index|key, collection).
+ *
+ * Many lodash methods are guarded to work as iteratees for methods like
+ * `_.every`, `_.filter`, `_.map`, `_.mapValues`, `_.reject`, and `_.some`.
+ *
+ * The guarded methods are:
+ * `ary`, `chunk`, `curry`, `curryRight`, `drop`, `dropRight`, `every`,
+ * `fill`, `invert`, `parseInt`, `random`, `range`, `rangeRight`, `repeat`,
+ * `sampleSize`, `slice`, `some`, `sortBy`, `split`, `take`, `takeRight`,
+ * `template`, `trim`, `trimEnd`, `trimStart`, and `words`
+ *
+ * @static
+ * @memberOf _
+ * @since 0.1.0
+ * @category Collection
+ * @param {Array|Object} collection The collection to iterate over.
+ * @param {Function} [iteratee=_.identity] The function invoked per iteration.
+ * @returns {Array} Returns the new mapped array.
+ * @example
+ *
+ * function square(n) {
+ * return n * n;
+ * }
+ *
+ * _.map([4, 8], square);
+ * // => [16, 64]
+ *
+ * _.map({ 'a': 4, 'b': 8 }, square);
+ * // => [16, 64] (iteration order is not guaranteed)
+ *
+ * var users = [
+ * { 'user': 'barney' },
+ * { 'user': 'fred' }
+ * ];
+ *
+ * // The `_.property` iteratee shorthand.
+ * _.map(users, 'user');
+ * // => ['barney', 'fred']
+ */
+ function map(collection, iteratee) {
+ var func = isArray(collection) ? arrayMap : baseMap;
+ return func(collection, getIteratee(iteratee, 3));
+ }
+
+ /**
+ * This method is like `_.sortBy` except that it allows specifying the sort
+ * orders of the iteratees to sort by. If `orders` is unspecified, all values
+ * are sorted in ascending order. Otherwise, specify an order of "desc" for
+ * descending or "asc" for ascending sort order of corresponding values.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Collection
+ * @param {Array|Object} collection The collection to iterate over.
+ * @param {Array[]|Function[]|Object[]|string[]} [iteratees=[_.identity]]
+ * The iteratees to sort by.
+ * @param {string[]} [orders] The sort orders of `iteratees`.
+ * @param- {Object} [guard] Enables use as an iteratee for methods like `_.reduce`.
+ * @returns {Array} Returns the new sorted array.
+ * @example
+ *
+ * var users = [
+ * { 'user': 'fred', 'age': 48 },
+ * { 'user': 'barney', 'age': 34 },
+ * { 'user': 'fred', 'age': 40 },
+ * { 'user': 'barney', 'age': 36 }
+ * ];
+ *
+ * // Sort by `user` in ascending order and by `age` in descending order.
+ * _.orderBy(users, ['user', 'age'], ['asc', 'desc']);
+ * // => objects for [['barney', 36], ['barney', 34], ['fred', 48], ['fred', 40]]
+ */
+ function orderBy(collection, iteratees, orders, guard) {
+ if (collection == null) {
+ return [];
+ }
+ if (!isArray(iteratees)) {
+ iteratees = iteratees == null ? [] : [iteratees];
+ }
+ orders = guard ? undefined : orders;
+ if (!isArray(orders)) {
+ orders = orders == null ? [] : [orders];
+ }
+ return baseOrderBy(collection, iteratees, orders);
+ }
+
+ /**
+ * Creates an array of elements split into two groups, the first of which
+ * contains elements `predicate` returns truthy for, the second of which
+ * contains elements `predicate` returns falsey for. The predicate is
+ * invoked with one argument: (value).
+ *
+ * @static
+ * @memberOf _
+ * @since 3.0.0
+ * @category Collection
+ * @param {Array|Object} collection The collection to iterate over.
+ * @param {Function} [predicate=_.identity] The function invoked per iteration.
+ * @returns {Array} Returns the array of grouped elements.
+ * @example
+ *
+ * var users = [
+ * { 'user': 'barney', 'age': 36, 'active': false },
+ * { 'user': 'fred', 'age': 40, 'active': true },
+ * { 'user': 'pebbles', 'age': 1, 'active': false }
+ * ];
+ *
+ * _.partition(users, function(o) { return o.active; });
+ * // => objects for [['fred'], ['barney', 'pebbles']]
+ *
+ * // The `_.matches` iteratee shorthand.
+ * _.partition(users, { 'age': 1, 'active': false });
+ * // => objects for [['pebbles'], ['barney', 'fred']]
+ *
+ * // The `_.matchesProperty` iteratee shorthand.
+ * _.partition(users, ['active', false]);
+ * // => objects for [['barney', 'pebbles'], ['fred']]
+ *
+ * // The `_.property` iteratee shorthand.
+ * _.partition(users, 'active');
+ * // => objects for [['fred'], ['barney', 'pebbles']]
+ */
+ var partition = createAggregator(function(result, value, key) {
+ result[key ? 0 : 1].push(value);
+ }, function() { return [[], []]; });
+
+ /**
+ * Reduces `collection` to a value which is the accumulated result of running
+ * each element in `collection` thru `iteratee`, where each successive
+ * invocation is supplied the return value of the previous. If `accumulator`
+ * is not given, the first element of `collection` is used as the initial
+ * value. The iteratee is invoked with four arguments:
+ * (accumulator, value, index|key, collection).
+ *
+ * Many lodash methods are guarded to work as iteratees for methods like
+ * `_.reduce`, `_.reduceRight`, and `_.transform`.
+ *
+ * The guarded methods are:
+ * `assign`, `defaults`, `defaultsDeep`, `includes`, `merge`, `orderBy`,
+ * and `sortBy`
+ *
+ * @static
+ * @memberOf _
+ * @since 0.1.0
+ * @category Collection
+ * @param {Array|Object} collection The collection to iterate over.
+ * @param {Function} [iteratee=_.identity] The function invoked per iteration.
+ * @param {*} [accumulator] The initial value.
+ * @returns {*} Returns the accumulated value.
+ * @see _.reduceRight
+ * @example
+ *
+ * _.reduce([1, 2], function(sum, n) {
+ * return sum + n;
+ * }, 0);
+ * // => 3
+ *
+ * _.reduce({ 'a': 1, 'b': 2, 'c': 1 }, function(result, value, key) {
+ * (result[value] || (result[value] = [])).push(key);
+ * return result;
+ * }, {});
+ * // => { '1': ['a', 'c'], '2': ['b'] } (iteration order is not guaranteed)
+ */
+ function reduce(collection, iteratee, accumulator) {
+ var func = isArray(collection) ? arrayReduce : baseReduce,
+ initAccum = arguments.length < 3;
+
+ return func(collection, getIteratee(iteratee, 4), accumulator, initAccum, baseEach);
+ }
+
+ /**
+ * This method is like `_.reduce` except that it iterates over elements of
+ * `collection` from right to left.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.1.0
+ * @category Collection
+ * @param {Array|Object} collection The collection to iterate over.
+ * @param {Function} [iteratee=_.identity] The function invoked per iteration.
+ * @param {*} [accumulator] The initial value.
+ * @returns {*} Returns the accumulated value.
+ * @see _.reduce
+ * @example
+ *
+ * var array = [[0, 1], [2, 3], [4, 5]];
+ *
+ * _.reduceRight(array, function(flattened, other) {
+ * return flattened.concat(other);
+ * }, []);
+ * // => [4, 5, 2, 3, 0, 1]
+ */
+ function reduceRight(collection, iteratee, accumulator) {
+ var func = isArray(collection) ? arrayReduceRight : baseReduce,
+ initAccum = arguments.length < 3;
+
+ return func(collection, getIteratee(iteratee, 4), accumulator, initAccum, baseEachRight);
+ }
+
+ /**
+ * The opposite of `_.filter`; this method returns the elements of `collection`
+ * that `predicate` does **not** return truthy for.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.1.0
+ * @category Collection
+ * @param {Array|Object} collection The collection to iterate over.
+ * @param {Function} [predicate=_.identity] The function invoked per iteration.
+ * @returns {Array} Returns the new filtered array.
+ * @see _.filter
+ * @example
+ *
+ * var users = [
+ * { 'user': 'barney', 'age': 36, 'active': false },
+ * { 'user': 'fred', 'age': 40, 'active': true }
+ * ];
+ *
+ * _.reject(users, function(o) { return !o.active; });
+ * // => objects for ['fred']
+ *
+ * // The `_.matches` iteratee shorthand.
+ * _.reject(users, { 'age': 40, 'active': true });
+ * // => objects for ['barney']
+ *
+ * // The `_.matchesProperty` iteratee shorthand.
+ * _.reject(users, ['active', false]);
+ * // => objects for ['fred']
+ *
+ * // The `_.property` iteratee shorthand.
+ * _.reject(users, 'active');
+ * // => objects for ['barney']
+ */
+ function reject(collection, predicate) {
+ var func = isArray(collection) ? arrayFilter : baseFilter;
+ return func(collection, negate(getIteratee(predicate, 3)));
+ }
+
+ /**
+ * Gets a random element from `collection`.
+ *
+ * @static
+ * @memberOf _
+ * @since 2.0.0
+ * @category Collection
+ * @param {Array|Object} collection The collection to sample.
+ * @returns {*} Returns the random element.
+ * @example
+ *
+ * _.sample([1, 2, 3, 4]);
+ * // => 2
+ */
+ function sample(collection) {
+ var func = isArray(collection) ? arraySample : baseSample;
+ return func(collection);
+ }
+
+ /**
+ * Gets `n` random elements at unique keys from `collection` up to the
+ * size of `collection`.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Collection
+ * @param {Array|Object} collection The collection to sample.
+ * @param {number} [n=1] The number of elements to sample.
+ * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.
+ * @returns {Array} Returns the random elements.
+ * @example
+ *
+ * _.sampleSize([1, 2, 3], 2);
+ * // => [3, 1]
+ *
+ * _.sampleSize([1, 2, 3], 4);
+ * // => [2, 3, 1]
+ */
+ function sampleSize(collection, n, guard) {
+ if ((guard ? isIterateeCall(collection, n, guard) : n === undefined)) {
+ n = 1;
+ } else {
+ n = toInteger(n);
+ }
+ var func = isArray(collection) ? arraySampleSize : baseSampleSize;
+ return func(collection, n);
+ }
+
+ /**
+ * Creates an array of shuffled values, using a version of the
+ * [Fisher-Yates shuffle](https://en.wikipedia.org/wiki/Fisher-Yates_shuffle).
+ *
+ * @static
+ * @memberOf _
+ * @since 0.1.0
+ * @category Collection
+ * @param {Array|Object} collection The collection to shuffle.
+ * @returns {Array} Returns the new shuffled array.
+ * @example
+ *
+ * _.shuffle([1, 2, 3, 4]);
+ * // => [4, 1, 3, 2]
+ */
+ function shuffle(collection) {
+ var func = isArray(collection) ? arrayShuffle : baseShuffle;
+ return func(collection);
+ }
+
+ /**
+ * Gets the size of `collection` by returning its length for array-like
+ * values or the number of own enumerable string keyed properties for objects.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.1.0
+ * @category Collection
+ * @param {Array|Object|string} collection The collection to inspect.
+ * @returns {number} Returns the collection size.
+ * @example
+ *
+ * _.size([1, 2, 3]);
+ * // => 3
+ *
+ * _.size({ 'a': 1, 'b': 2 });
+ * // => 2
+ *
+ * _.size('pebbles');
+ * // => 7
+ */
+ function size(collection) {
+ if (collection == null) {
+ return 0;
+ }
+ if (isArrayLike(collection)) {
+ return isString(collection) ? stringSize(collection) : collection.length;
+ }
+ var tag = getTag(collection);
+ if (tag == mapTag || tag == setTag) {
+ return collection.size;
+ }
+ return baseKeys(collection).length;
+ }
+
+ /**
+ * Checks if `predicate` returns truthy for **any** element of `collection`.
+ * Iteration is stopped once `predicate` returns truthy. The predicate is
+ * invoked with three arguments: (value, index|key, collection).
+ *
+ * @static
+ * @memberOf _
+ * @since 0.1.0
+ * @category Collection
+ * @param {Array|Object} collection The collection to iterate over.
+ * @param {Function} [predicate=_.identity] The function invoked per iteration.
+ * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.
+ * @returns {boolean} Returns `true` if any element passes the predicate check,
+ * else `false`.
+ * @example
+ *
+ * _.some([null, 0, 'yes', false], Boolean);
+ * // => true
+ *
+ * var users = [
+ * { 'user': 'barney', 'active': true },
+ * { 'user': 'fred', 'active': false }
+ * ];
+ *
+ * // The `_.matches` iteratee shorthand.
+ * _.some(users, { 'user': 'barney', 'active': false });
+ * // => false
+ *
+ * // The `_.matchesProperty` iteratee shorthand.
+ * _.some(users, ['active', false]);
+ * // => true
+ *
+ * // The `_.property` iteratee shorthand.
+ * _.some(users, 'active');
+ * // => true
+ */
+ function some(collection, predicate, guard) {
+ var func = isArray(collection) ? arraySome : baseSome;
+ if (guard && isIterateeCall(collection, predicate, guard)) {
+ predicate = undefined;
+ }
+ return func(collection, getIteratee(predicate, 3));
+ }
+
+ /**
+ * Creates an array of elements, sorted in ascending order by the results of
+ * running each element in a collection thru each iteratee. This method
+ * performs a stable sort, that is, it preserves the original sort order of
+ * equal elements. The iteratees are invoked with one argument: (value).
+ *
+ * @static
+ * @memberOf _
+ * @since 0.1.0
+ * @category Collection
+ * @param {Array|Object} collection The collection to iterate over.
+ * @param {...(Function|Function[])} [iteratees=[_.identity]]
+ * The iteratees to sort by.
+ * @returns {Array} Returns the new sorted array.
+ * @example
+ *
+ * var users = [
+ * { 'user': 'fred', 'age': 48 },
+ * { 'user': 'barney', 'age': 36 },
+ * { 'user': 'fred', 'age': 40 },
+ * { 'user': 'barney', 'age': 34 }
+ * ];
+ *
+ * _.sortBy(users, [function(o) { return o.user; }]);
+ * // => objects for [['barney', 36], ['barney', 34], ['fred', 48], ['fred', 40]]
+ *
+ * _.sortBy(users, ['user', 'age']);
+ * // => objects for [['barney', 34], ['barney', 36], ['fred', 40], ['fred', 48]]
+ */
+ var sortBy = baseRest(function(collection, iteratees) {
+ if (collection == null) {
+ return [];
+ }
+ var length = iteratees.length;
+ if (length > 1 && isIterateeCall(collection, iteratees[0], iteratees[1])) {
+ iteratees = [];
+ } else if (length > 2 && isIterateeCall(iteratees[0], iteratees[1], iteratees[2])) {
+ iteratees = [iteratees[0]];
+ }
+ return baseOrderBy(collection, baseFlatten(iteratees, 1), []);
+ });
+
+ /*------------------------------------------------------------------------*/
+
+ /**
+ * Gets the timestamp of the number of milliseconds that have elapsed since
+ * the Unix epoch (1 January 1970 00:00:00 UTC).
+ *
+ * @static
+ * @memberOf _
+ * @since 2.4.0
+ * @category Date
+ * @returns {number} Returns the timestamp.
+ * @example
+ *
+ * _.defer(function(stamp) {
+ * console.log(_.now() - stamp);
+ * }, _.now());
+ * // => Logs the number of milliseconds it took for the deferred invocation.
+ */
+ var now = ctxNow || function() {
+ return root.Date.now();
+ };
+
+ /*------------------------------------------------------------------------*/
+
+ /**
+ * The opposite of `_.before`; this method creates a function that invokes
+ * `func` once it's called `n` or more times.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.1.0
+ * @category Function
+ * @param {number} n The number of calls before `func` is invoked.
+ * @param {Function} func The function to restrict.
+ * @returns {Function} Returns the new restricted function.
+ * @example
+ *
+ * var saves = ['profile', 'settings'];
+ *
+ * var done = _.after(saves.length, function() {
+ * console.log('done saving!');
+ * });
+ *
+ * _.forEach(saves, function(type) {
+ * asyncSave({ 'type': type, 'complete': done });
+ * });
+ * // => Logs 'done saving!' after the two async saves have completed.
+ */
+ function after(n, func) {
+ if (typeof func != 'function') {
+ throw new TypeError(FUNC_ERROR_TEXT);
+ }
+ n = toInteger(n);
+ return function() {
+ if (--n < 1) {
+ return func.apply(this, arguments);
+ }
+ };
+ }
+
+ /**
+ * Creates a function that invokes `func`, with up to `n` arguments,
+ * ignoring any additional arguments.
+ *
+ * @static
+ * @memberOf _
+ * @since 3.0.0
+ * @category Function
+ * @param {Function} func The function to cap arguments for.
+ * @param {number} [n=func.length] The arity cap.
+ * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.
+ * @returns {Function} Returns the new capped function.
+ * @example
+ *
+ * _.map(['6', '8', '10'], _.ary(parseInt, 1));
+ * // => [6, 8, 10]
+ */
+ function ary(func, n, guard) {
+ n = guard ? undefined : n;
+ n = (func && n == null) ? func.length : n;
+ return createWrap(func, ARY_FLAG, undefined, undefined, undefined, undefined, n);
+ }
+
+ /**
+ * Creates a function that invokes `func`, with the `this` binding and arguments
+ * of the created function, while it's called less than `n` times. Subsequent
+ * calls to the created function return the result of the last `func` invocation.
+ *
+ * @static
+ * @memberOf _
+ * @since 3.0.0
+ * @category Function
+ * @param {number} n The number of calls at which `func` is no longer invoked.
+ * @param {Function} func The function to restrict.
+ * @returns {Function} Returns the new restricted function.
+ * @example
+ *
+ * jQuery(element).on('click', _.before(5, addContactToList));
+ * // => Allows adding up to 4 contacts to the list.
+ */
+ function before(n, func) {
+ var result;
+ if (typeof func != 'function') {
+ throw new TypeError(FUNC_ERROR_TEXT);
+ }
+ n = toInteger(n);
+ return function() {
+ if (--n > 0) {
+ result = func.apply(this, arguments);
+ }
+ if (n <= 1) {
+ func = undefined;
+ }
+ return result;
+ };
+ }
+
+ /**
+ * Creates a function that invokes `func` with the `this` binding of `thisArg`
+ * and `partials` prepended to the arguments it receives.
+ *
+ * The `_.bind.placeholder` value, which defaults to `_` in monolithic builds,
+ * may be used as a placeholder for partially applied arguments.
+ *
+ * **Note:** Unlike native `Function#bind`, this method doesn't set the "length"
+ * property of bound functions.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.1.0
+ * @category Function
+ * @param {Function} func The function to bind.
+ * @param {*} thisArg The `this` binding of `func`.
+ * @param {...*} [partials] The arguments to be partially applied.
+ * @returns {Function} Returns the new bound function.
+ * @example
+ *
+ * function greet(greeting, punctuation) {
+ * return greeting + ' ' + this.user + punctuation;
+ * }
+ *
+ * var object = { 'user': 'fred' };
+ *
+ * var bound = _.bind(greet, object, 'hi');
+ * bound('!');
+ * // => 'hi fred!'
+ *
+ * // Bound with placeholders.
+ * var bound = _.bind(greet, object, _, '!');
+ * bound('hi');
+ * // => 'hi fred!'
+ */
+ var bind = baseRest(function(func, thisArg, partials) {
+ var bitmask = BIND_FLAG;
+ if (partials.length) {
+ var holders = replaceHolders(partials, getHolder(bind));
+ bitmask |= PARTIAL_FLAG;
+ }
+ return createWrap(func, bitmask, thisArg, partials, holders);
+ });
+
+ /**
+ * Creates a function that invokes the method at `object[key]` with `partials`
+ * prepended to the arguments it receives.
+ *
+ * This method differs from `_.bind` by allowing bound functions to reference
+ * methods that may be redefined or don't yet exist. See
+ * [Peter Michaux's article](http://peter.michaux.ca/articles/lazy-function-definition-pattern)
+ * for more details.
+ *
+ * The `_.bindKey.placeholder` value, which defaults to `_` in monolithic
+ * builds, may be used as a placeholder for partially applied arguments.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.10.0
+ * @category Function
+ * @param {Object} object The object to invoke the method on.
+ * @param {string} key The key of the method.
+ * @param {...*} [partials] The arguments to be partially applied.
+ * @returns {Function} Returns the new bound function.
+ * @example
+ *
+ * var object = {
+ * 'user': 'fred',
+ * 'greet': function(greeting, punctuation) {
+ * return greeting + ' ' + this.user + punctuation;
+ * }
+ * };
+ *
+ * var bound = _.bindKey(object, 'greet', 'hi');
+ * bound('!');
+ * // => 'hi fred!'
+ *
+ * object.greet = function(greeting, punctuation) {
+ * return greeting + 'ya ' + this.user + punctuation;
+ * };
+ *
+ * bound('!');
+ * // => 'hiya fred!'
+ *
+ * // Bound with placeholders.
+ * var bound = _.bindKey(object, 'greet', _, '!');
+ * bound('hi');
+ * // => 'hiya fred!'
+ */
+ var bindKey = baseRest(function(object, key, partials) {
+ var bitmask = BIND_FLAG | BIND_KEY_FLAG;
+ if (partials.length) {
+ var holders = replaceHolders(partials, getHolder(bindKey));
+ bitmask |= PARTIAL_FLAG;
+ }
+ return createWrap(key, bitmask, object, partials, holders);
+ });
+
+ /**
+ * Creates a function that accepts arguments of `func` and either invokes
+ * `func` returning its result, if at least `arity` number of arguments have
+ * been provided, or returns a function that accepts the remaining `func`
+ * arguments, and so on. The arity of `func` may be specified if `func.length`
+ * is not sufficient.
+ *
+ * The `_.curry.placeholder` value, which defaults to `_` in monolithic builds,
+ * may be used as a placeholder for provided arguments.
+ *
+ * **Note:** This method doesn't set the "length" property of curried functions.
+ *
+ * @static
+ * @memberOf _
+ * @since 2.0.0
+ * @category Function
+ * @param {Function} func The function to curry.
+ * @param {number} [arity=func.length] The arity of `func`.
+ * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.
+ * @returns {Function} Returns the new curried function.
+ * @example
+ *
+ * var abc = function(a, b, c) {
+ * return [a, b, c];
+ * };
+ *
+ * var curried = _.curry(abc);
+ *
+ * curried(1)(2)(3);
+ * // => [1, 2, 3]
+ *
+ * curried(1, 2)(3);
+ * // => [1, 2, 3]
+ *
+ * curried(1, 2, 3);
+ * // => [1, 2, 3]
+ *
+ * // Curried with placeholders.
+ * curried(1)(_, 3)(2);
+ * // => [1, 2, 3]
+ */
+ function curry(func, arity, guard) {
+ arity = guard ? undefined : arity;
+ var result = createWrap(func, CURRY_FLAG, undefined, undefined, undefined, undefined, undefined, arity);
+ result.placeholder = curry.placeholder;
+ return result;
+ }
+
+ /**
+ * This method is like `_.curry` except that arguments are applied to `func`
+ * in the manner of `_.partialRight` instead of `_.partial`.
+ *
+ * The `_.curryRight.placeholder` value, which defaults to `_` in monolithic
+ * builds, may be used as a placeholder for provided arguments.
+ *
+ * **Note:** This method doesn't set the "length" property of curried functions.
+ *
+ * @static
+ * @memberOf _
+ * @since 3.0.0
+ * @category Function
+ * @param {Function} func The function to curry.
+ * @param {number} [arity=func.length] The arity of `func`.
+ * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.
+ * @returns {Function} Returns the new curried function.
+ * @example
+ *
+ * var abc = function(a, b, c) {
+ * return [a, b, c];
+ * };
+ *
+ * var curried = _.curryRight(abc);
+ *
+ * curried(3)(2)(1);
+ * // => [1, 2, 3]
+ *
+ * curried(2, 3)(1);
+ * // => [1, 2, 3]
+ *
+ * curried(1, 2, 3);
+ * // => [1, 2, 3]
+ *
+ * // Curried with placeholders.
+ * curried(3)(1, _)(2);
+ * // => [1, 2, 3]
+ */
+ function curryRight(func, arity, guard) {
+ arity = guard ? undefined : arity;
+ var result = createWrap(func, CURRY_RIGHT_FLAG, undefined, undefined, undefined, undefined, undefined, arity);
+ result.placeholder = curryRight.placeholder;
+ return result;
+ }
+
+ /**
+ * Creates a debounced function that delays invoking `func` until after `wait`
+ * milliseconds have elapsed since the last time the debounced function was
+ * invoked. The debounced function comes with a `cancel` method to cancel
+ * delayed `func` invocations and a `flush` method to immediately invoke them.
+ * Provide `options` to indicate whether `func` should be invoked on the
+ * leading and/or trailing edge of the `wait` timeout. The `func` is invoked
+ * with the last arguments provided to the debounced function. Subsequent
+ * calls to the debounced function return the result of the last `func`
+ * invocation.
+ *
+ * **Note:** If `leading` and `trailing` options are `true`, `func` is
+ * invoked on the trailing edge of the timeout only if the debounced function
+ * is invoked more than once during the `wait` timeout.
+ *
+ * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred
+ * until to the next tick, similar to `setTimeout` with a timeout of `0`.
+ *
+ * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/)
+ * for details over the differences between `_.debounce` and `_.throttle`.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.1.0
+ * @category Function
+ * @param {Function} func The function to debounce.
+ * @param {number} [wait=0] The number of milliseconds to delay.
+ * @param {Object} [options={}] The options object.
+ * @param {boolean} [options.leading=false]
+ * Specify invoking on the leading edge of the timeout.
+ * @param {number} [options.maxWait]
+ * The maximum time `func` is allowed to be delayed before it's invoked.
+ * @param {boolean} [options.trailing=true]
+ * Specify invoking on the trailing edge of the timeout.
+ * @returns {Function} Returns the new debounced function.
+ * @example
+ *
+ * // Avoid costly calculations while the window size is in flux.
+ * jQuery(window).on('resize', _.debounce(calculateLayout, 150));
+ *
+ * // Invoke `sendMail` when clicked, debouncing subsequent calls.
+ * jQuery(element).on('click', _.debounce(sendMail, 300, {
+ * 'leading': true,
+ * 'trailing': false
+ * }));
+ *
+ * // Ensure `batchLog` is invoked once after 1 second of debounced calls.
+ * var debounced = _.debounce(batchLog, 250, { 'maxWait': 1000 });
+ * var source = new EventSource('/stream');
+ * jQuery(source).on('message', debounced);
+ *
+ * // Cancel the trailing debounced invocation.
+ * jQuery(window).on('popstate', debounced.cancel);
+ */
+ function debounce(func, wait, options) {
+ var lastArgs,
+ lastThis,
+ maxWait,
+ result,
+ timerId,
+ lastCallTime,
+ lastInvokeTime = 0,
+ leading = false,
+ maxing = false,
+ trailing = true;
+
+ if (typeof func != 'function') {
+ throw new TypeError(FUNC_ERROR_TEXT);
+ }
+ wait = toNumber(wait) || 0;
+ if (isObject(options)) {
+ leading = !!options.leading;
+ maxing = 'maxWait' in options;
+ maxWait = maxing ? nativeMax(toNumber(options.maxWait) || 0, wait) : maxWait;
+ trailing = 'trailing' in options ? !!options.trailing : trailing;
+ }
+
+ function invokeFunc(time) {
+ var args = lastArgs,
+ thisArg = lastThis;
+
+ lastArgs = lastThis = undefined;
+ lastInvokeTime = time;
+ result = func.apply(thisArg, args);
+ return result;
+ }
+
+ function leadingEdge(time) {
+ // Reset any `maxWait` timer.
+ lastInvokeTime = time;
+ // Start the timer for the trailing edge.
+ timerId = setTimeout(timerExpired, wait);
+ // Invoke the leading edge.
+ return leading ? invokeFunc(time) : result;
+ }
+
+ function remainingWait(time) {
+ var timeSinceLastCall = time - lastCallTime,
+ timeSinceLastInvoke = time - lastInvokeTime,
+ result = wait - timeSinceLastCall;
+
+ return maxing ? nativeMin(result, maxWait - timeSinceLastInvoke) : result;
+ }
+
+ function shouldInvoke(time) {
+ var timeSinceLastCall = time - lastCallTime,
+ timeSinceLastInvoke = time - lastInvokeTime;
+
+ // Either this is the first call, activity has stopped and we're at the
+ // trailing edge, the system time has gone backwards and we're treating
+ // it as the trailing edge, or we've hit the `maxWait` limit.
+ return (lastCallTime === undefined || (timeSinceLastCall >= wait) ||
+ (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait));
+ }
+
+ function timerExpired() {
+ var time = now();
+ if (shouldInvoke(time)) {
+ return trailingEdge(time);
+ }
+ // Restart the timer.
+ timerId = setTimeout(timerExpired, remainingWait(time));
+ }
+
+ function trailingEdge(time) {
+ timerId = undefined;
+
+ // Only invoke if we have `lastArgs` which means `func` has been
+ // debounced at least once.
+ if (trailing && lastArgs) {
+ return invokeFunc(time);
+ }
+ lastArgs = lastThis = undefined;
+ return result;
+ }
+
+ function cancel() {
+ if (timerId !== undefined) {
+ clearTimeout(timerId);
+ }
+ lastInvokeTime = 0;
+ lastArgs = lastCallTime = lastThis = timerId = undefined;
+ }
+
+ function flush() {
+ return timerId === undefined ? result : trailingEdge(now());
+ }
+
+ function debounced() {
+ var time = now(),
+ isInvoking = shouldInvoke(time);
+
+ lastArgs = arguments;
+ lastThis = this;
+ lastCallTime = time;
+
+ if (isInvoking) {
+ if (timerId === undefined) {
+ return leadingEdge(lastCallTime);
+ }
+ if (maxing) {
+ // Handle invocations in a tight loop.
+ timerId = setTimeout(timerExpired, wait);
+ return invokeFunc(lastCallTime);
+ }
+ }
+ if (timerId === undefined) {
+ timerId = setTimeout(timerExpired, wait);
+ }
+ return result;
+ }
+ debounced.cancel = cancel;
+ debounced.flush = flush;
+ return debounced;
+ }
+
+ /**
+ * Defers invoking the `func` until the current call stack has cleared. Any
+ * additional arguments are provided to `func` when it's invoked.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.1.0
+ * @category Function
+ * @param {Function} func The function to defer.
+ * @param {...*} [args] The arguments to invoke `func` with.
+ * @returns {number} Returns the timer id.
+ * @example
+ *
+ * _.defer(function(text) {
+ * console.log(text);
+ * }, 'deferred');
+ * // => Logs 'deferred' after one millisecond.
+ */
+ var defer = baseRest(function(func, args) {
+ return baseDelay(func, 1, args);
+ });
+
+ /**
+ * Invokes `func` after `wait` milliseconds. Any additional arguments are
+ * provided to `func` when it's invoked.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.1.0
+ * @category Function
+ * @param {Function} func The function to delay.
+ * @param {number} wait The number of milliseconds to delay invocation.
+ * @param {...*} [args] The arguments to invoke `func` with.
+ * @returns {number} Returns the timer id.
+ * @example
+ *
+ * _.delay(function(text) {
+ * console.log(text);
+ * }, 1000, 'later');
+ * // => Logs 'later' after one second.
+ */
+ var delay = baseRest(function(func, wait, args) {
+ return baseDelay(func, toNumber(wait) || 0, args);
+ });
+
+ /**
+ * Creates a function that invokes `func` with arguments reversed.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Function
+ * @param {Function} func The function to flip arguments for.
+ * @returns {Function} Returns the new flipped function.
+ * @example
+ *
+ * var flipped = _.flip(function() {
+ * return _.toArray(arguments);
+ * });
+ *
+ * flipped('a', 'b', 'c', 'd');
+ * // => ['d', 'c', 'b', 'a']
+ */
+ function flip(func) {
+ return createWrap(func, FLIP_FLAG);
+ }
+
+ /**
+ * Creates a function that memoizes the result of `func`. If `resolver` is
+ * provided, it determines the cache key for storing the result based on the
+ * arguments provided to the memoized function. By default, the first argument
+ * provided to the memoized function is used as the map cache key. The `func`
+ * is invoked with the `this` binding of the memoized function.
+ *
+ * **Note:** The cache is exposed as the `cache` property on the memoized
+ * function. Its creation may be customized by replacing the `_.memoize.Cache`
+ * constructor with one whose instances implement the
+ * [`Map`](http://ecma-international.org/ecma-262/7.0/#sec-properties-of-the-map-prototype-object)
+ * method interface of `delete`, `get`, `has`, and `set`.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.1.0
+ * @category Function
+ * @param {Function} func The function to have its output memoized.
+ * @param {Function} [resolver] The function to resolve the cache key.
+ * @returns {Function} Returns the new memoized function.
+ * @example
+ *
+ * var object = { 'a': 1, 'b': 2 };
+ * var other = { 'c': 3, 'd': 4 };
+ *
+ * var values = _.memoize(_.values);
+ * values(object);
+ * // => [1, 2]
+ *
+ * values(other);
+ * // => [3, 4]
+ *
+ * object.a = 2;
+ * values(object);
+ * // => [1, 2]
+ *
+ * // Modify the result cache.
+ * values.cache.set(object, ['a', 'b']);
+ * values(object);
+ * // => ['a', 'b']
+ *
+ * // Replace `_.memoize.Cache`.
+ * _.memoize.Cache = WeakMap;
+ */
+ function memoize(func, resolver) {
+ if (typeof func != 'function' || (resolver && typeof resolver != 'function')) {
+ throw new TypeError(FUNC_ERROR_TEXT);
+ }
+ var memoized = function() {
+ var args = arguments,
+ key = resolver ? resolver.apply(this, args) : args[0],
+ cache = memoized.cache;
+
+ if (cache.has(key)) {
+ return cache.get(key);
+ }
+ var result = func.apply(this, args);
+ memoized.cache = cache.set(key, result) || cache;
+ return result;
+ };
+ memoized.cache = new (memoize.Cache || MapCache);
+ return memoized;
+ }
+
+ // Expose `MapCache`.
+ memoize.Cache = MapCache;
+
+ /**
+ * Creates a function that negates the result of the predicate `func`. The
+ * `func` predicate is invoked with the `this` binding and arguments of the
+ * created function.
+ *
+ * @static
+ * @memberOf _
+ * @since 3.0.0
+ * @category Function
+ * @param {Function} predicate The predicate to negate.
+ * @returns {Function} Returns the new negated function.
+ * @example
+ *
+ * function isEven(n) {
+ * return n % 2 == 0;
+ * }
+ *
+ * _.filter([1, 2, 3, 4, 5, 6], _.negate(isEven));
+ * // => [1, 3, 5]
+ */
+ function negate(predicate) {
+ if (typeof predicate != 'function') {
+ throw new TypeError(FUNC_ERROR_TEXT);
+ }
+ return function() {
+ var args = arguments;
+ switch (args.length) {
+ case 0: return !predicate.call(this);
+ case 1: return !predicate.call(this, args[0]);
+ case 2: return !predicate.call(this, args[0], args[1]);
+ case 3: return !predicate.call(this, args[0], args[1], args[2]);
+ }
+ return !predicate.apply(this, args);
+ };
+ }
+
+ /**
+ * Creates a function that is restricted to invoking `func` once. Repeat calls
+ * to the function return the value of the first invocation. The `func` is
+ * invoked with the `this` binding and arguments of the created function.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.1.0
+ * @category Function
+ * @param {Function} func The function to restrict.
+ * @returns {Function} Returns the new restricted function.
+ * @example
+ *
+ * var initialize = _.once(createApplication);
+ * initialize();
+ * initialize();
+ * // => `createApplication` is invoked once
+ */
+ function once(func) {
+ return before(2, func);
+ }
+
+ /**
+ * Creates a function that invokes `func` with its arguments transformed.
+ *
+ * @static
+ * @since 4.0.0
+ * @memberOf _
+ * @category Function
+ * @param {Function} func The function to wrap.
+ * @param {...(Function|Function[])} [transforms=[_.identity]]
+ * The argument transforms.
+ * @returns {Function} Returns the new function.
+ * @example
+ *
+ * function doubled(n) {
+ * return n * 2;
+ * }
+ *
+ * function square(n) {
+ * return n * n;
+ * }
+ *
+ * var func = _.overArgs(function(x, y) {
+ * return [x, y];
+ * }, [square, doubled]);
+ *
+ * func(9, 3);
+ * // => [81, 6]
+ *
+ * func(10, 5);
+ * // => [100, 10]
+ */
+ var overArgs = castRest(function(func, transforms) {
+ transforms = (transforms.length == 1 && isArray(transforms[0]))
+ ? arrayMap(transforms[0], baseUnary(getIteratee()))
+ : arrayMap(baseFlatten(transforms, 1), baseUnary(getIteratee()));
+
+ var funcsLength = transforms.length;
+ return baseRest(function(args) {
+ var index = -1,
+ length = nativeMin(args.length, funcsLength);
+
+ while (++index < length) {
+ args[index] = transforms[index].call(this, args[index]);
+ }
+ return apply(func, this, args);
+ });
+ });
+
+ /**
+ * Creates a function that invokes `func` with `partials` prepended to the
+ * arguments it receives. This method is like `_.bind` except it does **not**
+ * alter the `this` binding.
+ *
+ * The `_.partial.placeholder` value, which defaults to `_` in monolithic
+ * builds, may be used as a placeholder for partially applied arguments.
+ *
+ * **Note:** This method doesn't set the "length" property of partially
+ * applied functions.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.2.0
+ * @category Function
+ * @param {Function} func The function to partially apply arguments to.
+ * @param {...*} [partials] The arguments to be partially applied.
+ * @returns {Function} Returns the new partially applied function.
+ * @example
+ *
+ * function greet(greeting, name) {
+ * return greeting + ' ' + name;
+ * }
+ *
+ * var sayHelloTo = _.partial(greet, 'hello');
+ * sayHelloTo('fred');
+ * // => 'hello fred'
+ *
+ * // Partially applied with placeholders.
+ * var greetFred = _.partial(greet, _, 'fred');
+ * greetFred('hi');
+ * // => 'hi fred'
+ */
+ var partial = baseRest(function(func, partials) {
+ var holders = replaceHolders(partials, getHolder(partial));
+ return createWrap(func, PARTIAL_FLAG, undefined, partials, holders);
+ });
+
+ /**
+ * This method is like `_.partial` except that partially applied arguments
+ * are appended to the arguments it receives.
+ *
+ * The `_.partialRight.placeholder` value, which defaults to `_` in monolithic
+ * builds, may be used as a placeholder for partially applied arguments.
+ *
+ * **Note:** This method doesn't set the "length" property of partially
+ * applied functions.
+ *
+ * @static
+ * @memberOf _
+ * @since 1.0.0
+ * @category Function
+ * @param {Function} func The function to partially apply arguments to.
+ * @param {...*} [partials] The arguments to be partially applied.
+ * @returns {Function} Returns the new partially applied function.
+ * @example
+ *
+ * function greet(greeting, name) {
+ * return greeting + ' ' + name;
+ * }
+ *
+ * var greetFred = _.partialRight(greet, 'fred');
+ * greetFred('hi');
+ * // => 'hi fred'
+ *
+ * // Partially applied with placeholders.
+ * var sayHelloTo = _.partialRight(greet, 'hello', _);
+ * sayHelloTo('fred');
+ * // => 'hello fred'
+ */
+ var partialRight = baseRest(function(func, partials) {
+ var holders = replaceHolders(partials, getHolder(partialRight));
+ return createWrap(func, PARTIAL_RIGHT_FLAG, undefined, partials, holders);
+ });
+
+ /**
+ * Creates a function that invokes `func` with arguments arranged according
+ * to the specified `indexes` where the argument value at the first index is
+ * provided as the first argument, the argument value at the second index is
+ * provided as the second argument, and so on.
+ *
+ * @static
+ * @memberOf _
+ * @since 3.0.0
+ * @category Function
+ * @param {Function} func The function to rearrange arguments for.
+ * @param {...(number|number[])} indexes The arranged argument indexes.
+ * @returns {Function} Returns the new function.
+ * @example
+ *
+ * var rearged = _.rearg(function(a, b, c) {
+ * return [a, b, c];
+ * }, [2, 0, 1]);
+ *
+ * rearged('b', 'c', 'a')
+ * // => ['a', 'b', 'c']
+ */
+ var rearg = flatRest(function(func, indexes) {
+ return createWrap(func, REARG_FLAG, undefined, undefined, undefined, indexes);
+ });
+
+ /**
+ * Creates a function that invokes `func` with the `this` binding of the
+ * created function and arguments from `start` and beyond provided as
+ * an array.
+ *
+ * **Note:** This method is based on the
+ * [rest parameter](https://mdn.io/rest_parameters).
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Function
+ * @param {Function} func The function to apply a rest parameter to.
+ * @param {number} [start=func.length-1] The start position of the rest parameter.
+ * @returns {Function} Returns the new function.
+ * @example
+ *
+ * var say = _.rest(function(what, names) {
+ * return what + ' ' + _.initial(names).join(', ') +
+ * (_.size(names) > 1 ? ', & ' : '') + _.last(names);
+ * });
+ *
+ * say('hello', 'fred', 'barney', 'pebbles');
+ * // => 'hello fred, barney, & pebbles'
+ */
+ function rest(func, start) {
+ if (typeof func != 'function') {
+ throw new TypeError(FUNC_ERROR_TEXT);
+ }
+ start = start === undefined ? start : toInteger(start);
+ return baseRest(func, start);
+ }
+
+ /**
+ * Creates a function that invokes `func` with the `this` binding of the
+ * create function and an array of arguments much like
+ * [`Function#apply`](http://www.ecma-international.org/ecma-262/7.0/#sec-function.prototype.apply).
+ *
+ * **Note:** This method is based on the
+ * [spread operator](https://mdn.io/spread_operator).
+ *
+ * @static
+ * @memberOf _
+ * @since 3.2.0
+ * @category Function
+ * @param {Function} func The function to spread arguments over.
+ * @param {number} [start=0] The start position of the spread.
+ * @returns {Function} Returns the new function.
+ * @example
+ *
+ * var say = _.spread(function(who, what) {
+ * return who + ' says ' + what;
+ * });
+ *
+ * say(['fred', 'hello']);
+ * // => 'fred says hello'
+ *
+ * var numbers = Promise.all([
+ * Promise.resolve(40),
+ * Promise.resolve(36)
+ * ]);
+ *
+ * numbers.then(_.spread(function(x, y) {
+ * return x + y;
+ * }));
+ * // => a Promise of 76
+ */
+ function spread(func, start) {
+ if (typeof func != 'function') {
+ throw new TypeError(FUNC_ERROR_TEXT);
+ }
+ start = start === undefined ? 0 : nativeMax(toInteger(start), 0);
+ return baseRest(function(args) {
+ var array = args[start],
+ otherArgs = castSlice(args, 0, start);
+
+ if (array) {
+ arrayPush(otherArgs, array);
+ }
+ return apply(func, this, otherArgs);
+ });
+ }
+
+ /**
+ * Creates a throttled function that only invokes `func` at most once per
+ * every `wait` milliseconds. The throttled function comes with a `cancel`
+ * method to cancel delayed `func` invocations and a `flush` method to
+ * immediately invoke them. Provide `options` to indicate whether `func`
+ * should be invoked on the leading and/or trailing edge of the `wait`
+ * timeout. The `func` is invoked with the last arguments provided to the
+ * throttled function. Subsequent calls to the throttled function return the
+ * result of the last `func` invocation.
+ *
+ * **Note:** If `leading` and `trailing` options are `true`, `func` is
+ * invoked on the trailing edge of the timeout only if the throttled function
+ * is invoked more than once during the `wait` timeout.
+ *
+ * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred
+ * until to the next tick, similar to `setTimeout` with a timeout of `0`.
+ *
+ * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/)
+ * for details over the differences between `_.throttle` and `_.debounce`.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.1.0
+ * @category Function
+ * @param {Function} func The function to throttle.
+ * @param {number} [wait=0] The number of milliseconds to throttle invocations to.
+ * @param {Object} [options={}] The options object.
+ * @param {boolean} [options.leading=true]
+ * Specify invoking on the leading edge of the timeout.
+ * @param {boolean} [options.trailing=true]
+ * Specify invoking on the trailing edge of the timeout.
+ * @returns {Function} Returns the new throttled function.
+ * @example
+ *
+ * // Avoid excessively updating the position while scrolling.
+ * jQuery(window).on('scroll', _.throttle(updatePosition, 100));
+ *
+ * // Invoke `renewToken` when the click event is fired, but not more than once every 5 minutes.
+ * var throttled = _.throttle(renewToken, 300000, { 'trailing': false });
+ * jQuery(element).on('click', throttled);
+ *
+ * // Cancel the trailing throttled invocation.
+ * jQuery(window).on('popstate', throttled.cancel);
+ */
+ function throttle(func, wait, options) {
+ var leading = true,
+ trailing = true;
+
+ if (typeof func != 'function') {
+ throw new TypeError(FUNC_ERROR_TEXT);
+ }
+ if (isObject(options)) {
+ leading = 'leading' in options ? !!options.leading : leading;
+ trailing = 'trailing' in options ? !!options.trailing : trailing;
+ }
+ return debounce(func, wait, {
+ 'leading': leading,
+ 'maxWait': wait,
+ 'trailing': trailing
+ });
+ }
+
+ /**
+ * Creates a function that accepts up to one argument, ignoring any
+ * additional arguments.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Function
+ * @param {Function} func The function to cap arguments for.
+ * @returns {Function} Returns the new capped function.
+ * @example
+ *
+ * _.map(['6', '8', '10'], _.unary(parseInt));
+ * // => [6, 8, 10]
+ */
+ function unary(func) {
+ return ary(func, 1);
+ }
+
+ /**
+ * Creates a function that provides `value` to `wrapper` as its first
+ * argument. Any additional arguments provided to the function are appended
+ * to those provided to the `wrapper`. The wrapper is invoked with the `this`
+ * binding of the created function.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.1.0
+ * @category Function
+ * @param {*} value The value to wrap.
+ * @param {Function} [wrapper=identity] The wrapper function.
+ * @returns {Function} Returns the new function.
+ * @example
+ *
+ * var p = _.wrap(_.escape, function(func, text) {
+ * return '<p>' + func(text) + '</p>';
+ * });
+ *
+ * p('fred, barney, & pebbles');
+ * // => '<p>fred, barney, &amp; pebbles</p>'
+ */
+ function wrap(value, wrapper) {
+ wrapper = wrapper == null ? identity : wrapper;
+ return partial(wrapper, value);
+ }
+
+ /*------------------------------------------------------------------------*/
+
+ /**
+ * Casts `value` as an array if it's not one.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.4.0
+ * @category Lang
+ * @param {*} value The value to inspect.
+ * @returns {Array} Returns the cast array.
+ * @example
+ *
+ * _.castArray(1);
+ * // => [1]
+ *
+ * _.castArray({ 'a': 1 });
+ * // => [{ 'a': 1 }]
+ *
+ * _.castArray('abc');
+ * // => ['abc']
+ *
+ * _.castArray(null);
+ * // => [null]
+ *
+ * _.castArray(undefined);
+ * // => [undefined]
+ *
+ * _.castArray();
+ * // => []
+ *
+ * var array = [1, 2, 3];
+ * console.log(_.castArray(array) === array);
+ * // => true
+ */
+ function castArray() {
+ if (!arguments.length) {
+ return [];
+ }
+ var value = arguments[0];
+ return isArray(value) ? value : [value];
+ }
+
+ /**
+ * Creates a shallow clone of `value`.
+ *
+ * **Note:** This method is loosely based on the
+ * [structured clone algorithm](https://mdn.io/Structured_clone_algorithm)
+ * and supports cloning arrays, array buffers, booleans, date objects, maps,
+ * numbers, `Object` objects, regexes, sets, strings, symbols, and typed
+ * arrays. The own enumerable properties of `arguments` objects are cloned
+ * as plain objects. An empty object is returned for uncloneable values such
+ * as error objects, functions, DOM nodes, and WeakMaps.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.1.0
+ * @category Lang
+ * @param {*} value The value to clone.
+ * @returns {*} Returns the cloned value.
+ * @see _.cloneDeep
+ * @example
+ *
+ * var objects = [{ 'a': 1 }, { 'b': 2 }];
+ *
+ * var shallow = _.clone(objects);
+ * console.log(shallow[0] === objects[0]);
+ * // => true
+ */
+ function clone(value) {
+ return baseClone(value, false, true);
+ }
+
+ /**
+ * This method is like `_.clone` except that it accepts `customizer` which
+ * is invoked to produce the cloned value. If `customizer` returns `undefined`,
+ * cloning is handled by the method instead. The `customizer` is invoked with
+ * up to four arguments; (value [, index|key, object, stack]).
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Lang
+ * @param {*} value The value to clone.
+ * @param {Function} [customizer] The function to customize cloning.
+ * @returns {*} Returns the cloned value.
+ * @see _.cloneDeepWith
+ * @example
+ *
+ * function customizer(value) {
+ * if (_.isElement(value)) {
+ * return value.cloneNode(false);
+ * }
+ * }
+ *
+ * var el = _.cloneWith(document.body, customizer);
+ *
+ * console.log(el === document.body);
+ * // => false
+ * console.log(el.nodeName);
+ * // => 'BODY'
+ * console.log(el.childNodes.length);
+ * // => 0
+ */
+ function cloneWith(value, customizer) {
+ return baseClone(value, false, true, customizer);
+ }
+
+ /**
+ * This method is like `_.clone` except that it recursively clones `value`.
+ *
+ * @static
+ * @memberOf _
+ * @since 1.0.0
+ * @category Lang
+ * @param {*} value The value to recursively clone.
+ * @returns {*} Returns the deep cloned value.
+ * @see _.clone
+ * @example
+ *
+ * var objects = [{ 'a': 1 }, { 'b': 2 }];
+ *
+ * var deep = _.cloneDeep(objects);
+ * console.log(deep[0] === objects[0]);
+ * // => false
+ */
+ function cloneDeep(value) {
+ return baseClone(value, true, true);
+ }
+
+ /**
+ * This method is like `_.cloneWith` except that it recursively clones `value`.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Lang
+ * @param {*} value The value to recursively clone.
+ * @param {Function} [customizer] The function to customize cloning.
+ * @returns {*} Returns the deep cloned value.
+ * @see _.cloneWith
+ * @example
+ *
+ * function customizer(value) {
+ * if (_.isElement(value)) {
+ * return value.cloneNode(true);
+ * }
+ * }
+ *
+ * var el = _.cloneDeepWith(document.body, customizer);
+ *
+ * console.log(el === document.body);
+ * // => false
+ * console.log(el.nodeName);
+ * // => 'BODY'
+ * console.log(el.childNodes.length);
+ * // => 20
+ */
+ function cloneDeepWith(value, customizer) {
+ return baseClone(value, true, true, customizer);
+ }
+
+ /**
+ * Checks if `object` conforms to `source` by invoking the predicate
+ * properties of `source` with the corresponding property values of `object`.
+ *
+ * **Note:** This method is equivalent to `_.conforms` when `source` is
+ * partially applied.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.14.0
+ * @category Lang
+ * @param {Object} object The object to inspect.
+ * @param {Object} source The object of property predicates to conform to.
+ * @returns {boolean} Returns `true` if `object` conforms, else `false`.
+ * @example
+ *
+ * var object = { 'a': 1, 'b': 2 };
+ *
+ * _.conformsTo(object, { 'b': function(n) { return n > 1; } });
+ * // => true
+ *
+ * _.conformsTo(object, { 'b': function(n) { return n > 2; } });
+ * // => false
+ */
+ function conformsTo(object, source) {
+ return source == null || baseConformsTo(object, source, keys(source));
+ }
+
+ /**
+ * Performs a
+ * [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)
+ * comparison between two values to determine if they are equivalent.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Lang
+ * @param {*} value The value to compare.
+ * @param {*} other The other value to compare.
+ * @returns {boolean} Returns `true` if the values are equivalent, else `false`.
+ * @example
+ *
+ * var object = { 'a': 1 };
+ * var other = { 'a': 1 };
+ *
+ * _.eq(object, object);
+ * // => true
+ *
+ * _.eq(object, other);
+ * // => false
+ *
+ * _.eq('a', 'a');
+ * // => true
+ *
+ * _.eq('a', Object('a'));
+ * // => false
+ *
+ * _.eq(NaN, NaN);
+ * // => true
+ */
+ function eq(value, other) {
+ return value === other || (value !== value && other !== other);
+ }
+
+ /**
+ * Checks if `value` is greater than `other`.
+ *
+ * @static
+ * @memberOf _
+ * @since 3.9.0
+ * @category Lang
+ * @param {*} value The value to compare.
+ * @param {*} other The other value to compare.
+ * @returns {boolean} Returns `true` if `value` is greater than `other`,
+ * else `false`.
+ * @see _.lt
+ * @example
+ *
+ * _.gt(3, 1);
+ * // => true
+ *
+ * _.gt(3, 3);
+ * // => false
+ *
+ * _.gt(1, 3);
+ * // => false
+ */
+ var gt = createRelationalOperation(baseGt);
+
+ /**
+ * Checks if `value` is greater than or equal to `other`.
+ *
+ * @static
+ * @memberOf _
+ * @since 3.9.0
+ * @category Lang
+ * @param {*} value The value to compare.
+ * @param {*} other The other value to compare.
+ * @returns {boolean} Returns `true` if `value` is greater than or equal to
+ * `other`, else `false`.
+ * @see _.lte
+ * @example
+ *
+ * _.gte(3, 1);
+ * // => true
+ *
+ * _.gte(3, 3);
+ * // => true
+ *
+ * _.gte(1, 3);
+ * // => false
+ */
+ var gte = createRelationalOperation(function(value, other) {
+ return value >= other;
+ });
+
+ /**
+ * Checks if `value` is likely an `arguments` object.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.1.0
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is an `arguments` object,
+ * else `false`.
+ * @example
+ *
+ * _.isArguments(function() { return arguments; }());
+ * // => true
+ *
+ * _.isArguments([1, 2, 3]);
+ * // => false
+ */
+ var isArguments = baseIsArguments(function() { return arguments; }()) ? baseIsArguments : function(value) {
+ return isObjectLike(value) && hasOwnProperty.call(value, 'callee') &&
+ !propertyIsEnumerable.call(value, 'callee');
+ };
+
+ /**
+ * Checks if `value` is classified as an `Array` object.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.1.0
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is an array, else `false`.
+ * @example
+ *
+ * _.isArray([1, 2, 3]);
+ * // => true
+ *
+ * _.isArray(document.body.children);
+ * // => false
+ *
+ * _.isArray('abc');
+ * // => false
+ *
+ * _.isArray(_.noop);
+ * // => false
+ */
+ var isArray = Array.isArray;
+
+ /**
+ * Checks if `value` is classified as an `ArrayBuffer` object.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.3.0
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is an array buffer, else `false`.
+ * @example
+ *
+ * _.isArrayBuffer(new ArrayBuffer(2));
+ * // => true
+ *
+ * _.isArrayBuffer(new Array(2));
+ * // => false
+ */
+ var isArrayBuffer = nodeIsArrayBuffer ? baseUnary(nodeIsArrayBuffer) : baseIsArrayBuffer;
+
+ /**
+ * Checks if `value` is array-like. A value is considered array-like if it's
+ * not a function and has a `value.length` that's an integer greater than or
+ * equal to `0` and less than or equal to `Number.MAX_SAFE_INTEGER`.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is array-like, else `false`.
+ * @example
+ *
+ * _.isArrayLike([1, 2, 3]);
+ * // => true
+ *
+ * _.isArrayLike(document.body.children);
+ * // => true
+ *
+ * _.isArrayLike('abc');
+ * // => true
+ *
+ * _.isArrayLike(_.noop);
+ * // => false
+ */
+ function isArrayLike(value) {
+ return value != null && isLength(value.length) && !isFunction(value);
+ }
+
+ /**
+ * This method is like `_.isArrayLike` except that it also checks if `value`
+ * is an object.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is an array-like object,
+ * else `false`.
+ * @example
+ *
+ * _.isArrayLikeObject([1, 2, 3]);
+ * // => true
+ *
+ * _.isArrayLikeObject(document.body.children);
+ * // => true
+ *
+ * _.isArrayLikeObject('abc');
+ * // => false
+ *
+ * _.isArrayLikeObject(_.noop);
+ * // => false
+ */
+ function isArrayLikeObject(value) {
+ return isObjectLike(value) && isArrayLike(value);
+ }
+
+ /**
+ * Checks if `value` is classified as a boolean primitive or object.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.1.0
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is a boolean, else `false`.
+ * @example
+ *
+ * _.isBoolean(false);
+ * // => true
+ *
+ * _.isBoolean(null);
+ * // => false
+ */
+ function isBoolean(value) {
+ return value === true || value === false ||
+ (isObjectLike(value) && objectToString.call(value) == boolTag);
+ }
+
+ /**
+ * Checks if `value` is a buffer.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.3.0
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is a buffer, else `false`.
+ * @example
+ *
+ * _.isBuffer(new Buffer(2));
+ * // => true
+ *
+ * _.isBuffer(new Uint8Array(2));
+ * // => false
+ */
+ var isBuffer = nativeIsBuffer || stubFalse;
+
+ /**
+ * Checks if `value` is classified as a `Date` object.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.1.0
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is a date object, else `false`.
+ * @example
+ *
+ * _.isDate(new Date);
+ * // => true
+ *
+ * _.isDate('Mon April 23 2012');
+ * // => false
+ */
+ var isDate = nodeIsDate ? baseUnary(nodeIsDate) : baseIsDate;
+
+ /**
+ * Checks if `value` is likely a DOM element.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.1.0
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is a DOM element, else `false`.
+ * @example
+ *
+ * _.isElement(document.body);
+ * // => true
+ *
+ * _.isElement('<body>');
+ * // => false
+ */
+ function isElement(value) {
+ return value != null && value.nodeType === 1 && isObjectLike(value) && !isPlainObject(value);
+ }
+
+ /**
+ * Checks if `value` is an empty object, collection, map, or set.
+ *
+ * Objects are considered empty if they have no own enumerable string keyed
+ * properties.
+ *
+ * Array-like values such as `arguments` objects, arrays, buffers, strings, or
+ * jQuery-like collections are considered empty if they have a `length` of `0`.
+ * Similarly, maps and sets are considered empty if they have a `size` of `0`.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.1.0
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is empty, else `false`.
+ * @example
+ *
+ * _.isEmpty(null);
+ * // => true
+ *
+ * _.isEmpty(true);
+ * // => true
+ *
+ * _.isEmpty(1);
+ * // => true
+ *
+ * _.isEmpty([1, 2, 3]);
+ * // => false
+ *
+ * _.isEmpty({ 'a': 1 });
+ * // => false
+ */
+ function isEmpty(value) {
+ if (isArrayLike(value) &&
+ (isArray(value) || typeof value == 'string' || typeof value.splice == 'function' ||
+ isBuffer(value) || isTypedArray(value) || isArguments(value))) {
+ return !value.length;
+ }
+ var tag = getTag(value);
+ if (tag == mapTag || tag == setTag) {
+ return !value.size;
+ }
+ if (isPrototype(value)) {
+ return !baseKeys(value).length;
+ }
+ for (var key in value) {
+ if (hasOwnProperty.call(value, key)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Performs a deep comparison between two values to determine if they are
+ * equivalent.
+ *
+ * **Note:** This method supports comparing arrays, array buffers, booleans,
+ * date objects, error objects, maps, numbers, `Object` objects, regexes,
+ * sets, strings, symbols, and typed arrays. `Object` objects are compared
+ * by their own, not inherited, enumerable properties. Functions and DOM
+ * nodes are **not** supported.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.1.0
+ * @category Lang
+ * @param {*} value The value to compare.
+ * @param {*} other The other value to compare.
+ * @returns {boolean} Returns `true` if the values are equivalent, else `false`.
+ * @example
+ *
+ * var object = { 'a': 1 };
+ * var other = { 'a': 1 };
+ *
+ * _.isEqual(object, other);
+ * // => true
+ *
+ * object === other;
+ * // => false
+ */
+ function isEqual(value, other) {
+ return baseIsEqual(value, other);
+ }
+
+ /**
+ * This method is like `_.isEqual` except that it accepts `customizer` which
+ * is invoked to compare values. If `customizer` returns `undefined`, comparisons
+ * are handled by the method instead. The `customizer` is invoked with up to
+ * six arguments: (objValue, othValue [, index|key, object, other, stack]).
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Lang
+ * @param {*} value The value to compare.
+ * @param {*} other The other value to compare.
+ * @param {Function} [customizer] The function to customize comparisons.
+ * @returns {boolean} Returns `true` if the values are equivalent, else `false`.
+ * @example
+ *
+ * function isGreeting(value) {
+ * return /^h(?:i|ello)$/.test(value);
+ * }
+ *
+ * function customizer(objValue, othValue) {
+ * if (isGreeting(objValue) && isGreeting(othValue)) {
+ * return true;
+ * }
+ * }
+ *
+ * var array = ['hello', 'goodbye'];
+ * var other = ['hi', 'goodbye'];
+ *
+ * _.isEqualWith(array, other, customizer);
+ * // => true
+ */
+ function isEqualWith(value, other, customizer) {
+ customizer = typeof customizer == 'function' ? customizer : undefined;
+ var result = customizer ? customizer(value, other) : undefined;
+ return result === undefined ? baseIsEqual(value, other, customizer) : !!result;
+ }
+
+ /**
+ * Checks if `value` is an `Error`, `EvalError`, `RangeError`, `ReferenceError`,
+ * `SyntaxError`, `TypeError`, or `URIError` object.
+ *
+ * @static
+ * @memberOf _
+ * @since 3.0.0
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is an error object, else `false`.
+ * @example
+ *
+ * _.isError(new Error);
+ * // => true
+ *
+ * _.isError(Error);
+ * // => false
+ */
+ function isError(value) {
+ if (!isObjectLike(value)) {
+ return false;
+ }
+ return (objectToString.call(value) == errorTag) ||
+ (typeof value.message == 'string' && typeof value.name == 'string');
+ }
+
+ /**
+ * Checks if `value` is a finite primitive number.
+ *
+ * **Note:** This method is based on
+ * [`Number.isFinite`](https://mdn.io/Number/isFinite).
+ *
+ * @static
+ * @memberOf _
+ * @since 0.1.0
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is a finite number, else `false`.
+ * @example
+ *
+ * _.isFinite(3);
+ * // => true
+ *
+ * _.isFinite(Number.MIN_VALUE);
+ * // => true
+ *
+ * _.isFinite(Infinity);
+ * // => false
+ *
+ * _.isFinite('3');
+ * // => false
+ */
+ function isFinite(value) {
+ return typeof value == 'number' && nativeIsFinite(value);
+ }
+
+ /**
+ * Checks if `value` is classified as a `Function` object.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.1.0
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is a function, else `false`.
+ * @example
+ *
+ * _.isFunction(_);
+ * // => true
+ *
+ * _.isFunction(/abc/);
+ * // => false
+ */
+ function isFunction(value) {
+ // The use of `Object#toString` avoids issues with the `typeof` operator
+ // in Safari 9 which returns 'object' for typed array and other constructors.
+ var tag = isObject(value) ? objectToString.call(value) : '';
+ return tag == funcTag || tag == genTag || tag == proxyTag;
+ }
+
+ /**
+ * Checks if `value` is an integer.
+ *
+ * **Note:** This method is based on
+ * [`Number.isInteger`](https://mdn.io/Number/isInteger).
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is an integer, else `false`.
+ * @example
+ *
+ * _.isInteger(3);
+ * // => true
+ *
+ * _.isInteger(Number.MIN_VALUE);
+ * // => false
+ *
+ * _.isInteger(Infinity);
+ * // => false
+ *
+ * _.isInteger('3');
+ * // => false
+ */
+ function isInteger(value) {
+ return typeof value == 'number' && value == toInteger(value);
+ }
+
+ /**
+ * Checks if `value` is a valid array-like length.
+ *
+ * **Note:** This method is loosely based on
+ * [`ToLength`](http://ecma-international.org/ecma-262/7.0/#sec-tolength).
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is a valid length, else `false`.
+ * @example
+ *
+ * _.isLength(3);
+ * // => true
+ *
+ * _.isLength(Number.MIN_VALUE);
+ * // => false
+ *
+ * _.isLength(Infinity);
+ * // => false
+ *
+ * _.isLength('3');
+ * // => false
+ */
+ function isLength(value) {
+ return typeof value == 'number' &&
+ value > -1 && value % 1 == 0 && value <= MAX_SAFE_INTEGER;
+ }
+
+ /**
+ * Checks if `value` is the
+ * [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types)
+ * of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`)
+ *
+ * @static
+ * @memberOf _
+ * @since 0.1.0
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is an object, else `false`.
+ * @example
+ *
+ * _.isObject({});
+ * // => true
+ *
+ * _.isObject([1, 2, 3]);
+ * // => true
+ *
+ * _.isObject(_.noop);
+ * // => true
+ *
+ * _.isObject(null);
+ * // => false
+ */
+ function isObject(value) {
+ var type = typeof value;
+ return value != null && (type == 'object' || type == 'function');
+ }
+
+ /**
+ * Checks if `value` is object-like. A value is object-like if it's not `null`
+ * and has a `typeof` result of "object".
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is object-like, else `false`.
+ * @example
+ *
+ * _.isObjectLike({});
+ * // => true
+ *
+ * _.isObjectLike([1, 2, 3]);
+ * // => true
+ *
+ * _.isObjectLike(_.noop);
+ * // => false
+ *
+ * _.isObjectLike(null);
+ * // => false
+ */
+ function isObjectLike(value) {
+ return value != null && typeof value == 'object';
+ }
+
+ /**
+ * Checks if `value` is classified as a `Map` object.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.3.0
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is a map, else `false`.
+ * @example
+ *
+ * _.isMap(new Map);
+ * // => true
+ *
+ * _.isMap(new WeakMap);
+ * // => false
+ */
+ var isMap = nodeIsMap ? baseUnary(nodeIsMap) : baseIsMap;
+
+ /**
+ * Performs a partial deep comparison between `object` and `source` to
+ * determine if `object` contains equivalent property values.
+ *
+ * **Note:** This method is equivalent to `_.matches` when `source` is
+ * partially applied.
+ *
+ * Partial comparisons will match empty array and empty object `source`
+ * values against any array or object value, respectively. See `_.isEqual`
+ * for a list of supported value comparisons.
+ *
+ * @static
+ * @memberOf _
+ * @since 3.0.0
+ * @category Lang
+ * @param {Object} object The object to inspect.
+ * @param {Object} source The object of property values to match.
+ * @returns {boolean} Returns `true` if `object` is a match, else `false`.
+ * @example
+ *
+ * var object = { 'a': 1, 'b': 2 };
+ *
+ * _.isMatch(object, { 'b': 2 });
+ * // => true
+ *
+ * _.isMatch(object, { 'b': 1 });
+ * // => false
+ */
+ function isMatch(object, source) {
+ return object === source || baseIsMatch(object, source, getMatchData(source));
+ }
+
+ /**
+ * This method is like `_.isMatch` except that it accepts `customizer` which
+ * is invoked to compare values. If `customizer` returns `undefined`, comparisons
+ * are handled by the method instead. The `customizer` is invoked with five
+ * arguments: (objValue, srcValue, index|key, object, source).
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Lang
+ * @param {Object} object The object to inspect.
+ * @param {Object} source The object of property values to match.
+ * @param {Function} [customizer] The function to customize comparisons.
+ * @returns {boolean} Returns `true` if `object` is a match, else `false`.
+ * @example
+ *
+ * function isGreeting(value) {
+ * return /^h(?:i|ello)$/.test(value);
+ * }
+ *
+ * function customizer(objValue, srcValue) {
+ * if (isGreeting(objValue) && isGreeting(srcValue)) {
+ * return true;
+ * }
+ * }
+ *
+ * var object = { 'greeting': 'hello' };
+ * var source = { 'greeting': 'hi' };
+ *
+ * _.isMatchWith(object, source, customizer);
+ * // => true
+ */
+ function isMatchWith(object, source, customizer) {
+ customizer = typeof customizer == 'function' ? customizer : undefined;
+ return baseIsMatch(object, source, getMatchData(source), customizer);
+ }
+
+ /**
+ * Checks if `value` is `NaN`.
+ *
+ * **Note:** This method is based on
+ * [`Number.isNaN`](https://mdn.io/Number/isNaN) and is not the same as
+ * global [`isNaN`](https://mdn.io/isNaN) which returns `true` for
+ * `undefined` and other non-number values.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.1.0
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is `NaN`, else `false`.
+ * @example
+ *
+ * _.isNaN(NaN);
+ * // => true
+ *
+ * _.isNaN(new Number(NaN));
+ * // => true
+ *
+ * isNaN(undefined);
+ * // => true
+ *
+ * _.isNaN(undefined);
+ * // => false
+ */
+ function isNaN(value) {
+ // An `NaN` primitive is the only value that is not equal to itself.
+ // Perform the `toStringTag` check first to avoid errors with some
+ // ActiveX objects in IE.
+ return isNumber(value) && value != +value;
+ }
+
+ /**
+ * Checks if `value` is a pristine native function.
+ *
+ * **Note:** This method can't reliably detect native functions in the presence
+ * of the core-js package because core-js circumvents this kind of detection.
+ * Despite multiple requests, the core-js maintainer has made it clear: any
+ * attempt to fix the detection will be obstructed. As a result, we're left
+ * with little choice but to throw an error. Unfortunately, this also affects
+ * packages, like [babel-polyfill](https://www.npmjs.com/package/babel-polyfill),
+ * which rely on core-js.
+ *
+ * @static
+ * @memberOf _
+ * @since 3.0.0
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is a native function,
+ * else `false`.
+ * @example
+ *
+ * _.isNative(Array.prototype.push);
+ * // => true
+ *
+ * _.isNative(_);
+ * // => false
+ */
+ function isNative(value) {
+ if (isMaskable(value)) {
+ throw new Error(CORE_ERROR_TEXT);
+ }
+ return baseIsNative(value);
+ }
+
+ /**
+ * Checks if `value` is `null`.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.1.0
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is `null`, else `false`.
+ * @example
+ *
+ * _.isNull(null);
+ * // => true
+ *
+ * _.isNull(void 0);
+ * // => false
+ */
+ function isNull(value) {
+ return value === null;
+ }
+
+ /**
+ * Checks if `value` is `null` or `undefined`.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is nullish, else `false`.
+ * @example
+ *
+ * _.isNil(null);
+ * // => true
+ *
+ * _.isNil(void 0);
+ * // => true
+ *
+ * _.isNil(NaN);
+ * // => false
+ */
+ function isNil(value) {
+ return value == null;
+ }
+
+ /**
+ * Checks if `value` is classified as a `Number` primitive or object.
+ *
+ * **Note:** To exclude `Infinity`, `-Infinity`, and `NaN`, which are
+ * classified as numbers, use the `_.isFinite` method.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.1.0
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is a number, else `false`.
+ * @example
+ *
+ * _.isNumber(3);
+ * // => true
+ *
+ * _.isNumber(Number.MIN_VALUE);
+ * // => true
+ *
+ * _.isNumber(Infinity);
+ * // => true
+ *
+ * _.isNumber('3');
+ * // => false
+ */
+ function isNumber(value) {
+ return typeof value == 'number' ||
+ (isObjectLike(value) && objectToString.call(value) == numberTag);
+ }
+
+ /**
+ * Checks if `value` is a plain object, that is, an object created by the
+ * `Object` constructor or one with a `[[Prototype]]` of `null`.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.8.0
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is a plain object, else `false`.
+ * @example
+ *
+ * function Foo() {
+ * this.a = 1;
+ * }
+ *
+ * _.isPlainObject(new Foo);
+ * // => false
+ *
+ * _.isPlainObject([1, 2, 3]);
+ * // => false
+ *
+ * _.isPlainObject({ 'x': 0, 'y': 0 });
+ * // => true
+ *
+ * _.isPlainObject(Object.create(null));
+ * // => true
+ */
+ function isPlainObject(value) {
+ if (!isObjectLike(value) || objectToString.call(value) != objectTag) {
+ return false;
+ }
+ var proto = getPrototype(value);
+ if (proto === null) {
+ return true;
+ }
+ var Ctor = hasOwnProperty.call(proto, 'constructor') && proto.constructor;
+ return (typeof Ctor == 'function' &&
+ Ctor instanceof Ctor && funcToString.call(Ctor) == objectCtorString);
+ }
+
+ /**
+ * Checks if `value` is classified as a `RegExp` object.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.1.0
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is a regexp, else `false`.
+ * @example
+ *
+ * _.isRegExp(/abc/);
+ * // => true
+ *
+ * _.isRegExp('/abc/');
+ * // => false
+ */
+ var isRegExp = nodeIsRegExp ? baseUnary(nodeIsRegExp) : baseIsRegExp;
+
+ /**
+ * Checks if `value` is a safe integer. An integer is safe if it's an IEEE-754
+ * double precision number which isn't the result of a rounded unsafe integer.
+ *
+ * **Note:** This method is based on
+ * [`Number.isSafeInteger`](https://mdn.io/Number/isSafeInteger).
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is a safe integer, else `false`.
+ * @example
+ *
+ * _.isSafeInteger(3);
+ * // => true
+ *
+ * _.isSafeInteger(Number.MIN_VALUE);
+ * // => false
+ *
+ * _.isSafeInteger(Infinity);
+ * // => false
+ *
+ * _.isSafeInteger('3');
+ * // => false
+ */
+ function isSafeInteger(value) {
+ return isInteger(value) && value >= -MAX_SAFE_INTEGER && value <= MAX_SAFE_INTEGER;
+ }
+
+ /**
+ * Checks if `value` is classified as a `Set` object.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.3.0
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is a set, else `false`.
+ * @example
+ *
+ * _.isSet(new Set);
+ * // => true
+ *
+ * _.isSet(new WeakSet);
+ * // => false
+ */
+ var isSet = nodeIsSet ? baseUnary(nodeIsSet) : baseIsSet;
+
+ /**
+ * Checks if `value` is classified as a `String` primitive or object.
+ *
+ * @static
+ * @since 0.1.0
+ * @memberOf _
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is a string, else `false`.
+ * @example
+ *
+ * _.isString('abc');
+ * // => true
+ *
+ * _.isString(1);
+ * // => false
+ */
+ function isString(value) {
+ return typeof value == 'string' ||
+ (!isArray(value) && isObjectLike(value) && objectToString.call(value) == stringTag);
+ }
+
+ /**
+ * Checks if `value` is classified as a `Symbol` primitive or object.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is a symbol, else `false`.
+ * @example
+ *
+ * _.isSymbol(Symbol.iterator);
+ * // => true
+ *
+ * _.isSymbol('abc');
+ * // => false
+ */
+ function isSymbol(value) {
+ return typeof value == 'symbol' ||
+ (isObjectLike(value) && objectToString.call(value) == symbolTag);
+ }
+
+ /**
+ * Checks if `value` is classified as a typed array.
+ *
+ * @static
+ * @memberOf _
+ * @since 3.0.0
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is a typed array, else `false`.
+ * @example
+ *
+ * _.isTypedArray(new Uint8Array);
+ * // => true
+ *
+ * _.isTypedArray([]);
+ * // => false
+ */
+ var isTypedArray = nodeIsTypedArray ? baseUnary(nodeIsTypedArray) : baseIsTypedArray;
+
+ /**
+ * Checks if `value` is `undefined`.
+ *
+ * @static
+ * @since 0.1.0
+ * @memberOf _
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is `undefined`, else `false`.
+ * @example
+ *
+ * _.isUndefined(void 0);
+ * // => true
+ *
+ * _.isUndefined(null);
+ * // => false
+ */
+ function isUndefined(value) {
+ return value === undefined;
+ }
+
+ /**
+ * Checks if `value` is classified as a `WeakMap` object.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.3.0
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is a weak map, else `false`.
+ * @example
+ *
+ * _.isWeakMap(new WeakMap);
+ * // => true
+ *
+ * _.isWeakMap(new Map);
+ * // => false
+ */
+ function isWeakMap(value) {
+ return isObjectLike(value) && getTag(value) == weakMapTag;
+ }
+
+ /**
+ * Checks if `value` is classified as a `WeakSet` object.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.3.0
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is a weak set, else `false`.
+ * @example
+ *
+ * _.isWeakSet(new WeakSet);
+ * // => true
+ *
+ * _.isWeakSet(new Set);
+ * // => false
+ */
+ function isWeakSet(value) {
+ return isObjectLike(value) && objectToString.call(value) == weakSetTag;
+ }
+
+ /**
+ * Checks if `value` is less than `other`.
+ *
+ * @static
+ * @memberOf _
+ * @since 3.9.0
+ * @category Lang
+ * @param {*} value The value to compare.
+ * @param {*} other The other value to compare.
+ * @returns {boolean} Returns `true` if `value` is less than `other`,
+ * else `false`.
+ * @see _.gt
+ * @example
+ *
+ * _.lt(1, 3);
+ * // => true
+ *
+ * _.lt(3, 3);
+ * // => false
+ *
+ * _.lt(3, 1);
+ * // => false
+ */
+ var lt = createRelationalOperation(baseLt);
+
+ /**
+ * Checks if `value` is less than or equal to `other`.
+ *
+ * @static
+ * @memberOf _
+ * @since 3.9.0
+ * @category Lang
+ * @param {*} value The value to compare.
+ * @param {*} other The other value to compare.
+ * @returns {boolean} Returns `true` if `value` is less than or equal to
+ * `other`, else `false`.
+ * @see _.gte
+ * @example
+ *
+ * _.lte(1, 3);
+ * // => true
+ *
+ * _.lte(3, 3);
+ * // => true
+ *
+ * _.lte(3, 1);
+ * // => false
+ */
+ var lte = createRelationalOperation(function(value, other) {
+ return value <= other;
+ });
+
+ /**
+ * Converts `value` to an array.
+ *
+ * @static
+ * @since 0.1.0
+ * @memberOf _
+ * @category Lang
+ * @param {*} value The value to convert.
+ * @returns {Array} Returns the converted array.
+ * @example
+ *
+ * _.toArray({ 'a': 1, 'b': 2 });
+ * // => [1, 2]
+ *
+ * _.toArray('abc');
+ * // => ['a', 'b', 'c']
+ *
+ * _.toArray(1);
+ * // => []
+ *
+ * _.toArray(null);
+ * // => []
+ */
+ function toArray(value) {
+ if (!value) {
+ return [];
+ }
+ if (isArrayLike(value)) {
+ return isString(value) ? stringToArray(value) : copyArray(value);
+ }
+ if (iteratorSymbol && value[iteratorSymbol]) {
+ return iteratorToArray(value[iteratorSymbol]());
+ }
+ var tag = getTag(value),
+ func = tag == mapTag ? mapToArray : (tag == setTag ? setToArray : values);
+
+ return func(value);
+ }
+
+ /**
+ * Converts `value` to a finite number.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.12.0
+ * @category Lang
+ * @param {*} value The value to convert.
+ * @returns {number} Returns the converted number.
+ * @example
+ *
+ * _.toFinite(3.2);
+ * // => 3.2
+ *
+ * _.toFinite(Number.MIN_VALUE);
+ * // => 5e-324
+ *
+ * _.toFinite(Infinity);
+ * // => 1.7976931348623157e+308
+ *
+ * _.toFinite('3.2');
+ * // => 3.2
+ */
+ function toFinite(value) {
+ if (!value) {
+ return value === 0 ? value : 0;
+ }
+ value = toNumber(value);
+ if (value === INFINITY || value === -INFINITY) {
+ var sign = (value < 0 ? -1 : 1);
+ return sign * MAX_INTEGER;
+ }
+ return value === value ? value : 0;
+ }
+
+ /**
+ * Converts `value` to an integer.
+ *
+ * **Note:** This method is loosely based on
+ * [`ToInteger`](http://www.ecma-international.org/ecma-262/7.0/#sec-tointeger).
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Lang
+ * @param {*} value The value to convert.
+ * @returns {number} Returns the converted integer.
+ * @example
+ *
+ * _.toInteger(3.2);
+ * // => 3
+ *
+ * _.toInteger(Number.MIN_VALUE);
+ * // => 0
+ *
+ * _.toInteger(Infinity);
+ * // => 1.7976931348623157e+308
+ *
+ * _.toInteger('3.2');
+ * // => 3
+ */
+ function toInteger(value) {
+ var result = toFinite(value),
+ remainder = result % 1;
+
+ return result === result ? (remainder ? result - remainder : result) : 0;
+ }
+
+ /**
+ * Converts `value` to an integer suitable for use as the length of an
+ * array-like object.
+ *
+ * **Note:** This method is based on
+ * [`ToLength`](http://ecma-international.org/ecma-262/7.0/#sec-tolength).
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Lang
+ * @param {*} value The value to convert.
+ * @returns {number} Returns the converted integer.
+ * @example
+ *
+ * _.toLength(3.2);
+ * // => 3
+ *
+ * _.toLength(Number.MIN_VALUE);
+ * // => 0
+ *
+ * _.toLength(Infinity);
+ * // => 4294967295
+ *
+ * _.toLength('3.2');
+ * // => 3
+ */
+ function toLength(value) {
+ return value ? baseClamp(toInteger(value), 0, MAX_ARRAY_LENGTH) : 0;
+ }
+
+ /**
+ * Converts `value` to a number.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Lang
+ * @param {*} value The value to process.
+ * @returns {number} Returns the number.
+ * @example
+ *
+ * _.toNumber(3.2);
+ * // => 3.2
+ *
+ * _.toNumber(Number.MIN_VALUE);
+ * // => 5e-324
+ *
+ * _.toNumber(Infinity);
+ * // => Infinity
+ *
+ * _.toNumber('3.2');
+ * // => 3.2
+ */
+ function toNumber(value) {
+ if (typeof value == 'number') {
+ return value;
+ }
+ if (isSymbol(value)) {
+ return NAN;
+ }
+ if (isObject(value)) {
+ var other = typeof value.valueOf == 'function' ? value.valueOf() : value;
+ value = isObject(other) ? (other + '') : other;
+ }
+ if (typeof value != 'string') {
+ return value === 0 ? value : +value;
+ }
+ value = value.replace(reTrim, '');
+ var isBinary = reIsBinary.test(value);
+ return (isBinary || reIsOctal.test(value))
+ ? freeParseInt(value.slice(2), isBinary ? 2 : 8)
+ : (reIsBadHex.test(value) ? NAN : +value);
+ }
+
+ /**
+ * Converts `value` to a plain object flattening inherited enumerable string
+ * keyed properties of `value` to own properties of the plain object.
+ *
+ * @static
+ * @memberOf _
+ * @since 3.0.0
+ * @category Lang
+ * @param {*} value The value to convert.
+ * @returns {Object} Returns the converted plain object.
+ * @example
+ *
+ * function Foo() {
+ * this.b = 2;
+ * }
+ *
+ * Foo.prototype.c = 3;
+ *
+ * _.assign({ 'a': 1 }, new Foo);
+ * // => { 'a': 1, 'b': 2 }
+ *
+ * _.assign({ 'a': 1 }, _.toPlainObject(new Foo));
+ * // => { 'a': 1, 'b': 2, 'c': 3 }
+ */
+ function toPlainObject(value) {
+ return copyObject(value, keysIn(value));
+ }
+
+ /**
+ * Converts `value` to a safe integer. A safe integer can be compared and
+ * represented correctly.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Lang
+ * @param {*} value The value to convert.
+ * @returns {number} Returns the converted integer.
+ * @example
+ *
+ * _.toSafeInteger(3.2);
+ * // => 3
+ *
+ * _.toSafeInteger(Number.MIN_VALUE);
+ * // => 0
+ *
+ * _.toSafeInteger(Infinity);
+ * // => 9007199254740991
+ *
+ * _.toSafeInteger('3.2');
+ * // => 3
+ */
+ function toSafeInteger(value) {
+ return baseClamp(toInteger(value), -MAX_SAFE_INTEGER, MAX_SAFE_INTEGER);
+ }
+
+ /**
+ * Converts `value` to a string. An empty string is returned for `null`
+ * and `undefined` values. The sign of `-0` is preserved.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Lang
+ * @param {*} value The value to convert.
+ * @returns {string} Returns the converted string.
+ * @example
+ *
+ * _.toString(null);
+ * // => ''
+ *
+ * _.toString(-0);
+ * // => '-0'
+ *
+ * _.toString([1, 2, 3]);
+ * // => '1,2,3'
+ */
+ function toString(value) {
+ return value == null ? '' : baseToString(value);
+ }
+
+ /*------------------------------------------------------------------------*/
+
+ /**
+ * Assigns own enumerable string keyed properties of source objects to the
+ * destination object. Source objects are applied from left to right.
+ * Subsequent sources overwrite property assignments of previous sources.
+ *
+ * **Note:** This method mutates `object` and is loosely based on
+ * [`Object.assign`](https://mdn.io/Object/assign).
+ *
+ * @static
+ * @memberOf _
+ * @since 0.10.0
+ * @category Object
+ * @param {Object} object The destination object.
+ * @param {...Object} [sources] The source objects.
+ * @returns {Object} Returns `object`.
+ * @see _.assignIn
+ * @example
+ *
+ * function Foo() {
+ * this.a = 1;
+ * }
+ *
+ * function Bar() {
+ * this.c = 3;
+ * }
+ *
+ * Foo.prototype.b = 2;
+ * Bar.prototype.d = 4;
+ *
+ * _.assign({ 'a': 0 }, new Foo, new Bar);
+ * // => { 'a': 1, 'c': 3 }
+ */
+ var assign = createAssigner(function(object, source) {
+ if (isPrototype(source) || isArrayLike(source)) {
+ copyObject(source, keys(source), object);
+ return;
+ }
+ for (var key in source) {
+ if (hasOwnProperty.call(source, key)) {
+ assignValue(object, key, source[key]);
+ }
+ }
+ });
+
+ /**
+ * This method is like `_.assign` except that it iterates over own and
+ * inherited source properties.
+ *
+ * **Note:** This method mutates `object`.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @alias extend
+ * @category Object
+ * @param {Object} object The destination object.
+ * @param {...Object} [sources] The source objects.
+ * @returns {Object} Returns `object`.
+ * @see _.assign
+ * @example
+ *
+ * function Foo() {
+ * this.a = 1;
+ * }
+ *
+ * function Bar() {
+ * this.c = 3;
+ * }
+ *
+ * Foo.prototype.b = 2;
+ * Bar.prototype.d = 4;
+ *
+ * _.assignIn({ 'a': 0 }, new Foo, new Bar);
+ * // => { 'a': 1, 'b': 2, 'c': 3, 'd': 4 }
+ */
+ var assignIn = createAssigner(function(object, source) {
+ copyObject(source, keysIn(source), object);
+ });
+
+ /**
+ * This method is like `_.assignIn` except that it accepts `customizer`
+ * which is invoked to produce the assigned values. If `customizer` returns
+ * `undefined`, assignment is handled by the method instead. The `customizer`
+ * is invoked with five arguments: (objValue, srcValue, key, object, source).
+ *
+ * **Note:** This method mutates `object`.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @alias extendWith
+ * @category Object
+ * @param {Object} object The destination object.
+ * @param {...Object} sources The source objects.
+ * @param {Function} [customizer] The function to customize assigned values.
+ * @returns {Object} Returns `object`.
+ * @see _.assignWith
+ * @example
+ *
+ * function customizer(objValue, srcValue) {
+ * return _.isUndefined(objValue) ? srcValue : objValue;
+ * }
+ *
+ * var defaults = _.partialRight(_.assignInWith, customizer);
+ *
+ * defaults({ 'a': 1 }, { 'b': 2 }, { 'a': 3 });
+ * // => { 'a': 1, 'b': 2 }
+ */
+ var assignInWith = createAssigner(function(object, source, srcIndex, customizer) {
+ copyObject(source, keysIn(source), object, customizer);
+ });
+
+ /**
+ * This method is like `_.assign` except that it accepts `customizer`
+ * which is invoked to produce the assigned values. If `customizer` returns
+ * `undefined`, assignment is handled by the method instead. The `customizer`
+ * is invoked with five arguments: (objValue, srcValue, key, object, source).
+ *
+ * **Note:** This method mutates `object`.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Object
+ * @param {Object} object The destination object.
+ * @param {...Object} sources The source objects.
+ * @param {Function} [customizer] The function to customize assigned values.
+ * @returns {Object} Returns `object`.
+ * @see _.assignInWith
+ * @example
+ *
+ * function customizer(objValue, srcValue) {
+ * return _.isUndefined(objValue) ? srcValue : objValue;
+ * }
+ *
+ * var defaults = _.partialRight(_.assignWith, customizer);
+ *
+ * defaults({ 'a': 1 }, { 'b': 2 }, { 'a': 3 });
+ * // => { 'a': 1, 'b': 2 }
+ */
+ var assignWith = createAssigner(function(object, source, srcIndex, customizer) {
+ copyObject(source, keys(source), object, customizer);
+ });
+
+ /**
+ * Creates an array of values corresponding to `paths` of `object`.
+ *
+ * @static
+ * @memberOf _
+ * @since 1.0.0
+ * @category Object
+ * @param {Object} object The object to iterate over.
+ * @param {...(string|string[])} [paths] The property paths of elements to pick.
+ * @returns {Array} Returns the picked values.
+ * @example
+ *
+ * var object = { 'a': [{ 'b': { 'c': 3 } }, 4] };
+ *
+ * _.at(object, ['a[0].b.c', 'a[1]']);
+ * // => [3, 4]
+ */
+ var at = flatRest(baseAt);
+
+ /**
+ * Creates an object that inherits from the `prototype` object. If a
+ * `properties` object is given, its own enumerable string keyed properties
+ * are assigned to the created object.
+ *
+ * @static
+ * @memberOf _
+ * @since 2.3.0
+ * @category Object
+ * @param {Object} prototype The object to inherit from.
+ * @param {Object} [properties] The properties to assign to the object.
+ * @returns {Object} Returns the new object.
+ * @example
+ *
+ * function Shape() {
+ * this.x = 0;
+ * this.y = 0;
+ * }
+ *
+ * function Circle() {
+ * Shape.call(this);
+ * }
+ *
+ * Circle.prototype = _.create(Shape.prototype, {
+ * 'constructor': Circle
+ * });
+ *
+ * var circle = new Circle;
+ * circle instanceof Circle;
+ * // => true
+ *
+ * circle instanceof Shape;
+ * // => true
+ */
+ function create(prototype, properties) {
+ var result = baseCreate(prototype);
+ return properties ? baseAssign(result, properties) : result;
+ }
+
+ /**
+ * Assigns own and inherited enumerable string keyed properties of source
+ * objects to the destination object for all destination properties that
+ * resolve to `undefined`. Source objects are applied from left to right.
+ * Once a property is set, additional values of the same property are ignored.
+ *
+ * **Note:** This method mutates `object`.
+ *
+ * @static
+ * @since 0.1.0
+ * @memberOf _
+ * @category Object
+ * @param {Object} object The destination object.
+ * @param {...Object} [sources] The source objects.
+ * @returns {Object} Returns `object`.
+ * @see _.defaultsDeep
+ * @example
+ *
+ * _.defaults({ 'a': 1 }, { 'b': 2 }, { 'a': 3 });
+ * // => { 'a': 1, 'b': 2 }
+ */
+ var defaults = baseRest(function(args) {
+ args.push(undefined, assignInDefaults);
+ return apply(assignInWith, undefined, args);
+ });
+
+ /**
+ * This method is like `_.defaults` except that it recursively assigns
+ * default properties.
+ *
+ * **Note:** This method mutates `object`.
+ *
+ * @static
+ * @memberOf _
+ * @since 3.10.0
+ * @category Object
+ * @param {Object} object The destination object.
+ * @param {...Object} [sources] The source objects.
+ * @returns {Object} Returns `object`.
+ * @see _.defaults
+ * @example
+ *
+ * _.defaultsDeep({ 'a': { 'b': 2 } }, { 'a': { 'b': 1, 'c': 3 } });
+ * // => { 'a': { 'b': 2, 'c': 3 } }
+ */
+ var defaultsDeep = baseRest(function(args) {
+ args.push(undefined, mergeDefaults);
+ return apply(mergeWith, undefined, args);
+ });
+
+ /**
+ * This method is like `_.find` except that it returns the key of the first
+ * element `predicate` returns truthy for instead of the element itself.
+ *
+ * @static
+ * @memberOf _
+ * @since 1.1.0
+ * @category Object
+ * @param {Object} object The object to inspect.
+ * @param {Function} [predicate=_.identity] The function invoked per iteration.
+ * @returns {string|undefined} Returns the key of the matched element,
+ * else `undefined`.
+ * @example
+ *
+ * var users = {
+ * 'barney': { 'age': 36, 'active': true },
+ * 'fred': { 'age': 40, 'active': false },
+ * 'pebbles': { 'age': 1, 'active': true }
+ * };
+ *
+ * _.findKey(users, function(o) { return o.age < 40; });
+ * // => 'barney' (iteration order is not guaranteed)
+ *
+ * // The `_.matches` iteratee shorthand.
+ * _.findKey(users, { 'age': 1, 'active': true });
+ * // => 'pebbles'
+ *
+ * // The `_.matchesProperty` iteratee shorthand.
+ * _.findKey(users, ['active', false]);
+ * // => 'fred'
+ *
+ * // The `_.property` iteratee shorthand.
+ * _.findKey(users, 'active');
+ * // => 'barney'
+ */
+ function findKey(object, predicate) {
+ return baseFindKey(object, getIteratee(predicate, 3), baseForOwn);
+ }
+
+ /**
+ * This method is like `_.findKey` except that it iterates over elements of
+ * a collection in the opposite order.
+ *
+ * @static
+ * @memberOf _
+ * @since 2.0.0
+ * @category Object
+ * @param {Object} object The object to inspect.
+ * @param {Function} [predicate=_.identity] The function invoked per iteration.
+ * @returns {string|undefined} Returns the key of the matched element,
+ * else `undefined`.
+ * @example
+ *
+ * var users = {
+ * 'barney': { 'age': 36, 'active': true },
+ * 'fred': { 'age': 40, 'active': false },
+ * 'pebbles': { 'age': 1, 'active': true }
+ * };
+ *
+ * _.findLastKey(users, function(o) { return o.age < 40; });
+ * // => returns 'pebbles' assuming `_.findKey` returns 'barney'
+ *
+ * // The `_.matches` iteratee shorthand.
+ * _.findLastKey(users, { 'age': 36, 'active': true });
+ * // => 'barney'
+ *
+ * // The `_.matchesProperty` iteratee shorthand.
+ * _.findLastKey(users, ['active', false]);
+ * // => 'fred'
+ *
+ * // The `_.property` iteratee shorthand.
+ * _.findLastKey(users, 'active');
+ * // => 'pebbles'
+ */
+ function findLastKey(object, predicate) {
+ return baseFindKey(object, getIteratee(predicate, 3), baseForOwnRight);
+ }
+
+ /**
+ * Iterates over own and inherited enumerable string keyed properties of an
+ * object and invokes `iteratee` for each property. The iteratee is invoked
+ * with three arguments: (value, key, object). Iteratee functions may exit
+ * iteration early by explicitly returning `false`.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.3.0
+ * @category Object
+ * @param {Object} object The object to iterate over.
+ * @param {Function} [iteratee=_.identity] The function invoked per iteration.
+ * @returns {Object} Returns `object`.
+ * @see _.forInRight
+ * @example
+ *
+ * function Foo() {
+ * this.a = 1;
+ * this.b = 2;
+ * }
+ *
+ * Foo.prototype.c = 3;
+ *
+ * _.forIn(new Foo, function(value, key) {
+ * console.log(key);
+ * });
+ * // => Logs 'a', 'b', then 'c' (iteration order is not guaranteed).
+ */
+ function forIn(object, iteratee) {
+ return object == null
+ ? object
+ : baseFor(object, getIteratee(iteratee, 3), keysIn);
+ }
+
+ /**
+ * This method is like `_.forIn` except that it iterates over properties of
+ * `object` in the opposite order.
+ *
+ * @static
+ * @memberOf _
+ * @since 2.0.0
+ * @category Object
+ * @param {Object} object The object to iterate over.
+ * @param {Function} [iteratee=_.identity] The function invoked per iteration.
+ * @returns {Object} Returns `object`.
+ * @see _.forIn
+ * @example
+ *
+ * function Foo() {
+ * this.a = 1;
+ * this.b = 2;
+ * }
+ *
+ * Foo.prototype.c = 3;
+ *
+ * _.forInRight(new Foo, function(value, key) {
+ * console.log(key);
+ * });
+ * // => Logs 'c', 'b', then 'a' assuming `_.forIn` logs 'a', 'b', then 'c'.
+ */
+ function forInRight(object, iteratee) {
+ return object == null
+ ? object
+ : baseForRight(object, getIteratee(iteratee, 3), keysIn);
+ }
+
+ /**
+ * Iterates over own enumerable string keyed properties of an object and
+ * invokes `iteratee` for each property. The iteratee is invoked with three
+ * arguments: (value, key, object). Iteratee functions may exit iteration
+ * early by explicitly returning `false`.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.3.0
+ * @category Object
+ * @param {Object} object The object to iterate over.
+ * @param {Function} [iteratee=_.identity] The function invoked per iteration.
+ * @returns {Object} Returns `object`.
+ * @see _.forOwnRight
+ * @example
+ *
+ * function Foo() {
+ * this.a = 1;
+ * this.b = 2;
+ * }
+ *
+ * Foo.prototype.c = 3;
+ *
+ * _.forOwn(new Foo, function(value, key) {
+ * console.log(key);
+ * });
+ * // => Logs 'a' then 'b' (iteration order is not guaranteed).
+ */
+ function forOwn(object, iteratee) {
+ return object && baseForOwn(object, getIteratee(iteratee, 3));
+ }
+
+ /**
+ * This method is like `_.forOwn` except that it iterates over properties of
+ * `object` in the opposite order.
+ *
+ * @static
+ * @memberOf _
+ * @since 2.0.0
+ * @category Object
+ * @param {Object} object The object to iterate over.
+ * @param {Function} [iteratee=_.identity] The function invoked per iteration.
+ * @returns {Object} Returns `object`.
+ * @see _.forOwn
+ * @example
+ *
+ * function Foo() {
+ * this.a = 1;
+ * this.b = 2;
+ * }
+ *
+ * Foo.prototype.c = 3;
+ *
+ * _.forOwnRight(new Foo, function(value, key) {
+ * console.log(key);
+ * });
+ * // => Logs 'b' then 'a' assuming `_.forOwn` logs 'a' then 'b'.
+ */
+ function forOwnRight(object, iteratee) {
+ return object && baseForOwnRight(object, getIteratee(iteratee, 3));
+ }
+
+ /**
+ * Creates an array of function property names from own enumerable properties
+ * of `object`.
+ *
+ * @static
+ * @since 0.1.0
+ * @memberOf _
+ * @category Object
+ * @param {Object} object The object to inspect.
+ * @returns {Array} Returns the function names.
+ * @see _.functionsIn
+ * @example
+ *
+ * function Foo() {
+ * this.a = _.constant('a');
+ * this.b = _.constant('b');
+ * }
+ *
+ * Foo.prototype.c = _.constant('c');
+ *
+ * _.functions(new Foo);
+ * // => ['a', 'b']
+ */
+ function functions(object) {
+ return object == null ? [] : baseFunctions(object, keys(object));
+ }
+
+ /**
+ * Creates an array of function property names from own and inherited
+ * enumerable properties of `object`.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Object
+ * @param {Object} object The object to inspect.
+ * @returns {Array} Returns the function names.
+ * @see _.functions
+ * @example
+ *
+ * function Foo() {
+ * this.a = _.constant('a');
+ * this.b = _.constant('b');
+ * }
+ *
+ * Foo.prototype.c = _.constant('c');
+ *
+ * _.functionsIn(new Foo);
+ * // => ['a', 'b', 'c']
+ */
+ function functionsIn(object) {
+ return object == null ? [] : baseFunctions(object, keysIn(object));
+ }
+
+ /**
+ * Gets the value at `path` of `object`. If the resolved value is
+ * `undefined`, the `defaultValue` is returned in its place.
+ *
+ * @static
+ * @memberOf _
+ * @since 3.7.0
+ * @category Object
+ * @param {Object} object The object to query.
+ * @param {Array|string} path The path of the property to get.
+ * @param {*} [defaultValue] The value returned for `undefined` resolved values.
+ * @returns {*} Returns the resolved value.
+ * @example
+ *
+ * var object = { 'a': [{ 'b': { 'c': 3 } }] };
+ *
+ * _.get(object, 'a[0].b.c');
+ * // => 3
+ *
+ * _.get(object, ['a', '0', 'b', 'c']);
+ * // => 3
+ *
+ * _.get(object, 'a.b.c', 'default');
+ * // => 'default'
+ */
+ function get(object, path, defaultValue) {
+ var result = object == null ? undefined : baseGet(object, path);
+ return result === undefined ? defaultValue : result;
+ }
+
+ /**
+ * Checks if `path` is a direct property of `object`.
+ *
+ * @static
+ * @since 0.1.0
+ * @memberOf _
+ * @category Object
+ * @param {Object} object The object to query.
+ * @param {Array|string} path The path to check.
+ * @returns {boolean} Returns `true` if `path` exists, else `false`.
+ * @example
+ *
+ * var object = { 'a': { 'b': 2 } };
+ * var other = _.create({ 'a': _.create({ 'b': 2 }) });
+ *
+ * _.has(object, 'a');
+ * // => true
+ *
+ * _.has(object, 'a.b');
+ * // => true
+ *
+ * _.has(object, ['a', 'b']);
+ * // => true
+ *
+ * _.has(other, 'a');
+ * // => false
+ */
+ function has(object, path) {
+ return object != null && hasPath(object, path, baseHas);
+ }
+
+ /**
+ * Checks if `path` is a direct or inherited property of `object`.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Object
+ * @param {Object} object The object to query.
+ * @param {Array|string} path The path to check.
+ * @returns {boolean} Returns `true` if `path` exists, else `false`.
+ * @example
+ *
+ * var object = _.create({ 'a': _.create({ 'b': 2 }) });
+ *
+ * _.hasIn(object, 'a');
+ * // => true
+ *
+ * _.hasIn(object, 'a.b');
+ * // => true
+ *
+ * _.hasIn(object, ['a', 'b']);
+ * // => true
+ *
+ * _.hasIn(object, 'b');
+ * // => false
+ */
+ function hasIn(object, path) {
+ return object != null && hasPath(object, path, baseHasIn);
+ }
+
+ /**
+ * Creates an object composed of the inverted keys and values of `object`.
+ * If `object` contains duplicate values, subsequent values overwrite
+ * property assignments of previous values.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.7.0
+ * @category Object
+ * @param {Object} object The object to invert.
+ * @returns {Object} Returns the new inverted object.
+ * @example
+ *
+ * var object = { 'a': 1, 'b': 2, 'c': 1 };
+ *
+ * _.invert(object);
+ * // => { '1': 'c', '2': 'b' }
+ */
+ var invert = createInverter(function(result, value, key) {
+ result[value] = key;
+ }, constant(identity));
+
+ /**
+ * This method is like `_.invert` except that the inverted object is generated
+ * from the results of running each element of `object` thru `iteratee`. The
+ * corresponding inverted value of each inverted key is an array of keys
+ * responsible for generating the inverted value. The iteratee is invoked
+ * with one argument: (value).
+ *
+ * @static
+ * @memberOf _
+ * @since 4.1.0
+ * @category Object
+ * @param {Object} object The object to invert.
+ * @param {Function} [iteratee=_.identity] The iteratee invoked per element.
+ * @returns {Object} Returns the new inverted object.
+ * @example
+ *
+ * var object = { 'a': 1, 'b': 2, 'c': 1 };
+ *
+ * _.invertBy(object);
+ * // => { '1': ['a', 'c'], '2': ['b'] }
+ *
+ * _.invertBy(object, function(value) {
+ * return 'group' + value;
+ * });
+ * // => { 'group1': ['a', 'c'], 'group2': ['b'] }
+ */
+ var invertBy = createInverter(function(result, value, key) {
+ if (hasOwnProperty.call(result, value)) {
+ result[value].push(key);
+ } else {
+ result[value] = [key];
+ }
+ }, getIteratee);
+
+ /**
+ * Invokes the method at `path` of `object`.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Object
+ * @param {Object} object The object to query.
+ * @param {Array|string} path The path of the method to invoke.
+ * @param {...*} [args] The arguments to invoke the method with.
+ * @returns {*} Returns the result of the invoked method.
+ * @example
+ *
+ * var object = { 'a': [{ 'b': { 'c': [1, 2, 3, 4] } }] };
+ *
+ * _.invoke(object, 'a[0].b.c.slice', 1, 3);
+ * // => [2, 3]
+ */
+ var invoke = baseRest(baseInvoke);
+
+ /**
+ * Creates an array of the own enumerable property names of `object`.
+ *
+ * **Note:** Non-object values are coerced to objects. See the
+ * [ES spec](http://ecma-international.org/ecma-262/7.0/#sec-object.keys)
+ * for more details.
+ *
+ * @static
+ * @since 0.1.0
+ * @memberOf _
+ * @category Object
+ * @param {Object} object The object to query.
+ * @returns {Array} Returns the array of property names.
+ * @example
+ *
+ * function Foo() {
+ * this.a = 1;
+ * this.b = 2;
+ * }
+ *
+ * Foo.prototype.c = 3;
+ *
+ * _.keys(new Foo);
+ * // => ['a', 'b'] (iteration order is not guaranteed)
+ *
+ * _.keys('hi');
+ * // => ['0', '1']
+ */
+ function keys(object) {
+ return isArrayLike(object) ? arrayLikeKeys(object) : baseKeys(object);
+ }
+
+ /**
+ * Creates an array of the own and inherited enumerable property names of `object`.
+ *
+ * **Note:** Non-object values are coerced to objects.
+ *
+ * @static
+ * @memberOf _
+ * @since 3.0.0
+ * @category Object
+ * @param {Object} object The object to query.
+ * @returns {Array} Returns the array of property names.
+ * @example
+ *
+ * function Foo() {
+ * this.a = 1;
+ * this.b = 2;
+ * }
+ *
+ * Foo.prototype.c = 3;
+ *
+ * _.keysIn(new Foo);
+ * // => ['a', 'b', 'c'] (iteration order is not guaranteed)
+ */
+ function keysIn(object) {
+ return isArrayLike(object) ? arrayLikeKeys(object, true) : baseKeysIn(object);
+ }
+
+ /**
+ * The opposite of `_.mapValues`; this method creates an object with the
+ * same values as `object` and keys generated by running each own enumerable
+ * string keyed property of `object` thru `iteratee`. The iteratee is invoked
+ * with three arguments: (value, key, object).
+ *
+ * @static
+ * @memberOf _
+ * @since 3.8.0
+ * @category Object
+ * @param {Object} object The object to iterate over.
+ * @param {Function} [iteratee=_.identity] The function invoked per iteration.
+ * @returns {Object} Returns the new mapped object.
+ * @see _.mapValues
+ * @example
+ *
+ * _.mapKeys({ 'a': 1, 'b': 2 }, function(value, key) {
+ * return key + value;
+ * });
+ * // => { 'a1': 1, 'b2': 2 }
+ */
+ function mapKeys(object, iteratee) {
+ var result = {};
+ iteratee = getIteratee(iteratee, 3);
+
+ baseForOwn(object, function(value, key, object) {
+ baseAssignValue(result, iteratee(value, key, object), value);
+ });
+ return result;
+ }
+
+ /**
+ * Creates an object with the same keys as `object` and values generated
+ * by running each own enumerable string keyed property of `object` thru
+ * `iteratee`. The iteratee is invoked with three arguments:
+ * (value, key, object).
+ *
+ * @static
+ * @memberOf _
+ * @since 2.4.0
+ * @category Object
+ * @param {Object} object The object to iterate over.
+ * @param {Function} [iteratee=_.identity] The function invoked per iteration.
+ * @returns {Object} Returns the new mapped object.
+ * @see _.mapKeys
+ * @example
+ *
+ * var users = {
+ * 'fred': { 'user': 'fred', 'age': 40 },
+ * 'pebbles': { 'user': 'pebbles', 'age': 1 }
+ * };
+ *
+ * _.mapValues(users, function(o) { return o.age; });
+ * // => { 'fred': 40, 'pebbles': 1 } (iteration order is not guaranteed)
+ *
+ * // The `_.property` iteratee shorthand.
+ * _.mapValues(users, 'age');
+ * // => { 'fred': 40, 'pebbles': 1 } (iteration order is not guaranteed)
+ */
+ function mapValues(object, iteratee) {
+ var result = {};
+ iteratee = getIteratee(iteratee, 3);
+
+ baseForOwn(object, function(value, key, object) {
+ baseAssignValue(result, key, iteratee(value, key, object));
+ });
+ return result;
+ }
+
+ /**
+ * This method is like `_.assign` except that it recursively merges own and
+ * inherited enumerable string keyed properties of source objects into the
+ * destination object. Source properties that resolve to `undefined` are
+ * skipped if a destination value exists. Array and plain object properties
+ * are merged recursively. Other objects and value types are overridden by
+ * assignment. Source objects are applied from left to right. Subsequent
+ * sources overwrite property assignments of previous sources.
+ *
+ * **Note:** This method mutates `object`.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.5.0
+ * @category Object
+ * @param {Object} object The destination object.
+ * @param {...Object} [sources] The source objects.
+ * @returns {Object} Returns `object`.
+ * @example
+ *
+ * var object = {
+ * 'a': [{ 'b': 2 }, { 'd': 4 }]
+ * };
+ *
+ * var other = {
+ * 'a': [{ 'c': 3 }, { 'e': 5 }]
+ * };
+ *
+ * _.merge(object, other);
+ * // => { 'a': [{ 'b': 2, 'c': 3 }, { 'd': 4, 'e': 5 }] }
+ */
+ var merge = createAssigner(function(object, source, srcIndex) {
+ baseMerge(object, source, srcIndex);
+ });
+
+ /**
+ * This method is like `_.merge` except that it accepts `customizer` which
+ * is invoked to produce the merged values of the destination and source
+ * properties. If `customizer` returns `undefined`, merging is handled by the
+ * method instead. The `customizer` is invoked with six arguments:
+ * (objValue, srcValue, key, object, source, stack).
+ *
+ * **Note:** This method mutates `object`.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Object
+ * @param {Object} object The destination object.
+ * @param {...Object} sources The source objects.
+ * @param {Function} customizer The function to customize assigned values.
+ * @returns {Object} Returns `object`.
+ * @example
+ *
+ * function customizer(objValue, srcValue) {
+ * if (_.isArray(objValue)) {
+ * return objValue.concat(srcValue);
+ * }
+ * }
+ *
+ * var object = { 'a': [1], 'b': [2] };
+ * var other = { 'a': [3], 'b': [4] };
+ *
+ * _.mergeWith(object, other, customizer);
+ * // => { 'a': [1, 3], 'b': [2, 4] }
+ */
+ var mergeWith = createAssigner(function(object, source, srcIndex, customizer) {
+ baseMerge(object, source, srcIndex, customizer);
+ });
+
+ /**
+ * The opposite of `_.pick`; this method creates an object composed of the
+ * own and inherited enumerable string keyed properties of `object` that are
+ * not omitted.
+ *
+ * @static
+ * @since 0.1.0
+ * @memberOf _
+ * @category Object
+ * @param {Object} object The source object.
+ * @param {...(string|string[])} [props] The property identifiers to omit.
+ * @returns {Object} Returns the new object.
+ * @example
+ *
+ * var object = { 'a': 1, 'b': '2', 'c': 3 };
+ *
+ * _.omit(object, ['a', 'c']);
+ * // => { 'b': '2' }
+ */
+ var omit = flatRest(function(object, props) {
+ if (object == null) {
+ return {};
+ }
+ props = arrayMap(props, toKey);
+ return basePick(object, baseDifference(getAllKeysIn(object), props));
+ });
+
+ /**
+ * The opposite of `_.pickBy`; this method creates an object composed of
+ * the own and inherited enumerable string keyed properties of `object` that
+ * `predicate` doesn't return truthy for. The predicate is invoked with two
+ * arguments: (value, key).
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Object
+ * @param {Object} object The source object.
+ * @param {Function} [predicate=_.identity] The function invoked per property.
+ * @returns {Object} Returns the new object.
+ * @example
+ *
+ * var object = { 'a': 1, 'b': '2', 'c': 3 };
+ *
+ * _.omitBy(object, _.isNumber);
+ * // => { 'b': '2' }
+ */
+ function omitBy(object, predicate) {
+ return pickBy(object, negate(getIteratee(predicate)));
+ }
+
+ /**
+ * Creates an object composed of the picked `object` properties.
+ *
+ * @static
+ * @since 0.1.0
+ * @memberOf _
+ * @category Object
+ * @param {Object} object The source object.
+ * @param {...(string|string[])} [props] The property identifiers to pick.
+ * @returns {Object} Returns the new object.
+ * @example
+ *
+ * var object = { 'a': 1, 'b': '2', 'c': 3 };
+ *
+ * _.pick(object, ['a', 'c']);
+ * // => { 'a': 1, 'c': 3 }
+ */
+ var pick = flatRest(function(object, props) {
+ return object == null ? {} : basePick(object, arrayMap(props, toKey));
+ });
+
+ /**
+ * Creates an object composed of the `object` properties `predicate` returns
+ * truthy for. The predicate is invoked with two arguments: (value, key).
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Object
+ * @param {Object} object The source object.
+ * @param {Function} [predicate=_.identity] The function invoked per property.
+ * @returns {Object} Returns the new object.
+ * @example
+ *
+ * var object = { 'a': 1, 'b': '2', 'c': 3 };
+ *
+ * _.pickBy(object, _.isNumber);
+ * // => { 'a': 1, 'c': 3 }
+ */
+ function pickBy(object, predicate) {
+ return object == null ? {} : basePickBy(object, getAllKeysIn(object), getIteratee(predicate));
+ }
+
+ /**
+ * This method is like `_.get` except that if the resolved value is a
+ * function it's invoked with the `this` binding of its parent object and
+ * its result is returned.
+ *
+ * @static
+ * @since 0.1.0
+ * @memberOf _
+ * @category Object
+ * @param {Object} object The object to query.
+ * @param {Array|string} path The path of the property to resolve.
+ * @param {*} [defaultValue] The value returned for `undefined` resolved values.
+ * @returns {*} Returns the resolved value.
+ * @example
+ *
+ * var object = { 'a': [{ 'b': { 'c1': 3, 'c2': _.constant(4) } }] };
+ *
+ * _.result(object, 'a[0].b.c1');
+ * // => 3
+ *
+ * _.result(object, 'a[0].b.c2');
+ * // => 4
+ *
+ * _.result(object, 'a[0].b.c3', 'default');
+ * // => 'default'
+ *
+ * _.result(object, 'a[0].b.c3', _.constant('default'));
+ * // => 'default'
+ */
+ function result(object, path, defaultValue) {
+ path = isKey(path, object) ? [path] : castPath(path);
+
+ var index = -1,
+ length = path.length;
+
+ // Ensure the loop is entered when path is empty.
+ if (!length) {
+ object = undefined;
+ length = 1;
+ }
+ while (++index < length) {
+ var value = object == null ? undefined : object[toKey(path[index])];
+ if (value === undefined) {
+ index = length;
+ value = defaultValue;
+ }
+ object = isFunction(value) ? value.call(object) : value;
+ }
+ return object;
+ }
+
+ /**
+ * Sets the value at `path` of `object`. If a portion of `path` doesn't exist,
+ * it's created. Arrays are created for missing index properties while objects
+ * are created for all other missing properties. Use `_.setWith` to customize
+ * `path` creation.
+ *
+ * **Note:** This method mutates `object`.
+ *
+ * @static
+ * @memberOf _
+ * @since 3.7.0
+ * @category Object
+ * @param {Object} object The object to modify.
+ * @param {Array|string} path The path of the property to set.
+ * @param {*} value The value to set.
+ * @returns {Object} Returns `object`.
+ * @example
+ *
+ * var object = { 'a': [{ 'b': { 'c': 3 } }] };
+ *
+ * _.set(object, 'a[0].b.c', 4);
+ * console.log(object.a[0].b.c);
+ * // => 4
+ *
+ * _.set(object, ['x', '0', 'y', 'z'], 5);
+ * console.log(object.x[0].y.z);
+ * // => 5
+ */
+ function set(object, path, value) {
+ return object == null ? object : baseSet(object, path, value);
+ }
+
+ /**
+ * This method is like `_.set` except that it accepts `customizer` which is
+ * invoked to produce the objects of `path`. If `customizer` returns `undefined`
+ * path creation is handled by the method instead. The `customizer` is invoked
+ * with three arguments: (nsValue, key, nsObject).
+ *
+ * **Note:** This method mutates `object`.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Object
+ * @param {Object} object The object to modify.
+ * @param {Array|string} path The path of the property to set.
+ * @param {*} value The value to set.
+ * @param {Function} [customizer] The function to customize assigned values.
+ * @returns {Object} Returns `object`.
+ * @example
+ *
+ * var object = {};
+ *
+ * _.setWith(object, '[0][1]', 'a', Object);
+ * // => { '0': { '1': 'a' } }
+ */
+ function setWith(object, path, value, customizer) {
+ customizer = typeof customizer == 'function' ? customizer : undefined;
+ return object == null ? object : baseSet(object, path, value, customizer);
+ }
+
+ /**
+ * Creates an array of own enumerable string keyed-value pairs for `object`
+ * which can be consumed by `_.fromPairs`. If `object` is a map or set, its
+ * entries are returned.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @alias entries
+ * @category Object
+ * @param {Object} object The object to query.
+ * @returns {Array} Returns the key-value pairs.
+ * @example
+ *
+ * function Foo() {
+ * this.a = 1;
+ * this.b = 2;
+ * }
+ *
+ * Foo.prototype.c = 3;
+ *
+ * _.toPairs(new Foo);
+ * // => [['a', 1], ['b', 2]] (iteration order is not guaranteed)
+ */
+ var toPairs = createToPairs(keys);
+
+ /**
+ * Creates an array of own and inherited enumerable string keyed-value pairs
+ * for `object` which can be consumed by `_.fromPairs`. If `object` is a map
+ * or set, its entries are returned.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @alias entriesIn
+ * @category Object
+ * @param {Object} object The object to query.
+ * @returns {Array} Returns the key-value pairs.
+ * @example
+ *
+ * function Foo() {
+ * this.a = 1;
+ * this.b = 2;
+ * }
+ *
+ * Foo.prototype.c = 3;
+ *
+ * _.toPairsIn(new Foo);
+ * // => [['a', 1], ['b', 2], ['c', 3]] (iteration order is not guaranteed)
+ */
+ var toPairsIn = createToPairs(keysIn);
+
+ /**
+ * An alternative to `_.reduce`; this method transforms `object` to a new
+ * `accumulator` object which is the result of running each of its own
+ * enumerable string keyed properties thru `iteratee`, with each invocation
+ * potentially mutating the `accumulator` object. If `accumulator` is not
+ * provided, a new object with the same `[[Prototype]]` will be used. The
+ * iteratee is invoked with four arguments: (accumulator, value, key, object).
+ * Iteratee functions may exit iteration early by explicitly returning `false`.
+ *
+ * @static
+ * @memberOf _
+ * @since 1.3.0
+ * @category Object
+ * @param {Object} object The object to iterate over.
+ * @param {Function} [iteratee=_.identity] The function invoked per iteration.
+ * @param {*} [accumulator] The custom accumulator value.
+ * @returns {*} Returns the accumulated value.
+ * @example
+ *
+ * _.transform([2, 3, 4], function(result, n) {
+ * result.push(n *= n);
+ * return n % 2 == 0;
+ * }, []);
+ * // => [4, 9]
+ *
+ * _.transform({ 'a': 1, 'b': 2, 'c': 1 }, function(result, value, key) {
+ * (result[value] || (result[value] = [])).push(key);
+ * }, {});
+ * // => { '1': ['a', 'c'], '2': ['b'] }
+ */
+ function transform(object, iteratee, accumulator) {
+ var isArr = isArray(object),
+ isArrLike = isArr || isBuffer(object) || isTypedArray(object);
+
+ iteratee = getIteratee(iteratee, 4);
+ if (accumulator == null) {
+ var Ctor = object && object.constructor;
+ if (isArrLike) {
+ accumulator = isArr ? new Ctor : [];
+ }
+ else if (isObject(object)) {
+ accumulator = isFunction(Ctor) ? baseCreate(getPrototype(object)) : {};
+ }
+ else {
+ accumulator = {};
+ }
+ }
+ (isArrLike ? arrayEach : baseForOwn)(object, function(value, index, object) {
+ return iteratee(accumulator, value, index, object);
+ });
+ return accumulator;
+ }
+
+ /**
+ * Removes the property at `path` of `object`.
+ *
+ * **Note:** This method mutates `object`.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Object
+ * @param {Object} object The object to modify.
+ * @param {Array|string} path The path of the property to unset.
+ * @returns {boolean} Returns `true` if the property is deleted, else `false`.
+ * @example
+ *
+ * var object = { 'a': [{ 'b': { 'c': 7 } }] };
+ * _.unset(object, 'a[0].b.c');
+ * // => true
+ *
+ * console.log(object);
+ * // => { 'a': [{ 'b': {} }] };
+ *
+ * _.unset(object, ['a', '0', 'b', 'c']);
+ * // => true
+ *
+ * console.log(object);
+ * // => { 'a': [{ 'b': {} }] };
+ */
+ function unset(object, path) {
+ return object == null ? true : baseUnset(object, path);
+ }
+
+ /**
+ * This method is like `_.set` except that accepts `updater` to produce the
+ * value to set. Use `_.updateWith` to customize `path` creation. The `updater`
+ * is invoked with one argument: (value).
+ *
+ * **Note:** This method mutates `object`.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.6.0
+ * @category Object
+ * @param {Object} object The object to modify.
+ * @param {Array|string} path The path of the property to set.
+ * @param {Function} updater The function to produce the updated value.
+ * @returns {Object} Returns `object`.
+ * @example
+ *
+ * var object = { 'a': [{ 'b': { 'c': 3 } }] };
+ *
+ * _.update(object, 'a[0].b.c', function(n) { return n * n; });
+ * console.log(object.a[0].b.c);
+ * // => 9
+ *
+ * _.update(object, 'x[0].y.z', function(n) { return n ? n + 1 : 0; });
+ * console.log(object.x[0].y.z);
+ * // => 0
+ */
+ function update(object, path, updater) {
+ return object == null ? object : baseUpdate(object, path, castFunction(updater));
+ }
+
+ /**
+ * This method is like `_.update` except that it accepts `customizer` which is
+ * invoked to produce the objects of `path`. If `customizer` returns `undefined`
+ * path creation is handled by the method instead. The `customizer` is invoked
+ * with three arguments: (nsValue, key, nsObject).
+ *
+ * **Note:** This method mutates `object`.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.6.0
+ * @category Object
+ * @param {Object} object The object to modify.
+ * @param {Array|string} path The path of the property to set.
+ * @param {Function} updater The function to produce the updated value.
+ * @param {Function} [customizer] The function to customize assigned values.
+ * @returns {Object} Returns `object`.
+ * @example
+ *
+ * var object = {};
+ *
+ * _.updateWith(object, '[0][1]', _.constant('a'), Object);
+ * // => { '0': { '1': 'a' } }
+ */
+ function updateWith(object, path, updater, customizer) {
+ customizer = typeof customizer == 'function' ? customizer : undefined;
+ return object == null ? object : baseUpdate(object, path, castFunction(updater), customizer);
+ }
+
+ /**
+ * Creates an array of the own enumerable string keyed property values of `object`.
+ *
+ * **Note:** Non-object values are coerced to objects.
+ *
+ * @static
+ * @since 0.1.0
+ * @memberOf _
+ * @category Object
+ * @param {Object} object The object to query.
+ * @returns {Array} Returns the array of property values.
+ * @example
+ *
+ * function Foo() {
+ * this.a = 1;
+ * this.b = 2;
+ * }
+ *
+ * Foo.prototype.c = 3;
+ *
+ * _.values(new Foo);
+ * // => [1, 2] (iteration order is not guaranteed)
+ *
+ * _.values('hi');
+ * // => ['h', 'i']
+ */
+ function values(object) {
+ return object ? baseValues(object, keys(object)) : [];
+ }
+
+ /**
+ * Creates an array of the own and inherited enumerable string keyed property
+ * values of `object`.
+ *
+ * **Note:** Non-object values are coerced to objects.
+ *
+ * @static
+ * @memberOf _
+ * @since 3.0.0
+ * @category Object
+ * @param {Object} object The object to query.
+ * @returns {Array} Returns the array of property values.
+ * @example
+ *
+ * function Foo() {
+ * this.a = 1;
+ * this.b = 2;
+ * }
+ *
+ * Foo.prototype.c = 3;
+ *
+ * _.valuesIn(new Foo);
+ * // => [1, 2, 3] (iteration order is not guaranteed)
+ */
+ function valuesIn(object) {
+ return object == null ? [] : baseValues(object, keysIn(object));
+ }
+
+ /*------------------------------------------------------------------------*/
+
+ /**
+ * Clamps `number` within the inclusive `lower` and `upper` bounds.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Number
+ * @param {number} number The number to clamp.
+ * @param {number} [lower] The lower bound.
+ * @param {number} upper The upper bound.
+ * @returns {number} Returns the clamped number.
+ * @example
+ *
+ * _.clamp(-10, -5, 5);
+ * // => -5
+ *
+ * _.clamp(10, -5, 5);
+ * // => 5
+ */
+ function clamp(number, lower, upper) {
+ if (upper === undefined) {
+ upper = lower;
+ lower = undefined;
+ }
+ if (upper !== undefined) {
+ upper = toNumber(upper);
+ upper = upper === upper ? upper : 0;
+ }
+ if (lower !== undefined) {
+ lower = toNumber(lower);
+ lower = lower === lower ? lower : 0;
+ }
+ return baseClamp(toNumber(number), lower, upper);
+ }
+
+ /**
+ * Checks if `n` is between `start` and up to, but not including, `end`. If
+ * `end` is not specified, it's set to `start` with `start` then set to `0`.
+ * If `start` is greater than `end` the params are swapped to support
+ * negative ranges.
+ *
+ * @static
+ * @memberOf _
+ * @since 3.3.0
+ * @category Number
+ * @param {number} number The number to check.
+ * @param {number} [start=0] The start of the range.
+ * @param {number} end The end of the range.
+ * @returns {boolean} Returns `true` if `number` is in the range, else `false`.
+ * @see _.range, _.rangeRight
+ * @example
+ *
+ * _.inRange(3, 2, 4);
+ * // => true
+ *
+ * _.inRange(4, 8);
+ * // => true
+ *
+ * _.inRange(4, 2);
+ * // => false
+ *
+ * _.inRange(2, 2);
+ * // => false
+ *
+ * _.inRange(1.2, 2);
+ * // => true
+ *
+ * _.inRange(5.2, 4);
+ * // => false
+ *
+ * _.inRange(-3, -2, -6);
+ * // => true
+ */
+ function inRange(number, start, end) {
+ start = toFinite(start);
+ if (end === undefined) {
+ end = start;
+ start = 0;
+ } else {
+ end = toFinite(end);
+ }
+ number = toNumber(number);
+ return baseInRange(number, start, end);
+ }
+
+ /**
+ * Produces a random number between the inclusive `lower` and `upper` bounds.
+ * If only one argument is provided a number between `0` and the given number
+ * is returned. If `floating` is `true`, or either `lower` or `upper` are
+ * floats, a floating-point number is returned instead of an integer.
+ *
+ * **Note:** JavaScript follows the IEEE-754 standard for resolving
+ * floating-point values which can produce unexpected results.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.7.0
+ * @category Number
+ * @param {number} [lower=0] The lower bound.
+ * @param {number} [upper=1] The upper bound.
+ * @param {boolean} [floating] Specify returning a floating-point number.
+ * @returns {number} Returns the random number.
+ * @example
+ *
+ * _.random(0, 5);
+ * // => an integer between 0 and 5
+ *
+ * _.random(5);
+ * // => also an integer between 0 and 5
+ *
+ * _.random(5, true);
+ * // => a floating-point number between 0 and 5
+ *
+ * _.random(1.2, 5.2);
+ * // => a floating-point number between 1.2 and 5.2
+ */
+ function random(lower, upper, floating) {
+ if (floating && typeof floating != 'boolean' && isIterateeCall(lower, upper, floating)) {
+ upper = floating = undefined;
+ }
+ if (floating === undefined) {
+ if (typeof upper == 'boolean') {
+ floating = upper;
+ upper = undefined;
+ }
+ else if (typeof lower == 'boolean') {
+ floating = lower;
+ lower = undefined;
+ }
+ }
+ if (lower === undefined && upper === undefined) {
+ lower = 0;
+ upper = 1;
+ }
+ else {
+ lower = toFinite(lower);
+ if (upper === undefined) {
+ upper = lower;
+ lower = 0;
+ } else {
+ upper = toFinite(upper);
+ }
+ }
+ if (lower > upper) {
+ var temp = lower;
+ lower = upper;
+ upper = temp;
+ }
+ if (floating || lower % 1 || upper % 1) {
+ var rand = nativeRandom();
+ return nativeMin(lower + (rand * (upper - lower + freeParseFloat('1e-' + ((rand + '').length - 1)))), upper);
+ }
+ return baseRandom(lower, upper);
+ }
+
+ /*------------------------------------------------------------------------*/
+
+ /**
+ * Converts `string` to [camel case](https://en.wikipedia.org/wiki/CamelCase).
+ *
+ * @static
+ * @memberOf _
+ * @since 3.0.0
+ * @category String
+ * @param {string} [string=''] The string to convert.
+ * @returns {string} Returns the camel cased string.
+ * @example
+ *
+ * _.camelCase('Foo Bar');
+ * // => 'fooBar'
+ *
+ * _.camelCase('--foo-bar--');
+ * // => 'fooBar'
+ *
+ * _.camelCase('__FOO_BAR__');
+ * // => 'fooBar'
+ */
+ var camelCase = createCompounder(function(result, word, index) {
+ word = word.toLowerCase();
+ return result + (index ? capitalize(word) : word);
+ });
+
+ /**
+ * Converts the first character of `string` to upper case and the remaining
+ * to lower case.
+ *
+ * @static
+ * @memberOf _
+ * @since 3.0.0
+ * @category String
+ * @param {string} [string=''] The string to capitalize.
+ * @returns {string} Returns the capitalized string.
+ * @example
+ *
+ * _.capitalize('FRED');
+ * // => 'Fred'
+ */
+ function capitalize(string) {
+ return upperFirst(toString(string).toLowerCase());
+ }
+
+ /**
+ * Deburrs `string` by converting
+ * [Latin-1 Supplement](https://en.wikipedia.org/wiki/Latin-1_Supplement_(Unicode_block)#Character_table)
+ * and [Latin Extended-A](https://en.wikipedia.org/wiki/Latin_Extended-A)
+ * letters to basic Latin letters and removing
+ * [combining diacritical marks](https://en.wikipedia.org/wiki/Combining_Diacritical_Marks).
+ *
+ * @static
+ * @memberOf _
+ * @since 3.0.0
+ * @category String
+ * @param {string} [string=''] The string to deburr.
+ * @returns {string} Returns the deburred string.
+ * @example
+ *
+ * _.deburr('déjà vu');
+ * // => 'deja vu'
+ */
+ function deburr(string) {
+ string = toString(string);
+ return string && string.replace(reLatin, deburrLetter).replace(reComboMark, '');
+ }
+
+ /**
+ * Checks if `string` ends with the given target string.
+ *
+ * @static
+ * @memberOf _
+ * @since 3.0.0
+ * @category String
+ * @param {string} [string=''] The string to inspect.
+ * @param {string} [target] The string to search for.
+ * @param {number} [position=string.length] The position to search up to.
+ * @returns {boolean} Returns `true` if `string` ends with `target`,
+ * else `false`.
+ * @example
+ *
+ * _.endsWith('abc', 'c');
+ * // => true
+ *
+ * _.endsWith('abc', 'b');
+ * // => false
+ *
+ * _.endsWith('abc', 'b', 2);
+ * // => true
+ */
+ function endsWith(string, target, position) {
+ string = toString(string);
+ target = baseToString(target);
+
+ var length = string.length;
+ position = position === undefined
+ ? length
+ : baseClamp(toInteger(position), 0, length);
+
+ var end = position;
+ position -= target.length;
+ return position >= 0 && string.slice(position, end) == target;
+ }
+
+ /**
+ * Converts the characters "&", "<", ">", '"', and "'" in `string` to their
+ * corresponding HTML entities.
+ *
+ * **Note:** No other characters are escaped. To escape additional
+ * characters use a third-party library like [_he_](https://mths.be/he).
+ *
+ * Though the ">" character is escaped for symmetry, characters like
+ * ">" and "/" don't need escaping in HTML and have no special meaning
+ * unless they're part of a tag or unquoted attribute value. See
+ * [Mathias Bynens's article](https://mathiasbynens.be/notes/ambiguous-ampersands)
+ * (under "semi-related fun fact") for more details.
+ *
+ * When working with HTML you should always
+ * [quote attribute values](http://wonko.com/post/html-escaping) to reduce
+ * XSS vectors.
+ *
+ * @static
+ * @since 0.1.0
+ * @memberOf _
+ * @category String
+ * @param {string} [string=''] The string to escape.
+ * @returns {string} Returns the escaped string.
+ * @example
+ *
+ * _.escape('fred, barney, & pebbles');
+ * // => 'fred, barney, &amp; pebbles'
+ */
+ function escape(string) {
+ string = toString(string);
+ return (string && reHasUnescapedHtml.test(string))
+ ? string.replace(reUnescapedHtml, escapeHtmlChar)
+ : string;
+ }
+
+ /**
+ * Escapes the `RegExp` special characters "^", "$", "\", ".", "*", "+",
+ * "?", "(", ")", "[", "]", "{", "}", and "|" in `string`.
+ *
+ * @static
+ * @memberOf _
+ * @since 3.0.0
+ * @category String
+ * @param {string} [string=''] The string to escape.
+ * @returns {string} Returns the escaped string.
+ * @example
+ *
+ * _.escapeRegExp('[lodash](https://lodash.com/)');
+ * // => '\[lodash\]\(https://lodash\.com/\)'
+ */
+ function escapeRegExp(string) {
+ string = toString(string);
+ return (string && reHasRegExpChar.test(string))
+ ? string.replace(reRegExpChar, '\\$&')
+ : string;
+ }
+
+ /**
+ * Converts `string` to
+ * [kebab case](https://en.wikipedia.org/wiki/Letter_case#Special_case_styles).
+ *
+ * @static
+ * @memberOf _
+ * @since 3.0.0
+ * @category String
+ * @param {string} [string=''] The string to convert.
+ * @returns {string} Returns the kebab cased string.
+ * @example
+ *
+ * _.kebabCase('Foo Bar');
+ * // => 'foo-bar'
+ *
+ * _.kebabCase('fooBar');
+ * // => 'foo-bar'
+ *
+ * _.kebabCase('__FOO_BAR__');
+ * // => 'foo-bar'
+ */
+ var kebabCase = createCompounder(function(result, word, index) {
+ return result + (index ? '-' : '') + word.toLowerCase();
+ });
+
+ /**
+ * Converts `string`, as space separated words, to lower case.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category String
+ * @param {string} [string=''] The string to convert.
+ * @returns {string} Returns the lower cased string.
+ * @example
+ *
+ * _.lowerCase('--Foo-Bar--');
+ * // => 'foo bar'
+ *
+ * _.lowerCase('fooBar');
+ * // => 'foo bar'
+ *
+ * _.lowerCase('__FOO_BAR__');
+ * // => 'foo bar'
+ */
+ var lowerCase = createCompounder(function(result, word, index) {
+ return result + (index ? ' ' : '') + word.toLowerCase();
+ });
+
+ /**
+ * Converts the first character of `string` to lower case.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category String
+ * @param {string} [string=''] The string to convert.
+ * @returns {string} Returns the converted string.
+ * @example
+ *
+ * _.lowerFirst('Fred');
+ * // => 'fred'
+ *
+ * _.lowerFirst('FRED');
+ * // => 'fRED'
+ */
+ var lowerFirst = createCaseFirst('toLowerCase');
+
+ /**
+ * Pads `string` on the left and right sides if it's shorter than `length`.
+ * Padding characters are truncated if they can't be evenly divided by `length`.
+ *
+ * @static
+ * @memberOf _
+ * @since 3.0.0
+ * @category String
+ * @param {string} [string=''] The string to pad.
+ * @param {number} [length=0] The padding length.
+ * @param {string} [chars=' '] The string used as padding.
+ * @returns {string} Returns the padded string.
+ * @example
+ *
+ * _.pad('abc', 8);
+ * // => ' abc '
+ *
+ * _.pad('abc', 8, '_-');
+ * // => '_-abc_-_'
+ *
+ * _.pad('abc', 3);
+ * // => 'abc'
+ */
+ function pad(string, length, chars) {
+ string = toString(string);
+ length = toInteger(length);
+
+ var strLength = length ? stringSize(string) : 0;
+ if (!length || strLength >= length) {
+ return string;
+ }
+ var mid = (length - strLength) / 2;
+ return (
+ createPadding(nativeFloor(mid), chars) +
+ string +
+ createPadding(nativeCeil(mid), chars)
+ );
+ }
+
+ /**
+ * Pads `string` on the right side if it's shorter than `length`. Padding
+ * characters are truncated if they exceed `length`.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category String
+ * @param {string} [string=''] The string to pad.
+ * @param {number} [length=0] The padding length.
+ * @param {string} [chars=' '] The string used as padding.
+ * @returns {string} Returns the padded string.
+ * @example
+ *
+ * _.padEnd('abc', 6);
+ * // => 'abc '
+ *
+ * _.padEnd('abc', 6, '_-');
+ * // => 'abc_-_'
+ *
+ * _.padEnd('abc', 3);
+ * // => 'abc'
+ */
+ function padEnd(string, length, chars) {
+ string = toString(string);
+ length = toInteger(length);
+
+ var strLength = length ? stringSize(string) : 0;
+ return (length && strLength < length)
+ ? (string + createPadding(length - strLength, chars))
+ : string;
+ }
+
+ /**
+ * Pads `string` on the left side if it's shorter than `length`. Padding
+ * characters are truncated if they exceed `length`.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category String
+ * @param {string} [string=''] The string to pad.
+ * @param {number} [length=0] The padding length.
+ * @param {string} [chars=' '] The string used as padding.
+ * @returns {string} Returns the padded string.
+ * @example
+ *
+ * _.padStart('abc', 6);
+ * // => ' abc'
+ *
+ * _.padStart('abc', 6, '_-');
+ * // => '_-_abc'
+ *
+ * _.padStart('abc', 3);
+ * // => 'abc'
+ */
+ function padStart(string, length, chars) {
+ string = toString(string);
+ length = toInteger(length);
+
+ var strLength = length ? stringSize(string) : 0;
+ return (length && strLength < length)
+ ? (createPadding(length - strLength, chars) + string)
+ : string;
+ }
+
+ /**
+ * Converts `string` to an integer of the specified radix. If `radix` is
+ * `undefined` or `0`, a `radix` of `10` is used unless `value` is a
+ * hexadecimal, in which case a `radix` of `16` is used.
+ *
+ * **Note:** This method aligns with the
+ * [ES5 implementation](https://es5.github.io/#x15.1.2.2) of `parseInt`.
+ *
+ * @static
+ * @memberOf _
+ * @since 1.1.0
+ * @category String
+ * @param {string} string The string to convert.
+ * @param {number} [radix=10] The radix to interpret `value` by.
+ * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.
+ * @returns {number} Returns the converted integer.
+ * @example
+ *
+ * _.parseInt('08');
+ * // => 8
+ *
+ * _.map(['6', '08', '10'], _.parseInt);
+ * // => [6, 8, 10]
+ */
+ function parseInt(string, radix, guard) {
+ if (guard || radix == null) {
+ radix = 0;
+ } else if (radix) {
+ radix = +radix;
+ }
+ return nativeParseInt(toString(string).replace(reTrimStart, ''), radix || 0);
+ }
+
+ /**
+ * Repeats the given string `n` times.
+ *
+ * @static
+ * @memberOf _
+ * @since 3.0.0
+ * @category String
+ * @param {string} [string=''] The string to repeat.
+ * @param {number} [n=1] The number of times to repeat the string.
+ * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.
+ * @returns {string} Returns the repeated string.
+ * @example
+ *
+ * _.repeat('*', 3);
+ * // => '***'
+ *
+ * _.repeat('abc', 2);
+ * // => 'abcabc'
+ *
+ * _.repeat('abc', 0);
+ * // => ''
+ */
+ function repeat(string, n, guard) {
+ if ((guard ? isIterateeCall(string, n, guard) : n === undefined)) {
+ n = 1;
+ } else {
+ n = toInteger(n);
+ }
+ return baseRepeat(toString(string), n);
+ }
+
+ /**
+ * Replaces matches for `pattern` in `string` with `replacement`.
+ *
+ * **Note:** This method is based on
+ * [`String#replace`](https://mdn.io/String/replace).
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category String
+ * @param {string} [string=''] The string to modify.
+ * @param {RegExp|string} pattern The pattern to replace.
+ * @param {Function|string} replacement The match replacement.
+ * @returns {string} Returns the modified string.
+ * @example
+ *
+ * _.replace('Hi Fred', 'Fred', 'Barney');
+ * // => 'Hi Barney'
+ */
+ function replace() {
+ var args = arguments,
+ string = toString(args[0]);
+
+ return args.length < 3 ? string : string.replace(args[1], args[2]);
+ }
+
+ /**
+ * Converts `string` to
+ * [snake case](https://en.wikipedia.org/wiki/Snake_case).
+ *
+ * @static
+ * @memberOf _
+ * @since 3.0.0
+ * @category String
+ * @param {string} [string=''] The string to convert.
+ * @returns {string} Returns the snake cased string.
+ * @example
+ *
+ * _.snakeCase('Foo Bar');
+ * // => 'foo_bar'
+ *
+ * _.snakeCase('fooBar');
+ * // => 'foo_bar'
+ *
+ * _.snakeCase('--FOO-BAR--');
+ * // => 'foo_bar'
+ */
+ var snakeCase = createCompounder(function(result, word, index) {
+ return result + (index ? '_' : '') + word.toLowerCase();
+ });
+
+ /**
+ * Splits `string` by `separator`.
+ *
+ * **Note:** This method is based on
+ * [`String#split`](https://mdn.io/String/split).
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category String
+ * @param {string} [string=''] The string to split.
+ * @param {RegExp|string} separator The separator pattern to split by.
+ * @param {number} [limit] The length to truncate results to.
+ * @returns {Array} Returns the string segments.
+ * @example
+ *
+ * _.split('a-b-c', '-', 2);
+ * // => ['a', 'b']
+ */
+ function split(string, separator, limit) {
+ if (limit && typeof limit != 'number' && isIterateeCall(string, separator, limit)) {
+ separator = limit = undefined;
+ }
+ limit = limit === undefined ? MAX_ARRAY_LENGTH : limit >>> 0;
+ if (!limit) {
+ return [];
+ }
+ string = toString(string);
+ if (string && (
+ typeof separator == 'string' ||
+ (separator != null && !isRegExp(separator))
+ )) {
+ separator = baseToString(separator);
+ if (!separator && hasUnicode(string)) {
+ return castSlice(stringToArray(string), 0, limit);
+ }
+ }
+ return string.split(separator, limit);
+ }
+
+ /**
+ * Converts `string` to
+ * [start case](https://en.wikipedia.org/wiki/Letter_case#Stylistic_or_specialised_usage).
+ *
+ * @static
+ * @memberOf _
+ * @since 3.1.0
+ * @category String
+ * @param {string} [string=''] The string to convert.
+ * @returns {string} Returns the start cased string.
+ * @example
+ *
+ * _.startCase('--foo-bar--');
+ * // => 'Foo Bar'
+ *
+ * _.startCase('fooBar');
+ * // => 'Foo Bar'
+ *
+ * _.startCase('__FOO_BAR__');
+ * // => 'FOO BAR'
+ */
+ var startCase = createCompounder(function(result, word, index) {
+ return result + (index ? ' ' : '') + upperFirst(word);
+ });
+
+ /**
+ * Checks if `string` starts with the given target string.
+ *
+ * @static
+ * @memberOf _
+ * @since 3.0.0
+ * @category String
+ * @param {string} [string=''] The string to inspect.
+ * @param {string} [target] The string to search for.
+ * @param {number} [position=0] The position to search from.
+ * @returns {boolean} Returns `true` if `string` starts with `target`,
+ * else `false`.
+ * @example
+ *
+ * _.startsWith('abc', 'a');
+ * // => true
+ *
+ * _.startsWith('abc', 'b');
+ * // => false
+ *
+ * _.startsWith('abc', 'b', 1);
+ * // => true
+ */
+ function startsWith(string, target, position) {
+ string = toString(string);
+ position = baseClamp(toInteger(position), 0, string.length);
+ target = baseToString(target);
+ return string.slice(position, position + target.length) == target;
+ }
+
+ /**
+ * Creates a compiled template function that can interpolate data properties
+ * in "interpolate" delimiters, HTML-escape interpolated data properties in
+ * "escape" delimiters, and execute JavaScript in "evaluate" delimiters. Data
+ * properties may be accessed as free variables in the template. If a setting
+ * object is given, it takes precedence over `_.templateSettings` values.
+ *
+ * **Note:** In the development build `_.template` utilizes
+ * [sourceURLs](http://www.html5rocks.com/en/tutorials/developertools/sourcemaps/#toc-sourceurl)
+ * for easier debugging.
+ *
+ * For more information on precompiling templates see
+ * [lodash's custom builds documentation](https://lodash.com/custom-builds).
+ *
+ * For more information on Chrome extension sandboxes see
+ * [Chrome's extensions documentation](https://developer.chrome.com/extensions/sandboxingEval).
+ *
+ * @static
+ * @since 0.1.0
+ * @memberOf _
+ * @category String
+ * @param {string} [string=''] The template string.
+ * @param {Object} [options={}] The options object.
+ * @param {RegExp} [options.escape=_.templateSettings.escape]
+ * The HTML "escape" delimiter.
+ * @param {RegExp} [options.evaluate=_.templateSettings.evaluate]
+ * The "evaluate" delimiter.
+ * @param {Object} [options.imports=_.templateSettings.imports]
+ * An object to import into the template as free variables.
+ * @param {RegExp} [options.interpolate=_.templateSettings.interpolate]
+ * The "interpolate" delimiter.
+ * @param {string} [options.sourceURL='lodash.templateSources[n]']
+ * The sourceURL of the compiled template.
+ * @param {string} [options.variable='obj']
+ * The data object variable name.
+ * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.
+ * @returns {Function} Returns the compiled template function.
+ * @example
+ *
+ * // Use the "interpolate" delimiter to create a compiled template.
+ * var compiled = _.template('hello <%= user %>!');
+ * compiled({ 'user': 'fred' });
+ * // => 'hello fred!'
+ *
+ * // Use the HTML "escape" delimiter to escape data property values.
+ * var compiled = _.template('<b><%- value %></b>');
+ * compiled({ 'value': '<script>' });
+ * // => '<b>&lt;script&gt;</b>'
+ *
+ * // Use the "evaluate" delimiter to execute JavaScript and generate HTML.
+ * var compiled = _.template('<% _.forEach(users, function(user) { %><li><%- user %></li><% }); %>');
+ * compiled({ 'users': ['fred', 'barney'] });
+ * // => '<li>fred</li><li>barney</li>'
+ *
+ * // Use the internal `print` function in "evaluate" delimiters.
+ * var compiled = _.template('<% print("hello " + user); %>!');
+ * compiled({ 'user': 'barney' });
+ * // => 'hello barney!'
+ *
+ * // Use the ES template literal delimiter as an "interpolate" delimiter.
+ * // Disable support by replacing the "interpolate" delimiter.
+ * var compiled = _.template('hello ${ user }!');
+ * compiled({ 'user': 'pebbles' });
+ * // => 'hello pebbles!'
+ *
+ * // Use backslashes to treat delimiters as plain text.
+ * var compiled = _.template('<%= "\\<%- value %\\>" %>');
+ * compiled({ 'value': 'ignored' });
+ * // => '<%- value %>'
+ *
+ * // Use the `imports` option to import `jQuery` as `jq`.
+ * var text = '<% jq.each(users, function(user) { %><li><%- user %></li><% }); %>';
+ * var compiled = _.template(text, { 'imports': { 'jq': jQuery } });
+ * compiled({ 'users': ['fred', 'barney'] });
+ * // => '<li>fred</li><li>barney</li>'
+ *
+ * // Use the `sourceURL` option to specify a custom sourceURL for the template.
+ * var compiled = _.template('hello <%= user %>!', { 'sourceURL': '/basic/greeting.jst' });
+ * compiled(data);
+ * // => Find the source of "greeting.jst" under the Sources tab or Resources panel of the web inspector.
+ *
+ * // Use the `variable` option to ensure a with-statement isn't used in the compiled template.
+ * var compiled = _.template('hi <%= data.user %>!', { 'variable': 'data' });
+ * compiled.source;
+ * // => function(data) {
+ * // var __t, __p = '';
+ * // __p += 'hi ' + ((__t = ( data.user )) == null ? '' : __t) + '!';
+ * // return __p;
+ * // }
+ *
+ * // Use custom template delimiters.
+ * _.templateSettings.interpolate = /{{([\s\S]+?)}}/g;
+ * var compiled = _.template('hello {{ user }}!');
+ * compiled({ 'user': 'mustache' });
+ * // => 'hello mustache!'
+ *
+ * // Use the `source` property to inline compiled templates for meaningful
+ * // line numbers in error messages and stack traces.
+ * fs.writeFileSync(path.join(process.cwd(), 'jst.js'), '\
+ * var JST = {\
+ * "main": ' + _.template(mainText).source + '\
+ * };\
+ * ');
+ */
+ function template(string, options, guard) {
+ // Based on John Resig's `tmpl` implementation
+ // (http://ejohn.org/blog/javascript-micro-templating/)
+ // and Laura Doktorova's doT.js (https://github.com/olado/doT).
+ var settings = lodash.templateSettings;
+
+ if (guard && isIterateeCall(string, options, guard)) {
+ options = undefined;
+ }
+ string = toString(string);
+ options = assignInWith({}, options, settings, assignInDefaults);
+
+ var imports = assignInWith({}, options.imports, settings.imports, assignInDefaults),
+ importsKeys = keys(imports),
+ importsValues = baseValues(imports, importsKeys);
+
+ var isEscaping,
+ isEvaluating,
+ index = 0,
+ interpolate = options.interpolate || reNoMatch,
+ source = "__p += '";
+
+ // Compile the regexp to match each delimiter.
+ var reDelimiters = RegExp(
+ (options.escape || reNoMatch).source + '|' +
+ interpolate.source + '|' +
+ (interpolate === reInterpolate ? reEsTemplate : reNoMatch).source + '|' +
+ (options.evaluate || reNoMatch).source + '|$'
+ , 'g');
+
+ // Use a sourceURL for easier debugging.
+ var sourceURL = '//# sourceURL=' +
+ ('sourceURL' in options
+ ? options.sourceURL
+ : ('lodash.templateSources[' + (++templateCounter) + ']')
+ ) + '\n';
+
+ string.replace(reDelimiters, function(match, escapeValue, interpolateValue, esTemplateValue, evaluateValue, offset) {
+ interpolateValue || (interpolateValue = esTemplateValue);
+
+ // Escape characters that can't be included in string literals.
+ source += string.slice(index, offset).replace(reUnescapedString, escapeStringChar);
+
+ // Replace delimiters with snippets.
+ if (escapeValue) {
+ isEscaping = true;
+ source += "' +\n__e(" + escapeValue + ") +\n'";
+ }
+ if (evaluateValue) {
+ isEvaluating = true;
+ source += "';\n" + evaluateValue + ";\n__p += '";
+ }
+ if (interpolateValue) {
+ source += "' +\n((__t = (" + interpolateValue + ")) == null ? '' : __t) +\n'";
+ }
+ index = offset + match.length;
+
+ // The JS engine embedded in Adobe products needs `match` returned in
+ // order to produce the correct `offset` value.
+ return match;
+ });
+
+ source += "';\n";
+
+ // If `variable` is not specified wrap a with-statement around the generated
+ // code to add the data object to the top of the scope chain.
+ var variable = options.variable;
+ if (!variable) {
+ source = 'with (obj) {\n' + source + '\n}\n';
+ }
+ // Cleanup code by stripping empty strings.
+ source = (isEvaluating ? source.replace(reEmptyStringLeading, '') : source)
+ .replace(reEmptyStringMiddle, '$1')
+ .replace(reEmptyStringTrailing, '$1;');
+
+ // Frame code as the function body.
+ source = 'function(' + (variable || 'obj') + ') {\n' +
+ (variable
+ ? ''
+ : 'obj || (obj = {});\n'
+ ) +
+ "var __t, __p = ''" +
+ (isEscaping
+ ? ', __e = _.escape'
+ : ''
+ ) +
+ (isEvaluating
+ ? ', __j = Array.prototype.join;\n' +
+ "function print() { __p += __j.call(arguments, '') }\n"
+ : ';\n'
+ ) +
+ source +
+ 'return __p\n}';
+
+ var result = attempt(function() {
+ return Function(importsKeys, sourceURL + 'return ' + source)
+ .apply(undefined, importsValues);
+ });
+
+ // Provide the compiled function's source by its `toString` method or
+ // the `source` property as a convenience for inlining compiled templates.
+ result.source = source;
+ if (isError(result)) {
+ throw result;
+ }
+ return result;
+ }
+
+ /**
+ * Converts `string`, as a whole, to lower case just like
+ * [String#toLowerCase](https://mdn.io/toLowerCase).
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category String
+ * @param {string} [string=''] The string to convert.
+ * @returns {string} Returns the lower cased string.
+ * @example
+ *
+ * _.toLower('--Foo-Bar--');
+ * // => '--foo-bar--'
+ *
+ * _.toLower('fooBar');
+ * // => 'foobar'
+ *
+ * _.toLower('__FOO_BAR__');
+ * // => '__foo_bar__'
+ */
+ function toLower(value) {
+ return toString(value).toLowerCase();
+ }
+
+ /**
+ * Converts `string`, as a whole, to upper case just like
+ * [String#toUpperCase](https://mdn.io/toUpperCase).
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category String
+ * @param {string} [string=''] The string to convert.
+ * @returns {string} Returns the upper cased string.
+ * @example
+ *
+ * _.toUpper('--foo-bar--');
+ * // => '--FOO-BAR--'
+ *
+ * _.toUpper('fooBar');
+ * // => 'FOOBAR'
+ *
+ * _.toUpper('__foo_bar__');
+ * // => '__FOO_BAR__'
+ */
+ function toUpper(value) {
+ return toString(value).toUpperCase();
+ }
+
+ /**
+ * Removes leading and trailing whitespace or specified characters from `string`.
+ *
+ * @static
+ * @memberOf _
+ * @since 3.0.0
+ * @category String
+ * @param {string} [string=''] The string to trim.
+ * @param {string} [chars=whitespace] The characters to trim.
+ * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.
+ * @returns {string} Returns the trimmed string.
+ * @example
+ *
+ * _.trim(' abc ');
+ * // => 'abc'
+ *
+ * _.trim('-_-abc-_-', '_-');
+ * // => 'abc'
+ *
+ * _.map([' foo ', ' bar '], _.trim);
+ * // => ['foo', 'bar']
+ */
+ function trim(string, chars, guard) {
+ string = toString(string);
+ if (string && (guard || chars === undefined)) {
+ return string.replace(reTrim, '');
+ }
+ if (!string || !(chars = baseToString(chars))) {
+ return string;
+ }
+ var strSymbols = stringToArray(string),
+ chrSymbols = stringToArray(chars),
+ start = charsStartIndex(strSymbols, chrSymbols),
+ end = charsEndIndex(strSymbols, chrSymbols) + 1;
+
+ return castSlice(strSymbols, start, end).join('');
+ }
+
+ /**
+ * Removes trailing whitespace or specified characters from `string`.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category String
+ * @param {string} [string=''] The string to trim.
+ * @param {string} [chars=whitespace] The characters to trim.
+ * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.
+ * @returns {string} Returns the trimmed string.
+ * @example
+ *
+ * _.trimEnd(' abc ');
+ * // => ' abc'
+ *
+ * _.trimEnd('-_-abc-_-', '_-');
+ * // => '-_-abc'
+ */
+ function trimEnd(string, chars, guard) {
+ string = toString(string);
+ if (string && (guard || chars === undefined)) {
+ return string.replace(reTrimEnd, '');
+ }
+ if (!string || !(chars = baseToString(chars))) {
+ return string;
+ }
+ var strSymbols = stringToArray(string),
+ end = charsEndIndex(strSymbols, stringToArray(chars)) + 1;
+
+ return castSlice(strSymbols, 0, end).join('');
+ }
+
+ /**
+ * Removes leading whitespace or specified characters from `string`.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category String
+ * @param {string} [string=''] The string to trim.
+ * @param {string} [chars=whitespace] The characters to trim.
+ * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.
+ * @returns {string} Returns the trimmed string.
+ * @example
+ *
+ * _.trimStart(' abc ');
+ * // => 'abc '
+ *
+ * _.trimStart('-_-abc-_-', '_-');
+ * // => 'abc-_-'
+ */
+ function trimStart(string, chars, guard) {
+ string = toString(string);
+ if (string && (guard || chars === undefined)) {
+ return string.replace(reTrimStart, '');
+ }
+ if (!string || !(chars = baseToString(chars))) {
+ return string;
+ }
+ var strSymbols = stringToArray(string),
+ start = charsStartIndex(strSymbols, stringToArray(chars));
+
+ return castSlice(strSymbols, start).join('');
+ }
+
+ /**
+ * Truncates `string` if it's longer than the given maximum string length.
+ * The last characters of the truncated string are replaced with the omission
+ * string which defaults to "...".
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category String
+ * @param {string} [string=''] The string to truncate.
+ * @param {Object} [options={}] The options object.
+ * @param {number} [options.length=30] The maximum string length.
+ * @param {string} [options.omission='...'] The string to indicate text is omitted.
+ * @param {RegExp|string} [options.separator] The separator pattern to truncate to.
+ * @returns {string} Returns the truncated string.
+ * @example
+ *
+ * _.truncate('hi-diddly-ho there, neighborino');
+ * // => 'hi-diddly-ho there, neighbo...'
+ *
+ * _.truncate('hi-diddly-ho there, neighborino', {
+ * 'length': 24,
+ * 'separator': ' '
+ * });
+ * // => 'hi-diddly-ho there,...'
+ *
+ * _.truncate('hi-diddly-ho there, neighborino', {
+ * 'length': 24,
+ * 'separator': /,? +/
+ * });
+ * // => 'hi-diddly-ho there...'
+ *
+ * _.truncate('hi-diddly-ho there, neighborino', {
+ * 'omission': ' [...]'
+ * });
+ * // => 'hi-diddly-ho there, neig [...]'
+ */
+ function truncate(string, options) {
+ var length = DEFAULT_TRUNC_LENGTH,
+ omission = DEFAULT_TRUNC_OMISSION;
+
+ if (isObject(options)) {
+ var separator = 'separator' in options ? options.separator : separator;
+ length = 'length' in options ? toInteger(options.length) : length;
+ omission = 'omission' in options ? baseToString(options.omission) : omission;
+ }
+ string = toString(string);
+
+ var strLength = string.length;
+ if (hasUnicode(string)) {
+ var strSymbols = stringToArray(string);
+ strLength = strSymbols.length;
+ }
+ if (length >= strLength) {
+ return string;
+ }
+ var end = length - stringSize(omission);
+ if (end < 1) {
+ return omission;
+ }
+ var result = strSymbols
+ ? castSlice(strSymbols, 0, end).join('')
+ : string.slice(0, end);
+
+ if (separator === undefined) {
+ return result + omission;
+ }
+ if (strSymbols) {
+ end += (result.length - end);
+ }
+ if (isRegExp(separator)) {
+ if (string.slice(end).search(separator)) {
+ var match,
+ substring = result;
+
+ if (!separator.global) {
+ separator = RegExp(separator.source, toString(reFlags.exec(separator)) + 'g');
+ }
+ separator.lastIndex = 0;
+ while ((match = separator.exec(substring))) {
+ var newEnd = match.index;
+ }
+ result = result.slice(0, newEnd === undefined ? end : newEnd);
+ }
+ } else if (string.indexOf(baseToString(separator), end) != end) {
+ var index = result.lastIndexOf(separator);
+ if (index > -1) {
+ result = result.slice(0, index);
+ }
+ }
+ return result + omission;
+ }
+
+ /**
+ * The inverse of `_.escape`; this method converts the HTML entities
+ * `&amp;`, `&lt;`, `&gt;`, `&quot;`, and `&#39;` in `string` to
+ * their corresponding characters.
+ *
+ * **Note:** No other HTML entities are unescaped. To unescape additional
+ * HTML entities use a third-party library like [_he_](https://mths.be/he).
+ *
+ * @static
+ * @memberOf _
+ * @since 0.6.0
+ * @category String
+ * @param {string} [string=''] The string to unescape.
+ * @returns {string} Returns the unescaped string.
+ * @example
+ *
+ * _.unescape('fred, barney, &amp; pebbles');
+ * // => 'fred, barney, & pebbles'
+ */
+ function unescape(string) {
+ string = toString(string);
+ return (string && reHasEscapedHtml.test(string))
+ ? string.replace(reEscapedHtml, unescapeHtmlChar)
+ : string;
+ }
+
+ /**
+ * Converts `string`, as space separated words, to upper case.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category String
+ * @param {string} [string=''] The string to convert.
+ * @returns {string} Returns the upper cased string.
+ * @example
+ *
+ * _.upperCase('--foo-bar');
+ * // => 'FOO BAR'
+ *
+ * _.upperCase('fooBar');
+ * // => 'FOO BAR'
+ *
+ * _.upperCase('__foo_bar__');
+ * // => 'FOO BAR'
+ */
+ var upperCase = createCompounder(function(result, word, index) {
+ return result + (index ? ' ' : '') + word.toUpperCase();
+ });
+
+ /**
+ * Converts the first character of `string` to upper case.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category String
+ * @param {string} [string=''] The string to convert.
+ * @returns {string} Returns the converted string.
+ * @example
+ *
+ * _.upperFirst('fred');
+ * // => 'Fred'
+ *
+ * _.upperFirst('FRED');
+ * // => 'FRED'
+ */
+ var upperFirst = createCaseFirst('toUpperCase');
+
+ /**
+ * Splits `string` into an array of its words.
+ *
+ * @static
+ * @memberOf _
+ * @since 3.0.0
+ * @category String
+ * @param {string} [string=''] The string to inspect.
+ * @param {RegExp|string} [pattern] The pattern to match words.
+ * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.
+ * @returns {Array} Returns the words of `string`.
+ * @example
+ *
+ * _.words('fred, barney, & pebbles');
+ * // => ['fred', 'barney', 'pebbles']
+ *
+ * _.words('fred, barney, & pebbles', /[^, ]+/g);
+ * // => ['fred', 'barney', '&', 'pebbles']
+ */
+ function words(string, pattern, guard) {
+ string = toString(string);
+ pattern = guard ? undefined : pattern;
+
+ if (pattern === undefined) {
+ return hasUnicodeWord(string) ? unicodeWords(string) : asciiWords(string);
+ }
+ return string.match(pattern) || [];
+ }
+
+ /*------------------------------------------------------------------------*/
+
+ /**
+ * Attempts to invoke `func`, returning either the result or the caught error
+ * object. Any additional arguments are provided to `func` when it's invoked.
+ *
+ * @static
+ * @memberOf _
+ * @since 3.0.0
+ * @category Util
+ * @param {Function} func The function to attempt.
+ * @param {...*} [args] The arguments to invoke `func` with.
+ * @returns {*} Returns the `func` result or error object.
+ * @example
+ *
+ * // Avoid throwing errors for invalid selectors.
+ * var elements = _.attempt(function(selector) {
+ * return document.querySelectorAll(selector);
+ * }, '>_>');
+ *
+ * if (_.isError(elements)) {
+ * elements = [];
+ * }
+ */
+ var attempt = baseRest(function(func, args) {
+ try {
+ return apply(func, undefined, args);
+ } catch (e) {
+ return isError(e) ? e : new Error(e);
+ }
+ });
+
+ /**
+ * Binds methods of an object to the object itself, overwriting the existing
+ * method.
+ *
+ * **Note:** This method doesn't set the "length" property of bound functions.
+ *
+ * @static
+ * @since 0.1.0
+ * @memberOf _
+ * @category Util
+ * @param {Object} object The object to bind and assign the bound methods to.
+ * @param {...(string|string[])} methodNames The object method names to bind.
+ * @returns {Object} Returns `object`.
+ * @example
+ *
+ * var view = {
+ * 'label': 'docs',
+ * 'click': function() {
+ * console.log('clicked ' + this.label);
+ * }
+ * };
+ *
+ * _.bindAll(view, ['click']);
+ * jQuery(element).on('click', view.click);
+ * // => Logs 'clicked docs' when clicked.
+ */
+ var bindAll = flatRest(function(object, methodNames) {
+ arrayEach(methodNames, function(key) {
+ key = toKey(key);
+ baseAssignValue(object, key, bind(object[key], object));
+ });
+ return object;
+ });
+
+ /**
+ * Creates a function that iterates over `pairs` and invokes the corresponding
+ * function of the first predicate to return truthy. The predicate-function
+ * pairs are invoked with the `this` binding and arguments of the created
+ * function.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Util
+ * @param {Array} pairs The predicate-function pairs.
+ * @returns {Function} Returns the new composite function.
+ * @example
+ *
+ * var func = _.cond([
+ * [_.matches({ 'a': 1 }), _.constant('matches A')],
+ * [_.conforms({ 'b': _.isNumber }), _.constant('matches B')],
+ * [_.stubTrue, _.constant('no match')]
+ * ]);
+ *
+ * func({ 'a': 1, 'b': 2 });
+ * // => 'matches A'
+ *
+ * func({ 'a': 0, 'b': 1 });
+ * // => 'matches B'
+ *
+ * func({ 'a': '1', 'b': '2' });
+ * // => 'no match'
+ */
+ function cond(pairs) {
+ var length = pairs ? pairs.length : 0,
+ toIteratee = getIteratee();
+
+ pairs = !length ? [] : arrayMap(pairs, function(pair) {
+ if (typeof pair[1] != 'function') {
+ throw new TypeError(FUNC_ERROR_TEXT);
+ }
+ return [toIteratee(pair[0]), pair[1]];
+ });
+
+ return baseRest(function(args) {
+ var index = -1;
+ while (++index < length) {
+ var pair = pairs[index];
+ if (apply(pair[0], this, args)) {
+ return apply(pair[1], this, args);
+ }
+ }
+ });
+ }
+
+ /**
+ * Creates a function that invokes the predicate properties of `source` with
+ * the corresponding property values of a given object, returning `true` if
+ * all predicates return truthy, else `false`.
+ *
+ * **Note:** The created function is equivalent to `_.conformsTo` with
+ * `source` partially applied.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Util
+ * @param {Object} source The object of property predicates to conform to.
+ * @returns {Function} Returns the new spec function.
+ * @example
+ *
+ * var objects = [
+ * { 'a': 2, 'b': 1 },
+ * { 'a': 1, 'b': 2 }
+ * ];
+ *
+ * _.filter(objects, _.conforms({ 'b': function(n) { return n > 1; } }));
+ * // => [{ 'a': 1, 'b': 2 }]
+ */
+ function conforms(source) {
+ return baseConforms(baseClone(source, true));
+ }
+
+ /**
+ * Creates a function that returns `value`.
+ *
+ * @static
+ * @memberOf _
+ * @since 2.4.0
+ * @category Util
+ * @param {*} value The value to return from the new function.
+ * @returns {Function} Returns the new constant function.
+ * @example
+ *
+ * var objects = _.times(2, _.constant({ 'a': 1 }));
+ *
+ * console.log(objects);
+ * // => [{ 'a': 1 }, { 'a': 1 }]
+ *
+ * console.log(objects[0] === objects[1]);
+ * // => true
+ */
+ function constant(value) {
+ return function() {
+ return value;
+ };
+ }
+
+ /**
+ * Checks `value` to determine whether a default value should be returned in
+ * its place. The `defaultValue` is returned if `value` is `NaN`, `null`,
+ * or `undefined`.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.14.0
+ * @category Util
+ * @param {*} value The value to check.
+ * @param {*} defaultValue The default value.
+ * @returns {*} Returns the resolved value.
+ * @example
+ *
+ * _.defaultTo(1, 10);
+ * // => 1
+ *
+ * _.defaultTo(undefined, 10);
+ * // => 10
+ */
+ function defaultTo(value, defaultValue) {
+ return (value == null || value !== value) ? defaultValue : value;
+ }
+
+ /**
+ * Creates a function that returns the result of invoking the given functions
+ * with the `this` binding of the created function, where each successive
+ * invocation is supplied the return value of the previous.
+ *
+ * @static
+ * @memberOf _
+ * @since 3.0.0
+ * @category Util
+ * @param {...(Function|Function[])} [funcs] The functions to invoke.
+ * @returns {Function} Returns the new composite function.
+ * @see _.flowRight
+ * @example
+ *
+ * function square(n) {
+ * return n * n;
+ * }
+ *
+ * var addSquare = _.flow([_.add, square]);
+ * addSquare(1, 2);
+ * // => 9
+ */
+ var flow = createFlow();
+
+ /**
+ * This method is like `_.flow` except that it creates a function that
+ * invokes the given functions from right to left.
+ *
+ * @static
+ * @since 3.0.0
+ * @memberOf _
+ * @category Util
+ * @param {...(Function|Function[])} [funcs] The functions to invoke.
+ * @returns {Function} Returns the new composite function.
+ * @see _.flow
+ * @example
+ *
+ * function square(n) {
+ * return n * n;
+ * }
+ *
+ * var addSquare = _.flowRight([square, _.add]);
+ * addSquare(1, 2);
+ * // => 9
+ */
+ var flowRight = createFlow(true);
+
+ /**
+ * This method returns the first argument it receives.
+ *
+ * @static
+ * @since 0.1.0
+ * @memberOf _
+ * @category Util
+ * @param {*} value Any value.
+ * @returns {*} Returns `value`.
+ * @example
+ *
+ * var object = { 'a': 1 };
+ *
+ * console.log(_.identity(object) === object);
+ * // => true
+ */
+ function identity(value) {
+ return value;
+ }
+
+ /**
+ * Creates a function that invokes `func` with the arguments of the created
+ * function. If `func` is a property name, the created function returns the
+ * property value for a given element. If `func` is an array or object, the
+ * created function returns `true` for elements that contain the equivalent
+ * source properties, otherwise it returns `false`.
+ *
+ * @static
+ * @since 4.0.0
+ * @memberOf _
+ * @category Util
+ * @param {*} [func=_.identity] The value to convert to a callback.
+ * @returns {Function} Returns the callback.
+ * @example
+ *
+ * var users = [
+ * { 'user': 'barney', 'age': 36, 'active': true },
+ * { 'user': 'fred', 'age': 40, 'active': false }
+ * ];
+ *
+ * // The `_.matches` iteratee shorthand.
+ * _.filter(users, _.iteratee({ 'user': 'barney', 'active': true }));
+ * // => [{ 'user': 'barney', 'age': 36, 'active': true }]
+ *
+ * // The `_.matchesProperty` iteratee shorthand.
+ * _.filter(users, _.iteratee(['user', 'fred']));
+ * // => [{ 'user': 'fred', 'age': 40 }]
+ *
+ * // The `_.property` iteratee shorthand.
+ * _.map(users, _.iteratee('user'));
+ * // => ['barney', 'fred']
+ *
+ * // Create custom iteratee shorthands.
+ * _.iteratee = _.wrap(_.iteratee, function(iteratee, func) {
+ * return !_.isRegExp(func) ? iteratee(func) : function(string) {
+ * return func.test(string);
+ * };
+ * });
+ *
+ * _.filter(['abc', 'def'], /ef/);
+ * // => ['def']
+ */
+ function iteratee(func) {
+ return baseIteratee(typeof func == 'function' ? func : baseClone(func, true));
+ }
+
+ /**
+ * Creates a function that performs a partial deep comparison between a given
+ * object and `source`, returning `true` if the given object has equivalent
+ * property values, else `false`.
+ *
+ * **Note:** The created function is equivalent to `_.isMatch` with `source`
+ * partially applied.
+ *
+ * Partial comparisons will match empty array and empty object `source`
+ * values against any array or object value, respectively. See `_.isEqual`
+ * for a list of supported value comparisons.
+ *
+ * @static
+ * @memberOf _
+ * @since 3.0.0
+ * @category Util
+ * @param {Object} source The object of property values to match.
+ * @returns {Function} Returns the new spec function.
+ * @example
+ *
+ * var objects = [
+ * { 'a': 1, 'b': 2, 'c': 3 },
+ * { 'a': 4, 'b': 5, 'c': 6 }
+ * ];
+ *
+ * _.filter(objects, _.matches({ 'a': 4, 'c': 6 }));
+ * // => [{ 'a': 4, 'b': 5, 'c': 6 }]
+ */
+ function matches(source) {
+ return baseMatches(baseClone(source, true));
+ }
+
+ /**
+ * Creates a function that performs a partial deep comparison between the
+ * value at `path` of a given object to `srcValue`, returning `true` if the
+ * object value is equivalent, else `false`.
+ *
+ * **Note:** Partial comparisons will match empty array and empty object
+ * `srcValue` values against any array or object value, respectively. See
+ * `_.isEqual` for a list of supported value comparisons.
+ *
+ * @static
+ * @memberOf _
+ * @since 3.2.0
+ * @category Util
+ * @param {Array|string} path The path of the property to get.
+ * @param {*} srcValue The value to match.
+ * @returns {Function} Returns the new spec function.
+ * @example
+ *
+ * var objects = [
+ * { 'a': 1, 'b': 2, 'c': 3 },
+ * { 'a': 4, 'b': 5, 'c': 6 }
+ * ];
+ *
+ * _.find(objects, _.matchesProperty('a', 4));
+ * // => { 'a': 4, 'b': 5, 'c': 6 }
+ */
+ function matchesProperty(path, srcValue) {
+ return baseMatchesProperty(path, baseClone(srcValue, true));
+ }
+
+ /**
+ * Creates a function that invokes the method at `path` of a given object.
+ * Any additional arguments are provided to the invoked method.
+ *
+ * @static
+ * @memberOf _
+ * @since 3.7.0
+ * @category Util
+ * @param {Array|string} path The path of the method to invoke.
+ * @param {...*} [args] The arguments to invoke the method with.
+ * @returns {Function} Returns the new invoker function.
+ * @example
+ *
+ * var objects = [
+ * { 'a': { 'b': _.constant(2) } },
+ * { 'a': { 'b': _.constant(1) } }
+ * ];
+ *
+ * _.map(objects, _.method('a.b'));
+ * // => [2, 1]
+ *
+ * _.map(objects, _.method(['a', 'b']));
+ * // => [2, 1]
+ */
+ var method = baseRest(function(path, args) {
+ return function(object) {
+ return baseInvoke(object, path, args);
+ };
+ });
+
+ /**
+ * The opposite of `_.method`; this method creates a function that invokes
+ * the method at a given path of `object`. Any additional arguments are
+ * provided to the invoked method.
+ *
+ * @static
+ * @memberOf _
+ * @since 3.7.0
+ * @category Util
+ * @param {Object} object The object to query.
+ * @param {...*} [args] The arguments to invoke the method with.
+ * @returns {Function} Returns the new invoker function.
+ * @example
+ *
+ * var array = _.times(3, _.constant),
+ * object = { 'a': array, 'b': array, 'c': array };
+ *
+ * _.map(['a[2]', 'c[0]'], _.methodOf(object));
+ * // => [2, 0]
+ *
+ * _.map([['a', '2'], ['c', '0']], _.methodOf(object));
+ * // => [2, 0]
+ */
+ var methodOf = baseRest(function(object, args) {
+ return function(path) {
+ return baseInvoke(object, path, args);
+ };
+ });
+
+ /**
+ * Adds all own enumerable string keyed function properties of a source
+ * object to the destination object. If `object` is a function, then methods
+ * are added to its prototype as well.
+ *
+ * **Note:** Use `_.runInContext` to create a pristine `lodash` function to
+ * avoid conflicts caused by modifying the original.
+ *
+ * @static
+ * @since 0.1.0
+ * @memberOf _
+ * @category Util
+ * @param {Function|Object} [object=lodash] The destination object.
+ * @param {Object} source The object of functions to add.
+ * @param {Object} [options={}] The options object.
+ * @param {boolean} [options.chain=true] Specify whether mixins are chainable.
+ * @returns {Function|Object} Returns `object`.
+ * @example
+ *
+ * function vowels(string) {
+ * return _.filter(string, function(v) {
+ * return /[aeiou]/i.test(v);
+ * });
+ * }
+ *
+ * _.mixin({ 'vowels': vowels });
+ * _.vowels('fred');
+ * // => ['e']
+ *
+ * _('fred').vowels().value();
+ * // => ['e']
+ *
+ * _.mixin({ 'vowels': vowels }, { 'chain': false });
+ * _('fred').vowels();
+ * // => ['e']
+ */
+ function mixin(object, source, options) {
+ var props = keys(source),
+ methodNames = baseFunctions(source, props);
+
+ if (options == null &&
+ !(isObject(source) && (methodNames.length || !props.length))) {
+ options = source;
+ source = object;
+ object = this;
+ methodNames = baseFunctions(source, keys(source));
+ }
+ var chain = !(isObject(options) && 'chain' in options) || !!options.chain,
+ isFunc = isFunction(object);
+
+ arrayEach(methodNames, function(methodName) {
+ var func = source[methodName];
+ object[methodName] = func;
+ if (isFunc) {
+ object.prototype[methodName] = function() {
+ var chainAll = this.__chain__;
+ if (chain || chainAll) {
+ var result = object(this.__wrapped__),
+ actions = result.__actions__ = copyArray(this.__actions__);
+
+ actions.push({ 'func': func, 'args': arguments, 'thisArg': object });
+ result.__chain__ = chainAll;
+ return result;
+ }
+ return func.apply(object, arrayPush([this.value()], arguments));
+ };
+ }
+ });
+
+ return object;
+ }
+
+ /**
+ * Reverts the `_` variable to its previous value and returns a reference to
+ * the `lodash` function.
+ *
+ * @static
+ * @since 0.1.0
+ * @memberOf _
+ * @category Util
+ * @returns {Function} Returns the `lodash` function.
+ * @example
+ *
+ * var lodash = _.noConflict();
+ */
+ function noConflict() {
+ if (root._ === this) {
+ root._ = oldDash;
+ }
+ return this;
+ }
+
+ /**
+ * This method returns `undefined`.
+ *
+ * @static
+ * @memberOf _
+ * @since 2.3.0
+ * @category Util
+ * @example
+ *
+ * _.times(2, _.noop);
+ * // => [undefined, undefined]
+ */
+ function noop() {
+ // No operation performed.
+ }
+
+ /**
+ * Creates a function that gets the argument at index `n`. If `n` is negative,
+ * the nth argument from the end is returned.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Util
+ * @param {number} [n=0] The index of the argument to return.
+ * @returns {Function} Returns the new pass-thru function.
+ * @example
+ *
+ * var func = _.nthArg(1);
+ * func('a', 'b', 'c', 'd');
+ * // => 'b'
+ *
+ * var func = _.nthArg(-2);
+ * func('a', 'b', 'c', 'd');
+ * // => 'c'
+ */
+ function nthArg(n) {
+ n = toInteger(n);
+ return baseRest(function(args) {
+ return baseNth(args, n);
+ });
+ }
+
+ /**
+ * Creates a function that invokes `iteratees` with the arguments it receives
+ * and returns their results.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Util
+ * @param {...(Function|Function[])} [iteratees=[_.identity]]
+ * The iteratees to invoke.
+ * @returns {Function} Returns the new function.
+ * @example
+ *
+ * var func = _.over([Math.max, Math.min]);
+ *
+ * func(1, 2, 3, 4);
+ * // => [4, 1]
+ */
+ var over = createOver(arrayMap);
+
+ /**
+ * Creates a function that checks if **all** of the `predicates` return
+ * truthy when invoked with the arguments it receives.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Util
+ * @param {...(Function|Function[])} [predicates=[_.identity]]
+ * The predicates to check.
+ * @returns {Function} Returns the new function.
+ * @example
+ *
+ * var func = _.overEvery([Boolean, isFinite]);
+ *
+ * func('1');
+ * // => true
+ *
+ * func(null);
+ * // => false
+ *
+ * func(NaN);
+ * // => false
+ */
+ var overEvery = createOver(arrayEvery);
+
+ /**
+ * Creates a function that checks if **any** of the `predicates` return
+ * truthy when invoked with the arguments it receives.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Util
+ * @param {...(Function|Function[])} [predicates=[_.identity]]
+ * The predicates to check.
+ * @returns {Function} Returns the new function.
+ * @example
+ *
+ * var func = _.overSome([Boolean, isFinite]);
+ *
+ * func('1');
+ * // => true
+ *
+ * func(null);
+ * // => true
+ *
+ * func(NaN);
+ * // => false
+ */
+ var overSome = createOver(arraySome);
+
+ /**
+ * Creates a function that returns the value at `path` of a given object.
+ *
+ * @static
+ * @memberOf _
+ * @since 2.4.0
+ * @category Util
+ * @param {Array|string} path The path of the property to get.
+ * @returns {Function} Returns the new accessor function.
+ * @example
+ *
+ * var objects = [
+ * { 'a': { 'b': 2 } },
+ * { 'a': { 'b': 1 } }
+ * ];
+ *
+ * _.map(objects, _.property('a.b'));
+ * // => [2, 1]
+ *
+ * _.map(_.sortBy(objects, _.property(['a', 'b'])), 'a.b');
+ * // => [1, 2]
+ */
+ function property(path) {
+ return isKey(path) ? baseProperty(toKey(path)) : basePropertyDeep(path);
+ }
+
+ /**
+ * The opposite of `_.property`; this method creates a function that returns
+ * the value at a given path of `object`.
+ *
+ * @static
+ * @memberOf _
+ * @since 3.0.0
+ * @category Util
+ * @param {Object} object The object to query.
+ * @returns {Function} Returns the new accessor function.
+ * @example
+ *
+ * var array = [0, 1, 2],
+ * object = { 'a': array, 'b': array, 'c': array };
+ *
+ * _.map(['a[2]', 'c[0]'], _.propertyOf(object));
+ * // => [2, 0]
+ *
+ * _.map([['a', '2'], ['c', '0']], _.propertyOf(object));
+ * // => [2, 0]
+ */
+ function propertyOf(object) {
+ return function(path) {
+ return object == null ? undefined : baseGet(object, path);
+ };
+ }
+
+ /**
+ * Creates an array of numbers (positive and/or negative) progressing from
+ * `start` up to, but not including, `end`. A step of `-1` is used if a negative
+ * `start` is specified without an `end` or `step`. If `end` is not specified,
+ * it's set to `start` with `start` then set to `0`.
+ *
+ * **Note:** JavaScript follows the IEEE-754 standard for resolving
+ * floating-point values which can produce unexpected results.
+ *
+ * @static
+ * @since 0.1.0
+ * @memberOf _
+ * @category Util
+ * @param {number} [start=0] The start of the range.
+ * @param {number} end The end of the range.
+ * @param {number} [step=1] The value to increment or decrement by.
+ * @returns {Array} Returns the range of numbers.
+ * @see _.inRange, _.rangeRight
+ * @example
+ *
+ * _.range(4);
+ * // => [0, 1, 2, 3]
+ *
+ * _.range(-4);
+ * // => [0, -1, -2, -3]
+ *
+ * _.range(1, 5);
+ * // => [1, 2, 3, 4]
+ *
+ * _.range(0, 20, 5);
+ * // => [0, 5, 10, 15]
+ *
+ * _.range(0, -4, -1);
+ * // => [0, -1, -2, -3]
+ *
+ * _.range(1, 4, 0);
+ * // => [1, 1, 1]
+ *
+ * _.range(0);
+ * // => []
+ */
+ var range = createRange();
+
+ /**
+ * This method is like `_.range` except that it populates values in
+ * descending order.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Util
+ * @param {number} [start=0] The start of the range.
+ * @param {number} end The end of the range.
+ * @param {number} [step=1] The value to increment or decrement by.
+ * @returns {Array} Returns the range of numbers.
+ * @see _.inRange, _.range
+ * @example
+ *
+ * _.rangeRight(4);
+ * // => [3, 2, 1, 0]
+ *
+ * _.rangeRight(-4);
+ * // => [-3, -2, -1, 0]
+ *
+ * _.rangeRight(1, 5);
+ * // => [4, 3, 2, 1]
+ *
+ * _.rangeRight(0, 20, 5);
+ * // => [15, 10, 5, 0]
+ *
+ * _.rangeRight(0, -4, -1);
+ * // => [-3, -2, -1, 0]
+ *
+ * _.rangeRight(1, 4, 0);
+ * // => [1, 1, 1]
+ *
+ * _.rangeRight(0);
+ * // => []
+ */
+ var rangeRight = createRange(true);
+
+ /**
+ * This method returns a new empty array.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.13.0
+ * @category Util
+ * @returns {Array} Returns the new empty array.
+ * @example
+ *
+ * var arrays = _.times(2, _.stubArray);
+ *
+ * console.log(arrays);
+ * // => [[], []]
+ *
+ * console.log(arrays[0] === arrays[1]);
+ * // => false
+ */
+ function stubArray() {
+ return [];
+ }
+
+ /**
+ * This method returns `false`.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.13.0
+ * @category Util
+ * @returns {boolean} Returns `false`.
+ * @example
+ *
+ * _.times(2, _.stubFalse);
+ * // => [false, false]
+ */
+ function stubFalse() {
+ return false;
+ }
+
+ /**
+ * This method returns a new empty object.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.13.0
+ * @category Util
+ * @returns {Object} Returns the new empty object.
+ * @example
+ *
+ * var objects = _.times(2, _.stubObject);
+ *
+ * console.log(objects);
+ * // => [{}, {}]
+ *
+ * console.log(objects[0] === objects[1]);
+ * // => false
+ */
+ function stubObject() {
+ return {};
+ }
+
+ /**
+ * This method returns an empty string.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.13.0
+ * @category Util
+ * @returns {string} Returns the empty string.
+ * @example
+ *
+ * _.times(2, _.stubString);
+ * // => ['', '']
+ */
+ function stubString() {
+ return '';
+ }
+
+ /**
+ * This method returns `true`.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.13.0
+ * @category Util
+ * @returns {boolean} Returns `true`.
+ * @example
+ *
+ * _.times(2, _.stubTrue);
+ * // => [true, true]
+ */
+ function stubTrue() {
+ return true;
+ }
+
+ /**
+ * Invokes the iteratee `n` times, returning an array of the results of
+ * each invocation. The iteratee is invoked with one argument; (index).
+ *
+ * @static
+ * @since 0.1.0
+ * @memberOf _
+ * @category Util
+ * @param {number} n The number of times to invoke `iteratee`.
+ * @param {Function} [iteratee=_.identity] The function invoked per iteration.
+ * @returns {Array} Returns the array of results.
+ * @example
+ *
+ * _.times(3, String);
+ * // => ['0', '1', '2']
+ *
+ * _.times(4, _.constant(0));
+ * // => [0, 0, 0, 0]
+ */
+ function times(n, iteratee) {
+ n = toInteger(n);
+ if (n < 1 || n > MAX_SAFE_INTEGER) {
+ return [];
+ }
+ var index = MAX_ARRAY_LENGTH,
+ length = nativeMin(n, MAX_ARRAY_LENGTH);
+
+ iteratee = getIteratee(iteratee);
+ n -= MAX_ARRAY_LENGTH;
+
+ var result = baseTimes(length, iteratee);
+ while (++index < n) {
+ iteratee(index);
+ }
+ return result;
+ }
+
+ /**
+ * Converts `value` to a property path array.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Util
+ * @param {*} value The value to convert.
+ * @returns {Array} Returns the new property path array.
+ * @example
+ *
+ * _.toPath('a.b.c');
+ * // => ['a', 'b', 'c']
+ *
+ * _.toPath('a[0].b.c');
+ * // => ['a', '0', 'b', 'c']
+ */
+ function toPath(value) {
+ if (isArray(value)) {
+ return arrayMap(value, toKey);
+ }
+ return isSymbol(value) ? [value] : copyArray(stringToPath(value));
+ }
+
+ /**
+ * Generates a unique ID. If `prefix` is given, the ID is appended to it.
+ *
+ * @static
+ * @since 0.1.0
+ * @memberOf _
+ * @category Util
+ * @param {string} [prefix=''] The value to prefix the ID with.
+ * @returns {string} Returns the unique ID.
+ * @example
+ *
+ * _.uniqueId('contact_');
+ * // => 'contact_104'
+ *
+ * _.uniqueId();
+ * // => '105'
+ */
+ function uniqueId(prefix) {
+ var id = ++idCounter;
+ return toString(prefix) + id;
+ }
+
+ /*------------------------------------------------------------------------*/
+
+ /**
+ * Adds two numbers.
+ *
+ * @static
+ * @memberOf _
+ * @since 3.4.0
+ * @category Math
+ * @param {number} augend The first number in an addition.
+ * @param {number} addend The second number in an addition.
+ * @returns {number} Returns the total.
+ * @example
+ *
+ * _.add(6, 4);
+ * // => 10
+ */
+ var add = createMathOperation(function(augend, addend) {
+ return augend + addend;
+ }, 0);
+
+ /**
+ * Computes `number` rounded up to `precision`.
+ *
+ * @static
+ * @memberOf _
+ * @since 3.10.0
+ * @category Math
+ * @param {number} number The number to round up.
+ * @param {number} [precision=0] The precision to round up to.
+ * @returns {number} Returns the rounded up number.
+ * @example
+ *
+ * _.ceil(4.006);
+ * // => 5
+ *
+ * _.ceil(6.004, 2);
+ * // => 6.01
+ *
+ * _.ceil(6040, -2);
+ * // => 6100
+ */
+ var ceil = createRound('ceil');
+
+ /**
+ * Divide two numbers.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.7.0
+ * @category Math
+ * @param {number} dividend The first number in a division.
+ * @param {number} divisor The second number in a division.
+ * @returns {number} Returns the quotient.
+ * @example
+ *
+ * _.divide(6, 4);
+ * // => 1.5
+ */
+ var divide = createMathOperation(function(dividend, divisor) {
+ return dividend / divisor;
+ }, 1);
+
+ /**
+ * Computes `number` rounded down to `precision`.
+ *
+ * @static
+ * @memberOf _
+ * @since 3.10.0
+ * @category Math
+ * @param {number} number The number to round down.
+ * @param {number} [precision=0] The precision to round down to.
+ * @returns {number} Returns the rounded down number.
+ * @example
+ *
+ * _.floor(4.006);
+ * // => 4
+ *
+ * _.floor(0.046, 2);
+ * // => 0.04
+ *
+ * _.floor(4060, -2);
+ * // => 4000
+ */
+ var floor = createRound('floor');
+
+ /**
+ * Computes the maximum value of `array`. If `array` is empty or falsey,
+ * `undefined` is returned.
+ *
+ * @static
+ * @since 0.1.0
+ * @memberOf _
+ * @category Math
+ * @param {Array} array The array to iterate over.
+ * @returns {*} Returns the maximum value.
+ * @example
+ *
+ * _.max([4, 2, 8, 6]);
+ * // => 8
+ *
+ * _.max([]);
+ * // => undefined
+ */
+ function max(array) {
+ return (array && array.length)
+ ? baseExtremum(array, identity, baseGt)
+ : undefined;
+ }
+
+ /**
+ * This method is like `_.max` except that it accepts `iteratee` which is
+ * invoked for each element in `array` to generate the criterion by which
+ * the value is ranked. The iteratee is invoked with one argument: (value).
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Math
+ * @param {Array} array The array to iterate over.
+ * @param {Function} [iteratee=_.identity] The iteratee invoked per element.
+ * @returns {*} Returns the maximum value.
+ * @example
+ *
+ * var objects = [{ 'n': 1 }, { 'n': 2 }];
+ *
+ * _.maxBy(objects, function(o) { return o.n; });
+ * // => { 'n': 2 }
+ *
+ * // The `_.property` iteratee shorthand.
+ * _.maxBy(objects, 'n');
+ * // => { 'n': 2 }
+ */
+ function maxBy(array, iteratee) {
+ return (array && array.length)
+ ? baseExtremum(array, getIteratee(iteratee, 2), baseGt)
+ : undefined;
+ }
+
+ /**
+ * Computes the mean of the values in `array`.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Math
+ * @param {Array} array The array to iterate over.
+ * @returns {number} Returns the mean.
+ * @example
+ *
+ * _.mean([4, 2, 8, 6]);
+ * // => 5
+ */
+ function mean(array) {
+ return baseMean(array, identity);
+ }
+
+ /**
+ * This method is like `_.mean` except that it accepts `iteratee` which is
+ * invoked for each element in `array` to generate the value to be averaged.
+ * The iteratee is invoked with one argument: (value).
+ *
+ * @static
+ * @memberOf _
+ * @since 4.7.0
+ * @category Math
+ * @param {Array} array The array to iterate over.
+ * @param {Function} [iteratee=_.identity] The iteratee invoked per element.
+ * @returns {number} Returns the mean.
+ * @example
+ *
+ * var objects = [{ 'n': 4 }, { 'n': 2 }, { 'n': 8 }, { 'n': 6 }];
+ *
+ * _.meanBy(objects, function(o) { return o.n; });
+ * // => 5
+ *
+ * // The `_.property` iteratee shorthand.
+ * _.meanBy(objects, 'n');
+ * // => 5
+ */
+ function meanBy(array, iteratee) {
+ return baseMean(array, getIteratee(iteratee, 2));
+ }
+
+ /**
+ * Computes the minimum value of `array`. If `array` is empty or falsey,
+ * `undefined` is returned.
+ *
+ * @static
+ * @since 0.1.0
+ * @memberOf _
+ * @category Math
+ * @param {Array} array The array to iterate over.
+ * @returns {*} Returns the minimum value.
+ * @example
+ *
+ * _.min([4, 2, 8, 6]);
+ * // => 2
+ *
+ * _.min([]);
+ * // => undefined
+ */
+ function min(array) {
+ return (array && array.length)
+ ? baseExtremum(array, identity, baseLt)
+ : undefined;
+ }
+
+ /**
+ * This method is like `_.min` except that it accepts `iteratee` which is
+ * invoked for each element in `array` to generate the criterion by which
+ * the value is ranked. The iteratee is invoked with one argument: (value).
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Math
+ * @param {Array} array The array to iterate over.
+ * @param {Function} [iteratee=_.identity] The iteratee invoked per element.
+ * @returns {*} Returns the minimum value.
+ * @example
+ *
+ * var objects = [{ 'n': 1 }, { 'n': 2 }];
+ *
+ * _.minBy(objects, function(o) { return o.n; });
+ * // => { 'n': 1 }
+ *
+ * // The `_.property` iteratee shorthand.
+ * _.minBy(objects, 'n');
+ * // => { 'n': 1 }
+ */
+ function minBy(array, iteratee) {
+ return (array && array.length)
+ ? baseExtremum(array, getIteratee(iteratee, 2), baseLt)
+ : undefined;
+ }
+
+ /**
+ * Multiply two numbers.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.7.0
+ * @category Math
+ * @param {number} multiplier The first number in a multiplication.
+ * @param {number} multiplicand The second number in a multiplication.
+ * @returns {number} Returns the product.
+ * @example
+ *
+ * _.multiply(6, 4);
+ * // => 24
+ */
+ var multiply = createMathOperation(function(multiplier, multiplicand) {
+ return multiplier * multiplicand;
+ }, 1);
+
+ /**
+ * Computes `number` rounded to `precision`.
+ *
+ * @static
+ * @memberOf _
+ * @since 3.10.0
+ * @category Math
+ * @param {number} number The number to round.
+ * @param {number} [precision=0] The precision to round to.
+ * @returns {number} Returns the rounded number.
+ * @example
+ *
+ * _.round(4.006);
+ * // => 4
+ *
+ * _.round(4.006, 2);
+ * // => 4.01
+ *
+ * _.round(4060, -2);
+ * // => 4100
+ */
+ var round = createRound('round');
+
+ /**
+ * Subtract two numbers.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Math
+ * @param {number} minuend The first number in a subtraction.
+ * @param {number} subtrahend The second number in a subtraction.
+ * @returns {number} Returns the difference.
+ * @example
+ *
+ * _.subtract(6, 4);
+ * // => 2
+ */
+ var subtract = createMathOperation(function(minuend, subtrahend) {
+ return minuend - subtrahend;
+ }, 0);
+
+ /**
+ * Computes the sum of the values in `array`.
+ *
+ * @static
+ * @memberOf _
+ * @since 3.4.0
+ * @category Math
+ * @param {Array} array The array to iterate over.
+ * @returns {number} Returns the sum.
+ * @example
+ *
+ * _.sum([4, 2, 8, 6]);
+ * // => 20
+ */
+ function sum(array) {
+ return (array && array.length)
+ ? baseSum(array, identity)
+ : 0;
+ }
+
+ /**
+ * This method is like `_.sum` except that it accepts `iteratee` which is
+ * invoked for each element in `array` to generate the value to be summed.
+ * The iteratee is invoked with one argument: (value).
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Math
+ * @param {Array} array The array to iterate over.
+ * @param {Function} [iteratee=_.identity] The iteratee invoked per element.
+ * @returns {number} Returns the sum.
+ * @example
+ *
+ * var objects = [{ 'n': 4 }, { 'n': 2 }, { 'n': 8 }, { 'n': 6 }];
+ *
+ * _.sumBy(objects, function(o) { return o.n; });
+ * // => 20
+ *
+ * // The `_.property` iteratee shorthand.
+ * _.sumBy(objects, 'n');
+ * // => 20
+ */
+ function sumBy(array, iteratee) {
+ return (array && array.length)
+ ? baseSum(array, getIteratee(iteratee, 2))
+ : 0;
+ }
+
+ /*------------------------------------------------------------------------*/
+
+ // Add methods that return wrapped values in chain sequences.
+ lodash.after = after;
+ lodash.ary = ary;
+ lodash.assign = assign;
+ lodash.assignIn = assignIn;
+ lodash.assignInWith = assignInWith;
+ lodash.assignWith = assignWith;
+ lodash.at = at;
+ lodash.before = before;
+ lodash.bind = bind;
+ lodash.bindAll = bindAll;
+ lodash.bindKey = bindKey;
+ lodash.castArray = castArray;
+ lodash.chain = chain;
+ lodash.chunk = chunk;
+ lodash.compact = compact;
+ lodash.concat = concat;
+ lodash.cond = cond;
+ lodash.conforms = conforms;
+ lodash.constant = constant;
+ lodash.countBy = countBy;
+ lodash.create = create;
+ lodash.curry = curry;
+ lodash.curryRight = curryRight;
+ lodash.debounce = debounce;
+ lodash.defaults = defaults;
+ lodash.defaultsDeep = defaultsDeep;
+ lodash.defer = defer;
+ lodash.delay = delay;
+ lodash.difference = difference;
+ lodash.differenceBy = differenceBy;
+ lodash.differenceWith = differenceWith;
+ lodash.drop = drop;
+ lodash.dropRight = dropRight;
+ lodash.dropRightWhile = dropRightWhile;
+ lodash.dropWhile = dropWhile;
+ lodash.fill = fill;
+ lodash.filter = filter;
+ lodash.flatMap = flatMap;
+ lodash.flatMapDeep = flatMapDeep;
+ lodash.flatMapDepth = flatMapDepth;
+ lodash.flatten = flatten;
+ lodash.flattenDeep = flattenDeep;
+ lodash.flattenDepth = flattenDepth;
+ lodash.flip = flip;
+ lodash.flow = flow;
+ lodash.flowRight = flowRight;
+ lodash.fromPairs = fromPairs;
+ lodash.functions = functions;
+ lodash.functionsIn = functionsIn;
+ lodash.groupBy = groupBy;
+ lodash.initial = initial;
+ lodash.intersection = intersection;
+ lodash.intersectionBy = intersectionBy;
+ lodash.intersectionWith = intersectionWith;
+ lodash.invert = invert;
+ lodash.invertBy = invertBy;
+ lodash.invokeMap = invokeMap;
+ lodash.iteratee = iteratee;
+ lodash.keyBy = keyBy;
+ lodash.keys = keys;
+ lodash.keysIn = keysIn;
+ lodash.map = map;
+ lodash.mapKeys = mapKeys;
+ lodash.mapValues = mapValues;
+ lodash.matches = matches;
+ lodash.matchesProperty = matchesProperty;
+ lodash.memoize = memoize;
+ lodash.merge = merge;
+ lodash.mergeWith = mergeWith;
+ lodash.method = method;
+ lodash.methodOf = methodOf;
+ lodash.mixin = mixin;
+ lodash.negate = negate;
+ lodash.nthArg = nthArg;
+ lodash.omit = omit;
+ lodash.omitBy = omitBy;
+ lodash.once = once;
+ lodash.orderBy = orderBy;
+ lodash.over = over;
+ lodash.overArgs = overArgs;
+ lodash.overEvery = overEvery;
+ lodash.overSome = overSome;
+ lodash.partial = partial;
+ lodash.partialRight = partialRight;
+ lodash.partition = partition;
+ lodash.pick = pick;
+ lodash.pickBy = pickBy;
+ lodash.property = property;
+ lodash.propertyOf = propertyOf;
+ lodash.pull = pull;
+ lodash.pullAll = pullAll;
+ lodash.pullAllBy = pullAllBy;
+ lodash.pullAllWith = pullAllWith;
+ lodash.pullAt = pullAt;
+ lodash.range = range;
+ lodash.rangeRight = rangeRight;
+ lodash.rearg = rearg;
+ lodash.reject = reject;
+ lodash.remove = remove;
+ lodash.rest = rest;
+ lodash.reverse = reverse;
+ lodash.sampleSize = sampleSize;
+ lodash.set = set;
+ lodash.setWith = setWith;
+ lodash.shuffle = shuffle;
+ lodash.slice = slice;
+ lodash.sortBy = sortBy;
+ lodash.sortedUniq = sortedUniq;
+ lodash.sortedUniqBy = sortedUniqBy;
+ lodash.split = split;
+ lodash.spread = spread;
+ lodash.tail = tail;
+ lodash.take = take;
+ lodash.takeRight = takeRight;
+ lodash.takeRightWhile = takeRightWhile;
+ lodash.takeWhile = takeWhile;
+ lodash.tap = tap;
+ lodash.throttle = throttle;
+ lodash.thru = thru;
+ lodash.toArray = toArray;
+ lodash.toPairs = toPairs;
+ lodash.toPairsIn = toPairsIn;
+ lodash.toPath = toPath;
+ lodash.toPlainObject = toPlainObject;
+ lodash.transform = transform;
+ lodash.unary = unary;
+ lodash.union = union;
+ lodash.unionBy = unionBy;
+ lodash.unionWith = unionWith;
+ lodash.uniq = uniq;
+ lodash.uniqBy = uniqBy;
+ lodash.uniqWith = uniqWith;
+ lodash.unset = unset;
+ lodash.unzip = unzip;
+ lodash.unzipWith = unzipWith;
+ lodash.update = update;
+ lodash.updateWith = updateWith;
+ lodash.values = values;
+ lodash.valuesIn = valuesIn;
+ lodash.without = without;
+ lodash.words = words;
+ lodash.wrap = wrap;
+ lodash.xor = xor;
+ lodash.xorBy = xorBy;
+ lodash.xorWith = xorWith;
+ lodash.zip = zip;
+ lodash.zipObject = zipObject;
+ lodash.zipObjectDeep = zipObjectDeep;
+ lodash.zipWith = zipWith;
+
+ // Add aliases.
+ lodash.entries = toPairs;
+ lodash.entriesIn = toPairsIn;
+ lodash.extend = assignIn;
+ lodash.extendWith = assignInWith;
+
+ // Add methods to `lodash.prototype`.
+ mixin(lodash, lodash);
+
+ /*------------------------------------------------------------------------*/
+
+ // Add methods that return unwrapped values in chain sequences.
+ lodash.add = add;
+ lodash.attempt = attempt;
+ lodash.camelCase = camelCase;
+ lodash.capitalize = capitalize;
+ lodash.ceil = ceil;
+ lodash.clamp = clamp;
+ lodash.clone = clone;
+ lodash.cloneDeep = cloneDeep;
+ lodash.cloneDeepWith = cloneDeepWith;
+ lodash.cloneWith = cloneWith;
+ lodash.conformsTo = conformsTo;
+ lodash.deburr = deburr;
+ lodash.defaultTo = defaultTo;
+ lodash.divide = divide;
+ lodash.endsWith = endsWith;
+ lodash.eq = eq;
+ lodash.escape = escape;
+ lodash.escapeRegExp = escapeRegExp;
+ lodash.every = every;
+ lodash.find = find;
+ lodash.findIndex = findIndex;
+ lodash.findKey = findKey;
+ lodash.findLast = findLast;
+ lodash.findLastIndex = findLastIndex;
+ lodash.findLastKey = findLastKey;
+ lodash.floor = floor;
+ lodash.forEach = forEach;
+ lodash.forEachRight = forEachRight;
+ lodash.forIn = forIn;
+ lodash.forInRight = forInRight;
+ lodash.forOwn = forOwn;
+ lodash.forOwnRight = forOwnRight;
+ lodash.get = get;
+ lodash.gt = gt;
+ lodash.gte = gte;
+ lodash.has = has;
+ lodash.hasIn = hasIn;
+ lodash.head = head;
+ lodash.identity = identity;
+ lodash.includes = includes;
+ lodash.indexOf = indexOf;
+ lodash.inRange = inRange;
+ lodash.invoke = invoke;
+ lodash.isArguments = isArguments;
+ lodash.isArray = isArray;
+ lodash.isArrayBuffer = isArrayBuffer;
+ lodash.isArrayLike = isArrayLike;
+ lodash.isArrayLikeObject = isArrayLikeObject;
+ lodash.isBoolean = isBoolean;
+ lodash.isBuffer = isBuffer;
+ lodash.isDate = isDate;
+ lodash.isElement = isElement;
+ lodash.isEmpty = isEmpty;
+ lodash.isEqual = isEqual;
+ lodash.isEqualWith = isEqualWith;
+ lodash.isError = isError;
+ lodash.isFinite = isFinite;
+ lodash.isFunction = isFunction;
+ lodash.isInteger = isInteger;
+ lodash.isLength = isLength;
+ lodash.isMap = isMap;
+ lodash.isMatch = isMatch;
+ lodash.isMatchWith = isMatchWith;
+ lodash.isNaN = isNaN;
+ lodash.isNative = isNative;
+ lodash.isNil = isNil;
+ lodash.isNull = isNull;
+ lodash.isNumber = isNumber;
+ lodash.isObject = isObject;
+ lodash.isObjectLike = isObjectLike;
+ lodash.isPlainObject = isPlainObject;
+ lodash.isRegExp = isRegExp;
+ lodash.isSafeInteger = isSafeInteger;
+ lodash.isSet = isSet;
+ lodash.isString = isString;
+ lodash.isSymbol = isSymbol;
+ lodash.isTypedArray = isTypedArray;
+ lodash.isUndefined = isUndefined;
+ lodash.isWeakMap = isWeakMap;
+ lodash.isWeakSet = isWeakSet;
+ lodash.join = join;
+ lodash.kebabCase = kebabCase;
+ lodash.last = last;
+ lodash.lastIndexOf = lastIndexOf;
+ lodash.lowerCase = lowerCase;
+ lodash.lowerFirst = lowerFirst;
+ lodash.lt = lt;
+ lodash.lte = lte;
+ lodash.max = max;
+ lodash.maxBy = maxBy;
+ lodash.mean = mean;
+ lodash.meanBy = meanBy;
+ lodash.min = min;
+ lodash.minBy = minBy;
+ lodash.stubArray = stubArray;
+ lodash.stubFalse = stubFalse;
+ lodash.stubObject = stubObject;
+ lodash.stubString = stubString;
+ lodash.stubTrue = stubTrue;
+ lodash.multiply = multiply;
+ lodash.nth = nth;
+ lodash.noConflict = noConflict;
+ lodash.noop = noop;
+ lodash.now = now;
+ lodash.pad = pad;
+ lodash.padEnd = padEnd;
+ lodash.padStart = padStart;
+ lodash.parseInt = parseInt;
+ lodash.random = random;
+ lodash.reduce = reduce;
+ lodash.reduceRight = reduceRight;
+ lodash.repeat = repeat;
+ lodash.replace = replace;
+ lodash.result = result;
+ lodash.round = round;
+ lodash.runInContext = runInContext;
+ lodash.sample = sample;
+ lodash.size = size;
+ lodash.snakeCase = snakeCase;
+ lodash.some = some;
+ lodash.sortedIndex = sortedIndex;
+ lodash.sortedIndexBy = sortedIndexBy;
+ lodash.sortedIndexOf = sortedIndexOf;
+ lodash.sortedLastIndex = sortedLastIndex;
+ lodash.sortedLastIndexBy = sortedLastIndexBy;
+ lodash.sortedLastIndexOf = sortedLastIndexOf;
+ lodash.startCase = startCase;
+ lodash.startsWith = startsWith;
+ lodash.subtract = subtract;
+ lodash.sum = sum;
+ lodash.sumBy = sumBy;
+ lodash.template = template;
+ lodash.times = times;
+ lodash.toFinite = toFinite;
+ lodash.toInteger = toInteger;
+ lodash.toLength = toLength;
+ lodash.toLower = toLower;
+ lodash.toNumber = toNumber;
+ lodash.toSafeInteger = toSafeInteger;
+ lodash.toString = toString;
+ lodash.toUpper = toUpper;
+ lodash.trim = trim;
+ lodash.trimEnd = trimEnd;
+ lodash.trimStart = trimStart;
+ lodash.truncate = truncate;
+ lodash.unescape = unescape;
+ lodash.uniqueId = uniqueId;
+ lodash.upperCase = upperCase;
+ lodash.upperFirst = upperFirst;
+
+ // Add aliases.
+ lodash.each = forEach;
+ lodash.eachRight = forEachRight;
+ lodash.first = head;
+
+ mixin(lodash, (function() {
+ var source = {};
+ baseForOwn(lodash, function(func, methodName) {
+ if (!hasOwnProperty.call(lodash.prototype, methodName)) {
+ source[methodName] = func;
+ }
+ });
+ return source;
+ }()), { 'chain': false });
+
+ /*------------------------------------------------------------------------*/
+
+ /**
+ * The semantic version number.
+ *
+ * @static
+ * @memberOf _
+ * @type {string}
+ */
+ lodash.VERSION = VERSION;
+
+ // Assign default placeholders.
+ arrayEach(['bind', 'bindKey', 'curry', 'curryRight', 'partial', 'partialRight'], function(methodName) {
+ lodash[methodName].placeholder = lodash;
+ });
+
+ // Add `LazyWrapper` methods for `_.drop` and `_.take` variants.
+ arrayEach(['drop', 'take'], function(methodName, index) {
+ LazyWrapper.prototype[methodName] = function(n) {
+ var filtered = this.__filtered__;
+ if (filtered && !index) {
+ return new LazyWrapper(this);
+ }
+ n = n === undefined ? 1 : nativeMax(toInteger(n), 0);
+
+ var result = this.clone();
+ if (filtered) {
+ result.__takeCount__ = nativeMin(n, result.__takeCount__);
+ } else {
+ result.__views__.push({
+ 'size': nativeMin(n, MAX_ARRAY_LENGTH),
+ 'type': methodName + (result.__dir__ < 0 ? 'Right' : '')
+ });
+ }
+ return result;
+ };
+
+ LazyWrapper.prototype[methodName + 'Right'] = function(n) {
+ return this.reverse()[methodName](n).reverse();
+ };
+ });
+
+ // Add `LazyWrapper` methods that accept an `iteratee` value.
+ arrayEach(['filter', 'map', 'takeWhile'], function(methodName, index) {
+ var type = index + 1,
+ isFilter = type == LAZY_FILTER_FLAG || type == LAZY_WHILE_FLAG;
+
+ LazyWrapper.prototype[methodName] = function(iteratee) {
+ var result = this.clone();
+ result.__iteratees__.push({
+ 'iteratee': getIteratee(iteratee, 3),
+ 'type': type
+ });
+ result.__filtered__ = result.__filtered__ || isFilter;
+ return result;
+ };
+ });
+
+ // Add `LazyWrapper` methods for `_.head` and `_.last`.
+ arrayEach(['head', 'last'], function(methodName, index) {
+ var takeName = 'take' + (index ? 'Right' : '');
+
+ LazyWrapper.prototype[methodName] = function() {
+ return this[takeName](1).value()[0];
+ };
+ });
+
+ // Add `LazyWrapper` methods for `_.initial` and `_.tail`.
+ arrayEach(['initial', 'tail'], function(methodName, index) {
+ var dropName = 'drop' + (index ? '' : 'Right');
+
+ LazyWrapper.prototype[methodName] = function() {
+ return this.__filtered__ ? new LazyWrapper(this) : this[dropName](1);
+ };
+ });
+
+ LazyWrapper.prototype.compact = function() {
+ return this.filter(identity);
+ };
+
+ LazyWrapper.prototype.find = function(predicate) {
+ return this.filter(predicate).head();
+ };
+
+ LazyWrapper.prototype.findLast = function(predicate) {
+ return this.reverse().find(predicate);
+ };
+
+ LazyWrapper.prototype.invokeMap = baseRest(function(path, args) {
+ if (typeof path == 'function') {
+ return new LazyWrapper(this);
+ }
+ return this.map(function(value) {
+ return baseInvoke(value, path, args);
+ });
+ });
+
+ LazyWrapper.prototype.reject = function(predicate) {
+ return this.filter(negate(getIteratee(predicate)));
+ };
+
+ LazyWrapper.prototype.slice = function(start, end) {
+ start = toInteger(start);
+
+ var result = this;
+ if (result.__filtered__ && (start > 0 || end < 0)) {
+ return new LazyWrapper(result);
+ }
+ if (start < 0) {
+ result = result.takeRight(-start);
+ } else if (start) {
+ result = result.drop(start);
+ }
+ if (end !== undefined) {
+ end = toInteger(end);
+ result = end < 0 ? result.dropRight(-end) : result.take(end - start);
+ }
+ return result;
+ };
+
+ LazyWrapper.prototype.takeRightWhile = function(predicate) {
+ return this.reverse().takeWhile(predicate).reverse();
+ };
+
+ LazyWrapper.prototype.toArray = function() {
+ return this.take(MAX_ARRAY_LENGTH);
+ };
+
+ // Add `LazyWrapper` methods to `lodash.prototype`.
+ baseForOwn(LazyWrapper.prototype, function(func, methodName) {
+ var checkIteratee = /^(?:filter|find|map|reject)|While$/.test(methodName),
+ isTaker = /^(?:head|last)$/.test(methodName),
+ lodashFunc = lodash[isTaker ? ('take' + (methodName == 'last' ? 'Right' : '')) : methodName],
+ retUnwrapped = isTaker || /^find/.test(methodName);
+
+ if (!lodashFunc) {
+ return;
+ }
+ lodash.prototype[methodName] = function() {
+ var value = this.__wrapped__,
+ args = isTaker ? [1] : arguments,
+ isLazy = value instanceof LazyWrapper,
+ iteratee = args[0],
+ useLazy = isLazy || isArray(value);
+
+ var interceptor = function(value) {
+ var result = lodashFunc.apply(lodash, arrayPush([value], args));
+ return (isTaker && chainAll) ? result[0] : result;
+ };
+
+ if (useLazy && checkIteratee && typeof iteratee == 'function' && iteratee.length != 1) {
+ // Avoid lazy use if the iteratee has a "length" value other than `1`.
+ isLazy = useLazy = false;
+ }
+ var chainAll = this.__chain__,
+ isHybrid = !!this.__actions__.length,
+ isUnwrapped = retUnwrapped && !chainAll,
+ onlyLazy = isLazy && !isHybrid;
+
+ if (!retUnwrapped && useLazy) {
+ value = onlyLazy ? value : new LazyWrapper(this);
+ var result = func.apply(value, args);
+ result.__actions__.push({ 'func': thru, 'args': [interceptor], 'thisArg': undefined });
+ return new LodashWrapper(result, chainAll);
+ }
+ if (isUnwrapped && onlyLazy) {
+ return func.apply(this, args);
+ }
+ result = this.thru(interceptor);
+ return isUnwrapped ? (isTaker ? result.value()[0] : result.value()) : result;
+ };
+ });
+
+ // Add `Array` methods to `lodash.prototype`.
+ arrayEach(['pop', 'push', 'shift', 'sort', 'splice', 'unshift'], function(methodName) {
+ var func = arrayProto[methodName],
+ chainName = /^(?:push|sort|unshift)$/.test(methodName) ? 'tap' : 'thru',
+ retUnwrapped = /^(?:pop|shift)$/.test(methodName);
+
+ lodash.prototype[methodName] = function() {
+ var args = arguments;
+ if (retUnwrapped && !this.__chain__) {
+ var value = this.value();
+ return func.apply(isArray(value) ? value : [], args);
+ }
+ return this[chainName](function(value) {
+ return func.apply(isArray(value) ? value : [], args);
+ });
+ };
+ });
+
+ // Map minified method names to their real names.
+ baseForOwn(LazyWrapper.prototype, function(func, methodName) {
+ var lodashFunc = lodash[methodName];
+ if (lodashFunc) {
+ var key = (lodashFunc.name + ''),
+ names = realNames[key] || (realNames[key] = []);
+
+ names.push({ 'name': methodName, 'func': lodashFunc });
+ }
+ });
+
+ realNames[createHybrid(undefined, BIND_KEY_FLAG).name] = [{
+ 'name': 'wrapper',
+ 'func': undefined
+ }];
+
+ // Add methods to `LazyWrapper`.
+ LazyWrapper.prototype.clone = lazyClone;
+ LazyWrapper.prototype.reverse = lazyReverse;
+ LazyWrapper.prototype.value = lazyValue;
+
+ // Add chain sequence methods to the `lodash` wrapper.
+ lodash.prototype.at = wrapperAt;
+ lodash.prototype.chain = wrapperChain;
+ lodash.prototype.commit = wrapperCommit;
+ lodash.prototype.next = wrapperNext;
+ lodash.prototype.plant = wrapperPlant;
+ lodash.prototype.reverse = wrapperReverse;
+ lodash.prototype.toJSON = lodash.prototype.valueOf = lodash.prototype.value = wrapperValue;
+
+ // Add lazy aliases.
+ lodash.prototype.first = lodash.prototype.head;
+
+ if (iteratorSymbol) {
+ lodash.prototype[iteratorSymbol] = wrapperToIterator;
+ }
+ return lodash;
+ });
+
+ /*--------------------------------------------------------------------------*/
+
+ // Export lodash.
+ var _ = runInContext();
+
+ // Some AMD build optimizers, like r.js, check for condition patterns like:
+ if (true) {
+ // Expose Lodash on the global object to prevent errors when Lodash is
+ // loaded by a script tag in the presence of an AMD loader.
+ // See http://requirejs.org/docs/errors.html#mismatch for more details.
+ // Use `_.noConflict` to remove Lodash from the global object.
+ root._ = _;
+
+ // Define as an anonymous module so, through path mapping, it can be
+ // referenced as the "underscore" module.
+ !(__WEBPACK_AMD_DEFINE_RESULT__ = function() {
+ return _;
+ }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+ }
+ // Check for `exports` after `define` in case a build optimizer adds it.
+ else if (freeModule) {
+ // Export for Node.js.
+ (freeModule.exports = _)._ = _;
+ // Export for CommonJS support.
+ freeExports._ = _;
+ }
+ else {
+ // Export to the global object.
+ root._ = _;
+ }
+ }.call(this));
+
+ /* WEBPACK VAR INJECTION */}.call(exports, (function() { return this; }()), __webpack_require__(77)(module)))
+
+/***/ },
+/* 409 */
+/***/ function(module, exports) {
+
+ // removed by extract-text-webpack-plugin
+
+/***/ },
+/* 410 */,
+/* 411 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var React = __webpack_require__(2);
+ var dom = React.DOM;
+
+
+ var ReactDOM = __webpack_require__(16);
+
+ function renderConditionalPanel(_ref) {
+ var condition = _ref.condition;
+ var closePanel = _ref.closePanel;
+ var setBreakpoint = _ref.setBreakpoint;
+
+ var panel = document.createElement("div");
+
+ function onKey(e) {
+ if (e.key != "Enter") {
+ return;
+ }
+
+ setBreakpoint(e.target.value);
+ closePanel();
+ }
+
+ ReactDOM.render(dom.div({ className: "conditional-breakpoint-panel" }, dom.input({
+ defaultValue: condition,
+ placeholder: "This breakpoint will pause when the expression is true",
+ onKeyPress: onKey
+ })), panel);
+
+ return panel;
+ }
+
+ module.exports = {
+ renderConditionalPanel
+ };
+
+/***/ },
+/* 412 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var React = __webpack_require__(2);
+ var ReactDOM = __webpack_require__(16);
+
+ var PropTypes = React.PropTypes;
+
+ var classnames = __webpack_require__(211);
+ var Svg = __webpack_require__(310);
+
+ var breakpointSvg = document.createElement("div");
+ ReactDOM.render(Svg("breakpoint"), breakpointSvg);
+
+ function makeMarker(isDisabled) {
+ var bp = breakpointSvg.cloneNode(true);
+ bp.className = classnames("editor new-breakpoint", { "breakpoint-disabled": isDisabled });
+
+ return bp;
+ }
+
+ var Breakpoint = React.createClass({
+ propTypes: {
+ breakpoint: PropTypes.object,
+ editor: PropTypes.object
+ },
+
+ displayName: "Breakpoint",
+
+ addBreakpoint() {
+ var bp = this.props.breakpoint;
+ var line = bp.location.line - 1;
+
+ this.props.editor.setGutterMarker(line, "breakpoints", makeMarker(bp.disabled));
+ this.props.editor.addLineClass(line, "line", "new-breakpoint");
+ if (bp.condition) {
+ this.props.editor.addLineClass(line, "line", "has-condition");
+ }
+ },
+
+ shouldComponentUpdate(nextProps) {
+ return this.props.editor !== nextProps.editor || this.props.breakpoint.disabled !== nextProps.breakpoint.disabled || this.props.breakpoint.condition !== nextProps.breakpoint.condition;
+ },
+
+ componentDidMount() {
+ if (!this.props.editor) {
+ return;
+ }
+
+ this.addBreakpoint();
+ },
+
+ componentDidUpdate() {
+ this.addBreakpoint();
+ },
+
+ componentWillUnmount() {
+ if (!this.props.editor) {
+ return;
+ }
+
+ var bp = this.props.breakpoint;
+ var line = bp.location.line - 1;
+
+ this.props.editor.setGutterMarker(line, "breakpoints", null);
+ this.props.editor.removeLineClass(line, "line", "new-breakpoint");
+ this.props.editor.removeLineClass(line, "line", "has-condition");
+ },
+
+ render() {
+ return null;
+ }
+ });
+
+ module.exports = Breakpoint;
+
+/***/ },
+/* 413 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var _require = __webpack_require__(28);
+
+ var Menu = _require.Menu;
+ var MenuItem = _require.MenuItem;
+
+ var _require2 = __webpack_require__(89);
+
+ var isFirefoxPanel = _require2.isFirefoxPanel;
+
+
+ function createPopup(doc) {
+ var popup = doc.createElement("menupopup");
+
+ if (popup.openPopupAtScreen) {
+ return popup;
+ }
+
+ function preventDefault(e) {
+ e.preventDefault();
+ e.returnValue = false;
+ }
+
+ var mask = document.querySelector("#contextmenu-mask");
+ if (!mask) {
+ mask = doc.createElement("div");
+ mask.id = "contextmenu-mask";
+ document.body.appendChild(mask);
+ }
+
+ mask.onclick = () => popup.hidePopup();
+
+ popup.openPopupAtScreen = function (clientX, clientY) {
+ this.style.setProperty("left", clientX + "px");
+ this.style.setProperty("top", clientY + "px");
+ mask = document.querySelector("#contextmenu-mask");
+ window.onwheel = preventDefault;
+ mask.classList.add("show");
+ this.dispatchEvent(new Event("popupshown"));
+ this.popupshown;
+ };
+
+ popup.hidePopup = function () {
+ this.remove();
+ mask = document.querySelector("#contextmenu-mask");
+ mask.classList.remove("show");
+ window.onwheel = null;
+ };
+
+ return popup;
+ }
+
+ if (!isFirefoxPanel()) {
+ Menu.prototype.createPopup = createPopup;
+ }
+
+ function onShown(menu, popup) {
+ popup.childNodes.forEach((menuitem, index) => {
+ var item = menu.items[index];
+ menuitem.onclick = () => {
+ item.click();
+ popup.hidePopup();
+ };
+ });
+ }
+
+ function showMenu(e, items) {
+ var menu = new Menu();
+ items.forEach(item => menu.append(new MenuItem(item)));
+
+ if (isFirefoxPanel()) {
+ return menu.popup(e.screenX, e.screenY, { doc: window.parent.document });
+ }
+
+ menu.on("open", (_, popup) => onShown(menu, popup));
+ return menu.popup(e.clientX, e.clientY, { doc: document });
+ }
+
+ function buildMenu(items) {
+ return items.map(itm => {
+ var hide = typeof itm.hidden === "function" ? itm.hidden() : itm.hidden;
+ return hide ? null : itm.item;
+ }).filter(itm => itm !== null);
+ }
+
+ module.exports = {
+ showMenu,
+ buildMenu
+ };
+
+/***/ },
+/* 414 */
+/***/ function(module, exports) {
+
+ // removed by extract-text-webpack-plugin
+
+/***/ },
+/* 415 */,
+/* 416 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var React = __webpack_require__(2);
+ var dom = React.DOM;
+ var PropTypes = React.PropTypes;
+
+ var _require = __webpack_require__(19);
+
+ var connect = _require.connect;
+
+ var _require2 = __webpack_require__(3);
+
+ var bindActionCreators = _require2.bindActionCreators;
+
+ var _require3 = __webpack_require__(259);
+
+ var getPause = _require3.getPause;
+ var getIsWaitingOnBreak = _require3.getIsWaitingOnBreak;
+ var getBreakpointsDisabled = _require3.getBreakpointsDisabled;
+ var getShouldPauseOnExceptions = _require3.getShouldPauseOnExceptions;
+ var getShouldIgnoreCaughtExceptions = _require3.getShouldIgnoreCaughtExceptions;
+ var getBreakpoints = _require3.getBreakpoints;
+ var getBreakpointsLoading = _require3.getBreakpointsLoading;
+
+ var _require4 = __webpack_require__(89);
+
+ var isEnabled = _require4.isEnabled;
+
+ var Svg = __webpack_require__(310);
+ var ImPropTypes = __webpack_require__(235);
+
+ var _require5 = __webpack_require__(30);
+
+ var appinfo = _require5.Services.appinfo;
+
+ var shiftKey = appinfo.OS === "Darwin" ? "\u21E7" : "Shift+";
+ var ctrlKey = appinfo.OS === "Linux" ? "Ctrl+" : "";
+
+ var actions = __webpack_require__(262);
+ var Breakpoints = React.createFactory(__webpack_require__(417));
+ var Expressions = React.createFactory(__webpack_require__(420));
+ var Scopes = React.createFactory(__webpack_require__(424));
+ var Frames = React.createFactory(__webpack_require__(444));
+ var Accordion = React.createFactory(__webpack_require__(447));
+ __webpack_require__(450);
+
+ function debugBtn(onClick, type, className, tooltip) {
+ className = `${ type } ${ className }`;
+ return dom.span({ onClick, className, key: type }, Svg(type, { title: tooltip }));
+ }
+
+ var RightSidebar = React.createClass({
+ propTypes: {
+ sources: PropTypes.object,
+ selectedSource: PropTypes.object,
+ resume: PropTypes.func,
+ stepIn: PropTypes.func,
+ stepOut: PropTypes.func,
+ stepOver: PropTypes.func,
+ toggleAllBreakpoints: PropTypes.func,
+ breakOnNext: PropTypes.func,
+ pause: ImPropTypes.map,
+ pauseOnExceptions: PropTypes.func,
+ shouldPauseOnExceptions: PropTypes.bool,
+ shouldIgnoreCaughtExceptions: PropTypes.bool,
+ breakpoints: ImPropTypes.map,
+ isWaitingOnBreak: PropTypes.bool,
+ breakpointsDisabled: PropTypes.bool,
+ breakpointsLoading: PropTypes.bool,
+ evaluateExpressions: PropTypes.func
+ },
+
+ contextTypes: {
+ shortcuts: PropTypes.object
+ },
+
+ displayName: "RightSidebar",
+
+ getInitialState() {
+ return {
+ expressionInputVisibility: true
+ };
+ },
+
+ resume() {
+ if (this.props.pause) {
+ this.props.resume();
+ } else if (!this.props.isWaitingOnBreak) {
+ this.props.breakOnNext();
+ }
+ },
+
+ stepOver() {
+ if (!this.props.pause) {
+ return;
+ }
+ this.props.stepOver();
+ },
+
+ stepIn() {
+ if (!this.props.pause) {
+ return;
+ }
+ this.props.stepIn();
+ },
+
+ stepOut() {
+ if (!this.props.pause) {
+ return;
+ }
+ this.props.stepOut();
+ },
+
+ componentWillUnmount() {
+ var shortcuts = this.context.shortcuts;
+ shortcuts.off("F8", this.resume);
+ shortcuts.off("F10", this.stepOver);
+ shortcuts.off(`${ ctrlKey }F11`, this.stepIn);
+ shortcuts.off(`${ ctrlKey }Shift+F11`, this.stepOut);
+ },
+
+ componentDidMount() {
+ var shortcuts = this.context.shortcuts;
+ shortcuts.on("F8", this.resume);
+ shortcuts.on("F10", this.stepOver);
+ shortcuts.on(`${ ctrlKey }F11`, this.stepIn);
+ shortcuts.on(`${ ctrlKey }Shift+F11`, this.stepOut);
+ },
+
+ renderStepButtons() {
+ var className = this.props.pause ? "active" : "disabled";
+ return [debugBtn(this.stepOver, "stepOver", className, L10N.getStr("stepOverTooltip")), debugBtn(this.stepIn, "stepIn", className, L10N.getFormatStr("stepInTooltip", ctrlKey)), debugBtn(this.stepOut, "stepOut", className, L10N.getFormatStr("stepOutTooltip", ctrlKey + shiftKey))];
+ },
+
+ renderPauseButton() {
+ var _props = this.props;
+ var pause = _props.pause;
+ var breakOnNext = _props.breakOnNext;
+ var isWaitingOnBreak = _props.isWaitingOnBreak;
+
+
+ if (pause) {
+ return debugBtn(this.resume, "resume", "active", L10N.getStr("resumeButtonTooltip"));
+ }
+
+ if (isWaitingOnBreak) {
+ return debugBtn(null, "pause", "disabled", L10N.getStr("pausePendingButtonTooltip"));
+ }
+
+ return debugBtn(breakOnNext, "pause", "active", L10N.getStr("pauseButtonTooltip"));
+ },
+
+ /*
+ * The pause on exception button has three states in this order:
+ * 1. don't pause on exceptions [false, false]
+ * 2. pause on uncaught exceptions [true, true]
+ * 3. pause on all exceptions [true, false]
+ */
+ renderPauseOnExceptions() {
+ var _props2 = this.props;
+ var shouldPauseOnExceptions = _props2.shouldPauseOnExceptions;
+ var shouldIgnoreCaughtExceptions = _props2.shouldIgnoreCaughtExceptions;
+ var pauseOnExceptions = _props2.pauseOnExceptions;
+
+
+ if (!shouldPauseOnExceptions && !shouldIgnoreCaughtExceptions) {
+ return debugBtn(() => pauseOnExceptions(true, true), "pause-exceptions", "enabled", L10N.getStr("ignoreExceptions"));
+ }
+
+ if (shouldPauseOnExceptions && shouldIgnoreCaughtExceptions) {
+ return debugBtn(() => pauseOnExceptions(true, false), "pause-exceptions", "uncaught enabled", L10N.getStr("pauseOnUncaughtExceptions"));
+ }
+
+ return debugBtn(() => pauseOnExceptions(false, false), "pause-exceptions", "all enabled", L10N.getStr("pauseOnExceptions"));
+ },
+
+ renderDisableBreakpoints() {
+ var _props3 = this.props;
+ var toggleAllBreakpoints = _props3.toggleAllBreakpoints;
+ var breakpoints = _props3.breakpoints;
+ var breakpointsDisabled = _props3.breakpointsDisabled;
+ var breakpointsLoading = _props3.breakpointsLoading;
+
+
+ if (breakpoints.size == 0 || breakpointsLoading) {
+ return debugBtn(null, "toggleBreakpoints", "disabled", "Disable Breakpoints");
+ }
+
+ return debugBtn(() => toggleAllBreakpoints(!breakpointsDisabled), "toggleBreakpoints", breakpointsDisabled ? "breakpoints-disabled" : "", "Disable Breakpoints");
+ },
+
+ getItems() {
+ var expressionInputVisibility = this.state.expressionInputVisibility;
+
+ var items = [{ header: L10N.getStr("breakpoints.header"),
+ component: Breakpoints,
+ opened: true }, { header: L10N.getStr("callStack.header"),
+ component: Frames }, { header: L10N.getStr("scopes.header"),
+ component: Scopes }];
+ if (isEnabled("watchExpressions")) {
+ items.unshift({ header: L10N.getStr("watchExpressions.header"),
+ buttons: [debugBtn(evt => {
+ evt.stopPropagation();
+ this.props.evaluateExpressions();
+ }, "domain", "accordion-button", "Refresh"), debugBtn(evt => {
+ evt.stopPropagation();
+ this.setState({
+ expressionInputVisibility: !expressionInputVisibility
+ });
+ }, "file", "accordion-button", "Add Watch Expression")],
+ component: Expressions,
+ componentProps: { expressionInputVisibility },
+ opened: true
+ });
+ }
+ return items;
+ },
+
+ render() {
+ return dom.div({ className: "right-sidebar",
+ style: { overflowX: "hidden" } }, dom.div({ className: "command-bar" }, this.renderPauseButton(), this.renderStepButtons(), this.renderDisableBreakpoints(), this.renderPauseOnExceptions()), Accordion({
+ items: this.getItems()
+ }));
+ }
+
+ });
+
+ module.exports = connect(state => {
+ return {
+ pause: getPause(state),
+ isWaitingOnBreak: getIsWaitingOnBreak(state),
+ shouldPauseOnExceptions: getShouldPauseOnExceptions(state),
+ shouldIgnoreCaughtExceptions: getShouldIgnoreCaughtExceptions(state),
+ breakpointsDisabled: getBreakpointsDisabled(state),
+ breakpoints: getBreakpoints(state),
+ breakpointsLoading: getBreakpointsLoading(state)
+ };
+ }, dispatch => bindActionCreators(actions, dispatch))(RightSidebar);
+
+/***/ },
+/* 417 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var React = __webpack_require__(2);
+
+ var _require = __webpack_require__(19);
+
+ var connect = _require.connect;
+
+ var _require2 = __webpack_require__(3);
+
+ var bindActionCreators = _require2.bindActionCreators;
+
+ var ImPropTypes = __webpack_require__(235);
+ var classnames = __webpack_require__(211);
+ var actions = __webpack_require__(262);
+
+ var _require3 = __webpack_require__(259);
+
+ var getSource = _require3.getSource;
+ var getPause = _require3.getPause;
+ var getBreakpoints = _require3.getBreakpoints;
+
+ var _require4 = __webpack_require__(255);
+
+ var makeLocationId = _require4.makeLocationId;
+
+ var _require5 = __webpack_require__(244);
+
+ var truncateStr = _require5.truncateStr;
+ var dom = React.DOM;
+ var PropTypes = React.PropTypes;
+
+ var _require6 = __webpack_require__(244);
+
+ var endTruncateStr = _require6.endTruncateStr;
+
+ var _require7 = __webpack_require__(278);
+
+ var basename = _require7.basename;
+
+ var CloseButton = __webpack_require__(336);
+
+ __webpack_require__(418);
+
+ function isCurrentlyPausedAtBreakpoint(state, breakpoint) {
+ var pause = getPause(state);
+ if (!pause || pause.get("isInterrupted")) {
+ return false;
+ }
+
+ var bpId = makeLocationId(breakpoint.location);
+ var pausedId = makeLocationId(pause.getIn(["frame", "location"]).toJS());
+
+ return bpId === pausedId;
+ }
+
+ function renderSourceLocation(source, line) {
+ var url = source.get("url") ? basename(source.get("url")) : null;
+ // const line = url !== "" ? `: ${line}` : "";
+ return url ? dom.div({ className: "location" }, `${ endTruncateStr(url, 30) }: ${ line }`) : null;
+ }
+
+ var Breakpoints = React.createClass({
+ propTypes: {
+ breakpoints: ImPropTypes.map.isRequired,
+ enableBreakpoint: PropTypes.func.isRequired,
+ disableBreakpoint: PropTypes.func.isRequired,
+ selectSource: PropTypes.func.isRequired,
+ removeBreakpoint: PropTypes.func.isRequired
+ },
+
+ displayName: "Breakpoints",
+
+ handleCheckbox(breakpoint) {
+ if (breakpoint.loading) {
+ return;
+ }
+
+ if (breakpoint.disabled) {
+ this.props.enableBreakpoint(breakpoint.location);
+ } else {
+ this.props.disableBreakpoint(breakpoint.location);
+ }
+ },
+
+ selectBreakpoint(breakpoint) {
+ var sourceId = breakpoint.location.sourceId;
+ var line = breakpoint.location.line;
+ this.props.selectSource(sourceId, { line });
+ },
+
+ removeBreakpoint(event, breakpoint) {
+ event.stopPropagation();
+ this.props.removeBreakpoint(breakpoint.location);
+ },
+
+ renderBreakpoint(breakpoint) {
+ var snippet = truncateStr(breakpoint.text || "", 30);
+ var locationId = breakpoint.locationId;
+ var line = breakpoint.location.line;
+ var isCurrentlyPaused = breakpoint.isCurrentlyPaused;
+ var isDisabled = breakpoint.disabled;
+
+ return dom.div({
+ className: classnames({
+ breakpoint,
+ paused: isCurrentlyPaused,
+ disabled: isDisabled
+ }),
+ key: locationId,
+ onClick: () => this.selectBreakpoint(breakpoint)
+ }, dom.input({
+ type: "checkbox",
+ className: "breakpoint-checkbox",
+ checked: !isDisabled,
+ onChange: () => this.handleCheckbox(breakpoint),
+ // Prevent clicking on the checkbox from triggering the onClick of
+ // the surrounding div
+ onClick: ev => ev.stopPropagation()
+ }), dom.div({ className: "breakpoint-label", title: breakpoint.text }, dom.div({}, renderSourceLocation(breakpoint.location.source, line))), dom.div({ className: "breakpoint-snippet" }, snippet), CloseButton({
+ handleClick: ev => this.removeBreakpoint(ev, breakpoint)
+ }));
+ },
+
+ render() {
+ var breakpoints = this.props.breakpoints;
+
+ return dom.div({ className: "pane breakpoints-list" }, breakpoints.size === 0 ? dom.div({ className: "pane-info" }, "No Breakpoints") : breakpoints.valueSeq().map(bp => {
+ return this.renderBreakpoint(bp);
+ }));
+ }
+ });
+
+ function _getBreakpoints(state) {
+ return getBreakpoints(state).map(bp => {
+ var source = getSource(state, bp.location.sourceId);
+ var isCurrentlyPaused = isCurrentlyPausedAtBreakpoint(state, bp);
+ var locationId = makeLocationId(bp.location);
+
+ bp = Object.assign({}, bp);
+ bp.location.source = source;
+ bp.locationId = locationId;
+ bp.isCurrentlyPaused = isCurrentlyPaused;
+ return bp;
+ }).filter(bp => bp.location.source);
+ }
+
+ module.exports = connect((state, props) => ({
+ breakpoints: _getBreakpoints(state)
+ }), dispatch => bindActionCreators(actions, dispatch))(Breakpoints);
+
+/***/ },
+/* 418 */
+/***/ function(module, exports) {
+
+ // removed by extract-text-webpack-plugin
+
+/***/ },
+/* 419 */,
+/* 420 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var React = __webpack_require__(2);
+
+ var _require = __webpack_require__(19);
+
+ var connect = _require.connect;
+
+ var _require2 = __webpack_require__(3);
+
+ var bindActionCreators = _require2.bindActionCreators;
+
+ var ImPropTypes = __webpack_require__(235);
+ var actions = __webpack_require__(262);
+
+ var _require3 = __webpack_require__(259);
+
+ var getExpressions = _require3.getExpressions;
+ var getPause = _require3.getPause;
+
+ var Rep = React.createFactory(__webpack_require__(421));
+ var CloseButton = React.createFactory(__webpack_require__(336));
+ var dom = React.DOM;
+ var PropTypes = React.PropTypes;
+
+
+ __webpack_require__(422);
+
+ var Expressions = React.createClass({
+ propTypes: {
+ expressions: ImPropTypes.list,
+ addExpression: PropTypes.func,
+ updateExpression: PropTypes.func,
+ deleteExpression: PropTypes.func,
+ expressionInputVisibility: PropTypes.bool,
+ loadObjectProperties: PropTypes.func,
+ loadedObjects: ImPropTypes.map
+ },
+
+ displayName: "Expressions",
+
+ inputKeyPress(e, _ref) {
+ var id = _ref.id;
+
+ if (e.key !== "Enter") {
+ return;
+ }
+ var addExpression = this.props.addExpression;
+
+ var expression = {
+ input: e.target.value
+ };
+ if (id !== undefined) {
+ expression.id = id;
+ }
+ e.target.value = "";
+ addExpression(expression);
+ },
+
+ updateExpression(e, _ref2) {
+ var id = _ref2.id;
+
+ e.stopPropagation();
+ var updateExpression = this.props.updateExpression;
+
+ var expression = {
+ id,
+ input: e.target.textContent
+ };
+ updateExpression(expression);
+ },
+
+ renderExpressionValue(value) {
+ if (!value) {
+ return dom.span({ className: "expression-error" }, "<not available>");
+ }
+ if (value.exception) {
+ return Rep({ object: value.exception });
+ }
+ return Rep({ object: value.result });
+ },
+
+ deleteExpression(e, expression) {
+ e.stopPropagation();
+ var deleteExpression = this.props.deleteExpression;
+
+ deleteExpression(expression);
+ },
+
+ renderExpressionUpdating(expression) {
+ return dom.span({ className: "expression-input-container" }, dom.input({ type: "text",
+ className: "input-expression",
+ onKeyPress: e => this.inputKeyPress(e, expression),
+ defaultValue: expression.input,
+ ref: c => {
+ this._input = c;
+ }
+ }));
+ },
+
+ renderExpression(expression) {
+ return dom.span({ className: "expression-output-container",
+ key: expression.id }, dom.span({ className: "expression-input",
+ onClick: e => this.updateExpression(e, expression) }, expression.input), dom.span({ className: "expression-seperator" }, ": "), dom.span({ className: "expression-value" }, this.renderExpressionValue(expression.value)), CloseButton({ handleClick: e => this.deleteExpression(e, expression) }));
+ },
+
+ renderExpressionContainer(expression) {
+ return dom.div({ className: "expression-container",
+ key: expression.id + expression.input }, expression.updating ? this.renderExpressionUpdating(expression) : this.renderExpression(expression));
+ },
+
+ componentDidUpdate() {
+ if (this._input) {
+ this._input.focus();
+ }
+ },
+
+ render() {
+ var expressions = this.props.expressions;
+
+ return dom.span({ className: "pane expressions-list" }, this.props.expressionInputVisibility ? dom.input({ type: "text",
+ className: "input-expression",
+ placeholder: "Add Watch Expression",
+ onKeyPress: e => this.inputKeyPress(e, {}) }) : null, expressions.toSeq().map(expression => this.renderExpressionContainer(expression)));
+ }
+ });
+
+ module.exports = connect(state => ({ pauseInfo: getPause(state),
+ expressions: getExpressions(state)
+ }), dispatch => bindActionCreators(actions, dispatch))(Expressions);
+
+/***/ },
+/* 421 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var React = __webpack_require__(2);
+
+ var _require = __webpack_require__(30);
+
+ var rep = _require.rep;
+ var Grip = _require.Grip;
+
+ var Rep = React.createFactory(rep);
+
+ function renderRep(_ref) {
+ var object = _ref.object;
+ var mode = _ref.mode;
+
+ return Rep({ object, defaultRep: Grip, mode });
+ }
+
+ module.exports = renderRep;
+
+/***/ },
+/* 422 */
+/***/ function(module, exports) {
+
+ // removed by extract-text-webpack-plugin
+
+/***/ },
+/* 423 */,
+/* 424 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var React = __webpack_require__(2);
+
+ var _require = __webpack_require__(3);
+
+ var bindActionCreators = _require.bindActionCreators;
+
+ var _require2 = __webpack_require__(19);
+
+ var connect = _require2.connect;
+
+ var ImPropTypes = __webpack_require__(235);
+ var actions = __webpack_require__(262);
+
+ var _require3 = __webpack_require__(259);
+
+ var getSelectedFrame = _require3.getSelectedFrame;
+ var getLoadedObjects = _require3.getLoadedObjects;
+ var getPause = _require3.getPause;
+
+ var ObjectInspector = React.createFactory(__webpack_require__(425));
+ var dom = React.DOM;
+ var PropTypes = React.PropTypes;
+
+ var toPairs = __webpack_require__(428);
+
+ __webpack_require__(442);
+
+ function info(text) {
+ return dom.div({ className: "pane-info" }, text);
+ }
+
+ // Create the tree nodes representing all the variables and arguments
+ // for the bindings from a scope.
+ function getBindingVariables(bindings, parentName) {
+ var args = bindings.arguments.map(arg => toPairs(arg)[0]);
+ var variables = toPairs(bindings.variables);
+
+ return args.concat(variables).filter(binding => !(binding[1].value.missingArguments || binding[1].value.optimizedOut)).map(binding => ({
+ name: binding[0],
+ path: parentName + "/" + binding[0],
+ contents: binding[1]
+ }));
+ }
+
+ function getSpecialVariables(pauseInfo, path) {
+ var thrown = pauseInfo.getIn(["why", "frameFinished", "throw"]);
+ var returned = pauseInfo.getIn(["why", "frameFinished", "return"]);
+ var vars = [];
+
+ if (thrown) {
+ // handle dehydrating exception strings and errors.
+ thrown = thrown.toJS ? thrown.toJS() : thrown;
+
+ vars.push({
+ name: "<exception>",
+ path: path + "/<exception>",
+ contents: { value: thrown }
+ });
+ }
+
+ if (returned) {
+ vars.push({
+ name: "<return>",
+ path: path + "/<return>",
+ contents: { value: returned.toJS() }
+ });
+ }
+
+ return vars;
+ }
+
+ function getThisVariable(frame, path) {
+ var this_ = frame.this;
+
+ if (!this_) {
+ return null;
+ }
+
+ return {
+ name: "<this>",
+ path: path + "/<this>",
+ contents: { value: this_ }
+ };
+ }
+
+ function getScopes(pauseInfo, selectedFrame) {
+ if (!pauseInfo || !selectedFrame) {
+ return null;
+ }
+
+ var selectedScope = selectedFrame.scope;
+
+ if (!selectedScope) {
+ return null;
+ }
+
+ var scopes = [];
+
+ var scope = selectedScope;
+ var pausedScopeActor = pauseInfo.getIn(["frame", "scope"]).get("actor");
+
+ do {
+ var type = scope.type;
+ var key = scope.actor;
+ if (type === "function" || type === "block") {
+ var bindings = scope.bindings;
+ var title = void 0;
+ if (type === "function") {
+ title = scope.function.displayName || "(anonymous)";
+ } else {
+ title = "Block";
+ }
+
+ var vars = getBindingVariables(bindings, title);
+
+ // show exception, return, and this variables in innermost scope
+ if (scope.actor === pausedScopeActor) {
+ vars = vars.concat(getSpecialVariables(pauseInfo, key));
+ }
+
+ if (scope.actor === selectedScope.actor) {
+ var this_ = getThisVariable(selectedFrame, key);
+
+ if (this_) {
+ vars.push(this_);
+ }
+ }
+
+ if (vars && vars.length) {
+ vars.sort((a, b) => a.name.localeCompare(b.name));
+ scopes.push({ name: title, path: key, contents: vars });
+ }
+ } else if (type === "object") {
+ var value = scope.object;
+ // If this is the global window scope, mark it as such so that it will
+ // preview Window: Global instead of Window: Window
+ if (value.class === "Window") {
+ value = Object.assign({}, scope.object, { isGlobal: true });
+ }
+ scopes.push({
+ name: scope.object.class,
+ path: key,
+ contents: { value }
+ });
+ }
+ } while (scope = scope.parent); // eslint-disable-line no-cond-assign
+
+ return scopes;
+ }
+
+ var Scopes = React.createClass({
+ propTypes: {
+ pauseInfo: ImPropTypes.map,
+ loadedObjects: ImPropTypes.map,
+ loadObjectProperties: PropTypes.func,
+ selectedFrame: PropTypes.object
+ },
+
+ displayName: "Scopes",
+
+ getInitialState() {
+ var _props = this.props;
+ var pauseInfo = _props.pauseInfo;
+ var selectedFrame = _props.selectedFrame;
+
+ return { scopes: getScopes(pauseInfo, selectedFrame) };
+ },
+
+ componentWillReceiveProps(nextProps) {
+ var _props2 = this.props;
+ var pauseInfo = _props2.pauseInfo;
+ var selectedFrame = _props2.selectedFrame;
+
+ var pauseInfoChanged = pauseInfo !== nextProps.pauseInfo;
+ var selectedFrameChange = selectedFrame !== nextProps.selectedFrame;
+
+ if (pauseInfoChanged || selectedFrameChange) {
+ this.setState({
+ scopes: getScopes(nextProps.pauseInfo, nextProps.selectedFrame)
+ });
+ }
+ },
+
+ render() {
+ var _props3 = this.props;
+ var pauseInfo = _props3.pauseInfo;
+ var loadObjectProperties = _props3.loadObjectProperties;
+ var loadedObjects = _props3.loadedObjects;
+ var scopes = this.state.scopes;
+
+
+ var scopeInspector = info(L10N.getStr("scopes.notAvailable"));
+ if (scopes) {
+ scopeInspector = ObjectInspector({
+ roots: scopes,
+ getObjectProperties: id => loadedObjects.get(id),
+ loadObjectProperties: loadObjectProperties
+ });
+ }
+
+ return dom.div({ className: "pane scopes-list" }, pauseInfo ? scopeInspector : info(L10N.getStr("scopes.notPaused")));
+ }
+ });
+
+ module.exports = connect(state => ({
+ pauseInfo: getPause(state),
+ selectedFrame: getSelectedFrame(state),
+ loadedObjects: getLoadedObjects(state)
+ }), dispatch => bindActionCreators(actions, dispatch))(Scopes);
+
+/***/ },
+/* 425 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var React = __webpack_require__(2);
+ var classnames = __webpack_require__(211);
+ var ManagedTree = React.createFactory(__webpack_require__(394));
+ var Svg = __webpack_require__(310);
+ var Rep = __webpack_require__(421);
+ var dom = React.DOM;
+ var PropTypes = React.PropTypes;
+
+
+ __webpack_require__(426);
+
+ // This implements a component that renders an interactive inspector
+ // for looking at JavaScript objects. It expects descriptions of
+ // objects from the protocol, and will dynamically fetch child
+ // properties as objects are expanded.
+ //
+ // If you want to inspect a single object, pass the name and the
+ // protocol descriptor of it:
+ //
+ // ObjectInspector({
+ // name: "foo",
+ // desc: { writable: true, ..., { value: { actor: "1", ... }}},
+ // ...
+ // })
+ //
+ // If you want multiple top-level objects (like scopes), you can pass
+ // an array of manually constructed nodes as `roots`:
+ //
+ // ObjectInspector({
+ // roots: [{ name: ... }, ...],
+ // ...
+ // });
+
+ // There are 3 types of nodes: a simple node with a children array, an
+ // object that has properties that should be children when they are
+ // fetched, and a primitive value that should be displayed with no
+ // children.
+
+ function nodeHasChildren(item) {
+ return Array.isArray(item.contents);
+ }
+
+ function nodeHasProperties(item) {
+ return !nodeHasChildren(item) && item.contents.value.type === "object";
+ }
+
+ function nodeIsPrimitive(item) {
+ return !nodeHasChildren(item) && !nodeHasProperties(item);
+ }
+
+ function createNode(name, path, contents) {
+ // The path is important to uniquely identify the item in the entire
+ // tree. This helps debugging & optimizes React's rendering of large
+ // lists. The path will be separated by property name,
+ // i.e. `{ foo: { bar: { baz: 5 }}}` will have a path of `foo/bar/baz`
+ // for the inner object.
+ return { name, path, contents };
+ }
+
+ var ObjectInspector = React.createClass({
+ propTypes: {
+ name: PropTypes.string,
+ desc: PropTypes.object,
+ roots: PropTypes.array,
+ getObjectProperties: PropTypes.func.isRequired,
+ loadObjectProperties: PropTypes.func.isRequired
+ },
+
+ displayName: "ObjectInspector",
+
+ getInitialState() {
+ // Cache of dynamically built nodes. We shouldn't need to clear
+ // this out ever, since we don't ever "switch out" the object
+ // being inspected.
+ this.actorCache = {};
+ return {};
+ },
+
+ makeNodesForProperties(objProps, parentPath) {
+ var ownProperties = objProps.ownProperties;
+ var prototype = objProps.prototype;
+
+
+ var nodes = Object.keys(ownProperties).filter(name => {
+ // Ignore non-concrete values like getters and setters
+ // for now by making sure we have a value.
+ return "value" in ownProperties[name];
+ }).map(name => {
+ return createNode(name, parentPath + "/" + name, ownProperties[name]);
+ });
+
+ // Add the prototype if it exists and is not null
+ if (prototype && prototype.type !== "null") {
+ nodes.push(createNode("__proto__", parentPath + "/__proto__", { value: prototype }));
+ }
+
+ return nodes;
+ },
+
+ getChildren(item) {
+ var getObjectProperties = this.props.getObjectProperties;
+
+ var obj = item.contents;
+
+ // Nodes can either have children already, or be an object with
+ // properties that we need to go and fetch.
+ if (nodeHasChildren(item)) {
+ return item.contents;
+ } else if (nodeHasProperties(item)) {
+ var actor = obj.value.actor;
+
+ // Because we are dynamically creating the tree as the user
+ // expands it (not precalcuated tree structure), we cache child
+ // arrays. This not only helps performance, but is necessary
+ // because the expanded state depends on instances of nodes
+ // being the same across renders. If we didn't do this, each
+ // node would be a new instance every render.
+ var key = item.path;
+ if (this.actorCache[key]) {
+ return this.actorCache[key];
+ }
+
+ var loadedProps = getObjectProperties(actor);
+ if (loadedProps) {
+ var children = this.makeNodesForProperties(loadedProps, item.path);
+ this.actorCache[actor] = children;
+ return children;
+ }
+ return [];
+ }
+ return [];
+ },
+
+ renderItem(item, depth, focused, _, expanded, _ref) {
+ var setExpanded = _ref.setExpanded;
+
+ var objectValue = void 0;
+ if (nodeHasProperties(item) || nodeIsPrimitive(item)) {
+ var object = item.contents.value;
+ objectValue = Rep({ object, mode: "tiny" });
+ }
+
+ return dom.div({ className: classnames("node", { focused }),
+ style: { marginLeft: depth * 15 },
+ onClick: e => {
+ e.stopPropagation();
+ setExpanded(item, !expanded);
+ }
+ }, Svg("arrow", {
+ className: classnames({
+ expanded: expanded,
+ hidden: nodeIsPrimitive(item)
+ })
+ }), dom.span({ className: "object-label" }, item.name), dom.span({ className: "object-delimiter" }, objectValue ? ": " : ""), dom.span({ className: "object-value" }, objectValue || ""));
+ },
+
+ render() {
+ var _props = this.props;
+ var name = _props.name;
+ var desc = _props.desc;
+ var loadObjectProperties = _props.loadObjectProperties;
+
+
+ var roots = this.props.roots;
+ if (!roots) {
+ roots = [createNode(name, name, desc)];
+ }
+
+ return ManagedTree({
+ itemHeight: 20,
+ getParent: item => null,
+ getChildren: this.getChildren,
+ getRoots: () => roots,
+ getKey: item => item.path,
+ autoExpand: 0,
+ disabledFocus: true,
+ onExpand: item => {
+ if (nodeHasProperties(item)) {
+ loadObjectProperties(item.contents.value);
+ }
+ },
+
+ renderItem: this.renderItem
+ });
+ }
+ });
+
+ module.exports = ObjectInspector;
+
+/***/ },
+/* 426 */
+/***/ function(module, exports) {
+
+ // removed by extract-text-webpack-plugin
+
+/***/ },
+/* 427 */,
+/* 428 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var createToPairs = __webpack_require__(429),
+ keys = __webpack_require__(439);
+
+ /**
+ * Creates an array of own enumerable string keyed-value pairs for `object`
+ * which can be consumed by `_.fromPairs`. If `object` is a map or set, its
+ * entries are returned.
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @alias entries
+ * @category Object
+ * @param {Object} object The object to query.
+ * @returns {Array} Returns the key-value pairs.
+ * @example
+ *
+ * function Foo() {
+ * this.a = 1;
+ * this.b = 2;
+ * }
+ *
+ * Foo.prototype.c = 3;
+ *
+ * _.toPairs(new Foo);
+ * // => [['a', 1], ['b', 2]] (iteration order is not guaranteed)
+ */
+ var toPairs = createToPairs(keys);
+
+ module.exports = toPairs;
+
+
+/***/ },
+/* 429 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var baseToPairs = __webpack_require__(430),
+ getTag = __webpack_require__(431),
+ mapToArray = __webpack_require__(437),
+ setToPairs = __webpack_require__(438);
+
+ /** `Object#toString` result references. */
+ var mapTag = '[object Map]',
+ setTag = '[object Set]';
+
+ /**
+ * Creates a `_.toPairs` or `_.toPairsIn` function.
+ *
+ * @private
+ * @param {Function} keysFunc The function to get the keys of a given object.
+ * @returns {Function} Returns the new pairs function.
+ */
+ function createToPairs(keysFunc) {
+ return function(object) {
+ var tag = getTag(object);
+ if (tag == mapTag) {
+ return mapToArray(object);
+ }
+ if (tag == setTag) {
+ return setToPairs(object);
+ }
+ return baseToPairs(object, keysFunc(object));
+ };
+ }
+
+ module.exports = createToPairs;
+
+
+/***/ },
+/* 430 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var arrayMap = __webpack_require__(135);
+
+ /**
+ * The base implementation of `_.toPairs` and `_.toPairsIn` which creates an array
+ * of key-value pairs for `object` corresponding to the property names of `props`.
+ *
+ * @private
+ * @param {Object} object The object to query.
+ * @param {Array} props The property names to get values for.
+ * @returns {Object} Returns the key-value pairs.
+ */
+ function baseToPairs(object, props) {
+ return arrayMap(props, function(key) {
+ return [key, object[key]];
+ });
+ }
+
+ module.exports = baseToPairs;
+
+
+/***/ },
+/* 431 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var DataView = __webpack_require__(432),
+ Map = __webpack_require__(125),
+ Promise = __webpack_require__(433),
+ Set = __webpack_require__(434),
+ WeakMap = __webpack_require__(435),
+ baseGetTag = __webpack_require__(436),
+ toSource = __webpack_require__(111);
+
+ /** `Object#toString` result references. */
+ var mapTag = '[object Map]',
+ objectTag = '[object Object]',
+ promiseTag = '[object Promise]',
+ setTag = '[object Set]',
+ weakMapTag = '[object WeakMap]';
+
+ var dataViewTag = '[object DataView]';
+
+ /** Used for built-in method references. */
+ var objectProto = Object.prototype;
+
+ /**
+ * Used to resolve the
+ * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring)
+ * of values.
+ */
+ var objectToString = objectProto.toString;
+
+ /** Used to detect maps, sets, and weakmaps. */
+ var dataViewCtorString = toSource(DataView),
+ mapCtorString = toSource(Map),
+ promiseCtorString = toSource(Promise),
+ setCtorString = toSource(Set),
+ weakMapCtorString = toSource(WeakMap);
+
+ /**
+ * Gets the `toStringTag` of `value`.
+ *
+ * @private
+ * @param {*} value The value to query.
+ * @returns {string} Returns the `toStringTag`.
+ */
+ var getTag = baseGetTag;
+
+ // Fallback for data views, maps, sets, and weak maps in IE 11 and promises in Node.js < 6.
+ if ((DataView && getTag(new DataView(new ArrayBuffer(1))) != dataViewTag) ||
+ (Map && getTag(new Map) != mapTag) ||
+ (Promise && getTag(Promise.resolve()) != promiseTag) ||
+ (Set && getTag(new Set) != setTag) ||
+ (WeakMap && getTag(new WeakMap) != weakMapTag)) {
+ getTag = function(value) {
+ var result = objectToString.call(value),
+ Ctor = result == objectTag ? value.constructor : undefined,
+ ctorString = Ctor ? toSource(Ctor) : undefined;
+
+ if (ctorString) {
+ switch (ctorString) {
+ case dataViewCtorString: return dataViewTag;
+ case mapCtorString: return mapTag;
+ case promiseCtorString: return promiseTag;
+ case setCtorString: return setTag;
+ case weakMapCtorString: return weakMapTag;
+ }
+ }
+ return result;
+ };
+ }
+
+ module.exports = getTag;
+
+
+/***/ },
+/* 432 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var getNative = __webpack_require__(103),
+ root = __webpack_require__(109);
+
+ /* Built-in method references that are verified to be native. */
+ var DataView = getNative(root, 'DataView');
+
+ module.exports = DataView;
+
+
+/***/ },
+/* 433 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var getNative = __webpack_require__(103),
+ root = __webpack_require__(109);
+
+ /* Built-in method references that are verified to be native. */
+ var Promise = getNative(root, 'Promise');
+
+ module.exports = Promise;
+
+
+/***/ },
+/* 434 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var getNative = __webpack_require__(103),
+ root = __webpack_require__(109);
+
+ /* Built-in method references that are verified to be native. */
+ var Set = getNative(root, 'Set');
+
+ module.exports = Set;
+
+
+/***/ },
+/* 435 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var getNative = __webpack_require__(103),
+ root = __webpack_require__(109);
+
+ /* Built-in method references that are verified to be native. */
+ var WeakMap = getNative(root, 'WeakMap');
+
+ module.exports = WeakMap;
+
+
+/***/ },
+/* 436 */
+/***/ function(module, exports) {
+
+ /** Used for built-in method references. */
+ var objectProto = Object.prototype;
+
+ /**
+ * Used to resolve the
+ * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring)
+ * of values.
+ */
+ var objectToString = objectProto.toString;
+
+ /**
+ * The base implementation of `getTag`.
+ *
+ * @private
+ * @param {*} value The value to query.
+ * @returns {string} Returns the `toStringTag`.
+ */
+ function baseGetTag(value) {
+ return objectToString.call(value);
+ }
+
+ module.exports = baseGetTag;
+
+
+/***/ },
+/* 437 */
+/***/ function(module, exports) {
+
+ /**
+ * Converts `map` to its key-value pairs.
+ *
+ * @private
+ * @param {Object} map The map to convert.
+ * @returns {Array} Returns the key-value pairs.
+ */
+ function mapToArray(map) {
+ var index = -1,
+ result = Array(map.size);
+
+ map.forEach(function(value, key) {
+ result[++index] = [key, value];
+ });
+ return result;
+ }
+
+ module.exports = mapToArray;
+
+
+/***/ },
+/* 438 */
+/***/ function(module, exports) {
+
+ /**
+ * Converts `set` to its value-value pairs.
+ *
+ * @private
+ * @param {Object} set The set to convert.
+ * @returns {Array} Returns the value-value pairs.
+ */
+ function setToPairs(set) {
+ var index = -1,
+ result = Array(set.size);
+
+ set.forEach(function(value) {
+ result[++index] = [value, value];
+ });
+ return result;
+ }
+
+ module.exports = setToPairs;
+
+
+/***/ },
+/* 439 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var arrayLikeKeys = __webpack_require__(379),
+ baseKeys = __webpack_require__(440),
+ isArrayLike = __webpack_require__(367);
+
+ /**
+ * Creates an array of the own enumerable property names of `object`.
+ *
+ * **Note:** Non-object values are coerced to objects. See the
+ * [ES spec](http://ecma-international.org/ecma-262/7.0/#sec-object.keys)
+ * for more details.
+ *
+ * @static
+ * @since 0.1.0
+ * @memberOf _
+ * @category Object
+ * @param {Object} object The object to query.
+ * @returns {Array} Returns the array of property names.
+ * @example
+ *
+ * function Foo() {
+ * this.a = 1;
+ * this.b = 2;
+ * }
+ *
+ * Foo.prototype.c = 3;
+ *
+ * _.keys(new Foo);
+ * // => ['a', 'b'] (iteration order is not guaranteed)
+ *
+ * _.keys('hi');
+ * // => ['0', '1']
+ */
+ function keys(object) {
+ return isArrayLike(object) ? arrayLikeKeys(object) : baseKeys(object);
+ }
+
+ module.exports = keys;
+
+
+/***/ },
+/* 440 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var isPrototype = __webpack_require__(363),
+ nativeKeys = __webpack_require__(441);
+
+ /** Used for built-in method references. */
+ var objectProto = Object.prototype;
+
+ /** Used to check objects for own properties. */
+ var hasOwnProperty = objectProto.hasOwnProperty;
+
+ /**
+ * The base implementation of `_.keys` which doesn't treat sparse arrays as dense.
+ *
+ * @private
+ * @param {Object} object The object to query.
+ * @returns {Array} Returns the array of property names.
+ */
+ function baseKeys(object) {
+ if (!isPrototype(object)) {
+ return nativeKeys(object);
+ }
+ var result = [];
+ for (var key in Object(object)) {
+ if (hasOwnProperty.call(object, key) && key != 'constructor') {
+ result.push(key);
+ }
+ }
+ return result;
+ }
+
+ module.exports = baseKeys;
+
+
+/***/ },
+/* 441 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var overArg = __webpack_require__(7);
+
+ /* Built-in method references for those with the same name as other `lodash` methods. */
+ var nativeKeys = overArg(Object.keys, Object);
+
+ module.exports = nativeKeys;
+
+
+/***/ },
+/* 442 */
+/***/ function(module, exports) {
+
+ // removed by extract-text-webpack-plugin
+
+/***/ },
+/* 443 */,
+/* 444 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var React = __webpack_require__(2);
+ var dom = React.DOM;
+ var PropTypes = React.PropTypes;
+ var div = dom.div;
+
+ var _require = __webpack_require__(3);
+
+ var bindActionCreators = _require.bindActionCreators;
+
+ var _require2 = __webpack_require__(19);
+
+ var connect = _require2.connect;
+
+ var ImPropTypes = __webpack_require__(235);
+ var actions = __webpack_require__(262);
+
+ var _require3 = __webpack_require__(244);
+
+ var endTruncateStr = _require3.endTruncateStr;
+
+ var _require4 = __webpack_require__(277);
+
+ var getFilename = _require4.getFilename;
+
+ var _require5 = __webpack_require__(259);
+
+ var getFrames = _require5.getFrames;
+ var getSelectedFrame = _require5.getSelectedFrame;
+ var getSource = _require5.getSource;
+
+
+ if (typeof window == "object") {
+ __webpack_require__(445);
+ }
+
+ var NUM_FRAMES_SHOWN = 7;
+
+ function renderFrameTitle(frame) {
+ return div({ className: "title" }, endTruncateStr(frame.displayName, 40));
+ }
+
+ function renderFrameLocation(frame) {
+ var filename = getFilename(frame.source);
+ return div({ className: "location" }, `${ filename }: ${ frame.location.line }`);
+ }
+
+ var Frames = React.createClass({
+ propTypes: {
+ frames: ImPropTypes.list,
+ selectedFrame: PropTypes.object,
+ selectFrame: PropTypes.func.isRequired
+ },
+
+ displayName: "Frames",
+
+ getInitialState() {
+ return { showAllFrames: false };
+ },
+
+ toggleFramesDisplay() {
+ this.setState({
+ showAllFrames: !this.state.showAllFrames
+ });
+ },
+
+ renderFrame(frame) {
+ var _props = this.props;
+ var selectedFrame = _props.selectedFrame;
+ var selectFrame = _props.selectFrame;
+
+
+ var selectedClass = selectedFrame && (selectedFrame.id === frame.id ? "selected" : "");
+
+ return dom.li({ key: frame.id,
+ className: `frame ${ selectedClass }`,
+ onMouseDown: () => selectFrame(frame),
+ tabIndex: 0
+ }, renderFrameTitle(frame), renderFrameLocation(frame));
+ },
+
+ renderFrames() {
+ var frames = this.props.frames;
+
+ if (!frames) {
+ return null;
+ }
+
+ var numFramesToShow = this.state.showAllFrames ? frames.size : NUM_FRAMES_SHOWN;
+ frames = frames.slice(0, numFramesToShow);
+
+ return dom.ul({}, frames.map(frame => this.renderFrame(frame)));
+ },
+
+ renderToggleButton() {
+ var frames = this.props.frames;
+
+ var buttonMessage = this.state.showAllFrames ? L10N.getStr("callStack.collapse") : L10N.getStr("callStack.expand");
+
+ if (frames.size < NUM_FRAMES_SHOWN) {
+ return null;
+ }
+
+ return dom.div({ className: "show-more", onClick: this.toggleFramesDisplay }, buttonMessage);
+ },
+
+ render() {
+ var frames = this.props.frames;
+
+
+ if (!frames) {
+ return div({ className: "pane frames" }, div({ className: "pane-info empty" }, L10N.getStr("callStack.notPaused")));
+ }
+
+ return div({ className: "pane frames" }, this.renderFrames(), this.renderToggleButton());
+ }
+ });
+
+ function getAndProcessFrames(state) {
+ var frames = getFrames(state);
+ if (!frames) {
+ return null;
+ }
+ return frames.filter(frame => getSource(state, frame.location.sourceId)).map(frame => Object.assign({}, frame, {
+ source: getSource(state, frame.location.sourceId).toJS()
+ }));
+ }
+
+ module.exports = connect(state => ({
+ frames: getAndProcessFrames(state),
+ selectedFrame: getSelectedFrame(state)
+ }), dispatch => bindActionCreators(actions, dispatch))(Frames);
+
+/***/ },
+/* 445 */
+/***/ function(module, exports) {
+
+ // removed by extract-text-webpack-plugin
+
+/***/ },
+/* 446 */,
+/* 447 */
+/***/ function(module, exports, __webpack_require__) {
+
+ function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
+
+ var React = __webpack_require__(2);
+ var dom = React.DOM;
+ var PropTypes = React.PropTypes;
+ var div = dom.div;
+ var span = dom.span;
+
+ var Svg = __webpack_require__(310);
+
+ __webpack_require__(448);
+
+ var Accordion = React.createClass({
+ propTypes: {
+ items: PropTypes.array
+ },
+
+ displayName: "Accordion",
+
+ getInitialState: function () {
+ return { opened: this.props.items.map(item => item.opened),
+ created: [] };
+ },
+
+ handleHeaderClick: function (i) {
+ var opened = [].concat(_toConsumableArray(this.state.opened));
+ var created = [].concat(_toConsumableArray(this.state.created));
+ var item = this.props.items[i];
+
+ opened[i] = !opened[i];
+ created[i] = true;
+
+ if (opened[i] && item.onOpened) {
+ item.onOpened();
+ }
+
+ this.setState({ opened, created });
+ },
+
+ renderContainer: function (item, i) {
+ var _state = this.state;
+ var opened = _state.opened;
+ var created = _state.created;
+
+ var containerClassName = item.header.toLowerCase().replace(/\s/g, "-") + "-pane";
+
+ return div({ className: containerClassName, key: i }, div({ className: "_header",
+ onClick: () => this.handleHeaderClick(i) }, Svg("arrow", { className: opened[i] ? "expanded" : "" }), item.header, item.buttons ? dom.span({ className: "header-buttons" }, item.buttons.map((button, id) => span({ key: id }, button))) : null), created[i] || opened[i] ? div({ className: "_content",
+ style: { display: opened[i] ? "block" : "none" }
+ }, React.createElement(item.component, item.componentProps || {})) : null);
+ },
+
+ render: function () {
+ return div({ className: "accordion" }, this.props.items.map(this.renderContainer));
+ }
+ });
+
+ module.exports = Accordion;
+
+/***/ },
+/* 448 */
+/***/ function(module, exports) {
+
+ // removed by extract-text-webpack-plugin
+
+/***/ },
+/* 449 */,
+/* 450 */
+/***/ function(module, exports) {
+
+ // removed by extract-text-webpack-plugin
+
+/***/ },
+/* 451 */,
+/* 452 */
+/***/ function(module, exports, __webpack_require__) {
+
+ function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
+
+ var React = __webpack_require__(2);
+ var dom = React.DOM;
+ var PropTypes = React.PropTypes;
+
+ var ImPropTypes = __webpack_require__(235);
+
+ var _require = __webpack_require__(19);
+
+ var connect = _require.connect;
+
+ var _require2 = __webpack_require__(3);
+
+ var bindActionCreators = _require2.bindActionCreators;
+
+ var _require3 = __webpack_require__(259);
+
+ var getSelectedSource = _require3.getSelectedSource;
+ var getSourceTabs = _require3.getSourceTabs;
+ var getFileSearchState = _require3.getFileSearchState;
+
+ var _require4 = __webpack_require__(277);
+
+ var getFilename = _require4.getFilename;
+
+ var classnames = __webpack_require__(211);
+ var actions = __webpack_require__(262);
+
+ var _require5 = __webpack_require__(89);
+
+ var isEnabled = _require5.isEnabled;
+
+ var CloseButton = __webpack_require__(336);
+ var Svg = __webpack_require__(310);
+ var Dropdown = React.createFactory(__webpack_require__(453));
+
+ var _require6 = __webpack_require__(413);
+
+ var showMenu = _require6.showMenu;
+ var buildMenu = _require6.buildMenu;
+
+
+ __webpack_require__(454);
+ __webpack_require__(456);
+
+ /*
+ * Finds the hidden tabs by comparing the tabs' top offset.
+ * hidden tabs will have a great top offset.
+ *
+ * @param sourceTabs Immutable.list
+ * @param sourceTabEls HTMLCollection
+ *
+ * @returns Immutable.list
+ */
+ function getHiddenTabs(sourceTabs, sourceTabEls) {
+ sourceTabEls = [].slice.call(sourceTabEls);
+ function getTopOffset() {
+ var topOffsets = sourceTabEls.map(t => t.getBoundingClientRect().top);
+ return Math.min.apply(Math, _toConsumableArray(topOffsets));
+ }
+
+ var tabTopOffset = getTopOffset();
+ return sourceTabs.filter((tab, index) => {
+ return sourceTabEls[index].getBoundingClientRect().top > tabTopOffset;
+ });
+ }
+
+ var SourceTabs = React.createClass({
+ propTypes: {
+ sourceTabs: ImPropTypes.list,
+ selectedSource: ImPropTypes.map,
+ selectSource: PropTypes.func.isRequired,
+ closeTab: PropTypes.func.isRequired,
+ toggleFileSearch: PropTypes.func.isRequired
+ },
+
+ displayName: "SourceTabs",
+
+ getInitialState() {
+ return {
+ dropdownShown: false,
+ hiddenSourceTabs: null
+ };
+ },
+
+ componentDidUpdate() {
+ this.updateHiddenSourceTabs(this.props.sourceTabs);
+ },
+
+ onTabContextMenu(event, tab) {
+ event.preventDefault();
+ this.showContextMenu(event, tab);
+ },
+
+ showContextMenu(e, tab) {
+ var _props = this.props;
+ var closeTab = _props.closeTab;
+ var sourceTabs = _props.sourceTabs;
+
+
+ var closeTabLabel = L10N.getStr("sourceTabs.closeTab");
+ var closeOtherTabsLabel = L10N.getStr("sourceTabs.closeOtherTabs");
+ var closeTabsToRightLabel = L10N.getStr("sourceTabs.closeTabsToRight");
+ var closeAllTabsLabel = L10N.getStr("sourceTabs.closeAllTabs");
+
+ var tabs = sourceTabs.map(t => t.get("id"));
+
+ var closeTabMenuItem = {
+ id: "node-menu-close-tab",
+ label: closeTabLabel,
+ accesskey: "C",
+ disabled: false,
+ click: () => closeTab(tab)
+ };
+
+ var closeOtherTabsMenuItem = {
+ id: "node-menu-close-other-tabs",
+ label: closeOtherTabsLabel,
+ accesskey: "O",
+ disabled: false,
+ click: () => {
+ tabs.forEach(t => {
+ if (t !== tab) {
+ closeTab(t);
+ }
+ });
+ }
+ };
+
+ var closeTabsToRightMenuItem = {
+ id: "node-menu-close-tabs-to-right",
+ label: closeTabsToRightLabel,
+ accesskey: "R",
+ disabled: false,
+ click: () => {
+ tabs.reverse().every(t => {
+ if (t === tab) {
+ return false;
+ }
+ closeTab(t);
+ return true;
+ });
+ }
+ };
+
+ var closeAllTabsMenuItem = {
+ id: "node-menu-close-all-tabs",
+ label: closeAllTabsLabel,
+ accesskey: "A",
+ disabled: false,
+ click: () => tabs.forEach(closeTab)
+ };
+
+ showMenu(e, buildMenu([{ item: closeTabMenuItem }, { item: closeOtherTabsMenuItem, hidden: () => tabs.size === 1 }, { item: closeTabsToRightMenuItem, hidden: () => tabs.some((t, i) => t === tab && tabs.size - 1 === i) }, { item: closeAllTabsMenuItem }]));
+ },
+
+ /*
+ * Updates the hiddenSourceTabs state, by
+ * finding the source tabs who have wrapped and are not on the top row.
+ */
+ updateHiddenSourceTabs(sourceTabs) {
+ if (!this.refs.sourceTabs) {
+ return;
+ }
+
+ var sourceTabEls = this.refs.sourceTabs.children;
+ var hiddenSourceTabs = getHiddenTabs(sourceTabs, sourceTabEls);
+
+ if (!hiddenSourceTabs.equals(this.state.hiddenSourceTabs)) {
+ this.setState({ hiddenSourceTabs });
+ }
+ },
+
+ toggleSourcesDropdown(e) {
+ this.setState({
+ dropdownShown: !this.state.dropdownShown
+ });
+ },
+
+ renderDropdownSource(source) {
+ var selectSource = this.props.selectSource;
+
+ var filename = getFilename(source.toJS());
+
+ return dom.li({
+ key: source.get("id"),
+ onClick: () => {
+ // const tabIndex = getLastVisibleTabIndex(sourceTabs, sourceTabEls);
+ var tabIndex = 0;
+ selectSource(source.get("id"), { tabIndex });
+ }
+ }, filename);
+ },
+
+ renderTabs() {
+ var sourceTabs = this.props.sourceTabs;
+ return dom.div({ className: "source-tabs", ref: "sourceTabs" }, sourceTabs.map(this.renderTab));
+ },
+
+ renderTab(source) {
+ var _props2 = this.props;
+ var selectedSource = _props2.selectedSource;
+ var selectSource = _props2.selectSource;
+ var closeTab = _props2.closeTab;
+
+ var filename = getFilename(source.toJS());
+ var active = source.get("id") == selectedSource.get("id");
+
+ function onClickClose(ev) {
+ ev.stopPropagation();
+ closeTab(source.get("id"));
+ }
+
+ return dom.div({
+ className: classnames("source-tab", { active }),
+ key: source.get("id"),
+ onClick: () => selectSource(source.get("id")),
+ onContextMenu: e => this.onTabContextMenu(e, source.get("id")),
+ title: source.get("url")
+ }, dom.div({ className: "filename" }, filename), CloseButton({ handleClick: onClickClose }));
+ },
+
+ renderNewButton() {
+ return dom.div({
+ className: "new-tab-btn",
+ onClick: () => this.props.toggleFileSearch(true)
+ }, Svg("plus"));
+ },
+
+ renderDropdown() {
+ var hiddenSourceTabs = this.state.hiddenSourceTabs;
+ if (!hiddenSourceTabs || hiddenSourceTabs.size == 0) {
+ return dom.div({});
+ }
+
+ return Dropdown({
+ panel: dom.ul({}, this.state.hiddenSourceTabs.map(this.renderDropdownSource))
+ });
+ },
+
+ render() {
+ if (!isEnabled("tabs")) {
+ return dom.div({ className: "source-header" });
+ }
+
+ return dom.div({ className: "source-header" }, this.renderTabs(), this.renderNewButton(), this.renderDropdown());
+ }
+ });
+
+ module.exports = connect(state => ({
+ selectedSource: getSelectedSource(state),
+ sourceTabs: getSourceTabs(state),
+ searchOn: getFileSearchState(state)
+ }), dispatch => bindActionCreators(actions, dispatch))(SourceTabs);
+
+/***/ },
+/* 453 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var React = __webpack_require__(2);
+ var dom = React.DOM;
+ var PropTypes = React.PropTypes;
+
+
+ var Dropdown = React.createClass({
+ propTypes: {
+ panel: PropTypes.object
+ },
+
+ displayName: "Dropdown",
+
+ getInitialState() {
+ return {
+ dropdownShown: false
+ };
+ },
+
+ toggleDropdown(e) {
+ this.setState({
+ dropdownShown: !this.state.dropdownShown
+ });
+ },
+
+ renderPanel() {
+ return dom.div({
+ className: "dropdown",
+ onClick: this.toggleDropdown,
+ style: { display: this.state.dropdownShown ? "block" : "none" }
+ }, this.props.panel);
+ },
+
+ renderButton() {
+ return dom.span({
+ className: "dropdown-button",
+ onClick: this.toggleDropdown
+ }, "»");
+ },
+
+ renderMask() {
+ return dom.div({
+ className: "dropdown-mask",
+ onClick: this.toggleDropdown,
+ style: { display: this.state.dropdownShown ? "block" : "none" }
+ });
+ },
+
+ render() {
+ return dom.div({}, this.renderPanel(), this.renderButton(), this.renderMask());
+ }
+
+ });
+
+ module.exports = Dropdown;
+
+/***/ },
+/* 454 */
+/***/ function(module, exports) {
+
+ // removed by extract-text-webpack-plugin
+
+/***/ },
+/* 455 */,
+/* 456 */
+/***/ function(module, exports) {
+
+ // removed by extract-text-webpack-plugin
+
+/***/ },
+/* 457 */,
+/* 458 */
+/***/ function(module, exports) {
+
+ module.exports = {
+ "breakpoints.header": "Breakpoints",
+ "callStack.header": "Call Stack",
+ "callStack.notPaused": "Not Paused",
+ "callStack.collapse": "Collapse Rows",
+ "callStack.expand": "Expand Rows",
+ "editor.searchResults": "%d of %d results",
+ "editor.noResults": "no results",
+ "editor.addBreakpoint": "Add Breakpoint",
+ "editor.removeBreakpoint": "Remove Breakpoint",
+ "editor.editBreakpoint": "Edit Breakpoint",
+ "editor.addConditionalBreakpoint": "Add Conditional Breakpoint",
+ "scopes.header": "Scopes",
+ "scopes.notAvailable": "Scopes Unavailable",
+ "scopes.notPaused": "Not Paused",
+ "sources.header": "Sources",
+ "sources.search": "%S to search",
+ "watchExpressions.header": "Watch Expressions",
+ "welcome.search": "%S to search for files",
+ "sourceSearch.search": "Search...",
+ "sourceSearch.resultsSummary": "%d instances of \"%S\"",
+ "sourceSearch.noResults": "No files matching %S found",
+ "ignoreExceptions": "Ignore exceptions. Click to pause on uncaught exceptions",
+ "pauseOnUncaughtExceptions": "Pause on uncaught exceptions. Click to pause on all exceptions",
+ "pauseOnExceptions": "Pause on all exceptions. Click to ignore exceptions",
+ "stepOutTooltip": "Step Out (%SF11)",
+ "stepInTooltip": "Step In (%SF11)",
+ "stepOverTooltip": "Step Over (F10)",
+ "resumeButtonTooltip": "Click to resume (F8)",
+ "pausePendingButtonTooltip": "Waiting for next execution",
+ "pauseButtonTooltip": "Click to pause (F8)",
+ "sourceTabs.closeTab": "Close tab",
+ "sourceTabs.closeOtherTabs": "Close others",
+ "sourceTabs.closeTabsToRight": "Close tabs to the right",
+ "sourceTabs.closeAllTabs": "Close all tabs"
+ };
+
+/***/ },
+/* 459 */
+/***/ function(module, exports, __webpack_require__) {
+
+ function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
+
+ var _require = __webpack_require__(30);
+
+ var sprintf = _require.sprintf;
+
+ var strings = {};
+
+ function setBundle(bundle) {
+ strings = bundle;
+ }
+
+ function getStr(key) {
+ if (!strings[key]) {
+ throw new Error(`L10N key ${ key } cannot be found.`);
+ }
+ return strings[key];
+ }
+
+ function getFormatStr(name) {
+ for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
+ args[_key - 1] = arguments[_key];
+ }
+
+ return sprintf.apply(undefined, [getStr(name)].concat(_toConsumableArray(args)));
+ }
+
+ module.exports = {
+ getStr,
+ getFormatStr,
+ setBundle
+ };
+
+/***/ }
+/******/ ]);
+//# sourceMappingURL=bundle.js.map
diff --git a/devtools/client/debugger/new/images/Icons.js b/devtools/client/debugger/new/images/Icons.js
new file mode 100644
index 000000000..bda48ef93
--- /dev/null
+++ b/devtools/client/debugger/new/images/Icons.js
@@ -0,0 +1,46 @@
+const React = require("react");
+const InlineSVG = require("svg-inline-react");
+const { DOM: dom } = React;
+
+const DomainIcon = props => {
+ return dom.span(
+ props,
+ React.createElement(InlineSVG, {
+ src: require("./domain.svg")
+ })
+ );
+};
+
+const FileIcon = props => {
+ return dom.span(
+ props,
+ React.createElement(InlineSVG, {
+ src: require("./file.svg")
+ })
+ );
+};
+
+const FolderIcon = props => {
+ return dom.span(
+ props,
+ React.createElement(InlineSVG, {
+ src: require("./folder.svg")
+ })
+ );
+};
+
+const WorkerIcon = props => {
+ return dom.span(
+ props,
+ React.createElement(InlineSVG, {
+ src: require("./worker.svg")
+ })
+ );
+};
+
+module.exports = {
+ DomainIcon,
+ FileIcon,
+ FolderIcon,
+ WorkerIcon
+};
diff --git a/devtools/client/debugger/new/images/Svg.js b/devtools/client/debugger/new/images/Svg.js
new file mode 100644
index 000000000..775aecfc0
--- /dev/null
+++ b/devtools/client/debugger/new/images/Svg.js
@@ -0,0 +1,43 @@
+const React = require("react");
+const InlineSVG = require("svg-inline-react");
+
+const svg = {
+ "angle-brackets": require("./angle-brackets.svg"),
+ "arrow": require("./arrow.svg"),
+ "blackBox": require("./blackBox.svg"),
+ "breakpoint": require("./breakpoint.svg"),
+ "close": require("./close.svg"),
+ "domain": require("./domain.svg"),
+ "file": require("./file.svg"),
+ "folder": require("./folder.svg"),
+ "globe": require("./globe.svg"),
+ "magnifying-glass": require("./magnifying-glass.svg"),
+ "pause": require("./pause.svg"),
+ "pause-exceptions": require("./pause-exceptions.svg"),
+ "plus": require("./plus.svg"),
+ "prettyPrint": require("./prettyPrint.svg"),
+ "resume": require("./resume.svg"),
+ "settings": require("./settings.svg"),
+ "stepIn": require("./stepIn.svg"),
+ "stepOut": require("./stepOut.svg"),
+ "stepOver": require("./stepOver.svg"),
+ "subSettings": require("./subSettings.svg"),
+ "toggleBreakpoints": require("./toggle-breakpoints.svg"),
+ "worker": require("./worker.svg"),
+ "sad-face": require("./sad-face.svg")
+};
+
+module.exports = function(name, props) { // eslint-disable-line
+ if (!svg[name]) {
+ throw new Error("Unknown SVG: " + name);
+ }
+ let className = name;
+ if (props && props.className) {
+ className = `${name} ${props.className}`;
+ }
+ if (name === "subSettings") {
+ className = "";
+ }
+ props = Object.assign({}, props, { className, src: svg[name] });
+ return React.createElement(InlineSVG, props);
+};
diff --git a/devtools/client/debugger/new/images/angle-brackets.svg b/devtools/client/debugger/new/images/angle-brackets.svg
new file mode 100644
index 000000000..b353bee9e
--- /dev/null
+++ b/devtools/client/debugger/new/images/angle-brackets.svg
@@ -0,0 +1,9 @@
+<!-- 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/. -->
+<svg width="16px" height="11px" viewBox="-1 73 16 11" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <g id="Shape-Copy-3-+-Shape-Copy-4" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" transform="translate(0.000000, 74.000000)">
+ <path d="M0.749321284,4.16081709 L4.43130681,0.242526751 C4.66815444,-0.00952143591 5.06030999,-0.0211407611 5.30721074,0.216574262 C5.55411149,0.454289284 5.56226116,0.851320812 5.32541353,1.103369 L1.95384971,4.69131519 L5.48809879,8.09407556 C5.73499955,8.33179058 5.74314922,8.72882211 5.50630159,8.9808703 C5.26945396,9.23291849 4.87729841,9.24453781 4.63039766,9.00682279 L0.827097345,5.34502101 C0.749816996,5.31670099 0.677016974,5.27216098 0.613753508,5.21125118 C0.427367989,5.03179997 0.377040713,4.7615583 0.465458792,4.53143559 C0.492371834,4.43667624 0.541703274,4.34676528 0.613628034,4.27022448 C0.654709457,4.22650651 0.70046335,4.19002189 0.749321284,4.16081709 Z" id="Shape-Copy-3" stroke="#FFFFFF" stroke-width="0.05" fill="#DDE1E4"></path>
+ <path d="M13.7119065,5.44453032 L9.77062746,9.09174784 C9.51677479,9.3266604 9.12476399,9.31089603 8.89504684,9.05653714 C8.66532968,8.80217826 8.68489539,8.40554539 8.93874806,8.17063283 L12.5546008,4.82456128 L9.26827469,1.18571135 C9.03855754,0.931352463 9.05812324,0.534719593 9.31197591,0.299807038 C9.56582858,0.0648944831 9.95783938,0.0806588502 10.1875565,0.335017737 L13.72891,4.25625178 C13.8013755,4.28980469 13.8684335,4.3382578 13.9254821,4.40142604 C14.0883019,4.58171146 14.1258883,4.83347168 14.0435812,5.04846202 C14.0126705,5.15680232 13.9526426,5.2583679 13.8641331,5.34027361 C13.8174417,5.38348136 13.7660763,5.41820853 13.7119065,5.44453032 Z" id="Shape-Copy-4" stroke="#FFFFFF" stroke-width="0.05" fill="#DDE1E4"></path>
+ </g>
+</svg>
diff --git a/devtools/client/debugger/new/images/arrow.svg b/devtools/client/debugger/new/images/arrow.svg
new file mode 100644
index 000000000..33a107797
--- /dev/null
+++ b/devtools/client/debugger/new/images/arrow.svg
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 16 16">
+ <path d="M8 13.4c-.5 0-.9-.2-1.2-.6L.4 5.2C0 4.7-.1 4.3.2 3.7S1 3 1.6 3h12.8c.6 0 1.2.1 1.4.7.3.6.2 1.1-.2 1.6l-6.4 7.6c-.3.4-.7.5-1.2.5z"/>
+</svg> \ No newline at end of file
diff --git a/devtools/client/debugger/new/images/blackBox.svg b/devtools/client/debugger/new/images/blackBox.svg
new file mode 100644
index 000000000..b98d62f13
--- /dev/null
+++ b/devtools/client/debugger/new/images/blackBox.svg
@@ -0,0 +1,9 @@
+<!-- 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/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
+ <g fill-rule="evenodd">
+ <circle cx="8" cy="8.5" r="1.5"/>
+ <path d="M15.498 8.28l-.001-.03v-.002-.004l-.002-.018-.004-.031c0-.002 0-.002 0 0l-.004-.035.006.082c-.037-.296-.133-.501-.28-.661-.4-.522-.915-1.042-1.562-1.604-1.36-1.182-2.74-1.975-4.178-2.309a6.544 6.544 0 0 0-2.755-.042c-.78.153-1.565.462-2.369.91C3.252 5.147 2.207 6 1.252 7.035c-.216.233-.36.398-.499.577-.338.437-.338 1 0 1.437.428.552.941 1.072 1.59 1.635 1.359 1.181 2.739 1.975 4.177 2.308.907.21 1.829.223 2.756.043.78-.153 1.564-.462 2.369-.91 1.097-.612 2.141-1.464 3.097-2.499.217-.235.36-.398.498-.578.12-.128.216-.334.248-.554 0 .01 0 .01-.008.04l.013-.079-.001.011.003-.031.001-.017v.005l.001-.02v.008l.002-.03.001-.05-.001-.044v-.004-.004zm-.954.045v.007l.001.004V8.33v.012l-.001.01v-.005-.005l.002-.015-.001.008c-.002.014-.002.014 0 0l-.007.084c.003-.057-.004-.041-.014-.031-.143.182-.27.327-.468.543-.89.963-1.856 1.752-2.86 2.311-.724.404-1.419.677-2.095.81a5.63 5.63 0 0 1-2.374-.036c-1.273-.295-2.523-1.014-3.774-2.101-.604-.525-1.075-1.001-1.457-1.496-.054-.07-.054-.107 0-.177.117-.152.244-.298.442-.512.89-.963 1.856-1.752 2.86-2.311.724-.404 1.419-.678 2.095-.81a5.631 5.631 0 0 1 2.374.036c1.272.295 2.523 1.014 3.774 2.101.603.524 1.074 1 1.457 1.496.035.041.043.057.046.076 0 .01 0 .01.008.043l-.009-.047.003.02-.002-.013v-.008.016c0-.004 0-.004 0 0v-.004z"/>
+ </g>
+</svg>
diff --git a/devtools/client/debugger/new/images/breakpoint.svg b/devtools/client/debugger/new/images/breakpoint.svg
new file mode 100644
index 000000000..f0e5de106
--- /dev/null
+++ b/devtools/client/debugger/new/images/breakpoint.svg
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 60 12">
+ <path id="base-path" d="M53.9,0H1C0.4,0,0,0.4,0,1v10c0,0.6,0.4,1,1,1h52.9c0.6,0,1.2-0.3,1.5-0.7L60,6l-4.4-5.3C55,0.3,54.5,0,53.9,0z"/>
+</svg>
diff --git a/devtools/client/debugger/new/images/close.svg b/devtools/client/debugger/new/images/close.svg
new file mode 100644
index 000000000..7efd07f80
--- /dev/null
+++ b/devtools/client/debugger/new/images/close.svg
@@ -0,0 +1,7 @@
+<!-- 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/. -->
+<svg width="16px" height="16px" viewBox="0 0 6 6" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <path d="M1.35191454,5.27895256 L5.31214367,1.35518468 C5.50830675,1.16082764 5.50977084,0.844248536 5.3154138,0.648085456 C5.12105677,0.451922377 4.80447766,0.450458288 4.60831458,0.644815324 L0.648085456,4.56858321 C0.451922377,4.76294025 0.450458288,5.07951935 0.644815324,5.27568243 C0.83917236,5.47184551 1.15575146,5.4733096 1.35191454,5.27895256 L1.35191454,5.27895256 Z" id="Line" stroke="none" fill="#696969" fill-rule="evenodd"></path>
+ <path d="M5.31214367,4.56858321 L1.35191454,0.644815324 C1.15575146,0.450458288 0.83917236,0.451922377 0.644815324,0.648085456 C0.450458288,0.844248536 0.451922377,1.16082764 0.648085456,1.35518468 L4.60831458,5.27895256 C4.80447766,5.4733096 5.12105677,5.47184551 5.3154138,5.27568243 C5.50977084,5.07951935 5.50830675,4.76294025 5.31214367,4.56858321 L5.31214367,4.56858321 Z" id="Line-Copy-2" stroke="none" fill="#696969" fill-rule="evenodd"></path>
+</svg>
diff --git a/devtools/client/debugger/new/images/disableBreakpoints.svg b/devtools/client/debugger/new/images/disableBreakpoints.svg
new file mode 100644
index 000000000..bdf28ffcf
--- /dev/null
+++ b/devtools/client/debugger/new/images/disableBreakpoints.svg
@@ -0,0 +1,8 @@
+<!-- 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/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="##4A464C">
+ <g fill-rule="evenodd">
+ <path d="M3.233 11.25l-.417 1H1.712C.763 12.25 0 11.574 0 10.747V6.503C0 5.675.755 5 1.712 5h4.127l-.417 1H1.597C1.257 6 1 6.225 1 6.503v4.244c0 .277.267.503.597.503h1.636zM7.405 11.27L7 12.306c.865.01 2.212-.024 2.315-.04.112-.016.112-.016.185-.035.075-.02.156-.046.251-.082.152-.056.349-.138.592-.244.415-.182.962-.435 1.612-.744l.138-.066a179.35 179.35 0 0 0 2.255-1.094c1.191-.546 1.191-2.074-.025-2.632l-.737-.34a3547.554 3547.554 0 0 0-3.854-1.78c-.029.11-.065.222-.11.336l-.232.596c.894.408 4.56 2.107 4.56 2.107.458.21.458.596 0 .806L9.197 11.27H7.405zM4.462 14.692l5-12a.5.5 0 1 0-.924-.384l-5 12a.5.5 0 1 0 .924.384z"/>
+ </g>
+</svg>
diff --git a/devtools/client/debugger/new/images/domain.svg b/devtools/client/debugger/new/images/domain.svg
new file mode 100644
index 000000000..f00c9b37d
--- /dev/null
+++ b/devtools/client/debugger/new/images/domain.svg
@@ -0,0 +1,7 @@
+<!-- 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/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
+ <path d="M9.05 4.634l-2.144.003-.116.116v1.445l.92.965.492.034.116-.116v-.617L9.13 5.7l.035-.95M12.482 10.38l-1.505-1.462H9.362l-.564.516-.034 1.108.72.768 1.323.034-.117-.116v1.2l.972 1.02.315.034.116-.116v-1.154l.422-.374.034-.927-.117.117h.26l.408-.36V10.5l-.125-.124-.575-.033"/>
+ <path d="M8.47 15.073c-3.088 0-5.6-2.513-5.6-5.602V9.4v-.003c0-.018 0-.018.002-.034l.182-.088.724.587.49.033.497.543-.034.9.317.383h.47l.114.096-.032 1.9.524.553h.105l.025-.338 1.004-.95.054-.474.53-.462v-.888l-.588-.038-1.118-1.155H4.48l-.154-.09V9.01l.155-.1h1.164v-.273l.12-.115.7.033.494-.443.034-.746-.624-.655h-.724v.28l-.11.07H4.64l-.114-.09.025-.64.48-.43v-.244h-.382c-.102 0-.152-.128-.08-.2 1.04-1.01 2.428-1.59 3.903-1.59 1.374 0 2.672.5 3.688 1.39.08.068.03.198-.075.198l-1.144-.034-.81.803.52.523v.16l-.382.388h-.158l-.176-.177v-.16l.076-.074-.252-.252-.37.362.53.53c.072.072.005.194-.096.194l-.752-.005v.844h.783L9.885 8l.16-.143h.16l.62.61v.267l.58.027.003.002V8.76l.18-.03 1.234 1.24.753-.708h.382l.116.108c0 .02.003.016.003.036v.065c0 3.09-2.515 5.603-5.605 5.603M8.47 3C4.904 3 2 5.903 2 9.47c0 3.57 2.903 6.472 6.47 6.472 3.57 0 6.472-2.903 6.472-6.47C14.942 5.9 12.04 3 8.472 3"/>
+</svg>
diff --git a/devtools/client/debugger/new/images/favicon.png b/devtools/client/debugger/new/images/favicon.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/devtools/client/debugger/new/images/favicon.png
diff --git a/devtools/client/debugger/new/images/file.svg b/devtools/client/debugger/new/images/file.svg
new file mode 100644
index 000000000..7f5a70855
--- /dev/null
+++ b/devtools/client/debugger/new/images/file.svg
@@ -0,0 +1,7 @@
+<!-- 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/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
+ <path d="M4 2v12h9V4.775L9.888 2H4zm0-1h5.888c.246 0 .483.09.666.254l3.112 2.774c.212.19.334.462.334.747V14c0 .552-.448 1-1 1H4c-.552 0-1-.448-1-1V2c0-.552.448-1 1-1z"/>
+ <path d="M9 1.5v4c0 .325.306.564.62.485l4-1c.27-.067.432-.338.365-.606-.067-.27-.338-.432-.606-.365l-4 1L10 5.5v-4c0-.276-.224-.5-.5-.5s-.5.224-.5.5z"/>
+</svg>
diff --git a/devtools/client/debugger/new/images/folder.svg b/devtools/client/debugger/new/images/folder.svg
new file mode 100644
index 000000000..6b8ef6ac3
--- /dev/null
+++ b/devtools/client/debugger/new/images/folder.svg
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
+ <path d="M2 5.193v7.652c0 .003-.002 0 .007 0H14v-7.69c0-.003.002 0-.007 0h-7.53v-2.15c0-.002-.004-.005-.01-.005H2.01C2 3 2 3 2 3.005V5.193zm-1 0V3.005C1 2.45 1.444 2 2.01 2h4.442c.558 0 1.01.45 1.01 1.005v1.15h6.53c.557 0 1.008.44 1.008 1v7.69c0 .553-.45 1-1.007 1H2.007c-.556 0-1.007-.44-1.007-1V5.193zM6.08 4.15H2v1h4.46v-1h-.38z" fill-rule="evenodd"/>
+</svg>
diff --git a/devtools/client/debugger/new/images/globe.svg b/devtools/client/debugger/new/images/globe.svg
new file mode 100644
index 000000000..d513a659f
--- /dev/null
+++ b/devtools/client/debugger/new/images/globe.svg
@@ -0,0 +1,10 @@
+<!-- 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/. -->
+<svg width="13px" height="12px" viewBox="14 6 13 12" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <g id="world" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" transform="translate(14.000000, 6.000000)" stroke-linecap="round" stroke-linejoin="round">
+ <path d="M6.35076107,0.354 C3.25095418,0.354 0.729,2.87582735 0.729,5.9758879 C0.729,9.07544113 3.25082735,11.5972685 6.35076107,11.5972685 C9.45044113,11.5972685 11.9723953,9.07544113 11.9723953,5.97576107 C11.9723953,2.87582735 9.45044113,0.354 6.35076107,0.354 L6.35076107,0.354 Z M6.35076107,10.8289121 C3.67445071,10.8289121 1.49722956,8.65181776 1.49722956,5.97576107 C1.49722956,5.9443064 1.49900522,5.91335907 1.49976622,5.88215806 L2.20090094,6.4213266 L2.56313696,6.4213266 L2.97268183,6.8306178 L2.97268183,7.68217686 L3.32324919,8.03287105 L3.73926255,8.03287105 L3.73926255,9.79940584 L4.27386509,10.3361645 L4.4591686,10.3361645 L4.4591686,10.000183 L5.37655417,9.08343163 L5.37655417,8.73400577 L5.85585737,8.25203907 L5.85585737,7.37206934 L5.32518666,7.37206934 L4.28439226,6.33140176 L2.82225748,6.33140176 L2.82225748,5.56938704 L3.96286973,5.56938704 L3.96286973,5.23949352 L4.65068695,5.23949352 L5.11477015,4.77667865 L5.11477015,4.03001076 L4.49087694,3.40662489 L3.75359472,3.40662489 L3.75359472,3.78725175 L2.96228149,3.78725175 L2.96228149,3.28385021 L3.42217919,2.82319151 L3.42217919,2.49786399 L2.97001833,2.49786399 C3.84466106,1.64744643 5.03714814,1.12222956 6.35063424,1.12222956 C7.57292716,1.12222956 8.69020207,1.57730759 9.54442463,2.32587797 L8.46164839,2.32587797 L7.680355,3.10666403 L8.21508437,3.64088607 L7.87238068,3.98257509 L7.7165025,3.82669692 L7.85297518,3.68946324 L7.78930484,3.62566607 L7.78943167,3.62566607 L7.56011699,3.39559038 L7.55986332,3.39571722 L7.49758815,3.33318838 L7.01904595,3.78585658 L7.55910232,4.32654712 L6.8069806,4.32198112 L6.8069806,5.25864535 L7.66716433,5.25864535 L7.6723645,4.72112565 L7.81289584,4.57996014 L8.31819988,5.08653251 L8.31819988,5.41921636 L9.00703176,5.41921636 L9.03366676,5.39321553 L9.03430093,5.39194719 L10.195587,6.55259911 L10.8637451,5.88520206 L11.2018828,5.88520206 C11.2023901,5.9153884 11.2041658,5.94532107 11.2041658,5.97563424 C11.2040389,8.65181776 9.0269446,10.8289121 6.35076107,10.8289121 L6.35076107,10.8289121 Z" id="Shape" stroke="#DDE1E5" stroke-width="0.25" fill="#DDE1E5"></path>
+ <polygon id="Shape" stroke="#DDE1E5" stroke-width="0.25" fill="#DDE1E5" points="6.50676608 1.61523076 4.52892694 1.61789426 4.52892694 2.95192735 5.34560683 3.76733891 5.72496536 3.76733891 5.72496536 3.1967157 6.50676608 2.41592965"></polygon>
+ <polygon id="Shape" stroke="#DDE1E5" stroke-width="0.25" fill="#DDE1E5" points="9.59959714 6.88718547 8.28623788 5.57268471 8.28623788 5.57002121 6.79607294 5.57002121 6.35101474 6.01469891 6.35101474 6.96201714 6.98429362 7.59466185 8.12909136 7.59466185 8.12909136 8.70343893 8.99434843 9.56882283 9.20971144 9.56882283 9.20971144 8.50329592 9.63029081 8.08271655 9.63029081 7.3026915 9.87025949 7.3026915 10.1711082 7.00082814 10.0558167 6.88718547"></polygon>
+ </g>
+</svg>
diff --git a/devtools/client/debugger/new/images/magnifying-glass.svg b/devtools/client/debugger/new/images/magnifying-glass.svg
new file mode 100644
index 000000000..856013283
--- /dev/null
+++ b/devtools/client/debugger/new/images/magnifying-glass.svg
@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+ <path class="st0" d="M9 9.3l3.6 3.6"/>
+ <ellipse fill="transparent" cx="5.9" cy="6.2" rx="4.5" ry="4.5"/>
+</svg>
diff --git a/devtools/client/debugger/new/images/pause-circle.svg b/devtools/client/debugger/new/images/pause-circle.svg
new file mode 100644
index 000000000..2d4c116e5
--- /dev/null
+++ b/devtools/client/debugger/new/images/pause-circle.svg
@@ -0,0 +1,10 @@
+<!-- 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/. -->
+<svg width="16px" height="15px" viewBox="975 569 11 11" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <g id="Pause-circle" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" transform="translate(976.000000, 570.000000)">
+ <path d="M4.5,0.538639227 C2.3152037,0.538639227 0.538639227,2.31614868 0.538639227,4.5 C0.538639227,6.6847963 2.3152037,8.46136077 4.5,8.46136077 C6.6847963,8.46136077 8.46136077,6.6847963 8.46136077,4.5 C8.46136077,2.31614868 6.6847963,0.538639227 4.5,0.538639227 M4.5,9 C2.01847963,9 0,6.98152037 0,4.5 C0,2.01847963 2.01847963,0 4.5,0 C6.98152037,0 9,2.01847963 9,4.5 C9,6.98152037 6.98152037,9 4.5,9" id="Fill-1-Copy" stroke="#4990E2" stroke-width="0.5" fill="#4990E2"></path>
+ <path d="M3,3 L3,6.5" id="Line" stroke="#4990E2" stroke-width="1.15" stroke-linecap="round"></path>
+ <path d="M6,3 L6,6.5" id="Line" stroke="#4990E2" stroke-width="1.15" stroke-linecap="round"></path>
+ </g>
+</svg>
diff --git a/devtools/client/debugger/new/images/pause-exceptions.svg b/devtools/client/debugger/new/images/pause-exceptions.svg
new file mode 100644
index 000000000..8a0eb2c83
--- /dev/null
+++ b/devtools/client/debugger/new/images/pause-exceptions.svg
@@ -0,0 +1,7 @@
+<!-- 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/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
+ <path d="M10.483 13.995H5.517l-3.512-3.512V5.516l3.512-3.512h4.966l3.512 3.512v4.967l-3.512 3.512zm4.37-9.042l-3.807-3.805A.503.503 0 0 0 10.691 1H5.309a.503.503 0 0 0-.356.148L1.147 4.953A.502.502 0 0 0 1 5.308v5.383c0 .134.053.262.147.356l3.806 3.806a.503.503 0 0 0 .356.147h5.382a.503.503 0 0 0 .355-.147l3.806-3.806A.502.502 0 0 0 15 10.69V5.308a.502.502 0 0 0-.147-.355z"/>
+ <path d="M10 10.5a.5.5 0 1 0 1 0v-5a.5.5 0 1 0-1 0v5zM5 10.5a.5.5 0 1 0 1 0v-5a.5.5 0 0 0-1 0v5z"/>
+</svg>
diff --git a/devtools/client/debugger/new/images/pause.svg b/devtools/client/debugger/new/images/pause.svg
new file mode 100644
index 000000000..b27bf2a85
--- /dev/null
+++ b/devtools/client/debugger/new/images/pause.svg
@@ -0,0 +1,8 @@
+<!-- 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/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
+ <g fill-rule="evenodd">
+ <path d="M6.5 12.003l.052-9a.5.5 0 1 0-1-.006l-.052 9a.5.5 0 1 0 1 .006zM13 11.997l-.05-9a.488.488 0 0 0-.477-.497.488.488 0 0 0-.473.503l.05 9a.488.488 0 0 0 .477.497.488.488 0 0 0 .473-.503z"/>
+ </g>
+</svg>
diff --git a/devtools/client/debugger/new/images/play.svg b/devtools/client/debugger/new/images/play.svg
new file mode 100644
index 000000000..21ffb0fe5
--- /dev/null
+++ b/devtools/client/debugger/new/images/play.svg
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="#696969">
+ <path d="M4 13l7.778-5L4 3v10zm-1 0V3a1 1 0 0 1 1.54-.841l7.779 5a1 1 0 0 1 0 1.682l-7.778 5A1 1 0 0 1 3 13z" fill-rule="evenodd"/>
+</svg>
diff --git a/devtools/client/debugger/new/images/plus.svg b/devtools/client/debugger/new/images/plus.svg
new file mode 100644
index 000000000..ae7a69dfd
--- /dev/null
+++ b/devtools/client/debugger/new/images/plus.svg
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
+ <path d="M8.5 8.5V14a.5.5 0 1 1-1 0V8.5H2a.5.5 0 0 1 0-1h5.5V2a.5.5 0 0 1 1 0v5.5H14a.5.5 0 1 1 0 1H8.5z" fill-rule="evenodd"/>
+</svg>
diff --git a/devtools/client/debugger/new/images/prettyPrint.svg b/devtools/client/debugger/new/images/prettyPrint.svg
new file mode 100644
index 000000000..62e2707f9
--- /dev/null
+++ b/devtools/client/debugger/new/images/prettyPrint.svg
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
+ <path d="M4.525 13.21h-.472c-.574 0-.987-.154-1.24-.463-.253-.31-.38-.882-.38-1.719v-.573c0-.746-.097-1.265-.292-1.557-.196-.293-.51-.44-.945-.44v-.974c.435 0 .75-.146.945-.44.195-.292.293-.811.293-1.556v-.58c0-.833.126-1.404.379-1.712.253-.31.666-.464 1.24-.464h.472v.783h-.179c-.37 0-.628.08-.774.24-.145.159-.218.54-.218 1.141v.383c0 .824-.096 1.432-.287 1.823-.191.39-.516.679-.974.866.458.191.783.482.974.873.191.39.287.998.287 1.823v.382c0 .602.073.982.218 1.142.146.16.404.239.774.239h.18v.783zm9.502-4.752c-.43 0-.744.147-.942.44-.197.292-.296.811-.296 1.557v.573c0 .837-.125 1.41-.376 1.719-.251.309-.664.463-1.237.463h-.478v-.783h.185c.37 0 .628-.08.774-.24.145-.159.218-.539.218-1.14v-.383c0-.825.096-1.433.287-1.823.191-.39.516-.682.974-.873-.458-.187-.783-.476-.974-.866-.191-.391-.287-.999-.287-1.823v-.383c0-.602-.073-.982-.218-1.142-.146-.159-.404-.239-.774-.239h-.185v-.783h.478c.573 0 .986.155 1.237.464.25.308.376.88.376 1.712v.58c0 .673.088 1.174.263 1.503.176.329.5.493.975.493v.974z" fill-rule="evenodd"/>
+</svg>
diff --git a/devtools/client/debugger/new/images/resume.svg b/devtools/client/debugger/new/images/resume.svg
new file mode 100644
index 000000000..4a8b7fcd4
--- /dev/null
+++ b/devtools/client/debugger/new/images/resume.svg
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
+ <path d="M6.925 12.5l7.4-5-7.4-5v10zM6 12.5v-10c0-.785.8-1.264 1.415-.848l7.4 5c.58.392.58 1.304 0 1.696l-7.4 5C6.8 13.764 6 13.285 6 12.5z" fill-rule="evenodd"/>
+</svg>
diff --git a/devtools/client/debugger/new/images/sad-face.svg b/devtools/client/debugger/new/images/sad-face.svg
new file mode 100644
index 000000000..6c42ca43b
--- /dev/null
+++ b/devtools/client/debugger/new/images/sad-face.svg
@@ -0,0 +1,9 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="#D92215">
+ <path d="M8 14.5c-3.6 0-6.5-2.9-6.5-6.5S4.4 1.5 8 1.5s6.5 2.9 6.5 6.5-2.9 6.5-6.5 6.5zm0-12C5 2.5 2.5 5 2.5 8S5 13.5 8 13.5 13.5 11 13.5 8 11 2.5 8 2.5z"/>
+ <circle cx="5" cy="6" r="1" transform="translate(1 1)"/>
+ <circle cx="9" cy="6" r="1" transform="translate(1 1)"/>
+ <path d="M5.5 11c-.1 0-.2 0-.3-.1-.2-.1-.3-.4-.1-.7C6 9 7 8.5 8.1 8.5c1.7.1 2.8 1.7 2.8 1.8.2.2.1.5-.1.7-.2.1-.6 0-.7-.2 0 0-.9-1.3-2-1.3-.7 0-1.4.4-2.1 1.3-.2.2-.4.2-.5.2z"/>
+</svg>
diff --git a/devtools/client/debugger/new/images/settings.svg b/devtools/client/debugger/new/images/settings.svg
new file mode 100644
index 000000000..310438f7e
--- /dev/null
+++ b/devtools/client/debugger/new/images/settings.svg
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="33" height="12" viewBox="0 0 33 12">
+ <path id="base-path" d="M27.1,0H1C0.4,0,0,0.4,0,1v10c0,0.6,0.4,1,1,1h26.1 c0.6,0,1.2-0.3,1.5-0.7L33,6l-4.4-5.3C28.2,0.3,27.7,0,27.1,0z"/>
+</svg>
diff --git a/devtools/client/debugger/new/images/stepIn.svg b/devtools/client/debugger/new/images/stepIn.svg
new file mode 100644
index 000000000..eff11c0c9
--- /dev/null
+++ b/devtools/client/debugger/new/images/stepIn.svg
@@ -0,0 +1,8 @@
+<!-- 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/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
+ <g fill-rule="evenodd">
+ <path d="M1.5 14.042h4.095a.5.5 0 0 0 0-1H1.5a.5.5 0 1 0 0 1zM7.983 2a.5.5 0 0 1 .517.5v7.483l3.136-3.326a.5.5 0 1 1 .728.686l-4 4.243a.499.499 0 0 1-.73-.004L3.635 7.343a.5.5 0 0 1 .728-.686L7.5 9.983V3H1.536C1.24 3 1 2.776 1 2.5s.24-.5.536-.5h6.447zM10.5 14.042h4.095a.5.5 0 0 0 0-1H10.5a.5.5 0 1 0 0 1z"/>
+ </g>
+</svg>
diff --git a/devtools/client/debugger/new/images/stepOut.svg b/devtools/client/debugger/new/images/stepOut.svg
new file mode 100644
index 000000000..4e5457141
--- /dev/null
+++ b/devtools/client/debugger/new/images/stepOut.svg
@@ -0,0 +1,8 @@
+<!-- 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/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
+ <g fill-rule="evenodd">
+ <path d="M5 13.5H1a.5.5 0 1 0 0 1h4a.5.5 0 1 0 0-1zM12 13.5H8a.5.5 0 1 0 0 1h4a.5.5 0 1 0 0-1zM6.11 5.012A.427.427 0 0 1 6.21 5h7.083L9.646 1.354a.5.5 0 1 1 .708-.708l4.5 4.5a.498.498 0 0 1 0 .708l-4.5 4.5a.5.5 0 0 1-.708-.708L13.293 6H6.5v5.5a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .61-.488z"/>
+ </g>
+</svg>
diff --git a/devtools/client/debugger/new/images/stepOver.svg b/devtools/client/debugger/new/images/stepOver.svg
new file mode 100644
index 000000000..c1d30c051
--- /dev/null
+++ b/devtools/client/debugger/new/images/stepOver.svg
@@ -0,0 +1,9 @@
+<!-- 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/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
+ <g fill-rule="evenodd">
+ <path d="M13.297 6.912C12.595 4.39 10.167 2.5 7.398 2.5A5.898 5.898 0 0 0 1.5 8.398a.5.5 0 0 0 1 0A4.898 4.898 0 0 1 7.398 3.5c2.75 0 5.102 2.236 5.102 4.898v.004L8.669 7.029a.5.5 0 0 0-.338.942l4.462 1.598a.5.5 0 0 0 .651-.34.506.506 0 0 0 .02-.043l2-5a.5.5 0 1 0-.928-.372l-1.24 3.098z"/>
+ <circle cx="7" cy="12" r="1"/>
+ </g>
+</svg>
diff --git a/devtools/client/debugger/new/images/subSettings.svg b/devtools/client/debugger/new/images/subSettings.svg
new file mode 100644
index 000000000..6b2355584
--- /dev/null
+++ b/devtools/client/debugger/new/images/subSettings.svg
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
+ <path d="M12.219 7c.345 0 .635.117.869.352.234.234.351.524.351.869 0 .351-.118.652-.356.903-.238.25-.526.376-.864.376-.332 0-.615-.125-.85-.376a1.276 1.276 0 0 1-.351-.903A1.185 1.185 0 0 1 12.218 7zM8.234 7c.345 0 .635.117.87.352.234.234.351.524.351.869 0 .351-.119.652-.356.903-.238.25-.526.376-.865.376-.332 0-.613-.125-.844-.376a1.286 1.286 0 0 1-.347-.903c0-.352.114-.643.342-.874.228-.231.51-.347.85-.347zM4.201 7c.339 0 .627.117.864.352.238.234.357.524.357.869 0 .351-.119.652-.357.903-.237.25-.525.376-.864.376-.338 0-.623-.125-.854-.376A1.286 1.286 0 0 1 3 8.221 1.185 1.185 0 0 1 4.201 7z" fill-rule="evenodd"/>
+</svg>
diff --git a/devtools/client/debugger/new/images/toggle-breakpoints.svg b/devtools/client/debugger/new/images/toggle-breakpoints.svg
new file mode 100644
index 000000000..9b2cccf82
--- /dev/null
+++ b/devtools/client/debugger/new/images/toggle-breakpoints.svg
@@ -0,0 +1,8 @@
+<!-- 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/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
+ <g fill-rule="evenodd">
+ <path d="M3.233 11.25l-.417 1H1.712C.763 12.25 0 11.574 0 10.747V6.503C0 5.675.755 5 1.712 5h4.127l-.417 1H1.597C1.257 6 1 6.225 1 6.503v4.244c0 .277.267.503.597.503h1.636zM7.405 11.27L7 12.306c.865.01 2.212-.024 2.315-.04.112-.016.112-.016.185-.035.075-.02.156-.046.251-.082.152-.056.349-.138.592-.244.415-.182.962-.435 1.612-.744l.138-.066a179.35 179.35 0 0 0 2.255-1.094c1.191-.546 1.191-2.074-.025-2.632l-.737-.34a3547.554 3547.554 0 0 0-3.854-1.78c-.029.11-.065.222-.11.336l-.232.596c.894.408 4.56 2.107 4.56 2.107.458.21.458.596 0 .806L9.197 11.27H7.405zM4.462 14.692l5-12a.5.5 0 1 0-.924-.384l-5 12a.5.5 0 1 0 .924.384z"/>
+ </g>
+</svg>
diff --git a/devtools/client/debugger/new/images/worker.svg b/devtools/client/debugger/new/images/worker.svg
new file mode 100644
index 000000000..4a9874efb
--- /dev/null
+++ b/devtools/client/debugger/new/images/worker.svg
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
+ <path fill-rule="evenodd" d="M8.5 8.793L5.854 6.146l-.04-.035L7.5 4.426c.2-.2.3-.4.3-.6 0-.2-.1-.4-.2-.6l-1-1c-.4-.3-.9-.3-1.2 0l-4.1 4.1c-.2.2-.3.4-.3.6 0 .2.1.4.2.6l1 1c.3.3.9.3 1.2 0l1.71-1.71.036.04L7.793 9.5l-3.647 3.646c-.195.196-.195.512 0 .708.196.195.512.195.708 0L8.5 10.207l3.646 3.647c.196.195.512.195.708 0 .195-.196.195-.512 0-.708L9.207 9.5l2.565-2.565L13.3 8.5c.1.1 2.3 1.1 2.7.7.4-.4-.3-2.7-.5-2.9l-1.1-1.1c.1-.1.2-.4.2-.6 0-.2-.1-.4-.2-.6l-.4-.4c-.3-.3-.8-.3-1.1 0l-1.5-1.4c-.2-.2-.3-.2-.5-.2s-.3.1-.5.2L9.2 3.4c-.2.1-.2.2-.2.4s.1.4.2.5l1.874 1.92L8.5 8.792z"/>
+</svg>
diff --git a/devtools/client/debugger/new/index.html b/devtools/client/debugger/new/index.html
new file mode 100644
index 000000000..ed4c976fc
--- /dev/null
+++ b/devtools/client/debugger/new/index.html
@@ -0,0 +1,31 @@
+<!-- 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/. -->
+<!DOCTYPE html>
+<html dir="">
+ <head>
+ <link rel="stylesheet"
+ type="text/css"
+ href="chrome://devtools/content/sourceeditor/codemirror/lib/codemirror.css" />
+ <link rel="stylesheet"
+ type="text/css"
+ href="chrome://devtools/content/sourceeditor/codemirror/addon/dialog/dialog.css" />
+ <link rel="stylesheet"
+ type="text/css"
+ href="chrome://devtools/content/sourceeditor/codemirror/mozilla.css" />
+ <link rel="stylesheet" type="text/css" href="resource://devtools/client/debugger/new/styles.css" />
+ </head>
+ <body>
+ <div id="mount"></div>
+ <script type="application/javascript;version=1.8"
+ src="chrome://devtools/content/shared/theme-switching.js"></script>
+ <script type="text/javascript">
+ const { BrowserLoader } = Components.utils.import("resource://devtools/client/shared/browser-loader.js", {});
+ const { require: devtoolsRequire } = BrowserLoader({
+ baseURI: "resource://devtools/client/debugger/new/",
+ window,
+ });
+ </script>
+ <script type="text/javascript" src="resource://devtools/client/debugger/new/bundle.js"></script>
+ </body>
+</html>
diff --git a/devtools/client/debugger/new/moz.build b/devtools/client/debugger/new/moz.build
new file mode 100644
index 000000000..09d1f908e
--- /dev/null
+++ b/devtools/client/debugger/new/moz.build
@@ -0,0 +1,12 @@
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'bundle.js',
+ 'panel.js',
+ 'pretty-print-worker.js',
+ 'source-map-worker.js',
+ 'styles.css'
+)
diff --git a/devtools/client/debugger/new/panel.js b/devtools/client/debugger/new/panel.js
new file mode 100644
index 000000000..62d4a9f4f
--- /dev/null
+++ b/devtools/client/debugger/new/panel.js
@@ -0,0 +1,77 @@
+/* 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 { Task } = require("devtools/shared/task");
+var {LocalizationHelper} = require("devtools/shared/l10n");
+
+const DBG_STRINGS_URI = "devtools/client/locales/debugger.properties";
+var L10N = new LocalizationHelper(DBG_STRINGS_URI);
+
+function DebuggerPanel(iframeWindow, toolbox) {
+ this.panelWin = iframeWindow;
+ this.panelWin.L10N = L10N;
+ this.toolbox = toolbox;
+}
+
+DebuggerPanel.prototype = {
+ open: Task.async(function* () {
+ if (!this.toolbox.target.isRemote) {
+ yield this.toolbox.target.makeRemote();
+ }
+
+ yield this.panelWin.Debugger.bootstrap({
+ threadClient: this.toolbox.threadClient,
+ tabTarget: this.toolbox.target
+ });
+
+ this.isReady = true;
+ return this;
+ }),
+
+ _store: function () {
+ return this.panelWin.Debugger.store;
+ },
+
+ _getState: function () {
+ return this._store().getState();
+ },
+
+ _actions: function () {
+ return this.panelWin.Debugger.actions;
+ },
+
+ _selectors: function () {
+ return this.panelWin.Debugger.selectors;
+ },
+
+ getFrames: function () {
+ let frames = this._selectors().getFrames(this._getState());
+
+ // Frames is null when the debugger is not paused.
+ if (!frames) {
+ return {
+ frames: [],
+ selected: -1
+ };
+ }
+
+ frames = frames.toJS();
+ const selectedFrame = this._selectors().getSelectedFrame(this._getState());
+ const selected = frames.findIndex(frame => frame.id == selectedFrame.id);
+
+ frames.forEach(frame => {
+ frame.actor = frame.id;
+ });
+
+ return { frames, selected };
+ },
+
+ destroy: function () {
+ this.panelWin.Debugger.destroy();
+ this.emit("destroyed");
+ }
+};
+
+exports.DebuggerPanel = DebuggerPanel;
diff --git a/devtools/client/debugger/new/pretty-print-worker.js b/devtools/client/debugger/new/pretty-print-worker.js
new file mode 100644
index 000000000..4fa735e16
--- /dev/null
+++ b/devtools/client/debugger/new/pretty-print-worker.js
@@ -0,0 +1,5904 @@
+var Debugger =
+/******/ (function(modules) { // webpackBootstrap
+/******/ // The module cache
+/******/ var installedModules = {};
+/******/
+/******/ // The require function
+/******/ function __webpack_require__(moduleId) {
+/******/
+/******/ // Check if module is in cache
+/******/ if(installedModules[moduleId])
+/******/ return installedModules[moduleId].exports;
+/******/
+/******/ // Create a new module (and put it into the cache)
+/******/ var module = installedModules[moduleId] = {
+/******/ exports: {},
+/******/ id: moduleId,
+/******/ loaded: false
+/******/ };
+/******/
+/******/ // Execute the module function
+/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
+/******/
+/******/ // Flag the module as loaded
+/******/ module.loaded = true;
+/******/
+/******/ // Return the exports of the module
+/******/ return module.exports;
+/******/ }
+/******/
+/******/
+/******/ // expose the modules object (__webpack_modules__)
+/******/ __webpack_require__.m = modules;
+/******/
+/******/ // expose the module cache
+/******/ __webpack_require__.c = installedModules;
+/******/
+/******/ // __webpack_public_path__
+/******/ __webpack_require__.p = "/public/build";
+/******/
+/******/ // Load entry module and return exports
+/******/ return __webpack_require__(0);
+/******/ })
+/************************************************************************/
+/******/ ({
+
+/***/ 0:
+/***/ function(module, exports, __webpack_require__) {
+
+ var prettyFast = __webpack_require__(460);
+ var assert = __webpack_require__(247);
+
+ function prettyPrint(_ref) {
+ var url = _ref.url;
+ var indent = _ref.indent;
+ var source = _ref.source;
+
+ try {
+ var prettified = prettyFast(source, {
+ url: url,
+ indent: " ".repeat(indent)
+ });
+
+ return {
+ code: prettified.code,
+ mappings: prettified.map._mappings
+ };
+ } catch (e) {
+ return new Error(e.message + "\n" + e.stack);
+ }
+ }
+
+ function invertMappings(mappings) {
+ return mappings._array.map(m => {
+ var mapping = {
+ generated: {
+ line: m.originalLine,
+ column: m.originalColumn
+ }
+ };
+ if (m.source) {
+ mapping.source = m.source;
+ mapping.original = {
+ line: m.generatedLine,
+ column: m.generatedColumn
+ };
+ mapping.name = m.name;
+ }
+ return mapping;
+ });
+ }
+
+ self.onmessage = function (msg) {
+ var _msg$data = msg.data;
+ var id = _msg$data.id;
+ var args = _msg$data.args;
+
+ assert(msg.data.method === "prettyPrint", "Method must be `prettyPrint`");
+
+ try {
+ var _prettyPrint = prettyPrint(args[0]);
+
+ var code = _prettyPrint.code;
+ var mappings = _prettyPrint.mappings;
+
+ self.postMessage({ id, response: {
+ code, mappings: invertMappings(mappings)
+ } });
+ } catch (e) {
+ self.postMessage({ id, error: e });
+ }
+ };
+
+/***/ },
+
+/***/ 247:
+/***/ function(module, exports) {
+
+ function assert(condition, message) {
+ if (!condition) {
+ throw new Error("Assertion failure: " + message);
+ }
+ }
+
+ module.exports = assert;
+
+/***/ },
+
+/***/ 460:
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_RESULT__;/* -*- indent-tabs-mode: nil; js-indent-level: 2; fill-column: 80 -*- */
+ /*
+ * Copyright 2013 Mozilla Foundation and contributors
+ * Licensed under the New BSD license. See LICENSE.md or:
+ * http://opensource.org/licenses/BSD-2-Clause
+ */
+ (function (root, factory) {
+ "use strict";
+
+ if (true) {
+ !(__WEBPACK_AMD_DEFINE_FACTORY__ = (factory), __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? (__WEBPACK_AMD_DEFINE_FACTORY__.call(exports, __webpack_require__, exports, module)) : __WEBPACK_AMD_DEFINE_FACTORY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+ } else if (typeof exports === "object") {
+ module.exports = factory();
+ } else {
+ root.prettyFast = factory();
+ }
+ }(this, function () {
+ "use strict";
+
+ var acorn = this.acorn || __webpack_require__(461);
+ var sourceMap = this.sourceMap || __webpack_require__(462);
+ var SourceNode = sourceMap.SourceNode;
+
+ // If any of these tokens are seen before a "[" token, we know that "[" token
+ // is the start of an array literal, rather than a property access.
+ //
+ // The only exception is "}", which would need to be disambiguated by
+ // parsing. The majority of the time, an open bracket following a closing
+ // curly is going to be an array literal, so we brush the complication under
+ // the rug, and handle the ambiguity by always assuming that it will be an
+ // array literal.
+ var PRE_ARRAY_LITERAL_TOKENS = {
+ "typeof": true,
+ "void": true,
+ "delete": true,
+ "case": true,
+ "do": true,
+ "=": true,
+ "in": true,
+ "{": true,
+ "*": true,
+ "/": true,
+ "%": true,
+ "else": true,
+ ";": true,
+ "++": true,
+ "--": true,
+ "+": true,
+ "-": true,
+ "~": true,
+ "!": true,
+ ":": true,
+ "?": true,
+ ">>": true,
+ ">>>": true,
+ "<<": true,
+ "||": true,
+ "&&": true,
+ "<": true,
+ ">": true,
+ "<=": true,
+ ">=": true,
+ "instanceof": true,
+ "&": true,
+ "^": true,
+ "|": true,
+ "==": true,
+ "!=": true,
+ "===": true,
+ "!==": true,
+ ",": true,
+
+ "}": true
+ };
+
+ /**
+ * Determines if we think that the given token starts an array literal.
+ *
+ * @param Object token
+ * The token we want to determine if it is an array literal.
+ * @param Object lastToken
+ * The last token we added to the pretty printed results.
+ *
+ * @returns Boolean
+ * True if we believe it is an array literal, false otherwise.
+ */
+ function isArrayLiteral(token, lastToken) {
+ if (token.type.type != "[") {
+ return false;
+ }
+ if (!lastToken) {
+ return true;
+ }
+ if (lastToken.type.isAssign) {
+ return true;
+ }
+ return !!PRE_ARRAY_LITERAL_TOKENS[
+ lastToken.type.keyword || lastToken.type.type
+ ];
+ }
+
+ // If any of these tokens are followed by a token on a new line, we know that
+ // ASI cannot happen.
+ var PREVENT_ASI_AFTER_TOKENS = {
+ // Binary operators
+ "*": true,
+ "/": true,
+ "%": true,
+ "+": true,
+ "-": true,
+ "<<": true,
+ ">>": true,
+ ">>>": true,
+ "<": true,
+ ">": true,
+ "<=": true,
+ ">=": true,
+ "instanceof": true,
+ "in": true,
+ "==": true,
+ "!=": true,
+ "===": true,
+ "!==": true,
+ "&": true,
+ "^": true,
+ "|": true,
+ "&&": true,
+ "||": true,
+ ",": true,
+ ".": true,
+ "=": true,
+ "*=": true,
+ "/=": true,
+ "%=": true,
+ "+=": true,
+ "-=": true,
+ "<<=": true,
+ ">>=": true,
+ ">>>=": true,
+ "&=": true,
+ "^=": true,
+ "|=": true,
+ // Unary operators
+ "delete": true,
+ "void": true,
+ "typeof": true,
+ "~": true,
+ "!": true,
+ "new": true,
+ // Function calls and grouped expressions
+ "(": true
+ };
+
+ // If any of these tokens are on a line after the token before it, we know
+ // that ASI cannot happen.
+ var PREVENT_ASI_BEFORE_TOKENS = {
+ // Binary operators
+ "*": true,
+ "/": true,
+ "%": true,
+ "<<": true,
+ ">>": true,
+ ">>>": true,
+ "<": true,
+ ">": true,
+ "<=": true,
+ ">=": true,
+ "instanceof": true,
+ "in": true,
+ "==": true,
+ "!=": true,
+ "===": true,
+ "!==": true,
+ "&": true,
+ "^": true,
+ "|": true,
+ "&&": true,
+ "||": true,
+ ",": true,
+ ".": true,
+ "=": true,
+ "*=": true,
+ "/=": true,
+ "%=": true,
+ "+=": true,
+ "-=": true,
+ "<<=": true,
+ ">>=": true,
+ ">>>=": true,
+ "&=": true,
+ "^=": true,
+ "|=": true,
+ // Function calls
+ "(": true
+ };
+
+ /**
+ * Determines if Automatic Semicolon Insertion (ASI) occurs between these
+ * tokens.
+ *
+ * @param Object token
+ * The current token.
+ * @param Object lastToken
+ * The last token we added to the pretty printed results.
+ *
+ * @returns Boolean
+ * True if we believe ASI occurs.
+ */
+ function isASI(token, lastToken) {
+ if (!lastToken) {
+ return false;
+ }
+ if (token.startLoc.line === lastToken.startLoc.line) {
+ return false;
+ }
+ if (PREVENT_ASI_AFTER_TOKENS[
+ lastToken.type.type || lastToken.type.keyword
+ ]) {
+ return false;
+ }
+ if (PREVENT_ASI_BEFORE_TOKENS[token.type.type || token.type.keyword]) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Determine if we have encountered a getter or setter.
+ *
+ * @param Object token
+ * The current token. If this is a getter or setter, it would be the
+ * property name.
+ * @param Object lastToken
+ * The last token we added to the pretty printed results. If this is a
+ * getter or setter, it would be the `get` or `set` keyword
+ * respectively.
+ * @param Array stack
+ * The stack of open parens/curlies/brackets/etc.
+ *
+ * @returns Boolean
+ * True if this is a getter or setter.
+ */
+ function isGetterOrSetter(token, lastToken, stack) {
+ return stack[stack.length - 1] == "{"
+ && lastToken
+ && lastToken.type.type == "name"
+ && (lastToken.value == "get" || lastToken.value == "set")
+ && token.type.type == "name";
+ }
+
+ /**
+ * Determine if we should add a newline after the given token.
+ *
+ * @param Object token
+ * The token we are looking at.
+ * @param Array stack
+ * The stack of open parens/curlies/brackets/etc.
+ *
+ * @returns Boolean
+ * True if we should add a newline.
+ */
+ function isLineDelimiter(token, stack) {
+ if (token.isArrayLiteral) {
+ return true;
+ }
+ var ttt = token.type.type;
+ var top = stack[stack.length - 1];
+ return ttt == ";" && top != "("
+ || ttt == "{"
+ || ttt == "," && top != "("
+ || ttt == ":" && (top == "case" || top == "default");
+ }
+
+ /**
+ * Append the necessary whitespace to the result after we have added the given
+ * token.
+ *
+ * @param Object token
+ * The token that was just added to the result.
+ * @param Function write
+ * The function to write to the pretty printed results.
+ * @param Array stack
+ * The stack of open parens/curlies/brackets/etc.
+ *
+ * @returns Boolean
+ * Returns true if we added a newline to result, false in all other
+ * cases.
+ */
+ function appendNewline(token, write, stack) {
+ if (isLineDelimiter(token, stack)) {
+ write("\n", token.startLoc.line, token.startLoc.column);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Determines if we need to add a space between the last token we added and
+ * the token we are about to add.
+ *
+ * @param Object token
+ * The token we are about to add to the pretty printed code.
+ * @param Object lastToken
+ * The last token added to the pretty printed code.
+ */
+ function needsSpaceAfter(token, lastToken) {
+ if (lastToken) {
+ if (lastToken.type.isLoop) {
+ return true;
+ }
+ if (lastToken.type.isAssign) {
+ return true;
+ }
+ if (lastToken.type.binop != null) {
+ return true;
+ }
+
+ var ltt = lastToken.type.type;
+ if (ltt == "?") {
+ return true;
+ }
+ if (ltt == ":") {
+ return true;
+ }
+ if (ltt == ",") {
+ return true;
+ }
+ if (ltt == ";") {
+ return true;
+ }
+
+ var ltk = lastToken.type.keyword;
+ if (ltk != null) {
+ if (ltk == "break" || ltk == "continue" || ltk == "return") {
+ return token.type.type != ";";
+ }
+ if (ltk != "debugger"
+ && ltk != "null"
+ && ltk != "true"
+ && ltk != "false"
+ && ltk != "this"
+ && ltk != "default") {
+ return true;
+ }
+ }
+
+ if (ltt == ")" && (token.type.type != ")"
+ && token.type.type != "]"
+ && token.type.type != ";"
+ && token.type.type != ","
+ && token.type.type != ".")) {
+ return true;
+ }
+ }
+
+ if (token.type.isAssign) {
+ return true;
+ }
+ if (token.type.binop != null) {
+ return true;
+ }
+ if (token.type.type == "?") {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Add the required whitespace before this token, whether that is a single
+ * space, newline, and/or the indent on fresh lines.
+ *
+ * @param Object token
+ * The token we are about to add to the pretty printed code.
+ * @param Object lastToken
+ * The last token we added to the pretty printed code.
+ * @param Boolean addedNewline
+ * Whether we added a newline after adding the last token to the pretty
+ * printed code.
+ * @param Function write
+ * The function to write pretty printed code to the result SourceNode.
+ * @param Object options
+ * The options object.
+ * @param Number indentLevel
+ * The number of indents deep we are.
+ * @param Array stack
+ * The stack of open curlies, brackets, etc.
+ */
+ function prependWhiteSpace(token, lastToken, addedNewline, write, options,
+ indentLevel, stack) {
+ var ttk = token.type.keyword;
+ var ttt = token.type.type;
+ var newlineAdded = addedNewline;
+ var ltt = lastToken ? lastToken.type.type : null;
+
+ // Handle whitespace and newlines after "}" here instead of in
+ // `isLineDelimiter` because it is only a line delimiter some of the
+ // time. For example, we don't want to put "else if" on a new line after
+ // the first if's block.
+ if (lastToken && ltt == "}") {
+ if (ttk == "while" && stack[stack.length - 1] == "do") {
+ write(" ",
+ lastToken.startLoc.line,
+ lastToken.startLoc.column);
+ } else if (ttk == "else" ||
+ ttk == "catch" ||
+ ttk == "finally") {
+ write(" ",
+ lastToken.startLoc.line,
+ lastToken.startLoc.column);
+ } else if (ttt != "(" &&
+ ttt != ";" &&
+ ttt != "," &&
+ ttt != ")" &&
+ ttt != ".") {
+ write("\n",
+ lastToken.startLoc.line,
+ lastToken.startLoc.column);
+ newlineAdded = true;
+ }
+ }
+
+ if (isGetterOrSetter(token, lastToken, stack)) {
+ write(" ",
+ lastToken.startLoc.line,
+ lastToken.startLoc.column);
+ }
+
+ if (ttt == ":" && stack[stack.length - 1] == "?") {
+ write(" ",
+ lastToken.startLoc.line,
+ lastToken.startLoc.column);
+ }
+
+ if (lastToken && ltt != "}" && ttk == "else") {
+ write(" ",
+ lastToken.startLoc.line,
+ lastToken.startLoc.column);
+ }
+
+ function ensureNewline() {
+ if (!newlineAdded) {
+ write("\n",
+ lastToken.startLoc.line,
+ lastToken.startLoc.column);
+ newlineAdded = true;
+ }
+ }
+
+ if (isASI(token, lastToken)) {
+ ensureNewline();
+ }
+
+ if (decrementsIndent(ttt, stack)) {
+ ensureNewline();
+ }
+
+ if (newlineAdded) {
+ if (ttk == "case" || ttk == "default") {
+ write(repeat(options.indent, indentLevel - 1),
+ token.startLoc.line,
+ token.startLoc.column);
+ } else {
+ write(repeat(options.indent, indentLevel),
+ token.startLoc.line,
+ token.startLoc.column);
+ }
+ } else if (needsSpaceAfter(token, lastToken)) {
+ write(" ",
+ lastToken.startLoc.line,
+ lastToken.startLoc.column);
+ }
+ }
+
+ /**
+ * Repeat the `str` string `n` times.
+ *
+ * @param String str
+ * The string to be repeated.
+ * @param Number n
+ * The number of times to repeat the string.
+ *
+ * @returns String
+ * The repeated string.
+ */
+ function repeat(str, n) {
+ var result = "";
+ while (n > 0) {
+ if (n & 1) {
+ result += str;
+ }
+ n >>= 1;
+ str += str;
+ }
+ return result;
+ }
+
+ /**
+ * Make sure that we output the escaped character combination inside string
+ * literals instead of various problematic characters.
+ */
+ var sanitize = (function () {
+ var escapeCharacters = {
+ // Backslash
+ "\\": "\\\\",
+ // Newlines
+ "\n": "\\n",
+ // Carriage return
+ "\r": "\\r",
+ // Tab
+ "\t": "\\t",
+ // Vertical tab
+ "\v": "\\v",
+ // Form feed
+ "\f": "\\f",
+ // Null character
+ "\0": "\\0",
+ // Single quotes
+ "'": "\\'"
+ };
+
+ var regExpString = "("
+ + Object.keys(escapeCharacters)
+ .map(function (c) { return escapeCharacters[c]; })
+ .join("|")
+ + ")";
+ var escapeCharactersRegExp = new RegExp(regExpString, "g");
+
+ return function (str) {
+ return str.replace(escapeCharactersRegExp, function (_, c) {
+ return escapeCharacters[c];
+ });
+ };
+ }());
+ /**
+ * Add the given token to the pretty printed results.
+ *
+ * @param Object token
+ * The token to add.
+ * @param Function write
+ * The function to write pretty printed code to the result SourceNode.
+ */
+ function addToken(token, write) {
+ if (token.type.type == "string") {
+ write("'" + sanitize(token.value) + "'",
+ token.startLoc.line,
+ token.startLoc.column);
+ } else if (token.type.type == "regexp") {
+ write(String(token.value.value),
+ token.startLoc.line,
+ token.startLoc.column);
+ } else {
+ write(String(token.value != null ? token.value : token.type.type),
+ token.startLoc.line,
+ token.startLoc.column);
+ }
+ }
+
+ /**
+ * Returns true if the given token type belongs on the stack.
+ */
+ function belongsOnStack(token) {
+ var ttt = token.type.type;
+ var ttk = token.type.keyword;
+ return ttt == "{"
+ || ttt == "("
+ || ttt == "["
+ || ttt == "?"
+ || ttk == "do"
+ || ttk == "switch"
+ || ttk == "case"
+ || ttk == "default";
+ }
+
+ /**
+ * Returns true if the given token should cause us to pop the stack.
+ */
+ function shouldStackPop(token, stack) {
+ var ttt = token.type.type;
+ var ttk = token.type.keyword;
+ var top = stack[stack.length - 1];
+ return ttt == "]"
+ || ttt == ")"
+ || ttt == "}"
+ || (ttt == ":" && (top == "case" || top == "default" || top == "?"))
+ || (ttk == "while" && top == "do");
+ }
+
+ /**
+ * Returns true if the given token type should cause us to decrement the
+ * indent level.
+ */
+ function decrementsIndent(tokenType, stack) {
+ return tokenType == "}"
+ || (tokenType == "]" && stack[stack.length - 1] == "[\n");
+ }
+
+ /**
+ * Returns true if the given token should cause us to increment the indent
+ * level.
+ */
+ function incrementsIndent(token) {
+ return token.type.type == "{"
+ || token.isArrayLiteral
+ || token.type.keyword == "switch";
+ }
+
+ /**
+ * Add a comment to the pretty printed code.
+ *
+ * @param Function write
+ * The function to write pretty printed code to the result SourceNode.
+ * @param Number indentLevel
+ * The number of indents deep we are.
+ * @param Object options
+ * The options object.
+ * @param Boolean block
+ * True if the comment is a multiline block style comment.
+ * @param String text
+ * The text of the comment.
+ * @param Number line
+ * The line number to comment appeared on.
+ * @param Number column
+ * The column number the comment appeared on.
+ */
+ function addComment(write, indentLevel, options, block, text, line, column) {
+ var indentString = repeat(options.indent, indentLevel);
+
+ write(indentString, line, column);
+ if (block) {
+ write("/*");
+ write(text
+ .split(new RegExp("/\n" + indentString + "/", "g"))
+ .join("\n" + indentString));
+ write("*/");
+ } else {
+ write("//");
+ write(text);
+ }
+ write("\n");
+ }
+
+ /**
+ * The main function.
+ *
+ * @param String input
+ * The ugly JS code we want to pretty print.
+ * @param Object options
+ * The options object. Provides configurability of the pretty
+ * printing. Properties:
+ * - url: The URL string of the ugly JS code.
+ * - indent: The string to indent code by.
+ *
+ * @returns Object
+ * An object with the following properties:
+ * - code: The pretty printed code string.
+ * - map: A SourceMapGenerator instance.
+ */
+ return function prettyFast(input, options) {
+ // The level of indents deep we are.
+ var indentLevel = 0;
+
+ // We will accumulate the pretty printed code in this SourceNode.
+ var result = new SourceNode();
+
+ /**
+ * Write a pretty printed string to the result SourceNode.
+ *
+ * We buffer our writes so that we only create one mapping for each line in
+ * the source map. This enhances performance by avoiding extraneous mapping
+ * serialization, and flattening the tree that
+ * `SourceNode#toStringWithSourceMap` will have to recursively walk. When
+ * timing how long it takes to pretty print jQuery, this optimization
+ * brought the time down from ~390 ms to ~190ms!
+ *
+ * @param String str
+ * The string to be added to the result.
+ * @param Number line
+ * The line number the string came from in the ugly source.
+ * @param Number column
+ * The column number the string came from in the ugly source.
+ */
+ var write = (function () {
+ var buffer = [];
+ var bufferLine = -1;
+ var bufferColumn = -1;
+ return function write(str, line, column) {
+ if (line != null && bufferLine === -1) {
+ bufferLine = line;
+ }
+ if (column != null && bufferColumn === -1) {
+ bufferColumn = column;
+ }
+ buffer.push(str);
+
+ if (str == "\n") {
+ var lineStr = "";
+ for (var i = 0, len = buffer.length; i < len; i++) {
+ lineStr += buffer[i];
+ }
+ result.add(new SourceNode(bufferLine, bufferColumn, options.url,
+ lineStr));
+ buffer.splice(0, buffer.length);
+ bufferLine = -1;
+ bufferColumn = -1;
+ }
+ };
+ }());
+
+ // Whether or not we added a newline on after we added the last token.
+ var addedNewline = false;
+
+ // The current token we will be adding to the pretty printed code.
+ var token;
+
+ // Shorthand for token.type.type, so we don't have to repeatedly access
+ // properties.
+ var ttt;
+
+ // Shorthand for token.type.keyword, so we don't have to repeatedly access
+ // properties.
+ var ttk;
+
+ // The last token we added to the pretty printed code.
+ var lastToken;
+
+ // Stack of token types/keywords that can affect whether we want to add a
+ // newline or a space. We can make that decision based on what token type is
+ // on the top of the stack. For example, a comma in a parameter list should
+ // be followed by a space, while a comma in an object literal should be
+ // followed by a newline.
+ //
+ // Strings that go on the stack:
+ //
+ // - "{"
+ // - "("
+ // - "["
+ // - "[\n"
+ // - "do"
+ // - "?"
+ // - "switch"
+ // - "case"
+ // - "default"
+ //
+ // The difference between "[" and "[\n" is that "[\n" is used when we are
+ // treating "[" and "]" tokens as line delimiters and should increment and
+ // decrement the indent level when we find them.
+ var stack = [];
+
+ // Acorn's tokenizer will always yield comments *before* the token they
+ // follow (unless the very first thing in the source is a comment), so we
+ // have to queue the comments in order to pretty print them in the correct
+ // location. For example, the source file:
+ //
+ // foo
+ // // a
+ // // b
+ // bar
+ //
+ // When tokenized by acorn, gives us the following token stream:
+ //
+ // [ '// a', '// b', foo, bar ]
+ var commentQueue = [];
+
+ var getToken = acorn.tokenize(input, {
+ locations: true,
+ sourceFile: options.url,
+ onComment: function (block, text, start, end, startLoc, endLoc) {
+ if (lastToken) {
+ commentQueue.push({
+ block: block,
+ text: text,
+ line: startLoc.line,
+ column: startLoc.column,
+ trailing: lastToken.endLoc.line == startLoc.line
+ });
+ } else {
+ addComment(write, indentLevel, options, block, text, startLoc.line,
+ startLoc.column);
+ addedNewline = true;
+ }
+ }
+ });
+
+ for (;;) {
+ token = getToken();
+
+ ttk = token.type.keyword;
+ ttt = token.type.type;
+
+ if (ttt == "eof") {
+ if (!addedNewline) {
+ write("\n");
+ }
+ break;
+ }
+
+ token.isArrayLiteral = isArrayLiteral(token, lastToken);
+
+ if (belongsOnStack(token)) {
+ if (token.isArrayLiteral) {
+ stack.push("[\n");
+ } else {
+ stack.push(ttt || ttk);
+ }
+ }
+
+ if (decrementsIndent(ttt, stack)) {
+ indentLevel--;
+ if (ttt == "}"
+ && stack.length > 1
+ && stack[stack.length - 2] == "switch") {
+ indentLevel--;
+ }
+ }
+
+ prependWhiteSpace(token, lastToken, addedNewline, write, options,
+ indentLevel, stack);
+ addToken(token, write);
+ if (commentQueue.length === 0 || !commentQueue[0].trailing) {
+ addedNewline = appendNewline(token, write, stack);
+ }
+
+ if (shouldStackPop(token, stack)) {
+ stack.pop();
+ if (token == "}" && stack.length
+ && stack[stack.length - 1] == "switch") {
+ stack.pop();
+ }
+ }
+
+ if (incrementsIndent(token)) {
+ indentLevel++;
+ }
+
+ // Acorn's tokenizer re-uses tokens, so we have to copy the last token on
+ // every iteration. We follow acorn's lead here, and reuse the lastToken
+ // object the same way that acorn reuses the token object. This allows us
+ // to avoid allocations and minimize GC pauses.
+ if (!lastToken) {
+ lastToken = { startLoc: {}, endLoc: {} };
+ }
+ lastToken.start = token.start;
+ lastToken.end = token.end;
+ lastToken.startLoc.line = token.startLoc.line;
+ lastToken.startLoc.column = token.startLoc.column;
+ lastToken.endLoc.line = token.endLoc.line;
+ lastToken.endLoc.column = token.endLoc.column;
+ lastToken.type = token.type;
+ lastToken.value = token.value;
+ lastToken.isArrayLiteral = token.isArrayLiteral;
+
+ // Apply all the comments that have been queued up.
+ if (commentQueue.length) {
+ if (!addedNewline && !commentQueue[0].trailing) {
+ write("\n");
+ }
+ if (commentQueue[0].trailing) {
+ write(" ");
+ }
+ for (var i = 0, n = commentQueue.length; i < n; i++) {
+ var comment = commentQueue[i];
+ var commentIndentLevel = commentQueue[i].trailing ? 0 : indentLevel;
+ addComment(write, commentIndentLevel, options, comment.block,
+ comment.text, comment.line, comment.column);
+ }
+ addedNewline = true;
+ commentQueue.splice(0, commentQueue.length);
+ }
+ }
+
+ return result.toStringWithSourceMap({ file: options.url });
+ };
+
+ }.bind(this)));
+
+
+/***/ },
+
+/***/ 461:
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;// Acorn is a tiny, fast JavaScript parser written in JavaScript.
+ //
+ // Acorn was written by Marijn Haverbeke and various contributors and
+ // released under an MIT license. The Unicode regexps (for identifiers
+ // and whitespace) were taken from [Esprima](http://esprima.org) by
+ // Ariya Hidayat.
+ //
+ // Git repositories for Acorn are available at
+ //
+ // http://marijnhaverbeke.nl/git/acorn
+ // https://github.com/marijnh/acorn.git
+ //
+ // Please use the [github bug tracker][ghbt] to report issues.
+ //
+ // [ghbt]: https://github.com/marijnh/acorn/issues
+ //
+ // This file defines the main parser interface. The library also comes
+ // with a [error-tolerant parser][dammit] and an
+ // [abstract syntax tree walker][walk], defined in other files.
+ //
+ // [dammit]: acorn_loose.js
+ // [walk]: util/walk.js
+
+ (function(root, mod) {
+ if (true) return mod(exports); // CommonJS
+ if (true) return !(__WEBPACK_AMD_DEFINE_ARRAY__ = [exports], __WEBPACK_AMD_DEFINE_FACTORY__ = (mod), __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); // AMD
+ mod(root.acorn || (root.acorn = {})); // Plain browser env
+ })(this, function(exports) {
+ "use strict";
+
+ exports.version = "0.11.0";
+
+ // The main exported interface (under `self.acorn` when in the
+ // browser) is a `parse` function that takes a code string and
+ // returns an abstract syntax tree as specified by [Mozilla parser
+ // API][api], with the caveat that inline XML is not recognized.
+ //
+ // [api]: https://developer.mozilla.org/en-US/docs/SpiderMonkey/Parser_API
+
+ var options, input, inputLen, sourceFile;
+
+ exports.parse = function(inpt, opts) {
+ input = String(inpt); inputLen = input.length;
+ setOptions(opts);
+ initTokenState();
+ var startPos = options.locations ? [tokPos, curPosition()] : tokPos;
+ initParserState();
+ return parseTopLevel(options.program || startNodeAt(startPos));
+ };
+
+ // A second optional argument can be given to further configure
+ // the parser process. These options are recognized:
+
+ var defaultOptions = exports.defaultOptions = {
+ // `ecmaVersion` indicates the ECMAScript version to parse. Must
+ // be either 3, or 5, or 6. This influences support for strict
+ // mode, the set of reserved words, support for getters and
+ // setters and other features.
+ ecmaVersion: 5,
+ // Turn on `strictSemicolons` to prevent the parser from doing
+ // automatic semicolon insertion.
+ strictSemicolons: false,
+ // When `allowTrailingCommas` is false, the parser will not allow
+ // trailing commas in array and object literals.
+ allowTrailingCommas: true,
+ // By default, reserved words are not enforced. Enable
+ // `forbidReserved` to enforce them. When this option has the
+ // value "everywhere", reserved words and keywords can also not be
+ // used as property names.
+ forbidReserved: false,
+ // When enabled, a return at the top level is not considered an
+ // error.
+ allowReturnOutsideFunction: false,
+ // When enabled, import/export statements are not constrained to
+ // appearing at the top of the program.
+ allowImportExportEverywhere: false,
+ // When `locations` is on, `loc` properties holding objects with
+ // `start` and `end` properties in `{line, column}` form (with
+ // line being 1-based and column 0-based) will be attached to the
+ // nodes.
+ locations: false,
+ // A function can be passed as `onToken` option, which will
+ // cause Acorn to call that function with object in the same
+ // format as tokenize() returns. Note that you are not
+ // allowed to call the parser from the callback—that will
+ // corrupt its internal state.
+ onToken: null,
+ // A function can be passed as `onComment` option, which will
+ // cause Acorn to call that function with `(block, text, start,
+ // end)` parameters whenever a comment is skipped. `block` is a
+ // boolean indicating whether this is a block (`/* */`) comment,
+ // `text` is the content of the comment, and `start` and `end` are
+ // character offsets that denote the start and end of the comment.
+ // When the `locations` option is on, two more parameters are
+ // passed, the full `{line, column}` locations of the start and
+ // end of the comments. Note that you are not allowed to call the
+ // parser from the callback—that will corrupt its internal state.
+ onComment: null,
+ // Nodes have their start and end characters offsets recorded in
+ // `start` and `end` properties (directly on the node, rather than
+ // the `loc` object, which holds line/column data. To also add a
+ // [semi-standardized][range] `range` property holding a `[start,
+ // end]` array with the same numbers, set the `ranges` option to
+ // `true`.
+ //
+ // [range]: https://bugzilla.mozilla.org/show_bug.cgi?id=745678
+ ranges: false,
+ // It is possible to parse multiple files into a single AST by
+ // passing the tree produced by parsing the first file as
+ // `program` option in subsequent parses. This will add the
+ // toplevel forms of the parsed file to the `Program` (top) node
+ // of an existing parse tree.
+ program: null,
+ // When `locations` is on, you can pass this to record the source
+ // file in every node's `loc` object.
+ sourceFile: null,
+ // This value, if given, is stored in every node, whether
+ // `locations` is on or off.
+ directSourceFile: null,
+ // When enabled, parenthesized expressions are represented by
+ // (non-standard) ParenthesizedExpression nodes
+ preserveParens: false
+ };
+
+ // This function tries to parse a single expression at a given
+ // offset in a string. Useful for parsing mixed-language formats
+ // that embed JavaScript expressions.
+
+ exports.parseExpressionAt = function(inpt, pos, opts) {
+ input = String(inpt); inputLen = input.length;
+ setOptions(opts);
+ initTokenState(pos);
+ initParserState();
+ return parseExpression();
+ };
+
+ var isArray = function (obj) {
+ return Object.prototype.toString.call(obj) === "[object Array]";
+ };
+
+ function setOptions(opts) {
+ options = {};
+ for (var opt in defaultOptions)
+ options[opt] = opts && has(opts, opt) ? opts[opt] : defaultOptions[opt];
+ sourceFile = options.sourceFile || null;
+ if (isArray(options.onToken)) {
+ var tokens = options.onToken;
+ options.onToken = function (token) {
+ tokens.push(token);
+ };
+ }
+ if (isArray(options.onComment)) {
+ var comments = options.onComment;
+ options.onComment = function (block, text, start, end, startLoc, endLoc) {
+ var comment = {
+ type: block ? 'Block' : 'Line',
+ value: text,
+ start: start,
+ end: end
+ };
+ if (options.locations) {
+ comment.loc = new SourceLocation();
+ comment.loc.start = startLoc;
+ comment.loc.end = endLoc;
+ }
+ if (options.ranges)
+ comment.range = [start, end];
+ comments.push(comment);
+ };
+ }
+ isKeyword = options.ecmaVersion >= 6 ? isEcma6Keyword : isEcma5AndLessKeyword;
+ }
+
+ // The `getLineInfo` function is mostly useful when the
+ // `locations` option is off (for performance reasons) and you
+ // want to find the line/column position for a given character
+ // offset. `input` should be the code string that the offset refers
+ // into.
+
+ var getLineInfo = exports.getLineInfo = function(input, offset) {
+ for (var line = 1, cur = 0;;) {
+ lineBreak.lastIndex = cur;
+ var match = lineBreak.exec(input);
+ if (match && match.index < offset) {
+ ++line;
+ cur = match.index + match[0].length;
+ } else break;
+ }
+ return {line: line, column: offset - cur};
+ };
+
+ function Token() {
+ this.type = tokType;
+ this.value = tokVal;
+ this.start = tokStart;
+ this.end = tokEnd;
+ if (options.locations) {
+ this.loc = new SourceLocation();
+ this.loc.end = tokEndLoc;
+ // TODO: remove in next major release
+ this.startLoc = tokStartLoc;
+ this.endLoc = tokEndLoc;
+ }
+ if (options.ranges)
+ this.range = [tokStart, tokEnd];
+ }
+
+ exports.Token = Token;
+
+ // Acorn is organized as a tokenizer and a recursive-descent parser.
+ // The `tokenize` export provides an interface to the tokenizer.
+ // Because the tokenizer is optimized for being efficiently used by
+ // the Acorn parser itself, this interface is somewhat crude and not
+ // very modular. Performing another parse or call to `tokenize` will
+ // reset the internal state, and invalidate existing tokenizers.
+
+ exports.tokenize = function(inpt, opts) {
+ input = String(inpt); inputLen = input.length;
+ setOptions(opts);
+ initTokenState();
+ skipSpace();
+
+ function getToken(forceRegexp) {
+ lastEnd = tokEnd;
+ readToken(forceRegexp);
+ return new Token();
+ }
+ getToken.jumpTo = function(pos, reAllowed) {
+ tokPos = pos;
+ if (options.locations) {
+ tokCurLine = 1;
+ tokLineStart = lineBreak.lastIndex = 0;
+ var match;
+ while ((match = lineBreak.exec(input)) && match.index < pos) {
+ ++tokCurLine;
+ tokLineStart = match.index + match[0].length;
+ }
+ }
+ tokRegexpAllowed = reAllowed;
+ skipSpace();
+ };
+ getToken.noRegexp = function() {
+ tokRegexpAllowed = false;
+ };
+ getToken.options = options;
+ return getToken;
+ };
+
+ // State is kept in (closure-)global variables. We already saw the
+ // `options`, `input`, and `inputLen` variables above.
+
+ // The current position of the tokenizer in the input.
+
+ var tokPos;
+
+ // The start and end offsets of the current token.
+
+ var tokStart, tokEnd;
+
+ // When `options.locations` is true, these hold objects
+ // containing the tokens start and end line/column pairs.
+
+ var tokStartLoc, tokEndLoc;
+
+ // The type and value of the current token. Token types are objects,
+ // named by variables against which they can be compared, and
+ // holding properties that describe them (indicating, for example,
+ // the precedence of an infix operator, and the original name of a
+ // keyword token). The kind of value that's held in `tokVal` depends
+ // on the type of the token. For literals, it is the literal value,
+ // for operators, the operator name, and so on.
+
+ var tokType, tokVal;
+
+ // Internal state for the tokenizer. To distinguish between division
+ // operators and regular expressions, it remembers whether the last
+ // token was one that is allowed to be followed by an expression.
+ // (If it is, a slash is probably a regexp, if it isn't it's a
+ // division operator. See the `parseStatement` function for a
+ // caveat.)
+
+ var tokRegexpAllowed;
+
+ // When `options.locations` is true, these are used to keep
+ // track of the current line, and know when a new line has been
+ // entered.
+
+ var tokCurLine, tokLineStart;
+
+ // These store the position of the previous token, which is useful
+ // when finishing a node and assigning its `end` position.
+
+ var lastStart, lastEnd, lastEndLoc;
+
+ // This is the parser's state. `inFunction` is used to reject
+ // `return` statements outside of functions, `inGenerator` to
+ // reject `yield`s outside of generators, `labels` to verify
+ // that `break` and `continue` have somewhere to jump to, and
+ // `strict` indicates whether strict mode is on.
+
+ var inFunction, inGenerator, labels, strict;
+
+ // This counter is used for checking that arrow expressions did
+ // not contain nested parentheses in argument list.
+
+ var metParenL;
+
+ // This is used by the tokenizer to track the template strings it is
+ // inside, and count the amount of open braces seen inside them, to
+ // be able to switch back to a template token when the } to match ${
+ // is encountered. It will hold an array of integers.
+
+ var templates;
+
+ function initParserState() {
+ lastStart = lastEnd = tokPos;
+ if (options.locations) lastEndLoc = curPosition();
+ inFunction = inGenerator = strict = false;
+ labels = [];
+ skipSpace();
+ readToken();
+ }
+
+ // This function is used to raise exceptions on parse errors. It
+ // takes an offset integer (into the current `input`) to indicate
+ // the location of the error, attaches the position to the end
+ // of the error message, and then raises a `SyntaxError` with that
+ // message.
+
+ function raise(pos, message) {
+ var loc = getLineInfo(input, pos);
+ message += " (" + loc.line + ":" + loc.column + ")";
+ var err = new SyntaxError(message);
+ err.pos = pos; err.loc = loc; err.raisedAt = tokPos;
+ throw err;
+ }
+
+ // Reused empty array added for node fields that are always empty.
+
+ var empty = [];
+
+ // ## Token types
+
+ // The assignment of fine-grained, information-carrying type objects
+ // allows the tokenizer to store the information it has about a
+ // token in a way that is very cheap for the parser to look up.
+
+ // All token type variables start with an underscore, to make them
+ // easy to recognize.
+
+ // These are the general types. The `type` property is only used to
+ // make them recognizeable when debugging.
+
+ var _num = {type: "num"}, _regexp = {type: "regexp"}, _string = {type: "string"};
+ var _name = {type: "name"}, _eof = {type: "eof"};
+
+ // Keyword tokens. The `keyword` property (also used in keyword-like
+ // operators) indicates that the token originated from an
+ // identifier-like word, which is used when parsing property names.
+ //
+ // The `beforeExpr` property is used to disambiguate between regular
+ // expressions and divisions. It is set on all token types that can
+ // be followed by an expression (thus, a slash after them would be a
+ // regular expression).
+ //
+ // `isLoop` marks a keyword as starting a loop, which is important
+ // to know when parsing a label, in order to allow or disallow
+ // continue jumps to that label.
+
+ var _break = {keyword: "break"}, _case = {keyword: "case", beforeExpr: true}, _catch = {keyword: "catch"};
+ var _continue = {keyword: "continue"}, _debugger = {keyword: "debugger"}, _default = {keyword: "default"};
+ var _do = {keyword: "do", isLoop: true}, _else = {keyword: "else", beforeExpr: true};
+ var _finally = {keyword: "finally"}, _for = {keyword: "for", isLoop: true}, _function = {keyword: "function"};
+ var _if = {keyword: "if"}, _return = {keyword: "return", beforeExpr: true}, _switch = {keyword: "switch"};
+ var _throw = {keyword: "throw", beforeExpr: true}, _try = {keyword: "try"}, _var = {keyword: "var"};
+ var _let = {keyword: "let"}, _const = {keyword: "const"};
+ var _while = {keyword: "while", isLoop: true}, _with = {keyword: "with"}, _new = {keyword: "new", beforeExpr: true};
+ var _this = {keyword: "this"};
+ var _class = {keyword: "class"}, _extends = {keyword: "extends", beforeExpr: true};
+ var _export = {keyword: "export"}, _import = {keyword: "import"};
+ var _yield = {keyword: "yield", beforeExpr: true};
+
+ // The keywords that denote values.
+
+ var _null = {keyword: "null", atomValue: null}, _true = {keyword: "true", atomValue: true};
+ var _false = {keyword: "false", atomValue: false};
+
+ // Some keywords are treated as regular operators. `in` sometimes
+ // (when parsing `for`) needs to be tested against specifically, so
+ // we assign a variable name to it for quick comparing.
+
+ var _in = {keyword: "in", binop: 7, beforeExpr: true};
+
+ // Map keyword names to token types.
+
+ var keywordTypes = {"break": _break, "case": _case, "catch": _catch,
+ "continue": _continue, "debugger": _debugger, "default": _default,
+ "do": _do, "else": _else, "finally": _finally, "for": _for,
+ "function": _function, "if": _if, "return": _return, "switch": _switch,
+ "throw": _throw, "try": _try, "var": _var, "let": _let, "const": _const,
+ "while": _while, "with": _with,
+ "null": _null, "true": _true, "false": _false, "new": _new, "in": _in,
+ "instanceof": {keyword: "instanceof", binop: 7, beforeExpr: true}, "this": _this,
+ "typeof": {keyword: "typeof", prefix: true, beforeExpr: true},
+ "void": {keyword: "void", prefix: true, beforeExpr: true},
+ "delete": {keyword: "delete", prefix: true, beforeExpr: true},
+ "class": _class, "extends": _extends,
+ "export": _export, "import": _import, "yield": _yield};
+
+ // Punctuation token types. Again, the `type` property is purely for debugging.
+
+ var _bracketL = {type: "[", beforeExpr: true}, _bracketR = {type: "]"}, _braceL = {type: "{", beforeExpr: true};
+ var _braceR = {type: "}"}, _parenL = {type: "(", beforeExpr: true}, _parenR = {type: ")"};
+ var _comma = {type: ",", beforeExpr: true}, _semi = {type: ";", beforeExpr: true};
+ var _colon = {type: ":", beforeExpr: true}, _dot = {type: "."}, _question = {type: "?", beforeExpr: true};
+ var _arrow = {type: "=>", beforeExpr: true}, _template = {type: "template"}, _templateContinued = {type: "templateContinued"};
+ var _ellipsis = {type: "...", prefix: true, beforeExpr: true};
+
+ // Operators. These carry several kinds of properties to help the
+ // parser use them properly (the presence of these properties is
+ // what categorizes them as operators).
+ //
+ // `binop`, when present, specifies that this operator is a binary
+ // operator, and will refer to its precedence.
+ //
+ // `prefix` and `postfix` mark the operator as a prefix or postfix
+ // unary operator. `isUpdate` specifies that the node produced by
+ // the operator should be of type UpdateExpression rather than
+ // simply UnaryExpression (`++` and `--`).
+ //
+ // `isAssign` marks all of `=`, `+=`, `-=` etcetera, which act as
+ // binary operators with a very low precedence, that should result
+ // in AssignmentExpression nodes.
+
+ var _slash = {binop: 10, beforeExpr: true}, _eq = {isAssign: true, beforeExpr: true};
+ var _assign = {isAssign: true, beforeExpr: true};
+ var _incDec = {postfix: true, prefix: true, isUpdate: true}, _prefix = {prefix: true, beforeExpr: true};
+ var _logicalOR = {binop: 1, beforeExpr: true};
+ var _logicalAND = {binop: 2, beforeExpr: true};
+ var _bitwiseOR = {binop: 3, beforeExpr: true};
+ var _bitwiseXOR = {binop: 4, beforeExpr: true};
+ var _bitwiseAND = {binop: 5, beforeExpr: true};
+ var _equality = {binop: 6, beforeExpr: true};
+ var _relational = {binop: 7, beforeExpr: true};
+ var _bitShift = {binop: 8, beforeExpr: true};
+ var _plusMin = {binop: 9, prefix: true, beforeExpr: true};
+ var _modulo = {binop: 10, beforeExpr: true};
+
+ // '*' may be multiply or have special meaning in ES6
+ var _star = {binop: 10, beforeExpr: true};
+
+ // Provide access to the token types for external users of the
+ // tokenizer.
+
+ exports.tokTypes = {bracketL: _bracketL, bracketR: _bracketR, braceL: _braceL, braceR: _braceR,
+ parenL: _parenL, parenR: _parenR, comma: _comma, semi: _semi, colon: _colon,
+ dot: _dot, ellipsis: _ellipsis, question: _question, slash: _slash, eq: _eq,
+ name: _name, eof: _eof, num: _num, regexp: _regexp, string: _string,
+ arrow: _arrow, template: _template, templateContinued: _templateContinued, star: _star,
+ assign: _assign};
+ for (var kw in keywordTypes) exports.tokTypes["_" + kw] = keywordTypes[kw];
+
+ // This is a trick taken from Esprima. It turns out that, on
+ // non-Chrome browsers, to check whether a string is in a set, a
+ // predicate containing a big ugly `switch` statement is faster than
+ // a regular expression, and on Chrome the two are about on par.
+ // This function uses `eval` (non-lexical) to produce such a
+ // predicate from a space-separated string of words.
+ //
+ // It starts by sorting the words by length.
+
+ function makePredicate(words) {
+ words = words.split(" ");
+ var f = "", cats = [];
+ out: for (var i = 0; i < words.length; ++i) {
+ for (var j = 0; j < cats.length; ++j)
+ if (cats[j][0].length == words[i].length) {
+ cats[j].push(words[i]);
+ continue out;
+ }
+ cats.push([words[i]]);
+ }
+ function compareTo(arr) {
+ if (arr.length == 1) return f += "return str === " + JSON.stringify(arr[0]) + ";";
+ f += "switch(str){";
+ for (var i = 0; i < arr.length; ++i) f += "case " + JSON.stringify(arr[i]) + ":";
+ f += "return true}return false;";
+ }
+
+ // When there are more than three length categories, an outer
+ // switch first dispatches on the lengths, to save on comparisons.
+
+ if (cats.length > 3) {
+ cats.sort(function(a, b) {return b.length - a.length;});
+ f += "switch(str.length){";
+ for (var i = 0; i < cats.length; ++i) {
+ var cat = cats[i];
+ f += "case " + cat[0].length + ":";
+ compareTo(cat);
+ }
+ f += "}";
+
+ // Otherwise, simply generate a flat `switch` statement.
+
+ } else {
+ compareTo(words);
+ }
+ return new Function("str", f);
+ }
+
+ // The ECMAScript 3 reserved word list.
+
+ var isReservedWord3 = makePredicate("abstract boolean byte char class double enum export extends final float goto implements import int interface long native package private protected public short static super synchronized throws transient volatile");
+
+ // ECMAScript 5 reserved words.
+
+ var isReservedWord5 = makePredicate("class enum extends super const export import");
+
+ // The additional reserved words in strict mode.
+
+ var isStrictReservedWord = makePredicate("implements interface let package private protected public static yield");
+
+ // The forbidden variable names in strict mode.
+
+ var isStrictBadIdWord = makePredicate("eval arguments");
+
+ // And the keywords.
+
+ var ecma5AndLessKeywords = "break case catch continue debugger default do else finally for function if return switch throw try var while with null true false instanceof typeof void delete new in this";
+
+ var isEcma5AndLessKeyword = makePredicate(ecma5AndLessKeywords);
+
+ var isEcma6Keyword = makePredicate(ecma5AndLessKeywords + " let const class extends export import yield");
+
+ var isKeyword = isEcma5AndLessKeyword;
+
+ // ## Character categories
+
+ // Big ugly regular expressions that match characters in the
+ // whitespace, identifier, and identifier-start categories. These
+ // are only applied when a character is found to actually have a
+ // code point above 128.
+ // Generated by `tools/generate-identifier-regex.js`.
+
+ var nonASCIIwhitespace = /[\u1680\u180e\u2000-\u200a\u202f\u205f\u3000\ufeff]/;
+ var nonASCIIidentifierStartChars = "\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0-\u08B2\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58\u0C59\u0C60\u0C61\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D60\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F4\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19C1-\u19C7\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FCC\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6EF\uA717-\uA71F\uA722-\uA788\uA78B-\uA78E\uA790-\uA7AD\uA7B0\uA7B1\uA7F7-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB5F\uAB64\uAB65\uABC0-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC";
+ var nonASCIIidentifierChars = "\u0300-\u036F\u0483-\u0487\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u0610-\u061A\u064B-\u0669\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7\u06E8\u06EA-\u06ED\u06F0-\u06F9\u0711\u0730-\u074A\u07A6-\u07B0\u07C0-\u07C9\u07EB-\u07F3\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u08E4-\u0903\u093A-\u093C\u093E-\u094F\u0951-\u0957\u0962\u0963\u0966-\u096F\u0981-\u0983\u09BC\u09BE-\u09C4\u09C7\u09C8\u09CB-\u09CD\u09D7\u09E2\u09E3\u09E6-\u09EF\u0A01-\u0A03\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A66-\u0A71\u0A75\u0A81-\u0A83\u0ABC\u0ABE-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AE2\u0AE3\u0AE6-\u0AEF\u0B01-\u0B03\u0B3C\u0B3E-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B62\u0B63\u0B66-\u0B6F\u0B82\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD7\u0BE6-\u0BEF\u0C00-\u0C03\u0C3E-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C62\u0C63\u0C66-\u0C6F\u0C81-\u0C83\u0CBC\u0CBE-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CE2\u0CE3\u0CE6-\u0CEF\u0D01-\u0D03\u0D3E-\u0D44\u0D46-\u0D48\u0D4A-\u0D4D\u0D57\u0D62\u0D63\u0D66-\u0D6F\u0D82\u0D83\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DE6-\u0DEF\u0DF2\u0DF3\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0E50-\u0E59\u0EB1\u0EB4-\u0EB9\u0EBB\u0EBC\u0EC8-\u0ECD\u0ED0-\u0ED9\u0F18\u0F19\u0F20-\u0F29\u0F35\u0F37\u0F39\u0F3E\u0F3F\u0F71-\u0F84\u0F86\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u102B-\u103E\u1040-\u1049\u1056-\u1059\u105E-\u1060\u1062-\u1064\u1067-\u106D\u1071-\u1074\u1082-\u108D\u108F-\u109D\u135D-\u135F\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17B4-\u17D3\u17DD\u17E0-\u17E9\u180B-\u180D\u1810-\u1819\u18A9\u1920-\u192B\u1930-\u193B\u1946-\u194F\u19B0-\u19C0\u19C8\u19C9\u19D0-\u19D9\u1A17-\u1A1B\u1A55-\u1A5E\u1A60-\u1A7C\u1A7F-\u1A89\u1A90-\u1A99\u1AB0-\u1ABD\u1B00-\u1B04\u1B34-\u1B44\u1B50-\u1B59\u1B6B-\u1B73\u1B80-\u1B82\u1BA1-\u1BAD\u1BB0-\u1BB9\u1BE6-\u1BF3\u1C24-\u1C37\u1C40-\u1C49\u1C50-\u1C59\u1CD0-\u1CD2\u1CD4-\u1CE8\u1CED\u1CF2-\u1CF4\u1CF8\u1CF9\u1DC0-\u1DF5\u1DFC-\u1DFF\u200C\u200D\u203F\u2040\u2054\u20D0-\u20DC\u20E1\u20E5-\u20F0\u2CEF-\u2CF1\u2D7F\u2DE0-\u2DFF\u302A-\u302F\u3099\u309A\uA620-\uA629\uA66F\uA674-\uA67D\uA69F\uA6F0\uA6F1\uA802\uA806\uA80B\uA823-\uA827\uA880\uA881\uA8B4-\uA8C4\uA8D0-\uA8D9\uA8E0-\uA8F1\uA900-\uA909\uA926-\uA92D\uA947-\uA953\uA980-\uA983\uA9B3-\uA9C0\uA9D0-\uA9D9\uA9E5\uA9F0-\uA9F9\uAA29-\uAA36\uAA43\uAA4C\uAA4D\uAA50-\uAA59\uAA7B-\uAA7D\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAEB-\uAAEF\uAAF5\uAAF6\uABE3-\uABEA\uABEC\uABED\uABF0-\uABF9\uFB1E\uFE00-\uFE0F\uFE20-\uFE2D\uFE33\uFE34\uFE4D-\uFE4F\uFF10-\uFF19\uFF3F";
+ var nonASCIIidentifierStart = new RegExp("[" + nonASCIIidentifierStartChars + "]");
+ var nonASCIIidentifier = new RegExp("[" + nonASCIIidentifierStartChars + nonASCIIidentifierChars + "]");
+
+ // Whether a single character denotes a newline.
+
+ var newline = /[\n\r\u2028\u2029]/;
+
+ function isNewLine(code) {
+ return code === 10 || code === 13 || code === 0x2028 || code == 0x2029;
+ }
+
+ // Matches a whole line break (where CRLF is considered a single
+ // line break). Used to count lines.
+
+ var lineBreak = /\r\n|[\n\r\u2028\u2029]/g;
+
+ // Test whether a given character code starts an identifier.
+
+ var isIdentifierStart = exports.isIdentifierStart = function(code) {
+ if (code < 65) return code === 36;
+ if (code < 91) return true;
+ if (code < 97) return code === 95;
+ if (code < 123)return true;
+ return code >= 0xaa && nonASCIIidentifierStart.test(String.fromCharCode(code));
+ };
+
+ // Test whether a given character is part of an identifier.
+
+ var isIdentifierChar = exports.isIdentifierChar = function(code) {
+ if (code < 48) return code === 36;
+ if (code < 58) return true;
+ if (code < 65) return false;
+ if (code < 91) return true;
+ if (code < 97) return code === 95;
+ if (code < 123)return true;
+ return code >= 0xaa && nonASCIIidentifier.test(String.fromCharCode(code));
+ };
+
+ // ## Tokenizer
+
+ // These are used when `options.locations` is on, for the
+ // `tokStartLoc` and `tokEndLoc` properties.
+
+ function Position(line, col) {
+ this.line = line;
+ this.column = col;
+ }
+
+ Position.prototype.offset = function(n) {
+ return new Position(this.line, this.column + n);
+ }
+
+ function curPosition() {
+ return new Position(tokCurLine, tokPos - tokLineStart);
+ }
+
+ // Reset the token state. Used at the start of a parse.
+
+ function initTokenState(pos) {
+ if (pos) {
+ tokPos = pos;
+ tokLineStart = Math.max(0, input.lastIndexOf("\n", pos));
+ tokCurLine = input.slice(0, tokLineStart).split(newline).length;
+ } else {
+ tokCurLine = 1;
+ tokPos = tokLineStart = 0;
+ }
+ tokRegexpAllowed = true;
+ metParenL = 0;
+ templates = [];
+ }
+
+ // Called at the end of every token. Sets `tokEnd`, `tokVal`, and
+ // `tokRegexpAllowed`, and skips the space after the token, so that
+ // the next one's `tokStart` will point at the right position.
+
+ function finishToken(type, val, shouldSkipSpace) {
+ tokEnd = tokPos;
+ if (options.locations) tokEndLoc = curPosition();
+ tokType = type;
+ if (shouldSkipSpace !== false) skipSpace();
+ tokVal = val;
+ tokRegexpAllowed = type.beforeExpr;
+ if (options.onToken) {
+ options.onToken(new Token());
+ }
+ }
+
+ function skipBlockComment() {
+ var startLoc = options.onComment && options.locations && curPosition();
+ var start = tokPos, end = input.indexOf("*/", tokPos += 2);
+ if (end === -1) raise(tokPos - 2, "Unterminated comment");
+ tokPos = end + 2;
+ if (options.locations) {
+ lineBreak.lastIndex = start;
+ var match;
+ while ((match = lineBreak.exec(input)) && match.index < tokPos) {
+ ++tokCurLine;
+ tokLineStart = match.index + match[0].length;
+ }
+ }
+ if (options.onComment)
+ options.onComment(true, input.slice(start + 2, end), start, tokPos,
+ startLoc, options.locations && curPosition());
+ }
+
+ function skipLineComment(startSkip) {
+ var start = tokPos;
+ var startLoc = options.onComment && options.locations && curPosition();
+ var ch = input.charCodeAt(tokPos+=startSkip);
+ while (tokPos < inputLen && ch !== 10 && ch !== 13 && ch !== 8232 && ch !== 8233) {
+ ++tokPos;
+ ch = input.charCodeAt(tokPos);
+ }
+ if (options.onComment)
+ options.onComment(false, input.slice(start + startSkip, tokPos), start, tokPos,
+ startLoc, options.locations && curPosition());
+ }
+
+ // Called at the start of the parse and after every token. Skips
+ // whitespace and comments, and.
+
+ function skipSpace() {
+ while (tokPos < inputLen) {
+ var ch = input.charCodeAt(tokPos);
+ if (ch === 32) { // ' '
+ ++tokPos;
+ } else if (ch === 13) {
+ ++tokPos;
+ var next = input.charCodeAt(tokPos);
+ if (next === 10) {
+ ++tokPos;
+ }
+ if (options.locations) {
+ ++tokCurLine;
+ tokLineStart = tokPos;
+ }
+ } else if (ch === 10 || ch === 8232 || ch === 8233) {
+ ++tokPos;
+ if (options.locations) {
+ ++tokCurLine;
+ tokLineStart = tokPos;
+ }
+ } else if (ch > 8 && ch < 14) {
+ ++tokPos;
+ } else if (ch === 47) { // '/'
+ var next = input.charCodeAt(tokPos + 1);
+ if (next === 42) { // '*'
+ skipBlockComment();
+ } else if (next === 47) { // '/'
+ skipLineComment(2);
+ } else break;
+ } else if (ch === 160) { // '\xa0'
+ ++tokPos;
+ } else if (ch >= 5760 && nonASCIIwhitespace.test(String.fromCharCode(ch))) {
+ ++tokPos;
+ } else {
+ break;
+ }
+ }
+ }
+
+ // ### Token reading
+
+ // This is the function that is called to fetch the next token. It
+ // is somewhat obscure, because it works in character codes rather
+ // than characters, and because operator parsing has been inlined
+ // into it.
+ //
+ // All in the name of speed.
+ //
+ // The `forceRegexp` parameter is used in the one case where the
+ // `tokRegexpAllowed` trick does not work. See `parseStatement`.
+
+ function readToken_dot() {
+ var next = input.charCodeAt(tokPos + 1);
+ if (next >= 48 && next <= 57) return readNumber(true);
+ var next2 = input.charCodeAt(tokPos + 2);
+ if (options.ecmaVersion >= 6 && next === 46 && next2 === 46) { // 46 = dot '.'
+ tokPos += 3;
+ return finishToken(_ellipsis);
+ } else {
+ ++tokPos;
+ return finishToken(_dot);
+ }
+ }
+
+ function readToken_slash() { // '/'
+ var next = input.charCodeAt(tokPos + 1);
+ if (tokRegexpAllowed) {++tokPos; return readRegexp();}
+ if (next === 61) return finishOp(_assign, 2);
+ return finishOp(_slash, 1);
+ }
+
+ function readToken_mult_modulo(code) { // '%*'
+ var next = input.charCodeAt(tokPos + 1);
+ if (next === 61) return finishOp(_assign, 2);
+ return finishOp(code === 42 ? _star : _modulo, 1);
+ }
+
+ function readToken_pipe_amp(code) { // '|&'
+ var next = input.charCodeAt(tokPos + 1);
+ if (next === code) return finishOp(code === 124 ? _logicalOR : _logicalAND, 2);
+ if (next === 61) return finishOp(_assign, 2);
+ return finishOp(code === 124 ? _bitwiseOR : _bitwiseAND, 1);
+ }
+
+ function readToken_caret() { // '^'
+ var next = input.charCodeAt(tokPos + 1);
+ if (next === 61) return finishOp(_assign, 2);
+ return finishOp(_bitwiseXOR, 1);
+ }
+
+ function readToken_plus_min(code) { // '+-'
+ var next = input.charCodeAt(tokPos + 1);
+ if (next === code) {
+ if (next == 45 && input.charCodeAt(tokPos + 2) == 62 &&
+ newline.test(input.slice(lastEnd, tokPos))) {
+ // A `-->` line comment
+ skipLineComment(3);
+ skipSpace();
+ return readToken();
+ }
+ return finishOp(_incDec, 2);
+ }
+ if (next === 61) return finishOp(_assign, 2);
+ return finishOp(_plusMin, 1);
+ }
+
+ function readToken_lt_gt(code) { // '<>'
+ var next = input.charCodeAt(tokPos + 1);
+ var size = 1;
+ if (next === code) {
+ size = code === 62 && input.charCodeAt(tokPos + 2) === 62 ? 3 : 2;
+ if (input.charCodeAt(tokPos + size) === 61) return finishOp(_assign, size + 1);
+ return finishOp(_bitShift, size);
+ }
+ if (next == 33 && code == 60 && input.charCodeAt(tokPos + 2) == 45 &&
+ input.charCodeAt(tokPos + 3) == 45) {
+ // `<!--`, an XML-style comment that should be interpreted as a line comment
+ skipLineComment(4);
+ skipSpace();
+ return readToken();
+ }
+ if (next === 61)
+ size = input.charCodeAt(tokPos + 2) === 61 ? 3 : 2;
+ return finishOp(_relational, size);
+ }
+
+ function readToken_eq_excl(code) { // '=!', '=>'
+ var next = input.charCodeAt(tokPos + 1);
+ if (next === 61) return finishOp(_equality, input.charCodeAt(tokPos + 2) === 61 ? 3 : 2);
+ if (code === 61 && next === 62 && options.ecmaVersion >= 6) { // '=>'
+ tokPos += 2;
+ return finishToken(_arrow);
+ }
+ return finishOp(code === 61 ? _eq : _prefix, 1);
+ }
+
+ function getTokenFromCode(code) {
+ switch (code) {
+ // The interpretation of a dot depends on whether it is followed
+ // by a digit or another two dots.
+ case 46: // '.'
+ return readToken_dot();
+
+ // Punctuation tokens.
+ case 40: ++tokPos; return finishToken(_parenL);
+ case 41: ++tokPos; return finishToken(_parenR);
+ case 59: ++tokPos; return finishToken(_semi);
+ case 44: ++tokPos; return finishToken(_comma);
+ case 91: ++tokPos; return finishToken(_bracketL);
+ case 93: ++tokPos; return finishToken(_bracketR);
+ case 123:
+ ++tokPos;
+ if (templates.length) ++templates[templates.length - 1];
+ return finishToken(_braceL);
+ case 125:
+ ++tokPos;
+ if (templates.length && --templates[templates.length - 1] === 0)
+ return readTemplateString(_templateContinued);
+ else
+ return finishToken(_braceR);
+ case 58: ++tokPos; return finishToken(_colon);
+ case 63: ++tokPos; return finishToken(_question);
+
+ case 96: // '`'
+ if (options.ecmaVersion >= 6) {
+ ++tokPos;
+ return readTemplateString(_template);
+ }
+
+ case 48: // '0'
+ var next = input.charCodeAt(tokPos + 1);
+ if (next === 120 || next === 88) return readRadixNumber(16); // '0x', '0X' - hex number
+ if (options.ecmaVersion >= 6) {
+ if (next === 111 || next === 79) return readRadixNumber(8); // '0o', '0O' - octal number
+ if (next === 98 || next === 66) return readRadixNumber(2); // '0b', '0B' - binary number
+ }
+ // Anything else beginning with a digit is an integer, octal
+ // number, or float.
+ case 49: case 50: case 51: case 52: case 53: case 54: case 55: case 56: case 57: // 1-9
+ return readNumber(false);
+
+ // Quotes produce strings.
+ case 34: case 39: // '"', "'"
+ return readString(code);
+
+ // Operators are parsed inline in tiny state machines. '=' (61) is
+ // often referred to. `finishOp` simply skips the amount of
+ // characters it is given as second argument, and returns a token
+ // of the type given by its first argument.
+
+ case 47: // '/'
+ return readToken_slash();
+
+ case 37: case 42: // '%*'
+ return readToken_mult_modulo(code);
+
+ case 124: case 38: // '|&'
+ return readToken_pipe_amp(code);
+
+ case 94: // '^'
+ return readToken_caret();
+
+ case 43: case 45: // '+-'
+ return readToken_plus_min(code);
+
+ case 60: case 62: // '<>'
+ return readToken_lt_gt(code);
+
+ case 61: case 33: // '=!'
+ return readToken_eq_excl(code);
+
+ case 126: // '~'
+ return finishOp(_prefix, 1);
+ }
+
+ return false;
+ }
+
+ function readToken(forceRegexp) {
+ if (!forceRegexp) tokStart = tokPos;
+ else tokPos = tokStart + 1;
+ if (options.locations) tokStartLoc = curPosition();
+ if (forceRegexp) return readRegexp();
+ if (tokPos >= inputLen) return finishToken(_eof);
+
+ var code = input.charCodeAt(tokPos);
+
+ // Identifier or keyword. '\uXXXX' sequences are allowed in
+ // identifiers, so '\' also dispatches to that.
+ if (isIdentifierStart(code) || code === 92 /* '\' */) return readWord();
+
+ var tok = getTokenFromCode(code);
+
+ if (tok === false) {
+ // If we are here, we either found a non-ASCII identifier
+ // character, or something that's entirely disallowed.
+ var ch = String.fromCharCode(code);
+ if (ch === "\\" || nonASCIIidentifierStart.test(ch)) return readWord();
+ raise(tokPos, "Unexpected character '" + ch + "'");
+ }
+ return tok;
+ }
+
+ function finishOp(type, size) {
+ var str = input.slice(tokPos, tokPos + size);
+ tokPos += size;
+ finishToken(type, str);
+ }
+
+ var regexpUnicodeSupport = false;
+ try { new RegExp("\uffff", "u"); regexpUnicodeSupport = true; }
+ catch(e) {}
+
+ // Parse a regular expression. Some context-awareness is necessary,
+ // since a '/' inside a '[]' set does not end the expression.
+
+ function readRegexp() {
+ var content = "", escaped, inClass, start = tokPos;
+ for (;;) {
+ if (tokPos >= inputLen) raise(start, "Unterminated regular expression");
+ var ch = input.charAt(tokPos);
+ if (newline.test(ch)) raise(start, "Unterminated regular expression");
+ if (!escaped) {
+ if (ch === "[") inClass = true;
+ else if (ch === "]" && inClass) inClass = false;
+ else if (ch === "/" && !inClass) break;
+ escaped = ch === "\\";
+ } else escaped = false;
+ ++tokPos;
+ }
+ var content = input.slice(start, tokPos);
+ ++tokPos;
+ // Need to use `readWord1` because '\uXXXX' sequences are allowed
+ // here (don't ask).
+ var mods = readWord1();
+ var tmp = content;
+ if (mods) {
+ var validFlags = /^[gmsiy]*$/;
+ if (options.ecmaVersion >= 6) validFlags = /^[gmsiyu]*$/;
+ if (!validFlags.test(mods)) raise(start, "Invalid regular expression flag");
+ if (mods.indexOf('u') >= 0 && !regexpUnicodeSupport) {
+ // Replace each astral symbol and every Unicode code point
+ // escape sequence that represents such a symbol with a single
+ // ASCII symbol to avoid throwing on regular expressions that
+ // are only valid in combination with the `/u` flag.
+ tmp = tmp
+ .replace(/\\u\{([0-9a-fA-F]{5,6})\}/g, "x")
+ .replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g, "x");
+ }
+ }
+ // Detect invalid regular expressions.
+ try {
+ new RegExp(tmp);
+ } catch (e) {
+ if (e instanceof SyntaxError) raise(start, "Error parsing regular expression: " + e.message);
+ raise(e);
+ }
+ // Get a regular expression object for this pattern-flag pair, or `null` in
+ // case the current environment doesn't support the flags it uses.
+ try {
+ var value = new RegExp(content, mods);
+ } catch (err) {
+ value = null;
+ }
+ return finishToken(_regexp, {pattern: content, flags: mods, value: value});
+ }
+
+ // Read an integer in the given radix. Return null if zero digits
+ // were read, the integer value otherwise. When `len` is given, this
+ // will return `null` unless the integer has exactly `len` digits.
+
+ function readInt(radix, len) {
+ var start = tokPos, total = 0;
+ for (var i = 0, e = len == null ? Infinity : len; i < e; ++i) {
+ var code = input.charCodeAt(tokPos), val;
+ if (code >= 97) val = code - 97 + 10; // a
+ else if (code >= 65) val = code - 65 + 10; // A
+ else if (code >= 48 && code <= 57) val = code - 48; // 0-9
+ else val = Infinity;
+ if (val >= radix) break;
+ ++tokPos;
+ total = total * radix + val;
+ }
+ if (tokPos === start || len != null && tokPos - start !== len) return null;
+
+ return total;
+ }
+
+ function readRadixNumber(radix) {
+ tokPos += 2; // 0x
+ var val = readInt(radix);
+ if (val == null) raise(tokStart + 2, "Expected number in radix " + radix);
+ if (isIdentifierStart(input.charCodeAt(tokPos))) raise(tokPos, "Identifier directly after number");
+ return finishToken(_num, val);
+ }
+
+ // Read an integer, octal integer, or floating-point number.
+
+ function readNumber(startsWithDot) {
+ var start = tokPos, isFloat = false, octal = input.charCodeAt(tokPos) === 48;
+ if (!startsWithDot && readInt(10) === null) raise(start, "Invalid number");
+ if (input.charCodeAt(tokPos) === 46) {
+ ++tokPos;
+ readInt(10);
+ isFloat = true;
+ }
+ var next = input.charCodeAt(tokPos);
+ if (next === 69 || next === 101) { // 'eE'
+ next = input.charCodeAt(++tokPos);
+ if (next === 43 || next === 45) ++tokPos; // '+-'
+ if (readInt(10) === null) raise(start, "Invalid number");
+ isFloat = true;
+ }
+ if (isIdentifierStart(input.charCodeAt(tokPos))) raise(tokPos, "Identifier directly after number");
+
+ var str = input.slice(start, tokPos), val;
+ if (isFloat) val = parseFloat(str);
+ else if (!octal || str.length === 1) val = parseInt(str, 10);
+ else if (/[89]/.test(str) || strict) raise(start, "Invalid number");
+ else val = parseInt(str, 8);
+ return finishToken(_num, val);
+ }
+
+ // Read a string value, interpreting backslash-escapes.
+
+ function readCodePoint() {
+ var ch = input.charCodeAt(tokPos), code;
+
+ if (ch === 123) {
+ if (options.ecmaVersion < 6) unexpected();
+ ++tokPos;
+ code = readHexChar(input.indexOf('}', tokPos) - tokPos);
+ ++tokPos;
+ if (code > 0x10FFFF) unexpected();
+ } else {
+ code = readHexChar(4);
+ }
+
+ // UTF-16 Encoding
+ if (code <= 0xFFFF) {
+ return String.fromCharCode(code);
+ }
+ var cu1 = ((code - 0x10000) >> 10) + 0xD800;
+ var cu2 = ((code - 0x10000) & 1023) + 0xDC00;
+ return String.fromCharCode(cu1, cu2);
+ }
+
+ function readString(quote) {
+ ++tokPos;
+ var out = "";
+ for (;;) {
+ if (tokPos >= inputLen) raise(tokStart, "Unterminated string constant");
+ var ch = input.charCodeAt(tokPos);
+ if (ch === quote) {
+ ++tokPos;
+ return finishToken(_string, out);
+ }
+ if (ch === 92) { // '\'
+ out += readEscapedChar();
+ } else {
+ ++tokPos;
+ if (newline.test(String.fromCharCode(ch))) {
+ raise(tokStart, "Unterminated string constant");
+ }
+ out += String.fromCharCode(ch); // '\'
+ }
+ }
+ }
+
+ function readTemplateString(type) {
+ if (type == _templateContinued) templates.pop();
+ var out = "", start = tokPos;;
+ for (;;) {
+ if (tokPos >= inputLen) raise(tokStart, "Unterminated template");
+ var ch = input.charAt(tokPos);
+ if (ch === "`" || ch === "$" && input.charCodeAt(tokPos + 1) === 123) { // '`', '${'
+ var raw = input.slice(start, tokPos);
+ ++tokPos;
+ if (ch == "$") { ++tokPos; templates.push(1); }
+ return finishToken(type, {cooked: out, raw: raw});
+ }
+
+ if (ch === "\\") { // '\'
+ out += readEscapedChar();
+ } else {
+ ++tokPos;
+ if (newline.test(ch)) {
+ if (ch === "\r" && input.charCodeAt(tokPos) === 10) {
+ ++tokPos;
+ ch = "\n";
+ }
+ if (options.locations) {
+ ++tokCurLine;
+ tokLineStart = tokPos;
+ }
+ }
+ out += ch;
+ }
+ }
+ }
+
+ // Used to read escaped characters
+
+ function readEscapedChar() {
+ var ch = input.charCodeAt(++tokPos);
+ var octal = /^[0-7]+/.exec(input.slice(tokPos, tokPos + 3));
+ if (octal) octal = octal[0];
+ while (octal && parseInt(octal, 8) > 255) octal = octal.slice(0, -1);
+ if (octal === "0") octal = null;
+ ++tokPos;
+ if (octal) {
+ if (strict) raise(tokPos - 2, "Octal literal in strict mode");
+ tokPos += octal.length - 1;
+ return String.fromCharCode(parseInt(octal, 8));
+ } else {
+ switch (ch) {
+ case 110: return "\n"; // 'n' -> '\n'
+ case 114: return "\r"; // 'r' -> '\r'
+ case 120: return String.fromCharCode(readHexChar(2)); // 'x'
+ case 117: return readCodePoint(); // 'u'
+ case 116: return "\t"; // 't' -> '\t'
+ case 98: return "\b"; // 'b' -> '\b'
+ case 118: return "\u000b"; // 'v' -> '\u000b'
+ case 102: return "\f"; // 'f' -> '\f'
+ case 48: return "\0"; // 0 -> '\0'
+ case 13: if (input.charCodeAt(tokPos) === 10) ++tokPos; // '\r\n'
+ case 10: // ' \n'
+ if (options.locations) { tokLineStart = tokPos; ++tokCurLine; }
+ return "";
+ default: return String.fromCharCode(ch);
+ }
+ }
+ }
+
+ // Used to read character escape sequences ('\x', '\u', '\U').
+
+ function readHexChar(len) {
+ var n = readInt(16, len);
+ if (n === null) raise(tokStart, "Bad character escape sequence");
+ return n;
+ }
+
+ // Used to signal to callers of `readWord1` whether the word
+ // contained any escape sequences. This is needed because words with
+ // escape sequences must not be interpreted as keywords.
+
+ var containsEsc;
+
+ // Read an identifier, and return it as a string. Sets `containsEsc`
+ // to whether the word contained a '\u' escape.
+ //
+ // Only builds up the word character-by-character when it actually
+ // containeds an escape, as a micro-optimization.
+
+ function readWord1() {
+ containsEsc = false;
+ var word, first = true, start = tokPos;
+ for (;;) {
+ var ch = input.charCodeAt(tokPos);
+ if (isIdentifierChar(ch)) {
+ if (containsEsc) word += input.charAt(tokPos);
+ ++tokPos;
+ } else if (ch === 92) { // "\"
+ if (!containsEsc) word = input.slice(start, tokPos);
+ containsEsc = true;
+ if (input.charCodeAt(++tokPos) != 117) // "u"
+ raise(tokPos, "Expecting Unicode escape sequence \\uXXXX");
+ ++tokPos;
+ var esc = readHexChar(4);
+ var escStr = String.fromCharCode(esc);
+ if (!escStr) raise(tokPos - 1, "Invalid Unicode escape");
+ if (!(first ? isIdentifierStart(esc) : isIdentifierChar(esc)))
+ raise(tokPos - 4, "Invalid Unicode escape");
+ word += escStr;
+ } else {
+ break;
+ }
+ first = false;
+ }
+ return containsEsc ? word : input.slice(start, tokPos);
+ }
+
+ // Read an identifier or keyword token. Will check for reserved
+ // words when necessary.
+
+ function readWord() {
+ var word = readWord1();
+ var type = _name;
+ if (!containsEsc && isKeyword(word))
+ type = keywordTypes[word];
+ return finishToken(type, word);
+ }
+
+ // ## Parser
+
+ // A recursive descent parser operates by defining functions for all
+ // syntactic elements, and recursively calling those, each function
+ // advancing the input stream and returning an AST node. Precedence
+ // of constructs (for example, the fact that `!x[1]` means `!(x[1])`
+ // instead of `(!x)[1]` is handled by the fact that the parser
+ // function that parses unary prefix operators is called first, and
+ // in turn calls the function that parses `[]` subscripts — that
+ // way, it'll receive the node for `x[1]` already parsed, and wraps
+ // *that* in the unary operator node.
+ //
+ // Acorn uses an [operator precedence parser][opp] to handle binary
+ // operator precedence, because it is much more compact than using
+ // the technique outlined above, which uses different, nesting
+ // functions to specify precedence, for all of the ten binary
+ // precedence levels that JavaScript defines.
+ //
+ // [opp]: http://en.wikipedia.org/wiki/Operator-precedence_parser
+
+ // ### Parser utilities
+
+ // Continue to the next token.
+
+ function next() {
+ lastStart = tokStart;
+ lastEnd = tokEnd;
+ lastEndLoc = tokEndLoc;
+ readToken();
+ }
+
+ // Enter strict mode. Re-reads the next token to please pedantic
+ // tests ("use strict"; 010; -- should fail).
+
+ function setStrict(strct) {
+ strict = strct;
+ tokPos = tokStart;
+ if (options.locations) {
+ while (tokPos < tokLineStart) {
+ tokLineStart = input.lastIndexOf("\n", tokLineStart - 2) + 1;
+ --tokCurLine;
+ }
+ }
+ skipSpace();
+ readToken();
+ }
+
+ // Start an AST node, attaching a start offset.
+
+ function Node() {
+ this.type = null;
+ this.start = tokStart;
+ this.end = null;
+ }
+
+ exports.Node = Node;
+
+ function SourceLocation() {
+ this.start = tokStartLoc;
+ this.end = null;
+ if (sourceFile !== null) this.source = sourceFile;
+ }
+
+ function startNode() {
+ var node = new Node();
+ if (options.locations)
+ node.loc = new SourceLocation();
+ if (options.directSourceFile)
+ node.sourceFile = options.directSourceFile;
+ if (options.ranges)
+ node.range = [tokStart, 0];
+ return node;
+ }
+
+ // Sometimes, a node is only started *after* the token stream passed
+ // its start position. The functions below help storing a position
+ // and creating a node from a previous position.
+
+ function storeCurrentPos() {
+ return options.locations ? [tokStart, tokStartLoc] : tokStart;
+ }
+
+ function startNodeAt(pos) {
+ var node = new Node(), start = pos;
+ if (options.locations) {
+ node.loc = new SourceLocation();
+ node.loc.start = start[1];
+ start = pos[0];
+ }
+ node.start = start;
+ if (options.directSourceFile)
+ node.sourceFile = options.directSourceFile;
+ if (options.ranges)
+ node.range = [start, 0];
+
+ return node;
+ }
+
+ // Finish an AST node, adding `type` and `end` properties.
+
+ function finishNode(node, type) {
+ node.type = type;
+ node.end = lastEnd;
+ if (options.locations)
+ node.loc.end = lastEndLoc;
+ if (options.ranges)
+ node.range[1] = lastEnd;
+ return node;
+ }
+
+ function finishNodeAt(node, type, pos) {
+ if (options.locations) { node.loc.end = pos[1]; pos = pos[0]; }
+ node.type = type;
+ node.end = pos;
+ if (options.ranges)
+ node.range[1] = pos;
+ return node;
+ }
+
+ // Test whether a statement node is the string literal `"use strict"`.
+
+ function isUseStrict(stmt) {
+ return options.ecmaVersion >= 5 && stmt.type === "ExpressionStatement" &&
+ stmt.expression.type === "Literal" && stmt.expression.value === "use strict";
+ }
+
+ // Predicate that tests whether the next token is of the given
+ // type, and if yes, consumes it as a side effect.
+
+ function eat(type) {
+ if (tokType === type) {
+ next();
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ // Test whether a semicolon can be inserted at the current position.
+
+ function canInsertSemicolon() {
+ return !options.strictSemicolons &&
+ (tokType === _eof || tokType === _braceR || newline.test(input.slice(lastEnd, tokStart)));
+ }
+
+ // Consume a semicolon, or, failing that, see if we are allowed to
+ // pretend that there is a semicolon at this position.
+
+ function semicolon() {
+ if (!eat(_semi) && !canInsertSemicolon()) unexpected();
+ }
+
+ // Expect a token of a given type. If found, consume it, otherwise,
+ // raise an unexpected token error.
+
+ function expect(type) {
+ eat(type) || unexpected();
+ }
+
+ // Raise an unexpected token error.
+
+ function unexpected(pos) {
+ raise(pos != null ? pos : tokStart, "Unexpected token");
+ }
+
+ // Checks if hash object has a property.
+
+ function has(obj, propName) {
+ return Object.prototype.hasOwnProperty.call(obj, propName);
+ }
+ // Convert existing expression atom to assignable pattern
+ // if possible.
+
+ function toAssignable(node, allowSpread, checkType) {
+ if (options.ecmaVersion >= 6 && node) {
+ switch (node.type) {
+ case "Identifier":
+ case "MemberExpression":
+ break;
+
+ case "ObjectExpression":
+ node.type = "ObjectPattern";
+ for (var i = 0; i < node.properties.length; i++) {
+ var prop = node.properties[i];
+ if (prop.kind !== "init") unexpected(prop.key.start);
+ toAssignable(prop.value, false, checkType);
+ }
+ break;
+
+ case "ArrayExpression":
+ node.type = "ArrayPattern";
+ for (var i = 0, lastI = node.elements.length - 1; i <= lastI; i++) {
+ toAssignable(node.elements[i], i === lastI, checkType);
+ }
+ break;
+
+ case "SpreadElement":
+ if (allowSpread) {
+ toAssignable(node.argument, false, checkType);
+ checkSpreadAssign(node.argument);
+ } else {
+ unexpected(node.start);
+ }
+ break;
+
+ default:
+ if (checkType) unexpected(node.start);
+ }
+ }
+ return node;
+ }
+
+ // Checks if node can be assignable spread argument.
+
+ function checkSpreadAssign(node) {
+ if (node.type !== "Identifier" && node.type !== "ArrayPattern")
+ unexpected(node.start);
+ }
+
+ // Verify that argument names are not repeated, and it does not
+ // try to bind the words `eval` or `arguments`.
+
+ function checkFunctionParam(param, nameHash) {
+ switch (param.type) {
+ case "Identifier":
+ if (isStrictReservedWord(param.name) || isStrictBadIdWord(param.name))
+ raise(param.start, "Defining '" + param.name + "' in strict mode");
+ if (has(nameHash, param.name))
+ raise(param.start, "Argument name clash in strict mode");
+ nameHash[param.name] = true;
+ break;
+
+ case "ObjectPattern":
+ for (var i = 0; i < param.properties.length; i++)
+ checkFunctionParam(param.properties[i].value, nameHash);
+ break;
+
+ case "ArrayPattern":
+ for (var i = 0; i < param.elements.length; i++) {
+ var elem = param.elements[i];
+ if (elem) checkFunctionParam(elem, nameHash);
+ }
+ break;
+ }
+ }
+
+ // Check if property name clashes with already added.
+ // Object/class getters and setters are not allowed to clash —
+ // either with each other or with an init property — and in
+ // strict mode, init properties are also not allowed to be repeated.
+
+ function checkPropClash(prop, propHash) {
+ if (options.ecmaVersion >= 6) return;
+ var key = prop.key, name;
+ switch (key.type) {
+ case "Identifier": name = key.name; break;
+ case "Literal": name = String(key.value); break;
+ default: return;
+ }
+ var kind = prop.kind || "init", other;
+ if (has(propHash, name)) {
+ other = propHash[name];
+ var isGetSet = kind !== "init";
+ if ((strict || isGetSet) && other[kind] || !(isGetSet ^ other.init))
+ raise(key.start, "Redefinition of property");
+ } else {
+ other = propHash[name] = {
+ init: false,
+ get: false,
+ set: false
+ };
+ }
+ other[kind] = true;
+ }
+
+ // Verify that a node is an lval — something that can be assigned
+ // to.
+
+ function checkLVal(expr, isBinding) {
+ switch (expr.type) {
+ case "Identifier":
+ if (strict && (isStrictBadIdWord(expr.name) || isStrictReservedWord(expr.name)))
+ raise(expr.start, isBinding
+ ? "Binding " + expr.name + " in strict mode"
+ : "Assigning to " + expr.name + " in strict mode"
+ );
+ break;
+
+ case "MemberExpression":
+ if (!isBinding) break;
+
+ case "ObjectPattern":
+ for (var i = 0; i < expr.properties.length; i++)
+ checkLVal(expr.properties[i].value, isBinding);
+ break;
+
+ case "ArrayPattern":
+ for (var i = 0; i < expr.elements.length; i++) {
+ var elem = expr.elements[i];
+ if (elem) checkLVal(elem, isBinding);
+ }
+ break;
+
+ case "SpreadElement":
+ break;
+
+ default:
+ raise(expr.start, "Assigning to rvalue");
+ }
+ }
+
+ // ### Statement parsing
+
+ // Parse a program. Initializes the parser, reads any number of
+ // statements, and wraps them in a Program node. Optionally takes a
+ // `program` argument. If present, the statements will be appended
+ // to its body instead of creating a new node.
+
+ function parseTopLevel(node) {
+ var first = true;
+ if (!node.body) node.body = [];
+ while (tokType !== _eof) {
+ var stmt = parseStatement(true);
+ node.body.push(stmt);
+ if (first && isUseStrict(stmt)) setStrict(true);
+ first = false;
+ }
+
+ lastStart = tokStart;
+ lastEnd = tokEnd;
+ lastEndLoc = tokEndLoc;
+ return finishNode(node, "Program");
+ }
+
+ var loopLabel = {kind: "loop"}, switchLabel = {kind: "switch"};
+
+ // Parse a single statement.
+ //
+ // If expecting a statement and finding a slash operator, parse a
+ // regular expression literal. This is to handle cases like
+ // `if (foo) /blah/.exec(foo);`, where looking at the previous token
+ // does not help.
+
+ function parseStatement(topLevel) {
+ if (tokType === _slash || tokType === _assign && tokVal == "/=")
+ readToken(true);
+
+ var starttype = tokType, node = startNode();
+
+ // Most types of statements are recognized by the keyword they
+ // start with. Many are trivial to parse, some require a bit of
+ // complexity.
+
+ switch (starttype) {
+ case _break: case _continue: return parseBreakContinueStatement(node, starttype.keyword);
+ case _debugger: return parseDebuggerStatement(node);
+ case _do: return parseDoStatement(node);
+ case _for: return parseForStatement(node);
+ case _function: return parseFunctionStatement(node);
+ case _class: return parseClass(node, true);
+ case _if: return parseIfStatement(node);
+ case _return: return parseReturnStatement(node);
+ case _switch: return parseSwitchStatement(node);
+ case _throw: return parseThrowStatement(node);
+ case _try: return parseTryStatement(node);
+ case _var: case _let: case _const: return parseVarStatement(node, starttype.keyword);
+ case _while: return parseWhileStatement(node);
+ case _with: return parseWithStatement(node);
+ case _braceL: return parseBlock(); // no point creating a function for this
+ case _semi: return parseEmptyStatement(node);
+ case _export:
+ case _import:
+ if (!topLevel && !options.allowImportExportEverywhere)
+ raise(tokStart, "'import' and 'export' may only appear at the top level");
+ return starttype === _import ? parseImport(node) : parseExport(node);
+
+ // If the statement does not start with a statement keyword or a
+ // brace, it's an ExpressionStatement or LabeledStatement. We
+ // simply start parsing an expression, and afterwards, if the
+ // next token is a colon and the expression was a simple
+ // Identifier node, we switch to interpreting it as a label.
+ default:
+ var maybeName = tokVal, expr = parseExpression();
+ if (starttype === _name && expr.type === "Identifier" && eat(_colon))
+ return parseLabeledStatement(node, maybeName, expr);
+ else return parseExpressionStatement(node, expr);
+ }
+ }
+
+ function parseBreakContinueStatement(node, keyword) {
+ var isBreak = keyword == "break";
+ next();
+ if (eat(_semi) || canInsertSemicolon()) node.label = null;
+ else if (tokType !== _name) unexpected();
+ else {
+ node.label = parseIdent();
+ semicolon();
+ }
+
+ // Verify that there is an actual destination to break or
+ // continue to.
+ for (var i = 0; i < labels.length; ++i) {
+ var lab = labels[i];
+ if (node.label == null || lab.name === node.label.name) {
+ if (lab.kind != null && (isBreak || lab.kind === "loop")) break;
+ if (node.label && isBreak) break;
+ }
+ }
+ if (i === labels.length) raise(node.start, "Unsyntactic " + keyword);
+ return finishNode(node, isBreak ? "BreakStatement" : "ContinueStatement");
+ }
+
+ function parseDebuggerStatement(node) {
+ next();
+ semicolon();
+ return finishNode(node, "DebuggerStatement");
+ }
+
+ function parseDoStatement(node) {
+ next();
+ labels.push(loopLabel);
+ node.body = parseStatement();
+ labels.pop();
+ expect(_while);
+ node.test = parseParenExpression();
+ if (options.ecmaVersion >= 6)
+ eat(_semi);
+ else
+ semicolon();
+ return finishNode(node, "DoWhileStatement");
+ }
+
+ // Disambiguating between a `for` and a `for`/`in` or `for`/`of`
+ // loop is non-trivial. Basically, we have to parse the init `var`
+ // statement or expression, disallowing the `in` operator (see
+ // the second parameter to `parseExpression`), and then check
+ // whether the next token is `in` or `of`. When there is no init
+ // part (semicolon immediately after the opening parenthesis), it
+ // is a regular `for` loop.
+
+ function parseForStatement(node) {
+ next();
+ labels.push(loopLabel);
+ expect(_parenL);
+ if (tokType === _semi) return parseFor(node, null);
+ if (tokType === _var || tokType === _let) {
+ var init = startNode(), varKind = tokType.keyword, isLet = tokType === _let;
+ next();
+ parseVar(init, true, varKind);
+ finishNode(init, "VariableDeclaration");
+ if ((tokType === _in || (options.ecmaVersion >= 6 && tokType === _name && tokVal === "of")) && init.declarations.length === 1 &&
+ !(isLet && init.declarations[0].init))
+ return parseForIn(node, init);
+ return parseFor(node, init);
+ }
+ var init = parseExpression(false, true);
+ if (tokType === _in || (options.ecmaVersion >= 6 && tokType === _name && tokVal === "of")) {
+ checkLVal(init);
+ return parseForIn(node, init);
+ }
+ return parseFor(node, init);
+ }
+
+ function parseFunctionStatement(node) {
+ next();
+ return parseFunction(node, true);
+ }
+
+ function parseIfStatement(node) {
+ next();
+ node.test = parseParenExpression();
+ node.consequent = parseStatement();
+ node.alternate = eat(_else) ? parseStatement() : null;
+ return finishNode(node, "IfStatement");
+ }
+
+ function parseReturnStatement(node) {
+ if (!inFunction && !options.allowReturnOutsideFunction)
+ raise(tokStart, "'return' outside of function");
+ next();
+
+ // In `return` (and `break`/`continue`), the keywords with
+ // optional arguments, we eagerly look for a semicolon or the
+ // possibility to insert one.
+
+ if (eat(_semi) || canInsertSemicolon()) node.argument = null;
+ else { node.argument = parseExpression(); semicolon(); }
+ return finishNode(node, "ReturnStatement");
+ }
+
+ function parseSwitchStatement(node) {
+ next();
+ node.discriminant = parseParenExpression();
+ node.cases = [];
+ expect(_braceL);
+ labels.push(switchLabel);
+
+ // Statements under must be grouped (by label) in SwitchCase
+ // nodes. `cur` is used to keep the node that we are currently
+ // adding statements to.
+
+ for (var cur, sawDefault; tokType != _braceR;) {
+ if (tokType === _case || tokType === _default) {
+ var isCase = tokType === _case;
+ if (cur) finishNode(cur, "SwitchCase");
+ node.cases.push(cur = startNode());
+ cur.consequent = [];
+ next();
+ if (isCase) cur.test = parseExpression();
+ else {
+ if (sawDefault) raise(lastStart, "Multiple default clauses"); sawDefault = true;
+ cur.test = null;
+ }
+ expect(_colon);
+ } else {
+ if (!cur) unexpected();
+ cur.consequent.push(parseStatement());
+ }
+ }
+ if (cur) finishNode(cur, "SwitchCase");
+ next(); // Closing brace
+ labels.pop();
+ return finishNode(node, "SwitchStatement");
+ }
+
+ function parseThrowStatement(node) {
+ next();
+ if (newline.test(input.slice(lastEnd, tokStart)))
+ raise(lastEnd, "Illegal newline after throw");
+ node.argument = parseExpression();
+ semicolon();
+ return finishNode(node, "ThrowStatement");
+ }
+
+ function parseTryStatement(node) {
+ next();
+ node.block = parseBlock();
+ node.handler = null;
+ if (tokType === _catch) {
+ var clause = startNode();
+ next();
+ expect(_parenL);
+ clause.param = parseIdent();
+ if (strict && isStrictBadIdWord(clause.param.name))
+ raise(clause.param.start, "Binding " + clause.param.name + " in strict mode");
+ expect(_parenR);
+ clause.guard = null;
+ clause.body = parseBlock();
+ node.handler = finishNode(clause, "CatchClause");
+ }
+ node.guardedHandlers = empty;
+ node.finalizer = eat(_finally) ? parseBlock() : null;
+ if (!node.handler && !node.finalizer)
+ raise(node.start, "Missing catch or finally clause");
+ return finishNode(node, "TryStatement");
+ }
+
+ function parseVarStatement(node, kind) {
+ next();
+ parseVar(node, false, kind);
+ semicolon();
+ return finishNode(node, "VariableDeclaration");
+ }
+
+ function parseWhileStatement(node) {
+ next();
+ node.test = parseParenExpression();
+ labels.push(loopLabel);
+ node.body = parseStatement();
+ labels.pop();
+ return finishNode(node, "WhileStatement");
+ }
+
+ function parseWithStatement(node) {
+ if (strict) raise(tokStart, "'with' in strict mode");
+ next();
+ node.object = parseParenExpression();
+ node.body = parseStatement();
+ return finishNode(node, "WithStatement");
+ }
+
+ function parseEmptyStatement(node) {
+ next();
+ return finishNode(node, "EmptyStatement");
+ }
+
+ function parseLabeledStatement(node, maybeName, expr) {
+ for (var i = 0; i < labels.length; ++i)
+ if (labels[i].name === maybeName) raise(expr.start, "Label '" + maybeName + "' is already declared");
+ var kind = tokType.isLoop ? "loop" : tokType === _switch ? "switch" : null;
+ labels.push({name: maybeName, kind: kind});
+ node.body = parseStatement();
+ labels.pop();
+ node.label = expr;
+ return finishNode(node, "LabeledStatement");
+ }
+
+ function parseExpressionStatement(node, expr) {
+ node.expression = expr;
+ semicolon();
+ return finishNode(node, "ExpressionStatement");
+ }
+
+ // Used for constructs like `switch` and `if` that insist on
+ // parentheses around their expression.
+
+ function parseParenExpression() {
+ expect(_parenL);
+ var val = parseExpression();
+ expect(_parenR);
+ return val;
+ }
+
+ // Parse a semicolon-enclosed block of statements, handling `"use
+ // strict"` declarations when `allowStrict` is true (used for
+ // function bodies).
+
+ function parseBlock(allowStrict) {
+ var node = startNode(), first = true, oldStrict;
+ node.body = [];
+ expect(_braceL);
+ while (!eat(_braceR)) {
+ var stmt = parseStatement();
+ node.body.push(stmt);
+ if (first && allowStrict && isUseStrict(stmt)) {
+ oldStrict = strict;
+ setStrict(strict = true);
+ }
+ first = false;
+ }
+ if (oldStrict === false) setStrict(false);
+ return finishNode(node, "BlockStatement");
+ }
+
+ // Parse a regular `for` loop. The disambiguation code in
+ // `parseStatement` will already have parsed the init statement or
+ // expression.
+
+ function parseFor(node, init) {
+ node.init = init;
+ expect(_semi);
+ node.test = tokType === _semi ? null : parseExpression();
+ expect(_semi);
+ node.update = tokType === _parenR ? null : parseExpression();
+ expect(_parenR);
+ node.body = parseStatement();
+ labels.pop();
+ return finishNode(node, "ForStatement");
+ }
+
+ // Parse a `for`/`in` and `for`/`of` loop, which are almost
+ // same from parser's perspective.
+
+ function parseForIn(node, init) {
+ var type = tokType === _in ? "ForInStatement" : "ForOfStatement";
+ next();
+ node.left = init;
+ node.right = parseExpression();
+ expect(_parenR);
+ node.body = parseStatement();
+ labels.pop();
+ return finishNode(node, type);
+ }
+
+ // Parse a list of variable declarations.
+
+ function parseVar(node, noIn, kind) {
+ node.declarations = [];
+ node.kind = kind;
+ for (;;) {
+ var decl = startNode();
+ decl.id = options.ecmaVersion >= 6 ? toAssignable(parseExprAtom()) : parseIdent();
+ checkLVal(decl.id, true);
+ decl.init = eat(_eq) ? parseExpression(true, noIn) : (kind === _const.keyword ? unexpected() : null);
+ node.declarations.push(finishNode(decl, "VariableDeclarator"));
+ if (!eat(_comma)) break;
+ }
+ return node;
+ }
+
+ // ### Expression parsing
+
+ // These nest, from the most general expression type at the top to
+ // 'atomic', nondivisible expression types at the bottom. Most of
+ // the functions will simply let the function(s) below them parse,
+ // and, *if* the syntactic construct they handle is present, wrap
+ // the AST node that the inner parser gave them in another node.
+
+ // Parse a full expression. The arguments are used to forbid comma
+ // sequences (in argument lists, array literals, or object literals)
+ // or the `in` operator (in for loops initalization expressions).
+
+ function parseExpression(noComma, noIn) {
+ var start = storeCurrentPos();
+ var expr = parseMaybeAssign(noIn);
+ if (!noComma && tokType === _comma) {
+ var node = startNodeAt(start);
+ node.expressions = [expr];
+ while (eat(_comma)) node.expressions.push(parseMaybeAssign(noIn));
+ return finishNode(node, "SequenceExpression");
+ }
+ return expr;
+ }
+
+ // Parse an assignment expression. This includes applications of
+ // operators like `+=`.
+
+ function parseMaybeAssign(noIn) {
+ var start = storeCurrentPos();
+ var left = parseMaybeConditional(noIn);
+ if (tokType.isAssign) {
+ var node = startNodeAt(start);
+ node.operator = tokVal;
+ node.left = tokType === _eq ? toAssignable(left) : left;
+ checkLVal(left);
+ next();
+ node.right = parseMaybeAssign(noIn);
+ return finishNode(node, "AssignmentExpression");
+ }
+ return left;
+ }
+
+ // Parse a ternary conditional (`?:`) operator.
+
+ function parseMaybeConditional(noIn) {
+ var start = storeCurrentPos();
+ var expr = parseExprOps(noIn);
+ if (eat(_question)) {
+ var node = startNodeAt(start);
+ node.test = expr;
+ node.consequent = parseExpression(true);
+ expect(_colon);
+ node.alternate = parseExpression(true, noIn);
+ return finishNode(node, "ConditionalExpression");
+ }
+ return expr;
+ }
+
+ // Start the precedence parser.
+
+ function parseExprOps(noIn) {
+ var start = storeCurrentPos();
+ return parseExprOp(parseMaybeUnary(), start, -1, noIn);
+ }
+
+ // Parse binary operators with the operator precedence parsing
+ // algorithm. `left` is the left-hand side of the operator.
+ // `minPrec` provides context that allows the function to stop and
+ // defer further parser to one of its callers when it encounters an
+ // operator that has a lower precedence than the set it is parsing.
+
+ function parseExprOp(left, leftStart, minPrec, noIn) {
+ var prec = tokType.binop;
+ if (prec != null && (!noIn || tokType !== _in)) {
+ if (prec > minPrec) {
+ var node = startNodeAt(leftStart);
+ node.left = left;
+ node.operator = tokVal;
+ var op = tokType;
+ next();
+ var start = storeCurrentPos();
+ node.right = parseExprOp(parseMaybeUnary(), start, prec, noIn);
+ finishNode(node, (op === _logicalOR || op === _logicalAND) ? "LogicalExpression" : "BinaryExpression");
+ return parseExprOp(node, leftStart, minPrec, noIn);
+ }
+ }
+ return left;
+ }
+
+ // Parse unary operators, both prefix and postfix.
+
+ function parseMaybeUnary() {
+ if (tokType.prefix) {
+ var node = startNode(), update = tokType.isUpdate, nodeType;
+ if (tokType === _ellipsis) {
+ nodeType = "SpreadElement";
+ } else {
+ nodeType = update ? "UpdateExpression" : "UnaryExpression";
+ node.operator = tokVal;
+ node.prefix = true;
+ }
+ tokRegexpAllowed = true;
+ next();
+ node.argument = parseMaybeUnary();
+ if (update) checkLVal(node.argument);
+ else if (strict && node.operator === "delete" &&
+ node.argument.type === "Identifier")
+ raise(node.start, "Deleting local variable in strict mode");
+ return finishNode(node, nodeType);
+ }
+ var start = storeCurrentPos();
+ var expr = parseExprSubscripts();
+ while (tokType.postfix && !canInsertSemicolon()) {
+ var node = startNodeAt(start);
+ node.operator = tokVal;
+ node.prefix = false;
+ node.argument = expr;
+ checkLVal(expr);
+ next();
+ expr = finishNode(node, "UpdateExpression");
+ }
+ return expr;
+ }
+
+ // Parse call, dot, and `[]`-subscript expressions.
+
+ function parseExprSubscripts() {
+ var start = storeCurrentPos();
+ return parseSubscripts(parseExprAtom(), start);
+ }
+
+ function parseSubscripts(base, start, noCalls) {
+ if (eat(_dot)) {
+ var node = startNodeAt(start);
+ node.object = base;
+ node.property = parseIdent(true);
+ node.computed = false;
+ return parseSubscripts(finishNode(node, "MemberExpression"), start, noCalls);
+ } else if (eat(_bracketL)) {
+ var node = startNodeAt(start);
+ node.object = base;
+ node.property = parseExpression();
+ node.computed = true;
+ expect(_bracketR);
+ return parseSubscripts(finishNode(node, "MemberExpression"), start, noCalls);
+ } else if (!noCalls && eat(_parenL)) {
+ var node = startNodeAt(start);
+ node.callee = base;
+ node.arguments = parseExprList(_parenR, false);
+ return parseSubscripts(finishNode(node, "CallExpression"), start, noCalls);
+ } else if (tokType === _template) {
+ var node = startNodeAt(start);
+ node.tag = base;
+ node.quasi = parseTemplate();
+ return parseSubscripts(finishNode(node, "TaggedTemplateExpression"), start, noCalls);
+ } return base;
+ }
+
+ // Parse an atomic expression — either a single token that is an
+ // expression, an expression started by a keyword like `function` or
+ // `new`, or an expression wrapped in punctuation like `()`, `[]`,
+ // or `{}`.
+
+ function parseExprAtom() {
+ switch (tokType) {
+ case _this:
+ var node = startNode();
+ next();
+ return finishNode(node, "ThisExpression");
+
+ case _yield:
+ if (inGenerator) return parseYield();
+
+ case _name:
+ var start = storeCurrentPos();
+ var id = parseIdent(tokType !== _name);
+ if (eat(_arrow)) {
+ return parseArrowExpression(startNodeAt(start), [id]);
+ }
+ return id;
+
+ case _regexp:
+ var node = startNode();
+ node.regex = {pattern: tokVal.pattern, flags: tokVal.flags};
+ node.value = tokVal.value;
+ node.raw = input.slice(tokStart, tokEnd);
+ next();
+ return finishNode(node, "Literal");
+
+ case _num: case _string:
+ var node = startNode();
+ node.value = tokVal;
+ node.raw = input.slice(tokStart, tokEnd);
+ next();
+ return finishNode(node, "Literal");
+
+ case _null: case _true: case _false:
+ var node = startNode();
+ node.value = tokType.atomValue;
+ node.raw = tokType.keyword;
+ next();
+ return finishNode(node, "Literal");
+
+ case _parenL:
+ var start = storeCurrentPos();
+ var val, exprList;
+ next();
+ // check whether this is generator comprehension or regular expression
+ if (options.ecmaVersion >= 7 && tokType === _for) {
+ val = parseComprehension(startNodeAt(start), true);
+ } else {
+ var oldParenL = ++metParenL;
+ if (tokType !== _parenR) {
+ val = parseExpression();
+ exprList = val.type === "SequenceExpression" ? val.expressions : [val];
+ } else {
+ exprList = [];
+ }
+ expect(_parenR);
+ // if '=>' follows '(...)', convert contents to arguments
+ if (metParenL === oldParenL && eat(_arrow)) {
+ val = parseArrowExpression(startNodeAt(start), exprList);
+ } else {
+ // forbid '()' before everything but '=>'
+ if (!val) unexpected(lastStart);
+ // forbid '...' in sequence expressions
+ if (options.ecmaVersion >= 6) {
+ for (var i = 0; i < exprList.length; i++) {
+ if (exprList[i].type === "SpreadElement") unexpected();
+ }
+ }
+
+ if (options.preserveParens) {
+ var par = startNodeAt(start);
+ par.expression = val;
+ val = finishNode(par, "ParenthesizedExpression");
+ }
+ }
+ }
+ return val;
+
+ case _bracketL:
+ var node = startNode();
+ next();
+ // check whether this is array comprehension or regular array
+ if (options.ecmaVersion >= 7 && tokType === _for) {
+ return parseComprehension(node, false);
+ }
+ node.elements = parseExprList(_bracketR, true, true);
+ return finishNode(node, "ArrayExpression");
+
+ case _braceL:
+ return parseObj();
+
+ case _function:
+ var node = startNode();
+ next();
+ return parseFunction(node, false);
+
+ case _class:
+ return parseClass(startNode(), false);
+
+ case _new:
+ return parseNew();
+
+ case _template:
+ return parseTemplate();
+
+ default:
+ unexpected();
+ }
+ }
+
+ // New's precedence is slightly tricky. It must allow its argument
+ // to be a `[]` or dot subscript expression, but not a call — at
+ // least, not without wrapping it in parentheses. Thus, it uses the
+
+ function parseNew() {
+ var node = startNode();
+ next();
+ var start = storeCurrentPos();
+ node.callee = parseSubscripts(parseExprAtom(), start, true);
+ if (eat(_parenL)) node.arguments = parseExprList(_parenR, false);
+ else node.arguments = empty;
+ return finishNode(node, "NewExpression");
+ }
+
+ // Parse template expression.
+
+ function parseTemplateElement() {
+ var elem = startNodeAt(options.locations ? [tokStart + 1, tokStartLoc.offset(1)] : tokStart + 1);
+ elem.value = tokVal;
+ elem.tail = input.charCodeAt(tokEnd - 1) !== 123; // '{'
+ next();
+ var endOff = elem.tail ? 1 : 2;
+ return finishNodeAt(elem, "TemplateElement", options.locations ? [lastEnd - endOff, lastEndLoc.offset(-endOff)] : lastEnd - endOff);
+ }
+
+ function parseTemplate() {
+ var node = startNode();
+ node.expressions = [];
+ var curElt = parseTemplateElement();
+ node.quasis = [curElt];
+ while (!curElt.tail) {
+ node.expressions.push(parseExpression());
+ if (tokType !== _templateContinued) unexpected();
+ node.quasis.push(curElt = parseTemplateElement());
+ }
+ return finishNode(node, "TemplateLiteral");
+ }
+
+ // Parse an object literal.
+
+ function parseObj() {
+ var node = startNode(), first = true, propHash = {};
+ node.properties = [];
+ next();
+ while (!eat(_braceR)) {
+ if (!first) {
+ expect(_comma);
+ if (options.allowTrailingCommas && eat(_braceR)) break;
+ } else first = false;
+
+ var prop = startNode(), isGenerator;
+ if (options.ecmaVersion >= 6) {
+ prop.method = false;
+ prop.shorthand = false;
+ isGenerator = eat(_star);
+ }
+ parsePropertyName(prop);
+ if (eat(_colon)) {
+ prop.value = parseExpression(true);
+ prop.kind = "init";
+ } else if (options.ecmaVersion >= 6 && tokType === _parenL) {
+ prop.kind = "init";
+ prop.method = true;
+ prop.value = parseMethod(isGenerator);
+ } else if (options.ecmaVersion >= 5 && !prop.computed && prop.key.type === "Identifier" &&
+ (prop.key.name === "get" || prop.key.name === "set")) {
+ if (isGenerator) unexpected();
+ prop.kind = prop.key.name;
+ parsePropertyName(prop);
+ prop.value = parseMethod(false);
+ } else if (options.ecmaVersion >= 6 && !prop.computed && prop.key.type === "Identifier") {
+ prop.kind = "init";
+ prop.value = prop.key;
+ prop.shorthand = true;
+ } else unexpected();
+
+ checkPropClash(prop, propHash);
+ node.properties.push(finishNode(prop, "Property"));
+ }
+ return finishNode(node, "ObjectExpression");
+ }
+
+ function parsePropertyName(prop) {
+ if (options.ecmaVersion >= 6) {
+ if (eat(_bracketL)) {
+ prop.computed = true;
+ prop.key = parseExpression();
+ expect(_bracketR);
+ return;
+ } else {
+ prop.computed = false;
+ }
+ }
+ prop.key = (tokType === _num || tokType === _string) ? parseExprAtom() : parseIdent(true);
+ }
+
+ // Initialize empty function node.
+
+ function initFunction(node) {
+ node.id = null;
+ node.params = [];
+ if (options.ecmaVersion >= 6) {
+ node.defaults = [];
+ node.rest = null;
+ node.generator = false;
+ }
+ }
+
+ // Parse a function declaration or literal (depending on the
+ // `isStatement` parameter).
+
+ function parseFunction(node, isStatement, allowExpressionBody) {
+ initFunction(node);
+ if (options.ecmaVersion >= 6) {
+ node.generator = eat(_star);
+ }
+ if (isStatement || tokType === _name) {
+ node.id = parseIdent();
+ }
+ parseFunctionParams(node);
+ parseFunctionBody(node, allowExpressionBody);
+ return finishNode(node, isStatement ? "FunctionDeclaration" : "FunctionExpression");
+ }
+
+ // Parse object or class method.
+
+ function parseMethod(isGenerator) {
+ var node = startNode();
+ initFunction(node);
+ parseFunctionParams(node);
+ var allowExpressionBody;
+ if (options.ecmaVersion >= 6) {
+ node.generator = isGenerator;
+ allowExpressionBody = true;
+ } else {
+ allowExpressionBody = false;
+ }
+ parseFunctionBody(node, allowExpressionBody);
+ return finishNode(node, "FunctionExpression");
+ }
+
+ // Parse arrow function expression with given parameters.
+
+ function parseArrowExpression(node, params) {
+ initFunction(node);
+
+ var defaults = node.defaults, hasDefaults = false;
+
+ for (var i = 0, lastI = params.length - 1; i <= lastI; i++) {
+ var param = params[i];
+
+ if (param.type === "AssignmentExpression" && param.operator === "=") {
+ hasDefaults = true;
+ params[i] = param.left;
+ defaults.push(param.right);
+ } else {
+ toAssignable(param, i === lastI, true);
+ defaults.push(null);
+ if (param.type === "SpreadElement") {
+ params.length--;
+ node.rest = param.argument;
+ break;
+ }
+ }
+ }
+
+ node.params = params;
+ if (!hasDefaults) node.defaults = [];
+
+ parseFunctionBody(node, true);
+ return finishNode(node, "ArrowFunctionExpression");
+ }
+
+ // Parse function parameters.
+
+ function parseFunctionParams(node) {
+ var defaults = [], hasDefaults = false;
+
+ expect(_parenL);
+ for (;;) {
+ if (eat(_parenR)) {
+ break;
+ } else if (options.ecmaVersion >= 6 && eat(_ellipsis)) {
+ node.rest = toAssignable(parseExprAtom(), false, true);
+ checkSpreadAssign(node.rest);
+ expect(_parenR);
+ defaults.push(null);
+ break;
+ } else {
+ node.params.push(options.ecmaVersion >= 6 ? toAssignable(parseExprAtom(), false, true) : parseIdent());
+ if (options.ecmaVersion >= 6) {
+ if (eat(_eq)) {
+ hasDefaults = true;
+ defaults.push(parseExpression(true));
+ } else {
+ defaults.push(null);
+ }
+ }
+ if (!eat(_comma)) {
+ expect(_parenR);
+ break;
+ }
+ }
+ }
+
+ if (hasDefaults) node.defaults = defaults;
+ }
+
+ // Parse function body and check parameters.
+
+ function parseFunctionBody(node, allowExpression) {
+ var isExpression = allowExpression && tokType !== _braceL;
+
+ if (isExpression) {
+ node.body = parseExpression(true);
+ node.expression = true;
+ } else {
+ // Start a new scope with regard to labels and the `inFunction`
+ // flag (restore them to their old value afterwards).
+ var oldInFunc = inFunction, oldInGen = inGenerator, oldLabels = labels;
+ inFunction = true; inGenerator = node.generator; labels = [];
+ node.body = parseBlock(true);
+ node.expression = false;
+ inFunction = oldInFunc; inGenerator = oldInGen; labels = oldLabels;
+ }
+
+ // If this is a strict mode function, verify that argument names
+ // are not repeated, and it does not try to bind the words `eval`
+ // or `arguments`.
+ if (strict || !isExpression && node.body.body.length && isUseStrict(node.body.body[0])) {
+ var nameHash = {};
+ if (node.id)
+ checkFunctionParam(node.id, {});
+ for (var i = 0; i < node.params.length; i++)
+ checkFunctionParam(node.params[i], nameHash);
+ if (node.rest)
+ checkFunctionParam(node.rest, nameHash);
+ }
+ }
+
+ // Parse a class declaration or literal (depending on the
+ // `isStatement` parameter).
+
+ function parseClass(node, isStatement) {
+ next();
+ node.id = tokType === _name ? parseIdent() : isStatement ? unexpected() : null;
+ node.superClass = eat(_extends) ? parseExpression() : null;
+ var classBody = startNode();
+ classBody.body = [];
+ expect(_braceL);
+ while (!eat(_braceR)) {
+ var method = startNode();
+ if (tokType === _name && tokVal === "static") {
+ next();
+ method['static'] = true;
+ } else {
+ method['static'] = false;
+ }
+ var isGenerator = eat(_star);
+ parsePropertyName(method);
+ if (tokType !== _parenL && !method.computed && method.key.type === "Identifier" &&
+ (method.key.name === "get" || method.key.name === "set")) {
+ if (isGenerator) unexpected();
+ method.kind = method.key.name;
+ parsePropertyName(method);
+ } else {
+ method.kind = "";
+ }
+ method.value = parseMethod(isGenerator);
+ classBody.body.push(finishNode(method, "MethodDefinition"));
+ eat(_semi);
+ }
+ node.body = finishNode(classBody, "ClassBody");
+ return finishNode(node, isStatement ? "ClassDeclaration" : "ClassExpression");
+ }
+
+ // Parses a comma-separated list of expressions, and returns them as
+ // an array. `close` is the token type that ends the list, and
+ // `allowEmpty` can be turned on to allow subsequent commas with
+ // nothing in between them to be parsed as `null` (which is needed
+ // for array literals).
+
+ function parseExprList(close, allowTrailingComma, allowEmpty) {
+ var elts = [], first = true;
+ while (!eat(close)) {
+ if (!first) {
+ expect(_comma);
+ if (allowTrailingComma && options.allowTrailingCommas && eat(close)) break;
+ } else first = false;
+
+ if (allowEmpty && tokType === _comma) elts.push(null);
+ else elts.push(parseExpression(true));
+ }
+ return elts;
+ }
+
+ // Parse the next token as an identifier. If `liberal` is true (used
+ // when parsing properties), it will also convert keywords into
+ // identifiers.
+
+ function parseIdent(liberal) {
+ var node = startNode();
+ if (liberal && options.forbidReserved == "everywhere") liberal = false;
+ if (tokType === _name) {
+ if (!liberal &&
+ (options.forbidReserved &&
+ (options.ecmaVersion === 3 ? isReservedWord3 : isReservedWord5)(tokVal) ||
+ strict && isStrictReservedWord(tokVal)) &&
+ input.slice(tokStart, tokEnd).indexOf("\\") == -1)
+ raise(tokStart, "The keyword '" + tokVal + "' is reserved");
+ node.name = tokVal;
+ } else if (liberal && tokType.keyword) {
+ node.name = tokType.keyword;
+ } else {
+ unexpected();
+ }
+ tokRegexpAllowed = false;
+ next();
+ return finishNode(node, "Identifier");
+ }
+
+ // Parses module export declaration.
+
+ function parseExport(node) {
+ next();
+ // export var|const|let|function|class ...;
+ if (tokType === _var || tokType === _const || tokType === _let || tokType === _function || tokType === _class) {
+ node.declaration = parseStatement();
+ node['default'] = false;
+ node.specifiers = null;
+ node.source = null;
+ } else
+ // export default ...;
+ if (eat(_default)) {
+ node.declaration = parseExpression(true);
+ node['default'] = true;
+ node.specifiers = null;
+ node.source = null;
+ semicolon();
+ } else {
+ // export * from '...';
+ // export { x, y as z } [from '...'];
+ var isBatch = tokType === _star;
+ node.declaration = null;
+ node['default'] = false;
+ node.specifiers = parseExportSpecifiers();
+ if (tokType === _name && tokVal === "from") {
+ next();
+ node.source = tokType === _string ? parseExprAtom() : unexpected();
+ } else {
+ if (isBatch) unexpected();
+ node.source = null;
+ }
+ semicolon();
+ }
+ return finishNode(node, "ExportDeclaration");
+ }
+
+ // Parses a comma-separated list of module exports.
+
+ function parseExportSpecifiers() {
+ var nodes = [], first = true;
+ if (tokType === _star) {
+ // export * from '...'
+ var node = startNode();
+ next();
+ nodes.push(finishNode(node, "ExportBatchSpecifier"));
+ } else {
+ // export { x, y as z } [from '...']
+ expect(_braceL);
+ while (!eat(_braceR)) {
+ if (!first) {
+ expect(_comma);
+ if (options.allowTrailingCommas && eat(_braceR)) break;
+ } else first = false;
+
+ var node = startNode();
+ node.id = parseIdent(tokType === _default);
+ if (tokType === _name && tokVal === "as") {
+ next();
+ node.name = parseIdent(true);
+ } else {
+ node.name = null;
+ }
+ nodes.push(finishNode(node, "ExportSpecifier"));
+ }
+ }
+ return nodes;
+ }
+
+ // Parses import declaration.
+
+ function parseImport(node) {
+ next();
+ // import '...';
+ if (tokType === _string) {
+ node.specifiers = [];
+ node.source = parseExprAtom();
+ node.kind = "";
+ } else {
+ node.specifiers = parseImportSpecifiers();
+ if (tokType !== _name || tokVal !== "from") unexpected();
+ next();
+ node.source = tokType === _string ? parseExprAtom() : unexpected();
+ }
+ semicolon();
+ return finishNode(node, "ImportDeclaration");
+ }
+
+ // Parses a comma-separated list of module imports.
+
+ function parseImportSpecifiers() {
+ var nodes = [], first = true;
+ if (tokType === _name) {
+ // import defaultObj, { x, y as z } from '...'
+ var node = startNode();
+ node.id = parseIdent();
+ checkLVal(node.id, true);
+ node.name = null;
+ node['default'] = true;
+ nodes.push(finishNode(node, "ImportSpecifier"));
+ if (!eat(_comma)) return nodes;
+ }
+ if (tokType === _star) {
+ var node = startNode();
+ next();
+ if (tokType !== _name || tokVal !== "as") unexpected();
+ next();
+ node.name = parseIdent();
+ checkLVal(node.name, true);
+ nodes.push(finishNode(node, "ImportBatchSpecifier"));
+ return nodes;
+ }
+ expect(_braceL);
+ while (!eat(_braceR)) {
+ if (!first) {
+ expect(_comma);
+ if (options.allowTrailingCommas && eat(_braceR)) break;
+ } else first = false;
+
+ var node = startNode();
+ node.id = parseIdent(true);
+ if (tokType === _name && tokVal === "as") {
+ next();
+ node.name = parseIdent();
+ } else {
+ node.name = null;
+ }
+ checkLVal(node.name || node.id, true);
+ node['default'] = false;
+ nodes.push(finishNode(node, "ImportSpecifier"));
+ }
+ return nodes;
+ }
+
+ // Parses yield expression inside generator.
+
+ function parseYield() {
+ var node = startNode();
+ next();
+ if (eat(_semi) || canInsertSemicolon()) {
+ node.delegate = false;
+ node.argument = null;
+ } else {
+ node.delegate = eat(_star);
+ node.argument = parseExpression(true);
+ }
+ return finishNode(node, "YieldExpression");
+ }
+
+ // Parses array and generator comprehensions.
+
+ function parseComprehension(node, isGenerator) {
+ node.blocks = [];
+ while (tokType === _for) {
+ var block = startNode();
+ next();
+ expect(_parenL);
+ block.left = toAssignable(parseExprAtom());
+ checkLVal(block.left, true);
+ if (tokType !== _name || tokVal !== "of") unexpected();
+ next();
+ // `of` property is here for compatibility with Esprima's AST
+ // which also supports deprecated [for (... in ...) expr]
+ block.of = true;
+ block.right = parseExpression();
+ expect(_parenR);
+ node.blocks.push(finishNode(block, "ComprehensionBlock"));
+ }
+ node.filter = eat(_if) ? parseParenExpression() : null;
+ node.body = parseExpression();
+ expect(isGenerator ? _parenR : _bracketR);
+ node.generator = isGenerator;
+ return finishNode(node, "ComprehensionExpression");
+ }
+
+ });
+
+
+/***/ },
+
+/***/ 462:
+/***/ function(module, exports, __webpack_require__) {
+
+ /*
+ * Copyright 2009-2011 Mozilla Foundation and contributors
+ * Licensed under the New BSD license. See LICENSE.txt or:
+ * http://opensource.org/licenses/BSD-3-Clause
+ */
+ exports.SourceMapGenerator = __webpack_require__(463).SourceMapGenerator;
+ exports.SourceMapConsumer = __webpack_require__(469).SourceMapConsumer;
+ exports.SourceNode = __webpack_require__(471).SourceNode;
+
+
+/***/ },
+
+/***/ 463:
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_RESULT__;/* -*- Mode: js; js-indent-level: 2; -*- */
+ /*
+ * Copyright 2011 Mozilla Foundation and contributors
+ * Licensed under the New BSD license. See LICENSE or:
+ * http://opensource.org/licenses/BSD-3-Clause
+ */
+ if (false) {
+ var define = require('amdefine')(module, require);
+ }
+ !(__WEBPACK_AMD_DEFINE_RESULT__ = function (require, exports, module) {
+
+ var base64VLQ = __webpack_require__(464);
+ var util = __webpack_require__(466);
+ var ArraySet = __webpack_require__(467).ArraySet;
+ var MappingList = __webpack_require__(468).MappingList;
+
+ /**
+ * An instance of the SourceMapGenerator represents a source map which is
+ * being built incrementally. You may pass an object with the following
+ * properties:
+ *
+ * - file: The filename of the generated source.
+ * - sourceRoot: A root for all relative URLs in this source map.
+ */
+ function SourceMapGenerator(aArgs) {
+ if (!aArgs) {
+ aArgs = {};
+ }
+ this._file = util.getArg(aArgs, 'file', null);
+ this._sourceRoot = util.getArg(aArgs, 'sourceRoot', null);
+ this._skipValidation = util.getArg(aArgs, 'skipValidation', false);
+ this._sources = new ArraySet();
+ this._names = new ArraySet();
+ this._mappings = new MappingList();
+ this._sourcesContents = null;
+ }
+
+ SourceMapGenerator.prototype._version = 3;
+
+ /**
+ * Creates a new SourceMapGenerator based on a SourceMapConsumer
+ *
+ * @param aSourceMapConsumer The SourceMap.
+ */
+ SourceMapGenerator.fromSourceMap =
+ function SourceMapGenerator_fromSourceMap(aSourceMapConsumer) {
+ var sourceRoot = aSourceMapConsumer.sourceRoot;
+ var generator = new SourceMapGenerator({
+ file: aSourceMapConsumer.file,
+ sourceRoot: sourceRoot
+ });
+ aSourceMapConsumer.eachMapping(function (mapping) {
+ var newMapping = {
+ generated: {
+ line: mapping.generatedLine,
+ column: mapping.generatedColumn
+ }
+ };
+
+ if (mapping.source != null) {
+ newMapping.source = mapping.source;
+ if (sourceRoot != null) {
+ newMapping.source = util.relative(sourceRoot, newMapping.source);
+ }
+
+ newMapping.original = {
+ line: mapping.originalLine,
+ column: mapping.originalColumn
+ };
+
+ if (mapping.name != null) {
+ newMapping.name = mapping.name;
+ }
+ }
+
+ generator.addMapping(newMapping);
+ });
+ aSourceMapConsumer.sources.forEach(function (sourceFile) {
+ var content = aSourceMapConsumer.sourceContentFor(sourceFile);
+ if (content != null) {
+ generator.setSourceContent(sourceFile, content);
+ }
+ });
+ return generator;
+ };
+
+ /**
+ * Add a single mapping from original source line and column to the generated
+ * source's line and column for this source map being created. The mapping
+ * object should have the following properties:
+ *
+ * - generated: An object with the generated line and column positions.
+ * - original: An object with the original line and column positions.
+ * - source: The original source file (relative to the sourceRoot).
+ * - name: An optional original token name for this mapping.
+ */
+ SourceMapGenerator.prototype.addMapping =
+ function SourceMapGenerator_addMapping(aArgs) {
+ var generated = util.getArg(aArgs, 'generated');
+ var original = util.getArg(aArgs, 'original', null);
+ var source = util.getArg(aArgs, 'source', null);
+ var name = util.getArg(aArgs, 'name', null);
+
+ if (!this._skipValidation) {
+ this._validateMapping(generated, original, source, name);
+ }
+
+ if (source != null && !this._sources.has(source)) {
+ this._sources.add(source);
+ }
+
+ if (name != null && !this._names.has(name)) {
+ this._names.add(name);
+ }
+
+ this._mappings.add({
+ generatedLine: generated.line,
+ generatedColumn: generated.column,
+ originalLine: original != null && original.line,
+ originalColumn: original != null && original.column,
+ source: source,
+ name: name
+ });
+ };
+
+ /**
+ * Set the source content for a source file.
+ */
+ SourceMapGenerator.prototype.setSourceContent =
+ function SourceMapGenerator_setSourceContent(aSourceFile, aSourceContent) {
+ var source = aSourceFile;
+ if (this._sourceRoot != null) {
+ source = util.relative(this._sourceRoot, source);
+ }
+
+ if (aSourceContent != null) {
+ // Add the source content to the _sourcesContents map.
+ // Create a new _sourcesContents map if the property is null.
+ if (!this._sourcesContents) {
+ this._sourcesContents = {};
+ }
+ this._sourcesContents[util.toSetString(source)] = aSourceContent;
+ } else if (this._sourcesContents) {
+ // Remove the source file from the _sourcesContents map.
+ // If the _sourcesContents map is empty, set the property to null.
+ delete this._sourcesContents[util.toSetString(source)];
+ if (Object.keys(this._sourcesContents).length === 0) {
+ this._sourcesContents = null;
+ }
+ }
+ };
+
+ /**
+ * Applies the mappings of a sub-source-map for a specific source file to the
+ * source map being generated. Each mapping to the supplied source file is
+ * rewritten using the supplied source map. Note: The resolution for the
+ * resulting mappings is the minimium of this map and the supplied map.
+ *
+ * @param aSourceMapConsumer The source map to be applied.
+ * @param aSourceFile Optional. The filename of the source file.
+ * If omitted, SourceMapConsumer's file property will be used.
+ * @param aSourceMapPath Optional. The dirname of the path to the source map
+ * to be applied. If relative, it is relative to the SourceMapConsumer.
+ * This parameter is needed when the two source maps aren't in the same
+ * directory, and the source map to be applied contains relative source
+ * paths. If so, those relative source paths need to be rewritten
+ * relative to the SourceMapGenerator.
+ */
+ SourceMapGenerator.prototype.applySourceMap =
+ function SourceMapGenerator_applySourceMap(aSourceMapConsumer, aSourceFile, aSourceMapPath) {
+ var sourceFile = aSourceFile;
+ // If aSourceFile is omitted, we will use the file property of the SourceMap
+ if (aSourceFile == null) {
+ if (aSourceMapConsumer.file == null) {
+ throw new Error(
+ 'SourceMapGenerator.prototype.applySourceMap requires either an explicit source file, ' +
+ 'or the source map\'s "file" property. Both were omitted.'
+ );
+ }
+ sourceFile = aSourceMapConsumer.file;
+ }
+ var sourceRoot = this._sourceRoot;
+ // Make "sourceFile" relative if an absolute Url is passed.
+ if (sourceRoot != null) {
+ sourceFile = util.relative(sourceRoot, sourceFile);
+ }
+ // Applying the SourceMap can add and remove items from the sources and
+ // the names array.
+ var newSources = new ArraySet();
+ var newNames = new ArraySet();
+
+ // Find mappings for the "sourceFile"
+ this._mappings.unsortedForEach(function (mapping) {
+ if (mapping.source === sourceFile && mapping.originalLine != null) {
+ // Check if it can be mapped by the source map, then update the mapping.
+ var original = aSourceMapConsumer.originalPositionFor({
+ line: mapping.originalLine,
+ column: mapping.originalColumn
+ });
+ if (original.source != null) {
+ // Copy mapping
+ mapping.source = original.source;
+ if (aSourceMapPath != null) {
+ mapping.source = util.join(aSourceMapPath, mapping.source)
+ }
+ if (sourceRoot != null) {
+ mapping.source = util.relative(sourceRoot, mapping.source);
+ }
+ mapping.originalLine = original.line;
+ mapping.originalColumn = original.column;
+ if (original.name != null) {
+ mapping.name = original.name;
+ }
+ }
+ }
+
+ var source = mapping.source;
+ if (source != null && !newSources.has(source)) {
+ newSources.add(source);
+ }
+
+ var name = mapping.name;
+ if (name != null && !newNames.has(name)) {
+ newNames.add(name);
+ }
+
+ }, this);
+ this._sources = newSources;
+ this._names = newNames;
+
+ // Copy sourcesContents of applied map.
+ aSourceMapConsumer.sources.forEach(function (sourceFile) {
+ var content = aSourceMapConsumer.sourceContentFor(sourceFile);
+ if (content != null) {
+ if (aSourceMapPath != null) {
+ sourceFile = util.join(aSourceMapPath, sourceFile);
+ }
+ if (sourceRoot != null) {
+ sourceFile = util.relative(sourceRoot, sourceFile);
+ }
+ this.setSourceContent(sourceFile, content);
+ }
+ }, this);
+ };
+
+ /**
+ * A mapping can have one of the three levels of data:
+ *
+ * 1. Just the generated position.
+ * 2. The Generated position, original position, and original source.
+ * 3. Generated and original position, original source, as well as a name
+ * token.
+ *
+ * To maintain consistency, we validate that any new mapping being added falls
+ * in to one of these categories.
+ */
+ SourceMapGenerator.prototype._validateMapping =
+ function SourceMapGenerator_validateMapping(aGenerated, aOriginal, aSource,
+ aName) {
+ if (aGenerated && 'line' in aGenerated && 'column' in aGenerated
+ && aGenerated.line > 0 && aGenerated.column >= 0
+ && !aOriginal && !aSource && !aName) {
+ // Case 1.
+ return;
+ }
+ else if (aGenerated && 'line' in aGenerated && 'column' in aGenerated
+ && aOriginal && 'line' in aOriginal && 'column' in aOriginal
+ && aGenerated.line > 0 && aGenerated.column >= 0
+ && aOriginal.line > 0 && aOriginal.column >= 0
+ && aSource) {
+ // Cases 2 and 3.
+ return;
+ }
+ else {
+ throw new Error('Invalid mapping: ' + JSON.stringify({
+ generated: aGenerated,
+ source: aSource,
+ original: aOriginal,
+ name: aName
+ }));
+ }
+ };
+
+ /**
+ * Serialize the accumulated mappings in to the stream of base 64 VLQs
+ * specified by the source map format.
+ */
+ SourceMapGenerator.prototype._serializeMappings =
+ function SourceMapGenerator_serializeMappings() {
+ var previousGeneratedColumn = 0;
+ var previousGeneratedLine = 1;
+ var previousOriginalColumn = 0;
+ var previousOriginalLine = 0;
+ var previousName = 0;
+ var previousSource = 0;
+ var result = '';
+ var mapping;
+
+ var mappings = this._mappings.toArray();
+
+ for (var i = 0, len = mappings.length; i < len; i++) {
+ mapping = mappings[i];
+
+ if (mapping.generatedLine !== previousGeneratedLine) {
+ previousGeneratedColumn = 0;
+ while (mapping.generatedLine !== previousGeneratedLine) {
+ result += ';';
+ previousGeneratedLine++;
+ }
+ }
+ else {
+ if (i > 0) {
+ if (!util.compareByGeneratedPositions(mapping, mappings[i - 1])) {
+ continue;
+ }
+ result += ',';
+ }
+ }
+
+ result += base64VLQ.encode(mapping.generatedColumn
+ - previousGeneratedColumn);
+ previousGeneratedColumn = mapping.generatedColumn;
+
+ if (mapping.source != null) {
+ result += base64VLQ.encode(this._sources.indexOf(mapping.source)
+ - previousSource);
+ previousSource = this._sources.indexOf(mapping.source);
+
+ // lines are stored 0-based in SourceMap spec version 3
+ result += base64VLQ.encode(mapping.originalLine - 1
+ - previousOriginalLine);
+ previousOriginalLine = mapping.originalLine - 1;
+
+ result += base64VLQ.encode(mapping.originalColumn
+ - previousOriginalColumn);
+ previousOriginalColumn = mapping.originalColumn;
+
+ if (mapping.name != null) {
+ result += base64VLQ.encode(this._names.indexOf(mapping.name)
+ - previousName);
+ previousName = this._names.indexOf(mapping.name);
+ }
+ }
+ }
+
+ return result;
+ };
+
+ SourceMapGenerator.prototype._generateSourcesContent =
+ function SourceMapGenerator_generateSourcesContent(aSources, aSourceRoot) {
+ return aSources.map(function (source) {
+ if (!this._sourcesContents) {
+ return null;
+ }
+ if (aSourceRoot != null) {
+ source = util.relative(aSourceRoot, source);
+ }
+ var key = util.toSetString(source);
+ return Object.prototype.hasOwnProperty.call(this._sourcesContents,
+ key)
+ ? this._sourcesContents[key]
+ : null;
+ }, this);
+ };
+
+ /**
+ * Externalize the source map.
+ */
+ SourceMapGenerator.prototype.toJSON =
+ function SourceMapGenerator_toJSON() {
+ var map = {
+ version: this._version,
+ sources: this._sources.toArray(),
+ names: this._names.toArray(),
+ mappings: this._serializeMappings()
+ };
+ if (this._file != null) {
+ map.file = this._file;
+ }
+ if (this._sourceRoot != null) {
+ map.sourceRoot = this._sourceRoot;
+ }
+ if (this._sourcesContents) {
+ map.sourcesContent = this._generateSourcesContent(map.sources, map.sourceRoot);
+ }
+
+ return map;
+ };
+
+ /**
+ * Render the source map being generated to a string.
+ */
+ SourceMapGenerator.prototype.toString =
+ function SourceMapGenerator_toString() {
+ return JSON.stringify(this);
+ };
+
+ exports.SourceMapGenerator = SourceMapGenerator;
+
+ }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+
+/***/ },
+
+/***/ 464:
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_RESULT__;/* -*- Mode: js; js-indent-level: 2; -*- */
+ /*
+ * Copyright 2011 Mozilla Foundation and contributors
+ * Licensed under the New BSD license. See LICENSE or:
+ * http://opensource.org/licenses/BSD-3-Clause
+ *
+ * Based on the Base 64 VLQ implementation in Closure Compiler:
+ * https://code.google.com/p/closure-compiler/source/browse/trunk/src/com/google/debugging/sourcemap/Base64VLQ.java
+ *
+ * Copyright 2011 The Closure Compiler Authors. All rights reserved.
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+ if (false) {
+ var define = require('amdefine')(module, require);
+ }
+ !(__WEBPACK_AMD_DEFINE_RESULT__ = function (require, exports, module) {
+
+ var base64 = __webpack_require__(465);
+
+ // A single base 64 digit can contain 6 bits of data. For the base 64 variable
+ // length quantities we use in the source map spec, the first bit is the sign,
+ // the next four bits are the actual value, and the 6th bit is the
+ // continuation bit. The continuation bit tells us whether there are more
+ // digits in this value following this digit.
+ //
+ // Continuation
+ // | Sign
+ // | |
+ // V V
+ // 101011
+
+ var VLQ_BASE_SHIFT = 5;
+
+ // binary: 100000
+ var VLQ_BASE = 1 << VLQ_BASE_SHIFT;
+
+ // binary: 011111
+ var VLQ_BASE_MASK = VLQ_BASE - 1;
+
+ // binary: 100000
+ var VLQ_CONTINUATION_BIT = VLQ_BASE;
+
+ /**
+ * Converts from a two-complement value to a value where the sign bit is
+ * placed in the least significant bit. For example, as decimals:
+ * 1 becomes 2 (10 binary), -1 becomes 3 (11 binary)
+ * 2 becomes 4 (100 binary), -2 becomes 5 (101 binary)
+ */
+ function toVLQSigned(aValue) {
+ return aValue < 0
+ ? ((-aValue) << 1) + 1
+ : (aValue << 1) + 0;
+ }
+
+ /**
+ * Converts to a two-complement value from a value where the sign bit is
+ * placed in the least significant bit. For example, as decimals:
+ * 2 (10 binary) becomes 1, 3 (11 binary) becomes -1
+ * 4 (100 binary) becomes 2, 5 (101 binary) becomes -2
+ */
+ function fromVLQSigned(aValue) {
+ var isNegative = (aValue & 1) === 1;
+ var shifted = aValue >> 1;
+ return isNegative
+ ? -shifted
+ : shifted;
+ }
+
+ /**
+ * Returns the base 64 VLQ encoded value.
+ */
+ exports.encode = function base64VLQ_encode(aValue) {
+ var encoded = "";
+ var digit;
+
+ var vlq = toVLQSigned(aValue);
+
+ do {
+ digit = vlq & VLQ_BASE_MASK;
+ vlq >>>= VLQ_BASE_SHIFT;
+ if (vlq > 0) {
+ // There are still more digits in this value, so we must make sure the
+ // continuation bit is marked.
+ digit |= VLQ_CONTINUATION_BIT;
+ }
+ encoded += base64.encode(digit);
+ } while (vlq > 0);
+
+ return encoded;
+ };
+
+ /**
+ * Decodes the next base 64 VLQ value from the given string and returns the
+ * value and the rest of the string via the out parameter.
+ */
+ exports.decode = function base64VLQ_decode(aStr, aOutParam) {
+ var i = 0;
+ var strLen = aStr.length;
+ var result = 0;
+ var shift = 0;
+ var continuation, digit;
+
+ do {
+ if (i >= strLen) {
+ throw new Error("Expected more digits in base 64 VLQ value.");
+ }
+ digit = base64.decode(aStr.charAt(i++));
+ continuation = !!(digit & VLQ_CONTINUATION_BIT);
+ digit &= VLQ_BASE_MASK;
+ result = result + (digit << shift);
+ shift += VLQ_BASE_SHIFT;
+ } while (continuation);
+
+ aOutParam.value = fromVLQSigned(result);
+ aOutParam.rest = aStr.slice(i);
+ };
+
+ }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+
+/***/ },
+
+/***/ 465:
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_RESULT__;/* -*- Mode: js; js-indent-level: 2; -*- */
+ /*
+ * Copyright 2011 Mozilla Foundation and contributors
+ * Licensed under the New BSD license. See LICENSE or:
+ * http://opensource.org/licenses/BSD-3-Clause
+ */
+ if (false) {
+ var define = require('amdefine')(module, require);
+ }
+ !(__WEBPACK_AMD_DEFINE_RESULT__ = function (require, exports, module) {
+
+ var charToIntMap = {};
+ var intToCharMap = {};
+
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
+ .split('')
+ .forEach(function (ch, index) {
+ charToIntMap[ch] = index;
+ intToCharMap[index] = ch;
+ });
+
+ /**
+ * Encode an integer in the range of 0 to 63 to a single base 64 digit.
+ */
+ exports.encode = function base64_encode(aNumber) {
+ if (aNumber in intToCharMap) {
+ return intToCharMap[aNumber];
+ }
+ throw new TypeError("Must be between 0 and 63: " + aNumber);
+ };
+
+ /**
+ * Decode a single base 64 digit to an integer.
+ */
+ exports.decode = function base64_decode(aChar) {
+ if (aChar in charToIntMap) {
+ return charToIntMap[aChar];
+ }
+ throw new TypeError("Not a valid base 64 digit: " + aChar);
+ };
+
+ }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+
+/***/ },
+
+/***/ 466:
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_RESULT__;/* -*- Mode: js; js-indent-level: 2; -*- */
+ /*
+ * Copyright 2011 Mozilla Foundation and contributors
+ * Licensed under the New BSD license. See LICENSE or:
+ * http://opensource.org/licenses/BSD-3-Clause
+ */
+ if (false) {
+ var define = require('amdefine')(module, require);
+ }
+ !(__WEBPACK_AMD_DEFINE_RESULT__ = function (require, exports, module) {
+
+ /**
+ * This is a helper function for getting values from parameter/options
+ * objects.
+ *
+ * @param args The object we are extracting values from
+ * @param name The name of the property we are getting.
+ * @param defaultValue An optional value to return if the property is missing
+ * from the object. If this is not specified and the property is missing, an
+ * error will be thrown.
+ */
+ function getArg(aArgs, aName, aDefaultValue) {
+ if (aName in aArgs) {
+ return aArgs[aName];
+ } else if (arguments.length === 3) {
+ return aDefaultValue;
+ } else {
+ throw new Error('"' + aName + '" is a required argument.');
+ }
+ }
+ exports.getArg = getArg;
+
+ var urlRegexp = /^(?:([\w+\-.]+):)?\/\/(?:(\w+:\w+)@)?([\w.]*)(?::(\d+))?(\S*)$/;
+ var dataUrlRegexp = /^data:.+\,.+$/;
+
+ function urlParse(aUrl) {
+ var match = aUrl.match(urlRegexp);
+ if (!match) {
+ return null;
+ }
+ return {
+ scheme: match[1],
+ auth: match[2],
+ host: match[3],
+ port: match[4],
+ path: match[5]
+ };
+ }
+ exports.urlParse = urlParse;
+
+ function urlGenerate(aParsedUrl) {
+ var url = '';
+ if (aParsedUrl.scheme) {
+ url += aParsedUrl.scheme + ':';
+ }
+ url += '//';
+ if (aParsedUrl.auth) {
+ url += aParsedUrl.auth + '@';
+ }
+ if (aParsedUrl.host) {
+ url += aParsedUrl.host;
+ }
+ if (aParsedUrl.port) {
+ url += ":" + aParsedUrl.port
+ }
+ if (aParsedUrl.path) {
+ url += aParsedUrl.path;
+ }
+ return url;
+ }
+ exports.urlGenerate = urlGenerate;
+
+ /**
+ * Normalizes a path, or the path portion of a URL:
+ *
+ * - Replaces consequtive slashes with one slash.
+ * - Removes unnecessary '.' parts.
+ * - Removes unnecessary '<dir>/..' parts.
+ *
+ * Based on code in the Node.js 'path' core module.
+ *
+ * @param aPath The path or url to normalize.
+ */
+ function normalize(aPath) {
+ var path = aPath;
+ var url = urlParse(aPath);
+ if (url) {
+ if (!url.path) {
+ return aPath;
+ }
+ path = url.path;
+ }
+ var isAbsolute = (path.charAt(0) === '/');
+
+ var parts = path.split(/\/+/);
+ for (var part, up = 0, i = parts.length - 1; i >= 0; i--) {
+ part = parts[i];
+ if (part === '.') {
+ parts.splice(i, 1);
+ } else if (part === '..') {
+ up++;
+ } else if (up > 0) {
+ if (part === '') {
+ // The first part is blank if the path is absolute. Trying to go
+ // above the root is a no-op. Therefore we can remove all '..' parts
+ // directly after the root.
+ parts.splice(i + 1, up);
+ up = 0;
+ } else {
+ parts.splice(i, 2);
+ up--;
+ }
+ }
+ }
+ path = parts.join('/');
+
+ if (path === '') {
+ path = isAbsolute ? '/' : '.';
+ }
+
+ if (url) {
+ url.path = path;
+ return urlGenerate(url);
+ }
+ return path;
+ }
+ exports.normalize = normalize;
+
+ /**
+ * Joins two paths/URLs.
+ *
+ * @param aRoot The root path or URL.
+ * @param aPath The path or URL to be joined with the root.
+ *
+ * - If aPath is a URL or a data URI, aPath is returned, unless aPath is a
+ * scheme-relative URL: Then the scheme of aRoot, if any, is prepended
+ * first.
+ * - Otherwise aPath is a path. If aRoot is a URL, then its path portion
+ * is updated with the result and aRoot is returned. Otherwise the result
+ * is returned.
+ * - If aPath is absolute, the result is aPath.
+ * - Otherwise the two paths are joined with a slash.
+ * - Joining for example 'http://' and 'www.example.com' is also supported.
+ */
+ function join(aRoot, aPath) {
+ if (aRoot === "") {
+ aRoot = ".";
+ }
+ if (aPath === "") {
+ aPath = ".";
+ }
+ var aPathUrl = urlParse(aPath);
+ var aRootUrl = urlParse(aRoot);
+ if (aRootUrl) {
+ aRoot = aRootUrl.path || '/';
+ }
+
+ // `join(foo, '//www.example.org')`
+ if (aPathUrl && !aPathUrl.scheme) {
+ if (aRootUrl) {
+ aPathUrl.scheme = aRootUrl.scheme;
+ }
+ return urlGenerate(aPathUrl);
+ }
+
+ if (aPathUrl || aPath.match(dataUrlRegexp)) {
+ return aPath;
+ }
+
+ // `join('http://', 'www.example.com')`
+ if (aRootUrl && !aRootUrl.host && !aRootUrl.path) {
+ aRootUrl.host = aPath;
+ return urlGenerate(aRootUrl);
+ }
+
+ var joined = aPath.charAt(0) === '/'
+ ? aPath
+ : normalize(aRoot.replace(/\/+$/, '') + '/' + aPath);
+
+ if (aRootUrl) {
+ aRootUrl.path = joined;
+ return urlGenerate(aRootUrl);
+ }
+ return joined;
+ }
+ exports.join = join;
+
+ /**
+ * Make a path relative to a URL or another path.
+ *
+ * @param aRoot The root path or URL.
+ * @param aPath The path or URL to be made relative to aRoot.
+ */
+ function relative(aRoot, aPath) {
+ if (aRoot === "") {
+ aRoot = ".";
+ }
+
+ aRoot = aRoot.replace(/\/$/, '');
+
+ // XXX: It is possible to remove this block, and the tests still pass!
+ var url = urlParse(aRoot);
+ if (aPath.charAt(0) == "/" && url && url.path == "/") {
+ return aPath.slice(1);
+ }
+
+ return aPath.indexOf(aRoot + '/') === 0
+ ? aPath.substr(aRoot.length + 1)
+ : aPath;
+ }
+ exports.relative = relative;
+
+ /**
+ * Because behavior goes wacky when you set `__proto__` on objects, we
+ * have to prefix all the strings in our set with an arbitrary character.
+ *
+ * See https://github.com/mozilla/source-map/pull/31 and
+ * https://github.com/mozilla/source-map/issues/30
+ *
+ * @param String aStr
+ */
+ function toSetString(aStr) {
+ return '$' + aStr;
+ }
+ exports.toSetString = toSetString;
+
+ function fromSetString(aStr) {
+ return aStr.substr(1);
+ }
+ exports.fromSetString = fromSetString;
+
+ function strcmp(aStr1, aStr2) {
+ var s1 = aStr1 || "";
+ var s2 = aStr2 || "";
+ return (s1 > s2) - (s1 < s2);
+ }
+
+ /**
+ * Comparator between two mappings where the original positions are compared.
+ *
+ * Optionally pass in `true` as `onlyCompareGenerated` to consider two
+ * mappings with the same original source/line/column, but different generated
+ * line and column the same. Useful when searching for a mapping with a
+ * stubbed out mapping.
+ */
+ function compareByOriginalPositions(mappingA, mappingB, onlyCompareOriginal) {
+ var cmp;
+
+ cmp = strcmp(mappingA.source, mappingB.source);
+ if (cmp) {
+ return cmp;
+ }
+
+ cmp = mappingA.originalLine - mappingB.originalLine;
+ if (cmp) {
+ return cmp;
+ }
+
+ cmp = mappingA.originalColumn - mappingB.originalColumn;
+ if (cmp || onlyCompareOriginal) {
+ return cmp;
+ }
+
+ cmp = strcmp(mappingA.name, mappingB.name);
+ if (cmp) {
+ return cmp;
+ }
+
+ cmp = mappingA.generatedLine - mappingB.generatedLine;
+ if (cmp) {
+ return cmp;
+ }
+
+ return mappingA.generatedColumn - mappingB.generatedColumn;
+ };
+ exports.compareByOriginalPositions = compareByOriginalPositions;
+
+ /**
+ * Comparator between two mappings where the generated positions are
+ * compared.
+ *
+ * Optionally pass in `true` as `onlyCompareGenerated` to consider two
+ * mappings with the same generated line and column, but different
+ * source/name/original line and column the same. Useful when searching for a
+ * mapping with a stubbed out mapping.
+ */
+ function compareByGeneratedPositions(mappingA, mappingB, onlyCompareGenerated) {
+ var cmp;
+
+ cmp = mappingA.generatedLine - mappingB.generatedLine;
+ if (cmp) {
+ return cmp;
+ }
+
+ cmp = mappingA.generatedColumn - mappingB.generatedColumn;
+ if (cmp || onlyCompareGenerated) {
+ return cmp;
+ }
+
+ cmp = strcmp(mappingA.source, mappingB.source);
+ if (cmp) {
+ return cmp;
+ }
+
+ cmp = mappingA.originalLine - mappingB.originalLine;
+ if (cmp) {
+ return cmp;
+ }
+
+ cmp = mappingA.originalColumn - mappingB.originalColumn;
+ if (cmp) {
+ return cmp;
+ }
+
+ return strcmp(mappingA.name, mappingB.name);
+ };
+ exports.compareByGeneratedPositions = compareByGeneratedPositions;
+
+ }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+
+/***/ },
+
+/***/ 467:
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_RESULT__;/* -*- Mode: js; js-indent-level: 2; -*- */
+ /*
+ * Copyright 2011 Mozilla Foundation and contributors
+ * Licensed under the New BSD license. See LICENSE or:
+ * http://opensource.org/licenses/BSD-3-Clause
+ */
+ if (false) {
+ var define = require('amdefine')(module, require);
+ }
+ !(__WEBPACK_AMD_DEFINE_RESULT__ = function (require, exports, module) {
+
+ var util = __webpack_require__(466);
+
+ /**
+ * A data structure which is a combination of an array and a set. Adding a new
+ * member is O(1), testing for membership is O(1), and finding the index of an
+ * element is O(1). Removing elements from the set is not supported. Only
+ * strings are supported for membership.
+ */
+ function ArraySet() {
+ this._array = [];
+ this._set = {};
+ }
+
+ /**
+ * Static method for creating ArraySet instances from an existing array.
+ */
+ ArraySet.fromArray = function ArraySet_fromArray(aArray, aAllowDuplicates) {
+ var set = new ArraySet();
+ for (var i = 0, len = aArray.length; i < len; i++) {
+ set.add(aArray[i], aAllowDuplicates);
+ }
+ return set;
+ };
+
+ /**
+ * Add the given string to this set.
+ *
+ * @param String aStr
+ */
+ ArraySet.prototype.add = function ArraySet_add(aStr, aAllowDuplicates) {
+ var isDuplicate = this.has(aStr);
+ var idx = this._array.length;
+ if (!isDuplicate || aAllowDuplicates) {
+ this._array.push(aStr);
+ }
+ if (!isDuplicate) {
+ this._set[util.toSetString(aStr)] = idx;
+ }
+ };
+
+ /**
+ * Is the given string a member of this set?
+ *
+ * @param String aStr
+ */
+ ArraySet.prototype.has = function ArraySet_has(aStr) {
+ return Object.prototype.hasOwnProperty.call(this._set,
+ util.toSetString(aStr));
+ };
+
+ /**
+ * What is the index of the given string in the array?
+ *
+ * @param String aStr
+ */
+ ArraySet.prototype.indexOf = function ArraySet_indexOf(aStr) {
+ if (this.has(aStr)) {
+ return this._set[util.toSetString(aStr)];
+ }
+ throw new Error('"' + aStr + '" is not in the set.');
+ };
+
+ /**
+ * What is the element at the given index?
+ *
+ * @param Number aIdx
+ */
+ ArraySet.prototype.at = function ArraySet_at(aIdx) {
+ if (aIdx >= 0 && aIdx < this._array.length) {
+ return this._array[aIdx];
+ }
+ throw new Error('No element indexed by ' + aIdx);
+ };
+
+ /**
+ * Returns the array representation of this set (which has the proper indices
+ * indicated by indexOf). Note that this is a copy of the internal array used
+ * for storing the members so that no one can mess with internal state.
+ */
+ ArraySet.prototype.toArray = function ArraySet_toArray() {
+ return this._array.slice();
+ };
+
+ exports.ArraySet = ArraySet;
+
+ }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+
+/***/ },
+
+/***/ 468:
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_RESULT__;/* -*- Mode: js; js-indent-level: 2; -*- */
+ /*
+ * Copyright 2014 Mozilla Foundation and contributors
+ * Licensed under the New BSD license. See LICENSE or:
+ * http://opensource.org/licenses/BSD-3-Clause
+ */
+ if (false) {
+ var define = require('amdefine')(module, require);
+ }
+ !(__WEBPACK_AMD_DEFINE_RESULT__ = function (require, exports, module) {
+
+ var util = __webpack_require__(466);
+
+ /**
+ * Determine whether mappingB is after mappingA with respect to generated
+ * position.
+ */
+ function generatedPositionAfter(mappingA, mappingB) {
+ // Optimized for most common case
+ var lineA = mappingA.generatedLine;
+ var lineB = mappingB.generatedLine;
+ var columnA = mappingA.generatedColumn;
+ var columnB = mappingB.generatedColumn;
+ return lineB > lineA || lineB == lineA && columnB >= columnA ||
+ util.compareByGeneratedPositions(mappingA, mappingB) <= 0;
+ }
+
+ /**
+ * A data structure to provide a sorted view of accumulated mappings in a
+ * performance conscious manner. It trades a neglibable overhead in general
+ * case for a large speedup in case of mappings being added in order.
+ */
+ function MappingList() {
+ this._array = [];
+ this._sorted = true;
+ // Serves as infimum
+ this._last = {generatedLine: -1, generatedColumn: 0};
+ }
+
+ /**
+ * Iterate through internal items. This method takes the same arguments that
+ * `Array.prototype.forEach` takes.
+ *
+ * NOTE: The order of the mappings is NOT guaranteed.
+ */
+ MappingList.prototype.unsortedForEach =
+ function MappingList_forEach(aCallback, aThisArg) {
+ this._array.forEach(aCallback, aThisArg);
+ };
+
+ /**
+ * Add the given source mapping.
+ *
+ * @param Object aMapping
+ */
+ MappingList.prototype.add = function MappingList_add(aMapping) {
+ var mapping;
+ if (generatedPositionAfter(this._last, aMapping)) {
+ this._last = aMapping;
+ this._array.push(aMapping);
+ } else {
+ this._sorted = false;
+ this._array.push(aMapping);
+ }
+ };
+
+ /**
+ * Returns the flat, sorted array of mappings. The mappings are sorted by
+ * generated position.
+ *
+ * WARNING: This method returns internal data without copying, for
+ * performance. The return value must NOT be mutated, and should be treated as
+ * an immutable borrow. If you want to take ownership, you must make your own
+ * copy.
+ */
+ MappingList.prototype.toArray = function MappingList_toArray() {
+ if (!this._sorted) {
+ this._array.sort(util.compareByGeneratedPositions);
+ this._sorted = true;
+ }
+ return this._array;
+ };
+
+ exports.MappingList = MappingList;
+
+ }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+
+/***/ },
+
+/***/ 469:
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_RESULT__;/* -*- Mode: js; js-indent-level: 2; -*- */
+ /*
+ * Copyright 2011 Mozilla Foundation and contributors
+ * Licensed under the New BSD license. See LICENSE or:
+ * http://opensource.org/licenses/BSD-3-Clause
+ */
+ if (false) {
+ var define = require('amdefine')(module, require);
+ }
+ !(__WEBPACK_AMD_DEFINE_RESULT__ = function (require, exports, module) {
+
+ var util = __webpack_require__(466);
+ var binarySearch = __webpack_require__(470);
+ var ArraySet = __webpack_require__(467).ArraySet;
+ var base64VLQ = __webpack_require__(464);
+
+ /**
+ * A SourceMapConsumer instance represents a parsed source map which we can
+ * query for information about the original file positions by giving it a file
+ * position in the generated source.
+ *
+ * The only parameter is the raw source map (either as a JSON string, or
+ * already parsed to an object). According to the spec, source maps have the
+ * following attributes:
+ *
+ * - version: Which version of the source map spec this map is following.
+ * - sources: An array of URLs to the original source files.
+ * - names: An array of identifiers which can be referrenced by individual mappings.
+ * - sourceRoot: Optional. The URL root from which all sources are relative.
+ * - sourcesContent: Optional. An array of contents of the original source files.
+ * - mappings: A string of base64 VLQs which contain the actual mappings.
+ * - file: Optional. The generated file this source map is associated with.
+ *
+ * Here is an example source map, taken from the source map spec[0]:
+ *
+ * {
+ * version : 3,
+ * file: "out.js",
+ * sourceRoot : "",
+ * sources: ["foo.js", "bar.js"],
+ * names: ["src", "maps", "are", "fun"],
+ * mappings: "AA,AB;;ABCDE;"
+ * }
+ *
+ * [0]: https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit?pli=1#
+ */
+ function SourceMapConsumer(aSourceMap) {
+ var sourceMap = aSourceMap;
+ if (typeof aSourceMap === 'string') {
+ sourceMap = JSON.parse(aSourceMap.replace(/^\)\]\}'/, ''));
+ }
+
+ var version = util.getArg(sourceMap, 'version');
+ var sources = util.getArg(sourceMap, 'sources');
+ // Sass 3.3 leaves out the 'names' array, so we deviate from the spec (which
+ // requires the array) to play nice here.
+ var names = util.getArg(sourceMap, 'names', []);
+ var sourceRoot = util.getArg(sourceMap, 'sourceRoot', null);
+ var sourcesContent = util.getArg(sourceMap, 'sourcesContent', null);
+ var mappings = util.getArg(sourceMap, 'mappings');
+ var file = util.getArg(sourceMap, 'file', null);
+
+ // Once again, Sass deviates from the spec and supplies the version as a
+ // string rather than a number, so we use loose equality checking here.
+ if (version != this._version) {
+ throw new Error('Unsupported version: ' + version);
+ }
+
+ // Some source maps produce relative source paths like "./foo.js" instead of
+ // "foo.js". Normalize these first so that future comparisons will succeed.
+ // See bugzil.la/1090768.
+ sources = sources.map(util.normalize);
+
+ // Pass `true` below to allow duplicate names and sources. While source maps
+ // are intended to be compressed and deduplicated, the TypeScript compiler
+ // sometimes generates source maps with duplicates in them. See Github issue
+ // #72 and bugzil.la/889492.
+ this._names = ArraySet.fromArray(names, true);
+ this._sources = ArraySet.fromArray(sources, true);
+
+ this.sourceRoot = sourceRoot;
+ this.sourcesContent = sourcesContent;
+ this._mappings = mappings;
+ this.file = file;
+ }
+
+ /**
+ * Create a SourceMapConsumer from a SourceMapGenerator.
+ *
+ * @param SourceMapGenerator aSourceMap
+ * The source map that will be consumed.
+ * @returns SourceMapConsumer
+ */
+ SourceMapConsumer.fromSourceMap =
+ function SourceMapConsumer_fromSourceMap(aSourceMap) {
+ var smc = Object.create(SourceMapConsumer.prototype);
+
+ smc._names = ArraySet.fromArray(aSourceMap._names.toArray(), true);
+ smc._sources = ArraySet.fromArray(aSourceMap._sources.toArray(), true);
+ smc.sourceRoot = aSourceMap._sourceRoot;
+ smc.sourcesContent = aSourceMap._generateSourcesContent(smc._sources.toArray(),
+ smc.sourceRoot);
+ smc.file = aSourceMap._file;
+
+ smc.__generatedMappings = aSourceMap._mappings.toArray().slice();
+ smc.__originalMappings = aSourceMap._mappings.toArray().slice()
+ .sort(util.compareByOriginalPositions);
+
+ return smc;
+ };
+
+ /**
+ * The version of the source mapping spec that we are consuming.
+ */
+ SourceMapConsumer.prototype._version = 3;
+
+ /**
+ * The list of original sources.
+ */
+ Object.defineProperty(SourceMapConsumer.prototype, 'sources', {
+ get: function () {
+ return this._sources.toArray().map(function (s) {
+ return this.sourceRoot != null ? util.join(this.sourceRoot, s) : s;
+ }, this);
+ }
+ });
+
+ // `__generatedMappings` and `__originalMappings` are arrays that hold the
+ // parsed mapping coordinates from the source map's "mappings" attribute. They
+ // are lazily instantiated, accessed via the `_generatedMappings` and
+ // `_originalMappings` getters respectively, and we only parse the mappings
+ // and create these arrays once queried for a source location. We jump through
+ // these hoops because there can be many thousands of mappings, and parsing
+ // them is expensive, so we only want to do it if we must.
+ //
+ // Each object in the arrays is of the form:
+ //
+ // {
+ // generatedLine: The line number in the generated code,
+ // generatedColumn: The column number in the generated code,
+ // source: The path to the original source file that generated this
+ // chunk of code,
+ // originalLine: The line number in the original source that
+ // corresponds to this chunk of generated code,
+ // originalColumn: The column number in the original source that
+ // corresponds to this chunk of generated code,
+ // name: The name of the original symbol which generated this chunk of
+ // code.
+ // }
+ //
+ // All properties except for `generatedLine` and `generatedColumn` can be
+ // `null`.
+ //
+ // `_generatedMappings` is ordered by the generated positions.
+ //
+ // `_originalMappings` is ordered by the original positions.
+
+ SourceMapConsumer.prototype.__generatedMappings = null;
+ Object.defineProperty(SourceMapConsumer.prototype, '_generatedMappings', {
+ get: function () {
+ if (!this.__generatedMappings) {
+ this.__generatedMappings = [];
+ this.__originalMappings = [];
+ this._parseMappings(this._mappings, this.sourceRoot);
+ }
+
+ return this.__generatedMappings;
+ }
+ });
+
+ SourceMapConsumer.prototype.__originalMappings = null;
+ Object.defineProperty(SourceMapConsumer.prototype, '_originalMappings', {
+ get: function () {
+ if (!this.__originalMappings) {
+ this.__generatedMappings = [];
+ this.__originalMappings = [];
+ this._parseMappings(this._mappings, this.sourceRoot);
+ }
+
+ return this.__originalMappings;
+ }
+ });
+
+ SourceMapConsumer.prototype._nextCharIsMappingSeparator =
+ function SourceMapConsumer_nextCharIsMappingSeparator(aStr) {
+ var c = aStr.charAt(0);
+ return c === ";" || c === ",";
+ };
+
+ /**
+ * Parse the mappings in a string in to a data structure which we can easily
+ * query (the ordered arrays in the `this.__generatedMappings` and
+ * `this.__originalMappings` properties).
+ */
+ SourceMapConsumer.prototype._parseMappings =
+ function SourceMapConsumer_parseMappings(aStr, aSourceRoot) {
+ var generatedLine = 1;
+ var previousGeneratedColumn = 0;
+ var previousOriginalLine = 0;
+ var previousOriginalColumn = 0;
+ var previousSource = 0;
+ var previousName = 0;
+ var str = aStr;
+ var temp = {};
+ var mapping;
+
+ while (str.length > 0) {
+ if (str.charAt(0) === ';') {
+ generatedLine++;
+ str = str.slice(1);
+ previousGeneratedColumn = 0;
+ }
+ else if (str.charAt(0) === ',') {
+ str = str.slice(1);
+ }
+ else {
+ mapping = {};
+ mapping.generatedLine = generatedLine;
+
+ // Generated column.
+ base64VLQ.decode(str, temp);
+ mapping.generatedColumn = previousGeneratedColumn + temp.value;
+ previousGeneratedColumn = mapping.generatedColumn;
+ str = temp.rest;
+
+ if (str.length > 0 && !this._nextCharIsMappingSeparator(str)) {
+ // Original source.
+ base64VLQ.decode(str, temp);
+ mapping.source = this._sources.at(previousSource + temp.value);
+ previousSource += temp.value;
+ str = temp.rest;
+ if (str.length === 0 || this._nextCharIsMappingSeparator(str)) {
+ throw new Error('Found a source, but no line and column');
+ }
+
+ // Original line.
+ base64VLQ.decode(str, temp);
+ mapping.originalLine = previousOriginalLine + temp.value;
+ previousOriginalLine = mapping.originalLine;
+ // Lines are stored 0-based
+ mapping.originalLine += 1;
+ str = temp.rest;
+ if (str.length === 0 || this._nextCharIsMappingSeparator(str)) {
+ throw new Error('Found a source and line, but no column');
+ }
+
+ // Original column.
+ base64VLQ.decode(str, temp);
+ mapping.originalColumn = previousOriginalColumn + temp.value;
+ previousOriginalColumn = mapping.originalColumn;
+ str = temp.rest;
+
+ if (str.length > 0 && !this._nextCharIsMappingSeparator(str)) {
+ // Original name.
+ base64VLQ.decode(str, temp);
+ mapping.name = this._names.at(previousName + temp.value);
+ previousName += temp.value;
+ str = temp.rest;
+ }
+ }
+
+ this.__generatedMappings.push(mapping);
+ if (typeof mapping.originalLine === 'number') {
+ this.__originalMappings.push(mapping);
+ }
+ }
+ }
+
+ this.__generatedMappings.sort(util.compareByGeneratedPositions);
+ this.__originalMappings.sort(util.compareByOriginalPositions);
+ };
+
+ /**
+ * Find the mapping that best matches the hypothetical "needle" mapping that
+ * we are searching for in the given "haystack" of mappings.
+ */
+ SourceMapConsumer.prototype._findMapping =
+ function SourceMapConsumer_findMapping(aNeedle, aMappings, aLineName,
+ aColumnName, aComparator) {
+ // To return the position we are searching for, we must first find the
+ // mapping for the given position and then return the opposite position it
+ // points to. Because the mappings are sorted, we can use binary search to
+ // find the best mapping.
+
+ if (aNeedle[aLineName] <= 0) {
+ throw new TypeError('Line must be greater than or equal to 1, got '
+ + aNeedle[aLineName]);
+ }
+ if (aNeedle[aColumnName] < 0) {
+ throw new TypeError('Column must be greater than or equal to 0, got '
+ + aNeedle[aColumnName]);
+ }
+
+ return binarySearch.search(aNeedle, aMappings, aComparator);
+ };
+
+ /**
+ * Compute the last column for each generated mapping. The last column is
+ * inclusive.
+ */
+ SourceMapConsumer.prototype.computeColumnSpans =
+ function SourceMapConsumer_computeColumnSpans() {
+ for (var index = 0; index < this._generatedMappings.length; ++index) {
+ var mapping = this._generatedMappings[index];
+
+ // Mappings do not contain a field for the last generated columnt. We
+ // can come up with an optimistic estimate, however, by assuming that
+ // mappings are contiguous (i.e. given two consecutive mappings, the
+ // first mapping ends where the second one starts).
+ if (index + 1 < this._generatedMappings.length) {
+ var nextMapping = this._generatedMappings[index + 1];
+
+ if (mapping.generatedLine === nextMapping.generatedLine) {
+ mapping.lastGeneratedColumn = nextMapping.generatedColumn - 1;
+ continue;
+ }
+ }
+
+ // The last mapping for each line spans the entire line.
+ mapping.lastGeneratedColumn = Infinity;
+ }
+ };
+
+ /**
+ * Returns the original source, line, and column information for the generated
+ * source's line and column positions provided. The only argument is an object
+ * with the following properties:
+ *
+ * - line: The line number in the generated source.
+ * - column: The column number in the generated source.
+ *
+ * and an object is returned with the following properties:
+ *
+ * - source: The original source file, or null.
+ * - line: The line number in the original source, or null.
+ * - column: The column number in the original source, or null.
+ * - name: The original identifier, or null.
+ */
+ SourceMapConsumer.prototype.originalPositionFor =
+ function SourceMapConsumer_originalPositionFor(aArgs) {
+ var needle = {
+ generatedLine: util.getArg(aArgs, 'line'),
+ generatedColumn: util.getArg(aArgs, 'column')
+ };
+
+ var index = this._findMapping(needle,
+ this._generatedMappings,
+ "generatedLine",
+ "generatedColumn",
+ util.compareByGeneratedPositions);
+
+ if (index >= 0) {
+ var mapping = this._generatedMappings[index];
+
+ if (mapping.generatedLine === needle.generatedLine) {
+ var source = util.getArg(mapping, 'source', null);
+ if (source != null && this.sourceRoot != null) {
+ source = util.join(this.sourceRoot, source);
+ }
+ return {
+ source: source,
+ line: util.getArg(mapping, 'originalLine', null),
+ column: util.getArg(mapping, 'originalColumn', null),
+ name: util.getArg(mapping, 'name', null)
+ };
+ }
+ }
+
+ return {
+ source: null,
+ line: null,
+ column: null,
+ name: null
+ };
+ };
+
+ /**
+ * Returns the original source content. The only argument is the url of the
+ * original source file. Returns null if no original source content is
+ * availible.
+ */
+ SourceMapConsumer.prototype.sourceContentFor =
+ function SourceMapConsumer_sourceContentFor(aSource) {
+ if (!this.sourcesContent) {
+ return null;
+ }
+
+ if (this.sourceRoot != null) {
+ aSource = util.relative(this.sourceRoot, aSource);
+ }
+
+ if (this._sources.has(aSource)) {
+ return this.sourcesContent[this._sources.indexOf(aSource)];
+ }
+
+ var url;
+ if (this.sourceRoot != null
+ && (url = util.urlParse(this.sourceRoot))) {
+ // XXX: file:// URIs and absolute paths lead to unexpected behavior for
+ // many users. We can help them out when they expect file:// URIs to
+ // behave like it would if they were running a local HTTP server. See
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=885597.
+ var fileUriAbsPath = aSource.replace(/^file:\/\//, "");
+ if (url.scheme == "file"
+ && this._sources.has(fileUriAbsPath)) {
+ return this.sourcesContent[this._sources.indexOf(fileUriAbsPath)]
+ }
+
+ if ((!url.path || url.path == "/")
+ && this._sources.has("/" + aSource)) {
+ return this.sourcesContent[this._sources.indexOf("/" + aSource)];
+ }
+ }
+
+ throw new Error('"' + aSource + '" is not in the SourceMap.');
+ };
+
+ /**
+ * Returns the generated line and column information for the original source,
+ * line, and column positions provided. The only argument is an object with
+ * the following properties:
+ *
+ * - source: The filename of the original source.
+ * - line: The line number in the original source.
+ * - column: The column number in the original source.
+ *
+ * and an object is returned with the following properties:
+ *
+ * - line: The line number in the generated source, or null.
+ * - column: The column number in the generated source, or null.
+ */
+ SourceMapConsumer.prototype.generatedPositionFor =
+ function SourceMapConsumer_generatedPositionFor(aArgs) {
+ var needle = {
+ source: util.getArg(aArgs, 'source'),
+ originalLine: util.getArg(aArgs, 'line'),
+ originalColumn: util.getArg(aArgs, 'column')
+ };
+
+ if (this.sourceRoot != null) {
+ needle.source = util.relative(this.sourceRoot, needle.source);
+ }
+
+ var index = this._findMapping(needle,
+ this._originalMappings,
+ "originalLine",
+ "originalColumn",
+ util.compareByOriginalPositions);
+
+ if (index >= 0) {
+ var mapping = this._originalMappings[index];
+
+ return {
+ line: util.getArg(mapping, 'generatedLine', null),
+ column: util.getArg(mapping, 'generatedColumn', null),
+ lastColumn: util.getArg(mapping, 'lastGeneratedColumn', null)
+ };
+ }
+
+ return {
+ line: null,
+ column: null,
+ lastColumn: null
+ };
+ };
+
+ /**
+ * Returns all generated line and column information for the original source
+ * and line provided. The only argument is an object with the following
+ * properties:
+ *
+ * - source: The filename of the original source.
+ * - line: The line number in the original source.
+ *
+ * and an array of objects is returned, each with the following properties:
+ *
+ * - line: The line number in the generated source, or null.
+ * - column: The column number in the generated source, or null.
+ */
+ SourceMapConsumer.prototype.allGeneratedPositionsFor =
+ function SourceMapConsumer_allGeneratedPositionsFor(aArgs) {
+ // When there is no exact match, SourceMapConsumer.prototype._findMapping
+ // returns the index of the closest mapping less than the needle. By
+ // setting needle.originalColumn to Infinity, we thus find the last
+ // mapping for the given line, provided such a mapping exists.
+ var needle = {
+ source: util.getArg(aArgs, 'source'),
+ originalLine: util.getArg(aArgs, 'line'),
+ originalColumn: Infinity
+ };
+
+ if (this.sourceRoot != null) {
+ needle.source = util.relative(this.sourceRoot, needle.source);
+ }
+
+ var mappings = [];
+
+ var index = this._findMapping(needle,
+ this._originalMappings,
+ "originalLine",
+ "originalColumn",
+ util.compareByOriginalPositions);
+ if (index >= 0) {
+ var mapping = this._originalMappings[index];
+
+ while (mapping && mapping.originalLine === needle.originalLine) {
+ mappings.push({
+ line: util.getArg(mapping, 'generatedLine', null),
+ column: util.getArg(mapping, 'generatedColumn', null),
+ lastColumn: util.getArg(mapping, 'lastGeneratedColumn', null)
+ });
+
+ mapping = this._originalMappings[--index];
+ }
+ }
+
+ return mappings.reverse();
+ };
+
+ SourceMapConsumer.GENERATED_ORDER = 1;
+ SourceMapConsumer.ORIGINAL_ORDER = 2;
+
+ /**
+ * Iterate over each mapping between an original source/line/column and a
+ * generated line/column in this source map.
+ *
+ * @param Function aCallback
+ * The function that is called with each mapping.
+ * @param Object aContext
+ * Optional. If specified, this object will be the value of `this` every
+ * time that `aCallback` is called.
+ * @param aOrder
+ * Either `SourceMapConsumer.GENERATED_ORDER` or
+ * `SourceMapConsumer.ORIGINAL_ORDER`. Specifies whether you want to
+ * iterate over the mappings sorted by the generated file's line/column
+ * order or the original's source/line/column order, respectively. Defaults to
+ * `SourceMapConsumer.GENERATED_ORDER`.
+ */
+ SourceMapConsumer.prototype.eachMapping =
+ function SourceMapConsumer_eachMapping(aCallback, aContext, aOrder) {
+ var context = aContext || null;
+ var order = aOrder || SourceMapConsumer.GENERATED_ORDER;
+
+ var mappings;
+ switch (order) {
+ case SourceMapConsumer.GENERATED_ORDER:
+ mappings = this._generatedMappings;
+ break;
+ case SourceMapConsumer.ORIGINAL_ORDER:
+ mappings = this._originalMappings;
+ break;
+ default:
+ throw new Error("Unknown order of iteration.");
+ }
+
+ var sourceRoot = this.sourceRoot;
+ mappings.map(function (mapping) {
+ var source = mapping.source;
+ if (source != null && sourceRoot != null) {
+ source = util.join(sourceRoot, source);
+ }
+ return {
+ source: source,
+ generatedLine: mapping.generatedLine,
+ generatedColumn: mapping.generatedColumn,
+ originalLine: mapping.originalLine,
+ originalColumn: mapping.originalColumn,
+ name: mapping.name
+ };
+ }).forEach(aCallback, context);
+ };
+
+ exports.SourceMapConsumer = SourceMapConsumer;
+
+ }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+
+/***/ },
+
+/***/ 470:
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_RESULT__;/* -*- Mode: js; js-indent-level: 2; -*- */
+ /*
+ * Copyright 2011 Mozilla Foundation and contributors
+ * Licensed under the New BSD license. See LICENSE or:
+ * http://opensource.org/licenses/BSD-3-Clause
+ */
+ if (false) {
+ var define = require('amdefine')(module, require);
+ }
+ !(__WEBPACK_AMD_DEFINE_RESULT__ = function (require, exports, module) {
+
+ /**
+ * Recursive implementation of binary search.
+ *
+ * @param aLow Indices here and lower do not contain the needle.
+ * @param aHigh Indices here and higher do not contain the needle.
+ * @param aNeedle The element being searched for.
+ * @param aHaystack The non-empty array being searched.
+ * @param aCompare Function which takes two elements and returns -1, 0, or 1.
+ */
+ function recursiveSearch(aLow, aHigh, aNeedle, aHaystack, aCompare) {
+ // This function terminates when one of the following is true:
+ //
+ // 1. We find the exact element we are looking for.
+ //
+ // 2. We did not find the exact element, but we can return the index of
+ // the next closest element that is less than that element.
+ //
+ // 3. We did not find the exact element, and there is no next-closest
+ // element which is less than the one we are searching for, so we
+ // return -1.
+ var mid = Math.floor((aHigh - aLow) / 2) + aLow;
+ var cmp = aCompare(aNeedle, aHaystack[mid], true);
+ if (cmp === 0) {
+ // Found the element we are looking for.
+ return mid;
+ }
+ else if (cmp > 0) {
+ // aHaystack[mid] is greater than our needle.
+ if (aHigh - mid > 1) {
+ // The element is in the upper half.
+ return recursiveSearch(mid, aHigh, aNeedle, aHaystack, aCompare);
+ }
+ // We did not find an exact match, return the next closest one
+ // (termination case 2).
+ return mid;
+ }
+ else {
+ // aHaystack[mid] is less than our needle.
+ if (mid - aLow > 1) {
+ // The element is in the lower half.
+ return recursiveSearch(aLow, mid, aNeedle, aHaystack, aCompare);
+ }
+ // The exact needle element was not found in this haystack. Determine if
+ // we are in termination case (2) or (3) and return the appropriate thing.
+ return aLow < 0 ? -1 : aLow;
+ }
+ }
+
+ /**
+ * This is an implementation of binary search which will always try and return
+ * the index of next lowest value checked if there is no exact hit. This is
+ * because mappings between original and generated line/col pairs are single
+ * points, and there is an implicit region between each of them, so a miss
+ * just means that you aren't on the very start of a region.
+ *
+ * @param aNeedle The element you are looking for.
+ * @param aHaystack The array that is being searched.
+ * @param aCompare A function which takes the needle and an element in the
+ * array and returns -1, 0, or 1 depending on whether the needle is less
+ * than, equal to, or greater than the element, respectively.
+ */
+ exports.search = function search(aNeedle, aHaystack, aCompare) {
+ if (aHaystack.length === 0) {
+ return -1;
+ }
+ return recursiveSearch(-1, aHaystack.length, aNeedle, aHaystack, aCompare)
+ };
+
+ }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+
+/***/ },
+
+/***/ 471:
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_RESULT__;/* -*- Mode: js; js-indent-level: 2; -*- */
+ /*
+ * Copyright 2011 Mozilla Foundation and contributors
+ * Licensed under the New BSD license. See LICENSE or:
+ * http://opensource.org/licenses/BSD-3-Clause
+ */
+ if (false) {
+ var define = require('amdefine')(module, require);
+ }
+ !(__WEBPACK_AMD_DEFINE_RESULT__ = function (require, exports, module) {
+
+ var SourceMapGenerator = __webpack_require__(463).SourceMapGenerator;
+ var util = __webpack_require__(466);
+
+ // Matches a Windows-style `\r\n` newline or a `\n` newline used by all other
+ // operating systems these days (capturing the result).
+ var REGEX_NEWLINE = /(\r?\n)/;
+
+ // Newline character code for charCodeAt() comparisons
+ var NEWLINE_CODE = 10;
+
+ // Private symbol for identifying `SourceNode`s when multiple versions of
+ // the source-map library are loaded. This MUST NOT CHANGE across
+ // versions!
+ var isSourceNode = "$$$isSourceNode$$$";
+
+ /**
+ * SourceNodes provide a way to abstract over interpolating/concatenating
+ * snippets of generated JavaScript source code while maintaining the line and
+ * column information associated with the original source code.
+ *
+ * @param aLine The original line number.
+ * @param aColumn The original column number.
+ * @param aSource The original source's filename.
+ * @param aChunks Optional. An array of strings which are snippets of
+ * generated JS, or other SourceNodes.
+ * @param aName The original identifier.
+ */
+ function SourceNode(aLine, aColumn, aSource, aChunks, aName) {
+ this.children = [];
+ this.sourceContents = {};
+ this.line = aLine == null ? null : aLine;
+ this.column = aColumn == null ? null : aColumn;
+ this.source = aSource == null ? null : aSource;
+ this.name = aName == null ? null : aName;
+ this[isSourceNode] = true;
+ if (aChunks != null) this.add(aChunks);
+ }
+
+ /**
+ * Creates a SourceNode from generated code and a SourceMapConsumer.
+ *
+ * @param aGeneratedCode The generated code
+ * @param aSourceMapConsumer The SourceMap for the generated code
+ * @param aRelativePath Optional. The path that relative sources in the
+ * SourceMapConsumer should be relative to.
+ */
+ SourceNode.fromStringWithSourceMap =
+ function SourceNode_fromStringWithSourceMap(aGeneratedCode, aSourceMapConsumer, aRelativePath) {
+ // The SourceNode we want to fill with the generated code
+ // and the SourceMap
+ var node = new SourceNode();
+
+ // All even indices of this array are one line of the generated code,
+ // while all odd indices are the newlines between two adjacent lines
+ // (since `REGEX_NEWLINE` captures its match).
+ // Processed fragments are removed from this array, by calling `shiftNextLine`.
+ var remainingLines = aGeneratedCode.split(REGEX_NEWLINE);
+ var shiftNextLine = function() {
+ var lineContents = remainingLines.shift();
+ // The last line of a file might not have a newline.
+ var newLine = remainingLines.shift() || "";
+ return lineContents + newLine;
+ };
+
+ // We need to remember the position of "remainingLines"
+ var lastGeneratedLine = 1, lastGeneratedColumn = 0;
+
+ // The generate SourceNodes we need a code range.
+ // To extract it current and last mapping is used.
+ // Here we store the last mapping.
+ var lastMapping = null;
+
+ aSourceMapConsumer.eachMapping(function (mapping) {
+ if (lastMapping !== null) {
+ // We add the code from "lastMapping" to "mapping":
+ // First check if there is a new line in between.
+ if (lastGeneratedLine < mapping.generatedLine) {
+ var code = "";
+ // Associate first line with "lastMapping"
+ addMappingWithCode(lastMapping, shiftNextLine());
+ lastGeneratedLine++;
+ lastGeneratedColumn = 0;
+ // The remaining code is added without mapping
+ } else {
+ // There is no new line in between.
+ // Associate the code between "lastGeneratedColumn" and
+ // "mapping.generatedColumn" with "lastMapping"
+ var nextLine = remainingLines[0];
+ var code = nextLine.substr(0, mapping.generatedColumn -
+ lastGeneratedColumn);
+ remainingLines[0] = nextLine.substr(mapping.generatedColumn -
+ lastGeneratedColumn);
+ lastGeneratedColumn = mapping.generatedColumn;
+ addMappingWithCode(lastMapping, code);
+ // No more remaining code, continue
+ lastMapping = mapping;
+ return;
+ }
+ }
+ // We add the generated code until the first mapping
+ // to the SourceNode without any mapping.
+ // Each line is added as separate string.
+ while (lastGeneratedLine < mapping.generatedLine) {
+ node.add(shiftNextLine());
+ lastGeneratedLine++;
+ }
+ if (lastGeneratedColumn < mapping.generatedColumn) {
+ var nextLine = remainingLines[0];
+ node.add(nextLine.substr(0, mapping.generatedColumn));
+ remainingLines[0] = nextLine.substr(mapping.generatedColumn);
+ lastGeneratedColumn = mapping.generatedColumn;
+ }
+ lastMapping = mapping;
+ }, this);
+ // We have processed all mappings.
+ if (remainingLines.length > 0) {
+ if (lastMapping) {
+ // Associate the remaining code in the current line with "lastMapping"
+ addMappingWithCode(lastMapping, shiftNextLine());
+ }
+ // and add the remaining lines without any mapping
+ node.add(remainingLines.join(""));
+ }
+
+ // Copy sourcesContent into SourceNode
+ aSourceMapConsumer.sources.forEach(function (sourceFile) {
+ var content = aSourceMapConsumer.sourceContentFor(sourceFile);
+ if (content != null) {
+ if (aRelativePath != null) {
+ sourceFile = util.join(aRelativePath, sourceFile);
+ }
+ node.setSourceContent(sourceFile, content);
+ }
+ });
+
+ return node;
+
+ function addMappingWithCode(mapping, code) {
+ if (mapping === null || mapping.source === undefined) {
+ node.add(code);
+ } else {
+ var source = aRelativePath
+ ? util.join(aRelativePath, mapping.source)
+ : mapping.source;
+ node.add(new SourceNode(mapping.originalLine,
+ mapping.originalColumn,
+ source,
+ code,
+ mapping.name));
+ }
+ }
+ };
+
+ /**
+ * Add a chunk of generated JS to this source node.
+ *
+ * @param aChunk A string snippet of generated JS code, another instance of
+ * SourceNode, or an array where each member is one of those things.
+ */
+ SourceNode.prototype.add = function SourceNode_add(aChunk) {
+ if (Array.isArray(aChunk)) {
+ aChunk.forEach(function (chunk) {
+ this.add(chunk);
+ }, this);
+ }
+ else if (aChunk[isSourceNode] || typeof aChunk === "string") {
+ if (aChunk) {
+ this.children.push(aChunk);
+ }
+ }
+ else {
+ throw new TypeError(
+ "Expected a SourceNode, string, or an array of SourceNodes and strings. Got " + aChunk
+ );
+ }
+ return this;
+ };
+
+ /**
+ * Add a chunk of generated JS to the beginning of this source node.
+ *
+ * @param aChunk A string snippet of generated JS code, another instance of
+ * SourceNode, or an array where each member is one of those things.
+ */
+ SourceNode.prototype.prepend = function SourceNode_prepend(aChunk) {
+ if (Array.isArray(aChunk)) {
+ for (var i = aChunk.length-1; i >= 0; i--) {
+ this.prepend(aChunk[i]);
+ }
+ }
+ else if (aChunk[isSourceNode] || typeof aChunk === "string") {
+ this.children.unshift(aChunk);
+ }
+ else {
+ throw new TypeError(
+ "Expected a SourceNode, string, or an array of SourceNodes and strings. Got " + aChunk
+ );
+ }
+ return this;
+ };
+
+ /**
+ * Walk over the tree of JS snippets in this node and its children. The
+ * walking function is called once for each snippet of JS and is passed that
+ * snippet and the its original associated source's line/column location.
+ *
+ * @param aFn The traversal function.
+ */
+ SourceNode.prototype.walk = function SourceNode_walk(aFn) {
+ var chunk;
+ for (var i = 0, len = this.children.length; i < len; i++) {
+ chunk = this.children[i];
+ if (chunk[isSourceNode]) {
+ chunk.walk(aFn);
+ }
+ else {
+ if (chunk !== '') {
+ aFn(chunk, { source: this.source,
+ line: this.line,
+ column: this.column,
+ name: this.name });
+ }
+ }
+ }
+ };
+
+ /**
+ * Like `String.prototype.join` except for SourceNodes. Inserts `aStr` between
+ * each of `this.children`.
+ *
+ * @param aSep The separator.
+ */
+ SourceNode.prototype.join = function SourceNode_join(aSep) {
+ var newChildren;
+ var i;
+ var len = this.children.length;
+ if (len > 0) {
+ newChildren = [];
+ for (i = 0; i < len-1; i++) {
+ newChildren.push(this.children[i]);
+ newChildren.push(aSep);
+ }
+ newChildren.push(this.children[i]);
+ this.children = newChildren;
+ }
+ return this;
+ };
+
+ /**
+ * Call String.prototype.replace on the very right-most source snippet. Useful
+ * for trimming whitespace from the end of a source node, etc.
+ *
+ * @param aPattern The pattern to replace.
+ * @param aReplacement The thing to replace the pattern with.
+ */
+ SourceNode.prototype.replaceRight = function SourceNode_replaceRight(aPattern, aReplacement) {
+ var lastChild = this.children[this.children.length - 1];
+ if (lastChild[isSourceNode]) {
+ lastChild.replaceRight(aPattern, aReplacement);
+ }
+ else if (typeof lastChild === 'string') {
+ this.children[this.children.length - 1] = lastChild.replace(aPattern, aReplacement);
+ }
+ else {
+ this.children.push(''.replace(aPattern, aReplacement));
+ }
+ return this;
+ };
+
+ /**
+ * Set the source content for a source file. This will be added to the SourceMapGenerator
+ * in the sourcesContent field.
+ *
+ * @param aSourceFile The filename of the source file
+ * @param aSourceContent The content of the source file
+ */
+ SourceNode.prototype.setSourceContent =
+ function SourceNode_setSourceContent(aSourceFile, aSourceContent) {
+ this.sourceContents[util.toSetString(aSourceFile)] = aSourceContent;
+ };
+
+ /**
+ * Walk over the tree of SourceNodes. The walking function is called for each
+ * source file content and is passed the filename and source content.
+ *
+ * @param aFn The traversal function.
+ */
+ SourceNode.prototype.walkSourceContents =
+ function SourceNode_walkSourceContents(aFn) {
+ for (var i = 0, len = this.children.length; i < len; i++) {
+ if (this.children[i][isSourceNode]) {
+ this.children[i].walkSourceContents(aFn);
+ }
+ }
+
+ var sources = Object.keys(this.sourceContents);
+ for (var i = 0, len = sources.length; i < len; i++) {
+ aFn(util.fromSetString(sources[i]), this.sourceContents[sources[i]]);
+ }
+ };
+
+ /**
+ * Return the string representation of this source node. Walks over the tree
+ * and concatenates all the various snippets together to one string.
+ */
+ SourceNode.prototype.toString = function SourceNode_toString() {
+ var str = "";
+ this.walk(function (chunk) {
+ str += chunk;
+ });
+ return str;
+ };
+
+ /**
+ * Returns the string representation of this source node along with a source
+ * map.
+ */
+ SourceNode.prototype.toStringWithSourceMap = function SourceNode_toStringWithSourceMap(aArgs) {
+ var generated = {
+ code: "",
+ line: 1,
+ column: 0
+ };
+ var map = new SourceMapGenerator(aArgs);
+ var sourceMappingActive = false;
+ var lastOriginalSource = null;
+ var lastOriginalLine = null;
+ var lastOriginalColumn = null;
+ var lastOriginalName = null;
+ this.walk(function (chunk, original) {
+ generated.code += chunk;
+ if (original.source !== null
+ && original.line !== null
+ && original.column !== null) {
+ if(lastOriginalSource !== original.source
+ || lastOriginalLine !== original.line
+ || lastOriginalColumn !== original.column
+ || lastOriginalName !== original.name) {
+ map.addMapping({
+ source: original.source,
+ original: {
+ line: original.line,
+ column: original.column
+ },
+ generated: {
+ line: generated.line,
+ column: generated.column
+ },
+ name: original.name
+ });
+ }
+ lastOriginalSource = original.source;
+ lastOriginalLine = original.line;
+ lastOriginalColumn = original.column;
+ lastOriginalName = original.name;
+ sourceMappingActive = true;
+ } else if (sourceMappingActive) {
+ map.addMapping({
+ generated: {
+ line: generated.line,
+ column: generated.column
+ }
+ });
+ lastOriginalSource = null;
+ sourceMappingActive = false;
+ }
+ for (var idx = 0, length = chunk.length; idx < length; idx++) {
+ if (chunk.charCodeAt(idx) === NEWLINE_CODE) {
+ generated.line++;
+ generated.column = 0;
+ // Mappings end at eol
+ if (idx + 1 === length) {
+ lastOriginalSource = null;
+ sourceMappingActive = false;
+ } else if (sourceMappingActive) {
+ map.addMapping({
+ source: original.source,
+ original: {
+ line: original.line,
+ column: original.column
+ },
+ generated: {
+ line: generated.line,
+ column: generated.column
+ },
+ name: original.name
+ });
+ }
+ } else {
+ generated.column++;
+ }
+ }
+ });
+ this.walkSourceContents(function (sourceFile, sourceContent) {
+ map.setSourceContent(sourceFile, sourceContent);
+ });
+
+ return { code: generated.code, map: map };
+ };
+
+ exports.SourceNode = SourceNode;
+
+ }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+
+/***/ }
+
+/******/ });
+//# sourceMappingURL=pretty-print-worker.js.map \ No newline at end of file
diff --git a/devtools/client/debugger/new/source-map-worker.js b/devtools/client/debugger/new/source-map-worker.js
new file mode 100644
index 000000000..6b1a55521
--- /dev/null
+++ b/devtools/client/debugger/new/source-map-worker.js
@@ -0,0 +1,5831 @@
+var Debugger =
+/******/ (function(modules) { // webpackBootstrap
+/******/ // The module cache
+/******/ var installedModules = {};
+/******/
+/******/ // The require function
+/******/ function __webpack_require__(moduleId) {
+/******/
+/******/ // Check if module is in cache
+/******/ if(installedModules[moduleId])
+/******/ return installedModules[moduleId].exports;
+/******/
+/******/ // Create a new module (and put it into the cache)
+/******/ var module = installedModules[moduleId] = {
+/******/ exports: {},
+/******/ id: moduleId,
+/******/ loaded: false
+/******/ };
+/******/
+/******/ // Execute the module function
+/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
+/******/
+/******/ // Flag the module as loaded
+/******/ module.loaded = true;
+/******/
+/******/ // Return the exports of the module
+/******/ return module.exports;
+/******/ }
+/******/
+/******/
+/******/ // expose the modules object (__webpack_modules__)
+/******/ __webpack_require__.m = modules;
+/******/
+/******/ // expose the module cache
+/******/ __webpack_require__.c = installedModules;
+/******/
+/******/ // __webpack_public_path__
+/******/ __webpack_require__.p = "/public/build";
+/******/
+/******/ // Load entry module and return exports
+/******/ return __webpack_require__(0);
+/******/ })
+/************************************************************************/
+/******/ ({
+
+/***/ 0:
+/***/ function(module, exports, __webpack_require__) {
+
+ var _resolveAndFetch = (() => {
+ var _ref = _asyncToGenerator(function* (generatedSource) {
+ // Fetch the sourcemap over the network and create it.
+ var sourceMapURL = _resolveSourceMapURL(generatedSource);
+ var fetched = yield networkRequest(sourceMapURL, { loadFromCache: false });
+
+ // Create the source map and fix it up.
+ var map = new SourceMapConsumer(fetched.content);
+ _setSourceMapRoot(map, sourceMapURL, generatedSource);
+ return map;
+ });
+
+ return function _resolveAndFetch(_x) {
+ return _ref.apply(this, arguments);
+ };
+ })();
+
+ var getOriginalURLs = (() => {
+ var _ref2 = _asyncToGenerator(function* (generatedSource) {
+ var map = yield _fetchSourceMap(generatedSource);
+ return map && map.sources;
+ });
+
+ return function getOriginalURLs(_x2) {
+ return _ref2.apply(this, arguments);
+ };
+ })();
+
+ var getGeneratedLocation = (() => {
+ var _ref3 = _asyncToGenerator(function* (location, originalSource) {
+ if (!isOriginalId(location.sourceId)) {
+ return location;
+ }
+
+ var generatedSourceId = originalToGeneratedId(location.sourceId);
+ var map = yield _getSourceMap(generatedSourceId);
+ if (!map) {
+ return location;
+ }
+
+ var _map$generatedPositio = map.generatedPositionFor({
+ source: originalSource.url,
+ line: location.line,
+ column: location.column == null ? 0 : location.column,
+ bias: SourceMapConsumer.LEAST_UPPER_BOUND
+ });
+
+ var line = _map$generatedPositio.line;
+ var column = _map$generatedPositio.column;
+
+
+ return {
+ sourceId: generatedSourceId,
+ line: line,
+ // Treat 0 as no column so that line breakpoints work correctly.
+ column: column === 0 ? undefined : column
+ };
+ });
+
+ return function getGeneratedLocation(_x3, _x4) {
+ return _ref3.apply(this, arguments);
+ };
+ })();
+
+ var getOriginalLocation = (() => {
+ var _ref4 = _asyncToGenerator(function* (location) {
+ if (!isGeneratedId(location.sourceId)) {
+ return location;
+ }
+
+ var map = yield _getSourceMap(location.sourceId);
+ if (!map) {
+ return location;
+ }
+
+ var _map$originalPosition = map.originalPositionFor({
+ line: location.line,
+ column: location.column == null ? Infinity : location.column
+ });
+
+ var url = _map$originalPosition.source;
+ var line = _map$originalPosition.line;
+ var column = _map$originalPosition.column;
+
+
+ if (url == null) {
+ // No url means the location didn't map.
+ return location;
+ }
+
+ return {
+ sourceId: generatedToOriginalId(location.sourceId, url),
+ line,
+ column
+ };
+ });
+
+ return function getOriginalLocation(_x5) {
+ return _ref4.apply(this, arguments);
+ };
+ })();
+
+ var getOriginalSourceText = (() => {
+ var _ref5 = _asyncToGenerator(function* (originalSource) {
+ assert(isOriginalId(originalSource.id), "Source is not an original source");
+
+ var generatedSourceId = originalToGeneratedId(originalSource.id);
+ var map = yield _getSourceMap(generatedSourceId);
+ if (!map) {
+ return null;
+ }
+
+ var text = map.sourceContentFor(originalSource.url);
+ if (!text) {
+ text = (yield networkRequest(originalSource.url, { loadFromCache: false })).content;
+ }
+
+ return {
+ text,
+ contentType: isJavaScript(originalSource.url || "") ? "text/javascript" : "text/plain"
+ };
+ });
+
+ return function getOriginalSourceText(_x6) {
+ return _ref5.apply(this, arguments);
+ };
+ })();
+
+ function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; }
+
+ /**
+ * Source Map Worker
+ * @module utils/source-map-worker
+ */
+
+ var networkRequest = __webpack_require__(207);
+
+ var _require = __webpack_require__(293);
+
+ var parse = _require.parse;
+
+ var path = __webpack_require__(278);
+
+ var _require2 = __webpack_require__(472);
+
+ var SourceMapConsumer = _require2.SourceMapConsumer;
+ var SourceMapGenerator = _require2.SourceMapGenerator;
+
+ var _require3 = __webpack_require__(277);
+
+ var isJavaScript = _require3.isJavaScript;
+
+ var assert = __webpack_require__(247);
+
+ var _require4 = __webpack_require__(265);
+
+ var originalToGeneratedId = _require4.originalToGeneratedId;
+ var generatedToOriginalId = _require4.generatedToOriginalId;
+ var isGeneratedId = _require4.isGeneratedId;
+ var isOriginalId = _require4.isOriginalId;
+
+
+ var sourceMapRequests = new Map();
+ var sourceMapsEnabled = false;
+
+ function clearSourceMaps() {
+ sourceMapRequests.clear();
+ }
+
+ function enableSourceMaps() {
+ sourceMapsEnabled = true;
+ }
+
+ function _resolveSourceMapURL(source) {
+ var _source$url = source.url;
+ var url = _source$url === undefined ? "" : _source$url;
+ var _source$sourceMapURL = source.sourceMapURL;
+ var sourceMapURL = _source$sourceMapURL === undefined ? "" : _source$sourceMapURL;
+
+ if (path.isURL(sourceMapURL) || url == "") {
+ // If it's already a full URL or the source doesn't have a URL,
+ // don't resolve anything.
+ return sourceMapURL;
+ } else if (path.isAbsolute(sourceMapURL)) {
+ // If it's an absolute path, it should be resolved relative to the
+ // host of the source.
+ var _parse = parse(url);
+
+ var _parse$protocol = _parse.protocol;
+ var protocol = _parse$protocol === undefined ? "" : _parse$protocol;
+ var _parse$host = _parse.host;
+ var host = _parse$host === undefined ? "" : _parse$host;
+
+ return `${ protocol }//${ host }${ sourceMapURL }`;
+ }
+ // Otherwise, it's a relative path and should be resolved relative
+ // to the source.
+ return path.dirname(url) + "/" + sourceMapURL;
+ }
+
+ /**
+ * Sets the source map's sourceRoot to be relative to the source map url.
+ * @memberof utils/source-map-worker
+ * @static
+ */
+ function _setSourceMapRoot(sourceMap, absSourceMapURL, source) {
+ // No need to do this fiddling if we won't be fetching any sources over the
+ // wire.
+ if (sourceMap.hasContentsOfAllSources()) {
+ return;
+ }
+
+ var base = path.dirname(absSourceMapURL.indexOf("data:") === 0 && source.url ? source.url : absSourceMapURL);
+
+ if (sourceMap.sourceRoot) {
+ sourceMap.sourceRoot = path.join(base, sourceMap.sourceRoot);
+ } else {
+ sourceMap.sourceRoot = base;
+ }
+
+ return sourceMap;
+ }
+
+ function _getSourceMap(generatedSourceId) {
+ return sourceMapRequests.get(generatedSourceId);
+ }
+
+ function _fetchSourceMap(generatedSource) {
+ var existingRequest = sourceMapRequests.get(generatedSource.id);
+ if (existingRequest) {
+ // If it has already been requested, return the request. Make sure
+ // to do this even if sourcemapping is turned off, because
+ // pretty-printing uses sourcemaps.
+ //
+ // An important behavior here is that if it's in the middle of
+ // requesting it, all subsequent calls will block on the initial
+ // request.
+ return existingRequest;
+ } else if (!generatedSource.sourceMapURL || !sourceMapsEnabled) {
+ return Promise.resolve(null);
+ }
+
+ // Fire off the request, set it in the cache, and return it.
+ // Suppress any errors and just return null (ignores bogus
+ // sourcemaps).
+ var req = _resolveAndFetch(generatedSource).catch(() => null);
+ sourceMapRequests.set(generatedSource.id, req);
+ return req;
+ }
+
+ function applySourceMap(generatedId, url, code, mappings) {
+ var generator = new SourceMapGenerator({ file: url });
+ mappings.forEach(mapping => generator.addMapping(mapping));
+ generator.setSourceContent(url, code);
+
+ var map = SourceMapConsumer(generator.toJSON());
+ sourceMapRequests.set(generatedId, Promise.resolve(map));
+ }
+
+ var publicInterface = {
+ getOriginalURLs,
+ getGeneratedLocation,
+ getOriginalLocation,
+ getOriginalSourceText,
+ enableSourceMaps,
+ applySourceMap,
+ clearSourceMaps
+ };
+
+ self.onmessage = function (msg) {
+ var _msg$data = msg.data;
+ var id = _msg$data.id;
+ var method = _msg$data.method;
+ var args = _msg$data.args;
+
+ var response = publicInterface[method].apply(undefined, args);
+ if (response instanceof Promise) {
+ response.then(val => self.postMessage({ id, response: val }), err => self.postMessage({ id, error: err }));
+ } else {
+ self.postMessage({ id, response });
+ }
+ };
+
+/***/ },
+
+/***/ 77:
+/***/ function(module, exports) {
+
+ module.exports = function(module) {
+ if(!module.webpackPolyfill) {
+ module.deprecate = function() {};
+ module.paths = [];
+ // module.parent = undefined by default
+ module.children = [];
+ module.webpackPolyfill = 1;
+ }
+ return module;
+ }
+
+
+/***/ },
+
+/***/ 207:
+/***/ function(module, exports) {
+
+ function networkRequest(url, opts) {
+ return new Promise((resolve, reject) => {
+ var req = new XMLHttpRequest();
+
+ req.addEventListener("readystatechange", () => {
+ if (req.readyState === XMLHttpRequest.DONE) {
+ if (req.status === 200) {
+ resolve({ content: req.responseText });
+ } else {
+ resolve(req.statusText);
+ }
+ }
+ });
+
+ // Not working yet.
+ // if (!opts.loadFromCache) {
+ // req.channel.loadFlags = (
+ // Components.interfaces.nsIRequest.LOAD_BYPASS_CACHE |
+ // Components.interfaces.nsIRequest.INHIBIT_CACHING |
+ // Components.interfaces.nsIRequest.LOAD_ANONYMOUS
+ // );
+ // }
+
+ req.open("GET", url);
+ req.send();
+ });
+ }
+
+ module.exports = networkRequest;
+
+/***/ },
+
+/***/ 244:
+/***/ function(module, exports, __webpack_require__) {
+
+ var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }();
+
+ function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
+
+ /* -*- 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/. */
+
+ /**
+ * Utils for utils, by utils
+ * @module utils/utils
+ */
+
+ var co = __webpack_require__(245);
+
+ /**
+ * @memberof utils/utils
+ * @static
+ */
+ function asPaused(client, func) {
+ if (client.state != "paused") {
+ return co(function* () {
+ yield client.interrupt();
+ var result = void 0;
+
+ try {
+ result = yield func();
+ } catch (e) {
+ // Try to put the debugger back in a working state by resuming
+ // it
+ yield client.resume();
+ throw e;
+ }
+
+ yield client.resume();
+ return result;
+ });
+ }
+ return func();
+ }
+
+ /**
+ * @memberof utils/utils
+ * @static
+ */
+ function handleError(err) {
+ console.log("ERROR: ", err);
+ }
+
+ /**
+ * @memberof utils/utils
+ * @static
+ */
+ function promisify(context, method) {
+ for (var _len = arguments.length, args = Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) {
+ args[_key - 2] = arguments[_key];
+ }
+
+ return new Promise((resolve, reject) => {
+ args.push(response => {
+ if (response.error) {
+ reject(response);
+ } else {
+ resolve(response);
+ }
+ });
+ method.apply(context, args);
+ });
+ }
+
+ /**
+ * @memberof utils/utils
+ * @static
+ */
+ function truncateStr(str, size) {
+ if (str.length > size) {
+ return str.slice(0, size) + "...";
+ }
+ return str;
+ }
+
+ /**
+ * @memberof utils/utils
+ * @static
+ */
+ function endTruncateStr(str, size) {
+ if (str.length > size) {
+ return "..." + str.slice(str.length - size);
+ }
+ return str;
+ }
+
+ var msgId = 1;
+ /**
+ * @memberof utils/utils
+ * @static
+ */
+ function workerTask(worker, method) {
+ return function () {
+ for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
+ args[_key2] = arguments[_key2];
+ }
+
+ return new Promise((resolve, reject) => {
+ var id = msgId++;
+ worker.postMessage({ id, method, args });
+
+ var listener = (_ref) => {
+ var result = _ref.data;
+
+ if (result.id !== id) {
+ return;
+ }
+
+ worker.removeEventListener("message", listener);
+ if (result.error) {
+ reject(result.error);
+ } else {
+ resolve(result.response);
+ }
+ };
+
+ worker.addEventListener("message", listener);
+ });
+ };
+ }
+
+ /**
+ * Interleaves two arrays element by element, returning the combined array, like
+ * a zip. In the case of arrays with different sizes, undefined values will be
+ * interleaved at the end along with the extra values of the larger array.
+ *
+ * @param Array a
+ * @param Array b
+ * @returns Array
+ * The combined array, in the form [a1, b1, a2, b2, ...]
+ * @memberof utils/utils
+ * @static
+ */
+ function zip(a, b) {
+ if (!b) {
+ return a;
+ }
+ if (!a) {
+ return b;
+ }
+ var pairs = [];
+ for (var i = 0, aLength = a.length, bLength = b.length; i < aLength || i < bLength; i++) {
+ pairs.push([a[i], b[i]]);
+ }
+ return pairs;
+ }
+
+ /**
+ * Converts an object into an array with 2-element arrays as key/value
+ * pairs of the object. `{ foo: 1, bar: 2}` would become
+ * `[[foo, 1], [bar 2]]` (order not guaranteed);
+ *
+ * @returns array
+ * @memberof utils/utils
+ * @static
+ */
+ function entries(obj) {
+ return Object.keys(obj).map(k => [k, obj[k]]);
+ }
+
+ /**
+ * @memberof utils/utils
+ * @static
+ */
+ function mapObject(obj, iteratee) {
+ return toObject(entries(obj).map((_ref2) => {
+ var _ref3 = _slicedToArray(_ref2, 2);
+
+ var key = _ref3[0];
+ var value = _ref3[1];
+
+ return [key, iteratee(key, value)];
+ }));
+ }
+
+ /**
+ * Takes an array of 2-element arrays as key/values pairs and
+ * constructs an object using them.
+ * @memberof utils/utils
+ * @static
+ */
+ function toObject(arr) {
+ var obj = {};
+ for (var pair of arr) {
+ obj[pair[0]] = pair[1];
+ }
+ return obj;
+ }
+
+ /**
+ * Composes the given functions into a single function, which will
+ * apply the results of each function right-to-left, starting with
+ * applying the given arguments to the right-most function.
+ * `compose(foo, bar, baz)` === `args => foo(bar(baz(args)`
+ *
+ * @param ...function funcs
+ * @returns function
+ * @memberof utils/utils
+ * @static
+ */
+ function compose() {
+ for (var _len3 = arguments.length, funcs = Array(_len3), _key3 = 0; _key3 < _len3; _key3++) {
+ funcs[_key3] = arguments[_key3];
+ }
+
+ return function () {
+ for (var _len4 = arguments.length, args = Array(_len4), _key4 = 0; _key4 < _len4; _key4++) {
+ args[_key4] = arguments[_key4];
+ }
+
+ var initialValue = funcs[funcs.length - 1].apply(null, args);
+ var leftFuncs = funcs.slice(0, -1);
+ return leftFuncs.reduceRight((composed, f) => f(composed), initialValue);
+ };
+ }
+
+ /**
+ * @memberof utils/utils
+ * @static
+ */
+ function updateObj(obj, fields) {
+ return Object.assign({}, obj, fields);
+ }
+
+ /**
+ * @memberof utils/utils
+ * @static
+ */
+ function throttle(func, ms) {
+ var timeout = void 0,
+ _this = void 0;
+ return function () {
+ for (var _len5 = arguments.length, args = Array(_len5), _key5 = 0; _key5 < _len5; _key5++) {
+ args[_key5] = arguments[_key5];
+ }
+
+ _this = this;
+ if (!timeout) {
+ timeout = setTimeout(() => {
+ func.apply.apply(func, [_this].concat(_toConsumableArray(args)));
+ timeout = null;
+ }, ms);
+ }
+ };
+ }
+
+ module.exports = {
+ asPaused,
+ handleError,
+ promisify,
+ truncateStr,
+ endTruncateStr,
+ workerTask,
+ zip,
+ entries,
+ toObject,
+ mapObject,
+ compose,
+ updateObj,
+ throttle
+ };
+
+/***/ },
+
+/***/ 245:
+/***/ function(module, exports) {
+
+
+ /**
+ * slice() reference.
+ */
+
+ var slice = Array.prototype.slice;
+
+ /**
+ * Expose `co`.
+ */
+
+ module.exports = co['default'] = co.co = co;
+
+ /**
+ * Wrap the given generator `fn` into a
+ * function that returns a promise.
+ * This is a separate function so that
+ * every `co()` call doesn't create a new,
+ * unnecessary closure.
+ *
+ * @param {GeneratorFunction} fn
+ * @return {Function}
+ * @api public
+ */
+
+ co.wrap = function (fn) {
+ createPromise.__generatorFunction__ = fn;
+ return createPromise;
+ function createPromise() {
+ return co.call(this, fn.apply(this, arguments));
+ }
+ };
+
+ /**
+ * Execute the generator function or a generator
+ * and return a promise.
+ *
+ * @param {Function} fn
+ * @return {Promise}
+ * @api public
+ */
+
+ function co(gen) {
+ var ctx = this;
+ var args = slice.call(arguments, 1)
+
+ // we wrap everything in a promise to avoid promise chaining,
+ // which leads to memory leak errors.
+ // see https://github.com/tj/co/issues/180
+ return new Promise(function(resolve, reject) {
+ if (typeof gen === 'function') gen = gen.apply(ctx, args);
+ if (!gen || typeof gen.next !== 'function') return resolve(gen);
+
+ onFulfilled();
+
+ /**
+ * @param {Mixed} res
+ * @return {Promise}
+ * @api private
+ */
+
+ function onFulfilled(res) {
+ var ret;
+ try {
+ ret = gen.next(res);
+ } catch (e) {
+ return reject(e);
+ }
+ next(ret);
+ }
+
+ /**
+ * @param {Error} err
+ * @return {Promise}
+ * @api private
+ */
+
+ function onRejected(err) {
+ var ret;
+ try {
+ ret = gen.throw(err);
+ } catch (e) {
+ return reject(e);
+ }
+ next(ret);
+ }
+
+ /**
+ * Get the next value in the generator,
+ * return a promise.
+ *
+ * @param {Object} ret
+ * @return {Promise}
+ * @api private
+ */
+
+ function next(ret) {
+ if (ret.done) return resolve(ret.value);
+ var value = toPromise.call(ctx, ret.value);
+ if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
+ return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
+ + 'but the following object was passed: "' + String(ret.value) + '"'));
+ }
+ });
+ }
+
+ /**
+ * Convert a `yield`ed value into a promise.
+ *
+ * @param {Mixed} obj
+ * @return {Promise}
+ * @api private
+ */
+
+ function toPromise(obj) {
+ if (!obj) return obj;
+ if (isPromise(obj)) return obj;
+ if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
+ if ('function' == typeof obj) return thunkToPromise.call(this, obj);
+ if (Array.isArray(obj)) return arrayToPromise.call(this, obj);
+ if (isObject(obj)) return objectToPromise.call(this, obj);
+ return obj;
+ }
+
+ /**
+ * Convert a thunk to a promise.
+ *
+ * @param {Function}
+ * @return {Promise}
+ * @api private
+ */
+
+ function thunkToPromise(fn) {
+ var ctx = this;
+ return new Promise(function (resolve, reject) {
+ fn.call(ctx, function (err, res) {
+ if (err) return reject(err);
+ if (arguments.length > 2) res = slice.call(arguments, 1);
+ resolve(res);
+ });
+ });
+ }
+
+ /**
+ * Convert an array of "yieldables" to a promise.
+ * Uses `Promise.all()` internally.
+ *
+ * @param {Array} obj
+ * @return {Promise}
+ * @api private
+ */
+
+ function arrayToPromise(obj) {
+ return Promise.all(obj.map(toPromise, this));
+ }
+
+ /**
+ * Convert an object of "yieldables" to a promise.
+ * Uses `Promise.all()` internally.
+ *
+ * @param {Object} obj
+ * @return {Promise}
+ * @api private
+ */
+
+ function objectToPromise(obj){
+ var results = new obj.constructor();
+ var keys = Object.keys(obj);
+ var promises = [];
+ for (var i = 0; i < keys.length; i++) {
+ var key = keys[i];
+ var promise = toPromise.call(this, obj[key]);
+ if (promise && isPromise(promise)) defer(promise, key);
+ else results[key] = obj[key];
+ }
+ return Promise.all(promises).then(function () {
+ return results;
+ });
+
+ function defer(promise, key) {
+ // predefine the key in the result
+ results[key] = undefined;
+ promises.push(promise.then(function (res) {
+ results[key] = res;
+ }));
+ }
+ }
+
+ /**
+ * Check if `obj` is a promise.
+ *
+ * @param {Object} obj
+ * @return {Boolean}
+ * @api private
+ */
+
+ function isPromise(obj) {
+ return 'function' == typeof obj.then;
+ }
+
+ /**
+ * Check if `obj` is a generator.
+ *
+ * @param {Mixed} obj
+ * @return {Boolean}
+ * @api private
+ */
+
+ function isGenerator(obj) {
+ return 'function' == typeof obj.next && 'function' == typeof obj.throw;
+ }
+
+ /**
+ * Check if `obj` is a generator function.
+ *
+ * @param {Mixed} obj
+ * @return {Boolean}
+ * @api private
+ */
+ function isGeneratorFunction(obj) {
+ var constructor = obj.constructor;
+ if (!constructor) return false;
+ if ('GeneratorFunction' === constructor.name || 'GeneratorFunction' === constructor.displayName) return true;
+ return isGenerator(constructor.prototype);
+ }
+
+ /**
+ * Check for plain object.
+ *
+ * @param {Mixed} val
+ * @return {Boolean}
+ * @api private
+ */
+
+ function isObject(val) {
+ return Object == val.constructor;
+ }
+
+
+/***/ },
+
+/***/ 247:
+/***/ function(module, exports) {
+
+ function assert(condition, message) {
+ if (!condition) {
+ throw new Error("Assertion failure: " + message);
+ }
+ }
+
+ module.exports = assert;
+
+/***/ },
+
+/***/ 265:
+/***/ function(module, exports, __webpack_require__) {
+
+ var md5 = __webpack_require__(266);
+
+ function originalToGeneratedId(originalId) {
+ var match = originalId.match(/(.*)\/originalSource/);
+ return match ? match[1] : "";
+ }
+
+ function generatedToOriginalId(generatedId, url) {
+ return generatedId + "/originalSource-" + md5(url);
+ }
+
+ function isOriginalId(id) {
+ return !!id.match(/\/originalSource/);
+ }
+
+ function isGeneratedId(id) {
+ return !isOriginalId(id);
+ }
+
+ module.exports = {
+ originalToGeneratedId, generatedToOriginalId, isOriginalId, isGeneratedId
+ };
+
+/***/ },
+
+/***/ 266:
+/***/ function(module, exports, __webpack_require__) {
+
+ (function(){
+ var crypt = __webpack_require__(267),
+ utf8 = __webpack_require__(268).utf8,
+ isBuffer = __webpack_require__(269),
+ bin = __webpack_require__(268).bin,
+
+ // The core
+ md5 = function (message, options) {
+ // Convert to byte array
+ if (message.constructor == String)
+ if (options && options.encoding === 'binary')
+ message = bin.stringToBytes(message);
+ else
+ message = utf8.stringToBytes(message);
+ else if (isBuffer(message))
+ message = Array.prototype.slice.call(message, 0);
+ else if (!Array.isArray(message))
+ message = message.toString();
+ // else, assume byte array already
+
+ var m = crypt.bytesToWords(message),
+ l = message.length * 8,
+ a = 1732584193,
+ b = -271733879,
+ c = -1732584194,
+ d = 271733878;
+
+ // Swap endian
+ for (var i = 0; i < m.length; i++) {
+ m[i] = ((m[i] << 8) | (m[i] >>> 24)) & 0x00FF00FF |
+ ((m[i] << 24) | (m[i] >>> 8)) & 0xFF00FF00;
+ }
+
+ // Padding
+ m[l >>> 5] |= 0x80 << (l % 32);
+ m[(((l + 64) >>> 9) << 4) + 14] = l;
+
+ // Method shortcuts
+ var FF = md5._ff,
+ GG = md5._gg,
+ HH = md5._hh,
+ II = md5._ii;
+
+ for (var i = 0; i < m.length; i += 16) {
+
+ var aa = a,
+ bb = b,
+ cc = c,
+ dd = d;
+
+ a = FF(a, b, c, d, m[i+ 0], 7, -680876936);
+ d = FF(d, a, b, c, m[i+ 1], 12, -389564586);
+ c = FF(c, d, a, b, m[i+ 2], 17, 606105819);
+ b = FF(b, c, d, a, m[i+ 3], 22, -1044525330);
+ a = FF(a, b, c, d, m[i+ 4], 7, -176418897);
+ d = FF(d, a, b, c, m[i+ 5], 12, 1200080426);
+ c = FF(c, d, a, b, m[i+ 6], 17, -1473231341);
+ b = FF(b, c, d, a, m[i+ 7], 22, -45705983);
+ a = FF(a, b, c, d, m[i+ 8], 7, 1770035416);
+ d = FF(d, a, b, c, m[i+ 9], 12, -1958414417);
+ c = FF(c, d, a, b, m[i+10], 17, -42063);
+ b = FF(b, c, d, a, m[i+11], 22, -1990404162);
+ a = FF(a, b, c, d, m[i+12], 7, 1804603682);
+ d = FF(d, a, b, c, m[i+13], 12, -40341101);
+ c = FF(c, d, a, b, m[i+14], 17, -1502002290);
+ b = FF(b, c, d, a, m[i+15], 22, 1236535329);
+
+ a = GG(a, b, c, d, m[i+ 1], 5, -165796510);
+ d = GG(d, a, b, c, m[i+ 6], 9, -1069501632);
+ c = GG(c, d, a, b, m[i+11], 14, 643717713);
+ b = GG(b, c, d, a, m[i+ 0], 20, -373897302);
+ a = GG(a, b, c, d, m[i+ 5], 5, -701558691);
+ d = GG(d, a, b, c, m[i+10], 9, 38016083);
+ c = GG(c, d, a, b, m[i+15], 14, -660478335);
+ b = GG(b, c, d, a, m[i+ 4], 20, -405537848);
+ a = GG(a, b, c, d, m[i+ 9], 5, 568446438);
+ d = GG(d, a, b, c, m[i+14], 9, -1019803690);
+ c = GG(c, d, a, b, m[i+ 3], 14, -187363961);
+ b = GG(b, c, d, a, m[i+ 8], 20, 1163531501);
+ a = GG(a, b, c, d, m[i+13], 5, -1444681467);
+ d = GG(d, a, b, c, m[i+ 2], 9, -51403784);
+ c = GG(c, d, a, b, m[i+ 7], 14, 1735328473);
+ b = GG(b, c, d, a, m[i+12], 20, -1926607734);
+
+ a = HH(a, b, c, d, m[i+ 5], 4, -378558);
+ d = HH(d, a, b, c, m[i+ 8], 11, -2022574463);
+ c = HH(c, d, a, b, m[i+11], 16, 1839030562);
+ b = HH(b, c, d, a, m[i+14], 23, -35309556);
+ a = HH(a, b, c, d, m[i+ 1], 4, -1530992060);
+ d = HH(d, a, b, c, m[i+ 4], 11, 1272893353);
+ c = HH(c, d, a, b, m[i+ 7], 16, -155497632);
+ b = HH(b, c, d, a, m[i+10], 23, -1094730640);
+ a = HH(a, b, c, d, m[i+13], 4, 681279174);
+ d = HH(d, a, b, c, m[i+ 0], 11, -358537222);
+ c = HH(c, d, a, b, m[i+ 3], 16, -722521979);
+ b = HH(b, c, d, a, m[i+ 6], 23, 76029189);
+ a = HH(a, b, c, d, m[i+ 9], 4, -640364487);
+ d = HH(d, a, b, c, m[i+12], 11, -421815835);
+ c = HH(c, d, a, b, m[i+15], 16, 530742520);
+ b = HH(b, c, d, a, m[i+ 2], 23, -995338651);
+
+ a = II(a, b, c, d, m[i+ 0], 6, -198630844);
+ d = II(d, a, b, c, m[i+ 7], 10, 1126891415);
+ c = II(c, d, a, b, m[i+14], 15, -1416354905);
+ b = II(b, c, d, a, m[i+ 5], 21, -57434055);
+ a = II(a, b, c, d, m[i+12], 6, 1700485571);
+ d = II(d, a, b, c, m[i+ 3], 10, -1894986606);
+ c = II(c, d, a, b, m[i+10], 15, -1051523);
+ b = II(b, c, d, a, m[i+ 1], 21, -2054922799);
+ a = II(a, b, c, d, m[i+ 8], 6, 1873313359);
+ d = II(d, a, b, c, m[i+15], 10, -30611744);
+ c = II(c, d, a, b, m[i+ 6], 15, -1560198380);
+ b = II(b, c, d, a, m[i+13], 21, 1309151649);
+ a = II(a, b, c, d, m[i+ 4], 6, -145523070);
+ d = II(d, a, b, c, m[i+11], 10, -1120210379);
+ c = II(c, d, a, b, m[i+ 2], 15, 718787259);
+ b = II(b, c, d, a, m[i+ 9], 21, -343485551);
+
+ a = (a + aa) >>> 0;
+ b = (b + bb) >>> 0;
+ c = (c + cc) >>> 0;
+ d = (d + dd) >>> 0;
+ }
+
+ return crypt.endian([a, b, c, d]);
+ };
+
+ // Auxiliary functions
+ md5._ff = function (a, b, c, d, x, s, t) {
+ var n = a + (b & c | ~b & d) + (x >>> 0) + t;
+ return ((n << s) | (n >>> (32 - s))) + b;
+ };
+ md5._gg = function (a, b, c, d, x, s, t) {
+ var n = a + (b & d | c & ~d) + (x >>> 0) + t;
+ return ((n << s) | (n >>> (32 - s))) + b;
+ };
+ md5._hh = function (a, b, c, d, x, s, t) {
+ var n = a + (b ^ c ^ d) + (x >>> 0) + t;
+ return ((n << s) | (n >>> (32 - s))) + b;
+ };
+ md5._ii = function (a, b, c, d, x, s, t) {
+ var n = a + (c ^ (b | ~d)) + (x >>> 0) + t;
+ return ((n << s) | (n >>> (32 - s))) + b;
+ };
+
+ // Package private blocksize
+ md5._blocksize = 16;
+ md5._digestsize = 16;
+
+ module.exports = function (message, options) {
+ if (message === undefined || message === null)
+ throw new Error('Illegal argument ' + message);
+
+ var digestbytes = crypt.wordsToBytes(md5(message, options));
+ return options && options.asBytes ? digestbytes :
+ options && options.asString ? bin.bytesToString(digestbytes) :
+ crypt.bytesToHex(digestbytes);
+ };
+
+ })();
+
+
+/***/ },
+
+/***/ 267:
+/***/ function(module, exports) {
+
+ (function() {
+ var base64map
+ = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/',
+
+ crypt = {
+ // Bit-wise rotation left
+ rotl: function(n, b) {
+ return (n << b) | (n >>> (32 - b));
+ },
+
+ // Bit-wise rotation right
+ rotr: function(n, b) {
+ return (n << (32 - b)) | (n >>> b);
+ },
+
+ // Swap big-endian to little-endian and vice versa
+ endian: function(n) {
+ // If number given, swap endian
+ if (n.constructor == Number) {
+ return crypt.rotl(n, 8) & 0x00FF00FF | crypt.rotl(n, 24) & 0xFF00FF00;
+ }
+
+ // Else, assume array and swap all items
+ for (var i = 0; i < n.length; i++)
+ n[i] = crypt.endian(n[i]);
+ return n;
+ },
+
+ // Generate an array of any length of random bytes
+ randomBytes: function(n) {
+ for (var bytes = []; n > 0; n--)
+ bytes.push(Math.floor(Math.random() * 256));
+ return bytes;
+ },
+
+ // Convert a byte array to big-endian 32-bit words
+ bytesToWords: function(bytes) {
+ for (var words = [], i = 0, b = 0; i < bytes.length; i++, b += 8)
+ words[b >>> 5] |= bytes[i] << (24 - b % 32);
+ return words;
+ },
+
+ // Convert big-endian 32-bit words to a byte array
+ wordsToBytes: function(words) {
+ for (var bytes = [], b = 0; b < words.length * 32; b += 8)
+ bytes.push((words[b >>> 5] >>> (24 - b % 32)) & 0xFF);
+ return bytes;
+ },
+
+ // Convert a byte array to a hex string
+ bytesToHex: function(bytes) {
+ for (var hex = [], i = 0; i < bytes.length; i++) {
+ hex.push((bytes[i] >>> 4).toString(16));
+ hex.push((bytes[i] & 0xF).toString(16));
+ }
+ return hex.join('');
+ },
+
+ // Convert a hex string to a byte array
+ hexToBytes: function(hex) {
+ for (var bytes = [], c = 0; c < hex.length; c += 2)
+ bytes.push(parseInt(hex.substr(c, 2), 16));
+ return bytes;
+ },
+
+ // Convert a byte array to a base-64 string
+ bytesToBase64: function(bytes) {
+ for (var base64 = [], i = 0; i < bytes.length; i += 3) {
+ var triplet = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2];
+ for (var j = 0; j < 4; j++)
+ if (i * 8 + j * 6 <= bytes.length * 8)
+ base64.push(base64map.charAt((triplet >>> 6 * (3 - j)) & 0x3F));
+ else
+ base64.push('=');
+ }
+ return base64.join('');
+ },
+
+ // Convert a base-64 string to a byte array
+ base64ToBytes: function(base64) {
+ // Remove non-base-64 characters
+ base64 = base64.replace(/[^A-Z0-9+\/]/ig, '');
+
+ for (var bytes = [], i = 0, imod4 = 0; i < base64.length;
+ imod4 = ++i % 4) {
+ if (imod4 == 0) continue;
+ bytes.push(((base64map.indexOf(base64.charAt(i - 1))
+ & (Math.pow(2, -2 * imod4 + 8) - 1)) << (imod4 * 2))
+ | (base64map.indexOf(base64.charAt(i)) >>> (6 - imod4 * 2)));
+ }
+ return bytes;
+ }
+ };
+
+ module.exports = crypt;
+ })();
+
+
+/***/ },
+
+/***/ 268:
+/***/ function(module, exports) {
+
+ var charenc = {
+ // UTF-8 encoding
+ utf8: {
+ // Convert a string to a byte array
+ stringToBytes: function(str) {
+ return charenc.bin.stringToBytes(unescape(encodeURIComponent(str)));
+ },
+
+ // Convert a byte array to a string
+ bytesToString: function(bytes) {
+ return decodeURIComponent(escape(charenc.bin.bytesToString(bytes)));
+ }
+ },
+
+ // Binary encoding
+ bin: {
+ // Convert a string to a byte array
+ stringToBytes: function(str) {
+ for (var bytes = [], i = 0; i < str.length; i++)
+ bytes.push(str.charCodeAt(i) & 0xFF);
+ return bytes;
+ },
+
+ // Convert a byte array to a string
+ bytesToString: function(bytes) {
+ for (var str = [], i = 0; i < bytes.length; i++)
+ str.push(String.fromCharCode(bytes[i]));
+ return str.join('');
+ }
+ }
+ };
+
+ module.exports = charenc;
+
+
+/***/ },
+
+/***/ 269:
+/***/ function(module, exports) {
+
+ /*!
+ * Determine if an object is a Buffer
+ *
+ * @author Feross Aboukhadijeh <feross@feross.org> <http://feross.org>
+ * @license MIT
+ */
+
+ // The _isBuffer check is for Safari 5-7 support, because it's missing
+ // Object.prototype.constructor. Remove this eventually
+ module.exports = function (obj) {
+ return obj != null && (isBuffer(obj) || isSlowBuffer(obj) || !!obj._isBuffer)
+ }
+
+ function isBuffer (obj) {
+ return !!obj.constructor && typeof obj.constructor.isBuffer === 'function' && obj.constructor.isBuffer(obj)
+ }
+
+ // For Node v0.10 support. Remove this eventually.
+ function isSlowBuffer (obj) {
+ return typeof obj.readFloatLE === 'function' && typeof obj.slice === 'function' && isBuffer(obj.slice(0, 0))
+ }
+
+
+/***/ },
+
+/***/ 277:
+/***/ function(module, exports, __webpack_require__) {
+
+
+
+ /**
+ * Utils for working with Source URLs
+ * @module utils/source
+ */
+
+ var _require = __webpack_require__(244);
+
+ var endTruncateStr = _require.endTruncateStr;
+
+ var _require2 = __webpack_require__(278);
+
+ var basename = _require2.basename;
+
+
+ /**
+ * Trims the query part or reference identifier of a url string, if necessary.
+ *
+ * @memberof utils/source
+ * @static
+ */
+ function trimUrlQuery(url) {
+ var length = url.length;
+ var q1 = url.indexOf("?");
+ var q2 = url.indexOf("&");
+ var q3 = url.indexOf("#");
+ var q = Math.min(q1 != -1 ? q1 : length, q2 != -1 ? q2 : length, q3 != -1 ? q3 : length);
+
+ return url.slice(0, q);
+ }
+
+ /**
+ * Returns true if the specified url and/or content type are specific to
+ * javascript files.
+ *
+ * @return boolean
+ * True if the source is likely javascript.
+ *
+ * @memberof utils/source
+ * @static
+ */
+ function isJavaScript(url) {
+ var contentType = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "";
+
+ return url && /\.(jsm|js)?$/.test(trimUrlQuery(url)) || contentType.includes("javascript");
+ }
+
+ /**
+ * @memberof utils/source
+ * @static
+ */
+ function isPretty(source) {
+ return source.url ? /formatted$/.test(source.url) : false;
+ }
+
+ /**
+ * Show a source url's filename.
+ * If the source does not have a url, use the source id.
+ *
+ * @memberof utils/source
+ * @static
+ */
+ function getFilename(source) {
+ var url = source.url;
+ var id = source.id;
+
+ if (!url) {
+ var sourceId = id.split("/")[1];
+ return `SOURCE${ sourceId }`;
+ }
+
+ var name = basename(source.url || "") || "(index)";
+ return endTruncateStr(name, 50);
+ }
+
+ module.exports = {
+ isJavaScript,
+ isPretty,
+ getFilename
+ };
+
+/***/ },
+
+/***/ 278:
+/***/ function(module, exports) {
+
+ function basename(path) {
+ return path.split("/").pop();
+ }
+
+ function dirname(path) {
+ var idx = path.lastIndexOf("/");
+ return path.slice(0, idx);
+ }
+
+ function isURL(str) {
+ return str.indexOf("://") !== -1;
+ }
+
+ function isAbsolute(str) {
+ return str[0] === "/";
+ }
+
+ function join(base, dir) {
+ return base + "/" + dir;
+ }
+
+ module.exports = {
+ basename, dirname, isURL, isAbsolute, join
+ };
+
+/***/ },
+
+/***/ 293:
+/***/ function(module, exports, __webpack_require__) {
+
+ // Copyright Joyent, Inc. and other Node contributors.
+ //
+ // Permission is hereby granted, free of charge, to any person obtaining a
+ // copy of this software and associated documentation files (the
+ // "Software"), to deal in the Software without restriction, including
+ // without limitation the rights to use, copy, modify, merge, publish,
+ // distribute, sublicense, and/or sell copies of the Software, and to permit
+ // persons to whom the Software is furnished to do so, subject to the
+ // following conditions:
+ //
+ // The above copyright notice and this permission notice shall be included
+ // in all copies or substantial portions of the Software.
+ //
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+ // NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+ // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+ // USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+ var punycode = __webpack_require__(294);
+
+ exports.parse = urlParse;
+ exports.resolve = urlResolve;
+ exports.resolveObject = urlResolveObject;
+ exports.format = urlFormat;
+
+ exports.Url = Url;
+
+ function Url() {
+ this.protocol = null;
+ this.slashes = null;
+ this.auth = null;
+ this.host = null;
+ this.port = null;
+ this.hostname = null;
+ this.hash = null;
+ this.search = null;
+ this.query = null;
+ this.pathname = null;
+ this.path = null;
+ this.href = null;
+ }
+
+ // Reference: RFC 3986, RFC 1808, RFC 2396
+
+ // define these here so at least they only have to be
+ // compiled once on the first module load.
+ var protocolPattern = /^([a-z0-9.+-]+:)/i,
+ portPattern = /:[0-9]*$/,
+
+ // RFC 2396: characters reserved for delimiting URLs.
+ // We actually just auto-escape these.
+ delims = ['<', '>', '"', '`', ' ', '\r', '\n', '\t'],
+
+ // RFC 2396: characters not allowed for various reasons.
+ unwise = ['{', '}', '|', '\\', '^', '`'].concat(delims),
+
+ // Allowed by RFCs, but cause of XSS attacks. Always escape these.
+ autoEscape = ['\''].concat(unwise),
+ // Characters that are never ever allowed in a hostname.
+ // Note that any invalid chars are also handled, but these
+ // are the ones that are *expected* to be seen, so we fast-path
+ // them.
+ nonHostChars = ['%', '/', '?', ';', '#'].concat(autoEscape),
+ hostEndingChars = ['/', '?', '#'],
+ hostnameMaxLen = 255,
+ hostnamePartPattern = /^[a-z0-9A-Z_-]{0,63}$/,
+ hostnamePartStart = /^([a-z0-9A-Z_-]{0,63})(.*)$/,
+ // protocols that can allow "unsafe" and "unwise" chars.
+ unsafeProtocol = {
+ 'javascript': true,
+ 'javascript:': true
+ },
+ // protocols that never have a hostname.
+ hostlessProtocol = {
+ 'javascript': true,
+ 'javascript:': true
+ },
+ // protocols that always contain a // bit.
+ slashedProtocol = {
+ 'http': true,
+ 'https': true,
+ 'ftp': true,
+ 'gopher': true,
+ 'file': true,
+ 'http:': true,
+ 'https:': true,
+ 'ftp:': true,
+ 'gopher:': true,
+ 'file:': true
+ },
+ querystring = __webpack_require__(295);
+
+ function urlParse(url, parseQueryString, slashesDenoteHost) {
+ if (url && isObject(url) && url instanceof Url) return url;
+
+ var u = new Url;
+ u.parse(url, parseQueryString, slashesDenoteHost);
+ return u;
+ }
+
+ Url.prototype.parse = function(url, parseQueryString, slashesDenoteHost) {
+ if (!isString(url)) {
+ throw new TypeError("Parameter 'url' must be a string, not " + typeof url);
+ }
+
+ var rest = url;
+
+ // trim before proceeding.
+ // This is to support parse stuff like " http://foo.com \n"
+ rest = rest.trim();
+
+ var proto = protocolPattern.exec(rest);
+ if (proto) {
+ proto = proto[0];
+ var lowerProto = proto.toLowerCase();
+ this.protocol = lowerProto;
+ rest = rest.substr(proto.length);
+ }
+
+ // figure out if it's got a host
+ // user@server is *always* interpreted as a hostname, and url
+ // resolution will treat //foo/bar as host=foo,path=bar because that's
+ // how the browser resolves relative URLs.
+ if (slashesDenoteHost || proto || rest.match(/^\/\/[^@\/]+@[^@\/]+/)) {
+ var slashes = rest.substr(0, 2) === '//';
+ if (slashes && !(proto && hostlessProtocol[proto])) {
+ rest = rest.substr(2);
+ this.slashes = true;
+ }
+ }
+
+ if (!hostlessProtocol[proto] &&
+ (slashes || (proto && !slashedProtocol[proto]))) {
+
+ // there's a hostname.
+ // the first instance of /, ?, ;, or # ends the host.
+ //
+ // If there is an @ in the hostname, then non-host chars *are* allowed
+ // to the left of the last @ sign, unless some host-ending character
+ // comes *before* the @-sign.
+ // URLs are obnoxious.
+ //
+ // ex:
+ // http://a@b@c/ => user:a@b host:c
+ // http://a@b?@c => user:a host:c path:/?@c
+
+ // v0.12 TODO(isaacs): This is not quite how Chrome does things.
+ // Review our test case against browsers more comprehensively.
+
+ // find the first instance of any hostEndingChars
+ var hostEnd = -1;
+ for (var i = 0; i < hostEndingChars.length; i++) {
+ var hec = rest.indexOf(hostEndingChars[i]);
+ if (hec !== -1 && (hostEnd === -1 || hec < hostEnd))
+ hostEnd = hec;
+ }
+
+ // at this point, either we have an explicit point where the
+ // auth portion cannot go past, or the last @ char is the decider.
+ var auth, atSign;
+ if (hostEnd === -1) {
+ // atSign can be anywhere.
+ atSign = rest.lastIndexOf('@');
+ } else {
+ // atSign must be in auth portion.
+ // http://a@b/c@d => host:b auth:a path:/c@d
+ atSign = rest.lastIndexOf('@', hostEnd);
+ }
+
+ // Now we have a portion which is definitely the auth.
+ // Pull that off.
+ if (atSign !== -1) {
+ auth = rest.slice(0, atSign);
+ rest = rest.slice(atSign + 1);
+ this.auth = decodeURIComponent(auth);
+ }
+
+ // the host is the remaining to the left of the first non-host char
+ hostEnd = -1;
+ for (var i = 0; i < nonHostChars.length; i++) {
+ var hec = rest.indexOf(nonHostChars[i]);
+ if (hec !== -1 && (hostEnd === -1 || hec < hostEnd))
+ hostEnd = hec;
+ }
+ // if we still have not hit it, then the entire thing is a host.
+ if (hostEnd === -1)
+ hostEnd = rest.length;
+
+ this.host = rest.slice(0, hostEnd);
+ rest = rest.slice(hostEnd);
+
+ // pull out port.
+ this.parseHost();
+
+ // we've indicated that there is a hostname,
+ // so even if it's empty, it has to be present.
+ this.hostname = this.hostname || '';
+
+ // if hostname begins with [ and ends with ]
+ // assume that it's an IPv6 address.
+ var ipv6Hostname = this.hostname[0] === '[' &&
+ this.hostname[this.hostname.length - 1] === ']';
+
+ // validate a little.
+ if (!ipv6Hostname) {
+ var hostparts = this.hostname.split(/\./);
+ for (var i = 0, l = hostparts.length; i < l; i++) {
+ var part = hostparts[i];
+ if (!part) continue;
+ if (!part.match(hostnamePartPattern)) {
+ var newpart = '';
+ for (var j = 0, k = part.length; j < k; j++) {
+ if (part.charCodeAt(j) > 127) {
+ // we replace non-ASCII char with a temporary placeholder
+ // we need this to make sure size of hostname is not
+ // broken by replacing non-ASCII by nothing
+ newpart += 'x';
+ } else {
+ newpart += part[j];
+ }
+ }
+ // we test again with ASCII char only
+ if (!newpart.match(hostnamePartPattern)) {
+ var validParts = hostparts.slice(0, i);
+ var notHost = hostparts.slice(i + 1);
+ var bit = part.match(hostnamePartStart);
+ if (bit) {
+ validParts.push(bit[1]);
+ notHost.unshift(bit[2]);
+ }
+ if (notHost.length) {
+ rest = '/' + notHost.join('.') + rest;
+ }
+ this.hostname = validParts.join('.');
+ break;
+ }
+ }
+ }
+ }
+
+ if (this.hostname.length > hostnameMaxLen) {
+ this.hostname = '';
+ } else {
+ // hostnames are always lower case.
+ this.hostname = this.hostname.toLowerCase();
+ }
+
+ if (!ipv6Hostname) {
+ // IDNA Support: Returns a puny coded representation of "domain".
+ // It only converts the part of the domain name that
+ // has non ASCII characters. I.e. it dosent matter if
+ // you call it with a domain that already is in ASCII.
+ var domainArray = this.hostname.split('.');
+ var newOut = [];
+ for (var i = 0; i < domainArray.length; ++i) {
+ var s = domainArray[i];
+ newOut.push(s.match(/[^A-Za-z0-9_-]/) ?
+ 'xn--' + punycode.encode(s) : s);
+ }
+ this.hostname = newOut.join('.');
+ }
+
+ var p = this.port ? ':' + this.port : '';
+ var h = this.hostname || '';
+ this.host = h + p;
+ this.href += this.host;
+
+ // strip [ and ] from the hostname
+ // the host field still retains them, though
+ if (ipv6Hostname) {
+ this.hostname = this.hostname.substr(1, this.hostname.length - 2);
+ if (rest[0] !== '/') {
+ rest = '/' + rest;
+ }
+ }
+ }
+
+ // now rest is set to the post-host stuff.
+ // chop off any delim chars.
+ if (!unsafeProtocol[lowerProto]) {
+
+ // First, make 100% sure that any "autoEscape" chars get
+ // escaped, even if encodeURIComponent doesn't think they
+ // need to be.
+ for (var i = 0, l = autoEscape.length; i < l; i++) {
+ var ae = autoEscape[i];
+ var esc = encodeURIComponent(ae);
+ if (esc === ae) {
+ esc = escape(ae);
+ }
+ rest = rest.split(ae).join(esc);
+ }
+ }
+
+
+ // chop off from the tail first.
+ var hash = rest.indexOf('#');
+ if (hash !== -1) {
+ // got a fragment string.
+ this.hash = rest.substr(hash);
+ rest = rest.slice(0, hash);
+ }
+ var qm = rest.indexOf('?');
+ if (qm !== -1) {
+ this.search = rest.substr(qm);
+ this.query = rest.substr(qm + 1);
+ if (parseQueryString) {
+ this.query = querystring.parse(this.query);
+ }
+ rest = rest.slice(0, qm);
+ } else if (parseQueryString) {
+ // no query string, but parseQueryString still requested
+ this.search = '';
+ this.query = {};
+ }
+ if (rest) this.pathname = rest;
+ if (slashedProtocol[lowerProto] &&
+ this.hostname && !this.pathname) {
+ this.pathname = '/';
+ }
+
+ //to support http.request
+ if (this.pathname || this.search) {
+ var p = this.pathname || '';
+ var s = this.search || '';
+ this.path = p + s;
+ }
+
+ // finally, reconstruct the href based on what has been validated.
+ this.href = this.format();
+ return this;
+ };
+
+ // format a parsed object into a url string
+ function urlFormat(obj) {
+ // ensure it's an object, and not a string url.
+ // If it's an obj, this is a no-op.
+ // this way, you can call url_format() on strings
+ // to clean up potentially wonky urls.
+ if (isString(obj)) obj = urlParse(obj);
+ if (!(obj instanceof Url)) return Url.prototype.format.call(obj);
+ return obj.format();
+ }
+
+ Url.prototype.format = function() {
+ var auth = this.auth || '';
+ if (auth) {
+ auth = encodeURIComponent(auth);
+ auth = auth.replace(/%3A/i, ':');
+ auth += '@';
+ }
+
+ var protocol = this.protocol || '',
+ pathname = this.pathname || '',
+ hash = this.hash || '',
+ host = false,
+ query = '';
+
+ if (this.host) {
+ host = auth + this.host;
+ } else if (this.hostname) {
+ host = auth + (this.hostname.indexOf(':') === -1 ?
+ this.hostname :
+ '[' + this.hostname + ']');
+ if (this.port) {
+ host += ':' + this.port;
+ }
+ }
+
+ if (this.query &&
+ isObject(this.query) &&
+ Object.keys(this.query).length) {
+ query = querystring.stringify(this.query);
+ }
+
+ var search = this.search || (query && ('?' + query)) || '';
+
+ if (protocol && protocol.substr(-1) !== ':') protocol += ':';
+
+ // only the slashedProtocols get the //. Not mailto:, xmpp:, etc.
+ // unless they had them to begin with.
+ if (this.slashes ||
+ (!protocol || slashedProtocol[protocol]) && host !== false) {
+ host = '//' + (host || '');
+ if (pathname && pathname.charAt(0) !== '/') pathname = '/' + pathname;
+ } else if (!host) {
+ host = '';
+ }
+
+ if (hash && hash.charAt(0) !== '#') hash = '#' + hash;
+ if (search && search.charAt(0) !== '?') search = '?' + search;
+
+ pathname = pathname.replace(/[?#]/g, function(match) {
+ return encodeURIComponent(match);
+ });
+ search = search.replace('#', '%23');
+
+ return protocol + host + pathname + search + hash;
+ };
+
+ function urlResolve(source, relative) {
+ return urlParse(source, false, true).resolve(relative);
+ }
+
+ Url.prototype.resolve = function(relative) {
+ return this.resolveObject(urlParse(relative, false, true)).format();
+ };
+
+ function urlResolveObject(source, relative) {
+ if (!source) return relative;
+ return urlParse(source, false, true).resolveObject(relative);
+ }
+
+ Url.prototype.resolveObject = function(relative) {
+ if (isString(relative)) {
+ var rel = new Url();
+ rel.parse(relative, false, true);
+ relative = rel;
+ }
+
+ var result = new Url();
+ Object.keys(this).forEach(function(k) {
+ result[k] = this[k];
+ }, this);
+
+ // hash is always overridden, no matter what.
+ // even href="" will remove it.
+ result.hash = relative.hash;
+
+ // if the relative url is empty, then there's nothing left to do here.
+ if (relative.href === '') {
+ result.href = result.format();
+ return result;
+ }
+
+ // hrefs like //foo/bar always cut to the protocol.
+ if (relative.slashes && !relative.protocol) {
+ // take everything except the protocol from relative
+ Object.keys(relative).forEach(function(k) {
+ if (k !== 'protocol')
+ result[k] = relative[k];
+ });
+
+ //urlParse appends trailing / to urls like http://www.example.com
+ if (slashedProtocol[result.protocol] &&
+ result.hostname && !result.pathname) {
+ result.path = result.pathname = '/';
+ }
+
+ result.href = result.format();
+ return result;
+ }
+
+ if (relative.protocol && relative.protocol !== result.protocol) {
+ // if it's a known url protocol, then changing
+ // the protocol does weird things
+ // first, if it's not file:, then we MUST have a host,
+ // and if there was a path
+ // to begin with, then we MUST have a path.
+ // if it is file:, then the host is dropped,
+ // because that's known to be hostless.
+ // anything else is assumed to be absolute.
+ if (!slashedProtocol[relative.protocol]) {
+ Object.keys(relative).forEach(function(k) {
+ result[k] = relative[k];
+ });
+ result.href = result.format();
+ return result;
+ }
+
+ result.protocol = relative.protocol;
+ if (!relative.host && !hostlessProtocol[relative.protocol]) {
+ var relPath = (relative.pathname || '').split('/');
+ while (relPath.length && !(relative.host = relPath.shift()));
+ if (!relative.host) relative.host = '';
+ if (!relative.hostname) relative.hostname = '';
+ if (relPath[0] !== '') relPath.unshift('');
+ if (relPath.length < 2) relPath.unshift('');
+ result.pathname = relPath.join('/');
+ } else {
+ result.pathname = relative.pathname;
+ }
+ result.search = relative.search;
+ result.query = relative.query;
+ result.host = relative.host || '';
+ result.auth = relative.auth;
+ result.hostname = relative.hostname || relative.host;
+ result.port = relative.port;
+ // to support http.request
+ if (result.pathname || result.search) {
+ var p = result.pathname || '';
+ var s = result.search || '';
+ result.path = p + s;
+ }
+ result.slashes = result.slashes || relative.slashes;
+ result.href = result.format();
+ return result;
+ }
+
+ var isSourceAbs = (result.pathname && result.pathname.charAt(0) === '/'),
+ isRelAbs = (
+ relative.host ||
+ relative.pathname && relative.pathname.charAt(0) === '/'
+ ),
+ mustEndAbs = (isRelAbs || isSourceAbs ||
+ (result.host && relative.pathname)),
+ removeAllDots = mustEndAbs,
+ srcPath = result.pathname && result.pathname.split('/') || [],
+ relPath = relative.pathname && relative.pathname.split('/') || [],
+ psychotic = result.protocol && !slashedProtocol[result.protocol];
+
+ // if the url is a non-slashed url, then relative
+ // links like ../.. should be able
+ // to crawl up to the hostname, as well. This is strange.
+ // result.protocol has already been set by now.
+ // Later on, put the first path part into the host field.
+ if (psychotic) {
+ result.hostname = '';
+ result.port = null;
+ if (result.host) {
+ if (srcPath[0] === '') srcPath[0] = result.host;
+ else srcPath.unshift(result.host);
+ }
+ result.host = '';
+ if (relative.protocol) {
+ relative.hostname = null;
+ relative.port = null;
+ if (relative.host) {
+ if (relPath[0] === '') relPath[0] = relative.host;
+ else relPath.unshift(relative.host);
+ }
+ relative.host = null;
+ }
+ mustEndAbs = mustEndAbs && (relPath[0] === '' || srcPath[0] === '');
+ }
+
+ if (isRelAbs) {
+ // it's absolute.
+ result.host = (relative.host || relative.host === '') ?
+ relative.host : result.host;
+ result.hostname = (relative.hostname || relative.hostname === '') ?
+ relative.hostname : result.hostname;
+ result.search = relative.search;
+ result.query = relative.query;
+ srcPath = relPath;
+ // fall through to the dot-handling below.
+ } else if (relPath.length) {
+ // it's relative
+ // throw away the existing file, and take the new path instead.
+ if (!srcPath) srcPath = [];
+ srcPath.pop();
+ srcPath = srcPath.concat(relPath);
+ result.search = relative.search;
+ result.query = relative.query;
+ } else if (!isNullOrUndefined(relative.search)) {
+ // just pull out the search.
+ // like href='?foo'.
+ // Put this after the other two cases because it simplifies the booleans
+ if (psychotic) {
+ result.hostname = result.host = srcPath.shift();
+ //occationaly the auth can get stuck only in host
+ //this especialy happens in cases like
+ //url.resolveObject('mailto:local1@domain1', 'local2@domain2')
+ var authInHost = result.host && result.host.indexOf('@') > 0 ?
+ result.host.split('@') : false;
+ if (authInHost) {
+ result.auth = authInHost.shift();
+ result.host = result.hostname = authInHost.shift();
+ }
+ }
+ result.search = relative.search;
+ result.query = relative.query;
+ //to support http.request
+ if (!isNull(result.pathname) || !isNull(result.search)) {
+ result.path = (result.pathname ? result.pathname : '') +
+ (result.search ? result.search : '');
+ }
+ result.href = result.format();
+ return result;
+ }
+
+ if (!srcPath.length) {
+ // no path at all. easy.
+ // we've already handled the other stuff above.
+ result.pathname = null;
+ //to support http.request
+ if (result.search) {
+ result.path = '/' + result.search;
+ } else {
+ result.path = null;
+ }
+ result.href = result.format();
+ return result;
+ }
+
+ // if a url ENDs in . or .., then it must get a trailing slash.
+ // however, if it ends in anything else non-slashy,
+ // then it must NOT get a trailing slash.
+ var last = srcPath.slice(-1)[0];
+ var hasTrailingSlash = (
+ (result.host || relative.host) && (last === '.' || last === '..') ||
+ last === '');
+
+ // strip single dots, resolve double dots to parent dir
+ // if the path tries to go above the root, `up` ends up > 0
+ var up = 0;
+ for (var i = srcPath.length; i >= 0; i--) {
+ last = srcPath[i];
+ if (last == '.') {
+ srcPath.splice(i, 1);
+ } else if (last === '..') {
+ srcPath.splice(i, 1);
+ up++;
+ } else if (up) {
+ srcPath.splice(i, 1);
+ up--;
+ }
+ }
+
+ // if the path is allowed to go above the root, restore leading ..s
+ if (!mustEndAbs && !removeAllDots) {
+ for (; up--; up) {
+ srcPath.unshift('..');
+ }
+ }
+
+ if (mustEndAbs && srcPath[0] !== '' &&
+ (!srcPath[0] || srcPath[0].charAt(0) !== '/')) {
+ srcPath.unshift('');
+ }
+
+ if (hasTrailingSlash && (srcPath.join('/').substr(-1) !== '/')) {
+ srcPath.push('');
+ }
+
+ var isAbsolute = srcPath[0] === '' ||
+ (srcPath[0] && srcPath[0].charAt(0) === '/');
+
+ // put the host back
+ if (psychotic) {
+ result.hostname = result.host = isAbsolute ? '' :
+ srcPath.length ? srcPath.shift() : '';
+ //occationaly the auth can get stuck only in host
+ //this especialy happens in cases like
+ //url.resolveObject('mailto:local1@domain1', 'local2@domain2')
+ var authInHost = result.host && result.host.indexOf('@') > 0 ?
+ result.host.split('@') : false;
+ if (authInHost) {
+ result.auth = authInHost.shift();
+ result.host = result.hostname = authInHost.shift();
+ }
+ }
+
+ mustEndAbs = mustEndAbs || (result.host && srcPath.length);
+
+ if (mustEndAbs && !isAbsolute) {
+ srcPath.unshift('');
+ }
+
+ if (!srcPath.length) {
+ result.pathname = null;
+ result.path = null;
+ } else {
+ result.pathname = srcPath.join('/');
+ }
+
+ //to support request.http
+ if (!isNull(result.pathname) || !isNull(result.search)) {
+ result.path = (result.pathname ? result.pathname : '') +
+ (result.search ? result.search : '');
+ }
+ result.auth = relative.auth || result.auth;
+ result.slashes = result.slashes || relative.slashes;
+ result.href = result.format();
+ return result;
+ };
+
+ Url.prototype.parseHost = function() {
+ var host = this.host;
+ var port = portPattern.exec(host);
+ if (port) {
+ port = port[0];
+ if (port !== ':') {
+ this.port = port.substr(1);
+ }
+ host = host.substr(0, host.length - port.length);
+ }
+ if (host) this.hostname = host;
+ };
+
+ function isString(arg) {
+ return typeof arg === "string";
+ }
+
+ function isObject(arg) {
+ return typeof arg === 'object' && arg !== null;
+ }
+
+ function isNull(arg) {
+ return arg === null;
+ }
+ function isNullOrUndefined(arg) {
+ return arg == null;
+ }
+
+
+/***/ },
+
+/***/ 294:
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_RESULT__;/* WEBPACK VAR INJECTION */(function(module, global) {/*! https://mths.be/punycode v1.3.2 by @mathias */
+ ;(function(root) {
+
+ /** Detect free variables */
+ var freeExports = typeof exports == 'object' && exports &&
+ !exports.nodeType && exports;
+ var freeModule = typeof module == 'object' && module &&
+ !module.nodeType && module;
+ var freeGlobal = typeof global == 'object' && global;
+ if (
+ freeGlobal.global === freeGlobal ||
+ freeGlobal.window === freeGlobal ||
+ freeGlobal.self === freeGlobal
+ ) {
+ root = freeGlobal;
+ }
+
+ /**
+ * The `punycode` object.
+ * @name punycode
+ * @type Object
+ */
+ var punycode,
+
+ /** Highest positive signed 32-bit float value */
+ maxInt = 2147483647, // aka. 0x7FFFFFFF or 2^31-1
+
+ /** Bootstring parameters */
+ base = 36,
+ tMin = 1,
+ tMax = 26,
+ skew = 38,
+ damp = 700,
+ initialBias = 72,
+ initialN = 128, // 0x80
+ delimiter = '-', // '\x2D'
+
+ /** Regular expressions */
+ regexPunycode = /^xn--/,
+ regexNonASCII = /[^\x20-\x7E]/, // unprintable ASCII chars + non-ASCII chars
+ regexSeparators = /[\x2E\u3002\uFF0E\uFF61]/g, // RFC 3490 separators
+
+ /** Error messages */
+ errors = {
+ 'overflow': 'Overflow: input needs wider integers to process',
+ 'not-basic': 'Illegal input >= 0x80 (not a basic code point)',
+ 'invalid-input': 'Invalid input'
+ },
+
+ /** Convenience shortcuts */
+ baseMinusTMin = base - tMin,
+ floor = Math.floor,
+ stringFromCharCode = String.fromCharCode,
+
+ /** Temporary variable */
+ key;
+
+ /*--------------------------------------------------------------------------*/
+
+ /**
+ * A generic error utility function.
+ * @private
+ * @param {String} type The error type.
+ * @returns {Error} Throws a `RangeError` with the applicable error message.
+ */
+ function error(type) {
+ throw RangeError(errors[type]);
+ }
+
+ /**
+ * A generic `Array#map` utility function.
+ * @private
+ * @param {Array} array The array to iterate over.
+ * @param {Function} callback The function that gets called for every array
+ * item.
+ * @returns {Array} A new array of values returned by the callback function.
+ */
+ function map(array, fn) {
+ var length = array.length;
+ var result = [];
+ while (length--) {
+ result[length] = fn(array[length]);
+ }
+ return result;
+ }
+
+ /**
+ * A simple `Array#map`-like wrapper to work with domain name strings or email
+ * addresses.
+ * @private
+ * @param {String} domain The domain name or email address.
+ * @param {Function} callback The function that gets called for every
+ * character.
+ * @returns {Array} A new string of characters returned by the callback
+ * function.
+ */
+ function mapDomain(string, fn) {
+ var parts = string.split('@');
+ var result = '';
+ if (parts.length > 1) {
+ // In email addresses, only the domain name should be punycoded. Leave
+ // the local part (i.e. everything up to `@`) intact.
+ result = parts[0] + '@';
+ string = parts[1];
+ }
+ // Avoid `split(regex)` for IE8 compatibility. See #17.
+ string = string.replace(regexSeparators, '\x2E');
+ var labels = string.split('.');
+ var encoded = map(labels, fn).join('.');
+ return result + encoded;
+ }
+
+ /**
+ * Creates an array containing the numeric code points of each Unicode
+ * character in the string. While JavaScript uses UCS-2 internally,
+ * this function will convert a pair of surrogate halves (each of which
+ * UCS-2 exposes as separate characters) into a single code point,
+ * matching UTF-16.
+ * @see `punycode.ucs2.encode`
+ * @see <https://mathiasbynens.be/notes/javascript-encoding>
+ * @memberOf punycode.ucs2
+ * @name decode
+ * @param {String} string The Unicode input string (UCS-2).
+ * @returns {Array} The new array of code points.
+ */
+ function ucs2decode(string) {
+ var output = [],
+ counter = 0,
+ length = string.length,
+ value,
+ extra;
+ while (counter < length) {
+ value = string.charCodeAt(counter++);
+ if (value >= 0xD800 && value <= 0xDBFF && counter < length) {
+ // high surrogate, and there is a next character
+ extra = string.charCodeAt(counter++);
+ if ((extra & 0xFC00) == 0xDC00) { // low surrogate
+ output.push(((value & 0x3FF) << 10) + (extra & 0x3FF) + 0x10000);
+ } else {
+ // unmatched surrogate; only append this code unit, in case the next
+ // code unit is the high surrogate of a surrogate pair
+ output.push(value);
+ counter--;
+ }
+ } else {
+ output.push(value);
+ }
+ }
+ return output;
+ }
+
+ /**
+ * Creates a string based on an array of numeric code points.
+ * @see `punycode.ucs2.decode`
+ * @memberOf punycode.ucs2
+ * @name encode
+ * @param {Array} codePoints The array of numeric code points.
+ * @returns {String} The new Unicode string (UCS-2).
+ */
+ function ucs2encode(array) {
+ return map(array, function(value) {
+ var output = '';
+ if (value > 0xFFFF) {
+ value -= 0x10000;
+ output += stringFromCharCode(value >>> 10 & 0x3FF | 0xD800);
+ value = 0xDC00 | value & 0x3FF;
+ }
+ output += stringFromCharCode(value);
+ return output;
+ }).join('');
+ }
+
+ /**
+ * Converts a basic code point into a digit/integer.
+ * @see `digitToBasic()`
+ * @private
+ * @param {Number} codePoint The basic numeric code point value.
+ * @returns {Number} The numeric value of a basic code point (for use in
+ * representing integers) in the range `0` to `base - 1`, or `base` if
+ * the code point does not represent a value.
+ */
+ function basicToDigit(codePoint) {
+ if (codePoint - 48 < 10) {
+ return codePoint - 22;
+ }
+ if (codePoint - 65 < 26) {
+ return codePoint - 65;
+ }
+ if (codePoint - 97 < 26) {
+ return codePoint - 97;
+ }
+ return base;
+ }
+
+ /**
+ * Converts a digit/integer into a basic code point.
+ * @see `basicToDigit()`
+ * @private
+ * @param {Number} digit The numeric value of a basic code point.
+ * @returns {Number} The basic code point whose value (when used for
+ * representing integers) is `digit`, which needs to be in the range
+ * `0` to `base - 1`. If `flag` is non-zero, the uppercase form is
+ * used; else, the lowercase form is used. The behavior is undefined
+ * if `flag` is non-zero and `digit` has no uppercase form.
+ */
+ function digitToBasic(digit, flag) {
+ // 0..25 map to ASCII a..z or A..Z
+ // 26..35 map to ASCII 0..9
+ return digit + 22 + 75 * (digit < 26) - ((flag != 0) << 5);
+ }
+
+ /**
+ * Bias adaptation function as per section 3.4 of RFC 3492.
+ * http://tools.ietf.org/html/rfc3492#section-3.4
+ * @private
+ */
+ function adapt(delta, numPoints, firstTime) {
+ var k = 0;
+ delta = firstTime ? floor(delta / damp) : delta >> 1;
+ delta += floor(delta / numPoints);
+ for (/* no initialization */; delta > baseMinusTMin * tMax >> 1; k += base) {
+ delta = floor(delta / baseMinusTMin);
+ }
+ return floor(k + (baseMinusTMin + 1) * delta / (delta + skew));
+ }
+
+ /**
+ * Converts a Punycode string of ASCII-only symbols to a string of Unicode
+ * symbols.
+ * @memberOf punycode
+ * @param {String} input The Punycode string of ASCII-only symbols.
+ * @returns {String} The resulting string of Unicode symbols.
+ */
+ function decode(input) {
+ // Don't use UCS-2
+ var output = [],
+ inputLength = input.length,
+ out,
+ i = 0,
+ n = initialN,
+ bias = initialBias,
+ basic,
+ j,
+ index,
+ oldi,
+ w,
+ k,
+ digit,
+ t,
+ /** Cached calculation results */
+ baseMinusT;
+
+ // Handle the basic code points: let `basic` be the number of input code
+ // points before the last delimiter, or `0` if there is none, then copy
+ // the first basic code points to the output.
+
+ basic = input.lastIndexOf(delimiter);
+ if (basic < 0) {
+ basic = 0;
+ }
+
+ for (j = 0; j < basic; ++j) {
+ // if it's not a basic code point
+ if (input.charCodeAt(j) >= 0x80) {
+ error('not-basic');
+ }
+ output.push(input.charCodeAt(j));
+ }
+
+ // Main decoding loop: start just after the last delimiter if any basic code
+ // points were copied; start at the beginning otherwise.
+
+ for (index = basic > 0 ? basic + 1 : 0; index < inputLength; /* no final expression */) {
+
+ // `index` is the index of the next character to be consumed.
+ // Decode a generalized variable-length integer into `delta`,
+ // which gets added to `i`. The overflow checking is easier
+ // if we increase `i` as we go, then subtract off its starting
+ // value at the end to obtain `delta`.
+ for (oldi = i, w = 1, k = base; /* no condition */; k += base) {
+
+ if (index >= inputLength) {
+ error('invalid-input');
+ }
+
+ digit = basicToDigit(input.charCodeAt(index++));
+
+ if (digit >= base || digit > floor((maxInt - i) / w)) {
+ error('overflow');
+ }
+
+ i += digit * w;
+ t = k <= bias ? tMin : (k >= bias + tMax ? tMax : k - bias);
+
+ if (digit < t) {
+ break;
+ }
+
+ baseMinusT = base - t;
+ if (w > floor(maxInt / baseMinusT)) {
+ error('overflow');
+ }
+
+ w *= baseMinusT;
+
+ }
+
+ out = output.length + 1;
+ bias = adapt(i - oldi, out, oldi == 0);
+
+ // `i` was supposed to wrap around from `out` to `0`,
+ // incrementing `n` each time, so we'll fix that now:
+ if (floor(i / out) > maxInt - n) {
+ error('overflow');
+ }
+
+ n += floor(i / out);
+ i %= out;
+
+ // Insert `n` at position `i` of the output
+ output.splice(i++, 0, n);
+
+ }
+
+ return ucs2encode(output);
+ }
+
+ /**
+ * Converts a string of Unicode symbols (e.g. a domain name label) to a
+ * Punycode string of ASCII-only symbols.
+ * @memberOf punycode
+ * @param {String} input The string of Unicode symbols.
+ * @returns {String} The resulting Punycode string of ASCII-only symbols.
+ */
+ function encode(input) {
+ var n,
+ delta,
+ handledCPCount,
+ basicLength,
+ bias,
+ j,
+ m,
+ q,
+ k,
+ t,
+ currentValue,
+ output = [],
+ /** `inputLength` will hold the number of code points in `input`. */
+ inputLength,
+ /** Cached calculation results */
+ handledCPCountPlusOne,
+ baseMinusT,
+ qMinusT;
+
+ // Convert the input in UCS-2 to Unicode
+ input = ucs2decode(input);
+
+ // Cache the length
+ inputLength = input.length;
+
+ // Initialize the state
+ n = initialN;
+ delta = 0;
+ bias = initialBias;
+
+ // Handle the basic code points
+ for (j = 0; j < inputLength; ++j) {
+ currentValue = input[j];
+ if (currentValue < 0x80) {
+ output.push(stringFromCharCode(currentValue));
+ }
+ }
+
+ handledCPCount = basicLength = output.length;
+
+ // `handledCPCount` is the number of code points that have been handled;
+ // `basicLength` is the number of basic code points.
+
+ // Finish the basic string - if it is not empty - with a delimiter
+ if (basicLength) {
+ output.push(delimiter);
+ }
+
+ // Main encoding loop:
+ while (handledCPCount < inputLength) {
+
+ // All non-basic code points < n have been handled already. Find the next
+ // larger one:
+ for (m = maxInt, j = 0; j < inputLength; ++j) {
+ currentValue = input[j];
+ if (currentValue >= n && currentValue < m) {
+ m = currentValue;
+ }
+ }
+
+ // Increase `delta` enough to advance the decoder's <n,i> state to <m,0>,
+ // but guard against overflow
+ handledCPCountPlusOne = handledCPCount + 1;
+ if (m - n > floor((maxInt - delta) / handledCPCountPlusOne)) {
+ error('overflow');
+ }
+
+ delta += (m - n) * handledCPCountPlusOne;
+ n = m;
+
+ for (j = 0; j < inputLength; ++j) {
+ currentValue = input[j];
+
+ if (currentValue < n && ++delta > maxInt) {
+ error('overflow');
+ }
+
+ if (currentValue == n) {
+ // Represent delta as a generalized variable-length integer
+ for (q = delta, k = base; /* no condition */; k += base) {
+ t = k <= bias ? tMin : (k >= bias + tMax ? tMax : k - bias);
+ if (q < t) {
+ break;
+ }
+ qMinusT = q - t;
+ baseMinusT = base - t;
+ output.push(
+ stringFromCharCode(digitToBasic(t + qMinusT % baseMinusT, 0))
+ );
+ q = floor(qMinusT / baseMinusT);
+ }
+
+ output.push(stringFromCharCode(digitToBasic(q, 0)));
+ bias = adapt(delta, handledCPCountPlusOne, handledCPCount == basicLength);
+ delta = 0;
+ ++handledCPCount;
+ }
+ }
+
+ ++delta;
+ ++n;
+
+ }
+ return output.join('');
+ }
+
+ /**
+ * Converts a Punycode string representing a domain name or an email address
+ * to Unicode. Only the Punycoded parts of the input will be converted, i.e.
+ * it doesn't matter if you call it on a string that has already been
+ * converted to Unicode.
+ * @memberOf punycode
+ * @param {String} input The Punycoded domain name or email address to
+ * convert to Unicode.
+ * @returns {String} The Unicode representation of the given Punycode
+ * string.
+ */
+ function toUnicode(input) {
+ return mapDomain(input, function(string) {
+ return regexPunycode.test(string)
+ ? decode(string.slice(4).toLowerCase())
+ : string;
+ });
+ }
+
+ /**
+ * Converts a Unicode string representing a domain name or an email address to
+ * Punycode. Only the non-ASCII parts of the domain name will be converted,
+ * i.e. it doesn't matter if you call it with a domain that's already in
+ * ASCII.
+ * @memberOf punycode
+ * @param {String} input The domain name or email address to convert, as a
+ * Unicode string.
+ * @returns {String} The Punycode representation of the given domain name or
+ * email address.
+ */
+ function toASCII(input) {
+ return mapDomain(input, function(string) {
+ return regexNonASCII.test(string)
+ ? 'xn--' + encode(string)
+ : string;
+ });
+ }
+
+ /*--------------------------------------------------------------------------*/
+
+ /** Define the public API */
+ punycode = {
+ /**
+ * A string representing the current Punycode.js version number.
+ * @memberOf punycode
+ * @type String
+ */
+ 'version': '1.3.2',
+ /**
+ * An object of methods to convert from JavaScript's internal character
+ * representation (UCS-2) to Unicode code points, and back.
+ * @see <https://mathiasbynens.be/notes/javascript-encoding>
+ * @memberOf punycode
+ * @type Object
+ */
+ 'ucs2': {
+ 'decode': ucs2decode,
+ 'encode': ucs2encode
+ },
+ 'decode': decode,
+ 'encode': encode,
+ 'toASCII': toASCII,
+ 'toUnicode': toUnicode
+ };
+
+ /** Expose `punycode` */
+ // Some AMD build optimizers, like r.js, check for specific condition patterns
+ // like the following:
+ if (
+ true
+ ) {
+ !(__WEBPACK_AMD_DEFINE_RESULT__ = function() {
+ return punycode;
+ }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+ } else if (freeExports && freeModule) {
+ if (module.exports == freeExports) { // in Node.js or RingoJS v0.8.0+
+ freeModule.exports = punycode;
+ } else { // in Narwhal or RingoJS v0.7.0-
+ for (key in punycode) {
+ punycode.hasOwnProperty(key) && (freeExports[key] = punycode[key]);
+ }
+ }
+ } else { // in Rhino or a web browser
+ root.punycode = punycode;
+ }
+
+ }(this));
+
+ /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(77)(module), (function() { return this; }())))
+
+/***/ },
+
+/***/ 295:
+/***/ function(module, exports, __webpack_require__) {
+
+ 'use strict';
+
+ exports.decode = exports.parse = __webpack_require__(296);
+ exports.encode = exports.stringify = __webpack_require__(297);
+
+
+/***/ },
+
+/***/ 296:
+/***/ function(module, exports) {
+
+ // Copyright Joyent, Inc. and other Node contributors.
+ //
+ // Permission is hereby granted, free of charge, to any person obtaining a
+ // copy of this software and associated documentation files (the
+ // "Software"), to deal in the Software without restriction, including
+ // without limitation the rights to use, copy, modify, merge, publish,
+ // distribute, sublicense, and/or sell copies of the Software, and to permit
+ // persons to whom the Software is furnished to do so, subject to the
+ // following conditions:
+ //
+ // The above copyright notice and this permission notice shall be included
+ // in all copies or substantial portions of the Software.
+ //
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+ // NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+ // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+ // USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+ 'use strict';
+
+ // If obj.hasOwnProperty has been overridden, then calling
+ // obj.hasOwnProperty(prop) will break.
+ // See: https://github.com/joyent/node/issues/1707
+ function hasOwnProperty(obj, prop) {
+ return Object.prototype.hasOwnProperty.call(obj, prop);
+ }
+
+ module.exports = function(qs, sep, eq, options) {
+ sep = sep || '&';
+ eq = eq || '=';
+ var obj = {};
+
+ if (typeof qs !== 'string' || qs.length === 0) {
+ return obj;
+ }
+
+ var regexp = /\+/g;
+ qs = qs.split(sep);
+
+ var maxKeys = 1000;
+ if (options && typeof options.maxKeys === 'number') {
+ maxKeys = options.maxKeys;
+ }
+
+ var len = qs.length;
+ // maxKeys <= 0 means that we should not limit keys count
+ if (maxKeys > 0 && len > maxKeys) {
+ len = maxKeys;
+ }
+
+ for (var i = 0; i < len; ++i) {
+ var x = qs[i].replace(regexp, '%20'),
+ idx = x.indexOf(eq),
+ kstr, vstr, k, v;
+
+ if (idx >= 0) {
+ kstr = x.substr(0, idx);
+ vstr = x.substr(idx + 1);
+ } else {
+ kstr = x;
+ vstr = '';
+ }
+
+ k = decodeURIComponent(kstr);
+ v = decodeURIComponent(vstr);
+
+ if (!hasOwnProperty(obj, k)) {
+ obj[k] = v;
+ } else if (Array.isArray(obj[k])) {
+ obj[k].push(v);
+ } else {
+ obj[k] = [obj[k], v];
+ }
+ }
+
+ return obj;
+ };
+
+
+/***/ },
+
+/***/ 297:
+/***/ function(module, exports) {
+
+ // Copyright Joyent, Inc. and other Node contributors.
+ //
+ // Permission is hereby granted, free of charge, to any person obtaining a
+ // copy of this software and associated documentation files (the
+ // "Software"), to deal in the Software without restriction, including
+ // without limitation the rights to use, copy, modify, merge, publish,
+ // distribute, sublicense, and/or sell copies of the Software, and to permit
+ // persons to whom the Software is furnished to do so, subject to the
+ // following conditions:
+ //
+ // The above copyright notice and this permission notice shall be included
+ // in all copies or substantial portions of the Software.
+ //
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+ // NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+ // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+ // USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+ 'use strict';
+
+ var stringifyPrimitive = function(v) {
+ switch (typeof v) {
+ case 'string':
+ return v;
+
+ case 'boolean':
+ return v ? 'true' : 'false';
+
+ case 'number':
+ return isFinite(v) ? v : '';
+
+ default:
+ return '';
+ }
+ };
+
+ module.exports = function(obj, sep, eq, name) {
+ sep = sep || '&';
+ eq = eq || '=';
+ if (obj === null) {
+ obj = undefined;
+ }
+
+ if (typeof obj === 'object') {
+ return Object.keys(obj).map(function(k) {
+ var ks = encodeURIComponent(stringifyPrimitive(k)) + eq;
+ if (Array.isArray(obj[k])) {
+ return obj[k].map(function(v) {
+ return ks + encodeURIComponent(stringifyPrimitive(v));
+ }).join(sep);
+ } else {
+ return ks + encodeURIComponent(stringifyPrimitive(obj[k]));
+ }
+ }).join(sep);
+
+ }
+
+ if (!name) return '';
+ return encodeURIComponent(stringifyPrimitive(name)) + eq +
+ encodeURIComponent(stringifyPrimitive(obj));
+ };
+
+
+/***/ },
+
+/***/ 472:
+/***/ function(module, exports, __webpack_require__) {
+
+ /*
+ * Copyright 2009-2011 Mozilla Foundation and contributors
+ * Licensed under the New BSD license. See LICENSE.txt or:
+ * http://opensource.org/licenses/BSD-3-Clause
+ */
+ exports.SourceMapGenerator = __webpack_require__(473).SourceMapGenerator;
+ exports.SourceMapConsumer = __webpack_require__(479).SourceMapConsumer;
+ exports.SourceNode = __webpack_require__(482).SourceNode;
+
+
+/***/ },
+
+/***/ 473:
+/***/ function(module, exports, __webpack_require__) {
+
+ /* -*- Mode: js; js-indent-level: 2; -*- */
+ /*
+ * Copyright 2011 Mozilla Foundation and contributors
+ * Licensed under the New BSD license. See LICENSE or:
+ * http://opensource.org/licenses/BSD-3-Clause
+ */
+
+ var base64VLQ = __webpack_require__(474);
+ var util = __webpack_require__(476);
+ var ArraySet = __webpack_require__(477).ArraySet;
+ var MappingList = __webpack_require__(478).MappingList;
+
+ /**
+ * An instance of the SourceMapGenerator represents a source map which is
+ * being built incrementally. You may pass an object with the following
+ * properties:
+ *
+ * - file: The filename of the generated source.
+ * - sourceRoot: A root for all relative URLs in this source map.
+ */
+ function SourceMapGenerator(aArgs) {
+ if (!aArgs) {
+ aArgs = {};
+ }
+ this._file = util.getArg(aArgs, 'file', null);
+ this._sourceRoot = util.getArg(aArgs, 'sourceRoot', null);
+ this._skipValidation = util.getArg(aArgs, 'skipValidation', false);
+ this._sources = new ArraySet();
+ this._names = new ArraySet();
+ this._mappings = new MappingList();
+ this._sourcesContents = null;
+ }
+
+ SourceMapGenerator.prototype._version = 3;
+
+ /**
+ * Creates a new SourceMapGenerator based on a SourceMapConsumer
+ *
+ * @param aSourceMapConsumer The SourceMap.
+ */
+ SourceMapGenerator.fromSourceMap =
+ function SourceMapGenerator_fromSourceMap(aSourceMapConsumer) {
+ var sourceRoot = aSourceMapConsumer.sourceRoot;
+ var generator = new SourceMapGenerator({
+ file: aSourceMapConsumer.file,
+ sourceRoot: sourceRoot
+ });
+ aSourceMapConsumer.eachMapping(function (mapping) {
+ var newMapping = {
+ generated: {
+ line: mapping.generatedLine,
+ column: mapping.generatedColumn
+ }
+ };
+
+ if (mapping.source != null) {
+ newMapping.source = mapping.source;
+ if (sourceRoot != null) {
+ newMapping.source = util.relative(sourceRoot, newMapping.source);
+ }
+
+ newMapping.original = {
+ line: mapping.originalLine,
+ column: mapping.originalColumn
+ };
+
+ if (mapping.name != null) {
+ newMapping.name = mapping.name;
+ }
+ }
+
+ generator.addMapping(newMapping);
+ });
+ aSourceMapConsumer.sources.forEach(function (sourceFile) {
+ var content = aSourceMapConsumer.sourceContentFor(sourceFile);
+ if (content != null) {
+ generator.setSourceContent(sourceFile, content);
+ }
+ });
+ return generator;
+ };
+
+ /**
+ * Add a single mapping from original source line and column to the generated
+ * source's line and column for this source map being created. The mapping
+ * object should have the following properties:
+ *
+ * - generated: An object with the generated line and column positions.
+ * - original: An object with the original line and column positions.
+ * - source: The original source file (relative to the sourceRoot).
+ * - name: An optional original token name for this mapping.
+ */
+ SourceMapGenerator.prototype.addMapping =
+ function SourceMapGenerator_addMapping(aArgs) {
+ var generated = util.getArg(aArgs, 'generated');
+ var original = util.getArg(aArgs, 'original', null);
+ var source = util.getArg(aArgs, 'source', null);
+ var name = util.getArg(aArgs, 'name', null);
+
+ if (!this._skipValidation) {
+ this._validateMapping(generated, original, source, name);
+ }
+
+ if (source != null) {
+ source = String(source);
+ if (!this._sources.has(source)) {
+ this._sources.add(source);
+ }
+ }
+
+ if (name != null) {
+ name = String(name);
+ if (!this._names.has(name)) {
+ this._names.add(name);
+ }
+ }
+
+ this._mappings.add({
+ generatedLine: generated.line,
+ generatedColumn: generated.column,
+ originalLine: original != null && original.line,
+ originalColumn: original != null && original.column,
+ source: source,
+ name: name
+ });
+ };
+
+ /**
+ * Set the source content for a source file.
+ */
+ SourceMapGenerator.prototype.setSourceContent =
+ function SourceMapGenerator_setSourceContent(aSourceFile, aSourceContent) {
+ var source = aSourceFile;
+ if (this._sourceRoot != null) {
+ source = util.relative(this._sourceRoot, source);
+ }
+
+ if (aSourceContent != null) {
+ // Add the source content to the _sourcesContents map.
+ // Create a new _sourcesContents map if the property is null.
+ if (!this._sourcesContents) {
+ this._sourcesContents = Object.create(null);
+ }
+ this._sourcesContents[util.toSetString(source)] = aSourceContent;
+ } else if (this._sourcesContents) {
+ // Remove the source file from the _sourcesContents map.
+ // If the _sourcesContents map is empty, set the property to null.
+ delete this._sourcesContents[util.toSetString(source)];
+ if (Object.keys(this._sourcesContents).length === 0) {
+ this._sourcesContents = null;
+ }
+ }
+ };
+
+ /**
+ * Applies the mappings of a sub-source-map for a specific source file to the
+ * source map being generated. Each mapping to the supplied source file is
+ * rewritten using the supplied source map. Note: The resolution for the
+ * resulting mappings is the minimium of this map and the supplied map.
+ *
+ * @param aSourceMapConsumer The source map to be applied.
+ * @param aSourceFile Optional. The filename of the source file.
+ * If omitted, SourceMapConsumer's file property will be used.
+ * @param aSourceMapPath Optional. The dirname of the path to the source map
+ * to be applied. If relative, it is relative to the SourceMapConsumer.
+ * This parameter is needed when the two source maps aren't in the same
+ * directory, and the source map to be applied contains relative source
+ * paths. If so, those relative source paths need to be rewritten
+ * relative to the SourceMapGenerator.
+ */
+ SourceMapGenerator.prototype.applySourceMap =
+ function SourceMapGenerator_applySourceMap(aSourceMapConsumer, aSourceFile, aSourceMapPath) {
+ var sourceFile = aSourceFile;
+ // If aSourceFile is omitted, we will use the file property of the SourceMap
+ if (aSourceFile == null) {
+ if (aSourceMapConsumer.file == null) {
+ throw new Error(
+ 'SourceMapGenerator.prototype.applySourceMap requires either an explicit source file, ' +
+ 'or the source map\'s "file" property. Both were omitted.'
+ );
+ }
+ sourceFile = aSourceMapConsumer.file;
+ }
+ var sourceRoot = this._sourceRoot;
+ // Make "sourceFile" relative if an absolute Url is passed.
+ if (sourceRoot != null) {
+ sourceFile = util.relative(sourceRoot, sourceFile);
+ }
+ // Applying the SourceMap can add and remove items from the sources and
+ // the names array.
+ var newSources = new ArraySet();
+ var newNames = new ArraySet();
+
+ // Find mappings for the "sourceFile"
+ this._mappings.unsortedForEach(function (mapping) {
+ if (mapping.source === sourceFile && mapping.originalLine != null) {
+ // Check if it can be mapped by the source map, then update the mapping.
+ var original = aSourceMapConsumer.originalPositionFor({
+ line: mapping.originalLine,
+ column: mapping.originalColumn
+ });
+ if (original.source != null) {
+ // Copy mapping
+ mapping.source = original.source;
+ if (aSourceMapPath != null) {
+ mapping.source = util.join(aSourceMapPath, mapping.source)
+ }
+ if (sourceRoot != null) {
+ mapping.source = util.relative(sourceRoot, mapping.source);
+ }
+ mapping.originalLine = original.line;
+ mapping.originalColumn = original.column;
+ if (original.name != null) {
+ mapping.name = original.name;
+ }
+ }
+ }
+
+ var source = mapping.source;
+ if (source != null && !newSources.has(source)) {
+ newSources.add(source);
+ }
+
+ var name = mapping.name;
+ if (name != null && !newNames.has(name)) {
+ newNames.add(name);
+ }
+
+ }, this);
+ this._sources = newSources;
+ this._names = newNames;
+
+ // Copy sourcesContents of applied map.
+ aSourceMapConsumer.sources.forEach(function (sourceFile) {
+ var content = aSourceMapConsumer.sourceContentFor(sourceFile);
+ if (content != null) {
+ if (aSourceMapPath != null) {
+ sourceFile = util.join(aSourceMapPath, sourceFile);
+ }
+ if (sourceRoot != null) {
+ sourceFile = util.relative(sourceRoot, sourceFile);
+ }
+ this.setSourceContent(sourceFile, content);
+ }
+ }, this);
+ };
+
+ /**
+ * A mapping can have one of the three levels of data:
+ *
+ * 1. Just the generated position.
+ * 2. The Generated position, original position, and original source.
+ * 3. Generated and original position, original source, as well as a name
+ * token.
+ *
+ * To maintain consistency, we validate that any new mapping being added falls
+ * in to one of these categories.
+ */
+ SourceMapGenerator.prototype._validateMapping =
+ function SourceMapGenerator_validateMapping(aGenerated, aOriginal, aSource,
+ aName) {
+ if (aGenerated && 'line' in aGenerated && 'column' in aGenerated
+ && aGenerated.line > 0 && aGenerated.column >= 0
+ && !aOriginal && !aSource && !aName) {
+ // Case 1.
+ return;
+ }
+ else if (aGenerated && 'line' in aGenerated && 'column' in aGenerated
+ && aOriginal && 'line' in aOriginal && 'column' in aOriginal
+ && aGenerated.line > 0 && aGenerated.column >= 0
+ && aOriginal.line > 0 && aOriginal.column >= 0
+ && aSource) {
+ // Cases 2 and 3.
+ return;
+ }
+ else {
+ throw new Error('Invalid mapping: ' + JSON.stringify({
+ generated: aGenerated,
+ source: aSource,
+ original: aOriginal,
+ name: aName
+ }));
+ }
+ };
+
+ /**
+ * Serialize the accumulated mappings in to the stream of base 64 VLQs
+ * specified by the source map format.
+ */
+ SourceMapGenerator.prototype._serializeMappings =
+ function SourceMapGenerator_serializeMappings() {
+ var previousGeneratedColumn = 0;
+ var previousGeneratedLine = 1;
+ var previousOriginalColumn = 0;
+ var previousOriginalLine = 0;
+ var previousName = 0;
+ var previousSource = 0;
+ var result = '';
+ var next;
+ var mapping;
+ var nameIdx;
+ var sourceIdx;
+
+ var mappings = this._mappings.toArray();
+ for (var i = 0, len = mappings.length; i < len; i++) {
+ mapping = mappings[i];
+ next = ''
+
+ if (mapping.generatedLine !== previousGeneratedLine) {
+ previousGeneratedColumn = 0;
+ while (mapping.generatedLine !== previousGeneratedLine) {
+ next += ';';
+ previousGeneratedLine++;
+ }
+ }
+ else {
+ if (i > 0) {
+ if (!util.compareByGeneratedPositionsInflated(mapping, mappings[i - 1])) {
+ continue;
+ }
+ next += ',';
+ }
+ }
+
+ next += base64VLQ.encode(mapping.generatedColumn
+ - previousGeneratedColumn);
+ previousGeneratedColumn = mapping.generatedColumn;
+
+ if (mapping.source != null) {
+ sourceIdx = this._sources.indexOf(mapping.source);
+ next += base64VLQ.encode(sourceIdx - previousSource);
+ previousSource = sourceIdx;
+
+ // lines are stored 0-based in SourceMap spec version 3
+ next += base64VLQ.encode(mapping.originalLine - 1
+ - previousOriginalLine);
+ previousOriginalLine = mapping.originalLine - 1;
+
+ next += base64VLQ.encode(mapping.originalColumn
+ - previousOriginalColumn);
+ previousOriginalColumn = mapping.originalColumn;
+
+ if (mapping.name != null) {
+ nameIdx = this._names.indexOf(mapping.name);
+ next += base64VLQ.encode(nameIdx - previousName);
+ previousName = nameIdx;
+ }
+ }
+
+ result += next;
+ }
+
+ return result;
+ };
+
+ SourceMapGenerator.prototype._generateSourcesContent =
+ function SourceMapGenerator_generateSourcesContent(aSources, aSourceRoot) {
+ return aSources.map(function (source) {
+ if (!this._sourcesContents) {
+ return null;
+ }
+ if (aSourceRoot != null) {
+ source = util.relative(aSourceRoot, source);
+ }
+ var key = util.toSetString(source);
+ return Object.prototype.hasOwnProperty.call(this._sourcesContents, key)
+ ? this._sourcesContents[key]
+ : null;
+ }, this);
+ };
+
+ /**
+ * Externalize the source map.
+ */
+ SourceMapGenerator.prototype.toJSON =
+ function SourceMapGenerator_toJSON() {
+ var map = {
+ version: this._version,
+ sources: this._sources.toArray(),
+ names: this._names.toArray(),
+ mappings: this._serializeMappings()
+ };
+ if (this._file != null) {
+ map.file = this._file;
+ }
+ if (this._sourceRoot != null) {
+ map.sourceRoot = this._sourceRoot;
+ }
+ if (this._sourcesContents) {
+ map.sourcesContent = this._generateSourcesContent(map.sources, map.sourceRoot);
+ }
+
+ return map;
+ };
+
+ /**
+ * Render the source map being generated to a string.
+ */
+ SourceMapGenerator.prototype.toString =
+ function SourceMapGenerator_toString() {
+ return JSON.stringify(this.toJSON());
+ };
+
+ exports.SourceMapGenerator = SourceMapGenerator;
+
+
+/***/ },
+
+/***/ 474:
+/***/ function(module, exports, __webpack_require__) {
+
+ /* -*- Mode: js; js-indent-level: 2; -*- */
+ /*
+ * Copyright 2011 Mozilla Foundation and contributors
+ * Licensed under the New BSD license. See LICENSE or:
+ * http://opensource.org/licenses/BSD-3-Clause
+ *
+ * Based on the Base 64 VLQ implementation in Closure Compiler:
+ * https://code.google.com/p/closure-compiler/source/browse/trunk/src/com/google/debugging/sourcemap/Base64VLQ.java
+ *
+ * Copyright 2011 The Closure Compiler Authors. All rights reserved.
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+ var base64 = __webpack_require__(475);
+
+ // A single base 64 digit can contain 6 bits of data. For the base 64 variable
+ // length quantities we use in the source map spec, the first bit is the sign,
+ // the next four bits are the actual value, and the 6th bit is the
+ // continuation bit. The continuation bit tells us whether there are more
+ // digits in this value following this digit.
+ //
+ // Continuation
+ // | Sign
+ // | |
+ // V V
+ // 101011
+
+ var VLQ_BASE_SHIFT = 5;
+
+ // binary: 100000
+ var VLQ_BASE = 1 << VLQ_BASE_SHIFT;
+
+ // binary: 011111
+ var VLQ_BASE_MASK = VLQ_BASE - 1;
+
+ // binary: 100000
+ var VLQ_CONTINUATION_BIT = VLQ_BASE;
+
+ /**
+ * Converts from a two-complement value to a value where the sign bit is
+ * placed in the least significant bit. For example, as decimals:
+ * 1 becomes 2 (10 binary), -1 becomes 3 (11 binary)
+ * 2 becomes 4 (100 binary), -2 becomes 5 (101 binary)
+ */
+ function toVLQSigned(aValue) {
+ return aValue < 0
+ ? ((-aValue) << 1) + 1
+ : (aValue << 1) + 0;
+ }
+
+ /**
+ * Converts to a two-complement value from a value where the sign bit is
+ * placed in the least significant bit. For example, as decimals:
+ * 2 (10 binary) becomes 1, 3 (11 binary) becomes -1
+ * 4 (100 binary) becomes 2, 5 (101 binary) becomes -2
+ */
+ function fromVLQSigned(aValue) {
+ var isNegative = (aValue & 1) === 1;
+ var shifted = aValue >> 1;
+ return isNegative
+ ? -shifted
+ : shifted;
+ }
+
+ /**
+ * Returns the base 64 VLQ encoded value.
+ */
+ exports.encode = function base64VLQ_encode(aValue) {
+ var encoded = "";
+ var digit;
+
+ var vlq = toVLQSigned(aValue);
+
+ do {
+ digit = vlq & VLQ_BASE_MASK;
+ vlq >>>= VLQ_BASE_SHIFT;
+ if (vlq > 0) {
+ // There are still more digits in this value, so we must make sure the
+ // continuation bit is marked.
+ digit |= VLQ_CONTINUATION_BIT;
+ }
+ encoded += base64.encode(digit);
+ } while (vlq > 0);
+
+ return encoded;
+ };
+
+ /**
+ * Decodes the next base 64 VLQ value from the given string and returns the
+ * value and the rest of the string via the out parameter.
+ */
+ exports.decode = function base64VLQ_decode(aStr, aIndex, aOutParam) {
+ var strLen = aStr.length;
+ var result = 0;
+ var shift = 0;
+ var continuation, digit;
+
+ do {
+ if (aIndex >= strLen) {
+ throw new Error("Expected more digits in base 64 VLQ value.");
+ }
+
+ digit = base64.decode(aStr.charCodeAt(aIndex++));
+ if (digit === -1) {
+ throw new Error("Invalid base64 digit: " + aStr.charAt(aIndex - 1));
+ }
+
+ continuation = !!(digit & VLQ_CONTINUATION_BIT);
+ digit &= VLQ_BASE_MASK;
+ result = result + (digit << shift);
+ shift += VLQ_BASE_SHIFT;
+ } while (continuation);
+
+ aOutParam.value = fromVLQSigned(result);
+ aOutParam.rest = aIndex;
+ };
+
+
+/***/ },
+
+/***/ 475:
+/***/ function(module, exports) {
+
+ /* -*- Mode: js; js-indent-level: 2; -*- */
+ /*
+ * Copyright 2011 Mozilla Foundation and contributors
+ * Licensed under the New BSD license. See LICENSE or:
+ * http://opensource.org/licenses/BSD-3-Clause
+ */
+
+ var intToCharMap = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'.split('');
+
+ /**
+ * Encode an integer in the range of 0 to 63 to a single base 64 digit.
+ */
+ exports.encode = function (number) {
+ if (0 <= number && number < intToCharMap.length) {
+ return intToCharMap[number];
+ }
+ throw new TypeError("Must be between 0 and 63: " + number);
+ };
+
+ /**
+ * Decode a single base 64 character code digit to an integer. Returns -1 on
+ * failure.
+ */
+ exports.decode = function (charCode) {
+ var bigA = 65; // 'A'
+ var bigZ = 90; // 'Z'
+
+ var littleA = 97; // 'a'
+ var littleZ = 122; // 'z'
+
+ var zero = 48; // '0'
+ var nine = 57; // '9'
+
+ var plus = 43; // '+'
+ var slash = 47; // '/'
+
+ var littleOffset = 26;
+ var numberOffset = 52;
+
+ // 0 - 25: ABCDEFGHIJKLMNOPQRSTUVWXYZ
+ if (bigA <= charCode && charCode <= bigZ) {
+ return (charCode - bigA);
+ }
+
+ // 26 - 51: abcdefghijklmnopqrstuvwxyz
+ if (littleA <= charCode && charCode <= littleZ) {
+ return (charCode - littleA + littleOffset);
+ }
+
+ // 52 - 61: 0123456789
+ if (zero <= charCode && charCode <= nine) {
+ return (charCode - zero + numberOffset);
+ }
+
+ // 62: +
+ if (charCode == plus) {
+ return 62;
+ }
+
+ // 63: /
+ if (charCode == slash) {
+ return 63;
+ }
+
+ // Invalid base64 digit.
+ return -1;
+ };
+
+
+/***/ },
+
+/***/ 476:
+/***/ function(module, exports) {
+
+ /* -*- Mode: js; js-indent-level: 2; -*- */
+ /*
+ * Copyright 2011 Mozilla Foundation and contributors
+ * Licensed under the New BSD license. See LICENSE or:
+ * http://opensource.org/licenses/BSD-3-Clause
+ */
+
+ /**
+ * This is a helper function for getting values from parameter/options
+ * objects.
+ *
+ * @param args The object we are extracting values from
+ * @param name The name of the property we are getting.
+ * @param defaultValue An optional value to return if the property is missing
+ * from the object. If this is not specified and the property is missing, an
+ * error will be thrown.
+ */
+ function getArg(aArgs, aName, aDefaultValue) {
+ if (aName in aArgs) {
+ return aArgs[aName];
+ } else if (arguments.length === 3) {
+ return aDefaultValue;
+ } else {
+ throw new Error('"' + aName + '" is a required argument.');
+ }
+ }
+ exports.getArg = getArg;
+
+ var urlRegexp = /^(?:([\w+\-.]+):)?\/\/(?:(\w+:\w+)@)?([\w.]*)(?::(\d+))?(\S*)$/;
+ var dataUrlRegexp = /^data:.+\,.+$/;
+
+ function urlParse(aUrl) {
+ var match = aUrl.match(urlRegexp);
+ if (!match) {
+ return null;
+ }
+ return {
+ scheme: match[1],
+ auth: match[2],
+ host: match[3],
+ port: match[4],
+ path: match[5]
+ };
+ }
+ exports.urlParse = urlParse;
+
+ function urlGenerate(aParsedUrl) {
+ var url = '';
+ if (aParsedUrl.scheme) {
+ url += aParsedUrl.scheme + ':';
+ }
+ url += '//';
+ if (aParsedUrl.auth) {
+ url += aParsedUrl.auth + '@';
+ }
+ if (aParsedUrl.host) {
+ url += aParsedUrl.host;
+ }
+ if (aParsedUrl.port) {
+ url += ":" + aParsedUrl.port
+ }
+ if (aParsedUrl.path) {
+ url += aParsedUrl.path;
+ }
+ return url;
+ }
+ exports.urlGenerate = urlGenerate;
+
+ /**
+ * Normalizes a path, or the path portion of a URL:
+ *
+ * - Replaces consecutive slashes with one slash.
+ * - Removes unnecessary '.' parts.
+ * - Removes unnecessary '<dir>/..' parts.
+ *
+ * Based on code in the Node.js 'path' core module.
+ *
+ * @param aPath The path or url to normalize.
+ */
+ function normalize(aPath) {
+ var path = aPath;
+ var url = urlParse(aPath);
+ if (url) {
+ if (!url.path) {
+ return aPath;
+ }
+ path = url.path;
+ }
+ var isAbsolute = exports.isAbsolute(path);
+
+ var parts = path.split(/\/+/);
+ for (var part, up = 0, i = parts.length - 1; i >= 0; i--) {
+ part = parts[i];
+ if (part === '.') {
+ parts.splice(i, 1);
+ } else if (part === '..') {
+ up++;
+ } else if (up > 0) {
+ if (part === '') {
+ // The first part is blank if the path is absolute. Trying to go
+ // above the root is a no-op. Therefore we can remove all '..' parts
+ // directly after the root.
+ parts.splice(i + 1, up);
+ up = 0;
+ } else {
+ parts.splice(i, 2);
+ up--;
+ }
+ }
+ }
+ path = parts.join('/');
+
+ if (path === '') {
+ path = isAbsolute ? '/' : '.';
+ }
+
+ if (url) {
+ url.path = path;
+ return urlGenerate(url);
+ }
+ return path;
+ }
+ exports.normalize = normalize;
+
+ /**
+ * Joins two paths/URLs.
+ *
+ * @param aRoot The root path or URL.
+ * @param aPath The path or URL to be joined with the root.
+ *
+ * - If aPath is a URL or a data URI, aPath is returned, unless aPath is a
+ * scheme-relative URL: Then the scheme of aRoot, if any, is prepended
+ * first.
+ * - Otherwise aPath is a path. If aRoot is a URL, then its path portion
+ * is updated with the result and aRoot is returned. Otherwise the result
+ * is returned.
+ * - If aPath is absolute, the result is aPath.
+ * - Otherwise the two paths are joined with a slash.
+ * - Joining for example 'http://' and 'www.example.com' is also supported.
+ */
+ function join(aRoot, aPath) {
+ if (aRoot === "") {
+ aRoot = ".";
+ }
+ if (aPath === "") {
+ aPath = ".";
+ }
+ var aPathUrl = urlParse(aPath);
+ var aRootUrl = urlParse(aRoot);
+ if (aRootUrl) {
+ aRoot = aRootUrl.path || '/';
+ }
+
+ // `join(foo, '//www.example.org')`
+ if (aPathUrl && !aPathUrl.scheme) {
+ if (aRootUrl) {
+ aPathUrl.scheme = aRootUrl.scheme;
+ }
+ return urlGenerate(aPathUrl);
+ }
+
+ if (aPathUrl || aPath.match(dataUrlRegexp)) {
+ return aPath;
+ }
+
+ // `join('http://', 'www.example.com')`
+ if (aRootUrl && !aRootUrl.host && !aRootUrl.path) {
+ aRootUrl.host = aPath;
+ return urlGenerate(aRootUrl);
+ }
+
+ var joined = aPath.charAt(0) === '/'
+ ? aPath
+ : normalize(aRoot.replace(/\/+$/, '') + '/' + aPath);
+
+ if (aRootUrl) {
+ aRootUrl.path = joined;
+ return urlGenerate(aRootUrl);
+ }
+ return joined;
+ }
+ exports.join = join;
+
+ exports.isAbsolute = function (aPath) {
+ return aPath.charAt(0) === '/' || !!aPath.match(urlRegexp);
+ };
+
+ /**
+ * Make a path relative to a URL or another path.
+ *
+ * @param aRoot The root path or URL.
+ * @param aPath The path or URL to be made relative to aRoot.
+ */
+ function relative(aRoot, aPath) {
+ if (aRoot === "") {
+ aRoot = ".";
+ }
+
+ aRoot = aRoot.replace(/\/$/, '');
+
+ // It is possible for the path to be above the root. In this case, simply
+ // checking whether the root is a prefix of the path won't work. Instead, we
+ // need to remove components from the root one by one, until either we find
+ // a prefix that fits, or we run out of components to remove.
+ var level = 0;
+ while (aPath.indexOf(aRoot + '/') !== 0) {
+ var index = aRoot.lastIndexOf("/");
+ if (index < 0) {
+ return aPath;
+ }
+
+ // If the only part of the root that is left is the scheme (i.e. http://,
+ // file:///, etc.), one or more slashes (/), or simply nothing at all, we
+ // have exhausted all components, so the path is not relative to the root.
+ aRoot = aRoot.slice(0, index);
+ if (aRoot.match(/^([^\/]+:\/)?\/*$/)) {
+ return aPath;
+ }
+
+ ++level;
+ }
+
+ // Make sure we add a "../" for each component we removed from the root.
+ return Array(level + 1).join("../") + aPath.substr(aRoot.length + 1);
+ }
+ exports.relative = relative;
+
+ var supportsNullProto = (function () {
+ var obj = Object.create(null);
+ return !('__proto__' in obj);
+ }());
+
+ function identity (s) {
+ return s;
+ }
+
+ /**
+ * Because behavior goes wacky when you set `__proto__` on objects, we
+ * have to prefix all the strings in our set with an arbitrary character.
+ *
+ * See https://github.com/mozilla/source-map/pull/31 and
+ * https://github.com/mozilla/source-map/issues/30
+ *
+ * @param String aStr
+ */
+ function toSetString(aStr) {
+ if (isProtoString(aStr)) {
+ return '$' + aStr;
+ }
+
+ return aStr;
+ }
+ exports.toSetString = supportsNullProto ? identity : toSetString;
+
+ function fromSetString(aStr) {
+ if (isProtoString(aStr)) {
+ return aStr.slice(1);
+ }
+
+ return aStr;
+ }
+ exports.fromSetString = supportsNullProto ? identity : fromSetString;
+
+ function isProtoString(s) {
+ if (!s) {
+ return false;
+ }
+
+ var length = s.length;
+
+ if (length < 9 /* "__proto__".length */) {
+ return false;
+ }
+
+ if (s.charCodeAt(length - 1) !== 95 /* '_' */ ||
+ s.charCodeAt(length - 2) !== 95 /* '_' */ ||
+ s.charCodeAt(length - 3) !== 111 /* 'o' */ ||
+ s.charCodeAt(length - 4) !== 116 /* 't' */ ||
+ s.charCodeAt(length - 5) !== 111 /* 'o' */ ||
+ s.charCodeAt(length - 6) !== 114 /* 'r' */ ||
+ s.charCodeAt(length - 7) !== 112 /* 'p' */ ||
+ s.charCodeAt(length - 8) !== 95 /* '_' */ ||
+ s.charCodeAt(length - 9) !== 95 /* '_' */) {
+ return false;
+ }
+
+ for (var i = length - 10; i >= 0; i--) {
+ if (s.charCodeAt(i) !== 36 /* '$' */) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Comparator between two mappings where the original positions are compared.
+ *
+ * Optionally pass in `true` as `onlyCompareGenerated` to consider two
+ * mappings with the same original source/line/column, but different generated
+ * line and column the same. Useful when searching for a mapping with a
+ * stubbed out mapping.
+ */
+ function compareByOriginalPositions(mappingA, mappingB, onlyCompareOriginal) {
+ var cmp = mappingA.source - mappingB.source;
+ if (cmp !== 0) {
+ return cmp;
+ }
+
+ cmp = mappingA.originalLine - mappingB.originalLine;
+ if (cmp !== 0) {
+ return cmp;
+ }
+
+ cmp = mappingA.originalColumn - mappingB.originalColumn;
+ if (cmp !== 0 || onlyCompareOriginal) {
+ return cmp;
+ }
+
+ cmp = mappingA.generatedColumn - mappingB.generatedColumn;
+ if (cmp !== 0) {
+ return cmp;
+ }
+
+ cmp = mappingA.generatedLine - mappingB.generatedLine;
+ if (cmp !== 0) {
+ return cmp;
+ }
+
+ return mappingA.name - mappingB.name;
+ }
+ exports.compareByOriginalPositions = compareByOriginalPositions;
+
+ /**
+ * Comparator between two mappings with deflated source and name indices where
+ * the generated positions are compared.
+ *
+ * Optionally pass in `true` as `onlyCompareGenerated` to consider two
+ * mappings with the same generated line and column, but different
+ * source/name/original line and column the same. Useful when searching for a
+ * mapping with a stubbed out mapping.
+ */
+ function compareByGeneratedPositionsDeflated(mappingA, mappingB, onlyCompareGenerated) {
+ var cmp = mappingA.generatedLine - mappingB.generatedLine;
+ if (cmp !== 0) {
+ return cmp;
+ }
+
+ cmp = mappingA.generatedColumn - mappingB.generatedColumn;
+ if (cmp !== 0 || onlyCompareGenerated) {
+ return cmp;
+ }
+
+ cmp = mappingA.source - mappingB.source;
+ if (cmp !== 0) {
+ return cmp;
+ }
+
+ cmp = mappingA.originalLine - mappingB.originalLine;
+ if (cmp !== 0) {
+ return cmp;
+ }
+
+ cmp = mappingA.originalColumn - mappingB.originalColumn;
+ if (cmp !== 0) {
+ return cmp;
+ }
+
+ return mappingA.name - mappingB.name;
+ }
+ exports.compareByGeneratedPositionsDeflated = compareByGeneratedPositionsDeflated;
+
+ function strcmp(aStr1, aStr2) {
+ if (aStr1 === aStr2) {
+ return 0;
+ }
+
+ if (aStr1 > aStr2) {
+ return 1;
+ }
+
+ return -1;
+ }
+
+ /**
+ * Comparator between two mappings with inflated source and name strings where
+ * the generated positions are compared.
+ */
+ function compareByGeneratedPositionsInflated(mappingA, mappingB) {
+ var cmp = mappingA.generatedLine - mappingB.generatedLine;
+ if (cmp !== 0) {
+ return cmp;
+ }
+
+ cmp = mappingA.generatedColumn - mappingB.generatedColumn;
+ if (cmp !== 0) {
+ return cmp;
+ }
+
+ cmp = strcmp(mappingA.source, mappingB.source);
+ if (cmp !== 0) {
+ return cmp;
+ }
+
+ cmp = mappingA.originalLine - mappingB.originalLine;
+ if (cmp !== 0) {
+ return cmp;
+ }
+
+ cmp = mappingA.originalColumn - mappingB.originalColumn;
+ if (cmp !== 0) {
+ return cmp;
+ }
+
+ return strcmp(mappingA.name, mappingB.name);
+ }
+ exports.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflated;
+
+
+/***/ },
+
+/***/ 477:
+/***/ function(module, exports, __webpack_require__) {
+
+ /* -*- Mode: js; js-indent-level: 2; -*- */
+ /*
+ * Copyright 2011 Mozilla Foundation and contributors
+ * Licensed under the New BSD license. See LICENSE or:
+ * http://opensource.org/licenses/BSD-3-Clause
+ */
+
+ var util = __webpack_require__(476);
+ var has = Object.prototype.hasOwnProperty;
+
+ /**
+ * A data structure which is a combination of an array and a set. Adding a new
+ * member is O(1), testing for membership is O(1), and finding the index of an
+ * element is O(1). Removing elements from the set is not supported. Only
+ * strings are supported for membership.
+ */
+ function ArraySet() {
+ this._array = [];
+ this._set = Object.create(null);
+ }
+
+ /**
+ * Static method for creating ArraySet instances from an existing array.
+ */
+ ArraySet.fromArray = function ArraySet_fromArray(aArray, aAllowDuplicates) {
+ var set = new ArraySet();
+ for (var i = 0, len = aArray.length; i < len; i++) {
+ set.add(aArray[i], aAllowDuplicates);
+ }
+ return set;
+ };
+
+ /**
+ * Return how many unique items are in this ArraySet. If duplicates have been
+ * added, than those do not count towards the size.
+ *
+ * @returns Number
+ */
+ ArraySet.prototype.size = function ArraySet_size() {
+ return Object.getOwnPropertyNames(this._set).length;
+ };
+
+ /**
+ * Add the given string to this set.
+ *
+ * @param String aStr
+ */
+ ArraySet.prototype.add = function ArraySet_add(aStr, aAllowDuplicates) {
+ var sStr = util.toSetString(aStr);
+ var isDuplicate = has.call(this._set, sStr);
+ var idx = this._array.length;
+ if (!isDuplicate || aAllowDuplicates) {
+ this._array.push(aStr);
+ }
+ if (!isDuplicate) {
+ this._set[sStr] = idx;
+ }
+ };
+
+ /**
+ * Is the given string a member of this set?
+ *
+ * @param String aStr
+ */
+ ArraySet.prototype.has = function ArraySet_has(aStr) {
+ var sStr = util.toSetString(aStr);
+ return has.call(this._set, sStr);
+ };
+
+ /**
+ * What is the index of the given string in the array?
+ *
+ * @param String aStr
+ */
+ ArraySet.prototype.indexOf = function ArraySet_indexOf(aStr) {
+ var sStr = util.toSetString(aStr);
+ if (has.call(this._set, sStr)) {
+ return this._set[sStr];
+ }
+ throw new Error('"' + aStr + '" is not in the set.');
+ };
+
+ /**
+ * What is the element at the given index?
+ *
+ * @param Number aIdx
+ */
+ ArraySet.prototype.at = function ArraySet_at(aIdx) {
+ if (aIdx >= 0 && aIdx < this._array.length) {
+ return this._array[aIdx];
+ }
+ throw new Error('No element indexed by ' + aIdx);
+ };
+
+ /**
+ * Returns the array representation of this set (which has the proper indices
+ * indicated by indexOf). Note that this is a copy of the internal array used
+ * for storing the members so that no one can mess with internal state.
+ */
+ ArraySet.prototype.toArray = function ArraySet_toArray() {
+ return this._array.slice();
+ };
+
+ exports.ArraySet = ArraySet;
+
+
+/***/ },
+
+/***/ 478:
+/***/ function(module, exports, __webpack_require__) {
+
+ /* -*- Mode: js; js-indent-level: 2; -*- */
+ /*
+ * Copyright 2014 Mozilla Foundation and contributors
+ * Licensed under the New BSD license. See LICENSE or:
+ * http://opensource.org/licenses/BSD-3-Clause
+ */
+
+ var util = __webpack_require__(476);
+
+ /**
+ * Determine whether mappingB is after mappingA with respect to generated
+ * position.
+ */
+ function generatedPositionAfter(mappingA, mappingB) {
+ // Optimized for most common case
+ var lineA = mappingA.generatedLine;
+ var lineB = mappingB.generatedLine;
+ var columnA = mappingA.generatedColumn;
+ var columnB = mappingB.generatedColumn;
+ return lineB > lineA || lineB == lineA && columnB >= columnA ||
+ util.compareByGeneratedPositionsInflated(mappingA, mappingB) <= 0;
+ }
+
+ /**
+ * A data structure to provide a sorted view of accumulated mappings in a
+ * performance conscious manner. It trades a neglibable overhead in general
+ * case for a large speedup in case of mappings being added in order.
+ */
+ function MappingList() {
+ this._array = [];
+ this._sorted = true;
+ // Serves as infimum
+ this._last = {generatedLine: -1, generatedColumn: 0};
+ }
+
+ /**
+ * Iterate through internal items. This method takes the same arguments that
+ * `Array.prototype.forEach` takes.
+ *
+ * NOTE: The order of the mappings is NOT guaranteed.
+ */
+ MappingList.prototype.unsortedForEach =
+ function MappingList_forEach(aCallback, aThisArg) {
+ this._array.forEach(aCallback, aThisArg);
+ };
+
+ /**
+ * Add the given source mapping.
+ *
+ * @param Object aMapping
+ */
+ MappingList.prototype.add = function MappingList_add(aMapping) {
+ if (generatedPositionAfter(this._last, aMapping)) {
+ this._last = aMapping;
+ this._array.push(aMapping);
+ } else {
+ this._sorted = false;
+ this._array.push(aMapping);
+ }
+ };
+
+ /**
+ * Returns the flat, sorted array of mappings. The mappings are sorted by
+ * generated position.
+ *
+ * WARNING: This method returns internal data without copying, for
+ * performance. The return value must NOT be mutated, and should be treated as
+ * an immutable borrow. If you want to take ownership, you must make your own
+ * copy.
+ */
+ MappingList.prototype.toArray = function MappingList_toArray() {
+ if (!this._sorted) {
+ this._array.sort(util.compareByGeneratedPositionsInflated);
+ this._sorted = true;
+ }
+ return this._array;
+ };
+
+ exports.MappingList = MappingList;
+
+
+/***/ },
+
+/***/ 479:
+/***/ function(module, exports, __webpack_require__) {
+
+ /* -*- Mode: js; js-indent-level: 2; -*- */
+ /*
+ * Copyright 2011 Mozilla Foundation and contributors
+ * Licensed under the New BSD license. See LICENSE or:
+ * http://opensource.org/licenses/BSD-3-Clause
+ */
+
+ var util = __webpack_require__(476);
+ var binarySearch = __webpack_require__(480);
+ var ArraySet = __webpack_require__(477).ArraySet;
+ var base64VLQ = __webpack_require__(474);
+ var quickSort = __webpack_require__(481).quickSort;
+
+ function SourceMapConsumer(aSourceMap) {
+ var sourceMap = aSourceMap;
+ if (typeof aSourceMap === 'string') {
+ sourceMap = JSON.parse(aSourceMap.replace(/^\)\]\}'/, ''));
+ }
+
+ return sourceMap.sections != null
+ ? new IndexedSourceMapConsumer(sourceMap)
+ : new BasicSourceMapConsumer(sourceMap);
+ }
+
+ SourceMapConsumer.fromSourceMap = function(aSourceMap) {
+ return BasicSourceMapConsumer.fromSourceMap(aSourceMap);
+ }
+
+ /**
+ * The version of the source mapping spec that we are consuming.
+ */
+ SourceMapConsumer.prototype._version = 3;
+
+ // `__generatedMappings` and `__originalMappings` are arrays that hold the
+ // parsed mapping coordinates from the source map's "mappings" attribute. They
+ // are lazily instantiated, accessed via the `_generatedMappings` and
+ // `_originalMappings` getters respectively, and we only parse the mappings
+ // and create these arrays once queried for a source location. We jump through
+ // these hoops because there can be many thousands of mappings, and parsing
+ // them is expensive, so we only want to do it if we must.
+ //
+ // Each object in the arrays is of the form:
+ //
+ // {
+ // generatedLine: The line number in the generated code,
+ // generatedColumn: The column number in the generated code,
+ // source: The path to the original source file that generated this
+ // chunk of code,
+ // originalLine: The line number in the original source that
+ // corresponds to this chunk of generated code,
+ // originalColumn: The column number in the original source that
+ // corresponds to this chunk of generated code,
+ // name: The name of the original symbol which generated this chunk of
+ // code.
+ // }
+ //
+ // All properties except for `generatedLine` and `generatedColumn` can be
+ // `null`.
+ //
+ // `_generatedMappings` is ordered by the generated positions.
+ //
+ // `_originalMappings` is ordered by the original positions.
+
+ SourceMapConsumer.prototype.__generatedMappings = null;
+ Object.defineProperty(SourceMapConsumer.prototype, '_generatedMappings', {
+ get: function () {
+ if (!this.__generatedMappings) {
+ this._parseMappings(this._mappings, this.sourceRoot);
+ }
+
+ return this.__generatedMappings;
+ }
+ });
+
+ SourceMapConsumer.prototype.__originalMappings = null;
+ Object.defineProperty(SourceMapConsumer.prototype, '_originalMappings', {
+ get: function () {
+ if (!this.__originalMappings) {
+ this._parseMappings(this._mappings, this.sourceRoot);
+ }
+
+ return this.__originalMappings;
+ }
+ });
+
+ SourceMapConsumer.prototype._charIsMappingSeparator =
+ function SourceMapConsumer_charIsMappingSeparator(aStr, index) {
+ var c = aStr.charAt(index);
+ return c === ";" || c === ",";
+ };
+
+ /**
+ * Parse the mappings in a string in to a data structure which we can easily
+ * query (the ordered arrays in the `this.__generatedMappings` and
+ * `this.__originalMappings` properties).
+ */
+ SourceMapConsumer.prototype._parseMappings =
+ function SourceMapConsumer_parseMappings(aStr, aSourceRoot) {
+ throw new Error("Subclasses must implement _parseMappings");
+ };
+
+ SourceMapConsumer.GENERATED_ORDER = 1;
+ SourceMapConsumer.ORIGINAL_ORDER = 2;
+
+ SourceMapConsumer.GREATEST_LOWER_BOUND = 1;
+ SourceMapConsumer.LEAST_UPPER_BOUND = 2;
+
+ /**
+ * Iterate over each mapping between an original source/line/column and a
+ * generated line/column in this source map.
+ *
+ * @param Function aCallback
+ * The function that is called with each mapping.
+ * @param Object aContext
+ * Optional. If specified, this object will be the value of `this` every
+ * time that `aCallback` is called.
+ * @param aOrder
+ * Either `SourceMapConsumer.GENERATED_ORDER` or
+ * `SourceMapConsumer.ORIGINAL_ORDER`. Specifies whether you want to
+ * iterate over the mappings sorted by the generated file's line/column
+ * order or the original's source/line/column order, respectively. Defaults to
+ * `SourceMapConsumer.GENERATED_ORDER`.
+ */
+ SourceMapConsumer.prototype.eachMapping =
+ function SourceMapConsumer_eachMapping(aCallback, aContext, aOrder) {
+ var context = aContext || null;
+ var order = aOrder || SourceMapConsumer.GENERATED_ORDER;
+
+ var mappings;
+ switch (order) {
+ case SourceMapConsumer.GENERATED_ORDER:
+ mappings = this._generatedMappings;
+ break;
+ case SourceMapConsumer.ORIGINAL_ORDER:
+ mappings = this._originalMappings;
+ break;
+ default:
+ throw new Error("Unknown order of iteration.");
+ }
+
+ var sourceRoot = this.sourceRoot;
+ mappings.map(function (mapping) {
+ var source = mapping.source === null ? null : this._sources.at(mapping.source);
+ if (source != null && sourceRoot != null) {
+ source = util.join(sourceRoot, source);
+ }
+ return {
+ source: source,
+ generatedLine: mapping.generatedLine,
+ generatedColumn: mapping.generatedColumn,
+ originalLine: mapping.originalLine,
+ originalColumn: mapping.originalColumn,
+ name: mapping.name === null ? null : this._names.at(mapping.name)
+ };
+ }, this).forEach(aCallback, context);
+ };
+
+ /**
+ * Returns all generated line and column information for the original source,
+ * line, and column provided. If no column is provided, returns all mappings
+ * corresponding to a either the line we are searching for or the next
+ * closest line that has any mappings. Otherwise, returns all mappings
+ * corresponding to the given line and either the column we are searching for
+ * or the next closest column that has any offsets.
+ *
+ * The only argument is an object with the following properties:
+ *
+ * - source: The filename of the original source.
+ * - line: The line number in the original source.
+ * - column: Optional. the column number in the original source.
+ *
+ * and an array of objects is returned, each with the following properties:
+ *
+ * - line: The line number in the generated source, or null.
+ * - column: The column number in the generated source, or null.
+ */
+ SourceMapConsumer.prototype.allGeneratedPositionsFor =
+ function SourceMapConsumer_allGeneratedPositionsFor(aArgs) {
+ var line = util.getArg(aArgs, 'line');
+
+ // When there is no exact match, BasicSourceMapConsumer.prototype._findMapping
+ // returns the index of the closest mapping less than the needle. By
+ // setting needle.originalColumn to 0, we thus find the last mapping for
+ // the given line, provided such a mapping exists.
+ var needle = {
+ source: util.getArg(aArgs, 'source'),
+ originalLine: line,
+ originalColumn: util.getArg(aArgs, 'column', 0)
+ };
+
+ if (this.sourceRoot != null) {
+ needle.source = util.relative(this.sourceRoot, needle.source);
+ }
+ if (!this._sources.has(needle.source)) {
+ return [];
+ }
+ needle.source = this._sources.indexOf(needle.source);
+
+ var mappings = [];
+
+ var index = this._findMapping(needle,
+ this._originalMappings,
+ "originalLine",
+ "originalColumn",
+ util.compareByOriginalPositions,
+ binarySearch.LEAST_UPPER_BOUND);
+ if (index >= 0) {
+ var mapping = this._originalMappings[index];
+
+ if (aArgs.column === undefined) {
+ var originalLine = mapping.originalLine;
+
+ // Iterate until either we run out of mappings, or we run into
+ // a mapping for a different line than the one we found. Since
+ // mappings are sorted, this is guaranteed to find all mappings for
+ // the line we found.
+ while (mapping && mapping.originalLine === originalLine) {
+ mappings.push({
+ line: util.getArg(mapping, 'generatedLine', null),
+ column: util.getArg(mapping, 'generatedColumn', null),
+ lastColumn: util.getArg(mapping, 'lastGeneratedColumn', null)
+ });
+
+ mapping = this._originalMappings[++index];
+ }
+ } else {
+ var originalColumn = mapping.originalColumn;
+
+ // Iterate until either we run out of mappings, or we run into
+ // a mapping for a different line than the one we were searching for.
+ // Since mappings are sorted, this is guaranteed to find all mappings for
+ // the line we are searching for.
+ while (mapping &&
+ mapping.originalLine === line &&
+ mapping.originalColumn == originalColumn) {
+ mappings.push({
+ line: util.getArg(mapping, 'generatedLine', null),
+ column: util.getArg(mapping, 'generatedColumn', null),
+ lastColumn: util.getArg(mapping, 'lastGeneratedColumn', null)
+ });
+
+ mapping = this._originalMappings[++index];
+ }
+ }
+ }
+
+ return mappings;
+ };
+
+ exports.SourceMapConsumer = SourceMapConsumer;
+
+ /**
+ * A BasicSourceMapConsumer instance represents a parsed source map which we can
+ * query for information about the original file positions by giving it a file
+ * position in the generated source.
+ *
+ * The only parameter is the raw source map (either as a JSON string, or
+ * already parsed to an object). According to the spec, source maps have the
+ * following attributes:
+ *
+ * - version: Which version of the source map spec this map is following.
+ * - sources: An array of URLs to the original source files.
+ * - names: An array of identifiers which can be referrenced by individual mappings.
+ * - sourceRoot: Optional. The URL root from which all sources are relative.
+ * - sourcesContent: Optional. An array of contents of the original source files.
+ * - mappings: A string of base64 VLQs which contain the actual mappings.
+ * - file: Optional. The generated file this source map is associated with.
+ *
+ * Here is an example source map, taken from the source map spec[0]:
+ *
+ * {
+ * version : 3,
+ * file: "out.js",
+ * sourceRoot : "",
+ * sources: ["foo.js", "bar.js"],
+ * names: ["src", "maps", "are", "fun"],
+ * mappings: "AA,AB;;ABCDE;"
+ * }
+ *
+ * [0]: https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit?pli=1#
+ */
+ function BasicSourceMapConsumer(aSourceMap) {
+ var sourceMap = aSourceMap;
+ if (typeof aSourceMap === 'string') {
+ sourceMap = JSON.parse(aSourceMap.replace(/^\)\]\}'/, ''));
+ }
+
+ var version = util.getArg(sourceMap, 'version');
+ var sources = util.getArg(sourceMap, 'sources');
+ // Sass 3.3 leaves out the 'names' array, so we deviate from the spec (which
+ // requires the array) to play nice here.
+ var names = util.getArg(sourceMap, 'names', []);
+ var sourceRoot = util.getArg(sourceMap, 'sourceRoot', null);
+ var sourcesContent = util.getArg(sourceMap, 'sourcesContent', null);
+ var mappings = util.getArg(sourceMap, 'mappings');
+ var file = util.getArg(sourceMap, 'file', null);
+
+ // Once again, Sass deviates from the spec and supplies the version as a
+ // string rather than a number, so we use loose equality checking here.
+ if (version != this._version) {
+ throw new Error('Unsupported version: ' + version);
+ }
+
+ sources = sources
+ .map(String)
+ // Some source maps produce relative source paths like "./foo.js" instead of
+ // "foo.js". Normalize these first so that future comparisons will succeed.
+ // See bugzil.la/1090768.
+ .map(util.normalize)
+ // Always ensure that absolute sources are internally stored relative to
+ // the source root, if the source root is absolute. Not doing this would
+ // be particularly problematic when the source root is a prefix of the
+ // source (valid, but why??). See github issue #199 and bugzil.la/1188982.
+ .map(function (source) {
+ return sourceRoot && util.isAbsolute(sourceRoot) && util.isAbsolute(source)
+ ? util.relative(sourceRoot, source)
+ : source;
+ });
+
+ // Pass `true` below to allow duplicate names and sources. While source maps
+ // are intended to be compressed and deduplicated, the TypeScript compiler
+ // sometimes generates source maps with duplicates in them. See Github issue
+ // #72 and bugzil.la/889492.
+ this._names = ArraySet.fromArray(names.map(String), true);
+ this._sources = ArraySet.fromArray(sources, true);
+
+ this.sourceRoot = sourceRoot;
+ this.sourcesContent = sourcesContent;
+ this._mappings = mappings;
+ this.file = file;
+ }
+
+ BasicSourceMapConsumer.prototype = Object.create(SourceMapConsumer.prototype);
+ BasicSourceMapConsumer.prototype.consumer = SourceMapConsumer;
+
+ /**
+ * Create a BasicSourceMapConsumer from a SourceMapGenerator.
+ *
+ * @param SourceMapGenerator aSourceMap
+ * The source map that will be consumed.
+ * @returns BasicSourceMapConsumer
+ */
+ BasicSourceMapConsumer.fromSourceMap =
+ function SourceMapConsumer_fromSourceMap(aSourceMap) {
+ var smc = Object.create(BasicSourceMapConsumer.prototype);
+
+ var names = smc._names = ArraySet.fromArray(aSourceMap._names.toArray(), true);
+ var sources = smc._sources = ArraySet.fromArray(aSourceMap._sources.toArray(), true);
+ smc.sourceRoot = aSourceMap._sourceRoot;
+ smc.sourcesContent = aSourceMap._generateSourcesContent(smc._sources.toArray(),
+ smc.sourceRoot);
+ smc.file = aSourceMap._file;
+
+ // Because we are modifying the entries (by converting string sources and
+ // names to indices into the sources and names ArraySets), we have to make
+ // a copy of the entry or else bad things happen. Shared mutable state
+ // strikes again! See github issue #191.
+
+ var generatedMappings = aSourceMap._mappings.toArray().slice();
+ var destGeneratedMappings = smc.__generatedMappings = [];
+ var destOriginalMappings = smc.__originalMappings = [];
+
+ for (var i = 0, length = generatedMappings.length; i < length; i++) {
+ var srcMapping = generatedMappings[i];
+ var destMapping = new Mapping;
+ destMapping.generatedLine = srcMapping.generatedLine;
+ destMapping.generatedColumn = srcMapping.generatedColumn;
+
+ if (srcMapping.source) {
+ destMapping.source = sources.indexOf(srcMapping.source);
+ destMapping.originalLine = srcMapping.originalLine;
+ destMapping.originalColumn = srcMapping.originalColumn;
+
+ if (srcMapping.name) {
+ destMapping.name = names.indexOf(srcMapping.name);
+ }
+
+ destOriginalMappings.push(destMapping);
+ }
+
+ destGeneratedMappings.push(destMapping);
+ }
+
+ quickSort(smc.__originalMappings, util.compareByOriginalPositions);
+
+ return smc;
+ };
+
+ /**
+ * The version of the source mapping spec that we are consuming.
+ */
+ BasicSourceMapConsumer.prototype._version = 3;
+
+ /**
+ * The list of original sources.
+ */
+ Object.defineProperty(BasicSourceMapConsumer.prototype, 'sources', {
+ get: function () {
+ return this._sources.toArray().map(function (s) {
+ return this.sourceRoot != null ? util.join(this.sourceRoot, s) : s;
+ }, this);
+ }
+ });
+
+ /**
+ * Provide the JIT with a nice shape / hidden class.
+ */
+ function Mapping() {
+ this.generatedLine = 0;
+ this.generatedColumn = 0;
+ this.source = null;
+ this.originalLine = null;
+ this.originalColumn = null;
+ this.name = null;
+ }
+
+ /**
+ * Parse the mappings in a string in to a data structure which we can easily
+ * query (the ordered arrays in the `this.__generatedMappings` and
+ * `this.__originalMappings` properties).
+ */
+ BasicSourceMapConsumer.prototype._parseMappings =
+ function SourceMapConsumer_parseMappings(aStr, aSourceRoot) {
+ var generatedLine = 1;
+ var previousGeneratedColumn = 0;
+ var previousOriginalLine = 0;
+ var previousOriginalColumn = 0;
+ var previousSource = 0;
+ var previousName = 0;
+ var length = aStr.length;
+ var index = 0;
+ var cachedSegments = {};
+ var temp = {};
+ var originalMappings = [];
+ var generatedMappings = [];
+ var mapping, str, segment, end, value;
+
+ while (index < length) {
+ if (aStr.charAt(index) === ';') {
+ generatedLine++;
+ index++;
+ previousGeneratedColumn = 0;
+ }
+ else if (aStr.charAt(index) === ',') {
+ index++;
+ }
+ else {
+ mapping = new Mapping();
+ mapping.generatedLine = generatedLine;
+
+ // Because each offset is encoded relative to the previous one,
+ // many segments often have the same encoding. We can exploit this
+ // fact by caching the parsed variable length fields of each segment,
+ // allowing us to avoid a second parse if we encounter the same
+ // segment again.
+ for (end = index; end < length; end++) {
+ if (this._charIsMappingSeparator(aStr, end)) {
+ break;
+ }
+ }
+ str = aStr.slice(index, end);
+
+ segment = cachedSegments[str];
+ if (segment) {
+ index += str.length;
+ } else {
+ segment = [];
+ while (index < end) {
+ base64VLQ.decode(aStr, index, temp);
+ value = temp.value;
+ index = temp.rest;
+ segment.push(value);
+ }
+
+ if (segment.length === 2) {
+ throw new Error('Found a source, but no line and column');
+ }
+
+ if (segment.length === 3) {
+ throw new Error('Found a source and line, but no column');
+ }
+
+ cachedSegments[str] = segment;
+ }
+
+ // Generated column.
+ mapping.generatedColumn = previousGeneratedColumn + segment[0];
+ previousGeneratedColumn = mapping.generatedColumn;
+
+ if (segment.length > 1) {
+ // Original source.
+ mapping.source = previousSource + segment[1];
+ previousSource += segment[1];
+
+ // Original line.
+ mapping.originalLine = previousOriginalLine + segment[2];
+ previousOriginalLine = mapping.originalLine;
+ // Lines are stored 0-based
+ mapping.originalLine += 1;
+
+ // Original column.
+ mapping.originalColumn = previousOriginalColumn + segment[3];
+ previousOriginalColumn = mapping.originalColumn;
+
+ if (segment.length > 4) {
+ // Original name.
+ mapping.name = previousName + segment[4];
+ previousName += segment[4];
+ }
+ }
+
+ generatedMappings.push(mapping);
+ if (typeof mapping.originalLine === 'number') {
+ originalMappings.push(mapping);
+ }
+ }
+ }
+
+ quickSort(generatedMappings, util.compareByGeneratedPositionsDeflated);
+ this.__generatedMappings = generatedMappings;
+
+ quickSort(originalMappings, util.compareByOriginalPositions);
+ this.__originalMappings = originalMappings;
+ };
+
+ /**
+ * Find the mapping that best matches the hypothetical "needle" mapping that
+ * we are searching for in the given "haystack" of mappings.
+ */
+ BasicSourceMapConsumer.prototype._findMapping =
+ function SourceMapConsumer_findMapping(aNeedle, aMappings, aLineName,
+ aColumnName, aComparator, aBias) {
+ // To return the position we are searching for, we must first find the
+ // mapping for the given position and then return the opposite position it
+ // points to. Because the mappings are sorted, we can use binary search to
+ // find the best mapping.
+
+ if (aNeedle[aLineName] <= 0) {
+ throw new TypeError('Line must be greater than or equal to 1, got '
+ + aNeedle[aLineName]);
+ }
+ if (aNeedle[aColumnName] < 0) {
+ throw new TypeError('Column must be greater than or equal to 0, got '
+ + aNeedle[aColumnName]);
+ }
+
+ return binarySearch.search(aNeedle, aMappings, aComparator, aBias);
+ };
+
+ /**
+ * Compute the last column for each generated mapping. The last column is
+ * inclusive.
+ */
+ BasicSourceMapConsumer.prototype.computeColumnSpans =
+ function SourceMapConsumer_computeColumnSpans() {
+ for (var index = 0; index < this._generatedMappings.length; ++index) {
+ var mapping = this._generatedMappings[index];
+
+ // Mappings do not contain a field for the last generated columnt. We
+ // can come up with an optimistic estimate, however, by assuming that
+ // mappings are contiguous (i.e. given two consecutive mappings, the
+ // first mapping ends where the second one starts).
+ if (index + 1 < this._generatedMappings.length) {
+ var nextMapping = this._generatedMappings[index + 1];
+
+ if (mapping.generatedLine === nextMapping.generatedLine) {
+ mapping.lastGeneratedColumn = nextMapping.generatedColumn - 1;
+ continue;
+ }
+ }
+
+ // The last mapping for each line spans the entire line.
+ mapping.lastGeneratedColumn = Infinity;
+ }
+ };
+
+ /**
+ * Returns the original source, line, and column information for the generated
+ * source's line and column positions provided. The only argument is an object
+ * with the following properties:
+ *
+ * - line: The line number in the generated source.
+ * - column: The column number in the generated source.
+ * - bias: Either 'SourceMapConsumer.GREATEST_LOWER_BOUND' or
+ * 'SourceMapConsumer.LEAST_UPPER_BOUND'. Specifies whether to return the
+ * closest element that is smaller than or greater than the one we are
+ * searching for, respectively, if the exact element cannot be found.
+ * Defaults to 'SourceMapConsumer.GREATEST_LOWER_BOUND'.
+ *
+ * and an object is returned with the following properties:
+ *
+ * - source: The original source file, or null.
+ * - line: The line number in the original source, or null.
+ * - column: The column number in the original source, or null.
+ * - name: The original identifier, or null.
+ */
+ BasicSourceMapConsumer.prototype.originalPositionFor =
+ function SourceMapConsumer_originalPositionFor(aArgs) {
+ var needle = {
+ generatedLine: util.getArg(aArgs, 'line'),
+ generatedColumn: util.getArg(aArgs, 'column')
+ };
+
+ var index = this._findMapping(
+ needle,
+ this._generatedMappings,
+ "generatedLine",
+ "generatedColumn",
+ util.compareByGeneratedPositionsDeflated,
+ util.getArg(aArgs, 'bias', SourceMapConsumer.GREATEST_LOWER_BOUND)
+ );
+
+ if (index >= 0) {
+ var mapping = this._generatedMappings[index];
+
+ if (mapping.generatedLine === needle.generatedLine) {
+ var source = util.getArg(mapping, 'source', null);
+ if (source !== null) {
+ source = this._sources.at(source);
+ if (this.sourceRoot != null) {
+ source = util.join(this.sourceRoot, source);
+ }
+ }
+ var name = util.getArg(mapping, 'name', null);
+ if (name !== null) {
+ name = this._names.at(name);
+ }
+ return {
+ source: source,
+ line: util.getArg(mapping, 'originalLine', null),
+ column: util.getArg(mapping, 'originalColumn', null),
+ name: name
+ };
+ }
+ }
+
+ return {
+ source: null,
+ line: null,
+ column: null,
+ name: null
+ };
+ };
+
+ /**
+ * Return true if we have the source content for every source in the source
+ * map, false otherwise.
+ */
+ BasicSourceMapConsumer.prototype.hasContentsOfAllSources =
+ function BasicSourceMapConsumer_hasContentsOfAllSources() {
+ if (!this.sourcesContent) {
+ return false;
+ }
+ return this.sourcesContent.length >= this._sources.size() &&
+ !this.sourcesContent.some(function (sc) { return sc == null; });
+ };
+
+ /**
+ * Returns the original source content. The only argument is the url of the
+ * original source file. Returns null if no original source content is
+ * available.
+ */
+ BasicSourceMapConsumer.prototype.sourceContentFor =
+ function SourceMapConsumer_sourceContentFor(aSource, nullOnMissing) {
+ if (!this.sourcesContent) {
+ return null;
+ }
+
+ if (this.sourceRoot != null) {
+ aSource = util.relative(this.sourceRoot, aSource);
+ }
+
+ if (this._sources.has(aSource)) {
+ return this.sourcesContent[this._sources.indexOf(aSource)];
+ }
+
+ var url;
+ if (this.sourceRoot != null
+ && (url = util.urlParse(this.sourceRoot))) {
+ // XXX: file:// URIs and absolute paths lead to unexpected behavior for
+ // many users. We can help them out when they expect file:// URIs to
+ // behave like it would if they were running a local HTTP server. See
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=885597.
+ var fileUriAbsPath = aSource.replace(/^file:\/\//, "");
+ if (url.scheme == "file"
+ && this._sources.has(fileUriAbsPath)) {
+ return this.sourcesContent[this._sources.indexOf(fileUriAbsPath)]
+ }
+
+ if ((!url.path || url.path == "/")
+ && this._sources.has("/" + aSource)) {
+ return this.sourcesContent[this._sources.indexOf("/" + aSource)];
+ }
+ }
+
+ // This function is used recursively from
+ // IndexedSourceMapConsumer.prototype.sourceContentFor. In that case, we
+ // don't want to throw if we can't find the source - we just want to
+ // return null, so we provide a flag to exit gracefully.
+ if (nullOnMissing) {
+ return null;
+ }
+ else {
+ throw new Error('"' + aSource + '" is not in the SourceMap.');
+ }
+ };
+
+ /**
+ * Returns the generated line and column information for the original source,
+ * line, and column positions provided. The only argument is an object with
+ * the following properties:
+ *
+ * - source: The filename of the original source.
+ * - line: The line number in the original source.
+ * - column: The column number in the original source.
+ * - bias: Either 'SourceMapConsumer.GREATEST_LOWER_BOUND' or
+ * 'SourceMapConsumer.LEAST_UPPER_BOUND'. Specifies whether to return the
+ * closest element that is smaller than or greater than the one we are
+ * searching for, respectively, if the exact element cannot be found.
+ * Defaults to 'SourceMapConsumer.GREATEST_LOWER_BOUND'.
+ *
+ * and an object is returned with the following properties:
+ *
+ * - line: The line number in the generated source, or null.
+ * - column: The column number in the generated source, or null.
+ */
+ BasicSourceMapConsumer.prototype.generatedPositionFor =
+ function SourceMapConsumer_generatedPositionFor(aArgs) {
+ var source = util.getArg(aArgs, 'source');
+ if (this.sourceRoot != null) {
+ source = util.relative(this.sourceRoot, source);
+ }
+ if (!this._sources.has(source)) {
+ return {
+ line: null,
+ column: null,
+ lastColumn: null
+ };
+ }
+ source = this._sources.indexOf(source);
+
+ var needle = {
+ source: source,
+ originalLine: util.getArg(aArgs, 'line'),
+ originalColumn: util.getArg(aArgs, 'column')
+ };
+
+ var index = this._findMapping(
+ needle,
+ this._originalMappings,
+ "originalLine",
+ "originalColumn",
+ util.compareByOriginalPositions,
+ util.getArg(aArgs, 'bias', SourceMapConsumer.GREATEST_LOWER_BOUND)
+ );
+
+ if (index >= 0) {
+ var mapping = this._originalMappings[index];
+
+ if (mapping.source === needle.source) {
+ return {
+ line: util.getArg(mapping, 'generatedLine', null),
+ column: util.getArg(mapping, 'generatedColumn', null),
+ lastColumn: util.getArg(mapping, 'lastGeneratedColumn', null)
+ };
+ }
+ }
+
+ return {
+ line: null,
+ column: null,
+ lastColumn: null
+ };
+ };
+
+ exports.BasicSourceMapConsumer = BasicSourceMapConsumer;
+
+ /**
+ * An IndexedSourceMapConsumer instance represents a parsed source map which
+ * we can query for information. It differs from BasicSourceMapConsumer in
+ * that it takes "indexed" source maps (i.e. ones with a "sections" field) as
+ * input.
+ *
+ * The only parameter is a raw source map (either as a JSON string, or already
+ * parsed to an object). According to the spec for indexed source maps, they
+ * have the following attributes:
+ *
+ * - version: Which version of the source map spec this map is following.
+ * - file: Optional. The generated file this source map is associated with.
+ * - sections: A list of section definitions.
+ *
+ * Each value under the "sections" field has two fields:
+ * - offset: The offset into the original specified at which this section
+ * begins to apply, defined as an object with a "line" and "column"
+ * field.
+ * - map: A source map definition. This source map could also be indexed,
+ * but doesn't have to be.
+ *
+ * Instead of the "map" field, it's also possible to have a "url" field
+ * specifying a URL to retrieve a source map from, but that's currently
+ * unsupported.
+ *
+ * Here's an example source map, taken from the source map spec[0], but
+ * modified to omit a section which uses the "url" field.
+ *
+ * {
+ * version : 3,
+ * file: "app.js",
+ * sections: [{
+ * offset: {line:100, column:10},
+ * map: {
+ * version : 3,
+ * file: "section.js",
+ * sources: ["foo.js", "bar.js"],
+ * names: ["src", "maps", "are", "fun"],
+ * mappings: "AAAA,E;;ABCDE;"
+ * }
+ * }],
+ * }
+ *
+ * [0]: https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit#heading=h.535es3xeprgt
+ */
+ function IndexedSourceMapConsumer(aSourceMap) {
+ var sourceMap = aSourceMap;
+ if (typeof aSourceMap === 'string') {
+ sourceMap = JSON.parse(aSourceMap.replace(/^\)\]\}'/, ''));
+ }
+
+ var version = util.getArg(sourceMap, 'version');
+ var sections = util.getArg(sourceMap, 'sections');
+
+ if (version != this._version) {
+ throw new Error('Unsupported version: ' + version);
+ }
+
+ this._sources = new ArraySet();
+ this._names = new ArraySet();
+
+ var lastOffset = {
+ line: -1,
+ column: 0
+ };
+ this._sections = sections.map(function (s) {
+ if (s.url) {
+ // The url field will require support for asynchronicity.
+ // See https://github.com/mozilla/source-map/issues/16
+ throw new Error('Support for url field in sections not implemented.');
+ }
+ var offset = util.getArg(s, 'offset');
+ var offsetLine = util.getArg(offset, 'line');
+ var offsetColumn = util.getArg(offset, 'column');
+
+ if (offsetLine < lastOffset.line ||
+ (offsetLine === lastOffset.line && offsetColumn < lastOffset.column)) {
+ throw new Error('Section offsets must be ordered and non-overlapping.');
+ }
+ lastOffset = offset;
+
+ return {
+ generatedOffset: {
+ // The offset fields are 0-based, but we use 1-based indices when
+ // encoding/decoding from VLQ.
+ generatedLine: offsetLine + 1,
+ generatedColumn: offsetColumn + 1
+ },
+ consumer: new SourceMapConsumer(util.getArg(s, 'map'))
+ }
+ });
+ }
+
+ IndexedSourceMapConsumer.prototype = Object.create(SourceMapConsumer.prototype);
+ IndexedSourceMapConsumer.prototype.constructor = SourceMapConsumer;
+
+ /**
+ * The version of the source mapping spec that we are consuming.
+ */
+ IndexedSourceMapConsumer.prototype._version = 3;
+
+ /**
+ * The list of original sources.
+ */
+ Object.defineProperty(IndexedSourceMapConsumer.prototype, 'sources', {
+ get: function () {
+ var sources = [];
+ for (var i = 0; i < this._sections.length; i++) {
+ for (var j = 0; j < this._sections[i].consumer.sources.length; j++) {
+ sources.push(this._sections[i].consumer.sources[j]);
+ }
+ }
+ return sources;
+ }
+ });
+
+ /**
+ * Returns the original source, line, and column information for the generated
+ * source's line and column positions provided. The only argument is an object
+ * with the following properties:
+ *
+ * - line: The line number in the generated source.
+ * - column: The column number in the generated source.
+ *
+ * and an object is returned with the following properties:
+ *
+ * - source: The original source file, or null.
+ * - line: The line number in the original source, or null.
+ * - column: The column number in the original source, or null.
+ * - name: The original identifier, or null.
+ */
+ IndexedSourceMapConsumer.prototype.originalPositionFor =
+ function IndexedSourceMapConsumer_originalPositionFor(aArgs) {
+ var needle = {
+ generatedLine: util.getArg(aArgs, 'line'),
+ generatedColumn: util.getArg(aArgs, 'column')
+ };
+
+ // Find the section containing the generated position we're trying to map
+ // to an original position.
+ var sectionIndex = binarySearch.search(needle, this._sections,
+ function(needle, section) {
+ var cmp = needle.generatedLine - section.generatedOffset.generatedLine;
+ if (cmp) {
+ return cmp;
+ }
+
+ return (needle.generatedColumn -
+ section.generatedOffset.generatedColumn);
+ });
+ var section = this._sections[sectionIndex];
+
+ if (!section) {
+ return {
+ source: null,
+ line: null,
+ column: null,
+ name: null
+ };
+ }
+
+ return section.consumer.originalPositionFor({
+ line: needle.generatedLine -
+ (section.generatedOffset.generatedLine - 1),
+ column: needle.generatedColumn -
+ (section.generatedOffset.generatedLine === needle.generatedLine
+ ? section.generatedOffset.generatedColumn - 1
+ : 0),
+ bias: aArgs.bias
+ });
+ };
+
+ /**
+ * Return true if we have the source content for every source in the source
+ * map, false otherwise.
+ */
+ IndexedSourceMapConsumer.prototype.hasContentsOfAllSources =
+ function IndexedSourceMapConsumer_hasContentsOfAllSources() {
+ return this._sections.every(function (s) {
+ return s.consumer.hasContentsOfAllSources();
+ });
+ };
+
+ /**
+ * Returns the original source content. The only argument is the url of the
+ * original source file. Returns null if no original source content is
+ * available.
+ */
+ IndexedSourceMapConsumer.prototype.sourceContentFor =
+ function IndexedSourceMapConsumer_sourceContentFor(aSource, nullOnMissing) {
+ for (var i = 0; i < this._sections.length; i++) {
+ var section = this._sections[i];
+
+ var content = section.consumer.sourceContentFor(aSource, true);
+ if (content) {
+ return content;
+ }
+ }
+ if (nullOnMissing) {
+ return null;
+ }
+ else {
+ throw new Error('"' + aSource + '" is not in the SourceMap.');
+ }
+ };
+
+ /**
+ * Returns the generated line and column information for the original source,
+ * line, and column positions provided. The only argument is an object with
+ * the following properties:
+ *
+ * - source: The filename of the original source.
+ * - line: The line number in the original source.
+ * - column: The column number in the original source.
+ *
+ * and an object is returned with the following properties:
+ *
+ * - line: The line number in the generated source, or null.
+ * - column: The column number in the generated source, or null.
+ */
+ IndexedSourceMapConsumer.prototype.generatedPositionFor =
+ function IndexedSourceMapConsumer_generatedPositionFor(aArgs) {
+ for (var i = 0; i < this._sections.length; i++) {
+ var section = this._sections[i];
+
+ // Only consider this section if the requested source is in the list of
+ // sources of the consumer.
+ if (section.consumer.sources.indexOf(util.getArg(aArgs, 'source')) === -1) {
+ continue;
+ }
+ var generatedPosition = section.consumer.generatedPositionFor(aArgs);
+ if (generatedPosition) {
+ var ret = {
+ line: generatedPosition.line +
+ (section.generatedOffset.generatedLine - 1),
+ column: generatedPosition.column +
+ (section.generatedOffset.generatedLine === generatedPosition.line
+ ? section.generatedOffset.generatedColumn - 1
+ : 0)
+ };
+ return ret;
+ }
+ }
+
+ return {
+ line: null,
+ column: null
+ };
+ };
+
+ /**
+ * Parse the mappings in a string in to a data structure which we can easily
+ * query (the ordered arrays in the `this.__generatedMappings` and
+ * `this.__originalMappings` properties).
+ */
+ IndexedSourceMapConsumer.prototype._parseMappings =
+ function IndexedSourceMapConsumer_parseMappings(aStr, aSourceRoot) {
+ this.__generatedMappings = [];
+ this.__originalMappings = [];
+ for (var i = 0; i < this._sections.length; i++) {
+ var section = this._sections[i];
+ var sectionMappings = section.consumer._generatedMappings;
+ for (var j = 0; j < sectionMappings.length; j++) {
+ var mapping = sectionMappings[j];
+
+ var source = section.consumer._sources.at(mapping.source);
+ if (section.consumer.sourceRoot !== null) {
+ source = util.join(section.consumer.sourceRoot, source);
+ }
+ this._sources.add(source);
+ source = this._sources.indexOf(source);
+
+ var name = section.consumer._names.at(mapping.name);
+ this._names.add(name);
+ name = this._names.indexOf(name);
+
+ // The mappings coming from the consumer for the section have
+ // generated positions relative to the start of the section, so we
+ // need to offset them to be relative to the start of the concatenated
+ // generated file.
+ var adjustedMapping = {
+ source: source,
+ generatedLine: mapping.generatedLine +
+ (section.generatedOffset.generatedLine - 1),
+ generatedColumn: mapping.generatedColumn +
+ (section.generatedOffset.generatedLine === mapping.generatedLine
+ ? section.generatedOffset.generatedColumn - 1
+ : 0),
+ originalLine: mapping.originalLine,
+ originalColumn: mapping.originalColumn,
+ name: name
+ };
+
+ this.__generatedMappings.push(adjustedMapping);
+ if (typeof adjustedMapping.originalLine === 'number') {
+ this.__originalMappings.push(adjustedMapping);
+ }
+ }
+ }
+
+ quickSort(this.__generatedMappings, util.compareByGeneratedPositionsDeflated);
+ quickSort(this.__originalMappings, util.compareByOriginalPositions);
+ };
+
+ exports.IndexedSourceMapConsumer = IndexedSourceMapConsumer;
+
+
+/***/ },
+
+/***/ 480:
+/***/ function(module, exports) {
+
+ /* -*- Mode: js; js-indent-level: 2; -*- */
+ /*
+ * Copyright 2011 Mozilla Foundation and contributors
+ * Licensed under the New BSD license. See LICENSE or:
+ * http://opensource.org/licenses/BSD-3-Clause
+ */
+
+ exports.GREATEST_LOWER_BOUND = 1;
+ exports.LEAST_UPPER_BOUND = 2;
+
+ /**
+ * Recursive implementation of binary search.
+ *
+ * @param aLow Indices here and lower do not contain the needle.
+ * @param aHigh Indices here and higher do not contain the needle.
+ * @param aNeedle The element being searched for.
+ * @param aHaystack The non-empty array being searched.
+ * @param aCompare Function which takes two elements and returns -1, 0, or 1.
+ * @param aBias Either 'binarySearch.GREATEST_LOWER_BOUND' or
+ * 'binarySearch.LEAST_UPPER_BOUND'. Specifies whether to return the
+ * closest element that is smaller than or greater than the one we are
+ * searching for, respectively, if the exact element cannot be found.
+ */
+ function recursiveSearch(aLow, aHigh, aNeedle, aHaystack, aCompare, aBias) {
+ // This function terminates when one of the following is true:
+ //
+ // 1. We find the exact element we are looking for.
+ //
+ // 2. We did not find the exact element, but we can return the index of
+ // the next-closest element.
+ //
+ // 3. We did not find the exact element, and there is no next-closest
+ // element than the one we are searching for, so we return -1.
+ var mid = Math.floor((aHigh - aLow) / 2) + aLow;
+ var cmp = aCompare(aNeedle, aHaystack[mid], true);
+ if (cmp === 0) {
+ // Found the element we are looking for.
+ return mid;
+ }
+ else if (cmp > 0) {
+ // Our needle is greater than aHaystack[mid].
+ if (aHigh - mid > 1) {
+ // The element is in the upper half.
+ return recursiveSearch(mid, aHigh, aNeedle, aHaystack, aCompare, aBias);
+ }
+
+ // The exact needle element was not found in this haystack. Determine if
+ // we are in termination case (3) or (2) and return the appropriate thing.
+ if (aBias == exports.LEAST_UPPER_BOUND) {
+ return aHigh < aHaystack.length ? aHigh : -1;
+ } else {
+ return mid;
+ }
+ }
+ else {
+ // Our needle is less than aHaystack[mid].
+ if (mid - aLow > 1) {
+ // The element is in the lower half.
+ return recursiveSearch(aLow, mid, aNeedle, aHaystack, aCompare, aBias);
+ }
+
+ // we are in termination case (3) or (2) and return the appropriate thing.
+ if (aBias == exports.LEAST_UPPER_BOUND) {
+ return mid;
+ } else {
+ return aLow < 0 ? -1 : aLow;
+ }
+ }
+ }
+
+ /**
+ * This is an implementation of binary search which will always try and return
+ * the index of the closest element if there is no exact hit. This is because
+ * mappings between original and generated line/col pairs are single points,
+ * and there is an implicit region between each of them, so a miss just means
+ * that you aren't on the very start of a region.
+ *
+ * @param aNeedle The element you are looking for.
+ * @param aHaystack The array that is being searched.
+ * @param aCompare A function which takes the needle and an element in the
+ * array and returns -1, 0, or 1 depending on whether the needle is less
+ * than, equal to, or greater than the element, respectively.
+ * @param aBias Either 'binarySearch.GREATEST_LOWER_BOUND' or
+ * 'binarySearch.LEAST_UPPER_BOUND'. Specifies whether to return the
+ * closest element that is smaller than or greater than the one we are
+ * searching for, respectively, if the exact element cannot be found.
+ * Defaults to 'binarySearch.GREATEST_LOWER_BOUND'.
+ */
+ exports.search = function search(aNeedle, aHaystack, aCompare, aBias) {
+ if (aHaystack.length === 0) {
+ return -1;
+ }
+
+ var index = recursiveSearch(-1, aHaystack.length, aNeedle, aHaystack,
+ aCompare, aBias || exports.GREATEST_LOWER_BOUND);
+ if (index < 0) {
+ return -1;
+ }
+
+ // We have found either the exact element, or the next-closest element than
+ // the one we are searching for. However, there may be more than one such
+ // element. Make sure we always return the smallest of these.
+ while (index - 1 >= 0) {
+ if (aCompare(aHaystack[index], aHaystack[index - 1], true) !== 0) {
+ break;
+ }
+ --index;
+ }
+
+ return index;
+ };
+
+
+/***/ },
+
+/***/ 481:
+/***/ function(module, exports) {
+
+ /* -*- Mode: js; js-indent-level: 2; -*- */
+ /*
+ * Copyright 2011 Mozilla Foundation and contributors
+ * Licensed under the New BSD license. See LICENSE or:
+ * http://opensource.org/licenses/BSD-3-Clause
+ */
+
+ // It turns out that some (most?) JavaScript engines don't self-host
+ // `Array.prototype.sort`. This makes sense because C++ will likely remain
+ // faster than JS when doing raw CPU-intensive sorting. However, when using a
+ // custom comparator function, calling back and forth between the VM's C++ and
+ // JIT'd JS is rather slow *and* loses JIT type information, resulting in
+ // worse generated code for the comparator function than would be optimal. In
+ // fact, when sorting with a comparator, these costs outweigh the benefits of
+ // sorting in C++. By using our own JS-implemented Quick Sort (below), we get
+ // a ~3500ms mean speed-up in `bench/bench.html`.
+
+ /**
+ * Swap the elements indexed by `x` and `y` in the array `ary`.
+ *
+ * @param {Array} ary
+ * The array.
+ * @param {Number} x
+ * The index of the first item.
+ * @param {Number} y
+ * The index of the second item.
+ */
+ function swap(ary, x, y) {
+ var temp = ary[x];
+ ary[x] = ary[y];
+ ary[y] = temp;
+ }
+
+ /**
+ * Returns a random integer within the range `low .. high` inclusive.
+ *
+ * @param {Number} low
+ * The lower bound on the range.
+ * @param {Number} high
+ * The upper bound on the range.
+ */
+ function randomIntInRange(low, high) {
+ return Math.round(low + (Math.random() * (high - low)));
+ }
+
+ /**
+ * The Quick Sort algorithm.
+ *
+ * @param {Array} ary
+ * An array to sort.
+ * @param {function} comparator
+ * Function to use to compare two items.
+ * @param {Number} p
+ * Start index of the array
+ * @param {Number} r
+ * End index of the array
+ */
+ function doQuickSort(ary, comparator, p, r) {
+ // If our lower bound is less than our upper bound, we (1) partition the
+ // array into two pieces and (2) recurse on each half. If it is not, this is
+ // the empty array and our base case.
+
+ if (p < r) {
+ // (1) Partitioning.
+ //
+ // The partitioning chooses a pivot between `p` and `r` and moves all
+ // elements that are less than or equal to the pivot to the before it, and
+ // all the elements that are greater than it after it. The effect is that
+ // once partition is done, the pivot is in the exact place it will be when
+ // the array is put in sorted order, and it will not need to be moved
+ // again. This runs in O(n) time.
+
+ // Always choose a random pivot so that an input array which is reverse
+ // sorted does not cause O(n^2) running time.
+ var pivotIndex = randomIntInRange(p, r);
+ var i = p - 1;
+
+ swap(ary, pivotIndex, r);
+ var pivot = ary[r];
+
+ // Immediately after `j` is incremented in this loop, the following hold
+ // true:
+ //
+ // * Every element in `ary[p .. i]` is less than or equal to the pivot.
+ //
+ // * Every element in `ary[i+1 .. j-1]` is greater than the pivot.
+ for (var j = p; j < r; j++) {
+ if (comparator(ary[j], pivot) <= 0) {
+ i += 1;
+ swap(ary, i, j);
+ }
+ }
+
+ swap(ary, i + 1, j);
+ var q = i + 1;
+
+ // (2) Recurse on each half.
+
+ doQuickSort(ary, comparator, p, q - 1);
+ doQuickSort(ary, comparator, q + 1, r);
+ }
+ }
+
+ /**
+ * Sort the given array in-place with the given comparator function.
+ *
+ * @param {Array} ary
+ * An array to sort.
+ * @param {function} comparator
+ * Function to use to compare two items.
+ */
+ exports.quickSort = function (ary, comparator) {
+ doQuickSort(ary, comparator, 0, ary.length - 1);
+ };
+
+
+/***/ },
+
+/***/ 482:
+/***/ function(module, exports, __webpack_require__) {
+
+ /* -*- Mode: js; js-indent-level: 2; -*- */
+ /*
+ * Copyright 2011 Mozilla Foundation and contributors
+ * Licensed under the New BSD license. See LICENSE or:
+ * http://opensource.org/licenses/BSD-3-Clause
+ */
+
+ var SourceMapGenerator = __webpack_require__(473).SourceMapGenerator;
+ var util = __webpack_require__(476);
+
+ // Matches a Windows-style `\r\n` newline or a `\n` newline used by all other
+ // operating systems these days (capturing the result).
+ var REGEX_NEWLINE = /(\r?\n)/;
+
+ // Newline character code for charCodeAt() comparisons
+ var NEWLINE_CODE = 10;
+
+ // Private symbol for identifying `SourceNode`s when multiple versions of
+ // the source-map library are loaded. This MUST NOT CHANGE across
+ // versions!
+ var isSourceNode = "$$$isSourceNode$$$";
+
+ /**
+ * SourceNodes provide a way to abstract over interpolating/concatenating
+ * snippets of generated JavaScript source code while maintaining the line and
+ * column information associated with the original source code.
+ *
+ * @param aLine The original line number.
+ * @param aColumn The original column number.
+ * @param aSource The original source's filename.
+ * @param aChunks Optional. An array of strings which are snippets of
+ * generated JS, or other SourceNodes.
+ * @param aName The original identifier.
+ */
+ function SourceNode(aLine, aColumn, aSource, aChunks, aName) {
+ this.children = [];
+ this.sourceContents = {};
+ this.line = aLine == null ? null : aLine;
+ this.column = aColumn == null ? null : aColumn;
+ this.source = aSource == null ? null : aSource;
+ this.name = aName == null ? null : aName;
+ this[isSourceNode] = true;
+ if (aChunks != null) this.add(aChunks);
+ }
+
+ /**
+ * Creates a SourceNode from generated code and a SourceMapConsumer.
+ *
+ * @param aGeneratedCode The generated code
+ * @param aSourceMapConsumer The SourceMap for the generated code
+ * @param aRelativePath Optional. The path that relative sources in the
+ * SourceMapConsumer should be relative to.
+ */
+ SourceNode.fromStringWithSourceMap =
+ function SourceNode_fromStringWithSourceMap(aGeneratedCode, aSourceMapConsumer, aRelativePath) {
+ // The SourceNode we want to fill with the generated code
+ // and the SourceMap
+ var node = new SourceNode();
+
+ // All even indices of this array are one line of the generated code,
+ // while all odd indices are the newlines between two adjacent lines
+ // (since `REGEX_NEWLINE` captures its match).
+ // Processed fragments are removed from this array, by calling `shiftNextLine`.
+ var remainingLines = aGeneratedCode.split(REGEX_NEWLINE);
+ var shiftNextLine = function() {
+ var lineContents = remainingLines.shift();
+ // The last line of a file might not have a newline.
+ var newLine = remainingLines.shift() || "";
+ return lineContents + newLine;
+ };
+
+ // We need to remember the position of "remainingLines"
+ var lastGeneratedLine = 1, lastGeneratedColumn = 0;
+
+ // The generate SourceNodes we need a code range.
+ // To extract it current and last mapping is used.
+ // Here we store the last mapping.
+ var lastMapping = null;
+
+ aSourceMapConsumer.eachMapping(function (mapping) {
+ if (lastMapping !== null) {
+ // We add the code from "lastMapping" to "mapping":
+ // First check if there is a new line in between.
+ if (lastGeneratedLine < mapping.generatedLine) {
+ // Associate first line with "lastMapping"
+ addMappingWithCode(lastMapping, shiftNextLine());
+ lastGeneratedLine++;
+ lastGeneratedColumn = 0;
+ // The remaining code is added without mapping
+ } else {
+ // There is no new line in between.
+ // Associate the code between "lastGeneratedColumn" and
+ // "mapping.generatedColumn" with "lastMapping"
+ var nextLine = remainingLines[0];
+ var code = nextLine.substr(0, mapping.generatedColumn -
+ lastGeneratedColumn);
+ remainingLines[0] = nextLine.substr(mapping.generatedColumn -
+ lastGeneratedColumn);
+ lastGeneratedColumn = mapping.generatedColumn;
+ addMappingWithCode(lastMapping, code);
+ // No more remaining code, continue
+ lastMapping = mapping;
+ return;
+ }
+ }
+ // We add the generated code until the first mapping
+ // to the SourceNode without any mapping.
+ // Each line is added as separate string.
+ while (lastGeneratedLine < mapping.generatedLine) {
+ node.add(shiftNextLine());
+ lastGeneratedLine++;
+ }
+ if (lastGeneratedColumn < mapping.generatedColumn) {
+ var nextLine = remainingLines[0];
+ node.add(nextLine.substr(0, mapping.generatedColumn));
+ remainingLines[0] = nextLine.substr(mapping.generatedColumn);
+ lastGeneratedColumn = mapping.generatedColumn;
+ }
+ lastMapping = mapping;
+ }, this);
+ // We have processed all mappings.
+ if (remainingLines.length > 0) {
+ if (lastMapping) {
+ // Associate the remaining code in the current line with "lastMapping"
+ addMappingWithCode(lastMapping, shiftNextLine());
+ }
+ // and add the remaining lines without any mapping
+ node.add(remainingLines.join(""));
+ }
+
+ // Copy sourcesContent into SourceNode
+ aSourceMapConsumer.sources.forEach(function (sourceFile) {
+ var content = aSourceMapConsumer.sourceContentFor(sourceFile);
+ if (content != null) {
+ if (aRelativePath != null) {
+ sourceFile = util.join(aRelativePath, sourceFile);
+ }
+ node.setSourceContent(sourceFile, content);
+ }
+ });
+
+ return node;
+
+ function addMappingWithCode(mapping, code) {
+ if (mapping === null || mapping.source === undefined) {
+ node.add(code);
+ } else {
+ var source = aRelativePath
+ ? util.join(aRelativePath, mapping.source)
+ : mapping.source;
+ node.add(new SourceNode(mapping.originalLine,
+ mapping.originalColumn,
+ source,
+ code,
+ mapping.name));
+ }
+ }
+ };
+
+ /**
+ * Add a chunk of generated JS to this source node.
+ *
+ * @param aChunk A string snippet of generated JS code, another instance of
+ * SourceNode, or an array where each member is one of those things.
+ */
+ SourceNode.prototype.add = function SourceNode_add(aChunk) {
+ if (Array.isArray(aChunk)) {
+ aChunk.forEach(function (chunk) {
+ this.add(chunk);
+ }, this);
+ }
+ else if (aChunk[isSourceNode] || typeof aChunk === "string") {
+ if (aChunk) {
+ this.children.push(aChunk);
+ }
+ }
+ else {
+ throw new TypeError(
+ "Expected a SourceNode, string, or an array of SourceNodes and strings. Got " + aChunk
+ );
+ }
+ return this;
+ };
+
+ /**
+ * Add a chunk of generated JS to the beginning of this source node.
+ *
+ * @param aChunk A string snippet of generated JS code, another instance of
+ * SourceNode, or an array where each member is one of those things.
+ */
+ SourceNode.prototype.prepend = function SourceNode_prepend(aChunk) {
+ if (Array.isArray(aChunk)) {
+ for (var i = aChunk.length-1; i >= 0; i--) {
+ this.prepend(aChunk[i]);
+ }
+ }
+ else if (aChunk[isSourceNode] || typeof aChunk === "string") {
+ this.children.unshift(aChunk);
+ }
+ else {
+ throw new TypeError(
+ "Expected a SourceNode, string, or an array of SourceNodes and strings. Got " + aChunk
+ );
+ }
+ return this;
+ };
+
+ /**
+ * Walk over the tree of JS snippets in this node and its children. The
+ * walking function is called once for each snippet of JS and is passed that
+ * snippet and the its original associated source's line/column location.
+ *
+ * @param aFn The traversal function.
+ */
+ SourceNode.prototype.walk = function SourceNode_walk(aFn) {
+ var chunk;
+ for (var i = 0, len = this.children.length; i < len; i++) {
+ chunk = this.children[i];
+ if (chunk[isSourceNode]) {
+ chunk.walk(aFn);
+ }
+ else {
+ if (chunk !== '') {
+ aFn(chunk, { source: this.source,
+ line: this.line,
+ column: this.column,
+ name: this.name });
+ }
+ }
+ }
+ };
+
+ /**
+ * Like `String.prototype.join` except for SourceNodes. Inserts `aStr` between
+ * each of `this.children`.
+ *
+ * @param aSep The separator.
+ */
+ SourceNode.prototype.join = function SourceNode_join(aSep) {
+ var newChildren;
+ var i;
+ var len = this.children.length;
+ if (len > 0) {
+ newChildren = [];
+ for (i = 0; i < len-1; i++) {
+ newChildren.push(this.children[i]);
+ newChildren.push(aSep);
+ }
+ newChildren.push(this.children[i]);
+ this.children = newChildren;
+ }
+ return this;
+ };
+
+ /**
+ * Call String.prototype.replace on the very right-most source snippet. Useful
+ * for trimming whitespace from the end of a source node, etc.
+ *
+ * @param aPattern The pattern to replace.
+ * @param aReplacement The thing to replace the pattern with.
+ */
+ SourceNode.prototype.replaceRight = function SourceNode_replaceRight(aPattern, aReplacement) {
+ var lastChild = this.children[this.children.length - 1];
+ if (lastChild[isSourceNode]) {
+ lastChild.replaceRight(aPattern, aReplacement);
+ }
+ else if (typeof lastChild === 'string') {
+ this.children[this.children.length - 1] = lastChild.replace(aPattern, aReplacement);
+ }
+ else {
+ this.children.push(''.replace(aPattern, aReplacement));
+ }
+ return this;
+ };
+
+ /**
+ * Set the source content for a source file. This will be added to the SourceMapGenerator
+ * in the sourcesContent field.
+ *
+ * @param aSourceFile The filename of the source file
+ * @param aSourceContent The content of the source file
+ */
+ SourceNode.prototype.setSourceContent =
+ function SourceNode_setSourceContent(aSourceFile, aSourceContent) {
+ this.sourceContents[util.toSetString(aSourceFile)] = aSourceContent;
+ };
+
+ /**
+ * Walk over the tree of SourceNodes. The walking function is called for each
+ * source file content and is passed the filename and source content.
+ *
+ * @param aFn The traversal function.
+ */
+ SourceNode.prototype.walkSourceContents =
+ function SourceNode_walkSourceContents(aFn) {
+ for (var i = 0, len = this.children.length; i < len; i++) {
+ if (this.children[i][isSourceNode]) {
+ this.children[i].walkSourceContents(aFn);
+ }
+ }
+
+ var sources = Object.keys(this.sourceContents);
+ for (var i = 0, len = sources.length; i < len; i++) {
+ aFn(util.fromSetString(sources[i]), this.sourceContents[sources[i]]);
+ }
+ };
+
+ /**
+ * Return the string representation of this source node. Walks over the tree
+ * and concatenates all the various snippets together to one string.
+ */
+ SourceNode.prototype.toString = function SourceNode_toString() {
+ var str = "";
+ this.walk(function (chunk) {
+ str += chunk;
+ });
+ return str;
+ };
+
+ /**
+ * Returns the string representation of this source node along with a source
+ * map.
+ */
+ SourceNode.prototype.toStringWithSourceMap = function SourceNode_toStringWithSourceMap(aArgs) {
+ var generated = {
+ code: "",
+ line: 1,
+ column: 0
+ };
+ var map = new SourceMapGenerator(aArgs);
+ var sourceMappingActive = false;
+ var lastOriginalSource = null;
+ var lastOriginalLine = null;
+ var lastOriginalColumn = null;
+ var lastOriginalName = null;
+ this.walk(function (chunk, original) {
+ generated.code += chunk;
+ if (original.source !== null
+ && original.line !== null
+ && original.column !== null) {
+ if(lastOriginalSource !== original.source
+ || lastOriginalLine !== original.line
+ || lastOriginalColumn !== original.column
+ || lastOriginalName !== original.name) {
+ map.addMapping({
+ source: original.source,
+ original: {
+ line: original.line,
+ column: original.column
+ },
+ generated: {
+ line: generated.line,
+ column: generated.column
+ },
+ name: original.name
+ });
+ }
+ lastOriginalSource = original.source;
+ lastOriginalLine = original.line;
+ lastOriginalColumn = original.column;
+ lastOriginalName = original.name;
+ sourceMappingActive = true;
+ } else if (sourceMappingActive) {
+ map.addMapping({
+ generated: {
+ line: generated.line,
+ column: generated.column
+ }
+ });
+ lastOriginalSource = null;
+ sourceMappingActive = false;
+ }
+ for (var idx = 0, length = chunk.length; idx < length; idx++) {
+ if (chunk.charCodeAt(idx) === NEWLINE_CODE) {
+ generated.line++;
+ generated.column = 0;
+ // Mappings end at eol
+ if (idx + 1 === length) {
+ lastOriginalSource = null;
+ sourceMappingActive = false;
+ } else if (sourceMappingActive) {
+ map.addMapping({
+ source: original.source,
+ original: {
+ line: original.line,
+ column: original.column
+ },
+ generated: {
+ line: generated.line,
+ column: generated.column
+ },
+ name: original.name
+ });
+ }
+ } else {
+ generated.column++;
+ }
+ }
+ });
+ this.walkSourceContents(function (sourceFile, sourceContent) {
+ map.setSourceContent(sourceFile, sourceContent);
+ });
+
+ return { code: generated.code, map: map };
+ };
+
+ exports.SourceNode = SourceNode;
+
+
+/***/ }
+
+/******/ });
+//# sourceMappingURL=source-map-worker.js.map
diff --git a/devtools/client/debugger/new/styles.css b/devtools/client/debugger/new/styles.css
new file mode 100644
index 000000000..479bee363
--- /dev/null
+++ b/devtools/client/debugger/new/styles.css
@@ -0,0 +1,1724 @@
+:root.theme-light,
+:root .theme-light {
+ --theme-search-overlays-semitransparent: rgba(221, 225, 228, 0.66);
+}
+
+* {
+ box-sizing: border-box;
+}
+
+html,
+body {
+ height: 100%;
+ margin: 0;
+ padding: 0;
+ width: 100%;
+}
+
+#mount {
+ display: flex;
+ height: 100%;
+}
+
+
+::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+ background: transparent;
+}
+
+::-webkit-scrollbar-track {
+ border-radius: 8px;
+ background: transparent;
+}
+
+::-webkit-scrollbar-thumb {
+ border-radius: 8px;
+ background: rgba(113,113,113,0.5);
+}
+
+:root.theme-dark .CodeMirror-scrollbar-filler {
+ background: transparent;
+}
+.landing-page {
+ flex: 1;
+ display: flex;
+ width: 100%;
+ height: 100%;
+ flex-direction: row;
+}
+
+.landing-page .sidebar {
+ display: flex;
+ background-color: var(--theme-tab-toolbar-background);
+ width: 200px;
+ height: 100%;
+ flex-direction: column;
+}
+
+.landing-page .sidebar h1 {
+ color: var(--theme-body-color);
+ font-size: 24px;
+ margin: 0;
+ line-height: 30px;
+ font-weight: normal;
+ padding: 40px 20px;
+}
+
+.landing-page .sidebar ul {
+ list-style: none;
+ padding: 0;
+ line-height: 30px;
+ font-size: 18px;
+}
+
+.landing-page .sidebar li {
+ padding: 5px 20px;
+}
+
+.landing-page .sidebar li.selected {
+ background: var(--theme-search-overlays-semitransparent);
+ transition: all 0.25s ease;
+}
+
+.landing-page .sidebar li:hover {
+ background: var(--theme-selection-background);
+ cursor: pointer;
+}
+
+.landing-page .sidebar li a {
+ color: var(--theme-body-color);
+}
+
+.landing-page .sidebar li:hover a {
+ color: var(--theme-selection-color);
+}
+
+.landing-page .panel {
+ display: flex;
+ flex: 1;
+ height: 100%;
+ overflow: auto;
+ flex-direction: column;
+}
+
+.landing-page .panel .title {
+ margin: 20px 40px;
+ width: calc(100% - 80px);
+ font-size: 16px;
+ border-bottom: 1px solid var(--theme-splitter-color);
+ height: 54px;
+}
+
+.landing-page .panel h2 {
+ color: var(--theme-body-color);
+ font-weight: normal;
+}
+
+.landing-page .panel .center {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+}
+
+.landing-page .panel .center .center-message {
+ margin: 40px;
+ font-size: 16px;
+ line-height: 25px;
+ padding: 10px;
+}
+
+.landing-page .center a {
+ color: var(--theme-highlight-bluegrey);
+ text-decoration: none;
+}
+
+.landing-page .tab-group {
+ margin: 40px;
+}
+
+.landing-page .tab-list {
+ list-style: none;
+ padding: 0px;
+ margin: 0px;
+}
+
+.landing-page .tab {
+ border-bottom: 1px solid var(--theme-splitter-color);
+ padding: 10px;
+ font-family: sans-serif;
+}
+
+.landing-page .tab:hover {
+ background-color: var(--theme-toolbar-background);
+ cursor: pointer;
+}
+
+.landing-page .tab-title {
+ line-height: 25px;
+ font-size: 16px;
+ color: var(--theme-highlight-bluegrey);
+}
+
+.landing-page .tab-url {
+ color: var(--theme-comment);
+}
+
+.landing-page .panel .center .footer-note {
+ flex: 1;
+ padding: 50px;
+ font-size: 14px;
+ color: var(--theme-comment);
+ bottom: 0;
+ position: absolute;
+}
+/* vim:set ts=2 sw=2 sts=2 et: */
+
+/* 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/. */
+
+:root.theme-light,
+:root .theme-light {
+ --theme-search-overlays-semitransparent: rgba(221, 225, 228, 0.66);
+ --theme-faded-tab-color: #7e7e7e;
+}
+
+:root.theme-dark,
+:root .theme-dark {
+ --theme-faded-tab-color: #6e7d8c;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+html,
+body {
+ height: 100%;
+ margin: 0;
+ padding: 0;
+ width: 100%;
+}
+
+#mount {
+ display: flex;
+ height: 100%;
+}
+
+.debugger {
+ display: flex;
+ flex: 1;
+ height: 100%;
+}
+
+.center-pane {
+ display: flex;
+ position: relative;
+ flex: 1;
+ background-color: var(--theme-tab-toolbar-background);
+ overflow: hidden;
+}
+
+.editor-container {
+ display: flex;
+ flex: 1;
+}
+
+.subsettings:hover {
+ cursor: pointer;
+}
+
+.search-container {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ display: flex;
+ z-index: 200;
+ background-color: var(--theme-search-overlays-semitransparent);
+}
+
+.search-container .autocomplete {
+ flex: 1;
+}
+
+.search-container .close-button {
+ width: 16px;
+ margin-top: 25px;
+ margin-right: 20px;
+}
+
+.welcomebox {
+ width: calc(100% - 1px);
+
+ /* Offsetting it by 30px for the sources-header area */
+ height: calc(100% - 30px);
+ position: absolute;
+ top: 30px;
+ left: 0;
+ padding: 50px 0;
+ text-align: center;
+ font-size: 1.25em;
+ color: var(--theme-comment-alt);
+ background-color: var(--theme-tab-toolbar-background);
+ font-weight: lighter;
+ z-index: 100;
+}
+menupopup {
+ position: fixed;
+ z-index: 10000;
+ background: white;
+ border: 1px solid #cccccc;
+ padding: 5px 0;
+ background: #f2f2f2;
+ border-radius: 5px;
+ color: #585858;
+ box-shadow: 0 0 4px 0 rgba(190, 190, 190, 0.8);
+ min-width: 130px;
+}
+
+menuitem {
+ display: block;
+ padding: 0 20px;
+ line-height: 20px;
+ font-weight: 500;
+ font-size: 13px;
+}
+
+menuitem:hover {
+ background: #3780fb;
+ color: white;
+ cursor: pointer;
+}
+
+menuseparator {
+ border-bottom: 1px solid #cacdd3;
+ width: 100%;
+ height: 5px;
+ display: block;
+ margin-bottom: 5px;
+}
+
+#contextmenu-mask.show {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 999;
+}
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+.split-box {
+ display: flex;
+ flex: 1;
+ min-width: 0;
+ height: 100%;
+ width: 100%;
+}
+
+.split-box.vert {
+ flex-direction: row;
+}
+
+.split-box.horz {
+ flex-direction: column;
+}
+
+.split-box > .uncontrolled {
+ display: flex;
+ flex: 1;
+ min-width: 0;
+ overflow: auto;
+}
+
+.split-box > .controlled {
+ display: flex;
+ overflow: auto;
+}
+
+.split-box > .splitter {
+ background-image: none;
+ border: 0;
+ border-style: solid;
+ border-color: transparent;
+ background-color: var(--theme-splitter-color);
+ background-clip: content-box;
+ position: relative;
+
+ box-sizing: border-box;
+
+ /* Positive z-index positions the splitter on top of its siblings and makes
+ it clickable on both sides. */
+ z-index: 1;
+}
+
+.split-box.vert > .splitter {
+ min-width: calc(var(--devtools-splitter-inline-start-width) +
+ var(--devtools-splitter-inline-end-width) + 1px);
+
+ border-left-width: var(--devtools-splitter-inline-start-width);
+ border-right-width: var(--devtools-splitter-inline-end-width);
+
+ margin-left: calc(-1 * var(--devtools-splitter-inline-start-width) - 1px);
+ margin-right: calc(-1 * var(--devtools-splitter-inline-end-width));
+
+ cursor: ew-resize;
+}
+
+.split-box.horz > .splitter {
+ min-height: calc(var(--devtools-splitter-top-width) +
+ var(--devtools-splitter-bottom-width) + 1px);
+
+ border-top-width: var(--devtools-splitter-top-width);
+ border-bottom-width: var(--devtools-splitter-bottom-width);
+
+ margin-top: calc(-1 * var(--devtools-splitter-top-width) - 1px);
+ margin-bottom: calc(-1 * var(--devtools-splitter-bottom-width));
+
+ cursor: ns-resize;
+}
+
+.split-box.disabled {
+ pointer-events: none;
+}
+
+/**
+ * Make sure splitter panels are not processing any mouse
+ * events. This is good for performance during splitter
+ * bar dragging.
+ */
+.split-box.dragging > .controlled,
+.split-box.dragging > .uncontrolled {
+ pointer-events: none;
+}
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+.theme-dark,
+.theme-light {
+ --number-color: var(--theme-highlight-green);
+ --string-color: var(--theme-highlight-orange);
+ --null-color: var(--theme-comment);
+ --object-color: var(--theme-body-color);
+ --caption-color: var(--theme-highlight-blue);
+ --location-color: var(--theme-content-color1);
+ --source-link-color: var(--theme-highlight-blue);
+ --node-color: var(--theme-highlight-bluegrey);
+ --reference-color: var(--theme-highlight-purple);
+}
+
+.theme-firebug {
+ --number-color: #000088;
+ --string-color: #FF0000;
+ --null-color: #787878;
+ --object-color: DarkGreen;
+ --caption-color: #444444;
+ --location-color: #555555;
+ --source-link-color: blue;
+ --node-color: rgb(0, 0, 136);
+ --reference-color: rgb(102, 102, 255);
+}
+
+/******************************************************************************/
+
+.objectLink:hover {
+ cursor: pointer;
+ text-decoration: underline;
+}
+
+.inline {
+ display: inline;
+ white-space: normal;
+}
+
+.objectBox-object {
+ font-weight: bold;
+ color: var(--object-color);
+ white-space: pre-wrap;
+}
+
+.objectBox-string,
+.objectBox-text,
+.objectLink-textNode,
+.objectBox-table {
+ white-space: pre-wrap;
+}
+
+.objectBox-number,
+.objectLink-styleRule,
+.objectLink-element,
+.objectLink-textNode,
+.objectBox-array > .length {
+ color: var(--number-color);
+}
+
+.objectBox-string {
+ color: var(--string-color);
+}
+
+.objectLink-function,
+.objectBox-stackTrace,
+.objectLink-profile {
+ color: var(--object-color);
+}
+
+.objectLink-Location {
+ font-style: italic;
+ color: var(--location-color);
+}
+
+.objectBox-null,
+.objectBox-undefined,
+.objectBox-hint,
+.logRowHint {
+ font-style: italic;
+ color: var(--null-color);
+}
+
+.objectLink-sourceLink {
+ position: absolute;
+ right: 4px;
+ top: 2px;
+ padding-left: 8px;
+ font-weight: bold;
+ color: var(--source-link-color);
+}
+
+/******************************************************************************/
+
+.objectLink-event,
+.objectLink-eventLog,
+.objectLink-regexp,
+.objectLink-object,
+.objectLink-Date {
+ font-weight: bold;
+ color: var(--object-color);
+ white-space: pre-wrap;
+}
+
+/******************************************************************************/
+
+.objectLink-object .nodeName,
+.objectLink-NamedNodeMap .nodeName,
+.objectLink-NamedNodeMap .objectEqual,
+.objectLink-NamedNodeMap .arrayLeftBracket,
+.objectLink-NamedNodeMap .arrayRightBracket,
+.objectLink-Attr .attrEqual,
+.objectLink-Attr .attrTitle {
+ color: var(--node-color);
+}
+
+.objectLink-object .nodeName {
+ font-weight: normal;
+}
+
+/******************************************************************************/
+
+.objectLeftBrace,
+.objectRightBrace,
+.arrayLeftBracket,
+.arrayRightBracket {
+ cursor: pointer;
+ font-weight: bold;
+}
+
+.objectLeftBrace,
+.arrayLeftBracket {
+ margin-right: 4px;
+}
+
+.objectRightBrace,
+.arrayRightBracket {
+ margin-left: 4px;
+}
+
+/******************************************************************************/
+/* Cycle reference*/
+
+.objectLink-Reference {
+ font-weight: bold;
+ color: var(--reference-color);
+}
+
+.objectBox-array > .objectTitle {
+ font-weight: bold;
+ color: var(--object-color);
+}
+
+.caption {
+ font-weight: bold;
+ color: var(--caption-color);
+}
+
+/******************************************************************************/
+/* Themes */
+
+.theme-dark .objectBox-null,
+.theme-dark .objectBox-undefined,
+.theme-light .objectBox-null,
+.theme-light .objectBox-undefined {
+ font-style: normal;
+}
+
+.theme-dark .objectBox-object,
+.theme-light .objectBox-object {
+ font-weight: normal;
+ white-space: pre-wrap;
+}
+
+.theme-dark .caption,
+.theme-light .caption {
+ font-weight: normal;
+}
+/* vim:set ts=2 sw=2 sts=2 et: */
+
+/* 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/. */
+
+.search-container {
+ position: absolute;
+ top: 30px;
+ left: 0;
+ width: calc(100% - 1px);
+ height: calc(100% - 31px);
+ display: flex;
+ z-index: 200;
+ background-color: var(--theme-body-background);
+}
+
+.search-container .autocomplete {
+ flex: 1;
+}
+
+.searchinput-container {
+ display: flex;
+}
+
+.searchinput-container .close-btn-big {
+ border-bottom: 1px solid var(--theme-splitter-color);
+}
+
+.autocomplete {
+ width: 100%;
+}
+
+.autocomplete .results * {
+ -moz-user-select: none;
+ user-select: none;
+}
+
+.autocomplete .results-summary {
+ margin: 10px;
+}
+
+.autocomplete ul {
+ list-style: none;
+ width: 100%;
+ max-height: calc(100% - 32px);
+ margin: 0px;
+ padding: 0px;
+ overflow: auto;
+}
+
+.autocomplete li {
+ border: 2px solid var(--theme-splitter-color);
+ background-color: var(--theme-tab-toolbar-background);
+ padding: 10px;
+ margin: 10px;
+}
+
+.autocomplete li:hover {
+ background: var(--theme-tab-toolbar-background);
+ cursor: pointer;
+}
+
+.autocomplete li.selected {
+ border: 2px solid var(--theme-selection-background);
+}
+
+.autocomplete li .title {
+ line-height: 1.5em;
+ word-break: break-all;
+}
+
+.autocomplete li .subtitle {
+ line-height: 1.5em;
+ color: grey;
+ word-break: break-all;
+}
+
+.autocomplete input {
+ width: 100%;
+ border: none;
+ background-color: var(--theme-body-background);
+ color: var(--theme-comment);
+ border-bottom: 1px solid var(--theme-splitter-color);
+ outline: none;
+ line-height: 30px;
+ font-size: 14px;
+ height: 40px;
+ padding-left: 30px;
+}
+
+.autocomplete input::placeholder {
+ color: var(--theme-body-color-inactive);
+}
+
+.autocomplete .magnifying-glass svg {
+ width: 16px;
+ position: absolute;
+ top: 12px;
+ left: 10px;
+}
+
+.autocomplete.focused .magnifying-glass path,
+.autocomplete.focused .magnifying-glass ellipse {
+ stroke: var(--theme-highlight-blue);
+}
+
+.autocomplete .magnifying-glass path,
+.autocomplete .magnifying-glass ellipse {
+ stroke: var(--theme-splitter-color);
+}
+
+.autocomplete .no-result-msg {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ height: 100%;
+ color: var(--theme-graphs-full-red);
+ font-size: 24px;
+}
+
+.autocomplete .no-result-msg .sad-face {
+ width: 24px;
+ margin-right: 4px;
+ line-height: 0;
+}
+
+.autocomplete .no-result-msg .sad-face svg {
+ fill: var(--theme-graphs-full-red);
+}
+.close-btn path {
+ fill: var(--theme-body-color);
+}
+
+.close-btn .close {
+ cursor: pointer;
+ width: 12px;
+ height: 12px;
+ padding: 2px;
+ text-align: center;
+ margin-top: 2px;
+ line-height: 5px;
+ transition: all 0.25s easeinout;
+}
+
+.close-btn .close svg {
+ width: 6px;
+}
+
+.close-btn .close:hover {
+ background: var(--theme-selection-background);
+ border-radius: 2px;
+}
+
+.close-btn .close:hover path {
+ fill: white;
+}
+
+.close-btn-big {
+ padding: 13px;
+ width: 40px;
+ height: 40px;
+}
+
+.close-btn-big path {
+ fill: var(--theme-body-color);
+}
+
+.close-btn-big .close {
+ cursor: pointer;
+ display: inline-block;
+ padding: 2px;
+ text-align: center;
+ transition: all 0.25s easeinout;
+ line-height: 100%;
+ width: 16px;
+ height: 16px;
+}
+
+.close-btn-big .close svg {
+ width: 9px;
+ height: 9px;
+}
+
+.close-btn-big .close:hover {
+ background: var(--theme-selection-background);
+ border-radius: 2px;
+}
+
+.close-btn-big .close:hover path {
+ fill: white;
+}
+.tree {
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ -o-user-select: none;
+ user-select: none;
+
+ flex: 1;
+ white-space: nowrap;
+ overflow: auto;
+}
+
+.tree button {
+ display: block;
+}
+
+.tree .node {
+ padding: 2px 5px;
+ position: relative;
+}
+
+.tree .node.focused {
+ color: white;
+ background-color: var(--theme-selection-background);
+}
+
+html:not([dir="rtl"]) .tree .node > div {
+ margin-left: 10px;
+}
+
+html[dir="rtl"] .tree .node > div {
+ margin-right: 10px;
+}
+
+.tree .node.focused svg {
+ fill: white;
+}
+
+.tree-node button {
+ position: fixed;
+}
+.sources-panel {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+}
+
+.sources-panel * {
+ -moz-user-select: none;
+ user-select: none;
+}
+
+.sources-header {
+ height: 30px;
+ border-bottom: 1px solid var(--theme-splitter-color);
+ padding-top: 0px;
+ padding-bottom: 0px;
+ line-height: 30px;
+ font-size: 1.2em;
+ display: flex;
+ align-items: baseline;
+ justify-content: space-between;
+ -moz-user-select: none;
+ user-select: none;
+}
+
+html:not([dir="rtl"]) .sources-header {
+ padding-left: 10px;
+}
+
+html[dir="rtl"] .sources-header {
+ padding-right: 10px;
+}
+
+.sources-header-info {
+ font-size: 12px;
+ color: var(--theme-comment-alt);
+ font-weight: lighter;
+ white-space: nowrap;
+}
+
+html:not([dir="rtl"]) .sources-header-info {
+ padding-right: 10px;
+ float: right;
+}
+
+html[dir="rtl"] .sources-header-info {
+ padding-left: 10px;
+ float: left;
+}
+
+.sources-list {
+ flex: 1;
+ display: flex;
+ overflow: hidden;
+}
+
+.arrow,
+.folder,
+.domain,
+.file,
+.worker {
+ fill: var(--theme-splitter-color);
+}
+
+.domain,
+.file,
+.worker {
+ position: relative;
+ top: 1px;
+}
+
+.worker,
+.folder {
+ position: relative;
+ top: 2px;
+}
+
+.domain svg,
+.folder svg,
+.worker svg {
+ width: 15px;
+}
+
+.file svg {
+ width: 13px;
+}
+
+html:not([dir="rtl"]) .file svg,
+html:not([dir="rtl"]) .domain svg,
+html:not([dir="rtl"]) .folder svg,
+html:not([dir="rtl"]) .worker svg {
+ margin-right: 5px;
+}
+
+html[dir="rtl"] .file svg,
+html[dir="rtl"] .domain svg,
+html[dir="rtl"] .folder svg,
+html[dir="rtl"] .worker svg {
+ margin-left: 5px;
+}
+
+.tree {
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ -o-user-select: none;
+ user-select: none;
+
+ flex: 1;
+ white-space: nowrap;
+ overflow: auto;
+}
+
+.tree button {
+ display: block;
+}
+
+.tree .node {
+ padding: 2px 5px;
+ position: relative;
+ cursor: pointer;
+}
+
+.tree .node:hover {
+ background: var(--theme-tab-toolbar-background);
+}
+
+.tree .node.focused {
+ color: white;
+ background-color: var(--theme-selection-background);
+}
+
+.tree .node > div {
+ margin-left: 10px;
+}
+
+.tree .node.focused svg {
+ fill: white;
+}
+
+.sources-list .tree-node button {
+ position: fixed;
+}
+
+.source-footer {
+ background: var(--theme-body-background);
+ border-top: 1px solid var(--theme-splitter-color);
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 1px;
+ opacity: 1;
+ z-index: 100;
+ -moz-user-select: none;
+ user-select: none;
+}
+
+html:not([dir="rtl"]) .source-footer .command-bar {
+ float: right;
+}
+
+html[dir="rtl"] .source-footer .command-bar {
+ float: left;
+}
+
+.source-footer .command-bar * {
+ -moz-user-select: none;
+ user-select: none;
+}
+
+.command-bar > span {
+ cursor: pointer;
+ width: 1em;
+ height: 1.1em;
+ display: inline-block;
+ text-align: center;
+ transition: opacity 200ms;
+}
+
+html:not([dir="rtl"]) .command-bar > span {
+ margin-right: 0.7em;
+}
+
+html[dir="rtl"] .command-bar > span {
+ margin-left: 0.7em;
+}
+
+.source-footer .prettyPrint.pretty {
+ stroke: var(--theme-highlight-blue);
+}
+
+.source-footer input:focus {
+ border-color: var(--theme-highlight-blue);
+ outline: none;
+}
+
+.source-footer input {
+ line-height: 16px;
+ margin: 7px;
+ border-radius: 2px;
+ border: 1px solid var(--theme-splitter-color);
+ padding-left: 4px;
+ font-size: 10px;
+}
+.search-bar {
+ width: calc(100% - 1px);
+ height: 40px;
+ background: white;
+ border-bottom: 1px solid var(--theme-splitter-color);
+ display: flex;
+}
+
+.search-bar i {
+ display: block;
+ padding: 13px 0 0 13px;
+ width: 40px;
+}
+
+.search-bar i svg {
+ width: 16px;
+}
+
+.search-bar input {
+ border: none;
+ line-height: 30px;
+ font-size: 14px;
+ background-color: var(--theme-body-background);
+ color: var(--theme-comment);
+ width: calc(100% - 38px);
+ flex: 1;
+}
+
+.search-bar .magnifying-glass {
+ background-color: var(--theme-body-background);
+ width: 40px;
+}
+
+.search-bar .magnifying-glass path,
+.search-bar .magnifying-glass ellipse {
+ stroke: var(--theme-splitter-color);
+}
+
+.search-bar input::placeholder {
+ color: var(--theme-body-color-inactive);
+}
+
+.search-bar input:focus {
+ outline-width: 0;
+}
+
+.search-bar input.empty {
+ color: var(--theme-highlight-orange);
+}
+
+.search-bar .summary {
+ line-height: 40px;
+ padding-right: 10px;
+ color: var(--theme-body-color-inactive);
+}
+/* vim:set ts=2 sw=2 sts=2 et: */
+
+/* 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/. */
+
+/**
+ * There's a known codemirror flex issue with chrome that this addresses.
+ * BUG https://github.com/devtools-html/debugger.html/issues/63
+ */
+.editor-wrapper {
+ position: absolute;
+ height: calc(100% - 31px);
+ width: 100%;
+ top: 30px;
+ left: 0px;
+}
+
+html[dir="rtl"] .editor-mount {
+ direction: ltr;
+}
+
+.editor-wrapper .breakpoints {
+ position: absolute;
+ top: 0;
+ left: 0;
+}
+
+.editor.new-breakpoint svg {
+ fill: var(--theme-selection-background);
+ width: 60px;
+ height: 14px;
+ position: absolute;
+ top: 0px;
+ right: -4px;
+}
+
+.new-breakpoint.has-condition svg {
+ fill: var(--theme-graphs-yellow);
+}
+
+.editor.new-breakpoint.breakpoint-disabled svg {
+ opacity: 0.3;
+}
+
+.CodeMirror {
+ width: 100%;
+ height: 100%;
+}
+
+.editor-wrapper .editor-mount {
+ width: 100%;
+ height: calc(100% - 32px);
+ background-color: var(--theme-body-background);
+}
+
+.search-bar ~ .editor-mount {
+ height: calc(100% - 72px);
+}
+
+.CodeMirror-linenumber {
+ font-size: 11px;
+ line-height: 14px;
+}
+
+/* set the linenumber white when there is a breakpoint */
+.new-breakpoint .CodeMirror-gutter-wrapper .CodeMirror-linenumber {
+ color: white;
+}
+
+/* move the breakpoint below the linenumber */
+.new-breakpoint .CodeMirror-gutter-elt:last-child {
+ z-index: 0;
+}
+
+.editor-wrapper .CodeMirror-line {
+ font-size: 11px;
+ line-height: 14px;
+}
+
+.debug-line .CodeMirror-line {
+ background-color: var(--breakpoint-active-color) !important;
+}
+
+/* Don't display the highlight color since the debug line
+ is already highlighted */
+.debug-line .CodeMirror-activeline-background {
+ display: none;
+}
+
+.highlight-line .CodeMirror-line {
+ animation: fade-highlight-out 1.5s normal forwards;
+}
+
+@keyframes fade-highlight-out {
+ 0% { background-color: var(--theme-highlight-gray); }
+ 100% { background-color: transparent; }
+}
+
+.welcomebox {
+ width: calc(100% - 1px);
+
+ /* Offsetting it by 30px for the sources-header area */
+ height: calc(100% - 30px);
+ position: absolute;
+ top: 30px;
+ left: 0;
+ padding: 50px 0;
+ text-align: center;
+ font-size: 1.25em;
+ color: var(--theme-comment-alt);
+ background-color: var(--theme-tab-toolbar-background);
+ font-weight: lighter;
+ z-index: 100;
+ -moz-user-select: none;
+ user-select: none;
+}
+
+.conditional-breakpoint-panel {
+ cursor: initial;
+ margin: 1em 0;
+ position: relative;
+ background: var(--theme-toolbar-background);
+ border-top: 1px solid var(--theme-splitter-color);
+ border-bottom: 1px solid var(--theme-splitter-color);
+}
+
+.conditional-breakpoint-panel input {
+ margin: 5px 10px;
+ width: calc(100% - 2em);
+ border: none;
+ background: var(--theme-toolbar-background);
+ font-size: 14px;
+ color: var(--theme-comment);
+ line-height: 30px;
+}
+
+.conditional-breakpoint-panel input:focus {
+ outline-width: 0;
+}
+.breakpoints-list * {
+ -moz-user-select: none;
+ user-select: none;
+}
+
+.breakpoints-list .breakpoint {
+ font-size: 12px;
+ color: var(--theme-content-color1);
+ padding: 0.5em 1px;
+ line-height: 1em;
+ position: relative;
+ border-left: 4px solid transparent;
+ transition: all 0.25s ease;
+}
+
+.breakpoints-list .breakpoint:last-of-type {
+ padding-bottom: 0.45em;
+}
+
+.breakpoints-list .breakpoint.paused {
+ background-color: var(--theme-toolbar-background-alt);
+ border-color: var(--breakpoint-active-color);
+}
+
+.breakpoints-list .breakpoint.disabled .breakpoint-label {
+ color: var(--theme-content-color3);
+ transition: color 0.5s linear;
+}
+
+.breakpoints-list .breakpoint:hover {
+ cursor: pointer;
+ background-color: var(--theme-search-overlays-semitransparent);
+}
+
+.breakpoints-list .breakpoint.paused:hover {
+ border-color: var(--breakpoint-active-color-hover);
+}
+
+.breakpoints-list .breakpoint-checkbox {
+ margin-left: 0;
+}
+
+.breakpoints-list .breakpoint-label {
+ display: inline-block;
+ padding-left: 2px;
+ padding-bottom: 4px;
+}
+
+.breakpoints-list .pause-indicator {
+ flex: 0 1 content;
+ order: 3;
+}
+
+.breakpoint-snippet {
+ color: var(--theme-comment);
+ padding-left: 18px;
+}
+
+.breakpoint .close-btn {
+ position: absolute;
+ right: 6px;
+ top: 12px;
+}
+
+.breakpoint .close {
+ display: none;
+}
+
+.breakpoint:hover .close {
+ display: block;
+}
+.input-expression {
+ width: 100%;
+ padding: 5px;
+ margin: 0px;
+ border: none;
+ cursor: hand;
+}
+
+.expression-container {
+ border: 1px;
+ padding: 5px 2px 5px 5px;
+ margin: 1px;
+ width: 100%;
+ color: var(--theme-body-color) !important;
+ background-color: var(--theme-tab-toolbar-background);
+}
+
+.expression-container:hover {
+ background-color: var(--theme-selection-background);
+ color: var(--theme-body-background) !important;
+}
+
+.expression-output-container .close-btn {
+ width: 6px;
+ height: 6px;
+ float: right;
+ margin-right: 6px;
+ display: block;
+ cursor: pointer;
+}
+
+.expression-input {
+ cursor: pointer;
+ max-width: 50%;
+}
+
+.expression-value {
+ overflow-x: scroll;
+ color: var(--theme-content-color2);
+ max-width: 50% !important;
+}
+
+.expression-error {
+ color: var(--theme-highlight-red);
+}
+.arrow svg {
+ fill: var(--theme-splitter-color);
+ margin-top: 3px;
+ transition: transform 0.25s ease;
+ width: 10px;
+}
+
+html:not([dir="rtl"]) .arrow svg {
+ margin-right: 5px;
+ transform: rotate(-90deg);
+}
+
+html[dir="rtl"] .arrow svg {
+ margin-left: 5px;
+ transform: rotate(90deg);
+}
+
+/* TODO (Amit): html is just for specificity. keep it like this? */
+html .arrow.expanded svg {
+ transform: rotate(0deg);
+}
+
+.arrow.hidden {
+ visibility: hidden;
+}
+
+.object-label {
+ color: var(--theme-highlight-blue);
+}
+
+.objectBox-object,
+.objectBox-string,
+.objectBox-text,
+.objectBox-table,
+.objectLink-textNode,
+.objectLink-event,
+.objectLink-eventLog,
+.objectLink-regexp,
+.objectLink-object,
+.objectLink-Date,
+.theme-dark .objectBox-object,
+.theme-light .objectBox-object {
+ white-space: nowrap;
+}
+
+.scopes-list .tree-node {
+ overflow: hidden;
+}
+.frames ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+.frames ul li {
+ cursor: pointer;
+ padding: 7px 10px 7px 21px;
+ clear: both;
+ overflow: hidden;
+}
+
+/* Style the focused call frame like so:
+.frames ul li:focus {
+ border: 3px solid red;
+}
+*/
+
+.frames ul li * {
+ -moz-user-select: none;
+ user-select: none;
+}
+
+.frames ul li:nth-of-type(2n) {
+ background-color: var(--theme-tab-toolbar-background);
+}
+
+.frames .location {
+ float: right;
+ color: var(--theme-comment);
+ font-weight: lighter;
+}
+
+.frames .title {
+ float: left;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ margin-right: 1em;
+}
+
+.frames ul li.selected,
+.frames ul li.selected .location {
+ background-color: var(--theme-selection-background);
+ color: white;
+}
+
+.show-more {
+ cursor: pointer;
+ text-align: center;
+ padding: 8px 0px;
+ border-top: 1px solid var(--theme-splitter-color);
+ background-color: var(--theme-tab-toolbar-background);
+}
+
+.show-more:hover {
+ background-color: var(--theme-search-overlays-semitransparent);
+}
+.accordion {
+ background-color: var(--theme-body-background);
+ width: 100%;
+}
+
+.accordion ._header {
+ background-color: var(--theme-toolbar-background);
+ border-bottom: 1px solid var(--theme-splitter-color);
+ cursor: pointer;
+ font-size: 12px;
+ padding: 5px;
+ transition: all 0.25s ease;
+ width: 100%;
+
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ -o-user-select: none;
+ user-select: none;
+}
+
+.accordion ._header:hover {
+ background-color: var(--theme-search-overlays-semitransparent);
+}
+
+.accordion ._header:hover svg {
+ fill: var(--theme-comment-alt);
+}
+
+.accordion ._content {
+ border-bottom: 1px solid var(--theme-splitter-color);
+ font-size: 12px;
+}
+.right-sidebar {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ white-space: nowrap;
+}
+
+.right-siderbar * {
+ -moz-user-select: none;
+ user-select: none;
+}
+
+.right-sidebar .accordion {
+ overflow-y: auto;
+ overflow-x: hidden;
+}
+
+.right-sidebar .command-bar {
+ border-bottom: 1px solid var(--theme-splitter-color);
+}
+
+.command-bar {
+ height: 30px;
+}
+
+html:not([dir="rtl"]) .command-bar {
+ padding: 8px 5px 10px 1px;
+}
+
+html[dir="rtl"] .command-bar {
+ padding: 8px 1px 10px 5px;
+}
+
+.command-bar > span {
+ cursor: pointer;
+ width: 16px;
+ height: 17px;
+ display: inline-block;
+ text-align: center;
+ transition: all 0.25s ease;
+}
+
+:root.theme-dark .command-bar > span {
+ fill: var(--theme-body-color);
+}
+
+:root.theme-dark .command-bar > span:hover {
+ fill: var(--theme-selection-color);
+}
+
+html:not([dir="rtl"]) .command-bar > span {
+ margin-right: 0.7em;
+}
+
+html[dir="rtl"] .command-bar > span {
+ margin-left: 0.7em;
+}
+
+.command-bar > span.disabled {
+ opacity: 0.3;
+ cursor: default;
+}
+
+html:not([dir="rtl"]) .command-bar .stepOut {
+ margin-right: 2em;
+}
+
+html[dir="rtl"] .command-bar .stepOut {
+ margin-left: 2em;
+}
+
+.command-bar .subSettings {
+ float: right;
+}
+
+.pane {
+ color: var(--theme-body-color);
+}
+
+.pane .pane-info {
+ font-style: italic;
+ text-align: center;
+ padding: 0.5em;
+ -moz-user-select: none;
+ user-select: none;
+}
+
+.toggleBreakpoints.breakpoints-disabled path {
+ stroke: var(--theme-highlight-blue);
+}
+
+span.pause-exceptions.uncaught {
+ stroke: var(--theme-highlight-purple);
+}
+
+span.pause-exceptions.all {
+ stroke: var(--theme-highlight-blue);
+}
+.source-header {
+ border-bottom: 1px solid var(--theme-splitter-color);
+ height: 30px;
+ flex: 1;
+}
+
+.source-header * {
+ -moz-user-select: none;
+ user-select: none;
+}
+
+.source-tabs {
+ min-width: 50px;
+ max-width: calc(100% - 60px);
+ overflow: hidden;
+ float: left;
+}
+
+.source-header .new-tab-btn {
+ width: 16px;
+ display: inline-block;
+ position: relative;
+ top: 4px;
+ margin: 4px;
+ line-height: 0;
+}
+
+.source-header .new-tab-btn path {
+ fill: var(--theme-splitter-color);
+}
+
+.source-header .new-tab-btn:hover path {
+ fill: var(--theme-comment);
+}
+
+.source-tab {
+ background-color: var(--theme-toolbar-background-alt);
+ color: var(--theme-faded-tab-color);
+ border: 1px solid var(--theme-splitter-color);
+ border-top-left-radius: 2px;
+ border-top-right-radius: 2px;
+ height: 23px;
+ line-height: 20px;
+ display: inline-block;
+ border-bottom: none;
+ position: relative;
+ transition: all 0.25s ease;
+ min-width: 40px;
+ overflow: hidden;
+}
+
+html:not([dir="rtl"]) .source-tab {
+ padding: 2px 20px 2px 12px;
+ margin: 6px 0 0 8px;
+}
+
+html[dir="rtl"] .source-tab {
+ padding: 2px 12px 2px 20px;
+ margin: 6px 8px 0 0;
+}
+
+.source-tab:hover {
+ background: var(--theme-toolbar-background);
+ cursor: pointer;
+}
+
+.source-tab.active {
+ color: var(--theme-body-color);
+ background-color: var(--theme-body-background);
+}
+
+.source-tab path {
+ fill: var(--theme-faded-tab-color);
+}
+
+.source-tab.active path {
+ fill: var(--theme-body-color);
+}
+
+.source-tab .close-btn {
+ position: absolute;
+ top: 3px;
+}
+
+.source-tab .filename {
+ text-overflow: ellipsis;
+ overflow: hidden;
+}
+
+html:not([dir="rtl"]) .source-tab .close-btn {
+ right: 4px;
+}
+
+html[dir="rtl"] .source-tab .close-btn {
+ left: 4px;
+}
+
+.source-tab .close {
+ display: none;
+}
+
+.source-tab:hover .close {
+ display: block;
+}
+.dropdown {
+ background: var(--theme-body-background);
+ border: 1px solid var(--theme-splitter-color);
+ box-shadow: 0 4px 4px 0 var(--theme-search-overlays-semitransparent);
+ max-height: 300px;
+ position: absolute;
+ right: 8px;
+ top: 35px;
+ width: 150px;
+ z-index: 1000;
+}
+
+.dropdown-button {
+ position: absolute;
+ right: 12px;
+ top: 5px;
+ font-size: 16px;
+ color: var(--theme-body-color);
+ cursor: pointer;
+}
+
+.dropdown li {
+ transition: all 0.25s ease;
+ padding: 2px 10px 10px 5px;
+ overflow: hidden;
+ height: 30px;
+ text-overflow: ellipsis;
+}
+
+.dropdown li:hover {
+ background-color: var(--theme-search-overlays-semitransparent);
+ cursor: pointer;
+}
+
+.dropdown ul {
+ list-style: none;
+ line-height: 2em;
+ font-size: 1em;
+ margin: 0;
+ padding: 0;
+}
+
+.dropdown-mask {
+ position: fixed;
+ width: 100%;
+ height: 100%;
+ background: transparent;
+ z-index: 999;
+ left: 0;
+ top: 0;
+}
+
+/*# sourceMappingURL=styles.css.map*/ \ No newline at end of file
diff --git a/devtools/client/debugger/new/test/mochitest/.eslintrc b/devtools/client/debugger/new/test/mochitest/.eslintrc
new file mode 100644
index 000000000..017b921f8
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/.eslintrc
@@ -0,0 +1,80 @@
+{
+ "globals": {
+ "add_task": false,
+ "Assert": false,
+ "BrowserTestUtils": false,
+ "content": false,
+ "ContentTask": false,
+ "ContentTaskUtils": false,
+ "EventUtils": false,
+ "executeSoon": false,
+ "expectUncaughtException": false,
+ "export_assertions": false,
+ "extractJarToTmp": false,
+ "finish": false,
+ "getJar": false,
+ "getRootDirectory": false,
+ "getTestFilePath": false,
+ "gBrowser": false,
+ "gTestPath": false,
+ "info": false,
+ "is": false,
+ "isnot": false,
+ "ok": false,
+ "registerCleanupFunction": false,
+ "requestLongerTimeout": false,
+ "SimpleTest": false,
+ "SpecialPowers": false,
+ "TestUtils": false,
+ "thisTestLeaksUncaughtRejectionsAndShouldBeFixed": false,
+ "todo": false,
+ "todo_is": false,
+ "todo_isnot": false,
+ "waitForClipboard": false,
+ "waitForExplicitFinish": false,
+ "waitForFocus": false,
+
+ // Globals introduced in debugger-specific head.js
+ "promise": false,
+ "BrowserToolboxProcess": false,
+ "OS": false,
+ "waitForNextDispatch": false,
+ "waitForDispatch": false,
+ "waitForThreadEvents": false,
+ "waitForState": false,
+ "waitForElement": false,
+ "waitForPaused": false,
+ "waitForSources": false,
+ "isPaused": false,
+ "assertPausedLocation": false,
+ "assertHighlightLocation": false,
+ "createDebuggerContext": false,
+ "initDebugger": false,
+ "invokeInTab": false,
+ "findSource": false,
+ "findElement": false,
+ "findElementWithSelector": false,
+ "findAllElements": false,
+ "openNewTabAndToolbox": false,
+ "selectSource": false,
+ "stepOver": false,
+ "stepIn": false,
+ "stepOut": false,
+ "resume": false,
+ "reload": false,
+ "navigate": false,
+ "removeBreakpoint": false,
+ "addBreakpoint": false,
+ "toggleCallStack": false,
+ "toggleScopes": false,
+ "isVisibleWithin": false,
+ "clickElement": false,
+ "rightClickElement": false,
+ "selectMenuItem": false,
+ "togglePauseOnExceptions": false,
+ "type": false,
+ "pressKey": false,
+ "EXAMPLE_URL": false,
+ "waitUntil": false
+ }
+}
diff --git a/devtools/client/debugger/new/test/mochitest/browser.ini b/devtools/client/debugger/new/test/mochitest/browser.ini
new file mode 100644
index 000000000..d0e40a4a7
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/browser.ini
@@ -0,0 +1,60 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+skip-if = (os == 'linux' && debug && bits == 32)
+support-files =
+ head.js
+ !/devtools/client/commandline/test/helpers.js
+ !/devtools/client/framework/test/shared-head.js
+ examples/bundle.js
+ examples/bundle.js.map
+ examples/doc-scripts.html
+ examples/doc-script-switching.html
+ examples/doc-exceptions.html
+ examples/doc-iframes.html
+ examples/doc-frames.html
+ examples/doc-debugger-statements.html
+ examples/doc-minified.html
+ examples/doc-sourcemaps.html
+ examples/doc-sourcemap-bogus.html
+ examples/doc-sources.html
+ examples/bogus-map.js
+ examples/entry.js
+ examples/exceptions.js
+ examples/long.js
+ examples/math.min.js
+ examples/nested/nested-source.js
+ examples/opts.js
+ examples/output.js
+ examples/simple1.js
+ examples/simple2.js
+ examples/frames.js
+ examples/script-switching-02.js
+ examples/script-switching-01.js
+ examples/times2.js
+
+[browser_dbg-breaking.js]
+[browser_dbg-breaking-from-console.js]
+[browser_dbg-breakpoints.js]
+[browser_dbg-breakpoints-cond.js]
+[browser_dbg-call-stack.js]
+[browser_dbg-scopes.js]
+[browser_dbg-chrome-create.js]
+[browser_dbg-chrome-debugging.js]
+[browser_dbg-console.js]
+[browser_dbg-debugger-buttons.js]
+[browser_dbg-editor-gutter.js]
+[browser_dbg-editor-mode.js]
+[browser_dbg-editor-select.js]
+[browser_dbg-editor-highlight.js]
+[browser_dbg-iframes.js]
+[browser_dbg_keyboard-shortcuts.js]
+[browser_dbg-pause-exceptions.js]
+[browser_dbg-navigation.js]
+[browser_dbg-pretty-print.js]
+[browser_dbg-pretty-print-paused.js]
+[browser_dbg-searching.js]
+skip-if = true
+[browser_dbg-sourcemaps.js]
+[browser_dbg-sourcemaps-bogus.js]
+[browser_dbg-sources.js] \ No newline at end of file
diff --git a/devtools/client/debugger/new/test/mochitest/browser_dbg-breaking-from-console.js b/devtools/client/debugger/new/test/mochitest/browser_dbg-breaking-from-console.js
new file mode 100644
index 000000000..8005b518d
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg-breaking-from-console.js
@@ -0,0 +1,31 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that `debugger` statements are hit before the debugger even
+// initializes and it properly highlights the right location in the
+// debugger.
+
+add_task(function* () {
+ const url = EXAMPLE_URL + "doc-script-switching.html";
+ const toolbox = yield openNewTabAndToolbox(url, "webconsole");
+
+ // Type "debugger" into console
+ let jsterm = toolbox.getPanel("webconsole").hud.jsterm;
+ jsterm.execute("debugger");
+
+ // Wait for the debugger to be selected and make sure it's paused
+ yield new Promise((resolve) => {
+ toolbox.on("jsdebugger-selected", resolve);
+ });
+ is(toolbox.threadClient.state, "paused");
+
+ // Create a dbg context
+ const dbg = createDebuggerContext(toolbox);
+ const { selectors: { getSelectedSource }, getState } = dbg;
+
+ // Make sure the thread is paused in the right source and location
+ yield waitForDispatch(dbg, "LOAD_SOURCE_TEXT");
+ is(dbg.win.cm.getValue(), "debugger");
+ const source = getSelectedSource(getState()).toJS();
+ assertPausedLocation(dbg, source, 1);
+});
diff --git a/devtools/client/debugger/new/test/mochitest/browser_dbg-breaking.js b/devtools/client/debugger/new/test/mochitest/browser_dbg-breaking.js
new file mode 100644
index 000000000..8994897c4
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg-breaking.js
@@ -0,0 +1,32 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests the breakpoints are hit in various situations.
+
+add_task(function* () {
+ const dbg = yield initDebugger("doc-scripts.html");
+ const { selectors: { getSelectedSource }, getState } = dbg;
+
+ // Make sure we can set a top-level breakpoint and it will be hit on
+ // reload.
+ yield addBreakpoint(dbg, "scripts.html", 18);
+ reload(dbg);
+ yield waitForPaused(dbg);
+ assertPausedLocation(dbg, "scripts.html", 18);
+ yield resume(dbg);
+
+ const paused = waitForPaused(dbg);
+
+ // Create an eval script that pauses itself.
+ invokeInTab("doEval");
+
+ yield paused;
+ yield resume(dbg);
+ const source = getSelectedSource(getState()).toJS();
+ ok(!source.url, "It is an eval source");
+
+ yield addBreakpoint(dbg, source, 5);
+ invokeInTab("evaledFunc");
+ yield waitForPaused(dbg);
+ assertPausedLocation(dbg, source, 5);
+});
diff --git a/devtools/client/debugger/new/test/mochitest/browser_dbg-breakpoints-cond.js b/devtools/client/debugger/new/test/mochitest/browser_dbg-breakpoints-cond.js
new file mode 100644
index 000000000..b6f7fb021
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg-breakpoints-cond.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function findBreakpoint(dbg, url, line) {
+ const { selectors: { getBreakpoint }, getState } = dbg;
+ const source = findSource(dbg, url);
+ return getBreakpoint(getState(), { sourceId: source.id, line });
+}
+
+function setConditionalBreakpoint(dbg, index, condition) {
+ return Task.spawn(function* () {
+ rightClickElement(dbg, "gutter", index);
+ selectMenuItem(dbg, 2);
+ yield waitForElement(dbg, ".conditional-breakpoint-panel input");
+ findElementWithSelector(dbg, ".conditional-breakpoint-panel input").focus();
+ type(dbg, condition);
+ pressKey(dbg, "Enter");
+ });
+}
+
+add_task(function* () {
+ const dbg = yield initDebugger("doc-scripts.html");
+ yield selectSource(dbg, "simple2");
+
+ // Adding a conditional Breakpoint
+ yield setConditionalBreakpoint(dbg, 5, "1");
+ yield waitForDispatch(dbg, "ADD_BREAKPOINT");
+ let bp = findBreakpoint(dbg, "simple2", 5);
+ is(bp.condition, "1", "breakpoint is created with the condition");
+
+ // Editing a conditional Breakpoint
+ yield setConditionalBreakpoint(dbg, 5, "2");
+ yield waitForDispatch(dbg, "SET_BREAKPOINT_CONDITION");
+ bp = findBreakpoint(dbg, "simple2", 5);
+ is(bp.condition, "21", "breakpoint is created with the condition");
+
+ // Removing a conditional breakpoint
+ clickElement(dbg, "gutter", 5);
+ yield waitForDispatch(dbg, "REMOVE_BREAKPOINT");
+ bp = findBreakpoint(dbg, "simple2", 5);
+ is(bp, null, "breakpoint was removed");
+
+ // Adding a condition to a breakpoint
+ clickElement(dbg, "gutter", 5);
+ yield waitForDispatch(dbg, "ADD_BREAKPOINT");
+ yield setConditionalBreakpoint(dbg, 5, "1");
+ bp = findBreakpoint(dbg, "simple2", 5);
+ is(bp.condition, "1", "breakpoint is created with the condition");
+});
+
diff --git a/devtools/client/debugger/new/test/mochitest/browser_dbg-breakpoints.js b/devtools/client/debugger/new/test/mochitest/browser_dbg-breakpoints.js
new file mode 100644
index 000000000..10bf44957
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg-breakpoints.js
@@ -0,0 +1,101 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function toggleBreakpoint(dbg, index) {
+ const bp = findElement(dbg, "breakpointItem", index);
+ const input = bp.querySelector("input");
+ input.click();
+}
+
+function removeBreakpoint(dbg, index) {
+ return Task.spawn(function* () {
+ const bp = findElement(dbg, "breakpointItem", index);
+ bp.querySelector(".close-btn").click();
+ yield waitForDispatch(dbg, "REMOVE_BREAKPOINT");
+ });
+}
+
+function disableBreakpoint(dbg, index) {
+ return Task.spawn(function* () {
+ toggleBreakpoint(dbg, index);
+ yield waitForDispatch(dbg, "REMOVE_BREAKPOINT");
+ });
+}
+
+function enableBreakpoint(dbg, index) {
+ return Task.spawn(function* () {
+ toggleBreakpoint(dbg, index);
+ yield waitForDispatch(dbg, "ADD_BREAKPOINT");
+ });
+}
+
+function toggleBreakpoints(dbg) {
+ return Task.spawn(function* () {
+ const btn = findElement(dbg, "toggleBreakpoints");
+ btn.click();
+ yield waitForDispatch(dbg, "TOGGLE_BREAKPOINTS");
+ });
+}
+
+function findBreakpoint(dbg, url, line) {
+ const { selectors: { getBreakpoint }, getState } = dbg;
+ const source = findSource(dbg, url);
+ return getBreakpoint(getState(), { sourceId: source.id, line });
+}
+
+function findBreakpoints(dbg) {
+ const { selectors: { getBreakpoints }, getState } = dbg;
+ return getBreakpoints(getState());
+}
+
+add_task(function* () {
+ const dbg = yield initDebugger("doc-scripts.html");
+
+ // Create two breakpoints
+ yield selectSource(dbg, "simple2");
+ yield addBreakpoint(dbg, "simple2", 3);
+ yield addBreakpoint(dbg, "simple2", 5);
+
+ // Disable the first one
+ yield disableBreakpoint(dbg, 1);
+ let bp1 = findBreakpoint(dbg, "simple2", 3);
+ let bp2 = findBreakpoint(dbg, "simple2", 5);
+ is(bp1.disabled, true, "first breakpoint is disabled");
+ is(bp2.disabled, false, "second breakpoint is enabled");
+
+ // Disable and Re-Enable the second one
+ yield disableBreakpoint(dbg, 2);
+ yield enableBreakpoint(dbg, 2);
+ bp2 = findBreakpoint(dbg, "simple2", 5);
+ is(bp2.disabled, false, "second breakpoint is enabled");
+});
+
+// toggle all
+add_task(function* () {
+ const dbg = yield initDebugger("doc-scripts.html");
+
+ // Create two breakpoints
+ yield selectSource(dbg, "simple2");
+ yield addBreakpoint(dbg, "simple2", 3);
+ yield addBreakpoint(dbg, "simple2", 5);
+
+ // Disable all of the breakpoints
+ yield toggleBreakpoints(dbg);
+ let bp1 = findBreakpoint(dbg, "simple2", 3);
+ let bp2 = findBreakpoint(dbg, "simple2", 5);
+ is(bp1.disabled, true, "first breakpoint is disabled");
+ is(bp2.disabled, true, "second breakpoint is disabled");
+
+ // Enable all of the breakpoints
+ yield toggleBreakpoints(dbg);
+ bp1 = findBreakpoint(dbg, "simple2", 3);
+ bp2 = findBreakpoint(dbg, "simple2", 5);
+ is(bp1.disabled, false, "first breakpoint is enabled");
+ is(bp2.disabled, false, "second breakpoint is enabled");
+
+ // Remove the breakpoints
+ yield removeBreakpoint(dbg, 1);
+ yield removeBreakpoint(dbg, 1);
+ const bps = findBreakpoints(dbg);
+ is(bps.size, 0, "breakpoints are removed");
+});
diff --git a/devtools/client/debugger/new/test/mochitest/browser_dbg-call-stack.js b/devtools/client/debugger/new/test/mochitest/browser_dbg-call-stack.js
new file mode 100644
index 000000000..54a401eeb
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg-call-stack.js
@@ -0,0 +1,62 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// checks to see if the frame is selected and the title is correct
+function isFrameSelected(dbg, index, title) {
+ const $frame = findElement(dbg, "frame", index);
+ const frame = dbg.selectors.getSelectedFrame(dbg.getState());
+
+ const elSelected = $frame.classList.contains("selected");
+ const titleSelected = frame.displayName == title;
+
+ return elSelected && titleSelected;
+}
+
+function toggleButton(dbg) {
+ const callStackBody = findElement(dbg, "callStackBody");
+ return callStackBody.querySelector(".show-more");
+}
+
+add_task(function* () {
+ const dbg = yield initDebugger("doc-script-switching.html");
+
+ toggleCallStack(dbg);
+
+ const notPaused = findElement(dbg, "callStackBody").innerText;
+ is(notPaused, "Not Paused", "Not paused message is shown");
+
+ invokeInTab("firstCall");
+ yield waitForPaused(dbg);
+
+ ok(isFrameSelected(dbg, 1, "secondCall"), "the first frame is selected");
+
+ clickElement(dbg, "frame", 2);
+ ok(isFrameSelected(dbg, 2, "firstCall"), "the second frame is selected");
+
+ let button = toggleButton(dbg);
+ ok(!button, "toggle button shouldn't be there");
+});
+
+add_task(function* () {
+ const dbg = yield initDebugger("doc-frames.html");
+
+ toggleCallStack(dbg);
+
+ invokeInTab("startRecursion");
+ yield waitForPaused(dbg);
+
+ ok(isFrameSelected(dbg, 1, "recurseA"), "the first frame is selected");
+
+ // check to make sure that the toggle button isn't there
+ let button = toggleButton(dbg);
+ let frames = findAllElements(dbg, "frames");
+ is(button.innerText, "Expand Rows", "toggle button should be expand");
+ is(frames.length, 7, "There should be at most seven frames");
+
+ button.click();
+
+ button = toggleButton(dbg);
+ frames = findAllElements(dbg, "frames");
+ is(button.innerText, "Collapse Rows", "toggle button should be collapse");
+ is(frames.length, 22, "All of the frames should be shown");
+});
diff --git a/devtools/client/debugger/new/test/mochitest/browser_dbg-chrome-create.js b/devtools/client/debugger/new/test/mochitest/browser_dbg-chrome-create.js
new file mode 100644
index 000000000..a2d88c064
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg-chrome-create.js
@@ -0,0 +1,72 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests that a chrome debugger can be created in a new process.
+ */
+
+const { BrowserToolboxProcess } = Cu.import("resource://devtools/client/framework/ToolboxProcess.jsm", {});
+let gProcess = undefined;
+
+function initChromeDebugger() {
+ info("Initializing a chrome debugger process.");
+ return new Promise(resolve => {
+ BrowserToolboxProcess.init(onClose, (event, _process) => {
+ info("Browser toolbox process started successfully.");
+ resolve(_process);
+ });
+ });
+}
+
+function onClose() {
+ ok(!gProcess._dbgProcess.isRunning,
+ "The remote debugger process isn't closed as it should be!");
+ is(gProcess._dbgProcess.exitValue, (Services.appinfo.OS == "WINNT" ? 0 : 256),
+ "The remote debugger process didn't die cleanly.");
+
+ info("process exit value: " + gProcess._dbgProcess.exitValue);
+
+ info("profile path: " + gProcess._dbgProfilePath);
+
+ finish();
+}
+
+registerCleanupFunction(function() {
+ Services.prefs.clearUserPref("devtools.debugger.remote-enabled");
+ gProcess = null;
+});
+
+add_task(function* () {
+ // Windows XP and 8.1 test slaves are terribly slow at this test.
+ requestLongerTimeout(5);
+ Services.prefs.setBoolPref("devtools.debugger.remote-enabled", true);
+
+ gProcess = yield initChromeDebugger();
+
+ ok(gProcess._dbgProcess,
+ "The remote debugger process wasn't created properly!");
+ ok(gProcess._dbgProcess.isRunning,
+ "The remote debugger process isn't running!");
+ is(typeof gProcess._dbgProcess.pid, "number",
+ "The remote debugger process doesn't have a pid (?!)");
+
+ info("process location: " + gProcess._dbgProcess.location);
+ info("process pid: " + gProcess._dbgProcess.pid);
+ info("process name: " + gProcess._dbgProcess.processName);
+ info("process sig: " + gProcess._dbgProcess.processSignature);
+
+ ok(gProcess._dbgProfilePath,
+ "The remote debugger profile wasn't created properly!");
+
+ is(
+ gProcess._dbgProfilePath,
+ OS.Path.join(OS.Constants.Path.profileDir, "chrome_debugger_profile"),
+ "The remote debugger profile isn't where we expect it!"
+ );
+
+ info("profile path: " + gProcess._dbgProfilePath);
+
+ gProcess.close();
+});
diff --git a/devtools/client/debugger/new/test/mochitest/browser_dbg-chrome-debugging.js b/devtools/client/debugger/new/test/mochitest/browser_dbg-chrome-debugging.js
new file mode 100644
index 000000000..3933c919b
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg-chrome-debugging.js
@@ -0,0 +1,88 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests that chrome debugging works.
+ */
+
+var gClient, gThreadClient;
+var gNewGlobal = promise.defer();
+var gNewChromeSource = promise.defer();
+
+var { DevToolsLoader } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+var customLoader = new DevToolsLoader();
+customLoader.invisibleToDebugger = true;
+var { DebuggerServer } = customLoader.require("devtools/server/main");
+var { DebuggerClient } = require("devtools/shared/client/main");
+
+function initDebuggerClient() {
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+ DebuggerServer.allowChromeProcess = true;
+
+ let transport = DebuggerServer.connectPipe();
+ return new DebuggerClient(transport);
+}
+
+function attachThread(client, actor) {
+ return new Promise(resolve => {
+ client.attachTab(actor, (response, tabClient) => {
+ tabClient.attachThread(null, (r, threadClient) => {
+ resolve(threadClient);
+ });
+ });
+ });
+}
+
+function onNewGlobal() {
+ ok(true, "Received a new chrome global.");
+ gClient.removeListener("newGlobal", onNewGlobal);
+ gNewGlobal.resolve();
+}
+
+function onNewSource(event, packet) {
+ if (packet.source.url.startsWith("chrome:")) {
+ ok(true, "Received a new chrome source: " + packet.source.url);
+ gThreadClient.removeListener("newSource", onNewSource);
+ gNewChromeSource.resolve();
+ }
+}
+
+function resumeAndCloseConnection() {
+ return new Promise(resolve => {
+ gThreadClient.resume(() => resolve(gClient.close()));
+ });
+}
+
+registerCleanupFunction(function() {
+ gClient = null;
+ gThreadClient = null;
+ gNewGlobal = null;
+ gNewChromeSource = null;
+
+ customLoader = null;
+ DebuggerServer = null;
+});
+
+add_task(function* () {
+ gClient = initDebuggerClient();
+
+ const [type] = yield gClient.connect();
+ is(type, "browser", "Root actor should identify itself as a browser.");
+
+ const response = yield gClient.getProcess();
+ let actor = response.form.actor;
+ gThreadClient = yield attachThread(gClient, actor);
+ gBrowser.selectedTab = gBrowser.addTab("about:mozilla");
+
+ // listen for a new source and global
+ gThreadClient.addListener("newSource", onNewSource);
+ gClient.addListener("newGlobal", onNewGlobal);
+ yield promise.all([ gNewGlobal.promise, gNewChromeSource.promise ]);
+
+ yield resumeAndCloseConnection();
+});
diff --git a/devtools/client/debugger/new/test/mochitest/browser_dbg-console.js b/devtools/client/debugger/new/test/mochitest/browser_dbg-console.js
new file mode 100644
index 000000000..c57103663
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg-console.js
@@ -0,0 +1,34 @@
+// Return a promise with a reference to jsterm, opening the split
+// console if necessary. This cleans up the split console pref so
+// it won't pollute other tests.
+function getSplitConsole(dbg) {
+ const { toolbox, win } = dbg;
+
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("devtools.toolbox.splitconsoleEnabled");
+ });
+
+ if (!win) {
+ win = toolbox.win;
+ }
+
+ if (!toolbox.splitConsole) {
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, win);
+ }
+
+ return new Promise(resolve => {
+ toolbox.getPanelWhenReady("webconsole").then(() => {
+ ok(toolbox.splitConsole, "Split console is shown.");
+ let jsterm = toolbox.getPanel("webconsole").hud.jsterm;
+ resolve(jsterm);
+ });
+ });
+}
+
+add_task(function* () {
+ Services.prefs.setBoolPref("devtools.toolbox.splitconsoleEnabled", true);
+ const dbg = yield initDebugger("doc-script-switching.html");
+
+ yield getSplitConsole(dbg);
+ ok(dbg.toolbox.splitConsole, "Split console is shown.");
+});
diff --git a/devtools/client/debugger/new/test/mochitest/browser_dbg-debugger-buttons.js b/devtools/client/debugger/new/test/mochitest/browser_dbg-debugger-buttons.js
new file mode 100644
index 000000000..0094650bc
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg-debugger-buttons.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function clickStepOver(dbg) {
+ clickElement(dbg, "stepOver");
+ return waitForPaused(dbg);
+}
+
+function clickStepIn(dbg) {
+ clickElement(dbg, "stepIn");
+ return waitForPaused(dbg);
+}
+
+function clickStepOut(dbg) {
+ clickElement(dbg, "stepOut");
+ return waitForPaused(dbg);
+}
+
+/**
+ * Test debugger buttons
+ * 1. resume
+ * 2. stepOver
+ * 3. stepIn
+ * 4. stepOver to the end of a function
+ * 5. stepUp at the end of a function
+ */
+add_task(function* () {
+ const dbg = yield initDebugger("doc-debugger-statements.html");
+
+ yield reload(dbg);
+ yield waitForPaused(dbg);
+ assertPausedLocation(dbg, "debugger-statements.html", 8);
+
+ // resume
+ clickElement(dbg, "resume");
+ yield waitForPaused(dbg);
+ assertPausedLocation(dbg, "debugger-statements.html", 12);
+
+ // step over
+ yield clickStepOver(dbg);
+ assertPausedLocation(dbg, "debugger-statements.html", 13);
+
+ // step into
+ yield clickStepIn(dbg);
+ assertPausedLocation(dbg, "debugger-statements.html", 18);
+
+ // step over
+ yield clickStepOver(dbg);
+ assertPausedLocation(dbg, "debugger-statements.html", 20);
+
+ // step out
+ yield clickStepOut(dbg);
+ assertPausedLocation(dbg, "debugger-statements.html", 20);
+});
diff --git a/devtools/client/debugger/new/test/mochitest/browser_dbg-editor-gutter.js b/devtools/client/debugger/new/test/mochitest/browser_dbg-editor-gutter.js
new file mode 100644
index 000000000..12a771c31
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg-editor-gutter.js
@@ -0,0 +1,64 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests the breakpoint gutter and making sure breakpoint icons exist
+// correctly
+
+// Utilities for interacting with the editor
+function clickGutter(dbg, line) {
+ clickElement(dbg, "gutter", line);
+}
+
+function getLineEl(dbg, line) {
+ const lines = dbg.win.document.querySelectorAll(".CodeMirror-code > div");
+ return lines[line - 1];
+}
+
+function assertEditorBreakpoint(dbg, line, shouldExist) {
+ const exists = !!getLineEl(dbg, line).querySelector(".new-breakpoint");
+ ok(exists === shouldExist,
+ "Breakpoint " + (shouldExist ? "exists" : "does not exist") +
+ " on line " + line);
+}
+
+add_task(function* () {
+ const dbg = yield initDebugger("doc-scripts.html");
+ const { selectors: { getBreakpoints, getBreakpoint }, getState } = dbg;
+ const source = findSource(dbg, "simple1.js");
+
+ yield selectSource(dbg, source.url);
+
+ // Make sure that clicking the gutter creates a breakpoint icon.
+ clickGutter(dbg, 4);
+ yield waitForDispatch(dbg, "ADD_BREAKPOINT");
+ is(getBreakpoints(getState()).size, 1, "One breakpoint exists");
+ assertEditorBreakpoint(dbg, 4, true);
+
+ // Make sure clicking at the same place removes the icon.
+ clickGutter(dbg, 4);
+ yield waitForDispatch(dbg, "REMOVE_BREAKPOINT");
+ is(getBreakpoints(getState()).size, 0, "No breakpoints exist");
+ assertEditorBreakpoint(dbg, 4, false);
+
+ // Test that a breakpoint icon slides down to the correct line.
+ clickGutter(dbg, 2);
+ yield waitForDispatch(dbg, "ADD_BREAKPOINT");
+ is(getBreakpoints(getState()).size, 1, "One breakpoint exists");
+ ok(getBreakpoint(getState(), { sourceId: source.id, line: 4 }),
+ "Breakpoint has correct line");
+ assertEditorBreakpoint(dbg, 2, false);
+ assertEditorBreakpoint(dbg, 4, true);
+
+ // Do the same sliding and make sure it works if there's already a
+ // breakpoint.
+ clickGutter(dbg, 2);
+ yield waitForDispatch(dbg, "ADD_BREAKPOINT");
+ is(getBreakpoints(getState()).size, 1, "One breakpoint exists");
+ assertEditorBreakpoint(dbg, 2, false);
+ assertEditorBreakpoint(dbg, 4, true);
+
+ clickGutter(dbg, 4);
+ yield waitForDispatch(dbg, "REMOVE_BREAKPOINT");
+ is(getBreakpoints(getState()).size, 0, "No breakpoints exist");
+ assertEditorBreakpoint(dbg, 4, false);
+});
diff --git a/devtools/client/debugger/new/test/mochitest/browser_dbg-editor-highlight.js b/devtools/client/debugger/new/test/mochitest/browser_dbg-editor-highlight.js
new file mode 100644
index 000000000..d7892e629
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg-editor-highlight.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the editor will always highight the right line, no
+// matter if the source text doesn't exist yet or even if the source
+// doesn't exist.
+
+add_task(function* () {
+ const dbg = yield initDebugger("doc-scripts.html");
+ const { selectors: { getSourceText }, getState } = dbg;
+ const sourceUrl = EXAMPLE_URL + "long.js";
+
+ // The source itself doesn't even exist yet, and using
+ // `selectSourceURL` will set a pending request to load this source
+ // and highlight a specific line.
+ dbg.actions.selectSourceURL(sourceUrl, { line: 66 });
+
+ // Wait for the source text to load and make sure we're in the right
+ // place.
+ yield waitForDispatch(dbg, "LOAD_SOURCE_TEXT");
+ assertHighlightLocation(dbg, "long.js", 66);
+
+ // Jump to line 16 and make sure the editor scrolled.
+ yield selectSource(dbg, "long.js", 16);
+ assertHighlightLocation(dbg, "long.js", 16);
+
+ // Make sure only one line is ever highlighted and the flash
+ // animation is cancelled on old lines.
+ yield selectSource(dbg, "long.js", 17);
+ yield selectSource(dbg, "long.js", 18);
+ assertHighlightLocation(dbg, "long.js", 18);
+ is(findAllElements(dbg, "highlightLine").length, 1,
+ "Only 1 line is highlighted");
+
+ // Test jumping to a line in a source that exists but hasn't been
+ // loaded yet.
+ selectSource(dbg, "simple1.js", 6);
+
+ // Make sure the source is in the loading state, wait for it to be
+ // fully loaded, and check the highlighted line.
+ const simple1 = findSource(dbg, "simple1.js");
+ ok(getSourceText(getState(), simple1.id).get("loading"));
+ yield waitForDispatch(dbg, "LOAD_SOURCE_TEXT");
+ ok(getSourceText(getState(), simple1.id).get("text"));
+ assertHighlightLocation(dbg, "simple1.js", 6);
+});
diff --git a/devtools/client/debugger/new/test/mochitest/browser_dbg-editor-mode.js b/devtools/client/debugger/new/test/mochitest/browser_dbg-editor-mode.js
new file mode 100644
index 000000000..2a23aa09f
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg-editor-mode.js
@@ -0,0 +1,14 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the editor sets the correct mode for different file
+// types
+add_task(function* () {
+ const dbg = yield initDebugger("doc-scripts.html");
+
+ yield selectSource(dbg, "simple1.js");
+ is(dbg.win.cm.getOption("mode").name, "javascript", "Mode is correct");
+
+ yield selectSource(dbg, "doc-scripts.html");
+ is(dbg.win.cm.getOption("mode").name, "htmlmixed", "Mode is correct");
+});
diff --git a/devtools/client/debugger/new/test/mochitest/browser_dbg-editor-select.js b/devtools/client/debugger/new/test/mochitest/browser_dbg-editor-select.js
new file mode 100644
index 000000000..8b954f899
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg-editor-select.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the editor highlights the correct location when the
+// debugger pauses
+
+// checks to see if the first breakpoint is visible
+function isElementVisible(dbg, elementName) {
+ const bpLine = findElement(dbg, elementName);
+ const cm = findElement(dbg, "codeMirror");
+ return bpLine && isVisibleWithin(cm, bpLine);
+}
+
+add_task(function* () {
+ // This test runs too slowly on linux debug. I'd like to figure out
+ // which is the slowest part of this and make it run faster, but to
+ // fix a frequent failure allow a longer timeout.
+ requestLongerTimeout(2);
+
+ const dbg = yield initDebugger("doc-scripts.html");
+ const { selectors: { getSelectedSource }, getState } = dbg;
+ const simple1 = findSource(dbg, "simple1.js");
+ const simple2 = findSource(dbg, "simple2.js");
+
+ // Set the initial breakpoint.
+ yield addBreakpoint(dbg, simple1, 4);
+ ok(!getSelectedSource(getState()), "No selected source");
+
+ // Call the function that we set a breakpoint in.
+ invokeInTab("main");
+ yield waitForPaused(dbg);
+ assertPausedLocation(dbg, simple1, 4);
+
+ // Step through to another file and make sure it's paused in the
+ // right place.
+ yield stepIn(dbg);
+ assertPausedLocation(dbg, simple2, 2);
+
+ // Step back out to the initial file.
+ yield stepOut(dbg);
+ yield stepOut(dbg);
+ assertPausedLocation(dbg, simple1, 5);
+ yield resume(dbg);
+
+ // Make sure that we can set a breakpoint on a line out of the
+ // viewport, and that pausing there scrolls the editor to it.
+ let longSrc = findSource(dbg, "long.js");
+ yield addBreakpoint(dbg, longSrc, 66);
+
+ invokeInTab("testModel");
+ yield waitForPaused(dbg);
+ assertPausedLocation(dbg, longSrc, 66);
+ ok(isElementVisible(dbg, "breakpoint"), "Breakpoint is visible");
+});
diff --git a/devtools/client/debugger/new/test/mochitest/browser_dbg-iframes.js b/devtools/client/debugger/new/test/mochitest/browser_dbg-iframes.js
new file mode 100644
index 000000000..9039da1be
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg-iframes.js
@@ -0,0 +1,26 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test debugging a page with iframes
+ * 1. pause in the main thread
+ * 2. pause in the iframe
+ */
+add_task(function* () {
+ const dbg = yield initDebugger("doc-iframes.html");
+
+ // test pausing in the main thread
+ yield reload(dbg);
+ yield waitForPaused(dbg);
+ assertPausedLocation(dbg, "iframes.html", 8);
+
+ // test pausing in the iframe
+ yield resume(dbg);
+ yield waitForPaused(dbg);
+ assertPausedLocation(dbg, "debugger-statements.html", 8);
+
+ // test pausing in the iframe
+ yield resume(dbg);
+ yield waitForPaused(dbg);
+ assertPausedLocation(dbg, "debugger-statements.html", 12);
+});
diff --git a/devtools/client/debugger/new/test/mochitest/browser_dbg-navigation.js b/devtools/client/debugger/new/test/mochitest/browser_dbg-navigation.js
new file mode 100644
index 000000000..381b6b7fd
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg-navigation.js
@@ -0,0 +1,47 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function countSources(dbg) {
+ const sources = dbg.selectors.getSources(dbg.getState());
+ return sources.size;
+}
+
+/**
+ * Test navigating
+ * navigating while paused will reset the pause state and sources
+ */
+add_task(function* () {
+ const dbg = yield initDebugger("doc-script-switching.html");
+ const { selectors: { getSelectedSource, getPause }, getState } = dbg;
+
+ invokeInTab("firstCall");
+ yield waitForPaused(dbg);
+
+ yield navigate(dbg, "doc-scripts.html", "simple1.js");
+ yield addBreakpoint(dbg, "simple1.js", 4);
+ invokeInTab("main");
+ yield waitForPaused(dbg);
+ assertPausedLocation(dbg, "simple1.js", 4);
+ is(countSources(dbg), 4, "4 sources are loaded.");
+
+ yield navigate(dbg, "about:blank");
+ yield waitForDispatch(dbg, "NAVIGATE");
+ is(countSources(dbg), 0, "0 sources are loaded.");
+ ok(!getPause(getState()), "No pause state exists");
+
+ yield navigate(dbg,
+ "doc-scripts.html",
+ "simple1.js",
+ "simple2.js",
+ "long.js",
+ "scripts.html"
+ );
+
+ is(countSources(dbg), 4, "4 sources are loaded.");
+
+ // Test that the current select source persists across reloads
+ yield selectSource(dbg, "long.js");
+ yield reload(dbg, "long.js");
+ ok(getSelectedSource(getState()).get("url").includes("long.js"),
+ "Selected source is long.js");
+});
diff --git a/devtools/client/debugger/new/test/mochitest/browser_dbg-pause-exceptions.js b/devtools/client/debugger/new/test/mochitest/browser_dbg-pause-exceptions.js
new file mode 100644
index 000000000..133316b54
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg-pause-exceptions.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function uncaughtException() {
+ return invokeInTab("uncaughtException").catch(() => {});
+}
+
+function caughtException() {
+ return invokeInTab("caughtException");
+}
+
+/*
+ Tests Pausing on exception
+ 1. skip an uncaught exception
+ 2. pause on an uncaught exception
+ 3. pause on a caught error
+ 4. skip a caught error
+*/
+add_task(function* () {
+ const dbg = yield initDebugger("doc-exceptions.html");
+
+ // test skipping an uncaught exception
+ yield togglePauseOnExceptions(dbg, false, false);
+ yield uncaughtException();
+ ok(!isPaused(dbg));
+
+ // Test pausing on an uncaught exception
+ yield togglePauseOnExceptions(dbg, true, false);
+ uncaughtException();
+ yield waitForPaused(dbg);
+ assertPausedLocation(dbg, "exceptions.js", 2);
+ yield resume(dbg);
+
+ // Test pausing on a caught Error
+ caughtException();
+ yield waitForPaused(dbg);
+ assertPausedLocation(dbg, "exceptions.js", 15);
+ yield resume(dbg);
+
+ // Test skipping a caught error
+ yield togglePauseOnExceptions(dbg, true, true);
+ caughtException();
+ yield waitForPaused(dbg);
+ assertPausedLocation(dbg, "exceptions.js", 17);
+ yield resume(dbg);
+});
diff --git a/devtools/client/debugger/new/test/mochitest/browser_dbg-pretty-print-paused.js b/devtools/client/debugger/new/test/mochitest/browser_dbg-pretty-print-paused.js
new file mode 100644
index 000000000..73919e65e
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg-pretty-print-paused.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests pretty-printing a source that is currently paused.
+
+add_task(function* () {
+ const dbg = yield initDebugger("doc-minified.html");
+
+ yield selectSource(dbg, "math.min.js");
+ yield addBreakpoint(dbg, "math.min.js", 2);
+
+ invokeInTab("arithmetic");
+ yield waitForPaused(dbg);
+ assertPausedLocation(dbg, "math.min.js", 2);
+
+ clickElement(dbg, "prettyPrintButton");
+ yield waitForDispatch(dbg, "TOGGLE_PRETTY_PRINT");
+
+ assertPausedLocation(dbg, "math.min.js:formatted", 18);
+
+ yield resume(dbg);
+});
diff --git a/devtools/client/debugger/new/test/mochitest/browser_dbg-pretty-print.js b/devtools/client/debugger/new/test/mochitest/browser_dbg-pretty-print.js
new file mode 100644
index 000000000..260bfef38
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg-pretty-print.js
@@ -0,0 +1,31 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests basic pretty-printing functionality.
+
+add_task(function* () {
+ const dbg = yield initDebugger("doc-minified.html");
+
+ yield selectSource(dbg, "math.min.js");
+ clickElement(dbg, "prettyPrintButton");
+ yield waitForDispatch(dbg, "TOGGLE_PRETTY_PRINT");
+
+ const ppSrc = findSource(dbg, "math.min.js:formatted");
+ ok(ppSrc, "Pretty-printed source exists");
+
+ yield addBreakpoint(dbg, ppSrc, 18);
+
+ invokeInTab("arithmetic");
+ yield waitForPaused(dbg);
+ assertPausedLocation(dbg, ppSrc, 18);
+ yield stepOver(dbg);
+ assertPausedLocation(dbg, ppSrc, 27);
+ yield resume(dbg);
+
+ // The pretty-print button should go away in the pretty-printed
+ // source.
+ ok(!findElement(dbg, "sourceFooter"), "Footer is hidden");
+
+ yield selectSource(dbg, "math.min.js");
+ ok(findElement(dbg, "sourceFooter"), "Footer is hidden");
+});
diff --git a/devtools/client/debugger/new/test/mochitest/browser_dbg-scopes.js b/devtools/client/debugger/new/test/mochitest/browser_dbg-scopes.js
new file mode 100644
index 000000000..adb99be84
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg-scopes.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function toggleNode(dbg, index) {
+ clickElement(dbg, "scopeNode", index);
+}
+
+function getLabel(dbg, index) {
+ return findElement(dbg, "scopeNode", index).innerText;
+}
+
+add_task(function* () {
+ const dbg = yield initDebugger("doc-script-switching.html");
+
+ toggleScopes(dbg);
+
+ invokeInTab("firstCall");
+ yield waitForPaused(dbg);
+
+ toggleNode(dbg, 1);
+ toggleNode(dbg, 2);
+
+ yield waitForDispatch(dbg, "LOAD_OBJECT_PROPERTIES");
+
+ is(getLabel(dbg, 1), "secondCall");
+ is(getLabel(dbg, 2), "<this>");
+});
diff --git a/devtools/client/debugger/new/test/mochitest/browser_dbg-searching.js b/devtools/client/debugger/new/test/mochitest/browser_dbg-searching.js
new file mode 100644
index 000000000..dd25e2b54
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg-searching.js
@@ -0,0 +1,28 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Testing source search
+add_task(function* () {
+ const dbg = yield initDebugger("doc-script-switching.html");
+
+ pressKey(dbg, "sourceSearch");
+ yield waitForElement(dbg, "input");
+ findElementWithSelector(dbg, "input").focus();
+ type(dbg, "sw");
+ pressKey(dbg, "Enter");
+
+ yield waitForDispatch(dbg, "LOAD_SOURCE_TEXT");
+ let source = dbg.selectors.getSelectedSource(dbg.getState());
+ ok(source.get("url").match(/switching-01/), "first source is selected");
+
+ // 2. arrow keys and check to see if source is selected
+ pressKey(dbg, "sourceSearch");
+ findElementWithSelector(dbg, "input").focus();
+ type(dbg, "sw");
+ pressKey(dbg, "Down");
+ pressKey(dbg, "Enter");
+
+ yield waitForDispatch(dbg, "LOAD_SOURCE_TEXT");
+ source = dbg.selectors.getSelectedSource(dbg.getState());
+ ok(source.get("url").match(/switching-02/), "second source is selected");
+});
diff --git a/devtools/client/debugger/new/test/mochitest/browser_dbg-sourcemaps-bogus.js b/devtools/client/debugger/new/test/mochitest/browser_dbg-sourcemaps-bogus.js
new file mode 100644
index 000000000..e8c6070fc
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg-sourcemaps-bogus.js
@@ -0,0 +1,23 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that an error while loading a sourcemap does not break
+// debugging.
+
+add_task(function* () {
+ const dbg = yield initDebugger("doc-sourcemap-bogus.html");
+ const { selectors: { getSources }, getState } = dbg;
+
+ yield selectSource(dbg, "bogus-map.js");
+
+ // We should still be able to set breakpoints and pause in the
+ // generated source.
+ yield addBreakpoint(dbg, "bogus-map.js", 4);
+ invokeInTab("runCode");
+ yield waitForPaused(dbg);
+ assertPausedLocation(dbg, "bogus-map.js", 4);
+
+ // Make sure that only the single generated source exists. The
+ // sourcemap failed to download.
+ is(getSources(getState()).size, 1, "Only 1 source exists");
+});
diff --git a/devtools/client/debugger/new/test/mochitest/browser_dbg-sourcemaps.js b/devtools/client/debugger/new/test/mochitest/browser_dbg-sourcemaps.js
new file mode 100644
index 000000000..30fd7b70c
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg-sourcemaps.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests loading sourcemapped sources, setting breakpoints, and
+// stepping in them.
+
+add_task(function* () {
+ const dbg = yield initDebugger("doc-sourcemaps.html");
+ const { selectors: { getBreakpoint, getBreakpoints }, getState } = dbg;
+
+ yield waitForSources(dbg, "entry.js", "output.js", "times2.js", "opts.js");
+ ok(true, "Original sources exist");
+ const entrySrc = findSource(dbg, "entry.js");
+
+ yield selectSource(dbg, entrySrc);
+ ok(dbg.win.cm.getValue().includes("window.keepMeAlive"),
+ "Original source text loaded correctly");
+
+ // Test that breakpoint sliding is not attempted. The breakpoint
+ // should not move anywhere.
+ yield addBreakpoint(dbg, entrySrc, 13);
+ is(getBreakpoints(getState()).size, 1, "One breakpoint exists");
+ ok(getBreakpoint(getState(), { sourceId: entrySrc.id, line: 13 }),
+ "Breakpoint has correct line");
+
+ // Test breaking on a breakpoint
+ yield addBreakpoint(dbg, "entry.js", 15);
+ is(getBreakpoints(getState()).size, 2, "Two breakpoints exist");
+ ok(getBreakpoint(getState(), { sourceId: entrySrc.id, line: 15 }),
+ "Breakpoint has correct line");
+
+ invokeInTab("keepMeAlive");
+ yield waitForPaused(dbg);
+ assertPausedLocation(dbg, entrySrc, 15);
+
+ yield stepIn(dbg);
+ assertPausedLocation(dbg, "times2.js", 2);
+ yield stepOver(dbg);
+ assertPausedLocation(dbg, "times2.js", 3);
+
+ yield stepOut(dbg);
+ yield stepOut(dbg);
+ assertPausedLocation(dbg, "entry.js", 16);
+});
diff --git a/devtools/client/debugger/new/test/mochitest/browser_dbg-sources.js b/devtools/client/debugger/new/test/mochitest/browser_dbg-sources.js
new file mode 100644
index 000000000..64b7f56ae
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg-sources.js
@@ -0,0 +1,58 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the source tree works.
+
+function* waitForSourceCount(dbg, i) {
+ // We are forced to wait until the DOM nodes appear because the
+ // source tree batches its rendering.
+ yield waitUntil(() => {
+ return findAllElements(dbg, "sourceNodes").length === i;
+ });
+}
+
+add_task(function* () {
+ const dbg = yield initDebugger("doc-sources.html");
+ const { selectors: { getSelectedSource }, getState } = dbg;
+
+ // Expand nodes and make sure more sources appear.
+ is(findAllElements(dbg, "sourceNodes").length, 2);
+
+ clickElement(dbg, "sourceArrow", 2);
+ is(findAllElements(dbg, "sourceNodes").length, 7);
+
+ clickElement(dbg, "sourceArrow", 3);
+ is(findAllElements(dbg, "sourceNodes").length, 8);
+
+ // Select a source.
+ ok(!findElementWithSelector(dbg, ".sources-list .focused"),
+ "Source is not focused");
+ const selected = waitForDispatch(dbg, "SELECT_SOURCE");
+ clickElement(dbg, "sourceNode", 4);
+ yield selected;
+ ok(findElementWithSelector(dbg, ".sources-list .focused"),
+ "Source is focused");
+ ok(getSelectedSource(getState()).get("url").includes("nested-source.js"),
+ "The right source is selected");
+
+ // Make sure new sources appear in the list.
+ ContentTask.spawn(gBrowser.selectedBrowser, null, function() {
+ const script = content.document.createElement("script");
+ script.src = "math.min.js";
+ content.document.body.appendChild(script);
+ });
+
+ yield waitForSourceCount(dbg, 9);
+ is(findElement(dbg, "sourceNode", 7).querySelector("span").innerText,
+ "math.min.js",
+ "The dynamic script exists");
+
+ // Make sure named eval sources appear in the list.
+ ContentTask.spawn(gBrowser.selectedBrowser, null, function() {
+ content.eval("window.evaledFunc = function() {} //# sourceURL=evaled.js");
+ });
+ yield waitForSourceCount(dbg, 11);
+ is(findElement(dbg, "sourceNode", 2).querySelector("span").innerText,
+ "evaled.js",
+ "The eval script exists");
+});
diff --git a/devtools/client/debugger/new/test/mochitest/browser_dbg_keyboard-shortcuts.js b/devtools/client/debugger/new/test/mochitest/browser_dbg_keyboard-shortcuts.js
new file mode 100644
index 000000000..0d7e572ef
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg_keyboard-shortcuts.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test keyboard shortcuts.
+ */
+
+function pressResume(dbg) {
+ pressKey(dbg, "resumeKey");
+ return waitForPaused(dbg);
+}
+
+function pressStepOver(dbg) {
+ pressKey(dbg, "stepOverKey");
+ return waitForPaused(dbg);
+}
+
+function pressStepIn(dbg) {
+ pressKey(dbg, "stepInKey");
+ return waitForPaused(dbg);
+}
+
+function pressStepOut(dbg) {
+ pressKey(dbg, "stepOutKey");
+ return waitForPaused(dbg);
+}
+
+add_task(function*() {
+ const dbg = yield initDebugger("doc-debugger-statements.html");
+
+ yield reload(dbg);
+ yield waitForPaused(dbg);
+ assertPausedLocation(dbg, "debugger-statements.html", 8);
+
+ yield pressResume(dbg);
+ assertPausedLocation(dbg, "debugger-statements.html", 12);
+
+ yield pressStepIn(dbg);
+ assertPausedLocation(dbg, "debugger-statements.html", 13);
+
+ yield pressStepOut(dbg);
+ assertPausedLocation(dbg, "debugger-statements.html", 14);
+
+ yield pressStepOver(dbg);
+ assertPausedLocation(dbg, "debugger-statements.html", 9);
+});
diff --git a/devtools/client/debugger/new/test/mochitest/examples/README.md b/devtools/client/debugger/new/test/mochitest/examples/README.md
new file mode 100644
index 000000000..1be42619d
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/examples/README.md
@@ -0,0 +1,7 @@
+### Test Examples
+
+##### Pages
+* **doc_script-switching-01** - includes two scripts that reference each other. The second function has a debugger.
+* **doc-scripts** - includes three sources, a long source and two sources that reference each other.
+* **doc-iframes** - includes an iframe with the debugger statements source.
+* **debugger-statements** - inline script with functions for testing stepping.
diff --git a/devtools/client/debugger/new/test/mochitest/examples/bogus-map.js b/devtools/client/debugger/new/test/mochitest/examples/bogus-map.js
new file mode 100644
index 000000000..20b5bbf7e
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/examples/bogus-map.js
@@ -0,0 +1,8 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+function runCode(){
+ var a=1;
+ a=a*2;
+ return a;
+}
+//# sourceMappingURL=bogus.map
diff --git a/devtools/client/debugger/new/test/mochitest/examples/bundle.js b/devtools/client/debugger/new/test/mochitest/examples/bundle.js
new file mode 100644
index 000000000..a03ace934
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/examples/bundle.js
@@ -0,0 +1,96 @@
+/******/ (function(modules) { // webpackBootstrap
+/******/ // The module cache
+/******/ var installedModules = {};
+/******/
+/******/ // The require function
+/******/ function __webpack_require__(moduleId) {
+/******/
+/******/ // Check if module is in cache
+/******/ if(installedModules[moduleId])
+/******/ return installedModules[moduleId].exports;
+/******/
+/******/ // Create a new module (and put it into the cache)
+/******/ var module = installedModules[moduleId] = {
+/******/ exports: {},
+/******/ id: moduleId,
+/******/ loaded: false
+/******/ };
+/******/
+/******/ // Execute the module function
+/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
+/******/
+/******/ // Flag the module as loaded
+/******/ module.loaded = true;
+/******/
+/******/ // Return the exports of the module
+/******/ return module.exports;
+/******/ }
+/******/
+/******/
+/******/ // expose the modules object (__webpack_modules__)
+/******/ __webpack_require__.m = modules;
+/******/
+/******/ // expose the module cache
+/******/ __webpack_require__.c = installedModules;
+/******/
+/******/ // __webpack_public_path__
+/******/ __webpack_require__.p = "";
+/******/
+/******/ // Load entry module and return exports
+/******/ return __webpack_require__(0);
+/******/ })
+/************************************************************************/
+/******/ ([
+/* 0 */
+/***/ function(module, exports, __webpack_require__) {
+
+ const times2 = __webpack_require__(1);
+ const { output } = __webpack_require__(2);
+ const opts = __webpack_require__(3);
+
+ output(times2(1));
+ output(times2(2));
+
+ if(opts.extra) {
+ output(times2(3));
+ }
+
+ window.keepMeAlive = function() {
+ // This function exists to make sure this script is never garbage
+ // collected. It is also callable from tests.
+ return times2(4);
+ }
+
+
+/***/ },
+/* 1 */
+/***/ function(module, exports) {
+
+ module.exports = function(x) {
+ return x * 2;
+ }
+
+
+/***/ },
+/* 2 */
+/***/ function(module, exports) {
+
+ function output(str) {
+ console.log(str);
+ }
+
+ module.exports = { output };
+
+
+/***/ },
+/* 3 */
+/***/ function(module, exports) {
+
+ module.exports = {
+ extra: true
+ };
+
+
+/***/ }
+/******/ ]);
+//# sourceMappingURL=bundle.js.map \ No newline at end of file
diff --git a/devtools/client/debugger/new/test/mochitest/examples/bundle.js.map b/devtools/client/debugger/new/test/mochitest/examples/bundle.js.map
new file mode 100644
index 000000000..ed7336ad1
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/examples/bundle.js.map
@@ -0,0 +1 @@
+{"version":3,"sources":["webpack:///webpack/bootstrap 4ef8c7ec7c1df790781e","webpack:///./entry.js","webpack:///./times2.js","webpack:///./output.js","webpack:///./opts.js"],"names":[],"mappings":";AAAA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA,uBAAe;AACf;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;;AAGA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;;;;;;;ACtCA;AACA,QAAO,SAAS;AAChB;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;;;;;;ACfA;AACA;AACA;;;;;;;ACFA;AACA;AACA;;AAEA,mBAAkB;;;;;;;ACJlB;AACA;AACA","file":"bundle.js","sourcesContent":[" \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId])\n \t\t\treturn installedModules[moduleId].exports;\n\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\texports: {},\n \t\t\tid: moduleId,\n \t\t\tloaded: false\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.loaded = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(0);\n\n\n\n// WEBPACK FOOTER //\n// webpack/bootstrap 4ef8c7ec7c1df790781e","const times2 = require(\"./times2\");\nconst { output } = require(\"./output\");\nconst opts = require(\"./opts\");\n\noutput(times2(1));\noutput(times2(2));\n\nif(opts.extra) {\n output(times2(3));\n}\n\nwindow.keepMeAlive = function() {\n // This function exists to make sure this script is never garbage\n // collected. It is also callable from tests.\n return times2(4);\n}\n\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./entry.js\n// module id = 0\n// module chunks = 0","module.exports = function(x) {\n return x * 2;\n}\n\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./times2.js\n// module id = 1\n// module chunks = 0","function output(str) {\n console.log(str);\n}\n\nmodule.exports = { output };\n\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./output.js\n// module id = 2\n// module chunks = 0","module.exports = {\n extra: true\n};\n\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./opts.js\n// module id = 3\n// module chunks = 0"],"sourceRoot":""} \ No newline at end of file
diff --git a/devtools/client/debugger/new/test/mochitest/examples/doc-debugger-statements.html b/devtools/client/debugger/new/test/mochitest/examples/doc-debugger-statements.html
new file mode 100644
index 000000000..967619d31
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/examples/doc-debugger-statements.html
@@ -0,0 +1,27 @@
+<html>
+ <head>
+ <title>Debugger Statements</title>
+ </head>
+
+ <body>
+ <script>
+ debugger;
+ test();
+
+ function test() {
+ debugger;
+ stepIntoMe();
+ }
+
+ function stepIntoMe() {
+ // step in
+ stepOverMe();
+ // step out
+ }
+
+ function stepOverMe() {
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/debugger/new/test/mochitest/examples/doc-exceptions.html b/devtools/client/debugger/new/test/mochitest/examples/doc-exceptions.html
new file mode 100644
index 000000000..5ca65b755
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/examples/doc-exceptions.html
@@ -0,0 +1,7 @@
+<html>
+ <head>
+ <title>Debugger test page</title>
+ <script type="text/javascript" src="exceptions.js"></script>
+ </head>
+ <body></body>
+</html>
diff --git a/devtools/client/debugger/new/test/mochitest/examples/doc-frames.html b/devtools/client/debugger/new/test/mochitest/examples/doc-frames.html
new file mode 100644
index 000000000..408c55b28
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/examples/doc-frames.html
@@ -0,0 +1,17 @@
+<html>
+ <head>
+ <title>Frames</title>
+ </head>
+
+ <body>
+ <script>
+ debugger;
+ // This inline script allows this HTML page to show up as a
+ // source. It also needs to introduce a new global variable so
+ // it's not immediately garbage collected.
+ function inline_script() { var x = 5; }
+ </script>
+ <script type="text/javascript" src="frames.js"></script>
+ </body>
+
+</html>
diff --git a/devtools/client/debugger/new/test/mochitest/examples/doc-iframes.html b/devtools/client/debugger/new/test/mochitest/examples/doc-iframes.html
new file mode 100644
index 000000000..26446eaa1
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/examples/doc-iframes.html
@@ -0,0 +1,17 @@
+<html>
+ <head>
+ <title>Iframe</title>
+ </head>
+
+ <body>
+ <script>
+ debugger;
+ // This inline script allows this HTML page to show up as a
+ // source. It also needs to introduce a new global variable so
+ // it's not immediately garbage collected.
+ function inline_script() { var x = 5; }
+ </script>
+ <iframe src="doc-debugger-statements.html"></iframe>
+ </body>
+
+</html>
diff --git a/devtools/client/debugger/new/test/mochitest/examples/doc-minified.html b/devtools/client/debugger/new/test/mochitest/examples/doc-minified.html
new file mode 100644
index 000000000..4c95a9b4a
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/examples/doc-minified.html
@@ -0,0 +1,14 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Debugger test page</title>
+ </head>
+
+ <body>
+ <script src="math.min.js"></script>
+ </body>
+</html>
diff --git a/devtools/client/debugger/new/test/mochitest/examples/doc-script-switching.html b/devtools/client/debugger/new/test/mochitest/examples/doc-script-switching.html
new file mode 100644
index 000000000..3c71497c2
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/examples/doc-script-switching.html
@@ -0,0 +1,18 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Debugger test page</title>
+ </head>
+
+ <body>
+ <button onclick="firstCall()">Click me!</button>
+
+ <script type="text/javascript" src="script-switching-01.js"></script>
+ <script type="text/javascript" src="script-switching-02.js"></script>
+ </body>
+
+</html>
diff --git a/devtools/client/debugger/new/test/mochitest/examples/doc-scripts.html b/devtools/client/debugger/new/test/mochitest/examples/doc-scripts.html
new file mode 100644
index 000000000..212b4802f
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/examples/doc-scripts.html
@@ -0,0 +1,21 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Debugger test page</title>
+ </head>
+
+ <body>
+ <script src="simple1.js"></script>
+ <script src="simple2.js"></script>
+ <script src="long.js"></script>
+ <script>
+ // This inline script allows this HTML page to show up as a
+ // source. It also needs to introduce a new global variable so
+ // it's not immediately garbage collected.
+ function inline_script() { var x = 5; }
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/debugger/new/test/mochitest/examples/doc-sourcemap-bogus.html b/devtools/client/debugger/new/test/mochitest/examples/doc-sourcemap-bogus.html
new file mode 100644
index 000000000..da448a2cd
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/examples/doc-sourcemap-bogus.html
@@ -0,0 +1,13 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Debugger test page</title>
+ </head>
+
+ <body>
+ <script src="bogus-map.js"></script>
+ </body>
+</html>
diff --git a/devtools/client/debugger/new/test/mochitest/examples/doc-sourcemaps.html b/devtools/client/debugger/new/test/mochitest/examples/doc-sourcemaps.html
new file mode 100644
index 000000000..10f5da047
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/examples/doc-sourcemaps.html
@@ -0,0 +1,13 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Debugger test page</title>
+ </head>
+
+ <body>
+ <script src="bundle.js"></script>
+ </body>
+</html>
diff --git a/devtools/client/debugger/new/test/mochitest/examples/doc-sources.html b/devtools/client/debugger/new/test/mochitest/examples/doc-sources.html
new file mode 100644
index 000000000..14cc86701
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/examples/doc-sources.html
@@ -0,0 +1,23 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Debugger test page</title>
+ </head>
+
+ <body>
+ <script src="simple1.js"></script>
+ <script src="simple2.js"></script>
+ <script src="long.js"></script>
+ <script>
+ // This inline script allows this HTML page to show up as a
+ // source. It also needs to introduce a new global variable so
+ // it's not immediately garbage collected.
+ function inline_script() { var x = 5; }
+ </script>
+ <script src="nested/nested-source.js"></script>
+ <script src="nested/deeper/deeper-source.js"></script>
+ </body>
+</html>
diff --git a/devtools/client/debugger/new/test/mochitest/examples/entry.js b/devtools/client/debugger/new/test/mochitest/examples/entry.js
new file mode 100644
index 000000000..d397a966b
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/examples/entry.js
@@ -0,0 +1,16 @@
+const times2 = require("./times2");
+const { output } = require("./output");
+const opts = require("./opts");
+
+output(times2(1));
+output(times2(2));
+
+if(opts.extra) {
+ output(times2(3));
+}
+
+window.keepMeAlive = function() {
+ // This function exists to make sure this script is never garbage
+ // collected. It is also callable from tests.
+ return times2(4);
+}
diff --git a/devtools/client/debugger/new/test/mochitest/examples/exceptions.js b/devtools/client/debugger/new/test/mochitest/examples/exceptions.js
new file mode 100644
index 000000000..9523f00ca
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/examples/exceptions.js
@@ -0,0 +1,19 @@
+function uncaughtException() {
+ throw "unreachable"
+}
+
+function caughtError() {
+ try {
+ throw new Error("error");
+ } catch (e) {
+ debugger;
+ }
+}
+
+function caughtException() {
+ try {
+ throw "reachable";
+ } catch (e) {
+ debugger;
+ }
+}
diff --git a/devtools/client/debugger/new/test/mochitest/examples/frames.js b/devtools/client/debugger/new/test/mochitest/examples/frames.js
new file mode 100644
index 000000000..0f031582e
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/examples/frames.js
@@ -0,0 +1,24 @@
+function recurseA(i) {
+ if (i == 20) {
+ debugger;
+ return;
+ }
+
+ // down into the rabbit hole we go
+ return (i % 2) ? recurseA(++i) : recurseB(++i);
+}
+
+function recurseB(i) {
+ if (i == 20) {
+ debugger;
+ return;
+ }
+
+ // down into the rabbit hole we go
+ return (i % 2) ? recurseA(++i) : recurseB(++i);
+}
+
+
+window.startRecursion = function() {
+ return recurseA(0);
+}
diff --git a/devtools/client/debugger/new/test/mochitest/examples/long.js b/devtools/client/debugger/new/test/mochitest/examples/long.js
new file mode 100644
index 000000000..58d605b36
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/examples/long.js
@@ -0,0 +1,76 @@
+var app = {};
+
+// Generic "model" object. You can use whatever
+// framework you want. For this application it
+// may not even be worth separating this logic
+// out, but we do this to demonstrate one way to
+// separate out parts of your application.
+app.TodoModel = function (key) {
+ this.key = key;
+ this.todos = [];
+ this.onChanges = [];
+};
+
+app.TodoModel.prototype.addTodo = function (title) {
+ this.todos = this.todos.concat([{
+ id: Utils.uuid(),
+ title: title,
+ completed: false
+ }]);
+};
+
+app.TodoModel.prototype.inform = function() {
+ // Something changed, but we do nothing
+ return null;
+};
+
+app.TodoModel.prototype.toggleAll = function (checked) {
+ // Note: it's usually better to use immutable data structures since they're
+ // easier to reason about and React works very well with them. That's why
+ // we use map() and filter() everywhere instead of mutating the array or
+ // todo items themselves.
+ this.todos = this.todos.map(function (todo) {
+ return Object.assign({}, todo, {completed: checked});
+ });
+
+ this.inform();
+};
+
+app.TodoModel.prototype.toggle = function (todoToToggle) {
+ this.todos = this.todos.map(function (todo) {
+ return todo !== todoToToggle ?
+ todo :
+ Object.assign({}, todo, {completed: !todo.completed});
+ });
+
+ this.inform();
+};
+
+app.TodoModel.prototype.destroy = function (todo) {
+ this.todos = this.todos.filter(function (candidate) {
+ return candidate !== todo;
+ });
+
+ this.inform();
+};
+
+app.TodoModel.prototype.save = function (todoToSave, text) {
+ this.todos = this.todos.map(function (todo) {
+ return todo !== todoToSave ? todo : Object.assign({}, todo, {title: text});
+ });
+
+ this.inform();
+};
+
+app.TodoModel.prototype.clearCompleted = function () {
+ this.todos = this.todos.filter(function (todo) {
+ return !todo.completed;
+ });
+
+ this.inform();
+};
+
+function testModel() {
+ const model = new app.TodoModel();
+ model.clearCompleted();
+}
diff --git a/devtools/client/debugger/new/test/mochitest/examples/math.min.js b/devtools/client/debugger/new/test/mochitest/examples/math.min.js
new file mode 100644
index 000000000..5a8593345
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/examples/math.min.js
@@ -0,0 +1,3 @@
+function add(a,b,k){var result=a+b;return k(result)}function sub(a,b,k){var result=a-b;return k(result)}function mul(a,b,k){var result=a*b;return k(result)}function div(a,b,k){var result=a/b;return k(result)}function arithmetic(){
+ add(4,4,function(a){
+ sub(a,2,function(b){mul(b,3,function(c){div(c,2,function(d){console.log(d)})})})})};
diff --git a/devtools/client/debugger/new/test/mochitest/examples/nested/nested-source.js b/devtools/client/debugger/new/test/mochitest/examples/nested/nested-source.js
new file mode 100644
index 000000000..a7b20f015
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/examples/nested/nested-source.js
@@ -0,0 +1,3 @@
+function computeSomething() {
+ return 1;
+}
diff --git a/devtools/client/debugger/new/test/mochitest/examples/opts.js b/devtools/client/debugger/new/test/mochitest/examples/opts.js
new file mode 100644
index 000000000..20988fa4a
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/examples/opts.js
@@ -0,0 +1,3 @@
+module.exports = {
+ extra: true
+};
diff --git a/devtools/client/debugger/new/test/mochitest/examples/output.js b/devtools/client/debugger/new/test/mochitest/examples/output.js
new file mode 100644
index 000000000..14281fdbf
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/examples/output.js
@@ -0,0 +1,5 @@
+function output(str) {
+ console.log(str);
+}
+
+module.exports = { output };
diff --git a/devtools/client/debugger/new/test/mochitest/examples/script-switching-01.js b/devtools/client/debugger/new/test/mochitest/examples/script-switching-01.js
new file mode 100644
index 000000000..4ba2772de
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/examples/script-switching-01.js
@@ -0,0 +1,6 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function firstCall() {
+ secondCall();
+}
diff --git a/devtools/client/debugger/new/test/mochitest/examples/script-switching-02.js b/devtools/client/debugger/new/test/mochitest/examples/script-switching-02.js
new file mode 100644
index 000000000..feb74315f
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/examples/script-switching-02.js
@@ -0,0 +1,13 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function secondCall() {
+ // This comment is useful: ☺
+ debugger;
+ function foo() {}
+ if (x) {
+ foo();
+ }
+}
+
+var x = true;
diff --git a/devtools/client/debugger/new/test/mochitest/examples/simple1.js b/devtools/client/debugger/new/test/mochitest/examples/simple1.js
new file mode 100644
index 000000000..87cc50f44
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/examples/simple1.js
@@ -0,0 +1,31 @@
+function main() {
+ // A comment so we can test that breakpoint sliding works across
+ // multiple lines
+ const func = foo(1, 2);
+ const result = func();
+ return result;
+}
+
+function doEval() {
+ eval("(" + function() {
+ debugger;
+
+ window.evaledFunc = function() {
+ var foo = 1;
+ var bar = 2;
+ return foo + bar;
+ };
+ }.toString() + ")()");
+}
+
+function doNamedEval() {
+ eval("(" + function() {
+ debugger;
+
+ window.evaledFunc = function() {
+ var foo = 1;
+ var bar = 2;
+ return foo + bar;
+ };
+ }.toString() + ")();\n //# sourceURL=evaled.js");
+}
diff --git a/devtools/client/debugger/new/test/mochitest/examples/simple2.js b/devtools/client/debugger/new/test/mochitest/examples/simple2.js
new file mode 100644
index 000000000..40c280edf
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/examples/simple2.js
@@ -0,0 +1,6 @@
+function foo(x, y) {
+ function bar() {
+ return x + y;
+ }
+ return bar;
+}
diff --git a/devtools/client/debugger/new/test/mochitest/examples/times2.js b/devtools/client/debugger/new/test/mochitest/examples/times2.js
new file mode 100644
index 000000000..2d51ed87a
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/examples/times2.js
@@ -0,0 +1,3 @@
+module.exports = function(x) {
+ return x * 2;
+}
diff --git a/devtools/client/debugger/new/test/mochitest/examples/webpack.config.js b/devtools/client/debugger/new/test/mochitest/examples/webpack.config.js
new file mode 100644
index 000000000..ff22342ce
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/examples/webpack.config.js
@@ -0,0 +1,8 @@
+
+module.exports = {
+ entry: "./entry.js",
+ output: {
+ filename: "bundle.js"
+ },
+ devtool: "sourcemap"
+}
diff --git a/devtools/client/debugger/new/test/mochitest/head.js b/devtools/client/debugger/new/test/mochitest/head.js
new file mode 100644
index 000000000..b0964d890
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/head.js
@@ -0,0 +1,684 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * The Mochitest API documentation
+ * @module mochitest
+ */
+
+/**
+ * The mochitest API to wait for certain events.
+ * @module mochitest/waits
+ * @parent mochitest
+ */
+
+/**
+ * The mochitest API predefined asserts.
+ * @module mochitest/asserts
+ * @parent mochitest
+ */
+
+/**
+ * The mochitest API for interacting with the debugger.
+ * @module mochitest/actions
+ * @parent mochitest
+ */
+
+/**
+ * Helper methods for the mochitest API.
+ * @module mochitest/helpers
+ * @parent mochitest
+ */
+
+// shared-head.js handles imports, constants, and utility functions
+Services.scriptloader.loadSubScript("chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js", this);
+var { Toolbox } = require("devtools/client/framework/toolbox");
+const EXAMPLE_URL = "http://example.com/browser/devtools/client/debugger/new/test/mochitest/examples/";
+
+Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", true);
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend");
+ delete window.resumeTest;
+});
+
+// Wait until an action of `type` is dispatched. This is different
+// then `_afterDispatchDone` because it doesn't wait for async actions
+// to be done/errored. Use this if you want to listen for the "start"
+// action of an async operation (somewhat rare).
+function waitForNextDispatch(store, type) {
+ return new Promise(resolve => {
+ store.dispatch({
+ // Normally we would use `services.WAIT_UNTIL`, but use the
+ // internal name here so tests aren't forced to always pass it
+ // in
+ type: "@@service/waitUntil",
+ predicate: action => action.type === type,
+ run: (dispatch, getState, action) => {
+ resolve(action);
+ }
+ });
+ });
+}
+
+// Wait until an action of `type` is dispatched. If it's part of an
+// async operation, wait until the `status` field is "done" or "error"
+function _afterDispatchDone(store, type) {
+ return new Promise(resolve => {
+ store.dispatch({
+ // Normally we would use `services.WAIT_UNTIL`, but use the
+ // internal name here so tests aren't forced to always pass it
+ // in
+ type: "@@service/waitUntil",
+ predicate: action => {
+ if (action.type === type) {
+ return action.status ?
+ (action.status === "done" || action.status === "error") :
+ true;
+ }
+ },
+ run: (dispatch, getState, action) => {
+ resolve(action);
+ }
+ });
+ });
+}
+
+/**
+ * Wait for a specific action type to be dispatch.
+ * If an async action, will wait for it to be done.
+ *
+ * @memberof mochitest/waits
+ * @param {Object} dbg
+ * @param {String} type
+ * @param {Number} eventRepeat
+ * @return {Promise}
+ * @static
+ */
+function waitForDispatch(dbg, type, eventRepeat = 1) {
+ let count = 0;
+
+ return Task.spawn(function* () {
+ info("Waiting for " + type + " to dispatch " + eventRepeat + " time(s)");
+ while (count < eventRepeat) {
+ yield _afterDispatchDone(dbg.store, type);
+ count++;
+ info(type + " dispatched " + count + " time(s)");
+ }
+ });
+}
+
+/**
+ * Waits for specific thread events.
+ *
+ * @memberof mochitest/waits
+ * @param {Object} dbg
+ * @param {String} eventName
+ * @return {Promise}
+ * @static
+ */
+function waitForThreadEvents(dbg, eventName) {
+ info("Waiting for thread event '" + eventName + "' to fire.");
+ const thread = dbg.toolbox.threadClient;
+
+ return new Promise(function(resolve, reject) {
+ thread.addListener(eventName, function onEvent(eventName, ...args) {
+ info("Thread event '" + eventName + "' fired.");
+ thread.removeListener(eventName, onEvent);
+ resolve.apply(resolve, args);
+ });
+ });
+}
+
+/**
+ * Waits for `predicate(state)` to be true. `state` is the redux app state.
+ *
+ * @memberof mochitest/waits
+ * @param {Object} dbg
+ * @param {Function} predicate
+ * @return {Promise}
+ * @static
+ */
+function waitForState(dbg, predicate) {
+ return new Promise(resolve => {
+ const unsubscribe = dbg.store.subscribe(() => {
+ if (predicate(dbg.store.getState())) {
+ unsubscribe();
+ resolve();
+ }
+ });
+ });
+}
+
+/**
+ * Waits for sources to be loaded.
+ *
+ * @memberof mochitest/waits
+ * @param {Object} dbg
+ * @param {Array} sources
+ * @return {Promise}
+ * @static
+ */
+function waitForSources(dbg, ...sources) {
+ if (sources.length === 0) {
+ return Promise.resolve();
+ }
+
+ info("Waiting on sources: " + sources.join(", "));
+ const { selectors: { getSources }, store } = dbg;
+ return Promise.all(sources.map(url => {
+ function sourceExists(state) {
+ return getSources(state).some(s => {
+ return s.get("url").includes(url);
+ });
+ }
+
+ if (!sourceExists(store.getState())) {
+ return waitForState(dbg, sourceExists);
+ }
+ }));
+}
+
+function waitForElement(dbg, selector) {
+ return waitUntil(() => findElementWithSelector(dbg, selector))
+}
+
+/**
+ * Assert that the debugger is paused at the correct location.
+ *
+ * @memberof mochitest/asserts
+ * @param {Object} dbg
+ * @param {String} source
+ * @param {Number} line
+ * @static
+ */
+function assertPausedLocation(dbg, source, line) {
+ const { selectors: { getSelectedSource, getPause }, getState } = dbg;
+ source = findSource(dbg, source);
+
+ // Check the selected source
+ is(getSelectedSource(getState()).get("id"), source.id);
+
+ // Check the pause location
+ const location = getPause(getState()).getIn(["frame", "location"]);
+ is(location.get("sourceId"), source.id);
+ is(location.get("line"), line);
+
+ // Check the debug line
+ ok(dbg.win.cm.lineInfo(line - 1).wrapClass.includes("debug-line"),
+ "Line is highlighted as paused");
+}
+
+/**
+ * Assert that the debugger is highlighting the correct location.
+ *
+ * @memberof mochitest/asserts
+ * @param {Object} dbg
+ * @param {String} source
+ * @param {Number} line
+ * @static
+ */
+function assertHighlightLocation(dbg, source, line) {
+ const { selectors: { getSelectedSource, getPause }, getState } = dbg;
+ source = findSource(dbg, source);
+
+ // Check the selected source
+ is(getSelectedSource(getState()).get("url"), source.url);
+
+ // Check the highlight line
+ const lineEl = findElement(dbg, "highlightLine");
+ ok(lineEl, "Line is highlighted");
+ ok(isVisibleWithin(findElement(dbg, "codeMirror"), lineEl),
+ "Highlighted line is visible");
+ ok(dbg.win.cm.lineInfo(line - 1).wrapClass.includes("highlight-line"),
+ "Line is highlighted");
+}
+
+/**
+ * Returns boolean for whether the debugger is paused.
+ *
+ * @memberof mochitest/asserts
+ * @param {Object} dbg
+ * @static
+ */
+function isPaused(dbg) {
+ const { selectors: { getPause }, getState } = dbg;
+ return !!getPause(getState());
+}
+
+/**
+ * Waits for the debugger to be fully paused.
+ *
+ * @memberof mochitest/waits
+ * @param {Object} dbg
+ * @static
+ */
+function waitForPaused(dbg) {
+ return Task.spawn(function* () {
+ // We want to make sure that we get both a real paused event and
+ // that the state is fully populated. The client may do some more
+ // work (call other client methods) before populating the state.
+ yield waitForThreadEvents(dbg, "paused"),
+ yield waitForState(dbg, state => {
+ const pause = dbg.selectors.getPause(state);
+ // Make sure we have the paused state.
+ if (!pause) {
+ return false;
+ }
+ // Make sure the source text is completely loaded for the
+ // source we are paused in.
+ const sourceId = pause.getIn(["frame", "location", "sourceId"]);
+ const sourceText = dbg.selectors.getSourceText(dbg.getState(), sourceId);
+ return sourceText && !sourceText.get("loading");
+ });
+ });
+}
+
+function createDebuggerContext(toolbox) {
+ const win = toolbox.getPanel("jsdebugger").panelWin;
+ const store = win.Debugger.store;
+
+ return {
+ actions: win.Debugger.actions,
+ selectors: win.Debugger.selectors,
+ getState: store.getState,
+ store: store,
+ client: win.Debugger.client,
+ toolbox: toolbox,
+ win: win
+ };
+}
+
+/**
+ * Intilializes the debugger.
+ *
+ * @memberof mochitest
+ * @param {String} url
+ * @param {Array} sources
+ * @return {Promise} dbg
+ * @static
+ */
+function initDebugger(url, ...sources) {
+ return Task.spawn(function* () {
+ const toolbox = yield openNewTabAndToolbox(EXAMPLE_URL + url, "jsdebugger");
+ return createDebuggerContext(toolbox);
+ });
+}
+
+window.resumeTest = undefined;
+/**
+ * Pause the test and let you interact with the debugger.
+ * The test can be resumed by invoking `resumeTest` in the console.
+ *
+ * @memberof mochitest
+ * @static
+ */
+function pauseTest() {
+ info("Test paused. Invoke resumeTest to continue.");
+ return new Promise(resolve => resumeTest = resolve);
+}
+
+// Actions
+/**
+ * Returns a source that matches the URL.
+ *
+ * @memberof mochitest/actions
+ * @param {Object} dbg
+ * @param {String} url
+ * @return {Object} source
+ * @static
+ */
+function findSource(dbg, url) {
+ if (typeof url !== "string") {
+ // Support passing in a source object itelf all APIs that use this
+ // function support both styles
+ const source = url;
+ return source;
+ }
+
+ const sources = dbg.selectors.getSources(dbg.getState());
+ const source = sources.find(s => s.get("url").includes(url));
+
+ if (!source) {
+ throw new Error("Unable to find source: " + url);
+ }
+
+ return source.toJS();
+}
+
+/**
+ * Selects the source.
+ *
+ * @memberof mochitest/actions
+ * @param {Object} dbg
+ * @param {String} url
+ * @param {Number} line
+ * @return {Promise}
+ * @static
+ */
+function selectSource(dbg, url, line) {
+ info("Selecting source: " + url);
+ const source = findSource(dbg, url);
+ const hasText = !!dbg.selectors.getSourceText(dbg.getState(), source.id);
+ dbg.actions.selectSource(source.id, { line });
+
+ if (!hasText) {
+ return waitForDispatch(dbg, "LOAD_SOURCE_TEXT");
+ }
+}
+
+/**
+ * Steps over.
+ *
+ * @memberof mochitest/actions
+ * @param {Object} dbg
+ * @return {Promise}
+ * @static
+ */
+function stepOver(dbg) {
+ info("Stepping over");
+ dbg.actions.stepOver();
+ return waitForPaused(dbg);
+}
+
+/**
+ * Steps in.
+ *
+ * @memberof mochitest/actions
+ * @param {Object} dbg
+ * @return {Promise}
+ * @static
+ */
+function stepIn(dbg) {
+ info("Stepping in");
+ dbg.actions.stepIn();
+ return waitForPaused(dbg);
+}
+
+/**
+ * Steps out.
+ *
+ * @memberof mochitest/actions
+ * @param {Object} dbg
+ * @return {Promise}
+ * @static
+ */
+function stepOut(dbg) {
+ info("Stepping out");
+ dbg.actions.stepOut();
+ return waitForPaused(dbg);
+}
+
+/**
+ * Resumes.
+ *
+ * @memberof mochitest/actions
+ * @param {Object} dbg
+ * @return {Promise}
+ * @static
+ */
+function resume(dbg) {
+ info("Resuming");
+ dbg.actions.resume();
+ return waitForThreadEvents(dbg, "resumed");
+}
+
+/**
+ * Reloads the debuggee.
+ *
+ * @memberof mochitest/actions
+ * @param {Object} dbg
+ * @param {Array} sources
+ * @return {Promise}
+ * @static
+ */
+function reload(dbg, ...sources) {
+ return dbg.client.reload().then(() => waitForSources(...sources));
+}
+
+/**
+ * Navigates the debuggee to another url.
+ *
+ * @memberof mochitest/actions
+ * @param {Object} dbg
+ * @param {String} url
+ * @param {Array} sources
+ * @return {Promise}
+ * @static
+ */
+function navigate(dbg, url, ...sources) {
+ dbg.client.navigate(url);
+ return waitForSources(dbg, ...sources);
+}
+
+/**
+ * Adds a breakpoint to a source at line/col.
+ *
+ * @memberof mochitest/actions
+ * @param {Object} dbg
+ * @param {String} source
+ * @param {Number} line
+ * @param {Number} col
+ * @return {Promise}
+ * @static
+ */
+function addBreakpoint(dbg, source, line, col) {
+ source = findSource(dbg, source);
+ const sourceId = source.id;
+ dbg.actions.addBreakpoint({ sourceId, line, col });
+ return waitForDispatch(dbg, "ADD_BREAKPOINT");
+}
+
+/**
+ * Removes a breakpoint from a source at line/col.
+ *
+ * @memberof mochitest/actions
+ * @param {Object} dbg
+ * @param {String} source
+ * @param {Number} line
+ * @param {Number} col
+ * @return {Promise}
+ * @static
+ */
+function removeBreakpoint(dbg, sourceId, line, col) {
+ return dbg.actions.removeBreakpoint({ sourceId, line, col });
+}
+
+/**
+ * Toggles the Pause on exceptions feature in the debugger.
+ *
+ * @memberof mochitest/actions
+ * @param {Object} dbg
+ * @param {Boolean} pauseOnExceptions
+ * @param {Boolean} ignoreCaughtExceptions
+ * @return {Promise}
+ * @static
+ */
+function togglePauseOnExceptions(dbg,
+ pauseOnExceptions, ignoreCaughtExceptions) {
+ const command = dbg.actions.pauseOnExceptions(
+ pauseOnExceptions,
+ ignoreCaughtExceptions
+ );
+
+ if (!isPaused(dbg)) {
+ return waitForThreadEvents(dbg, "resumed");
+ }
+
+ return command;
+}
+
+// Helpers
+
+/**
+ * Invokes a global function in the debuggee tab.
+ *
+ * @memberof mochitest/helpers
+ * @param {String} fnc
+ * @return {Promise}
+ * @static
+ */
+function invokeInTab(fnc) {
+ info(`Invoking function ${fnc} in tab`);
+ return ContentTask.spawn(gBrowser.selectedBrowser, fnc, function* (fnc) {
+ content.wrappedJSObject[fnc](); // eslint-disable-line mozilla/no-cpows-in-tests, max-len
+ });
+}
+
+const isLinux = Services.appinfo.OS === "Linux";
+const cmdOrCtrl = isLinux ? { ctrlKey: true } : { metaKey: true };
+const keyMappings = {
+ sourceSearch: { code: "p", modifiers: cmdOrCtrl},
+ fileSearch: { code: "f", modifiers: cmdOrCtrl},
+ "Enter": { code: "VK_RETURN" },
+ "Up": { code: "VK_UP" },
+ "Down": { code: "VK_DOWN" },
+ pauseKey: { code: "VK_F8" },
+ resumeKey: { code: "VK_F8" },
+ stepOverKey: { code: "VK_F10" },
+ stepInKey: { code: "VK_F11", modifiers: { ctrlKey: isLinux }},
+ stepOutKey: { code: "VK_F11", modifiers: { ctrlKey: isLinux, shiftKey: true }}
+};
+
+/**
+ * Simulates a key press in the debugger window.
+ *
+ * @memberof mochitest/helpers
+ * @param {Object} dbg
+ * @param {String} keyName
+ * @return {Promise}
+ * @static
+ */
+function pressKey(dbg, keyName) {
+ let keyEvent = keyMappings[keyName];
+
+ const { code, modifiers } = keyEvent;
+ return EventUtils.synthesizeKey(
+ code,
+ modifiers || {},
+ dbg.win
+ );
+}
+
+function type(dbg, string) {
+ string.split("").forEach(char => {
+ EventUtils.synthesizeKey(char, {}, dbg.win);
+ });
+}
+
+function isVisibleWithin(outerEl, innerEl) {
+ const innerRect = innerEl.getBoundingClientRect();
+ const outerRect = outerEl.getBoundingClientRect();
+ return innerRect.top > outerRect.top &&
+ innerRect.bottom < outerRect.bottom;
+}
+
+const selectors = {
+ callStackHeader: ".call-stack-pane ._header",
+ callStackBody: ".call-stack-pane .pane",
+ scopesHeader: ".scopes-pane ._header",
+ breakpointItem: i => `.breakpoints-list .breakpoint:nth-child(${i})`,
+ scopeNode: i => `.scopes-list .tree-node:nth-child(${i}) .object-label`,
+ frame: i => `.frames ul li:nth-child(${i})`,
+ frames: ".frames ul li",
+ gutter: i => `.CodeMirror-code *:nth-child(${i}) .CodeMirror-linenumber`,
+ menuitem: i => `menupopup menuitem:nth-child(${i})`,
+ pauseOnExceptions: ".pause-exceptions",
+ breakpoint: ".CodeMirror-code > .new-breakpoint",
+ highlightLine: ".CodeMirror-code > .highlight-line",
+ codeMirror: ".CodeMirror",
+ resume: ".resume.active",
+ stepOver: ".stepOver.active",
+ stepOut: ".stepOut.active",
+ stepIn: ".stepIn.active",
+ toggleBreakpoints: ".toggleBreakpoints",
+ prettyPrintButton: ".prettyPrint",
+ sourceFooter: ".source-footer",
+ sourceNode: i => `.sources-list .tree-node:nth-child(${i})`,
+ sourceNodes: ".sources-list .tree-node",
+ sourceArrow: i => `.sources-list .tree-node:nth-child(${i}) .arrow`,
+};
+
+function getSelector(elementName, ...args) {
+ let selector = selectors[elementName];
+ if (!selector) {
+ throw new Error(`The selector ${elementName} is not defined`);
+ }
+
+ if (typeof selector == "function") {
+ selector = selector(...args);
+ }
+
+ return selector;
+}
+
+function findElement(dbg, elementName, ...args) {
+ const selector = getSelector(elementName, ...args);
+ return findElementWithSelector(dbg, selector);
+}
+
+function findElementWithSelector(dbg, selector) {
+ return dbg.win.document.querySelector(selector);
+}
+
+function findAllElements(dbg, elementName, ...args) {
+ const selector = getSelector(elementName, ...args);
+ return dbg.win.document.querySelectorAll(selector);
+}
+
+/**
+ * Simulates a mouse click in the debugger DOM.
+ *
+ * @memberof mochitest/helpers
+ * @param {Object} dbg
+ * @param {String} elementName
+ * @param {Array} args
+ * @return {Promise}
+ * @static
+ */
+function clickElement(dbg, elementName, ...args) {
+ const selector = getSelector(elementName, ...args);
+ return EventUtils.synthesizeMouseAtCenter(
+ findElementWithSelector(dbg, selector),
+ {},
+ dbg.win
+ );
+}
+
+function rightClickElement(dbg, elementName, ...args) {
+ const selector = getSelector(elementName, ...args);
+ const doc = dbg.win.document;
+ return EventUtils.synthesizeMouseAtCenter(
+ doc.querySelector(selector),
+ {type: "contextmenu"},
+ dbg.win
+ );
+}
+
+function selectMenuItem(dbg, index) {
+ // the context menu is in the toolbox window
+ const doc = dbg.toolbox.win.document;
+
+ // there are several context menus, we want the one with the menu-api
+ const popup = doc.querySelector("menupopup[menu-api=\"true\"]");
+
+ const item = popup.querySelector(`menuitem:nth-child(${index})`);
+ return EventUtils.synthesizeMouseAtCenter(item, {}, dbg.toolbox.win );
+}
+
+/**
+ * Toggles the debugger call stack accordian.
+ *
+ * @memberof mochitest/actions
+ * @param {Object} dbg
+ * @return {Promise}
+ * @static
+ */
+function toggleCallStack(dbg) {
+ return findElement(dbg, "callStackHeader").click();
+}
+
+function toggleScopes(dbg) {
+ return findElement(dbg, "scopesHeader").click();
+}
diff --git a/devtools/client/debugger/panel.js b/devtools/client/debugger/panel.js
new file mode 100644
index 000000000..352d7b284
--- /dev/null
+++ b/devtools/client/debugger/panel.js
@@ -0,0 +1,180 @@
+/* -*- 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 { Cc, Ci, Cu, Cr } = require("chrome");
+const promise = require("promise");
+const EventEmitter = require("devtools/shared/event-emitter");
+const DevToolsUtils = require("devtools/shared/DevToolsUtils");
+
+function DebuggerPanel(iframeWindow, toolbox) {
+ this.panelWin = iframeWindow;
+ this._toolbox = toolbox;
+ this._destroyer = null;
+
+ this._view = this.panelWin.DebuggerView;
+ this._controller = this.panelWin.DebuggerController;
+ this._view._hostType = this._toolbox.hostType;
+ this._controller._target = this.target;
+ this._controller._toolbox = this._toolbox;
+
+ this.handleHostChanged = this.handleHostChanged.bind(this);
+ EventEmitter.decorate(this);
+}
+
+exports.DebuggerPanel = DebuggerPanel;
+
+DebuggerPanel.prototype = {
+ /**
+ * Open is effectively an asynchronous constructor.
+ *
+ * @return object
+ * A promise that is resolved when the Debugger completes opening.
+ */
+ open: function () {
+ let targetPromise;
+
+ // Local debugging needs to make the target remote.
+ if (!this.target.isRemote) {
+ targetPromise = this.target.makeRemote();
+ // Listen for tab switching events to manage focus when the content window
+ // is paused and events suppressed.
+ this.target.tab.addEventListener("TabSelect", this);
+ } else {
+ targetPromise = promise.resolve(this.target);
+ }
+
+ return targetPromise
+ .then(() => this._controller.startupDebugger())
+ .then(() => this._controller.connect())
+ .then(() => {
+ this._toolbox.on("host-changed", this.handleHostChanged);
+ // Add keys from this document's keyset to the toolbox, so they
+ // can work when the split console is focused.
+ let keysToClone = ["resumeKey", "stepOverKey", "stepInKey", "stepOutKey"];
+ for (let key of keysToClone) {
+ let elm = this.panelWin.document.getElementById(key);
+ let keycode = elm.getAttribute("keycode");
+ let modifiers = elm.getAttribute("modifiers");
+ let command = elm.getAttribute("command");
+ let handler = this._view.Toolbar.getCommandHandler(command);
+
+ let keyShortcut = this.translateToKeyShortcut(keycode, modifiers);
+ this._toolbox.useKeyWithSplitConsole(keyShortcut, handler, "jsdebugger");
+ }
+ this.isReady = true;
+ this.emit("ready");
+ return this;
+ })
+ .then(null, function onError(aReason) {
+ DevToolsUtils.reportException("DebuggerPanel.prototype.open", aReason);
+ });
+ },
+
+ /**
+ * Translate a VK_ keycode, with modifiers, to a key shortcut that can be used with
+ * shared/key-shortcut.
+ *
+ * @param {String} keycode
+ * The VK_* keycode to translate
+ * @param {String} modifiers
+ * The list (blank-space separated) of modifiers applying to this keycode.
+ * @return {String} a key shortcut ready to be used with shared/key-shortcut.js
+ */
+ translateToKeyShortcut: function (keycode, modifiers) {
+ // Remove the VK_ prefix.
+ keycode = keycode.replace("VK_", "");
+
+ // Translate modifiers
+ if (modifiers.includes("shift")) {
+ keycode = "Shift+" + keycode;
+ }
+ if (modifiers.includes("alt")) {
+ keycode = "Alt+" + keycode;
+ }
+ if (modifiers.includes("control")) {
+ keycode = "Ctrl+" + keycode;
+ }
+ if (modifiers.includes("meta")) {
+ keycode = "Cmd+" + keycode;
+ }
+ if (modifiers.includes("accel")) {
+ keycode = "CmdOrCtrl+" + keycode;
+ }
+
+ return keycode;
+ },
+
+ // DevToolPanel API
+
+ get target() {
+ return this._toolbox.target;
+ },
+
+ destroy: function () {
+ // Make sure this panel is not already destroyed.
+ if (this._destroyer) {
+ return this._destroyer;
+ }
+
+ if (!this.target.isRemote) {
+ this.target.tab.removeEventListener("TabSelect", this);
+ }
+
+ return this._destroyer = this._controller.shutdownDebugger().then(() => {
+ this.emit("destroyed");
+ });
+ },
+
+ // DebuggerPanel API
+
+ getFrames() {
+ let framesController = this.panelWin.DebuggerController.StackFrames;
+ let thread = framesController.activeThread;
+ if (thread && thread.paused) {
+ return {
+ frames: thread.cachedFrames,
+ selected: framesController.currentFrameDepth,
+ };
+ }
+
+ return null;
+ },
+
+ addBreakpoint: function (location) {
+ const { actions } = this.panelWin;
+ const { dispatch } = this._controller;
+
+ return dispatch(actions.addBreakpoint(location));
+ },
+
+ removeBreakpoint: function (location) {
+ const { actions } = this.panelWin;
+ const { dispatch } = this._controller;
+
+ return dispatch(actions.removeBreakpoint(location));
+ },
+
+ blackbox: function (source, flag) {
+ const { actions } = this.panelWin;
+ const { dispatch } = this._controller;
+ return dispatch(actions.blackbox(source, flag));
+ },
+
+ handleHostChanged: function () {
+ this._view.handleHostChanged(this._toolbox.hostType);
+ },
+
+ // nsIDOMEventListener API
+
+ handleEvent: function (aEvent) {
+ if (aEvent.target == this.target.tab &&
+ this._controller.activeThread.state == "paused") {
+ // Wait a tick for the content focus event to be delivered.
+ DevToolsUtils.executeSoon(() => this._toolbox.focusTool("jsdebugger"));
+ }
+ }
+};
diff --git a/devtools/client/debugger/test/.eslintrc.js b/devtools/client/debugger/test/.eslintrc.js
new file mode 100644
index 000000000..8d15a76d9
--- /dev/null
+++ b/devtools/client/debugger/test/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../../.eslintrc.mochitests.js"
+};
diff --git a/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon3/lib/main.js b/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon3/lib/main.js
new file mode 100644
index 000000000..fc00b60a1
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon3/lib/main.js
@@ -0,0 +1,13 @@
+var { Cc, Ci } = require("chrome");
+var { once } = require("sdk/system/events");
+
+var observerService = Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService);
+var observer = {
+ observe: function () {
+ debugger;
+ }
+};
+
+once("sdk:loader:destroy", () => observerService.removeObserver(observer, "debuggerAttached"));
+
+observerService.addObserver(observer, "debuggerAttached", false);
diff --git a/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon3/package.json b/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon3/package.json
new file mode 100644
index 000000000..4bf1bed50
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon3/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "browser_dbg_addon3",
+ "title": "browser_dbg_addon3",
+ "id": "jid1-ami3akps3baaeg",
+ "description": "a basic add-on",
+ "author": "",
+ "license": "MPL 2.0",
+ "version": "0.1"
+}
diff --git a/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon4/bootstrap.js b/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon4/bootstrap.js
new file mode 100644
index 000000000..e8bb9fcce
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon4/bootstrap.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var { interfaces: Ci, utils: Cu } = Components;
+
+function notify() {
+ // Log objects so makeDebuggeeValue can get the global to use
+ console.log({ msg: "Hello again" });
+}
+
+function startup(aParams, aReason) {
+ const { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
+ let res = Services.io.getProtocolHandler("resource")
+ .QueryInterface(Ci.nsIResProtocolHandler);
+ res.setSubstitution("browser_dbg_addon4", aParams.resourceURI);
+
+ // Load a JS module
+ Cu.import("resource://browser_dbg_addon4/test.jsm"); // eslint-disable-line mozilla/no-single-arg-cu-import
+ // Log objects so makeDebuggeeValue can get the global to use
+ console.log({ msg: "Hello from the test add-on" });
+
+ Services.obs.addObserver(notify, "addon-test-ping", false);
+}
+
+function shutdown(aParams, aReason) {
+ Services.obs.removeObserver(notify, "addon-test-ping");
+
+ // Unload the JS module
+ Cu.unload("resource://browser_dbg_addon4/test.jsm");
+
+ let res = Services.io.getProtocolHandler("resource")
+ .QueryInterface(Ci.nsIResProtocolHandler);
+ res.setSubstitution("browser_dbg_addon4", null);
+}
diff --git a/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon4/chrome.manifest b/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon4/chrome.manifest
new file mode 100644
index 000000000..ccb88ddf1
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon4/chrome.manifest
@@ -0,0 +1 @@
+content browser_dbg_addon4 .
diff --git a/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon4/install.rdf b/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon4/install.rdf
new file mode 100644
index 000000000..45679ffc9
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon4/install.rdf
@@ -0,0 +1,19 @@
+<?xml version="1.0"?>
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+ <Description about="urn:mozilla:install-manifest">
+ <em:id>browser_dbg_addon4@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+ <em:name>Test add-on with JS Modules</em:name>
+ <em:bootstrap>true</em:bootstrap>
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+</RDF>
diff --git a/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon4/test.jsm b/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon4/test.jsm
new file mode 100644
index 000000000..17bebfd8e
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon4/test.jsm
@@ -0,0 +1,6 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const EXPORTED_SYMBOLS = ["Foo"];
+
+const Foo = {};
diff --git a/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon4/test.xul b/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon4/test.xul
new file mode 100644
index 000000000..733817ad8
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon4/test.xul
@@ -0,0 +1,8 @@
+<?xml version="1.0"?>
+
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script type="text/javascript" src="testxul.js"/>
+ <label value="test.xul"/>
+</window>
diff --git a/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon4/test2.jsm b/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon4/test2.jsm
new file mode 100644
index 000000000..703869f43
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon4/test2.jsm
@@ -0,0 +1,6 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const EXPORTED_SYMBOLS = ["Bar"];
+
+const Bar = {};
diff --git a/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon4/test2.xul b/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon4/test2.xul
new file mode 100644
index 000000000..372d05587
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon4/test2.xul
@@ -0,0 +1,8 @@
+<?xml version="1.0"?>
+
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script type="text/javascript" src="testxul2.js"/>
+ <label value="test2.xul"/>
+</window>
diff --git a/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon4/testxul.js b/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon4/testxul.js
new file mode 100644
index 000000000..30ad9d2f8
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon4/testxul.js
@@ -0,0 +1,4 @@
+// Define something in here or the script may get collected
+window.addEventListener("unload", function () {
+ window.foo = "bar";
+});
diff --git a/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon4/testxul2.js b/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon4/testxul2.js
new file mode 100644
index 000000000..30ad9d2f8
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon4/testxul2.js
@@ -0,0 +1,4 @@
+// Define something in here or the script may get collected
+window.addEventListener("unload", function () {
+ window.foo = "bar";
+});
diff --git a/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon5/bootstrap.js b/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon5/bootstrap.js
new file mode 100644
index 000000000..8edc53756
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon5/bootstrap.js
@@ -0,0 +1,23 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var { interfaces: Ci, classes: Cc } = Components;
+
+function startup(aParams, aReason) {
+ Components.utils.import("resource://gre/modules/Services.jsm");
+ let res = Services.io.getProtocolHandler("resource")
+ .QueryInterface(Ci.nsIResProtocolHandler);
+ res.setSubstitution("browser_dbg_addon5", aParams.resourceURI);
+
+ // Load a JS module
+ Components.utils.import("resource://browser_dbg_addon5/test.jsm");
+}
+
+function shutdown(aParams, aReason) {
+ // Unload the JS module
+ Components.utils.unload("resource://browser_dbg_addon5/test.jsm");
+
+ let res = Services.io.getProtocolHandler("resource")
+ .QueryInterface(Ci.nsIResProtocolHandler);
+ res.setSubstitution("browser_dbg_addon5", null);
+}
diff --git a/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon5/chrome.manifest b/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon5/chrome.manifest
new file mode 100644
index 000000000..ceef8d06d
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon5/chrome.manifest
@@ -0,0 +1 @@
+content browser_dbg_addon5 .
diff --git a/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon5/install.rdf b/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon5/install.rdf
new file mode 100644
index 000000000..af2cbbb5d
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon5/install.rdf
@@ -0,0 +1,20 @@
+<?xml version="1.0"?>
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+ <Description about="urn:mozilla:install-manifest">
+ <em:id>browser_dbg_addon5@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+ <em:name>Test unpacked add-on with JS Modules</em:name>
+ <em:bootstrap>true</em:bootstrap>
+ <em:unpack>true</em:unpack>
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+</RDF>
diff --git a/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon5/test.jsm b/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon5/test.jsm
new file mode 100644
index 000000000..17bebfd8e
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon5/test.jsm
@@ -0,0 +1,6 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const EXPORTED_SYMBOLS = ["Foo"];
+
+const Foo = {};
diff --git a/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon5/test.xul b/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon5/test.xul
new file mode 100644
index 000000000..733817ad8
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon5/test.xul
@@ -0,0 +1,8 @@
+<?xml version="1.0"?>
+
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script type="text/javascript" src="testxul.js"/>
+ <label value="test.xul"/>
+</window>
diff --git a/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon5/test2.jsm b/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon5/test2.jsm
new file mode 100644
index 000000000..703869f43
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon5/test2.jsm
@@ -0,0 +1,6 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const EXPORTED_SYMBOLS = ["Bar"];
+
+const Bar = {};
diff --git a/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon5/test2.xul b/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon5/test2.xul
new file mode 100644
index 000000000..372d05587
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon5/test2.xul
@@ -0,0 +1,8 @@
+<?xml version="1.0"?>
+
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script type="text/javascript" src="testxul2.js"/>
+ <label value="test2.xul"/>
+</window>
diff --git a/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon5/testxul.js b/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon5/testxul.js
new file mode 100644
index 000000000..30ad9d2f8
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon5/testxul.js
@@ -0,0 +1,4 @@
+// Define something in here or the script may get collected
+window.addEventListener("unload", function () {
+ window.foo = "bar";
+});
diff --git a/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon5/testxul2.js b/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon5/testxul2.js
new file mode 100644
index 000000000..30ad9d2f8
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon5/testxul2.js
@@ -0,0 +1,4 @@
+// Define something in here or the script may get collected
+window.addEventListener("unload", function () {
+ window.foo = "bar";
+});
diff --git a/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon_webext_contentscript/manifest.json b/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon_webext_contentscript/manifest.json
new file mode 100644
index 000000000..ebc834bf7
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon_webext_contentscript/manifest.json
@@ -0,0 +1,18 @@
+{
+ "manifest_version": 2,
+ "name": "test content script sources",
+ "description": "test content script sources",
+ "version": "0.1.0",
+ "applications": {
+ "gecko": {
+ "id": "test-contentscript-sources@mozilla.com"
+ }
+ },
+ "content_scripts": [
+ {
+ "matches": ["<all_urls>"],
+ "js": ["webext-content-script.js"],
+ "run_at": "document_start"
+ }
+ ]
+}
diff --git a/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon_webext_contentscript/webext-content-script.js b/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon_webext_contentscript/webext-content-script.js
new file mode 100644
index 000000000..591c78840
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon_webext_contentscript/webext-content-script.js
@@ -0,0 +1 @@
+console.log("CONTENT SCRIPT LOADED");
diff --git a/devtools/client/debugger/test/mochitest/addon-webext-contentscript.xpi b/devtools/client/debugger/test/mochitest/addon-webext-contentscript.xpi
new file mode 100644
index 000000000..9e61dec1d
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/addon-webext-contentscript.xpi
Binary files differ
diff --git a/devtools/client/debugger/test/mochitest/addon1.xpi b/devtools/client/debugger/test/mochitest/addon1.xpi
new file mode 100644
index 000000000..689689ebe
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/addon1.xpi
Binary files differ
diff --git a/devtools/client/debugger/test/mochitest/addon2.xpi b/devtools/client/debugger/test/mochitest/addon2.xpi
new file mode 100644
index 000000000..8f6ec6dc1
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/addon2.xpi
Binary files differ
diff --git a/devtools/client/debugger/test/mochitest/addon3.xpi b/devtools/client/debugger/test/mochitest/addon3.xpi
new file mode 100644
index 000000000..b22fc3da7
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/addon3.xpi
Binary files differ
diff --git a/devtools/client/debugger/test/mochitest/addon4.xpi b/devtools/client/debugger/test/mochitest/addon4.xpi
new file mode 100644
index 000000000..1f6f106f3
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/addon4.xpi
Binary files differ
diff --git a/devtools/client/debugger/test/mochitest/addon5.xpi b/devtools/client/debugger/test/mochitest/addon5.xpi
new file mode 100644
index 000000000..56b38761d
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/addon5.xpi
Binary files differ
diff --git a/devtools/client/debugger/test/mochitest/browser.ini b/devtools/client/debugger/test/mochitest/browser.ini
new file mode 100644
index 000000000..832addf89
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser.ini
@@ -0,0 +1,317 @@
+# Tests in this directory are split into two manifests (this and browser2.ini)
+# to facilitate better chunking; see bug 1294489.
+
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+skip-if = (os == 'linux' && debug && bits == 32)
+support-files =
+ addon1.xpi
+ addon2.xpi
+ addon3.xpi
+ addon4.xpi
+ addon5.xpi
+ addon-webext-contentscript.xpi
+ addon-source/browser_dbg_addon5/*
+ code_binary_search.coffee
+ code_binary_search.js
+ code_binary_search.map
+ code_blackboxing_blackboxme.js
+ code_blackboxing_one.js
+ code_blackboxing_three.js
+ code_blackboxing_two.js
+ code_blackboxing_unblackbox.min.js
+ code_breakpoints-break-on-last-line-of-script-on-reload.js
+ code_breakpoints-other-tabs.js
+ code_bug-896139.js
+ code_frame-script.js
+ code_function-jump-01.js
+ code_function-search-01.js
+ code_function-search-02.js
+ code_function-search-03.js
+ code_location-changes.js
+ code_listworkers-worker1.js
+ code_listworkers-worker2.js
+ code_math.js
+ code_math.map
+ code_math.min.js
+ code_math_bogus_map.js
+ code_same-line-functions.js
+ code_script-eval.js
+ code_script-switching-01.js
+ code_script-switching-02.js
+ code_test-editor-mode
+ code_ugly.js
+ code_ugly-2.js
+ code_ugly-3.js
+ code_ugly-4.js
+ code_ugly-5.js
+ code_ugly-6.js
+ code_ugly-7.js
+ code_ugly-8
+ code_ugly-8^headers^
+ code_worker-source-map.coffee
+ code_worker-source-map.js
+ code_worker-source-map.js.map
+ code_WorkerActor.attach-worker1.js
+ code_WorkerActor.attach-worker2.js
+ code_WorkerActor.attachThread-worker.js
+ doc_auto-pretty-print-01.html
+ doc_auto-pretty-print-02.html
+ doc_binary_search.html
+ doc_blackboxing.html
+ doc_blackboxing_unblackbox.html
+ doc_breakpoints-break-on-last-line-of-script-on-reload.html
+ doc_breakpoints-other-tabs.html
+ doc_breakpoints-reload.html
+ doc_bug-896139.html
+ doc_closures.html
+ doc_closure-optimized-out.html
+ doc_cmd-break.html
+ doc_cmd-dbg.html
+ doc_breakpoint-move.html
+ doc_conditional-breakpoints.html
+ doc_domnode-variables.html
+ doc_editor-mode.html
+ doc_empty-tab-01.html
+ doc_empty-tab-02.html
+ doc_event-listeners-01.html
+ doc_event-listeners-02.html
+ doc_event-listeners-03.html
+ doc_event-listeners-04.html
+ doc_frame-parameters.html
+ doc_function-display-name.html
+ doc_function-jump.html
+ doc_function-search.html
+ doc_global-method-override.html
+ doc_iframes.html
+ doc_included-script.html
+ doc_inline-debugger-statement.html
+ doc_inline-script.html
+ doc_large-array-buffer.html
+ doc_listworkers-tab.html
+ doc_map-set.html
+ doc_minified.html
+ doc_minified_bogus_map.html
+ doc_native-event-handler.html
+ doc_no-page-sources.html
+ doc_pause-exceptions.html
+ doc_pretty-print.html
+ doc_pretty-print-2.html
+ doc_pretty-print-3.html
+ doc_pretty-print-on-paused.html
+ doc_promise-get-allocation-stack.html
+ doc_promise-get-fulfillment-stack.html
+ doc_promise-get-rejection-stack.html
+ doc_promise.html
+ doc_proxy.html
+ doc_random-javascript.html
+ doc_recursion-stack.html
+ doc_scope-variable.html
+ doc_scope-variable-2.html
+ doc_scope-variable-3.html
+ doc_scope-variable-4.html
+ doc_script-eval.html
+ doc_script-bookmarklet.html
+ doc_script-switching-01.html
+ doc_script-switching-02.html
+ doc_script_webext_contentscript.html
+ doc_split-console-paused-reload.html
+ doc_step-many-statements.html
+ doc_step-out.html
+ doc_terminate-on-tab-close.html
+ doc_watch-expressions.html
+ doc_watch-expression-button.html
+ doc_whitespace-property-names.html
+ doc_with-frame.html
+ doc_worker-source-map.html
+ doc_WorkerActor.attach-tab1.html
+ doc_WorkerActor.attach-tab2.html
+ doc_WorkerActor.attachThread-tab.html
+ head.js
+ sjs_post-page.sjs
+ sjs_random-javascript.sjs
+ testactors.js
+ !/devtools/client/commandline/test/helpers.js
+ !/devtools/client/framework/test/shared-head.js
+
+[browser_dbg_aaa_run_first_leaktest.js]
+skip-if = e10s && debug
+[browser_dbg_addonactor.js]
+tags = addons
+[browser_dbg_addon-sources.js]
+tags = addons
+[browser_dbg_addon-workers-dbg-enabled.js]
+tags = addons
+[browser_dbg_addon-modules.js]
+skip-if = e10s # TODO
+tags = addons
+[browser_dbg_addon-modules-unpacked.js]
+skip-if = e10s # TODO
+tags = addons
+[browser_dbg_addon-panels.js]
+tags = addons
+[browser_dbg_addon-console.js]
+skip-if = e10s && debug || os == 'win' # bug 1005274
+tags = addons
+[browser_dbg_auto-pretty-print-01.js]
+[browser_dbg_auto-pretty-print-02.js]
+[browser_dbg_auto-pretty-print-03.js]
+[browser_dbg_bfcache.js]
+skip-if = e10s || true # bug 1113935
+[browser_dbg_blackboxing-01.js]
+[browser_dbg_blackboxing-02.js]
+[browser_dbg_blackboxing-03.js]
+[browser_dbg_blackboxing-04.js]
+[browser_dbg_blackboxing-05.js]
+[browser_dbg_blackboxing-06.js]
+[browser_dbg_blackboxing-07.js]
+[browser_dbg_breadcrumbs-access.js]
+[browser_dbg_break-in-anon.js]
+[browser_dbg_break-on-next.js]
+[browser_dbg_break-on-next-console.js]
+[browser_dbg_break-on-dom-01.js]
+[browser_dbg_break-on-dom-02.js]
+[browser_dbg_break-on-dom-03.js]
+[browser_dbg_break-on-dom-04.js]
+[browser_dbg_break-on-dom-05.js]
+[browser_dbg_break-on-dom-06.js]
+[browser_dbg_break-on-dom-07.js]
+[browser_dbg_break-on-dom-08.js]
+[browser_dbg_break-on-dom-event-01.js]
+skip-if = e10s || os == "mac" || e10s # Bug 895426
+[browser_dbg_break-on-dom-event-02.js]
+skip-if = e10s # TODO
+[browser_dbg_break-on-dom-event-03.js]
+skip-if = e10s # TODO
+[browser_dbg_break-unselected.js]
+[browser_dbg_breakpoints-actual-location.js]
+[browser_dbg_breakpoints-actual-location2.js]
+[browser_dbg_breakpoints-break-on-last-line-of-script-on-reload.js]
+skip-if = e10s # Bug 1093535
+[browser_dbg_breakpoints-button-01.js]
+[browser_dbg_breakpoints-button-02.js]
+[browser_dbg_breakpoints-condition-thrown-message.js]
+skip-if = e10s && debug
+[browser_dbg_breakpoints-contextmenu-add.js]
+[browser_dbg_breakpoints-contextmenu.js]
+[browser_dbg_breakpoints-disabled-reload.js]
+skip-if = e10s # Bug 1093535
+[browser_dbg_breakpoints-editor.js]
+skip-if = e10s && debug
+[browser_dbg_breakpoints-eval.js]
+skip-if = e10s && debug
+[browser_dbg_breakpoints-highlight.js]
+skip-if = e10s && debug
+[browser_dbg_breakpoints-new-script.js]
+skip-if = e10s && debug
+[browser_dbg_breakpoints-other-tabs.js]
+skip-if = e10s && debug
+[browser_dbg_breakpoints-pane.js]
+skip-if = e10s && debug
+[browser_dbg_breakpoints-reload.js]
+skip-if = e10s && debug
+[browser_dbg_bug-896139.js]
+skip-if = e10s && debug
+[browser_dbg_chrome-create.js]
+skip-if = e10s && debug
+[browser_dbg_chrome-debugging.js]
+skip-if = e10s && debug
+[browser_dbg_clean-exit-window.js]
+skip-if = true # Bug 933950 (leaky test)
+[browser_dbg_clean-exit.js]
+skip-if = true # Bug 1044985 (racy test)
+[browser_dbg_closure-inspection.js]
+skip-if = e10s && debug
+[browser_dbg_cmd-blackbox.js]
+skip-if = e10s && debug
+[browser_dbg_cmd-break.js]
+skip-if = e10s # TODO
+[browser_dbg_cmd-dbg.js]
+skip-if = e10s # TODO
+[browser_dbg_conditional-breakpoints-01.js]
+skip-if = e10s && debug
+[browser_dbg_conditional-breakpoints-02.js]
+skip-if = e10s && debug
+[browser_dbg_conditional-breakpoints-03.js]
+skip-if = e10s && debug
+[browser_dbg_conditional-breakpoints-04.js]
+skip-if = e10s && debug
+[browser_dbg_conditional-breakpoints-05.js]
+skip-if = e10s && debug
+[browser_dbg_console-eval.js]
+skip-if = e10s && debug
+[browser_dbg_console-named-eval.js]
+skip-if = e10s && debug
+[browser_dbg_server-conditional-bp-01.js]
+skip-if = e10s && debug
+[browser_dbg_server-conditional-bp-02.js]
+skip-if = e10s && debug
+[browser_dbg_server-conditional-bp-03.js]
+skip-if = e10s && debug
+[browser_dbg_server-conditional-bp-04.js]
+skip-if = e10s && debug
+[browser_dbg_server-conditional-bp-05.js]
+skip-if = e10s && debug
+[browser_dbg_controller-evaluate-01.js]
+skip-if = e10s && debug
+[browser_dbg_controller-evaluate-02.js]
+skip-if = e10s && debug
+[browser_dbg_debugger-statement.js]
+skip-if = e10s && debug
+[browser_dbg_editor-contextmenu.js]
+skip-if = e10s && debug
+[browser_dbg_editor-mode.js]
+skip-if = e10s && debug
+[browser_dbg_event-listeners-01.js]
+skip-if = e10s && debug
+[browser_dbg_event-listeners-02.js]
+skip-if = e10s && debug
+[browser_dbg_event-listeners-03.js]
+skip-if = e10s && debug
+[browser_dbg_event-listeners-04.js]
+skip-if = debug || e10s # debug bug 1142597, e10s bug 1146603.
+[browser_dbg_file-reload.js]
+skip-if = e10s && debug
+[browser_dbg_function-display-name.js]
+skip-if = e10s && debug
+[browser_dbg_global-method-override.js]
+skip-if = e10s && debug
+[browser_dbg_globalactor.js]
+skip-if = e10s # TODO
+[browser_dbg_hide-toolbar-buttons.js]
+skip-if = e10s
+[browser_dbg_host-layout.js]
+skip-if = e10s && debug
+[browser_dbg_jump-to-function-definition.js]
+skip-if = e10s && debug
+[browser_dbg_iframes.js]
+skip-if = e10s # TODO
+[browser_dbg_instruments-pane-collapse.js]
+skip-if = e10s && debug
+[browser_dbg_instruments-pane-collapse_keyboard.js]
+skip-if = (os == 'mac' && e10s && debug) # Full keyboard navigation on OSX only works if Full Keyboard Access setting is set to All Control
+[browser_dbg_interrupts.js]
+skip-if = e10s && debug
+[browser_dbg_listaddons.js]
+skip-if = e10s && debug
+tags = addons
+[browser_dbg_listtabs-01.js]
+skip-if = e10s # TODO
+[browser_dbg_listtabs-02.js]
+skip-if = true # Never worked for remote frames, needs a mock DebuggerServerConnection
+[browser_dbg_listtabs-03.js]
+skip-if = e10s && debug
+[browser_dbg_listworkers.js]
+[browser_dbg_location-changes-01-simple.js]
+skip-if = e10s && debug
+[browser_dbg_location-changes-02-blank.js]
+skip-if = e10s && debug
+[browser_dbg_location-changes-03-new.js]
+skip-if = e10s # TODO
+[browser_dbg_location-changes-04-breakpoint.js]
+skip-if = e10s # TODO
+[browser_dbg_multiple-windows.js]
+skip-if = e10s # TODO
+[browser_dbg_navigation.js]
+skip-if = e10s && debug
diff --git a/devtools/client/debugger/test/mochitest/browser2.ini b/devtools/client/debugger/test/mochitest/browser2.ini
new file mode 100644
index 000000000..193c510ff
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser2.ini
@@ -0,0 +1,460 @@
+# Tests in this directory are split into two manifests (this and browser.ini)
+# to facilitate better chunking; see bug 1294489.
+
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+skip-if = (os == 'linux' && debug && bits == 32)
+support-files =
+ addon1.xpi
+ addon2.xpi
+ addon3.xpi
+ addon4.xpi
+ addon5.xpi
+ addon-webext-contentscript.xpi
+ addon-source/browser_dbg_addon5/*
+ code_binary_search.coffee
+ code_binary_search.js
+ code_binary_search.map
+ code_blackboxing_blackboxme.js
+ code_blackboxing_one.js
+ code_blackboxing_three.js
+ code_blackboxing_two.js
+ code_blackboxing_unblackbox.min.js
+ code_breakpoints-break-on-last-line-of-script-on-reload.js
+ code_breakpoints-other-tabs.js
+ code_bug-896139.js
+ code_frame-script.js
+ code_function-jump-01.js
+ code_function-search-01.js
+ code_function-search-02.js
+ code_function-search-03.js
+ code_location-changes.js
+ code_listworkers-worker1.js
+ code_listworkers-worker2.js
+ code_math.js
+ code_math.map
+ code_math.min.js
+ code_math_bogus_map.js
+ code_same-line-functions.js
+ code_script-eval.js
+ code_script-switching-01.js
+ code_script-switching-02.js
+ code_test-editor-mode
+ code_ugly.js
+ code_ugly-2.js
+ code_ugly-3.js
+ code_ugly-4.js
+ code_ugly-5.js
+ code_ugly-6.js
+ code_ugly-7.js
+ code_ugly-8
+ code_ugly-8^headers^
+ code_worker-source-map.coffee
+ code_worker-source-map.js
+ code_worker-source-map.js.map
+ code_WorkerActor.attach-worker1.js
+ code_WorkerActor.attach-worker2.js
+ code_WorkerActor.attachThread-worker.js
+ doc_auto-pretty-print-01.html
+ doc_auto-pretty-print-02.html
+ doc_binary_search.html
+ doc_blackboxing.html
+ doc_blackboxing_unblackbox.html
+ doc_breakpoints-break-on-last-line-of-script-on-reload.html
+ doc_breakpoints-other-tabs.html
+ doc_breakpoints-reload.html
+ doc_bug-896139.html
+ doc_closures.html
+ doc_closure-optimized-out.html
+ doc_cmd-break.html
+ doc_cmd-dbg.html
+ doc_breakpoint-move.html
+ doc_conditional-breakpoints.html
+ doc_domnode-variables.html
+ doc_editor-mode.html
+ doc_empty-tab-01.html
+ doc_empty-tab-02.html
+ doc_event-listeners-01.html
+ doc_event-listeners-02.html
+ doc_event-listeners-03.html
+ doc_event-listeners-04.html
+ doc_frame-parameters.html
+ doc_function-display-name.html
+ doc_function-jump.html
+ doc_function-search.html
+ doc_global-method-override.html
+ doc_iframes.html
+ doc_included-script.html
+ doc_inline-debugger-statement.html
+ doc_inline-script.html
+ doc_large-array-buffer.html
+ doc_listworkers-tab.html
+ doc_map-set.html
+ doc_minified.html
+ doc_minified_bogus_map.html
+ doc_native-event-handler.html
+ doc_no-page-sources.html
+ doc_pause-exceptions.html
+ doc_pretty-print.html
+ doc_pretty-print-2.html
+ doc_pretty-print-3.html
+ doc_pretty-print-on-paused.html
+ doc_promise-get-allocation-stack.html
+ doc_promise-get-fulfillment-stack.html
+ doc_promise-get-rejection-stack.html
+ doc_promise.html
+ doc_proxy.html
+ doc_random-javascript.html
+ doc_recursion-stack.html
+ doc_scope-variable.html
+ doc_scope-variable-2.html
+ doc_scope-variable-3.html
+ doc_scope-variable-4.html
+ doc_script-eval.html
+ doc_script-bookmarklet.html
+ doc_script-switching-01.html
+ doc_script-switching-02.html
+ doc_script_webext_contentscript.html
+ doc_split-console-paused-reload.html
+ doc_step-many-statements.html
+ doc_step-out.html
+ doc_terminate-on-tab-close.html
+ doc_watch-expressions.html
+ doc_watch-expression-button.html
+ doc_whitespace-property-names.html
+ doc_with-frame.html
+ doc_worker-source-map.html
+ doc_WorkerActor.attach-tab1.html
+ doc_WorkerActor.attach-tab2.html
+ doc_WorkerActor.attachThread-tab.html
+ head.js
+ sjs_post-page.sjs
+ sjs_random-javascript.sjs
+ testactors.js
+ !/devtools/client/commandline/test/helpers.js
+ !/devtools/client/framework/test/shared-head.js
+
+[browser_dbg_no-dangling-breakpoints.js]
+skip-if = e10s && debug
+[browser_dbg_no-page-sources.js]
+skip-if = e10s && debug
+[browser_dbg_on-pause-highlight.js]
+skip-if = e10s && debug
+[browser_dbg_on-pause-raise.js]
+skip-if = e10s && debug || os == "linux" # Bug 888811 & bug 891176
+[browser_dbg_optimized-out-vars.js]
+skip-if = e10s && debug
+[browser_dbg_panel-size.js]
+skip-if = e10s && debug
+[browser_dbg_parser-01.js]
+skip-if = e10s && debug
+[browser_dbg_parser-02.js]
+skip-if = e10s && debug
+[browser_dbg_parser-03.js]
+skip-if = e10s && debug
+[browser_dbg_parser-04.js]
+skip-if = e10s && debug
+[browser_dbg_parser-05.js]
+skip-if = e10s && debug
+[browser_dbg_parser-06.js]
+skip-if = e10s && debug
+[browser_dbg_parser-07.js]
+skip-if = e10s && debug
+[browser_dbg_parser-08.js]
+skip-if = e10s && debug
+[browser_dbg_parser-09.js]
+skip-if = e10s && debug
+[browser_dbg_parser-10.js]
+skip-if = e10s && debug
+[browser_dbg_parser-11.js]
+[browser_dbg_parser-computed-name.js]
+[browser_dbg_parser-function-defaults.js]
+[browser_dbg_parser-spread-expression.js]
+[browser_dbg_parser-template-strings.js]
+skip-if = e10s && debug
+[browser_dbg_pause-exceptions-01.js]
+skip-if = e10s && debug
+[browser_dbg_pause-exceptions-02.js]
+skip-if = e10s && debug
+[browser_dbg_pause-no-step.js]
+skip-if = e10s && debug
+[browser_dbg_pause-resume.js]
+skip-if = e10s && debug
+[browser_dbg_pause-warning.js]
+skip-if = e10s && debug
+[browser_dbg_paused-keybindings.js]
+skip-if = e10s
+[browser_dbg_post-page.js]
+[browser_dbg_pretty-print-01.js]
+skip-if = e10s && debug
+[browser_dbg_pretty-print-02.js]
+skip-if = e10s && debug
+[browser_dbg_pretty-print-03.js]
+skip-if = e10s && debug
+[browser_dbg_pretty-print-04.js]
+skip-if = e10s && debug
+[browser_dbg_pretty-print-05.js]
+skip-if = e10s && debug
+[browser_dbg_pretty-print-06.js]
+skip-if = e10s && debug
+[browser_dbg_pretty-print-07.js]
+skip-if = e10s && debug
+[browser_dbg_pretty-print-08.js]
+skip-if = e10s && debug
+[browser_dbg_pretty-print-09.js]
+skip-if = e10s && debug
+[browser_dbg_pretty-print-10.js]
+skip-if = e10s && debug
+[browser_dbg_pretty-print-11.js]
+skip-if = e10s && debug
+[browser_dbg_pretty-print-12.js]
+skip-if = e10s && debug
+[browser_dbg_pretty-print-13.js]
+skip-if = e10s && debug
+[browser_dbg_pretty-print-on-paused.js]
+skip-if = e10s && debug
+[browser_dbg_progress-listener-bug.js]
+skip-if = e10s && debug
+[browser_dbg_promises-allocation-stack.js]
+skip-if = e10s && debug
+[browser_dbg_promises-chrome-allocation-stack.js]
+skip-if = true # Bug 1177730
+[browser_dbg_promises-fulfillment-stack.js]
+skip-if = e10s && debug
+[browser_dbg_promises-rejection-stack.js]
+skip-if = e10s && debug
+[browser_dbg_reload-preferred-script-02.js]
+skip-if = e10s && debug
+[browser_dbg_reload-preferred-script-03.js]
+skip-if = e10s && debug
+[browser_dbg_reload-same-script.js]
+skip-if = e10s && debug
+[browser_dbg_scripts-switching-01.js]
+skip-if = e10s && debug
+[browser_dbg_scripts-switching-02.js]
+skip-if = e10s && debug
+[browser_dbg_scripts-switching-03.js]
+skip-if = e10s && debug
+[browser_dbg_search-autofill-identifier.js]
+skip-if = e10s && debug
+[browser_dbg_search-basic-01.js]
+skip-if = e10s && debug
+[browser_dbg_search-basic-02.js]
+skip-if = e10s && debug
+[browser_dbg_search-basic-03.js]
+skip-if = e10s && debug
+[browser_dbg_search-basic-04.js]
+skip-if = e10s && debug
+[browser_dbg_search-global-01.js]
+skip-if = e10s && debug
+[browser_dbg_search-global-02.js]
+skip-if = e10s && debug
+[browser_dbg_search-global-03.js]
+skip-if = e10s # Bug 1093535
+[browser_dbg_search-global-04.js]
+skip-if = e10s && debug
+[browser_dbg_search-global-05.js]
+skip-if = e10s && debug
+[browser_dbg_search-global-06.js]
+skip-if = e10s && debug
+[browser_dbg_search-popup-jank.js]
+skip-if = e10s && debug
+[browser_dbg_search-sources-01.js]
+skip-if = e10s && debug
+[browser_dbg_search-sources-02.js]
+skip-if = e10s && debug
+[browser_dbg_search-sources-03.js]
+skip-if = e10s && debug
+[browser_dbg_search-symbols.js]
+skip-if = (e10s && debug) || os == "linux" # Bug 1132375
+[browser_dbg_searchbox-help-popup-01.js]
+skip-if = e10s && debug
+[browser_dbg_searchbox-help-popup-02.js]
+skip-if = e10s && debug
+[browser_dbg_searchbox-parse.js]
+skip-if = (debug) || (os == 'linux' && asan) # asan, bug 1313861, debug: bug 1313861
+[browser_dbg_source-maps-01.js]
+skip-if = e10s && debug
+[browser_dbg_source-maps-02.js]
+skip-if = e10s && debug
+[browser_dbg_source-maps-03.js]
+skip-if = e10s && debug
+[browser_dbg_source-maps-04.js]
+skip-if = e10s # Bug 1093535
+[browser_dbg_sources-cache.js]
+[browser_dbg_sources-contextmenu-01.js]
+subsuite = clipboard
+[browser_dbg_sources-contextmenu-02.js]
+skip-if = e10s && debug
+[browser_dbg_sources-eval-01.js]
+skip-if = true # non-named eval sources turned off for now, bug 1124106
+[browser_dbg_sources-eval-02.js]
+[browser_dbg_sources-iframe-reload.js]
+[browser_dbg_sources-keybindings.js]
+subsuite = clipboard
+skip-if = e10s && debug
+[browser_dbg_sources-labels.js]
+skip-if = e10s && debug
+[browser_dbg_sources-large.js]
+[browser_dbg_sources-sorting.js]
+skip-if = e10s && debug
+[browser_dbg_sources-bookmarklet.js]
+skip-if = e10s && debug
+[browser_dbg_sources-webext-contentscript.js]
+[browser_dbg_split-console-paused-reload.js]
+skip-if = true # Bug 1288348 - previously e10s && debug
+[browser_dbg_stack-01.js]
+skip-if = e10s && debug
+[browser_dbg_stack-02.js]
+skip-if = e10s && debug
+[browser_dbg_stack-03.js]
+skip-if = e10s # TODO
+[browser_dbg_stack-04.js]
+skip-if = e10s && debug
+[browser_dbg_stack-05.js]
+skip-if = e10s && (debug || asan) # timeouts
+[browser_dbg_stack-06.js]
+skip-if = e10s && debug
+[browser_dbg_stack-07.js]
+skip-if = e10s && debug
+[browser_dbg_stack-contextmenu-01.js]
+skip-if = e10s && debug
+[browser_dbg_stack-contextmenu-02.js]
+subsuite = clipboard
+skip-if = e10s && debug
+[browser_dbg_step-out.js]
+skip-if = e10s && debug
+[browser_dbg_tabactor-01.js]
+skip-if = e10s # TODO
+[browser_dbg_tabactor-02.js]
+skip-if = e10s # TODO
+[browser_dbg_terminate-on-tab-close.js]
+skip-if = e10s && debug
+[browser_dbg_variables-view-01.js]
+skip-if = e10s && debug
+[browser_dbg_variables-view-02.js]
+skip-if = e10s && debug
+[browser_dbg_variables-view-03.js]
+skip-if = e10s && debug
+[browser_dbg_variables-view-04.js]
+skip-if = e10s && debug
+[browser_dbg_variables-view-05.js]
+skip-if = e10s && debug
+[browser_dbg_variables-view-06.js]
+skip-if = e10s && debug
+[browser_dbg_variables-view-07.js]
+skip-if = e10s && debug
+[browser_dbg_variables-view-08.js]
+skip-if = e10s && debug
+[browser_dbg_variables-view-accessibility.js]
+subsuite = clipboard
+skip-if = e10s && debug
+[browser_dbg_variables-view-data.js]
+skip-if = e10s && debug
+[browser_dbg_variables-view-edit-cancel.js]
+skip-if = e10s && debug
+[browser_dbg_variables-view-edit-click.js]
+skip-if = e10s || (os == 'mac' || os == 'win') && (debug == false) # Bug 986166
+[browser_dbg_variables-view-edit-getset-01.js]
+skip-if = e10s && debug
+[browser_dbg_variables-view-edit-getset-02.js]
+skip-if = e10s && debug
+[browser_dbg_variables-view-edit-value.js]
+skip-if = e10s && debug
+[browser_dbg_variables-view-edit-watch.js]
+skip-if = e10s && debug
+[browser_dbg_variables-view-filter-01.js]
+skip-if = e10s && debug
+[browser_dbg_variables-view-filter-02.js]
+skip-if = e10s && debug
+[browser_dbg_variables-view-filter-03.js]
+skip-if = e10s && debug
+[browser_dbg_variables-view-filter-04.js]
+skip-if = e10s && debug
+[browser_dbg_variables-view-filter-05.js]
+skip-if = e10s && debug
+[browser_dbg_variables-view-filter-pref.js]
+skip-if = e10s && debug
+[browser_dbg_variables-view-filter-searchbox.js]
+skip-if = e10s && debug
+[browser_dbg_variables-view-frame-parameters-01.js]
+skip-if = e10s && debug
+[browser_dbg_variables-view-frame-parameters-02.js]
+skip-if = e10s && debug
+[browser_dbg_variables-view-frame-parameters-03.js]
+skip-if = e10s && debug
+[browser_dbg_variables-view-frame-with.js]
+skip-if = e10s && debug
+[browser_dbg_variables-view-frozen-sealed-nonext.js]
+skip-if = e10s && debug
+[browser_dbg_variables-view-hide-non-enums.js]
+[browser_dbg_variables-view-large-array-buffer.js]
+[browser_dbg_variables-view-map-set.js]
+skip-if = e10s && debug
+[browser_dbg_variables-view-override-01.js]
+skip-if = e10s && debug
+[browser_dbg_variables-view-override-02.js]
+skip-if = e10s && debug
+[browser_dbg_variables-view-popup-01.js]
+skip-if = e10s && debug
+[browser_dbg_variables-view-popup-02.js]
+skip-if = e10s && debug
+[browser_dbg_variables-view-popup-03.js]
+skip-if = e10s && debug
+[browser_dbg_variables-view-popup-04.js]
+skip-if = e10s && debug
+[browser_dbg_variables-view-popup-05.js]
+skip-if = e10s && debug
+[browser_dbg_variables-view-popup-06.js]
+skip-if = e10s && debug
+[browser_dbg_variables-view-popup-07.js]
+skip-if = e10s && debug
+[browser_dbg_variables-view-popup-08.js]
+skip-if = e10s && debug
+[browser_dbg_variables-view-popup-09.js]
+skip-if = e10s && debug
+[browser_dbg_variables-view-popup-10.js]
+skip-if = e10s && debug
+[browser_dbg_variables-view-popup-11.js]
+skip-if = e10s && debug
+[browser_dbg_variables-view-popup-12.js]
+skip-if = e10s && debug
+[browser_dbg_variables-view-popup-13.js]
+skip-if = e10s && debug
+[browser_dbg_variables-view-popup-14.js]
+skip-if = true # Bug 1029545
+[browser_dbg_variables-view-popup-15.js]
+skip-if = e10s && debug
+[browser_dbg_variables-view-popup-16.js]
+skip-if = e10s && debug
+[browser_dbg_variables-view-popup-17.js]
+skip-if = e10s && debug
+[browser_dbg_variables-view-reexpand-01.js]
+skip-if = e10s && debug
+[browser_dbg_variables-view-reexpand-02.js]
+skip-if = e10s && debug
+[browser_dbg_variables-view-reexpand-03.js]
+skip-if = e10s && debug
+[browser_dbg_variables-view-webidl.js]
+skip-if = e10s && debug
+[browser_dbg_watch-expressions-01.js]
+skip-if = e10s && debug
+[browser_dbg_watch-expressions-02.js]
+skip-if = e10s && debug
+[browser_dbg_worker-console-01.js]
+skip-if = e10s && debug
+[browser_dbg_worker-console-02.js]
+skip-if = e10s && debug
+[browser_dbg_worker-console-03.js]
+skip-if = e10s && debug
+[browser_dbg_worker-source-map.js]
+skip-if = e10s && debug
+[browser_dbg_worker-window.js]
+skip-if = e10s && debug
+[browser_dbg_WorkerActor.attach.js]
+skip-if = e10s && debug
+[browser_dbg_WorkerActor.attachThread.js]
+skip-if = e10s && debug
+[browser_dbg_split-console-keypress.js]
+skip-if = e10s && (debug || os == "linux") # Bug 1214439
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_WorkerActor.attach.js b/devtools/client/debugger/test/mochitest/browser_dbg_WorkerActor.attach.js
new file mode 100644
index 000000000..68d7f1b26
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_WorkerActor.attach.js
@@ -0,0 +1,62 @@
+var MAX_TOTAL_VIEWERS = "browser.sessionhistory.max_total_viewers";
+
+var TAB1_URL = EXAMPLE_URL + "doc_WorkerActor.attach-tab1.html";
+var TAB2_URL = EXAMPLE_URL + "doc_WorkerActor.attach-tab2.html";
+var WORKER1_URL = "code_WorkerActor.attach-worker1.js";
+var WORKER2_URL = "code_WorkerActor.attach-worker2.js";
+
+function test() {
+ Task.spawn(function* () {
+ let oldMaxTotalViewers = SpecialPowers.getIntPref(MAX_TOTAL_VIEWERS);
+ SpecialPowers.setIntPref(MAX_TOTAL_VIEWERS, 10);
+
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+
+ let client = new DebuggerClient(DebuggerServer.connectPipe());
+ yield connect(client);
+
+ let tab = yield addTab(TAB1_URL);
+ let { tabs } = yield listTabs(client);
+ let [, tabClient] = yield attachTab(client, findTab(tabs, TAB1_URL));
+ yield listWorkers(tabClient);
+
+ // If a page still has pending network requests, it will not be moved into
+ // the bfcache. Consequently, we cannot use waitForWorkerListChanged here,
+ // because the worker is not guaranteed to have finished loading when it is
+ // registered. Instead, we have to wait for the promise returned by
+ // createWorker in the tab to be resolved.
+ yield createWorkerInTab(tab, WORKER1_URL);
+ let { workers } = yield listWorkers(tabClient);
+ let [, workerClient1] = yield attachWorker(tabClient,
+ findWorker(workers, WORKER1_URL));
+ is(workerClient1.isClosed, false, "worker in tab 1 should not be closed");
+
+ executeSoon(() => {
+ tab.linkedBrowser.loadURI(TAB2_URL);
+ });
+ yield waitForWorkerClose(workerClient1);
+ is(workerClient1.isClosed, true, "worker in tab 1 should be closed");
+
+ yield createWorkerInTab(tab, WORKER2_URL);
+ ({ workers } = yield listWorkers(tabClient));
+ let [, workerClient2] = yield attachWorker(tabClient,
+ findWorker(workers, WORKER2_URL));
+ is(workerClient2.isClosed, false, "worker in tab 2 should not be closed");
+
+ executeSoon(() => {
+ tab.linkedBrowser.contentWindow.history.back();
+ });
+ yield waitForWorkerClose(workerClient2);
+ is(workerClient2.isClosed, true, "worker in tab 2 should be closed");
+
+ ({ workers } = yield listWorkers(tabClient));
+ [, workerClient1] = yield attachWorker(tabClient,
+ findWorker(workers, WORKER1_URL));
+ is(workerClient1.isClosed, false, "worker in tab 1 should not be closed");
+
+ yield close(client);
+ SpecialPowers.setIntPref(MAX_TOTAL_VIEWERS, oldMaxTotalViewers);
+ finish();
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_WorkerActor.attachThread.js b/devtools/client/debugger/test/mochitest/browser_dbg_WorkerActor.attachThread.js
new file mode 100644
index 000000000..34e59a418
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_WorkerActor.attachThread.js
@@ -0,0 +1,100 @@
+var TAB_URL = EXAMPLE_URL + "doc_WorkerActor.attachThread-tab.html";
+var WORKER_URL = "code_WorkerActor.attachThread-worker.js";
+
+function test() {
+ Task.spawn(function* () {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+
+ let client1 = new DebuggerClient(DebuggerServer.connectPipe());
+ yield connect(client1);
+ let client2 = new DebuggerClient(DebuggerServer.connectPipe());
+ yield connect(client2);
+
+ let tab = yield addTab(TAB_URL);
+ let { tabs: tabs1 } = yield listTabs(client1);
+ let [, tabClient1] = yield attachTab(client1, findTab(tabs1, TAB_URL));
+ let { tabs: tabs2 } = yield listTabs(client2);
+ let [, tabClient2] = yield attachTab(client2, findTab(tabs2, TAB_URL));
+
+ yield listWorkers(tabClient1);
+ yield listWorkers(tabClient2);
+ yield createWorkerInTab(tab, WORKER_URL);
+ let { workers: workers1 } = yield listWorkers(tabClient1);
+ let [, workerClient1] = yield attachWorker(tabClient1,
+ findWorker(workers1, WORKER_URL));
+ let { workers: workers2 } = yield listWorkers(tabClient2);
+ let [, workerClient2] = yield attachWorker(tabClient2,
+ findWorker(workers2, WORKER_URL));
+
+ let location = { line: 5 };
+
+ let [, threadClient1] = yield attachThread(workerClient1);
+ let sources1 = yield getSources(threadClient1);
+ let sourceClient1 = threadClient1.source(findSource(sources1,
+ EXAMPLE_URL + WORKER_URL));
+ let [, breakpointClient1] = yield setBreakpoint(sourceClient1, location);
+ yield resume(threadClient1);
+
+ let [, threadClient2] = yield attachThread(workerClient2);
+ let sources2 = yield getSources(threadClient2);
+ let sourceClient2 = threadClient2.source(findSource(sources2,
+ EXAMPLE_URL + WORKER_URL));
+ let [, breakpointClient2] = yield setBreakpoint(sourceClient2, location);
+ yield resume(threadClient2);
+
+ let packet = yield source(sourceClient1);
+ let text = (yield new Promise(function (resolve) {
+ let request = new XMLHttpRequest();
+ request.open("GET", EXAMPLE_URL + WORKER_URL, true);
+ request.send();
+ request.onload = function () {
+ resolve(request.responseText);
+ };
+ }));
+ is(packet.source, text);
+
+ postMessageToWorkerInTab(tab, WORKER_URL, "ping");
+ yield Promise.all([
+ waitForPause(threadClient1).then((packet) => {
+ is(packet.type, "paused");
+ let why = packet.why;
+ is(why.type, "breakpoint");
+ is(why.actors.length, 1);
+ is(why.actors[0], breakpointClient1.actor);
+ let frame = packet.frame;
+ let where = frame.where;
+ is(where.source.actor, sourceClient1.actor);
+ is(where.line, location.line);
+ let variables = frame.environment.bindings.variables;
+ is(variables.a.value, 1);
+ is(variables.b.value.type, "undefined");
+ is(variables.c.value.type, "undefined");
+ return resume(threadClient1);
+ }),
+ waitForPause(threadClient2).then((packet) => {
+ is(packet.type, "paused");
+ let why = packet.why;
+ is(why.type, "breakpoint");
+ is(why.actors.length, 1);
+ is(why.actors[0], breakpointClient2.actor);
+ let frame = packet.frame;
+ let where = frame.where;
+ is(where.source.actor, sourceClient2.actor);
+ is(where.line, location.line);
+ let variables = frame.environment.bindings.variables;
+ is(variables.a.value, 1);
+ is(variables.b.value.type, "undefined");
+ is(variables.c.value.type, "undefined");
+ return resume(threadClient2);
+ }),
+ ]);
+
+ terminateWorkerInTab(tab, WORKER_URL);
+ yield waitForWorkerClose(workerClient1);
+ yield waitForWorkerClose(workerClient2);
+ yield close(client1);
+ yield close(client2);
+ finish();
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_aaa_run_first_leaktest.js b/devtools/client/debugger/test/mochitest/browser_dbg_aaa_run_first_leaktest.js
new file mode 100644
index 000000000..bd612456b
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_aaa_run_first_leaktest.js
@@ -0,0 +1,33 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * This tests if the debugger leaks on initialization and sudden destruction.
+ * You can also use this initialization format as a template for other tests.
+ * If leaks happen here, there's something very, very fishy going on.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
+
+function test() {
+ // Wait longer for this very simple test that comes first, to make sure that
+ // GC from previous tests does not interfere with the debugger suite.
+ requestLongerTimeout(2);
+
+ let options = {
+ source: EXAMPLE_URL + "code_script-switching-01.js",
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ ok(aTab, "Should have a tab available.");
+ ok(aPanel, "Should have a debugger pane available.");
+
+ waitForSourceAndCaretAndScopes(aPanel, "-02.js", 1).then(() => {
+ resumeDebuggerThenCloseAndFinish(aPanel);
+ });
+
+ callInTab(aTab, "firstCall");
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_addon-console.js b/devtools/client/debugger/test/mochitest/browser_dbg_addon-console.js
new file mode 100644
index 000000000..cf615f181
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_addon-console.js
@@ -0,0 +1,47 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Test that the we can see console messages from the add-on
+
+const ADDON_ID = "browser_dbg_addon4@tests.mozilla.org";
+const ADDON_PATH = "addon4.xpi";
+
+function getCachedMessages(webConsole) {
+ let deferred = promise.defer();
+ webConsole.getCachedMessages(["ConsoleAPI"], (aResponse) => {
+ if (aResponse.error) {
+ deferred.reject(aResponse.error);
+ return;
+ }
+ deferred.resolve(aResponse.messages);
+ });
+ return deferred.promise;
+}
+
+function test() {
+ Task.spawn(function* () {
+ let addon = yield addTemporaryAddon(ADDON_PATH);
+ let addonDebugger = yield initAddonDebugger(ADDON_ID);
+
+ let webConsole = addonDebugger.webConsole;
+ let messages = yield getCachedMessages(webConsole);
+ is(messages.length, 1, "Should be one cached message");
+ is(messages[0].arguments[0].type, "object", "Should have logged an object");
+ is(messages[0].arguments[0].preview.ownProperties.msg.value, "Hello from the test add-on", "Should have got the right message");
+
+ let consolePromise = addonDebugger.once("console");
+
+ console.log("Bad message");
+ Services.obs.notifyObservers(null, "addon-test-ping", "");
+
+ let messageGrip = yield consolePromise;
+ is(messageGrip.arguments[0].type, "object", "Should have logged an object");
+ is(messageGrip.arguments[0].preview.ownProperties.msg.value, "Hello again", "Should have got the right message");
+
+ yield addonDebugger.destroy();
+ yield removeAddon(addon);
+ finish();
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_addon-modules-unpacked.js b/devtools/client/debugger/test/mochitest/browser_dbg_addon-modules-unpacked.js
new file mode 100644
index 000000000..5784eecb4
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_addon-modules-unpacked.js
@@ -0,0 +1,67 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Make sure the add-on actor can see loaded JS Modules from an add-on
+
+const ADDON_ID = "browser_dbg_addon5@tests.mozilla.org";
+const ADDON_PATH = "addon-source/browser_dbg_addon5/";
+const ADDON_URL = getTemporaryAddonURLFromPath(ADDON_PATH);
+
+function test() {
+ Task.spawn(function* () {
+ let addon = yield addTemporaryAddon(ADDON_PATH);
+ let tab1 = yield addTab("chrome://browser_dbg_addon5/content/test.xul");
+
+ let addonDebugger = yield initAddonDebugger(ADDON_ID);
+
+ is(addonDebugger.title,
+ `Developer Tools - Test unpacked add-on with JS Modules - ${ADDON_URL}`,
+ "Saw the right toolbox title.");
+
+ // Check the inital list of sources is correct
+ let groups = yield addonDebugger.getSourceGroups();
+ is(groups[0].name, "browser_dbg_addon5@tests.mozilla.org", "Add-on code should be the first group");
+ is(groups[1].name, "chrome://global", "XUL code should be the second group");
+ is(groups.length, 2, "Should be only two groups.");
+
+ let sources = groups[0].sources;
+ is(sources.length, 3, "Should be three sources");
+ ok(sources[0].url.endsWith("/browser_dbg_addon5/bootstrap.js"), "correct url for bootstrap code");
+ is(sources[0].label, "bootstrap.js", "correct label for bootstrap code");
+ is(sources[1].url, "resource://browser_dbg_addon5/test.jsm", "correct url for addon code");
+ is(sources[1].label, "test.jsm", "correct label for addon code");
+ is(sources[2].url, "chrome://browser_dbg_addon5/content/testxul.js", "correct url for addon tab code");
+ is(sources[2].label, "testxul.js", "correct label for addon tab code");
+
+ // Load a new module and tab and check they appear in the list of sources
+ Cu.import("resource://browser_dbg_addon5/test2.jsm", {});
+ let tab2 = yield addTab("chrome://browser_dbg_addon5/content/test2.xul");
+
+ groups = yield addonDebugger.getSourceGroups();
+ is(groups[0].name, "browser_dbg_addon5@tests.mozilla.org", "Add-on code should be the first group");
+ is(groups[1].name, "chrome://global", "XUL code should be the second group");
+ is(groups.length, 2, "Should be only two groups.");
+
+ sources = groups[0].sources;
+ is(sources.length, 5, "Should be five sources");
+ ok(sources[0].url.endsWith("/browser_dbg_addon5/bootstrap.js"), "correct url for bootstrap code");
+ is(sources[0].label, "bootstrap.js", "correct label for bootstrap code");
+ is(sources[1].url, "resource://browser_dbg_addon5/test.jsm", "correct url for addon code");
+ is(sources[1].label, "test.jsm", "correct label for addon code");
+ is(sources[2].url, "chrome://browser_dbg_addon5/content/testxul.js", "correct url for addon tab code");
+ is(sources[2].label, "testxul.js", "correct label for addon tab code");
+ is(sources[3].url, "resource://browser_dbg_addon5/test2.jsm", "correct url for addon code");
+ is(sources[3].label, "test2.jsm", "correct label for addon code");
+ is(sources[4].url, "chrome://browser_dbg_addon5/content/testxul2.js", "correct url for addon tab code");
+ is(sources[4].label, "testxul2.js", "correct label for addon tab code");
+
+ Cu.unload("resource://browser_dbg_addon5/test2.jsm");
+ yield addonDebugger.destroy();
+ yield removeTab(tab1);
+ yield removeTab(tab2);
+ yield removeAddon(addon);
+ finish();
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_addon-modules.js b/devtools/client/debugger/test/mochitest/browser_dbg_addon-modules.js
new file mode 100644
index 000000000..1ff7c600d
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_addon-modules.js
@@ -0,0 +1,66 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Make sure the add-on actor can see loaded JS Modules from an add-on
+
+const ADDON_ID = "browser_dbg_addon4@tests.mozilla.org";
+const ADDON_PATH = "addon4.xpi";
+const ADDON_URL = getTemporaryAddonURLFromPath(ADDON_PATH);
+
+function test() {
+ Task.spawn(function* () {
+ let addon = yield addTemporaryAddon(ADDON_PATH);
+ let tab1 = yield addTab("chrome://browser_dbg_addon4/content/test.xul");
+
+ let addonDebugger = yield initAddonDebugger(ADDON_ID);
+
+ is(addonDebugger.title, `Developer Tools - Test add-on with JS Modules - ${ADDON_URL}`,
+ "Saw the right toolbox title.");
+
+ // Check the inital list of sources is correct
+ let groups = yield addonDebugger.getSourceGroups();
+ is(groups[0].name, "browser_dbg_addon4@tests.mozilla.org", "Add-on code should be the first group");
+ is(groups[1].name, "chrome://global", "XUL code should be the second group");
+ is(groups.length, 2, "Should be only two groups.");
+
+ let sources = groups[0].sources;
+ is(sources.length, 3, "Should be three sources");
+ ok(sources[0].url.endsWith("/addon4.xpi!/bootstrap.js"), "correct url for bootstrap code");
+ is(sources[0].label, "bootstrap.js", "correct label for bootstrap code");
+ is(sources[1].url, "resource://browser_dbg_addon4/test.jsm", "correct url for addon code");
+ is(sources[1].label, "test.jsm", "correct label for addon code");
+ is(sources[2].url, "chrome://browser_dbg_addon4/content/testxul.js", "correct url for addon tab code");
+ is(sources[2].label, "testxul.js", "correct label for addon tab code");
+
+ // Load a new module and tab and check they appear in the list of sources
+ Cu.import("resource://browser_dbg_addon4/test2.jsm", {});
+ let tab2 = yield addTab("chrome://browser_dbg_addon4/content/test2.xul");
+
+ groups = yield addonDebugger.getSourceGroups();
+ is(groups[0].name, "browser_dbg_addon4@tests.mozilla.org", "Add-on code should be the first group");
+ is(groups[1].name, "chrome://global", "XUL code should be the second group");
+ is(groups.length, 2, "Should be only two groups.");
+
+ sources = groups[0].sources;
+ is(sources.length, 5, "Should be five sources");
+ ok(sources[0].url.endsWith("/addon4.xpi!/bootstrap.js"), "correct url for bootstrap code");
+ is(sources[0].label, "bootstrap.js", "correct label for bootstrap code");
+ is(sources[1].url, "resource://browser_dbg_addon4/test.jsm", "correct url for addon code");
+ is(sources[1].label, "test.jsm", "correct label for addon code");
+ is(sources[2].url, "chrome://browser_dbg_addon4/content/testxul.js", "correct url for addon tab code");
+ is(sources[2].label, "testxul.js", "correct label for addon tab code");
+ is(sources[3].url, "resource://browser_dbg_addon4/test2.jsm", "correct url for addon code");
+ is(sources[3].label, "test2.jsm", "correct label for addon code");
+ is(sources[4].url, "chrome://browser_dbg_addon4/content/testxul2.js", "correct url for addon tab code");
+ is(sources[4].label, "testxul2.js", "correct label for addon tab code");
+
+ Cu.unload("resource://browser_dbg_addon4/test2.jsm");
+ yield addonDebugger.destroy();
+ yield removeTab(tab1);
+ yield removeTab(tab2);
+ yield removeAddon(addon);
+ finish();
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_addon-panels.js b/devtools/client/debugger/test/mochitest/browser_dbg_addon-panels.js
new file mode 100644
index 000000000..aeda501b0
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_addon-panels.js
@@ -0,0 +1,49 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Ensure that only panels that are relevant to the addon debugger
+// display in the toolbox
+
+const ADDON_ID = "jid1-ami3akps3baaeg@jetpack";
+const ADDON_PATH = "addon3.xpi";
+
+var gAddon, gClient, gThreadClient, gDebugger, gSources;
+var PREFS = [
+ ["devtools.canvasdebugger.enabled", true],
+ ["devtools.shadereditor.enabled", true],
+ ["devtools.performance.enabled", true],
+ ["devtools.netmonitor.enabled", true],
+ ["devtools.scratchpad.enabled", true]
+];
+function test() {
+ Task.spawn(function* () {
+ // Store and enable all optional dev tools panels
+ yield pushPrefs(...PREFS);
+
+ let addon = yield addTemporaryAddon(ADDON_PATH);
+ let addonDebugger = yield initAddonDebugger(ADDON_ID);
+
+ // Check only valid tabs are shown
+ let tabs = addonDebugger.frame.contentDocument.getElementById("toolbox-tabs").children;
+ let expectedTabs = ["webconsole", "jsdebugger", "scratchpad"];
+
+ is(tabs.length, expectedTabs.length, "displaying only " + expectedTabs.length + " tabs in addon debugger");
+ Array.forEach(tabs, (tab, i) => {
+ let toolName = expectedTabs[i];
+ is(tab.getAttribute("toolid"), toolName, "displaying " + toolName);
+ });
+
+ // Check no toolbox buttons are shown
+ let buttons = addonDebugger.frame.contentDocument.getElementById("toolbox-buttons").children;
+ Array.forEach(buttons, (btn, i) => {
+ is(btn.hidden, true, "no toolbox buttons for the addon debugger -- " + btn.className);
+ });
+
+ yield addonDebugger.destroy();
+ yield removeAddon(addon);
+
+ finish();
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_addon-sources.js b/devtools/client/debugger/test/mochitest/browser_dbg_addon-sources.js
new file mode 100644
index 000000000..eaa4741eb
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_addon-sources.js
@@ -0,0 +1,42 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Ensure that the sources listed when debugging an addon are either from the
+// addon itself, or the SDK, with proper groups and labels.
+
+const ADDON_ID = "jid1-ami3akps3baaeg@jetpack";
+const ADDON_PATH = "addon3.xpi";
+const ADDON_URL = getTemporaryAddonURLFromPath(ADDON_PATH);
+
+var gClient;
+
+function test() {
+ Task.spawn(function* () {
+ let addon = yield addTemporaryAddon(ADDON_PATH);
+ let addonDebugger = yield initAddonDebugger(ADDON_ID);
+
+ is(addonDebugger.title, `Developer Tools - browser_dbg_addon3 - ${ADDON_URL}`,
+ "Saw the right toolbox title.");
+
+ // Check the inital list of sources is correct
+ let groups = yield addonDebugger.getSourceGroups();
+ is(groups[0].name, "jid1-ami3akps3baaeg@jetpack", "Add-on code should be the first group");
+ is(groups[1].name, "Add-on SDK", "Add-on SDK should be the second group");
+ is(groups.length, 2, "Should be only two groups.");
+
+ let sources = groups[0].sources;
+ is(sources.length, 2, "Should be two sources");
+ ok(sources[0].url.endsWith("/addon3.xpi!/bootstrap.js"), "correct url for bootstrap code");
+ is(sources[0].label, "bootstrap.js", "correct label for bootstrap code");
+ is(sources[1].url, "resource://jid1-ami3akps3baaeg-at-jetpack/browser_dbg_addon3/lib/main.js", "correct url for add-on code");
+ is(sources[1].label, "resources/browser_dbg_addon3/lib/main.js", "correct label for add-on code");
+
+ ok(groups[1].sources.length > 10, "SDK modules are listed");
+
+ yield addonDebugger.destroy();
+ yield removeAddon(addon);
+ finish();
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_addon-workers-dbg-enabled.js b/devtools/client/debugger/test/mochitest/browser_dbg_addon-workers-dbg-enabled.js
new file mode 100644
index 000000000..158a3d69e
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_addon-workers-dbg-enabled.js
@@ -0,0 +1,41 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 Addon Debugger works when devtools.debugger.workers is enabled.
+// Workers controller cannot be used when debugging an Addon actor.
+
+const ADDON_ID = "jid1-ami3akps3baaeg@jetpack";
+const ADDON_PATH = "addon3.xpi";
+const ADDON_URL = getTemporaryAddonURLFromPath(ADDON_PATH);
+
+function test() {
+ Task.spawn(function* () {
+ info("Enable worker debugging.");
+ yield new Promise(resolve => {
+ SpecialPowers.pushPrefEnv({
+ "set": [["devtools.debugger.workers", true]]
+ }, resolve);
+ });
+
+ let addon = yield addTemporaryAddon(ADDON_PATH);
+ let addonDebugger = yield initAddonDebugger(ADDON_ID);
+
+ is(addonDebugger.title,
+ `Developer Tools - browser_dbg_addon3 - ${ADDON_URL}`,
+ "Saw the right toolbox title.");
+
+ info("Check that groups and sources are displayed.");
+ let groups = yield addonDebugger.getSourceGroups();
+ is(groups.length, 2, "Should be only two groups.");
+ let sources = groups[0].sources;
+ is(sources.length, 2, "Should be two sources");
+
+ yield addonDebugger.destroy();
+ yield removeAddon(addon);
+ finish();
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_addonactor.js b/devtools/client/debugger/test/mochitest/browser_dbg_addonactor.js
new file mode 100644
index 000000000..1bee0b933
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_addonactor.js
@@ -0,0 +1,95 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Make sure we can attach to addon actors.
+
+const ADDON3_PATH = "addon3.xpi";
+const ADDON3_ID = "jid1-ami3akps3baaeg@jetpack";
+const ADDON_MODULE_URL = "resource://jid1-ami3akps3baaeg-at-jetpack/browser_dbg_addon3/lib/main.js";
+
+var gAddon, gClient, gThreadClient;
+
+function test() {
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+
+ let transport = DebuggerServer.connectPipe();
+ gClient = new DebuggerClient(transport);
+ gClient.connect().then(([aType, aTraits]) => {
+ is(aType, "browser",
+ "Root actor should identify itself as a browser.");
+
+ installAddon()
+ .then(attachAddonActorForId.bind(null, gClient, ADDON3_ID))
+ .then(attachAddonThread)
+ .then(testDebugger)
+ .then(testSources)
+ .then(() => gClient.close())
+ .then(uninstallAddon)
+ .then(finish)
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+ });
+}
+
+function installAddon() {
+ return addTemporaryAddon(ADDON3_PATH).then(aAddon => {
+ gAddon = aAddon;
+ });
+}
+
+function attachAddonThread([aGrip, aResponse]) {
+ info("attached addon actor for Addon ID");
+ let deferred = promise.defer();
+
+ gClient.attachThread(aResponse.threadActor, (aResponse, aThreadClient) => {
+ info("attached thread");
+ gThreadClient = aThreadClient;
+ gThreadClient.resume(deferred.resolve);
+ });
+ return deferred.promise;
+}
+
+function testDebugger() {
+ info("Entering testDebugger");
+ let deferred = promise.defer();
+
+ once(gClient, "paused").then(() => {
+ ok(true, "Should be able to attach to addon actor");
+ gThreadClient.resume(deferred.resolve);
+ });
+
+ Services.obs.notifyObservers(null, "debuggerAttached", null);
+
+ return deferred.promise;
+}
+
+function testSources() {
+ let deferred = promise.defer();
+
+ gThreadClient.getSources(aResponse => {
+ // source URLs contain launch-specific temporary directory path,
+ // hence the ".contains" call.
+ const matches = aResponse.sources.filter(s => s.url.includes(ADDON_MODULE_URL));
+ ok(matches.length > 0,
+ "the main script of the addon is present in the source list");
+ deferred.resolve();
+ });
+
+ return deferred.promise;
+}
+
+function uninstallAddon() {
+ return removeAddon(gAddon);
+}
+
+registerCleanupFunction(function () {
+ gClient = null;
+ gAddon = null;
+ gThreadClient = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_auto-pretty-print-01.js b/devtools/client/debugger/test/mochitest/browser_dbg_auto-pretty-print-01.js
new file mode 100644
index 000000000..dcad48cf8
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_auto-pretty-print-01.js
@@ -0,0 +1,117 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Test auto pretty printing.
+
+const TAB_URL = EXAMPLE_URL + "doc_auto-pretty-print-01.html";
+
+var gTab, gPanel, gDebugger;
+var gEditor, gSources, gPrefs, gOptions, gView;
+
+var gFirstSource = EXAMPLE_URL + "code_ugly-5.js";
+var gSecondSource = EXAMPLE_URL + "code_ugly-6.js";
+
+var gOriginalPref = Services.prefs.getBoolPref("devtools.debugger.auto-pretty-print");
+
+function test() {
+ let options = {
+ source: gFirstSource,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gEditor = gDebugger.DebuggerView.editor;
+ gSources = gDebugger.DebuggerView.Sources;
+ gPrefs = gDebugger.Prefs;
+ gOptions = gDebugger.DebuggerView.Options;
+ gView = gDebugger.DebuggerView;
+
+ Task.spawn(function* () {
+ testSourceIsUgly();
+
+ enableAutoPrettyPrint();
+ testAutoPrettyPrintOn();
+
+ reload(gPanel);
+ yield waitForSourceShown(gPanel, gFirstSource);
+ testSourceIsUgly();
+ yield waitForSourceShown(gPanel, gFirstSource);
+ testSourceIsPretty();
+ disableAutoPrettyPrint();
+ testAutoPrettyPrintOff();
+
+ let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.SOURCE_SHOWN);
+ gSources.selectedIndex = 1;
+ yield finished;
+
+ testSecondSourceLabel();
+ testSourceIsUgly();
+
+ enableAutoPrettyPrint();
+ yield closeDebuggerAndFinish(gPanel);
+ });
+ });
+}
+
+function testSourceIsUgly() {
+ ok(!gEditor.getText().includes("\n "),
+ "The source shouldn't be pretty printed yet.");
+}
+
+function testSecondSourceLabel() {
+ let source = gSources.selectedItem.attachment.source;
+ ok(source.url === gSecondSource,
+ "Second source url is correct.");
+}
+
+function testProgressBarShown() {
+ const deck = gDebugger.document.getElementById("editor-deck");
+ is(deck.selectedIndex, 2, "The progress bar should be shown");
+}
+
+function testAutoPrettyPrintOn() {
+ is(gPrefs.autoPrettyPrint, true,
+ "The auto-pretty-print pref should be on.");
+ is(gOptions._autoPrettyPrint.getAttribute("checked"), "true",
+ "The Auto pretty print menu item should be checked.");
+}
+
+function disableAutoPrettyPrint() {
+ gOptions._autoPrettyPrint.setAttribute("checked", "false");
+ gOptions._toggleAutoPrettyPrint();
+ gOptions._onPopupHidden();
+}
+
+function enableAutoPrettyPrint() {
+ gOptions._autoPrettyPrint.setAttribute("checked", "true");
+ gOptions._toggleAutoPrettyPrint();
+ gOptions._onPopupHidden();
+}
+
+function testAutoPrettyPrintOff() {
+ is(gPrefs.autoPrettyPrint, false,
+ "The auto-pretty-print pref should be off.");
+ isnot(gOptions._autoPrettyPrint.getAttribute("checked"), "true",
+ "The Auto pretty print menu item should not be checked.");
+}
+
+function testSourceIsPretty() {
+ ok(gEditor.getText().includes("\n "),
+ "The source should be pretty printed.");
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gEditor = null;
+ gSources = null;
+ gOptions = null;
+ gPrefs = null;
+ gView = null;
+ Services.prefs.setBoolPref("devtools.debugger.auto-pretty-print", gOriginalPref);
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_auto-pretty-print-02.js b/devtools/client/debugger/test/mochitest/browser_dbg_auto-pretty-print-02.js
new file mode 100644
index 000000000..432bc73d2
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_auto-pretty-print-02.js
@@ -0,0 +1,126 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Test that auto pretty printing doesn't accidentally toggle
+ * pretty printing off when we switch to a minified source
+ * that is already pretty printed.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_auto-pretty-print-02.html";
+
+var gTab, gDebuggee, gPanel, gDebugger;
+var gEditor, gSources, gPrefs, gOptions, gView;
+
+var gFirstSource = EXAMPLE_URL + "code_ugly-6.js";
+var gSecondSource = EXAMPLE_URL + "code_ugly-7.js";
+
+var gOriginalPref = Services.prefs.getBoolPref("devtools.debugger.auto-pretty-print");
+Services.prefs.setBoolPref("devtools.debugger.auto-pretty-print", true);
+
+function test() {
+ let options = {
+ source: gFirstSource,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab, aDebuggee, aPanel]) => {
+ const gTab = aTab;
+ const gDebuggee = aDebuggee;
+ const gPanel = aPanel;
+ const gDebugger = gPanel.panelWin;
+ const gEditor = gDebugger.DebuggerView.editor;
+ const gSources = gDebugger.DebuggerView.Sources;
+ const gPrefs = gDebugger.Prefs;
+ const gOptions = gDebugger.DebuggerView.Options;
+ const gView = gDebugger.DebuggerView;
+
+ // Should be on by default.
+ testAutoPrettyPrintOn();
+
+ Task.spawn(function* () {
+
+ testSourceIsUgly();
+
+ yield waitForSourceShown(gPanel, gFirstSource);
+ testSourceIsPretty();
+ testPrettyPrintButtonOn();
+
+ // select second source
+ yield selectSecondSource();
+ testSecondSourceLabel();
+
+ // select first source
+ yield selectFirstSource();
+ testFirstSourceLabel();
+ testPrettyPrintButtonOn();
+
+ // Disable auto pretty printing so it does not affect the following tests.
+ yield disableAutoPrettyPrint();
+
+ closeDebuggerAndFinish(gPanel)
+ .then(null, aError => {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(aError));
+ });
+ });
+
+ function selectSecondSource() {
+ let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.SOURCE_SHOWN, 2);
+ gSources.selectedIndex = 1;
+ return finished;
+ }
+
+ function selectFirstSource() {
+ let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.SOURCE_SHOWN);
+ gSources.selectedIndex = 0;
+ return finished;
+ }
+
+ function testSourceIsUgly() {
+ ok(!gEditor.getText().includes("\n "),
+ "The source shouldn't be pretty printed yet.");
+ }
+
+ function testFirstSourceLabel() {
+ let source = gSources.selectedItem.attachment.source;
+ ok(source.url === gFirstSource,
+ "First source url is correct.");
+ }
+
+ function testSecondSourceLabel() {
+ let source = gSources.selectedItem.attachment.source;
+ ok(source.url === gSecondSource,
+ "Second source url is correct.");
+ }
+
+ function testAutoPrettyPrintOn() {
+ is(gPrefs.autoPrettyPrint, true,
+ "The auto-pretty-print pref should be on.");
+ is(gOptions._autoPrettyPrint.getAttribute("checked"), "true",
+ "The Auto pretty print menu item should be checked.");
+ }
+
+ function testPrettyPrintButtonOn() {
+ is(gDebugger.document.getElementById("pretty-print").checked, true,
+ "The button should be checked when the source is selected.");
+ }
+
+ function disableAutoPrettyPrint() {
+ gOptions._autoPrettyPrint.setAttribute("checked", "false");
+ gOptions._toggleAutoPrettyPrint();
+ gOptions._onPopupHidden();
+ info("Disabled auto pretty printing.");
+ }
+
+ function testSourceIsPretty() {
+ ok(gEditor.getText().includes("\n "),
+ "The source should be pretty printed.");
+ }
+
+ registerCleanupFunction(function () {
+ Services.prefs.setBoolPref("devtools.debugger.auto-pretty-print", gOriginalPref);
+ });
+
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_auto-pretty-print-03.js b/devtools/client/debugger/test/mochitest/browser_dbg_auto-pretty-print-03.js
new file mode 100644
index 000000000..bf73ef0cc
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_auto-pretty-print-03.js
@@ -0,0 +1,58 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * If auto pretty-printing it enabled, make sure that if
+ * pretty-printing fails that it still properly shows the original
+ * source.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_auto-pretty-print-02.html";
+
+var FIRST_SOURCE = EXAMPLE_URL + "code_ugly-6.js";
+var SECOND_SOURCE = EXAMPLE_URL + "code_ugly-7.js";
+
+function test() {
+ let options = {
+ source: FIRST_SOURCE,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab, aDebuggee, aPanel]) => {
+ const gTab = aTab;
+ const gPanel = aPanel;
+ const gDebugger = gPanel.panelWin;
+
+ const gController = gDebugger.DebuggerController;
+ const gEditor = gDebugger.DebuggerView.editor;
+ const constants = gDebugger.require("./content/constants");
+ const queries = gDebugger.require("./content/queries");
+ const actions = bindActionCreators(gPanel);
+
+ Task.spawn(function* () {
+ const secondSource = queries.getSourceByURL(gController.getState(), SECOND_SOURCE);
+ actions.selectSource(secondSource);
+
+ // It should be showing the loading text
+ is(gEditor.getText(), gDebugger.DebuggerView._loadingText,
+ "The editor loading text is shown");
+
+ gController.dispatch({
+ type: constants.TOGGLE_PRETTY_PRINT,
+ status: "error",
+ source: secondSource,
+ });
+
+ is(gEditor.getText(), gDebugger.DebuggerView._loadingText,
+ "The editor loading text is shown");
+
+ yield waitForSourceShown(gPanel, SECOND_SOURCE);
+
+ ok(gEditor.getText().includes("function foo"),
+ "The second source is shown");
+
+ yield closeDebuggerAndFinish(gPanel);
+ });
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_bfcache.js b/devtools/client/debugger/test/mochitest/browser_dbg_bfcache.js
new file mode 100644
index 000000000..8b3c9ca34
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_bfcache.js
@@ -0,0 +1,95 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure that the debugger is updated with the correct sources when moving
+ * back and forward in the tab.
+ */
+
+const TAB_URL_1 = EXAMPLE_URL + "doc_script-switching-01.html";
+const TAB_URL_2 = EXAMPLE_URL + "doc_recursion-stack.html";
+
+var gTab, gDebuggee, gPanel, gDebugger;
+var gSources;
+
+const test = Task.async(function* () {
+ info("Starting browser_dbg_bfcache.js's `test`.");
+
+ let options = {
+ source: EXAMPLE_URL + "code_script-switching-01.js",
+ line: 1
+ };
+ ([gTab, gDebuggee, gPanel]) = yield initDebugger(TAB_URL_1, options);
+ gDebugger = gPanel.panelWin;
+ gSources = gDebugger.DebuggerView.Sources;
+
+ yield testFirstPage();
+ yield testLocationChange();
+ yield testBack();
+ yield testForward();
+ return closeDebuggerAndFinish(gPanel);
+});
+
+function testFirstPage() {
+ info("Testing first page.");
+
+ // Spin the event loop before causing the debuggee to pause, to allow
+ // this function to return first.
+ executeSoon(() => gDebuggee.firstCall());
+
+ return waitForSourceAndCaretAndScopes(gPanel, "-02.js", 1)
+ .then(validateFirstPage);
+}
+
+function testLocationChange() {
+ info("Navigating to a different page.");
+
+ return navigateActiveTabTo(gPanel,
+ TAB_URL_2,
+ gDebugger.EVENTS.SOURCES_ADDED)
+ .then(validateSecondPage);
+}
+
+function testBack() {
+ info("Going back.");
+
+ return navigateActiveTabInHistory(gPanel,
+ "back",
+ gDebugger.EVENTS.SOURCES_ADDED)
+ .then(validateFirstPage);
+}
+
+function testForward() {
+ info("Going forward.");
+
+ return navigateActiveTabInHistory(gPanel,
+ "forward",
+ gDebugger.EVENTS.SOURCES_ADDED)
+ .then(validateSecondPage);
+}
+
+function validateFirstPage() {
+ is(gSources.itemCount, 2,
+ "Found the expected number of sources.");
+ ok(gSources.getItemForAttachment(e => e.label == "code_script-switching-01.js"),
+ "Found the first source label.");
+ ok(gSources.getItemForAttachment(e => e.label == "code_script-switching-02.js"),
+ "Found the second source label.");
+}
+
+function validateSecondPage() {
+ is(gSources.itemCount, 1,
+ "Found the expected number of sources.");
+ ok(gSources.getItemForAttachment(e => e.label == "doc_recursion-stack.html"),
+ "Found the single source label.");
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gDebuggee = null;
+ gPanel = null;
+ gDebugger = null;
+ gSources = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_blackboxing-01.js b/devtools/client/debugger/test/mochitest/browser_dbg_blackboxing-01.js
new file mode 100644
index 000000000..f4ecd5b95
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_blackboxing-01.js
@@ -0,0 +1,57 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Test that if we black box a source and then refresh, it is still black boxed.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_binary_search.html";
+
+var gTab, gPanel, gDebugger;
+
+function test() {
+ let options = {
+ source: EXAMPLE_URL + "code_binary_search.coffee",
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+
+ testBlackBoxSource()
+ .then(testBlackBoxReload)
+ .then(() => closeDebuggerAndFinish(gPanel))
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+ });
+}
+
+function testBlackBoxSource() {
+ const bbButton = getBlackBoxButton(gPanel);
+ ok(!bbButton.checked, "Should not be black boxed by default");
+
+ return toggleBlackBoxing(gPanel).then(source => {
+ ok(source.isBlackBoxed, "The source should be black boxed now.");
+ ok(bbButton.checked, "The checkbox should no longer be checked.");
+ });
+}
+
+function testBlackBoxReload() {
+ return reloadActiveTab(gPanel, gDebugger.EVENTS.SOURCE_SHOWN).then(() => {
+ const bbButton = getBlackBoxButton(gPanel);
+ const selectedSource = getSelectedSourceElement(gPanel);
+ ok(bbButton.checked, "Should still be black boxed.");
+ ok(selectedSource.classList.contains("black-boxed"),
+ "'black-boxed' class should still be applied");
+ });
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_blackboxing-02.js b/devtools/client/debugger/test/mochitest/browser_dbg_blackboxing-02.js
new file mode 100644
index 000000000..2eca3ec92
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_blackboxing-02.js
@@ -0,0 +1,60 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Test that black boxed frames are compressed into a single frame on the stack
+ * view.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_blackboxing.html";
+const BLACKBOXME_URL = EXAMPLE_URL + "code_blackboxing_blackboxme.js";
+
+var gTab, gPanel, gDebugger;
+var gFrames;
+
+function test() {
+ let options = {
+ source: BLACKBOXME_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gFrames = gDebugger.DebuggerView.StackFrames;
+
+ testBlackBoxSource()
+ .then(testBlackBoxStack)
+ .then(() => resumeDebuggerThenCloseAndFinish(gPanel))
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+ });
+}
+
+function testBlackBoxSource() {
+ return toggleBlackBoxing(gPanel).then(source => {
+ ok(source.isBlackBoxed, "The source should be black boxed now.");
+ });
+}
+
+function testBlackBoxStack() {
+ let finished = waitForSourceAndCaretAndScopes(gPanel, ".html", 21).then(() => {
+ is(gFrames.itemCount, 3,
+ "Should only get 3 frames.");
+ is(gDebugger.document.querySelectorAll(".dbg-stackframe-black-boxed").length, 1,
+ "And one of them should be the combined black boxed frames.");
+ });
+
+ callInTab(gTab, "runTest");
+ return finished;
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gFrames = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_blackboxing-03.js b/devtools/client/debugger/test/mochitest/browser_dbg_blackboxing-03.js
new file mode 100644
index 000000000..fa4489ac7
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_blackboxing-03.js
@@ -0,0 +1,65 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Test that black boxed frames are compressed into a single frame on the stack
+ * view when we are already paused.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_blackboxing.html";
+const BLACKBOXME_URL = EXAMPLE_URL + "code_blackboxing_blackboxme.js";
+
+var gTab, gPanel, gDebugger;
+var gFrames, gSources;
+
+function test() {
+ let options = {
+ source: BLACKBOXME_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gFrames = gDebugger.DebuggerView.StackFrames;
+ gSources = gDebugger.DebuggerView.Sources;
+
+ waitForSourceAndCaretAndScopes(gPanel, ".html", 21)
+ .then(testBlackBoxStack)
+ .then(testBlackBoxSource)
+ .then(() => resumeDebuggerThenCloseAndFinish(gPanel))
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+
+ callInTab(gTab, "runTest");
+ });
+}
+
+function testBlackBoxStack() {
+ is(gFrames.itemCount, 6,
+ "Should get 6 frames.");
+ is(gDebugger.document.querySelectorAll(".dbg-stackframe-black-boxed").length, 0,
+ "And none of them are black boxed.");
+}
+
+function testBlackBoxSource() {
+ return toggleBlackBoxing(gPanel, getSourceActor(gSources, BLACKBOXME_URL)).then(aSource => {
+ ok(aSource.isBlackBoxed, "The source should be black boxed now.");
+
+ is(gFrames.itemCount, 3,
+ "Should only get 3 frames.");
+ is(gDebugger.document.querySelectorAll(".dbg-stackframe-black-boxed").length, 1,
+ "And one of them should be the combined black boxed frames.");
+ });
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gFrames = null;
+ gSources = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_blackboxing-04.js b/devtools/client/debugger/test/mochitest/browser_dbg_blackboxing-04.js
new file mode 100644
index 000000000..ea9cd84f3
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_blackboxing-04.js
@@ -0,0 +1,65 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Test that we get a stack frame for each black boxed source, not a single one
+ * for all of them.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_blackboxing.html";
+const BLACKBOXME_URL = EXAMPLE_URL + "code_blackboxing_blackboxme.js";
+
+var gTab, gPanel, gDebugger;
+var gFrames, gSources;
+
+function test() {
+ let options = {
+ source: BLACKBOXME_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gFrames = gDebugger.DebuggerView.StackFrames;
+ gSources = gDebugger.DebuggerView.Sources;
+
+ blackBoxSources()
+ .then(testBlackBoxStack)
+ .then(() => resumeDebuggerThenCloseAndFinish(gPanel))
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+ });
+}
+
+function blackBoxSources() {
+ let finished = waitForThreadEvents(gPanel, "blackboxchange", 3);
+
+ toggleBlackBoxing(gPanel, getSourceActor(gSources, EXAMPLE_URL + "code_blackboxing_one.js"));
+ toggleBlackBoxing(gPanel, getSourceActor(gSources, EXAMPLE_URL + "code_blackboxing_two.js"));
+ toggleBlackBoxing(gPanel, getSourceActor(gSources, EXAMPLE_URL + "code_blackboxing_three.js"));
+ return finished;
+}
+
+function testBlackBoxStack() {
+ let finished = waitForSourceAndCaretAndScopes(gPanel, ".html", 21).then(() => {
+ is(gFrames.itemCount, 4,
+ "Should get 4 frames (one -> two -> three -> doDebuggerStatement).");
+ is(gDebugger.document.querySelectorAll(".dbg-stackframe-black-boxed").length, 3,
+ "And 'one', 'two', and 'three' should each have their own black boxed frame.");
+ });
+
+ callInTab(gTab, "one");
+ return finished;
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gFrames = null;
+ gSources = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_blackboxing-05.js b/devtools/client/debugger/test/mochitest/browser_dbg_blackboxing-05.js
new file mode 100644
index 000000000..96e2b9873
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_blackboxing-05.js
@@ -0,0 +1,74 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Test that a "this source is blackboxed" message is shown when necessary
+ * and can be properly dismissed.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_binary_search.html";
+
+var gTab, gPanel, gDebugger;
+var gDeck;
+
+function test() {
+ let options = {
+ source: EXAMPLE_URL + "code_binary_search.coffee",
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gDeck = gDebugger.document.getElementById("editor-deck");
+
+ testSourceEditorShown();
+ toggleBlackBoxing(gPanel)
+ .then(testBlackBoxMessageShown)
+ .then(clickStopBlackBoxingButton)
+ .then(testSourceEditorShownAgain)
+ .then(() => closeDebuggerAndFinish(gPanel))
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+ });
+}
+
+function testSourceEditorShown() {
+ is(gDeck.selectedIndex, "0",
+ "The first item in the deck should be selected (the source editor).");
+}
+
+function testBlackBoxMessageShown() {
+ is(gDeck.selectedIndex, "1",
+ "The second item in the deck should be selected (the black box message).");
+}
+
+function clickStopBlackBoxingButton() {
+ // Give the test a chance to finish before triggering the click event.
+ executeSoon(() => getEditorBlackboxMessageButton().click());
+ return waitForDispatch(gPanel, gDebugger.constants.BLACKBOX);
+}
+
+function testSourceEditorShownAgain() {
+ // Wait a tick for the final check to make sure the frontend's click handlers
+ // have finished.
+ return new Promise(resolve => {
+ is(gDeck.selectedIndex, "0",
+ "The first item in the deck should be selected again (the source editor).");
+ resolve();
+ });
+}
+
+function getEditorBlackboxMessageButton() {
+ return gDebugger.document.getElementById("black-boxed-message-button");
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gDeck = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_blackboxing-06.js b/devtools/client/debugger/test/mochitest/browser_dbg_blackboxing-06.js
new file mode 100644
index 000000000..23a13f4db
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_blackboxing-06.js
@@ -0,0 +1,61 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Test that clicking the black box checkbox when paused doesn't re-select the
+ * currently paused frame's source.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_blackboxing.html";
+
+var gTab, gPanel, gDebugger;
+var gSources;
+
+function test() {
+ let options = {
+ source: EXAMPLE_URL + "code_blackboxing_blackboxme.js",
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gSources = gDebugger.DebuggerView.Sources;
+
+ waitForCaretAndScopes(gPanel, 21)
+ .then(testBlackBox)
+ .then(() => resumeDebuggerThenCloseAndFinish(gPanel))
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+
+ callInTab(gTab, "runTest");
+ });
+}
+
+function testBlackBox() {
+ const selectedActor = gSources.selectedValue;
+
+ let finished = waitForSourceShown(gPanel, "blackboxme.js").then(() => {
+ const newSelectedActor = gSources.selectedValue;
+ isnot(selectedActor, newSelectedActor,
+ "Should not have the same url selected.");
+
+ return toggleBlackBoxing(gPanel).then(() => {
+ is(gSources.selectedValue, newSelectedActor,
+ "The selected source did not change.");
+ });
+ });
+
+ gSources.selectedIndex = 0;
+ return finished;
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gSources = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_blackboxing-07.js b/devtools/client/debugger/test/mochitest/browser_dbg_blackboxing-07.js
new file mode 100644
index 000000000..1aa6b0bd1
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_blackboxing-07.js
@@ -0,0 +1,53 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Test that if we unblackbox a source which has been automatically blackboxed
+ * and then refresh, it is still unblackboxed.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_blackboxing_unblackbox.html";
+
+var gTab, gPanel, gDebugger;
+
+function test() {
+ let options = {
+ source: EXAMPLE_URL + "code_blackboxing_unblackbox.min.js",
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+
+ testBlackBoxSource()
+ .then(testBlackBoxReload)
+ .then(() => closeDebuggerAndFinish(gPanel))
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+ });
+}
+
+function testBlackBoxSource() {
+ const bbButton = getBlackBoxButton(gPanel);
+ ok(bbButton.checked, "Should be black boxed by default");
+
+ return toggleBlackBoxing(gPanel).then(aSource => {
+ ok(!aSource.isBlackBoxed, "The source should no longer be blackboxed.");
+ });
+}
+
+function testBlackBoxReload() {
+ return reloadActiveTab(gPanel, gDebugger.EVENTS.SOURCE_SHOWN).then(() => {
+ const selectedSource = getSelectedSourceElement(gPanel);
+ ok(!selectedSource.isBlackBoxed, "The source should not be blackboxed.");
+ });
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_breadcrumbs-access.js b/devtools/client/debugger/test/mochitest/browser_dbg_breadcrumbs-access.js
new file mode 100644
index 000000000..596bcd336
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_breadcrumbs-access.js
@@ -0,0 +1,98 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests if the stackframe breadcrumbs are keyboard accessible.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
+
+function test() {
+ let gTab, gPanel, gDebugger;
+ let gSources, gFrames;
+
+ let options = {
+ source: EXAMPLE_URL + "code_script-switching-01.js",
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gSources = gDebugger.DebuggerView.Sources;
+ gFrames = gDebugger.DebuggerView.StackFrames;
+
+ waitForSourceAndCaretAndScopes(gPanel, "-02.js", 6)
+ .then(checkNavigationWhileNotFocused)
+ .then(focusCurrentStackFrame)
+ .then(checkNavigationWhileFocused)
+ .then(() => resumeDebuggerThenCloseAndFinish(gPanel))
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+
+ callInTab(gTab, "firstCall");
+ });
+
+ function checkNavigationWhileNotFocused() {
+ checkState({ frame: 1, source: 1, line: 6 });
+
+ return Task.spawn(function* () {
+ EventUtils.sendKey("DOWN", gDebugger);
+ checkState({ frame: 1, source: 1, line: 7 });
+
+ EventUtils.sendKey("UP", gDebugger);
+ checkState({ frame: 1, source: 1, line: 6 });
+ });
+ }
+
+ function focusCurrentStackFrame() {
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ gFrames.selectedItem.target,
+ gDebugger);
+ }
+
+ function checkNavigationWhileFocused() {
+ return Task.spawn(function* () {
+ yield promise.all([
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES),
+ waitForSourceAndCaret(gPanel, "-01.js", 5),
+ EventUtils.sendKey("UP", gDebugger)
+ ]);
+ checkState({ frame: 0, source: 0, line: 5 });
+
+ // Need to refocus the stack frame due to a focus bug in e10s
+ // (See Bug 1205482)
+ focusCurrentStackFrame();
+
+ yield promise.all([
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES),
+ waitForSourceAndCaret(gPanel, "-02.js", 6),
+ EventUtils.sendKey("END", gDebugger)
+ ]);
+ checkState({ frame: 1, source: 1, line: 6 });
+
+ // Need to refocus the stack frame due to a focus bug in e10s
+ // (See Bug 1205482)
+ focusCurrentStackFrame();
+
+ yield promise.all([
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES),
+ waitForSourceAndCaret(gPanel, "-01.js", 5),
+ EventUtils.sendKey("HOME", gDebugger)
+ ]);
+ checkState({ frame: 0, source: 0, line: 5 });
+ });
+ }
+
+ function checkState({ frame, source, line, column }) {
+ is(gFrames.selectedIndex, frame,
+ "The currently selected stackframe is incorrect.");
+ is(gSources.selectedIndex, source,
+ "The currently selected source is incorrect.");
+ ok(isCaretPos(gPanel, line, column),
+ "The source editor caret position was incorrect.");
+ }
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_break-in-anon.js b/devtools/client/debugger/test/mochitest/browser_dbg_break-in-anon.js
new file mode 100644
index 000000000..23d55f4b9
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_break-in-anon.js
@@ -0,0 +1,40 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure anonymous eval scripts can still break with a `debugger`
+ * statement
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_script-eval.html";
+
+function test() {
+ const options = {
+ source: EXAMPLE_URL + "code_script-eval.js",
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ const gTab = aTab;
+ const gPanel = aPanel;
+ const gDebugger = gPanel.panelWin;
+ const gSources = gDebugger.DebuggerView.Sources;
+
+ return Task.spawn(function* () {
+ is(gSources.values.length, 1, "Should have 1 source");
+
+ callInTab(gTab, "evalSourceWithDebugger");
+ yield waitForDebuggerEvents(gPanel, gDebugger.EVENTS.SOURCE_SHOWN);
+
+ is(gSources.values.length, 2, "Should have 2 sources");
+
+ let item = gSources.getItemForAttachment(e => e.label.indexOf("SCRIPT") === 0);
+ ok(item, "Source label is incorrect.");
+ is(item.attachment.group, gDebugger.L10N.getStr("anonymousSourcesLabel"),
+ "Source group is incorrect");
+
+ yield resumeDebuggerThenCloseAndFinish(gPanel);
+ });
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_break-on-dom-01.js b/devtools/client/debugger/test/mochitest/browser_dbg_break-on-dom-01.js
new file mode 100644
index 000000000..82481187a
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_break-on-dom-01.js
@@ -0,0 +1,58 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Whitelisting this test.
+// As part of bug 1077403, the leaking uncaught rejections should be fixed.
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed("[object Object]");
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed(
+ "TypeError: this.transport is null");
+
+/**
+ * Tests that event listeners aren't fetched when the events tab isn't selected.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_event-listeners-02.html";
+
+function test() {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ let gPanel = aPanel;
+ let gDebugger = aPanel.panelWin;
+ let gView = gDebugger.DebuggerView;
+ let gEvents = gView.EventListeners;
+ let gController = gDebugger.DebuggerController;
+ let constants = gDebugger.require("./content/constants");
+
+ gDebugger.on(gDebugger.EVENTS.EVENT_LISTENERS_FETCHED, () => {
+ ok(false, "Shouldn't have fetched any event listeners.");
+ });
+ gDebugger.on(gDebugger.EVENTS.EVENT_BREAKPOINTS_UPDATED, () => {
+ ok(false, "Shouldn't have updated any event breakpoints.");
+ });
+
+ gView.toggleInstrumentsPane({ visible: true, animated: false });
+
+ is(gView.instrumentsPaneHidden, false,
+ "The instruments pane should be visible now.");
+ is(gView.instrumentsPaneTab, "variables-tab",
+ "The variables tab should be selected by default.");
+
+ Task.spawn(function* () {
+ is(gEvents.itemCount, 0, "There should be no events before reloading.");
+
+ let reloaded = waitForNavigation(gPanel);
+ gDebugger.DebuggerController._target.activeTab.reload();
+
+ is(gEvents.itemCount, 0, "There should be no events while reloading.");
+ yield reloaded;
+ is(gEvents.itemCount, 0, "There should be no events after reloading.");
+
+ yield closeDebuggerAndFinish(aPanel);
+ });
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_break-on-dom-02.js b/devtools/client/debugger/test/mochitest/browser_dbg_break-on-dom-02.js
new file mode 100644
index 000000000..dcbb1dca1
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_break-on-dom-02.js
@@ -0,0 +1,135 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests that event listeners are fetched when the events tab is selected
+ * or while sources are fetched and the events tab is focused.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_event-listeners-02.html";
+
+function test() {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ let gPanel = aPanel;
+ let gDebugger = aPanel.panelWin;
+ let gView = gDebugger.DebuggerView;
+ let gEvents = gView.EventListeners;
+ let gController = gDebugger.DebuggerController;
+ let constants = gDebugger.require("./content/constants");
+
+ Task.spawn(function* () {
+ yield testFetchOnFocus();
+ yield testFetchOnReloadWhenFocused();
+ yield testFetchOnReloadWhenNotFocused();
+ yield closeDebuggerAndFinish(aPanel);
+ });
+
+ function testFetchOnFocus() {
+ return Task.spawn(function* () {
+ let fetched = waitForDispatch(aPanel, constants.FETCH_EVENT_LISTENERS);
+
+ gView.toggleInstrumentsPane({ visible: true, animated: false }, 1);
+ is(gView.instrumentsPaneHidden, false,
+ "The instruments pane should be visible now.");
+ is(gView.instrumentsPaneTab, "events-tab",
+ "The events tab should be selected.");
+
+ yield fetched;
+
+ ok(true,
+ "Event listeners were fetched when the events tab was selected");
+ is(gEvents.itemCount, 4,
+ "There should be 4 events displayed in the view.");
+ });
+ }
+
+ function testFetchOnReloadWhenFocused() {
+ return Task.spawn(function* () {
+ let fetched = waitForDispatch(aPanel, constants.FETCH_EVENT_LISTENERS);
+
+ let reloading = once(gDebugger.gTarget, "will-navigate");
+ let reloaded = waitForNavigation(gPanel);
+ gDebugger.DebuggerController._target.activeTab.reload();
+
+ yield reloading;
+
+ is(gEvents.itemCount, 0,
+ "There should be no events displayed in the view while reloading.");
+ ok(true,
+ "Event listeners were removed when the target started navigating.");
+
+ yield reloaded;
+
+ is(gView.instrumentsPaneHidden, false,
+ "The instruments pane should still be visible.");
+ is(gView.instrumentsPaneTab, "events-tab",
+ "The events tab should still be selected.");
+
+ yield fetched;
+
+ is(gEvents.itemCount, 4,
+ "There should be 4 events displayed in the view after reloading.");
+ ok(true,
+ "Event listeners were added back after the target finished navigating.");
+ });
+ }
+
+ function testFetchOnReloadWhenNotFocused() {
+ return Task.spawn(function* () {
+ gController.dispatch({
+ type: gDebugger.services.WAIT_UNTIL,
+ predicate: action => {
+ return (action.type === constants.FETCH_EVENT_LISTENERS ||
+ action.type === constants.UPDATE_EVENT_BREAKPOINTS);
+ },
+ run: (dispatch, getState, action) => {
+ if (action.type === constants.FETCH_EVENT_LISTENERS) {
+ ok(false, "Shouldn't have fetched any event listeners.");
+ }
+ else if (action.type === constants.UPDATE_EVENT_BREAKPOINTS) {
+ ok(false, "Shouldn't have updated any event breakpoints.");
+ }
+ }
+ });
+
+ gView.toggleInstrumentsPane({ visible: true, animated: false }, 0);
+ is(gView.instrumentsPaneHidden, false,
+ "The instruments pane should still be visible.");
+ is(gView.instrumentsPaneTab, "variables-tab",
+ "The variables tab should be selected.");
+
+ let reloading = once(gDebugger.gTarget, "will-navigate");
+ let reloaded = waitForNavigation(gPanel);
+ gDebugger.DebuggerController._target.activeTab.reload();
+
+ yield reloading;
+
+ is(gEvents.itemCount, 0,
+ "There should be no events displayed in the view while reloading.");
+ ok(true,
+ "Event listeners were removed when the target started navigating.");
+
+ yield reloaded;
+
+ is(gView.instrumentsPaneHidden, false,
+ "The instruments pane should still be visible.");
+ is(gView.instrumentsPaneTab, "variables-tab",
+ "The variables tab should still be selected.");
+
+ // Just to be really sure that the events will never ever fire.
+ yield waitForTime(1000);
+
+ is(gEvents.itemCount, 0,
+ "There should be no events displayed in the view after reloading.");
+ ok(true,
+ "Event listeners were not added after the target finished navigating.");
+ });
+ }
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_break-on-dom-03.js b/devtools/client/debugger/test/mochitest/browser_dbg_break-on-dom-03.js
new file mode 100644
index 000000000..6d0d9708c
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_break-on-dom-03.js
@@ -0,0 +1,102 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests that event listeners are properly displayed in the view.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_event-listeners-02.html";
+
+function test() {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ let gDebugger = aPanel.panelWin;
+ let gView = gDebugger.DebuggerView;
+ let gEvents = gView.EventListeners;
+ let gController = gDebugger.DebuggerController;
+ let constants = gDebugger.require("./content/constants");
+
+ Task.spawn(function* () {
+ let fetched = waitForDispatch(aPanel, constants.FETCH_EVENT_LISTENERS);
+ gView.toggleInstrumentsPane({ visible: true, animated: false }, 1);
+ yield fetched;
+
+ is(gEvents.widget._parent.querySelectorAll(".side-menu-widget-group").length, 3,
+ "There should be 3 groups shown in the view.");
+
+ let groupCheckboxes = gEvents.widget._parent.querySelectorAll(
+ ".side-menu-widget-group-checkbox");
+ is(groupCheckboxes.length, 3,
+ "There should be a checkbox for each group shown in the view.");
+ for (let cb of groupCheckboxes) {
+ isnot(cb.getAttribute("tooltiptext"), "undefined",
+ "A valid tooltip text should be defined on group checkboxes");
+ }
+
+ is(gEvents.widget._parent.querySelectorAll(".side-menu-widget-item").length, 4,
+ "There should be 4 items shown in the view.");
+ is(gEvents.widget._parent.querySelectorAll(".side-menu-widget-item-checkbox").length, 4,
+ "There should be a checkbox for each item shown in the view.");
+
+ testEventItem(0, "doc_event-listeners-02.html", "change", ["body > input:nth-child(2)"], false);
+ testEventItem(1, "doc_event-listeners-02.html", "click", ["body > button:nth-child(1)"], false);
+ testEventItem(2, "doc_event-listeners-02.html", "keydown", ["window", "body"], false);
+ testEventItem(3, "doc_event-listeners-02.html", "keyup", ["body > input:nth-child(2)"], false);
+
+ testEventGroup("interactionEvents", false);
+ testEventGroup("keyboardEvents", false);
+ testEventGroup("mouseEvents", false);
+
+ is(gEvents.getAllEvents().toString(), "change,click,keydown,keyup",
+ "The getAllEvents() method returns the correct stuff.");
+ is(gEvents.getCheckedEvents().toString(), "",
+ "The getCheckedEvents() method returns the correct stuff.");
+
+ yield ensureThreadClientState(aPanel, "attached");
+ yield closeDebuggerAndFinish(aPanel);
+ });
+
+ function testEventItem(index, label, type, selectors, checked) {
+ let item = gEvents.items[index];
+ let node = item.target;
+
+ ok(item.attachment.url.includes(label),
+ "The event at index " + index + " has the correct url.");
+ is(item.attachment.type, type,
+ "The event at index " + index + " has the correct type.");
+ is(item.attachment.selectors.toString(), selectors,
+ "The event at index " + index + " has the correct selectors.");
+ is(item.attachment.checkboxState, checked,
+ "The event at index " + index + " has the correct checkbox state.");
+
+ let targets = selectors.length > 1
+ ? gDebugger.L10N.getFormatStr("eventNodes", selectors.length)
+ : selectors.toString();
+
+ is(node.querySelector(".dbg-event-listener-type").getAttribute("value"), type,
+ "The correct type is shown for this event.");
+ is(node.querySelector(".dbg-event-listener-targets").getAttribute("value"), targets,
+ "The correct target is shown for this event.");
+ is(node.querySelector(".dbg-event-listener-location").getAttribute("value"), label,
+ "The correct location is shown for this event.");
+ is(node.parentNode.querySelector(".side-menu-widget-item-checkbox").checked, checked,
+ "The correct checkbox state is shown for this event.");
+ }
+
+ function testEventGroup(string, checked) {
+ let name = gDebugger.L10N.getStr(string);
+ let group = gEvents.widget._parent
+ .querySelector(".side-menu-widget-group[name=" + name + "]");
+
+ is(group.querySelector(".side-menu-widget-group-title > .name").value, name,
+ "The correct label is shown for the group named " + name + ".");
+ is(group.querySelector(".side-menu-widget-group-checkbox").checked, checked,
+ "The correct checkbox state is shown for the group named " + name + ".");
+ }
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_break-on-dom-04.js b/devtools/client/debugger/test/mochitest/browser_dbg_break-on-dom-04.js
new file mode 100644
index 000000000..3f2b2948e
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_break-on-dom-04.js
@@ -0,0 +1,101 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests that checking/unchecking an event listener in the view correctly
+ * causes the active thread to get updated with the new event breakpoints.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_event-listeners-02.html";
+
+function test() {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ let gDebugger = aPanel.panelWin;
+ let gView = gDebugger.DebuggerView;
+ let gController = gDebugger.DebuggerController;
+ let gEvents = gView.EventListeners;
+ let constants = gDebugger.require("./content/constants");
+
+ Task.spawn(function* () {
+ let fetched = waitForDispatch(aPanel, constants.FETCH_EVENT_LISTENERS);
+ gView.toggleInstrumentsPane({ visible: true, animated: false }, 1);
+ yield fetched;
+
+ testEventItem(0, false);
+ testEventItem(1, false);
+ testEventItem(2, false);
+ testEventItem(3, false);
+ testEventGroup("interactionEvents", false);
+ testEventGroup("keyboardEvents", false);
+ testEventGroup("mouseEvents", false);
+ testEventArrays("change,click,keydown,keyup", "");
+
+ let updated = waitForDispatch(aPanel, constants.UPDATE_EVENT_BREAKPOINTS);
+ EventUtils.sendMouseEvent({ type: "click" }, getItemCheckboxNode(0), gDebugger);
+ yield updated;
+
+ testEventItem(0, true);
+ testEventItem(1, false);
+ testEventItem(2, false);
+ testEventItem(3, false);
+ testEventGroup("interactionEvents", false);
+ testEventGroup("keyboardEvents", false);
+ testEventGroup("mouseEvents", false);
+ testEventArrays("change,click,keydown,keyup", "change");
+
+ updated = waitForDispatch(aPanel, constants.UPDATE_EVENT_BREAKPOINTS);
+ EventUtils.sendMouseEvent({ type: "click" }, getItemCheckboxNode(0), gDebugger);
+ yield updated;
+
+ testEventItem(0, false);
+ testEventItem(1, false);
+ testEventItem(2, false);
+ testEventItem(3, false);
+ testEventGroup("interactionEvents", false);
+ testEventGroup("keyboardEvents", false);
+ testEventGroup("mouseEvents", false);
+ testEventArrays("change,click,keydown,keyup", "");
+
+ yield ensureThreadClientState(aPanel, "attached");
+ yield closeDebuggerAndFinish(aPanel);
+ });
+
+ function getItemCheckboxNode(index) {
+ return gEvents.items[index].target.parentNode
+ .querySelector(".side-menu-widget-item-checkbox");
+ }
+
+ function getGroupCheckboxNode(string) {
+ return gEvents.widget._parent
+ .querySelector(".side-menu-widget-group[name=" + gDebugger.L10N.getStr(string) + "]")
+ .querySelector(".side-menu-widget-group-checkbox");
+ }
+
+ function testEventItem(index, checked) {
+ is(gEvents.attachments[index].checkboxState, checked,
+ "The event at index " + index + " has the correct checkbox state.");
+ is(getItemCheckboxNode(index).checked, checked,
+ "The correct checkbox state is shown for this event.");
+ }
+
+ function testEventGroup(string, checked) {
+ is(getGroupCheckboxNode(string).checked, checked,
+ "The correct checkbox state is shown for the group " + string + ".");
+ }
+
+ function testEventArrays(all, checked) {
+ is(gEvents.getAllEvents().toString(), all,
+ "The getAllEvents() method returns the correct stuff.");
+ is(gEvents.getCheckedEvents().toString(), checked,
+ "The getCheckedEvents() method returns the correct stuff.");
+ is(gController.getState().eventListeners.activeEventNames.toString(), checked,
+ "The correct event names are listed as being active breakpoints.");
+ }
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_break-on-dom-05.js b/devtools/client/debugger/test/mochitest/browser_dbg_break-on-dom-05.js
new file mode 100644
index 000000000..d0a552e81
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_break-on-dom-05.js
@@ -0,0 +1,128 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests that checking/unchecking an event listener's group in the view will
+ * cause the active thread to get updated with the new event breakpoints for
+ * all children inside that group.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_event-listeners-02.html";
+
+function test() {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ let gDebugger = aPanel.panelWin;
+ let gView = gDebugger.DebuggerView;
+ let gController = gDebugger.DebuggerController;
+ let gEvents = gView.EventListeners;
+ let constants = gDebugger.require("./content/constants");
+
+ Task.spawn(function* () {
+ let fetched = waitForDispatch(aPanel, constants.FETCH_EVENT_LISTENERS);
+ gView.toggleInstrumentsPane({ visible: true, animated: false }, 1);
+ yield fetched;
+
+ testEventItem(0, false);
+ testEventItem(1, false);
+ testEventItem(2, false);
+ testEventItem(3, false);
+ testEventGroup("interactionEvents", false);
+ testEventGroup("keyboardEvents", false);
+ testEventGroup("mouseEvents", false);
+ testEventArrays("change,click,keydown,keyup", "");
+
+ let updated = waitForDispatch(aPanel, constants.UPDATE_EVENT_BREAKPOINTS);
+ EventUtils.sendMouseEvent({ type: "click" }, getGroupCheckboxNode("interactionEvents"), gDebugger);
+ yield updated;
+
+ testEventItem(0, true);
+ testEventItem(1, false);
+ testEventItem(2, false);
+ testEventItem(3, false);
+ testEventGroup("interactionEvents", true);
+ testEventGroup("keyboardEvents", false);
+ testEventGroup("mouseEvents", false);
+ testEventArrays("change,click,keydown,keyup", "change");
+
+ updated = waitForDispatch(aPanel, constants.UPDATE_EVENT_BREAKPOINTS);
+ EventUtils.sendMouseEvent({ type: "click" }, getGroupCheckboxNode("interactionEvents"), gDebugger);
+ yield updated;
+
+ testEventItem(0, false);
+ testEventItem(1, false);
+ testEventItem(2, false);
+ testEventItem(3, false);
+ testEventGroup("interactionEvents", false);
+ testEventGroup("keyboardEvents", false);
+ testEventGroup("mouseEvents", false);
+ testEventArrays("change,click,keydown,keyup", "");
+
+ updated = waitForDispatch(aPanel, constants.UPDATE_EVENT_BREAKPOINTS);
+ EventUtils.sendMouseEvent({ type: "click" }, getGroupCheckboxNode("keyboardEvents"), gDebugger);
+ yield updated;
+
+ testEventItem(0, false);
+ testEventItem(1, false);
+ testEventItem(2, true);
+ testEventItem(3, true);
+ testEventGroup("interactionEvents", false);
+ testEventGroup("keyboardEvents", true);
+ testEventGroup("mouseEvents", false);
+ testEventArrays("change,click,keydown,keyup", "keydown,keyup");
+
+ updated = waitForDispatch(aPanel, constants.UPDATE_EVENT_BREAKPOINTS);
+ EventUtils.sendMouseEvent({ type: "click" }, getGroupCheckboxNode("keyboardEvents"), gDebugger);
+ yield updated;
+
+ testEventItem(0, false);
+ testEventItem(1, false);
+ testEventItem(2, false);
+ testEventItem(3, false);
+ testEventGroup("interactionEvents", false);
+ testEventGroup("keyboardEvents", false);
+ testEventGroup("mouseEvents", false);
+ testEventArrays("change,click,keydown,keyup", "");
+
+ yield ensureThreadClientState(aPanel, "attached");
+ yield closeDebuggerAndFinish(aPanel);
+ });
+
+ function getItemCheckboxNode(index) {
+ return gEvents.items[index].target.parentNode
+ .querySelector(".side-menu-widget-item-checkbox");
+ }
+
+ function getGroupCheckboxNode(string) {
+ return gEvents.widget._parent
+ .querySelector(".side-menu-widget-group[name=" + gDebugger.L10N.getStr(string) + "]")
+ .querySelector(".side-menu-widget-group-checkbox");
+ }
+
+ function testEventItem(index, checked) {
+ is(gEvents.attachments[index].checkboxState, checked,
+ "The event at index " + index + " has the correct checkbox state.");
+ is(getItemCheckboxNode(index).checked, checked,
+ "The correct checkbox state is shown for this event.");
+ }
+
+ function testEventGroup(string, checked) {
+ is(getGroupCheckboxNode(string).checked, checked,
+ "The correct checkbox state is shown for the group " + string + ".");
+ }
+
+ function testEventArrays(all, checked) {
+ is(gEvents.getAllEvents().toString(), all,
+ "The getAllEvents() method returns the correct stuff.");
+ is(gEvents.getCheckedEvents().toString(), checked,
+ "The getCheckedEvents() method returns the correct stuff.");
+ is(gController.getState().eventListeners.activeEventNames.toString(), checked,
+ "The correct event names are listed as being active breakpoints.");
+ }
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_break-on-dom-06.js b/devtools/client/debugger/test/mochitest/browser_dbg_break-on-dom-06.js
new file mode 100644
index 000000000..3197b640a
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_break-on-dom-06.js
@@ -0,0 +1,130 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests that the event listener states are preserved in the view after the
+ * target navigates.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_event-listeners-02.html";
+
+function test() {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ let gDebugger = aPanel.panelWin;
+ let gView = gDebugger.DebuggerView;
+ let gController = gDebugger.DebuggerController;
+ let gEvents = gView.EventListeners;
+ let gBreakpoints = gController.Breakpoints;
+ let constants = gDebugger.require("./content/constants");
+
+ Task.spawn(function* () {
+ let fetched = waitForDispatch(aPanel, constants.FETCH_EVENT_LISTENERS);
+ gView.toggleInstrumentsPane({ visible: true, animated: false }, 1);
+ yield fetched;
+
+ testEventItem(0, false);
+ testEventItem(1, false);
+ testEventItem(2, false);
+ testEventItem(3, false);
+ testEventGroup("interactionEvents", false);
+ testEventGroup("keyboardEvents", false);
+ testEventGroup("mouseEvents", false);
+ testEventArrays("change,click,keydown,keyup", "");
+
+ let updated = waitForDispatch(aPanel, constants.UPDATE_EVENT_BREAKPOINTS);
+ EventUtils.sendMouseEvent({ type: "click" }, getItemCheckboxNode(0), gDebugger);
+ EventUtils.sendMouseEvent({ type: "click" }, getItemCheckboxNode(1), gDebugger);
+ EventUtils.sendMouseEvent({ type: "click" }, getItemCheckboxNode(2), gDebugger);
+ yield updated;
+
+ testEventItem(0, true);
+ testEventItem(1, true);
+ testEventItem(2, true);
+ testEventItem(3, false);
+ testEventGroup("interactionEvents", false);
+ testEventGroup("keyboardEvents", false);
+ testEventGroup("mouseEvents", false);
+ testEventArrays("change,click,keydown,keyup", "change,click,keydown");
+
+ reload(aPanel);
+ yield waitForDispatch(aPanel, constants.FETCH_EVENT_LISTENERS);
+
+ testEventItem(0, true);
+ testEventItem(1, true);
+ testEventItem(2, true);
+ testEventItem(3, false);
+ testEventGroup("interactionEvents", false);
+ testEventGroup("keyboardEvents", false);
+ testEventGroup("mouseEvents", false);
+ testEventArrays("change,click,keydown,keyup", "change,click,keydown");
+
+ updated = waitForDispatch(aPanel, constants.UPDATE_EVENT_BREAKPOINTS);
+ EventUtils.sendMouseEvent({ type: "click" }, getItemCheckboxNode(0), gDebugger);
+ EventUtils.sendMouseEvent({ type: "click" }, getItemCheckboxNode(1), gDebugger);
+ EventUtils.sendMouseEvent({ type: "click" }, getItemCheckboxNode(2), gDebugger);
+ yield updated;
+
+ testEventItem(0, false);
+ testEventItem(1, false);
+ testEventItem(2, false);
+ testEventItem(3, false);
+ testEventGroup("interactionEvents", false);
+ testEventGroup("keyboardEvents", false);
+ testEventGroup("mouseEvents", false);
+ testEventArrays("change,click,keydown,keyup", "");
+
+ reload(aPanel);
+ yield waitForDispatch(aPanel, constants.FETCH_EVENT_LISTENERS);
+
+ testEventItem(0, false);
+ testEventItem(1, false);
+ testEventItem(2, false);
+ testEventItem(3, false);
+ testEventGroup("interactionEvents", false);
+ testEventGroup("keyboardEvents", false);
+ testEventGroup("mouseEvents", false);
+ testEventArrays("change,click,keydown,keyup", "");
+
+ yield ensureThreadClientState(aPanel, "attached");
+ yield closeDebuggerAndFinish(aPanel);
+ });
+
+ function getItemCheckboxNode(index) {
+ return gEvents.items[index].target.parentNode
+ .querySelector(".side-menu-widget-item-checkbox");
+ }
+
+ function getGroupCheckboxNode(string) {
+ return gEvents.widget._parent
+ .querySelector(".side-menu-widget-group[name=" + gDebugger.L10N.getStr(string) + "]")
+ .querySelector(".side-menu-widget-group-checkbox");
+ }
+
+ function testEventItem(index, checked) {
+ is(gEvents.attachments[index].checkboxState, checked,
+ "The event at index " + index + " has the correct checkbox state.");
+ is(getItemCheckboxNode(index).checked, checked,
+ "The correct checkbox state is shown for this event.");
+ }
+
+ function testEventGroup(string, checked) {
+ is(getGroupCheckboxNode(string).checked, checked,
+ "The correct checkbox state is shown for the group " + string + ".");
+ }
+
+ function testEventArrays(all, checked) {
+ is(gEvents.getAllEvents().toString(), all,
+ "The getAllEvents() method returns the correct stuff.");
+ is(gEvents.getCheckedEvents().toString(), checked,
+ "The getCheckedEvents() method returns the correct stuff.");
+ is(gController.getState().eventListeners.activeEventNames.toString(), checked,
+ "The correct event names are listed as being active breakpoints.");
+ }
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_break-on-dom-07.js b/devtools/client/debugger/test/mochitest/browser_dbg_break-on-dom-07.js
new file mode 100644
index 000000000..107eab5f8
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_break-on-dom-07.js
@@ -0,0 +1,106 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests that system event listeners don't get duplicated in the view.
+ */
+
+function test() {
+ initDebugger().then(([aTab,, aPanel]) => {
+ let gDebugger = aPanel.panelWin;
+ let gView = gDebugger.DebuggerView;
+ let gEvents = gView.EventListeners;
+ let gL10N = gDebugger.L10N;
+
+ is(gEvents.itemCount, 0,
+ "There are no events displayed in the corresponding pane yet.");
+
+ gEvents.addListener({
+ type: "foo",
+ node: { selector: "#first" },
+ function: { url: null }
+ });
+
+ is(gEvents.itemCount, 1,
+ "There was a system event listener added in the view.");
+ is(gEvents.attachments[0].url, gL10N.getStr("eventNative"),
+ "The correct string is used as the event's url.");
+ is(gEvents.attachments[0].type, "foo",
+ "The correct string is used as the event's type.");
+ is(gEvents.attachments[0].selectors.toString(), "#first",
+ "The correct array of selectors is used as the event's target.");
+
+ gEvents.addListener({
+ type: "bar",
+ node: { selector: "#second" },
+ function: { url: null }
+ });
+
+ is(gEvents.itemCount, 2,
+ "There was another system event listener added in the view.");
+ is(gEvents.attachments[1].url, gL10N.getStr("eventNative"),
+ "The correct string is used as the event's url.");
+ is(gEvents.attachments[1].type, "bar",
+ "The correct string is used as the event's type.");
+ is(gEvents.attachments[1].selectors.toString(), "#second",
+ "The correct array of selectors is used as the event's target.");
+
+ gEvents.addListener({
+ type: "foo",
+ node: { selector: "#first" },
+ function: { url: null }
+ });
+
+ is(gEvents.itemCount, 2,
+ "There wasn't another system event listener added in the view.");
+ is(gEvents.attachments[0].url, gL10N.getStr("eventNative"),
+ "The correct string is used as the event's url.");
+ is(gEvents.attachments[0].type, "foo",
+ "The correct string is used as the event's type.");
+ is(gEvents.attachments[0].selectors.toString(), "#first",
+ "The correct array of selectors is used as the event's target.");
+
+ gEvents.addListener({
+ type: "foo",
+ node: { selector: "#second" },
+ function: { url: null }
+ });
+
+ is(gEvents.itemCount, 2,
+ "There still wasn't another system event listener added in the view.");
+ is(gEvents.attachments[0].url, gL10N.getStr("eventNative"),
+ "The correct string is used as the event's url.");
+ is(gEvents.attachments[0].type, "foo",
+ "The correct string is used as the event's type.");
+ is(gEvents.attachments[0].selectors.toString(), "#first,#second",
+ "The correct array of selectors is used as the event's target.");
+
+
+ gEvents.addListener({
+ type: null,
+ node: { selector: "#bogus" },
+ function: { url: null }
+ });
+
+ is(gEvents.itemCount, 2,
+ "No bogus system event listener was added in the view.");
+
+ is(gEvents.attachments[0].url, gL10N.getStr("eventNative"),
+ "The correct string is used as the first event's url.");
+ is(gEvents.attachments[0].type, "foo",
+ "The correct string is used as the first event's type.");
+ is(gEvents.attachments[0].selectors.toString(), "#first,#second",
+ "The correct array of selectors is used as the first event's target.");
+
+ is(gEvents.attachments[1].url, gL10N.getStr("eventNative"),
+ "The correct string is used as the second event's url.");
+ is(gEvents.attachments[1].type, "bar",
+ "The correct string is used as the second event's type.");
+ is(gEvents.attachments[1].selectors.toString(), "#second",
+ "The correct array of selectors is used as the second event's target.");
+
+ closeDebuggerAndFinish(aPanel);
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_break-on-dom-08.js b/devtools/client/debugger/test/mochitest/browser_dbg_break-on-dom-08.js
new file mode 100644
index 000000000..c2bad132b
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_break-on-dom-08.js
@@ -0,0 +1,61 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests that breaking on an event selects the variables view tab.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_event-listeners-02.html";
+
+function test() {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ let gTab = aTab;
+ let gDebugger = aPanel.panelWin;
+ let gView = gDebugger.DebuggerView;
+ let gEvents = gView.EventListeners;
+ let gController = gDebugger.DebuggerController;
+ let constants = gDebugger.require("./content/constants");
+
+ Task.spawn(function* () {
+ yield callInTab(gTab, "addBodyClickEventListener");
+
+ let fetched = waitForDispatch(aPanel, constants.FETCH_EVENT_LISTENERS);
+ gView.toggleInstrumentsPane({ visible: true, animated: false }, 1);
+ yield fetched;
+ yield ensureThreadClientState(aPanel, "attached");
+
+ is(gView.instrumentsPaneHidden, false,
+ "The instruments pane should be visible.");
+ is(gView.instrumentsPaneTab, "events-tab",
+ "The events tab should be selected.");
+
+ let updated = waitForDispatch(aPanel, constants.UPDATE_EVENT_BREAKPOINTS);
+ EventUtils.sendMouseEvent({ type: "click" }, getItemCheckboxNode(1), gDebugger);
+ yield updated;
+ yield ensureThreadClientState(aPanel, "attached");
+
+ let paused = waitForCaretAndScopes(aPanel, 48);
+ generateMouseClickInTab(gTab, "content.document.body");
+ yield paused;
+ yield ensureThreadClientState(aPanel, "paused");
+
+ is(gView.instrumentsPaneHidden, false,
+ "The instruments pane should be visible.");
+ is(gView.instrumentsPaneTab, "variables-tab",
+ "The variables tab should be selected.");
+
+ yield resumeDebuggerThenCloseAndFinish(aPanel);
+ });
+
+ function getItemCheckboxNode(index) {
+ return gEvents.items[index].target.parentNode
+ .querySelector(".side-menu-widget-item-checkbox");
+ }
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_break-on-dom-event-01.js b/devtools/client/debugger/test/mochitest/browser_dbg_break-on-dom-event-01.js
new file mode 100644
index 000000000..a066b7d6b
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_break-on-dom-event-01.js
@@ -0,0 +1,225 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests that the break-on-dom-events request works.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_event-listeners-01.html";
+
+var gClient, gThreadClient, gInput, gButton;
+
+function test() {
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+
+ let transport = DebuggerServer.connectPipe();
+ gClient = new DebuggerClient(transport);
+ gClient.connect().then(([aType, aTraits]) => {
+ is(aType, "browser",
+ "Root actor should identify itself as a browser.");
+
+ addTab(TAB_URL)
+ .then(() => attachThreadActorForUrl(gClient, TAB_URL))
+ .then(setupGlobals)
+ .then(pauseDebuggee)
+ .then(testBreakOnAll)
+ .then(testBreakOnDisabled)
+ .then(testBreakOnNone)
+ .then(testBreakOnClick)
+ .then(() => gClient.close())
+ .then(finish)
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+ });
+}
+
+function setupGlobals(aThreadClient) {
+ gThreadClient = aThreadClient;
+ gInput = content.document.querySelector("input");
+ gButton = content.document.querySelector("button");
+}
+
+function pauseDebuggee() {
+ let deferred = promise.defer();
+
+ gClient.addOneTimeListener("paused", (aEvent, aPacket) => {
+ is(aPacket.type, "paused",
+ "We should now be paused.");
+ is(aPacket.why.type, "debuggerStatement",
+ "The debugger statement was hit.");
+
+ deferred.resolve();
+ });
+
+ // Spin the event loop before causing the debuggee to pause, to allow
+ // this function to return first.
+ executeSoon(triggerButtonClick);
+
+ return deferred.promise;
+}
+
+// Test pause on all events.
+function testBreakOnAll() {
+ let deferred = promise.defer();
+
+ // Test calling pauseOnDOMEvents from a paused state.
+ gThreadClient.pauseOnDOMEvents("*", (aPacket) => {
+ is(aPacket.error, undefined,
+ "The pause-on-any-event request completed successfully.");
+
+ gClient.addOneTimeListener("paused", (aEvent, aPacket) => {
+ is(aPacket.why.type, "pauseOnDOMEvents",
+ "A hidden breakpoint was hit.");
+ is(aPacket.frame.callee.name, "keyupHandler",
+ "The keyupHandler is entered.");
+
+ gClient.addOneTimeListener("paused", (aEvent, aPacket) => {
+ is(aPacket.why.type, "pauseOnDOMEvents",
+ "A hidden breakpoint was hit.");
+ is(aPacket.frame.callee.name, "clickHandler",
+ "The clickHandler is entered.");
+
+ gClient.addOneTimeListener("paused", (aEvent, aPacket) => {
+ is(aPacket.why.type, "pauseOnDOMEvents",
+ "A hidden breakpoint was hit.");
+ is(aPacket.frame.callee.name, "onchange",
+ "The onchange handler is entered.");
+
+ gThreadClient.resume(deferred.resolve);
+ });
+
+ gThreadClient.resume(triggerInputChange);
+ });
+
+ gThreadClient.resume(triggerButtonClick);
+ });
+
+ gThreadClient.resume(triggerInputKeyup);
+ });
+
+ return deferred.promise;
+}
+
+// Test that removing events from the array disables them.
+function testBreakOnDisabled() {
+ let deferred = promise.defer();
+
+ // Test calling pauseOnDOMEvents from a running state.
+ gThreadClient.pauseOnDOMEvents(["click"], (aPacket) => {
+ is(aPacket.error, undefined,
+ "The pause-on-click-only request completed successfully.");
+
+ gClient.addListener("paused", unexpectedListener);
+
+ // This non-capturing event listener is guaranteed to run after the page's
+ // capturing one had a chance to execute and modify window.foobar.
+ once(gInput, "keyup").then(() => {
+ is(content.wrappedJSObject.foobar, "keyupHandler",
+ "No hidden breakpoint was hit.");
+
+ gClient.removeListener("paused", unexpectedListener);
+ deferred.resolve();
+ });
+
+ triggerInputKeyup();
+ });
+
+ return deferred.promise;
+}
+
+// Test that specifying an empty event array clears all hidden breakpoints.
+function testBreakOnNone() {
+ let deferred = promise.defer();
+
+ // Test calling pauseOnDOMEvents from a running state.
+ gThreadClient.pauseOnDOMEvents([], (aPacket) => {
+ is(aPacket.error, undefined,
+ "The pause-on-none request completed successfully.");
+
+ gClient.addListener("paused", unexpectedListener);
+
+ // This non-capturing event listener is guaranteed to run after the page's
+ // capturing one had a chance to execute and modify window.foobar.
+ once(gInput, "keyup").then(() => {
+ is(content.wrappedJSObject.foobar, "keyupHandler",
+ "No hidden breakpoint was hit.");
+
+ gClient.removeListener("paused", unexpectedListener);
+ deferred.resolve();
+ });
+
+ triggerInputKeyup();
+ });
+
+ return deferred.promise;
+}
+
+// Test pause on a single event.
+function testBreakOnClick() {
+ let deferred = promise.defer();
+
+ // Test calling pauseOnDOMEvents from a running state.
+ gThreadClient.pauseOnDOMEvents(["click"], (aPacket) => {
+ is(aPacket.error, undefined,
+ "The pause-on-click request completed successfully.");
+
+ gClient.addOneTimeListener("paused", (aEvent, aPacket) => {
+ is(aPacket.why.type, "pauseOnDOMEvents",
+ "A hidden breakpoint was hit.");
+ is(aPacket.frame.callee.name, "clickHandler",
+ "The clickHandler is entered.");
+
+ gThreadClient.resume(deferred.resolve);
+ });
+
+ triggerButtonClick();
+ });
+
+ return deferred.promise;
+}
+
+function unexpectedListener() {
+ gClient.removeListener("paused", unexpectedListener);
+ ok(false, "An unexpected hidden breakpoint was hit.");
+ gThreadClient.resume(testBreakOnClick);
+}
+
+function triggerInputKeyup() {
+ // Make sure that the focus is not on the input box so that a focus event
+ // will be triggered.
+ window.focus();
+ gBrowser.selectedBrowser.focus();
+ gButton.focus();
+
+ // Focus the element and wait for focus event.
+ once(gInput, "focus").then(() => {
+ executeSoon(() => {
+ EventUtils.synthesizeKey("e", { shiftKey: 1 }, content);
+ });
+ });
+
+ gInput.focus();
+}
+
+function triggerButtonClick() {
+ EventUtils.sendMouseEvent({ type: "click" }, gButton);
+}
+
+function triggerInputChange() {
+ gInput.focus();
+ gInput.value = "foo";
+ gInput.blur();
+}
+
+registerCleanupFunction(function () {
+ gClient = null;
+ gThreadClient = null;
+ gInput = null;
+ gButton = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_break-on-dom-event-02.js b/devtools/client/debugger/test/mochitest/browser_dbg_break-on-dom-event-02.js
new file mode 100644
index 000000000..d6d502343
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_break-on-dom-event-02.js
@@ -0,0 +1,105 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests that the break-on-dom-events request works even for bound event
+ * listeners and handler objects with 'handleEvent' methods.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_event-listeners-03.html";
+
+var gClient, gThreadClient;
+
+function test() {
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+
+ let transport = DebuggerServer.connectPipe();
+ gClient = new DebuggerClient(transport);
+ gClient.connect().then(([aType, aTraits]) => {
+ is(aType, "browser",
+ "Root actor should identify itself as a browser.");
+
+ addTab(TAB_URL)
+ .then(() => attachThreadActorForUrl(gClient, TAB_URL))
+ .then(aThreadClient => gThreadClient = aThreadClient)
+ .then(pauseDebuggee)
+ .then(testBreakOnClick)
+ .then(() => gClient.close())
+ .then(finish)
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+ });
+}
+
+function pauseDebuggee() {
+ let deferred = promise.defer();
+
+ gClient.addOneTimeListener("paused", (aEvent, aPacket) => {
+ is(aPacket.type, "paused",
+ "We should now be paused.");
+ is(aPacket.why.type, "debuggerStatement",
+ "The debugger statement was hit.");
+
+ gThreadClient.resume(deferred.resolve);
+ });
+
+ // Spin the event loop before causing the debuggee to pause, to allow
+ // this function to return first.
+ executeSoon(() => triggerButtonClick("initialSetup"));
+
+ return deferred.promise;
+}
+
+// Test pause on a single event.
+function testBreakOnClick() {
+ let deferred = promise.defer();
+
+ // Test calling pauseOnDOMEvents from a running state.
+ gThreadClient.pauseOnDOMEvents(["click"], (aPacket) => {
+ is(aPacket.error, undefined,
+ "The pause-on-click request completed successfully.");
+ let handlers = ["clicker"];
+
+ gClient.addListener("paused", function tester(aEvent, aPacket) {
+ is(aPacket.why.type, "pauseOnDOMEvents",
+ "A hidden breakpoint was hit.");
+
+ switch (handlers.length) {
+ case 1:
+ is(aPacket.frame.where.line, 26, "Found the clicker handler.");
+ handlers.push("handleEventClick");
+ break;
+ case 2:
+ is(aPacket.frame.where.line, 36, "Found the handleEventClick handler.");
+ handlers.push("boundHandleEventClick");
+ break;
+ case 3:
+ is(aPacket.frame.where.line, 46, "Found the boundHandleEventClick handler.");
+ gClient.removeListener("paused", tester);
+ deferred.resolve();
+ }
+
+ gThreadClient.resume(() => triggerButtonClick(handlers.slice(-1)));
+ });
+
+ triggerButtonClick(handlers.slice(-1));
+ });
+
+ return deferred.promise;
+}
+
+function triggerButtonClick(aNodeId) {
+ let button = content.document.getElementById(aNodeId);
+ EventUtils.sendMouseEvent({ type: "click" }, button);
+}
+
+registerCleanupFunction(function () {
+ gClient = null;
+ gThreadClient = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_break-on-dom-event-03.js b/devtools/client/debugger/test/mochitest/browser_dbg_break-on-dom-event-03.js
new file mode 100644
index 000000000..3ffe830e9
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_break-on-dom-event-03.js
@@ -0,0 +1,97 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests that the break-on-dom-events request works for load event listeners.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_event-listeners-04.html";
+
+var gClient, gThreadClient;
+
+function test() {
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+
+ let transport = DebuggerServer.connectPipe();
+ gClient = new DebuggerClient(transport);
+ gClient.connect().then(([aType, aTraits]) => {
+ is(aType, "browser",
+ "Root actor should identify itself as a browser.");
+
+ addTab(TAB_URL)
+ .then(() => attachThreadActorForUrl(gClient, TAB_URL))
+ .then(aThreadClient => gThreadClient = aThreadClient)
+ .then(pauseDebuggee)
+ .then(testBreakOnLoad)
+ .then(() => gClient.close())
+ .then(finish)
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+ });
+}
+
+function pauseDebuggee() {
+ let deferred = promise.defer();
+
+ gClient.addOneTimeListener("paused", (aEvent, aPacket) => {
+ is(aPacket.type, "paused",
+ "We should now be paused.");
+ is(aPacket.why.type, "debuggerStatement",
+ "The debugger statement was hit.");
+
+ gThreadClient.resume(deferred.resolve);
+ });
+
+ // Spin the event loop before causing the debuggee to pause, to allow
+ // this function to return first.
+ executeSoon(() => triggerButtonClick());
+
+ return deferred.promise;
+}
+
+// Test pause on a load event.
+function testBreakOnLoad() {
+ let deferred = promise.defer();
+
+ // Test calling pauseOnDOMEvents from a running state.
+ gThreadClient.pauseOnDOMEvents(["load"], (aPacket) => {
+ is(aPacket.error, undefined,
+ "The pause-on-load request completed successfully.");
+ let handlers = ["loadHandler"];
+
+ gClient.addListener("paused", function tester(aEvent, aPacket) {
+ is(aPacket.why.type, "pauseOnDOMEvents",
+ "A hidden breakpoint was hit.");
+
+ is(aPacket.frame.where.line, 15, "Found the load event listener.");
+ gClient.removeListener("paused", tester);
+ deferred.resolve();
+
+ gThreadClient.resume(() => triggerButtonClick(handlers.slice(-1)));
+ });
+
+ getTabActorForUrl(gClient, TAB_URL).then(aGrip => {
+ gClient.attachTab(aGrip.actor, (aResponse, aTabClient) => {
+ aTabClient.reload();
+ });
+ });
+ });
+
+ return deferred.promise;
+}
+
+function triggerButtonClick() {
+ let button = content.document.querySelector("button");
+ EventUtils.sendMouseEvent({ type: "click" }, button);
+}
+
+registerCleanupFunction(function () {
+ gClient = null;
+ gThreadClient = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_break-on-next-console.js b/devtools/client/debugger/test/mochitest/browser_dbg_break-on-next-console.js
new file mode 100644
index 000000000..c30694520
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_break-on-next-console.js
@@ -0,0 +1,61 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Test if 'break on next' functionality works from executions
+ * in content triggered by the console in the toolbox.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_script-eval.html";
+
+function test() {
+ let gTab, gPanel, gDebugger;
+ let gSources, gBreakpoints, gTarget, gResumeButton, gResumeKey, gThreadClient;
+
+ let options = {
+ source: EXAMPLE_URL + "code_script-eval.js",
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gSources = gDebugger.DebuggerView.Sources;
+ gTarget = gDebugger.gTarget;
+ gThreadClient = gDebugger.gThreadClient;
+ gResumeButton = gDebugger.document.getElementById("resume");
+ gResumeKey = gDebugger.document.getElementById("resumeKey");
+
+ testConsole()
+ .then(() => closeDebuggerAndFinish(gPanel));
+ });
+
+ let testConsole = Task.async(function* () {
+ info("Starting testConsole");
+
+ let oncePaused = gTarget.once("thread-paused");
+ EventUtils.sendMouseEvent({ type: "mousedown" }, gResumeButton, gDebugger);
+ let jsterm = yield getSplitConsole(gDevTools.getToolbox(gPanel.target));
+ let executed = jsterm.execute("1+1");
+ yield oncePaused;
+
+ let updatedFrame = yield waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES);
+ let variables = gDebugger.DebuggerView.Variables;
+
+ is(variables._store.length, 3, "Correct number of scopes available");
+ is(variables.getScopeAtIndex(0).name, "With scope [Object]",
+ "Paused with correct scope (0)");
+ is(variables.getScopeAtIndex(1).name, "Block scope",
+ "Paused with correct scope (1)");
+ is(variables.getScopeAtIndex(2).name, "Global scope [Window]",
+ "Paused with correct scope (2)");
+
+ let onceResumed = gTarget.once("thread-resumed");
+ EventUtils.sendMouseEvent({ type: "mousedown" }, gResumeButton, gDebugger);
+ yield onceResumed;
+
+ yield executed;
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_break-on-next.js b/devtools/client/debugger/test/mochitest/browser_dbg_break-on-next.js
new file mode 100644
index 000000000..53f03c183
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_break-on-next.js
@@ -0,0 +1,103 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Test if 'break on next' functionality works from executions
+ * in content that are triggered by the page.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_script-eval.html";
+
+function test() {
+ let gTab, gPanel, gDebugger;
+ let gSources, gBreakpoints, gTarget, gResumeButton, gResumeKey, gThreadClient;
+
+ const options = {
+ source: EXAMPLE_URL + "code_script-eval.js",
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gSources = gDebugger.DebuggerView.Sources;
+ gTarget = gDebugger.gTarget;
+ gThreadClient = gDebugger.gThreadClient;
+ gResumeButton = gDebugger.document.getElementById("resume");
+ gResumeKey = gDebugger.document.getElementById("resumeKey");
+
+ testInterval()
+ .then(testEvent)
+ .then(() => closeDebuggerAndFinish(gPanel));
+ });
+
+ // Testing an interval instead of a timeout / rAF because
+ // it's less likely to fail due to timing issues. If the
+ // first callback happens to fire before the break request
+ // happens then we'll just get it next time.
+ let testInterval = Task.async(function* () {
+ info("Starting testInterval");
+
+ yield evalInTab(gTab, `
+ var interval = setInterval(function() {
+ return 1+1;
+ }, 100);
+ `);
+
+ let oncePaused = gTarget.once("thread-paused");
+ EventUtils.sendMouseEvent({ type: "mousedown" }, gResumeButton, gDebugger);
+ yield oncePaused;
+
+ yield waitForDebuggerEvents(gPanel, gDebugger.EVENTS.SOURCE_SHOWN);
+ let variables = gDebugger.DebuggerView.Variables;
+
+ is(variables._store.length, 3, "Correct number of scopes available");
+ is(variables.getScopeAtIndex(0).name, "Function scope [interval<]",
+ "Paused with correct scope (0)");
+ is(variables.getScopeAtIndex(1).name, "Block scope",
+ "Paused with correct scope (1)");
+ is(variables.getScopeAtIndex(2).name, "Global scope [Window]",
+ "Paused with correct scope (2)");
+
+ yield evalInTab(gTab, "clearInterval(interval)");
+ let onceResumed = gTarget.once("thread-resumed");
+ EventUtils.sendMouseEvent({ type: "mousedown" }, gResumeButton, gDebugger);
+ yield onceResumed;
+ });
+
+ let testEvent = Task.async(function* () {
+ info("Starting testEvent");
+
+ let oncePaused = gTarget.once("thread-paused");
+ EventUtils.sendMouseEvent({ type: "mousedown" }, gResumeButton, gDebugger);
+ once(gDebugger.gClient, "willInterrupt").then(() => {
+ generateMouseClickInTab(gTab, "content.document.querySelector('button')");
+ });
+ yield oncePaused;
+
+ yield waitForDebuggerEvents(gPanel, gDebugger.EVENTS.SOURCE_SHOWN);
+ let variables = gDebugger.DebuggerView.Variables;
+
+ is(variables._store.length, 6, "Correct number of scopes available");
+ is(variables.getScopeAtIndex(0).name, "Function scope [onclick]",
+ "Paused with correct scope (0)");
+ // Non-syntactic lexical scope introduced by non-syntactic scope chain.
+ is(variables.getScopeAtIndex(1).name, "Block scope",
+ "Paused with correct scope (1)");
+ is(variables.getScopeAtIndex(2).name, "With scope [HTMLButtonElement]",
+ "Paused with correct scope (2)");
+ is(variables.getScopeAtIndex(3).name, "With scope [HTMLDocument]",
+ "Paused with correct scope (3)");
+ // Global lexical scope.
+ is(variables.getScopeAtIndex(4).name, "Block scope",
+ "Paused with correct scope (4)");
+ is(variables.getScopeAtIndex(5).name, "Global scope [Window]",
+ "Paused with correct scope (5)");
+
+ let onceResumed = gTarget.once("thread-resumed");
+ EventUtils.sendMouseEvent({ type: "mousedown" }, gResumeButton, gDebugger);
+ yield onceResumed;
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_break-unselected.js b/devtools/client/debugger/test/mochitest/browser_dbg_break-unselected.js
new file mode 100644
index 000000000..b76a7606a
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_break-unselected.js
@@ -0,0 +1,48 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Test breaking in code and jumping to the debugger before
+ * the debugger UI has been initialized.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_inline-debugger-statement.html";
+
+function test() {
+ Task.spawn(function* () {
+ const tab = yield getTab(TAB_URL);
+ const target = TargetFactory.forTab(tab);
+ const toolbox = yield gDevTools.showToolbox(target, "webconsole");
+
+ is(toolbox.currentToolId, "webconsole", "Console is the current panel");
+
+ toolbox.target.on("thread-paused", Task.async(function* () {
+ // Wait for the toolbox to handle the event and switch tools
+ yield waitForTick();
+
+ is(toolbox.currentToolId, "jsdebugger", "Debugger is the current panel");
+
+ // Wait until it's actually fully loaded
+ yield toolbox.loadTool("jsdebugger");
+
+ const panel = toolbox.getCurrentPanel();
+ const queries = panel.panelWin.require("./content/queries");
+ const getState = panel.panelWin.DebuggerController.getState;
+
+ is(panel.panelWin.gThreadClient.state, "paused",
+ "Thread is still paused");
+
+ yield waitForSourceAndCaret(panel, "debugger-statement.html", 16);
+ is(queries.getSelectedSource(getState()).url, TAB_URL,
+ "Selected source is the current tab url");
+ is(queries.getSelectedSourceOpts(getState()).line, 16,
+ "Line 16 is highlighted in the editor");
+
+ resumeDebuggerThenCloseAndFinish(panel);
+ }));
+
+ callInTab(tab, "runDebuggerStatement");
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-actual-location.js b/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-actual-location.js
new file mode 100644
index 000000000..0f880b9cc
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-actual-location.js
@@ -0,0 +1,55 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Bug 737803: Setting a breakpoint in a line without code should move
+ * the icon to the actual location.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
+
+function test() {
+ let options = {
+ source: EXAMPLE_URL + "code_script-switching-01.js",
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ const gTab = aTab;
+ const gPanel = aPanel;
+ const gDebugger = gPanel.panelWin;
+ const gEditor = gDebugger.DebuggerView.editor;
+ const gSources = gDebugger.DebuggerView.Sources;
+ const gController = gDebugger.DebuggerController;
+ const constants = gDebugger.require("./content/constants");
+ const queries = gDebugger.require("./content/queries");
+ const actions = bindActionCreators(gPanel);
+
+ Task.spawn(function* () {
+ yield waitForSourceAndCaretAndScopes(gPanel, "-02.js", 1);
+
+ is(queries.getBreakpoints(gController.getState()).length, 0,
+ "There are no breakpoints in the editor");
+
+ const response = yield actions.addBreakpoint({
+ actor: gSources.selectedValue, line: 4
+ });
+
+ ok(response.actualLocation, "has an actualLocation");
+ is(response.actualLocation.line, 6, "moved to line 6");
+
+ is(queries.getBreakpoints(gController.getState()).length, 1,
+ "There is only one breakpoint in the editor");
+
+ ok(!queries.getBreakpoint(gController.getState(), { actor: gSources.selectedValue, line: 4 }),
+ "There isn't any breakpoint added on an invalid line.");
+ ok(queries.getBreakpoint(gController.getState(), { actor: gSources.selectedValue, line: 6 }),
+ "There isn't any breakpoint added on an invalid line.");
+
+ resumeDebuggerThenCloseAndFinish(gPanel);
+ });
+
+ callInTab(gTab, "firstCall");
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-actual-location2.js b/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-actual-location2.js
new file mode 100644
index 000000000..16082d2cc
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-actual-location2.js
@@ -0,0 +1,88 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Bug 1008372: Setting a breakpoint in a line without code should move
+ * the icon to the actual location, and if a breakpoint already exists
+ * on the new location don't duplicate
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_breakpoint-move.html";
+
+function test() {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ const gTab = aTab;
+ const gPanel = aPanel;
+ const gDebugger = gPanel.panelWin;
+ const gEditor = gDebugger.DebuggerView.editor;
+ const gSources = gDebugger.DebuggerView.Sources;
+ const gController = gDebugger.DebuggerController;
+ const actions = bindActionCreators(gPanel);
+ const constants = gDebugger.require("./content/constants");
+ const queries = gDebugger.require("./content/queries");
+
+ function resumeAndTestBreakpoint(line) {
+ return Task.spawn(function* () {
+ let event = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES);
+ doResume(gPanel);
+ yield event;
+ testBreakpoint(line);
+ });
+ }
+
+ function testBreakpoint(line) {
+ let bp = gSources._selectedBreakpoint;
+ ok(bp, "There should be a selected breakpoint on line " + line);
+ is(bp.location.line, line,
+ "The breakpoint on line " + line + " was not hit");
+ }
+
+ Task.spawn(function* () {
+ let onCaretUpdated = waitForCaretAndScopes(gPanel, 16);
+ callInTab(gTab, "ermahgerd");
+ yield onCaretUpdated;
+
+ is(queries.getBreakpoints(gController.getState()).length, 0,
+ "There are no breakpoints in the editor");
+
+ yield actions.addBreakpoint({
+ actor: gSources.selectedValue,
+ line: 19
+ });
+ yield actions.addBreakpoint({
+ actor: gSources.selectedValue,
+ line: 20
+ });
+
+ const response = yield actions.addBreakpoint({
+ actor: gSources.selectedValue,
+ line: 17
+ });
+
+ is(response.actualLocation.line, 19,
+ "Breakpoint client line is new.");
+
+ yield resumeAndTestBreakpoint(19);
+
+ yield actions.removeBreakpoint({
+ actor: gSources.selectedValue,
+ line: 19
+ });
+
+ yield resumeAndTestBreakpoint(20);
+ yield doResume(gPanel);
+
+ callInTab(gTab, "ermahgerd");
+ yield waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES);
+
+ yield resumeAndTestBreakpoint(20);
+ resumeDebuggerThenCloseAndFinish(gPanel);
+ });
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-break-on-last-line-of-script-on-reload.js b/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-break-on-last-line-of-script-on-reload.js
new file mode 100644
index 000000000..124f8b1c2
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-break-on-last-line-of-script-on-reload.js
@@ -0,0 +1,116 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Bug 978019: Setting a breakpoint on the last line of a Debugger.Script and
+ * reloading should still hit the breakpoint.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_breakpoints-break-on-last-line-of-script-on-reload.html";
+const CODE_URL = EXAMPLE_URL + "code_breakpoints-break-on-last-line-of-script-on-reload.js";
+
+function test() {
+ // Debug test slaves are a bit slow at this test.
+ requestLongerTimeout(2);
+
+ let gPanel, gDebugger, gThreadClient, gEvents, gSources;
+
+ const options = {
+ source: CODE_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gThreadClient = gDebugger.gThreadClient;
+ gEvents = gDebugger.EVENTS;
+ gSources = gDebugger.DebuggerView.Sources;
+ const actions = bindActionCreators(gPanel);
+
+ Task.spawn(function* () {
+ try {
+
+ // Refresh and hit the debugger statement before the location we want to
+ // set our breakpoints. We have to pause before the breakpoint locations
+ // so that GC doesn't get a chance to kick in and collect the IIFE's
+ // script, which would causes us to receive a 'noScript' error from the
+ // server when we try to set the breakpoints.
+ const [paused, ] = yield promise.all([
+ waitForThreadEvents(gPanel, "paused"),
+ reloadActiveTab(gPanel, gEvents.SOURCE_SHOWN),
+ ]);
+
+ is(paused.why.type, "debuggerStatement");
+
+ // Set our breakpoints.
+ const sourceActor = getSourceActor(gSources, CODE_URL);
+ yield promise.all([
+ actions.addBreakpoint({
+ actor: sourceActor,
+ line: 3
+ }),
+ actions.addBreakpoint({
+ actor: sourceActor,
+ line: 4
+ }),
+ actions.addBreakpoint({
+ actor: sourceActor,
+ line: 5
+ })
+ ]);
+
+ // Refresh and hit the debugger statement again.
+ yield promise.all([
+ reloadActiveTab(gPanel, gEvents.SOURCE_SHOWN),
+ waitForCaretAndScopes(gPanel, 1)
+ ]);
+
+ // And we should hit the breakpoints as we resume.
+ yield promise.all([
+ doResume(gPanel),
+ waitForCaretAndScopes(gPanel, 3)
+ ]);
+ yield promise.all([
+ doResume(gPanel),
+ waitForCaretAndScopes(gPanel, 4)
+ ]);
+ yield promise.all([
+ doResume(gPanel),
+ waitForCaretAndScopes(gPanel, 5)
+ ]);
+
+ // Clean up the breakpoints.
+ yield promise.all([
+ actions.removeBreakpoint({ actor: sourceActor, line: 3 }),
+ actions.removeBreakpoint({ actor: sourceActor, line: 4 }),
+ actions.removeBreakpoint({ actor: sourceActor, line: 5 })
+ ]);
+
+ yield resumeDebuggerThenCloseAndFinish(gPanel);
+
+ } catch (e) {
+ DevToolsUtils.reportException(
+ "browser_dbg_breakpoints-break-on-last-line-of-script-on-reload.js",
+ e
+ );
+ ok(false);
+ }
+ });
+ });
+
+ function setBreakpoint(location) {
+ let item = gSources.getItemByValue(getSourceActor(gSources, location.url));
+ let source = gThreadClient.source(item.attachment.source);
+
+ let deferred = promise.defer();
+ source.setBreakpoint(location, ({ error, message }, bpClient) => {
+ if (error) {
+ deferred.reject(error + ": " + message);
+ }
+ deferred.resolve(bpClient);
+ });
+ return deferred.promise;
+ }
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-button-01.js b/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-button-01.js
new file mode 100644
index 000000000..25bf1fe06
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-button-01.js
@@ -0,0 +1,55 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Test if the breakpoints toggle button works as advertised.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
+
+function test() {
+ let options = {
+ source: EXAMPLE_URL + "code_script-switching-01.js",
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ const gTab = aTab;
+ const gPanel = aPanel;
+ const gDebugger = gPanel.panelWin;
+ const gSources = gDebugger.DebuggerView.Sources;
+ const actions = bindActionCreators(gPanel);
+ const getState = gDebugger.DebuggerController.getState;
+
+ function checkBreakpointsDisabled(isDisabled, total = 3) {
+ let breakpoints = gDebugger.queries.getBreakpoints(getState());
+
+ is(breakpoints.length, total,
+ "Breakpoints should still be set.");
+ is(breakpoints.filter(bp => bp.disabled === isDisabled).length, total,
+ "Breakpoints should be " + (isDisabled ? "disabled" : "enabled") + ".");
+ }
+
+ Task.spawn(function* () {
+ yield actions.addBreakpoint({ actor: gSources.values[0], line: 5 });
+ yield actions.addBreakpoint({ actor: gSources.values[1], line: 6 });
+ yield actions.addBreakpoint({ actor: gSources.values[1], line: 7 });
+ yield ensureThreadClientState(gPanel, "resumed");
+
+ gSources.toggleBreakpoints();
+ yield waitForDispatch(gPanel, gDebugger.constants.REMOVE_BREAKPOINT, 3);
+ checkBreakpointsDisabled(true);
+
+ const finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.BREAKPOINT_ADDED, 3);
+ gSources.toggleBreakpoints();
+ yield waitForDispatch(gPanel, gDebugger.constants.ADD_BREAKPOINT, 3);
+ checkBreakpointsDisabled(false);
+
+ if (gDebugger.gThreadClient.state !== "attached") {
+ yield waitForThreadEvents(gPanel, "resumed");
+ }
+ closeDebuggerAndFinish(gPanel);
+ });
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-button-02.js b/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-button-02.js
new file mode 100644
index 000000000..6a763bf7e
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-button-02.js
@@ -0,0 +1,64 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Test if the breakpoints toggle button works as advertised when there are
+ * some breakpoints already disabled.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
+
+function test() {
+ let options = {
+ source: EXAMPLE_URL + "code_script-switching-01.js",
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ const gTab = aTab;
+ const gPanel = aPanel;
+ const gDebugger = gPanel.panelWin;
+ const gSources = gDebugger.DebuggerView.Sources;
+ const actions = bindActionCreators(gPanel);
+ const getState = gDebugger.DebuggerController.getState;
+
+ function checkBreakpointsDisabled(isDisabled, total = 3) {
+ let breakpoints = gDebugger.queries.getBreakpoints(getState());
+
+ is(breakpoints.length, total,
+ "Breakpoints should still be set.");
+ is(breakpoints.filter(bp => bp.disabled === isDisabled).length, total,
+ "Breakpoints should be " + (isDisabled ? "disabled" : "enabled") + ".");
+ }
+
+ Task.spawn(function* () {
+ yield promise.all([
+ actions.addBreakpoint({ actor: gSources.values[0], line: 5 }),
+ actions.addBreakpoint({ actor: gSources.values[1], line: 6 }),
+ actions.addBreakpoint({ actor: gSources.values[1], line: 7 })
+ ]);
+ if (gDebugger.gThreadClient.state !== "attached") {
+ yield waitForThreadEvents(gPanel, "resumed");
+ }
+
+ yield promise.all([
+ actions.disableBreakpoint({ actor: gSources.values[0], line: 5 }),
+ actions.disableBreakpoint({ actor: gSources.values[1], line: 6 })
+ ]);
+
+ gSources.toggleBreakpoints();
+ yield waitForDispatch(gPanel, gDebugger.constants.REMOVE_BREAKPOINT, 1);
+ checkBreakpointsDisabled(true);
+
+ gSources.toggleBreakpoints();
+ yield waitForDispatch(gPanel, gDebugger.constants.ADD_BREAKPOINT, 3);
+ checkBreakpointsDisabled(false);
+
+ if (gDebugger.gThreadClient.state !== "attached") {
+ yield waitForThreadEvents(gPanel, "resumed");
+ }
+ closeDebuggerAndFinish(gPanel);
+ });
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-condition-thrown-message.js b/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-condition-thrown-message.js
new file mode 100644
index 000000000..cefd429d2
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-condition-thrown-message.js
@@ -0,0 +1,107 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure that the message which breakpoint condition throws
+ * could be displayed on UI correctly
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_conditional-breakpoints.html";
+
+function test() {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ const gTab = aTab;
+ const gPanel = aPanel;
+ const gDebugger = gPanel.panelWin;
+ const gEditor = gDebugger.DebuggerView.editor;
+ const gSources = gDebugger.DebuggerView.Sources;
+ const actions = bindActionCreators(gPanel);
+ const getState = gDebugger.DebuggerController.getState;
+
+ function initialCheck(aCaretLine) {
+ let bp = gDebugger.queries.getBreakpoint(getState(),
+ { actor: gSources.values[0], line: aCaretLine });
+ ok(bp, "There should be a breakpoint on line " + aCaretLine);
+
+ let attachment = gSources._getBreakpoint(bp).attachment;
+ ok(attachment,
+ "There should be a breakpoint on line " + aCaretLine + " in the sources pane.");
+
+ let thrownNode = attachment.view.container.querySelector(".dbg-breakpoint-condition-thrown-message");
+ ok(thrownNode,
+ "The breakpoint item should contain a thrown message node.");
+
+ ok(!attachment.view.container.classList.contains("dbg-breakpoint-condition-thrown"),
+ "The thrown message on line " + aCaretLine + " should be hidden when condition has not been evaluated.");
+ }
+
+ function resumeAndTestThrownMessage(line) {
+ doResume(gPanel);
+
+ return waitForCaretUpdated(gPanel, line).then(() => {
+ // Test that the thrown message is correctly shown.
+ let bp = gDebugger.queries.getBreakpoint(
+ getState(),
+ { actor: gSources.values[0], line: line }
+ );
+ let attachment = gSources._getBreakpoint(bp).attachment;
+ ok(attachment.view.container.classList.contains("dbg-breakpoint-condition-thrown"),
+ "Message on line " + line + " should be shown when condition throws.");
+ });
+ }
+
+ function resumeAndTestNoThrownMessage(line) {
+ doResume(gPanel);
+
+ return waitForCaretUpdated(gPanel, line).then(() => {
+ // test that the thrown message is correctly shown
+ let bp = gDebugger.queries.getBreakpoint(
+ getState(),
+ { actor: gSources.values[0], line: line }
+ );
+ let attachment = gSources._getBreakpoint(bp).attachment;
+ ok(!attachment.view.container.classList.contains("dbg-breakpoint-condition-thrown"),
+ "Message on line " + line + " should be hidden if condition doesn't throw.");
+ });
+ }
+
+ Task.spawn(function* () {
+ let onCaretUpdated = waitForCaretAndScopes(gPanel, 17);
+ callInTab(gTab, "ermahgerd");
+ yield onCaretUpdated;
+
+ yield actions.addBreakpoint({ actor: gSources.selectedValue, line: 18 }, " 1afff");
+ // Close the popup because a SET_BREAKPOINT_CONDITION action is
+ // fired when it's closed, and it sets it on the currently
+ // selected breakpoint and we want to make sure it uses the
+ // current breakpoint. This isn't a problem outside of tests
+ // because any UI interaction will close the popup before the
+ // new breakpoint is added.
+ gSources._hideConditionalPopup();
+ initialCheck(18);
+
+ yield actions.addBreakpoint({ actor: gSources.selectedValue, line: 19 }, "true");
+ gSources._hideConditionalPopup();
+ initialCheck(19);
+
+ yield actions.addBreakpoint({ actor: gSources.selectedValue, line: 20 }, "false");
+ gSources._hideConditionalPopup();
+ initialCheck(20);
+
+ yield actions.addBreakpoint({ actor: gSources.selectedValue, line: 22 }, "randomVar");
+ gSources._hideConditionalPopup();
+ initialCheck(22);
+
+ yield resumeAndTestThrownMessage(18);
+ yield resumeAndTestNoThrownMessage(19);
+ yield resumeAndTestThrownMessage(22);
+ resumeDebuggerThenCloseAndFinish(gPanel);
+ });
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-contextmenu-add.js b/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-contextmenu-add.js
new file mode 100644
index 000000000..2b50d53aa
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-contextmenu-add.js
@@ -0,0 +1,84 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Test adding breakpoints from the source editor context menu
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
+
+function test() {
+ let options = {
+ source: EXAMPLE_URL + "code_script-switching-01.js",
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ const gTab = aTab;
+ const gPanel = aPanel;
+ const gDebugger = gPanel.panelWin;
+ const gEditor = gDebugger.DebuggerView.editor;
+ const gSources = gDebugger.DebuggerView.Sources;
+ const gContextMenu = gDebugger.document.getElementById("sourceEditorContextMenu");
+ const queries = gDebugger.require("./content/queries");
+ const actions = bindActionCreators(gPanel);
+ const getState = gDebugger.DebuggerController.getState;
+
+ Task.spawn(function* () {
+ yield waitForSourceAndCaretAndScopes(gPanel, "-02.js", 1);
+
+ is(gDebugger.gThreadClient.state, "paused",
+ "Should only be getting stack frames while paused.");
+ is(queries.getSourceCount(getState()), 2,
+ "Found the expected number of sources.");
+ isnot(gEditor.getText().indexOf("debugger"), -1,
+ "The correct source was loaded initially.");
+ is(gSources.selectedValue, gSources.values[1],
+ "The correct source is selected.");
+
+ ok(gContextMenu,
+ "The source editor's context menupopup is available.");
+
+ gEditor.focus();
+ gEditor.setSelection({ line: 1, ch: 0 }, { line: 1, ch: 10 });
+
+ gContextMenu.openPopup(gEditor.container, "overlap", 0, 0, true, false);
+ gEditor.emit("gutterClick", 6, 2);
+
+ yield once(gContextMenu, "popupshown");
+ is(queries.getBreakpoints(getState()).length, 0, "no breakpoints added");
+
+ let cmd = gContextMenu.querySelector("menuitem[command=addBreakpointCommand]");
+ EventUtils.synthesizeMouseAtCenter(cmd, {}, gDebugger);
+ yield waitForDispatch(gPanel, gDebugger.constants.ADD_BREAKPOINT);
+
+ is(queries.getBreakpoints(getState()).length, 1,
+ "1 breakpoint correctly added");
+ ok(queries.getBreakpoint(getState(),
+ { actor: gSources.values[1], line: 7 }),
+ "Breakpoint on line 7 exists");
+
+ gContextMenu.openPopup(gEditor.container, "overlap", 0, 0, true, false);
+ gEditor.emit("gutterClick", 7, 2);
+
+ yield once(gContextMenu, "popupshown");
+ is(queries.getBreakpoints(getState()).length, 1,
+ "1 breakpoint correctly added");
+
+ cmd = gContextMenu.querySelector("menuitem[command=addConditionalBreakpointCommand]");
+ EventUtils.synthesizeMouseAtCenter(cmd, {}, gDebugger);
+ yield waitForDispatch(gPanel, gDebugger.constants.ADD_BREAKPOINT);
+
+ is(queries.getBreakpoints(getState()).length, 2,
+ "2 breakpoints correctly added");
+ ok(queries.getBreakpoint(getState(),
+ { actor: gSources.values[1], line: 8 }),
+ "Breakpoint on line 8 exists");
+
+ resumeDebuggerThenCloseAndFinish(gPanel);
+ });
+
+ callInTab(gTab, "firstCall");
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-contextmenu.js b/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-contextmenu.js
new file mode 100644
index 000000000..913d32073
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-contextmenu.js
@@ -0,0 +1,252 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Test if the context menu associated with each breakpoint does what it should.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
+
+function test() {
+ // Debug test slaves are a bit slow at this test.
+ requestLongerTimeout(2);
+
+ Task.spawn(function* () {
+ const options = {
+ source: EXAMPLE_URL + "code_script-switching-01.js",
+ line: 1
+ };
+ const [gTab,, gPanel ] = yield initDebugger(TAB_URL, options);
+ const gDebugger = gPanel.panelWin;
+ const gSources = gDebugger.DebuggerView.Sources;
+ const queries = gDebugger.require("./content/queries");
+ const actions = bindActionCreators(gPanel);
+ const getState = gDebugger.DebuggerController.getState;
+
+ const addBreakpoints = Task.async(function* () {
+ yield actions.addBreakpoint({ actor: gSources.values[0], line: 5 });
+ yield actions.addBreakpoint({ actor: gSources.values[1], line: 6 });
+ yield actions.addBreakpoint({ actor: gSources.values[1], line: 7 });
+ yield actions.addBreakpoint({ actor: gSources.values[1], line: 8 });
+ yield actions.addBreakpoint({ actor: gSources.values[1], line: 9 });
+ yield ensureThreadClientState(gPanel, "resumed");
+ gSources.highlightBreakpoint({ actor: gSources.values[1], line: 9 });
+ });
+
+ const pauseAndCheck = Task.async(function* () {
+ let source = queries.getSelectedSource(getState());
+ is(source.url, EXAMPLE_URL + "code_script-switching-02.js",
+ "The currently selected source is incorrect (1).");
+ is(gSources.selectedIndex, 1,
+ "The currently selected source is incorrect (2).");
+ ok(isCaretPos(gPanel, 9),
+ "The editor location is correct before pausing.");
+
+ generateMouseClickInTab(gTab, "content.document.querySelector('button')");
+
+ return waitForSourceAndCaretAndScopes(gPanel, "-01.js", 5).then(() => {
+ let source = queries.getSelectedSource(getState());
+ is(source.url, EXAMPLE_URL + "code_script-switching-01.js",
+ "The currently selected source is incorrect (3).");
+ is(gSources.selectedIndex, 0,
+ "The currently selected source is incorrect (4).");
+ ok(isCaretPos(gPanel, 5),
+ "The editor location is correct after pausing.");
+ });
+ });
+
+ let initialChecks = Task.async(function* () {
+ for (let bp of queries.getBreakpoints(getState())) {
+ ok(bp.actor, "All breakpoint items should have an actor");
+ ok(!bp.disabled, "All breakpoints should initially be enabled.");
+
+ let prefix = "bp-cMenu-"; // "breakpoints context menu"
+ let identifier = queries.makeLocationId(bp.location);
+ let enableSelfId = prefix + "enableSelf-" + identifier + "-menuitem";
+ let disableSelfId = prefix + "disableSelf-" + identifier + "-menuitem";
+
+ // Check to make sure that only the bp context menu is shown when right clicking
+ // this node (Bug 1159276).
+ let breakpointItem = gSources._getBreakpoint(bp);
+ let menu = gDebugger.document.getElementById("bp-mPop-" + identifier);
+ let contextMenuShown = once(gDebugger.document, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(breakpointItem.prebuiltNode, {type: "contextmenu", button: 2}, gDebugger);
+ let event = yield contextMenuShown;
+ is(event.originalTarget.id, menu.id, "The correct context menu was shown");
+ let contextMenuHidden = once(gDebugger.document, "popuphidden");
+ menu.hidePopup();
+ yield contextMenuHidden;
+
+ is(gDebugger.document.getElementById(enableSelfId).getAttribute("hidden"), "true",
+ "The 'Enable breakpoint' context menu item should initially be hidden'.");
+ ok(!gDebugger.document.getElementById(disableSelfId).hasAttribute("hidden"),
+ "The 'Disable breakpoint' context menu item should initially not be hidden'.");
+
+ is(breakpointItem.attachment.view.checkbox.getAttribute("checked"), "true",
+ "All breakpoints should initially have a checked checkbox.");
+ }
+ });
+
+ const checkBreakpointToggleSelf = Task.async(function* (index) {
+ EventUtils.sendMouseEvent({ type: "click" },
+ gDebugger.document.querySelectorAll(".dbg-breakpoint")[index],
+ gDebugger);
+
+ let selectedBreakpoint = gSources._selectedBreakpoint;
+ let selectedBreakpointItem = gSources._getBreakpoint(selectedBreakpoint);
+
+ ok(selectedBreakpoint.actor,
+ "Selected breakpoint should have an actor.");
+ ok(!selectedBreakpoint.disabled,
+ "The breakpoint should not be disabled yet (" + index + ").");
+
+ let prefix = "bp-cMenu-"; // "breakpoints context menu"
+ let identifier = queries.makeLocationId(selectedBreakpoint.location);
+ let enableSelfId = prefix + "enableSelf-" + identifier + "-menuitem";
+ let disableSelfId = prefix + "disableSelf-" + identifier + "-menuitem";
+
+ is(gDebugger.document.getElementById(enableSelfId).getAttribute("hidden"), "true",
+ "The 'Enable breakpoint' context menu item should be hidden'.");
+ ok(!gDebugger.document.getElementById(disableSelfId).hasAttribute("hidden"),
+ "The 'Disable breakpoint' context menu item should not be hidden'.");
+
+ ok(isCaretPos(gPanel, selectedBreakpoint.location.line),
+ "The source editor caret position was incorrect (" + index + ").");
+
+ // Test disabling this breakpoint.
+ gSources._onDisableSelf(selectedBreakpoint.location);
+ yield waitForDispatch(gPanel, gDebugger.constants.REMOVE_BREAKPOINT);
+
+ ok(!!queries.getBreakpoint(getState(), selectedBreakpoint.location).disabled,
+ "The breakpoint should be disabled.");
+
+ ok(!gDebugger.document.getElementById(enableSelfId).hasAttribute("hidden"),
+ "The 'Enable breakpoint' context menu item should not be hidden'.");
+ is(gDebugger.document.getElementById(disableSelfId).getAttribute("hidden"), "true",
+ "The 'Disable breakpoint' context menu item should be hidden'.");
+ ok(!selectedBreakpointItem.attachment.view.checkbox.hasAttribute("checked"),
+ "The breakpoint should now be unchecked.");
+
+ gSources._onEnableSelf(selectedBreakpoint.location);
+ yield waitForDispatch(gPanel, gDebugger.constants.ADD_BREAKPOINT);
+
+ ok(!queries.getBreakpoint(getState(), selectedBreakpoint.location).disabled,
+ "The breakpoint should be enabled.");
+ is(gDebugger.document.getElementById(enableSelfId).getAttribute("hidden"), "true",
+ "The 'Enable breakpoint' context menu item should be hidden'.");
+ ok(!gDebugger.document.getElementById(disableSelfId).hasAttribute("hidden"),
+ "The 'Disable breakpoint' context menu item should not be hidden'.");
+ ok(selectedBreakpointItem.attachment.view.checkbox.hasAttribute("checked"),
+ "The breakpoint should now be checked.");
+ });
+
+ const checkBreakpointToggleOthers = Task.async(function* (index) {
+ EventUtils.sendMouseEvent(
+ { type: "click" },
+ gDebugger.document.querySelectorAll(".dbg-breakpoint")[index],
+ gDebugger
+ );
+
+ // Test disabling other breakpoints.
+ disableOthers();
+ yield waitForDispatch(gPanel, gDebugger.constants.REMOVE_BREAKPOINT, 4);
+
+ let selectedBreakpoint = queries.getBreakpoint(getState(), gSources._selectedBreakpoint.location);
+
+ ok(selectedBreakpoint.actor,
+ "There should be a breakpoint actor.");
+ ok(!selectedBreakpoint.disabled,
+ "The targetted breakpoint should not have been disabled (" + index + ").");
+
+ for (let bp of queries.getBreakpoints(getState())) {
+ if (bp !== selectedBreakpoint) {
+ ok(bp.disabled,
+ "Non-targetted breakpoints should have been disabled.");
+ }
+ }
+
+ // Test re-enabling other breakpoints.
+ enableOthers();
+ yield waitForDispatch(gPanel, gDebugger.constants.ADD_BREAKPOINT, 4);
+ for (let bp of queries.getBreakpoints(getState())) {
+ ok(!bp.disabled, "All breakpoints should be enabled.");
+ }
+
+ // Test disabling all breakpoints.
+ disableAll();
+ yield waitForDispatch(gPanel, gDebugger.constants.REMOVE_BREAKPOINT, 5);
+ for (let bp of queries.getBreakpoints(getState())) {
+ ok(!!bp.disabled, "All breakpoints should be disabled.");
+ }
+
+ // Test re-enabling all breakpoints.
+ enableAll();
+ yield waitForDispatch(gPanel, gDebugger.constants.ADD_BREAKPOINT, 5);
+ for (let bp of queries.getBreakpoints(getState())) {
+ ok(!bp.disabled, "All breakpoints should be enabled.");
+ }
+ });
+
+ const testDeleteAll = Task.async(function* () {
+ // Test deleting all breakpoints.
+ deleteAll();
+ yield waitForDispatch(gPanel, gDebugger.constants.REMOVE_BREAKPOINT, 5);
+
+ ok(!gSources._selectedBreakpoint,
+ "There should be no breakpoint available after removing all breakpoints.");
+
+ for (let bp of queries.getBreakpoints(getState())) {
+ ok(false, "It's a trap!");
+ }
+ });
+
+ function disableOthers() {
+ gSources._onDisableOthers(gSources._selectedBreakpoint.location);
+ }
+ function enableOthers() {
+ gSources._onEnableOthers(gSources._selectedBreakpoint.location);
+ }
+ function disableAll() {
+ gSources._onDisableAll();
+ }
+ function enableAll() {
+ gSources._onEnableAll();
+ }
+ function deleteAll() {
+ gSources._onDeleteAll();
+ }
+
+ yield addBreakpoints();
+ yield initialChecks();
+ yield checkBreakpointToggleSelf(0);
+ yield checkBreakpointToggleOthers(0);
+ yield checkBreakpointToggleSelf(1);
+ yield checkBreakpointToggleOthers(1);
+ yield checkBreakpointToggleSelf(2);
+ yield checkBreakpointToggleOthers(2);
+ yield checkBreakpointToggleSelf(3);
+ yield checkBreakpointToggleOthers(3);
+ yield checkBreakpointToggleSelf(4);
+ yield checkBreakpointToggleOthers(4);
+ yield testDeleteAll();
+
+ yield addBreakpoints();
+ yield initialChecks();
+ yield pauseAndCheck();
+ yield checkBreakpointToggleSelf(0);
+ yield checkBreakpointToggleOthers(0);
+ yield checkBreakpointToggleSelf(1);
+ yield checkBreakpointToggleOthers(1);
+ yield checkBreakpointToggleSelf(2);
+ yield checkBreakpointToggleOthers(2);
+ yield checkBreakpointToggleSelf(3);
+ yield checkBreakpointToggleOthers(3);
+ yield checkBreakpointToggleSelf(4);
+ yield checkBreakpointToggleOthers(4);
+ yield testDeleteAll();
+
+ resumeDebuggerThenCloseAndFinish(gPanel);
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-disabled-reload.js b/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-disabled-reload.js
new file mode 100644
index 000000000..1caf4994f
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-disabled-reload.js
@@ -0,0 +1,124 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Test that disabled breakpoints survive target navigation.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
+
+function test() {
+ let options = {
+ source: EXAMPLE_URL + "code_script-switching-01.js",
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ const gPanel = aPanel;
+ const gTab = aTab;
+ const gDebugger = gPanel.panelWin;
+ const gEvents = gDebugger.EVENTS;
+ const gEditor = gDebugger.DebuggerView.editor;
+ const gSources = gDebugger.DebuggerView.Sources;
+ const queries = gDebugger.require("./content/queries");
+ const actions = bindActionCreators(gPanel);
+ const getState = gDebugger.DebuggerController.getState;
+ let gBreakpointLocation;
+
+ Task.spawn(function* () {
+ gBreakpointLocation = { actor: getSourceActor(gSources, EXAMPLE_URL + "code_script-switching-01.js"),
+ line: 5 };
+
+ yield actions.addBreakpoint(gBreakpointLocation);
+
+ yield ensureThreadClientState(gPanel, "resumed");
+ yield testWhenBreakpointEnabledAndFirstSourceShown();
+
+ gSources._preferredSourceURL = EXAMPLE_URL + "code_script-switching-02.js";
+ yield reloadActiveTab(gPanel, gEvents.SOURCE_SHOWN);
+ yield testWhenBreakpointEnabledAndSecondSourceShown();
+
+ yield actions.disableBreakpoint(gBreakpointLocation);
+ yield reloadActiveTab(gPanel, gEvents.SOURCE_SHOWN);
+
+ yield testWhenBreakpointDisabledAndSecondSourceShown();
+
+ yield actions.enableBreakpoint(gBreakpointLocation);
+ yield reloadActiveTab(gPanel, gEvents.SOURCE_SHOWN);
+ yield testWhenBreakpointEnabledAndSecondSourceShown();
+
+ yield resumeDebuggerThenCloseAndFinish(gPanel);
+ });
+
+ function verifyView({ disabled }) {
+ return Task.spawn(function* () {
+ // It takes a tick for the checkbox in the SideMenuWidget and the
+ // gutter in the editor to get updated.
+ yield waitForTick();
+
+ let breakpoint = queries.getBreakpoint(getState(), gBreakpointLocation);
+ let breakpointItem = gSources._getBreakpoint(breakpoint);
+ is(!!breakpoint.disabled, disabled,
+ "The selected breakpoint state was correct.");
+
+ is(breakpointItem.attachment.view.checkbox.hasAttribute("checked"), !disabled,
+ "The selected breakpoint's checkbox state was correct.");
+ });
+ }
+
+ // All the following executeSoon()'s are required to spin the event loop
+ // before causing the debuggee to pause, to allow functions to yield first.
+
+ function testWhenBreakpointEnabledAndFirstSourceShown() {
+ return Task.spawn(function* () {
+ yield ensureSourceIs(gPanel, "-01.js");
+ yield verifyView({ disabled: false });
+
+ callInTab(gTab, "firstCall");
+ yield waitForDebuggerEvents(gPanel, gEvents.FETCHED_SCOPES);
+ yield ensureSourceIs(gPanel, "-01.js");
+ yield ensureCaretAt(gPanel, 5);
+ yield verifyView({ disabled: false });
+
+ executeSoon(() => gDebugger.gThreadClient.resume());
+ yield waitForSourceAndCaretAndScopes(gPanel, "-02.js", 6);
+ yield verifyView({ disabled: false });
+ });
+ }
+
+ function testWhenBreakpointEnabledAndSecondSourceShown() {
+ return Task.spawn(function* () {
+ yield ensureSourceIs(gPanel, "-02.js", true);
+ yield verifyView({ disabled: false });
+
+ callInTab(gTab, "firstCall");
+ yield waitForSourceAndCaretAndScopes(gPanel, "-01.js", 1);
+ yield verifyView({ disabled: false });
+
+ executeSoon(() => gDebugger.gThreadClient.resume());
+ yield waitForSourceAndCaretAndScopes(gPanel, "-02.js", 6);
+ yield verifyView({ disabled: false });
+ });
+ }
+
+ function testWhenBreakpointDisabledAndSecondSourceShown() {
+ return Task.spawn(function* () {
+ yield ensureSourceIs(gPanel, "-02.js", true);
+ yield verifyView({ disabled: true });
+
+ callInTab(gTab, "firstCall");
+ yield waitForDebuggerEvents(gPanel, gEvents.FETCHED_SCOPES);
+ yield ensureSourceIs(gPanel, "-02.js");
+ yield ensureCaretAt(gPanel, 6);
+ yield verifyView({ disabled: true });
+
+ executeSoon(() => gDebugger.gThreadClient.resume());
+ yield waitForDebuggerEvents(gPanel, gEvents.AFTER_FRAMES_CLEARED);
+ yield ensureSourceIs(gPanel, "-02.js");
+ yield ensureCaretAt(gPanel, 6);
+ yield verifyView({ disabled: true });
+ });
+ }
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-editor.js b/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-editor.js
new file mode 100644
index 000000000..d5232a50d
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-editor.js
@@ -0,0 +1,241 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Bug 723069: Test the debugger breakpoint API and connection to the
+ * source editor.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
+
+function test() {
+ let options = {
+ source: EXAMPLE_URL + "code_script-switching-01.js",
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ const gTab = aTab;
+ const gPanel = aPanel;
+ const gDebugger = gPanel.panelWin;
+ const gEditor = gDebugger.DebuggerView.editor;
+ const gSources = gDebugger.DebuggerView.Sources;
+ const queries = gDebugger.require("./content/queries");
+ const constants = gDebugger.require("./content/constants");
+ const actions = bindActionCreators(gPanel);
+ const getState = gDebugger.DebuggerController.getState;
+
+ Task.spawn(function* () {
+ yield waitForSourceAndCaretAndScopes(gPanel, "-02.js", 1);
+
+ is(gDebugger.gThreadClient.state, "paused",
+ "Should only be getting stack frames while paused.");
+ is(queries.getSourceCount(getState()), 2,
+ "Found the expected number of sources.");
+ is(gEditor.getText().indexOf("debugger"), 166,
+ "The correct source was loaded initially.");
+ is(queries.getSelectedSource(getState()).actor, gSources.values[1],
+ "The correct source is selected.");
+
+ is(queries.getBreakpoints(getState()).length, 0,
+ "No breakpoints currently added.");
+
+ info("Add the first breakpoint.");
+ gEditor.once("breakpointAdded", onEditorBreakpointAddFirst);
+ let location = { actor: gSources.selectedValue, line: 6 };
+ yield actions.addBreakpoint(location);
+ checkFirstBreakpoint(location);
+
+ info("Remove the first breakpoint.");
+ gEditor.once("breakpointRemoved", onEditorBreakpointRemoveFirst);
+ yield actions.removeBreakpoint(location);
+ checkFirstBreakpointRemoved(location);
+ checkBackgroundBreakpoint(yield testBreakpointAddBackground());
+
+ info("Switch to the first source, which is not yet selected");
+ gEditor.once("breakpointAdded", onEditorBreakpointAddSwitch);
+ gEditor.once("change", onEditorTextChanged);
+ actions.selectSource(gSources.items[0].attachment.source);
+ yield waitForDebuggerEvents(gPanel, gDebugger.EVENTS.SOURCE_SHOWN);
+ onReadyForClick();
+ });
+
+ callInTab(gTab, "firstCall");
+
+ let breakpointsAdded = 0;
+ let breakpointsRemoved = 0;
+ let editorBreakpointChanges = 0;
+
+ function onEditorBreakpointAddFirst(aEvent, aLine) {
+ editorBreakpointChanges++;
+
+ ok(aEvent,
+ "breakpoint1 added to the editor.");
+ is(aLine, 5,
+ "Editor breakpoint line is correct.");
+
+ is(gEditor.getBreakpoints().length, 1,
+ "editor.getBreakpoints().length is correct.");
+ }
+
+ function onEditorBreakpointRemoveFirst(aEvent, aLine) {
+ editorBreakpointChanges++;
+
+ ok(aEvent,
+ "breakpoint1 removed from the editor.");
+ is(aLine, 5,
+ "Editor breakpoint line is correct.");
+
+ is(gEditor.getBreakpoints().length, 0,
+ "editor.getBreakpoints().length is correct.");
+ }
+
+ function checkFirstBreakpoint(location) {
+ breakpointsAdded++;
+ const bp = queries.getBreakpoint(getState(), location);
+
+ ok(bp,
+ "breakpoint1 exists");
+ is(bp.location.actor, queries.getSelectedSource(getState()).actor,
+ "breakpoint1 actor is correct.");
+ is(bp.location.line, 6,
+ "breakpoint1 line is correct.");
+
+ is(queries.getBreakpoints(getState()).length, 1,
+ "The list of added breakpoints holds only one breakpoint.");
+
+ is(queries.getSelectedSource(getState()).actor, gSources.values[1],
+ "The second source should be currently selected.");
+ }
+
+ function checkFirstBreakpointRemoved(location) {
+ breakpointsRemoved++;
+ const bp = queries.getBreakpoint(getState(), location);
+ ok(!bp, "breakpoint1 removed");
+ }
+
+ function testBreakpointAddBackground() {
+ is(queries.getBreakpoints(getState()).length, 0,
+ "No breakpoints currently added.");
+
+ is(gSources.values[1], gSources.selectedValue,
+ "The second source should be currently selected.");
+
+ info("Add a breakpoint to the first source, which is not selected.");
+ let location = { actor: gSources.values[0], line: 5 };
+ gEditor.on("breakpointAdded", onEditorBreakpointAddBackgroundTrap);
+ return actions.addBreakpoint(location).then(() => location);
+ }
+
+ function onEditorBreakpointAddBackgroundTrap() {
+ // Trap listener: no breakpoint must be added to the editor when a
+ // breakpoint is added to a source that is not currently selected.
+ editorBreakpointChanges++;
+ ok(false, "breakpoint2 must not be added to the editor.");
+ }
+
+ function checkBackgroundBreakpoint(location) {
+ breakpointsAdded++;
+ const bp = queries.getBreakpoint(getState(), location);
+
+ ok(bp,
+ "breakpoint2 added, client received");
+ is(bp.location.actor, gSources.values[0],
+ "breakpoint2 client url is correct.");
+ is(bp.location.line, 5,
+ "breakpoint2 client line is correct.");
+
+ ok(queries.getBreakpoint(getState(), bp.location),
+ "breakpoint2 found in the list of added breakpoints.");
+
+ is(queries.getBreakpoints(getState()).length, 1,
+ "The list of added breakpoints holds only one breakpoint.");
+
+ is(queries.getSelectedSource(getState()).actor, gSources.values[1],
+ "The second source should be currently selected.");
+
+ // Remove the trap listener.
+ gEditor.off("breakpointAdded", onEditorBreakpointAddBackgroundTrap);
+ }
+
+ function onEditorBreakpointAddSwitch(aEvent, aLine) {
+ editorBreakpointChanges++;
+
+ ok(aEvent,
+ "breakpoint2 added to the editor.");
+ is(aLine, 4,
+ "Editor breakpoint line is correct.");
+
+ is(gEditor.getBreakpoints().length, 1,
+ "editor.getBreakpoints().length is correct");
+ }
+
+ function onEditorTextChanged() {
+ // Wait for the actual text to be shown.
+ if (gEditor.getText() == gDebugger.L10N.getStr("loadingText"))
+ return void gEditor.once("change", onEditorTextChanged);
+
+ is(gEditor.getText().indexOf("debugger"), -1,
+ "The second source is no longer displayed.");
+ is(gEditor.getText().indexOf("firstCall"), 118,
+ "The first source is displayed.");
+
+ is(gSources.values[0], gSources.selectedValue,
+ "The first source should be currently selected.");
+ }
+
+ function onReadyForClick() {
+ info("Remove the second breakpoint using the mouse.");
+ gEditor.once("breakpointRemoved", onEditorBreakpointRemoveSecond);
+
+ let iframe = gEditor.container;
+ let testWin = iframe.ownerDocument.defaultView;
+
+ // Flush the layout for the iframe.
+ info("rect " + iframe.contentDocument.documentElement.getBoundingClientRect());
+
+ let utils = testWin
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+
+ let coords = gEditor.getCoordsFromPosition({ line: 4, ch: 0 });
+ let rect = iframe.getBoundingClientRect();
+ let left = rect.left + 10;
+ let top = rect.top + coords.top + 4;
+ utils.sendMouseEventToWindow("mousedown", left, top, 0, 1, 0, false, 0, 0);
+ utils.sendMouseEventToWindow("mouseup", left, top, 0, 1, 0, false, 0, 0);
+ }
+
+ function onEditorBreakpointRemoveSecond(aEvent, aLine) {
+ editorBreakpointChanges++;
+
+ ok(aEvent,
+ "breakpoint2 removed from the editor.");
+ is(aLine, 4,
+ "Editor breakpoint line is correct.");
+
+ is(gEditor.getBreakpoints().length, 0,
+ "editor.getBreakpoints().length is correct.");
+
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.AFTER_FRAMES_CLEARED).then(() => {
+ finalCheck();
+ closeDebuggerAndFinish(gPanel);
+ });
+
+ gDebugger.gThreadClient.resume();
+ }
+
+ function finalCheck() {
+ is(queries.getBreakpoints(getState()).length, 0,
+ "No breakpoints currently added.");
+
+ is(breakpointsAdded, 2,
+ "Correct number of breakpoints have been added.");
+ is(breakpointsRemoved, 1,
+ "Correct number of breakpoints have been removed.");
+ is(editorBreakpointChanges, 4,
+ "Correct number of editor breakpoint changes.");
+ }
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-eval.js b/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-eval.js
new file mode 100644
index 000000000..795059c70
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-eval.js
@@ -0,0 +1,47 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Test setting breakpoints on an eval script
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_script-eval.html";
+
+function test() {
+ let options = {
+ source: EXAMPLE_URL + "code_script-eval.js",
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ const gTab = aTab;
+ const gPanel = aPanel;
+ const gDebugger = gPanel.panelWin;
+ const gSources = gDebugger.DebuggerView.Sources;
+ const actions = bindActionCreators(gPanel);
+
+ Task.spawn(function* () {
+ let newSource = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.NEW_SOURCE);
+ callInTab(gTab, "evalSourceWithSourceURL");
+ yield newSource;
+ // Wait for it to be added to the UI
+ yield waitForTick();
+
+ const newSourceActor = getSourceActor(gSources, EXAMPLE_URL + "bar.js");
+ yield actions.addBreakpoint({
+ actor: newSourceActor,
+ line: 2
+ });
+ yield ensureThreadClientState(gPanel, "resumed");
+
+ const paused = waitForThreadEvents(gPanel, "paused");
+ callInTab(gTab, "bar");
+ let frame = (yield paused).frame;
+ is(frame.where.source.actor, newSourceActor, "Should have broken on the eval'ed source");
+ is(frame.where.line, 2, "Should break on line 2");
+
+ yield resumeDebuggerThenCloseAndFinish(gPanel);
+ });
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-highlight.js b/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-highlight.js
new file mode 100644
index 000000000..d04a752ca
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-highlight.js
@@ -0,0 +1,90 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Test if breakpoints are highlighted when they should.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
+
+function test() {
+ let options = {
+ source: EXAMPLE_URL + "code_script-switching-01.js",
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ const gTab = aTab;
+ const gPanel = aPanel;
+ const gDebugger = gPanel.panelWin;
+ const gEditor = gDebugger.DebuggerView.editor;
+ const gSources = gDebugger.DebuggerView.Sources;
+ const queries = gDebugger.require("./content/queries");
+ const actions = bindActionCreators(gPanel);
+ const getState = gDebugger.DebuggerController.getState;
+
+ const addBreakpoints = Task.async(function* () {
+ yield actions.addBreakpoint({ actor: gSources.values[0], line: 5 });
+ yield actions.addBreakpoint({ actor: gSources.values[1], line: 6 });
+ yield actions.addBreakpoint({ actor: gSources.values[1], line: 7 });
+ yield actions.addBreakpoint({ actor: gSources.values[1], line: 8 });
+ yield actions.addBreakpoint({ actor: gSources.values[1], line: 9 });
+ });
+
+ function clickBreakpointAndCheck(aBreakpointIndex, aSourceIndex, aCaretLine) {
+ let finished = waitForCaretUpdated(gPanel, aCaretLine).then(() => {
+ checkHighlight(gSources.values[aSourceIndex], aCaretLine);
+ checkEditorContents(aSourceIndex);
+
+ is(queries.getSelectedSource(getState()).actor,
+ gSources.items[aSourceIndex].value,
+ "The currently selected source value is incorrect (1).");
+ ok(isCaretPos(gPanel, aCaretLine),
+ "The editor caret line and column were incorrect (1).");
+ });
+
+ EventUtils.sendMouseEvent(
+ { type: "click" },
+ gDebugger.document.querySelectorAll(".dbg-breakpoint")[aBreakpointIndex],
+ gDebugger
+ );
+
+ return finished;
+ }
+
+ function checkHighlight(actor, line) {
+ let breakpoint = gSources._selectedBreakpoint;
+ let breakpointItem = gSources._getBreakpoint(breakpoint);
+
+ is(breakpoint.location.actor, actor,
+ "The currently selected breakpoint actor is incorrect.");
+ is(breakpoint.location.line, line,
+ "The currently selected breakpoint line is incorrect.");
+ is(breakpointItem.attachment.actor, actor,
+ "The selected breakpoint item's source location attachment is incorrect.");
+ ok(breakpointItem.target.classList.contains("selected"),
+ "The selected breakpoint item's target should have a selected class.");
+ }
+
+ function checkEditorContents(aSourceIndex) {
+ if (aSourceIndex == 0) {
+ is(gEditor.getText().indexOf("firstCall"), 118,
+ "The first source is correctly displayed.");
+ } else {
+ is(gEditor.getText().indexOf("debugger"), 166,
+ "The second source is correctly displayed.");
+ }
+ }
+
+ Task.spawn(function* () {
+ yield addBreakpoints();
+ yield clickBreakpointAndCheck(0, 0, 5);
+ yield clickBreakpointAndCheck(1, 1, 6);
+ yield clickBreakpointAndCheck(2, 1, 7);
+ yield clickBreakpointAndCheck(3, 1, 8);
+ yield clickBreakpointAndCheck(4, 1, 9);
+ closeDebuggerAndFinish(gPanel);
+ });
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-new-script.js b/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-new-script.js
new file mode 100644
index 000000000..6e0537f82
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-new-script.js
@@ -0,0 +1,92 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Bug 771452: Make sure that setting a breakpoint in an inline source doesn't
+ * add it twice.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_inline-script.html";
+
+function test() {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ const gTab = aTab;
+ const gPanel = aPanel;
+ const gDebugger = gPanel.panelWin;
+ const gSources = gDebugger.DebuggerView.Sources;
+ const queries = gDebugger.require('./content/queries');
+ const actions = bindActionCreators(gPanel);
+ const getState = gDebugger.DebuggerController.getState;
+
+ function testResume() {
+ const deferred = promise.defer();
+ is(gDebugger.gThreadClient.state, "paused",
+ "The breakpoint wasn't hit yet.");
+
+ gDebugger.gThreadClient.resume(() => {
+ gDebugger.gThreadClient.addOneTimeListener("paused", (aEvent, aPacket) => {
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES).then(() => {
+ is(aPacket.why.type, "breakpoint",
+ "Execution has advanced to the next breakpoint.");
+ isnot(aPacket.why.type, "debuggerStatement",
+ "The breakpoint was hit before the debugger statement.");
+ ok(isCaretPos(gPanel, 20),
+ "The source editor caret position is incorrect (2).");
+
+ deferred.resolve();
+ });
+ });
+
+ generateMouseClickInTab(gTab, "content.document.querySelector('button')");
+ });
+
+ return deferred.promise;
+ }
+
+ function testBreakpointHit() {
+ const deferred = promise.defer();
+ is(gDebugger.gThreadClient.state, "paused",
+ "The breakpoint was hit.");
+
+ gDebugger.gThreadClient.addOneTimeListener("paused", (aEvent, aPacket) => {
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES).then(() => {
+ is(aPacket.why.type, "debuggerStatement",
+ "Execution has advanced to the next line.");
+ isnot(aPacket.why.type, "breakpoint",
+ "No ghost breakpoint was hit.");
+ ok(isCaretPos(gPanel, 20),
+ "The source editor caret position is incorrect (3).");
+
+ deferred.resolve();
+ });
+ });
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ gDebugger.document.getElementById("resume"),
+ gDebugger);
+ return deferred.promise;
+ }
+
+ Task.spawn(function(){
+ let onCaretUpdated = waitForCaretAndScopes(gPanel, 16);
+ callInTab(gTab, "runDebuggerStatement");
+ yield onCaretUpdated;
+
+ is(gDebugger.gThreadClient.state, "paused",
+ "The debugger statement was reached.");
+ ok(isCaretPos(gPanel, 16),
+ "The source editor caret position is incorrect (1).");
+
+ yield actions.addBreakpoint({ actor: getSourceActor(gSources, TAB_URL), line: 20 });
+ yield testResume();
+ yield testBreakpointHit();
+ resumeDebuggerThenCloseAndFinish(gPanel);
+ });
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-other-tabs.js b/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-other-tabs.js
new file mode 100644
index 000000000..2763eee95
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-other-tabs.js
@@ -0,0 +1,41 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure that setting a breakpoint in one tab, doesn't cause another tab at
+ * the same source to pause at that location.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_breakpoints-other-tabs.html";
+
+var test = Task.async(function* () {
+ const options = {
+ source: EXAMPLE_URL + "code_breakpoints-other-tabs.js",
+ line: 1
+ };
+ const [tab1,, panel1] = yield initDebugger(TAB_URL, options);
+ const [tab2,, panel2] = yield initDebugger(TAB_URL, options);
+ const queries = panel1.panelWin.require("./content/queries");
+ const actions = bindActionCreators(panel1);
+ const getState = panel1.panelWin.DebuggerController.getState;
+
+ const sources = panel1.panelWin.DebuggerView.Sources;
+
+ yield actions.addBreakpoint({
+ actor: queries.getSelectedSource(getState()).actor,
+ line: 2
+ });
+
+ const paused = waitForThreadEvents(panel2, "paused");
+ callInTab(tab2, "testCase");
+ const packet = yield paused;
+
+ is(packet.why.type, "debuggerStatement",
+ "Should have stopped at the debugger statement, not the other tab's breakpoint");
+ is(packet.frame.where.line, 3,
+ "Should have stopped at line 3 (debugger statement), not line 2 (other tab's breakpoint)");
+
+ yield resumeDebuggerThenCloseAndFinish(panel2);
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-pane.js b/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-pane.js
new file mode 100644
index 000000000..c576603d4
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-pane.js
@@ -0,0 +1,238 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Bug 723071: Test adding a pane to display the list of breakpoints across
+ * all sources in the debuggee.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
+
+function test() {
+ let options = {
+ source: EXAMPLE_URL + "code_script-switching-01.js",
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ const gTab = aTab;
+ const gPanel = aPanel;
+ const gDebugger = gPanel.panelWin;
+ const gEditor = gDebugger.DebuggerView.editor;
+ const gSources = gDebugger.DebuggerView.Sources;
+ const queries = gDebugger.require("./content/queries");
+ const actions = bindActionCreators(gPanel);
+ const getState = gDebugger.DebuggerController.getState;
+ const { getBreakpoint } = queries;
+
+ let breakpointsAdded = 0;
+ let breakpointsDisabled = 0;
+ let breakpointsRemoved = 0;
+ let breakpointsList;
+
+ const addBreakpoints = Task.async(function* (aIncrementFlag) {
+ const loc1 = { actor: gSources.selectedValue, line: 6 };
+ yield actions.addBreakpoint(loc1);
+ onBreakpointAdd(getBreakpoint(getState(), loc1), {
+ increment: aIncrementFlag,
+ line: 6,
+ text: "debugger;"
+ });
+
+ const loc2 = { actor: gSources.selectedValue, line: 7 };
+ yield actions.addBreakpoint(loc2);
+ onBreakpointAdd(getBreakpoint(getState(), loc2), {
+ increment: aIncrementFlag,
+ line: 7,
+ text: "function foo() {}"
+ });
+
+ const loc3 = {actor: gSources.selectedValue, line: 9 };
+ yield actions.addBreakpoint(loc3);
+ onBreakpointAdd(getBreakpoint(getState(), loc3), {
+ increment: aIncrementFlag,
+ line: 9,
+ text: "foo();"
+ });
+ });
+
+ function disableBreakpoints() {
+ let deferred = promise.defer();
+
+ let nodes = breakpointsList.querySelectorAll(".dbg-breakpoint");
+ info("Nodes to disable: " + breakpointsAdded.length);
+
+ is(nodes.length, breakpointsAdded,
+ "The number of nodes to disable is incorrect.");
+
+ for (let node of nodes) {
+ info("Disabling breakpoint: " + node.id);
+
+ let sourceItem = gSources.getItemForElement(node);
+ let breakpointItem = gSources.getItemForElement.call(sourceItem, node);
+ info("Found data: " + breakpointItem.attachment.toSource());
+
+ actions.disableBreakpoint(breakpointItem.attachment).then(() => {
+ if (++breakpointsDisabled == breakpointsAdded) {
+ deferred.resolve();
+ }
+ });
+ }
+
+ return deferred.promise;
+ }
+
+ function removeBreakpoints() {
+ let deferred = promise.defer();
+
+ let nodes = breakpointsList.querySelectorAll(".dbg-breakpoint");
+ info("Nodes to remove: " + breakpointsAdded.length);
+
+ is(nodes.length, breakpointsAdded,
+ "The number of nodes to remove is incorrect.");
+
+ for (let node of nodes) {
+ info("Removing breakpoint: " + node.id);
+
+ let sourceItem = gSources.getItemForElement(node);
+ let breakpointItem = gSources.getItemForElement.call(sourceItem, node);
+ info("Found data: " + breakpointItem.attachment.toSource());
+
+ actions.removeBreakpoint(breakpointItem.attachment).then(() => {
+ if (++breakpointsRemoved == breakpointsAdded) {
+ deferred.resolve();
+ }
+ });
+ }
+
+ return deferred.promise;
+ }
+
+ function onBreakpointAdd(bp, testData) {
+ if (testData.increment) {
+ breakpointsAdded++;
+ }
+
+ is(breakpointsList.querySelectorAll(".dbg-breakpoint").length, breakpointsAdded,
+ testData.increment
+ ? "Should have added a breakpoint in the pane."
+ : "Should have the same number of breakpoints in the pane.");
+
+ let identifier = queries.makeLocationId(bp.location);
+ let node = gDebugger.document.getElementById("breakpoint-" + identifier);
+ let line = node.getElementsByClassName("dbg-breakpoint-line")[0];
+ let text = node.getElementsByClassName("dbg-breakpoint-text")[0];
+ let check = node.querySelector("checkbox");
+
+ ok(node,
+ "Breakpoint element found successfully.");
+ is(line.getAttribute("value"), testData.line,
+ "The expected information wasn't found in the breakpoint element.");
+ is(text.getAttribute("value"), testData.text,
+ "The expected line text wasn't found in the breakpoint element.");
+ is(check.getAttribute("checked"), "true",
+ "The breakpoint enable checkbox is checked as expected.");
+ }
+
+ Task.spawn(function* () {
+ yield waitForSourceAndCaretAndScopes(gPanel, "-02.js", 1);
+
+ is(gDebugger.gThreadClient.state, "paused",
+ "Should only be getting stack frames while paused.");
+ is(queries.getSourceCount(getState()), 2,
+ "Found the expected number of sources.");
+ is(gEditor.getText().indexOf("debugger"), 166,
+ "The correct source was loaded initially.");
+ is(gSources.selectedValue, gSources.values[1],
+ "The correct source is selected.");
+
+ is(queries.getBreakpoints(getState()).length, 0,
+ "No breakpoints currently added.");
+
+ let breakpointsParent = gSources.widget._parent;
+ breakpointsList = gSources.widget._list;
+
+ is(breakpointsParent.childNodes.length, 1, // one sources list
+ "Found junk in the breakpoints container.");
+ is(breakpointsList.childNodes.length, 1, // one sources group
+ "Found junk in the breakpoints container.");
+ is(breakpointsList.querySelectorAll(".dbg-breakpoint").length, 0,
+ "No breakpoints should be visible at this point.");
+
+ yield addBreakpoints(true);
+
+ is(breakpointsAdded, 3,
+ "Should have added 3 breakpoints so far.");
+ is(breakpointsDisabled, 0,
+ "Shouldn't have disabled anything so far.");
+ is(breakpointsRemoved, 0,
+ "Shouldn't have removed anything so far.");
+
+ is(breakpointsParent.childNodes.length, 1, // one sources list
+ "Found junk in the breakpoints container.");
+ is(breakpointsList.childNodes.length, 1, // one sources group
+ "Found junk in the breakpoints container.");
+ is(breakpointsList.querySelectorAll(".dbg-breakpoint").length, 3,
+ "3 breakpoints should be visible at this point.");
+
+ yield disableBreakpoints();
+
+ is(breakpointsAdded, 3,
+ "Should still have 3 breakpoints added so far.");
+ is(breakpointsDisabled, 3,
+ "Should have 3 disabled breakpoints.");
+ is(breakpointsRemoved, 0,
+ "Shouldn't have removed anything so far.");
+
+ is(breakpointsParent.childNodes.length, 1, // one sources list
+ "Found junk in the breakpoints container.");
+ is(breakpointsList.childNodes.length, 1, // one sources group
+ "Found junk in the breakpoints container.");
+ is(breakpointsList.querySelectorAll(".dbg-breakpoint").length, breakpointsAdded,
+ "Should have the same number of breakpoints in the pane.");
+ is(breakpointsList.querySelectorAll(".dbg-breakpoint").length, breakpointsDisabled,
+ "Should have the same number of disabled breakpoints.");
+
+ yield addBreakpoints();
+
+ is(breakpointsAdded, 3,
+ "Should still have only 3 breakpoints added so far.");
+ is(breakpointsDisabled, 3,
+ "Should still have 3 disabled breakpoints.");
+ is(breakpointsRemoved, 0,
+ "Shouldn't have removed anything so far.");
+
+ is(breakpointsParent.childNodes.length, 1, // one sources list
+ "Found junk in the breakpoints container.");
+ is(breakpointsList.childNodes.length, 1, // one sources group
+ "Found junk in the breakpoints container.");
+ is(breakpointsList.querySelectorAll(".dbg-breakpoint").length, breakpointsAdded,
+ "Since half of the breakpoints already existed, but disabled, " +
+ "only half of the added breakpoints are actually in the pane.");
+
+ yield removeBreakpoints();
+
+ is(breakpointsRemoved, 3,
+ "Should have 3 removed breakpoints.");
+
+ is(breakpointsParent.childNodes.length, 1, // one sources list
+ "Found junk in the breakpoints container.");
+ is(breakpointsList.childNodes.length, 1, // one sources group
+ "Found junk in the breakpoints container.");
+ is(breakpointsList.querySelectorAll(".dbg-breakpoint").length, 0,
+ "No breakpoints should be visible at this point.");
+
+ const cleared = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.AFTER_FRAMES_CLEARED);
+ gDebugger.gThreadClient.resume();
+ yield cleared;
+
+ is(queries.getBreakpoints(getState()).length, 0,
+ "No breakpoints currently added.");
+
+ closeDebuggerAndFinish(gPanel);
+ });
+
+ callInTab(gTab, "firstCall");
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-reload.js b/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-reload.js
new file mode 100644
index 000000000..b86c0ec04
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-reload.js
@@ -0,0 +1,39 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure that setting a breakpoint on code that gets run on load, will get
+ * hit when we reload.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_breakpoints-reload.html";
+
+var test = Task.async(function* () {
+ requestLongerTimeout(4);
+
+ const options = {
+ source: TAB_URL,
+ line: 1
+ };
+ const [tab,, panel] = yield initDebugger(TAB_URL, options);
+ const actions = bindActionCreators(panel);
+
+ const sources = panel.panelWin.DebuggerView.Sources;
+ yield actions.addBreakpoint({
+ actor: sources.selectedValue,
+ line: 10 // "break on me" string
+ });
+
+ const paused = waitForThreadEvents(panel, "paused");
+ yield reloadActiveTab(panel, panel.panelWin.EVENTS.SOURCE_SHOWN);
+ const packet = yield paused;
+
+ is(packet.why.type, "breakpoint",
+ "Should have hit the breakpoint after the reload");
+ is(packet.frame.where.line, 10,
+ "Should have stopped at line 10, where we set the breakpoint");
+
+ yield resumeDebuggerThenCloseAndFinish(panel);
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_bug-896139.js b/devtools/client/debugger/test/mochitest/browser_dbg_bug-896139.js
new file mode 100644
index 000000000..39df159bc
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_bug-896139.js
@@ -0,0 +1,48 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Bug 896139 - Breakpoints not triggering when reloading script.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_bug-896139.html";
+const SCRIPT_URL = EXAMPLE_URL + "code_bug-896139.js";
+
+function test() {
+ Task.spawn(function* () {
+ function testBreakpoint() {
+ let promise = waitForDebuggerEvents(panel, win.EVENTS.FETCHED_SCOPES);
+ callInTab(tab, "f");
+ return promise.then(() => doResume(panel));
+ }
+
+ let options = {
+ source: SCRIPT_URL,
+ line: 1
+ };
+ let [tab,, panel] = yield initDebugger(TAB_URL, options);
+ let win = panel.panelWin;
+
+ let Sources = win.DebuggerView.Sources;
+
+ yield panel.addBreakpoint({
+ actor: getSourceActor(win.DebuggerView.Sources, SCRIPT_URL),
+ line: 6
+ });
+
+ // Race condition: the setBreakpoint request sometimes leaves the
+ // debugger in paused state for a bit because we are called before
+ // that request finishes (see bug 1156531 for plans to fix)
+ if (panel.panelWin.gThreadClient.state !== "attached") {
+ yield waitForThreadEvents(panel, "resumed");
+ }
+
+ yield testBreakpoint();
+ yield reloadActiveTab(panel, win.EVENTS.SOURCE_SHOWN);
+ yield testBreakpoint();
+
+ yield closeDebuggerAndFinish(panel);
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_chrome-create.js b/devtools/client/debugger/test/mochitest/browser_dbg_chrome-create.js
new file mode 100644
index 000000000..33c21c3d5
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_chrome-create.js
@@ -0,0 +1,64 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests that a chrome debugger can be created in a new process.
+ */
+
+var gProcess;
+
+function test() {
+ // Windows XP and 8.1 test slaves are terribly slow at this test.
+ requestLongerTimeout(5);
+ Services.prefs.setBoolPref("devtools.debugger.remote-enabled", true);
+
+ initChromeDebugger(aOnClose).then(aProcess => {
+ gProcess = aProcess;
+
+ info("Starting test...");
+ performTest();
+ });
+}
+
+function performTest() {
+ ok(gProcess._dbgProcess,
+ "The remote debugger process wasn't created properly!");
+ ok(gProcess._dbgProcess.isRunning,
+ "The remote debugger process isn't running!");
+ is(typeof gProcess._dbgProcess.pid, "number",
+ "The remote debugger process doesn't have a pid (?!)");
+
+ info("process location: " + gProcess._dbgProcess.location);
+ info("process pid: " + gProcess._dbgProcess.pid);
+ info("process name: " + gProcess._dbgProcess.processName);
+ info("process sig: " + gProcess._dbgProcess.processSignature);
+
+ ok(gProcess._dbgProfilePath,
+ "The remote debugger profile wasn't created properly!");
+ is(gProcess._dbgProfilePath, OS.Path.join(OS.Constants.Path.profileDir, "chrome_debugger_profile"),
+ "The remote debugger profile isn't where we expect it!");
+
+ info("profile path: " + gProcess._dbgProfilePath);
+
+ gProcess.close();
+}
+
+function aOnClose() {
+ ok(!gProcess._dbgProcess.isRunning,
+ "The remote debugger process isn't closed as it should be!");
+ is(gProcess._dbgProcess.exitValue, (Services.appinfo.OS == "WINNT" ? 0 : 256),
+ "The remote debugger process didn't die cleanly.");
+
+ info("process exit value: " + gProcess._dbgProcess.exitValue);
+
+ info("profile path: " + gProcess._dbgProfilePath);
+
+ finish();
+}
+
+registerCleanupFunction(function () {
+ Services.prefs.clearUserPref("devtools.debugger.remote-enabled");
+ gProcess = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_chrome-debugging.js b/devtools/client/debugger/test/mochitest/browser_dbg_chrome-debugging.js
new file mode 100644
index 000000000..79e9e6566
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_chrome-debugging.js
@@ -0,0 +1,102 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests that chrome debugging works.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_inline-debugger-statement.html";
+
+var gClient, gThreadClient;
+var gAttached = promise.defer();
+var gNewGlobal = promise.defer();
+var gNewChromeSource = promise.defer();
+
+var { DevToolsLoader } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+var customLoader = new DevToolsLoader();
+customLoader.invisibleToDebugger = true;
+var { DebuggerServer } = customLoader.require("devtools/server/main");
+
+function test() {
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+ DebuggerServer.allowChromeProcess = true;
+
+ let transport = DebuggerServer.connectPipe();
+ gClient = new DebuggerClient(transport);
+ gClient.connect().then(([aType, aTraits]) => {
+ is(aType, "browser",
+ "Root actor should identify itself as a browser.");
+
+ promise.all([gAttached.promise, gNewGlobal.promise, gNewChromeSource.promise])
+ .then(resumeAndCloseConnection)
+ .then(finish)
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+
+ testChromeActor();
+ });
+}
+
+function testChromeActor() {
+ gClient.getProcess().then(aResponse => {
+ gClient.addListener("newGlobal", onNewGlobal);
+
+ let actor = aResponse.form.actor;
+ gClient.attachTab(actor, (response, tabClient) => {
+ tabClient.attachThread(null, (aResponse, aThreadClient) => {
+ gThreadClient = aThreadClient;
+ gThreadClient.addListener("newSource", onNewSource);
+
+ if (aResponse.error) {
+ ok(false, "Couldn't attach to the chrome debugger.");
+ gAttached.reject();
+ } else {
+ ok(true, "Attached to the chrome debugger.");
+ gAttached.resolve();
+
+ // Ensure that a new chrome global will be created.
+ gBrowser.selectedTab = gBrowser.addTab("about:mozilla");
+ }
+ });
+ });
+ });
+}
+
+function onNewGlobal() {
+ ok(true, "Received a new chrome global.");
+
+ gClient.removeListener("newGlobal", onNewGlobal);
+ gNewGlobal.resolve();
+}
+
+function onNewSource(aEvent, aPacket) {
+ if (aPacket.source.url.startsWith("chrome:")) {
+ ok(true, "Received a new chrome source: " + aPacket.source.url);
+
+ gThreadClient.removeListener("newSource", onNewSource);
+ gNewChromeSource.resolve();
+ }
+}
+
+function resumeAndCloseConnection() {
+ let deferred = promise.defer();
+ gThreadClient.resume(() => deferred.resolve(gClient.close()));
+ return deferred.promise;
+}
+
+registerCleanupFunction(function () {
+ gClient = null;
+ gThreadClient = null;
+ gAttached = null;
+ gNewGlobal = null;
+ gNewChromeSource = null;
+
+ customLoader = null;
+ DebuggerServer = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_clean-exit-window.js b/devtools/client/debugger/test/mochitest/browser_dbg_clean-exit-window.js
new file mode 100644
index 000000000..d09e8c70c
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_clean-exit-window.js
@@ -0,0 +1,86 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Test that closing a window with the debugger in a paused state exits cleanly.
+ */
+
+var gDebuggee, gPanel, gDebugger, gWindow;
+
+const TAB_URL = EXAMPLE_URL + "doc_inline-debugger-statement.html";
+
+function test() {
+ addWindow(TAB_URL)
+ .then(win => initDebugger(TAB_URL, { window: win }))
+ .then(([aTab, aDebuggee, aPanel, aWindow]) => {
+ gDebuggee = aDebuggee;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gWindow = aWindow;
+
+ return testCleanExit();
+ })
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+}
+
+function testCleanExit() {
+ let deferred = promise.defer();
+
+ ok(!!gWindow, "Second window created.");
+
+ gWindow.focus();
+
+ is(Services.wm.getMostRecentWindow("navigator:browser"), gWindow,
+ "The second window is on top.");
+
+ let isActive = promise.defer();
+ let isLoaded = promise.defer();
+
+ promise.all([isActive.promise, isLoaded.promise]).then(() => {
+ waitForSourceAndCaretAndScopes(gPanel, ".html", 16).then(() => {
+ is(gDebugger.gThreadClient.paused, true,
+ "Should be paused after the debugger statement.");
+ gWindow.close();
+ deferred.resolve();
+ finish();
+ });
+
+ gDebuggee.runDebuggerStatement();
+ });
+
+ if (Services.focus.activeWindow != gWindow) {
+ gWindow.addEventListener("activate", function onActivate(aEvent) {
+ if (aEvent.target != gWindow) {
+ return;
+ }
+ gWindow.removeEventListener("activate", onActivate, true);
+ isActive.resolve();
+ }, true);
+ } else {
+ isActive.resolve();
+ }
+
+ if (gWindow.content.location.href != TAB_URL) {
+ gWindow.document.addEventListener("load", function onLoad(aEvent) {
+ if (aEvent.target.documentURI != TAB_URL) {
+ return;
+ }
+ gWindow.document.removeEventListener("load", onLoad, true);
+ isLoaded.resolve();
+ }, true);
+ } else {
+ isLoaded.resolve();
+ }
+ return deferred.promise;
+}
+
+registerCleanupFunction(function () {
+ gWindow = null;
+ gDebuggee = null;
+ gPanel = null;
+ gDebugger = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_clean-exit.js b/devtools/client/debugger/test/mochitest/browser_dbg_clean-exit.js
new file mode 100644
index 000000000..25cbf550d
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_clean-exit.js
@@ -0,0 +1,44 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Test that closing a tab with the debugger in a paused state exits cleanly.
+ */
+
+var gTab, gPanel, gDebugger;
+
+const TAB_URL = EXAMPLE_URL + "doc_inline-debugger-statement.html";
+
+function test() {
+ const options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+
+ testCleanExit();
+ });
+}
+
+function testCleanExit() {
+ promise.all([
+ waitForSourceAndCaretAndScopes(gPanel, ".html", 16),
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.AFTER_FRAMES_REFILLED)
+ ]).then(() => {
+ is(gDebugger.gThreadClient.paused, true,
+ "Should be paused after the debugger statement.");
+ }).then(() => closeDebuggerAndFinish(gPanel, { whilePaused: true }));
+
+ callInTab(gTab, "runDebuggerStatement");
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_closure-inspection.js b/devtools/client/debugger/test/mochitest/browser_dbg_closure-inspection.js
new file mode 100644
index 000000000..739d3b2a7
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_closure-inspection.js
@@ -0,0 +1,153 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+const TAB_URL = EXAMPLE_URL + "doc_closures.html";
+
+// Test that inspecting a closure works as expected.
+
+function test() {
+ let gPanel, gTab, gDebugger;
+
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+
+ testClosure()
+ .then(() => resumeDebuggerThenCloseAndFinish(gPanel))
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+ });
+
+ function testClosure() {
+ generateMouseClickInTab(gTab, "content.document.querySelector('button')");
+
+ return waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES).then(() => {
+ let gVars = gDebugger.DebuggerView.Variables;
+ let localScope = gVars.getScopeAtIndex(0);
+ let localNodes = localScope.target.querySelector(".variables-view-element-details").childNodes;
+
+ is(localNodes[4].querySelector(".name").getAttribute("value"), "person",
+ "Should have the right property name for |person|.");
+ is(localNodes[4].querySelector(".value").getAttribute("value"), "Object",
+ "Should have the right property value for |person|.");
+
+ // Expand the 'person' tree node. This causes its properties to be
+ // retrieved and displayed.
+ let personNode = gVars.getItemForNode(localNodes[4]);
+ let personFetched = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_PROPERTIES);
+ personNode.expand();
+
+ return personFetched.then(() => {
+ is(personNode.expanded, true,
+ "|person| should be expanded at this point.");
+
+ is(personNode.get("getName").target.querySelector(".name")
+ .getAttribute("value"), "getName",
+ "Should have the right property name for 'getName' in person.");
+ is(personNode.get("getName").target.querySelector(".value")
+ .getAttribute("value"), "_pfactory/<.getName()",
+ "'getName' in person should have the right value.");
+ is(personNode.get("getFoo").target.querySelector(".name")
+ .getAttribute("value"), "getFoo",
+ "Should have the right property name for 'getFoo' in person.");
+ is(personNode.get("getFoo").target.querySelector(".value")
+ .getAttribute("value"), "_pfactory/<.getFoo()",
+ "'getFoo' in person should have the right value.");
+
+ // Expand the function nodes. This causes their properties to be
+ // retrieved and displayed.
+ let getFooNode = personNode.get("getFoo");
+ let getNameNode = personNode.get("getName");
+ let funcsFetched = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_PROPERTIES, 2);
+ let funcClosuresFetched = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES, 2);
+ getFooNode.expand();
+ getNameNode.expand();
+
+ return funcsFetched.then(() => {
+ is(getFooNode.expanded, true,
+ "|person.getFoo| should be expanded at this point.");
+ is(getNameNode.expanded, true,
+ "|person.getName| should be expanded at this point.");
+
+ is(getFooNode.get("<Closure>").target.querySelector(".name")
+ .getAttribute("value"), "<Closure>",
+ "Found the closure node for getFoo.");
+ is(getFooNode.get("<Closure>").target.querySelector(".value")
+ .getAttribute("value"), "",
+ "The closure node has no value for getFoo.");
+ is(getNameNode.get("<Closure>").target.querySelector(".name")
+ .getAttribute("value"), "<Closure>",
+ "Found the closure node for getName.");
+ is(getNameNode.get("<Closure>").target.querySelector(".value")
+ .getAttribute("value"), "",
+ "The closure node has no value for getName.");
+
+ // Expand the closure nodes. This causes their environments to be
+ // retrieved and displayed.
+ let getFooClosure = getFooNode.get("<Closure>");
+ let getNameClosure = getNameNode.get("<Closure>");
+ getFooClosure.expand();
+ getNameClosure.expand();
+
+ return funcClosuresFetched.then(() => {
+ is(getFooClosure.expanded, true,
+ "|person.getFoo| closure should be expanded at this point.");
+ is(getNameClosure.expanded, true,
+ "|person.getName| closure should be expanded at this point.");
+
+ is(getFooClosure.get("Function scope [_pfactory]").target.querySelector(".name")
+ .getAttribute("value"), "Function scope [_pfactory]",
+ "Found the function scope node for the getFoo closure.");
+ is(getFooClosure.get("Function scope [_pfactory]").target.querySelector(".value")
+ .getAttribute("value"), "",
+ "The function scope node has no value for the getFoo closure.");
+ is(getNameClosure.get("Function scope [_pfactory]").target.querySelector(".name")
+ .getAttribute("value"), "Function scope [_pfactory]",
+ "Found the function scope node for the getName closure.");
+ is(getNameClosure.get("Function scope [_pfactory]").target.querySelector(".value")
+ .getAttribute("value"), "",
+ "The function scope node has no value for the getName closure.");
+
+ // Expand the scope nodes.
+ let getFooInnerScope = getFooClosure.get("Function scope [_pfactory]");
+ let getNameInnerScope = getNameClosure.get("Function scope [_pfactory]");
+ let innerFuncsFetched = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_PROPERTIES, 2);
+ getFooInnerScope.expand();
+ getNameInnerScope.expand();
+
+ return funcsFetched.then(() => {
+ is(getFooInnerScope.expanded, true,
+ "|person.getFoo| inner scope should be expanded at this point.");
+ is(getNameInnerScope.expanded, true,
+ "|person.getName| inner scope should be expanded at this point.");
+
+ // Only test that each function closes over the necessary variable.
+ // We wouldn't want future SpiderMonkey closure space
+ // optimizations to break this test.
+ is(getFooInnerScope.get("foo").target.querySelector(".name")
+ .getAttribute("value"), "foo",
+ "Found the foo node for the getFoo inner scope.");
+ is(getFooInnerScope.get("foo").target.querySelector(".value")
+ .getAttribute("value"), "10",
+ "The foo node has the expected value.");
+ is(getNameInnerScope.get("name").target.querySelector(".name")
+ .getAttribute("value"), "name",
+ "Found the name node for the getName inner scope.");
+ is(getNameInnerScope.get("name").target.querySelector(".value")
+ .getAttribute("value"), '"Bob"',
+ "The name node has the expected value.");
+ });
+ });
+ });
+ });
+ });
+ }
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_cmd-blackbox.js b/devtools/client/debugger/test/mochitest/browser_dbg_cmd-blackbox.js
new file mode 100644
index 000000000..06ff8a4f3
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_cmd-blackbox.js
@@ -0,0 +1,117 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests that the 'dbg blackbox' and 'dbg unblackbox' commands work as
+ * they should.
+ */
+
+const TEST_URL = EXAMPLE_URL + "doc_blackboxing.html";
+const BLACKBOXME_URL = EXAMPLE_URL + "code_blackboxing_blackboxme.js";
+const BLACKBOXONE_URL = EXAMPLE_URL + "code_blackboxing_one.js";
+const BLACKBOXTWO_URL = EXAMPLE_URL + "code_blackboxing_two.js";
+const BLACKBOXTHREE_URL = EXAMPLE_URL + "code_blackboxing_three.js";
+
+function test() {
+ return Task.spawn(spawnTest).then(finish, helpers.handleError);
+}
+
+function* spawnTest() {
+ let options = yield helpers.openTab(TEST_URL);
+ yield helpers.openToolbar(options);
+
+ let toolbox = yield gDevTools.showToolbox(options.target, "jsdebugger");
+ let panel = toolbox.getCurrentPanel();
+ let constants = panel.panelWin.require("./content/constants");
+
+ yield waitForDebuggerEvents(panel, panel.panelWin.EVENTS.SOURCE_SHOWN);
+
+ function cmd(aTyped, aEventRepeat = 1, aOutput = "") {
+ return promise.all([
+ waitForDispatch(panel, constants.BLACKBOX, aEventRepeat),
+ helpers.audit(options, [{ setup: aTyped, output: aOutput, exec: {} }])
+ ]);
+ }
+
+ // test Black-Box Source
+ yield cmd("dbg blackbox " + BLACKBOXME_URL);
+
+ let bbButton = yield selectSourceAndGetBlackBoxButton(panel, BLACKBOXME_URL);
+ ok(bbButton.checked,
+ "Should be able to black box a specific source.");
+
+ // test Un-Black-Box Source
+ yield cmd("dbg unblackbox " + BLACKBOXME_URL);
+
+ bbButton = yield selectSourceAndGetBlackBoxButton(panel, BLACKBOXME_URL);
+ ok(!bbButton.checked,
+ "Should be able to stop black boxing a specific source.");
+
+ // test Black-Box Glob
+ yield cmd("dbg blackbox --glob *blackboxing_t*.js", 2,
+ [/blackboxing_three\.js/g, /blackboxing_two\.js/g]);
+
+ bbButton = yield selectSourceAndGetBlackBoxButton(panel, BLACKBOXME_URL);
+ ok(!bbButton.checked,
+ "blackboxme should not be black boxed because it doesn't match the glob.");
+ bbButton = yield selectSourceAndGetBlackBoxButton(panel, BLACKBOXONE_URL);
+ ok(!bbButton.checked,
+ "blackbox_one should not be black boxed because it doesn't match the glob.");
+
+ bbButton = yield selectSourceAndGetBlackBoxButton(panel, BLACKBOXTWO_URL);
+ ok(bbButton.checked,
+ "blackbox_two should be black boxed because it matches the glob.");
+ bbButton = yield selectSourceAndGetBlackBoxButton(panel, BLACKBOXTHREE_URL);
+ ok(bbButton.checked,
+ "blackbox_three should be black boxed because it matches the glob.");
+
+ // test Un-Black-Box Glob
+ yield cmd("dbg unblackbox --glob *blackboxing_t*.js", 2);
+
+ bbButton = yield selectSourceAndGetBlackBoxButton(panel, BLACKBOXTWO_URL);
+ ok(!bbButton.checked,
+ "blackbox_two should be un-black boxed because it matches the glob.");
+ bbButton = yield selectSourceAndGetBlackBoxButton(panel, BLACKBOXTHREE_URL);
+ ok(!bbButton.checked,
+ "blackbox_three should be un-black boxed because it matches the glob.");
+
+ // test Black-Box Invert
+ yield cmd("dbg blackbox --invert --glob *blackboxing_t*.js", 3,
+ [/blackboxing_three\.js/g, /blackboxing_two\.js/g]);
+
+ bbButton = yield selectSourceAndGetBlackBoxButton(panel, BLACKBOXME_URL);
+ ok(bbButton.checked,
+ "blackboxme should be black boxed because it doesn't match the glob.");
+ bbButton = yield selectSourceAndGetBlackBoxButton(panel, BLACKBOXONE_URL);
+ ok(bbButton.checked,
+ "blackbox_one should be black boxed because it doesn't match the glob.");
+ bbButton = yield selectSourceAndGetBlackBoxButton(panel, TEST_URL);
+ ok(bbButton.checked,
+ "TEST_URL should be black boxed because it doesn't match the glob.");
+
+ bbButton = yield selectSourceAndGetBlackBoxButton(panel, BLACKBOXTWO_URL);
+ ok(!bbButton.checked,
+ "blackbox_two should not be black boxed because it matches the glob.");
+ bbButton = yield selectSourceAndGetBlackBoxButton(panel, BLACKBOXTHREE_URL);
+ ok(!bbButton.checked,
+ "blackbox_three should not be black boxed because it matches the glob.");
+
+ // test Un-Black-Box Invert
+ yield cmd("dbg unblackbox --invert --glob *blackboxing_t*.js", 3);
+
+ bbButton = yield selectSourceAndGetBlackBoxButton(panel, BLACKBOXME_URL);
+ ok(!bbButton.checked,
+ "blackboxme should be un-black boxed because it does not match the glob.");
+ bbButton = yield selectSourceAndGetBlackBoxButton(panel, BLACKBOXONE_URL);
+ ok(!bbButton.checked,
+ "blackbox_one should be un-black boxed because it does not match the glob.");
+ bbButton = yield selectSourceAndGetBlackBoxButton(panel, TEST_URL);
+ ok(!bbButton.checked,
+ "TEST_URL should be un-black boxed because it doesn't match the glob.");
+
+ yield teardown(panel, { noTabRemoval: true });
+ yield helpers.closeToolbar(options);
+ yield helpers.closeTab(options);
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_cmd-break.js b/devtools/client/debugger/test/mochitest/browser_dbg_cmd-break.js
new file mode 100644
index 000000000..121bc5e99
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_cmd-break.js
@@ -0,0 +1,225 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests that the break commands works as they should.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_cmd-break.html";
+var TAB_URL_ACTOR;
+
+function test() {
+ let gPanel, gDebugger, gThreadClient, gSources;
+ let gLineNumber;
+
+ let expectedActorObj = {
+ value: null,
+ message: ""
+ };
+
+ helpers.addTabWithToolbar(TAB_URL, aOptions => {
+ return Task.spawn(function* () {
+ yield helpers.audit(aOptions, [{
+ setup: "break",
+ check: {
+ input: "break",
+ hints: " add line",
+ markup: "IIIII",
+ status: "ERROR",
+ }
+ }]);
+
+ yield helpers.audit(aOptions, [{
+ setup: "break add",
+ check: {
+ input: "break add",
+ hints: " line",
+ markup: "IIIIIVIII",
+ status: "ERROR"
+ }
+ }]);
+
+ yield helpers.audit(aOptions, [{
+ setup: "break add line",
+ check: {
+ input: "break add line",
+ hints: " <file> <line>",
+ markup: "VVVVVVVVVVVVVV",
+ status: "ERROR"
+ }
+ }]);
+
+ yield helpers.audit(aOptions, [{
+ name: "open toolbox",
+ setup: Task.async(function* () {
+ let [aTab, aDebuggee, aPanel] = yield initDebugger(gBrowser.selectedTab);
+
+ // Spin the event loop before causing the debuggee to pause, to allow this
+ // function to return first.
+ executeSoon(() => aDebuggee.firstCall());
+
+ yield waitForSourceAndCaretAndScopes(aPanel, ".html", 1);
+
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gThreadClient = gPanel.panelWin.gThreadClient;
+ gLineNumber = yield ContentTask.spawn(aOptions.browser, {}, function* () {
+ return "" + content.wrappedJSObject.gLineNumber;
+ });
+ gSources = gDebugger.DebuggerView.Sources;
+
+ expectedActorObj.value = getSourceActor(gSources, TAB_URL);
+ }),
+ post: function () {
+ ok(gThreadClient, "Debugger client exists.");
+ is(gLineNumber, 14, "gLineNumber is correct.");
+ },
+ }]);
+
+ yield helpers.audit(aOptions, [{
+ name: "break add line .../doc_cmd-break.html 14",
+ setup: function () {
+ // We have to setup in a function to allow gLineNumber to be initialized.
+ let line = "break add line " + TAB_URL + " " + gLineNumber;
+ return helpers.setInput(aOptions, line);
+ },
+ check: {
+ hints: "",
+ status: "VALID",
+ message: "",
+ args: {
+ file: expectedActorObj,
+ line: { value: 14 }
+ }
+ },
+ exec: {
+ output: "Added breakpoint"
+ }
+ }]);
+
+ yield helpers.audit(aOptions, [{
+ setup: "break add line " + TAB_URL + " 17",
+ check: {
+ hints: "",
+ status: "VALID",
+ message: "",
+ args: {
+ file: expectedActorObj,
+ line: { value: 17 }
+ }
+ },
+ exec: {
+ output: "Added breakpoint"
+ }
+ }]);
+
+ yield helpers.audit(aOptions, [{
+ setup: "break list",
+ check: {
+ input: "break list",
+ hints: "",
+ markup: "VVVVVVVVVV",
+ status: "VALID"
+ },
+ exec: {
+ output: [
+ /Source/, /Remove/,
+ /doc_cmd-break\.html:14/,
+ /doc_cmd-break\.html:17/
+ ]
+ }
+ }]);
+
+ yield helpers.audit(aOptions, [{
+ name: "cleanup",
+ setup: function () {
+ let deferred = promise.defer();
+ gThreadClient.resume(deferred.resolve);
+ return deferred.promise;
+ }
+ }]);
+
+ yield helpers.audit(aOptions, [{
+ setup: "break del 14",
+ check: {
+ input: "break del 14",
+ hints: " -> doc_cmd-break.html:14",
+ markup: "VVVVVVVVVVII",
+ status: "ERROR",
+ args: {
+ breakpoint: {
+ status: "INCOMPLETE",
+ message: "Value required for \u2018breakpoint\u2019."
+ }
+ }
+ }
+ }]);
+
+ yield helpers.audit(aOptions, [{
+ setup: "break del doc_cmd-break.html:14",
+ check: {
+ input: "break del doc_cmd-break.html:14",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV",
+ status: "VALID",
+ args: {
+ breakpoint: { arg: " doc_cmd-break.html:14" },
+ }
+ },
+ exec: {
+ output: "Breakpoint removed"
+ }
+ }]);
+
+ yield helpers.audit(aOptions, [{
+ setup: "break list",
+ check: {
+ input: "break list",
+ hints: "",
+ markup: "VVVVVVVVVV",
+ status: "VALID"
+ },
+ exec: {
+ output: [
+ /Source/, /Remove/,
+ /doc_cmd-break\.html:17/
+ ]
+ }
+ }]);
+
+ yield helpers.audit(aOptions, [{
+ setup: "break del doc_cmd-break.html:17",
+ check: {
+ input: "break del doc_cmd-break.html:17",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV",
+ status: "VALID",
+ args: {
+ breakpoint: { arg: " doc_cmd-break.html:17" },
+ }
+ },
+ exec: {
+ output: "Breakpoint removed"
+ }
+ }]);
+
+ yield helpers.audit(aOptions, [{
+ setup: "break list",
+ check: {
+ input: "break list",
+ hints: "",
+ markup: "VVVVVVVVVV",
+ status: "VALID"
+ },
+ exec: {
+ output: "No breakpoints set"
+ },
+ post: function () {
+ return teardown(gPanel, { noTabRemoval: true });
+ }
+ }]);
+ });
+ }).then(finish);
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_cmd-dbg.js b/devtools/client/debugger/test/mochitest/browser_dbg_cmd-dbg.js
new file mode 100644
index 000000000..e843d0cd5
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_cmd-dbg.js
@@ -0,0 +1,102 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests that the debugger commands work as they should.
+ */
+
+const TEST_URI = EXAMPLE_URL + "doc_cmd-dbg.html";
+
+function test() {
+ return Task.spawn(function* () {
+ let options = yield helpers.openTab(TEST_URI);
+ yield helpers.openToolbar(options);
+
+ yield helpers.audit(options, [{
+ setup: "dbg open",
+ exec: { output: "" }
+ }]);
+
+ let [gTab, gDebuggee, gPanel] = yield initDebugger(gBrowser.selectedTab);
+ let gDebugger = gPanel.panelWin;
+ let gThreadClient = gDebugger.gThreadClient;
+
+ yield helpers.audit(options, [{
+ setup: "dbg list",
+ exec: { output: /doc_cmd-dbg.html/ }
+ }]);
+
+ let button = gDebuggee.document.querySelector("input[type=button]");
+ let output = gDebuggee.document.querySelector("input[type=text]");
+
+ let cmd = function (aTyped, aState) {
+ return promise.all([
+ waitForThreadEvents(gPanel, aState),
+ helpers.audit(options, [{ setup: aTyped, exec: { output: "" } }])
+ ]);
+ };
+
+ let click = function (aElement, aState) {
+ return promise.all([
+ waitForThreadEvents(gPanel, aState),
+ executeSoon(() => EventUtils.sendMouseEvent({ type: "click" }, aElement, gDebuggee))
+ ]);
+ };
+
+ yield cmd("dbg interrupt", "paused");
+ is(gThreadClient.state, "paused", "Debugger is paused.");
+
+ yield cmd("dbg continue", "resumed");
+ isnot(gThreadClient.state, "paused", "Debugger has continued.");
+
+ yield click(button, "paused");
+ is(gThreadClient.state, "paused", "Debugger is paused again.");
+
+ yield cmd("dbg step in", "paused");
+ yield cmd("dbg step in", "paused");
+ yield cmd("dbg step in", "paused");
+ is(output.value, "step in", "Debugger stepped in.");
+
+ yield cmd("dbg step over", "paused");
+ is(output.value, "step over", "Debugger stepped over.");
+
+ yield cmd("dbg step out", "paused");
+ is(output.value, "step out", "Debugger stepped out.");
+
+ yield cmd("dbg continue", "paused");
+ is(output.value, "dbg continue", "Debugger continued.");
+
+ let closeDebugger = function () {
+ let deferred = promise.defer();
+
+ helpers.audit(options, [{
+ setup: "dbg close",
+ exec: { output: "" }
+ }])
+ .then(() => {
+ let toolbox = gDevTools.getToolbox(options.target);
+ if (!toolbox) {
+ ok(true, "Debugger is closed.");
+ deferred.resolve();
+ } else {
+ toolbox.on("destroyed", () => {
+ ok(true, "Debugger just closed.");
+ deferred.resolve();
+ });
+ }
+ });
+
+ return deferred.promise;
+ };
+
+ // We close the debugger twice to ensure 'dbg close' doesn't error when
+ // toolbox is already closed. See bug 884638 for more info.
+ yield closeDebugger();
+ yield closeDebugger();
+ yield helpers.closeToolbar(options);
+ yield helpers.closeTab(options);
+
+ }).then(finish, helpers.handleError);
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_conditional-breakpoints-01.js b/devtools/client/debugger/test/mochitest/browser_dbg_conditional-breakpoints-01.js
new file mode 100644
index 000000000..d8f9fca04
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_conditional-breakpoints-01.js
@@ -0,0 +1,218 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Bug 740825: Test the debugger conditional breakpoints.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_conditional-breakpoints.html";
+
+function test() {
+ // Linux debug test slaves are a bit slow at this test sometimes.
+ requestLongerTimeout(2);
+
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ const gTab = aTab;
+ const gPanel = aPanel;
+ const gDebugger = gPanel.panelWin;
+ const gEditor = gDebugger.DebuggerView.editor;
+ const gSources = gDebugger.DebuggerView.Sources;
+ const queries = gDebugger.require("./content/queries");
+ const constants = gDebugger.require("./content/constants");
+ const actions = bindActionCreators(gPanel);
+ const getState = gDebugger.DebuggerController.getState;
+
+ // This test forces conditional breakpoints to be evaluated on the
+ // client-side
+ var client = gPanel.target.client;
+ client.mainRoot.traits.conditionalBreakpoints = false;
+
+ const addBreakpoints = Task.async(function* () {
+ yield actions.addBreakpoint({ actor: gSources.selectedValue, line: 18 },
+ "undefined");
+ yield actions.addBreakpoint({ actor: gSources.selectedValue, line: 19 },
+ "null");
+ yield actions.addBreakpoint({ actor: gSources.selectedValue, line: 20 },
+ "42");
+ yield actions.addBreakpoint({ actor: gSources.selectedValue, line: 21 },
+ "true");
+ yield actions.addBreakpoint({ actor: gSources.selectedValue, line: 22 },
+ "'nasu'");
+ yield actions.addBreakpoint({ actor: gSources.selectedValue, line: 23 },
+ "/regexp/");
+ yield actions.addBreakpoint({ actor: gSources.selectedValue, line: 24 },
+ "({})");
+ yield actions.addBreakpoint({ actor: gSources.selectedValue, line: 25 },
+ "(function() {})");
+ yield actions.addBreakpoint({ actor: gSources.selectedValue, line: 26 },
+ "(function() { return false; })()");
+ yield actions.addBreakpoint({ actor: gSources.selectedValue, line: 27 },
+ "a");
+ yield actions.addBreakpoint({ actor: gSources.selectedValue, line: 28 },
+ "a !== undefined");
+ yield actions.addBreakpoint({ actor: gSources.selectedValue, line: 29 },
+ "b");
+ yield actions.addBreakpoint({ actor: gSources.selectedValue, line: 30 },
+ "a !== null");
+ });
+
+ function resumeAndTestBreakpoint(line) {
+ let finished = waitForCaretUpdated(gPanel, line).then(() => testBreakpoint(line));
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ gDebugger.document.getElementById("resume"),
+ gDebugger);
+
+ return finished;
+ }
+
+ function resumeAndTestNoBreakpoint() {
+ let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.AFTER_FRAMES_CLEARED).then(() => {
+ is(gSources.itemCount, 1,
+ "Found the expected number of sources.");
+ is(gEditor.getText().indexOf("ermahgerd"), 253,
+ "The correct source was loaded initially.");
+ is(gSources.selectedValue, gSources.values[0],
+ "The correct source is selected.");
+
+ ok(gSources.selectedItem,
+ "There should be a selected source in the sources pane.");
+ ok(!gSources._selectedBreakpoint,
+ "There should be no selected breakpoint in the sources pane.");
+ is(gSources._conditionalPopupVisible, false,
+ "The breakpoint conditional expression popup should not be shown.");
+
+ is(gDebugger.document.querySelectorAll(".dbg-stackframe").length, 0,
+ "There should be no visible stackframes.");
+ is(gDebugger.document.querySelectorAll(".dbg-breakpoint").length, 13,
+ "There should be thirteen visible breakpoints.");
+ });
+
+ gDebugger.gThreadClient.resume();
+
+ return finished;
+ }
+
+ function testBreakpoint(line, highlightBreakpoint) {
+ // Highlight the breakpoint only if required.
+ if (highlightBreakpoint) {
+ let finished = waitForCaretUpdated(gPanel, line).then(() => testBreakpoint(line));
+ gSources.highlightBreakpoint({ actor: gSources.selectedValue, line: line });
+ return finished;
+ }
+
+ let selectedActor = gSources.selectedValue;
+ let selectedBreakpoint = gSources._selectedBreakpoint;
+ let selectedBreakpointItem = gSources._getBreakpoint(selectedBreakpoint);
+
+ ok(selectedActor,
+ "There should be a selected item in the sources pane.");
+ ok(selectedBreakpoint,
+ "There should be a selected breakpoint in the sources pane.");
+
+ let source = gSources.selectedItem.attachment.source;
+ let bp = queries.getBreakpoint(getState(), selectedBreakpoint.location);
+
+ ok(bp, "The selected breakpoint exists");
+ is(bp.location.actor, source.actor,
+ "The breakpoint on line " + line + " wasn't added on the correct source.");
+ is(bp.location.line, line,
+ "The breakpoint on line " + line + " wasn't found.");
+ is(!!bp.disabled, false,
+ "The breakpoint on line " + line + " should be enabled.");
+ is(!!selectedBreakpointItem.attachment.openPopup, false,
+ "The breakpoint on line " + line + " should not have opened a popup.");
+ is(gSources._conditionalPopupVisible, false,
+ "The breakpoint conditional expression popup should not have been shown.");
+ isnot(bp.condition, undefined,
+ "The breakpoint on line " + line + " should have a conditional expression.");
+ ok(isCaretPos(gPanel, line),
+ "The editor caret position is not properly set.");
+ }
+
+ const testAfterReload = Task.async(function* () {
+ let selectedActor = gSources.selectedValue;
+ let selectedBreakpoint = gSources._selectedBreakpoint;
+
+ ok(selectedActor,
+ "There should be a selected item in the sources pane after reload.");
+ ok(!selectedBreakpoint,
+ "There should be no selected breakpoint in the sources pane after reload.");
+
+ yield testBreakpoint(18, true);
+ yield testBreakpoint(19, true);
+ yield testBreakpoint(20, true);
+ yield testBreakpoint(21, true);
+ yield testBreakpoint(22, true);
+ yield testBreakpoint(23, true);
+ yield testBreakpoint(24, true);
+ yield testBreakpoint(25, true);
+ yield testBreakpoint(26, true);
+ yield testBreakpoint(27, true);
+ yield testBreakpoint(28, true);
+ yield testBreakpoint(29, true);
+ yield testBreakpoint(30, true);
+
+ is(gSources.itemCount, 1,
+ "Found the expected number of sources.");
+ is(gEditor.getText().indexOf("ermahgerd"), 253,
+ "The correct source was loaded again.");
+ is(gSources.selectedValue, gSources.values[0],
+ "The correct source is selected.");
+
+ ok(gSources.selectedItem,
+ "There should be a selected source in the sources pane.");
+ ok(gSources._selectedBreakpoint,
+ "There should be a selected breakpoint in the sources pane.");
+ is(gSources._conditionalPopupVisible, false,
+ "The breakpoint conditional expression popup should not be shown.");
+ });
+
+ Task.spawn(function* () {
+ let onCaretUpdated = waitForCaretAndScopes(gPanel, 17);
+ callInTab(gTab, "ermahgerd");
+ yield onCaretUpdated;
+
+ yield addBreakpoints();
+
+ is(gDebugger.gThreadClient.state, "paused",
+ "Should only be getting stack frames while paused.");
+ is(queries.getSourceCount(getState()), 1,
+ "Found the expected number of sources.");
+ is(gEditor.getText().indexOf("ermahgerd"), 253,
+ "The correct source was loaded initially.");
+ is(gSources.selectedValue, gSources.values[0],
+ "The correct source is selected.");
+ is(queries.getBreakpoints(getState()).length, 13,
+ "13 breakpoints currently added.");
+
+ yield resumeAndTestBreakpoint(20);
+ yield resumeAndTestBreakpoint(21);
+ yield resumeAndTestBreakpoint(22);
+ yield resumeAndTestBreakpoint(23);
+ yield resumeAndTestBreakpoint(24);
+ yield resumeAndTestBreakpoint(25);
+ yield resumeAndTestBreakpoint(27);
+ yield resumeAndTestBreakpoint(28);
+ yield resumeAndTestBreakpoint(29);
+ yield resumeAndTestBreakpoint(30);
+ yield resumeAndTestNoBreakpoint();
+
+ let sourceShown = waitForSourceShown(gPanel, ".html");
+ reload(gPanel),
+ yield sourceShown;
+
+ testAfterReload();
+
+ // Reset traits back to default value
+ client.mainRoot.traits.conditionalBreakpoints = true;
+ closeDebuggerAndFinish(gPanel);
+ });
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_conditional-breakpoints-02.js b/devtools/client/debugger/test/mochitest/browser_dbg_conditional-breakpoints-02.js
new file mode 100644
index 000000000..d3d857753
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_conditional-breakpoints-02.js
@@ -0,0 +1,219 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Bug 740825: Test the debugger conditional breakpoints.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_conditional-breakpoints.html";
+
+function test() {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ const gTab = aTab;
+ const gPanel = aPanel;
+ const gDebugger = gPanel.panelWin;
+ const gEditor = gDebugger.DebuggerView.editor;
+ const gSources = gDebugger.DebuggerView.Sources;
+ const queries = gDebugger.require("./content/queries");
+ const constants = gDebugger.require("./content/constants");
+ const actions = bindActionCreators(gPanel);
+ const getState = gDebugger.DebuggerController.getState;
+ const CONDITIONAL_POPUP_SHOWN = gDebugger.EVENTS.CONDITIONAL_BREAKPOINT_POPUP_SHOWN;
+
+ // This test forces conditional breakpoints to be evaluated on the
+ // client-side
+ var client = gPanel.target.client;
+ client.mainRoot.traits.conditionalBreakpoints = false;
+
+ function addBreakpoint1() {
+ return actions.addBreakpoint({ actor: gSources.selectedValue, line: 18 });
+ }
+
+ function addBreakpoint2() {
+ let finished = waitForDispatch(gPanel, constants.ADD_BREAKPOINT);
+ setCaretPosition(19);
+ gSources._onCmdAddBreakpoint();
+ return finished;
+ }
+
+ function modBreakpoint2() {
+ setCaretPosition(19);
+ let popupShown = waitForDebuggerEvents(gPanel, CONDITIONAL_POPUP_SHOWN);
+ gSources._onCmdAddConditionalBreakpoint();
+ return popupShown;
+ }
+
+ function* addBreakpoint3() {
+ let finished = waitForDispatch(gPanel, constants.ADD_BREAKPOINT);
+ let popupShown = waitForDebuggerEvents(gPanel, CONDITIONAL_POPUP_SHOWN);
+ setCaretPosition(20);
+ gSources._onCmdAddConditionalBreakpoint();
+ yield finished;
+ yield popupShown;
+ }
+
+ function* modBreakpoint3() {
+ setCaretPosition(20);
+
+ let popupShown = waitForDebuggerEvents(gPanel, CONDITIONAL_POPUP_SHOWN);
+ gSources._onCmdAddConditionalBreakpoint();
+ yield popupShown;
+
+ typeText(gSources._cbTextbox, "bamboocha");
+
+ let finished = waitForDispatch(gPanel, constants.SET_BREAKPOINT_CONDITION);
+ EventUtils.sendKey("RETURN", gDebugger);
+ yield finished;
+ }
+
+ function addBreakpoint4() {
+ let finished = waitForDispatch(gPanel, constants.ADD_BREAKPOINT);
+ setCaretPosition(21);
+ gSources._onCmdAddBreakpoint();
+ return finished;
+ }
+
+ function delBreakpoint4() {
+ let finished = waitForDispatch(gPanel, constants.REMOVE_BREAKPOINT);
+ setCaretPosition(21);
+ gSources._onCmdAddBreakpoint();
+ return finished;
+ }
+
+ function testBreakpoint(aLine, aPopupVisible, aConditionalExpression) {
+ const source = queries.getSelectedSource(getState());
+ ok(source,
+ "There should be a selected item in the sources pane.");
+
+ const bp = queries.getBreakpoint(getState(), {
+ actor: source.actor,
+ line: aLine
+ });
+ const bpItem = gSources._getBreakpoint(bp);
+ ok(bp, "There should be a breakpoint.");
+ ok(bpItem, "There should be a breakpoint in the sources pane.");
+
+ is(bp.location.actor, source.actor,
+ "The breakpoint on line " + aLine + " wasn't added on the correct source.");
+ is(bp.location.line, aLine,
+ "The breakpoint on line " + aLine + " wasn't found.");
+ is(!!bp.disabled, false,
+ "The breakpoint on line " + aLine + " should be enabled.");
+ is(gSources._conditionalPopupVisible, aPopupVisible,
+ "The breakpoint on line " + aLine + " should have a correct popup state (2).");
+ is(bp.condition, aConditionalExpression,
+ "The breakpoint on line " + aLine + " should have a correct conditional expression.");
+ }
+
+ function testNoBreakpoint(aLine) {
+ let selectedActor = gSources.selectedValue;
+ let selectedBreakpoint = gSources._selectedBreakpoint;
+
+ ok(selectedActor,
+ "There should be a selected item in the sources pane for line " + aLine + ".");
+ ok(!selectedBreakpoint,
+ "There should be no selected brekapoint in the sources pane for line " + aLine + ".");
+
+ ok(isCaretPos(gPanel, aLine),
+ "The editor caret position is not properly set.");
+ }
+
+ function setCaretPosition(aLine) {
+ gEditor.setCursor({ line: aLine - 1, ch: 0 });
+ }
+
+ function clickOnBreakpoint(aIndex) {
+ EventUtils.sendMouseEvent({ type: "click" },
+ gDebugger.document.querySelectorAll(".dbg-breakpoint")[aIndex],
+ gDebugger);
+ }
+
+ function waitForConditionUpdate() {
+ // This will close the popup and send another request to update
+ // the condition
+ gSources._hideConditionalPopup();
+ return waitForDispatch(gPanel, constants.SET_BREAKPOINT_CONDITION);
+ }
+
+ Task.spawn(function* () {
+ let onCaretUpdated = waitForCaretAndScopes(gPanel, 17);
+ callInTab(gTab, "ermahgerd");
+ yield onCaretUpdated;
+
+ is(gDebugger.gThreadClient.state, "paused",
+ "Should only be getting stack frames while paused.");
+ is(queries.getSourceCount(getState()), 1,
+ "Found the expected number of sources.");
+ is(gEditor.getText().indexOf("ermahgerd"), 253,
+ "The correct source was loaded initially.");
+ is(gSources.selectedValue, gSources.values[0],
+ "The correct source is selected.");
+
+ is(queries.getBreakpoints(getState()).length, 0,
+ "No breakpoints currently added.");
+
+ yield addBreakpoint1();
+ testBreakpoint(18, false, undefined);
+
+ yield addBreakpoint2();
+ testBreakpoint(19, false, undefined);
+ yield modBreakpoint2();
+ testBreakpoint(19, true, undefined);
+ yield waitForConditionUpdate();
+ yield addBreakpoint3();
+ testBreakpoint(20, true, "");
+ yield waitForConditionUpdate();
+ yield modBreakpoint3();
+ testBreakpoint(20, false, "bamboocha");
+ yield addBreakpoint4();
+ testBreakpoint(21, false, undefined);
+ yield delBreakpoint4();
+
+ setCaretPosition(18);
+ is(gSources._selectedBreakpoint.location.line, 18,
+ "The selected breakpoint is line 18");
+ yield testBreakpoint(18, false, undefined);
+
+ setCaretPosition(19);
+ is(gSources._selectedBreakpoint.location.line, 19,
+ "The selected breakpoint is line 19");
+ yield testBreakpoint(19, false, "");
+
+ setCaretPosition(20);
+ is(gSources._selectedBreakpoint.location.line, 20,
+ "The selected breakpoint is line 20");
+ yield testBreakpoint(20, false, "bamboocha");
+
+ setCaretPosition(17);
+ yield testNoBreakpoint(17);
+
+ setCaretPosition(21);
+ yield testNoBreakpoint(21);
+
+ clickOnBreakpoint(0);
+ is(gSources._selectedBreakpoint.location.line, 18,
+ "The selected breakpoint is line 18");
+ yield testBreakpoint(18, false, undefined);
+
+ clickOnBreakpoint(1);
+ is(gSources._selectedBreakpoint.location.line, 19,
+ "The selected breakpoint is line 19");
+ yield testBreakpoint(19, false, "");
+
+ clickOnBreakpoint(2);
+ is(gSources._selectedBreakpoint.location.line, 20,
+ "The selected breakpoint is line 20");
+ testBreakpoint(20, true, "bamboocha");
+
+ // Reset traits back to default value
+ client.mainRoot.traits.conditionalBreakpoints = true;
+ resumeDebuggerThenCloseAndFinish(gPanel);
+ });
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_conditional-breakpoints-03.js b/devtools/client/debugger/test/mochitest/browser_dbg_conditional-breakpoints-03.js
new file mode 100644
index 000000000..fbd9c6ae8
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_conditional-breakpoints-03.js
@@ -0,0 +1,78 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests that conditional breakpoint expressions survive disabled breakpoints.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_conditional-breakpoints.html";
+
+function test() {
+ initDebugger().then(([aTab,, aPanel]) => {
+ const gTab = aTab;
+ const gPanel = aPanel;
+ const gDebugger = gPanel.panelWin;
+ const gSources = gDebugger.DebuggerView.Sources;
+ const queries = gDebugger.require("./content/queries");
+ const constants = gDebugger.require("./content/constants");
+ const actions = bindActionCreators(gPanel);
+ const getState = gDebugger.DebuggerController.getState;
+
+ // This test forces conditional breakpoints to be evaluated on the
+ // client-side
+ var client = gPanel.target.client;
+ client.mainRoot.traits.conditionalBreakpoints = false;
+
+ function waitForConditionUpdate() {
+ // This will close the popup and send another request to update
+ // the condition
+ gSources._hideConditionalPopup();
+ return waitForDispatch(gPanel, constants.SET_BREAKPOINT_CONDITION);
+ }
+
+ Task.spawn(function* () {
+ let onCaretUpdated = waitForCaretUpdated(gPanel, 17);
+ yield navigateActiveTabTo(gPanel,
+ TAB_URL,
+ gDebugger.EVENTS.SOURCE_SHOWN);
+ callInTab(gTab, "ermahgerd");
+ yield onCaretUpdated;
+
+ const location = { actor: gSources.selectedValue, line: 18 };
+
+ yield actions.addBreakpoint(location, "hello");
+ yield actions.disableBreakpoint(location);
+ yield actions.addBreakpoint(location);
+
+ const bp = queries.getBreakpoint(getState(), location);
+ is(bp.condition, "hello", "The conditional expression is correct.");
+
+ let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.CONDITIONAL_BREAKPOINT_POPUP_SHOWN);
+ EventUtils.sendMouseEvent({ type: "click" },
+ gDebugger.document.querySelector(".dbg-breakpoint"),
+ gDebugger);
+ yield finished;
+
+ const textbox = gDebugger.document.getElementById("conditional-breakpoint-panel-textbox");
+ is(textbox.value, "hello", "The expression is correct (2).");
+
+ yield waitForConditionUpdate();
+ yield actions.disableBreakpoint(location);
+ yield actions.setBreakpointCondition(location, "foo");
+ yield actions.addBreakpoint(location);
+
+ finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.CONDITIONAL_BREAKPOINT_POPUP_SHOWN);
+ EventUtils.sendMouseEvent({ type: "click" },
+ gDebugger.document.querySelector(".dbg-breakpoint"),
+ gDebugger);
+ yield finished;
+ is(textbox.value, "foo", "The expression is correct (3).");
+
+ // Reset traits back to default value
+ client.mainRoot.traits.conditionalBreakpoints = true;
+ resumeDebuggerThenCloseAndFinish(gPanel);
+ });
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_conditional-breakpoints-04.js b/devtools/client/debugger/test/mochitest/browser_dbg_conditional-breakpoints-04.js
new file mode 100644
index 000000000..55b217405
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_conditional-breakpoints-04.js
@@ -0,0 +1,52 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure that conditional breakpoints with blank expressions
+ * maintain their conditions after enabling them.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_conditional-breakpoints.html";
+
+function test() {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ const gTab = aTab;
+ const gPanel = aPanel;
+ const gDebugger = gPanel.panelWin;
+ const gSources = gDebugger.DebuggerView.Sources;
+ const queries = gDebugger.require("./content/queries");
+ const constants = gDebugger.require("./content/constants");
+ const actions = bindActionCreators(gPanel);
+ const getState = gDebugger.DebuggerController.getState;
+
+ // This test forces conditional breakpoints to be evaluated on the
+ // client-side
+ var client = gPanel.target.client;
+ client.mainRoot.traits.conditionalBreakpoints = false;
+
+ Task.spawn(function* () {
+ let onCaretUpdated = waitForCaretAndScopes(gPanel, 17);
+ callInTab(gTab, "ermahgerd");
+ yield onCaretUpdated;
+
+ const location = { actor: gSources.selectedValue, line: 18 };
+
+ yield actions.addBreakpoint(location, "");
+ yield actions.disableBreakpoint(location);
+ yield actions.addBreakpoint(location);
+
+ const bp = queries.getBreakpoint(getState(), location);
+ is(bp.condition, "", "The conditional expression is correct.");
+
+ // Reset traits back to default value
+ client.mainRoot.traits.conditionalBreakpoints = true;
+ resumeDebuggerThenCloseAndFinish(gPanel);
+ });
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_conditional-breakpoints-05.js b/devtools/client/debugger/test/mochitest/browser_dbg_conditional-breakpoints-05.js
new file mode 100644
index 000000000..f143ef535
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_conditional-breakpoints-05.js
@@ -0,0 +1,141 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure that conditional breakpoints with an exception-throwing expression
+ * could pause on hit
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_conditional-breakpoints.html";
+
+function test() {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ const gTab = aTab;
+ const gPanel = aPanel;
+ const gDebugger = gPanel.panelWin;
+ const gEditor = gDebugger.DebuggerView.editor;
+ const gSources = gDebugger.DebuggerView.Sources;
+ const queries = gDebugger.require("./content/queries");
+ const constants = gDebugger.require("./content/constants");
+ const actions = bindActionCreators(gPanel);
+ const getState = gDebugger.DebuggerController.getState;
+
+ // This test forces conditional breakpoints to be evaluated on the
+ // client-side
+ var client = gPanel.target.client;
+ client.mainRoot.traits.conditionalBreakpoints = false;
+
+ function resumeAndTestBreakpoint(line) {
+ let finished = waitForCaretUpdated(gPanel, line).then(() => testBreakpoint(line));
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ gDebugger.document.getElementById("resume"),
+ gDebugger);
+
+ return finished;
+ }
+
+ function resumeAndTestNoBreakpoint() {
+ let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.AFTER_FRAMES_CLEARED).then(() => {
+ is(gSources.itemCount, 1,
+ "Found the expected number of sources.");
+ is(gEditor.getText().indexOf("ermahgerd"), 253,
+ "The correct source was loaded initially.");
+ is(gSources.selectedValue, gSources.values[0],
+ "The correct source is selected.");
+
+ ok(gSources.selectedItem,
+ "There should be a selected source in the sources pane.");
+ ok(!gSources._selectedBreakpoint,
+ "There should be no selected breakpoint in the sources pane.");
+ is(gSources._conditionalPopupVisible, false,
+ "The breakpoint conditional expression popup should not be shown.");
+
+ is(gDebugger.document.querySelectorAll(".dbg-stackframe").length, 0,
+ "There should be no visible stackframes.");
+ is(gDebugger.document.querySelectorAll(".dbg-breakpoint").length, 6,
+ "There should be thirteen visible breakpoints.");
+ });
+
+ gDebugger.gThreadClient.resume();
+
+ return finished;
+ }
+
+ function testBreakpoint(line, highlightBreakpoint) {
+ // Highlight the breakpoint only if required.
+ if (highlightBreakpoint) {
+ let finished = waitForCaretUpdated(gPanel, line).then(() => testBreakpoint(line));
+ gSources.highlightBreakpoint({ actor: gSources.selectedValue, line: line });
+ return finished;
+ }
+
+ let selectedActor = gSources.selectedValue;
+ let selectedBreakpoint = gSources._selectedBreakpoint;
+ let selectedBreakpointItem = gSources._getBreakpoint(selectedBreakpoint);
+ let source = queries.getSource(getState(), selectedActor);
+
+ ok(selectedActor,
+ "There should be a selected item in the sources pane.");
+ ok(selectedBreakpoint,
+ "There should be a selected breakpoint.");
+ ok(selectedBreakpointItem,
+ "There should be a selected breakpoint item in the sources pane.");
+
+ is(selectedBreakpoint.location.actor, source.actor,
+ "The breakpoint on line " + line + " wasn't added on the correct source.");
+ is(selectedBreakpoint.location.line, line,
+ "The breakpoint on line " + line + " wasn't found.");
+ is(!!selectedBreakpoint.location.disabled, false,
+ "The breakpoint on line " + line + " should be enabled.");
+ is(gSources._conditionalPopupVisible, false,
+ "The breakpoint conditional expression popup should not have been shown.");
+
+ isnot(selectedBreakpoint.condition, undefined,
+ "The breakpoint on line " + line + " should have a conditional expression.");
+
+ ok(isCaretPos(gPanel, line),
+ "The editor caret position is not properly set.");
+ }
+
+ Task.spawn(function* () {
+ let onCaretUpdated = waitForCaretAndScopes(gPanel, 17);
+ callInTab(gTab, "ermahgerd");
+ yield onCaretUpdated;
+
+ yield actions.addBreakpoint(
+ { actor: gSources.selectedValue, line: 18 }, " 1a"
+ );
+ yield actions.addBreakpoint(
+ { actor: gSources.selectedValue, line: 19 }, "new Error()"
+ );
+ yield actions.addBreakpoint(
+ { actor: gSources.selectedValue, line: 20 }, "true"
+ );
+ yield actions.addBreakpoint(
+ { actor: gSources.selectedValue, line: 21 }, "false"
+ );
+ yield actions.addBreakpoint(
+ { actor: gSources.selectedValue, line: 22 }, "0"
+ );
+ yield actions.addBreakpoint(
+ { actor: gSources.selectedValue, line: 23 }, "randomVar"
+ );
+
+ yield resumeAndTestBreakpoint(18);
+ yield resumeAndTestBreakpoint(19);
+ yield resumeAndTestBreakpoint(20);
+ yield resumeAndTestBreakpoint(23);
+ yield resumeAndTestNoBreakpoint();
+
+ // Reset traits back to default value
+ client.mainRoot.traits.conditionalBreakpoints = true;
+ closeDebuggerAndFinish(gPanel);
+ });
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_console-eval.js b/devtools/client/debugger/test/mochitest/browser_dbg_console-eval.js
new file mode 100644
index 000000000..37e0be1b1
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_console-eval.js
@@ -0,0 +1,41 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Breaking in the middle of a script evaluated by the console should
+ * work
+ */
+
+function test() {
+ Task.spawn(function* () {
+ let TAB_URL = EXAMPLE_URL + "doc_empty-tab-01.html";
+ let [,, panel] = yield initDebugger(TAB_URL, { source: null });
+ let dbgWin = panel.panelWin;
+ let sources = dbgWin.DebuggerView.Sources;
+ let frames = dbgWin.DebuggerView.StackFrames;
+ let editor = dbgWin.DebuggerView.editor;
+ let toolbox = gDevTools.getToolbox(panel.target);
+
+ let paused = promise.all([
+ waitForEditorEvents(panel, "cursorActivity"),
+ waitForDebuggerEvents(panel, dbgWin.EVENTS.SOURCE_SHOWN)
+ ]);
+
+ toolbox.once("webconsole-ready", () => {
+ ok(toolbox.splitConsole, "Split console is shown.");
+ let jsterm = toolbox.getPanel("webconsole").hud.jsterm;
+ jsterm.execute("debugger");
+ });
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, dbgWin);
+
+ yield paused;
+ is(sources.selectedItem.attachment.label, "SCRIPT0",
+ "Anonymous source is selected in sources");
+ ok(editor.getText() === "debugger", "Editor has correct text");
+
+ yield toolbox.closeSplitConsole();
+ yield resumeDebuggerThenCloseAndFinish(panel);
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_console-named-eval.js b/devtools/client/debugger/test/mochitest/browser_dbg_console-named-eval.js
new file mode 100644
index 000000000..a6c3c96bb
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_console-named-eval.js
@@ -0,0 +1,42 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Breaking in the middle of a named eval script created by the
+ * console should work
+ */
+
+function test() {
+ Task.spawn(runTests);
+}
+
+function* runTests() {
+ let TAB_URL = EXAMPLE_URL + "doc_empty-tab-01.html";
+ let [,, panel] = yield initDebugger(TAB_URL, { source: null });
+ let dbgWin = panel.panelWin;
+ let sources = dbgWin.DebuggerView.Sources;
+ let frames = dbgWin.DebuggerView.StackFrames;
+ let editor = dbgWin.DebuggerView.editor;
+ let toolbox = gDevTools.getToolbox(panel.target);
+
+ let paused = waitForSourceAndCaretAndScopes(panel, "foo.js", 1);
+
+ toolbox.once("webconsole-ready", () => {
+ ok(toolbox.splitConsole, "Split console is shown.");
+ let jsterm = toolbox.getPanel("webconsole").hud.jsterm;
+ jsterm.execute("eval('debugger; //# sourceURL=foo.js')");
+ });
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, dbgWin);
+
+ yield paused;
+ is(sources.selectedItem.attachment.label, "foo.js",
+ "New source is selected in sources");
+ is(sources.selectedItem.attachment.group, "http://example.com",
+ "New source is in the right group");
+ ok(editor.getText() === "debugger; //# sourceURL=foo.js", "Editor has correct text");
+
+ yield toolbox.closeSplitConsole();
+ yield resumeDebuggerThenCloseAndFinish(panel);
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_controller-evaluate-01.js b/devtools/client/debugger/test/mochitest/browser_dbg_controller-evaluate-01.js
new file mode 100644
index 000000000..9db259b98
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_controller-evaluate-01.js
@@ -0,0 +1,106 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests the public evaluation API from the debugger controller.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
+
+function test() {
+ Task.spawn(function* () {
+ const options = {
+ source: EXAMPLE_URL + "code_script-switching-01.js",
+ line: 1
+ };
+ const [tab,, panel] = yield initDebugger(TAB_URL, options);
+ const win = panel.panelWin;
+ const frames = win.DebuggerController.StackFrames;
+ const framesView = win.DebuggerView.StackFrames;
+ const sourcesView = win.DebuggerView.Sources;
+ const editorView = win.DebuggerView.editor;
+ const events = win.EVENTS;
+ const queries = win.require("./content/queries");
+ const constants = win.require("./content/constants");
+ const actions = bindActionCreators(panel);
+ const getState = win.DebuggerController.getState;
+
+ function checkView(frameDepth, selectedSource, caretLine, editorText) {
+ is(win.gThreadClient.state, "paused",
+ "Should only be getting stack frames while paused.");
+ is(framesView.itemCount, 2,
+ "Should have four frames.");
+ is(framesView.selectedDepth, frameDepth,
+ "The correct frame is selected in the widget.");
+ is(sourcesView.selectedIndex, selectedSource,
+ "The correct source is selected in the widget.");
+ ok(isCaretPos(panel, caretLine),
+ "Editor caret location is correct.");
+ is(editorView.getText().search(editorText[0]), editorText[1],
+ "The correct source is not displayed.");
+ }
+
+ // Cache the sources text to avoid having to wait for their
+ // retrieval.
+ const sources = queries.getSources(getState());
+ yield promise.all(Object.keys(sources).map(k => {
+ return actions.loadSourceText(sources[k]);
+ }));
+
+ is(Object.keys(getState().sources.sourcesText).length, 2,
+ "There should be two cached sources in the cache.");
+
+ // Eval while not paused.
+ try {
+ yield frames.evaluate("foo");
+ } catch (error) {
+ is(error.message, "No stack frame available.",
+ "Evaluating shouldn't work while the debuggee isn't paused.");
+ }
+
+ callInTab(tab, "firstCall");
+ yield waitForSourceAndCaretAndScopes(panel, "-02.js", 6);
+ checkView(0, 1, 6, [/secondCall/, 118]);
+
+ // Eval in the topmost frame, while paused.
+ let updatedView = waitForDebuggerEvents(panel, events.FETCHED_SCOPES);
+ let result = yield frames.evaluate("foo");
+ ok(!result.throw, "The evaluation hasn't thrown.");
+ is(result.return.type, "object", "The evaluation return type is correct.");
+ is(result.return.class, "Function", "The evaluation return class is correct.");
+
+ yield updatedView;
+ checkView(0, 1, 6, [/secondCall/, 118]);
+ ok(true, "Evaluating in the topmost frame works properly.");
+
+ // Eval in a different frame, while paused.
+ updatedView = waitForDebuggerEvents(panel, events.FETCHED_SCOPES);
+ try {
+ yield frames.evaluate("foo", { depth: 1 }); // oldest frame
+ } catch (result) {
+ is(result.return.type, "object", "The evaluation thrown type is correct.");
+ is(result.return.class, "Error", "The evaluation thrown class is correct.");
+ ok(!result.return, "The evaluation hasn't returned.");
+ }
+
+ yield updatedView;
+ checkView(0, 1, 6, [/secondCall/, 118]);
+ ok(true, "Evaluating in a custom frame works properly.");
+
+ // Eval in a non-existent frame, while paused.
+ waitForDebuggerEvents(panel, events.FETCHED_SCOPES).then(() => {
+ ok(false, "Shouldn't have updated the view when trying to evaluate " +
+ "an expression in a non-existent stack frame.");
+ });
+ try {
+ yield frames.evaluate("foo", { depth: 4 }); // non-existent frame
+ } catch (error) {
+ is(error.message, "No stack frame available.",
+ "Evaluating shouldn't work if the specified frame doesn't exist.");
+ }
+
+ yield resumeDebuggerThenCloseAndFinish(panel);
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_controller-evaluate-02.js b/devtools/client/debugger/test/mochitest/browser_dbg_controller-evaluate-02.js
new file mode 100644
index 000000000..ff4092e1d
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_controller-evaluate-02.js
@@ -0,0 +1,78 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests the public evaluation API from the debugger controller.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
+
+function test() {
+ Task.spawn(function* () {
+ const options = {
+ source: EXAMPLE_URL + "code_script-switching-01.js",
+ line: 1
+ };
+ const [tab,, panel] = yield initDebugger(TAB_URL, options);
+ const win = panel.panelWin;
+ const frames = win.DebuggerController.StackFrames;
+ const framesView = win.DebuggerView.StackFrames;
+ const sourcesView = win.DebuggerView.Sources;
+ const editorView = win.DebuggerView.editor;
+ const events = win.EVENTS;
+ const queries = win.require("./content/queries");
+ const constants = win.require("./content/constants");
+ const actions = bindActionCreators(panel);
+ const getState = win.DebuggerController.getState;
+
+ function checkView(selectedFrame, selectedSource, caretLine, editorText) {
+ is(win.gThreadClient.state, "paused",
+ "Should only be getting stack frames while paused.");
+ is(framesView.itemCount, 2,
+ "Should have four frames.");
+ is(framesView.selectedDepth, selectedFrame,
+ "The correct frame is selected in the widget.");
+ is(sourcesView.selectedIndex, selectedSource,
+ "The correct source is selected in the widget.");
+ ok(isCaretPos(panel, caretLine),
+ "Editor caret location is correct.");
+ is(editorView.getText().search(editorText[0]), editorText[1],
+ "The correct source is not displayed.");
+ }
+
+ // Cache the sources text to avoid having to wait for their
+ // retrieval.
+ const sources = queries.getSources(getState());
+ yield promise.all(Object.keys(sources).map(k => {
+ return actions.loadSourceText(sources[k]);
+ }));
+
+ // Allow this generator function to yield first.
+ callInTab(tab, "firstCall");
+ yield waitForSourceAndCaretAndScopes(panel, "-02.js", 6);
+ checkView(0, 1, 6, [/secondCall/, 118]);
+
+ // Change the selected frame and eval inside it.
+ let updatedFrame = waitForDebuggerEvents(panel, events.FETCHED_SCOPES);
+ framesView.selectedDepth = 1; // oldest frame
+ yield updatedFrame;
+ checkView(1, 0, 5, [/firstCall/, 118]);
+
+ let updatedView = waitForDebuggerEvents(panel, events.FETCHED_SCOPES);
+ try {
+ yield frames.evaluate("foo");
+ } catch (result) {
+ is(result.return.type, "object", "The evaluation thrown type is correct.");
+ is(result.return.class, "Error", "The evaluation thrown class is correct.");
+ ok(!result.return, "The evaluation hasn't returned.");
+ }
+
+ yield updatedView;
+ checkView(1, 0, 5, [/firstCall/, 118]);
+ ok(true, "Evaluating while in a user-selected frame works properly.");
+
+ yield resumeDebuggerThenCloseAndFinish(panel);
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_debugger-statement.js b/devtools/client/debugger/test/mochitest/browser_dbg_debugger-statement.js
new file mode 100644
index 000000000..7378123f8
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_debugger-statement.js
@@ -0,0 +1,87 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests the behavior of the debugger statement.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_inline-debugger-statement.html";
+
+var gClient;
+var gTab;
+
+function test() {
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+
+ let transport = DebuggerServer.connectPipe();
+ gClient = new DebuggerClient(transport);
+ gClient.connect().then(([aType, aTraits]) => {
+ is(aType, "browser",
+ "Root actor should identify itself as a browser.");
+
+ addTab(TAB_URL)
+ .then((aTab) => {
+ gTab = aTab;
+ return attachTabActorForUrl(gClient, TAB_URL);
+ })
+ .then(testEarlyDebuggerStatement)
+ .then(testDebuggerStatement)
+ .then(() => gClient.close())
+ .then(finish)
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+ });
+}
+
+function testEarlyDebuggerStatement([aGrip, aResponse]) {
+ let deferred = promise.defer();
+
+ let onPaused = function (aEvent, aPacket) {
+ ok(false, "Pause shouldn't be called before we've attached!");
+ deferred.reject();
+ };
+
+ gClient.addListener("paused", onPaused);
+
+ // This should continue without nesting an event loop and calling
+ // the onPaused hook, because we haven't attached yet.
+ callInTab(gTab, "runDebuggerStatement");
+
+ gClient.removeListener("paused", onPaused);
+
+ // Now attach and resume...
+ gClient.request({ to: aResponse.threadActor, type: "attach" }, () => {
+ gClient.request({ to: aResponse.threadActor, type: "resume" }, () => {
+ ok(true, "Pause wasn't called before we've attached.");
+ deferred.resolve([aGrip, aResponse]);
+ });
+ });
+
+ return deferred.promise;
+}
+
+function testDebuggerStatement([aGrip, aResponse]) {
+ let deferred = promise.defer();
+
+ gClient.addListener("paused", (aEvent, aPacket) => {
+ gClient.request({ to: aResponse.threadActor, type: "resume" }, () => {
+ ok(true, "The pause handler was triggered on a debugger statement.");
+ deferred.resolve();
+ });
+ });
+
+ // Reach around the debugging protocol and execute the debugger statement.
+ callInTab(gTab, "runDebuggerStatement");
+
+ return deferred.promise;
+}
+
+registerCleanupFunction(function () {
+ gClient = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_editor-contextmenu.js b/devtools/client/debugger/test/mochitest/browser_dbg_editor-contextmenu.js
new file mode 100644
index 000000000..b42e3e123
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_editor-contextmenu.js
@@ -0,0 +1,68 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Bug 731394: Test the debugger source editor default context menu.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
+
+function test() {
+ let gTab, gPanel, gDebugger;
+ let gEditor, gSources, gContextMenu;
+
+ let options = {
+ source: EXAMPLE_URL + "code_script-switching-01.js",
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gEditor = gDebugger.DebuggerView.editor;
+ gSources = gDebugger.DebuggerView.Sources;
+ gContextMenu = gDebugger.document.getElementById("sourceEditorContextMenu");
+
+ waitForSourceAndCaretAndScopes(gPanel, "-02.js", 1).then(performTest).then(null, info);
+ callInTab(gTab, "firstCall");
+ });
+
+ function performTest() {
+ is(gDebugger.gThreadClient.state, "paused",
+ "Should only be getting stack frames while paused.");
+ is(gSources.itemCount, 2,
+ "Found the expected number of sources.");
+ is(gEditor.getText().indexOf("debugger"), 166,
+ "The correct source was loaded initially.");
+ is(gSources.selectedValue, gSources.values[1],
+ "The correct source is selected.");
+
+ is(gEditor.getText().indexOf("\u263a"), 162,
+ "Unicode characters are converted correctly.");
+
+ ok(gContextMenu,
+ "The source editor's context menupopup is available.");
+ ok(gEditor.getOption("readOnly"),
+ "The source editor is read only.");
+
+ gEditor.focus();
+ gEditor.setSelection({ line: 1, ch: 0 }, { line: 1, ch: 10 });
+
+ once(gContextMenu, "popupshown").then(testContextMenu).then(null, info);
+ gContextMenu.openPopup(gEditor.container, "overlap", 0, 0, true, false);
+ }
+
+ function testContextMenu() {
+ let document = gDebugger.document;
+
+ ok(document.getElementById("editMenuCommands"),
+ "#editMenuCommands found.");
+ ok(!document.getElementById("editMenuKeys"),
+ "#editMenuKeys not found.");
+
+ gContextMenu.hidePopup();
+ resumeDebuggerThenCloseAndFinish(gPanel);
+ }
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_editor-mode.js b/devtools/client/debugger/test/mochitest/browser_dbg_editor-mode.js
new file mode 100644
index 000000000..5982d9afd
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_editor-mode.js
@@ -0,0 +1,97 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure that updating the editor mode sets the right highlighting engine,
+ * and source URIs with extra query parameters also get the right engine.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_editor-mode.html";
+
+var gTab, gPanel, gDebugger;
+var gEditor, gSources;
+
+function test() {
+ let options = {
+ source: EXAMPLE_URL + "code_script-switching-01.js?a=b",
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gEditor = gDebugger.DebuggerView.editor;
+ gSources = gDebugger.DebuggerView.Sources;
+
+ waitForSourceAndCaretAndScopes(gPanel, "code_test-editor-mode", 1)
+ .then(testInitialSource)
+ .then(testSwitch1)
+ .then(testSwitch2)
+ .then(() => resumeDebuggerThenCloseAndFinish(gPanel))
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+
+ callInTab(gTab, "firstCall");
+ });
+}
+
+function testInitialSource() {
+ is(gSources.itemCount, 3,
+ "Found the expected number of sources.");
+
+ is(gEditor.getMode().name, "text",
+ "Found the expected editor mode.");
+ is(gEditor.getText().search(/firstCall/), -1,
+ "The first source is not displayed.");
+ is(gEditor.getText().search(/debugger/), 135,
+ "The second source is displayed.");
+ is(gEditor.getText().search(/banana/), -1,
+ "The third source is not displayed.");
+
+ let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.SOURCE_SHOWN);
+ gSources.selectedItem = e => e.attachment.label == "code_script-switching-01.js";
+ return finished;
+}
+
+function testSwitch1() {
+ is(gSources.itemCount, 3,
+ "Found the expected number of sources.");
+
+ is(gEditor.getMode().name, "javascript",
+ "Found the expected editor mode.");
+ is(gEditor.getText().search(/firstCall/), 118,
+ "The first source is displayed.");
+ is(gEditor.getText().search(/debugger/), -1,
+ "The second source is not displayed.");
+ is(gEditor.getText().search(/banana/), -1,
+ "The third source is not displayed.");
+
+ let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.SOURCE_SHOWN);
+ gSources.selectedItem = e => e.attachment.label == "doc_editor-mode.html";
+ return finished;
+}
+
+function testSwitch2() {
+ is(gSources.itemCount, 3,
+ "Found the expected number of sources.");
+
+ is(gEditor.getMode().name, "htmlmixed",
+ "Found the expected editor mode.");
+ is(gEditor.getText().search(/firstCall/), -1,
+ "The first source is not displayed.");
+ is(gEditor.getText().search(/debugger/), -1,
+ "The second source is not displayed.");
+ is(gEditor.getText().search(/banana/), 443,
+ "The third source is displayed.");
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gEditor = null;
+ gSources = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_event-listeners-01.js b/devtools/client/debugger/test/mochitest/browser_dbg_event-listeners-01.js
new file mode 100644
index 000000000..fa24e4507
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_event-listeners-01.js
@@ -0,0 +1,147 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests that the eventListeners request works.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_event-listeners-01.html";
+
+var gClient;
+var gTab;
+
+function test() {
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+
+ let transport = DebuggerServer.connectPipe();
+ gClient = new DebuggerClient(transport);
+ gClient.connect().then(([aType, aTraits]) => {
+ is(aType, "browser",
+ "Root actor should identify itself as a browser.");
+
+ addTab(TAB_URL)
+ .then((aTab) => {
+ gTab = aTab;
+ return attachThreadActorForUrl(gClient, TAB_URL);
+ })
+ .then(pauseDebuggee)
+ .then(testEventListeners)
+ .then(() => gClient.close())
+ .then(finish)
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+ });
+}
+
+function pauseDebuggee(aThreadClient) {
+ let deferred = promise.defer();
+
+ gClient.addOneTimeListener("paused", (aEvent, aPacket) => {
+ is(aPacket.type, "paused",
+ "We should now be paused.");
+ is(aPacket.why.type, "debuggerStatement",
+ "The debugger statement was hit.");
+
+ deferred.resolve(aThreadClient);
+ });
+
+ generateMouseClickInTab(gTab, "content.document.querySelector('button')");
+
+ return deferred.promise;
+}
+
+function testEventListeners(aThreadClient) {
+ let deferred = promise.defer();
+
+ aThreadClient.eventListeners(aPacket => {
+ if (aPacket.error) {
+ let msg = "Error getting event listeners: " + aPacket.message;
+ ok(false, msg);
+ deferred.reject(msg);
+ return;
+ }
+
+ is(aPacket.listeners.length, 3,
+ "Found all event listeners.");
+
+ promise.all(aPacket.listeners.map(listener => {
+ const lDeferred = promise.defer();
+ aThreadClient.pauseGrip(listener.function).getDefinitionSite(aResponse => {
+ if (aResponse.error) {
+ const msg = "Error getting function definition site: " + aResponse.message;
+ ok(false, msg);
+ lDeferred.reject(msg);
+ return;
+ }
+ listener.function.url = aResponse.source.url;
+ lDeferred.resolve(listener);
+ });
+ return lDeferred.promise;
+ })).then(listeners => {
+ let types = [];
+
+ for (let l of listeners) {
+ info("Listener for the " + l.type + " event.");
+ let node = l.node;
+ ok(node, "There is a node property.");
+ ok(node.object, "There is a node object property.");
+ ok(node.selector == "window" ||
+ content.document.querySelectorAll(node.selector).length == 1,
+ "The node property is a unique CSS selector.");
+
+ let func = l.function;
+ ok(func, "There is a function property.");
+ is(func.type, "object", "The function form is of type 'object'.");
+ is(func.class, "Function", "The function form is of class 'Function'.");
+
+ // The onchange handler is an inline string that doesn't have
+ // a URL because it's basically eval'ed
+ if (l.type !== "change") {
+ is(func.url, TAB_URL, "The function url is correct.");
+ }
+
+ is(l.allowsUntrusted, true,
+ "'allowsUntrusted' property has the right value.");
+ is(l.inSystemEventGroup, false,
+ "'inSystemEventGroup' property has the right value.");
+
+ types.push(l.type);
+
+ if (l.type == "keyup") {
+ is(l.capturing, true,
+ "Capturing property has the right value.");
+ is(l.isEventHandler, false,
+ "'isEventHandler' property has the right value.");
+ } else if (l.type == "load") {
+ is(l.capturing, false,
+ "Capturing property has the right value.");
+ is(l.isEventHandler, false,
+ "'isEventHandler' property has the right value.");
+ } else {
+ is(l.capturing, false,
+ "Capturing property has the right value.");
+ is(l.isEventHandler, true,
+ "'isEventHandler' property has the right value.");
+ }
+ }
+
+ ok(types.indexOf("click") != -1, "Found the click handler.");
+ ok(types.indexOf("change") != -1, "Found the change handler.");
+ ok(types.indexOf("keyup") != -1, "Found the keyup handler.");
+
+ aThreadClient.resume(deferred.resolve);
+ });
+ });
+
+ return deferred.promise;
+}
+
+registerCleanupFunction(function () {
+ gClient = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_event-listeners-02.js b/devtools/client/debugger/test/mochitest/browser_dbg_event-listeners-02.js
new file mode 100644
index 000000000..d7b13e4c5
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_event-listeners-02.js
@@ -0,0 +1,123 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests that the eventListeners request works when bound functions are used as
+ * event listeners.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_event-listeners-03.html";
+
+var gClient;
+var gTab;
+
+function test() {
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+
+ let transport = DebuggerServer.connectPipe();
+ gClient = new DebuggerClient(transport);
+ gClient.connect().then(([aType, aTraits]) => {
+ is(aType, "browser",
+ "Root actor should identify itself as a browser.");
+
+ addTab(TAB_URL)
+ .then((aTab) => {
+ gTab = aTab;
+ return attachThreadActorForUrl(gClient, TAB_URL);
+ })
+ .then(pauseDebuggee)
+ .then(testEventListeners)
+ .then(() => gClient.close())
+ .then(finish)
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+ });
+}
+
+function pauseDebuggee(aThreadClient) {
+ let deferred = promise.defer();
+
+ gClient.addOneTimeListener("paused", (aEvent, aPacket) => {
+ is(aPacket.type, "paused",
+ "We should now be paused.");
+ is(aPacket.why.type, "debuggerStatement",
+ "The debugger statement was hit.");
+
+ deferred.resolve(aThreadClient);
+ });
+
+ generateMouseClickInTab(gTab, "content.document.querySelector('button')");
+
+ return deferred.promise;
+}
+
+function testEventListeners(aThreadClient) {
+ let deferred = promise.defer();
+
+ aThreadClient.eventListeners(aPacket => {
+ if (aPacket.error) {
+ let msg = "Error getting event listeners: " + aPacket.message;
+ ok(false, msg);
+ deferred.reject(msg);
+ return;
+ }
+
+ is(aPacket.listeners.length, 3,
+ "Found all event listeners.");
+
+ promise.all(aPacket.listeners.map(listener => {
+ const lDeferred = promise.defer();
+ aThreadClient.pauseGrip(listener.function).getDefinitionSite(aResponse => {
+ if (aResponse.error) {
+ const msg = "Error getting function definition site: " + aResponse.message;
+ ok(false, msg);
+ lDeferred.reject(msg);
+ return;
+ }
+ listener.function.url = aResponse.source.url;
+ lDeferred.resolve(listener);
+ });
+ return lDeferred.promise;
+ })).then(listeners => {
+ is(listeners.length, 3, "Found three event listeners.");
+ for (let l of listeners) {
+ let node = l.node;
+ ok(node, "There is a node property.");
+ ok(node.object, "There is a node object property.");
+ ok(node.selector == "window" ||
+ content.document.querySelectorAll(node.selector).length == 1,
+ "The node property is a unique CSS selector.");
+
+ let func = l.function;
+ ok(func, "There is a function property.");
+ is(func.type, "object", "The function form is of type 'object'.");
+ is(func.class, "Function", "The function form is of class 'Function'.");
+ is(func.url, TAB_URL, "The function url is correct.");
+
+ is(l.type, "click", "This is a click event listener.");
+ is(l.allowsUntrusted, true,
+ "'allowsUntrusted' property has the right value.");
+ is(l.inSystemEventGroup, false,
+ "'inSystemEventGroup' property has the right value.");
+ is(l.isEventHandler, false,
+ "'isEventHandler' property has the right value.");
+ is(l.capturing, false,
+ "Capturing property has the right value.");
+ }
+
+ aThreadClient.resume(deferred.resolve);
+ });
+ });
+
+ return deferred.promise;
+}
+
+registerCleanupFunction(function () {
+ gClient = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_event-listeners-03.js b/devtools/client/debugger/test/mochitest/browser_dbg_event-listeners-03.js
new file mode 100644
index 000000000..8e193d8a6
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_event-listeners-03.js
@@ -0,0 +1,82 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests that the eventListeners request works when there are event handlers
+ * that the debugger cannot unwrap.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_native-event-handler.html";
+
+var gClient;
+var gTab;
+
+function test() {
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+
+ let transport = DebuggerServer.connectPipe();
+ gClient = new DebuggerClient(transport);
+ gClient.connect().then(([aType, aTraits]) => {
+ is(aType, "browser",
+ "Root actor should identify itself as a browser.");
+
+ addTab(TAB_URL)
+ .then((aTab) => {
+ gTab = aTab;
+ return attachThreadActorForUrl(gClient, TAB_URL);
+ })
+ .then(pauseDebuggee)
+ .then(testEventListeners)
+ .then(() => gClient.close())
+ .then(finish)
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+ });
+}
+
+function pauseDebuggee(aThreadClient) {
+ let deferred = promise.defer();
+
+ gClient.addOneTimeListener("paused", (aEvent, aPacket) => {
+ is(aPacket.type, "paused",
+ "We should now be paused.");
+ is(aPacket.why.type, "debuggerStatement",
+ "The debugger statement was hit.");
+
+ deferred.resolve(aThreadClient);
+ });
+
+ generateMouseClickInTab(gTab, "content.document.querySelector('button')");
+
+ return deferred.promise;
+}
+
+function testEventListeners(aThreadClient) {
+ let deferred = promise.defer();
+
+ aThreadClient.eventListeners(aPacket => {
+ if (aPacket.error) {
+ let msg = "Error getting event listeners: " + aPacket.message;
+ ok(false, msg);
+ deferred.reject(msg);
+ return;
+ }
+
+ // There are 3 event listeners in the page: button.onclick, window.onload
+ // and one more from the video element controls.
+ is(aPacket.listeners.length, 3, "Found all event listeners.");
+ aThreadClient.resume(deferred.resolve);
+ });
+
+ return deferred.promise;
+}
+
+registerCleanupFunction(function () {
+ gClient = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_event-listeners-04.js b/devtools/client/debugger/test/mochitest/browser_dbg_event-listeners-04.js
new file mode 100644
index 000000000..f1b0036b3
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_event-listeners-04.js
@@ -0,0 +1,55 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Test that event listeners are properly fetched even if one of the listeners
+ * don't have a Debugger.Source object (bug 942899).
+ *
+ * This test is skipped on debug and e10s builds for following reasons:
+ * - debug: requiring sdk/tabs causes memory leaks when new windows are opened
+ * in tests executed after this one. Bug 1142597.
+ * - e10s: tab.attach is not e10s safe and only works when add-on compatibility
+ * shims are in place. Bug 1146603.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_event-listeners-01.html";
+
+function test() {
+ Task.spawn(function* () {
+ let tab = yield addTab(TAB_URL);
+
+ // Create a sandboxed content script the Add-on SDK way. Inspired by bug
+ // 1145996.
+ let tabs = require("sdk/tabs");
+ let sdkTab = [...tabs].find(tab => tab.url === TAB_URL);
+ ok(sdkTab, "Add-on SDK found the loaded tab.");
+
+ info("Attaching an event handler via add-on sdk content scripts.");
+ let worker = sdkTab.attach({
+ contentScript: "document.body.addEventListener('click', e => alert(e))",
+ onError: ok.bind(this, false)
+ });
+
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ let [,, panel, win] = yield initDebugger(tab, options);
+ let dbg = panel.panelWin;
+ let controller = dbg.DebuggerController;
+ let constants = dbg.require("./content/constants");
+ let actions = dbg.require("./content/actions/event-listeners");
+ let fetched = waitForDispatch(panel, constants.FETCH_EVENT_LISTENERS);
+
+ info("Scheduling event listener fetch.");
+ controller.dispatch(actions.fetchEventListeners());
+
+ info("Waiting for updated event listeners to arrive.");
+ yield fetched;
+
+ ok(true, "The listener update did not hang.");
+ closeDebuggerAndFinish(panel);
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_file-reload.js b/devtools/client/debugger/test/mochitest/browser_dbg_file-reload.js
new file mode 100644
index 000000000..cb7ceef8f
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_file-reload.js
@@ -0,0 +1,72 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests that source contents are invalidated when the target navigates.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_random-javascript.html";
+const JS_URL = EXAMPLE_URL + "sjs_random-javascript.sjs";
+
+function test() {
+ let options = {
+ source: JS_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ const gPanel = aPanel;
+ const gDebugger = aPanel.panelWin;
+ const gEditor = gDebugger.DebuggerView.editor;
+ const gSources = gDebugger.DebuggerView.Sources;
+ const queries = gDebugger.require("./content/queries");
+ const constants = gDebugger.require("./content/constants");
+ const actions = bindActionCreators(gPanel);
+ const getState = gDebugger.DebuggerController.getState;
+
+ Task.spawn(function* () {
+ let source = queries.getSelectedSource(getState());
+
+ is(queries.getSourceCount(getState()), 1,
+ "There should be one source displayed in the view.");
+ is(source.url, JS_URL,
+ "The correct source is currently selected in the view.");
+ ok(gEditor.getText().includes("bacon"),
+ "The currently shown source contains bacon. Mmm, delicious!");
+
+ const { text: firstText } = yield queries.getSourceText(getState(), source.actor);
+ const firstNumber = parseFloat(firstText.match(/\d\.\d+/)[0]);
+
+ is(firstText, gEditor.getText(),
+ "gControllerSources.getText() returned the expected contents.");
+ ok(firstNumber <= 1 && firstNumber >= 0,
+ "The generated number seems to be created correctly.");
+
+ yield reloadActiveTab(aPanel, gDebugger.EVENTS.SOURCE_SHOWN);
+
+ is(queries.getSourceCount(getState()), 1,
+ "There should be one source displayed in the view.");
+ is(source.url, JS_URL,
+ "The correct source is currently selected in the view.");
+ ok(gEditor.getText().includes("bacon"),
+ "The newly shown source contains bacon. Mmm, delicious!");
+
+ source = queries.getSelectedSource(getState());
+ const { text: secondText } = yield queries.getSourceText(getState(), source.actor);
+ const secondNumber = parseFloat(secondText.match(/\d\.\d+/)[0]);
+
+ is(secondText, gEditor.getText(),
+ "gControllerSources.getText() returned the expected contents.");
+ ok(secondNumber <= 1 && secondNumber >= 0,
+ "The generated number seems to be created correctly.");
+
+ isnot(firstText, secondText,
+ "The displayed sources were different across reloads.");
+ isnot(firstNumber, secondNumber,
+ "The displayed sources differences were correct across reloads.");
+
+ yield closeDebuggerAndFinish(aPanel);
+ });
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_function-display-name.js b/devtools/client/debugger/test/mochitest/browser_dbg_function-display-name.js
new file mode 100644
index 000000000..fe9ff19eb
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_function-display-name.js
@@ -0,0 +1,68 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests that anonymous functions appear in the stack frame list with either
+ * their displayName property or a SpiderMonkey-inferred name.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_function-display-name.html";
+
+var gTab, gPanel, gDebugger;
+
+function test() {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+
+ testAnonCall();
+ });
+}
+
+function testAnonCall() {
+ let onCaretUpdated = waitForCaretUpdated(gPanel, 15);
+ let onScopes = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES);
+ callInTab(gTab, "evalCall");
+ promise.all([onCaretUpdated, onScopes]).then(() => {
+ ok(isCaretPos(gPanel, 15),
+ "The source editor caret position was incorrect.");
+ is(gDebugger.gThreadClient.state, "paused",
+ "Should only be getting stack frames while paused.");
+ is(gDebugger.document.querySelectorAll(".dbg-stackframe").length, 3,
+ "Should have three frames.");
+ is(gDebugger.document.querySelector("#stackframe-0 .dbg-stackframe-title").getAttribute("value"),
+ "anonFunc", "Frame name should be 'anonFunc'.");
+
+ testInferredName();
+ });
+}
+
+function testInferredName() {
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES).then(() => {
+ ok(isCaretPos(gPanel, 15),
+ "The source editor caret position was incorrect.");
+ is(gDebugger.gThreadClient.state, "paused",
+ "Should only be getting stack frames while paused.");
+ is(gDebugger.document.querySelectorAll(".dbg-stackframe").length, 3,
+ "Should have three frames.");
+ is(gDebugger.document.querySelector("#stackframe-0 .dbg-stackframe-title").getAttribute("value"),
+ "a/<", "Frame name should be 'a/<'.");
+
+ resumeDebuggerThenCloseAndFinish(gPanel);
+ });
+
+ gDebugger.gThreadClient.resume();
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_global-method-override.js b/devtools/client/debugger/test/mochitest/browser_dbg_global-method-override.js
new file mode 100644
index 000000000..ef2018b64
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_global-method-override.js
@@ -0,0 +1,26 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests that scripts that override properties of the global object, like
+ * toString don't break the debugger. The test page used to cause the debugger
+ * to throw when trying to attach to the thread actor.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_global-method-override.html";
+
+function test() {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ let gDebugger = aPanel.panelWin;
+ ok(gDebugger, "Should have a debugger available.");
+ is(gDebugger.gThreadClient.state, "attached", "Debugger should be attached.");
+
+ closeDebuggerAndFinish(aPanel);
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_globalactor.js b/devtools/client/debugger/test/mochitest/browser_dbg_globalactor.js
new file mode 100644
index 000000000..3f1533a1f
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_globalactor.js
@@ -0,0 +1,61 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Check extension-added global actor API.
+ */
+
+const ACTORS_URL = CHROME_URL + "testactors.js";
+
+function test() {
+ let gClient;
+
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+
+ DebuggerServer.addActors(ACTORS_URL);
+
+ let transport = DebuggerServer.connectPipe();
+ gClient = new DebuggerClient(transport);
+ gClient.connect().then(([aType, aTraits]) => {
+ is(aType, "browser",
+ "Root actor should identify itself as a browser.");
+
+ gClient.listTabs(aResponse => {
+ let globalActor = aResponse.testGlobalActor1;
+ ok(globalActor, "Found the test tab actor.");
+ ok(globalActor.includes("test_one"),
+ "testGlobalActor1's actorPrefix should be used.");
+
+ gClient.request({ to: globalActor, type: "ping" }, aResponse => {
+ is(aResponse.pong, "pong", "Actor should respond to requests.");
+
+ // Send another ping to see if the same actor is used.
+ gClient.request({ to: globalActor, type: "ping" }, aResponse => {
+ is(aResponse.pong, "pong", "Actor should respond to requests.");
+
+ // Make sure that lazily-created actors are created only once.
+ let count = 0;
+ for (let connID of Object.getOwnPropertyNames(DebuggerServer._connections)) {
+ let conn = DebuggerServer._connections[connID];
+ let actorPrefix = conn._prefix + "test_one";
+ for (let pool of conn._extraPools) {
+ count += Object.keys(pool._actors).filter(e => {
+ return e.startsWith(actorPrefix);
+ }).length;
+ }
+ }
+
+ is(count, 2,
+ "Only two actor exists in all pools. One tab actor and one global.");
+
+ gClient.close().then(finish);
+ });
+ });
+ });
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_hide-toolbar-buttons.js b/devtools/client/debugger/test/mochitest/browser_dbg_hide-toolbar-buttons.js
new file mode 100644
index 000000000..2fb9f9ddb
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_hide-toolbar-buttons.js
@@ -0,0 +1,34 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Bug 1093349: Test that the pretty-printing and blackboxing buttons
+ * are hidden if the server doesn't support them
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_auto-pretty-print-01.html";
+
+var { RootActor } = require("devtools/server/actors/root");
+
+function test() {
+ RootActor.prototype.traits.noBlackBoxing = true;
+ RootActor.prototype.traits.noPrettyPrinting = true;
+
+ let options = {
+ source: EXAMPLE_URL + "code_ugly-5.js",
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab, aDebuggee, aPanel]) => {
+ let document = aPanel.panelWin.document;
+ let ppButton = document.querySelector("#pretty-print");
+ let bbButton = document.querySelector("#black-box");
+ let sep = document.querySelector("#sources-toolbar .devtools-separator");
+
+ is(ppButton.style.display, "none", "The pretty-print button is hidden");
+ is(bbButton.style.display, "none", "The blackboxing button is hidden");
+ is(sep.style.display, "none", "The separator is hidden");
+ closeDebuggerAndFinish(aPanel);
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_host-layout.js b/devtools/client/debugger/test/mochitest/browser_dbg_host-layout.js
new file mode 100644
index 000000000..82a23cada
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_host-layout.js
@@ -0,0 +1,166 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * This if the debugger's layout is correctly modified when the toolbox's
+ * host changes.
+ */
+
+"use strict";
+
+var gDefaultHostType = Services.prefs.getCharPref("devtools.toolbox.host");
+
+function test() {
+ // test is too slow on some platforms due to the number of test cases
+ requestLongerTimeout(3);
+
+ Task.spawn(function*() {
+ yield testHosts(["bottom", "side", "window:big"], ["horizontal", "vertical", "horizontal"]);
+ yield testHosts(["side", "bottom", "side"], ["vertical", "horizontal", "vertical"]);
+ yield testHosts(["bottom", "side", "bottom"], ["horizontal", "vertical", "horizontal"]);
+ yield testHosts(["side", "window:big", "side"], ["vertical", "horizontal", "vertical"]);
+ yield testHosts(["window:big", "side", "window:big"], ["horizontal", "vertical", "horizontal"]);
+ yield testHosts(["window:small", "bottom", "side"], ["vertical", "horizontal", "vertical"]);
+ yield testHosts(["window:small", "window:big", "window:small"], ["vertical", "horizontal", "vertical"]);
+ finish();
+ });
+}
+
+function testHosts(aHostTypes, aLayoutTypes) {
+ let [firstHost, secondHost, thirdHost] = aHostTypes;
+ let [firstLayout, secondLayout, thirdLayout] = aLayoutTypes;
+
+ Services.prefs.setCharPref("devtools.toolbox.host", getHost(firstHost));
+
+ return Task.spawn(function*() {
+ let [tab, debuggee, panel] = yield initDebugger();
+ if (getHost(firstHost) === "window") {
+ yield resizeToolboxWindow(panel, firstHost);
+ }
+
+ yield testHost(panel, getHost(firstHost), firstLayout);
+ yield switchAndTestHost(tab, panel, secondHost, secondLayout);
+ yield switchAndTestHost(tab, panel, thirdHost, thirdLayout);
+ yield teardown(panel);
+ });
+}
+
+function switchAndTestHost(aTab, aPanel, aHostType, aLayoutType) {
+ let gToolbox = aPanel._toolbox;
+ let gDebugger = aPanel.panelWin;
+
+ return Task.spawn(function*() {
+ let layoutChanged = waitEventOnce(gDebugger, gDebugger.EVENTS.LAYOUT_CHANGED);
+ let hostChanged = gToolbox.switchHost(getHost(aHostType));
+
+ yield hostChanged;
+ info("The toolbox's host has changed.");
+
+ if (getHost(aHostType) === "window") {
+ yield resizeToolboxWindow(aPanel, aHostType);
+ }
+
+ yield layoutChanged;
+ info("The debugger's layout has changed.");
+
+ yield testHost(aPanel, getHost(aHostType), aLayoutType);
+ });
+}
+
+function waitEventOnce(aTarget, aEvent) {
+ let deferred = promise.defer();
+ aTarget.once(aEvent, deferred.resolve);
+ return deferred.promise;
+}
+
+function getHost(host) {
+ if (host.indexOf("window") == 0) {
+ return "window";
+ }
+ return host;
+}
+
+function resizeToolboxWindow(panel, host) {
+ let sizeOption = host.split(":")[1];
+ let win = panel._toolbox.win.parent;
+
+ // should be the same value as BREAKPOINT_SMALL_WINDOW_WIDTH in debugger-view.js
+ let breakpoint = 850;
+
+ if (sizeOption == "big" && win.outerWidth <= breakpoint) {
+ yield resizeAndWaitForLayoutChange(panel, breakpoint + 300);
+ } else if (sizeOption == "small" && win.outerWidth >= breakpoint) {
+ yield resizeAndWaitForLayoutChange(panel, breakpoint - 300);
+ } else {
+ info("Window resize unnecessary for host " + host);
+ }
+}
+
+function resizeAndWaitForLayoutChange(panel, width) {
+ info("Updating toolbox window width to " + width);
+
+ let win = panel._toolbox.win.parent;
+ let gDebugger = panel.panelWin;
+
+ win.resizeTo(width, window.screen.availHeight);
+ yield waitEventOnce(gDebugger, gDebugger.EVENTS.LAYOUT_CHANGED);
+}
+
+function testHost(aPanel, aHostType, aLayoutType) {
+ let gDebugger = aPanel.panelWin;
+ let gView = gDebugger.DebuggerView;
+
+ is(gView._hostType, aHostType,
+ "The default host type should've been set on the panel window (1).");
+ is(gDebugger.gHostType, aHostType,
+ "The default host type should've been set on the panel window (2).");
+
+ is(gView._body.getAttribute("layout"), aLayoutType,
+ "The default host type is present as an attribute on the panel's body.");
+
+ if (aLayoutType == "horizontal") {
+ is(gView._workersAndSourcesPane.parentNode.id, "debugger-widgets",
+ "The workers and sources pane's parent is correct for the horizontal layout.");
+ is(gView._instrumentsPane.parentNode.id, "editor-and-instruments-pane",
+ "The instruments pane's parent is correct for the horizontal layout.");
+ } else {
+ is(gView._workersAndSourcesPane.parentNode.id, "vertical-layout-panes-container",
+ "The workers and sources pane's parent is correct for the vertical layout.");
+ is(gView._instrumentsPane.parentNode.id, "vertical-layout-panes-container",
+ "The instruments pane's parent is correct for the vertical layout.");
+ }
+
+ let widgets = gDebugger.document.getElementById("debugger-widgets").childNodes;
+ let content = gDebugger.document.getElementById("debugger-content").childNodes;
+ let editorPane =
+ gDebugger.document.getElementById("editor-and-instruments-pane").childNodes;
+ let verticalPane =
+ gDebugger.document.getElementById("vertical-layout-panes-container").childNodes;
+
+ if (aLayoutType == "horizontal") {
+ is(widgets.length, 5, // 1 pane, 1 content box, 2 splitters and a phantom box.
+ "Found the correct number of debugger widgets.");
+ is(content.length, 1, // 1 pane
+ "Found the correct number of debugger content.");
+ is(editorPane.length, 3, // 2 panes, 1 splitter
+ "Found the correct number of debugger panes.");
+ is(verticalPane.length, 1, // 1 lonely splitter in the phantom box.
+ "Found the correct number of debugger panes.");
+ } else {
+ is(widgets.length, 4, // 1 content box, 2 splitters and a phantom box.
+ "Found the correct number of debugger widgets.");
+ is(content.length, 1, // 1 pane
+ "Found the correct number of debugger content.");
+ is(editorPane.length, 2, // 1 pane, 1 splitter
+ "Found the correct number of debugger panes.");
+ is(verticalPane.length, 3, // 2 panes and 1 splitter in the phantom box.
+ "Found the correct number of debugger panes.");
+ }
+}
+
+registerCleanupFunction(function() {
+ Services.prefs.setCharPref("devtools.toolbox.host", gDefaultHostType);
+ gDefaultHostType = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_iframes.js b/devtools/client/debugger/test/mochitest/browser_dbg_iframes.js
new file mode 100644
index 000000000..afc3f9682
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_iframes.js
@@ -0,0 +1,72 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests that iframes can be added as debuggees.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_iframes.html";
+
+function test() {
+ let gTab, gDebuggee, gPanel, gDebugger;
+ let gIframe, gEditor, gSources, gFrames;
+
+ let options = {
+ source: EXAMPLE_URL + "doc_inline-debugger-statement.html",
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab, aDebuggee, aPanel]) => {
+ gTab = aTab;
+ gDebuggee = aDebuggee;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gIframe = gDebuggee.frames[0];
+ gEditor = gDebugger.DebuggerView.editor;
+ gSources = gDebugger.DebuggerView.Sources;
+ gFrames = gDebugger.DebuggerView.StackFrames;
+
+ checkIframeSource();
+ checkIframePause()
+ .then(() => resumeDebuggerThenCloseAndFinish(gPanel))
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+ });
+
+ function checkIframeSource() {
+ is(gDebugger.gThreadClient.paused, false,
+ "Should be running after starting the test.");
+
+ ok(isCaretPos(gPanel, 1),
+ "The source editor caret position was incorrect.");
+ is(gFrames.itemCount, 0,
+ "Should have only no frames.");
+
+ is(gSources.itemCount, 1,
+ "Found the expected number of entries in the sources widget.");
+ is(gEditor.getText().indexOf("debugger"), 348,
+ "The correct source was loaded initially.");
+ is(getSelectedSourceURL(gSources), EXAMPLE_URL + "doc_inline-debugger-statement.html",
+ "The currently selected source value is incorrect (0).");
+ is(gSources.selectedValue, gSources.values[0],
+ "The currently selected source value is incorrect (1).");
+ }
+
+ function checkIframePause() {
+ // Spin the event loop before causing the debuggee to pause, to allow
+ // this function to return first.
+ executeSoon(() => gIframe.runDebuggerStatement());
+
+ return waitForCaretAndScopes(gPanel, 16).then(() => {
+ is(gDebugger.gThreadClient.paused, true,
+ "Should be paused after an interrupt request.");
+
+ ok(isCaretPos(gPanel, 16),
+ "The source editor caret position was incorrect.");
+ is(gFrames.itemCount, 1,
+ "Should have only one frame.");
+ });
+ }
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_instruments-pane-collapse.js b/devtools/client/debugger/test/mochitest/browser_dbg_instruments-pane-collapse.js
new file mode 100644
index 000000000..31b3318cd
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_instruments-pane-collapse.js
@@ -0,0 +1,167 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests that the debugger panes collapse properly.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_recursion-stack.html";
+
+var gTab, gPanel, gDebugger;
+var gPrefs, gOptions;
+
+function test() {
+ Task.spawn(function* () {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+
+ let [aTab,, aPanel] = yield initDebugger(TAB_URL, options);
+
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gPrefs = gDebugger.Prefs;
+ gOptions = gDebugger.DebuggerView.Options;
+
+ testPanesState();
+
+ gDebugger.DebuggerView.toggleInstrumentsPane({ visible: true, animated: false });
+
+ yield testInstrumentsPaneCollapse();
+ testPanesStartupPref();
+
+ closeDebuggerAndFinish(gPanel);
+ });
+}
+
+function testPanesState() {
+ let instrumentsPane =
+ gDebugger.document.getElementById("instruments-pane");
+ let instrumentsPaneToggleButton =
+ gDebugger.document.getElementById("instruments-pane-toggle");
+
+ ok(instrumentsPane.classList.contains("pane-collapsed") &&
+ instrumentsPaneToggleButton.classList.contains("pane-collapsed"),
+ "The debugger view instruments pane should initially be hidden.");
+ is(gPrefs.panesVisibleOnStartup, false,
+ "The debugger view instruments pane should initially be preffed as hidden.");
+ isnot(gOptions._showPanesOnStartupItem.getAttribute("checked"), "true",
+ "The options menu item should not be checked.");
+}
+
+function* testInstrumentsPaneCollapse () {
+ let instrumentsPane =
+ gDebugger.document.getElementById("instruments-pane");
+ let instrumentsPaneToggleButton =
+ gDebugger.document.getElementById("instruments-pane-toggle");
+
+ let width = parseInt(instrumentsPane.getAttribute("width"));
+ is(width, gPrefs.instrumentsWidth,
+ "The instruments pane has an incorrect width.");
+ is(instrumentsPane.style.marginLeft, "0px",
+ "The instruments pane has an incorrect left margin.");
+ is(instrumentsPane.style.marginRight, "0px",
+ "The instruments pane has an incorrect right margin.");
+ ok(!instrumentsPane.hasAttribute("animated"),
+ "The instruments pane has an incorrect animated attribute.");
+ ok(!instrumentsPane.classList.contains("pane-collapsed") &&
+ !instrumentsPaneToggleButton.classList.contains("pane-collapsed"),
+ "The instruments pane should at this point be visible.");
+
+ // Trigger reflow to make sure the UI is in required state.
+ gDebugger.document.documentElement.getBoundingClientRect();
+
+ gDebugger.DebuggerView.toggleInstrumentsPane({ visible: false, animated: true });
+
+ yield once(instrumentsPane, "transitionend");
+
+ is(gPrefs.panesVisibleOnStartup, false,
+ "The debugger view panes should still initially be preffed as hidden.");
+ isnot(gOptions._showPanesOnStartupItem.getAttribute("checked"), "true",
+ "The options menu item should still not be checked.");
+
+ let margin = -(width + 1) + "px";
+ is(width, gPrefs.instrumentsWidth,
+ "The instruments pane has an incorrect width after collapsing.");
+ is(instrumentsPane.style.marginLeft, margin,
+ "The instruments pane has an incorrect left margin after collapsing.");
+ is(instrumentsPane.style.marginRight, margin,
+ "The instruments pane has an incorrect right margin after collapsing.");
+
+ ok(!instrumentsPane.hasAttribute("animated"),
+ "The instruments pane has an incorrect attribute after an animated collapsing.");
+ ok(instrumentsPane.classList.contains("pane-collapsed") &&
+ instrumentsPaneToggleButton.classList.contains("pane-collapsed"),
+ "The instruments pane should not be visible after collapsing.");
+
+ gDebugger.DebuggerView.toggleInstrumentsPane({ visible: true, animated: false });
+
+ is(gPrefs.panesVisibleOnStartup, false,
+ "The debugger view panes should still initially be preffed as hidden.");
+ isnot(gOptions._showPanesOnStartupItem.getAttribute("checked"), "true",
+ "The options menu item should still not be checked.");
+
+ is(width, gPrefs.instrumentsWidth,
+ "The instruments pane has an incorrect width after uncollapsing.");
+ is(instrumentsPane.style.marginLeft, "0px",
+ "The instruments pane has an incorrect left margin after uncollapsing.");
+ is(instrumentsPane.style.marginRight, "0px",
+ "The instruments pane has an incorrect right margin after uncollapsing.");
+ ok(!instrumentsPane.hasAttribute("animated"),
+ "The instruments pane has an incorrect attribute after an unanimated uncollapsing.");
+ ok(!instrumentsPane.classList.contains("pane-collapsed") &&
+ !instrumentsPaneToggleButton.classList.contains("pane-collapsed"),
+ "The instruments pane should be visible again after uncollapsing.");
+}
+
+function testPanesStartupPref() {
+ let instrumentsPane =
+ gDebugger.document.getElementById("instruments-pane");
+ let instrumentsPaneToggleButton =
+ gDebugger.document.getElementById("instruments-pane-toggle");
+
+ is(gPrefs.panesVisibleOnStartup, false,
+ "The debugger view panes should still initially be preffed as hidden.");
+
+ ok(!instrumentsPane.classList.contains("pane-collapsed") &&
+ !instrumentsPaneToggleButton.classList.contains("pane-collapsed"),
+ "The debugger instruments pane should at this point be visible.");
+ is(gPrefs.panesVisibleOnStartup, false,
+ "The debugger view panes should initially be preffed as hidden.");
+ isnot(gOptions._showPanesOnStartupItem.getAttribute("checked"), "true",
+ "The options menu item should still not be checked.");
+
+ gOptions._showPanesOnStartupItem.setAttribute("checked", "true");
+ gOptions._toggleShowPanesOnStartup();
+
+ ok(!instrumentsPane.classList.contains("pane-collapsed") &&
+ !instrumentsPaneToggleButton.classList.contains("pane-collapsed"),
+ "The debugger instruments pane should at this point be visible.");
+ is(gPrefs.panesVisibleOnStartup, true,
+ "The debugger view panes should now be preffed as visible.");
+ is(gOptions._showPanesOnStartupItem.getAttribute("checked"), "true",
+ "The options menu item should now be checked.");
+
+ gOptions._showPanesOnStartupItem.setAttribute("checked", "false");
+ gOptions._toggleShowPanesOnStartup();
+
+ ok(!instrumentsPane.classList.contains("pane-collapsed") &&
+ !instrumentsPaneToggleButton.classList.contains("pane-collapsed"),
+ "The debugger instruments pane should at this point be visible.");
+ is(gPrefs.panesVisibleOnStartup, false,
+ "The debugger view panes should now be preffed as hidden.");
+ isnot(gOptions._showPanesOnStartupItem.getAttribute("checked"), "true",
+ "The options menu item should now be unchecked.");
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gPrefs = null;
+ gOptions = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_instruments-pane-collapse_keyboard.js b/devtools/client/debugger/test/mochitest/browser_dbg_instruments-pane-collapse_keyboard.js
new file mode 100644
index 000000000..c26c476cc
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_instruments-pane-collapse_keyboard.js
@@ -0,0 +1,40 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests that the debugger panes collapse properly.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_recursion-stack.html";
+
+function test() {
+ initDebugger(TAB_URL).then(([aTab,, aPanel]) => {
+ Task.spawn(function* () {
+ let doc = aPanel.panelWin.document;
+ let panel = doc.getElementById("instruments-pane");
+ let button = doc.getElementById("instruments-pane-toggle");
+ ok(panel.classList.contains("pane-collapsed"),
+ "The instruments panel is initially in collapsed state");
+
+ yield togglePane(button, "Press on the toggle button to expand", panel, "VK_RETURN");
+ ok(!panel.classList.contains("pane-collapsed"),
+ "The instruments panel is in the expanded state");
+
+ yield togglePane(button, "Press on the toggle button to collapse", panel, "VK_SPACE");
+ ok(panel.classList.contains("pane-collapsed"),
+ "The instruments panel is in the collapsed state");
+
+ closeDebuggerAndFinish(aPanel);
+ });
+ });
+}
+
+function* togglePane(button, message, pane, keycode) {
+ let onTransitionEnd = once(pane, "transitionend");
+ info(message);
+ button.focus();
+ EventUtils.synthesizeKey(keycode, {});
+ yield onTransitionEnd;
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_interrupts.js b/devtools/client/debugger/test/mochitest/browser_dbg_interrupts.js
new file mode 100644
index 000000000..d6d61a76b
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_interrupts.js
@@ -0,0 +1,123 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Test resuming from button and keyboard shortcuts.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
+
+function test() {
+ let gTab, gPanel, gDebugger;
+ let gSources, gBreakpoints, gTarget, gResumeButton, gResumeKey, gThreadClient;
+
+ let options = {
+ source: EXAMPLE_URL + "code_script-switching-01.js",
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gSources = gDebugger.DebuggerView.Sources;
+ gBreakpoints = gDebugger.DebuggerController.Breakpoints;
+ gTarget = gDebugger.gTarget;
+ gThreadClient = gDebugger.gThreadClient;
+ gResumeButton = gDebugger.document.getElementById("resume");
+ gResumeKey = gDebugger.document.getElementById("resumeKey");
+
+ gTarget.on("thread-paused", failOnPause);
+ addBreakpoints()
+ .then(() => { gTarget.off("thread-paused", failOnPause); })
+ .then(testResumeButton)
+ .then(testResumeKeyboard)
+ .then(() => closeDebuggerAndFinish(gPanel))
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+ });
+
+ function failOnPause() {
+ ok(false, "A pause was sent, but it shouldn't have been");
+ }
+
+ function addBreakpoints() {
+ return promise.resolve(null)
+ .then(() => gPanel.addBreakpoint({ actor: gSources.values[0], line: 5 }))
+ .then(() => gPanel.addBreakpoint({ actor: gSources.values[1], line: 6 }))
+ .then(() => gPanel.addBreakpoint({ actor: gSources.values[1], line: 7 }))
+ .then(() => ensureThreadClientState(gPanel, "resumed"));
+ }
+
+ function resume() {
+ let onceResumed = gTarget.once("thread-resumed");
+ gThreadClient.resume();
+ return onceResumed;
+ }
+
+ function testResumeButton() {
+ info("Pressing the resume button, expecting a thread-paused");
+
+ ok(!gResumeButton.hasAttribute("disabled"), "Resume button is not disabled 1");
+ ok(!gResumeButton.hasAttribute("break-on-next"), "Resume button isn't waiting for next execution");
+ ok(!gResumeButton.hasAttribute("checked"), "Resume button is not checked");
+ let oncePaused = gTarget.once("thread-paused");
+
+ // Click the pause button to break on next execution
+ EventUtils.sendMouseEvent({ type: "mousedown" }, gResumeButton, gDebugger);
+ ok(gResumeButton.hasAttribute("disabled"), "Resume button is disabled");
+ ok(gResumeButton.hasAttribute("break-on-next"), "Resume button is waiting for next execution");
+ ok(!gResumeButton.hasAttribute("checked"), "Resume button is not checked");
+
+ // Evaluate a script to fully pause the debugger
+ once(gDebugger.gClient, "willInterrupt").then(() => {
+ evalInTab(gTab, "1+1;");
+ });
+
+ return waitForDebuggerEvents(gPanel, gDebugger.EVENTS.SOURCE_SHOWN)
+ .then(() => {
+ ok(!gResumeButton.hasAttribute("break-on-next"), "Resume button isn't waiting for next execution");
+ is(gResumeButton.getAttribute("checked"), "true", "Resume button is checked");
+ ok(!gResumeButton.hasAttribute("disabled"), "Resume button is not disabled 2");
+ })
+ .then(() => {
+ let p = ensureThreadClientState(gPanel, "resumed");
+ gThreadClient.resume();
+ return p;
+ });
+ }
+
+ function testResumeKeyboard() {
+ let key = gResumeKey.getAttribute("keycode");
+ info("Triggering a pause with keyboard (" + key + "), expecting a thread-paused");
+
+ ok(!gResumeButton.hasAttribute("disabled"), "Resume button is not disabled 3");
+ ok(!gResumeButton.hasAttribute("break-on-next"), "Resume button isn't waiting for next execution");
+ ok(!gResumeButton.hasAttribute("checked"), "Resume button is not checked");
+
+ // Press the key to break on next execution
+ EventUtils.synthesizeKey(key, { }, gDebugger);
+ ok(gResumeButton.hasAttribute("disabled"), "Resume button is disabled");
+ ok(gResumeButton.hasAttribute("break-on-next"), "Resume button is waiting for next execution");
+ ok(!gResumeButton.hasAttribute("checked"), "Resume button is not checked");
+
+ // Evaluate a script to fully pause the debugger
+ once(gDebugger.gClient, "willInterrupt").then(() => {
+ evalInTab(gTab, "1+1;");
+ });
+
+ return waitForDebuggerEvents(gPanel, gDebugger.EVENTS.SOURCE_SHOWN)
+ .then(() => {
+ ok(!gResumeButton.hasAttribute("break-on-next"), "Resume button isn't waiting for next execution");
+ is(gResumeButton.getAttribute("checked"), "true", "Resume button is checked");
+ ok(!gResumeButton.hasAttribute("disabled"), "Resume button is not disabled 4");
+ })
+ .then(() => {
+ let p = ensureThreadClientState(gPanel, "resumed");
+ gThreadClient.resume();
+ return p;
+ });
+ }
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_jump-to-function-definition.js b/devtools/client/debugger/test/mochitest/browser_dbg_jump-to-function-definition.js
new file mode 100644
index 000000000..71d9c340c
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_jump-to-function-definition.js
@@ -0,0 +1,50 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests if the jump to function definition works properly.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_function-jump.html";
+const SCRIPT_URI = EXAMPLE_URL + "code_function-jump-01.js";
+
+
+function test() {
+ let gTab, gPanel, gDebugger, gSources;
+
+ let options = {
+ source: EXAMPLE_URL + "code_function-jump-01.js",
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gSources = gDebugger.DebuggerView.Sources;
+
+ jumpToFunctionDefinition()
+ .then(() => closeDebuggerAndFinish(gPanel))
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+ });
+
+ function jumpToFunctionDefinition() {
+ let callLocation = {line: 5, ch: 0};
+ let editor = gDebugger.DebuggerView.editor;
+ let coords = editor.getCoordsFromPosition(callLocation);
+
+ gDebugger.DebuggerView.Sources._onMouseDown({ clientX: coords.left,
+ clientY: coords.top,
+ metaKey: true });
+
+ let deferred = promise.defer();
+ executeSoon(() => {
+ is(editor.getDebugLocation(), 1, "foo definition should be highlighted");
+ deferred.resolve();
+ });
+ return deferred.promise;
+ }
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_listaddons.js b/devtools/client/debugger/test/mochitest/browser_dbg_listaddons.js
new file mode 100644
index 000000000..1490c9670
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_listaddons.js
@@ -0,0 +1,112 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure the listAddons request works as specified.
+ */
+const ADDON1_ID = "jid1-oBAwBoE5rSecNg@jetpack";
+const ADDON1_PATH = "addon1.xpi";
+const ADDON2_ID = "jid1-qjtzNGV8xw5h2A@jetpack";
+const ADDON2_PATH = "addon2.xpi";
+
+var gAddon1, gAddon1Actor, gAddon2, gAddon2Actor, gClient;
+
+function test() {
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+
+ let transport = DebuggerServer.connectPipe();
+ gClient = new DebuggerClient(transport);
+ gClient.connect().then(([aType, aTraits]) => {
+ is(aType, "browser",
+ "Root actor should identify itself as a browser.");
+
+ promise.resolve(null)
+ .then(testFirstAddon)
+ .then(testSecondAddon)
+ .then(testRemoveFirstAddon)
+ .then(testRemoveSecondAddon)
+ .then(() => gClient.close())
+ .then(finish)
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+ });
+}
+
+function testFirstAddon() {
+ let addonListChanged = false;
+ gClient.addOneTimeListener("addonListChanged", () => {
+ addonListChanged = true;
+ });
+
+ return addTemporaryAddon(ADDON1_PATH).then(aAddon => {
+ gAddon1 = aAddon;
+
+ return getAddonActorForId(gClient, ADDON1_ID).then(aGrip => {
+ ok(!addonListChanged, "Should not yet be notified that list of addons changed.");
+ ok(aGrip, "Should find an addon actor for addon1.");
+ gAddon1Actor = aGrip.actor;
+ });
+ });
+}
+
+function testSecondAddon() {
+ let addonListChanged = false;
+ gClient.addOneTimeListener("addonListChanged", function () {
+ addonListChanged = true;
+ });
+
+ return addTemporaryAddon(ADDON2_PATH).then(aAddon => {
+ gAddon2 = aAddon;
+
+ return getAddonActorForId(gClient, ADDON1_ID).then(aFirstGrip => {
+ return getAddonActorForId(gClient, ADDON2_ID).then(aSecondGrip => {
+ ok(addonListChanged, "Should be notified that list of addons changed.");
+ is(aFirstGrip.actor, gAddon1Actor, "First addon's actor shouldn't have changed.");
+ ok(aSecondGrip, "Should find a addon actor for the second addon.");
+ gAddon2Actor = aSecondGrip.actor;
+ });
+ });
+ });
+}
+
+function testRemoveFirstAddon() {
+ let addonListChanged = false;
+ gClient.addOneTimeListener("addonListChanged", function () {
+ addonListChanged = true;
+ });
+
+ return removeAddon(gAddon1).then(() => {
+ return getAddonActorForId(gClient, ADDON1_ID).then(aGrip => {
+ ok(addonListChanged, "Should be notified that list of addons changed.");
+ ok(!aGrip, "Shouldn't find a addon actor for the first addon anymore.");
+ });
+ });
+}
+
+function testRemoveSecondAddon() {
+ let addonListChanged = false;
+ gClient.addOneTimeListener("addonListChanged", function () {
+ addonListChanged = true;
+ });
+
+ return removeAddon(gAddon2).then(() => {
+ return getAddonActorForId(gClient, ADDON2_ID).then(aGrip => {
+ ok(addonListChanged, "Should be notified that list of addons changed.");
+ ok(!aGrip, "Shouldn't find a addon actor for the second addon anymore.");
+ });
+ });
+}
+
+registerCleanupFunction(function () {
+ gAddon1 = null;
+ gAddon1Actor = null;
+ gAddon2 = null;
+ gAddon2Actor = null;
+ gClient = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_listtabs-01.js b/devtools/client/debugger/test/mochitest/browser_dbg_listtabs-01.js
new file mode 100644
index 000000000..dc804713b
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_listtabs-01.js
@@ -0,0 +1,98 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure the listTabs request works as specified.
+ */
+
+const TAB1_URL = EXAMPLE_URL + "doc_empty-tab-01.html";
+const TAB2_URL = EXAMPLE_URL + "doc_empty-tab-02.html";
+
+var gTab1, gTab1Actor, gTab2, gTab2Actor, gClient;
+
+function test() {
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+
+ let transport = DebuggerServer.connectPipe();
+ gClient = new DebuggerClient(transport);
+ gClient.connect().then(([aType, aTraits]) => {
+ is(aType, "browser",
+ "Root actor should identify itself as a browser.");
+
+ promise.resolve(null)
+ .then(testFirstTab)
+ .then(testSecondTab)
+ .then(testRemoveTab)
+ .then(testAttachRemovedTab)
+ .then(() => gClient.close())
+ .then(finish)
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+ });
+}
+
+function testFirstTab() {
+ return addTab(TAB1_URL).then(aTab => {
+ gTab1 = aTab;
+
+ return getTabActorForUrl(gClient, TAB1_URL).then(aGrip => {
+ ok(aGrip, "Should find a tab actor for the first tab.");
+ gTab1Actor = aGrip.actor;
+ });
+ });
+}
+
+function testSecondTab() {
+ return addTab(TAB2_URL).then(aTab => {
+ gTab2 = aTab;
+
+ return getTabActorForUrl(gClient, TAB1_URL).then(aFirstGrip => {
+ return getTabActorForUrl(gClient, TAB2_URL).then(aSecondGrip => {
+ is(aFirstGrip.actor, gTab1Actor, "First tab's actor shouldn't have changed.");
+ ok(aSecondGrip, "Should find a tab actor for the second tab.");
+ gTab2Actor = aSecondGrip.actor;
+ });
+ });
+ });
+}
+
+function testRemoveTab() {
+ return removeTab(gTab1).then(() => {
+ return getTabActorForUrl(gClient, TAB1_URL).then(aGrip => {
+ ok(!aGrip, "Shouldn't find a tab actor for the first tab anymore.");
+ });
+ });
+}
+
+function testAttachRemovedTab() {
+ return removeTab(gTab2).then(() => {
+ let deferred = promise.defer();
+
+ gClient.addListener("paused", (aEvent, aPacket) => {
+ ok(false, "Attaching to an exited tab actor shouldn't generate a pause.");
+ deferred.reject();
+ });
+
+ gClient.request({ to: gTab2Actor, type: "attach" }, aResponse => {
+ is(aResponse.error, "connectionClosed",
+ "Connection is gone since the tab was removed.");
+ deferred.resolve();
+ });
+
+ return deferred.promise;
+ });
+}
+
+registerCleanupFunction(function () {
+ gTab1 = null;
+ gTab1Actor = null;
+ gTab2 = null;
+ gTab2Actor = null;
+ gClient = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_listtabs-02.js b/devtools/client/debugger/test/mochitest/browser_dbg_listtabs-02.js
new file mode 100644
index 000000000..f696b6cb0
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_listtabs-02.js
@@ -0,0 +1,219 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure the root actor's live tab list implementation works as specified.
+ */
+
+var { BrowserTabList } = require("devtools/server/actors/webbrowser");
+
+var gTestPage = "data:text/html;charset=utf-8," + encodeURIComponent(
+ "<title>JS Debugger BrowserTabList test page</title><body>Yo.</body>");
+
+// The tablist object whose behavior we observe.
+var gTabList;
+var gFirstActor, gActorA;
+var gTabA, gTabB, gTabC;
+var gNewWindow;
+
+// Stock onListChanged handler.
+var onListChangedCount = 0;
+function onListChangedHandler() {
+ onListChangedCount++;
+}
+
+function test() {
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+
+ gTabList = new BrowserTabList("fake DebuggerServerConnection");
+ gTabList._testing = true;
+ gTabList.onListChanged = onListChangedHandler;
+
+ checkSingleTab()
+ .then(addTabA)
+ .then(testTabA)
+ .then(addTabB)
+ .then(testTabB)
+ .then(removeTabA)
+ .then(testTabClosed)
+ .then(addTabC)
+ .then(testTabC)
+ .then(removeTabC)
+ .then(testNewWindow)
+ .then(removeNewWindow)
+ .then(testWindowClosed)
+ .then(removeTabB)
+ .then(checkSingleTab)
+ .then(finishUp);
+}
+
+function checkSingleTab() {
+ return gTabList.getList().then(aTabActors => {
+ is(aTabActors.length, 1, "initial tab list: contains initial tab");
+ gFirstActor = aTabActors[0];
+ is(gFirstActor.url, "about:blank", "initial tab list: initial tab URL is 'about:blank'");
+ is(gFirstActor.title, "New Tab", "initial tab list: initial tab title is 'New Tab'");
+ });
+}
+
+function addTabA() {
+ return addTab(gTestPage).then(aTab => {
+ gTabA = aTab;
+ });
+}
+
+function testTabA() {
+ is(onListChangedCount, 1, "onListChanged handler call count");
+
+ return gTabList.getList().then(aTabActors => {
+ let tabActors = new Set(aTabActors);
+ is(tabActors.size, 2, "gTabA opened: two tabs in list");
+ ok(tabActors.has(gFirstActor), "gTabA opened: initial tab present");
+
+ info("actors: " + [...tabActors].map(a => a.url));
+ gActorA = [...tabActors].filter(a => a !== gFirstActor)[0];
+ ok(gActorA.url.match(/^data:text\/html;/), "gTabA opened: new tab URL");
+ is(gActorA.title, "JS Debugger BrowserTabList test page", "gTabA opened: new tab title");
+ });
+}
+
+function addTabB() {
+ return addTab(gTestPage).then(aTab => {
+ gTabB = aTab;
+ });
+}
+
+function testTabB() {
+ is(onListChangedCount, 2, "onListChanged handler call count");
+
+ return gTabList.getList().then(aTabActors => {
+ let tabActors = new Set(aTabActors);
+ is(tabActors.size, 3, "gTabB opened: three tabs in list");
+ });
+}
+
+function removeTabA() {
+ let deferred = promise.defer();
+
+ once(gBrowser.tabContainer, "TabClose").then(aEvent => {
+ ok(!aEvent.detail.adoptedBy, "This was a normal tab close");
+
+ // Let the actor's TabClose handler finish first.
+ executeSoon(deferred.resolve);
+ }, false);
+
+ removeTab(gTabA);
+ return deferred.promise;
+}
+
+function testTabClosed() {
+ is(onListChangedCount, 3, "onListChanged handler call count");
+
+ gTabList.getList().then(aTabActors => {
+ let tabActors = new Set(aTabActors);
+ is(tabActors.size, 2, "gTabA closed: two tabs in list");
+ ok(tabActors.has(gFirstActor), "gTabA closed: initial tab present");
+
+ info("actors: " + [...tabActors].map(a => a.url));
+ gActorA = [...tabActors].filter(a => a !== gFirstActor)[0];
+ ok(gActorA.url.match(/^data:text\/html;/), "gTabA closed: new tab URL");
+ is(gActorA.title, "JS Debugger BrowserTabList test page", "gTabA closed: new tab title");
+ });
+}
+
+function addTabC() {
+ return addTab(gTestPage).then(aTab => {
+ gTabC = aTab;
+ });
+}
+
+function testTabC() {
+ is(onListChangedCount, 4, "onListChanged handler call count");
+
+ gTabList.getList().then(aTabActors => {
+ let tabActors = new Set(aTabActors);
+ is(tabActors.size, 3, "gTabC opened: three tabs in list");
+ });
+}
+
+function removeTabC() {
+ let deferred = promise.defer();
+
+ once(gBrowser.tabContainer, "TabClose").then(aEvent => {
+ ok(aEvent.detail.adoptedBy, "This was a tab closed by moving");
+
+ // Let the actor's TabClose handler finish first.
+ executeSoon(deferred.resolve);
+ }, false);
+
+ gNewWindow = gBrowser.replaceTabWithWindow(gTabC);
+ return deferred.promise;
+}
+
+function testNewWindow() {
+ is(onListChangedCount, 5, "onListChanged handler call count");
+
+ return gTabList.getList().then(aTabActors => {
+ let tabActors = new Set(aTabActors);
+ is(tabActors.size, 3, "gTabC closed: three tabs in list");
+ ok(tabActors.has(gFirstActor), "gTabC closed: initial tab present");
+
+ info("actors: " + [...tabActors].map(a => a.url));
+ gActorA = [...tabActors].filter(a => a !== gFirstActor)[0];
+ ok(gActorA.url.match(/^data:text\/html;/), "gTabC closed: new tab URL");
+ is(gActorA.title, "JS Debugger BrowserTabList test page", "gTabC closed: new tab title");
+ });
+}
+
+function removeNewWindow() {
+ let deferred = promise.defer();
+
+ once(gNewWindow, "unload").then(aEvent => {
+ ok(!aEvent.detail, "This was a normal window close");
+
+ // Let the actor's TabClose handler finish first.
+ executeSoon(deferred.resolve);
+ }, false);
+
+ gNewWindow.close();
+ return deferred.promise;
+}
+
+function testWindowClosed() {
+ is(onListChangedCount, 6, "onListChanged handler call count");
+
+ return gTabList.getList().then(aTabActors => {
+ let tabActors = new Set(aTabActors);
+ is(tabActors.size, 2, "gNewWindow closed: two tabs in list");
+ ok(tabActors.has(gFirstActor), "gNewWindow closed: initial tab present");
+
+ info("actors: " + [...tabActors].map(a => a.url));
+ gActorA = [...tabActors].filter(a => a !== gFirstActor)[0];
+ ok(gActorA.url.match(/^data:text\/html;/), "gNewWindow closed: new tab URL");
+ is(gActorA.title, "JS Debugger BrowserTabList test page", "gNewWindow closed: new tab title");
+ });
+}
+
+function removeTabB() {
+ let deferred = promise.defer();
+
+ once(gBrowser.tabContainer, "TabClose").then(aEvent => {
+ ok(!aEvent.detail.adoptedBy, "This was a normal tab close");
+
+ // Let the actor's TabClose handler finish first.
+ executeSoon(deferred.resolve);
+ }, false);
+
+ removeTab(gTabB);
+ return deferred.promise;
+}
+
+function finishUp() {
+ gTabList = gFirstActor = gActorA = gTabA = gTabB = gTabC = gNewWindow = null;
+ finish();
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_listtabs-03.js b/devtools/client/debugger/test/mochitest/browser_dbg_listtabs-03.js
new file mode 100644
index 000000000..d5584dcdb
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_listtabs-03.js
@@ -0,0 +1,61 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure the listTabs request works as specified.
+ */
+
+const TAB1_URL = EXAMPLE_URL + "doc_empty-tab-01.html";
+
+var gTab1, gTab1Actor, gTab2, gTab2Actor, gClient;
+
+function test() {
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+
+ let transport = DebuggerServer.connectPipe();
+ gClient = new DebuggerClient(transport);
+ gClient.connect().then(Task.async(function* ([aType, aTraits]) {
+ is(aType, "browser",
+ "Root actor should identify itself as a browser.");
+ let tab = yield addTab(TAB1_URL);
+
+ let { tabs } = yield gClient.listTabs();
+ is(tabs.length, 2, "Should be two tabs");
+ let tabGrip = tabs.filter(a => a.url == TAB1_URL).pop();
+ ok(tabGrip, "Should have an actor for the tab");
+
+ let response = yield gClient.request({ to: tabGrip.actor, type: "attach" });
+ is(response.type, "tabAttached", "Should have attached");
+
+ response = yield gClient.listTabs();
+ tabs = response.tabs;
+
+ response = yield gClient.request({ to: tabGrip.actor, type: "detach" });
+ is(response.type, "detached", "Should have detached");
+
+ let newGrip = tabs.filter(a => a.url == TAB1_URL).pop();
+ is(newGrip.actor, tabGrip.actor, "Should have the same actor for the same tab");
+
+ response = yield gClient.request({ to: tabGrip.actor, type: "attach" });
+ is(response.type, "tabAttached", "Should have attached");
+ response = yield gClient.request({ to: tabGrip.actor, type: "detach" });
+ is(response.type, "detached", "Should have detached");
+
+ yield removeTab(tab);
+ yield gClient.close();
+ finish();
+ }));
+}
+
+registerCleanupFunction(function () {
+ gTab1 = null;
+ gTab1Actor = null;
+ gTab2 = null;
+ gTab2Actor = null;
+ gClient = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_listworkers.js b/devtools/client/debugger/test/mochitest/browser_dbg_listworkers.js
new file mode 100644
index 000000000..315acc231
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_listworkers.js
@@ -0,0 +1,59 @@
+var TAB_URL = EXAMPLE_URL + "doc_listworkers-tab.html";
+var WORKER1_URL = "code_listworkers-worker1.js";
+var WORKER2_URL = "code_listworkers-worker2.js";
+
+function test() {
+ Task.spawn(function* () {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+
+ let client = new DebuggerClient(DebuggerServer.connectPipe());
+ yield connect(client);
+
+ let tab = yield addTab(TAB_URL);
+ let { tabs } = yield listTabs(client);
+ let [, tabClient] = yield attachTab(client, findTab(tabs, TAB_URL));
+
+ let { workers } = yield listWorkers(tabClient);
+ is(workers.length, 0);
+
+ executeSoon(() => {
+ evalInTab(tab, "var worker1 = new Worker('" + WORKER1_URL + "');");
+ });
+ yield waitForWorkerListChanged(tabClient);
+
+ ({ workers } = yield listWorkers(tabClient));
+ is(workers.length, 1);
+ is(workers[0].url, WORKER1_URL);
+
+ executeSoon(() => {
+ evalInTab(tab, "var worker2 = new Worker('" + WORKER2_URL + "');");
+ });
+ yield waitForWorkerListChanged(tabClient);
+
+ ({ workers } = yield listWorkers(tabClient));
+ is(workers.length, 2);
+ is(workers[0].url, WORKER1_URL);
+ is(workers[1].url, WORKER2_URL);
+
+ executeSoon(() => {
+ evalInTab(tab, "worker1.terminate()");
+ });
+ yield waitForWorkerListChanged(tabClient);
+
+ ({ workers } = yield listWorkers(tabClient));
+ is(workers.length, 1);
+ is(workers[0].url, WORKER2_URL);
+
+ executeSoon(() => {
+ evalInTab(tab, "worker2.terminate()");
+ });
+ yield waitForWorkerListChanged(tabClient);
+
+ ({ workers } = yield listWorkers(tabClient));
+ is(workers.length, 0);
+
+ yield close(client);
+ finish();
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_location-changes-01-simple.js b/devtools/client/debugger/test/mochitest/browser_dbg_location-changes-01-simple.js
new file mode 100644
index 000000000..261881835
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_location-changes-01-simple.js
@@ -0,0 +1,60 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure that changing the tab location URL works.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_recursion-stack.html";
+
+function test() {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ const gTab = aTab;
+ const gPanel = aPanel;
+ const gDebugger = gPanel.panelWin;
+ const gEditor = gDebugger.DebuggerView.editor;
+ const gSources = gDebugger.DebuggerView.Sources;
+ const gFrames = gDebugger.DebuggerView.StackFrames;
+ const constants = gDebugger.require("./content/constants");
+
+ Task.spawn(function* () {
+ let onCaretUpdated = waitForCaretAndScopes(gPanel, 14);
+ callInTab(gTab, "simpleCall");
+ yield onCaretUpdated;
+
+ is(gDebugger.gThreadClient.state, "paused",
+ "Should only be getting stack frames while paused.");
+
+ is(gFrames.itemCount, 1,
+ "Should have only one frame.");
+
+ is(gSources.itemCount, 1,
+ "Found the expected number of entries in the sources widget.");
+
+ isnot(gSources.selectedValue, null,
+ "There should be a selected source value.");
+ isnot(gEditor.getText().length, 0,
+ "The source editor should have some text displayed.");
+ isnot(gEditor.getText(), gDebugger.L10N.getStr("loadingText"),
+ "The source editor text should not be 'Loading...'");
+
+ is(gDebugger.document.querySelectorAll("#sources .side-menu-widget-empty-notice-container").length, 0,
+ "The sources widget should not display any notice at this point (1).");
+ is(gDebugger.document.querySelectorAll("#sources .side-menu-widget-empty-notice").length, 0,
+ "The sources widget should not display any notice at this point (2).");
+ is(gDebugger.document.querySelector("#sources .side-menu-widget-empty-notice > label"), null,
+ "The sources widget should not display a notice at this point (3).");
+
+ yield doResume(gPanel);
+ navigateActiveTabTo(gPanel, "about:blank");
+ yield waitForDispatch(gPanel, constants.UNLOAD);
+ closeDebuggerAndFinish(gPanel);
+ });
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_location-changes-02-blank.js b/devtools/client/debugger/test/mochitest/browser_dbg_location-changes-02-blank.js
new file mode 100644
index 000000000..10c7d98ba
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_location-changes-02-blank.js
@@ -0,0 +1,57 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure that changing the tab location URL to a page with no sources works.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_recursion-stack.html";
+
+function test() {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ const gTab = aTab;
+ const gPanel = aPanel;
+ const gDebugger = gPanel.panelWin;
+ const gEditor = gDebugger.DebuggerView.editor;
+ const gSources = gDebugger.DebuggerView.Sources;
+ const gFrames = gDebugger.DebuggerView.StackFrames;
+ const constants = gDebugger.require("./content/constants");
+
+ Task.spawn(function* () {
+ let onCaretUpdated = waitForCaretUpdated(gPanel, 14);
+ callInTab(gTab, "simpleCall");
+ yield onCaretUpdated;
+
+ navigateActiveTabTo(gPanel, "about:blank");
+ yield waitForNavigation(gPanel);
+
+ isnot(gDebugger.gThreadClient.state, "paused",
+ "Should not be paused after a tab navigation.");
+
+ is(gFrames.itemCount, 0,
+ "Should have no frames.");
+
+ is(gSources.itemCount, 0,
+ "Found no entries in the sources widget.");
+
+ is(gSources.selectedValue, "",
+ "There should be no selected source value.");
+ is(gEditor.getText().length, 0,
+ "The source editor should not have any text displayed.");
+
+ is(gDebugger.document.querySelectorAll("#sources .side-menu-widget-empty-text").length, 1,
+ "The sources widget should now display a notice (1).");
+ is(gDebugger.document.querySelectorAll("#sources .side-menu-widget-empty-text")[0].getAttribute("value"),
+ gDebugger.L10N.getStr("noSourcesText"),
+ "The sources widget should now display a notice (2).");
+
+ closeDebuggerAndFinish(gPanel);
+ });
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_location-changes-03-new.js b/devtools/client/debugger/test/mochitest/browser_dbg_location-changes-03-new.js
new file mode 100644
index 000000000..878c7be81
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_location-changes-03-new.js
@@ -0,0 +1,59 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure that changing the tab location URL to a page with other sources works.
+ */
+
+const TAB_URL_1 = EXAMPLE_URL + "doc_recursion-stack.html";
+const TAB_URL_2 = EXAMPLE_URL + "doc_iframes.html";
+
+function test() {
+ let options = {
+ source: TAB_URL_1,
+ line: 1
+ };
+ initDebugger(TAB_URL_1, options).then(([aTab, aDebuggee, aPanel]) => {
+ const gTab = aTab;
+ const gDebuggee = aDebuggee;
+ const gPanel = aPanel;
+ const gDebugger = gPanel.panelWin;
+ const gEditor = gDebugger.DebuggerView.editor;
+ const gSources = gDebugger.DebuggerView.Sources;
+ const gFrames = gDebugger.DebuggerView.StackFrames;
+ const constants = gDebugger.require("./content/constants");
+
+ Task.spawn(function* () {
+ let onCaretUpdated = waitForCaretUpdated(gPanel, 14);
+ callInTab(gTab, "simpleCall");
+ yield onCaretUpdated;
+
+ const startedLoading = waitForNextDispatch(gDebugger.DebuggerController,
+ constants.LOAD_SOURCE_TEXT);
+ navigateActiveTabTo(gPanel, TAB_URL_2, gDebugger.EVENTS.SOURCE_SHOWN);
+ yield startedLoading;
+
+ isnot(gDebugger.gThreadClient.state, "paused",
+ "Should not be paused after a tab navigation.");
+ is(gFrames.itemCount, 0,
+ "Should have no frames.");
+ is(gSources.itemCount, 1,
+ "Found the expected number of entries in the sources widget.");
+
+ is(getSelectedSourceURL(gSources), EXAMPLE_URL + "doc_inline-debugger-statement.html",
+ "There should be a selected source value.");
+ isnot(gEditor.getText().length, 0,
+ "The source editor should have some text displayed.");
+ is(gEditor.getText(), gDebugger.L10N.getStr("loadingText"),
+ "The source editor text should be 'Loading...'");
+
+ is(gDebugger.document.querySelectorAll("#sources .side-menu-widget-empty-text").length, 0,
+ "The sources widget should not display any notice at this point.");
+
+ yield waitForDispatch(gPanel, constants.LOAD_SOURCE_TEXT);
+ closeDebuggerAndFinish(gPanel);
+ });
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_location-changes-04-breakpoint.js b/devtools/client/debugger/test/mochitest/browser_dbg_location-changes-04-breakpoint.js
new file mode 100644
index 000000000..493796720
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_location-changes-04-breakpoint.js
@@ -0,0 +1,165 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure that reloading a page with a breakpoint set does not cause it to
+ * fire more than once.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_included-script.html";
+const SOURCE_URL = EXAMPLE_URL + "code_location-changes.js";
+
+function test() {
+ const options = {
+ source: SOURCE_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab, aDebuggee, aPanel]) => {
+ const gTab = aTab;
+ const gDebuggee = aDebuggee;
+ const gPanel = aPanel;
+ const gDebugger = gPanel.panelWin;
+ const gEditor = gDebugger.DebuggerView.editor;
+ const gSources = gDebugger.DebuggerView.Sources;
+ const queries = gDebugger.require("./content/queries");
+ const actions = bindActionCreators(gPanel);
+ const getState = gDebugger.DebuggerController.getState;
+
+ function clickButtonAndPause() {
+ const paused = waitForPause(gDebugger.gThreadClient);
+ BrowserTestUtils.synthesizeMouse("button", 2, 2, {}, gBrowser.selectedBrowser);
+ return paused;
+ }
+
+ Task.spawn(function* () {
+ let onCaretUpdated = waitForCaretUpdated(gPanel, 17);
+ callInTab(gTab, "runDebuggerStatement");
+ yield onCaretUpdated;
+
+ const location = { actor: getSourceActor(gSources, SOURCE_URL), line: 5 };
+ yield actions.addBreakpoint(location);
+
+ const caretUpdated = waitForSourceAndCaret(gPanel, ".js", 5);
+ gSources.highlightBreakpoint(location);
+ yield caretUpdated;
+ ok(true, "Switched to the desired function when adding a breakpoint");
+
+ is(gDebugger.gThreadClient.state, "paused",
+ "The breakpoint was hit (1).");
+ is(getSelectedSourceURL(gSources), SOURCE_URL,
+ "The currently shown source is correct (1).");
+ ok(isCaretPos(gPanel, 5),
+ "The source editor caret position is correct (1).");
+
+ yield doResume(gPanel);
+
+ isnot(gDebugger.gThreadClient.state, "paused",
+ "The breakpoint was not hit yet (2).");
+ is(getSelectedSourceURL(gSources), SOURCE_URL,
+ "The currently shown source is correct (2).");
+ ok(isCaretPos(gPanel, 5),
+ "The source editor caret position is correct (2).");
+
+ let packet = yield clickButtonAndPause();
+ is(packet.why.type, "breakpoint",
+ "Execution has advanced to the breakpoint.");
+ isnot(packet.why.type, "debuggerStatement",
+ "The breakpoint was hit before the debugger statement.");
+ yield ensureCaretAt(gPanel, 5, 1, true);
+
+ is(gDebugger.gThreadClient.state, "paused",
+ "The breakpoint was hit (3).");
+ is(getSelectedSourceURL(gSources), SOURCE_URL,
+ "The currently shown source is incorrect (3).");
+ ok(isCaretPos(gPanel, 5),
+ "The source editor caret position is incorrect (3).");
+
+ let paused = waitForPause(gDebugger.gThreadClient);
+ gDebugger.gThreadClient.resume();
+ packet = yield paused;
+
+ is(packet.why.type, "debuggerStatement",
+ "Execution has advanced to the next line.");
+ isnot(packet.why.type, "breakpoint",
+ "No ghost breakpoint was hit.");
+
+ yield ensureCaretAt(gPanel, 6, 1, true);
+
+ is(gDebugger.gThreadClient.state, "paused",
+ "The debugger statement was hit (4).");
+ is(getSelectedSourceURL(gSources), SOURCE_URL,
+ "The currently shown source is incorrect (4).");
+ ok(isCaretPos(gPanel, 6),
+ "The source editor caret position is incorrect (4).");
+
+ yield promise.all([
+ reload(gPanel),
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.SOURCE_SHOWN)
+ ]);
+
+ isnot(gDebugger.gThreadClient.state, "paused",
+ "The breakpoint wasn't hit yet (5).");
+ is(getSelectedSourceURL(gSources), SOURCE_URL,
+ "The currently shown source is incorrect (5).");
+ ok(isCaretPos(gPanel, 1),
+ "The source editor caret position is incorrect (5).");
+
+ paused = waitForPause(gDebugger.gThreadClient);
+ clickButtonAndPause();
+ packet = yield paused;
+ is(packet.why.type, "breakpoint",
+ "Execution has advanced to the breakpoint.");
+ isnot(packet.why.type, "debuggerStatement",
+ "The breakpoint was hit before the debugger statement.");
+ yield ensureCaretAt(gPanel, 5, 1, true);
+
+ is(gDebugger.gThreadClient.state, "paused",
+ "The breakpoint was hit (6).");
+ is(getSelectedSourceURL(gSources), SOURCE_URL,
+ "The currently shown source is incorrect (6).");
+ ok(isCaretPos(gPanel, 5),
+ "The source editor caret position is incorrect (6).");
+
+ paused = waitForPause(gDebugger.gThreadClient);
+ gDebugger.gThreadClient.resume();
+ packet = yield paused;
+
+ is(packet.why.type, "debuggerStatement",
+ "Execution has advanced to the next line.");
+ isnot(packet.why.type, "breakpoint",
+ "No ghost breakpoint was hit.");
+
+ yield ensureCaretAt(gPanel, 6, 1, true);
+
+ is(gDebugger.gThreadClient.state, "paused",
+ "The debugger statement was hit (7).");
+ is(getSelectedSourceURL(gSources), SOURCE_URL,
+ "The currently shown source is incorrect (7).");
+ ok(isCaretPos(gPanel, 6),
+ "The source editor caret position is incorrect (7).");
+
+ let sourceShown = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.SOURCE_SHOWN);
+ // Click the second source in the list.
+ yield actions.selectSource(getSourceForm(gSources, TAB_URL));
+ yield sourceShown;
+ is(gEditor.getText().indexOf("debugger"), 447,
+ "The correct source is shown in the source editor.");
+ is(gEditor.getBreakpoints().length, 0,
+ "No breakpoints should be shown for the second source.");
+ yield ensureCaretAt(gPanel, 1, 1, true);
+
+ sourceShown = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.SOURCE_SHOWN);
+ yield actions.selectSource(getSourceForm(gSources, SOURCE_URL));
+ yield sourceShown;
+ is(gEditor.getText().indexOf("debugger"), 148,
+ "The correct source is shown in the source editor.");
+ is(gEditor.getBreakpoints().length, 1,
+ "One breakpoint should be shown for the first source.");
+
+ yield ensureCaretAt(gPanel, 6, 1, true);
+ resumeDebuggerThenCloseAndFinish(gPanel);
+ });
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_multiple-windows.js b/devtools/client/debugger/test/mochitest/browser_dbg_multiple-windows.js
new file mode 100644
index 000000000..b0bb1834c
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_multiple-windows.js
@@ -0,0 +1,165 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure that the debugger attaches to the right tab when multiple windows
+ * are open.
+ */
+
+const TAB1_URL = EXAMPLE_URL + "doc_script-switching-01.html";
+const TAB2_URL = EXAMPLE_URL + "doc_script-switching-02.html";
+
+var gNewTab, gNewWindow;
+var gClient;
+
+function test() {
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+
+ let transport = DebuggerServer.connectPipe();
+ gClient = new DebuggerClient(transport);
+ gClient.connect().then(([aType, aTraits]) => {
+ is(aType, "browser",
+ "Root actor should identify itself as a browser.");
+
+ promise.resolve(null)
+ .then(() => addTab(TAB1_URL))
+ .then(testFirstTab)
+ .then(() => addWindow(TAB2_URL))
+ .then(testNewWindow)
+ .then(testFocusFirst)
+ .then(testRemoveTab)
+ .then(() => gClient.close())
+ .then(finish)
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+ });
+}
+
+function testFirstTab(aTab) {
+ let deferred = promise.defer();
+
+ gNewTab = aTab;
+ ok(!!gNewTab, "Second tab created.");
+
+ gClient.listTabs(aResponse => {
+ let tabActor = aResponse.tabs.filter(aGrip => aGrip.url == TAB1_URL).pop();
+ ok(tabActor,
+ "Should find a tab actor for the first tab.");
+
+ is(aResponse.selected, 1,
+ "The first tab is selected.");
+
+ deferred.resolve();
+ });
+
+ return deferred.promise;
+}
+
+function testNewWindow(aWindow) {
+ let deferred = promise.defer();
+
+ gNewWindow = aWindow;
+ ok(!!gNewWindow, "Second window created.");
+
+ gNewWindow.focus();
+
+ let topWindow = Services.wm.getMostRecentWindow("navigator:browser");
+ is(topWindow, gNewWindow,
+ "The second window is on top.");
+
+ let isActive = promise.defer();
+ let isLoaded = promise.defer();
+
+ promise.all([isActive.promise, isLoaded.promise]).then(() => {
+ gClient.listTabs(aResponse => {
+ is(aResponse.selected, 2,
+ "The second tab is selected.");
+
+ deferred.resolve();
+ });
+ });
+
+ if (Services.focus.activeWindow != gNewWindow) {
+ gNewWindow.addEventListener("activate", function onActivate(aEvent) {
+ if (aEvent.target != gNewWindow) {
+ return;
+ }
+ gNewWindow.removeEventListener("activate", onActivate, true);
+ isActive.resolve();
+ }, true);
+ } else {
+ isActive.resolve();
+ }
+
+ let contentLocation = gNewWindow.content.location.href;
+ if (contentLocation != TAB2_URL) {
+ gNewWindow.document.addEventListener("load", function onLoad(aEvent) {
+ if (aEvent.target.documentURI != TAB2_URL) {
+ return;
+ }
+ gNewWindow.document.removeEventListener("load", onLoad, true);
+ isLoaded.resolve();
+ }, true);
+ } else {
+ isLoaded.resolve();
+ }
+
+ return deferred.promise;
+}
+
+function testFocusFirst() {
+ let deferred = promise.defer();
+
+ once(window.content, "focus").then(() => {
+ gClient.listTabs(aResponse => {
+ is(aResponse.selected, 1,
+ "The first tab is selected after focusing on it.");
+
+ deferred.resolve();
+ });
+ });
+
+ window.content.focus();
+
+ return deferred.promise;
+}
+
+function testRemoveTab() {
+ let deferred = promise.defer();
+
+ gNewWindow.close();
+
+ // give it time to close
+ executeSoon(function () { continue_remove_tab(deferred); });
+ return deferred.promise;
+}
+
+function continue_remove_tab(deferred)
+{
+ removeTab(gNewTab);
+
+ gClient.listTabs(aResponse => {
+ // Verify that tabs are no longer included in listTabs.
+ let foundTab1 = aResponse.tabs.some(aGrip => aGrip.url == TAB1_URL);
+ let foundTab2 = aResponse.tabs.some(aGrip => aGrip.url == TAB2_URL);
+ ok(!foundTab1, "Tab1 should be gone.");
+ ok(!foundTab2, "Tab2 should be gone.");
+
+ is(aResponse.selected, 0,
+ "The original tab is selected.");
+
+ deferred.resolve();
+ });
+}
+
+registerCleanupFunction(function () {
+ gNewTab = null;
+ gNewWindow = null;
+ gClient = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_navigation.js b/devtools/client/debugger/test/mochitest/browser_dbg_navigation.js
new file mode 100644
index 000000000..df48601e6
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_navigation.js
@@ -0,0 +1,75 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Check tab attach/navigation.
+ */
+
+const TAB1_URL = EXAMPLE_URL + "doc_empty-tab-01.html";
+const TAB2_URL = EXAMPLE_URL + "doc_empty-tab-02.html";
+
+var gClient;
+
+function test() {
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+
+ let transport = DebuggerServer.connectPipe();
+ gClient = new DebuggerClient(transport);
+ gClient.connect().then(([aType, aTraits]) => {
+ is(aType, "browser",
+ "Root actor should identify itself as a browser.");
+
+ addTab(TAB1_URL)
+ .then(() => attachTabActorForUrl(gClient, TAB1_URL))
+ .then(testNavigate)
+ .then(testDetach)
+ .then(finish)
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+ });
+}
+
+function testNavigate([aGrip, aResponse]) {
+ let outstanding = [promise.defer(), promise.defer()];
+
+ gClient.addListener("tabNavigated", function onTabNavigated(aEvent, aPacket) {
+ is(aPacket.url, TAB2_URL,
+ "Got a tab navigation notification.");
+
+ if (aPacket.state == "start") {
+ ok(true, "Tab started to navigate.");
+ outstanding[0].resolve();
+ } else {
+ ok(true, "Tab finished navigating.");
+ gClient.removeListener("tabNavigated", onTabNavigated);
+ outstanding[1].resolve();
+ }
+ });
+
+ gBrowser.selectedBrowser.loadURI(TAB2_URL);
+ return promise.all(outstanding.map(e => e.promise))
+ .then(() => aGrip.actor);
+}
+
+function testDetach(aActor) {
+ let deferred = promise.defer();
+
+ gClient.addOneTimeListener("tabDetached", (aType, aPacket) => {
+ ok(true, "Got a tab detach notification.");
+ is(aPacket.from, aActor, "tab detach message comes from the expected actor");
+ deferred.resolve(gClient.close());
+ });
+
+ removeTab(gBrowser.selectedTab);
+ return deferred.promise;
+}
+
+registerCleanupFunction(function () {
+ gClient = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_no-dangling-breakpoints.js b/devtools/client/debugger/test/mochitest/browser_dbg_no-dangling-breakpoints.js
new file mode 100644
index 000000000..b55e0132d
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_no-dangling-breakpoints.js
@@ -0,0 +1,25 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Bug 1201008 - Make sure you can't set a breakpoint in a blank
+ * editor
+ */
+
+function test() {
+ initDebugger('data:text/html,hi', { source: null }).then(([aTab,, aPanel]) => {
+ const gPanel = aPanel;
+ const gDebugger = gPanel.panelWin;
+
+ Task.spawn(function* () {
+ const editor = gDebugger.DebuggerView.editor;
+ editor.emit("gutterClick", 0);
+ is(editor.getBreakpoints().length, 0,
+ "A breakpoint should not exist");
+
+ closeDebuggerAndFinish(gPanel);
+ });
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_no-page-sources.js b/devtools/client/debugger/test/mochitest/browser_dbg_no-page-sources.js
new file mode 100644
index 000000000..ff7e6f04a
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_no-page-sources.js
@@ -0,0 +1,54 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure the right text shows when the page has no sources.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_no-page-sources.html";
+
+var gTab, gDebuggee, gPanel, gDebugger;
+var gEditor, gSources;
+
+function test() {
+ initDebugger(TAB_URL, { source: null }).then(([aTab, aDebuggee, aPanel]) => {
+ gTab = aTab;
+ gDebuggee = aDebuggee;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gEditor = gDebugger.DebuggerView.editor;
+ gSources = gDebugger.DebuggerView.Sources;
+ const constants = gDebugger.require("./content/constants");
+
+ reloadActiveTab(gPanel);
+ waitForNavigation(gPanel)
+ .then(testSourcesEmptyText)
+ .then(() => closeDebuggerAndFinish(gPanel))
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+ });
+}
+
+function testSourcesEmptyText() {
+ is(gSources.itemCount, 0,
+ "Found no entries in the sources widget.");
+
+ is(gEditor.getText().length, 0,
+ "The source editor should not have any text displayed.");
+
+ is(gDebugger.document.querySelector("#sources .side-menu-widget-empty-text").getAttribute("value"),
+ gDebugger.L10N.getStr("noSourcesText"),
+ "The sources widget should now display 'This page has no sources'.");
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gDebuggee = null;
+ gPanel = null;
+ gDebugger = null;
+ gEditor = null;
+ gSources = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_on-pause-highlight.js b/devtools/client/debugger/test/mochitest/browser_dbg_on-pause-highlight.js
new file mode 100644
index 000000000..07e2360af
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_on-pause-highlight.js
@@ -0,0 +1,86 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests that debugger's tab is highlighted when it is paused and not the
+ * currently selected tool.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_recursion-stack.html";
+
+var gTab, gPanel, gDebugger;
+var gToolbox, gToolboxTab;
+
+function test() {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gToolbox = gPanel._toolbox;
+ gToolboxTab = gToolbox.doc.getElementById("toolbox-tab-jsdebugger");
+
+ testPause();
+ });
+}
+
+function testPause() {
+ is(gDebugger.gThreadClient.paused, false,
+ "Should be running after starting test.");
+
+ gDebugger.gThreadClient.addOneTimeListener("paused", () => {
+ gToolbox.selectTool("webconsole").then(() => {
+ ok(gToolboxTab.hasAttribute("highlighted") &&
+ gToolboxTab.getAttribute("highlighted") == "true",
+ "The highlighted class is present");
+ ok(!gToolboxTab.hasAttribute("selected") ||
+ gToolboxTab.getAttribute("selected") != "true",
+ "The tab is not selected");
+ }).then(() => gToolbox.selectTool("jsdebugger")).then(() => {
+ ok(gToolboxTab.hasAttribute("highlighted") &&
+ gToolboxTab.getAttribute("highlighted") == "true",
+ "The highlighted class is present");
+ ok(gToolboxTab.hasAttribute("selected") &&
+ gToolboxTab.getAttribute("selected") == "true",
+ "...and the tab is selected, so the glow will not be present.");
+ }).then(testResume);
+ });
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ gDebugger.document.getElementById("resume"),
+ gDebugger);
+
+ // Evaluate a script to fully pause the debugger
+ once(gDebugger.gClient, "willInterrupt").then(() => {
+ evalInTab(gTab, "1+1;");
+ });
+}
+
+function testResume() {
+ gDebugger.gThreadClient.addOneTimeListener("resumed", () => {
+ gToolbox.selectTool("webconsole").then(() => {
+ ok(!gToolboxTab.classList.contains("highlighted"),
+ "The highlighted class is not present now after the resume");
+ ok(!gToolboxTab.hasAttribute("selected") ||
+ gToolboxTab.getAttribute("selected") != "true",
+ "The tab is not selected");
+ }).then(() => closeDebuggerAndFinish(gPanel));
+ });
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ gDebugger.document.getElementById("resume"),
+ gDebugger);
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gToolbox = null;
+ gToolboxTab = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_on-pause-raise.js b/devtools/client/debugger/test/mochitest/browser_dbg_on-pause-raise.js
new file mode 100644
index 000000000..6f6f15247
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_on-pause-raise.js
@@ -0,0 +1,120 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests that the toolbox is raised when the debugger gets paused.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_recursion-stack.html";
+
+add_task(function *() {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ let [tab,, panel] = yield initDebugger(TAB_URL, options);
+ let panelWin = panel.panelWin;
+ let toolbox = panel._toolbox;
+ let toolboxTab = toolbox.doc.getElementById("toolbox-tab-jsdebugger");
+
+ let newTab = yield addTab(TAB_URL);
+ isnot(newTab, tab,
+ "The newly added tab is different from the debugger's tab.");
+ is(gBrowser.selectedTab, newTab,
+ "Debugger's tab is not the selected tab.");
+
+ info("Run tests against bottom host.");
+ yield testPause();
+ yield testResume();
+
+ // testResume selected the console, select back the debugger.
+ yield toolbox.selectTool("jsdebugger");
+
+ info("Switching to a toolbox window host.");
+ yield toolbox.switchHost(Toolbox.HostType.WINDOW);
+
+ info("Run tests against window host.");
+ yield testPause();
+ yield testResume();
+
+ info("Cleanup after the test.");
+ yield toolbox.switchHost(Toolbox.HostType.BOTTOM);
+ yield closeDebuggerAndFinish(panel);
+
+ function* testPause() {
+ is(panelWin.gThreadClient.paused, false,
+ "Should be running after starting the test.");
+
+ let onFocus, onTabSelect;
+ if (toolbox.hostType == Toolbox.HostType.WINDOW) {
+ onFocus = new Promise(done => {
+ toolbox.win.parent.addEventListener("focus", function onFocus() {
+ toolbox.win.parent.removeEventListener("focus", onFocus, true);
+ done();
+ }, true);
+ });
+ } else {
+ onTabSelect = new Promise(done => {
+ tab.parentNode.addEventListener("TabSelect", function listener({type}) {
+ tab.parentNode.removeEventListener(type, listener);
+ done();
+ });
+ });
+ }
+
+ let onPaused = waitForPause(panelWin.gThreadClient);
+
+ // Evaluate a script to fully pause the debugger
+ evalInTab(tab, "debugger;");
+
+ yield onPaused;
+ yield onFocus;
+ yield onTabSelect;
+
+ if (toolbox.hostType != Toolbox.HostType.WINDOW) {
+ is(gBrowser.selectedTab, tab,
+ "Debugger's tab got selected.");
+ }
+
+ yield toolbox.selectTool("webconsole");
+ ok(toolboxTab.hasAttribute("highlighted") &&
+ toolboxTab.getAttribute("highlighted") == "true",
+ "The highlighted class is present");
+ ok(!toolboxTab.hasAttribute("selected") ||
+ toolboxTab.getAttribute("selected") != "true",
+ "The tab is not selected");
+ yield toolbox.selectTool("jsdebugger");
+ ok(toolboxTab.hasAttribute("highlighted") &&
+ toolboxTab.getAttribute("highlighted") == "true",
+ "The highlighted class is present");
+ ok(toolboxTab.hasAttribute("selected") &&
+ toolboxTab.getAttribute("selected") == "true",
+ "...and the tab is selected, so the glow will not be present.");
+ }
+
+ function* testResume() {
+ let onPaused = waitForEvent(panelWin.gThreadClient, "resumed");
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ panelWin.document.getElementById("resume"),
+ panelWin);
+
+ yield onPaused;
+
+ yield toolbox.selectTool("webconsole");
+ ok(!toolboxTab.hasAttribute("highlighted") ||
+ toolboxTab.getAttribute("highlighted") != "true",
+ "The highlighted class is not present now after the resume");
+ ok(!toolboxTab.hasAttribute("selected") ||
+ toolboxTab.getAttribute("selected") != "true",
+ "The tab is not selected");
+ }
+});
+
+registerCleanupFunction(function () {
+ // Revert to the default toolbox host, so that the following tests proceed
+ // normally and not inside a non-default host.
+ Services.prefs.setCharPref("devtools.toolbox.host", Toolbox.HostType.BOTTOM);
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_optimized-out-vars.js b/devtools/client/debugger/test/mochitest/browser_dbg_optimized-out-vars.js
new file mode 100644
index 000000000..ba60a0068
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_optimized-out-vars.js
@@ -0,0 +1,50 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Test that optimized out variables aren't present in the variables view.
+
+function test() {
+ Task.spawn(function* () {
+ const TAB_URL = EXAMPLE_URL + "doc_closure-optimized-out.html";
+ let gDebugger, sources;
+
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ let [tab,, panel] = yield initDebugger(TAB_URL, options);
+ gDebugger = panel.panelWin;
+ sources = gDebugger.DebuggerView.Sources;
+
+ yield panel.addBreakpoint({ actor: sources.values[0],
+ line: 18 });
+ yield ensureThreadClientState(panel, "resumed");
+
+ // Spin the event loop before causing the debuggee to pause, to allow
+ // this function to return first.
+ generateMouseClickInTab(tab, "content.document.querySelector('button')");
+
+ yield waitForDebuggerEvents(panel, gDebugger.EVENTS.FETCHED_SCOPES);
+ let gVars = gDebugger.DebuggerView.Variables;
+ let outerScope = gVars.getScopeAtIndex(1);
+ outerScope.expand();
+
+ let upvarVar = outerScope.get("upvar");
+ ok(upvarVar, "The variable `upvar` is shown.");
+ is(upvarVar.target.querySelector(".value").getAttribute("value"),
+ gDebugger.L10N.getStr("variablesViewOptimizedOut"),
+ "Should show the optimized out message for upvar.");
+
+ let argVar = outerScope.get("arg");
+ is(argVar.target.querySelector(".name").getAttribute("value"), "arg",
+ "Should have the right property name for |arg|.");
+ is(argVar.target.querySelector(".value").getAttribute("value"), 42,
+ "Should have the right property value for |arg|.");
+
+ yield resumeDebuggerThenCloseAndFinish(panel);
+ }).then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_panel-size.js b/devtools/client/debugger/test/mochitest/browser_dbg_panel-size.js
new file mode 100644
index 000000000..32e2df0c7
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_panel-size.js
@@ -0,0 +1,88 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Test that the sources and instruments panels widths are properly
+ * remembered when the debugger closes.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_recursion-stack.html";
+
+function test() {
+ let gTab, gPanel, gDebugger;
+ let gPrefs, gSources, gInstruments;
+
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gPrefs = gDebugger.Prefs;
+ gSources = gDebugger.document.getElementById("workers-and-sources-pane");
+ gInstruments = gDebugger.document.getElementById("instruments-pane");
+
+ performTest();
+ });
+
+ function performTest() {
+ let preferredWsw = Services.prefs.getIntPref("devtools.debugger.ui.panes-workers-and-sources-width");
+ let preferredIw = Services.prefs.getIntPref("devtools.debugger.ui.panes-instruments-width");
+ let someWidth1, someWidth2;
+
+ do {
+ someWidth1 = parseInt(Math.random() * 200) + 100;
+ someWidth2 = parseInt(Math.random() * 300) + 100;
+ } while ((someWidth1 == preferredWsw) || (someWidth2 == preferredIw));
+
+ info("Preferred sources width: " + preferredWsw);
+ info("Preferred instruments width: " + preferredIw);
+ info("Generated sources width: " + someWidth1);
+ info("Generated instruments width: " + someWidth2);
+
+ ok(gPrefs.workersAndSourcesWidth,
+ "The debugger preferences should have a saved workersAndSourcesWidth value.");
+ ok(gPrefs.instrumentsWidth,
+ "The debugger preferences should have a saved instrumentsWidth value.");
+
+ is(gPrefs.workersAndSourcesWidth, preferredWsw,
+ "The debugger preferences should have a correct workersAndSourcesWidth value.");
+ is(gPrefs.instrumentsWidth, preferredIw,
+ "The debugger preferences should have a correct instrumentsWidth value.");
+
+ is(gSources.getAttribute("width"), gPrefs.workersAndSourcesWidth,
+ "The workers and sources pane width should be the same as the preferred value.");
+ is(gInstruments.getAttribute("width"), gPrefs.instrumentsWidth,
+ "The instruments pane width should be the same as the preferred value.");
+
+ gSources.setAttribute("width", someWidth1);
+ gInstruments.setAttribute("width", someWidth2);
+
+ is(gPrefs.workersAndSourcesWidth, preferredWsw,
+ "The workers and sources pane width pref should still be the same as the preferred value.");
+ is(gPrefs.instrumentsWidth, preferredIw,
+ "The instruments pane width pref should still be the same as the preferred value.");
+
+ isnot(gSources.getAttribute("width"), gPrefs.workersAndSourcesWidth,
+ "The workers and sources pane width should not be the preferred value anymore.");
+ isnot(gInstruments.getAttribute("width"), gPrefs.instrumentsWidth,
+ "The instruments pane width should not be the preferred value anymore.");
+
+ teardown(gPanel).then(() => {
+ is(gPrefs.workersAndSourcesWidth, someWidth1,
+ "The workers and sources pane width should have been saved by now.");
+ is(gPrefs.instrumentsWidth, someWidth2,
+ "The instruments pane width should have been saved by now.");
+
+ // Cleanup after ourselves!
+ Services.prefs.setIntPref("devtools.debugger.ui.panes-workers-and-sources-width", preferredWsw);
+ Services.prefs.setIntPref("devtools.debugger.ui.panes-instruments-width", preferredIw);
+
+ finish();
+ });
+ }
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_parser-01.js b/devtools/client/debugger/test/mochitest/browser_dbg_parser-01.js
new file mode 100644
index 000000000..8481e2d5f
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_parser-01.js
@@ -0,0 +1,33 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Check that simple JS can be parsed and cached with the reflection API.
+ */
+
+function test() {
+ let { Parser } = Cu.import("resource://devtools/shared/Parser.jsm", {});
+
+ let source = "let x = 42;";
+ let parser = new Parser();
+ let first = parser.get(source);
+ let second = parser.get(source);
+
+ isnot(first, second,
+ "The two syntax trees should be different.");
+
+ let third = parser.get(source, "url");
+ let fourth = parser.get(source, "url");
+
+ isnot(first, third,
+ "The new syntax trees should be different than the old ones.");
+ is(third, fourth,
+ "The new syntax trees were cached once an identifier was specified.");
+
+ is(parser.errors.length, 0,
+ "There should be no errors logged when parsing.");
+
+ finish();
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_parser-02.js b/devtools/client/debugger/test/mochitest/browser_dbg_parser-02.js
new file mode 100644
index 000000000..6cf41b380
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_parser-02.js
@@ -0,0 +1,30 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Check that syntax errors are reported correctly.
+ */
+
+function test() {
+ let { Parser } = Cu.import("resource://devtools/shared/Parser.jsm", {});
+
+ let source = "let x + 42;";
+ let parser = new Parser();
+ // Don't pollute the logs with exceptions that we are going to check anyhow.
+ parser.logExceptions = false;
+ let parsed = parser.get(source);
+
+ ok(parsed,
+ "An object should be returned even though the source had a syntax error.");
+
+ is(parser.errors.length, 1,
+ "There should be one error logged when parsing.");
+ is(parser.errors[0].name, "SyntaxError",
+ "The correct exception was caught.");
+ is(parser.errors[0].message, "missing ; before statement",
+ "The correct exception was caught.");
+
+ finish();
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_parser-03.js b/devtools/client/debugger/test/mochitest/browser_dbg_parser-03.js
new file mode 100644
index 000000000..439df705b
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_parser-03.js
@@ -0,0 +1,79 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Check that JS inside HTML can be separated and parsed correctly.
+ */
+
+function test() {
+ let { Parser } = Cu.import("resource://devtools/shared/Parser.jsm", {});
+
+ let source = [
+ "<!doctype html>",
+ "<head>",
+ "<script>",
+ "let a = 42;",
+ "</script>",
+ "<script type='text/javascript'>",
+ "let b = 42;",
+ "</script>",
+ "<script type='text/javascript;version=1.8'>",
+ "let c = 42;",
+ "</script>",
+ "</head>"
+ ].join("\n");
+ let parser = new Parser();
+ let parsed = parser.get(source);
+
+ ok(parsed,
+ "HTML code should be parsed correctly.");
+ is(parser.errors.length, 0,
+ "There should be no errors logged when parsing.");
+
+ is(parsed.scriptCount, 3,
+ "There should be 3 scripts parsed in the parent HTML source.");
+
+ is(parsed.getScriptInfo(0).toSource(), "({start:-1, length:-1, index:-1})",
+ "There is no script at the beginning of the parent source.");
+ is(parsed.getScriptInfo(source.length - 1).toSource(), "({start:-1, length:-1, index:-1})",
+ "There is no script at the end of the parent source.");
+
+ is(parsed.getScriptInfo(source.indexOf("let a")).toSource(), "({start:31, length:13, index:0})",
+ "The first script was located correctly.");
+ is(parsed.getScriptInfo(source.indexOf("let b")).toSource(), "({start:85, length:13, index:1})",
+ "The second script was located correctly.");
+ is(parsed.getScriptInfo(source.indexOf("let c")).toSource(), "({start:151, length:13, index:2})",
+ "The third script was located correctly.");
+
+ is(parsed.getScriptInfo(source.indexOf("let a") - 1).toSource(), "({start:31, length:13, index:0})",
+ "The left edge of the first script was interpreted correctly.");
+ is(parsed.getScriptInfo(source.indexOf("let b") - 1).toSource(), "({start:85, length:13, index:1})",
+ "The left edge of the second script was interpreted correctly.");
+ is(parsed.getScriptInfo(source.indexOf("let c") - 1).toSource(), "({start:151, length:13, index:2})",
+ "The left edge of the third script was interpreted correctly.");
+
+ is(parsed.getScriptInfo(source.indexOf("let a") - 2).toSource(), "({start:-1, length:-1, index:-1})",
+ "The left outside of the first script was interpreted correctly.");
+ is(parsed.getScriptInfo(source.indexOf("let b") - 2).toSource(), "({start:-1, length:-1, index:-1})",
+ "The left outside of the second script was interpreted correctly.");
+ is(parsed.getScriptInfo(source.indexOf("let c") - 2).toSource(), "({start:-1, length:-1, index:-1})",
+ "The left outside of the third script was interpreted correctly.");
+
+ is(parsed.getScriptInfo(source.indexOf("let a") + 12).toSource(), "({start:31, length:13, index:0})",
+ "The right edge of the first script was interpreted correctly.");
+ is(parsed.getScriptInfo(source.indexOf("let b") + 12).toSource(), "({start:85, length:13, index:1})",
+ "The right edge of the second script was interpreted correctly.");
+ is(parsed.getScriptInfo(source.indexOf("let c") + 12).toSource(), "({start:151, length:13, index:2})",
+ "The right edge of the third script was interpreted correctly.");
+
+ is(parsed.getScriptInfo(source.indexOf("let a") + 13).toSource(), "({start:-1, length:-1, index:-1})",
+ "The right outside of the first script was interpreted correctly.");
+ is(parsed.getScriptInfo(source.indexOf("let b") + 13).toSource(), "({start:-1, length:-1, index:-1})",
+ "The right outside of the second script was interpreted correctly.");
+ is(parsed.getScriptInfo(source.indexOf("let c") + 13).toSource(), "({start:-1, length:-1, index:-1})",
+ "The right outside of the third script was interpreted correctly.");
+
+ finish();
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_parser-04.js b/devtools/client/debugger/test/mochitest/browser_dbg_parser-04.js
new file mode 100644
index 000000000..b6ae0dfb6
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_parser-04.js
@@ -0,0 +1,58 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Check that faulty JS inside HTML can be separated and identified correctly.
+ */
+
+function test() {
+ let { Parser } = Cu.import("resource://devtools/shared/Parser.jsm", {});
+
+ let source = [
+ "<!doctype html>",
+ "<head>",
+ "<SCRIPT>",
+ "let a + 42;",
+ "</SCRIPT>",
+ "<script type='text/javascript'>",
+ "let b = 42;",
+ "</SCRIPT>",
+ "<script type='text/javascript;version=1.8'>",
+ "let c + 42;",
+ "</SCRIPT>",
+ "</head>"
+ ].join("\n");
+ let parser = new Parser();
+ // Don't pollute the logs with exceptions that we are going to check anyhow.
+ parser.logExceptions = false;
+ let parsed = parser.get(source);
+
+ ok(parsed,
+ "HTML code should be parsed correctly.");
+ is(parser.errors.length, 2,
+ "There should be two errors logged when parsing.");
+
+ is(parser.errors[0].name, "SyntaxError",
+ "The correct first exception was caught.");
+ is(parser.errors[0].message, "missing ; before statement",
+ "The correct first exception was caught.");
+
+ is(parser.errors[1].name, "SyntaxError",
+ "The correct second exception was caught.");
+ is(parser.errors[1].message, "missing ; before statement",
+ "The correct second exception was caught.");
+
+ is(parsed.scriptCount, 1,
+ "There should be 1 script parsed in the parent HTML source.");
+
+ is(parsed.getScriptInfo(source.indexOf("let a")).toSource(), "({start:-1, length:-1, index:-1})",
+ "The first script shouldn't be considered valid.");
+ is(parsed.getScriptInfo(source.indexOf("let b")).toSource(), "({start:85, length:13, index:0})",
+ "The second script was located correctly.");
+ is(parsed.getScriptInfo(source.indexOf("let c")).toSource(), "({start:-1, length:-1, index:-1})",
+ "The third script shouldn't be considered valid.");
+
+ finish();
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_parser-05.js b/devtools/client/debugger/test/mochitest/browser_dbg_parser-05.js
new file mode 100644
index 000000000..b34d3952a
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_parser-05.js
@@ -0,0 +1,45 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Check that JS code containing strings that might look like <script> tags
+ * inside an HTML source is parsed correctly.
+ */
+
+function test() {
+ let { Parser } = Cu.import("resource://devtools/shared/Parser.jsm", {});
+
+ let source = [
+ "let a = [];",
+ "a.push('<script>');",
+ "a.push('var a = 42;');",
+ "a.push('</script>');",
+ "a.push('<script type=\"text/javascript\">');",
+ "a.push('var b = 42;');",
+ "a.push('</script>');",
+ "a.push('<script type=\"text/javascript;version=1.8\">');",
+ "a.push('var c = 42;');",
+ "a.push('</script>');"
+ ].join("\n");
+ let parser = new Parser();
+ let parsed = parser.get(source);
+
+ ok(parsed,
+ "The javascript code should be parsed correctly.");
+ is(parser.errors.length, 0,
+ "There should be no errors logged when parsing.");
+
+ is(parsed.scriptCount, 1,
+ "There should be 1 script parsed in the parent source.");
+
+ is(parsed.getScriptInfo(source.indexOf("let a")).toSource(), "({start:0, length:261, index:0})",
+ "The script location is correct (1).");
+ is(parsed.getScriptInfo(source.indexOf("<script>")).toSource(), "({start:0, length:261, index:0})",
+ "The script location is correct (2).");
+ is(parsed.getScriptInfo(source.indexOf("</script>")).toSource(), "({start:0, length:261, index:0})",
+ "The script location is correct (3).");
+
+ finish();
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_parser-06.js b/devtools/client/debugger/test/mochitest/browser_dbg_parser-06.js
new file mode 100644
index 000000000..4e5583e00
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_parser-06.js
@@ -0,0 +1,80 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Check that some potentially problematic identifier nodes have the
+ * right location information attached.
+ */
+
+function test() {
+ let { Parser, ParserHelpers, SyntaxTreeVisitor } =
+ Cu.import("resource://devtools/shared/Parser.jsm", {});
+
+ function verify(source, predicate, [sline, scol], [eline, ecol]) {
+ let ast = Parser.reflectionAPI.parse(source);
+ let node = SyntaxTreeVisitor.filter(ast, predicate).pop();
+ let loc = ParserHelpers.getNodeLocation(node);
+
+ is(loc.start.toSource(), { line: sline, column: scol }.toSource(),
+ "The start location was correct for the identifier in: '" + source + "'.");
+ is(loc.end.toSource(), { line: eline, column: ecol }.toSource(),
+ "The end location was correct for the identifier in: '" + source + "'.");
+ }
+
+ // FunctionDeclarations and FunctionExpressions.
+
+ // The location is unavailable for the identifier node "foo".
+ verify("function foo(){}", e => e.name == "foo", [1, 9], [1, 12]);
+ verify("\nfunction\nfoo\n(\n)\n{\n}\n", e => e.name == "foo", [3, 0], [3, 3]);
+
+ verify("({bar:function foo(){}})", e => e.name == "foo", [1, 15], [1, 18]);
+ verify("(\n{\nbar\n:\nfunction\nfoo\n(\n)\n{\n}\n}\n)", e => e.name == "foo", [6, 0], [6, 3]);
+
+ // Just to be sure, check the identifier node "bar" as well.
+ verify("({bar:function foo(){}})", e => e.name == "bar", [1, 2], [1, 5]);
+ verify("(\n{\nbar\n:\nfunction\nfoo\n(\n)\n{\n}\n}\n)", e => e.name == "bar", [3, 0], [3, 3]);
+
+ // MemberExpressions.
+
+ // The location is unavailable for the identifier node "bar".
+ verify("foo.bar", e => e.name == "bar", [1, 4], [1, 7]);
+ verify("\nfoo\n.\nbar\n", e => e.name == "bar", [4, 0], [4, 3]);
+
+ // Just to be sure, check the identifier node "foo" as well.
+ verify("foo.bar", e => e.name == "foo", [1, 0], [1, 3]);
+ verify("\nfoo\n.\nbar\n", e => e.name == "foo", [2, 0], [2, 3]);
+
+ // VariableDeclarator
+
+ // The location is incorrect for the identifier node "foo".
+ verify("let foo = bar", e => e.name == "foo", [1, 4], [1, 7]);
+ verify("\nlet\nfoo\n=\nbar\n", e => e.name == "foo", [3, 0], [3, 3]);
+
+ // Just to be sure, check the identifier node "bar" as well.
+ verify("let foo = bar", e => e.name == "bar", [1, 10], [1, 13]);
+ verify("\nlet\nfoo\n=\nbar\n", e => e.name == "bar", [5, 0], [5, 3]);
+
+ // Just to be sure, check AssignmentExpreesions as well.
+ verify("foo = bar", e => e.name == "foo", [1, 0], [1, 3]);
+ verify("\nfoo\n=\nbar\n", e => e.name == "foo", [2, 0], [2, 3]);
+ verify("foo = bar", e => e.name == "bar", [1, 6], [1, 9]);
+ verify("\nfoo\n=\nbar\n", e => e.name == "bar", [4, 0], [4, 3]);
+
+ // LabeledStatement, BreakStatement and ContinueStatement, because it's 1968 again
+
+ verify("foo: bar", e => e.name == "foo", [1, 0], [1, 3]);
+ verify("\nfoo\n:\nbar\n", e => e.name == "foo", [2, 0], [2, 3]);
+
+ verify("foo: for(;;) break foo", e => e.name == "foo", [1, 19], [1, 22]);
+ verify("\nfoo\n:\nfor(\n;\n;\n)\nbreak\nfoo\n", e => e.name == "foo", [9, 0], [9, 3]);
+
+ verify("foo: bar", e => e.name == "foo", [1, 0], [1, 3]);
+ verify("\nfoo\n:\nbar\n", e => e.name == "foo", [2, 0], [2, 3]);
+
+ verify("foo: for(;;) continue foo", e => e.name == "foo", [1, 22], [1, 25]);
+ verify("\nfoo\n:\nfor(\n;\n;\n)\ncontinue\nfoo\n", e => e.name == "foo", [9, 0], [9, 3]);
+
+ finish();
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_parser-07.js b/devtools/client/debugger/test/mochitest/browser_dbg_parser-07.js
new file mode 100644
index 000000000..bea913a9e
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_parser-07.js
@@ -0,0 +1,57 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Check that nodes with locaiton information attached can be properly
+ * verified for containing lines and columns.
+ */
+
+function test() {
+ let { ParserHelpers } = Cu.import("resource://devtools/shared/Parser.jsm", {});
+
+ let node1 = { loc: {
+ start: { line: 1, column: 10 },
+ end: { line: 10, column: 1 }
+ }};
+ let node2 = { loc: {
+ start: { line: 1, column: 10 },
+ end: { line: 1, column: 20 }
+ }};
+
+ ok(ParserHelpers.nodeContainsLine(node1, 1), "1st check.");
+ ok(ParserHelpers.nodeContainsLine(node1, 5), "2nd check.");
+ ok(ParserHelpers.nodeContainsLine(node1, 10), "3rd check.");
+
+ ok(!ParserHelpers.nodeContainsLine(node1, 0), "4th check.");
+ ok(!ParserHelpers.nodeContainsLine(node1, 11), "5th check.");
+
+ ok(ParserHelpers.nodeContainsLine(node2, 1), "6th check.");
+ ok(!ParserHelpers.nodeContainsLine(node2, 0), "7th check.");
+ ok(!ParserHelpers.nodeContainsLine(node2, 2), "8th check.");
+
+ ok(!ParserHelpers.nodeContainsPoint(node1, 1, 10), "9th check.");
+ ok(!ParserHelpers.nodeContainsPoint(node1, 10, 1), "10th check.");
+
+ ok(!ParserHelpers.nodeContainsPoint(node1, 0, 10), "11th check.");
+ ok(!ParserHelpers.nodeContainsPoint(node1, 11, 1), "12th check.");
+
+ ok(!ParserHelpers.nodeContainsPoint(node1, 1, 9), "13th check.");
+ ok(!ParserHelpers.nodeContainsPoint(node1, 10, 2), "14th check.");
+
+ ok(ParserHelpers.nodeContainsPoint(node2, 1, 10), "15th check.");
+ ok(ParserHelpers.nodeContainsPoint(node2, 1, 15), "16th check.");
+ ok(ParserHelpers.nodeContainsPoint(node2, 1, 20), "17th check.");
+
+ ok(!ParserHelpers.nodeContainsPoint(node2, 0, 10), "18th check.");
+ ok(!ParserHelpers.nodeContainsPoint(node2, 2, 20), "19th check.");
+
+ ok(!ParserHelpers.nodeContainsPoint(node2, 0, 9), "20th check.");
+ ok(!ParserHelpers.nodeContainsPoint(node2, 2, 21), "21th check.");
+
+ ok(!ParserHelpers.nodeContainsPoint(node2, 1, 9), "22th check.");
+ ok(!ParserHelpers.nodeContainsPoint(node2, 1, 21), "23th check.");
+
+ finish();
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_parser-08.js b/devtools/client/debugger/test/mochitest/browser_dbg_parser-08.js
new file mode 100644
index 000000000..624f3c293
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_parser-08.js
@@ -0,0 +1,291 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Test that inferring anonymous function information is done correctly.
+ */
+
+function test() {
+ let { Parser, ParserHelpers, SyntaxTreeVisitor } =
+ Cu.import("resource://devtools/shared/Parser.jsm", {});
+
+ function verify(source, predicate, details) {
+ let { name, chain } = details;
+ let [[sline, scol], [eline, ecol]] = details.loc;
+ let ast = Parser.reflectionAPI.parse(source);
+ let node = SyntaxTreeVisitor.filter(ast, predicate).pop();
+ let info = ParserHelpers.inferFunctionExpressionInfo(node);
+
+ is(info.name, name,
+ "The function expression assignment property name is correct.");
+ is(chain ? info.chain.toSource() : info.chain, chain ? chain.toSource() : chain,
+ "The function expression assignment property chain is correct.");
+ is(info.loc.start.toSource(), { line: sline, column: scol }.toSource(),
+ "The start location was correct for the identifier in: '" + source + "'.");
+ is(info.loc.end.toSource(), { line: eline, column: ecol }.toSource(),
+ "The end location was correct for the identifier in: '" + source + "'.");
+ }
+
+ // VariableDeclarator
+
+ verify("var foo=function(){}", e => e.type == "FunctionExpression", {
+ name: "foo",
+ chain: null,
+ loc: [[1, 4], [1, 7]]
+ });
+ verify("\nvar\nfoo\n=\nfunction\n(\n)\n{\n}\n", e => e.type == "FunctionExpression", {
+ name: "foo",
+ chain: null,
+ loc: [[3, 0], [3, 3]]
+ });
+
+ // AssignmentExpression
+
+ verify("foo=function(){}", e => e.type == "FunctionExpression",
+ { name: "foo", chain: [], loc: [[1, 0], [1, 3]] });
+
+ verify("\nfoo\n=\nfunction\n(\n)\n{\n}\n", e => e.type == "FunctionExpression",
+ { name: "foo", chain: [], loc: [[2, 0], [2, 3]] });
+
+ verify("foo.bar=function(){}", e => e.type == "FunctionExpression",
+ { name: "bar", chain: ["foo"], loc: [[1, 0], [1, 7]] });
+
+ verify("\nfoo.bar\n=\nfunction\n(\n)\n{\n}\n", e => e.type == "FunctionExpression",
+ { name: "bar", chain: ["foo"], loc: [[2, 0], [2, 7]] });
+
+ verify("this.foo=function(){}", e => e.type == "FunctionExpression",
+ { name: "foo", chain: ["this"], loc: [[1, 0], [1, 8]] });
+
+ verify("\nthis.foo\n=\nfunction\n(\n)\n{\n}\n", e => e.type == "FunctionExpression",
+ { name: "foo", chain: ["this"], loc: [[2, 0], [2, 8]] });
+
+ verify("this.foo.bar=function(){}", e => e.type == "FunctionExpression",
+ { name: "bar", chain: ["this", "foo"], loc: [[1, 0], [1, 12]] });
+
+ verify("\nthis.foo.bar\n=\nfunction\n(\n)\n{\n}\n", e => e.type == "FunctionExpression",
+ { name: "bar", chain: ["this", "foo"], loc: [[2, 0], [2, 12]] });
+
+ verify("foo.this.bar=function(){}", e => e.type == "FunctionExpression",
+ { name: "bar", chain: ["foo", "this"], loc: [[1, 0], [1, 12]] });
+
+ verify("\nfoo.this.bar\n=\nfunction\n(\n)\n{\n}\n", e => e.type == "FunctionExpression",
+ { name: "bar", chain: ["foo", "this"], loc: [[2, 0], [2, 12]] });
+
+ // ObjectExpression
+
+ verify("({foo:function(){}})", e => e.type == "FunctionExpression",
+ { name: "foo", chain: [], loc: [[1, 2], [1, 5]] });
+
+ verify("(\n{\nfoo\n:\nfunction\n(\n)\n{\n}\n}\n)", e => e.type == "FunctionExpression",
+ { name: "foo", chain: [], loc: [[3, 0], [3, 3]] });
+
+ verify("({foo:{bar:function(){}}})", e => e.type == "FunctionExpression",
+ { name: "bar", chain: ["foo"], loc: [[1, 7], [1, 10]] });
+
+ verify("(\n{\nfoo\n:\n{\nbar\n:\nfunction\n(\n)\n{\n}\n}\n}\n)", e => e.type == "FunctionExpression",
+ { name: "bar", chain: ["foo"], loc: [[6, 0], [6, 3]] });
+
+ // AssignmentExpression + ObjectExpression
+
+ verify("foo={bar:function(){}}", e => e.type == "FunctionExpression",
+ { name: "bar", chain: ["foo"], loc: [[1, 5], [1, 8]] });
+
+ verify("\nfoo\n=\n{\nbar\n:\nfunction\n(\n)\n{\n}\n}\n", e => e.type == "FunctionExpression",
+ { name: "bar", chain: ["foo"], loc: [[5, 0], [5, 3]] });
+
+ verify("foo={bar:{baz:function(){}}}", e => e.type == "FunctionExpression",
+ { name: "baz", chain: ["foo", "bar"], loc: [[1, 10], [1, 13]] });
+
+ verify("\nfoo\n=\n{\nbar\n:\n{\nbaz\n:\nfunction\n(\n)\n{\n}\n}\n}\n", e => e.type == "FunctionExpression",
+ { name: "baz", chain: ["foo", "bar"], loc: [[8, 0], [8, 3]] });
+
+ verify("nested.foo={bar:function(){}}", e => e.type == "FunctionExpression",
+ { name: "bar", chain: ["nested", "foo"], loc: [[1, 12], [1, 15]] });
+
+ verify("\nnested.foo\n=\n{\nbar\n:\nfunction\n(\n)\n{\n}\n}\n", e => e.type == "FunctionExpression",
+ { name: "bar", chain: ["nested", "foo"], loc: [[5, 0], [5, 3]] });
+
+ verify("nested.foo={bar:{baz:function(){}}}", e => e.type == "FunctionExpression",
+ { name: "baz", chain: ["nested", "foo", "bar"], loc: [[1, 17], [1, 20]] });
+
+ verify("\nnested.foo\n=\n{\nbar\n:\n{\nbaz\n:\nfunction\n(\n)\n{\n}\n}\n}\n", e => e.type == "FunctionExpression",
+ { name: "baz", chain: ["nested", "foo", "bar"], loc: [[8, 0], [8, 3]] });
+
+ verify("this.foo={bar:function(){}}", e => e.type == "FunctionExpression",
+ { name: "bar", chain: ["this", "foo"], loc: [[1, 10], [1, 13]] });
+
+ verify("\nthis.foo\n=\n{\nbar\n:\nfunction\n(\n)\n{\n}\n}\n", e => e.type == "FunctionExpression",
+ { name: "bar", chain: ["this", "foo"], loc: [[5, 0], [5, 3]] });
+
+ verify("this.foo={bar:{baz:function(){}}}", e => e.type == "FunctionExpression",
+ { name: "baz", chain: ["this", "foo", "bar"], loc: [[1, 15], [1, 18]] });
+
+ verify("\nthis.foo\n=\n{\nbar\n:\n{\nbaz\n:\nfunction\n(\n)\n{\n}\n}\n}\n", e => e.type == "FunctionExpression",
+ { name: "baz", chain: ["this", "foo", "bar"], loc: [[8, 0], [8, 3]] });
+
+ verify("this.nested.foo={bar:function(){}}", e => e.type == "FunctionExpression",
+ { name: "bar", chain: ["this", "nested", "foo"], loc: [[1, 17], [1, 20]] });
+
+ verify("\nthis.nested.foo\n=\n{\nbar\n:\nfunction\n(\n)\n{\n}\n}\n", e => e.type == "FunctionExpression",
+ { name: "bar", chain: ["this", "nested", "foo"], loc: [[5, 0], [5, 3]] });
+
+ verify("this.nested.foo={bar:{baz:function(){}}}", e => e.type == "FunctionExpression",
+ { name: "baz", chain: ["this", "nested", "foo", "bar"], loc: [[1, 22], [1, 25]] });
+
+ verify("\nthis.nested.foo\n=\n{\nbar\n:\n{\nbaz\n:\nfunction\n(\n)\n{\n}\n}\n}\n", e => e.type == "FunctionExpression",
+ { name: "baz", chain: ["this", "nested", "foo", "bar"], loc: [[8, 0], [8, 3]] });
+
+ verify("nested.this.foo={bar:function(){}}", e => e.type == "FunctionExpression",
+ { name: "bar", chain: ["nested", "this", "foo"], loc: [[1, 17], [1, 20]] });
+
+ verify("\nnested.this.foo\n=\n{\nbar\n:\nfunction\n(\n)\n{\n}\n}\n", e => e.type == "FunctionExpression",
+ { name: "bar", chain: ["nested", "this", "foo"], loc: [[5, 0], [5, 3]] });
+
+ verify("nested.this.foo={bar:{baz:function(){}}}", e => e.type == "FunctionExpression",
+ { name: "baz", chain: ["nested", "this", "foo", "bar"], loc: [[1, 22], [1, 25]] });
+
+ verify("\nnested.this.foo\n=\n{\nbar\n:\n{\nbaz\n:\nfunction\n(\n)\n{\n}\n}\n}\n", e => e.type == "FunctionExpression",
+ { name: "baz", chain: ["nested", "this", "foo", "bar"], loc: [[8, 0], [8, 3]] });
+
+ // VariableDeclarator + AssignmentExpression + ObjectExpression
+
+ verify("let foo={bar:function(){}}", e => e.type == "FunctionExpression",
+ { name: "bar", chain: ["foo"], loc: [[1, 9], [1, 12]] });
+
+ verify("\nlet\nfoo\n=\n{\nbar\n:\nfunction\n(\n)\n{\n}\n}\n", e => e.type == "FunctionExpression",
+ { name: "bar", chain: ["foo"], loc: [[6, 0], [6, 3]] });
+
+ verify("let foo={bar:{baz:function(){}}}", e => e.type == "FunctionExpression",
+ { name: "baz", chain: ["foo", "bar"], loc: [[1, 14], [1, 17]] });
+
+ verify("\nlet\nfoo\n=\n{\nbar\n:\n{\nbaz\n:\nfunction\n(\n)\n{\n}\n}\n}\n", e => e.type == "FunctionExpression",
+ { name: "baz", chain: ["foo", "bar"], loc: [[9, 0], [9, 3]] });
+
+ // New/CallExpression + AssignmentExpression + ObjectExpression
+
+ verify("foo({bar:function(){}})", e => e.type == "FunctionExpression",
+ { name: "bar", chain: [], loc: [[1, 5], [1, 8]] });
+
+ verify("\nfoo\n(\n{\nbar\n:\nfunction\n(\n)\n{\n}\n}\n)\n", e => e.type == "FunctionExpression",
+ { name: "bar", chain: [], loc: [[5, 0], [5, 3]] });
+
+ verify("foo({bar:{baz:function(){}}})", e => e.type == "FunctionExpression",
+ { name: "baz", chain: ["bar"], loc: [[1, 10], [1, 13]] });
+
+ verify("\nfoo\n(\n{\nbar\n:\n{\nbaz\n:\nfunction\n(\n)\n{\n}\n}\n}\n)\n", e => e.type == "FunctionExpression",
+ { name: "baz", chain: ["bar"], loc: [[8, 0], [8, 3]] });
+
+ verify("nested.foo({bar:function(){}})", e => e.type == "FunctionExpression",
+ { name: "bar", chain: [], loc: [[1, 12], [1, 15]] });
+
+ verify("\nnested.foo\n(\n{\nbar\n:\nfunction\n(\n)\n{\n}\n}\n)\n", e => e.type == "FunctionExpression",
+ { name: "bar", chain: [], loc: [[5, 0], [5, 3]] });
+
+ verify("nested.foo({bar:{baz:function(){}}})", e => e.type == "FunctionExpression",
+ { name: "baz", chain: ["bar"], loc: [[1, 17], [1, 20]] });
+
+ verify("\nnested.foo\n(\n{\nbar\n:\n{\nbaz\n:\nfunction\n(\n)\n{\n}\n}\n}\n)\n", e => e.type == "FunctionExpression",
+ { name: "baz", chain: ["bar"], loc: [[8, 0], [8, 3]] });
+
+ verify("this.foo({bar:function(){}})", e => e.type == "FunctionExpression",
+ { name: "bar", chain: [], loc: [[1, 10], [1, 13]] });
+
+ verify("\nthis.foo\n(\n{\nbar\n:\nfunction\n(\n)\n{\n}\n}\n)\n", e => e.type == "FunctionExpression",
+ { name: "bar", chain: [], loc: [[5, 0], [5, 3]] });
+
+ verify("this.foo({bar:{baz:function(){}}})", e => e.type == "FunctionExpression",
+ { name: "baz", chain: ["bar"], loc: [[1, 15], [1, 18]] });
+
+ verify("\nthis.foo\n(\n{\nbar\n:\n{\nbaz\n:\nfunction\n(\n)\n{\n}\n}\n}\n)\n", e => e.type == "FunctionExpression",
+ { name: "baz", chain: ["bar"], loc: [[8, 0], [8, 3]] });
+
+ verify("this.nested.foo({bar:function(){}})", e => e.type == "FunctionExpression",
+ { name: "bar", chain: [], loc: [[1, 17], [1, 20]] });
+
+ verify("\nthis.nested.foo\n(\n{\nbar\n:\nfunction\n(\n)\n{\n}\n}\n)\n", e => e.type == "FunctionExpression",
+ { name: "bar", chain: [], loc: [[5, 0], [5, 3]] });
+
+ verify("this.nested.foo({bar:{baz:function(){}}})", e => e.type == "FunctionExpression",
+ { name: "baz", chain: ["bar"], loc: [[1, 22], [1, 25]] });
+
+ verify("\nthis.nested.foo\n(\n{\nbar\n:\n{\nbaz\n:\nfunction\n(\n)\n{\n}\n}\n}\n)\n", e => e.type == "FunctionExpression",
+ { name: "baz", chain: ["bar"], loc: [[8, 0], [8, 3]] });
+
+ verify("nested.this.foo({bar:function(){}})", e => e.type == "FunctionExpression",
+ { name: "bar", chain: [], loc: [[1, 17], [1, 20]] });
+
+ verify("\nnested.this.foo\n(\n{\nbar\n:\nfunction\n(\n)\n{\n}\n}\n)\n", e => e.type == "FunctionExpression",
+ { name: "bar", chain: [], loc: [[5, 0], [5, 3]] });
+
+ verify("nested.this.foo({bar:{baz:function(){}}})", e => e.type == "FunctionExpression",
+ { name: "baz", chain: ["bar"], loc: [[1, 22], [1, 25]] });
+
+ verify("\nnested.this.foo\n(\n{\nbar\n:\n{\nbaz\n:\nfunction\n(\n)\n{\n}\n}\n}\n)\n", e => e.type == "FunctionExpression",
+ { name: "baz", chain: ["bar"], loc: [[8, 0], [8, 3]] });
+
+ // New/CallExpression + VariableDeclarator + AssignmentExpression + ObjectExpression
+
+ verify("let target=foo({bar:function(){}})", e => e.type == "FunctionExpression",
+ { name: "bar", chain: ["target"], loc: [[1, 16], [1, 19]] });
+
+ verify("\nlet\ntarget=\nfoo\n(\n{\nbar\n:\nfunction\n(\n)\n{\n}\n}\n)\n", e => e.type == "FunctionExpression",
+ { name: "bar", chain: ["target"], loc: [[7, 0], [7, 3]] });
+
+ verify("let target=foo({bar:{baz:function(){}}})", e => e.type == "FunctionExpression",
+ { name: "baz", chain: ["target", "bar"], loc: [[1, 21], [1, 24]] });
+
+ verify("\nlet\ntarget=\nfoo\n(\n{\nbar\n:\n{\nbaz\n:\nfunction\n(\n)\n{\n}\n}\n}\n)\n", e => e.type == "FunctionExpression",
+ { name: "baz", chain: ["target", "bar"], loc: [[10, 0], [10, 3]] });
+
+ verify("let target=nested.foo({bar:function(){}})", e => e.type == "FunctionExpression",
+ { name: "bar", chain: ["target"], loc: [[1, 23], [1, 26]] });
+
+ verify("\nlet\ntarget=\nnested.foo\n(\n{\nbar\n:\nfunction\n(\n)\n{\n}\n}\n)\n", e => e.type == "FunctionExpression",
+ { name: "bar", chain: ["target"], loc: [[7, 0], [7, 3]] });
+
+ verify("let target=nested.foo({bar:{baz:function(){}}})", e => e.type == "FunctionExpression",
+ { name: "baz", chain: ["target", "bar"], loc: [[1, 28], [1, 31]] });
+
+ verify("\nlet\ntarget=\nnested.foo\n(\n{\nbar\n:\n{\nbaz\n:\nfunction\n(\n)\n{\n}\n}\n}\n)\n", e => e.type == "FunctionExpression",
+ { name: "baz", chain: ["target", "bar"], loc: [[10, 0], [10, 3]] });
+
+ verify("let target=this.foo({bar:function(){}})", e => e.type == "FunctionExpression",
+ { name: "bar", chain: ["target"], loc: [[1, 21], [1, 24]] });
+
+ verify("\nlet\ntarget=\nthis.foo\n(\n{\nbar\n:\nfunction\n(\n)\n{\n}\n}\n)\n", e => e.type == "FunctionExpression",
+ { name: "bar", chain: ["target"], loc: [[7, 0], [7, 3]] });
+
+ verify("let target=this.foo({bar:{baz:function(){}}})", e => e.type == "FunctionExpression",
+ { name: "baz", chain: ["target", "bar"], loc: [[1, 26], [1, 29]] });
+
+ verify("\nlet\ntarget=\nthis.foo\n(\n{\nbar\n:\n{\nbaz\n:\nfunction\n(\n)\n{\n}\n}\n}\n)\n", e => e.type == "FunctionExpression",
+ { name: "baz", chain: ["target", "bar"], loc: [[10, 0], [10, 3]] });
+
+ verify("let target=this.nested.foo({bar:function(){}})", e => e.type == "FunctionExpression",
+ { name: "bar", chain: ["target"], loc: [[1, 28], [1, 31]] });
+
+ verify("\nlet\ntarget=\nthis.nested.foo\n(\n{\nbar\n:\nfunction\n(\n)\n{\n}\n}\n)\n", e => e.type == "FunctionExpression",
+ { name: "bar", chain: ["target"], loc: [[7, 0], [7, 3]] });
+
+ verify("let target=this.nested.foo({bar:{baz:function(){}}})", e => e.type == "FunctionExpression",
+ { name: "baz", chain: ["target", "bar"], loc: [[1, 33], [1, 36]] });
+
+ verify("\nlet\ntarget=\nthis.nested.foo\n(\n{\nbar\n:\n{\nbaz\n:\nfunction\n(\n)\n{\n}\n}\n}\n)\n", e => e.type == "FunctionExpression",
+ { name: "baz", chain: ["target", "bar"], loc: [[10, 0], [10, 3]] });
+
+ verify("let target=nested.this.foo({bar:function(){}})", e => e.type == "FunctionExpression",
+ { name: "bar", chain: ["target"], loc: [[1, 28], [1, 31]] });
+
+ verify("\nlet\ntarget=\nnested.this.foo\n(\n{\nbar\n:\nfunction\n(\n)\n{\n}\n}\n)\n", e => e.type == "FunctionExpression",
+ { name: "bar", chain: ["target"], loc: [[7, 0], [7, 3]] });
+
+ verify("let target=nested.this.foo({bar:{baz:function(){}}})", e => e.type == "FunctionExpression",
+ { name: "baz", chain: ["target", "bar"], loc: [[1, 33], [1, 36]] });
+
+ verify("\nlet\ntarget=\nnested.this.foo\n(\n{\nbar\n:\n{\nbaz\n:\nfunction\n(\n)\n{\n}\n}\n}\n)\n", e => e.type == "FunctionExpression",
+ { name: "baz", chain: ["target", "bar"], loc: [[10, 0], [10, 3]] });
+
+ finish();
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_parser-09.js b/devtools/client/debugger/test/mochitest/browser_dbg_parser-09.js
new file mode 100644
index 000000000..2e0ac3b89
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_parser-09.js
@@ -0,0 +1,292 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Test that inferring anonymous function information is done correctly
+ * from arrow expressions.
+ */
+
+function test() {
+ let { Parser, ParserHelpers, SyntaxTreeVisitor } =
+ Cu.import("resource://devtools/shared/Parser.jsm", {});
+
+ function verify(source, predicate, details) {
+ let { name, chain } = details;
+ let [[sline, scol], [eline, ecol]] = details.loc;
+ let ast = Parser.reflectionAPI.parse(source);
+ let node = SyntaxTreeVisitor.filter(ast, predicate).pop();
+ let info = ParserHelpers.inferFunctionExpressionInfo(node);
+
+ is(info.name, name,
+ "The function expression assignment property name is correct.");
+ is(chain ? info.chain.toSource() : info.chain, chain ? chain.toSource() : chain,
+ "The function expression assignment property chain is correct.");
+ is(info.loc.start.toSource(), { line: sline, column: scol }.toSource(),
+ "The start location was correct for the identifier in: '" + source + "'.");
+ is(info.loc.end.toSource(), { line: eline, column: ecol }.toSource(),
+ "The end location was correct for the identifier in: '" + source + "'.");
+ }
+
+ // VariableDeclarator
+
+ verify("var foo=()=>{}", e => e.type == "ArrowFunctionExpression", {
+ name: "foo",
+ chain: null,
+ loc: [[1, 4], [1, 7]]
+ });
+ verify("\nvar\nfoo\n=\n(\n)=>\n{\n}\n", e => e.type == "ArrowFunctionExpression", {
+ name: "foo",
+ chain: null,
+ loc: [[3, 0], [3, 3]]
+ });
+
+ // AssignmentExpression
+
+ verify("foo=()=>{}", e => e.type == "ArrowFunctionExpression",
+ { name: "foo", chain: [], loc: [[1, 0], [1, 3]] });
+
+ verify("\nfoo\n=\n(\n)=>\n{\n}\n", e => e.type == "ArrowFunctionExpression",
+ { name: "foo", chain: [], loc: [[2, 0], [2, 3]] });
+
+ verify("foo.bar=()=>{}", e => e.type == "ArrowFunctionExpression",
+ { name: "bar", chain: ["foo"], loc: [[1, 0], [1, 7]] });
+
+ verify("\nfoo.bar\n=\n(\n)=>\n{\n}\n", e => e.type == "ArrowFunctionExpression",
+ { name: "bar", chain: ["foo"], loc: [[2, 0], [2, 7]] });
+
+ verify("this.foo=()=>{}", e => e.type == "ArrowFunctionExpression",
+ { name: "foo", chain: ["this"], loc: [[1, 0], [1, 8]] });
+
+ verify("\nthis.foo\n=\n(\n)=>\n{\n}\n", e => e.type == "ArrowFunctionExpression",
+ { name: "foo", chain: ["this"], loc: [[2, 0], [2, 8]] });
+
+ verify("this.foo.bar=()=>{}", e => e.type == "ArrowFunctionExpression",
+ { name: "bar", chain: ["this", "foo"], loc: [[1, 0], [1, 12]] });
+
+ verify("\nthis.foo.bar\n=\n(\n)=>\n{\n}\n", e => e.type == "ArrowFunctionExpression",
+ { name: "bar", chain: ["this", "foo"], loc: [[2, 0], [2, 12]] });
+
+ verify("foo.this.bar=()=>{}", e => e.type == "ArrowFunctionExpression",
+ { name: "bar", chain: ["foo", "this"], loc: [[1, 0], [1, 12]] });
+
+ verify("\nfoo.this.bar\n=\n(\n)=>\n{\n}\n", e => e.type == "ArrowFunctionExpression",
+ { name: "bar", chain: ["foo", "this"], loc: [[2, 0], [2, 12]] });
+
+ // ObjectExpression
+
+ verify("({foo:()=>{}})", e => e.type == "ArrowFunctionExpression",
+ { name: "foo", chain: [], loc: [[1, 2], [1, 5]] });
+
+ verify("(\n{\nfoo\n:\n(\n)=>\n{\n}\n}\n)", e => e.type == "ArrowFunctionExpression",
+ { name: "foo", chain: [], loc: [[3, 0], [3, 3]] });
+
+ verify("({foo:{bar:()=>{}}})", e => e.type == "ArrowFunctionExpression",
+ { name: "bar", chain: ["foo"], loc: [[1, 7], [1, 10]] });
+
+ verify("(\n{\nfoo\n:\n{\nbar\n:\n(\n)=>\n{\n}\n}\n}\n)", e => e.type == "ArrowFunctionExpression",
+ { name: "bar", chain: ["foo"], loc: [[6, 0], [6, 3]] });
+
+ // AssignmentExpression + ObjectExpression
+
+ verify("foo={bar:()=>{}}", e => e.type == "ArrowFunctionExpression",
+ { name: "bar", chain: ["foo"], loc: [[1, 5], [1, 8]] });
+
+ verify("\nfoo\n=\n{\nbar\n:\n(\n)=>\n{\n}\n}\n", e => e.type == "ArrowFunctionExpression",
+ { name: "bar", chain: ["foo"], loc: [[5, 0], [5, 3]] });
+
+ verify("foo={bar:{baz:()=>{}}}", e => e.type == "ArrowFunctionExpression",
+ { name: "baz", chain: ["foo", "bar"], loc: [[1, 10], [1, 13]] });
+
+ verify("\nfoo\n=\n{\nbar\n:\n{\nbaz\n:\n(\n)=>\n{\n}\n}\n}\n", e => e.type == "ArrowFunctionExpression",
+ { name: "baz", chain: ["foo", "bar"], loc: [[8, 0], [8, 3]] });
+
+ verify("nested.foo={bar:()=>{}}", e => e.type == "ArrowFunctionExpression",
+ { name: "bar", chain: ["nested", "foo"], loc: [[1, 12], [1, 15]] });
+
+ verify("\nnested.foo\n=\n{\nbar\n:\n(\n)=>\n{\n}\n}\n", e => e.type == "ArrowFunctionExpression",
+ { name: "bar", chain: ["nested", "foo"], loc: [[5, 0], [5, 3]] });
+
+ verify("nested.foo={bar:{baz:()=>{}}}", e => e.type == "ArrowFunctionExpression",
+ { name: "baz", chain: ["nested", "foo", "bar"], loc: [[1, 17], [1, 20]] });
+
+ verify("\nnested.foo\n=\n{\nbar\n:\n{\nbaz\n:\n(\n)=>\n{\n}\n}\n}\n", e => e.type == "ArrowFunctionExpression",
+ { name: "baz", chain: ["nested", "foo", "bar"], loc: [[8, 0], [8, 3]] });
+
+ verify("this.foo={bar:()=>{}}", e => e.type == "ArrowFunctionExpression",
+ { name: "bar", chain: ["this", "foo"], loc: [[1, 10], [1, 13]] });
+
+ verify("\nthis.foo\n=\n{\nbar\n:\n(\n)=>\n{\n}\n}\n", e => e.type == "ArrowFunctionExpression",
+ { name: "bar", chain: ["this", "foo"], loc: [[5, 0], [5, 3]] });
+
+ verify("this.foo={bar:{baz:()=>{}}}", e => e.type == "ArrowFunctionExpression",
+ { name: "baz", chain: ["this", "foo", "bar"], loc: [[1, 15], [1, 18]] });
+
+ verify("\nthis.foo\n=\n{\nbar\n:\n{\nbaz\n:\n(\n)=>\n{\n}\n}\n}\n", e => e.type == "ArrowFunctionExpression",
+ { name: "baz", chain: ["this", "foo", "bar"], loc: [[8, 0], [8, 3]] });
+
+ verify("this.nested.foo={bar:()=>{}}", e => e.type == "ArrowFunctionExpression",
+ { name: "bar", chain: ["this", "nested", "foo"], loc: [[1, 17], [1, 20]] });
+
+ verify("\nthis.nested.foo\n=\n{\nbar\n:\n(\n)=>\n{\n}\n}\n", e => e.type == "ArrowFunctionExpression",
+ { name: "bar", chain: ["this", "nested", "foo"], loc: [[5, 0], [5, 3]] });
+
+ verify("this.nested.foo={bar:{baz:()=>{}}}", e => e.type == "ArrowFunctionExpression",
+ { name: "baz", chain: ["this", "nested", "foo", "bar"], loc: [[1, 22], [1, 25]] });
+
+ verify("\nthis.nested.foo\n=\n{\nbar\n:\n{\nbaz\n:\n(\n)=>\n{\n}\n}\n}\n", e => e.type == "ArrowFunctionExpression",
+ { name: "baz", chain: ["this", "nested", "foo", "bar"], loc: [[8, 0], [8, 3]] });
+
+ verify("nested.this.foo={bar:()=>{}}", e => e.type == "ArrowFunctionExpression",
+ { name: "bar", chain: ["nested", "this", "foo"], loc: [[1, 17], [1, 20]] });
+
+ verify("\nnested.this.foo\n=\n{\nbar\n:\n(\n)=>\n{\n}\n}\n", e => e.type == "ArrowFunctionExpression",
+ { name: "bar", chain: ["nested", "this", "foo"], loc: [[5, 0], [5, 3]] });
+
+ verify("nested.this.foo={bar:{baz:()=>{}}}", e => e.type == "ArrowFunctionExpression",
+ { name: "baz", chain: ["nested", "this", "foo", "bar"], loc: [[1, 22], [1, 25]] });
+
+ verify("\nnested.this.foo\n=\n{\nbar\n:\n{\nbaz\n:\n(\n)=>\n{\n}\n}\n}\n", e => e.type == "ArrowFunctionExpression",
+ { name: "baz", chain: ["nested", "this", "foo", "bar"], loc: [[8, 0], [8, 3]] });
+
+ // VariableDeclarator + AssignmentExpression + ObjectExpression
+
+ verify("let foo={bar:()=>{}}", e => e.type == "ArrowFunctionExpression",
+ { name: "bar", chain: ["foo"], loc: [[1, 9], [1, 12]] });
+
+ verify("\nlet\nfoo\n=\n{\nbar\n:\n(\n)=>\n{\n}\n}\n", e => e.type == "ArrowFunctionExpression",
+ { name: "bar", chain: ["foo"], loc: [[6, 0], [6, 3]] });
+
+ verify("let foo={bar:{baz:()=>{}}}", e => e.type == "ArrowFunctionExpression",
+ { name: "baz", chain: ["foo", "bar"], loc: [[1, 14], [1, 17]] });
+
+ verify("\nlet\nfoo\n=\n{\nbar\n:\n{\nbaz\n:\n(\n)=>\n{\n}\n}\n}\n", e => e.type == "ArrowFunctionExpression",
+ { name: "baz", chain: ["foo", "bar"], loc: [[9, 0], [9, 3]] });
+
+ // New/CallExpression + AssignmentExpression + ObjectExpression
+
+ verify("foo({bar:()=>{}})", e => e.type == "ArrowFunctionExpression",
+ { name: "bar", chain: [], loc: [[1, 5], [1, 8]] });
+
+ verify("\nfoo\n(\n{\nbar\n:\n(\n)=>\n{\n}\n}\n)\n", e => e.type == "ArrowFunctionExpression",
+ { name: "bar", chain: [], loc: [[5, 0], [5, 3]] });
+
+ verify("foo({bar:{baz:()=>{}}})", e => e.type == "ArrowFunctionExpression",
+ { name: "baz", chain: ["bar"], loc: [[1, 10], [1, 13]] });
+
+ verify("\nfoo\n(\n{\nbar\n:\n{\nbaz\n:\n(\n)=>\n{\n}\n}\n}\n)\n", e => e.type == "ArrowFunctionExpression",
+ { name: "baz", chain: ["bar"], loc: [[8, 0], [8, 3]] });
+
+ verify("nested.foo({bar:()=>{}})", e => e.type == "ArrowFunctionExpression",
+ { name: "bar", chain: [], loc: [[1, 12], [1, 15]] });
+
+ verify("\nnested.foo\n(\n{\nbar\n:\n(\n)=>\n{\n}\n}\n)\n", e => e.type == "ArrowFunctionExpression",
+ { name: "bar", chain: [], loc: [[5, 0], [5, 3]] });
+
+ verify("nested.foo({bar:{baz:()=>{}}})", e => e.type == "ArrowFunctionExpression",
+ { name: "baz", chain: ["bar"], loc: [[1, 17], [1, 20]] });
+
+ verify("\nnested.foo\n(\n{\nbar\n:\n{\nbaz\n:\n(\n)=>\n{\n}\n}\n}\n)\n", e => e.type == "ArrowFunctionExpression",
+ { name: "baz", chain: ["bar"], loc: [[8, 0], [8, 3]] });
+
+ verify("this.foo({bar:()=>{}})", e => e.type == "ArrowFunctionExpression",
+ { name: "bar", chain: [], loc: [[1, 10], [1, 13]] });
+
+ verify("\nthis.foo\n(\n{\nbar\n:\n(\n)=>\n{\n}\n}\n)\n", e => e.type == "ArrowFunctionExpression",
+ { name: "bar", chain: [], loc: [[5, 0], [5, 3]] });
+
+ verify("this.foo({bar:{baz:()=>{}}})", e => e.type == "ArrowFunctionExpression",
+ { name: "baz", chain: ["bar"], loc: [[1, 15], [1, 18]] });
+
+ verify("\nthis.foo\n(\n{\nbar\n:\n{\nbaz\n:\n(\n)=>\n{\n}\n}\n}\n)\n", e => e.type == "ArrowFunctionExpression",
+ { name: "baz", chain: ["bar"], loc: [[8, 0], [8, 3]] });
+
+ verify("this.nested.foo({bar:()=>{}})", e => e.type == "ArrowFunctionExpression",
+ { name: "bar", chain: [], loc: [[1, 17], [1, 20]] });
+
+ verify("\nthis.nested.foo\n(\n{\nbar\n:\n(\n)=>\n{\n}\n}\n)\n", e => e.type == "ArrowFunctionExpression",
+ { name: "bar", chain: [], loc: [[5, 0], [5, 3]] });
+
+ verify("this.nested.foo({bar:{baz:()=>{}}})", e => e.type == "ArrowFunctionExpression",
+ { name: "baz", chain: ["bar"], loc: [[1, 22], [1, 25]] });
+
+ verify("\nthis.nested.foo\n(\n{\nbar\n:\n{\nbaz\n:\n(\n)=>\n{\n}\n}\n}\n)\n", e => e.type == "ArrowFunctionExpression",
+ { name: "baz", chain: ["bar"], loc: [[8, 0], [8, 3]] });
+
+ verify("nested.this.foo({bar:()=>{}})", e => e.type == "ArrowFunctionExpression",
+ { name: "bar", chain: [], loc: [[1, 17], [1, 20]] });
+
+ verify("\nnested.this.foo\n(\n{\nbar\n:\n(\n)=>\n{\n}\n}\n)\n", e => e.type == "ArrowFunctionExpression",
+ { name: "bar", chain: [], loc: [[5, 0], [5, 3]] });
+
+ verify("nested.this.foo({bar:{baz:()=>{}}})", e => e.type == "ArrowFunctionExpression",
+ { name: "baz", chain: ["bar"], loc: [[1, 22], [1, 25]] });
+
+ verify("\nnested.this.foo\n(\n{\nbar\n:\n{\nbaz\n:\n(\n)=>\n{\n}\n}\n}\n)\n", e => e.type == "ArrowFunctionExpression",
+ { name: "baz", chain: ["bar"], loc: [[8, 0], [8, 3]] });
+
+ // New/CallExpression + VariableDeclarator + AssignmentExpression + ObjectExpression
+
+ verify("let target=foo({bar:()=>{}})", e => e.type == "ArrowFunctionExpression",
+ { name: "bar", chain: ["target"], loc: [[1, 16], [1, 19]] });
+
+ verify("\nlet\ntarget=\nfoo\n(\n{\nbar\n:\n(\n)=>\n{\n}\n}\n)\n", e => e.type == "ArrowFunctionExpression",
+ { name: "bar", chain: ["target"], loc: [[7, 0], [7, 3]] });
+
+ verify("let target=foo({bar:{baz:()=>{}}})", e => e.type == "ArrowFunctionExpression",
+ { name: "baz", chain: ["target", "bar"], loc: [[1, 21], [1, 24]] });
+
+ verify("\nlet\ntarget=\nfoo\n(\n{\nbar\n:\n{\nbaz\n:\n(\n)=>\n{\n}\n}\n}\n)\n", e => e.type == "ArrowFunctionExpression",
+ { name: "baz", chain: ["target", "bar"], loc: [[10, 0], [10, 3]] });
+
+ verify("let target=nested.foo({bar:()=>{}})", e => e.type == "ArrowFunctionExpression",
+ { name: "bar", chain: ["target"], loc: [[1, 23], [1, 26]] });
+
+ verify("\nlet\ntarget=\nnested.foo\n(\n{\nbar\n:\n(\n)=>\n{\n}\n}\n)\n", e => e.type == "ArrowFunctionExpression",
+ { name: "bar", chain: ["target"], loc: [[7, 0], [7, 3]] });
+
+ verify("let target=nested.foo({bar:{baz:()=>{}}})", e => e.type == "ArrowFunctionExpression",
+ { name: "baz", chain: ["target", "bar"], loc: [[1, 28], [1, 31]] });
+
+ verify("\nlet\ntarget=\nnested.foo\n(\n{\nbar\n:\n{\nbaz\n:\n(\n)=>\n{\n}\n}\n}\n)\n", e => e.type == "ArrowFunctionExpression",
+ { name: "baz", chain: ["target", "bar"], loc: [[10, 0], [10, 3]] });
+
+ verify("let target=this.foo({bar:()=>{}})", e => e.type == "ArrowFunctionExpression",
+ { name: "bar", chain: ["target"], loc: [[1, 21], [1, 24]] });
+
+ verify("\nlet\ntarget=\nthis.foo\n(\n{\nbar\n:\n(\n)=>\n{\n}\n}\n)\n", e => e.type == "ArrowFunctionExpression",
+ { name: "bar", chain: ["target"], loc: [[7, 0], [7, 3]] });
+
+ verify("let target=this.foo({bar:{baz:()=>{}}})", e => e.type == "ArrowFunctionExpression",
+ { name: "baz", chain: ["target", "bar"], loc: [[1, 26], [1, 29]] });
+
+ verify("\nlet\ntarget=\nthis.foo\n(\n{\nbar\n:\n{\nbaz\n:\n(\n)=>\n{\n}\n}\n}\n)\n", e => e.type == "ArrowFunctionExpression",
+ { name: "baz", chain: ["target", "bar"], loc: [[10, 0], [10, 3]] });
+
+ verify("let target=this.nested.foo({bar:()=>{}})", e => e.type == "ArrowFunctionExpression",
+ { name: "bar", chain: ["target"], loc: [[1, 28], [1, 31]] });
+
+ verify("\nlet\ntarget=\nthis.nested.foo\n(\n{\nbar\n:\n(\n)=>\n{\n}\n}\n)\n", e => e.type == "ArrowFunctionExpression",
+ { name: "bar", chain: ["target"], loc: [[7, 0], [7, 3]] });
+
+ verify("let target=this.nested.foo({bar:{baz:()=>{}}})", e => e.type == "ArrowFunctionExpression",
+ { name: "baz", chain: ["target", "bar"], loc: [[1, 33], [1, 36]] });
+
+ verify("\nlet\ntarget=\nthis.nested.foo\n(\n{\nbar\n:\n{\nbaz\n:\n(\n)=>\n{\n}\n}\n}\n)\n", e => e.type == "ArrowFunctionExpression",
+ { name: "baz", chain: ["target", "bar"], loc: [[10, 0], [10, 3]] });
+
+ verify("let target=nested.this.foo({bar:()=>{}})", e => e.type == "ArrowFunctionExpression",
+ { name: "bar", chain: ["target"], loc: [[1, 28], [1, 31]] });
+
+ verify("\nlet\ntarget=\nnested.this.foo\n(\n{\nbar\n:\n(\n)=>\n{\n}\n}\n)\n", e => e.type == "ArrowFunctionExpression",
+ { name: "bar", chain: ["target"], loc: [[7, 0], [7, 3]] });
+
+ verify("let target=nested.this.foo({bar:{baz:()=>{}}})", e => e.type == "ArrowFunctionExpression",
+ { name: "baz", chain: ["target", "bar"], loc: [[1, 33], [1, 36]] });
+
+ verify("\nlet\ntarget=\nnested.this.foo\n(\n{\nbar\n:\n{\nbaz\n:\n(\n)=>\n{\n}\n}\n}\n)\n", e => e.type == "ArrowFunctionExpression",
+ { name: "baz", chain: ["target", "bar"], loc: [[10, 0], [10, 3]] });
+
+ finish();
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_parser-10.js b/devtools/client/debugger/test/mochitest/browser_dbg_parser-10.js
new file mode 100644
index 000000000..d340f9f9c
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_parser-10.js
@@ -0,0 +1,129 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Test that creating an evaluation string for certain nodes works properly.
+ */
+
+function test() {
+ let { Parser, ParserHelpers, SyntaxTreeVisitor } =
+ Cu.import("resource://devtools/shared/Parser.jsm", {});
+
+ function verify(source, predicate, string) {
+ let ast = Parser.reflectionAPI.parse(source);
+ let node = SyntaxTreeVisitor.filter(ast, predicate).pop();
+ let info = ParserHelpers.getIdentifierEvalString(node);
+ is(info, string, "The identifier evaluation string is correct.");
+ }
+
+ // Indentifier or Literal
+
+ verify("foo", e => e.type == "Identifier", "foo");
+ verify("undefined", e => e.type == "Identifier", "undefined");
+ verify("null", e => e.type == "Literal", "null");
+ verify("42", e => e.type == "Literal", "42");
+ verify("true", e => e.type == "Literal", "true");
+ verify("\"nasu\"", e => e.type == "Literal", "\"nasu\"");
+
+ // MemberExpression or ThisExpression
+
+ verify("this", e => e.type == "ThisExpression", "this");
+ verify("foo.bar", e => e.name == "foo", "foo");
+ verify("foo.bar", e => e.name == "bar", "foo.bar");
+
+ // MemberExpression + ThisExpression
+
+ verify("this.foo.bar", e => e.type == "ThisExpression", "this");
+ verify("this.foo.bar", e => e.name == "foo", "this.foo");
+ verify("this.foo.bar", e => e.name == "bar", "this.foo.bar");
+
+ verify("foo.this.bar", e => e.name == "foo", "foo");
+ verify("foo.this.bar", e => e.name == "this", "foo.this");
+ verify("foo.this.bar", e => e.name == "bar", "foo.this.bar");
+
+ // ObjectExpression + VariableDeclarator
+
+ verify("let foo={bar:baz}", e => e.name == "baz", "baz");
+ verify("let foo={bar:undefined}", e => e.name == "undefined", "undefined");
+ verify("let foo={bar:null}", e => e.type == "Literal", "null");
+ verify("let foo={bar:42}", e => e.type == "Literal", "42");
+ verify("let foo={bar:true}", e => e.type == "Literal", "true");
+ verify("let foo={bar:\"nasu\"}", e => e.type == "Literal", "\"nasu\"");
+ verify("let foo={bar:this}", e => e.type == "ThisExpression", "this");
+
+ verify("let foo={bar:{nested:baz}}", e => e.name == "baz", "baz");
+ verify("let foo={bar:{nested:undefined}}", e => e.name == "undefined", "undefined");
+ verify("let foo={bar:{nested:null}}", e => e.type == "Literal", "null");
+ verify("let foo={bar:{nested:42}}", e => e.type == "Literal", "42");
+ verify("let foo={bar:{nested:true}}", e => e.type == "Literal", "true");
+ verify("let foo={bar:{nested:\"nasu\"}}", e => e.type == "Literal", "\"nasu\"");
+ verify("let foo={bar:{nested:this}}", e => e.type == "ThisExpression", "this");
+
+ verify("let foo={bar:baz}", e => e.name == "bar", "foo.bar");
+ verify("let foo={bar:baz}", e => e.name == "foo", "foo");
+
+ verify("let foo={bar:{nested:baz}}", e => e.name == "nested", "foo.bar.nested");
+ verify("let foo={bar:{nested:baz}}", e => e.name == "bar", "foo.bar");
+ verify("let foo={bar:{nested:baz}}", e => e.name == "foo", "foo");
+
+ // ObjectExpression + MemberExpression
+
+ verify("parent.foo={bar:baz}", e => e.name == "bar", "parent.foo.bar");
+ verify("parent.foo={bar:baz}", e => e.name == "foo", "parent.foo");
+ verify("parent.foo={bar:baz}", e => e.name == "parent", "parent");
+
+ verify("parent.foo={bar:{nested:baz}}", e => e.name == "nested", "parent.foo.bar.nested");
+ verify("parent.foo={bar:{nested:baz}}", e => e.name == "bar", "parent.foo.bar");
+ verify("parent.foo={bar:{nested:baz}}", e => e.name == "foo", "parent.foo");
+ verify("parent.foo={bar:{nested:baz}}", e => e.name == "parent", "parent");
+
+ verify("this.foo={bar:{nested:baz}}", e => e.name == "nested", "this.foo.bar.nested");
+ verify("this.foo={bar:{nested:baz}}", e => e.name == "bar", "this.foo.bar");
+ verify("this.foo={bar:{nested:baz}}", e => e.name == "foo", "this.foo");
+ verify("this.foo={bar:{nested:baz}}", e => e.type == "ThisExpression", "this");
+
+ verify("this.parent.foo={bar:{nested:baz}}", e => e.name == "nested", "this.parent.foo.bar.nested");
+ verify("this.parent.foo={bar:{nested:baz}}", e => e.name == "bar", "this.parent.foo.bar");
+ verify("this.parent.foo={bar:{nested:baz}}", e => e.name == "foo", "this.parent.foo");
+ verify("this.parent.foo={bar:{nested:baz}}", e => e.name == "parent", "this.parent");
+ verify("this.parent.foo={bar:{nested:baz}}", e => e.type == "ThisExpression", "this");
+
+ verify("parent.this.foo={bar:{nested:baz}}", e => e.name == "nested", "parent.this.foo.bar.nested");
+ verify("parent.this.foo={bar:{nested:baz}}", e => e.name == "bar", "parent.this.foo.bar");
+ verify("parent.this.foo={bar:{nested:baz}}", e => e.name == "foo", "parent.this.foo");
+ verify("parent.this.foo={bar:{nested:baz}}", e => e.name == "this", "parent.this");
+ verify("parent.this.foo={bar:{nested:baz}}", e => e.name == "parent", "parent");
+
+ // FunctionExpression
+
+ verify("function foo(){}", e => e.name == "foo", "foo");
+ verify("var foo=function(){}", e => e.name == "foo", "foo");
+ verify("var foo=function bar(){}", e => e.name == "bar", "bar");
+
+ // New/CallExpression
+
+ verify("foo()", e => e.name == "foo", "foo");
+ verify("new foo()", e => e.name == "foo", "foo");
+
+ verify("foo(bar)", e => e.name == "bar", "bar");
+ verify("foo(bar, baz)", e => e.name == "baz", "baz");
+ verify("foo(undefined)", e => e.name == "undefined", "undefined");
+ verify("foo(null)", e => e.type == "Literal", "null");
+ verify("foo(42)", e => e.type == "Literal", "42");
+ verify("foo(true)", e => e.type == "Literal", "true");
+ verify("foo(\"nasu\")", e => e.type == "Literal", "\"nasu\"");
+ verify("foo(this)", e => e.type == "ThisExpression", "this");
+
+ // New/CallExpression + ObjectExpression + MemberExpression
+
+ verify("fun(this.parent.foo={bar:{nested:baz}})", e => e.name == "nested", "this.parent.foo.bar.nested");
+ verify("fun(this.parent.foo={bar:{nested:baz}})", e => e.name == "bar", "this.parent.foo.bar");
+ verify("fun(this.parent.foo={bar:{nested:baz}})", e => e.name == "foo", "this.parent.foo");
+ verify("fun(this.parent.foo={bar:{nested:baz}})", e => e.name == "parent", "this.parent");
+ verify("fun(this.parent.foo={bar:{nested:baz}})", e => e.type == "ThisExpression", "this");
+ verify("fun(this.parent.foo={bar:{nested:baz}})", e => e.name == "fun", "fun");
+
+ finish();
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_parser-11.js b/devtools/client/debugger/test/mochitest/browser_dbg_parser-11.js
new file mode 100644
index 000000000..ee2b4c89d
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_parser-11.js
@@ -0,0 +1,41 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Checks if self-closing <script/> tags are parsed by Parser.jsm
+ */
+
+function test() {
+ let { Parser } = Cu.import("resource://devtools/shared/Parser.jsm", {});
+
+ let source = [
+ '<script type="text/javascript" src="chrome://foo.js"/>',
+ '<script type="application/javascript;version=1.8" src="chrome://baz.js"/>',
+ '<script async defer src="chrome://foobar.js"/>',
+ '<script type="application/javascript"/>"hello third"',
+ '<script type="application/javascript">"hello fourth"</script>',
+ ].join("\n");
+ let parser = new Parser();
+ let parsed = parser.get(source);
+
+ is(parser.errors.length, 0,
+ "There should be no errors logged when parsing.");
+ is(parsed.scriptCount, 5,
+ "There should be 5 scripts parsed in the parent HTML source.");
+
+ is(parsed.getScriptInfo(source.indexOf("foo.js\"/>") + 1).toSource(), "({start:-1, length:-1, index:-1})",
+ "the first script is empty");
+ is(parsed.getScriptInfo(source.indexOf("baz.js\"/>") + 1).toSource(), "({start:-1, length:-1, index:-1})",
+ "the second script is empty");
+ is(parsed.getScriptInfo(source.indexOf("foobar.js\"/>") + 1).toSource(), "({start:-1, length:-1, index:-1})",
+ "the third script is empty");
+
+ is(parsed.getScriptInfo(source.indexOf("hello third!")).toSource(), "({start:-1, length:-1, index:-1})",
+ "Inline script on self-closing tag not considered a script");
+ is(parsed.getScriptInfo(source.indexOf("hello fourth")).toSource(), "({start:267, length:14, index:4})",
+ "The fourth script was located correctly.");
+
+ finish();
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_parser-computed-name.js b/devtools/client/debugger/test/mochitest/browser_dbg_parser-computed-name.js
new file mode 100644
index 000000000..085f9781b
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_parser-computed-name.js
@@ -0,0 +1,32 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Test that template strings are correctly processed.
+ */
+
+"use strict";
+
+function test() {
+ let { Parser, SyntaxTreeVisitor } =
+ Cu.import("resource://devtools/shared/Parser.jsm", {});
+
+ let ast = Parser.reflectionAPI.parse("({ [i]: 1 })");
+ let nodes = SyntaxTreeVisitor.filter(ast, e => e.type == "ComputedName");
+ ok(nodes && nodes.length === 1, "Found the ComputedName node");
+
+ let name = nodes[0].name;
+ ok(name, "The ComputedName node has a name property");
+ is(name.type, "Identifier", "The name has a correct type");
+ is(name.name, "i", "The name has a correct name");
+
+ let identNodes = SyntaxTreeVisitor.filter(ast, e => e.type == "Identifier");
+ ok(identNodes && identNodes.length === 1, "Found the Identifier node");
+
+ is(identNodes[0].type, "Identifier", "The identifier has a correct type");
+ is(identNodes[0].name, "i", "The identifier has a correct name");
+
+ finish();
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_parser-function-defaults.js b/devtools/client/debugger/test/mochitest/browser_dbg_parser-function-defaults.js
new file mode 100644
index 000000000..55fac4055
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_parser-function-defaults.js
@@ -0,0 +1,31 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Test that function default arguments are correctly processed.
+ */
+
+"use strict";
+
+function test() {
+ let { Parser, ParserHelpers, SyntaxTreeVisitor } =
+ Cu.import("resource://devtools/shared/Parser.jsm", {});
+
+ function verify(source, predicate, string) {
+ let ast = Parser.reflectionAPI.parse(source);
+ let node = SyntaxTreeVisitor.filter(ast, predicate).pop();
+ let info = ParserHelpers.getIdentifierEvalString(node);
+ is(info, string, "The identifier evaluation string is correct.");
+ }
+
+ // FunctionDeclaration
+ verify("function foo(a, b='b') {}", e => e.type == "Literal", "\"b\"");
+ // FunctionExpression
+ verify("let foo=function(a, b='b') {}", e => e.type == "Literal", "\"b\"");
+ // ArrowFunctionExpression
+ verify("let foo=(a, b='b')=> {}", e => e.type == "Literal", "\"b\"");
+
+ finish();
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_parser-spread-expression.js b/devtools/client/debugger/test/mochitest/browser_dbg_parser-spread-expression.js
new file mode 100644
index 000000000..4b7d93d2f
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_parser-spread-expression.js
@@ -0,0 +1,32 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Test that spread expressions work both in arrays and function calls.
+ */
+
+"use strict";
+
+function test() {
+ let { Parser, SyntaxTreeVisitor } =
+ Cu.import("resource://devtools/shared/Parser.jsm", {});
+
+ const SCRIPTS = ["[...a]", "foo(...a)"];
+
+ for (let script of SCRIPTS) {
+ info(`Testing spread expression in '${script}'`);
+ let ast = Parser.reflectionAPI.parse(script);
+ let nodes = SyntaxTreeVisitor.filter(ast,
+ e => e.type == "SpreadExpression");
+ ok(nodes && nodes.length === 1, "Found the SpreadExpression node");
+
+ let expr = nodes[0].expression;
+ ok(expr, "The SpreadExpression node has the sub-expression");
+ is(expr.type, "Identifier", "The sub-expression is an Identifier");
+ is(expr.name, "a", "The sub-expression identifier has a correct name");
+ }
+
+ finish();
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_parser-template-strings.js b/devtools/client/debugger/test/mochitest/browser_dbg_parser-template-strings.js
new file mode 100644
index 000000000..6ee271137
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_parser-template-strings.js
@@ -0,0 +1,29 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Test that template strings are correctly processed.
+ */
+
+"use strict";
+
+function test() {
+ let { Parser, SyntaxTreeVisitor } =
+ Cu.import("resource://devtools/shared/Parser.jsm", {});
+
+ let ast = Parser.reflectionAPI.parse("`foo${i}bar`");
+ let nodes = SyntaxTreeVisitor.filter(ast, e => e.type == "TemplateLiteral");
+ ok(nodes && nodes.length === 1, "Found the TemplateLiteral node");
+
+ let elements = nodes[0].elements;
+ ok(elements, "The TemplateLiteral node has elements");
+ is(elements.length, 3, "There are 3 elements in the literal");
+
+ ["Literal", "Identifier", "Literal"].forEach((type, i) => {
+ is(elements[i].type, type, `Element at index ${i} is '${type}'`);
+ });
+
+ finish();
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_pause-exceptions-01.js b/devtools/client/debugger/test/mochitest/browser_dbg_pause-exceptions-01.js
new file mode 100644
index 000000000..92cd7e4bc
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_pause-exceptions-01.js
@@ -0,0 +1,246 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure that pausing on exceptions works.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_pause-exceptions.html";
+
+var gTab, gPanel, gDebugger;
+var gFrames, gVariables, gPrefs, gOptions;
+
+function test() {
+ requestLongerTimeout(2);
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gFrames = gDebugger.DebuggerView.StackFrames;
+ gVariables = gDebugger.DebuggerView.Variables;
+ gPrefs = gDebugger.Prefs;
+ gOptions = gDebugger.DebuggerView.Options;
+
+ is(gPrefs.pauseOnExceptions, false,
+ "The pause-on-exceptions pref should be disabled by default.");
+ isnot(gOptions._pauseOnExceptionsItem.getAttribute("checked"), "true",
+ "The pause-on-exceptions menu item should not be checked.");
+
+ testPauseOnExceptionsDisabled()
+ .then(enablePauseOnExceptions)
+ .then(disableIgnoreCaughtExceptions)
+ .then(testPauseOnExceptionsEnabled)
+ .then(disablePauseOnExceptions)
+ .then(enableIgnoreCaughtExceptions)
+ .then(() => closeDebuggerAndFinish(gPanel))
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+ });
+}
+
+function testPauseOnExceptionsDisabled() {
+ let finished = waitForCaretAndScopes(gPanel, 26).then(() => {
+ info("Testing disabled pause-on-exceptions.");
+
+ is(gDebugger.gThreadClient.state, "paused",
+ "Should only be getting stack frames while paused (1).");
+ ok(isCaretPos(gPanel, 26),
+ "Should be paused on the debugger statement (1).");
+
+ let innerScope = gVariables.getScopeAtIndex(0);
+ let innerNodes = innerScope.target.querySelector(".variables-view-element-details").childNodes;
+
+ is(gFrames.itemCount, 1,
+ "Should have one frame.");
+ is(gVariables._store.length, 4,
+ "Should have four scopes.");
+
+ is(innerNodes[0].querySelector(".name").getAttribute("value"), "this",
+ "Should have the right property name for 'this'.");
+ is(innerNodes[0].querySelector(".value").getAttribute("value"), "<button>",
+ "Should have the right property value for 'this'.");
+
+ let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.AFTER_FRAMES_CLEARED).then(() => {
+ isnot(gDebugger.gThreadClient.state, "paused",
+ "Should not be paused after resuming.");
+ ok(isCaretPos(gPanel, 26),
+ "Should be idle on the debugger statement.");
+
+ ok(true, "Frames were cleared, debugger didn't pause again.");
+ });
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ gDebugger.document.getElementById("resume"),
+ gDebugger);
+
+ return finished;
+ });
+
+ generateMouseClickInTab(gTab, "content.document.querySelector('button')");
+
+ return finished;
+}
+
+function testPauseOnExceptionsEnabled() {
+ let finished = waitForCaretAndScopes(gPanel, 19).then(() => {
+ info("Testing enabled pause-on-exceptions.");
+
+ is(gDebugger.gThreadClient.state, "paused",
+ "Should only be getting stack frames while paused.");
+ ok(isCaretPos(gPanel, 19),
+ "Should be paused on the debugger statement.");
+
+ let innerScope = gVariables.getScopeAtIndex(0);
+ let innerNodes = innerScope.target.querySelector(".variables-view-element-details").childNodes;
+
+ is(gFrames.itemCount, 1,
+ "Should have one frame.");
+ is(gVariables._store.length, 4,
+ "Should have four scopes.");
+
+ is(innerNodes[0].querySelector(".name").getAttribute("value"), "<exception>",
+ "Should have the right property name for <exception>.");
+ is(innerNodes[0].querySelector(".value").getAttribute("value"), "Error",
+ "Should have the right property value for <exception>.");
+
+ let finished = waitForCaretAndScopes(gPanel, 26).then(() => {
+ info("Testing enabled pause-on-exceptions and resumed after pause.");
+
+ is(gDebugger.gThreadClient.state, "paused",
+ "Should only be getting stack frames while paused.");
+ ok(isCaretPos(gPanel, 26),
+ "Should be paused on the debugger statement.");
+
+ let innerScope = gVariables.getScopeAtIndex(0);
+ let innerNodes = innerScope.target.querySelector(".variables-view-element-details").childNodes;
+
+ is(gFrames.itemCount, 1,
+ "Should have one frame.");
+ is(gVariables._store.length, 4,
+ "Should have four scopes.");
+
+ is(innerNodes[0].querySelector(".name").getAttribute("value"), "this",
+ "Should have the right property name for 'this'.");
+ is(innerNodes[0].querySelector(".value").getAttribute("value"), "<button>",
+ "Should have the right property value for 'this'.");
+
+ let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.AFTER_FRAMES_CLEARED).then(() => {
+ isnot(gDebugger.gThreadClient.state, "paused",
+ "Should not be paused after resuming.");
+ ok(isCaretPos(gPanel, 26),
+ "Should be idle on the debugger statement.");
+
+ ok(true, "Frames were cleared, debugger didn't pause again.");
+ });
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ gDebugger.document.getElementById("resume"),
+ gDebugger);
+
+ return finished;
+ });
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ gDebugger.document.getElementById("resume"),
+ gDebugger);
+
+ return finished;
+ });
+
+ generateMouseClickInTab(gTab, "content.document.querySelector('button')");
+
+ return finished;
+}
+
+function enablePauseOnExceptions() {
+ let deferred = promise.defer();
+
+ gDebugger.gThreadClient.addOneTimeListener("resumed", () => {
+ is(gPrefs.pauseOnExceptions, true,
+ "The pause-on-exceptions pref should now be enabled.");
+ is(gOptions._pauseOnExceptionsItem.getAttribute("checked"), "true",
+ "The pause-on-exceptions menu item should now be checked.");
+
+ ok(true, "Pausing on exceptions was enabled.");
+ deferred.resolve();
+ });
+
+ gOptions._pauseOnExceptionsItem.setAttribute("checked", "true");
+ gOptions._togglePauseOnExceptions();
+
+ return deferred.promise;
+}
+
+function disablePauseOnExceptions() {
+ let deferred = promise.defer();
+
+ gDebugger.gThreadClient.addOneTimeListener("resumed", () => {
+ is(gPrefs.pauseOnExceptions, false,
+ "The pause-on-exceptions pref should now be disabled.");
+ isnot(gOptions._pauseOnExceptionsItem.getAttribute("checked"), "true",
+ "The pause-on-exceptions menu item should now be unchecked.");
+
+ ok(true, "Pausing on exceptions was disabled.");
+ deferred.resolve();
+ });
+
+ gOptions._pauseOnExceptionsItem.setAttribute("checked", "false");
+ gOptions._togglePauseOnExceptions();
+
+ return deferred.promise;
+}
+
+function enableIgnoreCaughtExceptions() {
+ let deferred = promise.defer();
+
+ gDebugger.gThreadClient.addOneTimeListener("resumed", () => {
+ is(gPrefs.ignoreCaughtExceptions, true,
+ "The ignore-caught-exceptions pref should now be enabled.");
+ is(gOptions._ignoreCaughtExceptionsItem.getAttribute("checked"), "true",
+ "The ignore-caught-exceptions menu item should now be checked.");
+
+ ok(true, "Ignore caught exceptions was enabled.");
+ deferred.resolve();
+ });
+
+ gOptions._ignoreCaughtExceptionsItem.setAttribute("checked", "true");
+ gOptions._toggleIgnoreCaughtExceptions();
+
+ return deferred.promise;
+}
+
+function disableIgnoreCaughtExceptions() {
+ let deferred = promise.defer();
+
+ gDebugger.gThreadClient.addOneTimeListener("resumed", () => {
+ is(gPrefs.ignoreCaughtExceptions, false,
+ "The ignore-caught-exceptions pref should now be disabled.");
+ isnot(gOptions._ignoreCaughtExceptionsItem.getAttribute("checked"), "true",
+ "The ignore-caught-exceptions menu item should now be unchecked.");
+
+ ok(true, "Ignore caught exceptions was disabled.");
+ deferred.resolve();
+ });
+
+ gOptions._ignoreCaughtExceptionsItem.setAttribute("checked", "false");
+ gOptions._toggleIgnoreCaughtExceptions();
+
+ return deferred.promise;
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gFrames = null;
+ gVariables = null;
+ gPrefs = null;
+ gOptions = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_pause-exceptions-02.js b/devtools/client/debugger/test/mochitest/browser_dbg_pause-exceptions-02.js
new file mode 100644
index 000000000..21b28ce26
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_pause-exceptions-02.js
@@ -0,0 +1,204 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure that pausing on exceptions works after reload.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_pause-exceptions.html";
+
+var gTab, gPanel, gDebugger;
+var gFrames, gVariables, gPrefs, gOptions;
+
+function test() {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gFrames = gDebugger.DebuggerView.StackFrames;
+ gVariables = gDebugger.DebuggerView.Variables;
+ gPrefs = gDebugger.Prefs;
+ gOptions = gDebugger.DebuggerView.Options;
+
+ is(gPrefs.pauseOnExceptions, false,
+ "The pause-on-exceptions pref should be disabled by default.");
+ isnot(gOptions._pauseOnExceptionsItem.getAttribute("checked"), "true",
+ "The pause-on-exceptions menu item should not be checked.");
+
+ enablePauseOnExceptions()
+ .then(disableIgnoreCaughtExceptions)
+ .then(() => reloadActiveTab(gPanel, gDebugger.EVENTS.SOURCE_SHOWN))
+ .then(() => {
+ generateMouseClickInTab(gTab, "content.document.querySelector('button')");
+ })
+ .then(testPauseOnExceptionsAfterReload)
+ .then(disablePauseOnExceptions)
+ .then(enableIgnoreCaughtExceptions)
+ .then(() => closeDebuggerAndFinish(gPanel))
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+ });
+}
+
+function testPauseOnExceptionsAfterReload() {
+ let finished = waitForCaretAndScopes(gPanel, 19).then(() => {
+ info("Testing enabled pause-on-exceptions.");
+
+ is(gDebugger.gThreadClient.state, "paused",
+ "Should only be getting stack frames while paused.");
+ ok(isCaretPos(gPanel, 19),
+ "Should be paused on the debugger statement.");
+
+ let innerScope = gVariables.getScopeAtIndex(0);
+ let innerNodes = innerScope.target.querySelector(".variables-view-element-details").childNodes;
+
+ is(gFrames.itemCount, 1,
+ "Should have one frame.");
+ is(gVariables._store.length, 4,
+ "Should have four scopes.");
+
+ is(innerNodes[0].querySelector(".name").getAttribute("value"), "<exception>",
+ "Should have the right property name for <exception>.");
+ is(innerNodes[0].querySelector(".value").getAttribute("value"), "Error",
+ "Should have the right property value for <exception>.");
+
+ let finished = waitForCaretAndScopes(gPanel, 26).then(() => {
+ info("Testing enabled pause-on-exceptions and resumed after pause.");
+
+ is(gDebugger.gThreadClient.state, "paused",
+ "Should only be getting stack frames while paused.");
+ ok(isCaretPos(gPanel, 26),
+ "Should be paused on the debugger statement.");
+
+ let innerScope = gVariables.getScopeAtIndex(0);
+ let innerNodes = innerScope.target.querySelector(".variables-view-element-details").childNodes;
+
+ is(gFrames.itemCount, 1,
+ "Should have one frame.");
+ is(gVariables._store.length, 4,
+ "Should have four scopes.");
+
+ is(innerNodes[0].querySelector(".name").getAttribute("value"), "this",
+ "Should have the right property name for 'this'.");
+ is(innerNodes[0].querySelector(".value").getAttribute("value"), "<button>",
+ "Should have the right property value for 'this'.");
+
+ let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.AFTER_FRAMES_CLEARED).then(() => {
+ isnot(gDebugger.gThreadClient.state, "paused",
+ "Should not be paused after resuming.");
+ ok(isCaretPos(gPanel, 26),
+ "Should be idle on the debugger statement.");
+
+ ok(true, "Frames were cleared, debugger didn't pause again.");
+ });
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ gDebugger.document.getElementById("resume"),
+ gDebugger);
+
+ return finished;
+ });
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ gDebugger.document.getElementById("resume"),
+ gDebugger);
+
+ return finished;
+ });
+
+ generateMouseClickInTab(gTab, "content.document.querySelector('button')");
+
+ return finished;
+}
+
+function enablePauseOnExceptions() {
+ let deferred = promise.defer();
+
+ gDebugger.gThreadClient.addOneTimeListener("resumed", () => {
+ is(gPrefs.pauseOnExceptions, true,
+ "The pause-on-exceptions pref should now be enabled.");
+ is(gOptions._pauseOnExceptionsItem.getAttribute("checked"), "true",
+ "The pause-on-exceptions menu item should now be checked.");
+
+ ok(true, "Pausing on exceptions was enabled.");
+ deferred.resolve();
+ });
+
+ gOptions._pauseOnExceptionsItem.setAttribute("checked", "true");
+ gOptions._togglePauseOnExceptions();
+ return deferred.promise;
+}
+
+function disablePauseOnExceptions() {
+ let deferred = promise.defer();
+
+ gDebugger.gThreadClient.addOneTimeListener("resumed", () => {
+ is(gPrefs.pauseOnExceptions, false,
+ "The pause-on-exceptions pref should now be disabled.");
+ isnot(gOptions._pauseOnExceptionsItem.getAttribute("checked"), "true",
+ "The pause-on-exceptions menu item should now be unchecked.");
+
+ ok(true, "Pausing on exceptions was disabled.");
+ deferred.resolve();
+ });
+
+ gOptions._pauseOnExceptionsItem.setAttribute("checked", "false");
+ gOptions._togglePauseOnExceptions();
+
+ return deferred.promise;
+}
+
+function enableIgnoreCaughtExceptions() {
+ let deferred = promise.defer();
+
+ gDebugger.gThreadClient.addOneTimeListener("resumed", () => {
+ is(gPrefs.ignoreCaughtExceptions, true,
+ "The ignore-caught-exceptions pref should now be enabled.");
+ is(gOptions._ignoreCaughtExceptionsItem.getAttribute("checked"), "true",
+ "The ignore-caught-exceptions menu item should now be checked.");
+
+ ok(true, "Ignore caught exceptions was enabled.");
+ deferred.resolve();
+ });
+
+ gOptions._ignoreCaughtExceptionsItem.setAttribute("checked", "true");
+ gOptions._toggleIgnoreCaughtExceptions();
+
+ return deferred.promise;
+}
+
+function disableIgnoreCaughtExceptions() {
+ let deferred = promise.defer();
+
+ gDebugger.gThreadClient.addOneTimeListener("resumed", () => {
+ is(gPrefs.ignoreCaughtExceptions, false,
+ "The ignore-caught-exceptions pref should now be disabled.");
+ isnot(gOptions._ignoreCaughtExceptionsItem.getAttribute("checked"), "true",
+ "The ignore-caught-exceptions menu item should now be unchecked.");
+
+ ok(true, "Ignore caught exceptions was disabled.");
+ deferred.resolve();
+ });
+
+ gOptions._ignoreCaughtExceptionsItem.setAttribute("checked", "false");
+ gOptions._toggleIgnoreCaughtExceptions();
+
+ return deferred.promise;
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gFrames = null;
+ gVariables = null;
+ gPrefs = null;
+ gOptions = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_pause-no-step.js b/devtools/client/debugger/test/mochitest/browser_dbg_pause-no-step.js
new file mode 100644
index 000000000..d4e8a05d2
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_pause-no-step.js
@@ -0,0 +1,94 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests that pausing / stepping is only enabled when there is a
+ * location.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_pause-exceptions.html";
+
+var gTab, gPanel, gDebugger;
+var gResumeButton, gStepOverButton, gStepOutButton, gStepInButton;
+var gResumeKey, gFrames;
+
+function test() {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gResumeButton = gDebugger.document.getElementById("resume");
+ gStepOverButton = gDebugger.document.getElementById("step-over");
+ gStepInButton = gDebugger.document.getElementById("step-in");
+ gStepOutButton = gDebugger.document.getElementById("step-out");
+ gResumeKey = gDebugger.document.getElementById("resumeKey");
+ gFrames = gDebugger.DebuggerView.StackFrames;
+
+ testPause();
+ });
+}
+
+function testPause() {
+ ok(!gDebugger.gThreadClient.paused, "Should be running after starting the test.");
+ ok(gStepOutButton.disabled, "Stepping out button should be disabled");
+ ok(gStepInButton.disabled, "Stepping in button should be disabled");
+ ok(gStepOverButton.disabled, "Stepping over button should be disabled");
+
+ gDebugger.gThreadClient.addOneTimeListener("paused", () => {
+ ok(gDebugger.gThreadClient.paused,
+ "Should be paused after an interrupt request.");
+
+ ok(!gStepOutButton.disabled, "Stepping out button should be enabled");
+ ok(!gStepInButton.disabled, "Stepping in button should be enabled");
+ ok(!gStepOverButton.disabled, "Stepping over button should be enabled");
+
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.AFTER_FRAMES_REFILLED).then(() => {
+ is(gFrames.itemCount, 1,
+ "Should have 1 frame from the evalInTab call.");
+ gDebugger.gThreadClient.resume(testBreakAtLocation);
+ });
+
+ });
+
+ EventUtils.sendMouseEvent({ type: "mousedown" }, gResumeButton, gDebugger);
+
+ ok(!gDebugger.gThreadClient.paused,
+ "Shouldn't be paused until the next script is executed.");
+ ok(gStepOutButton.disabled, "Stepping out button should be disabled");
+ ok(gStepInButton.disabled, "Stepping in button should be disabled");
+ ok(gStepOverButton.disabled, "Stepping over button should be disabled");
+
+ // Evaluate a script to fully pause the debugger
+ once(gDebugger.gClient, "willInterrupt").then(() => {
+ evalInTab(gTab, "1+1;");
+ });
+}
+
+function testBreakAtLocation() {
+ gDebugger.gThreadClient.addOneTimeListener("paused", () => {
+ ok(!gStepOutButton.disabled, "Stepping out button should be enabled");
+ ok(!gStepInButton.disabled, "Stepping in button should be enabled");
+ ok(!gStepOverButton.disabled, "Stepping over button should be enabled");
+
+ resumeDebuggerThenCloseAndFinish(gPanel);
+ });
+
+ BrowserTestUtils.synthesizeMouseAtCenter("button", {}, gBrowser.selectedBrowser);
+}
+
+registerCleanupFunction(function () {
+ gPanel = null;
+ gDebugger = null;
+ gResumeButton = null;
+ gStepOverButton = null;
+ gStepInButton = null;
+ gStepOutButton = null;
+ gResumeKey = null;
+ gFrames = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_pause-resume.js b/devtools/client/debugger/test/mochitest/browser_dbg_pause-resume.js
new file mode 100644
index 000000000..e9aaebe55
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_pause-resume.js
@@ -0,0 +1,91 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests if pausing and resuming in the main loop works properly.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_pause-exceptions.html";
+
+var gTab, gPanel, gDebugger;
+var gResumeButton, gResumeKey, gFrames;
+
+function test() {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gResumeButton = gDebugger.document.getElementById("resume");
+ gResumeKey = gDebugger.document.getElementById("resumeKey");
+ gFrames = gDebugger.DebuggerView.StackFrames;
+
+ testPause();
+ });
+}
+
+function testPause() {
+ is(gDebugger.gThreadClient.paused, false,
+ "Should be running after starting the test.");
+
+ is(gResumeButton.getAttribute("tooltiptext"),
+ gDebugger.L10N.getFormatStr("pauseButtonTooltip",
+ gDebugger.ShortcutUtils.prettifyShortcut(gResumeKey)),
+ "Button tooltip should be 'pause' when running.");
+
+ gDebugger.gThreadClient.addOneTimeListener("paused", () => {
+ is(gDebugger.gThreadClient.paused, true,
+ "Should be paused after an interrupt request.");
+
+ is(gResumeButton.getAttribute("tooltiptext"),
+ gDebugger.L10N.getFormatStr("resumeButtonTooltip",
+ gDebugger.ShortcutUtils.prettifyShortcut(gResumeKey)),
+ "Button tooltip should be 'resume' when paused.");
+
+ is(gFrames.itemCount, 0,
+ "Should have no frames when paused in the main loop.");
+
+ testResume();
+ });
+
+ EventUtils.sendMouseEvent({ type: "mousedown" }, gResumeButton, gDebugger);
+
+ is(gResumeButton.getAttribute("tooltiptext"),
+ gDebugger.L10N.getFormatStr("pausePendingButtonTooltip"),
+ "Button tooltip should be 'waiting for execution' when breaking on nex.");
+
+ // Evaluate a script to fully pause the debugger
+ once(gDebugger.gClient, "willInterrupt").then(() => {
+ evalInTab(gTab, "1+1;");
+ });
+}
+
+function testResume() {
+ gDebugger.gThreadClient.addOneTimeListener("resumed", () => {
+ is(gDebugger.gThreadClient.paused, false,
+ "Should be paused after an interrupt request.");
+
+ is(gResumeButton.getAttribute("tooltiptext"),
+ gDebugger.L10N.getFormatStr("pauseButtonTooltip",
+ gDebugger.ShortcutUtils.prettifyShortcut(gResumeKey)),
+ "Button tooltip should be pause when running.");
+
+ closeDebuggerAndFinish(gPanel);
+ });
+
+ EventUtils.sendMouseEvent({ type: "mousedown" }, gResumeButton, gDebugger);
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gResumeButton = null;
+ gResumeKey = null;
+ gFrames = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_pause-warning.js b/devtools/client/debugger/test/mochitest/browser_dbg_pause-warning.js
new file mode 100644
index 000000000..bcd2599dc
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_pause-warning.js
@@ -0,0 +1,109 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests if a warning is shown in the inspector when debugger is paused.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_inline-script.html";
+
+var gTab, gPanel, gDebugger;
+var gTarget, gToolbox;
+
+function test() {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gTarget = gPanel.target;
+ gToolbox = gPanel._toolbox;
+
+ testPause();
+ });
+}
+
+function testPause() {
+ gDebugger.gThreadClient.addOneTimeListener("paused", () => {
+ ok(gDebugger.gThreadClient.paused,
+ "threadClient.paused has been updated to true.");
+
+ gToolbox.once("inspector-selected").then(inspector => {
+ inspector.once("inspector-updated").then(testNotificationIsUp1);
+ });
+ gToolbox.selectTool("inspector");
+ });
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ gDebugger.document.getElementById("resume"),
+ gDebugger);
+
+ // Evaluate a script to fully pause the debugger
+ once(gDebugger.gClient, "willInterrupt").then(() => {
+ evalInTab(gTab, "1+1;");
+ });
+}
+
+function testNotificationIsUp1() {
+ let notificationBox = gToolbox.getNotificationBox();
+ let notification = notificationBox.getNotificationWithValue("inspector-script-paused");
+
+ ok(notification,
+ "Inspector notification is present (1).");
+
+ gToolbox.once("jsdebugger-selected", testNotificationIsHidden);
+ gToolbox.selectTool("jsdebugger");
+}
+
+function testNotificationIsHidden() {
+ let notificationBox = gToolbox.getNotificationBox();
+ let notification = notificationBox.getNotificationWithValue("inspector-script-paused");
+
+ ok(!notification,
+ "Inspector notification is hidden (2).");
+
+ gToolbox.once("inspector-selected", testNotificationIsUp2);
+ gToolbox.selectTool("inspector");
+}
+
+function testNotificationIsUp2() {
+ let notificationBox = gToolbox.getNotificationBox();
+ let notification = notificationBox.getNotificationWithValue("inspector-script-paused");
+
+ ok(notification,
+ "Inspector notification is present again (3).");
+
+ testResume();
+}
+
+function testResume() {
+ gDebugger.gThreadClient.addOneTimeListener("resumed", () => {
+ ok(!gDebugger.gThreadClient.paused,
+ "threadClient.paused has been updated to false.");
+
+ let notificationBox = gToolbox.getNotificationBox();
+ let notification = notificationBox.getNotificationWithValue("inspector-script-paused");
+
+ ok(!notification,
+ "Inspector notification was removed once debugger resumed.");
+
+ closeDebuggerAndFinish(gPanel);
+ });
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ gDebugger.document.getElementById("resume"),
+ gDebugger);
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gTarget = null;
+ gToolbox = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_paused-keybindings.js b/devtools/client/debugger/test/mochitest/browser_dbg_paused-keybindings.js
new file mode 100644
index 000000000..ebffbdd9d
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_paused-keybindings.js
@@ -0,0 +1,50 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Test that keybindings still work when the content window is paused and
+// the tab is selected again.
+
+function test() {
+ Task.spawn(function* () {
+ const TAB_URL = EXAMPLE_URL + "doc_inline-script.html";
+ let gDebugger, searchBox;
+
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ let [tab, debuggee, panel] = yield initDebugger(TAB_URL, options);
+ gDebugger = panel.panelWin;
+ searchBox = gDebugger.DebuggerView.Filtering._searchbox;
+
+ let onCaretUpdated = ensureCaretAt(panel, 20, 1, true);
+ let onThreadPaused = ensureThreadClientState(panel, "paused");
+ ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.document.querySelector("button").click();
+ });
+ yield onCaretUpdated;
+ yield onThreadPaused
+
+ // Now open a tab and close it.
+ let tab2 = yield addTab(TAB_URL);
+ yield waitForTick();
+ yield removeTab(tab2);
+ yield ensureCaretAt(panel, 20);
+
+ // Try to use the Cmd-L keybinding to see if it still works.
+ let caretMove = ensureCaretAt(panel, 15, 1, true);
+ // Wait a tick for the editor focus event to occur first.
+ executeSoon(function () {
+ EventUtils.synthesizeKey("l", { accelKey: true });
+ EventUtils.synthesizeKey("1", {});
+ EventUtils.synthesizeKey("5", {});
+ });
+ yield caretMove;
+
+ yield resumeDebuggerThenCloseAndFinish(panel);
+ }).then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_post-page.js b/devtools/client/debugger/test/mochitest/browser_dbg_post-page.js
new file mode 100644
index 000000000..9d7d418de
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_post-page.js
@@ -0,0 +1,53 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests that source contents are invalidated when the target navigates.
+ */
+
+const TAB_URL = EXAMPLE_URL + "sjs_post-page.sjs";
+
+const FORM = "<form method=\"POST\"><input type=\"submit\"></form>";
+const GET_CONTENT = "<script>\"GET\";</script>" + FORM;
+const POST_CONTENT = "<script>\"POST\";</script>" + FORM;
+
+add_task(function* () {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ let [tab,, panel] = yield initDebugger(TAB_URL, options);
+ let win = panel.panelWin;
+ let editor = win.DebuggerView.editor;
+ let queries = win.require("./content/queries");
+ let getState = win.DebuggerController.getState;
+
+ let source = queries.getSelectedSource(getState());
+
+ is(queries.getSourceCount(getState()), 1,
+ "There should be one source displayed in the view.");
+ is(source.url, TAB_URL,
+ "The correct source is currently selected in the view.");
+ is(editor.getText(), GET_CONTENT,
+ "The currently shown source contains bacon. Mmm, delicious!");
+
+ // Submit the form and wait for debugger update
+ let onSourceUpdated = waitForSourceShown(panel, TAB_URL);
+ yield ContentTask.spawn(tab.linkedBrowser, null, function () {
+ content.document.querySelector("input[type=\"submit\"]").click();
+ });
+ yield onSourceUpdated;
+
+ // Verify that the source updates to the POST page content
+ source = queries.getSelectedSource(getState());
+ is(queries.getSourceCount(getState()), 1,
+ "There should be one source displayed in the view.");
+ is(source.url, TAB_URL,
+ "The correct source is currently selected in the view.");
+ is(editor.getText(), POST_CONTENT,
+ "The currently shown source contains bacon. Mmm, delicious!");
+
+ yield closeDebuggerAndFinish(panel);
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-01.js b/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-01.js
new file mode 100644
index 000000000..be6cb76f7
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-01.js
@@ -0,0 +1,52 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure that clicking the pretty print button prettifies the source.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_pretty-print.html";
+
+function test() {
+ // Wait for debugger panel to be fully set and break on debugger statement
+ let options = {
+ source: EXAMPLE_URL + "code_ugly.js",
+ line: 2
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ const gTab = aTab;
+ const gPanel = aPanel;
+ const gDebugger = gPanel.panelWin;
+ const gEditor = gDebugger.DebuggerView.editor;
+ const gSources = gDebugger.DebuggerView.Sources;
+ const queries = gDebugger.require("./content/queries");
+ const constants = gDebugger.require("./content/constants");
+ const actions = bindActionCreators(gPanel);
+ const getState = gDebugger.DebuggerController.getState;
+
+ Task.spawn(function* () {
+ ok(!gEditor.getText().includes("\n "),
+ "The source shouldn't be pretty printed yet.");
+
+ const finished = waitForSourceShown(gPanel, "code_ugly.js");
+ gDebugger.document.getElementById("pretty-print").click();
+ const deck = gDebugger.document.getElementById("editor-deck");
+ is(deck.selectedIndex, 2, "The progress bar should be shown");
+ yield finished;
+
+ ok(gEditor.getText().includes("\n "),
+ "The source should be pretty printed.");
+ is(deck.selectedIndex, 0, "The editor should be shown");
+
+ const source = queries.getSelectedSource(getState());
+ const { loading, text } = queries.getSourceText(getState(), source.actor);
+ ok(!loading, "Source text is not loading");
+ ok(text.includes("\n "),
+ "Subsequent calls to getText return the pretty printed source.");
+
+ resumeDebuggerThenCloseAndFinish(gPanel);
+ });
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-02.js b/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-02.js
new file mode 100644
index 000000000..4cbdfc9a8
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-02.js
@@ -0,0 +1,41 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure that right clicking and selecting the pretty print context menu
+ * item prettifies the source.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_pretty-print.html";
+
+function test() {
+ // Wait for debugger panel to be fully set and break on debugger statement
+ let options = {
+ source: EXAMPLE_URL + "code_ugly.js",
+ line: 2
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ const gTab = aTab;
+ const gPanel = aPanel;
+ const gDebugger = gPanel.panelWin;
+ const gEditor = gDebugger.DebuggerView.editor;
+ const gContextMenu = gDebugger.document.getElementById("sourceEditorContextMenu");
+
+ Task.spawn(function* () {
+ const finished = waitForSourceShown(gPanel, "code_ugly.js");
+ once(gContextMenu, "popupshown").then(() => {
+ const menuItem = gDebugger.document.getElementById("se-dbg-cMenu-prettyPrint");
+ menuItem.click();
+ });
+ gContextMenu.openPopup(gEditor.container, "overlap", 0, 0, true, false);
+ yield finished;
+
+ ok(gEditor.getText().includes("\n "),
+ "The source should be pretty printed.");
+
+ resumeDebuggerThenCloseAndFinish(gPanel);
+ });
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-03.js b/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-03.js
new file mode 100644
index 000000000..13de30ac0
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-03.js
@@ -0,0 +1,40 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure that we have the correct line selected after pretty printing.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_pretty-print.html";
+
+function test() {
+ // Wait for debugger panel to be fully set and break on debugger statement
+ let options = {
+ source: EXAMPLE_URL + "code_ugly.js",
+ line: 2
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ const gTab = aTab;
+ const gPanel = aPanel;
+ const gDebugger = gPanel.panelWin;
+
+ Task.spawn(function* () {
+ yield doResume(gPanel);
+
+ const paused = waitForPause(gDebugger.gThreadClient);
+ callInTab(gTab, "foo");
+ yield paused;
+
+ const finished = promise.all([
+ waitForSourceShown(gPanel, "code_ugly.js"),
+ waitForCaretUpdated(gPanel, 7)
+ ]);
+ gDebugger.document.getElementById("pretty-print").click();
+ yield finished;
+
+ resumeDebuggerThenCloseAndFinish(gPanel);
+ });
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-04.js b/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-04.js
new file mode 100644
index 000000000..a45aca91e
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-04.js
@@ -0,0 +1,50 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests that the function searching works with pretty printed sources.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_pretty-print.html";
+
+function test() {
+ // Wait for debugger panel to be fully set and break on debugger statement
+ let options = {
+ source: EXAMPLE_URL + "code_ugly.js",
+ line: 2
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ const gTab = aTab;
+ const gPanel = aPanel;
+ const gDebugger = gPanel.panelWin;
+ const gSearchBox = gDebugger.DebuggerView.Filtering._searchbox;
+
+ Task.spawn(function* () {
+ let popupShown = promise.defer();
+ once(gDebugger, "popupshown").then(() => {
+ ok(isCaretPos(gPanel, 2, 10),
+ "The bar function's non-pretty-printed location should be shown.");
+ popupShown.resolve();
+ });
+ setText(gSearchBox, "@bar");
+ yield popupShown.promise;
+
+ const finished = waitForSourceShown(gPanel, "code_ugly.js");
+ gDebugger.document.getElementById("pretty-print").click();
+ yield finished;
+
+ popupShown = promise.defer();
+ once(gDebugger, "popupshown").then(() => {
+ ok(isCaretPos(gPanel, 6, 10),
+ "The bar function's pretty printed location should be shown.");
+ popupShown.resolve();
+ });
+ setText(gSearchBox, "@bar");
+ yield popupShown.promise;
+
+ resumeDebuggerThenCloseAndFinish(gPanel);
+ });
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-05.js b/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-05.js
new file mode 100644
index 000000000..de1198103
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-05.js
@@ -0,0 +1,66 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure that prettifying HTML sources doesn't do anything.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_included-script.html";
+const SCRIPT_URL = EXAMPLE_URL + "code_location-changes.js";
+
+function test() {
+ let options = {
+ source: SCRIPT_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ const gTab = aTab;
+ const gPanel = aPanel;
+ const gDebugger = gPanel.panelWin;
+ const gEditor = gDebugger.DebuggerView.editor;
+ const gSources = gDebugger.DebuggerView.Sources;
+ const queries = gDebugger.require("./content/queries");
+ const constants = gDebugger.require("./content/constants");
+ const actions = bindActionCreators(gPanel);
+ const getState = gDebugger.DebuggerController.getState;
+
+ Task.spawn(function* () {
+ // Now, select the html page
+ const sourceShown = waitForSourceShown(gPanel, TAB_URL);
+ gSources.selectedValue = getSourceActor(gSources, TAB_URL);
+ yield sourceShown;
+
+ // From this point onward, the source editor's text should never change.
+ gEditor.once("change", () => {
+ ok(false, "The source editor text shouldn't have changed.");
+ });
+
+ is(getSelectedSourceURL(gSources), TAB_URL,
+ "The correct source is currently selected.");
+ ok(gEditor.getText().includes("myFunction"),
+ "The source shouldn't be pretty printed yet.");
+
+ const source = queries.getSelectedSource(getState());
+ try {
+ yield actions.togglePrettyPrint(source);
+ ok(false, "An error occurred while pretty-printing");
+ }
+ catch (err) {
+ is(err.message, "Can't prettify non-javascript files.",
+ "The promise was correctly rejected with a meaningful message.");
+ }
+
+ const { text } = yield queries.getSourceText(getState(), source.actor);
+ is(getSelectedSourceURL(gSources), TAB_URL,
+ "The correct source is still selected.");
+ ok(gEditor.getText().includes("myFunction"),
+ "The displayed source hasn't changed.");
+ ok(text.includes("myFunction"),
+ "The cached source text wasn't altered in any way.");
+
+ yield closeDebuggerAndFinish(gPanel);
+ });
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-06.js b/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-06.js
new file mode 100644
index 000000000..608df3140
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-06.js
@@ -0,0 +1,80 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure that prettifying JS sources with type errors works as expected.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_included-script.html";
+const JS_URL = EXAMPLE_URL + "code_location-changes.js";
+
+function test() {
+ let options = {
+ source: JS_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ const gTab = aTab;
+ const gPanel = aPanel;
+ const gDebugger = gPanel.panelWin;
+ const gClient = gDebugger.gClient;
+ const gEditor = gDebugger.DebuggerView.editor;
+ const gSources = gDebugger.DebuggerView.Sources;
+ const queries = gDebugger.require("./content/queries");
+ const constants = gDebugger.require("./content/constants");
+ const actions = bindActionCreators(gPanel);
+ const getState = gDebugger.DebuggerController.getState;
+ let gPrettyPrinted = false;
+
+ // We can't feed javascript files with syntax errors to the debugger,
+ // because they will never run, thus sometimes getting gc'd before the
+ // debugger is opened, or even before the target finishes navigating.
+ // Make the client lie about being able to parse perfectly fine code.
+ gClient.request = (function (aOriginalRequestMethod) {
+ return function (aPacket, aCallback) {
+ if (aPacket.type == "prettyPrint") {
+ gPrettyPrinted = true;
+ return promise.reject({ error: "prettyPrintError" });
+ }
+ return aOriginalRequestMethod(aPacket, aCallback);
+ };
+ }(gClient.request));
+
+ Task.spawn(function* () {
+ // From this point onward, the source editor's text should never change.
+ gEditor.once("change", () => {
+ ok(false, "The source editor text shouldn't have changed.");
+ });
+
+ is(getSelectedSourceURL(gSources), JS_URL,
+ "The correct source is currently selected.");
+ ok(gEditor.getText().includes("myFunction"),
+ "The source shouldn't be pretty printed yet.");
+
+ const source = queries.getSelectedSource(getState());
+ try {
+ yield actions.togglePrettyPrint(source);
+ ok(false, "The promise for a prettified source should be rejected!");
+ } catch (error) {
+ ok(error.error, "Error came from a RDP request");
+ ok(error.error.includes("prettyPrintError"),
+ "The promise was correctly rejected with a meaningful message.");
+ }
+
+ const { text } = yield queries.getSourceText(getState(), source.actor);
+ is(getSelectedSourceURL(gSources), JS_URL,
+ "The correct source is still selected.");
+ ok(gEditor.getText().includes("myFunction"),
+ "The displayed source hasn't changed.");
+ ok(text.includes("myFunction"),
+ "The cached source text wasn't altered in any way.");
+
+ is(gPrettyPrinted, true,
+ "The hijacked pretty print method was executed.");
+
+ yield closeDebuggerAndFinish(gPanel);
+ });
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-07.js b/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-07.js
new file mode 100644
index 000000000..4776d16ba
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-07.js
@@ -0,0 +1,62 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Test basic pretty printing functionality. Would be an xpcshell test, except
+// for bug 921252.
+
+var gTab, gPanel, gClient, gThreadClient, gSource;
+
+const TAB_URL = EXAMPLE_URL + "doc_pretty-print-2.html";
+
+function test() {
+ let options = {
+ source: EXAMPLE_URL + "code_ugly-2.js",
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gClient = gPanel.panelWin.gClient;
+ gThreadClient = gPanel.panelWin.DebuggerController.activeThread;
+
+ findSource();
+ });
+}
+
+function findSource() {
+ gThreadClient.getSources(({ error, sources }) => {
+ ok(!error);
+ sources = sources.filter(s => s.url.includes("code_ugly-2.js"));
+ is(sources.length, 1);
+ gSource = sources[0];
+ prettyPrintSource();
+ });
+}
+
+function prettyPrintSource() {
+ gThreadClient.source(gSource).prettyPrint(4, testPrettyPrinted);
+}
+
+function testPrettyPrinted({ error, source }) {
+ ok(!error, "Should not get an error while pretty-printing");
+ ok(source.includes("\n "),
+ "Source should be pretty-printed");
+ disablePrettyPrint();
+}
+
+function disablePrettyPrint() {
+ gThreadClient.source(gSource).disablePrettyPrint(testUgly);
+}
+
+function testUgly({ error, source }) {
+ ok(!error, "Should not get an error while disabling pretty-printing");
+ ok(!source.includes("\n "),
+ "Source should not be pretty after disabling pretty-printing");
+ closeDebuggerAndFinish(gPanel);
+}
+
+registerCleanupFunction(function () {
+ gTab = gPanel = gClient = gThreadClient = gSource = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-08.js b/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-08.js
new file mode 100644
index 000000000..e49910972
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-08.js
@@ -0,0 +1,99 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Test stepping through pretty printed sources.
+
+var gTab, gPanel, gClient, gThreadClient, gSource;
+
+const TAB_URL = EXAMPLE_URL + "doc_pretty-print-2.html";
+
+function test() {
+ let options = {
+ source: EXAMPLE_URL + "code_ugly-2.js",
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gClient = gPanel.panelWin.gClient;
+ gThreadClient = gPanel.panelWin.DebuggerController.activeThread;
+
+ findSource();
+ });
+}
+
+const BP_LOCATION = {
+ line: 5,
+ // column: 0
+};
+
+function findSource() {
+ gThreadClient.getSources(({ error, sources }) => {
+ ok(!error, "error should exist");
+ sources = sources.filter(s => s.url.includes("code_ugly-3.js"));
+ is(sources.length, 1, "sources.length should be 1");
+ [gSource] = sources;
+ BP_LOCATION.actor = gSource.actor;
+
+ prettyPrintSource(sources[0]);
+ });
+}
+
+function prettyPrintSource(source) {
+ gThreadClient.source(gSource).prettyPrint(2, runCode);
+}
+
+function runCode({ error }) {
+ ok(!error);
+ gClient.addOneTimeListener("paused", testDbgStatement);
+ callInTab(gTab, "main3");
+}
+
+function testDbgStatement(event, { why, frame }) {
+ is(why.type, "debuggerStatement");
+ const { source, line, column } = frame.where;
+ is(source.actor, BP_LOCATION.actor, "source.actor should be the right actor");
+ is(line, 3, "the line should be 3");
+ setBreakpoint();
+}
+
+function setBreakpoint() {
+ gThreadClient.source(gSource).setBreakpoint(
+ { line: BP_LOCATION.line,
+ column: BP_LOCATION.column },
+ ({ error, actualLocation }) => {
+ ok(!error, "error should not exist");
+ ok(!actualLocation, "actualLocation should not exist");
+ testStepping();
+ }
+ );
+}
+
+function testStepping() {
+ gClient.addOneTimeListener("paused", (event, { why, frame }) => {
+ is(why.type, "resumeLimit");
+ const { source, line } = frame.where;
+ is(source.actor, BP_LOCATION.actor, "source.actor should be the right actor");
+ is(line, 4, "the line should be 4");
+ testHitBreakpoint();
+ });
+ gThreadClient.stepIn();
+}
+
+function testHitBreakpoint() {
+ gClient.addOneTimeListener("paused", (event, { why, frame }) => {
+ is(why.type, "breakpoint");
+ const { source, line } = frame.where;
+ is(source.actor, BP_LOCATION.actor, "source.actor should be the right actor");
+ is(line, BP_LOCATION.line, "the line should the right line");
+
+ resumeDebuggerThenCloseAndFinish(gPanel);
+ });
+ gThreadClient.resume();
+}
+
+registerCleanupFunction(function () {
+ gTab = gPanel = gClient = gThreadClient = gSource = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-09.js b/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-09.js
new file mode 100644
index 000000000..16eab24ea
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-09.js
@@ -0,0 +1,92 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Test pretty printing source mapped sources.
+
+var gClient;
+var gThreadClient;
+var gSource;
+
+var gTab, gPanel, gClient, gThreadClient, gSource;
+
+const TAB_URL = EXAMPLE_URL + "doc_pretty-print-2.html";
+
+function test() {
+ let options = {
+ source: EXAMPLE_URL + "code_ugly-2.js",
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gClient = gPanel.panelWin.gClient;
+ gThreadClient = gPanel.panelWin.DebuggerController.activeThread;
+
+ findSource();
+ });
+}
+
+const dataUrl = s => "data:text/javascript," + s;
+
+// These should match the instructions in code_ugly-4.js.
+const A = "function a(){b()}";
+const A_URL = dataUrl(A);
+const B = "function b(){debugger}";
+const B_URL = dataUrl(B);
+
+function findSource() {
+ gThreadClient.getSources(({ error, sources }) => {
+ ok(!error);
+ sources = sources.filter(s => s.url === B_URL);
+ is(sources.length, 1);
+ gSource = sources[0];
+ prettyPrint();
+ });
+}
+
+function prettyPrint() {
+ gThreadClient.source(gSource).prettyPrint(2, runCode);
+}
+
+function runCode({ error }) {
+ ok(!error);
+ gClient.addOneTimeListener("paused", testDbgStatement);
+ callInTab(gTab, "a");
+}
+
+function testDbgStatement(event, { frame, why }) {
+ is(why.type, "debuggerStatement");
+ const { source, line } = frame.where;
+ is(source.url, B_URL);
+ is(line, 2);
+
+ disablePrettyPrint();
+}
+
+function disablePrettyPrint() {
+ gThreadClient.source(gSource).disablePrettyPrint(testUgly);
+}
+
+function testUgly({ error, source }) {
+ ok(!error);
+ ok(!source.includes("\n "));
+ getFrame();
+}
+
+function getFrame() {
+ gThreadClient.getFrames(0, 1, testFrame);
+}
+
+function testFrame({ frames: [frame] }) {
+ const { source, line } = frame.where;
+ is(source.url, B_URL);
+ is(line, 1);
+
+ resumeDebuggerThenCloseAndFinish(gPanel);
+}
+
+registerCleanupFunction(function () {
+ gTab = gPanel = gClient = gThreadClient = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-10.js b/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-10.js
new file mode 100644
index 000000000..862d553fb
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-10.js
@@ -0,0 +1,48 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure that we disable the pretty print button for black boxed sources,
+ * and that clicking it doesn't do anything.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_pretty-print.html";
+
+function test() {
+ // Wait for debugger panel to be fully set and break on debugger statement
+ let options = {
+ source: EXAMPLE_URL + "code_ugly.js",
+ line: 2
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ const gTab = aTab;
+ const gPanel = aPanel;
+ const gDebugger = gPanel.panelWin;
+ const gEditor = gDebugger.DebuggerView.editor;
+ const gSources = gDebugger.DebuggerView.Sources;
+ const queries = gDebugger.require("./content/queries");
+ const getState = gDebugger.DebuggerController.getState;
+
+ Task.spawn(function* () {
+ ok(!gEditor.getText().includes("\n "),
+ "The source shouldn't be pretty printed yet.");
+
+ yield toggleBlackBoxing(gPanel);
+
+ // Wait a tick before clicking to make sure the frontend's blackboxchange
+ // handlers have finished.
+ yield waitForTick();
+ gDebugger.document.getElementById("pretty-print").click();
+ // Make sure the text updates
+ yield waitForTick();
+
+ const source = queries.getSelectedSource(getState());
+ const { text } = queries.getSourceText(getState(), source.actor);
+ ok(!text.includes("\n "));
+
+ resumeDebuggerThenCloseAndFinish(gPanel);
+ });
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-11.js b/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-11.js
new file mode 100644
index 000000000..9e8bebc3f
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-11.js
@@ -0,0 +1,65 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure that pretty printing is maintained across refreshes.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_pretty-print.html";
+
+var gTab, gPanel, gDebugger;
+var gEditor, gSources;
+
+function test() {
+ // Wait for debugger panel to be fully set and break on debugger statement
+ let options = {
+ source: EXAMPLE_URL + "code_ugly.js",
+ line: 2
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gEditor = gDebugger.DebuggerView.editor;
+ gSources = gDebugger.DebuggerView.Sources;
+
+ testSourceIsUgly();
+ const finished = waitForCaretUpdated(gPanel, 7);
+ clickPrettyPrintButton();
+ finished.then(testSourceIsPretty)
+ .then(() => {
+ const finished = waitForCaretUpdated(gPanel, 7);
+ const reloaded = reloadActiveTab(gPanel, gDebugger.EVENTS.SOURCE_SHOWN);
+ return Promise.all([finished, reloaded]);
+ })
+ .then(testSourceIsPretty)
+ .then(() => resumeDebuggerThenCloseAndFinish(gPanel))
+ .then(null, aError => {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(aError));
+ });
+ });
+}
+
+function testSourceIsUgly() {
+ ok(!gEditor.getText().includes("\n "),
+ "The source shouldn't be pretty printed yet.");
+}
+
+function clickPrettyPrintButton() {
+ gDebugger.document.getElementById("pretty-print").click();
+}
+
+function testSourceIsPretty() {
+ ok(gEditor.getText().includes("\n "),
+ "The source should be pretty printed.");
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gEditor = null;
+ gSources = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-12.js b/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-12.js
new file mode 100644
index 000000000..57ce54fc5
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-12.js
@@ -0,0 +1,51 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure that we don't leave the pretty print button checked when we fail to
+ * pretty print a source (because it isn't a JS file, for example).
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_blackboxing.html";
+const SCRIPT_URL = EXAMPLE_URL + "code_blackboxing_blackboxme.js";
+
+function test() {
+ let options = {
+ source: SCRIPT_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ const gTab = aTab;
+ const gPanel = aPanel;
+ const gDebugger = gPanel.panelWin;
+ const gEditor = gDebugger.DebuggerView.editor;
+ const gSources = gDebugger.DebuggerView.Sources;
+ const queries = gDebugger.require("./content/queries");
+ const constants = gDebugger.require("./content/constants");
+ const actions = bindActionCreators(gPanel);
+ const getState = gDebugger.DebuggerController.getState;
+
+ Task.spawn(function* () {
+ const source = getSourceForm(gSources, TAB_URL);
+ let shown = ensureSourceIs(gPanel, TAB_URL, true);
+ actions.selectSource(source);
+ yield shown;
+
+ try {
+ yield actions.togglePrettyPrint(source);
+ ok(false, "An error occurred while pretty-printing");
+ }
+ catch (err) {
+ is(err.message, "Can't prettify non-javascript files.",
+ "The promise was correctly rejected with a meaningful message.");
+ }
+
+ is(gDebugger.document.getElementById("pretty-print").checked, false,
+ "The button shouldn't be checked after trying to pretty print a non-js file.");
+
+ closeDebuggerAndFinish(gPanel);
+ });
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-13.js b/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-13.js
new file mode 100644
index 000000000..7409f88f5
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-13.js
@@ -0,0 +1,53 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure that clicking the pretty print button prettifies the source, even
+ * when the source URL does not end in ".js", but the content type is
+ * JavaScript.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_pretty-print-3.html";
+
+function test() {
+ // Wait for debugger panel to be fully set and break on debugger statement
+ let options = {
+ source: EXAMPLE_URL + "code_ugly-8",
+ line: 2
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ const gTab = aTab;
+ const gPanel = aPanel;
+ const gDebugger = gPanel.panelWin;
+ const gEditor = gDebugger.DebuggerView.editor;
+ const gSources = gDebugger.DebuggerView.Sources;
+ const queries = gDebugger.require("./content/queries");
+ const constants = gDebugger.require("./content/constants");
+ const actions = bindActionCreators(gPanel);
+ const getState = gDebugger.DebuggerController.getState;
+
+ Task.spawn(function* () {
+ ok(!gEditor.getText().includes("\n "),
+ "The source shouldn't be pretty printed yet.");
+
+ const finished = waitForSourceShown(gPanel, "code_ugly-8");
+ gDebugger.document.getElementById("pretty-print").click();
+ const deck = gDebugger.document.getElementById("editor-deck");
+ is(deck.selectedIndex, 2, "The progress bar should be shown");
+ yield finished;
+
+ ok(gEditor.getText().includes("\n "),
+ "The source should be pretty printed.");
+ is(deck.selectedIndex, 0, "The editor should be shown");
+
+ const source = queries.getSelectedSource(getState());
+ const { text } = queries.getSourceText(getState(), source.actor);
+ ok(text.includes("\n "),
+ "Subsequent calls to getText return the pretty printed source.");
+
+ resumeDebuggerThenCloseAndFinish(gPanel);
+ })
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-on-paused.js b/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-on-paused.js
new file mode 100644
index 000000000..12e3a20fc
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-on-paused.js
@@ -0,0 +1,69 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Test that pretty printing when the debugger is paused does not switch away
+ * from the selected source.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_pretty-print-on-paused.html";
+
+var gTab, gPanel, gDebugger, gThreadClient, gSources;
+
+const SECOND_SOURCE_VALUE = EXAMPLE_URL + "code_ugly-2.js";
+
+function test() {
+ // Wait for debugger panel to be fully set and break on debugger statement
+ let options = {
+ source: EXAMPLE_URL + "code_script-switching-02.js",
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gThreadClient = gDebugger.gThreadClient;
+ gSources = gDebugger.DebuggerView.Sources;
+
+ Task.spawn(function* () {
+ try {
+ yield doInterrupt(gPanel);
+
+ let source = gThreadClient.source(getSourceForm(gSources, SECOND_SOURCE_VALUE));
+ yield source.setBreakpoint({
+ line: 6
+ });
+ yield doResume(gPanel);
+
+ const bpHit = waitForCaretAndScopes(gPanel, 6);
+ callInTab(gTab, "secondCall");
+ yield bpHit;
+
+ info("Switch to the second source.");
+ const sourceShown = waitForSourceShown(gPanel, SECOND_SOURCE_VALUE);
+ gSources.selectedValue = getSourceActor(gSources, SECOND_SOURCE_VALUE);
+ yield sourceShown;
+
+ info("Pretty print the source.");
+ const prettyPrinted = waitForSourceShown(gPanel, SECOND_SOURCE_VALUE);
+ gDebugger.document.getElementById("pretty-print").click();
+ yield prettyPrinted;
+
+ yield resumeDebuggerThenCloseAndFinish(gPanel);
+ } catch (e) {
+ DevToolsUtils.reportException("browser_dbg_pretty-print-on-paused.js", e);
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ }
+ });
+ });
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gThreadClient = null;
+ gSources = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_progress-listener-bug.js b/devtools/client/debugger/test/mochitest/browser_dbg_progress-listener-bug.js
new file mode 100644
index 000000000..e5aac615a
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_progress-listener-bug.js
@@ -0,0 +1,89 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests that the debugger does show up even if a progress listener reads the
+ * WebProgress argument's DOMWindow property in onStateChange() (bug 771655).
+ */
+
+var gTab, gPanel, gDebugger;
+var gOldListener;
+
+const TAB_URL = EXAMPLE_URL + "doc_inline-script.html";
+
+function test() {
+ installListener();
+
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+
+ is(!!gDebugger.DebuggerController._startup, true,
+ "Controller should be initialized after starting the test.");
+
+ testPause();
+ });
+}
+
+function testPause() {
+ let onCaretUpdated = waitForCaretUpdated(gPanel, 16);
+ callInTab(gTab, "runDebuggerStatement");
+ onCaretUpdated.then(() => {
+ is(gDebugger.gThreadClient.state, "paused",
+ "The debugger statement was reached.");
+
+ resumeDebuggerThenCloseAndFinish(gPanel);
+ });
+}
+
+// This is taken almost verbatim from bug 771655.
+function installListener() {
+ if ("_testPL" in window) {
+ gOldListener = _testPL;
+
+ Cc["@mozilla.org/docloaderservice;1"]
+ .getService(Ci.nsIWebProgress)
+ .removeProgressListener(_testPL);
+ }
+
+ window._testPL = {
+ START_DOC: Ci.nsIWebProgressListener.STATE_START |
+ Ci.nsIWebProgressListener.STATE_IS_DOCUMENT,
+ onStateChange: function (wp, req, stateFlags, status) {
+ if ((stateFlags & this.START_DOC) === this.START_DOC) {
+ // This DOMWindow access triggers the unload event.
+ wp.DOMWindow;
+ }
+ },
+ QueryInterface: function (iid) {
+ if (iid.equals(Ci.nsISupportsWeakReference) ||
+ iid.equals(Ci.nsIWebProgressListener))
+ return this;
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ }
+ };
+
+ Cc["@mozilla.org/docloaderservice;1"]
+ .getService(Ci.nsIWebProgress)
+ .addProgressListener(_testPL, Ci.nsIWebProgress.NOTIFY_STATE_REQUEST);
+}
+
+registerCleanupFunction(function () {
+ if (gOldListener) {
+ window._testPL = gOldListener;
+ } else {
+ delete window._testPL;
+ }
+
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gOldListener = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_promises-allocation-stack.js b/devtools/client/debugger/test/mochitest/browser_dbg_promises-allocation-stack.js
new file mode 100644
index 000000000..2d55c3a8d
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_promises-allocation-stack.js
@@ -0,0 +1,87 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Test that we can get a stack to a promise's allocation point.
+ */
+
+"use strict";
+
+const TAB_URL = EXAMPLE_URL + "doc_promise-get-allocation-stack.html";
+const { PromisesFront } = require("devtools/shared/fronts/promises");
+var events = require("sdk/event/core");
+
+function test() {
+ Task.spawn(function* () {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ const [ tab,, panel ] = yield initDebugger(TAB_URL, options);
+
+ let client = new DebuggerClient(DebuggerServer.connectPipe());
+ yield connect(client);
+
+ let { tabs } = yield listTabs(client);
+ let targetTab = findTab(tabs, TAB_URL);
+ yield attachTab(client, targetTab);
+
+ yield testGetAllocationStack(client, targetTab, tab);
+
+ yield close(client);
+ yield closeDebuggerAndFinish(panel);
+ }).then(null, error => {
+ ok(false, "Got an error: " + error.message + "\n" + error.stack);
+ });
+}
+
+function* testGetAllocationStack(client, form, tab) {
+ 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);
+ }
+ }
+ });
+ });
+
+ callInTab(tab, "makePromises");
+
+ let grip = yield onNewPromise;
+ ok(grip, "Found our promise p");
+
+ let objectClient = new ObjectClient(client, grip);
+ ok(objectClient, "Got Object Client");
+
+ yield new Promise(resolve => {
+ objectClient.getPromiseAllocationStack(response => {
+ ok(response.allocationStack.length, "Got promise allocation stack.");
+
+ for (let stack of response.allocationStack) {
+ is(stack.source.url, TAB_URL, "Got correct source URL.");
+ is(stack.functionDisplayName, "makePromises",
+ "Got correct function display name.");
+ is(typeof stack.line, "number", "Expect stack line to be a number.");
+ is(typeof stack.column, "number",
+ "Expect stack column to be a number.");
+ }
+
+ resolve();
+ });
+ });
+
+ yield front.detach();
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_promises-chrome-allocation-stack.js b/devtools/client/debugger/test/mochitest/browser_dbg_promises-chrome-allocation-stack.js
new file mode 100644
index 000000000..48e9ab229
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_promises-chrome-allocation-stack.js
@@ -0,0 +1,100 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Test that we can get a stack to a promise's allocation point in the chrome
+ * process.
+ */
+
+"use strict";
+
+const SOURCE_URL = "browser_dbg_promises-chrome-allocation-stack.js";
+const PromisesFront = require("devtools/shared/fronts/promises");
+var events = require("sdk/event/core");
+
+const STACK_DATA = [
+ { functionDisplayName: "test/</<" },
+ { functionDisplayName: "testGetAllocationStack" },
+];
+
+function test() {
+ Task.spawn(function* () {
+ requestLongerTimeout(10);
+
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ DebuggerServer.allowChromeProcess = true;
+
+ let client = new DebuggerClient(DebuggerServer.connectPipe());
+ yield connect(client);
+ let chrome = yield client.getProcess();
+ let [, tabClient] = yield attachTab(client, chrome.form);
+ yield tabClient.attachThread();
+
+ yield testGetAllocationStack(client, chrome.form, () => {
+ let p = new Promise(() => {});
+ p.name = "p";
+ let q = p.then();
+ q.name = "q";
+ let r = p.then(null, () => {});
+ r.name = "r";
+ });
+
+ yield close(client);
+ finish();
+ }).then(null, error => {
+ ok(false, "Got an error: " + error.message + "\n" + error.stack);
+ });
+}
+
+function* testGetAllocationStack(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);
+ }
+ }
+ });
+ });
+
+ makePromises();
+
+ let grip = yield onNewPromise;
+ ok(grip, "Found our promise p");
+
+ let objectClient = new ObjectClient(client, grip);
+ ok(objectClient, "Got Object Client");
+
+ yield new Promise(resolve => {
+ objectClient.getPromiseAllocationStack(response => {
+ ok(response.allocationStack.length, "Got promise allocation stack.");
+
+ for (let i = 0; i < STACK_DATA.length; i++) {
+ let data = STACK_DATA[i];
+ let stack = response.allocationStack[i];
+
+ ok(stack.source.url.startsWith("chrome:"), "Got a chrome source URL");
+ ok(stack.source.url.endsWith(SOURCE_URL), "Got correct source URL.");
+ is(stack.functionDisplayName, data.functionDisplayName,
+ "Got correct function display name.");
+ is(typeof stack.line, "number", "Expect stack line to be a number.");
+ is(typeof stack.column, "number",
+ "Expect stack column to be a number.");
+ }
+
+ resolve();
+ });
+ });
+
+ yield front.detach();
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_promises-fulfillment-stack.js b/devtools/client/debugger/test/mochitest/browser_dbg_promises-fulfillment-stack.js
new file mode 100644
index 000000000..a5f592eb6
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_promises-fulfillment-stack.js
@@ -0,0 +1,106 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Test that we can get a stack to a promise's fulfillment point.
+ */
+
+"use strict";
+
+const TAB_URL = EXAMPLE_URL + "doc_promise-get-fulfillment-stack.html";
+const { PromisesFront } = require("devtools/shared/fronts/promises");
+var events = require("sdk/event/core");
+
+const TEST_DATA = [
+ {
+ functionDisplayName: "returnPromise/<",
+ line: 19,
+ column: 37
+ },
+ {
+ functionDisplayName: "returnPromise",
+ line: 19,
+ column: 14
+ },
+ {
+ functionDisplayName: "makePromise",
+ line: 14,
+ column: 15
+ },
+];
+
+function test() {
+ Task.spawn(function* () {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ const [ tab,, panel ] = yield initDebugger(TAB_URL, options);
+
+ let client = new DebuggerClient(DebuggerServer.connectPipe());
+ yield connect(client);
+
+ let { tabs } = yield listTabs(client);
+ let targetTab = findTab(tabs, TAB_URL);
+ yield attachTab(client, targetTab);
+
+ yield testGetFulfillmentStack(client, targetTab, tab);
+
+ yield close(client);
+ yield closeDebuggerAndFinish(panel);
+ }).then(null, error => {
+ ok(false, "Got an error: " + error.message + "\n" + error.stack);
+ });
+}
+
+function* testGetFulfillmentStack(client, form, tab) {
+ 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);
+ }
+ }
+ });
+ });
+
+ callInTab(tab, "makePromise");
+
+ let grip = yield onNewPromise;
+ ok(grip, "Found our promise p");
+
+ let objectClient = new ObjectClient(client, grip);
+ ok(objectClient, "Got Object Client");
+
+ yield new Promise(resolve => {
+ objectClient.getPromiseFulfillmentStack(response => {
+ ok(response.fulfillmentStack.length, "Got promise allocation stack.");
+
+ for (let i = 0; i < TEST_DATA.length; i++) {
+ let stack = response.fulfillmentStack[i];
+ let data = TEST_DATA[i];
+ is(stack.source.url, TAB_URL, "Got correct source URL.");
+ is(stack.functionDisplayName, data.functionDisplayName,
+ "Got correct function display name.");
+ is(stack.line, data.line, "Got correct stack line number.");
+ is(stack.column, data.column, "Got correct stack column number.");
+ }
+
+ resolve();
+ });
+ });
+
+ yield front.detach();
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_promises-rejection-stack.js b/devtools/client/debugger/test/mochitest/browser_dbg_promises-rejection-stack.js
new file mode 100644
index 000000000..9434024e3
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_promises-rejection-stack.js
@@ -0,0 +1,106 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Test that we can get a stack to a promise's rejection point.
+ */
+
+"use strict";
+
+const TAB_URL = EXAMPLE_URL + "doc_promise-get-rejection-stack.html";
+const { PromisesFront } = require("devtools/shared/fronts/promises");
+var events = require("sdk/event/core");
+
+const TEST_DATA = [
+ {
+ functionDisplayName: "returnPromise/<",
+ line: 19,
+ column: 47
+ },
+ {
+ functionDisplayName: "returnPromise",
+ line: 19,
+ column: 14
+ },
+ {
+ functionDisplayName: "makePromise",
+ line: 14,
+ column: 15
+ },
+];
+
+function test() {
+ Task.spawn(function* () {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ const [ tab,, panel ] = yield initDebugger(TAB_URL, options);
+
+ let client = new DebuggerClient(DebuggerServer.connectPipe());
+ yield connect(client);
+
+ let { tabs } = yield listTabs(client);
+ let targetTab = findTab(tabs, TAB_URL);
+ yield attachTab(client, targetTab);
+
+ yield testGetRejectionStack(client, targetTab, tab);
+
+ yield close(client);
+ yield closeDebuggerAndFinish(panel);
+ }).then(null, error => {
+ ok(false, "Got an error: " + error.message + "\n" + error.stack);
+ });
+}
+
+function* testGetRejectionStack(client, form, tab) {
+ 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);
+ }
+ }
+ });
+ });
+
+ callInTab(tab, "makePromise");
+
+ let grip = yield onNewPromise;
+ ok(grip, "Found our promise p");
+
+ let objectClient = new ObjectClient(client, grip);
+ ok(objectClient, "Got Object Client");
+
+ yield new Promise(resolve => {
+ objectClient.getPromiseRejectionStack(response => {
+ ok(response.rejectionStack.length, "Got promise allocation stack.");
+
+ for (let i = 0; i < TEST_DATA.length; i++) {
+ let stack = response.rejectionStack[i];
+ let data = TEST_DATA[i];
+ is(stack.source.url, TAB_URL, "Got correct source URL.");
+ is(stack.functionDisplayName, data.functionDisplayName,
+ "Got correct function display name.");
+ is(stack.line, data.line, "Got correct stack line number.");
+ is(stack.column, data.column, "Got correct stack column number.");
+ }
+
+ resolve();
+ });
+ });
+
+ yield front.detach();
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_reload-preferred-script-02.js b/devtools/client/debugger/test/mochitest/browser_dbg_reload-preferred-script-02.js
new file mode 100644
index 000000000..75e7cfc1c
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_reload-preferred-script-02.js
@@ -0,0 +1,50 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests if the preferred source is shown when a page is loaded and
+ * the preferred source is specified after another source might have been shown.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
+const PREFERRED_URL = EXAMPLE_URL + "code_script-switching-02.js";
+
+var gTab, gPanel, gDebugger;
+var gSources;
+
+function test() {
+ let options = {
+ source: EXAMPLE_URL + "code_script-switching-01.js",
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gSources = gDebugger.DebuggerView.Sources;
+
+ waitForSourceShown(gPanel, PREFERRED_URL).then(finishTest);
+ gSources.preferredSource = getSourceActor(gSources, PREFERRED_URL);
+ });
+}
+
+function finishTest() {
+ info("Currently preferred source: " + gSources.preferredValue);
+ info("Currently selected source: " + gSources.selectedValue);
+
+ is(getSourceURL(gSources, gSources.preferredValue), PREFERRED_URL,
+ "The preferred source url wasn't set correctly.");
+ is(getSourceURL(gSources, gSources.selectedValue), PREFERRED_URL,
+ "The selected source isn't the correct one.");
+
+ closeDebuggerAndFinish(gPanel);
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gSources = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_reload-preferred-script-03.js b/devtools/client/debugger/test/mochitest/browser_dbg_reload-preferred-script-03.js
new file mode 100644
index 000000000..d286a5072
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_reload-preferred-script-03.js
@@ -0,0 +1,62 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests if the preferred source is shown when a page is loaded and
+ * the preferred source is specified after another source was definitely shown.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
+const FIRST_URL = EXAMPLE_URL + "code_script-switching-01.js";
+const SECOND_URL = EXAMPLE_URL + "code_script-switching-02.js";
+
+var gTab, gPanel, gDebugger;
+var gSources;
+
+function test() {
+ let options = {
+ source: FIRST_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gSources = gDebugger.DebuggerView.Sources;
+
+ testSource(undefined, FIRST_URL);
+ switchToSource(SECOND_URL)
+ .then(() => testSource(SECOND_URL))
+ .then(() => switchToSource(FIRST_URL))
+ .then(() => testSource(FIRST_URL))
+ .then(() => closeDebuggerAndFinish(gPanel))
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+ });
+}
+
+function testSource(aPreferredUrl, aSelectedUrl = aPreferredUrl) {
+ info("Currently preferred source: " + gSources.preferredValue);
+ info("Currently selected source: " + gSources.selectedValue);
+
+ is(getSourceURL(gSources, gSources.preferredValue), aPreferredUrl,
+ "The preferred source url wasn't set correctly.");
+ is(getSourceURL(gSources, gSources.selectedValue), aSelectedUrl,
+ "The selected source isn't the correct one.");
+}
+
+function switchToSource(aUrl) {
+ let finished = waitForSourceShown(gPanel, aUrl);
+ gSources.preferredSource = getSourceActor(gSources, aUrl);
+ return finished;
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gSources = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_reload-same-script.js b/devtools/client/debugger/test/mochitest/browser_dbg_reload-same-script.js
new file mode 100644
index 000000000..754599418
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_reload-same-script.js
@@ -0,0 +1,88 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests if the same source is shown after a page is reloaded.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
+const FIRST_URL = EXAMPLE_URL + "code_script-switching-01.js";
+const SECOND_URL = EXAMPLE_URL + "code_script-switching-02.js";
+
+function test() {
+ // Debug test slaves are a bit slow at this test.
+ requestLongerTimeout(2);
+
+ let options = {
+ source: FIRST_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ const gTab = aTab;
+ const gPanel = aPanel;
+ const gDebugger = aPanel.panelWin;
+ const gTarget = gDebugger.gTarget;
+ const gSources = gDebugger.DebuggerView.Sources;
+ const queries = gDebugger.require("./content/queries");
+ const actions = bindActionCreators(gPanel);
+ const getState = gDebugger.DebuggerController.getState;
+ let gStep = 0;
+
+ function reloadPage() {
+ const navigated = waitForNavigation(gPanel);
+ reload(gPanel);
+ return navigated;
+ }
+
+ function switchAndReload(aUrl) {
+ actions.selectSource(getSourceForm(gSources, aUrl));
+ return reloadPage();
+ }
+
+ function testCurrentSource(aUrl, aExpectedUrl = aUrl) {
+ const prefSource = getSourceURL(gSources, gSources.preferredValue);
+ const selSource = getSourceURL(gSources, gSources.selectedValue);
+
+ info("Currently preferred source: '" + prefSource + "'.");
+ info("Currently selected source: '" + selSource + "'.");
+
+ is(prefSource, aExpectedUrl,
+ "The preferred source url wasn't set correctly (" + gStep + ").");
+ is(selSource, aUrl,
+ "The selected source isn't the correct one (" + gStep + ").");
+ }
+
+ function performTest() {
+ switch (gStep++) {
+ case 0:
+ testCurrentSource(FIRST_URL, null);
+ reloadPage().then(performTest);
+ break;
+ case 1:
+ testCurrentSource(FIRST_URL);
+ reloadPage().then(performTest);
+ break;
+ case 2:
+ testCurrentSource(FIRST_URL);
+ switchAndReload(SECOND_URL).then(performTest);
+ break;
+ case 3:
+ testCurrentSource(SECOND_URL);
+ reloadPage().then(performTest);
+ break;
+ case 4:
+ testCurrentSource(SECOND_URL);
+ reloadPage().then(performTest);
+ break;
+ case 5:
+ testCurrentSource(SECOND_URL);
+ closeDebuggerAndFinish(gPanel);
+ break;
+ }
+ }
+
+ performTest();
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_scripts-switching-01.js b/devtools/client/debugger/test/mochitest/browser_dbg_scripts-switching-01.js
new file mode 100644
index 000000000..b213040c0
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_scripts-switching-01.js
@@ -0,0 +1,162 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure that switching the displayed source in the UI works as advertised.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
+
+function test() {
+ let options = {
+ source: EXAMPLE_URL + "code_script-switching-01.js",
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ const gTab = aTab;
+ const gPanel = aPanel;
+ const gDebugger = gPanel.panelWin;
+ const gEditor = gDebugger.DebuggerView.editor;
+ const gSources = gDebugger.DebuggerView.Sources;
+
+ const gLabel1 = "code_script-switching-01.js";
+ const gLabel2 = "code_script-switching-02.js";
+
+ function testSourcesDisplay() {
+ let deferred = promise.defer();
+
+ is(gSources.itemCount, 2,
+ "Found the expected number of sources. (1)");
+
+ is(gSources.items[0].target.querySelector(".dbg-source-item").getAttribute("tooltiptext"),
+ EXAMPLE_URL + "code_script-switching-01.js",
+ "The correct tooltip text is displayed for the first source. (1)");
+ is(gSources.items[1].target.querySelector(".dbg-source-item").getAttribute("tooltiptext"),
+ EXAMPLE_URL + "code_script-switching-02.js",
+ "The correct tooltip text is displayed for the second source. (1)");
+
+ ok(getSourceActor(gSources, EXAMPLE_URL + gLabel1),
+ "First source url is incorrect. (1)");
+ ok(getSourceActor(gSources, EXAMPLE_URL + gLabel2),
+ "Second source url is incorrect. (1)");
+
+ ok(gSources.getItemForAttachment(e => e.label == gLabel1),
+ "First source label is incorrect. (1)");
+ ok(gSources.getItemForAttachment(e => e.label == gLabel2),
+ "Second source label is incorrect. (1)");
+
+ ok(gSources.selectedItem,
+ "There should be a selected item in the sources pane. (1)");
+ is(getSelectedSourceURL(gSources), EXAMPLE_URL + gLabel2,
+ "The selected value is the sources pane is incorrect. (1)");
+
+ is(gEditor.getText().search(/firstCall/), -1,
+ "The first source is not displayed. (1)");
+ is(gEditor.getText().search(/debugger/), 166,
+ "The second source is displayed. (1)");
+
+ ok(isCaretPos(gPanel, 6),
+ "Editor caret location is correct. (1)");
+
+ // The editor's debug location takes a tick to update.
+ is(gEditor.getDebugLocation(), 5,
+ "Editor debugger location is correct. (1)");
+ ok(gEditor.hasLineClass(5, "debug-line"),
+ "The debugged line is highlighted appropriately (1).");
+
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.SOURCE_SHOWN).then(deferred.resolve);
+ gSources.selectedIndex = 0;
+
+ return deferred.promise;
+ }
+
+ function testSwitchPaused1() {
+ let deferred = promise.defer();
+
+ ok(gSources.selectedItem,
+ "There should be a selected item in the sources pane. (2)");
+ is(getSelectedSourceURL(gSources), EXAMPLE_URL + gLabel1,
+ "The selected value is the sources pane is incorrect. (2)");
+
+ is(gEditor.getText().search(/firstCall/), 118,
+ "The first source is displayed. (2)");
+ is(gEditor.getText().search(/debugger/), -1,
+ "The second source is not displayed. (2)");
+
+ // The editor's debug location takes a tick to update.
+ ok(isCaretPos(gPanel, 1),
+ "Editor caret location is correct. (2)");
+ is(gEditor.getDebugLocation(), null,
+ "Editor debugger location is correct. (2)");
+ ok(!gEditor.hasLineClass(5, "debug-line"),
+ "The debugged line highlight was removed. (2)");
+
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.SOURCE_SHOWN).then(deferred.resolve);
+ gSources.selectedIndex = 1;
+ return deferred.promise;
+ }
+
+ function testSwitchPaused2() {
+ let deferred = promise.defer();
+
+ ok(gSources.selectedItem,
+ "There should be a selected item in the sources pane. (3)");
+ is(getSelectedSourceURL(gSources), EXAMPLE_URL + gLabel2,
+ "The selected value is the sources pane is incorrect. (3)");
+
+ is(gEditor.getText().search(/firstCall/), -1,
+ "The first source is not displayed. (3)");
+ is(gEditor.getText().search(/debugger/), 166,
+ "The second source is displayed. (3)");
+
+ ok(isCaretPos(gPanel, 6),
+ "Editor caret location is correct. (3)");
+ is(gEditor.getDebugLocation(), 5,
+ "Editor debugger location is correct. (3)");
+ ok(gEditor.hasLineClass(5, "debug-line"),
+ "The debugged line is highlighted appropriately (3).");
+
+ // Step out twice.
+ waitForThreadEvents(gPanel, "paused").then(() => {
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.SOURCE_SHOWN).then(deferred.resolve);
+ gDebugger.gThreadClient.stepOut();
+ });
+ gDebugger.gThreadClient.stepOut();
+
+ return deferred.promise;
+ }
+
+ function testSwitchRunning() {
+ ok(gSources.selectedItem,
+ "There should be a selected item in the sources pane. (4)");
+ is(getSelectedSourceURL(gSources), EXAMPLE_URL + gLabel1,
+ "The selected value is the sources pane is incorrect. (4)");
+
+ is(gEditor.getText().search(/firstCall/), 118,
+ "The first source is displayed. (4)");
+ is(gEditor.getText().search(/debugger/), -1,
+ "The second source is not displayed. (4)");
+
+ ok(isCaretPos(gPanel, 6),
+ "Editor caret location is correct. (4)");
+ is(gEditor.getDebugLocation(), 5,
+ "Editor debugger location is correct. (4)");
+ ok(gEditor.hasLineClass(5, "debug-line"),
+ "The debugged line is highlighted appropriately (3). (4)");
+ }
+
+ Task.spawn(function* () {
+ const shown = waitForSourceAndCaretAndScopes(gPanel, "-02.js", 1);
+ callInTab(gTab, "firstCall");
+ yield shown;
+
+ yield testSourcesDisplay();
+ yield testSwitchPaused1();
+ yield testSwitchPaused2();
+ yield testSwitchRunning();
+ resumeDebuggerThenCloseAndFinish(gPanel);
+ });
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_scripts-switching-02.js b/devtools/client/debugger/test/mochitest/browser_dbg_scripts-switching-02.js
new file mode 100644
index 000000000..0d524db2c
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_scripts-switching-02.js
@@ -0,0 +1,163 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure that switching the displayed source in the UI works as advertised.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_script-switching-02.html";
+
+var gLabel1 = "code_script-switching-01.js";
+var gLabel2 = "code_script-switching-02.js";
+var gParams = "?foo=bar,baz|lol";
+
+function test() {
+ let options = {
+ source: EXAMPLE_URL + "code_script-switching-01.js",
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ const gTab = aTab;
+ const gPanel = aPanel;
+ const gDebugger = gPanel.panelWin;
+ const gEditor = gDebugger.DebuggerView.editor;
+ const gSources = gDebugger.DebuggerView.Sources;
+
+ function testSourcesDisplay() {
+ let deferred = promise.defer();
+
+ is(gSources.itemCount, 2,
+ "Found the expected number of sources. (1)");
+
+ ok(getSourceActor(gSources, EXAMPLE_URL + gLabel1),
+ "First source url is incorrect. (1)");
+ ok(getSourceActor(gSources, EXAMPLE_URL + gLabel2 + gParams),
+ "Second source url is incorrect. (1)");
+
+ ok(gSources.getItemForAttachment(e => e.label == gLabel1),
+ "First source label is incorrect. (1)");
+ ok(gSources.getItemForAttachment(e => e.label == gLabel2),
+ "Second source label is incorrect. (1)");
+
+ ok(gSources.selectedItem,
+ "There should be a selected item in the sources pane. (1)");
+ is(getSelectedSourceURL(gSources), EXAMPLE_URL + gLabel2 + gParams,
+ "The selected value is the sources pane is incorrect. (1)");
+
+ is(gEditor.getText().search(/firstCall/), -1,
+ "The first source is not displayed. (1)");
+ is(gEditor.getText().search(/debugger/), 166,
+ "The second source is displayed. (1)");
+
+ ok(isCaretPos(gPanel, 6),
+ "Editor caret location is correct. (1)");
+ is(gEditor.getDebugLocation(), 5,
+ "Editor debugger location is correct. (1)");
+ ok(gEditor.hasLineClass(5, "debug-line"),
+ "The debugged line is highlighted appropriately. (1)");
+
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.SOURCE_SHOWN).then(deferred.resolve);
+ gSources.selectedItem = e => e.attachment.label == gLabel1;
+
+ return deferred.promise;
+ }
+
+ function testSwitchPaused1() {
+ let deferred = promise.defer();
+
+ ok(gSources.selectedItem,
+ "There should be a selected item in the sources pane. (2)");
+ is(getSelectedSourceURL(gSources), EXAMPLE_URL + gLabel1,
+ "The selected value is the sources pane is incorrect. (2)");
+
+ is(gEditor.getText().search(/firstCall/), 118,
+ "The first source is displayed. (2)");
+ is(gEditor.getText().search(/debugger/), -1,
+ "The second source is not displayed. (2)");
+
+ // The editor's debug location takes a tick to update.
+ ok(isCaretPos(gPanel, 1),
+ "Editor caret location is correct. (2)");
+
+ is(gEditor.getDebugLocation(), null,
+ "Editor debugger location is correct. (2)");
+ ok(!gEditor.hasLineClass(5, "debug-line"),
+ "The debugged line highlight was removed. (2)");
+
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.SOURCE_SHOWN).then(deferred.resolve);
+ gSources.selectedItem = e => e.attachment.label == gLabel2;
+
+ return deferred.promise;
+ }
+
+ function testSwitchPaused2() {
+ let deferred = promise.defer();
+
+ ok(gSources.selectedItem,
+ "There should be a selected item in the sources pane. (3)");
+ is(getSelectedSourceURL(gSources), EXAMPLE_URL + gLabel2 + gParams,
+ "The selected value is the sources pane is incorrect. (3)");
+
+ is(gEditor.getText().search(/firstCall/), -1,
+ "The first source is not displayed. (3)");
+ is(gEditor.getText().search(/debugger/), 166,
+ "The second source is displayed. (3)");
+
+ // The editor's debug location takes a tick to update.
+ ok(isCaretPos(gPanel, 6),
+ "Editor caret location is correct. (3)");
+ is(gEditor.getDebugLocation(), 5,
+ "Editor debugger location is correct. (3)");
+ ok(gEditor.hasLineClass(5, "debug-line"),
+ "The debugged line is highlighted appropriately. (3)");
+
+ // Step out three times.
+ waitForThreadEvents(gPanel, "paused").then(() => {
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.SOURCE_SHOWN).then(deferred.resolve);
+ gDebugger.gThreadClient.stepOut();
+ });
+ gDebugger.gThreadClient.stepOut();
+
+ return deferred.promise;
+ }
+
+ function testSwitchRunning() {
+ let deferred = promise.defer();
+
+ ok(gSources.selectedItem,
+ "There should be a selected item in the sources pane. (4)");
+ is(getSelectedSourceURL(gSources), EXAMPLE_URL + gLabel1,
+ "The selected value is the sources pane is incorrect. (4)");
+
+ is(gEditor.getText().search(/firstCall/), 118,
+ "The first source is displayed. (4)");
+ is(gEditor.getText().search(/debugger/), -1,
+ "The second source is not displayed. (4)");
+
+ // The editor's debug location takes a tick to update.
+ ok(isCaretPos(gPanel, 6),
+ "Editor caret location is correct. (4)");
+ is(gEditor.getDebugLocation(), 5,
+ "Editor debugger location is correct. (4)");
+ ok(gEditor.hasLineClass(5, "debug-line"),
+ "The debugged line is highlighted appropriately. (4)");
+
+ deferred.resolve();
+
+ return deferred.promise;
+ }
+
+ Task.spawn(function* () {
+ yield waitForSourceAndCaretAndScopes(gPanel, "-02.js", 1);
+ yield testSourcesDisplay();
+ yield testSwitchPaused1();
+ yield testSwitchPaused2();
+ yield testSwitchRunning();
+ resumeDebuggerThenCloseAndFinish(gPanel);
+ });
+
+ callInTab(gTab, "firstCall");
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_scripts-switching-03.js b/devtools/client/debugger/test/mochitest/browser_dbg_scripts-switching-03.js
new file mode 100644
index 000000000..ab691b03c
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_scripts-switching-03.js
@@ -0,0 +1,63 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure that the DebuggerView error loading source text is correct.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
+
+function test() {
+ let options = {
+ source: EXAMPLE_URL + "code_script-switching-01.js",
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ const gTab = aTab;
+ const gPanel = aPanel;
+ const gDebugger = gPanel.panelWin;
+ const gView = gDebugger.DebuggerView;
+ const gEditor = gDebugger.DebuggerView.editor;
+ const gL10N = gDebugger.L10N;
+ const require = gDebugger.require;
+ const actions = bindActionCreators(gPanel);
+ const constants = require("./content/constants");
+ const controller = gDebugger.DebuggerController;
+
+ function showBogusSource() {
+ const source = { actor: "fake.actor", url: "http://fake.url/" };
+ actions.newSource(source);
+
+ controller.dispatch({
+ type: constants.LOAD_SOURCE_TEXT,
+ source: source,
+ status: "start"
+ });
+
+ controller.dispatch({
+ type: constants.SELECT_SOURCE,
+ source: source
+ });
+
+ controller.dispatch({
+ type: constants.LOAD_SOURCE_TEXT,
+ source: source,
+ status: "error",
+ error: "bogus actor"
+ });
+ }
+
+ function testDebuggerLoadingError() {
+ ok(gEditor.getText().includes(gL10N.getFormatStr("errorLoadingText2", "")),
+ "The valid error loading message is displayed.");
+ }
+
+ Task.spawn(function* () {
+ showBogusSource();
+ testDebuggerLoadingError();
+ closeDebuggerAndFinish(gPanel);
+ });
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_search-autofill-identifier.js b/devtools/client/debugger/test/mochitest/browser_dbg_search-autofill-identifier.js
new file mode 100644
index 000000000..b86666ef5
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_search-autofill-identifier.js
@@ -0,0 +1,138 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests that Debugger Search uses the identifier under cursor if nothing is
+ * selected or manually passed and searching using certain operators.
+ */
+"use strict";
+
+function test() {
+ const TAB_URL = EXAMPLE_URL + "doc_function-search.html";
+
+ let options = {
+ source: EXAMPLE_URL + "code_function-search-01.js",
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ let Debugger = aPanel.panelWin;
+ let Editor = Debugger.DebuggerView.editor;
+ let Filtering = Debugger.DebuggerView.Filtering;
+
+ function doSearch(aOperator) {
+ Editor.dropSelection();
+ Filtering._doSearch(aOperator);
+ }
+
+ info("Testing with cursor at the beginning of the file...");
+
+ doSearch();
+ is(Filtering._searchbox.value, "",
+ "The searchbox value should not be auto-filled when searching for files.");
+ is(Filtering._searchbox.selectionStart, Filtering._searchbox.selectionEnd,
+ "The searchbox contents should not be selected");
+ is(Editor.getSelection(), "",
+ "The selection in the editor should be empty.");
+
+ doSearch("!");
+ is(Filtering._searchbox.value, "!",
+ "The searchbox value should not be auto-filled when searching across all files.");
+ is(Filtering._searchbox.selectionStart, Filtering._searchbox.selectionEnd,
+ "The searchbox contents should not be selected");
+ is(Editor.getSelection(), "",
+ "The selection in the editor should be empty.");
+
+ doSearch("@");
+ is(Filtering._searchbox.value, "@",
+ "The searchbox value should not be auto-filled when searching for functions.");
+ is(Filtering._searchbox.selectionStart, Filtering._searchbox.selectionEnd,
+ "The searchbox contents should not be selected");
+ is(Editor.getSelection(), "",
+ "The selection in the editor should be empty.");
+
+ doSearch("#");
+ is(Filtering._searchbox.value, "#",
+ "The searchbox value should not be auto-filled when searching inside a file.");
+ is(Filtering._searchbox.selectionStart, Filtering._searchbox.selectionEnd,
+ "The searchbox contents should not be selected");
+ is(Editor.getSelection(), "",
+ "The selection in the editor should be empty.");
+
+ doSearch(":");
+ is(Filtering._searchbox.value, ":",
+ "The searchbox value should not be auto-filled when searching for a line.");
+ is(Filtering._searchbox.selectionStart, Filtering._searchbox.selectionEnd,
+ "The searchbox contents should not be selected");
+ is(Editor.getSelection(), "",
+ "The selection in the editor should be empty.");
+
+ doSearch("*");
+ is(Filtering._searchbox.value, "*",
+ "The searchbox value should not be auto-filled when searching for variables.");
+ is(Filtering._searchbox.selectionStart, Filtering._searchbox.selectionEnd,
+ "The searchbox contents should not be selected");
+ is(Editor.getSelection(), "",
+ "The selection in the editor should be empty.");
+
+ Editor.setCursor({ line: 7, ch: 0});
+ info("Testing with cursor at line 8 and char 1...");
+
+ doSearch();
+ is(Filtering._searchbox.value, "",
+ "The searchbox value should not be auto-filled when searching for files.");
+ is(Filtering._searchbox.selectionStart, Filtering._searchbox.selectionEnd,
+ "The searchbox contents should not be selected");
+ is(Editor.getSelection(), "",
+ "The selection in the editor should be empty.");
+
+ doSearch("!");
+ is(Filtering._searchbox.value, "!test",
+ "The searchbox value was incorrect when searching across all files.");
+ is(Filtering._searchbox.selectionStart, 1,
+ "The searchbox operator should not be selected");
+ is(Filtering._searchbox.selectionEnd, 5,
+ "The searchbox contents should be selected");
+ is(Editor.getSelection(), "",
+ "The selection in the editor should be empty.");
+
+ doSearch("@");
+ is(Filtering._searchbox.value, "@test",
+ "The searchbox value was incorrect when searching for functions.");
+ is(Filtering._searchbox.selectionStart, 1,
+ "The searchbox operator should not be selected");
+ is(Filtering._searchbox.selectionEnd, 5,
+ "The searchbox contents should be selected");
+ is(Editor.getSelection(), "",
+ "The selection in the editor should be empty.");
+
+ doSearch("#");
+ is(Filtering._searchbox.value, "#test",
+ "The searchbox value should be auto-filled when searching inside a file.");
+ is(Filtering._searchbox.selectionStart, 1,
+ "The searchbox operator should not be selected");
+ is(Filtering._searchbox.selectionEnd, 5,
+ "The searchbox contents should be selected");
+ is(Editor.getSelection(), "test",
+ "The selection in the editor should be 'test'.");
+
+ doSearch(":");
+ is(Filtering._searchbox.value, ":",
+ "The searchbox value should not be auto-filled when searching for a line.");
+ is(Filtering._searchbox.selectionStart, Filtering._searchbox.selectionEnd,
+ "The searchbox contents should not be selected");
+ is(Editor.getSelection(), "",
+ "The selection in the editor should be empty.");
+
+ doSearch("*");
+ is(Filtering._searchbox.value, "*",
+ "The searchbox value should not be auto-filled when searching for variables.");
+ is(Filtering._searchbox.selectionStart, Filtering._searchbox.selectionEnd,
+ "The searchbox contents should not be selected");
+ is(Editor.getSelection(), "",
+ "The selection in the editor should be empty.");
+
+ closeDebuggerAndFinish(aPanel);
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_search-basic-01.js b/devtools/client/debugger/test/mochitest/browser_dbg_search-basic-01.js
new file mode 100644
index 000000000..e2262d4e8
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_search-basic-01.js
@@ -0,0 +1,330 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests basic search functionality (find token and jump to line).
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_recursion-stack.html";
+
+var gTab, gPanel, gDebugger;
+var gEditor, gSources, gFiltering, gSearchBox;
+
+function test() {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gEditor = gDebugger.DebuggerView.editor;
+ gSources = gDebugger.DebuggerView.Sources;
+ gFiltering = gDebugger.DebuggerView.Filtering;
+ gSearchBox = gDebugger.DebuggerView.Filtering._searchbox;
+
+ performTest();
+ });
+}
+
+function performTest() {
+ // Make sure that the search box becomes focused when pressing ctrl+f - Bug 1211038
+ gEditor.focus();
+ synthesizeKeyFromKeyTag(gDebugger.document.getElementById("tokenSearchKey"));
+ let focusedEl = Services.focus.focusedElement;
+ focusedEl = focusedEl.ownerDocument.getBindingParent(focusedEl) || focusedEl;
+ is(focusedEl, gDebugger.document.getElementById("searchbox"), "Searchbox is focused");
+
+ setText(gSearchBox, "#html");
+
+ EventUtils.synthesizeKey("VK_RETURN", { shiftKey: true }, gDebugger);
+ is(gFiltering.searchData.toSource(), '["#", ["", "html"]]',
+ "The searchbox data wasn't parsed correctly.");
+ ok(isCaretPos(gPanel, 35, 7),
+ "The editor didn't jump to the correct line.");
+
+ EventUtils.synthesizeKey("VK_RETURN", { shiftKey: true }, gDebugger);
+ is(gFiltering.searchData.toSource(), '["#", ["", "html"]]',
+ "The searchbox data wasn't parsed correctly.");
+ ok(isCaretPos(gPanel, 5, 6),
+ "The editor didn't jump to the correct line.");
+
+ EventUtils.synthesizeKey("VK_RETURN", { shiftKey: true }, gDebugger);
+ is(gFiltering.searchData.toSource(), '["#", ["", "html"]]',
+ "The searchbox data wasn't parsed correctly.");
+ ok(isCaretPos(gPanel, 3, 15),
+ "The editor didn't jump to the correct line.");
+
+ setText(gSearchBox, ":12");
+ is(gFiltering.searchData.toSource(), '[":", ["", 12]]',
+ "The searchbox data wasn't parsed correctly.");
+ ok(isCaretPos(gPanel, 12),
+ "The editor didn't jump to the correct line.");
+
+ EventUtils.synthesizeKey("g", { metaKey: true }, gDebugger);
+ is(gFiltering.searchData.toSource(), '[":", ["", 13]]',
+ "The searchbox data wasn't parsed correctly.");
+ ok(isCaretPos(gPanel, 13),
+ "The editor didn't jump to the correct line after Meta+G.");
+
+ EventUtils.synthesizeKey("n", { ctrlKey: true }, gDebugger);
+ is(gFiltering.searchData.toSource(), '[":", ["", 14]]',
+ "The searchbox data wasn't parsed correctly.");
+ ok(isCaretPos(gPanel, 14),
+ "The editor didn't jump to the correct line after Ctrl+N.");
+
+ EventUtils.synthesizeKey("G", { metaKey: true, shiftKey: true }, gDebugger);
+ is(gFiltering.searchData.toSource(), '[":", ["", 13]]',
+ "The searchbox data wasn't parsed correctly.");
+ ok(isCaretPos(gPanel, 13),
+ "The editor didn't jump to the correct line after Meta+Shift+G.");
+
+ EventUtils.synthesizeKey("p", { ctrlKey: true }, gDebugger);
+ is(gFiltering.searchData.toSource(), '[":", ["", 12]]',
+ "The searchbox data wasn't parsed correctly.");
+ ok(isCaretPos(gPanel, 12),
+ "The editor didn't jump to the correct line after Ctrl+P.");
+
+ for (let i = 0; i < 100; i++) {
+ EventUtils.sendKey("DOWN", gDebugger);
+ }
+ is(gFiltering.searchData.toSource(), '[":", ["", 36]]',
+ "The searchbox data wasn't parsed correctly.");
+ ok(isCaretPos(gPanel, 36),
+ "The editor didn't jump to the correct line after multiple DOWN keys.");
+
+ for (let i = 0; i < 100; i++) {
+ EventUtils.sendKey("UP", gDebugger);
+ }
+ is(gFiltering.searchData.toSource(), '[":", ["", 1]]',
+ "The searchbox data wasn't parsed correctly.");
+ ok(isCaretPos(gPanel, 1),
+ "The editor didn't jump to the correct line after multiple UP keys.");
+
+
+ let token = "debugger";
+ setText(gSearchBox, "#" + token);
+ is(gFiltering.searchData.toSource(), '["#", ["", "debugger"]]',
+ "The searchbox data wasn't parsed correctly.");
+ ok(isCaretPos(gPanel, 8, 12 + token.length),
+ "The editor didn't jump to the correct token (1).");
+
+ EventUtils.sendKey("DOWN", gDebugger);
+ is(gFiltering.searchData.toSource(), '["#", ["", "debugger"]]',
+ "The searchbox data wasn't parsed correctly.");
+ ok(isCaretPos(gPanel, 14, 9 + token.length),
+ "The editor didn't jump to the correct token (2).");
+
+ EventUtils.sendKey("DOWN", gDebugger);
+ is(gFiltering.searchData.toSource(), '["#", ["", "debugger"]]',
+ "The searchbox data wasn't parsed correctly.");
+ ok(isCaretPos(gPanel, 18, 15 + token.length),
+ "The editor didn't jump to the correct token (3).");
+
+ EventUtils.sendKey("RETURN", gDebugger);
+ is(gFiltering.searchData.toSource(), '["#", ["", "debugger"]]',
+ "The searchbox data wasn't parsed correctly.");
+ ok(isCaretPos(gPanel, 26, 11 + token.length),
+ "The editor didn't jump to the correct token (4).");
+
+ EventUtils.sendKey("RETURN", gDebugger);
+ is(gFiltering.searchData.toSource(), '["#", ["", "debugger"]]',
+ "The searchbox data wasn't parsed correctly.");
+ ok(isCaretPos(gPanel, 8, 12 + token.length),
+ "The editor didn't jump to the correct token (5).");
+
+ EventUtils.sendKey("UP", gDebugger);
+ is(gFiltering.searchData.toSource(), '["#", ["", "debugger"]]',
+ "The searchbox data wasn't parsed correctly.");
+ ok(isCaretPos(gPanel, 26, 11 + token.length),
+ "The editor didn't jump to the correct token (6).");
+
+ setText(gSearchBox, ":bogus#" + token + ";");
+ is(gFiltering.searchData.toSource(), '["#", [":bogus", "debugger;"]]',
+ "The searchbox data wasn't parsed correctly.");
+ ok(isCaretPos(gPanel, 14, 9 + token.length + 1),
+ "The editor didn't jump to the correct token (7).");
+
+ setText(gSearchBox, ":13#" + token + ";");
+ is(gFiltering.searchData.toSource(), '["#", [":13", "debugger;"]]',
+ "The searchbox data wasn't parsed correctly.");
+ ok(isCaretPos(gPanel, 14, 9 + token.length + 1),
+ "The editor didn't jump to the correct token (8).");
+
+ setText(gSearchBox, ":#" + token + ";");
+ is(gFiltering.searchData.toSource(), '["#", [":", "debugger;"]]',
+ "The searchbox data wasn't parsed correctly.");
+ ok(isCaretPos(gPanel, 14, 9 + token.length + 1),
+ "The editor didn't jump to the correct token (9).");
+
+ setText(gSearchBox, "::#" + token + ";");
+ is(gFiltering.searchData.toSource(), '["#", ["::", "debugger;"]]',
+ "The searchbox data wasn't parsed correctly.");
+ ok(isCaretPos(gPanel, 14, 9 + token.length + 1),
+ "The editor didn't jump to the correct token (10).");
+
+ setText(gSearchBox, ":::#" + token + ";");
+ is(gFiltering.searchData.toSource(), '["#", [":::", "debugger;"]]',
+ "The searchbox data wasn't parsed correctly.");
+ ok(isCaretPos(gPanel, 14, 9 + token.length + 1),
+ "The editor didn't jump to the correct token (11).");
+
+
+ setText(gSearchBox, "#" + token + ";" + ":bogus");
+ is(gFiltering.searchData.toSource(), '["#", ["", "debugger;:bogus"]]',
+ "The searchbox data wasn't parsed correctly.");
+ ok(isCaretPos(gPanel, 14, 9 + token.length + 1),
+ "The editor didn't jump to the correct token (12).");
+
+ setText(gSearchBox, "#" + token + ";" + ":13");
+ is(gFiltering.searchData.toSource(), '["#", ["", "debugger;:13"]]',
+ "The searchbox data wasn't parsed correctly.");
+ ok(isCaretPos(gPanel, 14, 9 + token.length + 1),
+ "The editor didn't jump to the correct token (13).");
+
+ setText(gSearchBox, "#" + token + ";" + ":");
+ is(gFiltering.searchData.toSource(), '["#", ["", "debugger;:"]]',
+ "The searchbox data wasn't parsed correctly.");
+ ok(isCaretPos(gPanel, 14, 9 + token.length + 1),
+ "The editor didn't jump to the correct token (14).");
+
+ setText(gSearchBox, "#" + token + ";" + "::");
+ is(gFiltering.searchData.toSource(), '["#", ["", "debugger;::"]]',
+ "The searchbox data wasn't parsed correctly.");
+ ok(isCaretPos(gPanel, 14, 9 + token.length + 1),
+ "The editor didn't jump to the correct token (15).");
+
+ setText(gSearchBox, "#" + token + ";" + ":::");
+ is(gFiltering.searchData.toSource(), '["#", ["", "debugger;:::"]]',
+ "The searchbox data wasn't parsed correctly.");
+ ok(isCaretPos(gPanel, 14, 9 + token.length + 1),
+ "The editor didn't jump to the correct token (16).");
+
+
+ setText(gSearchBox, ":i am not a number");
+ is(gFiltering.searchData.toSource(), '[":", ["", 0]]',
+ "The searchbox data wasn't parsed correctly.");
+ ok(isCaretPos(gPanel, 14, 9 + token.length + 1),
+ "The editor didn't remain at the correct token (17).");
+
+ setText(gSearchBox, "#__i do not exist__");
+ is(gFiltering.searchData.toSource(), '["#", ["", "__i do not exist__"]]',
+ "The searchbox data wasn't parsed correctly.");
+ ok(isCaretPos(gPanel, 14, 9 + token.length + 1),
+ "The editor didn't remain at the correct token (18).");
+
+
+ setText(gSearchBox, "#" + token);
+ is(gFiltering.searchData.toSource(), '["#", ["", "debugger"]]',
+ "The searchbox data wasn't parsed correctly.");
+ ok(isCaretPos(gPanel, 8, 12 + token.length),
+ "The editor didn't jump to the correct token (19).");
+
+
+ clearText(gSearchBox);
+ is(gFiltering.searchData.toSource(), '["", [""]]',
+ "The searchbox data wasn't parsed correctly.");
+
+ EventUtils.sendKey("RETURN", gDebugger);
+ is(gFiltering.searchData.toSource(), '["", [""]]',
+ "The searchbox data wasn't parsed correctly.");
+ ok(isCaretPos(gPanel, 8, 12 + token.length),
+ "The editor shouldn't jump to another token (20).");
+
+ EventUtils.sendKey("RETURN", gDebugger);
+ is(gFiltering.searchData.toSource(), '["", [""]]',
+ "The searchbox data wasn't parsed correctly.");
+ ok(isCaretPos(gPanel, 8, 12 + token.length),
+ "The editor shouldn't jump to another token (21).");
+
+
+ setText(gSearchBox, ":1:2:3:a:b:c:::12");
+ is(gFiltering.searchData.toSource(), '[":", [":1:2:3:a:b:c::", 12]]',
+ "The searchbox data wasn't parsed correctly.");
+ ok(isCaretPos(gPanel, 12),
+ "The editor didn't jump to the correct line (22).");
+
+ setText(gSearchBox, "#don't#find#me#instead#find#" + token);
+ is(gFiltering.searchData.toSource(), '["#", ["#don\'t#find#me#instead#find", "debugger"]]',
+ "The searchbox data wasn't parsed correctly.");
+ ok(isCaretPos(gPanel, 8, 12 + token.length),
+ "The editor didn't jump to the correct token (23).");
+
+ EventUtils.sendKey("DOWN", gDebugger);
+ is(gFiltering.searchData.toSource(), '["#", ["#don\'t#find#me#instead#find", "debugger"]]',
+ "The searchbox data wasn't parsed correctly.");
+ ok(isCaretPos(gPanel, 14, 9 + token.length),
+ "The editor didn't jump to the correct token (24).");
+
+ EventUtils.sendKey("DOWN", gDebugger);
+ is(gFiltering.searchData.toSource(), '["#", ["#don\'t#find#me#instead#find", "debugger"]]',
+ "The searchbox data wasn't parsed correctly.");
+ ok(isCaretPos(gPanel, 18, 15 + token.length),
+ "The editor didn't jump to the correct token (25).");
+
+ EventUtils.sendKey("RETURN", gDebugger);
+ is(gFiltering.searchData.toSource(), '["#", ["#don\'t#find#me#instead#find", "debugger"]]',
+ "The searchbox data wasn't parsed correctly.");
+ ok(isCaretPos(gPanel, 26, 11 + token.length),
+ "The editor didn't jump to the correct token (26).");
+
+ EventUtils.sendKey("RETURN", gDebugger);
+ is(gFiltering.searchData.toSource(), '["#", ["#don\'t#find#me#instead#find", "debugger"]]',
+ "The searchbox data wasn't parsed correctly.");
+ ok(isCaretPos(gPanel, 8, 12 + token.length),
+ "The editor didn't jump to the correct token (27).");
+
+ EventUtils.sendKey("UP", gDebugger);
+ is(gFiltering.searchData.toSource(), '["#", ["#don\'t#find#me#instead#find", "debugger"]]',
+ "The searchbox data wasn't parsed correctly.");
+ ok(isCaretPos(gPanel, 26, 11 + token.length),
+ "The editor didn't jump to the correct token (28).");
+
+
+ clearText(gSearchBox);
+ is(gFiltering.searchData.toSource(), '["", [""]]',
+ "The searchbox data wasn't parsed correctly.");
+ ok(isCaretPos(gPanel, 26, 11 + token.length),
+ "The editor didn't remain at the correct token (29).");
+ is(gSources.visibleItems.length, 1,
+ "Not all the sources are shown after the search (30).");
+
+
+ gEditor.focus();
+ gEditor.setSelection.apply(gEditor, gEditor.getPosition(1, 5));
+ ok(isCaretPos(gPanel, 1, 6),
+ "The editor caret position didn't update after selecting some text.");
+
+ EventUtils.synthesizeKey("F", { accelKey: true });
+ is(gFiltering.searchData.toSource(), '["#", ["", "!-- "]]',
+ "The searchbox data wasn't parsed correctly.");
+ is(gSearchBox.value, "#!-- ",
+ "The search field has the right initial value (1).");
+
+ gEditor.focus();
+ gEditor.setSelection.apply(gEditor, gEditor.getPosition(415, 418));
+ ok(isCaretPos(gPanel, 21, 30),
+ "The editor caret position didn't update after selecting some number.");
+
+ EventUtils.synthesizeKey("L", { accelKey: true });
+ is(gFiltering.searchData.toSource(), '[":", ["", 100]]',
+ "The searchbox data wasn't parsed correctly.");
+ is(gSearchBox.value, ":100",
+ "The search field has the right initial value (2).");
+
+
+ closeDebuggerAndFinish(gPanel);
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gEditor = null;
+ gSources = null;
+ gFiltering = null;
+ gSearchBox = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_search-basic-02.js b/devtools/client/debugger/test/mochitest/browser_dbg_search-basic-02.js
new file mode 100644
index 000000000..ef09e16da
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_search-basic-02.js
@@ -0,0 +1,129 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests basic file search functionality.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
+
+var gTab, gPanel, gDebugger;
+var gSources, gSearchBox;
+
+function test() {
+ let options = {
+ source: EXAMPLE_URL + "code_script-switching-01.js",
+ line: 1,
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gSources = gDebugger.DebuggerView.Sources;
+ gSearchBox = gDebugger.DebuggerView.Filtering._searchbox;
+
+ // Calling `firstCall` is going to break into the other script
+ waitForSourceAndCaretAndScopes(gPanel, "-02.js", 6)
+ .then(performSimpleSearch)
+ .then(() => verifySourceAndCaret("-01.js", 1, 1, [1, 1]))
+ .then(combineWithLineSearch)
+ .then(() => verifySourceAndCaret("-01.js", 2, 1, [53, 53]))
+ .then(combineWithTokenSearch)
+ .then(() => verifySourceAndCaret("-01.js", 2, 48, [96, 100]))
+ .then(combineWithTokenColonSearch)
+ .then(() => verifySourceAndCaret("-01.js", 2, 11, [56, 63]))
+ .then(() => resumeDebuggerThenCloseAndFinish(gPanel))
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+
+ callInTab(gTab, "firstCall");
+ });
+}
+
+function performSimpleSearch() {
+ let finished = promise.all([
+ ensureSourceIs(gPanel, "-02.js"),
+ ensureCaretAt(gPanel, 6),
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_FOUND),
+ waitForSourceShown(gPanel, "-01.js")
+ ]);
+
+ setText(gSearchBox, "1");
+
+ return finished.then(() => promise.all([
+ ensureSourceIs(gPanel, "-01.js"),
+ ensureCaretAt(gPanel, 1)
+ ]));
+}
+
+function combineWithLineSearch() {
+ let finished = promise.all([
+ ensureSourceIs(gPanel, "-01.js"),
+ ensureCaretAt(gPanel, 1),
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_FOUND),
+ waitForCaretUpdated(gPanel, 2)
+ ]);
+
+ typeText(gSearchBox, ":2");
+
+ return finished.then(() => promise.all([
+ ensureSourceIs(gPanel, "-01.js"),
+ ensureCaretAt(gPanel, 2)
+ ]));
+}
+
+function combineWithTokenSearch() {
+ let finished = promise.all([
+ ensureSourceIs(gPanel, "-01.js"),
+ ensureCaretAt(gPanel, 2),
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_FOUND),
+ waitForCaretUpdated(gPanel, 2, 48)
+ ]);
+
+ backspaceText(gSearchBox, 2);
+ typeText(gSearchBox, "#zero");
+
+ return finished.then(() => promise.all([
+ ensureSourceIs(gPanel, "-01.js"),
+ ensureCaretAt(gPanel, 2, 48)
+ ]));
+}
+
+function combineWithTokenColonSearch() {
+ let finished = promise.all([
+ ensureSourceIs(gPanel, "-01.js"),
+ ensureCaretAt(gPanel, 2, 48),
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_FOUND),
+ waitForCaretUpdated(gPanel, 2, 11)
+ ]);
+
+ backspaceText(gSearchBox, 4);
+ typeText(gSearchBox, "http://");
+
+ return finished.then(() => promise.all([
+ ensureSourceIs(gPanel, "-01.js"),
+ ensureCaretAt(gPanel, 2, 11)
+ ]));
+}
+
+function verifySourceAndCaret(aUrl, aLine, aColumn, aSelection) {
+ ok(gSources.selectedItem.attachment.label.includes(aUrl),
+ "The selected item's label appears to be correct.");
+ ok(gSources.selectedItem.attachment.source.url.includes(aUrl),
+ "The selected item's value appears to be correct.");
+ ok(isCaretPos(gPanel, aLine, aColumn),
+ "The current caret position appears to be correct.");
+ ok(isEditorSel(gPanel, aSelection),
+ "The current editor selection appears to be correct.");
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gSources = null;
+ gSearchBox = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_search-basic-03.js b/devtools/client/debugger/test/mochitest/browser_dbg_search-basic-03.js
new file mode 100644
index 000000000..0020776d6
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_search-basic-03.js
@@ -0,0 +1,123 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that searches which cause a popup to be shown properly handle the
+ * ESCAPE key.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
+
+var gTab, gPanel, gDebugger;
+var gSources, gSearchBox;
+
+function test() {
+ let options = {
+ source: EXAMPLE_URL + "code_script-switching-01.js",
+ line: 1,
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gSources = gDebugger.DebuggerView.Sources;
+ gSearchBox = gDebugger.DebuggerView.Filtering._searchbox;
+
+ // Calling `firstCall` is going to break into the other script
+ waitForSourceAndCaretAndScopes(gPanel, "-02.js", 6)
+ .then(performFileSearch)
+ .then(escapeAndHide)
+ .then(escapeAndClear)
+ .then(() => verifySourceAndCaret("-01.js", 1, 1))
+ .then(performFunctionSearch)
+ .then(escapeAndHide)
+ .then(escapeAndClear)
+ .then(() => verifySourceAndCaret("-01.js", 4, 10))
+ .then(performGlobalSearch)
+ .then(escapeAndClear)
+ .then(() => verifySourceAndCaret("-01.js", 4, 10))
+ .then(() => resumeDebuggerThenCloseAndFinish(gPanel))
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+
+ callInTab(gTab, "firstCall");
+ });
+}
+
+function performFileSearch() {
+ let finished = promise.all([
+ ensureSourceIs(gPanel, "-02.js"),
+ ensureCaretAt(gPanel, 6),
+ once(gDebugger, "popupshown"),
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_FOUND),
+ waitForSourceShown(gPanel, "-01.js")
+ ]);
+
+ setText(gSearchBox, ".");
+
+ return finished.then(() => promise.all([
+ ensureSourceIs(gPanel, "-01.js"),
+ ensureCaretAt(gPanel, 1)
+ ]));
+}
+
+function performFunctionSearch() {
+ let finished = promise.all([
+ ensureSourceIs(gPanel, "-01.js"),
+ ensureCaretAt(gPanel, 1),
+ once(gDebugger, "popupshown"),
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FUNCTION_SEARCH_MATCH_FOUND)
+ ]);
+
+ setText(gSearchBox, "@");
+
+ return finished.then(() => promise.all([
+ ensureSourceIs(gPanel, "-01.js"),
+ ensureCaretAt(gPanel, 4, 10)
+ ]));
+}
+
+function performGlobalSearch() {
+ let finished = promise.all([
+ ensureSourceIs(gPanel, "-01.js"),
+ ensureCaretAt(gPanel, 4, 10),
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.GLOBAL_SEARCH_MATCH_FOUND)
+ ]);
+
+ setText(gSearchBox, "!first");
+
+ return finished.then(() => promise.all([
+ ensureSourceIs(gPanel, "-01.js"),
+ ensureCaretAt(gPanel, 4, 10)
+ ]));
+}
+
+function escapeAndHide() {
+ let finished = once(gDebugger, "popuphidden", true);
+ EventUtils.sendKey("ESCAPE", gDebugger);
+ return finished;
+}
+
+function escapeAndClear() {
+ EventUtils.sendKey("ESCAPE", gDebugger);
+ is(gSearchBox.getAttribute("value"), "",
+ "The searchbox has properly emptied after pressing escape.");
+}
+
+function verifySourceAndCaret(aUrl, aLine, aColumn) {
+ ok(gSources.selectedItem.attachment.label.includes(aUrl),
+ "The selected item's label appears to be correct.");
+ ok(gSources.selectedItem.attachment.source.url.includes(aUrl),
+ "The selected item's value appears to be correct.");
+ ok(isCaretPos(gPanel, aLine, aColumn),
+ "The current caret position appears to be correct.");
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gSources = null;
+ gSearchBox = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_search-basic-04.js b/devtools/client/debugger/test/mochitest/browser_dbg_search-basic-04.js
new file mode 100644
index 000000000..4d708797d
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_search-basic-04.js
@@ -0,0 +1,132 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests that the selection is dropped for line and token searches, after
+ * pressing backspace enough times.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
+
+var gTab, gPanel, gDebugger;
+var gEditor, gSources, gSearchBox;
+
+function test() {
+ let options = {
+ source: EXAMPLE_URL + "code_script-switching-01.js",
+ line: 1,
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gEditor = gDebugger.DebuggerView.editor;
+ gSources = gDebugger.DebuggerView.Sources;
+ gSearchBox = gDebugger.DebuggerView.Filtering._searchbox;
+
+ testLineSearch();
+ testTokenSearch();
+ closeDebuggerAndFinish(gPanel);
+ });
+}
+
+function testLineSearch() {
+ setText(gSearchBox, ":42");
+ ok(isCaretPos(gPanel, 7),
+ "The editor caret position appears to be correct (1.1).");
+ ok(isEditorSel(gPanel, [151, 151]),
+ "The editor selection appears to be correct (1.1).");
+ is(gEditor.getSelection(), "",
+ "The editor selected text appears to be correct (1.1).");
+
+ backspaceText(gSearchBox, 1);
+ ok(isCaretPos(gPanel, 4),
+ "The editor caret position appears to be correct (1.2).");
+ ok(isEditorSel(gPanel, [110, 110]),
+ "The editor selection appears to be correct (1.2).");
+ is(gEditor.getSelection(), "",
+ "The editor selected text appears to be correct (1.2).");
+
+ backspaceText(gSearchBox, 1);
+ ok(isCaretPos(gPanel, 4),
+ "The editor caret position appears to be correct (1.3).");
+ ok(isEditorSel(gPanel, [110, 110]),
+ "The editor selection appears to be correct (1.3).");
+ is(gEditor.getSelection(), "",
+ "The editor selected text appears to be correct (1.3).");
+
+ setText(gSearchBox, ":4");
+ ok(isCaretPos(gPanel, 4),
+ "The editor caret position appears to be correct (1.4).");
+ ok(isEditorSel(gPanel, [110, 110]),
+ "The editor selection appears to be correct (1.4).");
+ is(gEditor.getSelection(), "",
+ "The editor selected text appears to be correct (1.4).");
+
+ gSearchBox.select();
+ backspaceText(gSearchBox, 1);
+ ok(isCaretPos(gPanel, 4),
+ "The editor caret position appears to be correct (1.5).");
+ ok(isEditorSel(gPanel, [110, 110]),
+ "The editor selection appears to be correct (1.5).");
+ is(gEditor.getSelection(), "",
+ "The editor selected text appears to be correct (1.5).");
+ is(gSearchBox.value, "",
+ "The searchbox should have been cleared.");
+}
+
+function testTokenSearch() {
+ setText(gSearchBox, "#();");
+ ok(isCaretPos(gPanel, 5, 16),
+ "The editor caret position appears to be correct (2.1).");
+ ok(isEditorSel(gPanel, [145, 148]),
+ "The editor selection appears to be correct (2.1).");
+ is(gEditor.getSelection(), "();",
+ "The editor selected text appears to be correct (2.1).");
+
+ backspaceText(gSearchBox, 1);
+ ok(isCaretPos(gPanel, 4, 21),
+ "The editor caret position appears to be correct (2.2).");
+ ok(isEditorSel(gPanel, [128, 130]),
+ "The editor selection appears to be correct (2.2).");
+ is(gEditor.getSelection(), "()",
+ "The editor selected text appears to be correct (2.2).");
+
+ backspaceText(gSearchBox, 2);
+ ok(isCaretPos(gPanel, 4, 20),
+ "The editor caret position appears to be correct (2.3).");
+ ok(isEditorSel(gPanel, [129, 129]),
+ "The editor selection appears to be correct (2.3).");
+ is(gEditor.getSelection(), "",
+ "The editor selected text appears to be correct (2.3).");
+
+ setText(gSearchBox, "#;");
+ ok(isCaretPos(gPanel, 5, 16),
+ "The editor caret position appears to be correct (2.4).");
+ ok(isEditorSel(gPanel, [147, 148]),
+ "The editor selection appears to be correct (2.4).");
+ is(gEditor.getSelection(), ";",
+ "The editor selected text appears to be correct (2.4).");
+
+ gSearchBox.select();
+ backspaceText(gSearchBox, 1);
+ ok(isCaretPos(gPanel, 5, 16),
+ "The editor caret position appears to be correct (2.5).");
+ ok(isEditorSel(gPanel, [148, 148]),
+ "The editor selection appears to be correct (2.5).");
+ is(gEditor.getSelection(), "",
+ "The editor selected text appears to be correct (2.5).");
+ is(gSearchBox.value, "",
+ "The searchbox should have been cleared.");
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gEditor = null;
+ gSources = null;
+ gSearchBox = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_search-global-01.js b/devtools/client/debugger/test/mochitest/browser_dbg_search-global-01.js
new file mode 100644
index 000000000..c301dbbbc
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_search-global-01.js
@@ -0,0 +1,278 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests basic functionality of global search (lowercase + upper case, expected
+ * UI behavior, number of results found etc.)
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
+
+var gTab, gPanel, gDebugger;
+var gEditor, gSources, gSearchView, gSearchBox;
+
+function test() {
+ let options = {
+ source: EXAMPLE_URL + "code_script-switching-01.js",
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gEditor = gDebugger.DebuggerView.editor;
+ gSources = gDebugger.DebuggerView.Sources;
+ gSearchView = gDebugger.DebuggerView.GlobalSearch;
+ gSearchBox = gDebugger.DebuggerView.Filtering._searchbox;
+
+ waitForSourceAndCaretAndScopes(gPanel, "-02.js", 1)
+ .then(firstSearch)
+ .then(secondSearch)
+ .then(clearSearch)
+ .then(() => resumeDebuggerThenCloseAndFinish(gPanel))
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+
+ callInTab(gTab, "firstCall");
+ });
+}
+
+function firstSearch() {
+ let deferred = promise.defer();
+
+ is(gSearchView.itemCount, 0,
+ "The global search pane shouldn't have any entries yet.");
+ is(gSearchView.widget._parent.hidden, true,
+ "The global search pane shouldn't be visible yet.");
+ is(gSearchView._splitter.hidden, true,
+ "The global search pane splitter shouldn't be visible yet.");
+
+ gDebugger.once(gDebugger.EVENTS.GLOBAL_SEARCH_MATCH_FOUND, () => {
+ // Some operations are synchronously dispatched on the main thread,
+ // to avoid blocking UI, thus giving the impression of faster searching.
+ executeSoon(() => {
+ info("Current source url:\n" + getSelectedSourceURL(gSources));
+ info("Debugger editor text:\n" + gEditor.getText());
+
+ ok(isCaretPos(gPanel, 6),
+ "The editor shouldn't have jumped to a matching line yet.");
+ ok(getSelectedSourceURL(gSources).includes("-02.js"),
+ "The current source shouldn't have changed after a global search.");
+ is(gSources.visibleItems.length, 2,
+ "Not all the sources are shown after the global search.");
+
+ let sourceResults = gDebugger.document.querySelectorAll(".dbg-source-results");
+ is(sourceResults.length, 2,
+ "There should be matches found in two sources.");
+
+ let item0 = gDebugger.SourceResults.getItemForElement(sourceResults[0]);
+ let item1 = gDebugger.SourceResults.getItemForElement(sourceResults[1]);
+ is(item0.instance.expanded, true,
+ "The first source results should automatically be expanded.");
+ is(item1.instance.expanded, true,
+ "The second source results should automatically be expanded.");
+
+ let searchResult0 = sourceResults[0].querySelectorAll(".dbg-search-result");
+ let searchResult1 = sourceResults[1].querySelectorAll(".dbg-search-result");
+ is(searchResult0.length, 1,
+ "There should be one line result for the first url.");
+ is(searchResult1.length, 2,
+ "There should be two line results for the second url.");
+
+ let firstLine0 = searchResult0[0];
+ is(firstLine0.querySelector(".dbg-results-line-number").getAttribute("value"), "1",
+ "The first result for the first source doesn't have the correct line attached.");
+
+ is(firstLine0.querySelectorAll(".dbg-results-line-contents").length, 1,
+ "The first result for the first source doesn't have the correct number of nodes for a line.");
+ is(firstLine0.querySelectorAll(".dbg-results-line-contents-string").length, 3,
+ "The first result for the first source doesn't have the correct number of strings in a line.");
+
+ is(firstLine0.querySelectorAll(".dbg-results-line-contents-string[match=true]").length, 1,
+ "The first result for the first source doesn't have the correct number of matches in a line.");
+ is(firstLine0.querySelectorAll(".dbg-results-line-contents-string[match=true]")[0].getAttribute("value"), "de",
+ "The first result for the first source doesn't have the correct match in a line.");
+
+ is(firstLine0.querySelectorAll(".dbg-results-line-contents-string[match=false]").length, 2,
+ "The first result for the first source doesn't have the correct number of non-matches in a line.");
+ is(firstLine0.querySelectorAll(".dbg-results-line-contents-string[match=false]")[0].getAttribute("value"), "/* Any copyright is ",
+ "The first result for the first source doesn't have the correct non-matches in a line.");
+ is(firstLine0.querySelectorAll(".dbg-results-line-contents-string[match=false]")[1].getAttribute("value"), "dicated to the Public Domain.",
+ "The first result for the first source doesn't have the correct non-matches in a line.");
+
+ let firstLine1 = searchResult1[0];
+ is(firstLine1.querySelector(".dbg-results-line-number").getAttribute("value"), "1",
+ "The first result for the second source doesn't have the correct line attached.");
+
+ is(firstLine1.querySelectorAll(".dbg-results-line-contents").length, 1,
+ "The first result for the second source doesn't have the correct number of nodes for a line.");
+ is(firstLine1.querySelectorAll(".dbg-results-line-contents-string").length, 3,
+ "The first result for the second source doesn't have the correct number of strings in a line.");
+
+ is(firstLine1.querySelectorAll(".dbg-results-line-contents-string[match=true]").length, 1,
+ "The first result for the second source doesn't have the correct number of matches in a line.");
+ is(firstLine1.querySelectorAll(".dbg-results-line-contents-string[match=true]")[0].getAttribute("value"), "de",
+ "The first result for the second source doesn't have the correct match in a line.");
+
+ is(firstLine1.querySelectorAll(".dbg-results-line-contents-string[match=false]").length, 2,
+ "The first result for the second source doesn't have the correct number of non-matches in a line.");
+ is(firstLine1.querySelectorAll(".dbg-results-line-contents-string[match=false]")[0].getAttribute("value"), "/* Any copyright is ",
+ "The first result for the second source doesn't have the correct non-matches in a line.");
+ is(firstLine1.querySelectorAll(".dbg-results-line-contents-string[match=false]")[1].getAttribute("value"), "dicated to the Public Domain.",
+ "The first result for the second source doesn't have the correct non-matches in a line.");
+
+ let secondLine1 = searchResult1[1];
+ is(secondLine1.querySelector(".dbg-results-line-number").getAttribute("value"), "6",
+ "The second result for the second source doesn't have the correct line attached.");
+
+ is(secondLine1.querySelectorAll(".dbg-results-line-contents").length, 1,
+ "The second result for the second source doesn't have the correct number of nodes for a line.");
+ is(secondLine1.querySelectorAll(".dbg-results-line-contents-string").length, 3,
+ "The second result for the second source doesn't have the correct number of strings in a line.");
+
+ is(secondLine1.querySelectorAll(".dbg-results-line-contents-string[match=true]").length, 1,
+ "The second result for the second source doesn't have the correct number of matches in a line.");
+ is(secondLine1.querySelectorAll(".dbg-results-line-contents-string[match=true]")[0].getAttribute("value"), "de",
+ "The second result for the second source doesn't have the correct match in a line.");
+
+ is(secondLine1.querySelectorAll(".dbg-results-line-contents-string[match=false]").length, 2,
+ "The second result for the second source doesn't have the correct number of non-matches in a line.");
+ is(secondLine1.querySelectorAll(".dbg-results-line-contents-string[match=false]")[0].getAttribute("value"), " ",
+ "The second result for the second source doesn't have the correct non-matches in a line.");
+ is(secondLine1.querySelectorAll(".dbg-results-line-contents-string[match=false]")[1].getAttribute("value"), "bugger;",
+ "The second result for the second source doesn't have the correct non-matches in a line.");
+
+ deferred.resolve();
+ });
+ });
+
+ setText(gSearchBox, "!de");
+
+ return deferred.promise;
+}
+
+function secondSearch() {
+ let deferred = promise.defer();
+
+ is(gSearchView.itemCount, 2,
+ "The global search pane should have some child nodes from the previous search.");
+ is(gSearchView.widget._parent.hidden, false,
+ "The global search pane should be visible from the previous search.");
+ is(gSearchView._splitter.hidden, false,
+ "The global search pane splitter should be visible from the previous search.");
+
+ gDebugger.once(gDebugger.EVENTS.GLOBAL_SEARCH_MATCH_FOUND, () => {
+ // Some operations are synchronously dispatched on the main thread,
+ // to avoid blocking UI, thus giving the impression of faster searching.
+ executeSoon(() => {
+ info("Current source url:\n" + getSelectedSourceURL(gSources));
+ info("Debugger editor text:\n" + gEditor.getText());
+
+ ok(isCaretPos(gPanel, 6),
+ "The editor shouldn't have jumped to a matching line yet.");
+ ok(getSelectedSourceURL(gSources).includes("-02.js"),
+ "The current source shouldn't have changed after a global search.");
+ is(gSources.visibleItems.length, 2,
+ "Not all the sources are shown after the global search.");
+
+ let sourceResults = gDebugger.document.querySelectorAll(".dbg-source-results");
+ is(sourceResults.length, 2,
+ "There should be matches found in two sources.");
+
+ let item0 = gDebugger.SourceResults.getItemForElement(sourceResults[0]);
+ let item1 = gDebugger.SourceResults.getItemForElement(sourceResults[1]);
+ is(item0.instance.expanded, true,
+ "The first source results should automatically be expanded.");
+ is(item1.instance.expanded, true,
+ "The second source results should automatically be expanded.");
+
+ let searchResult0 = sourceResults[0].querySelectorAll(".dbg-search-result");
+ let searchResult1 = sourceResults[1].querySelectorAll(".dbg-search-result");
+ is(searchResult0.length, 1,
+ "There should be one line result for the first url.");
+ is(searchResult1.length, 1,
+ "There should be one line result for the second url.");
+
+ let firstLine0 = searchResult0[0];
+ is(firstLine0.querySelector(".dbg-results-line-number").getAttribute("value"), "1",
+ "The first result for the first source doesn't have the correct line attached.");
+
+ is(firstLine0.querySelectorAll(".dbg-results-line-contents").length, 1,
+ "The first result for the first source doesn't have the correct number of nodes for a line.");
+ is(firstLine0.querySelectorAll(".dbg-results-line-contents-string").length, 5,
+ "The first result for the first source doesn't have the correct number of strings in a line.");
+
+ is(firstLine0.querySelectorAll(".dbg-results-line-contents-string[match=true]").length, 2,
+ "The first result for the first source doesn't have the correct number of matches in a line.");
+ is(firstLine0.querySelectorAll(".dbg-results-line-contents-string[match=true]")[0].getAttribute("value"), "ed",
+ "The first result for the first source doesn't have the correct matches in a line.");
+ is(firstLine0.querySelectorAll(".dbg-results-line-contents-string[match=true]")[1].getAttribute("value"), "ed",
+ "The first result for the first source doesn't have the correct matches in a line.");
+
+ is(firstLine0.querySelectorAll(".dbg-results-line-contents-string[match=false]").length, 3,
+ "The first result for the first source doesn't have the correct number of non-matches in a line.");
+ is(firstLine0.querySelectorAll(".dbg-results-line-contents-string[match=false]")[0].getAttribute("value"), "/* Any copyright is d",
+ "The first result for the first source doesn't have the correct non-matches in a line.");
+ is(firstLine0.querySelectorAll(".dbg-results-line-contents-string[match=false]")[1].getAttribute("value"), "icat",
+ "The first result for the first source doesn't have the correct non-matches in a line.");
+ is(firstLine0.querySelectorAll(".dbg-results-line-contents-string[match=false]")[2].getAttribute("value"), " to the Public Domain.",
+ "The first result for the first source doesn't have the correct non-matches in a line.");
+
+ let firstLine1 = searchResult1[0];
+ is(firstLine1.querySelector(".dbg-results-line-number").getAttribute("value"), "1",
+ "The first result for the second source doesn't have the correct line attached.");
+
+ is(firstLine1.querySelectorAll(".dbg-results-line-contents").length, 1,
+ "The first result for the second source doesn't have the correct number of nodes for a line.");
+ is(firstLine1.querySelectorAll(".dbg-results-line-contents-string").length, 5,
+ "The first result for the second source doesn't have the correct number of strings in a line.");
+
+ is(firstLine1.querySelectorAll(".dbg-results-line-contents-string[match=true]").length, 2,
+ "The first result for the second source doesn't have the correct number of matches in a line.");
+ is(firstLine1.querySelectorAll(".dbg-results-line-contents-string[match=true]")[0].getAttribute("value"), "ed",
+ "The first result for the second source doesn't have the correct matches in a line.");
+ is(firstLine1.querySelectorAll(".dbg-results-line-contents-string[match=true]")[1].getAttribute("value"), "ed",
+ "The first result for the second source doesn't have the correct matches in a line.");
+
+ is(firstLine1.querySelectorAll(".dbg-results-line-contents-string[match=false]").length, 3,
+ "The first result for the second source doesn't have the correct number of non-matches in a line.");
+ is(firstLine1.querySelectorAll(".dbg-results-line-contents-string[match=false]")[0].getAttribute("value"), "/* Any copyright is d",
+ "The first result for the second source doesn't have the correct non-matches in a line.");
+ is(firstLine1.querySelectorAll(".dbg-results-line-contents-string[match=false]")[1].getAttribute("value"), "icat",
+ "The first result for the second source doesn't have the correct non-matches in a line.");
+ is(firstLine1.querySelectorAll(".dbg-results-line-contents-string[match=false]")[2].getAttribute("value"), " to the Public Domain.",
+ "The first result for the second source doesn't have the correct non-matches in a line.");
+
+ deferred.resolve();
+ });
+ });
+
+ backspaceText(gSearchBox, 2);
+ typeText(gSearchBox, "ED");
+
+ return deferred.promise;
+}
+
+function clearSearch() {
+ gSearchView.clearView();
+
+ is(gSearchView.itemCount, 0,
+ "The global search pane shouldn't have any child nodes after clearing.");
+ is(gSearchView.widget._parent.hidden, true,
+ "The global search pane shouldn't be visible after clearing.");
+ is(gSearchView._splitter.hidden, true,
+ "The global search pane splitter shouldn't be visible after clearing.");
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gEditor = null;
+ gSources = null;
+ gSearchView = null;
+ gSearchBox = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_search-global-02.js b/devtools/client/debugger/test/mochitest/browser_dbg_search-global-02.js
new file mode 100644
index 000000000..5713b3822
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_search-global-02.js
@@ -0,0 +1,203 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests if the global search results switch back and forth, and wrap around
+ * when switching between them.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
+
+var gTab, gPanel, gDebugger;
+var gEditor, gSources, gSearchView, gSearchBox;
+
+function test() {
+ let options = {
+ source: EXAMPLE_URL + "code_script-switching-01.js",
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gEditor = gDebugger.DebuggerView.editor;
+ gSources = gDebugger.DebuggerView.Sources;
+ gSearchView = gDebugger.DebuggerView.GlobalSearch;
+ gSearchBox = gDebugger.DebuggerView.Filtering._searchbox;
+
+ function firstSearch() {
+ let deferred = promise.defer();
+
+ is(gSearchView.itemCount, 0,
+ "The global search pane shouldn't have any entries yet.");
+ is(gSearchView.widget._parent.hidden, true,
+ "The global search pane shouldn't be visible yet.");
+ is(gSearchView._splitter.hidden, true,
+ "The global search pane splitter shouldn't be visible yet.");
+
+ gDebugger.once(gDebugger.EVENTS.GLOBAL_SEARCH_MATCH_FOUND, () => {
+ // Some operations are synchronously dispatched on the main thread,
+ // to avoid blocking UI, thus giving the impression of faster searching.
+ executeSoon(() => {
+ info("Current source url:\n" + getSelectedSourceURL(gSources));
+ info("Debugger editor text:\n" + gEditor.getText());
+
+ ok(isCaretPos(gPanel, 6),
+ "The editor shouldn't have jumped to a matching line yet.");
+ ok(getSelectedSourceURL(gSources).includes("-02.js"),
+ "The current source shouldn't have changed after a global search.");
+ is(gSources.visibleItems.length, 2,
+ "Not all the sources are shown after the global search.");
+
+ deferred.resolve();
+ });
+ });
+
+ setText(gSearchBox, "!function");
+
+ return deferred.promise;
+ }
+
+ function doFirstJump() {
+ let deferred = promise.defer();
+
+ waitForSourceAndCaret(gPanel, "-01.js", 4).then(() => {
+ info("Current source url:\n" + getSelectedSourceURL(gSources));
+ info("Debugger editor text:\n" + gEditor.getText());
+
+ ok(getSelectedSourceURL(gSources).includes("-01.js"),
+ "The currently shown source is incorrect (1).");
+ is(gSources.visibleItems.length, 2,
+ "Not all the sources are shown after the global search (1).");
+
+ // The editor's selected text takes a tick to update.
+ ok(isCaretPos(gPanel, 4, 9),
+ "The editor didn't jump to the correct line (1).");
+ is(gEditor.getSelection(), "function",
+ "The editor didn't select the correct text (1).");
+
+ deferred.resolve();
+ });
+
+ EventUtils.sendKey("DOWN", gDebugger);
+
+ return deferred.promise;
+ }
+
+ function doSecondJump() {
+ let deferred = promise.defer();
+
+ waitForSourceAndCaret(gPanel, "-02.js", 4).then(() => {
+ info("Current source url:\n" + getSelectedSourceURL(gSources));
+ info("Debugger editor text:\n" + gEditor.getText());
+
+ ok(getSelectedSourceURL(gSources).includes("-02.js"),
+ "The currently shown source is incorrect (2).");
+ is(gSources.visibleItems.length, 2,
+ "Not all the sources are shown after the global search (2).");
+
+ ok(isCaretPos(gPanel, 4, 9),
+ "The editor didn't jump to the correct line (2).");
+ is(gEditor.getSelection(), "function",
+ "The editor didn't select the correct text (2).");
+
+ deferred.resolve();
+ });
+
+ EventUtils.sendKey("DOWN", gDebugger);
+
+ return deferred.promise;
+ }
+
+ function doWrapAroundJump() {
+ let deferred = promise.defer();
+
+ waitForSourceAndCaret(gPanel, "-01.js", 4).then(() => {
+ info("Current source url:\n" + getSelectedSourceURL(gSources));
+ info("Debugger editor text:\n" + gEditor.getText());
+
+ ok(getSelectedSourceURL(gSources).includes("-01.js"),
+ "The currently shown source is incorrect (3).");
+ is(gSources.visibleItems.length, 2,
+ "Not all the sources are shown after the global search (3).");
+
+ // The editor's selected text takes a tick to update.
+ ok(isCaretPos(gPanel, 4, 9),
+ "The editor didn't jump to the correct line (3).");
+ is(gEditor.getSelection(), "function",
+ "The editor didn't select the correct text (3).");
+
+ deferred.resolve();
+ });
+
+ EventUtils.sendKey("DOWN", gDebugger);
+ EventUtils.sendKey("DOWN", gDebugger);
+
+ return deferred.promise;
+ }
+
+ function doBackwardsWrapAroundJump() {
+ let deferred = promise.defer();
+
+ waitForSourceAndCaret(gPanel, "-02.js", 7).then(() => {
+ info("Current source url:\n" + getSelectedSourceURL(gSources));
+ info("Debugger editor text:\n" + gEditor.getText());
+
+ ok(getSelectedSourceURL(gSources).includes("-02.js"),
+ "The currently shown source is incorrect (4).");
+ is(gSources.visibleItems.length, 2,
+ "Not all the sources are shown after the global search (4).");
+
+ // The editor's selected text takes a tick to update.
+ ok(isCaretPos(gPanel, 7, 11),
+ "The editor didn't jump to the correct line (4).");
+ is(gEditor.getSelection(), "function",
+ "The editor didn't select the correct text (4).");
+
+ deferred.resolve();
+ });
+
+ EventUtils.sendKey("UP", gDebugger);
+
+ return deferred.promise;
+ }
+
+ function testSearchTokenEmpty() {
+ backspaceText(gSearchBox, 4);
+
+ info("Current source url:\n" + getSelectedSourceURL(gSources));
+ info("Debugger editor text:\n" + gEditor.getText());
+
+ ok(getSelectedSourceURL(gSources).includes("-02.js"),
+ "The currently shown source is incorrect (4).");
+ is(gSources.visibleItems.length, 2,
+ "Not all the sources are shown after the global search (4).");
+ ok(isCaretPos(gPanel, 7, 11),
+ "The editor didn't remain at the correct line (4).");
+ is(gEditor.getSelection(), "",
+ "The editor shouldn't keep the previous text selected (4).");
+
+ is(gSearchView.itemCount, 0,
+ "The global search pane shouldn't have any child nodes after clearing.");
+ is(gSearchView.widget._parent.hidden, true,
+ "The global search pane shouldn't be visible after clearing.");
+ is(gSearchView._splitter.hidden, true,
+ "The global search pane splitter shouldn't be visible after clearing.");
+ }
+
+ Task.spawn(function* () {
+ yield waitForSourceAndCaretAndScopes(gPanel, "-02.js", 1);
+ yield firstSearch();
+ yield doFirstJump();
+ yield doSecondJump();
+ yield doWrapAroundJump();
+ yield doBackwardsWrapAroundJump();
+ yield testSearchTokenEmpty();
+ resumeDebuggerThenCloseAndFinish(gPanel);
+ });
+
+ callInTab(gTab, "firstCall");
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_search-global-03.js b/devtools/client/debugger/test/mochitest/browser_dbg_search-global-03.js
new file mode 100644
index 000000000..d36f42b59
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_search-global-03.js
@@ -0,0 +1,110 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests if the global search results are cleared on location changes, and
+ * the expected UI behaviors are triggered.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
+
+var gTab, gPanel, gDebugger;
+var gEditor, gSources, gSearchView, gSearchBox;
+
+function test() {
+ let options = {
+ source: EXAMPLE_URL + "code_script-switching-01.js",
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gEditor = gDebugger.DebuggerView.editor;
+ gSources = gDebugger.DebuggerView.Sources;
+ gSearchView = gDebugger.DebuggerView.GlobalSearch;
+ gSearchBox = gDebugger.DebuggerView.Filtering._searchbox;
+
+ waitForSourceAndCaretAndScopes(gPanel, "-02.js", 1)
+ .then(firstSearch)
+ .then(performTest)
+ .then(() => closeDebuggerAndFinish(gPanel))
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+
+ callInTab(gTab, "firstCall");
+ });
+}
+
+function firstSearch() {
+ let deferred = promise.defer();
+
+ is(gSearchView.itemCount, 0,
+ "The global search pane shouldn't have any entries yet.");
+ is(gSearchView.widget._parent.hidden, true,
+ "The global search pane shouldn't be visible yet.");
+ is(gSearchView._splitter.hidden, true,
+ "The global search pane splitter shouldn't be visible yet.");
+
+ gDebugger.once(gDebugger.EVENTS.GLOBAL_SEARCH_MATCH_FOUND, () => {
+ // Some operations are synchronously dispatched on the main thread,
+ // to avoid blocking UI, thus giving the impression of faster searching.
+ executeSoon(() => {
+ info("Current source url:\n" + getSelectedSourceURL(gSources));
+ info("Debugger editor text:\n" + gEditor.getText());
+
+ ok(isCaretPos(gPanel, 6),
+ "The editor shouldn't have jumped to a matching line yet.");
+ ok(getSelectedSourceURL(gSources).includes("-02.js"),
+ "The current source shouldn't have changed after a global search.");
+ is(gSources.visibleItems.length, 2,
+ "Not all the sources are shown after the global search.");
+
+ deferred.resolve();
+ });
+ });
+
+ setText(gSearchBox, "!function");
+
+ return deferred.promise;
+}
+
+function performTest() {
+ let deferred = promise.defer();
+
+ is(gSearchView.itemCount, 2,
+ "The global search pane should have some entries from the previous search.");
+ is(gSearchView.widget._parent.hidden, false,
+ "The global search pane should be visible from the previous search.");
+ is(gSearchView._splitter.hidden, false,
+ "The global search pane splitter should be visible from the previous search.");
+
+ reloadActiveTab(gPanel, gDebugger.EVENTS.SOURCE_SHOWN).then(() => {
+ info("Current source url:\n" + getSelectedSourceURL(gSources));
+ info("Debugger editor text:\n" + gEditor.getText());
+
+ is(gSearchView.itemCount, 0,
+ "The global search pane shouldn't have any entries after a page navigation.");
+ is(gSearchView.widget._parent.hidden, true,
+ "The global search pane shouldn't be visible after a page navigation.");
+ is(gSearchView._splitter.hidden, true,
+ "The global search pane splitter shouldn't be visible after a page navigation.");
+
+ deferred.resolve();
+ });
+
+ return deferred.promise;
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gEditor = null;
+ gSources = null;
+ gSearchView = null;
+ gSearchBox = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_search-global-04.js b/devtools/client/debugger/test/mochitest/browser_dbg_search-global-04.js
new file mode 100644
index 000000000..3dde460e4
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_search-global-04.js
@@ -0,0 +1,98 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests if the global search results trigger MatchFound and NoMatchFound events
+ * properly, and triggers the expected UI behavior.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
+
+var gTab, gPanel, gDebugger;
+var gEditor, gSources, gSearchView, gSearchBox;
+
+function test() {
+ let options = {
+ source: EXAMPLE_URL + "code_script-switching-01.js",
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gEditor = gDebugger.DebuggerView.editor;
+ gSources = gDebugger.DebuggerView.Sources;
+ gSearchView = gDebugger.DebuggerView.GlobalSearch;
+ gSearchBox = gDebugger.DebuggerView.Filtering._searchbox;
+
+ waitForSourceAndCaretAndScopes(gPanel, "-02.js", 1)
+ .then(firstSearch)
+ .then(secondSearch)
+ .then(() => resumeDebuggerThenCloseAndFinish(gPanel))
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+
+ callInTab(gTab, "firstCall");
+ });
+}
+
+function firstSearch() {
+ let deferred = promise.defer();
+
+ gDebugger.once(gDebugger.EVENTS.GLOBAL_SEARCH_MATCH_FOUND, () => {
+ // Some operations are synchronously dispatched on the main thread,
+ // to avoid blocking UI, thus giving the impression of faster searching.
+ executeSoon(() => {
+ info("Current source url:\n" + getSelectedSourceURL(gSources));
+ info("Debugger editor text:\n" + gEditor.getText());
+
+ ok(isCaretPos(gPanel, 6),
+ "The editor shouldn't have jumped to a matching line yet.");
+ ok(getSelectedSourceURL(gSources).includes("-02.js"),
+ "The current source shouldn't have changed after a global search.");
+ is(gSources.visibleItems.length, 2,
+ "Not all the sources are shown after the global search.");
+
+ deferred.resolve();
+ });
+ });
+
+ setText(gSearchBox, "!function");
+
+ return deferred.promise;
+}
+
+function secondSearch() {
+ let deferred = promise.defer();
+
+ gDebugger.once(gDebugger.EVENTS.GLOBAL_SEARCH_MATCH_NOT_FOUND, () => {
+ info("Current source url:\n" + getSelectedSourceURL(gSources));
+ info("Debugger editor text:\n" + gEditor.getText());
+
+ ok(isCaretPos(gPanel, 6),
+ "The editor shouldn't have jumped to a matching line yet.");
+ ok(getSelectedSourceURL(gSources).includes("-02.js"),
+ "The current source shouldn't have changed after a global search.");
+ is(gSources.visibleItems.length, 2,
+ "Not all the sources are shown after the global search.");
+
+ deferred.resolve();
+ });
+
+ typeText(gSearchBox, "/");
+
+ return deferred.promise;
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gEditor = null;
+ gSources = null;
+ gSearchView = null;
+ gSearchBox = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_search-global-05.js b/devtools/client/debugger/test/mochitest/browser_dbg_search-global-05.js
new file mode 100644
index 000000000..c441a88ac
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_search-global-05.js
@@ -0,0 +1,160 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests if the global search results are expanded/collapsed on click, and
+ * clicking matches makes the source editor shows the correct source and
+ * makes a selection based on the match.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
+
+var gTab, gPanel, gDebugger;
+var gEditor, gSources, gSearchView, gSearchBox;
+
+function test() {
+ let options = {
+ source: EXAMPLE_URL + "code_script-switching-01.js",
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gEditor = gDebugger.DebuggerView.editor;
+ gSources = gDebugger.DebuggerView.Sources;
+ gSearchView = gDebugger.DebuggerView.GlobalSearch;
+ gSearchBox = gDebugger.DebuggerView.Filtering._searchbox;
+
+ waitForSourceAndCaretAndScopes(gPanel, "-02.js", 1)
+ .then(doSearch)
+ .then(testExpandCollapse)
+ .then(testClickLineToJump)
+ .then(testClickMatchToJump)
+ .then(() => resumeDebuggerThenCloseAndFinish(gPanel))
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+
+ callInTab(gTab, "firstCall");
+ });
+}
+
+function doSearch() {
+ let deferred = promise.defer();
+
+ gDebugger.once(gDebugger.EVENTS.GLOBAL_SEARCH_MATCH_FOUND, () => {
+ // Some operations are synchronously dispatched on the main thread,
+ // to avoid blocking UI, thus giving the impression of faster searching.
+ executeSoon(() => {
+ info("Current source url:\n" + getSelectedSourceURL(gSources));
+ info("Debugger editor text:\n" + gEditor.getText());
+
+ ok(isCaretPos(gPanel, 6),
+ "The editor shouldn't have jumped to a matching line yet. (1)");
+ ok(getSelectedSourceURL(gSources).includes("-02.js"),
+ "The current source shouldn't have changed after a global search. (2)");
+ is(gSources.visibleItems.length, 2,
+ "Not all the sources are shown after the global search. (3)");
+
+ deferred.resolve();
+ });
+ });
+
+ setText(gSearchBox, "!a");
+
+ return deferred.promise;
+}
+
+function testExpandCollapse() {
+ let sourceResults = gDebugger.document.querySelectorAll(".dbg-source-results");
+ let item0 = gDebugger.SourceResults.getItemForElement(sourceResults[0]);
+ let item1 = gDebugger.SourceResults.getItemForElement(sourceResults[1]);
+ let firstHeader = sourceResults[0].querySelector(".dbg-results-header");
+ let secondHeader = sourceResults[1].querySelector(".dbg-results-header");
+
+ EventUtils.sendMouseEvent({ type: "click" }, firstHeader);
+ EventUtils.sendMouseEvent({ type: "click" }, secondHeader);
+
+ is(item0.instance.expanded, false,
+ "The first source results should be collapsed on click. (2)");
+ is(item1.instance.expanded, false,
+ "The second source results should be collapsed on click. (2)");
+
+ EventUtils.sendMouseEvent({ type: "click" }, firstHeader);
+ EventUtils.sendMouseEvent({ type: "click" }, secondHeader);
+
+ is(item0.instance.expanded, true,
+ "The first source results should be expanded on an additional click. (3)");
+ is(item1.instance.expanded, true,
+ "The second source results should be expanded on an additional click. (3)");
+}
+
+function testClickLineToJump() {
+ let deferred = promise.defer();
+
+ let sourceResults = gDebugger.document.querySelectorAll(".dbg-source-results");
+ let firstHeader = sourceResults[0].querySelector(".dbg-results-header");
+ let firstLine = sourceResults[0].querySelector(".dbg-results-line-contents");
+
+ waitForSourceAndCaret(gPanel, "-01.js", 1, 1).then(() => {
+ info("Current source url:\n" + getSelectedSourceURL(gSources));
+ info("Debugger editor text:\n" + gEditor.getText());
+
+ ok(isCaretPos(gPanel, 1, 5),
+ "The editor didn't jump to the correct line (4).");
+ is(gEditor.getSelection(), "A",
+ "The editor didn't select the correct text (4).");
+ ok(getSelectedSourceURL(gSources).includes("-01.js"),
+ "The currently shown source is incorrect (4).");
+ is(gSources.visibleItems.length, 2,
+ "Not all the sources are shown after the global search (4).");
+
+ deferred.resolve();
+ });
+
+ EventUtils.sendMouseEvent({ type: "click" }, firstLine);
+
+ return deferred.promise;
+}
+
+function testClickMatchToJump() {
+ let deferred = promise.defer();
+
+ let sourceResults = gDebugger.document.querySelectorAll(".dbg-source-results");
+ let secondHeader = sourceResults[1].querySelector(".dbg-results-header");
+ let secondMatches = sourceResults[1].querySelectorAll(".dbg-results-line-contents-string[match=true]");
+ let lastMatch = Array.slice(secondMatches).pop();
+
+ waitForSourceAndCaret(gPanel, "-02.js", 13, 3).then(() => {
+ info("Current source url:\n" + getSelectedSourceURL(gSources));
+ info("Debugger editor text:\n" + gEditor.getText());
+
+ ok(isCaretPos(gPanel, 13, 3),
+ "The editor didn't jump to the correct line (5).");
+ is(gEditor.getSelection(), "a",
+ "The editor didn't select the correct text (5).");
+ ok(getSelectedSourceURL(gSources).includes("-02.js"),
+ "The currently shown source is incorrect (5).");
+ is(gSources.visibleItems.length, 2,
+ "Not all the sources are shown after the global search (5).");
+
+ deferred.resolve();
+ });
+
+ EventUtils.sendMouseEvent({ type: "click" }, lastMatch);
+
+ return deferred.promise;
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gEditor = null;
+ gSources = null;
+ gSearchView = null;
+ gSearchBox = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_search-global-06.js b/devtools/client/debugger/test/mochitest/browser_dbg_search-global-06.js
new file mode 100644
index 000000000..2de3ac558
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_search-global-06.js
@@ -0,0 +1,125 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests if the global search results are hidden when they're supposed to
+ * (after a focus lost, or when ESCAPE is pressed).
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
+
+var gTab, gPanel, gDebugger;
+var gEditor, gSources, gSearchView, gSearchBox;
+
+function test() {
+ let options = {
+ source: EXAMPLE_URL + "code_script-switching-01.js",
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gEditor = gDebugger.DebuggerView.editor;
+ gSources = gDebugger.DebuggerView.Sources;
+ gSearchView = gDebugger.DebuggerView.GlobalSearch;
+ gSearchBox = gDebugger.DebuggerView.Filtering._searchbox;
+
+ waitForSourceAndCaretAndScopes(gPanel, "-02.js", 1)
+ .then(doSearch)
+ .then(testFocusLost)
+ .then(doSearch)
+ .then(testEscape)
+ .then(() => resumeDebuggerThenCloseAndFinish(gPanel))
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+
+ callInTab(gTab, "firstCall");
+ });
+}
+
+function doSearch() {
+ let deferred = promise.defer();
+
+ is(gSearchView.itemCount, 0,
+ "The global search pane shouldn't have any entries yet.");
+ is(gSearchView.widget._parent.hidden, true,
+ "The global search pane shouldn't be visible yet.");
+ is(gSearchView._splitter.hidden, true,
+ "The global search pane splitter shouldn't be visible yet.");
+
+ gDebugger.once(gDebugger.EVENTS.GLOBAL_SEARCH_MATCH_FOUND, () => {
+ // Some operations are synchronously dispatched on the main thread,
+ // to avoid blocking UI, thus giving the impression of faster searching.
+ executeSoon(() => {
+ info("Current source url:\n" + getSelectedSourceURL(gSources));
+ info("Debugger editor text:\n" + gEditor.getText());
+
+ ok(isCaretPos(gPanel, 6),
+ "The editor shouldn't have jumped to a matching line yet.");
+ ok(getSelectedSourceURL(gSources).includes("-02.js"),
+ "The current source shouldn't have changed after a global search.");
+ is(gSources.visibleItems.length, 2,
+ "Not all the sources are shown after the global search.");
+
+ deferred.resolve();
+ });
+ });
+
+ setText(gSearchBox, "!a");
+
+ return deferred.promise;
+}
+
+function testFocusLost() {
+ is(gSearchView.itemCount, 2,
+ "The global search pane should have some entries from the previous search.");
+ is(gSearchView.widget._parent.hidden, false,
+ "The global search pane should be visible from the previous search.");
+ is(gSearchView._splitter.hidden, false,
+ "The global search pane splitter should be visible from the previous search.");
+
+ gDebugger.DebuggerView.editor.focus();
+
+ info("Current source url:\n" + getSelectedSourceURL(gSources));
+ info("Debugger editor text:\n" + gEditor.getText());
+
+ is(gSearchView.itemCount, 0,
+ "The global search pane shouldn't have any child nodes after clearing.");
+ is(gSearchView.widget._parent.hidden, true,
+ "The global search pane shouldn't be visible after clearing.");
+ is(gSearchView._splitter.hidden, true,
+ "The global search pane splitter shouldn't be visible after clearing.");
+}
+
+function testEscape() {
+ is(gSearchView.itemCount, 2,
+ "The global search pane should have some entries from the previous search.");
+ is(gSearchView.widget._parent.hidden, false,
+ "The global search pane should be visible from the previous search.");
+ is(gSearchView._splitter.hidden, false,
+ "The global search pane splitter should be visible from the previous search.");
+
+ gSearchBox.focus();
+ EventUtils.sendKey("ESCAPE", gDebugger);
+
+ is(gSearchView.itemCount, 0,
+ "The global search pane shouldn't have any child nodes after clearing.");
+ is(gSearchView.widget._parent.hidden, true,
+ "The global search pane shouldn't be visible after clearing.");
+ is(gSearchView._splitter.hidden, true,
+ "The global search pane splitter shouldn't be visible after clearing.");
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gEditor = null;
+ gSources = null;
+ gSearchView = null;
+ gSearchBox = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_search-popup-jank.js b/devtools/client/debugger/test/mochitest/browser_dbg_search-popup-jank.js
new file mode 100644
index 000000000..317dd6369
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_search-popup-jank.js
@@ -0,0 +1,128 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests that sources aren't selected by default when finding a match.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_editor-mode.html";
+
+var gTab, gPanel, gDebugger;
+var gSearchBox;
+
+function test() {
+ let options = {
+ source: EXAMPLE_URL + "code_script-switching-01.js?a=b",
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gSearchBox = gDebugger.DebuggerView.Filtering._searchbox;
+
+ gDebugger.DebuggerView.Filtering.FilteredSources._autoSelectFirstItem = false;
+ gDebugger.DebuggerView.Filtering.FilteredFunctions._autoSelectFirstItem = false;
+
+ superGenericFileSearch()
+ .then(() => ensureSourceIs(aPanel, "-01.js"))
+ .then(() => ensureCaretAt(aPanel, 1))
+
+ .then(superAccurateFileSearch)
+ .then(() => ensureSourceIs(aPanel, "-01.js"))
+ .then(() => ensureCaretAt(aPanel, 1))
+ .then(() => pressKeyToHide("RETURN"))
+ .then(() => ensureSourceIs(aPanel, "code_test-editor-mode", true))
+ .then(() => ensureCaretAt(aPanel, 1))
+
+ .then(superGenericFileSearch)
+ .then(() => ensureSourceIs(aPanel, "code_test-editor-mode"))
+ .then(() => ensureCaretAt(aPanel, 1))
+ .then(() => {
+ const shown = waitForSourceShown(aPanel, "doc_editor-mode");
+ pressKey("UP");
+ return shown;
+ })
+ .then(() => ensureCaretAt(aPanel, 1))
+ .then(() => pressKeyToHide("RETURN"))
+ .then(() => ensureSourceIs(aPanel, "doc_editor-mode"))
+ .then(() => ensureCaretAt(aPanel, 1))
+
+ .then(superAccurateFileSearch)
+ .then(() => ensureSourceIs(aPanel, "doc_editor-mode"))
+ .then(() => ensureCaretAt(aPanel, 1))
+ .then(() => {
+ const shown = waitForSourceShown(gPanel, "code_test-editor-mode");
+ typeText(gSearchBox, ":");
+ return shown;
+ })
+ .then(() => ensureSourceIs(aPanel, "code_test-editor-mode", true))
+ .then(() => ensureCaretAt(aPanel, 1))
+ .then(() => typeText(gSearchBox, "5"))
+ .then(() => ensureSourceIs(aPanel, "code_test-editor-mode"))
+ .then(() => ensureCaretAt(aPanel, 5))
+ .then(() => pressKey("DOWN"))
+ .then(() => ensureSourceIs(aPanel, "code_test-editor-mode"))
+ .then(() => ensureCaretAt(aPanel, 6))
+
+ .then(superGenericFunctionSearch)
+ .then(() => ensureSourceIs(aPanel, "code_test-editor-mode"))
+ .then(() => ensureCaretAt(aPanel, 6))
+ .then(() => pressKey("RETURN"))
+ .then(() => ensureSourceIs(aPanel, "code_test-editor-mode"))
+ .then(() => ensureCaretAt(aPanel, 4, 10))
+
+ .then(() => closeDebuggerAndFinish(gPanel))
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+ });
+}
+
+function waitForMatchFoundAndResultsShown(aName) {
+ return promise.all([
+ once(gDebugger, "popupshown"),
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS[aName])
+ ]);
+}
+
+function waitForResultsHidden() {
+ return once(gDebugger, "popuphidden");
+}
+
+function superGenericFunctionSearch() {
+ let finished = waitForMatchFoundAndResultsShown("FUNCTION_SEARCH_MATCH_FOUND");
+ setText(gSearchBox, "@");
+ return finished;
+}
+
+function superGenericFileSearch() {
+ let finished = waitForMatchFoundAndResultsShown("FILE_SEARCH_MATCH_FOUND");
+ setText(gSearchBox, ".");
+ return finished;
+}
+
+function superAccurateFileSearch() {
+ let finished = waitForMatchFoundAndResultsShown("FILE_SEARCH_MATCH_FOUND");
+ setText(gSearchBox, "editor");
+ return finished;
+}
+
+function pressKey(aKey) {
+ EventUtils.sendKey(aKey, gDebugger);
+}
+
+function pressKeyToHide(aKey) {
+ let finished = waitForResultsHidden();
+ EventUtils.sendKey(aKey, gDebugger);
+ return finished;
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gSearchBox = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_search-sources-01.js b/devtools/client/debugger/test/mochitest/browser_dbg_search-sources-01.js
new file mode 100644
index 000000000..671f931a7
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_search-sources-01.js
@@ -0,0 +1,232 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests basic functionality of sources filtering (file search).
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
+
+function test() {
+ // Debug test slaves are a bit slow at this test.
+ requestLongerTimeout(3);
+
+ let options = {
+ source: EXAMPLE_URL + "code_script-switching-01.js",
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ const gTab = aTab;
+ const gPanel = aPanel;
+ const gDebugger = gPanel.panelWin;
+ const gSources = gDebugger.DebuggerView.Sources;
+ const gSearchView = gDebugger.DebuggerView.Filtering.FilteredSources;
+ const gSearchBox = gDebugger.DebuggerView.Filtering._searchbox;
+
+ Task.spawn(function* () {
+ // move searches to yields
+ // not sure what to do with the error...
+ yield bogusSearch();
+ yield firstSearch();
+ yield secondSearch();
+ yield thirdSearch();
+ yield fourthSearch();
+ yield fifthSearch();
+ yield sixthSearch();
+ yield seventhSearch();
+
+ return closeDebuggerAndFinish(gPanel)
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+ });
+
+ function bogusSearch() {
+ let finished = promise.all([
+ ensureSourceIs(gPanel, "-01.js"),
+ ensureCaretAt(gPanel, 1),
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_NOT_FOUND)
+ ]);
+
+ setText(gSearchBox, "BOGUS");
+
+ return finished.then(() => promise.all([
+ ensureSourceIs(gPanel, "-01.js"),
+ ensureCaretAt(gPanel, 1),
+ verifyContents({ itemCount: 0, hidden: true })
+ ]));
+ }
+
+ function firstSearch() {
+ let finished = promise.all([
+ ensureSourceIs(gPanel, "-01.js"),
+ ensureCaretAt(gPanel, 1),
+ once(gDebugger, "popupshown"),
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_FOUND),
+ waitForSourceShown(gPanel, "-02.js")
+ ]);
+
+ setText(gSearchBox, "-02.js");
+
+ return finished.then(() => promise.all([
+ ensureSourceIs(gPanel, "-02.js"),
+ ensureCaretAt(gPanel, 1),
+ verifyContents({ itemCount: 1, hidden: false })
+ ]));
+ }
+
+ function secondSearch() {
+ let finished = promise.all([
+ once(gDebugger, "popupshown"),
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_FOUND),
+ waitForSourceShown(gPanel, "-01.js")
+ ])
+ .then(() => {
+ let finished = promise.all([
+ once(gDebugger, "popupshown"),
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_FOUND),
+ waitForCaretUpdated(gPanel, 5)
+ ])
+ .then(() => promise.all([
+ ensureSourceIs(gPanel, "-01.js"),
+ ensureCaretAt(gPanel, 5),
+ verifyContents({ itemCount: 1, hidden: false })
+ ]));
+
+ typeText(gSearchBox, ":5");
+ return finished;
+ });
+
+ setText(gSearchBox, ".*-01\.js");
+ return finished;
+ }
+
+ function thirdSearch() {
+ let finished = promise.all([
+ once(gDebugger, "popupshown"),
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_FOUND),
+ waitForSourceShown(gPanel, "-02.js")
+ ])
+ .then(() => {
+ let finished = promise.all([
+ once(gDebugger, "popupshown"),
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_FOUND),
+ waitForCaretUpdated(gPanel, 6, 6)
+ ])
+ .then(() => promise.all([
+ ensureSourceIs(gPanel, "-02.js"),
+ ensureCaretAt(gPanel, 6, 6),
+ verifyContents({ itemCount: 1, hidden: false })
+ ]));
+
+ typeText(gSearchBox, "#deb");
+ return finished;
+ });
+
+ setText(gSearchBox, ".*-02\.js");
+ return finished;
+ }
+
+ function fourthSearch() {
+ let finished = promise.all([
+ once(gDebugger, "popupshown"),
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_FOUND),
+ waitForSourceShown(gPanel, "-01.js")
+ ])
+ .then(() => {
+ let finished = promise.all([
+ once(gDebugger, "popupshown"),
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_FOUND),
+ waitForCaretUpdated(gPanel, 2, 9),
+ ])
+ .then(() => promise.all([
+ ensureSourceIs(gPanel, "-01.js"),
+ ensureCaretAt(gPanel, 2, 9),
+ verifyContents({ itemCount: 1, hidden: false })
+ // ...because we simply searched for ":" in the current file.
+ ]));
+
+ typeText(gSearchBox, "#:"); // # has precedence.
+ return finished;
+ });
+
+ setText(gSearchBox, ".*-01\.js");
+ return finished;
+ }
+
+ function fifthSearch() {
+ let finished = promise.all([
+ once(gDebugger, "popupshown"),
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_FOUND),
+ waitForSourceShown(gPanel, "-02.js")
+ ])
+ .then(() => {
+ let finished = promise.all([
+ once(gDebugger, "popuphidden"),
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_NOT_FOUND),
+ waitForCaretUpdated(gPanel, 1, 3)
+ ])
+ .then(() => promise.all([
+ ensureSourceIs(gPanel, "-02.js"),
+ ensureCaretAt(gPanel, 1, 3),
+ verifyContents({ itemCount: 0, hidden: true })
+ // ...because the searched label includes ":5", so nothing is found.
+ ]));
+
+ typeText(gSearchBox, ":5#*"); // # has precedence.
+ return finished;
+ });
+
+ setText(gSearchBox, ".*-02\.js");
+ return finished;
+ }
+
+ function sixthSearch() {
+ let finished = promise.all([
+ ensureSourceIs(gPanel, "-02.js"),
+ ensureCaretAt(gPanel, 1, 3),
+ once(gDebugger, "popupshown"),
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_FOUND),
+ waitForCaretUpdated(gPanel, 5)
+ ]);
+
+ backspaceText(gSearchBox, 2);
+
+ return finished.then(() => promise.all([
+ ensureSourceIs(gPanel, "-02.js"),
+ ensureCaretAt(gPanel, 5),
+ verifyContents({ itemCount: 1, hidden: false })
+ ]));
+ }
+
+ function seventhSearch() {
+ let finished = promise.all([
+ ensureSourceIs(gPanel, "-02.js"),
+ ensureCaretAt(gPanel, 5),
+ once(gDebugger, "popupshown"),
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_FOUND),
+ waitForSourceShown(gPanel, "-01.js"),
+ ]);
+
+ backspaceText(gSearchBox, 6);
+
+ return finished.then(() => promise.all([
+ ensureSourceIs(gPanel, "-01.js"),
+ ensureCaretAt(gPanel, 2, 9),
+ verifyContents({ itemCount: 2, hidden: false })
+ ]));
+ }
+
+ function verifyContents(aArgs) {
+ is(gSources.visibleItems.length, 2,
+ "The unmatched sources in the widget should not be hidden.");
+ is(gSearchView.itemCount, aArgs.itemCount,
+ "No sources should be displayed in the sources container after a bogus search.");
+ is(gSearchView.hidden, aArgs.hidden,
+ "No sources should be displayed in the sources container after a bogus search.");
+ }
+
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_search-sources-02.js b/devtools/client/debugger/test/mochitest/browser_dbg_search-sources-02.js
new file mode 100644
index 000000000..3e06a1bdf
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_search-sources-02.js
@@ -0,0 +1,281 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests more complex functionality of sources filtering (file search).
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_editor-mode.html";
+
+var gTab, gPanel, gDebugger;
+var gSources, gSourceUtils, gSearchView, gSearchBox;
+
+function test() {
+ // Debug test slaves are a bit slow at this test.
+ requestLongerTimeout(3);
+
+ let options = {
+ source: EXAMPLE_URL + "code_script-switching-01.js?a=b",
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gSources = gDebugger.DebuggerView.Sources;
+ gSourceUtils = gDebugger.SourceUtils;
+ gSearchView = gDebugger.DebuggerView.Filtering.FilteredSources;
+ gSearchBox = gDebugger.DebuggerView.Filtering._searchbox;
+
+ firstSearch()
+ .then(secondSearch)
+ .then(thirdSearch)
+ .then(fourthSearch)
+ .then(fifthSearch)
+ .then(goDown)
+ .then(goDownAndWrap)
+ .then(goUpAndWrap)
+ .then(goUp)
+ .then(returnAndSwitch)
+ .then(firstSearch)
+ .then(clickAndSwitch)
+ .then(() => closeDebuggerAndFinish(gPanel))
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+ });
+}
+
+function firstSearch() {
+ let finished = promise.all([
+ ensureSourceIs(gPanel, "-01.js"),
+ ensureCaretAt(gPanel, 1),
+ once(gDebugger, "popupshown"),
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_FOUND)
+ ]);
+
+ setText(gSearchBox, ".");
+
+ return finished.then(() => promise.all([
+ ensureSourceIs(gPanel, "-01.js"),
+ ensureCaretAt(gPanel, 1),
+ verifyContents([
+ "code_script-switching-01.js?a=b",
+ "code_test-editor-mode?c=d",
+ "doc_editor-mode.html"
+ ])
+ ]));
+}
+
+function secondSearch() {
+ let finished = promise.all([
+ ensureSourceIs(gPanel, "-01.js"),
+ ensureCaretAt(gPanel, 1),
+ once(gDebugger, "popupshown"),
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_FOUND)
+ ]);
+
+ typeText(gSearchBox, "-0");
+
+ return finished.then(() => promise.all([
+ ensureSourceIs(gPanel, "-01.js"),
+ ensureCaretAt(gPanel, 1),
+ verifyContents(["code_script-switching-01.js?a=b"])
+ ]));
+}
+
+function thirdSearch() {
+ let finished = promise.all([
+ ensureSourceIs(gPanel, "-01.js"),
+ ensureCaretAt(gPanel, 1),
+ once(gDebugger, "popupshown"),
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_FOUND)
+ ]);
+
+ backspaceText(gSearchBox, 1);
+
+ return finished.then(() => promise.all([
+ ensureSourceIs(gPanel, "-01.js"),
+ ensureCaretAt(gPanel, 1),
+ verifyContents([
+ "code_script-switching-01.js?a=b",
+ "code_test-editor-mode?c=d",
+ "doc_editor-mode.html"
+ ])
+ ]));
+}
+
+function fourthSearch() {
+ let finished = promise.all([
+ ensureSourceIs(gPanel, "-01.js"),
+ ensureCaretAt(gPanel, 1),
+ once(gDebugger, "popupshown"),
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_FOUND),
+ waitForSourceShown(gPanel, "test-editor-mode")
+ ]);
+
+ setText(gSearchBox, "code_test");
+
+ return finished.then(() => promise.all([
+ ensureSourceIs(gPanel, "test-editor-mode"),
+ ensureCaretAt(gPanel, 1),
+ verifyContents(["code_test-editor-mode?c=d"])
+ ]));
+}
+
+function fifthSearch() {
+ let finished = promise.all([
+ ensureSourceIs(gPanel, "test-editor-mode"),
+ ensureCaretAt(gPanel, 1),
+ once(gDebugger, "popupshown"),
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_FOUND),
+ waitForSourceShown(gPanel, "-01.js")
+ ]);
+
+ backspaceText(gSearchBox, 4);
+
+ return finished.then(() => promise.all([
+ ensureSourceIs(gPanel, "-01.js"),
+ ensureCaretAt(gPanel, 1),
+ verifyContents([
+ "code_script-switching-01.js?a=b",
+ "code_test-editor-mode?c=d"
+ ])
+ ]));
+}
+
+function goDown() {
+ let finished = promise.all([
+ ensureSourceIs(gPanel, "-01.js"),
+ ensureCaretAt(gPanel, 1),
+ waitForSourceShown(gPanel, "test-editor-mode"),
+ ]);
+
+ EventUtils.sendKey("DOWN", gDebugger);
+
+ return finished.then(() => promise.all([
+ ensureSourceIs(gPanel, "test-editor-mode"),
+ ensureCaretAt(gPanel, 1),
+ verifyContents([
+ "code_script-switching-01.js?a=b",
+ "code_test-editor-mode?c=d"
+ ])
+ ]));
+}
+
+function goDownAndWrap() {
+ let finished = promise.all([
+ ensureSourceIs(gPanel, "test-editor-mode"),
+ ensureCaretAt(gPanel, 1),
+ waitForSourceShown(gPanel, "-01.js")
+ ]);
+
+ EventUtils.synthesizeKey("g", { metaKey: true }, gDebugger);
+
+ return finished.then(() => promise.all([
+ ensureSourceIs(gPanel, "-01.js"),
+ ensureCaretAt(gPanel, 1),
+ verifyContents([
+ "code_script-switching-01.js?a=b",
+ "code_test-editor-mode?c=d"
+ ])
+ ]));
+}
+
+function goUpAndWrap() {
+ let finished = promise.all([
+ ensureSourceIs(gPanel, "-01.js"),
+ ensureCaretAt(gPanel, 1),
+ waitForSourceShown(gPanel, "test-editor-mode")
+ ]);
+
+ EventUtils.synthesizeKey("G", { metaKey: true }, gDebugger);
+
+ return finished.then(() => promise.all([
+ ensureSourceIs(gPanel, "test-editor-mode"),
+ ensureCaretAt(gPanel, 1),
+ verifyContents([
+ "code_script-switching-01.js?a=b",
+ "code_test-editor-mode?c=d"
+ ])
+ ]));
+}
+
+function goUp() {
+ let finished = promise.all([
+ ensureSourceIs(gPanel, "test-editor-mode"),
+ ensureCaretAt(gPanel, 1),
+ waitForSourceShown(gPanel, "-01.js"),
+ ]);
+
+ EventUtils.sendKey("UP", gDebugger);
+
+ return finished.then(() => promise.all([
+ ensureSourceIs(gPanel, "-01.js"),
+ ensureCaretAt(gPanel, 1),
+ verifyContents([
+ "code_script-switching-01.js?a=b",
+ "code_test-editor-mode?c=d"
+ ])
+ ]));
+}
+
+function returnAndSwitch() {
+ let finished = promise.all([
+ ensureSourceIs(gPanel, "-01.js"),
+ ensureCaretAt(gPanel, 1),
+ once(gDebugger, "popuphidden")
+ ]);
+
+ EventUtils.sendKey("RETURN", gDebugger);
+
+ return finished.then(() => promise.all([
+ ensureSourceIs(gPanel, "-01.js"),
+ ensureCaretAt(gPanel, 1)
+ ]));
+}
+
+function clickAndSwitch() {
+ let finished = promise.all([
+ ensureSourceIs(gPanel, "-01.js"),
+ ensureCaretAt(gPanel, 1),
+ once(gDebugger, "popuphidden"),
+ waitForSourceShown(gPanel, "test-editor-mode")
+ ]);
+
+ EventUtils.sendMouseEvent({ type: "click" }, gSearchView.items[1].target, gDebugger);
+
+ return finished.then(() => promise.all([
+ ensureSourceIs(gPanel, "test-editor-mode"),
+ ensureCaretAt(gPanel, 1)
+ ]));
+}
+
+function verifyContents(aMatches) {
+ is(gSources.visibleItems.length, 3,
+ "The unmatched sources in the widget should not be hidden.");
+ is(gSearchView.itemCount, aMatches.length,
+ "The filtered sources view should have the right items available.");
+
+ for (let i = 0; i < gSearchView.itemCount; i++) {
+ let trimmedLabel = gSourceUtils.trimUrlLength(gSourceUtils.trimUrlQuery(aMatches[i]));
+ let trimmedLocation = gSourceUtils.trimUrlLength(EXAMPLE_URL + aMatches[i], 0, "start");
+
+ ok(gSearchView.widget._parent.querySelector(".results-panel-item-label[value=\"" + trimmedLabel + "\"]"),
+ "The filtered sources view should have the correct source labels.");
+ ok(gSearchView.widget._parent.querySelector(".results-panel-item-label-below[value=\"" + trimmedLocation + "\"]"),
+ "The filtered sources view should have the correct source locations.");
+ }
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gSources = null;
+ gSourceUtils = null;
+ gSearchView = null;
+ gSearchBox = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_search-sources-03.js b/devtools/client/debugger/test/mochitest/browser_dbg_search-sources-03.js
new file mode 100644
index 000000000..904a51f76
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_search-sources-03.js
@@ -0,0 +1,103 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests that while searching for files, the sources list remains unchanged.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_editor-mode.html";
+
+var gTab, gPanel, gDebugger;
+var gSources, gSearchBox;
+
+function test() {
+ let options = {
+ source: EXAMPLE_URL + "code_script-switching-01.js?a=b",
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gSources = gDebugger.DebuggerView.Sources;
+ gSearchBox = gDebugger.DebuggerView.Filtering._searchbox;
+
+ superGenericSearch()
+ .then(verifySourcesPane)
+ .then(kindaInterpretableSearch)
+ .then(verifySourcesPane)
+ .then(incrediblySpecificSearch)
+ .then(verifySourcesPane)
+ .then(returnAndHide)
+ .then(verifySourcesPane)
+ .then(() => closeDebuggerAndFinish(gPanel))
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+ });
+}
+
+function waitForMatchFoundAndResultsShown() {
+ return promise.all([
+ once(gDebugger, "popupshown"),
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_FOUND)
+ ]).then(() => promise.all([
+ ensureSourceIs(gPanel, "-01.js"),
+ ensureCaretAt(gPanel, 1)
+ ]));
+}
+
+function waitForResultsHidden() {
+ return once(gDebugger, "popuphidden").then(() => promise.all([
+ ensureSourceIs(gPanel, "-01.js"),
+ ensureCaretAt(gPanel, 1)
+ ]));
+}
+
+function superGenericSearch() {
+ let finished = waitForMatchFoundAndResultsShown();
+ setText(gSearchBox, ".");
+ return finished;
+}
+
+function kindaInterpretableSearch() {
+ let finished = waitForMatchFoundAndResultsShown();
+ typeText(gSearchBox, "-0");
+ return finished;
+}
+
+function incrediblySpecificSearch() {
+ let finished = waitForMatchFoundAndResultsShown();
+ typeText(gSearchBox, "1.js");
+ return finished;
+}
+
+function returnAndHide() {
+ let finished = waitForResultsHidden();
+ EventUtils.sendKey("RETURN", gDebugger);
+ return finished;
+}
+
+function verifySourcesPane() {
+ is(gSources.itemCount, 3,
+ "There should be 3 items present in the sources container.");
+ is(gSources.visibleItems.length, 3,
+ "There should be no hidden items in the sources container.");
+
+ ok(gSources.getItemForAttachment(e => e.label == "code_script-switching-01.js"),
+ "The first source's label should be correct.");
+ ok(gSources.getItemForAttachment(e => e.label == "code_test-editor-mode"),
+ "The second source's label should be correct.");
+ ok(gSources.getItemForAttachment(e => e.label == "doc_editor-mode.html"),
+ "The third source's label should be correct.");
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gSources = null;
+ gSearchBox = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_search-symbols.js b/devtools/client/debugger/test/mochitest/browser_dbg_search-symbols.js
new file mode 100644
index 000000000..5998acf8e
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_search-symbols.js
@@ -0,0 +1,472 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests if the function searching works properly.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_function-search.html";
+
+var gTab, gPanel, gDebugger;
+var gEditor, gSources, gSearchBox, gFilteredFunctions;
+
+function test() {
+ let options = {
+ source: EXAMPLE_URL + "code_function-search-01.js",
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gEditor = gDebugger.DebuggerView.editor;
+ gSources = gDebugger.DebuggerView.Sources;
+ gSearchBox = gDebugger.DebuggerView.Filtering._searchbox;
+ gFilteredFunctions = gDebugger.DebuggerView.Filtering.FilteredFunctions;
+
+ showSource("doc_function-search.html")
+ .then(htmlSearch)
+ .then(() => showSource("code_function-search-01.js"))
+ .then(firstJsSearch)
+ .then(() => showSource("code_function-search-02.js"))
+ .then(secondJsSearch)
+ .then(() => showSource("code_function-search-03.js"))
+ .then(thirdJsSearch)
+ .then(saveSearch)
+ .then(filterSearch)
+ .then(bogusSearch)
+ .then(incrementalSearch)
+ .then(emptySearch)
+ .then(() => closeDebuggerAndFinish(gPanel))
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+ });
+}
+
+function htmlSearch() {
+ let deferred = promise.defer();
+
+ once(gDebugger, "popupshown").then(() => {
+ writeInfo();
+
+ is(gFilteredFunctions.selectedIndex, 0,
+ "An item should be selected in the filtered functions view (1).");
+ ok(gFilteredFunctions.selectedItem,
+ "An item should be selected in the filtered functions view (2).");
+
+ if (gSources.selectedItem.attachment.source.url.indexOf(".html") != -1) {
+ let expectedResults = [
+ ["inline", ".html", "", 19, 16],
+ ["arrow", ".html", "", 20, 11],
+ ["foo", ".html", "", 22, 11],
+ ["foo2", ".html", "", 23, 11],
+ ["bar2", ".html", "", 23, 18]
+ ];
+
+ for (let [label, value, description, line, column] of expectedResults) {
+ let target = gFilteredFunctions.selectedItem.target;
+
+ if (label) {
+ is(target.querySelector(".results-panel-item-label").getAttribute("value"),
+ gDebugger.SourceUtils.trimUrlLength(label + "()"),
+ "The correct label (" + label + ") is currently selected.");
+ } else {
+ ok(!target.querySelector(".results-panel-item-label"),
+ "Shouldn't create empty label nodes.");
+ }
+ if (value) {
+ ok(target.querySelector(".results-panel-item-label-below").getAttribute("value").includes(value),
+ "The correct value (" + value + ") is attached.");
+ } else {
+ ok(!target.querySelector(".results-panel-item-label-below"),
+ "Shouldn't create empty label nodes.");
+ }
+ if (description) {
+ is(target.querySelector(".results-panel-item-label-before").getAttribute("value"), description,
+ "The correct description (" + description + ") is currently shown.");
+ } else {
+ ok(!target.querySelector(".results-panel-item-label-before"),
+ "Shouldn't create empty label nodes.");
+ }
+
+ ok(isCaretPos(gPanel, line, column),
+ "The editor didn't jump to the correct line.");
+
+ EventUtils.sendKey("DOWN", gDebugger);
+ }
+
+ ok(isCaretPos(gPanel, expectedResults[0][3], expectedResults[0][4]),
+ "The editor didn't jump to the correct line again.");
+
+ deferred.resolve();
+ } else {
+ ok(false, "How did you get here? Go away, you.");
+ }
+ });
+
+ setText(gSearchBox, "@");
+ return deferred.promise;
+}
+
+function firstJsSearch() {
+ let deferred = promise.defer();
+
+ once(gDebugger, "popupshown").then(() => {
+ writeInfo();
+
+ is(gFilteredFunctions.selectedIndex, 0,
+ "An item should be selected in the filtered functions view (1).");
+ ok(gFilteredFunctions.selectedItem,
+ "An item should be selected in the filtered functions view (2).");
+
+ if (gSources.selectedItem.attachment.source.url.indexOf("-01.js") != -1) {
+ let s = " " + gDebugger.L10N.getStr("functionSearchSeparatorLabel") + " ";
+ let expectedResults = [
+ ["test", "-01.js", "", 4, 10],
+ ["anonymousExpression", "-01.js", "test.prototype", 9, 3],
+ ["namedExpression" + s + "NAME", "-01.js", "test.prototype", 11, 3],
+ ["a_test", "-01.js", "foo", 22, 3],
+ ["n_test" + s + "x", "-01.js", "foo", 24, 3],
+ ["a_test", "-01.js", "foo.sub", 27, 5],
+ ["n_test" + s + "y", "-01.js", "foo.sub", 29, 5],
+ ["a_test", "-01.js", "foo.sub.sub", 32, 7],
+ ["n_test" + s + "z", "-01.js", "foo.sub.sub", 34, 7],
+ ["test_SAME_NAME", "-01.js", "foo.sub.sub.sub", 37, 9]
+ ];
+
+ for (let [label, value, description, line, column] of expectedResults) {
+ let target = gFilteredFunctions.selectedItem.target;
+
+ if (label) {
+ is(target.querySelector(".results-panel-item-label").getAttribute("value"),
+ gDebugger.SourceUtils.trimUrlLength(label + "()"),
+ "The correct label (" + label + ") is currently selected.");
+ } else {
+ ok(!target.querySelector(".results-panel-item-label"),
+ "Shouldn't create empty label nodes.");
+ }
+ if (value) {
+ ok(target.querySelector(".results-panel-item-label-below").getAttribute("value").includes(value),
+ "The correct value (" + value + ") is attached.");
+ } else {
+ ok(!target.querySelector(".results-panel-item-label-below"),
+ "Shouldn't create empty label nodes.");
+ }
+ if (description) {
+ is(target.querySelector(".results-panel-item-label-before").getAttribute("value"), description,
+ "The correct description (" + description + ") is currently shown.");
+ } else {
+ ok(!target.querySelector(".results-panel-item-label-before"),
+ "Shouldn't create empty label nodes.");
+ }
+
+ ok(isCaretPos(gPanel, line, column),
+ "The editor didn't jump to the correct line.");
+
+ EventUtils.sendKey("DOWN", gDebugger);
+ }
+
+ ok(isCaretPos(gPanel, expectedResults[0][3], expectedResults[0][4]),
+ "The editor didn't jump to the correct line again.");
+
+ deferred.resolve();
+ } else {
+ ok(false, "How did you get here? Go away, you.");
+ }
+ });
+
+ setText(gSearchBox, "@");
+ return deferred.promise;
+}
+
+function secondJsSearch() {
+ let deferred = promise.defer();
+
+ once(gDebugger, "popupshown").then(() => {
+ writeInfo();
+
+ is(gFilteredFunctions.selectedIndex, 0,
+ "An item should be selected in the filtered functions view (1).");
+ ok(gFilteredFunctions.selectedItem,
+ "An item should be selected in the filtered functions view (2).");
+
+ if (gSources.selectedItem.attachment.source.url.indexOf("-02.js") != -1) {
+ let s = " " + gDebugger.L10N.getStr("functionSearchSeparatorLabel") + " ";
+ let expectedResults = [
+ ["test2", "-02.js", "", 4, 5],
+ ["test3" + s + "test3_NAME", "-02.js", "", 8, 5],
+ ["test4_SAME_NAME", "-02.js", "", 11, 5],
+ ["x" + s + "X", "-02.js", "test.prototype", 14, 1],
+ ["y" + s + "Y", "-02.js", "test.prototype.sub", 16, 1],
+ ["z" + s + "Z", "-02.js", "test.prototype.sub.sub", 18, 1],
+ ["t", "-02.js", "test.prototype.sub.sub.sub", 20, 1],
+ ["x", "-02.js", "this", 20, 32],
+ ["y", "-02.js", "this", 20, 41],
+ ["z", "-02.js", "this", 20, 50]
+ ];
+
+ for (let [label, value, description, line, column] of expectedResults) {
+ let target = gFilteredFunctions.selectedItem.target;
+
+ if (label) {
+ is(target.querySelector(".results-panel-item-label").getAttribute("value"),
+ gDebugger.SourceUtils.trimUrlLength(label + "()"),
+ "The correct label (" + label + ") is currently selected.");
+ } else {
+ ok(!target.querySelector(".results-panel-item-label"),
+ "Shouldn't create empty label nodes.");
+ }
+ if (value) {
+ ok(target.querySelector(".results-panel-item-label-below").getAttribute("value").includes(value),
+ "The correct value (" + value + ") is attached.");
+ } else {
+ ok(!target.querySelector(".results-panel-item-label-below"),
+ "Shouldn't create empty label nodes.");
+ }
+ if (description) {
+ is(target.querySelector(".results-panel-item-label-before").getAttribute("value"), description,
+ "The correct description (" + description + ") is currently shown.");
+ } else {
+ ok(!target.querySelector(".results-panel-item-label-before"),
+ "Shouldn't create empty label nodes.");
+ }
+
+ ok(isCaretPos(gPanel, line, column),
+ "The editor didn't jump to the correct line.");
+
+ EventUtils.sendKey("DOWN", gDebugger);
+ }
+
+ ok(isCaretPos(gPanel, expectedResults[0][3], expectedResults[0][4]),
+ "The editor didn't jump to the correct line again.");
+
+ deferred.resolve();
+ } else {
+ ok(false, "How did you get here? Go away, you.");
+ }
+ });
+
+ setText(gSearchBox, "@");
+ return deferred.promise;
+}
+
+function thirdJsSearch() {
+ let deferred = promise.defer();
+
+ once(gDebugger, "popupshown").then(() => {
+ writeInfo();
+
+ is(gFilteredFunctions.selectedIndex, 0,
+ "An item should be selected in the filtered functions view (1).");
+ ok(gFilteredFunctions.selectedItem,
+ "An item should be selected in the filtered functions view (2).");
+
+ if (gSources.selectedItem.attachment.source.url.indexOf("-03.js") != -1) {
+ let s = " " + gDebugger.L10N.getStr("functionSearchSeparatorLabel") + " ";
+ let expectedResults = [
+ ["namedEventListener", "-03.js", "", 4, 43],
+ ["a" + s + "A", "-03.js", "bar", 10, 5],
+ ["b" + s + "B", "-03.js", "bar.alpha", 15, 5],
+ ["c" + s + "C", "-03.js", "bar.alpha.beta", 20, 5],
+ ["d" + s + "D", "-03.js", "this.theta", 25, 5],
+ ["fun", "-03.js", "", 29, 7],
+ ["foo", "-03.js", "", 29, 13],
+ ["bar", "-03.js", "", 29, 19],
+ ["t_foo", "-03.js", "this", 29, 25],
+ ["w_bar" + s + "baz", "-03.js", "window", 29, 38]
+ ];
+
+ for (let [label, value, description, line, column] of expectedResults) {
+ let target = gFilteredFunctions.selectedItem.target;
+
+ if (label) {
+ is(target.querySelector(".results-panel-item-label").getAttribute("value"),
+ gDebugger.SourceUtils.trimUrlLength(label + "()"),
+ "The correct label (" + label + ") is currently selected.");
+ } else {
+ ok(!target.querySelector(".results-panel-item-label"),
+ "Shouldn't create empty label nodes.");
+ }
+ if (value) {
+ ok(target.querySelector(".results-panel-item-label-below").getAttribute("value").includes(value),
+ "The correct value (" + value + ") is attached.");
+ } else {
+ ok(!target.querySelector(".results-panel-item-label-below"),
+ "Shouldn't create empty label nodes.");
+ }
+ if (description) {
+ is(target.querySelector(".results-panel-item-label-before").getAttribute("value"), description,
+ "The correct description (" + description + ") is currently shown.");
+ } else {
+ ok(!target.querySelector(".results-panel-item-label-before"),
+ "Shouldn't create empty label nodes.");
+ }
+
+ ok(isCaretPos(gPanel, line, column),
+ "The editor didn't jump to the correct line.");
+
+ EventUtils.sendKey("DOWN", gDebugger);
+ }
+
+ ok(isCaretPos(gPanel, expectedResults[0][3], expectedResults[0][4]),
+ "The editor didn't jump to the correct line again.");
+
+ deferred.resolve();
+ } else {
+ ok(false, "How did you get here? Go away, you.");
+ }
+ });
+
+ setText(gSearchBox, "@");
+ return deferred.promise;
+}
+
+function filterSearch() {
+ let deferred = promise.defer();
+
+ once(gDebugger, "popupshown").then(() => {
+ writeInfo();
+
+ is(gFilteredFunctions.selectedIndex, 0,
+ "An item should be selected in the filtered functions view (1).");
+ ok(gFilteredFunctions.selectedItem,
+ "An item should be selected in the filtered functions view (2).");
+
+ if (gSources.selectedItem.attachment.source.url.indexOf("-03.js") != -1) {
+ let s = " " + gDebugger.L10N.getStr("functionSearchSeparatorLabel") + " ";
+ let expectedResults = [
+ ["namedEventListener", "-03.js", "", 4, 43],
+ ["bar", "-03.js", "", 29, 19],
+ ["w_bar" + s + "baz", "-03.js", "window", 29, 38],
+ ["anonymousExpression", "-01.js", "test.prototype", 9, 3],
+ ["namedExpression" + s + "NAME", "-01.js", "test.prototype", 11, 3],
+ ["arrow", ".html", "", 20, 11],
+ ["bar2", ".html", "", 23, 18]
+ ];
+
+ for (let [label, value, description, line, column] of expectedResults) {
+ let target = gFilteredFunctions.selectedItem.target;
+
+ if (label) {
+ is(target.querySelector(".results-panel-item-label").getAttribute("value"),
+ gDebugger.SourceUtils.trimUrlLength(label + "()"),
+ "The correct label (" + label + ") is currently selected.");
+ } else {
+ ok(!target.querySelector(".results-panel-item-label"),
+ "Shouldn't create empty label nodes.");
+ }
+ if (value) {
+ ok(target.querySelector(".results-panel-item-label-below").getAttribute("value").includes(value),
+ "The correct value (" + value + ") is attached.");
+ } else {
+ ok(!target.querySelector(".results-panel-item-label-below"),
+ "Shouldn't create empty label nodes.");
+ }
+ if (description) {
+ is(target.querySelector(".results-panel-item-label-before").getAttribute("value"), description,
+ "The correct description (" + description + ") is currently shown.");
+ } else {
+ ok(!target.querySelector(".results-panel-item-label-before"),
+ "Shouldn't create empty label nodes.");
+ }
+
+ ok(isCaretPos(gPanel, line, column),
+ "The editor didn't jump to the correct line.");
+
+ EventUtils.sendKey("DOWN", gDebugger);
+ }
+
+ ok(isCaretPos(gPanel, expectedResults[0][3], expectedResults[0][4]),
+ "The editor didn't jump to the correct line again.");
+
+ deferred.resolve();
+ } else {
+ ok(false, "How did you get here? Go away, you.");
+ }
+ });
+
+ setText(gSearchBox, "@r");
+ return deferred.promise;
+}
+
+function bogusSearch() {
+ let deferred = promise.defer();
+
+ once(gDebugger, "popuphidden").then(() => {
+ ok(true, "Popup was successfully hidden after no matches were found.");
+ deferred.resolve();
+ });
+
+ setText(gSearchBox, "@bogus");
+ return deferred.promise;
+}
+
+function incrementalSearch() {
+ let deferred = promise.defer();
+
+ once(gDebugger, "popupshown").then(() => {
+ ok(true, "Popup was successfully shown after some matches were found.");
+ deferred.resolve();
+ });
+
+ setText(gSearchBox, "@NAME");
+ return deferred.promise;
+}
+
+function emptySearch() {
+ let deferred = promise.defer();
+
+ once(gDebugger, "popuphidden").then(() => {
+ ok(true, "Popup was successfully hidden when nothing was searched.");
+ deferred.resolve();
+ });
+
+ clearText(gSearchBox);
+ return deferred.promise;
+}
+
+function showSource(aLabel) {
+ let deferred = promise.defer();
+
+ waitForSourceShown(gPanel, aLabel).then(deferred.resolve);
+ gSources.selectedItem = e => e.attachment.label == aLabel;
+
+ return deferred.promise;
+}
+
+function saveSearch() {
+ let finished = once(gDebugger, "popuphidden");
+
+ // Either by pressing RETURN or clicking on an item in the popup,
+ // the popup should hide and the item should become selected.
+ let random = Math.random();
+ if (random >= 0.33) {
+ EventUtils.sendKey("RETURN", gDebugger);
+ } else if (random >= 0.66) {
+ EventUtils.sendKey("RETURN", gDebugger);
+ } else {
+ EventUtils.sendMouseEvent({ type: "click" },
+ gFilteredFunctions.selectedItem.target,
+ gDebugger);
+ }
+
+ return finished;
+}
+
+function writeInfo() {
+ info("Current source url:\n" + getSelectedSourceURL(gSources));
+ info("Debugger editor text:\n" + gEditor.getText());
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gEditor = null;
+ gSources = null;
+ gSearchBox = null;
+ gFilteredFunctions = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_searchbox-help-popup-01.js b/devtools/client/debugger/test/mochitest/browser_dbg_searchbox-help-popup-01.js
new file mode 100644
index 000000000..6276567c2
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_searchbox-help-popup-01.js
@@ -0,0 +1,64 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure that the searchbox popup is displayed when focusing the searchbox,
+ * and hidden when the user starts typing.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
+
+var gTab, gPanel, gDebugger;
+var gSearchBox, gSearchBoxPanel;
+
+function test() {
+ let options = {
+ source: EXAMPLE_URL + "code_script-switching-01.js",
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gSearchBox = gDebugger.DebuggerView.Filtering._searchbox;
+ gSearchBoxPanel = gDebugger.DebuggerView.Filtering._searchboxHelpPanel;
+
+ waitForSourceAndCaretAndScopes(gPanel, "-02.js", 1)
+ .then(showPopup)
+ .then(hidePopup)
+ .then(() => resumeDebuggerThenCloseAndFinish(gPanel))
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+
+ callInTab(gTab, "firstCall");
+ });
+}
+
+function showPopup() {
+ is(gSearchBoxPanel.state, "closed",
+ "The search box panel shouldn't be visible yet.");
+
+ let finished = once(gSearchBoxPanel, "popupshown");
+ EventUtils.sendMouseEvent({ type: "click" }, gSearchBox, gDebugger);
+ return finished;
+}
+
+function hidePopup() {
+ is(gSearchBoxPanel.state, "open",
+ "The search box panel should be visible after searching started.");
+
+ let finished = once(gSearchBoxPanel, "popuphidden");
+ setText(gSearchBox, "#");
+ return finished;
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gSearchBox = null;
+ gSearchBoxPanel = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_searchbox-help-popup-02.js b/devtools/client/debugger/test/mochitest/browser_dbg_searchbox-help-popup-02.js
new file mode 100644
index 000000000..277dd0bc4
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_searchbox-help-popup-02.js
@@ -0,0 +1,90 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure that the searchbox popup isn't displayed when there's some text
+ * already present.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
+
+var gTab, gPanel, gDebugger;
+var gEditor, gSearchBox, gSearchBoxPanel;
+
+function test() {
+ let options = {
+ source: EXAMPLE_URL + "code_script-switching-01.js",
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gEditor = gDebugger.DebuggerView.editor;
+ gSearchBox = gDebugger.DebuggerView.Filtering._searchbox;
+ gSearchBoxPanel = gDebugger.DebuggerView.Filtering._searchboxHelpPanel;
+
+ once(gSearchBoxPanel, "popupshown").then(() => {
+ ok(false, "Damn it, this shouldn't have happened.");
+ });
+
+ waitForSourceAndCaretAndScopes(gPanel, "-02.js", 1)
+ .then(tryShowPopup)
+ .then(focusEditor)
+ .then(testFocusLost)
+ .then(() => resumeDebuggerThenCloseAndFinish(gPanel))
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+
+ callInTab(gTab, "firstCall");
+ });
+}
+
+function tryShowPopup() {
+ setText(gSearchBox, "#call()");
+ ok(isCaretPos(gPanel, 4, 22),
+ "The editor caret position appears to be correct.");
+ ok(isEditorSel(gPanel, [125, 131]),
+ "The editor selection appears to be correct.");
+ is(gEditor.getSelection(), "Call()",
+ "The editor selected text appears to be correct.");
+
+ is(gSearchBoxPanel.state, "closed",
+ "The search box panel shouldn't be visible yet.");
+
+ EventUtils.sendMouseEvent({ type: "click" }, gSearchBox, gDebugger);
+}
+
+function focusEditor() {
+ let deferred = promise.defer();
+
+ // Focusing the editor takes a tick to update the caret and selection.
+ gEditor.focus();
+ executeSoon(deferred.resolve);
+
+ return deferred.promise;
+}
+
+function testFocusLost() {
+ ok(isCaretPos(gPanel, 4, 22),
+ "The editor caret position appears to be correct after gaining focus.");
+ ok(isEditorSel(gPanel, [125, 131]),
+ "The editor selection appears to be correct after gaining focus.");
+ is(gEditor.getSelection(), "Call()",
+ "The editor selected text appears to be correct after gaining focus.");
+
+ is(gSearchBoxPanel.state, "closed",
+ "The search box panel should still not be visible.");
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gEditor = null;
+ gSearchBox = null;
+ gSearchBoxPanel = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_searchbox-parse.js b/devtools/client/debugger/test/mochitest/browser_dbg_searchbox-parse.js
new file mode 100644
index 000000000..2bb8e4150
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_searchbox-parse.js
@@ -0,0 +1,126 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests that text entered in the debugger's searchbox is properly parsed.
+ */
+
+function test() {
+ initDebugger().then(([aTab,, aPanel]) => {
+ let filterView = aPanel.panelWin.DebuggerView.Filtering;
+ let searchbox = aPanel.panelWin.DebuggerView.Filtering._searchbox;
+
+ setText(searchbox, "");
+ is(filterView.searchData.toSource(), '["", [""]]',
+ "The searchbox data wasn't parsed correctly (1).");
+
+ setText(searchbox, "#token");
+ is(filterView.searchData.toSource(), '["#", ["", "token"]]',
+ "The searchbox data wasn't parsed correctly (2).");
+
+ setText(searchbox, ":42");
+ is(filterView.searchData.toSource(), '[":", ["", 42]]',
+ "The searchbox data wasn't parsed correctly (3).");
+
+ setText(searchbox, "#token:42");
+ is(filterView.searchData.toSource(), '["#", ["", "token:42"]]',
+ "The searchbox data wasn't parsed correctly (4).");
+
+ setText(searchbox, ":42#token");
+ is(filterView.searchData.toSource(), '["#", [":42", "token"]]',
+ "The searchbox data wasn't parsed correctly (5).");
+
+ setText(searchbox, "#token:42#token:42");
+ is(filterView.searchData.toSource(), '["#", ["#token:42", "token:42"]]',
+ "The searchbox data wasn't parsed correctly (6).");
+
+ setText(searchbox, ":42#token:42#token");
+ is(filterView.searchData.toSource(), '["#", [":42#token:42", "token"]]',
+ "The searchbox data wasn't parsed correctly (7).");
+
+
+ setText(searchbox, "file");
+ is(filterView.searchData.toSource(), '["", ["file"]]',
+ "The searchbox data wasn't parsed correctly (8).");
+
+ setText(searchbox, "file#token");
+ is(filterView.searchData.toSource(), '["#", ["file", "token"]]',
+ "The searchbox data wasn't parsed correctly (9).");
+
+ setText(searchbox, "file:42");
+ is(filterView.searchData.toSource(), '[":", ["file", 42]]',
+ "The searchbox data wasn't parsed correctly (10).");
+
+ setText(searchbox, "file#token:42");
+ is(filterView.searchData.toSource(), '["#", ["file", "token:42"]]',
+ "The searchbox data wasn't parsed correctly (11).");
+
+ setText(searchbox, "file:42#token");
+ is(filterView.searchData.toSource(), '["#", ["file:42", "token"]]',
+ "The searchbox data wasn't parsed correctly (12).");
+
+ setText(searchbox, "file#token:42#token:42");
+ is(filterView.searchData.toSource(), '["#", ["file#token:42", "token:42"]]',
+ "The searchbox data wasn't parsed correctly (13).");
+
+ setText(searchbox, "file:42#token:42#token");
+ is(filterView.searchData.toSource(), '["#", ["file:42#token:42", "token"]]',
+ "The searchbox data wasn't parsed correctly (14).");
+
+
+ setText(searchbox, "!token");
+ is(filterView.searchData.toSource(), '["!", ["token"]]',
+ "The searchbox data wasn't parsed correctly (15).");
+
+ setText(searchbox, "!token#global");
+ is(filterView.searchData.toSource(), '["!", ["token#global"]]',
+ "The searchbox data wasn't parsed correctly (16).");
+
+ setText(searchbox, "!token#global:42");
+ is(filterView.searchData.toSource(), '["!", ["token#global:42"]]',
+ "The searchbox data wasn't parsed correctly (17).");
+
+ setText(searchbox, "!token:42#global");
+ is(filterView.searchData.toSource(), '["!", ["token:42#global"]]',
+ "The searchbox data wasn't parsed correctly (18).");
+
+
+ setText(searchbox, "@token");
+ is(filterView.searchData.toSource(), '["@", ["token"]]',
+ "The searchbox data wasn't parsed correctly (19).");
+
+ setText(searchbox, "@token#global");
+ is(filterView.searchData.toSource(), '["@", ["token#global"]]',
+ "The searchbox data wasn't parsed correctly (20).");
+
+ setText(searchbox, "@token#global:42");
+ is(filterView.searchData.toSource(), '["@", ["token#global:42"]]',
+ "The searchbox data wasn't parsed correctly (21).");
+
+ setText(searchbox, "@token:42#global");
+ is(filterView.searchData.toSource(), '["@", ["token:42#global"]]',
+ "The searchbox data wasn't parsed correctly (22).");
+
+
+ setText(searchbox, "*token");
+ is(filterView.searchData.toSource(), '["*", ["token"]]',
+ "The searchbox data wasn't parsed correctly (23).");
+
+ setText(searchbox, "*token#global");
+ is(filterView.searchData.toSource(), '["*", ["token#global"]]',
+ "The searchbox data wasn't parsed correctly (24).");
+
+ setText(searchbox, "*token#global:42");
+ is(filterView.searchData.toSource(), '["*", ["token#global:42"]]',
+ "The searchbox data wasn't parsed correctly (25).");
+
+ setText(searchbox, "*token:42#global");
+ is(filterView.searchData.toSource(), '["*", ["token:42#global"]]',
+ "The searchbox data wasn't parsed correctly (26).");
+
+
+ closeDebuggerAndFinish(aPanel);
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_server-conditional-bp-01.js b/devtools/client/debugger/test/mochitest/browser_dbg_server-conditional-bp-01.js
new file mode 100644
index 000000000..b318d8798
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_server-conditional-bp-01.js
@@ -0,0 +1,218 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Test adding conditional breakpoints (with server-side support)
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_conditional-breakpoints.html";
+
+function test() {
+ // Linux debug test slaves are a bit slow at this test sometimes.
+ requestLongerTimeout(2);
+
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ const gTab = aTab;
+ const gPanel = aPanel;
+ const gDebugger = gPanel.panelWin;
+ const gEditor = gDebugger.DebuggerView.editor;
+ const gSources = gDebugger.DebuggerView.Sources;
+ const queries = gDebugger.require("./content/queries");
+ const constants = gDebugger.require("./content/constants");
+ const actions = bindActionCreators(gPanel);
+ const getState = gDebugger.DebuggerController.getState;
+
+ const addBreakpoints = Task.async(function* () {
+ yield actions.addBreakpoint({ actor: gSources.selectedValue, line: 18 },
+ "undefined");
+ yield actions.addBreakpoint({ actor: gSources.selectedValue, line: 19 },
+ "null");
+ yield actions.addBreakpoint({ actor: gSources.selectedValue, line: 20 },
+ "42");
+ yield actions.addBreakpoint({ actor: gSources.selectedValue, line: 21 },
+ "true");
+ yield actions.addBreakpoint({ actor: gSources.selectedValue, line: 22 },
+ "'nasu'");
+ yield actions.addBreakpoint({ actor: gSources.selectedValue, line: 23 },
+ "/regexp/");
+ yield actions.addBreakpoint({ actor: gSources.selectedValue, line: 24 },
+ "({})");
+ yield actions.addBreakpoint({ actor: gSources.selectedValue, line: 25 },
+ "(function() {})");
+ yield actions.addBreakpoint({ actor: gSources.selectedValue, line: 26 },
+ "(function() { return false; })()");
+ yield actions.addBreakpoint({ actor: gSources.selectedValue, line: 27 },
+ "a");
+ yield actions.addBreakpoint({ actor: gSources.selectedValue, line: 28 },
+ "a !== undefined");
+ yield actions.addBreakpoint({ actor: gSources.selectedValue, line: 29 },
+ "b");
+ yield actions.addBreakpoint({ actor: gSources.selectedValue, line: 30 },
+ "a !== null");
+ });
+
+ function resumeAndTestBreakpoint(line) {
+ let finished = waitForCaretUpdated(gPanel, line).then(() => testBreakpoint(line));
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ gDebugger.document.getElementById("resume"),
+ gDebugger);
+
+ return finished;
+ }
+
+ function resumeAndTestNoBreakpoint() {
+ let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.AFTER_FRAMES_CLEARED).then(() => {
+ is(gSources.itemCount, 1,
+ "Found the expected number of sources.");
+ is(gEditor.getText().indexOf("ermahgerd"), 253,
+ "The correct source was loaded initially.");
+ is(gSources.selectedValue, gSources.values[0],
+ "The correct source is selected.");
+
+ ok(gSources.selectedItem,
+ "There should be a selected source in the sources pane.");
+ ok(!gSources._selectedBreakpoint,
+ "There should be no selected breakpoint in the sources pane.");
+ is(gSources._conditionalPopupVisible, false,
+ "The breakpoint conditional expression popup should not be shown.");
+
+ is(gDebugger.document.querySelectorAll(".dbg-stackframe").length, 0,
+ "There should be no visible stackframes.");
+ is(gDebugger.document.querySelectorAll(".dbg-breakpoint").length, 13,
+ "There should be thirteen visible breakpoints.");
+ });
+
+ gDebugger.gThreadClient.resume();
+
+ return finished;
+ }
+
+ function testBreakpoint(line, highlightBreakpoint) {
+ // Highlight the breakpoint only if required.
+ if (highlightBreakpoint) {
+ let finished = waitForCaretUpdated(gPanel, line).then(() => testBreakpoint(line));
+ gSources.highlightBreakpoint({ actor: gSources.selectedValue, line: line });
+ return finished;
+ }
+
+ let selectedActor = gSources.selectedValue;
+ let selectedBreakpoint = gSources._selectedBreakpoint;
+ let selectedBreakpointItem = gSources._getBreakpoint(selectedBreakpoint);
+
+ ok(selectedActor,
+ "There should be a selected item in the sources pane.");
+ ok(selectedBreakpoint,
+ "There should be a selected breakpoint in the sources pane.");
+
+ let source = gSources.selectedItem.attachment.source;
+ let bp = queries.getBreakpoint(getState(), selectedBreakpoint.location);
+
+ ok(bp, "The selected breakpoint exists");
+ is(bp.location.actor, source.actor,
+ "The breakpoint on line " + line + " wasn't added on the correct source.");
+ is(bp.location.line, line,
+ "The breakpoint on line " + line + " wasn't found.");
+ is(!!bp.disabled, false,
+ "The breakpoint on line " + line + " should be enabled.");
+ is(!!selectedBreakpointItem.attachment.openPopup, false,
+ "The breakpoint on line " + line + " should not have opened a popup.");
+ is(gSources._conditionalPopupVisible, false,
+ "The breakpoint conditional expression popup should not have been shown.");
+ isnot(bp.condition, undefined,
+ "The breakpoint on line " + line + " should have a conditional expression.");
+ ok(isCaretPos(gPanel, line),
+ "The editor caret position is not properly set.");
+ }
+
+ const testAfterReload = Task.async(function* () {
+ let selectedActor = gSources.selectedValue;
+ let selectedBreakpoint = gSources._selectedBreakpoint;
+
+ ok(selectedActor,
+ "There should be a selected item in the sources pane after reload.");
+ ok(!selectedBreakpoint,
+ "There should be no selected breakpoint in the sources pane after reload.");
+
+ yield testBreakpoint(18, true);
+ yield testBreakpoint(19, true);
+ yield testBreakpoint(20, true);
+ yield testBreakpoint(21, true);
+ yield testBreakpoint(22, true);
+ yield testBreakpoint(23, true);
+ yield testBreakpoint(24, true);
+ yield testBreakpoint(25, true);
+ yield testBreakpoint(26, true);
+ yield testBreakpoint(27, true);
+ yield testBreakpoint(28, true);
+ yield testBreakpoint(29, true);
+ yield testBreakpoint(30, true);
+
+ is(gSources.itemCount, 1,
+ "Found the expected number of sources.");
+ is(gEditor.getText().indexOf("ermahgerd"), 253,
+ "The correct source was loaded again.");
+ is(gSources.selectedValue, gSources.values[0],
+ "The correct source is selected.");
+
+ ok(gSources.selectedItem,
+ "There should be a selected source in the sources pane.");
+ ok(gSources._selectedBreakpoint,
+ "There should be a selected breakpoint in the sources pane.");
+ is(gSources._conditionalPopupVisible, false,
+ "The breakpoint conditional expression popup should not be shown.");
+ });
+
+ Task.spawn(function* () {
+ let onCaretUpdated = waitForCaretAndScopes(gPanel, 17);
+ callInTab(gTab, "ermahgerd");
+ yield onCaretUpdated;
+
+ yield addBreakpoints();
+
+ is(gDebugger.gThreadClient.state, "paused",
+ "Should only be getting stack frames while paused.");
+ is(queries.getSourceCount(getState()), 1,
+ "Found the expected number of sources.");
+ is(gEditor.getText().indexOf("ermahgerd"), 253,
+ "The correct source was loaded initially.");
+ is(gSources.selectedValue, gSources.values[0],
+ "The correct source is selected.");
+ is(queries.getBreakpoints(getState()).length, 13,
+ "13 breakpoints currently added.");
+
+ yield resumeAndTestBreakpoint(20);
+ yield resumeAndTestBreakpoint(21);
+ yield resumeAndTestBreakpoint(22);
+ yield resumeAndTestBreakpoint(23);
+ yield resumeAndTestBreakpoint(24);
+ yield resumeAndTestBreakpoint(25);
+ yield resumeAndTestBreakpoint(27);
+ yield resumeAndTestBreakpoint(28);
+ yield resumeAndTestBreakpoint(29);
+ yield resumeAndTestBreakpoint(30);
+ yield resumeAndTestNoBreakpoint();
+
+ let sourceShown = waitForSourceShown(gPanel, ".html");
+ reload(gPanel),
+ yield sourceShown;
+ testAfterReload();
+
+ // When a breakpoint is highlighted, the conditional expression
+ // popup opens, and then closes, and when it closes it sends the
+ // expression to the server which pauses the server. Make sure
+ // we wait if there is a pending request.
+ if (gDebugger.gThreadClient.state === "paused") {
+ yield waitForThreadEvents(gPanel, "resumed");
+ }
+
+ closeDebuggerAndFinish(gPanel);
+ });
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_server-conditional-bp-02.js b/devtools/client/debugger/test/mochitest/browser_dbg_server-conditional-bp-02.js
new file mode 100644
index 000000000..31f2af36c
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_server-conditional-bp-02.js
@@ -0,0 +1,214 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Test adding and modifying conditional breakpoints (with server-side support)
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_conditional-breakpoints.html";
+
+function test() {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ const gTab = aTab;
+ const gPanel = aPanel;
+ const gDebugger = gPanel.panelWin;
+ const gEditor = gDebugger.DebuggerView.editor;
+ const gSources = gDebugger.DebuggerView.Sources;
+ const queries = gDebugger.require("./content/queries");
+ const constants = gDebugger.require("./content/constants");
+ const actions = bindActionCreators(gPanel);
+ const getState = gDebugger.DebuggerController.getState;
+ const CONDITIONAL_POPUP_SHOWN = gDebugger.EVENTS.CONDITIONAL_BREAKPOINT_POPUP_SHOWN;
+
+ function addBreakpoint1() {
+ return actions.addBreakpoint({ actor: gSources.selectedValue, line: 18 });
+ }
+
+ function addBreakpoint2() {
+ let finished = waitForDispatch(gPanel, constants.ADD_BREAKPOINT);
+ setCaretPosition(19);
+ gSources._onCmdAddBreakpoint();
+ return finished;
+ }
+
+ function modBreakpoint2() {
+ setCaretPosition(19);
+
+ let popupShown = waitForDebuggerEvents(gPanel, CONDITIONAL_POPUP_SHOWN);
+ gSources._onCmdAddConditionalBreakpoint();
+ return popupShown;
+ }
+
+ function* addBreakpoint3() {
+ let finished = waitForDispatch(gPanel, constants.ADD_BREAKPOINT);
+ let popupShown = waitForDebuggerEvents(gPanel, CONDITIONAL_POPUP_SHOWN);
+
+ setCaretPosition(20);
+ gSources._onCmdAddConditionalBreakpoint();
+ yield finished;
+ yield popupShown;
+ }
+
+ function* modBreakpoint3() {
+ setCaretPosition(20);
+
+ let popupShown = waitForDebuggerEvents(gPanel, CONDITIONAL_POPUP_SHOWN);
+ gSources._onCmdAddConditionalBreakpoint();
+ yield popupShown;
+
+ typeText(gSources._cbTextbox, "bamboocha");
+
+ let finished = waitForDispatch(gPanel, constants.SET_BREAKPOINT_CONDITION);
+ EventUtils.sendKey("RETURN", gDebugger);
+ yield finished;
+ }
+
+ function addBreakpoint4() {
+ let finished = waitForDispatch(gPanel, constants.ADD_BREAKPOINT);
+ setCaretPosition(21);
+ gSources._onCmdAddBreakpoint();
+ return finished;
+ }
+
+ function delBreakpoint4() {
+ let finished = waitForDispatch(gPanel, constants.REMOVE_BREAKPOINT);
+ setCaretPosition(21);
+ gSources._onCmdAddBreakpoint();
+ return finished;
+ }
+
+ function testBreakpoint(aLine, aPopupVisible, aConditionalExpression) {
+ const source = queries.getSelectedSource(getState());
+ ok(source,
+ "There should be a selected item in the sources pane.");
+
+ const bp = queries.getBreakpoint(getState(), {
+ actor: source.actor,
+ line: aLine
+ });
+ const bpItem = gSources._getBreakpoint(bp);
+ ok(bp, "There should be a breakpoint.");
+ ok(bpItem, "There should be a breakpoint in the sources pane.");
+
+ is(bp.location.actor, source.actor,
+ "The breakpoint on line " + aLine + " wasn't added on the correct source.");
+ is(bp.location.line, aLine,
+ "The breakpoint on line " + aLine + " wasn't found.");
+ is(!!bp.disabled, false,
+ "The breakpoint on line " + aLine + " should be enabled.");
+ is(gSources._conditionalPopupVisible, aPopupVisible,
+ "The breakpoint on line " + aLine + " should have a correct popup state (2).");
+ is(bp.condition, aConditionalExpression,
+ "The breakpoint on line " + aLine + " should have a correct conditional expression.");
+ }
+
+ function testNoBreakpoint(aLine) {
+ let selectedActor = gSources.selectedValue;
+ let selectedBreakpoint = gSources._selectedBreakpoint;
+
+ ok(selectedActor,
+ "There should be a selected item in the sources pane for line " + aLine + ".");
+ ok(!selectedBreakpoint,
+ "There should be no selected brekapoint in the sources pane for line " + aLine + ".");
+
+ ok(isCaretPos(gPanel, aLine),
+ "The editor caret position is not properly set.");
+ }
+
+ function setCaretPosition(aLine) {
+ gEditor.setCursor({ line: aLine - 1, ch: 0 });
+ }
+
+ function clickOnBreakpoint(aIndex) {
+ EventUtils.sendMouseEvent({ type: "click" },
+ gDebugger.document.querySelectorAll(".dbg-breakpoint")[aIndex],
+ gDebugger);
+ }
+
+ function waitForConditionUpdate() {
+ // This will close the popup and send another request to update
+ // the condition
+ gSources._hideConditionalPopup();
+ return waitForDispatch(gPanel, constants.SET_BREAKPOINT_CONDITION);
+ }
+
+ Task.spawn(function* () {
+ let onCaretUpdated = waitForCaretAndScopes(gPanel, 17);
+ callInTab(gTab, "ermahgerd");
+ yield onCaretUpdated;
+
+ is(gDebugger.gThreadClient.state, "paused",
+ "Should only be getting stack frames while paused.");
+ is(queries.getSourceCount(getState()), 1,
+ "Found the expected number of sources.");
+ is(gEditor.getText().indexOf("ermahgerd"), 253,
+ "The correct source was loaded initially.");
+ is(gSources.selectedValue, gSources.values[0],
+ "The correct source is selected.");
+
+ is(queries.getBreakpoints(getState()).length, 0,
+ "No breakpoints currently added.");
+
+ yield addBreakpoint1();
+ testBreakpoint(18, false, undefined);
+
+ yield addBreakpoint2();
+ testBreakpoint(19, false, undefined);
+ yield modBreakpoint2();
+ testBreakpoint(19, true, undefined);
+ yield waitForConditionUpdate();
+ yield addBreakpoint3();
+ testBreakpoint(20, true, "");
+ yield waitForConditionUpdate();
+ yield modBreakpoint3();
+ testBreakpoint(20, false, "bamboocha");
+ yield addBreakpoint4();
+ testBreakpoint(21, false, undefined);
+ yield delBreakpoint4();
+
+ setCaretPosition(18);
+ is(gSources._selectedBreakpoint.location.line, 18,
+ "The selected breakpoint is line 18");
+ yield testBreakpoint(18, false, undefined);
+
+ setCaretPosition(19);
+ is(gSources._selectedBreakpoint.location.line, 19,
+ "The selected breakpoint is line 19");
+ yield testBreakpoint(19, false, "");
+
+ setCaretPosition(20);
+ is(gSources._selectedBreakpoint.location.line, 20,
+ "The selected breakpoint is line 20");
+ yield testBreakpoint(20, false, "bamboocha");
+
+ setCaretPosition(17);
+ yield testNoBreakpoint(17);
+
+ setCaretPosition(21);
+ yield testNoBreakpoint(21);
+
+ clickOnBreakpoint(0);
+ is(gSources._selectedBreakpoint.location.line, 18,
+ "The selected breakpoint is line 18");
+ yield testBreakpoint(18, false, undefined);
+
+ clickOnBreakpoint(1);
+ is(gSources._selectedBreakpoint.location.line, 19,
+ "The selected breakpoint is line 19");
+ yield testBreakpoint(19, false, "");
+
+ clickOnBreakpoint(2);
+ is(gSources._selectedBreakpoint.location.line, 20,
+ "The selected breakpoint is line 20");
+ testBreakpoint(20, true, "bamboocha");
+
+ resumeDebuggerThenCloseAndFinish(gPanel);
+ });
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_server-conditional-bp-03.js b/devtools/client/debugger/test/mochitest/browser_dbg_server-conditional-bp-03.js
new file mode 100644
index 000000000..b83c96e39
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_server-conditional-bp-03.js
@@ -0,0 +1,73 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Test that conditional breakpoints survive disabled breakpoints
+ * (with server-side support)
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_conditional-breakpoints.html";
+
+function test() {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ const gTab = aTab;
+ const gPanel = aPanel;
+ const gDebugger = gPanel.panelWin;
+ const gSources = gDebugger.DebuggerView.Sources;
+ const queries = gDebugger.require("./content/queries");
+ const constants = gDebugger.require("./content/constants");
+ const actions = bindActionCreators(gPanel);
+ const getState = gDebugger.DebuggerController.getState;
+
+ function waitForConditionUpdate() {
+ // This will close the popup and send another request to update
+ // the condition
+ gSources._hideConditionalPopup();
+ return waitForDispatch(gPanel, constants.SET_BREAKPOINT_CONDITION);
+ }
+
+ Task.spawn(function* () {
+ let onCaretUpdated = waitForCaretAndScopes(gPanel, 17);
+ callInTab(gTab, "ermahgerd");
+ yield onCaretUpdated;
+
+ const location = { actor: gSources.selectedValue, line: 18 };
+
+ yield actions.addBreakpoint(location, "hello");
+ yield actions.disableBreakpoint(location);
+ yield actions.addBreakpoint(location);
+
+ const bp = queries.getBreakpoint(getState(), location);
+ is(bp.condition, "hello", "The conditional expression is correct.");
+
+ let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.CONDITIONAL_BREAKPOINT_POPUP_SHOWN);
+ EventUtils.sendMouseEvent({ type: "click" },
+ gDebugger.document.querySelector(".dbg-breakpoint"),
+ gDebugger);
+ yield finished;
+
+ const textbox = gDebugger.document.getElementById("conditional-breakpoint-panel-textbox");
+ is(textbox.value, "hello", "The expression is correct (2).");
+
+ yield waitForConditionUpdate();
+ yield actions.disableBreakpoint(location);
+ yield actions.setBreakpointCondition(location, "foo");
+ yield actions.addBreakpoint(location);
+
+ finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.CONDITIONAL_BREAKPOINT_POPUP_SHOWN);
+ EventUtils.sendMouseEvent({ type: "click" },
+ gDebugger.document.querySelector(".dbg-breakpoint"),
+ gDebugger);
+ yield finished;
+ is(textbox.value, "foo", "The expression is correct (3).");
+
+ yield resumeDebuggerThenCloseAndFinish(gPanel);
+ });
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_server-conditional-bp-04.js b/devtools/client/debugger/test/mochitest/browser_dbg_server-conditional-bp-04.js
new file mode 100644
index 000000000..2f35c4d60
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_server-conditional-bp-04.js
@@ -0,0 +1,46 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure that conditional breakpoints with undefined expressions
+ * maintain their conditions when re-enabling them (with
+ * server-side support)
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_conditional-breakpoints.html";
+
+function test() {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ const gTab = aTab;
+ const gPanel = aPanel;
+ const gDebugger = gPanel.panelWin;
+ const gSources = gDebugger.DebuggerView.Sources;
+ const queries = gDebugger.require("./content/queries");
+ const constants = gDebugger.require("./content/constants");
+ const actions = bindActionCreators(gPanel);
+ const getState = gDebugger.DebuggerController.getState;
+
+ Task.spawn(function* () {
+ let onCaretUpdated = waitForCaretAndScopes(gPanel, 17);
+ callInTab(gTab, "ermahgerd");
+ yield onCaretUpdated;
+
+ const location = { actor: gSources.selectedValue, line: 18 };
+
+ yield actions.addBreakpoint(location, "");
+ yield actions.disableBreakpoint(location);
+ yield actions.addBreakpoint(location);
+
+ const bp = queries.getBreakpoint(getState(), location);
+ is(bp.condition, "", "The conditional expression is correct.");
+
+ resumeDebuggerThenCloseAndFinish(gPanel);
+ });
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_server-conditional-bp-05.js b/devtools/client/debugger/test/mochitest/browser_dbg_server-conditional-bp-05.js
new file mode 100644
index 000000000..21607d8fd
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_server-conditional-bp-05.js
@@ -0,0 +1,134 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Test conditional breakpoints throwing exceptions
+ * with server support
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_conditional-breakpoints.html";
+
+function test() {
+ const options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ const gTab = aTab;
+ const gPanel = aPanel;
+ const gDebugger = gPanel.panelWin;
+ const gEditor = gDebugger.DebuggerView.editor;
+ const gSources = gDebugger.DebuggerView.Sources;
+ const queries = gDebugger.require("./content/queries");
+ const constants = gDebugger.require("./content/constants");
+ const actions = bindActionCreators(gPanel);
+ const getState = gDebugger.DebuggerController.getState;
+
+ function resumeAndTestBreakpoint(line) {
+ let finished = waitForCaretUpdated(gPanel, line).then(() => testBreakpoint(line));
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ gDebugger.document.getElementById("resume"),
+ gDebugger);
+
+ return finished;
+ }
+
+ function resumeAndTestNoBreakpoint() {
+ let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.AFTER_FRAMES_CLEARED).then(() => {
+ is(gSources.itemCount, 1,
+ "Found the expected number of sources.");
+ is(gEditor.getText().indexOf("ermahgerd"), 253,
+ "The correct source was loaded initially.");
+ is(gSources.selectedValue, gSources.values[0],
+ "The correct source is selected.");
+
+ ok(gSources.selectedItem,
+ "There should be a selected source in the sources pane.");
+ ok(!gSources._selectedBreakpoint,
+ "There should be no selected breakpoint in the sources pane.");
+ is(gSources._conditionalPopupVisible, false,
+ "The breakpoint conditional expression popup should not be shown.");
+
+ is(gDebugger.document.querySelectorAll(".dbg-stackframe").length, 0,
+ "There should be no visible stackframes.");
+ is(gDebugger.document.querySelectorAll(".dbg-breakpoint").length, 6,
+ "There should be thirteen visible breakpoints.");
+ });
+
+ gDebugger.gThreadClient.resume();
+
+ return finished;
+ }
+
+ function testBreakpoint(line, highlightBreakpoint) {
+ // Highlight the breakpoint only if required.
+ if (highlightBreakpoint) {
+ let finished = waitForCaretUpdated(gPanel, line).then(() => testBreakpoint(line));
+ gSources.highlightBreakpoint({ actor: gSources.selectedValue, line: line });
+ return finished;
+ }
+
+ let selectedActor = gSources.selectedValue;
+ let selectedBreakpoint = gSources._selectedBreakpoint;
+ let selectedBreakpointItem = gSources._getBreakpoint(selectedBreakpoint);
+ let source = queries.getSource(getState(), selectedActor);
+
+ ok(selectedActor,
+ "There should be a selected item in the sources pane.");
+ ok(selectedBreakpoint,
+ "There should be a selected breakpoint.");
+ ok(selectedBreakpointItem,
+ "There should be a selected breakpoint item in the sources pane.");
+
+ is(selectedBreakpoint.location.actor, source.actor,
+ "The breakpoint on line " + line + " wasn't added on the correct source.");
+ is(selectedBreakpoint.location.line, line,
+ "The breakpoint on line " + line + " wasn't found.");
+ is(!!selectedBreakpoint.location.disabled, false,
+ "The breakpoint on line " + line + " should be enabled.");
+ is(gSources._conditionalPopupVisible, false,
+ "The breakpoint conditional expression popup should not have been shown.");
+
+ isnot(selectedBreakpoint.condition, undefined,
+ "The breakpoint on line " + line + " should have a conditional expression.");
+
+ ok(isCaretPos(gPanel, line),
+ "The editor caret position is not properly set.");
+ }
+
+ Task.spawn(function* () {
+ let onCaretUpdated = waitForCaretAndScopes(gPanel, 17);
+ callInTab(gTab, "ermahgerd");
+ yield onCaretUpdated;
+
+ yield actions.addBreakpoint(
+ { actor: gSources.selectedValue, line: 18 }, " 1a"
+ );
+ yield actions.addBreakpoint(
+ { actor: gSources.selectedValue, line: 19 }, "new Error()"
+ );
+ yield actions.addBreakpoint(
+ { actor: gSources.selectedValue, line: 20 }, "true"
+ );
+ yield actions.addBreakpoint(
+ { actor: gSources.selectedValue, line: 21 }, "false"
+ );
+ yield actions.addBreakpoint(
+ { actor: gSources.selectedValue, line: 22 }, "0"
+ );
+ yield actions.addBreakpoint(
+ { actor: gSources.selectedValue, line: 23 }, "randomVar"
+ );
+
+ yield resumeAndTestBreakpoint(18);
+ yield resumeAndTestBreakpoint(19);
+ yield resumeAndTestBreakpoint(20);
+ yield resumeAndTestBreakpoint(23);
+ yield resumeAndTestNoBreakpoint();
+
+ closeDebuggerAndFinish(gPanel);
+ });
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_source-maps-01.js b/devtools/client/debugger/test/mochitest/browser_dbg_source-maps-01.js
new file mode 100644
index 000000000..c0681fec6
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_source-maps-01.js
@@ -0,0 +1,170 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Test that we can set breakpoints and step through source mapped
+ * coffee script.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_binary_search.html";
+const COFFEE_URL = EXAMPLE_URL + "code_binary_search.coffee";
+
+var gTab, gPanel, gDebugger;
+var gEditor, gSources;
+
+function test() {
+ let options = {
+ source: COFFEE_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gEditor = gDebugger.DebuggerView.editor;
+ gSources = gDebugger.DebuggerView.Sources;
+
+ checkSourceMapsEnabled();
+
+ checkInitialSource();
+ testSetBreakpoint()
+ .then(testSetBreakpointBlankLine)
+ .then(testHitBreakpoint)
+ .then(testStepping)
+ .then(() => resumeDebuggerThenCloseAndFinish(gPanel))
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+ });
+}
+
+function checkSourceMapsEnabled() {
+ is(Services.prefs.getBoolPref("devtools.debugger.source-maps-enabled"), true,
+ "The source maps functionality should be enabled by default.");
+ is(gDebugger.Prefs.sourceMapsEnabled, true,
+ "The source maps pref should be true from startup.");
+ is(gDebugger.DebuggerView.Options._showOriginalSourceItem.getAttribute("checked"), "true",
+ "Source maps should be enabled from startup.");
+}
+
+function checkInitialSource() {
+ isnot(gSources.selectedItem.attachment.source.url.indexOf(".coffee"), -1,
+ "The debugger should show the source mapped coffee source file.");
+ is(gSources.selectedValue.indexOf(".js"), -1,
+ "The debugger should not show the generated js source file.");
+ is(gEditor.getText().indexOf("isnt"), 218,
+ "The debugger's editor should have the coffee source source displayed.");
+ is(gEditor.getText().indexOf("function"), -1,
+ "The debugger's editor should not have the JS source displayed.");
+}
+
+function testSetBreakpoint() {
+ let deferred = promise.defer();
+ let sourceForm = getSourceForm(gSources, COFFEE_URL);
+
+ gDebugger.gThreadClient.interrupt(aResponse => {
+ let source = gDebugger.gThreadClient.source(sourceForm);
+ source.setBreakpoint({ line: 5 }, aResponse => {
+ ok(!aResponse.error,
+ "Should be able to set a breakpoint in a coffee source file.");
+ ok(!aResponse.actualLocation,
+ "Should be able to set a breakpoint on line 5.");
+
+ deferred.resolve();
+ });
+ });
+
+ return deferred.promise;
+}
+
+function testSetBreakpointBlankLine() {
+ let deferred = promise.defer();
+ let sourceForm = getSourceForm(gSources, COFFEE_URL);
+
+ let source = gDebugger.gThreadClient.source(sourceForm);
+ source.setBreakpoint({ line: 8 }, aResponse => {
+ ok(!aResponse.error,
+ "Should be able to set a breakpoint in a coffee source file on a blank line.");
+ ok(!aResponse.isPending,
+ "Should not be a pending breakpoint.");
+ ok(!aResponse.actualLocation,
+ "Should not be a moved breakpoint.");
+
+ deferred.resolve();
+ });
+
+ return deferred.promise;
+}
+
+function testHitBreakpoint() {
+ let deferred = promise.defer();
+
+ gDebugger.gThreadClient.resume(aResponse => {
+ ok(!aResponse.error, "Shouldn't get an error resuming.");
+ is(aResponse.type, "resumed", "Type should be 'resumed'.");
+
+ gDebugger.gThreadClient.addOneTimeListener("paused", (aEvent, aPacket) => {
+ is(aPacket.type, "paused",
+ "We should now be paused again.");
+ is(aPacket.why.type, "breakpoint",
+ "and the reason we should be paused is because we hit a breakpoint.");
+
+ // Check that we stopped at the right place, by making sure that the
+ // environment is in the state that we expect.
+ is(aPacket.frame.environment.bindings.variables.start.value, 0,
+ "'start' is 0.");
+ is(aPacket.frame.environment.bindings.variables.stop.value.type, "undefined",
+ "'stop' hasn't been assigned to yet.");
+ is(aPacket.frame.environment.bindings.variables.pivot.value.type, "undefined",
+ "'pivot' hasn't been assigned to yet.");
+
+ waitForCaretUpdated(gPanel, 5).then(deferred.resolve);
+ });
+
+ // This will cause the breakpoint to be hit, and put us back in the
+ // paused state.
+ callInTab(gTab, "binary_search", [0, 2, 3, 5, 7, 10], 5);
+ });
+
+ return deferred.promise;
+}
+
+function testStepping() {
+ let deferred = promise.defer();
+
+ gDebugger.gThreadClient.stepIn(aResponse => {
+ ok(!aResponse.error, "Shouldn't get an error resuming.");
+ is(aResponse.type, "resumed", "Type should be 'resumed'.");
+
+ // After stepping, we will pause again, so listen for that.
+ gDebugger.gThreadClient.addOneTimeListener("paused", (aEvent, aPacket) => {
+ is(aPacket.type, "paused",
+ "We should now be paused again.");
+ is(aPacket.why.type, "resumeLimit",
+ "and the reason we should be paused is because we hit the resume limit.");
+
+ // Check that we stopped at the right place, by making sure that the
+ // environment is in the state that we expect.
+ is(aPacket.frame.environment.bindings.variables.start.value, 0,
+ "'start' is 0.");
+ is(aPacket.frame.environment.bindings.variables.stop.value, 5,
+ "'stop' is 5.");
+ is(aPacket.frame.environment.bindings.variables.pivot.value.type, "undefined",
+ "'pivot' hasn't been assigned to yet.");
+
+ waitForCaretUpdated(gPanel, 6).then(deferred.resolve);
+ });
+ });
+
+ return deferred.promise;
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gEditor = null;
+ gSources = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_source-maps-02.js b/devtools/client/debugger/test/mochitest/browser_dbg_source-maps-02.js
new file mode 100644
index 000000000..e406c9ce4
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_source-maps-02.js
@@ -0,0 +1,153 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Test that we can toggle between the original and generated sources.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_binary_search.html";
+const JS_URL = EXAMPLE_URL + "code_binary_search.js";
+
+var gTab, gPanel, gDebugger, gEditor;
+var gSources, gFrames, gPrefs, gOptions;
+
+function test() {
+ let options = {
+ source: EXAMPLE_URL + "code_binary_search.coffee",
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gEditor = gDebugger.DebuggerView.editor;
+ gSources = gDebugger.DebuggerView.Sources;
+ gFrames = gDebugger.DebuggerView.StackFrames;
+ gPrefs = gDebugger.Prefs;
+ gOptions = gDebugger.DebuggerView.Options;
+
+ testToggleGeneratedSource()
+ .then(testSetBreakpoint)
+ .then(testToggleOnPause)
+ .then(testResume)
+ .then(() => closeDebuggerAndFinish(gPanel))
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+ });
+}
+
+function testToggleGeneratedSource() {
+ let finished = waitForSourceShown(gPanel, ".js").then(() => {
+ is(gPrefs.sourceMapsEnabled, false,
+ "The source maps pref should have been set to false.");
+ is(gOptions._showOriginalSourceItem.getAttribute("checked"), "false",
+ "Source maps should now be disabled.");
+
+ is(gSources.selectedItem.attachment.source.url.indexOf(".coffee"), -1,
+ "The debugger should not show the source mapped coffee source file.");
+ isnot(gSources.selectedItem.attachment.source.url.indexOf(".js"), -1,
+ "The debugger should show the generated js source file.");
+
+ is(gEditor.getText().indexOf("isnt"), -1,
+ "The debugger's editor should not have the coffee source source displayed.");
+ is(gEditor.getText().indexOf("function"), 36,
+ "The debugger's editor should have the JS source displayed.");
+ });
+
+ gOptions._showOriginalSourceItem.setAttribute("checked", "false");
+ gOptions._toggleShowOriginalSource();
+ gOptions._onPopupHidden();
+
+ return finished;
+}
+
+function testSetBreakpoint() {
+ let deferred = promise.defer();
+ let sourceForm = getSourceForm(gSources, JS_URL);
+ let source = gDebugger.gThreadClient.source(sourceForm);
+
+ source.setBreakpoint({ line: 7 }, aResponse => {
+ ok(!aResponse.error,
+ "Should be able to set a breakpoint in a js file.");
+
+ gDebugger.gClient.addOneTimeListener("resumed", () => {
+ waitForCaretAndScopes(gPanel, 7).then(() => {
+ // Make sure that we have JavaScript stack frames.
+ is(gFrames.itemCount, 1,
+ "Should have only one frame.");
+ is(gFrames.getItemAtIndex(0).attachment.url.indexOf(".coffee"), -1,
+ "First frame should not be a coffee source frame.");
+ isnot(gFrames.getItemAtIndex(0).attachment.url.indexOf(".js"), -1,
+ "First frame should be a JS frame.");
+
+ deferred.resolve();
+ });
+
+ // This will cause the breakpoint to be hit, and put us back in the
+ // paused state.
+ callInTab(gTab, "binary_search", [0, 2, 3, 5, 7, 10], 5);
+ });
+ });
+
+ return deferred.promise;
+}
+
+function testToggleOnPause() {
+ let finished = waitForSourceAndCaretAndScopes(gPanel, ".coffee", 5).then(() => {
+ is(gPrefs.sourceMapsEnabled, true,
+ "The source maps pref should have been set to true.");
+ is(gOptions._showOriginalSourceItem.getAttribute("checked"), "true",
+ "Source maps should now be enabled.");
+
+ isnot(gSources.selectedItem.attachment.source.url.indexOf(".coffee"), -1,
+ "The debugger should show the source mapped coffee source file.");
+ is(gSources.selectedItem.attachment.source.url.indexOf(".js"), -1,
+ "The debugger should not show the generated js source file.");
+
+ is(gEditor.getText().indexOf("isnt"), 218,
+ "The debugger's editor should have the coffee source source displayed.");
+ is(gEditor.getText().indexOf("function"), -1,
+ "The debugger's editor should not have the JS source displayed.");
+
+ // Make sure that we have coffee source stack frames.
+ is(gFrames.itemCount, 1,
+ "Should have only one frame.");
+ is(gFrames.getItemAtIndex(0).attachment.url.indexOf(".js"), -1,
+ "First frame should not be a JS frame.");
+ isnot(gFrames.getItemAtIndex(0).attachment.url.indexOf(".coffee"), -1,
+ "First frame should be a coffee source frame.");
+ });
+
+ gOptions._showOriginalSourceItem.setAttribute("checked", "true");
+ gOptions._toggleShowOriginalSource();
+ gOptions._onPopupHidden();
+
+ return finished;
+}
+
+function testResume() {
+ let deferred = promise.defer();
+
+ gDebugger.gThreadClient.resume(aResponse => {
+ ok(!aResponse.error, "Shouldn't get an error resuming.");
+ is(aResponse.type, "resumed", "Type should be 'resumed'.");
+
+ deferred.resolve();
+ });
+
+ return deferred.promise;
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gEditor = null;
+ gSources = null;
+ gFrames = null;
+ gPrefs = null;
+ gOptions = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_source-maps-03.js b/devtools/client/debugger/test/mochitest/browser_dbg_source-maps-03.js
new file mode 100644
index 000000000..b729be49f
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_source-maps-03.js
@@ -0,0 +1,88 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Test that we can debug minified javascript with source maps.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_minified.html";
+const JS_URL = EXAMPLE_URL + "code_math.js";
+
+var gTab, gPanel, gDebugger;
+var gEditor, gSources, gFrames;
+
+function test() {
+ let options = {
+ source: JS_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gEditor = gDebugger.DebuggerView.editor;
+ gSources = gDebugger.DebuggerView.Sources;
+ gFrames = gDebugger.DebuggerView.StackFrames;
+
+ checkInitialSource()
+ testSetBreakpoint()
+ .then(() => resumeDebuggerThenCloseAndFinish(gPanel))
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+ });
+}
+
+function checkInitialSource() {
+ isnot(gSources.selectedItem.attachment.source.url.indexOf(".js"), -1,
+ "The debugger should not show the minified js file.");
+ is(gSources.selectedItem.attachment.source.url.indexOf(".min.js"), -1,
+ "The debugger should show the original js file.");
+ is(gEditor.getText().split("\n").length, 46,
+ "The debugger's editor should have the original source displayed, " +
+ "not the whitespace stripped minified version.");
+}
+
+function testSetBreakpoint() {
+ let deferred = promise.defer();
+ let sourceForm = getSourceForm(gSources, JS_URL);
+ let source = gDebugger.gThreadClient.source(sourceForm);
+
+ source.setBreakpoint({ line: 30 }, aResponse => {
+ ok(!aResponse.error,
+ "Should be able to set a breakpoint in a js file.");
+ ok(!aResponse.actualLocation,
+ "Should be able to set a breakpoint on line 30.");
+
+ gDebugger.gClient.addOneTimeListener("resumed", () => {
+ waitForCaretAndScopes(gPanel, 30).then(() => {
+ // Make sure that we have the right stack frames.
+ is(gFrames.itemCount, 9,
+ "Should have nine frames.");
+ is(gFrames.getItemAtIndex(0).attachment.url.indexOf(".min.js"), -1,
+ "First frame should not be a minified JS frame.");
+ isnot(gFrames.getItemAtIndex(0).attachment.url.indexOf(".js"), -1,
+ "First frame should be a JS frame.");
+
+ deferred.resolve();
+ });
+
+ // This will cause the breakpoint to be hit, and put us back in the
+ // paused state.
+ callInTab(gTab, "arithmetic");
+ });
+ });
+
+ return deferred.promise;
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gEditor = null;
+ gSources = null;
+ gFrames = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_source-maps-04.js b/devtools/client/debugger/test/mochitest/browser_dbg_source-maps-04.js
new file mode 100644
index 000000000..f3c4e89a8
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_source-maps-04.js
@@ -0,0 +1,187 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Test that bogus source maps don't break debugging.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_minified_bogus_map.html";
+const JS_URL = EXAMPLE_URL + "code_math_bogus_map.js";
+
+// This test causes an error to be logged in the console, which appears in TBPL
+// logs, so we are disabling that here.
+DevToolsUtils.reportingDisabled = true;
+
+var gPanel, gDebugger, gFrames, gSources, gPrefs, gOptions;
+
+function test() {
+ let options = {
+ source: JS_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gFrames = gDebugger.DebuggerView.StackFrames;
+ gSources = gDebugger.DebuggerView.Sources;
+ gPrefs = gDebugger.Prefs;
+ gOptions = gDebugger.DebuggerView.Options;
+
+ is(gPrefs.pauseOnExceptions, false,
+ "The pause-on-exceptions pref should be disabled by default.");
+ isnot(gOptions._pauseOnExceptionsItem.getAttribute("checked"), "true",
+ "The pause-on-exceptions menu item should not be checked.");
+
+ checkInitialSource();
+ enablePauseOnExceptions()
+ .then(disableIgnoreCaughtExceptions)
+ .then(testSetBreakpoint)
+ .then(reloadPage)
+ .then(testHitBreakpoint)
+ .then(enableIgnoreCaughtExceptions)
+ .then(disablePauseOnExceptions)
+ .then(() => closeDebuggerAndFinish(gPanel))
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+ });
+}
+
+function checkInitialSource() {
+ isnot(gSources.selectedItem.attachment.source.url.indexOf("code_math_bogus_map.js"), -1,
+ "The debugger should show the minified js file.");
+}
+
+function enablePauseOnExceptions() {
+ let deferred = promise.defer();
+
+ gDebugger.gThreadClient.addOneTimeListener("resumed", () => {
+ is(gPrefs.pauseOnExceptions, true,
+ "The pause-on-exceptions pref should now be enabled.");
+
+ ok(true, "Pausing on exceptions was enabled.");
+ deferred.resolve();
+ });
+
+ gOptions._pauseOnExceptionsItem.setAttribute("checked", "true");
+ gOptions._togglePauseOnExceptions();
+
+ return deferred.promise;
+}
+
+function disableIgnoreCaughtExceptions() {
+ let deferred = promise.defer();
+
+ gDebugger.gThreadClient.addOneTimeListener("resumed", () => {
+ is(gPrefs.ignoreCaughtExceptions, false,
+ "The ignore-caught-exceptions pref should now be disabled.");
+
+ ok(true, "Ignore caught exceptions was disabled.");
+ deferred.resolve();
+ });
+
+ gOptions._ignoreCaughtExceptionsItem.setAttribute("checked", "false");
+ gOptions._toggleIgnoreCaughtExceptions();
+
+ return deferred.promise;
+}
+
+function testSetBreakpoint() {
+ let deferred = promise.defer();
+ let sourceForm = getSourceForm(gSources, JS_URL);
+ let source = gDebugger.gThreadClient.source(sourceForm);
+
+ source.setBreakpoint({ line: 3, column: 18 }, aResponse => {
+ ok(!aResponse.error,
+ "Should be able to set a breakpoint in a js file.");
+ ok(!aResponse.actualLocation,
+ "Should be able to set a breakpoint on line 3 and column 18.");
+
+ deferred.resolve();
+ });
+
+ return deferred.promise;
+}
+
+function reloadPage() {
+ let loaded = waitForSourceAndCaret(gPanel, ".js", 3);
+ gDebugger.DebuggerController._target.activeTab.reload();
+ return loaded.then(() => ok(true, "Page was reloaded and execution resumed."));
+}
+
+function testHitBreakpoint() {
+ let deferred = promise.defer();
+
+ gDebugger.gThreadClient.resume(aResponse => {
+ ok(!aResponse.error, "Shouldn't get an error resuming.");
+ is(aResponse.type, "resumed", "Type should be 'resumed'.");
+
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES).then(() => {
+ is(gFrames.itemCount, 2, "Should have two frames.");
+
+ // This is weird, but we need to let the debugger a chance to
+ // update first
+ executeSoon(() => {
+ gDebugger.gThreadClient.resume(() => {
+ gDebugger.gThreadClient.addOneTimeListener("paused", () => {
+ gDebugger.gThreadClient.resume(() => {
+ // We also need to make sure the next step doesn't add a
+ // "resumed" handler until this is completely finished
+ executeSoon(() => {
+ deferred.resolve();
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+
+ return deferred.promise;
+}
+
+function enableIgnoreCaughtExceptions() {
+ let deferred = promise.defer();
+
+ gDebugger.gThreadClient.addOneTimeListener("resumed", () => {
+ is(gPrefs.ignoreCaughtExceptions, true,
+ "The ignore-caught-exceptions pref should now be enabled.");
+
+ ok(true, "Ignore caught exceptions was enabled.");
+ deferred.resolve();
+ });
+
+ gOptions._ignoreCaughtExceptionsItem.setAttribute("checked", "true");
+ gOptions._toggleIgnoreCaughtExceptions();
+
+ return deferred.promise;
+}
+
+function disablePauseOnExceptions() {
+ let deferred = promise.defer();
+
+ gDebugger.gThreadClient.addOneTimeListener("resumed", () => {
+ is(gPrefs.pauseOnExceptions, false,
+ "The pause-on-exceptions pref should now be disabled.");
+
+ ok(true, "Pausing on exceptions was disabled.");
+ deferred.resolve();
+ });
+
+ gOptions._pauseOnExceptionsItem.setAttribute("checked", "false");
+ gOptions._togglePauseOnExceptions();
+
+ return deferred.promise;
+}
+
+registerCleanupFunction(function () {
+ gPanel = null;
+ gDebugger = null;
+ gFrames = null;
+ gSources = null;
+ gPrefs = null;
+ gOptions = null;
+ DevToolsUtils.reportingDisabled = false;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_sources-bookmarklet.js b/devtools/client/debugger/test/mochitest/browser_dbg_sources-bookmarklet.js
new file mode 100644
index 000000000..506e24006
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_sources-bookmarklet.js
@@ -0,0 +1,53 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure javascript bookmarklet scripts appear and load correctly in the source list
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_script-bookmarklet.html";
+
+const BOOKMARKLET_SCRIPT_CODE = "console.log('bookmarklet executed');";
+
+function test() {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ const gTab = aTab;
+ const gPanel = aPanel;
+ const gDebugger = gPanel.panelWin;
+ const gSources = gDebugger.DebuggerView.Sources;
+ const gBreakpoints = gDebugger.DebuggerController.Breakpoints;
+ const getState = gDebugger.DebuggerController.getState;
+ const constants = gDebugger.require("./content/constants");
+ const queries = gDebugger.require("./content/queries");
+ const actions = bindActionCreators(gPanel);
+
+ return Task.spawn(function* () {
+ const added = waitForNextDispatch(gDebugger.DebuggerController, constants.ADD_SOURCE);
+ // NOTE: devtools debugger panel needs to be already open,
+ // or the bookmarklet script will not be shown in the sources panel
+ callInTab(gTab, "injectBookmarklet", BOOKMARKLET_SCRIPT_CODE);
+ yield added;
+
+ is(queries.getSourceCount(getState()), 2, "Should have 2 sources");
+
+ const sources = queries.getSources(getState());
+ const sourceActor = Object.keys(sources).filter(k => {
+ return sources[k].url.indexOf("javascript:") === 0;
+ })[0];
+ const source = sources[sourceActor];
+ ok(source, "Source exists.");
+
+ let res = yield actions.loadSourceText(source);
+ is(res.text, BOOKMARKLET_SCRIPT_CODE, "source is correct");
+ is(res.contentType, "text/javascript", "contentType is correct");
+
+ yield closeDebuggerAndFinish(gPanel);
+ });
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_sources-cache.js b/devtools/client/debugger/test/mochitest/browser_dbg_sources-cache.js
new file mode 100644
index 000000000..2c5d9d0e2
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_sources-cache.js
@@ -0,0 +1,147 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests if the sources cache knows how to cache sources when prompted.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_function-search.html";
+const TOTAL_SOURCES = 4;
+
+function test() {
+ let options = {
+ source: EXAMPLE_URL + "code_function-search-01.js",
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab, aDebuggee, aPanel]) => {
+ const gTab = aTab;
+ const gDebuggee = aDebuggee;
+ const gPanel = aPanel;
+ const gDebugger = gPanel.panelWin;
+ const gEditor = gDebugger.DebuggerView.editor;
+ const gSources = gDebugger.DebuggerView.Sources;
+ const gPrevLabelsCache = gDebugger.SourceUtils._labelsCache;
+ const gPrevGroupsCache = gDebugger.SourceUtils._groupsCache;
+ const getState = gDebugger.DebuggerController.getState;
+ const queries = gDebugger.require("./content/queries");
+ const actions = bindActionCreators(gPanel);
+
+ function initialChecks() {
+ ok(gEditor.getText().includes("First source!"),
+ "Editor text contents appears to be correct.");
+ is(gSources.selectedItem.attachment.label, "code_function-search-01.js",
+ "The currently selected label in the sources container is correct.");
+ ok(getSelectedSourceURL(gSources).includes("code_function-search-01.js"),
+ "The currently selected value in the sources container appears to be correct.");
+
+ is(gSources.itemCount, TOTAL_SOURCES,
+ "There should be " + TOTAL_SOURCES + " sources present in the sources list.");
+ is(gSources.visibleItems.length, TOTAL_SOURCES,
+ "There should be " + TOTAL_SOURCES + " sources visible in the sources list.");
+ is(gSources.attachments.length, TOTAL_SOURCES,
+ "There should be " + TOTAL_SOURCES + " attachments stored in the sources container model.");
+ is(gSources.values.length, TOTAL_SOURCES,
+ "There should be " + TOTAL_SOURCES + " values stored in the sources container model.");
+
+ info("Source labels: " + gSources.attachments.toSource());
+ info("Source values: " + gSources.values.toSource());
+
+ is(gSources.attachments[0].label, "code_function-search-01.js",
+ "The first source label is correct.");
+ ok(gSources.attachments[0].source.url.includes("code_function-search-01.js"),
+ "The first source value appears to be correct.");
+
+ is(gSources.attachments[1].label, "code_function-search-02.js",
+ "The second source label is correct.");
+ ok(gSources.attachments[1].source.url.includes("code_function-search-02.js"),
+ "The second source value appears to be correct.");
+
+ is(gSources.attachments[2].label, "code_function-search-03.js",
+ "The third source label is correct.");
+ ok(gSources.attachments[2].source.url.includes("code_function-search-03.js"),
+ "The third source value appears to be correct.");
+
+ is(gSources.attachments[3].label, "doc_function-search.html",
+ "The third source label is correct.");
+ ok(gSources.attachments[3].source.url.includes("doc_function-search.html"),
+ "The third source value appears to be correct.");
+
+ is(gDebugger.SourceUtils._labelsCache.size, TOTAL_SOURCES,
+ "There should be " + TOTAL_SOURCES + " labels cached.");
+ is(gDebugger.SourceUtils._groupsCache.size, TOTAL_SOURCES,
+ "There should be " + TOTAL_SOURCES + " groups cached.");
+ }
+
+ function performReloadAndTestState() {
+ gDebugger.gTarget.once("will-navigate", testStateBeforeReload);
+ gDebugger.gTarget.once("navigate", testStateAfterReload);
+ return reloadActiveTab(gPanel, gDebugger.EVENTS.SOURCE_SHOWN);
+ }
+
+ function testCacheIntegrity(cachedSources) {
+ const contents = {
+ [EXAMPLE_URL + "code_function-search-01.js"]: "First source!",
+ [EXAMPLE_URL + "code_function-search-02.js"]: "Second source!",
+ [EXAMPLE_URL + "code_function-search-03.js"]: "Third source!",
+ [EXAMPLE_URL + "doc_function-search.html"]: "Peanut butter jelly time!"
+ };
+
+ const sourcesText = getState().sources.sourcesText;
+ is(Object.keys(sourcesText).length, cachedSources.length,
+ "The right number of sources is cached");
+
+ cachedSources.forEach(sourceUrl => {
+ const source = queries.getSourceByURL(getState(), EXAMPLE_URL + sourceUrl);
+ const content = queries.getSourceText(getState(), source.actor);
+ ok(content, "Source text is cached");
+ ok(content.text.includes(contents[source.url]), "Source text is correct");
+ });
+ }
+
+ function fetchAllSources() {
+ const sources = queries.getSources(getState());
+ return Promise.all(Object.keys(sources).map(k => {
+ const source = sources[k];
+ return actions.loadSourceText(source);
+ }));
+ }
+
+ function testStateBeforeReload() {
+ is(gSources.itemCount, 0,
+ "There should be no sources present in the sources list during reload.");
+ is(gDebugger.SourceUtils._labelsCache, gPrevLabelsCache,
+ "The labels cache has been refreshed during reload and no new objects were created.");
+ is(gDebugger.SourceUtils._groupsCache, gPrevGroupsCache,
+ "The groups cache has been refreshed during reload and no new objects were created.");
+ is(gDebugger.SourceUtils._labelsCache.size, 0,
+ "There should be no labels cached during reload");
+ is(gDebugger.SourceUtils._groupsCache.size, 0,
+ "There should be no groups cached during reload");
+ }
+
+ function testStateAfterReload() {
+ is(gSources.itemCount, TOTAL_SOURCES,
+ "There should be " + TOTAL_SOURCES + " sources present in the sources list.");
+ is(gDebugger.SourceUtils._labelsCache.size, TOTAL_SOURCES,
+ "There should be " + TOTAL_SOURCES + " labels cached after reload.");
+ is(gDebugger.SourceUtils._groupsCache.size, TOTAL_SOURCES,
+ "There should be " + TOTAL_SOURCES + " groups cached after reload.");
+ }
+
+ Task.spawn(function* () {
+ yield initialChecks();
+ yield testCacheIntegrity(["code_function-search-01.js"]);
+ yield fetchAllSources();
+ yield testCacheIntegrity([
+ "code_function-search-01.js",
+ "code_function-search-02.js",
+ "code_function-search-03.js",
+ "doc_function-search.html"
+ ]);
+ yield performReloadAndTestState();
+ closeDebuggerAndFinish(gPanel);
+ });
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_sources-contextmenu-01.js b/devtools/client/debugger/test/mochitest/browser_dbg_sources-contextmenu-01.js
new file mode 100644
index 000000000..967c98cff
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_sources-contextmenu-01.js
@@ -0,0 +1,57 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests the "Copy URL" functionality of the sources panel context menu
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_function-search.html";
+const SCRIPT_URI = EXAMPLE_URL + "code_function-search-01.js";
+
+function test() {
+ let gTab, gPanel, gDebugger, gSources;
+
+ let options = {
+ source: SCRIPT_URI,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gSources = gDebugger.DebuggerView.Sources;
+
+ openContextMenu()
+ .then(testCopyMenuItem)
+ .then(() => closeDebuggerAndFinish(gPanel))
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+ });
+
+ function clickCopyURL() {
+ return new Promise((resolve, reject) => {
+ let copyURLMenuItem = gDebugger.document.getElementById("debugger-sources-context-copyurl");
+ if (!copyURLMenuItem) {
+ reject(new Error("The Copy URL context menu item is not available."));
+ }
+
+ ok(copyURLMenuItem, "The Copy URL context menu item is available.");
+ EventUtils.synthesizeMouseAtCenter(copyURLMenuItem, {}, gDebugger);
+ resolve();
+ });
+ }
+
+ function testCopyMenuItem() {
+ return waitForClipboardPromise(clickCopyURL, SCRIPT_URI);
+ }
+
+ function openContextMenu() {
+ let contextMenu = gDebugger.document.getElementById("debuggerSourcesContextMenu");
+ let contextMenuShown = once(contextMenu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(gSources.selectedItem.prebuiltNode, {type: "contextmenu"}, gDebugger);
+ return contextMenuShown;
+ }
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_sources-contextmenu-02.js b/devtools/client/debugger/test/mochitest/browser_dbg_sources-contextmenu-02.js
new file mode 100644
index 000000000..da6668f51
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_sources-contextmenu-02.js
@@ -0,0 +1,75 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests the "Open in New Tab" functionality of the sources panel context menu
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_function-search.html";
+const SCRIPT_URI = EXAMPLE_URL + "code_function-search-01.js";
+
+function test() {
+ let gTab, gPanel, gDebugger;
+ let gSources;
+
+ let options = {
+ source: SCRIPT_URI,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gSources = gDebugger.DebuggerView.Sources;
+
+ openContextMenu()
+ .then(testNewTabMenuItem)
+ .then(testNewTabURI)
+ .then(() => closeDebuggerAndFinish(gPanel))
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+ });
+
+ function testNewTabURI(tabUri) {
+ is(tabUri, SCRIPT_URI, "The tab contains the right script.");
+ gBrowser.removeCurrentTab();
+ }
+
+ function waitForTabOpen() {
+ return new Promise(resolve => {
+ gBrowser.tabContainer.addEventListener("TabOpen", function onOpen(e) {
+ gBrowser.tabContainer.removeEventListener("TabOpen", onOpen, false);
+ ok(true, "A new tab loaded");
+
+ gBrowser.addEventListener("DOMContentLoaded", function onTabLoad(e) {
+ gBrowser.removeEventListener("DOMContentLoaded", onTabLoad, false);
+ // Pass along the new tab's URI.
+ resolve(gBrowser.currentURI.spec);
+ }, false);
+ }, false);
+ });
+ }
+
+ function testNewTabMenuItem() {
+ return new Promise((resolve, reject) => {
+ let newTabMenuItem = gDebugger.document.getElementById("debugger-sources-context-newtab");
+ if (!newTabMenuItem) {
+ reject(new Error("The Open in New Tab context menu item is not available."));
+ }
+
+ ok(newTabMenuItem, "The Open in New Tab context menu item is available.");
+ waitForTabOpen().then(resolve);
+ EventUtils.synthesizeMouseAtCenter(newTabMenuItem, {}, gDebugger);
+ });
+ }
+
+ function openContextMenu() {
+ let contextMenu = gDebugger.document.getElementById("debuggerSourcesContextMenu");
+ let contextMenuShown = once(contextMenu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(gSources.selectedItem.prebuiltNode, {type: "contextmenu"}, gDebugger);
+ return contextMenuShown;
+ }
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_sources-eval-01.js b/devtools/client/debugger/test/mochitest/browser_dbg_sources-eval-01.js
new file mode 100644
index 000000000..0e794d06c
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_sources-eval-01.js
@@ -0,0 +1,44 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure eval scripts appear in the source list
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_script-eval.html";
+
+function test() {
+ let gTab, gPanel, gDebugger;
+ let gSources, gBreakpoints;
+
+ let options = {
+ source: EXAMPLE_URL + "code_script-eval.js",
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gSources = gDebugger.DebuggerView.Sources;
+ gBreakpoints = gDebugger.DebuggerController.Breakpoints;
+
+ return Task.spawn(function* () {
+ is(gSources.values.length, 1, "Should have 1 source");
+
+ let newSource = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.NEW_SOURCE);
+ callInTab(gTab, "evalSource");
+ yield newSource;
+
+ is(gSources.values.length, 2, "Should have 2 sources");
+
+ let item = gSources.getItemForAttachment(e => e.label.indexOf("> eval") !== -1);
+ ok(item, "Source label is incorrect.");
+ is(item.attachment.group, gDebugger.L10N.getStr("evalGroupLabel"),
+ "Source group is incorrect");
+
+ yield closeDebuggerAndFinish(gPanel);
+ });
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_sources-eval-02.js b/devtools/client/debugger/test/mochitest/browser_dbg_sources-eval-02.js
new file mode 100644
index 000000000..b932df143
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_sources-eval-02.js
@@ -0,0 +1,55 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure eval scripts with the sourceURL pragma are correctly
+ * displayed
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_script-eval.html";
+
+function test() {
+ let gTab, gPanel, gDebugger;
+ let gSources, gBreakpoints, gEditor;
+
+ let options = {
+ source: EXAMPLE_URL + "code_script-eval.js",
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gSources = gDebugger.DebuggerView.Sources;
+ gBreakpoints = gDebugger.DebuggerController.Breakpoints;
+ gEditor = gDebugger.DebuggerView.editor;
+ const constants = gDebugger.require("./content/constants");
+ const queries = gDebugger.require("./content/queries");
+ const actions = bindActionCreators(gPanel);
+ const getState = gDebugger.DebuggerController.getState;
+
+ return Task.spawn(function* () {
+ is(queries.getSourceCount(getState()), 1, "Should have 1 source");
+
+ const newSource = waitForDispatch(gPanel, constants.ADD_SOURCE);
+ callInTab(gTab, "evalSourceWithSourceURL");
+ yield newSource;
+
+ is(queries.getSourceCount(getState()), 2, "Should have 2 sources");
+
+ const source = queries.getSourceByURL(getState(), EXAMPLE_URL + "bar.js");
+ ok(source, "Source exists.");
+
+ let shown = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.SOURCE_SHOWN);
+ actions.selectSource(source);
+ yield shown;
+
+ ok(gEditor.getText().indexOf("bar = function() {") === 0,
+ "Correct source is shown");
+
+ yield closeDebuggerAndFinish(gPanel);
+ });
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_sources-iframe-reload.js b/devtools/client/debugger/test/mochitest/browser_dbg_sources-iframe-reload.js
new file mode 100644
index 000000000..63c53fba5
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_sources-iframe-reload.js
@@ -0,0 +1,35 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure iframe scripts don't disappear after few reloads (bug 1259743)
+ */
+
+"use strict";
+
+const IFRAME_URL = "data:text/html;charset=utf-8," +
+ "<script>function fn() { console.log('hello'); }</script>" +
+ "<div onclick='fn()'>hello</div>";
+const TAB_URL = `data:text/html;charset=utf-8,<iframe src="${IFRAME_URL}"/>`;
+
+add_task(function* () {
+ let [,, panel] = yield initDebugger();
+ let dbg = panel.panelWin;
+ let newSource;
+
+ newSource = waitForDebuggerEvents(panel, dbg.EVENTS.NEW_SOURCE);
+ reload(panel, TAB_URL);
+ yield newSource;
+ ok(true, "Source event fired on initial load");
+
+ for (let i = 0; i < 5; i++) {
+ newSource = waitForDebuggerEvents(panel, dbg.EVENTS.NEW_SOURCE);
+ reload(panel);
+ yield newSource;
+ ok(true, `Source event fired after ${i + 1} reloads`);
+ }
+
+ yield closeDebuggerAndFinish(panel);
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_sources-keybindings.js b/devtools/client/debugger/test/mochitest/browser_dbg_sources-keybindings.js
new file mode 100644
index 000000000..80f043637
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_sources-keybindings.js
@@ -0,0 +1,40 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests related to source panel keyboard shortcut bindings
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_function-search.html";
+const SCRIPT_URI = EXAMPLE_URL + "code_function-search-01.js";
+
+function test() {
+ let gTab, gPanel, gDebugger, gSources;
+
+ let options = {
+ source: SCRIPT_URI,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gSources = gDebugger.DebuggerView.Sources;
+
+ testCopyURLShortcut()
+ .then(() => closeDebuggerAndFinish(gPanel))
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+ });
+
+ function testCopyURLShortcut() {
+ return waitForClipboardPromise(sendCopyShortcut, SCRIPT_URI);
+ }
+
+ function sendCopyShortcut() {
+ EventUtils.synthesizeKey("C", { accelKey: true }, gDebugger);
+ }
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_sources-labels.js b/devtools/client/debugger/test/mochitest/browser_dbg_sources-labels.js
new file mode 100644
index 000000000..1c4cfd6da
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_sources-labels.js
@@ -0,0 +1,172 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests that urls are correctly shortened to unique labels.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_recursion-stack.html";
+const { ELLIPSIS } = require("devtools/shared/l10n");
+
+function test() {
+ let gTab, gPanel, gDebugger;
+ let gSources, gUtils;
+
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(Task.async(function* ([aTab,, aPanel]) {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gSources = gDebugger.DebuggerView.Sources;
+ gUtils = gDebugger.SourceUtils;
+
+ let nananana = new Array(20).join(NaN);
+
+ // Test trimming url queries.
+
+ let someUrl = "a/b/c.d?test=1&random=4#reference";
+ let shortenedUrl = "a/b/c.d";
+ is(gUtils.trimUrlQuery(someUrl), shortenedUrl,
+ "Trimming the url query isn't done properly.");
+
+ // Test trimming long urls with an ellipsis.
+
+ let largeLabel = new Array(100).join("Beer can in Jamaican sounds like Bacon!");
+ let trimmedLargeLabel = gUtils.trimUrlLength(largeLabel, 1234);
+ is(trimmedLargeLabel.length, 1235,
+ "Trimming large labels isn't done properly.");
+ ok(trimmedLargeLabel.endsWith(ELLIPSIS),
+ "Trimming large labels should add an ellipsis at the end : " + ELLIPSIS);
+
+ // Test the sources list behaviour with certain urls.
+
+ let urls = [
+ { href: "http://some.address.com/random/", leaf: "subrandom/" },
+ { href: "http://some.address.com/random/", leaf: "suprandom/?a=1" },
+ { href: "http://some.address.com/random/", leaf: "?a=1" },
+ { href: "https://another.address.org/random/subrandom/", leaf: "page.html" },
+
+ { href: "ftp://interesting.address.org/random/", leaf: "script.js" },
+ { href: "ftp://interesting.address.com/random/", leaf: "script.js" },
+ { href: "ftp://interesting.address.com/random/", leaf: "x/script.js" },
+ { href: "ftp://interesting.address.com/random/", leaf: "x/y/script.js?a=1" },
+ { href: "ftp://interesting.address.com/random/x/", leaf: "y/script.js?a=1&b=2" },
+ { href: "ftp://interesting.address.com/random/x/y/", leaf: "script.js?a=1&b=2&c=3" },
+ { href: "ftp://interesting.address.com/random/", leaf: "x/y/script.js?a=2" },
+ { href: "ftp://interesting.address.com/random/x/", leaf: "y/script.js?a=2&b=3" },
+ { href: "ftp://interesting.address.com/random/x/y/", leaf: "script.js?a=2&b=3&c=4" },
+
+ { href: "file://random/", leaf: "script_t1.js&a=1&b=2&c=3" },
+ { href: "file://random/", leaf: "script_t2_1.js#id" },
+ { href: "file://random/", leaf: "script_t2_2.js?a" },
+ { href: "file://random/", leaf: "script_t2_3.js&b" },
+ { href: "resource://random/", leaf: "script_t3_1.js#id?a=1&b=2" },
+ { href: "resource://random/", leaf: "script_t3_2.js?a=1&b=2#id" },
+ { href: "resource://random/", leaf: "script_t3_3.js&a=1&b=2#id" },
+
+ { href: nananana, leaf: "Batman!" + "{trim me, now and forevermore}" }
+ ];
+
+ is(gSources.itemCount, 1,
+ "Should contain the original source label in the sources widget.");
+ is(gSources.selectedIndex, 0,
+ "The first item in the sources widget should be selected (1).");
+ is(gSources.selectedItem.attachment.label, "doc_recursion-stack.html",
+ "The first item in the sources widget should be selected (2).");
+ is(getSelectedSourceURL(gSources), TAB_URL,
+ "The first item in the sources widget should be selected (3).");
+
+ let id = 0;
+ for (let { href, leaf } of urls) {
+ let url = href + leaf;
+ let actor = "actor" + id++;
+ let label = gUtils.trimUrlLength(gUtils.getSourceLabel(url));
+ let group = gUtils.getSourceGroup(url);
+ let dummy = document.createElement("label");
+ dummy.setAttribute("value", label);
+
+ gSources.push([dummy, actor], {
+ attachment: {
+ source: { actor: actor, url: url },
+ label: label,
+ group: group
+ }
+ });
+ }
+
+ info("Source locations:");
+ info(gSources.values.toSource());
+
+ info("Source attachments:");
+ info(gSources.attachments.toSource());
+
+ for (let { href, leaf, dupe } of urls) {
+ let url = href + leaf;
+ if (dupe) {
+ ok(!gSources.containsValue(getSourceActor(gSources, url)), "Shouldn't contain source: " + url);
+ } else {
+ ok(gSources.containsValue(getSourceActor(gSources, url)), "Should contain source: " + url);
+ }
+ }
+
+ ok(gSources.getItemForAttachment(e => e.label == "random/subrandom/"),
+ "Source (0) label is incorrect.");
+ ok(gSources.getItemForAttachment(e => e.label == "random/suprandom/?a=1"),
+ "Source (1) label is incorrect.");
+ ok(gSources.getItemForAttachment(e => e.label == "random/?a=1"),
+ "Source (2) label is incorrect.");
+ ok(gSources.getItemForAttachment(e => e.label == "page.html"),
+ "Source (3) label is incorrect.");
+
+ ok(gSources.getItemForAttachment(e => e.label == "script.js"),
+ "Source (4) label is incorrect.");
+ ok(gSources.getItemForAttachment(e => e.label == "random/script.js"),
+ "Source (5) label is incorrect.");
+ ok(gSources.getItemForAttachment(e => e.label == "random/x/script.js"),
+ "Source (6) label is incorrect.");
+ ok(gSources.getItemForAttachment(e => e.label == "script.js?a=1"),
+ "Source (7) label is incorrect.");
+
+ ok(gSources.getItemForAttachment(e => e.label == "script_t1.js"),
+ "Source (8) label is incorrect.");
+ ok(gSources.getItemForAttachment(e => e.label == "script_t2_1.js"),
+ "Source (9) label is incorrect.");
+ ok(gSources.getItemForAttachment(e => e.label == "script_t2_2.js"),
+ "Source (10) label is incorrect.");
+ ok(gSources.getItemForAttachment(e => e.label == "script_t2_3.js"),
+ "Source (11) label is incorrect.");
+ ok(gSources.getItemForAttachment(e => e.label == "script_t3_1.js"),
+ "Source (12) label is incorrect.");
+ ok(gSources.getItemForAttachment(e => e.label == "script_t3_2.js"),
+ "Source (13) label is incorrect.");
+ ok(gSources.getItemForAttachment(e => e.label == "script_t3_3.js"),
+ "Source (14) label is incorrect.");
+
+ ok(gSources.getItemForAttachment(e => e.label == nananana + "Batman!" + ELLIPSIS),
+ "Source (15) label is incorrect.");
+
+ is(gSources.itemCount, urls.filter(({ dupe }) => !dupe).length + 1,
+ "Didn't get the correct number of sources in the list.");
+
+ is(gSources.getItemByValue(getSourceActor(gSources, "http://some.address.com/random/subrandom/")).attachment.label,
+ "random/subrandom/",
+ "gSources.getItemByValue isn't functioning properly (0).");
+ is(gSources.getItemByValue(getSourceActor(gSources, "http://some.address.com/random/suprandom/?a=1")).attachment.label,
+ "random/suprandom/?a=1",
+ "gSources.getItemByValue isn't functioning properly (1).");
+
+ is(gSources.getItemForAttachment(e => e.label == "random/subrandom/").attachment.source.url,
+ "http://some.address.com/random/subrandom/",
+ "gSources.getItemForAttachment isn't functioning properly (0).");
+ is(gSources.getItemForAttachment(e => e.label == "random/suprandom/?a=1").attachment.source.url,
+ "http://some.address.com/random/suprandom/?a=1",
+ "gSources.getItemForAttachment isn't functioning properly (1).");
+
+ closeDebuggerAndFinish(gPanel);
+ }));
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_sources-large.js b/devtools/client/debugger/test/mochitest/browser_dbg_sources-large.js
new file mode 100644
index 000000000..31b64c2fd
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_sources-large.js
@@ -0,0 +1,80 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests that large files are treated differently in the debugger:
+ * 1) No parsing to determine current symbol is attempted when
+ * starting a search
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_function-search.html";
+
+function test() {
+ let options = {
+ source: EXAMPLE_URL + "code_function-search-01.js",
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab, aDebuggee, aPanel]) => {
+ const gTab = aTab;
+ const gDebuggee = aDebuggee;
+ const gPanel = aPanel;
+ const gDebugger = gPanel.panelWin;
+
+ const gEditor = gDebugger.DebuggerView.editor;
+ const gSources = gDebugger.DebuggerView.Sources;
+ const Filtering = gDebugger.DebuggerView.Filtering;
+
+ // Setting max size so that code_function-search-01.js will be
+ // considered a large file on first load
+ gDebugger.DebuggerView.LARGE_FILE_SIZE = 1;
+
+ function testLargeFile() {
+ ok(gEditor.getText().length > gDebugger.DebuggerView.LARGE_FILE_SIZE,
+ "First source is considered a large file.");
+ is(gEditor.getMode().name, "javascript",
+ "Editor is syntax highlighting.");
+ ok(gEditor.getText().includes("First source!"),
+ "Editor text contents appears to be correct.");
+
+ // Press ctrl+f with the cursor in a token
+ gEditor.focus();
+ gEditor.setCursor({ line: 3, ch: 10});
+ synthesizeKeyFromKeyTag(gDebugger.document.getElementById("tokenSearchKey"));
+ is(Filtering._searchbox.value, "#",
+ "Search box is NOT prefilled with current token");
+ }
+
+ function testSmallFile() {
+ ok(gEditor.getText().length < gDebugger.DebuggerView.LARGE_FILE_SIZE,
+ "Second source is considered a small file.");
+ is(gEditor.getMode().name, "javascript",
+ "Editor is syntax highlighting.");
+ ok(gEditor.getText().includes("First source!"),
+ "Editor text contents appears to be correct.");
+
+ // Press ctrl+f with the cursor in a token
+ gEditor.focus();
+ gEditor.setCursor({ line: 3, ch: 10});
+ synthesizeKeyFromKeyTag(gDebugger.document.getElementById("tokenSearchKey"));
+ is(Filtering._searchbox.value, "#test",
+ "Search box is prefilled with current token");
+ }
+
+ Task.spawn(function* () {
+ yield testLargeFile();
+
+ info("Making it appear as a small file and then reselecting 01.js");
+ gDebugger.DebuggerView.LARGE_FILE_SIZE = 1000;
+ gSources.selectedIndex = 1;
+ yield waitForSourceShown(gPanel, "-02.js");
+ gSources.selectedIndex = 0;
+ yield waitForSourceShown(gPanel, "-01.js");
+
+ yield testSmallFile();
+
+ closeDebuggerAndFinish(gPanel);
+ });
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_sources-sorting.js b/devtools/client/debugger/test/mochitest/browser_dbg_sources-sorting.js
new file mode 100644
index 000000000..2a600893b
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_sources-sorting.js
@@ -0,0 +1,141 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests that urls are correctly sorted when added to the sources widget.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_recursion-stack.html";
+
+var gTab, gPanel, gDebugger;
+var gSources, gUtils;
+
+function test() {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gSources = gDebugger.DebuggerView.Sources;
+ gUtils = gDebugger.SourceUtils;
+
+ addSourceAndCheckOrder(1);
+ addSourceAndCheckOrder(2);
+ addSourceAndCheckOrder(3);
+ closeDebuggerAndFinish(gPanel);
+ });
+}
+
+function addSourceAndCheckOrder(aMethod) {
+ gSources.empty();
+ gSources.suppressSelectionEvents = true;
+
+ let urls = [
+ { href: "ici://some.address.com/random/", leaf: "subrandom/" },
+ { href: "ni://another.address.org/random/subrandom/", leaf: "page.html" },
+ { href: "san://interesting.address.gro/random/", leaf: "script.js" },
+ { href: "si://interesting.address.moc/random/", leaf: "script.js" },
+ { href: "si://interesting.address.moc/random/", leaf: "x/script.js" },
+ { href: "si://interesting.address.moc/random/", leaf: "x/y/script.js?a=1" },
+ { href: "si://interesting.address.moc/random/x/", leaf: "y/script.js?a=1&b=2" },
+ { href: "si://interesting.address.moc/random/x/y/", leaf: "script.js?a=1&b=2&c=3" }
+ ];
+
+ urls.sort(function (a, b) {
+ return Math.random() - 0.5;
+ });
+
+ let id = 0;
+
+ switch (aMethod) {
+ case 1:
+ for (let { href, leaf } of urls) {
+ let url = href + leaf;
+ let actor = "actor" + id++;
+ let label = gUtils.getSourceLabel(url);
+ let dummy = document.createElement("label");
+ gSources.push([dummy, actor], {
+ staged: true,
+ attachment: {
+ label: label
+ }
+ });
+ }
+ gSources.commit({ sorted: true });
+ break;
+
+ case 2:
+ for (let { href, leaf } of urls) {
+ let url = href + leaf;
+ let actor = "actor" + id++;
+ let label = gUtils.getSourceLabel(url);
+ let dummy = document.createElement("label");
+ gSources.push([dummy, actor], {
+ staged: false,
+ attachment: {
+ label: label
+ }
+ });
+ }
+ break;
+
+ case 3:
+ let i = 0;
+ for (; i < urls.length / 2; i++) {
+ let { href, leaf } = urls[i];
+ let url = href + leaf;
+ let actor = "actor" + id++;
+ let label = gUtils.getSourceLabel(url);
+ let dummy = document.createElement("label");
+ gSources.push([dummy, actor], {
+ staged: true,
+ attachment: {
+ label: label
+ }
+ });
+ }
+ gSources.commit({ sorted: true });
+
+ for (; i < urls.length; i++) {
+ let { href, leaf } = urls[i];
+ let url = href + leaf;
+ let actor = "actor" + id++;
+ let label = gUtils.getSourceLabel(url);
+ let dummy = document.createElement("label");
+ gSources.push([dummy, actor], {
+ staged: false,
+ attachment: {
+ label: label
+ }
+ });
+ }
+ break;
+ }
+
+ checkSourcesOrder(aMethod);
+}
+
+function checkSourcesOrder(aMethod) {
+ let attachments = gSources.attachments;
+
+ for (let i = 0; i < attachments.length - 1; i++) {
+ let first = attachments[i].label;
+ let second = attachments[i + 1].label;
+ ok(first < second,
+ "Using method " + aMethod + ", " +
+ "the sources weren't in the correct order: " + first + " vs. " + second);
+ }
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gSources = null;
+ gUtils = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_sources-webext-contentscript.js b/devtools/client/debugger/test/mochitest/browser_dbg_sources-webext-contentscript.js
new file mode 100644
index 000000000..2fd8067f9
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_sources-webext-contentscript.js
@@ -0,0 +1,63 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure eval scripts appear in the source list
+ */
+
+const ADDON_PATH = "addon-webext-contentscript.xpi";
+const TAB_URL = EXAMPLE_URL + "doc_script_webext_contentscript.html";
+
+let {getExtensionUUID} = Cu.import("resource://gre/modules/Extension.jsm", {});
+
+function test() {
+ let gPanel, gDebugger;
+ let gSources, gAddon;
+
+ let cleanup = function* (e) {
+ if (gAddon) {
+ // Remove the addon, if any.
+ yield removeAddon(gAddon);
+ }
+ if (gPanel) {
+ // Close the debugger panel, if any.
+ yield closeDebuggerAndFinish(gPanel);
+ } else {
+ // If no debugger panel was opened, call finish directly.
+ finish();
+ }
+ };
+
+ return Task.spawn(function* () {
+ gAddon = yield addTemporaryAddon(ADDON_PATH);
+ let uuid = getExtensionUUID(gAddon.id);
+
+ let options = {
+ source: `moz-extension://${uuid}/webext-content-script.js`,
+ line: 1
+ };
+ [,, gPanel] = yield initDebugger(TAB_URL, options);
+ gDebugger = gPanel.panelWin;
+ gSources = gDebugger.DebuggerView.Sources;
+
+ is(gSources.values.length, 2, "Should have 2 sources");
+
+ let item = gSources.getItemForAttachment(attachment => {
+ return attachment.source.url.includes("moz-extension");
+ });
+
+ ok(item, "Got the expected WebExtensions ContentScript source");
+ ok(item && item.attachment.source.url.includes(item.attachment.group),
+ "The source is in the expected source group");
+ is(item && item.attachment.label, "webext-content-script.js",
+ "Got the expected filename in the label");
+
+ yield cleanup();
+ }).catch((e) => {
+ ok(false, `Got an unexpected exception: ${e}`);
+ // Cleanup in case of failures in the above task.
+ return Task.spawn(cleanup);
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_split-console-keypress.js b/devtools/client/debugger/test/mochitest/browser_dbg_split-console-keypress.js
new file mode 100644
index 000000000..5bfe0a61e
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_split-console-keypress.js
@@ -0,0 +1,108 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * When the split console is focused and the debugger is open,
+ * debugger shortcut keys like F11 should work
+ */
+const TAB_URL = EXAMPLE_URL + "doc_step-many-statements.html";
+
+function test() {
+ // This does the same assertions over a series of sub-tests, and it
+ // can timeout in linux e10s. No sense in breaking it up into multiple
+ // tests, so request extra time.
+ requestLongerTimeout(2);
+
+ let gDebugger, gToolbox, gThreadClient, gTab, gPanel;
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab, debuggeeWin, aPanel]) => {
+ gPanel = aPanel;
+ gDebugger = aPanel.panelWin;
+ gToolbox = gDevTools.getToolbox(aPanel.target);
+ gTab = aTab;
+ gThreadClient = gDebugger.DebuggerController.activeThread;
+ testConsole();
+ });
+ let testConsole = Task.async(function* () {
+ // We need to open the split console (with an ESC keypress),
+ // then get the script into a paused state by pressing a button in the page,
+ // ensure focus is in the split console,
+ // synthesize a few keys - important ones we share listener for are
+ // "resumeKey", "stepOverKey", "stepInKey", "stepOutKey"
+ // then check that
+ // * The input cursor remains in the console's input box
+ // * The paused state is as expected
+ // * the debugger cursor is where we want it
+ let jsterm = yield getSplitConsole(gToolbox, gDebugger);
+ // The console is now open (if not make the test fail already)
+ ok(gToolbox.splitConsole, "Split console is shown.");
+
+ // Information for sub-tests. When 'key' is synthesized 'keyRepeat' times,
+ // cursor should be at 'caretLine' of this test..
+ let stepTests = [
+ {key: "VK_F11", keyRepeat: 1, caretLine: 16},
+ {key: "VK_F11", keyRepeat: 2, caretLine: 18},
+ {key: "VK_F11", keyRepeat: 2, caretLine: 27},
+ {key: "VK_F10", keyRepeat: 1, caretLine: 27},
+ {key: "VK_F11", keyRepeat: 1, caretLine: 18},
+ {key: "VK_F11", keyRepeat: 5, caretLine: 32},
+ {key: "VK_F11", modifier:"Shift", keyRepeat: 1, caretLine: 29},
+ {key: "VK_F11", modifier:"Shift", keyRepeat: 2, caretLine: 34},
+ {key: "VK_F11", modifier:"Shift", keyRepeat: 2, caretLine: 34}
+ ];
+ // Trigger script that stops at debugger statement
+ executeSoon(() => generateMouseClickInTab(gTab,
+ "content.document.getElementById('start')"));
+ yield waitForPause(gThreadClient);
+
+ // Focus the console and add event listener to track whether it loses focus
+ // (Must happen after generateMouseClickInTab() call)
+ let consoleLostFocus = false;
+ jsterm.focus();
+ jsterm.inputNode.addEventListener("blur", () => {consoleLostFocus = true;});
+
+ is(gThreadClient.paused, true,
+ "Should be paused at debugger statement.");
+ // As long as we have test work to do..
+ for (let i = 0, thisTest; thisTest = stepTests[i]; i++) {
+ // First we send another key event if required by the test
+ while (thisTest.keyRepeat > 0) {
+ thisTest.keyRepeat --;
+ let keyMods = thisTest.modifier === "Shift" ? {shiftKey:true} : {};
+ executeSoon(() => {EventUtils.synthesizeKey(thisTest.key, keyMods);});
+ yield waitForPause(gThreadClient);
+ }
+
+ // We've sent the required number of keys
+ // Here are the conditions we're interested in: paused state,
+ // cursor still in console (tested later), caret correct in editor
+ is(gThreadClient.paused, true,
+ "Should still be paused");
+ // ok(isCaretPos(gPanel, thisTest.caretLine),
+ // "Test " + i + ": CaretPos at line " + thisTest.caretLine);
+ ok(isDebugPos(gPanel, thisTest.caretLine),
+ "Test " + i + ": DebugPos at line " + thisTest.caretLine);
+ }
+ // Did focus go missing while we were stepping?
+ is(consoleLostFocus, false, "Console input should not lose focus");
+ // We're done with the tests in the stepTests array
+ // Last key we test is "resume"
+ executeSoon(() => EventUtils.synthesizeKey("VK_F8", {}));
+
+ // We reset the variable tracking loss of focus to test the resume case
+ consoleLostFocus = false;
+
+ gPanel.target.on("thread-resumed", () => {
+ is(gThreadClient.paused, false,
+ "Should not be paused after resume");
+ // Final test: did we preserve console inputNode focus during resume?
+ is(consoleLostFocus, false, "Resume - console should keep focus");
+ closeDebuggerAndFinish(gPanel);
+ });
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_split-console-paused-reload.js b/devtools/client/debugger/test/mochitest/browser_dbg_split-console-paused-reload.js
new file mode 100644
index 000000000..e9daaa4bc
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_split-console-paused-reload.js
@@ -0,0 +1,67 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Hitting ESC to open the split console when paused on reload should not stop
+ * the pending navigation.
+ */
+
+function test() {
+ Task.spawn(runTests);
+}
+
+function* runTests() {
+ let TAB_URL = EXAMPLE_URL + "doc_split-console-paused-reload.html";
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ let [,, panel] = yield initDebugger(TAB_URL, options);
+ let dbgWin = panel.panelWin;
+ let sources = dbgWin.DebuggerView.Sources;
+ let frames = dbgWin.DebuggerView.StackFrames;
+ let toolbox = gDevTools.getToolbox(panel.target);
+
+ yield panel.addBreakpoint({ actor: getSourceActor(sources, TAB_URL), line: 16 });
+ info("Breakpoint was set.");
+ dbgWin.DebuggerController._target.activeTab.reload();
+ info("Page reloaded.");
+ yield waitForSourceAndCaretAndScopes(panel, ".html", 16);
+ yield ensureThreadClientState(panel, "paused");
+ info("Breakpoint was hit.");
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ frames.selectedItem.target,
+ dbgWin);
+ info("The breadcrumb received focus.");
+
+ // This is the meat of the test.
+ let jsterm = yield getSplitConsole(toolbox);
+
+ is(dbgWin.gThreadClient.state, "paused", "Execution is still paused.");
+
+ let dbgFrameConsoleEvalResult = yield jsterm.execute("privateVar");
+
+ is(
+ dbgFrameConsoleEvalResult.querySelector(".console-string").textContent,
+ '"privateVarValue"',
+ "Got the expected split console result on paused debugger"
+ );
+
+ yield dbgWin.gThreadClient.resume();
+
+ is(dbgWin.gThreadClient.state, "attached", "Execution is resumed.");
+
+ // Get the last evaluation result adopted by the new debugger.
+ let mainTargetConsoleEvalResult = yield jsterm.execute("$_");
+
+ is(
+ mainTargetConsoleEvalResult.querySelector(".console-string").textContent,
+ '"privateVarValue"',
+ "Got the expected split console log on $_ executed on resumed debugger"
+ );
+
+ Services.prefs.clearUserPref("devtools.toolbox.splitconsoleEnabled");
+ yield closeDebuggerAndFinish(panel);
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_stack-01.js b/devtools/client/debugger/test/mochitest/browser_dbg_stack-01.js
new file mode 100644
index 000000000..513823ba9
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_stack-01.js
@@ -0,0 +1,49 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Test that stackframes are added when debugger is paused.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_recursion-stack.html";
+
+var gTab, gPanel, gDebugger;
+var gFrames, gClassicFrames;
+
+function test() {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gFrames = gDebugger.DebuggerView.StackFrames;
+ gClassicFrames = gDebugger.DebuggerView.StackFramesClassicList;
+
+ waitForCaretAndScopes(gPanel, 14).then(performTest);
+ callInTab(gTab, "simpleCall");
+ });
+}
+
+function performTest() {
+ is(gDebugger.gThreadClient.state, "paused",
+ "Should only be getting stack frames while paused.");
+ is(gFrames.itemCount, 1,
+ "Should have only one frame.");
+ is(gClassicFrames.itemCount, 1,
+ "Should also have only one frame in the mirrored view.");
+
+ resumeDebuggerThenCloseAndFinish(gPanel);
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gFrames = null;
+ gClassicFrames = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_stack-02.js b/devtools/client/debugger/test/mochitest/browser_dbg_stack-02.js
new file mode 100644
index 000000000..5c972f4ee
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_stack-02.js
@@ -0,0 +1,115 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Test that stackframes are added when debugger is paused in eval calls.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_recursion-stack.html";
+
+function test() {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ const gTab = aTab;
+ const gPanel = aPanel;
+ const gDebugger = gPanel.panelWin;
+ const gFrames = gDebugger.DebuggerView.StackFrames;
+ const gClassicFrames = gDebugger.DebuggerView.StackFramesClassicList;
+
+ const performTest = Task.async(function* () {
+ is(gDebugger.gThreadClient.state, "paused",
+ "Should only be getting stack frames while paused.");
+ is(gFrames.itemCount, 2,
+ "Should have two frames.");
+ is(gClassicFrames.itemCount, 2,
+ "Should also have only two in the mirrored view.");
+
+ is(gFrames.getItemAtIndex(0).attachment.title,
+ "evalCall", "Oldest frame name should be correct.");
+ is(gFrames.getItemAtIndex(0).attachment.url,
+ TAB_URL, "Oldest frame url should be correct.");
+ is(gClassicFrames.getItemAtIndex(0).attachment.depth,
+ 0, "Oldest frame name is mirrored correctly.");
+
+ is(gFrames.getItemAtIndex(1).attachment.title,
+ "(eval)", "Newest frame name should be correct.");
+ is(gFrames.getItemAtIndex(1).attachment.url,
+ "SCRIPT0", "Newest frame url should be correct.");
+ is(gClassicFrames.getItemAtIndex(1).attachment.depth,
+ 1, "Newest frame name is mirrored correctly.");
+
+ is(gFrames.selectedIndex, 1,
+ "Newest frame should be selected by default.");
+ is(gClassicFrames.selectedIndex, 0,
+ "Newest frame should be selected by default in the mirrored view.");
+
+ isnot(gFrames.selectedIndex, 0,
+ "Oldest frame should not be selected.");
+ isnot(gClassicFrames.selectedIndex, 1,
+ "Oldest frame should not be selected in the mirrored view.");
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ gFrames.getItemAtIndex(0).target,
+ gDebugger);
+
+ isnot(gFrames.selectedIndex, 1,
+ "Newest frame should not be selected after click.");
+ isnot(gClassicFrames.selectedIndex, 0,
+ "Newest frame in the mirrored view should not be selected.");
+
+ is(gFrames.selectedIndex, 0,
+ "Oldest frame should be selected after click.");
+ is(gClassicFrames.selectedIndex, 1,
+ "Oldest frame in the mirrored view should be selected.");
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ gFrames.getItemAtIndex(1).target.querySelector(".dbg-stackframe-title"),
+ gDebugger);
+ // Give the UI some time to update. For some reason if we don't
+ // do this there is global window leakage. We are continually
+ // cleaning up our tests so this will be refactored out at some
+ // point.
+ yield waitForTime(1);
+
+ is(gFrames.selectedIndex, 1,
+ "Newest frame should be selected after click inside the newest frame.");
+ is(gClassicFrames.selectedIndex, 0,
+ "Newest frame in the mirrored view should be selected.");
+
+ isnot(gFrames.selectedIndex, 0,
+ "Oldest frame should not be selected after click inside the newest frame.");
+ isnot(gClassicFrames.selectedIndex, 1,
+ "Oldest frame in the mirrored view should not be selected.");
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ gFrames.getItemAtIndex(0).target.querySelector(".dbg-stackframe-details"),
+ gDebugger);
+ // See comment above on the same statement.
+ yield waitForTime(1);
+
+ isnot(gFrames.selectedIndex, 1,
+ "Newest frame should not be selected after click inside the oldest frame.");
+ isnot(gClassicFrames.selectedIndex, 0,
+ "Newest frame in the mirrored view should not be selected.");
+
+ is(gFrames.selectedIndex, 0,
+ "Oldest frame should be selected after click inside the oldest frame.");
+ is(gClassicFrames.selectedIndex, 1,
+ "Oldest frame in the mirrored view should be selected.");
+
+ resumeDebuggerThenCloseAndFinish(gPanel);
+ });
+
+ Task.spawn(function* () {
+ yield waitForCaretAndScopes(gPanel, 1);
+ performTest();
+ });
+
+ callInTab(gTab, "evalCall");
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_stack-03.js b/devtools/client/debugger/test/mochitest/browser_dbg_stack-03.js
new file mode 100644
index 000000000..28993bfd5
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_stack-03.js
@@ -0,0 +1,64 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Test that stackframes are scrollable.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_recursion-stack.html";
+let framesScrollingInterval;
+
+function test() {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab, aDebuggee, aPanel]) => {
+ const tab = aTab;
+ const debuggee = aDebuggee;
+ const panel = aPanel;
+ const gDebugger = panel.panelWin;
+ const frames = gDebugger.DebuggerView.StackFrames;
+ const classicFrames = gDebugger.DebuggerView.StackFramesClassicList;
+
+ Task.spawn(function* () {
+ framesScrollingInterval = window.setInterval(() => {
+ frames.widget._list.scrollByIndex(-1);
+ }, 100);
+
+ yield waitForDebuggerEvents(panel, gDebugger.EVENTS.AFTER_FRAMES_REFILLED);
+
+ is(gDebugger.gThreadClient.state, "paused",
+ "Should only be getting stack frames while paused.");
+ is(frames.itemCount, gDebugger.gCallStackPageSize,
+ "Should have only the max limit of frames.");
+ is(classicFrames.itemCount, gDebugger.gCallStackPageSize,
+ "Should have only the max limit of frames in the mirrored view as well.");
+
+ yield waitForDebuggerEvents(panel, gDebugger.EVENTS.AFTER_FRAMES_REFILLED);
+
+ is(frames.itemCount, gDebugger.gCallStackPageSize * 2,
+ "Should now have twice the max limit of frames.");
+ is(classicFrames.itemCount, gDebugger.gCallStackPageSize * 2,
+ "Should now have twice the max limit of frames in the mirrored view as well.");
+
+ yield waitForDebuggerEvents(panel, gDebugger.EVENTS.AFTER_FRAMES_REFILLED);
+
+ is(frames.itemCount, debuggee.gRecurseLimit,
+ "Should have reached the recurse limit.");
+ is(classicFrames.itemCount, debuggee.gRecurseLimit,
+ "Should have reached the recurse limit in the mirrored view as well.");
+
+
+ // Call stack frame scrolling should stop before
+ // we resume the gDebugger as it could be a source of race conditions.
+ window.clearInterval(framesScrollingInterval);
+ resumeDebuggerThenCloseAndFinish(panel);
+ });
+
+ debuggee.gRecurseLimit = (gDebugger.gCallStackPageSize * 2) + 1;
+ debuggee.recurse();
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_stack-04.js b/devtools/client/debugger/test/mochitest/browser_dbg_stack-04.js
new file mode 100644
index 000000000..808ec634e
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_stack-04.js
@@ -0,0 +1,58 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Test that stackframes are cleared after resume.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_recursion-stack.html";
+
+var gTab, gPanel, gDebugger;
+var gFrames, gClassicFrames;
+
+function test() {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gFrames = gDebugger.DebuggerView.StackFrames;
+ gClassicFrames = gDebugger.DebuggerView.StackFramesClassicList;
+
+ waitForCaretAndScopes(gPanel, 1).then(performTest);
+ callInTab(gTab, "evalCall");
+ });
+}
+
+function performTest() {
+ is(gDebugger.gThreadClient.state, "paused",
+ "Should only be getting stack frames while paused.");
+ is(gFrames.itemCount, 2,
+ "Should have two frames.");
+ is(gClassicFrames.itemCount, 2,
+ "Should also have two frames in the mirrored view.");
+
+ gDebugger.once(gDebugger.EVENTS.AFTER_FRAMES_CLEARED, () => {
+ is(gFrames.itemCount, 0,
+ "Should have no frames after resume.");
+ is(gClassicFrames.itemCount, 0,
+ "Should also have no frames in the mirrored view after resume.");
+
+ closeDebuggerAndFinish(gPanel);
+ }, true);
+
+ gDebugger.gThreadClient.resume();
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gFrames = null;
+ gClassicFrames = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_stack-05.js b/devtools/client/debugger/test/mochitest/browser_dbg_stack-05.js
new file mode 100644
index 000000000..2e5648922
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_stack-05.js
@@ -0,0 +1,102 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Test that switching between stack frames properly sets the current debugger
+ * location in the source editor.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
+
+function test() {
+ let options = {
+ source: EXAMPLE_URL + "code_script-switching-01.js",
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ const gTab = aTab;
+ const gPanel = aPanel;
+ const gDebugger = gPanel.panelWin;
+ const gEditor = gDebugger.DebuggerView.editor;
+ const gSources = gDebugger.DebuggerView.Sources;
+ const gFrames = gDebugger.DebuggerView.StackFrames;
+ const gClassicFrames = gDebugger.DebuggerView.StackFramesClassicList;
+
+ function initialChecks() {
+ is(gDebugger.gThreadClient.state, "paused",
+ "Should only be getting stack frames while paused.");
+ is(gFrames.itemCount, 2,
+ "Should have four frames.");
+ is(gClassicFrames.itemCount, 2,
+ "Should also have four frames in the mirrored view.");
+ }
+
+ function testNewestFrame() {
+ is(gFrames.selectedIndex, 1,
+ "Newest frame should be selected by default.");
+ is(gClassicFrames.selectedIndex, 0,
+ "Newest frame should be selected in the mirrored view as well.");
+ is(gSources.selectedIndex, 1,
+ "The second source is selected in the widget.");
+ ok(isCaretPos(gPanel, 6),
+ "Editor caret location is correct.");
+ is(gEditor.getDebugLocation(), 5,
+ "Editor debug location is correct.");
+ }
+
+ function testOldestFrame() {
+ const shown = waitForSourceAndCaret(gPanel, "-01.js", 5).then(() => {
+ is(gFrames.selectedIndex, 0,
+ "Second frame should be selected after click.");
+ is(gClassicFrames.selectedIndex, 1,
+ "Second frame should be selected in the mirrored view as well.");
+ is(gSources.selectedIndex, 0,
+ "The first source is now selected in the widget.");
+ ok(isCaretPos(gPanel, 5),
+ "Editor caret location is correct (3).");
+ is(gEditor.getDebugLocation(), 4,
+ "Editor debug location is correct.");
+ });
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ gDebugger.document.querySelector("#stackframe-1"),
+ gDebugger);
+
+ return shown;
+ }
+
+ function testAfterResume() {
+ let deferred = promise.defer();
+
+ gDebugger.once(gDebugger.EVENTS.AFTER_FRAMES_CLEARED, () => {
+ is(gFrames.itemCount, 0,
+ "Should have no frames after resume.");
+ is(gClassicFrames.itemCount, 0,
+ "Should have no frames in the mirrored view as well.");
+ ok(isCaretPos(gPanel, 5),
+ "Editor caret location is correct after resume.");
+ is(gEditor.getDebugLocation(), null,
+ "Editor debug location is correct after resume.");
+
+ deferred.resolve();
+ }, true);
+
+ gDebugger.gThreadClient.resume();
+
+ return deferred.promise;
+ }
+
+ Task.spawn(function* () {
+ yield waitForSourceAndCaretAndScopes(gPanel, "-02.js", 6);
+ yield initialChecks();
+ yield testNewestFrame();
+ yield testOldestFrame();
+ yield testAfterResume();
+ closeDebuggerAndFinish(gPanel);
+ });
+
+ callInTab(gTab, "firstCall");
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_stack-06.js b/devtools/client/debugger/test/mochitest/browser_dbg_stack-06.js
new file mode 100644
index 000000000..11f3b9534
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_stack-06.js
@@ -0,0 +1,92 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure that selecting a stack frame loads the right source in the editor
+ * pane and highlights the proper line.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
+
+var gTab, gPanel, gDebugger;
+var gEditor, gSources, gFrames, gClassicFrames;
+
+function test() {
+ let options = {
+ source: EXAMPLE_URL + "code_script-switching-01.js",
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gEditor = gDebugger.DebuggerView.editor;
+ gSources = gDebugger.DebuggerView.Sources;
+ gFrames = gDebugger.DebuggerView.StackFrames;
+ gClassicFrames = gDebugger.DebuggerView.StackFramesClassicList;
+
+ waitForSourceAndCaretAndScopes(gPanel, "-02.js", 1).then(performTest);
+ callInTab(gTab, "firstCall");
+ });
+}
+
+function performTest() {
+ is(gFrames.selectedIndex, 1,
+ "Newest frame should be selected by default.");
+ is(gClassicFrames.selectedIndex, 0,
+ "Newest frame should also be selected in the mirrored view.");
+ is(gSources.selectedIndex, 1,
+ "The second source is selected in the widget.");
+ is(gEditor.getText().search(/firstCall/), -1,
+ "The first source is not displayed.");
+ is(gEditor.getText().search(/debugger/), 166,
+ "The second source is displayed.");
+
+ waitForSourceAndCaret(gPanel, "-01.js", 5).then(waitForTick).then(() => {
+ is(gFrames.selectedIndex, 0,
+ "Oldest frame should be selected after click.");
+ is(gClassicFrames.selectedIndex, 1,
+ "Oldest frame should also be selected in the mirrored view.");
+ is(gSources.selectedIndex, 0,
+ "The first source is now selected in the widget.");
+ is(gEditor.getText().search(/firstCall/), 118,
+ "The first source is displayed.");
+ is(gEditor.getText().search(/debugger/), -1,
+ "The second source is not displayed.");
+
+ waitForSourceAndCaret(gPanel, "-02.js", 6).then(waitForTick).then(() => {
+ is(gFrames.selectedIndex, 1,
+ "Newest frame should be selected again after click.");
+ is(gClassicFrames.selectedIndex, 0,
+ "Newest frame should also be selected again in the mirrored view.");
+ is(gSources.selectedIndex, 1,
+ "The second source is selected in the widget.");
+ is(gEditor.getText().search(/firstCall/), -1,
+ "The first source is not displayed.");
+ is(gEditor.getText().search(/debugger/), 166,
+ "The second source is displayed.");
+
+ resumeDebuggerThenCloseAndFinish(gPanel);
+ });
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ gDebugger.document.querySelector("#classic-stackframe-0"),
+ gDebugger);
+ });
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ gDebugger.document.querySelector("#stackframe-1"),
+ gDebugger);
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gEditor = null;
+ gSources = null;
+ gFrames = null;
+ gClassicFrames = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_stack-07.js b/devtools/client/debugger/test/mochitest/browser_dbg_stack-07.js
new file mode 100644
index 000000000..a5bbc5a10
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_stack-07.js
@@ -0,0 +1,113 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure that after selecting a different stack frame, resuming reselects
+ * the topmost stackframe, loads the right source in the editor pane and
+ * highlights the proper line.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
+
+var gTab, gPanel, gDebugger;
+var gEditor, gSources, gFrames, gClassicFrames, gToolbar;
+
+function test() {
+ let options = {
+ source: EXAMPLE_URL + "code_script-switching-01.js",
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gEditor = gDebugger.DebuggerView.editor;
+ gSources = gDebugger.DebuggerView.Sources;
+ gFrames = gDebugger.DebuggerView.StackFrames;
+ gClassicFrames = gDebugger.DebuggerView.StackFramesClassicList;
+ gToolbar = gDebugger.DebuggerView.Toolbar;
+
+ waitForSourceAndCaretAndScopes(gPanel, "-02.js", 1).then(performTest);
+ callInTab(gTab, "firstCall");
+ });
+}
+
+function performTest() {
+ return Task.spawn(function* () {
+ yield selectBottomFrame();
+ testBottomFrame(4);
+
+ yield performStep("StepOver");
+ testTopFrame(1);
+
+ yield selectBottomFrame();
+ testBottomFrame(4);
+
+ yield performStep("StepIn");
+ testTopFrame(1);
+
+ yield selectBottomFrame();
+ testBottomFrame(4);
+
+ yield performStep("StepOut");
+ testTopFrame(1);
+
+ yield resumeDebuggerThenCloseAndFinish(gPanel);
+ });
+
+ function selectBottomFrame() {
+ let shown = waitForSourceShown(gPanel, "-01.js");
+ gClassicFrames.selectedIndex = gClassicFrames.itemCount - 1;
+ return shown;
+ }
+
+ function testBottomFrame(debugLocation) {
+ is(gFrames.selectedIndex, 0,
+ "Oldest frame should be selected after click.");
+ is(gClassicFrames.selectedIndex, gFrames.itemCount - 1,
+ "Oldest frame should also be selected in the mirrored view.");
+ is(gSources.selectedIndex, 0,
+ "The first source is now selected in the widget.");
+ is(gEditor.getText().search(/firstCall/), 118,
+ "The first source is displayed.");
+ is(gEditor.getText().search(/debugger/), -1,
+ "The second source is not displayed.");
+
+ is(gEditor.getDebugLocation(), debugLocation,
+ "Editor debugger location is correct.");
+ ok(gEditor.hasLineClass(debugLocation, "debug-line"),
+ "The debugged line is highlighted appropriately.");
+ }
+
+ function performStep(type) {
+ let updated = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES);
+ gToolbar["_on" + type + "Pressed"]();
+ return updated.then(waitForTick);
+ }
+
+ function testTopFrame(frameIndex) {
+ is(gFrames.selectedIndex, frameIndex,
+ "Topmost frame should be selected after click.");
+ is(gClassicFrames.selectedIndex, gFrames.itemCount - frameIndex - 1,
+ "Topmost frame should also be selected in the mirrored view.");
+ is(gSources.selectedIndex, 1,
+ "The second source is now selected in the widget.");
+ is(gEditor.getText().search(/firstCall/), -1,
+ "The second source is displayed.");
+ is(gEditor.getText().search(/debugger/), 166,
+ "The first source is not displayed.");
+ }
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gEditor = null;
+ gSources = null;
+ gFrames = null;
+ gClassicFrames = null;
+ gToolbar = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_stack-contextmenu-01.js b/devtools/client/debugger/test/mochitest/browser_dbg_stack-contextmenu-01.js
new file mode 100644
index 000000000..61d964b91
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_stack-contextmenu-01.js
@@ -0,0 +1,58 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Test that the copy contextmenu has been added to the stack frames view.
+ */
+
+ const TAB_URL = EXAMPLE_URL + "doc_recursion-stack.html";
+ let gTab, gPanel, gDebugger;
+ let gFrames, gContextMenu;
+
+ function test() {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gFrames = gDebugger.DebuggerView.StackFrames;
+
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.AFTER_FRAMES_REFILLED)
+ .then(performTest);
+ callInTab(gTab, "simpleCall");
+ });
+ }
+
+ function performTest() {
+ gContextMenu = gDebugger.document.getElementById("stackFramesContextMenu");
+ is(gDebugger.gThreadClient.state, "paused",
+ "Should only be getting stack frames while paused.");
+ is(gFrames.itemCount, 1,
+ "Should have only one frame.");
+ ok(gContextMenu, "The stack frame's context menupopup is available.");
+
+ once(gContextMenu, "popupshown").then(testContextMenu);
+ EventUtils.synthesizeMouseAtCenter(gFrames.getItemAtIndex(0).prebuiltNode, {type: "contextmenu", button: 2}, gDebugger);
+ }
+
+ function testContextMenu() {
+ let document = gDebugger.document;
+ ok(document.getElementById("copyStackMenuItem"),
+ "#copyStackMenuItem found.");
+
+ gContextMenu.hidePopup();
+ resumeDebuggerThenCloseAndFinish(gPanel);
+ }
+
+ registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gFrames = null;
+ gContextMenu = null;
+ });
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_stack-contextmenu-02.js b/devtools/client/debugger/test/mochitest/browser_dbg_stack-contextmenu-02.js
new file mode 100644
index 000000000..828bce6c8
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_stack-contextmenu-02.js
@@ -0,0 +1,58 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Test that the copy contextmenu copys the stack frames to the clipboard.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_recursion-stack.html";
+const STACK_STRING = "simpleCall@" + EXAMPLE_URL + "doc_recursion-stack.html:14:8";
+
+function test() {
+ let gTab, gPanel, gDebugger, gFrames;
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gFrames = gDebugger.DebuggerView.StackFrames;
+
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.AFTER_FRAMES_REFILLED)
+ .then(openContextMenu)
+ .then(testCopyStackMenuItem)
+ .then(() => resumeDebuggerThenCloseAndFinish(gPanel))
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+ callInTab(gTab, "simpleCall");
+ });
+
+ function clickCopyStack() {
+ return new Promise((resolve, reject) => {
+ let copyStackMenuItem = gDebugger.document.getElementById("copyStackMenuItem");
+ if (!copyStackMenuItem) {
+ reject(new Error("The Copy stack context menu item is not available."));
+ }
+
+ ok(copyStackMenuItem, "The Copy stack context menu item is available.");
+ EventUtils.synthesizeMouseAtCenter(copyStackMenuItem, {}, gDebugger);
+ resolve();
+ });
+ }
+
+ function testCopyStackMenuItem() {
+ return waitForClipboardPromise(clickCopyStack, STACK_STRING);
+ }
+
+ function openContextMenu() {
+ let contextMenu = gDebugger.document.getElementById("stackFramesContextMenu");
+ let contextMenuShown = once(contextMenu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(gFrames.getItemAtIndex(0).prebuiltNode, {type: "contextmenu", button: 2}, gDebugger);
+ return contextMenuShown;
+ }
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_step-out.js b/devtools/client/debugger/test/mochitest/browser_dbg_step-out.js
new file mode 100644
index 000000000..ae1099a92
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_step-out.js
@@ -0,0 +1,91 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure that stepping out of a function displays the right return value.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_step-out.html";
+
+var gTab, gPanel, gDebugger;
+var gVars;
+
+function test() {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gVars = gDebugger.DebuggerView.Variables;
+
+ testNormalReturn();
+ });
+}
+
+function testNormalReturn() {
+ waitForCaretAndScopes(gPanel, 17).then(() => {
+ waitForCaretAndScopes(gPanel, 20).then(() => {
+ let innerScope = gVars.getScopeAtIndex(0);
+ let returnVar = innerScope.get("<return>");
+
+ is(returnVar.name, "<return>",
+ "Should have the right property name for the returned value.");
+ is(returnVar.value, 10,
+ "Should have the right property value for the returned value.");
+ ok(returnVar._internalItem, "Should be an internal item");
+ ok(returnVar._target.hasAttribute("pseudo-item"),
+ "Element should be marked as a pseudo-item");
+
+ resumeDebuggee().then(() => testReturnWithException());
+ });
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ gDebugger.document.getElementById("step-out"),
+ gDebugger);
+ });
+
+ generateMouseClickInTab(gTab, "content.document.getElementById('return')");
+}
+
+function testReturnWithException() {
+ waitForCaretAndScopes(gPanel, 24).then(() => {
+ waitForCaretAndScopes(gPanel, 26).then(() => {
+ let innerScope = gVars.getScopeAtIndex(0);
+ let exceptionVar = innerScope.get("<exception>");
+
+ is(exceptionVar.name, "<exception>",
+ "Should have the right property name for the returned value.");
+ is(exceptionVar.value, "boom",
+ "Should have the right property value for the returned value.");
+ ok(exceptionVar._internalItem, "Should be an internal item");
+ ok(exceptionVar._target.hasAttribute("pseudo-item"),
+ "Element should be marked as a pseudo-item");
+
+ resumeDebuggee().then(() => closeDebuggerAndFinish(gPanel));
+ });
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ gDebugger.document.getElementById("step-out"),
+ gDebugger);
+ });
+
+ generateMouseClickInTab(gTab, "content.document.getElementById('throw')");
+}
+
+function resumeDebuggee() {
+ let deferred = promise.defer();
+ gDebugger.gThreadClient.resume(deferred.resolve);
+ return deferred.promise;
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gVars = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_tabactor-01.js b/devtools/client/debugger/test/mochitest/browser_dbg_tabactor-01.js
new file mode 100644
index 000000000..dfb073617
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_tabactor-01.js
@@ -0,0 +1,65 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Check extension-added tab actor lifetimes.
+ */
+
+const ACTORS_URL = CHROME_URL + "testactors.js";
+const TAB_URL = EXAMPLE_URL + "doc_empty-tab-01.html";
+
+var gClient;
+
+function test() {
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+
+ DebuggerServer.addActors(ACTORS_URL);
+
+ let transport = DebuggerServer.connectPipe();
+ gClient = new DebuggerClient(transport);
+ gClient.connect().then(([aType, aTraits]) => {
+ is(aType, "browser",
+ "Root actor should identify itself as a browser.");
+
+ addTab(TAB_URL)
+ .then(() => attachTabActorForUrl(gClient, TAB_URL))
+ .then(testTabActor)
+ .then(closeTab)
+ .then(() => gClient.close())
+ .then(finish)
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+ });
+}
+
+function testTabActor([aGrip, aResponse]) {
+ let deferred = promise.defer();
+
+ ok(aGrip.testTabActor1,
+ "Found the test tab actor.");
+ ok(aGrip.testTabActor1.includes("test_one"),
+ "testTabActor1's actorPrefix should be used.");
+
+ gClient.request({ to: aGrip.testTabActor1, type: "ping" }, aResponse => {
+ is(aResponse.pong, "pong",
+ "Actor should respond to requests.");
+
+ deferred.resolve();
+ });
+
+ return deferred.promise;
+}
+
+function closeTab() {
+ return removeTab(gBrowser.selectedTab);
+}
+
+registerCleanupFunction(function () {
+ gClient = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_tabactor-02.js b/devtools/client/debugger/test/mochitest/browser_dbg_tabactor-02.js
new file mode 100644
index 000000000..c9f506db2
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_tabactor-02.js
@@ -0,0 +1,79 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Check extension-added tab actor lifetimes.
+ */
+
+const ACTORS_URL = CHROME_URL + "testactors.js";
+const TAB_URL = EXAMPLE_URL + "doc_empty-tab-01.html";
+
+var gClient;
+
+function test() {
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+
+ DebuggerServer.addActors(ACTORS_URL);
+
+ let transport = DebuggerServer.connectPipe();
+ gClient = new DebuggerClient(transport);
+ gClient.connect().then(([aType, aTraits]) => {
+ is(aType, "browser",
+ "Root actor should identify itself as a browser.");
+
+ addTab(TAB_URL)
+ .then(() => attachTabActorForUrl(gClient, TAB_URL))
+ .then(testTabActor)
+ .then(closeTab)
+ .then(() => gClient.close())
+ .then(finish)
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+ });
+}
+
+function testTabActor([aGrip, aResponse]) {
+ let deferred = promise.defer();
+
+ ok(aGrip.testTabActor1,
+ "Found the test tab actor.");
+ ok(aGrip.testTabActor1.includes("test_one"),
+ "testTabActor1's actorPrefix should be used.");
+
+ gClient.request({ to: aGrip.testTabActor1, type: "ping" }, aResponse => {
+ is(aResponse.pong, "pong",
+ "Actor should respond to requests.");
+
+ deferred.resolve(aResponse.actor);
+ });
+
+ return deferred.promise;
+}
+
+function closeTab(aTestActor) {
+ return removeTab(gBrowser.selectedTab).then(() => {
+ let deferred = promise.defer();
+
+ try {
+ gClient.request({ to: aTestActor, type: "ping" }, aResponse => {
+ ok(false, "testTabActor1 didn't go away with the tab.");
+ deferred.reject(aResponse);
+ });
+ } catch (e) {
+ is(e.message, "'ping' request packet has no destination.", "testTabActor1 went away.");
+ deferred.resolve();
+ }
+
+ return deferred.promise;
+ });
+}
+
+registerCleanupFunction(function () {
+ gClient = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_terminate-on-tab-close.js b/devtools/client/debugger/test/mochitest/browser_dbg_terminate-on-tab-close.js
new file mode 100644
index 000000000..42a1e6c70
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_terminate-on-tab-close.js
@@ -0,0 +1,34 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Whitelisting this test.
+// As part of bug 1077403, the leaking uncaught rejections should be fixed.
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed("[object Object]");
+
+/**
+ * Tests that debuggee scripts are terminated on tab closure.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_terminate-on-tab-close.html";
+
+function test() {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ const gTab = aTab;
+ const gPanel = aPanel;
+ const gDebugger = gPanel.panelWin;
+
+ gDebugger.gThreadClient.addOneTimeListener("paused", () => {
+ resumeDebuggerThenCloseAndFinish(gPanel).then(function () {
+ ok(true, "should not throw after this point");
+ });
+ });
+
+ callInTab(gTab, "debuggerThenThrow");
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-01.js b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-01.js
new file mode 100644
index 000000000..c5c846978
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-01.js
@@ -0,0 +1,132 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests that creating, collpasing and expanding scopes in the
+ * variables view works as expected.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_recursion-stack.html";
+
+function test() {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ let variables = aPanel.panelWin.DebuggerView.Variables;
+ let testScope = variables.addScope("test");
+
+ ok(testScope,
+ "Should have created a scope.");
+ ok(testScope.id.includes("test"),
+ "The newly created scope should have the default id set.");
+ is(testScope.name, "test",
+ "The newly created scope should have the desired name set.");
+
+ ok(!testScope.displayValue,
+ "The newly created scope should not have a displayed value (1).");
+ ok(!testScope.displayValueClassName,
+ "The newly created scope should not have a displayed value (2).");
+
+ ok(testScope.target,
+ "The newly created scope should point to a target node.");
+ ok(testScope.target.id.includes("test"),
+ "Should have the correct scope id on the element.");
+
+ is(testScope.target.querySelector(".name").getAttribute("value"), "test",
+ "Any new scope should have the designated name.");
+ is(testScope.target.querySelector(".variables-view-element-details.enum").childNodes.length, 0,
+ "Any new scope should have a container with no enumerable child nodes.");
+ is(testScope.target.querySelector(".variables-view-element-details.nonenum").childNodes.length, 0,
+ "Any new scope should have a container with no non-enumerable child nodes.");
+
+ ok(!testScope.expanded,
+ "Any new created scope should be initially collapsed.");
+ ok(testScope.visible,
+ "Any new created scope should be initially visible.");
+
+ let expandCallbackArg = null;
+ let collapseCallbackArg = null;
+ let toggleCallbackArg = null;
+ let hideCallbackArg = null;
+ let showCallbackArg = null;
+
+ testScope.onexpand = aScope => expandCallbackArg = aScope;
+ testScope.oncollapse = aScope => collapseCallbackArg = aScope;
+ testScope.ontoggle = aScope => toggleCallbackArg = aScope;
+ testScope.onhide = aScope => hideCallbackArg = aScope;
+ testScope.onshow = aScope => showCallbackArg = aScope;
+
+ testScope.expand();
+ ok(testScope.expanded,
+ "The testScope shouldn't be collapsed anymore.");
+ is(expandCallbackArg, testScope,
+ "The expandCallback wasn't called as it should.");
+
+ testScope.collapse();
+ ok(!testScope.expanded,
+ "The testScope should be collapsed again.");
+ is(collapseCallbackArg, testScope,
+ "The collapseCallback wasn't called as it should.");
+
+ testScope.expanded = true;
+ ok(testScope.expanded,
+ "The testScope shouldn't be collapsed anymore.");
+
+ testScope.toggle();
+ ok(!testScope.expanded,
+ "The testScope should be collapsed again.");
+ is(toggleCallbackArg, testScope,
+ "The toggleCallback wasn't called as it should.");
+
+ testScope.hide();
+ ok(!testScope.visible,
+ "The testScope should be invisible after hiding.");
+ is(hideCallbackArg, testScope,
+ "The hideCallback wasn't called as it should.");
+
+ testScope.show();
+ ok(testScope.visible,
+ "The testScope should be visible again.");
+ is(showCallbackArg, testScope,
+ "The showCallback wasn't called as it should.");
+
+ testScope.visible = false;
+ ok(!testScope.visible,
+ "The testScope should be invisible after hiding.");
+ ok(!testScope.expanded,
+ "The testScope should remember it is collapsed even if it is hidden.");
+
+ testScope.visible = true;
+ ok(testScope.visible,
+ "The testScope should be visible after reshowing.");
+ ok(!testScope.expanded,
+ "The testScope should remember it is collapsed after it is reshown.");
+
+ EventUtils.sendMouseEvent({ type: "mousedown", button: 1 },
+ testScope.target.querySelector(".title"),
+ aPanel.panelWin);
+
+ ok(!testScope.expanded,
+ "Clicking the testScope title with the right mouse button should't expand it.");
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ testScope.target.querySelector(".title"),
+ aPanel.panelWin);
+
+ ok(testScope.expanded,
+ "Clicking the testScope title should expand it.");
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ testScope.target.querySelector(".title"),
+ aPanel.panelWin);
+
+ ok(!testScope.expanded,
+ "Clicking again the testScope title should collapse it.");
+
+ closeDebuggerAndFinish(aPanel);
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-02.js b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-02.js
new file mode 100644
index 000000000..f97353dba
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-02.js
@@ -0,0 +1,227 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests that creating, collapsing and expanding variables in the
+ * variables view works as expected.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_recursion-stack.html";
+
+function test() {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ let variables = aPanel.panelWin.DebuggerView.Variables;
+ let testScope = variables.addScope("test");
+ let testVar = testScope.addItem("something");
+ let duplVar = testScope.addItem("something");
+
+ info("Scope id: " + testScope.id);
+ info("Scope name: " + testScope.name);
+ info("Variable id: " + testVar.id);
+ info("Variable name: " + testVar.name);
+
+ ok(testScope,
+ "Should have created a scope.");
+ is(duplVar, testVar,
+ "Shouldn't be able to duplicate variables in the same scope.");
+
+ ok(testVar,
+ "Should have created a variable.");
+ ok(testVar.id.includes("something"),
+ "The newly created variable should have the default id set.");
+ is(testVar.name, "something",
+ "The newly created variable should have the desired name set.");
+
+ ok(!testVar.displayValue,
+ "The newly created variable should not have a displayed value yet (1).");
+ ok(!testVar.displayValueClassName,
+ "The newly created variable should not have a displayed value yet (2).");
+
+ ok(testVar.target,
+ "The newly created scope should point to a target node.");
+ ok(testVar.target.id.includes("something"),
+ "Should have the correct variable id on the element.");
+
+ is(testVar.target.querySelector(".name").getAttribute("value"), "something",
+ "Any new variable should have the designated name.");
+ is(testVar.target.querySelector(".variables-view-element-details.enum").childNodes.length, 0,
+ "Any new variable should have a container with no enumerable child nodes.");
+ is(testVar.target.querySelector(".variables-view-element-details.nonenum").childNodes.length, 0,
+ "Any new variable should have a container with no non-enumerable child nodes.");
+
+ ok(!testVar.expanded,
+ "Any new created scope should be initially collapsed.");
+ ok(testVar.visible,
+ "Any new created scope should be initially visible.");
+
+ let expandCallbackArg = null;
+ let collapseCallbackArg = null;
+ let toggleCallbackArg = null;
+ let hideCallbackArg = null;
+ let showCallbackArg = null;
+
+ testVar.onexpand = aScope => expandCallbackArg = aScope;
+ testVar.oncollapse = aScope => collapseCallbackArg = aScope;
+ testVar.ontoggle = aScope => toggleCallbackArg = aScope;
+ testVar.onhide = aScope => hideCallbackArg = aScope;
+ testVar.onshow = aScope => showCallbackArg = aScope;
+
+ testVar.expand();
+ ok(testVar.expanded,
+ "The testVar shouldn't be collapsed anymore.");
+ is(expandCallbackArg, testVar,
+ "The expandCallback wasn't called as it should.");
+
+ testVar.collapse();
+ ok(!testVar.expanded,
+ "The testVar should be collapsed again.");
+ is(collapseCallbackArg, testVar,
+ "The collapseCallback wasn't called as it should.");
+
+ testVar.expanded = true;
+ ok(testVar.expanded,
+ "The testVar shouldn't be collapsed anymore.");
+
+ testVar.toggle();
+ ok(!testVar.expanded,
+ "The testVar should be collapsed again.");
+ is(toggleCallbackArg, testVar,
+ "The toggleCallback wasn't called as it should.");
+
+ testVar.hide();
+ ok(!testVar.visible,
+ "The testVar should be invisible after hiding.");
+ is(hideCallbackArg, testVar,
+ "The hideCallback wasn't called as it should.");
+
+ testVar.show();
+ ok(testVar.visible,
+ "The testVar should be visible again.");
+ is(showCallbackArg, testVar,
+ "The showCallback wasn't called as it should.");
+
+ testVar.visible = false;
+ ok(!testVar.visible,
+ "The testVar should be invisible after hiding.");
+ ok(!testVar.expanded,
+ "The testVar should remember it is collapsed even if it is hidden.");
+
+ testVar.visible = true;
+ ok(testVar.visible,
+ "The testVar should be visible after reshowing.");
+ ok(!testVar.expanded,
+ "The testVar should remember it is collapsed after it is reshown.");
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ testVar.target.querySelector(".name"),
+ aPanel.panelWin);
+
+ ok(testVar.expanded,
+ "Clicking the testVar name should expand it.");
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ testVar.target.querySelector(".name"),
+ aPanel.panelWin);
+
+ ok(!testVar.expanded,
+ "Clicking again the testVar name should collapse it.");
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ testVar.target.querySelector(".arrow"),
+ aPanel.panelWin);
+
+ ok(testVar.expanded,
+ "Clicking the testVar arrow should expand it.");
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ testVar.target.querySelector(".arrow"),
+ aPanel.panelWin);
+
+ ok(!testVar.expanded,
+ "Clicking again the testVar arrow should collapse it.");
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ testVar.target.querySelector(".title"),
+ aPanel.panelWin);
+
+ ok(testVar.expanded,
+ "Clicking the testVar title should expand it again.");
+
+ testVar.addItem("child", {
+ value: {
+ type: "object",
+ class: "Object"
+ }
+ });
+
+ let testChild = testVar.get("child");
+ ok(testChild,
+ "Should have created a child property.");
+ ok(testChild.id.includes("child"),
+ "The newly created property should have the default id set.");
+ is(testChild.name, "child",
+ "The newly created property should have the desired name set.");
+
+ is(testChild.displayValue, "Object",
+ "The newly created property should not have a displayed value yet (1).");
+ is(testChild.displayValueClassName, "token-other",
+ "The newly created property should not have a displayed value yet (2).");
+
+ ok(testChild.target,
+ "The newly created scope should point to a target node.");
+ ok(testChild.target.id.includes("child"),
+ "Should have the correct property id on the element.");
+
+ is(testChild.target.querySelector(".name").getAttribute("value"), "child",
+ "Any new property should have the designated name.");
+ is(testChild.target.querySelector(".variables-view-element-details.enum").childNodes.length, 0,
+ "Any new property should have a container with no enumerable child nodes.");
+ is(testChild.target.querySelector(".variables-view-element-details.nonenum").childNodes.length, 0,
+ "Any new property should have a container with no non-enumerable child nodes.");
+
+ ok(!testChild.expanded,
+ "Any new created scope should be initially collapsed.");
+ ok(testChild.visible,
+ "Any new created scope should be initially visible.");
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ testChild.target.querySelector(".name"),
+ aPanel.panelWin);
+
+ ok(testChild.expanded,
+ "Clicking the testChild name should expand it.");
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ testChild.target.querySelector(".name"),
+ aPanel.panelWin);
+
+ ok(!testChild.expanded,
+ "Clicking again the testChild name should collapse it.");
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ testChild.target.querySelector(".arrow"),
+ aPanel.panelWin);
+
+ ok(testChild.expanded,
+ "Clicking the testChild arrow should expand it.");
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ testChild.target.querySelector(".arrow"),
+ aPanel.panelWin);
+
+ ok(!testChild.expanded,
+ "Clicking again the testChild arrow should collapse it.");
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ testChild.target.querySelector(".title"),
+ aPanel.panelWin);
+
+ closeDebuggerAndFinish(aPanel);
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-03.js b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-03.js
new file mode 100644
index 000000000..64e4d45a2
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-03.js
@@ -0,0 +1,157 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests that recursively creating properties in the variables view works
+ * as expected.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_recursion-stack.html";
+
+function test() {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ let variables = aPanel.panelWin.DebuggerView.Variables;
+ let testScope = variables.addScope("test");
+
+ is(testScope.target.querySelectorAll(".variables-view-element-details.enum").length, 1,
+ "One enumerable container should be present in the scope.");
+ is(testScope.target.querySelectorAll(".variables-view-element-details.nonenum").length, 1,
+ "One non-enumerable container should be present in the scope.");
+ is(testScope.target.querySelector(".variables-view-element-details.enum").childNodes.length, 0,
+ "No enumerable variables should be present in the scope.");
+ is(testScope.target.querySelector(".variables-view-element-details.nonenum").childNodes.length, 0,
+ "No non-enumerable variables should be present in the scope.");
+
+ testScope.addItem("something", {
+ value: {
+ type: "object",
+ class: "Object"
+ },
+ enumerable: true
+ });
+
+ is(testScope.target.querySelectorAll(".variables-view-element-details.enum").length, 2,
+ "Two enumerable containers should be present in the tree.");
+ is(testScope.target.querySelectorAll(".variables-view-element-details.nonenum").length, 2,
+ "Two non-enumerable containers should be present in the tree.");
+
+ is(testScope.target.querySelector(".variables-view-element-details.enum").childNodes.length, 1,
+ "A new enumerable variable should have been added in the scope.");
+ is(testScope.target.querySelector(".variables-view-element-details.nonenum").childNodes.length, 0,
+ "No new non-enumerable variables should have been added in the scope.");
+
+ let testVar = testScope.get("something");
+ ok(testVar,
+ "The added variable should be accessible from the scope.");
+
+ is(testVar.target.querySelectorAll(".variables-view-element-details.enum").length, 1,
+ "One enumerable container should be present in the variable.");
+ is(testVar.target.querySelectorAll(".variables-view-element-details.nonenum").length, 1,
+ "One non-enumerable container should be present in the variable.");
+ is(testVar.target.querySelector(".variables-view-element-details.enum").childNodes.length, 0,
+ "No enumerable properties should be present in the variable.");
+ is(testVar.target.querySelector(".variables-view-element-details.nonenum").childNodes.length, 0,
+ "No non-enumerable properties should be present in the variable.");
+
+ testVar.addItem("child", {
+ value: {
+ type: "object",
+ class: "Object"
+ },
+ enumerable: true
+ });
+
+ is(testScope.target.querySelectorAll(".variables-view-element-details.enum").length, 3,
+ "Three enumerable containers should be present in the tree.");
+ is(testScope.target.querySelectorAll(".variables-view-element-details.nonenum").length, 3,
+ "Three non-enumerable containers should be present in the tree.");
+
+ is(testVar.target.querySelector(".variables-view-element-details.enum").childNodes.length, 1,
+ "A new enumerable property should have been added in the variable.");
+ is(testVar.target.querySelector(".variables-view-element-details.nonenum").childNodes.length, 0,
+ "No new non-enumerable properties should have been added in the variable.");
+
+ let testChild = testVar.get("child");
+ ok(testChild,
+ "The added property should be accessible from the variable.");
+
+ is(testChild.target.querySelectorAll(".variables-view-element-details.enum").length, 1,
+ "One enumerable container should be present in the property.");
+ is(testChild.target.querySelectorAll(".variables-view-element-details.nonenum").length, 1,
+ "One non-enumerable container should be present in the property.");
+ is(testChild.target.querySelector(".variables-view-element-details.enum").childNodes.length, 0,
+ "No enumerable sub-properties should be present in the property.");
+ is(testChild.target.querySelector(".variables-view-element-details.nonenum").childNodes.length, 0,
+ "No non-enumerable sub-properties should be present in the property.");
+
+ testChild.addItem("grandChild", {
+ value: {
+ type: "object",
+ class: "Object"
+ },
+ enumerable: true
+ });
+
+ is(testScope.target.querySelectorAll(".variables-view-element-details.enum").length, 4,
+ "Four enumerable containers should be present in the tree.");
+ is(testScope.target.querySelectorAll(".variables-view-element-details.nonenum").length, 4,
+ "Four non-enumerable containers should be present in the tree.");
+
+ is(testChild.target.querySelector(".variables-view-element-details.enum").childNodes.length, 1,
+ "A new enumerable sub-property should have been added in the property.");
+ is(testChild.target.querySelector(".variables-view-element-details.nonenum").childNodes.length, 0,
+ "No new non-enumerable sub-properties should have been added in the property.");
+
+ let testGrandChild = testChild.get("grandChild");
+ ok(testGrandChild,
+ "The added sub-property should be accessible from the property.");
+
+ is(testGrandChild.target.querySelectorAll(".variables-view-element-details.enum").length, 1,
+ "One enumerable container should be present in the property.");
+ is(testGrandChild.target.querySelectorAll(".variables-view-element-details.nonenum").length, 1,
+ "One non-enumerable container should be present in the property.");
+ is(testGrandChild.target.querySelector(".variables-view-element-details.enum").childNodes.length, 0,
+ "No enumerable sub-properties should be present in the property.");
+ is(testGrandChild.target.querySelector(".variables-view-element-details.nonenum").childNodes.length, 0,
+ "No non-enumerable sub-properties should be present in the property.");
+
+ testGrandChild.addItem("granderChild", {
+ value: {
+ type: "object",
+ class: "Object"
+ },
+ enumerable: true
+ });
+
+ is(testScope.target.querySelectorAll(".variables-view-element-details.enum").length, 5,
+ "Five enumerable containers should be present in the tree.");
+ is(testScope.target.querySelectorAll(".variables-view-element-details.nonenum").length, 5,
+ "Five non-enumerable containers should be present in the tree.");
+
+ is(testGrandChild.target.querySelector(".variables-view-element-details.enum").childNodes.length, 1,
+ "A new enumerable variable should have been added in the variable.");
+ is(testGrandChild.target.querySelector(".variables-view-element-details.nonenum").childNodes.length, 0,
+ "No new non-enumerable variables should have been added in the variable.");
+
+ let testGranderChild = testGrandChild.get("granderChild");
+ ok(testGranderChild,
+ "The added sub-property should be accessible from the property.");
+
+ is(testGranderChild.target.querySelectorAll(".variables-view-element-details.enum").length, 1,
+ "One enumerable container should be present in the property.");
+ is(testGranderChild.target.querySelectorAll(".variables-view-element-details.nonenum").length, 1,
+ "One non-enumerable container should be present in the property.");
+ is(testGranderChild.target.querySelector(".variables-view-element-details.enum").childNodes.length, 0,
+ "No enumerable sub-properties should be present in the property.");
+ is(testGranderChild.target.querySelector(".variables-view-element-details.nonenum").childNodes.length, 0,
+ "No non-enumerable sub-properties should be present in the property.");
+
+ closeDebuggerAndFinish(aPanel);
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-04.js b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-04.js
new file mode 100644
index 000000000..9db8b9cb8
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-04.js
@@ -0,0 +1,156 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests that grips are correctly applied to variables.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_recursion-stack.html";
+
+function test() {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ let variables = aPanel.panelWin.DebuggerView.Variables;
+ let testScope = variables.addScope("test");
+ let testVar = testScope.addItem("something");
+
+ testVar.setGrip(1.618);
+
+ is(testVar.target.querySelector(".value").getAttribute("value"), "1.618",
+ "The grip information for the variable wasn't set correctly (1).");
+ is(testVar.target.querySelector(".variables-view-element-details.enum").childNodes.length, 0,
+ "Setting the grip alone shouldn't add any new tree nodes (1).");
+ is(testVar.target.querySelector(".variables-view-element-details.nonenum").childNodes.length, 0,
+ "Setting the grip alone shouldn't add any new tree nodes (2).");
+
+ testVar.setGrip({
+ type: "object",
+ class: "Window"
+ });
+
+ is(testVar.target.querySelector(".value").getAttribute("value"), "Window",
+ "The grip information for the variable wasn't set correctly (2).");
+ is(testVar.target.querySelector(".variables-view-element-details.enum").childNodes.length, 0,
+ "Setting the grip alone shouldn't add any new tree nodes (3).");
+ is(testVar.target.querySelector(".variables-view-element-details.nonenum").childNodes.length, 0,
+ "Setting the grip alone shouldn't add any new tree nodes (4).");
+
+ testVar.addItems({
+ helloWorld: {
+ value: "hello world",
+ enumerable: true
+ }
+ });
+
+ is(testVar.target.querySelector(".variables-view-element-details").childNodes.length, 1,
+ "A new detail node should have been added in the variable tree.");
+ is(testVar.get("helloWorld").target.querySelector(".value").getAttribute("value"), "\"hello world\"",
+ "The grip information for the variable wasn't set correctly (3).");
+
+ testVar.addItems({
+ helloWorld: {
+ value: "hello jupiter",
+ enumerable: true
+ }
+ });
+
+ is(testVar.target.querySelector(".variables-view-element-details").childNodes.length, 1,
+ "Shouldn't be able to duplicate nodes added in the variable tree.");
+ is(testVar.get("helloWorld").target.querySelector(".value").getAttribute("value"), "\"hello world\"",
+ "The grip information for the variable wasn't preserved correctly (4).");
+
+ testVar.addItems({
+ someProp0: {
+ value: "random string",
+ enumerable: true
+ },
+ someProp1: {
+ value: "another string",
+ enumerable: true
+ }
+ });
+
+ is(testVar.target.querySelector(".variables-view-element-details").childNodes.length, 3,
+ "Two new detail nodes should have been added in the variable tree.");
+ is(testVar.get("someProp0").target.querySelector(".value").getAttribute("value"), "\"random string\"",
+ "The grip information for the variable wasn't set correctly (5).");
+ is(testVar.get("someProp1").target.querySelector(".value").getAttribute("value"), "\"another string\"",
+ "The grip information for the variable wasn't set correctly (6).");
+
+ testVar.addItems({
+ someProp2: {
+ value: {
+ type: "null"
+ },
+ enumerable: true
+ },
+ someProp3: {
+ value: {
+ type: "undefined"
+ },
+ enumerable: true
+ },
+ someProp4: {
+ value: {
+ type: "object",
+ class: "Object"
+ },
+ enumerable: true
+ }
+ });
+
+ is(testVar.target.querySelector(".variables-view-element-details").childNodes.length, 6,
+ "Three new detail nodes should have been added in the variable tree.");
+ is(testVar.get("someProp2").target.querySelector(".value").getAttribute("value"), "null",
+ "The grip information for the variable wasn't set correctly (7).");
+ is(testVar.get("someProp3").target.querySelector(".value").getAttribute("value"), "undefined",
+ "The grip information for the variable wasn't set correctly (8).");
+ is(testVar.get("someProp4").target.querySelector(".value").getAttribute("value"), "Object",
+ "The grip information for the variable wasn't set correctly (9).");
+
+ let parent = testVar.get("someProp2");
+ let child = parent.addItem("child", {
+ value: {
+ type: "null"
+ }
+ });
+
+ is(variables.getItemForNode(parent.target), parent,
+ "VariablesView should have a record of the parent.");
+ is(variables.getItemForNode(child.target), child,
+ "VariablesView should have a record of the child.");
+ is([...parent].length, 1,
+ "Parent should have one child.");
+
+ parent.remove();
+
+ is(variables.getItemForNode(parent.target), undefined,
+ "VariablesView should not have a record of the parent anymore.");
+ is(parent.target.parentNode, null,
+ "Parent element should not have a parent.");
+ is(variables.getItemForNode(child.target), undefined,
+ "VariablesView should not have a record of the child anymore.");
+ is(child.target.parentNode, null,
+ "Child element should not have a parent.");
+ is([...parent].length, 0,
+ "Parent should have zero children.");
+
+ testScope.remove();
+
+ is([...variables].length, 0,
+ "VariablesView should have been emptied.");
+ is(ThreadSafeChromeUtils.nondeterministicGetWeakMapKeys(variables._itemsByElement).length,
+ 0, "VariablesView _itemsByElement map has been emptied.");
+ is(variables._currHierarchy.size, 0,
+ "VariablesView _currHierarchy map has been emptied.");
+ is(variables._list.children.length, 0,
+ "VariablesView element should have no children.");
+
+ closeDebuggerAndFinish(aPanel);
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-05.js b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-05.js
new file mode 100644
index 000000000..ebad7c4e2
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-05.js
@@ -0,0 +1,234 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests that grips are correctly applied to variables and properties.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_recursion-stack.html";
+
+function test() {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ let variables = aPanel.panelWin.DebuggerView.Variables;
+
+ let globalScope = variables.addScope("Test-Global");
+ let localScope = variables.addScope("Test-Local");
+
+ ok(globalScope, "The globalScope hasn't been created correctly.");
+ ok(localScope, "The localScope hasn't been created correctly.");
+
+ is(globalScope.target.querySelector(".separator"), null,
+ "No separator string should be created for scopes (1).");
+ is(localScope.target.querySelector(".separator"), null,
+ "No separator string should be created for scopes (2).");
+
+ let windowVar = globalScope.addItem("window");
+ let documentVar = globalScope.addItem("document");
+
+ ok(windowVar, "The windowVar hasn't been created correctly.");
+ ok(documentVar, "The documentVar hasn't been created correctly.");
+
+ ok(windowVar.target.querySelector(".separator").hidden,
+ "No separator string should be shown for variables without a grip (1).");
+ ok(documentVar.target.querySelector(".separator").hidden,
+ "No separator string should be shown for variables without a grip (2).");
+
+ windowVar.setGrip({ type: "object", class: "Window" });
+ documentVar.setGrip({ type: "object", class: "HTMLDocument" });
+
+ is(windowVar.target.querySelector(".separator").hidden, false,
+ "A separator string should now be shown after setting the grip (1).");
+ is(documentVar.target.querySelector(".separator").hidden, false,
+ "A separator string should now be shown after setting the grip (2).");
+
+ is(windowVar.target.querySelector(".separator").getAttribute("value"), ": ",
+ "The separator string label is correct (1).");
+ is(documentVar.target.querySelector(".separator").getAttribute("value"), ": ",
+ "The separator string label is correct (2).");
+
+ let localVar0 = localScope.addItem("localVar0");
+ let localVar1 = localScope.addItem("localVar1");
+ let localVar2 = localScope.addItem("localVar2");
+ let localVar3 = localScope.addItem("localVar3");
+ let localVar4 = localScope.addItem("localVar4");
+ let localVar5 = localScope.addItem("localVar5");
+
+ let localVar6 = localScope.addItem("localVar6");
+ let localVar7 = localScope.addItem("localVar7");
+ let localVar8 = localScope.addItem("localVar8");
+ let localVar9 = localScope.addItem("localVar9");
+
+ ok(localVar0, "The localVar0 hasn't been created correctly.");
+ ok(localVar1, "The localVar1 hasn't been created correctly.");
+ ok(localVar2, "The localVar2 hasn't been created correctly.");
+ ok(localVar3, "The localVar3 hasn't been created correctly.");
+ ok(localVar4, "The localVar4 hasn't been created correctly.");
+ ok(localVar5, "The localVar5 hasn't been created correctly.");
+ ok(localVar6, "The localVar6 hasn't been created correctly.");
+ ok(localVar7, "The localVar7 hasn't been created correctly.");
+ ok(localVar8, "The localVar8 hasn't been created correctly.");
+ ok(localVar9, "The localVar9 hasn't been created correctly.");
+
+ localVar0.setGrip(42);
+ localVar1.setGrip(true);
+ localVar2.setGrip("nasu");
+
+ localVar3.setGrip({ type: "undefined" });
+ localVar4.setGrip({ type: "null" });
+ localVar5.setGrip({ type: "object", class: "Object" });
+ localVar6.setGrip({ type: "Infinity" });
+ localVar7.setGrip({ type: "-Infinity" });
+ localVar8.setGrip({ type: "NaN" });
+ localVar9.setGrip({ type: "-0" });
+
+ localVar5.addItems({
+ someProp0: { value: 42, enumerable: true },
+ someProp1: { value: true, enumerable: true },
+ someProp2: { value: "nasu", enumerable: true },
+ someProp3: { value: { type: "undefined" }, enumerable: true },
+ someProp4: { value: { type: "null" }, enumerable: true },
+ someProp5: { value: { type: "object", class: "Object" }, enumerable: true },
+ someProp6: { value: { type: "Infinity" }, enumerable: true },
+ someProp7: { value: { type: "-Infinity" }, enumerable: true },
+ someProp8: { value: { type: "NaN" }, enumerable: true },
+ someProp9: { value: { type: "-0" }, enumerable: true },
+ someUndefined: {
+ get: { type: "undefined" },
+ set: { type: "undefined" },
+ enumerable: true
+ },
+ someAccessor: {
+ get: { type: "object", class: "Function" },
+ set: { type: "undefined" },
+ enumerable: true
+ }
+ });
+
+ localVar5.get("someProp5").addItems({
+ someProp0: { value: 42, enumerable: true },
+ someProp1: { value: true, enumerable: true },
+ someProp2: { value: "nasu", enumerable: true },
+ someProp3: { value: { type: "undefined" }, enumerable: true },
+ someProp4: { value: { type: "null" }, enumerable: true },
+ someProp5: { value: { type: "object", class: "Object" }, enumerable: true },
+ someProp6: { value: { type: "Infinity" }, enumerable: true },
+ someProp7: { value: { type: "-Infinity" }, enumerable: true },
+ someProp8: { value: { type: "NaN" }, enumerable: true },
+ someProp9: { value: { type: "-0" }, enumerable: true },
+ someUndefined: {
+ get: { type: "undefined" },
+ set: { type: "undefined" },
+ enumerable: true
+ },
+ someAccessor: {
+ get: { type: "object", class: "Function" },
+ set: { type: "undefined" },
+ enumerable: true
+ }
+ });
+
+ is(globalScope.target.querySelector(".enum").childNodes.length, 0,
+ "The globalScope doesn't contain all the created enumerable variable elements.");
+ is(globalScope.target.querySelector(".nonenum").childNodes.length, 2,
+ "The globalScope doesn't contain all the created non-enumerable variable elements.");
+
+ is(localScope.target.querySelector(".enum").childNodes.length, 0,
+ "The localScope doesn't contain all the created enumerable variable elements.");
+ is(localScope.target.querySelector(".nonenum").childNodes.length, 10,
+ "The localScope doesn't contain all the created non-enumerable variable elements.");
+
+ is(localVar5.target.querySelector(".enum").childNodes.length, 12,
+ "The localVar5 doesn't contain all the created enumerable properties.");
+ is(localVar5.target.querySelector(".nonenum").childNodes.length, 0,
+ "The localVar5 doesn't contain all the created non-enumerable properties.");
+
+ is(localVar5.get("someProp5").target.querySelector(".enum").childNodes.length, 12,
+ "The localVar5.someProp5 doesn't contain all the created enumerable properties.");
+ is(localVar5.get("someProp5").target.querySelector(".nonenum").childNodes.length, 0,
+ "The localVar5.someProp5 doesn't contain all the created non-enumerable properties.");
+
+ is(windowVar.target.querySelector(".value").getAttribute("value"), "Window",
+ "The grip information for the windowVar wasn't set correctly.");
+ is(documentVar.target.querySelector(".value").getAttribute("value"), "HTMLDocument",
+ "The grip information for the documentVar wasn't set correctly.");
+
+ is(localVar0.target.querySelector(".value").getAttribute("value"), "42",
+ "The grip information for the localVar0 wasn't set correctly.");
+ is(localVar1.target.querySelector(".value").getAttribute("value"), "true",
+ "The grip information for the localVar1 wasn't set correctly.");
+ is(localVar2.target.querySelector(".value").getAttribute("value"), "\"nasu\"",
+ "The grip information for the localVar2 wasn't set correctly.");
+ is(localVar3.target.querySelector(".value").getAttribute("value"), "undefined",
+ "The grip information for the localVar3 wasn't set correctly.");
+ is(localVar4.target.querySelector(".value").getAttribute("value"), "null",
+ "The grip information for the localVar4 wasn't set correctly.");
+ is(localVar5.target.querySelector(".value").getAttribute("value"), "Object",
+ "The grip information for the localVar5 wasn't set correctly.");
+ is(localVar6.target.querySelector(".value").getAttribute("value"), "Infinity",
+ "The grip information for the localVar6 wasn't set correctly.");
+ is(localVar7.target.querySelector(".value").getAttribute("value"), "-Infinity",
+ "The grip information for the localVar7 wasn't set correctly.");
+ is(localVar8.target.querySelector(".value").getAttribute("value"), "NaN",
+ "The grip information for the localVar8 wasn't set correctly.");
+ is(localVar9.target.querySelector(".value").getAttribute("value"), "-0",
+ "The grip information for the localVar9 wasn't set correctly.");
+
+ is(localVar5.get("someProp0").target.querySelector(".value").getAttribute("value"), "42",
+ "The grip information for the someProp0 wasn't set correctly.");
+ is(localVar5.get("someProp1").target.querySelector(".value").getAttribute("value"), "true",
+ "The grip information for the someProp1 wasn't set correctly.");
+ is(localVar5.get("someProp2").target.querySelector(".value").getAttribute("value"), "\"nasu\"",
+ "The grip information for the someProp2 wasn't set correctly.");
+ is(localVar5.get("someProp3").target.querySelector(".value").getAttribute("value"), "undefined",
+ "The grip information for the someProp3 wasn't set correctly.");
+ is(localVar5.get("someProp4").target.querySelector(".value").getAttribute("value"), "null",
+ "The grip information for the someProp4 wasn't set correctly.");
+ is(localVar5.get("someProp5").target.querySelector(".value").getAttribute("value"), "Object",
+ "The grip information for the someProp5 wasn't set correctly.");
+ is(localVar5.get("someProp6").target.querySelector(".value").getAttribute("value"), "Infinity",
+ "The grip information for the someProp6 wasn't set correctly.");
+ is(localVar5.get("someProp7").target.querySelector(".value").getAttribute("value"), "-Infinity",
+ "The grip information for the someProp7 wasn't set correctly.");
+ is(localVar5.get("someProp8").target.querySelector(".value").getAttribute("value"), "NaN",
+ "The grip information for the someProp8 wasn't set correctly.");
+ is(localVar5.get("someProp9").target.querySelector(".value").getAttribute("value"), "-0",
+ "The grip information for the someProp9 wasn't set correctly.");
+ is(localVar5.get("someUndefined").target.querySelector(".value").getAttribute("value"), "",
+ "The grip information for the someUndefined wasn't set correctly.");
+ is(localVar5.get("someAccessor").target.querySelector(".value").getAttribute("value"), "",
+ "The grip information for the someAccessor wasn't set correctly.");
+
+ is(localVar5.get("someProp5").get("someProp0").target.querySelector(".value").getAttribute("value"), "42",
+ "The grip information for the sub-someProp0 wasn't set correctly.");
+ is(localVar5.get("someProp5").get("someProp1").target.querySelector(".value").getAttribute("value"), "true",
+ "The grip information for the sub-someProp1 wasn't set correctly.");
+ is(localVar5.get("someProp5").get("someProp2").target.querySelector(".value").getAttribute("value"), "\"nasu\"",
+ "The grip information for the sub-someProp2 wasn't set correctly.");
+ is(localVar5.get("someProp5").get("someProp3").target.querySelector(".value").getAttribute("value"), "undefined",
+ "The grip information for the sub-someProp3 wasn't set correctly.");
+ is(localVar5.get("someProp5").get("someProp4").target.querySelector(".value").getAttribute("value"), "null",
+ "The grip information for the sub-someProp4 wasn't set correctly.");
+ is(localVar5.get("someProp5").get("someProp5").target.querySelector(".value").getAttribute("value"), "Object",
+ "The grip information for the sub-someProp5 wasn't set correctly.");
+ is(localVar5.get("someProp5").get("someProp6").target.querySelector(".value").getAttribute("value"), "Infinity",
+ "The grip information for the sub-someProp6 wasn't set correctly.");
+ is(localVar5.get("someProp5").get("someProp7").target.querySelector(".value").getAttribute("value"), "-Infinity",
+ "The grip information for the sub-someProp7 wasn't set correctly.");
+ is(localVar5.get("someProp5").get("someProp8").target.querySelector(".value").getAttribute("value"), "NaN",
+ "The grip information for the sub-someProp8 wasn't set correctly.");
+ is(localVar5.get("someProp5").get("someProp9").target.querySelector(".value").getAttribute("value"), "-0",
+ "The grip information for the sub-someProp9 wasn't set correctly.");
+ is(localVar5.get("someProp5").get("someUndefined").target.querySelector(".value").getAttribute("value"), "",
+ "The grip information for the sub-someUndefined wasn't set correctly.");
+ is(localVar5.get("someProp5").get("someAccessor").target.querySelector(".value").getAttribute("value"), "",
+ "The grip information for the sub-someAccessor wasn't set correctly.");
+
+ closeDebuggerAndFinish(aPanel);
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-06.js b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-06.js
new file mode 100644
index 000000000..6d923eb02
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-06.js
@@ -0,0 +1,125 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Test that Promises get their internal state added as psuedo properties.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_promise.html";
+
+var test = Task.async(function* () {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ const [tab,, panel] = yield initDebugger(TAB_URL, options);
+
+ const scopes = waitForCaretAndScopes(panel, 21);
+ callInTab(tab, "doPause");
+ yield scopes;
+
+ const variables = panel.panelWin.DebuggerView.Variables;
+ ok(variables, "Should get the variables view.");
+
+ const scope = [...variables][0];
+ ok(scope, "Should get the current function's scope.");
+
+ const promiseVariables = [...scope].filter(([name]) =>
+ ["p", "f", "r"].indexOf(name) !== -1);
+
+ is(promiseVariables.length, 3,
+ "Should have our 3 promise variables: p, f, r");
+
+ for (let [name, item] of promiseVariables) {
+ info("Expanding variable '" + name + "'");
+ let expanded = once(variables, "fetched");
+ item.expand();
+ yield expanded;
+
+ let foundState = false;
+ switch (name) {
+ case "p":
+ for (let [property, { value }] of item) {
+ if (property !== "<state>") {
+ isnot(property, "<value>",
+ "A pending promise shouldn't have a value");
+ isnot(property, "<reason>",
+ "A pending promise shouldn't have a reason");
+ continue;
+ }
+
+ foundState = true;
+ is(value, "pending", "The state should be pending.");
+ }
+ ok(foundState, "We should have found the <state> property.");
+ break;
+
+ case "f":
+ let foundValue = false;
+ for (let [property, value] of item) {
+ if (property === "<state>") {
+ foundState = true;
+ is(value.value, "fulfilled", "The state should be fulfilled.");
+ } else if (property === "<value>") {
+ foundValue = true;
+
+ let expanded = once(variables, "fetched");
+ value.expand();
+ yield expanded;
+
+ let expectedProps = new Map([["a", 1], ["b", 2], ["c", 3]]);
+ for (let [prop, val] of value) {
+ if (prop === "__proto__") {
+ continue;
+ }
+ ok(expectedProps.has(prop), "The property should be expected.");
+ is(val.value, expectedProps.get(prop), "The property value should be correct.");
+ expectedProps.delete(prop);
+ }
+ is(Object.keys(expectedProps).length, 0,
+ "Should have found all of the expected properties.");
+ } else {
+ isnot(property, "<reason>",
+ "A fulfilled promise shouldn't have a reason");
+ }
+ }
+ ok(foundState, "We should have found the <state> property.");
+ ok(foundValue, "We should have found the <value> property.");
+ break;
+
+ case "r":
+ let foundReason = false;
+ for (let [property, value] of item) {
+ if (property === "<state>") {
+ foundState = true;
+ is(value.value, "rejected", "The state should be rejected.");
+ } else if (property === "<reason>") {
+ foundReason = true;
+
+ let expanded = once(variables, "fetched");
+ value.expand();
+ yield expanded;
+
+ let foundMessage = false;
+ for (let [prop, val] of value) {
+ if (prop !== "message") {
+ continue;
+ }
+ foundMessage = true;
+ is(val.value, "uh oh", "Should have the correct error message.");
+ }
+ ok(foundMessage, "Should have found the error's message");
+ } else {
+ isnot(property, "<value>",
+ "A rejected promise shouldn't have a value");
+ }
+ }
+ ok(foundState, "We should have found the <state> property.");
+ break;
+ }
+ }
+
+ resumeDebuggerThenCloseAndFinish(panel);
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-07.js b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-07.js
new file mode 100644
index 000000000..a05f33e7f
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-07.js
@@ -0,0 +1,69 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Test that proxy objects get their internal state added as pseudo properties.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_proxy.html";
+
+var test = Task.async(function* () {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ var dbg = initDebugger(TAB_URL, options);
+ const [tab,, panel] = yield dbg;
+ const debuggerLineNumber = 34;
+ const scopes = waitForCaretAndScopes(panel, debuggerLineNumber);
+ callInTab(tab, "doPause");
+ yield scopes;
+
+ const variables = panel.panelWin.DebuggerView.Variables;
+ ok(variables, "Should get the variables view.");
+
+ const scope = [...variables][0];
+ ok(scope, "Should get the current function's scope.");
+
+ let proxy;
+ for (let [name, value] of scope) {
+ if (name === "proxy") {
+ proxy = value;
+ }
+ }
+ ok(proxy, "Should have found the proxy variable");
+
+ info("Expanding variable 'proxy'");
+ let expanded = once(variables, "fetched");
+ proxy.expand();
+ yield expanded;
+
+ let foundTarget = false;
+ let foundHandler = false;
+ for (let [property, data] of proxy) {
+ info("Expanding property '" + property + "'");
+ let expanded = once(variables, "fetched");
+ data.expand();
+ yield expanded;
+ if (property === "<target>") {
+ for(let [subprop, subdata] of data) if(subprop === "name") {
+ is(subdata.value, "target", "The value of '<target>' should be the [[ProxyTarget]]");
+ foundTarget = true;
+ }
+ } else {
+ is(property, "<handler>", "There shouldn't be properties other than <target> and <handler>");
+ for (let [subprop, subdata] of data) {
+ if(subprop === "name") {
+ is(subdata.value, "handler", "The value of '<handler>' should be the [[ProxyHandler]]");
+ foundHandler = true;
+ }
+ }
+ }
+ }
+ ok(foundTarget, "Should have found the '<target>' property containing the [[ProxyTarget]]");
+ ok(foundHandler, "Should have found the '<handler>' property containing the [[ProxyHandler]]");
+
+ resumeDebuggerThenCloseAndFinish(panel);
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-08.js b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-08.js
new file mode 100644
index 000000000..83083eef3
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-08.js
@@ -0,0 +1,61 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Test that property values are not missing when the property names only contain whitespace.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_whitespace-property-names.html";
+
+var test = Task.async(function* () {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ var dbg = initDebugger(TAB_URL, options);
+ const [tab,, panel] = yield dbg;
+ const debuggerLineNumber = 24;
+ const scopes = waitForCaretAndScopes(panel, debuggerLineNumber);
+ callInTab(tab, "doPause");
+ yield scopes;
+
+ const variables = panel.panelWin.DebuggerView.Variables;
+ ok(variables, "Should get the variables view.");
+
+ const scope = [...variables][0];
+ ok(scope, "Should get the current function's scope.");
+
+ let obj;
+ for (let [name, value] of scope) {
+ if (name === "obj") {
+ obj = value;
+ }
+ }
+ ok(obj, "Should have found the 'obj' variable");
+
+ info("Expanding variable 'obj'");
+ let expanded = once(variables, "fetched");
+ obj.expand();
+ yield expanded;
+
+ let values = ["", " ", "\r", "\n", "\t", "\f", "\uFEFF", "\xA0"];
+ let count = values.length;
+
+ for (let [property, value] of obj) {
+ let index = values.indexOf(property);
+ if (index >= 0) {
+ --count;
+ is(value._nameString, property,
+ "The _nameString is different than the property name");
+ is(value._valueString, index + "",
+ "The _valueString is different than the stringified value");
+ is(value._valueLabel.getAttribute("value"), index + "",
+ "The _valueLabel value is different than the stringified value");
+ }
+ }
+ is(count, 0, "There are " + count + " missing properties");
+
+ resumeDebuggerThenCloseAndFinish(panel);
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-accessibility.js b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-accessibility.js
new file mode 100644
index 000000000..6acec5583
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-accessibility.js
@@ -0,0 +1,557 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure that the variables view is keyboard accessible.
+ */
+
+var gTab, gPanel, gDebugger;
+var gVariablesView;
+
+function test() {
+ initDebugger().then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gVariablesView = gDebugger.DebuggerView.Variables;
+
+ performTest().then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+ });
+}
+
+function performTest() {
+ let arr = [
+ 42,
+ true,
+ "nasu",
+ undefined,
+ null,
+ [0, 1, 2],
+ { prop1: 9, prop2: 8 }
+ ];
+
+ let obj = {
+ p0: 42,
+ p1: true,
+ p2: "nasu",
+ p3: undefined,
+ p4: null,
+ p5: [3, 4, 5],
+ p6: { prop1: 7, prop2: 6 },
+ get p7() { return arr; },
+ set p8(value) { arr[0] = value; }
+ };
+
+ let test = {
+ someProp0: 42,
+ someProp1: true,
+ someProp2: "nasu",
+ someProp3: undefined,
+ someProp4: null,
+ someProp5: arr,
+ someProp6: obj,
+ get someProp7() { return arr; },
+ set someProp7(value) { arr[0] = value; }
+ };
+
+ gVariablesView.eval = function () {};
+ gVariablesView.switch = function () {};
+ gVariablesView.delete = function () {};
+ gVariablesView.rawObject = test;
+ gVariablesView.scrollPageSize = 5;
+
+ return Task.spawn(function* () {
+ yield waitForTick();
+
+ // Part 0: Test generic focus methods on the variables view.
+
+ gVariablesView.focusFirstVisibleItem();
+ is(gVariablesView.getFocusedItem().name, "someProp0",
+ "The 'someProp0' item should be focused.");
+
+ gVariablesView.focusNextItem();
+ is(gVariablesView.getFocusedItem().name, "someProp1",
+ "The 'someProp1' item should be focused.");
+
+ gVariablesView.focusPrevItem();
+ is(gVariablesView.getFocusedItem().name, "someProp0",
+ "The 'someProp0' item should be focused.");
+
+ // Part 1: Make sure that UP/DOWN keys don't scroll the variables view.
+
+ yield synthesizeKeyAndWaitForTick("VK_DOWN", {});
+ is(gVariablesView._parent.scrollTop, 0,
+ "The 'variables' view shouldn't scroll when pressing the DOWN key.");
+
+ yield synthesizeKeyAndWaitForTick("VK_UP", {});
+ is(gVariablesView._parent.scrollTop, 0,
+ "The 'variables' view shouldn't scroll when pressing the UP key.");
+
+ // Part 2: Make sure that RETURN/ESCAPE toggle input elements.
+
+ yield synthesizeKeyAndWaitForElement("VK_RETURN", {}, ".element-value-input", true);
+ yield synthesizeKeyAndWaitForElement("VK_ESCAPE", {}, ".element-value-input", false);
+ yield synthesizeKeyAndWaitForElement("VK_RETURN", { shiftKey: true }, ".element-name-input", true);
+ yield synthesizeKeyAndWaitForElement("VK_ESCAPE", {}, ".element-name-input", false);
+
+ // Part 3: Test simple navigation.
+
+ EventUtils.sendKey("DOWN", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "someProp1",
+ "The 'someProp1' item should be focused.");
+
+ EventUtils.sendKey("UP", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "someProp0",
+ "The 'someProp0' item should be focused.");
+
+ EventUtils.sendKey("PAGE_DOWN", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "someProp5",
+ "The 'someProp5' item should be focused.");
+
+ EventUtils.sendKey("PAGE_UP", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "someProp0",
+ "The 'someProp0' item should be focused.");
+
+ EventUtils.sendKey("END", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "__proto__",
+ "The '__proto__' item should be focused.");
+
+ EventUtils.sendKey("HOME", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "someProp0",
+ "The 'someProp0' item should be focused.");
+
+ // Part 4: Test if pressing the same navigation key twice works as expected.
+
+ EventUtils.sendKey("DOWN", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "someProp1",
+ "The 'someProp1' item should be focused.");
+
+ EventUtils.sendKey("DOWN", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "someProp2",
+ "The 'someProp2' item should be focused.");
+
+ EventUtils.sendKey("UP", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "someProp1",
+ "The 'someProp1' item should be focused.");
+
+ EventUtils.sendKey("UP", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "someProp0",
+ "The 'someProp0' item should be focused.");
+
+ EventUtils.sendKey("PAGE_DOWN", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "someProp5",
+ "The 'someProp5' item should be focused.");
+
+ EventUtils.sendKey("PAGE_DOWN", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "__proto__",
+ "The '__proto__' item should be focused.");
+
+ EventUtils.sendKey("PAGE_UP", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "someProp5",
+ "The 'someProp5' item should be focused.");
+
+ EventUtils.sendKey("PAGE_UP", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "someProp0",
+ "The 'someProp0' item should be focused.");
+
+ // Part 5: Test that HOME/PAGE_UP/PAGE_DOWN are symmetrical.
+
+ EventUtils.sendKey("HOME", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "someProp0",
+ "The 'someProp0' item should be focused.");
+
+ EventUtils.sendKey("HOME", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "someProp0",
+ "The 'someProp0' item should be focused.");
+
+ EventUtils.sendKey("PAGE_UP", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "someProp0",
+ "The 'someProp0' item should be focused.");
+
+ EventUtils.sendKey("HOME", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "someProp0",
+ "The 'someProp0' item should be focused.");
+
+ EventUtils.sendKey("END", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "__proto__",
+ "The '__proto__' item should be focused.");
+
+ EventUtils.sendKey("END", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "__proto__",
+ "The '__proto__' item should be focused.");
+
+ EventUtils.sendKey("PAGE_DOWN", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "__proto__",
+ "The '__proto__' item should be focused.");
+
+ EventUtils.sendKey("END", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "__proto__",
+ "The '__proto__' item should be focused.");
+
+ // Part 6: Test that focus doesn't leave the variables view.
+
+ EventUtils.sendKey("PAGE_UP", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "someProp5",
+ "The 'someProp5' item should be focused.");
+
+ EventUtils.sendKey("PAGE_UP", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "someProp0",
+ "The 'someProp0' item should be focused.");
+
+ EventUtils.sendKey("UP", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "someProp0",
+ "The 'someProp0' item should be focused.");
+
+ EventUtils.sendKey("UP", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "someProp0",
+ "The 'someProp0' item should be focused.");
+
+ EventUtils.sendKey("PAGE_DOWN", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "someProp5",
+ "The 'someProp5' item should be focused.");
+
+ EventUtils.sendKey("PAGE_DOWN", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "__proto__",
+ "The '__proto__' item should be focused.");
+
+ EventUtils.sendKey("PAGE_DOWN", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "__proto__",
+ "The '__proto__' item should be focused.");
+
+ EventUtils.sendKey("PAGE_DOWN", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "__proto__",
+ "The '__proto__' item should be focused.");
+
+ // Part 7: Test that random offsets don't occur in tandem with HOME/END.
+
+ EventUtils.sendKey("HOME", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "someProp0",
+ "The 'someProp0' item should be focused.");
+
+ EventUtils.sendKey("DOWN", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "someProp1",
+ "The 'someProp1' item should be focused.");
+
+ EventUtils.sendKey("END", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "__proto__",
+ "The '__proto__' item should be focused.");
+
+ EventUtils.sendKey("DOWN", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "__proto__",
+ "The '__proto__' item should be focused.");
+
+ // Part 8: Test that the RIGHT key expands elements as intended.
+
+ EventUtils.sendKey("PAGE_UP", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "someProp5",
+ "The 'someProp5' item should be focused.");
+ is(gVariablesView.getFocusedItem().expanded, false,
+ "The 'someProp5' item should not be expanded yet.");
+
+ yield synthesizeKeyAndWaitForTick("VK_RIGHT", {});
+ is(gVariablesView.getFocusedItem().name, "someProp5",
+ "The 'someProp5' item should be focused.");
+ is(gVariablesView.getFocusedItem().expanded, true,
+ "The 'someProp5' item should now be expanded.");
+ is(gVariablesView.getFocusedItem()._store.size, 9,
+ "There should be 9 properties in the selected variable.");
+ is(gVariablesView.getFocusedItem()._enumItems.length, 7,
+ "There should be 7 enumerable properties in the selected variable.");
+ is(gVariablesView.getFocusedItem()._nonEnumItems.length, 2,
+ "There should be 2 non-enumerable properties in the selected variable.");
+
+ yield waitForChildNodes(gVariablesView.getFocusedItem()._enum, 7);
+ yield waitForChildNodes(gVariablesView.getFocusedItem()._nonenum, 2);
+
+ EventUtils.sendKey("RIGHT", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "0",
+ "The '0' item should be focused.");
+
+ EventUtils.sendKey("RIGHT", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "0",
+ "The '0' item should still be focused.");
+
+ EventUtils.sendKey("PAGE_DOWN", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "5",
+ "The '5' item should be focused.");
+ is(gVariablesView.getFocusedItem().expanded, false,
+ "The '5' item should not be expanded yet.");
+
+ yield synthesizeKeyAndWaitForTick("VK_RIGHT", {});
+ is(gVariablesView.getFocusedItem().name, "5",
+ "The '5' item should be focused.");
+ is(gVariablesView.getFocusedItem().expanded, true,
+ "The '5' item should now be expanded.");
+ is(gVariablesView.getFocusedItem()._store.size, 5,
+ "There should be 5 properties in the selected variable.");
+ is(gVariablesView.getFocusedItem()._enumItems.length, 3,
+ "There should be 3 enumerable properties in the selected variable.");
+ is(gVariablesView.getFocusedItem()._nonEnumItems.length, 2,
+ "There should be 2 non-enumerable properties in the selected variable.");
+
+ yield waitForChildNodes(gVariablesView.getFocusedItem()._enum, 3);
+ yield waitForChildNodes(gVariablesView.getFocusedItem()._nonenum, 2);
+
+ EventUtils.sendKey("RIGHT", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "0",
+ "The '0' item should be focused.");
+
+ EventUtils.sendKey("RIGHT", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "0",
+ "The '0' item should still be focused.");
+
+ EventUtils.sendKey("PAGE_DOWN", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "6",
+ "The '6' item should be focused.");
+ is(gVariablesView.getFocusedItem().expanded, false,
+ "The '6' item should not be expanded yet.");
+
+ yield synthesizeKeyAndWaitForTick("VK_RIGHT", {});
+ is(gVariablesView.getFocusedItem().name, "6",
+ "The '6' item should be focused.");
+ is(gVariablesView.getFocusedItem().expanded, true,
+ "The '6' item should now be expanded.");
+ is(gVariablesView.getFocusedItem()._store.size, 3,
+ "There should be 3 properties in the selected variable.");
+ is(gVariablesView.getFocusedItem()._enumItems.length, 2,
+ "There should be 2 enumerable properties in the selected variable.");
+ is(gVariablesView.getFocusedItem()._nonEnumItems.length, 1,
+ "There should be 1 non-enumerable properties in the selected variable.");
+
+ yield waitForChildNodes(gVariablesView.getFocusedItem()._enum, 2);
+ yield waitForChildNodes(gVariablesView.getFocusedItem()._nonenum, 1);
+
+ EventUtils.sendKey("RIGHT", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "prop1",
+ "The 'prop1' item should be focused.");
+
+ EventUtils.sendKey("RIGHT", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "prop1",
+ "The 'prop1' item should still be focused.");
+
+ EventUtils.sendKey("PAGE_DOWN", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "someProp6",
+ "The 'someProp6' item should be focused.");
+ is(gVariablesView.getFocusedItem().expanded, false,
+ "The 'someProp6' item should not be expanded yet.");
+
+ // Part 9: Test that the RIGHT key collapses elements as intended.
+
+ EventUtils.sendKey("LEFT", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "someProp6",
+ "The 'someProp6' item should be focused.");
+
+ EventUtils.sendKey("UP", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "__proto__",
+ "The '__proto__' item should be focused.");
+
+ EventUtils.sendKey("LEFT", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "someProp5",
+ "The 'someProp5' item should be focused.");
+ is(gVariablesView.getFocusedItem().expanded, true,
+ "The '6' item should still be expanded.");
+
+ EventUtils.sendKey("LEFT", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "someProp5",
+ "The 'someProp5' item should still be focused.");
+ is(gVariablesView.getFocusedItem().expanded, false,
+ "The '6' item should still not be expanded anymore.");
+
+ EventUtils.sendKey("LEFT", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "someProp5",
+ "The 'someProp5' item should still be focused.");
+
+ // Part 9: Test continuous navigation.
+
+ EventUtils.sendKey("UP", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "someProp4",
+ "The 'someProp4' item should be focused.");
+
+ EventUtils.sendKey("UP", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "someProp3",
+ "The 'someProp3' item should be focused.");
+
+ EventUtils.sendKey("UP", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "someProp2",
+ "The 'someProp2' item should be focused.");
+
+ EventUtils.sendKey("UP", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "someProp1",
+ "The 'someProp1' item should be focused.");
+
+ EventUtils.sendKey("UP", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "someProp0",
+ "The 'someProp0' item should be focused.");
+
+ EventUtils.sendKey("PAGE_UP", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "someProp0",
+ "The 'someProp0' item should be focused.");
+
+ EventUtils.sendKey("PAGE_DOWN", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "someProp5",
+ "The 'someProp5' item should be focused.");
+
+ EventUtils.sendKey("DOWN", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "someProp6",
+ "The 'someProp6' item should be focused.");
+
+ EventUtils.sendKey("DOWN", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "someProp7",
+ "The 'someProp7' item should be focused.");
+
+ EventUtils.sendKey("DOWN", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "get",
+ "The 'get' item should be focused.");
+
+ EventUtils.sendKey("DOWN", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "set",
+ "The 'set' item should be focused.");
+
+ EventUtils.sendKey("DOWN", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "__proto__",
+ "The '__proto__' item should be focused.");
+
+ // Part 10: Test that BACKSPACE deletes items in the variables view.
+
+ EventUtils.sendKey("BACK_SPACE", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "__proto__",
+ "The '__proto__' variable should still be focused.");
+ is(gVariablesView.getFocusedItem().value, "[object Object]",
+ "The '__proto__' variable should not have an empty value.");
+ is(gVariablesView.getFocusedItem().visible, false,
+ "The '__proto__' variable should be hidden.");
+
+ EventUtils.sendKey("UP", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "set",
+ "The 'set' item should be focused.");
+ is(gVariablesView.getFocusedItem().value, "[object Object]",
+ "The 'set' item should not have an empty value.");
+ is(gVariablesView.getFocusedItem().visible, true,
+ "The 'set' item should be visible.");
+
+ EventUtils.sendKey("BACK_SPACE", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "set",
+ "The 'set' item should still be focused.");
+ is(gVariablesView.getFocusedItem().value, "[object Object]",
+ "The 'set' item should not have an empty value.");
+ is(gVariablesView.getFocusedItem().visible, true,
+ "The 'set' item should be visible.");
+ is(gVariablesView.getFocusedItem().twisty, false,
+ "The 'set' item should be disabled and have a hidden twisty.");
+
+ EventUtils.sendKey("UP", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "get",
+ "The 'get' item should be focused.");
+ is(gVariablesView.getFocusedItem().value, "[object Object]",
+ "The 'get' item should not have an empty value.");
+ is(gVariablesView.getFocusedItem().visible, true,
+ "The 'get' item should be visible.");
+
+ EventUtils.sendKey("BACK_SPACE", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "get",
+ "The 'get' item should still be focused.");
+ is(gVariablesView.getFocusedItem().value, "[object Object]",
+ "The 'get' item should not have an empty value.");
+ is(gVariablesView.getFocusedItem().visible, true,
+ "The 'get' item should be visible.");
+ is(gVariablesView.getFocusedItem().twisty, false,
+ "The 'get' item should be disabled and have a hidden twisty.");
+
+ EventUtils.sendKey("UP", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "someProp7",
+ "The 'someProp7' item should be focused.");
+ is(gVariablesView.getFocusedItem().value, undefined,
+ "The 'someProp7' variable should have an empty value.");
+ is(gVariablesView.getFocusedItem().visible, true,
+ "The 'someProp7' variable should be visible.");
+
+ EventUtils.sendKey("BACK_SPACE", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "someProp7",
+ "The 'someProp7' variable should still be focused.");
+ is(gVariablesView.getFocusedItem().value, undefined,
+ "The 'someProp7' variable should have an empty value.");
+ is(gVariablesView.getFocusedItem().visible, false,
+ "The 'someProp7' variable should be hidden.");
+
+ // Part 11: Test that Ctrl-C copies the current item to the system clipboard
+
+ gVariablesView.focusFirstVisibleItem();
+ let copied = promise.defer();
+ let expectedValue = gVariablesView.getFocusedItem().name
+ + gVariablesView.getFocusedItem().separatorStr
+ + gVariablesView.getFocusedItem().value;
+
+ waitForClipboard(expectedValue, function setup() {
+ EventUtils.synthesizeKey("C", { metaKey: true }, gDebugger);
+ }, copied.resolve, copied.reject
+ );
+
+ try {
+ yield copied.promise;
+ ok(true,
+ "Ctrl-C copied the selected item to the clipboard.");
+ } catch (e) {
+ ok(false,
+ "Ctrl-C didn't copy the selected item to the clipboard.");
+ }
+
+ yield closeDebuggerAndFinish(gPanel);
+ });
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gVariablesView = null;
+});
+
+function synthesizeKeyAndWaitForElement(aKey, aModifiers, aSelector, aExistence) {
+ EventUtils.synthesizeKey(aKey, aModifiers, gDebugger);
+ return waitForElement(aSelector, aExistence);
+}
+
+function synthesizeKeyAndWaitForTick(aKey, aModifiers) {
+ EventUtils.synthesizeKey(aKey, aModifiers, gDebugger);
+ return waitForTick();
+}
+
+function waitForElement(aSelector, aExistence) {
+ return waitForPredicate(() => {
+ return !!gVariablesView._list.querySelector(aSelector) == aExistence;
+ });
+}
+
+function waitForChildNodes(aTarget, aCount) {
+ return waitForPredicate(() => {
+ return aTarget.childNodes.length == aCount;
+ });
+}
+
+function waitForPredicate(aPredicate, aInterval = 10) {
+ let deferred = promise.defer();
+
+ // Poll every few milliseconds until the element is retrieved.
+ let count = 0;
+ let intervalID = window.setInterval(() => {
+ // Make sure we don't wait for too long.
+ if (++count > 1000) {
+ deferred.reject("Timed out while polling for the element.");
+ window.clearInterval(intervalID);
+ return;
+ }
+ // Check if the predicate condition is fulfilled.
+ if (!aPredicate()) {
+ return;
+ }
+ // We got the element, it's safe to callback.
+ window.clearInterval(intervalID);
+ deferred.resolve();
+ }, aInterval);
+
+ return deferred.promise;
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-data.js b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-data.js
new file mode 100644
index 000000000..02679e073
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-data.js
@@ -0,0 +1,611 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure that the variables view correctly populates itself
+ * when given some raw data.
+ */
+
+var gTab, gPanel, gDebugger;
+var gVariablesView, gScope, gVariable;
+
+function test() {
+ initDebugger().then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gVariablesView = gDebugger.DebuggerView.Variables;
+
+ performTest();
+ });
+}
+
+function performTest() {
+ let arr = [
+ 42,
+ true,
+ "nasu",
+ undefined,
+ null,
+ [0, 1, 2],
+ { prop1: 9, prop2: 8 }
+ ];
+
+ let obj = {
+ p0: 42,
+ p1: true,
+ p2: "nasu",
+ p3: undefined,
+ p4: null,
+ p5: [3, 4, 5],
+ p6: { prop1: 7, prop2: 6 },
+ get p7() { return arr; },
+ set p8(value) { arr[0] = value; }
+ };
+
+ let test = {
+ someProp0: 42,
+ someProp1: true,
+ someProp2: "nasu",
+ someProp3: undefined,
+ someProp4: null,
+ someProp5: arr,
+ someProp6: obj,
+ get someProp7() { return arr; },
+ set someProp7(value) { arr[0] = value; }
+ };
+
+ gVariablesView.eval = function () {};
+ gVariablesView.switch = function () {};
+ gVariablesView.delete = function () {};
+ gVariablesView.new = function () {};
+ gVariablesView.rawObject = test;
+
+ testHierarchy();
+ testHeader();
+ testFirstLevelContents();
+ testSecondLevelContents();
+ testThirdLevelContents();
+ testOriginalRawDataIntegrity(arr, obj);
+
+ let fooScope = gVariablesView.addScope("foo");
+ let anonymousVar = fooScope.addItem();
+
+ let anonymousScope = gVariablesView.addScope();
+ let barVar = anonymousScope.addItem("bar");
+ let bazProperty = barVar.addItem("baz");
+
+ testAnonymousHeaders(fooScope, anonymousVar, anonymousScope, barVar, bazProperty);
+ testPropertyInheritance(fooScope, anonymousVar, anonymousScope, barVar, bazProperty);
+
+ testClearHierarchy();
+ closeDebuggerAndFinish(gPanel);
+}
+
+function testHierarchy() {
+ is(gVariablesView._currHierarchy.size, 13,
+ "There should be 1 scope, 1 var, 1 proto, 8 props, 1 getter and 1 setter.");
+
+ gScope = gVariablesView._currHierarchy.get("");
+ gVariable = gVariablesView._currHierarchy.get("[]");
+
+ is(gVariablesView._store.length, 1,
+ "There should be only one scope in the view.");
+ is(gScope._store.size, 1,
+ "There should be only one variable in the scope.");
+ is(gVariable._store.size, 9,
+ "There should be 1 __proto__ and 8 properties in the variable.");
+}
+
+function testHeader() {
+ is(gScope.header, false,
+ "The scope title header should be hidden.");
+ is(gVariable.header, false,
+ "The variable title header should be hidden.");
+
+ gScope.showHeader();
+ gVariable.showHeader();
+
+ is(gScope.header, false,
+ "The scope title header should still not be visible.");
+ is(gVariable.header, false,
+ "The variable title header should still not be visible.");
+
+ gScope.hideHeader();
+ gVariable.hideHeader();
+
+ is(gScope.header, false,
+ "The scope title header should now still be hidden.");
+ is(gVariable.header, false,
+ "The variable title header should now still be hidden.");
+}
+
+function testFirstLevelContents() {
+ let someProp0 = gVariable.get("someProp0");
+ let someProp1 = gVariable.get("someProp1");
+ let someProp2 = gVariable.get("someProp2");
+ let someProp3 = gVariable.get("someProp3");
+ let someProp4 = gVariable.get("someProp4");
+ let someProp5 = gVariable.get("someProp5");
+ let someProp6 = gVariable.get("someProp6");
+ let someProp7 = gVariable.get("someProp7");
+ let __proto__ = gVariable.get("__proto__");
+
+ is(someProp0.visible, true, "The first property visible state is correct.");
+ is(someProp1.visible, true, "The second property visible state is correct.");
+ is(someProp2.visible, true, "The third property visible state is correct.");
+ is(someProp3.visible, true, "The fourth property visible state is correct.");
+ is(someProp4.visible, true, "The fifth property visible state is correct.");
+ is(someProp5.visible, true, "The sixth property visible state is correct.");
+ is(someProp6.visible, true, "The seventh property visible state is correct.");
+ is(someProp7.visible, true, "The eight property visible state is correct.");
+ is(__proto__.visible, true, "The __proto__ property visible state is correct.");
+
+ is(someProp0.expanded, false, "The first property expanded state is correct.");
+ is(someProp1.expanded, false, "The second property expanded state is correct.");
+ is(someProp2.expanded, false, "The third property expanded state is correct.");
+ is(someProp3.expanded, false, "The fourth property expanded state is correct.");
+ is(someProp4.expanded, false, "The fifth property expanded state is correct.");
+ is(someProp5.expanded, false, "The sixth property expanded state is correct.");
+ is(someProp6.expanded, false, "The seventh property expanded state is correct.");
+ is(someProp7.expanded, true, "The eight property expanded state is correct.");
+ is(__proto__.expanded, false, "The __proto__ property expanded state is correct.");
+
+ is(someProp0.header, true, "The first property header state is correct.");
+ is(someProp1.header, true, "The second property header state is correct.");
+ is(someProp2.header, true, "The third property header state is correct.");
+ is(someProp3.header, true, "The fourth property header state is correct.");
+ is(someProp4.header, true, "The fifth property header state is correct.");
+ is(someProp5.header, true, "The sixth property header state is correct.");
+ is(someProp6.header, true, "The seventh property header state is correct.");
+ is(someProp7.header, true, "The eight property header state is correct.");
+ is(__proto__.header, true, "The __proto__ property header state is correct.");
+
+ is(someProp0.twisty, false, "The first property twisty state is correct.");
+ is(someProp1.twisty, false, "The second property twisty state is correct.");
+ is(someProp2.twisty, false, "The third property twisty state is correct.");
+ is(someProp3.twisty, false, "The fourth property twisty state is correct.");
+ is(someProp4.twisty, false, "The fifth property twisty state is correct.");
+ is(someProp5.twisty, true, "The sixth property twisty state is correct.");
+ is(someProp6.twisty, true, "The seventh property twisty state is correct.");
+ is(someProp7.twisty, true, "The eight property twisty state is correct.");
+ is(__proto__.twisty, true, "The __proto__ property twisty state is correct.");
+
+ is(someProp0.name, "someProp0", "The first property name is correct.");
+ is(someProp1.name, "someProp1", "The second property name is correct.");
+ is(someProp2.name, "someProp2", "The third property name is correct.");
+ is(someProp3.name, "someProp3", "The fourth property name is correct.");
+ is(someProp4.name, "someProp4", "The fifth property name is correct.");
+ is(someProp5.name, "someProp5", "The sixth property name is correct.");
+ is(someProp6.name, "someProp6", "The seventh property name is correct.");
+ is(someProp7.name, "someProp7", "The eight property name is correct.");
+ is(__proto__.name, "__proto__", "The __proto__ property name is correct.");
+
+ is(someProp0.value, 42, "The first property value is correct.");
+ is(someProp1.value, true, "The second property value is correct.");
+ is(someProp2.value, "nasu", "The third property value is correct.");
+ is(someProp3.value.type, "undefined", "The fourth property value is correct.");
+ is(someProp4.value.type, "null", "The fifth property value is correct.");
+ is(someProp5.value.type, "object", "The sixth property value type is correct.");
+ is(someProp5.value.class, "Array", "The sixth property value class is correct.");
+ is(someProp6.value.type, "object", "The seventh property value type is correct.");
+ is(someProp6.value.class, "Object", "The seventh property value class is correct.");
+ is(someProp7.value, null, "The eight property value is correct.");
+ isnot(someProp7.getter, null, "The eight property getter is correct.");
+ isnot(someProp7.setter, null, "The eight property setter is correct.");
+ is(someProp7.getter.type, "object", "The eight property getter type is correct.");
+ is(someProp7.getter.class, "Function", "The eight property getter class is correct.");
+ is(someProp7.setter.type, "object", "The eight property setter type is correct.");
+ is(someProp7.setter.class, "Function", "The eight property setter class is correct.");
+ is(__proto__.value.type, "object", "The __proto__ property value type is correct.");
+ is(__proto__.value.class, "Object", "The __proto__ property value class is correct.");
+
+ someProp0.expand();
+ someProp1.expand();
+ someProp2.expand();
+ someProp3.expand();
+ someProp4.expand();
+ someProp7.expand();
+
+ ok(!someProp0.get("__proto__"), "Number primitives should not have a prototype");
+ ok(!someProp1.get("__proto__"), "Boolean primitives should not have a prototype");
+ ok(!someProp2.get("__proto__"), "String literals should not have a prototype");
+ ok(!someProp3.get("__proto__"), "Undefined values should not have a prototype");
+ ok(!someProp4.get("__proto__"), "Null values should not have a prototype");
+ ok(!someProp7.get("__proto__"), "Getter properties should not have a prototype");
+}
+
+function testSecondLevelContents() {
+ let someProp5 = gVariable.get("someProp5");
+ let someProp6 = gVariable.get("someProp6");
+
+ is(someProp5._store.size, 0, "No properties should be in someProp5 before expanding");
+ someProp5.expand();
+ is(someProp5._store.size, 9, "Some properties should be in someProp5 before expanding");
+
+ let arrayItem0 = someProp5.get("0");
+ let arrayItem1 = someProp5.get("1");
+ let arrayItem2 = someProp5.get("2");
+ let arrayItem3 = someProp5.get("3");
+ let arrayItem4 = someProp5.get("4");
+ let arrayItem5 = someProp5.get("5");
+ let arrayItem6 = someProp5.get("6");
+ let __proto__ = someProp5.get("__proto__");
+
+ is(arrayItem0.visible, true, "The first array item visible state is correct.");
+ is(arrayItem1.visible, true, "The second array item visible state is correct.");
+ is(arrayItem2.visible, true, "The third array item visible state is correct.");
+ is(arrayItem3.visible, true, "The fourth array item visible state is correct.");
+ is(arrayItem4.visible, true, "The fifth array item visible state is correct.");
+ is(arrayItem5.visible, true, "The sixth array item visible state is correct.");
+ is(arrayItem6.visible, true, "The seventh array item visible state is correct.");
+ is(__proto__.visible, true, "The __proto__ property visible state is correct.");
+
+ is(arrayItem0.expanded, false, "The first array item expanded state is correct.");
+ is(arrayItem1.expanded, false, "The second array item expanded state is correct.");
+ is(arrayItem2.expanded, false, "The third array item expanded state is correct.");
+ is(arrayItem3.expanded, false, "The fourth array item expanded state is correct.");
+ is(arrayItem4.expanded, false, "The fifth array item expanded state is correct.");
+ is(arrayItem5.expanded, false, "The sixth array item expanded state is correct.");
+ is(arrayItem6.expanded, false, "The seventh array item expanded state is correct.");
+ is(__proto__.expanded, false, "The __proto__ property expanded state is correct.");
+
+ is(arrayItem0.header, true, "The first array item header state is correct.");
+ is(arrayItem1.header, true, "The second array item header state is correct.");
+ is(arrayItem2.header, true, "The third array item header state is correct.");
+ is(arrayItem3.header, true, "The fourth array item header state is correct.");
+ is(arrayItem4.header, true, "The fifth array item header state is correct.");
+ is(arrayItem5.header, true, "The sixth array item header state is correct.");
+ is(arrayItem6.header, true, "The seventh array item header state is correct.");
+ is(__proto__.header, true, "The __proto__ property header state is correct.");
+
+ is(arrayItem0.twisty, false, "The first array item twisty state is correct.");
+ is(arrayItem1.twisty, false, "The second array item twisty state is correct.");
+ is(arrayItem2.twisty, false, "The third array item twisty state is correct.");
+ is(arrayItem3.twisty, false, "The fourth array item twisty state is correct.");
+ is(arrayItem4.twisty, false, "The fifth array item twisty state is correct.");
+ is(arrayItem5.twisty, true, "The sixth array item twisty state is correct.");
+ is(arrayItem6.twisty, true, "The seventh array item twisty state is correct.");
+ is(__proto__.twisty, true, "The __proto__ property twisty state is correct.");
+
+ is(arrayItem0.name, "0", "The first array item name is correct.");
+ is(arrayItem1.name, "1", "The second array item name is correct.");
+ is(arrayItem2.name, "2", "The third array item name is correct.");
+ is(arrayItem3.name, "3", "The fourth array item name is correct.");
+ is(arrayItem4.name, "4", "The fifth array item name is correct.");
+ is(arrayItem5.name, "5", "The sixth array item name is correct.");
+ is(arrayItem6.name, "6", "The seventh array item name is correct.");
+ is(__proto__.name, "__proto__", "The __proto__ property name is correct.");
+
+ is(arrayItem0.value, 42, "The first array item value is correct.");
+ is(arrayItem1.value, true, "The second array item value is correct.");
+ is(arrayItem2.value, "nasu", "The third array item value is correct.");
+ is(arrayItem3.value.type, "undefined", "The fourth array item value is correct.");
+ is(arrayItem4.value.type, "null", "The fifth array item value is correct.");
+ is(arrayItem5.value.type, "object", "The sixth array item value type is correct.");
+ is(arrayItem5.value.class, "Array", "The sixth array item value class is correct.");
+ is(arrayItem6.value.type, "object", "The seventh array item value type is correct.");
+ is(arrayItem6.value.class, "Object", "The seventh array item value class is correct.");
+ is(__proto__.value.type, "object", "The __proto__ property value type is correct.");
+ is(__proto__.value.class, "Array", "The __proto__ property value class is correct.");
+
+ is(someProp6._store.size, 0, "No properties should be in someProp6 before expanding");
+ someProp6.expand();
+ is(someProp6._store.size, 10, "Some properties should be in someProp6 before expanding");
+
+ let objectItem0 = someProp6.get("p0");
+ let objectItem1 = someProp6.get("p1");
+ let objectItem2 = someProp6.get("p2");
+ let objectItem3 = someProp6.get("p3");
+ let objectItem4 = someProp6.get("p4");
+ let objectItem5 = someProp6.get("p5");
+ let objectItem6 = someProp6.get("p6");
+ let objectItem7 = someProp6.get("p7");
+ let objectItem8 = someProp6.get("p8");
+ __proto__ = someProp6.get("__proto__");
+
+ is(objectItem0.visible, true, "The first object item visible state is correct.");
+ is(objectItem1.visible, true, "The second object item visible state is correct.");
+ is(objectItem2.visible, true, "The third object item visible state is correct.");
+ is(objectItem3.visible, true, "The fourth object item visible state is correct.");
+ is(objectItem4.visible, true, "The fifth object item visible state is correct.");
+ is(objectItem5.visible, true, "The sixth object item visible state is correct.");
+ is(objectItem6.visible, true, "The seventh object item visible state is correct.");
+ is(objectItem7.visible, true, "The eight object item visible state is correct.");
+ is(objectItem8.visible, true, "The ninth object item visible state is correct.");
+ is(__proto__.visible, true, "The __proto__ property visible state is correct.");
+
+ is(objectItem0.expanded, false, "The first object item expanded state is correct.");
+ is(objectItem1.expanded, false, "The second object item expanded state is correct.");
+ is(objectItem2.expanded, false, "The third object item expanded state is correct.");
+ is(objectItem3.expanded, false, "The fourth object item expanded state is correct.");
+ is(objectItem4.expanded, false, "The fifth object item expanded state is correct.");
+ is(objectItem5.expanded, false, "The sixth object item expanded state is correct.");
+ is(objectItem6.expanded, false, "The seventh object item expanded state is correct.");
+ is(objectItem7.expanded, true, "The eight object item expanded state is correct.");
+ is(objectItem8.expanded, true, "The ninth object item expanded state is correct.");
+ is(__proto__.expanded, false, "The __proto__ property expanded state is correct.");
+
+ is(objectItem0.header, true, "The first object item header state is correct.");
+ is(objectItem1.header, true, "The second object item header state is correct.");
+ is(objectItem2.header, true, "The third object item header state is correct.");
+ is(objectItem3.header, true, "The fourth object item header state is correct.");
+ is(objectItem4.header, true, "The fifth object item header state is correct.");
+ is(objectItem5.header, true, "The sixth object item header state is correct.");
+ is(objectItem6.header, true, "The seventh object item header state is correct.");
+ is(objectItem7.header, true, "The eight object item header state is correct.");
+ is(objectItem8.header, true, "The ninth object item header state is correct.");
+ is(__proto__.header, true, "The __proto__ property header state is correct.");
+
+ is(objectItem0.twisty, false, "The first object item twisty state is correct.");
+ is(objectItem1.twisty, false, "The second object item twisty state is correct.");
+ is(objectItem2.twisty, false, "The third object item twisty state is correct.");
+ is(objectItem3.twisty, false, "The fourth object item twisty state is correct.");
+ is(objectItem4.twisty, false, "The fifth object item twisty state is correct.");
+ is(objectItem5.twisty, true, "The sixth object item twisty state is correct.");
+ is(objectItem6.twisty, true, "The seventh object item twisty state is correct.");
+ is(objectItem7.twisty, true, "The eight object item twisty state is correct.");
+ is(objectItem8.twisty, true, "The ninth object item twisty state is correct.");
+ is(__proto__.twisty, true, "The __proto__ property twisty state is correct.");
+
+ is(objectItem0.name, "p0", "The first object item name is correct.");
+ is(objectItem1.name, "p1", "The second object item name is correct.");
+ is(objectItem2.name, "p2", "The third object item name is correct.");
+ is(objectItem3.name, "p3", "The fourth object item name is correct.");
+ is(objectItem4.name, "p4", "The fifth object item name is correct.");
+ is(objectItem5.name, "p5", "The sixth object item name is correct.");
+ is(objectItem6.name, "p6", "The seventh object item name is correct.");
+ is(objectItem7.name, "p7", "The eight seventh object item name is correct.");
+ is(objectItem8.name, "p8", "The ninth seventh object item name is correct.");
+ is(__proto__.name, "__proto__", "The __proto__ property name is correct.");
+
+ is(objectItem0.value, 42, "The first object item value is correct.");
+ is(objectItem1.value, true, "The second object item value is correct.");
+ is(objectItem2.value, "nasu", "The third object item value is correct.");
+ is(objectItem3.value.type, "undefined", "The fourth object item value is correct.");
+ is(objectItem4.value.type, "null", "The fifth object item value is correct.");
+ is(objectItem5.value.type, "object", "The sixth object item value type is correct.");
+ is(objectItem5.value.class, "Array", "The sixth object item value class is correct.");
+ is(objectItem6.value.type, "object", "The seventh object item value type is correct.");
+ is(objectItem6.value.class, "Object", "The seventh object item value class is correct.");
+ is(objectItem7.value, null, "The eight object item value is correct.");
+ isnot(objectItem7.getter, null, "The eight object item getter is correct.");
+ isnot(objectItem7.setter, null, "The eight object item setter is correct.");
+ is(objectItem7.setter.type, "undefined", "The eight object item setter type is correct.");
+ is(objectItem7.getter.type, "object", "The eight object item getter type is correct.");
+ is(objectItem7.getter.class, "Function", "The eight object item getter class is correct.");
+ is(objectItem8.value, null, "The ninth object item value is correct.");
+ isnot(objectItem8.getter, null, "The ninth object item getter is correct.");
+ isnot(objectItem8.setter, null, "The ninth object item setter is correct.");
+ is(objectItem8.getter.type, "undefined", "The eight object item getter type is correct.");
+ is(objectItem8.setter.type, "object", "The ninth object item setter type is correct.");
+ is(objectItem8.setter.class, "Function", "The ninth object item setter class is correct.");
+ is(__proto__.value.type, "object", "The __proto__ property value type is correct.");
+ is(__proto__.value.class, "Object", "The __proto__ property value class is correct.");
+}
+
+function testThirdLevelContents() {
+ (function () {
+ let someProp5 = gVariable.get("someProp5");
+ let arrayItem5 = someProp5.get("5");
+ let arrayItem6 = someProp5.get("6");
+
+ is(arrayItem5._store.size, 0, "No properties should be in arrayItem5 before expanding");
+ arrayItem5.expand();
+ is(arrayItem5._store.size, 5, "Some properties should be in arrayItem5 before expanding");
+
+ is(arrayItem6._store.size, 0, "No properties should be in arrayItem6 before expanding");
+ arrayItem6.expand();
+ is(arrayItem6._store.size, 3, "Some properties should be in arrayItem6 before expanding");
+
+ let arraySubItem0 = arrayItem5.get("0");
+ let arraySubItem1 = arrayItem5.get("1");
+ let arraySubItem2 = arrayItem5.get("2");
+ let objectSubItem0 = arrayItem6.get("prop1");
+ let objectSubItem1 = arrayItem6.get("prop2");
+
+ is(arraySubItem0.value, 0, "The first array sub-item value is correct.");
+ is(arraySubItem1.value, 1, "The second array sub-item value is correct.");
+ is(arraySubItem2.value, 2, "The third array sub-item value is correct.");
+
+ is(objectSubItem0.value, 9, "The first object sub-item value is correct.");
+ is(objectSubItem1.value, 8, "The second object sub-item value is correct.");
+
+ let array__proto__ = arrayItem5.get("__proto__");
+ let object__proto__ = arrayItem6.get("__proto__");
+
+ ok(array__proto__, "The array should have a __proto__ property.");
+ ok(object__proto__, "The object should have a __proto__ property.");
+ })();
+
+ (function () {
+ let someProp6 = gVariable.get("someProp6");
+ let objectItem5 = someProp6.get("p5");
+ let objectItem6 = someProp6.get("p6");
+
+ is(objectItem5._store.size, 0, "No properties should be in objectItem5 before expanding");
+ objectItem5.expand();
+ is(objectItem5._store.size, 5, "Some properties should be in objectItem5 before expanding");
+
+ is(objectItem6._store.size, 0, "No properties should be in objectItem6 before expanding");
+ objectItem6.expand();
+ is(objectItem6._store.size, 3, "Some properties should be in objectItem6 before expanding");
+
+ let arraySubItem0 = objectItem5.get("0");
+ let arraySubItem1 = objectItem5.get("1");
+ let arraySubItem2 = objectItem5.get("2");
+ let objectSubItem0 = objectItem6.get("prop1");
+ let objectSubItem1 = objectItem6.get("prop2");
+
+ is(arraySubItem0.value, 3, "The first array sub-item value is correct.");
+ is(arraySubItem1.value, 4, "The second array sub-item value is correct.");
+ is(arraySubItem2.value, 5, "The third array sub-item value is correct.");
+
+ is(objectSubItem0.value, 7, "The first object sub-item value is correct.");
+ is(objectSubItem1.value, 6, "The second object sub-item value is correct.");
+
+ let array__proto__ = objectItem5.get("__proto__");
+ let object__proto__ = objectItem6.get("__proto__");
+
+ ok(array__proto__, "The array should have a __proto__ property.");
+ ok(object__proto__, "The object should have a __proto__ property.");
+ })();
+}
+
+function testOriginalRawDataIntegrity(arr, obj) {
+ is(arr[0], 42, "The first array item should not have changed.");
+ is(arr[1], true, "The second array item should not have changed.");
+ is(arr[2], "nasu", "The third array item should not have changed.");
+ is(arr[3], undefined, "The fourth array item should not have changed.");
+ is(arr[4], null, "The fifth array item should not have changed.");
+ ok(arr[5] instanceof Array, "The sixth array item should be an Array.");
+ is(arr[5][0], 0, "The sixth array item should not have changed.");
+ is(arr[5][1], 1, "The sixth array item should not have changed.");
+ is(arr[5][2], 2, "The sixth array item should not have changed.");
+ ok(arr[6] instanceof Object, "The seventh array item should be an Object.");
+ is(arr[6].prop1, 9, "The seventh array item should not have changed.");
+ is(arr[6].prop2, 8, "The seventh array item should not have changed.");
+
+ is(obj.p0, 42, "The first object property should not have changed.");
+ is(obj.p1, true, "The first object property should not have changed.");
+ is(obj.p2, "nasu", "The first object property should not have changed.");
+ is(obj.p3, undefined, "The first object property should not have changed.");
+ is(obj.p4, null, "The first object property should not have changed.");
+ ok(obj.p5 instanceof Array, "The sixth object property should be an Array.");
+ is(obj.p5[0], 3, "The sixth object property should not have changed.");
+ is(obj.p5[1], 4, "The sixth object property should not have changed.");
+ is(obj.p5[2], 5, "The sixth object property should not have changed.");
+ ok(obj.p6 instanceof Object, "The seventh object property should be an Object.");
+ is(obj.p6.prop1, 7, "The seventh object property should not have changed.");
+ is(obj.p6.prop2, 6, "The seventh object property should not have changed.");
+}
+
+function testAnonymousHeaders(fooScope, anonymousVar, anonymousScope, barVar, bazProperty) {
+ is(fooScope.header, true,
+ "A named scope should have a header visible.");
+ is(fooScope.target.hasAttribute("untitled"), false,
+ "The non-header attribute should not be applied to scopes with headers.");
+
+ is(anonymousScope.header, false,
+ "An anonymous scope should have a header visible.");
+ is(anonymousScope.target.hasAttribute("untitled"), true,
+ "The non-header attribute should not be applied to scopes without headers.");
+
+ is(barVar.header, true,
+ "A named variable should have a header visible.");
+ is(barVar.target.hasAttribute("untitled"), false,
+ "The non-header attribute should not be applied to variables with headers.");
+
+ is(anonymousVar.header, false,
+ "An anonymous variable should have a header visible.");
+ is(anonymousVar.target.hasAttribute("untitled"), true,
+ "The non-header attribute should not be applied to variables without headers.");
+}
+
+function testPropertyInheritance(fooScope, anonymousVar, anonymousScope, barVar, bazProperty) {
+ is(fooScope.preventDisableOnChange, gVariablesView.preventDisableOnChange,
+ "The preventDisableOnChange property should persist from the view to all scopes.");
+ is(fooScope.preventDescriptorModifiers, gVariablesView.preventDescriptorModifiers,
+ "The preventDescriptorModifiers property should persist from the view to all scopes.");
+ is(fooScope.editableNameTooltip, gVariablesView.editableNameTooltip,
+ "The editableNameTooltip property should persist from the view to all scopes.");
+ is(fooScope.editableValueTooltip, gVariablesView.editableValueTooltip,
+ "The editableValueTooltip property should persist from the view to all scopes.");
+ is(fooScope.editButtonTooltip, gVariablesView.editButtonTooltip,
+ "The editButtonTooltip property should persist from the view to all scopes.");
+ is(fooScope.deleteButtonTooltip, gVariablesView.deleteButtonTooltip,
+ "The deleteButtonTooltip property should persist from the view to all scopes.");
+ is(fooScope.contextMenuId, gVariablesView.contextMenuId,
+ "The contextMenuId property should persist from the view to all scopes.");
+ is(fooScope.separatorStr, gVariablesView.separatorStr,
+ "The separatorStr property should persist from the view to all scopes.");
+ is(fooScope.eval, gVariablesView.eval,
+ "The eval property should persist from the view to all scopes.");
+ is(fooScope.switch, gVariablesView.switch,
+ "The switch property should persist from the view to all scopes.");
+ is(fooScope.delete, gVariablesView.delete,
+ "The delete property should persist from the view to all scopes.");
+ is(fooScope.new, gVariablesView.new,
+ "The new property should persist from the view to all scopes.");
+ isnot(fooScope.eval, fooScope.switch,
+ "The eval and switch functions got mixed up in the scope.");
+ isnot(fooScope.switch, fooScope.delete,
+ "The eval and switch functions got mixed up in the scope.");
+
+ is(barVar.preventDisableOnChange, gVariablesView.preventDisableOnChange,
+ "The preventDisableOnChange property should persist from the view to all variables.");
+ is(barVar.preventDescriptorModifiers, gVariablesView.preventDescriptorModifiers,
+ "The preventDescriptorModifiers property should persist from the view to all variables.");
+ is(barVar.editableNameTooltip, gVariablesView.editableNameTooltip,
+ "The editableNameTooltip property should persist from the view to all variables.");
+ is(barVar.editableValueTooltip, gVariablesView.editableValueTooltip,
+ "The editableValueTooltip property should persist from the view to all variables.");
+ is(barVar.editButtonTooltip, gVariablesView.editButtonTooltip,
+ "The editButtonTooltip property should persist from the view to all variables.");
+ is(barVar.deleteButtonTooltip, gVariablesView.deleteButtonTooltip,
+ "The deleteButtonTooltip property should persist from the view to all variables.");
+ is(barVar.contextMenuId, gVariablesView.contextMenuId,
+ "The contextMenuId property should persist from the view to all variables.");
+ is(barVar.separatorStr, gVariablesView.separatorStr,
+ "The separatorStr property should persist from the view to all variables.");
+ is(barVar.eval, gVariablesView.eval,
+ "The eval property should persist from the view to all variables.");
+ is(barVar.switch, gVariablesView.switch,
+ "The switch property should persist from the view to all variables.");
+ is(barVar.delete, gVariablesView.delete,
+ "The delete property should persist from the view to all variables.");
+ is(barVar.new, gVariablesView.new,
+ "The new property should persist from the view to all variables.");
+ isnot(barVar.eval, barVar.switch,
+ "The eval and switch functions got mixed up in the variable.");
+ isnot(barVar.switch, barVar.delete,
+ "The eval and switch functions got mixed up in the variable.");
+
+ is(bazProperty.preventDisableOnChange, gVariablesView.preventDisableOnChange,
+ "The preventDisableOnChange property should persist from the view to all properties.");
+ is(bazProperty.preventDescriptorModifiers, gVariablesView.preventDescriptorModifiers,
+ "The preventDescriptorModifiers property should persist from the view to all properties.");
+ is(bazProperty.editableNameTooltip, gVariablesView.editableNameTooltip,
+ "The editableNameTooltip property should persist from the view to all properties.");
+ is(bazProperty.editableValueTooltip, gVariablesView.editableValueTooltip,
+ "The editableValueTooltip property should persist from the view to all properties.");
+ is(bazProperty.editButtonTooltip, gVariablesView.editButtonTooltip,
+ "The editButtonTooltip property should persist from the view to all properties.");
+ is(bazProperty.deleteButtonTooltip, gVariablesView.deleteButtonTooltip,
+ "The deleteButtonTooltip property should persist from the view to all properties.");
+ is(bazProperty.contextMenuId, gVariablesView.contextMenuId,
+ "The contextMenuId property should persist from the view to all properties.");
+ is(bazProperty.separatorStr, gVariablesView.separatorStr,
+ "The separatorStr property should persist from the view to all properties.");
+ is(bazProperty.eval, gVariablesView.eval,
+ "The eval property should persist from the view to all properties.");
+ is(bazProperty.switch, gVariablesView.switch,
+ "The switch property should persist from the view to all properties.");
+ is(bazProperty.delete, gVariablesView.delete,
+ "The delete property should persist from the view to all properties.");
+ is(bazProperty.new, gVariablesView.new,
+ "The new property should persist from the view to all properties.");
+ isnot(bazProperty.eval, bazProperty.switch,
+ "The eval and switch functions got mixed up in the property.");
+ isnot(bazProperty.switch, bazProperty.delete,
+ "The eval and switch functions got mixed up in the property.");
+}
+
+function testClearHierarchy() {
+ gVariablesView.clearHierarchy();
+ ok(!gVariablesView._prevHierarchy.size,
+ "The previous hierarchy should have been cleared.");
+ ok(!gVariablesView._currHierarchy.size,
+ "The current hierarchy should have been cleared.");
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gVariablesView = null;
+ gScope = null;
+ gVariable = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-edit-cancel.js b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-edit-cancel.js
new file mode 100644
index 000000000..dd4954717
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-edit-cancel.js
@@ -0,0 +1,58 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure that canceling a name change correctly unhides the separator and
+ * value elements.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_watch-expressions.html";
+
+function test() {
+ Task.spawn(function* () {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ let [tab,, panel] = yield initDebugger(TAB_URL, options);
+ let win = panel.panelWin;
+ let vars = win.DebuggerView.Variables;
+
+ win.DebuggerView.WatchExpressions.addExpression("this");
+
+ callInTab(tab, "ermahgerd");
+ yield waitForDebuggerEvents(panel, win.EVENTS.FETCHED_WATCH_EXPRESSIONS);
+
+ let exprScope = vars.getScopeAtIndex(0);
+ let {target} = exprScope.get("this");
+
+ let name = target.querySelector(".title > .name");
+ let separator = target.querySelector(".separator");
+ let value = target.querySelector(".value");
+
+ is(separator.hidden, false,
+ "The separator element should not be hidden.");
+ is(value.hidden, false,
+ "The value element should not be hidden.");
+
+ for (let key of ["ESCAPE", "RETURN"]) {
+ EventUtils.sendMouseEvent({ type: "dblclick" }, name, win);
+
+ is(separator.hidden, true,
+ "The separator element should be hidden.");
+ is(value.hidden, true,
+ "The value element should be hidden.");
+
+ EventUtils.sendKey(key, win);
+
+ is(separator.hidden, false,
+ "The separator element should not be hidden.");
+ is(value.hidden, false,
+ "The value element should not be hidden.");
+ }
+
+ yield resumeDebuggerThenCloseAndFinish(panel);
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-edit-click.js b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-edit-click.js
new file mode 100644
index 000000000..9ea9230ef
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-edit-click.js
@@ -0,0 +1,58 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Check that the editing state of a Variable is correctly tracked. Clicking on
+ * the textbox while editing should not cancel editing.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_watch-expressions.html";
+
+function test() {
+ Task.spawn(function* () {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ let [tab, debuggee, panel] = yield initDebugger(TAB_URL, options);
+ let win = panel.panelWin;
+ let vars = win.DebuggerView.Variables;
+
+ win.DebuggerView.WatchExpressions.addExpression("this");
+
+ // Allow this generator function to yield first.
+ executeSoon(() => debuggee.ermahgerd());
+ yield waitForDebuggerEvents(panel, win.EVENTS.FETCHED_WATCH_EXPRESSIONS);
+
+ let exprScope = vars.getScopeAtIndex(0);
+ let exprVar = exprScope.get("this");
+ let name = exprVar.target.querySelector(".title > .name");
+
+ is(exprVar.editing, false,
+ "The expression should indicate it is not being edited.");
+
+ EventUtils.sendMouseEvent({ type: "dblclick" }, name, win);
+ let input = exprVar.target.querySelector(".title > .element-name-input");
+ is(exprVar.editing, true,
+ "The expression should indicate it is being edited.");
+ is(input.selectionStart !== input.selectionEnd, true,
+ "The expression text should be selected.");
+
+ EventUtils.synthesizeMouse(input, 2, 2, {}, win);
+ is(exprVar.editing, true,
+ "The expression should indicate it is still being edited after a click.");
+ is(input.selectionStart === input.selectionEnd, true,
+ "The expression text should not be selected.");
+
+ EventUtils.sendKey("ESCAPE", win);
+ is(exprVar.editing, false,
+ "The expression should indicate it is not being edited after cancelling.");
+
+ // Why is this needed?
+ EventUtils.synthesizeMouse(vars.parentNode, 2, 2, {}, win);
+
+ yield resumeDebuggerThenCloseAndFinish(panel);
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-edit-getset-01.js b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-edit-getset-01.js
new file mode 100644
index 000000000..5b5fce266
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-edit-getset-01.js
@@ -0,0 +1,300 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure that the variables view knows how to edit getters and setters.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_frame-parameters.html";
+
+var gTab, gPanel, gDebugger;
+var gL10N, gEditor, gVars, gWatch;
+
+function test() {
+ // Debug test slaves are a bit slow at this test.
+ requestLongerTimeout(2);
+
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gL10N = gDebugger.L10N;
+ gEditor = gDebugger.DebuggerView.editor;
+ gVars = gDebugger.DebuggerView.Variables;
+ gWatch = gDebugger.DebuggerView.WatchExpressions;
+
+ gVars.switch = function () {};
+ gVars.delete = function () {};
+
+ waitForCaretAndScopes(gPanel, 24)
+ .then(() => addWatchExpressions())
+ .then(() => testEdit("set", "this._prop = value + ' BEER CAN'", {
+ "myVar.prop": "xlerb BEER CAN",
+ "myVar.prop + 42": "xlerb BEER CAN42",
+ "myVar.prop = 'xlerb'": "xlerb"
+ }))
+ .then(() => testEdit("set", "{ this._prop = value + ' BEACON' }", {
+ "myVar.prop": "xlerb BEACON",
+ "myVar.prop + 42": "xlerb BEACON42",
+ "myVar.prop = 'xlerb'": "xlerb"
+ }))
+ .then(() => testEdit("set", "{ this._prop = value + ' BEACON;'; }", {
+ "myVar.prop": "xlerb BEACON;",
+ "myVar.prop + 42": "xlerb BEACON;42",
+ "myVar.prop = 'xlerb'": "xlerb"
+ }))
+ .then(() => testEdit("set", "{ return this._prop = value + ' BEACON;;'; }", {
+ "myVar.prop": "xlerb BEACON;;",
+ "myVar.prop + 42": "xlerb BEACON;;42",
+ "myVar.prop = 'xlerb'": "xlerb"
+ }))
+ .then(() => testEdit("set", "function(value) { this._prop = value + ' BACON' }", {
+ "myVar.prop": "xlerb BACON",
+ "myVar.prop + 42": "xlerb BACON42",
+ "myVar.prop = 'xlerb'": "xlerb"
+ }))
+ .then(() => testEdit("get", "'brelx BEER CAN'", {
+ "myVar.prop": "brelx BEER CAN",
+ "myVar.prop + 42": "brelx BEER CAN42",
+ "myVar.prop = 'xlerb'": "xlerb"
+ }))
+ .then(() => testEdit("get", "{ 'brelx BEACON' }", {
+ "myVar.prop": undefined,
+ "myVar.prop + 42": NaN,
+ "myVar.prop = 'xlerb'": "xlerb"
+ }))
+ .then(() => testEdit("get", "{ 'brelx BEACON;'; }", {
+ "myVar.prop": undefined,
+ "myVar.prop + 42": NaN,
+ "myVar.prop = 'xlerb'": "xlerb"
+ }))
+ .then(() => testEdit("get", "{ return 'brelx BEACON;;'; }", {
+ "myVar.prop": "brelx BEACON;;",
+ "myVar.prop + 42": "brelx BEACON;;42",
+ "myVar.prop = 'xlerb'": "xlerb"
+ }))
+ .then(() => testEdit("get", "function() { return 'brelx BACON'; }", {
+ "myVar.prop": "brelx BACON",
+ "myVar.prop + 42": "brelx BACON42",
+ "myVar.prop = 'xlerb'": "xlerb"
+ }))
+ .then(() => testEdit("get", "bogus", {
+ "myVar.prop": "ReferenceError: bogus is not defined",
+ "myVar.prop + 42": "ReferenceError: bogus is not defined",
+ "myVar.prop = 'xlerb'": "xlerb"
+ }))
+ .then(() => testEdit("set", "sugob", {
+ "myVar.prop": "ReferenceError: bogus is not defined",
+ "myVar.prop + 42": "ReferenceError: bogus is not defined",
+ "myVar.prop = 'xlerb'": "ReferenceError: sugob is not defined"
+ }))
+ .then(() => testEdit("get", "", {
+ "myVar.prop": undefined,
+ "myVar.prop + 42": NaN,
+ "myVar.prop = 'xlerb'": "ReferenceError: sugob is not defined"
+ }))
+ .then(() => testEdit("set", "", {
+ "myVar.prop": "xlerb",
+ "myVar.prop + 42": NaN,
+ "myVar.prop = 'xlerb'": "xlerb"
+ }))
+ .then(() => deleteWatchExpression("myVar.prop = 'xlerb'"))
+ .then(() => testEdit("self", "2507", {
+ "myVar.prop": 2507,
+ "myVar.prop + 42": 2549
+ }))
+ .then(() => deleteWatchExpression("myVar.prop + 42"))
+ .then(() => testEdit("self", "0910", {
+ "myVar.prop": 910
+ }))
+ .then(() => deleteLastWatchExpression("myVar.prop"))
+ .then(() => testWatchExpressionsRemoved())
+ .then(() => resumeDebuggerThenCloseAndFinish(gPanel))
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+
+ generateMouseClickInTab(gTab, "content.document.querySelector('button')");
+ });
+}
+
+function addWatchExpressions() {
+ return promise.resolve(null)
+ .then(() => {
+ let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_WATCH_EXPRESSIONS);
+ gWatch.addExpression("myVar.prop");
+ gEditor.focus();
+ return finished;
+ })
+ .then(() => {
+ let exprScope = gVars.getScopeAtIndex(0);
+ ok(exprScope,
+ "There should be a wach expressions scope in the variables view.");
+ is(exprScope.name, gL10N.getStr("watchExpressionsScopeLabel"),
+ "The scope's name should be marked as 'Watch Expressions'.");
+ is(exprScope._store.size, 1,
+ "There should be 1 evaluation available.");
+
+ let w1 = exprScope.get("myVar.prop");
+ let w2 = exprScope.get("myVar.prop + 42");
+ let w3 = exprScope.get("myVar.prop = 'xlerb'");
+
+ ok(w1, "The first watch expression should be present in the scope.");
+ ok(!w2, "The second watch expression should not be present in the scope.");
+ ok(!w3, "The third watch expression should not be present in the scope.");
+
+ is(w1.value, 42, "The first value is correct.");
+ })
+ .then(() => {
+ let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_WATCH_EXPRESSIONS);
+ gWatch.addExpression("myVar.prop + 42");
+ gEditor.focus();
+ return finished;
+ })
+ .then(() => {
+ let exprScope = gVars.getScopeAtIndex(0);
+ ok(exprScope,
+ "There should be a wach expressions scope in the variables view.");
+ is(exprScope.name, gL10N.getStr("watchExpressionsScopeLabel"),
+ "The scope's name should be marked as 'Watch Expressions'.");
+ is(exprScope._store.size, 2,
+ "There should be 2 evaluations available.");
+
+ let w1 = exprScope.get("myVar.prop");
+ let w2 = exprScope.get("myVar.prop + 42");
+ let w3 = exprScope.get("myVar.prop = 'xlerb'");
+
+ ok(w1, "The first watch expression should be present in the scope.");
+ ok(w2, "The second watch expression should be present in the scope.");
+ ok(!w3, "The third watch expression should not be present in the scope.");
+
+ is(w1.value, "42", "The first expression value is correct.");
+ is(w2.value, "84", "The second expression value is correct.");
+ })
+ .then(() => {
+ let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_WATCH_EXPRESSIONS);
+ gWatch.addExpression("myVar.prop = 'xlerb'");
+ gEditor.focus();
+ return finished;
+ })
+ .then(() => {
+ let exprScope = gVars.getScopeAtIndex(0);
+ ok(exprScope,
+ "There should be a wach expressions scope in the variables view.");
+ is(exprScope.name, gL10N.getStr("watchExpressionsScopeLabel"),
+ "The scope's name should be marked as 'Watch Expressions'.");
+ is(exprScope._store.size, 3,
+ "There should be 3 evaluations available.");
+
+ let w1 = exprScope.get("myVar.prop");
+ let w2 = exprScope.get("myVar.prop + 42");
+ let w3 = exprScope.get("myVar.prop = 'xlerb'");
+
+ ok(w1, "The first watch expression should be present in the scope.");
+ ok(w2, "The second watch expression should be present in the scope.");
+ ok(w3, "The third watch expression should be present in the scope.");
+
+ is(w1.value, "xlerb", "The first expression value is correct.");
+ is(w2.value, "xlerb42", "The second expression value is correct.");
+ is(w3.value, "xlerb", "The third expression value is correct.");
+ });
+}
+
+function deleteWatchExpression(aString) {
+ let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_WATCH_EXPRESSIONS);
+ gWatch.deleteExpression({ name: aString });
+ return finished;
+}
+
+function deleteLastWatchExpression(aString) {
+ let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES);
+ gWatch.deleteExpression({ name: aString });
+ return finished;
+}
+
+function testEdit(aWhat, aString, aExpected) {
+ let localScope = gVars.getScopeAtIndex(1);
+ let myVar = localScope.get("myVar");
+
+ let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_PROPERTIES).then(() => {
+ let propVar = myVar.get("prop");
+ let getterOrSetterOrVar = aWhat != "self" ? propVar.get(aWhat) : propVar;
+
+ let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_WATCH_EXPRESSIONS).then(() => {
+ let exprScope = gVars.getScopeAtIndex(0);
+ ok(exprScope,
+ "There should be a wach expressions scope in the variables view.");
+ is(exprScope.name, gL10N.getStr("watchExpressionsScopeLabel"),
+ "The scope's name should be marked as 'Watch Expressions'.");
+ is(exprScope._store.size, Object.keys(aExpected).length,
+ "There should be a certain number of evaluations available.");
+
+ function testExpression(aExpression) {
+ if (!aExpression) {
+ return;
+ }
+ let value = aExpected[aExpression.name];
+ if (isNaN(value)) {
+ ok(isNaN(aExpression.value),
+ "The expression value is correct after the edit.");
+ } else if (value == null) {
+ is(aExpression.value.type, value + "",
+ "The expression value is correct after the edit.");
+ } else {
+ is(aExpression.value, value,
+ "The expression value is correct after the edit.");
+ }
+ }
+
+ testExpression(exprScope.get(Object.keys(aExpected)[0]));
+ testExpression(exprScope.get(Object.keys(aExpected)[1]));
+ testExpression(exprScope.get(Object.keys(aExpected)[2]));
+ });
+
+ let editTarget = getterOrSetterOrVar.target;
+
+ // Allow the target variable to get painted, so that clicking on
+ // its value would scroll the new textbox node into view.
+ executeSoon(() => {
+ let varValue = editTarget.querySelector(".title > .value");
+ EventUtils.sendMouseEvent({ type: "mousedown" }, varValue, gDebugger);
+
+ let varInput = editTarget.querySelector(".title > .element-value-input");
+ setText(varInput, aString);
+ EventUtils.sendKey("RETURN", gDebugger);
+ });
+
+ return finished;
+ });
+
+ myVar.expand();
+ gVars.clearHierarchy();
+
+ return finished;
+}
+
+function testWatchExpressionsRemoved() {
+ let scope = gVars.getScopeAtIndex(0);
+ ok(scope,
+ "There should be a local scope in the variables view.");
+ isnot(scope.name, gL10N.getStr("watchExpressionsScopeLabel"),
+ "The scope's name should not be marked as 'Watch Expressions'.");
+ isnot(scope._store.size, 0,
+ "There should be some variables available.");
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gL10N = null;
+ gEditor = null;
+ gVars = null;
+ gWatch = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-edit-getset-02.js b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-edit-getset-02.js
new file mode 100644
index 000000000..c0455a189
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-edit-getset-02.js
@@ -0,0 +1,107 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure that the variables view is able to override getter properties
+ * to plain value properties.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_frame-parameters.html";
+
+var gTab, gPanel, gDebugger;
+var gL10N, gEditor, gVars, gWatch;
+
+function test() {
+ // Debug test slaves are a bit slow at this test.
+ requestLongerTimeout(2);
+
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gL10N = gDebugger.L10N;
+ gEditor = gDebugger.DebuggerView.editor;
+ gVars = gDebugger.DebuggerView.Variables;
+ gWatch = gDebugger.DebuggerView.WatchExpressions;
+
+ gVars.switch = function () {};
+ gVars.delete = function () {};
+
+ waitForCaretAndScopes(gPanel, 24)
+ .then(() => addWatchExpression())
+ .then(() => testEdit("\"xlerb\"", "xlerb"))
+ .then(() => resumeDebuggerThenCloseAndFinish(gPanel))
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+
+ generateMouseClickInTab(gTab, "content.document.querySelector('button')");
+ });
+}
+
+function addWatchExpression() {
+ let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_WATCH_EXPRESSIONS);
+
+ gWatch.addExpression("myVar.prop");
+ gEditor.focus();
+
+ return finished;
+}
+
+function testEdit(aString, aExpected) {
+ let localScope = gVars.getScopeAtIndex(1);
+ let myVar = localScope.get("myVar");
+
+ let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_PROPERTIES).then(() => {
+ let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_WATCH_EXPRESSIONS).then(() => {
+ let exprScope = gVars.getScopeAtIndex(0);
+
+ ok(exprScope,
+ "There should be a wach expressions scope in the variables view.");
+ is(exprScope.name, gL10N.getStr("watchExpressionsScopeLabel"),
+ "The scope's name should be marked as 'Watch Expressions'.");
+ is(exprScope._store.size, 1,
+ "There should be one evaluation available.");
+
+ is(exprScope.get("myVar.prop").value, aExpected,
+ "The expression value is correct after the edit.");
+ });
+
+ let editTarget = myVar.get("prop").target;
+
+ // Allow the target variable to get painted, so that clicking on
+ // its value would scroll the new textbox node into view.
+ executeSoon(() => {
+ let varEdit = editTarget.querySelector(".title > .variables-view-edit");
+ EventUtils.sendMouseEvent({ type: "mousedown" }, varEdit, gDebugger);
+
+ let varInput = editTarget.querySelector(".title > .element-value-input");
+ setText(varInput, aString);
+ EventUtils.sendKey("RETURN", gDebugger);
+ });
+
+ return finished;
+ });
+
+ myVar.expand();
+ gVars.clearHierarchy();
+
+ return finished;
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gL10N = null;
+ gEditor = null;
+ gVars = null;
+ gWatch = null;
+});
+
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-edit-value.js b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-edit-value.js
new file mode 100644
index 000000000..7fe887152
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-edit-value.js
@@ -0,0 +1,91 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure that the editing variables or properties values works properly.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_frame-parameters.html";
+
+var gTab, gPanel, gDebugger;
+var gVars;
+
+function test() {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gVars = gDebugger.DebuggerView.Variables;
+
+ waitForCaretAndScopes(gPanel, 24)
+ .then(() => initialChecks())
+ .then(() => testModification("a", "1"))
+ .then(() => testModification("{ a: 1 }", "Object"))
+ .then(() => testModification("[a]", "Array[1]"))
+ .then(() => testModification("b", "Object"))
+ .then(() => testModification("b.a", "1"))
+ .then(() => testModification("c.a", "1"))
+ .then(() => testModification("Infinity", "Infinity"))
+ .then(() => testModification("NaN", "NaN"))
+ .then(() => testModification("new Function", "anonymous()"))
+ .then(() => testModification("+0", "0"))
+ .then(() => testModification("-0", "-0"))
+ .then(() => testModification("Object.keys({})", "Array[0]"))
+ .then(() => testModification("document.title", '"Debugger test page"'))
+ .then(() => resumeDebuggerThenCloseAndFinish(gPanel))
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+
+ generateMouseClickInTab(gTab, "content.document.querySelector('button')");
+ });
+}
+
+function initialChecks() {
+ let localScope = gVars.getScopeAtIndex(0);
+ let aVar = localScope.get("a");
+
+ is(aVar.target.querySelector(".name").getAttribute("value"), "a",
+ "Should have the right name for 'a'.");
+ is(aVar.target.querySelector(".value").getAttribute("value"), "1",
+ "Should have the right initial value for 'a'.");
+}
+
+function testModification(aNewValue, aNewResult) {
+ let localScope = gVars.getScopeAtIndex(0);
+ let aVar = localScope.get("a");
+
+ // Allow the target variable to get painted, so that clicking on
+ // its value would scroll the new textbox node into view.
+ executeSoon(() => {
+ let varValue = aVar.target.querySelector(".title > .value");
+ EventUtils.sendMouseEvent({ type: "mousedown" }, varValue, gDebugger);
+
+ let varInput = aVar.target.querySelector(".title > .element-value-input");
+ setText(varInput, aNewValue);
+ EventUtils.sendKey("RETURN", gDebugger);
+ });
+
+ return waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES).then(() => {
+ let localScope = gVars.getScopeAtIndex(0);
+ let aVar = localScope.get("a");
+
+ is(aVar.target.querySelector(".name").getAttribute("value"), "a",
+ "Should have the right name for 'a'.");
+ is(aVar.target.querySelector(".value").getAttribute("value"), aNewResult,
+ "Should have the right new value for 'a'.");
+ });
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gVars = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-edit-watch.js b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-edit-watch.js
new file mode 100644
index 000000000..0271f3738
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-edit-watch.js
@@ -0,0 +1,510 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure that the editing or removing watch expressions works properly.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_watch-expressions.html";
+
+var gTab, gPanel, gDebugger;
+var gL10N, gEditor, gVars, gWatch;
+
+function test() {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gL10N = gDebugger.L10N;
+ gEditor = gDebugger.DebuggerView.editor;
+ gVars = gDebugger.DebuggerView.Variables;
+ gWatch = gDebugger.DebuggerView.WatchExpressions;
+
+ promise.all([
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_WATCH_EXPRESSIONS),
+ waitForCaretAndScopes(gPanel, 18)])
+ .then(() => testInitialVariablesInScope())
+ .then(() => testInitialExpressionsInScope())
+ .then(() => testModification("document.title = 42", "document.title = 43", "43", "undefined"))
+ .then(() => testIntegrity1())
+ .then(() => testModification("aArg", "aArg = 44", "44", "44"))
+ .then(() => testIntegrity2())
+ .then(() => testModification("aArg = 44", "\ \t\r\ndocument.title\ \t\r\n", "\"43\"", "44"))
+ .then(() => testIntegrity3())
+ .then(() => testModification("document.title = 43", "\ \t\r\ndocument.title\ \t\r\n", "\"43\"", "44"))
+ .then(() => testIntegrity4())
+ .then(() => testModification("document.title", "\ \t\r\n", "\"43\"", "44"))
+ .then(() => testIntegrity5())
+ .then(() => testExprDeletion("this", "44"))
+ .then(() => testIntegrity6())
+ .then(() => testExprFinalDeletion("ermahgerd", "44"))
+ .then(() => testIntegrity7())
+ .then(() => resumeDebuggerThenCloseAndFinish(gPanel))
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+
+ addExpressions();
+ callInTab(gTab, "ermahgerd");
+ });
+}
+
+function addExpressions() {
+ addExpression("this");
+ addExpression("ermahgerd");
+ addExpression("aArg");
+ addExpression("document.title");
+ addCmdExpression("document.title = 42");
+
+ is(gWatch.itemCount, 5,
+ "There should be 5 items availalble in the watch expressions view.");
+
+ is(gWatch.getItemAtIndex(4).attachment.initialExpression, "this",
+ "The first expression's initial value should be correct.");
+ is(gWatch.getItemAtIndex(3).attachment.initialExpression, "ermahgerd",
+ "The second expression's initial value should be correct.");
+ is(gWatch.getItemAtIndex(2).attachment.initialExpression, "aArg",
+ "The third expression's initial value should be correct.");
+ is(gWatch.getItemAtIndex(1).attachment.initialExpression, "document.title",
+ "The fourth expression's initial value should be correct.");
+ is(gWatch.getItemAtIndex(0).attachment.initialExpression, "document.title = 42",
+ "The fifth expression's initial value should be correct.");
+
+ is(gWatch.getItemAtIndex(4).attachment.currentExpression, "this",
+ "The first expression's current value should be correct.");
+ is(gWatch.getItemAtIndex(3).attachment.currentExpression, "ermahgerd",
+ "The second expression's current value should be correct.");
+ is(gWatch.getItemAtIndex(2).attachment.currentExpression, "aArg",
+ "The third expression's current value should be correct.");
+ is(gWatch.getItemAtIndex(1).attachment.currentExpression, "document.title",
+ "The fourth expression's current value should be correct.");
+ is(gWatch.getItemAtIndex(0).attachment.currentExpression, "document.title = 42",
+ "The fifth expression's current value should be correct.");
+}
+
+function testInitialVariablesInScope() {
+ let localScope = gVars.getScopeAtIndex(1);
+ let argVar = localScope.get("aArg");
+
+ is(argVar.visible, true,
+ "Should have the right visibility state for 'aArg'.");
+ is(argVar.name, "aArg",
+ "Should have the right name for 'aArg'.");
+ is(argVar.value.type, "undefined",
+ "Should have the right initial value for 'aArg'.");
+}
+
+function testInitialExpressionsInScope() {
+ let exprScope = gVars.getScopeAtIndex(0);
+ let thisExpr = exprScope.get("this");
+ let ermExpr = exprScope.get("ermahgerd");
+ let argExpr = exprScope.get("aArg");
+ let docExpr = exprScope.get("document.title");
+ let docExpr2 = exprScope.get("document.title = 42");
+
+ ok(exprScope,
+ "There should be a wach expressions scope in the variables view.");
+ is(exprScope.name, gL10N.getStr("watchExpressionsScopeLabel"),
+ "The scope's name should be marked as 'Watch Expressions'.");
+ is(exprScope._store.size, 5,
+ "There should be 5 evaluations available.");
+
+ is(thisExpr.visible, true,
+ "Should have the right visibility state for 'this'.");
+ is(thisExpr.target.querySelectorAll(".variables-view-delete").length, 1,
+ "Should have the one close button visible for 'this'.");
+ is(thisExpr.name, "this",
+ "Should have the right name for 'this'.");
+ is(thisExpr.value.type, "object",
+ "Should have the right value type for 'this'.");
+ is(thisExpr.value.class, "Window",
+ "Should have the right value type for 'this'.");
+
+ is(ermExpr.visible, true,
+ "Should have the right visibility state for 'ermahgerd'.");
+ is(ermExpr.target.querySelectorAll(".variables-view-delete").length, 1,
+ "Should have the one close button visible for 'ermahgerd'.");
+ is(ermExpr.name, "ermahgerd",
+ "Should have the right name for 'ermahgerd'.");
+ is(ermExpr.value.type, "object",
+ "Should have the right value type for 'ermahgerd'.");
+ is(ermExpr.value.class, "Function",
+ "Should have the right value type for 'ermahgerd'.");
+
+ is(argExpr.visible, true,
+ "Should have the right visibility state for 'aArg'.");
+ is(argExpr.target.querySelectorAll(".variables-view-delete").length, 1,
+ "Should have the one close button visible for 'aArg'.");
+ is(argExpr.name, "aArg",
+ "Should have the right name for 'aArg'.");
+ is(argExpr.value.type, "undefined",
+ "Should have the right value for 'aArg'.");
+
+ is(docExpr.visible, true,
+ "Should have the right visibility state for 'document.title'.");
+ is(docExpr.target.querySelectorAll(".variables-view-delete").length, 1,
+ "Should have the one close button visible for 'document.title'.");
+ is(docExpr.name, "document.title",
+ "Should have the right name for 'document.title'.");
+ is(docExpr.value, "42",
+ "Should have the right value for 'document.title'.");
+
+ is(docExpr2.visible, true,
+ "Should have the right visibility state for 'document.title = 42'.");
+ is(docExpr2.target.querySelectorAll(".variables-view-delete").length, 1,
+ "Should have the one close button visible for 'document.title = 42'.");
+ is(docExpr2.name, "document.title = 42",
+ "Should have the right name for 'document.title = 42'.");
+ is(docExpr2.value, 42,
+ "Should have the right value for 'document.title = 42'.");
+
+ is(gDebugger.document.querySelectorAll(".dbg-expression[hidden=true]").length, 5,
+ "There should be 5 hidden nodes in the watch expressions container.");
+ is(gDebugger.document.querySelectorAll(".dbg-expression:not([hidden=true])").length, 0,
+ "There should be 0 visible nodes in the watch expressions container.");
+}
+
+function testModification(aName, aNewValue, aNewResult, aArgResult) {
+ let exprScope = gVars.getScopeAtIndex(0);
+ let exprVar = exprScope.get(aName);
+
+ let finished = promise.all([
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES),
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_WATCH_EXPRESSIONS)
+ ])
+ .then(() => {
+ let localScope = gVars.getScopeAtIndex(1);
+ let argVar = localScope.get("aArg");
+
+ is(argVar.visible, true,
+ "Should have the right visibility state for 'aArg'.");
+ is(argVar.target.querySelector(".name").getAttribute("value"), "aArg",
+ "Should have the right name for 'aArg'.");
+ is(argVar.target.querySelector(".value").getAttribute("value"), aArgResult,
+ "Should have the right new value for 'aArg'.");
+
+ let exprScope = gVars.getScopeAtIndex(0);
+ let exprOldVar = exprScope.get(aName);
+ let exprNewVar = exprScope.get(aNewValue.trim());
+
+ if (!aNewValue.trim()) {
+ ok(!exprOldVar,
+ "The old watch expression should have been removed.");
+ ok(!exprNewVar,
+ "No new watch expression should have been added.");
+ } else {
+ ok(!exprOldVar,
+ "The old watch expression should have been removed.");
+ ok(exprNewVar,
+ "The new watch expression should have been added.");
+
+ is(exprNewVar.visible, true,
+ "Should have the right visibility state for the watch expression.");
+ is(exprNewVar.target.querySelector(".name").getAttribute("value"), aNewValue.trim(),
+ "Should have the right name for the watch expression.");
+ is(exprNewVar.target.querySelector(".value").getAttribute("value"), aNewResult,
+ "Should have the right new value for the watch expression.");
+ }
+ });
+
+ let varValue = exprVar.target.querySelector(".title > .name");
+ EventUtils.sendMouseEvent({ type: "dblclick" }, varValue, gDebugger);
+
+ let varInput = exprVar.target.querySelector(".title > .element-name-input");
+ setText(varInput, aNewValue);
+ EventUtils.sendKey("RETURN", gDebugger);
+
+ return finished;
+}
+
+function testExprDeletion(aName, aArgResult) {
+ let exprScope = gVars.getScopeAtIndex(0);
+ let exprVar = exprScope.get(aName);
+
+ let finished = promise.all([
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES),
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_WATCH_EXPRESSIONS)
+ ])
+ .then(() => {
+ let localScope = gVars.getScopeAtIndex(1);
+ let argVar = localScope.get("aArg");
+
+ is(argVar.visible, true,
+ "Should have the right visibility state for 'aArg'.");
+ is(argVar.target.querySelector(".name").getAttribute("value"), "aArg",
+ "Should have the right name for 'aArg'.");
+ is(argVar.target.querySelector(".value").getAttribute("value"), aArgResult,
+ "Should have the right new value for 'aArg'.");
+
+ let exprScope = gVars.getScopeAtIndex(0);
+ let exprOldVar = exprScope.get(aName);
+
+ ok(!exprOldVar,
+ "The watch expression should have been deleted.");
+ });
+
+ let varDelete = exprVar.target.querySelector(".variables-view-delete");
+ EventUtils.sendMouseEvent({ type: "click" }, varDelete, gDebugger);
+
+ return finished;
+}
+
+function testExprFinalDeletion(aName, aArgResult) {
+ let exprScope = gVars.getScopeAtIndex(0);
+ let exprVar = exprScope.get(aName);
+
+ let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES).then(() => {
+ let localScope = gVars.getScopeAtIndex(0);
+ let argVar = localScope.get("aArg");
+
+ is(argVar.visible, true,
+ "Should have the right visibility state for 'aArg'.");
+ is(argVar.target.querySelector(".name").getAttribute("value"), "aArg",
+ "Should have the right name for 'aArg'.");
+ is(argVar.target.querySelector(".value").getAttribute("value"), aArgResult,
+ "Should have the right new value for 'aArg'.");
+
+ let exprScope = gVars.getScopeAtIndex(0);
+ let exprOldVar = exprScope.get(aName);
+
+ ok(!exprOldVar,
+ "The watch expression should have been deleted.");
+ });
+
+ let varDelete = exprVar.target.querySelector(".variables-view-delete");
+ EventUtils.sendMouseEvent({ type: "click" }, varDelete, gDebugger);
+
+ return finished;
+}
+
+function testIntegrity1() {
+ is(gDebugger.document.querySelectorAll(".dbg-expression[hidden=true]").length, 5,
+ "There should be 5 hidden nodes in the watch expressions container.");
+ is(gDebugger.document.querySelectorAll(".dbg-expression:not([hidden=true])").length, 0,
+ "There should be 0 visible nodes in the watch expressions container.");
+
+ let exprScope = gVars.getScopeAtIndex(0);
+ ok(exprScope,
+ "There should be a wach expressions scope in the variables view.");
+ is(exprScope.name, gL10N.getStr("watchExpressionsScopeLabel"),
+ "The scope's name should be marked as 'Watch Expressions'.");
+ is(exprScope._store.size, 5,
+ "There should be 5 visible evaluations available.");
+
+ is(gWatch.itemCount, 5,
+ "There should be 5 hidden expression input available.");
+ is(gWatch.getItemAtIndex(0).attachment.view.inputNode.value, "document.title = 43",
+ "The first textbox input value is not the correct one.");
+ is(gWatch.getItemAtIndex(0).attachment.currentExpression, "document.title = 43",
+ "The first textbox input value is not the correct one.");
+ is(gWatch.getItemAtIndex(1).attachment.view.inputNode.value, "document.title",
+ "The second textbox input value is not the correct one.");
+ is(gWatch.getItemAtIndex(1).attachment.currentExpression, "document.title",
+ "The second textbox input value is not the correct one.");
+ is(gWatch.getItemAtIndex(2).attachment.view.inputNode.value, "aArg",
+ "The third textbox input value is not the correct one.");
+ is(gWatch.getItemAtIndex(2).attachment.currentExpression, "aArg",
+ "The third textbox input value is not the correct one.");
+ is(gWatch.getItemAtIndex(3).attachment.view.inputNode.value, "ermahgerd",
+ "The fourth textbox input value is not the correct one.");
+ is(gWatch.getItemAtIndex(3).attachment.currentExpression, "ermahgerd",
+ "The fourth textbox input value is not the correct one.");
+ is(gWatch.getItemAtIndex(4).attachment.view.inputNode.value, "this",
+ "The fifth textbox input value is not the correct one.");
+ is(gWatch.getItemAtIndex(4).attachment.currentExpression, "this",
+ "The fifth textbox input value is not the correct one.");
+}
+
+function testIntegrity2() {
+ is(gDebugger.document.querySelectorAll(".dbg-expression[hidden=true]").length, 5,
+ "There should be 5 hidden nodes in the watch expressions container.");
+ is(gDebugger.document.querySelectorAll(".dbg-expression:not([hidden=true])").length, 0,
+ "There should be 0 visible nodes in the watch expressions container.");
+
+ let exprScope = gVars.getScopeAtIndex(0);
+ ok(exprScope,
+ "There should be a wach expressions scope in the variables view.");
+ is(exprScope.name, gL10N.getStr("watchExpressionsScopeLabel"),
+ "The scope's name should be marked as 'Watch Expressions'.");
+ is(exprScope._store.size, 5,
+ "There should be 5 visible evaluations available.");
+
+ is(gWatch.itemCount, 5,
+ "There should be 5 hidden expression input available.");
+ is(gWatch.getItemAtIndex(0).attachment.view.inputNode.value, "document.title = 43",
+ "The first textbox input value is not the correct one.");
+ is(gWatch.getItemAtIndex(0).attachment.currentExpression, "document.title = 43",
+ "The first textbox input value is not the correct one.");
+ is(gWatch.getItemAtIndex(1).attachment.view.inputNode.value, "document.title",
+ "The second textbox input value is not the correct one.");
+ is(gWatch.getItemAtIndex(1).attachment.currentExpression, "document.title",
+ "The second textbox input value is not the correct one.");
+ is(gWatch.getItemAtIndex(2).attachment.view.inputNode.value, "aArg = 44",
+ "The third textbox input value is not the correct one.");
+ is(gWatch.getItemAtIndex(2).attachment.currentExpression, "aArg = 44",
+ "The third textbox input value is not the correct one.");
+ is(gWatch.getItemAtIndex(3).attachment.view.inputNode.value, "ermahgerd",
+ "The fourth textbox input value is not the correct one.");
+ is(gWatch.getItemAtIndex(3).attachment.currentExpression, "ermahgerd",
+ "The fourth textbox input value is not the correct one.");
+ is(gWatch.getItemAtIndex(4).attachment.view.inputNode.value, "this",
+ "The fifth textbox input value is not the correct one.");
+ is(gWatch.getItemAtIndex(4).attachment.currentExpression, "this",
+ "The fifth textbox input value is not the correct one.");
+}
+
+function testIntegrity3() {
+ is(gDebugger.document.querySelectorAll(".dbg-expression[hidden=true]").length, 4,
+ "There should be 4 hidden nodes in the watch expressions container.");
+ is(gDebugger.document.querySelectorAll(".dbg-expression:not([hidden=true])").length, 0,
+ "There should be 0 visible nodes in the watch expressions container.");
+
+ let exprScope = gVars.getScopeAtIndex(0);
+ ok(exprScope,
+ "There should be a wach expressions scope in the variables view.");
+ is(exprScope.name, gL10N.getStr("watchExpressionsScopeLabel"),
+ "The scope's name should be marked as 'Watch Expressions'.");
+ is(exprScope._store.size, 4,
+ "There should be 4 visible evaluations available.");
+
+ is(gWatch.itemCount, 4,
+ "There should be 4 hidden expression input available.");
+ is(gWatch.getItemAtIndex(0).attachment.view.inputNode.value, "document.title = 43",
+ "The first textbox input value is not the correct one.");
+ is(gWatch.getItemAtIndex(0).attachment.currentExpression, "document.title = 43",
+ "The first textbox input value is not the correct one.");
+ is(gWatch.getItemAtIndex(1).attachment.view.inputNode.value, "document.title",
+ "The second textbox input value is not the correct one.");
+ is(gWatch.getItemAtIndex(1).attachment.currentExpression, "document.title",
+ "The second textbox input value is not the correct one.");
+ is(gWatch.getItemAtIndex(2).attachment.view.inputNode.value, "ermahgerd",
+ "The third textbox input value is not the correct one.");
+ is(gWatch.getItemAtIndex(2).attachment.currentExpression, "ermahgerd",
+ "The third textbox input value is not the correct one.");
+ is(gWatch.getItemAtIndex(3).attachment.view.inputNode.value, "this",
+ "The fourth textbox input value is not the correct one.");
+ is(gWatch.getItemAtIndex(3).attachment.currentExpression, "this",
+ "The fourth textbox input value is not the correct one.");
+}
+
+function testIntegrity4() {
+ is(gDebugger.document.querySelectorAll(".dbg-expression[hidden=true]").length, 3,
+ "There should be 3 hidden nodes in the watch expressions container.");
+ is(gDebugger.document.querySelectorAll(".dbg-expression:not([hidden=true])").length, 0,
+ "There should be 0 visible nodes in the watch expressions container.");
+
+ let exprScope = gVars.getScopeAtIndex(0);
+ ok(exprScope,
+ "There should be a wach expressions scope in the variables view.");
+ is(exprScope.name, gL10N.getStr("watchExpressionsScopeLabel"),
+ "The scope's name should be marked as 'Watch Expressions'.");
+ is(exprScope._store.size, 3,
+ "There should be 3 visible evaluations available.");
+
+ is(gWatch.itemCount, 3,
+ "There should be 3 hidden expression input available.");
+ is(gWatch.getItemAtIndex(0).attachment.view.inputNode.value, "document.title",
+ "The first textbox input value is not the correct one.");
+ is(gWatch.getItemAtIndex(0).attachment.currentExpression, "document.title",
+ "The first textbox input value is not the correct one.");
+ is(gWatch.getItemAtIndex(1).attachment.view.inputNode.value, "ermahgerd",
+ "The second textbox input value is not the correct one.");
+ is(gWatch.getItemAtIndex(1).attachment.currentExpression, "ermahgerd",
+ "The second textbox input value is not the correct one.");
+ is(gWatch.getItemAtIndex(2).attachment.view.inputNode.value, "this",
+ "The third textbox input value is not the correct one.");
+ is(gWatch.getItemAtIndex(2).attachment.currentExpression, "this",
+ "The third textbox input value is not the correct one.");
+}
+
+function testIntegrity5() {
+ is(gDebugger.document.querySelectorAll(".dbg-expression[hidden=true]").length, 2,
+ "There should be 2 hidden nodes in the watch expressions container.");
+ is(gDebugger.document.querySelectorAll(".dbg-expression:not([hidden=true])").length, 0,
+ "There should be 0 visible nodes in the watch expressions container.");
+
+ let exprScope = gVars.getScopeAtIndex(0);
+ ok(exprScope,
+ "There should be a wach expressions scope in the variables view.");
+ is(exprScope.name, gL10N.getStr("watchExpressionsScopeLabel"),
+ "The scope's name should be marked as 'Watch Expressions'.");
+ is(exprScope._store.size, 2,
+ "There should be 2 visible evaluations available.");
+
+ is(gWatch.itemCount, 2,
+ "There should be 2 hidden expression input available.");
+ is(gWatch.getItemAtIndex(0).attachment.view.inputNode.value, "ermahgerd",
+ "The first textbox input value is not the correct one.");
+ is(gWatch.getItemAtIndex(0).attachment.currentExpression, "ermahgerd",
+ "The first textbox input value is not the correct one.");
+ is(gWatch.getItemAtIndex(1).attachment.view.inputNode.value, "this",
+ "The second textbox input value is not the correct one.");
+ is(gWatch.getItemAtIndex(1).attachment.currentExpression, "this",
+ "The second textbox input value is not the correct one.");
+}
+
+function testIntegrity6() {
+ is(gDebugger.document.querySelectorAll(".dbg-expression[hidden=true]").length, 1,
+ "There should be 1 hidden nodes in the watch expressions container.");
+ is(gDebugger.document.querySelectorAll(".dbg-expression:not([hidden=true])").length, 0,
+ "There should be 0 visible nodes in the watch expressions container.");
+
+ let exprScope = gVars.getScopeAtIndex(0);
+ ok(exprScope,
+ "There should be a wach expressions scope in the variables view.");
+ is(exprScope.name, gL10N.getStr("watchExpressionsScopeLabel"),
+ "The scope's name should be marked as 'Watch Expressions'.");
+ is(exprScope._store.size, 1,
+ "There should be 1 visible evaluation available.");
+
+ is(gWatch.itemCount, 1,
+ "There should be 1 hidden expression input available.");
+ is(gWatch.getItemAtIndex(0).attachment.view.inputNode.value, "ermahgerd",
+ "The first textbox input value is not the correct one.");
+ is(gWatch.getItemAtIndex(0).attachment.currentExpression, "ermahgerd",
+ "The first textbox input value is not the correct one.");
+}
+
+function testIntegrity7() {
+ is(gDebugger.document.querySelectorAll(".dbg-expression[hidden=true]").length, 0,
+ "There should be 0 hidden nodes in the watch expressions container.");
+ is(gDebugger.document.querySelectorAll(".dbg-expression:not([hidden=true])").length, 0,
+ "There should be 0 visible nodes in the watch expressions container.");
+
+ let localScope = gVars.getScopeAtIndex(0);
+ ok(localScope,
+ "There should be a local scope in the variables view.");
+ isnot(localScope.name, gL10N.getStr("watchExpressionsScopeLabel"),
+ "The scope's name should not be marked as 'Watch Expressions'.");
+ isnot(localScope._store.size, 0,
+ "There should be some variables available.");
+
+ is(gWatch.itemCount, 0,
+ "The watch expressions container should be empty.");
+}
+
+function addExpression(aString) {
+ gWatch.addExpression(aString);
+ gEditor.focus();
+}
+
+function addCmdExpression(aString) {
+ gWatch._onCmdAddExpression(aString);
+ gEditor.focus();
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gL10N = null;
+ gEditor = null;
+ gVars = null;
+ gWatch = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-filter-01.js b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-filter-01.js
new file mode 100644
index 000000000..f2c142c85
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-filter-01.js
@@ -0,0 +1,241 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure that the variables view correctly filters nodes by name.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_with-frame.html";
+
+var gTab, gPanel, gDebugger;
+var gVariables, gSearchBox;
+
+function test() {
+ // Debug test slaves are quite slow at this test.
+ requestLongerTimeout(4);
+
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gVariables = gDebugger.DebuggerView.Variables;
+
+ gVariables._enableSearch();
+ gSearchBox = gVariables._searchboxNode;
+
+ // The first 'with' scope should be expanded by default, but the
+ // variables haven't been fetched yet. This is how 'with' scopes work.
+ promise.all([
+ waitForCaretAndScopes(gPanel, 22),
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_VARIABLES)
+ ]).then(prepareVariablesAndProperties)
+ .then(testVariablesAndPropertiesFiltering)
+ .then(() => resumeDebuggerThenCloseAndFinish(gPanel))
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+
+ generateMouseClickInTab(gTab, "content.document.querySelector('button')");
+ });
+}
+
+function testVariablesAndPropertiesFiltering() {
+ let localScope = gVariables.getScopeAtIndex(0);
+ let withScope = gVariables.getScopeAtIndex(1);
+ let functionScope = gVariables.getScopeAtIndex(2);
+ let globalLexicalScope = gVariables.getScopeAtIndex(3);
+ let globalScope = gVariables.getScopeAtIndex(4);
+ let protoVar = localScope.get("__proto__");
+ let constrVar = protoVar.get("constructor");
+ let proto2Var = constrVar.get("__proto__");
+ let constr2Var = proto2Var.get("constructor");
+
+ function testFiltered() {
+ is(localScope.expanded, true,
+ "The localScope should be expanded.");
+ is(withScope.expanded, true,
+ "The withScope should be expanded.");
+ is(functionScope.expanded, true,
+ "The functionScope should be expanded.");
+ is(globalLexicalScope.expanded, true,
+ "The globalLexicalScope should be expanded.");
+ is(globalScope.expanded, true,
+ "The globalScope should be expanded.");
+
+ is(protoVar.expanded, true,
+ "The protoVar should be expanded.");
+ is(constrVar.expanded, true,
+ "The constrVar should be expanded.");
+ is(proto2Var.expanded, true,
+ "The proto2Var should be expanded.");
+ is(constr2Var.expanded, true,
+ "The constr2Var should be expanded.");
+
+ is(localScope.target.querySelectorAll(".variables-view-variable:not([unmatched])").length, 1,
+ "There should be 1 variable displayed in the local scope.");
+ is(withScope.target.querySelectorAll(".variables-view-variable:not([unmatched])").length, 0,
+ "There should be 0 variables displayed in the with scope.");
+ is(functionScope.target.querySelectorAll(".variables-view-variable:not([unmatched])").length, 0,
+ "There should be 0 variables displayed in the function scope.");
+ is(globalLexicalScope.target.querySelectorAll(".variables-view-variable:not([unmatched])").length, 0,
+ "There should be 0 variables displayed in the global lexical scope.");
+ is(globalScope.target.querySelectorAll(".variables-view-variable:not([unmatched])").length, 0,
+ "There should be 0 variables displayed in the global scope.");
+
+ is(withScope.target.querySelectorAll(".variables-view-property:not([unmatched])").length, 0,
+ "There should be 0 properties displayed in the with scope.");
+ is(functionScope.target.querySelectorAll(".variables-view-property:not([unmatched])").length, 0,
+ "There should be 0 properties displayed in the function scope.");
+ is(globalLexicalScope.target.querySelectorAll(".variables-view-property:not([unmatched])").length, 0,
+ "There should be 0 properties displayed in the global lexical scope.");
+ is(globalScope.target.querySelectorAll(".variables-view-property:not([unmatched])").length, 0,
+ "There should be 0 properties displayed in the global scope.");
+
+ is(localScope.target.querySelectorAll(".variables-view-variable:not([unmatched]) > .title > .name")[0].getAttribute("value"),
+ "__proto__", "The only inner variable displayed should be '__proto__'");
+ is(localScope.target.querySelectorAll(".variables-view-property:not([unmatched]) > .title > .name")[0].getAttribute("value"),
+ "constructor", "The first inner property displayed should be 'constructor'");
+ is(localScope.target.querySelectorAll(".variables-view-property:not([unmatched]) > .title > .name")[1].getAttribute("value"),
+ "__proto__", "The second inner property displayed should be '__proto__'");
+ is(localScope.target.querySelectorAll(".variables-view-property:not([unmatched]) > .title > .name")[2].getAttribute("value"),
+ "constructor", "The third inner property displayed should be 'constructor'");
+ }
+
+ function firstFilter() {
+ let expanded = once(gVariables, "fetched");
+ typeText(gSearchBox, "constructor");
+ gSearchBox.doCommand();
+ return expanded.then(testFiltered);
+ }
+
+ function secondFilter() {
+ localScope.collapse();
+ withScope.collapse();
+ functionScope.collapse();
+ globalLexicalScope.collapse();
+ globalScope.collapse();
+ protoVar.collapse();
+ constrVar.collapse();
+ proto2Var.collapse();
+ constr2Var.collapse();
+
+ is(localScope.expanded, false,
+ "The localScope should not be expanded.");
+ is(withScope.expanded, false,
+ "The withScope should not be expanded.");
+ is(functionScope.expanded, false,
+ "The functionScope should not be expanded.");
+ is(globalLexicalScope.expanded, false,
+ "The globalLexicalScope should not be expanded.");
+ is(globalScope.expanded, false,
+ "The globalScope should not be expanded.");
+
+ is(protoVar.expanded, false,
+ "The protoVar should not be expanded.");
+ is(constrVar.expanded, false,
+ "The constrVar should not be expanded.");
+ is(proto2Var.expanded, false,
+ "The proto2Var should not be expanded.");
+ is(constr2Var.expanded, false,
+ "The constr2Var should not be expanded.");
+
+ let expanded = once(gVariables, "fetched");
+ clearText(gSearchBox);
+ typeText(gSearchBox, "constructor");
+ expanded.then(testFiltered);
+ }
+
+ firstFilter().then(secondFilter);
+}
+
+function prepareVariablesAndProperties() {
+ let deferred = promise.defer();
+
+ let localScope = gVariables.getScopeAtIndex(0);
+ let withScope = gVariables.getScopeAtIndex(1);
+ let functionScope = gVariables.getScopeAtIndex(2);
+ let globalLexicalScope = gVariables.getScopeAtIndex(3);
+ let globalScope = gVariables.getScopeAtIndex(4);
+
+ is(localScope.expanded, true,
+ "The localScope should be expanded.");
+ is(withScope.expanded, false,
+ "The withScope should not be expanded yet.");
+ is(functionScope.expanded, false,
+ "The functionScope should not be expanded yet.");
+ is(globalLexicalScope.expanded, false,
+ "The globalLexicalScope should not be expanded yet.");
+ is(globalScope.expanded, false,
+ "The globalScope should not be expanded yet.");
+
+ // Wait for only two events to be triggered, because the Function scope is
+ // an environment to which scope arguments and variables are already attached.
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_VARIABLES, 2).then(() => {
+ is(localScope.expanded, true,
+ "The localScope should now be expanded.");
+ is(withScope.expanded, true,
+ "The withScope should now be expanded.");
+ is(functionScope.expanded, true,
+ "The functionScope should now be expanded.");
+ is(globalLexicalScope.expanded, true,
+ "The globalLexicalScope should be expanded.");
+ is(globalScope.expanded, true,
+ "The globalScope should now be expanded.");
+
+ let protoVar = localScope.get("__proto__");
+
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_PROPERTIES, 1).then(() => {
+ let constrVar = protoVar.get("constructor");
+
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_PROPERTIES, 1).then(() => {
+ let proto2Var = constrVar.get("__proto__");
+
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_PROPERTIES, 1).then(() => {
+ let constr2Var = proto2Var.get("constructor");
+
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_PROPERTIES, 1).then(() => {
+ is(protoVar.expanded, true,
+ "The local scope '__proto__' should be expanded.");
+ is(constrVar.expanded, true,
+ "The local scope '__proto__.constructor' should be expanded.");
+ is(proto2Var.expanded, true,
+ "The local scope '__proto__.constructor.__proto__' should be expanded.");
+ is(constr2Var.expanded, true,
+ "The local scope '__proto__.constructor.__proto__.constructor' should be expanded.");
+
+ deferred.resolve();
+ });
+
+ constr2Var.expand();
+ });
+
+ proto2Var.expand();
+ });
+
+ constrVar.expand();
+ });
+
+ protoVar.expand();
+ });
+
+ withScope.expand();
+ functionScope.expand();
+ globalLexicalScope.expand();
+ globalScope.expand();
+
+ return deferred.promise;
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gVariables = null;
+ gSearchBox = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-filter-02.js b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-filter-02.js
new file mode 100644
index 000000000..967deb3a5
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-filter-02.js
@@ -0,0 +1,249 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure that the variables view correctly filters nodes by value.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_with-frame.html";
+
+var gTab, gPanel, gDebugger;
+var gVariables, gSearchBox;
+
+function test() {
+ // Debug test slaves are quite slow at this test.
+ requestLongerTimeout(4);
+
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gVariables = gDebugger.DebuggerView.Variables;
+
+ gVariables._enableSearch();
+ gSearchBox = gVariables._searchboxNode;
+
+ // The first 'with' scope should be expanded by default, but the
+ // variables haven't been fetched yet. This is how 'with' scopes work.
+ promise.all([
+ waitForCaretAndScopes(gPanel, 22),
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_VARIABLES)
+ ]).then(prepareVariablesAndProperties)
+ .then(testVariablesAndPropertiesFiltering)
+ .then(() => resumeDebuggerThenCloseAndFinish(gPanel))
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+
+ generateMouseClickInTab(gTab, "content.document.querySelector('button')");
+ });
+}
+
+function testVariablesAndPropertiesFiltering() {
+ let localScope = gVariables.getScopeAtIndex(0);
+ let withScope = gVariables.getScopeAtIndex(1);
+ let functionScope = gVariables.getScopeAtIndex(2);
+ let globalLexicalScope = gVariables.getScopeAtIndex(3);
+ let globalScope = gVariables.getScopeAtIndex(4);
+ let protoVar = localScope.get("__proto__");
+ let constrVar = protoVar.get("constructor");
+ let proto2Var = constrVar.get("__proto__");
+ let constr2Var = proto2Var.get("constructor");
+
+ function testFiltered() {
+ is(localScope.expanded, true,
+ "The localScope should be expanded.");
+ is(withScope.expanded, true,
+ "The withScope should be expanded.");
+ is(functionScope.expanded, true,
+ "The functionScope should be expanded.");
+ is(globalLexicalScope.expanded, true,
+ "The globalScope should be expanded.");
+ is(globalScope.expanded, true,
+ "The globalScope should be expanded.");
+
+ is(protoVar.expanded, true,
+ "The protoVar should be expanded.");
+ is(constrVar.expanded, true,
+ "The constrVar should be expanded.");
+ is(proto2Var.expanded, true,
+ "The proto2Var should be expanded.");
+ is(constr2Var.expanded, true,
+ "The constr2Var should be expanded.");
+
+ is(localScope.target.querySelectorAll(".variables-view-variable:not([unmatched])").length, 1,
+ "There should be 1 variable displayed in the local scope.");
+ is(withScope.target.querySelectorAll(".variables-view-variable:not([unmatched])").length, 0,
+ "There should be 0 variables displayed in the with scope.");
+ is(functionScope.target.querySelectorAll(".variables-view-variable:not([unmatched])").length, 0,
+ "There should be 0 variables displayed in the function scope.");
+ is(globalLexicalScope.target.querySelectorAll(".variables-view-variable:not([unmatched])").length, 0,
+ "There should be no variables displayed in the global lexical scope.");
+ is(globalScope.target.querySelectorAll(".variables-view-variable:not([unmatched])").length, 0,
+ "There should be no variables displayed in the global scope.");
+
+ is(localScope.target.querySelectorAll(".variables-view-property:not([unmatched])").length, 4,
+ "There should be 4 properties displayed in the local scope.");
+ is(withScope.target.querySelectorAll(".variables-view-property:not([unmatched])").length, 0,
+ "There should be 0 properties displayed in the with scope.");
+ is(functionScope.target.querySelectorAll(".variables-view-property:not([unmatched])").length, 0,
+ "There should be 0 properties displayed in the function scope.");
+ is(globalLexicalScope.target.querySelectorAll(".variables-view-property:not([unmatched])").length, 0,
+ "There should be 0 properties displayed in the global lexical scope.");
+ is(globalScope.target.querySelectorAll(".variables-view-property:not([unmatched])").length, 0,
+ "There should be 0 properties displayed in the global scope.");
+
+ is(localScope.target.querySelectorAll(".variables-view-variable:not([unmatched]) > .title > .name")[0].getAttribute("value"),
+ "__proto__", "The only inner variable displayed should be '__proto__'");
+ is(localScope.target.querySelectorAll(".variables-view-property:not([unmatched]) > .title > .name")[0].getAttribute("value"),
+ "constructor", "The first inner property displayed should be 'constructor'");
+ is(localScope.target.querySelectorAll(".variables-view-property:not([unmatched]) > .title > .name")[1].getAttribute("value"),
+ "__proto__", "The second inner property displayed should be '__proto__'");
+ is(localScope.target.querySelectorAll(".variables-view-property:not([unmatched]) > .title > .name")[2].getAttribute("value"),
+ "constructor", "The third inner property displayed should be 'constructor'");
+
+ is(localScope.target.querySelectorAll(".variables-view-property:not([unmatched]) > .title > .name")[3].getAttribute("value"),
+ "name", "The fourth inner property displayed should be 'name'");
+ is(localScope.target.querySelectorAll(".variables-view-property:not([unmatched]) > .title > .value")[3].getAttribute("value"),
+ "\"Function\"", "The fourth inner property displayed should be '\"Function\"'");
+ }
+
+ function firstFilter() {
+ let expanded = once(gVariables, "fetched");
+ typeText(gSearchBox, "\"Function\"");
+ gSearchBox.doCommand();
+ return expanded.then(testFiltered);
+ }
+
+ function secondFilter() {
+ localScope.collapse();
+ withScope.collapse();
+ functionScope.collapse();
+ globalLexicalScope.collapse();
+ globalScope.collapse();
+ protoVar.collapse();
+ constrVar.collapse();
+ proto2Var.collapse();
+ constr2Var.collapse();
+
+ is(localScope.expanded, false,
+ "The localScope should not be expanded.");
+ is(withScope.expanded, false,
+ "The withScope should not be expanded.");
+ is(functionScope.expanded, false,
+ "The functionScope should not be expanded.");
+ is(globalLexicalScope.expanded, false,
+ "The globalScope should not be expanded.");
+ is(globalScope.expanded, false,
+ "The globalScope should not be expanded.");
+
+ is(protoVar.expanded, false,
+ "The protoVar should not be expanded.");
+ is(constrVar.expanded, false,
+ "The constrVar should not be expanded.");
+ is(proto2Var.expanded, false,
+ "The proto2Var should not be expanded.");
+ is(constr2Var.expanded, false,
+ "The constr2Var should not be expanded.");
+
+ backspaceText(gSearchBox, 10);
+ let expanded = once(gVariables, "fetched");
+ typeText(gSearchBox, "\"Function\"");
+ gSearchBox.doCommand();
+ expanded.then(testFiltered);
+ }
+
+ firstFilter().then(secondFilter);
+}
+
+function prepareVariablesAndProperties() {
+ let deferred = promise.defer();
+
+ let localScope = gVariables.getScopeAtIndex(0);
+ let withScope = gVariables.getScopeAtIndex(1);
+ let functionScope = gVariables.getScopeAtIndex(2);
+ let globalLexicalScope = gVariables.getScopeAtIndex(3);
+ let globalScope = gVariables.getScopeAtIndex(4);
+
+ is(localScope.expanded, true,
+ "The localScope should be expanded.");
+ is(withScope.expanded, false,
+ "The withScope should not be expanded yet.");
+ is(functionScope.expanded, false,
+ "The functionScope should not be expanded yet.");
+ is(globalLexicalScope.expanded, false,
+ "The globalScope should not be expanded yet.");
+ is(globalScope.expanded, false,
+ "The globalScope should not be expanded yet.");
+
+ // Wait for only two events to be triggered, because the Function scope is
+ // an environment to which scope arguments and variables are already attached.
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_VARIABLES, 2).then(() => {
+ is(localScope.expanded, true,
+ "The localScope should now be expanded.");
+ is(withScope.expanded, true,
+ "The withScope should now be expanded.");
+ is(functionScope.expanded, true,
+ "The functionScope should now be expanded.");
+ is(globalLexicalScope.expanded, true,
+ "The globalScope should now be expanded.");
+ is(globalScope.expanded, true,
+ "The globalScope should now be expanded.");
+
+ let protoVar = localScope.get("__proto__");
+
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_PROPERTIES, 1).then(() => {
+ let constrVar = protoVar.get("constructor");
+
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_PROPERTIES, 1).then(() => {
+ let proto2Var = constrVar.get("__proto__");
+
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_PROPERTIES, 1).then(() => {
+ let constr2Var = proto2Var.get("constructor");
+
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_PROPERTIES, 1).then(() => {
+ is(protoVar.expanded, true,
+ "The local scope '__proto__' should be expanded.");
+ is(constrVar.expanded, true,
+ "The local scope '__proto__.constructor' should be expanded.");
+ is(proto2Var.expanded, true,
+ "The local scope '__proto__.constructor.__proto__' should be expanded.");
+ is(constr2Var.expanded, true,
+ "The local scope '__proto__.constructor.__proto__.constructor' should be expanded.");
+
+ deferred.resolve();
+ });
+
+ constr2Var.expand();
+ });
+
+ proto2Var.expand();
+ });
+
+ constrVar.expand();
+ });
+
+ protoVar.expand();
+ });
+
+ withScope.expand();
+ functionScope.expand();
+ globalLexicalScope.expand();
+ globalScope.expand();
+
+ return deferred.promise;
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gVariables = null;
+ gSearchBox = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-filter-03.js b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-filter-03.js
new file mode 100644
index 000000000..cd4927e0f
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-filter-03.js
@@ -0,0 +1,178 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure that the variables view correctly filters nodes when triggered
+ * from the debugger's searchbox via an operator.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_with-frame.html";
+
+var gTab, gPanel, gDebugger;
+var gVariables, gSearchBox;
+
+function test() {
+ // Debug test slaves are a bit slow at this test.
+ requestLongerTimeout(2);
+
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gVariables = gDebugger.DebuggerView.Variables;
+ gSearchBox = gDebugger.DebuggerView.Filtering._searchbox;
+
+ // The first 'with' scope should be expanded by default, but the
+ // variables haven't been fetched yet. This is how 'with' scopes work.
+ promise.all([
+ waitForCaretAndScopes(gPanel, 22),
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_VARIABLES)
+ ]).then(prepareVariablesAndProperties)
+ .then(testVariablesAndPropertiesFiltering)
+ .then(() => resumeDebuggerThenCloseAndFinish(gPanel))
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+
+ generateMouseClickInTab(gTab, "content.document.querySelector('button')");
+ });
+}
+
+function testVariablesAndPropertiesFiltering() {
+ let localScope = gVariables.getScopeAtIndex(0);
+ let withScope = gVariables.getScopeAtIndex(1);
+ let functionScope = gVariables.getScopeAtIndex(2);
+ let globalLexicalScope = gVariables.getScopeAtIndex(3);
+ let globalScope = gVariables.getScopeAtIndex(4);
+
+ function testFiltered() {
+ is(localScope.expanded, true,
+ "The localScope should be expanded.");
+ is(withScope.expanded, true,
+ "The withScope should be expanded.");
+ is(functionScope.expanded, true,
+ "The functionScope should be expanded.");
+ is(globalLexicalScope.expanded, true,
+ "The globalScope should be expanded.");
+ is(globalScope.expanded, true,
+ "The globalScope should be expanded.");
+
+ is(localScope.target.querySelectorAll(".variables-view-variable:not([unmatched])").length, 1,
+ "There should be 1 variable displayed in the local scope.");
+ is(withScope.target.querySelectorAll(".variables-view-variable:not([unmatched])").length, 0,
+ "There should be 0 variables displayed in the with scope.");
+ is(functionScope.target.querySelectorAll(".variables-view-variable:not([unmatched])").length, 0,
+ "There should be 0 variables displayed in the function scope.");
+ is(globalLexicalScope.target.querySelectorAll(".variables-view-variable:not([unmatched])").length, 0,
+ "There should be 0 variables displayed in the global scope.");
+ is(globalScope.target.querySelectorAll(".variables-view-variable:not([unmatched])").length, 0,
+ "There should be 0 variables displayed in the global scope.");
+
+ is(localScope.target.querySelectorAll(".variables-view-property:not([unmatched])").length, 0,
+ "There should be 0 properties displayed in the local scope.");
+ is(withScope.target.querySelectorAll(".variables-view-property:not([unmatched])").length, 0,
+ "There should be 0 properties displayed in the with scope.");
+ is(functionScope.target.querySelectorAll(".variables-view-property:not([unmatched])").length, 0,
+ "There should be 0 properties displayed in the function scope.");
+ is(globalLexicalScope.target.querySelectorAll(".variables-view-property:not([unmatched])").length, 0,
+ "There should be 0 properties displayed in the global scope.");
+ is(globalScope.target.querySelectorAll(".variables-view-property:not([unmatched])").length, 0,
+ "There should be 0 properties displayed in the global scope.");
+ }
+
+ function firstFilter() {
+ typeText(gSearchBox, "*alpha");
+ testFiltered("alpha");
+
+ is(localScope.target.querySelectorAll(".variables-view-variable:not([unmatched]) > .title > .name")[0].getAttribute("value"),
+ "alpha", "The only inner variable displayed should be 'alpha'");
+ }
+
+ function secondFilter() {
+ localScope.collapse();
+ withScope.collapse();
+ functionScope.collapse();
+ globalLexicalScope.collapse();
+ globalScope.collapse();
+
+ is(localScope.expanded, false,
+ "The localScope should not be expanded.");
+ is(withScope.expanded, false,
+ "The withScope should not be expanded.");
+ is(functionScope.expanded, false,
+ "The functionScope should not be expanded.");
+ is(globalLexicalScope.expanded, false,
+ "The globalScope should not be expanded.");
+ is(globalScope.expanded, false,
+ "The globalScope should not be expanded.");
+
+ backspaceText(gSearchBox, 6);
+ typeText(gSearchBox, "*beta");
+ testFiltered("beta");
+
+ is(localScope.target.querySelectorAll(".variables-view-variable:not([unmatched]) > .title > .name")[0].getAttribute("value"),
+ "beta", "The only inner variable displayed should be 'beta'");
+ }
+
+ firstFilter();
+ secondFilter();
+}
+
+function prepareVariablesAndProperties() {
+ let deferred = promise.defer();
+
+ let localScope = gVariables.getScopeAtIndex(0);
+ let withScope = gVariables.getScopeAtIndex(1);
+ let functionScope = gVariables.getScopeAtIndex(2);
+ let globalLexicalScope = gVariables.getScopeAtIndex(3);
+ let globalScope = gVariables.getScopeAtIndex(4);
+
+ is(localScope.expanded, true,
+ "The localScope should be expanded.");
+ is(withScope.expanded, false,
+ "The withScope should not be expanded yet.");
+ is(functionScope.expanded, false,
+ "The functionScope should not be expanded yet.");
+ is(globalLexicalScope.expanded, false,
+ "The globalScope should not be expanded yet.");
+ is(globalScope.expanded, false,
+ "The globalScope should not be expanded yet.");
+
+ // Wait for only two events to be triggered, because the Function scope is
+ // an environment to which scope arguments and variables are already attached.
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_VARIABLES, 2).then(() => {
+ is(localScope.expanded, true,
+ "The localScope should now be expanded.");
+ is(withScope.expanded, true,
+ "The withScope should now be expanded.");
+ is(functionScope.expanded, true,
+ "The functionScope should now be expanded.");
+ is(globalLexicalScope.expanded, true,
+ "The globalScope should now be expanded.");
+ is(globalScope.expanded, true,
+ "The globalScope should now be expanded.");
+
+ deferred.resolve();
+ });
+
+ withScope.expand();
+ functionScope.expand();
+ globalLexicalScope.expand();
+ globalScope.expand();
+
+ return deferred.promise;
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gVariables = null;
+ gSearchBox = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-filter-04.js b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-filter-04.js
new file mode 100644
index 000000000..0838f1517
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-filter-04.js
@@ -0,0 +1,243 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure that the variables view correctly shows/hides nodes when various
+ * keyboard shortcuts are pressed in the debugger's searchbox.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_with-frame.html";
+
+var gTab, gPanel, gDebugger;
+var gEditor, gVariables, gSearchBox;
+
+function test() {
+ // Debug test slaves are a bit slow at this test.
+ requestLongerTimeout(2);
+
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gEditor = gDebugger.DebuggerView.editor;
+ gVariables = gDebugger.DebuggerView.Variables;
+ gSearchBox = gDebugger.DebuggerView.Filtering._searchbox;
+
+ // The first 'with' scope should be expanded by default, but the
+ // variables haven't been fetched yet. This is how 'with' scopes work.
+ promise.all([
+ waitForCaretAndScopes(gPanel, 22),
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_VARIABLES)
+ ]).then(prepareVariablesAndProperties)
+ .then(testVariablesAndPropertiesFiltering)
+ .then(() => resumeDebuggerThenCloseAndFinish(gPanel))
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+
+ generateMouseClickInTab(gTab, "content.document.querySelector('button')");
+ });
+}
+
+function testVariablesAndPropertiesFiltering() {
+ let localScope = gVariables.getScopeAtIndex(0);
+ let withScope = gVariables.getScopeAtIndex(1);
+ let functionScope = gVariables.getScopeAtIndex(2);
+ let globalLexicalScope = gVariables.getScopeAtIndex(3);
+ let globalScope = gVariables.getScopeAtIndex(4);
+ let step = 0;
+
+ let tests = [
+ function () {
+ assertExpansion([true, false, false, false, false]);
+ EventUtils.sendKey("RETURN", gDebugger);
+ },
+ function () {
+ assertExpansion([true, false, false, false, false]);
+ EventUtils.sendKey("RETURN", gDebugger);
+ },
+ function () {
+ assertExpansion([true, false, false, false, false]);
+ gEditor.focus();
+ },
+ function () {
+ assertExpansion([true, false, false, false, false]);
+ typeText(gSearchBox, "*");
+ },
+ function () {
+ assertExpansion([true, true, true, true, true]);
+ EventUtils.sendKey("RETURN", gDebugger);
+ },
+ function () {
+ assertExpansion([true, true, true, true, true]);
+ EventUtils.sendKey("RETURN", gDebugger);
+ },
+ function () {
+ assertExpansion([true, true, true, true, true]);
+ gEditor.focus();
+ },
+ function () {
+ assertExpansion([true, true, true, true, true]);
+ backspaceText(gSearchBox, 1);
+ },
+ function () {
+ assertExpansion([true, true, true, true, true]);
+ EventUtils.sendKey("RETURN", gDebugger);
+ },
+ function () {
+ assertExpansion([true, true, true, true, true]);
+ EventUtils.sendKey("RETURN", gDebugger);
+ },
+ function () {
+ assertExpansion([true, true, true, true, true]);
+ gEditor.focus();
+ },
+ function () {
+ assertExpansion([true, true, true, true, true]);
+ localScope.collapse();
+ withScope.collapse();
+ functionScope.collapse();
+ globalLexicalScope.collapse();
+ globalScope.collapse();
+ },
+ function () {
+ assertExpansion([false, false, false, false, false]);
+ EventUtils.sendKey("RETURN", gDebugger);
+ },
+ function () {
+ assertExpansion([false, false, false, false, false]);
+ EventUtils.sendKey("RETURN", gDebugger);
+ },
+ function () {
+ assertExpansion([false, false, false, false, false]);
+ gEditor.focus();
+ },
+ function () {
+ assertExpansion([false, false, false, false, false]);
+ clearText(gSearchBox);
+ typeText(gSearchBox, "*");
+ },
+ function () {
+ assertExpansion([true, true, true, true, true]);
+ EventUtils.sendKey("RETURN", gDebugger);
+ },
+ function () {
+ assertExpansion([true, true, true, true, true]);
+ EventUtils.sendKey("RETURN", gDebugger);
+ },
+ function () {
+ assertExpansion([true, true, true, true, true]);
+ gEditor.focus();
+ },
+ function () {
+ assertExpansion([true, true, true, true, true]);
+ backspaceText(gSearchBox, 1);
+ },
+ function () {
+ assertExpansion([true, true, true, true, true]);
+ EventUtils.sendKey("RETURN", gDebugger);
+ },
+ function () {
+ assertExpansion([true, true, true, true, true]);
+ EventUtils.sendKey("RETURN", gDebugger);
+ },
+ function () {
+ assertExpansion([true, true, true, true, true]);
+ gEditor.focus();
+ },
+ function () {
+ assertExpansion([true, true, true, true, true]);
+ }
+ ];
+
+ function assertExpansion(aFlags) {
+ is(localScope.expanded, aFlags[0],
+ "The localScope should " + (aFlags[0] ? "" : "not ") +
+ "be expanded at this point (" + step + ").");
+
+ is(withScope.expanded, aFlags[1],
+ "The withScope should " + (aFlags[1] ? "" : "not ") +
+ "be expanded at this point (" + step + ").");
+
+ is(functionScope.expanded, aFlags[2],
+ "The functionScope should " + (aFlags[2] ? "" : "not ") +
+ "be expanded at this point (" + step + ").");
+
+ is(globalLexicalScope.expanded, aFlags[3],
+ "The globalLexicalScope should " + (aFlags[3] ? "" : "not ") +
+ "be expanded at this point (" + step + ").");
+
+ is(globalScope.expanded, aFlags[4],
+ "The globalScope should " + (aFlags[4] ? "" : "not ") +
+ "be expanded at this point (" + step + ").");
+
+ step++;
+ }
+
+ return promise.all(tests.map(f => f()));
+}
+
+function prepareVariablesAndProperties() {
+ let deferred = promise.defer();
+
+ let localScope = gVariables.getScopeAtIndex(0);
+ let withScope = gVariables.getScopeAtIndex(1);
+ let functionScope = gVariables.getScopeAtIndex(2);
+ let globalLexicalScope = gVariables.getScopeAtIndex(3);
+ let globalScope = gVariables.getScopeAtIndex(4);
+
+ is(localScope.expanded, true,
+ "The localScope should be expanded.");
+ is(withScope.expanded, false,
+ "The withScope should not be expanded yet.");
+ is(functionScope.expanded, false,
+ "The functionScope should not be expanded yet.");
+ is(globalLexicalScope.expanded, false,
+ "The globalLexicalScope should not be expanded yet.");
+ is(globalScope.expanded, false,
+ "The globalScope should not be expanded yet.");
+
+ // Wait for only two events to be triggered, because the Function scope is
+ // an environment to which scope arguments and variables are already attached.
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_VARIABLES, 2).then(() => {
+ is(localScope.expanded, true,
+ "The localScope should now be expanded.");
+ is(withScope.expanded, true,
+ "The withScope should now be expanded.");
+ is(functionScope.expanded, true,
+ "The functionScope should now be expanded.");
+ is(globalLexicalScope.expanded, true,
+ "The globalLexicalScope should now be expanded.");
+ is(globalScope.expanded, true,
+ "The globalScope should now be expanded.");
+
+ withScope.collapse();
+ functionScope.collapse();
+ globalLexicalScope.collapse();
+ globalScope.collapse();
+
+ deferred.resolve();
+ });
+
+ withScope.expand();
+ functionScope.expand();
+ globalLexicalScope.expand();
+ globalScope.expand();
+
+ return deferred.promise;
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gEditor = null;
+ gVariables = null;
+ gSearchBox = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-filter-05.js b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-filter-05.js
new file mode 100644
index 000000000..4390955eb
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-filter-05.js
@@ -0,0 +1,254 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure that the variables view correctly shows/hides nodes when various
+ * keyboard shortcuts are pressed in the debugger's searchbox.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_with-frame.html";
+
+var gTab, gPanel, gDebugger;
+var gVariables, gSearchBox;
+
+function test() {
+ // Debug test slaves are a bit slow at this test.
+ requestLongerTimeout(2);
+
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gVariables = gDebugger.DebuggerView.Variables;
+ gSearchBox = gDebugger.DebuggerView.Filtering._searchbox;
+
+ // The first 'with' scope should be expanded by default, but the
+ // variables haven't been fetched yet. This is how 'with' scopes work.
+ promise.all([
+ waitForCaretAndScopes(gPanel, 22),
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_VARIABLES)
+ ]).then(prepareVariablesAndProperties)
+ .then(testVariablesAndPropertiesFiltering)
+ .then(() => resumeDebuggerThenCloseAndFinish(gPanel))
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+
+ generateMouseClickInTab(gTab, "content.document.querySelector('button')");
+ });
+}
+
+function testVariablesAndPropertiesFiltering() {
+ let localScope = gVariables.getScopeAtIndex(0);
+ let withScope = gVariables.getScopeAtIndex(1);
+ let functionScope = gVariables.getScopeAtIndex(2);
+ let globalLexicalScope = gVariables.getScopeAtIndex(3);
+ let globalScope = gVariables.getScopeAtIndex(4);
+ let step = 0;
+
+ let tests = [
+ function () {
+ assertScopeExpansion([true, false, false, false, false]);
+ typeText(gSearchBox, "*arguments");
+ },
+ function () {
+ assertScopeExpansion([true, true, true, true, true]);
+ assertVariablesCountAtLeast([0, 0, 1, 0, 0]);
+
+ is(functionScope.target.querySelectorAll(".variables-view-variable:not([unmatched]) > .title > .name")[0].getAttribute("value"),
+ "arguments", "The arguments pseudoarray should be visible.");
+ is(functionScope.get("arguments").expanded, false,
+ "The arguments pseudoarray in functionScope should not be expanded.");
+
+ backspaceText(gSearchBox, 6);
+ },
+ function () {
+ assertScopeExpansion([true, true, true, true, true]);
+ assertVariablesCountAtLeast([0, 0, 1, 0, 1]);
+
+ is(functionScope.target.querySelectorAll(".variables-view-variable:not([unmatched]) > .title > .name")[0].getAttribute("value"),
+ "arguments", "The arguments pseudoarray should be visible.");
+ is(functionScope.get("arguments").expanded, false,
+ "The arguments pseudoarray in functionScope should not be expanded.");
+
+ is(globalScope.target.querySelectorAll(".variables-view-variable:not([unmatched]) > .title > .name")[0].getAttribute("value"),
+ "EventTarget", "The EventTarget object should be visible.");
+ is(globalScope.get("EventTarget").expanded, false,
+ "The EventTarget object in globalScope should not be expanded.");
+
+ backspaceText(gSearchBox, 2);
+ },
+ function () {
+ assertScopeExpansion([true, true, true, true, true]);
+ assertVariablesCountAtLeast([0, 1, 3, 0, 1]);
+
+ is(functionScope.target.querySelectorAll(".variables-view-variable:not([unmatched]) > .title > .name")[0].getAttribute("value"),
+ "aNumber", "The aNumber param should be visible.");
+ is(functionScope.get("aNumber").expanded, false,
+ "The aNumber param in functionScope should not be expanded.");
+
+ is(functionScope.target.querySelectorAll(".variables-view-variable:not([unmatched]) > .title > .name")[1].getAttribute("value"),
+ "a", "The a variable should be visible.");
+ is(functionScope.get("a").expanded, false,
+ "The a variable in functionScope should not be expanded.");
+
+ is(functionScope.target.querySelectorAll(".variables-view-variable:not([unmatched]) > .title > .name")[2].getAttribute("value"),
+ "arguments", "The arguments pseudoarray should be visible.");
+ is(functionScope.get("arguments").expanded, false,
+ "The arguments pseudoarray in functionScope should not be expanded.");
+
+ backspaceText(gSearchBox, 1);
+ },
+ function () {
+ assertScopeExpansion([true, true, true, true, true]);
+ assertVariablesCountAtLeast([4, 1, 3, 0, 1]);
+
+ is(localScope.target.querySelectorAll(".variables-view-variable:not([unmatched]) > .title > .name")[0].getAttribute("value"),
+ "this", "The this reference should be visible.");
+ is(localScope.get("this").expanded, false,
+ "The this reference in localScope should not be expanded.");
+
+ is(localScope.target.querySelectorAll(".variables-view-variable:not([unmatched]) > .title > .name")[1].getAttribute("value"),
+ "alpha", "The alpha variable should be visible.");
+ is(localScope.get("alpha").expanded, false,
+ "The alpha variable in localScope should not be expanded.");
+
+ is(localScope.target.querySelectorAll(".variables-view-variable:not([unmatched]) > .title > .name")[2].getAttribute("value"),
+ "beta", "The beta variable should be visible.");
+ is(localScope.get("beta").expanded, false,
+ "The beta variable in localScope should not be expanded.");
+
+ is(localScope.target.querySelectorAll(".variables-view-variable:not([unmatched]) > .title > .name")[3].getAttribute("value"),
+ "__proto__", "The __proto__ reference should be visible.");
+ is(localScope.get("__proto__").expanded, false,
+ "The __proto__ reference in localScope should not be expanded.");
+
+ is(functionScope.target.querySelectorAll(".variables-view-variable:not([unmatched]) > .title > .name")[0].getAttribute("value"),
+ "aNumber", "The aNumber param should be visible.");
+ is(functionScope.get("aNumber").expanded, false,
+ "The aNumber param in functionScope should not be expanded.");
+
+ is(functionScope.target.querySelectorAll(".variables-view-variable:not([unmatched]) > .title > .name")[1].getAttribute("value"),
+ "a", "The a variable should be visible.");
+ is(functionScope.get("a").expanded, false,
+ "The a variable in functionScope should not be expanded.");
+
+ is(functionScope.target.querySelectorAll(".variables-view-variable:not([unmatched]) > .title > .name")[2].getAttribute("value"),
+ "arguments", "The arguments pseudoarray should be visible.");
+ is(functionScope.get("arguments").expanded, false,
+ "The arguments pseudoarray in functionScope should not be expanded.");
+ }
+ ];
+
+ function assertScopeExpansion(aFlags) {
+ is(localScope.expanded, aFlags[0],
+ "The localScope should " + (aFlags[0] ? "" : "not ") +
+ "be expanded at this point (" + step + ").");
+
+ is(withScope.expanded, aFlags[1],
+ "The withScope should " + (aFlags[1] ? "" : "not ") +
+ "be expanded at this point (" + step + ").");
+
+ is(functionScope.expanded, aFlags[2],
+ "The functionScope should " + (aFlags[2] ? "" : "not ") +
+ "be expanded at this point (" + step + ").");
+
+ is(globalLexicalScope.expanded, aFlags[3],
+ "The globalLexicalScope should " + (aFlags[3] ? "" : "not ") +
+ "be expanded at this point (" + step + ").");
+
+ is(globalScope.expanded, aFlags[4],
+ "The globalScope should " + (aFlags[4] ? "" : "not ") +
+ "be expanded at this point (" + step + ").");
+ }
+
+ function assertVariablesCountAtLeast(aCounts) {
+ ok(localScope.target.querySelectorAll(".variables-view-variable:not([unmatched])").length >= aCounts[0],
+ "There should be " + aCounts[0] +
+ " variable displayed in the local scope (" + step + ").");
+
+ ok(withScope.target.querySelectorAll(".variables-view-variable:not([unmatched])").length >= aCounts[1],
+ "There should be " + aCounts[1] +
+ " variable displayed in the with scope (" + step + ").");
+
+ ok(functionScope.target.querySelectorAll(".variables-view-variable:not([unmatched])").length >= aCounts[2],
+ "There should be " + aCounts[2] +
+ " variable displayed in the function scope (" + step + ").");
+
+ ok(globalLexicalScope.target.querySelectorAll(".variables-view-variable:not([unmatched])").length >= aCounts[3],
+ "There should be " + aCounts[3] +
+ " variable displayed in the global scope (" + step + ").");
+
+ ok(globalScope.target.querySelectorAll(".variables-view-variable:not([unmatched])").length >= aCounts[4],
+ "There should be " + aCounts[4] +
+ " variable displayed in the global scope (" + step + ").");
+
+ step++;
+ }
+
+ return promise.all(tests.map(f => f()));
+}
+
+function prepareVariablesAndProperties() {
+ let deferred = promise.defer();
+
+ let localScope = gVariables.getScopeAtIndex(0);
+ let withScope = gVariables.getScopeAtIndex(1);
+ let functionScope = gVariables.getScopeAtIndex(2);
+ let globalLexicalScope = gVariables.getScopeAtIndex(3);
+ let globalScope = gVariables.getScopeAtIndex(4);
+
+ is(localScope.expanded, true,
+ "The localScope should be expanded.");
+ is(withScope.expanded, false,
+ "The withScope should not be expanded yet.");
+ is(functionScope.expanded, false,
+ "The functionScope should not be expanded yet.");
+ is(globalLexicalScope.expanded, false,
+ "The globalScope should not be expanded yet.");
+ is(globalScope.expanded, false,
+ "The globalScope should not be expanded yet.");
+
+ // Wait for only two events to be triggered, because the Function scope is
+ // an environment to which scope arguments and variables are already attached.
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_VARIABLES, 2).then(() => {
+ is(localScope.expanded, true,
+ "The localScope should now be expanded.");
+ is(withScope.expanded, true,
+ "The withScope should now be expanded.");
+ is(functionScope.expanded, true,
+ "The functionScope should now be expanded.");
+ is(globalLexicalScope.expanded, true,
+ "The globalScope should now be expanded.");
+ is(globalScope.expanded, true,
+ "The globalScope should now be expanded.");
+
+ withScope.collapse();
+ functionScope.collapse();
+ globalLexicalScope.collapse();
+ globalScope.collapse();
+
+ deferred.resolve();
+ });
+
+ withScope.expand();
+ functionScope.expand();
+ globalLexicalScope.expand();
+ globalScope.expand();
+
+ return deferred.promise;
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gVariables = null;
+ gSearchBox = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-filter-pref.js b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-filter-pref.js
new file mode 100644
index 000000000..783fc8a23
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-filter-pref.js
@@ -0,0 +1,85 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure that the variables view filter prefs work properly.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_with-frame.html";
+
+var gTab, gPanel, gDebugger;
+var gPrefs, gOptions, gVariables;
+
+function test() {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gPrefs = gDebugger.Prefs;
+ gOptions = gDebugger.DebuggerView.Options;
+ gVariables = gDebugger.DebuggerView.Variables;
+
+ performTest();
+ });
+}
+
+function performTest() {
+ ok(!gVariables._searchboxNode,
+ "There should not initially be a searchbox available in the variables view.");
+ ok(!gVariables._searchboxContainer,
+ "There should not initially be a searchbox container available in the variables view.");
+ ok(!gVariables._parent.parentNode.querySelector(".variables-view-searchinput"),
+ "The searchbox element should not be found.");
+
+ is(gPrefs.variablesSearchboxVisible, false,
+ "The debugger searchbox should be preffed as hidden.");
+ isnot(gOptions._showVariablesFilterBoxItem.getAttribute("checked"), "true",
+ "The options menu item should not be checked.");
+
+ gOptions._showVariablesFilterBoxItem.setAttribute("checked", "true");
+ gOptions._toggleShowVariablesFilterBox();
+
+ ok(gVariables._searchboxNode,
+ "There should be a searchbox available in the variables view.");
+ ok(gVariables._searchboxContainer,
+ "There should be a searchbox container available in the variables view.");
+ ok(gVariables._parent.parentNode.querySelector(".variables-view-searchinput"),
+ "There searchbox element should be found.");
+
+ is(gPrefs.variablesSearchboxVisible, true,
+ "The debugger searchbox should now be preffed as visible.");
+ is(gOptions._showVariablesFilterBoxItem.getAttribute("checked"), "true",
+ "The options menu item should now be checked.");
+
+ gOptions._showVariablesFilterBoxItem.setAttribute("checked", "false");
+ gOptions._toggleShowVariablesFilterBox();
+
+ ok(!gVariables._searchboxNode,
+ "There should not be a searchbox available in the variables view.");
+ ok(!gVariables._searchboxContainer,
+ "There should not be a searchbox container available in the variables view.");
+ ok(!gVariables._parent.parentNode.querySelector(".variables-view-searchinput"),
+ "There searchbox element should not be found.");
+
+ is(gPrefs.variablesSearchboxVisible, false,
+ "The debugger searchbox should now be preffed as hidden.");
+ isnot(gOptions._showVariablesFilterBoxItem.getAttribute("checked"), "true",
+ "The options menu item should now be unchecked.");
+
+ closeDebuggerAndFinish(gPanel);
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gPrefs = null;
+ gOptions = null;
+ gVariables = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-filter-searchbox.js b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-filter-searchbox.js
new file mode 100644
index 000000000..1030d105d
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-filter-searchbox.js
@@ -0,0 +1,150 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure that the variables view correctly shows the searchbox
+ * when prompted.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_with-frame.html";
+
+var gTab, gPanel, gDebugger;
+var gVariables;
+
+function test() {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gVariables = gDebugger.DebuggerView.Variables;
+
+ performTest();
+ });
+}
+
+function performTest() {
+ // Step 1: the searchbox shouldn't initially be shown.
+
+ ok(!gVariables._searchboxNode,
+ "There should not initially be a searchbox available in the variables view.");
+ ok(!gVariables._searchboxContainer,
+ "There should not initially be a searchbox container available in the variables view.");
+ ok(!gVariables._parent.parentNode.querySelector(".variables-view-searchinput"),
+ "The searchbox element should not be found.");
+
+ // Step 2: test enable/disable cycles.
+
+ gVariables._enableSearch();
+ ok(gVariables._searchboxNode,
+ "There should be a searchbox available after enabling.");
+ ok(gVariables._searchboxContainer,
+ "There should be a searchbox container available after enabling.");
+ ok(gVariables._searchboxContainer.hidden,
+ "The searchbox container should be hidden at this point.");
+ ok(gVariables._parent.parentNode.querySelector(".variables-view-searchinput"),
+ "The searchbox element should be found.");
+
+ gVariables._disableSearch();
+ ok(!gVariables._searchboxNode,
+ "There shouldn't be a searchbox available after disabling.");
+ ok(!gVariables._searchboxContainer,
+ "There shouldn't be a searchbox container available after disabling.");
+ ok(!gVariables._parent.parentNode.querySelector(".variables-view-searchinput"),
+ "The searchbox element should not be found.");
+
+ // Step 3: add a placeholder while the searchbox is hidden.
+
+ var placeholder = "not freshly squeezed mango juice";
+
+ gVariables.searchPlaceholder = placeholder;
+ is(gVariables.searchPlaceholder, placeholder,
+ "The placeholder getter didn't return the expected string");
+
+ // Step 4: enable search and check the placeholder.
+
+ gVariables._enableSearch();
+ ok(gVariables._searchboxNode,
+ "There should be a searchbox available after enabling.");
+ ok(gVariables._searchboxContainer,
+ "There should be a searchbox container available after enabling.");
+ ok(gVariables._searchboxContainer.hidden,
+ "The searchbox container should be hidden at this point.");
+ ok(gVariables._parent.parentNode.querySelector(".variables-view-searchinput"),
+ "The searchbox element should be found.");
+
+ is(gVariables._searchboxNode.getAttribute("placeholder"),
+ placeholder, "There correct placeholder should be applied to the searchbox.");
+
+ // Step 5: add a placeholder while the searchbox is visible and check wether
+ // it has been immediatey applied.
+
+ var placeholder = "freshly squeezed mango juice";
+
+ gVariables.searchPlaceholder = placeholder;
+ is(gVariables.searchPlaceholder, placeholder,
+ "The placeholder getter didn't return the expected string");
+
+ is(gVariables._searchboxNode.getAttribute("placeholder"),
+ placeholder, "There correct placeholder should be applied to the searchbox.");
+
+ // Step 4: disable, enable, then test the placeholder.
+
+ gVariables._disableSearch();
+ ok(!gVariables._searchboxNode,
+ "There shouldn't be a searchbox available after disabling again.");
+ ok(!gVariables._searchboxContainer,
+ "There shouldn't be a searchbox container available after disabling again.");
+ ok(!gVariables._parent.parentNode.querySelector(".variables-view-searchinput"),
+ "The searchbox element should not be found.");
+
+ gVariables._enableSearch();
+ ok(gVariables._searchboxNode,
+ "There should be a searchbox available after enabling again.");
+ ok(gVariables._searchboxContainer,
+ "There should be a searchbox container available after enabling again.");
+ ok(gVariables._searchboxContainer.hidden,
+ "The searchbox container should be hidden at this point.");
+ ok(gVariables._parent.parentNode.querySelector(".variables-view-searchinput"),
+ "The searchbox element should be found.");
+
+ is(gVariables._searchboxNode.getAttribute("placeholder"),
+ placeholder, "There correct placeholder should be applied to the searchbox again.");
+
+ // Step 5: alternate disable, enable, then test the placeholder.
+
+ gVariables.searchEnabled = false;
+ ok(!gVariables._searchboxNode,
+ "There shouldn't be a searchbox available after disabling again.");
+ ok(!gVariables._searchboxContainer,
+ "There shouldn't be a searchbox container available after disabling again.");
+ ok(!gVariables._parent.parentNode.querySelector(".variables-view-searchinput"),
+ "The searchbox element should not be found.");
+
+ gVariables.searchEnabled = true;
+ ok(gVariables._searchboxNode,
+ "There should be a searchbox available after enabling again.");
+ ok(gVariables._searchboxContainer,
+ "There should be a searchbox container available after enabling again.");
+ ok(gVariables._searchboxContainer.hidden,
+ "The searchbox container should be hidden at this point.");
+ ok(gVariables._parent.parentNode.querySelector(".variables-view-searchinput"),
+ "The searchbox element should be found.");
+
+ is(gVariables._searchboxNode.getAttribute("placeholder"),
+ placeholder, "There correct placeholder should be applied to the searchbox again.");
+
+ closeDebuggerAndFinish(gPanel);
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gVariables = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-frame-parameters-01.js b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-frame-parameters-01.js
new file mode 100644
index 000000000..03dcf1440
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-frame-parameters-01.js
@@ -0,0 +1,270 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure that the variables view correctly displays the properties
+ * of objects when debugger is paused.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_frame-parameters.html";
+
+var gTab, gPanel, gDebugger;
+var gVariables;
+
+function test() {
+ // Debug test slaves are a bit slow at this test.
+ requestLongerTimeout(2);
+
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gVariables = gDebugger.DebuggerView.Variables;
+
+ waitForCaretAndScopes(gPanel, 24)
+ .then(initialChecks)
+ .then(testExpandVariables)
+ .then(() => resumeDebuggerThenCloseAndFinish(gPanel))
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+
+ generateMouseClickInTab(gTab, "content.document.querySelector('button')");
+ });
+}
+
+function initialChecks() {
+ let scopeNodes = gDebugger.document.querySelectorAll(".variables-view-scope");
+ is(scopeNodes.length, 3,
+ "There should be 3 scopes available.");
+
+ ok(scopeNodes[0].querySelector(".name").getAttribute("value").includes("[test]"),
+ "The local scope should be properly identified.");
+ ok(scopeNodes[1].querySelector(".name").getAttribute("value").includes("Block"),
+ "The global lexical scope should be properly identified.");
+ ok(scopeNodes[2].querySelector(".name").getAttribute("value").includes("[Window]"),
+ "The global scope should be properly identified.");
+
+ is(gVariables.getScopeAtIndex(0).target, scopeNodes[0],
+ "getScopeAtIndex(0) didn't return the expected scope.");
+ is(gVariables.getScopeAtIndex(1).target, scopeNodes[1],
+ "getScopeAtIndex(1) didn't return the expected scope.");
+ is(gVariables.getScopeAtIndex(2).target, scopeNodes[2],
+ "getScopeAtIndex(2) didn't return the expected scope.");
+
+ is(gVariables.getItemForNode(scopeNodes[0]).target, scopeNodes[0],
+ "getItemForNode([0]) didn't return the expected scope.");
+ is(gVariables.getItemForNode(scopeNodes[1]).target, scopeNodes[1],
+ "getItemForNode([1]) didn't return the expected scope.");
+ is(gVariables.getItemForNode(scopeNodes[2]).target, scopeNodes[2],
+ "getItemForNode([2]) didn't return the expected scope.");
+
+ is(gVariables.getItemForNode(scopeNodes[0]).expanded, true,
+ "The local scope should be expanded by default.");
+ is(gVariables.getItemForNode(scopeNodes[1]).expanded, false,
+ "The global lexical scope should not be collapsed by default.");
+ is(gVariables.getItemForNode(scopeNodes[2]).expanded, false,
+ "The global scope should not be collapsed by default.");
+}
+
+function testExpandVariables() {
+ let deferred = promise.defer();
+
+ let localScope = gVariables.getScopeAtIndex(0);
+ let localEnums = localScope.target.querySelector(".variables-view-element-details.enum").childNodes;
+
+ let thisVar = gVariables.getItemForNode(localEnums[0]);
+ let argsVar = gVariables.getItemForNode(localEnums[8]);
+ let cVar = gVariables.getItemForNode(localEnums[10]);
+
+ is(thisVar.target.querySelector(".name").getAttribute("value"), "this",
+ "Should have the right property name for 'this'.");
+ is(argsVar.target.querySelector(".name").getAttribute("value"), "arguments",
+ "Should have the right property name for 'arguments'.");
+ is(cVar.target.querySelector(".name").getAttribute("value"), "c",
+ "Should have the right property name for 'c'.");
+
+ is(thisVar.expanded, false,
+ "The thisVar should not be expanded at this point.");
+ is(argsVar.expanded, false,
+ "The argsVar should not be expanded at this point.");
+ is(cVar.expanded, false,
+ "The cVar should not be expanded at this point.");
+
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_PROPERTIES, 3).then(() => {
+ is(thisVar.get("window").target.querySelector(".name").getAttribute("value"), "window",
+ "Should have the right property name for 'window'.");
+ is(thisVar.get("window").target.querySelector(".value").getAttribute("value"),
+ "Window \u2192 doc_frame-parameters.html",
+ "Should have the right property value for 'window'.");
+ ok(thisVar.get("window").target.querySelector(".value").className.includes("token-other"),
+ "Should have the right token class for 'window'.");
+
+ is(thisVar.get("document").target.querySelector(".name").getAttribute("value"), "document",
+ "Should have the right property name for 'document'.");
+ is(thisVar.get("document").target.querySelector(".value").getAttribute("value"),
+ "HTMLDocument \u2192 doc_frame-parameters.html",
+ "Should have the right property value for 'document'.");
+ ok(thisVar.get("document").target.querySelector(".value").className.includes("token-domnode"),
+ "Should have the right token class for 'document'.");
+
+ let argsProps = argsVar.target.querySelectorAll(".variables-view-property");
+ is(argsProps.length, 8,
+ "The 'arguments' variable should contain 5 enumerable and 3 non-enumerable properties");
+
+ is(argsProps[0].querySelector(".name").getAttribute("value"), "0",
+ "Should have the right property name for '0'.");
+ is(argsProps[0].querySelector(".value").getAttribute("value"), "Object",
+ "Should have the right property value for '0'.");
+ ok(argsProps[0].querySelector(".value").className.includes("token-other"),
+ "Should have the right token class for '0'.");
+
+ is(argsProps[1].querySelector(".name").getAttribute("value"), "1",
+ "Should have the right property name for '1'.");
+ is(argsProps[1].querySelector(".value").getAttribute("value"), "\"beta\"",
+ "Should have the right property value for '1'.");
+ ok(argsProps[1].querySelector(".value").className.includes("token-string"),
+ "Should have the right token class for '1'.");
+
+ is(argsProps[2].querySelector(".name").getAttribute("value"), "2",
+ "Should have the right property name for '2'.");
+ is(argsProps[2].querySelector(".value").getAttribute("value"), "3",
+ "Should have the right property name for '2'.");
+ ok(argsProps[2].querySelector(".value").className.includes("token-number"),
+ "Should have the right token class for '2'.");
+
+ is(argsProps[3].querySelector(".name").getAttribute("value"), "3",
+ "Should have the right property name for '3'.");
+ is(argsProps[3].querySelector(".value").getAttribute("value"), "false",
+ "Should have the right property value for '3'.");
+ ok(argsProps[3].querySelector(".value").className.includes("token-boolean"),
+ "Should have the right token class for '3'.");
+
+ is(argsProps[4].querySelector(".name").getAttribute("value"), "4",
+ "Should have the right property name for '4'.");
+ is(argsProps[4].querySelector(".value").getAttribute("value"), "null",
+ "Should have the right property name for '4'.");
+ ok(argsProps[4].querySelector(".value").className.includes("token-null"),
+ "Should have the right token class for '4'.");
+
+ is(gVariables.getItemForNode(argsProps[0]).target,
+ argsVar.target.querySelectorAll(".variables-view-property")[0],
+ "getItemForNode([0]) didn't return the expected property.");
+
+ is(gVariables.getItemForNode(argsProps[1]).target,
+ argsVar.target.querySelectorAll(".variables-view-property")[1],
+ "getItemForNode([1]) didn't return the expected property.");
+
+ is(gVariables.getItemForNode(argsProps[2]).target,
+ argsVar.target.querySelectorAll(".variables-view-property")[2],
+ "getItemForNode([2]) didn't return the expected property.");
+
+ is(argsVar.find(argsProps[0]).target,
+ argsVar.target.querySelectorAll(".variables-view-property")[0],
+ "find([0]) didn't return the expected property.");
+
+ is(argsVar.find(argsProps[1]).target,
+ argsVar.target.querySelectorAll(".variables-view-property")[1],
+ "find([1]) didn't return the expected property.");
+
+ is(argsVar.find(argsProps[2]).target,
+ argsVar.target.querySelectorAll(".variables-view-property")[2],
+ "find([2]) didn't return the expected property.");
+
+ let cProps = cVar.target.querySelectorAll(".variables-view-property");
+ is(cProps.length, 7,
+ "The 'c' variable should contain 6 enumerable and 1 non-enumerable properties");
+
+ is(cProps[0].querySelector(".name").getAttribute("value"), "a",
+ "Should have the right property name for 'a'.");
+ is(cProps[0].querySelector(".value").getAttribute("value"), "1",
+ "Should have the right property value for 'a'.");
+ ok(cProps[0].querySelector(".value").className.includes("token-number"),
+ "Should have the right token class for 'a'.");
+
+ is(cProps[1].querySelector(".name").getAttribute("value"), "b",
+ "Should have the right property name for 'b'.");
+ is(cProps[1].querySelector(".value").getAttribute("value"), "\"beta\"",
+ "Should have the right property value for 'b'.");
+ ok(cProps[1].querySelector(".value").className.includes("token-string"),
+ "Should have the right token class for 'b'.");
+
+ is(cProps[2].querySelector(".name").getAttribute("value"), "c",
+ "Should have the right property name for 'c'.");
+ is(cProps[2].querySelector(".value").getAttribute("value"), "3",
+ "Should have the right property value for 'c'.");
+ ok(cProps[2].querySelector(".value").className.includes("token-number"),
+ "Should have the right token class for 'c'.");
+
+ is(cProps[3].querySelector(".name").getAttribute("value"), "d",
+ "Should have the right property name for 'd'.");
+ is(cProps[3].querySelector(".value").getAttribute("value"), "false",
+ "Should have the right property value for 'd'.");
+ ok(cProps[3].querySelector(".value").className.includes("token-boolean"),
+ "Should have the right token class for 'd'.");
+
+ is(cProps[4].querySelector(".name").getAttribute("value"), "e",
+ "Should have the right property name for 'e'.");
+ is(cProps[4].querySelector(".value").getAttribute("value"), "null",
+ "Should have the right property value for 'e'.");
+ ok(cProps[4].querySelector(".value").className.includes("token-null"),
+ "Should have the right token class for 'e'.");
+
+ is(cProps[5].querySelector(".name").getAttribute("value"), "f",
+ "Should have the right property name for 'f'.");
+ is(cProps[5].querySelector(".value").getAttribute("value"), "undefined",
+ "Should have the right property value for 'f'.");
+ ok(cProps[5].querySelector(".value").className.includes("token-undefined"),
+ "Should have the right token class for 'f'.");
+
+ is(gVariables.getItemForNode(cProps[0]).target,
+ cVar.target.querySelectorAll(".variables-view-property")[0],
+ "getItemForNode([0]) didn't return the expected property.");
+
+ is(gVariables.getItemForNode(cProps[1]).target,
+ cVar.target.querySelectorAll(".variables-view-property")[1],
+ "getItemForNode([1]) didn't return the expected property.");
+
+ is(gVariables.getItemForNode(cProps[2]).target,
+ cVar.target.querySelectorAll(".variables-view-property")[2],
+ "getItemForNode([2]) didn't return the expected property.");
+
+ is(cVar.find(cProps[0]).target,
+ cVar.target.querySelectorAll(".variables-view-property")[0],
+ "find([0]) didn't return the expected property.");
+
+ is(cVar.find(cProps[1]).target,
+ cVar.target.querySelectorAll(".variables-view-property")[1],
+ "find([1]) didn't return the expected property.");
+
+ is(cVar.find(cProps[2]).target,
+ cVar.target.querySelectorAll(".variables-view-property")[2],
+ "find([2]) didn't return the expected property.");
+ });
+
+ // Expand the 'this', 'arguments' and 'c' variables view nodes. This causes
+ // their properties to be retrieved and displayed.
+ thisVar.expand();
+ argsVar.expand();
+ cVar.expand();
+
+ is(thisVar.expanded, true,
+ "The thisVar should be immediately marked as expanded.");
+ is(argsVar.expanded, true,
+ "The argsVar should be immediately marked as expanded.");
+ is(cVar.expanded, true,
+ "The cVar should be immediately marked as expanded.");
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gVariables = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-frame-parameters-02.js b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-frame-parameters-02.js
new file mode 100644
index 000000000..61bfb5275
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-frame-parameters-02.js
@@ -0,0 +1,552 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure that the variables view displays the right variables and
+ * properties when debugger is paused.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_frame-parameters.html";
+
+var gTab, gPanel, gDebugger;
+var gVariables;
+
+function test() {
+ // Debug test slaves are a bit slow at this test.
+ requestLongerTimeout(2);
+
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gVariables = gDebugger.DebuggerView.Variables;
+
+ waitForCaretAndScopes(gPanel, 24)
+ .then(testScopeVariables)
+ .then(testArgumentsProperties)
+ .then(testSimpleObject)
+ .then(testComplexObject)
+ .then(testArgumentObject)
+ .then(testInnerArgumentObject)
+ .then(testGetterSetterObject)
+ .then(() => resumeDebuggerThenCloseAndFinish(gPanel))
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+
+ generateMouseClickInTab(gTab, "content.document.querySelector('button')");
+ });
+}
+
+function testScopeVariables() {
+ let localScope = gVariables.getScopeAtIndex(0);
+ is(localScope.expanded, true,
+ "The local scope should be expanded by default.");
+
+ let localEnums = localScope.target.querySelector(".variables-view-element-details.enum").childNodes;
+ let localNonEnums = localScope.target.querySelector(".variables-view-element-details.nonenum").childNodes;
+
+ is(localEnums.length, 12,
+ "The local scope should contain all the created enumerable elements.");
+ is(localNonEnums.length, 0,
+ "The local scope should contain all the created non-enumerable elements.");
+
+ is(localEnums[0].querySelector(".name").getAttribute("value"), "this",
+ "Should have the right property name for 'this'.");
+ is(localEnums[0].querySelector(".value").getAttribute("value"),
+ "Window \u2192 doc_frame-parameters.html",
+ "Should have the right property value for 'this'.");
+ ok(localEnums[0].querySelector(".value").className.includes("token-other"),
+ "Should have the right token class for 'this'.");
+
+ is(localEnums[1].querySelector(".name").getAttribute("value"), "aArg",
+ "Should have the right property name for 'aArg'.");
+ is(localEnums[1].querySelector(".value").getAttribute("value"), "Object",
+ "Should have the right property value for 'aArg'.");
+ ok(localEnums[1].querySelector(".value").className.includes("token-other"),
+ "Should have the right token class for 'aArg'.");
+
+ is(localEnums[2].querySelector(".name").getAttribute("value"), "bArg",
+ "Should have the right property name for 'bArg'.");
+ is(localEnums[2].querySelector(".value").getAttribute("value"), "\"beta\"",
+ "Should have the right property value for 'bArg'.");
+ ok(localEnums[2].querySelector(".value").className.includes("token-string"),
+ "Should have the right token class for 'bArg'.");
+
+ is(localEnums[3].querySelector(".name").getAttribute("value"), "cArg",
+ "Should have the right property name for 'cArg'.");
+ is(localEnums[3].querySelector(".value").getAttribute("value"), "3",
+ "Should have the right property value for 'cArg'.");
+ ok(localEnums[3].querySelector(".value").className.includes("token-number"),
+ "Should have the right token class for 'cArg'.");
+
+ is(localEnums[4].querySelector(".name").getAttribute("value"), "dArg",
+ "Should have the right property name for 'dArg'.");
+ is(localEnums[4].querySelector(".value").getAttribute("value"), "false",
+ "Should have the right property value for 'dArg'.");
+ ok(localEnums[4].querySelector(".value").className.includes("token-boolean"),
+ "Should have the right token class for 'dArg'.");
+
+ is(localEnums[5].querySelector(".name").getAttribute("value"), "eArg",
+ "Should have the right property name for 'eArg'.");
+ is(localEnums[5].querySelector(".value").getAttribute("value"), "null",
+ "Should have the right property value for 'eArg'.");
+ ok(localEnums[5].querySelector(".value").className.includes("token-null"),
+ "Should have the right token class for 'eArg'.");
+
+ is(localEnums[6].querySelector(".name").getAttribute("value"), "fArg",
+ "Should have the right property name for 'fArg'.");
+ is(localEnums[6].querySelector(".value").getAttribute("value"), "undefined",
+ "Should have the right property value for 'fArg'.");
+ ok(localEnums[6].querySelector(".value").className.includes("token-undefined"),
+ "Should have the right token class for 'fArg'.");
+
+ is(localEnums[7].querySelector(".name").getAttribute("value"), "a",
+ "Should have the right property name for 'a'.");
+ is(localEnums[7].querySelector(".value").getAttribute("value"), "1",
+ "Should have the right property value for 'a'.");
+ ok(localEnums[7].querySelector(".value").className.includes("token-number"),
+ "Should have the right token class for 'a'.");
+
+ is(localEnums[8].querySelector(".name").getAttribute("value"), "arguments",
+ "Should have the right property name for 'arguments'.");
+ is(localEnums[8].querySelector(".value").getAttribute("value"), "Arguments",
+ "Should have the right property value for 'arguments'.");
+ ok(localEnums[8].querySelector(".value").className.includes("token-other"),
+ "Should have the right token class for 'arguments'.");
+
+ is(localEnums[9].querySelector(".name").getAttribute("value"), "b",
+ "Should have the right property name for 'b'.");
+ is(localEnums[9].querySelector(".value").getAttribute("value"), "Object",
+ "Should have the right property value for 'b'.");
+ ok(localEnums[9].querySelector(".value").className.includes("token-other"),
+ "Should have the right token class for 'b'.");
+
+ is(localEnums[10].querySelector(".name").getAttribute("value"), "c",
+ "Should have the right property name for 'c'.");
+ is(localEnums[10].querySelector(".value").getAttribute("value"), "Object",
+ "Should have the right property value for 'c'.");
+ ok(localEnums[10].querySelector(".value").className.includes("token-other"),
+ "Should have the right token class for 'c'.");
+
+ is(localEnums[11].querySelector(".name").getAttribute("value"), "myVar",
+ "Should have the right property name for 'myVar'.");
+ is(localEnums[11].querySelector(".value").getAttribute("value"), "Object",
+ "Should have the right property value for 'myVar'.");
+ ok(localEnums[11].querySelector(".value").className.includes("token-other"),
+ "Should have the right token class for 'myVar'.");
+}
+
+function testArgumentsProperties() {
+ let deferred = promise.defer();
+
+ let argsVar = gVariables.getScopeAtIndex(0).get("arguments");
+ is(argsVar.expanded, false,
+ "The 'arguments' variable should not be expanded by default.");
+
+ let argsEnums = argsVar.target.querySelector(".variables-view-element-details.enum").childNodes;
+ let argsNonEnums = argsVar.target.querySelector(".variables-view-element-details.nonenum").childNodes;
+
+ gDebugger.once(gDebugger.EVENTS.FETCHED_PROPERTIES, () => {
+ is(argsEnums.length, 5,
+ "The 'arguments' variable should contain all the created enumerable elements.");
+ is(argsNonEnums.length, 3,
+ "The 'arguments' variable should contain all the created non-enumerable elements.");
+
+ is(argsEnums[0].querySelector(".name").getAttribute("value"), "0",
+ "Should have the right property name for '0'.");
+ is(argsEnums[0].querySelector(".value").getAttribute("value"), "Object",
+ "Should have the right property value for '0'.");
+ ok(argsEnums[0].querySelector(".value").className.includes("token-other"),
+ "Should have the right token class for '0'.");
+
+ is(argsEnums[1].querySelector(".name").getAttribute("value"), "1",
+ "Should have the right property name for '1'.");
+ is(argsEnums[1].querySelector(".value").getAttribute("value"), "\"beta\"",
+ "Should have the right property value for '1'.");
+ ok(argsEnums[1].querySelector(".value").className.includes("token-string"),
+ "Should have the right token class for '1'.");
+
+ is(argsEnums[2].querySelector(".name").getAttribute("value"), "2",
+ "Should have the right property name for '2'.");
+ is(argsEnums[2].querySelector(".value").getAttribute("value"), "3",
+ "Should have the right property name for '2'.");
+ ok(argsEnums[2].querySelector(".value").className.includes("token-number"),
+ "Should have the right token class for '2'.");
+
+ is(argsEnums[3].querySelector(".name").getAttribute("value"), "3",
+ "Should have the right property name for '3'.");
+ is(argsEnums[3].querySelector(".value").getAttribute("value"), "false",
+ "Should have the right property value for '3'.");
+ ok(argsEnums[3].querySelector(".value").className.includes("token-boolean"),
+ "Should have the right token class for '3'.");
+
+ is(argsEnums[4].querySelector(".name").getAttribute("value"), "4",
+ "Should have the right property name for '4'.");
+ is(argsEnums[4].querySelector(".value").getAttribute("value"), "null",
+ "Should have the right property name for '4'.");
+ ok(argsEnums[4].querySelector(".value").className.includes("token-null"),
+ "Should have the right token class for '4'.");
+
+ is(argsNonEnums[0].querySelector(".name").getAttribute("value"), "callee",
+ "Should have the right property name for 'callee'.");
+ is(argsNonEnums[0].querySelector(".value").getAttribute("value"),
+ "test(aArg,bArg,cArg,dArg,eArg,fArg)",
+ "Should have the right property name for 'callee'.");
+ ok(argsNonEnums[0].querySelector(".value").className.includes("token-other"),
+ "Should have the right token class for 'callee'.");
+
+ is(argsNonEnums[1].querySelector(".name").getAttribute("value"), "length",
+ "Should have the right property name for 'length'.");
+ is(argsNonEnums[1].querySelector(".value").getAttribute("value"), "5",
+ "Should have the right property value for 'length'.");
+ ok(argsNonEnums[1].querySelector(".value").className.includes("token-number"),
+ "Should have the right token class for 'length'.");
+
+ is(argsNonEnums[2].querySelector(".name").getAttribute("value"), "__proto__",
+ "Should have the right property name for '__proto__'.");
+ is(argsNonEnums[2].querySelector(".value").getAttribute("value"), "Object",
+ "Should have the right property value for '__proto__'.");
+ ok(argsNonEnums[2].querySelector(".value").className.includes("token-other"),
+ "Should have the right token class for '__proto__'.");
+
+ deferred.resolve();
+ });
+
+ argsVar.expand();
+ return deferred.promise;
+}
+
+function testSimpleObject() {
+ let deferred = promise.defer();
+
+ let bVar = gVariables.getScopeAtIndex(0).get("b");
+ is(bVar.expanded, false,
+ "The 'b' variable should not be expanded by default.");
+
+ let bEnums = bVar.target.querySelector(".variables-view-element-details.enum").childNodes;
+ let bNonEnums = bVar.target.querySelector(".variables-view-element-details.nonenum").childNodes;
+
+ gDebugger.once(gDebugger.EVENTS.FETCHED_PROPERTIES, () => {
+ is(bEnums.length, 1,
+ "The 'b' variable should contain all the created enumerable elements.");
+ is(bNonEnums.length, 1,
+ "The 'b' variable should contain all the created non-enumerable elements.");
+
+ is(bEnums[0].querySelector(".name").getAttribute("value"), "a",
+ "Should have the right property name for 'a'.");
+ is(bEnums[0].querySelector(".value").getAttribute("value"), "1",
+ "Should have the right property value for 'a'.");
+ ok(bEnums[0].querySelector(".value").className.includes("token-number"),
+ "Should have the right token class for 'a'.");
+
+ is(bNonEnums[0].querySelector(".name").getAttribute("value"), "__proto__",
+ "Should have the right property name for '__proto__'.");
+ is(bNonEnums[0].querySelector(".value").getAttribute("value"), "Object",
+ "Should have the right property value for '__proto__'.");
+ ok(bNonEnums[0].querySelector(".value").className.includes("token-other"),
+ "Should have the right token class for '__proto__'.");
+
+ deferred.resolve();
+ });
+
+ bVar.expand();
+ return deferred.promise;
+}
+
+function testComplexObject() {
+ let deferred = promise.defer();
+
+ let cVar = gVariables.getScopeAtIndex(0).get("c");
+ is(cVar.expanded, false,
+ "The 'c' variable should not be expanded by default.");
+
+ let cEnums = cVar.target.querySelector(".variables-view-element-details.enum").childNodes;
+ let cNonEnums = cVar.target.querySelector(".variables-view-element-details.nonenum").childNodes;
+
+ gDebugger.once(gDebugger.EVENTS.FETCHED_PROPERTIES, () => {
+ is(cEnums.length, 6,
+ "The 'c' variable should contain all the created enumerable elements.");
+ is(cNonEnums.length, 1,
+ "The 'c' variable should contain all the created non-enumerable elements.");
+
+ is(cEnums[0].querySelector(".name").getAttribute("value"), "a",
+ "Should have the right property name for 'a'.");
+ is(cEnums[0].querySelector(".value").getAttribute("value"), "1",
+ "Should have the right property value for 'a'.");
+ ok(cEnums[0].querySelector(".value").className.includes("token-number"),
+ "Should have the right token class for 'a'.");
+
+ is(cEnums[1].querySelector(".name").getAttribute("value"), "b",
+ "Should have the right property name for 'b'.");
+ is(cEnums[1].querySelector(".value").getAttribute("value"), "\"beta\"",
+ "Should have the right property value for 'b'.");
+ ok(cEnums[1].querySelector(".value").className.includes("token-string"),
+ "Should have the right token class for 'b'.");
+
+ is(cEnums[2].querySelector(".name").getAttribute("value"), "c",
+ "Should have the right property name for 'c'.");
+ is(cEnums[2].querySelector(".value").getAttribute("value"), "3",
+ "Should have the right property value for 'c'.");
+ ok(cEnums[2].querySelector(".value").className.includes("token-number"),
+ "Should have the right token class for 'c'.");
+
+ is(cEnums[3].querySelector(".name").getAttribute("value"), "d",
+ "Should have the right property name for 'd'.");
+ is(cEnums[3].querySelector(".value").getAttribute("value"), "false",
+ "Should have the right property value for 'd'.");
+ ok(cEnums[3].querySelector(".value").className.includes("token-boolean"),
+ "Should have the right token class for 'd'.");
+
+ is(cEnums[4].querySelector(".name").getAttribute("value"), "e",
+ "Should have the right property name for 'e'.");
+ is(cEnums[4].querySelector(".value").getAttribute("value"), "null",
+ "Should have the right property value for 'e'.");
+ ok(cEnums[4].querySelector(".value").className.includes("token-null"),
+ "Should have the right token class for 'e'.");
+
+ is(cEnums[5].querySelector(".name").getAttribute("value"), "f",
+ "Should have the right property name for 'f'.");
+ is(cEnums[5].querySelector(".value").getAttribute("value"), "undefined",
+ "Should have the right property value for 'f'.");
+ ok(cEnums[5].querySelector(".value").className.includes("token-undefined"),
+ "Should have the right token class for 'f'.");
+
+ is(cNonEnums[0].querySelector(".name").getAttribute("value"), "__proto__",
+ "Should have the right property name for '__proto__'.");
+ is(cNonEnums[0].querySelector(".value").getAttribute("value"), "Object",
+ "Should have the right property value for '__proto__'.");
+ ok(cNonEnums[0].querySelector(".value").className.includes("token-other"),
+ "Should have the right token class for '__proto__'.");
+
+ deferred.resolve();
+ });
+
+ cVar.expand();
+ return deferred.promise;
+}
+
+function testArgumentObject() {
+ let deferred = promise.defer();
+
+ let argVar = gVariables.getScopeAtIndex(0).get("aArg");
+ is(argVar.expanded, false,
+ "The 'aArg' variable should not be expanded by default.");
+
+ let argEnums = argVar.target.querySelector(".variables-view-element-details.enum").childNodes;
+ let argNonEnums = argVar.target.querySelector(".variables-view-element-details.nonenum").childNodes;
+
+ gDebugger.once(gDebugger.EVENTS.FETCHED_PROPERTIES, () => {
+ is(argEnums.length, 6,
+ "The 'aArg' variable should contain all the created enumerable elements.");
+ is(argNonEnums.length, 1,
+ "The 'aArg' variable should contain all the created non-enumerable elements.");
+
+ is(argEnums[0].querySelector(".name").getAttribute("value"), "a",
+ "Should have the right property name for 'a'.");
+ is(argEnums[0].querySelector(".value").getAttribute("value"), "1",
+ "Should have the right property value for 'a'.");
+ ok(argEnums[0].querySelector(".value").className.includes("token-number"),
+ "Should have the right token class for 'a'.");
+
+ is(argEnums[1].querySelector(".name").getAttribute("value"), "b",
+ "Should have the right property name for 'b'.");
+ is(argEnums[1].querySelector(".value").getAttribute("value"), "\"beta\"",
+ "Should have the right property value for 'b'.");
+ ok(argEnums[1].querySelector(".value").className.includes("token-string"),
+ "Should have the right token class for 'b'.");
+
+ is(argEnums[2].querySelector(".name").getAttribute("value"), "c",
+ "Should have the right property name for 'c'.");
+ is(argEnums[2].querySelector(".value").getAttribute("value"), "3",
+ "Should have the right property value for 'c'.");
+ ok(argEnums[2].querySelector(".value").className.includes("token-number"),
+ "Should have the right token class for 'c'.");
+
+ is(argEnums[3].querySelector(".name").getAttribute("value"), "d",
+ "Should have the right property name for 'd'.");
+ is(argEnums[3].querySelector(".value").getAttribute("value"), "false",
+ "Should have the right property value for 'd'.");
+ ok(argEnums[3].querySelector(".value").className.includes("token-boolean"),
+ "Should have the right token class for 'd'.");
+
+ is(argEnums[4].querySelector(".name").getAttribute("value"), "e",
+ "Should have the right property name for 'e'.");
+ is(argEnums[4].querySelector(".value").getAttribute("value"), "null",
+ "Should have the right property value for 'e'.");
+ ok(argEnums[4].querySelector(".value").className.includes("token-null"),
+ "Should have the right token class for 'e'.");
+
+ is(argEnums[5].querySelector(".name").getAttribute("value"), "f",
+ "Should have the right property name for 'f'.");
+ is(argEnums[5].querySelector(".value").getAttribute("value"), "undefined",
+ "Should have the right property value for 'f'.");
+ ok(argEnums[5].querySelector(".value").className.includes("token-undefined"),
+ "Should have the right token class for 'f'.");
+
+ is(argNonEnums[0].querySelector(".name").getAttribute("value"), "__proto__",
+ "Should have the right property name for '__proto__'.");
+ is(argNonEnums[0].querySelector(".value").getAttribute("value"), "Object",
+ "Should have the right property value for '__proto__'.");
+ ok(argNonEnums[0].querySelector(".value").className.includes("token-other"),
+ "Should have the right token class for '__proto__'.");
+
+ deferred.resolve();
+ });
+
+ argVar.expand();
+ return deferred.promise;
+}
+
+function testInnerArgumentObject() {
+ let deferred = promise.defer();
+
+ let argProp = gVariables.getScopeAtIndex(0).get("arguments").get("0");
+ is(argProp.expanded, false,
+ "The 'arguments[0]' property should not be expanded by default.");
+
+ let argEnums = argProp.target.querySelector(".variables-view-element-details.enum").childNodes;
+ let argNonEnums = argProp.target.querySelector(".variables-view-element-details.nonenum").childNodes;
+
+ gDebugger.once(gDebugger.EVENTS.FETCHED_PROPERTIES, () => {
+ is(argEnums.length, 6,
+ "The 'arguments[0]' property should contain all the created enumerable elements.");
+ is(argNonEnums.length, 1,
+ "The 'arguments[0]' property should contain all the created non-enumerable elements.");
+
+ is(argEnums[0].querySelector(".name").getAttribute("value"), "a",
+ "Should have the right property name for 'a'.");
+ is(argEnums[0].querySelector(".value").getAttribute("value"), "1",
+ "Should have the right property value for 'a'.");
+ ok(argEnums[0].querySelector(".value").className.includes("token-number"),
+ "Should have the right token class for 'a'.");
+
+ is(argEnums[1].querySelector(".name").getAttribute("value"), "b",
+ "Should have the right property name for 'b'.");
+ is(argEnums[1].querySelector(".value").getAttribute("value"), "\"beta\"",
+ "Should have the right property value for 'b'.");
+ ok(argEnums[1].querySelector(".value").className.includes("token-string"),
+ "Should have the right token class for 'b'.");
+
+ is(argEnums[2].querySelector(".name").getAttribute("value"), "c",
+ "Should have the right property name for 'c'.");
+ is(argEnums[2].querySelector(".value").getAttribute("value"), "3",
+ "Should have the right property value for 'c'.");
+ ok(argEnums[2].querySelector(".value").className.includes("token-number"),
+ "Should have the right token class for 'c'.");
+
+ is(argEnums[3].querySelector(".name").getAttribute("value"), "d",
+ "Should have the right property name for 'd'.");
+ is(argEnums[3].querySelector(".value").getAttribute("value"), "false",
+ "Should have the right property value for 'd'.");
+ ok(argEnums[3].querySelector(".value").className.includes("token-boolean"),
+ "Should have the right token class for 'd'.");
+
+ is(argEnums[4].querySelector(".name").getAttribute("value"), "e",
+ "Should have the right property name for 'e'.");
+ is(argEnums[4].querySelector(".value").getAttribute("value"), "null",
+ "Should have the right property value for 'e'.");
+ ok(argEnums[4].querySelector(".value").className.includes("token-null"),
+ "Should have the right token class for 'e'.");
+
+ is(argEnums[5].querySelector(".name").getAttribute("value"), "f",
+ "Should have the right property name for 'f'.");
+ is(argEnums[5].querySelector(".value").getAttribute("value"), "undefined",
+ "Should have the right property value for 'f'.");
+ ok(argEnums[5].querySelector(".value").className.includes("token-undefined"),
+ "Should have the right token class for 'f'.");
+
+ is(argNonEnums[0].querySelector(".name").getAttribute("value"), "__proto__",
+ "Should have the right property name for '__proto__'.");
+ is(argNonEnums[0].querySelector(".value").getAttribute("value"), "Object",
+ "Should have the right property value for '__proto__'.");
+ ok(argNonEnums[0].querySelector(".value").className.includes("token-other"),
+ "Should have the right token class for '__proto__'.");
+
+ deferred.resolve();
+ });
+
+ argProp.expand();
+ return deferred.promise;
+}
+
+function testGetterSetterObject() {
+ let deferred = promise.defer();
+
+ let myVar = gVariables.getScopeAtIndex(0).get("myVar");
+ is(myVar.expanded, false,
+ "The myVar variable should not be expanded by default.");
+
+ let myVarEnums = myVar.target.querySelector(".variables-view-element-details.enum").childNodes;
+ let myVarNonEnums = myVar.target.querySelector(".variables-view-element-details.nonenum").childNodes;
+
+ gDebugger.once(gDebugger.EVENTS.FETCHED_PROPERTIES, () => {
+ is(myVarEnums.length, 2,
+ "The myVar should contain all the created enumerable elements.");
+ is(myVarNonEnums.length, 1,
+ "The myVar should contain all the created non-enumerable elements.");
+
+ is(myVarEnums[0].querySelector(".name").getAttribute("value"), "_prop",
+ "Should have the right property name for '_prop'.");
+ is(myVarEnums[0].querySelector(".value").getAttribute("value"), "42",
+ "Should have the right property value for '_prop'.");
+ ok(myVarEnums[0].querySelector(".value").className.includes("token-number"),
+ "Should have the right token class for '_prop'.");
+
+ is(myVarEnums[1].querySelector(".name").getAttribute("value"), "prop",
+ "Should have the right property name for 'prop'.");
+ is(myVarEnums[1].querySelector(".value").getAttribute("value"), "",
+ "Should have the right property value for 'prop'.");
+ ok(!myVarEnums[1].querySelector(".value").className.includes("token"),
+ "Should have no token class for 'prop'.");
+
+ is(myVarNonEnums[0].querySelector(".name").getAttribute("value"), "__proto__",
+ "Should have the right property name for '__proto__'.");
+ is(myVarNonEnums[0].querySelector(".value").getAttribute("value"), "Object",
+ "Should have the right property value for '__proto__'.");
+ ok(myVarNonEnums[0].querySelector(".value").className.includes("token-other"),
+ "Should have the right token class for '__proto__'.");
+
+ let propEnums = myVarEnums[1].querySelector(".variables-view-element-details.enum").childNodes;
+ let propNonEnums = myVarEnums[1].querySelector(".variables-view-element-details.nonenum").childNodes;
+
+ is(propEnums.length, 0,
+ "The propEnums should contain all the created enumerable elements.");
+ is(propNonEnums.length, 2,
+ "The propEnums should contain all the created non-enumerable elements.");
+
+ is(propNonEnums[0].querySelector(".name").getAttribute("value"), "get",
+ "Should have the right property name for 'get'.");
+ is(propNonEnums[0].querySelector(".value").getAttribute("value"),
+ "get prop()",
+ "Should have the right property value for 'get'.");
+ ok(propNonEnums[0].querySelector(".value").className.includes("token-other"),
+ "Should have the right token class for 'get'.");
+
+ is(propNonEnums[1].querySelector(".name").getAttribute("value"), "set",
+ "Should have the right property name for 'set'.");
+ is(propNonEnums[1].querySelector(".value").getAttribute("value"),
+ "set prop(val)",
+ "Should have the right property value for 'set'.");
+ ok(propNonEnums[1].querySelector(".value").className.includes("token-other"),
+ "Should have the right token class for 'set'.");
+
+ deferred.resolve();
+ });
+
+ myVar.expand();
+ return deferred.promise;
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gVariables = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-frame-parameters-03.js b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-frame-parameters-03.js
new file mode 100644
index 000000000..04f23ac7c
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-frame-parameters-03.js
@@ -0,0 +1,157 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure that the variables view displays the right variables and
+ * properties in the global scope when debugger is paused.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_frame-parameters.html";
+
+var gTab, gPanel, gDebugger;
+var gVariables;
+
+function test() {
+ // Debug test slaves are a bit slow at this test.
+ requestLongerTimeout(2);
+
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gVariables = gDebugger.DebuggerView.Variables;
+
+ waitForCaretAndScopes(gPanel, 24)
+ .then(expandGlobalScope)
+ .then(testGlobalScope)
+ .then(expandWindowVariable)
+ .then(testWindowVariable)
+ .then(() => resumeDebuggerThenCloseAndFinish(gPanel))
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+
+ generateMouseClickInTab(gTab, "content.document.querySelector('button')");
+ });
+}
+
+function expandGlobalScope() {
+ let deferred = promise.defer();
+
+ let globalScope = gVariables.getScopeAtIndex(2);
+ is(globalScope.expanded, false,
+ "The global scope should not be expanded by default.");
+
+ gDebugger.once(gDebugger.EVENTS.FETCHED_VARIABLES, deferred.resolve);
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ globalScope.target.querySelector(".name"),
+ gDebugger);
+
+ return deferred.promise;
+}
+
+function testGlobalScope() {
+ let globalScope = gVariables.getScopeAtIndex(2);
+ is(globalScope.expanded, true,
+ "The global scope should now be expanded.");
+
+ is(globalScope.get("InstallTrigger").target.querySelector(".name").getAttribute("value"), "InstallTrigger",
+ "Should have the right property name for 'InstallTrigger'.");
+ is(globalScope.get("InstallTrigger").target.querySelector(".value").getAttribute("value"), "InstallTriggerImpl",
+ "Should have the right property value for 'InstallTrigger'.");
+
+ is(globalScope.get("SpecialPowers").target.querySelector(".name").getAttribute("value"), "SpecialPowers",
+ "Should have the right property name for 'SpecialPowers'.");
+ is(globalScope.get("SpecialPowers").target.querySelector(".value").getAttribute("value"), "Object",
+ "Should have the right property value for 'SpecialPowers'.");
+
+ is(globalScope.get("window").target.querySelector(".name").getAttribute("value"), "window",
+ "Should have the right property name for 'window'.");
+ is(globalScope.get("window").target.querySelector(".value").getAttribute("value"),
+ "Window \u2192 doc_frame-parameters.html",
+ "Should have the right property value for 'window'.");
+
+ is(globalScope.get("document").target.querySelector(".name").getAttribute("value"), "document",
+ "Should have the right property name for 'document'.");
+ is(globalScope.get("document").target.querySelector(".value").getAttribute("value"),
+ "HTMLDocument \u2192 doc_frame-parameters.html",
+ "Should have the right property value for 'document'.");
+
+ is(globalScope.get("undefined").target.querySelector(".name").getAttribute("value"), "undefined",
+ "Should have the right property name for 'undefined'.");
+ is(globalScope.get("undefined").target.querySelector(".value").getAttribute("value"), "undefined",
+ "Should have the right property value for 'undefined'.");
+
+ is(globalScope.get("undefined").target.querySelector(".enum").childNodes.length, 0,
+ "Should have no child enumerable properties for 'undefined'.");
+ is(globalScope.get("undefined").target.querySelector(".nonenum").childNodes.length, 0,
+ "Should have no child non-enumerable properties for 'undefined'.");
+}
+
+function expandWindowVariable() {
+ let deferred = promise.defer();
+
+ let windowVar = gVariables.getScopeAtIndex(2).get("window");
+ is(windowVar.expanded, false,
+ "The window variable should not be expanded by default.");
+
+ gDebugger.once(gDebugger.EVENTS.FETCHED_PROPERTIES, deferred.resolve);
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ windowVar.target.querySelector(".name"),
+ gDebugger);
+
+ return deferred.promise;
+}
+
+function testWindowVariable() {
+ let windowVar = gVariables.getScopeAtIndex(2).get("window");
+ is(windowVar.expanded, true,
+ "The window variable should now be expanded.");
+
+ is(windowVar.get("InstallTrigger").target.querySelector(".name").getAttribute("value"), "InstallTrigger",
+ "Should have the right property name for 'InstallTrigger'.");
+ is(windowVar.get("InstallTrigger").target.querySelector(".value").getAttribute("value"), "InstallTriggerImpl",
+ "Should have the right property value for 'InstallTrigger'.");
+
+ is(windowVar.get("SpecialPowers").target.querySelector(".name").getAttribute("value"), "SpecialPowers",
+ "Should have the right property name for 'SpecialPowers'.");
+ is(windowVar.get("SpecialPowers").target.querySelector(".value").getAttribute("value"), "Object",
+ "Should have the right property value for 'SpecialPowers'.");
+
+ is(windowVar.get("window").target.querySelector(".name").getAttribute("value"), "window",
+ "Should have the right property name for 'window'.");
+ is(windowVar.get("window").target.querySelector(".value").getAttribute("value"),
+ "Window \u2192 doc_frame-parameters.html",
+ "Should have the right property value for 'window'.");
+
+ is(windowVar.get("document").target.querySelector(".name").getAttribute("value"), "document",
+ "Should have the right property name for 'document'.");
+ is(windowVar.get("document").target.querySelector(".value").getAttribute("value"),
+ "HTMLDocument \u2192 doc_frame-parameters.html",
+ "Should have the right property value for 'document'.");
+
+ is(windowVar.get("undefined").target.querySelector(".name").getAttribute("value"), "undefined",
+ "Should have the right property name for 'undefined'.");
+ is(windowVar.get("undefined").target.querySelector(".value").getAttribute("value"), "undefined",
+ "Should have the right property value for 'undefined'.");
+
+ is(windowVar.get("undefined").target.querySelector(".enum").childNodes.length, 0,
+ "Should have no child enumerable properties for 'undefined'.");
+ is(windowVar.get("undefined").target.querySelector(".nonenum").childNodes.length, 0,
+ "Should have no child non-enumerable properties for 'undefined'.");
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gVariables = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-frame-with.js b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-frame-with.js
new file mode 100644
index 000000000..dc98e2d9b
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-frame-with.js
@@ -0,0 +1,212 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure that the variables view is correctly populated in 'with' frames.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_with-frame.html";
+
+var gTab, gPanel, gDebugger;
+var gVariables;
+
+function test() {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gVariables = gDebugger.DebuggerView.Variables;
+
+ // The first 'with' scope should be expanded by default, but the
+ // variables haven't been fetched yet. This is how 'with' scopes work.
+ promise.all([
+ waitForCaretAndScopes(gPanel, 22),
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_VARIABLES)
+ ]).then(testFirstWithScope)
+ .then(expandSecondWithScope)
+ .then(testSecondWithScope)
+ .then(expandFunctionScope)
+ .then(testFunctionScope)
+ .then(() => resumeDebuggerThenCloseAndFinish(gPanel))
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+
+ generateMouseClickInTab(gTab, "content.document.querySelector('button')");
+ });
+}
+
+function testFirstWithScope() {
+ let firstWithScope = gVariables.getScopeAtIndex(0);
+ is(firstWithScope.expanded, true,
+ "The first 'with' scope should be expanded by default.");
+ ok(firstWithScope.target.querySelector(".name").getAttribute("value").includes("[Object]"),
+ "The first 'with' scope should be properly identified.");
+
+ let withEnums = firstWithScope._enum.childNodes;
+ let withNonEnums = firstWithScope._nonenum.childNodes;
+
+ is(withEnums.length, 3,
+ "The first 'with' scope should contain all the created enumerable elements.");
+ is(withNonEnums.length, 1,
+ "The first 'with' scope should contain all the created non-enumerable elements.");
+
+ is(withEnums[0].querySelector(".name").getAttribute("value"), "this",
+ "Should have the right property name for 'this'.");
+ is(withEnums[0].querySelector(".value").getAttribute("value"),
+ "Window \u2192 doc_with-frame.html",
+ "Should have the right property value for 'this'.");
+ ok(withEnums[0].querySelector(".value").className.includes("token-other"),
+ "Should have the right token class for 'this'.");
+
+ is(withEnums[1].querySelector(".name").getAttribute("value"), "alpha",
+ "Should have the right property name for 'alpha'.");
+ is(withEnums[1].querySelector(".value").getAttribute("value"), "1",
+ "Should have the right property value for 'alpha'.");
+ ok(withEnums[1].querySelector(".value").className.includes("token-number"),
+ "Should have the right token class for 'alpha'.");
+
+ is(withEnums[2].querySelector(".name").getAttribute("value"), "beta",
+ "Should have the right property name for 'beta'.");
+ is(withEnums[2].querySelector(".value").getAttribute("value"), "2",
+ "Should have the right property value for 'beta'.");
+ ok(withEnums[2].querySelector(".value").className.includes("token-number"),
+ "Should have the right token class for 'beta'.");
+
+ is(withNonEnums[0].querySelector(".name").getAttribute("value"), "__proto__",
+ "Should have the right property name for '__proto__'.");
+ is(withNonEnums[0].querySelector(".value").getAttribute("value"), "Object",
+ "Should have the right property value for '__proto__'.");
+ ok(withNonEnums[0].querySelector(".value").className.includes("token-other"),
+ "Should have the right token class for '__proto__'.");
+}
+
+function expandSecondWithScope() {
+ let deferred = promise.defer();
+
+ let secondWithScope = gVariables.getScopeAtIndex(1);
+ is(secondWithScope.expanded, false,
+ "The second 'with' scope should not be expanded by default.");
+
+ gDebugger.once(gDebugger.EVENTS.FETCHED_VARIABLES, deferred.resolve);
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ secondWithScope.target.querySelector(".name"),
+ gDebugger);
+
+ return deferred.promise;
+}
+
+function testSecondWithScope() {
+ let secondWithScope = gVariables.getScopeAtIndex(1);
+ is(secondWithScope.expanded, true,
+ "The second 'with' scope should now be expanded.");
+ ok(secondWithScope.target.querySelector(".name").getAttribute("value").includes("[Math]"),
+ "The second 'with' scope should be properly identified.");
+
+ let withEnums = secondWithScope._enum.childNodes;
+ let withNonEnums = secondWithScope._nonenum.childNodes;
+
+ is(withEnums.length, 0,
+ "The second 'with' scope should contain all the created enumerable elements.");
+ isnot(withNonEnums.length, 0,
+ "The second 'with' scope should contain all the created non-enumerable elements.");
+
+ is(secondWithScope.get("E").target.querySelector(".name").getAttribute("value"), "E",
+ "Should have the right property name for 'E'.");
+ is(secondWithScope.get("E").target.querySelector(".value").getAttribute("value"), "2.718281828459045",
+ "Should have the right property value for 'E'.");
+ ok(secondWithScope.get("E").target.querySelector(".value").className.includes("token-number"),
+ "Should have the right token class for 'E'.");
+
+ is(secondWithScope.get("PI").target.querySelector(".name").getAttribute("value"), "PI",
+ "Should have the right property name for 'PI'.");
+ is(secondWithScope.get("PI").target.querySelector(".value").getAttribute("value"), "3.141592653589793",
+ "Should have the right property value for 'PI'.");
+ ok(secondWithScope.get("PI").target.querySelector(".value").className.includes("token-number"),
+ "Should have the right token class for 'PI'.");
+
+ is(secondWithScope.get("random").target.querySelector(".name").getAttribute("value"), "random",
+ "Should have the right property name for 'random'.");
+ is(secondWithScope.get("random").target.querySelector(".value").getAttribute("value"), "random()",
+ "Should have the right property value for 'random'.");
+ ok(secondWithScope.get("random").target.querySelector(".value").className.includes("token-other"),
+ "Should have the right token class for 'random'.");
+
+ is(secondWithScope.get("__proto__").target.querySelector(".name").getAttribute("value"), "__proto__",
+ "Should have the right property name for '__proto__'.");
+ is(secondWithScope.get("__proto__").target.querySelector(".value").getAttribute("value"), "Object",
+ "Should have the right property value for '__proto__'.");
+ ok(secondWithScope.get("__proto__").target.querySelector(".value").className.includes("token-other"),
+ "Should have the right token class for '__proto__'.");
+}
+
+function expandFunctionScope() {
+ let funcScope = gVariables.getScopeAtIndex(2);
+ is(funcScope.expanded, false,
+ "The function scope shouldn't be expanded by default, but the " +
+ "variables have been already fetched. This is how local scopes work.");
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ funcScope.target.querySelector(".name"),
+ gDebugger);
+
+ return promise.resolve(null);
+}
+
+function testFunctionScope() {
+ let funcScope = gVariables.getScopeAtIndex(2);
+ is(funcScope.expanded, true,
+ "The function scope should now be expanded.");
+ ok(funcScope.target.querySelector(".name").getAttribute("value").includes("[test]"),
+ "The function scope should be properly identified.");
+
+ let funcEnums = funcScope._enum.childNodes;
+ let funcNonEnums = funcScope._nonenum.childNodes;
+
+ is(funcEnums.length, 6,
+ "The function scope should contain all the created enumerable elements.");
+ is(funcNonEnums.length, 0,
+ "The function scope should contain all the created non-enumerable elements.");
+
+ is(funcScope.get("aNumber").target.querySelector(".name").getAttribute("value"), "aNumber",
+ "Should have the right property name for 'aNumber'.");
+ is(funcScope.get("aNumber").target.querySelector(".value").getAttribute("value"), "10",
+ "Should have the right property value for 'aNumber'.");
+ ok(funcScope.get("aNumber").target.querySelector(".value").className.includes("token-number"),
+ "Should have the right token class for 'aNumber'.");
+
+ is(funcScope.get("a").target.querySelector(".name").getAttribute("value"), "a",
+ "Should have the right property name for 'a'.");
+ is(funcScope.get("a").target.querySelector(".value").getAttribute("value"), "314.1592653589793",
+ "Should have the right property value for 'a'.");
+ ok(funcScope.get("a").target.querySelector(".value").className.includes("token-number"),
+ "Should have the right token class for 'a'.");
+
+ is(funcScope.get("r").target.querySelector(".name").getAttribute("value"), "r",
+ "Should have the right property name for 'r'.");
+ is(funcScope.get("r").target.querySelector(".value").getAttribute("value"), "10",
+ "Should have the right property value for 'r'.");
+ ok(funcScope.get("r").target.querySelector(".value").className.includes("token-number"),
+ "Should have the right token class for 'r'.");
+
+ is(funcScope.get("foo").target.querySelector(".name").getAttribute("value"), "foo",
+ "Should have the right property name for 'foo'.");
+ is(funcScope.get("foo").target.querySelector(".value").getAttribute("value"), "6.283185307179586",
+ "Should have the right property value for 'foo'.");
+ ok(funcScope.get("foo").target.querySelector(".value").className.includes("token-number"),
+ "Should have the right token class for 'foo'.");
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gVariables = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-frozen-sealed-nonext.js b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-frozen-sealed-nonext.js
new file mode 100644
index 000000000..736deeba1
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-frozen-sealed-nonext.js
@@ -0,0 +1,93 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * This test checks that we properly set the frozen, sealed, and non-extensbile
+ * attributes on variables so that the F/S/N is shown in the variables view.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_frame-parameters.html";
+
+var gTab, gPanel, gDebugger;
+
+function test() {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+
+ prepareTest();
+ });
+}
+
+function prepareTest() {
+ gDebugger.once(gDebugger.EVENTS.FETCHED_SCOPES, runTest);
+
+ evalInTab(gTab, "(" + function () {
+ var frozen = Object.freeze({});
+ var sealed = Object.seal({});
+ var nonExtensible = Object.preventExtensions({});
+ var extensible = {};
+ var string = "foo bar baz";
+
+ debugger;
+ } + "())");
+}
+
+function runTest() {
+ let hasNoneTester = function (aVariable) {
+ ok(!aVariable.hasAttribute("frozen"),
+ "The variable should not be frozen.");
+ ok(!aVariable.hasAttribute("sealed"),
+ "The variable should not be sealed.");
+ ok(!aVariable.hasAttribute("non-extensible"),
+ "The variable should be extensible.");
+ };
+
+ let testers = {
+ frozen: function (aVariable) {
+ ok(aVariable.hasAttribute("frozen"),
+ "The variable should be frozen.");
+ },
+ sealed: function (aVariable) {
+ ok(aVariable.hasAttribute("sealed"),
+ "The variable should be sealed.");
+ },
+ nonExtensible: function (aVariable) {
+ ok(aVariable.hasAttribute("non-extensible"),
+ "The variable should be non-extensible.");
+ },
+ extensible: hasNoneTester,
+ string: hasNoneTester,
+ arguments: hasNoneTester,
+ this: hasNoneTester
+ };
+
+ let variables = gDebugger.document.querySelectorAll(".variable-or-property");
+
+ for (let variable of variables) {
+ let name = variable.querySelector(".name").getAttribute("value");
+ let tester = testers[name];
+ delete testers[name];
+
+ ok(tester, "We should have a tester for the '" + name + "' variable.");
+ tester(variable);
+ }
+
+ is(Object.keys(testers).length, 0,
+ "We should have run and removed all the testers.");
+
+ resumeDebuggerThenCloseAndFinish(gPanel);
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-hide-non-enums.js b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-hide-non-enums.js
new file mode 100644
index 000000000..8094cfdc2
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-hide-non-enums.js
@@ -0,0 +1,111 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Test that non-enumerable variables and properties can be hidden
+ * in the variables view.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_recursion-stack.html";
+
+var gTab, gPanel, gDebugger;
+
+function test() {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+
+ waitForCaretAndScopes(gPanel, 14).then(performTest);
+ callInTab(gTab, "simpleCall");
+ });
+}
+
+function performTest() {
+ let testScope = gDebugger.DebuggerView.Variables.addScope("test-scope");
+ let testVar = testScope.addItem("foo");
+
+ testVar.addItems({
+ foo: {
+ value: "bar",
+ enumerable: true
+ },
+ bar: {
+ value: "foo",
+ enumerable: false
+ }
+ });
+
+ // Expand the scope and variable.
+ testScope.expand();
+ testVar.expand();
+
+ // Expanding the non-enumerable container is synchronously dispatched
+ // on the main thread, so wait for the next tick.
+ executeSoon(() => {
+ let details = testVar._enum;
+ let nonenum = testVar._nonenum;
+
+ is(details.childNodes.length, 1,
+ "There should be just one property in the .details container.");
+ ok(details.hasAttribute("open"),
+ ".details container should be visible.");
+ ok(nonenum.hasAttribute("open"),
+ ".nonenum container should be visible.");
+ is(nonenum.childNodes.length, 1,
+ "There should be just one property in the .nonenum container.");
+
+ // Uncheck 'show hidden properties'.
+ gDebugger.DebuggerView.Options._showVariablesOnlyEnumItem.setAttribute("checked", "true");
+ gDebugger.DebuggerView.Options._toggleShowVariablesOnlyEnum();
+
+ ok(details.hasAttribute("open"),
+ ".details container should stay visible.");
+ ok(!nonenum.hasAttribute("open"),
+ ".nonenum container should become hidden.");
+
+ // Check 'show hidden properties'.
+ gDebugger.DebuggerView.Options._showVariablesOnlyEnumItem.setAttribute("checked", "false");
+ gDebugger.DebuggerView.Options._toggleShowVariablesOnlyEnum();
+
+ ok(details.hasAttribute("open"),
+ ".details container should stay visible.");
+ ok(nonenum.hasAttribute("open"),
+ ".nonenum container should become visible.");
+
+ // Collapse the variable. This is done on the current tick.
+ testVar.collapse();
+
+ ok(!details.hasAttribute("open"),
+ ".details container should be hidden.");
+ ok(!nonenum.hasAttribute("open"),
+ ".nonenum container should be hidden.");
+
+ // Uncheck 'show hidden properties'.
+ gDebugger.DebuggerView.Options._showVariablesOnlyEnumItem.setAttribute("checked", "true");
+ gDebugger.DebuggerView.Options._toggleShowVariablesOnlyEnum();
+
+ ok(!details.hasAttribute("open"),
+ ".details container should stay hidden.");
+ ok(!nonenum.hasAttribute("open"),
+ ".nonenum container should stay hidden.");
+
+ // Check 'show hidden properties'.
+ gDebugger.DebuggerView.Options._showVariablesOnlyEnumItem.setAttribute("checked", "false");
+ gDebugger.DebuggerView.Options._toggleShowVariablesOnlyEnum();
+
+ resumeDebuggerThenCloseAndFinish(gPanel);
+ });
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-large-array-buffer.js b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-large-array-buffer.js
new file mode 100644
index 000000000..7ec1fe2f0
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-large-array-buffer.js
@@ -0,0 +1,253 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure that the variables view remains responsive when faced with
+ * huge ammounts of data.
+ */
+
+"use strict";
+
+const TAB_URL = EXAMPLE_URL + "doc_large-array-buffer.html";
+const {ELLIPSIS} = require("devtools/shared/l10n");
+
+
+var gTab, gPanel, gDebugger, gVariables;
+
+function test() {
+ // this test does a lot of work on large objects, default 45s is not enough
+ requestLongerTimeout(4);
+
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gVariables = gDebugger.DebuggerView.Variables;
+
+ waitForCaretAndScopes(gPanel, 28, 1)
+ .then(() => performTests())
+ .then(() => resumeDebuggerThenCloseAndFinish(gPanel))
+ .then(null, error => {
+ ok(false, "Got an error: " + error.message + "\n" + error.stack);
+ });
+
+ generateMouseClickInTab(gTab, "content.document.querySelector('button')");
+ });
+}
+
+const VARS_TO_TEST = [
+ {
+ varName: "buffer",
+ stringified: "ArrayBuffer",
+ doNotExpand: true
+ },
+ {
+ varName: "largeArray",
+ stringified: "Int8Array[10000]",
+ extraProps: [
+ [ "buffer", "ArrayBuffer" ],
+ [ "byteLength", "10000" ],
+ [ "byteOffset", "0" ],
+ [ "length", "10000" ],
+ [ "__proto__", "Int8ArrayPrototype" ]
+ ]
+ },
+ {
+ varName: "largeObject",
+ stringified: "Object[10000]",
+ extraProps: [
+ [ "__proto__", "Object" ]
+ ]
+ },
+ {
+ varName: "largeMap",
+ stringified: "Map[10000]",
+ hasEntries: true,
+ extraProps: [
+ [ "size", "10000" ],
+ [ "__proto__", "Object" ]
+ ]
+ },
+ {
+ varName: "largeSet",
+ stringified: "Set[10000]",
+ hasEntries: true,
+ extraProps: [
+ [ "size", "10000" ],
+ [ "__proto__", "Object" ]
+ ]
+ }
+];
+
+const PAGE_RANGES = [
+ [0, 2499], [2500, 4999], [5000, 7499], [7500, 9999]
+];
+
+function toPageNames(ranges) {
+ return ranges.map(([ from, to ]) => "[" + from + ELLIPSIS + to + "]");
+}
+
+function performTests() {
+ let localScope = gVariables.getScopeAtIndex(0);
+
+ return promise.all(VARS_TO_TEST.map(spec => {
+ let { varName, stringified, doNotExpand } = spec;
+
+ let variable = localScope.get(varName);
+ ok(variable,
+ `There should be a '${varName}' variable present in the scope.`);
+
+ is(variable.target.querySelector(".name").getAttribute("value"), varName,
+ `Should have the right property name for '${varName}'.`);
+ is(variable.target.querySelector(".value").getAttribute("value"), stringified,
+ `Should have the right property value for '${varName}'.`);
+ ok(variable.target.querySelector(".value").className.includes("token-other"),
+ `Should have the right token class for '${varName}'.`);
+
+ is(variable.expanded, false,
+ `The '${varName}' variable shouldn't be expanded.`);
+
+ if (doNotExpand) {
+ return promise.resolve();
+ }
+
+ return variable.expand()
+ .then(() => verifyFirstLevel(variable, spec));
+ }));
+}
+
+// In objects and arrays, the sliced pages are at the top-level of
+// the expanded object, but with Maps and Sets, we have to expand
+// <entries> first and look there.
+function getExpandedPages(variable, hasEntries) {
+ let expandedPages = promise.defer();
+ if (hasEntries) {
+ let entries = variable.get("<entries>");
+ ok(entries, "<entries> retrieved");
+ entries.expand().then(() => expandedPages.resolve(entries));
+ } else {
+ expandedPages.resolve(variable);
+ }
+
+ return expandedPages.promise;
+}
+
+function verifyFirstLevel(variable, spec) {
+ let { varName, hasEntries, extraProps } = spec;
+
+ let enums = variable._enum.childNodes;
+ let nonEnums = variable._nonenum.childNodes;
+
+ is(enums.length, hasEntries ? 1 : 4,
+ `The '${varName}' contains the right number of enumerable elements.`);
+ is(nonEnums.length, extraProps.length,
+ `The '${varName}' contains the right number of non-enumerable elements.`);
+
+ // the sliced pages begin after <entries> row
+ let pagesOffset = hasEntries ? 1 : 0;
+ let expandedPages = getExpandedPages(variable, hasEntries);
+
+ return expandedPages.then((pagesList) => {
+ toPageNames(PAGE_RANGES).forEach((pageName, i) => {
+ let index = i + pagesOffset;
+
+ is(pagesList.target.querySelectorAll(".variables-view-property .name")[index].getAttribute("value"),
+ pageName, `The page #${i + 1} in the '${varName}' is named correctly.`);
+ is(pagesList.target.querySelectorAll(".variables-view-property .value")[index].getAttribute("value"),
+ "", `The page #${i + 1} in the '${varName}' should not have a corresponding value.`);
+ });
+ }).then(() => {
+ extraProps.forEach(([ propName, propValue ], i) => {
+ // the extra props start after the 4 pages
+ let index = i + pagesOffset + 4;
+
+ is(variable.target.querySelectorAll(".variables-view-property .name")[index].getAttribute("value"),
+ propName, `The other properties in '${varName}' are named correctly.`);
+ is(variable.target.querySelectorAll(".variables-view-property .value")[index].getAttribute("value"),
+ propValue, `The other properties in '${varName}' have the correct value.`);
+ });
+ }).then(() => verifyNextLevels(variable, spec));
+}
+
+function verifyNextLevels(variable, spec) {
+ let { varName, hasEntries } = spec;
+
+ // the entries are already expanded in verifyFirstLevel
+ let pagesList = hasEntries ? variable.get("<entries>") : variable;
+
+ let lastPage = pagesList.get(toPageNames(PAGE_RANGES)[3]);
+ ok(lastPage, `The last page in the 1st level of '${varName}' was retrieved successfully.`);
+
+ return lastPage.expand()
+ .then(() => verifyNextLevels2(lastPage, varName));
+}
+
+function verifyNextLevels2(lastPage1, varName) {
+ const PAGE_RANGES_IN_LAST_PAGE = [
+ [7500, 8124], [8125, 8749], [8750, 9374], [9375, 9999]
+ ];
+
+ let pageEnums1 = lastPage1._enum.childNodes;
+ let pageNonEnums1 = lastPage1._nonenum.childNodes;
+ is(pageEnums1.length, 4,
+ `The last page in the 1st level of '${varName}' should contain all the created enumerable elements.`);
+ is(pageNonEnums1.length, 0,
+ `The last page in the 1st level of '${varName}' should not contain any non-enumerable elements.`);
+
+ let pageNames = toPageNames(PAGE_RANGES_IN_LAST_PAGE);
+ pageNames.forEach((pageName, i) => {
+ is(lastPage1._enum.querySelectorAll(".variables-view-property .name")[i].getAttribute("value"),
+ pageName, `The page #${i + 1} in the 2nd level of '${varName}' is named correctly.`);
+ });
+
+ let lastPage2 = lastPage1.get(pageNames[3]);
+ ok(lastPage2, "The last page in the 2nd level was retrieved successfully.");
+
+ return lastPage2.expand()
+ .then(() => verifyNextLevels3(lastPage2, varName));
+}
+
+function verifyNextLevels3(lastPage2, varName) {
+ let pageEnums2 = lastPage2._enum.childNodes;
+ let pageNonEnums2 = lastPage2._nonenum.childNodes;
+ is(pageEnums2.length, 625,
+ `The last page in the 3rd level of '${varName}' should contain all the created enumerable elements.`);
+ is(pageNonEnums2.length, 0,
+ `The last page in the 3rd level of '${varName}' shouldn't contain any non-enumerable elements.`);
+
+ const LEAF_ITEMS = [
+ [0, 9375, 624],
+ [1, 9376, 623],
+ [623, 9998, 1],
+ [624, 9999, 0]
+ ];
+
+ function expectedValue(name, value) {
+ switch (varName) {
+ case "largeArray": return 0;
+ case "largeObject": return value;
+ case "largeMap": return name + " \u2192 " + value;
+ case "largeSet": return value;
+ }
+ }
+
+ LEAF_ITEMS.forEach(([index, name, value]) => {
+ is(lastPage2._enum.querySelectorAll(".variables-view-property .name")[index].getAttribute("value"),
+ name, `The properties in the leaf level of '${varName}' are named correctly.`);
+ is(lastPage2._enum.querySelectorAll(".variables-view-property .value")[index].getAttribute("value"),
+ expectedValue(name, value), `The properties in the leaf level of '${varName}' have the correct value.`);
+ });
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gVariables = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-map-set.js b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-map-set.js
new file mode 100644
index 000000000..0c301c683
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-map-set.js
@@ -0,0 +1,117 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Test that Map and Set and their Weak friends are displayed in variables view.
+ */
+
+"use strict";
+
+const TAB_URL = EXAMPLE_URL + "doc_map-set.html";
+
+var test = Task.async(function* () {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ const [tab,, panel] = yield initDebugger(TAB_URL, options);
+
+ const scopes = waitForCaretAndScopes(panel, 37);
+ callInTab(tab, "startTest");
+ yield scopes;
+
+ const variables = panel.panelWin.DebuggerView.Variables;
+ ok(variables, "Should get the variables view.");
+
+ const scope = variables.getScopeAtIndex(0);
+ ok(scope, "Should get the current function's scope.");
+
+ /* Test the maps */
+ for (let varName of ["map", "weakMap"]) {
+ const mapVar = scope.get(varName);
+ ok(mapVar, `Retrieved the '${varName}' variable from the scope`);
+
+ info(`Expanding '${varName}' variable`);
+ yield mapVar.expand();
+
+ const entries = mapVar.get("<entries>");
+ ok(entries, `Retrieved the '${varName}' entries`);
+
+ info(`Expanding '${varName}' entries`);
+ yield entries.expand();
+
+ // Check the entries. WeakMap returns its entries in a nondeterministic
+ // order, so we make our job easier by not testing the exact values.
+ let i = 0;
+ for (let [ name, entry ] of entries) {
+ is(name, i, `The '${varName}' entry's property name is correct`);
+ ok(entry.displayValue.startsWith("Object \u2192 "),
+ `The '${varName}' entry's property value is correct`);
+ yield entry.expand();
+
+ let key = entry.get("key");
+ ok(key, `The '${varName}' entry has the 'key' property`);
+ yield key.expand();
+
+ let keyProperty = key.get("a");
+ ok(keyProperty,
+ `The '${varName}' entry's 'key' has the correct property`);
+
+ let value = entry.get("value");
+ ok(value, `The '${varName}' entry has the 'value' property`);
+
+ i++;
+ }
+
+ is(i, 2, `The '${varName}' entry count is correct`);
+
+ // Check the extra property on the object
+ let extraProp = mapVar.get("extraProp");
+ ok(extraProp, `Retrieved the '${varName}' extraProp`);
+ is(extraProp.displayValue, "true",
+ `The '${varName}' extraProp's value is correct`);
+ }
+
+ /* Test the sets */
+ for (let varName of ["set", "weakSet"]) {
+ const setVar = scope.get(varName);
+ ok(setVar, `Retrieved the '${varName}' variable from the scope`);
+
+ info(`Expanding '${varName}' variable`);
+ yield setVar.expand();
+
+ const entries = setVar.get("<entries>");
+ ok(entries, `Retrieved the '${varName}' entries`);
+
+ info(`Expanding '${varName}' entries`);
+ yield entries.expand();
+
+ // Check the entries. WeakSet returns its entries in a nondeterministic
+ // order, so we make our job easier by not testing the exact values.
+ let i = 0;
+ for (let [ name, entry ] of entries) {
+ is(name, i, `The '${varName}' entry's property name is correct`);
+ is(entry.displayValue, "Object",
+ `The '${varName}' entry's property value is correct`);
+ yield entry.expand();
+
+ let entryProperty = entry.get("a");
+ ok(entryProperty,
+ `The '${varName}' entry's value has the correct property`);
+
+ i++;
+ }
+
+ is(i, 2, `The '${varName}' entry count is correct`);
+
+ // Check the extra property on the object
+ let extraProp = setVar.get("extraProp");
+ ok(extraProp, `Retrieved the '${varName}' extraProp`);
+ is(extraProp.displayValue, "true",
+ `The '${varName}' extraProp's value is correct`);
+ }
+
+ resumeDebuggerThenCloseAndFinish(panel);
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-override-01.js b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-override-01.js
new file mode 100644
index 000000000..f923d7f53
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-override-01.js
@@ -0,0 +1,240 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that VariablesView methods responsible for styling variables
+ * as overridden work properly.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_scope-variable-2.html";
+
+function test() {
+ Task.spawn(function* () {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ let [tab,, panel] = yield initDebugger(TAB_URL, options);
+ let win = panel.panelWin;
+ let events = win.EVENTS;
+ let variables = win.DebuggerView.Variables;
+
+ callInTab(tab, "test");
+ yield waitForCaretAndScopes(panel, 23);
+
+ let firstScope = variables.getScopeAtIndex(0);
+ let secondScope = variables.getScopeAtIndex(1);
+ let thirdScope = variables.getScopeAtIndex(2);
+ let globalLexicalScope = variables.getScopeAtIndex(3);
+ let globalScope = variables.getScopeAtIndex(4);
+
+ ok(firstScope, "The first scope is available.");
+ ok(secondScope, "The second scope is available.");
+ ok(thirdScope, "The third scope is available.");
+ ok(globalLexicalScope, "The global lexical scope is available.");
+ ok(globalScope, "The global scope is available.");
+
+ is(firstScope.name, "Function scope [secondNest]",
+ "The first scope's name is correct.");
+ is(secondScope.name, "Function scope [firstNest]",
+ "The second scope's name is correct.");
+ is(thirdScope.name, "Function scope [test]",
+ "The third scope's name is correct.");
+ is(globalLexicalScope.name, "Block scope",
+ "The global lexical scope's name is correct.");
+ is(globalScope.name, "Global scope [Window]",
+ "The global scope's name is correct.");
+
+ is(firstScope.expanded, true,
+ "The first scope's expansion state is correct.");
+ is(secondScope.expanded, false,
+ "The second scope's expansion state is correct.");
+ is(thirdScope.expanded, false,
+ "The third scope's expansion state is correct.");
+ is(globalLexicalScope.expanded, false,
+ "The global lexical scope's expansion state is correct.");
+ is(globalScope.expanded, false,
+ "The global scope's expansion state is correct.");
+
+ is(firstScope._store.size, 3,
+ "The first scope should have all the variables available.");
+ is(secondScope._store.size, 0,
+ "The second scope should have no variables available yet.");
+ is(thirdScope._store.size, 0,
+ "The third scope should have no variables available yet.");
+ is(globalLexicalScope._store.size, 0,
+ "The global scope should have no variables available yet.");
+ is(globalScope._store.size, 0,
+ "The global scope should have no variables available yet.");
+
+ // Test getOwnerScopeForVariableOrProperty with simple variables.
+
+ let thisVar = firstScope.get("this");
+ let thisOwner = variables.getOwnerScopeForVariableOrProperty(thisVar);
+ is(thisOwner, firstScope,
+ "The getOwnerScopeForVariableOrProperty method works properly (1).");
+
+ let someVar1 = firstScope.get("a");
+ let someOwner1 = variables.getOwnerScopeForVariableOrProperty(someVar1);
+ is(someOwner1, firstScope,
+ "The getOwnerScopeForVariableOrProperty method works properly (2).");
+
+ // Test getOwnerScopeForVariableOrProperty with first-degree properties.
+
+ let argsVar1 = firstScope.get("arguments");
+ let fetched = waitForDebuggerEvents(panel, events.FETCHED_PROPERTIES);
+ argsVar1.expand();
+ yield fetched;
+
+ let calleeProp1 = argsVar1.get("callee");
+ let calleeOwner1 = variables.getOwnerScopeForVariableOrProperty(calleeProp1);
+ is(calleeOwner1, firstScope,
+ "The getOwnerScopeForVariableOrProperty method works properly (3).");
+
+ // Test getOwnerScopeForVariableOrProperty with second-degree properties.
+
+ let protoVar1 = argsVar1.get("__proto__");
+ fetched = waitForDebuggerEvents(panel, events.FETCHED_PROPERTIES);
+ protoVar1.expand();
+ yield fetched;
+
+ let constrProp1 = protoVar1.get("constructor");
+ let constrOwner1 = variables.getOwnerScopeForVariableOrProperty(constrProp1);
+ is(constrOwner1, firstScope,
+ "The getOwnerScopeForVariableOrProperty method works properly (4).");
+
+ // Test getOwnerScopeForVariableOrProperty with a simple variable
+ // from non-topmost scopes.
+
+ // Only need to wait for a single FETCHED_VARIABLES event, just for the
+ // global scope, because the other local scopes already have the
+ // arguments and variables available as evironment bindings.
+ fetched = waitForDebuggerEvents(panel, events.FETCHED_VARIABLES);
+ secondScope.expand();
+ thirdScope.expand();
+ globalLexicalScope.expand();
+ globalScope.expand();
+ yield fetched;
+
+ let someVar2 = secondScope.get("a");
+ let someOwner2 = variables.getOwnerScopeForVariableOrProperty(someVar2);
+ is(someOwner2, secondScope,
+ "The getOwnerScopeForVariableOrProperty method works properly (5).");
+
+ let someVar3 = thirdScope.get("a");
+ let someOwner3 = variables.getOwnerScopeForVariableOrProperty(someVar3);
+ is(someOwner3, thirdScope,
+ "The getOwnerScopeForVariableOrProperty method works properly (6).");
+
+ // Test getOwnerScopeForVariableOrProperty with first-degree properies
+ // from non-topmost scopes.
+
+ let argsVar2 = secondScope.get("arguments");
+ fetched = waitForDebuggerEvents(panel, events.FETCHED_PROPERTIES);
+ argsVar2.expand();
+ yield fetched;
+
+ let calleeProp2 = argsVar2.get("callee");
+ let calleeOwner2 = variables.getOwnerScopeForVariableOrProperty(calleeProp2);
+ is(calleeOwner2, secondScope,
+ "The getOwnerScopeForVariableOrProperty method works properly (7).");
+
+ let argsVar3 = thirdScope.get("arguments");
+ fetched = waitForDebuggerEvents(panel, events.FETCHED_PROPERTIES);
+ argsVar3.expand();
+ yield fetched;
+
+ let calleeProp3 = argsVar3.get("callee");
+ let calleeOwner3 = variables.getOwnerScopeForVariableOrProperty(calleeProp3);
+ is(calleeOwner3, thirdScope,
+ "The getOwnerScopeForVariableOrProperty method works properly (8).");
+
+ // Test getOwnerScopeForVariableOrProperty with second-degree properties
+ // from non-topmost scopes.
+
+ let protoVar2 = argsVar2.get("__proto__");
+ fetched = waitForDebuggerEvents(panel, events.FETCHED_PROPERTIES);
+ protoVar2.expand();
+ yield fetched;
+
+ let constrProp2 = protoVar2.get("constructor");
+ let constrOwner2 = variables.getOwnerScopeForVariableOrProperty(constrProp2);
+ is(constrOwner2, secondScope,
+ "The getOwnerScopeForVariableOrProperty method works properly (9).");
+
+ let protoVar3 = argsVar3.get("__proto__");
+ fetched = waitForDebuggerEvents(panel, events.FETCHED_PROPERTIES);
+ protoVar3.expand();
+ yield fetched;
+
+ let constrProp3 = protoVar3.get("constructor");
+ let constrOwner3 = variables.getOwnerScopeForVariableOrProperty(constrProp3);
+ is(constrOwner3, thirdScope,
+ "The getOwnerScopeForVariableOrProperty method works properly (10).");
+
+ // Test getParentScopesForVariableOrProperty with simple variables.
+
+ let varOwners1 = variables.getParentScopesForVariableOrProperty(someVar1);
+ let varOwners2 = variables.getParentScopesForVariableOrProperty(someVar2);
+ let varOwners3 = variables.getParentScopesForVariableOrProperty(someVar3);
+
+ is(varOwners1.length, 0,
+ "There should be no owner scopes for the first variable.");
+
+ is(varOwners2.length, 1,
+ "There should be one owner scope for the second variable.");
+ is(varOwners2[0], firstScope,
+ "The only owner scope for the second variable is correct.");
+
+ is(varOwners3.length, 2,
+ "There should be two owner scopes for the third variable.");
+ is(varOwners3[0], firstScope,
+ "The first owner scope for the third variable is correct.");
+ is(varOwners3[1], secondScope,
+ "The second owner scope for the third variable is correct.");
+
+ // Test getParentScopesForVariableOrProperty with first-degree properties.
+
+ let propOwners1 = variables.getParentScopesForVariableOrProperty(calleeProp1);
+ let propOwners2 = variables.getParentScopesForVariableOrProperty(calleeProp2);
+ let propOwners3 = variables.getParentScopesForVariableOrProperty(calleeProp3);
+
+ is(propOwners1.length, 0,
+ "There should be no owner scopes for the first property.");
+
+ is(propOwners2.length, 1,
+ "There should be one owner scope for the second property.");
+ is(propOwners2[0], firstScope,
+ "The only owner scope for the second property is correct.");
+
+ is(propOwners3.length, 2,
+ "There should be two owner scopes for the third property.");
+ is(propOwners3[0], firstScope,
+ "The first owner scope for the third property is correct.");
+ is(propOwners3[1], secondScope,
+ "The second owner scope for the third property is correct.");
+
+ // Test getParentScopesForVariableOrProperty with second-degree properties.
+
+ let secPropOwners1 = variables.getParentScopesForVariableOrProperty(constrProp1);
+ let secPropOwners2 = variables.getParentScopesForVariableOrProperty(constrProp2);
+ let secPropOwners3 = variables.getParentScopesForVariableOrProperty(constrProp3);
+
+ is(secPropOwners1.length, 0,
+ "There should be no owner scopes for the first inner property.");
+
+ is(secPropOwners2.length, 1,
+ "There should be one owner scope for the second inner property.");
+ is(secPropOwners2[0], firstScope,
+ "The only owner scope for the second inner property is correct.");
+
+ is(secPropOwners3.length, 2,
+ "There should be two owner scopes for the third inner property.");
+ is(secPropOwners3[0], firstScope,
+ "The first owner scope for the third inner property is correct.");
+ is(secPropOwners3[1], secondScope,
+ "The second owner scope for the third inner property is correct.");
+
+ yield resumeDebuggerThenCloseAndFinish(panel);
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-override-02.js b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-override-02.js
new file mode 100644
index 000000000..276efb665
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-override-02.js
@@ -0,0 +1,75 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests that overridden variables in the VariablesView are styled properly.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_scope-variable-2.html";
+
+function test() {
+ Task.spawn(function* () {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ let [tab,, panel] = yield initDebugger(TAB_URL, options);
+ let win = panel.panelWin;
+ let events = win.EVENTS;
+ let variables = win.DebuggerView.Variables;
+
+ // Wait for the hierarchy to be committed by the VariablesViewController.
+ let committedLocalScopeHierarchy = promise.defer();
+ variables.oncommit = committedLocalScopeHierarchy.resolve;
+
+
+ let onCaretAndScopes = waitForCaretAndScopes(panel, 23);
+ callInTab(tab, "test");
+ yield onCaretAndScopes;
+ yield committedLocalScopeHierarchy.promise;
+
+ let firstScope = variables.getScopeAtIndex(0);
+ let secondScope = variables.getScopeAtIndex(1);
+ let thirdScope = variables.getScopeAtIndex(2);
+
+ let someVar1 = firstScope.get("a");
+ let argsVar1 = firstScope.get("arguments");
+
+ is(someVar1.target.hasAttribute("overridden"), false,
+ "The first 'a' variable should not be marked as being overridden.");
+ is(argsVar1.target.hasAttribute("overridden"), false,
+ "The first 'arguments' variable should not be marked as being overridden.");
+
+ // Wait for the hierarchy to be committed by the VariablesViewController.
+ let committedSecondScopeHierarchy = promise.defer();
+ variables.oncommit = committedSecondScopeHierarchy.resolve;
+ secondScope.expand();
+ yield committedSecondScopeHierarchy.promise;
+
+ let someVar2 = secondScope.get("a");
+ let argsVar2 = secondScope.get("arguments");
+
+ is(someVar2.target.hasAttribute("overridden"), true,
+ "The second 'a' variable should be marked as being overridden.");
+ is(argsVar2.target.hasAttribute("overridden"), true,
+ "The second 'arguments' variable should be marked as being overridden.");
+
+ // Wait for the hierarchy to be committed by the VariablesViewController.
+ let committedThirdScopeHierarchy = promise.defer();
+ variables.oncommit = committedThirdScopeHierarchy.resolve;
+ thirdScope.expand();
+ yield committedThirdScopeHierarchy.promise;
+
+ let someVar3 = thirdScope.get("a");
+ let argsVar3 = thirdScope.get("arguments");
+
+ is(someVar3.target.hasAttribute("overridden"), true,
+ "The third 'a' variable should be marked as being overridden.");
+ is(argsVar3.target.hasAttribute("overridden"), true,
+ "The third 'arguments' variable should be marked as being overridden.");
+
+ yield resumeDebuggerThenCloseAndFinish(panel);
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-01.js b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-01.js
new file mode 100644
index 000000000..2e7244fad
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-01.js
@@ -0,0 +1,67 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests opening the variable inspection popup on a variable which has a
+ * simple literal as the value.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_frame-parameters.html";
+
+function test() {
+ Task.spawn(function* () {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ let [tab,, panel] = yield initDebugger(TAB_URL, options);
+ let win = panel.panelWin;
+ let bubble = win.DebuggerView.VariableBubble;
+ let tooltip = bubble._tooltip.panel;
+
+ bubble._ignoreLiterals = false;
+
+ function verifyContents(textContent, className) {
+ is(tooltip.querySelectorAll(".variables-view-container").length, 0,
+ "There should be no variables view containers added to the tooltip.");
+ is(tooltip.querySelectorAll(".devtools-tooltip-simple-text").length, 1,
+ "There should be a simple text node added to the tooltip instead.");
+
+ is(tooltip.querySelector(".devtools-tooltip-simple-text").textContent, textContent,
+ "The inspected property's value is correct.");
+ ok(tooltip.querySelector(".devtools-tooltip-simple-text").className.includes(className),
+ "The inspected property's value is colorized correctly.");
+ }
+
+
+ let onCaretAndScopes = waitForCaretAndScopes(panel, 24);
+ callInTab(tab, "start");
+ yield onCaretAndScopes;
+
+ // Inspect variables.
+ yield openVarPopup(panel, { line: 15, ch: 12 });
+ verifyContents("1", "token-number");
+
+ yield reopenVarPopup(panel, { line: 16, ch: 21 });
+ verifyContents("1", "token-number");
+
+ yield reopenVarPopup(panel, { line: 17, ch: 21 });
+ verifyContents("1", "token-number");
+
+ yield reopenVarPopup(panel, { line: 17, ch: 27 });
+ verifyContents("\"beta\"", "token-string");
+
+ yield reopenVarPopup(panel, { line: 17, ch: 44 });
+ verifyContents("false", "token-boolean");
+
+ yield reopenVarPopup(panel, { line: 17, ch: 54 });
+ verifyContents("null", "token-null");
+
+ yield reopenVarPopup(panel, { line: 17, ch: 63 });
+ verifyContents("undefined", "token-undefined");
+
+ yield resumeDebuggerThenCloseAndFinish(panel);
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-02.js b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-02.js
new file mode 100644
index 000000000..0f9843fc2
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-02.js
@@ -0,0 +1,53 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests opening the variable inspection popup on a variable which has a
+ * a property accessible via getters and setters.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_frame-parameters.html";
+
+function test() {
+ Task.spawn(function* () {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ let [tab,, panel] = yield initDebugger(TAB_URL, options);
+ let win = panel.panelWin;
+ let bubble = win.DebuggerView.VariableBubble;
+ let tooltip = bubble._tooltip.panel;
+
+ function verifyContents(textContent, className) {
+ is(tooltip.querySelectorAll(".variables-view-container").length, 0,
+ "There should be no variables view containers added to the tooltip.");
+ is(tooltip.querySelectorAll(".devtools-tooltip-simple-text").length, 1,
+ "There should be a simple text node added to the tooltip instead.");
+
+ is(tooltip.querySelector(".devtools-tooltip-simple-text").textContent, textContent,
+ "The inspected property's value is correct.");
+ ok(tooltip.querySelector(".devtools-tooltip-simple-text").className.includes(className),
+ "The inspected property's value is colorized correctly.");
+ }
+
+
+ let onCaretAndScopes = waitForCaretAndScopes(panel, 24);
+ callInTab(tab, "start");
+ yield onCaretAndScopes;
+
+ // Inspect properties.
+ yield openVarPopup(panel, { line: 19, ch: 10 });
+ verifyContents("42", "token-number");
+
+ yield reopenVarPopup(panel, { line: 20, ch: 14 });
+ verifyContents("42", "token-number");
+
+ yield reopenVarPopup(panel, { line: 21, ch: 14 });
+ verifyContents("42", "token-number");
+
+ yield resumeDebuggerThenCloseAndFinish(panel);
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-03.js b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-03.js
new file mode 100644
index 000000000..2b59fcfc5
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-03.js
@@ -0,0 +1,49 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests that the inspected indentifier is highlighted.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_frame-parameters.html";
+
+function test() {
+ Task.spawn(function* () {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ let [tab,, panel] = yield initDebugger(TAB_URL, options);
+ let win = panel.panelWin;
+ let bubble = win.DebuggerView.VariableBubble;
+
+ let onCaretAndScopes = waitForCaretAndScopes(panel, 24);
+ callInTab(tab, "start");
+ yield onCaretAndScopes;
+
+ // Inspect variable.
+ yield openVarPopup(panel, { line: 15, ch: 12 });
+
+ ok(bubble.contentsShown(),
+ "The variable should register as being shown.");
+ ok(!bubble._tooltip.isEmpty(),
+ "The variable inspection popup isn't empty.");
+ ok(bubble._markedText,
+ "There's some marked text in the editor.");
+ ok(bubble._markedText.clear,
+ "The marked text in the editor can be cleared.");
+
+ yield hideVarPopup(panel);
+
+ ok(!bubble.contentsShown(),
+ "The variable should register as being hidden.");
+ ok(bubble._tooltip.isEmpty(),
+ "The variable inspection popup is now empty.");
+ ok(!bubble._markedText,
+ "The marked text in the editor was removed.");
+
+ yield resumeDebuggerThenCloseAndFinish(panel);
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-04.js b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-04.js
new file mode 100644
index 000000000..b175b7a50
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-04.js
@@ -0,0 +1,38 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests that the variable inspection popup is hidden when the editor scrolls.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_frame-parameters.html";
+
+function test() {
+ Task.spawn(function* () {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ let [tab,, panel] = yield initDebugger(TAB_URL, options);
+ let win = panel.panelWin;
+ let bubble = win.DebuggerView.VariableBubble;
+
+ let onCaretAndScopes = waitForCaretAndScopes(panel, 24);
+ callInTab(tab, "start");
+ yield onCaretAndScopes;
+
+ // Inspect variable.
+ yield openVarPopup(panel, { line: 15, ch: 12 });
+ yield hideVarPopupByScrollingEditor(panel);
+ ok(true, "The variable inspection popup was hidden.");
+
+ ok(bubble._tooltip.isEmpty(),
+ "The variable inspection popup is now empty.");
+ ok(!bubble._markedText,
+ "The marked text in the editor was removed.");
+
+ yield resumeDebuggerThenCloseAndFinish(panel);
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-05.js b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-05.js
new file mode 100644
index 000000000..5f974efdf
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-05.js
@@ -0,0 +1,57 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests opening the variable inspection popup on a variable which has a
+ * simple object as the value.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_frame-parameters.html";
+
+function test() {
+ Task.spawn(function* () {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ let [tab,, panel] = yield initDebugger(TAB_URL, options);
+ let win = panel.panelWin;
+ let bubble = win.DebuggerView.VariableBubble;
+ let tooltip = bubble._tooltip.panel;
+
+ function verifyContents() {
+ is(tooltip.querySelectorAll(".variables-view-container").length, 1,
+ "There should be one variables view container added to the tooltip.");
+
+ is(tooltip.querySelectorAll(".variables-view-scope[untitled]").length, 1,
+ "There should be one scope with no header displayed.");
+ is(tooltip.querySelectorAll(".variables-view-variable[untitled]").length, 1,
+ "There should be one variable with no header displayed.");
+
+ is(tooltip.querySelectorAll(".variables-view-property").length, 2,
+ "There should be 2 properties displayed.");
+
+ is(tooltip.querySelectorAll(".variables-view-property .name")[0].getAttribute("value"), "a",
+ "The first property's name is correct.");
+ is(tooltip.querySelectorAll(".variables-view-property .value")[0].getAttribute("value"), "1",
+ "The first property's value is correct.");
+
+ is(tooltip.querySelectorAll(".variables-view-property .name")[1].getAttribute("value"), "__proto__",
+ "The second property's name is correct.");
+ is(tooltip.querySelectorAll(".variables-view-property .value")[1].getAttribute("value"), "Object",
+ "The second property's value is correct.");
+ }
+
+ let onCaretAndScopes = waitForCaretAndScopes(panel, 24);
+ callInTab(tab, "start");
+ yield onCaretAndScopes;
+
+ // Inspect variable.
+ yield openVarPopup(panel, { line: 16, ch: 12 }, true);
+ verifyContents();
+
+ yield resumeDebuggerThenCloseAndFinish(panel);
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-06.js b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-06.js
new file mode 100644
index 000000000..2d0d5f06a
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-06.js
@@ -0,0 +1,83 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests opening the variable inspection popup on a variable which has a
+ * complext object as the value.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_frame-parameters.html";
+
+function test() {
+ requestLongerTimeout(2);
+ Task.spawn(function* () {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ let [tab,, panel] = yield initDebugger(TAB_URL, options);
+ let win = panel.panelWin;
+ let bubble = win.DebuggerView.VariableBubble;
+ let tooltip = bubble._tooltip.panel;
+
+ function verifyContents() {
+ is(tooltip.querySelectorAll(".variables-view-container").length, 1,
+ "There should be one variables view container added to the tooltip.");
+
+ is(tooltip.querySelectorAll(".variables-view-scope[untitled]").length, 1,
+ "There should be one scope with no header displayed.");
+ is(tooltip.querySelectorAll(".variables-view-variable[untitled]").length, 1,
+ "There should be one variable with no header displayed.");
+
+ is(tooltip.querySelectorAll(".variables-view-property").length, 7,
+ "There should be 7 properties displayed.");
+
+ is(tooltip.querySelectorAll(".variables-view-property .name")[0].getAttribute("value"), "a",
+ "The first property's name is correct.");
+ is(tooltip.querySelectorAll(".variables-view-property .value")[0].getAttribute("value"), "1",
+ "The first property's value is correct.");
+
+ is(tooltip.querySelectorAll(".variables-view-property .name")[1].getAttribute("value"), "b",
+ "The second property's name is correct.");
+ is(tooltip.querySelectorAll(".variables-view-property .value")[1].getAttribute("value"), "\"beta\"",
+ "The second property's value is correct.");
+
+ is(tooltip.querySelectorAll(".variables-view-property .name")[2].getAttribute("value"), "c",
+ "The third property's name is correct.");
+ is(tooltip.querySelectorAll(".variables-view-property .value")[2].getAttribute("value"), "3",
+ "The third property's value is correct.");
+
+ is(tooltip.querySelectorAll(".variables-view-property .name")[3].getAttribute("value"), "d",
+ "The fourth property's name is correct.");
+ is(tooltip.querySelectorAll(".variables-view-property .value")[3].getAttribute("value"), "false",
+ "The fourth property's value is correct.");
+
+ is(tooltip.querySelectorAll(".variables-view-property .name")[4].getAttribute("value"), "e",
+ "The fifth property's name is correct.");
+ is(tooltip.querySelectorAll(".variables-view-property .value")[4].getAttribute("value"), "null",
+ "The fifth property's value is correct.");
+
+ is(tooltip.querySelectorAll(".variables-view-property .name")[5].getAttribute("value"), "f",
+ "The sixth property's name is correct.");
+ is(tooltip.querySelectorAll(".variables-view-property .value")[5].getAttribute("value"), "undefined",
+ "The sixth property's value is correct.");
+
+ is(tooltip.querySelectorAll(".variables-view-property .name")[6].getAttribute("value"), "__proto__",
+ "The seventh property's name is correct.");
+ is(tooltip.querySelectorAll(".variables-view-property .value")[6].getAttribute("value"), "Object",
+ "The seventh property's value is correct.");
+ }
+
+ let onCaretAndScopes = waitForCaretAndScopes(panel, 24);
+ callInTab(tab, "start");
+ yield onCaretAndScopes;
+
+ // Inspect variable.
+ yield openVarPopup(panel, { line: 17, ch: 12 }, true);
+ verifyContents();
+
+ yield resumeDebuggerThenCloseAndFinish(panel);
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-07.js b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-07.js
new file mode 100644
index 000000000..1340f14c6
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-07.js
@@ -0,0 +1,70 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests the variable inspection popup behaves correctly when switching
+ * between simple and complex objects.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_frame-parameters.html";
+
+function test() {
+ Task.spawn(function* () {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ let [tab,, panel] = yield initDebugger(TAB_URL, options);
+ let win = panel.panelWin;
+ let bubble = win.DebuggerView.VariableBubble;
+ let tooltip = bubble._tooltip.panel;
+
+ function verifySimpleContents(textContent, className) {
+ is(tooltip.querySelectorAll(".variables-view-container").length, 0,
+ "There should be no variables view container added to the tooltip.");
+ is(tooltip.querySelectorAll(".devtools-tooltip-simple-text").length, 1,
+ "There should be one simple text node added to the tooltip.");
+
+ is(tooltip.querySelector(".devtools-tooltip-simple-text").textContent, textContent,
+ "The inspected property's value is correct.");
+ ok(tooltip.querySelector(".devtools-tooltip-simple-text").className.includes(className),
+ "The inspected property's value is colorized correctly.");
+ }
+
+ function verifyComplexContents(propertyCount) {
+ is(tooltip.querySelectorAll(".variables-view-container").length, 1,
+ "There should be one variables view container added to the tooltip.");
+ is(tooltip.querySelectorAll(".devtools-tooltip-simple-text").length, 0,
+ "There should be no simple text node added to the tooltip.");
+
+ is(tooltip.querySelectorAll(".variables-view-scope[untitled]").length, 1,
+ "There should be one scope with no header displayed.");
+ is(tooltip.querySelectorAll(".variables-view-variable[untitled]").length, 1,
+ "There should be one variable with no header displayed.");
+
+ ok(tooltip.querySelectorAll(".variables-view-property").length >= propertyCount,
+ "There should be some properties displayed.");
+ }
+
+ let onCaretAndScopes = waitForCaretAndScopes(panel, 24);
+ callInTab(tab, "start");
+ yield onCaretAndScopes;
+
+ // Inspect variables.
+ yield openVarPopup(panel, { line: 15, ch: 12 });
+ verifySimpleContents("1", "token-number");
+
+ yield reopenVarPopup(panel, { line: 16, ch: 12 }, true);
+ verifyComplexContents(2);
+
+ yield reopenVarPopup(panel, { line: 19, ch: 10 });
+ verifySimpleContents("42", "token-number");
+
+ yield reopenVarPopup(panel, { line: 31, ch: 10 }, true);
+ verifyComplexContents(100);
+
+ yield resumeDebuggerThenCloseAndFinish(panel);
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-08.js b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-08.js
new file mode 100644
index 000000000..d3ef69e7e
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-08.js
@@ -0,0 +1,75 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests opening inspecting variables works across scopes.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_scope-variable.html";
+
+function test() {
+ Task.spawn(function* () {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ let [tab,, panel] = yield initDebugger(TAB_URL, options);
+ let win = panel.panelWin;
+ let events = win.EVENTS;
+ let editor = win.DebuggerView.editor;
+ let frames = win.DebuggerView.StackFrames;
+ let bubble = win.DebuggerView.VariableBubble;
+ let tooltip = bubble._tooltip.panel;
+
+ function verifyContents(textContent, className) {
+ is(tooltip.querySelectorAll(".variables-view-container").length, 0,
+ "There should be no variables view containers added to the tooltip.");
+ is(tooltip.querySelectorAll(".devtools-tooltip-simple-text").length, 1,
+ "There should be a simple text node added to the tooltip instead.");
+
+ is(tooltip.querySelector(".devtools-tooltip-simple-text").textContent, textContent,
+ "The inspected property's value is correct.");
+ ok(tooltip.querySelector(".devtools-tooltip-simple-text").className.includes(className),
+ "The inspected property's value is colorized correctly.");
+ }
+
+ function checkView(selectedFrame, caretLine) {
+ is(win.gThreadClient.state, "paused",
+ "Should only be getting stack frames while paused.");
+ is(frames.itemCount, 2,
+ "Should have two frames.");
+ is(frames.selectedDepth, selectedFrame,
+ "The correct frame is selected in the widget.");
+ ok(isCaretPos(panel, caretLine),
+ "Editor caret location is correct.");
+ }
+
+ let onCaretAndScopes = waitForCaretAndScopes(panel, 20);
+ callInTab(tab, "test");
+ yield onCaretAndScopes;
+
+ checkView(0, 20);
+
+ // Inspect variable in topmost frame.
+ yield openVarPopup(panel, { line: 18, ch: 12 });
+ verifyContents("\"second scope\"", "token-string");
+ checkView(0, 20);
+
+ // Hide the popup and change the frame.
+ yield hideVarPopup(panel);
+
+ let updatedFrame = waitForDebuggerEvents(panel, events.FETCHED_SCOPES);
+ frames.selectedDepth = 1;
+ yield updatedFrame;
+ checkView(1, 15);
+
+ // Inspect variable in oldest frame.
+ yield openVarPopup(panel, { line: 13, ch: 12 });
+ verifyContents("\"first scope\"", "token-string");
+ checkView(1, 15);
+
+ yield resumeDebuggerThenCloseAndFinish(panel);
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-09.js b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-09.js
new file mode 100644
index 000000000..41824cb76
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-09.js
@@ -0,0 +1,39 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests opening inspecting variables works across scopes.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_scope-variable-3.html";
+
+function test() {
+ Task.spawn(function* () {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ let [tab,, panel] = yield initDebugger(TAB_URL, options);
+ let win = panel.panelWin;
+ let bubble = win.DebuggerView.VariableBubble;
+ let tooltip = bubble._tooltip.panel;
+
+ let onCaretAndScopes = waitForCaretAndScopes(panel, 15);
+ callInTab(tab, "test");
+ yield onCaretAndScopes;
+
+ yield openVarPopup(panel, { line: 12, ch: 10 });
+ ok(true, "The variable inspection popup was shown for the real variable.");
+
+ once(tooltip, "popupshown").then(() => {
+ ok(false, "The variable inspection popup shouldn't have been opened.");
+ });
+
+ reopenVarPopup(panel, { line: 18, ch: 10 });
+ yield waitForTime(1000);
+
+ yield resumeDebuggerThenCloseAndFinish(panel);
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-10.js b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-10.js
new file mode 100644
index 000000000..15dd02c44
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-10.js
@@ -0,0 +1,67 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Makes sure the source editor's scroll location doesn't change when
+ * a variable inspection popup is opened and a watch expression is
+ * also evaluated at the same time.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_frame-parameters.html";
+
+function test() {
+ Task.spawn(function* () {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ let [tab,, panel] = yield initDebugger(TAB_URL, options);
+ let win = panel.panelWin;
+ let events = win.EVENTS;
+ let editor = win.DebuggerView.editor;
+ let editorContainer = win.document.getElementById("editor");
+ let bubble = win.DebuggerView.VariableBubble;
+ let expressions = win.DebuggerView.WatchExpressions;
+ let tooltip = bubble._tooltip.panel;
+
+ let onCaretAndScopes = waitForCaretAndScopes(panel, 24);
+ callInTab(tab, "start");
+ yield onCaretAndScopes;
+
+ let expressionsEvaluated = waitForDebuggerEvents(panel, events.FETCHED_WATCH_EXPRESSIONS);
+ expressions.addExpression("this");
+ editor.focus();
+ yield expressionsEvaluated;
+
+ // Scroll to the top of the editor and inspect variables.
+ let breakpointScrollPosition = editor.getScrollInfo().top;
+ editor.setFirstVisibleLine(0);
+ let topmostScrollPosition = editor.getScrollInfo().top;
+
+ ok(topmostScrollPosition < breakpointScrollPosition,
+ "The editor is now scrolled to the top (0).");
+ is(editor.getFirstVisibleLine(), 0,
+ "The editor is now scrolled to the top (1).");
+
+ let failPopup = () => ok(false, "The popup has got unexpectedly hidden.");
+ let failScroll = () => ok(false, "The editor has got unexpectedly scrolled.");
+ tooltip.addEventListener("popuphiding", failPopup);
+ editorContainer.addEventListener("scroll", failScroll);
+ editor.on("scroll", () => {
+ if (editor.getScrollInfo().top > topmostScrollPosition) {
+ ok(false, "The editor scrolled back to the breakpoint location.");
+ }
+ });
+
+ expressionsEvaluated = waitForDebuggerEvents(panel, events.FETCHED_WATCH_EXPRESSIONS);
+ yield openVarPopup(panel, { line: 14, ch: 15 });
+ yield expressionsEvaluated;
+
+ tooltip.removeEventListener("popuphiding", failPopup);
+ editorContainer.removeEventListener("scroll", failScroll);
+
+ yield resumeDebuggerThenCloseAndFinish(panel);
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-11.js b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-11.js
new file mode 100644
index 000000000..57d31c727
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-11.js
@@ -0,0 +1,84 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests that the watch expression button is added in variable view popup.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_watch-expression-button.html";
+
+function test() {
+ Task.spawn(function* () {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ let [tab,, panel] = yield initDebugger(TAB_URL, options);
+ let win = panel.panelWin;
+ let events = win.EVENTS;
+ let watch = win.DebuggerView.WatchExpressions;
+ let bubble = win.DebuggerView.VariableBubble;
+ let tooltip = bubble._tooltip.panel;
+
+ let label = win.L10N.getStr("addWatchExpressionButton");
+ let className = "dbg-expression-button";
+
+ function testExpressionButton(aLabel, aClassName, aExpression) {
+ ok(tooltip.querySelector("button"),
+ "There should be a button available in variable view popup.");
+ is(tooltip.querySelector("button").label, aLabel,
+ "The button available is labeled correctly.");
+ is(tooltip.querySelector("button").className, aClassName,
+ "The button available is styled correctly.");
+
+ tooltip.querySelector("button").click();
+
+ ok(!tooltip.querySelector("button"),
+ "There should be no button available in variable view popup.");
+ ok(watch.getItemAtIndex(0),
+ "The expression at index 0 should be available.");
+ is(watch.getItemAtIndex(0).attachment.initialExpression, aExpression,
+ "The expression at index 0 is correct.");
+ }
+
+ let onCaretAndScopes = waitForCaretAndScopes(panel, 19);
+ callInTab(tab, "start");
+ yield onCaretAndScopes;
+
+ // Inspect primitive value variable.
+ yield openVarPopup(panel, { line: 15, ch: 12 });
+ let popupHiding = once(tooltip, "popuphiding");
+ let expressionsEvaluated = waitForDebuggerEvents(panel, events.FETCHED_WATCH_EXPRESSIONS);
+ testExpressionButton(label, className, "a");
+ yield promise.all([popupHiding, expressionsEvaluated]);
+ ok(true, "The new watch expressions were re-evaluated and the panel got hidden (1).");
+
+ // Inspect non primitive value variable.
+ expressionsEvaluated = waitForDebuggerEvents(panel, events.FETCHED_WATCH_EXPRESSIONS);
+ yield openVarPopup(panel, { line: 16, ch: 12 }, true);
+ yield expressionsEvaluated;
+ ok(true, "The watch expressions were re-evaluated when a new panel opened (1).");
+
+ popupHiding = once(tooltip, "popuphiding");
+ expressionsEvaluated = waitForDebuggerEvents(panel, events.FETCHED_WATCH_EXPRESSIONS);
+ testExpressionButton(label, className, "b");
+ yield promise.all([popupHiding, expressionsEvaluated]);
+ ok(true, "The new watch expressions were re-evaluated and the panel got hidden (2).");
+
+ // Inspect property of an object.
+ expressionsEvaluated = waitForDebuggerEvents(panel, events.FETCHED_WATCH_EXPRESSIONS);
+ yield openVarPopup(panel, { line: 17, ch: 10 });
+ yield expressionsEvaluated;
+ ok(true, "The watch expressions were re-evaluated when a new panel opened (2).");
+
+ popupHiding = once(tooltip, "popuphiding");
+ expressionsEvaluated = waitForDebuggerEvents(panel, events.FETCHED_WATCH_EXPRESSIONS);
+ testExpressionButton(label, className, "b.a");
+ yield promise.all([popupHiding, expressionsEvaluated]);
+ ok(true, "The new watch expressions were re-evaluated and the panel got hidden (3).");
+
+ yield resumeDebuggerThenCloseAndFinish(panel);
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-12.js b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-12.js
new file mode 100644
index 000000000..588276434
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-12.js
@@ -0,0 +1,77 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests that the clicking "Watch" button twice, for the same expression, only adds it
+ * once.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_watch-expression-button.html";
+
+function test() {
+ Task.spawn(function* () {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ let [tab,, panel] = yield initDebugger(TAB_URL, options);
+ let win = panel.panelWin;
+ let events = win.EVENTS;
+ let watch = win.DebuggerView.WatchExpressions;
+ let bubble = win.DebuggerView.VariableBubble;
+ let tooltip = bubble._tooltip.panel;
+
+ function verifyContent(aExpression, aItemCount) {
+
+ ok(watch.getItemAtIndex(0),
+ "The expression at index 0 should be available.");
+ is(watch.getItemAtIndex(0).attachment.initialExpression, aExpression,
+ "The expression at index 0 is correct.");
+ is(watch.itemCount, aItemCount,
+ "The expression count is correct.");
+ }
+
+ let onCaretAndScopes = waitForCaretAndScopes(panel, 19);
+ callInTab(tab, "start");
+ yield onCaretAndScopes;
+
+ // Inspect primitive value variable.
+ yield openVarPopup(panel, { line: 15, ch: 12 });
+ let popupHiding = once(tooltip, "popuphiding");
+ let expressionsEvaluated = waitForDebuggerEvents(panel, events.FETCHED_WATCH_EXPRESSIONS);
+ tooltip.querySelector("button").click();
+ verifyContent("a", 1);
+ yield promise.all([popupHiding, expressionsEvaluated]);
+ ok(true, "The new watch expressions were re-evaluated and the panel got hidden (1).");
+
+ // Inspect property of an object.
+ expressionsEvaluated = waitForDebuggerEvents(panel, events.FETCHED_WATCH_EXPRESSIONS);
+ yield openVarPopup(panel, { line: 17, ch: 10 });
+ yield expressionsEvaluated;
+ ok(true, "The watch expressions were re-evaluated when a new panel opened (1).");
+
+ popupHiding = once(tooltip, "popuphiding");
+ expressionsEvaluated = waitForDebuggerEvents(panel, events.FETCHED_WATCH_EXPRESSIONS);
+ tooltip.querySelector("button").click();
+ verifyContent("b.a", 2);
+ yield promise.all([popupHiding, expressionsEvaluated]);
+ ok(true, "The new watch expressions were re-evaluated and the panel got hidden (2).");
+
+ // Re-inspect primitive value variable.
+ expressionsEvaluated = waitForDebuggerEvents(panel, events.FETCHED_WATCH_EXPRESSIONS);
+ yield openVarPopup(panel, { line: 15, ch: 12 });
+ yield expressionsEvaluated;
+ ok(true, "The watch expressions were re-evaluated when a new panel opened (2).");
+
+ popupHiding = once(tooltip, "popuphiding");
+ expressionsEvaluated = waitForDebuggerEvents(panel, events.FETCHED_WATCH_EXPRESSIONS);
+ tooltip.querySelector("button").click();
+ verifyContent("b.a", 2);
+ yield promise.all([popupHiding, expressionsEvaluated]);
+ ok(true, "The new watch expressions were re-evaluated and the panel got hidden (3).");
+
+ yield resumeDebuggerThenCloseAndFinish(panel);
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-13.js b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-13.js
new file mode 100644
index 000000000..e8769ced7
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-13.js
@@ -0,0 +1,68 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests that the variable inspection popup has inspector links for DOMNode
+ * properties and that the popup closes when the link is clicked
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_domnode-variables.html";
+
+function test() {
+ Task.spawn(function* () {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ let [tab,, panel] = yield initDebugger(TAB_URL, options);
+ let win = panel.panelWin;
+ let bubble = win.DebuggerView.VariableBubble;
+ let tooltip = bubble._tooltip.panel;
+ let toolbox = gDevTools.getToolbox(panel.target);
+
+ function getDomNodeInTooltip(propertyName) {
+ let domNodeProperties = tooltip.querySelectorAll(".token-domnode");
+ for (let prop of domNodeProperties) {
+ let propName = prop.parentNode.querySelector(".name");
+ if (propName.getAttribute("value") === propertyName) {
+ ok(true, "DOMNode " + propertyName + " was found in the tooltip");
+ return prop;
+ }
+ }
+ ok(false, "DOMNode " + propertyName + " wasn't found in the tooltip");
+ }
+
+ let onCaretAndScopes = waitForCaretAndScopes(panel, 19);
+ callInTab(tab, "start");
+ yield onCaretAndScopes;
+
+ // Inspect the div DOM variable.
+ yield openVarPopup(panel, { line: 17, ch: 38 }, true);
+ let property = getDomNodeInTooltip("firstElementChild");
+
+ // Simulate mouseover on the property value
+ let highlighted = once(toolbox, "node-highlight");
+ EventUtils.sendMouseEvent({ type: "mouseover" }, property,
+ property.ownerDocument.defaultView);
+ yield highlighted;
+ ok(true, "The node-highlight event was fired on hover of the DOMNode");
+
+ // Simulate a click on the "select in inspector" button
+ let button = property.parentNode.querySelector(".variables-view-open-inspector");
+ ok(button, "The select-in-inspector button is present");
+ let inspectorSelected = once(toolbox, "inspector-selected");
+ EventUtils.sendMouseEvent({ type: "mousedown" }, button,
+ button.ownerDocument.defaultView);
+ yield inspectorSelected;
+ ok(true, "The inspector got selected when clicked on the select-in-inspector");
+
+ // Make sure the inspector's initialization is finalized before ending the test
+ // Listening to the event *after* triggering the switch to the inspector isn't
+ // a problem as the inspector is asynchronously loaded.
+ yield once(toolbox.getPanel("inspector"), "inspector-updated");
+
+ yield resumeDebuggerThenCloseAndFinish(panel);
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-14.js b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-14.js
new file mode 100644
index 000000000..b1cefc8b8
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-14.js
@@ -0,0 +1,55 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests that the variable inspection popup is hidden when
+ * selecting text in the editor.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_frame-parameters.html";
+
+function test() {
+ Task.spawn(function* () {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ let [tab,, panel] = yield initDebugger(TAB_URL, options);
+ let win = panel.panelWin;
+ let bubble = win.DebuggerView.VariableBubble;
+
+ let onCaretAndScopes = waitForCaretAndScopes(panel, 24);
+ callInTab(tab, "start");
+ yield onCaretAndScopes;
+
+ // Select some text.
+ let cursor = win.DebuggerView.editor.getOffset({ line: 15, ch: 12 });
+ let [ anchor, head ] = win.DebuggerView.editor.getPosition(
+ cursor,
+ cursor + 3
+ );
+ win.DebuggerView.editor.setSelection(anchor, head);
+
+ // Try to Inspect variable during selection.
+ let popupOpened = yield intendOpenVarPopup(panel, { line: 15, ch: 12 }, true);
+
+ // Ensure the bubble is not there
+ ok(!popupOpened,
+ "The popup is not opened");
+ ok(!bubble._markedText,
+ "The marked text in the editor is not there.");
+
+ // Try to Inspect variable after selection.
+ popupOpened = yield intendOpenVarPopup(panel, { line: 15, ch: 12 }, false);
+
+ // Ensure the bubble is not there
+ ok(popupOpened,
+ "The popup is opened");
+ ok(bubble._markedText,
+ "The marked text in the editor is there.");
+
+ yield resumeDebuggerThenCloseAndFinish(panel);
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-15.js b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-15.js
new file mode 100644
index 000000000..01c72df8c
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-15.js
@@ -0,0 +1,39 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests opening the variable inspection popup directly on literals.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_frame-parameters.html";
+
+function test() {
+ Task.spawn(function* () {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ let [tab,, panel] = yield initDebugger(TAB_URL, options);
+ let win = panel.panelWin;
+ let bubble = win.DebuggerView.VariableBubble;
+ let tooltip = bubble._tooltip.panel;
+
+ let onCaretAndScopes = waitForCaretAndScopes(panel, 24);
+ callInTab(tab, "start");
+ yield onCaretAndScopes;
+
+ yield openVarPopup(panel, { line: 15, ch: 12 });
+ ok(true, "The variable inspection popup was shown for the real variable.");
+
+ once(tooltip, "popupshown").then(() => {
+ ok(false, "The variable inspection popup shouldn't have been opened.");
+ });
+
+ reopenVarPopup(panel, { line: 17, ch: 27 });
+ yield waitForTime(1000);
+
+ yield resumeDebuggerThenCloseAndFinish(panel);
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-16.js b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-16.js
new file mode 100644
index 000000000..055517810
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-16.js
@@ -0,0 +1,77 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+requestLongerTimeout(2);
+
+/**
+ * Tests if opening the variables inspection popup preserves the highlighting
+ * associated with the currently debugged line.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_recursion-stack.html";
+
+function test() {
+ Task.spawn(function* () {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ let [tab,, panel] = yield initDebugger(TAB_URL, options);
+ let win = panel.panelWin;
+ let events = win.EVENTS;
+ let editor = win.DebuggerView.editor;
+ let frames = win.DebuggerView.StackFrames;
+ let variables = win.DebuggerView.Variables;
+ let bubble = win.DebuggerView.VariableBubble;
+ let tooltip = bubble._tooltip.panel;
+
+ function checkView(selectedFrame, caretLine, debugLine = caretLine) {
+ let deferred = promise.defer();
+
+ is(win.gThreadClient.state, "paused",
+ "Should only be getting stack frames while paused.");
+ is(frames.itemCount, 25,
+ "Should have 25 frames.");
+ is(frames.selectedDepth, selectedFrame,
+ "The correct frame is selected in the widget.");
+ ok(isCaretPos(panel, caretLine),
+ "Editor caret location is correct.");
+
+ // The editor's debug location takes a tick to update.
+ executeSoon(() => {
+ ok(isCaretPos(panel, caretLine), "Editor caret location is still correct.");
+ ok(isDebugPos(panel, debugLine), "Editor debug location is correct.");
+ deferred.resolve();
+ });
+
+ return deferred.promise;
+ }
+
+ function expandGlobalScope() {
+ let globalScope = variables.getScopeAtIndex(2);
+ is(globalScope.expanded, false,
+ "The globalScope should not be expanded yet.");
+
+ let finished = waitForDebuggerEvents(panel, events.FETCHED_VARIABLES);
+ globalScope.expand();
+ return finished;
+ }
+
+ let onCaretAndScopes = waitForCaretAndScopes(panel, 26);
+ callInTab(tab, "recurse");
+ yield onCaretAndScopes;
+
+ yield checkView(0, 26);
+
+ yield expandGlobalScope();
+ yield checkView(0, 26);
+
+ // Inspect variable in topmost frame.
+ yield openVarPopup(panel, { line: 26, ch: 11 });
+ yield checkView(0, 26);
+
+ yield resumeDebuggerThenCloseAndFinish(panel);
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-17.js b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-17.js
new file mode 100644
index 000000000..bdfe1a42b
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-popup-17.js
@@ -0,0 +1,80 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests opening the variable inspection popup while stopped at a debugger statement,
+ * clicking "step in" and verifying that the popup is gone.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_with-frame.html";
+
+let gTab, gPanel, gDebugger;
+let actions, gSources, gVariables;
+
+function test() {
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ actions = bindActionCreators(gPanel);
+ gSources = gDebugger.DebuggerView.Sources;
+ gVariables = gDebugger.DebuggerView.Variables;
+ let bubble = gDebugger.DebuggerView.VariableBubble;
+ let tooltip = bubble._tooltip.panel;
+ let testPopupHiding = Task.async(function* () {
+ yield addBreakpoint();
+ yield ensureThreadClientState(gPanel, "resumed");
+ yield pauseDebuggee();
+ yield openVarPopup(gPanel, { line: 20, ch: 17 });
+ is(tooltip.querySelectorAll(".devtools-tooltip-simple-text").length, 1,
+ "The popup should be open with a simple text entry");
+ // Now we're stopped at a breakpoint with an open popup
+ // we'll send a keypress and check if the popup closes
+ executeSoon(() => EventUtils.synthesizeKey("VK_F11", {}));
+ // The keypress should cause one resumed event and one paused event
+ yield waitForThreadEvents(gPanel, "resumed");
+ yield waitForThreadEvents(gPanel, "paused");
+ // Here's the state we're actually interested in checking..
+ checkVariablePopupClosed(bubble);
+ yield resumeDebuggerThenCloseAndFinish(gPanel);
+ });
+ testPopupHiding();
+ });
+}
+
+function addBreakpoint() {
+ return actions.addBreakpoint({ actor: gSources.selectedValue, line: 21 });
+}
+
+function pauseDebuggee() {
+ generateMouseClickInTab(gTab, "content.document.querySelector('button')");
+
+ // The first 'with' scope should be expanded by default, but the
+ // variables haven't been fetched yet. This is how 'with' scopes work.
+ return promise.all([
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES),
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_VARIABLES)
+ ]);
+}
+
+function checkVariablePopupClosed(bubble) {
+ ok(!bubble.contentsShown(),
+ "When stepping, popup should close and be hidden.");
+ ok(bubble._tooltip.isEmpty(),
+ "The variable inspection popup should now be empty.");
+ ok(!bubble._markedText,
+ "The marked text in the editor was removed.");
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ actions = null;
+ gSources = null;
+ gVariables = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-reexpand-01.js b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-reexpand-01.js
new file mode 100644
index 000000000..4b68fb052
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-reexpand-01.js
@@ -0,0 +1,211 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure that the variables view correctly re-expands nodes after pauses.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_with-frame.html";
+
+function test() {
+ // Debug test slaves are a bit slow at this test.
+ requestLongerTimeout(4);
+
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ const gTab = aTab;
+ const gPanel = aPanel;
+ const gDebugger = gPanel.panelWin;
+ const gSources = gDebugger.DebuggerView.Sources;
+ const gVariables = gDebugger.DebuggerView.Variables;
+ const queries = gDebugger.require("./content/queries");
+ const getState = gDebugger.DebuggerController.getState;
+ const actions = bindActionCreators(gPanel);
+
+ // Always expand all items between pauses except 'window' variables.
+ gVariables.commitHierarchyIgnoredItems = Object.create(null, { window: { value: true } });
+
+ function addBreakpoint() {
+ return actions.addBreakpoint({
+ actor: gSources.selectedValue,
+ line: 21
+ });
+ }
+
+ function pauseDebuggee() {
+ generateMouseClickInTab(gTab, "content.document.querySelector('button')");
+
+ // The first 'with' scope should be expanded by default, but the
+ // variables haven't been fetched yet. This is how 'with' scopes work.
+ return promise.all([
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES),
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_VARIABLES)
+ ]);
+ }
+
+ function stepInDebuggee() {
+ // Spin the event loop before causing the debuggee to pause, to allow
+ // this function to return first.
+ executeSoon(() => {
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ gDebugger.document.querySelector("#step-in"),
+ gDebugger);
+ });
+
+ return promise.all([
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES, 1),
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_VARIABLES, 3),
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_PROPERTIES, 1),
+ ]);
+ }
+
+ function testVariablesExpand() {
+ let localScope = gVariables.getScopeAtIndex(0);
+ let withScope = gVariables.getScopeAtIndex(1);
+ let functionScope = gVariables.getScopeAtIndex(2);
+ let globalLexicalScope = gVariables.getScopeAtIndex(3);
+ let globalScope = gVariables.getScopeAtIndex(4);
+
+ let thisVar = localScope.get("this");
+ let windowVar = thisVar.get("window");
+
+ is(localScope.target.querySelector(".arrow").hasAttribute("open"), true,
+ "The localScope arrow should still be expanded.");
+ is(withScope.target.querySelector(".arrow").hasAttribute("open"), true,
+ "The withScope arrow should still be expanded.");
+ is(functionScope.target.querySelector(".arrow").hasAttribute("open"), true,
+ "The functionScope arrow should still be expanded.");
+ is(globalLexicalScope.target.querySelector(".arrow").hasAttribute("open"), true,
+ "The globalLexicalScope arrow should still be expanded.");
+ is(globalScope.target.querySelector(".arrow").hasAttribute("open"), true,
+ "The globalScope arrow should still be expanded.");
+ is(thisVar.target.querySelector(".arrow").hasAttribute("open"), true,
+ "The thisVar arrow should still be expanded.");
+ is(windowVar.target.querySelector(".arrow").hasAttribute("open"), false,
+ "The windowVar arrow should not be expanded.");
+
+ is(localScope.target.querySelector(".variables-view-element-details").hasAttribute("open"), true,
+ "The localScope enumerables should still be expanded.");
+ is(withScope.target.querySelector(".variables-view-element-details").hasAttribute("open"), true,
+ "The withScope enumerables should still be expanded.");
+ is(functionScope.target.querySelector(".variables-view-element-details").hasAttribute("open"), true,
+ "The functionScope enumerables should still be expanded.");
+ is(globalLexicalScope.target.querySelector(".variables-view-element-details").hasAttribute("open"), true,
+ "The globalLexicalScope enumerables should still be expanded.");
+ is(globalScope.target.querySelector(".variables-view-element-details").hasAttribute("open"), true,
+ "The globalScope enumerables should still be expanded.");
+ is(thisVar.target.querySelector(".variables-view-element-details").hasAttribute("open"), true,
+ "The thisVar enumerables should still be expanded.");
+ is(windowVar.target.querySelector(".variables-view-element-details").hasAttribute("open"), false,
+ "The windowVar enumerables should not be expanded.");
+
+ is(localScope.expanded, true,
+ "The localScope expanded getter should return true.");
+ is(withScope.expanded, true,
+ "The withScope expanded getter should return true.");
+ is(functionScope.expanded, true,
+ "The functionScope expanded getter should return true.");
+ is(globalLexicalScope.expanded, true,
+ "The globalScope expanded getter should return true.");
+ is(globalScope.expanded, true,
+ "The globalScope expanded getter should return true.");
+ is(thisVar.expanded, true,
+ "The thisVar expanded getter should return true.");
+ is(windowVar.expanded, false,
+ "The windowVar expanded getter should return true.");
+ }
+
+ function prepareVariablesAndProperties() {
+ let deferred = promise.defer();
+
+ let localScope = gVariables.getScopeAtIndex(0);
+ let withScope = gVariables.getScopeAtIndex(1);
+ let functionScope = gVariables.getScopeAtIndex(2);
+ let globalLexicalScope = gVariables.getScopeAtIndex(3);
+ let globalScope = gVariables.getScopeAtIndex(4);
+
+ is(localScope.expanded, true,
+ "The localScope should be expanded.");
+ is(withScope.expanded, false,
+ "The withScope should not be expanded yet.");
+ is(functionScope.expanded, false,
+ "The functionScope should not be expanded yet.");
+ is(globalLexicalScope.expanded, false,
+ "The globalLexicalScope should not be expanded yet.");
+ is(globalScope.expanded, false,
+ "The globalScope should not be expanded yet.");
+
+ // Wait for only two events to be triggered, because the Function scope is
+ // an environment to which scope arguments and variables are already attached.
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_VARIABLES, 2).then(() => {
+ is(localScope.expanded, true,
+ "The localScope should now be expanded.");
+ is(withScope.expanded, true,
+ "The withScope should now be expanded.");
+ is(functionScope.expanded, true,
+ "The functionScope should now be expanded.");
+ is(globalLexicalScope.expanded, true,
+ "The globalLexicalScope should now be expanded.");
+ is(globalScope.expanded, true,
+ "The globalScope should now be expanded.");
+
+ let thisVar = localScope.get("this");
+
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_PROPERTIES, 1).then(() => {
+ let windowVar = thisVar.get("window");
+
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_PROPERTIES, 1).then(() => {
+ let documentVar = windowVar.get("document");
+
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_PROPERTIES, 1).then(() => {
+ let locationVar = documentVar.get("location");
+
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_PROPERTIES, 1).then(() => {
+ is(thisVar.expanded, true,
+ "The local scope 'this' should be expanded.");
+ is(windowVar.expanded, true,
+ "The local scope 'this.window' should be expanded.");
+ is(documentVar.expanded, true,
+ "The local scope 'this.window.document' should be expanded.");
+ is(locationVar.expanded, true,
+ "The local scope 'this.window.document.location' should be expanded.");
+
+ deferred.resolve();
+ });
+
+ locationVar.expand();
+ });
+
+ documentVar.expand();
+ });
+
+ windowVar.expand();
+ });
+
+ thisVar.expand();
+ });
+
+ withScope.expand();
+ functionScope.expand();
+ globalLexicalScope.expand();
+ globalScope.expand();
+
+ return deferred.promise;
+ }
+
+ Task.spawn(function* () {
+ yield addBreakpoint();
+ yield ensureThreadClientState(gPanel, "resumed");
+ yield pauseDebuggee();
+ yield prepareVariablesAndProperties();
+ yield stepInDebuggee();
+ yield testVariablesExpand();
+ resumeDebuggerThenCloseAndFinish(gPanel);
+ });
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-reexpand-02.js b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-reexpand-02.js
new file mode 100644
index 000000000..e292c6804
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-reexpand-02.js
@@ -0,0 +1,226 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure that the variables view correctly re-expands nodes after pauses,
+ * with the caveat that there are no ignored items in the hierarchy.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_with-frame.html";
+
+function test() {
+ // Debug test slaves are a bit slow at this test.
+ requestLongerTimeout(4);
+
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ const gTab = aTab;
+ const gPanel = aPanel;
+ const gDebugger = gPanel.panelWin;
+ const gSources = gDebugger.DebuggerView.Sources;
+ const gVariables = gDebugger.DebuggerView.Variables;
+ const queries = gDebugger.require("./content/queries");
+ const getState = gDebugger.DebuggerController.getState;
+ const actions = bindActionCreators(gPanel);
+
+ // Always expand all items between pauses.
+ gVariables.commitHierarchyIgnoredItems = Object.create(null);
+
+ function addBreakpoint() {
+ return actions.addBreakpoint({
+ actor: gSources.selectedValue,
+ line: 21
+ });
+ }
+
+ function pauseDebuggee() {
+ generateMouseClickInTab(gTab, "content.document.querySelector('button')");
+
+ // The first 'with' scope should be expanded by default, but the
+ // variables haven't been fetched yet. This is how 'with' scopes work.
+ return promise.all([
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES),
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_VARIABLES)
+ ]);
+ }
+
+ function stepInDebuggee() {
+ // Spin the event loop before causing the debuggee to pause, to allow
+ // this function to return first.
+ executeSoon(() => {
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ gDebugger.document.querySelector("#step-in"),
+ gDebugger);
+ });
+
+ return promise.all([
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES, 1),
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_VARIABLES, 3),
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_PROPERTIES, 4),
+ ]);
+ }
+
+ function testVariablesExpand() {
+ let localScope = gVariables.getScopeAtIndex(0);
+ let withScope = gVariables.getScopeAtIndex(1);
+ let functionScope = gVariables.getScopeAtIndex(2);
+ let globalLexicalScope = gVariables.getScopeAtIndex(3);
+ let globalScope = gVariables.getScopeAtIndex(4);
+
+ let thisVar = localScope.get("this");
+ let windowVar = thisVar.get("window");
+ let documentVar = windowVar.get("document");
+ let locationVar = documentVar.get("location");
+
+ is(localScope.target.querySelector(".arrow").hasAttribute("open"), true,
+ "The localScope arrow should still be expanded.");
+ is(withScope.target.querySelector(".arrow").hasAttribute("open"), true,
+ "The withScope arrow should still be expanded.");
+ is(functionScope.target.querySelector(".arrow").hasAttribute("open"), true,
+ "The functionScope arrow should still be expanded.");
+ is(globalLexicalScope.target.querySelector(".arrow").hasAttribute("open"), true,
+ "The globalLexicalScope arrow should still be expanded.");
+ is(globalScope.target.querySelector(".arrow").hasAttribute("open"), true,
+ "The globalScope arrow should still be expanded.");
+ is(thisVar.target.querySelector(".arrow").hasAttribute("open"), true,
+ "The thisVar arrow should still be expanded.");
+ is(windowVar.target.querySelector(".arrow").hasAttribute("open"), true,
+ "The windowVar arrow should still be expanded.");
+ is(documentVar.target.querySelector(".arrow").hasAttribute("open"), true,
+ "The documentVar arrow should still be expanded.");
+ is(locationVar.target.querySelector(".arrow").hasAttribute("open"), true,
+ "The locationVar arrow should still be expanded.");
+
+ is(localScope.target.querySelector(".variables-view-element-details").hasAttribute("open"), true,
+ "The localScope enumerables should still be expanded.");
+ is(withScope.target.querySelector(".variables-view-element-details").hasAttribute("open"), true,
+ "The withScope enumerables should still be expanded.");
+ is(functionScope.target.querySelector(".variables-view-element-details").hasAttribute("open"), true,
+ "The functionScope enumerables should still be expanded.");
+ is(globalLexicalScope.target.querySelector(".variables-view-element-details").hasAttribute("open"), true,
+ "The globalLexicalScope enumerables should still be expanded.");
+ is(globalScope.target.querySelector(".variables-view-element-details").hasAttribute("open"), true,
+ "The globalScope enumerables should still be expanded.");
+ is(thisVar.target.querySelector(".variables-view-element-details").hasAttribute("open"), true,
+ "The thisVar enumerables should still be expanded.");
+ is(windowVar.target.querySelector(".variables-view-element-details").hasAttribute("open"), true,
+ "The windowVar enumerables should still be expanded.");
+ is(documentVar.target.querySelector(".variables-view-element-details").hasAttribute("open"), true,
+ "The documentVar enumerables should still be expanded.");
+ is(locationVar.target.querySelector(".variables-view-element-details").hasAttribute("open"), true,
+ "The locationVar enumerables should still be expanded.");
+
+ is(localScope.expanded, true,
+ "The localScope expanded getter should return true.");
+ is(withScope.expanded, true,
+ "The withScope expanded getter should return true.");
+ is(functionScope.expanded, true,
+ "The functionScope expanded getter should return true.");
+ is(globalLexicalScope.expanded, true,
+ "The globalLexicalScope expanded getter should return true.");
+ is(globalScope.expanded, true,
+ "The globalScope expanded getter should return true.");
+ is(thisVar.expanded, true,
+ "The thisVar expanded getter should return true.");
+ is(windowVar.expanded, true,
+ "The windowVar expanded getter should return true.");
+ is(documentVar.expanded, true,
+ "The documentVar expanded getter should return true.");
+ is(locationVar.expanded, true,
+ "The locationVar expanded getter should return true.");
+ }
+
+ function prepareVariablesAndProperties() {
+ let deferred = promise.defer();
+
+ let localScope = gVariables.getScopeAtIndex(0);
+ let withScope = gVariables.getScopeAtIndex(1);
+ let functionScope = gVariables.getScopeAtIndex(2);
+ let globalLexicalScope = gVariables.getScopeAtIndex(3);
+ let globalScope = gVariables.getScopeAtIndex(4);
+
+ is(localScope.expanded, true,
+ "The localScope should be expanded.");
+ is(withScope.expanded, false,
+ "The withScope should not be expanded yet.");
+ is(functionScope.expanded, false,
+ "The functionScope should not be expanded yet.");
+ is(globalLexicalScope.expanded, false,
+ "The globalLexicalScope should not be expanded yet.");
+ is(globalScope.expanded, false,
+ "The globalScope should not be expanded yet.");
+
+ // Wait for only two events to be triggered, because the Function scope is
+ // an environment to which scope arguments and variables are already attached.
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_VARIABLES, 2).then(() => {
+ is(localScope.expanded, true,
+ "The localScope should now be expanded.");
+ is(withScope.expanded, true,
+ "The withScope should now be expanded.");
+ is(functionScope.expanded, true,
+ "The functionScope should now be expanded.");
+ is(globalLexicalScope.expanded, true,
+ "The globalLexicalScope should now be expanded.");
+ is(globalScope.expanded, true,
+ "The globalScope should now be expanded.");
+
+ let thisVar = localScope.get("this");
+
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_PROPERTIES, 1).then(() => {
+ let windowVar = thisVar.get("window");
+
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_PROPERTIES, 1).then(() => {
+ let documentVar = windowVar.get("document");
+
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_PROPERTIES, 1).then(() => {
+ let locationVar = documentVar.get("location");
+
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_PROPERTIES, 1).then(() => {
+ is(thisVar.expanded, true,
+ "The local scope 'this' should be expanded.");
+ is(windowVar.expanded, true,
+ "The local scope 'this.window' should be expanded.");
+ is(documentVar.expanded, true,
+ "The local scope 'this.window.document' should be expanded.");
+ is(locationVar.expanded, true,
+ "The local scope 'this.window.document.location' should be expanded.");
+
+ deferred.resolve();
+ });
+
+ locationVar.expand();
+ });
+
+ documentVar.expand();
+ });
+
+ windowVar.expand();
+ });
+
+ thisVar.expand();
+ });
+
+ withScope.expand();
+ functionScope.expand();
+ globalLexicalScope.expand();
+ globalScope.expand();
+
+ return deferred.promise;
+ }
+
+ Task.spawn(function* () {
+ yield addBreakpoint();
+ yield ensureThreadClientState(gPanel, "resumed");
+ yield pauseDebuggee();
+ yield prepareVariablesAndProperties();
+ yield stepInDebuggee();
+ yield testVariablesExpand();
+ resumeDebuggerThenCloseAndFinish(gPanel);
+ });
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-reexpand-03.js b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-reexpand-03.js
new file mode 100644
index 000000000..258fbed26
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-reexpand-03.js
@@ -0,0 +1,120 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure that the variables view correctly re-expands *scopes* after pauses.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_scope-variable-4.html";
+
+function test() {
+ // Debug test slaves are a bit slow at this test.
+ requestLongerTimeout(4);
+
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ const gTab = aTab;
+ const gPanel = aPanel;
+ const gDebugger = gPanel.panelWin;
+ const gSources = gDebugger.DebuggerView.Sources;
+ const gVariables = gDebugger.DebuggerView.Variables;
+ const queries = gDebugger.require("./content/queries");
+ const getState = gDebugger.DebuggerController.getState;
+ const actions = bindActionCreators(gPanel);
+
+ // Always expand all items between pauses.
+ gVariables.commitHierarchyIgnoredItems = Object.create(null);
+
+ function addBreakpoint() {
+ return actions.addBreakpoint({
+ actor: gSources.selectedValue,
+ line: 18
+ });
+ }
+
+ function pauseDebuggee() {
+ callInTab(gTab, "test");
+
+ return waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES);
+ }
+
+ function resumeDebuggee() {
+ // Spin the event loop before causing the debuggee to pause, to allow
+ // this function to return first.
+ executeSoon(() => {
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ gDebugger.document.querySelector("#resume"),
+ gDebugger);
+ });
+
+ return waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES);
+ }
+
+ function testVariablesExpand() {
+ let localScope = gVariables.getScopeAtIndex(0);
+ let functionScope = gVariables.getScopeAtIndex(1);
+ let globalScope = gVariables.getScopeAtIndex(2);
+
+ is(localScope.target.querySelector(".arrow").hasAttribute("open"), true,
+ "The localScope arrow should still be expanded.");
+ is(functionScope.target.querySelector(".arrow").hasAttribute("open"), true,
+ "The functionScope arrow should still be expanded.");
+ is(globalScope.target.querySelector(".arrow").hasAttribute("open"), false,
+ "The globalScope arrow should not be expanded.");
+
+ is(localScope.target.querySelector(".variables-view-element-details").hasAttribute("open"), true,
+ "The localScope enumerables should still be expanded.");
+ is(functionScope.target.querySelector(".variables-view-element-details").hasAttribute("open"), true,
+ "The functionScope enumerables should still be expanded.");
+ is(globalScope.target.querySelector(".variables-view-element-details").hasAttribute("open"), false,
+ "The globalScope enumerables should not be expanded.");
+
+ is(localScope.expanded, true,
+ "The localScope expanded getter should return true.");
+ is(functionScope.expanded, true,
+ "The functionScope expanded getter should return true.");
+ is(globalScope.expanded, false,
+ "The globalScope expanded getter should return false.");
+ }
+
+ function prepareScopes() {
+ let localScope = gVariables.getScopeAtIndex(0);
+ let functionScope = gVariables.getScopeAtIndex(1);
+ let globalScope = gVariables.getScopeAtIndex(2);
+
+ is(localScope.expanded, true,
+ "The localScope should be expanded.");
+ is(functionScope.expanded, false,
+ "The functionScope should not be expanded yet.");
+ is(globalScope.expanded, false,
+ "The globalScope should not be expanded yet.");
+
+ localScope.collapse();
+ functionScope.expand();
+
+ // Don't for any events to be triggered, because the Function scope is
+ // an environment to which scope arguments and variables are already attached.
+ is(localScope.expanded, false,
+ "The localScope should not be expanded anymore.");
+ is(functionScope.expanded, true,
+ "The functionScope should now be expanded.");
+ is(globalScope.expanded, false,
+ "The globalScope should still not be expanded.");
+ }
+
+ Task.spawn(function* () {
+ yield addBreakpoint();
+ yield ensureThreadClientState(gPanel, "resumed");
+ yield pauseDebuggee();
+ yield prepareScopes();
+ yield resumeDebuggee();
+ yield testVariablesExpand();
+ resumeDebuggerThenCloseAndFinish(gPanel);
+ });
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-webidl.js b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-webidl.js
new file mode 100644
index 000000000..4499ec18f
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-webidl.js
@@ -0,0 +1,262 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Make sure that the variables view correctly displays WebIDL attributes in DOM
+ * objects.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_frame-parameters.html";
+
+var gTab, gPanel, gDebugger;
+var gVariables;
+
+function test() {
+ // Debug test slaves are a bit slow at this test.
+ requestLongerTimeout(2);
+
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gVariables = gDebugger.DebuggerView.Variables;
+
+ waitForCaretAndScopes(gPanel, 24)
+ .then(expandGlobalScope)
+ .then(performTest)
+ .then(() => resumeDebuggerThenCloseAndFinish(gPanel))
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+
+ generateMouseClickInTab(gTab, "content.document.querySelector('button')");
+ });
+}
+
+function expandGlobalScope() {
+ let deferred = promise.defer();
+
+ let globalScope = gVariables.getScopeAtIndex(2);
+ is(globalScope.expanded, false,
+ "The global scope should not be expanded by default.");
+
+ gDebugger.once(gDebugger.EVENTS.FETCHED_VARIABLES, deferred.resolve);
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ globalScope.target.querySelector(".name"),
+ gDebugger);
+
+ return deferred.promise;
+}
+
+function performTest() {
+ let deferred = promise.defer();
+ let globalScope = gVariables.getScopeAtIndex(2);
+
+ let buttonVar = globalScope.get("button");
+ let buttonAsProtoVar = globalScope.get("buttonAsProto");
+ let documentVar = globalScope.get("document");
+
+ is(buttonVar.target.querySelector(".name").getAttribute("value"), "button",
+ "Should have the right property name for 'button'.");
+ is(buttonVar.target.querySelector(".value").getAttribute("value"), "<button>",
+ "Should have the right property value for 'button'.");
+ ok(buttonVar.target.querySelector(".value").className.includes("token-domnode"),
+ "Should have the right token class for 'button'.");
+
+ is(buttonAsProtoVar.target.querySelector(".name").getAttribute("value"), "buttonAsProto",
+ "Should have the right property name for 'buttonAsProto'.");
+ is(buttonAsProtoVar.target.querySelector(".value").getAttribute("value"), "Object",
+ "Should have the right property value for 'buttonAsProto'.");
+ ok(buttonAsProtoVar.target.querySelector(".value").className.includes("token-other"),
+ "Should have the right token class for 'buttonAsProto'.");
+
+ is(documentVar.target.querySelector(".name").getAttribute("value"), "document",
+ "Should have the right property name for 'document'.");
+ is(documentVar.target.querySelector(".value").getAttribute("value"),
+ "HTMLDocument \u2192 doc_frame-parameters.html",
+ "Should have the right property value for 'document'.");
+ ok(documentVar.target.querySelector(".value").className.includes("token-domnode"),
+ "Should have the right token class for 'document'.");
+
+ is(buttonVar.expanded, false,
+ "The buttonVar should not be expanded at this point.");
+ is(buttonAsProtoVar.expanded, false,
+ "The buttonAsProtoVar should not be expanded at this point.");
+ is(documentVar.expanded, false,
+ "The documentVar should not be expanded at this point.");
+
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_PROPERTIES, 3).then(() => {
+ is(buttonVar.get("type").target.querySelector(".name").getAttribute("value"), "type",
+ "Should have the right property name for 'type'.");
+ is(buttonVar.get("type").target.querySelector(".value").getAttribute("value"), "\"submit\"",
+ "Should have the right property value for 'type'.");
+ ok(buttonVar.get("type").target.querySelector(".value").className.includes("token-string"),
+ "Should have the right token class for 'type'.");
+
+ is(buttonVar.get("childNodes").target.querySelector(".name").getAttribute("value"), "childNodes",
+ "Should have the right property name for 'childNodes'.");
+ is(buttonVar.get("childNodes").target.querySelector(".value").getAttribute("value"), "NodeList[1]",
+ "Should have the right property value for 'childNodes'.");
+ ok(buttonVar.get("childNodes").target.querySelector(".value").className.includes("token-other"),
+ "Should have the right token class for 'childNodes'.");
+
+ is(buttonVar.get("onclick").target.querySelector(".name").getAttribute("value"), "onclick",
+ "Should have the right property name for 'onclick'.");
+ is(buttonVar.get("onclick").target.querySelector(".value").getAttribute("value"), "onclick(event)",
+ "Should have the right property value for 'onclick'.");
+ ok(buttonVar.get("onclick").target.querySelector(".value").className.includes("token-other"),
+ "Should have the right token class for 'onclick'.");
+
+ is(documentVar.get("title").target.querySelector(".name").getAttribute("value"), "title",
+ "Should have the right property name for 'title'.");
+ is(documentVar.get("title").target.querySelector(".value").getAttribute("value"), "\"Debugger test page\"",
+ "Should have the right property value for 'title'.");
+ ok(documentVar.get("title").target.querySelector(".value").className.includes("token-string"),
+ "Should have the right token class for 'title'.");
+
+ is(documentVar.get("childNodes").target.querySelector(".name").getAttribute("value"), "childNodes",
+ "Should have the right property name for 'childNodes'.");
+ is(documentVar.get("childNodes").target.querySelector(".value").getAttribute("value"), "NodeList[3]",
+ "Should have the right property value for 'childNodes'.");
+ ok(documentVar.get("childNodes").target.querySelector(".value").className.includes("token-other"),
+ "Should have the right token class for 'childNodes'.");
+
+ is(documentVar.get("onclick").target.querySelector(".name").getAttribute("value"), "onclick",
+ "Should have the right property name for 'onclick'.");
+ is(documentVar.get("onclick").target.querySelector(".value").getAttribute("value"), "null",
+ "Should have the right property value for 'onclick'.");
+ ok(documentVar.get("onclick").target.querySelector(".value").className.includes("token-null"),
+ "Should have the right token class for 'onclick'.");
+
+ let buttonProtoVar = buttonVar.get("__proto__");
+ let buttonAsProtoProtoVar = buttonAsProtoVar.get("__proto__");
+ let documentProtoVar = documentVar.get("__proto__");
+
+ is(buttonProtoVar.target.querySelector(".name").getAttribute("value"), "__proto__",
+ "Should have the right property name for '__proto__'.");
+ is(buttonProtoVar.target.querySelector(".value").getAttribute("value"), "HTMLButtonElementPrototype",
+ "Should have the right property value for '__proto__'.");
+ ok(buttonProtoVar.target.querySelector(".value").className.includes("token-other"),
+ "Should have the right token class for '__proto__'.");
+
+ is(buttonAsProtoProtoVar.target.querySelector(".name").getAttribute("value"), "__proto__",
+ "Should have the right property name for '__proto__'.");
+ is(buttonAsProtoProtoVar.target.querySelector(".value").getAttribute("value"), "<button>",
+ "Should have the right property value for '__proto__'.");
+ ok(buttonAsProtoProtoVar.target.querySelector(".value").className.includes("token-domnode"),
+ "Should have the right token class for '__proto__'.");
+
+ is(documentProtoVar.target.querySelector(".name").getAttribute("value"), "__proto__",
+ "Should have the right property name for '__proto__'.");
+ is(documentProtoVar.target.querySelector(".value").getAttribute("value"), "HTMLDocumentPrototype",
+ "Should have the right property value for '__proto__'.");
+ ok(documentProtoVar.target.querySelector(".value").className.includes("token-other"),
+ "Should have the right token class for '__proto__'.");
+
+ is(buttonProtoVar.expanded, false,
+ "The buttonProtoVar should not be expanded at this point.");
+ is(buttonAsProtoProtoVar.expanded, false,
+ "The buttonAsProtoProtoVar should not be expanded at this point.");
+ is(documentProtoVar.expanded, false,
+ "The documentProtoVar should not be expanded at this point.");
+
+ waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_PROPERTIES, 3).then(() => {
+ is(buttonAsProtoProtoVar.get("type").target.querySelector(".name").getAttribute("value"), "type",
+ "Should have the right property name for 'type'.");
+ is(buttonAsProtoProtoVar.get("type").target.querySelector(".value").getAttribute("value"), "\"submit\"",
+ "Should have the right property value for 'type'.");
+ ok(buttonAsProtoProtoVar.get("type").target.querySelector(".value").className.includes("token-string"),
+ "Should have the right token class for 'type'.");
+
+ is(buttonAsProtoProtoVar.get("childNodes").target.querySelector(".name").getAttribute("value"), "childNodes",
+ "Should have the right property name for 'childNodes'.");
+ is(buttonAsProtoProtoVar.get("childNodes").target.querySelector(".value").getAttribute("value"), "NodeList[1]",
+ "Should have the right property value for 'childNodes'.");
+ ok(buttonAsProtoProtoVar.get("childNodes").target.querySelector(".value").className.includes("token-other"),
+ "Should have the right token class for 'childNodes'.");
+
+ is(buttonAsProtoProtoVar.get("onclick").target.querySelector(".name").getAttribute("value"), "onclick",
+ "Should have the right property name for 'onclick'.");
+ is(buttonAsProtoProtoVar.get("onclick").target.querySelector(".value").getAttribute("value"), "onclick(event)",
+ "Should have the right property value for 'onclick'.");
+ ok(buttonAsProtoProtoVar.get("onclick").target.querySelector(".value").className.includes("token-other"),
+ "Should have the right token class for 'onclick'.");
+
+ let buttonProtoProtoVar = buttonProtoVar.get("__proto__");
+ let buttonAsProtoProtoProtoVar = buttonAsProtoProtoVar.get("__proto__");
+ let documentProtoProtoVar = documentProtoVar.get("__proto__");
+
+ is(buttonProtoProtoVar.target.querySelector(".name").getAttribute("value"), "__proto__",
+ "Should have the right property name for '__proto__'.");
+ is(buttonProtoProtoVar.target.querySelector(".value").getAttribute("value"), "HTMLElementPrototype",
+ "Should have the right property value for '__proto__'.");
+ ok(buttonProtoProtoVar.target.querySelector(".value").className.includes("token-other"),
+ "Should have the right token class for '__proto__'.");
+
+ is(buttonAsProtoProtoProtoVar.target.querySelector(".name").getAttribute("value"), "__proto__",
+ "Should have the right property name for '__proto__'.");
+ is(buttonAsProtoProtoProtoVar.target.querySelector(".value").getAttribute("value"), "HTMLButtonElementPrototype",
+ "Should have the right property value for '__proto__'.");
+ ok(buttonAsProtoProtoProtoVar.target.querySelector(".value").className.includes("token-other"),
+ "Should have the right token class for '__proto__'.");
+
+ is(documentProtoProtoVar.target.querySelector(".name").getAttribute("value"), "__proto__",
+ "Should have the right property name for '__proto__'.");
+ is(documentProtoProtoVar.target.querySelector(".value").getAttribute("value"), "DocumentPrototype",
+ "Should have the right property value for '__proto__'.");
+ ok(documentProtoProtoVar.target.querySelector(".value").className.includes("token-other"),
+ "Should have the right token class for '__proto__'.");
+
+ is(buttonAsProtoProtoProtoVar.expanded, false,
+ "The buttonAsProtoProtoProtoVar should not be expanded at this point.");
+ is(buttonAsProtoProtoProtoVar.expanded, false,
+ "The buttonAsProtoProtoProtoVar should not be expanded at this point.");
+ is(documentProtoProtoVar.expanded, false,
+ "The documentProtoProtoVar should not be expanded at this point.");
+
+ deferred.resolve();
+ });
+
+ // Similarly, expand the 'button.__proto__', 'buttonAsProto.__proto__' and
+ // 'document.__proto__' variables view nodes.
+ buttonProtoVar.expand();
+ buttonAsProtoProtoVar.expand();
+ documentProtoVar.expand();
+
+ is(buttonProtoVar.expanded, true,
+ "The buttonProtoVar should be immediately marked as expanded.");
+ is(buttonAsProtoProtoVar.expanded, true,
+ "The buttonAsProtoProtoVar should be immediately marked as expanded.");
+ is(documentProtoVar.expanded, true,
+ "The documentProtoVar should be immediately marked as expanded.");
+ });
+
+ // Expand the 'button', 'buttonAsProto' and 'document' variables view nodes.
+ // This causes their properties to be retrieved and displayed.
+ buttonVar.expand();
+ buttonAsProtoVar.expand();
+ documentVar.expand();
+
+ is(buttonVar.expanded, true,
+ "The buttonVar should be immediately marked as expanded.");
+ is(buttonAsProtoVar.expanded, true,
+ "The buttonAsProtoVar should be immediately marked as expanded.");
+ is(documentVar.expanded, true,
+ "The documentVar should be immediately marked as expanded.");
+
+ return deferred.promise;
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gVariables = null;
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_watch-expressions-01.js b/devtools/client/debugger/test/mochitest/browser_dbg_watch-expressions-01.js
new file mode 100644
index 000000000..fe55a5561
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_watch-expressions-01.js
@@ -0,0 +1,227 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Bug 727429: Test the debugger watch expressions.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_watch-expressions.html";
+
+function test() {
+ // Debug test slaves are a bit slow at this test.
+ requestLongerTimeout(2);
+
+ let gTab, gPanel, gDebugger;
+ let gEditor, gWatch, gVariables;
+
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gEditor = gDebugger.DebuggerView.editor;
+ gWatch = gDebugger.DebuggerView.WatchExpressions;
+ gVariables = gDebugger.DebuggerView.Variables;
+
+ gDebugger.DebuggerView.toggleInstrumentsPane({ visible: true, animated: false });
+
+ performTest();
+ closeDebuggerAndFinish(gPanel);
+ });
+
+ function performTest() {
+ is(gWatch.getAllStrings().length, 0,
+ "There should initially be no watch expressions.");
+
+ addAndCheckExpressions(1, 0, "a");
+ addAndCheckExpressions(2, 0, "b");
+ addAndCheckExpressions(3, 0, "c");
+
+ removeAndCheckExpression(2, 1, "a");
+ removeAndCheckExpression(1, 0, "a");
+
+ addAndCheckExpressions(2, 0, "", true);
+ gEditor.focus();
+ is(gWatch.getAllStrings().length, 1,
+ "Empty watch expressions are automatically removed.");
+
+ addAndCheckExpressions(2, 0, "a", true);
+ gEditor.focus();
+ is(gWatch.getAllStrings().length, 1,
+ "Duplicate watch expressions are automatically removed.");
+
+ addAndCheckExpressions(2, 0, "a\t", true);
+ addAndCheckExpressions(2, 0, "a\r", true);
+ addAndCheckExpressions(2, 0, "a\n", true);
+ gEditor.focus();
+ is(gWatch.getAllStrings().length, 1,
+ "Duplicate watch expressions are automatically removed.");
+
+ addAndCheckExpressions(2, 0, "\ta", true);
+ addAndCheckExpressions(2, 0, "\ra", true);
+ addAndCheckExpressions(2, 0, "\na", true);
+ gEditor.focus();
+ is(gWatch.getAllStrings().length, 1,
+ "Duplicate watch expressions are automatically removed.");
+
+ addAndCheckCustomExpression(2, 0, "bazΩΩka");
+ addAndCheckCustomExpression(3, 0, "bambøøcha");
+
+ EventUtils.sendMouseEvent({ type: "click" },
+ gWatch.getItemAtIndex(0).attachment.view.closeNode,
+ gDebugger);
+
+ is(gWatch.getAllStrings().length, 2,
+ "Watch expressions are removed when the close button is pressed.");
+ is(gWatch.getAllStrings()[0], "bazΩΩka",
+ "The expression at index " + 0 + " should be correct (1).");
+ is(gWatch.getAllStrings()[1], "a",
+ "The expression at index " + 1 + " should be correct (2).");
+
+ EventUtils.sendMouseEvent({ type: "click" },
+ gWatch.getItemAtIndex(0).attachment.view.closeNode,
+ gDebugger);
+
+ is(gWatch.getAllStrings().length, 1,
+ "Watch expressions are removed when the close button is pressed.");
+ is(gWatch.getAllStrings()[0], "a",
+ "The expression at index " + 0 + " should be correct (3).");
+
+ EventUtils.sendMouseEvent({ type: "click" },
+ gWatch.getItemAtIndex(0).attachment.view.closeNode,
+ gDebugger);
+
+ is(gWatch.getAllStrings().length, 0,
+ "Watch expressions are removed when the close button is pressed.");
+
+ EventUtils.sendMouseEvent({ type: "click" },
+ gWatch.widget._parent,
+ gDebugger);
+
+ is(gWatch.getAllStrings().length, 1,
+ "Watch expressions are added when the view container is pressed.");
+ }
+
+ function addAndCheckCustomExpression(aTotal, aIndex, aString, noBlur) {
+ addAndCheckExpressions(aTotal, aIndex, "", true);
+
+ EventUtils.sendString(aString, gDebugger);
+
+ gEditor.focus();
+
+ let element = gWatch.getItemAtIndex(aIndex).target;
+
+ is(gWatch.getItemAtIndex(aIndex).attachment.initialExpression, "",
+ "The initial expression at index " + aIndex + " should be correct (1).");
+ is(gWatch.getItemForElement(element).attachment.initialExpression, "",
+ "The initial expression at index " + aIndex + " should be correct (2).");
+
+ is(gWatch.getItemAtIndex(aIndex).attachment.currentExpression, aString,
+ "The expression at index " + aIndex + " should be correct (1).");
+ is(gWatch.getItemForElement(element).attachment.currentExpression, aString,
+ "The expression at index " + aIndex + " should be correct (2).");
+
+ is(gWatch.getString(aIndex), aString,
+ "The expression at index " + aIndex + " should be correct (3).");
+ is(gWatch.getAllStrings()[aIndex], aString,
+ "The expression at index " + aIndex + " should be correct (4).");
+ }
+
+ function addAndCheckExpressions(aTotal, aIndex, aString, noBlur) {
+ gWatch.addExpression(aString);
+
+ is(gWatch.getAllStrings().length, aTotal,
+ "There should be " + aTotal + " watch expressions available (1).");
+ is(gWatch.itemCount, aTotal,
+ "There should be " + aTotal + " watch expressions available (2).");
+
+ ok(gWatch.getItemAtIndex(aIndex),
+ "The expression at index " + aIndex + " should be available.");
+ is(gWatch.getItemAtIndex(aIndex).attachment.initialExpression, aString,
+ "The expression at index " + aIndex + " should have an initial expression.");
+
+ let element = gWatch.getItemAtIndex(aIndex).target;
+
+ ok(element,
+ "There should be a new expression item in the view.");
+ ok(gWatch.getItemForElement(element),
+ "The watch expression item should be accessible.");
+ is(gWatch.getItemForElement(element), gWatch.getItemAtIndex(aIndex),
+ "The correct watch expression item was accessed.");
+
+ ok(gWatch.widget.getItemAtIndex(aIndex) instanceof XULElement,
+ "The correct watch expression element was accessed (1).");
+ is(element, gWatch.widget.getItemAtIndex(aIndex),
+ "The correct watch expression element was accessed (2).");
+
+ is(gWatch.getItemForElement(element).attachment.view.arrowNode.hidden, false,
+ "The arrow node should be visible.");
+ is(gWatch.getItemForElement(element).attachment.view.closeNode.hidden, false,
+ "The close button should be visible.");
+ is(gWatch.getItemForElement(element).attachment.view.inputNode.getAttribute("focused"), "true",
+ "The textbox input should be focused.");
+
+ is(gVariables.parentNode.scrollTop, 0,
+ "The variables view should be scrolled to top");
+
+ is(gWatch.items[0], gWatch.getItemAtIndex(aIndex),
+ "The correct watch expression was added to the cache (1).");
+ is(gWatch.items[0], gWatch.getItemForElement(element),
+ "The correct watch expression was added to the cache (2).");
+
+ if (!noBlur) {
+ gEditor.focus();
+
+ is(gWatch.getItemAtIndex(aIndex).attachment.initialExpression, aString,
+ "The initial expression at index " + aIndex + " should be correct (1).");
+ is(gWatch.getItemForElement(element).attachment.initialExpression, aString,
+ "The initial expression at index " + aIndex + " should be correct (2).");
+
+ is(gWatch.getItemAtIndex(aIndex).attachment.currentExpression, aString,
+ "The expression at index " + aIndex + " should be correct (1).");
+ is(gWatch.getItemForElement(element).attachment.currentExpression, aString,
+ "The expression at index " + aIndex + " should be correct (2).");
+
+ is(gWatch.getString(aIndex), aString,
+ "The expression at index " + aIndex + " should be correct (3).");
+ is(gWatch.getAllStrings()[aIndex], aString,
+ "The expression at index " + aIndex + " should be correct (4).");
+ }
+ }
+
+ function removeAndCheckExpression(aTotal, aIndex, aString) {
+ gWatch.removeAt(aIndex);
+
+ is(gWatch.getAllStrings().length, aTotal,
+ "There should be " + aTotal + " watch expressions available (1).");
+ is(gWatch.itemCount, aTotal,
+ "There should be " + aTotal + " watch expressions available (2).");
+
+ ok(gWatch.getItemAtIndex(aIndex),
+ "The expression at index " + aIndex + " should still be available.");
+ is(gWatch.getItemAtIndex(aIndex).attachment.initialExpression, aString,
+ "The expression at index " + aIndex + " should still have an initial expression.");
+
+ let element = gWatch.getItemAtIndex(aIndex).target;
+
+ is(gWatch.getItemAtIndex(aIndex).attachment.initialExpression, aString,
+ "The initial expression at index " + aIndex + " should be correct (1).");
+ is(gWatch.getItemForElement(element).attachment.initialExpression, aString,
+ "The initial expression at index " + aIndex + " should be correct (2).");
+
+ is(gWatch.getItemAtIndex(aIndex).attachment.currentExpression, aString,
+ "The expression at index " + aIndex + " should be correct (1).");
+ is(gWatch.getItemForElement(element).attachment.currentExpression, aString,
+ "The expression at index " + aIndex + " should be correct (2).");
+
+ is(gWatch.getString(aIndex), aString,
+ "The expression at index " + aIndex + " should be correct (3).");
+ is(gWatch.getAllStrings()[aIndex], aString,
+ "The expression at index " + aIndex + " should be correct (4).");
+ }
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_watch-expressions-02.js b/devtools/client/debugger/test/mochitest/browser_dbg_watch-expressions-02.js
new file mode 100644
index 000000000..a9b22708d
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_watch-expressions-02.js
@@ -0,0 +1,383 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Bug 727429: Test the debugger watch expressions.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_watch-expressions.html";
+
+function test() {
+ // Debug test slaves are a bit slow at this test.
+ requestLongerTimeout(2);
+
+ let gTab, gPanel, gDebugger;
+ let gWatch, gVariables;
+
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gWatch = gDebugger.DebuggerView.WatchExpressions;
+ gVariables = gDebugger.DebuggerView.Variables;
+
+ gDebugger.DebuggerView.toggleInstrumentsPane({ visible: true, animated: false });
+
+ addExpressions();
+ performTest()
+ .then(finishTest)
+ .then(() => closeDebuggerAndFinish(gPanel))
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+ });
+
+ function addExpressions() {
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1205353
+ // BZ#1205353 - wrong result for string replace with backslash-dollar.
+ gWatch.addExpression("'$$$'.replace(/\\$/, 'foo')");
+ gWatch.addExpression("'a'");
+ gWatch.addExpression("\"a\"");
+ gWatch.addExpression("'a\"\"'");
+ gWatch.addExpression("\"a''\"");
+ gWatch.addExpression("?");
+ gWatch.addExpression("a");
+ gWatch.addExpression("this");
+ gWatch.addExpression("this.canada");
+ gWatch.addExpression("[1, 2, 3]");
+ gWatch.addExpression("x = [1, 2, 3]");
+ gWatch.addExpression("y = [1, 2, 3]; y.test = 4");
+ gWatch.addExpression("z = [1, 2, 3]; z.test = 4; z");
+ gWatch.addExpression("t = [1, 2, 3]; t.test = 4; !t");
+ gWatch.addExpression("arguments[0]");
+ gWatch.addExpression("encodeURI(\"\\\")");
+ gWatch.addExpression("decodeURI(\"\\\")");
+ gWatch.addExpression("decodeURIComponent(\"%\")");
+ gWatch.addExpression("//");
+ gWatch.addExpression("// 42");
+ gWatch.addExpression("{}.foo");
+ gWatch.addExpression("{}.foo()");
+ gWatch.addExpression("({}).foo()");
+ gWatch.addExpression("new Array(-1)");
+ gWatch.addExpression("4.2.toExponential(-4.2)");
+ gWatch.addExpression("throw new Error(\"bazinga\")");
+ gWatch.addExpression("({ get error() { throw new Error(\"bazinga\") } }).error");
+ gWatch.addExpression("throw { get name() { throw \"bazinga\" } }");
+
+ }
+
+ function performTest() {
+ let deferred = promise.defer();
+
+ is(gDebugger.document.querySelectorAll(".dbg-expression[hidden=true]").length, 0,
+ "There should be 0 hidden nodes in the watch expressions container");
+ is(gDebugger.document.querySelectorAll(".dbg-expression:not([hidden=true])").length, 28,
+ "There should be 28 visible nodes in the watch expressions container");
+
+ test1(function () {
+ test2(function () {
+ test3(function () {
+ test4(function () {
+ test5(function () {
+ test6(function () {
+ test7(function () {
+ test8(function () {
+ test9(function () {
+ deferred.resolve();
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+
+ return deferred.promise;
+ }
+
+ function finishTest() {
+ is(gDebugger.document.querySelectorAll(".dbg-expression[hidden=true]").length, 0,
+ "There should be 0 hidden nodes in the watch expressions container");
+ is(gDebugger.document.querySelectorAll(".dbg-expression:not([hidden=true])").length, 28,
+ "There should be 28 visible nodes in the watch expressions container");
+ }
+
+ function test1(aCallback) {
+ gDebugger.once(gDebugger.EVENTS.FETCHED_WATCH_EXPRESSIONS, () => {
+ checkWatchExpressions(27, {
+ a: "ReferenceError: a is not defined",
+ this: { type: "object", class: "Object" },
+ prop: { type: "object", class: "String" },
+ args: { type: "undefined" }
+ });
+ aCallback();
+ });
+
+ callInTab(gTab, "test");
+ }
+
+ function test2(aCallback) {
+ gDebugger.once(gDebugger.EVENTS.FETCHED_WATCH_EXPRESSIONS, () => {
+ checkWatchExpressions(27, {
+ a: { type: "undefined" },
+ this: { type: "object", class: "Window" },
+ prop: { type: "undefined" },
+ args: "sensational"
+ });
+ aCallback();
+ });
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ gDebugger.document.getElementById("resume"),
+ gDebugger);
+ }
+
+ function test3(aCallback) {
+ gDebugger.once(gDebugger.EVENTS.FETCHED_WATCH_EXPRESSIONS, () => {
+ checkWatchExpressions(27, {
+ a: { type: "object", class: "Object" },
+ this: { type: "object", class: "Window" },
+ prop: { type: "undefined" },
+ args: "sensational"
+ });
+ aCallback();
+ });
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ gDebugger.document.getElementById("resume"),
+ gDebugger);
+ }
+
+ function test4(aCallback) {
+ gDebugger.once(gDebugger.EVENTS.FETCHED_WATCH_EXPRESSIONS, () => {
+ checkWatchExpressions(28, {
+ a: 5,
+ this: { type: "object", class: "Window" },
+ prop: { type: "undefined" },
+ args: "sensational"
+ });
+ aCallback();
+ });
+
+ gWatch.addExpression("a = 5");
+ EventUtils.sendKey("RETURN", gDebugger);
+ }
+
+ function test5(aCallback) {
+ gDebugger.once(gDebugger.EVENTS.FETCHED_WATCH_EXPRESSIONS, () => {
+ checkWatchExpressions(28, {
+ a: 5,
+ this: { type: "object", class: "Window" },
+ prop: { type: "undefined" },
+ args: "sensational"
+ });
+ aCallback();
+ });
+
+ gWatch.addExpression("encodeURI(\"\\\")");
+ EventUtils.sendKey("RETURN", gDebugger);
+ }
+
+ function test6(aCallback) {
+ gDebugger.once(gDebugger.EVENTS.FETCHED_WATCH_EXPRESSIONS, () => {
+ checkWatchExpressions(28, {
+ a: 5,
+ this: { type: "object", class: "Window" },
+ prop: { type: "undefined" },
+ args: "sensational"
+ });
+ aCallback();
+ });
+
+ gWatch.addExpression("decodeURI(\"\\\")");
+ EventUtils.sendKey("RETURN", gDebugger);
+ }
+
+ function test7(aCallback) {
+ gDebugger.once(gDebugger.EVENTS.FETCHED_WATCH_EXPRESSIONS, () => {
+ checkWatchExpressions(28, {
+ a: 5,
+ this: { type: "object", class: "Window" },
+ prop: { type: "undefined" },
+ args: "sensational"
+ });
+ aCallback();
+ });
+
+ gWatch.addExpression("?");
+ EventUtils.sendKey("RETURN", gDebugger);
+ }
+
+ function test8(aCallback) {
+ gDebugger.once(gDebugger.EVENTS.FETCHED_WATCH_EXPRESSIONS, () => {
+ checkWatchExpressions(28, {
+ a: 5,
+ this: { type: "object", class: "Window" },
+ prop: { type: "undefined" },
+ args: "sensational"
+ });
+ aCallback();
+ });
+
+ gWatch.addExpression("a");
+ EventUtils.sendKey("RETURN", gDebugger);
+ }
+
+ function test9(aCallback) {
+ gDebugger.once(gDebugger.EVENTS.AFTER_FRAMES_CLEARED, () => {
+ aCallback();
+ });
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ gDebugger.document.getElementById("resume"),
+ gDebugger);
+ }
+
+ function checkWatchExpressions(aTotal, aExpectedExpressions) {
+ let {
+ a: expected_a,
+ this: expected_this,
+ prop: expected_prop,
+ args: expected_args
+ } = aExpectedExpressions;
+
+ is(gDebugger.document.querySelectorAll(".dbg-expression[hidden=true]").length, aTotal,
+ "There should be " + aTotal + " hidden nodes in the watch expressions container.");
+ is(gDebugger.document.querySelectorAll(".dbg-expression:not([hidden=true])").length, 0,
+ "There should be 0 visible nodes in the watch expressions container.");
+
+ let label = gDebugger.L10N.getStr("watchExpressionsScopeLabel");
+ let scope = gVariables._currHierarchy.get(label);
+
+ ok(scope, "There should be a wach expressions scope in the variables view.");
+ is(scope._store.size, aTotal, "There should be " + aTotal + " evaluations availalble.");
+
+ let w1 = scope.get("'a'");
+ let w2 = scope.get("\"a\"");
+ let w3 = scope.get("'a\"\"'");
+ let w4 = scope.get("\"a''\"");
+ let w5 = scope.get("?");
+ let w6 = scope.get("a");
+ let w7 = scope.get("this");
+ let w8 = scope.get("this.canada");
+ let w9 = scope.get("[1, 2, 3]");
+ let w10 = scope.get("x = [1, 2, 3]");
+ let w11 = scope.get("y = [1, 2, 3]; y.test = 4");
+ let w12 = scope.get("z = [1, 2, 3]; z.test = 4; z");
+ let w13 = scope.get("t = [1, 2, 3]; t.test = 4; !t");
+ let w14 = scope.get("arguments[0]");
+ let w15 = scope.get("encodeURI(\"\\\")");
+ let w16 = scope.get("decodeURI(\"\\\")");
+ let w17 = scope.get("decodeURIComponent(\"%\")");
+ let w18 = scope.get("//");
+ let w19 = scope.get("// 42");
+ let w20 = scope.get("{}.foo");
+ let w21 = scope.get("{}.foo()");
+ let w22 = scope.get("({}).foo()");
+ let w23 = scope.get("new Array(-1)");
+ let w24 = scope.get("4.2.toExponential(-4.2)");
+ let w25 = scope.get("throw new Error(\"bazinga\")");
+ let w26 = scope.get("({ get error() { throw new Error(\"bazinga\") } }).error");
+ let w27 = scope.get("throw { get name() { throw \"bazinga\" } }");
+ let w28 = scope.get("'$$$'.replace(/\\$/, 'foo')");
+
+ ok(w1, "The first watch expression should be present in the scope.");
+ ok(w2, "The second watch expression should be present in the scope.");
+ ok(w3, "The third watch expression should be present in the scope.");
+ ok(w4, "The fourth watch expression should be present in the scope.");
+ ok(w5, "The fifth watch expression should be present in the scope.");
+ ok(w6, "The sixth watch expression should be present in the scope.");
+ ok(w7, "The seventh watch expression should be present in the scope.");
+ ok(w8, "The eight watch expression should be present in the scope.");
+ ok(w9, "The ninth watch expression should be present in the scope.");
+ ok(w10, "The tenth watch expression should be present in the scope.");
+ ok(w11, "The eleventh watch expression should be present in the scope.");
+ ok(w12, "The twelfth watch expression should be present in the scope.");
+ ok(w13, "The 13th watch expression should be present in the scope.");
+ ok(w14, "The 14th watch expression should be present in the scope.");
+ ok(w15, "The 15th watch expression should be present in the scope.");
+ ok(w16, "The 16th watch expression should be present in the scope.");
+ ok(w17, "The 17th watch expression should be present in the scope.");
+ ok(w18, "The 18th watch expression should be present in the scope.");
+ ok(w19, "The 19th watch expression should be present in the scope.");
+ ok(w20, "The 20th watch expression should be present in the scope.");
+ ok(w21, "The 21st watch expression should be present in the scope.");
+ ok(w22, "The 22nd watch expression should be present in the scope.");
+ ok(w23, "The 23nd watch expression should be present in the scope.");
+ ok(w24, "The 24th watch expression should be present in the scope.");
+ ok(w25, "The 25th watch expression should be present in the scope.");
+ ok(w26, "The 26th watch expression should be present in the scope.");
+ ok(!w27, "The 27th watch expression should not be present in the scope.");
+ ok(w28, "The 28th watch expression should be present in the scope.");
+
+ is(w1.value, "a", "The first value is correct.");
+ is(w2.value, "a", "The second value is correct.");
+ is(w3.value, "a\"\"", "The third value is correct.");
+ is(w4.value, "a''", "The fourth value is correct.");
+ is(w5.value, "SyntaxError: expected expression, got '?'", "The fifth value is correct.");
+
+ if (typeof expected_a == "object") {
+ is(w6.value.type, expected_a.type, "The sixth value type is correct.");
+ is(w6.value.class, expected_a.class, "The sixth value class is correct.");
+ } else {
+ is(w6.value, expected_a, "The sixth value is correct.");
+ }
+
+ if (typeof expected_this == "object") {
+ is(w7.value.type, expected_this.type, "The seventh value type is correct.");
+ is(w7.value.class, expected_this.class, "The seventh value class is correct.");
+ } else {
+ is(w7.value, expected_this, "The seventh value is correct.");
+ }
+
+ if (typeof expected_prop == "object") {
+ is(w8.value.type, expected_prop.type, "The eighth value type is correct.");
+ is(w8.value.class, expected_prop.class, "The eighth value class is correct.");
+ } else {
+ is(w8.value, expected_prop, "The eighth value is correct.");
+ }
+
+ is(w9.value.type, "object", "The ninth value type is correct.");
+ is(w9.value.class, "Array", "The ninth value class is correct.");
+ is(w10.value.type, "object", "The tenth value type is correct.");
+ is(w10.value.class, "Array", "The tenth value class is correct.");
+ is(w11.value, "4", "The eleventh value is correct.");
+ is(w12.value.type, "object", "The eleventh value type is correct.");
+ is(w12.value.class, "Array", "The twelfth value class is correct.");
+ is(w13.value, false, "The 13th value is correct.");
+
+ if (typeof expected_args == "object") {
+ is(w14.value.type, expected_args.type, "The 14th value type is correct.");
+ is(w14.value.class, expected_args.class, "The 14th value class is correct.");
+ } else {
+ is(w14.value, expected_args, "The 14th value is correct.");
+ }
+
+ is(w15.value, "SyntaxError: unterminated string literal", "The 15th value is correct.");
+ is(w16.value, "SyntaxError: unterminated string literal", "The 16th value is correct.");
+ is(w17.value, "URIError: malformed URI sequence", "The 17th value is correct.");
+
+ is(w18.value.type, "undefined", "The 18th value type is correct.");
+ is(w18.value.class, undefined, "The 18th value class is correct.");
+
+ is(w19.value.type, "undefined", "The 19th value type is correct.");
+ is(w19.value.class, undefined, "The 19th value class is correct.");
+
+ is(w20.value, "SyntaxError: expected expression, got '.'", "The 20th value is correct.");
+ is(w21.value, "SyntaxError: expected expression, got '.'", "The 21th value is correct.");
+ is(w22.value, "TypeError: (intermediate value).foo is not a function", "The 22th value is correct.");
+ is(w23.value, "RangeError: invalid array length", "The 23th value is correct.");
+ is(w24.value, "RangeError: precision -4 out of range", "The 24th value is correct.");
+ is(w25.value, "Error: bazinga", "The 25th value is correct.");
+ is(w26.value, "Error: bazinga", "The 26th value is correct.");
+ is(w28.value, "foo$$", "The 28th value is correct.");
+ }
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_worker-console-01.js b/devtools/client/debugger/test/mochitest/browser_dbg_worker-console-01.js
new file mode 100644
index 000000000..72b0cbd16
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_worker-console-01.js
@@ -0,0 +1,21 @@
+// Check to make sure that a worker can be attached to a toolbox
+// and that the console works.
+
+var TAB_URL = EXAMPLE_URL + "doc_WorkerActor.attachThread-tab.html";
+var WORKER_URL = "code_WorkerActor.attachThread-worker.js";
+
+add_task(function* testNormalExecution() {
+ let {client, tab, tabClient, workerClient, toolbox, gDebugger} =
+ yield initWorkerDebugger(TAB_URL, WORKER_URL);
+
+ let jsterm = yield getSplitConsole(toolbox);
+ let executed = yield jsterm.execute("this.location.toString()");
+ ok(executed.textContent.includes(WORKER_URL),
+ "Evaluating the global's location works");
+
+ terminateWorkerInTab(tab, WORKER_URL);
+ yield waitForWorkerClose(workerClient);
+ yield gDevTools.closeToolbox(TargetFactory.forWorker(workerClient));
+ yield close(client);
+ yield removeTab(tab);
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_worker-console-02.js b/devtools/client/debugger/test/mochitest/browser_dbg_worker-console-02.js
new file mode 100644
index 000000000..b6e8d12af
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_worker-console-02.js
@@ -0,0 +1,58 @@
+// Check to make sure that a worker can be attached to a toolbox
+// and that the console works.
+
+var TAB_URL = EXAMPLE_URL + "doc_WorkerActor.attachThread-tab.html";
+var WORKER_URL = "code_WorkerActor.attachThread-worker.js";
+
+add_task(function* testWhilePaused() {
+ let {client, tab, tabClient, workerClient, toolbox, gDebugger} =
+ yield initWorkerDebugger(TAB_URL, WORKER_URL);
+
+ let gTarget = gDebugger.gTarget;
+ let gResumeButton = gDebugger.document.getElementById("resume");
+ let gResumeKey = gDebugger.document.getElementById("resumeKey");
+
+ // Execute some basic math to make sure evaluations are working.
+ let jsterm = yield getSplitConsole(toolbox);
+ let executed = yield jsterm.execute("10000+1");
+ ok(executed.textContent.includes("10001"), "Text for message appeared correct");
+
+ // Pause the worker by waiting for next execution and then sending a message to
+ // it from the main thread.
+ let oncePaused = gTarget.once("thread-paused");
+ EventUtils.sendMouseEvent({ type: "mousedown" }, gResumeButton, gDebugger);
+ once(gDebugger.gClient, "willInterrupt").then(() => {
+ info("Posting message to worker, then waiting for a pause");
+ postMessageToWorkerInTab(tab, WORKER_URL, "ping");
+ });
+ yield oncePaused;
+
+ let command1 = jsterm.execute("10000+2");
+ let command2 = jsterm.execute("10000+3");
+ let command3 = jsterm.execute("foobar"); // throw an error
+
+ info("Trying to get the result of command1");
+ executed = yield command1;
+ ok(executed.textContent.includes("10002"),
+ "command1 executed successfully");
+
+ info("Trying to get the result of command2");
+ executed = yield command2;
+ ok(executed.textContent.includes("10003"),
+ "command2 executed successfully");
+
+ info("Trying to get the result of command3");
+ executed = yield command3;
+ ok(executed.textContent.includes("ReferenceError: foobar is not defined"),
+ "command3 executed successfully");
+
+ let onceResumed = gTarget.once("thread-resumed");
+ EventUtils.sendMouseEvent({ type: "mousedown" }, gResumeButton, gDebugger);
+ yield onceResumed;
+
+ terminateWorkerInTab(tab, WORKER_URL);
+ yield waitForWorkerClose(workerClient);
+ yield gDevTools.closeToolbox(TargetFactory.forWorker(workerClient));
+ yield close(client);
+ yield removeTab(tab);
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_worker-console-03.js b/devtools/client/debugger/test/mochitest/browser_dbg_worker-console-03.js
new file mode 100644
index 000000000..49821492f
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_worker-console-03.js
@@ -0,0 +1,46 @@
+// Check to make sure that a worker can be attached to a toolbox
+// and that the console works.
+
+var TAB_URL = EXAMPLE_URL + "doc_WorkerActor.attachThread-tab.html";
+var WORKER_URL = "code_WorkerActor.attachThread-worker.js";
+
+// Test to see if creating the pause from the console works.
+add_task(function* testPausedByConsole() {
+ let {client, tab, tabClient, workerClient, toolbox, gDebugger} =
+ yield initWorkerDebugger(TAB_URL, WORKER_URL);
+
+ let gTarget = gDebugger.gTarget;
+ let gResumeButton = gDebugger.document.getElementById("resume");
+ let gResumeKey = gDebugger.document.getElementById("resumeKey");
+
+ let jsterm = yield getSplitConsole(toolbox);
+ let executed = yield jsterm.execute("10000+1");
+ ok(executed.textContent.includes("10001"),
+ "Text for message appeared correct");
+
+ let oncePaused = gTarget.once("thread-paused");
+ EventUtils.sendMouseEvent({ type: "mousedown" }, gResumeButton, gDebugger);
+ let pausedExecution = jsterm.execute("10000+2");
+
+ info("Executed a command with 'break on next' active, waiting for pause");
+ yield oncePaused;
+
+ executed = yield jsterm.execute("10000+3");
+ ok(executed.textContent.includes("10003"),
+ "Text for message appeared correct");
+
+ info("Waiting for a resume");
+ let onceResumed = gTarget.once("thread-resumed");
+ EventUtils.sendMouseEvent({ type: "mousedown" }, gResumeButton, gDebugger);
+ yield onceResumed;
+
+ executed = yield pausedExecution;
+ ok(executed.textContent.includes("10002"),
+ "Text for message appeared correct");
+
+ terminateWorkerInTab(tab, WORKER_URL);
+ yield waitForWorkerClose(workerClient);
+ yield gDevTools.closeToolbox(TargetFactory.forWorker(workerClient));
+ yield close(client);
+ yield removeTab(tab);
+});
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_worker-source-map.js b/devtools/client/debugger/test/mochitest/browser_dbg_worker-source-map.js
new file mode 100644
index 000000000..c4e8841d5
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_worker-source-map.js
@@ -0,0 +1,89 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+const TAB_URL = EXAMPLE_URL + "doc_worker-source-map.html";
+const WORKER_URL = "code_worker-source-map.js";
+const COFFEE_URL = EXAMPLE_URL + "code_worker-source-map.coffee";
+
+function selectWorker(aPanel, aURL) {
+ let panelWin = aPanel.panelWin;
+ let promise = waitForDebuggerEvents(aPanel, panelWin.EVENTS.WORKER_SELECTED);
+ let Workers = panelWin.DebuggerView.Workers;
+ let item = Workers.getItemForAttachment((workerForm) => {
+ return workerForm.url === aURL;
+ });
+ Workers.selectedItem = item;
+ return promise;
+}
+
+function test() {
+ return Task.spawn(function* () {
+ yield pushPrefs(["devtools.debugger.workers", true]);
+
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ let [tab,, panel] = yield initDebugger(TAB_URL, options);
+ let toolbox = yield selectWorker(panel, WORKER_URL);
+ let workerPanel = toolbox.getCurrentPanel();
+ yield waitForSourceShown(workerPanel, ".coffee");
+ let panelWin = workerPanel.panelWin;
+ let Sources = panelWin.DebuggerView.Sources;
+ let editor = panelWin.DebuggerView.editor;
+ let threadClient = panelWin.gThreadClient;
+
+ isnot(Sources.selectedItem.attachment.source.url.indexOf(".coffee"), -1,
+ "The debugger should show the source mapped coffee source file.");
+ is(Sources.selectedValue.indexOf(".js"), -1,
+ "The debugger should not show the generated js source file.");
+ is(editor.getText().indexOf("isnt"), 211,
+ "The debugger's editor should have the coffee source source displayed.");
+ is(editor.getText().indexOf("function"), -1,
+ "The debugger's editor should not have the JS source displayed.");
+
+ yield threadClient.interrupt();
+ let sourceForm = getSourceForm(Sources, COFFEE_URL);
+ let source = threadClient.source(sourceForm);
+ let response = yield source.setBreakpoint({ line: 5 });
+
+ ok(!response.error,
+ "Should be able to set a breakpoint in a coffee source file.");
+ ok(!response.actualLocation,
+ "Should be able to set a breakpoint on line 5.");
+
+ let promise = new Promise((resolve) => {
+ threadClient.addOneTimeListener("paused", (event, packet) => {
+ is(packet.type, "paused",
+ "We should now be paused again.");
+ is(packet.why.type, "breakpoint",
+ "and the reason we should be paused is because we hit a breakpoint.");
+
+ // Check that we stopped at the right place, by making sure that the
+ // environment is in the state that we expect.
+ is(packet.frame.environment.bindings.variables.start.value, 0,
+ "'start' is 0.");
+ is(packet.frame.environment.bindings.variables.stop.value.type, "undefined",
+ "'stop' hasn't been assigned to yet.");
+ is(packet.frame.environment.bindings.variables.pivot.value.type, "undefined",
+ "'pivot' hasn't been assigned to yet.");
+
+ waitForCaretUpdated(workerPanel, 5).then(resolve);
+ });
+ });
+
+ // This will cause the breakpoint to be hit, and put us back in the
+ // paused state.
+ yield threadClient.resume();
+ callInTab(tab, "binary_search", [0, 2, 3, 5, 7, 10], 5);
+ yield promise;
+
+ yield threadClient.resume();
+ yield toolbox.destroy();
+ yield closeDebuggerAndFinish(panel);
+
+ yield popPrefs();
+ });
+}
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_worker-window.js b/devtools/client/debugger/test/mochitest/browser_dbg_worker-window.js
new file mode 100644
index 000000000..46198d31c
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_worker-window.js
@@ -0,0 +1,61 @@
+// Check to make sure that a worker can be attached to a toolbox
+// directly, and that the toolbox has expected properties.
+
+"use strict";
+
+// Whitelisting this test.
+// As part of bug 1077403, the leaking uncaught rejections should be fixed.
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed("[object Object]");
+
+var TAB_URL = EXAMPLE_URL + "doc_WorkerActor.attachThread-tab.html";
+var WORKER_URL = "code_WorkerActor.attachThread-worker.js";
+
+add_task(function* () {
+ yield pushPrefs(["devtools.scratchpad.enabled", true]);
+
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+
+ let client = new DebuggerClient(DebuggerServer.connectPipe());
+ yield connect(client);
+
+ let tab = yield addTab(TAB_URL);
+ let { tabs } = yield listTabs(client);
+ let [, tabClient] = yield attachTab(client, findTab(tabs, TAB_URL));
+
+ yield listWorkers(tabClient);
+ yield createWorkerInTab(tab, WORKER_URL);
+
+ let { workers } = yield listWorkers(tabClient);
+ let [, workerClient] = yield attachWorker(tabClient,
+ findWorker(workers, WORKER_URL));
+
+ let toolbox = yield gDevTools.showToolbox(TargetFactory.forWorker(workerClient),
+ "jsdebugger",
+ Toolbox.HostType.WINDOW);
+
+ is(toolbox.hostType, "window", "correct host");
+
+ yield new Promise(done => {
+ toolbox.win.parent.addEventListener("message", function onmessage(event) {
+ if (event.data.name == "set-host-title") {
+ toolbox.win.parent.removeEventListener("message", onmessage);
+ done();
+ }
+ });
+ });
+ ok(toolbox.win.parent.document.title.includes(WORKER_URL),
+ "worker URL in host title");
+
+ let toolTabs = toolbox.doc.querySelectorAll(".devtools-tab");
+ let activeTools = [...toolTabs].map(tab=>tab.getAttribute("toolid"));
+
+ is(activeTools.join(","), "webconsole,jsdebugger,scratchpad,options",
+ "Correct set of tools supported by worker");
+
+ terminateWorkerInTab(tab, WORKER_URL);
+ yield waitForWorkerClose(workerClient);
+ yield close(client);
+
+ yield toolbox.destroy();
+});
diff --git a/devtools/client/debugger/test/mochitest/code_WorkerActor.attach-worker1.js b/devtools/client/debugger/test/mochitest/code_WorkerActor.attach-worker1.js
new file mode 100644
index 000000000..18f14864d
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/code_WorkerActor.attach-worker1.js
@@ -0,0 +1,5 @@
+"use strict";
+
+self.onmessage = function () {};
+
+postMessage("load");
diff --git a/devtools/client/debugger/test/mochitest/code_WorkerActor.attach-worker2.js b/devtools/client/debugger/test/mochitest/code_WorkerActor.attach-worker2.js
new file mode 100644
index 000000000..18f14864d
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/code_WorkerActor.attach-worker2.js
@@ -0,0 +1,5 @@
+"use strict";
+
+self.onmessage = function () {};
+
+postMessage("load");
diff --git a/devtools/client/debugger/test/mochitest/code_WorkerActor.attachThread-worker.js b/devtools/client/debugger/test/mochitest/code_WorkerActor.attachThread-worker.js
new file mode 100644
index 000000000..881eab0b8
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/code_WorkerActor.attachThread-worker.js
@@ -0,0 +1,16 @@
+"use strict";
+
+function f() {
+ var a = 1;
+ var b = 2;
+ var c = 3;
+}
+
+self.onmessage = function (event) {
+ if (event.data == "ping") {
+ f();
+ postMessage("pong");
+ }
+};
+
+postMessage("load");
diff --git a/devtools/client/debugger/test/mochitest/code_binary_search.coffee b/devtools/client/debugger/test/mochitest/code_binary_search.coffee
new file mode 100644
index 000000000..e3dacdaaa
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/code_binary_search.coffee
@@ -0,0 +1,18 @@
+# Uses a binary search algorithm to locate a value in the specified array.
+window.binary_search = (items, value) ->
+
+ start = 0
+ stop = items.length - 1
+ pivot = Math.floor (start + stop) / 2
+
+ while items[pivot] isnt value and start < stop
+
+ # Adjust the search area.
+ stop = pivot - 1 if value < items[pivot]
+ start = pivot + 1 if value > items[pivot]
+
+ # Recalculate the pivot.
+ pivot = Math.floor (stop + start) / 2
+
+ # Make sure we've found the correct value.
+ if items[pivot] is value then pivot else -1 \ No newline at end of file
diff --git a/devtools/client/debugger/test/mochitest/code_binary_search.js b/devtools/client/debugger/test/mochitest/code_binary_search.js
new file mode 100644
index 000000000..c43848a60
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/code_binary_search.js
@@ -0,0 +1,29 @@
+// Generated by CoffeeScript 1.6.1
+(function() {
+
+ window.binary_search = function(items, value) {
+ var pivot, start, stop;
+ start = 0;
+ stop = items.length - 1;
+ pivot = Math.floor((start + stop) / 2);
+ while (items[pivot] !== value && start < stop) {
+ if (value < items[pivot]) {
+ stop = pivot - 1;
+ }
+ if (value > items[pivot]) {
+ start = pivot + 1;
+ }
+ pivot = Math.floor((stop + start) / 2);
+ }
+ if (items[pivot] === value) {
+ return pivot;
+ } else {
+ return -1;
+ }
+ };
+
+}).call(this);
+
+/*
+//# sourceMappingURL=code_binary_search.map
+*/
diff --git a/devtools/client/debugger/test/mochitest/code_binary_search.map b/devtools/client/debugger/test/mochitest/code_binary_search.map
new file mode 100644
index 000000000..8d2251125
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/code_binary_search.map
@@ -0,0 +1,10 @@
+{
+ "version": 3,
+ "file": "code_binary_search.js",
+ "sourceRoot": "",
+ "sources": [
+ "code_binary_search.coffee"
+ ],
+ "names": [],
+ "mappings": ";AACA;CAAA;CAAA,CAAA,CAAuB,EAAA,CAAjB,GAAkB,IAAxB;CAEE,OAAA,UAAA;CAAA,EAAQ,CAAR,CAAA;CAAA,EACQ,CAAR,CAAa,CAAL;CADR,EAEQ,CAAR,CAAA;CAEA,EAA0C,CAAR,CAAtB,MAAN;CAGJ,EAA6B,CAAR,CAAA,CAArB;CAAA,EAAQ,CAAR,CAAQ,GAAR;QAAA;CACA,EAA6B,CAAR,CAAA,CAArB;CAAA,EAAQ,EAAR,GAAA;QADA;CAAA,EAIQ,CAAI,CAAZ,CAAA;CAXF,IAIA;CAUA,GAAA,CAAS;CAAT,YAA8B;MAA9B;AAA0C,CAAD,YAAA;MAhBpB;CAAvB,EAAuB;CAAvB"
+}
diff --git a/devtools/client/debugger/test/mochitest/code_blackboxing_blackboxme.js b/devtools/client/debugger/test/mochitest/code_blackboxing_blackboxme.js
new file mode 100644
index 000000000..713b3d50d
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/code_blackboxing_blackboxme.js
@@ -0,0 +1,9 @@
+function blackboxme(fn) {
+ (function one() {
+ (function two() {
+ (function three() {
+ fn();
+ }());
+ }());
+ }());
+}
diff --git a/devtools/client/debugger/test/mochitest/code_blackboxing_one.js b/devtools/client/debugger/test/mochitest/code_blackboxing_one.js
new file mode 100644
index 000000000..7f37b02ad
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/code_blackboxing_one.js
@@ -0,0 +1,4 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function one() { two(); }
diff --git a/devtools/client/debugger/test/mochitest/code_blackboxing_three.js b/devtools/client/debugger/test/mochitest/code_blackboxing_three.js
new file mode 100644
index 000000000..55ed6c4da
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/code_blackboxing_three.js
@@ -0,0 +1,4 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function three() { doDebuggerStatement(); }
diff --git a/devtools/client/debugger/test/mochitest/code_blackboxing_two.js b/devtools/client/debugger/test/mochitest/code_blackboxing_two.js
new file mode 100644
index 000000000..4790ea4a7
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/code_blackboxing_two.js
@@ -0,0 +1,4 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function two() { three(); }
diff --git a/devtools/client/debugger/test/mochitest/code_blackboxing_unblackbox.min.js b/devtools/client/debugger/test/mochitest/code_blackboxing_unblackbox.min.js
new file mode 100644
index 000000000..b8b285589
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/code_blackboxing_unblackbox.min.js
@@ -0,0 +1 @@
+function blackboxme() {one();} function one() {}
diff --git a/devtools/client/debugger/test/mochitest/code_breakpoints-break-on-last-line-of-script-on-reload.js b/devtools/client/debugger/test/mochitest/code_breakpoints-break-on-last-line-of-script-on-reload.js
new file mode 100644
index 000000000..839f22883
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/code_breakpoints-break-on-last-line-of-script-on-reload.js
@@ -0,0 +1,6 @@
+debugger;
+var a = (function () {
+ var b = 9;
+ console.log("x", b);
+ return b;
+})();
diff --git a/devtools/client/debugger/test/mochitest/code_breakpoints-other-tabs.js b/devtools/client/debugger/test/mochitest/code_breakpoints-other-tabs.js
new file mode 100644
index 000000000..2cf53ba2d
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/code_breakpoints-other-tabs.js
@@ -0,0 +1,4 @@
+function testCase() {
+ var foo = "break on me";
+ debugger;
+}
diff --git a/devtools/client/debugger/test/mochitest/code_bug-896139.js b/devtools/client/debugger/test/mochitest/code_bug-896139.js
new file mode 100644
index 000000000..65313bcb2
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/code_bug-896139.js
@@ -0,0 +1,8 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http:// creativecommons.org/publicdomain/zero/1.0/ -->
+
+function f() {
+ var a = 1;
+ var b = 2;
+ var c = 3;
+}
diff --git a/devtools/client/debugger/test/mochitest/code_frame-script.js b/devtools/client/debugger/test/mochitest/code_frame-script.js
new file mode 100644
index 000000000..35a950b01
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/code_frame-script.js
@@ -0,0 +1,106 @@
+"use strict";
+
+var { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+const { loadSubScript } = Cc["@mozilla.org/moz/jssubscript-loader;1"].
+ getService(Ci.mozIJSSubScriptLoader);
+
+// Set up a dummy environment so that EventUtils works. We need to be careful to
+// pass a window object into each EventUtils method we call rather than having
+// it rely on the |window| global.
+let EventUtils = {};
+EventUtils.window = content;
+EventUtils.parent = EventUtils.window;
+EventUtils._EU_Ci = Components.interfaces;
+EventUtils._EU_Cc = Components.classes;
+EventUtils.navigator = content.navigator;
+EventUtils.KeyboardEvent = content.KeyboardEvent;
+loadSubScript("chrome://mochikit/content/tests/SimpleTest/EventUtils.js", EventUtils);
+
+dump("Frame script loaded.\n");
+
+var workers = {};
+
+this.call = function (name, args) {
+ dump("Calling function with name " + name + ".\n");
+
+ dump("args " + JSON.stringify(args) + "\n");
+ return XPCNativeWrapper.unwrap(content)[name].apply(undefined, args);
+};
+
+this._eval = function (string) {
+ dump("Evalling string.\n");
+
+ return content.eval(string);
+};
+
+this.generateMouseClick = function (path) {
+ dump("Generating mouse click.\n");
+
+ let target = eval(path);
+ EventUtils.synthesizeMouseAtCenter(target, {},
+ target.ownerDocument.defaultView);
+};
+
+this.createWorker = function (url) {
+ dump("Creating worker with url '" + url + "'.\n");
+
+ return new Promise(function (resolve, reject) {
+ let worker = new content.Worker(url);
+ worker.addEventListener("message", function listener() {
+ worker.removeEventListener("message", listener);
+ workers[url] = worker;
+ resolve();
+ });
+ });
+};
+
+this.terminateWorker = function (url) {
+ dump("Terminating worker with url '" + url + "'.\n");
+
+ workers[url].terminate();
+ delete workers[url];
+};
+
+this.postMessageToWorker = function (url, message) {
+ dump("Posting message to worker with url '" + url + "'.\n");
+
+ return new Promise(function (resolve) {
+ let worker = workers[url];
+ worker.postMessage(message);
+ worker.addEventListener("message", function listener() {
+ worker.removeEventListener("message", listener);
+ resolve();
+ });
+ });
+};
+
+addMessageListener("jsonrpc", function ({ data: { method, params, id } }) {
+ method = this[method];
+ Promise.resolve().then(function () {
+ return method.apply(undefined, params);
+ }).then(function (result) {
+ sendAsyncMessage("jsonrpc", {
+ result: result,
+ error: null,
+ id: id
+ });
+ }, function (error) {
+ sendAsyncMessage("jsonrpc", {
+ result: null,
+ error: error.message.toString(),
+ id: id
+ });
+ });
+});
+
+addMessageListener("test:postMessageToWorker", function (message) {
+ dump("Posting message '" + message.data.message + "' to worker with url '" +
+ message.data.url + "'.\n");
+
+ let worker = workers[message.data.url];
+ worker.postMessage(message.data.message);
+ worker.addEventListener("message", function listener() {
+ worker.removeEventListener("message", listener);
+ sendAsyncMessage("test:postMessageToWorker");
+ });
+});
diff --git a/devtools/client/debugger/test/mochitest/code_function-jump-01.js b/devtools/client/debugger/test/mochitest/code_function-jump-01.js
new file mode 100644
index 000000000..d71cbfa93
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/code_function-jump-01.js
@@ -0,0 +1,6 @@
+
+function foo() {
+ // some function
+}
+
+foo();
diff --git a/devtools/client/debugger/test/mochitest/code_function-search-01.js b/devtools/client/debugger/test/mochitest/code_function-search-01.js
new file mode 100644
index 000000000..77331d722
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/code_function-search-01.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test() {
+ // Blah! First source!
+}
+
+test.prototype = {
+ anonymousExpression: function () {
+ },
+ namedExpression: function NAME() {
+ },
+ sub: {
+ sub: {
+ sub: {
+ }
+ }
+ }
+};
+
+var foo = {
+ a_test: function () {
+ },
+ n_test: function x() {
+ },
+ sub: {
+ a_test: function () {
+ },
+ n_test: function y() {
+ },
+ sub: {
+ a_test: function () {
+ },
+ n_test: function z() {
+ },
+ sub: {
+ test_SAME_NAME: function test_SAME_NAME() {
+ }
+ }
+ }
+ }
+};
diff --git a/devtools/client/debugger/test/mochitest/code_function-search-02.js b/devtools/client/debugger/test/mochitest/code_function-search-02.js
new file mode 100644
index 000000000..ab25641d2
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/code_function-search-02.js
@@ -0,0 +1,21 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var test2 = function () {
+ // Blah! Second source!
+};
+
+var test3 = function test3_NAME() {
+};
+
+var test4_SAME_NAME = function test4_SAME_NAME() {
+};
+
+test.prototype.x = function X() {
+};
+test.prototype.sub.y = function Y() {
+};
+test.prototype.sub.sub.z = function Z() {
+};
+test.prototype.sub.sub.sub.t = this.x = this.y = this.z = function () {
+};
diff --git a/devtools/client/debugger/test/mochitest/code_function-search-03.js b/devtools/client/debugger/test/mochitest/code_function-search-03.js
new file mode 100644
index 000000000..e64292a92
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/code_function-search-03.js
@@ -0,0 +1,32 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+window.addEventListener("bogus", function namedEventListener() {
+ // Blah! Third source!
+});
+
+try {
+ var bar = foo.sub.sub.test({
+ a: function A() {
+ }
+ });
+
+ bar.alpha = foo.sub.sub.test({
+ b: function B() {
+ }
+ });
+
+ bar.alpha.beta = new X(Y(Z(foo.sub.sub.test({
+ c: function C() {
+ }
+ }))));
+
+ this.theta = new X(new Y(new Z(new foo.sub.sub.test({
+ d: function D() {
+ }
+ }))));
+
+ var fun = foo = bar = this.t_foo = window.w_bar = function baz() {};
+
+} catch (e) {
+}
diff --git a/devtools/client/debugger/test/mochitest/code_listworkers-worker1.js b/devtools/client/debugger/test/mochitest/code_listworkers-worker1.js
new file mode 100644
index 000000000..8cee6809e
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/code_listworkers-worker1.js
@@ -0,0 +1,3 @@
+"use strict";
+
+self.onmessage = function () {};
diff --git a/devtools/client/debugger/test/mochitest/code_listworkers-worker2.js b/devtools/client/debugger/test/mochitest/code_listworkers-worker2.js
new file mode 100644
index 000000000..8cee6809e
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/code_listworkers-worker2.js
@@ -0,0 +1,3 @@
+"use strict";
+
+self.onmessage = function () {};
diff --git a/devtools/client/debugger/test/mochitest/code_location-changes.js b/devtools/client/debugger/test/mochitest/code_location-changes.js
new file mode 100644
index 000000000..d164b8bdf
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/code_location-changes.js
@@ -0,0 +1,7 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function myFunction() {
+ var a = 1;
+ debugger;
+}
diff --git a/devtools/client/debugger/test/mochitest/code_math.js b/devtools/client/debugger/test/mochitest/code_math.js
new file mode 100644
index 000000000..f765817bb
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/code_math.js
@@ -0,0 +1,45 @@
+function add(a, b, k) {
+ var result = a + b;
+ return k(result);
+}
+
+function sub(a, b, k) {
+ var result = a - b;
+ return k(result);
+}
+
+function mul(a, b, k) {
+ var result = a * b;
+ return k(result);
+}
+
+function div(a, b, k) {
+ var result = a / b;
+ return k(result);
+}
+
+function arithmetic() {
+ add(4, 4, function (a) {
+ // 8
+ sub(a, 2, function (b) {
+ // 6
+ mul(b, 3, function (c) {
+ // 18
+ div(c, 2, function (d) {
+ // 9
+ console.log(d);
+ });
+ });
+ });
+ });
+}
+
+// Compile with closure compiler and the following flags:
+//
+// --compilation_level WHITESPACE_ONLY
+// --source_map_format V3
+// --create_source_map code_math.map
+// --js_output_file code_math.min.js
+//
+// And then append the sourceMappingURL comment directive to code_math.min.js
+// manually.
diff --git a/devtools/client/debugger/test/mochitest/code_math.map b/devtools/client/debugger/test/mochitest/code_math.map
new file mode 100644
index 000000000..474304c39
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/code_math.map
@@ -0,0 +1,8 @@
+{
+"version":3,
+"file":"code_math.min.js",
+"lineCount":1,
+"mappings":"AAAAA,QAASA,IAAG,CAACC,CAAD,CAAIC,CAAJ,CAAOC,CAAP,CAAU,CACpB,IAAIC,OAASH,CAATG,CAAaF,CACjB,OAAOC,EAAA,CAAEC,MAAF,CAFa,CAKtBC,QAASA,IAAG,CAACJ,CAAD,CAAIC,CAAJ,CAAOC,CAAP,CAAU,CACpB,IAAIC,OAASH,CAATG,CAAaF,CACjB,OAAOC,EAAA,CAAEC,MAAF,CAFa,CAKtBE,QAASA,IAAG,CAACL,CAAD,CAAIC,CAAJ,CAAOC,CAAP,CAAU,CACpB,IAAIC,OAASH,CAATG,CAAaF,CACjB,OAAOC,EAAA,CAAEC,MAAF,CAFa,CAKtBG,QAASA,IAAG,CAACN,CAAD,CAAIC,CAAJ,CAAOC,CAAP,CAAU,CACpB,IAAIC,OAASH,CAATG,CAAaF,CACjB,OAAOC,EAAA,CAAEC,MAAF,CAFa,CAKtBI,QAASA,WAAU,EAAG,CACpBR,GAAA,CAAI,CAAJ,CAAO,CAAP,CAAU,QAAS,CAACC,CAAD,CAAI,CAErBI,GAAA,CAAIJ,CAAJ,CAAO,CAAP,CAAU,QAAS,CAACC,CAAD,CAAI,CAErBI,GAAA,CAAIJ,CAAJ,CAAO,CAAP,CAAU,QAAS,CAACO,CAAD,CAAI,CAErBF,GAAA,CAAIE,CAAJ,CAAO,CAAP,CAAU,QAAS,CAACC,CAAD,CAAI,CAErBC,OAAAC,IAAA,CAAYF,CAAZ,CAFqB,CAAvB,CAFqB,CAAvB,CAFqB,CAAvB,CAFqB,CAAvB,CADoB;",
+"sources":["code_math.js"],
+"names":["add","a","b","k","result","sub","mul","div","arithmetic","c","d","console","log"]
+}
diff --git a/devtools/client/debugger/test/mochitest/code_math.min.js b/devtools/client/debugger/test/mochitest/code_math.min.js
new file mode 100644
index 000000000..7d1fb48f0
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/code_math.min.js
@@ -0,0 +1,2 @@
+function add(a,b,k){var result=a+b;return k(result)}function sub(a,b,k){var result=a-b;return k(result)}function mul(a,b,k){var result=a*b;return k(result)}function div(a,b,k){var result=a/b;return k(result)}function arithmetic(){add(4,4,function(a){sub(a,2,function(b){mul(b,3,function(c){div(c,2,function(d){console.log(d)})})})})};
+//@ sourceMappingURL=code_math.map
diff --git a/devtools/client/debugger/test/mochitest/code_math_bogus_map.js b/devtools/client/debugger/test/mochitest/code_math_bogus_map.js
new file mode 100644
index 000000000..82e156b10
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/code_math_bogus_map.js
@@ -0,0 +1,4 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+function stopMe(){throw Error("boom");}try{stopMe();var a=1;a=a*2;}catch(e){};
+//# sourceMappingURL=bogus.map
diff --git a/devtools/client/debugger/test/mochitest/code_same-line-functions.js b/devtools/client/debugger/test/mochitest/code_same-line-functions.js
new file mode 100644
index 000000000..60a6c6ab1
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/code_same-line-functions.js
@@ -0,0 +1 @@
+function first() { var a = "first"; second(); function second() { var a = "second"; } }
diff --git a/devtools/client/debugger/test/mochitest/code_script-eval.js b/devtools/client/debugger/test/mochitest/code_script-eval.js
new file mode 100644
index 000000000..0d7ceba66
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/code_script-eval.js
@@ -0,0 +1,14 @@
+
+var bar;
+
+function evalSource() {
+ eval("bar = function() {\nvar x = 5;\n}");
+}
+
+function evalSourceWithSourceURL() {
+ eval("bar = function() {\nvar x = 6;\n} //# sourceURL=bar.js");
+}
+
+function evalSourceWithDebugger() {
+ eval("bar = function() {\nvar x = 7;\ndebugger; }\n bar();");
+}
diff --git a/devtools/client/debugger/test/mochitest/code_script-switching-01.js b/devtools/client/debugger/test/mochitest/code_script-switching-01.js
new file mode 100644
index 000000000..4ba2772de
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/code_script-switching-01.js
@@ -0,0 +1,6 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function firstCall() {
+ secondCall();
+}
diff --git a/devtools/client/debugger/test/mochitest/code_script-switching-02.js b/devtools/client/debugger/test/mochitest/code_script-switching-02.js
new file mode 100644
index 000000000..feb74315f
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/code_script-switching-02.js
@@ -0,0 +1,13 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function secondCall() {
+ // This comment is useful: ☺
+ debugger;
+ function foo() {}
+ if (x) {
+ foo();
+ }
+}
+
+var x = true;
diff --git a/devtools/client/debugger/test/mochitest/code_test-editor-mode b/devtools/client/debugger/test/mochitest/code_test-editor-mode
new file mode 100644
index 000000000..ca8a90889
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/code_test-editor-mode
@@ -0,0 +1,6 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function secondCall() {
+ debugger;
+}
diff --git a/devtools/client/debugger/test/mochitest/code_ugly-2.js b/devtools/client/debugger/test/mochitest/code_ugly-2.js
new file mode 100644
index 000000000..15fba0701
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/code_ugly-2.js
@@ -0,0 +1 @@
+function main2() { var a = 1 + 3; var b = a++; return b + a; }
diff --git a/devtools/client/debugger/test/mochitest/code_ugly-3.js b/devtools/client/debugger/test/mochitest/code_ugly-3.js
new file mode 100644
index 000000000..0424b288c
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/code_ugly-3.js
@@ -0,0 +1 @@
+function main3() { var a = 1; debugger; noop(a); return 10; };
diff --git a/devtools/client/debugger/test/mochitest/code_ugly-4.js b/devtools/client/debugger/test/mochitest/code_ugly-4.js
new file mode 100644
index 000000000..dbd596dc3
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/code_ugly-4.js
@@ -0,0 +1,25 @@
+function a(){b()}function b(){debugger}
+//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYWJjLmpzIiwic291cmNlcyI6WyJkYXRhOnRleHQvamF2YXNjcmlwdCxmdW5jdGlvbiBhKCl7YigpfSIsImRhdGE6dGV4dC9qYXZhc2NyaXB0LGZ1bmN0aW9uIGIoKXtkZWJ1Z2dlcn0iXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsaUJDQUEsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMifQ==
+
+// Generate this file by evaluating the following in a browser-environment
+// scratchpad:
+//
+// let { require } = Components.utils.import('resource://devtools/shared/Loader.jsm', {});
+// let { SourceNode } = require("source-map");
+//
+// let dataUrl = s => "data:text/javascript," + s;
+//
+// let A = "function a(){b()}";
+// let A_URL = dataUrl(A);
+// let B = "function b(){debugger}";
+// let B_URL = dataUrl(B);
+//
+// let result = (new SourceNode(null, null, null, [
+// new SourceNode(1, 0, A_URL, A),
+// B.split("").map((ch, i) => new SourceNode(1, i, B_URL, ch))
+// ])).toStringWithSourceMap({
+// file: "abc.js"
+// });
+//
+// result.code + "\n//# " + "sourceMappingURL=data:application/json;base64," + btoa(JSON.stringify(result.map));
+
diff --git a/devtools/client/debugger/test/mochitest/code_ugly-5.js b/devtools/client/debugger/test/mochitest/code_ugly-5.js
new file mode 100644
index 000000000..a94f521dc
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/code_ugly-5.js
@@ -0,0 +1,14 @@
+/*1385419625,181944095,JIT Construction: v1021776,en_US*/
+/**
+ * Copyright Test Inc.
+ *
+ * Licensed under the Apache License, Version 2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
+ */
+// Copyright Test Inc.
+//
+// etc...
+// etc...
+function foo(){var a=1;var b=2;bar(a,b);}
+function bar(c,d){return 3;}
+foo();
diff --git a/devtools/client/debugger/test/mochitest/code_ugly-6.js b/devtools/client/debugger/test/mochitest/code_ugly-6.js
new file mode 100644
index 000000000..0c678c140
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/code_ugly-6.js
@@ -0,0 +1,5 @@
+// Copyright Test Inc.
+//
+// etc...
+// etc...
+function main(){ return 0; } \ No newline at end of file
diff --git a/devtools/client/debugger/test/mochitest/code_ugly-7.js b/devtools/client/debugger/test/mochitest/code_ugly-7.js
new file mode 100644
index 000000000..8ce53b305
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/code_ugly-7.js
@@ -0,0 +1,5 @@
+// Copyright Test Inc.
+//
+// etc...
+// etc...
+function foo(){}; foo(); \ No newline at end of file
diff --git a/devtools/client/debugger/test/mochitest/code_ugly-8 b/devtools/client/debugger/test/mochitest/code_ugly-8
new file mode 100644
index 000000000..dc0d18500
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/code_ugly-8
@@ -0,0 +1,3 @@
+function foo() { var a=1; var b=2; bar(a, b); }
+function bar(c, d) { debugger; }
+foo();
diff --git a/devtools/client/debugger/test/mochitest/code_ugly-8^headers^ b/devtools/client/debugger/test/mochitest/code_ugly-8^headers^
new file mode 100644
index 000000000..a17a9a3a1
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/code_ugly-8^headers^
@@ -0,0 +1 @@
+Content-Type: application/javascript
diff --git a/devtools/client/debugger/test/mochitest/code_ugly.js b/devtools/client/debugger/test/mochitest/code_ugly.js
new file mode 100644
index 000000000..dc0d18500
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/code_ugly.js
@@ -0,0 +1,3 @@
+function foo() { var a=1; var b=2; bar(a, b); }
+function bar(c, d) { debugger; }
+foo();
diff --git a/devtools/client/debugger/test/mochitest/code_worker-source-map.coffee b/devtools/client/debugger/test/mochitest/code_worker-source-map.coffee
new file mode 100644
index 000000000..446e3aefe
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/code_worker-source-map.coffee
@@ -0,0 +1,22 @@
+# Uses a binary search algorithm to locate a value in the specified array.
+binary_search = (items, value) ->
+
+ start = 0
+ stop = items.length - 1
+ pivot = Math.floor (start + stop) / 2
+
+ while items[pivot] isnt value and start < stop
+
+ # Adjust the search area.
+ stop = pivot - 1 if value < items[pivot]
+ start = pivot + 1 if value > items[pivot]
+
+ # Recalculate the pivot.
+ pivot = Math.floor (stop + start) / 2
+
+ # Make sure we've found the correct value.
+ if items[pivot] is value then pivot else -1
+
+self.onmessage = (event) ->
+ data = event.data
+ binary_search(data.items, data.value)
diff --git a/devtools/client/debugger/test/mochitest/code_worker-source-map.js b/devtools/client/debugger/test/mochitest/code_worker-source-map.js
new file mode 100644
index 000000000..9a9f541a9
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/code_worker-source-map.js
@@ -0,0 +1,35 @@
+// Generated by CoffeeScript 1.10.0
+(function() {
+ var binary_search;
+
+ binary_search = function(items, value) {
+ var pivot, start, stop;
+ start = 0;
+ stop = items.length - 1;
+ pivot = Math.floor((start + stop) / 2);
+ while (items[pivot] !== value && start < stop) {
+ if (value < items[pivot]) {
+ stop = pivot - 1;
+ }
+ if (value > items[pivot]) {
+ start = pivot + 1;
+ }
+ pivot = Math.floor((stop + start) / 2);
+ }
+ if (items[pivot] === value) {
+ return pivot;
+ } else {
+ return -1;
+ }
+ };
+
+ self.onmessage = function(event) {
+ console.log("EUTA");
+ var data;
+ data = event.data;
+ return binary_search(data.items, data.value);
+ };
+
+}).call(this);
+
+//# sourceMappingURL=code_worker-source-map.js.map
diff --git a/devtools/client/debugger/test/mochitest/code_worker-source-map.js.map b/devtools/client/debugger/test/mochitest/code_worker-source-map.js.map
new file mode 100644
index 000000000..97c801a58
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/code_worker-source-map.js.map
@@ -0,0 +1,10 @@
+{
+ "version": 3,
+ "file": "code_worker-source-map.js",
+ "sourceRoot": "",
+ "sources": [
+ "code_worker-source-map.coffee"
+ ],
+ "names": [],
+ "mappings": ";AACA;AAAA,MAAA;;EAAA,aAAA,GAAgB,SAAC,KAAD,EAAQ,KAAR;AAEd,QAAA;IAAA,KAAA,GAAQ;IACR,IAAA,GAAQ,KAAK,CAAC,MAAN,GAAe;IACvB,KAAA,GAAQ,IAAI,CAAC,KAAL,CAAW,CAAC,KAAA,GAAQ,IAAT,CAAA,GAAiB,CAA5B;AAER,WAAM,KAAM,CAAA,KAAA,CAAN,KAAkB,KAAlB,IAA4B,KAAA,GAAQ,IAA1C;MAGE,IAAqB,KAAA,GAAQ,KAAM,CAAA,KAAA,CAAnC;QAAA,IAAA,GAAQ,KAAA,GAAQ,EAAhB;;MACA,IAAqB,KAAA,GAAQ,KAAM,CAAA,KAAA,CAAnC;QAAA,KAAA,GAAQ,KAAA,GAAQ,EAAhB;;MAGA,KAAA,GAAQ,IAAI,CAAC,KAAL,CAAW,CAAC,IAAA,GAAO,KAAR,CAAA,GAAiB,CAA5B;IAPV;IAUA,IAAG,KAAM,CAAA,KAAA,CAAN,KAAgB,KAAnB;aAA8B,MAA9B;KAAA,MAAA;aAAyC,CAAC,EAA1C;;EAhBc;;EAkBhB,IAAI,CAAC,SAAL,GAAiB,SAAC,KAAD;AACf,QAAA;IAAA,IAAA,GAAO,KAAK,CAAC;WACb,aAAA,CAAc,IAAI,CAAC,KAAnB,EAA0B,IAAI,CAAC,KAA/B;EAFe;AAlBjB"
+}
diff --git a/devtools/client/debugger/test/mochitest/code_workeractor-worker.js b/devtools/client/debugger/test/mochitest/code_workeractor-worker.js
new file mode 100644
index 000000000..18f14864d
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/code_workeractor-worker.js
@@ -0,0 +1,5 @@
+"use strict";
+
+self.onmessage = function () {};
+
+postMessage("load");
diff --git a/devtools/client/debugger/test/mochitest/doc_WorkerActor.attach-tab1.html b/devtools/client/debugger/test/mochitest/doc_WorkerActor.attach-tab1.html
new file mode 100644
index 000000000..62ab9be7d
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_WorkerActor.attach-tab1.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8"/>
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/devtools/client/debugger/test/mochitest/doc_WorkerActor.attach-tab2.html b/devtools/client/debugger/test/mochitest/doc_WorkerActor.attach-tab2.html
new file mode 100644
index 000000000..62ab9be7d
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_WorkerActor.attach-tab2.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8"/>
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/devtools/client/debugger/test/mochitest/doc_WorkerActor.attachThread-tab.html b/devtools/client/debugger/test/mochitest/doc_WorkerActor.attachThread-tab.html
new file mode 100644
index 000000000..62ab9be7d
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_WorkerActor.attachThread-tab.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8"/>
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/devtools/client/debugger/test/mochitest/doc_auto-pretty-print-01.html b/devtools/client/debugger/test/mochitest/doc_auto-pretty-print-01.html
new file mode 100644
index 000000000..dee2d52f2
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_auto-pretty-print-01.html
@@ -0,0 +1,14 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <title>Auto Pretty Printing Test Page</title>
+ </head>
+ <body>
+ <script src="code_ugly-5.js"></script>
+ <script src="code_ugly-6.js"></script>
+ </body>
+</html> \ No newline at end of file
diff --git a/devtools/client/debugger/test/mochitest/doc_auto-pretty-print-02.html b/devtools/client/debugger/test/mochitest/doc_auto-pretty-print-02.html
new file mode 100644
index 000000000..e96a63d9e
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_auto-pretty-print-02.html
@@ -0,0 +1,14 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <title>Auto Pretty Printing Test Page</title>
+ </head>
+ <body>
+ <script src="code_ugly-6.js"></script>
+ <script src="code_ugly-7.js"></script>
+ </body>
+</html> \ No newline at end of file
diff --git a/devtools/client/debugger/test/mochitest/doc_binary_search.html b/devtools/client/debugger/test/mochitest/doc_binary_search.html
new file mode 100644
index 000000000..803106fc5
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_binary_search.html
@@ -0,0 +1,15 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Debugger test page</title>
+ </head>
+
+ <body>
+ <script type="text/javascript" src="code_binary_search.js"></script>
+ </body>
+
+</html>
diff --git a/devtools/client/debugger/test/mochitest/doc_blackboxing.html b/devtools/client/debugger/test/mochitest/doc_blackboxing.html
new file mode 100644
index 000000000..a83b16de5
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_blackboxing.html
@@ -0,0 +1,26 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Debugger test page</title>
+ </head>
+
+ <body>
+ <script type="text/javascript" src="code_blackboxing_blackboxme.js"></script>
+ <script type="text/javascript" src="code_blackboxing_one.js"></script>
+ <script type="text/javascript" src="code_blackboxing_two.js"></script>
+ <script type="text/javascript" src="code_blackboxing_three.js"></script>
+ <script>
+ function runTest() {
+ blackboxme(doDebuggerStatement);
+ }
+ function doDebuggerStatement() {
+ debugger;
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/debugger/test/mochitest/doc_blackboxing_unblackbox.html b/devtools/client/debugger/test/mochitest/doc_blackboxing_unblackbox.html
new file mode 100644
index 000000000..3b26b25e3
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_blackboxing_unblackbox.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Debugger test page</title>
+ <script type="text/javascript" src="code_blackboxing_unblackbox.min.js"></script>
+</head>
+<body>
+
+</body>
+</html> \ No newline at end of file
diff --git a/devtools/client/debugger/test/mochitest/doc_breakpoint-move.html b/devtools/client/debugger/test/mochitest/doc_breakpoint-move.html
new file mode 100644
index 000000000..5124bbbcf
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_breakpoint-move.html
@@ -0,0 +1,25 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Debugger test page</title>
+ </head>
+
+ <body>
+ <button onclick="ermahgerd()">Click me!</button>
+
+ <script type="text/javascript">
+ function ermahgerd() {
+ debugger;
+ // This is just a line
+ // and here we are
+ var x = 5;
+ return x;
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/debugger/test/mochitest/doc_breakpoints-break-on-last-line-of-script-on-reload.html b/devtools/client/debugger/test/mochitest/doc_breakpoints-break-on-last-line-of-script-on-reload.html
new file mode 100644
index 000000000..c1730e506
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_breakpoints-break-on-last-line-of-script-on-reload.html
@@ -0,0 +1,8 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE html>
+<head>
+ <meta charset="utf-8"/>
+ <title>Debugger Break on Last Line of Script on Reload Test Page</title>
+</head>
+<script src="code_breakpoints-break-on-last-line-of-script-on-reload.js"></script>
diff --git a/devtools/client/debugger/test/mochitest/doc_breakpoints-other-tabs.html b/devtools/client/debugger/test/mochitest/doc_breakpoints-other-tabs.html
new file mode 100644
index 000000000..4273dbdd8
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_breakpoints-other-tabs.html
@@ -0,0 +1,8 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE html>
+<head>
+ <meta charset="utf-8"/>
+ <title>Debugger Breakpoints Other Tabs Test Page</title>
+</head>
+<script src="code_breakpoints-other-tabs.js"></script>
diff --git a/devtools/client/debugger/test/mochitest/doc_breakpoints-reload.html b/devtools/client/debugger/test/mochitest/doc_breakpoints-reload.html
new file mode 100644
index 000000000..0c6059c6d
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_breakpoints-reload.html
@@ -0,0 +1,13 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE html>
+<head>
+ <meta charset="utf-8"/>
+ <title>Debugger Breakpoints Other Tabs Test Page</title>
+</head>
+<script>
+ function theTest() {
+ window.foo = "break on me";
+ }
+ theTest();
+</script>
diff --git a/devtools/client/debugger/test/mochitest/doc_bug-896139.html b/devtools/client/debugger/test/mochitest/doc_bug-896139.html
new file mode 100644
index 000000000..166ad604f
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_bug-896139.html
@@ -0,0 +1,18 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8"/>
+ <script>
+ window.onload = function () {
+ var script = document.createElement("script");
+ script.setAttribute("src", "code_bug-896139.js");
+ document.body.appendChild(script);
+ }
+ </script>
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/devtools/client/debugger/test/mochitest/doc_closure-optimized-out.html b/devtools/client/debugger/test/mochitest/doc_closure-optimized-out.html
new file mode 100644
index 000000000..3ad4e8fc0
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_closure-optimized-out.html
@@ -0,0 +1,34 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset='utf-8'/>
+ <title>Debugger Test for Inspecting Optimized-Out Variables</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <script type="text/javascript">
+ window.addEventListener("load", function onload() {
+ window.removeEventListener("load", onload);
+ function clickHandler(event) {
+ button.removeEventListener("click", clickHandler, false);
+ function outer(arg) {
+ var upvar = arg * 2;
+ // The inner lambda only aliases arg, so the frontend alias analysis decides
+ // that upvar is not aliased and is not in the CallObject.
+ return function () {
+ arg += 2;
+ };
+ }
+
+ var f = outer(42);
+ f();
+ }
+ var button = document.querySelector("button");
+ button.addEventListener("click", clickHandler, false);
+ });
+ </script>
+
+ </head>
+ <body>
+ <button>Click me!</button>
+ </body>
+</html>
diff --git a/devtools/client/debugger/test/mochitest/doc_closures.html b/devtools/client/debugger/test/mochitest/doc_closures.html
new file mode 100644
index 000000000..1ba91601a
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_closures.html
@@ -0,0 +1,32 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset='utf-8'/>
+ <title>Debugger Test for Closure Inspection</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <script type="text/javascript">
+ window.addEventListener("load", function onload() {
+ window.removeEventListener("load", onload);
+ function clickHandler(event) {
+ button.removeEventListener("click", clickHandler, false);
+ var PersonFactory = function _pfactory(name) {
+ var foo = 10;
+ return {
+ getName: function() { return name; },
+ getFoo: function() { foo = Date.now(); return foo; }
+ };
+ };
+ var person = new PersonFactory("Bob");
+ debugger;
+ }
+ var button = document.querySelector("button");
+ button.addEventListener("click", clickHandler, false);
+ });
+ </script>
+
+ </head>
+ <body>
+ <button>Click me!</button>
+ </body>
+</html>
diff --git a/devtools/client/debugger/test/mochitest/doc_cmd-break.html b/devtools/client/debugger/test/mochitest/doc_cmd-break.html
new file mode 100644
index 000000000..4f434746e
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_cmd-break.html
@@ -0,0 +1,22 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Debugger test page</title>
+ </head>
+
+ <body>
+ <script type="text/javascript">
+ function firstCall() {
+ window.gLineNumber = Error().lineNumber; secondCall();
+ }
+ function secondCall() {
+ debugger;
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/debugger/test/mochitest/doc_cmd-dbg.html b/devtools/client/debugger/test/mochitest/doc_cmd-dbg.html
new file mode 100644
index 000000000..5ab41eb1b
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_cmd-dbg.html
@@ -0,0 +1,40 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Debugger test page</title>
+ </head>
+
+ <body>
+ <input type="text" value=""/>
+ <input type="button" value="Click me!" onclick="test()"/>
+
+ <script type="application/javascript;version=1.7">
+ let output = document.querySelector("input");
+ output.value = "";
+
+ function test() {
+ debugger;
+ stepIntoMe(); // step in
+
+ output.value = "dbg continue";
+ debugger;
+ }
+
+ function stepIntoMe() {
+ output.value = "step in"; // step in
+ stepOverMe(); // step over
+ let x = 0; // step out
+ output.value = "step out";
+ }
+
+ function stepOverMe() {
+ output.value = "step over";
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/debugger/test/mochitest/doc_conditional-breakpoints.html b/devtools/client/debugger/test/mochitest/doc_conditional-breakpoints.html
new file mode 100644
index 000000000..7adce7a18
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_conditional-breakpoints.html
@@ -0,0 +1,35 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Debugger test page</title>
+ </head>
+
+ <body>
+ <button onclick="ermahgerd()">Click me!</button>
+
+ <script type="text/javascript">
+ function ermahgerd() {
+ var a = {};
+ debugger;
+ a = "undefined";
+ a = "null";
+ a = "42";
+ a = "true";
+ a = "'nasu'";
+ a = "/regexp/";
+ a = "{}";
+ a = "function() {}";
+ a = "(function { return false; })()";
+ a = "a";
+ a = "a !== undefined";
+ a = "a !== null";
+ a = "b";
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/debugger/test/mochitest/doc_domnode-variables.html b/devtools/client/debugger/test/mochitest/doc_domnode-variables.html
new file mode 100644
index 000000000..9e7531036
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_domnode-variables.html
@@ -0,0 +1,24 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Debugger test page</title>
+ </head>
+
+ <body>
+ <div>Look at this DIV! Just look at it!</div>
+
+ <script type="text/javascript">
+ function start() {
+ var theDiv = document.querySelector("div");
+ var theBody = document.body;
+ var manyDomNodes = [theDiv, theBody];
+ debugger;
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/debugger/test/mochitest/doc_editor-mode.html b/devtools/client/debugger/test/mochitest/doc_editor-mode.html
new file mode 100644
index 000000000..8e3573cea
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_editor-mode.html
@@ -0,0 +1,20 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Debugger test page</title>
+ </head>
+
+ <body>
+ <script type="text/javascript" src="code_script-switching-01.js?a=b"></script>
+ <script type="text/javascript" src="code_test-editor-mode?c=d"></script>
+ <script type="text/javascript">
+ function banana() {
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/debugger/test/mochitest/doc_empty-tab-01.html b/devtools/client/debugger/test/mochitest/doc_empty-tab-01.html
new file mode 100644
index 000000000..28398f776
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_empty-tab-01.html
@@ -0,0 +1,14 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Empty test page 1</title>
+ </head>
+
+ <body>
+ </body>
+
+</html>
diff --git a/devtools/client/debugger/test/mochitest/doc_empty-tab-02.html b/devtools/client/debugger/test/mochitest/doc_empty-tab-02.html
new file mode 100644
index 000000000..5db150844
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_empty-tab-02.html
@@ -0,0 +1,14 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Empty test page 2</title>
+ </head>
+
+ <body>
+ </body>
+
+</html>
diff --git a/devtools/client/debugger/test/mochitest/doc_event-listeners-01.html b/devtools/client/debugger/test/mochitest/doc_event-listeners-01.html
new file mode 100644
index 000000000..b44400311
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_event-listeners-01.html
@@ -0,0 +1,43 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Debugger test page</title>
+ </head>
+
+ <body>
+ <button>Click me!</button>
+ <input type="text" onchange="changeHandler()">
+
+ <script type="text/javascript">
+ window.addEventListener("load", function onload() {
+ window.removeEventListener("load", onload);
+ function initialSetup(event) {
+ debugger;
+ var button = document.querySelector("button");
+ button.onclick = clickHandler;
+ }
+ function clickHandler(event) {
+ window.foobar = "clickHandler";
+ }
+ function changeHandler(event) {
+ window.foobar = "changeHandler";
+ }
+ function keyupHandler(event) {
+ window.foobar = "keyupHandler";
+ }
+
+ var button = document.querySelector("button");
+ button.onclick = initialSetup;
+
+ var input = document.querySelector("input");
+ input.addEventListener("keyup", keyupHandler, true);
+
+ window.changeHandler = changeHandler;
+ });
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/debugger/test/mochitest/doc_event-listeners-02.html b/devtools/client/debugger/test/mochitest/doc_event-listeners-02.html
new file mode 100644
index 000000000..6a4649de9
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_event-listeners-02.html
@@ -0,0 +1,53 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Debugger test page</title>
+ </head>
+
+ <body>
+ <button>Click me!</button>
+ <input type="text" onchange="changeHandler()">
+
+ <script type="text/javascript">
+ window.addEventListener("load", function onload() {
+ window.removeEventListener("load", onload);
+ function initialSetup(event) {
+ debugger;
+ var button = document.querySelector("button");
+ button.onclick = clickHandler;
+ }
+ function clickHandler(event) {
+ window.foobar = "clickHandler";
+ }
+ function changeHandler(event) {
+ window.foobar = "changeHandler";
+ }
+ function keyupHandler(event) {
+ window.foobar = "keyupHandler";
+ }
+ function keydownHandler(event) {
+ window.foobar = "keydownHandler";
+ }
+
+ var button = document.querySelector("button");
+ button.onclick = initialSetup;
+
+ var input = document.querySelector("input");
+ input.addEventListener("keyup", keyupHandler, true);
+
+ window.addEventListener("keydown", keydownHandler, true);
+ document.body.addEventListener("keydown", keydownHandler, true);
+
+ window.changeHandler = changeHandler;
+ });
+
+ function addBodyClickEventListener() {
+ document.body.addEventListener("click", function() { debugger; });
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/debugger/test/mochitest/doc_event-listeners-03.html b/devtools/client/debugger/test/mochitest/doc_event-listeners-03.html
new file mode 100644
index 000000000..b672a4360
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_event-listeners-03.html
@@ -0,0 +1,63 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Bound event listeners test page</title>
+ </head>
+
+ <body>
+ <button id="initialSetup">initialSetup</button>
+ <button id="clicker">clicker</button>
+ <button id="handleEventClick">handleEventClick</button>
+ <button id="boundHandleEventClick">boundHandleEventClick</button>
+
+ <script type="text/javascript">
+ window.addEventListener("load", function onload() {
+ window.removeEventListener("load", onload);
+ function initialSetup(event) {
+ var button = document.getElementById("initialSetup");
+ button.removeEventListener("click", initialSetup);
+ debugger;
+ }
+
+ function clicker(event) {
+ window.foobar = "clicker";
+ }
+
+ function handleEventClick() {
+ var button = document.getElementById("handleEventClick");
+ // Create a long prototype chain to test for weird edge cases.
+ button.addEventListener("click", Object.create(Object.create(this)));
+ }
+
+ handleEventClick.prototype.handleEvent = function() {
+ window.foobar = "handleEventClick";
+ };
+
+ function boundHandleEventClick() {
+ var button = document.getElementById("boundHandleEventClick");
+ this.handleEvent = this.handleEvent.bind(this);
+ button.addEventListener("click", this);
+ }
+
+ boundHandleEventClick.prototype.handleEvent = function() {
+ window.foobar = "boundHandleEventClick";
+ };
+
+ var button = document.getElementById("clicker");
+ // Bind more than once to test for weird edge cases.
+ var boundClicker = clicker.bind(this).bind(this).bind(this);
+ button.addEventListener("click", boundClicker);
+
+ new handleEventClick();
+ new boundHandleEventClick();
+
+ var initButton = document.getElementById("initialSetup");
+ initButton.addEventListener("click", initialSetup);
+ });
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/debugger/test/mochitest/doc_event-listeners-04.html b/devtools/client/debugger/test/mochitest/doc_event-listeners-04.html
new file mode 100644
index 000000000..d92488a70
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_event-listeners-04.html
@@ -0,0 +1,23 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Debugger test page</title>
+ </head>
+
+ <body>
+ <button>Click me!</button>
+
+ <script type="text/javascript">
+ window.addEventListener("load", function onload() {
+ var button = document.querySelector("button");
+ button.onclick = function () {
+ debugger;
+ };
+ });
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/debugger/test/mochitest/doc_frame-parameters.html b/devtools/client/debugger/test/mochitest/doc_frame-parameters.html
new file mode 100644
index 000000000..b3108d6bf
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_frame-parameters.html
@@ -0,0 +1,37 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Debugger test page</title>
+ </head>
+
+ <body>
+ <button onclick="start()">Click me!</button>
+
+ <script type="text/javascript">
+ function test(aArg, bArg, cArg, dArg, eArg, fArg) {
+ var a = 1;
+ var b = { a: a };
+ var c = { a: 1, b: "beta", c: 3, d: false, e: null, f: fArg };
+ var myVar = {
+ _prop: 42,
+ get prop() { return this._prop; },
+ set prop(val) { this._prop = val; }
+ };
+ debugger;
+ }
+
+ function start() {
+ var a = { a: 1, b: "beta", c: 3, d: false, e: null, f: undefined };
+ var e = eval("test(a, 'beta', 3, false, null);");
+ }
+
+ var button = document.querySelector("button");
+ var buttonAsProto = Object.create(button);
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/debugger/test/mochitest/doc_function-display-name.html b/devtools/client/debugger/test/mochitest/doc_function-display-name.html
new file mode 100644
index 000000000..84e8ce6e1
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_function-display-name.html
@@ -0,0 +1,31 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Debugger test page</title>
+ </head>
+
+ <body>
+ <script type="text/javascript">
+ var a = function() {
+ return function() {
+ debugger;
+ }
+ }
+
+ var anon = a();
+ anon.displayName = "anonFunc";
+
+ var inferred = a();
+
+ function evalCall() {
+ eval("anon();");
+ eval("inferred();");
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/debugger/test/mochitest/doc_function-jump.html b/devtools/client/debugger/test/mochitest/doc_function-jump.html
new file mode 100644
index 000000000..0cd99e662
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_function-jump.html
@@ -0,0 +1,17 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Debugger test page</title>
+ </head>
+
+ <body>
+ <p>Foo bar, bar, bazz, bar foo bar!</p>
+
+ <script type="text/javascript" src="code_function-jump-01.js"></script>
+ </body>
+
+</html>
diff --git a/devtools/client/debugger/test/mochitest/doc_function-search.html b/devtools/client/debugger/test/mochitest/doc_function-search.html
new file mode 100644
index 000000000..eb0e7eaea
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_function-search.html
@@ -0,0 +1,30 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Debugger test page</title>
+ </head>
+
+ <body>
+ <p>Peanut butter jelly time!</p>
+
+ <script type="text/javascript" src="code_function-search-01.js"></script>
+ <script type="text/javascript" src="code_function-search-02.js"></script>
+ <script type="text/javascript" src="code_function-search-03.js"></script>
+
+ <script type="text/javascript;version=1.8">
+ function inline() {}
+ var arrow = () => {}
+
+ var foo = bar => {}
+ var foo2 = bar2 = baz2 => 42;
+
+ setTimeout((foo, bar, baz) => {});
+ setTimeout((foo, bar, baz) => 42);
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/debugger/test/mochitest/doc_global-method-override.html b/devtools/client/debugger/test/mochitest/doc_global-method-override.html
new file mode 100644
index 000000000..d8cf750fc
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_global-method-override.html
@@ -0,0 +1,16 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Debugger global method override test page</title>
+ </head>
+ <body>
+ <script type="text/javascript">
+ console.log( "Error: " + toString( { x: 0, y: 0 } ) );
+ function toString(v) { return "[ " + v.x + ", " + v.y + " ]"; }
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/debugger/test/mochitest/doc_iframes.html b/devtools/client/debugger/test/mochitest/doc_iframes.html
new file mode 100644
index 000000000..e5a76c280
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_iframes.html
@@ -0,0 +1,15 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Debugger test page</title>
+ </head>
+
+ <body>
+ <iframe src="doc_inline-debugger-statement.html"></iframe>
+ </body>
+
+</html>
diff --git a/devtools/client/debugger/test/mochitest/doc_included-script.html b/devtools/client/debugger/test/mochitest/doc_included-script.html
new file mode 100644
index 000000000..8b134dd42
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_included-script.html
@@ -0,0 +1,22 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Debugger test page</title>
+ </head>
+
+ <body>
+ <button onclick="myFunction()">Click me!</button>
+
+ <script type="text/javascript" src="code_location-changes.js"></script>
+ <script type="text/javascript">
+ function runDebuggerStatement() {
+ debugger;
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/debugger/test/mochitest/doc_inline-debugger-statement.html b/devtools/client/debugger/test/mochitest/doc_inline-debugger-statement.html
new file mode 100644
index 000000000..406e9d9da
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_inline-debugger-statement.html
@@ -0,0 +1,21 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Debugger test page</title>
+ </head>
+
+ <body>
+ <button>Click me!</button>
+
+ <script type="text/javascript">
+ function runDebuggerStatement() {
+ debugger;
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/debugger/test/mochitest/doc_inline-script.html b/devtools/client/debugger/test/mochitest/doc_inline-script.html
new file mode 100644
index 000000000..d071cc084
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_inline-script.html
@@ -0,0 +1,25 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Debugger test page</title>
+ </head>
+
+ <body>
+ <button onclick="myFunction()">Click me!</button>
+
+ <script type="text/javascript">
+ function runDebuggerStatement() {
+ debugger;
+ }
+ function myFunction() {
+ var a = 1;
+ debugger;
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/debugger/test/mochitest/doc_large-array-buffer.html b/devtools/client/debugger/test/mochitest/doc_large-array-buffer.html
new file mode 100644
index 000000000..25b1b4d4e
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_large-array-buffer.html
@@ -0,0 +1,32 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Debugger test page</title>
+ </head>
+
+ <body>
+ <button onclick="test(10000)">Click me!</button>
+
+ <script type="text/javascript">
+ function test(aNumber) {
+ var buffer = new ArrayBuffer(aNumber);
+ var largeArray = new Int8Array(buffer);
+ var largeObject = {};
+ var largeMap = new Map();
+ var largeSet = new Set();
+
+ for (var i = 0; i < aNumber; i++) {
+ let value = aNumber - i - 1;
+ largeObject[i] = value;
+ largeMap.set(i, value);
+ largeSet.add(value);
+ }
+ debugger;
+ }
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/debugger/test/mochitest/doc_listworkers-tab.html b/devtools/client/debugger/test/mochitest/doc_listworkers-tab.html
new file mode 100644
index 000000000..62ab9be7d
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_listworkers-tab.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8"/>
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/devtools/client/debugger/test/mochitest/doc_map-set.html b/devtools/client/debugger/test/mochitest/doc_map-set.html
new file mode 100644
index 000000000..7c2c624e1
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_map-set.html
@@ -0,0 +1,42 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Debugger test page for Maps and Sets</title>
+ </head>
+
+ <body>
+ <script>
+ function startTest() {
+ let obj0 = { a: 0 };
+ let obj1 = { a: 1 };
+
+ let map = new Map();
+ map.set(obj0, 0);
+ map.set(obj1, 1);
+ map.extraProp = true;
+
+ let weakMap = new WeakMap();
+ weakMap.set(obj0, 0);
+ weakMap.set(obj1, 1);
+ weakMap.extraProp = true;
+
+ let set = new Set();
+ set.add(obj0);
+ set.add(obj1);
+ set.extraProp = true;
+
+ let weakSet = new WeakSet();
+ weakSet.add(obj0);
+ weakSet.add(obj1);
+ weakSet.extraProp = true;
+
+ debugger;
+ };
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/debugger/test/mochitest/doc_minified.html b/devtools/client/debugger/test/mochitest/doc_minified.html
new file mode 100644
index 000000000..b229e079f
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_minified.html
@@ -0,0 +1,14 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Debugger test page</title>
+ </head>
+
+ <body>
+ <script src="code_math.min.js"></script>
+ </body>
+</html>
diff --git a/devtools/client/debugger/test/mochitest/doc_minified_bogus_map.html b/devtools/client/debugger/test/mochitest/doc_minified_bogus_map.html
new file mode 100644
index 000000000..d6670a7e1
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_minified_bogus_map.html
@@ -0,0 +1,14 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Debugger test page</title>
+ </head>
+
+ <body>
+ <script src="code_math_bogus_map.js"></script>
+ </body>
+</html>
diff --git a/devtools/client/debugger/test/mochitest/doc_native-event-handler.html b/devtools/client/debugger/test/mochitest/doc_native-event-handler.html
new file mode 100644
index 000000000..cd2a656bf
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_native-event-handler.html
@@ -0,0 +1,22 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>A video element with native event handlers</title>
+ <script type="text/javascript">
+ function initialSetup(event) {
+ debugger;
+ }
+
+ window.addEventListener("load", function() {}, false);
+ </script>
+ </head>
+ <body>
+ <button onclick="initialSetup()">Click me!</button>
+ <!-- the "controls" attribute ensures that there are extra event handlers in
+ the element. -->
+ <video controls></video>
+ </body>
+</html>
diff --git a/devtools/client/debugger/test/mochitest/doc_no-page-sources.html b/devtools/client/debugger/test/mochitest/doc_no-page-sources.html
new file mode 100644
index 000000000..5131578ad
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_no-page-sources.html
@@ -0,0 +1,11 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>This page has no sources</title>
+ </head>
+ <body>
+ </body>
+</html> \ No newline at end of file
diff --git a/devtools/client/debugger/test/mochitest/doc_pause-exceptions.html b/devtools/client/debugger/test/mochitest/doc_pause-exceptions.html
new file mode 100644
index 000000000..7766fb49d
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_pause-exceptions.html
@@ -0,0 +1,35 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Debugger test page</title>
+ </head>
+
+ <body>
+ <button>Click me!</button>
+ <ul></ul>
+
+ <script type="text/javascript">
+ window.addEventListener("load", function() {
+ function test() {
+ try {
+ throw new Error("boom");
+ } catch (e) {
+ var list = document.querySelector("ul");
+ var item = document.createElement("li");
+ item.innerHTML = e.message;
+ list.appendChild(item);
+ } finally {
+ debugger;
+ }
+ }
+ var button = document.querySelector("button");
+ button.addEventListener("click", test, false);
+ });
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/debugger/test/mochitest/doc_pretty-print-2.html b/devtools/client/debugger/test/mochitest/doc_pretty-print-2.html
new file mode 100644
index 000000000..509f57d6b
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_pretty-print-2.html
@@ -0,0 +1,15 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE html>
+<head>
+ <meta charset="utf-8"/>
+ <title>Debugger Pretty Printing Test Page</title>
+</head>
+<script src="code_ugly-2.js"></script>
+<script src="code_ugly-3.js"></script>
+<script src="code_ugly-4.js"></script>
+<script>
+ function noop(x) {
+ return x;
+ }
+</script>
diff --git a/devtools/client/debugger/test/mochitest/doc_pretty-print-3.html b/devtools/client/debugger/test/mochitest/doc_pretty-print-3.html
new file mode 100644
index 000000000..6192642f3
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_pretty-print-3.html
@@ -0,0 +1,8 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE html>
+<head>
+ <meta charset="utf-8"/>
+ <title>Debugger Pretty Printing Test Page</title>
+</head>
+<script src="code_ugly-8"></script>
diff --git a/devtools/client/debugger/test/mochitest/doc_pretty-print-on-paused.html b/devtools/client/debugger/test/mochitest/doc_pretty-print-on-paused.html
new file mode 100644
index 000000000..a431d0898
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_pretty-print-on-paused.html
@@ -0,0 +1,14 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <title>Pretty printing when debugger is paused Test Page</title>
+ </head>
+ <body>
+ <script src="code_ugly-2.js"></script>
+ <script src="code_script-switching-02.js"></script>
+ </body>
+</html>
diff --git a/devtools/client/debugger/test/mochitest/doc_pretty-print.html b/devtools/client/debugger/test/mochitest/doc_pretty-print.html
new file mode 100644
index 000000000..dcf595a8d
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_pretty-print.html
@@ -0,0 +1,8 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE html>
+<head>
+ <meta charset="utf-8"/>
+ <title>Debugger Pretty Printing Test Page</title>
+</head>
+<script src="code_ugly.js"></script>
diff --git a/devtools/client/debugger/test/mochitest/doc_promise-get-allocation-stack.html b/devtools/client/debugger/test/mochitest/doc_promise-get-allocation-stack.html
new file mode 100644
index 000000000..48546c967
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_promise-get-allocation-stack.html
@@ -0,0 +1,24 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Promise test page</title>
+ </head>
+
+ <body>
+ <script type="text/javascript">
+ function makePromises() {
+ var p = new Promise(() => {});
+ p.name = "p";
+ var q = p.then();
+ q.name = "q";
+ var r = p.then(null, () => {});
+ r.name = "r";
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/debugger/test/mochitest/doc_promise-get-fulfillment-stack.html b/devtools/client/debugger/test/mochitest/doc_promise-get-fulfillment-stack.html
new file mode 100644
index 000000000..0b311a69a
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_promise-get-fulfillment-stack.html
@@ -0,0 +1,24 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Promise test page</title>
+ </head>
+
+ <body>
+ <script type="text/javascript">
+ function makePromise() {
+ var p = returnPromise();
+ p.name = "p";
+ }
+
+ function returnPromise() {
+ return new Promise(resolve => resolve("hello"));
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/debugger/test/mochitest/doc_promise-get-rejection-stack.html b/devtools/client/debugger/test/mochitest/doc_promise-get-rejection-stack.html
new file mode 100644
index 000000000..9fe203595
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_promise-get-rejection-stack.html
@@ -0,0 +1,24 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Promise test page</title>
+ </head>
+
+ <body>
+ <script type="text/javascript">
+ function makePromise() {
+ var p = returnPromise();
+ p.name = "p";
+ }
+
+ function returnPromise() {
+ return new Promise((resolve, reject) => reject("hello"));
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/debugger/test/mochitest/doc_promise.html b/devtools/client/debugger/test/mochitest/doc_promise.html
new file mode 100644
index 000000000..fe6c1d807
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_promise.html
@@ -0,0 +1,30 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Debugger + Promise test page</title>
+ </head>
+
+ <body>
+ <script>
+ window.pending = new Promise(function () {});
+ window.fulfilled = Promise.resolve({ a: 1, b: 2, c: 3 });
+ window.rejected = Promise.reject(new Error("uh oh"));
+
+ window.doPause = function () {
+ var p = window.pending;
+ var f = window.fulfilled;
+ var r = window.rejected;
+ debugger;
+ };
+
+ // Attach an error handler so that the logs don't have a warning about an
+ // unhandled, rejected promise.
+ window.rejected.then(null, function () {});
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/debugger/test/mochitest/doc_proxy.html b/devtools/client/debugger/test/mochitest/doc_proxy.html
new file mode 100644
index 000000000..e2f35104a
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_proxy.html
@@ -0,0 +1,39 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Debugger + Proxy test page</title>
+ </head>
+
+ <body>
+ <script>
+ window.target = {name: "target"};
+ window.handler = { /* Debugging a proxy shouldn't run any trap */
+ name: "handler",
+ getPrototypeOf() { throw new Error("proxy getPrototypeOf trap was called"); },
+ setPrototypeOf() { throw new Error("proxy setPrototypeOf trap was called"); },
+ isExtensible() { throw new Error("proxy isExtensible trap was called"); },
+ preventExtensions() { throw new Error("proxy preventExtensions trap was called"); },
+ getOwnPropertyDescriptor() { throw new Error("proxy getOwnPropertyDescriptor trap was called"); },
+ defineProperty() { throw new Error("proxy defineProperty trap was called"); },
+ has() { throw new Error("proxy has trap was called"); },
+ get() { throw new Error("proxy get trap was called"); },
+ set() { throw new Error("proxy set trap was called"); },
+ deleteProperty() { throw new Error("proxy deleteProperty trap was called"); },
+ ownKeys() { throw new Error("proxy ownKeys trap was called"); },
+ apply() { throw new Error("proxy apply trap was called"); },
+ construct() { throw new Error("proxy construct trap was called"); }
+ };
+ window.proxy = new Proxy(target, handler);
+
+ window.doPause = function () {
+ var proxy = window.proxy;
+ debugger;
+ };
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/debugger/test/mochitest/doc_random-javascript.html b/devtools/client/debugger/test/mochitest/doc_random-javascript.html
new file mode 100644
index 000000000..69269e409
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_random-javascript.html
@@ -0,0 +1,15 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Debugger test page</title>
+ </head>
+
+ <body>
+ <script src="sjs_random-javascript.sjs"></script>
+ </body>
+
+</html>
diff --git a/devtools/client/debugger/test/mochitest/doc_recursion-stack.html b/devtools/client/debugger/test/mochitest/doc_recursion-stack.html
new file mode 100644
index 000000000..d68fb1d18
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_recursion-stack.html
@@ -0,0 +1,35 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Debugger test page</title>
+ </head>
+
+ <body>
+ <script type="text/javascript">
+ function simpleCall() {
+ debugger;
+ }
+
+ function evalCall() {
+ eval("debugger;");
+ }
+
+ var gRecurseLimit = 100;
+ var gRecurseDepth = 0;
+
+ function recurse() {
+ if (++gRecurseDepth == gRecurseLimit) {
+ debugger;
+ gRecurseDepth = 0;
+ return;
+ }
+ recurse();
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/debugger/test/mochitest/doc_scope-variable-2.html b/devtools/client/debugger/test/mochitest/doc_scope-variable-2.html
new file mode 100644
index 000000000..afbfd166a
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_scope-variable-2.html
@@ -0,0 +1,30 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Debugger test page</title>
+ </head>
+
+ <body>
+ <script type="text/javascript">
+ function test() {
+ var a = "first scope";
+ firstNest();
+
+ function firstNest() {
+ var a = "second scope";
+ secondNest();
+
+ function secondNest() {
+ var a = "third scope";
+ debugger;
+ }
+ }
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/debugger/test/mochitest/doc_scope-variable-3.html b/devtools/client/debugger/test/mochitest/doc_scope-variable-3.html
new file mode 100644
index 000000000..fcd45cc0a
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_scope-variable-3.html
@@ -0,0 +1,23 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Debugger test page</title>
+ </head>
+
+ <body>
+ <script type="text/javascript">
+ var trap = "first script";
+ function test() {
+ debugger;
+ }
+ </script>
+ <script type="text/javascript">/*
+ trololol
+ */</script>
+ </body>
+
+</html>
diff --git a/devtools/client/debugger/test/mochitest/doc_scope-variable-4.html b/devtools/client/debugger/test/mochitest/doc_scope-variable-4.html
new file mode 100644
index 000000000..17b0e3b10
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_scope-variable-4.html
@@ -0,0 +1,25 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Debugger test page</title>
+ </head>
+
+ <body>
+ <script type="text/javascript">
+ function test() {
+ var a = "first scope";
+ nest();
+
+ function nest() {
+ var a = "second scope";
+ debugger;
+ }
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/debugger/test/mochitest/doc_scope-variable.html b/devtools/client/debugger/test/mochitest/doc_scope-variable.html
new file mode 100644
index 000000000..3fa28fab9
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_scope-variable.html
@@ -0,0 +1,25 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Debugger test page</title>
+ </head>
+
+ <body>
+ <script type="text/javascript">
+ function test() {
+ var a = "first scope";
+ nest();
+ }
+
+ function nest() {
+ var a = "second scope";
+ debugger;
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/debugger/test/mochitest/doc_script-bookmarklet.html b/devtools/client/debugger/test/mochitest/doc_script-bookmarklet.html
new file mode 100644
index 000000000..922010062
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_script-bookmarklet.html
@@ -0,0 +1,14 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Debugger test page</title>
+ </head>
+
+ <body>
+ <script>function injectBookmarklet(bookmarklet) { setTimeout(function() { window.location = "javascript:" + bookmarklet; }, 0); }</script>
+ </body>
+</html>
diff --git a/devtools/client/debugger/test/mochitest/doc_script-eval.html b/devtools/client/debugger/test/mochitest/doc_script-eval.html
new file mode 100644
index 000000000..7e3f253bb
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_script-eval.html
@@ -0,0 +1,16 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Debugger test page</title>
+ </head>
+
+ <body>
+ <button onclick="evalSource()">Click me!</button>
+
+ <script type="text/javascript" src="code_script-eval.js"></script>
+ </body>
+</html>
diff --git a/devtools/client/debugger/test/mochitest/doc_script-switching-01.html b/devtools/client/debugger/test/mochitest/doc_script-switching-01.html
new file mode 100644
index 000000000..afb4484b5
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_script-switching-01.html
@@ -0,0 +1,18 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Debugger test page</title>
+ </head>
+
+ <body>
+ <button onclick="firstCall()">Click me!</button>
+
+ <script type="text/javascript" src="code_script-switching-01.js"></script>
+ <script type="text/javascript" src="code_script-switching-02.js"></script>
+ </body>
+
+</html>
diff --git a/devtools/client/debugger/test/mochitest/doc_script-switching-02.html b/devtools/client/debugger/test/mochitest/doc_script-switching-02.html
new file mode 100644
index 000000000..cceeea2c8
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_script-switching-02.html
@@ -0,0 +1,18 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Debugger test page</title>
+ </head>
+
+ <body>
+ <button onclick="firstCall()">Click me!</button>
+
+ <script type="text/javascript" src="code_script-switching-01.js"></script>
+ <script type="text/javascript" src="code_script-switching-02.js?foo=bar,baz|lol"></script>
+ </body>
+
+</html>
diff --git a/devtools/client/debugger/test/mochitest/doc_script_webext_contentscript.html b/devtools/client/debugger/test/mochitest/doc_script_webext_contentscript.html
new file mode 100644
index 000000000..8e88997db
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_script_webext_contentscript.html
@@ -0,0 +1,13 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Debugger test page</title>
+ </head>
+
+ <body>
+ </body>
+</html>
diff --git a/devtools/client/debugger/test/mochitest/doc_split-console-paused-reload.html b/devtools/client/debugger/test/mochitest/doc_split-console-paused-reload.html
new file mode 100644
index 000000000..113c53468
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_split-console-paused-reload.html
@@ -0,0 +1,22 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Test page for opening a split-console when execution is paused</title>
+ </head>
+
+ <body>
+ <script type="text/javascript">
+ function executeFunction() {
+ let privateVar = { propKey: "privateVarValue" };
+
+ window.foobar = "foobar";
+ }
+ executeFunction();
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/debugger/test/mochitest/doc_step-many-statements.html b/devtools/client/debugger/test/mochitest/doc_step-many-statements.html
new file mode 100644
index 000000000..edd0e2882
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_step-many-statements.html
@@ -0,0 +1,50 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Debugger test page</title>
+ </head>
+
+ <body>
+ <button id="start">Start!</button>
+
+ <script type="text/javascript">
+ function normal(aArg) {
+ debugger;
+ var r = 10;
+ var a = squareAndOne(r);
+ var b = squareUntil(r, 99999999999); //recurses 3 times, returns on 4th call
+ var c = addUntil(r, 5, 1050); // recurses 208 times and returns on the 209th call
+ return a + b + c;
+
+ }
+
+ function squareAndOne(arg){
+ return (arg * arg) + 1;
+ }
+ function squareUntil(arg, limit){
+ if(arg * arg >= limit){
+ return arg * arg;
+ }else{
+ return squareUntil(arg * arg, limit);
+ }
+ }
+
+ function addUntil(arg1, arg2, limit){
+ if(arg1 + arg2 > limit){
+ return arg1 + arg2;
+ }else{
+ return addUntil(arg1 + arg2, arg2, limit);
+ }
+ }
+
+ var normalBtn = document.getElementById("start");
+ normalBtn.addEventListener("click", normal, false);
+
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/debugger/test/mochitest/doc_step-out.html b/devtools/client/debugger/test/mochitest/doc_step-out.html
new file mode 100644
index 000000000..89eda2be1
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_step-out.html
@@ -0,0 +1,42 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Debugger test page</title>
+ </head>
+
+ <body>
+ <button id="return">Return me!</button>
+ <button id="throw">Throw me!</button>
+
+ <script type="text/javascript">
+ function normal(aArg) {
+ debugger;
+ var r = 10;
+ return r;
+ }
+
+ function error(aArg) {
+ function inner(aArg) {
+ debugger;
+ var r = 10;
+ throw "boom";
+ return r;
+ }
+ try {
+ inner(aArg);
+ } catch (e) {}
+ }
+
+ var normalBtn = document.getElementById("return");
+ normalBtn.addEventListener("click", normal, false);
+
+ var throwBtn = document.getElementById("throw");
+ throwBtn.addEventListener("click", error, false);
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/debugger/test/mochitest/doc_terminate-on-tab-close.html b/devtools/client/debugger/test/mochitest/doc_terminate-on-tab-close.html
new file mode 100644
index 000000000..2101b3103
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_terminate-on-tab-close.html
@@ -0,0 +1,20 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Debugger test page</title>
+ </head>
+
+ <body>
+ <script type="text/javascript">
+ function debuggerThenThrow() {
+ debugger;
+ throw "unreachable";
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/debugger/test/mochitest/doc_watch-expression-button.html b/devtools/client/debugger/test/mochitest/doc_watch-expression-button.html
new file mode 100644
index 000000000..a4a5be26e
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_watch-expression-button.html
@@ -0,0 +1,31 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Debugger test page</title>
+ </head>
+
+ <body>
+ <button onclick="start()">Click me!</button>
+
+ <script type="text/javascript">
+ function test() {
+ var a = 1;
+ var b = { a: a };
+ b.a = 2;
+ debugger;
+ }
+
+ function start() {
+ var e = eval('test();');
+ }
+
+ var button = document.querySelector("button");
+ var buttonAsProto = Object.create(button);
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/debugger/test/mochitest/doc_watch-expressions.html b/devtools/client/debugger/test/mochitest/doc_watch-expressions.html
new file mode 100644
index 000000000..487b5a5a5
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_watch-expressions.html
@@ -0,0 +1,29 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Debugger test page</title>
+ </head>
+
+ <body>
+ <script type="text/javascript">
+ function test() {
+ ermahgerd.call({ canada: new String("eh") });
+ }
+ function ermahgerd(aArg) {
+ var t = document.title;
+ debugger;
+ (function() {
+ var a = undefined;
+ debugger;
+ var a = {};
+ debugger;
+ }("sensational"));
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/debugger/test/mochitest/doc_whitespace-property-names.html b/devtools/client/debugger/test/mochitest/doc_whitespace-property-names.html
new file mode 100644
index 000000000..6479a5978
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_whitespace-property-names.html
@@ -0,0 +1,29 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Debugger + Whitespace property name test page</title>
+ </head>
+
+ <body>
+ <script>
+ window.doPause = function () {
+ var obj = {
+ "": 0,
+ " ": 1,
+ "\r": 2,
+ "\n": 3,
+ "\t": 4,
+ "\f": 5,
+ "\uFEFF": 6,
+ "\xA0": 7
+ };
+ debugger;
+ };
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/debugger/test/mochitest/doc_with-frame.html b/devtools/client/debugger/test/mochitest/doc_with-frame.html
new file mode 100644
index 000000000..8fa202b18
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_with-frame.html
@@ -0,0 +1,29 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Debugger test page</title>
+ </head>
+
+ <body>
+ <button onclick="test(10)">Click me!</button>
+
+ <script type="text/javascript">
+ function test(aNumber) {
+ var a, obj = { alpha: 1, beta: 2 };
+ var r = aNumber;
+ with (Math) {
+ a = PI * r * r;
+ with (obj) {
+ var foo = beta * PI;
+ debugger;
+ }
+ }
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/debugger/test/mochitest/doc_worker-source-map.html b/devtools/client/debugger/test/mochitest/doc_worker-source-map.html
new file mode 100644
index 000000000..20a14e351
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_worker-source-map.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8"/>
+ <script>
+ var worker = new Worker("code_worker-source-map.js");
+
+ function binary_search(items, value) {
+ worker.postMessage({
+ items: items,
+ value: value
+ });
+ }
+ </script>
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/devtools/client/debugger/test/mochitest/head.js b/devtools/client/debugger/test/mochitest/head.js
new file mode 100644
index 000000000..1f9d38b82
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/head.js
@@ -0,0 +1,1351 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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";
+
+// shared-head.js handles imports, constants, and utility functions
+Services.scriptloader.loadSubScript("chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js", this);
+
+// Disable logging for faster test runs. Set this pref to true if you want to
+// debug a test in your try runs. Both the debugger server and frontend will
+// be affected by this pref.
+var gEnableLogging = Services.prefs.getBoolPref("devtools.debugger.log");
+Services.prefs.setBoolPref("devtools.debugger.log", false);
+
+var { BrowserToolboxProcess } = Cu.import("resource://devtools/client/framework/ToolboxProcess.jsm", {});
+var { DebuggerServer } = require("devtools/server/main");
+var { DebuggerClient, ObjectClient } = require("devtools/shared/client/main");
+var { AddonManager } = Cu.import("resource://gre/modules/AddonManager.jsm", {});
+var EventEmitter = require("devtools/shared/event-emitter");
+var { Toolbox } = require("devtools/client/framework/toolbox");
+
+const chromeRegistry = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(Ci.nsIChromeRegistry);
+
+// Override promise with deprecated-sync-thenables
+promise = Cu.import("resource://devtools/shared/deprecated-sync-thenables.js", {}).Promise;
+
+const EXAMPLE_URL = "http://example.com/browser/devtools/client/debugger/test/mochitest/";
+const FRAME_SCRIPT_URL = getRootDirectory(gTestPath) + "code_frame-script.js";
+const CHROME_URL = "chrome://mochitests/content/browser/devtools/client/debugger/test/mochitest/";
+const CHROME_URI = Services.io.newURI(CHROME_URL, null, null);
+
+Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false);
+
+registerCleanupFunction(function* () {
+ Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend");
+
+ info("finish() was called, cleaning up...");
+ Services.prefs.setBoolPref("devtools.debugger.log", gEnableLogging);
+
+ while (gBrowser && gBrowser.tabs && gBrowser.tabs.length > 1) {
+ info("Destroying toolbox.");
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ yield gDevTools.closeToolbox(target);
+
+ info("Removing tab.");
+ gBrowser.removeCurrentTab();
+ }
+
+ // Properly shut down the server to avoid memory leaks.
+ DebuggerServer.destroy();
+
+ // Debugger tests use a lot of memory, so force a GC to help fragmentation.
+ info("Forcing GC after debugger test.");
+ Cu.forceGC();
+});
+
+// Import the GCLI test helper
+var testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/"));
+testDir = testDir.replace(/\/\//g, "/");
+testDir = testDir.replace("chrome:/mochitest", "chrome://mochitest");
+var helpersjs = testDir + "/../../../commandline/test/helpers.js";
+Services.scriptloader.loadSubScript(helpersjs, this);
+
+function addWindow(aUrl) {
+ info("Adding window: " + aUrl);
+ return promise.resolve(getChromeWindow(window.open(aUrl)));
+}
+
+function getChromeWindow(aWindow) {
+ return aWindow
+ .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShellTreeItem).rootTreeItem
+ .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow);
+}
+
+// Override addTab/removeTab as defined by shared-head, since these have
+// an extra window parameter and add a frame script
+this.addTab = function addTab(aUrl, aWindow) {
+ info("Adding tab: " + aUrl);
+
+ let deferred = promise.defer();
+ let targetWindow = aWindow || window;
+ let targetBrowser = targetWindow.gBrowser;
+
+ targetWindow.focus();
+ let tab = targetBrowser.selectedTab = targetBrowser.addTab(aUrl);
+ let linkedBrowser = tab.linkedBrowser;
+
+ info("Loading frame script with url " + FRAME_SCRIPT_URL + ".");
+ linkedBrowser.messageManager.loadFrameScript(FRAME_SCRIPT_URL, false);
+
+ BrowserTestUtils.browserLoaded(linkedBrowser)
+ .then(function () {
+ info("Tab added and finished loading: " + aUrl);
+ deferred.resolve(tab);
+ });
+
+ return deferred.promise;
+};
+
+this.removeTab = function removeTab(aTab, aWindow) {
+ info("Removing tab.");
+
+ let deferred = promise.defer();
+ let targetWindow = aWindow || window;
+ let targetBrowser = targetWindow.gBrowser;
+ let tabContainer = targetBrowser.tabContainer;
+
+ tabContainer.addEventListener("TabClose", function onClose(aEvent) {
+ tabContainer.removeEventListener("TabClose", onClose, false);
+
+ info("Tab removed and finished closing.");
+ deferred.resolve();
+ }, false);
+
+ targetBrowser.removeTab(aTab);
+ return deferred.promise;
+};
+
+function getAddonURIFromPath(aPath) {
+ let chromeURI = Services.io.newURI(aPath, null, CHROME_URI);
+ return chromeRegistry.convertChromeURL(chromeURI).QueryInterface(Ci.nsIFileURL);
+}
+
+function getTemporaryAddonURLFromPath(aPath) {
+ return getAddonURIFromPath(aPath).spec;
+}
+
+function addTemporaryAddon(aPath) {
+ let addonFile = getAddonURIFromPath(aPath).file;
+ info("Installing addon: " + addonFile.path);
+
+ return AddonManager.installTemporaryAddon(addonFile);
+}
+
+function removeAddon(aAddon) {
+ info("Removing addon.");
+
+ let deferred = promise.defer();
+
+ let listener = {
+ onUninstalled: function (aUninstalledAddon) {
+ if (aUninstalledAddon != aAddon) {
+ return;
+ }
+ AddonManager.removeAddonListener(listener);
+ deferred.resolve();
+ }
+ };
+ AddonManager.addAddonListener(listener);
+ aAddon.uninstall();
+
+ return deferred.promise;
+}
+
+function getTabActorForUrl(aClient, aUrl) {
+ let deferred = promise.defer();
+
+ aClient.listTabs(aResponse => {
+ let tabActor = aResponse.tabs.filter(aGrip => aGrip.url == aUrl).pop();
+ deferred.resolve(tabActor);
+ });
+
+ return deferred.promise;
+}
+
+function getAddonActorForId(aClient, aAddonId) {
+ info("Get addon actor for ID: " + aAddonId);
+ let deferred = promise.defer();
+
+ aClient.listAddons(aResponse => {
+ let addonActor = aResponse.addons.filter(aGrip => aGrip.id == aAddonId).pop();
+ info("got addon actor for ID: " + aAddonId);
+ deferred.resolve(addonActor);
+ });
+
+ return deferred.promise;
+}
+
+function attachTabActorForUrl(aClient, aUrl) {
+ let deferred = promise.defer();
+
+ getTabActorForUrl(aClient, aUrl).then(aGrip => {
+ aClient.attachTab(aGrip.actor, aResponse => {
+ deferred.resolve([aGrip, aResponse]);
+ });
+ });
+
+ return deferred.promise;
+}
+
+function attachThreadActorForUrl(aClient, aUrl) {
+ let deferred = promise.defer();
+
+ attachTabActorForUrl(aClient, aUrl).then(([aGrip, aResponse]) => {
+ aClient.attachThread(aResponse.threadActor, (aResponse, aThreadClient) => {
+ aThreadClient.resume(aResponse => {
+ deferred.resolve(aThreadClient);
+ });
+ });
+ });
+
+ return deferred.promise;
+}
+
+function once(aTarget, aEventName, aUseCapture = false) {
+ info("Waiting for event: '" + aEventName + "' on " + aTarget + ".");
+
+ let deferred = promise.defer();
+
+ for (let [add, remove] of [
+ ["addEventListener", "removeEventListener"],
+ ["addListener", "removeListener"],
+ ["on", "off"]
+ ]) {
+ if ((add in aTarget) && (remove in aTarget)) {
+ aTarget[add](aEventName, function onEvent(...aArgs) {
+ aTarget[remove](aEventName, onEvent, aUseCapture);
+ deferred.resolve.apply(deferred, aArgs);
+ }, aUseCapture);
+ break;
+ }
+ }
+
+ return deferred.promise;
+}
+
+function waitForTick() {
+ let deferred = promise.defer();
+ executeSoon(deferred.resolve);
+ return deferred.promise;
+}
+
+function waitForTime(aDelay) {
+ let deferred = promise.defer();
+ setTimeout(deferred.resolve, aDelay);
+ return deferred.promise;
+}
+
+function waitForSourceLoaded(aPanel, aUrl) {
+ let { Sources } = aPanel.panelWin.DebuggerView;
+ let isLoaded = Sources.items.some(item =>
+ item.attachment.source.url === aUrl);
+ if (isLoaded) {
+ info("The correct source has been loaded.");
+ return promise.resolve(null);
+ } else {
+ return waitForDebuggerEvents(aPanel, aPanel.panelWin.EVENTS.NEW_SOURCE).then(() => {
+ // Wait for it to be loaded in the UI and appear into Sources.items.
+ return waitForTick();
+ }).then(() => {
+ return waitForSourceLoaded(aPanel, aUrl);
+ });
+ }
+
+}
+
+function waitForSourceShown(aPanel, aUrl) {
+ return waitForDebuggerEvents(aPanel, aPanel.panelWin.EVENTS.SOURCE_SHOWN).then(aSource => {
+ let sourceUrl = aSource.url || aSource.introductionUrl;
+ info("Source shown: " + sourceUrl);
+
+ if (!sourceUrl.includes(aUrl)) {
+ return waitForSourceShown(aPanel, aUrl);
+ } else {
+ ok(true, "The correct source has been shown.");
+ }
+ });
+}
+
+function waitForEditorLocationSet(aPanel) {
+ return waitForDebuggerEvents(aPanel, aPanel.panelWin.EVENTS.EDITOR_LOCATION_SET);
+}
+
+function ensureSourceIs(aPanel, aUrlOrSource, aWaitFlag = false) {
+ let sources = aPanel.panelWin.DebuggerView.Sources;
+
+ if (sources.selectedValue === aUrlOrSource ||
+ (sources.selectedItem &&
+ sources.selectedItem.attachment.source.url.includes(aUrlOrSource))) {
+ ok(true, "Expected source is shown: " + aUrlOrSource);
+ return promise.resolve(null);
+ }
+ if (aWaitFlag) {
+ return waitForSourceShown(aPanel, aUrlOrSource);
+ }
+ ok(false, "Expected source was not already shown: " + aUrlOrSource);
+ return promise.reject(null);
+}
+
+function waitForCaretUpdated(aPanel, aLine, aCol = 1) {
+ return waitForEditorEvents(aPanel, "cursorActivity").then(() => {
+ let cursor = aPanel.panelWin.DebuggerView.editor.getCursor();
+ info("Caret updated: " + (cursor.line + 1) + ", " + (cursor.ch + 1));
+
+ if (!isCaretPos(aPanel, aLine, aCol)) {
+ return waitForCaretUpdated(aPanel, aLine, aCol);
+ } else {
+ ok(true, "The correct caret position has been set.");
+ }
+ });
+}
+
+function ensureCaretAt(aPanel, aLine, aCol = 1, aWaitFlag = false) {
+ if (isCaretPos(aPanel, aLine, aCol)) {
+ ok(true, "Expected caret position is set: " + aLine + "," + aCol);
+ return promise.resolve(null);
+ }
+ if (aWaitFlag) {
+ return waitForCaretUpdated(aPanel, aLine, aCol);
+ }
+ ok(false, "Expected caret position was not already set: " + aLine + "," + aCol);
+ return promise.reject(null);
+}
+
+function isCaretPos(aPanel, aLine, aCol = 1) {
+ let editor = aPanel.panelWin.DebuggerView.editor;
+ let cursor = editor.getCursor();
+
+ // Source editor starts counting line and column numbers from 0.
+ info("Current editor caret position: " + (cursor.line + 1) + ", " + (cursor.ch + 1));
+ return cursor.line == (aLine - 1) && cursor.ch == (aCol - 1);
+}
+
+function isDebugPos(aPanel, aLine) {
+ let editor = aPanel.panelWin.DebuggerView.editor;
+ let location = editor.getDebugLocation();
+
+ // Source editor starts counting line and column numbers from 0.
+ info("Current editor debug position: " + (location + 1));
+ return location != null && editor.hasLineClass(aLine - 1, "debug-line");
+}
+
+function isEditorSel(aPanel, [start, end]) {
+ let editor = aPanel.panelWin.DebuggerView.editor;
+ let range = {
+ start: editor.getOffset(editor.getCursor("start")),
+ end: editor.getOffset(editor.getCursor())
+ };
+
+ // Source editor starts counting line and column numbers from 0.
+ info("Current editor selection: " + (range.start + 1) + ", " + (range.end + 1));
+ return range.start == (start - 1) && range.end == (end - 1);
+}
+
+function waitForSourceAndCaret(aPanel, aUrl, aLine, aCol) {
+ return promise.all([
+ waitForSourceShown(aPanel, aUrl),
+ waitForCaretUpdated(aPanel, aLine, aCol)
+ ]);
+}
+
+function waitForCaretAndScopes(aPanel, aLine, aCol) {
+ return promise.all([
+ waitForCaretUpdated(aPanel, aLine, aCol),
+ waitForDebuggerEvents(aPanel, aPanel.panelWin.EVENTS.FETCHED_SCOPES)
+ ]);
+}
+
+function waitForSourceAndCaretAndScopes(aPanel, aUrl, aLine, aCol) {
+ return promise.all([
+ waitForSourceAndCaret(aPanel, aUrl, aLine, aCol),
+ waitForDebuggerEvents(aPanel, aPanel.panelWin.EVENTS.FETCHED_SCOPES)
+ ]);
+}
+
+function waitForDebuggerEvents(aPanel, aEventName, aEventRepeat = 1) {
+ info("Waiting for debugger event: '" + aEventName + "' to fire: " + aEventRepeat + " time(s).");
+
+ let deferred = promise.defer();
+ let panelWin = aPanel.panelWin;
+ let count = 0;
+
+ panelWin.on(aEventName, function onEvent(aEventName, ...aArgs) {
+ info("Debugger event '" + aEventName + "' fired: " + (++count) + " time(s).");
+
+ if (count == aEventRepeat) {
+ ok(true, "Enough '" + aEventName + "' panel events have been fired.");
+ panelWin.off(aEventName, onEvent);
+ deferred.resolve.apply(deferred, aArgs);
+ }
+ });
+
+ return deferred.promise;
+}
+
+function waitForEditorEvents(aPanel, aEventName, aEventRepeat = 1) {
+ info("Waiting for editor event: '" + aEventName + "' to fire: " + aEventRepeat + " time(s).");
+
+ let deferred = promise.defer();
+ let editor = aPanel.panelWin.DebuggerView.editor;
+ let count = 0;
+
+ editor.on(aEventName, function onEvent(...aArgs) {
+ info("Editor event '" + aEventName + "' fired: " + (++count) + " time(s).");
+
+ if (count == aEventRepeat) {
+ ok(true, "Enough '" + aEventName + "' editor events have been fired.");
+ editor.off(aEventName, onEvent);
+ deferred.resolve.apply(deferred, aArgs);
+ }
+ });
+
+ return deferred.promise;
+}
+
+function waitForThreadEvents(aPanel, aEventName, aEventRepeat = 1) {
+ info("Waiting for thread event: '" + aEventName + "' to fire: " + aEventRepeat + " time(s).");
+
+ let deferred = promise.defer();
+ let thread = aPanel.panelWin.gThreadClient;
+ let count = 0;
+
+ thread.addListener(aEventName, function onEvent(aEventName, ...aArgs) {
+ info("Thread event '" + aEventName + "' fired: " + (++count) + " time(s).");
+
+ if (count == aEventRepeat) {
+ ok(true, "Enough '" + aEventName + "' thread events have been fired.");
+ thread.removeListener(aEventName, onEvent);
+ deferred.resolve.apply(deferred, aArgs);
+ }
+ });
+
+ return deferred.promise;
+}
+
+function waitForClientEvents(aPanel, aEventName, aEventRepeat = 1) {
+ info("Waiting for client event: '" + aEventName + "' to fire: " + aEventRepeat + " time(s).");
+
+ let deferred = promise.defer();
+ let client = aPanel.panelWin.gClient;
+ let count = 0;
+
+ client.addListener(aEventName, function onEvent(aEventName, ...aArgs) {
+ info("Thread event '" + aEventName + "' fired: " + (++count) + " time(s).");
+
+ if (count == aEventRepeat) {
+ ok(true, "Enough '" + aEventName + "' thread events have been fired.");
+ client.removeListener(aEventName, onEvent);
+ deferred.resolve.apply(deferred, aArgs);
+ }
+ });
+
+ return deferred.promise;
+}
+
+function ensureThreadClientState(aPanel, aState) {
+ let thread = aPanel.panelWin.gThreadClient;
+ let state = thread.state;
+
+ info("Thread is: '" + state + "'.");
+
+ if (state == aState) {
+ return promise.resolve(null);
+ } else {
+ return waitForThreadEvents(aPanel, aState);
+ }
+}
+
+function reload(aPanel, aUrl) {
+ let activeTab = aPanel.panelWin.DebuggerController._target.activeTab;
+ aUrl ? activeTab.navigateTo(aUrl) : activeTab.reload();
+}
+
+function navigateActiveTabTo(aPanel, aUrl, aWaitForEventName, aEventRepeat) {
+ let finished = waitForDebuggerEvents(aPanel, aWaitForEventName, aEventRepeat);
+ reload(aPanel, aUrl);
+ return finished;
+}
+
+function navigateActiveTabInHistory(aPanel, aDirection, aWaitForEventName, aEventRepeat) {
+ let finished = waitForDebuggerEvents(aPanel, aWaitForEventName, aEventRepeat);
+ content.history[aDirection]();
+ return finished;
+}
+
+function reloadActiveTab(aPanel, aWaitForEventName, aEventRepeat) {
+ return navigateActiveTabTo(aPanel, null, aWaitForEventName, aEventRepeat);
+}
+
+function clearText(aElement) {
+ info("Clearing text...");
+ aElement.focus();
+ aElement.value = "";
+}
+
+function setText(aElement, aText) {
+ clearText(aElement);
+ info("Setting text: " + aText);
+ aElement.value = aText;
+}
+
+function typeText(aElement, aText) {
+ info("Typing text: " + aText);
+ aElement.focus();
+ EventUtils.sendString(aText, aElement.ownerDocument.defaultView);
+}
+
+function backspaceText(aElement, aTimes) {
+ info("Pressing backspace " + aTimes + " times.");
+ for (let i = 0; i < aTimes; i++) {
+ aElement.focus();
+ EventUtils.sendKey("BACK_SPACE", aElement.ownerDocument.defaultView);
+ }
+}
+
+function getTab(aTarget, aWindow) {
+ if (aTarget instanceof XULElement) {
+ return promise.resolve(aTarget);
+ } else {
+ return addTab(aTarget, aWindow);
+ }
+}
+
+function getSources(aClient) {
+ info("Getting sources.");
+
+ let deferred = promise.defer();
+
+ aClient.getSources((packet) => {
+ deferred.resolve(packet.sources);
+ });
+
+ return deferred.promise;
+}
+
+/**
+ * Optionaly open a new tab and then open the debugger panel.
+ * The returned promise resolves only one the panel is fully set.
+
+ * @param {String|xul:tab} urlOrTab
+ * If a string, consider it as the url of the tab to open before opening the
+ * debugger panel.
+ * Otherwise, if a <xul:tab>, do nothing, but open the debugger panel against
+ * the given tab.
+ * @param {Object} options
+ * Set of optional arguments:
+ * - {String} source
+ * If given, assert the default loaded source once the debugger is loaded.
+ * This string can be partial to only match a part of the source name.
+ * If null, do not expect any source and skip SOURCE_SHOWN wait.
+ * - {Number} line
+ * If given, wait for the caret to be set on a precise line
+ *
+ * @return {Promise}
+ * Resolves once debugger panel is fully set according to the given options.
+ */
+let initDebugger = Task.async(function*(urlOrTab, options) {
+ let { window, source, line } = options || {};
+ info("Initializing a debugger panel.");
+
+ let tab, url;
+ if (urlOrTab instanceof XULElement) {
+ // `urlOrTab` Is a Tab.
+ tab = urlOrTab;
+ } else {
+ // `urlOrTab` is an url. Open an empty tab first in order to load the page
+ // only once the panel is ready. That to be able to safely catch the
+ // SOURCE_SHOWN event.
+ tab = yield addTab("about:blank", window);
+ url = urlOrTab;
+ }
+ info("Debugee tab added successfully: " + urlOrTab);
+
+ let debuggee = tab.linkedBrowser.contentWindow.wrappedJSObject;
+ let target = TargetFactory.forTab(tab);
+
+ let toolbox = yield gDevTools.showToolbox(target, "jsdebugger");
+ info("Debugger panel shown successfully.");
+
+ let debuggerPanel = toolbox.getCurrentPanel();
+ let panelWin = debuggerPanel.panelWin;
+ let { Sources } = panelWin.DebuggerView;
+
+ prepareDebugger(debuggerPanel);
+
+ if (url && url != "about:blank") {
+ let onCaretUpdated;
+ if (line) {
+ onCaretUpdated = waitForCaretUpdated(debuggerPanel, line);
+ }
+ if (source === null) {
+ // When there is no source in the document, we shouldn't wait for
+ // SOURCE_SHOWN event
+ yield reload(debuggerPanel, url);
+ } else {
+ yield navigateActiveTabTo(debuggerPanel,
+ url,
+ panelWin.EVENTS.SOURCE_SHOWN);
+ }
+ if (source) {
+ let isSelected = Sources.selectedItem.attachment.source.url === source;
+ if (!isSelected) {
+ // Ensure that the source is loaded first before trying to select it
+ yield waitForSourceLoaded(debuggerPanel, source);
+ // Select the js file.
+ let onSource = waitForSourceAndCaret(debuggerPanel, source, line ? line : 1);
+ Sources.selectedValue = getSourceActor(Sources, source);
+ yield onSource;
+ }
+ }
+ yield onCaretUpdated;
+ }
+
+ return [tab, debuggee, debuggerPanel, window];
+});
+
+// Creates an add-on debugger for a given add-on. The returned AddonDebugger
+// object must be destroyed before finishing the test
+function initAddonDebugger(aAddonId) {
+ let addonDebugger = new AddonDebugger();
+ return addonDebugger.init(aAddonId).then(() => addonDebugger);
+}
+
+function AddonDebugger() {
+ this._onMessage = this._onMessage.bind(this);
+ this._onConsoleAPICall = this._onConsoleAPICall.bind(this);
+ EventEmitter.decorate(this);
+}
+
+AddonDebugger.prototype = {
+ init: Task.async(function* (aAddonId) {
+ info("Initializing an addon debugger panel.");
+
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+ DebuggerServer.allowChromeProcess = true;
+
+ this.frame = document.createElement("iframe");
+ this.frame.setAttribute("height", 400);
+ document.documentElement.appendChild(this.frame);
+ window.addEventListener("message", this._onMessage);
+
+ let transport = DebuggerServer.connectPipe();
+ this.client = new DebuggerClient(transport);
+
+ yield this.client.connect();
+
+ let addonActor = yield getAddonActorForId(this.client, aAddonId);
+
+ let targetOptions = {
+ form: addonActor,
+ client: this.client,
+ chrome: true,
+ isTabActor: false
+ };
+
+ let toolboxOptions = {
+ customIframe: this.frame
+ };
+
+ this.target = TargetFactory.forTab(targetOptions);
+ let toolbox = yield gDevTools.showToolbox(this.target, "jsdebugger", Toolbox.HostType.CUSTOM, toolboxOptions);
+
+ info("Addon debugger panel shown successfully.");
+
+ this.debuggerPanel = toolbox.getCurrentPanel();
+ yield waitForSourceShown(this.debuggerPanel, "");
+
+ prepareDebugger(this.debuggerPanel);
+ yield this._attachConsole();
+ }),
+
+ destroy: Task.async(function* () {
+ yield this.client.close();
+ yield this.debuggerPanel._toolbox.destroy();
+ this.frame.remove();
+ window.removeEventListener("message", this._onMessage);
+ }),
+
+ _attachConsole: function () {
+ let deferred = promise.defer();
+ this.client.attachConsole(this.target.form.consoleActor, ["ConsoleAPI"], (aResponse, aWebConsoleClient) => {
+ if (aResponse.error) {
+ deferred.reject(aResponse);
+ }
+ else {
+ this.webConsole = aWebConsoleClient;
+ this.client.addListener("consoleAPICall", this._onConsoleAPICall);
+ deferred.resolve();
+ }
+ });
+ return deferred.promise;
+ },
+
+ _onConsoleAPICall: function (aType, aPacket) {
+ if (aPacket.from != this.webConsole.actor)
+ return;
+ this.emit("console", aPacket.message);
+ },
+
+ /**
+ * Returns a list of the groups and sources in the UI. The returned array
+ * contains objects for each group with properties name and sources. The
+ * sources property contains an array with objects for each source for that
+ * group with properties label and url.
+ */
+ getSourceGroups: Task.async(function* () {
+ let debuggerWin = this.debuggerPanel.panelWin;
+ let sources = yield getSources(debuggerWin.gThreadClient);
+ ok(sources.length, "retrieved sources");
+
+ // groups will be the return value, groupmap and the maps we put in it will
+ // be used as quick lookups to add the url information in below
+ let groups = [];
+ let groupmap = new Map();
+
+ let uigroups = this.debuggerPanel.panelWin.document.querySelectorAll(".side-menu-widget-group");
+ for (let g of uigroups) {
+ let name = g.querySelector(".side-menu-widget-group-title .name").value;
+ let group = {
+ name: name,
+ sources: []
+ };
+ groups.push(group);
+ let labelmap = new Map();
+ groupmap.set(name, labelmap);
+
+ for (let l of g.querySelectorAll(".dbg-source-item")) {
+ let source = {
+ label: l.value,
+ url: null
+ };
+
+ labelmap.set(l.value, source);
+ group.sources.push(source);
+ }
+ }
+
+ for (let source of sources) {
+ let { label, group } = debuggerWin.DebuggerView.Sources.getItemByValue(source.actor).attachment;
+
+ if (!groupmap.has(group)) {
+ ok(false, "Saw a source group not in the UI: " + group);
+ continue;
+ }
+
+ if (!groupmap.get(group).has(label)) {
+ ok(false, "Saw a source label not in the UI: " + label);
+ continue;
+ }
+
+ groupmap.get(group).get(label).url = source.url.split(" -> ").pop();
+ }
+
+ return groups;
+ }),
+
+ _onMessage: function (event) {
+ if (typeof(event.data) !== "string") {
+ return;
+ }
+ let json = JSON.parse(event.data);
+ switch (json.name) {
+ case "toolbox-title":
+ this.title = json.data.value;
+ break;
+ }
+ }
+};
+
+function initChromeDebugger(aOnClose) {
+ info("Initializing a chrome debugger process.");
+
+ let deferred = promise.defer();
+
+ // Wait for the toolbox process to start...
+ BrowserToolboxProcess.init(aOnClose, (aEvent, aProcess) => {
+ info("Browser toolbox process started successfully.");
+
+ prepareDebugger(aProcess);
+ deferred.resolve(aProcess);
+ });
+
+ return deferred.promise;
+}
+
+function prepareDebugger(aDebugger) {
+ if ("target" in aDebugger) {
+ let view = aDebugger.panelWin.DebuggerView;
+ view.Variables.lazyEmpty = false;
+ view.Variables.lazySearch = false;
+ view.Filtering.FilteredSources._autoSelectFirstItem = true;
+ view.Filtering.FilteredFunctions._autoSelectFirstItem = true;
+ } else {
+ // Nothing to do here yet.
+ }
+}
+
+function teardown(aPanel, aFlags = {}) {
+ info("Destroying the specified debugger.");
+
+ let toolbox = aPanel._toolbox;
+ let tab = aPanel.target.tab;
+ let debuggerRootActorDisconnected = once(window, "Debugger:Shutdown");
+ let debuggerPanelDestroyed = once(aPanel, "destroyed");
+ let devtoolsToolboxDestroyed = toolbox.destroy();
+
+ return promise.all([
+ debuggerRootActorDisconnected,
+ debuggerPanelDestroyed,
+ devtoolsToolboxDestroyed
+ ]).then(() => aFlags.noTabRemoval ? null : removeTab(tab));
+}
+
+function closeDebuggerAndFinish(aPanel, aFlags = {}) {
+ let thread = aPanel.panelWin.gThreadClient;
+ if (thread.state == "paused" && !aFlags.whilePaused) {
+ ok(false, "You should use 'resumeDebuggerThenCloseAndFinish' instead, " +
+ "unless you're absolutely sure about what you're doing.");
+ }
+ return teardown(aPanel, aFlags).then(finish);
+}
+
+function resumeDebuggerThenCloseAndFinish(aPanel, aFlags = {}) {
+ let deferred = promise.defer();
+ let thread = aPanel.panelWin.gThreadClient;
+ thread.resume(() => closeDebuggerAndFinish(aPanel, aFlags).then(deferred.resolve));
+ return deferred.promise;
+}
+
+// Blackboxing helpers
+
+function getBlackBoxButton(aPanel) {
+ return aPanel.panelWin.document.getElementById("black-box");
+}
+
+/**
+ * Returns the node that has the black-boxed class applied to it.
+ */
+function getSelectedSourceElement(aPanel) {
+ return aPanel.panelWin.DebuggerView.Sources.selectedItem.prebuiltNode;
+}
+
+function toggleBlackBoxing(aPanel, aSourceActor = null) {
+ function clickBlackBoxButton() {
+ getBlackBoxButton(aPanel).click();
+ }
+
+ const blackBoxChanged = waitForDispatch(
+ aPanel,
+ aPanel.panelWin.constants.BLACKBOX
+ ).then(() => {
+ return aSourceActor ?
+ getSource(aPanel, aSourceActor) :
+ getSelectedSource(aPanel);
+ });
+
+ if (aSourceActor) {
+ aPanel.panelWin.DebuggerView.Sources.selectedValue = aSourceActor;
+ ensureSourceIs(aPanel, aSourceActor, true).then(clickBlackBoxButton);
+ } else {
+ clickBlackBoxButton();
+ }
+
+ return blackBoxChanged;
+}
+
+function selectSourceAndGetBlackBoxButton(aPanel, aUrl) {
+ function returnBlackboxButton() {
+ return getBlackBoxButton(aPanel);
+ }
+
+ let sources = aPanel.panelWin.DebuggerView.Sources;
+ sources.selectedValue = getSourceActor(sources, aUrl);
+ return ensureSourceIs(aPanel, aUrl, true).then(returnBlackboxButton);
+}
+
+// Variables view inspection popup helpers
+
+function openVarPopup(aPanel, aCoords, aWaitForFetchedProperties) {
+ let events = aPanel.panelWin.EVENTS;
+ let editor = aPanel.panelWin.DebuggerView.editor;
+ let bubble = aPanel.panelWin.DebuggerView.VariableBubble;
+ let tooltip = bubble._tooltip.panel;
+
+ let popupShown = once(tooltip, "popupshown");
+ let fetchedProperties = aWaitForFetchedProperties
+ ? waitForDebuggerEvents(aPanel, events.FETCHED_BUBBLE_PROPERTIES)
+ : promise.resolve(null);
+ let updatedFrame = waitForDebuggerEvents(aPanel, events.FETCHED_SCOPES);
+
+ let { left, top } = editor.getCoordsFromPosition(aCoords);
+ bubble._findIdentifier(left, top);
+ return promise.all([popupShown, fetchedProperties, updatedFrame]).then(waitForTick);
+}
+
+// Simulates the mouse hovering a variable in the debugger
+// Takes in account the position of the cursor in the text, if the text is
+// selected and if a button is currently pushed (aButtonPushed > 0).
+// The function returns a promise which returns true if the popup opened or
+// false if it didn't
+function intendOpenVarPopup(aPanel, aPosition, aButtonPushed) {
+ let bubble = aPanel.panelWin.DebuggerView.VariableBubble;
+ let editor = aPanel.panelWin.DebuggerView.editor;
+ let tooltip = bubble._tooltip;
+
+ let { left, top } = editor.getCoordsFromPosition(aPosition);
+
+ const eventDescriptor = {
+ clientX: left,
+ clientY: top,
+ buttons: aButtonPushed
+ };
+
+ bubble._onMouseMove(eventDescriptor);
+
+ const deferred = promise.defer();
+ window.setTimeout(
+ function () {
+ if (tooltip.isEmpty()) {
+ deferred.resolve(false);
+ } else {
+ deferred.resolve(true);
+ }
+ },
+ bubble.TOOLTIP_SHOW_DELAY + 1000
+ );
+
+ return deferred.promise;
+}
+
+function hideVarPopup(aPanel) {
+ let bubble = aPanel.panelWin.DebuggerView.VariableBubble;
+ let tooltip = bubble._tooltip.panel;
+
+ let popupHiding = once(tooltip, "popuphiding");
+ bubble.hideContents();
+ return popupHiding.then(waitForTick);
+}
+
+function hideVarPopupByScrollingEditor(aPanel) {
+ let editor = aPanel.panelWin.DebuggerView.editor;
+ let bubble = aPanel.panelWin.DebuggerView.VariableBubble;
+ let tooltip = bubble._tooltip.panel;
+
+ let popupHiding = once(tooltip, "popuphiding");
+ editor.setFirstVisibleLine(0);
+ return popupHiding.then(waitForTick);
+}
+
+function reopenVarPopup(...aArgs) {
+ return hideVarPopup.apply(this, aArgs).then(() => openVarPopup.apply(this, aArgs));
+}
+
+function attachAddonActorForId(aClient, aAddonId) {
+ let deferred = promise.defer();
+
+ getAddonActorForId(aClient, aAddonId).then(aGrip => {
+ aClient.attachAddon(aGrip.actor, aResponse => {
+ deferred.resolve([aGrip, aResponse]);
+ });
+ });
+
+ return deferred.promise;
+}
+
+function doResume(aPanel) {
+ const threadClient = aPanel.panelWin.gThreadClient;
+ return threadClient.resume();
+}
+
+function doInterrupt(aPanel) {
+ const threadClient = aPanel.panelWin.gThreadClient;
+ return threadClient.interrupt();
+}
+
+function pushPrefs(...aPrefs) {
+ let deferred = promise.defer();
+ SpecialPowers.pushPrefEnv({"set": aPrefs}, deferred.resolve);
+ return deferred.promise;
+}
+
+function popPrefs() {
+ let deferred = promise.defer();
+ SpecialPowers.popPrefEnv(deferred.resolve);
+ return deferred.promise;
+}
+
+// Source helpers
+
+function getSelectedSource(panel) {
+ const win = panel.panelWin;
+ return win.queries.getSelectedSource(win.DebuggerController.getState());
+}
+
+function getSource(panel, actor) {
+ const win = panel.panelWin;
+ return win.queries.getSource(win.DebuggerController.getState(), actor);
+}
+
+function getSelectedSourceURL(aSources) {
+ return (aSources.selectedItem &&
+ aSources.selectedItem.attachment.source.url);
+}
+
+function getSourceURL(aSources, aActor) {
+ let item = aSources.getItemByValue(aActor);
+ return item && item.attachment.source.url;
+}
+
+function getSourceActor(aSources, aURL) {
+ let item = aSources.getItemForAttachment(a => a.source && a.source.url === aURL);
+ return item && item.value;
+}
+
+function getSourceForm(aSources, aURL) {
+ let item = aSources.getItemByValue(getSourceActor(aSources, aURL));
+ return item.attachment.source;
+}
+
+var nextId = 0;
+
+function jsonrpc(tab, method, params) {
+ return new Promise(function (resolve, reject) {
+ let currentId = nextId++;
+ let messageManager = tab.linkedBrowser.messageManager;
+ messageManager.sendAsyncMessage("jsonrpc", {
+ method: method,
+ params: params,
+ id: currentId
+ });
+ messageManager.addMessageListener("jsonrpc", function listener(res) {
+ const { data: { result, error, id } } = res;
+ if (id !== currentId) {
+ return;
+ }
+
+ messageManager.removeMessageListener("jsonrpc", listener);
+ if (error != null) {
+ reject(error);
+ }
+
+ resolve(result);
+ });
+ });
+}
+
+function callInTab(tab, name) {
+ info("Calling function with name '" + name + "' in tab.");
+
+ return jsonrpc(tab, "call", [name, Array.prototype.slice.call(arguments, 2)]);
+}
+
+function evalInTab(tab, string) {
+ info("Evalling string in tab.");
+
+ return jsonrpc(tab, "_eval", [string]);
+}
+
+function createWorkerInTab(tab, url) {
+ info("Creating worker with url '" + url + "' in tab.");
+
+ return jsonrpc(tab, "createWorker", [url]);
+}
+
+function terminateWorkerInTab(tab, url) {
+ info("Terminating worker with url '" + url + "' in tab.");
+
+ return jsonrpc(tab, "terminateWorker", [url]);
+}
+
+function postMessageToWorkerInTab(tab, url, message) {
+ info("Posting message to worker with url '" + url + "' in tab.");
+
+ return jsonrpc(tab, "postMessageToWorker", [url, message]);
+}
+
+function generateMouseClickInTab(tab, path) {
+ info("Generating mouse click in tab.");
+
+ return jsonrpc(tab, "generateMouseClick", [path]);
+}
+
+function connect(client) {
+ info("Connecting client.");
+ return client.connect();
+}
+
+function close(client) {
+ info("Waiting for client to close.\n");
+ return client.close();
+}
+
+function listTabs(client) {
+ info("Listing tabs.");
+ return client.listTabs();
+}
+
+function findTab(tabs, url) {
+ info("Finding tab with url '" + url + "'.");
+ for (let tab of tabs) {
+ if (tab.url === url) {
+ return tab;
+ }
+ }
+ return null;
+}
+
+function attachTab(client, tab) {
+ info("Attaching to tab with url '" + tab.url + "'.");
+ return new Promise(function (resolve) {
+ client.attachTab(tab.actor, function (response, tabClient) {
+ resolve([response, tabClient]);
+ });
+ });
+}
+
+function listWorkers(tabClient) {
+ info("Listing workers.");
+ return new Promise(function (resolve) {
+ tabClient.listWorkers(function (response) {
+ resolve(response);
+ });
+ });
+}
+
+function findWorker(workers, url) {
+ info("Finding worker with url '" + url + "'.");
+ for (let worker of workers) {
+ if (worker.url === url) {
+ return worker;
+ }
+ }
+ return null;
+}
+
+function attachWorker(tabClient, worker) {
+ info("Attaching to worker with url '" + worker.url + "'.");
+ return new Promise(function (resolve, reject) {
+ tabClient.attachWorker(worker.actor, function (response, workerClient) {
+ resolve([response, workerClient]);
+ });
+ });
+}
+
+function waitForWorkerListChanged(tabClient) {
+ info("Waiting for worker list to change.");
+ return new Promise(function (resolve) {
+ tabClient.addListener("workerListChanged", function listener() {
+ tabClient.removeListener("workerListChanged", listener);
+ resolve();
+ });
+ });
+}
+
+function attachThread(workerClient, options) {
+ info("Attaching to thread.");
+ return new Promise(function (resolve, reject) {
+ workerClient.attachThread(options, function (response, threadClient) {
+ resolve([response, threadClient]);
+ });
+ });
+}
+
+function waitForWorkerClose(workerClient) {
+ info("Waiting for worker to close.");
+ return new Promise(function (resolve) {
+ workerClient.addOneTimeListener("close", function () {
+ info("Worker did close.");
+ resolve();
+ });
+ });
+}
+
+function resume(threadClient) {
+ info("Resuming thread.");
+ return threadClient.resume();
+}
+
+function findSource(sources, url) {
+ info("Finding source with url '" + url + "'.\n");
+ for (let source of sources) {
+ if (source.url === url) {
+ return source;
+ }
+ }
+ return null;
+}
+
+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);
+ });
+ }
+ });
+}
+
+function waitForPause(threadClient) {
+ info("Waiting for pause.\n");
+ return waitForEvent(threadClient, "paused");
+}
+
+function setBreakpoint(sourceClient, location) {
+ info("Setting breakpoint.\n");
+ return sourceClient.setBreakpoint(location);
+}
+
+function source(sourceClient) {
+ info("Getting source.\n");
+ return sourceClient.source();
+}
+
+// Return a promise with a reference to jsterm, opening the split
+// console if necessary. This cleans up the split console pref so
+// it won't pollute other tests.
+function getSplitConsole(toolbox, win) {
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("devtools.toolbox.splitconsoleEnabled");
+ });
+
+ if (!win) {
+ win = toolbox.win;
+ }
+
+ if (!toolbox.splitConsole) {
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, win);
+ }
+
+ return new Promise(resolve => {
+ toolbox.getPanelWhenReady("webconsole").then(() => {
+ ok(toolbox.splitConsole, "Split console is shown.");
+ let jsterm = toolbox.getPanel("webconsole").hud.jsterm;
+ resolve(jsterm);
+ });
+ });
+}
+
+// navigation
+
+function waitForNavigation(gPanel) {
+ const target = gPanel.panelWin.gTarget;
+ const deferred = promise.defer();
+ target.once("navigate", () => {
+ deferred.resolve();
+ });
+ info("Waiting for navigation...");
+ return deferred.promise;
+}
+
+// actions
+
+function bindActionCreators(panel) {
+ const win = panel.panelWin;
+ const dispatch = win.DebuggerController.dispatch;
+ const { bindActionCreators } = win.require("devtools/client/shared/vendor/redux");
+ return bindActionCreators(win.actions, dispatch);
+}
+
+// Wait until an action of `type` is dispatched. This is different
+// then `_afterDispatchDone` because it doesn't wait for async actions
+// to be done/errored. Use this if you want to listen for the "start"
+// action of an async operation (somewhat rare).
+function waitForNextDispatch(store, type) {
+ return new Promise(resolve => {
+ store.dispatch({
+ // Normally we would use `services.WAIT_UNTIL`, but use the
+ // internal name here so tests aren't forced to always pass it
+ // in
+ type: "@@service/waitUntil",
+ predicate: action => action.type === type,
+ run: (dispatch, getState, action) => {
+ resolve(action);
+ }
+ });
+ });
+}
+
+// Wait until an action of `type` is dispatched. If it's part of an
+// async operation, wait until the `status` field is "done" or "error"
+function _afterDispatchDone(store, type) {
+ return new Promise(resolve => {
+ store.dispatch({
+ // Normally we would use `services.WAIT_UNTIL`, but use the
+ // internal name here so tests aren't forced to always pass it
+ // in
+ type: "@@service/waitUntil",
+ predicate: action => {
+ if (action.type === type) {
+ return action.status ?
+ (action.status === "done" || action.status === "error") :
+ true;
+ }
+ },
+ run: (dispatch, getState, action) => {
+ resolve(action);
+ }
+ });
+ });
+}
+
+function waitForDispatch(panel, type, eventRepeat = 1) {
+ const controller = panel.panelWin.DebuggerController;
+ const actionType = panel.panelWin.constants[type];
+ let count = 0;
+
+ return Task.spawn(function* () {
+ info("Waiting for " + type + " to dispatch " + eventRepeat + " time(s)");
+ while (count < eventRepeat) {
+ yield _afterDispatchDone(controller, actionType);
+ count++;
+ info(type + " dispatched " + count + " time(s)");
+ }
+ });
+}
+
+function* initWorkerDebugger(TAB_URL, WORKER_URL) {
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+
+ let client = new DebuggerClient(DebuggerServer.connectPipe());
+ yield connect(client);
+
+ let tab = yield addTab(TAB_URL);
+ let { tabs } = yield listTabs(client);
+ let [, tabClient] = yield attachTab(client, findTab(tabs, TAB_URL));
+
+ yield createWorkerInTab(tab, WORKER_URL);
+
+ let { workers } = yield listWorkers(tabClient);
+ let [, workerClient] = yield attachWorker(tabClient,
+ findWorker(workers, WORKER_URL));
+
+ let toolbox = yield gDevTools.showToolbox(TargetFactory.forWorker(workerClient),
+ "jsdebugger",
+ Toolbox.HostType.WINDOW);
+
+ let debuggerPanel = toolbox.getCurrentPanel();
+ let gDebugger = debuggerPanel.panelWin;
+
+ return {client, tab, tabClient, workerClient, toolbox, gDebugger};
+}
+
diff --git a/devtools/client/debugger/test/mochitest/sjs_post-page.sjs b/devtools/client/debugger/test/mochitest/sjs_post-page.sjs
new file mode 100644
index 000000000..06f7c60d0
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/sjs_post-page.sjs
@@ -0,0 +1,16 @@
+/* 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/. */
+
+const CC = Components.Constructor;
+const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1",
+ "nsIBinaryInputStream",
+ "setInputStream");
+
+function handleRequest(request, response)
+{
+ let method = request.method;
+ let body = "<script>\"" + method + "\";</script>";
+ body += "<form method=\"POST\"><input type=\"submit\"></form>";
+ response.bodyOutputStream.write(body, body.length);
+}
diff --git a/devtools/client/debugger/test/mochitest/sjs_random-javascript.sjs b/devtools/client/debugger/test/mochitest/sjs_random-javascript.sjs
new file mode 100644
index 000000000..3e0ea8e53
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/sjs_random-javascript.sjs
@@ -0,0 +1,11 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function handleRequest(request, response) {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "application/javascript; charset=utf-8", false);
+ response.write([
+ "window.setInterval(function bacon() {",
+ " var x = '" + Math.random() + "';",
+ "}, 0);"].join("\n"));
+}
diff --git a/devtools/client/debugger/test/mochitest/testactors.js b/devtools/client/debugger/test/mochitest/testactors.js
new file mode 100644
index 000000000..f7583b615
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/testactors.js
@@ -0,0 +1,33 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+function TestActor1(aConnection, aTab)
+{
+ this.conn = aConnection;
+ this.tab = aTab;
+}
+
+TestActor1.prototype = {
+ actorPrefix: "test_one",
+
+ grip: function TA1_grip() {
+ return { actor: this.actorID,
+ test: "TestActor1" };
+ },
+
+ onPing: function TA1_onPing() {
+ return { pong: "pong" };
+ }
+};
+
+TestActor1.prototype.requestTypes = {
+ "ping": TestActor1.prototype.onPing
+};
+
+DebuggerServer.removeTabActor(TestActor1);
+DebuggerServer.removeGlobalActor(TestActor1);
+
+DebuggerServer.addTabActor(TestActor1, "testTabActor1");
+DebuggerServer.addGlobalActor(TestActor1, "testGlobalActor1");
diff --git a/devtools/client/debugger/utils.js b/devtools/client/debugger/utils.js
new file mode 100644
index 000000000..e2d3fbebe
--- /dev/null
+++ b/devtools/client/debugger/utils.js
@@ -0,0 +1,378 @@
+/* -*- 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/. */
+/* globals document, window */
+/* import-globals-from ./debugger-controller.js */
+"use strict";
+
+// Maps known URLs to friendly source group names and put them at the
+// bottom of source list.
+var KNOWN_SOURCE_GROUPS = {
+ "Add-on SDK": "resource://gre/modules/commonjs/",
+};
+
+KNOWN_SOURCE_GROUPS[L10N.getStr("anonymousSourcesLabel")] = "anonymous";
+
+var XULUtils = {
+ /**
+ * Create <command> elements within `commandset` with event handlers
+ * bound to the `command` event
+ *
+ * @param commandset HTML Element
+ * A <commandset> element
+ * @param commands Object
+ * An object where keys specify <command> ids and values
+ * specify event handlers to be bound on the `command` event
+ */
+ addCommands: function (commandset, commands) {
+ Object.keys(commands).forEach(name => {
+ let node = document.createElement("command");
+ node.id = name;
+ // XXX bug 371900: the command element must have an oncommand
+ // attribute as a string set by `setAttribute` for keys to use it
+ node.setAttribute("oncommand", " ");
+ node.addEventListener("command", commands[name]);
+ commandset.appendChild(node);
+ });
+ }
+};
+
+// Used to detect minification for automatic pretty printing
+const SAMPLE_SIZE = 50; // no of lines
+const INDENT_COUNT_THRESHOLD = 5; // percentage
+const CHARACTER_LIMIT = 250; // line character limit
+
+/**
+ * Utility functions for handling sources.
+ */
+var SourceUtils = {
+ _labelsCache: new Map(), // Can't use WeakMaps because keys are strings.
+ _groupsCache: new Map(),
+ _minifiedCache: new Map(),
+
+ /**
+ * Returns true if the specified url and/or content type are specific to
+ * javascript files.
+ *
+ * @return boolean
+ * True if the source is likely javascript.
+ */
+ isJavaScript: function (aUrl, aContentType = "") {
+ return (aUrl && /\.jsm?$/.test(this.trimUrlQuery(aUrl))) ||
+ aContentType.includes("javascript");
+ },
+
+ /**
+ * Determines if the source text is minified by using
+ * the percentage indented of a subset of lines
+ *
+ * @return object
+ * A promise that resolves to true if source text is minified.
+ */
+ isMinified: function (key, text) {
+ if (this._minifiedCache.has(key)) {
+ return this._minifiedCache.get(key);
+ }
+
+ let isMinified;
+ let lineEndIndex = 0;
+ let lineStartIndex = 0;
+ let lines = 0;
+ let indentCount = 0;
+ let overCharLimit = false;
+
+ // Strip comments.
+ text = text.replace(/\/\*[\S\s]*?\*\/|\/\/(.+|\n)/g, "");
+
+ while (lines++ < SAMPLE_SIZE) {
+ lineEndIndex = text.indexOf("\n", lineStartIndex);
+ if (lineEndIndex == -1) {
+ break;
+ }
+ if (/^\s+/.test(text.slice(lineStartIndex, lineEndIndex))) {
+ indentCount++;
+ }
+ // For files with no indents but are not minified.
+ if ((lineEndIndex - lineStartIndex) > CHARACTER_LIMIT) {
+ overCharLimit = true;
+ break;
+ }
+ lineStartIndex = lineEndIndex + 1;
+ }
+
+ isMinified =
+ ((indentCount / lines) * 100) < INDENT_COUNT_THRESHOLD || overCharLimit;
+
+ this._minifiedCache.set(key, isMinified);
+ return isMinified;
+ },
+
+ /**
+ * Clears the labels, groups and minify cache, populated by methods like
+ * SourceUtils.getSourceLabel or Source Utils.getSourceGroup.
+ * This should be done every time the content location changes.
+ */
+ clearCache: function () {
+ this._labelsCache.clear();
+ this._groupsCache.clear();
+ this._minifiedCache.clear();
+ },
+
+ /**
+ * Gets a unique, simplified label from a source url.
+ *
+ * @param string aUrl
+ * The source url.
+ * @return string
+ * The simplified label.
+ */
+ getSourceLabel: function (aUrl) {
+ let cachedLabel = this._labelsCache.get(aUrl);
+ if (cachedLabel) {
+ return cachedLabel;
+ }
+
+ let sourceLabel = null;
+
+ for (let name of Object.keys(KNOWN_SOURCE_GROUPS)) {
+ if (aUrl.startsWith(KNOWN_SOURCE_GROUPS[name])) {
+ sourceLabel = aUrl.substring(KNOWN_SOURCE_GROUPS[name].length);
+ }
+ }
+
+ if (!sourceLabel) {
+ sourceLabel = this.trimUrl(aUrl);
+ }
+
+ let unicodeLabel = NetworkHelper.convertToUnicode(unescape(sourceLabel));
+ this._labelsCache.set(aUrl, unicodeLabel);
+ return unicodeLabel;
+ },
+
+ /**
+ * Gets as much information as possible about the hostname and directory paths
+ * of an url to create a short url group identifier.
+ *
+ * @param string aUrl
+ * The source url.
+ * @return string
+ * The simplified group.
+ */
+ getSourceGroup: function (aUrl) {
+ let cachedGroup = this._groupsCache.get(aUrl);
+ if (cachedGroup) {
+ return cachedGroup;
+ }
+
+ try {
+ // Use an nsIURL to parse all the url path parts.
+ var uri = Services.io.newURI(aUrl, null, null).QueryInterface(Ci.nsIURL);
+ } catch (e) {
+ // This doesn't look like a url, or nsIURL can't handle it.
+ return "";
+ }
+
+ let groupLabel = uri.prePath;
+
+ for (let name of Object.keys(KNOWN_SOURCE_GROUPS)) {
+ if (aUrl.startsWith(KNOWN_SOURCE_GROUPS[name])) {
+ groupLabel = name;
+ }
+ }
+
+ let unicodeLabel = NetworkHelper.convertToUnicode(unescape(groupLabel));
+ this._groupsCache.set(aUrl, unicodeLabel);
+ return unicodeLabel;
+ },
+
+ /**
+ * Trims the url by shortening it if it exceeds a certain length, adding an
+ * ellipsis at the end.
+ *
+ * @param string aUrl
+ * The source url.
+ * @param number aLength [optional]
+ * The expected source url length.
+ * @param number aSection [optional]
+ * The section to trim. Supported values: "start", "center", "end"
+ * @return string
+ * The shortened url.
+ */
+ trimUrlLength: function (aUrl, aLength, aSection) {
+ aLength = aLength || SOURCE_URL_DEFAULT_MAX_LENGTH;
+ aSection = aSection || "end";
+
+ if (aUrl.length > aLength) {
+ switch (aSection) {
+ case "start":
+ return ELLIPSIS + aUrl.slice(-aLength);
+ break;
+ case "center":
+ return aUrl.substr(0, aLength / 2 - 1) + ELLIPSIS + aUrl.slice(-aLength / 2 + 1);
+ break;
+ case "end":
+ return aUrl.substr(0, aLength) + ELLIPSIS;
+ break;
+ }
+ }
+ return aUrl;
+ },
+
+ /**
+ * Trims the query part or reference identifier of a url string, if necessary.
+ *
+ * @param string aUrl
+ * The source url.
+ * @return string
+ * The shortened url.
+ */
+ trimUrlQuery: function (aUrl) {
+ let length = aUrl.length;
+ let q1 = aUrl.indexOf("?");
+ let q2 = aUrl.indexOf("&");
+ let q3 = aUrl.indexOf("#");
+ let q = Math.min(q1 != -1 ? q1 : length,
+ q2 != -1 ? q2 : length,
+ q3 != -1 ? q3 : length);
+
+ return aUrl.slice(0, q);
+ },
+
+ /**
+ * Trims as much as possible from a url, while keeping the label unique
+ * in the sources container.
+ *
+ * @param string | nsIURL aUrl
+ * The source url.
+ * @param string aLabel [optional]
+ * The resulting label at each step.
+ * @param number aSeq [optional]
+ * The current iteration step.
+ * @return string
+ * The resulting label at the final step.
+ */
+ trimUrl: function (aUrl, aLabel, aSeq) {
+ if (!(aUrl instanceof Ci.nsIURL)) {
+ try {
+ // Use an nsIURL to parse all the url path parts.
+ aUrl = Services.io.newURI(aUrl, null, null).QueryInterface(Ci.nsIURL);
+ } catch (e) {
+ // This doesn't look like a url, or nsIURL can't handle it.
+ return aUrl;
+ }
+ }
+ if (!aSeq) {
+ let name = aUrl.fileName;
+ if (name) {
+ // This is a regular file url, get only the file name (contains the
+ // base name and extension if available).
+
+ // If this url contains an invalid query, unfortunately nsIURL thinks
+ // it's part of the file extension. It must be removed.
+ aLabel = aUrl.fileName.replace(/\&.*/, "");
+ } else {
+ // This is not a file url, hence there is no base name, nor extension.
+ // Proceed using other available information.
+ aLabel = "";
+ }
+ aSeq = 1;
+ }
+
+ // If we have a label and it doesn't only contain a query...
+ if (aLabel && aLabel.indexOf("?") != 0) {
+ // A page may contain multiple requests to the same url but with different
+ // queries. It is *not* redundant to show each one.
+ if (!DebuggerView.Sources.getItemForAttachment(e => e.label == aLabel)) {
+ return aLabel;
+ }
+ }
+
+ // Append the url query.
+ if (aSeq == 1) {
+ let query = aUrl.query;
+ if (query) {
+ return this.trimUrl(aUrl, aLabel + "?" + query, aSeq + 1);
+ }
+ aSeq++;
+ }
+ // Append the url reference.
+ if (aSeq == 2) {
+ let ref = aUrl.ref;
+ if (ref) {
+ return this.trimUrl(aUrl, aLabel + "#" + aUrl.ref, aSeq + 1);
+ }
+ aSeq++;
+ }
+ // Prepend the url directory.
+ if (aSeq == 3) {
+ let dir = aUrl.directory;
+ if (dir) {
+ return this.trimUrl(aUrl, dir.replace(/^\//, "") + aLabel, aSeq + 1);
+ }
+ aSeq++;
+ }
+ // Prepend the hostname and port number.
+ if (aSeq == 4) {
+ let host;
+ try {
+ // Bug 1261860: jar: URLs throw when accessing `hostPost`
+ host = aUrl.hostPort;
+ } catch (e) {}
+ if (host) {
+ return this.trimUrl(aUrl, host + "/" + aLabel, aSeq + 1);
+ }
+ aSeq++;
+ }
+ // Use the whole url spec but ignoring the reference.
+ if (aSeq == 5) {
+ return this.trimUrl(aUrl, aUrl.specIgnoringRef, aSeq + 1);
+ }
+ // Give up.
+ return aUrl.spec;
+ },
+
+ parseSource: function (aDebuggerView, aParser) {
+ let editor = aDebuggerView.editor;
+
+ let contents = editor.getText();
+ let location = aDebuggerView.Sources.selectedValue;
+ let parsedSource = aParser.get(contents, location);
+
+ return parsedSource;
+ },
+
+ findIdentifier: function (aEditor, parsedSource, x, y) {
+ let editor = aEditor;
+
+ // Calculate the editor's line and column at the current x and y coords.
+ let hoveredPos = editor.getPositionFromCoords({ left: x, top: y });
+ let hoveredOffset = editor.getOffset(hoveredPos);
+ let hoveredLine = hoveredPos.line;
+ let hoveredColumn = hoveredPos.ch;
+
+ let scriptInfo = parsedSource.getScriptInfo(hoveredOffset);
+
+ // If the script length is negative, we're not hovering JS source code.
+ if (scriptInfo.length == -1) {
+ return;
+ }
+
+ // Using the script offset, determine the actual line and column inside the
+ // script, to use when finding identifiers.
+ let scriptStart = editor.getPosition(scriptInfo.start);
+ let scriptLineOffset = scriptStart.line;
+ let scriptColumnOffset = (hoveredLine == scriptStart.line ? scriptStart.ch : 0);
+
+ let scriptLine = hoveredLine - scriptLineOffset;
+ let scriptColumn = hoveredColumn - scriptColumnOffset;
+ let identifierInfo = parsedSource.getIdentifierAt({
+ line: scriptLine + 1,
+ column: scriptColumn,
+ scriptIndex: scriptInfo.index
+ });
+
+ return identifierInfo;
+ }
+};
diff --git a/devtools/client/debugger/views/filter-view.js b/devtools/client/debugger/views/filter-view.js
new file mode 100644
index 000000000..460b1201c
--- /dev/null
+++ b/devtools/client/debugger/views/filter-view.js
@@ -0,0 +1,925 @@
+/* -*- 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/. */
+/* import-globals-from ../debugger-controller.js */
+/* import-globals-from ../debugger-view.js */
+/* import-globals-from ../utils.js */
+/* globals document, window */
+"use strict";
+
+/**
+ * Functions handling the filtering UI.
+ */
+function FilterView(DebuggerController, DebuggerView) {
+ dumpn("FilterView was instantiated");
+
+ this.Parser = DebuggerController.Parser;
+
+ this.DebuggerView = DebuggerView;
+ this.FilteredSources = new FilteredSourcesView(DebuggerView);
+ this.FilteredFunctions = new FilteredFunctionsView(DebuggerController.SourceScripts,
+ DebuggerController.Parser,
+ DebuggerView);
+
+ this._onClick = this._onClick.bind(this);
+ this._onInput = this._onInput.bind(this);
+ this._onKeyPress = this._onKeyPress.bind(this);
+ this._onBlur = this._onBlur.bind(this);
+}
+
+FilterView.prototype = {
+ /**
+ * Initialization function, called when the debugger is started.
+ */
+ initialize: function () {
+ dumpn("Initializing the FilterView");
+
+ this._searchbox = document.getElementById("searchbox");
+ this._searchboxHelpPanel = document.getElementById("searchbox-help-panel");
+ this._filterLabel = document.getElementById("filter-label");
+ this._globalOperatorButton = document.getElementById("global-operator-button");
+ this._globalOperatorLabel = document.getElementById("global-operator-label");
+ this._functionOperatorButton = document.getElementById("function-operator-button");
+ this._functionOperatorLabel = document.getElementById("function-operator-label");
+ this._tokenOperatorButton = document.getElementById("token-operator-button");
+ this._tokenOperatorLabel = document.getElementById("token-operator-label");
+ this._lineOperatorButton = document.getElementById("line-operator-button");
+ this._lineOperatorLabel = document.getElementById("line-operator-label");
+ this._variableOperatorButton = document.getElementById("variable-operator-button");
+ this._variableOperatorLabel = document.getElementById("variable-operator-label");
+
+ this._fileSearchKey = ShortcutUtils.prettifyShortcut(document.getElementById("fileSearchKey"));
+ this._globalSearchKey = ShortcutUtils.prettifyShortcut(document.getElementById("globalSearchKey"));
+ this._filteredFunctionsKey = ShortcutUtils.prettifyShortcut(document.getElementById("functionSearchKey"));
+ this._tokenSearchKey = ShortcutUtils.prettifyShortcut(document.getElementById("tokenSearchKey"));
+ this._lineSearchKey = ShortcutUtils.prettifyShortcut(document.getElementById("lineSearchKey"));
+ this._variableSearchKey = ShortcutUtils.prettifyShortcut(document.getElementById("variableSearchKey"));
+
+ this._searchbox.addEventListener("click", this._onClick, false);
+ this._searchbox.addEventListener("select", this._onInput, false);
+ this._searchbox.addEventListener("input", this._onInput, false);
+ this._searchbox.addEventListener("keypress", this._onKeyPress, false);
+ this._searchbox.addEventListener("blur", this._onBlur, false);
+
+ let placeholder = L10N.getFormatStr("emptySearchText", this._fileSearchKey);
+ this._searchbox.setAttribute("placeholder", placeholder);
+
+ this._globalOperatorButton.setAttribute("label", SEARCH_GLOBAL_FLAG);
+ this._functionOperatorButton.setAttribute("label", SEARCH_FUNCTION_FLAG);
+ this._tokenOperatorButton.setAttribute("label", SEARCH_TOKEN_FLAG);
+ this._lineOperatorButton.setAttribute("label", SEARCH_LINE_FLAG);
+ this._variableOperatorButton.setAttribute("label", SEARCH_VARIABLE_FLAG);
+
+ this._filterLabel.setAttribute("value",
+ L10N.getFormatStr("searchPanelFilter", this._fileSearchKey));
+ this._globalOperatorLabel.setAttribute("value",
+ L10N.getFormatStr("searchPanelGlobal", this._globalSearchKey));
+ this._functionOperatorLabel.setAttribute("value",
+ L10N.getFormatStr("searchPanelFunction", this._filteredFunctionsKey));
+ this._tokenOperatorLabel.setAttribute("value",
+ L10N.getFormatStr("searchPanelToken", this._tokenSearchKey));
+ this._lineOperatorLabel.setAttribute("value",
+ L10N.getFormatStr("searchPanelGoToLine", this._lineSearchKey));
+ this._variableOperatorLabel.setAttribute("value",
+ L10N.getFormatStr("searchPanelVariable", this._variableSearchKey));
+
+ this.FilteredSources.initialize();
+ this.FilteredFunctions.initialize();
+
+ this._addCommands();
+ },
+
+ /**
+ * Destruction function, called when the debugger is closed.
+ */
+ destroy: function () {
+ dumpn("Destroying the FilterView");
+
+ this._searchbox.removeEventListener("click", this._onClick, false);
+ this._searchbox.removeEventListener("select", this._onInput, false);
+ this._searchbox.removeEventListener("input", this._onInput, false);
+ this._searchbox.removeEventListener("keypress", this._onKeyPress, false);
+ this._searchbox.removeEventListener("blur", this._onBlur, false);
+
+ this.FilteredSources.destroy();
+ this.FilteredFunctions.destroy();
+ },
+
+ /**
+ * Add commands that XUL can fire.
+ */
+ _addCommands: function () {
+ XULUtils.addCommands(document.getElementById("debuggerCommands"), {
+ fileSearchCommand: () => this._doFileSearch(),
+ globalSearchCommand: () => this._doGlobalSearch(),
+ functionSearchCommand: () => this._doFunctionSearch(),
+ tokenSearchCommand: () => this._doTokenSearch(),
+ lineSearchCommand: () => this._doLineSearch(),
+ variableSearchCommand: () => this._doVariableSearch(),
+ variablesFocusCommand: () => this._doVariablesFocus()
+ });
+ },
+
+ /**
+ * Gets the entered operator and arguments in the searchbox.
+ * @return array
+ */
+ get searchData() {
+ let operator = "", args = [];
+
+ let rawValue = this._searchbox.value;
+ let rawLength = rawValue.length;
+ let globalFlagIndex = rawValue.indexOf(SEARCH_GLOBAL_FLAG);
+ let functionFlagIndex = rawValue.indexOf(SEARCH_FUNCTION_FLAG);
+ let variableFlagIndex = rawValue.indexOf(SEARCH_VARIABLE_FLAG);
+ let tokenFlagIndex = rawValue.lastIndexOf(SEARCH_TOKEN_FLAG);
+ let lineFlagIndex = rawValue.lastIndexOf(SEARCH_LINE_FLAG);
+
+ // This is not a global, function or variable search, allow file/line flags.
+ if (globalFlagIndex != 0 && functionFlagIndex != 0 && variableFlagIndex != 0) {
+ // Token search has precedence over line search.
+ if (tokenFlagIndex != -1) {
+ operator = SEARCH_TOKEN_FLAG;
+ args.push(rawValue.slice(0, tokenFlagIndex)); // file
+ args.push(rawValue.substr(tokenFlagIndex + 1, rawLength)); // token
+ } else if (lineFlagIndex != -1) {
+ operator = SEARCH_LINE_FLAG;
+ args.push(rawValue.slice(0, lineFlagIndex)); // file
+ args.push(+rawValue.substr(lineFlagIndex + 1, rawLength) || 0); // line
+ } else {
+ args.push(rawValue);
+ }
+ }
+ // Global searches dissalow the use of file or line flags.
+ else if (globalFlagIndex == 0) {
+ operator = SEARCH_GLOBAL_FLAG;
+ args.push(rawValue.slice(1));
+ }
+ // Function searches dissalow the use of file or line flags.
+ else if (functionFlagIndex == 0) {
+ operator = SEARCH_FUNCTION_FLAG;
+ args.push(rawValue.slice(1));
+ }
+ // Variable searches dissalow the use of file or line flags.
+ else if (variableFlagIndex == 0) {
+ operator = SEARCH_VARIABLE_FLAG;
+ args.push(rawValue.slice(1));
+ }
+
+ return [operator, args];
+ },
+
+ /**
+ * Returns the current search operator.
+ * @return string
+ */
+ get searchOperator() {
+ return this.searchData[0];
+ },
+
+ /**
+ * Returns the current search arguments.
+ * @return array
+ */
+ get searchArguments() {
+ return this.searchData[1];
+ },
+
+ /**
+ * Clears the text from the searchbox and any changed views.
+ */
+ clearSearch: function () {
+ this._searchbox.value = "";
+ this.clearViews();
+
+ this.FilteredSources.clearView();
+ this.FilteredFunctions.clearView();
+ },
+
+ /**
+ * Clears all the views that may pop up when searching.
+ */
+ clearViews: function () {
+ this.DebuggerView.GlobalSearch.clearView();
+ this.FilteredSources.clearView();
+ this.FilteredFunctions.clearView();
+ this._searchboxHelpPanel.hidePopup();
+ },
+
+ /**
+ * Performs a line search if necessary.
+ * (Jump to lines in the currently visible source).
+ *
+ * @param number aLine
+ * The source line number to jump to.
+ */
+ _performLineSearch: function (aLine) {
+ // Make sure we're actually searching for a valid line.
+ if (aLine) {
+ this.DebuggerView.editor.setCursor({ line: aLine - 1, ch: 0 }, "center");
+ }
+ },
+
+ /**
+ * Performs a token search if necessary.
+ * (Search for tokens in the currently visible source).
+ *
+ * @param string aToken
+ * The source token to find.
+ */
+ _performTokenSearch: function (aToken) {
+ // Make sure we're actually searching for a valid token.
+ if (!aToken) {
+ return;
+ }
+ this.DebuggerView.editor.find(aToken);
+ },
+
+ /**
+ * The click listener for the search container.
+ */
+ _onClick: function () {
+ // If there's some text in the searchbox, displaying a panel would
+ // interfere with double/triple click default behaviors.
+ if (!this._searchbox.value) {
+ this._searchboxHelpPanel.openPopup(this._searchbox);
+ }
+ },
+
+ /**
+ * The input listener for the search container.
+ */
+ _onInput: function () {
+ this.clearViews();
+
+ // Make sure we're actually searching for something.
+ if (!this._searchbox.value) {
+ return;
+ }
+
+ // Perform the required search based on the specified operator.
+ switch (this.searchOperator) {
+ case SEARCH_GLOBAL_FLAG:
+ // Schedule a global search for when the user stops typing.
+ this.DebuggerView.GlobalSearch.scheduleSearch(this.searchArguments[0]);
+ break;
+ case SEARCH_FUNCTION_FLAG:
+ // Schedule a function search for when the user stops typing.
+ this.FilteredFunctions.scheduleSearch(this.searchArguments[0]);
+ break;
+ case SEARCH_VARIABLE_FLAG:
+ // Schedule a variable search for when the user stops typing.
+ this.DebuggerView.Variables.scheduleSearch(this.searchArguments[0]);
+ break;
+ case SEARCH_TOKEN_FLAG:
+ // Schedule a file+token search for when the user stops typing.
+ this.FilteredSources.scheduleSearch(this.searchArguments[0]);
+ this._performTokenSearch(this.searchArguments[1]);
+ break;
+ case SEARCH_LINE_FLAG:
+ // Schedule a file+line search for when the user stops typing.
+ this.FilteredSources.scheduleSearch(this.searchArguments[0]);
+ this._performLineSearch(this.searchArguments[1]);
+ break;
+ default:
+ // Schedule a file only search for when the user stops typing.
+ this.FilteredSources.scheduleSearch(this.searchArguments[0]);
+ break;
+ }
+ },
+
+ /**
+ * The key press listener for the search container.
+ */
+ _onKeyPress: function (e) {
+ // This attribute is not implemented in Gecko at this time, see bug 680830.
+ e.char = String.fromCharCode(e.charCode);
+
+ // Perform the required action based on the specified operator.
+ let [operator, args] = this.searchData;
+ let isGlobalSearch = operator == SEARCH_GLOBAL_FLAG;
+ let isFunctionSearch = operator == SEARCH_FUNCTION_FLAG;
+ let isVariableSearch = operator == SEARCH_VARIABLE_FLAG;
+ let isTokenSearch = operator == SEARCH_TOKEN_FLAG;
+ let isLineSearch = operator == SEARCH_LINE_FLAG;
+ let isFileOnlySearch = !operator && args.length == 1;
+
+ // Depending on the pressed keys, determine to correct action to perform.
+ let actionToPerform;
+
+ // Meta+G and Ctrl+N focus next matches.
+ if ((e.char == "g" && e.metaKey) || e.char == "n" && e.ctrlKey) {
+ actionToPerform = "selectNext";
+ }
+ // Meta+Shift+G and Ctrl+P focus previous matches.
+ else if ((e.char == "G" && e.metaKey) || e.char == "p" && e.ctrlKey) {
+ actionToPerform = "selectPrev";
+ }
+ // Return, enter, down and up keys focus next or previous matches, while
+ // the escape key switches focus from the search container.
+ else switch (e.keyCode) {
+ case KeyCodes.DOM_VK_RETURN:
+ var isReturnKey = true;
+ // If the shift key is pressed, focus on the previous result
+ actionToPerform = e.shiftKey ? "selectPrev" : "selectNext";
+ break;
+ case KeyCodes.DOM_VK_DOWN:
+ actionToPerform = "selectNext";
+ break;
+ case KeyCodes.DOM_VK_UP:
+ actionToPerform = "selectPrev";
+ break;
+ }
+
+ // If there's no action to perform, or no operator, file line or token
+ // were specified, then this is either a broken or empty search.
+ if (!actionToPerform || (!operator && !args.length)) {
+ this.DebuggerView.editor.dropSelection();
+ return;
+ }
+
+ e.preventDefault();
+ e.stopPropagation();
+
+ // Jump to the next/previous entry in the global search, or perform
+ // a new global search immediately
+ if (isGlobalSearch) {
+ let targetView = this.DebuggerView.GlobalSearch;
+ if (!isReturnKey) {
+ targetView[actionToPerform]();
+ } else if (targetView.hidden) {
+ targetView.scheduleSearch(args[0], 0);
+ }
+ return;
+ }
+
+ // Jump to the next/previous entry in the function search, perform
+ // a new function search immediately, or clear it.
+ if (isFunctionSearch) {
+ let targetView = this.FilteredFunctions;
+ if (!isReturnKey) {
+ targetView[actionToPerform]();
+ } else if (targetView.hidden) {
+ targetView.scheduleSearch(args[0], 0);
+ } else {
+ if (!targetView.selectedItem) {
+ targetView.selectedIndex = 0;
+ }
+ this.clearSearch();
+ }
+ return;
+ }
+
+ // Perform a new variable search immediately.
+ if (isVariableSearch) {
+ let targetView = this.DebuggerView.Variables;
+ if (isReturnKey) {
+ targetView.scheduleSearch(args[0], 0);
+ }
+ return;
+ }
+
+ // Jump to the next/previous entry in the file search, perform
+ // a new file search immediately, or clear it.
+ if (isFileOnlySearch) {
+ let targetView = this.FilteredSources;
+ if (!isReturnKey) {
+ targetView[actionToPerform]();
+ } else if (targetView.hidden) {
+ targetView.scheduleSearch(args[0], 0);
+ } else {
+ if (!targetView.selectedItem) {
+ targetView.selectedIndex = 0;
+ }
+ this.clearSearch();
+ }
+ return;
+ }
+
+ // Jump to the next/previous instance of the currently searched token.
+ if (isTokenSearch) {
+ let methods = { selectNext: "findNext", selectPrev: "findPrev" };
+ this.DebuggerView.editor[methods[actionToPerform]]();
+ return;
+ }
+
+ // Increment/decrement the currently searched caret line.
+ if (isLineSearch) {
+ let [, line] = args;
+ let amounts = { selectNext: 1, selectPrev: -1 };
+
+ // Modify the line number and jump to it.
+ line += !isReturnKey ? amounts[actionToPerform] : 0;
+ let lineCount = this.DebuggerView.editor.lineCount();
+ let lineTarget = line < 1 ? 1 : line > lineCount ? lineCount : line;
+ this._doSearch(SEARCH_LINE_FLAG, lineTarget);
+ return;
+ }
+ },
+
+ /**
+ * The blur listener for the search container.
+ */
+ _onBlur: function () {
+ this.clearViews();
+ },
+
+ /**
+ * Called when a filtering key sequence was pressed.
+ *
+ * @param string aOperator
+ * The operator to use for filtering.
+ */
+ _doSearch: function (aOperator = "", aText = "") {
+ this._searchbox.focus();
+ this._searchbox.value = ""; // Need to clear value beforehand. Bug 779738.
+
+ if (aText) {
+ this._searchbox.value = aOperator + aText;
+ return;
+ }
+ if (this.DebuggerView.editor.somethingSelected()) {
+ this._searchbox.value = aOperator + this.DebuggerView.editor.getSelection();
+ return;
+ }
+
+ let content = this.DebuggerView.editor.getText();
+ if (content.length < this.DebuggerView.LARGE_FILE_SIZE &&
+ SEARCH_AUTOFILL.indexOf(aOperator) != -1) {
+ let cursor = this.DebuggerView.editor.getCursor();
+ let location = this.DebuggerView.Sources.selectedItem.attachment.source.url;
+ let source = this.Parser.get(content, location);
+ let identifier = source.getIdentifierAt({ line: cursor.line + 1, column: cursor.ch });
+
+ if (identifier && identifier.name) {
+ this._searchbox.value = aOperator + identifier.name;
+ this._searchbox.select();
+ this._searchbox.selectionStart += aOperator.length;
+ return;
+ }
+ }
+ this._searchbox.value = aOperator;
+ },
+
+ /**
+ * Called when the source location filter key sequence was pressed.
+ */
+ _doFileSearch: function () {
+ this._doSearch();
+ this._searchboxHelpPanel.openPopup(this._searchbox);
+ },
+
+ /**
+ * Called when the global search filter key sequence was pressed.
+ */
+ _doGlobalSearch: function () {
+ this._doSearch(SEARCH_GLOBAL_FLAG);
+ this._searchboxHelpPanel.hidePopup();
+ },
+
+ /**
+ * Called when the source function filter key sequence was pressed.
+ */
+ _doFunctionSearch: function () {
+ this._doSearch(SEARCH_FUNCTION_FLAG);
+ this._searchboxHelpPanel.hidePopup();
+ },
+
+ /**
+ * Called when the source token filter key sequence was pressed.
+ */
+ _doTokenSearch: function () {
+ this._doSearch(SEARCH_TOKEN_FLAG);
+ this._searchboxHelpPanel.hidePopup();
+ },
+
+ /**
+ * Called when the source line filter key sequence was pressed.
+ */
+ _doLineSearch: function () {
+ this._doSearch(SEARCH_LINE_FLAG);
+ this._searchboxHelpPanel.hidePopup();
+ },
+
+ /**
+ * Called when the variable search filter key sequence was pressed.
+ */
+ _doVariableSearch: function () {
+ this._doSearch(SEARCH_VARIABLE_FLAG);
+ this._searchboxHelpPanel.hidePopup();
+ },
+
+ /**
+ * Called when the variables focus key sequence was pressed.
+ */
+ _doVariablesFocus: function () {
+ this.DebuggerView.showInstrumentsPane();
+ this.DebuggerView.Variables.focusFirstVisibleItem();
+ },
+
+ _searchbox: null,
+ _searchboxHelpPanel: null,
+ _globalOperatorButton: null,
+ _globalOperatorLabel: null,
+ _functionOperatorButton: null,
+ _functionOperatorLabel: null,
+ _tokenOperatorButton: null,
+ _tokenOperatorLabel: null,
+ _lineOperatorButton: null,
+ _lineOperatorLabel: null,
+ _variableOperatorButton: null,
+ _variableOperatorLabel: null,
+ _fileSearchKey: "",
+ _globalSearchKey: "",
+ _filteredFunctionsKey: "",
+ _tokenSearchKey: "",
+ _lineSearchKey: "",
+ _variableSearchKey: "",
+};
+
+/**
+ * Functions handling the filtered sources UI.
+ */
+function FilteredSourcesView(DebuggerView) {
+ dumpn("FilteredSourcesView was instantiated");
+
+ this.DebuggerView = DebuggerView;
+
+ this._onClick = this._onClick.bind(this);
+ this._onSelect = this._onSelect.bind(this);
+}
+
+FilteredSourcesView.prototype = Heritage.extend(ResultsPanelContainer.prototype, {
+ /**
+ * Initialization function, called when the debugger is started.
+ */
+ initialize: function () {
+ dumpn("Initializing the FilteredSourcesView");
+
+ this.anchor = document.getElementById("searchbox");
+ this.widget.addEventListener("select", this._onSelect, false);
+ this.widget.addEventListener("click", this._onClick, false);
+ },
+
+ /**
+ * Destruction function, called when the debugger is closed.
+ */
+ destroy: function () {
+ dumpn("Destroying the FilteredSourcesView");
+
+ this.widget.removeEventListener("select", this._onSelect, false);
+ this.widget.removeEventListener("click", this._onClick, false);
+ this.anchor = null;
+ },
+
+ /**
+ * Schedules searching for a source.
+ *
+ * @param string aToken
+ * The function to search for.
+ * @param number aWait
+ * The amount of milliseconds to wait until draining.
+ */
+ scheduleSearch: function (aToken, aWait) {
+ // The amount of time to wait for the requests to settle.
+ let maxDelay = FILE_SEARCH_ACTION_MAX_DELAY;
+ let delay = aWait === undefined ? maxDelay / aToken.length : aWait;
+
+ // Allow requests to settle down first.
+ setNamedTimeout("sources-search", delay, () => this._doSearch(aToken));
+ },
+
+ /**
+ * Finds file matches in all the displayed sources.
+ *
+ * @param string aToken
+ * The string to search for.
+ */
+ _doSearch: function (aToken, aStore = []) {
+ // Don't continue filtering if the searched token is an empty string.
+ // In contrast with function searching, in this case we don't want to
+ // show a list of all the files when no search token was supplied.
+ if (!aToken) {
+ return;
+ }
+
+ for (let item of this.DebuggerView.Sources.items) {
+ let lowerCaseLabel = item.attachment.label.toLowerCase();
+ let lowerCaseToken = aToken.toLowerCase();
+ if (lowerCaseLabel.match(lowerCaseToken)) {
+ aStore.push(item);
+ }
+
+ // Once the maximum allowed number of results is reached, proceed
+ // with building the UI immediately.
+ if (aStore.length >= RESULTS_PANEL_MAX_RESULTS) {
+ this._syncView(aStore);
+ return;
+ }
+ }
+
+ // Couldn't reach the maximum allowed number of results, but that's ok,
+ // continue building the UI.
+ this._syncView(aStore);
+ },
+
+ /**
+ * Updates the list of sources displayed in this container.
+ *
+ * @param array aSearchResults
+ * The results array, containing search details for each source.
+ */
+ _syncView: function (aSearchResults) {
+ // If there are no matches found, keep the popup hidden and avoid
+ // creating the view.
+ if (!aSearchResults.length) {
+ window.emit(EVENTS.FILE_SEARCH_MATCH_NOT_FOUND);
+ return;
+ }
+
+ for (let item of aSearchResults) {
+ let url = item.attachment.source.url;
+
+ if (url) {
+ // Create the element node for the location item.
+ let itemView = this._createItemView(
+ SourceUtils.trimUrlLength(item.attachment.label),
+ SourceUtils.trimUrlLength(url, 0, "start")
+ );
+
+ // Append a location item to this container for each match.
+ this.push([itemView], {
+ index: -1, /* specifies on which position should the item be appended */
+ attachment: {
+ url: url
+ }
+ });
+ }
+ }
+
+ // There's at least one item displayed in this container. Don't select it
+ // automatically if not forced (by tests) or in tandem with an
+ // operator.
+ if (this._autoSelectFirstItem || this.DebuggerView.Filtering.searchOperator) {
+ this.selectedIndex = 0;
+ }
+ this.hidden = false;
+
+ // Signal that file search matches were found and displayed.
+ window.emit(EVENTS.FILE_SEARCH_MATCH_FOUND);
+ },
+
+ /**
+ * The click listener for this container.
+ */
+ _onClick: function (e) {
+ let locationItem = this.getItemForElement(e.target);
+ if (locationItem) {
+ this.selectedItem = locationItem;
+ this.DebuggerView.Filtering.clearSearch();
+ }
+ },
+
+ /**
+ * The select listener for this container.
+ *
+ * @param object aItem
+ * The item associated with the element to select.
+ */
+ _onSelect: function ({ detail: locationItem }) {
+ if (locationItem) {
+ let source = queries.getSourceByURL(DebuggerController.getState(),
+ locationItem.attachment.url);
+ this.DebuggerView.setEditorLocation(source.actor, undefined, {
+ noCaret: true,
+ noDebug: true
+ });
+ }
+ }
+});
+
+/**
+ * Functions handling the function search UI.
+ */
+function FilteredFunctionsView(SourceScripts, Parser, DebuggerView) {
+ dumpn("FilteredFunctionsView was instantiated");
+
+ this.SourceScripts = SourceScripts;
+ this.Parser = Parser;
+ this.DebuggerView = DebuggerView;
+
+ this._onClick = this._onClick.bind(this);
+ this._onSelect = this._onSelect.bind(this);
+}
+
+FilteredFunctionsView.prototype = Heritage.extend(ResultsPanelContainer.prototype, {
+ /**
+ * Initialization function, called when the debugger is started.
+ */
+ initialize: function () {
+ dumpn("Initializing the FilteredFunctionsView");
+
+ this.anchor = document.getElementById("searchbox");
+ this.widget.addEventListener("select", this._onSelect, false);
+ this.widget.addEventListener("click", this._onClick, false);
+ },
+
+ /**
+ * Destruction function, called when the debugger is closed.
+ */
+ destroy: function () {
+ dumpn("Destroying the FilteredFunctionsView");
+
+ this.widget.removeEventListener("select", this._onSelect, false);
+ this.widget.removeEventListener("click", this._onClick, false);
+ this.anchor = null;
+ },
+
+ /**
+ * Schedules searching for a function in all of the sources.
+ *
+ * @param string aToken
+ * The function to search for.
+ * @param number aWait
+ * The amount of milliseconds to wait until draining.
+ */
+ scheduleSearch: function (aToken, aWait) {
+ // The amount of time to wait for the requests to settle.
+ let maxDelay = FUNCTION_SEARCH_ACTION_MAX_DELAY;
+ let delay = aWait === undefined ? maxDelay / aToken.length : aWait;
+
+ // Allow requests to settle down first.
+ setNamedTimeout("function-search", delay, () => {
+ // Start fetching as many sources as possible, then perform the search.
+ let actors = this.DebuggerView.Sources.values;
+ let sourcesFetched = DebuggerController.dispatch(actions.getTextForSources(actors));
+ sourcesFetched.then(aSources => this._doSearch(aToken, aSources));
+ });
+ },
+
+ /**
+ * Finds function matches in all the sources stored in the cache, and groups
+ * them by location and line number.
+ *
+ * @param string aToken
+ * The string to search for.
+ * @param array aSources
+ * An array of [url, text] tuples for each source.
+ */
+ _doSearch: function (aToken, aSources, aStore = []) {
+ // Continue parsing even if the searched token is an empty string, to
+ // cache the syntax tree nodes generated by the reflection API.
+
+ // Make sure the currently displayed source is parsed first. Once the
+ // maximum allowed number of results are found, parsing will be halted.
+ let currentActor = this.DebuggerView.Sources.selectedValue;
+ let currentSource = aSources.filter(([actor]) => actor == currentActor)[0];
+ aSources.splice(aSources.indexOf(currentSource), 1);
+ aSources.unshift(currentSource);
+
+ // If not searching for a specific function, only parse the displayed source,
+ // which is now the first item in the sources array.
+ if (!aToken) {
+ aSources.splice(1);
+ }
+
+ for (let [actor, contents] of aSources) {
+ let item = this.DebuggerView.Sources.getItemByValue(actor);
+ let url = item.attachment.source.url;
+ if (!url) {
+ continue;
+ }
+
+ let parsedSource = this.Parser.get(contents, url);
+ let sourceResults = parsedSource.getNamedFunctionDefinitions(aToken);
+
+ for (let scriptResult of sourceResults) {
+ for (let parseResult of scriptResult) {
+ aStore.push({
+ sourceUrl: scriptResult.sourceUrl,
+ scriptOffset: scriptResult.scriptOffset,
+ functionName: parseResult.functionName,
+ functionLocation: parseResult.functionLocation,
+ inferredName: parseResult.inferredName,
+ inferredChain: parseResult.inferredChain,
+ inferredLocation: parseResult.inferredLocation
+ });
+
+ // Once the maximum allowed number of results is reached, proceed
+ // with building the UI immediately.
+ if (aStore.length >= RESULTS_PANEL_MAX_RESULTS) {
+ this._syncView(aStore);
+ return;
+ }
+ }
+ }
+ }
+
+ // Couldn't reach the maximum allowed number of results, but that's ok,
+ // continue building the UI.
+ this._syncView(aStore);
+ },
+
+ /**
+ * Updates the list of functions displayed in this container.
+ *
+ * @param array aSearchResults
+ * The results array, containing search details for each source.
+ */
+ _syncView: function (aSearchResults) {
+ // If there are no matches found, keep the popup hidden and avoid
+ // creating the view.
+ if (!aSearchResults.length) {
+ window.emit(EVENTS.FUNCTION_SEARCH_MATCH_NOT_FOUND);
+ return;
+ }
+
+ for (let item of aSearchResults) {
+ // Some function expressions don't necessarily have a name, but the
+ // parser provides us with an inferred name from an enclosing
+ // VariableDeclarator, AssignmentExpression, ObjectExpression node.
+ if (item.functionName && item.inferredName &&
+ item.functionName != item.inferredName) {
+ let s = " " + L10N.getStr("functionSearchSeparatorLabel") + " ";
+ item.displayedName = item.inferredName + s + item.functionName;
+ }
+ // The function doesn't have an explicit name, but it could be inferred.
+ else if (item.inferredName) {
+ item.displayedName = item.inferredName;
+ }
+ // The function only has an explicit name.
+ else {
+ item.displayedName = item.functionName;
+ }
+
+ // Some function expressions have unexpected bounds, since they may not
+ // necessarily have an associated name defining them.
+ if (item.inferredLocation) {
+ item.actualLocation = item.inferredLocation;
+ } else {
+ item.actualLocation = item.functionLocation;
+ }
+
+ // Create the element node for the function item.
+ let itemView = this._createItemView(
+ SourceUtils.trimUrlLength(item.displayedName + "()"),
+ SourceUtils.trimUrlLength(item.sourceUrl, 0, "start"),
+ (item.inferredChain || []).join(".")
+ );
+
+ // Append a function item to this container for each match.
+ this.push([itemView], {
+ index: -1, /* specifies on which position should the item be appended */
+ attachment: item
+ });
+ }
+
+ // There's at least one item displayed in this container. Don't select it
+ // automatically if not forced (by tests).
+ if (this._autoSelectFirstItem) {
+ this.selectedIndex = 0;
+ }
+ this.hidden = false;
+
+ // Signal that function search matches were found and displayed.
+ window.emit(EVENTS.FUNCTION_SEARCH_MATCH_FOUND);
+ },
+
+ /**
+ * The click listener for this container.
+ */
+ _onClick: function (e) {
+ let functionItem = this.getItemForElement(e.target);
+ if (functionItem) {
+ this.selectedItem = functionItem;
+ this.DebuggerView.Filtering.clearSearch();
+ }
+ },
+
+ /**
+ * The select listener for this container.
+ */
+ _onSelect: function ({ detail: functionItem }) {
+ if (functionItem) {
+ let sourceUrl = functionItem.attachment.sourceUrl;
+ let actor = queries.getSourceByURL(DebuggerController.getState(), sourceUrl).actor;
+ let scriptOffset = functionItem.attachment.scriptOffset;
+ let actualLocation = functionItem.attachment.actualLocation;
+
+ this.DebuggerView.setEditorLocation(actor, actualLocation.start.line, {
+ charOffset: scriptOffset,
+ columnOffset: actualLocation.start.column,
+ align: "center",
+ noDebug: true
+ });
+ }
+ },
+
+ _searchTimeout: null,
+ _searchFunction: null,
+ _searchedToken: ""
+});
+
+DebuggerView.Filtering = new FilterView(DebuggerController, DebuggerView);
diff --git a/devtools/client/debugger/views/global-search-view.js b/devtools/client/debugger/views/global-search-view.js
new file mode 100644
index 000000000..c6a627971
--- /dev/null
+++ b/devtools/client/debugger/views/global-search-view.js
@@ -0,0 +1,756 @@
+/* -*- 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/. */
+/* import-globals-from ../debugger-controller.js */
+/* import-globals-from ../debugger-view.js */
+/* import-globals-from ../utils.js */
+/* globals document, window */
+"use strict";
+
+/**
+ * Functions handling the global search UI.
+ */
+function GlobalSearchView(DebuggerController, DebuggerView) {
+ dumpn("GlobalSearchView was instantiated");
+
+ this.SourceScripts = DebuggerController.SourceScripts;
+ this.DebuggerView = DebuggerView;
+
+ this._onHeaderClick = this._onHeaderClick.bind(this);
+ this._onLineClick = this._onLineClick.bind(this);
+ this._onMatchClick = this._onMatchClick.bind(this);
+}
+
+GlobalSearchView.prototype = Heritage.extend(WidgetMethods, {
+ /**
+ * Initialization function, called when the debugger is started.
+ */
+ initialize: function () {
+ dumpn("Initializing the GlobalSearchView");
+
+ this.widget = new SimpleListWidget(document.getElementById("globalsearch"));
+ this._splitter = document.querySelector("#globalsearch + .devtools-horizontal-splitter");
+
+ this.emptyText = L10N.getStr("noMatchingStringsText");
+ },
+
+ /**
+ * Destruction function, called when the debugger is closed.
+ */
+ destroy: function () {
+ dumpn("Destroying the GlobalSearchView");
+ },
+
+ /**
+ * Sets the results container hidden or visible. It's hidden by default.
+ * @param boolean aFlag
+ */
+ set hidden(aFlag) {
+ this.widget.setAttribute("hidden", aFlag);
+ this._splitter.setAttribute("hidden", aFlag);
+ },
+
+ /**
+ * Gets the visibility state of the global search container.
+ * @return boolean
+ */
+ get hidden() {
+ return this.widget.getAttribute("hidden") == "true" ||
+ this._splitter.getAttribute("hidden") == "true";
+ },
+
+ /**
+ * Hides and removes all items from this search container.
+ */
+ clearView: function () {
+ this.hidden = true;
+ this.empty();
+ },
+
+ /**
+ * Selects the next found item in this container.
+ * Does not change the currently focused node.
+ */
+ selectNext: function () {
+ let totalLineResults = LineResults.size();
+ if (!totalLineResults) {
+ return;
+ }
+ if (++this._currentlyFocusedMatch >= totalLineResults) {
+ this._currentlyFocusedMatch = 0;
+ }
+ this._onMatchClick({
+ target: LineResults.getElementAtIndex(this._currentlyFocusedMatch)
+ });
+ },
+
+ /**
+ * Selects the previously found item in this container.
+ * Does not change the currently focused node.
+ */
+ selectPrev: function () {
+ let totalLineResults = LineResults.size();
+ if (!totalLineResults) {
+ return;
+ }
+ if (--this._currentlyFocusedMatch < 0) {
+ this._currentlyFocusedMatch = totalLineResults - 1;
+ }
+ this._onMatchClick({
+ target: LineResults.getElementAtIndex(this._currentlyFocusedMatch)
+ });
+ },
+
+ /**
+ * Schedules searching for a string in all of the sources.
+ *
+ * @param string aToken
+ * The string to search for.
+ * @param number aWait
+ * The amount of milliseconds to wait until draining.
+ */
+ scheduleSearch: function (aToken, aWait) {
+ // The amount of time to wait for the requests to settle.
+ let maxDelay = GLOBAL_SEARCH_ACTION_MAX_DELAY;
+ let delay = aWait === undefined ? maxDelay / aToken.length : aWait;
+
+ // Allow requests to settle down first.
+ setNamedTimeout("global-search", delay, () => {
+ // Start fetching as many sources as possible, then perform the search.
+ let actors = this.DebuggerView.Sources.values;
+ let sourcesFetched = DebuggerController.dispatch(actions.getTextForSources(actors));
+ sourcesFetched.then(aSources => this._doSearch(aToken, aSources));
+ });
+ },
+
+ /**
+ * Finds string matches in all the sources stored in the controller's cache,
+ * and groups them by url and line number.
+ *
+ * @param string aToken
+ * The string to search for.
+ * @param array aSources
+ * An array of [url, text] tuples for each source.
+ */
+ _doSearch: function (aToken, aSources) {
+ // Don't continue filtering if the searched token is an empty string.
+ if (!aToken) {
+ this.clearView();
+ return;
+ }
+
+ // Search is not case sensitive, prepare the actual searched token.
+ let lowerCaseToken = aToken.toLowerCase();
+ let tokenLength = aToken.length;
+
+ // Create a Map containing search details for each source.
+ let globalResults = new GlobalResults();
+
+ // Search for the specified token in each source's text.
+ for (let [actor, text] of aSources) {
+ let item = this.DebuggerView.Sources.getItemByValue(actor);
+ let url = item.attachment.source.url;
+ if (!url) {
+ continue;
+ }
+
+ // Verify that the search token is found anywhere in the source.
+ if (!text.toLowerCase().includes(lowerCaseToken)) {
+ continue;
+ }
+ // ...and if so, create a Map containing search details for each line.
+ let sourceResults = new SourceResults(actor,
+ globalResults,
+ this.DebuggerView.Sources);
+
+ // Search for the specified token in each line's text.
+ text.split("\n").forEach((aString, aLine) => {
+ // Search is not case sensitive, prepare the actual searched line.
+ let lowerCaseLine = aString.toLowerCase();
+
+ // Verify that the search token is found anywhere in this line.
+ if (!lowerCaseLine.includes(lowerCaseToken)) {
+ return;
+ }
+ // ...and if so, create a Map containing search details for each word.
+ let lineResults = new LineResults(aLine, sourceResults);
+
+ // Search for the specified token this line's text.
+ lowerCaseLine.split(lowerCaseToken).reduce((aPrev, aCurr, aIndex, aArray) => {
+ let prevLength = aPrev.length;
+ let currLength = aCurr.length;
+
+ // Everything before the token is unmatched.
+ let unmatched = aString.substr(prevLength, currLength);
+ lineResults.add(unmatched);
+
+ // The lowered-case line was split by the lowered-case token. So,
+ // get the actual matched text from the original line's text.
+ if (aIndex != aArray.length - 1) {
+ let matched = aString.substr(prevLength + currLength, tokenLength);
+ let range = { start: prevLength + currLength, length: matched.length };
+ lineResults.add(matched, range, true);
+ }
+
+ // Continue with the next sub-region in this line's text.
+ return aPrev + aToken + aCurr;
+ }, "");
+
+ if (lineResults.matchCount) {
+ sourceResults.add(lineResults);
+ }
+ });
+
+ if (sourceResults.matchCount) {
+ globalResults.add(sourceResults);
+ }
+ }
+
+ // Rebuild the results, then signal if there are any matches.
+ if (globalResults.matchCount) {
+ this.hidden = false;
+ this._currentlyFocusedMatch = -1;
+ this._createGlobalResultsUI(globalResults);
+ window.emit(EVENTS.GLOBAL_SEARCH_MATCH_FOUND);
+ } else {
+ window.emit(EVENTS.GLOBAL_SEARCH_MATCH_NOT_FOUND);
+ }
+ },
+
+ /**
+ * Creates global search results entries and adds them to this container.
+ *
+ * @param GlobalResults aGlobalResults
+ * An object containing all source results, grouped by source location.
+ */
+ _createGlobalResultsUI: function (aGlobalResults) {
+ let i = 0;
+
+ for (let sourceResults of aGlobalResults) {
+ if (i++ == 0) {
+ this._createSourceResultsUI(sourceResults);
+ } else {
+ // Dispatch subsequent document manipulation operations, to avoid
+ // blocking the main thread when a large number of search results
+ // is found, thus giving the impression of faster searching.
+ Services.tm.currentThread.dispatch({ run:
+ this._createSourceResultsUI.bind(this, sourceResults)
+ }, 0);
+ }
+ }
+ },
+
+ /**
+ * Creates source search results entries and adds them to this container.
+ *
+ * @param SourceResults aSourceResults
+ * An object containing all the matched lines for a specific source.
+ */
+ _createSourceResultsUI: function (aSourceResults) {
+ // Create the element node for the source results item.
+ let container = document.createElement("hbox");
+ aSourceResults.createView(container, {
+ onHeaderClick: this._onHeaderClick,
+ onLineClick: this._onLineClick,
+ onMatchClick: this._onMatchClick
+ });
+
+ // Append a source results item to this container.
+ let item = this.push([container], {
+ index: -1, /* specifies on which position should the item be appended */
+ attachment: {
+ sourceResults: aSourceResults
+ }
+ });
+ },
+
+ /**
+ * The click listener for a results header.
+ */
+ _onHeaderClick: function (e) {
+ let sourceResultsItem = SourceResults.getItemForElement(e.target);
+ sourceResultsItem.instance.toggle(e);
+ },
+
+ /**
+ * The click listener for a results line.
+ */
+ _onLineClick: function (e) {
+ let lineResultsItem = LineResults.getItemForElement(e.target);
+ this._onMatchClick({ target: lineResultsItem.firstMatch });
+ },
+
+ /**
+ * The click listener for a result match.
+ */
+ _onMatchClick: function (e) {
+ if (e instanceof Event) {
+ e.preventDefault();
+ e.stopPropagation();
+ }
+
+ let target = e.target;
+ let sourceResultsItem = SourceResults.getItemForElement(target);
+ let lineResultsItem = LineResults.getItemForElement(target);
+
+ sourceResultsItem.instance.expand();
+ this._currentlyFocusedMatch = LineResults.indexOfElement(target);
+ this._scrollMatchIntoViewIfNeeded(target);
+ this._bounceMatch(target);
+
+ let actor = sourceResultsItem.instance.actor;
+ let line = lineResultsItem.instance.line;
+
+ this.DebuggerView.setEditorLocation(actor, line + 1, { noDebug: true });
+
+ let range = lineResultsItem.lineData.range;
+ let cursor = this.DebuggerView.editor.getOffset({ line: line, ch: 0 });
+ let [ anchor, head ] = this.DebuggerView.editor.getPosition(
+ cursor + range.start,
+ cursor + range.start + range.length
+ );
+
+ this.DebuggerView.editor.setSelection(anchor, head);
+ },
+
+ /**
+ * Scrolls a match into view if not already visible.
+ *
+ * @param nsIDOMNode aMatch
+ * The match to scroll into view.
+ */
+ _scrollMatchIntoViewIfNeeded: function (aMatch) {
+ this.widget.ensureElementIsVisible(aMatch);
+ },
+
+ /**
+ * Starts a bounce animation for a match.
+ *
+ * @param nsIDOMNode aMatch
+ * The match to start a bounce animation for.
+ */
+ _bounceMatch: function (aMatch) {
+ Services.tm.currentThread.dispatch({ run: () => {
+ aMatch.addEventListener("transitionend", function onEvent() {
+ aMatch.removeEventListener("transitionend", onEvent);
+ aMatch.removeAttribute("focused");
+ });
+ aMatch.setAttribute("focused", "");
+ }}, 0);
+ aMatch.setAttribute("focusing", "");
+ },
+
+ _splitter: null,
+ _currentlyFocusedMatch: -1,
+ _forceExpandResults: false
+});
+
+DebuggerView.GlobalSearch = new GlobalSearchView(DebuggerController, DebuggerView);
+
+/**
+ * An object containing all source results, grouped by source location.
+ * Iterable via "for (let [location, sourceResults] of globalResults) { }".
+ */
+function GlobalResults() {
+ this._store = [];
+ SourceResults._itemsByElement = new Map();
+ LineResults._itemsByElement = new Map();
+}
+
+GlobalResults.prototype = {
+ /**
+ * Adds source results to this store.
+ *
+ * @param SourceResults aSourceResults
+ * An object containing search results for a specific source.
+ */
+ add: function (aSourceResults) {
+ this._store.push(aSourceResults);
+ },
+
+ /**
+ * Gets the number of source results in this store.
+ */
+ get matchCount() {
+ return this._store.length;
+ }
+};
+
+/**
+ * An object containing all the matched lines for a specific source.
+ * Iterable via "for (let [lineNumber, lineResults] of sourceResults) { }".
+ *
+ * @param string aActor
+ * The target source actor id.
+ * @param GlobalResults aGlobalResults
+ * An object containing all source results, grouped by source location.
+ */
+function SourceResults(aActor, aGlobalResults, sourcesView) {
+ let item = sourcesView.getItemByValue(aActor);
+ this.actor = aActor;
+ this.label = item.attachment.source.url;
+ this._globalResults = aGlobalResults;
+ this._store = [];
+}
+
+SourceResults.prototype = {
+ /**
+ * Adds line results to this store.
+ *
+ * @param LineResults aLineResults
+ * An object containing search results for a specific line.
+ */
+ add: function (aLineResults) {
+ this._store.push(aLineResults);
+ },
+
+ /**
+ * Gets the number of line results in this store.
+ */
+ get matchCount() {
+ return this._store.length;
+ },
+
+ /**
+ * Expands the element, showing all the added details.
+ */
+ expand: function () {
+ this._resultsContainer.removeAttribute("hidden");
+ this._arrow.setAttribute("open", "");
+ },
+
+ /**
+ * Collapses the element, hiding all the added details.
+ */
+ collapse: function () {
+ this._resultsContainer.setAttribute("hidden", "true");
+ this._arrow.removeAttribute("open");
+ },
+
+ /**
+ * Toggles between the element collapse/expand state.
+ */
+ toggle: function (e) {
+ this.expanded ^= 1;
+ },
+
+ /**
+ * Gets this element's expanded state.
+ * @return boolean
+ */
+ get expanded() {
+ return this._resultsContainer.getAttribute("hidden") != "true" &&
+ this._arrow.hasAttribute("open");
+ },
+
+ /**
+ * Sets this element's expanded state.
+ * @param boolean aFlag
+ */
+ set expanded(aFlag) {
+ this[aFlag ? "expand" : "collapse"]();
+ },
+
+ /**
+ * Gets the element associated with this item.
+ * @return nsIDOMNode
+ */
+ get target() {
+ return this._target;
+ },
+
+ /**
+ * Customization function for creating this item's UI.
+ *
+ * @param nsIDOMNode aElementNode
+ * The element associated with the displayed item.
+ * @param object aCallbacks
+ * An object containing all the necessary callback functions:
+ * - onHeaderClick
+ * - onMatchClick
+ */
+ createView: function (aElementNode, aCallbacks) {
+ this._target = aElementNode;
+
+ let arrow = this._arrow = document.createElement("box");
+ arrow.className = "arrow";
+
+ let locationNode = document.createElement("label");
+ locationNode.className = "plain dbg-results-header-location";
+ locationNode.setAttribute("value", this.label);
+
+ let matchCountNode = document.createElement("label");
+ matchCountNode.className = "plain dbg-results-header-match-count";
+ matchCountNode.setAttribute("value", "(" + this.matchCount + ")");
+
+ let resultsHeader = this._resultsHeader = document.createElement("hbox");
+ resultsHeader.className = "dbg-results-header";
+ resultsHeader.setAttribute("align", "center");
+ resultsHeader.appendChild(arrow);
+ resultsHeader.appendChild(locationNode);
+ resultsHeader.appendChild(matchCountNode);
+ resultsHeader.addEventListener("click", aCallbacks.onHeaderClick, false);
+
+ let resultsContainer = this._resultsContainer = document.createElement("vbox");
+ resultsContainer.className = "dbg-results-container";
+ resultsContainer.setAttribute("hidden", "true");
+
+ // Create lines search results entries and add them to this container.
+ // Afterwards, if the number of matches is reasonable, expand this
+ // container automatically.
+ for (let lineResults of this._store) {
+ lineResults.createView(resultsContainer, aCallbacks);
+ }
+ if (this.matchCount < GLOBAL_SEARCH_EXPAND_MAX_RESULTS) {
+ this.expand();
+ }
+
+ let resultsBox = document.createElement("vbox");
+ resultsBox.setAttribute("flex", "1");
+ resultsBox.appendChild(resultsHeader);
+ resultsBox.appendChild(resultsContainer);
+
+ aElementNode.id = "source-results-" + this.actor;
+ aElementNode.className = "dbg-source-results";
+ aElementNode.appendChild(resultsBox);
+
+ SourceResults._itemsByElement.set(aElementNode, { instance: this });
+ },
+
+ actor: "",
+ _globalResults: null,
+ _store: null,
+ _target: null,
+ _arrow: null,
+ _resultsHeader: null,
+ _resultsContainer: null
+};
+
+/**
+ * An object containing all the matches for a specific line.
+ * Iterable via "for (let chunk of lineResults) { }".
+ *
+ * @param number aLine
+ * The target line in the source.
+ * @param SourceResults aSourceResults
+ * An object containing all the matched lines for a specific source.
+ */
+function LineResults(aLine, aSourceResults) {
+ this.line = aLine;
+ this._sourceResults = aSourceResults;
+ this._store = [];
+ this._matchCount = 0;
+}
+
+LineResults.prototype = {
+ /**
+ * Adds string details to this store.
+ *
+ * @param string aString
+ * The text contents chunk in the line.
+ * @param object aRange
+ * An object containing the { start, length } of the chunk.
+ * @param boolean aMatchFlag
+ * True if the chunk is a matched string, false if just text content.
+ */
+ add: function (aString, aRange, aMatchFlag) {
+ this._store.push({ string: aString, range: aRange, match: !!aMatchFlag });
+ this._matchCount += aMatchFlag ? 1 : 0;
+ },
+
+ /**
+ * Gets the number of word results in this store.
+ */
+ get matchCount() {
+ return this._matchCount;
+ },
+
+ /**
+ * Gets the element associated with this item.
+ * @return nsIDOMNode
+ */
+ get target() {
+ return this._target;
+ },
+
+ /**
+ * Customization function for creating this item's UI.
+ *
+ * @param nsIDOMNode aElementNode
+ * The element associated with the displayed item.
+ * @param object aCallbacks
+ * An object containing all the necessary callback functions:
+ * - onMatchClick
+ * - onLineClick
+ */
+ createView: function (aElementNode, aCallbacks) {
+ this._target = aElementNode;
+
+ let lineNumberNode = document.createElement("label");
+ lineNumberNode.className = "plain dbg-results-line-number";
+ lineNumberNode.classList.add("devtools-monospace");
+ lineNumberNode.setAttribute("value", this.line + 1);
+
+ let lineContentsNode = document.createElement("hbox");
+ lineContentsNode.className = "dbg-results-line-contents";
+ lineContentsNode.classList.add("devtools-monospace");
+ lineContentsNode.setAttribute("flex", "1");
+
+ let lineString = "";
+ let lineLength = 0;
+ let firstMatch = null;
+
+ for (let lineChunk of this._store) {
+ let { string, range, match } = lineChunk;
+ lineString = string.substr(0, GLOBAL_SEARCH_LINE_MAX_LENGTH - lineLength);
+ lineLength += string.length;
+
+ let lineChunkNode = document.createElement("label");
+ lineChunkNode.className = "plain dbg-results-line-contents-string";
+ lineChunkNode.setAttribute("value", lineString);
+ lineChunkNode.setAttribute("match", match);
+ lineContentsNode.appendChild(lineChunkNode);
+
+ if (match) {
+ this._entangleMatch(lineChunkNode, lineChunk);
+ lineChunkNode.addEventListener("click", aCallbacks.onMatchClick, false);
+ firstMatch = firstMatch || lineChunkNode;
+ }
+ if (lineLength >= GLOBAL_SEARCH_LINE_MAX_LENGTH) {
+ lineContentsNode.appendChild(this._ellipsis.cloneNode(true));
+ break;
+ }
+ }
+
+ this._entangleLine(lineContentsNode, firstMatch);
+ lineContentsNode.addEventListener("click", aCallbacks.onLineClick, false);
+
+ let searchResult = document.createElement("hbox");
+ searchResult.className = "dbg-search-result";
+ searchResult.appendChild(lineNumberNode);
+ searchResult.appendChild(lineContentsNode);
+
+ aElementNode.appendChild(searchResult);
+ },
+
+ /**
+ * Handles a match while creating the view.
+ * @param nsIDOMNode aNode
+ * @param object aMatchChunk
+ */
+ _entangleMatch: function (aNode, aMatchChunk) {
+ LineResults._itemsByElement.set(aNode, {
+ instance: this,
+ lineData: aMatchChunk
+ });
+ },
+
+ /**
+ * Handles a line while creating the view.
+ * @param nsIDOMNode aNode
+ * @param nsIDOMNode aFirstMatch
+ */
+ _entangleLine: function (aNode, aFirstMatch) {
+ LineResults._itemsByElement.set(aNode, {
+ instance: this,
+ firstMatch: aFirstMatch,
+ ignored: true
+ });
+ },
+
+ /**
+ * An nsIDOMNode label with an ellipsis value.
+ */
+ _ellipsis: (function () {
+ let label = document.createElement("label");
+ label.className = "plain dbg-results-line-contents-string";
+ label.setAttribute("value", ELLIPSIS);
+ return label;
+ })(),
+
+ line: 0,
+ _sourceResults: null,
+ _store: null,
+ _target: null
+};
+
+/**
+ * A generator-iterator over the global, source or line results.
+ */
+GlobalResults.prototype[Symbol.iterator] =
+SourceResults.prototype[Symbol.iterator] =
+LineResults.prototype[Symbol.iterator] = function* () {
+ yield* this._store;
+};
+
+/**
+ * Gets the item associated with the specified element.
+ *
+ * @param nsIDOMNode aElement
+ * The element used to identify the item.
+ * @return object
+ * The matched item, or null if nothing is found.
+ */
+SourceResults.getItemForElement =
+LineResults.getItemForElement = function (aElement) {
+ return WidgetMethods.getItemForElement.call(this, aElement, { noSiblings: true });
+};
+
+/**
+ * Gets the element associated with a particular item at a specified index.
+ *
+ * @param number aIndex
+ * The index used to identify the item.
+ * @return nsIDOMNode
+ * The matched element, or null if nothing is found.
+ */
+SourceResults.getElementAtIndex =
+LineResults.getElementAtIndex = function (aIndex) {
+ for (let [element, item] of this._itemsByElement) {
+ if (!item.ignored && !aIndex--) {
+ return element;
+ }
+ }
+ return null;
+};
+
+/**
+ * Gets the index of an item associated with the specified element.
+ *
+ * @param nsIDOMNode aElement
+ * The element to get the index for.
+ * @return number
+ * The index of the matched element, or -1 if nothing is found.
+ */
+SourceResults.indexOfElement =
+LineResults.indexOfElement = function (aElement) {
+ let count = 0;
+ for (let [element, item] of this._itemsByElement) {
+ if (element == aElement) {
+ return count;
+ }
+ if (!item.ignored) {
+ count++;
+ }
+ }
+ return -1;
+};
+
+/**
+ * Gets the number of cached items associated with a specified element.
+ *
+ * @return number
+ * The number of key/value pairs in the corresponding map.
+ */
+SourceResults.size =
+LineResults.size = function () {
+ let count = 0;
+ for (let [, item] of this._itemsByElement) {
+ if (!item.ignored) {
+ count++;
+ }
+ }
+ return count;
+};
diff --git a/devtools/client/debugger/views/options-view.js b/devtools/client/debugger/views/options-view.js
new file mode 100644
index 000000000..2fb5b0600
--- /dev/null
+++ b/devtools/client/debugger/views/options-view.js
@@ -0,0 +1,215 @@
+/* -*- 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/. */
+/* import-globals-from ../debugger-controller.js */
+/* import-globals-from ../debugger-view.js */
+/* import-globals-from ../utils.js */
+/* globals document, window */
+"use strict";
+
+// A time interval sufficient for the options popup panel to finish hiding
+// itself.
+const POPUP_HIDDEN_DELAY = 100; // ms
+
+/**
+ * Functions handling the options UI.
+ */
+function OptionsView(DebuggerController, DebuggerView) {
+ dumpn("OptionsView was instantiated");
+
+ this.DebuggerController = DebuggerController;
+ this.DebuggerView = DebuggerView;
+
+ this._toggleAutoPrettyPrint = this._toggleAutoPrettyPrint.bind(this);
+ this._togglePauseOnExceptions = this._togglePauseOnExceptions.bind(this);
+ this._toggleIgnoreCaughtExceptions = this._toggleIgnoreCaughtExceptions.bind(this);
+ this._toggleShowPanesOnStartup = this._toggleShowPanesOnStartup.bind(this);
+ this._toggleShowVariablesOnlyEnum = this._toggleShowVariablesOnlyEnum.bind(this);
+ this._toggleShowVariablesFilterBox = this._toggleShowVariablesFilterBox.bind(this);
+ this._toggleShowOriginalSource = this._toggleShowOriginalSource.bind(this);
+ this._toggleAutoBlackBox = this._toggleAutoBlackBox.bind(this);
+}
+
+OptionsView.prototype = {
+ /**
+ * Initialization function, called when the debugger is started.
+ */
+ initialize: function () {
+ dumpn("Initializing the OptionsView");
+
+ this._button = document.getElementById("debugger-options");
+ this._autoPrettyPrint = document.getElementById("auto-pretty-print");
+ this._pauseOnExceptionsItem = document.getElementById("pause-on-exceptions");
+ this._ignoreCaughtExceptionsItem = document.getElementById("ignore-caught-exceptions");
+ this._showPanesOnStartupItem = document.getElementById("show-panes-on-startup");
+ this._showVariablesOnlyEnumItem = document.getElementById("show-vars-only-enum");
+ this._showVariablesFilterBoxItem = document.getElementById("show-vars-filter-box");
+ this._showOriginalSourceItem = document.getElementById("show-original-source");
+ this._autoBlackBoxItem = document.getElementById("auto-black-box");
+
+ this._autoPrettyPrint.setAttribute("checked", Prefs.autoPrettyPrint);
+ this._pauseOnExceptionsItem.setAttribute("checked", Prefs.pauseOnExceptions);
+ this._ignoreCaughtExceptionsItem.setAttribute("checked", Prefs.ignoreCaughtExceptions);
+ this._showPanesOnStartupItem.setAttribute("checked", Prefs.panesVisibleOnStartup);
+ this._showVariablesOnlyEnumItem.setAttribute("checked", Prefs.variablesOnlyEnumVisible);
+ this._showVariablesFilterBoxItem.setAttribute("checked", Prefs.variablesSearchboxVisible);
+ this._showOriginalSourceItem.setAttribute("checked", Prefs.sourceMapsEnabled);
+ this._autoBlackBoxItem.setAttribute("checked", Prefs.autoBlackBox);
+
+ this._addCommands();
+ },
+
+ /**
+ * Destruction function, called when the debugger is closed.
+ */
+ destroy: function () {
+ dumpn("Destroying the OptionsView");
+ // Nothing to do here yet.
+ },
+
+ /**
+ * Add commands that XUL can fire.
+ */
+ _addCommands: function () {
+ XULUtils.addCommands(document.getElementById("debuggerCommands"), {
+ toggleAutoPrettyPrint: () => this._toggleAutoPrettyPrint(),
+ togglePauseOnExceptions: () => this._togglePauseOnExceptions(),
+ toggleIgnoreCaughtExceptions: () => this._toggleIgnoreCaughtExceptions(),
+ toggleShowPanesOnStartup: () => this._toggleShowPanesOnStartup(),
+ toggleShowOnlyEnum: () => this._toggleShowVariablesOnlyEnum(),
+ toggleShowVariablesFilterBox: () => this._toggleShowVariablesFilterBox(),
+ toggleShowOriginalSource: () => this._toggleShowOriginalSource(),
+ toggleAutoBlackBox: () => this._toggleAutoBlackBox()
+ });
+ },
+
+ /**
+ * Listener handling the 'gear menu' popup showing event.
+ */
+ _onPopupShowing: function () {
+ this._button.setAttribute("open", "true");
+ window.emit(EVENTS.OPTIONS_POPUP_SHOWING);
+ },
+
+ /**
+ * Listener handling the 'gear menu' popup hiding event.
+ */
+ _onPopupHiding: function () {
+ this._button.removeAttribute("open");
+ },
+
+ /**
+ * Listener handling the 'gear menu' popup hidden event.
+ */
+ _onPopupHidden: function () {
+ window.emit(EVENTS.OPTIONS_POPUP_HIDDEN);
+ },
+
+ /**
+ * Listener handling the 'auto pretty print' menuitem command.
+ */
+ _toggleAutoPrettyPrint: function () {
+ Prefs.autoPrettyPrint =
+ this._autoPrettyPrint.getAttribute("checked") == "true";
+ },
+
+ /**
+ * Listener handling the 'pause on exceptions' menuitem command.
+ */
+ _togglePauseOnExceptions: function () {
+ Prefs.pauseOnExceptions =
+ this._pauseOnExceptionsItem.getAttribute("checked") == "true";
+
+ this.DebuggerController.activeThread.pauseOnExceptions(
+ Prefs.pauseOnExceptions,
+ Prefs.ignoreCaughtExceptions);
+ },
+
+ _toggleIgnoreCaughtExceptions: function () {
+ Prefs.ignoreCaughtExceptions =
+ this._ignoreCaughtExceptionsItem.getAttribute("checked") == "true";
+
+ this.DebuggerController.activeThread.pauseOnExceptions(
+ Prefs.pauseOnExceptions,
+ Prefs.ignoreCaughtExceptions);
+ },
+
+ /**
+ * Listener handling the 'show panes on startup' menuitem command.
+ */
+ _toggleShowPanesOnStartup: function () {
+ Prefs.panesVisibleOnStartup =
+ this._showPanesOnStartupItem.getAttribute("checked") == "true";
+ },
+
+ /**
+ * Listener handling the 'show non-enumerables' menuitem command.
+ */
+ _toggleShowVariablesOnlyEnum: function () {
+ let pref = Prefs.variablesOnlyEnumVisible =
+ this._showVariablesOnlyEnumItem.getAttribute("checked") == "true";
+
+ this.DebuggerView.Variables.onlyEnumVisible = pref;
+ },
+
+ /**
+ * Listener handling the 'show variables searchbox' menuitem command.
+ */
+ _toggleShowVariablesFilterBox: function () {
+ let pref = Prefs.variablesSearchboxVisible =
+ this._showVariablesFilterBoxItem.getAttribute("checked") == "true";
+
+ this.DebuggerView.Variables.searchEnabled = pref;
+ },
+
+ /**
+ * Listener handling the 'show original source' menuitem command.
+ */
+ _toggleShowOriginalSource: function () {
+ let pref = Prefs.sourceMapsEnabled =
+ this._showOriginalSourceItem.getAttribute("checked") == "true";
+
+ // Don't block the UI while reconfiguring the server.
+ window.once(EVENTS.OPTIONS_POPUP_HIDDEN, () => {
+ // The popup panel needs more time to hide after triggering onpopuphidden.
+ window.setTimeout(() => {
+ this.DebuggerController.reconfigureThread({
+ useSourceMaps: pref,
+ autoBlackBox: Prefs.autoBlackBox
+ });
+ }, POPUP_HIDDEN_DELAY);
+ });
+ },
+
+ /**
+ * Listener handling the 'automatically black box minified sources' menuitem
+ * command.
+ */
+ _toggleAutoBlackBox: function () {
+ let pref = Prefs.autoBlackBox =
+ this._autoBlackBoxItem.getAttribute("checked") == "true";
+
+ // Don't block the UI while reconfiguring the server.
+ window.once(EVENTS.OPTIONS_POPUP_HIDDEN, () => {
+ // The popup panel needs more time to hide after triggering onpopuphidden.
+ window.setTimeout(() => {
+ this.DebuggerController.reconfigureThread({
+ useSourceMaps: Prefs.sourceMapsEnabled,
+ autoBlackBox: pref
+ });
+ }, POPUP_HIDDEN_DELAY);
+ });
+ },
+
+ _button: null,
+ _pauseOnExceptionsItem: null,
+ _showPanesOnStartupItem: null,
+ _showVariablesOnlyEnumItem: null,
+ _showVariablesFilterBoxItem: null,
+ _showOriginalSourceItem: null,
+ _autoBlackBoxItem: null
+};
+
+DebuggerView.Options = new OptionsView(DebuggerController, DebuggerView);
diff --git a/devtools/client/debugger/views/stack-frames-classic-view.js b/devtools/client/debugger/views/stack-frames-classic-view.js
new file mode 100644
index 000000000..df1b93088
--- /dev/null
+++ b/devtools/client/debugger/views/stack-frames-classic-view.js
@@ -0,0 +1,141 @@
+/* -*- 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/. */
+/* import-globals-from ../debugger-controller.js */
+/* import-globals-from ../debugger-view.js */
+/* import-globals-from ../utils.js */
+/* globals document */
+"use strict";
+
+/*
+ * Functions handling the stackframes classic list UI.
+ * Controlled by the DebuggerView.StackFrames isntance.
+ */
+function StackFramesClassicListView(DebuggerController, DebuggerView) {
+ dumpn("StackFramesClassicListView was instantiated");
+
+ this.DebuggerView = DebuggerView;
+ this._onSelect = this._onSelect.bind(this);
+}
+
+StackFramesClassicListView.prototype = Heritage.extend(WidgetMethods, {
+ /**
+ * Initialization function, called when the debugger is started.
+ */
+ initialize: function () {
+ dumpn("Initializing the StackFramesClassicListView");
+
+ this.widget = new SideMenuWidget(document.getElementById("callstack-list"));
+ this.widget.addEventListener("select", this._onSelect, false);
+
+ this.emptyText = L10N.getStr("noStackFramesText");
+ this.autoFocusOnFirstItem = false;
+ this.autoFocusOnSelection = false;
+
+ // This view's contents are also mirrored in a different container.
+ this._mirror = this.DebuggerView.StackFrames;
+ },
+
+ /**
+ * Destruction function, called when the debugger is closed.
+ */
+ destroy: function () {
+ dumpn("Destroying the StackFramesClassicListView");
+
+ this.widget.removeEventListener("select", this._onSelect, false);
+ },
+
+ /**
+ * Adds a frame in this stackframes container.
+ *
+ * @param string aTitle
+ * The frame title (function name).
+ * @param string aUrl
+ * The frame source url.
+ * @param string aLine
+ * The frame line number.
+ * @param number aDepth
+ * The frame depth in the stack.
+ */
+ addFrame: function (aTitle, aUrl, aLine, aDepth) {
+ // Create the element node for the stack frame item.
+ let frameView = this._createFrameView.apply(this, arguments);
+
+ // Append a stack frame item to this container.
+ this.push([frameView], {
+ attachment: {
+ depth: aDepth
+ }
+ });
+ },
+
+ /**
+ * Customization function for creating an item's UI.
+ *
+ * @param string aTitle
+ * The frame title to be displayed in the list.
+ * @param string aUrl
+ * The frame source url.
+ * @param string aLine
+ * The frame line number.
+ * @param number aDepth
+ * The frame depth in the stack.
+ * @return nsIDOMNode
+ * The stack frame view.
+ */
+ _createFrameView: function (aTitle, aUrl, aLine, aDepth) {
+ let container = document.createElement("hbox");
+ container.id = "classic-stackframe-" + aDepth;
+ container.className = "dbg-classic-stackframe";
+ container.setAttribute("flex", "1");
+
+ let frameTitleNode = document.createElement("label");
+ frameTitleNode.className = "plain dbg-classic-stackframe-title";
+ frameTitleNode.setAttribute("value", aTitle);
+ frameTitleNode.setAttribute("crop", "center");
+
+ let frameDetailsNode = document.createElement("hbox");
+ frameDetailsNode.className = "plain dbg-classic-stackframe-details";
+
+ let frameUrlNode = document.createElement("label");
+ frameUrlNode.className = "plain dbg-classic-stackframe-details-url";
+ frameUrlNode.setAttribute("value", SourceUtils.getSourceLabel(aUrl));
+ frameUrlNode.setAttribute("crop", "center");
+ frameDetailsNode.appendChild(frameUrlNode);
+
+ let frameDetailsSeparator = document.createElement("label");
+ frameDetailsSeparator.className = "plain dbg-classic-stackframe-details-sep";
+ frameDetailsSeparator.setAttribute("value", SEARCH_LINE_FLAG);
+ frameDetailsNode.appendChild(frameDetailsSeparator);
+
+ let frameLineNode = document.createElement("label");
+ frameLineNode.className = "plain dbg-classic-stackframe-details-line";
+ frameLineNode.setAttribute("value", aLine);
+ frameDetailsNode.appendChild(frameLineNode);
+
+ container.appendChild(frameTitleNode);
+ container.appendChild(frameDetailsNode);
+
+ return container;
+ },
+
+ /**
+ * The select listener for the stackframes container.
+ */
+ _onSelect: function (e) {
+ let stackframeItem = this.selectedItem;
+ if (stackframeItem) {
+ // The container is not empty and an actual item was selected.
+ // Mirror the selected item in the breadcrumbs list.
+ let depth = stackframeItem.attachment.depth;
+ this._mirror.selectedItem = e => e.attachment.depth == depth;
+ }
+ },
+
+ _mirror: null
+});
+
+DebuggerView.StackFramesClassicList = new StackFramesClassicListView(DebuggerController,
+ DebuggerView);
diff --git a/devtools/client/debugger/views/stack-frames-view.js b/devtools/client/debugger/views/stack-frames-view.js
new file mode 100644
index 000000000..244f97b3d
--- /dev/null
+++ b/devtools/client/debugger/views/stack-frames-view.js
@@ -0,0 +1,283 @@
+/* -*- 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/. */
+/* import-globals-from ../debugger-controller.js */
+/* import-globals-from ../debugger-view.js */
+/* import-globals-from ../utils.js */
+/* globals document, window */
+"use strict";
+
+/**
+ * Functions handling the stackframes UI.
+ */
+function StackFramesView(DebuggerController, DebuggerView) {
+ dumpn("StackFramesView was instantiated");
+
+ this.StackFrames = DebuggerController.StackFrames;
+ this.DebuggerView = DebuggerView;
+
+ this._onStackframeRemoved = this._onStackframeRemoved.bind(this);
+ this._onSelect = this._onSelect.bind(this);
+ this._onScroll = this._onScroll.bind(this);
+ this._afterScroll = this._afterScroll.bind(this);
+ this._getStackAsString = this._getStackAsString.bind(this);
+}
+
+StackFramesView.prototype = Heritage.extend(WidgetMethods, {
+ /**
+ * Initialization function, called when the debugger is started.
+ */
+ initialize: function () {
+ dumpn("Initializing the StackFramesView");
+
+ this._popupset = document.getElementById("debuggerPopupset");
+
+ this.widget = new BreadcrumbsWidget(document.getElementById("stackframes"));
+ this.widget.addEventListener("select", this._onSelect, false);
+ this.widget.addEventListener("scroll", this._onScroll, true);
+ this.widget.setAttribute("context", "stackFramesContextMenu");
+ window.addEventListener("resize", this._onScroll, true);
+
+ this.autoFocusOnFirstItem = false;
+ this.autoFocusOnSelection = false;
+
+ // This view's contents are also mirrored in a different container.
+ this._mirror = this.DebuggerView.StackFramesClassicList;
+ },
+
+ /**
+ * Destruction function, called when the debugger is closed.
+ */
+ destroy: function () {
+ dumpn("Destroying the StackFramesView");
+
+ this.widget.removeEventListener("select", this._onSelect, false);
+ this.widget.removeEventListener("scroll", this._onScroll, true);
+ window.removeEventListener("resize", this._onScroll, true);
+ },
+
+ /**
+ * Adds a frame in this stackframes container.
+ *
+ * @param string aTitle
+ * The frame title (function name).
+ * @param string aUrl
+ * The frame source url.
+ * @param string aLine
+ * The frame line number.
+ * @param number aDepth
+ * The frame depth in the stack.
+ * @param boolean aIsBlackBoxed
+ * Whether or not the frame is black boxed.
+ */
+ addFrame: function (aFrame, aLine, aColumn, aDepth, aIsBlackBoxed) {
+ let { source } = aFrame;
+
+ // The source may not exist in the source listing yet because it's
+ // an unnamed eval source, which we hide, so we need to add it
+ if (!DebuggerView.Sources.getItemByValue(source.actor)) {
+ DebuggerView.Sources.addSource(source, { force: true });
+ }
+
+ let location = DebuggerView.Sources.getDisplayURL(source);
+ let title = StackFrameUtils.getFrameTitle(aFrame);
+
+ // Blackboxed stack frames are collapsed into a single entry in
+ // the view. By convention, only the first frame is displayed.
+ if (aIsBlackBoxed) {
+ if (this._prevBlackBoxedUrl == location) {
+ return;
+ }
+ this._prevBlackBoxedUrl = location;
+ } else {
+ this._prevBlackBoxedUrl = null;
+ }
+
+ // Create the element node for the stack frame item.
+ let frameView = this._createFrameView(
+ title, location, aLine, aDepth, aIsBlackBoxed
+ );
+
+ // Append a stack frame item to this container.
+ this.push([frameView], {
+ index: 0, /* specifies on which position should the item be appended */
+ attachment: {
+ title: title,
+ url: location,
+ line: aLine,
+ depth: aDepth,
+ column: aColumn
+ },
+ // Make sure that when the stack frame item is removed, the corresponding
+ // mirrored item in the classic list is also removed.
+ finalize: this._onStackframeRemoved
+ });
+
+ // Mirror this newly inserted item inside the "Call Stack" tab.
+ this._mirror.addFrame(title, location, aLine, aDepth);
+ },
+
+ _getStackAsString: function () {
+ return [...this].map(frameItem => {
+ const { attachment: { title, url, line, column }} = frameItem;
+ return title + "@" + url + ":" + line + ":" + column;
+ }).join("\n");
+ },
+
+ addCopyContextMenu: function () {
+ let menupopup = document.createElement("menupopup");
+ let menuitem = document.createElement("menuitem");
+
+ menupopup.id = "stackFramesContextMenu";
+ menuitem.id = "copyStackMenuItem";
+
+ menuitem.setAttribute("label", "Copy");
+ menuitem.addEventListener("command", () => {
+ let stack = this._getStackAsString();
+ clipboardHelper.copyString(stack);
+ }, false);
+ menupopup.appendChild(menuitem);
+ this._popupset.appendChild(menupopup);
+ },
+
+ /**
+ * Selects the frame at the specified depth in this container.
+ * @param number aDepth
+ */
+ set selectedDepth(aDepth) {
+ this.selectedItem = aItem => aItem.attachment.depth == aDepth;
+ },
+
+ /**
+ * Gets the currently selected stack frame's depth in this container.
+ * This will essentially be the opposite of |selectedIndex|, which deals
+ * with the position in the view, where the last item added is actually
+ * the bottommost, not topmost.
+ * @return number
+ */
+ get selectedDepth() {
+ return this.selectedItem.attachment.depth;
+ },
+
+ /**
+ * Specifies if the active thread has more frames that need to be loaded.
+ */
+ dirty: false,
+
+ /**
+ * Customization function for creating an item's UI.
+ *
+ * @param string aTitle
+ * The frame title to be displayed in the list.
+ * @param string aUrl
+ * The frame source url.
+ * @param string aLine
+ * The frame line number.
+ * @param number aDepth
+ * The frame depth in the stack.
+ * @param boolean aIsBlackBoxed
+ * Whether or not the frame is black boxed.
+ * @return nsIDOMNode
+ * The stack frame view.
+ */
+ _createFrameView: function (aTitle, aUrl, aLine, aDepth, aIsBlackBoxed) {
+ let container = document.createElement("hbox");
+ container.id = "stackframe-" + aDepth;
+ container.className = "dbg-stackframe";
+
+ let frameDetails = SourceUtils.trimUrlLength(
+ SourceUtils.getSourceLabel(aUrl),
+ STACK_FRAMES_SOURCE_URL_MAX_LENGTH,
+ STACK_FRAMES_SOURCE_URL_TRIM_SECTION);
+
+ if (aIsBlackBoxed) {
+ container.classList.add("dbg-stackframe-black-boxed");
+ } else {
+ let frameTitleNode = document.createElement("label");
+ frameTitleNode.className = "plain dbg-stackframe-title breadcrumbs-widget-item-tag";
+ frameTitleNode.setAttribute("value", aTitle);
+ container.appendChild(frameTitleNode);
+
+ frameDetails += SEARCH_LINE_FLAG + aLine;
+ }
+
+ let frameDetailsNode = document.createElement("label");
+ frameDetailsNode.className = "plain dbg-stackframe-details breadcrumbs-widget-item-id";
+ frameDetailsNode.setAttribute("value", frameDetails);
+ container.appendChild(frameDetailsNode);
+
+ return container;
+ },
+
+ /**
+ * Function called each time a stack frame item is removed.
+ *
+ * @param object aItem
+ * The corresponding item.
+ */
+ _onStackframeRemoved: function (aItem) {
+ dumpn("Finalizing stackframe item: " + aItem.stringify());
+
+ // Remove the mirrored item in the classic list.
+ let depth = aItem.attachment.depth;
+ this._mirror.remove(this._mirror.getItemForAttachment(e => e.depth == depth));
+
+ // Forget the previously blackboxed stack frame url.
+ this._prevBlackBoxedUrl = null;
+ },
+
+ /**
+ * The select listener for the stackframes container.
+ */
+ _onSelect: function (e) {
+ let stackframeItem = this.selectedItem;
+ if (stackframeItem) {
+ // The container is not empty and an actual item was selected.
+ let depth = stackframeItem.attachment.depth;
+
+ // Mirror the selected item in the classic list.
+ this.suppressSelectionEvents = true;
+ this._mirror.selectedItem = e => e.attachment.depth == depth;
+ this.suppressSelectionEvents = false;
+
+ DebuggerController.StackFrames.selectFrame(depth);
+ }
+ },
+
+ /**
+ * The scroll listener for the stackframes container.
+ */
+ _onScroll: function () {
+ // Update the stackframes container only if we have to.
+ if (!this.dirty) {
+ return;
+ }
+ // Allow requests to settle down first.
+ setNamedTimeout("stack-scroll", STACK_FRAMES_SCROLL_DELAY, this._afterScroll);
+ },
+
+ /**
+ * Requests the addition of more frames from the controller.
+ */
+ _afterScroll: function () {
+ let scrollPosition = this.widget.getAttribute("scrollPosition");
+ let scrollWidth = this.widget.getAttribute("scrollWidth");
+
+ // If the stackframes container scrolled almost to the end, with only
+ // 1/10 of a breadcrumb remaining, load more content.
+ if (scrollPosition - scrollWidth / 10 < 1) {
+ this.ensureIndexIsVisible(CALL_STACK_PAGE_SIZE - 1);
+ this.dirty = false;
+
+ // Loads more stack frames from the debugger server cache.
+ DebuggerController.StackFrames.addMoreFrames();
+ }
+ },
+
+ _mirror: null,
+ _prevBlackBoxedUrl: null
+});
+
+DebuggerView.StackFrames = new StackFramesView(DebuggerController, DebuggerView);
diff --git a/devtools/client/debugger/views/toolbar-view.js b/devtools/client/debugger/views/toolbar-view.js
new file mode 100644
index 000000000..d76275a71
--- /dev/null
+++ b/devtools/client/debugger/views/toolbar-view.js
@@ -0,0 +1,287 @@
+/* -*- 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/. */
+/* import-globals-from ../debugger-controller.js */
+/* import-globals-from ../debugger-view.js */
+/* import-globals-from ../utils.js */
+/* globals document */
+"use strict";
+
+/**
+ * Functions handling the toolbar view: close button, expand/collapse button,
+ * pause/resume and stepping buttons etc.
+ */
+function ToolbarView(DebuggerController, DebuggerView) {
+ dumpn("ToolbarView was instantiated");
+
+ this.StackFrames = DebuggerController.StackFrames;
+ this.ThreadState = DebuggerController.ThreadState;
+ this.DebuggerController = DebuggerController;
+ this.DebuggerView = DebuggerView;
+
+ this._onTogglePanesActivated = this._onTogglePanesActivated.bind(this);
+ this._onTogglePanesPressed = this._onTogglePanesPressed.bind(this);
+ this._onResumePressed = this._onResumePressed.bind(this);
+ this._onStepOverPressed = this._onStepOverPressed.bind(this);
+ this._onStepInPressed = this._onStepInPressed.bind(this);
+ this._onStepOutPressed = this._onStepOutPressed.bind(this);
+}
+
+ToolbarView.prototype = {
+ get activeThread() {
+ return this.DebuggerController.activeThread;
+ },
+
+ get resumptionWarnFunc() {
+ return this.DebuggerController._ensureResumptionOrder;
+ },
+
+ /**
+ * Initialization function, called when the debugger is started.
+ */
+ initialize: function () {
+ dumpn("Initializing the ToolbarView");
+
+ this._instrumentsPaneToggleButton = document.getElementById("instruments-pane-toggle");
+ this._resumeButton = document.getElementById("resume");
+ this._stepOverButton = document.getElementById("step-over");
+ this._stepInButton = document.getElementById("step-in");
+ this._stepOutButton = document.getElementById("step-out");
+ this._resumeOrderTooltip = new Tooltip(document);
+ this._resumeOrderTooltip.defaultPosition = TOOLBAR_ORDER_POPUP_POSITION;
+
+ let resumeKey = ShortcutUtils.prettifyShortcut(document.getElementById("resumeKey"));
+ let stepOverKey = ShortcutUtils.prettifyShortcut(document.getElementById("stepOverKey"));
+ let stepInKey = ShortcutUtils.prettifyShortcut(document.getElementById("stepInKey"));
+ let stepOutKey = ShortcutUtils.prettifyShortcut(document.getElementById("stepOutKey"));
+ this._resumeTooltip = L10N.getFormatStr("resumeButtonTooltip", resumeKey);
+ this._pauseTooltip = L10N.getFormatStr("pauseButtonTooltip", resumeKey);
+ this._pausePendingTooltip = L10N.getStr("pausePendingButtonTooltip");
+ this._stepOverTooltip = L10N.getFormatStr("stepOverTooltip", stepOverKey);
+ this._stepInTooltip = L10N.getFormatStr("stepInTooltip", stepInKey);
+ this._stepOutTooltip = L10N.getFormatStr("stepOutTooltip", stepOutKey);
+
+ this._instrumentsPaneToggleButton.addEventListener("mousedown",
+ this._onTogglePanesActivated, false);
+ this._instrumentsPaneToggleButton.addEventListener("keydown",
+ this._onTogglePanesPressed, false);
+ this._resumeButton.addEventListener("mousedown", this._onResumePressed, false);
+ this._stepOverButton.addEventListener("mousedown", this._onStepOverPressed, false);
+ this._stepInButton.addEventListener("mousedown", this._onStepInPressed, false);
+ this._stepOutButton.addEventListener("mousedown", this._onStepOutPressed, false);
+
+ this._stepOverButton.setAttribute("tooltiptext", this._stepOverTooltip);
+ this._stepInButton.setAttribute("tooltiptext", this._stepInTooltip);
+ this._stepOutButton.setAttribute("tooltiptext", this._stepOutTooltip);
+ this._toggleButtonsState({ enabled: false });
+
+ this._addCommands();
+ },
+
+ /**
+ * Destruction function, called when the debugger is closed.
+ */
+ destroy: function () {
+ dumpn("Destroying the ToolbarView");
+
+ this._instrumentsPaneToggleButton.removeEventListener("mousedown",
+ this._onTogglePanesActivated, false);
+ this._instrumentsPaneToggleButton.removeEventListener("keydown",
+ this._onTogglePanesPressed, false);
+ this._resumeButton.removeEventListener("mousedown", this._onResumePressed, false);
+ this._stepOverButton.removeEventListener("mousedown", this._onStepOverPressed, false);
+ this._stepInButton.removeEventListener("mousedown", this._onStepInPressed, false);
+ this._stepOutButton.removeEventListener("mousedown", this._onStepOutPressed, false);
+ },
+
+ /**
+ * Add commands that XUL can fire.
+ */
+ _addCommands: function () {
+ XULUtils.addCommands(document.getElementById("debuggerCommands"), {
+ resumeCommand: this.getCommandHandler("resumeCommand"),
+ stepOverCommand: this.getCommandHandler("stepOverCommand"),
+ stepInCommand: this.getCommandHandler("stepInCommand"),
+ stepOutCommand: this.getCommandHandler("stepOutCommand")
+ });
+ },
+
+ /**
+ * Retrieve the callback associated with the provided debugger command.
+ *
+ * @param {String} command
+ * The debugger command id.
+ * @return {Function} the corresponding callback.
+ */
+ getCommandHandler: function (command) {
+ switch (command) {
+ case "resumeCommand":
+ return () => this._onResumePressed();
+ case "stepOverCommand":
+ return () => this._onStepOverPressed();
+ case "stepInCommand":
+ return () => this._onStepInPressed();
+ case "stepOutCommand":
+ return () => this._onStepOutPressed();
+ default:
+ return () => {};
+ }
+ },
+
+ /**
+ * Display a warning when trying to resume a debuggee while another is paused.
+ * Debuggees must be unpaused in a Last-In-First-Out order.
+ *
+ * @param string aPausedUrl
+ * The URL of the last paused debuggee.
+ */
+ showResumeWarning: function (aPausedUrl) {
+ let label = L10N.getFormatStr("resumptionOrderPanelTitle", aPausedUrl);
+ let defaultStyle = "default-tooltip-simple-text-colors";
+ this._resumeOrderTooltip.setTextContent({ messages: [label] });
+ this._resumeOrderTooltip.show(this._resumeButton);
+ },
+
+ /**
+ * Sets the resume button state based on the debugger active thread.
+ *
+ * @param string aState
+ * Either "paused", "attached", or "breakOnNext".
+ * @param boolean hasLocation
+ * True if we are paused at a specific JS location
+ */
+ toggleResumeButtonState: function (aState, hasLocation) {
+ // Intermidiate state after pressing the pause button and waiting
+ // for the next script execution to happen.
+ if (aState == "breakOnNext") {
+ this._resumeButton.setAttribute("break-on-next", "true");
+ this._resumeButton.disabled = true;
+ this._resumeButton.setAttribute("tooltiptext", this._pausePendingTooltip);
+ return;
+ }
+
+ this._resumeButton.removeAttribute("break-on-next");
+ this._resumeButton.disabled = false;
+
+ // If we're paused, check and show a resume label on the button.
+ if (aState == "paused") {
+ this._resumeButton.setAttribute("checked", "true");
+ this._resumeButton.setAttribute("tooltiptext", this._resumeTooltip);
+
+ // Only enable the stepping buttons if we are paused at a
+ // specific location. After bug 789430, we'll always be paused
+ // at a location, but currently you can pause the entire engine
+ // at any point without knowing the location.
+ if (hasLocation) {
+ this._toggleButtonsState({ enabled: true });
+ }
+ }
+ // If we're attached, do the opposite.
+ else if (aState == "attached") {
+ this._resumeButton.removeAttribute("checked");
+ this._resumeButton.setAttribute("tooltiptext", this._pauseTooltip);
+ this._toggleButtonsState({ enabled: false });
+ }
+ },
+
+ _toggleButtonsState: function ({ enabled }) {
+ const buttons = [
+ this._stepOutButton,
+ this._stepInButton,
+ this._stepOverButton
+ ];
+ for (let button of buttons) {
+ button.disabled = !enabled;
+ }
+ },
+
+ /**
+ * Listener handling the toggle button space and return key event.
+ */
+ _onTogglePanesPressed: function (event) {
+ if (ViewHelpers.isSpaceOrReturn(event)) {
+ this._onTogglePanesActivated();
+ }
+ },
+
+ /**
+ * Listener handling the toggle button click event.
+ */
+ _onTogglePanesActivated: function() {
+ DebuggerView.toggleInstrumentsPane({
+ visible: DebuggerView.instrumentsPaneHidden,
+ animated: true,
+ delayed: true
+ });
+ },
+
+ /**
+ * Listener handling the pause/resume button click event.
+ */
+ _onResumePressed: function () {
+ if (this.StackFrames._currentFrameDescription != FRAME_TYPE.NORMAL ||
+ this._resumeButton.disabled) {
+ return;
+ }
+
+ if (this.activeThread.paused) {
+ this.StackFrames.currentFrameDepth = -1;
+ this.activeThread.resume(this.resumptionWarnFunc);
+ } else {
+ this.ThreadState.interruptedByResumeButton = true;
+ this.toggleResumeButtonState("breakOnNext");
+ this.activeThread.breakOnNext();
+ }
+ },
+
+ /**
+ * Listener handling the step over button click event.
+ */
+ _onStepOverPressed: function () {
+ if (this.activeThread.paused && !this._stepOverButton.disabled) {
+ this.StackFrames.currentFrameDepth = -1;
+ this.activeThread.stepOver(this.resumptionWarnFunc);
+ }
+ },
+
+ /**
+ * Listener handling the step in button click event.
+ */
+ _onStepInPressed: function () {
+ if (this.StackFrames._currentFrameDescription != FRAME_TYPE.NORMAL ||
+ this._stepInButton.disabled) {
+ return;
+ }
+
+ if (this.activeThread.paused) {
+ this.StackFrames.currentFrameDepth = -1;
+ this.activeThread.stepIn(this.resumptionWarnFunc);
+ }
+ },
+
+ /**
+ * Listener handling the step out button click event.
+ */
+ _onStepOutPressed: function () {
+ if (this.activeThread.paused && !this._stepOutButton.disabled) {
+ this.StackFrames.currentFrameDepth = -1;
+ this.activeThread.stepOut(this.resumptionWarnFunc);
+ }
+ },
+
+ _instrumentsPaneToggleButton: null,
+ _resumeButton: null,
+ _stepOverButton: null,
+ _stepInButton: null,
+ _stepOutButton: null,
+ _resumeOrderTooltip: null,
+ _resumeTooltip: "",
+ _pauseTooltip: "",
+ _stepOverTooltip: "",
+ _stepInTooltip: "",
+ _stepOutTooltip: ""
+};
+
+DebuggerView.Toolbar = new ToolbarView(DebuggerController, DebuggerView);
diff --git a/devtools/client/debugger/views/variable-bubble-view.js b/devtools/client/debugger/views/variable-bubble-view.js
new file mode 100644
index 000000000..3ac2f971e
--- /dev/null
+++ b/devtools/client/debugger/views/variable-bubble-view.js
@@ -0,0 +1,321 @@
+/* -*- 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/. */
+/* import-globals-from ../debugger-controller.js */
+/* import-globals-from ../debugger-view.js */
+/* import-globals-from ../utils.js */
+/* globals document, window */
+"use strict";
+
+const {setTooltipVariableContent} = require("devtools/client/shared/widgets/tooltip/VariableContentHelper");
+
+/**
+ * Functions handling the variables bubble UI.
+ */
+function VariableBubbleView(DebuggerController, DebuggerView) {
+ dumpn("VariableBubbleView was instantiated");
+
+ this.StackFrames = DebuggerController.StackFrames;
+ this.Parser = DebuggerController.Parser;
+ this.DebuggerView = DebuggerView;
+
+ this._onMouseMove = this._onMouseMove.bind(this);
+ this._onMouseOut = this._onMouseOut.bind(this);
+ this._onPopupHiding = this._onPopupHiding.bind(this);
+}
+
+VariableBubbleView.prototype = {
+ /**
+ * Delay before showing the variables bubble tooltip when hovering a valid
+ * target.
+ */
+ TOOLTIP_SHOW_DELAY: 750,
+
+ /**
+ * Tooltip position for the variables bubble tooltip.
+ */
+ TOOLTIP_POSITION: "topcenter bottomleft",
+
+ /**
+ * Initialization function, called when the debugger is started.
+ */
+ initialize: function () {
+ dumpn("Initializing the VariableBubbleView");
+
+ this._toolbox = DebuggerController._toolbox;
+ this._editorContainer = document.getElementById("editor");
+ this._editorContainer.addEventListener("mousemove", this._onMouseMove, false);
+ this._editorContainer.addEventListener("mouseout", this._onMouseOut, false);
+
+ this._tooltip = new Tooltip(document, {
+ closeOnEvents: [{
+ emitter: this._toolbox,
+ event: "select"
+ }, {
+ emitter: this._editorContainer,
+ event: "scroll",
+ useCapture: true
+ }, {
+ emitter: document,
+ event: "keydown"
+ }]
+ });
+ this._tooltip.defaultPosition = this.TOOLTIP_POSITION;
+ this._tooltip.panel.addEventListener("popuphiding", this._onPopupHiding);
+ },
+
+ /**
+ * Destruction function, called when the debugger is closed.
+ */
+ destroy: function () {
+ dumpn("Destroying the VariableBubbleView");
+
+ this._tooltip.panel.removeEventListener("popuphiding", this._onPopupHiding);
+ this._editorContainer.removeEventListener("mousemove", this._onMouseMove, false);
+ this._editorContainer.removeEventListener("mouseout", this._onMouseOut, false);
+ },
+
+ /**
+ * Specifies whether literals can be (redundantly) inspected in a popup.
+ * This behavior is deprecated, but still tested in a few places.
+ */
+ _ignoreLiterals: true,
+
+ /**
+ * Searches for an identifier underneath the specified position in the
+ * source editor, and if found, opens a VariablesView inspection popup.
+ *
+ * @param number x, y
+ * The left/top coordinates where to look for an identifier.
+ */
+ _findIdentifier: function (x, y) {
+ let editor = this.DebuggerView.editor;
+
+ // Calculate the editor's line and column at the current x and y coords.
+ let hoveredPos = editor.getPositionFromCoords({ left: x, top: y });
+ let hoveredOffset = editor.getOffset(hoveredPos);
+ let hoveredLine = hoveredPos.line;
+ let hoveredColumn = hoveredPos.ch;
+
+ // A source contains multiple scripts. Find the start index of the script
+ // containing the specified offset relative to its parent source.
+ let contents = editor.getText();
+ let location = this.DebuggerView.Sources.selectedValue;
+ let parsedSource = this.Parser.get(contents, location);
+ let scriptInfo = parsedSource.getScriptInfo(hoveredOffset);
+
+ // If the script length is negative, we're not hovering JS source code.
+ if (scriptInfo.length == -1) {
+ return;
+ }
+
+ // Using the script offset, determine the actual line and column inside the
+ // script, to use when finding identifiers.
+ let scriptStart = editor.getPosition(scriptInfo.start);
+ let scriptLineOffset = scriptStart.line;
+ let scriptColumnOffset = (hoveredLine == scriptStart.line ? scriptStart.ch : 0);
+
+ let scriptLine = hoveredLine - scriptLineOffset;
+ let scriptColumn = hoveredColumn - scriptColumnOffset;
+ let identifierInfo = parsedSource.getIdentifierAt({
+ line: scriptLine + 1,
+ column: scriptColumn,
+ scriptIndex: scriptInfo.index,
+ ignoreLiterals: this._ignoreLiterals
+ });
+
+ // If the info is null, we're not hovering any identifier.
+ if (!identifierInfo) {
+ return;
+ }
+
+ // Transform the line and column relative to the parsed script back
+ // to the context of the parent source.
+ let { start: identifierStart, end: identifierEnd } = identifierInfo.location;
+ let identifierCoords = {
+ line: identifierStart.line + scriptLineOffset,
+ column: identifierStart.column + scriptColumnOffset,
+ length: identifierEnd.column - identifierStart.column
+ };
+
+ // Evaluate the identifier in the current stack frame and show the
+ // results in a VariablesView inspection popup.
+ this.StackFrames.evaluate(identifierInfo.evalString)
+ .then(frameFinished => {
+ if ("return" in frameFinished) {
+ this.showContents({
+ coords: identifierCoords,
+ evalPrefix: identifierInfo.evalString,
+ objectActor: frameFinished.return
+ });
+ } else {
+ let msg = "Evaluation has thrown for: " + identifierInfo.evalString;
+ console.warn(msg);
+ dumpn(msg);
+ }
+ })
+ .then(null, err => {
+ let msg = "Couldn't evaluate: " + err.message;
+ console.error(msg);
+ dumpn(msg);
+ });
+ },
+
+ /**
+ * Shows an inspection popup for a specified object actor grip.
+ *
+ * @param string object
+ * An object containing the following properties:
+ * - coords: the inspected identifier coordinates in the editor,
+ * containing the { line, column, length } properties.
+ * - evalPrefix: a prefix for the variables view evaluation macros.
+ * - objectActor: the value grip for the object actor.
+ */
+ showContents: function ({ coords, evalPrefix, objectActor }) {
+ let editor = this.DebuggerView.editor;
+ let { line, column, length } = coords;
+
+ // Highlight the function found at the mouse position.
+ this._markedText = editor.markText(
+ { line: line - 1, ch: column },
+ { line: line - 1, ch: column + length });
+
+ // If the grip represents a primitive value, use a more lightweight
+ // machinery to display it.
+ if (VariablesView.isPrimitive({ value: objectActor })) {
+ let className = VariablesView.getClass(objectActor);
+ let textContent = VariablesView.getString(objectActor);
+ this._tooltip.setTextContent({
+ messages: [textContent],
+ messagesClass: className,
+ containerClass: "plain"
+ }, [{
+ label: L10N.getStr("addWatchExpressionButton"),
+ className: "dbg-expression-button",
+ command: () => {
+ this.DebuggerView.VariableBubble.hideContents();
+ this.DebuggerView.WatchExpressions.addExpression(evalPrefix, true);
+ }
+ }]);
+ } else {
+ setTooltipVariableContent(this._tooltip, objectActor, {
+ searchPlaceholder: L10N.getStr("emptyPropertiesFilterText"),
+ searchEnabled: Prefs.variablesSearchboxVisible,
+ eval: (variable, value) => {
+ let string = variable.evaluationMacro(variable, value);
+ this.StackFrames.evaluate(string);
+ this.DebuggerView.VariableBubble.hideContents();
+ }
+ }, {
+ getEnvironmentClient: aObject => gThreadClient.environment(aObject),
+ getObjectClient: aObject => gThreadClient.pauseGrip(aObject),
+ simpleValueEvalMacro: this._getSimpleValueEvalMacro(evalPrefix),
+ getterOrSetterEvalMacro: this._getGetterOrSetterEvalMacro(evalPrefix),
+ overrideValueEvalMacro: this._getOverrideValueEvalMacro(evalPrefix)
+ }, {
+ fetched: (aEvent, aType) => {
+ if (aType == "properties") {
+ window.emit(EVENTS.FETCHED_BUBBLE_PROPERTIES);
+ }
+ }
+ }, [{
+ label: L10N.getStr("addWatchExpressionButton"),
+ className: "dbg-expression-button",
+ command: () => {
+ this.DebuggerView.VariableBubble.hideContents();
+ this.DebuggerView.WatchExpressions.addExpression(evalPrefix, true);
+ }
+ }], this._toolbox);
+ }
+
+ this._tooltip.show(this._markedText.anchor);
+ },
+
+ /**
+ * Hides the inspection popup.
+ */
+ hideContents: function () {
+ clearNamedTimeout("editor-mouse-move");
+ this._tooltip.hide();
+ },
+
+ /**
+ * Checks whether the inspection popup is shown.
+ *
+ * @return boolean
+ * True if the panel is shown or showing, false otherwise.
+ */
+ contentsShown: function () {
+ return this._tooltip.isShown();
+ },
+
+ /**
+ * Functions for getting customized variables view evaluation macros.
+ *
+ * @param string aPrefix
+ * See the corresponding VariablesView.* functions.
+ */
+ _getSimpleValueEvalMacro: function (aPrefix) {
+ return (item, string) =>
+ VariablesView.simpleValueEvalMacro(item, string, aPrefix);
+ },
+ _getGetterOrSetterEvalMacro: function (aPrefix) {
+ return (item, string) =>
+ VariablesView.getterOrSetterEvalMacro(item, string, aPrefix);
+ },
+ _getOverrideValueEvalMacro: function (aPrefix) {
+ return (item, string) =>
+ VariablesView.overrideValueEvalMacro(item, string, aPrefix);
+ },
+
+ /**
+ * The mousemove listener for the source editor.
+ */
+ _onMouseMove: function (e) {
+ // Prevent the variable inspection popup from showing when the thread client
+ // is not paused, or while a popup is already visible, or when the user tries
+ // to select text in the editor.
+ let isResumed = gThreadClient && gThreadClient.state != "paused";
+ let isSelecting = this.DebuggerView.editor.somethingSelected() && e.buttons > 0;
+ let isPopupVisible = !this._tooltip.isHidden();
+ if (isResumed || isSelecting || isPopupVisible) {
+ clearNamedTimeout("editor-mouse-move");
+ return;
+ }
+ // Allow events to settle down first. If the mouse hovers over
+ // a certain point in the editor long enough, try showing a variable bubble.
+ setNamedTimeout("editor-mouse-move",
+ this.TOOLTIP_SHOW_DELAY, () => this._findIdentifier(e.clientX, e.clientY));
+ },
+
+ /**
+ * The mouseout listener for the source editor container node.
+ */
+ _onMouseOut: function () {
+ clearNamedTimeout("editor-mouse-move");
+ },
+
+ /**
+ * Listener handling the popup hiding event.
+ */
+ _onPopupHiding: function ({ target }) {
+ if (this._tooltip.panel != target) {
+ return;
+ }
+ if (this._markedText) {
+ this._markedText.clear();
+ this._markedText = null;
+ }
+ if (!this._tooltip.isEmpty()) {
+ this._tooltip.empty();
+ }
+ },
+
+ _editorContainer: null,
+ _markedText: null,
+ _tooltip: null
+};
+
+DebuggerView.VariableBubble = new VariableBubbleView(DebuggerController, DebuggerView);
diff --git a/devtools/client/debugger/views/watch-expressions-view.js b/devtools/client/debugger/views/watch-expressions-view.js
new file mode 100644
index 000000000..59d3ad5a0
--- /dev/null
+++ b/devtools/client/debugger/views/watch-expressions-view.js
@@ -0,0 +1,303 @@
+/* -*- 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/. */
+/* import-globals-from ../debugger-controller.js */
+/* import-globals-from ../debugger-view.js */
+/* import-globals-from ../utils.js */
+/* globals document */
+"use strict";
+
+const {KeyCodes} = require("devtools/client/shared/keycodes");
+
+/**
+ * Functions handling the watch expressions UI.
+ */
+function WatchExpressionsView(DebuggerController, DebuggerView) {
+ dumpn("WatchExpressionsView was instantiated");
+
+ this.StackFrames = DebuggerController.StackFrames;
+ this.DebuggerView = DebuggerView;
+
+ this.switchExpression = this.switchExpression.bind(this);
+ this.deleteExpression = this.deleteExpression.bind(this);
+ this._createItemView = this._createItemView.bind(this);
+ this._onClick = this._onClick.bind(this);
+ this._onClose = this._onClose.bind(this);
+ this._onBlur = this._onBlur.bind(this);
+ this._onKeyPress = this._onKeyPress.bind(this);
+}
+
+WatchExpressionsView.prototype = Heritage.extend(WidgetMethods, {
+ /**
+ * Initialization function, called when the debugger is started.
+ */
+ initialize: function () {
+ dumpn("Initializing the WatchExpressionsView");
+
+ this.widget = new SimpleListWidget(document.getElementById("expressions"));
+ this.widget.setAttribute("context", "debuggerWatchExpressionsContextMenu");
+ this.widget.addEventListener("click", this._onClick, false);
+
+ this.headerText = L10N.getStr("addWatchExpressionText");
+ this._addCommands();
+ },
+
+ /**
+ * Destruction function, called when the debugger is closed.
+ */
+ destroy: function () {
+ dumpn("Destroying the WatchExpressionsView");
+
+ this.widget.removeEventListener("click", this._onClick, false);
+ },
+
+ /**
+ * Add commands that XUL can fire.
+ */
+ _addCommands: function () {
+ XULUtils.addCommands(document.getElementById("debuggerCommands"), {
+ addWatchExpressionCommand: () => this._onCmdAddExpression(),
+ removeAllWatchExpressionsCommand: () => this._onCmdRemoveAllExpressions()
+ });
+ },
+
+ /**
+ * Adds a watch expression in this container.
+ *
+ * @param string aExpression [optional]
+ * An optional initial watch expression text.
+ * @param boolean aSkipUserInput [optional]
+ * Pass true to avoid waiting for additional user input
+ * on the watch expression.
+ */
+ addExpression: function (aExpression = "", aSkipUserInput = false) {
+ // Watch expressions are UI elements which benefit from visible panes.
+ this.DebuggerView.showInstrumentsPane();
+
+ // Create the element node for the watch expression item.
+ let itemView = this._createItemView(aExpression);
+
+ // Append a watch expression item to this container.
+ let expressionItem = this.push([itemView.container], {
+ index: 0, /* specifies on which position should the item be appended */
+ attachment: {
+ view: itemView,
+ initialExpression: aExpression,
+ currentExpression: "",
+ }
+ });
+
+ // Automatically focus the new watch expression input
+ // if additional user input is desired.
+ if (!aSkipUserInput) {
+ expressionItem.attachment.view.inputNode.select();
+ expressionItem.attachment.view.inputNode.focus();
+ this.DebuggerView.Variables.parentNode.scrollTop = 0;
+ }
+ // Otherwise, add and evaluate the new watch expression immediately.
+ else {
+ this.toggleContents(false);
+ this._onBlur({ target: expressionItem.attachment.view.inputNode });
+ }
+ },
+
+ /**
+ * Changes the watch expression corresponding to the specified variable item.
+ * This function is called whenever a watch expression's code is edited in
+ * the variables view container.
+ *
+ * @param Variable aVar
+ * The variable representing the watch expression evaluation.
+ * @param string aExpression
+ * The new watch expression text.
+ */
+ switchExpression: function (aVar, aExpression) {
+ let expressionItem =
+ [...this].filter(i => i.attachment.currentExpression == aVar.name)[0];
+
+ // Remove the watch expression if it's going to be empty or a duplicate.
+ if (!aExpression || this.getAllStrings().indexOf(aExpression) != -1) {
+ this.deleteExpression(aVar);
+ return;
+ }
+
+ // Save the watch expression code string.
+ expressionItem.attachment.currentExpression = aExpression;
+ expressionItem.attachment.view.inputNode.value = aExpression;
+
+ // Synchronize with the controller's watch expressions store.
+ this.StackFrames.syncWatchExpressions();
+ },
+
+ /**
+ * Removes the watch expression corresponding to the specified variable item.
+ * This function is called whenever a watch expression's value is edited in
+ * the variables view container.
+ *
+ * @param Variable aVar
+ * The variable representing the watch expression evaluation.
+ */
+ deleteExpression: function (aVar) {
+ let expressionItem =
+ [...this].filter(i => i.attachment.currentExpression == aVar.name)[0];
+
+ // Remove the watch expression.
+ this.remove(expressionItem);
+
+ // Synchronize with the controller's watch expressions store.
+ this.StackFrames.syncWatchExpressions();
+ },
+
+ /**
+ * Gets the watch expression code string for an item in this container.
+ *
+ * @param number aIndex
+ * The index used to identify the watch expression.
+ * @return string
+ * The watch expression code string.
+ */
+ getString: function (aIndex) {
+ return this.getItemAtIndex(aIndex).attachment.currentExpression;
+ },
+
+ /**
+ * Gets the watch expressions code strings for all items in this container.
+ *
+ * @return array
+ * The watch expressions code strings.
+ */
+ getAllStrings: function () {
+ return this.items.map(e => e.attachment.currentExpression);
+ },
+
+ /**
+ * Customization function for creating an item's UI.
+ *
+ * @param string aExpression
+ * The watch expression string.
+ */
+ _createItemView: function (aExpression) {
+ let container = document.createElement("hbox");
+ container.className = "list-widget-item dbg-expression";
+ container.setAttribute("align", "center");
+
+ let arrowNode = document.createElement("hbox");
+ arrowNode.className = "dbg-expression-arrow";
+
+ let inputNode = document.createElement("textbox");
+ inputNode.className = "plain dbg-expression-input devtools-monospace";
+ inputNode.setAttribute("value", aExpression);
+ inputNode.setAttribute("flex", "1");
+
+ let closeNode = document.createElement("toolbarbutton");
+ closeNode.className = "plain variables-view-delete";
+
+ closeNode.addEventListener("click", this._onClose, false);
+ inputNode.addEventListener("blur", this._onBlur, false);
+ inputNode.addEventListener("keypress", this._onKeyPress, false);
+
+ container.appendChild(arrowNode);
+ container.appendChild(inputNode);
+ container.appendChild(closeNode);
+
+ return {
+ container: container,
+ arrowNode: arrowNode,
+ inputNode: inputNode,
+ closeNode: closeNode
+ };
+ },
+
+ /**
+ * Called when the add watch expression key sequence was pressed.
+ */
+ _onCmdAddExpression: function (aText) {
+ // Only add a new expression if there's no pending input.
+ if (this.getAllStrings().indexOf("") == -1) {
+ this.addExpression(aText || this.DebuggerView.editor.getSelection());
+ }
+ },
+
+ /**
+ * Called when the remove all watch expressions key sequence was pressed.
+ */
+ _onCmdRemoveAllExpressions: function () {
+ // Empty the view of all the watch expressions and clear the cache.
+ this.empty();
+
+ // Synchronize with the controller's watch expressions store.
+ this.StackFrames.syncWatchExpressions();
+ },
+
+ /**
+ * The click listener for this container.
+ */
+ _onClick: function (e) {
+ if (e.button != 0) {
+ // Only allow left-click to trigger this event.
+ return;
+ }
+ let expressionItem = this.getItemForElement(e.target);
+ if (!expressionItem) {
+ // The container is empty or we didn't click on an actual item.
+ this.addExpression();
+ }
+ },
+
+ /**
+ * The click listener for a watch expression's close button.
+ */
+ _onClose: function (e) {
+ // Remove the watch expression.
+ this.remove(this.getItemForElement(e.target));
+
+ // Synchronize with the controller's watch expressions store.
+ this.StackFrames.syncWatchExpressions();
+
+ // Prevent clicking the expression element itself.
+ e.preventDefault();
+ e.stopPropagation();
+ },
+
+ /**
+ * The blur listener for a watch expression's textbox.
+ */
+ _onBlur: function ({ target: textbox }) {
+ let expressionItem = this.getItemForElement(textbox);
+ let oldExpression = expressionItem.attachment.currentExpression;
+ let newExpression = textbox.value.trim();
+
+ // Remove the watch expression if it's empty.
+ if (!newExpression) {
+ this.remove(expressionItem);
+ }
+ // Remove the watch expression if it's a duplicate.
+ else if (!oldExpression && this.getAllStrings().indexOf(newExpression) != -1) {
+ this.remove(expressionItem);
+ }
+ // Expression is eligible.
+ else {
+ expressionItem.attachment.currentExpression = newExpression;
+ }
+
+ // Synchronize with the controller's watch expressions store.
+ this.StackFrames.syncWatchExpressions();
+ },
+
+ /**
+ * The keypress listener for a watch expression's textbox.
+ */
+ _onKeyPress: function (e) {
+ switch (e.keyCode) {
+ case KeyCodes.DOM_VK_RETURN:
+ case KeyCodes.DOM_VK_ESCAPE:
+ e.stopPropagation();
+ this.DebuggerView.editor.focus();
+ }
+ }
+});
+
+DebuggerView.WatchExpressions = new WatchExpressionsView(DebuggerController,
+ DebuggerView);
diff --git a/devtools/client/debugger/views/workers-view.js b/devtools/client/debugger/views/workers-view.js
new file mode 100644
index 000000000..0dc8dc3a5
--- /dev/null
+++ b/devtools/client/debugger/views/workers-view.js
@@ -0,0 +1,55 @@
+/* -*- 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/. */
+/* import-globals-from ../debugger-controller.js */
+/* import-globals-from ../debugger-view.js */
+/* import-globals-from ../utils.js */
+/* globals document */
+"use strict";
+
+function WorkersView() {
+ this._onWorkerSelect = this._onWorkerSelect.bind(this);
+}
+
+WorkersView.prototype = Heritage.extend(WidgetMethods, {
+ initialize: function () {
+ if (!Prefs.workersEnabled) {
+ return;
+ }
+
+ document.getElementById("workers-pane").removeAttribute("hidden");
+ document.getElementById("workers-splitter").removeAttribute("hidden");
+
+ this.widget = new SideMenuWidget(document.getElementById("workers"), {
+ showArrows: true,
+ });
+ this.emptyText = L10N.getStr("noWorkersText");
+ this.widget.addEventListener("select", this._onWorkerSelect, false);
+ },
+
+ addWorker: function (workerForm) {
+ let element = document.createElement("label");
+ element.className = "plain dbg-worker-item";
+ element.setAttribute("value", workerForm.url);
+ element.setAttribute("flex", "1");
+
+ this.push([element, workerForm.actor], {
+ attachment: workerForm
+ });
+ },
+
+ removeWorker: function (workerForm) {
+ this.remove(this.getItemByValue(workerForm.actor));
+ },
+
+ _onWorkerSelect: function () {
+ if (this.selectedItem !== null) {
+ DebuggerController.Workers._onWorkerSelect(this.selectedItem.attachment);
+ this.selectedItem = null;
+ }
+ }
+});
+
+DebuggerView.Workers = new WorkersView();
diff --git a/devtools/client/definitions.js b/devtools/client/definitions.js
new file mode 100644
index 000000000..6c0796095
--- /dev/null
+++ b/devtools/client/definitions.js
@@ -0,0 +1,511 @@
+/* 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 Services = require("Services");
+const osString = Services.appinfo.OS;
+
+// Panels
+loader.lazyGetter(this, "OptionsPanel", () => require("devtools/client/framework/toolbox-options").OptionsPanel);
+loader.lazyGetter(this, "InspectorPanel", () => require("devtools/client/inspector/panel").InspectorPanel);
+loader.lazyGetter(this, "WebConsolePanel", () => require("devtools/client/webconsole/panel").WebConsolePanel);
+loader.lazyGetter(this, "DebuggerPanel", () => require("devtools/client/debugger/panel").DebuggerPanel);
+loader.lazyGetter(this, "StyleEditorPanel", () => require("devtools/client/styleeditor/styleeditor-panel").StyleEditorPanel);
+loader.lazyGetter(this, "ShaderEditorPanel", () => require("devtools/client/shadereditor/panel").ShaderEditorPanel);
+loader.lazyGetter(this, "CanvasDebuggerPanel", () => require("devtools/client/canvasdebugger/panel").CanvasDebuggerPanel);
+loader.lazyGetter(this, "WebAudioEditorPanel", () => require("devtools/client/webaudioeditor/panel").WebAudioEditorPanel);
+loader.lazyGetter(this, "MemoryPanel", () => require("devtools/client/memory/panel").MemoryPanel);
+loader.lazyGetter(this, "PerformancePanel", () => require("devtools/client/performance/panel").PerformancePanel);
+loader.lazyGetter(this, "NetMonitorPanel", () => require("devtools/client/netmonitor/panel").NetMonitorPanel);
+loader.lazyGetter(this, "StoragePanel", () => require("devtools/client/storage/panel").StoragePanel);
+loader.lazyGetter(this, "ScratchpadPanel", () => require("devtools/client/scratchpad/scratchpad-panel").ScratchpadPanel);
+loader.lazyGetter(this, "DomPanel", () => require("devtools/client/dom/dom-panel").DomPanel);
+
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper("devtools/client/locales/startup.properties");
+
+var Tools = {};
+exports.Tools = Tools;
+
+// Definitions
+Tools.options = {
+ id: "options",
+ ordinal: 0,
+ url: "chrome://devtools/content/framework/toolbox-options.xhtml",
+ icon: "chrome://devtools/skin/images/tool-options.svg",
+ invertIconForDarkTheme: true,
+ bgTheme: "theme-body",
+ label: l10n("options.label"),
+ iconOnly: true,
+ panelLabel: l10n("options.panelLabel"),
+ tooltip: l10n("optionsButton.tooltip"),
+ inMenu: false,
+
+ isTargetSupported: function () {
+ return true;
+ },
+
+ build: function (iframeWindow, toolbox) {
+ return new OptionsPanel(iframeWindow, toolbox);
+ }
+};
+
+Tools.inspector = {
+ id: "inspector",
+ accesskey: l10n("inspector.accesskey"),
+ key: l10n("inspector.commandkey"),
+ ordinal: 1,
+ modifiers: osString == "Darwin" ? "accel,alt" : "accel,shift",
+ icon: "chrome://devtools/skin/images/tool-inspector.svg",
+ invertIconForDarkTheme: true,
+ url: "chrome://devtools/content/inspector/inspector.xhtml",
+ label: l10n("inspector.label"),
+ panelLabel: l10n("inspector.panelLabel"),
+ get tooltip() {
+ return l10n("inspector.tooltip2",
+ (osString == "Darwin" ? "Cmd+Opt+" : "Ctrl+Shift+") + this.key);
+ },
+ inMenu: true,
+ commands: [
+ "devtools/client/responsivedesign/resize-commands",
+ "devtools/client/inspector/inspector-commands"
+ ],
+
+ preventClosingOnKey: true,
+ onkey: function (panel, toolbox) {
+ toolbox.highlighterUtils.togglePicker();
+ },
+
+ isTargetSupported: function (target) {
+ return target.hasActor("inspector");
+ },
+
+ build: function (iframeWindow, toolbox) {
+ return new InspectorPanel(iframeWindow, toolbox);
+ }
+};
+
+Tools.webConsole = {
+ id: "webconsole",
+ key: l10n("cmd.commandkey"),
+ accesskey: l10n("webConsoleCmd.accesskey"),
+ modifiers: Services.appinfo.OS == "Darwin" ? "accel,alt" : "accel,shift",
+ ordinal: 2,
+ icon: "chrome://devtools/skin/images/tool-webconsole.svg",
+ invertIconForDarkTheme: true,
+ url: "chrome://devtools/content/webconsole/webconsole.xul",
+ label: l10n("ToolboxTabWebconsole.label"),
+ menuLabel: l10n("MenuWebconsole.label"),
+ panelLabel: l10n("ToolboxWebConsole.panelLabel"),
+ get tooltip() {
+ return l10n("ToolboxWebconsole.tooltip2",
+ (osString == "Darwin" ? "Cmd+Opt+" : "Ctrl+Shift+") + this.key);
+ },
+ inMenu: true,
+ commands: "devtools/client/webconsole/console-commands",
+
+ preventClosingOnKey: true,
+ onkey: function (panel, toolbox) {
+ if (toolbox.splitConsole) {
+ return toolbox.focusConsoleInput();
+ }
+
+ panel.focusInput();
+ return undefined;
+ },
+
+ isTargetSupported: function () {
+ return true;
+ },
+
+ build: function (iframeWindow, toolbox) {
+ return new WebConsolePanel(iframeWindow, toolbox);
+ }
+};
+
+Tools.jsdebugger = {
+ id: "jsdebugger",
+ key: l10n("debuggerMenu.commandkey"),
+ accesskey: l10n("debuggerMenu.accesskey"),
+ modifiers: osString == "Darwin" ? "accel,alt" : "accel,shift",
+ ordinal: 3,
+ icon: "chrome://devtools/skin/images/tool-debugger.svg",
+ invertIconForDarkTheme: true,
+ highlightedicon: "chrome://devtools/skin/images/tool-debugger-paused.svg",
+ url: "chrome://devtools/content/debugger/debugger.xul",
+ label: l10n("ToolboxDebugger.label"),
+ panelLabel: l10n("ToolboxDebugger.panelLabel"),
+ get tooltip() {
+ return l10n("ToolboxDebugger.tooltip2",
+ (osString == "Darwin" ? "Cmd+Opt+" : "Ctrl+Shift+") + this.key);
+ },
+ inMenu: true,
+ commands: "devtools/client/debugger/debugger-commands",
+
+ isTargetSupported: function () {
+ return true;
+ },
+
+ build: function (iframeWindow, toolbox) {
+ return new DebuggerPanel(iframeWindow, toolbox);
+ }
+};
+
+function switchDebugger() {
+ if (Services.prefs.getBoolPref("devtools.debugger.new-debugger-frontend")) {
+ const NewDebuggerPanel = require("devtools/client/debugger/new/panel").DebuggerPanel;
+
+ Tools.jsdebugger.url = "chrome://devtools/content/debugger/new/index.html";
+ Tools.jsdebugger.build = function (iframeWindow, toolbox) {
+ return new NewDebuggerPanel(iframeWindow, toolbox);
+ };
+ } else {
+ Tools.jsdebugger.url = "chrome://devtools/content/debugger/debugger.xul";
+ Tools.jsdebugger.build = function (iframeWindow, toolbox) {
+ return new DebuggerPanel(iframeWindow, toolbox);
+ };
+ }
+}
+switchDebugger();
+
+Services.prefs.addObserver(
+ "devtools.debugger.new-debugger-frontend",
+ { observe: switchDebugger },
+ false
+);
+
+Tools.styleEditor = {
+ id: "styleeditor",
+ key: l10n("open.commandkey"),
+ ordinal: 4,
+ visibilityswitch: "devtools.styleeditor.enabled",
+ accesskey: l10n("open.accesskey"),
+ modifiers: "shift",
+ icon: "chrome://devtools/skin/images/tool-styleeditor.svg",
+ invertIconForDarkTheme: true,
+ url: "chrome://devtools/content/styleeditor/styleeditor.xul",
+ label: l10n("ToolboxStyleEditor.label"),
+ panelLabel: l10n("ToolboxStyleEditor.panelLabel"),
+ get tooltip() {
+ return l10n("ToolboxStyleEditor.tooltip3",
+ "Shift+" + functionkey(this.key));
+ },
+ inMenu: true,
+ commands: "devtools/client/styleeditor/styleeditor-commands",
+
+ isTargetSupported: function (target) {
+ return target.hasActor("styleEditor") || target.hasActor("styleSheets");
+ },
+
+ build: function (iframeWindow, toolbox) {
+ return new StyleEditorPanel(iframeWindow, toolbox);
+ }
+};
+
+Tools.shaderEditor = {
+ id: "shadereditor",
+ ordinal: 5,
+ visibilityswitch: "devtools.shadereditor.enabled",
+ icon: "chrome://devtools/skin/images/tool-shadereditor.svg",
+ invertIconForDarkTheme: true,
+ url: "chrome://devtools/content/shadereditor/shadereditor.xul",
+ label: l10n("ToolboxShaderEditor.label"),
+ panelLabel: l10n("ToolboxShaderEditor.panelLabel"),
+ tooltip: l10n("ToolboxShaderEditor.tooltip"),
+
+ isTargetSupported: function (target) {
+ return target.hasActor("webgl") && !target.chrome;
+ },
+
+ build: function (iframeWindow, toolbox) {
+ return new ShaderEditorPanel(iframeWindow, toolbox);
+ }
+};
+
+Tools.canvasDebugger = {
+ id: "canvasdebugger",
+ ordinal: 6,
+ visibilityswitch: "devtools.canvasdebugger.enabled",
+ icon: "chrome://devtools/skin/images/tool-canvas.svg",
+ invertIconForDarkTheme: true,
+ url: "chrome://devtools/content/canvasdebugger/canvasdebugger.xul",
+ label: l10n("ToolboxCanvasDebugger.label"),
+ panelLabel: l10n("ToolboxCanvasDebugger.panelLabel"),
+ tooltip: l10n("ToolboxCanvasDebugger.tooltip"),
+
+ // Hide the Canvas Debugger in the Add-on Debugger and Browser Toolbox
+ // (bug 1047520).
+ isTargetSupported: function (target) {
+ return target.hasActor("canvas") && !target.chrome;
+ },
+
+ build: function (iframeWindow, toolbox) {
+ return new CanvasDebuggerPanel(iframeWindow, toolbox);
+ }
+};
+
+Tools.performance = {
+ id: "performance",
+ ordinal: 7,
+ icon: "chrome://devtools/skin/images/tool-profiler.svg",
+ invertIconForDarkTheme: true,
+ highlightedicon: "chrome://devtools/skin/images/tool-profiler-active.svg",
+ url: "chrome://devtools/content/performance/performance.xul",
+ visibilityswitch: "devtools.performance.enabled",
+ label: l10n("performance.label"),
+ panelLabel: l10n("performance.panelLabel"),
+ get tooltip() {
+ return l10n("performance.tooltip", "Shift+" + functionkey(this.key));
+ },
+ accesskey: l10n("performance.accesskey"),
+ key: l10n("performance.commandkey"),
+ modifiers: "shift",
+ inMenu: true,
+
+ isTargetSupported: function (target) {
+ return target.hasActor("profiler");
+ },
+
+ build: function (frame, target) {
+ return new PerformancePanel(frame, target);
+ }
+};
+
+Tools.memory = {
+ id: "memory",
+ ordinal: 8,
+ icon: "chrome://devtools/skin/images/tool-memory.svg",
+ invertIconForDarkTheme: true,
+ highlightedicon: "chrome://devtools/skin/images/tool-memory-active.svg",
+ url: "chrome://devtools/content/memory/memory.xhtml",
+ visibilityswitch: "devtools.memory.enabled",
+ label: l10n("memory.label"),
+ panelLabel: l10n("memory.panelLabel"),
+ tooltip: l10n("memory.tooltip"),
+
+ isTargetSupported: function (target) {
+ return target.getTrait("heapSnapshots") && !target.isAddon;
+ },
+
+ build: function (frame, target) {
+ return new MemoryPanel(frame, target);
+ }
+};
+
+Tools.netMonitor = {
+ id: "netmonitor",
+ accesskey: l10n("netmonitor.accesskey"),
+ key: l10n("netmonitor.commandkey"),
+ ordinal: 9,
+ modifiers: osString == "Darwin" ? "accel,alt" : "accel,shift",
+ visibilityswitch: "devtools.netmonitor.enabled",
+ icon: "chrome://devtools/skin/images/tool-network.svg",
+ invertIconForDarkTheme: true,
+ url: "chrome://devtools/content/netmonitor/netmonitor.xul",
+ label: l10n("netmonitor.label"),
+ panelLabel: l10n("netmonitor.panelLabel"),
+ get tooltip() {
+ return l10n("netmonitor.tooltip2",
+ (osString == "Darwin" ? "Cmd+Opt+" : "Ctrl+Shift+") + this.key);
+ },
+ inMenu: true,
+
+ isTargetSupported: function (target) {
+ return target.getTrait("networkMonitor");
+ },
+
+ build: function (iframeWindow, toolbox) {
+ return new NetMonitorPanel(iframeWindow, toolbox);
+ }
+};
+
+Tools.storage = {
+ id: "storage",
+ key: l10n("storage.commandkey"),
+ ordinal: 10,
+ accesskey: l10n("storage.accesskey"),
+ modifiers: "shift",
+ visibilityswitch: "devtools.storage.enabled",
+ icon: "chrome://devtools/skin/images/tool-storage.svg",
+ invertIconForDarkTheme: true,
+ url: "chrome://devtools/content/storage/storage.xul",
+ label: l10n("storage.label"),
+ menuLabel: l10n("storage.menuLabel"),
+ panelLabel: l10n("storage.panelLabel"),
+ get tooltip() {
+ return l10n("storage.tooltip3", "Shift+" + functionkey(this.key));
+ },
+ inMenu: true,
+
+ isTargetSupported: function (target) {
+ return target.isLocalTab ||
+ (target.hasActor("storage") && target.getTrait("storageInspector"));
+ },
+
+ build: function (iframeWindow, toolbox) {
+ return new StoragePanel(iframeWindow, toolbox);
+ }
+};
+
+Tools.webAudioEditor = {
+ id: "webaudioeditor",
+ ordinal: 11,
+ visibilityswitch: "devtools.webaudioeditor.enabled",
+ icon: "chrome://devtools/skin/images/tool-webaudio.svg",
+ invertIconForDarkTheme: true,
+ url: "chrome://devtools/content/webaudioeditor/webaudioeditor.xul",
+ label: l10n("ToolboxWebAudioEditor1.label"),
+ panelLabel: l10n("ToolboxWebAudioEditor1.panelLabel"),
+ tooltip: l10n("ToolboxWebAudioEditor1.tooltip"),
+
+ isTargetSupported: function (target) {
+ return !target.chrome && target.hasActor("webaudio");
+ },
+
+ build: function (iframeWindow, toolbox) {
+ return new WebAudioEditorPanel(iframeWindow, toolbox);
+ }
+};
+
+Tools.scratchpad = {
+ id: "scratchpad",
+ ordinal: 12,
+ visibilityswitch: "devtools.scratchpad.enabled",
+ icon: "chrome://devtools/skin/images/tool-scratchpad.svg",
+ invertIconForDarkTheme: true,
+ url: "chrome://devtools/content/scratchpad/scratchpad.xul",
+ label: l10n("scratchpad.label"),
+ panelLabel: l10n("scratchpad.panelLabel"),
+ tooltip: l10n("scratchpad.tooltip"),
+ inMenu: false,
+ commands: "devtools/client/scratchpad/scratchpad-commands",
+
+ isTargetSupported: function (target) {
+ return target.hasActor("console");
+ },
+
+ build: function (iframeWindow, toolbox) {
+ return new ScratchpadPanel(iframeWindow, toolbox);
+ }
+};
+
+Tools.dom = {
+ id: "dom",
+ accesskey: l10n("dom.accesskey"),
+ key: l10n("dom.commandkey"),
+ ordinal: 13,
+ modifiers: osString == "Darwin" ? "accel,alt" : "accel,shift",
+ visibilityswitch: "devtools.dom.enabled",
+ icon: "chrome://devtools/skin/images/tool-dom.svg",
+ invertIconForDarkTheme: true,
+ url: "chrome://devtools/content/dom/dom.html",
+ label: l10n("dom.label"),
+ panelLabel: l10n("dom.panelLabel"),
+ get tooltip() {
+ return l10n("dom.tooltip",
+ (osString == "Darwin" ? "Cmd+Opt+" : "Ctrl+Shift+") + this.key);
+ },
+ inMenu: true,
+
+ isTargetSupported: function (target) {
+ return target.getTrait("webConsoleCommands");
+ },
+
+ build: function (iframeWindow, toolbox) {
+ return new DomPanel(iframeWindow, toolbox);
+ }
+};
+
+var defaultTools = [
+ Tools.options,
+ Tools.webConsole,
+ Tools.inspector,
+ Tools.jsdebugger,
+ Tools.styleEditor,
+ Tools.shaderEditor,
+ Tools.canvasDebugger,
+ Tools.webAudioEditor,
+ Tools.performance,
+ Tools.netMonitor,
+ Tools.storage,
+ Tools.scratchpad,
+ Tools.memory,
+ Tools.dom,
+];
+
+exports.defaultTools = defaultTools;
+
+Tools.darkTheme = {
+ id: "dark",
+ label: l10n("options.darkTheme.label2"),
+ ordinal: 1,
+ stylesheets: ["chrome://devtools/skin/dark-theme.css"],
+ classList: ["theme-dark"],
+};
+
+Tools.lightTheme = {
+ id: "light",
+ label: l10n("options.lightTheme.label2"),
+ ordinal: 2,
+ stylesheets: ["chrome://devtools/skin/light-theme.css"],
+ classList: ["theme-light"],
+};
+
+Tools.firebugTheme = {
+ id: "firebug",
+ label: l10n("options.firebugTheme.label2"),
+ ordinal: 3,
+ stylesheets: ["chrome://devtools/skin/firebug-theme.css"],
+ classList: ["theme-light", "theme-firebug"],
+};
+
+exports.defaultThemes = [
+ Tools.darkTheme,
+ Tools.lightTheme,
+ Tools.firebugTheme,
+];
+
+// White-list buttons that can be toggled to prevent adding prefs for
+// addons that have manually inserted toolbarbuttons into DOM.
+// (By default, supported target is only local tab)
+exports.ToolboxButtons = [
+ { id: "command-button-frames",
+ isTargetSupported: target => {
+ return target.activeTab && target.activeTab.traits.frames;
+ }
+ },
+ { id: "command-button-splitconsole",
+ isTargetSupported: target => !target.isAddon },
+ { id: "command-button-responsive" },
+ { id: "command-button-paintflashing" },
+ { id: "command-button-scratchpad" },
+ { id: "command-button-screenshot" },
+ { id: "command-button-rulers" },
+ { id: "command-button-measure" },
+ { id: "command-button-noautohide",
+ isTargetSupported: target => target.chrome },
+];
+
+/**
+ * Lookup l10n string from a string bundle.
+ *
+ * @param {string} name
+ * The key to lookup.
+ * @param {string} arg
+ * Optional format argument.
+ * @returns A localized version of the given key.
+ */
+function l10n(name, arg) {
+ try {
+ return arg ? L10N.getFormatStr(name, arg) : L10N.getStr(name);
+ } catch (ex) {
+ console.log("Error reading '" + name + "'");
+ throw new Error("l10n error with " + name);
+ }
+}
+
+function functionkey(shortkey) {
+ return shortkey.split("_")[1];
+}
diff --git a/devtools/client/devtools-startup.js b/devtools/client/devtools-startup.js
new file mode 100644
index 000000000..2271dd790
--- /dev/null
+++ b/devtools/client/devtools-startup.js
@@ -0,0 +1,215 @@
+/* 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/. */
+
+/**
+ * This XPCOM component is loaded very early.
+ * It handles command line arguments like -jsconsole, but also ensures starting
+ * core modules like 'devtools-browser.js' that hooks the browser windows
+ * and ensure setting up tools.
+ *
+ * Be careful to lazy load dependencies as much as possible.
+ **/
+
+"use strict";
+
+const { interfaces: Ci, utils: Cu } = Components;
+const kDebuggerPrefs = [
+ "devtools.debugger.remote-enabled",
+ "devtools.chrome.enabled"
+];
+const { XPCOMUtils } = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {});
+XPCOMUtils.defineLazyModuleGetter(this, "Services", "resource://gre/modules/Services.jsm");
+
+function DevToolsStartup() {}
+
+DevToolsStartup.prototype = {
+ handle: function (cmdLine) {
+ let consoleFlag = cmdLine.handleFlag("jsconsole", false);
+ let debuggerFlag = cmdLine.handleFlag("jsdebugger", false);
+ let devtoolsFlag = cmdLine.handleFlag("devtools", false);
+
+ if (consoleFlag) {
+ this.handleConsoleFlag(cmdLine);
+ }
+ if (debuggerFlag) {
+ this.handleDebuggerFlag(cmdLine);
+ }
+ let debuggerServerFlag;
+ try {
+ debuggerServerFlag =
+ cmdLine.handleFlagWithParam("start-debugger-server", false);
+ } catch (e) {
+ // We get an error if the option is given but not followed by a value.
+ // By catching and trying again, the value is effectively optional.
+ debuggerServerFlag = cmdLine.handleFlag("start-debugger-server", false);
+ }
+ if (debuggerServerFlag) {
+ this.handleDebuggerServerFlag(cmdLine, debuggerServerFlag);
+ }
+
+ let onStartup = function (window) {
+ Services.obs.removeObserver(onStartup,
+ "browser-delayed-startup-finished");
+ // Ensure loading core module once firefox is ready
+ this.initDevTools();
+
+ if (devtoolsFlag) {
+ this.handleDevToolsFlag(window);
+ }
+ }.bind(this);
+ Services.obs.addObserver(onStartup, "browser-delayed-startup-finished",
+ false);
+ },
+
+ initDevTools: function () {
+ let { loader } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+ // Ensure loading main devtools module that hooks up into browser UI
+ // and initialize all devtools machinery.
+ loader.require("devtools/client/framework/devtools-browser");
+ },
+
+ handleConsoleFlag: function (cmdLine) {
+ let window = Services.wm.getMostRecentWindow("devtools:webconsole");
+ if (!window) {
+ this.initDevTools();
+
+ let { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+ let hudservice = require("devtools/client/webconsole/hudservice");
+ let { console } = Cu.import("resource://gre/modules/Console.jsm", {});
+ hudservice.toggleBrowserConsole().then(null, console.error);
+ } else {
+ // the Browser Console was already open
+ window.focus();
+ }
+
+ if (cmdLine.state == Ci.nsICommandLine.STATE_REMOTE_AUTO) {
+ cmdLine.preventDefault = true;
+ }
+ },
+
+ // Open the toolbox on the selected tab once the browser starts up.
+ handleDevToolsFlag: function (window) {
+ const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+ const {gDevTools} = require("devtools/client/framework/devtools");
+ const {TargetFactory} = require("devtools/client/framework/target");
+ let target = TargetFactory.forTab(window.gBrowser.selectedTab);
+ gDevTools.showToolbox(target);
+ },
+
+ _isRemoteDebuggingEnabled() {
+ let remoteDebuggingEnabled = false;
+ try {
+ remoteDebuggingEnabled = kDebuggerPrefs.every(pref => {
+ return Services.prefs.getBoolPref(pref);
+ });
+ } catch (ex) {
+ console.error(ex);
+ return false;
+ }
+ if (!remoteDebuggingEnabled) {
+ let errorMsg = "Could not run chrome debugger! You need the following " +
+ "prefs to be set to true: " + kDebuggerPrefs.join(", ");
+ console.error(new Error(errorMsg));
+ // Dump as well, as we're doing this from a commandline, make sure people
+ // don't miss it:
+ dump(errorMsg + "\n");
+ }
+ return remoteDebuggingEnabled;
+ },
+
+ handleDebuggerFlag: function (cmdLine) {
+ if (!this._isRemoteDebuggingEnabled()) {
+ return;
+ }
+ const { BrowserToolboxProcess } = Cu.import("resource://devtools/client/framework/ToolboxProcess.jsm", {});
+ BrowserToolboxProcess.init();
+
+ if (cmdLine.state == Ci.nsICommandLine.STATE_REMOTE_AUTO) {
+ cmdLine.preventDefault = true;
+ }
+ },
+
+ /**
+ * Handle the --start-debugger-server command line flag. The options are:
+ * --start-debugger-server
+ * The portOrPath parameter is boolean true in this case. Reads and uses the defaults
+ * from devtools.debugger.remote-port and devtools.debugger.remote-websocket prefs.
+ * The default values of these prefs are port 6000, WebSocket disabled.
+ *
+ * --start-debugger-server 6789
+ * Start the non-WebSocket server on port 6789.
+ *
+ * --start-debugger-server /path/to/filename
+ * Start the server on a Unix domain socket.
+ *
+ * --start-debugger-server ws:6789
+ * Start the WebSocket server on port 6789.
+ *
+ * --start-debugger-server ws:
+ * Start the WebSocket server on the default port (taken from d.d.remote-port)
+ */
+ handleDebuggerServerFlag: function (cmdLine, portOrPath) {
+ if (!this._isRemoteDebuggingEnabled()) {
+ return;
+ }
+
+ let webSocket = false;
+ let defaultPort = Services.prefs.getIntPref("devtools.debugger.remote-port");
+ if (portOrPath === true) {
+ // Default to pref values if no values given on command line
+ webSocket = Services.prefs.getBoolPref("devtools.debugger.remote-websocket");
+ portOrPath = defaultPort;
+ } else if (portOrPath.startsWith("ws:")) {
+ webSocket = true;
+ let port = portOrPath.slice(3);
+ portOrPath = Number(port) ? port : defaultPort;
+ }
+
+ let { DevToolsLoader } =
+ Cu.import("resource://devtools/shared/Loader.jsm", {});
+
+ try {
+ // Create a separate loader instance, so that we can be sure to receive
+ // a separate instance of the DebuggingServer from the rest of the
+ // devtools. This allows us to safely use the tools against even the
+ // actors and DebuggingServer itself, especially since we can mark
+ // serverLoader as invisible to the debugger (unlike the usual loader
+ // settings).
+ let serverLoader = new DevToolsLoader();
+ serverLoader.invisibleToDebugger = true;
+ let { DebuggerServer: debuggerServer } =
+ serverLoader.require("devtools/server/main");
+ debuggerServer.init();
+ debuggerServer.addBrowserActors();
+ debuggerServer.allowChromeProcess = true;
+
+ let listener = debuggerServer.createListener();
+ listener.portOrPath = portOrPath;
+ listener.webSocket = webSocket;
+ listener.open();
+ dump("Started debugger server on " + portOrPath + "\n");
+ } catch (e) {
+ dump("Unable to start debugger server on " + portOrPath + ": " + e);
+ }
+
+ if (cmdLine.state == Ci.nsICommandLine.STATE_REMOTE_AUTO) {
+ cmdLine.preventDefault = true;
+ }
+ },
+
+ /* eslint-disable max-len */
+ helpInfo: " --jsconsole Open the Browser Console.\n" +
+ " --jsdebugger Open the Browser Toolbox.\n" +
+ " --devtools Open DevTools on initial load.\n" +
+ " --start-debugger-server [ws:][ <port> | <path> ] Start the debugger server on\n" +
+ " a TCP port or Unix domain socket path. Defaults to TCP port\n" +
+ " 6000. Use WebSocket protocol if ws: prefix is specified.\n",
+ /* eslint-disable max-len */
+
+ classID: Components.ID("{9e9a9283-0ce9-4e4a-8f1c-ba129a032c32}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsICommandLineHandler]),
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory(
+ [DevToolsStartup]);
diff --git a/devtools/client/devtools-startup.manifest b/devtools/client/devtools-startup.manifest
new file mode 100644
index 000000000..ac9583d26
--- /dev/null
+++ b/devtools/client/devtools-startup.manifest
@@ -0,0 +1,3 @@
+component {9e9a9283-0ce9-4e4a-8f1c-ba129a032c32} devtools-startup.js
+contract @mozilla.org/devtools/startup-clh;1 {9e9a9283-0ce9-4e4a-8f1c-ba129a032c32}
+category command-line-handler m-devtools @mozilla.org/devtools/startup-clh;1
diff --git a/devtools/client/dom/.eslintrc.js b/devtools/client/dom/.eslintrc.js
new file mode 100644
index 000000000..1ad36e780
--- /dev/null
+++ b/devtools/client/dom/.eslintrc.js
@@ -0,0 +1,17 @@
+"use strict";
+
+module.exports = {
+ "globals": {
+ "XMLHttpRequest": true,
+ "window": true,
+ "define": true,
+ "addEventListener": true,
+ "document": true,
+ "dispatchEvent": true,
+ "MessageEvent": true
+ },
+ "rules": {
+ "indent": "off",
+ "padded-blocks": "off",
+ }
+};
diff --git a/devtools/client/dom/content/actions/filter.js b/devtools/client/dom/content/actions/filter.js
new file mode 100644
index 000000000..3fac9d278
--- /dev/null
+++ b/devtools/client/dom/content/actions/filter.js
@@ -0,0 +1,21 @@
+/* -*- 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 constants = require("../constants");
+
+/**
+ * Used to filter DOM panel content.
+ */
+function setVisibilityFilter(filter) {
+ return {
+ filter: filter,
+ type: constants.SET_VISIBILITY_FILTER,
+ };
+}
+
+// Exports from this module
+exports.setVisibilityFilter = setVisibilityFilter;
diff --git a/devtools/client/dom/content/actions/grips.js b/devtools/client/dom/content/actions/grips.js
new file mode 100644
index 000000000..23d4fc895
--- /dev/null
+++ b/devtools/client/dom/content/actions/grips.js
@@ -0,0 +1,54 @@
+/* -*- 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/. */
+ /* globals DomProvider */
+"use strict";
+
+const constants = require("../constants");
+
+/**
+ * Used to fetch grip prototype and properties from the backend.
+ */
+function requestProperties(grip) {
+ return {
+ grip: grip,
+ type: constants.FETCH_PROPERTIES,
+ status: "start",
+ error: false
+ };
+}
+
+/**
+ * Executed when grip properties are received from the backend.
+ */
+function receiveProperties(grip, response, error) {
+ return {
+ grip: grip,
+ type: constants.FETCH_PROPERTIES,
+ status: "end",
+ response: response,
+ error: error
+ };
+}
+
+/**
+ * Used to get properties from the backend and fire an action
+ * when they are received.
+ */
+function fetchProperties(grip) {
+ return dispatch => {
+ // dispatch(requestProperties(grip));
+
+ // Use 'DomProvider' object exposed from the chrome scope.
+ return DomProvider.getPrototypeAndProperties(grip).then(response => {
+ dispatch(receiveProperties(grip, response));
+ });
+ };
+}
+
+// Exports from this module
+exports.requestProperties = requestProperties;
+exports.receiveProperties = receiveProperties;
+exports.fetchProperties = fetchProperties;
diff --git a/devtools/client/dom/content/actions/moz.build b/devtools/client/dom/content/actions/moz.build
new file mode 100644
index 000000000..6454c00cc
--- /dev/null
+++ b/devtools/client/dom/content/actions/moz.build
@@ -0,0 +1,9 @@
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'filter.js',
+ 'grips.js',
+)
diff --git a/devtools/client/dom/content/components/dom-tree.js b/devtools/client/dom/content/components/dom-tree.js
new file mode 100644
index 000000000..ef529ac3f
--- /dev/null
+++ b/devtools/client/dom/content/components/dom-tree.js
@@ -0,0 +1,91 @@
+/* -*- 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";
+
+// React & Redux
+const React = require("devtools/client/shared/vendor/react");
+const { connect } = require("devtools/client/shared/vendor/react-redux");
+
+// Reps
+const { createFactories } = require("devtools/client/shared/components/reps/rep-utils");
+const TreeView = React.createFactory(require("devtools/client/shared/components/tree/tree-view"));
+const { Rep } = createFactories(require("devtools/client/shared/components/reps/rep"));
+const { Grip } = require("devtools/client/shared/components/reps/grip");
+
+// DOM Panel
+const { GripProvider } = require("../grip-provider");
+const { DomDecorator } = require("../dom-decorator");
+
+// Shortcuts
+const PropTypes = React.PropTypes;
+
+/**
+ * Renders DOM panel tree.
+ */
+var DomTree = React.createClass({
+ displayName: "DomTree",
+
+ propTypes: {
+ object: PropTypes.any,
+ filter: PropTypes.string,
+ dispatch: PropTypes.func.isRequired,
+ grips: PropTypes.object,
+ },
+
+ /**
+ * Filter DOM properties. Return true if the object
+ * should be visible in the tree.
+ */
+ onFilter: function (object) {
+ if (!this.props.filter) {
+ return true;
+ }
+
+ return (object.name && object.name.indexOf(this.props.filter) > -1);
+ },
+
+ /**
+ * Render DOM panel content
+ */
+ render: function () {
+ let columns = [{
+ "id": "value"
+ }];
+
+ // This is the integration point with Reps. The DomTree is using
+ // Reps to render all values. The code also specifies default rep
+ // used for data types that don't have its own specific template.
+ let renderValue = props => {
+ return Rep(Object.assign({}, props, {
+ defaultRep: Grip,
+ cropLimit: 50,
+ }));
+ };
+
+ return (
+ TreeView({
+ object: this.props.object,
+ provider: new GripProvider(this.props.grips, this.props.dispatch),
+ decorator: new DomDecorator(),
+ mode: "short",
+ columns: columns,
+ renderValue: renderValue,
+ onFilter: this.onFilter
+ })
+ );
+ }
+});
+
+const mapStateToProps = (state) => {
+ return {
+ grips: state.grips,
+ filter: state.filter
+ };
+};
+
+// Exports from this module
+module.exports = connect(mapStateToProps)(DomTree);
+
diff --git a/devtools/client/dom/content/components/main-frame.js b/devtools/client/dom/content/components/main-frame.js
new file mode 100644
index 000000000..d786314e2
--- /dev/null
+++ b/devtools/client/dom/content/components/main-frame.js
@@ -0,0 +1,63 @@
+/* -*- 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";
+
+// React & Redux
+const React = require("devtools/client/shared/vendor/react");
+const { connect } = require("devtools/client/shared/vendor/react-redux");
+
+// DOM Panel
+const DomTree = React.createFactory(require("./dom-tree"));
+const MainToolbar = React.createFactory(require("./main-toolbar"));
+
+// Shortcuts
+const { div } = React.DOM;
+const PropTypes = React.PropTypes;
+
+/**
+ * Renders basic layout of the DOM panel. The DOM panel cotent consists
+ * from two main parts: toolbar and tree.
+ */
+var MainFrame = React.createClass({
+ displayName: "MainFrame",
+
+ propTypes: {
+ object: PropTypes.any,
+ filter: PropTypes.string,
+ dispatch: PropTypes.func.isRequired,
+ },
+
+ /**
+ * Render DOM panel content
+ */
+ render: function () {
+ return (
+ div({className: "mainFrame"},
+ MainToolbar({
+ dispatch: this.props.dispatch,
+ object: this.props.object
+ }),
+ div({className: "treeTableBox"},
+ DomTree({
+ object: this.props.object,
+ filter: this.props.filter,
+ })
+ )
+ )
+ );
+ }
+});
+
+// Transform state into props
+// Note: use https://github.com/faassen/reselect for better performance.
+const mapStateToProps = (state) => {
+ return {
+ filter: state.filter
+ };
+};
+
+// Exports from this module
+module.exports = connect(mapStateToProps)(MainFrame);
diff --git a/devtools/client/dom/content/components/main-toolbar.js b/devtools/client/dom/content/components/main-toolbar.js
new file mode 100644
index 000000000..c44a6b4ca
--- /dev/null
+++ b/devtools/client/dom/content/components/main-toolbar.js
@@ -0,0 +1,66 @@
+/* -*- 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";
+
+// React
+const React = require("devtools/client/shared/vendor/react");
+const { l10n } = require("../utils");
+
+// Reps
+const { createFactories } = require("devtools/client/shared/components/reps/rep-utils");
+const { Toolbar, ToolbarButton } = createFactories(require("devtools/client/jsonview/components/reps/toolbar"));
+
+// DOM Panel
+const SearchBox = React.createFactory(require("devtools/client/shared/components/search-box"));
+
+// Actions
+const { fetchProperties } = require("../actions/grips");
+const { setVisibilityFilter } = require("../actions/filter");
+
+// Shortcuts
+const PropTypes = React.PropTypes;
+
+/**
+ * This template is responsible for rendering a toolbar
+ * within the 'Headers' panel.
+ */
+var MainToolbar = React.createClass({
+ displayName: "MainToolbar",
+
+ propTypes: {
+ object: PropTypes.any.isRequired,
+ dispatch: PropTypes.func.isRequired,
+ },
+
+ onRefresh: function () {
+ this.props.dispatch(fetchProperties(this.props.object));
+ },
+
+ onSearch: function (value) {
+ this.props.dispatch(setVisibilityFilter(value));
+ },
+
+ render: function () {
+ return (
+ Toolbar({},
+ ToolbarButton({
+ className: "btn refresh",
+ onClick: this.onRefresh},
+ l10n.getStr("dom.refresh")
+ ),
+ SearchBox({
+ delay: 250,
+ onChange: this.onSearch,
+ placeholder: l10n.getStr("dom.filterDOMPanel"),
+ type: "filter"
+ })
+ )
+ );
+ }
+});
+
+// Exports from this module
+module.exports = MainToolbar;
diff --git a/devtools/client/dom/content/components/moz.build b/devtools/client/dom/content/components/moz.build
new file mode 100644
index 000000000..0fa1f8089
--- /dev/null
+++ b/devtools/client/dom/content/components/moz.build
@@ -0,0 +1,10 @@
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'dom-tree.js',
+ 'main-frame.js',
+ 'main-toolbar.js'
+)
diff --git a/devtools/client/dom/content/constants.js b/devtools/client/dom/content/constants.js
new file mode 100644
index 000000000..f06ca8512
--- /dev/null
+++ b/devtools/client/dom/content/constants.js
@@ -0,0 +1,9 @@
+/* -*- 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";
+
+exports.FETCH_PROPERTIES = "FETCH_PROPERTIES";
+exports.SET_VISIBILITY_FILTER = "SET_VISIBILITY_FILTER";
diff --git a/devtools/client/dom/content/dom-decorator.js b/devtools/client/dom/content/dom-decorator.js
new file mode 100644
index 000000000..4042df8d3
--- /dev/null
+++ b/devtools/client/dom/content/dom-decorator.js
@@ -0,0 +1,50 @@
+/* -*- 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 { Property } = require("./reducers/grips");
+
+// Implementation
+
+function DomDecorator() {
+}
+
+/**
+ * Decorator for DOM panel tree component. It's responsible for
+ * appending an icon to read only properties.
+ */
+DomDecorator.prototype = {
+ getRowClass: function (object) {
+ if (object instanceof Property) {
+ let value = object.value;
+ let names = [];
+
+ if (value.enumerable) {
+ names.push("enumerable");
+ }
+ if (value.writable) {
+ names.push("writable");
+ }
+ if (value.configurable) {
+ names.push("configurable");
+ }
+
+ return names;
+ }
+
+ return null;
+ },
+
+ /**
+ * Return custom React template for specified object. The template
+ * might depend on specified column.
+ */
+ getValueRep: function (value, colId) {
+ }
+};
+
+// Exports from this module
+exports.DomDecorator = DomDecorator;
diff --git a/devtools/client/dom/content/dom-view.css b/devtools/client/dom/content/dom-view.css
new file mode 100644
index 000000000..631f8c536
--- /dev/null
+++ b/devtools/client/dom/content/dom-view.css
@@ -0,0 +1,118 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+/******************************************************************************/
+/* General */
+
+body {
+ padding: 0;
+ margin: 0;
+ overflow: hidden;
+}
+
+.mainFrame {
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+}
+
+.mainFrame > .treeTableBox {
+ flex: 1 1 auto;
+ overflow: auto;
+}
+
+/******************************************************************************/
+/* TreeView Customization */
+
+.treeTable {
+ width: 100%;
+}
+
+/* Space for read only properties icon */
+.treeTable td.treeValueCell {
+ padding-inline-start: 16px;
+}
+
+.treeTable .treeLabel,
+.treeTable td.treeValueCell .objectBox {
+ direction: ltr; /* Don't change the direction of english labels */
+}
+
+/* Read only properties have a padlock icon */
+.treeTable tr:not(.writable) td.treeValueCell {
+ background: url("chrome://devtools/skin/images/firebug/read-only.svg") no-repeat;
+ background-position: 1px 5px;
+ background-size: 10px 10px;
+}
+
+.treeTable tr:not(.writable) td.treeValueCell:dir(rtl) {
+ background-position-x: right 1px;
+}
+
+/* Non-enumerable properties are grayed out */
+.treeTable tr:not(.enumerable) td.treeValueCell {
+ opacity: 0.7;
+}
+
+.treeTable > tbody > tr > td {
+ border-bottom: 1px solid #EFEFEF;
+}
+
+/* Label Types */
+.treeTable .userLabel,
+.treeTable .userClassLabel,
+.treeTable .userFunctionLabel {
+ font-weight: bold;
+}
+
+.treeTable .userLabel {
+ color: #000000;
+}
+
+.treeTable .userClassLabel {
+ color: #E90000;
+}
+
+.treeTable .userFunctionLabel {
+ color: #025E2A;
+}
+
+.treeTable .domLabel {
+ color: #000000;
+}
+
+.treeTable .domClassLabel {
+ color: #E90000;
+}
+
+.treeTable .domFunctionLabel {
+ color: #025E2A;
+}
+
+.treeTable .ordinalLabel {
+ color: SlateBlue;
+ font-weight: bold;
+}
+
+/******************************************************************************/
+/* Search box */
+.devtools-searchbox {
+ float: right;
+}
+
+.devtools-searchbox:dir(rtl) {
+ float: left;
+}
+
+/******************************************************************************/
+/* Theme Dark */
+
+.theme-dark .treeTable > tbody > tr > td {
+ border-bottom: none;
+}
+
+.theme-dark body {
+ background-color: var(--theme-body-background);
+}
diff --git a/devtools/client/dom/content/dom-view.js b/devtools/client/dom/content/dom-view.js
new file mode 100644
index 000000000..b0ea11dee
--- /dev/null
+++ b/devtools/client/dom/content/dom-view.js
@@ -0,0 +1,65 @@
+/* -*- 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";
+
+// React & Redux
+const React = require("devtools/client/shared/vendor/react");
+const ReactDOM = require("devtools/client/shared/vendor/react-dom");
+const { Provider } = require("devtools/client/shared/vendor/react-redux");
+const { combineReducers } = require("devtools/client/shared/vendor/redux");
+
+// DOM Panel
+const MainFrame = React.createFactory(require("./components/main-frame"));
+
+// Store
+const createStore = require("devtools/client/shared/redux/create-store")({
+ log: false
+});
+
+const { reducers } = require("./reducers/index");
+const store = createStore(combineReducers(reducers));
+
+/**
+ * This object represents view of the DOM panel and is responsible
+ * for rendering the content. It renders the top level ReactJS
+ * component: the MainFrame.
+ */
+function DomView(localStore) {
+ addEventListener("devtools/chrome/message",
+ this.onMessage.bind(this), true);
+
+ // Make it local so, tests can access it.
+ this.store = localStore;
+}
+
+DomView.prototype = {
+ initialize: function (rootGrip) {
+ let content = document.querySelector("#content");
+ let mainFrame = MainFrame({
+ object: rootGrip,
+ });
+
+ // Render top level component
+ let provider = React.createElement(Provider, {
+ store: this.store
+ }, mainFrame);
+
+ this.mainFrame = ReactDOM.render(provider, content);
+ },
+
+ onMessage: function (event) {
+ let data = event.data;
+ let method = data.type;
+
+ if (typeof this[method] == "function") {
+ this[method](data.args);
+ }
+ },
+};
+
+// Construct DOM panel view object and expose it to tests.
+// Tests can access it throught: |panel.panelWin.view|
+window.view = new DomView(store);
diff --git a/devtools/client/dom/content/grip-provider.js b/devtools/client/dom/content/grip-provider.js
new file mode 100644
index 000000000..bcda1ff18
--- /dev/null
+++ b/devtools/client/dom/content/grip-provider.js
@@ -0,0 +1,97 @@
+/* -*- 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 { fetchProperties } = require("./actions/grips");
+const { Property } = require("./reducers/grips");
+
+// Implementation
+function GripProvider(grips, dispatch) {
+ this.grips = grips;
+ this.dispatch = dispatch;
+}
+
+/**
+ * This object provides data for the tree displayed in the tooltip
+ * content.
+ */
+GripProvider.prototype = {
+ /**
+ * Fetches properties from the backend. These properties might be
+ * displayed as child objects in e.g. a tree UI widget.
+ */
+ getChildren: function (object) {
+ let grip = object;
+ if (object instanceof Property) {
+ grip = this.getValue(object);
+ }
+
+ if (!grip || !grip.actor) {
+ return [];
+ }
+
+ let props = this.grips.get(grip.actor);
+ if (!props) {
+ // Fetch missing data from the backend. Returning a promise
+ // from data provider causes the tree to show a spinner.
+ return this.dispatch(fetchProperties(grip));
+ }
+
+ return props;
+ },
+
+ hasChildren: function (object) {
+ if (object instanceof Property) {
+ let value = this.getValue(object);
+ if (!value) {
+ return false;
+ }
+
+ let hasChildren = value.ownPropertyLength > 0;
+
+ if (value.preview) {
+ hasChildren = hasChildren || value.preview.ownPropertiesLength > 0;
+ }
+
+ if (value.preview) {
+ let preview = value.preview;
+ let k = preview.kind;
+ let objectsWithProps = ["DOMNode", "ObjectWithURL"];
+ hasChildren = hasChildren || (objectsWithProps.indexOf(k) != -1);
+ hasChildren = hasChildren || (k == "ArrayLike" && preview.length > 0);
+ }
+
+ return (value.type == "object" && hasChildren);
+ }
+
+ return null;
+ },
+
+ getValue: function (object) {
+ if (object instanceof Property) {
+ let value = object.value;
+ return (typeof value.value != "undefined") ? value.value :
+ value.getterValue;
+ }
+
+ return object;
+ },
+
+ getLabel: function (object) {
+ return (object instanceof Property) ? object.name : null;
+ },
+
+ getKey: function (object) {
+ return (object instanceof Property) ? object.key : null;
+ },
+
+ getType: function (object) {
+ return object.class ? object.class : "";
+ },
+};
+
+// Exports from this module
+exports.GripProvider = GripProvider;
diff --git a/devtools/client/dom/content/moz.build b/devtools/client/dom/content/moz.build
new file mode 100644
index 000000000..b4a9c76bf
--- /dev/null
+++ b/devtools/client/dom/content/moz.build
@@ -0,0 +1,19 @@
+# vim: set filetype=python:
+# 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/.
+
+DIRS += [
+ 'actions',
+ 'components',
+ 'reducers',
+]
+
+DevToolsModules(
+ 'constants.js',
+ 'dom-decorator.js',
+ 'dom-view.css',
+ 'dom-view.js',
+ 'grip-provider.js',
+ 'utils.js',
+)
diff --git a/devtools/client/dom/content/reducers/filter.js b/devtools/client/dom/content/reducers/filter.js
new file mode 100644
index 000000000..3eb5bd3fc
--- /dev/null
+++ b/devtools/client/dom/content/reducers/filter.js
@@ -0,0 +1,29 @@
+/* -*- 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 constants = require("../constants");
+
+/**
+ * Initial state definition
+ */
+function getInitialState() {
+ return "";
+}
+
+/**
+ * Filter displayed object properties.
+ */
+function filter(state = getInitialState(), action) {
+ if (action.type == constants.SET_VISIBILITY_FILTER) {
+ return action.filter;
+ }
+
+ return state;
+}
+
+// Exports from this module
+exports.filter = filter;
diff --git a/devtools/client/dom/content/reducers/grips.js b/devtools/client/dom/content/reducers/grips.js
new file mode 100644
index 000000000..c7d589434
--- /dev/null
+++ b/devtools/client/dom/content/reducers/grips.js
@@ -0,0 +1,123 @@
+/* -*- 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 constants = require("../constants");
+
+/**
+ * Initial state definition
+ */
+function getInitialState() {
+ return new Map();
+}
+
+/**
+ * Maintain a cache of received grip responses from the backend.
+ */
+function grips(state = getInitialState(), action) {
+ // This reducer supports only one action, fetching actor properties
+ // from the backend so, bail out if we are dealing with any other
+ // action.
+ if (action.type != constants.FETCH_PROPERTIES) {
+ return state;
+ }
+
+ switch (action.status) {
+ case "start":
+ return onRequestProperties(state, action);
+ case "end":
+ return onReceiveProperties(state, action);
+ }
+
+ return state;
+}
+
+/**
+ * Handle requestProperties action
+ */
+function onRequestProperties(state, action) {
+ return state;
+}
+
+/**
+ * Handle receiveProperties action
+ */
+function onReceiveProperties(cache, action) {
+ let response = action.response;
+ let from = response.from;
+ let className = action.grip.class;
+
+ // Properly deal with getters.
+ mergeProperties(response);
+
+ // Compute list of requested children.
+ let previewProps = response.preview ? response.preview.ownProperties : null;
+ let ownProps = response.ownProperties || previewProps || [];
+
+ let props = Object.keys(ownProps).map(key => {
+ // Array indexes as a special case. We convert any keys that are string
+ // representations of integers to integers.
+ if (className === "Array" && isInteger(key)) {
+ key = parseInt(key, 10);
+ }
+ return new Property(key, ownProps[key], key);
+ });
+
+ props.sort(sortName);
+
+ // Return new state/map.
+ let newCache = new Map(cache);
+ newCache.set(from, props);
+
+ return newCache;
+}
+
+// Helpers
+
+function mergeProperties(response) {
+ let { ownProperties } = response;
+
+ // 'safeGetterValues' is new and isn't necessary defined on old grips.
+ let safeGetterValues = response.safeGetterValues || {};
+
+ // Merge the safe getter values into one object such that we can use it
+ // in variablesView.
+ for (let name of Object.keys(safeGetterValues)) {
+ if (name in ownProperties) {
+ let { getterValue, getterPrototypeLevel } = safeGetterValues[name];
+ ownProperties[name].getterValue = getterValue;
+ ownProperties[name].getterPrototypeLevel = getterPrototypeLevel;
+ } else {
+ ownProperties[name] = safeGetterValues[name];
+ }
+ }
+}
+
+function sortName(a, b) {
+ // Display non-enumerable properties at the end.
+ if (!a.value.enumerable && b.value.enumerable) {
+ return 1;
+ }
+ if (a.value.enumerable && !b.value.enumerable) {
+ return -1;
+ }
+ return a.name > b.name ? 1 : -1;
+}
+
+function isInteger(n) {
+ // We use parseInt(n, 10) == n to disregard scientific notation e.g. "3e24"
+ return isFinite(n) && parseInt(n, 10) == n;
+}
+
+function Property(name, value, key) {
+ this.name = name;
+ this.value = value;
+ this.key = key;
+}
+
+// Exports from this module
+exports.grips = grips;
+exports.Property = Property;
diff --git a/devtools/client/dom/content/reducers/index.js b/devtools/client/dom/content/reducers/index.js
new file mode 100644
index 000000000..1900487e1
--- /dev/null
+++ b/devtools/client/dom/content/reducers/index.js
@@ -0,0 +1,14 @@
+/* -*- 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 { grips } = require("./grips");
+const { filter } = require("./filter");
+
+exports.reducers = {
+ grips,
+ filter,
+};
diff --git a/devtools/client/dom/content/reducers/moz.build b/devtools/client/dom/content/reducers/moz.build
new file mode 100644
index 000000000..0a00b3feb
--- /dev/null
+++ b/devtools/client/dom/content/reducers/moz.build
@@ -0,0 +1,10 @@
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'filter.js',
+ 'grips.js',
+ 'index.js',
+)
diff --git a/devtools/client/dom/content/utils.js b/devtools/client/dom/content/utils.js
new file mode 100644
index 000000000..645ba7921
--- /dev/null
+++ b/devtools/client/dom/content/utils.js
@@ -0,0 +1,27 @@
+/* -*- 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";
+
+/**
+ * The default localization just returns the last part of the key
+ * (all after the last dot).
+ */
+const DefaultL10N = {
+ getStr: function (key) {
+ let index = key.lastIndexOf(".");
+ return key.substr(index + 1);
+ }
+};
+
+/**
+ * The 'l10n' object is set by main.js in case the DOM panel content
+ * runs within a scope with chrome privileges.
+ *
+ * Note that DOM panel content can also run within a scope with no chrome
+ * privileges, e.g. in an iframe with type 'content' or in a browser tab,
+ * which allows using our own tools for development.
+ */
+exports.l10n = window.l10n || DefaultL10N;
diff --git a/devtools/client/dom/dom-panel.js b/devtools/client/dom/dom-panel.js
new file mode 100644
index 000000000..5cb6d0061
--- /dev/null
+++ b/devtools/client/dom/dom-panel.js
@@ -0,0 +1,241 @@
+/* -*- 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 { Cu } = require("chrome");
+const defer = require("devtools/shared/defer");
+const { ObjectClient } = require("devtools/shared/client/main");
+
+const promise = require("promise");
+const EventEmitter = require("devtools/shared/event-emitter");
+const { Task } = require("devtools/shared/task");
+
+/**
+ * This object represents DOM panel. It's responsibility is to
+ * render Document Object Model of the current debugger target.
+ */
+function DomPanel(iframeWindow, toolbox) {
+ this.panelWin = iframeWindow;
+ this._toolbox = toolbox;
+
+ this.onTabNavigated = this.onTabNavigated.bind(this);
+ this.onContentMessage = this.onContentMessage.bind(this);
+ this.onPanelVisibilityChange = this.onPanelVisibilityChange.bind(this);
+
+ this.pendingRequests = new Map();
+
+ EventEmitter.decorate(this);
+}
+
+DomPanel.prototype = {
+ /**
+ * Open is effectively an asynchronous constructor.
+ *
+ * @return object
+ * A promise that is resolved when the DOM panel completes opening.
+ */
+ open: Task.async(function* () {
+ if (this._opening) {
+ return this._opening;
+ }
+
+ let deferred = promise.defer();
+ this._opening = deferred.promise;
+
+ // Local monitoring needs to make the target remote.
+ if (!this.target.isRemote) {
+ yield this.target.makeRemote();
+ }
+
+ this.initialize();
+
+ this.isReady = true;
+ this.emit("ready");
+ deferred.resolve(this);
+
+ return this._opening;
+ }),
+
+ // Initialization
+
+ initialize: function () {
+ this.panelWin.addEventListener("devtools/content/message",
+ this.onContentMessage, true);
+
+ this.target.on("navigate", this.onTabNavigated);
+ this._toolbox.on("select", this.onPanelVisibilityChange);
+
+ let provider = {
+ getPrototypeAndProperties: this.getPrototypeAndProperties.bind(this)
+ };
+
+ exportIntoContentScope(this.panelWin, provider, "DomProvider");
+
+ this.shouldRefresh = true;
+ },
+
+ destroy: Task.async(function* () {
+ if (this._destroying) {
+ return this._destroying;
+ }
+
+ let deferred = promise.defer();
+ this._destroying = deferred.promise;
+
+ this.target.off("navigate", this.onTabNavigated);
+ this._toolbox.off("select", this.onPanelVisibilityChange);
+
+ this.emit("destroyed");
+
+ deferred.resolve();
+ return this._destroying;
+ }),
+
+ // Events
+
+ refresh: function () {
+ // Do not refresh if the panel isn't visible.
+ if (!this.isPanelVisible()) {
+ return;
+ }
+
+ // Do not refresh if it isn't necessary.
+ if (!this.shouldRefresh) {
+ return;
+ }
+
+ // Alright reset the flag we are about to refresh the panel.
+ this.shouldRefresh = false;
+
+ this.getRootGrip().then(rootGrip => {
+ this.postContentMessage("initialize", rootGrip);
+ });
+ },
+
+ /**
+ * Make sure the panel is refreshed when the page is reloaded.
+ * The panel is refreshed immediatelly if it's currently selected
+ * or lazily when the user actually selects it.
+ */
+ onTabNavigated: function () {
+ this.shouldRefresh = true;
+ this.refresh();
+ },
+
+ /**
+ * Make sure the panel is refreshed (if needed) when it's selected.
+ */
+ onPanelVisibilityChange: function () {
+ this.refresh();
+ },
+
+ // Helpers
+
+ /**
+ * Return true if the DOM panel is currently selected.
+ */
+ isPanelVisible: function () {
+ return this._toolbox.currentToolId === "dom";
+ },
+
+ getPrototypeAndProperties: function (grip) {
+ let deferred = defer();
+
+ if (!grip.actor) {
+ console.error("No actor!", grip);
+ deferred.reject(new Error("Failed to get actor from grip."));
+ return deferred.promise;
+ }
+
+ // Bail out if target doesn't exist (toolbox maybe closed already).
+ if (!this.target) {
+ return deferred.promise;
+ }
+
+ // If a request for the grips is already in progress
+ // use the same promise.
+ let request = this.pendingRequests.get(grip.actor);
+ if (request) {
+ return request;
+ }
+
+ let client = new ObjectClient(this.target.client, grip);
+ client.getPrototypeAndProperties(response => {
+ this.pendingRequests.delete(grip.actor, deferred.promise);
+ deferred.resolve(response);
+
+ // Fire an event about not having any pending requests.
+ if (!this.pendingRequests.size) {
+ this.emit("no-pending-requests");
+ }
+ });
+
+ this.pendingRequests.set(grip.actor, deferred.promise);
+
+ return deferred.promise;
+ },
+
+ getRootGrip: function () {
+ let deferred = defer();
+
+ // Attach Console. It might involve RDP communication, so wait
+ // asynchronously for the result
+ this.target.activeConsole.evaluateJSAsync("window", res => {
+ deferred.resolve(res.result);
+ });
+
+ return deferred.promise;
+ },
+
+ postContentMessage: function (type, args) {
+ let data = {
+ type: type,
+ args: args,
+ };
+
+ let event = new this.panelWin.MessageEvent("devtools/chrome/message", {
+ bubbles: true,
+ cancelable: true,
+ data: data,
+ });
+
+ this.panelWin.dispatchEvent(event);
+ },
+
+ onContentMessage: function (event) {
+ let data = event.data;
+ let method = data.type;
+ if (typeof this[method] == "function") {
+ this[method](data.args);
+ }
+ },
+
+ get target() {
+ return this._toolbox.target;
+ },
+};
+
+// Helpers
+
+function exportIntoContentScope(win, obj, defineAs) {
+ let clone = Cu.createObjectIn(win, {
+ defineAs: defineAs
+ });
+
+ let props = Object.getOwnPropertyNames(obj);
+ for (let i = 0; i < props.length; i++) {
+ let propName = props[i];
+ let propValue = obj[propName];
+ if (typeof propValue == "function") {
+ Cu.exportFunction(propValue, clone, {
+ defineAs: propName
+ });
+ }
+ }
+}
+
+// Exports from this module
+exports.DomPanel = DomPanel;
diff --git a/devtools/client/dom/dom.html b/devtools/client/dom/dom.html
new file mode 100644
index 000000000..5fe473d09
--- /dev/null
+++ b/devtools/client/dom/dom.html
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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/. -->
+<!DOCTYPE html>
+<html dir="">
+<head>
+ <meta charset="utf-8"/>
+
+ <link href="resource://devtools/client/dom/content/dom-view.css" rel="stylesheet" />
+ <link href="resource://devtools/client/jsonview/css/toolbar.css" rel="stylesheet" />
+ <link href="resource://devtools/client/shared/components/tree/tree-view.css" rel="stylesheet" />
+
+ <script type="text/javascript;version=1.8"
+ src="chrome://devtools/content/shared/theme-switching.js"></script>
+</head>
+<body class="theme-body devtools-monospace" role="application">
+ <div id="content"></div>
+ <script type="text/javascript" src="./main.js"></script>
+</body>
+</html>
diff --git a/devtools/client/dom/main.js b/devtools/client/dom/main.js
new file mode 100644
index 000000000..085393428
--- /dev/null
+++ b/devtools/client/dom/main.js
@@ -0,0 +1,26 @@
+/* -*- 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 { utils: Cu } = Components;
+
+const { XPCOMUtils } = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {});
+const { BrowserLoader } = Cu.import("resource://devtools/client/shared/browser-loader.js", {});
+
+// Module Loader
+const require = BrowserLoader({
+ baseURI: "resource://devtools/client/dom/",
+ window
+}).require;
+
+XPCOMUtils.defineConstant(this, "require", require);
+
+// Localization
+const { LocalizationHelper } = require("devtools/shared/l10n");
+this.l10n = new LocalizationHelper("devtools/client/locales/dom.properties");
+
+// Load DOM panel content
+require("./content/dom-view.js");
diff --git a/devtools/client/dom/moz.build b/devtools/client/dom/moz.build
new file mode 100644
index 000000000..1e04a09dc
--- /dev/null
+++ b/devtools/client/dom/moz.build
@@ -0,0 +1,14 @@
+# vim: set filetype=python:
+# 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/.
+
+BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
+
+DIRS += [
+ 'content',
+]
+
+DevToolsModules(
+ 'dom-panel.js',
+)
diff --git a/devtools/client/dom/test/.eslintrc.js b/devtools/client/dom/test/.eslintrc.js
new file mode 100644
index 000000000..140985533
--- /dev/null
+++ b/devtools/client/dom/test/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../../.eslintrc.mochitests.js",
+};
diff --git a/devtools/client/dom/test/browser.ini b/devtools/client/dom/test/browser.ini
new file mode 100644
index 000000000..e8e35b32b
--- /dev/null
+++ b/devtools/client/dom/test/browser.ini
@@ -0,0 +1,12 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ head.js
+ page_array.html
+ page_basic.html
+ !/devtools/client/framework/test/shared-head.js
+
+[browser_dom_array.js]
+[browser_dom_basic.js]
+[browser_dom_refresh.js]
diff --git a/devtools/client/dom/test/browser_dom_array.js b/devtools/client/dom/test/browser_dom_array.js
new file mode 100644
index 000000000..2813af320
--- /dev/null
+++ b/devtools/client/dom/test/browser_dom_array.js
@@ -0,0 +1,40 @@
+/* -*- 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 TEST_PAGE_URL = URL_ROOT + "page_array.html";
+const TEST_ARRAY = [
+ "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"
+];
+
+/**
+ * Basic test that checks content of the DOM panel.
+ */
+add_task(function* () {
+ info("Test DOM Panel Array Expansion started");
+
+ let { panel } = yield addTestTab(TEST_PAGE_URL);
+
+ // Expand specified row and wait till children are displayed.
+ yield expandRow(panel, "_a");
+
+ // Verify that children is displayed now.
+ let childRows = getAllRowsForLabel(panel, "_a");
+
+ let item = childRows.pop();
+ is(item.name, "length", "length property is correct");
+ is(item.value, 26, "length property value is 26");
+
+ let i = 0;
+ for (let name in childRows) {
+ let row = childRows[name];
+
+ is(name, i++, `index ${name} is correct and sorted into the correct position`);
+ ok(typeof row.name === "number", "array index is displayed as a number");
+ is(TEST_ARRAY[name], row.value, `value for array[${name}] is ${row.value}`);
+ }
+});
diff --git a/devtools/client/dom/test/browser_dom_basic.js b/devtools/client/dom/test/browser_dom_basic.js
new file mode 100644
index 000000000..2b76fe0fe
--- /dev/null
+++ b/devtools/client/dom/test/browser_dom_basic.js
@@ -0,0 +1,24 @@
+/* -*- 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 TEST_PAGE_URL = URL_ROOT + "page_basic.html";
+
+/**
+ * Basic test that checks content of the DOM panel.
+ */
+add_task(function* () {
+ info("Test DOM panel basic started");
+
+ let { panel } = yield addTestTab(TEST_PAGE_URL);
+
+ // Expand specified row and wait till children are displayed.
+ yield expandRow(panel, "_a");
+
+ // Verify that child is displayed now.
+ let childRow = getRowByLabel(panel, "_data");
+ ok(childRow, "Child row must exist");
+});
diff --git a/devtools/client/dom/test/browser_dom_refresh.js b/devtools/client/dom/test/browser_dom_refresh.js
new file mode 100644
index 000000000..9fdc6aa7f
--- /dev/null
+++ b/devtools/client/dom/test/browser_dom_refresh.js
@@ -0,0 +1,25 @@
+/* -*- 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 TEST_PAGE_URL = URL_ROOT + "page_basic.html";
+
+/**
+ * Basic test that checks the Refresh action in DOM panel.
+ */
+add_task(function* () {
+ info("Test DOM panel basic started");
+
+ let { panel } = yield addTestTab(TEST_PAGE_URL);
+
+ // Create a new variable in the page scope and refresh the panel.
+ yield evaluateJSAsync(panel, "var _b = 10");
+ yield refreshPanel(panel);
+
+ // Verify that the variable is displayed now.
+ let row = getRowByLabel(panel, "_b");
+ ok(row, "New variable must be displayed");
+});
diff --git a/devtools/client/dom/test/head.js b/devtools/client/dom/test/head.js
new file mode 100644
index 000000000..f9382786b
--- /dev/null
+++ b/devtools/client/dom/test/head.js
@@ -0,0 +1,239 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint no-unused-vars: [2, {"vars": "local", "args": "none"}] */
+/* import-globals-from ../../framework/test/shared-head.js */
+
+"use strict";
+
+const FRAME_SCRIPT_UTILS_URL =
+ "chrome://devtools/content/shared/frame-script-utils.js";
+
+// shared-head.js handles imports, constants, and utility functions
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js", this);
+
+// DOM panel actions.
+const constants = require("devtools/client/dom/content/constants");
+
+// Uncomment this pref to dump all devtools emitted events to the console.
+// Services.prefs.setBoolPref("devtools.dom.enabled", true);
+
+// Enable the DOM panel
+Services.prefs.setBoolPref("devtools.dom.enabled", true);
+
+registerCleanupFunction(() => {
+ info("finish() was called, cleaning up...");
+ Services.prefs.clearUserPref("devtools.dump.emit");
+ Services.prefs.clearUserPref("devtools.dom.enabled");
+});
+
+/**
+ * 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 tab object when
+ * the url is loaded
+ */
+function addTestTab(url) {
+ info("Adding a new test tab with URL: '" + url + "'");
+
+ return new Promise(resolve => {
+ addTab(url).then(tab => {
+ // Load devtools/shared/frame-script-utils.js
+ getFrameScript();
+
+ // Select the DOM panel and wait till it's initialized.
+ initDOMPanel(tab).then(panel => {
+ waitForDispatch(panel, "FETCH_PROPERTIES").then(() => {
+ resolve({
+ tab: tab,
+ browser: tab.linkedBrowser,
+ panel: panel
+ });
+ });
+ });
+ });
+ });
+}
+
+/**
+ * Open the DOM panel for the given tab.
+ *
+ * @param {nsIDOMElement} tab
+ * Optional tab element for which you want open the DOM panel.
+ * The default tab is taken from the global variable |tab|.
+ * @return a promise that is resolved once the web console is open.
+ */
+function initDOMPanel(tab) {
+ return new Promise(resolve => {
+ let target = TargetFactory.forTab(tab || gBrowser.selectedTab);
+ gDevTools.showToolbox(target, "dom").then(toolbox => {
+ let panel = toolbox.getCurrentPanel();
+ resolve(panel);
+ });
+ });
+}
+
+/**
+ * Synthesize asynchronous click event (with clean stack trace).
+ */
+function synthesizeMouseClickSoon(panel, element) {
+ return new Promise(resolve => {
+ executeSoon(() => {
+ EventUtils.synthesizeMouse(element, 2, 2, {}, panel.panelWin);
+ resolve();
+ });
+ });
+}
+
+/**
+ * Returns tree row with specified label.
+ */
+function getRowByLabel(panel, text) {
+ let doc = panel.panelWin.document;
+ let labels = [...doc.querySelectorAll(".treeLabel")];
+ let label = labels.find(node => node.textContent == text);
+ return label ? label.closest(".treeRow") : null;
+}
+
+/**
+ * Returns the children (tree row text) of the specified object name as an
+ * array.
+ */
+function getAllRowsForLabel(panel, text) {
+ let rootObjectLevel;
+ let node;
+ let result = [];
+ let doc = panel.panelWin.document;
+ let nodes = [...doc.querySelectorAll(".treeLabel")];
+
+ // Find the label (object name) for which we want the children. We remove
+ // nodes from the start of the array until we reach the property. The children
+ // are then at the start of the array.
+ while (true) {
+ node = nodes.shift();
+
+ if (!node || node.textContent === text) {
+ rootObjectLevel = node.getAttribute("data-level");
+ break;
+ }
+ }
+
+ // Return an empty array if the node is not found.
+ if (!node) {
+ return result;
+ }
+
+ // Now get the children.
+ for (node of nodes) {
+ let level = node.getAttribute("data-level");
+
+ if (level > rootObjectLevel) {
+ result.push({
+ name: normalizeTreeValue(node.textContent),
+ value: normalizeTreeValue(node.parentNode.nextElementSibling.textContent)
+ });
+ } else {
+ break;
+ }
+ }
+
+ return result;
+}
+
+/**
+ * Strings in the tree are in the form ""a"" and numbers in the form "1". We
+ * normalize these values by converting ""a"" to "a" and "1" to 1.
+ *
+ * @param {String} value
+ * The value to normalize.
+ * @return {String|Number}
+ * The normalized value.
+ */
+function normalizeTreeValue(value) {
+ if (value === `""`) {
+ return "";
+ }
+ if (value.startsWith(`"`) && value.endsWith(`"`)) {
+ return value.substr(1, value.length - 2);
+ }
+ if (isFinite(value) && parseInt(value, 10) == value) {
+ return parseInt(value, 10);
+ }
+
+ return value;
+}
+
+/**
+ * Expands elements with given label and waits till
+ * children are received from the backend.
+ */
+function expandRow(panel, labelText) {
+ let row = getRowByLabel(panel, labelText);
+ return synthesizeMouseClickSoon(panel, row).then(() => {
+ // Wait till children (properties) are fetched
+ // from the backend.
+ return waitForDispatch(panel, "FETCH_PROPERTIES");
+ });
+}
+
+function evaluateJSAsync(panel, expression) {
+ return new Promise(resolve => {
+ panel.target.activeConsole.evaluateJSAsync(expression, res => {
+ resolve(res);
+ });
+ });
+}
+
+function refreshPanel(panel) {
+ let doc = panel.panelWin.document;
+ let button = doc.querySelector(".btn.refresh");
+ return synthesizeMouseClickSoon(panel, button).then(() => {
+ // Wait till children (properties) are fetched
+ // from the backend.
+ return waitForDispatch(panel, "FETCH_PROPERTIES");
+ });
+}
+
+// Redux related API, use from shared location
+// as soon as bug 1261076 is fixed.
+
+// Wait until an action of `type` is dispatched. If it's part of an
+// async operation, wait until the `status` field is "done" or "error"
+function _afterDispatchDone(store, type) {
+ return new Promise(resolve => {
+ store.dispatch({
+ // Normally we would use `services.WAIT_UNTIL`, but use the
+ // internal name here so tests aren't forced to always pass it
+ // in
+ type: "@@service/waitUntil",
+ predicate: action => {
+ if (action.type === type) {
+ return action.status ?
+ (action.status === "end" || action.status === "error") :
+ true;
+ }
+ return false;
+ },
+ run: (dispatch, getState, action) => {
+ resolve(action);
+ }
+ });
+ });
+}
+
+function waitForDispatch(panel, type, eventRepeat = 1) {
+ const store = panel.panelWin.view.mainFrame.store;
+ const actionType = constants[type];
+ let count = 0;
+
+ return Task.spawn(function* () {
+ info("Waiting for " + type + " to dispatch " + eventRepeat + " time(s)");
+ while (count < eventRepeat) {
+ yield _afterDispatchDone(store, actionType);
+ count++;
+ info(type + " dispatched " + count + " time(s)");
+ }
+ });
+}
diff --git a/devtools/client/dom/test/page_array.html b/devtools/client/dom/test/page_array.html
new file mode 100644
index 000000000..703b93a85
--- /dev/null
+++ b/devtools/client/dom/test/page_array.html
@@ -0,0 +1,19 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>DOM Panel Array Expansion Test Page</title>
+ </head>
+ <body>
+ <h2>DOM Panel Array Expansion Test Page</h2>
+ <script type="text/javascript">
+ "use strict";
+ window._a = [
+ "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"
+ ];
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/dom/test/page_basic.html b/devtools/client/dom/test/page_basic.html
new file mode 100644
index 000000000..170b3112a
--- /dev/null
+++ b/devtools/client/dom/test/page_basic.html
@@ -0,0 +1,15 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>DOM test page</title>
+ </head>
+ <body>
+ <script type="text/javascript">
+ "use strict";
+ window._a = {_data: "test"};
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/framework/ToolboxProcess.jsm b/devtools/client/framework/ToolboxProcess.jsm
new file mode 100644
index 000000000..cd12e92cd
--- /dev/null
+++ b/devtools/client/framework/ToolboxProcess.jsm
@@ -0,0 +1,291 @@
+/* -*- 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 { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+const DBG_XUL = "chrome://devtools/content/framework/toolbox-process-window.xul";
+const CHROME_DEBUGGER_PROFILE_NAME = "chrome_debugger_profile";
+
+const { require, DevToolsLoader } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "Telemetry", function () {
+ return require("devtools/client/shared/telemetry");
+});
+XPCOMUtils.defineLazyGetter(this, "EventEmitter", function () {
+ return require("devtools/shared/event-emitter");
+});
+const promise = require("promise");
+const Services = require("Services");
+
+this.EXPORTED_SYMBOLS = ["BrowserToolboxProcess"];
+
+var processes = new Set();
+
+/**
+ * Constructor for creating a process that will hold a chrome toolbox.
+ *
+ * @param function aOnClose [optional]
+ * A function called when the process stops running.
+ * @param function aOnRun [optional]
+ * A function called when the process starts running.
+ * @param object aOptions [optional]
+ * An object with properties for configuring BrowserToolboxProcess.
+ */
+this.BrowserToolboxProcess = function BrowserToolboxProcess(aOnClose, aOnRun, aOptions) {
+ let emitter = new EventEmitter();
+ this.on = emitter.on.bind(emitter);
+ this.off = emitter.off.bind(emitter);
+ this.once = emitter.once.bind(emitter);
+ // Forward any events to the shared emitter.
+ this.emit = function (...args) {
+ emitter.emit(...args);
+ BrowserToolboxProcess.emit(...args);
+ };
+
+ // If first argument is an object, use those properties instead of
+ // all three arguments
+ if (typeof aOnClose === "object") {
+ if (aOnClose.onClose) {
+ this.once("close", aOnClose.onClose);
+ }
+ if (aOnClose.onRun) {
+ this.once("run", aOnClose.onRun);
+ }
+ this._options = aOnClose;
+ } else {
+ if (aOnClose) {
+ this.once("close", aOnClose);
+ }
+ if (aOnRun) {
+ this.once("run", aOnRun);
+ }
+ this._options = aOptions || {};
+ }
+
+ this._telemetry = new Telemetry();
+
+ this.close = this.close.bind(this);
+ Services.obs.addObserver(this.close, "quit-application", false);
+ this._initServer();
+ this._initProfile();
+ this._create();
+
+ processes.add(this);
+};
+
+EventEmitter.decorate(BrowserToolboxProcess);
+
+/**
+ * Initializes and starts a chrome toolbox process.
+ * @return object
+ */
+BrowserToolboxProcess.init = function (aOnClose, aOnRun, aOptions) {
+ return new BrowserToolboxProcess(aOnClose, aOnRun, aOptions);
+};
+
+/**
+ * Passes a set of options to the BrowserAddonActors for the given ID.
+ *
+ * @param aId string
+ * The ID of the add-on to pass the options to
+ * @param aOptions object
+ * The options.
+ * @return a promise that will be resolved when complete.
+ */
+BrowserToolboxProcess.setAddonOptions = function DSC_setAddonOptions(aId, aOptions) {
+ let promises = [];
+
+ for (let process of processes.values()) {
+ promises.push(process.debuggerServer.setAddonOptions(aId, aOptions));
+ }
+
+ return promise.all(promises);
+};
+
+BrowserToolboxProcess.prototype = {
+ /**
+ * Initializes the debugger server.
+ */
+ _initServer: function () {
+ if (this.debuggerServer) {
+ dumpn("The chrome toolbox server is already running.");
+ return;
+ }
+
+ dumpn("Initializing the chrome toolbox server.");
+
+ // Create a separate loader instance, so that we can be sure to receive a
+ // separate instance of the DebuggingServer from the rest of the devtools.
+ // This allows us to safely use the tools against even the actors and
+ // DebuggingServer itself, especially since we can mark this loader as
+ // invisible to the debugger (unlike the usual loader settings).
+ this.loader = new DevToolsLoader();
+ this.loader.invisibleToDebugger = true;
+ let { DebuggerServer } = this.loader.require("devtools/server/main");
+ this.debuggerServer = DebuggerServer;
+ dumpn("Created a separate loader instance for the DebuggerServer.");
+
+ // Forward interesting events.
+ this.debuggerServer.on("connectionchange", this.emit);
+
+ this.debuggerServer.init();
+ this.debuggerServer.addBrowserActors();
+ this.debuggerServer.allowChromeProcess = true;
+ dumpn("initialized and added the browser actors for the DebuggerServer.");
+
+ let chromeDebuggingPort =
+ Services.prefs.getIntPref("devtools.debugger.chrome-debugging-port");
+ let chromeDebuggingWebSocket =
+ Services.prefs.getBoolPref("devtools.debugger.chrome-debugging-websocket");
+ let listener = this.debuggerServer.createListener();
+ listener.portOrPath = chromeDebuggingPort;
+ listener.webSocket = chromeDebuggingWebSocket;
+ listener.open();
+
+ dumpn("Finished initializing the chrome toolbox server.");
+ dumpn("Started listening on port: " + chromeDebuggingPort);
+ },
+
+ /**
+ * Initializes a profile for the remote debugger process.
+ */
+ _initProfile: function () {
+ dumpn("Initializing the chrome toolbox user profile.");
+
+ let debuggingProfileDir = Services.dirsvc.get("ProfLD", Ci.nsIFile);
+ debuggingProfileDir.append(CHROME_DEBUGGER_PROFILE_NAME);
+ try {
+ debuggingProfileDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+ } catch (ex) {
+ // Don't re-copy over the prefs again if this profile already exists
+ if (ex.result === Cr.NS_ERROR_FILE_ALREADY_EXISTS) {
+ this._dbgProfilePath = debuggingProfileDir.path;
+ } else {
+ dumpn("Error trying to create a profile directory, failing.");
+ dumpn("Error: " + (ex.message || ex));
+ }
+ return;
+ }
+
+ this._dbgProfilePath = debuggingProfileDir.path;
+
+ // We would like to copy prefs into this new profile...
+ let prefsFile = debuggingProfileDir.clone();
+ prefsFile.append("prefs.js");
+ // ... but unfortunately, when we run tests, it seems the starting profile
+ // clears out the prefs file before re-writing it, and in practice the
+ // file is empty when we get here. So just copying doesn't work in that
+ // case.
+ // We could force a sync pref flush and then copy it... but if we're doing
+ // that, we might as well just flush directly to the new profile, which
+ // always works:
+ Services.prefs.savePrefFile(prefsFile);
+
+ dumpn("Finished creating the chrome toolbox user profile at: " + this._dbgProfilePath);
+ },
+
+ /**
+ * Creates and initializes the profile & process for the remote debugger.
+ */
+ _create: function () {
+ dumpn("Initializing chrome debugging process.");
+ let process = this._dbgProcess = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess);
+ process.init(Services.dirsvc.get("XREExeF", Ci.nsIFile));
+
+ let xulURI = DBG_XUL;
+
+ if (this._options.addonID) {
+ xulURI += "?addonID=" + this._options.addonID;
+ }
+
+ dumpn("Running chrome debugging process.");
+ let args = ["-no-remote", "-foreground", "-profile", this._dbgProfilePath, "-chrome", xulURI];
+
+ // During local development, incremental builds can trigger the main process
+ // to clear its startup cache with the "flag file" .purgecaches, but this
+ // file is removed during app startup time, so we aren't able to know if it
+ // was present in order to also clear the child profile's startup cache as
+ // well.
+ //
+ // As an approximation of "isLocalBuild", check for an unofficial build.
+ if (!Services.appinfo.isOfficial) {
+ args.push("-purgecaches");
+ }
+
+ // Disable safe mode for the new process in case this was opened via the
+ // keyboard shortcut.
+ let nsIEnvironment = Components.classes["@mozilla.org/process/environment;1"].getService(Components.interfaces.nsIEnvironment);
+ let originalValue = nsIEnvironment.get("MOZ_DISABLE_SAFE_MODE_KEY");
+ nsIEnvironment.set("MOZ_DISABLE_SAFE_MODE_KEY", "1");
+
+ process.runwAsync(args, args.length, { observe: () => this.close() });
+
+ // Now that the process has started, it's safe to reset the env variable.
+ nsIEnvironment.set("MOZ_DISABLE_SAFE_MODE_KEY", originalValue);
+
+ this._telemetry.toolOpened("jsbrowserdebugger");
+
+ dumpn("Chrome toolbox is now running...");
+ this.emit("run", this);
+ },
+
+ /**
+ * Closes the remote debugging server and kills the toolbox process.
+ */
+ close: function () {
+ if (this.closed) {
+ return;
+ }
+
+ dumpn("Cleaning up the chrome debugging process.");
+ Services.obs.removeObserver(this.close, "quit-application");
+
+ if (this._dbgProcess.isRunning) {
+ this._dbgProcess.kill();
+ }
+
+ this._telemetry.toolClosed("jsbrowserdebugger");
+ if (this.debuggerServer) {
+ this.debuggerServer.off("connectionchange", this.emit);
+ this.debuggerServer.destroy();
+ this.debuggerServer = null;
+ }
+
+ dumpn("Chrome toolbox is now closed...");
+ this.closed = true;
+ this.emit("close", this);
+ processes.delete(this);
+
+ this._dbgProcess = null;
+ this._options = null;
+ if (this.loader) {
+ this.loader.destroy();
+ }
+ this.loader = null;
+ this._telemetry = null;
+ }
+};
+
+/**
+ * Helper method for debugging.
+ * @param string
+ */
+function dumpn(str) {
+ if (wantLogging) {
+ dump("DBG-FRONTEND: " + str + "\n");
+ }
+}
+
+var wantLogging = Services.prefs.getBoolPref("devtools.debugger.log");
+
+Services.prefs.addObserver("devtools.debugger.log", {
+ observe: (...args) => wantLogging = Services.prefs.getBoolPref(args.pop())
+}, false);
+
+Services.obs.notifyObservers(null, "ToolboxProcessLoaded", null);
diff --git a/devtools/client/framework/about-devtools-toolbox.js b/devtools/client/framework/about-devtools-toolbox.js
new file mode 100644
index 000000000..0ae776e37
--- /dev/null
+++ b/devtools/client/framework/about-devtools-toolbox.js
@@ -0,0 +1,61 @@
+/* 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";
+
+// Register about:devtools-toolbox which allows to open a devtools toolbox
+// in a Firefox tab or a custom html iframe in browser.html
+
+const { Ci, Cu, Cm, components } = require("chrome");
+const registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
+const Services = require("Services");
+const { XPCOMUtils } = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {});
+const { nsIAboutModule } = Ci;
+
+function AboutURL() {}
+
+AboutURL.prototype = {
+ uri: Services.io.newURI("chrome://devtools/content/framework/toolbox.xul",
+ null, null),
+ classDescription: "about:devtools-toolbox",
+ classID: components.ID("11342911-3135-45a8-8d71-737a2b0ad469"),
+ contractID: "@mozilla.org/network/protocol/about;1?what=devtools-toolbox",
+
+ QueryInterface: XPCOMUtils.generateQI([nsIAboutModule]),
+
+ newChannel: function (aURI, aLoadInfo) {
+ let chan = Services.io.newChannelFromURIWithLoadInfo(this.uri, aLoadInfo);
+ chan.owner = Services.scriptSecurityManager.getSystemPrincipal();
+ return chan;
+ },
+
+ getURIFlags: function (aURI) {
+ return nsIAboutModule.ALLOW_SCRIPT || nsIAboutModule.ENABLE_INDEXED_DB;
+ }
+};
+
+AboutURL.createInstance = function (outer, iid) {
+ if (outer) {
+ throw Cr.NS_ERROR_NO_AGGREGATION;
+ }
+ return new AboutURL();
+};
+
+exports.register = function () {
+ if (registrar.isCIDRegistered(AboutURL.prototype.classID)) {
+ console.error("Trying to register " + AboutURL.prototype.classDescription +
+ " more than once.");
+ } else {
+ registrar.registerFactory(AboutURL.prototype.classID,
+ AboutURL.prototype.classDescription,
+ AboutURL.prototype.contractID,
+ AboutURL);
+ }
+};
+
+exports.unregister = function () {
+ if (registrar.isCIDRegistered(AboutURL.prototype.classID)) {
+ registrar.unregisterFactory(AboutURL.prototype.classID, AboutURL);
+ }
+};
diff --git a/devtools/client/framework/attach-thread.js b/devtools/client/framework/attach-thread.js
new file mode 100644
index 000000000..db445ce23
--- /dev/null
+++ b/devtools/client/framework/attach-thread.js
@@ -0,0 +1,115 @@
+/* -*- 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/. */
+
+const {Cc, Ci, Cu} = require("chrome");
+const Services = require("Services");
+const defer = require("devtools/shared/defer");
+
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper("devtools/client/locales/toolbox.properties");
+
+function handleThreadState(toolbox, event, packet) {
+ // Suppress interrupted events by default because the thread is
+ // paused/resumed a lot for various actions.
+ if (event !== "paused" || packet.why.type !== "interrupted") {
+ // TODO: Bug 1225492, we continue emitting events on the target
+ // like we used to, but we should emit these only on the
+ // threadClient now.
+ toolbox.target.emit("thread-" + event);
+ }
+
+ if (event === "paused") {
+ toolbox.highlightTool("jsdebugger");
+
+ if (packet.why.type === "debuggerStatement" ||
+ packet.why.type === "breakpoint" ||
+ packet.why.type === "exception") {
+ toolbox.raise();
+ toolbox.selectTool("jsdebugger");
+ }
+ } else if (event === "resumed") {
+ toolbox.unhighlightTool("jsdebugger");
+ }
+}
+
+function attachThread(toolbox) {
+ let deferred = defer();
+
+ let target = toolbox.target;
+ let { form: { chromeDebugger, actor } } = target;
+
+ // Sourcemaps are always turned off when using the new debugger
+ // frontend. This is because it does sourcemapping on the
+ // client-side, so the server should not do it. It also does not support
+ // blackboxing yet.
+ let useSourceMaps = false;
+ let autoBlackBox = false;
+ if(!Services.prefs.getBoolPref("devtools.debugger.new-debugger-frontend")) {
+ useSourceMaps = Services.prefs.getBoolPref("devtools.debugger.source-maps-enabled");
+ autoBlackBox = Services.prefs.getBoolPref("devtools.debugger.auto-black-box");
+ }
+ let threadOptions = { useSourceMaps, autoBlackBox };
+
+ let handleResponse = (res, threadClient) => {
+ if (res.error) {
+ deferred.reject(new Error("Couldn't attach to thread: " + res.error));
+ return;
+ }
+ threadClient.addListener("paused", handleThreadState.bind(null, toolbox));
+ threadClient.addListener("resumed", handleThreadState.bind(null, toolbox));
+
+ if (!threadClient.paused) {
+ deferred.reject(
+ new Error("Thread in wrong state when starting up, should be paused")
+ );
+ }
+
+ // These flags need to be set here because the client sends them
+ // with the `resume` request. We make sure to do this before
+ // resuming to avoid another interrupt. We can't pass it in with
+ // `threadOptions` because the resume request will override them.
+ threadClient.pauseOnExceptions(
+ Services.prefs.getBoolPref("devtools.debugger.pause-on-exceptions"),
+ Services.prefs.getBoolPref("devtools.debugger.ignore-caught-exceptions")
+ );
+
+ threadClient.resume(res => {
+ if (res.error === "wrongOrder") {
+ const box = toolbox.getNotificationBox();
+ box.appendNotification(
+ L10N.getStr("toolbox.resumeOrderWarning"),
+ "wrong-resume-order",
+ "",
+ box.PRIORITY_WARNING_HIGH
+ );
+ }
+
+ deferred.resolve(threadClient);
+ });
+ };
+
+ if (target.isTabActor) {
+ // Attaching a tab, a browser process, or a WebExtensions add-on.
+ target.activeTab.attachThread(threadOptions, handleResponse);
+ } else if (target.isAddon) {
+ // Attaching a legacy addon.
+ target.client.attachAddon(actor, res => {
+ target.client.attachThread(res.threadActor, handleResponse);
+ });
+ } else {
+ // Attaching an old browser debugger or a content process.
+ target.client.attachThread(chromeDebugger, handleResponse);
+ }
+
+ return deferred.promise;
+}
+
+function detachThread(threadClient) {
+ threadClient.removeListener("paused");
+ threadClient.removeListener("resumed");
+}
+
+module.exports = { attachThread, detachThread };
diff --git a/devtools/client/framework/browser-menus.js b/devtools/client/framework/browser-menus.js
new file mode 100644
index 000000000..3d6c4def6
--- /dev/null
+++ b/devtools/client/framework/browser-menus.js
@@ -0,0 +1,390 @@
+/* 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";
+
+/**
+ * This module inject dynamically menu items and key shortcuts into browser UI.
+ *
+ * Menu and shortcut definitions are fetched from:
+ * - devtools/client/menus for top level entires
+ * - devtools/client/definitions for tool-specifics entries
+ */
+
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const MENUS_L10N = new LocalizationHelper("devtools/client/locales/menus.properties");
+
+loader.lazyRequireGetter(this, "gDevTools", "devtools/client/framework/devtools", true);
+loader.lazyRequireGetter(this, "gDevToolsBrowser", "devtools/client/framework/devtools-browser", true);
+
+// Keep list of inserted DOM Elements in order to remove them on unload
+// Maps browser xul document => list of DOM Elements
+const FragmentsCache = new Map();
+
+function l10n(key) {
+ return MENUS_L10N.getStr(key);
+}
+
+/**
+ * Create a xul:key element
+ *
+ * @param {XULDocument} doc
+ * The document to which keys are to be added.
+ * @param {String} id
+ * key's id, automatically prefixed with "key_".
+ * @param {String} shortcut
+ * The key shortcut value.
+ * @param {String} keytext
+ * If `shortcut` refers to a function key, refers to the localized
+ * string to describe a non-character shortcut.
+ * @param {String} modifiers
+ * Space separated list of modifier names.
+ * @param {Function} oncommand
+ * The function to call when the shortcut is pressed.
+ *
+ * @return XULKeyElement
+ */
+function createKey({ doc, id, shortcut, keytext, modifiers, oncommand }) {
+ let k = doc.createElement("key");
+ k.id = "key_" + id;
+
+ if (shortcut.startsWith("VK_")) {
+ k.setAttribute("keycode", shortcut);
+ if (keytext) {
+ k.setAttribute("keytext", keytext);
+ }
+ } else {
+ k.setAttribute("key", shortcut);
+ }
+
+ if (modifiers) {
+ k.setAttribute("modifiers", modifiers);
+ }
+
+ // Bug 371900: command event is fired only if "oncommand" attribute is set.
+ k.setAttribute("oncommand", ";");
+ k.addEventListener("command", oncommand);
+
+ return k;
+}
+
+/**
+ * Create a xul:menuitem element
+ *
+ * @param {XULDocument} doc
+ * The document to which keys are to be added.
+ * @param {String} id
+ * Element id.
+ * @param {String} label
+ * Menu label.
+ * @param {String} accesskey (optional)
+ * Access key of the menuitem, used as shortcut while opening the menu.
+ * @param {Boolean} isCheckbox (optional)
+ * If true, the menuitem will act as a checkbox and have an optional
+ * tick on its left.
+ *
+ * @return XULMenuItemElement
+ */
+function createMenuItem({ doc, id, label, accesskey, isCheckbox }) {
+ let menuitem = doc.createElement("menuitem");
+ menuitem.id = id;
+ menuitem.setAttribute("label", label);
+ if (accesskey) {
+ menuitem.setAttribute("accesskey", accesskey);
+ }
+ if (isCheckbox) {
+ menuitem.setAttribute("type", "checkbox");
+ menuitem.setAttribute("autocheck", "false");
+ }
+ return menuitem;
+}
+
+/**
+ * Add a <key> to <keyset id="devtoolsKeyset">.
+ * Appending a <key> element is not always enough. The <keyset> needs
+ * to be detached and reattached to make sure the <key> is taken into
+ * account (see bug 832984).
+ *
+ * @param {XULDocument} doc
+ * The document to which keys are to be added
+ * @param {XULElement} or {DocumentFragment} keys
+ * Keys to add
+ */
+function attachKeybindingsToBrowser(doc, keys) {
+ let devtoolsKeyset = doc.getElementById("devtoolsKeyset");
+
+ if (!devtoolsKeyset) {
+ devtoolsKeyset = doc.createElement("keyset");
+ devtoolsKeyset.setAttribute("id", "devtoolsKeyset");
+ }
+ devtoolsKeyset.appendChild(keys);
+ let mainKeyset = doc.getElementById("mainKeyset");
+ mainKeyset.parentNode.insertBefore(devtoolsKeyset, mainKeyset);
+}
+
+/**
+ * Add a menu entry for a tool definition
+ *
+ * @param {Object} toolDefinition
+ * Tool definition of the tool to add a menu entry.
+ * @param {XULDocument} doc
+ * The document to which the tool menu item is to be added.
+ */
+function createToolMenuElements(toolDefinition, doc) {
+ let id = toolDefinition.id;
+ let menuId = "menuitem_" + id;
+
+ // Prevent multiple entries for the same tool.
+ if (doc.getElementById(menuId)) {
+ return;
+ }
+
+ let oncommand = function (id, event) {
+ let window = event.target.ownerDocument.defaultView;
+ gDevToolsBrowser.selectToolCommand(window.gBrowser, id);
+ }.bind(null, id);
+
+ let key = null;
+ if (toolDefinition.key) {
+ key = createKey({
+ doc,
+ id,
+ shortcut: toolDefinition.key,
+ modifiers: toolDefinition.modifiers,
+ oncommand: oncommand
+ });
+ }
+
+ let menuitem = createMenuItem({
+ doc,
+ id: "menuitem_" + id,
+ label: toolDefinition.menuLabel || toolDefinition.label,
+ accesskey: toolDefinition.accesskey
+ });
+ if (key) {
+ // Refer to the key in order to display the key shortcut at menu ends
+ menuitem.setAttribute("key", key.id);
+ }
+ menuitem.addEventListener("command", oncommand);
+
+ return {
+ key,
+ menuitem
+ };
+}
+
+/**
+ * Create xul menuitem, key elements for a given tool.
+ * And then insert them into browser DOM.
+ *
+ * @param {XULDocument} doc
+ * The document to which the tool is to be registered.
+ * @param {Object} toolDefinition
+ * Tool definition of the tool to register.
+ * @param {Object} prevDef
+ * The tool definition after which the tool menu item is to be added.
+ */
+function insertToolMenuElements(doc, toolDefinition, prevDef) {
+ let { key, menuitem } = createToolMenuElements(toolDefinition, doc);
+
+ if (key) {
+ attachKeybindingsToBrowser(doc, key);
+ }
+
+ let ref;
+ if (prevDef) {
+ let menuitem = doc.getElementById("menuitem_" + prevDef.id);
+ ref = menuitem && menuitem.nextSibling ? menuitem.nextSibling : null;
+ } else {
+ ref = doc.getElementById("menu_devtools_separator");
+ }
+
+ if (ref) {
+ ref.parentNode.insertBefore(menuitem, ref);
+ }
+}
+exports.insertToolMenuElements = insertToolMenuElements;
+
+/**
+ * Remove a tool's menuitem from a window
+ *
+ * @param {string} toolId
+ * Id of the tool to add a menu entry for
+ * @param {XULDocument} doc
+ * The document to which the tool menu item is to be removed from
+ */
+function removeToolFromMenu(toolId, doc) {
+ let key = doc.getElementById("key_" + toolId);
+ if (key) {
+ key.remove();
+ }
+
+ let menuitem = doc.getElementById("menuitem_" + toolId);
+ if (menuitem) {
+ menuitem.remove();
+ }
+}
+exports.removeToolFromMenu = removeToolFromMenu;
+
+/**
+ * Add all tools to the developer tools menu of a window.
+ *
+ * @param {XULDocument} doc
+ * The document to which the tool items are to be added.
+ */
+function addAllToolsToMenu(doc) {
+ let fragKeys = doc.createDocumentFragment();
+ let fragMenuItems = doc.createDocumentFragment();
+
+ for (let toolDefinition of gDevTools.getToolDefinitionArray()) {
+ if (!toolDefinition.inMenu) {
+ continue;
+ }
+
+ let elements = createToolMenuElements(toolDefinition, doc);
+
+ if (!elements) {
+ continue;
+ }
+
+ if (elements.key) {
+ fragKeys.appendChild(elements.key);
+ }
+ fragMenuItems.appendChild(elements.menuitem);
+ }
+
+ attachKeybindingsToBrowser(doc, fragKeys);
+
+ let mps = doc.getElementById("menu_devtools_separator");
+ if (mps) {
+ mps.parentNode.insertBefore(fragMenuItems, mps);
+ }
+}
+
+/**
+ * Add global menus and shortcuts that are not panel specific.
+ *
+ * @param {XULDocument} doc
+ * The document to which keys and menus are to be added.
+ */
+function addTopLevelItems(doc) {
+ let keys = doc.createDocumentFragment();
+ let menuItems = doc.createDocumentFragment();
+
+ let { menuitems } = require("../menus");
+ for (let item of menuitems) {
+ if (item.separator) {
+ let separator = doc.createElement("menuseparator");
+ separator.id = item.id;
+ menuItems.appendChild(separator);
+ } else {
+ let { id, l10nKey } = item;
+
+ // Create a <menuitem>
+ let menuitem = createMenuItem({
+ doc,
+ id,
+ label: l10n(l10nKey + ".label"),
+ accesskey: l10n(l10nKey + ".accesskey"),
+ isCheckbox: item.checkbox
+ });
+ menuitem.addEventListener("command", item.oncommand);
+ menuItems.appendChild(menuitem);
+
+ if (item.key && l10nKey) {
+ // Create a <key>
+ let shortcut = l10n(l10nKey + ".key");
+ let key = createKey({
+ doc,
+ id: item.key.id,
+ shortcut: shortcut,
+ keytext: shortcut.startsWith("VK_") ? l10n(l10nKey + ".keytext") : null,
+ modifiers: item.key.modifiers,
+ oncommand: item.oncommand
+ });
+ // Refer to the key in order to display the key shortcut at menu ends
+ menuitem.setAttribute("key", key.id);
+ keys.appendChild(key);
+ }
+ if (item.additionalKeys) {
+ // Create additional <key>
+ for (let key of item.additionalKeys) {
+ let shortcut = l10n(key.l10nKey + ".key");
+ let node = createKey({
+ doc,
+ id: key.id,
+ shortcut: shortcut,
+ keytext: shortcut.startsWith("VK_") ? l10n(key.l10nKey + ".keytext") : null,
+ modifiers: key.modifiers,
+ oncommand: item.oncommand
+ });
+ keys.appendChild(node);
+ }
+ }
+ }
+ }
+
+ // Cache all nodes before insertion to be able to remove them on unload
+ let nodes = [];
+ for (let node of keys.children) {
+ nodes.push(node);
+ }
+ for (let node of menuItems.children) {
+ nodes.push(node);
+ }
+ FragmentsCache.set(doc, nodes);
+
+ attachKeybindingsToBrowser(doc, keys);
+
+ let menu = doc.getElementById("menuWebDeveloperPopup");
+ menu.appendChild(menuItems);
+
+ // There is still "Page Source" menuitem hardcoded into browser.xul. Instead
+ // of manually inserting everything around it, move it to the expected
+ // position.
+ let pageSource = doc.getElementById("menu_pageSource");
+ let endSeparator = doc.getElementById("devToolsEndSeparator");
+ menu.insertBefore(pageSource, endSeparator);
+}
+
+/**
+ * Remove global menus and shortcuts that are not panel specific.
+ *
+ * @param {XULDocument} doc
+ * The document to which keys and menus are to be added.
+ */
+function removeTopLevelItems(doc) {
+ let nodes = FragmentsCache.get(doc);
+ if (!nodes) {
+ return;
+ }
+ FragmentsCache.delete(doc);
+ for (let node of nodes) {
+ node.remove();
+ }
+}
+
+/**
+ * Add menus and shortcuts to a browser document
+ *
+ * @param {XULDocument} doc
+ * The document to which keys and menus are to be added.
+ */
+exports.addMenus = function (doc) {
+ addTopLevelItems(doc);
+
+ addAllToolsToMenu(doc);
+};
+
+/**
+ * Remove menus and shortcuts from a browser document
+ *
+ * @param {XULDocument} doc
+ * The document to which keys and menus are to be removed.
+ */
+exports.removeMenus = function (doc) {
+ // We only remove top level entries. Per-tool entries are removed while
+ // unregistering each tool.
+ removeTopLevelItems(doc);
+};
diff --git a/devtools/client/framework/connect/connect.css b/devtools/client/framework/connect/connect.css
new file mode 100644
index 000000000..23959b93b
--- /dev/null
+++ b/devtools/client/framework/connect/connect.css
@@ -0,0 +1,112 @@
+:root {
+ font: caption;
+}
+
+html {
+ background-color: #111;
+}
+
+body {
+ font-family: Arial, sans-serif;
+ color: white;
+ max-width: 600px;
+ margin: 30px auto 0;
+ box-shadow: 0 2px 3px black;
+ background-color: #3C3E40;
+}
+
+h1 {
+ margin: 0;
+ padding: 20px;
+ background-color: rgba(0,0,0,0.12);
+ background-image: radial-gradient(ellipse farthest-corner at center top , rgb(159, 223, 255), rgba(101, 203, 255, 0.3)), radial-gradient(ellipse farthest-side at center top , rgba(101, 203, 255, 0.4), rgba(101, 203, 255, 0));
+ background-size: 100% 2px, 100% 5px;
+ background-repeat: no-repeat;
+ border-bottom: 1px solid rgba(0,0,0,0.1);
+}
+
+form {
+ display: inline-block;
+}
+
+label {
+ display: block;
+ margin: 10px;
+}
+
+label > span {
+ display: inline-block;
+ min-width: 150px;
+ text-align: right;
+ margin-right: 10px;
+}
+
+#submit {
+ float: right;
+}
+
+input:invalid {
+ box-shadow: 0 0 2px 2px #F06;
+}
+
+section {
+ min-height: 160px;
+ margin: 60px 20px;
+ display: none; /* By default, hidden */
+}
+
+.error-message {
+ color: red;
+}
+
+.error-message:not(.active) {
+ display: none;
+}
+
+body:not(.actors-mode):not(.connecting) > #connection-form {
+ display: block;
+}
+
+body.actors-mode > #actors-list {
+ display: block;
+}
+
+body.connecting > #connecting {
+ display: block;
+}
+
+#connecting {
+ text-align: center;
+}
+
+#connecting > p > img {
+ vertical-align: top;
+}
+
+.actors {
+ padding-left: 0;
+}
+
+.actors > a {
+ display: block;
+ margin: 5px;
+ padding: 5px;
+ color: white;
+}
+
+.remote-process {
+ font-style: italic;
+ opacity: 0.8;
+}
+
+footer {
+ padding: 10px;
+ background-color: rgba(0,0,0,0.12);
+ border-top: 1px solid rgba(0,0,0,0.1);
+ font-size: small;
+}
+
+footer > a,
+footer > a:visited {
+ color: white;
+}
diff --git a/devtools/client/framework/connect/connect.js b/devtools/client/framework/connect/connect.js
new file mode 100644
index 000000000..d713231f9
--- /dev/null
+++ b/devtools/client/framework/connect/connect.js
@@ -0,0 +1,236 @@
+/* -*- 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";
+
+var Cu = Components.utils;
+var {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+var Services = require("Services");
+var {gDevTools} = require("devtools/client/framework/devtools");
+var {TargetFactory} = require("devtools/client/framework/target");
+var {Toolbox} = require("devtools/client/framework/toolbox");
+var {DebuggerClient} = require("devtools/shared/client/main");
+var {Task} = require("devtools/shared/task");
+var {LocalizationHelper} = require("devtools/shared/l10n");
+var L10N = new LocalizationHelper("devtools/client/locales/connection-screen.properties");
+
+var gClient;
+var gConnectionTimeout;
+
+/**
+ * Once DOM is ready, we prefil the host/port inputs with
+ * pref-stored values.
+ */
+window.addEventListener("DOMContentLoaded", function onDOMReady() {
+ window.removeEventListener("DOMContentLoaded", onDOMReady, true);
+ let host = Services.prefs.getCharPref("devtools.debugger.remote-host");
+ let port = Services.prefs.getIntPref("devtools.debugger.remote-port");
+
+ if (host) {
+ document.getElementById("host").value = host;
+ }
+
+ if (port) {
+ document.getElementById("port").value = port;
+ }
+
+ let form = document.querySelector("#connection-form form");
+ form.addEventListener("submit", function () {
+ window.submit().catch(e => {
+ console.error(e);
+ // Bug 921850: catch rare exception from DebuggerClient.socketConnect
+ showError("unexpected");
+ });
+ });
+}, true);
+
+/**
+ * Called when the "connect" button is clicked.
+ */
+var submit = Task.async(function* () {
+ // Show the "connecting" screen
+ document.body.classList.add("connecting");
+
+ let host = document.getElementById("host").value;
+ let port = document.getElementById("port").value;
+
+ // Save the host/port values
+ try {
+ Services.prefs.setCharPref("devtools.debugger.remote-host", host);
+ Services.prefs.setIntPref("devtools.debugger.remote-port", port);
+ } catch (e) {
+ // Fails in e10s mode, but not a critical feature.
+ }
+
+ // Initiate the connection
+ let transport = yield DebuggerClient.socketConnect({ host, port });
+ gClient = new DebuggerClient(transport);
+ let delay = Services.prefs.getIntPref("devtools.debugger.remote-timeout");
+ gConnectionTimeout = setTimeout(handleConnectionTimeout, delay);
+ let response = yield gClient.connect();
+ yield onConnectionReady(...response);
+});
+
+/**
+ * Connection is ready. List actors and build buttons.
+ */
+var onConnectionReady = Task.async(function* ([aType, aTraits]) {
+ clearTimeout(gConnectionTimeout);
+
+ let response = yield gClient.listAddons();
+
+ let parent = document.getElementById("addonActors");
+ if (!response.error && response.addons.length > 0) {
+ // Add one entry for each add-on.
+ for (let addon of response.addons) {
+ if (!addon.debuggable) {
+ continue;
+ }
+ buildAddonLink(addon, parent);
+ }
+ }
+ else {
+ // Hide the section when there are no add-ons
+ parent.previousElementSibling.remove();
+ parent.remove();
+ }
+
+ response = yield gClient.listTabs();
+
+ parent = document.getElementById("tabActors");
+
+ // Add Global Process debugging...
+ let globals = Cu.cloneInto(response, {});
+ delete globals.tabs;
+ delete globals.selected;
+ // ...only if there are appropriate actors (a 'from' property will always
+ // be there).
+
+ // Add one entry for each open tab.
+ for (let i = 0; i < response.tabs.length; i++) {
+ buildTabLink(response.tabs[i], parent, i == response.selected);
+ }
+
+ let gParent = document.getElementById("globalActors");
+
+ // Build the Remote Process button
+ // If Fx<39, tab actors were used to be exposed on RootActor
+ // but in Fx>=39, chrome is debuggable via getProcess() and ChromeActor
+ if (globals.consoleActor || gClient.mainRoot.traits.allowChromeProcess) {
+ let a = document.createElement("a");
+ a.onclick = function () {
+ if (gClient.mainRoot.traits.allowChromeProcess) {
+ gClient.getProcess()
+ .then(aResponse => {
+ openToolbox(aResponse.form, true);
+ });
+ } else if (globals.consoleActor) {
+ openToolbox(globals, true, "webconsole", false);
+ }
+ };
+ a.title = a.textContent = L10N.getStr("mainProcess");
+ a.className = "remote-process";
+ a.href = "#";
+ gParent.appendChild(a);
+ }
+ // Move the selected tab on top
+ let selectedLink = parent.querySelector("a.selected");
+ if (selectedLink) {
+ parent.insertBefore(selectedLink, parent.firstChild);
+ }
+
+ document.body.classList.remove("connecting");
+ document.body.classList.add("actors-mode");
+
+ // Ensure the first link is focused
+ let firstLink = parent.querySelector("a:first-of-type");
+ if (firstLink) {
+ firstLink.focus();
+ }
+});
+
+/**
+ * Build one button for an add-on actor.
+ */
+function buildAddonLink(addon, parent) {
+ let a = document.createElement("a");
+ a.onclick = function () {
+ openToolbox(addon, true, "jsdebugger", false);
+ };
+
+ a.textContent = addon.name;
+ a.title = addon.id;
+ a.href = "#";
+
+ parent.appendChild(a);
+}
+
+/**
+ * Build one button for a tab actor.
+ */
+function buildTabLink(tab, parent, selected) {
+ let a = document.createElement("a");
+ a.onclick = function () {
+ openToolbox(tab);
+ };
+
+ a.textContent = tab.title;
+ a.title = tab.url;
+ if (!a.textContent) {
+ a.textContent = tab.url;
+ }
+ a.href = "#";
+
+ if (selected) {
+ a.classList.add("selected");
+ }
+
+ parent.appendChild(a);
+}
+
+/**
+ * An error occured. Let's show it and return to the first screen.
+ */
+function showError(type) {
+ document.body.className = "error";
+ let activeError = document.querySelector(".error-message.active");
+ if (activeError) {
+ activeError.classList.remove("active");
+ }
+ activeError = document.querySelector(".error-" + type);
+ if (activeError) {
+ activeError.classList.add("active");
+ }
+}
+
+/**
+ * Connection timeout.
+ */
+function handleConnectionTimeout() {
+ showError("timeout");
+}
+
+/**
+ * The user clicked on one of the buttons.
+ * Opens the toolbox.
+ */
+function openToolbox(form, chrome = false, tool = "webconsole", isTabActor) {
+ let options = {
+ form: form,
+ client: gClient,
+ chrome: chrome,
+ isTabActor: isTabActor
+ };
+ TargetFactory.forRemoteTab(options).then((target) => {
+ let hostType = Toolbox.HostType.WINDOW;
+ gDevTools.showToolbox(target, tool, hostType).then((toolbox) => {
+ toolbox.once("destroyed", function () {
+ gClient.close();
+ });
+ }, console.error.bind(console));
+ window.close();
+ }, console.error.bind(console));
+}
diff --git a/devtools/client/framework/connect/connect.xhtml b/devtools/client/framework/connect/connect.xhtml
new file mode 100644
index 000000000..e8f8818f6
--- /dev/null
+++ b/devtools/client/framework/connect/connect.xhtml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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/. -->
+
+<!DOCTYPE html [
+<!ENTITY % connectionDTD SYSTEM "chrome://devtools/locale/connection-screen.dtd" >
+ %connectionDTD;
+]>
+
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <head>
+ <title>&title;</title>
+ <link rel="stylesheet" href="chrome://devtools/skin/dark-theme.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://devtools/content/framework/connect/connect.css" type="text/css"/>
+ <script type="application/javascript;version=1.8" src="connect.js"></script>
+ </head>
+ <body>
+ <h1>&header;</h1>
+ <section id="connection-form">
+ <form validate="validate" action="#">
+ <label>
+ <span>&host;</span>
+ <input required="required" class="devtools-textinput" id="host" type="text"></input>
+ </label>
+ <label>
+ <span>&port;</span>
+ <input required="required" class="devtools-textinput" id="port" type="number" pattern="\d+"></input>
+ </label>
+ <label>
+ <input class="devtools-toolbarbutton" id="submit" standalone="true" type="submit" value="&connect;"></input>
+ </label>
+ </form>
+ <p class="error-message error-timeout">&errorTimeout;</p>
+ <p class="error-message error-refused">&errorRefused;</p>
+ <p class="error-message error-unexpected">&errorUnexpected;</p>
+ </section>
+ <section id="actors-list">
+ <p>&availableTabs;</p>
+ <ul class="actors" id="tabActors"></ul>
+ <p>&availableAddons;</p>
+ <ul class="actors" id="addonActors"></ul>
+ <p>&availableProcesses;</p>
+ <ul class="actors" id="globalActors"></ul>
+ </section>
+ <section id="connecting">
+ <p class="devtools-throbber">&connecting;</p>
+ </section>
+ <footer>&remoteHelp;<a target='_' href='https://developer.mozilla.org/docs/Tools/Remote_Debugging'>&remoteDocumentation;</a>&remoteHelpSuffix;</footer>
+ </body>
+</html>
diff --git a/devtools/client/framework/dev-edition-promo/dev-edition-logo.png b/devtools/client/framework/dev-edition-promo/dev-edition-logo.png
new file mode 100644
index 000000000..4b90768d2
--- /dev/null
+++ b/devtools/client/framework/dev-edition-promo/dev-edition-logo.png
Binary files differ
diff --git a/devtools/client/framework/dev-edition-promo/dev-edition-promo.css b/devtools/client/framework/dev-edition-promo/dev-edition-promo.css
new file mode 100644
index 000000000..01489fd47
--- /dev/null
+++ b/devtools/client/framework/dev-edition-promo/dev-edition-promo.css
@@ -0,0 +1,94 @@
+/* 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/. */
+
+window {
+ -moz-appearance: none;
+ background-color: transparent;
+}
+
+#doorhanger-container {
+ width: 450px;
+}
+
+#top-panel {
+ padding: 20px;
+ background: #343c45; /* toolbars */
+ color: #8fa1b2; /* body text */
+/*
+ * Sloppy preprocessing since UNIX_BUT_NOT_MAC is only defined
+ * in `browser/app/profile/firefox.js`, which this file cannot
+ * depend on. Must style font-size to target linux.
+ */
+%ifdef XP_UNIX
+%ifndef XP_MACOSX
+ font-size: 13px;
+%else
+ font-size: 15px;
+%endif
+%else
+ font-size: 15px;
+%endif
+ line-height: 19px;
+ min-height: 100px;
+}
+
+#top-panel h1 {
+ font-weight: bold;
+ font-family: Open Sans, sans-serif;
+ font-size: 1.1em;
+}
+
+#top-panel p {
+ font-family: Open Sans, sans-serif;
+ font-size: 0.9em;
+ width: 300px;
+ display: block;
+ margin: 5px 0px 0px 0px;
+}
+
+#icon {
+ background-image: url("chrome://devtools/content/framework/dev-edition-promo/dev-edition-logo.png");
+ background-size: 64px 64px;
+ background-repeat: no-repeat;
+ width: 64px;
+ height: 64px;
+ margin-right: 20px;
+}
+
+#lower-panel {
+ padding: 20px;
+ background-color: #252c33; /* tab toolbars */
+ min-height: 75px;
+ border-top: 1px solid #292e33; /* text high contrast (light) */
+}
+
+#button-container {
+ margin: auto 20px;
+}
+
+#button-container button {
+ font: message-box !important;
+ font-size: 16px !important;
+ cursor: pointer;
+ width: 125px;
+ opacity: 1;
+ position: static;
+ -moz-appearance: none;
+ border-radius: 5px;
+ height: 30px;
+ width: 450px;
+ /* Override embossed borders on Windows/Linux */
+ border: none;
+}
+
+#close {
+ background-color: transparent;
+ color: #8fa1b2; /* body text */
+}
+
+#go {
+ margin-left: 100px;
+ background-color: #70bf53; /* green */
+ color: #f5f7fa; /* selection text color */
+}
diff --git a/devtools/client/framework/dev-edition-promo/dev-edition-promo.xul b/devtools/client/framework/dev-edition-promo/dev-edition-promo.xul
new file mode 100644
index 000000000..ca2515ab0
--- /dev/null
+++ b/devtools/client/framework/dev-edition-promo/dev-edition-promo.xul
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+<!DOCTYPE window [
+<!ENTITY % toolboxDTD SYSTEM "chrome://devtools/locale/toolbox.dtd" >
+ %toolboxDTD;
+]>
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet rel="stylesheet" href="chrome://devtools/content/framework/dev-edition-promo/dev-edition-promo.css" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" id="dev-edition-promo">
+ <vbox id="doorhanger-container">
+ <hbox flex="1" id="top-panel">
+ <image id="icon" />
+ <vbox id="info">
+ <h1>Using Developer Tools in your browser?</h1>
+ <p>Download Firefox Developer Edition, our first browser made just for you.</p>
+ </vbox>
+ </hbox>
+ <hbox id="lower-panel" flex="1">
+ <hbox id="button-container" flex="1">
+ <button id="close"
+ flex="1"
+ standalone="true"
+ label="No thanks">
+ </button>
+ <button id="go"
+ flex="1"
+ standalone="true"
+ label="Learn more »">
+ </button>
+ </hbox>
+ </hbox>
+ </vbox>
+</window>
diff --git a/devtools/client/framework/devtools-browser.js b/devtools/client/framework/devtools-browser.js
new file mode 100644
index 000000000..b9f4d92ba
--- /dev/null
+++ b/devtools/client/framework/devtools-browser.js
@@ -0,0 +1,758 @@
+/* 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";
+
+/**
+ * This is the main module loaded in Firefox desktop that handles browser
+ * windows and coordinates devtools around each window.
+ *
+ * This module is loaded lazily by devtools-clhandler.js, once the first
+ * browser window is ready (i.e. fired browser-delayed-startup-finished event)
+ **/
+
+const {Cc, Ci, Cu} = require("chrome");
+const Services = require("Services");
+const promise = require("promise");
+const defer = require("devtools/shared/defer");
+const Telemetry = require("devtools/client/shared/telemetry");
+const { gDevTools } = require("./devtools");
+const { when: unload } = require("sdk/system/unload");
+
+// Load target and toolbox lazily as they need gDevTools to be fully initialized
+loader.lazyRequireGetter(this, "TargetFactory", "devtools/client/framework/target", true);
+loader.lazyRequireGetter(this, "Toolbox", "devtools/client/framework/toolbox", true);
+loader.lazyRequireGetter(this, "DebuggerServer", "devtools/server/main", true);
+loader.lazyRequireGetter(this, "DebuggerClient", "devtools/shared/client/main", true);
+loader.lazyRequireGetter(this, "BrowserMenus", "devtools/client/framework/browser-menus");
+
+loader.lazyImporter(this, "CustomizableUI", "resource:///modules/CustomizableUI.jsm");
+loader.lazyImporter(this, "AppConstants", "resource://gre/modules/AppConstants.jsm");
+
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper("devtools/client/locales/toolbox.properties");
+
+const TABS_OPEN_PEAK_HISTOGRAM = "DEVTOOLS_TABS_OPEN_PEAK_LINEAR";
+const TABS_OPEN_AVG_HISTOGRAM = "DEVTOOLS_TABS_OPEN_AVERAGE_LINEAR";
+const TABS_PINNED_PEAK_HISTOGRAM = "DEVTOOLS_TABS_PINNED_PEAK_LINEAR";
+const TABS_PINNED_AVG_HISTOGRAM = "DEVTOOLS_TABS_PINNED_AVERAGE_LINEAR";
+
+/**
+ * gDevToolsBrowser exposes functions to connect the gDevTools instance with a
+ * Firefox instance.
+ */
+var gDevToolsBrowser = exports.gDevToolsBrowser = {
+ /**
+ * A record of the windows whose menus we altered, so we can undo the changes
+ * as the window is closed
+ */
+ _trackedBrowserWindows: new Set(),
+
+ _telemetry: new Telemetry(),
+
+ _tabStats: {
+ peakOpen: 0,
+ peakPinned: 0,
+ histOpen: [],
+ histPinned: []
+ },
+
+ /**
+ * This function is for the benefit of Tools:DevToolbox in
+ * browser/base/content/browser-sets.inc and should not be used outside
+ * of there
+ */
+ // used by browser-sets.inc, command
+ toggleToolboxCommand: function (gBrowser) {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ let toolbox = gDevTools.getToolbox(target);
+
+ // If a toolbox exists, using toggle from the Main window :
+ // - should close a docked toolbox
+ // - should focus a windowed toolbox
+ let isDocked = toolbox && toolbox.hostType != Toolbox.HostType.WINDOW;
+ isDocked ? gDevTools.closeToolbox(target) : gDevTools.showToolbox(target);
+ },
+
+ /**
+ * This function ensures the right commands are enabled in a window,
+ * depending on their relevant prefs. It gets run when a window is registered,
+ * or when any of the devtools prefs change.
+ */
+ updateCommandAvailability: function (win) {
+ let doc = win.document;
+
+ function toggleMenuItem(id, isEnabled) {
+ let cmd = doc.getElementById(id);
+ if (isEnabled) {
+ cmd.removeAttribute("disabled");
+ cmd.removeAttribute("hidden");
+ } else {
+ cmd.setAttribute("disabled", "true");
+ cmd.setAttribute("hidden", "true");
+ }
+ }
+
+ // Enable developer toolbar?
+ let devToolbarEnabled = Services.prefs.getBoolPref("devtools.toolbar.enabled");
+ toggleMenuItem("menu_devToolbar", devToolbarEnabled);
+ let focusEl = doc.getElementById("menu_devToolbar");
+ if (devToolbarEnabled) {
+ focusEl.removeAttribute("disabled");
+ } else {
+ focusEl.setAttribute("disabled", "true");
+ }
+ if (devToolbarEnabled && Services.prefs.getBoolPref("devtools.toolbar.visible")) {
+ win.DeveloperToolbar.show(false).catch(console.error);
+ }
+
+ // Enable WebIDE?
+ let webIDEEnabled = Services.prefs.getBoolPref("devtools.webide.enabled");
+ toggleMenuItem("menu_webide", webIDEEnabled);
+
+ let showWebIDEWidget = Services.prefs.getBoolPref("devtools.webide.widget.enabled");
+ if (webIDEEnabled && showWebIDEWidget) {
+ gDevToolsBrowser.installWebIDEWidget();
+ } else {
+ gDevToolsBrowser.uninstallWebIDEWidget();
+ }
+
+ // Enable Browser Toolbox?
+ let chromeEnabled = Services.prefs.getBoolPref("devtools.chrome.enabled");
+ let devtoolsRemoteEnabled = Services.prefs.getBoolPref("devtools.debugger.remote-enabled");
+ let remoteEnabled = chromeEnabled && devtoolsRemoteEnabled;
+ toggleMenuItem("menu_browserToolbox", remoteEnabled);
+ toggleMenuItem("menu_browserContentToolbox", remoteEnabled && win.gMultiProcessBrowser);
+
+ // Enable DevTools connection screen, if the preference allows this.
+ toggleMenuItem("menu_devtools_connect", devtoolsRemoteEnabled);
+ },
+
+ observe: function (subject, topic, prefName) {
+ switch (topic) {
+ case "browser-delayed-startup-finished":
+ this._registerBrowserWindow(subject);
+ break;
+ case "nsPref:changed":
+ if (prefName.endsWith("enabled")) {
+ for (let win of this._trackedBrowserWindows) {
+ this.updateCommandAvailability(win);
+ }
+ }
+ break;
+ }
+ },
+
+ _prefObserverRegistered: false,
+
+ ensurePrefObserver: function () {
+ if (!this._prefObserverRegistered) {
+ this._prefObserverRegistered = true;
+ Services.prefs.addObserver("devtools.", this, false);
+ }
+ },
+
+ /**
+ * This function is for the benefit of Tools:{toolId} commands,
+ * triggered from the WebDeveloper menu and keyboard shortcuts.
+ *
+ * selectToolCommand's behavior:
+ * - if the toolbox is closed,
+ * we open the toolbox and select the tool
+ * - if the toolbox is open, and the targeted tool is not selected,
+ * we select it
+ * - if the toolbox is open, and the targeted tool is selected,
+ * and the host is NOT a window, we close the toolbox
+ * - if the toolbox is open, and the targeted tool is selected,
+ * and the host is a window, we raise the toolbox window
+ */
+ // Used when: - registering a new tool
+ // - new xul window, to add menu items
+ selectToolCommand: function (gBrowser, toolId) {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ let toolbox = gDevTools.getToolbox(target);
+ let toolDefinition = gDevTools.getToolDefinition(toolId);
+
+ if (toolbox &&
+ (toolbox.currentToolId == toolId ||
+ (toolId == "webconsole" && toolbox.splitConsole)))
+ {
+ toolbox.fireCustomKey(toolId);
+
+ if (toolDefinition.preventClosingOnKey || toolbox.hostType == Toolbox.HostType.WINDOW) {
+ toolbox.raise();
+ } else {
+ gDevTools.closeToolbox(target);
+ }
+ gDevTools.emit("select-tool-command", toolId);
+ } else {
+ gDevTools.showToolbox(target, toolId).then(() => {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ let toolbox = gDevTools.getToolbox(target);
+
+ toolbox.fireCustomKey(toolId);
+ gDevTools.emit("select-tool-command", toolId);
+ });
+ }
+ },
+
+ /**
+ * Open a tab on "about:debugging", optionally pre-select a given tab.
+ */
+ // Used by browser-sets.inc, command
+ openAboutDebugging: function (gBrowser, hash) {
+ let url = "about:debugging" + (hash ? "#" + hash : "");
+ gBrowser.selectedTab = gBrowser.addTab(url);
+ },
+
+ /**
+ * Open a tab to allow connects to a remote browser
+ */
+ // Used by browser-sets.inc, command
+ openConnectScreen: function (gBrowser) {
+ gBrowser.selectedTab = gBrowser.addTab("chrome://devtools/content/framework/connect/connect.xhtml");
+ },
+
+ /**
+ * Open WebIDE
+ */
+ // Used by browser-sets.inc, command
+ // itself, webide widget
+ openWebIDE: function () {
+ let win = Services.wm.getMostRecentWindow("devtools:webide");
+ if (win) {
+ win.focus();
+ } else {
+ Services.ww.openWindow(null, "chrome://webide/content/", "webide", "chrome,centerscreen,resizable", null);
+ }
+ },
+
+ _getContentProcessTarget: function (processId) {
+ // Create a DebuggerServer in order to connect locally to it
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+ DebuggerServer.allowChromeProcess = true;
+
+ let transport = DebuggerServer.connectPipe();
+ let client = new DebuggerClient(transport);
+
+ let deferred = defer();
+ client.connect().then(() => {
+ client.getProcess(processId)
+ .then(response => {
+ let options = {
+ form: response.form,
+ client: client,
+ chrome: true,
+ isTabActor: false
+ };
+ return TargetFactory.forRemoteTab(options);
+ })
+ .then(target => {
+ // Ensure closing the connection in order to cleanup
+ // the debugger client and also the server created in the
+ // content process
+ target.on("close", () => {
+ client.close();
+ });
+ deferred.resolve(target);
+ });
+ });
+
+ return deferred.promise;
+ },
+
+ // Used by menus.js
+ openContentProcessToolbox: function (gBrowser) {
+ let { childCount } = Services.ppmm;
+ // Get the process message manager for the current tab
+ let mm = gBrowser.selectedBrowser.messageManager.processMessageManager;
+ let processId = null;
+ for (let i = 1; i < childCount; i++) {
+ let child = Services.ppmm.getChildAt(i);
+ if (child == mm) {
+ processId = i;
+ break;
+ }
+ }
+ if (processId) {
+ this._getContentProcessTarget(processId)
+ .then(target => {
+ // Display a new toolbox, in a new window, with debugger by default
+ return gDevTools.showToolbox(target, "jsdebugger",
+ Toolbox.HostType.WINDOW);
+ });
+ } else {
+ let msg = L10N.getStr("toolbox.noContentProcessForTab.message");
+ Services.prompt.alert(null, "", msg);
+ }
+ },
+
+ /**
+ * Install Developer widget
+ */
+ installDeveloperWidget: function () {
+ let id = "developer-button";
+ let widget = CustomizableUI.getWidget(id);
+ if (widget && widget.provider == CustomizableUI.PROVIDER_API) {
+ return;
+ }
+ CustomizableUI.createWidget({
+ id: id,
+ type: "view",
+ viewId: "PanelUI-developer",
+ shortcutId: "key_devToolboxMenuItem",
+ tooltiptext: "developer-button.tooltiptext2",
+ defaultArea: AppConstants.MOZ_DEV_EDITION ?
+ CustomizableUI.AREA_NAVBAR :
+ CustomizableUI.AREA_PANEL,
+ onViewShowing: function (aEvent) {
+ // Populate the subview with whatever menuitems are in the developer
+ // menu. We skip menu elements, because the menu panel has no way
+ // of dealing with those right now.
+ let doc = aEvent.target.ownerDocument;
+ let win = doc.defaultView;
+
+ let menu = doc.getElementById("menuWebDeveloperPopup");
+
+ let itemsToDisplay = [...menu.children];
+ // Hardcode the addition of the "work offline" menuitem at the bottom:
+ itemsToDisplay.push({localName: "menuseparator", getAttribute: () => {}});
+ itemsToDisplay.push(doc.getElementById("goOfflineMenuitem"));
+
+ let developerItems = doc.getElementById("PanelUI-developerItems");
+ // Import private helpers from CustomizableWidgets
+ let { clearSubview, fillSubviewFromMenuItems } =
+ Cu.import("resource:///modules/CustomizableWidgets.jsm", {});
+ clearSubview(developerItems);
+ fillSubviewFromMenuItems(itemsToDisplay, developerItems);
+ },
+ onBeforeCreated: function (doc) {
+ // Bug 1223127, CUI should make this easier to do.
+ if (doc.getElementById("PanelUI-developerItems")) {
+ return;
+ }
+ let view = doc.createElement("panelview");
+ view.id = "PanelUI-developerItems";
+ let panel = doc.createElement("vbox");
+ panel.setAttribute("class", "panel-subview-body");
+ view.appendChild(panel);
+ doc.getElementById("PanelUI-multiView").appendChild(view);
+ }
+ });
+ },
+
+ /**
+ * Install WebIDE widget
+ */
+ // Used by itself
+ installWebIDEWidget: function () {
+ if (this.isWebIDEWidgetInstalled()) {
+ return;
+ }
+
+ let defaultArea;
+ if (Services.prefs.getBoolPref("devtools.webide.widget.inNavbarByDefault")) {
+ defaultArea = CustomizableUI.AREA_NAVBAR;
+ } else {
+ defaultArea = CustomizableUI.AREA_PANEL;
+ }
+
+ CustomizableUI.createWidget({
+ id: "webide-button",
+ shortcutId: "key_webide",
+ label: "devtools-webide-button2.label",
+ tooltiptext: "devtools-webide-button2.tooltiptext",
+ defaultArea: defaultArea,
+ onCommand: function (aEvent) {
+ gDevToolsBrowser.openWebIDE();
+ }
+ });
+ },
+
+ isWebIDEWidgetInstalled: function () {
+ let widgetWrapper = CustomizableUI.getWidget("webide-button");
+ return !!(widgetWrapper && widgetWrapper.provider == CustomizableUI.PROVIDER_API);
+ },
+
+ /**
+ * The deferred promise will be resolved by WebIDE's UI.init()
+ */
+ isWebIDEInitialized: defer(),
+
+ /**
+ * Uninstall WebIDE widget
+ */
+ uninstallWebIDEWidget: function () {
+ if (this.isWebIDEWidgetInstalled()) {
+ CustomizableUI.removeWidgetFromArea("webide-button");
+ }
+ CustomizableUI.destroyWidget("webide-button");
+ },
+
+ /**
+ * Move WebIDE widget to the navbar
+ */
+ // Used by webide.js
+ moveWebIDEWidgetInNavbar: function () {
+ CustomizableUI.addWidgetToArea("webide-button", CustomizableUI.AREA_NAVBAR);
+ },
+
+ /**
+ * Add this DevTools's presence to a browser window's document
+ *
+ * @param {XULDocument} doc
+ * The document to which devtools should be hooked to.
+ */
+ _registerBrowserWindow: function (win) {
+ if (gDevToolsBrowser._trackedBrowserWindows.has(win)) {
+ return;
+ }
+ gDevToolsBrowser._trackedBrowserWindows.add(win);
+
+ BrowserMenus.addMenus(win.document);
+
+ // Register the Developer widget in the Hamburger menu or navbar
+ // only once menus are registered as it depends on it.
+ gDevToolsBrowser.installDeveloperWidget();
+
+ // Inject lazily DeveloperToolbar on the chrome window
+ loader.lazyGetter(win, "DeveloperToolbar", function () {
+ let { DeveloperToolbar } = require("devtools/client/shared/developer-toolbar");
+ return new DeveloperToolbar(win);
+ });
+
+ this.updateCommandAvailability(win);
+ this.ensurePrefObserver();
+ win.addEventListener("unload", this);
+
+ let tabContainer = win.gBrowser.tabContainer;
+ tabContainer.addEventListener("TabSelect", this, false);
+ tabContainer.addEventListener("TabOpen", this, false);
+ tabContainer.addEventListener("TabClose", this, false);
+ tabContainer.addEventListener("TabPinned", this, false);
+ tabContainer.addEventListener("TabUnpinned", this, false);
+ },
+
+ /**
+ * Hook the JS debugger tool to the "Debug Script" button of the slow script
+ * dialog.
+ */
+ setSlowScriptDebugHandler: function DT_setSlowScriptDebugHandler() {
+ let debugService = Cc["@mozilla.org/dom/slow-script-debug;1"]
+ .getService(Ci.nsISlowScriptDebug);
+ let tm = Cc["@mozilla.org/thread-manager;1"].getService(Ci.nsIThreadManager);
+
+ function slowScriptDebugHandler(aTab, aCallback) {
+ let target = TargetFactory.forTab(aTab);
+
+ gDevTools.showToolbox(target, "jsdebugger").then(toolbox => {
+ let threadClient = toolbox.getCurrentPanel().panelWin.gThreadClient;
+
+ // Break in place, which means resuming the debuggee thread and pausing
+ // right before the next step happens.
+ switch (threadClient.state) {
+ case "paused":
+ // When the debugger is already paused.
+ threadClient.resumeThenPause();
+ aCallback();
+ break;
+ case "attached":
+ // When the debugger is already open.
+ threadClient.interrupt(() => {
+ threadClient.resumeThenPause();
+ aCallback();
+ });
+ break;
+ case "resuming":
+ // The debugger is newly opened.
+ threadClient.addOneTimeListener("resumed", () => {
+ threadClient.interrupt(() => {
+ threadClient.resumeThenPause();
+ aCallback();
+ });
+ });
+ break;
+ default:
+ throw Error("invalid thread client state in slow script debug handler: " +
+ threadClient.state);
+ }
+ });
+ }
+
+ debugService.activationHandler = function (aWindow) {
+ let chromeWindow = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShellTreeItem)
+ .rootTreeItem
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow)
+ .QueryInterface(Ci.nsIDOMChromeWindow);
+
+ let setupFinished = false;
+ slowScriptDebugHandler(chromeWindow.gBrowser.selectedTab,
+ () => { setupFinished = true; });
+
+ // Don't return from the interrupt handler until the debugger is brought
+ // up; no reason to continue executing the slow script.
+ let utils = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ utils.enterModalState();
+ while (!setupFinished) {
+ tm.currentThread.processNextEvent(true);
+ }
+ utils.leaveModalState();
+ };
+
+ debugService.remoteActivationHandler = function (aBrowser, aCallback) {
+ let chromeWindow = aBrowser.ownerDocument.defaultView;
+ let tab = chromeWindow.gBrowser.getTabForBrowser(aBrowser);
+ chromeWindow.gBrowser.selected = tab;
+
+ function callback() {
+ aCallback.finishDebuggerStartup();
+ }
+
+ slowScriptDebugHandler(tab, callback);
+ };
+ },
+
+ /**
+ * Unset the slow script debug handler.
+ */
+ unsetSlowScriptDebugHandler: function DT_unsetSlowScriptDebugHandler() {
+ let debugService = Cc["@mozilla.org/dom/slow-script-debug;1"]
+ .getService(Ci.nsISlowScriptDebug);
+ debugService.activationHandler = undefined;
+ },
+
+ /**
+ * Add the menuitem for a tool to all open browser windows.
+ *
+ * @param {object} toolDefinition
+ * properties of the tool to add
+ */
+ _addToolToWindows: function DT_addToolToWindows(toolDefinition) {
+ // No menu item or global shortcut is required for options panel.
+ if (!toolDefinition.inMenu) {
+ return;
+ }
+
+ // Skip if the tool is disabled.
+ try {
+ if (toolDefinition.visibilityswitch &&
+ !Services.prefs.getBoolPref(toolDefinition.visibilityswitch)) {
+ return;
+ }
+ } catch (e) {}
+
+ // We need to insert the new tool in the right place, which means knowing
+ // the tool that comes before the tool that we're trying to add
+ let allDefs = gDevTools.getToolDefinitionArray();
+ let prevDef;
+ for (let def of allDefs) {
+ if (!def.inMenu) {
+ continue;
+ }
+ if (def === toolDefinition) {
+ break;
+ }
+ prevDef = def;
+ }
+
+ for (let win of gDevToolsBrowser._trackedBrowserWindows) {
+ BrowserMenus.insertToolMenuElements(win.document, toolDefinition, prevDef);
+ }
+
+ if (toolDefinition.id === "jsdebugger") {
+ gDevToolsBrowser.setSlowScriptDebugHandler();
+ }
+ },
+
+ hasToolboxOpened: function (win) {
+ let tab = win.gBrowser.selectedTab;
+ for (let [target, toolbox] of gDevTools._toolboxes) {
+ if (target.tab == tab) {
+ return true;
+ }
+ }
+ return false;
+ },
+
+ /**
+ * Update the "Toggle Tools" checkbox in the developer tools menu. This is
+ * called when a toolbox is created or destroyed.
+ */
+ _updateMenuCheckbox: function DT_updateMenuCheckbox() {
+ for (let win of gDevToolsBrowser._trackedBrowserWindows) {
+
+ let hasToolbox = gDevToolsBrowser.hasToolboxOpened(win);
+
+ let menu = win.document.getElementById("menu_devToolbox");
+ if (hasToolbox) {
+ menu.setAttribute("checked", "true");
+ } else {
+ menu.removeAttribute("checked");
+ }
+ }
+ },
+
+ /**
+ * Remove the menuitem for a tool to all open browser windows.
+ *
+ * @param {string} toolId
+ * id of the tool to remove
+ */
+ _removeToolFromWindows: function DT_removeToolFromWindows(toolId) {
+ for (let win of gDevToolsBrowser._trackedBrowserWindows) {
+ BrowserMenus.removeToolFromMenu(toolId, win.document);
+ }
+
+ if (toolId === "jsdebugger") {
+ gDevToolsBrowser.unsetSlowScriptDebugHandler();
+ }
+ },
+
+ /**
+ * Called on browser unload to remove menu entries, toolboxes and event
+ * listeners from the closed browser window.
+ *
+ * @param {XULWindow} win
+ * The window containing the menu entry
+ */
+ _forgetBrowserWindow: function (win) {
+ if (!gDevToolsBrowser._trackedBrowserWindows.has(win)) {
+ return;
+ }
+ gDevToolsBrowser._trackedBrowserWindows.delete(win);
+ win.removeEventListener("unload", this);
+
+ BrowserMenus.removeMenus(win.document);
+
+ // Destroy toolboxes for closed window
+ for (let [target, toolbox] of gDevTools._toolboxes) {
+ if (toolbox.win.top == win) {
+ toolbox.destroy();
+ }
+ }
+
+ // Destroy the Developer toolbar if it has been accessed
+ let desc = Object.getOwnPropertyDescriptor(win, "DeveloperToolbar");
+ if (desc && !desc.get) {
+ win.DeveloperToolbar.destroy();
+ }
+
+ let tabContainer = win.gBrowser.tabContainer;
+ tabContainer.removeEventListener("TabSelect", this, false);
+ tabContainer.removeEventListener("TabOpen", this, false);
+ tabContainer.removeEventListener("TabClose", this, false);
+ tabContainer.removeEventListener("TabPinned", this, false);
+ tabContainer.removeEventListener("TabUnpinned", this, false);
+ },
+
+ handleEvent: function (event) {
+ switch (event.type) {
+ case "TabOpen":
+ case "TabClose":
+ case "TabPinned":
+ case "TabUnpinned":
+ let open = 0;
+ let pinned = 0;
+
+ for (let win of this._trackedBrowserWindows) {
+ let tabContainer = win.gBrowser.tabContainer;
+ let numPinnedTabs = win.gBrowser._numPinnedTabs || 0;
+ let numTabs = tabContainer.itemCount - numPinnedTabs;
+
+ open += numTabs;
+ pinned += numPinnedTabs;
+ }
+
+ this._tabStats.histOpen.push(open);
+ this._tabStats.histPinned.push(pinned);
+ this._tabStats.peakOpen = Math.max(open, this._tabStats.peakOpen);
+ this._tabStats.peakPinned = Math.max(pinned, this._tabStats.peakPinned);
+ break;
+ case "TabSelect":
+ gDevToolsBrowser._updateMenuCheckbox();
+ break;
+ case "unload":
+ // top-level browser window unload
+ gDevToolsBrowser._forgetBrowserWindow(event.target.defaultView);
+ break;
+ }
+ },
+
+ _pingTelemetry: function () {
+ let mean = function (arr) {
+ if (arr.length === 0) {
+ return 0;
+ }
+
+ let total = arr.reduce((a, b) => a + b);
+ return Math.ceil(total / arr.length);
+ };
+
+ let tabStats = gDevToolsBrowser._tabStats;
+ this._telemetry.log(TABS_OPEN_PEAK_HISTOGRAM, tabStats.peakOpen);
+ this._telemetry.log(TABS_OPEN_AVG_HISTOGRAM, mean(tabStats.histOpen));
+ this._telemetry.log(TABS_PINNED_PEAK_HISTOGRAM, tabStats.peakPinned);
+ this._telemetry.log(TABS_PINNED_AVG_HISTOGRAM, mean(tabStats.histPinned));
+ },
+
+ /**
+ * All browser windows have been closed, tidy up remaining objects.
+ */
+ destroy: function () {
+ Services.prefs.removeObserver("devtools.", gDevToolsBrowser);
+ Services.obs.removeObserver(gDevToolsBrowser, "browser-delayed-startup-finished");
+ Services.obs.removeObserver(gDevToolsBrowser.destroy, "quit-application");
+
+ gDevToolsBrowser._pingTelemetry();
+ gDevToolsBrowser._telemetry = null;
+
+ for (let win of gDevToolsBrowser._trackedBrowserWindows) {
+ gDevToolsBrowser._forgetBrowserWindow(win);
+ }
+ },
+};
+
+// Handle all already registered tools,
+gDevTools.getToolDefinitionArray()
+ .forEach(def => gDevToolsBrowser._addToolToWindows(def));
+// and the new ones.
+gDevTools.on("tool-registered", function (ev, toolId) {
+ let toolDefinition = gDevTools._tools.get(toolId);
+ gDevToolsBrowser._addToolToWindows(toolDefinition);
+});
+
+gDevTools.on("tool-unregistered", function (ev, toolId) {
+ if (typeof toolId != "string") {
+ toolId = toolId.id;
+ }
+ gDevToolsBrowser._removeToolFromWindows(toolId);
+});
+
+gDevTools.on("toolbox-ready", gDevToolsBrowser._updateMenuCheckbox);
+gDevTools.on("toolbox-destroyed", gDevToolsBrowser._updateMenuCheckbox);
+
+Services.obs.addObserver(gDevToolsBrowser.destroy, "quit-application", false);
+Services.obs.addObserver(gDevToolsBrowser, "browser-delayed-startup-finished", false);
+
+// Fake end of browser window load event for all already opened windows
+// that is already fully loaded.
+let enumerator = Services.wm.getEnumerator(gDevTools.chromeWindowType);
+while (enumerator.hasMoreElements()) {
+ let win = enumerator.getNext();
+ if (win.gBrowserInit && win.gBrowserInit.delayedStartupFinished) {
+ gDevToolsBrowser._registerBrowserWindow(win);
+ }
+}
+
+// Watch for module loader unload. Fires when the tools are reloaded.
+unload(function () {
+ gDevToolsBrowser.destroy();
+});
diff --git a/devtools/client/framework/devtools.js b/devtools/client/framework/devtools.js
new file mode 100644
index 000000000..90f88023b
--- /dev/null
+++ b/devtools/client/framework/devtools.js
@@ -0,0 +1,534 @@
+/* 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 Services = require("Services");
+const promise = require("promise");
+const defer = require("devtools/shared/defer");
+
+// Load gDevToolsBrowser toolbox lazily as they need gDevTools to be fully initialized
+loader.lazyRequireGetter(this, "Toolbox", "devtools/client/framework/toolbox", true);
+loader.lazyRequireGetter(this, "ToolboxHostManager", "devtools/client/framework/toolbox-host-manager", true);
+loader.lazyRequireGetter(this, "gDevToolsBrowser", "devtools/client/framework/devtools-browser", true);
+
+const {defaultTools: DefaultTools, defaultThemes: DefaultThemes} =
+ require("devtools/client/definitions");
+const EventEmitter = require("devtools/shared/event-emitter");
+const {JsonView} = require("devtools/client/jsonview/main");
+const AboutDevTools = require("devtools/client/framework/about-devtools-toolbox");
+const {when: unload} = require("sdk/system/unload");
+const {Task} = require("devtools/shared/task");
+
+const FORBIDDEN_IDS = new Set(["toolbox", ""]);
+const MAX_ORDINAL = 99;
+
+/**
+ * DevTools is a class that represents a set of developer tools, it holds a
+ * set of tools and keeps track of open toolboxes in the browser.
+ */
+this.DevTools = function DevTools() {
+ this._tools = new Map(); // Map<toolId, tool>
+ this._themes = new Map(); // Map<themeId, theme>
+ this._toolboxes = new Map(); // Map<target, toolbox>
+ // List of toolboxes that are still in process of creation
+ this._creatingToolboxes = new Map(); // Map<target, toolbox Promise>
+
+ // destroy() is an observer's handler so we need to preserve context.
+ this.destroy = this.destroy.bind(this);
+
+ // JSON Viewer for 'application/json' documents.
+ JsonView.initialize();
+
+ AboutDevTools.register();
+
+ EventEmitter.decorate(this);
+
+ Services.obs.addObserver(this.destroy, "quit-application", false);
+
+ // This is important step in initialization codepath where we are going to
+ // start registering all default tools and themes: create menuitems, keys, emit
+ // related events.
+ this.registerDefaults();
+};
+
+DevTools.prototype = {
+ // The windowtype of the main window, used in various tools. This may be set
+ // to something different by other gecko apps.
+ chromeWindowType: "navigator:browser",
+
+ registerDefaults() {
+ // Ensure registering items in the sorted order (getDefault* functions
+ // return sorted lists)
+ this.getDefaultTools().forEach(definition => this.registerTool(definition));
+ this.getDefaultThemes().forEach(definition => this.registerTheme(definition));
+ },
+
+ unregisterDefaults() {
+ for (let definition of this.getToolDefinitionArray()) {
+ this.unregisterTool(definition.id);
+ }
+ for (let definition of this.getThemeDefinitionArray()) {
+ this.unregisterTheme(definition.id);
+ }
+ },
+
+ /**
+ * Register a new developer tool.
+ *
+ * A definition is a light object that holds different information about a
+ * developer tool. This object is not supposed to have any operational code.
+ * See it as a "manifest".
+ * The only actual code lives in the build() function, which will be used to
+ * start an instance of this tool.
+ *
+ * Each toolDefinition has the following properties:
+ * - id: Unique identifier for this tool (string|required)
+ * - visibilityswitch: Property name to allow us to hide this tool from the
+ * DevTools Toolbox.
+ * A falsy value indicates that it cannot be hidden.
+ * - icon: URL pointing to a graphic which will be used as the src for an
+ * 16x16 img tag (string|required)
+ * - invertIconForLightTheme: The icon can automatically have an inversion
+ * filter applied (default is false). All builtin tools are true, but
+ * addons may omit this to prevent unwanted changes to the `icon`
+ * image. filter: invert(1) is applied to the image (boolean|optional)
+ * - url: URL pointing to a XUL/XHTML document containing the user interface
+ * (string|required)
+ * - label: Localized name for the tool to be displayed to the user
+ * (string|required)
+ * - hideInOptions: Boolean indicating whether or not this tool should be
+ shown in toolbox options or not. Defaults to false.
+ * (boolean)
+ * - build: Function that takes an iframe, which has been populated with the
+ * markup from |url|, and also the toolbox containing the panel.
+ * And returns an instance of ToolPanel (function|required)
+ */
+ registerTool: function DT_registerTool(toolDefinition) {
+ let toolId = toolDefinition.id;
+
+ if (!toolId || FORBIDDEN_IDS.has(toolId)) {
+ throw new Error("Invalid definition.id");
+ }
+
+ // Make sure that additional tools will always be able to be hidden.
+ // When being called from main.js, defaultTools has not yet been exported.
+ // But, we can assume that in this case, it is a default tool.
+ if (DefaultTools.indexOf(toolDefinition) == -1) {
+ toolDefinition.visibilityswitch = "devtools." + toolId + ".enabled";
+ }
+
+ this._tools.set(toolId, toolDefinition);
+
+ this.emit("tool-registered", toolId);
+ },
+
+ /**
+ * Removes all tools that match the given |toolId|
+ * Needed so that add-ons can remove themselves when they are deactivated
+ *
+ * @param {string|object} tool
+ * Definition or the id of the tool to unregister. Passing the
+ * tool id should be avoided as it is a temporary measure.
+ * @param {boolean} isQuitApplication
+ * true to indicate that the call is due to app quit, so we should not
+ * cause a cascade of costly events
+ */
+ unregisterTool: function DT_unregisterTool(tool, isQuitApplication) {
+ let toolId = null;
+ if (typeof tool == "string") {
+ toolId = tool;
+ tool = this._tools.get(tool);
+ }
+ else {
+ toolId = tool.id;
+ }
+ this._tools.delete(toolId);
+
+ if (!isQuitApplication) {
+ this.emit("tool-unregistered", tool);
+ }
+ },
+
+ /**
+ * Sorting function used for sorting tools based on their ordinals.
+ */
+ ordinalSort: function DT_ordinalSort(d1, d2) {
+ let o1 = (typeof d1.ordinal == "number") ? d1.ordinal : MAX_ORDINAL;
+ let o2 = (typeof d2.ordinal == "number") ? d2.ordinal : MAX_ORDINAL;
+ return o1 - o2;
+ },
+
+ getDefaultTools: function DT_getDefaultTools() {
+ return DefaultTools.sort(this.ordinalSort);
+ },
+
+ getAdditionalTools: function DT_getAdditionalTools() {
+ let tools = [];
+ for (let [key, value] of this._tools) {
+ if (DefaultTools.indexOf(value) == -1) {
+ tools.push(value);
+ }
+ }
+ return tools.sort(this.ordinalSort);
+ },
+
+ getDefaultThemes() {
+ return DefaultThemes.sort(this.ordinalSort);
+ },
+
+ /**
+ * Get a tool definition if it exists and is enabled.
+ *
+ * @param {string} toolId
+ * The id of the tool to show
+ *
+ * @return {ToolDefinition|null} tool
+ * The ToolDefinition for the id or null.
+ */
+ getToolDefinition: function DT_getToolDefinition(toolId) {
+ let tool = this._tools.get(toolId);
+ if (!tool) {
+ return null;
+ } else if (!tool.visibilityswitch) {
+ return tool;
+ }
+
+ let enabled;
+ try {
+ enabled = Services.prefs.getBoolPref(tool.visibilityswitch);
+ } catch (e) {
+ enabled = true;
+ }
+
+ return enabled ? tool : null;
+ },
+
+ /**
+ * Allow ToolBoxes to get at the list of tools that they should populate
+ * themselves with.
+ *
+ * @return {Map} tools
+ * A map of the the tool definitions registered in this instance
+ */
+ getToolDefinitionMap: function DT_getToolDefinitionMap() {
+ let tools = new Map();
+
+ for (let [id, definition] of this._tools) {
+ if (this.getToolDefinition(id)) {
+ tools.set(id, definition);
+ }
+ }
+
+ return tools;
+ },
+
+ /**
+ * Tools have an inherent ordering that can't be represented in a Map so
+ * getToolDefinitionArray provides an alternative representation of the
+ * definitions sorted by ordinal value.
+ *
+ * @return {Array} tools
+ * A sorted array of the tool definitions registered in this instance
+ */
+ getToolDefinitionArray: function DT_getToolDefinitionArray() {
+ let definitions = [];
+
+ for (let [id, definition] of this._tools) {
+ if (this.getToolDefinition(id)) {
+ definitions.push(definition);
+ }
+ }
+
+ return definitions.sort(this.ordinalSort);
+ },
+
+ /**
+ * Register a new theme for developer tools toolbox.
+ *
+ * A definition is a light object that holds various information about a
+ * theme.
+ *
+ * Each themeDefinition has the following properties:
+ * - id: Unique identifier for this theme (string|required)
+ * - label: Localized name for the theme to be displayed to the user
+ * (string|required)
+ * - stylesheets: Array of URLs pointing to a CSS document(s) containing
+ * the theme style rules (array|required)
+ * - classList: Array of class names identifying the theme within a document.
+ * These names are set to document element when applying
+ * the theme (array|required)
+ * - onApply: Function that is executed by the framework when the theme
+ * is applied. The function takes the current iframe window
+ * and the previous theme id as arguments (function)
+ * - onUnapply: Function that is executed by the framework when the theme
+ * is unapplied. The function takes the current iframe window
+ * and the new theme id as arguments (function)
+ */
+ registerTheme: function DT_registerTheme(themeDefinition) {
+ let themeId = themeDefinition.id;
+
+ if (!themeId) {
+ throw new Error("Invalid theme id");
+ }
+
+ if (this._themes.get(themeId)) {
+ throw new Error("Theme with the same id is already registered");
+ }
+
+ this._themes.set(themeId, themeDefinition);
+
+ this.emit("theme-registered", themeId);
+ },
+
+ /**
+ * Removes an existing theme from the list of registered themes.
+ * Needed so that add-ons can remove themselves when they are deactivated
+ *
+ * @param {string|object} theme
+ * Definition or the id of the theme to unregister.
+ */
+ unregisterTheme: function DT_unregisterTheme(theme) {
+ let themeId = null;
+ if (typeof theme == "string") {
+ themeId = theme;
+ theme = this._themes.get(theme);
+ }
+ else {
+ themeId = theme.id;
+ }
+
+ let currTheme = Services.prefs.getCharPref("devtools.theme");
+
+ // Note that we can't check if `theme` is an item
+ // of `DefaultThemes` as we end up reloading definitions
+ // module and end up with different theme objects
+ let isCoreTheme = DefaultThemes.some(t => t.id === themeId);
+
+ // Reset the theme if an extension theme that's currently applied
+ // is being removed.
+ // Ignore shutdown since addons get disabled during that time.
+ if (!Services.startup.shuttingDown &&
+ !isCoreTheme &&
+ theme.id == currTheme) {
+ Services.prefs.setCharPref("devtools.theme", "light");
+
+ let data = {
+ pref: "devtools.theme",
+ newValue: "light",
+ oldValue: currTheme
+ };
+
+ this.emit("pref-changed", data);
+
+ this.emit("theme-unregistered", theme);
+ }
+
+ this._themes.delete(themeId);
+ },
+
+ /**
+ * Get a theme definition if it exists.
+ *
+ * @param {string} themeId
+ * The id of the theme
+ *
+ * @return {ThemeDefinition|null} theme
+ * The ThemeDefinition for the id or null.
+ */
+ getThemeDefinition: function DT_getThemeDefinition(themeId) {
+ let theme = this._themes.get(themeId);
+ if (!theme) {
+ return null;
+ }
+ return theme;
+ },
+
+ /**
+ * Get map of registered themes.
+ *
+ * @return {Map} themes
+ * A map of the the theme definitions registered in this instance
+ */
+ getThemeDefinitionMap: function DT_getThemeDefinitionMap() {
+ let themes = new Map();
+
+ for (let [id, definition] of this._themes) {
+ if (this.getThemeDefinition(id)) {
+ themes.set(id, definition);
+ }
+ }
+
+ return themes;
+ },
+
+ /**
+ * Get registered themes definitions sorted by ordinal value.
+ *
+ * @return {Array} themes
+ * A sorted array of the theme definitions registered in this instance
+ */
+ getThemeDefinitionArray: function DT_getThemeDefinitionArray() {
+ let definitions = [];
+
+ for (let [id, definition] of this._themes) {
+ if (this.getThemeDefinition(id)) {
+ definitions.push(definition);
+ }
+ }
+
+ return definitions.sort(this.ordinalSort);
+ },
+
+ /**
+ * Show a Toolbox for a target (either by creating a new one, or if a toolbox
+ * already exists for the target, by bring to the front the existing one)
+ * If |toolId| is specified then the displayed toolbox will have the
+ * specified tool selected.
+ * If |hostType| is specified then the toolbox will be displayed using the
+ * specified HostType.
+ *
+ * @param {Target} target
+ * The target the toolbox will debug
+ * @param {string} toolId
+ * The id of the tool to show
+ * @param {Toolbox.HostType} hostType
+ * The type of host (bottom, window, side)
+ * @param {object} hostOptions
+ * Options for host specifically
+ *
+ * @return {Toolbox} toolbox
+ * The toolbox that was opened
+ */
+ showToolbox: Task.async(function* (target, toolId, hostType, hostOptions) {
+ let toolbox = this._toolboxes.get(target);
+ if (toolbox) {
+
+ if (hostType != null && toolbox.hostType != hostType) {
+ yield toolbox.switchHost(hostType);
+ }
+
+ if (toolId != null && toolbox.currentToolId != toolId) {
+ yield toolbox.selectTool(toolId);
+ }
+
+ toolbox.raise();
+ } else {
+ // As toolbox object creation is async, we have to be careful about races
+ // Check for possible already in process of loading toolboxes before
+ // actually trying to create a new one.
+ let promise = this._creatingToolboxes.get(target);
+ if (promise) {
+ return yield promise;
+ }
+ let toolboxPromise = this.createToolbox(target, toolId, hostType, hostOptions);
+ this._creatingToolboxes.set(target, toolboxPromise);
+ toolbox = yield toolboxPromise;
+ this._creatingToolboxes.delete(target);
+ }
+ return toolbox;
+ }),
+
+ createToolbox: Task.async(function* (target, toolId, hostType, hostOptions) {
+ let manager = new ToolboxHostManager(target, hostType, hostOptions);
+
+ let toolbox = yield manager.create(toolId);
+
+ this._toolboxes.set(target, toolbox);
+
+ this.emit("toolbox-created", toolbox);
+
+ toolbox.once("destroy", () => {
+ this.emit("toolbox-destroy", target);
+ });
+
+ toolbox.once("destroyed", () => {
+ this._toolboxes.delete(target);
+ this.emit("toolbox-destroyed", target);
+ });
+
+ yield toolbox.open();
+ this.emit("toolbox-ready", toolbox);
+
+ return toolbox;
+ }),
+
+ /**
+ * Return the toolbox for a given target.
+ *
+ * @param {object} target
+ * Target value e.g. the target that owns this toolbox
+ *
+ * @return {Toolbox} toolbox
+ * The toolbox that is debugging the given target
+ */
+ getToolbox: function DT_getToolbox(target) {
+ return this._toolboxes.get(target);
+ },
+
+ /**
+ * Close the toolbox for a given target
+ *
+ * @return promise
+ * This promise will resolve to false if no toolbox was found
+ * associated to the target. true, if the toolbox was successfully
+ * closed.
+ */
+ closeToolbox: Task.async(function* (target) {
+ let toolbox = yield this._creatingToolboxes.get(target);
+ if (!toolbox) {
+ toolbox = this._toolboxes.get(target);
+ }
+ if (!toolbox) {
+ return false;
+ }
+ yield toolbox.destroy();
+ return true;
+ }),
+
+ /**
+ * Called to tear down a tools provider.
+ */
+ _teardown: function DT_teardown() {
+ for (let [target, toolbox] of this._toolboxes) {
+ toolbox.destroy();
+ }
+ AboutDevTools.unregister();
+ },
+
+ /**
+ * All browser windows have been closed, tidy up remaining objects.
+ */
+ destroy: function () {
+ Services.obs.removeObserver(this.destroy, "quit-application");
+
+ for (let [key, tool] of this.getToolDefinitionMap()) {
+ this.unregisterTool(key, true);
+ }
+
+ JsonView.destroy();
+
+ gDevTools.unregisterDefaults();
+
+ // Cleaning down the toolboxes: i.e.
+ // for (let [target, toolbox] of this._toolboxes) toolbox.destroy();
+ // Is taken care of by the gDevToolsBrowser.forgetBrowserWindow
+ },
+
+ /**
+ * Iterator that yields each of the toolboxes.
+ */
+ *[Symbol.iterator ]() {
+ for (let toolbox of this._toolboxes) {
+ yield toolbox;
+ }
+ }
+};
+
+const gDevTools = exports.gDevTools = new DevTools();
+
+// Watch for module loader unload. Fires when the tools are reloaded.
+unload(function () {
+ gDevTools._teardown();
+});
diff --git a/devtools/client/framework/gDevTools.jsm b/devtools/client/framework/gDevTools.jsm
new file mode 100644
index 000000000..d825c0eaa
--- /dev/null
+++ b/devtools/client/framework/gDevTools.jsm
@@ -0,0 +1,162 @@
+/* -*- 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";
+
+/**
+ * This JSM is here to keep some compatibility with existing add-ons.
+ * Please now use the modules:
+ * - devtools/client/framework/devtools for gDevTools
+ * - devtools/client/framework/devtools-browser for gDevToolsBrowser
+ */
+
+this.EXPORTED_SYMBOLS = [ "gDevTools", "gDevToolsBrowser" ];
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+const { loader } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+
+/**
+ * Do not directly map to the commonjs modules so that callsites of
+ * gDevTools.jsm do not have to do anything to access to the very last version
+ * of the module. The `devtools` and `browser` getter are always going to
+ * retrieve the very last version of the modules.
+ */
+Object.defineProperty(this, "require", {
+ get() {
+ let { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+ return require;
+ }
+});
+Object.defineProperty(this, "devtools", {
+ get() {
+ return require("devtools/client/framework/devtools").gDevTools;
+ }
+});
+Object.defineProperty(this, "browser", {
+ get() {
+ return require("devtools/client/framework/devtools-browser").gDevToolsBrowser;
+ }
+});
+
+/**
+ * gDevTools is a singleton that controls the Firefox Developer Tools.
+ *
+ * It is an instance of a DevTools class that holds a set of tools. It has the
+ * same lifetime as the browser.
+ */
+let gDevToolsMethods = [
+ // Used by the reload addon.
+ // Force reloading dependencies if the loader happens to have reloaded.
+ "reload",
+
+ // Used by: - b2g desktop.js
+ // - nsContextMenu
+ // - /devtools code
+ "showToolbox",
+
+ // Used by Addon SDK and /devtools
+ "closeToolbox",
+ "getToolbox",
+
+ // Used by Addon SDK, main.js and tests:
+ "registerTool",
+ "registerTheme",
+ "unregisterTool",
+ "unregisterTheme",
+
+ // Used by main.js and test
+ "getToolDefinitionArray",
+ "getThemeDefinitionArray",
+
+ // Used by theme-switching.js
+ "getThemeDefinition",
+ "emit",
+
+ // Used by /devtools
+ "on",
+ "off",
+ "once",
+
+ // Used by tests
+ "getToolDefinitionMap",
+ "getThemeDefinitionMap",
+ "getDefaultTools",
+ "getAdditionalTools",
+ "getToolDefinition",
+];
+this.gDevTools = {
+ // Used by tests
+ get _toolboxes() {
+ return devtools._toolboxes;
+ },
+ get _tools() {
+ return devtools._tools;
+ },
+ *[Symbol.iterator ]() {
+ for (let toolbox of this._toolboxes) {
+ yield toolbox;
+ }
+ }
+};
+gDevToolsMethods.forEach(name => {
+ this.gDevTools[name] = (...args) => {
+ return devtools[name].apply(devtools, args);
+ };
+});
+
+
+/**
+ * gDevToolsBrowser exposes functions to connect the gDevTools instance with a
+ * Firefox instance.
+ */
+let gDevToolsBrowserMethods = [
+ // used by browser-sets.inc, command
+ "toggleToolboxCommand",
+
+ // Used by browser.js itself, by setting a oncommand string...
+ "selectToolCommand",
+
+ // Used by browser-sets.inc, command
+ "openAboutDebugging",
+
+ // Used by browser-sets.inc, command
+ "openConnectScreen",
+
+ // Used by browser-sets.inc, command
+ // itself, webide widget
+ "openWebIDE",
+
+ // Used by browser-sets.inc, command
+ "openContentProcessToolbox",
+
+ // Used by webide.js
+ "moveWebIDEWidgetInNavbar",
+
+ // Used by browser.js
+ "registerBrowserWindow",
+
+ // Used by reload addon
+ "hasToolboxOpened",
+
+ // Used by browser.js
+ "forgetBrowserWindow"
+];
+this.gDevToolsBrowser = {
+ // Used by webide.js
+ get isWebIDEInitialized() {
+ return browser.isWebIDEInitialized;
+ },
+ // Used by a test (should be removed)
+ get _trackedBrowserWindows() {
+ return browser._trackedBrowserWindows;
+ }
+};
+gDevToolsBrowserMethods.forEach(name => {
+ this.gDevToolsBrowser[name] = (...args) => {
+ return browser[name].apply(browser, args);
+ };
+});
diff --git a/devtools/client/framework/location-store.js b/devtools/client/framework/location-store.js
new file mode 100644
index 000000000..96deb0a99
--- /dev/null
+++ b/devtools/client/framework/location-store.js
@@ -0,0 +1,103 @@
+/* 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 SOURCE_TOKEN = "<:>";
+
+function LocationStore (store) {
+ this._store = store || new Map();
+}
+
+/**
+ * Method to get a promised location from the Store.
+ * @param location
+ * @returns Promise<Object>
+ */
+LocationStore.prototype.get = function (location) {
+ this._safeAccessInit(location.url);
+ return this._store.get(location.url).get(location);
+};
+
+/**
+ * Method to set a promised location to the Store
+ * @param location
+ * @param promisedLocation
+ */
+LocationStore.prototype.set = function (location, promisedLocation = null) {
+ this._safeAccessInit(location.url);
+ this._store.get(location.url).set(serialize(location), promisedLocation);
+};
+
+/**
+ * Utility method to verify if key exists in Store before accessing it.
+ * If not, initializing it.
+ * @param url
+ * @private
+ */
+LocationStore.prototype._safeAccessInit = function (url) {
+ if (!this._store.has(url)) {
+ this._store.set(url, new Map());
+ }
+};
+
+/**
+ * Utility proxy method to Map.clear() method
+ */
+LocationStore.prototype.clear = function () {
+ this._store.clear();
+};
+
+/**
+ * Retrieves an object containing all locations to be resolved when `source-updated`
+ * event is triggered.
+ * @param url
+ * @returns {Array<String>}
+ */
+LocationStore.prototype.getByURL = function (url){
+ if (this._store.has(url)) {
+ return [...this._store.get(url).keys()];
+ }
+ return [];
+};
+
+/**
+ * Invalidates the stale location promises from the store when `source-updated`
+ * event is triggered, and when FrameView unsubscribes from a location.
+ * @param url
+ */
+LocationStore.prototype.clearByURL = function (url) {
+ this._safeAccessInit(url);
+ this._store.set(url, new Map());
+};
+
+exports.LocationStore = LocationStore;
+exports.serialize = serialize;
+exports.deserialize = deserialize;
+
+/**
+ * Utility method to serialize the source
+ * @param source
+ * @returns {string}
+ */
+function serialize(source) {
+ let { url, line, column } = source;
+ line = line || 0;
+ column = column || 0;
+ return `${url}${SOURCE_TOKEN}${line}${SOURCE_TOKEN}${column}`;
+};
+
+/**
+ * Utility method to serialize the source
+ * @param source
+ * @returns Object
+ */
+function deserialize(source) {
+ let [ url, line, column ] = source.split(SOURCE_TOKEN);
+ line = parseInt(line);
+ column = parseInt(column);
+ if (column === 0) {
+ return { url, line };
+ }
+ return { url, line, column };
+};
diff --git a/devtools/client/framework/menu-item.js b/devtools/client/framework/menu-item.js
new file mode 100644
index 000000000..f6afefa41
--- /dev/null
+++ b/devtools/client/framework/menu-item.js
@@ -0,0 +1,65 @@
+/* -*- 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";
+
+/**
+ * A partial implementation of the MenuItem API provided by electron:
+ * https://github.com/electron/electron/blob/master/docs/api/menu-item.md.
+ *
+ * Missing features:
+ * - id String - Unique within a single menu. If defined then it can be used
+ * as a reference to this item by the position attribute.
+ * - role String - Define the action of the menu item; when specified the
+ * click property will be ignored
+ * - sublabel String
+ * - accelerator Accelerator
+ * - icon NativeImage
+ * - position String - This field allows fine-grained definition of the
+ * specific location within a given menu.
+ *
+ * Implemented features:
+ * @param Object options
+ * Function click
+ * Will be called with click(menuItem, browserWindow) when the menu item
+ * is clicked
+ * String type
+ * Can be normal, separator, submenu, checkbox or radio
+ * String label
+ * Boolean enabled
+ * If false, the menu item will be greyed out and unclickable.
+ * Boolean checked
+ * Should only be specified for checkbox or radio type menu items.
+ * Menu submenu
+ * Should be specified for submenu type menu items. If submenu is specified,
+ * the type: 'submenu' can be omitted. If the value is not a Menu then it
+ * will be automatically converted to one using Menu.buildFromTemplate.
+ * Boolean visible
+ * If false, the menu item will be entirely hidden.
+ */
+function MenuItem({
+ accesskey = null,
+ checked = false,
+ click = () => {},
+ disabled = false,
+ label = "",
+ id = null,
+ submenu = null,
+ type = "normal",
+ visible = true,
+} = { }) {
+ this.accesskey = accesskey;
+ this.checked = checked;
+ this.click = click;
+ this.disabled = disabled;
+ this.id = id;
+ this.label = label;
+ this.submenu = submenu;
+ this.type = type;
+ this.visible = visible;
+}
+
+module.exports = MenuItem;
diff --git a/devtools/client/framework/menu.js b/devtools/client/framework/menu.js
new file mode 100644
index 000000000..c96dbc2c7
--- /dev/null
+++ b/devtools/client/framework/menu.js
@@ -0,0 +1,173 @@
+/* -*- 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 EventEmitter = require("devtools/shared/event-emitter");
+
+/**
+ * A partial implementation of the Menu API provided by electron:
+ * https://github.com/electron/electron/blob/master/docs/api/menu.md.
+ *
+ * Extra features:
+ * - Emits an 'open' and 'close' event when the menu is opened/closed
+
+ * @param String id (non standard)
+ * Needed so tests can confirm the XUL implementation is working
+ */
+function Menu({ id = null } = {}) {
+ this.menuitems = [];
+ this.id = id;
+
+ Object.defineProperty(this, "items", {
+ get() {
+ return this.menuitems;
+ }
+ });
+
+ EventEmitter.decorate(this);
+}
+
+/**
+ * Add an item to the end of the Menu
+ *
+ * @param {MenuItem} menuItem
+ */
+Menu.prototype.append = function (menuItem) {
+ this.menuitems.push(menuItem);
+};
+
+/**
+ * Add an item to a specified position in the menu
+ *
+ * @param {int} pos
+ * @param {MenuItem} menuItem
+ */
+Menu.prototype.insert = function (pos, menuItem) {
+ throw Error("Not implemented");
+};
+
+/**
+ * Show the Menu at a specified location on the screen
+ *
+ * Missing features:
+ * - browserWindow - BrowserWindow (optional) - Default is null.
+ * - positioningItem Number - (optional) OS X
+ *
+ * @param {int} screenX
+ * @param {int} screenY
+ * @param Toolbox toolbox (non standard)
+ * Needed so we in which window to inject XUL
+ */
+Menu.prototype.popup = function (screenX, screenY, toolbox) {
+ let doc = toolbox.doc;
+ let popupset = doc.querySelector("popupset");
+ // See bug 1285229, on Windows, opening the same popup multiple times in a
+ // row ends up duplicating the popup. The newly inserted popup doesn't
+ // dismiss the old one. So remove any previously displayed popup before
+ // opening a new one.
+ let popup = popupset.querySelector("menupopup[menu-api=\"true\"]");
+ if (popup) {
+ popup.hidePopup();
+ }
+
+ popup = doc.createElement("menupopup");
+ popup.setAttribute("menu-api", "true");
+
+ if (this.id) {
+ popup.id = this.id;
+ }
+ this._createMenuItems(popup);
+
+ // Remove the menu from the DOM once it's hidden.
+ popup.addEventListener("popuphidden", (e) => {
+ if (e.target === popup) {
+ popup.remove();
+ this.emit("close");
+ }
+ });
+
+ popup.addEventListener("popupshown", (e) => {
+ if (e.target === popup) {
+ this.emit("open");
+ }
+ });
+
+ popupset.appendChild(popup);
+ popup.openPopupAtScreen(screenX, screenY, true);
+};
+
+Menu.prototype._createMenuItems = function (parent) {
+ let doc = parent.ownerDocument;
+ this.menuitems.forEach(item => {
+ if (!item.visible) {
+ return;
+ }
+
+ if (item.submenu) {
+ let menupopup = doc.createElement("menupopup");
+ item.submenu._createMenuItems(menupopup);
+
+ let menu = doc.createElement("menu");
+ menu.appendChild(menupopup);
+ menu.setAttribute("label", item.label);
+ if (item.disabled) {
+ menu.setAttribute("disabled", "true");
+ }
+ if (item.accesskey) {
+ menu.setAttribute("accesskey", item.accesskey);
+ }
+ if (item.id) {
+ menu.id = item.id;
+ }
+ parent.appendChild(menu);
+ } else if (item.type === "separator") {
+ let menusep = doc.createElement("menuseparator");
+ parent.appendChild(menusep);
+ } else {
+ let menuitem = doc.createElement("menuitem");
+ menuitem.setAttribute("label", item.label);
+ menuitem.addEventListener("command", () => {
+ item.click();
+ });
+
+ if (item.type === "checkbox") {
+ menuitem.setAttribute("type", "checkbox");
+ }
+ if (item.type === "radio") {
+ menuitem.setAttribute("type", "radio");
+ }
+ if (item.disabled) {
+ menuitem.setAttribute("disabled", "true");
+ }
+ if (item.checked) {
+ menuitem.setAttribute("checked", "true");
+ }
+ if (item.accesskey) {
+ menuitem.setAttribute("accesskey", item.accesskey);
+ }
+ if (item.id) {
+ menuitem.id = item.id;
+ }
+
+ parent.appendChild(menuitem);
+ }
+ });
+};
+
+Menu.setApplicationMenu = () => {
+ throw Error("Not implemented");
+};
+
+Menu.sendActionToFirstResponder = () => {
+ throw Error("Not implemented");
+};
+
+Menu.buildFromTemplate = () => {
+ throw Error("Not implemented");
+};
+
+module.exports = Menu;
diff --git a/devtools/client/framework/moz.build b/devtools/client/framework/moz.build
new file mode 100644
index 000000000..7b28b4b9e
--- /dev/null
+++ b/devtools/client/framework/moz.build
@@ -0,0 +1,33 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
+TEST_HARNESS_FILES.xpcshell.devtools.client.framework.test += [
+ 'test/shared-redux-head.js',
+]
+
+DevToolsModules(
+ 'about-devtools-toolbox.js',
+ 'attach-thread.js',
+ 'browser-menus.js',
+ 'devtools-browser.js',
+ 'devtools.js',
+ 'gDevTools.jsm',
+ 'location-store.js',
+ 'menu-item.js',
+ 'menu.js',
+ 'selection.js',
+ 'sidebar.js',
+ 'source-map-service.js',
+ 'target-from-url.js',
+ 'target.js',
+ 'toolbox-highlighter-utils.js',
+ 'toolbox-host-manager.js',
+ 'toolbox-hosts.js',
+ 'toolbox-options.js',
+ 'toolbox.js',
+ 'ToolboxProcess.jsm',
+)
diff --git a/devtools/client/framework/options-panel.css b/devtools/client/framework/options-panel.css
new file mode 100644
index 000000000..4aad29e7b
--- /dev/null
+++ b/devtools/client/framework/options-panel.css
@@ -0,0 +1,107 @@
+/* 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/. */
+:root{
+ -moz-user-select: none;
+}
+
+#options-panel-container {
+ overflow: auto;
+}
+
+#options-panel {
+ display: block;
+}
+
+.options-vertical-pane {
+ display: inline;
+ float: left;
+}
+
+.options-vertical-pane {
+ margin: 5px;
+ width: calc(100%/3 - 10px);
+ min-width: 320px;
+ padding-inline-start: 5px;
+ box-sizing: border-box;
+}
+
+/* Snap to 50% width once there is not room for 3 columns anymore.
+ This prevents having 2 columns showing in a row, but taking up
+ only ~66% of the available space. */
+@media (max-width: 1000px) {
+ .options-vertical-pane {
+ width: calc(100%/2 - 10px);
+ }
+}
+
+.options-vertical-pane fieldset {
+ border: none;
+}
+
+.options-vertical-pane fieldset legend {
+ font-size: 1.4rem;
+ margin-inline-start: -15px;
+ margin-bottom: 3px;
+ cursor: default;
+}
+
+.options-vertical-pane fieldset + fieldset {
+ margin-top: 1rem;
+}
+
+.options-groupbox {
+ margin-inline-start: 15px;
+ padding: 2px;
+}
+
+.options-groupbox label {
+ display: flex;
+ padding: 4px 0;
+ align-items: center;
+}
+
+/* Add padding for label of select inputs in order to
+ align it with surrounding checkboxes */
+.options-groupbox label span:first-child {
+ padding-inline-start: 5px;
+}
+
+.options-groupbox label span + select {
+ margin-inline-start: 4px;
+}
+
+.options-groupbox.horizontal-options-groupbox label {
+ display: inline-flex;
+ align-items: flex-end;
+}
+
+.options-groupbox.horizontal-options-groupbox label + label {
+ margin-inline-start: 4px;
+}
+
+.options-groupbox > *,
+.options-groupbox > .hidden-labels-box > checkbox {
+ padding: 2px;
+}
+
+.options-groupbox > .hidden-labels-box {
+ padding: 0;
+}
+
+.options-citation-label {
+ display: inline-block;
+ font-size: 1rem;
+ font-style: italic;
+ /* To align it with the checkbox */
+ padding: 4px 0 0;
+ padding-inline-end: 4px;
+}
+
+#devtools-sourceeditor-keybinding-select {
+ min-width: 130px;
+}
+
+#devtools-sourceeditor-tabsize-select {
+ min-width: 80px;
+}
diff --git a/devtools/client/framework/selection.js b/devtools/client/framework/selection.js
new file mode 100644
index 000000000..8125f8508
--- /dev/null
+++ b/devtools/client/framework/selection.js
@@ -0,0 +1,247 @@
+/* -*- 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 nodeConstants = require("devtools/shared/dom-node-constants");
+var EventEmitter = require("devtools/shared/event-emitter");
+
+/**
+ * API
+ *
+ * new Selection(walker=null)
+ * destroy()
+ * node (readonly)
+ * setNode(node, origin="unknown")
+ *
+ * Helpers:
+ *
+ * window
+ * document
+ * isRoot()
+ * isNode()
+ * isHTMLNode()
+ *
+ * Check the nature of the node:
+ *
+ * isElementNode()
+ * isAttributeNode()
+ * isTextNode()
+ * isCDATANode()
+ * isEntityRefNode()
+ * isEntityNode()
+ * isProcessingInstructionNode()
+ * isCommentNode()
+ * isDocumentNode()
+ * isDocumentTypeNode()
+ * isDocumentFragmentNode()
+ * isNotationNode()
+ *
+ * Events:
+ * "new-node-front" when the inner node changed
+ * "attribute-changed" when an attribute is changed
+ * "detached-front" when the node (or one of its parents) is removed from
+ * the document
+ * "reparented" when the node (or one of its parents) is moved under
+ * a different node
+ */
+
+/**
+ * A Selection object. Hold a reference to a node.
+ * Includes some helpers, fire some helpful events.
+ */
+function Selection(walker) {
+ EventEmitter.decorate(this);
+
+ this._onMutations = this._onMutations.bind(this);
+ this.setWalker(walker);
+}
+
+exports.Selection = Selection;
+
+Selection.prototype = {
+ _walker: null,
+
+ _onMutations: function (mutations) {
+ let attributeChange = false;
+ let pseudoChange = false;
+ let detached = false;
+ let parentNode = null;
+
+ for (let m of mutations) {
+ if (!attributeChange && m.type == "attributes") {
+ attributeChange = true;
+ }
+ if (m.type == "childList") {
+ if (!detached && !this.isConnected()) {
+ if (this.isNode()) {
+ parentNode = m.target;
+ }
+ detached = true;
+ }
+ }
+ if (m.type == "pseudoClassLock") {
+ pseudoChange = true;
+ }
+ }
+
+ // Fire our events depending on what changed in the mutations array
+ if (attributeChange) {
+ this.emit("attribute-changed");
+ }
+ if (pseudoChange) {
+ this.emit("pseudoclass");
+ }
+ if (detached) {
+ this.emit("detached-front", parentNode);
+ }
+ },
+
+ destroy: function () {
+ this.setWalker(null);
+ },
+
+ setWalker: function (walker) {
+ if (this._walker) {
+ this._walker.off("mutations", this._onMutations);
+ }
+ this._walker = walker;
+ if (this._walker) {
+ this._walker.on("mutations", this._onMutations);
+ }
+ },
+
+ setNodeFront: function (value, reason = "unknown") {
+ this.reason = reason;
+
+ // If an inlineTextChild text node is being set, then set it's parent instead.
+ let parentNode = value && value.parentNode();
+ if (value && parentNode && parentNode.inlineTextChild === value) {
+ value = parentNode;
+ }
+
+ this._nodeFront = value;
+ this.emit("new-node-front", value, this.reason);
+ },
+
+ get documentFront() {
+ return this._walker.document(this._nodeFront);
+ },
+
+ get nodeFront() {
+ return this._nodeFront;
+ },
+
+ isRoot: function () {
+ return this.isNode() &&
+ this.isConnected() &&
+ this._nodeFront.isDocumentElement;
+ },
+
+ isNode: function () {
+ return !!this._nodeFront;
+ },
+
+ isConnected: function () {
+ let node = this._nodeFront;
+ if (!node || !node.actorID) {
+ return false;
+ }
+
+ while (node) {
+ if (node === this._walker.rootNode) {
+ return true;
+ }
+ node = node.parentNode();
+ }
+ return false;
+ },
+
+ isHTMLNode: function () {
+ let xhtmlNs = "http://www.w3.org/1999/xhtml";
+ return this.isNode() && this.nodeFront.namespaceURI == xhtmlNs;
+ },
+
+ // Node type
+
+ isElementNode: function () {
+ return this.isNode() && this.nodeFront.nodeType == nodeConstants.ELEMENT_NODE;
+ },
+
+ isPseudoElementNode: function () {
+ return this.isNode() && this.nodeFront.isPseudoElement;
+ },
+
+ isAnonymousNode: function () {
+ return this.isNode() && this.nodeFront.isAnonymous;
+ },
+
+ isAttributeNode: function () {
+ return this.isNode() && this.nodeFront.nodeType == nodeConstants.ATTRIBUTE_NODE;
+ },
+
+ isTextNode: function () {
+ return this.isNode() && this.nodeFront.nodeType == nodeConstants.TEXT_NODE;
+ },
+
+ isCDATANode: function () {
+ return this.isNode() && this.nodeFront.nodeType == nodeConstants.CDATA_SECTION_NODE;
+ },
+
+ isEntityRefNode: function () {
+ return this.isNode() &&
+ this.nodeFront.nodeType == nodeConstants.ENTITY_REFERENCE_NODE;
+ },
+
+ isEntityNode: function () {
+ return this.isNode() && this.nodeFront.nodeType == nodeConstants.ENTITY_NODE;
+ },
+
+ isProcessingInstructionNode: function () {
+ return this.isNode() &&
+ this.nodeFront.nodeType == nodeConstants.PROCESSING_INSTRUCTION_NODE;
+ },
+
+ isCommentNode: function () {
+ return this.isNode() &&
+ this.nodeFront.nodeType == nodeConstants.PROCESSING_INSTRUCTION_NODE;
+ },
+
+ isDocumentNode: function () {
+ return this.isNode() && this.nodeFront.nodeType == nodeConstants.DOCUMENT_NODE;
+ },
+
+ /**
+ * @returns true if the selection is the <body> HTML element.
+ */
+ isBodyNode: function () {
+ return this.isHTMLNode() &&
+ this.isConnected() &&
+ this.nodeFront.nodeName === "BODY";
+ },
+
+ /**
+ * @returns true if the selection is the <head> HTML element.
+ */
+ isHeadNode: function () {
+ return this.isHTMLNode() &&
+ this.isConnected() &&
+ this.nodeFront.nodeName === "HEAD";
+ },
+
+ isDocumentTypeNode: function () {
+ return this.isNode() && this.nodeFront.nodeType == nodeConstants.DOCUMENT_TYPE_NODE;
+ },
+
+ isDocumentFragmentNode: function () {
+ return this.isNode() &&
+ this.nodeFront.nodeType == nodeConstants.DOCUMENT_FRAGMENT_NODE;
+ },
+
+ isNotationNode: function () {
+ return this.isNode() && this.nodeFront.nodeType == nodeConstants.NOTATION_NODE;
+ },
+};
diff --git a/devtools/client/framework/sidebar.js b/devtools/client/framework/sidebar.js
new file mode 100644
index 000000000..c27732b5d
--- /dev/null
+++ b/devtools/client/framework/sidebar.js
@@ -0,0 +1,592 @@
+/* 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 Services = require("Services");
+var {Task} = require("devtools/shared/task");
+var EventEmitter = require("devtools/shared/event-emitter");
+var Telemetry = require("devtools/client/shared/telemetry");
+
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper("devtools/client/locales/toolbox.properties");
+
+const XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+/**
+ * ToolSidebar provides methods to register tabs in the sidebar.
+ * It's assumed that the sidebar contains a xul:tabbox.
+ * Typically, you'll want the tabbox parameter to be a XUL tabbox like this:
+ *
+ * <tabbox id="inspector-sidebar" handleCtrlTab="false" class="devtools-sidebar-tabs">
+ * <tabs/>
+ * <tabpanels flex="1"/>
+ * </tabbox>
+ *
+ * The ToolSidebar API has a method to add new tabs, so the tabs and tabpanels
+ * nodes can be empty. But they can also already contain items before the
+ * ToolSidebar is created.
+ *
+ * Tabs added through the addTab method are only identified by an ID and a URL
+ * which is used as the href of an iframe node that is inserted in the newly
+ * created tabpanel.
+ * Tabs already present before the ToolSidebar is created may contain anything.
+ * However, these tabs must have ID attributes if it is required for the various
+ * methods that accept an ID as argument to work here.
+ *
+ * @param {Node} tabbox
+ * <tabbox> node;
+ * @param {ToolPanel} panel
+ * Related ToolPanel instance;
+ * @param {String} uid
+ * Unique ID
+ * @param {Object} options
+ * - hideTabstripe: Should the tabs be hidden. Defaults to false
+ * - showAllTabsMenu: Should a drop-down menu be displayed in case tabs
+ * become hidden. Defaults to false.
+ * - disableTelemetry: By default, switching tabs on and off in the sidebar
+ * will record tool usage in telemetry, pass this option to true to avoid it.
+ *
+ * Events raised:
+ * - new-tab-registered : After a tab has been added via addTab. The tab ID
+ * is passed with the event. This however, is raised before the tab iframe
+ * is fully loaded.
+ * - <tabid>-ready : After the tab iframe has been loaded
+ * - <tabid>-selected : After tab <tabid> was selected
+ * - select : Same as above, but for any tab, the ID is passed with the event
+ * - <tabid>-unselected : After tab <tabid> is unselected
+ */
+function ToolSidebar(tabbox, panel, uid, options = {}) {
+ EventEmitter.decorate(this);
+
+ this._tabbox = tabbox;
+ this._uid = uid;
+ this._panelDoc = this._tabbox.ownerDocument;
+ this._toolPanel = panel;
+ this._options = options;
+
+ this._onTabBoxOverflow = this._onTabBoxOverflow.bind(this);
+ this._onTabBoxUnderflow = this._onTabBoxUnderflow.bind(this);
+
+ try {
+ this._width = Services.prefs.getIntPref("devtools.toolsidebar-width." + this._uid);
+ } catch (e) {}
+
+ if (!options.disableTelemetry) {
+ this._telemetry = new Telemetry();
+ }
+
+ this._tabbox.tabpanels.addEventListener("select", this, true);
+
+ this._tabs = new Map();
+
+ // Check for existing tabs in the DOM and add them.
+ this.addExistingTabs();
+
+ if (this._options.hideTabstripe) {
+ this._tabbox.setAttribute("hidetabs", "true");
+ }
+
+ if (this._options.showAllTabsMenu) {
+ this.addAllTabsMenu();
+ }
+
+ this._toolPanel.emit("sidebar-created", this);
+}
+
+exports.ToolSidebar = ToolSidebar;
+
+ToolSidebar.prototype = {
+ TAB_ID_PREFIX: "sidebar-tab-",
+
+ TABPANEL_ID_PREFIX: "sidebar-panel-",
+
+ /**
+ * Add a "…" button at the end of the tabstripe that toggles a dropdown menu
+ * containing the list of all tabs if any become hidden due to lack of room.
+ *
+ * If the ToolSidebar was created with the "showAllTabsMenu" option set to
+ * true, this is already done automatically. If not, you may call this
+ * function at any time to add the menu.
+ */
+ addAllTabsMenu: function () {
+ if (this._allTabsBtn) {
+ return;
+ }
+
+ let tabs = this._tabbox.tabs;
+
+ // Create a container and insert it first in the tabbox
+ let allTabsContainer = this._panelDoc.createElementNS(XULNS, "stack");
+ this._tabbox.insertBefore(allTabsContainer, tabs);
+
+ // Move the tabs inside and make them flex
+ allTabsContainer.appendChild(tabs);
+ tabs.setAttribute("flex", "1");
+
+ // Create the dropdown menu next to the tabs
+ this._allTabsBtn = this._panelDoc.createElementNS(XULNS, "toolbarbutton");
+ this._allTabsBtn.setAttribute("class", "devtools-sidebar-alltabs");
+ this._allTabsBtn.setAttribute("end", "0");
+ this._allTabsBtn.setAttribute("top", "0");
+ this._allTabsBtn.setAttribute("width", "15");
+ this._allTabsBtn.setAttribute("type", "menu");
+ this._allTabsBtn.setAttribute("tooltiptext",
+ L10N.getStr("sidebar.showAllTabs.tooltip"));
+ this._allTabsBtn.setAttribute("hidden", "true");
+ allTabsContainer.appendChild(this._allTabsBtn);
+
+ let menuPopup = this._panelDoc.createElementNS(XULNS, "menupopup");
+ this._allTabsBtn.appendChild(menuPopup);
+
+ // Listening to tabs overflow event to toggle the alltabs button
+ tabs.addEventListener("overflow", this._onTabBoxOverflow, false);
+ tabs.addEventListener("underflow", this._onTabBoxUnderflow, false);
+
+ // Add menuitems to the alltabs menu if there are already tabs in the
+ // sidebar
+ for (let [id, tab] of this._tabs) {
+ let item = this._addItemToAllTabsMenu(id, tab, {
+ selected: tab.hasAttribute("selected")
+ });
+ if (tab.hidden) {
+ item.hidden = true;
+ }
+ }
+ },
+
+ removeAllTabsMenu: function () {
+ if (!this._allTabsBtn) {
+ return;
+ }
+
+ let tabs = this._tabbox.tabs;
+
+ tabs.removeEventListener("overflow", this._onTabBoxOverflow, false);
+ tabs.removeEventListener("underflow", this._onTabBoxUnderflow, false);
+
+ // Moving back the tabs as a first child of the tabbox
+ this._tabbox.insertBefore(tabs, this._tabbox.tabpanels);
+ this._tabbox.querySelector("stack").remove();
+
+ this._allTabsBtn = null;
+ },
+
+ _onTabBoxOverflow: function () {
+ this._allTabsBtn.removeAttribute("hidden");
+ },
+
+ _onTabBoxUnderflow: function () {
+ this._allTabsBtn.setAttribute("hidden", "true");
+ },
+
+ /**
+ * Add an item in the allTabs menu for a given tab.
+ */
+ _addItemToAllTabsMenu: function (id, tab, options) {
+ if (!this._allTabsBtn) {
+ return;
+ }
+
+ let item = this._panelDoc.createElementNS(XULNS, "menuitem");
+ let idPrefix = "sidebar-alltabs-item-";
+ item.setAttribute("id", idPrefix + id);
+ item.setAttribute("label", tab.getAttribute("label"));
+ item.setAttribute("type", "checkbox");
+ if (options.selected) {
+ item.setAttribute("checked", true);
+ }
+ // The auto-checking of menuitems in this menu doesn't work, so let's do
+ // it manually
+ item.setAttribute("autocheck", false);
+
+ let menu = this._allTabsBtn.querySelector("menupopup");
+ if (options.insertBefore) {
+ let referenceItem = menu.querySelector(`#${idPrefix}${options.insertBefore}`);
+ menu.insertBefore(item, referenceItem);
+ } else {
+ menu.appendChild(item);
+ }
+
+ item.addEventListener("click", () => {
+ this._tabbox.selectedTab = tab;
+ }, false);
+
+ tab.allTabsMenuItem = item;
+
+ return item;
+ },
+
+ /**
+ * Register a tab. A tab is a document.
+ * The document must have a title, which will be used as the name of the tab.
+ *
+ * @param {string} id The unique id for this tab.
+ * @param {string} url The URL of the document to load in this new tab.
+ * @param {Object} options A set of options for this new tab:
+ * - {Boolean} selected Set to true to make this new tab selected by default.
+ * - {String} insertBefore By default, the new tab is appended at the end of the
+ * tabbox, pass the ID of an existing tab to insert it before that tab instead.
+ */
+ addTab: function (id, url, options = {}) {
+ let iframe = this._panelDoc.createElementNS(XULNS, "iframe");
+ iframe.className = "iframe-" + id;
+ iframe.setAttribute("flex", "1");
+ iframe.setAttribute("src", url);
+ iframe.tooltip = "aHTMLTooltip";
+
+ // Creating the tab and adding it to the tabbox
+ let tab = this._panelDoc.createElementNS(XULNS, "tab");
+
+ tab.setAttribute("id", this.TAB_ID_PREFIX + id);
+ tab.setAttribute("crop", "end");
+ // Avoid showing "undefined" while the tab is loading
+ tab.setAttribute("label", "");
+
+ if (options.insertBefore) {
+ let referenceTab = this.getTab(options.insertBefore);
+ this._tabbox.tabs.insertBefore(tab, referenceTab);
+ } else {
+ this._tabbox.tabs.appendChild(tab);
+ }
+
+ // Add the tab to the allTabs menu if exists
+ let allTabsItem = this._addItemToAllTabsMenu(id, tab, options);
+
+ let onIFrameLoaded = (event) => {
+ let doc = event.target;
+ let win = doc.defaultView;
+ tab.setAttribute("label", doc.title);
+
+ if (allTabsItem) {
+ allTabsItem.setAttribute("label", doc.title);
+ }
+
+ iframe.removeEventListener("load", onIFrameLoaded, true);
+ if ("setPanel" in win) {
+ win.setPanel(this._toolPanel, iframe);
+ }
+ this.emit(id + "-ready");
+ };
+
+ iframe.addEventListener("load", onIFrameLoaded, true);
+
+ let tabpanel = this._panelDoc.createElementNS(XULNS, "tabpanel");
+ tabpanel.setAttribute("id", this.TABPANEL_ID_PREFIX + id);
+ tabpanel.appendChild(iframe);
+
+ if (options.insertBefore) {
+ let referenceTabpanel = this.getTabPanel(options.insertBefore);
+ this._tabbox.tabpanels.insertBefore(tabpanel, referenceTabpanel);
+ } else {
+ this._tabbox.tabpanels.appendChild(tabpanel);
+ }
+
+ this._tooltip = this._panelDoc.createElementNS(XULNS, "tooltip");
+ this._tooltip.id = "aHTMLTooltip";
+ tabpanel.appendChild(this._tooltip);
+ this._tooltip.page = true;
+
+ tab.linkedPanel = this.TABPANEL_ID_PREFIX + id;
+
+ // We store the index of this tab.
+ this._tabs.set(id, tab);
+
+ if (options.selected) {
+ this._selectTabSoon(id);
+ }
+
+ this.emit("new-tab-registered", id);
+ },
+
+ untitledTabsIndex: 0,
+
+ /**
+ * Search for existing tabs in the markup that aren't know yet and add them.
+ */
+ addExistingTabs: function () {
+ let knownTabs = [...this._tabs.values()];
+
+ for (let tab of this._tabbox.tabs.querySelectorAll("tab")) {
+ if (knownTabs.indexOf(tab) !== -1) {
+ continue;
+ }
+
+ // Find an ID for this unknown tab
+ let id = tab.getAttribute("id") || "untitled-tab-" + (this.untitledTabsIndex++);
+
+ // If the existing tab contains the tab ID prefix, extract the ID of the
+ // tab
+ if (id.startsWith(this.TAB_ID_PREFIX)) {
+ id = id.split(this.TAB_ID_PREFIX).pop();
+ }
+
+ // Register the tab
+ this._tabs.set(id, tab);
+ this.emit("new-tab-registered", id);
+ }
+ },
+
+ /**
+ * Remove an existing tab.
+ * @param {String} tabId The ID of the tab that was used to register it, or
+ * the tab id attribute value if the tab existed before the sidebar got created.
+ * @param {String} tabPanelId Optional. If provided, this ID will be used
+ * instead of the tabId to retrieve and remove the corresponding <tabpanel>
+ */
+ removeTab: Task.async(function* (tabId, tabPanelId) {
+ // Remove the tab if it can be found
+ let tab = this.getTab(tabId);
+ if (!tab) {
+ return;
+ }
+
+ let win = this.getWindowForTab(tabId);
+ if (win && ("destroy" in win)) {
+ yield win.destroy();
+ }
+
+ tab.remove();
+
+ // Also remove the tabpanel
+ let panel = this.getTabPanel(tabPanelId || tabId);
+ if (panel) {
+ panel.remove();
+ }
+
+ this._tabs.delete(tabId);
+ this.emit("tab-unregistered", tabId);
+ }),
+
+ /**
+ * Show or hide a specific tab.
+ * @param {Boolean} isVisible True to show the tab/tabpanel, False to hide it.
+ * @param {String} id The ID of the tab to be hidden.
+ */
+ toggleTab: function (isVisible, id) {
+ // Toggle the tab.
+ let tab = this.getTab(id);
+ if (!tab) {
+ return;
+ }
+ tab.hidden = !isVisible;
+
+ // Toggle the item in the allTabs menu.
+ if (this._allTabsBtn) {
+ this._allTabsBtn.querySelector("#sidebar-alltabs-item-" + id).hidden = !isVisible;
+ }
+ },
+
+ /**
+ * Select a specific tab.
+ */
+ select: function (id) {
+ let tab = this.getTab(id);
+ if (tab) {
+ this._tabbox.selectedTab = tab;
+ }
+ },
+
+ /**
+ * Hack required to select a tab right after it was created.
+ *
+ * @param {String} id
+ * The sidebar tab id to select.
+ */
+ _selectTabSoon: function (id) {
+ this._panelDoc.defaultView.setTimeout(() => {
+ this.select(id);
+ }, 0);
+ },
+
+ /**
+ * Return the id of the selected tab.
+ */
+ getCurrentTabID: function () {
+ let currentID = null;
+ for (let [id, tab] of this._tabs) {
+ if (this._tabbox.tabs.selectedItem == tab) {
+ currentID = id;
+ break;
+ }
+ }
+ return currentID;
+ },
+
+ /**
+ * Returns the requested tab panel based on the id.
+ * @param {String} id
+ * @return {DOMNode}
+ */
+ getTabPanel: function (id) {
+ // Search with and without the ID prefix as there might have been existing
+ // tabpanels by the time the sidebar got created
+ return this._tabbox.tabpanels.querySelector("#" + this.TABPANEL_ID_PREFIX + id + ", #" + id);
+ },
+
+ /**
+ * Return the tab based on the provided id, if one was registered with this id.
+ * @param {String} id
+ * @return {DOMNode}
+ */
+ getTab: function (id) {
+ return this._tabs.get(id);
+ },
+
+ /**
+ * Event handler.
+ */
+ handleEvent: function (event) {
+ if (event.type !== "select" || this._destroyed) {
+ return;
+ }
+
+ if (this._currentTool == this.getCurrentTabID()) {
+ // Tool hasn't changed.
+ return;
+ }
+
+ let previousTool = this._currentTool;
+ this._currentTool = this.getCurrentTabID();
+ if (previousTool) {
+ if (this._telemetry) {
+ this._telemetry.toolClosed(previousTool);
+ }
+ this.emit(previousTool + "-unselected");
+ }
+
+ if (this._telemetry) {
+ this._telemetry.toolOpened(this._currentTool);
+ }
+
+ this.emit(this._currentTool + "-selected");
+ this.emit("select", this._currentTool);
+
+ // Handlers for "select"/"...-selected"/"...-unselected" events might have
+ // destroyed the sidebar in the meantime.
+ if (this._destroyed) {
+ return;
+ }
+
+ // Handle menuitem selection if the allTabsMenu is there by unchecking all
+ // items except the selected one.
+ let tab = this._tabbox.selectedTab;
+ if (tab.allTabsMenuItem) {
+ for (let otherItem of this._allTabsBtn.querySelectorAll("menuitem")) {
+ otherItem.removeAttribute("checked");
+ }
+ tab.allTabsMenuItem.setAttribute("checked", true);
+ }
+ },
+
+ /**
+ * Toggle sidebar's visibility state.
+ */
+ toggle: function () {
+ if (this._tabbox.hasAttribute("hidden")) {
+ this.show();
+ } else {
+ this.hide();
+ }
+ },
+
+ /**
+ * Show the sidebar.
+ *
+ * @param {String} id
+ * The sidebar tab id to select.
+ */
+ show: function (id) {
+ if (this._width) {
+ this._tabbox.width = this._width;
+ }
+ this._tabbox.removeAttribute("hidden");
+
+ // If an id is given, select the corresponding sidebar tab and record the
+ // tool opened.
+ if (id) {
+ this._currentTool = id;
+
+ if (this._telemetry) {
+ this._telemetry.toolOpened(this._currentTool);
+ }
+
+ this._selectTabSoon(id);
+ }
+
+ this.emit("show");
+ },
+
+ /**
+ * Show the sidebar.
+ */
+ hide: function () {
+ Services.prefs.setIntPref("devtools.toolsidebar-width." + this._uid, this._tabbox.width);
+ this._tabbox.setAttribute("hidden", "true");
+ this._panelDoc.activeElement.blur();
+
+ this.emit("hide");
+ },
+
+ /**
+ * Return the window containing the tab content.
+ */
+ getWindowForTab: function (id) {
+ if (!this._tabs.has(id)) {
+ return null;
+ }
+
+ // Get the tabpanel and make sure it contains an iframe
+ let panel = this.getTabPanel(id);
+ if (!panel || !panel.firstChild || !panel.firstChild.contentWindow) {
+ return;
+ }
+ return panel.firstChild.contentWindow;
+ },
+
+ /**
+ * Clean-up.
+ */
+ destroy: Task.async(function* () {
+ if (this._destroyed) {
+ return;
+ }
+ this._destroyed = true;
+
+ Services.prefs.setIntPref("devtools.toolsidebar-width." + this._uid, this._tabbox.width);
+
+ if (this._allTabsBtn) {
+ this.removeAllTabsMenu();
+ }
+
+ this._tabbox.tabpanels.removeEventListener("select", this, true);
+
+ // Note that we check for the existence of this._tabbox.tabpanels at each
+ // step as the container window may have been closed by the time one of the
+ // panel's destroy promise resolves.
+ while (this._tabbox.tabpanels && this._tabbox.tabpanels.hasChildNodes()) {
+ let panel = this._tabbox.tabpanels.firstChild;
+ let win = panel.firstChild.contentWindow;
+ if (win && ("destroy" in win)) {
+ yield win.destroy();
+ }
+ panel.remove();
+ }
+
+ while (this._tabbox.tabs && this._tabbox.tabs.hasChildNodes()) {
+ this._tabbox.tabs.removeChild(this._tabbox.tabs.firstChild);
+ }
+
+ if (this._currentTool && this._telemetry) {
+ this._telemetry.toolClosed(this._currentTool);
+ }
+
+ this._toolPanel.emit("sidebar-destroyed", this);
+
+ this._tabs = null;
+ this._tabbox = null;
+ this._panelDoc = null;
+ this._toolPanel = null;
+ })
+};
diff --git a/devtools/client/framework/source-map-service.js b/devtools/client/framework/source-map-service.js
new file mode 100644
index 000000000..838adc392
--- /dev/null
+++ b/devtools/client/framework/source-map-service.js
@@ -0,0 +1,209 @@
+/* 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 { Task } = require("devtools/shared/task");
+const EventEmitter = require("devtools/shared/event-emitter");
+const { LocationStore, serialize, deserialize } = require("./location-store");
+
+/**
+ * A manager class that wraps a TabTarget and listens to source changes
+ * from source maps and resolves non-source mapped locations to the source mapped
+ * versions and back and forth, and creating smart elements with a location that
+ * auto-update when the source changes (from pretty printing, source maps loading, etc)
+ *
+ * @param {TabTarget} target
+ */
+
+function SourceMapService(target) {
+ this._target = target;
+ this._locationStore = new LocationStore();
+ this._isNotSourceMapped = new Map();
+
+ EventEmitter.decorate(this);
+
+ this._onSourceUpdated = this._onSourceUpdated.bind(this);
+ this._resolveLocation = this._resolveLocation.bind(this);
+ this._resolveAndUpdate = this._resolveAndUpdate.bind(this);
+ this.subscribe = this.subscribe.bind(this);
+ this.unsubscribe = this.unsubscribe.bind(this);
+ this.reset = this.reset.bind(this);
+ this.destroy = this.destroy.bind(this);
+
+ target.on("source-updated", this._onSourceUpdated);
+ target.on("navigate", this.reset);
+ target.on("will-navigate", this.reset);
+}
+
+/**
+ * Clears the store containing the cached promised locations
+ */
+SourceMapService.prototype.reset = function () {
+ // Guard to prevent clearing the store when it is not initialized yet.
+ if (!this._locationStore) {
+ return;
+ }
+ this._locationStore.clear();
+ this._isNotSourceMapped.clear();
+};
+
+SourceMapService.prototype.destroy = function () {
+ this.reset();
+ this._target.off("source-updated", this._onSourceUpdated);
+ this._target.off("navigate", this.reset);
+ this._target.off("will-navigate", this.reset);
+ this._target.off("close", this.destroy);
+ this._target = this._locationStore = this._isNotSourceMapped = null;
+};
+
+/**
+ * Sets up listener for the callback to update the FrameView
+ * and tries to resolve location, if it is source-mappable
+ * @param location
+ * @param callback
+ */
+SourceMapService.prototype.subscribe = function (location, callback) {
+ // A valid candidate location for source-mapping should have a url and line.
+ // Abort if there's no `url`, which means it's unsourcemappable anyway,
+ // like an eval script.
+ // From previous attempts to source-map locations, we also determine if a location
+ // is not source-mapped.
+ if (!location.url || !location.line || this._isNotSourceMapped.get(location.url)) {
+ return;
+ }
+ this.on(serialize(location), callback);
+ this._locationStore.set(location);
+ this._resolveAndUpdate(location);
+};
+
+/**
+ * Removes the listener for the location and clears cached locations
+ * @param location
+ * @param callback
+ */
+SourceMapService.prototype.unsubscribe = function (location, callback) {
+ this.off(serialize(location), callback);
+ // Check to see if the store exists before attempting to clear a location
+ // Sometimes un-subscribe happens during the destruction cascades and this
+ // condition is to protect against that. Could be looked into in the future.
+ if (!this._locationStore) {
+ return;
+ }
+ this._locationStore.clearByURL(location.url);
+};
+
+/**
+ * Tries to resolve the location and if successful,
+ * emits the resolved location
+ * @param location
+ * @private
+ */
+SourceMapService.prototype._resolveAndUpdate = function (location) {
+ this._resolveLocation(location).then(resolvedLocation => {
+ // We try to source map the first console log to initiate the source-updated
+ // event from target. The isSameLocation check is to make sure we don't update
+ // the frame, if the location is not source-mapped.
+ if (resolvedLocation && !isSameLocation(location, resolvedLocation)) {
+ this.emit(serialize(location), location, resolvedLocation);
+ }
+ });
+};
+
+/**
+ * Checks if there is existing promise to resolve location, if so returns cached promise
+ * if not, tries to resolve location and returns a promised location
+ * @param location
+ * @return Promise<Object>
+ * @private
+ */
+SourceMapService.prototype._resolveLocation = Task.async(function* (location) {
+ let resolvedLocation;
+ const cachedLocation = this._locationStore.get(location);
+ if (cachedLocation) {
+ resolvedLocation = cachedLocation;
+ } else {
+ const promisedLocation = resolveLocation(this._target, location);
+ if (promisedLocation) {
+ this._locationStore.set(location, promisedLocation);
+ resolvedLocation = promisedLocation;
+ }
+ }
+ return resolvedLocation;
+});
+
+/**
+ * Checks if the `source-updated` event is fired from the target.
+ * Checks to see if location store has the source url in its cache,
+ * if so, tries to update each stale location in the store.
+ * Determines if the source should be source-mapped or not.
+ * @param _
+ * @param sourceEvent
+ * @private
+ */
+SourceMapService.prototype._onSourceUpdated = function (_, sourceEvent) {
+ let { type, source } = sourceEvent;
+
+ // If we get a new source, and it's not a source map, abort;
+ // we can have no actionable updates as this is just a new normal source.
+ // Check Source Actor for sourceMapURL property (after Firefox 48)
+ // If not present, utilize isSourceMapped and isPrettyPrinted properties
+ // to estimate if a source is not source-mapped.
+ const isNotSourceMapped = !(source.sourceMapURL ||
+ source.isSourceMapped || source.isPrettyPrinted);
+ if (type === "newSource" && isNotSourceMapped) {
+ this._isNotSourceMapped.set(source.url, true);
+ return;
+ }
+ let sourceUrl = null;
+ if (source.generatedUrl && source.isSourceMapped) {
+ sourceUrl = source.generatedUrl;
+ } else if (source.url && source.isPrettyPrinted) {
+ sourceUrl = source.url;
+ }
+ const locationsToResolve = this._locationStore.getByURL(sourceUrl);
+ if (locationsToResolve.length) {
+ this._locationStore.clearByURL(sourceUrl);
+ for (let location of locationsToResolve) {
+ this._resolveAndUpdate(deserialize(location));
+ }
+ }
+};
+
+exports.SourceMapService = SourceMapService;
+
+/**
+ * Take a TabTarget and a location, containing a `url`, `line`, and `column`, resolve
+ * the location to the latest location (so a source mapped location, or if pretty print
+ * status has been updated)
+ *
+ * @param {TabTarget} target
+ * @param {Object} location
+ * @return {Promise<Object>}
+ */
+function resolveLocation(target, location) {
+ return Task.spawn(function* () {
+ let newLocation = yield target.resolveLocation({
+ url: location.url,
+ line: location.line,
+ column: location.column || Infinity
+ });
+ // Source or mapping not found, so don't do anything
+ if (newLocation.error) {
+ return null;
+ }
+ return newLocation;
+ });
+}
+
+/**
+ * Returns true if the original location and resolved location are the same
+ * @param location
+ * @param resolvedLocation
+ * @returns {boolean}
+ */
+function isSameLocation(location, resolvedLocation) {
+ return location.url === resolvedLocation.url &&
+ location.line === resolvedLocation.line &&
+ location.column === resolvedLocation.column;
+}
diff --git a/devtools/client/framework/source-map-util.js b/devtools/client/framework/source-map-util.js
new file mode 100644
index 000000000..0bb25b3df
--- /dev/null
+++ b/devtools/client/framework/source-map-util.js
@@ -0,0 +1,20 @@
+function originalToGeneratedId(originalId) {
+ const match = originalId.match(/(.*)\/originalSource/);
+ return match ? match[1] : "";
+}
+
+function generatedToOriginalId(generatedId, url) {
+ return generatedId + "/originalSource-" + url.replace(/ \//, '-');
+}
+
+function isOriginalId(id) {
+ return !!id.match(/\/originalSource/);
+}
+
+function isGeneratedId(id) {
+ return !isOriginalId(id);
+}
+
+module.exports = {
+ originalToGeneratedId, generatedToOriginalId, isOriginalId, isGeneratedId
+};
diff --git a/devtools/client/framework/source-map-worker.js b/devtools/client/framework/source-map-worker.js
new file mode 100644
index 000000000..c68732f38
--- /dev/null
+++ b/devtools/client/framework/source-map-worker.js
@@ -0,0 +1,220 @@
+const { fetch, assert } = require("devtools/shared/DevToolsUtils");
+const { joinURI } = require("devtools/shared/path");
+const path = require("sdk/fs/path");
+const { SourceMapConsumer, SourceMapGenerator } = require("source-map");
+const { isJavaScript } = require("./source");
+const {
+ originalToGeneratedId,
+ generatedToOriginalId,
+ isGeneratedId,
+ isOriginalId
+} = require("./source-map-util");
+
+let sourceMapRequests = new Map();
+let sourceMapsEnabled = false;
+
+function clearSourceMaps() {
+ sourceMapRequests.clear();
+}
+
+function enableSourceMaps() {
+ sourceMapsEnabled = true;
+}
+
+function _resolveSourceMapURL(source) {
+ const { url = "", sourceMapURL = "" } = source;
+ if (path.isURL(sourceMapURL) || url == "") {
+ // If it's already a full URL or the source doesn't have a URL,
+ // don't resolve anything.
+ return sourceMapURL;
+ } else if (path.isAbsolute(sourceMapURL)) {
+ // If it's an absolute path, it should be resolved relative to the
+ // host of the source.
+ const { protocol = "", host = "" } = parse(url);
+ return `${protocol}//${host}${sourceMapURL}`;
+ }
+ // Otherwise, it's a relative path and should be resolved relative
+ // to the source.
+ return dirname(url) + "/" + sourceMapURL;
+}
+
+/**
+ * Sets the source map's sourceRoot to be relative to the source map url.
+ * @memberof utils/source-map-worker
+ * @static
+ */
+function _setSourceMapRoot(sourceMap, absSourceMapURL, source) {
+ // No need to do this fiddling if we won't be fetching any sources over the
+ // wire.
+ if (sourceMap.hasContentsOfAllSources()) {
+ return;
+ }
+
+ const base = dirname(
+ (absSourceMapURL.indexOf("data:") === 0 && source.url) ?
+ source.url :
+ absSourceMapURL
+ );
+
+ if (sourceMap.sourceRoot) {
+ sourceMap.sourceRoot = joinURI(base, sourceMap.sourceRoot);
+ } else {
+ sourceMap.sourceRoot = base;
+ }
+
+ return sourceMap;
+}
+
+function _getSourceMap(generatedSourceId)
+ : ?Promise<SourceMapConsumer> {
+ return sourceMapRequests.get(generatedSourceId);
+}
+
+async function _resolveAndFetch(generatedSource) : SourceMapConsumer {
+ // Fetch the sourcemap over the network and create it.
+ const sourceMapURL = _resolveSourceMapURL(generatedSource);
+ const fetched = await fetch(
+ sourceMapURL, { loadFromCache: false }
+ );
+
+ // Create the source map and fix it up.
+ const map = new SourceMapConsumer(fetched.content);
+ _setSourceMapRoot(map, sourceMapURL, generatedSource);
+ return map;
+}
+
+function _fetchSourceMap(generatedSource) {
+ const existingRequest = sourceMapRequests.get(generatedSource.id);
+ if (existingRequest) {
+ // If it has already been requested, return the request. Make sure
+ // to do this even if sourcemapping is turned off, because
+ // pretty-printing uses sourcemaps.
+ //
+ // An important behavior here is that if it's in the middle of
+ // requesting it, all subsequent calls will block on the initial
+ // request.
+ return existingRequest;
+ } else if (!generatedSource.sourceMapURL || !sourceMapsEnabled) {
+ return Promise.resolve(null);
+ }
+
+ // Fire off the request, set it in the cache, and return it.
+ // Suppress any errors and just return null (ignores bogus
+ // sourcemaps).
+ const req = _resolveAndFetch(generatedSource).catch(() => null);
+ sourceMapRequests.set(generatedSource.id, req);
+ return req;
+}
+
+async function getOriginalURLs(generatedSource) {
+ const map = await _fetchSourceMap(generatedSource);
+ return map && map.sources;
+}
+
+async function getGeneratedLocation(location: Location, originalSource: Source)
+ : Promise<Location> {
+ if (!isOriginalId(location.sourceId)) {
+ return location;
+ }
+
+ const generatedSourceId = originalToGeneratedId(location.sourceId);
+ const map = await _getSourceMap(generatedSourceId);
+ if (!map) {
+ return location;
+ }
+
+ const { line, column } = map.generatedPositionFor({
+ source: originalSource.url,
+ line: location.line,
+ column: location.column == null ? 0 : location.column
+ });
+
+ return {
+ sourceId: generatedSourceId,
+ line: line,
+ // Treat 0 as no column so that line breakpoints work correctly.
+ column: column === 0 ? undefined : column
+ };
+}
+
+async function getOriginalLocation(location) {
+ if (!isGeneratedId(location.sourceId)) {
+ return location;
+ }
+
+ const map = await _getSourceMap(location.sourceId);
+ if (!map) {
+ return location;
+ }
+
+ const { source: url, line, column } = map.originalPositionFor({
+ line: location.line,
+ column: location.column == null ? Infinity : location.column
+ });
+
+ if (url == null) {
+ // No url means the location didn't map.
+ return location;
+ }
+
+ return {
+ sourceId: generatedToOriginalId(location.sourceId, url),
+ line,
+ column
+ };
+}
+
+async function getOriginalSourceText(originalSource) {
+ assert(isOriginalId(originalSource.id),
+ "Source is not an original source");
+
+ const generatedSourceId = originalToGeneratedId(originalSource.id);
+ const map = await _getSourceMap(generatedSourceId);
+ if (!map) {
+ return null;
+ }
+
+ let text = map.sourceContentFor(originalSource.url);
+ if (!text) {
+ text = (await fetch(
+ originalSource.url, { loadFromCache: false }
+ )).content;
+ }
+
+ return {
+ text,
+ contentType: isJavaScript(originalSource.url || "") ?
+ "text/javascript" :
+ "text/plain"
+ };
+}
+
+function applySourceMap(generatedId, url, code, mappings) {
+ const generator = new SourceMapGenerator({ file: url });
+ mappings.forEach(mapping => generator.addMapping(mapping));
+ generator.setSourceContent(url, code);
+
+ const map = SourceMapConsumer(generator.toJSON());
+ sourceMapRequests.set(generatedId, Promise.resolve(map));
+}
+
+const publicInterface = {
+ getOriginalURLs,
+ getGeneratedLocation,
+ getOriginalLocation,
+ getOriginalSourceText,
+ enableSourceMaps,
+ applySourceMap,
+ clearSourceMaps
+};
+
+self.onmessage = function(msg) {
+ const { id, method, args } = msg.data;
+ const response = publicInterface[method].apply(undefined, args);
+ if (response instanceof Promise) {
+ response.then(val => self.postMessage({ id, response: val }),
+ err => self.postMessage({ id, error: err }));
+ } else {
+ self.postMessage({ id, response });
+ }
+};
diff --git a/devtools/client/framework/source-map.js b/devtools/client/framework/source-map.js
new file mode 100644
index 000000000..7c6805c85
--- /dev/null
+++ b/devtools/client/framework/source-map.js
@@ -0,0 +1,84 @@
+// @flow
+
+const {
+ originalToGeneratedId,
+ generatedToOriginalId,
+ isGeneratedId,
+ isOriginalId
+} = require("./source-map-util");
+
+function workerTask(worker, method) {
+ return function(...args: any) {
+ return new Promise((resolve, reject) => {
+ const id = msgId++;
+ worker.postMessage({ id, method, args });
+
+ const listener = ({ data: result }) => {
+ if (result.id !== id) {
+ return;
+ }
+
+ worker.removeEventListener("message", listener);
+ if (result.error) {
+ reject(result.error);
+ } else {
+ resolve(result.response);
+ }
+ };
+
+ worker.addEventListener("message", listener);
+ });
+ };
+}
+
+let sourceMapWorker;
+function restartWorker() {
+ if (sourceMapWorker) {
+ sourceMapWorker.terminate();
+ }
+ sourceMapWorker = new Worker(
+ "resource://devtools/client/framework/source-map-worker.js"
+ );
+
+ if (Services.prefs.getBoolPref("devtools.debugger.client-source-maps-enabled")) {
+ sourceMapWorker.postMessage({ id: 0, method: "enableSourceMaps" });
+ }
+}
+restartWorker();
+
+function destroyWorker() {
+ if (sourceMapWorker) {
+ sourceMapWorker.terminate();
+ sourceMapWorker = null;
+ }
+}
+
+function shouldSourceMap() {
+ return Services.prefs.getBoolPref("devtools.debugger.client-source-maps-enabled");
+}
+
+const getOriginalURLs = workerTask(sourceMapWorker, "getOriginalURLs");
+const getGeneratedLocation = workerTask(sourceMapWorker,
+ "getGeneratedLocation");
+const getOriginalLocation = workerTask(sourceMapWorker,
+ "getOriginalLocation");
+const getOriginalSourceText = workerTask(sourceMapWorker,
+ "getOriginalSourceText");
+const applySourceMap = workerTask(sourceMapWorker, "applySourceMap");
+const clearSourceMaps = workerTask(sourceMapWorker, "clearSourceMaps");
+
+module.exports = {
+ originalToGeneratedId,
+ generatedToOriginalId,
+ isGeneratedId,
+ isOriginalId,
+
+ getOriginalURLs,
+ getGeneratedLocation,
+ getOriginalLocation,
+ getOriginalSourceText,
+ applySourceMap,
+ clearSourceMaps,
+ destroyWorker,
+ shouldSourceMap
+};
diff --git a/devtools/client/framework/target-from-url.js b/devtools/client/framework/target-from-url.js
new file mode 100644
index 000000000..4e2c30377
--- /dev/null
+++ b/devtools/client/framework/target-from-url.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 { Cu, Ci } = require("chrome");
+
+const { TargetFactory } = require("devtools/client/framework/target");
+const { DebuggerServer } = require("devtools/server/main");
+const { DebuggerClient } = require("devtools/shared/client/main");
+const { Task } = require("devtools/shared/task");
+
+/**
+ * Construct a Target for a given URL object having various query parameters:
+ *
+ * host:
+ * {String} The hostname or IP address to connect to.
+ * port:
+ * {Number} The TCP port to connect to, to use with `host` argument.
+ * ws:
+ * {Boolean} If true, connect via websocket instread of regular TCP connection.
+ *
+ * type: tab, process
+ * {String} The type of target to connect to. Currently tabs and processes are supported types.
+ *
+ * If type="tab":
+ * id:
+ * {Number} the tab outerWindowID
+ * chrome: Optional
+ * {Boolean} Force the creation of a chrome target. Gives more privileges to the tab
+ * actor. Allows chrome execution in the webconsole and see chrome files in
+ * the debugger. (handy when contributing to firefox)
+ *
+ * If type="process":
+ * id:
+ * {Number} the process id to debug. Default to 0, which is the parent process.
+ *
+ * @param {URL} url
+ * The url to fetch query params from.
+ *
+ * @return A target object
+ */
+exports.targetFromURL = Task.async(function* (url) {
+ let params = url.searchParams;
+ let type = params.get("type");
+ if (!type) {
+ throw new Error("targetFromURL, missing type parameter");
+ }
+ let id = params.get("id");
+ // Allows to spawn a chrome enabled target for any context
+ // (handy to debug chrome stuff in a child process)
+ let chrome = params.has("chrome");
+
+ let client = yield createClient(params);
+
+ yield client.connect();
+
+ let form, isTabActor;
+ if (type === "tab") {
+ // Fetch target for a remote tab
+ id = parseInt(id);
+ if (isNaN(id)) {
+ throw new Error("targetFromURL, wrong tab id:'" + id + "', should be a number");
+ }
+ try {
+ let response = yield client.getTab({ outerWindowID: id });
+ form = response.tab;
+ } catch (ex) {
+ if (ex.error == "noTab") {
+ throw new Error("targetFromURL, tab with outerWindowID:'" + id + "' doesn't exist");
+ }
+ throw ex;
+ }
+ } else if (type == "process") {
+ // Fetch target for a remote chrome actor
+ DebuggerServer.allowChromeProcess = true;
+ try {
+ id = parseInt(id);
+ if (isNaN(id)) {
+ id = 0;
+ }
+ let response = yield client.getProcess(id);
+ form = response.form;
+ chrome = true;
+ if (id != 0) {
+ // Child process are not exposing tab actors and only support debugger+console
+ isTabActor = false;
+ }
+ } catch (ex) {
+ if (ex.error == "noProcess") {
+ throw new Error("targetFromURL, process with id:'" + id + "' doesn't exist");
+ }
+ throw ex;
+ }
+ } else {
+ throw new Error("targetFromURL, unsupported type='" + type + "' parameter");
+ }
+
+ return TargetFactory.forRemoteTab({ client, form, chrome, isTabActor });
+});
+
+function* createClient(params) {
+ let host = params.get("host");
+ let port = params.get("port");
+ let webSocket = !!params.get("ws");
+
+ let transport;
+ if (port) {
+ transport = yield DebuggerClient.socketConnect({ host, port, webSocket });
+ } else {
+ // Setup a server if we don't have one already running
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+ transport = DebuggerServer.connectPipe()
+ }
+ return new DebuggerClient(transport);
+}
diff --git a/devtools/client/framework/target.js b/devtools/client/framework/target.js
new file mode 100644
index 000000000..30a720b7e
--- /dev/null
+++ b/devtools/client/framework/target.js
@@ -0,0 +1,825 @@
+/* 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 promise = require("promise");
+const defer = require("devtools/shared/defer");
+const EventEmitter = require("devtools/shared/event-emitter");
+const Services = require("Services");
+const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
+
+loader.lazyRequireGetter(this, "DebuggerServer", "devtools/server/main", true);
+loader.lazyRequireGetter(this, "DebuggerClient",
+ "devtools/shared/client/main", true);
+loader.lazyRequireGetter(this, "gDevTools",
+ "devtools/client/framework/devtools", true);
+
+const targets = new WeakMap();
+const promiseTargets = new WeakMap();
+
+/**
+ * Functions for creating Targets
+ */
+const TargetFactory = exports.TargetFactory = {
+ /**
+ * Construct a Target
+ * @param {XULTab} tab
+ * The tab to use in creating a new target.
+ *
+ * @return A target object
+ */
+ forTab: function (tab) {
+ let target = targets.get(tab);
+ if (target == null) {
+ target = new TabTarget(tab);
+ targets.set(tab, target);
+ }
+ return target;
+ },
+
+ /**
+ * Return a promise of a Target for a remote tab.
+ * @param {Object} options
+ * The options object has the following properties:
+ * {
+ * form: the remote protocol form of a tab,
+ * client: a DebuggerClient instance
+ * (caller owns this and is responsible for closing),
+ * chrome: true if the remote target is the whole process
+ * }
+ *
+ * @return A promise of a target object
+ */
+ forRemoteTab: function (options) {
+ let targetPromise = promiseTargets.get(options);
+ if (targetPromise == null) {
+ let target = new TabTarget(options);
+ targetPromise = target.makeRemote().then(() => target);
+ promiseTargets.set(options, targetPromise);
+ }
+ return targetPromise;
+ },
+
+ forWorker: function (workerClient) {
+ let target = targets.get(workerClient);
+ if (target == null) {
+ target = new WorkerTarget(workerClient);
+ targets.set(workerClient, target);
+ }
+ return target;
+ },
+
+ /**
+ * Creating a target for a tab that is being closed is a problem because it
+ * allows a leak as a result of coming after the close event which normally
+ * clears things up. This function allows us to ask if there is a known
+ * target for a tab without creating a target
+ * @return true/false
+ */
+ isKnownTab: function (tab) {
+ return targets.has(tab);
+ },
+};
+
+/**
+ * A Target represents something that we can debug. Targets are generally
+ * read-only. Any changes that you wish to make to a target should be done via
+ * a Tool that attaches to the target. i.e. a Target is just a pointer saying
+ * "the thing to debug is over there".
+ *
+ * Providing a generalized abstraction of a web-page or web-browser (available
+ * either locally or remotely) is beyond the scope of this class (and maybe
+ * also beyond the scope of this universe) However Target does attempt to
+ * abstract some common events and read-only properties common to many Tools.
+ *
+ * Supported read-only properties:
+ * - name, isRemote, url
+ *
+ * Target extends EventEmitter and provides support for the following events:
+ * - close: The target window has been closed. All tools attached to this
+ * target should close. This event is not currently cancelable.
+ * - navigate: The target window has navigated to a different URL
+ *
+ * Optional events:
+ * - will-navigate: The target window will navigate to a different URL
+ * - hidden: The target is not visible anymore (for TargetTab, another tab is
+ * selected)
+ * - visible: The target is visible (for TargetTab, tab is selected)
+ *
+ * Comparing Targets: 2 instances of a Target object can point at the same
+ * thing, so t1 !== t2 and t1 != t2 even when they represent the same object.
+ * To compare to targets use 't1.equals(t2)'.
+ */
+
+/**
+ * A TabTarget represents a page living in a browser tab. Generally these will
+ * be web pages served over http(s), but they don't have to be.
+ */
+function TabTarget(tab) {
+ EventEmitter.decorate(this);
+ this.destroy = this.destroy.bind(this);
+ this.activeTab = this.activeConsole = null;
+ // Only real tabs need initialization here. Placeholder objects for remote
+ // targets will be initialized after a makeRemote method call.
+ if (tab && !["client", "form", "chrome"].every(tab.hasOwnProperty, tab)) {
+ this._tab = tab;
+ this._setupListeners();
+ } else {
+ this._form = tab.form;
+ this._url = this._form.url;
+ this._title = this._form.title;
+
+ this._client = tab.client;
+ this._chrome = tab.chrome;
+ }
+ // Default isTabActor to true if not explicitly specified
+ if (typeof tab.isTabActor == "boolean") {
+ this._isTabActor = tab.isTabActor;
+ } else {
+ this._isTabActor = true;
+ }
+}
+
+TabTarget.prototype = {
+ _webProgressListener: null,
+
+ /**
+ * Returns a promise for the protocol description from the root actor. Used
+ * internally with `target.actorHasMethod`. Takes advantage of caching if
+ * definition was fetched previously with the corresponding actor information.
+ * Actors are lazily loaded, so not only must the tool using a specific actor
+ * be in use, the actors are only registered after invoking a method (for
+ * performance reasons, added in bug 988237), so to use these actor detection
+ * methods, one must already be communicating with a specific actor of that
+ * type.
+ *
+ * Must be a remote target.
+ *
+ * @return {Promise}
+ * {
+ * "category": "actor",
+ * "typeName": "longstractor",
+ * "methods": [{
+ * "name": "substring",
+ * "request": {
+ * "type": "substring",
+ * "start": {
+ * "_arg": 0,
+ * "type": "primitive"
+ * },
+ * "end": {
+ * "_arg": 1,
+ * "type": "primitive"
+ * }
+ * },
+ * "response": {
+ * "substring": {
+ * "_retval": "primitive"
+ * }
+ * }
+ * }],
+ * "events": {}
+ * }
+ */
+ getActorDescription: function (actorName) {
+ if (!this.client) {
+ throw new Error("TabTarget#getActorDescription() can only be called on " +
+ "remote tabs.");
+ }
+
+ let deferred = defer();
+
+ if (this._protocolDescription &&
+ this._protocolDescription.types[actorName]) {
+ deferred.resolve(this._protocolDescription.types[actorName]);
+ } else {
+ this.client.mainRoot.protocolDescription(description => {
+ this._protocolDescription = description;
+ deferred.resolve(description.types[actorName]);
+ });
+ }
+
+ return deferred.promise;
+ },
+
+ /**
+ * Returns a boolean indicating whether or not the specific actor
+ * type exists. Must be a remote target.
+ *
+ * @param {String} actorName
+ * @return {Boolean}
+ */
+ hasActor: function (actorName) {
+ if (!this.client) {
+ throw new Error("TabTarget#hasActor() can only be called on remote " +
+ "tabs.");
+ }
+ if (this.form) {
+ return !!this.form[actorName + "Actor"];
+ }
+ return false;
+ },
+
+ /**
+ * Queries the protocol description to see if an actor has
+ * an available method. The actor must already be lazily-loaded (read
+ * the restrictions in the `getActorDescription` comments),
+ * so this is for use inside of tool. Returns a promise that
+ * resolves to a boolean. Must be a remote target.
+ *
+ * @param {String} actorName
+ * @param {String} methodName
+ * @return {Promise}
+ */
+ actorHasMethod: function (actorName, methodName) {
+ if (!this.client) {
+ throw new Error("TabTarget#actorHasMethod() can only be called on " +
+ "remote tabs.");
+ }
+ return this.getActorDescription(actorName).then(desc => {
+ if (desc && desc.methods) {
+ return !!desc.methods.find(method => method.name === methodName);
+ }
+ return false;
+ });
+ },
+
+ /**
+ * Returns a trait from the root actor.
+ *
+ * @param {String} traitName
+ * @return {Mixed}
+ */
+ getTrait: function (traitName) {
+ if (!this.client) {
+ throw new Error("TabTarget#getTrait() can only be called on remote " +
+ "tabs.");
+ }
+
+ // If the targeted actor exposes traits and has a defined value for this
+ // traits, override the root actor traits
+ if (this.form.traits && traitName in this.form.traits) {
+ return this.form.traits[traitName];
+ }
+
+ return this.client.traits[traitName];
+ },
+
+ get tab() {
+ return this._tab;
+ },
+
+ get form() {
+ return this._form;
+ },
+
+ // Get a promise of the root form returned by a listTabs request. This promise
+ // is cached.
+ get root() {
+ if (!this._root) {
+ this._root = this._getRoot();
+ }
+ return this._root;
+ },
+
+ _getRoot: function () {
+ return new Promise((resolve, reject) => {
+ this.client.listTabs(response => {
+ if (response.error) {
+ reject(new Error(response.error + ": " + response.message));
+ return;
+ }
+
+ resolve(response);
+ });
+ });
+ },
+
+ get client() {
+ return this._client;
+ },
+
+ // Tells us if we are debugging content document
+ // or if we are debugging chrome stuff.
+ // Allows to controls which features are available against
+ // a chrome or a content document.
+ get chrome() {
+ return this._chrome;
+ },
+
+ // Tells us if the related actor implements TabActor interface
+ // and requires to call `attach` request before being used
+ // and `detach` during cleanup
+ get isTabActor() {
+ return this._isTabActor;
+ },
+
+ get window() {
+ // XXX - this is a footgun for e10s - there .contentWindow will be null,
+ // and even though .contentWindowAsCPOW *might* work, it will not work
+ // in all contexts. Consumers of .window need to be refactored to not
+ // rely on this.
+ if (Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) {
+ console.error("The .window getter on devtools' |target| object isn't " +
+ "e10s friendly!\n" + Error().stack);
+ }
+ // Be extra careful here, since this may be called by HS_getHudByWindow
+ // during shutdown.
+ if (this._tab && this._tab.linkedBrowser) {
+ return this._tab.linkedBrowser.contentWindow;
+ }
+ return null;
+ },
+
+ get name() {
+ if (this.isAddon) {
+ return this._form.name;
+ }
+ return this._title;
+ },
+
+ get url() {
+ return this._url;
+ },
+
+ get isRemote() {
+ return !this.isLocalTab;
+ },
+
+ get isAddon() {
+ return !!(this._form && this._form.actor && (
+ this._form.actor.match(/conn\d+\.addon\d+/) ||
+ this._form.actor.match(/conn\d+\.webExtension\d+/)
+ ));
+ },
+
+ get isWebExtension() {
+ return !!(this._form && this._form.actor &&
+ this._form.actor.match(/conn\d+\.webExtension\d+/));
+ },
+
+ get isLocalTab() {
+ return !!this._tab;
+ },
+
+ get isMultiProcess() {
+ return !this.window;
+ },
+
+ /**
+ * Adds remote protocol capabilities to the target, so that it can be used
+ * for tools that support the Remote Debugging Protocol even for local
+ * connections.
+ */
+ makeRemote: function () {
+ if (this._remote) {
+ return this._remote.promise;
+ }
+
+ this._remote = defer();
+
+ if (this.isLocalTab) {
+ // Since a remote protocol connection will be made, let's start the
+ // DebuggerServer here, once and for all tools.
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+
+ this._client = new DebuggerClient(DebuggerServer.connectPipe());
+ // A local TabTarget will never perform chrome debugging.
+ this._chrome = false;
+ }
+
+ this._setupRemoteListeners();
+
+ let attachTab = () => {
+ this._client.attachTab(this._form.actor, (response, tabClient) => {
+ if (!tabClient) {
+ this._remote.reject("Unable to attach to the tab");
+ return;
+ }
+ this.activeTab = tabClient;
+ this.threadActor = response.threadActor;
+
+ attachConsole();
+ });
+ };
+
+ let onConsoleAttached = (response, consoleClient) => {
+ if (!consoleClient) {
+ this._remote.reject("Unable to attach to the console");
+ return;
+ }
+ this.activeConsole = consoleClient;
+ this._remote.resolve(null);
+ };
+
+ let attachConsole = () => {
+ this._client.attachConsole(this._form.consoleActor,
+ [ "NetworkActivity" ],
+ onConsoleAttached);
+ };
+
+ if (this.isLocalTab) {
+ this._client.connect()
+ .then(() => this._client.getTab({ tab: this.tab }))
+ .then(response => {
+ this._form = response.tab;
+ this._url = this._form.url;
+ this._title = this._form.title;
+
+ attachTab();
+ }, e => this._remote.reject(e));
+ } else if (this.isTabActor) {
+ // In the remote debugging case, the protocol connection will have been
+ // already initialized in the connection screen code.
+ attachTab();
+ } else {
+ // AddonActor and chrome debugging on RootActor doesn't inherits from
+ // TabActor and doesn't need to be attached.
+ attachConsole();
+ }
+
+ return this._remote.promise;
+ },
+
+ /**
+ * Listen to the different events.
+ */
+ _setupListeners: function () {
+ this._webProgressListener = new TabWebProgressListener(this);
+ this.tab.linkedBrowser.addProgressListener(this._webProgressListener);
+ this.tab.addEventListener("TabClose", this);
+ this.tab.parentNode.addEventListener("TabSelect", this);
+ this.tab.ownerDocument.defaultView.addEventListener("unload", this);
+ this.tab.addEventListener("TabRemotenessChange", this);
+ },
+
+ /**
+ * Teardown event listeners.
+ */
+ _teardownListeners: function () {
+ if (this._webProgressListener) {
+ this._webProgressListener.destroy();
+ }
+
+ this._tab.ownerDocument.defaultView.removeEventListener("unload", this);
+ this._tab.removeEventListener("TabClose", this);
+ this._tab.parentNode.removeEventListener("TabSelect", this);
+ this._tab.removeEventListener("TabRemotenessChange", this);
+ },
+
+ /**
+ * Setup listeners for remote debugging, updating existing ones as necessary.
+ */
+ _setupRemoteListeners: function () {
+ this.client.addListener("closed", this.destroy);
+
+ this._onTabDetached = (aType, aPacket) => {
+ // We have to filter message to ensure that this detach is for this tab
+ if (aPacket.from == this._form.actor) {
+ this.destroy();
+ }
+ };
+ this.client.addListener("tabDetached", this._onTabDetached);
+
+ this._onTabNavigated = (aType, aPacket) => {
+ let event = Object.create(null);
+ event.url = aPacket.url;
+ event.title = aPacket.title;
+ event.nativeConsoleAPI = aPacket.nativeConsoleAPI;
+ event.isFrameSwitching = aPacket.isFrameSwitching;
+
+ if (!aPacket.isFrameSwitching) {
+ // Update the title and url unless this is a frame switch.
+ this._url = aPacket.url;
+ this._title = aPacket.title;
+ }
+
+ // Send any stored event payload (DOMWindow or nsIRequest) for backwards
+ // compatibility with non-remotable tools.
+ if (aPacket.state == "start") {
+ event._navPayload = this._navRequest;
+ this.emit("will-navigate", event);
+ this._navRequest = null;
+ } else {
+ event._navPayload = this._navWindow;
+ this.emit("navigate", event);
+ this._navWindow = null;
+ }
+ };
+ this.client.addListener("tabNavigated", this._onTabNavigated);
+
+ this._onFrameUpdate = (aType, aPacket) => {
+ this.emit("frame-update", aPacket);
+ };
+ this.client.addListener("frameUpdate", this._onFrameUpdate);
+
+ this._onSourceUpdated = (event, packet) => this.emit("source-updated", packet);
+ this.client.addListener("newSource", this._onSourceUpdated);
+ this.client.addListener("updatedSource", this._onSourceUpdated);
+ },
+
+ /**
+ * Teardown listeners for remote debugging.
+ */
+ _teardownRemoteListeners: function () {
+ this.client.removeListener("closed", this.destroy);
+ this.client.removeListener("tabNavigated", this._onTabNavigated);
+ this.client.removeListener("tabDetached", this._onTabDetached);
+ this.client.removeListener("frameUpdate", this._onFrameUpdate);
+ this.client.removeListener("newSource", this._onSourceUpdated);
+ this.client.removeListener("updatedSource", this._onSourceUpdated);
+ },
+
+ /**
+ * Handle tabs events.
+ */
+ handleEvent: function (event) {
+ switch (event.type) {
+ case "TabClose":
+ case "unload":
+ this.destroy();
+ break;
+ case "TabSelect":
+ if (this.tab.selected) {
+ this.emit("visible", event);
+ } else {
+ this.emit("hidden", event);
+ }
+ break;
+ case "TabRemotenessChange":
+ this.onRemotenessChange();
+ break;
+ }
+ },
+
+ // Automatically respawn the toolbox when the tab changes between being
+ // loaded within the parent process and loaded from a content process.
+ // Process change can go in both ways.
+ onRemotenessChange: function () {
+ // Responsive design do a crazy dance around tabs and triggers
+ // remotenesschange events. But we should ignore them as at the end
+ // the content doesn't change its remoteness.
+ if (this._tab.isResponsiveDesignMode) {
+ return;
+ }
+
+ // Save a reference to the tab as it will be nullified on destroy
+ let tab = this._tab;
+ let onToolboxDestroyed = (event, target) => {
+ if (target != this) {
+ return;
+ }
+ gDevTools.off("toolbox-destroyed", target);
+
+ // Recreate a fresh target instance as the current one is now destroyed
+ let newTarget = TargetFactory.forTab(tab);
+ gDevTools.showToolbox(newTarget);
+ };
+ gDevTools.on("toolbox-destroyed", onToolboxDestroyed);
+ },
+
+ /**
+ * Target is not alive anymore.
+ */
+ destroy: function () {
+ // If several things call destroy then we give them all the same
+ // destruction promise so we're sure to destroy only once
+ if (this._destroyer) {
+ return this._destroyer.promise;
+ }
+
+ this._destroyer = defer();
+
+ // Before taking any action, notify listeners that destruction is imminent.
+ this.emit("close");
+
+ if (this._tab) {
+ this._teardownListeners();
+ }
+
+ let cleanupAndResolve = () => {
+ this._cleanup();
+ this._destroyer.resolve(null);
+ };
+ // If this target was not remoted, the promise will be resolved before the
+ // function returns.
+ if (this._tab && !this._client) {
+ cleanupAndResolve();
+ } else if (this._client) {
+ // If, on the other hand, this target was remoted, the promise will be
+ // resolved after the remote connection is closed.
+ this._teardownRemoteListeners();
+
+ if (this.isLocalTab) {
+ // We started with a local tab and created the client ourselves, so we
+ // should close it.
+ this._client.close().then(cleanupAndResolve);
+ } else if (this.activeTab) {
+ // The client was handed to us, so we are not responsible for closing
+ // it. We just need to detach from the tab, if already attached.
+ // |detach| may fail if the connection is already dead, so proceed with
+ // cleanup directly after this.
+ this.activeTab.detach();
+ cleanupAndResolve();
+ } else {
+ cleanupAndResolve();
+ }
+ }
+
+ return this._destroyer.promise;
+ },
+
+ /**
+ * Clean up references to what this target points to.
+ */
+ _cleanup: function () {
+ if (this._tab) {
+ targets.delete(this._tab);
+ } else {
+ promiseTargets.delete(this._form);
+ }
+
+ this.activeTab = null;
+ this.activeConsole = null;
+ this._client = null;
+ this._tab = null;
+ this._form = null;
+ this._remote = null;
+ this._root = null;
+ this._title = null;
+ this._url = null;
+ this.threadActor = null;
+ },
+
+ toString: function () {
+ let id = this._tab ? this._tab : (this._form && this._form.actor);
+ return `TabTarget:${id}`;
+ },
+
+ /**
+ * @see TabActor.prototype.onResolveLocation
+ */
+ resolveLocation(loc) {
+ let deferred = defer();
+
+ this.client.request(Object.assign({
+ to: this._form.actor,
+ type: "resolveLocation",
+ }, loc), deferred.resolve);
+
+ return deferred.promise;
+ },
+};
+
+/**
+ * WebProgressListener for TabTarget.
+ *
+ * @param object aTarget
+ * The TabTarget instance to work with.
+ */
+function TabWebProgressListener(aTarget) {
+ this.target = aTarget;
+}
+
+TabWebProgressListener.prototype = {
+ target: null,
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
+ Ci.nsISupportsWeakReference]),
+
+ onStateChange: function (progress, request, flag) {
+ let isStart = flag & Ci.nsIWebProgressListener.STATE_START;
+ let isDocument = flag & Ci.nsIWebProgressListener.STATE_IS_DOCUMENT;
+ let isNetwork = flag & Ci.nsIWebProgressListener.STATE_IS_NETWORK;
+ let isRequest = flag & Ci.nsIWebProgressListener.STATE_IS_REQUEST;
+
+ // Skip non-interesting states.
+ if (!isStart || !isDocument || !isRequest || !isNetwork) {
+ return;
+ }
+
+ // emit event if the top frame is navigating
+ if (progress.isTopLevel) {
+ // Emit the event if the target is not remoted or store the payload for
+ // later emission otherwise.
+ if (this.target._client) {
+ this.target._navRequest = request;
+ } else {
+ this.target.emit("will-navigate", request);
+ }
+ }
+ },
+
+ onProgressChange: function () {},
+ onSecurityChange: function () {},
+ onStatusChange: function () {},
+
+ onLocationChange: function (webProgress, request, URI, flags) {
+ if (this.target &&
+ !(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT)) {
+ let window = webProgress.DOMWindow;
+ // Emit the event if the target is not remoted or store the payload for
+ // later emission otherwise.
+ if (this.target._client) {
+ this.target._navWindow = window;
+ } else {
+ this.target.emit("navigate", window);
+ }
+ }
+ },
+
+ /**
+ * Destroy the progress listener instance.
+ */
+ destroy: function () {
+ if (this.target.tab) {
+ try {
+ this.target.tab.linkedBrowser.removeProgressListener(this);
+ } catch (ex) {
+ // This can throw when a tab crashes in e10s.
+ }
+ }
+ this.target._webProgressListener = null;
+ this.target._navRequest = null;
+ this.target._navWindow = null;
+ this.target = null;
+ }
+};
+
+function WorkerTarget(workerClient) {
+ EventEmitter.decorate(this);
+ this._workerClient = workerClient;
+}
+
+/**
+ * A WorkerTarget represents a worker. Unlike TabTarget, which can represent
+ * either a local or remote tab, WorkerTarget always represents a remote worker.
+ * Moreover, unlike TabTarget, which is constructed with a placeholder object
+ * for remote tabs (from which a TabClient can then be lazily obtained),
+ * WorkerTarget is constructed with a WorkerClient directly.
+ *
+ * WorkerClient is designed to mimic the interface of TabClient as closely as
+ * possible. This allows us to debug workers as if they were ordinary tabs,
+ * requiring only minimal changes to the rest of the frontend.
+ */
+WorkerTarget.prototype = {
+ get isRemote() {
+ return true;
+ },
+
+ get isTabActor() {
+ return true;
+ },
+
+ get name() {
+ return "Worker";
+ },
+
+ get url() {
+ return this._workerClient.url;
+ },
+
+ get isWorkerTarget() {
+ return true;
+ },
+
+ get form() {
+ return {
+ consoleActor: this._workerClient.consoleActor
+ };
+ },
+
+ get activeTab() {
+ return this._workerClient;
+ },
+
+ get client() {
+ return this._workerClient.client;
+ },
+
+ destroy: function () {
+ this._workerClient.detach();
+ },
+
+ hasActor: function (name) {
+ // console is the only one actor implemented by WorkerActor
+ if (name == "console") {
+ return true;
+ }
+ return false;
+ },
+
+ getTrait: function () {
+ return undefined;
+ },
+
+ makeRemote: function () {
+ return Promise.resolve();
+ }
+};
diff --git a/devtools/client/framework/test/.eslintrc.js b/devtools/client/framework/test/.eslintrc.js
new file mode 100644
index 000000000..8d15a76d9
--- /dev/null
+++ b/devtools/client/framework/test/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../../.eslintrc.mochitests.js"
+};
diff --git a/devtools/client/framework/test/browser.ini b/devtools/client/framework/test/browser.ini
new file mode 100644
index 000000000..f34cd66f0
--- /dev/null
+++ b/devtools/client/framework/test/browser.ini
@@ -0,0 +1,95 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ browser_toolbox_options_disable_js.html
+ browser_toolbox_options_disable_js_iframe.html
+ browser_toolbox_options_disable_cache.sjs
+ browser_toolbox_sidebar_tool.xul
+ browser_toolbox_window_title_changes_page.html
+ browser_toolbox_window_title_frame_select_page.html
+ code_binary_search.coffee
+ code_binary_search.js
+ code_binary_search.map
+ code_math.js
+ code_ugly.js
+ doc_empty-tab-01.html
+ head.js
+ shared-head.js
+ shared-redux-head.js
+ helper_disable_cache.js
+ doc_theme.css
+ doc_viewsource.html
+ browser_toolbox_options_enable_serviceworkers_testing_frame_script.js
+ browser_toolbox_options_enable_serviceworkers_testing.html
+ serviceworker.js
+
+[browser_browser_toolbox.js]
+[browser_browser_toolbox_debugger.js]
+[browser_devtools_api.js]
+[browser_devtools_api_destroy.js]
+[browser_dynamic_tool_enabling.js]
+[browser_ignore_toolbox_network_requests.js]
+[browser_keybindings_01.js]
+[browser_keybindings_02.js]
+[browser_keybindings_03.js]
+[browser_menu_api.js]
+[browser_new_activation_workflow.js]
+[browser_source_map-01.js]
+[browser_source_map-02.js]
+[browser_target_from_url.js]
+[browser_target_events.js]
+[browser_target_remote.js]
+[browser_target_support.js]
+[browser_toolbox_custom_host.js]
+[browser_toolbox_dynamic_registration.js]
+[browser_toolbox_getpanelwhenready.js]
+[browser_toolbox_highlight.js]
+[browser_toolbox_hosts.js]
+[browser_toolbox_hosts_size.js]
+[browser_toolbox_hosts_telemetry.js]
+[browser_toolbox_keyboard_navigation.js]
+skip-if = os == "mac" # Full keyboard navigation on OSX only works if Full Keyboard Access setting is set to All Control in System Keyboard Preferences
+[browser_toolbox_minimize.js]
+skip-if = true # Bug 1177463 - Temporarily hide the minimize button
+[browser_toolbox_options.js]
+[browser_toolbox_options_disable_buttons.js]
+[browser_toolbox_options_disable_cache-01.js]
+[browser_toolbox_options_disable_cache-02.js]
+[browser_toolbox_options_disable_js.js]
+[browser_toolbox_options_enable_serviceworkers_testing.js]
+# [browser_toolbox_raise.js] # Bug 962258
+# skip-if = os == "win"
+[browser_toolbox_races.js]
+[browser_toolbox_ready.js]
+[browser_toolbox_remoteness_change.js]
+run-if = e10s
+[browser_toolbox_select_event.js]
+skip-if = e10s # Bug 1069044 - destroyInspector may hang during shutdown
+[browser_toolbox_selected_tool_unavailable.js]
+[browser_toolbox_sidebar.js]
+[browser_toolbox_sidebar_events.js]
+[browser_toolbox_sidebar_existing_tabs.js]
+[browser_toolbox_sidebar_overflow_menu.js]
+[browser_toolbox_split_console.js]
+[browser_toolbox_target.js]
+[browser_toolbox_tabsswitch_shortcuts.js]
+[browser_toolbox_textbox_context_menu.js]
+[browser_toolbox_theme_registration.js]
+[browser_toolbox_toggle.js]
+[browser_toolbox_tool_ready.js]
+[browser_toolbox_tool_remote_reopen.js]
+[browser_toolbox_transport_events.js]
+[browser_toolbox_view_source_01.js]
+[browser_toolbox_view_source_02.js]
+[browser_toolbox_view_source_03.js]
+[browser_toolbox_view_source_04.js]
+[browser_toolbox_window_reload_target.js]
+[browser_toolbox_window_shortcuts.js]
+skip-if = os == "mac" && os_version == "10.8" || os == "win" && os_version == "5.1" # Bug 851129 - Re-enable browser_toolbox_window_shortcuts.js test after leaks are fixed
+[browser_toolbox_window_title_changes.js]
+[browser_toolbox_window_title_frame_select.js]
+[browser_toolbox_zoom.js]
+[browser_two_tabs.js]
+# We want this test to run for mochitest-dt as well, so we include it here:
+[../../../../browser/base/content/test/general/browser_parsable_css.js]
diff --git a/devtools/client/framework/test/browser_browser_toolbox.js b/devtools/client/framework/test/browser_browser_toolbox.js
new file mode 100644
index 000000000..08c8ac190
--- /dev/null
+++ b/devtools/client/framework/test/browser_browser_toolbox.js
@@ -0,0 +1,65 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// On debug test slave, it takes about 50s to run the test.
+requestLongerTimeout(4);
+
+add_task(function* runTest() {
+ yield new Promise(done => {
+ let options = {"set": [
+ ["devtools.debugger.prompt-connection", false],
+ ["devtools.debugger.remote-enabled", true],
+ ["devtools.chrome.enabled", true],
+ // Test-only pref to allow passing `testScript` argument to the browser
+ // toolbox
+ ["devtools.browser-toolbox.allow-unsafe-script", true],
+ // On debug test slave, it takes more than the default time (20s)
+ // to get a initialized console
+ ["devtools.debugger.remote-timeout", 120000]
+ ]};
+ SpecialPowers.pushPrefEnv(options, done);
+ });
+
+ // Wait for a notification sent by a script evaluated in the webconsole
+ // of the browser toolbox.
+ let onCustomMessage = new Promise(done => {
+ Services.obs.addObserver(function listener() {
+ Services.obs.removeObserver(listener, "browser-toolbox-console-works");
+ done();
+ }, "browser-toolbox-console-works", false);
+ });
+
+ // Be careful, this JS function is going to be executed in the addon toolbox,
+ // which lives in another process. So do not try to use any scope variable!
+ let env = Components.classes["@mozilla.org/process/environment;1"].getService(Components.interfaces.nsIEnvironment);
+ let testScript = function () {
+ toolbox.selectTool("webconsole")
+ .then(console => {
+ let { jsterm } = console.hud;
+ let js = "Services.obs.notifyObservers(null, 'browser-toolbox-console-works', null);";
+ return jsterm.execute(js);
+ })
+ .then(() => toolbox.destroy());
+ };
+ env.set("MOZ_TOOLBOX_TEST_SCRIPT", "new " + testScript);
+ registerCleanupFunction(() => {
+ env.set("MOZ_TOOLBOX_TEST_SCRIPT", "");
+ });
+
+ let { BrowserToolboxProcess } = Cu.import("resource://devtools/client/framework/ToolboxProcess.jsm", {});
+ let closePromise;
+ yield new Promise(onRun => {
+ closePromise = new Promise(onClose => {
+ info("Opening the browser toolbox\n");
+ BrowserToolboxProcess.init(onClose, onRun);
+ });
+ });
+ ok(true, "Browser toolbox started\n");
+
+ yield onCustomMessage;
+ ok(true, "Received the custom message");
+
+ yield closePromise;
+ ok(true, "Browser toolbox process just closed");
+});
diff --git a/devtools/client/framework/test/browser_browser_toolbox_debugger.js b/devtools/client/framework/test/browser_browser_toolbox_debugger.js
new file mode 100644
index 000000000..c0971cc7c
--- /dev/null
+++ b/devtools/client/framework/test/browser_browser_toolbox_debugger.js
@@ -0,0 +1,131 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// On debug test runner, it takes about 50s to run the test.
+requestLongerTimeout(4);
+
+const { setInterval, clearInterval } = require("sdk/timers");
+
+add_task(function* runTest() {
+ yield new Promise(done => {
+ let options = {"set": [
+ ["devtools.debugger.prompt-connection", false],
+ ["devtools.debugger.remote-enabled", true],
+ ["devtools.chrome.enabled", true],
+ // Test-only pref to allow passing `testScript` argument to the browser
+ // toolbox
+ ["devtools.browser-toolbox.allow-unsafe-script", true],
+ // On debug test runner, it takes more than the default time (20s)
+ // to get a initialized console
+ ["devtools.debugger.remote-timeout", 120000]
+ ]};
+ SpecialPowers.pushPrefEnv(options, done);
+ });
+
+ let s = Cu.Sandbox("http://mozilla.org");
+ // Pass a fake URL to evalInSandbox. If we just pass a filename,
+ // Debugger is going to fail and only display root folder (`/`) listing.
+ // But it won't try to fetch this url and use sandbox content as expected.
+ let testUrl = "http://mozilla.org/browser-toolbox-test.js";
+ Cu.evalInSandbox("(" + function () {
+ this.plop = function plop() {
+ return 1;
+ };
+ } + ").call(this)", s, "1.8", testUrl, 0);
+
+ // Execute the function every second in order to trigger the breakpoint
+ let interval = setInterval(s.plop, 1000);
+
+ // Be careful, this JS function is going to be executed in the browser toolbox,
+ // which lives in another process. So do not try to use any scope variable!
+ let env = Components.classes["@mozilla.org/process/environment;1"]
+ .getService(Components.interfaces.nsIEnvironment);
+ let testScript = function () {
+ const { Task } = Components.utils.import("resource://gre/modules/Task.jsm", {});
+ dump("Opening the browser toolbox and debugger panel\n");
+ let window, document;
+ let testUrl = "http://mozilla.org/browser-toolbox-test.js";
+ Task.spawn(function* () {
+ dump("Waiting for debugger load\n");
+ let panel = yield toolbox.selectTool("jsdebugger");
+ let window = panel.panelWin;
+ let document = window.document;
+
+ yield window.once(window.EVENTS.SOURCE_SHOWN);
+
+ dump("Loaded, selecting the test script to debug\n");
+ let item = document.querySelector(`.dbg-source-item[tooltiptext="${testUrl}"]`);
+ let onSourceShown = window.once(window.EVENTS.SOURCE_SHOWN);
+ item.click();
+ yield onSourceShown;
+
+ dump("Selected, setting a breakpoint\n");
+ let { Sources, editor } = window.DebuggerView;
+ let onBreak = window.once(window.EVENTS.FETCHED_SCOPES);
+ editor.emit("gutterClick", 1);
+ yield onBreak;
+
+ dump("Paused, asserting breakpoint position\n");
+ let url = Sources.selectedItem.attachment.source.url;
+ if (url != testUrl) {
+ throw new Error("Breaking on unexpected script: " + url);
+ }
+ let cursor = editor.getCursor();
+ if (cursor.line != 1) {
+ throw new Error("Breaking on unexpected line: " + cursor.line);
+ }
+
+ dump("Now, stepping over\n");
+ let stepOver = window.document.querySelector("#step-over");
+ let onFetchedScopes = window.once(window.EVENTS.FETCHED_SCOPES);
+ stepOver.click();
+ yield onFetchedScopes;
+
+ dump("Stepped, asserting step position\n");
+ url = Sources.selectedItem.attachment.source.url;
+ if (url != testUrl) {
+ throw new Error("Stepping on unexpected script: " + url);
+ }
+ cursor = editor.getCursor();
+ if (cursor.line != 2) {
+ throw new Error("Stepping on unexpected line: " + cursor.line);
+ }
+
+ dump("Resume script execution\n");
+ let resume = window.document.querySelector("#resume");
+ let onResume = toolbox.target.once("thread-resumed");
+ resume.click();
+ yield onResume;
+
+ dump("Close the browser toolbox\n");
+ toolbox.destroy();
+
+ }).catch(error => {
+ dump("Error while running code in the browser toolbox process:\n");
+ dump(error + "\n");
+ dump("stack:\n" + error.stack + "\n");
+ });
+ };
+ env.set("MOZ_TOOLBOX_TEST_SCRIPT", "new " + testScript);
+ registerCleanupFunction(() => {
+ env.set("MOZ_TOOLBOX_TEST_SCRIPT", "");
+ });
+
+ let { BrowserToolboxProcess } = Cu.import("resource://devtools/client/framework/ToolboxProcess.jsm", {});
+ // Use two promises, one for each BrowserToolboxProcess.init callback
+ // arguments, to ensure that we wait for toolbox run and close events.
+ let closePromise;
+ yield new Promise(onRun => {
+ closePromise = new Promise(onClose => {
+ info("Opening the browser toolbox\n");
+ BrowserToolboxProcess.init(onClose, onRun);
+ });
+ });
+ ok(true, "Browser toolbox started\n");
+
+ yield closePromise;
+ ok(true, "Browser toolbox process just closed");
+
+ clearInterval(interval);
+});
diff --git a/devtools/client/framework/test/browser_devtools_api.js b/devtools/client/framework/test/browser_devtools_api.js
new file mode 100644
index 000000000..72d415c0b
--- /dev/null
+++ b/devtools/client/framework/test/browser_devtools_api.js
@@ -0,0 +1,264 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+//
+// Whitelisting this test.
+// As part of bug 1077403, the leaking uncaught rejections should be fixed.
+//
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed("TypeError: this.docShell is null");
+
+// When running in a standalone directory, we get this error
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed("TypeError: this.doc is undefined");
+
+// Tests devtools API
+
+const toolId1 = "test-tool-1";
+const toolId2 = "test-tool-2";
+
+var EventEmitter = require("devtools/shared/event-emitter");
+
+function test() {
+ addTab("about:blank").then(runTests1);
+}
+
+// Test scenario 1: the tool definition build method returns a promise.
+function runTests1(aTab) {
+ let toolDefinition = {
+ id: toolId1,
+ isTargetSupported: () => true,
+ visibilityswitch: "devtools.test-tool.enabled",
+ url: "about:blank",
+ label: "someLabel",
+ build: function (iframeWindow, toolbox) {
+ let panel = new DevToolPanel(iframeWindow, toolbox);
+ return panel.open();
+ },
+ };
+
+ ok(gDevTools, "gDevTools exists");
+ ok(!gDevTools.getToolDefinitionMap().has(toolId1),
+ "The tool is not registered");
+
+ gDevTools.registerTool(toolDefinition);
+ ok(gDevTools.getToolDefinitionMap().has(toolId1),
+ "The tool is registered");
+
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+
+ let events = {};
+
+ // Check events on the gDevTools and toolbox objects.
+ gDevTools.once(toolId1 + "-init", (event, toolbox, iframe) => {
+ ok(iframe, "iframe argument available");
+
+ toolbox.once(toolId1 + "-init", (event, iframe) => {
+ ok(iframe, "iframe argument available");
+ events["init"] = true;
+ });
+ });
+
+ gDevTools.once(toolId1 + "-ready", (event, toolbox, panel) => {
+ ok(panel, "panel argument available");
+
+ toolbox.once(toolId1 + "-ready", (event, panel) => {
+ ok(panel, "panel argument available");
+ events["ready"] = true;
+ });
+ });
+
+ gDevTools.showToolbox(target, toolId1).then(function (toolbox) {
+ is(toolbox.target, target, "toolbox target is correct");
+ is(toolbox.target.tab, gBrowser.selectedTab, "targeted tab is correct");
+
+ ok(events["init"], "init event fired");
+ ok(events["ready"], "ready event fired");
+
+ gDevTools.unregisterTool(toolId1);
+
+ // Wait for unregisterTool to select the next tool before calling runTests2,
+ // otherwise we will receive the wrong select event when waiting for
+ // unregisterTool to select the next tool in continueTests below.
+ toolbox.once("select", runTests2);
+ });
+}
+
+// Test scenario 2: the tool definition build method returns panel instance.
+function runTests2() {
+ let toolDefinition = {
+ id: toolId2,
+ isTargetSupported: () => true,
+ visibilityswitch: "devtools.test-tool.enabled",
+ url: "about:blank",
+ label: "someLabel",
+ build: function (iframeWindow, toolbox) {
+ return new DevToolPanel(iframeWindow, toolbox);
+ },
+ };
+
+ ok(!gDevTools.getToolDefinitionMap().has(toolId2),
+ "The tool is not registered");
+
+ gDevTools.registerTool(toolDefinition);
+ ok(gDevTools.getToolDefinitionMap().has(toolId2),
+ "The tool is registered");
+
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+
+ let events = {};
+
+ // Check events on the gDevTools and toolbox objects.
+ gDevTools.once(toolId2 + "-init", (event, toolbox, iframe) => {
+ ok(iframe, "iframe argument available");
+
+ toolbox.once(toolId2 + "-init", (event, iframe) => {
+ ok(iframe, "iframe argument available");
+ events["init"] = true;
+ });
+ });
+
+ gDevTools.once(toolId2 + "-build", (event, toolbox, panel, iframe) => {
+ ok(panel, "panel argument available");
+
+ toolbox.once(toolId2 + "-build", (event, panel, iframe) => {
+ ok(panel, "panel argument available");
+ events["build"] = true;
+ });
+ });
+
+ gDevTools.once(toolId2 + "-ready", (event, toolbox, panel) => {
+ ok(panel, "panel argument available");
+
+ toolbox.once(toolId2 + "-ready", (event, panel) => {
+ ok(panel, "panel argument available");
+ events["ready"] = true;
+ });
+ });
+
+ gDevTools.showToolbox(target, toolId2).then(function (toolbox) {
+ is(toolbox.target, target, "toolbox target is correct");
+ is(toolbox.target.tab, gBrowser.selectedTab, "targeted tab is correct");
+
+ ok(events["init"], "init event fired");
+ ok(events["build"], "build event fired");
+ ok(events["ready"], "ready event fired");
+
+ continueTests(toolbox);
+ });
+}
+
+var continueTests = Task.async(function* (toolbox, panel) {
+ ok(toolbox.getCurrentPanel(), "panel value is correct");
+ is(toolbox.currentToolId, toolId2, "toolbox _currentToolId is correct");
+
+ ok(!toolbox.doc.getElementById("toolbox-tab-" + toolId2).hasAttribute("icon-invertable"),
+ "The tool tab does not have the invertable attribute");
+
+ ok(toolbox.doc.getElementById("toolbox-tab-inspector").hasAttribute("icon-invertable"),
+ "The builtin tool tabs do have the invertable attribute");
+
+ let toolDefinitions = gDevTools.getToolDefinitionMap();
+ ok(toolDefinitions.has(toolId2), "The tool is in gDevTools");
+
+ let toolDefinition = toolDefinitions.get(toolId2);
+ is(toolDefinition.id, toolId2, "toolDefinition id is correct");
+
+ info("Testing toolbox tool-unregistered event");
+ let toolSelected = toolbox.once("select");
+ let unregisteredTool = yield new Promise(resolve => {
+ toolbox.once("tool-unregistered", (e, id) => resolve(id));
+ gDevTools.unregisterTool(toolId2);
+ });
+ yield toolSelected;
+
+ is(unregisteredTool, toolId2, "Event returns correct id");
+ ok(!toolbox.isToolRegistered(toolId2),
+ "Toolbox: The tool is not registered");
+ ok(!gDevTools.getToolDefinitionMap().has(toolId2),
+ "The tool is no longer registered");
+
+ info("Testing toolbox tool-registered event");
+ let registeredTool = yield new Promise(resolve => {
+ toolbox.once("tool-registered", (e, id) => resolve(id));
+ gDevTools.registerTool(toolDefinition);
+ });
+
+ is(registeredTool, toolId2, "Event returns correct id");
+ ok(toolbox.isToolRegistered(toolId2),
+ "Toolbox: The tool is registered");
+ ok(gDevTools.getToolDefinitionMap().has(toolId2),
+ "The tool is registered");
+
+ info("Unregistering tool");
+ gDevTools.unregisterTool(toolId2);
+
+ destroyToolbox(toolbox);
+});
+
+function destroyToolbox(toolbox) {
+ toolbox.destroy().then(function () {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ ok(gDevTools._toolboxes.get(target) == null, "gDevTools doesn't know about target");
+ ok(toolbox.target == null, "toolbox doesn't know about target.");
+ finishUp();
+ });
+}
+
+function finishUp() {
+ gBrowser.removeCurrentTab();
+ finish();
+}
+
+/**
+* When a Toolbox is started it creates a DevToolPanel for each of the tools
+* by calling toolDefinition.build(). The returned object should
+* at least implement these functions. They will be used by the ToolBox.
+*
+* There may be no benefit in doing this as an abstract type, but if nothing
+* else gives us a place to write documentation.
+*/
+function DevToolPanel(iframeWindow, toolbox) {
+ EventEmitter.decorate(this);
+
+ this._toolbox = toolbox;
+
+ /* let doc = iframeWindow.document
+ let label = doc.createElement("label");
+ let textNode = doc.createTextNode("Some Tool");
+
+ label.appendChild(textNode);
+ doc.body.appendChild(label);*/
+}
+
+DevToolPanel.prototype = {
+ open: function () {
+ let deferred = defer();
+
+ executeSoon(() => {
+ this._isReady = true;
+ this.emit("ready");
+ deferred.resolve(this);
+ });
+
+ return deferred.promise;
+ },
+
+ get target() {
+ return this._toolbox.target;
+ },
+
+ get toolbox() {
+ return this._toolbox;
+ },
+
+ get isReady() {
+ return this._isReady;
+ },
+
+ _isReady: false,
+
+ destroy: function DTI_destroy() {
+ return defer(null);
+ },
+};
diff --git a/devtools/client/framework/test/browser_devtools_api_destroy.js b/devtools/client/framework/test/browser_devtools_api_destroy.js
new file mode 100644
index 000000000..084a7a0a1
--- /dev/null
+++ b/devtools/client/framework/test/browser_devtools_api_destroy.js
@@ -0,0 +1,71 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests devtools API
+
+function test() {
+ addTab("about:blank").then(runTests);
+}
+
+function runTests(aTab) {
+ let toolDefinition = {
+ id: "testTool",
+ visibilityswitch: "devtools.testTool.enabled",
+ isTargetSupported: () => true,
+ url: "about:blank",
+ label: "someLabel",
+ build: function (iframeWindow, toolbox) {
+ let deferred = defer();
+ executeSoon(() => {
+ deferred.resolve({
+ target: toolbox.target,
+ toolbox: toolbox,
+ isReady: true,
+ destroy: function () {},
+ });
+ });
+ return deferred.promise;
+ },
+ };
+
+ gDevTools.registerTool(toolDefinition);
+
+ let collectedEvents = [];
+
+ let target = TargetFactory.forTab(aTab);
+ gDevTools.showToolbox(target, toolDefinition.id).then(function (toolbox) {
+ let panel = toolbox.getPanel(toolDefinition.id);
+ ok(panel, "Tool open");
+
+ gDevTools.once("toolbox-destroy", (event, toolbox, iframe) => {
+ collectedEvents.push(event);
+ });
+
+ gDevTools.once(toolDefinition.id + "-destroy", (event, toolbox, iframe) => {
+ collectedEvents.push("gDevTools-" + event);
+ });
+
+ toolbox.once("destroy", (event) => {
+ collectedEvents.push(event);
+ });
+
+ toolbox.once(toolDefinition.id + "-destroy", (event) => {
+ collectedEvents.push("toolbox-" + event);
+ });
+
+ toolbox.destroy().then(function () {
+ is(collectedEvents.join(":"),
+ "toolbox-destroy:destroy:gDevTools-testTool-destroy:toolbox-testTool-destroy",
+ "Found the right amount of collected events.");
+
+ gDevTools.unregisterTool(toolDefinition.id);
+ gBrowser.removeCurrentTab();
+
+ executeSoon(function () {
+ finish();
+ });
+ });
+ });
+}
diff --git a/devtools/client/framework/test/browser_dynamic_tool_enabling.js b/devtools/client/framework/test/browser_dynamic_tool_enabling.js
new file mode 100644
index 000000000..6420afabe
--- /dev/null
+++ b/devtools/client/framework/test/browser_dynamic_tool_enabling.js
@@ -0,0 +1,41 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests that toggling prefs immediately (de)activates the relevant menuitem
+
+var gItemsToTest = {
+ "menu_devToolbar": "devtools.toolbar.enabled",
+ "menu_browserToolbox": ["devtools.chrome.enabled", "devtools.debugger.remote-enabled"],
+ "menu_devtools_connect": "devtools.debugger.remote-enabled",
+};
+
+function expectedAttributeValueFromPrefs(prefs) {
+ return prefs.every((pref) => Services.prefs.getBoolPref(pref)) ?
+ "" : "true";
+}
+
+function checkItem(el, prefs) {
+ let expectedValue = expectedAttributeValueFromPrefs(prefs);
+ is(el.getAttribute("disabled"), expectedValue, "disabled attribute should match current pref state");
+ is(el.getAttribute("hidden"), expectedValue, "hidden attribute should match current pref state");
+}
+
+function test() {
+ for (let k in gItemsToTest) {
+ let el = document.getElementById(k);
+ let prefs = gItemsToTest[k];
+ if (typeof prefs == "string") {
+ prefs = [prefs];
+ }
+ checkItem(el, prefs);
+ for (let pref of prefs) {
+ Services.prefs.setBoolPref(pref, !Services.prefs.getBoolPref(pref));
+ checkItem(el, prefs);
+ Services.prefs.setBoolPref(pref, !Services.prefs.getBoolPref(pref));
+ checkItem(el, prefs);
+ }
+ }
+ finish();
+}
diff --git a/devtools/client/framework/test/browser_ignore_toolbox_network_requests.js b/devtools/client/framework/test/browser_ignore_toolbox_network_requests.js
new file mode 100644
index 000000000..1cfc22f7e
--- /dev/null
+++ b/devtools/client/framework/test/browser_ignore_toolbox_network_requests.js
@@ -0,0 +1,33 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 network requests originating from the toolbox don't get recorded in
+// the network panel.
+
+add_task(function* () {
+ // TODO: This test tries to verify the normal behavior of the netmonitor and
+ // therefore needs to avoid the explicit check for tests. Bug 1167188 will
+ // allow us to remove this workaround.
+ let isTesting = flags.testing;
+ flags.testing = false;
+
+ let tab = yield addTab(URL_ROOT + "doc_viewsource.html");
+ let target = TargetFactory.forTab(tab);
+ let toolbox = yield gDevTools.showToolbox(target, "styleeditor");
+ let panel = toolbox.getPanel("styleeditor");
+
+ is(panel.UI.editors.length, 1, "correct number of editors opened");
+
+ let monitor = yield toolbox.selectTool("netmonitor");
+ let { RequestsMenu } = monitor.panelWin.NetMonitorView;
+ is(RequestsMenu.itemCount, 0, "No network requests appear in the network panel");
+
+ yield gDevTools.closeToolbox(target);
+ tab = target = toolbox = panel = null;
+ gBrowser.removeCurrentTab();
+ flags.testing = isTesting;
+});
diff --git a/devtools/client/framework/test/browser_keybindings_01.js b/devtools/client/framework/test/browser_keybindings_01.js
new file mode 100644
index 000000000..4e4effb07
--- /dev/null
+++ b/devtools/client/framework/test/browser_keybindings_01.js
@@ -0,0 +1,115 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests that the keybindings for opening and closing the inspector work as expected
+// Can probably make this a shared test that tests all of the tools global keybindings
+const TEST_URL = "data:text/html,<html><head><title>Test for the " +
+ "highlighter keybindings</title></head><body>" +
+ "<h1>Keybindings!</h1></body></html>"
+function test()
+{
+ waitForExplicitFinish();
+
+ let doc;
+ let node;
+ let inspector;
+ let keysetMap = { };
+
+ addTab(TEST_URL).then(function () {
+ doc = content.document;
+ node = doc.querySelector("h1");
+ waitForFocus(setupKeyBindingsTest);
+ });
+
+ function buildDevtoolsKeysetMap(keyset) {
+ [].forEach.call(keyset.querySelectorAll("key"), function (key) {
+
+ if (!key.getAttribute("key")) {
+ return;
+ }
+
+ let modifiers = key.getAttribute("modifiers");
+
+ keysetMap[key.id.split("_")[1]] = {
+ key: key.getAttribute("key"),
+ modifiers: modifiers,
+ modifierOpt: {
+ shiftKey: modifiers.match("shift"),
+ ctrlKey: modifiers.match("ctrl"),
+ altKey: modifiers.match("alt"),
+ metaKey: modifiers.match("meta"),
+ accelKey: modifiers.match("accel")
+ },
+ synthesizeKey: function () {
+ EventUtils.synthesizeKey(this.key, this.modifierOpt);
+ }
+ };
+ });
+ }
+
+ function setupKeyBindingsTest()
+ {
+ for (let win of gDevToolsBrowser._trackedBrowserWindows) {
+ buildDevtoolsKeysetMap(win.document.getElementById("devtoolsKeyset"));
+ }
+
+ gDevTools.once("toolbox-ready", (e, toolbox) => {
+ inspectorShouldBeOpenAndHighlighting(toolbox.getCurrentPanel(), toolbox);
+ });
+
+ keysetMap.inspector.synthesizeKey();
+ }
+
+ function inspectorShouldBeOpenAndHighlighting(aInspector, aToolbox)
+ {
+ is(aToolbox.currentToolId, "inspector", "Correct tool has been loaded");
+
+ aToolbox.once("picker-started", () => {
+ ok(true, "picker-started event received, highlighter started");
+ keysetMap.inspector.synthesizeKey();
+
+ aToolbox.once("picker-stopped", () => {
+ ok(true, "picker-stopped event received, highlighter stopped");
+ gDevTools.once("select-tool-command", () => {
+ webconsoleShouldBeSelected(aToolbox);
+ });
+ keysetMap.webconsole.synthesizeKey();
+ });
+ });
+ }
+
+ function webconsoleShouldBeSelected(aToolbox)
+ {
+ is(aToolbox.currentToolId, "webconsole", "webconsole should be selected.");
+
+ gDevTools.once("select-tool-command", () => {
+ jsdebuggerShouldBeSelected(aToolbox);
+ });
+ keysetMap.jsdebugger.synthesizeKey();
+ }
+
+ function jsdebuggerShouldBeSelected(aToolbox)
+ {
+ is(aToolbox.currentToolId, "jsdebugger", "jsdebugger should be selected.");
+
+ gDevTools.once("select-tool-command", () => {
+ netmonitorShouldBeSelected(aToolbox);
+ });
+
+ keysetMap.netmonitor.synthesizeKey();
+ }
+
+ function netmonitorShouldBeSelected(aToolbox, panel)
+ {
+ is(aToolbox.currentToolId, "netmonitor", "netmonitor should be selected.");
+ finishUp();
+ }
+
+ function finishUp() {
+ doc = node = inspector = keysetMap = null;
+ gBrowser.removeCurrentTab();
+ finish();
+ }
+}
diff --git a/devtools/client/framework/test/browser_keybindings_02.js b/devtools/client/framework/test/browser_keybindings_02.js
new file mode 100644
index 000000000..551fef873
--- /dev/null
+++ b/devtools/client/framework/test/browser_keybindings_02.js
@@ -0,0 +1,65 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 toolbox keybindings still work after the host is changed.
+
+const URL = "data:text/html;charset=utf8,test page";
+
+var {Toolbox} = require("devtools/client/framework/toolbox");
+
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper("devtools/client/locales/toolbox.properties");
+
+function getZoomValue() {
+ return parseFloat(Services.prefs.getCharPref("devtools.toolbox.zoomValue"));
+}
+
+add_task(function* () {
+ info("Create a test tab and open the toolbox");
+ let tab = yield addTab(URL);
+ let target = TargetFactory.forTab(tab);
+ let toolbox = yield gDevTools.showToolbox(target, "webconsole");
+
+ let {SIDE, BOTTOM} = Toolbox.HostType;
+ for (let type of [SIDE, BOTTOM, SIDE]) {
+ info("Switch to host type " + type);
+ yield toolbox.switchHost(type);
+
+ info("Try to use the toolbox shortcuts");
+ yield checkKeyBindings(toolbox);
+ }
+
+ Services.prefs.clearUserPref("devtools.toolbox.zoomValue");
+ Services.prefs.setCharPref("devtools.toolbox.host", BOTTOM);
+ yield toolbox.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+function zoomWithKey(toolbox, key) {
+ let shortcut = L10N.getStr(key);
+ if (!shortcut) {
+ info("Key was empty, skipping zoomWithKey");
+ return;
+ }
+ info("Zooming with key: " + key);
+ let currentZoom = getZoomValue();
+ synthesizeKeyShortcut(shortcut, toolbox.win);
+ isnot(getZoomValue(), currentZoom, "The zoom level was changed in the toolbox");
+}
+
+function* checkKeyBindings(toolbox) {
+ zoomWithKey(toolbox, "toolbox.zoomIn.key");
+ zoomWithKey(toolbox, "toolbox.zoomIn2.key");
+ zoomWithKey(toolbox, "toolbox.zoomIn3.key");
+
+ zoomWithKey(toolbox, "toolbox.zoomReset.key");
+
+ zoomWithKey(toolbox, "toolbox.zoomOut.key");
+ zoomWithKey(toolbox, "toolbox.zoomOut2.key");
+
+ zoomWithKey(toolbox, "toolbox.zoomReset2.key");
+}
diff --git a/devtools/client/framework/test/browser_keybindings_03.js b/devtools/client/framework/test/browser_keybindings_03.js
new file mode 100644
index 000000000..752087a09
--- /dev/null
+++ b/devtools/client/framework/test/browser_keybindings_03.js
@@ -0,0 +1,53 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 toolbox 'switch to previous host' feature works.
+// Pressing ctrl/cmd+shift+d should switch to the last used host.
+
+const URL = "data:text/html;charset=utf8,test page for toolbox switching";
+
+var {Toolbox} = require("devtools/client/framework/toolbox");
+
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper("devtools/client/locales/toolbox.properties");
+
+add_task(function* () {
+ info("Create a test tab and open the toolbox");
+ let tab = yield addTab(URL);
+ let target = TargetFactory.forTab(tab);
+ let toolbox = yield gDevTools.showToolbox(target, "webconsole");
+
+ let shortcut = L10N.getStr("toolbox.toggleHost.key");
+
+ let {SIDE, BOTTOM, WINDOW} = Toolbox.HostType;
+ checkHostType(toolbox, BOTTOM, SIDE);
+
+ info("Switching from bottom to side");
+ let onHostChanged = toolbox.once("host-changed");
+ synthesizeKeyShortcut(shortcut, toolbox.win);
+ yield onHostChanged;
+ checkHostType(toolbox, SIDE, BOTTOM);
+
+ info("Switching from side to bottom");
+ onHostChanged = toolbox.once("host-changed");
+ synthesizeKeyShortcut(shortcut, toolbox.win);
+ yield onHostChanged;
+ checkHostType(toolbox, BOTTOM, SIDE);
+
+ info("Switching to window");
+ yield toolbox.switchHost(WINDOW);
+ checkHostType(toolbox, WINDOW, BOTTOM);
+
+ info("Switching from window to bottom");
+ onHostChanged = toolbox.once("host-changed");
+ synthesizeKeyShortcut(shortcut, toolbox.win);
+ yield onHostChanged;
+ checkHostType(toolbox, BOTTOM, WINDOW);
+
+ yield toolbox.destroy();
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/client/framework/test/browser_menu_api.js b/devtools/client/framework/test/browser_menu_api.js
new file mode 100644
index 000000000..cf634ff6f
--- /dev/null
+++ b/devtools/client/framework/test/browser_menu_api.js
@@ -0,0 +1,181 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 Menu API works
+
+const URL = "data:text/html;charset=utf8,test page for menu api";
+const Menu = require("devtools/client/framework/menu");
+const MenuItem = require("devtools/client/framework/menu-item");
+
+add_task(function* () {
+ info("Create a test tab and open the toolbox");
+ let tab = yield addTab(URL);
+ let target = TargetFactory.forTab(tab);
+ let toolbox = yield gDevTools.showToolbox(target, "webconsole");
+
+ yield testMenuItems();
+ yield testMenuPopup(toolbox);
+ yield testSubmenu(toolbox);
+});
+
+function* testMenuItems() {
+ let menu = new Menu();
+ let menuItem1 = new MenuItem();
+ let menuItem2 = new MenuItem();
+
+ menu.append(menuItem1);
+ menu.append(menuItem2);
+
+ is(menu.items.length, 2, "Correct number of 'items'");
+ is(menu.items[0], menuItem1, "Correct reference to MenuItem");
+ is(menu.items[1], menuItem2, "Correct reference to MenuItem");
+}
+
+function* testMenuPopup(toolbox) {
+ let clickFired = false;
+
+ let menu = new Menu({
+ id: "menu-popup",
+ });
+ menu.append(new MenuItem({ type: "separator" }));
+
+ let MENU_ITEMS = [
+ new MenuItem({
+ id: "menu-item-1",
+ label: "Normal Item",
+ click: () => {
+ info("Click callback has fired for menu item");
+ clickFired = true;
+ },
+ }),
+ new MenuItem({
+ label: "Checked Item",
+ type: "checkbox",
+ checked: true,
+ }),
+ new MenuItem({
+ label: "Radio Item",
+ type: "radio",
+ }),
+ new MenuItem({
+ label: "Disabled Item",
+ disabled: true,
+ }),
+ ];
+
+ for (let item of MENU_ITEMS) {
+ menu.append(item);
+ }
+
+ // Append an invisible MenuItem, which shouldn't show up in the DOM
+ menu.append(new MenuItem({
+ label: "Invisible",
+ visible: false,
+ }));
+
+ menu.popup(0, 0, toolbox);
+
+ ok(toolbox.doc.querySelector("#menu-popup"), "A popup is in the DOM");
+
+ let menuSeparators =
+ toolbox.doc.querySelectorAll("#menu-popup > menuseparator");
+ is(menuSeparators.length, 1, "A separator is in the menu");
+
+ let menuItems = toolbox.doc.querySelectorAll("#menu-popup > menuitem");
+ is(menuItems.length, MENU_ITEMS.length, "Correct number of menuitems");
+
+ is(menuItems[0].id, MENU_ITEMS[0].id, "Correct id for menuitem");
+ is(menuItems[0].getAttribute("label"), MENU_ITEMS[0].label, "Correct label");
+
+ is(menuItems[1].getAttribute("label"), MENU_ITEMS[1].label, "Correct label");
+ is(menuItems[1].getAttribute("type"), "checkbox", "Correct type attr");
+ is(menuItems[1].getAttribute("checked"), "true", "Has checked attr");
+
+ is(menuItems[2].getAttribute("label"), MENU_ITEMS[2].label, "Correct label");
+ is(menuItems[2].getAttribute("type"), "radio", "Correct type attr");
+ ok(!menuItems[2].hasAttribute("checked"), "Doesn't have checked attr");
+
+ is(menuItems[3].getAttribute("label"), MENU_ITEMS[3].label, "Correct label");
+ is(menuItems[3].getAttribute("disabled"), "true", "disabled attr menuitem");
+
+ yield once(menu, "open");
+ let closed = once(menu, "close");
+ EventUtils.synthesizeMouseAtCenter(menuItems[0], {}, toolbox.win);
+ yield closed;
+ ok(clickFired, "Click has fired");
+
+ ok(!toolbox.doc.querySelector("#menu-popup"), "Popup removed from the DOM");
+}
+
+function* testSubmenu(toolbox) {
+ let clickFired = false;
+ let menu = new Menu({
+ id: "menu-popup",
+ });
+ let submenu = new Menu({
+ id: "submenu-popup",
+ });
+ submenu.append(new MenuItem({
+ label: "Submenu item",
+ click: () => {
+ info("Click callback has fired for submenu item");
+ clickFired = true;
+ },
+ }));
+ menu.append(new MenuItem({
+ label: "Submenu parent",
+ submenu: submenu,
+ }));
+ menu.append(new MenuItem({
+ label: "Submenu parent with attributes",
+ id: "submenu-parent-with-attrs",
+ submenu: submenu,
+ accesskey: "A",
+ disabled: true,
+ }));
+
+ menu.popup(0, 0, toolbox);
+ ok(toolbox.doc.querySelector("#menu-popup"), "A popup is in the DOM");
+ is(toolbox.doc.querySelectorAll("#menu-popup > menuitem").length, 0,
+ "No menuitem children");
+
+ let menus = toolbox.doc.querySelectorAll("#menu-popup > menu");
+ is(menus.length, 2, "Correct number of menus");
+ is(menus[0].getAttribute("label"), "Submenu parent", "Correct label");
+ ok(!menus[0].hasAttribute("disabled"), "Correct disabled state");
+
+ is(menus[1].getAttribute("accesskey"), "A", "Correct accesskey");
+ ok(menus[1].hasAttribute("disabled"), "Correct disabled state");
+ ok(menus[1].id, "submenu-parent-with-attrs", "Correct id");
+
+ let subMenuItems = menus[0].querySelectorAll("menupopup > menuitem");
+ is(subMenuItems.length, 1, "Correct number of submenu items");
+ is(subMenuItems[0].getAttribute("label"), "Submenu item", "Correct label");
+
+ yield once(menu, "open");
+ let closed = once(menu, "close");
+
+ info("Using keyboard navigation to open, close, and reopen the submenu");
+ let shown = once(menus[0], "popupshown");
+ EventUtils.synthesizeKey("VK_DOWN", {});
+ EventUtils.synthesizeKey("VK_RIGHT", {});
+ yield shown;
+
+ let hidden = once(menus[0], "popuphidden");
+ EventUtils.synthesizeKey("VK_LEFT", {});
+ yield hidden;
+
+ shown = once(menus[0], "popupshown");
+ EventUtils.synthesizeKey("VK_RIGHT", {});
+ yield shown;
+
+ info("Clicking the submenu item");
+ EventUtils.synthesizeMouseAtCenter(subMenuItems[0], {}, toolbox.win);
+
+ yield closed;
+ ok(clickFired, "Click has fired");
+}
diff --git a/devtools/client/framework/test/browser_new_activation_workflow.js b/devtools/client/framework/test/browser_new_activation_workflow.js
new file mode 100644
index 000000000..4092bf1a7
--- /dev/null
+++ b/devtools/client/framework/test/browser_new_activation_workflow.js
@@ -0,0 +1,69 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests devtools API
+
+var toolbox, target;
+
+var tempScope = {};
+
+function test() {
+ addTab("about:blank").then(function (aTab) {
+ target = TargetFactory.forTab(gBrowser.selectedTab);
+ loadWebConsole(aTab).then(function () {
+ console.log("loaded");
+ });
+ });
+}
+
+function loadWebConsole(aTab) {
+ ok(gDevTools, "gDevTools exists");
+
+ return gDevTools.showToolbox(target, "webconsole").then(function (aToolbox) {
+ toolbox = aToolbox;
+ checkToolLoading();
+ });
+}
+
+function checkToolLoading() {
+ is(toolbox.currentToolId, "webconsole", "The web console is selected");
+ ok(toolbox.isReady, "toolbox is ready");
+
+ selectAndCheckById("jsdebugger").then(function () {
+ selectAndCheckById("styleeditor").then(function () {
+ testToggle();
+ });
+ });
+}
+
+function selectAndCheckById(id) {
+ return toolbox.selectTool(id).then(function () {
+ let tab = toolbox.doc.getElementById("toolbox-tab-" + id);
+ is(tab.hasAttribute("selected"), true, "The " + id + " tab is selected");
+ });
+}
+
+function testToggle() {
+ toolbox.once("destroyed", () => {
+ // Cannot reuse a target after it's destroyed.
+ target = TargetFactory.forTab(gBrowser.selectedTab);
+ gDevTools.showToolbox(target, "styleeditor").then(function (aToolbox) {
+ toolbox = aToolbox;
+ is(toolbox.currentToolId, "styleeditor", "The style editor is selected");
+ finishUp();
+ });
+ });
+
+ toolbox.destroy();
+}
+
+function finishUp() {
+ toolbox.destroy().then(function () {
+ toolbox = null;
+ target = null;
+ gBrowser.removeCurrentTab();
+ finish();
+ });
+}
diff --git a/devtools/client/framework/test/browser_source_map-01.js b/devtools/client/framework/test/browser_source_map-01.js
new file mode 100644
index 000000000..af1808681
--- /dev/null
+++ b/devtools/client/framework/test/browser_source_map-01.js
@@ -0,0 +1,115 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Whitelisting this test.
+// As part of bug 1077403, the leaking uncaught rejections should be fixed.
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed("[object Object]");
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed(
+ "TypeError: this.transport is null");
+
+/**
+ * Tests the SourceMapService updates generated sources when source maps
+ * are subsequently found. Also checks when no column is provided, and
+ * when tagging an already source mapped location initially.
+ */
+
+// Force the old debugger UI since it's directly used (see Bug 1301705)
+Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false);
+registerCleanupFunction(function* () {
+ Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend");
+});
+
+const DEBUGGER_ROOT = "http://example.com/browser/devtools/client/debugger/test/mochitest/";
+// Empty page
+const PAGE_URL = `${DEBUGGER_ROOT}doc_empty-tab-01.html`;
+const JS_URL = `${URL_ROOT}code_binary_search.js`;
+const COFFEE_URL = `${URL_ROOT}code_binary_search.coffee`;
+const { SourceMapService } = require("devtools/client/framework/source-map-service");
+const { serialize } = require("devtools/client/framework/location-store");
+
+add_task(function* () {
+ const toolbox = yield openNewTabAndToolbox(PAGE_URL, "jsdebugger");
+ const service = new SourceMapService(toolbox.target);
+ let aggregator = new Map();
+
+ function onUpdate(e, oldLoc, newLoc) {
+ if (oldLoc.line === 6) {
+ checkLoc1(oldLoc, newLoc);
+ } else if (oldLoc.line === 8) {
+ checkLoc2(oldLoc, newLoc);
+ } else {
+ throw new Error(`Unexpected location update: ${JSON.stringify(oldLoc)}`);
+ }
+ aggregator.set(serialize(oldLoc), newLoc);
+ }
+
+ let loc1 = { url: JS_URL, line: 6 };
+ let loc2 = { url: JS_URL, line: 8, column: 3 };
+
+ service.subscribe(loc1, onUpdate);
+ service.subscribe(loc2, onUpdate);
+
+ // Inject JS script
+ let sourceShown = waitForSourceShown(toolbox.getCurrentPanel(), "code_binary_search");
+ yield createScript(JS_URL);
+ yield sourceShown;
+
+ yield waitUntil(() => aggregator.size === 2);
+
+ aggregator = Array.from(aggregator.values());
+
+ ok(aggregator.find(i => i.url === COFFEE_URL && i.line === 4), "found first updated location");
+ ok(aggregator.find(i => i.url === COFFEE_URL && i.line === 6), "found second updated location");
+
+ yield toolbox.destroy();
+ gBrowser.removeCurrentTab();
+ finish();
+});
+
+function checkLoc1(oldLoc, newLoc) {
+ is(oldLoc.line, 6, "Correct line for JS:6");
+ is(oldLoc.column, null, "Correct column for JS:6");
+ is(oldLoc.url, JS_URL, "Correct url for JS:6");
+ is(newLoc.line, 4, "Correct line for JS:6 -> COFFEE");
+ is(newLoc.column, 2, "Correct column for JS:6 -> COFFEE -- handles falsy column entries");
+ is(newLoc.url, COFFEE_URL, "Correct url for JS:6 -> COFFEE");
+}
+
+function checkLoc2(oldLoc, newLoc) {
+ is(oldLoc.line, 8, "Correct line for JS:8:3");
+ is(oldLoc.column, 3, "Correct column for JS:8:3");
+ is(oldLoc.url, JS_URL, "Correct url for JS:8:3");
+ is(newLoc.line, 6, "Correct line for JS:8:3 -> COFFEE");
+ is(newLoc.column, 10, "Correct column for JS:8:3 -> COFFEE");
+ is(newLoc.url, COFFEE_URL, "Correct url for JS:8:3 -> COFFEE");
+}
+
+function createScript(url) {
+ info(`Creating script: ${url}`);
+ let mm = getFrameScript();
+ let command = `
+ let script = document.createElement("script");
+ script.setAttribute("src", "${url}");
+ document.body.appendChild(script);
+ null;
+ `;
+ return evalInDebuggee(mm, command);
+}
+
+function waitForSourceShown(debuggerPanel, url) {
+ let { panelWin } = debuggerPanel;
+ let deferred = defer();
+
+ info(`Waiting for source ${url} to be shown in the debugger...`);
+ panelWin.on(panelWin.EVENTS.SOURCE_SHOWN, function onSourceShown(_, source) {
+
+ let sourceUrl = source.url || source.generatedUrl;
+ if (sourceUrl.includes(url)) {
+ panelWin.off(panelWin.EVENTS.SOURCE_SHOWN, onSourceShown);
+ info(`Source shown for ${url}`);
+ deferred.resolve(source);
+ }
+ });
+
+ return deferred.promise;
+}
diff --git a/devtools/client/framework/test/browser_source_map-02.js b/devtools/client/framework/test/browser_source_map-02.js
new file mode 100644
index 000000000..f31ce0175
--- /dev/null
+++ b/devtools/client/framework/test/browser_source_map-02.js
@@ -0,0 +1,113 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests the SourceMapService updates generated sources when pretty printing
+ * and un pretty printing.
+ */
+
+// Force the old debugger UI since it's directly used (see Bug 1301705)
+Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false);
+registerCleanupFunction(function* () {
+ Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend");
+});
+
+const DEBUGGER_ROOT = "http://example.com/browser/devtools/client/debugger/test/mochitest/";
+// Empty page
+const PAGE_URL = `${DEBUGGER_ROOT}doc_empty-tab-01.html`;
+const JS_URL = `${URL_ROOT}code_ugly.js`;
+const { SourceMapService } = require("devtools/client/framework/source-map-service");
+
+add_task(function* () {
+ let toolbox = yield openNewTabAndToolbox(PAGE_URL, "jsdebugger");
+
+ let service = new SourceMapService(toolbox.target);
+
+ let checkedPretty = false;
+ let checkedUnpretty = false;
+
+ function onUpdate(e, oldLoc, newLoc) {
+ if (oldLoc.line === 3) {
+ checkPrettified(oldLoc, newLoc);
+ checkedPretty = true;
+ } else if (oldLoc.line === 9) {
+ checkUnprettified(oldLoc, newLoc);
+ checkedUnpretty = true;
+ } else {
+ throw new Error(`Unexpected location update: ${JSON.stringify(oldLoc)}`);
+ }
+ }
+ const loc1 = { url: JS_URL, line: 3 };
+ service.subscribe(loc1, onUpdate);
+
+ // Inject JS script
+ let sourceShown = waitForSourceShown(toolbox.getCurrentPanel(), "code_ugly.js");
+ yield createScript(JS_URL);
+ yield sourceShown;
+
+ let ppButton = toolbox.getCurrentPanel().panelWin.document.getElementById("pretty-print");
+ sourceShown = waitForSourceShown(toolbox.getCurrentPanel(), "code_ugly.js");
+ ppButton.click();
+ yield sourceShown;
+ yield waitUntil(() => checkedPretty);
+
+ // TODO check unprettified change once bug 1177446 fixed
+ // info("Testing un-pretty printing.");
+ // sourceShown = waitForSourceShown(toolbox.getCurrentPanel(), "code_ugly.js");
+ // ppButton.click();
+ // yield sourceShown;
+ // yield waitUntil(() => checkedUnpretty);
+
+
+ yield toolbox.destroy();
+ gBrowser.removeCurrentTab();
+ finish();
+});
+
+function checkPrettified(oldLoc, newLoc) {
+ is(oldLoc.line, 3, "Correct line for JS:3");
+ is(oldLoc.column, null, "Correct column for JS:3");
+ is(oldLoc.url, JS_URL, "Correct url for JS:3");
+ is(newLoc.line, 9, "Correct line for JS:3 -> PRETTY");
+ is(newLoc.column, 0, "Correct column for JS:3 -> PRETTY");
+ is(newLoc.url, JS_URL, "Correct url for JS:3 -> PRETTY");
+}
+
+function checkUnprettified(oldLoc, newLoc) {
+ is(oldLoc.line, 9, "Correct line for JS:3 -> PRETTY");
+ is(oldLoc.column, 0, "Correct column for JS:3 -> PRETTY");
+ is(oldLoc.url, JS_URL, "Correct url for JS:3 -> PRETTY");
+ is(newLoc.line, 3, "Correct line for JS:3 -> UNPRETTIED");
+ is(newLoc.column, null, "Correct column for JS:3 -> UNPRETTIED");
+ is(newLoc.url, JS_URL, "Correct url for JS:3 -> UNPRETTIED");
+}
+
+function createScript(url) {
+ info(`Creating script: ${url}`);
+ let mm = getFrameScript();
+ let command = `
+ let script = document.createElement("script");
+ script.setAttribute("src", "${url}");
+ document.body.appendChild(script);
+ `;
+ return evalInDebuggee(mm, command);
+}
+
+function waitForSourceShown(debuggerPanel, url) {
+ let { panelWin } = debuggerPanel;
+ let deferred = defer();
+
+ info(`Waiting for source ${url} to be shown in the debugger...`);
+ panelWin.on(panelWin.EVENTS.SOURCE_SHOWN, function onSourceShown(_, source) {
+ let sourceUrl = source.url || source.introductionUrl;
+
+ if (sourceUrl.includes(url)) {
+ panelWin.off(panelWin.EVENTS.SOURCE_SHOWN, onSourceShown);
+ info(`Source shown for ${url}`);
+ deferred.resolve(source);
+ }
+ });
+
+ return deferred.promise;
+}
diff --git a/devtools/client/framework/test/browser_target_events.js b/devtools/client/framework/test/browser_target_events.js
new file mode 100644
index 000000000..d0054a484
--- /dev/null
+++ b/devtools/client/framework/test/browser_target_events.js
@@ -0,0 +1,56 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+var target;
+
+function test()
+{
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(onLoad);
+}
+
+function onLoad() {
+ target = TargetFactory.forTab(gBrowser.selectedTab);
+
+ is(target.tab, gBrowser.selectedTab, "Target linked to the right tab.");
+
+ target.once("hidden", onHidden);
+ gBrowser.selectedTab = gBrowser.addTab();
+}
+
+function onHidden() {
+ ok(true, "Hidden event received");
+ target.once("visible", onVisible);
+ gBrowser.removeCurrentTab();
+}
+
+function onVisible() {
+ ok(true, "Visible event received");
+ target.once("will-navigate", onWillNavigate);
+ let mm = getFrameScript();
+ mm.sendAsyncMessage("devtools:test:navigate", { location: "data:text/html,<meta charset='utf8'/>test navigation" });
+}
+
+function onWillNavigate(event, request) {
+ ok(true, "will-navigate event received");
+ // Wait for navigation handling to complete before removing the tab, in order
+ // to avoid triggering assertions.
+ target.once("navigate", executeSoon.bind(null, onNavigate));
+}
+
+function onNavigate() {
+ ok(true, "navigate event received");
+ target.once("close", onClose);
+ gBrowser.removeCurrentTab();
+}
+
+function onClose() {
+ ok(true, "close event received");
+
+ target = null;
+ finish();
+}
diff --git a/devtools/client/framework/test/browser_target_from_url.js b/devtools/client/framework/test/browser_target_from_url.js
new file mode 100644
index 000000000..0707ee7d7
--- /dev/null
+++ b/devtools/client/framework/test/browser_target_from_url.js
@@ -0,0 +1,133 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<p>browser_target-from-url.js</p>";
+
+const { DevToolsLoader } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const { targetFromURL } = require("devtools/client/framework/target-from-url");
+
+Services.prefs.setBoolPref("devtools.debugger.remote-enabled", true);
+Services.prefs.setBoolPref("devtools.debugger.prompt-connection", false);
+
+SimpleTest.registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("devtools.debugger.remote-enabled");
+ Services.prefs.clearUserPref("devtools.debugger.prompt-connection");
+});
+
+function assertIsTabTarget(target, url, chrome = false) {
+ is(target.url, url);
+ is(target.isLocalTab, false);
+ is(target.chrome, chrome);
+ is(target.isTabActor, true);
+ is(target.isRemote, true);
+}
+
+add_task(function* () {
+ let tab = yield addTab(TEST_URI);
+ let browser = tab.linkedBrowser;
+ let target;
+
+ info("Test invalid type");
+ try {
+ yield targetFromURL(new URL("http://foo?type=x"));
+ ok(false, "Shouldn't pass");
+ } catch (e) {
+ is(e.message, "targetFromURL, unsupported type='x' parameter");
+ }
+
+ info("Test tab");
+ let windowId = browser.outerWindowID;
+ target = yield targetFromURL(new URL("http://foo?type=tab&id=" + windowId));
+ assertIsTabTarget(target, TEST_URI);
+
+ info("Test tab with chrome privileges");
+ target = yield targetFromURL(new URL("http://foo?type=tab&id=" + windowId + "&chrome"));
+ assertIsTabTarget(target, TEST_URI, true);
+
+ info("Test invalid tab id");
+ try {
+ yield targetFromURL(new URL("http://foo?type=tab&id=10000"));
+ ok(false, "Shouldn't pass");
+ } catch (e) {
+ is(e.message, "targetFromURL, tab with outerWindowID:'10000' doesn't exist");
+ }
+
+ info("Test parent process");
+ target = yield targetFromURL(new URL("http://foo?type=process"));
+ let topWindow = Services.wm.getMostRecentWindow("navigator:browser");
+ assertIsTabTarget(target, topWindow.location.href, true);
+
+ yield testRemoteTCP();
+ yield testRemoteWebSocket();
+
+ gBrowser.removeCurrentTab();
+});
+
+function* setupDebuggerServer(websocket) {
+ info("Create a separate loader instance for the DebuggerServer.");
+ let loader = new DevToolsLoader();
+ let { DebuggerServer } = loader.require("devtools/server/main");
+
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ DebuggerServer.allowChromeProcess = true;
+
+ let listener = DebuggerServer.createListener();
+ ok(listener, "Socket listener created");
+ // Pass -1 to automatically choose an available port
+ listener.portOrPath = -1;
+ listener.webSocket = websocket;
+ yield listener.open();
+ is(DebuggerServer.listeningSockets, 1, "1 listening socket");
+
+ return { DebuggerServer, listener };
+}
+
+function teardownDebuggerServer({ DebuggerServer, listener }) {
+ info("Close the listener socket");
+ listener.close();
+ is(DebuggerServer.listeningSockets, 0, "0 listening sockets");
+
+ info("Destroy the temporary debugger server");
+ DebuggerServer.destroy();
+}
+
+function* testRemoteTCP() {
+ info("Test remote process via TCP Connection");
+
+ let server = yield setupDebuggerServer(false);
+
+ let { port } = server.listener;
+ let target = yield targetFromURL(new URL("http://foo?type=process&host=127.0.0.1&port=" + port));
+ let topWindow = Services.wm.getMostRecentWindow("navigator:browser");
+ assertIsTabTarget(target, topWindow.location.href, true);
+
+ let settings = target.client._transport.connectionSettings;
+ is(settings.host, "127.0.0.1");
+ is(settings.port, port);
+ is(settings.webSocket, false);
+
+ yield target.client.close();
+
+ teardownDebuggerServer(server);
+}
+
+function* testRemoteWebSocket() {
+ info("Test remote process via WebSocket Connection");
+
+ let server = yield setupDebuggerServer(true);
+
+ let { port } = server.listener;
+ let target = yield targetFromURL(new URL("http://foo?type=process&host=127.0.0.1&port=" + port + "&ws=true"));
+ let topWindow = Services.wm.getMostRecentWindow("navigator:browser");
+ assertIsTabTarget(target, topWindow.location.href, true);
+
+ let settings = target.client._transport.connectionSettings;
+ is(settings.host, "127.0.0.1");
+ is(settings.port, port);
+ is(settings.webSocket, true);
+ yield target.client.close();
+
+ teardownDebuggerServer(server);
+}
diff --git a/devtools/client/framework/test/browser_target_remote.js b/devtools/client/framework/test/browser_target_remote.js
new file mode 100644
index 000000000..b828d14ff
--- /dev/null
+++ b/devtools/client/framework/test/browser_target_remote.js
@@ -0,0 +1,25 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Ensure target is closed if client is closed directly
+function test() {
+ waitForExplicitFinish();
+
+ getChromeActors((client, response) => {
+ let options = {
+ form: response,
+ client: client,
+ chrome: true
+ };
+
+ TargetFactory.forRemoteTab(options).then(target => {
+ target.on("close", () => {
+ ok(true, "Target was closed");
+ finish();
+ });
+ client.close();
+ });
+ });
+}
diff --git a/devtools/client/framework/test/browser_target_support.js b/devtools/client/framework/test/browser_target_support.js
new file mode 100644
index 000000000..0cdbd565a
--- /dev/null
+++ b/devtools/client/framework/test/browser_target_support.js
@@ -0,0 +1,74 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Test support methods on Target, such as `hasActor`, `getActorDescription`,
+// `actorHasMethod` and `getTrait`.
+
+var { WebAudioFront } =
+ require("devtools/shared/fronts/webaudio");
+
+function* testTarget(client, target) {
+ yield target.makeRemote();
+
+ is(target.hasActor("timeline"), true, "target.hasActor() true when actor exists.");
+ is(target.hasActor("webaudio"), true, "target.hasActor() true when actor exists.");
+ is(target.hasActor("notreal"), false, "target.hasActor() false when actor does not exist.");
+ // Create a front to ensure the actor is loaded
+ let front = new WebAudioFront(target.client, target.form);
+
+ let desc = yield target.getActorDescription("webaudio");
+ is(desc.typeName, "webaudio",
+ "target.getActorDescription() returns definition data for corresponding actor");
+ is(desc.events["start-context"]["type"], "startContext",
+ "target.getActorDescription() returns event data for corresponding actor");
+
+ desc = yield target.getActorDescription("nope");
+ is(desc, undefined, "target.getActorDescription() returns undefined for non-existing actor");
+ desc = yield target.getActorDescription();
+ is(desc, undefined, "target.getActorDescription() returns undefined for undefined actor");
+
+ let hasMethod = yield target.actorHasMethod("audionode", "getType");
+ is(hasMethod, true,
+ "target.actorHasMethod() returns true for existing actor with method");
+ hasMethod = yield target.actorHasMethod("audionode", "nope");
+ is(hasMethod, false,
+ "target.actorHasMethod() returns false for existing actor with no method");
+ hasMethod = yield target.actorHasMethod("nope", "nope");
+ is(hasMethod, false,
+ "target.actorHasMethod() returns false for non-existing actor with no method");
+ hasMethod = yield target.actorHasMethod();
+ is(hasMethod, false,
+ "target.actorHasMethod() returns false for undefined params");
+
+ is(target.getTrait("customHighlighters"), true,
+ "target.getTrait() returns boolean when trait exists");
+ is(target.getTrait("giddyup"), undefined,
+ "target.getTrait() returns undefined when trait does not exist");
+
+ close(target, client);
+}
+
+// Ensure target is closed if client is closed directly
+function test() {
+ waitForExplicitFinish();
+
+ getChromeActors((client, response) => {
+ let options = {
+ form: response,
+ client: client,
+ chrome: true
+ };
+
+ TargetFactory.forRemoteTab(options).then(Task.async(testTarget).bind(null, client));
+ });
+}
+
+function close(target, client) {
+ target.on("close", () => {
+ ok(true, "Target was closed");
+ finish();
+ });
+ client.close();
+}
diff --git a/devtools/client/framework/test/browser_toolbox_custom_host.js b/devtools/client/framework/test/browser_toolbox_custom_host.js
new file mode 100644
index 000000000..5d3aeed54
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_custom_host.js
@@ -0,0 +1,57 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+const TEST_URL = "data:text/html,test custom host";
+
+function test() {
+ let {Toolbox} = require("devtools/client/framework/toolbox");
+
+ let toolbox, iframe, target;
+
+ window.addEventListener("message", onMessage);
+
+ iframe = document.createElement("iframe");
+ document.documentElement.appendChild(iframe);
+
+ addTab(TEST_URL).then(function (tab) {
+ target = TargetFactory.forTab(tab);
+ let options = {customIframe: iframe};
+ gDevTools.showToolbox(target, null, Toolbox.HostType.CUSTOM, options)
+ .then(testCustomHost, console.error)
+ .then(null, console.error);
+ });
+
+ function onMessage(event) {
+ if (typeof(event.data) !== "string") {
+ return;
+ }
+ info("onMessage: " + event.data);
+ let json = JSON.parse(event.data);
+ if (json.name == "toolbox-close") {
+ ok("Got the `toolbox-close` message");
+ window.removeEventListener("message", onMessage);
+ cleanup();
+ }
+ }
+
+ function testCustomHost(t) {
+ toolbox = t;
+ is(toolbox.win.top, window, "Toolbox is included in browser.xul");
+ is(toolbox.doc, iframe.contentDocument, "Toolbox is in the custom iframe");
+ executeSoon(() => gBrowser.removeCurrentTab());
+ }
+
+ function cleanup() {
+ iframe.remove();
+
+ // Even if we received "toolbox-close", the toolbox may still be destroying
+ // toolbox.destroy() returns a singleton promise that ensures
+ // everything is cleaned up before proceeding.
+ toolbox.destroy().then(() => {
+ toolbox = iframe = target = null;
+ finish();
+ });
+ }
+}
diff --git a/devtools/client/framework/test/browser_toolbox_dynamic_registration.js b/devtools/client/framework/test/browser_toolbox_dynamic_registration.js
new file mode 100644
index 000000000..2583ca68e
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_dynamic_registration.js
@@ -0,0 +1,105 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+const TEST_URL = "data:text/html,test for dynamically registering and unregistering tools";
+
+var toolbox;
+
+function test()
+{
+ addTab(TEST_URL).then(tab => {
+ let target = TargetFactory.forTab(tab);
+ gDevTools.showToolbox(target).then(testRegister);
+ });
+}
+
+function testRegister(aToolbox)
+{
+ toolbox = aToolbox;
+ gDevTools.once("tool-registered", toolRegistered);
+
+ gDevTools.registerTool({
+ id: "test-tool",
+ label: "Test Tool",
+ inMenu: true,
+ isTargetSupported: () => true,
+ build: function () {},
+ key: "t"
+ });
+}
+
+function toolRegistered(event, toolId)
+{
+ is(toolId, "test-tool", "tool-registered event handler sent tool id");
+
+ ok(gDevTools.getToolDefinitionMap().has(toolId), "tool added to map");
+
+ // test that it appeared in the UI
+ let doc = toolbox.doc;
+ let tab = doc.getElementById("toolbox-tab-" + toolId);
+ ok(tab, "new tool's tab exists in toolbox UI");
+
+ let panel = doc.getElementById("toolbox-panel-" + toolId);
+ ok(panel, "new tool's panel exists in toolbox UI");
+
+ for (let win of getAllBrowserWindows()) {
+ let key = win.document.getElementById("key_" + toolId);
+ ok(key, "key for new tool added to every browser window");
+ let menuitem = win.document.getElementById("menuitem_" + toolId);
+ ok(menuitem, "menu item of new tool added to every browser window");
+ }
+
+ // then unregister it
+ testUnregister();
+}
+
+function getAllBrowserWindows() {
+ let wins = [];
+ let enumerator = Services.wm.getEnumerator("navigator:browser");
+ while (enumerator.hasMoreElements()) {
+ wins.push(enumerator.getNext());
+ }
+ return wins;
+}
+
+function testUnregister()
+{
+ gDevTools.once("tool-unregistered", toolUnregistered);
+
+ gDevTools.unregisterTool("test-tool");
+}
+
+function toolUnregistered(event, toolDefinition)
+{
+ let toolId = toolDefinition.id;
+ is(toolId, "test-tool", "tool-unregistered event handler sent tool id");
+
+ ok(!gDevTools.getToolDefinitionMap().has(toolId), "tool removed from map");
+
+ // test that it disappeared from the UI
+ let doc = toolbox.doc;
+ let tab = doc.getElementById("toolbox-tab-" + toolId);
+ ok(!tab, "tool's tab was removed from the toolbox UI");
+
+ let panel = doc.getElementById("toolbox-panel-" + toolId);
+ ok(!panel, "tool's panel was removed from toolbox UI");
+
+ for (let win of getAllBrowserWindows()) {
+ let key = win.document.getElementById("key_" + toolId);
+ ok(!key, "key removed from every browser window");
+ let menuitem = win.document.getElementById("menuitem_" + toolId);
+ ok(!menuitem, "menu item removed from every browser window");
+ }
+
+ cleanup();
+}
+
+function cleanup()
+{
+ toolbox.destroy();
+ toolbox = null;
+ gBrowser.removeCurrentTab();
+ finish();
+}
diff --git a/devtools/client/framework/test/browser_toolbox_getpanelwhenready.js b/devtools/client/framework/test/browser_toolbox_getpanelwhenready.js
new file mode 100644
index 000000000..21dd236a1
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_getpanelwhenready.js
@@ -0,0 +1,36 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests that getPanelWhenReady returns the correct panel in promise
+// resolutions regardless of whether it has opened first.
+
+var toolbox = null;
+
+const URL = "data:text/html;charset=utf8,test for getPanelWhenReady";
+
+add_task(function* () {
+ let tab = yield addTab(URL);
+ let target = TargetFactory.forTab(tab);
+ toolbox = yield gDevTools.showToolbox(target);
+
+ let debuggerPanelPromise = toolbox.getPanelWhenReady("jsdebugger");
+ yield toolbox.selectTool("jsdebugger");
+ let debuggerPanel = yield debuggerPanelPromise;
+
+ is(debuggerPanel, toolbox.getPanel("jsdebugger"),
+ "The debugger panel from getPanelWhenReady before loading is the actual panel");
+
+ let debuggerPanel2 = yield toolbox.getPanelWhenReady("jsdebugger");
+ is(debuggerPanel2, toolbox.getPanel("jsdebugger"),
+ "The debugger panel from getPanelWhenReady after loading is the actual panel");
+
+ yield cleanup();
+});
+
+function* cleanup() {
+ yield toolbox.destroy();
+ gBrowser.removeCurrentTab();
+ toolbox = null;
+}
diff --git a/devtools/client/framework/test/browser_toolbox_highlight.js b/devtools/client/framework/test/browser_toolbox_highlight.js
new file mode 100644
index 000000000..d197fdc99
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_highlight.js
@@ -0,0 +1,81 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+var {Toolbox} = require("devtools/client/framework/toolbox");
+
+var toolbox = null;
+
+function test() {
+ const URL = "data:text/plain;charset=UTF-8,Nothing to see here, move along";
+
+ const TOOL_ID_1 = "jsdebugger";
+ const TOOL_ID_2 = "webconsole";
+
+ addTab(URL).then(() => {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ gDevTools.showToolbox(target, TOOL_ID_1, Toolbox.HostType.BOTTOM)
+ .then(aToolbox => {
+ toolbox = aToolbox;
+ // select tool 2
+ toolbox.selectTool(TOOL_ID_2)
+ // and highlight the first one
+ .then(highlightTab.bind(null, TOOL_ID_1))
+ // to see if it has the proper class.
+ .then(checkHighlighted.bind(null, TOOL_ID_1))
+ // Now switch back to first tool
+ .then(() => toolbox.selectTool(TOOL_ID_1))
+ // to check again. But there is no easy way to test if
+ // it is showing orange or not.
+ .then(checkNoHighlightWhenSelected.bind(null, TOOL_ID_1))
+ // Switch to tool 2 again
+ .then(() => toolbox.selectTool(TOOL_ID_2))
+ // and check again.
+ .then(checkHighlighted.bind(null, TOOL_ID_1))
+ // Now unhighlight the tool
+ .then(unhighlightTab.bind(null, TOOL_ID_1))
+ // to see the classes gone.
+ .then(checkNoHighlight.bind(null, TOOL_ID_1))
+ // Now close the toolbox and exit.
+ .then(() => executeSoon(() => {
+ toolbox.destroy()
+ .then(() => {
+ toolbox = null;
+ gBrowser.removeCurrentTab();
+ finish();
+ });
+ }));
+ });
+ });
+}
+
+function highlightTab(toolId) {
+ info("Highlighting tool " + toolId + "'s tab.");
+ toolbox.highlightTool(toolId);
+}
+
+function unhighlightTab(toolId) {
+ info("Unhighlighting tool " + toolId + "'s tab.");
+ toolbox.unhighlightTool(toolId);
+}
+
+function checkHighlighted(toolId) {
+ let tab = toolbox.doc.getElementById("toolbox-tab-" + toolId);
+ ok(tab.hasAttribute("highlighted"), "The highlighted attribute is present");
+ ok(!tab.hasAttribute("selected") || tab.getAttribute("selected") != "true",
+ "The tab is not selected");
+}
+
+function checkNoHighlightWhenSelected(toolId) {
+ let tab = toolbox.doc.getElementById("toolbox-tab-" + toolId);
+ ok(tab.hasAttribute("highlighted"), "The highlighted attribute is present");
+ ok(tab.hasAttribute("selected") && tab.getAttribute("selected") == "true",
+ "and the tab is selected, so the orange glow will not be present.");
+}
+
+function checkNoHighlight(toolId) {
+ let tab = toolbox.doc.getElementById("toolbox-tab-" + toolId);
+ ok(!tab.hasAttribute("highlighted"),
+ "The highlighted attribute is not present");
+}
diff --git a/devtools/client/framework/test/browser_toolbox_hosts.js b/devtools/client/framework/test/browser_toolbox_hosts.js
new file mode 100644
index 000000000..e16563ba7
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_hosts.js
@@ -0,0 +1,139 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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";
+
+var {Toolbox} = require("devtools/client/framework/toolbox");
+var {SIDE, BOTTOM, WINDOW} = Toolbox.HostType;
+var toolbox, target;
+
+const URL = "data:text/html;charset=utf8,test for opening toolbox in different hosts";
+
+add_task(function* runTest() {
+ info("Create a test tab and open the toolbox");
+ let tab = yield addTab(URL);
+ target = TargetFactory.forTab(tab);
+ toolbox = yield gDevTools.showToolbox(target, "webconsole");
+
+ yield testBottomHost();
+ yield testSidebarHost();
+ yield testWindowHost();
+ yield testToolSelect();
+ yield testDestroy();
+ yield testRememberHost();
+ yield testPreviousHost();
+
+ yield toolbox.destroy();
+
+ toolbox = target = null;
+ gBrowser.removeCurrentTab();
+});
+
+function* testBottomHost() {
+ checkHostType(toolbox, BOTTOM);
+
+ // test UI presence
+ let nbox = gBrowser.getNotificationBox();
+ let iframe = document.getAnonymousElementByAttribute(nbox, "class", "devtools-toolbox-bottom-iframe");
+ ok(iframe, "toolbox bottom iframe exists");
+
+ checkToolboxLoaded(iframe);
+}
+
+function* testSidebarHost() {
+ yield toolbox.switchHost(SIDE);
+ checkHostType(toolbox, SIDE);
+
+ // test UI presence
+ let nbox = gBrowser.getNotificationBox();
+ let bottom = document.getAnonymousElementByAttribute(nbox, "class", "devtools-toolbox-bottom-iframe");
+ ok(!bottom, "toolbox bottom iframe doesn't exist");
+
+ let iframe = document.getAnonymousElementByAttribute(nbox, "class", "devtools-toolbox-side-iframe");
+ ok(iframe, "toolbox side iframe exists");
+
+ checkToolboxLoaded(iframe);
+}
+
+function* testWindowHost() {
+ yield toolbox.switchHost(WINDOW);
+ checkHostType(toolbox, WINDOW);
+
+ let nbox = gBrowser.getNotificationBox();
+ let sidebar = document.getAnonymousElementByAttribute(nbox, "class", "devtools-toolbox-side-iframe");
+ ok(!sidebar, "toolbox sidebar iframe doesn't exist");
+
+ let win = Services.wm.getMostRecentWindow("devtools:toolbox");
+ ok(win, "toolbox separate window exists");
+
+ let iframe = win.document.getElementById("toolbox-iframe");
+ checkToolboxLoaded(iframe);
+}
+
+function* testToolSelect() {
+ // make sure we can load a tool after switching hosts
+ yield toolbox.selectTool("inspector");
+}
+
+function* testDestroy() {
+ yield toolbox.destroy();
+ target = TargetFactory.forTab(gBrowser.selectedTab);
+ toolbox = yield gDevTools.showToolbox(target);
+}
+
+function* testRememberHost() {
+ // last host was the window - make sure it's the same when re-opening
+ is(toolbox.hostType, WINDOW, "host remembered");
+
+ let win = Services.wm.getMostRecentWindow("devtools:toolbox");
+ ok(win, "toolbox separate window exists");
+}
+
+function* testPreviousHost() {
+ // last host was the window - make sure it's the same when re-opening
+ is(toolbox.hostType, WINDOW, "host remembered");
+
+ info("Switching to side");
+ yield toolbox.switchHost(SIDE);
+ checkHostType(toolbox, SIDE, WINDOW);
+
+ info("Switching to bottom");
+ yield toolbox.switchHost(BOTTOM);
+ checkHostType(toolbox, BOTTOM, SIDE);
+
+ info("Switching from bottom to side");
+ yield toolbox.switchToPreviousHost();
+ checkHostType(toolbox, SIDE, BOTTOM);
+
+ info("Switching from side to bottom");
+ yield toolbox.switchToPreviousHost();
+ checkHostType(toolbox, BOTTOM, SIDE);
+
+ info("Switching to window");
+ yield toolbox.switchHost(WINDOW);
+ checkHostType(toolbox, WINDOW, BOTTOM);
+
+ info("Switching from window to bottom");
+ yield toolbox.switchToPreviousHost();
+ checkHostType(toolbox, BOTTOM, WINDOW);
+
+ info("Forcing the previous host to match the current (bottom)");
+ Services.prefs.setCharPref("devtools.toolbox.previousHost", BOTTOM);
+
+ info("Switching from bottom to side (since previous=current=bottom");
+ yield toolbox.switchToPreviousHost();
+ checkHostType(toolbox, SIDE, BOTTOM);
+
+ info("Forcing the previous host to match the current (side)");
+ Services.prefs.setCharPref("devtools.toolbox.previousHost", SIDE);
+ info("Switching from side to bottom (since previous=current=side");
+ yield toolbox.switchToPreviousHost();
+ checkHostType(toolbox, BOTTOM, SIDE);
+}
+
+function checkToolboxLoaded(iframe) {
+ let tabs = iframe.contentDocument.getElementById("toolbox-tabs");
+ ok(tabs, "toolbox UI has been loaded into iframe");
+}
diff --git a/devtools/client/framework/test/browser_toolbox_hosts_size.js b/devtools/client/framework/test/browser_toolbox_hosts_size.js
new file mode 100644
index 000000000..4286fe438
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_hosts_size.js
@@ -0,0 +1,69 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests that getPanelWhenReady returns the correct panel in promise
+// resolutions regardless of whether it has opened first.
+
+const URL = "data:text/html;charset=utf8,test for host sizes";
+
+var {Toolbox} = require("devtools/client/framework/toolbox");
+
+add_task(function* () {
+ // Set size prefs to make the hosts way too big, so that the size has
+ // to be clamped to fit into the browser window.
+ Services.prefs.setIntPref("devtools.toolbox.footer.height", 10000);
+ Services.prefs.setIntPref("devtools.toolbox.sidebar.width", 10000);
+
+ let tab = yield addTab(URL);
+ let nbox = gBrowser.getNotificationBox();
+ let {clientHeight: nboxHeight, clientWidth: nboxWidth} = nbox;
+ let toolbox = yield gDevTools.showToolbox(TargetFactory.forTab(tab));
+
+ is(nbox.clientHeight, nboxHeight, "Opening the toolbox hasn't changed the height of the nbox");
+ is(nbox.clientWidth, nboxWidth, "Opening the toolbox hasn't changed the width of the nbox");
+
+ let iframe = document.getAnonymousElementByAttribute(nbox, "class", "devtools-toolbox-bottom-iframe");
+ is(iframe.clientHeight, nboxHeight - 25, "The iframe fits within the available space");
+
+ yield toolbox.switchHost(Toolbox.HostType.SIDE);
+ iframe = document.getAnonymousElementByAttribute(nbox, "class", "devtools-toolbox-side-iframe");
+ iframe.style.minWidth = "1px"; // Disable the min width set in css
+ is(iframe.clientWidth, nboxWidth - 25, "The iframe fits within the available space");
+
+ yield cleanup(toolbox);
+});
+
+add_task(function* () {
+ // Set size prefs to something reasonable, so we can check to make sure
+ // they are being set properly.
+ Services.prefs.setIntPref("devtools.toolbox.footer.height", 100);
+ Services.prefs.setIntPref("devtools.toolbox.sidebar.width", 100);
+
+ let tab = yield addTab(URL);
+ let nbox = gBrowser.getNotificationBox();
+ let {clientHeight: nboxHeight, clientWidth: nboxWidth} = nbox;
+ let toolbox = yield gDevTools.showToolbox(TargetFactory.forTab(tab));
+
+ is(nbox.clientHeight, nboxHeight, "Opening the toolbox hasn't changed the height of the nbox");
+ is(nbox.clientWidth, nboxWidth, "Opening the toolbox hasn't changed the width of the nbox");
+
+ let iframe = document.getAnonymousElementByAttribute(nbox, "class", "devtools-toolbox-bottom-iframe");
+ is(iframe.clientHeight, 100, "The iframe is resized properly");
+
+ yield toolbox.switchHost(Toolbox.HostType.SIDE);
+ iframe = document.getAnonymousElementByAttribute(nbox, "class", "devtools-toolbox-side-iframe");
+ iframe.style.minWidth = "1px"; // Disable the min width set in css
+ is(iframe.clientWidth, 100, "The iframe is resized properly");
+
+ yield cleanup(toolbox);
+});
+
+function* cleanup(toolbox) {
+ Services.prefs.clearUserPref("devtools.toolbox.host");
+ Services.prefs.clearUserPref("devtools.toolbox.footer.height");
+ Services.prefs.clearUserPref("devtools.toolbox.sidebar.width");
+ yield toolbox.destroy();
+ gBrowser.removeCurrentTab();
+}
diff --git a/devtools/client/framework/test/browser_toolbox_hosts_telemetry.js b/devtools/client/framework/test/browser_toolbox_hosts_telemetry.js
new file mode 100644
index 000000000..f8ff9b3e4
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_hosts_telemetry.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {Toolbox} = require("devtools/client/framework/toolbox");
+const {SIDE, BOTTOM, WINDOW} = Toolbox.HostType;
+
+const URL = "data:text/html;charset=utf8,browser_toolbox_hosts_telemetry.js";
+
+function getHostHistogram() {
+ return Services.telemetry.getHistogramById("DEVTOOLS_TOOLBOX_HOST");
+}
+
+add_task(function* () {
+ // Reset it to make counting easier
+ getHostHistogram().clear();
+
+ info("Create a test tab and open the toolbox");
+ let tab = yield addTab(URL);
+ let target = TargetFactory.forTab(tab);
+ let toolbox = yield gDevTools.showToolbox(target, "webconsole");
+
+ yield changeToolboxHost(toolbox);
+ yield checkResults();
+ yield toolbox.destroy();
+
+ toolbox = target = null;
+ gBrowser.removeCurrentTab();
+
+ // Cleanup
+ getHostHistogram().clear();
+});
+
+function* changeToolboxHost(toolbox) {
+ info("Switch toolbox host");
+ yield toolbox.switchHost(SIDE);
+ yield toolbox.switchHost(WINDOW);
+ yield toolbox.switchHost(BOTTOM);
+ yield toolbox.switchHost(SIDE);
+ yield toolbox.switchHost(WINDOW);
+ yield toolbox.switchHost(BOTTOM);
+}
+
+function checkResults() {
+ let counts = getHostHistogram().snapshot().counts;
+ is(counts[0], 3, "Toolbox HostType bottom has 3 successful entries");
+ is(counts[1], 2, "Toolbox HostType side has 2 successful entries");
+ is(counts[2], 2, "Toolbox HostType window has 2 successful entries");
+}
diff --git a/devtools/client/framework/test/browser_toolbox_keyboard_navigation.js b/devtools/client/framework/test/browser_toolbox_keyboard_navigation.js
new file mode 100644
index 000000000..a22f87064
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_keyboard_navigation.js
@@ -0,0 +1,81 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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";
+
+// Tests keyboard navigation of devtools tabbar.
+
+const TEST_URL =
+ "data:text/html;charset=utf8,test page for toolbar keyboard navigation";
+
+function containsFocus(aDoc, aElm) {
+ let elm = aDoc.activeElement;
+ while (elm) {
+ if (elm === aElm) { return true; }
+ elm = elm.parentNode;
+ }
+ return false;
+}
+
+add_task(function* () {
+ info("Create a test tab and open the toolbox");
+ let toolbox = yield openNewTabAndToolbox(TEST_URL, "webconsole");
+ let doc = toolbox.doc;
+
+ let toolbar = doc.querySelector(".devtools-tabbar");
+ let toolbarControls = [...toolbar.querySelectorAll(
+ ".devtools-tab, button")].filter(elm =>
+ !elm.hidden && doc.defaultView.getComputedStyle(elm).getPropertyValue(
+ "display") !== "none");
+
+ // Put the keyboard focus onto the first toolbar control.
+ toolbarControls[0].focus();
+ ok(containsFocus(doc, toolbar), "Focus is within the toolbar");
+
+ // Move the focus away from toolbar to a next focusable element.
+ EventUtils.synthesizeKey("VK_TAB", {});
+ ok(!containsFocus(doc, toolbar), "Focus is outside of the toolbar");
+
+ // Move the focus back to the toolbar.
+ EventUtils.synthesizeKey("VK_TAB", { shiftKey: true });
+ ok(containsFocus(doc, toolbar), "Focus is within the toolbar again");
+
+ // Move through the toolbar forward using the right arrow key.
+ for (let i = 0; i < toolbarControls.length; ++i) {
+ is(doc.activeElement.id, toolbarControls[i].id, "New control is focused");
+ if (i < toolbarControls.length - 1) {
+ EventUtils.synthesizeKey("VK_RIGHT", {});
+ }
+ }
+
+ // Move the focus away from toolbar to a next focusable element.
+ EventUtils.synthesizeKey("VK_TAB", {});
+ ok(!containsFocus(doc, toolbar), "Focus is outside of the toolbar");
+
+ // Move the focus back to the toolbar.
+ EventUtils.synthesizeKey("VK_TAB", { shiftKey: true });
+ ok(containsFocus(doc, toolbar), "Focus is within the toolbar again");
+
+ // Move through the toolbar backward using the left arrow key.
+ for (let i = toolbarControls.length - 1; i >= 0; --i) {
+ is(doc.activeElement.id, toolbarControls[i].id, "New control is focused");
+ if (i > 0) { EventUtils.synthesizeKey("VK_LEFT", {}); }
+ }
+
+ // Move focus to the 3rd (non-first) toolbar control.
+ let expectedFocusedControl = toolbarControls[2];
+ EventUtils.synthesizeKey("VK_RIGHT", {});
+ EventUtils.synthesizeKey("VK_RIGHT", {});
+ is(doc.activeElement.id, expectedFocusedControl.id, "New control is focused");
+
+ // Move the focus away from toolbar to a next focusable element.
+ EventUtils.synthesizeKey("VK_TAB", {});
+ ok(!containsFocus(doc, toolbar), "Focus is outside of the toolbar");
+
+ // Move the focus back to the toolbar, ensure we land on the last active
+ // descendant control.
+ EventUtils.synthesizeKey("VK_TAB", { shiftKey: true });
+ is(doc.activeElement.id, expectedFocusedControl.id, "New control is focused");
+});
diff --git a/devtools/client/framework/test/browser_toolbox_minimize.js b/devtools/client/framework/test/browser_toolbox_minimize.js
new file mode 100644
index 000000000..9b5126320
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_minimize.js
@@ -0,0 +1,106 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 when the toolbox is displayed in a bottom host, that host can be
+// minimized to just the tabbar height, and maximized again.
+// Also test that while minimized, switching to a tool, clicking on the
+// settings, or clicking on the selected tool's tab maximizes the toolbox again.
+// Finally test that the minimize button doesn't exist in other host types.
+
+const URL = "data:text/html;charset=utf8,test page";
+const {Toolbox} = require("devtools/client/framework/toolbox");
+
+add_task(function* () {
+ info("Create a test tab and open the toolbox");
+ let tab = yield addTab(URL);
+ let target = TargetFactory.forTab(tab);
+ let toolbox = yield gDevTools.showToolbox(target, "webconsole");
+
+ let button = toolbox.doc.querySelector("#toolbox-dock-bottom-minimize");
+ ok(button, "The minimize button exists in the default bottom host");
+
+ info("Try to minimize the toolbox");
+ yield minimize(toolbox);
+ ok(parseInt(toolbox._host.frame.style.marginBottom, 10) < 0,
+ "The toolbox host has been hidden away with a negative-margin");
+
+ info("Try to maximize again the toolbox");
+ yield maximize(toolbox);
+ ok(parseInt(toolbox._host.frame.style.marginBottom, 10) == 0,
+ "The toolbox host is shown again");
+
+ info("Try to minimize again using the keyboard shortcut");
+ yield minimizeWithShortcut(toolbox);
+ ok(parseInt(toolbox._host.frame.style.marginBottom, 10) < 0,
+ "The toolbox host has been hidden away with a negative-margin");
+
+ info("Try to maximize again using the keyboard shortcut");
+ yield maximizeWithShortcut(toolbox);
+ ok(parseInt(toolbox._host.frame.style.marginBottom, 10) == 0,
+ "The toolbox host is shown again");
+
+ info("Minimize again and switch to another tool");
+ yield minimize(toolbox);
+ let onMaximized = toolbox._host.once("maximized");
+ yield toolbox.selectTool("inspector");
+ yield onMaximized;
+
+ info("Minimize again and click on the tab of the current tool");
+ yield minimize(toolbox);
+ onMaximized = toolbox._host.once("maximized");
+ let tabButton = toolbox.doc.querySelector("#toolbox-tab-inspector");
+ EventUtils.synthesizeMouseAtCenter(tabButton, {}, toolbox.win);
+ yield onMaximized;
+
+ info("Minimize again and click on the settings tab");
+ yield minimize(toolbox);
+ onMaximized = toolbox._host.once("maximized");
+ let settingsButton = toolbox.doc.querySelector("#toolbox-tab-options");
+ EventUtils.synthesizeMouseAtCenter(settingsButton, {}, toolbox.win);
+ yield onMaximized;
+
+ info("Switch to a different host");
+ yield toolbox.switchHost(Toolbox.HostType.SIDE);
+ button = toolbox.doc.querySelector("#toolbox-dock-bottom-minimize");
+ ok(!button, "The minimize button doesn't exist in the side host");
+
+ Services.prefs.clearUserPref("devtools.toolbox.host");
+ yield toolbox.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+function* minimize(toolbox) {
+ let button = toolbox.doc.querySelector("#toolbox-dock-bottom-minimize");
+ let onMinimized = toolbox._host.once("minimized");
+ EventUtils.synthesizeMouseAtCenter(button, {}, toolbox.win);
+ yield onMinimized;
+}
+
+function* minimizeWithShortcut(toolbox) {
+ let key = toolbox.doc.getElementById("toolbox-minimize-key")
+ .getAttribute("key");
+ let onMinimized = toolbox._host.once("minimized");
+ EventUtils.synthesizeKey(key, {accelKey: true, shiftKey: true},
+ toolbox.win);
+ yield onMinimized;
+}
+
+function* maximize(toolbox) {
+ let button = toolbox.doc.querySelector("#toolbox-dock-bottom-minimize");
+ let onMaximized = toolbox._host.once("maximized");
+ EventUtils.synthesizeMouseAtCenter(button, {}, toolbox.win);
+ yield onMaximized;
+}
+
+function* maximizeWithShortcut(toolbox) {
+ let key = toolbox.doc.getElementById("toolbox-minimize-key")
+ .getAttribute("key");
+ let onMaximized = toolbox._host.once("maximized");
+ EventUtils.synthesizeKey(key, {accelKey: true, shiftKey: true},
+ toolbox.win);
+ yield onMaximized;
+}
diff --git a/devtools/client/framework/test/browser_toolbox_options.js b/devtools/client/framework/test/browser_toolbox_options.js
new file mode 100644
index 000000000..569ed86fb
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_options.js
@@ -0,0 +1,297 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/* import-globals-from shared-head.js */
+"use strict";
+
+// Tests that changing preferences in the options panel updates the prefs
+// and toggles appropriate things in the toolbox.
+
+var doc = null, toolbox = null, panelWin = null, modifiedPrefs = [];
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper("devtools/client/locales/toolbox.properties");
+
+add_task(function* () {
+ const URL = "data:text/html;charset=utf8,test for dynamically registering " +
+ "and unregistering tools";
+ registerNewTool();
+ let tab = yield addTab(URL);
+ let target = TargetFactory.forTab(tab);
+ toolbox = yield gDevTools.showToolbox(target);
+ doc = toolbox.doc;
+ yield testSelectTool();
+ yield testOptionsShortcut();
+ yield testOptions();
+ yield testToggleTools();
+ yield cleanup();
+});
+
+function registerNewTool() {
+ let toolDefinition = {
+ id: "test-tool",
+ isTargetSupported: () => true,
+ visibilityswitch: "devtools.test-tool.enabled",
+ url: "about:blank",
+ label: "someLabel"
+ };
+
+ ok(gDevTools, "gDevTools exists");
+ ok(!gDevTools.getToolDefinitionMap().has("test-tool"),
+ "The tool is not registered");
+
+ gDevTools.registerTool(toolDefinition);
+ ok(gDevTools.getToolDefinitionMap().has("test-tool"),
+ "The tool is registered");
+}
+
+function* testSelectTool() {
+ info("Checking to make sure that the options panel can be selected.");
+
+ let onceSelected = toolbox.once("options-selected");
+ toolbox.selectTool("options");
+ yield onceSelected;
+ ok(true, "Toolbox selected via selectTool method");
+}
+
+function* testOptionsShortcut() {
+ info("Selecting another tool, then reselecting options panel with keyboard.");
+
+ yield toolbox.selectTool("webconsole");
+ is(toolbox.currentToolId, "webconsole", "webconsole is selected");
+ synthesizeKeyShortcut(L10N.getStr("toolbox.options.key"));
+ is(toolbox.currentToolId, "options", "Toolbox selected via shortcut key (1)");
+ synthesizeKeyShortcut(L10N.getStr("toolbox.options.key"));
+ is(toolbox.currentToolId, "webconsole", "webconsole is selected (1)");
+
+ yield toolbox.selectTool("webconsole");
+ is(toolbox.currentToolId, "webconsole", "webconsole is selected");
+ synthesizeKeyShortcut(L10N.getStr("toolbox.help.key"));
+ is(toolbox.currentToolId, "options", "Toolbox selected via shortcut key (2)");
+ synthesizeKeyShortcut(L10N.getStr("toolbox.options.key"));
+ is(toolbox.currentToolId, "webconsole", "webconsole is reselected (2)");
+ synthesizeKeyShortcut(L10N.getStr("toolbox.help.key"));
+ is(toolbox.currentToolId, "options", "Toolbox selected via shortcut key (2)");
+}
+
+function* testOptions() {
+ let tool = toolbox.getPanel("options");
+ panelWin = tool.panelWin;
+ let prefNodes = tool.panelDoc.querySelectorAll(
+ "input[type=checkbox][data-pref]");
+
+ // Store modified pref names so that they can be cleared on error.
+ for (let node of tool.panelDoc.querySelectorAll("[data-pref]")) {
+ let pref = node.getAttribute("data-pref");
+ modifiedPrefs.push(pref);
+ }
+
+ for (let node of prefNodes) {
+ let prefValue = GetPref(node.getAttribute("data-pref"));
+
+ // Test clicking the checkbox for each options pref
+ yield testMouseClick(node, prefValue);
+
+ // Do again with opposite values to reset prefs
+ yield testMouseClick(node, !prefValue);
+ }
+
+ let prefSelects = tool.panelDoc.querySelectorAll("select[data-pref]");
+ for (let node of prefSelects) {
+ yield testSelect(node);
+ }
+}
+
+function* testSelect(select) {
+ let pref = select.getAttribute("data-pref");
+ let options = Array.from(select.options);
+ info("Checking select for: " + pref);
+
+ is(select.options[select.selectedIndex].value, GetPref(pref),
+ "select starts out selected");
+
+ for (let option of options) {
+ if (options.indexOf(option) === select.selectedIndex) {
+ continue;
+ }
+
+ let deferred = defer();
+ gDevTools.once("pref-changed", (event, data) => {
+ if (data.pref == pref) {
+ ok(true, "Correct pref was changed");
+ is(GetPref(pref), option.value, "Preference been switched for " + pref);
+ } else {
+ ok(false, "Pref " + pref + " was not changed correctly");
+ }
+ deferred.resolve();
+ });
+
+ select.selectedIndex = options.indexOf(option);
+ let changeEvent = new Event("change");
+ select.dispatchEvent(changeEvent);
+
+ yield deferred.promise;
+ }
+}
+
+function* testMouseClick(node, prefValue) {
+ let deferred = defer();
+
+ let pref = node.getAttribute("data-pref");
+ gDevTools.once("pref-changed", (event, data) => {
+ if (data.pref == pref) {
+ ok(true, "Correct pref was changed");
+ is(data.oldValue, prefValue, "Previous value is correct for " + pref);
+ is(data.newValue, !prefValue, "New value is correct for " + pref);
+ } else {
+ ok(false, "Pref " + pref + " was not changed correctly");
+ }
+ deferred.resolve();
+ });
+
+ node.scrollIntoView();
+
+ // We use executeSoon here to ensure that the element is in view and
+ // clickable.
+ executeSoon(function () {
+ info("Click event synthesized for pref " + pref);
+ EventUtils.synthesizeMouseAtCenter(node, {}, panelWin);
+ });
+
+ yield deferred.promise;
+}
+
+function* testToggleTools() {
+ let toolNodes = panelWin.document.querySelectorAll(
+ "#default-tools-box input[type=checkbox]:not([data-unsupported])," +
+ "#additional-tools-box input[type=checkbox]:not([data-unsupported])");
+ let enabledTools = [...toolNodes].filter(node => node.checked);
+
+ let toggleableTools = gDevTools.getDefaultTools().filter(tool => {
+ return tool.visibilityswitch;
+ }).concat(gDevTools.getAdditionalTools());
+
+ for (let node of toolNodes) {
+ let id = node.getAttribute("id");
+ ok(toggleableTools.some(tool => tool.id === id),
+ "There should be a toggle checkbox for: " + id);
+ }
+
+ // Store modified pref names so that they can be cleared on error.
+ for (let tool of toggleableTools) {
+ let pref = tool.visibilityswitch;
+ modifiedPrefs.push(pref);
+ }
+
+ // Toggle each tool
+ for (let node of toolNodes) {
+ yield toggleTool(node);
+ }
+ // Toggle again to reset tool enablement state
+ for (let node of toolNodes) {
+ yield toggleTool(node);
+ }
+
+ // Test that a tool can still be added when no tabs are present:
+ // Disable all tools
+ for (let node of enabledTools) {
+ yield toggleTool(node);
+ }
+ // Re-enable the tools which are enabled by default
+ for (let node of enabledTools) {
+ yield toggleTool(node);
+ }
+
+ // Toggle first, middle, and last tools to ensure that toolbox tabs are
+ // inserted in order
+ let firstTool = toolNodes[0];
+ let middleTool = toolNodes[(toolNodes.length / 2) | 0];
+ let lastTool = toolNodes[toolNodes.length - 1];
+
+ yield toggleTool(firstTool);
+ yield toggleTool(firstTool);
+ yield toggleTool(middleTool);
+ yield toggleTool(middleTool);
+ yield toggleTool(lastTool);
+ yield toggleTool(lastTool);
+}
+
+function* toggleTool(node) {
+ let deferred = defer();
+
+ let toolId = node.getAttribute("id");
+ if (node.checked) {
+ gDevTools.once("tool-unregistered",
+ checkUnregistered.bind(null, toolId, deferred));
+ } else {
+ gDevTools.once("tool-registered",
+ checkRegistered.bind(null, toolId, deferred));
+ }
+ node.scrollIntoView();
+ EventUtils.synthesizeMouseAtCenter(node, {}, panelWin);
+
+ yield deferred.promise;
+}
+
+function checkUnregistered(toolId, deferred, event, data) {
+ if (data.id == toolId) {
+ ok(true, "Correct tool removed");
+ // checking tab on the toolbox
+ ok(!doc.getElementById("toolbox-tab-" + toolId),
+ "Tab removed for " + toolId);
+ } else {
+ ok(false, "Something went wrong, " + toolId + " was not unregistered");
+ }
+ deferred.resolve();
+}
+
+function checkRegistered(toolId, deferred, event, data) {
+ if (data == toolId) {
+ ok(true, "Correct tool added back");
+ // checking tab on the toolbox
+ let radio = doc.getElementById("toolbox-tab-" + toolId);
+ ok(radio, "Tab added back for " + toolId);
+ if (radio.previousSibling) {
+ ok(+radio.getAttribute("ordinal") >=
+ +radio.previousSibling.getAttribute("ordinal"),
+ "Inserted tab's ordinal is greater than equal to its previous tab." +
+ "Expected " + radio.getAttribute("ordinal") + " >= " +
+ radio.previousSibling.getAttribute("ordinal"));
+ }
+ if (radio.nextSibling) {
+ ok(+radio.getAttribute("ordinal") <
+ +radio.nextSibling.getAttribute("ordinal"),
+ "Inserted tab's ordinal is less than its next tab. Expected " +
+ radio.getAttribute("ordinal") + " < " +
+ radio.nextSibling.getAttribute("ordinal"));
+ }
+ } else {
+ ok(false, "Something went wrong, " + toolId + " was not registered");
+ }
+ deferred.resolve();
+}
+
+function GetPref(name) {
+ let type = Services.prefs.getPrefType(name);
+ switch (type) {
+ case Services.prefs.PREF_STRING:
+ return Services.prefs.getCharPref(name);
+ case Services.prefs.PREF_INT:
+ return Services.prefs.getIntPref(name);
+ case Services.prefs.PREF_BOOL:
+ return Services.prefs.getBoolPref(name);
+ default:
+ throw new Error("Unknown type");
+ }
+}
+
+function* cleanup() {
+ gDevTools.unregisterTool("test-tool");
+ yield toolbox.destroy();
+ gBrowser.removeCurrentTab();
+ for (let pref of modifiedPrefs) {
+ Services.prefs.clearUserPref(pref);
+ }
+ toolbox = doc = panelWin = modifiedPrefs = null;
+}
diff --git a/devtools/client/framework/test/browser_toolbox_options_disable_buttons.js b/devtools/client/framework/test/browser_toolbox_options_disable_buttons.js
new file mode 100644
index 000000000..09cde4393
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_options_disable_buttons.js
@@ -0,0 +1,163 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/* import-globals-from shared-head.js */
+"use strict";
+
+const TEST_URL = "data:text/html;charset=utf8,test for dynamically " +
+ "registering and unregistering tools";
+var doc = null, toolbox = null, panelWin = null, modifiedPrefs = [];
+
+function test() {
+ addTab(TEST_URL).then(tab => {
+ let target = TargetFactory.forTab(tab);
+ gDevTools.showToolbox(target)
+ .then(testSelectTool)
+ .then(testToggleToolboxButtons)
+ .then(testPrefsAreRespectedWhenReopeningToolbox)
+ .then(cleanup, errorHandler);
+ });
+}
+
+function testPrefsAreRespectedWhenReopeningToolbox() {
+ let deferred = defer();
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+
+ info("Closing toolbox to test after reopening");
+ gDevTools.closeToolbox(target).then(() => {
+ let tabTarget = TargetFactory.forTab(gBrowser.selectedTab);
+ gDevTools.showToolbox(tabTarget)
+ .then(testSelectTool)
+ .then(() => {
+ info("Toolbox has been reopened. Checking UI state.");
+ testPreferenceAndUIStateIsConsistent();
+ deferred.resolve();
+ });
+ });
+
+ return deferred.promise;
+}
+
+function testSelectTool(devtoolsToolbox) {
+ let deferred = defer();
+ info("Selecting the options panel");
+
+ toolbox = devtoolsToolbox;
+ doc = toolbox.doc;
+ toolbox.once("options-selected", (event, tool) => {
+ ok(true, "Options panel selected via selectTool method");
+ panelWin = tool.panelWin;
+ deferred.resolve();
+ });
+ toolbox.selectTool("options");
+
+ return deferred.promise;
+}
+
+function testPreferenceAndUIStateIsConsistent() {
+ let checkNodes = [...panelWin.document.querySelectorAll(
+ "#enabled-toolbox-buttons-box input[type=checkbox]")];
+ let toolboxButtonNodes = [...doc.querySelectorAll(".command-button")];
+ toolboxButtonNodes.push(doc.getElementById("command-button-frames"));
+ let toggleableTools = toolbox.toolboxButtons;
+
+ // The noautohide button is only displayed in the browser toolbox
+ toggleableTools = toggleableTools.filter(
+ tool => tool.id != "command-button-noautohide");
+
+ for (let tool of toggleableTools) {
+ let isVisible = getBoolPref(tool.visibilityswitch);
+
+ let button = toolboxButtonNodes.filter(
+ toolboxButton => toolboxButton.id === tool.id)[0];
+ is(!button.hasAttribute("hidden"), isVisible,
+ "Button visibility matches pref for " + tool.id);
+
+ let check = checkNodes.filter(node => node.id === tool.id)[0];
+ is(check.checked, isVisible,
+ "Checkbox should be selected based on current pref for " + tool.id);
+ }
+}
+
+function testToggleToolboxButtons() {
+ let checkNodes = [...panelWin.document.querySelectorAll(
+ "#enabled-toolbox-buttons-box input[type=checkbox]")];
+ let toolboxButtonNodes = [...doc.querySelectorAll(".command-button")];
+ let toggleableTools = toolbox.toolboxButtons;
+
+ // The noautohide button is only displayed in the browser toolbox, and the element
+ // picker button is not toggleable.
+ toggleableTools = toggleableTools.filter(
+ tool => tool.id != "command-button-noautohide" && tool.id != "command-button-pick");
+ toolboxButtonNodes = toolboxButtonNodes.filter(
+ btn => btn.id != "command-button-noautohide" && btn.id != "command-button-pick");
+
+ is(checkNodes.length, toggleableTools.length,
+ "All of the buttons are toggleable.");
+ is(checkNodes.length, toolboxButtonNodes.length,
+ "All of the DOM buttons are toggleable.");
+
+ for (let tool of toggleableTools) {
+ let id = tool.id;
+ let matchedCheckboxes = checkNodes.filter(node => node.id === id);
+ let matchedButtons = toolboxButtonNodes.filter(button => button.id === id);
+ is(matchedCheckboxes.length, 1,
+ "There should be a single toggle checkbox for: " + id);
+ is(matchedButtons.length, 1,
+ "There should be a DOM button for: " + id);
+ is(matchedButtons[0], tool.button,
+ "DOM buttons should match for: " + id);
+
+ is(matchedCheckboxes[0].nextSibling.textContent, tool.label,
+ "The label for checkbox matches the tool definition.");
+ is(matchedButtons[0].getAttribute("title"), tool.label,
+ "The tooltip for button matches the tool definition.");
+ }
+
+ // Store modified pref names so that they can be cleared on error.
+ for (let tool of toggleableTools) {
+ let pref = tool.visibilityswitch;
+ modifiedPrefs.push(pref);
+ }
+
+ // Try checking each checkbox, making sure that it changes the preference
+ for (let node of checkNodes) {
+ let tool = toggleableTools.filter(
+ toggleableTool => toggleableTool.id === node.id)[0];
+ let isVisible = getBoolPref(tool.visibilityswitch);
+
+ testPreferenceAndUIStateIsConsistent();
+ node.click();
+ testPreferenceAndUIStateIsConsistent();
+
+ let isVisibleAfterClick = getBoolPref(tool.visibilityswitch);
+
+ is(isVisible, !isVisibleAfterClick,
+ "Clicking on the node should have toggled visibility preference for " +
+ tool.visibilityswitch);
+ }
+
+ return promise.resolve();
+}
+
+function getBoolPref(key) {
+ return Services.prefs.getBoolPref(key);
+}
+
+function cleanup() {
+ toolbox.destroy().then(function () {
+ gBrowser.removeCurrentTab();
+ for (let pref of modifiedPrefs) {
+ Services.prefs.clearUserPref(pref);
+ }
+ toolbox = doc = panelWin = modifiedPrefs = null;
+ finish();
+ });
+}
+
+function errorHandler(error) {
+ ok(false, "Unexpected error: " + error);
+ cleanup();
+}
diff --git a/devtools/client/framework/test/browser_toolbox_options_disable_cache-01.js b/devtools/client/framework/test/browser_toolbox_options_disable_cache-01.js
new file mode 100644
index 000000000..6badf069e
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_options_disable_cache-01.js
@@ -0,0 +1,34 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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";
+
+requestLongerTimeout(2);
+
+// Tests that disabling the cache for a tab works as it should when toolboxes
+// are not toggled.
+loadHelperScript("helper_disable_cache.js");
+
+add_task(function* () {
+ // Ensure that the setting is cleared after the test.
+ registerCleanupFunction(() => {
+ info("Resetting devtools.cache.disabled to false.");
+ Services.prefs.setBoolPref("devtools.cache.disabled", false);
+ });
+
+ // Initialise tabs: 1 and 2 with a toolbox, 3 and 4 without.
+ for (let tab of tabs) {
+ yield initTab(tab, tab.startToolbox);
+ }
+
+ // Ensure cache is enabled for all tabs.
+ yield checkCacheStateForAllTabs([true, true, true, true]);
+
+ // Check the checkbox in tab 0 and ensure cache is disabled for tabs 0 and 1.
+ yield setDisableCacheCheckboxChecked(tabs[0], true);
+ yield checkCacheStateForAllTabs([false, false, true, true]);
+
+ yield finishUp();
+});
diff --git a/devtools/client/framework/test/browser_toolbox_options_disable_cache-02.js b/devtools/client/framework/test/browser_toolbox_options_disable_cache-02.js
new file mode 100644
index 000000000..38c381cef
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_options_disable_cache-02.js
@@ -0,0 +1,47 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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";
+
+requestLongerTimeout(2);
+
+// Tests that disabling the cache for a tab works as it should when toolboxes
+// are toggled.
+loadHelperScript("helper_disable_cache.js");
+
+add_task(function* () {
+ // Ensure that the setting is cleared after the test.
+ registerCleanupFunction(() => {
+ info("Resetting devtools.cache.disabled to false.");
+ Services.prefs.setBoolPref("devtools.cache.disabled", false);
+ });
+
+ // Initialise tabs: 1 and 2 with a toolbox, 3 and 4 without.
+ for (let tab of tabs) {
+ yield initTab(tab, tab.startToolbox);
+ }
+
+ // Disable cache in tab 0
+ yield setDisableCacheCheckboxChecked(tabs[0], true);
+
+ // Open toolbox in tab 2 and ensure the cache is then disabled.
+ tabs[2].toolbox = yield gDevTools.showToolbox(tabs[2].target, "options");
+ yield checkCacheEnabled(tabs[2], false);
+
+ // Close toolbox in tab 2 and ensure the cache is enabled again
+ yield tabs[2].toolbox.destroy();
+ tabs[2].target = TargetFactory.forTab(tabs[2].tab);
+ yield checkCacheEnabled(tabs[2], true);
+
+ // Open toolbox in tab 2 and ensure the cache is then disabled.
+ tabs[2].toolbox = yield gDevTools.showToolbox(tabs[2].target, "options");
+ yield checkCacheEnabled(tabs[2], false);
+
+ // Check the checkbox in tab 2 and ensure cache is enabled for all tabs.
+ yield setDisableCacheCheckboxChecked(tabs[2], false);
+ yield checkCacheStateForAllTabs([true, true, true, true]);
+
+ yield finishUp();
+});
diff --git a/devtools/client/framework/test/browser_toolbox_options_disable_cache.sjs b/devtools/client/framework/test/browser_toolbox_options_disable_cache.sjs
new file mode 100644
index 000000000..c6c336981
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_options_disable_cache.sjs
@@ -0,0 +1,28 @@
+ /* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function handleRequest(request, response) {
+ let Etag = '"4d881ab-b03-435f0a0f9ef00"';
+ let IfNoneMatch = request.hasHeader("If-None-Match")
+ ? request.getHeader("If-None-Match")
+ : "";
+
+ let guid = 'xxxxxxxx-xxxx-xxxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
+ let r = Math.random() * 16 | 0;
+ let v = c === "x" ? r : (r & 0x3 | 0x8);
+
+ return v.toString(16);
+ });
+
+ let page = "<!DOCTYPE html><html><body><h1>" + guid + "</h1></body></html>";
+
+ response.setHeader("Etag", Etag, false);
+
+ if (IfNoneMatch === Etag) {
+ response.setStatusLine(request.httpVersion, "304", "Not Modified");
+ } else {
+ response.setHeader("Content-Type", "text/html; charset=utf-8", false);
+ response.setHeader("Content-Length", page.length + "", false);
+ response.write(page);
+ }
+}
diff --git a/devtools/client/framework/test/browser_toolbox_options_disable_js.html b/devtools/client/framework/test/browser_toolbox_options_disable_js.html
new file mode 100644
index 000000000..8df1119f6
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_options_disable_js.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>browser_toolbox_options_disablejs.html</title>
+ <meta charset="UTF-8">
+ <style>
+ div {
+ width: 260px;
+ height: 24px;
+ border: 1px solid #000;
+ margin-top: 10px;
+ }
+
+ iframe {
+ height: 90px;
+ border: 1px solid #000;
+ }
+
+ h1 {
+ font-size: 20px
+ }
+ </style>
+ <script type="application/javascript;version=1.8">
+ function log(msg) {
+ let output = document.getElementById("output");
+
+ output.innerHTML = msg;
+ }
+ </script>
+ </head>
+ <body>
+ <h1>Test in page</h1>
+ <input id="logJSEnabled"
+ type="button"
+ value="Log JS Enabled"
+ onclick="log('JavaScript Enabled')"/>
+ <input id="logJSDisabled"
+ type="button"
+ value="Log JS Disabled"
+ onclick="log('JavaScript Disabled')"/>
+ <br>
+ <div id="output">No output</div>
+ <h1>Test in iframe</h1>
+ <iframe src="browser_toolbox_options_disable_js_iframe.html"></iframe>
+ </body>
+</html>
diff --git a/devtools/client/framework/test/browser_toolbox_options_disable_js.js b/devtools/client/framework/test/browser_toolbox_options_disable_js.js
new file mode 100644
index 000000000..b0c14a805
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_options_disable_js.js
@@ -0,0 +1,119 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests that disabling JavaScript for a tab works as it should.
+
+const TEST_URI = URL_ROOT + "browser_toolbox_options_disable_js.html";
+
+function test() {
+ addTab(TEST_URI).then(tab => {
+ let target = TargetFactory.forTab(tab);
+ gDevTools.showToolbox(target).then(testSelectTool);
+ });
+}
+
+function testSelectTool(toolbox) {
+ toolbox.once("options-selected", () => testToggleJS(toolbox));
+ toolbox.selectTool("options");
+}
+
+let testToggleJS = Task.async(function* (toolbox) {
+ ok(true, "Toolbox selected via selectTool method");
+
+ yield testJSEnabled();
+ yield testJSEnabledIframe();
+
+ // Disable JS.
+ yield toggleJS(toolbox);
+
+ yield testJSDisabled();
+ yield testJSDisabledIframe();
+
+ // Re-enable JS.
+ yield toggleJS(toolbox);
+
+ yield testJSEnabled();
+ yield testJSEnabledIframe();
+
+ finishUp(toolbox);
+});
+
+function* testJSEnabled() {
+ info("Testing that JS is enabled");
+
+ // We use waitForTick here because switching docShell.allowJavascript to true
+ // takes a while to become live.
+ yield waitForTick();
+
+ yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function () {
+ let doc = content.document;
+ let output = doc.getElementById("output");
+ doc.querySelector("#logJSEnabled").click();
+ is(output.textContent, "JavaScript Enabled", 'Output is "JavaScript Enabled"');
+ });
+}
+
+function* testJSEnabledIframe() {
+ info("Testing that JS is enabled in the iframe");
+
+ yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function () {
+ let doc = content.document;
+ let iframe = doc.querySelector("iframe");
+ let iframeDoc = iframe.contentDocument;
+ let output = iframeDoc.getElementById("output");
+ iframeDoc.querySelector("#logJSEnabled").click();
+ is(output.textContent, "JavaScript Enabled",
+ 'Output is "JavaScript Enabled" in iframe');
+ });
+}
+
+function* toggleJS(toolbox) {
+ let panel = toolbox.getCurrentPanel();
+ let cbx = panel.panelDoc.getElementById("devtools-disable-javascript");
+
+ if (cbx.checked) {
+ info("Clearing checkbox to re-enable JS");
+ } else {
+ info("Checking checkbox to disable JS");
+ }
+
+ let browserLoaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ cbx.click();
+ yield browserLoaded;
+}
+
+function* testJSDisabled() {
+ info("Testing that JS is disabled");
+
+ yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function () {
+ let doc = content.document;
+ let output = doc.getElementById("output");
+ doc.querySelector("#logJSDisabled").click();
+
+ ok(output.textContent !== "JavaScript Disabled",
+ 'output is not "JavaScript Disabled"');
+ });
+}
+
+function* testJSDisabledIframe() {
+ info("Testing that JS is disabled in the iframe");
+
+ yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function () {
+ let doc = content.document;
+ let iframe = doc.querySelector("iframe");
+ let iframeDoc = iframe.contentDocument;
+ let output = iframeDoc.getElementById("output");
+ iframeDoc.querySelector("#logJSDisabled").click();
+ ok(output.textContent !== "JavaScript Disabled",
+ 'output is not "JavaScript Disabled" in iframe');
+ });
+}
+
+function finishUp(toolbox) {
+ toolbox.destroy().then(function () {
+ gBrowser.removeCurrentTab();
+ finish();
+ });
+}
diff --git a/devtools/client/framework/test/browser_toolbox_options_disable_js_iframe.html b/devtools/client/framework/test/browser_toolbox_options_disable_js_iframe.html
new file mode 100644
index 000000000..777bf86bf
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_options_disable_js_iframe.html
@@ -0,0 +1,33 @@
+<html>
+ <head>
+ <title>browser_toolbox_options_disablejs.html</title>
+ <meta charset="UTF-8">
+ <style>
+ div {
+ width: 260px;
+ height: 24px;
+ border: 1px solid #000;
+ margin-top: 10px;
+ }
+ </style>
+ <script type="application/javascript;version=1.8">
+ function log(msg) {
+ let output = document.getElementById("output");
+
+ output.innerHTML = msg;
+ }
+ </script>
+ </head>
+ <body>
+ <input id="logJSEnabled"
+ type="button"
+ value="Log JS Enabled"
+ onclick="log('JavaScript Enabled')"/>
+ <input id="logJSDisabled"
+ type="button"
+ value="Log JS Disabled"
+ onclick="log('JavaScript Disabled')"/>
+ <br>
+ <div id="output">No output</div>
+ </body>
+</html>
diff --git a/devtools/client/framework/test/browser_toolbox_options_enable_serviceworkers_testing.html b/devtools/client/framework/test/browser_toolbox_options_enable_serviceworkers_testing.html
new file mode 100644
index 000000000..0e4b824cb
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_options_enable_serviceworkers_testing.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>browser_toolbox_options_enable_serviceworkers_testing.html</title>
+ <meta charset="UTF-8">
+ </head>
+ <body>
+ <h1>SW-test</h1>
+ </body>
+</html>
diff --git a/devtools/client/framework/test/browser_toolbox_options_enable_serviceworkers_testing.js b/devtools/client/framework/test/browser_toolbox_options_enable_serviceworkers_testing.js
new file mode 100644
index 000000000..3273f4395
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_options_enable_serviceworkers_testing.js
@@ -0,0 +1,126 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Test that enabling Service Workers testing option enables the
+// mServiceWorkersTestingEnabled attribute added to nsPIDOMWindow.
+
+const COMMON_FRAME_SCRIPT_URL =
+ "chrome://devtools/content/shared/frame-script-utils.js";
+const ROOT_TEST_DIR =
+ getRootDirectory(gTestPath);
+const FRAME_SCRIPT_URL =
+ ROOT_TEST_DIR +
+ "browser_toolbox_options_enable_serviceworkers_testing_frame_script.js";
+const TEST_URI = URL_ROOT +
+ "browser_toolbox_options_enable_serviceworkers_testing.html";
+
+const ELEMENT_ID = "devtools-enable-serviceWorkersTesting";
+
+var toolbox;
+
+function test() {
+ // Note: Pref dom.serviceWorkers.testing.enabled is false since we are testing
+ // the same capabilities are enabled with the devtool pref.
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", false]
+ ]}, init);
+}
+
+function init() {
+ addTab(TEST_URI).then(tab => {
+ let target = TargetFactory.forTab(tab);
+ let linkedBrowser = tab.linkedBrowser;
+
+ linkedBrowser.messageManager.loadFrameScript(COMMON_FRAME_SCRIPT_URL, false);
+ linkedBrowser.messageManager.loadFrameScript(FRAME_SCRIPT_URL, false);
+
+ gDevTools.showToolbox(target).then(testSelectTool);
+ });
+}
+
+function testSelectTool(aToolbox) {
+ toolbox = aToolbox;
+ toolbox.once("options-selected", start);
+ toolbox.selectTool("options");
+}
+
+function register() {
+ return executeInContent("devtools:sw-test:register");
+}
+
+function unregister(swr) {
+ return executeInContent("devtools:sw-test:unregister");
+}
+
+function registerAndUnregisterInFrame() {
+ return executeInContent("devtools:sw-test:iframe:register-and-unregister");
+}
+
+function testRegisterFails(data) {
+ is(data.success, false, "Register should fail with security error");
+ return promise.resolve();
+}
+
+function toggleServiceWorkersTestingCheckbox() {
+ let panel = toolbox.getCurrentPanel();
+ let cbx = panel.panelDoc.getElementById(ELEMENT_ID);
+
+ cbx.scrollIntoView();
+
+ if (cbx.checked) {
+ info("Clearing checkbox to disable service workers testing");
+ } else {
+ info("Checking checkbox to enable service workers testing");
+ }
+
+ cbx.click();
+
+ return promise.resolve();
+}
+
+function reload() {
+ let deferred = defer();
+
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad(evt) {
+ gBrowser.selectedBrowser.removeEventListener(evt.type, onLoad, true);
+ deferred.resolve();
+ }, true);
+
+ executeInContent("devtools:test:reload", {}, {}, false);
+ return deferred.promise;
+}
+
+function testRegisterSuccesses(data) {
+ is(data.success, true, "Register should success");
+ return promise.resolve();
+}
+
+function start() {
+ register()
+ .then(testRegisterFails)
+ .then(toggleServiceWorkersTestingCheckbox)
+ .then(reload)
+ .then(register)
+ .then(testRegisterSuccesses)
+ .then(unregister)
+ .then(registerAndUnregisterInFrame)
+ .then(testRegisterSuccesses)
+ // Workers should be turned back off when we closes the toolbox
+ .then(toolbox.destroy.bind(toolbox))
+ .then(reload)
+ .then(register)
+ .then(testRegisterFails)
+ .catch(function (e) {
+ ok(false, "Some test failed with error " + e);
+ }).then(finishUp);
+}
+
+function finishUp() {
+ gBrowser.removeCurrentTab();
+ toolbox = null;
+ finish();
+}
diff --git a/devtools/client/framework/test/browser_toolbox_options_enable_serviceworkers_testing_frame_script.js b/devtools/client/framework/test/browser_toolbox_options_enable_serviceworkers_testing_frame_script.js
new file mode 100644
index 000000000..ec5ab3762
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_options_enable_serviceworkers_testing_frame_script.js
@@ -0,0 +1,46 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// A helper frame-script for devtools/client/framework service worker tests.
+
+"use strict";
+
+addMessageListener("devtools:sw-test:register", function (msg) {
+ content.navigator.serviceWorker.register("serviceworker.js")
+ .then(swr => {
+ sendAsyncMessage("devtools:sw-test:register", {success: true});
+ }, error => {
+ sendAsyncMessage("devtools:sw-test:register", {success: false});
+ });
+});
+
+addMessageListener("devtools:sw-test:unregister", function (msg) {
+ content.navigator.serviceWorker.getRegistration().then(swr => {
+ swr.unregister().then(result => {
+ sendAsyncMessage("devtools:sw-test:unregister",
+ {success: result ? true : false});
+ });
+ });
+});
+
+addMessageListener("devtools:sw-test:iframe:register-and-unregister", function (msg) {
+ var frame = content.document.createElement("iframe");
+ frame.addEventListener("load", function onLoad() {
+ frame.removeEventListener("load", onLoad);
+ frame.contentWindow.navigator.serviceWorker.register("serviceworker.js")
+ .then(swr => {
+ return swr.unregister();
+ }).then(_ => {
+ frame.remove();
+ sendAsyncMessage("devtools:sw-test:iframe:register-and-unregister",
+ {success: true});
+ }).catch(error => {
+ sendAsyncMessage("devtools:sw-test:iframe:register-and-unregister",
+ {success: false});
+ });
+ });
+ frame.src = "browser_toolbox_options_enabled_serviceworkers_testing.html";
+ content.document.body.appendChild(frame);
+});
diff --git a/devtools/client/framework/test/browser_toolbox_races.js b/devtools/client/framework/test/browser_toolbox_races.js
new file mode 100644
index 000000000..fedbc4402
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_races.js
@@ -0,0 +1,81 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 toggling the toolbox quickly and see if there is any race breaking it.
+
+const URL = "data:text/html;charset=utf-8,Toggling devtools quickly";
+
+add_task(function* () {
+ // Make sure this test starts with the selectedTool pref cleared. Previous
+ // tests select various tools, and that sets this pref.
+ Services.prefs.clearUserPref("devtools.toolbox.selectedTool");
+
+ let tab = yield addTab(URL);
+
+ let created = 0, ready = 0, destroy = 0, destroyed = 0;
+ let onCreated = () => {
+ created++;
+ };
+ let onReady = () => {
+ ready++;
+ };
+ let onDestroy = () => {
+ destroy++;
+ };
+ let onDestroyed = () => {
+ destroyed++;
+ };
+ gDevTools.on("toolbox-created", onCreated);
+ gDevTools.on("toolbox-ready", onReady);
+ gDevTools.on("toolbox-destroy", onDestroy);
+ gDevTools.on("toolbox-destroyed", onDestroyed);
+
+ // The current implementation won't toggle the toolbox many times,
+ // instead it will ignore toggles that happens while the toolbox is still
+ // creating or still destroying.
+
+ // Toggle the toolbox at least 3 times.
+ info("Trying to toggle the toolbox 3 times");
+ while (created < 3) {
+ // Sent multiple event to try to race the code during toolbox creation and destruction
+ toggle();
+ toggle();
+ toggle();
+
+ // Release the event loop to let a chance to actually create or destroy the toolbox!
+ yield wait(50);
+ }
+ info("Toggled the toolbox 3 times");
+
+ // Now wait for the 3rd toolbox to be fully ready before closing it.
+ // We close the last toolbox manually, out of the first while() loop to
+ // avoid races and be sure we end up we no toolbox and waited for all the
+ // requests to be done.
+ while (ready != 3) {
+ yield wait(100);
+ }
+ toggle();
+ while (destroyed != 3) {
+ yield wait(100);
+ }
+
+ is(created, 3, "right number of created events");
+ is(ready, 3, "right number of ready events");
+ is(destroy, 3, "right number of destroy events");
+ is(destroyed, 3, "right number of destroyed events");
+
+ gDevTools.off("toolbox-created", onCreated);
+ gDevTools.off("toolbox-ready", onReady);
+ gDevTools.off("toolbox-destroy", onDestroy);
+ gDevTools.off("toolbox-destroyed", onDestroyed);
+
+ gBrowser.removeCurrentTab();
+});
+
+function toggle() {
+ EventUtils.synthesizeKey("VK_F12", {});
+}
diff --git a/devtools/client/framework/test/browser_toolbox_raise.js b/devtools/client/framework/test/browser_toolbox_raise.js
new file mode 100644
index 000000000..0af1a4571
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_raise.js
@@ -0,0 +1,78 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+const TEST_URL = "data:text/html,test for opening toolbox in different hosts";
+
+var {Toolbox} = require("devtools/client/framework/toolbox");
+
+var toolbox, tab1, tab2;
+
+function test() {
+ addTab(TEST_URL).then(tab => {
+ tab2 = gBrowser.addTab();
+ let target = TargetFactory.forTab(tab);
+ gDevTools.showToolbox(target)
+ .then(testBottomHost, console.error)
+ .then(null, console.error);
+ });
+}
+
+function testBottomHost(aToolbox) {
+ toolbox = aToolbox;
+
+ // switch to another tab and test toolbox.raise()
+ gBrowser.selectedTab = tab2;
+ executeSoon(function () {
+ is(gBrowser.selectedTab, tab2, "Correct tab is selected before calling raise");
+ toolbox.raise();
+ executeSoon(function () {
+ is(gBrowser.selectedTab, tab1, "Correct tab was selected after calling raise");
+
+ toolbox.switchHost(Toolbox.HostType.WINDOW).then(testWindowHost).then(null, console.error);
+ });
+ });
+}
+
+function testWindowHost() {
+ // Make sure toolbox is not focused.
+ window.addEventListener("focus", onFocus, true);
+
+ // Need to wait for focus as otherwise window.focus() is overridden by
+ // toolbox window getting focused first on Linux and Mac.
+ let onToolboxFocus = () => {
+ toolbox.win.parent.removeEventListener("focus", onToolboxFocus, true);
+ info("focusing main window.");
+ window.focus();
+ };
+ // Need to wait for toolbox window to get focus.
+ toolbox.win.parent.addEventListener("focus", onToolboxFocus, true);
+}
+
+function onFocus() {
+ info("Main window is focused before calling toolbox.raise()");
+ window.removeEventListener("focus", onFocus, true);
+
+ // Check if toolbox window got focus.
+ let onToolboxFocusAgain = () => {
+ toolbox.win.parent.removeEventListener("focus", onToolboxFocusAgain, false);
+ ok(true, "Toolbox window is the focused window after calling toolbox.raise()");
+ cleanup();
+ };
+ toolbox.win.parent.addEventListener("focus", onToolboxFocusAgain, false);
+
+ // Now raise toolbox.
+ toolbox.raise();
+}
+
+function cleanup() {
+ Services.prefs.setCharPref("devtools.toolbox.host", Toolbox.HostType.BOTTOM);
+
+ toolbox.destroy().then(function () {
+ toolbox = null;
+ gBrowser.removeCurrentTab();
+ gBrowser.removeCurrentTab();
+ finish();
+ });
+}
diff --git a/devtools/client/framework/test/browser_toolbox_ready.js b/devtools/client/framework/test/browser_toolbox_ready.js
new file mode 100644
index 000000000..e1a59b3f0
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_ready.js
@@ -0,0 +1,21 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+const TEST_URL = "data:text/html,test for toolbox being ready";
+
+add_task(function* () {
+ let tab = yield addTab(TEST_URL);
+ let target = TargetFactory.forTab(tab);
+
+ const toolbox = yield gDevTools.showToolbox(target, "webconsole");
+ ok(toolbox.isReady, "toolbox isReady is set");
+ ok(toolbox.threadClient, "toolbox has a thread client");
+
+ const toolbox2 = yield gDevTools.showToolbox(toolbox.target, toolbox.toolId);
+ is(toolbox2, toolbox, "same toolbox");
+
+ yield toolbox.destroy();
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/client/framework/test/browser_toolbox_remoteness_change.js b/devtools/client/framework/test/browser_toolbox_remoteness_change.js
new file mode 100644
index 000000000..b30d633fa
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_remoteness_change.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var {Toolbox} = require("devtools/client/framework/toolbox");
+
+const URL_1 = "about:robots";
+const URL_2 = "data:text/html;charset=UTF-8," +
+ encodeURIComponent("<div id=\"remote-page\">foo</div>");
+
+add_task(function* () {
+ info("Open a tab on a URL supporting only running in parent process");
+ let tab = yield addTab(URL_1);
+ is(tab.linkedBrowser.currentURI.spec, URL_1, "We really are on the expected document");
+ is(tab.linkedBrowser.getAttribute("remote"), "", "And running in parent process");
+
+ let toolbox = yield openToolboxForTab(tab);
+
+ let onToolboxDestroyed = toolbox.once("destroyed");
+ let onToolboxCreated = gDevTools.once("toolbox-created");
+
+ info("Navigate to a URL supporting remote process");
+ let onLoaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ gBrowser.loadURI(URL_2);
+ yield onLoaded;
+
+ is(tab.linkedBrowser.getAttribute("remote"), "true", "Navigated to a data: URI and switching to remote");
+
+ info("Waiting for the toolbox to be destroyed");
+ yield onToolboxDestroyed;
+
+ info("Waiting for a new toolbox to be created");
+ toolbox = yield onToolboxCreated;
+
+ info("Waiting for the new toolbox to be ready");
+ yield toolbox.once("ready");
+
+ info("Veryify we are inspecting the new document");
+ let console = yield toolbox.selectTool("webconsole");
+ let { jsterm } = console.hud;
+ let url = yield jsterm.execute("document.location.href");
+ // Uses includes as the old console frontend prints a timestamp
+ ok(url.textContent.includes(URL_2), "The console inspects the second document");
+});
diff --git a/devtools/client/framework/test/browser_toolbox_select_event.js b/devtools/client/framework/test/browser_toolbox_select_event.js
new file mode 100644
index 000000000..ae104524e
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_select_event.js
@@ -0,0 +1,101 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 PAGE_URL = "data:text/html;charset=utf-8,test select events";
+
+requestLongerTimeout(2);
+
+add_task(function* () {
+ let tab = yield addTab(PAGE_URL);
+
+ let toolbox = yield openToolboxForTab(tab, "webconsole", "bottom");
+ yield testSelectEvent("inspector");
+ yield testSelectEvent("webconsole");
+ yield testSelectEvent("styleeditor");
+ yield testSelectEvent("inspector");
+ yield testSelectEvent("webconsole");
+ yield testSelectEvent("styleeditor");
+
+ yield testToolSelectEvent("inspector");
+ yield testToolSelectEvent("webconsole");
+ yield testToolSelectEvent("styleeditor");
+ yield toolbox.destroy();
+
+ toolbox = yield openToolboxForTab(tab, "webconsole", "side");
+ yield testSelectEvent("inspector");
+ yield testSelectEvent("webconsole");
+ yield testSelectEvent("styleeditor");
+ yield testSelectEvent("inspector");
+ yield testSelectEvent("webconsole");
+ yield testSelectEvent("styleeditor");
+ yield toolbox.destroy();
+
+ toolbox = yield openToolboxForTab(tab, "webconsole", "window");
+ yield testSelectEvent("inspector");
+ yield testSelectEvent("webconsole");
+ yield testSelectEvent("styleeditor");
+ yield testSelectEvent("inspector");
+ yield testSelectEvent("webconsole");
+ yield testSelectEvent("styleeditor");
+ yield toolbox.destroy();
+
+ yield testSelectToolRace();
+
+ /**
+ * Assert that selecting the given toolId raises a select event
+ * @param {toolId} Id of the tool to test
+ */
+ function* testSelectEvent(toolId) {
+ let onSelect = toolbox.once("select");
+ toolbox.selectTool(toolId);
+ let id = yield onSelect;
+ is(id, toolId, toolId + " selected");
+ }
+
+ /**
+ * Assert that selecting the given toolId raises its corresponding
+ * selected event
+ * @param {toolId} Id of the tool to test
+ */
+ function* testToolSelectEvent(toolId) {
+ let onSelected = toolbox.once(toolId + "-selected");
+ toolbox.selectTool(toolId);
+ yield onSelected;
+ is(toolbox.currentToolId, toolId, toolId + " tool selected");
+ }
+
+ /**
+ * Assert that two calls to selectTool won't race
+ */
+ function* testSelectToolRace() {
+ let toolbox = yield openToolboxForTab(tab, "webconsole");
+ let selected = false;
+ let onSelect = (event, id) => {
+ if (selected) {
+ ok(false, "Got more than one 'select' event");
+ } else {
+ selected = true;
+ }
+ };
+ toolbox.once("select", onSelect);
+ let p1 = toolbox.selectTool("inspector")
+ let p2 = toolbox.selectTool("inspector");
+ // Check that both promises don't resolve too early
+ let checkSelectToolResolution = panel => {
+ ok(selected, "selectTool resolves only after 'select' event is fired");
+ let inspector = toolbox.getPanel("inspector");
+ is(panel, inspector, "selecTool resolves to the panel instance");
+ };
+ p1.then(checkSelectToolResolution);
+ p2.then(checkSelectToolResolution);
+ yield p1;
+ yield p2;
+
+ yield toolbox.destroy();
+ }
+});
+
diff --git a/devtools/client/framework/test/browser_toolbox_selected_tool_unavailable.js b/devtools/client/framework/test/browser_toolbox_selected_tool_unavailable.js
new file mode 100644
index 000000000..d7cc5c94d
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_selected_tool_unavailable.js
@@ -0,0 +1,48 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 opening the toolbox doesn't throw when the previously selected
+// tool is not supported.
+
+const testToolDefinition = {
+ id: "test-tool",
+ isTargetSupported: () => true,
+ visibilityswitch: "devtools.test-tool.enabled",
+ url: "about:blank",
+ label: "someLabel",
+ build: (iframeWindow, toolbox) => {
+ return {
+ target: toolbox.target,
+ toolbox: toolbox,
+ isReady: true,
+ destroy: () => {},
+ panelDoc: iframeWindow.document
+ };
+ }
+};
+
+add_task(function* () {
+ gDevTools.registerTool(testToolDefinition);
+ let tab = yield addTab("about:blank");
+ let target = TargetFactory.forTab(tab);
+
+ let toolbox = yield gDevTools.showToolbox(target, testToolDefinition.id);
+ is(toolbox.currentToolId, "test-tool", "test-tool was selected");
+ yield gDevTools.closeToolbox(target);
+
+ // Make the previously selected tool unavailable.
+ testToolDefinition.isTargetSupported = () => false;
+
+ target = TargetFactory.forTab(tab);
+ toolbox = yield gDevTools.showToolbox(target);
+ is(toolbox.currentToolId, "webconsole", "web console was selected");
+
+ yield gDevTools.closeToolbox(target);
+ gDevTools.unregisterTool(testToolDefinition.id);
+ tab = toolbox = target = null;
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/client/framework/test/browser_toolbox_sidebar.js b/devtools/client/framework/test/browser_toolbox_sidebar.js
new file mode 100644
index 000000000..897f8cba5
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_sidebar.js
@@ -0,0 +1,181 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+function test() {
+ const Cu = Components.utils;
+ let {ToolSidebar} = require("devtools/client/framework/sidebar");
+
+ const toolURL = "data:text/xml;charset=utf8,<?xml version='1.0'?>" +
+ "<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'>" +
+ "<hbox flex='1'><description flex='1'>foo</description><splitter class='devtools-side-splitter'/>" +
+ "<tabbox flex='1' id='sidebar' class='devtools-sidebar-tabs'><tabs/><tabpanels flex='1'/></tabbox>" +
+ "</hbox>" +
+ "</window>";
+
+ const tab1URL = "data:text/html;charset=utf8,<title>1</title><p>1</p>";
+ const tab2URL = "data:text/html;charset=utf8,<title>2</title><p>2</p>";
+ const tab3URL = "data:text/html;charset=utf8,<title>3</title><p>3</p>";
+
+ let panelDoc;
+ let tab1Selected = false;
+ let registeredTabs = {};
+ let readyTabs = {};
+
+ let toolDefinition = {
+ id: "fakeTool4242",
+ visibilityswitch: "devtools.fakeTool4242.enabled",
+ url: toolURL,
+ label: "FAKE TOOL!!!",
+ isTargetSupported: () => true,
+ build: function (iframeWindow, toolbox) {
+ let deferred = defer();
+ executeSoon(() => {
+ deferred.resolve({
+ target: toolbox.target,
+ toolbox: toolbox,
+ isReady: true,
+ destroy: function () {},
+ panelDoc: iframeWindow.document,
+ });
+ });
+ return deferred.promise;
+ },
+ };
+
+ gDevTools.registerTool(toolDefinition);
+
+ addTab("about:blank").then(function (aTab) {
+ let target = TargetFactory.forTab(aTab);
+ gDevTools.showToolbox(target, toolDefinition.id).then(function (toolbox) {
+ let panel = toolbox.getPanel(toolDefinition.id);
+ panel.toolbox = toolbox;
+ ok(true, "Tool open");
+
+ let tabbox = panel.panelDoc.getElementById("sidebar");
+ panel.sidebar = new ToolSidebar(tabbox, panel, "testbug865688", true);
+
+ panel.sidebar.on("new-tab-registered", function (event, id) {
+ registeredTabs[id] = true;
+ });
+
+ panel.sidebar.once("tab1-ready", function (event) {
+ info(event);
+ readyTabs.tab1 = true;
+ allTabsReady(panel);
+ });
+
+ panel.sidebar.once("tab2-ready", function (event) {
+ info(event);
+ readyTabs.tab2 = true;
+ allTabsReady(panel);
+ });
+
+ panel.sidebar.once("tab3-ready", function (event) {
+ info(event);
+ readyTabs.tab3 = true;
+ allTabsReady(panel);
+ });
+
+ panel.sidebar.once("tab1-selected", function (event) {
+ info(event);
+ tab1Selected = true;
+ allTabsReady(panel);
+ });
+
+ panel.sidebar.addTab("tab1", tab1URL, {selected: true});
+ panel.sidebar.addTab("tab2", tab2URL);
+ panel.sidebar.addTab("tab3", tab3URL);
+
+ panel.sidebar.show();
+ }).then(null, console.error);
+ });
+
+ function allTabsReady(panel) {
+ if (!tab1Selected || !readyTabs.tab1 || !readyTabs.tab2 || !readyTabs.tab3) {
+ return;
+ }
+
+ ok(registeredTabs.tab1, "tab1 registered");
+ ok(registeredTabs.tab2, "tab2 registered");
+ ok(registeredTabs.tab3, "tab3 registered");
+ ok(readyTabs.tab1, "tab1 ready");
+ ok(readyTabs.tab2, "tab2 ready");
+ ok(readyTabs.tab3, "tab3 ready");
+
+ let tabs = panel.sidebar._tabbox.querySelectorAll("tab");
+ let panels = panel.sidebar._tabbox.querySelectorAll("tabpanel");
+ let label = 1;
+ for (let tab of tabs) {
+ is(tab.getAttribute("label"), label++, "Tab has the right title");
+ }
+
+ is(label, 4, "Found the right amount of tabs.");
+ is(panel.sidebar._tabbox.selectedPanel, panels[0], "First tab is selected");
+ is(panel.sidebar.getCurrentTabID(), "tab1", "getCurrentTabID() is correct");
+
+ panel.sidebar.once("tab1-unselected", function () {
+ ok(true, "received 'unselected' event");
+ panel.sidebar.once("tab2-selected", function () {
+ ok(true, "received 'selected' event");
+ tabs[1].focus();
+ is(panel.sidebar._panelDoc.activeElement, tabs[1],
+ "Focus is set to second tab");
+ panel.sidebar.hide();
+ isnot(panel.sidebar._panelDoc.activeElement, tabs[1],
+ "Focus is reset for sidebar");
+ is(panel.sidebar._tabbox.getAttribute("hidden"), "true", "Sidebar hidden");
+ is(panel.sidebar.getWindowForTab("tab1").location.href, tab1URL, "Window is accessible");
+ testRemoval(panel);
+ });
+ });
+
+ panel.sidebar.select("tab2");
+ }
+
+ function testRemoval(panel) {
+ panel.sidebar.once("tab-unregistered", function (event, id) {
+ info(event);
+ registeredTabs[id] = false;
+
+ is(id, "tab3", "The right tab must be removed");
+
+ let tabs = panel.sidebar._tabbox.querySelectorAll("tab");
+ let panels = panel.sidebar._tabbox.querySelectorAll("tabpanel");
+
+ is(tabs.length, 2, "There is the right number of tabs");
+ is(panels.length, 2, "There is the right number of panels");
+
+ testWidth(panel);
+ });
+
+ panel.sidebar.removeTab("tab3");
+ }
+
+ function testWidth(panel) {
+ let tabbox = panel.panelDoc.getElementById("sidebar");
+ tabbox.width = 420;
+ panel.sidebar.destroy().then(function () {
+ tabbox.width = 0;
+ panel.sidebar = new ToolSidebar(tabbox, panel, "testbug865688", true);
+ panel.sidebar.show();
+ is(panel.panelDoc.getElementById("sidebar").width, 420, "Width restored");
+
+ finishUp(panel);
+ });
+ }
+
+ function finishUp(panel) {
+ panel.sidebar.destroy();
+ panel.toolbox.destroy().then(function () {
+ gDevTools.unregisterTool(toolDefinition.id);
+
+ gBrowser.removeCurrentTab();
+
+ executeSoon(function () {
+ finish();
+ });
+ });
+ }
+}
diff --git a/devtools/client/framework/test/browser_toolbox_sidebar_events.js b/devtools/client/framework/test/browser_toolbox_sidebar_events.js
new file mode 100644
index 000000000..9137aaebe
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_sidebar_events.js
@@ -0,0 +1,93 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+function test() {
+ const Cu = Components.utils;
+ const { ToolSidebar } = require("devtools/client/framework/sidebar");
+
+ const toolURL = "data:text/xml;charset=utf8,<?xml version='1.0'?>" +
+ "<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'>" +
+ "<hbox flex='1'><description flex='1'>foo</description><splitter class='devtools-side-splitter'/>" +
+ "<tabbox flex='1' id='sidebar' class='devtools-sidebar-tabs'><tabs/><tabpanels flex='1'/></tabbox>" +
+ "</hbox>" +
+ "</window>";
+
+ const tab1URL = "data:text/html;charset=utf8,<title>1</title><p>1</p>";
+
+ let collectedEvents = [];
+
+ let toolDefinition = {
+ id: "testTool1072208",
+ visibilityswitch: "devtools.testTool1072208.enabled",
+ url: toolURL,
+ label: "Test tool",
+ isTargetSupported: () => true,
+ build: function (iframeWindow, toolbox) {
+ let deferred = defer();
+ executeSoon(() => {
+ deferred.resolve({
+ target: toolbox.target,
+ toolbox: toolbox,
+ isReady: true,
+ destroy: function () {},
+ panelDoc: iframeWindow.document,
+ });
+ });
+ return deferred.promise;
+ },
+ };
+
+ gDevTools.registerTool(toolDefinition);
+
+ addTab("about:blank").then(function (aTab) {
+ let target = TargetFactory.forTab(aTab);
+ gDevTools.showToolbox(target, toolDefinition.id).then(function (toolbox) {
+ let panel = toolbox.getPanel(toolDefinition.id);
+ ok(true, "Tool open");
+
+ panel.once("sidebar-created", function (event, id) {
+ collectedEvents.push(event);
+ });
+
+ panel.once("sidebar-destroyed", function (event, id) {
+ collectedEvents.push(event);
+ });
+
+ let tabbox = panel.panelDoc.getElementById("sidebar");
+ panel.sidebar = new ToolSidebar(tabbox, panel, "testbug1072208", true);
+
+ panel.sidebar.once("show", function (event, id) {
+ collectedEvents.push(event);
+ });
+
+ panel.sidebar.once("hide", function (event, id) {
+ collectedEvents.push(event);
+ });
+
+ panel.sidebar.once("tab1-selected", () => finishUp(panel));
+ panel.sidebar.addTab("tab1", tab1URL, {selected: true});
+ panel.sidebar.show();
+ }).then(null, console.error);
+ });
+
+ function finishUp(panel) {
+ panel.sidebar.hide();
+ panel.sidebar.destroy();
+
+ let events = collectedEvents.join(":");
+ is(events, "sidebar-created:show:hide:sidebar-destroyed",
+ "Found the right amount of collected events.");
+
+ panel.toolbox.destroy().then(function () {
+ gDevTools.unregisterTool(toolDefinition.id);
+ gBrowser.removeCurrentTab();
+
+ executeSoon(function () {
+ finish();
+ });
+ });
+ }
+}
+
diff --git a/devtools/client/framework/test/browser_toolbox_sidebar_existing_tabs.js b/devtools/client/framework/test/browser_toolbox_sidebar_existing_tabs.js
new file mode 100644
index 000000000..339687e10
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_sidebar_existing_tabs.js
@@ -0,0 +1,78 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 sidebar widget auto-registers existing tabs.
+
+const {ToolSidebar} = require("devtools/client/framework/sidebar");
+
+const testToolURL = "data:text/xml;charset=utf8,<?xml version='1.0'?>" +
+ "<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'>" +
+ "<hbox flex='1'><description flex='1'>test tool</description>" +
+ "<splitter class='devtools-side-splitter'/>" +
+ "<tabbox flex='1' id='sidebar' class='devtools-sidebar-tabs'>" +
+ "<tabs><tab id='tab1' label='tab 1'></tab><tab id='tab2' label='tab 2'></tab></tabs>" +
+ "<tabpanels flex='1'><tabpanel id='tabpanel1'>tab 1</tabpanel><tabpanel id='tabpanel2'>tab 2</tabpanel></tabpanels>" +
+ "</tabbox></hbox></window>";
+
+const testToolDefinition = {
+ id: "testTool",
+ url: testToolURL,
+ label: "Test Tool",
+ isTargetSupported: () => true,
+ build: (iframeWindow, toolbox) => {
+ return promise.resolve({
+ target: toolbox.target,
+ toolbox: toolbox,
+ isReady: true,
+ destroy: () => {},
+ panelDoc: iframeWindow.document,
+ });
+ }
+};
+
+add_task(function* () {
+ let tab = yield addTab("about:blank");
+
+ let target = TargetFactory.forTab(tab);
+
+ gDevTools.registerTool(testToolDefinition);
+ let toolbox = yield gDevTools.showToolbox(target, testToolDefinition.id);
+
+ let toolPanel = toolbox.getPanel(testToolDefinition.id);
+ let tabbox = toolPanel.panelDoc.getElementById("sidebar");
+
+ info("Creating the sidebar widget");
+ let sidebar = new ToolSidebar(tabbox, toolPanel, "bug1101569");
+
+ info("Checking that existing tabs have been registered");
+ ok(sidebar.getTab("tab1"), "Existing tab 1 was found");
+ ok(sidebar.getTab("tab2"), "Existing tab 2 was found");
+ ok(sidebar.getTabPanel("tabpanel1"), "Existing tabpanel 1 was found");
+ ok(sidebar.getTabPanel("tabpanel2"), "Existing tabpanel 2 was found");
+
+ info("Checking that the sidebar API works with existing tabs");
+
+ sidebar.select("tab2");
+ is(tabbox.selectedTab, tabbox.querySelector("#tab2"),
+ "Existing tabs can be selected");
+
+ sidebar.select("tab1");
+ is(tabbox.selectedTab, tabbox.querySelector("#tab1"),
+ "Existing tabs can be selected");
+
+ is(sidebar.getCurrentTabID(), "tab1", "getCurrentTabID returns the expected id");
+
+ info("Removing a tab");
+ sidebar.removeTab("tab2", "tabpanel2");
+ ok(!sidebar.getTab("tab2"), "Tab 2 was removed correctly");
+ ok(!sidebar.getTabPanel("tabpanel2"), "Tabpanel 2 was removed correctly");
+
+ sidebar.destroy();
+ yield toolbox.destroy();
+ gDevTools.unregisterTool(testToolDefinition.id);
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/client/framework/test/browser_toolbox_sidebar_overflow_menu.js b/devtools/client/framework/test/browser_toolbox_sidebar_overflow_menu.js
new file mode 100644
index 000000000..5f6914a2f
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_sidebar_overflow_menu.js
@@ -0,0 +1,80 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 sidebar widget correctly displays the "all tabs..." button
+// when the tabs overflow.
+
+const {ToolSidebar} = require("devtools/client/framework/sidebar");
+
+const testToolDefinition = {
+ id: "testTool",
+ url: CHROME_URL_ROOT + "browser_toolbox_sidebar_tool.xul",
+ label: "Test Tool",
+ isTargetSupported: () => true,
+ build: (iframeWindow, toolbox) => {
+ return {
+ target: toolbox.target,
+ toolbox: toolbox,
+ isReady: true,
+ destroy: () => {},
+ panelDoc: iframeWindow.document,
+ };
+ }
+};
+
+add_task(function* () {
+ let tab = yield addTab("about:blank");
+ let target = TargetFactory.forTab(tab);
+
+ gDevTools.registerTool(testToolDefinition);
+ let toolbox = yield gDevTools.showToolbox(target, testToolDefinition.id);
+
+ let toolPanel = toolbox.getPanel(testToolDefinition.id);
+ let tabbox = toolPanel.panelDoc.getElementById("sidebar");
+
+ info("Creating the sidebar widget");
+ let sidebar = new ToolSidebar(tabbox, toolPanel, "bug1101569", {
+ showAllTabsMenu: true
+ });
+
+ let allTabsMenu = toolPanel.panelDoc.querySelector(".devtools-sidebar-alltabs");
+ ok(allTabsMenu, "The all-tabs menu is available");
+ is(allTabsMenu.getAttribute("hidden"), "true", "The menu is hidden for now");
+
+ info("Adding 10 tabs to the sidebar widget");
+ for (let nb = 0; nb < 10; nb++) {
+ let url = `data:text/html;charset=utf8,<title>tab ${nb}</title><p>Test tab ${nb}</p>`;
+ sidebar.addTab("tab" + nb, url, {selected: nb === 0});
+ }
+
+ info("Fake an overflow event so that the all-tabs menu is visible");
+ sidebar._onTabBoxOverflow();
+ ok(!allTabsMenu.hasAttribute("hidden"), "The all-tabs menu is now shown");
+
+ info("Select each tab, one by one");
+ for (let nb = 0; nb < 10; nb++) {
+ let id = "tab" + nb;
+
+ info("Found tab item nb " + nb);
+ let item = allTabsMenu.querySelector("#sidebar-alltabs-item-" + id);
+
+ info("Click on the tab");
+ EventUtils.sendMouseEvent({type: "click"}, item, toolPanel.panelDoc.defaultView);
+
+ is(tabbox.selectedTab.id, "sidebar-tab-" + id,
+ "The selected tab is now nb " + nb);
+ }
+
+ info("Fake an underflow event so that the all-tabs menu gets hidden");
+ sidebar._onTabBoxUnderflow();
+ is(allTabsMenu.getAttribute("hidden"), "true", "The all-tabs menu is hidden");
+
+ yield sidebar.destroy();
+ yield toolbox.destroy();
+ gDevTools.unregisterTool(testToolDefinition.id);
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/client/framework/test/browser_toolbox_sidebar_tool.xul b/devtools/client/framework/test/browser_toolbox_sidebar_tool.xul
new file mode 100644
index 000000000..2ce495158
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_sidebar_tool.xul
@@ -0,0 +1,18 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/content/shared/widgets/widgets.css" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/skin/widgets.css" type="text/css"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script type="application/javascript;version=1.8" src="chrome://devtools/content/shared/theme-switching.js"/>
+ <box flex="1" class="devtools-responsive-container theme-body">
+ <vbox flex="1" class="devtools-main-content" id="content">test</vbox>
+ <splitter class="devtools-side-splitter"/>
+ <tabbox flex="1" id="sidebar" class="devtools-sidebar-tabs">
+ <tabs/>
+ <tabpanels flex="1"/>
+ </tabbox>
+ </box>
+</window>
diff --git a/devtools/client/framework/test/browser_toolbox_split_console.js b/devtools/client/framework/test/browser_toolbox_split_console.js
new file mode 100644
index 000000000..8e1fecd15
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_split_console.js
@@ -0,0 +1,85 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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";
+
+// Tests that these toolbox split console APIs work:
+// * toolbox.useKeyWithSplitConsole()
+// * toolbox.isSplitConsoleFocused
+
+let gToolbox = null;
+let panelWin = null;
+
+const URL = "data:text/html;charset=utf8,test split console key delegation";
+
+// Force the old debugger UI since it's directly used (see Bug 1301705)
+Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false);
+registerCleanupFunction(function* () {
+ Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend");
+});
+
+add_task(function* () {
+ let tab = yield addTab(URL);
+ let target = TargetFactory.forTab(tab);
+ gToolbox = yield gDevTools.showToolbox(target, "jsdebugger");
+ panelWin = gToolbox.getPanel("jsdebugger").panelWin;
+
+ yield gToolbox.openSplitConsole();
+ yield testIsSplitConsoleFocused();
+ yield testUseKeyWithSplitConsole();
+ yield testUseKeyWithSplitConsoleWrongTool();
+
+ yield cleanup();
+});
+
+function* testIsSplitConsoleFocused() {
+ yield gToolbox.openSplitConsole();
+ // The newly opened split console should have focus
+ ok(gToolbox.isSplitConsoleFocused(), "Split console is focused");
+ panelWin.focus();
+ ok(!gToolbox.isSplitConsoleFocused(), "Split console is no longer focused");
+}
+
+// A key bound to the selected tool should trigger it's command
+function* testUseKeyWithSplitConsole() {
+ let commandCalled = false;
+
+ info("useKeyWithSplitConsole on debugger while debugger is focused");
+ gToolbox.useKeyWithSplitConsole("F3", () => {
+ commandCalled = true;
+ }, "jsdebugger");
+
+ info("synthesizeKey with the console focused");
+ let consoleInput = gToolbox.getPanel("webconsole").hud.jsterm.inputNode;
+ consoleInput.focus();
+ synthesizeKeyShortcut("F3", panelWin);
+
+ ok(commandCalled, "Shortcut key should trigger the command");
+}
+
+// A key bound to a *different* tool should not trigger it's command
+function* testUseKeyWithSplitConsoleWrongTool() {
+ let commandCalled = false;
+
+ info("useKeyWithSplitConsole on inspector while debugger is focused");
+ gToolbox.useKeyWithSplitConsole("F4", () => {
+ commandCalled = true;
+ }, "inspector");
+
+ info("synthesizeKey with the console focused");
+ let consoleInput = gToolbox.getPanel("webconsole").hud.jsterm.inputNode;
+ consoleInput.focus();
+ synthesizeKeyShortcut("F4", panelWin);
+
+ ok(!commandCalled, "Shortcut key shouldn't trigger the command");
+}
+
+function* cleanup() {
+ // We don't want the open split console to confuse other tests..
+ Services.prefs.clearUserPref("devtools.toolbox.splitconsoleEnabled");
+ yield gToolbox.destroy();
+ gBrowser.removeCurrentTab();
+ gToolbox = panelWin = null;
+}
diff --git a/devtools/client/framework/test/browser_toolbox_tabsswitch_shortcuts.js b/devtools/client/framework/test/browser_toolbox_tabsswitch_shortcuts.js
new file mode 100644
index 000000000..b9401f768
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_tabsswitch_shortcuts.js
@@ -0,0 +1,68 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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";
+
+requestLongerTimeout(2);
+
+var {Toolbox} = require("devtools/client/framework/toolbox");
+
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper("devtools/client/locales/toolbox.properties");
+
+add_task(function* () {
+ let tab = yield addTab("about:blank");
+ let target = TargetFactory.forTab(tab);
+ yield target.makeRemote();
+
+ let toolIDs = gDevTools.getToolDefinitionArray()
+ .filter(def => def.isTargetSupported(target))
+ .map(def => def.id);
+
+ let toolbox = yield gDevTools.showToolbox(target, toolIDs[0], Toolbox.HostType.BOTTOM);
+ let nextShortcut = L10N.getStr("toolbox.nextTool.key");
+ let prevShortcut = L10N.getStr("toolbox.previousTool.key");
+
+ // Iterate over all tools, starting from options to netmonitor, in normal
+ // order.
+ for (let i = 1; i < toolIDs.length; i++) {
+ yield testShortcuts(toolbox, i, nextShortcut, toolIDs);
+ }
+
+ // Iterate again, in the same order, starting from netmonitor (so next one is
+ // 0: options).
+ for (let i = 0; i < toolIDs.length; i++) {
+ yield testShortcuts(toolbox, i, nextShortcut, toolIDs);
+ }
+
+ // Iterate over all tools in reverse order, starting from netmonitor to
+ // options.
+ for (let i = toolIDs.length - 2; i >= 0; i--) {
+ yield testShortcuts(toolbox, i, prevShortcut, toolIDs);
+ }
+
+ // Iterate again, in reverse order again, starting from options (so next one
+ // is length-1: netmonitor).
+ for (let i = toolIDs.length - 1; i >= 0; i--) {
+ yield testShortcuts(toolbox, i, prevShortcut, toolIDs);
+ }
+
+ yield toolbox.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+function* testShortcuts(toolbox, index, shortcut, toolIDs) {
+ info("Testing shortcut to switch to tool " + index + ":" + toolIDs[index] +
+ " using shortcut " + shortcut);
+
+ let onToolSelected = toolbox.once("select");
+ synthesizeKeyShortcut(shortcut);
+ let id = yield onToolSelected;
+
+ info("toolbox-select event from " + id);
+
+ is(toolIDs.indexOf(id), index,
+ "Correct tool is selected on pressing the shortcut for " + id);
+}
diff --git a/devtools/client/framework/test/browser_toolbox_target.js b/devtools/client/framework/test/browser_toolbox_target.js
new file mode 100644
index 000000000..68639c501
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_target.js
@@ -0,0 +1,60 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Test about:devtools-toolbox?target which allows opening a toolbox in an
+// iframe while defining which document to debug by setting a `target`
+// attribute refering to the document to debug.
+
+add_task(function *() {
+ // iframe loads the document to debug
+ let iframe = document.createElement("browser");
+ iframe.setAttribute("type", "content");
+ document.documentElement.appendChild(iframe);
+
+ let onLoad = once(iframe, "load", true);
+ iframe.setAttribute("src", "data:text/html,document to debug");
+ yield onLoad;
+ is(iframe.contentWindow.document.body.innerHTML, "document to debug");
+
+ // toolbox loads the toolbox document
+ let toolboxIframe = document.createElement("iframe");
+ document.documentElement.appendChild(toolboxIframe);
+
+ // Important step to define which target to debug
+ toolboxIframe.target = iframe;
+
+ let onToolboxReady = gDevTools.once("toolbox-ready");
+
+ onLoad = once(toolboxIframe, "load", true);
+ toolboxIframe.setAttribute("src", "about:devtools-toolbox?target");
+ yield onLoad;
+
+ // Also wait for toolbox-ready, as toolbox document load isn't enough, there
+ // is plenty of asynchronous steps during toolbox load
+ info("Waiting for toolbox-ready");
+ let toolbox = yield onToolboxReady;
+
+ let onToolboxDestroyed = gDevTools.once("toolbox-destroyed");
+ let onTabActorDetached = once(toolbox.target.client, "tabDetached");
+
+ info("Removing the iframes");
+ toolboxIframe.remove();
+
+ // And wait for toolbox-destroyed as toolbox unload is also full of
+ // asynchronous operation that outlast unload event
+ info("Waiting for toolbox-destroyed");
+ yield onToolboxDestroyed;
+ info("Toolbox destroyed");
+
+ // Also wait for tabDetached. Toolbox destroys the Target which calls
+ // TabActor.detach(). But Target doesn't wait for detach's end to resolve.
+ // Whereas it is quite important as it is a significant part of toolbox
+ // cleanup. If we do not wait for it and starts removing debugged document,
+ // the actor is still considered as being attached and continues processing
+ // events.
+ yield onTabActorDetached;
+
+ iframe.remove();
+});
diff --git a/devtools/client/framework/test/browser_toolbox_textbox_context_menu.js b/devtools/client/framework/test/browser_toolbox_textbox_context_menu.js
new file mode 100644
index 000000000..2e5f3210e
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_textbox_context_menu.js
@@ -0,0 +1,55 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+const URL = "data:text/html;charset=utf8,test for textbox context menu";
+
+add_task(function* () {
+ let toolbox = yield openNewTabAndToolbox(URL, "inspector");
+ let textboxContextMenu = toolbox.textBoxContextMenuPopup;
+
+ emptyClipboard();
+
+ // Make sure the focus is predictable.
+ let inspector = toolbox.getPanel("inspector");
+ let onFocus = once(inspector.searchBox, "focus");
+ inspector.searchBox.focus();
+ yield onFocus;
+
+ ok(textboxContextMenu, "The textbox context menu is loaded in the toolbox");
+
+ let cmdUndo = textboxContextMenu.querySelector("[command=cmd_undo]");
+ let cmdDelete = textboxContextMenu.querySelector("[command=cmd_delete]");
+ let cmdSelectAll = textboxContextMenu.querySelector("[command=cmd_selectAll]");
+ let cmdCut = textboxContextMenu.querySelector("[command=cmd_cut]");
+ let cmdCopy = textboxContextMenu.querySelector("[command=cmd_copy]");
+ let cmdPaste = textboxContextMenu.querySelector("[command=cmd_paste]");
+
+ info("Opening context menu");
+
+ let onContextMenuPopup = once(textboxContextMenu, "popupshowing");
+ textboxContextMenu.openPopupAtScreen(0, 0, true);
+ yield onContextMenuPopup;
+
+ is(cmdUndo.getAttribute("disabled"), "true", "cmdUndo is disabled");
+ is(cmdDelete.getAttribute("disabled"), "true", "cmdDelete is disabled");
+ is(cmdSelectAll.getAttribute("disabled"), "true", "cmdSelectAll is disabled");
+
+ // Cut/Copy items are enabled in context menu even if there
+ // is no selection. See also Bug 1303033
+ is(cmdCut.getAttribute("disabled"), "", "cmdCut is enabled");
+ is(cmdCopy.getAttribute("disabled"), "", "cmdCopy is enabled");
+
+ if (isWindows()) {
+ // emptyClipboard only works on Windows (666254), assert paste only for this OS.
+ is(cmdPaste.getAttribute("disabled"), "true", "cmdPaste is disabled");
+ }
+
+ yield cleanup(toolbox);
+});
+
+function* cleanup(toolbox) {
+ yield toolbox.destroy();
+ gBrowser.removeCurrentTab();
+}
diff --git a/devtools/client/framework/test/browser_toolbox_theme_registration.js b/devtools/client/framework/test/browser_toolbox_theme_registration.js
new file mode 100644
index 000000000..7794d457c
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_theme_registration.js
@@ -0,0 +1,102 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/* import-globals-from shared-head.js */
+"use strict";
+
+// Test for dynamically registering and unregistering themes
+const CHROME_URL = "chrome://mochitests/content/browser/devtools/client/framework/test/";
+
+var toolbox;
+
+add_task(function* themeRegistration() {
+ let tab = yield addTab("data:text/html,test");
+ let target = TargetFactory.forTab(tab);
+ toolbox = yield gDevTools.showToolbox(target, "options");
+
+ let themeId = yield new Promise(resolve => {
+ gDevTools.once("theme-registered", (e, registeredThemeId) => {
+ resolve(registeredThemeId);
+ });
+
+ gDevTools.registerTheme({
+ id: "test-theme",
+ label: "Test theme",
+ stylesheets: [CHROME_URL + "doc_theme.css"],
+ classList: ["theme-test"],
+ });
+ });
+
+ is(themeId, "test-theme", "theme-registered event handler sent theme id");
+
+ ok(gDevTools.getThemeDefinitionMap().has(themeId), "theme added to map");
+});
+
+add_task(function* themeInOptionsPanel() {
+ let panelWin = toolbox.getCurrentPanel().panelWin;
+ let doc = panelWin.frameElement.contentDocument;
+ let themeBox = doc.getElementById("devtools-theme-box");
+ let testThemeOption = themeBox.querySelector(
+ "input[type=radio][value=test-theme]");
+
+ ok(testThemeOption, "new theme exists in the Options panel");
+
+ let lightThemeOption = themeBox.querySelector(
+ "input[type=radio][value=light]");
+
+ let color = panelWin.getComputedStyle(themeBox).color;
+ isnot(color, "rgb(255, 0, 0)", "style unapplied");
+
+ let onThemeSwithComplete = once(panelWin, "theme-switch-complete");
+
+ // Select test theme.
+ testThemeOption.click();
+
+ info("Waiting for theme to finish loading");
+ yield onThemeSwithComplete;
+
+ color = panelWin.getComputedStyle(themeBox).color;
+ is(color, "rgb(255, 0, 0)", "style applied");
+
+ onThemeSwithComplete = once(panelWin, "theme-switch-complete");
+
+ // Select light theme
+ lightThemeOption.click();
+
+ info("Waiting for theme to finish loading");
+ yield onThemeSwithComplete;
+
+ color = panelWin.getComputedStyle(themeBox).color;
+ isnot(color, "rgb(255, 0, 0)", "style unapplied");
+
+ onThemeSwithComplete = once(panelWin, "theme-switch-complete");
+ // Select test theme again.
+ testThemeOption.click();
+ yield onThemeSwithComplete;
+});
+
+add_task(function* themeUnregistration() {
+ let panelWin = toolbox.getCurrentPanel().panelWin;
+ let onUnRegisteredTheme = once(gDevTools, "theme-unregistered");
+ let onThemeSwitchComplete = once(panelWin, "theme-switch-complete");
+ gDevTools.unregisterTheme("test-theme");
+ yield onUnRegisteredTheme;
+ yield onThemeSwitchComplete;
+
+ ok(!gDevTools.getThemeDefinitionMap().has("test-theme"),
+ "theme removed from map");
+
+ let doc = panelWin.frameElement.contentDocument;
+ let themeBox = doc.getElementById("devtools-theme-box");
+
+ // The default light theme must be selected now.
+ is(themeBox.querySelector("#devtools-theme-box [value=light]").checked, true,
+ "light theme must be selected");
+});
+
+add_task(function* cleanup() {
+ yield toolbox.destroy();
+ toolbox = null;
+});
diff --git a/devtools/client/framework/test/browser_toolbox_toggle.js b/devtools/client/framework/test/browser_toolbox_toggle.js
new file mode 100644
index 000000000..d5b6d0e96
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_toggle.js
@@ -0,0 +1,108 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 toggling the toolbox with ACCEL+SHIFT+I / ACCEL+ALT+I and F12 in docked
+// and detached (window) modes.
+
+const URL = "data:text/html;charset=utf-8,Toggling devtools using shortcuts";
+
+var {Toolbox} = require("devtools/client/framework/toolbox");
+
+add_task(function* () {
+ // Make sure this test starts with the selectedTool pref cleared. Previous
+ // tests select various tools, and that sets this pref.
+ Services.prefs.clearUserPref("devtools.toolbox.selectedTool");
+
+ // Test with ACCEL+SHIFT+I / ACCEL+ALT+I (MacOSX) ; modifiers should match :
+ // - toolbox-key-toggle in devtools/client/framework/toolbox-window.xul
+ // - key_devToolboxMenuItem in browser/base/content/browser.xul
+ info("Test toggle using CTRL+SHIFT+I/CMD+ALT+I");
+ yield testToggle("I", {
+ accelKey: true,
+ shiftKey: !navigator.userAgent.match(/Mac/),
+ altKey: navigator.userAgent.match(/Mac/)
+ });
+
+ // Test with F12 ; no modifiers
+ info("Test toggle using F12");
+ yield testToggle("VK_F12", {});
+});
+
+function* testToggle(key, modifiers) {
+ let tab = yield addTab(URL + " ; key : '" + key + "'");
+ yield gDevTools.showToolbox(TargetFactory.forTab(tab));
+
+ yield testToggleDockedToolbox(tab, key, modifiers);
+ yield testToggleDetachedToolbox(tab, key, modifiers);
+
+ yield cleanup();
+}
+
+function* testToggleDockedToolbox(tab, key, modifiers) {
+ let toolbox = getToolboxForTab(tab);
+
+ isnot(toolbox.hostType, Toolbox.HostType.WINDOW,
+ "Toolbox is docked in the main window");
+
+ info("verify docked toolbox is destroyed when using toggle key");
+ let onToolboxDestroyed = once(gDevTools, "toolbox-destroyed");
+ EventUtils.synthesizeKey(key, modifiers);
+ yield onToolboxDestroyed;
+ ok(true, "Docked toolbox is destroyed when using a toggle key");
+
+ info("verify new toolbox is created when using toggle key");
+ let onToolboxReady = once(gDevTools, "toolbox-ready");
+ EventUtils.synthesizeKey(key, modifiers);
+ yield onToolboxReady;
+ ok(true, "Toolbox is created by using when toggle key");
+}
+
+function* testToggleDetachedToolbox(tab, key, modifiers) {
+ let toolbox = getToolboxForTab(tab);
+
+ info("change the toolbox hostType to WINDOW");
+
+ yield toolbox.switchHost(Toolbox.HostType.WINDOW);
+ is(toolbox.hostType, Toolbox.HostType.WINDOW,
+ "Toolbox opened on separate window");
+
+ info("Wait for focus on the toolbox window");
+ yield new Promise(res => waitForFocus(res, toolbox.win));
+
+ info("Focus main window to put the toolbox window in the background");
+
+ let onMainWindowFocus = once(window, "focus");
+ window.focus();
+ yield onMainWindowFocus;
+ ok(true, "Main window focused");
+
+ info("Verify windowed toolbox is focused instead of closed when using " +
+ "toggle key from the main window");
+ let toolboxWindow = toolbox.win.top;
+ let onToolboxWindowFocus = once(toolboxWindow, "focus", true);
+ EventUtils.synthesizeKey(key, modifiers);
+ yield onToolboxWindowFocus;
+ ok(true, "Toolbox focused and not destroyed");
+
+ info("Verify windowed toolbox is destroyed when using toggle key from its " +
+ "own window");
+
+ let onToolboxDestroyed = once(gDevTools, "toolbox-destroyed");
+ EventUtils.synthesizeKey(key, modifiers, toolboxWindow);
+ yield onToolboxDestroyed;
+ ok(true, "Toolbox destroyed");
+}
+
+function getToolboxForTab(tab) {
+ return gDevTools.getToolbox(TargetFactory.forTab(tab));
+}
+
+function* cleanup() {
+ Services.prefs.setCharPref("devtools.toolbox.host",
+ Toolbox.HostType.BOTTOM);
+ gBrowser.removeCurrentTab();
+}
diff --git a/devtools/client/framework/test/browser_toolbox_tool_ready.js b/devtools/client/framework/test/browser_toolbox_tool_ready.js
new file mode 100644
index 000000000..7d430e7c5
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_tool_ready.js
@@ -0,0 +1,51 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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";
+
+requestLongerTimeout(5);
+
+/**
+ * Whitelisting this test.
+ * As part of bug 1077403, the leaking uncaught rejection should be fixed.
+ */
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Shader Editor is " +
+ "still waiting for a WebGL context to be created.");
+
+function performChecks(target) {
+ return Task.spawn(function* () {
+ let toolIds = gDevTools.getToolDefinitionArray()
+ .filter(def => def.isTargetSupported(target))
+ .map(def => def.id);
+
+ let toolbox;
+ for (let index = 0; index < toolIds.length; index++) {
+ let toolId = toolIds[index];
+
+ info("About to open " + index + "/" + toolId);
+ toolbox = yield gDevTools.showToolbox(target, toolId);
+ ok(toolbox, "toolbox exists for " + toolId);
+ is(toolbox.currentToolId, toolId, "currentToolId should be " + toolId);
+
+ let panel = toolbox.getCurrentPanel();
+ ok(panel.isReady, toolId + " panel should be ready");
+ }
+
+ yield toolbox.destroy();
+ });
+}
+
+function test() {
+ Task.spawn(function* () {
+ toggleAllTools(true);
+ let tab = yield addTab("about:blank");
+ let target = TargetFactory.forTab(tab);
+ yield target.makeRemote();
+ yield performChecks(target);
+ gBrowser.removeCurrentTab();
+ toggleAllTools(false);
+ finish();
+ }, console.error);
+}
diff --git a/devtools/client/framework/test/browser_toolbox_tool_remote_reopen.js b/devtools/client/framework/test/browser_toolbox_tool_remote_reopen.js
new file mode 100644
index 000000000..03461e953
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_tool_remote_reopen.js
@@ -0,0 +1,135 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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";
+
+/**
+ * Whitelisting this test.
+ * As part of bug 1077403, the leaking uncaught rejection should be fixed.
+ */
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Shader Editor is " +
+ "still waiting for a WebGL context to be created.");
+
+const { DebuggerServer } = require("devtools/server/main");
+const { DebuggerClient } = require("devtools/shared/client/main");
+
+// Bug 1277805: Too slow for debug runs
+requestLongerTimeout(2);
+
+/**
+ * Bug 979536: Ensure fronts are destroyed after toolbox close.
+ *
+ * The fronts need to be destroyed manually to unbind their onPacket handlers.
+ *
+ * When you initialize a front and call |this.manage|, it adds a client actor
+ * pool that the DebuggerClient uses to route packet replies to that actor.
+ *
+ * Most (all?) tools create a new front when they are opened. When the destroy
+ * step is skipped and the tool is reopened, a second front is created and also
+ * added to the client actor pool. When a packet reply is received, is ends up
+ * being routed to the first (now unwanted) front that is still in the client
+ * actor pool. Since this is not the same front that was used to make the
+ * request, an error occurs.
+ *
+ * This problem does not occur with the toolbox for a local tab because the
+ * toolbox target creates its own DebuggerClient for the local tab, and the
+ * client is destroyed when the toolbox is closed, which removes the client
+ * actor pools, and avoids this issue.
+ *
+ * In WebIDE, we do not destroy the DebuggerClient on toolbox close because it
+ * is still used for other purposes like managing apps, etc. that aren't part of
+ * a toolbox. Thus, the same client gets reused across multiple toolboxes,
+ * which leads to the tools failing if they don't destroy their fronts.
+ */
+
+function runTools(target) {
+ return Task.spawn(function* () {
+ let toolIds = gDevTools.getToolDefinitionArray()
+ .filter(def => def.isTargetSupported(target))
+ .map(def => def.id);
+
+ let toolbox;
+ for (let index = 0; index < toolIds.length; index++) {
+ let toolId = toolIds[index];
+
+ info("About to open " + index + "/" + toolId);
+ toolbox = yield gDevTools.showToolbox(target, toolId, "window");
+ ok(toolbox, "toolbox exists for " + toolId);
+ is(toolbox.currentToolId, toolId, "currentToolId should be " + toolId);
+
+ let panel = toolbox.getCurrentPanel();
+ ok(panel.isReady, toolId + " panel should be ready");
+ }
+
+ yield toolbox.destroy();
+ });
+}
+
+function getClient() {
+ let deferred = defer();
+
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+
+ let transport = DebuggerServer.connectPipe();
+ let client = new DebuggerClient(transport);
+
+ return client.connect().then(() => client);
+}
+
+function getTarget(client) {
+ let deferred = defer();
+
+ client.listTabs(tabList => {
+ let target = TargetFactory.forRemoteTab({
+ client: client,
+ form: tabList.tabs[tabList.selected],
+ chrome: false
+ });
+ deferred.resolve(target);
+ });
+
+ return deferred.promise;
+}
+
+function test() {
+ Task.spawn(function* () {
+ toggleAllTools(true);
+ yield addTab("about:blank");
+
+ let client = yield getClient();
+ let target = yield getTarget(client);
+ yield runTools(target);
+
+ // Actor fronts should be destroyed now that the toolbox has closed, but
+ // look for any that remain.
+ for (let pool of client.__pools) {
+ if (!pool.__poolMap) {
+ continue;
+ }
+ for (let actor of pool.__poolMap.keys()) {
+ // Bug 1056342: Profiler fails today because of framerate actor, but
+ // this appears more complex to rework, so leave it for that bug to
+ // resolve.
+ if (actor.includes("framerateActor")) {
+ todo(false, "Front for " + actor + " still held in pool!");
+ continue;
+ }
+ // gcliActor is for the commandline which is separate to the toolbox
+ if (actor.includes("gcliActor")) {
+ continue;
+ }
+ ok(false, "Front for " + actor + " still held in pool!");
+ }
+ }
+
+ gBrowser.removeCurrentTab();
+ DebuggerServer.destroy();
+ toggleAllTools(false);
+ finish();
+ }, console.error);
+}
diff --git a/devtools/client/framework/test/browser_toolbox_transport_events.js b/devtools/client/framework/test/browser_toolbox_transport_events.js
new file mode 100644
index 000000000..1e2b67ac4
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_transport_events.js
@@ -0,0 +1,108 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 { on, off } = require("sdk/event/core");
+const { DebuggerClient } = require("devtools/shared/client/main");
+
+function test() {
+ gDevTools.on("toolbox-created", onToolboxCreated);
+ on(DebuggerClient, "connect", onDebuggerClientConnect);
+
+ addTab("about:blank").then(function () {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ gDevTools.showToolbox(target, "webconsole").then(testResults);
+ });
+}
+
+function testResults(toolbox) {
+ testPackets(sent1, received1);
+ testPackets(sent2, received2);
+
+ cleanUp(toolbox);
+}
+
+function cleanUp(toolbox) {
+ gDevTools.off("toolbox-created", onToolboxCreated);
+ off(DebuggerClient, "connect", onDebuggerClientConnect);
+
+ toolbox.destroy().then(function () {
+ gBrowser.removeCurrentTab();
+ executeSoon(function () {
+ finish();
+ });
+ });
+}
+
+function testPackets(sent, received) {
+ ok(sent.length > 0, "There must be at least one sent packet");
+ ok(received.length > 0, "There must be at leaset one received packet");
+
+ if (!sent.length || received.length) {
+ return;
+ }
+
+ let sentPacket = sent[0];
+ let receivedPacket = received[0];
+
+ is(receivedPacket.from, "root",
+ "The first received packet is from the root");
+ is(receivedPacket.applicationType, "browser",
+ "The first received packet has browser type");
+ is(sentPacket.type, "listTabs",
+ "The first sent packet is for list of tabs");
+}
+
+// Listen to the transport object that is associated with the
+// default Toolbox debugger client
+var sent1 = [];
+var received1 = [];
+
+function send1(eventId, packet) {
+ sent1.push(packet);
+}
+
+function onPacket1(eventId, packet) {
+ received1.push(packet);
+}
+
+function onToolboxCreated(eventId, toolbox) {
+ toolbox.target.makeRemote();
+ let client = toolbox.target.client;
+ let transport = client._transport;
+
+ transport.on("send", send1);
+ transport.on("packet", onPacket1);
+
+ client.addOneTimeListener("closed", event => {
+ transport.off("send", send1);
+ transport.off("packet", onPacket1);
+ });
+}
+
+// Listen to all debugger client object protocols.
+var sent2 = [];
+var received2 = [];
+
+function send2(eventId, packet) {
+ sent2.push(packet);
+}
+
+function onPacket2(eventId, packet) {
+ received2.push(packet);
+}
+
+function onDebuggerClientConnect(client) {
+ let transport = client._transport;
+
+ transport.on("send", send2);
+ transport.on("packet", onPacket2);
+
+ client.addOneTimeListener("closed", event => {
+ transport.off("send", send2);
+ transport.off("packet", onPacket2);
+ });
+}
diff --git a/devtools/client/framework/test/browser_toolbox_view_source_01.js b/devtools/client/framework/test/browser_toolbox_view_source_01.js
new file mode 100644
index 000000000..5a9a6d9b0
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_view_source_01.js
@@ -0,0 +1,46 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests that Toolbox#viewSourceInDebugger works when debugger is not
+ * yet opened.
+ */
+
+var URL = `${URL_ROOT}doc_viewsource.html`;
+var JS_URL = `${URL_ROOT}code_math.js`;
+
+// Force the old debugger UI since it's directly used (see Bug 1301705)
+Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false);
+registerCleanupFunction(function* () {
+ Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend");
+});
+
+function* viewSource() {
+ let toolbox = yield openNewTabAndToolbox(URL);
+
+ yield toolbox.viewSourceInDebugger(JS_URL, 2);
+
+ let debuggerPanel = toolbox.getPanel("jsdebugger");
+ ok(debuggerPanel, "The debugger panel was opened.");
+ is(toolbox.currentToolId, "jsdebugger", "The debugger panel was selected.");
+
+ let { DebuggerView } = debuggerPanel.panelWin;
+ let Sources = DebuggerView.Sources;
+
+ is(Sources.selectedValue, getSourceActor(Sources, JS_URL),
+ "The correct source is shown in the debugger.");
+ is(DebuggerView.editor.getCursor().line + 1, 2,
+ "The correct line is highlighted in the debugger's source editor.");
+
+ yield closeToolboxAndTab(toolbox);
+ finish();
+}
+
+function test() {
+ Task.spawn(viewSource).then(finish, (aError) => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ finish();
+ });
+}
diff --git a/devtools/client/framework/test/browser_toolbox_view_source_02.js b/devtools/client/framework/test/browser_toolbox_view_source_02.js
new file mode 100644
index 000000000..c18e885cf
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_view_source_02.js
@@ -0,0 +1,54 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests that Toolbox#viewSourceInDebugger works when debugger is already loaded.
+ */
+
+var URL = `${URL_ROOT}doc_viewsource.html`;
+var JS_URL = `${URL_ROOT}code_math.js`;
+
+// Force the old debugger UI since it's directly used (see Bug 1301705)
+Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false);
+registerCleanupFunction(function* () {
+ Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend");
+});
+
+function* viewSource() {
+ let toolbox = yield openNewTabAndToolbox(URL);
+ let { panelWin: debuggerWin } = yield toolbox.selectTool("jsdebugger");
+ let debuggerEvents = debuggerWin.EVENTS;
+ let { DebuggerView } = debuggerWin;
+ let Sources = DebuggerView.Sources;
+
+ yield debuggerWin.once(debuggerEvents.SOURCE_SHOWN);
+ ok("A source was shown in the debugger.");
+
+ is(Sources.selectedValue, getSourceActor(Sources, JS_URL),
+ "The correct source is initially shown in the debugger.");
+ is(DebuggerView.editor.getCursor().line, 0,
+ "The correct line is initially highlighted in the debugger's source editor.");
+
+ yield toolbox.viewSourceInDebugger(JS_URL, 2);
+
+ let debuggerPanel = toolbox.getPanel("jsdebugger");
+ ok(debuggerPanel, "The debugger panel was opened.");
+ is(toolbox.currentToolId, "jsdebugger", "The debugger panel was selected.");
+
+ is(Sources.selectedValue, getSourceActor(Sources, JS_URL),
+ "The correct source is shown in the debugger.");
+ is(DebuggerView.editor.getCursor().line + 1, 2,
+ "The correct line is highlighted in the debugger's source editor.");
+
+ yield closeToolboxAndTab(toolbox);
+ finish();
+}
+
+function test() {
+ Task.spawn(viewSource).then(finish, (aError) => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ finish();
+ });
+}
diff --git a/devtools/client/framework/test/browser_toolbox_view_source_03.js b/devtools/client/framework/test/browser_toolbox_view_source_03.js
new file mode 100644
index 000000000..2d2cda76f
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_view_source_03.js
@@ -0,0 +1,40 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests that Toolbox#viewSourceInStyleEditor works when style editor is not
+ * yet opened.
+ */
+
+var URL = `${URL_ROOT}doc_viewsource.html`;
+var CSS_URL = `${URL_ROOT}doc_theme.css`;
+
+function* viewSource() {
+ let toolbox = yield openNewTabAndToolbox(URL);
+
+ let fileFound = yield toolbox.viewSourceInStyleEditor(CSS_URL, 2);
+ ok(fileFound, "viewSourceInStyleEditor should resolve to true if source found.");
+
+ let stylePanel = toolbox.getPanel("styleeditor");
+ ok(stylePanel, "The style editor panel was opened.");
+ is(toolbox.currentToolId, "styleeditor", "The style editor panel was selected.");
+
+ let { UI } = stylePanel;
+
+ is(UI.selectedEditor.styleSheet.href, CSS_URL,
+ "The correct source is shown in the style editor.");
+ is(UI.selectedEditor.sourceEditor.getCursor().line + 1, 2,
+ "The correct line is highlighted in the style editor's source editor.");
+
+ yield closeToolboxAndTab(toolbox);
+ finish();
+}
+
+function test() {
+ Task.spawn(viewSource).then(finish, (aError) => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ finish();
+ });
+}
diff --git a/devtools/client/framework/test/browser_toolbox_view_source_04.js b/devtools/client/framework/test/browser_toolbox_view_source_04.js
new file mode 100644
index 000000000..47d86fc11
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_view_source_04.js
@@ -0,0 +1,39 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Tests that Toolbox#viewSourceInScratchpad works.
+ */
+
+var URL = `${URL_ROOT}doc_viewsource.html`;
+
+function* viewSource() {
+ let toolbox = yield openNewTabAndToolbox(URL);
+ let win = yield openScratchpadWindow();
+ let { Scratchpad: scratchpad } = win;
+
+ // Brahm's Cello Sonata No.1, Op.38 now in the scratchpad
+ scratchpad.setText("E G B C B\nA B A G A B\nG E");
+ let scratchpadURL = scratchpad.uniqueName;
+
+ // Now select another tool for focus
+ yield toolbox.selectTool("webconsole");
+
+ yield toolbox.viewSourceInScratchpad(scratchpadURL, 2);
+
+ is(scratchpad.editor.getCursor().line, 2,
+ "The correct line is highlighted in scratchpad's editor.");
+
+ win.close();
+ yield closeToolboxAndTab(toolbox);
+ finish();
+}
+
+function test() {
+ Task.spawn(viewSource).then(finish, (aError) => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ finish();
+ });
+}
diff --git a/devtools/client/framework/test/browser_toolbox_window_reload_target.js b/devtools/client/framework/test/browser_toolbox_window_reload_target.js
new file mode 100644
index 000000000..9f3339728
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_window_reload_target.js
@@ -0,0 +1,100 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+requestLongerTimeout(10);
+
+const TEST_URL = "data:text/html;charset=utf-8," +
+ "<html><head><title>Test reload</title></head>" +
+ "<body><h1>Testing reload from devtools</h1></body></html>";
+
+var {Toolbox} = require("devtools/client/framework/toolbox");
+
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper("devtools/client/locales/toolbox.properties");
+
+var target, toolbox, description, reloadsSent, toolIDs;
+
+function test() {
+ addTab(TEST_URL).then(() => {
+ target = TargetFactory.forTab(gBrowser.selectedTab);
+
+ target.makeRemote().then(() => {
+ toolIDs = gDevTools.getToolDefinitionArray()
+ .filter(def => def.isTargetSupported(target))
+ .map(def => def.id);
+ gDevTools.showToolbox(target, toolIDs[0], Toolbox.HostType.BOTTOM)
+ .then(startReloadTest);
+ });
+ });
+}
+
+function startReloadTest(aToolbox) {
+ getFrameScript(); // causes frame-script-utils to be loaded into the child.
+ toolbox = aToolbox;
+
+ reloadsSent = 0;
+ let reloads = 0;
+ let reloadCounter = (msg) => {
+ reloads++;
+ info("Detected reload #" + reloads);
+ is(reloads, reloadsSent, "Reloaded from devtools window once and only for " + description + "");
+ };
+ gBrowser.selectedBrowser.messageManager.addMessageListener("devtools:test:load", reloadCounter);
+
+ testAllTheTools("docked", () => {
+ let origHostType = toolbox.hostType;
+ toolbox.switchHost(Toolbox.HostType.WINDOW).then(() => {
+ toolbox.win.focus();
+ testAllTheTools("undocked", () => {
+ toolbox.switchHost(origHostType).then(() => {
+ gBrowser.selectedBrowser.messageManager.removeMessageListener("devtools:test:load", reloadCounter);
+ // If we finish too early, the inspector breaks promises:
+ toolbox.getPanel("inspector").once("new-root", finishUp);
+ });
+ });
+ });
+ }, toolIDs.length - 1 /* only test 1 tool in docked mode, to cut down test time */);
+}
+
+function testAllTheTools(docked, callback, toolNum = 0) {
+ if (toolNum >= toolIDs.length) {
+ return callback();
+ }
+ toolbox.selectTool(toolIDs[toolNum]).then(() => {
+ testReload("toolbox.reload.key", docked, toolIDs[toolNum], () => {
+ testReload("toolbox.reload2.key", docked, toolIDs[toolNum], () => {
+ testReload("toolbox.forceReload.key", docked, toolIDs[toolNum], () => {
+ testReload("toolbox.forceReload2.key", docked, toolIDs[toolNum], () => {
+ testAllTheTools(docked, callback, toolNum + 1);
+ });
+ });
+ });
+ });
+ });
+}
+
+function testReload(shortcut, docked, toolID, callback) {
+ let complete = () => {
+ gBrowser.selectedBrowser.messageManager.removeMessageListener("devtools:test:load", complete);
+ return callback();
+ };
+ gBrowser.selectedBrowser.messageManager.addMessageListener("devtools:test:load", complete);
+
+ description = docked + " devtools with tool " + toolID + ", shortcut #" + shortcut;
+ info("Testing reload in " + description);
+ toolbox.win.focus();
+ synthesizeKeyShortcut(L10N.getStr(shortcut), toolbox.win);
+ reloadsSent++;
+}
+
+function finishUp() {
+ toolbox.destroy().then(() => {
+ gBrowser.removeCurrentTab();
+
+ target = toolbox = description = reloadsSent = toolIDs = null;
+
+ finish();
+ });
+}
diff --git a/devtools/client/framework/test/browser_toolbox_window_shortcuts.js b/devtools/client/framework/test/browser_toolbox_window_shortcuts.js
new file mode 100644
index 000000000..dde06dfea
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_window_shortcuts.js
@@ -0,0 +1,84 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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";
+
+var {Toolbox} = require("devtools/client/framework/toolbox");
+
+var toolbox, toolIDs, idIndex, modifiedPrefs = [];
+
+function test() {
+ addTab("about:blank").then(function () {
+ toolIDs = [];
+ for (let [id, definition] of gDevTools._tools) {
+ if (definition.key) {
+ toolIDs.push(id);
+
+ // Enable disabled tools
+ let pref = definition.visibilityswitch, prefValue;
+ try {
+ prefValue = Services.prefs.getBoolPref(pref);
+ } catch (e) {
+ continue;
+ }
+ if (!prefValue) {
+ modifiedPrefs.push(pref);
+ Services.prefs.setBoolPref(pref, true);
+ }
+ }
+ }
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ idIndex = 0;
+ gDevTools.showToolbox(target, toolIDs[0], Toolbox.HostType.WINDOW)
+ .then(testShortcuts);
+ });
+}
+
+function testShortcuts(aToolbox, aIndex) {
+ if (aIndex === undefined) {
+ aIndex = 1;
+ } else if (aIndex == toolIDs.length) {
+ tidyUp();
+ return;
+ }
+
+ toolbox = aToolbox;
+ info("Toolbox fired a `ready` event");
+
+ toolbox.once("select", selectCB);
+
+ let key = gDevTools._tools.get(toolIDs[aIndex]).key;
+ let toolModifiers = gDevTools._tools.get(toolIDs[aIndex]).modifiers;
+ let modifiers = {
+ accelKey: toolModifiers.includes("accel"),
+ altKey: toolModifiers.includes("alt"),
+ shiftKey: toolModifiers.includes("shift"),
+ };
+ idIndex = aIndex;
+ info("Testing shortcut for tool " + aIndex + ":" + toolIDs[aIndex] +
+ " using key " + key);
+ EventUtils.synthesizeKey(key, modifiers, toolbox.win.parent);
+}
+
+function selectCB(event, id) {
+ info("toolbox-select event from " + id);
+
+ is(toolIDs.indexOf(id), idIndex,
+ "Correct tool is selected on pressing the shortcut for " + id);
+
+ testShortcuts(toolbox, idIndex + 1);
+}
+
+function tidyUp() {
+ toolbox.destroy().then(function () {
+ gBrowser.removeCurrentTab();
+
+ for (let pref of modifiedPrefs) {
+ Services.prefs.clearUserPref(pref);
+ }
+ toolbox = toolIDs = idIndex = modifiedPrefs = Toolbox = null;
+ finish();
+ });
+}
diff --git a/devtools/client/framework/test/browser_toolbox_window_title_changes.js b/devtools/client/framework/test/browser_toolbox_window_title_changes.js
new file mode 100644
index 000000000..558c2094f
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_window_title_changes.js
@@ -0,0 +1,108 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+requestLongerTimeout(5);
+
+var {Toolbox} = require("devtools/client/framework/toolbox");
+
+function test() {
+ const URL_1 = "data:text/plain;charset=UTF-8,abcde";
+ const URL_2 = "data:text/plain;charset=UTF-8,12345";
+ const URL_3 = URL_ROOT + "browser_toolbox_window_title_changes_page.html";
+
+ const TOOL_ID_1 = "webconsole";
+ const TOOL_ID_2 = "jsdebugger";
+
+ const NAME_1 = "";
+ const NAME_2 = "";
+ const NAME_3 = "Toolbox test for title update";
+
+ let toolbox;
+
+ addTab(URL_1).then(function () {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ gDevTools.showToolbox(target, null, Toolbox.HostType.BOTTOM)
+ .then(function (aToolbox) { toolbox = aToolbox; })
+ .then(() => toolbox.selectTool(TOOL_ID_1))
+
+ // undock toolbox and check title
+ .then(() => {
+ // We have to first switch the host in order to spawn the new top level window
+ // on which we are going to listen from title change event
+ return toolbox.switchHost(Toolbox.HostType.WINDOW)
+ .then(() => waitForTitleChange(toolbox));
+ })
+ .then(checkTitle.bind(null, NAME_1, URL_1, "toolbox undocked"))
+
+ // switch to different tool and check title
+ .then(() => {
+ let onTitleChanged = waitForTitleChange(toolbox);
+ toolbox.selectTool(TOOL_ID_2);
+ return onTitleChanged;
+ })
+ .then(checkTitle.bind(null, NAME_1, URL_1, "tool changed"))
+
+ // navigate to different local url and check title
+ .then(function () {
+ let onTitleChanged = waitForTitleChange(toolbox);
+ gBrowser.loadURI(URL_2);
+ return onTitleChanged;
+ })
+ .then(checkTitle.bind(null, NAME_2, URL_2, "url changed"))
+
+ // navigate to a real url and check title
+ .then(() => {
+ let onTitleChanged = waitForTitleChange(toolbox);
+ gBrowser.loadURI(URL_3);
+ return onTitleChanged;
+ })
+ .then(checkTitle.bind(null, NAME_3, URL_3, "url changed"))
+
+ // destroy toolbox, create new one hosted in a window (with a
+ // different tool id), and check title
+ .then(function () {
+ // Give the tools a chance to handle the navigation event before
+ // destroying the toolbox.
+ executeSoon(function () {
+ toolbox.destroy()
+ .then(function () {
+ // After destroying the toolbox, a fresh target is required.
+ target = TargetFactory.forTab(gBrowser.selectedTab);
+ return gDevTools.showToolbox(target, null, Toolbox.HostType.WINDOW);
+ })
+ .then(function (aToolbox) { toolbox = aToolbox; })
+ .then(() => {
+ let onTitleChanged = waitForTitleChange(toolbox);
+ toolbox.selectTool(TOOL_ID_1);
+ return onTitleChanged;
+ })
+ .then(checkTitle.bind(null, NAME_3, URL_3,
+ "toolbox destroyed and recreated"))
+
+ // clean up
+ .then(() => toolbox.destroy())
+ .then(function () {
+ toolbox = null;
+ gBrowser.removeCurrentTab();
+ Services.prefs.clearUserPref("devtools.toolbox.host");
+ Services.prefs.clearUserPref("devtools.toolbox.selectedTool");
+ Services.prefs.clearUserPref("devtools.toolbox.sideEnabled");
+ finish();
+ });
+ });
+ });
+ });
+}
+
+function checkTitle(name, url, context) {
+ let win = Services.wm.getMostRecentWindow("devtools:toolbox");
+ let expectedTitle;
+ if (name) {
+ expectedTitle = `Developer Tools - ${name} - ${url}`;
+ } else {
+ expectedTitle = `Developer Tools - ${url}`;
+ }
+ is(win.document.title, expectedTitle, context);
+}
diff --git a/devtools/client/framework/test/browser_toolbox_window_title_changes_page.html b/devtools/client/framework/test/browser_toolbox_window_title_changes_page.html
new file mode 100644
index 000000000..8678469ee
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_window_title_changes_page.html
@@ -0,0 +1,10 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset="UTF-8">
+ <title>Toolbox test for title update</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ </head>
+ <body></body>
+</html>
diff --git a/devtools/client/framework/test/browser_toolbox_window_title_frame_select.js b/devtools/client/framework/test/browser_toolbox_window_title_frame_select.js
new file mode 100644
index 000000000..1e3d66646
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_window_title_frame_select.js
@@ -0,0 +1,94 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/* import-globals-from shared-head.js */
+
+"use strict";
+
+/**
+ * Check that the detached devtools window title is not updated when switching
+ * the selected frame. Also check that frames command button has 'open'
+ * attribute set when the list of frames is opened.
+ */
+
+var {Toolbox} = require("devtools/client/framework/toolbox");
+const URL = URL_ROOT + "browser_toolbox_window_title_frame_select_page.html";
+const IFRAME_URL = URL_ROOT + "browser_toolbox_window_title_changes_page.html";
+
+add_task(function* () {
+ Services.prefs.setBoolPref("devtools.command-button-frames.enabled", true);
+
+ yield addTab(URL);
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ let toolbox = yield gDevTools.showToolbox(target, null,
+ Toolbox.HostType.BOTTOM);
+
+ let onTitleChanged = waitForTitleChange(toolbox);
+ yield toolbox.selectTool("inspector");
+ yield onTitleChanged;
+
+ yield toolbox.switchHost(Toolbox.HostType.WINDOW);
+ // Wait for title change event *after* switch host, in order to listen
+ // for the event on the WINDOW host window, which only exists after switchHost
+ yield waitForTitleChange(toolbox);
+
+ is(getTitle(), `Developer Tools - Page title - ${URL}`,
+ "Devtools title correct after switching to detached window host");
+
+ // Wait for tick to avoid unexpected 'popuphidden' event, which
+ // blocks the frame popup menu opened below. See also bug 1276873
+ yield waitForTick();
+
+ // Open frame menu and wait till it's available on the screen.
+ // Also check 'open' attribute on the command button.
+ let btn = toolbox.doc.getElementById("command-button-frames");
+ ok(!btn.getAttribute("open"), "The open attribute must not be present");
+ let menu = toolbox.showFramesMenu({target: btn});
+ yield once(menu, "open");
+
+ is(btn.getAttribute("open"), "true", "The open attribute must be set");
+
+ // Verify that the frame list menu is populated
+ let frames = menu.items;
+ is(frames.length, 2, "We have both frames in the list");
+
+ let topFrameBtn = frames.filter(b => b.label == URL)[0];
+ let iframeBtn = frames.filter(b => b.label == IFRAME_URL)[0];
+ ok(topFrameBtn, "Got top level document in the list");
+ ok(iframeBtn, "Got iframe document in the list");
+
+ // Listen to will-navigate to check if the view is empty
+ let willNavigate = toolbox.target.once("will-navigate");
+
+ onTitleChanged = waitForTitleChange(toolbox);
+
+ // Only select the iframe after we are able to select an element from the top
+ // level document.
+ let newRoot = toolbox.getPanel("inspector").once("new-root");
+ info("Select the iframe");
+ iframeBtn.click();
+
+ yield willNavigate;
+ yield newRoot;
+ yield onTitleChanged;
+
+ info("Navigation to the iframe is done, the inspector should be back up");
+ is(getTitle(), `Developer Tools - Page title - ${URL}`,
+ "Devtools title was not updated after changing inspected frame");
+
+ info("Cleanup toolbox and test preferences.");
+ yield toolbox.destroy();
+ toolbox = null;
+ gBrowser.removeCurrentTab();
+ Services.prefs.clearUserPref("devtools.toolbox.host");
+ Services.prefs.clearUserPref("devtools.toolbox.selectedTool");
+ Services.prefs.clearUserPref("devtools.toolbox.sideEnabled");
+ Services.prefs.clearUserPref("devtools.command-button-frames.enabled");
+ finish();
+});
+
+function getTitle() {
+ return Services.wm.getMostRecentWindow("devtools:toolbox").document.title;
+}
diff --git a/devtools/client/framework/test/browser_toolbox_window_title_frame_select_page.html b/devtools/client/framework/test/browser_toolbox_window_title_frame_select_page.html
new file mode 100644
index 000000000..1eda94a9c
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_window_title_frame_select_page.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset="UTF-8">
+ <title>Page title</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <iframe src="browser_toolbox_window_title_changes_page.html"></iframe>
+ </head>
+ <body></body>
+</html>
diff --git a/devtools/client/framework/test/browser_toolbox_zoom.js b/devtools/client/framework/test/browser_toolbox_zoom.js
new file mode 100644
index 000000000..d078b4bc2
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_zoom.js
@@ -0,0 +1,67 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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";
+
+var toolbox;
+
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper("devtools/client/locales/toolbox.properties");
+
+function test() {
+ addTab("about:blank").then(openToolbox);
+}
+
+function openToolbox() {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+
+ gDevTools.showToolbox(target).then((aToolbox) => {
+ toolbox = aToolbox;
+ toolbox.selectTool("styleeditor").then(testZoom);
+ });
+}
+
+function testZoom() {
+ info("testing zoom keys");
+
+ testZoomLevel("In", 2, 1.2);
+ testZoomLevel("Out", 3, 0.9);
+ testZoomLevel("Reset", 1, 1);
+
+ tidyUp();
+}
+
+function testZoomLevel(type, times, expected) {
+ sendZoomKey("toolbox.zoom" + type + ".key", times);
+
+ let zoom = getCurrentZoom(toolbox);
+ is(zoom.toFixed(2), expected, "zoom level correct after zoom " + type);
+
+ let savedZoom = parseFloat(Services.prefs.getCharPref(
+ "devtools.toolbox.zoomValue"));
+ is(savedZoom.toFixed(2), expected,
+ "saved zoom level is correct after zoom " + type);
+}
+
+function sendZoomKey(shortcut, times) {
+ for (let i = 0; i < times; i++) {
+ synthesizeKeyShortcut(L10N.getStr(shortcut));
+ }
+}
+
+function getCurrentZoom() {
+ let windowUtils = toolbox.win.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ return windowUtils.fullZoom;
+}
+
+function tidyUp() {
+ toolbox.destroy().then(function () {
+ gBrowser.removeCurrentTab();
+
+ toolbox = null;
+ finish();
+ });
+}
diff --git a/devtools/client/framework/test/browser_two_tabs.js b/devtools/client/framework/test/browser_two_tabs.js
new file mode 100644
index 000000000..08d5f2391
--- /dev/null
+++ b/devtools/client/framework/test/browser_two_tabs.js
@@ -0,0 +1,149 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/**
+ * Check regression when opening two tabs
+ */
+
+var { DebuggerServer } = require("devtools/server/main");
+var { DebuggerClient } = require("devtools/shared/client/main");
+
+const TAB_URL_1 = "data:text/html;charset=utf-8,foo";
+const TAB_URL_2 = "data:text/html;charset=utf-8,bar";
+
+var gClient;
+var gTab1, gTab2;
+var gTabActor1, gTabActor2;
+
+function test() {
+ waitForExplicitFinish();
+
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+
+ openTabs();
+}
+
+function openTabs() {
+ // Open two tabs, select the second
+ addTab(TAB_URL_1).then(tab1 => {
+ gTab1 = tab1;
+ addTab(TAB_URL_2).then(tab2 => {
+ gTab2 = tab2;
+
+ connect();
+ });
+ });
+}
+
+function connect() {
+ // Connect to debugger server to fetch the two tab actors
+ gClient = new DebuggerClient(DebuggerServer.connectPipe());
+ gClient.connect()
+ .then(() => gClient.listTabs())
+ .then(response => {
+ // Fetch the tab actors for each tab
+ gTabActor1 = response.tabs.filter(a => a.url === TAB_URL_1)[0];
+ gTabActor2 = response.tabs.filter(a => a.url === TAB_URL_2)[0];
+
+ checkGetTab();
+ });
+}
+
+function checkGetTab() {
+ gClient.getTab({tab: gTab1})
+ .then(response => {
+ is(JSON.stringify(gTabActor1), JSON.stringify(response.tab),
+ "getTab returns the same tab grip for first tab");
+ })
+ .then(() => {
+ let filter = {};
+ // Filter either by tabId or outerWindowID,
+ // if we are running tests OOP or not.
+ if (gTab1.linkedBrowser.frameLoader.tabParent) {
+ filter.tabId = gTab1.linkedBrowser.frameLoader.tabParent.tabId;
+ } else {
+ let windowUtils = gTab1.linkedBrowser.contentWindow
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ filter.outerWindowID = windowUtils.outerWindowID;
+ }
+ return gClient.getTab(filter);
+ })
+ .then(response => {
+ is(JSON.stringify(gTabActor1), JSON.stringify(response.tab),
+ "getTab returns the same tab grip when filtering by tabId/outerWindowID");
+ })
+ .then(() => gClient.getTab({tab: gTab2}))
+ .then(response => {
+ is(JSON.stringify(gTabActor2), JSON.stringify(response.tab),
+ "getTab returns the same tab grip for second tab");
+ })
+ .then(checkGetTabFailures);
+}
+
+function checkGetTabFailures() {
+ gClient.getTab({ tabId: -999 })
+ .then(
+ response => ok(false, "getTab unexpectedly succeed with a wrong tabId"),
+ response => {
+ is(response.error, "noTab");
+ is(response.message, "Unable to find tab with tabId '-999'");
+ }
+ )
+ .then(() => gClient.getTab({ outerWindowID: -999 }))
+ .then(
+ response => ok(false, "getTab unexpectedly succeed with a wrong outerWindowID"),
+ response => {
+ is(response.error, "noTab");
+ is(response.message, "Unable to find tab with outerWindowID '-999'");
+ }
+ )
+ .then(checkSelectedTabActor);
+
+}
+
+function checkSelectedTabActor() {
+ // Send a naive request to the second tab actor
+ // to check if it works
+ gClient.request({ to: gTabActor2.consoleActor, type: "startListeners", listeners: [] }, aResponse => {
+ ok("startedListeners" in aResponse, "Actor from the selected tab should respond to the request.");
+
+ closeSecondTab();
+ });
+}
+
+function closeSecondTab() {
+ // Close the second tab, currently selected
+ let container = gBrowser.tabContainer;
+ container.addEventListener("TabClose", function onTabClose() {
+ container.removeEventListener("TabClose", onTabClose);
+
+ checkFirstTabActor();
+ });
+ gBrowser.removeTab(gTab2);
+}
+
+function checkFirstTabActor() {
+ // then send a request to the first tab actor
+ // to check if it still works
+ gClient.request({ to: gTabActor1.consoleActor, type: "startListeners", listeners: [] }, aResponse => {
+ ok("startedListeners" in aResponse, "Actor from the first tab should still respond.");
+
+ cleanup();
+ });
+}
+
+function cleanup() {
+ let container = gBrowser.tabContainer;
+ container.addEventListener("TabClose", function onTabClose() {
+ container.removeEventListener("TabClose", onTabClose);
+
+ gClient.close().then(finish);
+ });
+ gBrowser.removeTab(gTab1);
+}
diff --git a/devtools/client/framework/test/code_binary_search.coffee b/devtools/client/framework/test/code_binary_search.coffee
new file mode 100644
index 000000000..e3dacdaaa
--- /dev/null
+++ b/devtools/client/framework/test/code_binary_search.coffee
@@ -0,0 +1,18 @@
+# Uses a binary search algorithm to locate a value in the specified array.
+window.binary_search = (items, value) ->
+
+ start = 0
+ stop = items.length - 1
+ pivot = Math.floor (start + stop) / 2
+
+ while items[pivot] isnt value and start < stop
+
+ # Adjust the search area.
+ stop = pivot - 1 if value < items[pivot]
+ start = pivot + 1 if value > items[pivot]
+
+ # Recalculate the pivot.
+ pivot = Math.floor (stop + start) / 2
+
+ # Make sure we've found the correct value.
+ if items[pivot] is value then pivot else -1 \ No newline at end of file
diff --git a/devtools/client/framework/test/code_binary_search.js b/devtools/client/framework/test/code_binary_search.js
new file mode 100644
index 000000000..c43848a60
--- /dev/null
+++ b/devtools/client/framework/test/code_binary_search.js
@@ -0,0 +1,29 @@
+// Generated by CoffeeScript 1.6.1
+(function() {
+
+ window.binary_search = function(items, value) {
+ var pivot, start, stop;
+ start = 0;
+ stop = items.length - 1;
+ pivot = Math.floor((start + stop) / 2);
+ while (items[pivot] !== value && start < stop) {
+ if (value < items[pivot]) {
+ stop = pivot - 1;
+ }
+ if (value > items[pivot]) {
+ start = pivot + 1;
+ }
+ pivot = Math.floor((stop + start) / 2);
+ }
+ if (items[pivot] === value) {
+ return pivot;
+ } else {
+ return -1;
+ }
+ };
+
+}).call(this);
+
+/*
+//# sourceMappingURL=code_binary_search.map
+*/
diff --git a/devtools/client/framework/test/code_binary_search.map b/devtools/client/framework/test/code_binary_search.map
new file mode 100644
index 000000000..8d2251125
--- /dev/null
+++ b/devtools/client/framework/test/code_binary_search.map
@@ -0,0 +1,10 @@
+{
+ "version": 3,
+ "file": "code_binary_search.js",
+ "sourceRoot": "",
+ "sources": [
+ "code_binary_search.coffee"
+ ],
+ "names": [],
+ "mappings": ";AACA;CAAA;CAAA,CAAA,CAAuB,EAAA,CAAjB,GAAkB,IAAxB;CAEE,OAAA,UAAA;CAAA,EAAQ,CAAR,CAAA;CAAA,EACQ,CAAR,CAAa,CAAL;CADR,EAEQ,CAAR,CAAA;CAEA,EAA0C,CAAR,CAAtB,MAAN;CAGJ,EAA6B,CAAR,CAAA,CAArB;CAAA,EAAQ,CAAR,CAAQ,GAAR;QAAA;CACA,EAA6B,CAAR,CAAA,CAArB;CAAA,EAAQ,EAAR,GAAA;QADA;CAAA,EAIQ,CAAI,CAAZ,CAAA;CAXF,IAIA;CAUA,GAAA,CAAS;CAAT,YAA8B;MAA9B;AAA0C,CAAD,YAAA;MAhBpB;CAAvB,EAAuB;CAAvB"
+}
diff --git a/devtools/client/framework/test/code_math.js b/devtools/client/framework/test/code_math.js
new file mode 100644
index 000000000..9fe2a3541
--- /dev/null
+++ b/devtools/client/framework/test/code_math.js
@@ -0,0 +1,9 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+function add(a, b, k) {
+ var result = a + b;
+ return k(result);
+}
diff --git a/devtools/client/framework/test/code_ugly.js b/devtools/client/framework/test/code_ugly.js
new file mode 100644
index 000000000..ccf8d5488
--- /dev/null
+++ b/devtools/client/framework/test/code_ugly.js
@@ -0,0 +1,3 @@
+function foo() { var a=1; var b=2; bar(a, b); }
+function bar(c, d) { return c - d; }
+foo();
diff --git a/devtools/client/framework/test/doc_empty-tab-01.html b/devtools/client/framework/test/doc_empty-tab-01.html
new file mode 100644
index 000000000..28398f776
--- /dev/null
+++ b/devtools/client/framework/test/doc_empty-tab-01.html
@@ -0,0 +1,14 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Empty test page 1</title>
+ </head>
+
+ <body>
+ </body>
+
+</html>
diff --git a/devtools/client/framework/test/doc_theme.css b/devtools/client/framework/test/doc_theme.css
new file mode 100644
index 000000000..5ed6e866a
--- /dev/null
+++ b/devtools/client/framework/test/doc_theme.css
@@ -0,0 +1,3 @@
+.theme-test #devtools-theme-box {
+ color: red !important;
+}
diff --git a/devtools/client/framework/test/doc_viewsource.html b/devtools/client/framework/test/doc_viewsource.html
new file mode 100644
index 000000000..7094eb87e
--- /dev/null
+++ b/devtools/client/framework/test/doc_viewsource.html
@@ -0,0 +1,13 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset="UTF-8">
+ <title>Toolbox test for View Source methods</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <link charset="UTF-8" rel="stylesheet" href="doc_theme.css" />
+ <script src="code_math.js"></script>
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/devtools/client/framework/test/head.js b/devtools/client/framework/test/head.js
new file mode 100644
index 000000000..22433b237
--- /dev/null
+++ b/devtools/client/framework/test/head.js
@@ -0,0 +1,148 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/* import-globals-from shared-head.js */
+
+// shared-head.js handles imports, constants, and utility functions
+Services.scriptloader.loadSubScript("chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js", this);
+
+function toggleAllTools(state) {
+ for (let [, tool] of gDevTools._tools) {
+ if (!tool.visibilityswitch) {
+ continue;
+ }
+ if (state) {
+ Services.prefs.setBoolPref(tool.visibilityswitch, true);
+ } else {
+ Services.prefs.clearUserPref(tool.visibilityswitch);
+ }
+ }
+}
+
+function getChromeActors(callback)
+{
+ let { DebuggerServer } = require("devtools/server/main");
+ let { DebuggerClient } = require("devtools/shared/client/main");
+
+ 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);
+ });
+
+ SimpleTest.registerCleanupFunction(() => {
+ DebuggerServer.destroy();
+ });
+}
+
+function getSourceActor(aSources, aURL) {
+ let item = aSources.getItemForAttachment(a => a.source.url === aURL);
+ return item && item.value;
+}
+
+/**
+ * Open a Scratchpad window.
+ *
+ * @return nsIDOMWindow
+ * The new window object that holds Scratchpad.
+ */
+function* openScratchpadWindow() {
+ let { promise: p, resolve } = defer();
+ let win = ScratchpadManager.openScratchpad();
+
+ yield once(win, "load");
+
+ win.Scratchpad.addObserver({
+ onReady: function () {
+ win.Scratchpad.removeObserver(this);
+ resolve(win);
+ }
+ });
+ return p;
+}
+
+/**
+ * Wait for a content -> chrome message on the message manager (the window
+ * messagemanager is used).
+ * @param {String} name The message name
+ * @return {Promise} A promise that resolves to the response data when the
+ * message has been received
+ */
+function waitForContentMessage(name) {
+ info("Expecting message " + name + " from content");
+
+ let mm = gBrowser.selectedBrowser.messageManager;
+
+ let def = defer();
+ mm.addMessageListener(name, function onMessage(msg) {
+ mm.removeMessageListener(name, onMessage);
+ def.resolve(msg.data);
+ });
+ return def.promise;
+}
+
+/**
+ * Send an async message to the frame script (chrome -> content) and wait for a
+ * response message with the same name (content -> chrome).
+ * @param {String} name The message name. Should be one of the messages defined
+ * in doc_frame_script.js
+ * @param {Object} data Optional data to send along
+ * @param {Object} objects Optional CPOW objects to send along
+ * @param {Boolean} expectResponse If set to false, don't wait for a response
+ * with the same name from the content script. Defaults to true.
+ * @return {Promise} Resolves to the response data if a response is expected,
+ * immediately resolves otherwise
+ */
+function executeInContent(name, data = {}, objects = {}, expectResponse = true) {
+ info("Sending message " + name + " to content");
+ let mm = gBrowser.selectedBrowser.messageManager;
+
+ mm.sendAsyncMessage(name, data, objects);
+ if (expectResponse) {
+ return waitForContentMessage(name);
+ } else {
+ return promise.resolve();
+ }
+}
+
+/**
+ * Synthesize a keypress from a <key> element, taking into account
+ * any modifiers.
+ * @param {Element} el the <key> element to synthesize
+ */
+function synthesizeKeyElement(el) {
+ let key = el.getAttribute("key") || el.getAttribute("keycode");
+ let mod = {};
+ el.getAttribute("modifiers").split(" ").forEach((m) => mod[m + "Key"] = true);
+ info(`Synthesizing: key=${key}, mod=${JSON.stringify(mod)}`);
+ EventUtils.synthesizeKey(key, mod, el.ownerDocument.defaultView);
+}
+
+/* Check the toolbox host type and prefs to make sure they match the
+ * expected values
+ * @param {Toolbox}
+ * @param {HostType} hostType
+ * One of {SIDE, BOTTOM, WINDOW} from Toolbox.HostType
+ * @param {HostType} Optional previousHostType
+ * The host that will be switched to when calling switchToPreviousHost
+ */
+function checkHostType(toolbox, hostType, previousHostType) {
+ is(toolbox.hostType, hostType, "host type is " + hostType);
+
+ let pref = Services.prefs.getCharPref("devtools.toolbox.host");
+ is(pref, hostType, "host pref is " + hostType);
+
+ if (previousHostType) {
+ is(Services.prefs.getCharPref("devtools.toolbox.previousHost"),
+ previousHostType, "The previous host is correct");
+ }
+}
diff --git a/devtools/client/framework/test/helper_disable_cache.js b/devtools/client/framework/test/helper_disable_cache.js
new file mode 100644
index 000000000..5e2feef8f
--- /dev/null
+++ b/devtools/client/framework/test/helper_disable_cache.js
@@ -0,0 +1,128 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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";
+
+// Common code shared by browser_toolbox_options_disable_cache-*.js
+const TEST_URI = URL_ROOT + "browser_toolbox_options_disable_cache.sjs";
+var tabs = [
+ {
+ title: "Tab 0",
+ desc: "Toggles cache on.",
+ startToolbox: true
+ },
+ {
+ title: "Tab 1",
+ desc: "Toolbox open before Tab 1 toggles cache.",
+ startToolbox: true
+ },
+ {
+ title: "Tab 2",
+ desc: "Opens toolbox after Tab 1 has toggled cache. Also closes and opens.",
+ startToolbox: false
+ },
+ {
+ title: "Tab 3",
+ desc: "No toolbox",
+ startToolbox: false
+ }];
+
+function* initTab(tabX, startToolbox) {
+ tabX.tab = yield addTab(TEST_URI);
+ tabX.target = TargetFactory.forTab(tabX.tab);
+
+ if (startToolbox) {
+ tabX.toolbox = yield gDevTools.showToolbox(tabX.target, "options");
+ }
+}
+
+function* checkCacheStateForAllTabs(states) {
+ for (let i = 0; i < tabs.length; i++) {
+ let tab = tabs[i];
+ yield checkCacheEnabled(tab, states[i]);
+ }
+}
+
+function* checkCacheEnabled(tabX, expected) {
+ gBrowser.selectedTab = tabX.tab;
+
+ yield reloadTab(tabX);
+
+ let oldGuid = yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function () {
+ let doc = content.document;
+ let h1 = doc.querySelector("h1");
+ return h1.textContent;
+ });
+
+ yield reloadTab(tabX);
+
+ let guid = yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function () {
+ let doc = content.document;
+ let h1 = doc.querySelector("h1");
+ return h1.textContent;
+ });
+
+ if (expected) {
+ is(guid, oldGuid, tabX.title + " cache is enabled");
+ } else {
+ isnot(guid, oldGuid, tabX.title + " cache is not enabled");
+ }
+}
+
+function* setDisableCacheCheckboxChecked(tabX, state) {
+ gBrowser.selectedTab = tabX.tab;
+
+ let panel = tabX.toolbox.getCurrentPanel();
+ let cbx = panel.panelDoc.getElementById("devtools-disable-cache");
+
+ if (cbx.checked !== state) {
+ info("Setting disable cache checkbox to " + state + " for " + tabX.title);
+ cbx.click();
+
+ // We need to wait for all checkboxes to be updated and the docshells to
+ // apply the new cache settings.
+ yield waitForTick();
+ }
+}
+
+function reloadTab(tabX) {
+ let def = defer();
+ let browser = gBrowser.selectedBrowser;
+
+ BrowserTestUtils.browserLoaded(browser).then(function () {
+ info("Reloaded tab " + tabX.title);
+ def.resolve();
+ });
+
+ info("Reloading tab " + tabX.title);
+ let mm = getFrameScript();
+ mm.sendAsyncMessage("devtools:test:reload");
+
+ return def.promise;
+}
+
+function* destroyTab(tabX) {
+ let toolbox = gDevTools.getToolbox(tabX.target);
+
+ let onceDestroyed = promise.resolve();
+ if (toolbox) {
+ onceDestroyed = gDevTools.once("toolbox-destroyed");
+ }
+
+ info("Removing tab " + tabX.title);
+ gBrowser.removeTab(tabX.tab);
+ info("Removed tab " + tabX.title);
+
+ info("Waiting for toolbox-destroyed");
+ yield onceDestroyed;
+}
+
+function* finishUp() {
+ for (let tab of tabs) {
+ yield destroyTab(tab);
+ }
+
+ tabs = null;
+}
diff --git a/devtools/client/framework/test/serviceworker.js b/devtools/client/framework/test/serviceworker.js
new file mode 100644
index 000000000..ed3c1ec32
--- /dev/null
+++ b/devtools/client/framework/test/serviceworker.js
@@ -0,0 +1,6 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// empty service worker, always succeed!
diff --git a/devtools/client/framework/test/shared-head.js b/devtools/client/framework/test/shared-head.js
new file mode 100644
index 000000000..a89c6d752
--- /dev/null
+++ b/devtools/client/framework/test/shared-head.js
@@ -0,0 +1,596 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+
+"use strict";
+
+// This shared-head.js file is used for multiple mochitest test directories in
+// devtools.
+// It contains various common helper functions.
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr, Constructor: CC}
+ = Components;
+
+function scopedCuImport(path) {
+ const scope = {};
+ Cu.import(path, scope);
+ return scope;
+}
+
+const {console} = scopedCuImport("resource://gre/modules/Console.jsm");
+const {ScratchpadManager} = scopedCuImport("resource://devtools/client/scratchpad/scratchpad-manager.jsm");
+const {loader, require} = scopedCuImport("resource://devtools/shared/Loader.jsm");
+
+const {gDevTools} = require("devtools/client/framework/devtools");
+const {TargetFactory} = require("devtools/client/framework/target");
+const DevToolsUtils = require("devtools/shared/DevToolsUtils");
+const flags = require("devtools/shared/flags");
+let promise = require("promise");
+let defer = require("devtools/shared/defer");
+const Services = require("Services");
+const {Task} = require("devtools/shared/task");
+const {KeyShortcuts} = require("devtools/client/shared/key-shortcuts");
+
+const TEST_DIR = gTestPath.substr(0, gTestPath.lastIndexOf("/"));
+const CHROME_URL_ROOT = TEST_DIR + "/";
+const URL_ROOT = CHROME_URL_ROOT.replace("chrome://mochitests/content/",
+ "http://example.com/");
+const URL_ROOT_SSL = CHROME_URL_ROOT.replace("chrome://mochitests/content/",
+ "https://example.com/");
+
+// All test are asynchronous
+waitForExplicitFinish();
+
+var EXPECTED_DTU_ASSERT_FAILURE_COUNT = 0;
+
+registerCleanupFunction(function () {
+ if (DevToolsUtils.assertionFailureCount !==
+ EXPECTED_DTU_ASSERT_FAILURE_COUNT) {
+ ok(false,
+ "Should have had the expected number of DevToolsUtils.assert() failures."
+ + " Expected " + EXPECTED_DTU_ASSERT_FAILURE_COUNT
+ + ", got " + DevToolsUtils.assertionFailureCount);
+ }
+});
+
+// Uncomment this pref to dump all devtools emitted events to the console.
+// Services.prefs.setBoolPref("devtools.dump.emit", true);
+
+/**
+ * Watch console messages for failed propType definitions in React components.
+ */
+const ConsoleObserver = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
+
+ observe: function (subject, topic, data) {
+ let message = subject.wrappedJSObject.arguments[0];
+
+ if (/Failed propType/.test(message)) {
+ ok(false, message);
+ }
+ }
+};
+
+Services.obs.addObserver(ConsoleObserver, "console-api-log-event", false);
+registerCleanupFunction(() => {
+ Services.obs.removeObserver(ConsoleObserver, "console-api-log-event");
+});
+
+var waitForTime = DevToolsUtils.waitForTime;
+
+function getFrameScript() {
+ let mm = gBrowser.selectedBrowser.messageManager;
+ let frameURL = "chrome://devtools/content/shared/frame-script-utils.js";
+ mm.loadFrameScript(frameURL, false);
+ SimpleTest.registerCleanupFunction(() => {
+ mm = null;
+ });
+ return mm;
+}
+
+flags.testing = true;
+registerCleanupFunction(() => {
+ flags.testing = false;
+ Services.prefs.clearUserPref("devtools.dump.emit");
+ Services.prefs.clearUserPref("devtools.toolbox.host");
+ Services.prefs.clearUserPref("devtools.toolbox.previousHost");
+ Services.prefs.clearUserPref("devtools.toolbox.splitconsoleEnabled");
+});
+
+registerCleanupFunction(function* cleanup() {
+ while (gBrowser.tabs.length > 1) {
+ yield closeTabAndToolbox(gBrowser.selectedTab);
+ }
+});
+
+/**
+ * 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
+ * @param {Object} options Object with various optional fields:
+ * - {Boolean} background If true, open the tab in background
+ * - {ChromeWindow} window Firefox top level window we should use to open the tab
+ * @return a promise that resolves to the tab object when the url is loaded
+ */
+var addTab = Task.async(function* (url, options = { background: false, window: window }) {
+ info("Adding a new tab with URL: " + url);
+
+ let { background } = options;
+ let { gBrowser } = options.window ? options.window : window;
+
+ let tab = gBrowser.addTab(url);
+ if (!background) {
+ gBrowser.selectedTab = tab;
+ }
+ yield BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+ info("Tab added and finished loading");
+
+ return tab;
+});
+
+/**
+ * Remove the given tab.
+ * @param {Object} tab The tab to be removed.
+ * @return Promise<undefined> resolved when the tab is successfully removed.
+ */
+var removeTab = Task.async(function* (tab) {
+ info("Removing tab.");
+
+ let { gBrowser } = tab.ownerDocument.defaultView;
+ let onClose = once(gBrowser.tabContainer, "TabClose");
+ gBrowser.removeTab(tab);
+ yield onClose;
+
+ info("Tab removed and finished closing");
+});
+
+/**
+ * Refresh the given tab.
+ * @param {Object} tab The tab to be refreshed.
+ * @return Promise<undefined> resolved when the tab is successfully refreshed.
+ */
+var refreshTab = Task.async(function*(tab) {
+ info("Refreshing tab.");
+ const finished = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ gBrowser.reloadTab(gBrowser.selectedTab);
+ yield finished;
+ info("Tab finished refreshing.");
+});
+
+/**
+ * Simulate a key event from a <key> element.
+ * @param {DOMNode} key
+ */
+function synthesizeKeyFromKeyTag(key) {
+ is(key && key.tagName, "key", "Successfully retrieved the <key> node");
+
+ let modifiersAttr = key.getAttribute("modifiers");
+
+ let name = null;
+
+ if (key.getAttribute("keycode")) {
+ name = key.getAttribute("keycode");
+ } else if (key.getAttribute("key")) {
+ name = key.getAttribute("key");
+ }
+
+ isnot(name, null, "Successfully retrieved keycode/key");
+
+ let modifiers = {
+ shiftKey: !!modifiersAttr.match("shift"),
+ ctrlKey: !!modifiersAttr.match("control"),
+ altKey: !!modifiersAttr.match("alt"),
+ metaKey: !!modifiersAttr.match("meta"),
+ accelKey: !!modifiersAttr.match("accel")
+ };
+
+ info("Synthesizing key " + name + " " + JSON.stringify(modifiers));
+ EventUtils.synthesizeKey(name, modifiers);
+}
+
+/**
+ * Simulate a key event from an electron key shortcut string:
+ * https://github.com/electron/electron/blob/master/docs/api/accelerator.md
+ *
+ * @param {String} key
+ * @param {DOMWindow} target
+ * Optional window where to fire the key event
+ */
+function synthesizeKeyShortcut(key, target) {
+ // parseElectronKey requires any window, just to access `KeyboardEvent`
+ let window = Services.appShell.hiddenDOMWindow;
+ let shortcut = KeyShortcuts.parseElectronKey(window, key);
+ let keyEvent = {
+ altKey: shortcut.alt,
+ ctrlKey: shortcut.ctrl,
+ metaKey: shortcut.meta,
+ shiftKey: shortcut.shift
+ };
+ if (shortcut.keyCode) {
+ keyEvent.keyCode = shortcut.keyCode;
+ }
+
+ info("Synthesizing key shortcut: " + key);
+ EventUtils.synthesizeKey(shortcut.key || "", keyEvent, target);
+}
+
+/**
+ * Wait for eventName on target to be delivered a number of times.
+ *
+ * @param {Object} target
+ * An observable object that either supports on/off or
+ * addEventListener/removeEventListener
+ * @param {String} eventName
+ * @param {Number} numTimes
+ * Number of deliveries to wait for.
+ * @param {Boolean} useCapture
+ * Optional, for addEventListener/removeEventListener
+ * @return A promise that resolves when the event has been handled
+ */
+function waitForNEvents(target, eventName, numTimes, useCapture = false) {
+ info("Waiting for event: '" + eventName + "' on " + target + ".");
+
+ let deferred = defer();
+ let count = 0;
+
+ 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 + ".");
+ if (++count == numTimes) {
+ target[remove](eventName, onEvent, useCapture);
+ deferred.resolve.apply(deferred, aArgs);
+ }
+ }, useCapture);
+ break;
+ }
+ }
+
+ return deferred.promise;
+}
+
+/**
+ * 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) {
+ return waitForNEvents(target, eventName, 1, useCapture);
+}
+
+/**
+ * Some tests may need to import one or more of the test helper scripts.
+ * A test helper script is simply a js file that contains common test code that
+ * is either not common-enough to be in head.js, or that is located in a
+ * separate directory.
+ * The script will be loaded synchronously and in the test's scope.
+ * @param {String} filePath The file path, relative to the current directory.
+ * Examples:
+ * - "helper_attributes_test_runner.js"
+ * - "../../../commandline/test/helpers.js"
+ */
+function loadHelperScript(filePath) {
+ let testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/"));
+ Services.scriptloader.loadSubScript(testDir + "/" + filePath, this);
+}
+
+/**
+ * Wait for a tick.
+ * @return {Promise}
+ */
+function waitForTick() {
+ let deferred = defer();
+ executeSoon(deferred.resolve);
+ return deferred.promise;
+}
+
+/**
+ * This shouldn't be used in the tests, but is useful when writing new tests or
+ * debugging existing tests in order to introduce delays in the test steps
+ *
+ * @param {Number} ms
+ * The time to wait
+ * @return A promise that resolves when the time is passed
+ */
+function wait(ms) {
+ return new promise(resolve => setTimeout(resolve, ms));
+}
+
+/**
+ * Open the toolbox in a given tab.
+ * @param {XULNode} tab The tab the toolbox should be opened in.
+ * @param {String} toolId Optional. The ID of the tool to be selected.
+ * @param {String} hostType Optional. The type of toolbox host to be used.
+ * @return {Promise} Resolves with the toolbox, when it has been opened.
+ */
+var openToolboxForTab = Task.async(function* (tab, toolId, hostType) {
+ info("Opening the toolbox");
+
+ let toolbox;
+ let target = TargetFactory.forTab(tab);
+ yield target.makeRemote();
+
+ // Check if the toolbox is already loaded.
+ toolbox = gDevTools.getToolbox(target);
+ if (toolbox) {
+ if (!toolId || (toolId && toolbox.getPanel(toolId))) {
+ info("Toolbox is already opened");
+ return toolbox;
+ }
+ }
+
+ // If not, load it now.
+ toolbox = yield gDevTools.showToolbox(target, toolId, hostType);
+
+ // Make sure that the toolbox frame is focused.
+ yield new Promise(resolve => waitForFocus(resolve, toolbox.win));
+
+ info("Toolbox opened and focused");
+
+ return toolbox;
+});
+
+/**
+ * Add a new tab and open the toolbox in it.
+ * @param {String} url The URL for the tab to be opened.
+ * @param {String} toolId Optional. The ID of the tool to be selected.
+ * @param {String} hostType Optional. The type of toolbox host to be used.
+ * @return {Promise} Resolves when the tab has been added, loaded and the
+ * toolbox has been opened. Resolves to the toolbox.
+ */
+var openNewTabAndToolbox = Task.async(function* (url, toolId, hostType) {
+ let tab = yield addTab(url);
+ return openToolboxForTab(tab, toolId, hostType);
+});
+
+/**
+ * Close a tab and if necessary, the toolbox that belongs to it
+ * @param {Tab} tab The tab to close.
+ * @return {Promise} Resolves when the toolbox and tab have been destroyed and
+ * closed.
+ */
+var closeTabAndToolbox = Task.async(function* (tab = gBrowser.selectedTab) {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ if (target) {
+ yield gDevTools.closeToolbox(target);
+ }
+
+ yield removeTab(gBrowser.selectedTab);
+});
+
+/**
+ * Close a toolbox and the current tab.
+ * @param {Toolbox} toolbox The toolbox to close.
+ * @return {Promise} Resolves when the toolbox and tab have been destroyed and
+ * closed.
+ */
+var closeToolboxAndTab = Task.async(function* (toolbox) {
+ yield toolbox.destroy();
+ yield removeTab(gBrowser.selectedTab);
+});
+
+/**
+ * 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, interval).then(() => resolve(true));
+ }, interval);
+ });
+}
+
+/**
+ * Takes a string `script` and evaluates it directly in the content
+ * in potentially a different process.
+ */
+let MM_INC_ID = 0;
+function evalInDebuggee(mm, script) {
+ return new Promise(function (resolve, reject) {
+ let id = MM_INC_ID++;
+ mm.sendAsyncMessage("devtools:test:eval", { script, id });
+ mm.addMessageListener("devtools:test:eval:response", handler);
+
+ function handler({ data }) {
+ if (id !== data.id) {
+ return;
+ }
+
+ info(`Successfully evaled in debuggee: ${script}`);
+ mm.removeMessageListener("devtools:test:eval:response", handler);
+ resolve(data.value);
+ }
+ });
+}
+
+/**
+ * Wait for a context menu popup to open.
+ *
+ * @param nsIDOMElement popup
+ * The XUL popup you expect to open.
+ * @param nsIDOMElement button
+ * The button/element that receives the contextmenu event. This is
+ * expected to open the popup.
+ * @param function onShown
+ * Function to invoke on popupshown event.
+ * @param function onHidden
+ * Function to invoke on popuphidden event.
+ * @return object
+ * A Promise object that is resolved after the popuphidden event
+ * callback is invoked.
+ */
+function waitForContextMenu(popup, button, onShown, onHidden) {
+ let deferred = defer();
+
+ function onPopupShown() {
+ info("onPopupShown");
+ popup.removeEventListener("popupshown", onPopupShown);
+
+ onShown && onShown();
+
+ // Use executeSoon() to get out of the popupshown event.
+ popup.addEventListener("popuphidden", onPopupHidden);
+ executeSoon(() => popup.hidePopup());
+ }
+ function onPopupHidden() {
+ info("onPopupHidden");
+ popup.removeEventListener("popuphidden", onPopupHidden);
+
+ onHidden && onHidden();
+
+ deferred.resolve(popup);
+ }
+
+ popup.addEventListener("popupshown", onPopupShown);
+
+ info("wait for the context menu to open");
+ button.scrollIntoView();
+ let eventDetails = {type: "contextmenu", button: 2};
+ EventUtils.synthesizeMouse(button, 5, 2, eventDetails,
+ button.ownerDocument.defaultView);
+ return deferred.promise;
+}
+
+/**
+ * Promise wrapper around SimpleTest.waitForClipboard
+ */
+function waitForClipboardPromise(setup, expected) {
+ return new Promise((resolve, reject) => {
+ SimpleTest.waitForClipboard(expected, setup, resolve, reject);
+ });
+}
+
+/**
+ * Simple helper to push a temporary preference. Wrapper on SpecialPowers
+ * pushPrefEnv that returns a promise resolving when the preferences have been
+ * updated.
+ *
+ * @param {String} preferenceName
+ * The name of the preference to updated
+ * @param {} value
+ * The preference value, type can vary
+ * @return {Promise} resolves when the preferences have been updated
+ */
+function pushPref(preferenceName, value) {
+ return new Promise(resolve => {
+ let options = {"set": [[preferenceName, value]]};
+ SpecialPowers.pushPrefEnv(options, resolve);
+ });
+}
+
+/**
+ * Lookup the provided dotted path ("prop1.subprop2.myProp") in the provided object.
+ *
+ * @param {Object} obj
+ * Object to expand.
+ * @param {String} path
+ * Dotted path to use to expand the object.
+ * @return {?} anything that is found at the provided path in the object.
+ */
+function lookupPath(obj, path) {
+ let segments = path.split(".");
+ return segments.reduce((prev, current) => prev[current], obj);
+}
+
+var closeToolbox = Task.async(function* () {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ yield gDevTools.closeToolbox(target);
+});
+
+/**
+ * Load the Telemetry utils, then stub Telemetry.prototype.log and
+ * Telemetry.prototype.logKeyed in order to record everything that's logged in
+ * it.
+ * Store all recordings in Telemetry.telemetryInfo.
+ * @return {Telemetry}
+ */
+function loadTelemetryAndRecordLogs() {
+ info("Mock the Telemetry log function to record logged information");
+
+ let Telemetry = require("devtools/client/shared/telemetry");
+ Telemetry.prototype.telemetryInfo = {};
+ Telemetry.prototype._oldlog = Telemetry.prototype.log;
+ Telemetry.prototype.log = function (histogramId, value) {
+ if (!this.telemetryInfo) {
+ // Telemetry instance still in use after stopRecordingTelemetryLogs
+ return;
+ }
+ if (histogramId) {
+ if (!this.telemetryInfo[histogramId]) {
+ this.telemetryInfo[histogramId] = [];
+ }
+ this.telemetryInfo[histogramId].push(value);
+ }
+ };
+ Telemetry.prototype._oldlogKeyed = Telemetry.prototype.logKeyed;
+ Telemetry.prototype.logKeyed = function (histogramId, key, value) {
+ this.log(`${histogramId}|${key}`, value);
+ };
+
+ return Telemetry;
+}
+
+/**
+ * Stop recording the Telemetry logs and put back the utils as it was before.
+ * @param {Telemetry} Required Telemetry
+ * Telemetry object that needs to be stopped.
+ */
+function stopRecordingTelemetryLogs(Telemetry) {
+ info("Stopping Telemetry");
+ Telemetry.prototype.log = Telemetry.prototype._oldlog;
+ Telemetry.prototype.logKeyed = Telemetry.prototype._oldlogKeyed;
+ delete Telemetry.prototype._oldlog;
+ delete Telemetry.prototype._oldlogKeyed;
+ delete Telemetry.prototype.telemetryInfo;
+}
+
+/**
+ * Clean the logical clipboard content. This method only clears the OS clipboard on
+ * Windows (see Bug 666254).
+ */
+function emptyClipboard() {
+ let clipboard = Cc["@mozilla.org/widget/clipboard;1"]
+ .getService(SpecialPowers.Ci.nsIClipboard);
+ clipboard.emptyClipboard(clipboard.kGlobalClipboard);
+}
+
+/**
+ * Check if the current operating system is Windows.
+ */
+function isWindows() {
+ return Services.appinfo.OS === "WINNT";
+}
+
+/**
+ * Wait for a given toolbox to get its title updated.
+ */
+function waitForTitleChange(toolbox) {
+ let deferred = defer();
+ toolbox.win.parent.addEventListener("message", function onmessage(event) {
+ if (event.data.name == "set-host-title") {
+ toolbox.win.parent.removeEventListener("message", onmessage);
+ deferred.resolve();
+ }
+ });
+ return deferred.promise;
+}
diff --git a/devtools/client/framework/test/shared-redux-head.js b/devtools/client/framework/test/shared-redux-head.js
new file mode 100644
index 000000000..c7c939152
--- /dev/null
+++ b/devtools/client/framework/test/shared-redux-head.js
@@ -0,0 +1,85 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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";
+
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+/* import-globals-from ./shared-head.js */
+// Currently this file expects "defer" to be imported into scope.
+
+// Common utility functions for working with Redux stores. The file is meant
+// to be safe to load in both mochitest and xpcshell environments.
+
+/**
+ * A logging function that can be used from xpcshell and browser mochitest
+ * environments.
+ */
+function commonLog(message) {
+ let log;
+ if (Services && Services.appinfo && Services.appinfo.name &&
+ Services.appinfo.name == "Firefox") {
+ log = info;
+ } else {
+ log = do_print;
+ }
+ log(message);
+}
+
+/**
+ * Wait until the store has reached a state that matches the predicate.
+ * @param Store store
+ * The Redux store being used.
+ * @param function predicate
+ * A function that returns true when the store has reached the expected
+ * state.
+ * @return Promise
+ * Resolved once the store reaches the expected state.
+ */
+function waitUntilState(store, predicate) {
+ let deferred = defer();
+ let unsubscribe = store.subscribe(check);
+
+ commonLog(`Waiting for state predicate "${predicate}"`);
+ function check() {
+ if (predicate(store.getState())) {
+ commonLog(`Found state predicate "${predicate}"`);
+ unsubscribe();
+ deferred.resolve();
+ }
+ }
+
+ // Fire the check immediately in case the action has already occurred
+ check();
+
+ return deferred.promise;
+}
+
+/**
+ * Wait until a particular action has been emitted by the store.
+ * @param Store store
+ * The Redux store being used.
+ * @param string actionType
+ * The expected action to wait for.
+ * @return Promise
+ * Resolved once the expected action is emitted by the store.
+ */
+function waitUntilAction(store, actionType) {
+ let deferred = defer();
+ let unsubscribe = store.subscribe(check);
+ let history = store.history;
+ let index = history.length;
+
+ commonLog(`Waiting for action "${actionType}"`);
+ function check() {
+ let action = history[index++];
+ if (action && action.type === actionType) {
+ commonLog(`Found action "${actionType}"`);
+ unsubscribe();
+ deferred.resolve(store.getState());
+ }
+ }
+
+ return deferred.promise;
+}
diff --git a/devtools/client/framework/toolbox-highlighter-utils.js b/devtools/client/framework/toolbox-highlighter-utils.js
new file mode 100644
index 000000000..e7f343857
--- /dev/null
+++ b/devtools/client/framework/toolbox-highlighter-utils.js
@@ -0,0 +1,324 @@
+/* -*- 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 {Task} = require("devtools/shared/task");
+const flags = require("devtools/shared/flags");
+
+/**
+ * Client-side highlighter shared module.
+ * To be used by toolbox panels that need to highlight DOM elements.
+ *
+ * Highlighting and selecting elements is common enough that it needs to be at
+ * toolbox level, accessible by any panel that needs it.
+ * That's why the toolbox is the one that initializes the inspector and
+ * highlighter. It's also why the API returned by this module needs a reference
+ * to the toolbox which should be set once only.
+ */
+
+/**
+ * Get the highighterUtils instance for a given toolbox.
+ * This should be done once only by the toolbox itself and stored there so that
+ * panels can get it from there. That's because the API returned has a stateful
+ * scope that would be different for another instance returned by this function.
+ *
+ * @param {Toolbox} toolbox
+ * @return {Object} the highlighterUtils public API
+ */
+exports.getHighlighterUtils = function (toolbox) {
+ if (!toolbox || !toolbox.target) {
+ throw new Error("Missing or invalid toolbox passed to getHighlighterUtils");
+ return;
+ }
+
+ // Exported API properties will go here
+ let exported = {};
+
+ // The current toolbox target
+ let target = toolbox.target;
+
+ // Is the highlighter currently in pick mode
+ let isPicking = false;
+
+ // Is the box model already displayed, used to prevent dispatching
+ // unnecessary requests, especially during toolbox shutdown
+ let isNodeFrontHighlighted = false;
+
+ /**
+ * Release this utils, nullifying the references to the toolbox
+ */
+ exported.release = function () {
+ toolbox = target = null;
+ };
+
+ /**
+ * Does the target have the highlighter actor.
+ * The devtools must be backwards compatible with at least B2G 1.3 (28),
+ * which doesn't have the highlighter actor. This can be removed as soon as
+ * the minimal supported version becomes 1.4 (29)
+ */
+ let isRemoteHighlightable = exported.isRemoteHighlightable = function () {
+ return target.client.traits.highlightable;
+ };
+
+ /**
+ * Does the target support custom highlighters.
+ */
+ let supportsCustomHighlighters = exported.supportsCustomHighlighters = () => {
+ return !!target.client.traits.customHighlighters;
+ };
+
+ /**
+ * Make a function that initializes the inspector before it runs.
+ * Since the init of the inspector is asynchronous, the return value will be
+ * produced by Task.async and the argument should be a generator
+ * @param {Function*} generator A generator function
+ * @return {Function} A function
+ */
+ let isInspectorInitialized = false;
+ let requireInspector = generator => {
+ return Task.async(function* (...args) {
+ if (!isInspectorInitialized) {
+ yield toolbox.initInspector();
+ isInspectorInitialized = true;
+ }
+ return yield generator.apply(null, args);
+ });
+ };
+
+ /**
+ * Start/stop the element picker on the debuggee target.
+ * @param {Boolean} doFocus - Optionally focus the content area once the picker is
+ * activated.
+ * @return A promise that resolves when done
+ */
+ let togglePicker = exported.togglePicker = function (doFocus) {
+ if (isPicking) {
+ return cancelPicker();
+ } else {
+ return startPicker(doFocus);
+ }
+ };
+
+ /**
+ * Start the element picker on the debuggee target.
+ * This will request the inspector actor to start listening for mouse events
+ * on the target page to highlight the hovered/picked element.
+ * Depending on the server-side capabilities, this may fire events when nodes
+ * are hovered.
+ * @param {Boolean} doFocus - Optionally focus the content area once the picker is
+ * activated.
+ * @return A promise that resolves when the picker has started or immediately
+ * if it is already started
+ */
+ let startPicker = exported.startPicker = requireInspector(function* (doFocus = false) {
+ if (isPicking) {
+ return;
+ }
+ isPicking = true;
+
+ toolbox.pickerButtonChecked = true;
+ yield toolbox.selectTool("inspector");
+ toolbox.on("select", cancelPicker);
+
+ if (isRemoteHighlightable()) {
+ toolbox.walker.on("picker-node-hovered", onPickerNodeHovered);
+ toolbox.walker.on("picker-node-picked", onPickerNodePicked);
+ toolbox.walker.on("picker-node-previewed", onPickerNodePreviewed);
+ toolbox.walker.on("picker-node-canceled", onPickerNodeCanceled);
+
+ yield toolbox.highlighter.pick(doFocus);
+ toolbox.emit("picker-started");
+ } else {
+ // If the target doesn't have the highlighter actor, we can use the
+ // walker's pick method instead, knowing that it only responds when a node
+ // is picked (instead of emitting events)
+ toolbox.emit("picker-started");
+ let node = yield toolbox.walker.pick();
+ onPickerNodePicked({node: node});
+ }
+ });
+
+ /**
+ * Stop the element picker. Note that the picker is automatically stopped when
+ * an element is picked
+ * @return A promise that resolves when the picker has stopped or immediately
+ * if it is already stopped
+ */
+ let stopPicker = exported.stopPicker = requireInspector(function* () {
+ if (!isPicking) {
+ return;
+ }
+ isPicking = false;
+
+ toolbox.pickerButtonChecked = false;
+
+ if (isRemoteHighlightable()) {
+ yield toolbox.highlighter.cancelPick();
+ toolbox.walker.off("picker-node-hovered", onPickerNodeHovered);
+ toolbox.walker.off("picker-node-picked", onPickerNodePicked);
+ toolbox.walker.off("picker-node-previewed", onPickerNodePreviewed);
+ toolbox.walker.off("picker-node-canceled", onPickerNodeCanceled);
+ } else {
+ // If the target doesn't have the highlighter actor, use the walker's
+ // cancelPick method instead
+ yield toolbox.walker.cancelPick();
+ }
+
+ toolbox.off("select", cancelPicker);
+ toolbox.emit("picker-stopped");
+ });
+
+ /**
+ * Stop the picker, but also emit an event that the picker was canceled.
+ */
+ let cancelPicker = exported.cancelPicker = Task.async(function* () {
+ yield stopPicker();
+ toolbox.emit("picker-canceled");
+ });
+
+ /**
+ * When a node is hovered by the mouse when the highlighter is in picker mode
+ * @param {Object} data Information about the node being hovered
+ */
+ function onPickerNodeHovered(data) {
+ toolbox.emit("picker-node-hovered", data.node);
+ }
+
+ /**
+ * When a node has been picked while the highlighter is in picker mode
+ * @param {Object} data Information about the picked node
+ */
+ function onPickerNodePicked(data) {
+ toolbox.selection.setNodeFront(data.node, "picker-node-picked");
+ stopPicker();
+ }
+
+ /**
+ * When a node has been shift-clicked (previewed) while the highlighter is in
+ * picker mode
+ * @param {Object} data Information about the picked node
+ */
+ function onPickerNodePreviewed(data) {
+ toolbox.selection.setNodeFront(data.node, "picker-node-previewed");
+ }
+
+ /**
+ * When the picker is canceled, stop the picker, and make sure the toolbox
+ * gets the focus.
+ */
+ function onPickerNodeCanceled() {
+ cancelPicker();
+ toolbox.win.focus();
+ }
+
+ /**
+ * Show the box model highlighter on a node in the content page.
+ * The node needs to be a NodeFront, as defined by the inspector actor
+ * @see devtools/server/actors/inspector.js
+ * @param {NodeFront} nodeFront The node to highlight
+ * @param {Object} options
+ * @return A promise that resolves when the node has been highlighted
+ */
+ let highlightNodeFront = exported.highlightNodeFront = requireInspector(
+ function* (nodeFront, options = {}) {
+ if (!nodeFront) {
+ return;
+ }
+
+ isNodeFrontHighlighted = true;
+ if (isRemoteHighlightable()) {
+ yield toolbox.highlighter.showBoxModel(nodeFront, options);
+ } else {
+ // If the target doesn't have the highlighter actor, revert to the
+ // walker's highlight method, which draws a simple outline
+ yield toolbox.walker.highlight(nodeFront);
+ }
+
+ toolbox.emit("node-highlight", nodeFront, options.toSource());
+ });
+
+ /**
+ * This is a convenience method in case you don't have a nodeFront but a
+ * valueGrip. This is often the case with VariablesView properties.
+ * This method will simply translate the grip into a nodeFront and call
+ * highlightNodeFront, so it has the same signature.
+ * @see highlightNodeFront
+ */
+ let highlightDomValueGrip = exported.highlightDomValueGrip = requireInspector(
+ function* (valueGrip, options = {}) {
+ let nodeFront = yield gripToNodeFront(valueGrip);
+ if (nodeFront) {
+ yield highlightNodeFront(nodeFront, options);
+ } else {
+ throw new Error("The ValueGrip passed could not be translated to a NodeFront");
+ }
+ });
+
+ /**
+ * Translate a debugger value grip into a node front usable by the inspector
+ * @param {ValueGrip}
+ * @return a promise that resolves to the node front when done
+ */
+ let gripToNodeFront = exported.gripToNodeFront = requireInspector(
+ function* (grip) {
+ return yield toolbox.walker.getNodeActorFromObjectActor(grip.actor);
+ });
+
+ /**
+ * Hide the highlighter.
+ * @param {Boolean} forceHide Only really matters in test mode (when
+ * flags.testing is true). In test mode, hovering over several nodes
+ * in the markup view doesn't hide/show the highlighter to ease testing. The
+ * highlighter stays visible at all times, except when the mouse leaves the
+ * markup view, which is when this param is passed to true
+ * @return a promise that resolves when the highlighter is hidden
+ */
+ let unhighlight = exported.unhighlight = Task.async(
+ function* (forceHide = false) {
+ forceHide = forceHide || !flags.testing;
+
+ // Note that if isRemoteHighlightable is true, there's no need to hide the
+ // highlighter as the walker uses setTimeout to hide it after some time
+ if (isNodeFrontHighlighted && forceHide && toolbox.highlighter && isRemoteHighlightable()) {
+ isNodeFrontHighlighted = false;
+ yield toolbox.highlighter.hideBoxModel();
+ }
+
+ // unhighlight is called when destroying the toolbox, which means that by
+ // now, the toolbox reference might have been nullified already.
+ if (toolbox) {
+ toolbox.emit("node-unhighlight");
+ }
+ });
+
+ /**
+ * If the main, box-model, highlighter isn't enough, or if multiple
+ * highlighters are needed in parallel, this method can be used to return a
+ * new instance of a highlighter actor, given a type.
+ * The type of the highlighter passed must be known by the server.
+ * The highlighter actor returned will have the show(nodeFront) and hide()
+ * methods and needs to be released by the consumer when not needed anymore.
+ * @return a promise that resolves to the highlighter
+ */
+ let getHighlighterByType = exported.getHighlighterByType = requireInspector(
+ function* (typeName) {
+ let highlighter = null;
+
+ if (supportsCustomHighlighters()) {
+ highlighter = yield toolbox.inspector.getHighlighterByType(typeName);
+ }
+
+ return highlighter || promise.reject("The target doesn't support " +
+ `creating highlighters by types or ${typeName} is unknown`);
+
+ });
+
+ // Return the public API
+ return exported;
+};
diff --git a/devtools/client/framework/toolbox-host-manager.js b/devtools/client/framework/toolbox-host-manager.js
new file mode 100644
index 000000000..1638f3a9a
--- /dev/null
+++ b/devtools/client/framework/toolbox-host-manager.js
@@ -0,0 +1,244 @@
+const Services = require("Services");
+const {Ci} = require("chrome");
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper("devtools/client/locales/toolbox.properties");
+const DevToolsUtils = require("devtools/shared/DevToolsUtils");
+const {Task} = require("devtools/shared/task");
+
+loader.lazyRequireGetter(this, "Toolbox", "devtools/client/framework/toolbox", true);
+loader.lazyRequireGetter(this, "Hosts", "devtools/client/framework/toolbox-hosts", true);
+
+/**
+ * Implement a wrapper on the chrome side to setup a Toolbox within Firefox UI.
+ *
+ * This component handles iframe creation within Firefox, in which we are loading
+ * the toolbox document. Then both the chrome and the toolbox document communicate
+ * via "message" events.
+ *
+ * Messages sent by the toolbox to the chrome:
+ * - switch-host:
+ * Order to display the toolbox in another host (side, bottom, window, or the
+ * previously used one)
+ * - toggle-minimize-mode:
+ * When using the bottom host, the toolbox can be miximized to only display
+ * the tool titles
+ * - maximize-host:
+ * When using the bottom host in minimized mode, revert back to regular mode
+ * in order to see tool titles and the tools
+ * - raise-host:
+ * Focus the tools
+ * - set-host-title:
+ * When using the window host, update the window title
+ *
+ * Messages sent by the chrome to the toolbox:
+ * - host-minimized:
+ * The bottom host is done minimizing (after animation end)
+ * - host-maximized:
+ * The bottom host is done switching back to regular mode (after animation
+ * end)
+ * - switched-host:
+ * The `switch-host` command sent by the toolbox is done
+ */
+
+const LAST_HOST = "devtools.toolbox.host";
+const PREVIOUS_HOST = "devtools.toolbox.previousHost";
+let ID_COUNTER = 1;
+
+function ToolboxHostManager(target, hostType, hostOptions) {
+ this.target = target;
+
+ this.frameId = ID_COUNTER++;
+
+ if (!hostType) {
+ hostType = Services.prefs.getCharPref(LAST_HOST);
+ }
+ this.onHostMinimized = this.onHostMinimized.bind(this);
+ this.onHostMaximized = this.onHostMaximized.bind(this);
+ this.host = this.createHost(hostType, hostOptions);
+ this.hostType = hostType;
+}
+
+ToolboxHostManager.prototype = {
+ create: Task.async(function* (toolId) {
+ yield this.host.create();
+
+ this.host.frame.setAttribute("aria-label", L10N.getStr("toolbox.label"));
+ this.host.frame.ownerDocument.defaultView.addEventListener("message", this);
+ // We have to listen on capture as no event fires on bubble
+ this.host.frame.addEventListener("unload", this, true);
+
+ let toolbox = new Toolbox(this.target, toolId, this.host.type, this.host.frame.contentWindow, this.frameId);
+
+ // Prevent reloading the toolbox when loading the tools in a tab (e.g. from about:debugging)
+ if (!this.host.frame.contentWindow.location.href.startsWith("about:devtools-toolbox")) {
+ this.host.frame.setAttribute("src", "about:devtools-toolbox");
+ }
+
+ return toolbox;
+ }),
+
+ handleEvent(event) {
+ switch(event.type) {
+ case "message":
+ this.onMessage(event);
+ break;
+ case "unload":
+ // On unload, host iframe already lost its contentWindow attribute, so
+ // we can only compare against locations. Here we filter two very
+ // different cases: preliminary about:blank document as well as iframes
+ // like tool iframes.
+ if (!event.target.location.href.startsWith("about:devtools-toolbox")) {
+ break;
+ }
+ // Don't destroy the host during unload event (esp., don't remove the
+ // iframe from DOM!). Otherwise the unload event for the toolbox
+ // document doesn't fire within the toolbox *document*! This is
+ // the unload event that fires on the toolbox *iframe*.
+ DevToolsUtils.executeSoon(() => {
+ this.destroy();
+ });
+ break;
+ }
+ },
+
+ onMessage(event) {
+ if (!event.data) {
+ return;
+ }
+ // Toolbox document is still chrome and disallow identifying message
+ // origin via event.source as it is null. So use a custom id.
+ if (event.data.frameId != this.frameId) {
+ return;
+ }
+ switch (event.data.name) {
+ case "switch-host":
+ this.switchHost(event.data.hostType);
+ break;
+ case "maximize-host":
+ this.host.maximize();
+ break;
+ case "raise-host":
+ this.host.raise();
+ break;
+ case "toggle-minimize-mode":
+ this.host.toggleMinimizeMode(event.data.toolbarHeight);
+ break;
+ case "set-host-title":
+ this.host.setTitle(event.data.title);
+ break;
+ }
+ },
+
+ postMessage(data) {
+ let window = this.host.frame.contentWindow;
+ window.postMessage(data, "*");
+ },
+
+ destroy() {
+ this.destroyHost();
+ this.host = null;
+ this.hostType = null;
+ this.target = null;
+ },
+
+ /**
+ * Create a host object based on the given host type.
+ *
+ * Warning: bottom and sidebar hosts require that the toolbox target provides
+ * a reference to the attached tab. Not all Targets have a tab property -
+ * make sure you correctly mix and match hosts and targets.
+ *
+ * @param {string} hostType
+ * The host type of the new host object
+ *
+ * @return {Host} host
+ * The created host object
+ */
+ createHost(hostType, options) {
+ if (!Hosts[hostType]) {
+ throw new Error("Unknown hostType: " + hostType);
+ }
+
+ let newHost = new Hosts[hostType](this.target.tab, options);
+ // Update the label and icon when the state changes.
+ newHost.on("minimized", this.onHostMinimized);
+ newHost.on("maximized", this.onHostMaximized);
+ return newHost;
+ },
+
+ onHostMinimized() {
+ this.postMessage({
+ name: "host-minimized"
+ });
+ },
+
+ onHostMaximized() {
+ this.postMessage({
+ name: "host-maximized"
+ });
+ },
+
+ switchHost: Task.async(function* (hostType) {
+ if (hostType == "previous") {
+ // Switch to the last used host for the toolbox UI.
+ // This is determined by the devtools.toolbox.previousHost pref.
+ hostType = Services.prefs.getCharPref(PREVIOUS_HOST);
+
+ // Handle the case where the previous host happens to match the current
+ // host. If so, switch to bottom if it's not already used, and side if not.
+ if (hostType === this.hostType) {
+ if (hostType === Toolbox.HostType.BOTTOM) {
+ hostType = Toolbox.HostType.SIDE;
+ } else {
+ hostType = Toolbox.HostType.BOTTOM;
+ }
+ }
+ }
+ let iframe = this.host.frame;
+ let newHost = this.createHost(hostType);
+ let newIframe = yield newHost.create();
+ // change toolbox document's parent to the new host
+ newIframe.swapFrameLoaders(iframe);
+
+ this.destroyHost();
+
+ if (this.hostType != Toolbox.HostType.CUSTOM) {
+ Services.prefs.setCharPref(PREVIOUS_HOST, this.hostType);
+ }
+
+ this.host = newHost;
+ this.hostType = hostType;
+ this.host.setTitle(this.host.frame.contentWindow.document.title);
+ this.host.frame.ownerDocument.defaultView.addEventListener("message", this);
+ this.host.frame.addEventListener("unload", this, true);
+
+ if (hostType != Toolbox.HostType.CUSTOM) {
+ Services.prefs.setCharPref(LAST_HOST, hostType);
+ }
+
+ // Tell the toolbox the host changed
+ this.postMessage({
+ name: "switched-host",
+ hostType
+ });
+ }),
+
+ /**
+ * Destroy the current host, and remove event listeners from its frame.
+ *
+ * @return {promise} to be resolved when the host is destroyed.
+ */
+ destroyHost() {
+ // When Firefox toplevel is closed, the frame may already be detached and
+ // the top level document gone
+ if (this.host.frame.ownerDocument.defaultView) {
+ this.host.frame.ownerDocument.defaultView.removeEventListener("message", this);
+ }
+ this.host.frame.removeEventListener("unload", this, true);
+
+ this.host.off("minimized", this.onHostMinimized);
+ this.host.off("maximized", this.onHostMaximized);
+ return this.host.destroy();
+ }
+};
+exports.ToolboxHostManager = ToolboxHostManager;
diff --git a/devtools/client/framework/toolbox-hosts.js b/devtools/client/framework/toolbox-hosts.js
new file mode 100644
index 000000000..ea774549a
--- /dev/null
+++ b/devtools/client/framework/toolbox-hosts.js
@@ -0,0 +1,425 @@
+/* -*- 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 EventEmitter = require("devtools/shared/event-emitter");
+const promise = require("promise");
+const defer = require("devtools/shared/defer");
+const Services = require("Services");
+const {DOMHelpers} = require("resource://devtools/client/shared/DOMHelpers.jsm");
+
+loader.lazyRequireGetter(this, "system", "devtools/shared/system");
+
+/* A host should always allow this much space for the page to be displayed.
+ * There is also a min-height on the browser, but we still don't want to set
+ * frame.height to be larger than that, since it can cause problems with
+ * resizing the toolbox and panel layout. */
+const MIN_PAGE_SIZE = 25;
+
+/**
+ * A toolbox host represents an object that contains a toolbox (e.g. the
+ * sidebar or a separate window). Any host object should implement the
+ * following functions:
+ *
+ * create() - create the UI and emit a 'ready' event when the UI is ready to use
+ * destroy() - destroy the host's UI
+ */
+
+exports.Hosts = {
+ "bottom": BottomHost,
+ "side": SidebarHost,
+ "window": WindowHost,
+ "custom": CustomHost
+};
+
+/**
+ * Host object for the dock on the bottom of the browser
+ */
+function BottomHost(hostTab) {
+ this.hostTab = hostTab;
+
+ EventEmitter.decorate(this);
+}
+
+BottomHost.prototype = {
+ type: "bottom",
+
+ heightPref: "devtools.toolbox.footer.height",
+
+ /**
+ * Create a box at the bottom of the host tab.
+ */
+ create: function () {
+ let deferred = defer();
+
+ let gBrowser = this.hostTab.ownerDocument.defaultView.gBrowser;
+ let ownerDocument = gBrowser.ownerDocument;
+ this._nbox = gBrowser.getNotificationBox(this.hostTab.linkedBrowser);
+
+ this._splitter = ownerDocument.createElement("splitter");
+ this._splitter.setAttribute("class", "devtools-horizontal-splitter");
+ // Avoid resizing notification containers
+ this._splitter.setAttribute("resizebefore", "flex");
+
+ this.frame = ownerDocument.createElement("iframe");
+ this.frame.className = "devtools-toolbox-bottom-iframe";
+ this.frame.height = Math.min(
+ Services.prefs.getIntPref(this.heightPref),
+ this._nbox.clientHeight - MIN_PAGE_SIZE
+ );
+
+ this._nbox.appendChild(this._splitter);
+ this._nbox.appendChild(this.frame);
+
+ let frameLoad = () => {
+ this.emit("ready", this.frame);
+ deferred.resolve(this.frame);
+ };
+
+ this.frame.tooltip = "aHTMLTooltip";
+
+ // we have to load something so we can switch documents if we have to
+ this.frame.setAttribute("src", "about:blank");
+
+ let domHelper = new DOMHelpers(this.frame.contentWindow);
+ domHelper.onceDOMReady(frameLoad);
+
+ focusTab(this.hostTab);
+
+ return deferred.promise;
+ },
+
+ /**
+ * Raise the host.
+ */
+ raise: function () {
+ focusTab(this.hostTab);
+ },
+
+ /**
+ * Minimize this host so that only the toolbox tabbar remains visible.
+ * @param {Number} height The height to minimize to. Defaults to 0, which
+ * means that the toolbox won't be visible at all once minimized.
+ */
+ minimize: function (height = 0) {
+ if (this.isMinimized) {
+ return;
+ }
+ this.isMinimized = true;
+
+ let onTransitionEnd = event => {
+ if (event.propertyName !== "margin-bottom") {
+ // Ignore transitionend on unrelated properties.
+ return;
+ }
+
+ this.frame.removeEventListener("transitionend", onTransitionEnd);
+ this.emit("minimized");
+ };
+ this.frame.addEventListener("transitionend", onTransitionEnd);
+ this.frame.style.marginBottom = -this.frame.height + height + "px";
+ this._splitter.classList.add("disabled");
+ },
+
+ /**
+ * If the host was minimized before, maximize it again (the host will be
+ * maximized to the height it previously had).
+ */
+ maximize: function () {
+ if (!this.isMinimized) {
+ return;
+ }
+ this.isMinimized = false;
+
+ let onTransitionEnd = event => {
+ if (event.propertyName !== "margin-bottom") {
+ // Ignore transitionend on unrelated properties.
+ return;
+ }
+
+ this.frame.removeEventListener("transitionend", onTransitionEnd);
+ this.emit("maximized");
+ };
+ this.frame.addEventListener("transitionend", onTransitionEnd);
+ this.frame.style.marginBottom = "0";
+ this._splitter.classList.remove("disabled");
+ },
+
+ /**
+ * Toggle the minimize mode.
+ * @param {Number} minHeight The height to minimize to.
+ */
+ toggleMinimizeMode: function (minHeight) {
+ this.isMinimized ? this.maximize() : this.minimize(minHeight);
+ },
+
+ /**
+ * Set the toolbox title.
+ * Nothing to do for this host type.
+ */
+ setTitle: function () {},
+
+ /**
+ * Destroy the bottom dock.
+ */
+ destroy: function () {
+ if (!this._destroyed) {
+ this._destroyed = true;
+
+ Services.prefs.setIntPref(this.heightPref, this.frame.height);
+ this._nbox.removeChild(this._splitter);
+ this._nbox.removeChild(this.frame);
+ this.frame = null;
+ this._nbox = null;
+ this._splitter = null;
+ }
+
+ return promise.resolve(null);
+ }
+};
+
+/**
+ * Host object for the in-browser sidebar
+ */
+function SidebarHost(hostTab) {
+ this.hostTab = hostTab;
+
+ EventEmitter.decorate(this);
+}
+
+SidebarHost.prototype = {
+ type: "side",
+
+ widthPref: "devtools.toolbox.sidebar.width",
+
+ /**
+ * Create a box in the sidebar of the host tab.
+ */
+ create: function () {
+ let deferred = defer();
+
+ let gBrowser = this.hostTab.ownerDocument.defaultView.gBrowser;
+ let ownerDocument = gBrowser.ownerDocument;
+ this._sidebar = gBrowser.getSidebarContainer(this.hostTab.linkedBrowser);
+
+ this._splitter = ownerDocument.createElement("splitter");
+ this._splitter.setAttribute("class", "devtools-side-splitter");
+
+ this.frame = ownerDocument.createElement("iframe");
+ this.frame.className = "devtools-toolbox-side-iframe";
+
+ this.frame.width = Math.min(
+ Services.prefs.getIntPref(this.widthPref),
+ this._sidebar.clientWidth - MIN_PAGE_SIZE
+ );
+
+ this._sidebar.appendChild(this._splitter);
+ this._sidebar.appendChild(this.frame);
+
+ let frameLoad = () => {
+ this.emit("ready", this.frame);
+ deferred.resolve(this.frame);
+ };
+
+ this.frame.tooltip = "aHTMLTooltip";
+ this.frame.setAttribute("src", "about:blank");
+
+ let domHelper = new DOMHelpers(this.frame.contentWindow);
+ domHelper.onceDOMReady(frameLoad);
+
+ focusTab(this.hostTab);
+
+ return deferred.promise;
+ },
+
+ /**
+ * Raise the host.
+ */
+ raise: function () {
+ focusTab(this.hostTab);
+ },
+
+ /**
+ * Set the toolbox title.
+ * Nothing to do for this host type.
+ */
+ setTitle: function () {},
+
+ /**
+ * Destroy the sidebar.
+ */
+ destroy: function () {
+ if (!this._destroyed) {
+ this._destroyed = true;
+
+ Services.prefs.setIntPref(this.widthPref, this.frame.width);
+ this._sidebar.removeChild(this._splitter);
+ this._sidebar.removeChild(this.frame);
+ }
+
+ return promise.resolve(null);
+ }
+};
+
+/**
+ * Host object for the toolbox in a separate window
+ */
+function WindowHost() {
+ this._boundUnload = this._boundUnload.bind(this);
+
+ EventEmitter.decorate(this);
+}
+
+WindowHost.prototype = {
+ type: "window",
+
+ WINDOW_URL: "chrome://devtools/content/framework/toolbox-window.xul",
+
+ /**
+ * Create a new xul window to contain the toolbox.
+ */
+ create: function () {
+ let deferred = defer();
+
+ let flags = "chrome,centerscreen,resizable,dialog=no";
+ let win = Services.ww.openWindow(null, this.WINDOW_URL, "_blank",
+ flags, null);
+
+ let frameLoad = () => {
+ win.removeEventListener("load", frameLoad, true);
+ win.focus();
+
+ let key;
+ if (system.constants.platform === "macosx") {
+ key = win.document.getElementById("toolbox-key-toggle-osx");
+ } else {
+ key = win.document.getElementById("toolbox-key-toggle");
+ }
+ key.removeAttribute("disabled");
+
+ this.frame = win.document.getElementById("toolbox-iframe");
+ this.emit("ready", this.frame);
+
+ deferred.resolve(this.frame);
+ };
+
+ win.addEventListener("load", frameLoad, true);
+ win.addEventListener("unload", this._boundUnload);
+
+ this._window = win;
+
+ return deferred.promise;
+ },
+
+ /**
+ * Catch the user closing the window.
+ */
+ _boundUnload: function (event) {
+ if (event.target.location != this.WINDOW_URL) {
+ return;
+ }
+ this._window.removeEventListener("unload", this._boundUnload);
+
+ this.emit("window-closed");
+ },
+
+ /**
+ * Raise the host.
+ */
+ raise: function () {
+ this._window.focus();
+ },
+
+ /**
+ * Set the toolbox title.
+ */
+ setTitle: function (title) {
+ this._window.document.title = title;
+ },
+
+ /**
+ * Destroy the window.
+ */
+ destroy: function () {
+ if (!this._destroyed) {
+ this._destroyed = true;
+
+ this._window.removeEventListener("unload", this._boundUnload);
+ this._window.close();
+ }
+
+ return promise.resolve(null);
+ }
+};
+
+/**
+ * Host object for the toolbox in its own tab
+ */
+function CustomHost(hostTab, options) {
+ this.frame = options.customIframe;
+ this.uid = options.uid;
+ EventEmitter.decorate(this);
+}
+
+CustomHost.prototype = {
+ type: "custom",
+
+ _sendMessageToTopWindow: function (msg, data) {
+ // It's up to the custom frame owner (parent window) to honor
+ // "close" or "raise" instructions.
+ let topWindow = this.frame.ownerDocument.defaultView;
+ if (!topWindow) {
+ return;
+ }
+ let json = {name: "toolbox-" + msg, uid: this.uid};
+ if (data) {
+ json.data = data;
+ }
+ topWindow.postMessage(JSON.stringify(json), "*");
+ },
+
+ /**
+ * Create a new xul window to contain the toolbox.
+ */
+ create: function () {
+ return promise.resolve(this.frame);
+ },
+
+ /**
+ * Raise the host.
+ */
+ raise: function () {
+ this._sendMessageToTopWindow("raise");
+ },
+
+ /**
+ * Set the toolbox title.
+ */
+ setTitle: function (title) {
+ this._sendMessageToTopWindow("title", { value: title });
+ },
+
+ /**
+ * Destroy the window.
+ */
+ destroy: function () {
+ if (!this._destroyed) {
+ this._destroyed = true;
+ this._sendMessageToTopWindow("close");
+ }
+ return promise.resolve(null);
+ }
+};
+
+/**
+ * Switch to the given tab in a browser and focus the browser window
+ */
+function focusTab(tab) {
+ let browserWindow = tab.ownerDocument.defaultView;
+ browserWindow.focus();
+ browserWindow.gBrowser.selectedTab = tab;
+}
diff --git a/devtools/client/framework/toolbox-init.js b/devtools/client/framework/toolbox-init.js
new file mode 100644
index 000000000..cb041c22d
--- /dev/null
+++ b/devtools/client/framework/toolbox-init.js
@@ -0,0 +1,73 @@
+/* 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";
+
+// URL constructor doesn't support about: scheme
+let href = window.location.href.replace("about:", "http://");
+let url = new window.URL(href);
+
+// Only use this method to attach the toolbox if some query parameters are given
+if (url.search.length > 1) {
+ const Cu = Components.utils;
+ const Ci = Components.interfaces;
+ const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+ const { gDevTools } = require("devtools/client/framework/devtools");
+ const { targetFromURL } = require("devtools/client/framework/target-from-url");
+ const { Toolbox } = require("devtools/client/framework/toolbox");
+ const { TargetFactory } = require("devtools/client/framework/target");
+ const { DebuggerServer } = require("devtools/server/main");
+ const { DebuggerClient } = require("devtools/shared/client/main");
+ const { Task } = require("devtools/shared/task");
+
+ // `host` is the frame element loading the toolbox.
+ let host = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils)
+ .containerElement;
+
+ // Specify the default tool to open
+ let tool = url.searchParams.get("tool");
+
+ Task.spawn(function* () {
+ let target;
+ if (url.searchParams.has("target")) {
+ // Attach toolbox to a given browser iframe (<xul:browser> or <html:iframe
+ // mozbrowser>) whose reference is set on the host iframe.
+
+ // `iframe` is the targeted document to debug
+ let iframe = host.wrappedJSObject ? host.wrappedJSObject.target
+ : host.target;
+ // Need to use a xray and query some interfaces to have
+ // attributes and behavior expected by devtools codebase
+ iframe = XPCNativeWrapper(iframe);
+ iframe.QueryInterface(Ci.nsIFrameLoaderOwner);
+
+ if (iframe) {
+ // Fake a xul:tab object as we don't have one.
+ // linkedBrowser is the only one attribute being queried by client.getTab
+ let tab = { linkedBrowser: iframe };
+
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+ let client = new DebuggerClient(DebuggerServer.connectPipe());
+
+ yield client.connect();
+ // Creates a target for a given browser iframe.
+ let response = yield client.getTab({ tab });
+ let form = response.tab;
+ target = yield TargetFactory.forRemoteTab({client, form, chrome: false});
+ } else {
+ alert("Unable to find the targetted iframe to debug");
+ }
+ } else {
+ target = yield targetFromURL(url);
+ }
+ let options = { customIframe: host };
+ yield gDevTools.showToolbox(target, tool, Toolbox.HostType.CUSTOM, options);
+ }).catch(error => {
+ console.error("Exception while loading the toolbox", error);
+ });
+}
diff --git a/devtools/client/framework/toolbox-options.js b/devtools/client/framework/toolbox-options.js
new file mode 100644
index 000000000..6362d98dd
--- /dev/null
+++ b/devtools/client/framework/toolbox-options.js
@@ -0,0 +1,431 @@
+/* -*- 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 Services = require("Services");
+const defer = require("devtools/shared/defer");
+const {Task} = require("devtools/shared/task");
+const {gDevTools} = require("devtools/client/framework/devtools");
+
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper("devtools/client/locales/toolbox.properties");
+
+exports.OptionsPanel = OptionsPanel;
+
+function GetPref(name) {
+ let type = Services.prefs.getPrefType(name);
+ switch (type) {
+ case Services.prefs.PREF_STRING:
+ return Services.prefs.getCharPref(name);
+ case Services.prefs.PREF_INT:
+ return Services.prefs.getIntPref(name);
+ case Services.prefs.PREF_BOOL:
+ return Services.prefs.getBoolPref(name);
+ default:
+ throw new Error("Unknown type");
+ }
+}
+
+function SetPref(name, value) {
+ let type = Services.prefs.getPrefType(name);
+ switch (type) {
+ case Services.prefs.PREF_STRING:
+ return Services.prefs.setCharPref(name, value);
+ case Services.prefs.PREF_INT:
+ return Services.prefs.setIntPref(name, value);
+ case Services.prefs.PREF_BOOL:
+ return Services.prefs.setBoolPref(name, value);
+ default:
+ throw new Error("Unknown type");
+ }
+}
+
+function InfallibleGetBoolPref(key) {
+ try {
+ return Services.prefs.getBoolPref(key);
+ } catch (ex) {
+ return true;
+ }
+}
+
+/**
+ * Represents the Options Panel in the Toolbox.
+ */
+function OptionsPanel(iframeWindow, toolbox) {
+ this.panelDoc = iframeWindow.document;
+ this.panelWin = iframeWindow;
+
+ this.toolbox = toolbox;
+ this.isReady = false;
+
+ this._prefChanged = this._prefChanged.bind(this);
+ this._themeRegistered = this._themeRegistered.bind(this);
+ this._themeUnregistered = this._themeUnregistered.bind(this);
+ this._disableJSClicked = this._disableJSClicked.bind(this);
+
+ this.disableJSNode = this.panelDoc.getElementById(
+ "devtools-disable-javascript");
+
+ this._addListeners();
+
+ const EventEmitter = require("devtools/shared/event-emitter");
+ EventEmitter.decorate(this);
+}
+
+OptionsPanel.prototype = {
+
+ get target() {
+ return this.toolbox.target;
+ },
+
+ open: Task.async(function* () {
+ // For local debugging we need to make the target remote.
+ if (!this.target.isRemote) {
+ yield this.target.makeRemote();
+ }
+
+ this.setupToolsList();
+ this.setupToolbarButtonsList();
+ this.setupThemeList();
+ yield this.populatePreferences();
+ this.isReady = true;
+ this.emit("ready");
+ return this;
+ }),
+
+ _addListeners: function () {
+ Services.prefs.addObserver("devtools.cache.disabled", this._prefChanged, false);
+ Services.prefs.addObserver("devtools.theme", this._prefChanged, false);
+ gDevTools.on("theme-registered", this._themeRegistered);
+ gDevTools.on("theme-unregistered", this._themeUnregistered);
+ },
+
+ _removeListeners: function () {
+ Services.prefs.removeObserver("devtools.cache.disabled", this._prefChanged);
+ Services.prefs.removeObserver("devtools.theme", this._prefChanged);
+ gDevTools.off("theme-registered", this._themeRegistered);
+ gDevTools.off("theme-unregistered", this._themeUnregistered);
+ },
+
+ _prefChanged: function (subject, topic, prefName) {
+ if (prefName === "devtools.cache.disabled") {
+ let cacheDisabled = data.newValue;
+ let cbx = this.panelDoc.getElementById("devtools-disable-cache");
+
+ cbx.checked = cacheDisabled;
+ } else if (prefName === "devtools.theme") {
+ this.updateCurrentTheme();
+ }
+ },
+
+ _themeRegistered: function (event, themeId) {
+ this.setupThemeList();
+ },
+
+ _themeUnregistered: function (event, theme) {
+ let themeBox = this.panelDoc.getElementById("devtools-theme-box");
+ let themeInput = themeBox.querySelector(`[value=${theme.id}]`);
+
+ if (themeInput) {
+ themeInput.parentNode.remove();
+ }
+ },
+
+ setupToolbarButtonsList: function () {
+ let enabledToolbarButtonsBox = this.panelDoc.getElementById(
+ "enabled-toolbox-buttons-box");
+
+ let toggleableButtons = this.toolbox.toolboxButtons;
+ let setToolboxButtonsVisibility =
+ this.toolbox.setToolboxButtonsVisibility.bind(this.toolbox);
+
+ let onCheckboxClick = (checkbox) => {
+ let toolDefinition = toggleableButtons.filter(
+ toggleableButton => toggleableButton.id === checkbox.id)[0];
+ Services.prefs.setBoolPref(
+ toolDefinition.visibilityswitch, checkbox.checked);
+ setToolboxButtonsVisibility();
+ };
+
+ let createCommandCheckbox = tool => {
+ let checkboxLabel = this.panelDoc.createElement("label");
+ let checkboxSpanLabel = this.panelDoc.createElement("span");
+ checkboxSpanLabel.textContent = tool.label;
+ let checkboxInput = this.panelDoc.createElement("input");
+ checkboxInput.setAttribute("type", "checkbox");
+ checkboxInput.setAttribute("id", tool.id);
+ if (InfallibleGetBoolPref(tool.visibilityswitch)) {
+ checkboxInput.setAttribute("checked", true);
+ }
+ checkboxInput.addEventListener("change",
+ onCheckboxClick.bind(this, checkboxInput));
+
+ checkboxLabel.appendChild(checkboxInput);
+ checkboxLabel.appendChild(checkboxSpanLabel);
+ return checkboxLabel;
+ };
+
+ for (let tool of toggleableButtons) {
+ if (!tool.isTargetSupported(this.toolbox.target)) {
+ continue;
+ }
+
+ enabledToolbarButtonsBox.appendChild(createCommandCheckbox(tool));
+ }
+ },
+
+ setupToolsList: function () {
+ let defaultToolsBox = this.panelDoc.getElementById("default-tools-box");
+ let additionalToolsBox = this.panelDoc.getElementById(
+ "additional-tools-box");
+ let toolsNotSupportedLabel = this.panelDoc.getElementById(
+ "tools-not-supported-label");
+ let atleastOneToolNotSupported = false;
+
+ let onCheckboxClick = function (id) {
+ let toolDefinition = gDevTools._tools.get(id);
+ // Set the kill switch pref boolean to true
+ Services.prefs.setBoolPref(toolDefinition.visibilityswitch, this.checked);
+ if (this.checked) {
+ gDevTools.emit("tool-registered", id);
+ } else {
+ gDevTools.emit("tool-unregistered", toolDefinition);
+ }
+ };
+
+ let createToolCheckbox = tool => {
+ let checkboxLabel = this.panelDoc.createElement("label");
+ let checkboxInput = this.panelDoc.createElement("input");
+ checkboxInput.setAttribute("type", "checkbox");
+ checkboxInput.setAttribute("id", tool.id);
+ checkboxInput.setAttribute("title", tool.tooltip || "");
+
+ let checkboxSpanLabel = this.panelDoc.createElement("span");
+ if (tool.isTargetSupported(this.target)) {
+ checkboxSpanLabel.textContent = tool.label;
+ } else {
+ atleastOneToolNotSupported = true;
+ checkboxSpanLabel.textContent =
+ L10N.getFormatStr("options.toolNotSupportedMarker", tool.label);
+ checkboxInput.setAttribute("data-unsupported", "true");
+ checkboxInput.setAttribute("disabled", "true");
+ }
+
+ if (InfallibleGetBoolPref(tool.visibilityswitch)) {
+ checkboxInput.setAttribute("checked", "true");
+ }
+
+ checkboxInput.addEventListener("change",
+ onCheckboxClick.bind(checkboxInput, tool.id));
+
+ checkboxLabel.appendChild(checkboxInput);
+ checkboxLabel.appendChild(checkboxSpanLabel);
+ return checkboxLabel;
+ };
+
+ // Populating the default tools lists
+ let toggleableTools = gDevTools.getDefaultTools().filter(tool => {
+ return tool.visibilityswitch && !tool.hiddenInOptions;
+ });
+
+ for (let tool of toggleableTools) {
+ defaultToolsBox.appendChild(createToolCheckbox(tool));
+ }
+
+ // Populating the additional tools list that came from add-ons.
+ let atleastOneAddon = false;
+ for (let tool of gDevTools.getAdditionalTools()) {
+ atleastOneAddon = true;
+ additionalToolsBox.appendChild(createToolCheckbox(tool));
+ }
+
+ if (!atleastOneAddon) {
+ additionalToolsBox.style.display = "none";
+ }
+
+ if (!atleastOneToolNotSupported) {
+ toolsNotSupportedLabel.style.display = "none";
+ }
+
+ this.panelWin.focus();
+ },
+
+ setupThemeList: function () {
+ let themeBox = this.panelDoc.getElementById("devtools-theme-box");
+ let themeLabels = themeBox.querySelectorAll("label");
+ for (let label of themeLabels) {
+ label.remove();
+ }
+
+ let createThemeOption = theme => {
+ let inputLabel = this.panelDoc.createElement("label");
+ let inputRadio = this.panelDoc.createElement("input");
+ inputRadio.setAttribute("type", "radio");
+ inputRadio.setAttribute("value", theme.id);
+ inputRadio.setAttribute("name", "devtools-theme-item");
+ inputRadio.addEventListener("change", function (e) {
+ setPrefAndEmit(themeBox.getAttribute("data-pref"),
+ e.target.value);
+ });
+
+ let inputSpanLabel = this.panelDoc.createElement("span");
+ inputSpanLabel.textContent = theme.label;
+ inputLabel.appendChild(inputRadio);
+ inputLabel.appendChild(inputSpanLabel);
+
+ return inputLabel;
+ };
+
+ // Populating the default theme list
+ let themes = gDevTools.getThemeDefinitionArray();
+ for (let theme of themes) {
+ themeBox.appendChild(createThemeOption(theme));
+ }
+
+ this.updateCurrentTheme();
+ },
+
+ populatePreferences: function () {
+ let prefCheckboxes = this.panelDoc.querySelectorAll(
+ "input[type=checkbox][data-pref]");
+ for (let prefCheckbox of prefCheckboxes) {
+ if (GetPref(prefCheckbox.getAttribute("data-pref"))) {
+ prefCheckbox.setAttribute("checked", true);
+ }
+ prefCheckbox.addEventListener("change", function (e) {
+ let checkbox = e.target;
+ setPrefAndEmit(checkbox.getAttribute("data-pref"), checkbox.checked);
+ });
+ }
+ // Themes radio inputs are handled in setupThemeList
+ let prefRadiogroups = this.panelDoc.querySelectorAll(
+ ".radiogroup[data-pref]:not(#devtools-theme-box)");
+ for (let radioGroup of prefRadiogroups) {
+ let selectedValue = GetPref(radioGroup.getAttribute("data-pref"));
+
+ for (let radioInput of radioGroup.querySelectorAll("input[type=radio]")) {
+ if (radioInput.getAttribute("value") == selectedValue) {
+ radioInput.setAttribute("checked", true);
+ }
+
+ radioInput.addEventListener("change", function (e) {
+ setPrefAndEmit(radioGroup.getAttribute("data-pref"),
+ e.target.value);
+ });
+ }
+ }
+ let prefSelects = this.panelDoc.querySelectorAll("select[data-pref]");
+ for (let prefSelect of prefSelects) {
+ let pref = GetPref(prefSelect.getAttribute("data-pref"));
+ let options = [...prefSelect.options];
+ options.some(function (option) {
+ let value = option.value;
+ // non strict check to allow int values.
+ if (value == pref) {
+ prefSelect.selectedIndex = options.indexOf(option);
+ return true;
+ }
+ });
+
+ prefSelect.addEventListener("change", function (e) {
+ let select = e.target;
+ setPrefAndEmit(select.getAttribute("data-pref"),
+ select.options[select.selectedIndex].value);
+ });
+ }
+
+ if (this.target.activeTab) {
+ return this.target.client.attachTab(this.target.activeTab._actor)
+ .then(([response, client]) => {
+ this._origJavascriptEnabled = !response.javascriptEnabled;
+ this.disableJSNode.checked = this._origJavascriptEnabled;
+ this.disableJSNode.addEventListener("click",
+ this._disableJSClicked, false);
+ });
+ }
+ this.disableJSNode.hidden = true;
+ },
+
+ updateCurrentTheme: function () {
+ let currentTheme = GetPref("devtools.theme");
+ let themeBox = this.panelDoc.getElementById("devtools-theme-box");
+ let themeRadioInput = themeBox.querySelector(`[value=${currentTheme}]`);
+
+ if (themeRadioInput) {
+ themeRadioInput.checked = true;
+ } else {
+ // If the current theme does not exist anymore, switch to light theme
+ let lightThemeInputRadio = themeBox.querySelector("[value=light]");
+ lightThemeInputRadio.checked = true;
+ }
+ },
+
+ /**
+ * Disables JavaScript for the currently loaded tab. We force a page refresh
+ * here because setting docShell.allowJavascript to true fails to block JS
+ * execution from event listeners added using addEventListener(), AJAX calls
+ * and timers. The page refresh prevents these things from being added in the
+ * first place.
+ *
+ * @param {Event} event
+ * The event sent by checking / unchecking the disable JS checkbox.
+ */
+ _disableJSClicked: function (event) {
+ let checked = event.target.checked;
+
+ let options = {
+ "javascriptEnabled": !checked
+ };
+
+ this.target.activeTab.reconfigure(options);
+ },
+
+ destroy: function () {
+ if (this.destroyPromise) {
+ return this.destroyPromise;
+ }
+
+ let deferred = defer();
+ this.destroyPromise = deferred.promise;
+
+ this._removeListeners();
+
+ if (this.target.activeTab) {
+ this.disableJSNode.removeEventListener("click", this._disableJSClicked);
+ // FF41+ automatically cleans up state in actor on disconnect
+ if (!this.target.activeTab.traits.noTabReconfigureOnClose) {
+ let options = {
+ "javascriptEnabled": this._origJavascriptEnabled,
+ "performReload": false
+ };
+ this.target.activeTab.reconfigure(options, deferred.resolve);
+ } else {
+ deferred.resolve();
+ }
+ } else {
+ deferred.resolve();
+ }
+
+ this.panelWin = this.panelDoc = this.disableJSNode = this.toolbox = null;
+
+ return this.destroyPromise;
+ }
+};
+
+/* Set a pref and emit the pref-changed event if needed. */
+function setPrefAndEmit(prefName, newValue) {
+ let data = {
+ pref: prefName,
+ newValue: newValue
+ };
+ data.oldValue = GetPref(data.pref);
+ SetPref(data.pref, data.newValue);
+
+ if (data.newValue != data.oldValue) {
+ gDevTools.emit("pref-changed", data);
+ }
+}
diff --git a/devtools/client/framework/toolbox-options.xhtml b/devtools/client/framework/toolbox-options.xhtml
new file mode 100644
index 000000000..372a588ab
--- /dev/null
+++ b/devtools/client/framework/toolbox-options.xhtml
@@ -0,0 +1,201 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+<!DOCTYPE html [
+<!ENTITY % toolboxDTD SYSTEM "chrome://devtools/locale/toolbox.dtd" >
+ %toolboxDTD;
+]>
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <title>Toolbox option</title>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <link rel="stylesheet" href="chrome://devtools/content/framework/options-panel.css" type="text/css"/>
+ <script type="application/javascript;version=1.8" src="chrome://devtools/content/shared/theme-switching.js"/>
+ </head>
+ <body role="application" class="theme-body">
+ <form id="options-panel">
+ <div id="tools-box" class="options-vertical-pane">
+ <fieldset id="default-tools-box" class="options-groupbox">
+ <legend>&options.selectDefaultTools.label2;</legend>
+ </fieldset>
+
+ <fieldset id="additional-tools-box" class="options-groupbox">
+ <legend>&options.selectAdditionalTools.label;</legend>
+ </fieldset>
+
+ <fieldset id="enabled-toolbox-buttons-box" class="options-groupbox">
+ <legend>&options.selectEnabledToolboxButtons.label;</legend>
+ <span id="tools-not-supported-label"
+ class="options-citation-label theme-comment">
+ &options.toolNotSupported.label;</span>
+ </fieldset>
+ </div>
+
+ <div class="options-vertical-pane">
+ <fieldset id="devtools-theme-box"
+ class="options-groupbox
+ horizontal-options-groupbox
+ radiogroup"
+ data-pref="devtools.theme">
+ <legend>&options.selectDevToolsTheme.label2;</legend>
+ </fieldset>
+
+ <fieldset id="commonprefs-options" class="options-groupbox">
+ <legend>&options.commonPrefs.label;</legend>
+ <label title="&options.enablePersistentLogs.tooltip;">
+ <input type="checkbox" data-pref="devtools.webconsole.persistlog" />
+ <span>&options.enablePersistentLogs.label;</span>
+ </label>
+ </fieldset>
+
+ <fieldset id="inspector-options" class="options-groupbox">
+ <legend>&options.context.inspector;</legend>
+ <label title="&options.showUserAgentStyles.tooltip;">
+ <input type="checkbox"
+ data-pref="devtools.inspector.showUserAgentStyles"/>
+ <span>&options.showUserAgentStyles.label;</span>
+ </label>
+ <label title="&options.collapseAttrs.tooltip;">
+ <input type="checkbox"
+ data-pref="devtools.markup.collapseAttributes"/>
+ <span>&options.collapseAttrs.label;</span>
+ </label>
+ <label>
+ <span>&options.defaultColorUnit.label;</span>
+ <select id="defaultColorUnitMenuList"
+ data-pref="devtools.defaultColorUnit">
+ <option value="authored">&options.defaultColorUnit.authored;</option>
+ <option value="hex">&options.defaultColorUnit.hex;</option>
+ <option value="hsl">&options.defaultColorUnit.hsl;</option>
+ <option value="rgb">&options.defaultColorUnit.rgb;</option>
+ <option value="name">&options.defaultColorUnit.name;</option>
+ </select>
+ </label>
+ </fieldset>
+
+ <fieldset id="webconsole-options" class="options-groupbox">
+ <legend>&options.webconsole.label;</legend>
+ <label title="&options.timestampMessages.tooltip;">
+ <input type="checkbox"
+ id="webconsole-timestamp-messages"
+ data-pref="devtools.webconsole.timestampMessages"/>
+ <span>&options.timestampMessages.label;</span>
+ </label>
+ </fieldset>
+
+ <fieldset id="debugger-options" class="options-groupbox">
+ <legend>&options.debugger.label;</legend>
+ <label title="&options.sourceMaps.tooltip;">
+ <input type="checkbox"
+ id="debugger-sourcemaps"
+ data-pref="devtools.debugger.client-source-maps-enabled"/>
+ <span>&options.sourceMaps.label;</span>
+ </label>
+ </fieldset>
+
+ <fieldset id="styleeditor-options" class="options-groupbox">
+ <legend>&options.styleeditor.label;</legend>
+ <label title="&options.stylesheetSourceMaps.tooltip;">
+ <input type="checkbox"
+ data-pref="devtools.styleeditor.source-maps-enabled"/>
+ <span>&options.stylesheetSourceMaps.label;</span>
+ </label>
+ <label title="&options.stylesheetAutocompletion.tooltip;">
+ <input type="checkbox"
+ data-pref="devtools.styleeditor.autocompletion-enabled"/>
+ <span>&options.stylesheetAutocompletion.label;</span>
+ </label>
+ </fieldset>
+ </div>
+
+ <div class="options-vertical-pane">
+ <fieldset id="sourceeditor-options" class="options-groupbox">
+ <legend>&options.sourceeditor.label;</legend>
+ <label title="&options.sourceeditor.detectindentation.tooltip;">
+ <input type="checkbox"
+ id="devtools-sourceeditor-detectindentation"
+ data-pref="devtools.editor.detectindentation"/>
+ <span>&options.sourceeditor.detectindentation.label;</span>
+ </label>
+ <label title="&options.sourceeditor.autoclosebrackets.tooltip;">
+ <input type="checkbox"
+ id="devtools-sourceeditor-autoclosebrackets"
+ data-pref="devtools.editor.autoclosebrackets"/>
+ <span>&options.sourceeditor.autoclosebrackets.label;</span>
+ </label>
+ <label title="&options.sourceeditor.expandtab.tooltip;">
+ <input type="checkbox"
+ id="devtools-sourceeditor-expandtab"
+ data-pref="devtools.editor.expandtab"/>
+ <span>&options.sourceeditor.expandtab.label;</span>
+ </label>
+ <label>
+ <span>&options.sourceeditor.tabsize.label;</span>
+ <select id="devtools-sourceeditor-tabsize-select"
+ data-pref="devtools.editor.tabsize">
+ <option label="2">2</option>
+ <option label="4">4</option>
+ <option label="8">8</option>
+ </select>
+ </label>
+ <label>
+ <span>&options.sourceeditor.keybinding.label;</span>
+ <select id="devtools-sourceeditor-keybinding-select"
+ data-pref="devtools.editor.keymap">
+ <option value="default">&options.sourceeditor.keybinding.default.label;</option>
+ <option value="vim">Vim</option>
+ <option value="emacs">Emacs</option>
+ <option value="sublime">Sublime Text</option>
+ </select>
+ </label>
+ </fieldset>
+
+ <fieldset id="context-options" class="options-groupbox">
+ <legend>&options.context.advancedSettings;</legend>
+ <label title="&options.showPlatformData.tooltip;">
+ <input type="checkbox"
+ id="devtools-show-gecko-data"
+ data-pref="devtools.performance.ui.show-platform-data"/>
+ <span>&options.showPlatformData.label;</span>
+ </label>
+ <label title="&options.disableHTTPCache.tooltip;">
+ <input type="checkbox"
+ id="devtools-disable-cache"
+ data-pref="devtools.cache.disabled"/>
+ <span>&options.disableHTTPCache.label;</span>
+ </label>
+ <label title="&options.disableJavaScript.tooltip;">
+ <input type="checkbox"
+ id="devtools-disable-javascript"/>
+ <span>&options.disableJavaScript.label;</span>
+ </label>
+ <label title="&options.enableServiceWorkersHTTP.tooltip;">
+ <input type="checkbox"
+ id="devtools-enable-serviceWorkersTesting"
+ data-pref="devtools.serviceWorkers.testing.enabled"/>
+ <span>&options.enableServiceWorkersHTTP.label;</span>
+ </label>
+ <label title="&options.enableChrome.tooltip3;">
+ <input type="checkbox"
+ data-pref="devtools.chrome.enabled"/>
+ <span>&options.enableChrome.label5;</span>
+ </label>
+ <label title="&options.enableRemote.tooltip2;">
+ <input type="checkbox"
+ data-pref="devtools.debugger.remote-enabled"/>
+ <span>&options.enableRemote.label3;</span>
+ </label>
+ <label title="&options.enableWorkers.tooltip;">
+ <input type="checkbox"
+ data-pref="devtools.debugger.workers"/>
+ <span>&options.enableWorkers.label;</span>
+ </label>
+ <span class="options-citation-label theme-comment"
+ >&options.context.triggersPageRefresh;</span>
+ </fieldset>
+ </div>
+
+ </form>
+ </body>
+</html>
diff --git a/devtools/client/framework/toolbox-process-window.js b/devtools/client/framework/toolbox-process-window.js
new file mode 100644
index 000000000..8ead718b3
--- /dev/null
+++ b/devtools/client/framework/toolbox-process-window.js
@@ -0,0 +1,230 @@
+/* -*- 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";
+
+var { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+var { loader, require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+// Require this module to setup core modules
+loader.require("devtools/client/framework/devtools-browser");
+
+var { gDevTools } = require("devtools/client/framework/devtools");
+var { TargetFactory } = require("devtools/client/framework/target");
+var { Toolbox } = require("devtools/client/framework/toolbox");
+var Services = require("Services");
+var { DebuggerClient } = require("devtools/shared/client/main");
+var { PrefsHelper } = require("devtools/client/shared/prefs");
+var { Task } = require("devtools/shared/task");
+
+/**
+ * Shortcuts for accessing various debugger preferences.
+ */
+var Prefs = new PrefsHelper("devtools.debugger", {
+ chromeDebuggingHost: ["Char", "chrome-debugging-host"],
+ chromeDebuggingPort: ["Int", "chrome-debugging-port"],
+ chromeDebuggingWebSocket: ["Bool", "chrome-debugging-websocket"],
+});
+
+var gToolbox, gClient;
+
+var connect = Task.async(function*() {
+ window.removeEventListener("load", connect);
+ // Initiate the connection
+ let transport = yield DebuggerClient.socketConnect({
+ host: Prefs.chromeDebuggingHost,
+ port: Prefs.chromeDebuggingPort,
+ webSocket: Prefs.chromeDebuggingWebSocket,
+ });
+ gClient = new DebuggerClient(transport);
+ yield gClient.connect();
+ let addonID = getParameterByName("addonID");
+
+ if (addonID) {
+ let { addons } = yield gClient.listAddons();
+ let addonActor = addons.filter(addon => addon.id === addonID).pop();
+ openToolbox({
+ form: addonActor,
+ chrome: true,
+ isTabActor: addonActor.isWebExtension ? true : false
+ });
+ } else {
+ let response = yield gClient.getProcess();
+ openToolbox({
+ form: response.form,
+ chrome: true
+ });
+ }
+});
+
+// Certain options should be toggled since we can assume chrome debugging here
+function setPrefDefaults() {
+ Services.prefs.setBoolPref("devtools.inspector.showUserAgentStyles", true);
+ Services.prefs.setBoolPref("devtools.performance.ui.show-platform-data", true);
+ Services.prefs.setBoolPref("devtools.inspector.showAllAnonymousContent", true);
+ Services.prefs.setBoolPref("browser.dom.window.dump.enabled", true);
+ Services.prefs.setBoolPref("devtools.command-button-noautohide.enabled", true);
+ Services.prefs.setBoolPref("devtools.scratchpad.enabled", true);
+ // Bug 1225160 - Using source maps with browser debugging can lead to a crash
+ Services.prefs.setBoolPref("devtools.debugger.source-maps-enabled", false);
+ Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false);
+ Services.prefs.setBoolPref("devtools.debugger.client-source-maps-enabled", true);
+}
+
+window.addEventListener("load", function() {
+ let cmdClose = document.getElementById("toolbox-cmd-close");
+ cmdClose.addEventListener("command", onCloseCommand);
+ setPrefDefaults();
+ connect().catch(e => {
+ let errorMessageContainer = document.getElementById("error-message-container");
+ let errorMessage = document.getElementById("error-message");
+ errorMessage.value = e.message || e;
+ errorMessageContainer.hidden = false;
+ console.error(e);
+ });
+});
+
+function onCloseCommand(event) {
+ window.close();
+}
+
+function openToolbox({ form, chrome, isTabActor }) {
+ let options = {
+ form: form,
+ client: gClient,
+ chrome: chrome,
+ isTabActor: isTabActor
+ };
+ TargetFactory.forRemoteTab(options).then(target => {
+ let frame = document.getElementById("toolbox-iframe");
+ let selectedTool = "jsdebugger";
+
+ try {
+ // Remember the last panel that was used inside of this profile.
+ selectedTool = Services.prefs.getCharPref("devtools.toolbox.selectedTool");
+ } catch(e) {}
+
+ try {
+ // But if we are testing, then it should always open the debugger panel.
+ selectedTool = Services.prefs.getCharPref("devtools.browsertoolbox.panel");
+ } catch(e) {}
+
+ let options = { customIframe: frame };
+ gDevTools.showToolbox(target,
+ selectedTool,
+ Toolbox.HostType.CUSTOM,
+ options)
+ .then(onNewToolbox);
+ });
+}
+
+function onNewToolbox(toolbox) {
+ gToolbox = toolbox;
+ bindToolboxHandlers();
+ raise();
+ let env = Components.classes["@mozilla.org/process/environment;1"].getService(Components.interfaces.nsIEnvironment);
+ let testScript = env.get("MOZ_TOOLBOX_TEST_SCRIPT");
+ if (testScript) {
+ // Only allow executing random chrome scripts when a special
+ // test-only pref is set
+ let prefName = "devtools.browser-toolbox.allow-unsafe-script";
+ if (Services.prefs.getPrefType(prefName) == Services.prefs.PREF_BOOL &&
+ Services.prefs.getBoolPref(prefName) === true) {
+ evaluateTestScript(testScript, toolbox);
+ }
+ }
+}
+
+function evaluateTestScript(script, toolbox) {
+ let sandbox = Cu.Sandbox(window);
+ sandbox.window = window;
+ sandbox.toolbox = toolbox;
+ Cu.evalInSandbox(script, sandbox);
+}
+
+function bindToolboxHandlers() {
+ gToolbox.once("destroyed", quitApp);
+ window.addEventListener("unload", onUnload);
+
+#ifdef XP_MACOSX
+ // Badge the dock icon to differentiate this process from the main application process.
+ updateBadgeText(false);
+
+ // Once the debugger panel opens listen for thread pause / resume.
+ gToolbox.getPanelWhenReady("jsdebugger").then(panel => {
+ setupThreadListeners(panel);
+ });
+#endif
+}
+
+function setupThreadListeners(panel) {
+ updateBadgeText(panel._controller.activeThread.state == "paused");
+
+ let onPaused = updateBadgeText.bind(null, true);
+ let onResumed = updateBadgeText.bind(null, false);
+ panel.target.on("thread-paused", onPaused);
+ panel.target.on("thread-resumed", onResumed);
+
+ panel.once("destroyed", () => {
+ panel.off("thread-paused", onPaused);
+ panel.off("thread-resumed", onResumed);
+ });
+}
+
+function updateBadgeText(paused) {
+ let dockSupport = Cc["@mozilla.org/widget/macdocksupport;1"].getService(Ci.nsIMacDockSupport);
+ dockSupport.badgeText = paused ? "â–â– " : " â–¶";
+}
+
+function onUnload() {
+ window.removeEventListener("unload", onUnload);
+ window.removeEventListener("message", onMessage);
+ let cmdClose = document.getElementById("toolbox-cmd-close");
+ cmdClose.removeEventListener("command", onCloseCommand);
+ gToolbox.destroy();
+}
+
+function onMessage(event) {
+ try {
+ let json = JSON.parse(event.data);
+ switch (json.name) {
+ case "toolbox-raise":
+ raise();
+ break;
+ case "toolbox-title":
+ setTitle(json.data.value);
+ break;
+ }
+ } catch(e) { console.error(e); }
+}
+
+window.addEventListener("message", onMessage);
+
+function raise() {
+ window.focus();
+}
+
+function setTitle(title) {
+ document.title = title;
+}
+
+function quitApp() {
+ let quit = Cc["@mozilla.org/supports-PRBool;1"]
+ .createInstance(Ci.nsISupportsPRBool);
+ Services.obs.notifyObservers(quit, "quit-application-requested", null);
+
+ let shouldProceed = !quit.data;
+ if (shouldProceed) {
+ Services.startup.quit(Ci.nsIAppStartup.eForceQuit);
+ }
+}
+
+function getParameterByName (name) {
+ name = name.replace(/[\[]/, "\\\[").replace(/[\]]/, "\\\]");
+ let regex = new RegExp("[\\?&]" + name + "=([^&#]*)");
+ let results = regex.exec(window.location.search);
+ return results == null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
+}
diff --git a/devtools/client/framework/toolbox-process-window.xul b/devtools/client/framework/toolbox-process-window.xul
new file mode 100644
index 000000000..d2f8a741b
--- /dev/null
+++ b/devtools/client/framework/toolbox-process-window.xul
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+<!DOCTYPE window [
+<!ENTITY % toolboxDTD SYSTEM "chrome://devtools/locale/toolbox.dtd" >
+ %toolboxDTD;
+]>
+
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ id="devtools-toolbox-window"
+ macanimationtype="document"
+ fullscreenbutton="true"
+ windowtype="devtools:toolbox"
+ width="900" height="600"
+ persist="screenX screenY width height sizemode">
+
+ <script type="text/javascript" src="chrome://global/content/globalOverlay.js"/>
+ <script type="text/javascript" src="toolbox-process-window.js"/>
+ <script type="text/javascript" src="chrome://global/content/viewSourceUtils.js"/>
+ <script type="text/javascript" src="chrome://browser/content/utilityOverlay.js"/>
+
+ <commandset id="toolbox-commandset">
+ <command id="toolbox-cmd-close"/>
+ </commandset>
+
+ <keyset id="toolbox-keyset">
+ <key id="toolbox-key-close"
+ key="&closeCmd.key;"
+ command="toolbox-cmd-close"
+ modifiers="accel"/>
+ </keyset>
+
+ <!-- This will be used by the Web Console to hold any popups it may create,
+ for example when viewing network request details. -->
+ <popupset id="mainPopupSet"></popupset>
+
+ <vbox id="error-message-container" hidden="true" flex="1">
+ <box>&browserToolboxErrorMessage;</box>
+ <textbox multiline="true" id="error-message" flex="1"></textbox>
+ </vbox>
+
+ <tooltip id="aHTMLTooltip" page="true"/>
+ <iframe id="toolbox-iframe" flex="1" tooltip="aHTMLTooltip"></iframe>
+</window>
diff --git a/devtools/client/framework/toolbox-window.xul b/devtools/client/framework/toolbox-window.xul
new file mode 100644
index 000000000..cd14a3597
--- /dev/null
+++ b/devtools/client/framework/toolbox-window.xul
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+<!DOCTYPE window [
+<!ENTITY % toolboxDTD SYSTEM "chrome://devtools/locale/toolbox.dtd" >
+ %toolboxDTD;
+]>
+
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ id="devtools-toolbox-window"
+ macanimationtype="document"
+ fullscreenbutton="true"
+ windowtype="devtools:toolbox"
+ width="900" height="320"
+ persist="screenX screenY width height sizemode">
+
+ <commandset id="toolbox-commandset">
+ <command id="toolbox-cmd-close" oncommand="window.close();"/>
+ </commandset>
+
+ <keyset id="toolbox-keyset">
+ <key id="toolbox-key-close"
+ key="&closeCmd.key;"
+ command="toolbox-cmd-close"
+ modifiers="accel"/>
+ <key id="toolbox-key-toggle"
+ key="&toggleToolbox.key;"
+ command="toolbox-cmd-close"
+ modifiers="accel,shift"
+ disabled="true"/>
+ <key id="toolbox-key-toggle-osx"
+ key="&toggleToolbox.key;"
+ command="toolbox-cmd-close"
+ modifiers="accel,alt"
+ disabled="true"/>
+ <key id="toolbox-key-toggle-F12"
+ keycode="&toggleToolboxF12.keycode;"
+ keytext="&toggleToolboxF12.keytext;"
+ command="toolbox-cmd-close"/>
+ </keyset>
+
+ <tooltip id="aHTMLTooltip" page="true"/>
+ <iframe id="toolbox-iframe" flex="1" forceOwnRefreshDriver="" tooltip="aHTMLTooltip"></iframe>
+</window>
diff --git a/devtools/client/framework/toolbox.js b/devtools/client/framework/toolbox.js
new file mode 100644
index 000000000..82d5d2915
--- /dev/null
+++ b/devtools/client/framework/toolbox.js
@@ -0,0 +1,2417 @@
+/* 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 MAX_ORDINAL = 99;
+const SPLITCONSOLE_ENABLED_PREF = "devtools.toolbox.splitconsoleEnabled";
+const SPLITCONSOLE_HEIGHT_PREF = "devtools.toolbox.splitconsoleHeight";
+const OS_HISTOGRAM = "DEVTOOLS_OS_ENUMERATED_PER_USER";
+const OS_IS_64_BITS = "DEVTOOLS_OS_IS_64_BITS_PER_USER";
+const HOST_HISTOGRAM = "DEVTOOLS_TOOLBOX_HOST";
+const SCREENSIZE_HISTOGRAM = "DEVTOOLS_SCREEN_RESOLUTION_ENUMERATED_PER_USER";
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const { SourceMapService } = require("./source-map-service");
+
+var {Ci, Cu} = require("chrome");
+var promise = require("promise");
+var defer = require("devtools/shared/defer");
+var Services = require("Services");
+var {Task} = require("devtools/shared/task");
+var {gDevTools} = require("devtools/client/framework/devtools");
+var EventEmitter = require("devtools/shared/event-emitter");
+var Telemetry = require("devtools/client/shared/telemetry");
+var HUDService = require("devtools/client/webconsole/hudservice");
+var viewSource = require("devtools/client/shared/view-source");
+var { attachThread, detachThread } = require("./attach-thread");
+var Menu = require("devtools/client/framework/menu");
+var MenuItem = require("devtools/client/framework/menu-item");
+var { DOMHelpers } = require("resource://devtools/client/shared/DOMHelpers.jsm");
+const { KeyCodes } = require("devtools/client/shared/keycodes");
+
+const { BrowserLoader } =
+ Cu.import("resource://devtools/client/shared/browser-loader.js", {});
+
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper("devtools/client/locales/toolbox.properties");
+
+loader.lazyRequireGetter(this, "CommandUtils",
+ "devtools/client/shared/developer-toolbar", true);
+loader.lazyRequireGetter(this, "getHighlighterUtils",
+ "devtools/client/framework/toolbox-highlighter-utils", true);
+loader.lazyRequireGetter(this, "Selection",
+ "devtools/client/framework/selection", true);
+loader.lazyRequireGetter(this, "InspectorFront",
+ "devtools/shared/fronts/inspector", true);
+loader.lazyRequireGetter(this, "flags",
+ "devtools/shared/flags");
+loader.lazyRequireGetter(this, "showDoorhanger",
+ "devtools/client/shared/doorhanger", true);
+loader.lazyRequireGetter(this, "createPerformanceFront",
+ "devtools/shared/fronts/performance", true);
+loader.lazyRequireGetter(this, "system",
+ "devtools/shared/system");
+loader.lazyRequireGetter(this, "getPreferenceFront",
+ "devtools/shared/fronts/preference", true);
+loader.lazyRequireGetter(this, "KeyShortcuts",
+ "devtools/client/shared/key-shortcuts", true);
+loader.lazyRequireGetter(this, "ZoomKeys",
+ "devtools/client/shared/zoom-keys");
+loader.lazyRequireGetter(this, "settleAll",
+ "devtools/shared/ThreadSafeDevToolsUtils", true);
+loader.lazyRequireGetter(this, "ToolboxButtons",
+ "devtools/client/definitions", true);
+
+loader.lazyGetter(this, "registerHarOverlay", () => {
+ return require("devtools/client/netmonitor/har/toolbox-overlay").register;
+});
+
+/**
+ * A "Toolbox" is the component that holds all the tools for one specific
+ * target. Visually, it's a document that includes the tools tabs and all
+ * the iframes where the tool panels will be living in.
+ *
+ * @param {object} target
+ * The object the toolbox is debugging.
+ * @param {string} selectedTool
+ * Tool to select initially
+ * @param {Toolbox.HostType} hostType
+ * Type of host that will host the toolbox (e.g. sidebar, window)
+ * @param {DOMWindow} contentWindow
+ * The window object of the toolbox document
+ * @param {string} frameId
+ * A unique identifier to differentiate toolbox documents from the
+ * chrome codebase when passing DOM messages
+ */
+function Toolbox(target, selectedTool, hostType, contentWindow, frameId) {
+ this._target = target;
+ this._win = contentWindow;
+ this.frameId = frameId;
+
+ this._toolPanels = new Map();
+ this._telemetry = new Telemetry();
+ if (Services.prefs.getBoolPref("devtools.sourcemap.locations.enabled")) {
+ this._sourceMapService = new SourceMapService(this._target);
+ }
+
+ this._initInspector = null;
+ this._inspector = null;
+
+ // Map of frames (id => frame-info) and currently selected frame id.
+ this.frameMap = new Map();
+ this.selectedFrameId = null;
+
+ this._toolRegistered = this._toolRegistered.bind(this);
+ this._toolUnregistered = this._toolUnregistered.bind(this);
+ this._refreshHostTitle = this._refreshHostTitle.bind(this);
+ this._toggleAutohide = this._toggleAutohide.bind(this);
+ this.showFramesMenu = this.showFramesMenu.bind(this);
+ this._updateFrames = this._updateFrames.bind(this);
+ this._splitConsoleOnKeypress = this._splitConsoleOnKeypress.bind(this);
+ this.destroy = this.destroy.bind(this);
+ this.highlighterUtils = getHighlighterUtils(this);
+ this._highlighterReady = this._highlighterReady.bind(this);
+ this._highlighterHidden = this._highlighterHidden.bind(this);
+ this._prefChanged = this._prefChanged.bind(this);
+ this._saveSplitConsoleHeight = this._saveSplitConsoleHeight.bind(this);
+ this._onFocus = this._onFocus.bind(this);
+ this._onBrowserMessage = this._onBrowserMessage.bind(this);
+ this._showDevEditionPromo = this._showDevEditionPromo.bind(this);
+ this._updateTextBoxMenuItems = this._updateTextBoxMenuItems.bind(this);
+ this._onBottomHostMinimized = this._onBottomHostMinimized.bind(this);
+ this._onBottomHostMaximized = this._onBottomHostMaximized.bind(this);
+ this._onToolSelectWhileMinimized = this._onToolSelectWhileMinimized.bind(this);
+ this._onPerformanceFrontEvent = this._onPerformanceFrontEvent.bind(this);
+ this._onBottomHostWillChange = this._onBottomHostWillChange.bind(this);
+ this._toggleMinimizeMode = this._toggleMinimizeMode.bind(this);
+ this._onTabbarFocus = this._onTabbarFocus.bind(this);
+ this._onTabbarArrowKeypress = this._onTabbarArrowKeypress.bind(this);
+ this._onPickerClick = this._onPickerClick.bind(this);
+ this._onPickerKeypress = this._onPickerKeypress.bind(this);
+ this._onPickerStarted = this._onPickerStarted.bind(this);
+ this._onPickerStopped = this._onPickerStopped.bind(this);
+
+ this._target.on("close", this.destroy);
+
+ if (!selectedTool) {
+ selectedTool = Services.prefs.getCharPref(this._prefs.LAST_TOOL);
+ }
+ this._defaultToolId = selectedTool;
+
+ this._hostType = hostType;
+
+ EventEmitter.decorate(this);
+
+ this._target.on("navigate", this._refreshHostTitle);
+ this._target.on("frame-update", this._updateFrames);
+
+ this.on("host-changed", this._refreshHostTitle);
+ this.on("select", this._refreshHostTitle);
+
+ this.on("ready", this._showDevEditionPromo);
+
+ gDevTools.on("tool-registered", this._toolRegistered);
+ gDevTools.on("tool-unregistered", this._toolUnregistered);
+
+ this.on("picker-started", this._onPickerStarted);
+ this.on("picker-stopped", this._onPickerStopped);
+}
+exports.Toolbox = Toolbox;
+
+/**
+ * The toolbox can be 'hosted' either embedded in a browser window
+ * or in a separate window.
+ */
+Toolbox.HostType = {
+ BOTTOM: "bottom",
+ SIDE: "side",
+ WINDOW: "window",
+ CUSTOM: "custom"
+};
+
+Toolbox.prototype = {
+ _URL: "about:devtools-toolbox",
+
+ _prefs: {
+ LAST_TOOL: "devtools.toolbox.selectedTool",
+ SIDE_ENABLED: "devtools.toolbox.sideEnabled",
+ },
+
+ currentToolId: null,
+ lastUsedToolId: null,
+
+ /**
+ * Returns a *copy* of the _toolPanels collection.
+ *
+ * @return {Map} panels
+ * All the running panels in the toolbox
+ */
+ getToolPanels: function () {
+ return new Map(this._toolPanels);
+ },
+
+ /**
+ * Access the panel for a given tool
+ */
+ getPanel: function (id) {
+ return this._toolPanels.get(id);
+ },
+
+ /**
+ * Get the panel instance for a given tool once it is ready.
+ * If the tool is already opened, the promise will resolve immediately,
+ * otherwise it will wait until the tool has been opened before resolving.
+ *
+ * Note that this does not open the tool, use selectTool if you'd
+ * like to select the tool right away.
+ *
+ * @param {String} id
+ * The id of the panel, for example "jsdebugger".
+ * @returns Promise
+ * A promise that resolves once the panel is ready.
+ */
+ getPanelWhenReady: function (id) {
+ let deferred = defer();
+ let panel = this.getPanel(id);
+ if (panel) {
+ deferred.resolve(panel);
+ } else {
+ this.on(id + "-ready", (e, initializedPanel) => {
+ deferred.resolve(initializedPanel);
+ });
+ }
+
+ return deferred.promise;
+ },
+
+ /**
+ * This is a shortcut for getPanel(currentToolId) because it is much more
+ * likely that we're going to want to get the panel that we've just made
+ * visible
+ */
+ getCurrentPanel: function () {
+ return this._toolPanels.get(this.currentToolId);
+ },
+
+ /**
+ * Get/alter the target of a Toolbox so we're debugging something different.
+ * See Target.jsm for more details.
+ * TODO: Do we allow |toolbox.target = null;| ?
+ */
+ get target() {
+ return this._target;
+ },
+
+ get threadClient() {
+ return this._threadClient;
+ },
+
+ /**
+ * Get/alter the host of a Toolbox, i.e. is it in browser or in a separate
+ * tab. See HostType for more details.
+ */
+ get hostType() {
+ return this._hostType;
+ },
+
+ /**
+ * Shortcut to the window containing the toolbox UI
+ */
+ get win() {
+ return this._win;
+ },
+
+ /**
+ * Shortcut to the document containing the toolbox UI
+ */
+ get doc() {
+ return this.win.document;
+ },
+
+ /**
+ * Get the toolbox highlighter front. Note that it may not always have been
+ * initialized first. Use `initInspector()` if needed.
+ * Consider using highlighterUtils instead, it exposes the highlighter API in
+ * a useful way for the toolbox panels
+ */
+ get highlighter() {
+ return this._highlighter;
+ },
+
+ /**
+ * Get the toolbox's performance front. Note that it may not always have been
+ * initialized first. Use `initPerformance()` if needed.
+ */
+ get performance() {
+ return this._performance;
+ },
+
+ /**
+ * Get the toolbox's inspector front. Note that it may not always have been
+ * initialized first. Use `initInspector()` if needed.
+ */
+ get inspector() {
+ return this._inspector;
+ },
+
+ /**
+ * Get the toolbox's walker front. Note that it may not always have been
+ * initialized first. Use `initInspector()` if needed.
+ */
+ get walker() {
+ return this._walker;
+ },
+
+ /**
+ * Get the toolbox's node selection. Note that it may not always have been
+ * initialized first. Use `initInspector()` if needed.
+ */
+ get selection() {
+ return this._selection;
+ },
+
+ /**
+ * Get the toggled state of the split console
+ */
+ get splitConsole() {
+ return this._splitConsole;
+ },
+
+ /**
+ * Get the focused state of the split console
+ */
+ isSplitConsoleFocused: function () {
+ if (!this._splitConsole) {
+ return false;
+ }
+ let focusedWin = Services.focus.focusedWindow;
+ return focusedWin && focusedWin ===
+ this.doc.querySelector("#toolbox-panel-iframe-webconsole").contentWindow;
+ },
+
+ /**
+ * Open the toolbox
+ */
+ open: function () {
+ return Task.spawn(function* () {
+ this.browserRequire = BrowserLoader({
+ window: this.doc.defaultView,
+ useOnlyShared: true
+ }).require;
+
+ if (this.win.location.href.startsWith(this._URL)) {
+ // Update the URL so that onceDOMReady watch for the right url.
+ this._URL = this.win.location.href;
+ }
+
+ let domReady = defer();
+ let domHelper = new DOMHelpers(this.win);
+ domHelper.onceDOMReady(() => {
+ domReady.resolve();
+ }, this._URL);
+
+ // Optimization: fire up a few other things before waiting on
+ // the iframe being ready (makes startup faster)
+
+ // Load the toolbox-level actor fronts and utilities now
+ yield this._target.makeRemote();
+
+ // Attach the thread
+ this._threadClient = yield attachThread(this);
+ yield domReady.promise;
+
+ this.isReady = true;
+ let framesPromise = this._listFrames();
+
+ this.closeButton = this.doc.getElementById("toolbox-close");
+ this.closeButton.addEventListener("click", this.destroy, true);
+
+ gDevTools.on("pref-changed", this._prefChanged);
+
+ let framesMenu = this.doc.getElementById("command-button-frames");
+ framesMenu.addEventListener("click", this.showFramesMenu, false);
+
+ let noautohideMenu = this.doc.getElementById("command-button-noautohide");
+ noautohideMenu.addEventListener("click", this._toggleAutohide, true);
+
+ this.textBoxContextMenuPopup =
+ this.doc.getElementById("toolbox-textbox-context-popup");
+ this.textBoxContextMenuPopup.addEventListener("popupshowing",
+ this._updateTextBoxMenuItems, true);
+
+ this.shortcuts = new KeyShortcuts({
+ window: this.doc.defaultView
+ });
+ this._buildDockButtons();
+ this._buildOptions();
+ this._buildTabs();
+ this._applyCacheSettings();
+ this._applyServiceWorkersTestingSettings();
+ this._addKeysToWindow();
+ this._addReloadKeys();
+ this._addHostListeners();
+ this._registerOverlays();
+ if (!this._hostOptions || this._hostOptions.zoom === true) {
+ ZoomKeys.register(this.win);
+ }
+
+ this.tabbar = this.doc.querySelector(".devtools-tabbar");
+ this.tabbar.addEventListener("focus", this._onTabbarFocus, true);
+ this.tabbar.addEventListener("click", this._onTabbarFocus, true);
+ this.tabbar.addEventListener("keypress", this._onTabbarArrowKeypress);
+
+ this.webconsolePanel = this.doc.querySelector("#toolbox-panel-webconsole");
+ this.webconsolePanel.height = Services.prefs.getIntPref(SPLITCONSOLE_HEIGHT_PREF);
+ this.webconsolePanel.addEventListener("resize", this._saveSplitConsoleHeight);
+
+ let buttonsPromise = this._buildButtons();
+
+ this._pingTelemetry();
+
+ // The isTargetSupported check needs to happen after the target is
+ // remoted, otherwise we could have done it in the toolbox constructor
+ // (bug 1072764).
+ let toolDef = gDevTools.getToolDefinition(this._defaultToolId);
+ if (!toolDef || !toolDef.isTargetSupported(this._target)) {
+ this._defaultToolId = "webconsole";
+ }
+
+ yield this.selectTool(this._defaultToolId);
+
+ // Wait until the original tool is selected so that the split
+ // console input will receive focus.
+ let splitConsolePromise = promise.resolve();
+ if (Services.prefs.getBoolPref(SPLITCONSOLE_ENABLED_PREF)) {
+ splitConsolePromise = this.openSplitConsole();
+ }
+
+ yield promise.all([
+ splitConsolePromise,
+ buttonsPromise,
+ framesPromise
+ ]);
+
+ // Lazily connect to the profiler here and don't wait for it to complete,
+ // used to intercept console.profile calls before the performance tools are open.
+ let performanceFrontConnection = this.initPerformance();
+
+ // If in testing environment, wait for performance connection to finish,
+ // so we don't have to explicitly wait for this in tests; ideally, all tests
+ // will handle this on their own, but each have their own tear down function.
+ if (flags.testing) {
+ yield performanceFrontConnection;
+ }
+
+ this.emit("ready");
+ }.bind(this)).then(null, console.error.bind(console));
+ },
+
+ /**
+ * loading React modules when needed (to avoid performance penalties
+ * during Firefox start up time).
+ */
+ get React() {
+ return this.browserRequire("devtools/client/shared/vendor/react");
+ },
+
+ get ReactDOM() {
+ return this.browserRequire("devtools/client/shared/vendor/react-dom");
+ },
+
+ get ReactRedux() {
+ return this.browserRequire("devtools/client/shared/vendor/react-redux");
+ },
+
+ // Return HostType id for telemetry
+ _getTelemetryHostId: function () {
+ switch (this.hostType) {
+ case Toolbox.HostType.BOTTOM: return 0;
+ case Toolbox.HostType.SIDE: return 1;
+ case Toolbox.HostType.WINDOW: return 2;
+ case Toolbox.HostType.CUSTOM: return 3;
+ default: return 9;
+ }
+ },
+
+ _pingTelemetry: function () {
+ this._telemetry.toolOpened("toolbox");
+
+ this._telemetry.logOncePerBrowserVersion(OS_HISTOGRAM, system.getOSCPU());
+ this._telemetry.logOncePerBrowserVersion(OS_IS_64_BITS,
+ Services.appinfo.is64Bit ? 1 : 0);
+ this._telemetry.logOncePerBrowserVersion(SCREENSIZE_HISTOGRAM,
+ system.getScreenDimensions());
+ this._telemetry.log(HOST_HISTOGRAM, this._getTelemetryHostId());
+ },
+
+ /**
+ * Because our panels are lazy loaded this is a good place to watch for
+ * "pref-changed" events.
+ * @param {String} event
+ * The event type, "pref-changed".
+ * @param {Object} data
+ * {
+ * newValue: The new value
+ * oldValue: The old value
+ * pref: The name of the preference that has changed
+ * }
+ */
+ _prefChanged: function (event, data) {
+ switch (data.pref) {
+ case "devtools.cache.disabled":
+ this._applyCacheSettings();
+ break;
+ case "devtools.serviceWorkers.testing.enabled":
+ this._applyServiceWorkersTestingSettings();
+ break;
+ }
+ },
+
+ _buildOptions: function () {
+ let selectOptions = (name, event) => {
+ // Flip back to the last used panel if we are already
+ // on the options panel.
+ if (this.currentToolId === "options" &&
+ gDevTools.getToolDefinition(this.lastUsedToolId)) {
+ this.selectTool(this.lastUsedToolId);
+ } else {
+ this.selectTool("options");
+ }
+ // Prevent the opening of bookmarks window on toolbox.options.key
+ event.preventDefault();
+ };
+ this.shortcuts.on(L10N.getStr("toolbox.options.key"), selectOptions);
+ this.shortcuts.on(L10N.getStr("toolbox.help.key"), selectOptions);
+ },
+
+ _splitConsoleOnKeypress: function (e) {
+ if (e.keyCode === KeyCodes.DOM_VK_ESCAPE) {
+ this.toggleSplitConsole();
+ // If the debugger is paused, don't let the ESC key stop any pending
+ // navigation.
+ if (this._threadClient.state == "paused") {
+ e.preventDefault();
+ }
+ }
+ },
+
+ /**
+ * Add a shortcut key that should work when a split console
+ * has focus to the toolbox.
+ *
+ * @param {String} key
+ * The electron key shortcut.
+ * @param {Function} handler
+ * The callback that should be called when the provided key shortcut is pressed.
+ * @param {String} whichTool
+ * The tool the key belongs to. The corresponding handler will only be triggered
+ * if this tool is active.
+ */
+ useKeyWithSplitConsole: function (key, handler, whichTool) {
+ this.shortcuts.on(key, (name, event) => {
+ if (this.currentToolId === whichTool && this.isSplitConsoleFocused()) {
+ handler();
+ event.preventDefault();
+ }
+ });
+ },
+
+ _addReloadKeys: function () {
+ [
+ ["reload", false],
+ ["reload2", false],
+ ["forceReload", true],
+ ["forceReload2", true]
+ ].forEach(([id, force]) => {
+ let key = L10N.getStr("toolbox." + id + ".key");
+ this.shortcuts.on(key, (name, event) => {
+ this.reloadTarget(force);
+
+ // Prevent Firefox shortcuts from reloading the page
+ event.preventDefault();
+ });
+ });
+ },
+
+ _addHostListeners: function () {
+ this.shortcuts.on(L10N.getStr("toolbox.nextTool.key"),
+ (name, event) => {
+ this.selectNextTool();
+ event.preventDefault();
+ });
+ this.shortcuts.on(L10N.getStr("toolbox.previousTool.key"),
+ (name, event) => {
+ this.selectPreviousTool();
+ event.preventDefault();
+ });
+ this.shortcuts.on(L10N.getStr("toolbox.minimize.key"),
+ (name, event) => {
+ this._toggleMinimizeMode();
+ event.preventDefault();
+ });
+ this.shortcuts.on(L10N.getStr("toolbox.toggleHost.key"),
+ (name, event) => {
+ this.switchToPreviousHost();
+ event.preventDefault();
+ });
+
+ this.doc.addEventListener("keypress", this._splitConsoleOnKeypress, false);
+ this.doc.addEventListener("focus", this._onFocus, true);
+ this.win.addEventListener("unload", this.destroy);
+ this.win.addEventListener("message", this._onBrowserMessage, true);
+ },
+
+ _removeHostListeners: function () {
+ // The host iframe's contentDocument may already be gone.
+ if (this.doc) {
+ this.doc.removeEventListener("keypress", this._splitConsoleOnKeypress, false);
+ this.doc.removeEventListener("focus", this._onFocus, true);
+ this.win.removeEventListener("unload", this.destroy);
+ this.win.removeEventListener("message", this._onBrowserMessage, true);
+ }
+ },
+
+ // Called whenever the chrome send a message
+ _onBrowserMessage: function (event) {
+ if (!event.data) {
+ return;
+ }
+ switch (event.data.name) {
+ case "switched-host":
+ this._onSwitchedHost(event.data);
+ break;
+ case "host-minimized":
+ if (this.hostType == Toolbox.HostType.BOTTOM) {
+ this._onBottomHostMinimized();
+ }
+ break;
+ case "host-maximized":
+ if (this.hostType == Toolbox.HostType.BOTTOM) {
+ this._onBottomHostMaximized();
+ }
+ break;
+ }
+ },
+
+ _registerOverlays: function () {
+ registerHarOverlay(this);
+ },
+
+ _saveSplitConsoleHeight: function () {
+ Services.prefs.setIntPref(SPLITCONSOLE_HEIGHT_PREF,
+ this.webconsolePanel.height);
+ },
+
+ /**
+ * Make sure that the console is showing up properly based on all the
+ * possible conditions.
+ * 1) If the console tab is selected, then regardless of split state
+ * it should take up the full height of the deck, and we should
+ * hide the deck and splitter.
+ * 2) If the console tab is not selected and it is split, then we should
+ * show the splitter, deck, and console.
+ * 3) If the console tab is not selected and it is *not* split,
+ * then we should hide the console and splitter, and show the deck
+ * at full height.
+ */
+ _refreshConsoleDisplay: function () {
+ let deck = this.doc.getElementById("toolbox-deck");
+ let webconsolePanel = this.webconsolePanel;
+ let splitter = this.doc.getElementById("toolbox-console-splitter");
+ let openedConsolePanel = this.currentToolId === "webconsole";
+
+ if (openedConsolePanel) {
+ deck.setAttribute("collapsed", "true");
+ splitter.setAttribute("hidden", "true");
+ webconsolePanel.removeAttribute("collapsed");
+ } else {
+ deck.removeAttribute("collapsed");
+ if (this.splitConsole) {
+ webconsolePanel.removeAttribute("collapsed");
+ splitter.removeAttribute("hidden");
+ } else {
+ webconsolePanel.setAttribute("collapsed", "true");
+ splitter.setAttribute("hidden", "true");
+ }
+ }
+ },
+
+ /**
+ * Adds the keys and commands to the Toolbox Window in window mode.
+ */
+ _addKeysToWindow: function () {
+ if (this.hostType != Toolbox.HostType.WINDOW) {
+ return;
+ }
+
+ let doc = this.win.parent.document;
+
+ for (let [id, toolDefinition] of gDevTools.getToolDefinitionMap()) {
+ // Prevent multiple entries for the same tool.
+ if (!toolDefinition.key || doc.getElementById("key_" + id)) {
+ continue;
+ }
+
+ let toolId = id;
+ let key = doc.createElement("key");
+
+ key.id = "key_" + toolId;
+
+ if (toolDefinition.key.startsWith("VK_")) {
+ key.setAttribute("keycode", toolDefinition.key);
+ } else {
+ key.setAttribute("key", toolDefinition.key);
+ }
+
+ key.setAttribute("modifiers", toolDefinition.modifiers);
+ // needed. See bug 371900
+ key.setAttribute("oncommand", "void(0);");
+ key.addEventListener("command", () => {
+ this.selectTool(toolId).then(() => this.fireCustomKey(toolId));
+ }, true);
+ doc.getElementById("toolbox-keyset").appendChild(key);
+ }
+
+ // Add key for toggling the browser console from the detached window
+ if (!doc.getElementById("key_browserconsole")) {
+ let key = doc.createElement("key");
+ key.id = "key_browserconsole";
+
+ key.setAttribute("key", L10N.getStr("browserConsoleCmd.commandkey"));
+ key.setAttribute("modifiers", "accel,shift");
+ // needed. See bug 371900
+ key.setAttribute("oncommand", "void(0)");
+ key.addEventListener("command", () => {
+ HUDService.toggleBrowserConsole();
+ }, true);
+ doc.getElementById("toolbox-keyset").appendChild(key);
+ }
+ },
+
+ /**
+ * Handle any custom key events. Returns true if there was a custom key
+ * binding run.
+ * @param {string} toolId Which tool to run the command on (skip if not
+ * current)
+ */
+ fireCustomKey: function (toolId) {
+ let toolDefinition = gDevTools.getToolDefinition(toolId);
+
+ if (toolDefinition.onkey &&
+ ((this.currentToolId === toolId) ||
+ (toolId == "webconsole" && this.splitConsole))) {
+ toolDefinition.onkey(this.getCurrentPanel(), this);
+ }
+ },
+
+ /**
+ * Build the notification box as soon as needed.
+ */
+ get notificationBox() {
+ if (!this._notificationBox) {
+ let { NotificationBox, PriorityLevels } =
+ this.browserRequire(
+ "devtools/client/shared/components/notification-box");
+
+ NotificationBox = this.React.createFactory(NotificationBox);
+
+ // Render NotificationBox and assign priority levels to it.
+ let box = this.doc.getElementById("toolbox-notificationbox");
+ this._notificationBox = Object.assign(
+ this.ReactDOM.render(NotificationBox({}), box),
+ PriorityLevels);
+ }
+ return this._notificationBox;
+ },
+
+ /**
+ * Build the buttons for changing hosts. Called every time
+ * the host changes.
+ */
+ _buildDockButtons: function () {
+ let dockBox = this.doc.getElementById("toolbox-dock-buttons");
+
+ while (dockBox.firstChild) {
+ dockBox.removeChild(dockBox.firstChild);
+ }
+
+ if (!this._target.isLocalTab) {
+ return;
+ }
+
+ // Bottom-type host can be minimized, add a button for this.
+ if (this.hostType == Toolbox.HostType.BOTTOM) {
+ let minimizeBtn = this.doc.createElementNS(HTML_NS, "button");
+ minimizeBtn.id = "toolbox-dock-bottom-minimize";
+ minimizeBtn.className = "devtools-button";
+ /* Bug 1177463 - The minimize button is currently hidden until we agree on
+ the UI for it, and until bug 1173849 is fixed too. */
+ minimizeBtn.setAttribute("hidden", "true");
+
+ minimizeBtn.addEventListener("click", this._toggleMinimizeMode);
+ dockBox.appendChild(minimizeBtn);
+ // Show the button in its maximized state.
+ this._onBottomHostMaximized();
+
+ // Maximize again when a tool gets selected.
+ this.on("before-select", this._onToolSelectWhileMinimized);
+ // Maximize and stop listening before the host type changes.
+ this.once("host-will-change", this._onBottomHostWillChange);
+ }
+
+ if (this.hostType == Toolbox.HostType.WINDOW) {
+ this.closeButton.setAttribute("hidden", "true");
+ } else {
+ this.closeButton.removeAttribute("hidden");
+ }
+
+ let sideEnabled = Services.prefs.getBoolPref(this._prefs.SIDE_ENABLED);
+
+ for (let type in Toolbox.HostType) {
+ let position = Toolbox.HostType[type];
+ if (position == this.hostType ||
+ position == Toolbox.HostType.CUSTOM ||
+ (!sideEnabled && position == Toolbox.HostType.SIDE)) {
+ continue;
+ }
+
+ let button = this.doc.createElementNS(HTML_NS, "button");
+ button.id = "toolbox-dock-" + position;
+ button.className = "toolbox-dock-button devtools-button";
+ button.setAttribute("title", L10N.getStr("toolboxDockButtons." +
+ position + ".tooltip"));
+ button.addEventListener("click", this.switchHost.bind(this, position));
+
+ dockBox.appendChild(button);
+ }
+ },
+
+ _getMinimizeButtonShortcutTooltip: function () {
+ let str = L10N.getStr("toolbox.minimize.key");
+ let key = KeyShortcuts.parseElectronKey(this.win, str);
+ return "(" + KeyShortcuts.stringify(key) + ")";
+ },
+
+ _onBottomHostMinimized: function () {
+ let btn = this.doc.querySelector("#toolbox-dock-bottom-minimize");
+ btn.className = "minimized";
+
+ btn.setAttribute("title",
+ L10N.getStr("toolboxDockButtons.bottom.maximize") + " " +
+ this._getMinimizeButtonShortcutTooltip());
+ },
+
+ _onBottomHostMaximized: function () {
+ let btn = this.doc.querySelector("#toolbox-dock-bottom-minimize");
+ btn.className = "maximized";
+
+ btn.setAttribute("title",
+ L10N.getStr("toolboxDockButtons.bottom.minimize") + " " +
+ this._getMinimizeButtonShortcutTooltip());
+ },
+
+ _onToolSelectWhileMinimized: function () {
+ this.postMessage({
+ name: "maximize-host"
+ });
+ },
+
+ postMessage: function (msg) {
+ // We sometime try to send messages in middle of destroy(), where the
+ // toolbox iframe may already be detached and no longer have a parent.
+ if (this.win.parent) {
+ // Toolbox document is still chrome and disallow identifying message
+ // origin via event.source as it is null. So use a custom id.
+ msg.frameId = this.frameId;
+ this.win.parent.postMessage(msg, "*");
+ }
+ },
+
+ _onBottomHostWillChange: function () {
+ this.postMessage({
+ name: "maximize-host"
+ });
+
+ this.off("before-select", this._onToolSelectWhileMinimized);
+ },
+
+ _toggleMinimizeMode: function () {
+ if (this.hostType !== Toolbox.HostType.BOTTOM) {
+ return;
+ }
+
+ // Calculate the height to which the host should be minimized so the
+ // tabbar is still visible.
+ let toolbarHeight = this.tabbar.getBoxQuads({box: "content"})[0].bounds
+ .height;
+ this.postMessage({
+ name: "toggle-minimize-mode",
+ toolbarHeight
+ });
+ },
+
+ /**
+ * Add tabs to the toolbox UI for registered tools
+ */
+ _buildTabs: function () {
+ for (let definition of gDevTools.getToolDefinitionArray()) {
+ this._buildTabForTool(definition);
+ }
+ },
+
+ /**
+ * Get all dev tools tab bar focusable elements. These are visible elements
+ * such as buttons or elements with tabindex.
+ */
+ get tabbarFocusableElms() {
+ return [...this.tabbar.querySelectorAll(
+ "[tabindex]:not([hidden]), button:not([hidden])")];
+ },
+
+ /**
+ * Reset tabindex attributes across all focusable elements inside the tabbar.
+ * Only have one element with tabindex=0 at a time to make sure that tabbing
+ * results in navigating away from the tabbar container.
+ * @param {FocusEvent} event
+ */
+ _onTabbarFocus: function (event) {
+ this.tabbarFocusableElms.forEach(elm =>
+ elm.setAttribute("tabindex", event.target === elm ? "0" : "-1"));
+ },
+
+ /**
+ * On left/right arrow press, attempt to move the focus inside the tabbar to
+ * the previous/next focusable element.
+ * @param {KeyboardEvent} event
+ */
+ _onTabbarArrowKeypress: function (event) {
+ let { key, target, ctrlKey, shiftKey, altKey, metaKey } = event;
+
+ // If any of the modifier keys are pressed do not attempt navigation as it
+ // might conflict with global shortcuts (Bug 1327972).
+ if (ctrlKey || shiftKey || altKey || metaKey) {
+ return;
+ }
+
+ let focusableElms = this.tabbarFocusableElms;
+ let curIndex = focusableElms.indexOf(target);
+
+ if (curIndex === -1) {
+ console.warn(target + " is not found among Developer Tools tab bar " +
+ "focusable elements. It needs to either be a button or have " +
+ "tabindex. If it is intended to be hidden, 'hidden' attribute must " +
+ "be used.");
+ return;
+ }
+
+ let newTarget;
+
+ if (key === "ArrowLeft") {
+ // Do nothing if already at the beginning.
+ if (curIndex === 0) {
+ return;
+ }
+ newTarget = focusableElms[curIndex - 1];
+ } else if (key === "ArrowRight") {
+ // Do nothing if already at the end.
+ if (curIndex === focusableElms.length - 1) {
+ return;
+ }
+ newTarget = focusableElms[curIndex + 1];
+ } else {
+ return;
+ }
+
+ focusableElms.forEach(elm =>
+ elm.setAttribute("tabindex", newTarget === elm ? "0" : "-1"));
+ newTarget.focus();
+
+ event.preventDefault();
+ event.stopPropagation();
+ },
+
+ /**
+ * Add buttons to the UI as specified in the devtools.toolbox.toolbarSpec pref
+ */
+ _buildButtons: function () {
+ if (this.target.getTrait("highlightable")) {
+ this._buildPickerButton();
+ }
+
+ this.setToolboxButtonsVisibility();
+
+ // Old servers don't have a GCLI Actor, so just return
+ if (!this.target.hasActor("gcli")) {
+ return promise.resolve();
+ }
+ // Disable gcli in browser toolbox until there is usages of it
+ if (this.target.chrome) {
+ return promise.resolve();
+ }
+
+ const options = {
+ environment: CommandUtils.createEnvironment(this, "_target")
+ };
+ return CommandUtils.createRequisition(this.target, options).then(requisition => {
+ this._requisition = requisition;
+
+ const spec = CommandUtils.getCommandbarSpec("devtools.toolbox.toolbarSpec");
+ return CommandUtils.createButtons(spec, this.target, this.doc, requisition)
+ .then(buttons => {
+ let container = this.doc.getElementById("toolbox-buttons");
+ buttons.forEach(button => {
+ if (button) {
+ container.appendChild(button);
+ }
+ });
+ this.setToolboxButtonsVisibility();
+ });
+ });
+ },
+
+ /**
+ * Adding the element picker button is done here unlike the other buttons
+ * since we want it to work for remote targets too
+ */
+ _buildPickerButton: function () {
+ this._pickerButton = this.doc.createElementNS(HTML_NS, "button");
+ this._pickerButton.id = "command-button-pick";
+ this._pickerButton.className =
+ "command-button command-button-invertable devtools-button";
+ this._pickerButton.setAttribute("title", L10N.getStr("pickButton.tooltip"));
+
+ let container = this.doc.querySelector("#toolbox-picker-container");
+ container.appendChild(this._pickerButton);
+
+ this._pickerButton.addEventListener("click", this._onPickerClick, false);
+ },
+
+ /**
+ * Toggle the picker, but also decide whether or not the highlighter should
+ * focus the window. This is only desirable when the toolbox is mounted to the
+ * window. When devtools is free floating, then the target window should not
+ * pop in front of the viewer when the picker is clicked.
+ */
+ _onPickerClick: function () {
+ let focus = this.hostType === Toolbox.HostType.BOTTOM ||
+ this.hostType === Toolbox.HostType.SIDE;
+ this.highlighterUtils.togglePicker(focus);
+ },
+
+ /**
+ * If the picker is activated, then allow the Escape key to deactivate the
+ * functionality instead of the default behavior of toggling the console.
+ */
+ _onPickerKeypress: function (event) {
+ if (event.keyCode === KeyCodes.DOM_VK_ESCAPE) {
+ this.highlighterUtils.cancelPicker();
+ // Stop the console from toggling.
+ event.stopImmediatePropagation();
+ }
+ },
+
+ _onPickerStarted: function () {
+ this.doc.addEventListener("keypress", this._onPickerKeypress, true);
+ },
+
+ _onPickerStopped: function () {
+ this.doc.removeEventListener("keypress", this._onPickerKeypress, true);
+ },
+
+ /**
+ * Apply the current cache setting from devtools.cache.disabled to this
+ * toolbox's tab.
+ */
+ _applyCacheSettings: function () {
+ let pref = "devtools.cache.disabled";
+ let cacheDisabled = Services.prefs.getBoolPref(pref);
+
+ if (this.target.activeTab) {
+ this.target.activeTab.reconfigure({"cacheDisabled": cacheDisabled});
+ }
+ },
+
+ /**
+ * Apply the current service workers testing setting from
+ * devtools.serviceWorkers.testing.enabled to this toolbox's tab.
+ */
+ _applyServiceWorkersTestingSettings: function () {
+ let pref = "devtools.serviceWorkers.testing.enabled";
+ let serviceWorkersTestingEnabled =
+ Services.prefs.getBoolPref(pref) || false;
+
+ if (this.target.activeTab) {
+ this.target.activeTab.reconfigure({
+ "serviceWorkersTestingEnabled": serviceWorkersTestingEnabled
+ });
+ }
+ },
+
+ /**
+ * Setter for the checked state of the picker button in the toolbar
+ * @param {Boolean} isChecked
+ */
+ set pickerButtonChecked(isChecked) {
+ if (isChecked) {
+ this._pickerButton.setAttribute("checked", "true");
+ } else {
+ this._pickerButton.removeAttribute("checked");
+ }
+ },
+
+ /**
+ * Return all toolbox buttons (command buttons, plus any others that were
+ * added manually).
+ */
+ get toolboxButtons() {
+ return ToolboxButtons.map(options => {
+ let button = this.doc.getElementById(options.id);
+ // Some buttons may not exist inside of Browser Toolbox
+ if (!button) {
+ return false;
+ }
+
+ return {
+ id: options.id,
+ button: button,
+ label: button.getAttribute("title"),
+ visibilityswitch: "devtools." + options.id + ".enabled",
+ isTargetSupported: options.isTargetSupported
+ ? options.isTargetSupported
+ : target => target.isLocalTab,
+ };
+ }).filter(button=>button);
+ },
+
+ /**
+ * Ensure the visibility of each toolbox button matches the
+ * preference value. Simply hide buttons that are preffed off.
+ */
+ setToolboxButtonsVisibility: function () {
+ this.toolboxButtons.forEach(buttonSpec => {
+ let { visibilityswitch, button, isTargetSupported } = buttonSpec;
+ let on = true;
+ try {
+ on = Services.prefs.getBoolPref(visibilityswitch);
+ } catch (ex) {
+ // Do nothing.
+ }
+
+ on = on && isTargetSupported(this.target);
+
+ if (button) {
+ if (on) {
+ button.removeAttribute("hidden");
+ } else {
+ button.setAttribute("hidden", "true");
+ }
+ }
+ });
+
+ this._updateNoautohideButton();
+ },
+
+ /**
+ * Build a tab for one tool definition and add to the toolbox
+ *
+ * @param {string} toolDefinition
+ * Tool definition of the tool to build a tab for.
+ */
+ _buildTabForTool: function (toolDefinition) {
+ if (!toolDefinition.isTargetSupported(this._target)) {
+ return;
+ }
+
+ let tabs = this.doc.getElementById("toolbox-tabs");
+ let deck = this.doc.getElementById("toolbox-deck");
+
+ let id = toolDefinition.id;
+
+ if (toolDefinition.ordinal == undefined || toolDefinition.ordinal < 0) {
+ toolDefinition.ordinal = MAX_ORDINAL;
+ }
+
+ let radio = this.doc.createElement("radio");
+ // The radio element is not being used in the conventional way, thus
+ // the devtools-tab class replaces the radio XBL binding with its base
+ // binding (the control-item binding).
+ radio.className = "devtools-tab";
+ radio.id = "toolbox-tab-" + id;
+ radio.setAttribute("toolid", id);
+ radio.setAttribute("tabindex", "0");
+ radio.setAttribute("ordinal", toolDefinition.ordinal);
+ radio.setAttribute("tooltiptext", toolDefinition.tooltip);
+ if (toolDefinition.invertIconForLightTheme) {
+ radio.setAttribute("icon-invertable", "light-theme");
+ } else if (toolDefinition.invertIconForDarkTheme) {
+ radio.setAttribute("icon-invertable", "dark-theme");
+ }
+
+ radio.addEventListener("command", this.selectTool.bind(this, id));
+
+ // spacer lets us center the image and label, while allowing cropping
+ let spacer = this.doc.createElement("spacer");
+ spacer.setAttribute("flex", "1");
+ radio.appendChild(spacer);
+
+ if (toolDefinition.icon) {
+ let image = this.doc.createElement("image");
+ image.className = "default-icon";
+ image.setAttribute("src",
+ toolDefinition.icon || toolDefinition.highlightedicon);
+ radio.appendChild(image);
+ // Adding the highlighted icon image
+ image = this.doc.createElement("image");
+ image.className = "highlighted-icon";
+ image.setAttribute("src",
+ toolDefinition.highlightedicon || toolDefinition.icon);
+ radio.appendChild(image);
+ }
+
+ if (toolDefinition.label && !toolDefinition.iconOnly) {
+ let label = this.doc.createElement("label");
+ label.setAttribute("value", toolDefinition.label);
+ label.setAttribute("crop", "end");
+ label.setAttribute("flex", "1");
+ radio.appendChild(label);
+ }
+
+ if (!toolDefinition.bgTheme) {
+ toolDefinition.bgTheme = "theme-toolbar";
+ }
+ let vbox = this.doc.createElement("vbox");
+ vbox.className = "toolbox-panel " + toolDefinition.bgTheme;
+
+ // There is already a container for the webconsole frame.
+ if (!this.doc.getElementById("toolbox-panel-" + id)) {
+ vbox.id = "toolbox-panel-" + id;
+ }
+
+ if (id === "options") {
+ // Options panel is special. It doesn't belong in the same container as
+ // the other tabs.
+ radio.setAttribute("role", "button");
+ let optionTabContainer = this.doc.getElementById("toolbox-option-container");
+ optionTabContainer.appendChild(radio);
+ deck.appendChild(vbox);
+ } else {
+ radio.setAttribute("role", "tab");
+
+ // If there is no tab yet, or the ordinal to be added is the largest one.
+ if (tabs.childNodes.length == 0 ||
+ tabs.lastChild.getAttribute("ordinal") <= toolDefinition.ordinal) {
+ tabs.appendChild(radio);
+ deck.appendChild(vbox);
+ } else {
+ // else, iterate over all the tabs to get the correct location.
+ Array.some(tabs.childNodes, (node, i) => {
+ if (+node.getAttribute("ordinal") > toolDefinition.ordinal) {
+ tabs.insertBefore(radio, node);
+ deck.insertBefore(vbox, deck.childNodes[i]);
+ return true;
+ }
+ return false;
+ });
+ }
+ }
+
+ this._addKeysToWindow();
+ },
+
+ /**
+ * Ensure the tool with the given id is loaded.
+ *
+ * @param {string} id
+ * The id of the tool to load.
+ */
+ loadTool: function (id) {
+ if (id === "inspector" && !this._inspector) {
+ return this.initInspector().then(() => {
+ return this.loadTool(id);
+ });
+ }
+
+ let deferred = defer();
+ let iframe = this.doc.getElementById("toolbox-panel-iframe-" + id);
+
+ if (iframe) {
+ let panel = this._toolPanels.get(id);
+ if (panel) {
+ deferred.resolve(panel);
+ } else {
+ this.once(id + "-ready", initializedPanel => {
+ deferred.resolve(initializedPanel);
+ });
+ }
+ return deferred.promise;
+ }
+
+ let definition = gDevTools.getToolDefinition(id);
+ if (!definition) {
+ deferred.reject(new Error("no such tool id " + id));
+ return deferred.promise;
+ }
+
+ iframe = this.doc.createElement("iframe");
+ iframe.className = "toolbox-panel-iframe";
+ iframe.id = "toolbox-panel-iframe-" + id;
+ iframe.setAttribute("flex", 1);
+ iframe.setAttribute("forceOwnRefreshDriver", "");
+ iframe.tooltip = "aHTMLTooltip";
+ iframe.style.visibility = "hidden";
+
+ gDevTools.emit(id + "-init", this, iframe);
+ this.emit(id + "-init", iframe);
+
+ // If no parent yet, append the frame into default location.
+ if (!iframe.parentNode) {
+ let vbox = this.doc.getElementById("toolbox-panel-" + id);
+ vbox.appendChild(iframe);
+ }
+
+ let onLoad = () => {
+ // Prevent flicker while loading by waiting to make visible until now.
+ iframe.style.visibility = "visible";
+
+ // Try to set the dir attribute as early as possible.
+ this.setIframeDocumentDir(iframe);
+
+ // The build method should return a panel instance, so events can
+ // be fired with the panel as an argument. However, in order to keep
+ // backward compatibility with existing extensions do a check
+ // for a promise return value.
+ let built = definition.build(iframe.contentWindow, this);
+
+ if (!(typeof built.then == "function")) {
+ let panel = built;
+ iframe.panel = panel;
+
+ // The panel instance is expected to fire (and listen to) various
+ // framework events, so make sure it's properly decorated with
+ // appropriate API (on, off, once, emit).
+ // In this case we decorate panel instances directly returned by
+ // the tool definition 'build' method.
+ if (typeof panel.emit == "undefined") {
+ EventEmitter.decorate(panel);
+ }
+
+ gDevTools.emit(id + "-build", this, panel);
+ this.emit(id + "-build", panel);
+
+ // The panel can implement an 'open' method for asynchronous
+ // initialization sequence.
+ if (typeof panel.open == "function") {
+ built = panel.open();
+ } else {
+ let buildDeferred = defer();
+ buildDeferred.resolve(panel);
+ built = buildDeferred.promise;
+ }
+ }
+
+ // Wait till the panel is fully ready and fire 'ready' events.
+ promise.resolve(built).then((panel) => {
+ this._toolPanels.set(id, panel);
+
+ // Make sure to decorate panel object with event API also in case
+ // where the tool definition 'build' method returns only a promise
+ // and the actual panel instance is available as soon as the
+ // promise is resolved.
+ if (typeof panel.emit == "undefined") {
+ EventEmitter.decorate(panel);
+ }
+
+ gDevTools.emit(id + "-ready", this, panel);
+ this.emit(id + "-ready", panel);
+
+ deferred.resolve(panel);
+ }, console.error);
+ };
+
+ iframe.setAttribute("src", definition.url);
+ if (definition.panelLabel) {
+ iframe.setAttribute("aria-label", definition.panelLabel);
+ }
+
+ // Depending on the host, iframe.contentWindow is not always
+ // defined at this moment. If it is not defined, we use an
+ // event listener on the iframe DOM node. If it's defined,
+ // we use the chromeEventHandler. We can't use a listener
+ // on the DOM node every time because this won't work
+ // if the (xul chrome) iframe is loaded in a content docshell.
+ if (iframe.contentWindow) {
+ let domHelper = new DOMHelpers(iframe.contentWindow);
+ domHelper.onceDOMReady(onLoad);
+ } else {
+ let callback = () => {
+ iframe.removeEventListener("DOMContentLoaded", callback);
+ onLoad();
+ };
+
+ iframe.addEventListener("DOMContentLoaded", callback);
+ }
+
+ return deferred.promise;
+ },
+
+ /**
+ * Set the dir attribute on the content document element of the provided iframe.
+ *
+ * @param {IFrameElement} iframe
+ */
+ setIframeDocumentDir: function (iframe) {
+ let docEl = iframe.contentWindow && iframe.contentWindow.document.documentElement;
+ if (!docEl || docEl.namespaceURI !== HTML_NS) {
+ // Bail out if the content window or document is not ready or if the document is not
+ // HTML.
+ return;
+ }
+
+ if (docEl.hasAttribute("dir")) {
+ // Set the dir attribute value only if dir is already present on the document.
+ let top = this.win.top;
+ let topDocEl = top.document.documentElement;
+ let isRtl = top.getComputedStyle(topDocEl).direction === "rtl";
+ docEl.setAttribute("dir", isRtl ? "rtl" : "ltr");
+ }
+ },
+
+ /**
+ * Mark all in collection as unselected; and id as selected
+ * @param {string} collection
+ * DOM collection of items
+ * @param {string} id
+ * The Id of the item within the collection to select
+ */
+ selectSingleNode: function (collection, id) {
+ [...collection].forEach(node => {
+ if (node.id === id) {
+ node.setAttribute("selected", "true");
+ node.setAttribute("aria-selected", "true");
+ } else {
+ node.removeAttribute("selected");
+ node.removeAttribute("aria-selected");
+ }
+ });
+ },
+
+ /**
+ * Switch to the tool with the given id
+ *
+ * @param {string} id
+ * The id of the tool to switch to
+ */
+ selectTool: function (id) {
+ this.emit("before-select", id);
+
+ let tabs = this.doc.querySelectorAll(".devtools-tab");
+ this.selectSingleNode(tabs, "toolbox-tab-" + id);
+
+ // If options is selected, the separator between it and the
+ // command buttons should be hidden.
+ let sep = this.doc.getElementById("toolbox-controls-separator");
+ if (id === "options") {
+ sep.setAttribute("invisible", "true");
+ } else {
+ sep.removeAttribute("invisible");
+ }
+
+ if (this.currentToolId == id) {
+ let panel = this._toolPanels.get(id);
+ if (panel) {
+ // We have a panel instance, so the tool is already fully loaded.
+
+ // re-focus tool to get key events again
+ this.focusTool(id);
+
+ // Return the existing panel in order to have a consistent return value.
+ return promise.resolve(panel);
+ }
+ // Otherwise, if there is no panel instance, it is still loading,
+ // so we are racing another call to selectTool with the same id.
+ return this.once("select").then(() => promise.resolve(this._toolPanels.get(id)));
+ }
+
+ if (!this.isReady) {
+ throw new Error("Can't select tool, wait for toolbox 'ready' event");
+ }
+
+ let tab = this.doc.getElementById("toolbox-tab-" + id);
+
+ if (tab) {
+ if (this.currentToolId) {
+ this._telemetry.toolClosed(this.currentToolId);
+ }
+ this._telemetry.toolOpened(id);
+ } else {
+ throw new Error("No tool found");
+ }
+
+ let tabstrip = this.doc.getElementById("toolbox-tabs");
+
+ // select the right tab, making 0th index the default tab if right tab not
+ // found.
+ tabstrip.selectedItem = tab || tabstrip.childNodes[0];
+
+ // and select the right iframe
+ let toolboxPanels = this.doc.querySelectorAll(".toolbox-panel");
+ this.selectSingleNode(toolboxPanels, "toolbox-panel-" + id);
+
+ this.lastUsedToolId = this.currentToolId;
+ this.currentToolId = id;
+ this._refreshConsoleDisplay();
+ if (id != "options") {
+ Services.prefs.setCharPref(this._prefs.LAST_TOOL, id);
+ }
+
+ return this.loadTool(id).then(panel => {
+ // focus the tool's frame to start receiving key events
+ this.focusTool(id);
+
+ this.emit("select", id);
+ this.emit(id + "-selected", panel);
+ return panel;
+ });
+ },
+
+ /**
+ * Focus a tool's panel by id
+ * @param {string} id
+ * The id of tool to focus
+ */
+ focusTool: function (id, state = true) {
+ let iframe = this.doc.getElementById("toolbox-panel-iframe-" + id);
+
+ if (state) {
+ iframe.focus();
+ } else {
+ iframe.blur();
+ }
+ },
+
+ /**
+ * Focus split console's input line
+ */
+ focusConsoleInput: function () {
+ let consolePanel = this.getPanel("webconsole");
+ if (consolePanel) {
+ consolePanel.focusInput();
+ }
+ },
+
+ /**
+ * If the console is split and we are focusing an element outside
+ * of the console, then store the newly focused element, so that
+ * it can be restored once the split console closes.
+ */
+ _onFocus: function ({originalTarget}) {
+ // Ignore any non element nodes, or any elements contained
+ // within the webconsole frame.
+ let webconsoleURL = gDevTools.getToolDefinition("webconsole").url;
+ if (originalTarget.nodeType !== 1 ||
+ originalTarget.baseURI === webconsoleURL) {
+ return;
+ }
+
+ this._lastFocusedElement = originalTarget;
+ },
+
+ /**
+ * Opens the split console.
+ *
+ * @returns {Promise} a promise that resolves once the tool has been
+ * loaded and focused.
+ */
+ openSplitConsole: function () {
+ this._splitConsole = true;
+ Services.prefs.setBoolPref(SPLITCONSOLE_ENABLED_PREF, true);
+ this._refreshConsoleDisplay();
+ this.emit("split-console");
+
+ return this.loadTool("webconsole").then(() => {
+ this.focusConsoleInput();
+ });
+ },
+
+ /**
+ * Closes the split console.
+ *
+ * @returns {Promise} a promise that resolves once the tool has been
+ * closed.
+ */
+ closeSplitConsole: function () {
+ this._splitConsole = false;
+ Services.prefs.setBoolPref(SPLITCONSOLE_ENABLED_PREF, false);
+ this._refreshConsoleDisplay();
+ this.emit("split-console");
+
+ if (this._lastFocusedElement) {
+ this._lastFocusedElement.focus();
+ }
+ return promise.resolve();
+ },
+
+ /**
+ * Toggles the split state of the webconsole. If the webconsole panel
+ * is already selected then this command is ignored.
+ *
+ * @returns {Promise} a promise that resolves once the tool has been
+ * opened or closed.
+ */
+ toggleSplitConsole: function () {
+ if (this.currentToolId !== "webconsole") {
+ return this.splitConsole ?
+ this.closeSplitConsole() :
+ this.openSplitConsole();
+ }
+
+ return promise.resolve();
+ },
+
+ /**
+ * Tells the target tab to reload.
+ */
+ reloadTarget: function (force) {
+ this.target.activeTab.reload({ force: force });
+ },
+
+ /**
+ * Loads the tool next to the currently selected tool.
+ */
+ selectNextTool: function () {
+ let tools = this.doc.querySelectorAll(".devtools-tab");
+ let selected = this.doc.querySelector(".devtools-tab[selected]");
+ let nextIndex = [...tools].indexOf(selected) + 1;
+ let next = tools[nextIndex] || tools[0];
+ let tool = next.getAttribute("toolid");
+ return this.selectTool(tool);
+ },
+
+ /**
+ * Loads the tool just left to the currently selected tool.
+ */
+ selectPreviousTool: function () {
+ let tools = this.doc.querySelectorAll(".devtools-tab");
+ let selected = this.doc.querySelector(".devtools-tab[selected]");
+ let prevIndex = [...tools].indexOf(selected) - 1;
+ let prev = tools[prevIndex] || tools[tools.length - 1];
+ let tool = prev.getAttribute("toolid");
+ return this.selectTool(tool);
+ },
+
+ /**
+ * Highlights the tool's tab if it is not the currently selected tool.
+ *
+ * @param {string} id
+ * The id of the tool to highlight
+ */
+ highlightTool: function (id) {
+ let tab = this.doc.getElementById("toolbox-tab-" + id);
+ tab && tab.setAttribute("highlighted", "true");
+ },
+
+ /**
+ * De-highlights the tool's tab.
+ *
+ * @param {string} id
+ * The id of the tool to unhighlight
+ */
+ unhighlightTool: function (id) {
+ let tab = this.doc.getElementById("toolbox-tab-" + id);
+ tab && tab.removeAttribute("highlighted");
+ },
+
+ /**
+ * Raise the toolbox host.
+ */
+ raise: function () {
+ this.postMessage({
+ name: "raise-host"
+ });
+ },
+
+ /**
+ * Refresh the host's title.
+ */
+ _refreshHostTitle: function () {
+ let title;
+ if (this.target.name && this.target.name != this.target.url) {
+ title = L10N.getFormatStr("toolbox.titleTemplate2", this.target.name,
+ this.target.url);
+ } else {
+ title = L10N.getFormatStr("toolbox.titleTemplate1", this.target.url);
+ }
+ this.postMessage({
+ name: "set-host-title",
+ title
+ });
+ },
+
+ // Returns an instance of the preference actor
+ get _preferenceFront() {
+ return this.target.root.then(rootForm => {
+ return getPreferenceFront(this.target.client, rootForm);
+ });
+ },
+
+ _toggleAutohide: Task.async(function* () {
+ let prefName = "ui.popup.disable_autohide";
+ let front = yield this._preferenceFront;
+ let current = yield front.getBoolPref(prefName);
+ yield front.setBoolPref(prefName, !current);
+
+ this._updateNoautohideButton();
+ }),
+
+ _updateNoautohideButton: Task.async(function* () {
+ let menu = this.doc.getElementById("command-button-noautohide");
+ if (menu.getAttribute("hidden") === "true") {
+ return;
+ }
+ if (!this.target.root) {
+ return;
+ }
+ let prefName = "ui.popup.disable_autohide";
+ let front = yield this._preferenceFront;
+ let current = yield front.getBoolPref(prefName);
+ if (current) {
+ menu.setAttribute("checked", "true");
+ } else {
+ menu.removeAttribute("checked");
+ }
+ }),
+
+ _listFrames: function (event) {
+ if (!this._target.activeTab || !this._target.activeTab.traits.frames) {
+ // We are not targetting a regular TabActor
+ // it can be either an addon or browser toolbox actor
+ return promise.resolve();
+ }
+ let packet = {
+ to: this._target.form.actor,
+ type: "listFrames"
+ };
+ return this._target.client.request(packet, resp => {
+ this._updateFrames(null, { frames: resp.frames });
+ });
+ },
+
+ /**
+ * Show a drop down menu that allows the user to switch frames.
+ */
+ showFramesMenu: function (event) {
+ let menu = new Menu();
+ let target = event.target;
+
+ // Generate list of menu items from the list of frames.
+ this.frameMap.forEach(frame => {
+ // A frame is checked if it's the selected one.
+ let checked = frame.id == this.selectedFrameId;
+
+ // Create menu item.
+ menu.append(new MenuItem({
+ label: frame.url,
+ type: "radio",
+ checked,
+ click: () => {
+ this.onSelectFrame(frame.id);
+ }
+ }));
+ });
+
+ menu.once("open").then(() => {
+ target.setAttribute("open", "true");
+ });
+
+ menu.once("close").then(() => {
+ target.removeAttribute("open");
+ });
+
+ // Show a drop down menu with frames.
+ // XXX Missing menu API for specifying target (anchor)
+ // and relative position to it. See also:
+ // https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL/Method/openPopup
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1274551
+ let rect = target.getBoundingClientRect();
+ let screenX = target.ownerDocument.defaultView.mozInnerScreenX;
+ let screenY = target.ownerDocument.defaultView.mozInnerScreenY;
+ menu.popup(rect.left + screenX, rect.bottom + screenY, this);
+
+ return menu;
+ },
+
+ /**
+ * Select a frame by sending 'switchToFrame' packet to the backend.
+ */
+ onSelectFrame: function (frameId) {
+ // Send packet to the backend to select specified frame and
+ // wait for 'frameUpdate' event packet to update the UI.
+ let packet = {
+ to: this._target.form.actor,
+ type: "switchToFrame",
+ windowId: frameId
+ };
+ this._target.client.request(packet);
+ },
+
+ /**
+ * A handler for 'frameUpdate' packets received from the backend.
+ * Following properties might be set on the packet:
+ *
+ * destroyAll {Boolean}: All frames have been destroyed.
+ * selected {Number}: A frame has been selected
+ * frames {Array}: list of frames. Every frame can have:
+ * id {Number}: frame ID
+ * url {String}: frame URL
+ * title {String}: frame title
+ * destroy {Boolean}: Set to true if destroyed
+ * parentID {Number}: ID of the parent frame (not set
+ * for top level window)
+ */
+ _updateFrames: function (event, data) {
+ if (!Services.prefs.getBoolPref("devtools.command-button-frames.enabled")) {
+ return;
+ }
+
+ // We may receive this event before the toolbox is ready.
+ if (!this.isReady) {
+ return;
+ }
+
+ // Store (synchronize) data about all existing frames on the backend
+ if (data.destroyAll) {
+ this.frameMap.clear();
+ this.selectedFrameId = null;
+ } else if (data.selected) {
+ this.selectedFrameId = data.selected;
+ } else if (data.frames) {
+ data.frames.forEach(frame => {
+ if (frame.destroy) {
+ this.frameMap.delete(frame.id);
+
+ // Reset the currently selected frame if it's destroyed.
+ if (this.selectedFrameId == frame.id) {
+ this.selectedFrameId = null;
+ }
+ } else {
+ this.frameMap.set(frame.id, frame);
+ }
+ });
+ }
+
+ // If there is no selected frame select the first top level
+ // frame by default. Note that there might be more top level
+ // frames in case of the BrowserToolbox.
+ if (!this.selectedFrameId) {
+ let frames = [...this.frameMap.values()];
+ let topFrames = frames.filter(frame => !frame.parentID);
+ this.selectedFrameId = topFrames.length ? topFrames[0].id : null;
+ }
+
+ // Check out whether top frame is currently selected.
+ // Note that only child frame has parentID.
+ let frame = this.frameMap.get(this.selectedFrameId);
+ let topFrameSelected = frame ? !frame.parentID : false;
+ let button = this.doc.getElementById("command-button-frames");
+ button.removeAttribute("checked");
+
+ // If non-top level frame is selected the toolbar button is
+ // marked as 'checked' indicating that a child frame is active.
+ if (!topFrameSelected && this.selectedFrameId) {
+ button.setAttribute("checked", "true");
+ }
+ },
+
+ /**
+ * Switch to the last used host for the toolbox UI.
+ */
+ switchToPreviousHost: function () {
+ return this.switchHost("previous");
+ },
+
+ /**
+ * Switch to a new host for the toolbox UI. E.g. bottom, sidebar, window,
+ * and focus the window when done.
+ *
+ * @param {string} hostType
+ * The host type of the new host object
+ */
+ switchHost: function (hostType) {
+ if (hostType == this.hostType || !this._target.isLocalTab) {
+ return null;
+ }
+
+ this.emit("host-will-change", hostType);
+
+ // ToolboxHostManager is going to call swapFrameLoaders which mess up with
+ // focus. We have to blur before calling it in order to be able to restore
+ // the focus after, in _onSwitchedHost.
+ this.focusTool(this.currentToolId, false);
+
+ // Host code on the chrome side will send back a message once the host
+ // switched
+ this.postMessage({
+ name: "switch-host",
+ hostType
+ });
+
+ return this.once("host-changed");
+ },
+
+ _onSwitchedHost: function ({ hostType }) {
+ this._hostType = hostType;
+
+ this._buildDockButtons();
+ this._addKeysToWindow();
+
+ // We blurred the tools at start of switchHost, but also when clicking on
+ // host switching button. We now have to restore the focus.
+ this.focusTool(this.currentToolId, true);
+
+ this.emit("host-changed");
+ this._telemetry.log(HOST_HISTOGRAM, this._getTelemetryHostId());
+ },
+
+ /**
+ * Return if the tool is available as a tab (i.e. if it's checked
+ * in the options panel). This is different from Toolbox.getPanel -
+ * a tool could be registered but not yet opened in which case
+ * isToolRegistered would return true but getPanel would return false.
+ */
+ isToolRegistered: function (toolId) {
+ return gDevTools.getToolDefinitionMap().has(toolId);
+ },
+
+ /**
+ * Handler for the tool-registered event.
+ * @param {string} event
+ * Name of the event ("tool-registered")
+ * @param {string} toolId
+ * Id of the tool that was registered
+ */
+ _toolRegistered: function (event, toolId) {
+ let tool = gDevTools.getToolDefinition(toolId);
+ this._buildTabForTool(tool);
+ // Emit the event so tools can listen to it from the toolbox level
+ // instead of gDevTools
+ this.emit("tool-registered", toolId);
+ },
+
+ /**
+ * Handler for the tool-unregistered event.
+ * @param {string} event
+ * Name of the event ("tool-unregistered")
+ * @param {string|object} toolId
+ * Definition or id of the tool that was unregistered. Passing the
+ * tool id should be avoided as it is a temporary measure.
+ */
+ _toolUnregistered: function (event, toolId) {
+ if (typeof toolId != "string") {
+ toolId = toolId.id;
+ }
+
+ if (this._toolPanels.has(toolId)) {
+ let instance = this._toolPanels.get(toolId);
+ instance.destroy();
+ this._toolPanels.delete(toolId);
+ }
+
+ let radio = this.doc.getElementById("toolbox-tab-" + toolId);
+ let panel = this.doc.getElementById("toolbox-panel-" + toolId);
+
+ if (radio) {
+ if (this.currentToolId == toolId) {
+ let nextToolName = null;
+ if (radio.nextSibling) {
+ nextToolName = radio.nextSibling.getAttribute("toolid");
+ }
+ if (radio.previousSibling) {
+ nextToolName = radio.previousSibling.getAttribute("toolid");
+ }
+ if (nextToolName) {
+ this.selectTool(nextToolName);
+ }
+ }
+ radio.parentNode.removeChild(radio);
+ }
+
+ if (panel) {
+ panel.parentNode.removeChild(panel);
+ }
+
+ if (this.hostType == Toolbox.HostType.WINDOW) {
+ let doc = this.win.parent.document;
+ let key = doc.getElementById("key_" + toolId);
+ if (key) {
+ key.parentNode.removeChild(key);
+ }
+ }
+ // Emit the event so tools can listen to it from the toolbox level
+ // instead of gDevTools
+ this.emit("tool-unregistered", toolId);
+ },
+
+ /**
+ * Initialize the inspector/walker/selection/highlighter fronts.
+ * Returns a promise that resolves when the fronts are initialized
+ */
+ initInspector: function () {
+ if (!this._initInspector) {
+ this._initInspector = Task.spawn(function* () {
+ this._inspector = InspectorFront(this._target.client, this._target.form);
+ let pref = "devtools.inspector.showAllAnonymousContent";
+ let showAllAnonymousContent = Services.prefs.getBoolPref(pref);
+ this._walker = yield this._inspector.getWalker({ showAllAnonymousContent });
+ this._selection = new Selection(this._walker);
+
+ if (this.highlighterUtils.isRemoteHighlightable()) {
+ this.walker.on("highlighter-ready", this._highlighterReady);
+ this.walker.on("highlighter-hide", this._highlighterHidden);
+
+ let autohide = !flags.testing;
+ this._highlighter = yield this._inspector.getHighlighter(autohide);
+ }
+ }.bind(this));
+ }
+ return this._initInspector;
+ },
+
+ /**
+ * Destroy the inspector/walker/selection fronts
+ * Returns a promise that resolves when the fronts are destroyed
+ */
+ destroyInspector: function () {
+ if (this._destroyingInspector) {
+ return this._destroyingInspector;
+ }
+
+ this._destroyingInspector = Task.spawn(function* () {
+ if (!this._inspector) {
+ return;
+ }
+
+ // Releasing the walker (if it has been created)
+ // This can fail, but in any case, we want to continue destroying the
+ // inspector/highlighter/selection
+ // FF42+: Inspector actor starts managing Walker actor and auto destroy it.
+ if (this._walker && !this.walker.traits.autoReleased) {
+ try {
+ yield this._walker.release();
+ } catch (e) {
+ // Do nothing;
+ }
+ }
+
+ yield this.highlighterUtils.stopPicker();
+ yield this._inspector.destroy();
+ if (this._highlighter) {
+ // Note that if the toolbox is closed, this will work fine, but will fail
+ // in case the browser is closed and will trigger a noSuchActor message.
+ // We ignore the promise that |_hideBoxModel| returns, since we should still
+ // proceed with the rest of destruction if it fails.
+ // FF42+ now does the cleanup from the actor.
+ if (!this.highlighter.traits.autoHideOnDestroy) {
+ this.highlighterUtils.unhighlight();
+ }
+ yield this._highlighter.destroy();
+ }
+ if (this._selection) {
+ this._selection.destroy();
+ }
+
+ if (this.walker) {
+ this.walker.off("highlighter-ready", this._highlighterReady);
+ this.walker.off("highlighter-hide", this._highlighterHidden);
+ }
+
+ this._inspector = null;
+ this._highlighter = null;
+ this._selection = null;
+ this._walker = null;
+ }.bind(this));
+ return this._destroyingInspector;
+ },
+
+ /**
+ * Get the toolbox's notification component
+ *
+ * @return The notification box component.
+ */
+ getNotificationBox: function () {
+ return this.notificationBox;
+ },
+
+ /**
+ * Remove all UI elements, detach from target and clear up
+ */
+ destroy: function () {
+ // If several things call destroy then we give them all the same
+ // destruction promise so we're sure to destroy only once
+ if (this._destroyer) {
+ return this._destroyer;
+ }
+ let deferred = defer();
+ this._destroyer = deferred.promise;
+
+ this.emit("destroy");
+
+ this._target.off("navigate", this._refreshHostTitle);
+ this._target.off("frame-update", this._updateFrames);
+ this.off("select", this._refreshHostTitle);
+ this.off("host-changed", this._refreshHostTitle);
+ this.off("ready", this._showDevEditionPromo);
+
+ gDevTools.off("tool-registered", this._toolRegistered);
+ gDevTools.off("tool-unregistered", this._toolUnregistered);
+
+ gDevTools.off("pref-changed", this._prefChanged);
+
+ this._lastFocusedElement = null;
+ if (this._sourceMapService) {
+ this._sourceMapService.destroy();
+ this._sourceMapService = null;
+ }
+
+ if (this.webconsolePanel) {
+ this._saveSplitConsoleHeight();
+ this.webconsolePanel.removeEventListener("resize",
+ this._saveSplitConsoleHeight);
+ this.webconsolePanel = null;
+ }
+ if (this.closeButton) {
+ this.closeButton.removeEventListener("click", this.destroy, true);
+ this.closeButton = null;
+ }
+ if (this.textBoxContextMenuPopup) {
+ this.textBoxContextMenuPopup.removeEventListener("popupshowing",
+ this._updateTextBoxMenuItems, true);
+ this.textBoxContextMenuPopup = null;
+ }
+ if (this.tabbar) {
+ this.tabbar.removeEventListener("focus", this._onTabbarFocus, true);
+ this.tabbar.removeEventListener("click", this._onTabbarFocus, true);
+ this.tabbar.removeEventListener("keypress", this._onTabbarArrowKeypress);
+ this.tabbar = null;
+ }
+
+ let outstanding = [];
+ for (let [id, panel] of this._toolPanels) {
+ try {
+ gDevTools.emit(id + "-destroy", this, panel);
+ this.emit(id + "-destroy", panel);
+
+ outstanding.push(panel.destroy());
+ } catch (e) {
+ // We don't want to stop here if any panel fail to close.
+ console.error("Panel " + id + ":", e);
+ }
+ }
+
+ this.browserRequire = null;
+
+ // Now that we are closing the toolbox we can re-enable the cache settings
+ // and disable the service workers testing settings for the current tab.
+ // FF41+ automatically cleans up state in actor on disconnect.
+ if (this.target.activeTab && !this.target.activeTab.traits.noTabReconfigureOnClose) {
+ this.target.activeTab.reconfigure({
+ "cacheDisabled": false,
+ "serviceWorkersTestingEnabled": false
+ });
+ }
+
+ // Destroying the walker and inspector fronts
+ outstanding.push(this.destroyInspector().then(() => {
+ // Removing buttons
+ if (this._pickerButton) {
+ this._pickerButton.removeEventListener("click", this._togglePicker, false);
+ this._pickerButton = null;
+ }
+ }));
+
+ // Destroy the profiler connection
+ outstanding.push(this.destroyPerformance());
+
+ // Detach the thread
+ detachThread(this._threadClient);
+ this._threadClient = null;
+
+ // We need to grab a reference to win before this._host is destroyed.
+ let win = this.win;
+
+ if (this._requisition) {
+ CommandUtils.destroyRequisition(this._requisition, this.target);
+ }
+ this._telemetry.toolClosed("toolbox");
+ this._telemetry.destroy();
+
+ // Finish all outstanding tasks (which means finish destroying panels and
+ // then destroying the host, successfully or not) before destroying the
+ // target.
+ deferred.resolve(settleAll(outstanding)
+ .catch(console.error)
+ .then(() => {
+ this._removeHostListeners();
+
+ // `location` may already be null if the toolbox document is already
+ // in process of destruction. Otherwise if it is still around, ensure
+ // releasing toolbox document and triggering cleanup thanks to unload
+ // event. We do that precisely here, before nullifying the target as
+ // various cleanup code depends on the target attribute to be still
+ // defined.
+ if (win.location) {
+ win.location.replace("about:blank");
+ }
+
+ // Targets need to be notified that the toolbox is being torn down.
+ // This is done after other destruction tasks since it may tear down
+ // fronts and the debugger transport which earlier destroy methods may
+ // require to complete.
+ if (!this._target) {
+ return null;
+ }
+ let target = this._target;
+ this._target = null;
+ this.highlighterUtils.release();
+ target.off("close", this.destroy);
+ return target.destroy();
+ }, console.error).then(() => {
+ this.emit("destroyed");
+
+ // Free _host after the call to destroyed in order to let a chance
+ // to destroyed listeners to still query toolbox attributes
+ this._host = null;
+ this._win = null;
+ this._toolPanels.clear();
+
+ // Force GC to prevent long GC pauses when running tests and to free up
+ // memory in general when the toolbox is closed.
+ if (flags.testing) {
+ win.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils)
+ .garbageCollect();
+ }
+ }).then(null, console.error));
+
+ let leakCheckObserver = ({wrappedJSObject: barrier}) => {
+ // Make the leak detector wait until this toolbox is properly destroyed.
+ barrier.client.addBlocker("DevTools: Wait until toolbox is destroyed",
+ this._destroyer);
+ };
+
+ let topic = "shutdown-leaks-before-check";
+ Services.obs.addObserver(leakCheckObserver, topic, false);
+ this._destroyer.then(() => {
+ Services.obs.removeObserver(leakCheckObserver, topic);
+ });
+
+ return this._destroyer;
+ },
+
+ _highlighterReady: function () {
+ this.emit("highlighter-ready");
+ },
+
+ _highlighterHidden: function () {
+ this.emit("highlighter-hide");
+ },
+
+ /**
+ * For displaying the promotional Doorhanger on first opening of
+ * the developer tools, promoting the Developer Edition.
+ */
+ _showDevEditionPromo: function () {
+ // Do not display in browser toolbox
+ if (this.target.chrome) {
+ return;
+ }
+ showDoorhanger({ window: this.win, type: "deveditionpromo" });
+ },
+
+ /**
+ * Enable / disable necessary textbox menu items using globalOverlay.js.
+ */
+ _updateTextBoxMenuItems: function () {
+ let window = this.win;
+ ["cmd_undo", "cmd_delete", "cmd_cut",
+ "cmd_copy", "cmd_paste", "cmd_selectAll"].forEach(window.goUpdateCommand);
+ },
+
+ /**
+ * Open the textbox context menu at given coordinates.
+ * Panels in the toolbox can call this on contextmenu events with event.screenX/Y
+ * instead of having to implement their own copy/paste/selectAll menu.
+ * @param {Number} x
+ * @param {Number} y
+ */
+ openTextBoxContextMenu: function (x, y) {
+ this.textBoxContextMenuPopup.openPopupAtScreen(x, y, true);
+ },
+
+ /**
+ * Connects to the SPS profiler when the developer tools are open. This is
+ * necessary because of the WebConsole's `profile` and `profileEnd` methods.
+ */
+ initPerformance: Task.async(function* () {
+ // If target does not have profiler actor (addons), do not
+ // even register the shared performance connection.
+ if (!this.target.hasActor("profiler")) {
+ return promise.resolve();
+ }
+
+ if (this._performanceFrontConnection) {
+ return this._performanceFrontConnection.promise;
+ }
+
+ this._performanceFrontConnection = defer();
+ this._performance = createPerformanceFront(this._target);
+ yield this.performance.connect();
+
+ // Emit an event when connected, but don't wait on startup for this.
+ this.emit("profiler-connected");
+
+ this.performance.on("*", this._onPerformanceFrontEvent);
+ this._performanceFrontConnection.resolve(this.performance);
+ return this._performanceFrontConnection.promise;
+ }),
+
+ /**
+ * Disconnects the underlying Performance actor. If the connection
+ * has not finished initializing, as opening a toolbox does not wait,
+ * the performance connection destroy method will wait for it on its own.
+ */
+ destroyPerformance: Task.async(function* () {
+ if (!this.performance) {
+ return;
+ }
+ // If still connecting to performance actor, allow the
+ // actor to resolve its connection before attempting to destroy.
+ if (this._performanceFrontConnection) {
+ yield this._performanceFrontConnection.promise;
+ }
+ this.performance.off("*", this._onPerformanceFrontEvent);
+ yield this.performance.destroy();
+ this._performance = null;
+ }),
+
+ /**
+ * Called when any event comes from the PerformanceFront. If the performance tool is
+ * already loaded when the first event comes in, immediately unbind this handler, as
+ * this is only used to queue up observed recordings before the performance tool can
+ * handle them, which will only occur when `console.profile()` recordings are started
+ * before the tool loads.
+ */
+ _onPerformanceFrontEvent: Task.async(function* (eventName, recording) {
+ if (this.getPanel("performance")) {
+ this.performance.off("*", this._onPerformanceFrontEvent);
+ return;
+ }
+
+ this._performanceQueuedRecordings = this._performanceQueuedRecordings || [];
+ let recordings = this._performanceQueuedRecordings;
+
+ // Before any console recordings, we'll get a `console-profile-start` event
+ // warning us that a recording will come later (via `recording-started`), so
+ // start to boot up the tool and populate the tool with any other recordings
+ // observed during that time.
+ if (eventName === "console-profile-start" && !this._performanceToolOpenedViaConsole) {
+ this._performanceToolOpenedViaConsole = this.loadTool("performance");
+ let panel = yield this._performanceToolOpenedViaConsole;
+ yield panel.open();
+
+ panel.panelWin.PerformanceController.populateWithRecordings(recordings);
+ this.performance.off("*", this._onPerformanceFrontEvent);
+ }
+
+ // Otherwise, if it's a recording-started event, we've already started loading
+ // the tool, so just store this recording in our array to be later populated
+ // once the tool loads.
+ if (eventName === "recording-started") {
+ recordings.push(recording);
+ }
+ }),
+
+ /**
+ * Returns gViewSourceUtils for viewing source.
+ */
+ get gViewSourceUtils() {
+ return this.win.gViewSourceUtils;
+ },
+
+ /**
+ * Opens source in style editor. Falls back to plain "view-source:".
+ * @see devtools/client/shared/source-utils.js
+ */
+ viewSourceInStyleEditor: function (sourceURL, sourceLine) {
+ return viewSource.viewSourceInStyleEditor(this, sourceURL, sourceLine);
+ },
+
+ /**
+ * Opens source in debugger. Falls back to plain "view-source:".
+ * @see devtools/client/shared/source-utils.js
+ */
+ viewSourceInDebugger: function (sourceURL, sourceLine) {
+ return viewSource.viewSourceInDebugger(this, sourceURL, sourceLine);
+ },
+
+ /**
+ * Opens source in scratchpad. Falls back to plain "view-source:".
+ * TODO The `sourceURL` for scratchpad instances are like `Scratchpad/1`.
+ * If instances are scoped one-per-browser-window, then we should be able
+ * to infer the URL from this toolbox, or use the built in scratchpad IN
+ * the toolbox.
+ *
+ * @see devtools/client/shared/source-utils.js
+ */
+ viewSourceInScratchpad: function (sourceURL, sourceLine) {
+ return viewSource.viewSourceInScratchpad(sourceURL, sourceLine);
+ },
+
+ /**
+ * Opens source in plain "view-source:".
+ * @see devtools/client/shared/source-utils.js
+ */
+ viewSource: function (sourceURL, sourceLine) {
+ return viewSource.viewSource(this, sourceURL, sourceLine);
+ },
+};
diff --git a/devtools/client/framework/toolbox.xul b/devtools/client/framework/toolbox.xul
new file mode 100644
index 000000000..94aaecebd
--- /dev/null
+++ b/devtools/client/framework/toolbox.xul
@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/skin/toolbox.css" type="text/css"?>
+<?xml-stylesheet href="resource://devtools/client/shared/components/notification-box.css" type="text/css"?>
+
+<?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?>
+
+<!DOCTYPE window [
+<!ENTITY % toolboxDTD SYSTEM "chrome://devtools/locale/toolbox.dtd" >
+%toolboxDTD;
+<!ENTITY % editMenuStrings SYSTEM "chrome://global/locale/editMenuOverlay.dtd">
+%editMenuStrings;
+<!ENTITY % globalKeysDTD SYSTEM "chrome://global/locale/globalKeys.dtd">
+%globalKeysDTD;
+]>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml">
+
+ <script type="application/javascript;version=1.8"
+ src="chrome://devtools/content/shared/theme-switching.js"/>
+ <script type="application/javascript"
+ src="chrome://global/content/viewSourceUtils.js"/>
+
+ <script type="application/javascript" src="chrome://global/content/globalOverlay.js"/>
+ <script type="application/javascript;version=1.8"
+ src="chrome://devtools/content/framework/toolbox-init.js"/>
+
+ <commandset id="editMenuCommands"/>
+ <keyset id="editMenuKeys"/>
+
+ <popupset>
+ <menupopup id="toolbox-textbox-context-popup">
+ <menuitem id="cMenu_undo"/>
+ <menuseparator/>
+ <menuitem id="cMenu_cut"/>
+ <menuitem id="cMenu_copy"/>
+ <menuitem id="cMenu_paste"/>
+ <menuitem id="cMenu_delete"/>
+ <menuseparator/>
+ <menuitem id="cMenu_selectAll"/>
+ </menupopup>
+ </popupset>
+
+ <vbox id="toolbox-container" flex="1">
+ <div xmlns="http://www.w3.org/1999/xhtml" id="toolbox-notificationbox"/>
+ <toolbar class="devtools-tabbar">
+ <hbox id="toolbox-picker-container" />
+ <hbox id="toolbox-tabs" flex="1" role="tablist" />
+ <hbox id="toolbox-buttons" pack="end">
+ <html:button id="command-button-frames"
+ class="command-button command-button-invertable devtools-button"
+ title="&toolboxFramesTooltip;"
+ hidden="true" />
+ <html:button id="command-button-noautohide"
+ class="command-button command-button-invertable devtools-button"
+ title="&toolboxNoAutoHideTooltip;"
+ hidden="true" />
+ </hbox>
+ <vbox id="toolbox-controls-separator" class="devtools-separator"/>
+ <hbox id="toolbox-option-container"/>
+ <hbox id="toolbox-controls">
+ <hbox id="toolbox-dock-buttons"/>
+ <html:button id="toolbox-close"
+ class="devtools-button"
+ title="&toolboxCloseButton.tooltip;"/>
+ </hbox>
+ </toolbar>
+ <vbox flex="1" class="theme-body">
+ <!-- Set large flex to allow the toolbox-panel-webconsole to have a
+ height set to a small value without flexing to fill up extra
+ space. There must be a flex on both to ensure that the console
+ panel itself is sized properly -->
+ <box id="toolbox-deck" flex="1000" minheight="75" />
+ <splitter id="toolbox-console-splitter" class="devtools-horizontal-splitter" hidden="true" />
+ <box minheight="75" flex="1" id="toolbox-panel-webconsole" collapsed="true" />
+ </vbox>
+ <tooltip id="aHTMLTooltip" page="true" />
+ </vbox>
+</window>
diff --git a/devtools/client/inspector/.eslintrc.js b/devtools/client/inspector/.eslintrc.js
new file mode 100644
index 000000000..6f5ff309c
--- /dev/null
+++ b/devtools/client/inspector/.eslintrc.js
@@ -0,0 +1,15 @@
+"use strict";
+
+module.exports = {
+ // Extend from the devtools eslintrc.
+ "extends": "../../.eslintrc.js",
+
+ "rules": {
+ // The inspector is being migrated to HTML and cleaned of
+ // chrome-privileged code, so this rule disallows requiring chrome
+ // code. Some files in the inspector disable this rule still. The
+ // goal is to enable the rule globally on all files.
+ /* eslint-disable max-len */
+ "mozilla/reject-some-requires": ["error", "^(chrome|chrome:.*|resource:.*|devtools/server/.*|.*\\.jsm|devtools/shared/platform/(chome|content)/.*)$"],
+ },
+};
diff --git a/devtools/client/inspector/breadcrumbs.js b/devtools/client/inspector/breadcrumbs.js
new file mode 100644
index 000000000..b2041164c
--- /dev/null
+++ b/devtools/client/inspector/breadcrumbs.js
@@ -0,0 +1,921 @@
+/* -*- 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 {ELLIPSIS} = require("devtools/shared/l10n");
+
+const MAX_LABEL_LENGTH = 40;
+
+const NS_XHTML = "http://www.w3.org/1999/xhtml";
+const SCROLL_REPEAT_MS = 100;
+
+const EventEmitter = require("devtools/shared/event-emitter");
+const {KeyShortcuts} = require("devtools/client/shared/key-shortcuts");
+
+// Some margin may be required for visible element detection.
+const SCROLL_MARGIN = 1;
+
+/**
+ * Component to replicate functionality of XUL arrowscrollbox
+ * for breadcrumbs
+ *
+ * @param {Window} win The window containing the breadcrumbs
+ * @parem {DOMNode} container The element in which to put the scroll box
+ */
+function ArrowScrollBox(win, container) {
+ this.win = win;
+ this.doc = win.document;
+ this.container = container;
+ EventEmitter.decorate(this);
+ this.init();
+}
+
+ArrowScrollBox.prototype = {
+
+ // Scroll behavior, exposed for testing
+ scrollBehavior: "smooth",
+
+ /**
+ * Build the HTML, add to the DOM and start listening to
+ * events
+ */
+ init: function () {
+ this.constructHtml();
+
+ this.onUnderflow();
+
+ this.onScroll = this.onScroll.bind(this);
+ this.onStartBtnClick = this.onStartBtnClick.bind(this);
+ this.onEndBtnClick = this.onEndBtnClick.bind(this);
+ this.onStartBtnDblClick = this.onStartBtnDblClick.bind(this);
+ this.onEndBtnDblClick = this.onEndBtnDblClick.bind(this);
+ this.onUnderflow = this.onUnderflow.bind(this);
+ this.onOverflow = this.onOverflow.bind(this);
+
+ this.inner.addEventListener("scroll", this.onScroll, false);
+ this.startBtn.addEventListener("mousedown", this.onStartBtnClick, false);
+ this.endBtn.addEventListener("mousedown", this.onEndBtnClick, false);
+ this.startBtn.addEventListener("dblclick", this.onStartBtnDblClick, false);
+ this.endBtn.addEventListener("dblclick", this.onEndBtnDblClick, false);
+
+ // Overflow and underflow are moz specific events
+ this.inner.addEventListener("underflow", this.onUnderflow, false);
+ this.inner.addEventListener("overflow", this.onOverflow, false);
+ },
+
+ /**
+ * Determine whether the current text directionality is RTL
+ */
+ isRtl: function () {
+ return this.win.getComputedStyle(this.container).direction === "rtl";
+ },
+
+ /**
+ * Scroll to the specified element using the current scroll behavior
+ * @param {Element} element element to scroll
+ * @param {String} block desired alignment of element after scrolling
+ */
+ scrollToElement: function (element, block) {
+ element.scrollIntoView({ block: block, behavior: this.scrollBehavior });
+ },
+
+ /**
+ * Call the given function once; then continuously
+ * while the mouse button is held
+ * @param {Function} repeatFn the function to repeat while the button is held
+ */
+ clickOrHold: function (repeatFn) {
+ let timer;
+ let container = this.container;
+
+ function handleClick() {
+ cancelHold();
+ repeatFn();
+ }
+
+ let window = this.win;
+ function cancelHold() {
+ window.clearTimeout(timer);
+ container.removeEventListener("mouseout", cancelHold, false);
+ container.removeEventListener("mouseup", handleClick, false);
+ }
+
+ function repeated() {
+ repeatFn();
+ timer = window.setTimeout(repeated, SCROLL_REPEAT_MS);
+ }
+
+ container.addEventListener("mouseout", cancelHold, false);
+ container.addEventListener("mouseup", handleClick, false);
+ timer = window.setTimeout(repeated, SCROLL_REPEAT_MS);
+ },
+
+ /**
+ * When start button is dbl clicked scroll to first element
+ */
+ onStartBtnDblClick: function () {
+ let children = this.inner.childNodes;
+ if (children.length < 1) {
+ return;
+ }
+
+ let element = this.inner.childNodes[0];
+ this.scrollToElement(element, "start");
+ },
+
+ /**
+ * When end button is dbl clicked scroll to last element
+ */
+ onEndBtnDblClick: function () {
+ let children = this.inner.childNodes;
+ if (children.length < 1) {
+ return;
+ }
+
+ let element = children[children.length - 1];
+ this.scrollToElement(element, "start");
+ },
+
+ /**
+ * When start arrow button is clicked scroll towards first element
+ */
+ onStartBtnClick: function () {
+ let scrollToStart = () => {
+ let element = this.getFirstInvisibleElement();
+ if (!element) {
+ return;
+ }
+
+ let block = this.isRtl() ? "end" : "start";
+ this.scrollToElement(element, block);
+ };
+
+ this.clickOrHold(scrollToStart);
+ },
+
+ /**
+ * When end arrow button is clicked scroll towards last element
+ */
+ onEndBtnClick: function () {
+ let scrollToEnd = () => {
+ let element = this.getLastInvisibleElement();
+ if (!element) {
+ return;
+ }
+
+ let block = this.isRtl() ? "start" : "end";
+ this.scrollToElement(element, block);
+ };
+
+ this.clickOrHold(scrollToEnd);
+ },
+
+ /**
+ * Event handler for scrolling, update the
+ * enabled/disabled status of the arrow buttons
+ */
+ onScroll: function () {
+ let first = this.getFirstInvisibleElement();
+ if (!first) {
+ this.startBtn.setAttribute("disabled", "true");
+ } else {
+ this.startBtn.removeAttribute("disabled");
+ }
+
+ let last = this.getLastInvisibleElement();
+ if (!last) {
+ this.endBtn.setAttribute("disabled", "true");
+ } else {
+ this.endBtn.removeAttribute("disabled");
+ }
+ },
+
+ /**
+ * On underflow, make the arrow buttons invisible
+ */
+ onUnderflow: function () {
+ this.startBtn.style.visibility = "collapse";
+ this.endBtn.style.visibility = "collapse";
+ this.emit("underflow");
+ },
+
+ /**
+ * On overflow, show the arrow buttons
+ */
+ onOverflow: function () {
+ this.startBtn.style.visibility = "visible";
+ this.endBtn.style.visibility = "visible";
+ this.emit("overflow");
+ },
+
+ /**
+ * Check whether the element is to the left of its container but does
+ * not also span the entire container.
+ * @param {Number} left the left scroll point of the container
+ * @param {Number} right the right edge of the container
+ * @param {Number} elementLeft the left edge of the element
+ * @param {Number} elementRight the right edge of the element
+ */
+ elementLeftOfContainer: function (left, right, elementLeft, elementRight) {
+ return elementLeft < (left - SCROLL_MARGIN)
+ && elementRight < (right - SCROLL_MARGIN);
+ },
+
+ /**
+ * Check whether the element is to the right of its container but does
+ * not also span the entire container.
+ * @param {Number} left the left scroll point of the container
+ * @param {Number} right the right edge of the container
+ * @param {Number} elementLeft the left edge of the element
+ * @param {Number} elementRight the right edge of the element
+ */
+ elementRightOfContainer: function (left, right, elementLeft, elementRight) {
+ return elementLeft > (left + SCROLL_MARGIN)
+ && elementRight > (right + SCROLL_MARGIN);
+ },
+
+ /**
+ * Get the first (i.e. furthest left for LTR)
+ * non or partly visible element in the scroll box
+ */
+ getFirstInvisibleElement: function () {
+ let elementsList = Array.from(this.inner.childNodes).reverse();
+
+ let predicate = this.isRtl() ?
+ this.elementRightOfContainer : this.elementLeftOfContainer;
+ return this.findFirstWithBounds(elementsList, predicate);
+ },
+
+ /**
+ * Get the last (i.e. furthest right for LTR)
+ * non or partly visible element in the scroll box
+ */
+ getLastInvisibleElement: function () {
+ let predicate = this.isRtl() ?
+ this.elementLeftOfContainer : this.elementRightOfContainer;
+ return this.findFirstWithBounds(this.inner.childNodes, predicate);
+ },
+
+ /**
+ * Find the first element that matches the given predicate, called with bounds
+ * information
+ * @param {Array} elements an ordered list of elements
+ * @param {Function} predicate a function to be called with bounds
+ * information
+ */
+ findFirstWithBounds: function (elements, predicate) {
+ let left = this.inner.scrollLeft;
+ let right = left + this.inner.clientWidth;
+ for (let element of elements) {
+ let elementLeft = element.offsetLeft - element.parentElement.offsetLeft;
+ let elementRight = elementLeft + element.offsetWidth;
+
+ // Check that the starting edge of the element is out of the visible area
+ // and that the ending edge does not span the whole container
+ if (predicate(left, right, elementLeft, elementRight)) {
+ return element;
+ }
+ }
+
+ return null;
+ },
+
+ /**
+ * Build the HTML for the scroll box and insert it into the DOM
+ */
+ constructHtml: function () {
+ this.startBtn = this.createElement("div", "scrollbutton-up",
+ this.container);
+ this.createElement("div", "toolbarbutton-icon", this.startBtn);
+
+ this.createElement("div", "arrowscrollbox-overflow-start-indicator",
+ this.container);
+ this.inner = this.createElement("div", "html-arrowscrollbox-inner",
+ this.container);
+ this.createElement("div", "arrowscrollbox-overflow-end-indicator",
+ this.container);
+
+ this.endBtn = this.createElement("div", "scrollbutton-down",
+ this.container);
+ this.createElement("div", "toolbarbutton-icon", this.endBtn);
+ },
+
+ /**
+ * Create an XHTML element with the given class name, and append it to the
+ * parent.
+ * @param {String} tagName name of the tag to create
+ * @param {String} className class of the element
+ * @param {DOMNode} parent the parent node to which it should be appended
+ * @return {DOMNode} The new element
+ */
+ createElement: function (tagName, className, parent) {
+ let el = this.doc.createElementNS(NS_XHTML, tagName);
+ el.className = className;
+ if (parent) {
+ parent.appendChild(el);
+ }
+
+ return el;
+ },
+
+ /**
+ * Remove event handlers and clean up
+ */
+ destroy: function () {
+ this.inner.removeEventListener("scroll", this.onScroll, false);
+ this.startBtn.removeEventListener("mousedown",
+ this.onStartBtnClick, false);
+ this.endBtn.removeEventListener("mousedown", this.onEndBtnClick, false);
+ this.startBtn.removeEventListener("dblclick",
+ this.onStartBtnDblClick, false);
+ this.endBtn.removeEventListener("dblclick",
+ this.onRightBtnDblClick, false);
+
+ // Overflow and underflow are moz specific events
+ this.inner.removeEventListener("underflow", this.onUnderflow, false);
+ this.inner.removeEventListener("overflow", this.onOverflow, false);
+ },
+};
+
+/**
+ * Display the ancestors of the current node and its children.
+ * Only one "branch" of children are displayed (only one line).
+ *
+ * Mechanism:
+ * - If no nodes displayed yet:
+ * then display the ancestor of the selected node and the selected node;
+ * else select the node;
+ * - If the selected node is the last node displayed, append its first (if any).
+ *
+ * @param {InspectorPanel} inspector The inspector hosting this widget.
+ */
+function HTMLBreadcrumbs(inspector) {
+ this.inspector = inspector;
+ this.selection = this.inspector.selection;
+ this.win = this.inspector.panelWin;
+ this.doc = this.inspector.panelDoc;
+ this._init();
+}
+
+exports.HTMLBreadcrumbs = HTMLBreadcrumbs;
+
+HTMLBreadcrumbs.prototype = {
+ get walker() {
+ return this.inspector.walker;
+ },
+
+ _init: function () {
+ this.outer = this.doc.getElementById("inspector-breadcrumbs");
+ this.arrowScrollBox = new ArrowScrollBox(
+ this.win,
+ this.outer);
+
+ this.container = this.arrowScrollBox.inner;
+ this.scroll = this.scroll.bind(this);
+ this.arrowScrollBox.on("overflow", this.scroll);
+
+ this.outer.addEventListener("click", this, true);
+ this.outer.addEventListener("mouseover", this, true);
+ this.outer.addEventListener("mouseout", this, true);
+ this.outer.addEventListener("focus", this, true);
+
+ this.shortcuts = new KeyShortcuts({ window: this.win, target: this.outer });
+ this.handleShortcut = this.handleShortcut.bind(this);
+
+ this.shortcuts.on("Right", this.handleShortcut);
+ this.shortcuts.on("Left", this.handleShortcut);
+
+ // We will save a list of already displayed nodes in this array.
+ this.nodeHierarchy = [];
+
+ // Last selected node in nodeHierarchy.
+ this.currentIndex = -1;
+
+ // Used to build a unique breadcrumb button Id.
+ this.breadcrumbsWidgetItemId = 0;
+
+ this.update = this.update.bind(this);
+ this.updateSelectors = this.updateSelectors.bind(this);
+ this.selection.on("new-node-front", this.update);
+ this.selection.on("pseudoclass", this.updateSelectors);
+ this.selection.on("attribute-changed", this.updateSelectors);
+ this.inspector.on("markupmutation", this.update);
+ this.update();
+ },
+
+ /**
+
+ * Build a string that represents the node: tagName#id.class1.class2.
+ * @param {NodeFront} node The node to pretty-print
+ * @return {String}
+ */
+ prettyPrintNodeAsText: function (node) {
+ let text = node.displayName;
+ if (node.isPseudoElement) {
+ text = node.isBeforePseudoElement ? "::before" : "::after";
+ }
+
+ if (node.id) {
+ text += "#" + node.id;
+ }
+
+ if (node.className) {
+ let classList = node.className.split(/\s+/);
+ for (let i = 0; i < classList.length; i++) {
+ text += "." + classList[i];
+ }
+ }
+
+ for (let pseudo of node.pseudoClassLocks) {
+ text += pseudo;
+ }
+
+ return text;
+ },
+
+ /**
+ * Build <span>s that represent the node:
+ * <span class="breadcrumbs-widget-item-tag">tagName</span>
+ * <span class="breadcrumbs-widget-item-id">#id</span>
+ * <span class="breadcrumbs-widget-item-classes">.class1.class2</span>
+ * @param {NodeFront} node The node to pretty-print
+ * @returns {DocumentFragment}
+ */
+ prettyPrintNodeAsXHTML: function (node) {
+ let tagLabel = this.doc.createElementNS(NS_XHTML, "span");
+ tagLabel.className = "breadcrumbs-widget-item-tag plain";
+
+ let idLabel = this.doc.createElementNS(NS_XHTML, "span");
+ idLabel.className = "breadcrumbs-widget-item-id plain";
+
+ let classesLabel = this.doc.createElementNS(NS_XHTML, "span");
+ classesLabel.className = "breadcrumbs-widget-item-classes plain";
+
+ let pseudosLabel = this.doc.createElementNS(NS_XHTML, "span");
+ pseudosLabel.className = "breadcrumbs-widget-item-pseudo-classes plain";
+
+ let tagText = node.displayName;
+ if (node.isPseudoElement) {
+ tagText = node.isBeforePseudoElement ? "::before" : "::after";
+ }
+ let idText = node.id ? ("#" + node.id) : "";
+ let classesText = "";
+
+ if (node.className) {
+ let classList = node.className.split(/\s+/);
+ for (let i = 0; i < classList.length; i++) {
+ classesText += "." + classList[i];
+ }
+ }
+
+ // Figure out which element (if any) needs ellipsing.
+ // Substring for that element, then clear out any extras
+ // (except for pseudo elements).
+ let maxTagLength = MAX_LABEL_LENGTH;
+ let maxIdLength = MAX_LABEL_LENGTH - tagText.length;
+ let maxClassLength = MAX_LABEL_LENGTH - tagText.length - idText.length;
+
+ if (tagText.length > maxTagLength) {
+ tagText = tagText.substr(0, maxTagLength) + ELLIPSIS;
+ idText = classesText = "";
+ } else if (idText.length > maxIdLength) {
+ idText = idText.substr(0, maxIdLength) + ELLIPSIS;
+ classesText = "";
+ } else if (classesText.length > maxClassLength) {
+ classesText = classesText.substr(0, maxClassLength) + ELLIPSIS;
+ }
+
+ tagLabel.textContent = tagText;
+ idLabel.textContent = idText;
+ classesLabel.textContent = classesText;
+ pseudosLabel.textContent = node.pseudoClassLocks.join("");
+
+ let fragment = this.doc.createDocumentFragment();
+ fragment.appendChild(tagLabel);
+ fragment.appendChild(idLabel);
+ fragment.appendChild(classesLabel);
+ fragment.appendChild(pseudosLabel);
+
+ return fragment;
+ },
+
+ /**
+ * Generic event handler.
+ * @param {DOMEvent} event.
+ */
+ handleEvent: function (event) {
+ if (event.type == "click" && event.button == 0) {
+ this.handleClick(event);
+ } else if (event.type == "mouseover") {
+ this.handleMouseOver(event);
+ } else if (event.type == "mouseout") {
+ this.handleMouseOut(event);
+ } else if (event.type == "focus") {
+ this.handleFocus(event);
+ }
+ },
+
+ /**
+ * Focus event handler. When breadcrumbs container gets focus,
+ * aria-activedescendant needs to be updated to currently selected
+ * breadcrumb. Ensures that the focus stays on the container at all times.
+ * @param {DOMEvent} event.
+ */
+ handleFocus: function (event) {
+ event.stopPropagation();
+
+ let node = this.nodeHierarchy[this.currentIndex];
+ if (node) {
+ this.outer.setAttribute("aria-activedescendant", node.button.id);
+ } else {
+ this.outer.removeAttribute("aria-activedescendant");
+ }
+
+ this.outer.focus();
+ },
+
+ /**
+ * On click navigate to the correct node.
+ * @param {DOMEvent} event.
+ */
+ handleClick: function (event) {
+ let target = event.originalTarget;
+ if (target.tagName == "button") {
+ target.onBreadcrumbsClick();
+ }
+ },
+
+ /**
+ * On mouse over, highlight the corresponding content DOM Node.
+ * @param {DOMEvent} event.
+ */
+ handleMouseOver: function (event) {
+ let target = event.originalTarget;
+ if (target.tagName == "button") {
+ target.onBreadcrumbsHover();
+ }
+ },
+
+ /**
+ * On mouse out, make sure to unhighlight.
+ * @param {DOMEvent} event.
+ */
+ handleMouseOut: function (event) {
+ this.inspector.toolbox.highlighterUtils.unhighlight();
+ },
+
+ /**
+ * Handle a keyboard shortcut supported by the breadcrumbs widget.
+ *
+ * @param {String} name
+ * Name of the keyboard shortcut received.
+ * @param {DOMEvent} event
+ * Original event that triggered the shortcut.
+ */
+ handleShortcut: function (name, event) {
+ if (!this.selection.isElementNode()) {
+ return;
+ }
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ this.keyPromise = (this.keyPromise || promise.resolve(null)).then(() => {
+ let currentnode;
+ if (name === "Left" && this.currentIndex != 0) {
+ currentnode = this.nodeHierarchy[this.currentIndex - 1];
+ } else if (name === "Right" && this.currentIndex < this.nodeHierarchy.length - 1) {
+ currentnode = this.nodeHierarchy[this.currentIndex + 1];
+ } else {
+ return null;
+ }
+
+ this.outer.setAttribute("aria-activedescendant", currentnode.button.id);
+ return this.selection.setNodeFront(currentnode.node, "breadcrumbs");
+ });
+ },
+
+ /**
+ * Remove nodes and clean up.
+ */
+ destroy: function () {
+ this.selection.off("new-node-front", this.update);
+ this.selection.off("pseudoclass", this.updateSelectors);
+ this.selection.off("attribute-changed", this.updateSelectors);
+ this.inspector.off("markupmutation", this.update);
+
+ this.container.removeEventListener("click", this, true);
+ this.container.removeEventListener("mouseover", this, true);
+ this.container.removeEventListener("mouseout", this, true);
+ this.container.removeEventListener("focus", this, true);
+ this.shortcuts.destroy();
+
+ this.empty();
+
+ this.arrowScrollBox.off("overflow", this.scroll);
+ this.arrowScrollBox.destroy();
+ this.arrowScrollBox = null;
+ this.outer = null;
+ this.container = null;
+ this.nodeHierarchy = null;
+
+ this.isDestroyed = true;
+ },
+
+ /**
+ * Empty the breadcrumbs container.
+ */
+ empty: function () {
+ while (this.container.hasChildNodes()) {
+ this.container.firstChild.remove();
+ }
+ },
+
+ /**
+ * Set which button represent the selected node.
+ * @param {Number} index Index of the displayed-button to select.
+ */
+ setCursor: function (index) {
+ // Unselect the previously selected button
+ if (this.currentIndex > -1
+ && this.currentIndex < this.nodeHierarchy.length) {
+ this.nodeHierarchy[this.currentIndex].button.removeAttribute("checked");
+ }
+ if (index > -1) {
+ this.nodeHierarchy[index].button.setAttribute("checked", "true");
+ } else {
+ // Unset active active descendant when all buttons are unselected.
+ this.outer.removeAttribute("aria-activedescendant");
+ }
+ this.currentIndex = index;
+ },
+
+ /**
+ * Get the index of the node in the cache.
+ * @param {NodeFront} node.
+ * @returns {Number} The index for this node or -1 if not found.
+ */
+ indexOf: function (node) {
+ for (let i = this.nodeHierarchy.length - 1; i >= 0; i--) {
+ if (this.nodeHierarchy[i].node === node) {
+ return i;
+ }
+ }
+ return -1;
+ },
+
+ /**
+ * Remove all the buttons and their references in the cache after a given
+ * index.
+ * @param {Number} index.
+ */
+ cutAfter: function (index) {
+ while (this.nodeHierarchy.length > (index + 1)) {
+ let toRemove = this.nodeHierarchy.pop();
+ this.container.removeChild(toRemove.button);
+ }
+ },
+
+ /**
+ * Build a button representing the node.
+ * @param {NodeFront} node The node from the page.
+ * @return {DOMNode} The <button> for this node.
+ */
+ buildButton: function (node) {
+ let button = this.doc.createElementNS(NS_XHTML, "button");
+ button.appendChild(this.prettyPrintNodeAsXHTML(node));
+ button.className = "breadcrumbs-widget-item";
+ button.id = "breadcrumbs-widget-item-" + this.breadcrumbsWidgetItemId++;
+
+ button.setAttribute("tabindex", "-1");
+ button.setAttribute("title", this.prettyPrintNodeAsText(node));
+
+ button.onclick = () => {
+ button.focus();
+ };
+
+ button.onBreadcrumbsClick = () => {
+ this.selection.setNodeFront(node, "breadcrumbs");
+ };
+
+ button.onBreadcrumbsHover = () => {
+ this.inspector.toolbox.highlighterUtils.highlightNodeFront(node);
+ };
+
+ return button;
+ },
+
+ /**
+ * Connecting the end of the breadcrumbs to a node.
+ * @param {NodeFront} node The node to reach.
+ */
+ expand: function (node) {
+ let fragment = this.doc.createDocumentFragment();
+ let lastButtonInserted = null;
+ let originalLength = this.nodeHierarchy.length;
+ let stopNode = null;
+ if (originalLength > 0) {
+ stopNode = this.nodeHierarchy[originalLength - 1].node;
+ }
+ while (node && node != stopNode) {
+ if (node.tagName) {
+ let button = this.buildButton(node);
+ fragment.insertBefore(button, lastButtonInserted);
+ lastButtonInserted = button;
+ this.nodeHierarchy.splice(originalLength, 0, {
+ node,
+ button,
+ currentPrettyPrintText: this.prettyPrintNodeAsText(node)
+ });
+ }
+ node = node.parentNode();
+ }
+ this.container.appendChild(fragment, this.container.firstChild);
+ },
+
+ /**
+ * Find the "youngest" ancestor of a node which is already in the breadcrumbs.
+ * @param {NodeFront} node.
+ * @return {Number} Index of the ancestor in the cache, or -1 if not found.
+ */
+ getCommonAncestor: function (node) {
+ while (node) {
+ let idx = this.indexOf(node);
+ if (idx > -1) {
+ return idx;
+ }
+ node = node.parentNode();
+ }
+ return -1;
+ },
+
+ /**
+ * Ensure the selected node is visible.
+ */
+ scroll: function () {
+ // FIXME bug 684352: make sure its immediate neighbors are visible too.
+ if (!this.isDestroyed) {
+ let element = this.nodeHierarchy[this.currentIndex].button;
+ this.arrowScrollBox.scrollToElement(element, "end");
+ }
+ },
+
+ /**
+ * Update all button outputs.
+ */
+ updateSelectors: function () {
+ if (this.isDestroyed) {
+ return;
+ }
+
+ for (let i = this.nodeHierarchy.length - 1; i >= 0; i--) {
+ let {node, button, currentPrettyPrintText} = this.nodeHierarchy[i];
+
+ // If the output of the node doesn't change, skip the update.
+ let textOutput = this.prettyPrintNodeAsText(node);
+ if (currentPrettyPrintText === textOutput) {
+ continue;
+ }
+
+ // Otherwise, update the whole markup for the button.
+ while (button.hasChildNodes()) {
+ button.firstChild.remove();
+ }
+ button.appendChild(this.prettyPrintNodeAsXHTML(node));
+ button.setAttribute("title", textOutput);
+
+ this.nodeHierarchy[i].currentPrettyPrintText = textOutput;
+ }
+ },
+
+ /**
+ * Given a list of mutation changes (passed by the markupmutation event),
+ * decide whether or not they are "interesting" to the current state of the
+ * breadcrumbs widget, i.e. at least one of them should cause part of the
+ * widget to be updated.
+ * @param {Array} mutations The mutations array.
+ * @return {Boolean}
+ */
+ _hasInterestingMutations: function (mutations) {
+ if (!mutations || !mutations.length) {
+ return false;
+ }
+
+ for (let {type, added, removed, target, attributeName} of mutations) {
+ if (type === "childList") {
+ // Only interested in childList mutations if the added or removed
+ // nodes are currently displayed.
+ return added.some(node => this.indexOf(node) > -1) ||
+ removed.some(node => this.indexOf(node) > -1);
+ } else if (type === "attributes" && this.indexOf(target) > -1) {
+ // Only interested in attributes mutations if the target is
+ // currently displayed, and the attribute is either id or class.
+ return attributeName === "class" || attributeName === "id";
+ }
+ }
+
+ // Catch all return in case the mutations array was empty, or in case none
+ // of the changes iterated above were interesting.
+ return false;
+ },
+
+ /**
+ * Update the breadcrumbs display when a new node is selected.
+ * @param {String} reason The reason for the update, if any.
+ * @param {Array} mutations An array of mutations in case this was called as
+ * the "markupmutation" event listener.
+ */
+ update: function (reason, mutations) {
+ if (this.isDestroyed) {
+ return;
+ }
+
+ let hasInterestingMutations = this._hasInterestingMutations(mutations);
+ if (reason === "markupmutation" && !hasInterestingMutations) {
+ return;
+ }
+
+ if (!this.selection.isConnected()) {
+ // remove all the crumbs
+ this.cutAfter(-1);
+ return;
+ }
+
+ // If this was an interesting deletion; then trim the breadcrumb trail
+ let trimmed = false;
+ if (reason === "markupmutation") {
+ for (let {type, removed} of mutations) {
+ if (type !== "childList") {
+ continue;
+ }
+
+ for (let node of removed) {
+ let removedIndex = this.indexOf(node);
+ if (removedIndex > -1) {
+ this.cutAfter(removedIndex - 1);
+ trimmed = true;
+ }
+ }
+ }
+ }
+
+ if (!this.selection.isElementNode()) {
+ // no selection
+ this.setCursor(-1);
+ if (trimmed) {
+ // Since something changed, notify the interested parties.
+ this.inspector.emit("breadcrumbs-updated", this.selection.nodeFront);
+ }
+ return;
+ }
+
+ let idx = this.indexOf(this.selection.nodeFront);
+
+ // Is the node already displayed in the breadcrumbs?
+ // (and there are no mutations that need re-display of the crumbs)
+ if (idx > -1 && !hasInterestingMutations) {
+ // Yes. We select it.
+ this.setCursor(idx);
+ } else {
+ // No. Is the breadcrumbs display empty?
+ if (this.nodeHierarchy.length > 0) {
+ // No. We drop all the element that are not direct ancestors
+ // of the selection
+ let parent = this.selection.nodeFront.parentNode();
+ let ancestorIdx = this.getCommonAncestor(parent);
+ this.cutAfter(ancestorIdx);
+ }
+ // we append the missing button between the end of the breadcrumbs display
+ // and the current node.
+ this.expand(this.selection.nodeFront);
+
+ // we select the current node button
+ idx = this.indexOf(this.selection.nodeFront);
+ this.setCursor(idx);
+ }
+
+ let doneUpdating = this.inspector.updating("breadcrumbs");
+
+ this.updateSelectors();
+
+ // Make sure the selected node and its neighbours are visible.
+ setTimeout(() => {
+ try {
+ this.scroll();
+ this.inspector.emit("breadcrumbs-updated", this.selection.nodeFront);
+ doneUpdating();
+ } catch (e) {
+ // Only log this as an error if we haven't been destroyed in the meantime.
+ if (!this.isDestroyed) {
+ console.error(e);
+ }
+ }
+ }, 0);
+ }
+};
diff --git a/devtools/client/inspector/components/box-model.js b/devtools/client/inspector/components/box-model.js
new file mode 100644
index 000000000..fc36fac71
--- /dev/null
+++ b/devtools/client/inspector/components/box-model.js
@@ -0,0 +1,841 @@
+/* -*- 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 {Task} = require("devtools/shared/task");
+const {InplaceEditor, editableItem} =
+ require("devtools/client/shared/inplace-editor");
+const {ReflowFront} = require("devtools/shared/fronts/reflow");
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const {getCssProperties} = require("devtools/shared/fronts/css-properties");
+
+const STRINGS_URI = "devtools/client/locales/shared.properties";
+const STRINGS_INSPECTOR = "devtools/shared/locales/styleinspector.properties";
+const SHARED_L10N = new LocalizationHelper(STRINGS_URI);
+const INSPECTOR_L10N = new LocalizationHelper(STRINGS_INSPECTOR);
+const NUMERIC = /^-?[\d\.]+$/;
+const LONG_TEXT_ROTATE_LIMIT = 3;
+
+/**
+ * An instance of EditingSession tracks changes that have been made during the
+ * modification of box model values. All of these changes can be reverted by
+ * calling revert. The main parameter is the BoxModelView that created it.
+ *
+ * @param inspector The inspector panel.
+ * @param doc A DOM document that can be used to test style rules.
+ * @param rules An array of the style rules defined for the node being
+ * edited. These should be in order of priority, least
+ * important first.
+ */
+function EditingSession({inspector, doc, elementRules}) {
+ this._doc = doc;
+ this._rules = elementRules;
+ this._modifications = new Map();
+ this._cssProperties = getCssProperties(inspector.toolbox);
+}
+
+EditingSession.prototype = {
+ /**
+ * Gets the value of a single property from the CSS rule.
+ *
+ * @param {StyleRuleFront} rule The CSS rule.
+ * @param {String} property The name of the property.
+ * @return {String} The value.
+ */
+ getPropertyFromRule: function (rule, property) {
+ // Use the parsed declarations in the StyleRuleFront object if available.
+ let index = this.getPropertyIndex(property, rule);
+ if (index !== -1) {
+ return rule.declarations[index].value;
+ }
+
+ // Fallback to parsing the cssText locally otherwise.
+ let dummyStyle = this._element.style;
+ dummyStyle.cssText = rule.cssText;
+ return dummyStyle.getPropertyValue(property);
+ },
+
+ /**
+ * Returns the current value for a property as a string or the empty string if
+ * no style rules affect the property.
+ *
+ * @param property The name of the property as a string
+ */
+ getProperty: function (property) {
+ // Create a hidden element for getPropertyFromRule to use
+ let div = this._doc.createElement("div");
+ div.setAttribute("style", "display: none");
+ this._doc.getElementById("sidebar-panel-computedview").appendChild(div);
+ this._element = this._doc.createElement("p");
+ div.appendChild(this._element);
+
+ // As the rules are in order of priority we can just iterate until we find
+ // the first that defines a value for the property and return that.
+ for (let rule of this._rules) {
+ let value = this.getPropertyFromRule(rule, property);
+ if (value !== "") {
+ div.remove();
+ return value;
+ }
+ }
+ div.remove();
+ return "";
+ },
+
+ /**
+ * Get the index of a given css property name in a CSS rule.
+ * Or -1, if there are no properties in the rule yet.
+ * @param {String} name The property name.
+ * @param {StyleRuleFront} rule Optional, defaults to the element style rule.
+ * @return {Number} The property index in the rule.
+ */
+ getPropertyIndex: function (name, rule = this._rules[0]) {
+ let elementStyleRule = this._rules[0];
+ if (!elementStyleRule.declarations.length) {
+ return -1;
+ }
+
+ return elementStyleRule.declarations.findIndex(p => p.name === name);
+ },
+
+ /**
+ * Sets a number of properties on the node.
+ * @param properties An array of properties, each is an object with name and
+ * value properties. If the value is "" then the property
+ * is removed.
+ * @return {Promise} Resolves when the modifications are complete.
+ */
+ setProperties: Task.async(function* (properties) {
+ for (let property of properties) {
+ // Get a RuleModificationList or RuleRewriter helper object from the
+ // StyleRuleActor to make changes to CSS properties.
+ // Note that RuleRewriter doesn't support modifying several properties at
+ // once, so we do this in a sequence here.
+ let modifications = this._rules[0].startModifyingProperties(
+ this._cssProperties);
+
+ // Remember the property so it can be reverted.
+ if (!this._modifications.has(property.name)) {
+ this._modifications.set(property.name,
+ this.getPropertyFromRule(this._rules[0], property.name));
+ }
+
+ // Find the index of the property to be changed, or get the next index to
+ // insert the new property at.
+ let index = this.getPropertyIndex(property.name);
+ if (index === -1) {
+ index = this._rules[0].declarations.length;
+ }
+
+ if (property.value == "") {
+ modifications.removeProperty(index, property.name);
+ } else {
+ modifications.setProperty(index, property.name, property.value, "");
+ }
+
+ yield modifications.apply();
+ }
+ }),
+
+ /**
+ * Reverts all of the property changes made by this instance.
+ * @return {Promise} Resolves when all properties have been reverted.
+ */
+ revert: Task.async(function* () {
+ // Revert each property that we modified previously, one by one. See
+ // setProperties for information about why.
+ for (let [property, value] of this._modifications) {
+ let modifications = this._rules[0].startModifyingProperties(
+ this._cssProperties);
+
+ // Find the index of the property to be reverted.
+ let index = this.getPropertyIndex(property);
+
+ if (value != "") {
+ // If the property doesn't exist anymore, insert at the beginning of the
+ // rule.
+ if (index === -1) {
+ index = 0;
+ }
+ modifications.setProperty(index, property, value, "");
+ } else {
+ // If the property doesn't exist anymore, no need to remove it. It had
+ // not been added after all.
+ if (index === -1) {
+ continue;
+ }
+ modifications.removeProperty(index, property);
+ }
+
+ yield modifications.apply();
+ }
+ }),
+
+ destroy: function () {
+ this._doc = null;
+ this._rules = null;
+ this._modifications.clear();
+ }
+};
+
+/**
+ * The box model view
+ * @param {InspectorPanel} inspector
+ * An instance of the inspector-panel currently loaded in the toolbox
+ * @param {Document} document
+ * The document that will contain the box model view.
+ */
+function BoxModelView(inspector, document) {
+ this.inspector = inspector;
+ this.doc = document;
+ this.wrapper = this.doc.getElementById("boxmodel-wrapper");
+ this.container = this.doc.getElementById("boxmodel-container");
+ this.expander = this.doc.getElementById("boxmodel-expander");
+ this.sizeLabel = this.doc.querySelector(".boxmodel-size > span");
+ this.sizeHeadingLabel = this.doc.getElementById("boxmodel-element-size");
+ this._geometryEditorHighlighter = null;
+ this._cssProperties = getCssProperties(inspector.toolbox);
+
+ this.init();
+}
+
+BoxModelView.prototype = {
+ init: function () {
+ this.update = this.update.bind(this);
+
+ this.onNewSelection = this.onNewSelection.bind(this);
+ this.inspector.selection.on("new-node-front", this.onNewSelection);
+
+ this.onNewNode = this.onNewNode.bind(this);
+ this.inspector.sidebar.on("computedview-selected", this.onNewNode);
+
+ this.onSidebarSelect = this.onSidebarSelect.bind(this);
+ this.inspector.sidebar.on("select", this.onSidebarSelect);
+
+ this.onToggleExpander = this.onToggleExpander.bind(this);
+ this.expander.addEventListener("click", this.onToggleExpander);
+ let header = this.doc.getElementById("boxmodel-header");
+ header.addEventListener("dblclick", this.onToggleExpander);
+
+ this.onFilterComputedView = this.onFilterComputedView.bind(this);
+ this.inspector.on("computed-view-filtered",
+ this.onFilterComputedView);
+
+ this.onPickerStarted = this.onPickerStarted.bind(this);
+ this.onMarkupViewLeave = this.onMarkupViewLeave.bind(this);
+ this.onMarkupViewNodeHover = this.onMarkupViewNodeHover.bind(this);
+ this.onWillNavigate = this.onWillNavigate.bind(this);
+
+ this.initBoxModelHighlighter();
+
+ // Store for the different dimensions of the node.
+ // 'selector' refers to the element that holds the value;
+ // 'property' is what we are measuring;
+ // 'value' is the computed dimension, computed in update().
+ this.map = {
+ position: {
+ selector: "#boxmodel-element-position",
+ property: "position",
+ value: undefined
+ },
+ marginTop: {
+ selector: ".boxmodel-margin.boxmodel-top > span",
+ property: "margin-top",
+ value: undefined
+ },
+ marginBottom: {
+ selector: ".boxmodel-margin.boxmodel-bottom > span",
+ property: "margin-bottom",
+ value: undefined
+ },
+ marginLeft: {
+ selector: ".boxmodel-margin.boxmodel-left > span",
+ property: "margin-left",
+ value: undefined
+ },
+ marginRight: {
+ selector: ".boxmodel-margin.boxmodel-right > span",
+ property: "margin-right",
+ value: undefined
+ },
+ paddingTop: {
+ selector: ".boxmodel-padding.boxmodel-top > span",
+ property: "padding-top",
+ value: undefined
+ },
+ paddingBottom: {
+ selector: ".boxmodel-padding.boxmodel-bottom > span",
+ property: "padding-bottom",
+ value: undefined
+ },
+ paddingLeft: {
+ selector: ".boxmodel-padding.boxmodel-left > span",
+ property: "padding-left",
+ value: undefined
+ },
+ paddingRight: {
+ selector: ".boxmodel-padding.boxmodel-right > span",
+ property: "padding-right",
+ value: undefined
+ },
+ borderTop: {
+ selector: ".boxmodel-border.boxmodel-top > span",
+ property: "border-top-width",
+ value: undefined
+ },
+ borderBottom: {
+ selector: ".boxmodel-border.boxmodel-bottom > span",
+ property: "border-bottom-width",
+ value: undefined
+ },
+ borderLeft: {
+ selector: ".boxmodel-border.boxmodel-left > span",
+ property: "border-left-width",
+ value: undefined
+ },
+ borderRight: {
+ selector: ".boxmodel-border.boxmodel-right > span",
+ property: "border-right-width",
+ value: undefined
+ }
+ };
+
+ // Make each element the dimensions editable
+ for (let i in this.map) {
+ if (i == "position") {
+ continue;
+ }
+
+ let dimension = this.map[i];
+ editableItem({
+ element: this.doc.querySelector(dimension.selector)
+ }, (element, event) => {
+ this.initEditor(element, event, dimension);
+ });
+ }
+
+ this.onNewNode();
+
+ let nodeGeometry = this.doc.getElementById("layout-geometry-editor");
+ this.onGeometryButtonClick = this.onGeometryButtonClick.bind(this);
+ nodeGeometry.addEventListener("click", this.onGeometryButtonClick);
+ },
+
+ initBoxModelHighlighter: function () {
+ let highlightElts = this.doc.querySelectorAll("#boxmodel-container *[title]");
+ this.onHighlightMouseOver = this.onHighlightMouseOver.bind(this);
+ this.onHighlightMouseOut = this.onHighlightMouseOut.bind(this);
+
+ for (let element of highlightElts) {
+ element.addEventListener("mouseover", this.onHighlightMouseOver, true);
+ element.addEventListener("mouseout", this.onHighlightMouseOut, true);
+ }
+ },
+
+ /**
+ * Start listening to reflows in the current tab.
+ */
+ trackReflows: function () {
+ if (!this.reflowFront) {
+ let { target } = this.inspector;
+ if (target.form.reflowActor) {
+ this.reflowFront = ReflowFront(target.client,
+ target.form);
+ } else {
+ return;
+ }
+ }
+
+ this.reflowFront.on("reflows", this.update);
+ this.reflowFront.start();
+ },
+
+ /**
+ * Stop listening to reflows in the current tab.
+ */
+ untrackReflows: function () {
+ if (!this.reflowFront) {
+ return;
+ }
+
+ this.reflowFront.off("reflows", this.update);
+ this.reflowFront.stop();
+ },
+
+ /**
+ * Called when the user clicks on one of the editable values in the box model view
+ */
+ initEditor: function (element, event, dimension) {
+ let { property } = dimension;
+ let session = new EditingSession(this);
+ let initialValue = session.getProperty(property);
+
+ let editor = new InplaceEditor({
+ element: element,
+ initial: initialValue,
+ contentType: InplaceEditor.CONTENT_TYPES.CSS_VALUE,
+ property: {
+ name: dimension.property
+ },
+ start: self => {
+ self.elt.parentNode.classList.add("boxmodel-editing");
+ },
+ change: value => {
+ if (NUMERIC.test(value)) {
+ value += "px";
+ }
+
+ let properties = [
+ { name: property, value: value }
+ ];
+
+ if (property.substring(0, 7) == "border-") {
+ let bprop = property.substring(0, property.length - 5) + "style";
+ let style = session.getProperty(bprop);
+ if (!style || style == "none" || style == "hidden") {
+ properties.push({ name: bprop, value: "solid" });
+ }
+ }
+
+ session.setProperties(properties).catch(e => console.error(e));
+ },
+ done: (value, commit) => {
+ editor.elt.parentNode.classList.remove("boxmodel-editing");
+ if (!commit) {
+ session.revert().then(() => {
+ session.destroy();
+ }, e => console.error(e));
+ }
+ },
+ contextMenu: this.inspector.onTextBoxContextMenu,
+ cssProperties: this._cssProperties
+ }, event);
+ },
+
+ /**
+ * Is the BoxModelView visible in the sidebar.
+ * @return {Boolean}
+ */
+ isViewVisible: function () {
+ return this.inspector &&
+ this.inspector.sidebar.getCurrentTabID() == "computedview";
+ },
+
+ /**
+ * Is the BoxModelView visible in the sidebar and is the current node valid to
+ * be displayed in the view.
+ * @return {Boolean}
+ */
+ isViewVisibleAndNodeValid: function () {
+ return this.isViewVisible() &&
+ this.inspector.selection.isConnected() &&
+ this.inspector.selection.isElementNode();
+ },
+
+ /**
+ * Destroy the nodes. Remove listeners.
+ */
+ destroy: function () {
+ let highlightElts = this.doc.querySelectorAll("#boxmodel-container *[title]");
+
+ for (let element of highlightElts) {
+ element.removeEventListener("mouseover", this.onHighlightMouseOver, true);
+ element.removeEventListener("mouseout", this.onHighlightMouseOut, true);
+ }
+
+ this.expander.removeEventListener("click", this.onToggleExpander);
+ let header = this.doc.getElementById("boxmodel-header");
+ header.removeEventListener("dblclick", this.onToggleExpander);
+
+ let nodeGeometry = this.doc.getElementById("layout-geometry-editor");
+ nodeGeometry.removeEventListener("click", this.onGeometryButtonClick);
+
+ this.inspector.off("picker-started", this.onPickerStarted);
+
+ // Inspector Panel will destroy `markup` object on "will-navigate" event,
+ // therefore we have to check if it's still available in case BoxModelView
+ // is destroyed immediately after.
+ if (this.inspector.markup) {
+ this.inspector.markup.off("leave", this.onMarkupViewLeave);
+ this.inspector.markup.off("node-hover", this.onMarkupViewNodeHover);
+ }
+
+ this.inspector.sidebar.off("computedview-selected", this.onNewNode);
+ this.inspector.selection.off("new-node-front", this.onNewSelection);
+ this.inspector.sidebar.off("select", this.onSidebarSelect);
+ this.inspector.target.off("will-navigate", this.onWillNavigate);
+ this.inspector.off("computed-view-filtered", this.onFilterComputedView);
+
+ this.inspector = null;
+ this.doc = null;
+ this.wrapper = null;
+ this.container = null;
+ this.expander = null;
+ this.sizeLabel = null;
+ this.sizeHeadingLabel = null;
+
+ if (this.reflowFront) {
+ this.untrackReflows();
+ this.reflowFront.destroy();
+ this.reflowFront = null;
+ }
+ },
+
+ onSidebarSelect: function (e, sidebar) {
+ this.setActive(sidebar === "computedview");
+ },
+
+ /**
+ * Selection 'new-node-front' event handler.
+ */
+ onNewSelection: function () {
+ let done = this.inspector.updating("computed-view");
+ this.onNewNode()
+ .then(() => this.hideGeometryEditor())
+ .then(done, (err) => {
+ console.error(err);
+ done();
+ }).catch(console.error);
+ },
+
+ /**
+ * @return a promise that resolves when the view has been updated
+ */
+ onNewNode: function () {
+ this.setActive(this.isViewVisibleAndNodeValid());
+ return this.update();
+ },
+
+ onHighlightMouseOver: function (e) {
+ let region = e.target.getAttribute("data-box");
+ if (!region) {
+ return;
+ }
+
+ this.showBoxModel({
+ region,
+ showOnly: region,
+ onlyRegionArea: true
+ });
+ },
+
+ onHighlightMouseOut: function () {
+ this.hideBoxModel();
+ },
+
+ onGeometryButtonClick: function ({target}) {
+ if (target.hasAttribute("checked")) {
+ target.removeAttribute("checked");
+ this.hideGeometryEditor();
+ } else {
+ target.setAttribute("checked", "true");
+ this.showGeometryEditor();
+ }
+ },
+
+ onPickerStarted: function () {
+ this.hideGeometryEditor();
+ },
+
+ onToggleExpander: function () {
+ let isOpen = this.expander.hasAttribute("open");
+
+ if (isOpen) {
+ this.container.hidden = true;
+ this.expander.removeAttribute("open");
+ } else {
+ this.container.hidden = false;
+ this.expander.setAttribute("open", "");
+ }
+ },
+
+ onMarkupViewLeave: function () {
+ this.showGeometryEditor(true);
+ },
+
+ onMarkupViewNodeHover: function () {
+ this.hideGeometryEditor(false);
+ },
+
+ onWillNavigate: function () {
+ this._geometryEditorHighlighter.release().catch(console.error);
+ this._geometryEditorHighlighter = null;
+ },
+
+ /**
+ * Event handler that responds to the computed view being filtered
+ * @param {String} reason
+ * @param {Boolean} hidden
+ * Whether or not to hide the box model wrapper
+ */
+ onFilterComputedView: function (reason, hidden) {
+ this.wrapper.hidden = hidden;
+ },
+
+ /**
+ * Stop tracking reflows and hide all values when no node is selected or the
+ * box model view is hidden, otherwise track reflows and show values.
+ * @param {Boolean} isActive
+ */
+ setActive: function (isActive) {
+ if (isActive === this.isActive) {
+ return;
+ }
+ this.isActive = isActive;
+
+ if (isActive) {
+ this.trackReflows();
+ } else {
+ this.untrackReflows();
+ }
+ },
+
+ /**
+ * Compute the dimensions of the node and update the values in
+ * the inspector.xul document.
+ * @return a promise that will be resolved when complete.
+ */
+ update: function () {
+ let lastRequest = Task.spawn((function* () {
+ if (!this.isViewVisibleAndNodeValid()) {
+ this.wrapper.hidden = true;
+ this.inspector.emit("boxmodel-view-updated");
+ return null;
+ }
+
+ let node = this.inspector.selection.nodeFront;
+ let layout = yield this.inspector.pageStyle.getLayout(node, {
+ autoMargins: this.isActive
+ });
+ let styleEntries = yield this.inspector.pageStyle.getApplied(node, {});
+
+ yield this.updateGeometryButton();
+
+ // If a subsequent request has been made, wait for that one instead.
+ if (this._lastRequest != lastRequest) {
+ return this._lastRequest;
+ }
+
+ this._lastRequest = null;
+ let width = layout.width;
+ let height = layout.height;
+ let newLabel = SHARED_L10N.getFormatStr("dimensions", width, height);
+
+ if (this.sizeHeadingLabel.textContent != newLabel) {
+ this.sizeHeadingLabel.textContent = newLabel;
+ }
+
+ for (let i in this.map) {
+ let property = this.map[i].property;
+ if (!(property in layout)) {
+ // Depending on the actor version, some properties
+ // might be missing.
+ continue;
+ }
+ let parsedValue = parseFloat(layout[property]);
+ if (Number.isNaN(parsedValue)) {
+ // Not a number. We use the raw string.
+ // Useful for "position" for example.
+ this.map[i].value = layout[property];
+ } else {
+ this.map[i].value = parsedValue;
+ }
+ }
+
+ let margins = layout.autoMargins;
+ if ("top" in margins) {
+ this.map.marginTop.value = "auto";
+ }
+ if ("right" in margins) {
+ this.map.marginRight.value = "auto";
+ }
+ if ("bottom" in margins) {
+ this.map.marginBottom.value = "auto";
+ }
+ if ("left" in margins) {
+ this.map.marginLeft.value = "auto";
+ }
+
+ for (let i in this.map) {
+ let selector = this.map[i].selector;
+ let span = this.doc.querySelector(selector);
+ this.updateSourceRuleTooltip(span, this.map[i].property, styleEntries);
+ if (span.textContent.length > 0 &&
+ span.textContent == this.map[i].value) {
+ continue;
+ }
+ span.textContent = this.map[i].value;
+ this.manageOverflowingText(span);
+ }
+
+ width -= this.map.borderLeft.value + this.map.borderRight.value +
+ this.map.paddingLeft.value + this.map.paddingRight.value;
+ width = parseFloat(width.toPrecision(6));
+ height -= this.map.borderTop.value + this.map.borderBottom.value +
+ this.map.paddingTop.value + this.map.paddingBottom.value;
+ height = parseFloat(height.toPrecision(6));
+
+ let newValue = width + "\u00D7" + height;
+ if (this.sizeLabel.textContent != newValue) {
+ this.sizeLabel.textContent = newValue;
+ }
+
+ this.elementRules = styleEntries.map(e => e.rule);
+
+ this.wrapper.hidden = false;
+
+ this.inspector.emit("boxmodel-view-updated");
+ return null;
+ }).bind(this)).catch(console.error);
+
+ this._lastRequest = lastRequest;
+ return this._lastRequest;
+ },
+
+ /**
+ * Update the text in the tooltip shown when hovering over a value to provide
+ * information about the source CSS rule that sets this value.
+ * @param {DOMNode} el The element that will receive the tooltip.
+ * @param {String} property The name of the CSS property for the tooltip.
+ * @param {Array} rules An array of applied rules retrieved by
+ * styleActor.getApplied.
+ */
+ updateSourceRuleTooltip: function (el, property, rules) {
+ // Dummy element used to parse the cssText of applied rules.
+ let dummyEl = this.doc.createElement("div");
+
+ // Rules are in order of priority so iterate until we find the first that
+ // defines a value for the property.
+ let sourceRule, value;
+ for (let {rule} of rules) {
+ dummyEl.style.cssText = rule.cssText;
+ value = dummyEl.style.getPropertyValue(property);
+ if (value !== "") {
+ sourceRule = rule;
+ break;
+ }
+ }
+
+ let title = property;
+ if (sourceRule && sourceRule.selectors) {
+ title += "\n" + sourceRule.selectors.join(", ");
+ }
+ if (sourceRule && sourceRule.parentStyleSheet) {
+ if (sourceRule.parentStyleSheet.href) {
+ title += "\n" + sourceRule.parentStyleSheet.href + ":" + sourceRule.line;
+ } else {
+ title += "\n" + INSPECTOR_L10N.getStr("rule.sourceInline") +
+ ":" + sourceRule.line;
+ }
+ }
+
+ el.setAttribute("title", title);
+ },
+
+ /**
+ * Show the box-model highlighter on the currently selected element
+ * @param {Object} options Options passed to the highlighter actor
+ */
+ showBoxModel: function (options = {}) {
+ let toolbox = this.inspector.toolbox;
+ let nodeFront = this.inspector.selection.nodeFront;
+
+ toolbox.highlighterUtils.highlightNodeFront(nodeFront, options);
+ },
+
+ /**
+ * Hide the box-model highlighter on the currently selected element
+ */
+ hideBoxModel: function () {
+ let toolbox = this.inspector.toolbox;
+
+ toolbox.highlighterUtils.unhighlight();
+ },
+
+ /**
+ * Show the geometry editor highlighter on the currently selected element
+ * @param {Boolean} [showOnlyIfActive=false]
+ * Indicates if the Geometry Editor should be shown only if it's active but
+ * hidden.
+ */
+ showGeometryEditor: function (showOnlyIfActive = false) {
+ let toolbox = this.inspector.toolbox;
+ let nodeFront = this.inspector.selection.nodeFront;
+ let nodeGeometry = this.doc.getElementById("layout-geometry-editor");
+ let isActive = nodeGeometry.hasAttribute("checked");
+
+ if (showOnlyIfActive && !isActive) {
+ return;
+ }
+
+ if (this._geometryEditorHighlighter) {
+ this._geometryEditorHighlighter.show(nodeFront).catch(console.error);
+ return;
+ }
+
+ // instantiate Geometry Editor highlighter
+ toolbox.highlighterUtils
+ .getHighlighterByType("GeometryEditorHighlighter").then(highlighter => {
+ highlighter.show(nodeFront).catch(console.error);
+ this._geometryEditorHighlighter = highlighter;
+
+ // Hide completely the geometry editor if the picker is clicked
+ toolbox.on("picker-started", this.onPickerStarted);
+
+ // Temporary hide the geometry editor
+ this.inspector.markup.on("leave", this.onMarkupViewLeave);
+ this.inspector.markup.on("node-hover", this.onMarkupViewNodeHover);
+
+ // Release the actor on will-navigate event
+ this.inspector.target.once("will-navigate", this.onWillNavigate);
+ });
+ },
+
+ /**
+ * Hide the geometry editor highlighter on the currently selected element
+ * @param {Boolean} [updateButton=true]
+ * Indicates if the Geometry Editor's button needs to be unchecked too
+ */
+ hideGeometryEditor: function (updateButton = true) {
+ if (this._geometryEditorHighlighter) {
+ this._geometryEditorHighlighter.hide().catch(console.error);
+ }
+
+ if (updateButton) {
+ let nodeGeometry = this.doc.getElementById("layout-geometry-editor");
+ nodeGeometry.removeAttribute("checked");
+ }
+ },
+
+ /**
+ * Update the visibility and the state of the geometry editor button,
+ * based on the selected node.
+ */
+ updateGeometryButton: Task.async(function* () {
+ let node = this.inspector.selection.nodeFront;
+ let isEditable = false;
+
+ if (node) {
+ isEditable = yield this.inspector.pageStyle.isPositionEditable(node);
+ }
+
+ let nodeGeometry = this.doc.getElementById("layout-geometry-editor");
+ nodeGeometry.style.visibility = isEditable ? "visible" : "hidden";
+ }),
+
+ manageOverflowingText: function (span) {
+ let classList = span.parentNode.classList;
+
+ if (classList.contains("boxmodel-left") ||
+ classList.contains("boxmodel-right")) {
+ let force = span.textContent.length > LONG_TEXT_ROTATE_LIMIT;
+ classList.toggle("boxmodel-rotate", force);
+ }
+ }
+};
+
+exports.BoxModelView = BoxModelView;
diff --git a/devtools/client/inspector/components/inspector-tab-panel.css b/devtools/client/inspector/components/inspector-tab-panel.css
new file mode 100644
index 000000000..e85e5daed
--- /dev/null
+++ b/devtools/client/inspector/components/inspector-tab-panel.css
@@ -0,0 +1,15 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+.devtools-inspector-tab-frame {
+ border: none;
+ height: 100%;
+ width: 100%;
+}
+
+.devtools-inspector-tab-panel {
+ width: 100%;
+ height: 100%;
+}
diff --git a/devtools/client/inspector/components/inspector-tab-panel.js b/devtools/client/inspector/components/inspector-tab-panel.js
new file mode 100644
index 000000000..68db7781e
--- /dev/null
+++ b/devtools/client/inspector/components/inspector-tab-panel.js
@@ -0,0 +1,67 @@
+/* -*- 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 { DOM, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
+
+// Shortcuts
+const { div } = DOM;
+
+/**
+ * Helper panel component that is using an existing DOM node
+ * as the content. It's used by Sidebar as well as SplitBox
+ * components.
+ */
+var InspectorTabPanel = createClass({
+ displayName: "InspectorTabPanel",
+
+ propTypes: {
+ // ID of the node that should be rendered as the content.
+ id: PropTypes.string.isRequired,
+ // Optional prefix for panel IDs.
+ idPrefix: PropTypes.string,
+ // Optional mount callback
+ onMount: PropTypes.func,
+ },
+
+ getDefaultProps: function () {
+ return {
+ idPrefix: "",
+ };
+ },
+
+ componentDidMount: function () {
+ let doc = this.refs.content.ownerDocument;
+ let panel = doc.getElementById(this.props.idPrefix + this.props.id);
+
+ // Append existing DOM node into panel's content.
+ this.refs.content.appendChild(panel);
+
+ if (this.props.onMount) {
+ this.props.onMount(this.refs.content, this.props);
+ }
+ },
+
+ componentWillUnmount: function () {
+ let doc = this.refs.content.ownerDocument;
+ let panels = doc.getElementById("tabpanels");
+
+ // Move panel's content node back into list of tab panels.
+ panels.appendChild(this.refs.content.firstChild);
+ },
+
+ render: function () {
+ return (
+ div({
+ ref: "content",
+ className: "devtools-inspector-tab-panel",
+ })
+ );
+ }
+});
+
+module.exports = InspectorTabPanel;
diff --git a/devtools/client/inspector/components/moz.build b/devtools/client/inspector/components/moz.build
new file mode 100644
index 000000000..5e4dd40ed
--- /dev/null
+++ b/devtools/client/inspector/components/moz.build
@@ -0,0 +1,13 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'box-model.js',
+ 'inspector-tab-panel.css',
+ 'inspector-tab-panel.js',
+)
+
+BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
diff --git a/devtools/client/inspector/components/test/.eslintrc.js b/devtools/client/inspector/components/test/.eslintrc.js
new file mode 100644
index 000000000..698ae9181
--- /dev/null
+++ b/devtools/client/inspector/components/test/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../../../.eslintrc.mochitests.js"
+};
diff --git a/devtools/client/inspector/components/test/browser.ini b/devtools/client/inspector/components/test/browser.ini
new file mode 100644
index 000000000..42eb352d6
--- /dev/null
+++ b/devtools/client/inspector/components/test/browser.ini
@@ -0,0 +1,29 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ doc_boxmodel_iframe1.html
+ doc_boxmodel_iframe2.html
+ head.js
+ !/devtools/client/commandline/test/helpers.js
+ !/devtools/client/framework/test/shared-head.js
+ !/devtools/client/inspector/test/head.js
+ !/devtools/client/inspector/test/shared-head.js
+ !/devtools/client/shared/test/test-actor.js
+ !/devtools/client/shared/test/test-actor-registry.js
+
+[browser_boxmodel.js]
+[browser_boxmodel_editablemodel.js]
+# [browser_boxmodel_editablemodel_allproperties.js]
+# Disabled for too many intermittent failures (bug 1009322)
+[browser_boxmodel_editablemodel_bluronclick.js]
+[browser_boxmodel_editablemodel_border.js]
+[browser_boxmodel_editablemodel_stylerules.js]
+[browser_boxmodel_guides.js]
+[browser_boxmodel_rotate-labels-on-sides.js]
+[browser_boxmodel_sync.js]
+[browser_boxmodel_tooltips.js]
+[browser_boxmodel_update-after-navigation.js]
+[browser_boxmodel_update-after-reload.js]
+# [browser_boxmodel_update-in-iframes.js]
+# Bug 1020038 boxmodel-view updates for iframe elements changes
diff --git a/devtools/client/inspector/components/test/browser_boxmodel.js b/devtools/client/inspector/components/test/browser_boxmodel.js
new file mode 100644
index 000000000..f8b87f421
--- /dev/null
+++ b/devtools/client/inspector/components/test/browser_boxmodel.js
@@ -0,0 +1,168 @@
+/* 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";
+
+// Test that the box model displays the right values and that it updates when
+// the node's style is changed
+
+// Expected values:
+var res1 = [
+ {
+ selector: "#boxmodel-element-size",
+ value: "160" + "\u00D7" + "160.117"
+ },
+ {
+ selector: ".boxmodel-size > span",
+ value: "100" + "\u00D7" + "100.117"
+ },
+ {
+ selector: ".boxmodel-margin.boxmodel-top > span",
+ value: 30
+ },
+ {
+ selector: ".boxmodel-margin.boxmodel-left > span",
+ value: "auto"
+ },
+ {
+ selector: ".boxmodel-margin.boxmodel-bottom > span",
+ value: 30
+ },
+ {
+ selector: ".boxmodel-margin.boxmodel-right > span",
+ value: "auto"
+ },
+ {
+ selector: ".boxmodel-padding.boxmodel-top > span",
+ value: 20
+ },
+ {
+ selector: ".boxmodel-padding.boxmodel-left > span",
+ value: 20
+ },
+ {
+ selector: ".boxmodel-padding.boxmodel-bottom > span",
+ value: 20
+ },
+ {
+ selector: ".boxmodel-padding.boxmodel-right > span",
+ value: 20
+ },
+ {
+ selector: ".boxmodel-border.boxmodel-top > span",
+ value: 10
+ },
+ {
+ selector: ".boxmodel-border.boxmodel-left > span",
+ value: 10
+ },
+ {
+ selector: ".boxmodel-border.boxmodel-bottom > span",
+ value: 10
+ },
+ {
+ selector: ".boxmodel-border.boxmodel-right > span",
+ value: 10
+ },
+];
+
+var res2 = [
+ {
+ selector: "#boxmodel-element-size",
+ value: "190" + "\u00D7" + "210"
+ },
+ {
+ selector: ".boxmodel-size > span",
+ value: "100" + "\u00D7" + "150"
+ },
+ {
+ selector: ".boxmodel-margin.boxmodel-top > span",
+ value: 30
+ },
+ {
+ selector: ".boxmodel-margin.boxmodel-left > span",
+ value: "auto"
+ },
+ {
+ selector: ".boxmodel-margin.boxmodel-bottom > span",
+ value: 30
+ },
+ {
+ selector: ".boxmodel-margin.boxmodel-right > span",
+ value: "auto"
+ },
+ {
+ selector: ".boxmodel-padding.boxmodel-top > span",
+ value: 20
+ },
+ {
+ selector: ".boxmodel-padding.boxmodel-left > span",
+ value: 20
+ },
+ {
+ selector: ".boxmodel-padding.boxmodel-bottom > span",
+ value: 20
+ },
+ {
+ selector: ".boxmodel-padding.boxmodel-right > span",
+ value: 50
+ },
+ {
+ selector: ".boxmodel-border.boxmodel-top > span",
+ value: 10
+ },
+ {
+ selector: ".boxmodel-border.boxmodel-left > span",
+ value: 10
+ },
+ {
+ selector: ".boxmodel-border.boxmodel-bottom > span",
+ value: 10
+ },
+ {
+ selector: ".boxmodel-border.boxmodel-right > span",
+ value: 10
+ },
+];
+
+add_task(function* () {
+ let style = "div { position: absolute; top: 42px; left: 42px; " +
+ "height: 100.111px; width: 100px; border: 10px solid black; " +
+ "padding: 20px; margin: 30px auto;}";
+ let html = "<style>" + style + "</style><div></div>";
+
+ yield addTab("data:text/html," + encodeURIComponent(html));
+ let {inspector, view, testActor} = yield openBoxModelView();
+ yield selectNode("div", inspector);
+
+ yield testInitialValues(inspector, view);
+ yield testChangingValues(inspector, view, testActor);
+});
+
+function* testInitialValues(inspector, view) {
+ info("Test that the initial values of the box model are correct");
+ let viewdoc = view.doc;
+
+ for (let i = 0; i < res1.length; i++) {
+ let elt = viewdoc.querySelector(res1[i].selector);
+ is(elt.textContent, res1[i].value,
+ res1[i].selector + " has the right value.");
+ }
+}
+
+function* testChangingValues(inspector, view, testActor) {
+ info("Test that changing the document updates the box model");
+ let viewdoc = view.doc;
+
+ let onUpdated = waitForUpdate(inspector);
+ yield testActor.setAttribute("div", "style",
+ "height:150px;padding-right:50px;");
+ yield onUpdated;
+
+ for (let i = 0; i < res2.length; i++) {
+ let elt = viewdoc.querySelector(res2[i].selector);
+ is(elt.textContent, res2[i].value,
+ res2[i].selector + " has the right value after style update.");
+ }
+}
diff --git a/devtools/client/inspector/components/test/browser_boxmodel_editablemodel.js b/devtools/client/inspector/components/test/browser_boxmodel_editablemodel.js
new file mode 100644
index 000000000..5c32c2029
--- /dev/null
+++ b/devtools/client/inspector/components/test/browser_boxmodel_editablemodel.js
@@ -0,0 +1,194 @@
+/* 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";
+
+// Test that editing the box-model values works as expected and test various
+// key bindings
+
+const TEST_URI = "<style>" +
+ "div { margin: 10px; padding: 3px }" +
+ "#div1 { margin-top: 5px }" +
+ "#div2 { border-bottom: 1em solid black; }" +
+ "#div3 { padding: 2em; }" +
+ "#div4 { margin: 1px; }" +
+ "</style>" +
+ "<div id='div1'></div><div id='div2'></div>" +
+ "<div id='div3'></div><div id='div4'></div>";
+
+add_task(function* () {
+ yield addTab("data:text/html," + encodeURIComponent(TEST_URI));
+ let {inspector, view, testActor} = yield openBoxModelView();
+
+ yield testEditingMargins(inspector, view, testActor);
+ yield testKeyBindings(inspector, view, testActor);
+ yield testEscapeToUndo(inspector, view, testActor);
+ yield testDeletingValue(inspector, view, testActor);
+ yield testRefocusingOnClick(inspector, view, testActor);
+});
+
+function* testEditingMargins(inspector, view, testActor) {
+ info("Test that editing margin dynamically updates the document, pressing " +
+ "escape cancels the changes");
+
+ is((yield getStyle(testActor, "#div1", "margin-top")), "",
+ "Should be no margin-top on the element.");
+ yield selectNode("#div1", inspector);
+
+ let span = view.doc.querySelector(".boxmodel-margin.boxmodel-top > span");
+ is(span.textContent, 5, "Should have the right value in the box model.");
+
+ EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView);
+ let editor = view.doc.querySelector(".styleinspector-propertyeditor");
+ ok(editor, "Should have opened the editor.");
+ is(editor.value, "5px", "Should have the right value in the editor.");
+
+ EventUtils.synthesizeKey("3", {}, view.doc.defaultView);
+ yield waitForUpdate(inspector);
+
+ is((yield getStyle(testActor, "#div1", "margin-top")), "3px",
+ "Should have updated the margin.");
+
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, view.doc.defaultView);
+ yield waitForUpdate(inspector);
+
+ is((yield getStyle(testActor, "#div1", "margin-top")), "",
+ "Should be no margin-top on the element.");
+ is(span.textContent, 5, "Should have the right value in the box model.");
+}
+
+function* testKeyBindings(inspector, view, testActor) {
+ info("Test that arrow keys work correctly and pressing enter commits the " +
+ "changes");
+
+ is((yield getStyle(testActor, "#div1", "margin-left")), "",
+ "Should be no margin-top on the element.");
+ yield selectNode("#div1", inspector);
+
+ let span = view.doc.querySelector(".boxmodel-margin.boxmodel-left > span");
+ is(span.textContent, 10, "Should have the right value in the box model.");
+
+ EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView);
+ let editor = view.doc.querySelector(".styleinspector-propertyeditor");
+ ok(editor, "Should have opened the editor.");
+ is(editor.value, "10px", "Should have the right value in the editor.");
+
+ EventUtils.synthesizeKey("VK_UP", {}, view.doc.defaultView);
+ yield waitForUpdate(inspector);
+
+ is(editor.value, "11px", "Should have the right value in the editor.");
+ is((yield getStyle(testActor, "#div1", "margin-left")), "11px",
+ "Should have updated the margin.");
+
+ EventUtils.synthesizeKey("VK_DOWN", {}, view.doc.defaultView);
+ yield waitForUpdate(inspector);
+
+ is(editor.value, "10px", "Should have the right value in the editor.");
+ is((yield getStyle(testActor, "#div1", "margin-left")), "10px",
+ "Should have updated the margin.");
+
+ EventUtils.synthesizeKey("VK_UP", { shiftKey: true }, view.doc.defaultView);
+ yield waitForUpdate(inspector);
+
+ is(editor.value, "20px", "Should have the right value in the editor.");
+ is((yield getStyle(testActor, "#div1", "margin-left")), "20px",
+ "Should have updated the margin.");
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.doc.defaultView);
+
+ is((yield getStyle(testActor, "#div1", "margin-left")), "20px",
+ "Should be the right margin-top on the element.");
+ is(span.textContent, 20, "Should have the right value in the box model.");
+}
+
+function* testEscapeToUndo(inspector, view, testActor) {
+ info("Test that deleting the value removes the property but escape undoes " +
+ "that");
+
+ is((yield getStyle(testActor, "#div1", "margin-left")), "20px",
+ "Should be the right margin-top on the element.");
+ yield selectNode("#div1", inspector);
+
+ let span = view.doc.querySelector(".boxmodel-margin.boxmodel-left > span");
+ is(span.textContent, 20, "Should have the right value in the box model.");
+
+ EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView);
+ let editor = view.doc.querySelector(".styleinspector-propertyeditor");
+ ok(editor, "Should have opened the editor.");
+ is(editor.value, "20px", "Should have the right value in the editor.");
+
+ EventUtils.synthesizeKey("VK_DELETE", {}, view.doc.defaultView);
+ yield waitForUpdate(inspector);
+
+ is(editor.value, "", "Should have the right value in the editor.");
+ is((yield getStyle(testActor, "#div1", "margin-left")), "",
+ "Should have updated the margin.");
+
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, view.doc.defaultView);
+ yield waitForUpdate(inspector);
+
+ is((yield getStyle(testActor, "#div1", "margin-left")), "20px",
+ "Should be the right margin-top on the element.");
+ is(span.textContent, 20, "Should have the right value in the box model.");
+}
+
+function* testDeletingValue(inspector, view, testActor) {
+ info("Test that deleting the value removes the property");
+
+ yield setStyle(testActor, "#div1", "marginRight", "15px");
+ yield waitForUpdate(inspector);
+
+ yield selectNode("#div1", inspector);
+
+ let span = view.doc.querySelector(".boxmodel-margin.boxmodel-right > span");
+ is(span.textContent, 15, "Should have the right value in the box model.");
+
+ EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView);
+ let editor = view.doc.querySelector(".styleinspector-propertyeditor");
+ ok(editor, "Should have opened the editor.");
+ is(editor.value, "15px", "Should have the right value in the editor.");
+
+ EventUtils.synthesizeKey("VK_DELETE", {}, view.doc.defaultView);
+ yield waitForUpdate(inspector);
+
+ is(editor.value, "", "Should have the right value in the editor.");
+ is((yield getStyle(testActor, "#div1", "margin-right")), "",
+ "Should have updated the margin.");
+
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.doc.defaultView);
+
+ is((yield getStyle(testActor, "#div1", "margin-right")), "",
+ "Should be the right margin-top on the element.");
+ is(span.textContent, 10, "Should have the right value in the box model.");
+}
+
+function* testRefocusingOnClick(inspector, view, testActor) {
+ info("Test that clicking in the editor input does not remove focus");
+
+ yield selectNode("#div4", inspector);
+
+ let span = view.doc.querySelector(".boxmodel-margin.boxmodel-top > span");
+ is(span.textContent, 1, "Should have the right value in the box model.");
+
+ EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView);
+ let editor = view.doc.querySelector(".styleinspector-propertyeditor");
+ ok(editor, "Should have opened the editor.");
+
+ info("Click in the already opened editor input");
+ EventUtils.synthesizeMouseAtCenter(editor, {}, view.doc.defaultView);
+ is(editor, view.doc.activeElement,
+ "Inplace editor input should still have focus.");
+
+ info("Check the input can still be used as expected");
+ EventUtils.synthesizeKey("VK_UP", {}, view.doc.defaultView);
+ yield waitForUpdate(inspector);
+
+ is(editor.value, "2px", "Should have the right value in the editor.");
+ is((yield getStyle(testActor, "#div4", "margin-top")), "2px",
+ "Should have updated the margin.");
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.doc.defaultView);
+
+ is((yield getStyle(testActor, "#div4", "margin-top")), "2px",
+ "Should be the right margin-top on the element.");
+ is(span.textContent, 2, "Should have the right value in the box model.");
+}
diff --git a/devtools/client/inspector/components/test/browser_boxmodel_editablemodel_allproperties.js b/devtools/client/inspector/components/test/browser_boxmodel_editablemodel_allproperties.js
new file mode 100644
index 000000000..464a7b6c5
--- /dev/null
+++ b/devtools/client/inspector/components/test/browser_boxmodel_editablemodel_allproperties.js
@@ -0,0 +1,146 @@
+/* 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";
+
+// Test editing box model values when all values are set
+
+const TEST_URI = "<style>" +
+ "div { margin: 10px; padding: 3px }" +
+ "#div1 { margin-top: 5px }" +
+ "#div2 { border-bottom: 1em solid black; }" +
+ "#div3 { padding: 2em; }" +
+ "</style>" +
+ "<div id='div1'></div><div id='div2'></div><div id='div3'></div>";
+
+add_task(function* () {
+ yield addTab("data:text/html," + encodeURIComponent(TEST_URI));
+ let {inspector, view, testActor} = yield openBoxModelView();
+
+ yield testEditing(inspector, view, testActor);
+ yield testEditingAndCanceling(inspector, view, testActor);
+ yield testDeleting(inspector, view, testActor);
+ yield testDeletingAndCanceling(inspector, view, testActor);
+});
+
+function* testEditing(inspector, view, testActor) {
+ info("When all properties are set on the node editing one should work");
+
+ yield setStyle(testActor, "#div1", "padding", "5px");
+ yield waitForUpdate(inspector);
+
+ yield selectNode("#div1", inspector);
+
+ let span = view.doc.querySelector(".boxmodel-padding.boxmodel-bottom > span");
+ is(span.textContent, 5, "Should have the right value in the box model.");
+
+ EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView);
+ let editor = view.doc.querySelector(".styleinspector-propertyeditor");
+ ok(editor, "Should have opened the editor.");
+ is(editor.value, "5px", "Should have the right value in the editor.");
+
+ EventUtils.synthesizeKey("7", {}, view.doc.defaultView);
+ yield waitForUpdate(inspector);
+
+ is(editor.value, "7", "Should have the right value in the editor.");
+ is((yield getStyle(testActor, "#div1", "padding-bottom")), "7px",
+ "Should have updated the padding");
+
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.doc.defaultView);
+
+ is((yield getStyle(testActor, "#div1", "padding-bottom")), "7px",
+ "Should be the right padding.");
+ is(span.textContent, 7, "Should have the right value in the box model.");
+}
+
+function* testEditingAndCanceling(inspector, view, testActor) {
+ info("When all properties are set on the node editing one and then " +
+ "cancelling with ESCAPE should work");
+
+ yield setStyle(testActor, "#div1", "padding", "5px");
+ yield waitForUpdate(inspector);
+
+ yield selectNode("#div1", inspector);
+
+ let span = view.doc.querySelector(".boxmodel-padding.boxmodel-left > span");
+ is(span.textContent, 5, "Should have the right value in the box model.");
+
+ EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView);
+ let editor = view.doc.querySelector(".styleinspector-propertyeditor");
+ ok(editor, "Should have opened the editor.");
+ is(editor.value, "5px", "Should have the right value in the editor.");
+
+ EventUtils.synthesizeKey("8", {}, view.doc.defaultView);
+ yield waitForUpdate(inspector);
+
+ is(editor.value, "8", "Should have the right value in the editor.");
+ is((yield getStyle(testActor, "#div1", "padding-left")), "8px",
+ "Should have updated the padding");
+
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, view.doc.defaultView);
+ yield waitForUpdate(inspector);
+
+ is((yield getStyle(testActor, "#div1", "padding-left")), "5px",
+ "Should be the right padding.");
+ is(span.textContent, 5, "Should have the right value in the box model.");
+}
+
+function* testDeleting(inspector, view, testActor) {
+ info("When all properties are set on the node deleting one should work");
+
+ yield selectNode("#div1", inspector);
+
+ let span = view.doc.querySelector(".boxmodel-padding.boxmodel-left > span");
+ is(span.textContent, 5, "Should have the right value in the box model.");
+
+ EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView);
+ let editor = view.doc.querySelector(".styleinspector-propertyeditor");
+ ok(editor, "Should have opened the editor.");
+ is(editor.value, "5px", "Should have the right value in the editor.");
+
+ EventUtils.synthesizeKey("VK_DELETE", {}, view.doc.defaultView);
+ yield waitForUpdate(inspector);
+
+ is(editor.value, "", "Should have the right value in the editor.");
+ is((yield getStyle(testActor, "#div1", "padding-left")), "",
+ "Should have updated the padding");
+
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.doc.defaultView);
+
+ is((yield getStyle(testActor, "#div1", "padding-left")), "",
+ "Should be the right padding.");
+ is(span.textContent, 3, "Should have the right value in the box model.");
+}
+
+function* testDeletingAndCanceling(inspector, view, testActor) {
+ info("When all properties are set on the node deleting one then cancelling " +
+ "should work");
+
+ yield setStyle(testActor, "#div1", "padding", "5px");
+ yield waitForUpdate(inspector);
+
+ yield selectNode("#div1", inspector);
+
+ let span = view.doc.querySelector(".boxmodel-padding.boxmodel-left > span");
+ is(span.textContent, 5, "Should have the right value in the box model.");
+
+ EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView);
+ let editor = view.doc.querySelector(".styleinspector-propertyeditor");
+ ok(editor, "Should have opened the editor.");
+ is(editor.value, "5px", "Should have the right value in the editor.");
+
+ EventUtils.synthesizeKey("VK_DELETE", {}, view.doc.defaultView);
+ yield waitForUpdate(inspector);
+
+ is(editor.value, "", "Should have the right value in the editor.");
+ is((yield getStyle(testActor, "#div1", "padding-left")), "",
+ "Should have updated the padding");
+
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, view.doc.defaultView);
+ yield waitForUpdate(inspector);
+
+ is((yield getStyle(testActor, "#div1", "padding-left")), "5px",
+ "Should be the right padding.");
+ is(span.textContent, 5, "Should have the right value in the box model.");
+}
diff --git a/devtools/client/inspector/components/test/browser_boxmodel_editablemodel_bluronclick.js b/devtools/client/inspector/components/test/browser_boxmodel_editablemodel_bluronclick.js
new file mode 100644
index 000000000..9e65e4dc7
--- /dev/null
+++ b/devtools/client/inspector/components/test/browser_boxmodel_editablemodel_bluronclick.js
@@ -0,0 +1,74 @@
+/* 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";
+
+// Test that inplace editors can be blurred by clicking outside of the editor.
+
+const TEST_URI =
+ `<style>
+ #div1 {
+ margin: 10px;
+ padding: 3px;
+ }
+ </style>
+ <div id="div1"></div>`;
+
+add_task(function* () {
+ // Make sure the toolbox is tall enough to have empty space below the
+ // boxmodel-container.
+ yield pushPref("devtools.toolbox.footer.height", 500);
+
+ yield addTab("data:text/html," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openBoxModelView();
+
+ yield selectNode("#div1", inspector);
+ yield testClickingOutsideEditor(view);
+ yield testClickingBelowContainer(view);
+});
+
+function* testClickingOutsideEditor(view) {
+ info("Test that clicking outside the editor blurs it");
+ let span = view.doc.querySelector(".boxmodel-margin.boxmodel-top > span");
+ is(span.textContent, 10, "Should have the right value in the box model.");
+
+ EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView);
+ let editor = view.doc.querySelector(".styleinspector-propertyeditor");
+ ok(editor, "Should have opened the editor.");
+
+ info("Click next to the opened editor input.");
+ let onBlur = once(editor, "blur");
+ let rect = editor.getBoundingClientRect();
+ EventUtils.synthesizeMouse(editor, rect.width + 10, rect.height / 2, {},
+ view.doc.defaultView);
+ yield onBlur;
+
+ is(view.doc.querySelector(".styleinspector-propertyeditor"), null,
+ "Inplace editor has been removed.");
+}
+
+function* testClickingBelowContainer(view) {
+ info("Test that clicking below the box-model container blurs it");
+ let span = view.doc.querySelector(".boxmodel-margin.boxmodel-top > span");
+ is(span.textContent, 10, "Should have the right value in the box model.");
+
+ info("Test that clicking below the boxmodel-container blurs the opened editor");
+ EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView);
+ let editor = view.doc.querySelector(".styleinspector-propertyeditor");
+ ok(editor, "Should have opened the editor.");
+
+ let onBlur = once(editor, "blur");
+ let container = view.doc.querySelector("#boxmodel-container");
+ // Using getBoxQuads here because getBoundingClientRect (and therefore synthesizeMouse)
+ // use an erroneous height of ~50px for the boxmodel-container.
+ let bounds = container.getBoxQuads({relativeTo: view.doc})[0].bounds;
+ EventUtils.synthesizeMouseAtPoint(
+ bounds.left + 10,
+ bounds.top + bounds.height + 10,
+ {}, view.doc.defaultView);
+ yield onBlur;
+
+ is(view.doc.querySelector(".styleinspector-propertyeditor"), null,
+ "Inplace editor has been removed.");
+}
diff --git a/devtools/client/inspector/components/test/browser_boxmodel_editablemodel_border.js b/devtools/client/inspector/components/test/browser_boxmodel_editablemodel_border.js
new file mode 100644
index 000000000..6e9c04b14
--- /dev/null
+++ b/devtools/client/inspector/components/test/browser_boxmodel_editablemodel_border.js
@@ -0,0 +1,52 @@
+/* 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";
+
+// Test that editing the border value in the box model applies the border style
+
+const TEST_URI = "<style>" +
+ "div { margin: 10px; padding: 3px }" +
+ "#div1 { margin-top: 5px }" +
+ "#div2 { border-bottom: 1em solid black; }" +
+ "#div3 { padding: 2em; }" +
+ "</style>" +
+ "<div id='div1'></div><div id='div2'></div><div id='div3'></div>";
+
+add_task(function* () {
+ yield addTab("data:text/html," + encodeURIComponent(TEST_URI));
+ let {inspector, view, testActor} = yield openBoxModelView();
+
+ is((yield getStyle(testActor, "#div1", "border-top-width")), "",
+ "Should have the right border");
+ is((yield getStyle(testActor, "#div1", "border-top-style")), "",
+ "Should have the right border");
+ yield selectNode("#div1", inspector);
+
+ let span = view.doc.querySelector(".boxmodel-border.boxmodel-top > span");
+ is(span.textContent, 0, "Should have the right value in the box model.");
+
+ EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView);
+ let editor = view.doc.querySelector(".styleinspector-propertyeditor");
+ ok(editor, "Should have opened the editor.");
+ is(editor.value, "0", "Should have the right value in the editor.");
+
+ EventUtils.synthesizeKey("1", {}, view.doc.defaultView);
+ yield waitForUpdate(inspector);
+
+ is(editor.value, "1", "Should have the right value in the editor.");
+ is((yield getStyle(testActor, "#div1", "border-top-width")), "1px",
+ "Should have the right border");
+ is((yield getStyle(testActor, "#div1", "border-top-style")), "solid",
+ "Should have the right border");
+
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, view.doc.defaultView);
+ yield waitForUpdate(inspector);
+
+ is((yield getStyle(testActor, "#div1", "border-top-width")), "",
+ "Should be the right padding.");
+ is((yield getStyle(testActor, "#div1", "border-top-style")), "",
+ "Should have the right border");
+ is(span.textContent, 0, "Should have the right value in the box model.");
+});
diff --git a/devtools/client/inspector/components/test/browser_boxmodel_editablemodel_stylerules.js b/devtools/client/inspector/components/test/browser_boxmodel_editablemodel_stylerules.js
new file mode 100644
index 000000000..43346fa15
--- /dev/null
+++ b/devtools/client/inspector/components/test/browser_boxmodel_editablemodel_stylerules.js
@@ -0,0 +1,113 @@
+/* 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";
+
+// Test that units are displayed correctly when editing values in the box model
+// and that values are retrieved and parsed correctly from the back-end
+
+const TEST_URI = "<style>" +
+ "div { margin: 10px; padding: 3px }" +
+ "#div1 { margin-top: 5px }" +
+ "#div2 { border-bottom: 1em solid black; }" +
+ "#div3 { padding: 2em; }" +
+ "</style>" +
+ "<div id='div1'></div><div id='div2'></div><div id='div3'></div>";
+
+add_task(function* () {
+ yield addTab("data:text/html," + encodeURIComponent(TEST_URI));
+ let {inspector, view, testActor} = yield openBoxModelView();
+
+ yield testUnits(inspector, view, testActor);
+ yield testValueComesFromStyleRule(inspector, view, testActor);
+ yield testShorthandsAreParsed(inspector, view, testActor);
+});
+
+function* testUnits(inspector, view, testActor) {
+ info("Test that entering units works");
+
+ is((yield getStyle(testActor, "#div1", "padding-top")), "",
+ "Should have the right padding");
+ yield selectNode("#div1", inspector);
+
+ let span = view.doc.querySelector(".boxmodel-padding.boxmodel-top > span");
+ is(span.textContent, 3, "Should have the right value in the box model.");
+
+ EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView);
+ let editor = view.doc.querySelector(".styleinspector-propertyeditor");
+ ok(editor, "Should have opened the editor.");
+ is(editor.value, "3px", "Should have the right value in the editor.");
+
+ EventUtils.synthesizeKey("1", {}, view.doc.defaultView);
+ yield waitForUpdate(inspector);
+ EventUtils.synthesizeKey("e", {}, view.doc.defaultView);
+ yield waitForUpdate(inspector);
+
+ is((yield getStyle(testActor, "#div1", "padding-top")), "",
+ "An invalid value is handled cleanly");
+
+ EventUtils.synthesizeKey("m", {}, view.doc.defaultView);
+ yield waitForUpdate(inspector);
+
+ is(editor.value, "1em", "Should have the right value in the editor.");
+ is((yield getStyle(testActor, "#div1", "padding-top")),
+ "1em", "Should have updated the padding.");
+
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.doc.defaultView);
+
+ is((yield getStyle(testActor, "#div1", "padding-top")), "1em",
+ "Should be the right padding.");
+ is(span.textContent, 16, "Should have the right value in the box model.");
+}
+
+function* testValueComesFromStyleRule(inspector, view, testActor) {
+ info("Test that we pick up the value from a higher style rule");
+
+ is((yield getStyle(testActor, "#div2", "border-bottom-width")), "",
+ "Should have the right border-bottom-width");
+ yield selectNode("#div2", inspector);
+
+ let span = view.doc.querySelector(".boxmodel-border.boxmodel-bottom > span");
+ is(span.textContent, 16, "Should have the right value in the box model.");
+
+ EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView);
+ let editor = view.doc.querySelector(".styleinspector-propertyeditor");
+ ok(editor, "Should have opened the editor.");
+ is(editor.value, "1em", "Should have the right value in the editor.");
+
+ EventUtils.synthesizeKey("0", {}, view.doc.defaultView);
+ yield waitForUpdate(inspector);
+
+ is(editor.value, "0", "Should have the right value in the editor.");
+ is((yield getStyle(testActor, "#div2", "border-bottom-width")), "0px",
+ "Should have updated the border.");
+
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.doc.defaultView);
+
+ is((yield getStyle(testActor, "#div2", "border-bottom-width")), "0px",
+ "Should be the right border-bottom-width.");
+ is(span.textContent, 0, "Should have the right value in the box model.");
+}
+
+function* testShorthandsAreParsed(inspector, view, testActor) {
+ info("Test that shorthand properties are parsed correctly");
+
+ is((yield getStyle(testActor, "#div3", "padding-right")), "",
+ "Should have the right padding");
+ yield selectNode("#div3", inspector);
+
+ let span = view.doc.querySelector(".boxmodel-padding.boxmodel-right > span");
+ is(span.textContent, 32, "Should have the right value in the box model.");
+
+ EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView);
+ let editor = view.doc.querySelector(".styleinspector-propertyeditor");
+ ok(editor, "Should have opened the editor.");
+ is(editor.value, "2em", "Should have the right value in the editor.");
+
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.doc.defaultView);
+
+ is((yield getStyle(testActor, "#div3", "padding-right")), "",
+ "Should be the right padding.");
+ is(span.textContent, 32, "Should have the right value in the box model.");
+}
diff --git a/devtools/client/inspector/components/test/browser_boxmodel_guides.js b/devtools/client/inspector/components/test/browser_boxmodel_guides.js
new file mode 100644
index 000000000..612d9ace6
--- /dev/null
+++ b/devtools/client/inspector/components/test/browser_boxmodel_guides.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that hovering over regions in the box-model shows the highlighter with
+// the right options.
+// Tests that actually check the highlighter is displayed and correct are in the
+// devtools/inspector/test folder. This test only cares about checking that the
+// box model view does call the highlighter, and it does so by mocking it.
+
+const STYLE = "div { position: absolute; top: 50px; left: 50px; " +
+ "height: 10px; width: 10px; border: 10px solid black; " +
+ "padding: 10px; margin: 10px;}";
+const HTML = "<style>" + STYLE + "</style><div></div>";
+const TEST_URL = "data:text/html;charset=utf-8," + encodeURIComponent(HTML);
+
+var highlightedNodeFront, highlighterOptions;
+
+add_task(function* () {
+ yield addTab(TEST_URL);
+ let {toolbox, inspector, view} = yield openBoxModelView();
+ yield selectNode("div", inspector);
+
+ // Mock the highlighter by replacing the showBoxModel method.
+ toolbox.highlighter.showBoxModel = function (nodeFront, options) {
+ highlightedNodeFront = nodeFront;
+ highlighterOptions = options;
+ };
+
+ let elt = view.doc.getElementById("boxmodel-margins");
+ yield testGuideOnLayoutHover(elt, "margin", inspector, view);
+
+ elt = view.doc.getElementById("boxmodel-borders");
+ yield testGuideOnLayoutHover(elt, "border", inspector, view);
+
+ elt = view.doc.getElementById("boxmodel-padding");
+ yield testGuideOnLayoutHover(elt, "padding", inspector, view);
+
+ elt = view.doc.getElementById("boxmodel-content");
+ yield testGuideOnLayoutHover(elt, "content", inspector, view);
+});
+
+function* testGuideOnLayoutHover(elt, expectedRegion, inspector) {
+ info("Synthesizing mouseover on the boxmodel-view");
+ EventUtils.synthesizeMouse(elt, 2, 2, {type: "mouseover"},
+ elt.ownerDocument.defaultView);
+
+ info("Waiting for the node-highlight event from the toolbox");
+ yield inspector.toolbox.once("node-highlight");
+
+ is(highlightedNodeFront, inspector.selection.nodeFront,
+ "The right nodeFront was highlighted");
+ is(highlighterOptions.region, expectedRegion,
+ "Region " + expectedRegion + " was highlighted");
+}
diff --git a/devtools/client/inspector/components/test/browser_boxmodel_rotate-labels-on-sides.js b/devtools/client/inspector/components/test/browser_boxmodel_rotate-labels-on-sides.js
new file mode 100644
index 000000000..954cd298b
--- /dev/null
+++ b/devtools/client/inspector/components/test/browser_boxmodel_rotate-labels-on-sides.js
@@ -0,0 +1,49 @@
+/* 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";
+
+// Test that longer values are rotated on the side
+
+const res1 = [
+ {selector: ".boxmodel-margin.boxmodel-top > span", value: 30},
+ {selector: ".boxmodel-margin.boxmodel-left > span", value: "auto"},
+ {selector: ".boxmodel-margin.boxmodel-bottom > span", value: 30},
+ {selector: ".boxmodel-margin.boxmodel-right > span", value: "auto"},
+ {selector: ".boxmodel-padding.boxmodel-top > span", value: 20},
+ {selector: ".boxmodel-padding.boxmodel-left > span", value: 2000000},
+ {selector: ".boxmodel-padding.boxmodel-bottom > span", value: 20},
+ {selector: ".boxmodel-padding.boxmodel-right > span", value: 20},
+ {selector: ".boxmodel-border.boxmodel-top > span", value: 10},
+ {selector: ".boxmodel-border.boxmodel-left > span", value: 10},
+ {selector: ".boxmodel-border.boxmodel-bottom > span", value: 10},
+ {selector: ".boxmodel-border.boxmodel-right > span", value: 10},
+];
+
+const TEST_URI = encodeURIComponent([
+ "<style>",
+ "div { border:10px solid black; padding: 20px 20px 20px 2000000px; " +
+ "margin: 30px auto; }",
+ "</style>",
+ "<div></div>"
+].join(""));
+const LONG_TEXT_ROTATE_LIMIT = 3;
+
+add_task(function* () {
+ yield addTab("data:text/html," + TEST_URI);
+ let {inspector, view} = yield openBoxModelView();
+ yield selectNode("div", inspector);
+
+ for (let i = 0; i < res1.length; i++) {
+ let elt = view.doc.querySelector(res1[i].selector);
+ let isLong = elt.textContent.length > LONG_TEXT_ROTATE_LIMIT;
+ let classList = elt.parentNode.classList;
+ let canBeRotated = classList.contains("boxmodel-left") ||
+ classList.contains("boxmodel-right");
+ let isRotated = classList.contains("boxmodel-rotate");
+
+ is(canBeRotated && isLong,
+ isRotated, res1[i].selector + " correctly rotated.");
+ }
+});
diff --git a/devtools/client/inspector/components/test/browser_boxmodel_sync.js b/devtools/client/inspector/components/test/browser_boxmodel_sync.js
new file mode 100644
index 000000000..a896bfe06
--- /dev/null
+++ b/devtools/client/inspector/components/test/browser_boxmodel_sync.js
@@ -0,0 +1,44 @@
+/* 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";
+
+// Test editing box model syncs with the rule view.
+
+const TEST_URI = "<p>hello</p>";
+
+add_task(function* () {
+ yield addTab("data:text/html," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openBoxModelView();
+
+ info("When a property is edited, it should sync in the rule view");
+
+ yield selectNode("p", inspector);
+
+ info("Modify padding-bottom in box model view");
+ let span = view.doc.querySelector(".boxmodel-padding.boxmodel-bottom > span");
+ EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView);
+ let editor = view.doc.querySelector(".styleinspector-propertyeditor");
+
+ EventUtils.synthesizeKey("7", {}, view.doc.defaultView);
+ yield waitForUpdate(inspector);
+ is(editor.value, "7", "Should have the right value in the editor.");
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.doc.defaultView);
+
+ let onRuleViewRefreshed = once(inspector, "rule-view-refreshed");
+ let onRuleViewSelected = once(inspector.sidebar, "ruleview-selected");
+ info("Select the rule view and check that the property was synced there");
+ let ruleView = selectRuleView(inspector);
+
+ info("Wait for the rule view to be selected");
+ yield onRuleViewSelected;
+
+ info("Wait for the rule view to be refreshed");
+ yield onRuleViewRefreshed;
+ ok(true, "The rule view was refreshed");
+
+ let ruleEditor = getRuleViewRuleEditor(ruleView, 0);
+ let textProp = ruleEditor.rule.textProps[0];
+ is(textProp.value, "7px", "The property has the right value");
+});
diff --git a/devtools/client/inspector/components/test/browser_boxmodel_tooltips.js b/devtools/client/inspector/components/test/browser_boxmodel_tooltips.js
new file mode 100644
index 000000000..b65d2446a
--- /dev/null
+++ b/devtools/client/inspector/components/test/browser_boxmodel_tooltips.js
@@ -0,0 +1,126 @@
+/* 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";
+
+// Test that the regions in the box model view have tooltips, and that individual
+// values too. Also test that values that are set from a css rule have tooltips
+// referencing the rule.
+
+const TEST_URI = "<style>" +
+ "#div1 { color: red; margin: 3em; }\n" +
+ "#div2 { border-bottom: 1px solid black; background: red; }\n" +
+ "html, body, #div3 { box-sizing: border-box; padding: 0 2em; }" +
+ "</style>" +
+ "<div id='div1'></div><div id='div2'></div><div id='div3'></div>";
+
+// Test data for the tooltips over individual values.
+// Each entry should contain:
+// - selector: The selector for the node to be selected before starting to test
+// - values: An array containing objects for each of the values that are defined
+// by css rules. Each entry should contain:
+// - name: the name of the property that is set by the css rule
+// - ruleSelector: the selector of the rule
+// - styleSheetLocation: the fileName:lineNumber
+const VALUES_TEST_DATA = [{
+ selector: "#div1",
+ values: [{
+ name: "margin-top",
+ ruleSelector: "#div1",
+ styleSheetLocation: "inline:1"
+ }, {
+ name: "margin-right",
+ ruleSelector: "#div1",
+ styleSheetLocation: "inline:1"
+ }, {
+ name: "margin-bottom",
+ ruleSelector: "#div1",
+ styleSheetLocation: "inline:1"
+ }, {
+ name: "margin-left",
+ ruleSelector: "#div1",
+ styleSheetLocation: "inline:1"
+ }]
+}, {
+ selector: "#div2",
+ values: [{
+ name: "border-bottom-width",
+ ruleSelector: "#div2",
+ styleSheetLocation: "inline:2"
+ }]
+}, {
+ selector: "#div3",
+ values: [{
+ name: "padding-top",
+ ruleSelector: "html, body, #div3",
+ styleSheetLocation: "inline:3"
+ }, {
+ name: "padding-right",
+ ruleSelector: "html, body, #div3",
+ styleSheetLocation: "inline:3"
+ }, {
+ name: "padding-bottom",
+ ruleSelector: "html, body, #div3",
+ styleSheetLocation: "inline:3"
+ }, {
+ name: "padding-left",
+ ruleSelector: "html, body, #div3",
+ styleSheetLocation: "inline:3"
+ }]
+}];
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openBoxModelView();
+
+ info("Checking the regions tooltips");
+
+ ok(view.doc.querySelector("#boxmodel-margins").hasAttribute("title"),
+ "The margin region has a tooltip");
+ is(view.doc.querySelector("#boxmodel-margins").getAttribute("title"), "margin",
+ "The margin region has the correct tooltip content");
+
+ ok(view.doc.querySelector("#boxmodel-borders").hasAttribute("title"),
+ "The border region has a tooltip");
+ is(view.doc.querySelector("#boxmodel-borders").getAttribute("title"), "border",
+ "The border region has the correct tooltip content");
+
+ ok(view.doc.querySelector("#boxmodel-padding").hasAttribute("title"),
+ "The padding region has a tooltip");
+ is(view.doc.querySelector("#boxmodel-padding").getAttribute("title"), "padding",
+ "The padding region has the correct tooltip content");
+
+ ok(view.doc.querySelector("#boxmodel-content").hasAttribute("title"),
+ "The content region has a tooltip");
+ is(view.doc.querySelector("#boxmodel-content").getAttribute("title"), "content",
+ "The content region has the correct tooltip content");
+
+ for (let {selector, values} of VALUES_TEST_DATA) {
+ info("Selecting " + selector + " and checking the values tooltips");
+ yield selectNode(selector, inspector);
+
+ info("Iterate over all values");
+ for (let key in view.map) {
+ if (key === "position") {
+ continue;
+ }
+
+ let name = view.map[key].property;
+ let expectedTooltipData = values.find(o => o.name === name);
+ let el = view.doc.querySelector(view.map[key].selector);
+
+ ok(el.hasAttribute("title"), "The " + name + " value has a tooltip");
+
+ if (expectedTooltipData) {
+ info("The " + name + " value comes from a css rule");
+ let expectedTooltip = name + "\n" + expectedTooltipData.ruleSelector +
+ "\n" + expectedTooltipData.styleSheetLocation;
+ is(el.getAttribute("title"), expectedTooltip, "The tooltip is correct");
+ } else {
+ info("The " + name + " isn't set by a css rule");
+ is(el.getAttribute("title"), name, "The tooltip is correct");
+ }
+ }
+ }
+});
diff --git a/devtools/client/inspector/components/test/browser_boxmodel_update-after-navigation.js b/devtools/client/inspector/components/test/browser_boxmodel_update-after-navigation.js
new file mode 100644
index 000000000..cb5960229
--- /dev/null
+++ b/devtools/client/inspector/components/test/browser_boxmodel_update-after-navigation.js
@@ -0,0 +1,91 @@
+/* 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";
+
+// Test that the box model view continues to work after a page navigation and that
+// it also works after going back
+
+const IFRAME1 = URL_ROOT + "doc_boxmodel_iframe1.html";
+const IFRAME2 = URL_ROOT + "doc_boxmodel_iframe2.html";
+
+add_task(function* () {
+ yield addTab(IFRAME1);
+ let {inspector, view, testActor} = yield openBoxModelView();
+
+ yield testFirstPage(inspector, view, testActor);
+
+ info("Navigate to the second page");
+ yield testActor.eval(`content.location.href="${IFRAME2}"`);
+ yield inspector.once("markuploaded");
+
+ yield testSecondPage(inspector, view, testActor);
+
+ info("Go back to the first page");
+ yield testActor.eval("content.history.back();");
+ yield inspector.once("markuploaded");
+
+ yield testBackToFirstPage(inspector, view, testActor);
+});
+
+function* testFirstPage(inspector, view, testActor) {
+ info("Test that the box model view works on the first page");
+
+ info("Selecting the test node");
+ yield selectNode("p", inspector);
+
+ info("Checking that the box model view shows the right value");
+ let paddingElt = view.doc.querySelector(".boxmodel-padding.boxmodel-top > span");
+ is(paddingElt.textContent, "50");
+
+ info("Listening for box model view changes and modifying the padding");
+ let onUpdated = waitForUpdate(inspector);
+ yield setStyle(testActor, "p", "padding", "20px");
+ yield onUpdated;
+ ok(true, "Box model view got updated");
+
+ info("Checking that the box model view shows the right value after update");
+ is(paddingElt.textContent, "20");
+}
+
+function* testSecondPage(inspector, view, testActor) {
+ info("Test that the box model view works on the second page");
+
+ info("Selecting the test node");
+ yield selectNode("p", inspector);
+
+ info("Checking that the box model view shows the right value");
+ let sizeElt = view.doc.querySelector(".boxmodel-size > span");
+ is(sizeElt.textContent, "100" + "\u00D7" + "100");
+
+ info("Listening for box model view changes and modifying the size");
+ let onUpdated = waitForUpdate(inspector);
+ yield setStyle(testActor, "p", "width", "200px");
+ yield onUpdated;
+ ok(true, "Box model view got updated");
+
+ info("Checking that the box model view shows the right value after update");
+ is(sizeElt.textContent, "200" + "\u00D7" + "100");
+}
+
+function* testBackToFirstPage(inspector, view, testActor) {
+ info("Test that the box model view works on the first page after going back");
+
+ info("Selecting the test node");
+ yield selectNode("p", inspector);
+
+ info("Checking that the box model view shows the right value, which is the" +
+ "modified value from step one because of the bfcache");
+ let paddingElt = view.doc.querySelector(".boxmodel-padding.boxmodel-top > span");
+ is(paddingElt.textContent, "20");
+
+ info("Listening for box model view changes and modifying the padding");
+ let onUpdated = waitForUpdate(inspector);
+ yield setStyle(testActor, "p", "padding", "100px");
+ yield onUpdated;
+ ok(true, "Box model view got updated");
+
+ info("Checking that the box model view shows the right value after update");
+ is(paddingElt.textContent, "100");
+}
diff --git a/devtools/client/inspector/components/test/browser_boxmodel_update-after-reload.js b/devtools/client/inspector/components/test/browser_boxmodel_update-after-reload.js
new file mode 100644
index 000000000..7fc09bfa3
--- /dev/null
+++ b/devtools/client/inspector/components/test/browser_boxmodel_update-after-reload.js
@@ -0,0 +1,40 @@
+/* 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";
+
+// Test that the box model view continues to work after the page is reloaded
+
+add_task(function* () {
+ yield addTab(URL_ROOT + "doc_boxmodel_iframe1.html");
+ let {inspector, view, testActor} = yield openBoxModelView();
+
+ info("Test that the box model view works on the first page");
+ yield assertBoxModelView(inspector, view, testActor);
+
+ info("Reload the page");
+ yield testActor.reload();
+ yield inspector.once("markuploaded");
+
+ info("Test that the box model view works on the reloaded page");
+ yield assertBoxModelView(inspector, view, testActor);
+});
+
+function* assertBoxModelView(inspector, view, testActor) {
+ info("Selecting the test node");
+ yield selectNode("p", inspector);
+
+ info("Checking that the box model view shows the right value");
+ let paddingElt = view.doc.querySelector(".boxmodel-padding.boxmodel-top > span");
+ is(paddingElt.textContent, "50");
+
+ info("Listening for box model view changes and modifying the padding");
+ let onUpdated = waitForUpdate(inspector);
+ yield setStyle(testActor, "p", "padding", "20px");
+ yield onUpdated;
+ ok(true, "Box model view got updated");
+
+ info("Checking that the box model view shows the right value after update");
+ is(paddingElt.textContent, "20");
+}
diff --git a/devtools/client/inspector/components/test/browser_boxmodel_update-in-iframes.js b/devtools/client/inspector/components/test/browser_boxmodel_update-in-iframes.js
new file mode 100644
index 000000000..50014ad1c
--- /dev/null
+++ b/devtools/client/inspector/components/test/browser_boxmodel_update-in-iframes.js
@@ -0,0 +1,101 @@
+/* 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";
+
+// Test that the box model view for elements within iframes also updates when they
+// change
+
+add_task(function* () {
+ yield addTab(URL_ROOT + "doc_boxmodel_iframe1.html");
+ let {inspector, view, testActor} = yield openBoxModelView();
+
+ yield testResizingInIframe(inspector, view, testActor);
+ yield testReflowsAfterIframeDeletion(inspector, view, testActor);
+});
+
+function* testResizingInIframe(inspector, view, testActor) {
+ info("Test that resizing an element in an iframe updates its box model");
+
+ info("Selecting the nested test node");
+ yield selectNodeInIframe2("div", inspector);
+
+ info("Checking that the box model view shows the right value");
+ let sizeElt = view.doc.querySelector(".boxmodel-size > span");
+ is(sizeElt.textContent, "400\u00D7200");
+
+ info("Listening for box model view changes and modifying its size");
+ let onUpdated = waitForUpdate(inspector);
+ yield setStyleInIframe2(testActor, "div", "width", "200px");
+ yield onUpdated;
+ ok(true, "Box model view got updated");
+
+ info("Checking that the box model view shows the right value after update");
+ is(sizeElt.textContent, "200\u00D7200");
+}
+
+function* testReflowsAfterIframeDeletion(inspector, view, testActor) {
+ info("Test reflows are still sent to the box model view after deleting an " +
+ "iframe");
+
+ info("Deleting the iframe2");
+ yield removeIframe2(testActor);
+ yield inspector.once("inspector-updated");
+
+ info("Selecting the test node in iframe1");
+ yield selectNodeInIframe1("p", inspector);
+
+ info("Checking that the box model view shows the right value");
+ let sizeElt = view.doc.querySelector(".boxmodel-size > span");
+ is(sizeElt.textContent, "100\u00D7100");
+
+ info("Listening for box model view changes and modifying its size");
+ let onUpdated = waitForUpdate(inspector);
+ yield setStyleInIframe1(testActor, "p", "width", "200px");
+ yield onUpdated;
+ ok(true, "Box model view got updated");
+
+ info("Checking that the box model view shows the right value after update");
+ is(sizeElt.textContent, "200\u00D7100");
+}
+
+function* selectNodeInIframe1(selector, inspector) {
+ let iframe1 = yield getNodeFront("iframe", inspector);
+ let node = yield getNodeFrontInFrame(selector, iframe1, inspector);
+ yield selectNode(node, inspector);
+}
+
+function* selectNodeInIframe2(selector, inspector) {
+ let iframe1 = yield getNodeFront("iframe", inspector);
+ let iframe2 = yield getNodeFrontInFrame("iframe", iframe1, inspector);
+ let node = yield getNodeFrontInFrame(selector, iframe2, inspector);
+ yield selectNode(node, inspector);
+}
+
+function* setStyleInIframe1(testActor, selector, propertyName, value) {
+ yield testActor.eval(`
+ content.document.querySelector("iframe")
+ .contentDocument.querySelector("${selector}")
+ .style.${propertyName} = "${value}";
+ `);
+}
+
+function* setStyleInIframe2(testActor, selector, propertyName, value) {
+ yield testActor.eval(`
+ content.document.querySelector("iframe")
+ .contentDocument
+ .querySelector("iframe")
+ .contentDocument.querySelector("${selector}")
+ .style.${propertyName} = "${value}";
+ `);
+}
+
+function* removeIframe2(testActor) {
+ yield testActor.eval(`
+ content.document.querySelector("iframe")
+ .contentDocument
+ .querySelector("iframe")
+ .remove();
+ `);
+}
diff --git a/devtools/client/inspector/components/test/doc_boxmodel_iframe1.html b/devtools/client/inspector/components/test/doc_boxmodel_iframe1.html
new file mode 100644
index 000000000..eef48ce07
--- /dev/null
+++ b/devtools/client/inspector/components/test/doc_boxmodel_iframe1.html
@@ -0,0 +1,3 @@
+<!DOCTYPE html>
+<p style="padding:50px;color:#f06;">Root page</p>
+<iframe src="doc_boxmodel_iframe2.html"></iframe>
diff --git a/devtools/client/inspector/components/test/doc_boxmodel_iframe2.html b/devtools/client/inspector/components/test/doc_boxmodel_iframe2.html
new file mode 100644
index 000000000..1f1b0463c
--- /dev/null
+++ b/devtools/client/inspector/components/test/doc_boxmodel_iframe2.html
@@ -0,0 +1,3 @@
+<!DOCTYPE html>
+<p style="width:100px;height:100px;background:red;">iframe 1</p>
+<iframe src="data:text/html,<div style='width:400px;height:200px;background:yellow;'>iframe 2</div>"></iframe>
diff --git a/devtools/client/inspector/components/test/head.js b/devtools/client/inspector/components/test/head.js
new file mode 100644
index 000000000..fa86b5e9e
--- /dev/null
+++ b/devtools/client/inspector/components/test/head.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/ */
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+/* import-globals-from ../../../framework/test/shared-head.js */
+/* import-globals-from ../../test/head.js */
+"use strict";
+
+// Import the inspector's head.js first (which itself imports shared-head.js).
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/inspector/test/head.js",
+ this);
+
+Services.prefs.setIntPref("devtools.toolbox.footer.height", 350);
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("devtools.toolbox.footer.height");
+});
+
+/**
+ * Highlight a node and set the inspector's current selection to the node or
+ * the first match of the given css selector.
+ * @param {String|NodeFront} selectorOrNodeFront
+ * The selector for the node to be set, or the nodeFront
+ * @param {InspectorPanel} inspector
+ * The instance of InspectorPanel currently loaded in the toolbox
+ * @return a promise that resolves when the inspector is updated with the new
+ * node
+ */
+function* selectAndHighlightNode(selectorOrNodeFront, inspector) {
+ info("Highlighting and selecting the node " + selectorOrNodeFront);
+
+ let nodeFront = yield getNodeFront(selectorOrNodeFront, inspector);
+ let updated = inspector.toolbox.once("highlighter-ready");
+ inspector.selection.setNodeFront(nodeFront, "test-highlight");
+ yield updated;
+}
+
+/**
+ * Open the toolbox, with the inspector tool visible, and the computed view
+ * sidebar tab selected to display the box model view.
+ * @return a promise that resolves when the inspector is ready and the box model
+ * view is visible and ready
+ */
+function openBoxModelView() {
+ return openInspectorSidebarTab("computedview").then(data => {
+ // The actual highligher show/hide methods are mocked in box model tests.
+ // The highlighter is tested in devtools/inspector/test.
+ function mockHighlighter({highlighter}) {
+ highlighter.showBoxModel = function () {
+ return promise.resolve();
+ };
+ highlighter.hideBoxModel = function () {
+ return promise.resolve();
+ };
+ }
+ mockHighlighter(data.toolbox);
+
+ return {
+ toolbox: data.toolbox,
+ inspector: data.inspector,
+ view: data.inspector.computedview.boxModelView,
+ testActor: data.testActor
+ };
+ });
+}
+
+/**
+ * Wait for the boxmodel-view-updated event.
+ * @return a promise
+ */
+function waitForUpdate(inspector) {
+ return inspector.once("boxmodel-view-updated");
+}
+
+function getStyle(testActor, selector, propertyName) {
+ return testActor.eval(`
+ content.document.querySelector("${selector}")
+ .style.getPropertyValue("${propertyName}");
+ `);
+}
+
+function setStyle(testActor, selector, propertyName, value) {
+ return testActor.eval(`
+ content.document.querySelector("${selector}")
+ .style.${propertyName} = "${value}";
+ `);
+}
diff --git a/devtools/client/inspector/computed/computed.js b/devtools/client/inspector/computed/computed.js
new file mode 100644
index 000000000..71d602a4e
--- /dev/null
+++ b/devtools/client/inspector/computed/computed.js
@@ -0,0 +1,1522 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set 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 ToolDefinitions = require("devtools/client/definitions").Tools;
+const CssLogic = require("devtools/shared/inspector/css-logic");
+const {ELEMENT_STYLE} = require("devtools/shared/specs/styles");
+const promise = require("promise");
+const defer = require("devtools/shared/defer");
+const Services = require("Services");
+const {OutputParser} = require("devtools/client/shared/output-parser");
+const {PrefObserver, PREF_ORIG_SOURCES} = require("devtools/client/styleeditor/utils");
+const {createChild} = require("devtools/client/inspector/shared/utils");
+const {gDevTools} = require("devtools/client/framework/devtools");
+const {getCssProperties} = require("devtools/shared/fronts/css-properties");
+const HighlightersOverlay = require("devtools/client/inspector/shared/highlighters-overlay");
+const {
+ VIEW_NODE_SELECTOR_TYPE,
+ VIEW_NODE_PROPERTY_TYPE,
+ VIEW_NODE_VALUE_TYPE,
+ VIEW_NODE_IMAGE_URL_TYPE,
+} = require("devtools/client/inspector/shared/node-types");
+const StyleInspectorMenu = require("devtools/client/inspector/shared/style-inspector-menu");
+const TooltipsOverlay = require("devtools/client/inspector/shared/tooltips-overlay");
+const {KeyShortcuts} = require("devtools/client/shared/key-shortcuts");
+const {BoxModelView} = require("devtools/client/inspector/components/box-model");
+const clipboardHelper = require("devtools/shared/platform/clipboard");
+
+const STYLE_INSPECTOR_PROPERTIES = "devtools/shared/locales/styleinspector.properties";
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const STYLE_INSPECTOR_L10N = new LocalizationHelper(STYLE_INSPECTOR_PROPERTIES);
+
+const FILTER_CHANGED_TIMEOUT = 150;
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+
+/**
+ * Helper for long-running processes that should yield occasionally to
+ * the mainloop.
+ *
+ * @param {Window} win
+ * Timeouts will be set on this window when appropriate.
+ * @param {Array} array
+ * The array of items to process.
+ * @param {Object} options
+ * Options for the update process:
+ * onItem {function} Will be called with the value of each iteration.
+ * onBatch {function} Will be called after each batch of iterations,
+ * before yielding to the main loop.
+ * onDone {function} Will be called when iteration is complete.
+ * onCancel {function} Will be called if the process is canceled.
+ * threshold {int} How long to process before yielding, in ms.
+ */
+function UpdateProcess(win, array, options) {
+ this.win = win;
+ this.index = 0;
+ this.array = array;
+
+ this.onItem = options.onItem || function () {};
+ this.onBatch = options.onBatch || function () {};
+ this.onDone = options.onDone || function () {};
+ this.onCancel = options.onCancel || function () {};
+ this.threshold = options.threshold || 45;
+
+ this.canceled = false;
+}
+
+UpdateProcess.prototype = {
+ /**
+ * Error thrown when the array of items to process is empty.
+ */
+ ERROR_ITERATION_DONE: new Error("UpdateProcess iteration done"),
+
+ /**
+ * Schedule a new batch on the main loop.
+ */
+ schedule: function () {
+ if (this.canceled) {
+ return;
+ }
+ this._timeout = setTimeout(this._timeoutHandler.bind(this), 0);
+ },
+
+ /**
+ * Cancel the running process. onItem will not be called again,
+ * and onCancel will be called.
+ */
+ cancel: function () {
+ if (this._timeout) {
+ clearTimeout(this._timeout);
+ this._timeout = 0;
+ }
+ this.canceled = true;
+ this.onCancel();
+ },
+
+ _timeoutHandler: function () {
+ this._timeout = null;
+ try {
+ this._runBatch();
+ this.schedule();
+ } catch (e) {
+ if (e === this.ERROR_ITERATION_DONE) {
+ this.onBatch();
+ this.onDone();
+ return;
+ }
+ console.error(e);
+ throw e;
+ }
+ },
+
+ _runBatch: function () {
+ let time = Date.now();
+ while (!this.canceled) {
+ let next = this._next();
+ this.onItem(next);
+ if ((Date.now() - time) > this.threshold) {
+ this.onBatch();
+ return;
+ }
+ }
+ },
+
+ /**
+ * Returns the item at the current index and increases the index.
+ * If all items have already been processed, will throw ERROR_ITERATION_DONE.
+ */
+ _next: function () {
+ if (this.index < this.array.length) {
+ return this.array[this.index++];
+ }
+ throw this.ERROR_ITERATION_DONE;
+ },
+};
+
+/**
+ * CssComputedView is a panel that manages the display of a table
+ * sorted by style. There should be one instance of CssComputedView
+ * per style display (of which there will generally only be one).
+ *
+ * @param {Inspector} inspector
+ * Inspector toolbox panel
+ * @param {Document} document
+ * The document that will contain the computed view.
+ * @param {PageStyleFront} pageStyle
+ * Front for the page style actor that will be providing
+ * the style information.
+ */
+function CssComputedView(inspector, document, pageStyle) {
+ this.inspector = inspector;
+ this.styleDocument = document;
+ this.styleWindow = this.styleDocument.defaultView;
+ this.pageStyle = pageStyle;
+
+ this.propertyViews = [];
+
+ let cssProperties = getCssProperties(inspector.toolbox);
+ this._outputParser = new OutputParser(document, cssProperties);
+
+ // Create bound methods.
+ this.focusWindow = this.focusWindow.bind(this);
+ this._onContextMenu = this._onContextMenu.bind(this);
+ this._onClick = this._onClick.bind(this);
+ this._onCopy = this._onCopy.bind(this);
+ this._onFilterStyles = this._onFilterStyles.bind(this);
+ this._onClearSearch = this._onClearSearch.bind(this);
+ this._onIncludeBrowserStyles = this._onIncludeBrowserStyles.bind(this);
+
+ let doc = this.styleDocument;
+ this.element = doc.getElementById("propertyContainer");
+ this.searchField = doc.getElementById("computedview-searchbox");
+ this.searchClearButton = doc.getElementById("computedview-searchinput-clear");
+ this.includeBrowserStylesCheckbox =
+ doc.getElementById("browser-style-checkbox");
+
+ this.shortcuts = new KeyShortcuts({ window: this.styleWindow });
+ this._onShortcut = this._onShortcut.bind(this);
+ this.shortcuts.on("CmdOrCtrl+F", this._onShortcut);
+ this.shortcuts.on("Escape", this._onShortcut);
+ this.styleDocument.addEventListener("mousedown", this.focusWindow);
+ this.element.addEventListener("click", this._onClick);
+ this.element.addEventListener("copy", this._onCopy);
+ this.element.addEventListener("contextmenu", this._onContextMenu);
+ this.searchField.addEventListener("input", this._onFilterStyles);
+ this.searchField.addEventListener("contextmenu", this.inspector.onTextBoxContextMenu);
+ this.searchClearButton.addEventListener("click", this._onClearSearch);
+ this.includeBrowserStylesCheckbox.addEventListener("input",
+ this._onIncludeBrowserStyles);
+
+ this.searchClearButton.hidden = true;
+
+ // No results text.
+ this.noResults = this.styleDocument.getElementById("computedview-no-results");
+
+ // Refresh panel when color unit changed.
+ this._handlePrefChange = this._handlePrefChange.bind(this);
+ gDevTools.on("pref-changed", this._handlePrefChange);
+
+ // Refresh panel when pref for showing original sources changes
+ this._onSourcePrefChanged = this._onSourcePrefChanged.bind(this);
+ this._prefObserver = new PrefObserver("devtools.");
+ this._prefObserver.on(PREF_ORIG_SOURCES, this._onSourcePrefChanged);
+
+ // The element that we're inspecting, and the document that it comes from.
+ this._viewedElement = null;
+
+ this.createStyleViews();
+
+ this._contextmenu = new StyleInspectorMenu(this, { isRuleView: false });
+
+ // Add the tooltips and highlightersoverlay
+ this.tooltips = new TooltipsOverlay(this);
+ this.tooltips.addToView();
+
+ this.highlighters = new HighlightersOverlay(this);
+ this.highlighters.addToView();
+}
+
+/**
+ * Lookup a l10n string in the shared styleinspector string bundle.
+ *
+ * @param {String} name
+ * The key to lookup.
+ * @returns {String} localized version of the given key.
+ */
+CssComputedView.l10n = function (name) {
+ try {
+ return STYLE_INSPECTOR_L10N.getStr(name);
+ } catch (ex) {
+ console.log("Error reading '" + name + "'");
+ throw new Error("l10n error with " + name);
+ }
+};
+
+CssComputedView.prototype = {
+ // Cache the list of properties that match the selected element.
+ _matchedProperties: null,
+
+ // Used for cancelling timeouts in the style filter.
+ _filterChangedTimeout: null,
+
+ // Holds the ID of the panelRefresh timeout.
+ _panelRefreshTimeout: null,
+
+ // Toggle for zebra striping
+ _darkStripe: true,
+
+ // Number of visible properties
+ numVisibleProperties: 0,
+
+ setPageStyle: function (pageStyle) {
+ this.pageStyle = pageStyle;
+ },
+
+ get includeBrowserStyles() {
+ return this.includeBrowserStylesCheckbox.checked;
+ },
+
+ _handlePrefChange: function (event, data) {
+ if (this._computed && (data.pref === "devtools.defaultColorUnit" ||
+ data.pref === PREF_ORIG_SOURCES)) {
+ this.refreshPanel();
+ }
+ },
+
+ /**
+ * Update the view with a new selected element. The CssComputedView panel
+ * will show the style information for the given element.
+ *
+ * @param {NodeFront} element
+ * The highlighted node to get styles for.
+ * @returns a promise that will be resolved when highlighting is complete.
+ */
+ selectElement: function (element) {
+ if (!element) {
+ this._viewedElement = null;
+ this.noResults.hidden = false;
+
+ if (this._refreshProcess) {
+ this._refreshProcess.cancel();
+ }
+ // Hiding all properties
+ for (let propView of this.propertyViews) {
+ propView.refresh();
+ }
+ return promise.resolve(undefined);
+ }
+
+ if (element === this._viewedElement) {
+ return promise.resolve(undefined);
+ }
+
+ this._viewedElement = element;
+ this.refreshSourceFilter();
+
+ return this.refreshPanel();
+ },
+
+ /**
+ * Get the type of a given node in the computed-view
+ *
+ * @param {DOMNode} node
+ * The node which we want information about
+ * @return {Object} The type information object contains the following props:
+ * - type {String} One of the VIEW_NODE_XXX_TYPE const in
+ * client/inspector/shared/node-types
+ * - value {Object} Depends on the type of the node
+ * returns null if the node isn't anything we care about
+ */
+ getNodeInfo: function (node) {
+ if (!node) {
+ return null;
+ }
+
+ let classes = node.classList;
+
+ // Check if the node isn't a selector first since this doesn't require
+ // walking the DOM
+ if (classes.contains("matched") ||
+ classes.contains("bestmatch") ||
+ classes.contains("parentmatch")) {
+ let selectorText = "";
+ for (let child of node.childNodes) {
+ if (child.nodeType === node.TEXT_NODE) {
+ selectorText += child.textContent;
+ }
+ }
+ return {
+ type: VIEW_NODE_SELECTOR_TYPE,
+ value: selectorText.trim()
+ };
+ }
+
+ // Walk up the nodes to find out where node is
+ let propertyView;
+ let propertyContent;
+ let parent = node;
+ while (parent.parentNode) {
+ if (parent.classList.contains("property-view")) {
+ propertyView = parent;
+ break;
+ }
+ if (parent.classList.contains("property-content")) {
+ propertyContent = parent;
+ break;
+ }
+ parent = parent.parentNode;
+ }
+ if (!propertyView && !propertyContent) {
+ return null;
+ }
+
+ let value, type;
+
+ // Get the property and value for a node that's a property name or value
+ let isHref = classes.contains("theme-link") && !classes.contains("link");
+ if (propertyView && (classes.contains("property-name") ||
+ classes.contains("property-value") ||
+ isHref)) {
+ value = {
+ property: parent.querySelector(".property-name").textContent,
+ value: parent.querySelector(".property-value").textContent
+ };
+ }
+ if (propertyContent && (classes.contains("other-property-value") ||
+ isHref)) {
+ let view = propertyContent.previousSibling;
+ value = {
+ property: view.querySelector(".property-name").textContent,
+ value: node.textContent
+ };
+ }
+
+ // Get the type
+ if (classes.contains("property-name")) {
+ type = VIEW_NODE_PROPERTY_TYPE;
+ } else if (classes.contains("property-value") ||
+ classes.contains("other-property-value")) {
+ type = VIEW_NODE_VALUE_TYPE;
+ } else if (isHref) {
+ type = VIEW_NODE_IMAGE_URL_TYPE;
+ value.url = node.href;
+ } else {
+ return null;
+ }
+
+ return {type, value};
+ },
+
+ _createPropertyViews: function () {
+ if (this._createViewsPromise) {
+ return this._createViewsPromise;
+ }
+
+ let deferred = defer();
+ this._createViewsPromise = deferred.promise;
+
+ this.refreshSourceFilter();
+ this.numVisibleProperties = 0;
+ let fragment = this.styleDocument.createDocumentFragment();
+
+ this._createViewsProcess = new UpdateProcess(
+ this.styleWindow, CssComputedView.propertyNames, {
+ onItem: (propertyName) => {
+ // Per-item callback.
+ let propView = new PropertyView(this, propertyName);
+ fragment.appendChild(propView.buildMain());
+ fragment.appendChild(propView.buildSelectorContainer());
+
+ if (propView.visible) {
+ this.numVisibleProperties++;
+ }
+ this.propertyViews.push(propView);
+ },
+ onCancel: () => {
+ deferred.reject("_createPropertyViews cancelled");
+ },
+ onDone: () => {
+ // Completed callback.
+ this.element.appendChild(fragment);
+ this.noResults.hidden = this.numVisibleProperties > 0;
+ deferred.resolve(undefined);
+ }
+ }
+ );
+
+ this._createViewsProcess.schedule();
+ return deferred.promise;
+ },
+
+ /**
+ * Refresh the panel content.
+ */
+ refreshPanel: function () {
+ if (!this._viewedElement) {
+ return promise.resolve();
+ }
+
+ // Capture the current viewed element to return from the promise handler
+ // early if it changed
+ let viewedElement = this._viewedElement;
+
+ return promise.all([
+ this._createPropertyViews(),
+ this.pageStyle.getComputed(this._viewedElement, {
+ filter: this._sourceFilter,
+ onlyMatched: !this.includeBrowserStyles,
+ markMatched: true
+ })
+ ]).then(([, computed]) => {
+ if (viewedElement !== this._viewedElement) {
+ return promise.resolve();
+ }
+
+ this._matchedProperties = new Set();
+ for (let name in computed) {
+ if (computed[name].matched) {
+ this._matchedProperties.add(name);
+ }
+ }
+ this._computed = computed;
+
+ if (this._refreshProcess) {
+ this._refreshProcess.cancel();
+ }
+
+ this.noResults.hidden = true;
+
+ // Reset visible property count
+ this.numVisibleProperties = 0;
+
+ // Reset zebra striping.
+ this._darkStripe = true;
+
+ let deferred = defer();
+ this._refreshProcess = new UpdateProcess(
+ this.styleWindow, this.propertyViews, {
+ onItem: (propView) => {
+ propView.refresh();
+ },
+ onCancel: () => {
+ deferred.reject("_refreshProcess of computed view cancelled");
+ },
+ onDone: () => {
+ this._refreshProcess = null;
+ this.noResults.hidden = this.numVisibleProperties > 0;
+
+ if (this.searchField.value.length > 0 &&
+ !this.numVisibleProperties) {
+ this.searchField.classList
+ .add("devtools-style-searchbox-no-match");
+ } else {
+ this.searchField.classList
+ .remove("devtools-style-searchbox-no-match");
+ }
+
+ this.inspector.emit("computed-view-refreshed");
+ deferred.resolve(undefined);
+ }
+ }
+ );
+ this._refreshProcess.schedule();
+ return deferred.promise;
+ }).then(null, (err) => console.error(err));
+ },
+
+ /**
+ * Handle the shortcut events in the computed view.
+ */
+ _onShortcut: function (name, event) {
+ if (!event.target.closest("#sidebar-panel-computedview")) {
+ return;
+ }
+ // Handle the search box's keypress event. If the escape key is pressed,
+ // clear the search box field.
+ if (name === "Escape" && event.target === this.searchField &&
+ this._onClearSearch()) {
+ event.preventDefault();
+ event.stopPropagation();
+ } else if (name === "CmdOrCtrl+F") {
+ this.searchField.focus();
+ event.preventDefault();
+ }
+ },
+
+ /**
+ * Set the filter style search value.
+ * @param {String} value
+ * The search value.
+ */
+ setFilterStyles: function (value = "") {
+ this.searchField.value = value;
+ this.searchField.focus();
+ this._onFilterStyles();
+ },
+
+ /**
+ * Called when the user enters a search term in the filter style search box.
+ */
+ _onFilterStyles: function () {
+ if (this._filterChangedTimeout) {
+ clearTimeout(this._filterChangedTimeout);
+ }
+
+ let filterTimeout = (this.searchField.value.length > 0)
+ ? FILTER_CHANGED_TIMEOUT : 0;
+ this.searchClearButton.hidden = this.searchField.value.length === 0;
+
+ this._filterChangedTimeout = setTimeout(() => {
+ if (this.searchField.value.length > 0) {
+ this.searchField.setAttribute("filled", true);
+ this.inspector.emit("computed-view-filtered", true);
+ } else {
+ this.searchField.removeAttribute("filled");
+ this.inspector.emit("computed-view-filtered", false);
+ }
+
+ this.refreshPanel();
+ this._filterChangeTimeout = null;
+ }, filterTimeout);
+ },
+
+ /**
+ * Called when the user clicks on the clear button in the filter style search
+ * box. Returns true if the search box is cleared and false otherwise.
+ */
+ _onClearSearch: function () {
+ if (this.searchField.value) {
+ this.setFilterStyles("");
+ return true;
+ }
+
+ return false;
+ },
+
+ /**
+ * The change event handler for the includeBrowserStyles checkbox.
+ */
+ _onIncludeBrowserStyles: function () {
+ this.refreshSourceFilter();
+ this.refreshPanel();
+ },
+
+ /**
+ * When includeBrowserStylesCheckbox.checked is false we only display
+ * properties that have matched selectors and have been included by the
+ * document or one of thedocument's stylesheets. If .checked is false we
+ * display all properties including those that come from UA stylesheets.
+ */
+ refreshSourceFilter: function () {
+ this._matchedProperties = null;
+ this._sourceFilter = this.includeBrowserStyles ?
+ CssLogic.FILTER.UA :
+ CssLogic.FILTER.USER;
+ },
+
+ _onSourcePrefChanged: function () {
+ for (let propView of this.propertyViews) {
+ propView.updateSourceLinks();
+ }
+ this.inspector.emit("computed-view-sourcelinks-updated");
+ },
+
+ /**
+ * The CSS as displayed by the UI.
+ */
+ createStyleViews: function () {
+ if (CssComputedView.propertyNames) {
+ return;
+ }
+
+ CssComputedView.propertyNames = [];
+
+ // Here we build and cache a list of css properties supported by the browser
+ // We could use any element but let's use the main document's root element
+ let styles = this.styleWindow
+ .getComputedStyle(this.styleDocument.documentElement);
+ let mozProps = [];
+ for (let i = 0, numStyles = styles.length; i < numStyles; i++) {
+ let prop = styles.item(i);
+ if (prop.startsWith("--")) {
+ // Skip any CSS variables used inside of browser CSS files
+ continue;
+ } else if (prop.startsWith("-")) {
+ mozProps.push(prop);
+ } else {
+ CssComputedView.propertyNames.push(prop);
+ }
+ }
+
+ CssComputedView.propertyNames.sort();
+ CssComputedView.propertyNames.push.apply(CssComputedView.propertyNames,
+ mozProps.sort());
+
+ this._createPropertyViews().then(null, e => {
+ if (!this._isDestroyed) {
+ console.warn("The creation of property views was cancelled because " +
+ "the computed-view was destroyed before it was done creating views");
+ } else {
+ console.error(e);
+ }
+ });
+ },
+
+ /**
+ * Get a set of properties that have matched selectors.
+ *
+ * @return {Set} If a property name is in the set, it has matching selectors.
+ */
+ get matchedProperties() {
+ return this._matchedProperties || new Set();
+ },
+
+ /**
+ * Focus the window on mousedown.
+ */
+ focusWindow: function () {
+ this.styleWindow.focus();
+ },
+
+ /**
+ * Context menu handler.
+ */
+ _onContextMenu: function (event) {
+ this._contextmenu.show(event);
+ },
+
+ _onClick: function (event) {
+ let target = event.target;
+
+ if (target.nodeName === "a") {
+ event.stopPropagation();
+ event.preventDefault();
+ let browserWin = this.inspector.target.tab.ownerDocument.defaultView;
+ browserWin.openUILinkIn(target.href, "tab");
+ }
+ },
+
+ /**
+ * Callback for copy event. Copy selected text.
+ *
+ * @param {Event} event
+ * copy event object.
+ */
+ _onCopy: function (event) {
+ this.copySelection();
+ event.preventDefault();
+ },
+
+ /**
+ * Copy the current selection to the clipboard
+ */
+ copySelection: function () {
+ try {
+ let win = this.styleWindow;
+ let text = win.getSelection().toString().trim();
+
+ // Tidy up block headings by moving CSS property names and their
+ // values onto the same line and inserting a colon between them.
+ let textArray = text.split(/[\r\n]+/);
+ let result = "";
+
+ // Parse text array to output string.
+ if (textArray.length > 1) {
+ for (let prop of textArray) {
+ if (CssComputedView.propertyNames.indexOf(prop) !== -1) {
+ // Property name
+ result += prop;
+ } else {
+ // Property value
+ result += ": " + prop + ";\n";
+ }
+ }
+ } else {
+ // Short text fragment.
+ result = textArray[0];
+ }
+
+ clipboardHelper.copyString(result);
+ } catch (e) {
+ console.error(e);
+ }
+ },
+
+ /**
+ * Destructor for CssComputedView.
+ */
+ destroy: function () {
+ this._viewedElement = null;
+ this._outputParser = null;
+
+ gDevTools.off("pref-changed", this._handlePrefChange);
+
+ this._prefObserver.off(PREF_ORIG_SOURCES, this._onSourcePrefChanged);
+ this._prefObserver.destroy();
+
+ // Cancel tree construction
+ if (this._createViewsProcess) {
+ this._createViewsProcess.cancel();
+ }
+ if (this._refreshProcess) {
+ this._refreshProcess.cancel();
+ }
+
+ // Remove context menu
+ if (this._contextmenu) {
+ this._contextmenu.destroy();
+ this._contextmenu = null;
+ }
+
+ this.tooltips.destroy();
+ this.highlighters.destroy();
+
+ // Remove bound listeners
+ this.styleDocument.removeEventListener("mousedown", this.focusWindow);
+ this.element.removeEventListener("click", this._onClick);
+ this.element.removeEventListener("copy", this._onCopy);
+ this.element.removeEventListener("contextmenu", this._onContextMenu);
+ this.searchField.removeEventListener("input", this._onFilterStyles);
+ this.searchField.removeEventListener("contextmenu",
+ this.inspector.onTextBoxContextMenu);
+ this.searchClearButton.removeEventListener("click", this._onClearSearch);
+ this.includeBrowserStylesCheckbox.removeEventListener("input",
+ this._onIncludeBrowserStyles);
+
+ // Nodes used in templating
+ this.element = null;
+ this.panel = null;
+ this.searchField = null;
+ this.searchClearButton = null;
+ this.includeBrowserStylesCheckbox = null;
+
+ // Property views
+ for (let propView of this.propertyViews) {
+ propView.destroy();
+ }
+ this.propertyViews = null;
+
+ this.inspector = null;
+ this.styleDocument = null;
+ this.styleWindow = null;
+
+ this._isDestroyed = true;
+ }
+};
+
+function PropertyInfo(tree, name) {
+ this.tree = tree;
+ this.name = name;
+}
+
+PropertyInfo.prototype = {
+ get value() {
+ if (this.tree._computed) {
+ let value = this.tree._computed[this.name].value;
+ return value;
+ }
+ return null;
+ }
+};
+
+/**
+ * A container to give easy access to property data from the template engine.
+ *
+ * @param {CssComputedView} tree
+ * The CssComputedView instance we are working with.
+ * @param {String} name
+ * The CSS property name for which this PropertyView
+ * instance will render the rules.
+ */
+function PropertyView(tree, name) {
+ this.tree = tree;
+ this.name = name;
+
+ this.link = "https://developer.mozilla.org/CSS/" + name;
+
+ this._propertyInfo = new PropertyInfo(tree, name);
+}
+
+PropertyView.prototype = {
+ // The parent element which contains the open attribute
+ element: null,
+
+ // Property header node
+ propertyHeader: null,
+
+ // Destination for property names
+ nameNode: null,
+
+ // Destination for property values
+ valueNode: null,
+
+ // Are matched rules expanded?
+ matchedExpanded: false,
+
+ // Matched selector container
+ matchedSelectorsContainer: null,
+
+ // Matched selector expando
+ matchedExpander: null,
+
+ // Cache for matched selector views
+ _matchedSelectorViews: null,
+
+ // The previously selected element used for the selector view caches
+ _prevViewedElement: null,
+
+ /**
+ * Get the computed style for the current property.
+ *
+ * @return {String} the computed style for the current property of the
+ * currently highlighted element.
+ */
+ get value() {
+ return this.propertyInfo.value;
+ },
+
+ /**
+ * An easy way to access the CssPropertyInfo behind this PropertyView.
+ */
+ get propertyInfo() {
+ return this._propertyInfo;
+ },
+
+ /**
+ * Does the property have any matched selectors?
+ */
+ get hasMatchedSelectors() {
+ return this.tree.matchedProperties.has(this.name);
+ },
+
+ /**
+ * Should this property be visible?
+ */
+ get visible() {
+ if (!this.tree._viewedElement) {
+ return false;
+ }
+
+ if (!this.tree.includeBrowserStyles && !this.hasMatchedSelectors) {
+ return false;
+ }
+
+ let searchTerm = this.tree.searchField.value.toLowerCase();
+ let isValidSearchTerm = searchTerm.trim().length > 0;
+ if (isValidSearchTerm &&
+ this.name.toLowerCase().indexOf(searchTerm) === -1 &&
+ this.value.toLowerCase().indexOf(searchTerm) === -1) {
+ return false;
+ }
+
+ return true;
+ },
+
+ /**
+ * Returns the className that should be assigned to the propertyView.
+ *
+ * @return {String}
+ */
+ get propertyHeaderClassName() {
+ if (this.visible) {
+ let isDark = this.tree._darkStripe = !this.tree._darkStripe;
+ return isDark ? "property-view row-striped" : "property-view";
+ }
+ return "property-view-hidden";
+ },
+
+ /**
+ * Returns the className that should be assigned to the propertyView content
+ * container.
+ *
+ * @return {String}
+ */
+ get propertyContentClassName() {
+ if (this.visible) {
+ let isDark = this.tree._darkStripe;
+ return isDark ? "property-content row-striped" : "property-content";
+ }
+ return "property-content-hidden";
+ },
+
+ /**
+ * Build the markup for on computed style
+ *
+ * @return {Element}
+ */
+ buildMain: function () {
+ let doc = this.tree.styleDocument;
+
+ // Build the container element
+ this.onMatchedToggle = this.onMatchedToggle.bind(this);
+ this.element = doc.createElementNS(HTML_NS, "div");
+ this.element.setAttribute("class", this.propertyHeaderClassName);
+ this.element.addEventListener("dblclick", this.onMatchedToggle, false);
+
+ // Make it keyboard navigable
+ this.element.setAttribute("tabindex", "0");
+ this.shortcuts = new KeyShortcuts({
+ window: this.tree.styleWindow,
+ target: this.element
+ });
+ this.shortcuts.on("F1", (name, event) => {
+ this.mdnLinkClick(event);
+ // Prevent opening the options panel
+ event.preventDefault();
+ event.stopPropagation();
+ });
+ this.shortcuts.on("Return", (name, event) => this.onMatchedToggle(event));
+ this.shortcuts.on("Space", (name, event) => this.onMatchedToggle(event));
+
+ let nameContainer = doc.createElementNS(HTML_NS, "div");
+ nameContainer.className = "property-name-container";
+ this.element.appendChild(nameContainer);
+
+ // Build the twisty expand/collapse
+ this.matchedExpander = doc.createElementNS(HTML_NS, "div");
+ this.matchedExpander.className = "expander theme-twisty";
+ this.matchedExpander.addEventListener("click", this.onMatchedToggle, false);
+ nameContainer.appendChild(this.matchedExpander);
+
+ // Build the style name element
+ this.nameNode = doc.createElementNS(HTML_NS, "div");
+ this.nameNode.setAttribute("class", "property-name theme-fg-color5");
+ // Reset its tabindex attribute otherwise, if an ellipsis is applied
+ // it will be reachable via TABing
+ this.nameNode.setAttribute("tabindex", "");
+ // Avoid english text (css properties) from being altered
+ // by RTL mode
+ this.nameNode.setAttribute("dir", "ltr");
+ this.nameNode.textContent = this.nameNode.title = this.name;
+ // Make it hand over the focus to the container
+ this.onFocus = () => this.element.focus();
+ this.nameNode.addEventListener("click", this.onFocus, false);
+ nameContainer.appendChild(this.nameNode);
+
+ let valueContainer = doc.createElementNS(HTML_NS, "div");
+ valueContainer.className = "property-value-container";
+ this.element.appendChild(valueContainer);
+
+ // Build the style value element
+ this.valueNode = doc.createElementNS(HTML_NS, "div");
+ this.valueNode.setAttribute("class", "property-value theme-fg-color1");
+ // Reset its tabindex attribute otherwise, if an ellipsis is applied
+ // it will be reachable via TABing
+ this.valueNode.setAttribute("tabindex", "");
+ this.valueNode.setAttribute("dir", "ltr");
+ // Make it hand over the focus to the container
+ this.valueNode.addEventListener("click", this.onFocus, false);
+ valueContainer.appendChild(this.valueNode);
+
+ return this.element;
+ },
+
+ buildSelectorContainer: function () {
+ let doc = this.tree.styleDocument;
+ let element = doc.createElementNS(HTML_NS, "div");
+ element.setAttribute("class", this.propertyContentClassName);
+ this.matchedSelectorsContainer = doc.createElementNS(HTML_NS, "div");
+ this.matchedSelectorsContainer.setAttribute("class", "matchedselectors");
+ element.appendChild(this.matchedSelectorsContainer);
+
+ return element;
+ },
+
+ /**
+ * Refresh the panel's CSS property value.
+ */
+ refresh: function () {
+ this.element.className = this.propertyHeaderClassName;
+ this.element.nextElementSibling.className = this.propertyContentClassName;
+
+ if (this._prevViewedElement !== this.tree._viewedElement) {
+ this._matchedSelectorViews = null;
+ this._prevViewedElement = this.tree._viewedElement;
+ }
+
+ if (!this.tree._viewedElement || !this.visible) {
+ this.valueNode.textContent = this.valueNode.title = "";
+ this.matchedSelectorsContainer.parentNode.hidden = true;
+ this.matchedSelectorsContainer.textContent = "";
+ this.matchedExpander.removeAttribute("open");
+ return;
+ }
+
+ this.tree.numVisibleProperties++;
+
+ let outputParser = this.tree._outputParser;
+ let frag = outputParser.parseCssProperty(this.propertyInfo.name,
+ this.propertyInfo.value,
+ {
+ colorSwatchClass: "computedview-colorswatch",
+ colorClass: "computedview-color",
+ urlClass: "theme-link"
+ // No need to use baseURI here as computed URIs are never relative.
+ });
+ this.valueNode.innerHTML = "";
+ this.valueNode.appendChild(frag);
+
+ this.refreshMatchedSelectors();
+ },
+
+ /**
+ * Refresh the panel matched rules.
+ */
+ refreshMatchedSelectors: function () {
+ let hasMatchedSelectors = this.hasMatchedSelectors;
+ this.matchedSelectorsContainer.parentNode.hidden = !hasMatchedSelectors;
+
+ if (hasMatchedSelectors) {
+ this.matchedExpander.classList.add("expandable");
+ } else {
+ this.matchedExpander.classList.remove("expandable");
+ }
+
+ if (this.matchedExpanded && hasMatchedSelectors) {
+ return this.tree.pageStyle
+ .getMatchedSelectors(this.tree._viewedElement, this.name)
+ .then(matched => {
+ if (!this.matchedExpanded) {
+ return promise.resolve(undefined);
+ }
+
+ this._matchedSelectorResponse = matched;
+
+ return this._buildMatchedSelectors().then(() => {
+ this.matchedExpander.setAttribute("open", "");
+ this.tree.inspector.emit("computed-view-property-expanded");
+ });
+ }).then(null, console.error);
+ }
+
+ this.matchedSelectorsContainer.innerHTML = "";
+ this.matchedExpander.removeAttribute("open");
+ this.tree.inspector.emit("computed-view-property-collapsed");
+ return promise.resolve(undefined);
+ },
+
+ get matchedSelectors() {
+ return this._matchedSelectorResponse;
+ },
+
+ _buildMatchedSelectors: function () {
+ let promises = [];
+ let frag = this.element.ownerDocument.createDocumentFragment();
+
+ for (let selector of this.matchedSelectorViews) {
+ let p = createChild(frag, "p");
+ let span = createChild(p, "span", {
+ class: "rule-link"
+ });
+ let link = createChild(span, "a", {
+ target: "_blank",
+ class: "link theme-link",
+ title: selector.href,
+ sourcelocation: selector.source,
+ tabindex: "0",
+ textContent: selector.source
+ });
+ link.addEventListener("click", selector.openStyleEditor, false);
+ let shortcuts = new KeyShortcuts({
+ window: this.tree.styleWindow,
+ target: link
+ });
+ shortcuts.on("Return", () => selector.openStyleEditor());
+
+ let status = createChild(p, "span", {
+ dir: "ltr",
+ class: "rule-text theme-fg-color3 " + selector.statusClass,
+ title: selector.statusText,
+ textContent: selector.sourceText
+ });
+ let valueSpan = createChild(status, "span", {
+ class: "other-property-value theme-fg-color1"
+ });
+ valueSpan.appendChild(selector.outputFragment);
+ promises.push(selector.ready);
+ }
+
+ this.matchedSelectorsContainer.innerHTML = "";
+ this.matchedSelectorsContainer.appendChild(frag);
+ return promise.all(promises);
+ },
+
+ /**
+ * Provide access to the matched SelectorViews that we are currently
+ * displaying.
+ */
+ get matchedSelectorViews() {
+ if (!this._matchedSelectorViews) {
+ this._matchedSelectorViews = [];
+ this._matchedSelectorResponse.forEach(selectorInfo => {
+ let selectorView = new SelectorView(this.tree, selectorInfo);
+ this._matchedSelectorViews.push(selectorView);
+ }, this);
+ }
+ return this._matchedSelectorViews;
+ },
+
+ /**
+ * Update all the selector source links to reflect whether we're linking to
+ * original sources (e.g. Sass files).
+ */
+ updateSourceLinks: function () {
+ if (!this._matchedSelectorViews) {
+ return;
+ }
+ for (let view of this._matchedSelectorViews) {
+ view.updateSourceLink();
+ }
+ },
+
+ /**
+ * The action when a user expands matched selectors.
+ *
+ * @param {Event} event
+ * Used to determine the class name of the targets click
+ * event.
+ */
+ onMatchedToggle: function (event) {
+ if (event.shiftKey) {
+ return;
+ }
+ this.matchedExpanded = !this.matchedExpanded;
+ this.refreshMatchedSelectors();
+ event.preventDefault();
+ },
+
+ /**
+ * The action when a user clicks on the MDN help link for a property.
+ */
+ mdnLinkClick: function (event) {
+ let inspector = this.tree.inspector;
+
+ if (inspector.target.tab) {
+ let browserWin = inspector.target.tab.ownerDocument.defaultView;
+ browserWin.openUILinkIn(this.link, "tab");
+ }
+ },
+
+ /**
+ * Destroy this property view, removing event listeners
+ */
+ destroy: function () {
+ this.element.removeEventListener("dblclick", this.onMatchedToggle, false);
+ this.shortcuts.destroy();
+ this.element = null;
+
+ this.matchedExpander.removeEventListener("click", this.onMatchedToggle,
+ false);
+ this.matchedExpander = null;
+
+ this.nameNode.removeEventListener("click", this.onFocus, false);
+ this.nameNode = null;
+
+ this.valueNode.removeEventListener("click", this.onFocus, false);
+ this.valueNode = null;
+ }
+};
+
+/**
+ * A container to give us easy access to display data from a CssRule
+ *
+ * @param CssComputedView tree
+ * the owning CssComputedView
+ * @param selectorInfo
+ */
+function SelectorView(tree, selectorInfo) {
+ this.tree = tree;
+ this.selectorInfo = selectorInfo;
+ this._cacheStatusNames();
+
+ this.openStyleEditor = this.openStyleEditor.bind(this);
+
+ this.ready = this.updateSourceLink();
+}
+
+/**
+ * Decode for cssInfo.rule.status
+ * @see SelectorView.prototype._cacheStatusNames
+ * @see CssLogic.STATUS
+ */
+SelectorView.STATUS_NAMES = [
+ // "Parent Match", "Matched", "Best Match"
+];
+
+SelectorView.CLASS_NAMES = [
+ "parentmatch", "matched", "bestmatch"
+];
+
+SelectorView.prototype = {
+ /**
+ * Cache localized status names.
+ *
+ * These statuses are localized inside the styleinspector.properties string
+ * bundle.
+ * @see css-logic.js - the CssLogic.STATUS array.
+ */
+ _cacheStatusNames: function () {
+ if (SelectorView.STATUS_NAMES.length) {
+ return;
+ }
+
+ for (let status in CssLogic.STATUS) {
+ let i = CssLogic.STATUS[status];
+ if (i > CssLogic.STATUS.UNMATCHED) {
+ let value = CssComputedView.l10n("rule.status." + status);
+ // Replace normal spaces with non-breaking spaces
+ SelectorView.STATUS_NAMES[i] = value.replace(/ /g, "\u00A0");
+ }
+ }
+ },
+
+ /**
+ * A localized version of cssRule.status
+ */
+ get statusText() {
+ return SelectorView.STATUS_NAMES[this.selectorInfo.status];
+ },
+
+ /**
+ * Get class name for selector depending on status
+ */
+ get statusClass() {
+ return SelectorView.CLASS_NAMES[this.selectorInfo.status - 1];
+ },
+
+ get href() {
+ if (this._href) {
+ return this._href;
+ }
+ let sheet = this.selectorInfo.rule.parentStyleSheet;
+ this._href = sheet ? sheet.href : "#";
+ return this._href;
+ },
+
+ get sourceText() {
+ return this.selectorInfo.sourceText;
+ },
+
+ get value() {
+ return this.selectorInfo.value;
+ },
+
+ get outputFragment() {
+ // Sadly, because this fragment is added to the template by DOM Templater
+ // we lose any events that are attached. This means that URLs will open in a
+ // new window. At some point we should fix this by stopping using the
+ // templater.
+ let outputParser = this.tree._outputParser;
+ let frag = outputParser.parseCssProperty(
+ this.selectorInfo.name,
+ this.selectorInfo.value, {
+ colorSwatchClass: "computedview-colorswatch",
+ colorClass: "computedview-color",
+ urlClass: "theme-link",
+ baseURI: this.selectorInfo.rule.href
+ }
+ );
+ return frag;
+ },
+
+ /**
+ * Update the text of the source link to reflect whether we're showing
+ * original sources or not.
+ */
+ updateSourceLink: function () {
+ return this.updateSource().then((oldSource) => {
+ if (oldSource !== this.source && this.tree.element) {
+ let selector = '[sourcelocation="' + oldSource + '"]';
+ let link = this.tree.element.querySelector(selector);
+ if (link) {
+ link.textContent = this.source;
+ link.setAttribute("sourcelocation", this.source);
+ }
+ }
+ });
+ },
+
+ /**
+ * Update the 'source' store based on our original sources preference.
+ */
+ updateSource: function () {
+ let rule = this.selectorInfo.rule;
+ this.sheet = rule.parentStyleSheet;
+
+ if (!rule || !this.sheet) {
+ let oldSource = this.source;
+ this.source = CssLogic.l10n("rule.sourceElement");
+ return promise.resolve(oldSource);
+ }
+
+ let showOrig = Services.prefs.getBoolPref(PREF_ORIG_SOURCES);
+
+ if (showOrig && rule.type !== ELEMENT_STYLE) {
+ let deferred = defer();
+
+ // set as this first so we show something while we're fetching
+ this.source = CssLogic.shortSource(this.sheet) + ":" + rule.line;
+
+ rule.getOriginalLocation().then(({href, line}) => {
+ let oldSource = this.source;
+ this.source = CssLogic.shortSource({href: href}) + ":" + line;
+ deferred.resolve(oldSource);
+ });
+
+ return deferred.promise;
+ }
+
+ let oldSource = this.source;
+ this.source = CssLogic.shortSource(this.sheet) + ":" + rule.line;
+ return promise.resolve(oldSource);
+ },
+
+ /**
+ * When a css link is clicked this method is called in order to either:
+ * 1. Open the link in view source (for chrome stylesheets).
+ * 2. Open the link in the style editor.
+ *
+ * We can only view stylesheets contained in document.styleSheets inside the
+ * style editor.
+ */
+ openStyleEditor: function () {
+ let inspector = this.tree.inspector;
+ let rule = this.selectorInfo.rule;
+
+ // The style editor can only display stylesheets coming from content because
+ // chrome stylesheets are not listed in the editor's stylesheet selector.
+ //
+ // If the stylesheet is a content stylesheet we send it to the style
+ // editor else we display it in the view source window.
+ let parentStyleSheet = rule.parentStyleSheet;
+ if (!parentStyleSheet || parentStyleSheet.isSystem) {
+ let toolbox = gDevTools.getToolbox(inspector.target);
+ toolbox.viewSource(rule.href, rule.line);
+ return;
+ }
+
+ let location = promise.resolve(rule.location);
+ if (Services.prefs.getBoolPref(PREF_ORIG_SOURCES)) {
+ location = rule.getOriginalLocation();
+ }
+
+ location.then(({source, href, line, column}) => {
+ let target = inspector.target;
+ if (ToolDefinitions.styleEditor.isTargetSupported(target)) {
+ gDevTools.showToolbox(target, "styleeditor").then(function (toolbox) {
+ let sheet = source || href;
+ toolbox.getCurrentPanel().selectStyleSheet(sheet, line, column);
+ });
+ }
+ });
+ }
+};
+
+function ComputedViewTool(inspector, window) {
+ this.inspector = inspector;
+ this.document = window.document;
+
+ this.computedView = new CssComputedView(this.inspector, this.document,
+ this.inspector.pageStyle);
+ this.boxModelView = new BoxModelView(this.inspector, this.document);
+
+ this.onSelected = this.onSelected.bind(this);
+ this.refresh = this.refresh.bind(this);
+ this.onPanelSelected = this.onPanelSelected.bind(this);
+ this.onMutations = this.onMutations.bind(this);
+ this.onResized = this.onResized.bind(this);
+
+ this.inspector.selection.on("detached-front", this.onSelected);
+ this.inspector.selection.on("new-node-front", this.onSelected);
+ this.inspector.selection.on("pseudoclass", this.refresh);
+ this.inspector.sidebar.on("computedview-selected", this.onPanelSelected);
+ this.inspector.pageStyle.on("stylesheet-updated", this.refresh);
+ this.inspector.walker.on("mutations", this.onMutations);
+ this.inspector.walker.on("resize", this.onResized);
+
+ this.computedView.selectElement(null);
+
+ this.onSelected();
+}
+
+ComputedViewTool.prototype = {
+ isSidebarActive: function () {
+ if (!this.computedView) {
+ return false;
+ }
+ return this.inspector.sidebar.getCurrentTabID() == "computedview";
+ },
+
+ onSelected: function (event) {
+ // Ignore the event if the view has been destroyed, or if it's inactive.
+ // But only if the current selection isn't null. If it's been set to null,
+ // let the update go through as this is needed to empty the view on
+ // navigation.
+ if (!this.computedView) {
+ return;
+ }
+
+ let isInactive = !this.isSidebarActive() &&
+ this.inspector.selection.nodeFront;
+ if (isInactive) {
+ return;
+ }
+
+ this.computedView.setPageStyle(this.inspector.pageStyle);
+
+ if (!this.inspector.selection.isConnected() ||
+ !this.inspector.selection.isElementNode()) {
+ this.computedView.selectElement(null);
+ return;
+ }
+
+ if (!event || event == "new-node-front") {
+ let done = this.inspector.updating("computed-view");
+ this.computedView.selectElement(this.inspector.selection.nodeFront).then(() => {
+ done();
+ });
+ }
+ },
+
+ refresh: function () {
+ if (this.isSidebarActive()) {
+ this.computedView.refreshPanel();
+ }
+ },
+
+ onPanelSelected: function () {
+ if (this.inspector.selection.nodeFront === this.computedView._viewedElement) {
+ this.refresh();
+ } else {
+ this.onSelected();
+ }
+ },
+
+ /**
+ * When markup mutations occur, if an attribute of the selected node changes,
+ * we need to refresh the view as that might change the node's styles.
+ */
+ onMutations: function (mutations) {
+ for (let {type, target} of mutations) {
+ if (target === this.inspector.selection.nodeFront &&
+ type === "attributes") {
+ this.refresh();
+ break;
+ }
+ }
+ },
+
+ /**
+ * When the window gets resized, this may cause media-queries to match, and
+ * therefore, different styles may apply.
+ */
+ onResized: function () {
+ this.refresh();
+ },
+
+ destroy: function () {
+ this.inspector.walker.off("mutations", this.onMutations);
+ this.inspector.walker.off("resize", this.onResized);
+ this.inspector.sidebar.off("computedview-selected", this.refresh);
+ this.inspector.selection.off("pseudoclass", this.refresh);
+ this.inspector.selection.off("new-node-front", this.onSelected);
+ this.inspector.selection.off("detached-front", this.onSelected);
+ this.inspector.sidebar.off("computedview-selected", this.onPanelSelected);
+ if (this.inspector.pageStyle) {
+ this.inspector.pageStyle.off("stylesheet-updated", this.refresh);
+ }
+
+ this.computedView.destroy();
+ this.boxModelView.destroy();
+
+ this.computedView = this.boxModelView = this.document = this.inspector = null;
+ }
+};
+
+exports.CssComputedView = CssComputedView;
+exports.ComputedViewTool = ComputedViewTool;
+exports.PropertyView = PropertyView;
diff --git a/devtools/client/inspector/computed/moz.build b/devtools/client/inspector/computed/moz.build
new file mode 100644
index 000000000..5ce950325
--- /dev/null
+++ b/devtools/client/inspector/computed/moz.build
@@ -0,0 +1,11 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'computed.js',
+)
+
+BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
diff --git a/devtools/client/inspector/computed/test/.eslintrc.js b/devtools/client/inspector/computed/test/.eslintrc.js
new file mode 100644
index 000000000..698ae9181
--- /dev/null
+++ b/devtools/client/inspector/computed/test/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../../../.eslintrc.mochitests.js"
+};
diff --git a/devtools/client/inspector/computed/test/browser.ini b/devtools/client/inspector/computed/test/browser.ini
new file mode 100644
index 000000000..33293e1eb
--- /dev/null
+++ b/devtools/client/inspector/computed/test/browser.ini
@@ -0,0 +1,41 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ doc_matched_selectors.html
+ doc_media_queries.html
+ doc_pseudoelement.html
+ doc_sourcemaps.css
+ doc_sourcemaps.css.map
+ doc_sourcemaps.html
+ doc_sourcemaps.scss
+ head.js
+ !/devtools/client/commandline/test/helpers.js
+ !/devtools/client/framework/test/shared-head.js
+ !/devtools/client/inspector/test/head.js
+ !/devtools/client/inspector/test/shared-head.js
+ !/devtools/client/shared/test/test-actor.js
+ !/devtools/client/shared/test/test-actor-registry.js
+
+[browser_computed_browser-styles.js]
+[browser_computed_cycle_color.js]
+[browser_computed_getNodeInfo.js]
+[browser_computed_keybindings_01.js]
+[browser_computed_keybindings_02.js]
+[browser_computed_matched-selectors-toggle.js]
+[browser_computed_matched-selectors_01.js]
+[browser_computed_matched-selectors_02.js]
+[browser_computed_media-queries.js]
+[browser_computed_no-results-placeholder.js]
+[browser_computed_original-source-link.js]
+[browser_computed_pseudo-element_01.js]
+[browser_computed_refresh-on-style-change_01.js]
+[browser_computed_search-filter.js]
+[browser_computed_search-filter_clear.js]
+[browser_computed_search-filter_context-menu.js]
+subsuite = clipboard
+[browser_computed_search-filter_escape-keypress.js]
+[browser_computed_search-filter_noproperties.js]
+[browser_computed_select-and-copy-styles.js]
+subsuite = clipboard
+[browser_computed_style-editor-link.js]
diff --git a/devtools/client/inspector/computed/test/browser_computed_browser-styles.js b/devtools/client/inspector/computed/test/browser_computed_browser-styles.js
new file mode 100644
index 000000000..32de63650
--- /dev/null
+++ b/devtools/client/inspector/computed/test/browser_computed_browser-styles.js
@@ -0,0 +1,52 @@
+/* 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";
+
+// Tests that the checkbox to include browser styles works properly.
+
+const TEST_URI = `
+ <style type="text/css">
+ .matches {
+ color: #F00;
+ }
+ </style>
+ <span id="matches" class="matches">Some styled text</span>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openComputedView();
+ yield selectNode("#matches", inspector);
+
+ info("Checking the default styles");
+ is(isPropertyVisible("color", view), true,
+ "span #matches color property is visible");
+ is(isPropertyVisible("background-color", view), false,
+ "span #matches background-color property is hidden");
+
+ info("Toggling the browser styles");
+ let doc = view.styleDocument;
+ let checkbox = doc.querySelector(".includebrowserstyles");
+ let onRefreshed = inspector.once("computed-view-refreshed");
+ checkbox.click();
+ yield onRefreshed;
+
+ info("Checking the browser styles");
+ is(isPropertyVisible("color", view), true,
+ "span color property is visible");
+ is(isPropertyVisible("background-color", view), true,
+ "span background-color property is visible");
+});
+
+function isPropertyVisible(name, view) {
+ info("Checking property visibility for " + name);
+ let propertyViews = view.propertyViews;
+ for (let propView of propertyViews) {
+ if (propView.name == name) {
+ return propView.visible;
+ }
+ }
+ return false;
+}
diff --git a/devtools/client/inspector/computed/test/browser_computed_cycle_color.js b/devtools/client/inspector/computed/test/browser_computed_cycle_color.js
new file mode 100644
index 000000000..c9892fafe
--- /dev/null
+++ b/devtools/client/inspector/computed/test/browser_computed_cycle_color.js
@@ -0,0 +1,71 @@
+/* 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";
+
+// Computed view color cycling test.
+
+const TEST_URI = `
+ <style type="text/css">
+ .matches {
+ color: #f00;
+ }
+ </style>
+ <span id="matches" class="matches">Some styled text</span>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openComputedView();
+ yield selectNode("#matches", inspector);
+
+ info("Checking the property itself");
+ let container = getComputedViewPropertyView(view, "color").valueNode;
+ checkColorCycling(container, view);
+
+ info("Checking matched selectors");
+ container = yield getComputedViewMatchedRules(view, "color");
+ yield checkColorCycling(container, view);
+});
+
+function* checkColorCycling(container, view) {
+ let valueNode = container.querySelector(".computedview-color");
+ let win = view.styleWindow;
+
+ // "Authored" (default; currently the computed value)
+ is(valueNode.textContent, "rgb(255, 0, 0)",
+ "Color displayed as an RGB value.");
+
+ let tests = [{
+ value: "red",
+ comment: "Color displayed as a color name."
+ }, {
+ value: "#f00",
+ comment: "Color displayed as an authored value."
+ }, {
+ value: "hsl(0, 100%, 50%)",
+ comment: "Color displayed as an HSL value again."
+ }, {
+ value: "rgb(255, 0, 0)",
+ comment: "Color displayed as an RGB value again."
+ }];
+
+ for (let test of tests) {
+ yield checkSwatchShiftClick(container, win, test.value, test.comment);
+ }
+}
+
+function* checkSwatchShiftClick(container, win, expectedValue, comment) {
+ let swatch = container.querySelector(".computedview-colorswatch");
+ let valueNode = container.querySelector(".computedview-color");
+ swatch.scrollIntoView();
+
+ let onUnitChange = swatch.once("unit-change");
+ EventUtils.synthesizeMouseAtCenter(swatch, {
+ type: "mousedown",
+ shiftKey: true
+ }, win);
+ yield onUnitChange;
+ is(valueNode.textContent, expectedValue, comment);
+}
diff --git a/devtools/client/inspector/computed/test/browser_computed_getNodeInfo.js b/devtools/client/inspector/computed/test/browser_computed_getNodeInfo.js
new file mode 100644
index 000000000..30113e7ec
--- /dev/null
+++ b/devtools/client/inspector/computed/test/browser_computed_getNodeInfo.js
@@ -0,0 +1,178 @@
+/* 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";
+
+// Tests various output of the computed-view's getNodeInfo method.
+// This method is used by the HighlightersOverlay and TooltipsOverlay on mouseover to
+// decide which highlighter or tooltip to show when hovering over a value/name/selector
+// if any.
+//
+// For instance, browser_ruleview_selector-highlighter_01.js and
+// browser_ruleview_selector-highlighter_02.js test that the selector
+// highlighter appear when hovering over a selector in the rule-view.
+// Since the code to make this work for the computed-view is 90% the same,
+// there is no need for testing it again here.
+// This test however serves as a unit test for getNodeInfo.
+
+const {
+ VIEW_NODE_SELECTOR_TYPE,
+ VIEW_NODE_PROPERTY_TYPE,
+ VIEW_NODE_VALUE_TYPE,
+ VIEW_NODE_IMAGE_URL_TYPE
+} = require("devtools/client/inspector/shared/node-types");
+
+const TEST_URI = `
+ <style type="text/css">
+ body {
+ background: red;
+ color: white;
+ }
+ div {
+ background: green;
+ }
+ div div {
+ background-color: yellow;
+ background-image: url(chrome://global/skin/icons/warning-64.png);
+ color: red;
+ }
+ </style>
+ <div><div id="testElement">Test element</div></div>
+`;
+
+// Each item in this array must have the following properties:
+// - desc {String} will be logged for information
+// - getHoveredNode {Generator Function} received the computed-view instance as
+// argument and must return the node to be tested
+// - assertNodeInfo {Function} should check the validity of the nodeInfo
+// argument it receives
+const TEST_DATA = [
+ {
+ desc: "Testing a null node",
+ getHoveredNode: function* () {
+ return null;
+ },
+ assertNodeInfo: function (nodeInfo) {
+ is(nodeInfo, null);
+ }
+ },
+ {
+ desc: "Testing a useless node",
+ getHoveredNode: function* (view) {
+ return view.element;
+ },
+ assertNodeInfo: function (nodeInfo) {
+ is(nodeInfo, null);
+ }
+ },
+ {
+ desc: "Testing a property name",
+ getHoveredNode: function* (view) {
+ return getComputedViewProperty(view, "color").nameSpan;
+ },
+ assertNodeInfo: function (nodeInfo) {
+ is(nodeInfo.type, VIEW_NODE_PROPERTY_TYPE);
+ ok("property" in nodeInfo.value);
+ ok("value" in nodeInfo.value);
+ is(nodeInfo.value.property, "color");
+ is(nodeInfo.value.value, "rgb(255, 0, 0)");
+ }
+ },
+ {
+ desc: "Testing a property value",
+ getHoveredNode: function* (view) {
+ return getComputedViewProperty(view, "color").valueSpan;
+ },
+ assertNodeInfo: function (nodeInfo) {
+ is(nodeInfo.type, VIEW_NODE_VALUE_TYPE);
+ ok("property" in nodeInfo.value);
+ ok("value" in nodeInfo.value);
+ is(nodeInfo.value.property, "color");
+ is(nodeInfo.value.value, "rgb(255, 0, 0)");
+ }
+ },
+ {
+ desc: "Testing an image url",
+ getHoveredNode: function* (view) {
+ let {valueSpan} = getComputedViewProperty(view, "background-image");
+ return valueSpan.querySelector(".theme-link");
+ },
+ assertNodeInfo: function (nodeInfo) {
+ is(nodeInfo.type, VIEW_NODE_IMAGE_URL_TYPE);
+ ok("property" in nodeInfo.value);
+ ok("value" in nodeInfo.value);
+ is(nodeInfo.value.property, "background-image");
+ is(nodeInfo.value.value,
+ "url(\"chrome://global/skin/icons/warning-64.png\")");
+ is(nodeInfo.value.url, "chrome://global/skin/icons/warning-64.png");
+ }
+ },
+ {
+ desc: "Testing a matched rule selector (bestmatch)",
+ getHoveredNode: function* (view) {
+ let el = yield getComputedViewMatchedRules(view, "background-color");
+ return el.querySelector(".bestmatch");
+ },
+ assertNodeInfo: function (nodeInfo) {
+ is(nodeInfo.type, VIEW_NODE_SELECTOR_TYPE);
+ is(nodeInfo.value, "div div");
+ }
+ },
+ {
+ desc: "Testing a matched rule selector (matched)",
+ getHoveredNode: function* (view) {
+ let el = yield getComputedViewMatchedRules(view, "background-color");
+ return el.querySelector(".matched");
+ },
+ assertNodeInfo: function (nodeInfo) {
+ is(nodeInfo.type, VIEW_NODE_SELECTOR_TYPE);
+ is(nodeInfo.value, "div");
+ }
+ },
+ {
+ desc: "Testing a matched rule selector (parentmatch)",
+ getHoveredNode: function* (view) {
+ let el = yield getComputedViewMatchedRules(view, "color");
+ return el.querySelector(".parentmatch");
+ },
+ assertNodeInfo: function (nodeInfo) {
+ is(nodeInfo.type, VIEW_NODE_SELECTOR_TYPE);
+ is(nodeInfo.value, "body");
+ }
+ },
+ {
+ desc: "Testing a matched rule value",
+ getHoveredNode: function* (view) {
+ let el = yield getComputedViewMatchedRules(view, "color");
+ return el.querySelector(".other-property-value");
+ },
+ assertNodeInfo: function (nodeInfo) {
+ is(nodeInfo.type, VIEW_NODE_VALUE_TYPE);
+ is(nodeInfo.value.property, "color");
+ is(nodeInfo.value.value, "red");
+ }
+ },
+ {
+ desc: "Testing a matched rule stylesheet link",
+ getHoveredNode: function* (view) {
+ let el = yield getComputedViewMatchedRules(view, "color");
+ return el.querySelector(".rule-link .theme-link");
+ },
+ assertNodeInfo: function (nodeInfo) {
+ is(nodeInfo, null);
+ }
+ }
+];
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openComputedView();
+ yield selectNode("#testElement", inspector);
+
+ for (let {desc, getHoveredNode, assertNodeInfo} of TEST_DATA) {
+ info(desc);
+ let nodeInfo = view.getNodeInfo(yield getHoveredNode(view));
+ assertNodeInfo(nodeInfo);
+ }
+});
diff --git a/devtools/client/inspector/computed/test/browser_computed_keybindings_01.js b/devtools/client/inspector/computed/test/browser_computed_keybindings_01.js
new file mode 100644
index 000000000..199e125af
--- /dev/null
+++ b/devtools/client/inspector/computed/test/browser_computed_keybindings_01.js
@@ -0,0 +1,83 @@
+/* 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";
+
+// Tests computed view key bindings.
+
+const TEST_URI = `
+ <style type="text/css">
+ .matches {
+ color: #F00;
+ }
+ </style>
+ <span class="matches">Some styled text</span>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openComputedView();
+ yield selectNode(".matches", inspector);
+
+ let propView = getFirstVisiblePropertyView(view);
+ let rulesTable = propView.matchedSelectorsContainer;
+ let matchedExpander = propView.element;
+
+ info("Focusing the property");
+ matchedExpander.scrollIntoView();
+ let onMatchedExpanderFocus = once(matchedExpander, "focus", true);
+ EventUtils.synthesizeMouseAtCenter(matchedExpander, {}, view.styleWindow);
+ yield onMatchedExpanderFocus;
+
+ yield checkToggleKeyBinding(view.styleWindow, "VK_SPACE", rulesTable,
+ inspector);
+ yield checkToggleKeyBinding(view.styleWindow, "VK_RETURN", rulesTable,
+ inspector);
+ yield checkHelpLinkKeybinding(view);
+});
+
+function getFirstVisiblePropertyView(view) {
+ let propView = null;
+ view.propertyViews.some(p => {
+ if (p.visible) {
+ propView = p;
+ return true;
+ }
+ return false;
+ });
+
+ return propView;
+}
+
+function* checkToggleKeyBinding(win, key, rulesTable, inspector) {
+ info("Pressing " + key + " key a couple of times to check that the " +
+ "property gets expanded/collapsed");
+
+ let onExpand = inspector.once("computed-view-property-expanded");
+ let onCollapse = inspector.once("computed-view-property-collapsed");
+
+ info("Expanding the property");
+ EventUtils.synthesizeKey(key, {}, win);
+ yield onExpand;
+ isnot(rulesTable.innerHTML, "", "The property has been expanded");
+
+ info("Collapsing the property");
+ EventUtils.synthesizeKey(key, {}, win);
+ yield onCollapse;
+ is(rulesTable.innerHTML, "", "The property has been collapsed");
+}
+
+function checkHelpLinkKeybinding(view) {
+ info("Check that MDN link is opened on \"F1\"");
+ let def = defer();
+
+ let propView = getFirstVisiblePropertyView(view);
+ propView.mdnLinkClick = function (event) {
+ ok(true, "Pressing F1 opened the MDN link");
+ def.resolve();
+ };
+
+ EventUtils.synthesizeKey("VK_F1", {}, view.styleWindow);
+ return def.promise;
+}
diff --git a/devtools/client/inspector/computed/test/browser_computed_keybindings_02.js b/devtools/client/inspector/computed/test/browser_computed_keybindings_02.js
new file mode 100644
index 000000000..2a9220ec8
--- /dev/null
+++ b/devtools/client/inspector/computed/test/browser_computed_keybindings_02.js
@@ -0,0 +1,66 @@
+/* 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";
+
+// Tests the computed-view keyboard navigation.
+
+const TEST_URI = `
+ <style type="text/css">
+ span {
+ font-variant: small-caps;
+ color: #000000;
+ }
+ .nomatches {
+ color: #ff0000;
+ }
+ </style>
+ <div id="first" style="margin: 10em;
+ font-size: 14pt; font-family: helvetica, sans-serif; color: #AAA">
+ <h1>Some header text</h1>
+ <p id="salutation" style="font-size: 12pt">hi.</p>
+ <p id="body" style="font-size: 12pt">I am a test-case. This text exists
+ solely to provide some things to <span style="color: yellow">
+ highlight</span> and <span style="font-weight: bold">count</span>
+ style list-items in the box at right. If you are reading this,
+ you should go do something else instead. Maybe read a book. Or better
+ yet, write some test-cases for another bit of code.
+ <span style="font-style: italic">some text</span></p>
+ <p id="closing">more text</p>
+ <p>even more text</p>
+ </div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openComputedView();
+ yield selectNode("span", inspector);
+
+ info("Selecting the first computed style in the list");
+ let firstStyle = view.styleDocument.querySelector(".property-view");
+ ok(firstStyle, "First computed style found in panel");
+ firstStyle.focus();
+
+ info("Tab to select the 2nd style and press return");
+ let onExpanded = inspector.once("computed-view-property-expanded");
+ EventUtils.synthesizeKey("VK_TAB", {});
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ yield onExpanded;
+
+ info("Verify the 2nd style has been expanded");
+ let secondStyleSelectors = view.styleDocument.querySelectorAll(
+ ".property-content .matchedselectors")[1];
+ ok(secondStyleSelectors.childNodes.length > 0, "Matched selectors expanded");
+
+ info("Tab back up and test the same thing, with space");
+ onExpanded = inspector.once("computed-view-property-expanded");
+ EventUtils.synthesizeKey("VK_TAB", {shiftKey: true});
+ EventUtils.synthesizeKey("VK_SPACE", {});
+ yield onExpanded;
+
+ info("Verify the 1st style has been expanded too");
+ let firstStyleSelectors = view.styleDocument.querySelectorAll(
+ ".property-content .matchedselectors")[0];
+ ok(firstStyleSelectors.childNodes.length > 0, "Matched selectors expanded");
+});
diff --git a/devtools/client/inspector/computed/test/browser_computed_matched-selectors-toggle.js b/devtools/client/inspector/computed/test/browser_computed_matched-selectors-toggle.js
new file mode 100644
index 000000000..abbbb77be
--- /dev/null
+++ b/devtools/client/inspector/computed/test/browser_computed_matched-selectors-toggle.js
@@ -0,0 +1,104 @@
+/* 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";
+
+// Tests that the computed view properties can be expanded and collapsed with
+// either the twisty or by dbl-clicking on the container.
+
+const TEST_URI = `
+ <style type="text/css"> ,
+ html { color: #000000; font-size: 15pt; }
+ h1 { color: red; }
+ </style>
+ <h1>Some header text</h1>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openComputedView();
+ yield selectNode("h1", inspector);
+
+ yield testExpandOnTwistyClick(view, inspector);
+ yield testCollapseOnTwistyClick(view, inspector);
+ yield testExpandOnDblClick(view, inspector);
+ yield testCollapseOnDblClick(view, inspector);
+});
+
+function* testExpandOnTwistyClick({styleDocument, styleWindow}, inspector) {
+ info("Testing that a property expands on twisty click");
+
+ info("Getting twisty element");
+ let twisty = styleDocument.querySelector("#propertyContainer .expandable");
+ ok(twisty, "Twisty found");
+
+ let onExpand = inspector.once("computed-view-property-expanded");
+ info("Clicking on the twisty element");
+ twisty.click();
+
+ yield onExpand;
+
+ // Expanded means the matchedselectors div is not empty
+ let div = styleDocument.querySelector(".property-content .matchedselectors");
+ ok(div.childNodes.length > 0,
+ "Matched selectors are expanded on twisty click");
+}
+
+function* testCollapseOnTwistyClick({styleDocument, styleWindow}, inspector) {
+ info("Testing that a property collapses on twisty click");
+
+ info("Getting twisty element");
+ let twisty = styleDocument.querySelector("#propertyContainer .expandable");
+ ok(twisty, "Twisty found");
+
+ let onCollapse = inspector.once("computed-view-property-collapsed");
+ info("Clicking on the twisty element");
+ twisty.click();
+
+ yield onCollapse;
+
+ // Collapsed means the matchedselectors div is empty
+ let div = styleDocument.querySelector(".property-content .matchedselectors");
+ ok(div.childNodes.length === 0,
+ "Matched selectors are collapsed on twisty click");
+}
+
+function* testExpandOnDblClick({styleDocument, styleWindow}, inspector) {
+ info("Testing that a property expands on container dbl-click");
+
+ info("Getting computed property container");
+ let container = styleDocument.querySelector(".property-view");
+ ok(container, "Container found");
+
+ container.scrollIntoView();
+
+ let onExpand = inspector.once("computed-view-property-expanded");
+ info("Dbl-clicking on the container");
+ EventUtils.synthesizeMouseAtCenter(container, {clickCount: 2}, styleWindow);
+
+ yield onExpand;
+
+ // Expanded means the matchedselectors div is not empty
+ let div = styleDocument.querySelector(".property-content .matchedselectors");
+ ok(div.childNodes.length > 0, "Matched selectors are expanded on dblclick");
+}
+
+function* testCollapseOnDblClick({styleDocument, styleWindow}, inspector) {
+ info("Testing that a property collapses on container dbl-click");
+
+ info("Getting computed property container");
+ let container = styleDocument.querySelector(".property-view");
+ ok(container, "Container found");
+
+ let onCollapse = inspector.once("computed-view-property-collapsed");
+ info("Dbl-clicking on the container");
+ EventUtils.synthesizeMouseAtCenter(container, {clickCount: 2}, styleWindow);
+
+ yield onCollapse;
+
+ // Collapsed means the matchedselectors div is empty
+ let div = styleDocument.querySelector(".property-content .matchedselectors");
+ ok(div.childNodes.length === 0,
+ "Matched selectors are collapsed on dblclick");
+}
diff --git a/devtools/client/inspector/computed/test/browser_computed_matched-selectors_01.js b/devtools/client/inspector/computed/test/browser_computed_matched-selectors_01.js
new file mode 100644
index 000000000..66cabe7a9
--- /dev/null
+++ b/devtools/client/inspector/computed/test/browser_computed_matched-selectors_01.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";
+
+// Checking selector counts, matched rules and titles in the computed-view.
+
+const {PropertyView} =
+ require("devtools/client/inspector/computed/computed");
+const TEST_URI = URL_ROOT + "doc_matched_selectors.html";
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let {inspector, view} = yield openComputedView();
+
+ yield selectNode("#test", inspector);
+ yield testMatchedSelectors(view, inspector);
+});
+
+function* testMatchedSelectors(view, inspector) {
+ info("checking selector counts, matched rules and titles");
+
+ let nodeFront = yield getNodeFront("#test", inspector);
+ is(nodeFront, view._viewedElement,
+ "style inspector node matches the selected node");
+
+ let propertyView = new PropertyView(view, "color");
+ propertyView.buildMain();
+ propertyView.buildSelectorContainer();
+ propertyView.matchedExpanded = true;
+
+ yield propertyView.refreshMatchedSelectors();
+
+ let numMatchedSelectors = propertyView.matchedSelectors.length;
+ is(numMatchedSelectors, 6,
+ "CssLogic returns the correct number of matched selectors for div");
+ is(propertyView.hasMatchedSelectors, true,
+ "hasMatchedSelectors returns true");
+}
diff --git a/devtools/client/inspector/computed/test/browser_computed_matched-selectors_02.js b/devtools/client/inspector/computed/test/browser_computed_matched-selectors_02.js
new file mode 100644
index 000000000..43172d55f
--- /dev/null
+++ b/devtools/client/inspector/computed/test/browser_computed_matched-selectors_02.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";
+
+// Tests for matched selector texts in the computed view.
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8,<div style='color:blue;'></div>");
+ let {inspector, view} = yield openComputedView();
+ yield selectNode("div", inspector);
+
+ info("Checking the color property view");
+ let propertyView = getPropertyView(view, "color");
+ ok(propertyView, "found PropertyView for color");
+ is(propertyView.hasMatchedSelectors, true, "hasMatchedSelectors is true");
+
+ info("Expanding the matched selectors");
+ propertyView.matchedExpanded = true;
+ yield propertyView.refreshMatchedSelectors();
+
+ let span = propertyView.matchedSelectorsContainer
+ .querySelector("span.rule-text");
+ ok(span, "Found the first table row");
+
+ let selector = propertyView.matchedSelectorViews[0];
+ ok(selector, "Found the first matched selector view");
+});
+
+function getPropertyView(computedView, name) {
+ let propertyView = null;
+ computedView.propertyViews.some(function (view) {
+ if (view.name == name) {
+ propertyView = view;
+ return true;
+ }
+ return false;
+ });
+ return propertyView;
+}
diff --git a/devtools/client/inspector/computed/test/browser_computed_media-queries.js b/devtools/client/inspector/computed/test/browser_computed_media-queries.js
new file mode 100644
index 000000000..79cccb49b
--- /dev/null
+++ b/devtools/client/inspector/computed/test/browser_computed_media-queries.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";
+
+// Tests that we correctly display appropriate media query titles in the
+// property view.
+
+const TEST_URI = URL_ROOT + "doc_media_queries.html";
+
+var {PropertyView} = require("devtools/client/inspector/computed/computed");
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let {inspector, view} = yield openComputedView();
+ yield selectNode("div", inspector);
+ yield checkPropertyView(view);
+});
+
+function checkPropertyView(view) {
+ let propertyView = new PropertyView(view, "width");
+ propertyView.buildMain();
+ propertyView.buildSelectorContainer();
+ propertyView.matchedExpanded = true;
+
+ return propertyView.refreshMatchedSelectors().then(() => {
+ let numMatchedSelectors = propertyView.matchedSelectors.length;
+
+ is(numMatchedSelectors, 2,
+ "Property view has the correct number of matched selectors for div");
+
+ is(propertyView.hasMatchedSelectors, true,
+ "hasMatchedSelectors returns true");
+ });
+}
diff --git a/devtools/client/inspector/computed/test/browser_computed_no-results-placeholder.js b/devtools/client/inspector/computed/test/browser_computed_no-results-placeholder.js
new file mode 100644
index 000000000..b1371abd7
--- /dev/null
+++ b/devtools/client/inspector/computed/test/browser_computed_no-results-placeholder.js
@@ -0,0 +1,70 @@
+/* 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";
+
+// Tests that the no results placeholder works properly.
+
+const TEST_URI = `
+ <style type="text/css">
+ .matches {
+ color: #F00;
+ }
+ </style>
+ <span id="matches" class="matches">Some styled text</span>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openComputedView();
+ yield selectNode("#matches", inspector);
+
+ yield enterInvalidFilter(inspector, view);
+ checkNoResultsPlaceholderShown(view);
+
+ yield clearFilterText(inspector, view);
+ checkNoResultsPlaceholderHidden(view);
+});
+
+function* enterInvalidFilter(inspector, computedView) {
+ let searchbar = computedView.searchField;
+ let searchTerm = "xxxxx";
+
+ info("setting filter text to \"" + searchTerm + "\"");
+
+ let onRefreshed = inspector.once("computed-view-refreshed");
+ searchbar.focus();
+ synthesizeKeys(searchTerm, computedView.styleWindow);
+ yield onRefreshed;
+}
+
+function checkNoResultsPlaceholderShown(computedView) {
+ info("Checking that the no results placeholder is shown");
+
+ let placeholder = computedView.noResults;
+ let win = computedView.styleWindow;
+ let display = win.getComputedStyle(placeholder).display;
+ is(display, "block", "placeholder is visible");
+}
+
+function* clearFilterText(inspector, computedView) {
+ info("Clearing the filter text");
+
+ let searchbar = computedView.searchField;
+
+ let onRefreshed = inspector.once("computed-view-refreshed");
+ searchbar.focus();
+ searchbar.value = "";
+ EventUtils.synthesizeKey("c", {}, computedView.styleWindow);
+ yield onRefreshed;
+}
+
+function checkNoResultsPlaceholderHidden(computedView) {
+ info("Checking that the no results placeholder is hidden");
+
+ let placeholder = computedView.noResults;
+ let win = computedView.styleWindow;
+ let display = win.getComputedStyle(placeholder).display;
+ is(display, "none", "placeholder is hidden");
+}
diff --git a/devtools/client/inspector/computed/test/browser_computed_original-source-link.js b/devtools/client/inspector/computed/test/browser_computed_original-source-link.js
new file mode 100644
index 000000000..1bceed4e3
--- /dev/null
+++ b/devtools/client/inspector/computed/test/browser_computed_original-source-link.js
@@ -0,0 +1,73 @@
+/* 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";
+
+// Tests that the computed view shows the original source link when source maps
+// are enabled.
+
+const TESTCASE_URI = URL_ROOT_SSL + "doc_sourcemaps.html";
+const PREF = "devtools.styleeditor.source-maps-enabled";
+const SCSS_LOC = "doc_sourcemaps.scss:4";
+const CSS_LOC = "doc_sourcemaps.css:1";
+
+add_task(function* () {
+ info("Turning the pref " + PREF + " on");
+ Services.prefs.setBoolPref(PREF, true);
+
+ yield addTab(TESTCASE_URI);
+ let {toolbox, inspector, view} = yield openComputedView();
+ yield selectNode("div", inspector);
+
+ info("Expanding the first property");
+ yield expandComputedViewPropertyByIndex(view, 0);
+
+ info("Verifying the link text");
+ // Forcing a call to updateSourceLink on the SelectorView here. The
+ // computed-view already does it, but we have no way of waiting for it to be
+ // done here, so just call it again and wait for the returned promise to
+ // resolve.
+ let propertyView = getComputedViewPropertyView(view, "color");
+ yield propertyView.matchedSelectorViews[0].updateSourceLink();
+ verifyLinkText(view, SCSS_LOC);
+
+ info("Toggling the pref");
+ let onLinksUpdated = inspector.once("computed-view-sourcelinks-updated");
+ Services.prefs.setBoolPref(PREF, false);
+ yield onLinksUpdated;
+
+ info("Verifying that the link text has changed after the pref change");
+ yield verifyLinkText(view, CSS_LOC);
+
+ info("Toggling the pref again");
+ onLinksUpdated = inspector.once("computed-view-sourcelinks-updated");
+ Services.prefs.setBoolPref(PREF, true);
+ yield onLinksUpdated;
+
+ info("Testing that clicking on the link works");
+ yield testClickingLink(toolbox, view);
+
+ info("Turning the pref " + PREF + " off");
+ Services.prefs.clearUserPref(PREF);
+});
+
+function* testClickingLink(toolbox, view) {
+ let onEditor = waitForStyleEditor(toolbox, "doc_sourcemaps.scss");
+
+ info("Clicking the computedview stylesheet link");
+ let link = getComputedViewLinkByIndex(view, 0);
+ link.scrollIntoView();
+ link.click();
+
+ let editor = yield onEditor;
+
+ let {line} = editor.sourceEditor.getCursor();
+ is(line, 3, "cursor is at correct line number in original source");
+}
+
+function verifyLinkText(view, text) {
+ let link = getComputedViewLinkByIndex(view, 0);
+ is(link.textContent, text,
+ "Linked text changed to display the correct location");
+}
diff --git a/devtools/client/inspector/computed/test/browser_computed_pseudo-element_01.js b/devtools/client/inspector/computed/test/browser_computed_pseudo-element_01.js
new file mode 100644
index 000000000..9ca5451a5
--- /dev/null
+++ b/devtools/client/inspector/computed/test/browser_computed_pseudo-element_01.js
@@ -0,0 +1,39 @@
+/* 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";
+
+// Tests that pseudoelements are displayed correctly in the rule view.
+
+const TEST_URI = URL_ROOT + "doc_pseudoelement.html";
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let {inspector, view} = yield openComputedView();
+ yield testTopLeft(inspector, view);
+});
+
+function* testTopLeft(inspector, view) {
+ let node = yield getNodeFront("#topleft", inspector.markup);
+ yield selectNode(node, inspector);
+ let float = getComputedViewPropertyValue(view, "float");
+ is(float, "left", "The computed view shows the correct float");
+
+ let children = yield inspector.markup.walker.children(node);
+ is(children.nodes.length, 3, "Element has correct number of children");
+
+ let beforeElement = children.nodes[0];
+ yield selectNode(beforeElement, inspector);
+ let top = getComputedViewPropertyValue(view, "top");
+ is(top, "0px", "The computed view shows the correct top");
+ let left = getComputedViewPropertyValue(view, "left");
+ is(left, "0px", "The computed view shows the correct left");
+
+ let afterElement = children.nodes[children.nodes.length - 1];
+ yield selectNode(afterElement, inspector);
+ top = getComputedViewPropertyValue(view, "top");
+ is(top, "50%", "The computed view shows the correct top");
+ left = getComputedViewPropertyValue(view, "left");
+ is(left, "50%", "The computed view shows the correct left");
+}
diff --git a/devtools/client/inspector/computed/test/browser_computed_refresh-on-style-change_01.js b/devtools/client/inspector/computed/test/browser_computed_refresh-on-style-change_01.js
new file mode 100644
index 000000000..43f210307
--- /dev/null
+++ b/devtools/client/inspector/computed/test/browser_computed_refresh-on-style-change_01.js
@@ -0,0 +1,30 @@
+/* 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";
+
+// Tests that the computed view refreshes when the current node has its style
+// changed.
+
+const TEST_URI = "<div id='testdiv' style='font-size:10px;'>Test div!</div>";
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view, testActor} = yield openComputedView();
+ yield selectNode("#testdiv", inspector);
+
+ let fontSize = getComputedViewPropertyValue(view, "font-size");
+ is(fontSize, "10px", "The computed view shows the right font-size");
+
+ info("Changing the node's style and waiting for the update");
+ let onUpdated = inspector.once("computed-view-refreshed");
+ yield testActor.setAttribute("#testdiv", "style",
+ "font-size: 15px; color: red;");
+ yield onUpdated;
+
+ fontSize = getComputedViewPropertyValue(view, "font-size");
+ is(fontSize, "15px", "The computed view shows the updated font-size");
+ let color = getComputedViewPropertyValue(view, "color");
+ is(color, "rgb(255, 0, 0)", "The computed view also shows the color now");
+});
diff --git a/devtools/client/inspector/computed/test/browser_computed_search-filter.js b/devtools/client/inspector/computed/test/browser_computed_search-filter.js
new file mode 100644
index 000000000..10ba82293
--- /dev/null
+++ b/devtools/client/inspector/computed/test/browser_computed_search-filter.js
@@ -0,0 +1,66 @@
+/* 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";
+
+// Tests that the search filter works properly.
+
+const TEST_URI = `
+ <style type="text/css">
+ .matches {
+ color: #F00;
+ }
+ </style>
+ <span id="matches" class="matches">Some styled text</span>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openComputedView();
+ yield selectNode("#matches", inspector);
+ yield testToggleDefaultStyles(inspector, view);
+ yield testAddTextInFilter(inspector, view);
+});
+
+function* testToggleDefaultStyles(inspector, computedView) {
+ info("checking \"Browser styles\" checkbox");
+ let checkbox = computedView.includeBrowserStylesCheckbox;
+ let onRefreshed = inspector.once("computed-view-refreshed");
+ checkbox.click();
+ yield onRefreshed;
+}
+
+function* testAddTextInFilter(inspector, computedView) {
+ info("setting filter text to \"color\"");
+ let doc = computedView.styleDocument;
+ let boxModelWrapper = doc.querySelector("#boxmodel-wrapper");
+ let searchField = computedView.searchField;
+ let onRefreshed = inspector.once("computed-view-refreshed");
+ let win = computedView.styleWindow;
+
+ // First check to make sure that accel + F doesn't focus search if the
+ // container isn't focused
+ inspector.panelWin.focus();
+ EventUtils.synthesizeKey("f", { accelKey: true });
+ isnot(inspector.panelDoc.activeElement, searchField,
+ "Search field isn't focused");
+
+ computedView.element.focus();
+ EventUtils.synthesizeKey("f", { accelKey: true });
+ is(inspector.panelDoc.activeElement, searchField, "Search field is focused");
+
+ synthesizeKeys("color", win);
+ yield onRefreshed;
+
+ ok(boxModelWrapper.hidden, "Box model is hidden");
+
+ info("check that the correct properties are visible");
+
+ let propertyViews = computedView.propertyViews;
+ propertyViews.forEach(propView => {
+ let name = propView.name;
+ is(propView.visible, name.indexOf("color") > -1,
+ "span " + name + " property visibility check");
+ });
+}
diff --git a/devtools/client/inspector/computed/test/browser_computed_search-filter_clear.js b/devtools/client/inspector/computed/test/browser_computed_search-filter_clear.js
new file mode 100644
index 000000000..bd989854f
--- /dev/null
+++ b/devtools/client/inspector/computed/test/browser_computed_search-filter_clear.js
@@ -0,0 +1,71 @@
+/* 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";
+
+// Tests that the search filter clear button works properly.
+
+const TEST_URI = `
+ <style type="text/css">
+ .matches {
+ color: #F00;
+ background-color: #00F;
+ border-color: #0F0;
+ }
+ </style>
+ <span id="matches" class="matches">Some styled text</span>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openComputedView();
+ yield selectNode("#matches", inspector);
+ yield testAddTextInFilter(inspector, view);
+ yield testClearSearchFilter(inspector, view);
+});
+
+function* testAddTextInFilter(inspector, computedView) {
+ info("Setting filter text to \"background-color\"");
+
+ let win = computedView.styleWindow;
+ let propertyViews = computedView.propertyViews;
+ let searchField = computedView.searchField;
+
+ searchField.focus();
+ synthesizeKeys("background-color", win);
+ yield inspector.once("computed-view-refreshed");
+
+ info("Check that the correct properties are visible");
+
+ propertyViews.forEach((propView) => {
+ let name = propView.name;
+ is(propView.visible, name.indexOf("background-color") > -1,
+ "span " + name + " property visibility check");
+ });
+}
+
+function* testClearSearchFilter(inspector, computedView) {
+ info("Clearing the search filter");
+
+ let win = computedView.styleWindow;
+ let doc = computedView.styleDocument;
+ let boxModelWrapper = doc.querySelector("#boxmodel-wrapper");
+ let propertyViews = computedView.propertyViews;
+ let searchField = computedView.searchField;
+ let searchClearButton = computedView.searchClearButton;
+ let onRefreshed = inspector.once("computed-view-refreshed");
+
+ EventUtils.synthesizeMouseAtCenter(searchClearButton, {}, win);
+ yield onRefreshed;
+
+ ok(!boxModelWrapper.hidden, "Box model is displayed");
+
+ info("Check that the correct properties are visible");
+
+ ok(!searchField.value, "Search filter is cleared");
+ propertyViews.forEach((propView) => {
+ is(propView.visible, propView.hasMatchedSelectors,
+ "span " + propView.name + " property visibility check");
+ });
+}
diff --git a/devtools/client/inspector/computed/test/browser_computed_search-filter_context-menu.js b/devtools/client/inspector/computed/test/browser_computed_search-filter_context-menu.js
new file mode 100644
index 000000000..b5dbe4475
--- /dev/null
+++ b/devtools/client/inspector/computed/test/browser_computed_search-filter_context-menu.js
@@ -0,0 +1,84 @@
+/* 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";
+
+// Tests computed view search filter context menu works properly.
+
+const TEST_INPUT = "h1";
+
+const TEST_URI = "<h1>test filter context menu</h1>";
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {toolbox, inspector, view} = yield openComputedView();
+ yield selectNode("h1", inspector);
+
+ let win = view.styleWindow;
+ let searchField = view.searchField;
+ let searchContextMenu = toolbox.textBoxContextMenuPopup;
+ ok(searchContextMenu,
+ "The search filter context menu is loaded in the computed view");
+
+ let cmdUndo = searchContextMenu.querySelector("[command=cmd_undo]");
+ let cmdDelete = searchContextMenu.querySelector("[command=cmd_delete]");
+ let cmdSelectAll = searchContextMenu.querySelector("[command=cmd_selectAll]");
+ let cmdCut = searchContextMenu.querySelector("[command=cmd_cut]");
+ let cmdCopy = searchContextMenu.querySelector("[command=cmd_copy]");
+ let cmdPaste = searchContextMenu.querySelector("[command=cmd_paste]");
+
+ info("Opening context menu");
+
+ emptyClipboard();
+
+ let onFocus = once(searchField, "focus");
+ searchField.focus();
+ yield onFocus;
+
+ let onContextMenuPopup = once(searchContextMenu, "popupshowing");
+ EventUtils.synthesizeMouse(searchField, 2, 2,
+ {type: "contextmenu", button: 2}, win);
+ yield onContextMenuPopup;
+
+ is(cmdUndo.getAttribute("disabled"), "true", "cmdUndo is disabled");
+ is(cmdDelete.getAttribute("disabled"), "true", "cmdDelete is disabled");
+ is(cmdSelectAll.getAttribute("disabled"), "true", "cmdSelectAll is disabled");
+
+ // Cut/Copy items are enabled in context menu even if there
+ // is no selection. See also Bug 1303033
+ is(cmdCut.getAttribute("disabled"), "", "cmdCut is enabled");
+ is(cmdCopy.getAttribute("disabled"), "", "cmdCopy is enabled");
+
+ if (isWindows()) {
+ // emptyClipboard only works on Windows (666254), assert paste only for this OS.
+ is(cmdPaste.getAttribute("disabled"), "true", "cmdPaste is disabled");
+ }
+
+ info("Closing context menu");
+ let onContextMenuHidden = once(searchContextMenu, "popuphidden");
+ searchContextMenu.hidePopup();
+ yield onContextMenuHidden;
+
+ info("Copy text in search field using the context menu");
+ searchField.value = TEST_INPUT;
+ searchField.select();
+ EventUtils.synthesizeMouse(searchField, 2, 2,
+ {type: "contextmenu", button: 2}, win);
+ yield onContextMenuPopup;
+ yield waitForClipboardPromise(() => cmdCopy.click(), TEST_INPUT);
+ searchContextMenu.hidePopup();
+ yield onContextMenuHidden;
+
+ info("Reopen context menu and check command properties");
+ EventUtils.synthesizeMouse(searchField, 2, 2,
+ {type: "contextmenu", button: 2}, win);
+ yield onContextMenuPopup;
+
+ is(cmdUndo.getAttribute("disabled"), "", "cmdUndo is enabled");
+ is(cmdDelete.getAttribute("disabled"), "", "cmdDelete is enabled");
+ is(cmdSelectAll.getAttribute("disabled"), "", "cmdSelectAll is enabled");
+ is(cmdCut.getAttribute("disabled"), "", "cmdCut is enabled");
+ is(cmdCopy.getAttribute("disabled"), "", "cmdCopy is enabled");
+ is(cmdPaste.getAttribute("disabled"), "", "cmdPaste is enabled");
+});
diff --git a/devtools/client/inspector/computed/test/browser_computed_search-filter_escape-keypress.js b/devtools/client/inspector/computed/test/browser_computed_search-filter_escape-keypress.js
new file mode 100644
index 000000000..e52e2cc89
--- /dev/null
+++ b/devtools/client/inspector/computed/test/browser_computed_search-filter_escape-keypress.js
@@ -0,0 +1,75 @@
+/* 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";
+
+// Avoid test timeouts on Linux debug builds where the test takes just a bit too long to
+// run (see bug 1258081).
+requestLongerTimeout(2);
+
+// Tests that search filter escape keypress will clear the search field.
+
+const TEST_URI = `
+ <style type="text/css">
+ .matches {
+ color: #F00;
+ }
+ </style>
+ <span id="matches" class="matches">Some styled text</span>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openComputedView();
+ yield selectNode("#matches", inspector);
+ yield testAddTextInFilter(inspector, view);
+ yield testEscapeKeypress(inspector, view);
+});
+
+function* testAddTextInFilter(inspector, computedView) {
+ info("Setting filter text to \"background-color\"");
+
+ let win = computedView.styleWindow;
+ let propertyViews = computedView.propertyViews;
+ let searchField = computedView.searchField;
+ let checkbox = computedView.includeBrowserStylesCheckbox;
+
+ info("Include browser styles");
+ checkbox.click();
+ yield inspector.once("computed-view-refreshed");
+
+ searchField.focus();
+ synthesizeKeys("background-color", win);
+ yield inspector.once("computed-view-refreshed");
+
+ info("Check that the correct properties are visible");
+
+ propertyViews.forEach((propView) => {
+ let name = propView.name;
+ is(propView.visible, name.indexOf("background-color") > -1,
+ "span " + name + " property visibility check");
+ });
+}
+
+function* testEscapeKeypress(inspector, computedView) {
+ info("Pressing the escape key on search filter");
+
+ let win = computedView.styleWindow;
+ let propertyViews = computedView.propertyViews;
+ let searchField = computedView.searchField;
+ let onRefreshed = inspector.once("computed-view-refreshed");
+
+ searchField.focus();
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, win);
+ yield onRefreshed;
+
+ info("Check that the correct properties are visible");
+
+ ok(!searchField.value, "Search filter is cleared");
+ propertyViews.forEach((propView) => {
+ let name = propView.name;
+ is(propView.visible, true,
+ "span " + name + " property is visible");
+ });
+}
diff --git a/devtools/client/inspector/computed/test/browser_computed_search-filter_noproperties.js b/devtools/client/inspector/computed/test/browser_computed_search-filter_noproperties.js
new file mode 100644
index 000000000..99ee6d58a
--- /dev/null
+++ b/devtools/client/inspector/computed/test/browser_computed_search-filter_noproperties.js
@@ -0,0 +1,61 @@
+/* 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";
+
+// Tests that the "no-results" message is displayed when selecting an invalid element or
+// when all properties have been filtered out.
+
+const TEST_URI = `
+ <style type="text/css">
+ .matches {
+ color: #F00;
+ background-color: #00F;
+ border-color: #0F0;
+ }
+ </style>
+ <div>
+ <!-- comment node -->
+ <span id="matches" class="matches">Some styled text</span>
+ </div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openComputedView();
+ let propertyViews = view.propertyViews;
+
+ info("Select the #matches node");
+ let matchesNode = yield getNodeFront("#matches", inspector);
+ let onRefresh = inspector.once("computed-view-refreshed");
+ yield selectNode(matchesNode, inspector);
+ yield onRefresh;
+
+ ok(propertyViews.filter(p => p.visible).length > 0, "CSS properties are displayed");
+ ok(view.noResults.hasAttribute("hidden"), "no-results message is hidden");
+
+ info("Select a comment node");
+ let commentNode = yield inspector.walker.previousSibling(matchesNode);
+ yield selectNode(commentNode, inspector);
+
+ is(propertyViews.filter(p => p.visible).length, 0, "No properties displayed");
+ ok(!view.noResults.hasAttribute("hidden"), "no-results message is displayed");
+
+ info("Select the #matches node again");
+ onRefresh = inspector.once("computed-view-refreshed");
+ yield selectNode(matchesNode, inspector);
+ yield onRefresh;
+
+ ok(propertyViews.filter(p => p.visible).length > 0, "CSS properties are displayed");
+ ok(view.noResults.hasAttribute("hidden"), "no-results message is hidden");
+
+ info("Filter by 'will-not-match' and check the no-results message is displayed");
+ let searchField = view.searchField;
+ searchField.focus();
+ synthesizeKeys("will-not-match", view.styleWindow);
+ yield inspector.once("computed-view-refreshed");
+
+ is(propertyViews.filter(p => p.visible).length, 0, "No properties displayed");
+ ok(!view.noResults.hasAttribute("hidden"), "no-results message is displayed");
+});
diff --git a/devtools/client/inspector/computed/test/browser_computed_select-and-copy-styles.js b/devtools/client/inspector/computed/test/browser_computed_select-and-copy-styles.js
new file mode 100644
index 000000000..ce8be59ad
--- /dev/null
+++ b/devtools/client/inspector/computed/test/browser_computed_select-and-copy-styles.js
@@ -0,0 +1,118 @@
+/* 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";
+
+// Tests that properties can be selected and copied from the computed view.
+
+const osString = Services.appinfo.OS;
+
+const TEST_URI = `
+ <style type="text/css">
+ span {
+ font-variant-caps: small-caps;
+ color: #000000;
+ }
+ .nomatches {
+ color: #ff0000;
+ }
+ </style>
+ <div id="first" style="margin: 10em;
+ font-size: 14pt; font-family: helvetica, sans-serif; color: #AAA">
+ <h1>Some header text</h1>
+ <p id="salutation" style="font-size: 12pt">hi.</p>
+ <p id="body" style="font-size: 12pt">I am a test-case. This text exists
+ solely to provide some things to <span style="color: yellow">
+ highlight</span> and <span style="font-weight: bold">count</span>
+ style list-items in the box at right. If you are reading this,
+ you should go do something else instead. Maybe read a book. Or better
+ yet, write some test-cases for another bit of code.
+ <span style="font-style: italic">some text</span></p>
+ <p id="closing">more text</p>
+ <p>even more text</p>
+ </div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openComputedView();
+ yield selectNode("span", inspector);
+ yield checkCopySelection(view);
+ yield checkSelectAll(view);
+});
+
+function* checkCopySelection(view) {
+ info("Testing selection copy");
+
+ let contentDocument = view.styleDocument;
+ let props = contentDocument.querySelectorAll(".property-view");
+ ok(props, "captain, we have the property-view nodes");
+
+ let range = contentDocument.createRange();
+ range.setStart(props[1], 0);
+ range.setEnd(props[3], 2);
+ contentDocument.defaultView.getSelection().addRange(range);
+
+ info("Checking that cssHtmlTree.siBoundCopy() returns the correct " +
+ "clipboard value");
+
+ let expectedPattern = "font-family: helvetica,sans-serif;[\\r\\n]+" +
+ "font-size: 16px;[\\r\\n]+" +
+ "font-variant-caps: small-caps;[\\r\\n]*";
+
+ try {
+ yield waitForClipboardPromise(() => fireCopyEvent(props[0]),
+ () => checkClipboardData(expectedPattern));
+ } catch (e) {
+ failedClipboard(expectedPattern);
+ }
+}
+
+function* checkSelectAll(view) {
+ info("Testing select-all copy");
+
+ let contentDoc = view.styleDocument;
+ let prop = contentDoc.querySelector(".property-view");
+
+ info("Checking that _onSelectAll() then copy returns the correct " +
+ "clipboard value");
+ view._contextmenu._onSelectAll();
+ let expectedPattern = "color: rgb\\(255, 255, 0\\);[\\r\\n]+" +
+ "font-family: helvetica,sans-serif;[\\r\\n]+" +
+ "font-size: 16px;[\\r\\n]+" +
+ "font-variant-caps: small-caps;[\\r\\n]*";
+
+ try {
+ yield waitForClipboardPromise(() => fireCopyEvent(prop),
+ () => checkClipboardData(expectedPattern));
+ } catch (e) {
+ failedClipboard(expectedPattern);
+ }
+}
+
+function checkClipboardData(expectedPattern) {
+ let actual = SpecialPowers.getClipboardData("text/unicode");
+ let expectedRegExp = new RegExp(expectedPattern, "g");
+ return expectedRegExp.test(actual);
+}
+
+function failedClipboard(expectedPattern) {
+ // Format expected text for comparison
+ let terminator = osString == "WINNT" ? "\r\n" : "\n";
+ expectedPattern = expectedPattern.replace(/\[\\r\\n\][+*]/g, terminator);
+ expectedPattern = expectedPattern.replace(/\\\(/g, "(");
+ expectedPattern = expectedPattern.replace(/\\\)/g, ")");
+
+ let actual = SpecialPowers.getClipboardData("text/unicode");
+
+ // Trim the right hand side of our strings. This is because expectedPattern
+ // accounts for windows sometimes adding a newline to our copied data.
+ expectedPattern = expectedPattern.trimRight();
+ actual = actual.trimRight();
+
+ dump("TEST-UNEXPECTED-FAIL | Clipboard text does not match expected ... " +
+ "results (escaped for accurate comparison):\n");
+ info("Actual: " + escape(actual));
+ info("Expected: " + escape(expectedPattern));
+}
diff --git a/devtools/client/inspector/computed/test/browser_computed_style-editor-link.js b/devtools/client/inspector/computed/test/browser_computed_style-editor-link.js
new file mode 100644
index 000000000..6a95fd83f
--- /dev/null
+++ b/devtools/client/inspector/computed/test/browser_computed_style-editor-link.js
@@ -0,0 +1,142 @@
+/* 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";
+
+// Whitelisting this test.
+// As part of bug 1077403, the leaking uncaught rejection should be fixed.
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Unknown sheet source");
+
+// Tests the links from the computed view to the style editor.
+
+const STYLESHEET_URL = "data:text/css," + encodeURIComponent(
+ ".highlight {color: blue}");
+
+const DOCUMENT_URL = "data:text/html;charset=utf-8," + encodeURIComponent(
+ `<html>
+ <head>
+ <title>Computed view style editor link test</title>
+ <style type="text/css">
+ html { color: #000000; }
+ span { font-variant: small-caps; color: #000000; }
+ .nomatches {color: #ff0000;}</style> <div id="first" style="margin: 10em;
+ font-size: 14pt; font-family: helvetica, sans-serif; color: #AAA">
+ </style>
+ <style>
+ div { color: #f06; }
+ </style>
+ <link rel="stylesheet" type="text/css" href="${STYLESHEET_URL}">
+ </head>
+ <body>
+ <h1>Some header text</h1>
+ <p id="salutation" style="font-size: 12pt">hi.</p>
+ <p id="body" style="font-size: 12pt">I am a test-case. This text exists
+ solely to provide some things to
+ <span style="color: yellow" class="highlight">
+ highlight</span> and <span style="font-weight: bold">count</span>
+ style list-items in the box at right. If you are reading this,
+ you should go do something else instead. Maybe read a book. Or better
+ yet, write some test-cases for another bit of code.
+ <span style="font-style: italic">some text</span></p>
+ <p id="closing">more text</p>
+ <p>even more text</p>
+ </div>
+ </body>
+ </html>`);
+
+add_task(function* () {
+ yield addTab(DOCUMENT_URL);
+ let {toolbox, inspector, view, testActor} = yield openComputedView();
+ yield selectNode("span", inspector);
+
+ yield testInlineStyle(view);
+ yield testFirstInlineStyleSheet(view, toolbox, testActor);
+ yield testSecondInlineStyleSheet(view, toolbox, testActor);
+ yield testExternalStyleSheet(view, toolbox, testActor);
+});
+
+function* testInlineStyle(view) {
+ info("Testing inline style");
+
+ yield expandComputedViewPropertyByIndex(view, 0);
+
+ let onTab = waitForTab();
+ info("Clicking on the first rule-link in the computed-view");
+ clickLinkByIndex(view, 0);
+
+ let tab = yield onTab;
+
+ let tabURI = tab.linkedBrowser.documentURI.spec;
+ ok(tabURI.startsWith("view-source:"), "View source tab is open");
+ info("Closing tab");
+ gBrowser.removeTab(tab);
+}
+
+function* testFirstInlineStyleSheet(view, toolbox, testActor) {
+ info("Testing inline stylesheet");
+
+ info("Listening for toolbox switch to the styleeditor");
+ let onSwitch = waitForStyleEditor(toolbox);
+
+ info("Clicking an inline stylesheet");
+ clickLinkByIndex(view, 2);
+ let editor = yield onSwitch;
+
+ ok(true, "Switched to the style-editor panel in the toolbox");
+
+ yield validateStyleEditorSheet(editor, 0, testActor);
+}
+
+function* testSecondInlineStyleSheet(view, toolbox, testActor) {
+ info("Testing second inline stylesheet");
+
+ info("Waiting for the stylesheet editor to be selected");
+ let panel = toolbox.getCurrentPanel();
+ let onSelected = panel.UI.once("editor-selected");
+
+ info("Switching back to the inspector panel in the toolbox");
+ yield toolbox.selectTool("inspector");
+
+ info("Clicking on second inline stylesheet link");
+ clickLinkByIndex(view, 4);
+ let editor = yield onSelected;
+
+ is(toolbox.currentToolId, "styleeditor",
+ "The style editor is selected again");
+ yield validateStyleEditorSheet(editor, 1, testActor);
+}
+
+function* testExternalStyleSheet(view, toolbox, testActor) {
+ info("Testing external stylesheet");
+
+ info("Waiting for the stylesheet editor to be selected");
+ let panel = toolbox.getCurrentPanel();
+ let onSelected = panel.UI.once("editor-selected");
+
+ info("Switching back to the inspector panel in the toolbox");
+ yield toolbox.selectTool("inspector");
+
+ info("Clicking on an external stylesheet link");
+ clickLinkByIndex(view, 1);
+ let editor = yield onSelected;
+
+ is(toolbox.currentToolId, "styleeditor",
+ "The style editor is selected again");
+ yield validateStyleEditorSheet(editor, 2, testActor);
+}
+
+function* validateStyleEditorSheet(editor, expectedSheetIndex, testActor) {
+ info("Validating style editor stylesheet");
+ let expectedHref = yield testActor.eval(`
+ document.styleSheets[${expectedSheetIndex}].href;
+ `);
+ is(editor.styleSheet.href, expectedHref,
+ "loaded stylesheet matches document stylesheet");
+}
+
+function clickLinkByIndex(view, index) {
+ let link = getComputedViewLinkByIndex(view, index);
+ link.scrollIntoView();
+ link.click();
+}
diff --git a/devtools/client/inspector/computed/test/doc_matched_selectors.html b/devtools/client/inspector/computed/test/doc_matched_selectors.html
new file mode 100644
index 000000000..8fe007409
--- /dev/null
+++ b/devtools/client/inspector/computed/test/doc_matched_selectors.html
@@ -0,0 +1,28 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<html>
+ <head>
+ <style>
+ .matched1, .matched2, .matched3, .matched4, .matched5 {
+ color: #000;
+ }
+
+ div {
+ position: absolute;
+ top: 40px;
+ left: 20px;
+ border: 1px solid #000;
+ color: #111;
+ width: 100px;
+ height: 50px;
+ }
+ </style>
+ </head>
+ <body>
+ inspectstyle($("test"));
+ <div id="test" class="matched1 matched2 matched3 matched4 matched5">Test div</div>
+ <div id="dummy">
+ <div></div>
+ </div>
+ </body>
+</html>
diff --git a/devtools/client/inspector/computed/test/doc_media_queries.html b/devtools/client/inspector/computed/test/doc_media_queries.html
new file mode 100644
index 000000000..819e1ea7a
--- /dev/null
+++ b/devtools/client/inspector/computed/test/doc_media_queries.html
@@ -0,0 +1,21 @@
+<html>
+<head>
+ <title>test</title>
+ <style>
+ div {
+ width: 1000px;
+ height: 100px;
+ background-color: #f00;
+ }
+
+ @media screen and (min-width: 1px) {
+ div {
+ width: 200px;
+ }
+ }
+ </style>
+</head>
+<body>
+<div></div>
+</body>
+</html>
diff --git a/devtools/client/inspector/computed/test/doc_pseudoelement.html b/devtools/client/inspector/computed/test/doc_pseudoelement.html
new file mode 100644
index 000000000..6145d4bf1
--- /dev/null
+++ b/devtools/client/inspector/computed/test/doc_pseudoelement.html
@@ -0,0 +1,131 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<html>
+ <head>
+ <style>
+
+body {
+ color: #333;
+}
+
+.box {
+ float:left;
+ width: 128px;
+ height: 128px;
+ background: #ddd;
+ padding: 32px;
+ margin: 32px;
+ position:relative;
+}
+
+.box:first-line {
+ color: orange;
+ background: red;
+}
+
+.box:first-letter {
+ color: green;
+}
+
+* {
+ cursor: default;
+}
+
+nothing {
+ cursor: pointer;
+}
+
+p::-moz-selection {
+ color: white;
+ background: black;
+}
+p::selection {
+ color: white;
+ background: black;
+}
+
+p:first-line {
+ background: blue;
+}
+p:first-letter {
+ color: red;
+ font-size: 130%;
+}
+
+.box:before {
+ background: green;
+ content: " ";
+ position: absolute;
+ height:32px;
+ width:32px;
+}
+
+.box:after {
+ background: red;
+ content: " ";
+ position: absolute;
+ border-radius: 50%;
+ height:32px;
+ width:32px;
+ top: 50%;
+ left: 50%;
+ margin-top: -16px;
+ margin-left: -16px;
+}
+
+.topleft:before {
+ top:0;
+ left:0;
+}
+
+.topleft:first-line {
+ color: orange;
+}
+.topleft::selection {
+ color: orange;
+}
+
+.topright:before {
+ top:0;
+ right:0;
+}
+
+.bottomright:before {
+ bottom:10px;
+ right:10px;
+ color: red;
+}
+
+.bottomright:before {
+ bottom:0;
+ right:0;
+}
+
+.bottomleft:before {
+ bottom:0;
+ left:0;
+}
+
+ </style>
+ </head>
+ <body>
+ <h1>ruleview pseudoelement($("test"));</h1>
+
+ <div id="topleft" class="box topleft">
+ <p>Top Left<br />Position</p>
+ </div>
+
+ <div id="topright" class="box topright">
+ <p>Top Right<br />Position</p>
+ </div>
+
+ <div id="bottomright" class="box bottomright">
+ <p>Bottom Right<br />Position</p>
+ </div>
+
+ <div id="bottomleft" class="box bottomleft">
+ <p>Bottom Left<br />Position</p>
+ </div>
+
+ </body>
+</html>
diff --git a/devtools/client/inspector/computed/test/doc_sourcemaps.css b/devtools/client/inspector/computed/test/doc_sourcemaps.css
new file mode 100644
index 000000000..a9b437a40
--- /dev/null
+++ b/devtools/client/inspector/computed/test/doc_sourcemaps.css
@@ -0,0 +1,7 @@
+div {
+ color: #ff0066; }
+
+span {
+ background-color: #EEE; }
+
+/*# sourceMappingURL=doc_sourcemaps.css.map */ \ No newline at end of file
diff --git a/devtools/client/inspector/computed/test/doc_sourcemaps.css.map b/devtools/client/inspector/computed/test/doc_sourcemaps.css.map
new file mode 100644
index 000000000..0f7486fd9
--- /dev/null
+++ b/devtools/client/inspector/computed/test/doc_sourcemaps.css.map
@@ -0,0 +1,7 @@
+{
+"version": 3,
+"mappings": "AAGA,GAAI;EACF,KAAK,EAHU,OAAI;;AAMrB,IAAK;EACH,gBAAgB,EAAE,IAAI",
+"sources": ["doc_sourcemaps.scss"],
+"names": [],
+"file": "doc_sourcemaps.css"
+}
diff --git a/devtools/client/inspector/computed/test/doc_sourcemaps.html b/devtools/client/inspector/computed/test/doc_sourcemaps.html
new file mode 100644
index 000000000..0014e55fe
--- /dev/null
+++ b/devtools/client/inspector/computed/test/doc_sourcemaps.html
@@ -0,0 +1,11 @@
+<!doctype html>
+<html>
+<head>
+ <title>testcase for testing CSS source maps</title>
+ <link rel="stylesheet" type="text/css" href="simple.css"/>
+ <link rel="stylesheet" type="text/css" href="doc_sourcemaps.css"/>
+</head>
+<body>
+ <div>source maps <span>testcase</span></div>
+</body>
+</html>
diff --git a/devtools/client/inspector/computed/test/doc_sourcemaps.scss b/devtools/client/inspector/computed/test/doc_sourcemaps.scss
new file mode 100644
index 000000000..0ff6c471b
--- /dev/null
+++ b/devtools/client/inspector/computed/test/doc_sourcemaps.scss
@@ -0,0 +1,10 @@
+
+$paulrougetpink: #f06;
+
+div {
+ color: $paulrougetpink;
+}
+
+span {
+ background-color: #EEE;
+} \ No newline at end of file
diff --git a/devtools/client/inspector/computed/test/head.js b/devtools/client/inspector/computed/test/head.js
new file mode 100644
index 000000000..17c47be1a
--- /dev/null
+++ b/devtools/client/inspector/computed/test/head.js
@@ -0,0 +1,157 @@
+/* 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/ */
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+/* import-globals-from ../../test/head.js */
+"use strict";
+
+// Import the inspector's head.js first (which itself imports shared-head.js).
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/inspector/test/head.js",
+ this);
+
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("devtools.defaultColorUnit");
+});
+
+/**
+ * Dispatch the copy event on the given element
+ */
+function fireCopyEvent(element) {
+ let evt = element.ownerDocument.createEvent("Event");
+ evt.initEvent("copy", true, true);
+ element.dispatchEvent(evt);
+}
+
+/**
+ * Get references to the name and value span nodes corresponding to a given
+ * property name in the computed-view
+ *
+ * @param {CssComputedView} view
+ * The instance of the computed view panel
+ * @param {String} name
+ * The name of the property to retrieve
+ * @return an object {nameSpan, valueSpan}
+ */
+function getComputedViewProperty(view, name) {
+ let prop;
+ for (let property of view.styleDocument.querySelectorAll(".property-view")) {
+ let nameSpan = property.querySelector(".property-name");
+ let valueSpan = property.querySelector(".property-value");
+
+ if (nameSpan.textContent === name) {
+ prop = {nameSpan: nameSpan, valueSpan: valueSpan};
+ break;
+ }
+ }
+ return prop;
+}
+
+/**
+ * Get an instance of PropertyView from the computed-view.
+ *
+ * @param {CssComputedView} view
+ * The instance of the computed view panel
+ * @param {String} name
+ * The name of the property to retrieve
+ * @return {PropertyView}
+ */
+function getComputedViewPropertyView(view, name) {
+ let propView;
+ for (let propertyView of view.propertyViews) {
+ if (propertyView._propertyInfo.name === name) {
+ propView = propertyView;
+ break;
+ }
+ }
+ return propView;
+}
+
+/**
+ * Get a reference to the property-content element for a given property name in
+ * the computed-view.
+ * A property-content element always follows (nextSibling) the property itself
+ * and is only shown when the twisty icon is expanded on the property.
+ * A property-content element contains matched rules, with selectors,
+ * properties, values and stylesheet links
+ *
+ * @param {CssComputedView} view
+ * The instance of the computed view panel
+ * @param {String} name
+ * The name of the property to retrieve
+ * @return {Promise} A promise that resolves to the property matched rules
+ * container
+ */
+var getComputedViewMatchedRules = Task.async(function* (view, name) {
+ let expander;
+ let propertyContent;
+ for (let property of view.styleDocument.querySelectorAll(".property-view")) {
+ let nameSpan = property.querySelector(".property-name");
+ if (nameSpan.textContent === name) {
+ expander = property.querySelector(".expandable");
+ propertyContent = property.nextSibling;
+ break;
+ }
+ }
+
+ if (!expander.hasAttribute("open")) {
+ // Need to expand the property
+ let onExpand = view.inspector.once("computed-view-property-expanded");
+ expander.click();
+ yield onExpand;
+ }
+
+ return propertyContent;
+});
+
+/**
+ * Get the text value of the property corresponding to a given name in the
+ * computed-view
+ *
+ * @param {CssComputedView} view
+ * The instance of the computed view panel
+ * @param {String} name
+ * The name of the property to retrieve
+ * @return {String} The property value
+ */
+function getComputedViewPropertyValue(view, name, propertyName) {
+ return getComputedViewProperty(view, name, propertyName)
+ .valueSpan.textContent;
+}
+
+/**
+ * Expand a given property, given its index in the current property list of
+ * the computed view
+ *
+ * @param {CssComputedView} view
+ * The instance of the computed view panel
+ * @param {Number} index
+ * The index of the property to be expanded
+ * @return a promise that resolves when the property has been expanded, or
+ * rejects if the property was not found
+ */
+function expandComputedViewPropertyByIndex(view, index) {
+ info("Expanding property " + index + " in the computed view");
+ let expandos = view.styleDocument.querySelectorAll("#propertyContainer .expandable");
+ if (!expandos.length || !expandos[index]) {
+ return promise.reject();
+ }
+
+ let onExpand = view.inspector.once("computed-view-property-expanded");
+ expandos[index].click();
+ return onExpand;
+}
+
+/**
+ * Get a rule-link from the computed-view given its index
+ *
+ * @param {CssComputedView} view
+ * The instance of the computed view panel
+ * @param {Number} index
+ * The index of the link to be retrieved
+ * @return {DOMNode} The link at the given index, if one exists, null otherwise
+ */
+function getComputedViewLinkByIndex(view, index) {
+ let links = view.styleDocument.querySelectorAll(".rule-link .link");
+ return links[index];
+}
diff --git a/devtools/client/inspector/fonts/fonts.js b/devtools/client/inspector/fonts/fonts.js
new file mode 100644
index 000000000..b0087e9f6
--- /dev/null
+++ b/devtools/client/inspector/fonts/fonts.js
@@ -0,0 +1,250 @@
+/* -*- 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 {gDevTools} = require("devtools/client/framework/devtools");
+
+const DEFAULT_PREVIEW_TEXT = "Abc";
+const PREVIEW_UPDATE_DELAY = 150;
+
+const {Task} = require("devtools/shared/task");
+const {getColor} = require("devtools/client/shared/theme");
+
+function FontInspector(inspector, window) {
+ this.inspector = inspector;
+ this.pageStyle = this.inspector.pageStyle;
+ this.chromeDoc = window.document;
+ this.init();
+}
+
+FontInspector.prototype = {
+ init: function () {
+ this.update = this.update.bind(this);
+ this.onNewNode = this.onNewNode.bind(this);
+ this.onThemeChanged = this.onThemeChanged.bind(this);
+ this.inspector.selection.on("new-node-front", this.onNewNode);
+ this.inspector.sidebar.on("fontinspector-selected", this.onNewNode);
+ this.showAll = this.showAll.bind(this);
+ this.showAllLink = this.chromeDoc.getElementById("font-showall");
+ this.showAllLink.addEventListener("click", this.showAll);
+ this.previewTextChanged = this.previewTextChanged.bind(this);
+ this.previewInput = this.chromeDoc.getElementById("font-preview-text-input");
+ this.previewInput.addEventListener("input", this.previewTextChanged);
+ this.previewInput.addEventListener("contextmenu",
+ this.inspector.onTextBoxContextMenu);
+
+ // Listen for theme changes as the color of the previews depend on the theme
+ gDevTools.on("theme-switched", this.onThemeChanged);
+
+ this.update();
+ },
+
+ /**
+ * Is the fontinspector visible in the sidebar?
+ */
+ isActive: function () {
+ return this.inspector.sidebar &&
+ this.inspector.sidebar.getCurrentTabID() == "fontinspector";
+ },
+
+ /**
+ * Remove listeners.
+ */
+ destroy: function () {
+ this.chromeDoc = null;
+ this.inspector.sidebar.off("fontinspector-selected", this.onNewNode);
+ this.inspector.selection.off("new-node-front", this.onNewNode);
+ this.showAllLink.removeEventListener("click", this.showAll);
+ this.previewInput.removeEventListener("input", this.previewTextChanged);
+ this.previewInput.removeEventListener("contextmenu",
+ this.inspector.onTextBoxContextMenu);
+
+ gDevTools.off("theme-switched", this.onThemeChanged);
+
+ if (this._previewUpdateTimeout) {
+ clearTimeout(this._previewUpdateTimeout);
+ }
+ },
+
+ /**
+ * Selection 'new-node' event handler.
+ */
+ onNewNode: function () {
+ if (this.isActive() &&
+ this.inspector.selection.isConnected() &&
+ this.inspector.selection.isElementNode()) {
+ this.undim();
+ this.update();
+ } else {
+ this.dim();
+ }
+ },
+
+ /**
+ * The text to use for previews. Returns either the value user has typed to
+ * the preview input or DEFAULT_PREVIEW_TEXT if the input is empty or contains
+ * only whitespace.
+ */
+ getPreviewText: function () {
+ let inputText = this.previewInput.value.trim();
+ if (inputText === "") {
+ return DEFAULT_PREVIEW_TEXT;
+ }
+
+ return inputText;
+ },
+
+ /**
+ * Preview input 'input' event handler.
+ */
+ previewTextChanged: function () {
+ if (this._previewUpdateTimeout) {
+ clearTimeout(this._previewUpdateTimeout);
+ }
+
+ this._previewUpdateTimeout = setTimeout(() => {
+ this.update(this._lastUpdateShowedAllFonts);
+ }, PREVIEW_UPDATE_DELAY);
+ },
+
+ /**
+ * Callback for the theme-switched event.
+ */
+ onThemeChanged: function (event, frame) {
+ if (frame === this.chromeDoc.defaultView) {
+ this.update(this._lastUpdateShowedAllFonts);
+ }
+ },
+
+ /**
+ * Hide the font list. No node are selected.
+ */
+ dim: function () {
+ let panel = this.chromeDoc.getElementById("sidebar-panel-fontinspector");
+ panel.classList.add("dim");
+ this.clear();
+ },
+
+ /**
+ * Show the font list. A node is selected.
+ */
+ undim: function () {
+ let panel = this.chromeDoc.getElementById("sidebar-panel-fontinspector");
+ panel.classList.remove("dim");
+ },
+
+ /**
+ * Clears the font list.
+ */
+ clear: function () {
+ this.chromeDoc.querySelector("#all-fonts").innerHTML = "";
+ },
+
+ /**
+ * Retrieve all the font info for the selected node and display it.
+ */
+ update: Task.async(function* (showAllFonts) {
+ let node = this.inspector.selection.nodeFront;
+ let panel = this.chromeDoc.getElementById("sidebar-panel-fontinspector");
+
+ if (!node ||
+ !this.isActive() ||
+ !this.inspector.selection.isConnected() ||
+ !this.inspector.selection.isElementNode() ||
+ panel.classList.contains("dim")) {
+ return;
+ }
+
+ this._lastUpdateShowedAllFonts = showAllFonts;
+
+ let options = {
+ includePreviews: true,
+ previewText: this.getPreviewText(),
+ previewFillStyle: getColor("body-color")
+ };
+
+ let fonts = [];
+ if (showAllFonts) {
+ fonts = yield this.pageStyle.getAllUsedFontFaces(options)
+ .then(null, console.error);
+ } else {
+ fonts = yield this.pageStyle.getUsedFontFaces(node, options)
+ .then(null, console.error);
+ }
+
+ if (!fonts || !fonts.length) {
+ // No fonts to display. Clear the previously shown fonts.
+ this.clear();
+ return;
+ }
+
+ for (let font of fonts) {
+ font.previewUrl = yield font.preview.data.string();
+ }
+
+ // in case we've been destroyed in the meantime
+ if (!this.chromeDoc) {
+ return;
+ }
+
+ // Make room for the new fonts.
+ this.clear();
+
+ for (let font of fonts) {
+ this.render(font);
+ }
+
+ this.inspector.emit("fontinspector-updated");
+ }),
+
+ /**
+ * Display the information of one font.
+ */
+ render: function (font) {
+ let s = this.chromeDoc.querySelector("#font-template > section");
+ s = s.cloneNode(true);
+
+ s.querySelector(".font-name").textContent = font.name;
+ s.querySelector(".font-css-name").textContent = font.CSSFamilyName;
+
+ if (font.URI) {
+ s.classList.add("is-remote");
+ } else {
+ s.classList.add("is-local");
+ }
+
+ let formatElem = s.querySelector(".font-format");
+ if (font.format) {
+ formatElem.textContent = font.format;
+ } else {
+ formatElem.hidden = true;
+ }
+
+ s.querySelector(".font-url").value = font.URI;
+
+ if (font.rule) {
+ // This is the @font-face{…} code.
+ let cssText = font.ruleText;
+
+ s.classList.add("has-code");
+ s.querySelector(".font-css-code").textContent = cssText;
+ }
+ let preview = s.querySelector(".font-preview");
+ preview.src = font.previewUrl;
+
+ this.chromeDoc.querySelector("#all-fonts").appendChild(s);
+ },
+
+ /**
+ * Show all fonts for the document (including iframes)
+ */
+ showAll: function () {
+ this.update(true);
+ },
+};
+
+exports.FontInspector = FontInspector;
diff --git a/devtools/client/inspector/fonts/moz.build b/devtools/client/inspector/fonts/moz.build
new file mode 100644
index 000000000..a66982b71
--- /dev/null
+++ b/devtools/client/inspector/fonts/moz.build
@@ -0,0 +1,11 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'fonts.js',
+)
+
+BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
diff --git a/devtools/client/inspector/fonts/test/.eslintrc.js b/devtools/client/inspector/fonts/test/.eslintrc.js
new file mode 100644
index 000000000..698ae9181
--- /dev/null
+++ b/devtools/client/inspector/fonts/test/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../../../.eslintrc.mochitests.js"
+};
diff --git a/devtools/client/inspector/fonts/test/OstrichLicense.txt b/devtools/client/inspector/fonts/test/OstrichLicense.txt
new file mode 100644
index 000000000..14c043d60
--- /dev/null
+++ b/devtools/client/inspector/fonts/test/OstrichLicense.txt
@@ -0,0 +1,41 @@
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting, or substituting -- in part or in whole -- any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment.
+
+"Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission.
+
+5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. \ No newline at end of file
diff --git a/devtools/client/inspector/fonts/test/browser.ini b/devtools/client/inspector/fonts/test/browser.ini
new file mode 100644
index 000000000..99b00231d
--- /dev/null
+++ b/devtools/client/inspector/fonts/test/browser.ini
@@ -0,0 +1,20 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ browser_fontinspector.html
+ test_iframe.html
+ ostrich-black.ttf
+ ostrich-regular.ttf
+ head.js
+ !/devtools/client/commandline/test/helpers.js
+ !/devtools/client/framework/test/shared-head.js
+ !/devtools/client/inspector/test/head.js
+ !/devtools/client/inspector/test/shared-head.js
+ !/devtools/client/shared/test/test-actor.js
+ !/devtools/client/shared/test/test-actor-registry.js
+
+[browser_fontinspector.js]
+[browser_fontinspector_edit-previews.js]
+[browser_fontinspector_edit-previews-show-all.js]
+[browser_fontinspector_theme-change.js]
diff --git a/devtools/client/inspector/fonts/test/browser_fontinspector.html b/devtools/client/inspector/fonts/test/browser_fontinspector.html
new file mode 100644
index 000000000..009b2f087
--- /dev/null
+++ b/devtools/client/inspector/fonts/test/browser_fontinspector.html
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+
+<style>
+ @font-face {
+ font-family: bar;
+ src: url(bad/font/name.ttf), url(ostrich-regular.ttf) format("truetype");
+ }
+ @font-face {
+ font-family: barnormal;
+ font-weight: normal;
+ src: url(ostrich-regular.ttf);
+ }
+ @font-face {
+ font-family: bar;
+ font-weight: bold;
+ src: url(ostrich-black.ttf);
+ }
+ @font-face {
+ font-family: bar;
+ font-weight: 800;
+ src: url(ostrich-black.ttf);
+ }
+ body{
+ font-family:Arial;
+ font-size: 36px;
+ }
+ div {
+ font-family:Arial;
+ font-family:bar;
+ }
+ .normal-text {
+ font-family: barnormal;
+ font-weight: normal;
+ }
+ .bold-text {
+ font-family: bar;
+ font-weight: bold;
+ }
+ .black-text {
+ font-family: bar;
+ font-weight: 800;
+ }
+</style>
+
+<body>
+ BODY
+ <div>DIV</div>
+ <iframe src="test_iframe.html"></iframe>
+ <div class="normal-text">NORMAL DIV</div>
+ <div class="bold-text">BOLD DIV</div>
+ <div class="black-text">800 DIV</div>
+</body>
diff --git a/devtools/client/inspector/fonts/test/browser_fontinspector.js b/devtools/client/inspector/fonts/test/browser_fontinspector.js
new file mode 100644
index 000000000..a36c57771
--- /dev/null
+++ b/devtools/client/inspector/fonts/test/browser_fontinspector.js
@@ -0,0 +1,108 @@
+/* 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";
+
+requestLongerTimeout(2);
+
+const TEST_URI = URL_ROOT + "browser_fontinspector.html";
+const FONTS = [{
+ name: "Ostrich Sans Medium",
+ remote: true,
+ url: URL_ROOT + "ostrich-regular.ttf",
+ format: "truetype",
+ cssName: "bar"
+}, {
+ name: "Ostrich Sans Black",
+ remote: true,
+ url: URL_ROOT + "ostrich-black.ttf",
+ format: "",
+ cssName: "bar"
+}, {
+ name: "Ostrich Sans Black",
+ remote: true,
+ url: URL_ROOT + "ostrich-black.ttf",
+ format: "",
+ cssName: "bar"
+}, {
+ name: "Ostrich Sans Medium",
+ remote: true,
+ url: URL_ROOT + "ostrich-regular.ttf",
+ format: "",
+ cssName: "barnormal"
+}];
+
+add_task(function* () {
+ let { inspector, view } = yield openFontInspectorForURL(TEST_URI);
+ ok(!!view, "Font inspector document is alive.");
+
+ let viewDoc = view.chromeDoc;
+
+ yield testBodyFonts(inspector, viewDoc);
+ yield testDivFonts(inspector, viewDoc);
+ yield testShowAllFonts(inspector, viewDoc);
+});
+
+function* testBodyFonts(inspector, viewDoc) {
+ let s = viewDoc.querySelectorAll("#all-fonts > section");
+ is(s.length, 5, "Found 5 fonts");
+
+ for (let i = 0; i < FONTS.length; i++) {
+ let section = s[i];
+ let font = FONTS[i];
+ is(section.querySelector(".font-name").textContent, font.name,
+ "font " + i + " right font name");
+ is(section.classList.contains("is-remote"), font.remote,
+ "font " + i + " remote value correct");
+ is(section.querySelector(".font-url").value, font.url,
+ "font " + i + " url correct");
+ is(section.querySelector(".font-format").hidden, !font.format,
+ "font " + i + " format hidden value correct");
+ is(section.querySelector(".font-format").textContent,
+ font.format, "font " + i + " format correct");
+ is(section.querySelector(".font-css-name").textContent,
+ font.cssName, "font " + i + " css name correct");
+ }
+
+ // test that the bold and regular fonts have different previews
+ let regSrc = s[0].querySelector(".font-preview").src;
+ let boldSrc = s[1].querySelector(".font-preview").src;
+ isnot(regSrc, boldSrc, "preview for bold font is different from regular");
+
+ // test system font
+ let localFontName = s[4].querySelector(".font-name").textContent;
+ let localFontCSSName = s[4].querySelector(".font-css-name").textContent;
+
+ // On Linux test machines, the Arial font doesn't exist.
+ // The fallback is "Liberation Sans"
+ ok((localFontName == "Arial") || (localFontName == "Liberation Sans"),
+ "local font right font name");
+ ok(s[4].classList.contains("is-local"), "local font is local");
+ ok((localFontCSSName == "Arial") || (localFontCSSName == "Liberation Sans"),
+ "Arial", "local font has right css name");
+}
+
+function* testDivFonts(inspector, viewDoc) {
+ let updated = inspector.once("fontinspector-updated");
+ yield selectNode("div", inspector);
+ yield updated;
+
+ let sections1 = viewDoc.querySelectorAll("#all-fonts > section");
+ is(sections1.length, 1, "Found 1 font on DIV");
+ is(sections1[0].querySelector(".font-name").textContent,
+ "Ostrich Sans Medium",
+ "The DIV font has the right name");
+}
+
+function* testShowAllFonts(inspector, viewDoc) {
+ info("testing showing all fonts");
+
+ let updated = inspector.once("fontinspector-updated");
+ viewDoc.querySelector("#font-showall").click();
+ yield updated;
+
+ // shouldn't change the node selection
+ is(inspector.selection.nodeFront.nodeName, "DIV", "Show all fonts selected");
+ let sections = viewDoc.querySelectorAll("#all-fonts > section");
+ is(sections.length, 6, "Font inspector shows 6 fonts (1 from iframe)");
+}
diff --git a/devtools/client/inspector/fonts/test/browser_fontinspector_edit-previews-show-all.js b/devtools/client/inspector/fonts/test/browser_fontinspector_edit-previews-show-all.js
new file mode 100644
index 000000000..f1319b400
--- /dev/null
+++ b/devtools/client/inspector/fonts/test/browser_fontinspector_edit-previews-show-all.js
@@ -0,0 +1,44 @@
+/* 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";
+
+// Test that correct previews are shown if the text is edited after 'Show all'
+// button is pressed.
+
+const TEST_URI = URL_ROOT + "browser_fontinspector.html";
+
+add_task(function* () {
+ let { inspector, view } = yield openFontInspectorForURL(TEST_URI);
+ let viewDoc = view.chromeDoc;
+
+ info("Selecting a node that doesn't contain all document fonts.");
+ yield selectNode(".normal-text", inspector);
+
+ let normalTextNumPreviews =
+ viewDoc.querySelectorAll("#all-fonts .font-preview").length;
+
+ let onUpdated = inspector.once("fontinspector-updated");
+
+ info("Clicking 'Select all' button.");
+ viewDoc.getElementById("font-showall").click();
+
+ info("Waiting for font-inspector to update.");
+ yield onUpdated;
+
+ let allFontsNumPreviews =
+ viewDoc.querySelectorAll("#all-fonts .font-preview").length;
+
+ // Sanity check. If this fails all fonts apply also to the .normal-text node
+ // meaning we won't detect if preview editing causes the panel not to show all
+ // fonts.
+ isnot(allFontsNumPreviews, normalTextNumPreviews,
+ "The .normal-text didn't show all fonts.");
+
+ info("Editing the preview text.");
+ yield updatePreviewText(view, "The quick brown");
+
+ let numPreviews = viewDoc.querySelectorAll("#all-fonts .font-preview").length;
+ is(numPreviews, allFontsNumPreviews,
+ "All fonts are still shown after the preview text was edited.");
+});
diff --git a/devtools/client/inspector/fonts/test/browser_fontinspector_edit-previews.js b/devtools/client/inspector/fonts/test/browser_fontinspector_edit-previews.js
new file mode 100644
index 000000000..adc421b6b
--- /dev/null
+++ b/devtools/client/inspector/fonts/test/browser_fontinspector_edit-previews.js
@@ -0,0 +1,60 @@
+/* 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";
+
+// Test that previews change when the preview text changes. It doesn't check the
+// exact preview images because they are drawn on a canvas causing them to vary
+// between systems, platforms and software versions.
+
+const TEST_URI = URL_ROOT + "browser_fontinspector.html";
+
+add_task(function* () {
+ let {view} = yield openFontInspectorForURL(TEST_URI);
+ let viewDoc = view.chromeDoc;
+
+ let previews = viewDoc.querySelectorAll("#all-fonts .font-preview");
+ let initialPreviews = [...previews].map(p => p.src);
+
+ info("Typing 'Abc' to check that the reference previews are correct.");
+ yield updatePreviewText(view, "Abc");
+ checkPreviewImages(viewDoc, initialPreviews, true);
+
+ info("Typing something else to the preview box.");
+ yield updatePreviewText(view, "The quick brown");
+ checkPreviewImages(viewDoc, initialPreviews, false);
+
+ info("Blanking the input to restore default previews.");
+ yield updatePreviewText(view, "");
+ checkPreviewImages(viewDoc, initialPreviews, true);
+});
+
+/**
+ * Compares the previous preview image URIs to the current URIs.
+ *
+ * @param {Document} viewDoc
+ * The FontInspector document.
+ * @param {Array[String]} originalURIs
+ * An array of URIs to compare with the current URIs.
+ * @param {Boolean} assertIdentical
+ * If true, this method asserts that the previous and current URIs are
+ * identical. If false, this method asserts that the previous and current
+ * URI's are different.
+ */
+function checkPreviewImages(viewDoc, originalURIs, assertIdentical) {
+ let previews = viewDoc.querySelectorAll("#all-fonts .font-preview");
+ let newURIs = [...previews].map(p => p.src);
+
+ is(newURIs.length, originalURIs.length,
+ "The number of previews has not changed.");
+
+ for (let i = 0; i < newURIs.length; ++i) {
+ if (assertIdentical) {
+ is(newURIs[i], originalURIs[i],
+ `The preview image at index ${i} has stayed the same.`);
+ } else {
+ isnot(newURIs[i], originalURIs[i],
+ `The preview image at index ${i} has changed.`);
+ }
+ }
+}
diff --git a/devtools/client/inspector/fonts/test/browser_fontinspector_theme-change.js b/devtools/client/inspector/fonts/test/browser_fontinspector_theme-change.js
new file mode 100644
index 000000000..7fcfc9cc2
--- /dev/null
+++ b/devtools/client/inspector/fonts/test/browser_fontinspector_theme-change.js
@@ -0,0 +1,55 @@
+/* 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";
+
+requestLongerTimeout(2);
+
+// Test that the preview images are updated when the theme changes.
+
+const { getTheme, setTheme } = require("devtools/client/shared/theme");
+
+const TEST_URI = URL_ROOT + "browser_fontinspector.html";
+const originalTheme = getTheme();
+
+registerCleanupFunction(() => {
+ info(`Restoring theme to '${originalTheme}.`);
+ setTheme(originalTheme);
+});
+
+add_task(function* () {
+ let { inspector, view } = yield openFontInspectorForURL(TEST_URI);
+ let { chromeDoc: doc } = view;
+
+ yield selectNode(".normal-text", inspector);
+
+ // Store the original preview URI for later comparison.
+ let originalURI = doc.querySelector("#all-fonts .font-preview").src;
+ let newTheme = originalTheme === "light" ? "dark" : "light";
+
+ info(`Original theme was '${originalTheme}'.`);
+
+ yield setThemeAndWaitForUpdate(newTheme, inspector);
+ isnot(doc.querySelector("#all-fonts .font-preview").src, originalURI,
+ "The preview image changed with the theme.");
+
+ yield setThemeAndWaitForUpdate(originalTheme, inspector);
+ is(doc.querySelector("#all-fonts .font-preview").src, originalURI,
+ "The preview image is correct after the original theme was restored.");
+});
+
+/**
+ * Sets the current theme and waits for fontinspector-updated event.
+ *
+ * @param {String} theme - the new theme
+ * @param {Object} inspector - the inspector panel
+ */
+function* setThemeAndWaitForUpdate(theme, inspector) {
+ let onUpdated = inspector.once("fontinspector-updated");
+
+ info(`Setting theme to '${theme}'.`);
+ setTheme(theme);
+
+ info("Waiting for font-inspector to update.");
+ yield onUpdated;
+}
diff --git a/devtools/client/inspector/fonts/test/head.js b/devtools/client/inspector/fonts/test/head.js
new file mode 100644
index 000000000..f510ed798
--- /dev/null
+++ b/devtools/client/inspector/fonts/test/head.js
@@ -0,0 +1,86 @@
+ /* vim: set 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/. */
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+/* import-globals-from ../../test/head.js */
+"use strict";
+
+// Import the inspector's head.js first (which itself imports shared-head.js).
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/inspector/test/head.js",
+ this);
+
+Services.prefs.setBoolPref("devtools.fontinspector.enabled", true);
+Services.prefs.setCharPref("devtools.inspector.activeSidebar", "fontinspector");
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("devtools.fontinspector.enabled");
+});
+
+/**
+ * The font-inspector doesn't participate in the inspector's update mechanism
+ * (i.e. it doesn't call inspector.updating() when updating), so simply calling
+ * the default selectNode isn't enough to guaranty that the panel has finished
+ * updating. We also need to wait for the fontinspector-updated event.
+ */
+var _selectNode = selectNode;
+selectNode = function* (node, inspector, reason) {
+ let onUpdated = inspector.once("fontinspector-updated");
+ yield _selectNode(node, inspector, reason);
+ yield onUpdated;
+};
+
+/**
+ * Adds a new tab with the given URL, opens the inspector and selects the
+ * font-inspector tab.
+ * @return {Promise} resolves to a {toolbox, inspector, view} object
+ */
+var openFontInspectorForURL = Task.async(function* (url) {
+ yield addTab(url);
+ let {toolbox, inspector} = yield openInspector();
+
+ // Call selectNode again here to force a fontinspector update since we don't
+ // know if the fontinspector-updated event has been sent while the inspector
+ // was being opened or not.
+ yield selectNode("body", inspector);
+
+ return {
+ toolbox,
+ inspector,
+ view: inspector.fontInspector
+ };
+});
+
+/**
+ * Clears the preview input field, types new text into it and waits for the
+ * preview images to be updated.
+ *
+ * @param {FontInspector} view - The FontInspector instance.
+ * @param {String} text - The text to preview.
+ */
+function* updatePreviewText(view, text) {
+ info(`Changing the preview text to '${text}'`);
+
+ let doc = view.chromeDoc;
+ let input = doc.getElementById("font-preview-text-input");
+ let update = view.inspector.once("fontinspector-updated");
+
+ info("Focusing the input field.");
+ input.focus();
+
+ is(doc.activeElement, input, "The input was focused.");
+
+ info("Blanking the input field.");
+ for (let i = input.value.length; i >= 0; i--) {
+ EventUtils.sendKey("BACK_SPACE", doc.defaultView);
+ }
+
+ is(input.value, "", "The input is now blank.");
+
+ info("Typing the specified text to the input field.");
+ EventUtils.sendString(text, doc.defaultView);
+ is(input.value, text, "The input now contains the correct text.");
+
+ info("Waiting for the font-inspector to update.");
+ yield update;
+}
diff --git a/devtools/client/inspector/fonts/test/ostrich-black.ttf b/devtools/client/inspector/fonts/test/ostrich-black.ttf
new file mode 100755
index 000000000..a0ef8fe1c
--- /dev/null
+++ b/devtools/client/inspector/fonts/test/ostrich-black.ttf
Binary files differ
diff --git a/devtools/client/inspector/fonts/test/ostrich-regular.ttf b/devtools/client/inspector/fonts/test/ostrich-regular.ttf
new file mode 100755
index 000000000..9682c0735
--- /dev/null
+++ b/devtools/client/inspector/fonts/test/ostrich-regular.ttf
Binary files differ
diff --git a/devtools/client/inspector/fonts/test/test_iframe.html b/devtools/client/inspector/fonts/test/test_iframe.html
new file mode 100644
index 000000000..29393a9e9
--- /dev/null
+++ b/devtools/client/inspector/fonts/test/test_iframe.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+
+<style>
+ div{
+ font-family: "Times New Roman";
+ }
+</style>
+
+<body>
+ <div>Hello world</div>
+</body>
diff --git a/devtools/client/inspector/inspector-commands.js b/devtools/client/inspector/inspector-commands.js
new file mode 100644
index 000000000..ff26e4b94
--- /dev/null
+++ b/devtools/client/inspector/inspector-commands.js
@@ -0,0 +1,114 @@
+/* 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 l10n = require("gcli/l10n");
+const {gDevTools} = require("devtools/client/framework/devtools");
+/* eslint-disable mozilla/reject-some-requires */
+const {EyeDropper, HighlighterEnvironment} = require("devtools/server/actors/highlighters");
+/* eslint-enable mozilla/reject-some-requires */
+const Telemetry = require("devtools/client/shared/telemetry");
+
+const windowEyeDroppers = new WeakMap();
+
+exports.items = [{
+ item: "command",
+ runAt: "client",
+ name: "inspect",
+ description: l10n.lookup("inspectDesc"),
+ manual: l10n.lookup("inspectManual"),
+ params: [
+ {
+ name: "selector",
+ type: "string",
+ description: l10n.lookup("inspectNodeDesc"),
+ manual: l10n.lookup("inspectNodeManual")
+ }
+ ],
+ exec: function* (args, context) {
+ let target = context.environment.target;
+ let toolbox = yield gDevTools.showToolbox(target, "inspector");
+ let walker = toolbox.getCurrentPanel().walker;
+ let rootNode = yield walker.getRootNode();
+ let nodeFront = yield walker.querySelector(rootNode, args.selector);
+ toolbox.getCurrentPanel().selection.setNodeFront(nodeFront, "gcli");
+ },
+}, {
+ item: "command",
+ runAt: "client",
+ name: "eyedropper",
+ description: l10n.lookup("eyedropperDesc"),
+ manual: l10n.lookup("eyedropperManual"),
+ params: [{
+ // This hidden parameter is only set to true when the eyedropper browser menu item is
+ // used. It is useful to log a different telemetry event whether the tool was used
+ // from the menu, or from the gcli command line.
+ group: "hiddengroup",
+ params: [{
+ name: "frommenu",
+ type: "boolean",
+ hidden: true
+ }, {
+ name: "hide",
+ type: "boolean",
+ hidden: true
+ }]
+ }],
+ exec: function* (args, context) {
+ if (args.hide) {
+ context.updateExec("eyedropper_server_hide").catch(e => console.error(e));
+ return;
+ }
+
+ // If the inspector is already picking a color from the page, cancel it.
+ let target = context.environment.target;
+ let toolbox = gDevTools.getToolbox(target);
+ if (toolbox) {
+ let inspector = toolbox.getPanel("inspector");
+ if (inspector) {
+ yield inspector.hideEyeDropper();
+ }
+ }
+
+ let telemetry = new Telemetry();
+ telemetry.toolOpened(args.frommenu ? "menueyedropper" : "eyedropper");
+ context.updateExec("eyedropper_server").catch(e => console.error(e));
+ }
+}, {
+ item: "command",
+ runAt: "server",
+ name: "eyedropper_server",
+ hidden: true,
+ exec: function (args, {environment}) {
+ let eyeDropper = windowEyeDroppers.get(environment.window);
+
+ if (!eyeDropper) {
+ let env = new HighlighterEnvironment();
+ env.initFromWindow(environment.window);
+
+ eyeDropper = new EyeDropper(env);
+ eyeDropper.once("hidden", () => {
+ eyeDropper.destroy();
+ env.destroy();
+ windowEyeDroppers.delete(environment.window);
+ });
+
+ windowEyeDroppers.set(environment.window, eyeDropper);
+ }
+
+ eyeDropper.show(environment.document.documentElement, {copyOnSelect: true});
+ }
+}, {
+ item: "command",
+ runAt: "server",
+ name: "eyedropper_server_hide",
+ hidden: true,
+ exec: function (args, {environment}) {
+ let eyeDropper = windowEyeDroppers.get(environment.window);
+ if (eyeDropper) {
+ eyeDropper.hide();
+ }
+ }
+}];
diff --git a/devtools/client/inspector/inspector-search.js b/devtools/client/inspector/inspector-search.js
new file mode 100644
index 000000000..50e0383bc
--- /dev/null
+++ b/devtools/client/inspector/inspector-search.js
@@ -0,0 +1,549 @@
+/* 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 {Task} = require("devtools/shared/task");
+const {KeyCodes} = require("devtools/client/shared/keycodes");
+
+const EventEmitter = require("devtools/shared/event-emitter");
+const {AutocompletePopup} = require("devtools/client/shared/autocomplete-popup");
+const Services = require("Services");
+
+// Maximum number of selector suggestions shown in the panel.
+const MAX_SUGGESTIONS = 15;
+
+/**
+ * Converts any input field into a document search box.
+ *
+ * @param {InspectorPanel} inspector
+ * The InspectorPanel whose `walker` attribute should be used for
+ * document traversal.
+ * @param {DOMNode} input
+ * The input element to which the panel will be attached and from where
+ * search input will be taken.
+ * @param {DOMNode} clearBtn
+ * The clear button in the input field that will clear the input value.
+ *
+ * Emits the following events:
+ * - search-cleared: when the search box is emptied
+ * - search-result: when a search is made and a result is selected
+ */
+function InspectorSearch(inspector, input, clearBtn) {
+ this.inspector = inspector;
+ this.searchBox = input;
+ this.searchClearButton = clearBtn;
+ this._lastSearched = null;
+
+ this.searchClearButton.hidden = true;
+
+ this._onKeyDown = this._onKeyDown.bind(this);
+ this._onInput = this._onInput.bind(this);
+ this._onClearSearch = this._onClearSearch.bind(this);
+ this.searchBox.addEventListener("keydown", this._onKeyDown, true);
+ this.searchBox.addEventListener("input", this._onInput, true);
+ this.searchBox.addEventListener("contextmenu", this.inspector.onTextBoxContextMenu);
+ this.searchClearButton.addEventListener("click", this._onClearSearch);
+
+ // For testing, we need to be able to wait for the most recent node request
+ // to finish. Tests can watch this promise for that.
+ this._lastQuery = promise.resolve(null);
+
+ this.autocompleter = new SelectorAutocompleter(inspector, input);
+ EventEmitter.decorate(this);
+}
+
+exports.InspectorSearch = InspectorSearch;
+
+InspectorSearch.prototype = {
+ get walker() {
+ return this.inspector.walker;
+ },
+
+ destroy: function () {
+ this.searchBox.removeEventListener("keydown", this._onKeyDown, true);
+ this.searchBox.removeEventListener("input", this._onInput, true);
+ this.searchBox.removeEventListener("contextmenu",
+ this.inspector.onTextBoxContextMenu);
+ this.searchClearButton.removeEventListener("click", this._onClearSearch);
+ this.searchBox = null;
+ this.searchClearButton = null;
+ this.autocompleter.destroy();
+ },
+
+ _onSearch: function (reverse = false) {
+ this.doFullTextSearch(this.searchBox.value, reverse)
+ .catch(e => console.error(e));
+ },
+
+ doFullTextSearch: Task.async(function* (query, reverse) {
+ let lastSearched = this._lastSearched;
+ this._lastSearched = query;
+
+ if (query.length === 0) {
+ this.searchBox.classList.remove("devtools-style-searchbox-no-match");
+ if (!lastSearched || lastSearched.length > 0) {
+ this.emit("search-cleared");
+ }
+ return;
+ }
+
+ let res = yield this.walker.search(query, { reverse });
+
+ // Value has changed since we started this request, we're done.
+ if (query !== this.searchBox.value) {
+ return;
+ }
+
+ if (res) {
+ this.inspector.selection.setNodeFront(res.node, "inspectorsearch");
+ this.searchBox.classList.remove("devtools-style-searchbox-no-match");
+
+ res.query = query;
+ this.emit("search-result", res);
+ } else {
+ this.searchBox.classList.add("devtools-style-searchbox-no-match");
+ this.emit("search-result");
+ }
+ }),
+
+ _onInput: function () {
+ if (this.searchBox.value.length === 0) {
+ this.searchClearButton.hidden = true;
+ this._onSearch();
+ } else {
+ this.searchClearButton.hidden = false;
+ }
+ },
+
+ _onKeyDown: function (event) {
+ if (event.keyCode === KeyCodes.DOM_VK_RETURN) {
+ this._onSearch(event.shiftKey);
+ }
+
+ const modifierKey = Services.appinfo.OS === "Darwin"
+ ? event.metaKey : event.ctrlKey;
+ if (event.keyCode === KeyCodes.DOM_VK_G && modifierKey) {
+ this._onSearch(event.shiftKey);
+ event.preventDefault();
+ }
+ },
+
+ _onClearSearch: function () {
+ this.searchBox.classList.remove("devtools-style-searchbox-no-match");
+ this.searchBox.value = "";
+ this.searchClearButton.hidden = true;
+ this.emit("search-cleared");
+ }
+};
+
+/**
+ * Converts any input box on a page to a CSS selector search and suggestion box.
+ *
+ * Emits 'processing-done' event when it is done processing the current
+ * keypress, search request or selection from the list, whether that led to a
+ * search or not.
+ *
+ * @constructor
+ * @param InspectorPanel inspector
+ * The InspectorPanel whose `walker` attribute should be used for
+ * document traversal.
+ * @param nsiInputElement inputNode
+ * The input element to which the panel will be attached and from where
+ * search input will be taken.
+ */
+function SelectorAutocompleter(inspector, inputNode) {
+ this.inspector = inspector;
+ this.searchBox = inputNode;
+ this.panelDoc = this.searchBox.ownerDocument;
+
+ this.showSuggestions = this.showSuggestions.bind(this);
+ this._onSearchKeypress = this._onSearchKeypress.bind(this);
+ this._onSearchPopupClick = this._onSearchPopupClick.bind(this);
+ this._onMarkupMutation = this._onMarkupMutation.bind(this);
+
+ // Options for the AutocompletePopup.
+ let options = {
+ listId: "searchbox-panel-listbox",
+ autoSelect: true,
+ position: "top",
+ theme: "auto",
+ onClick: this._onSearchPopupClick,
+ };
+
+ // The popup will be attached to the toolbox document.
+ this.searchPopup = new AutocompletePopup(inspector._toolbox.doc, options);
+
+ this.searchBox.addEventListener("input", this.showSuggestions, true);
+ this.searchBox.addEventListener("keypress", this._onSearchKeypress, true);
+ this.inspector.on("markupmutation", this._onMarkupMutation);
+
+ // For testing, we need to be able to wait for the most recent node request
+ // to finish. Tests can watch this promise for that.
+ this._lastQuery = promise.resolve(null);
+ EventEmitter.decorate(this);
+}
+
+exports.SelectorAutocompleter = SelectorAutocompleter;
+
+SelectorAutocompleter.prototype = {
+ get walker() {
+ return this.inspector.walker;
+ },
+
+ // The possible states of the query.
+ States: {
+ CLASS: "class",
+ ID: "id",
+ TAG: "tag",
+ ATTRIBUTE: "attribute",
+ },
+
+ // The current state of the query.
+ _state: null,
+
+ // The query corresponding to last state computation.
+ _lastStateCheckAt: null,
+
+ /**
+ * Computes the state of the query. State refers to whether the query
+ * currently requires a class suggestion, or a tag, or an Id suggestion.
+ * This getter will effectively compute the state by traversing the query
+ * character by character each time the query changes.
+ *
+ * @example
+ * '#f' requires an Id suggestion, so the state is States.ID
+ * 'div > .foo' requires class suggestion, so state is States.CLASS
+ */
+ get state() {
+ if (!this.searchBox || !this.searchBox.value) {
+ return null;
+ }
+
+ let query = this.searchBox.value;
+ if (this._lastStateCheckAt == query) {
+ // If query is the same, return early.
+ return this._state;
+ }
+ this._lastStateCheckAt = query;
+
+ this._state = null;
+ let subQuery = "";
+ // Now we iterate over the query and decide the state character by
+ // character.
+ // The logic here is that while iterating, the state can go from one to
+ // another with some restrictions. Like, if the state is Class, then it can
+ // never go to Tag state without a space or '>' character; Or like, a Class
+ // state with only '.' cannot go to an Id state without any [a-zA-Z] after
+ // the '.' which means that '.#' is a selector matching a class name '#'.
+ // Similarily for '#.' which means a selctor matching an id '.'.
+ for (let i = 1; i <= query.length; i++) {
+ // Calculate the state.
+ subQuery = query.slice(0, i);
+ let [secondLastChar, lastChar] = subQuery.slice(-2);
+ switch (this._state) {
+ case null:
+ // This will happen only in the first iteration of the for loop.
+ lastChar = secondLastChar;
+
+ case this.States.TAG: // eslint-disable-line
+ if (lastChar === ".") {
+ this._state = this.States.CLASS;
+ } else if (lastChar === "#") {
+ this._state = this.States.ID;
+ } else if (lastChar === "[") {
+ this._state = this.States.ATTRIBUTE;
+ } else {
+ this._state = this.States.TAG;
+ }
+ break;
+
+ case this.States.CLASS:
+ if (subQuery.match(/[\.]+[^\.]*$/)[0].length > 2) {
+ // Checks whether the subQuery has atleast one [a-zA-Z] after the
+ // '.'.
+ if (lastChar === " " || lastChar === ">") {
+ this._state = this.States.TAG;
+ } else if (lastChar === "#") {
+ this._state = this.States.ID;
+ } else if (lastChar === "[") {
+ this._state = this.States.ATTRIBUTE;
+ } else {
+ this._state = this.States.CLASS;
+ }
+ }
+ break;
+
+ case this.States.ID:
+ if (subQuery.match(/[#]+[^#]*$/)[0].length > 2) {
+ // Checks whether the subQuery has atleast one [a-zA-Z] after the
+ // '#'.
+ if (lastChar === " " || lastChar === ">") {
+ this._state = this.States.TAG;
+ } else if (lastChar === ".") {
+ this._state = this.States.CLASS;
+ } else if (lastChar === "[") {
+ this._state = this.States.ATTRIBUTE;
+ } else {
+ this._state = this.States.ID;
+ }
+ }
+ break;
+
+ case this.States.ATTRIBUTE:
+ if (subQuery.match(/[\[][^\]]+[\]]/) !== null) {
+ // Checks whether the subQuery has at least one ']' after the '['.
+ if (lastChar === " " || lastChar === ">") {
+ this._state = this.States.TAG;
+ } else if (lastChar === ".") {
+ this._state = this.States.CLASS;
+ } else if (lastChar === "#") {
+ this._state = this.States.ID;
+ } else {
+ this._state = this.States.ATTRIBUTE;
+ }
+ }
+ break;
+ }
+ }
+ return this._state;
+ },
+
+ /**
+ * Removes event listeners and cleans up references.
+ */
+ destroy: function () {
+ this.searchBox.removeEventListener("input", this.showSuggestions, true);
+ this.searchBox.removeEventListener("keypress",
+ this._onSearchKeypress, true);
+ this.inspector.off("markupmutation", this._onMarkupMutation);
+ this.searchPopup.destroy();
+ this.searchPopup = null;
+ this.searchBox = null;
+ this.panelDoc = null;
+ },
+
+ /**
+ * Handles keypresses inside the input box.
+ */
+ _onSearchKeypress: function (event) {
+ let popup = this.searchPopup;
+
+ switch (event.keyCode) {
+ case KeyCodes.DOM_VK_RETURN:
+ case KeyCodes.DOM_VK_TAB:
+ if (popup.isOpen) {
+ if (popup.selectedItem) {
+ this.searchBox.value = popup.selectedItem.label;
+ }
+ this.hidePopup();
+ } else if (!popup.isOpen) {
+ // When tab is pressed with focus on searchbox and closed popup,
+ // do not prevent the default to avoid a keyboard trap and move focus
+ // to next/previous element.
+ this.emit("processing-done");
+ return;
+ }
+ break;
+
+ case KeyCodes.DOM_VK_UP:
+ if (popup.isOpen && popup.itemCount > 0) {
+ if (popup.selectedIndex === 0) {
+ popup.selectedIndex = popup.itemCount - 1;
+ } else {
+ popup.selectedIndex--;
+ }
+ this.searchBox.value = popup.selectedItem.label;
+ }
+ break;
+
+ case KeyCodes.DOM_VK_DOWN:
+ if (popup.isOpen && popup.itemCount > 0) {
+ if (popup.selectedIndex === popup.itemCount - 1) {
+ popup.selectedIndex = 0;
+ } else {
+ popup.selectedIndex++;
+ }
+ this.searchBox.value = popup.selectedItem.label;
+ }
+ break;
+
+ case KeyCodes.DOM_VK_ESCAPE:
+ if (popup.isOpen) {
+ this.hidePopup();
+ }
+ break;
+
+ default:
+ return;
+ }
+
+ event.preventDefault();
+ event.stopPropagation();
+ this.emit("processing-done");
+ },
+
+ /**
+ * Handles click events from the autocomplete popup.
+ */
+ _onSearchPopupClick: function (event) {
+ let selectedItem = this.searchPopup.selectedItem;
+ if (selectedItem) {
+ this.searchBox.value = selectedItem.label;
+ }
+ this.hidePopup();
+
+ event.preventDefault();
+ event.stopPropagation();
+ },
+
+ /**
+ * Reset previous search results on markup-mutations to make sure we search
+ * again after nodes have been added/removed/changed.
+ */
+ _onMarkupMutation: function () {
+ this._searchResults = null;
+ this._lastSearched = null;
+ },
+
+ /**
+ * Populates the suggestions list and show the suggestion popup.
+ *
+ * @return {Promise} promise that will resolve when the autocomplete popup is fully
+ * displayed or hidden.
+ */
+ _showPopup: function (list, firstPart, popupState) {
+ let total = 0;
+ let query = this.searchBox.value;
+ let items = [];
+
+ for (let [value, , state] of list) {
+ if (query.match(/[\s>+]$/)) {
+ // for cases like 'div ' or 'div >' or 'div+'
+ value = query + value;
+ } else if (query.match(/[\s>+][\.#a-zA-Z][^\s>+\.#\[]*$/)) {
+ // for cases like 'div #a' or 'div .a' or 'div > d' and likewise
+ let lastPart = query.match(/[\s>+][\.#a-zA-Z][^\s>+\.#\[]*$/)[0];
+ value = query.slice(0, -1 * lastPart.length + 1) + value;
+ } else if (query.match(/[a-zA-Z][#\.][^#\.\s+>]*$/)) {
+ // for cases like 'div.class' or '#foo.bar' and likewise
+ let lastPart = query.match(/[a-zA-Z][#\.][^#\.\s+>]*$/)[0];
+ value = query.slice(0, -1 * lastPart.length + 1) + value;
+ } else if (query.match(/[a-zA-Z]*\[[^\]]*\][^\]]*/)) {
+ // for cases like '[foo].bar' and likewise
+ let attrPart = query.substring(0, query.lastIndexOf("]") + 1);
+ value = attrPart + value;
+ }
+
+ let item = {
+ preLabel: query,
+ label: value
+ };
+
+ // In case the query's state is tag and the item's state is id or class
+ // adjust the preLabel
+ if (popupState === this.States.TAG && state === this.States.CLASS) {
+ item.preLabel = "." + item.preLabel;
+ }
+ if (popupState === this.States.TAG && state === this.States.ID) {
+ item.preLabel = "#" + item.preLabel;
+ }
+
+ items.unshift(item);
+ if (++total > MAX_SUGGESTIONS - 1) {
+ break;
+ }
+ }
+
+ if (total > 0) {
+ let onPopupOpened = this.searchPopup.once("popup-opened");
+ this.searchPopup.once("popup-closed", () => {
+ this.searchPopup.setItems(items);
+ this.searchPopup.openPopup(this.searchBox);
+ });
+ this.searchPopup.hidePopup();
+ return onPopupOpened;
+ }
+
+ return this.hidePopup();
+ },
+
+ /**
+ * Hide the suggestion popup if necessary.
+ */
+ hidePopup: function () {
+ let onPopupClosed = this.searchPopup.once("popup-closed");
+ this.searchPopup.hidePopup();
+ return onPopupClosed;
+ },
+
+ /**
+ * Suggests classes,ids and tags based on the user input as user types in the
+ * searchbox.
+ */
+ showSuggestions: function () {
+ let query = this.searchBox.value;
+ let state = this.state;
+ let firstPart = "";
+
+ if (query.endsWith("*") || state === this.States.ATTRIBUTE) {
+ // Hide the popup if the query ends with * (because we don't want to
+ // suggest all nodes) or if it is an attribute selector (because
+ // it would give a lot of useless results).
+ this.hidePopup();
+ return;
+ }
+
+ if (state === this.States.TAG) {
+ // gets the tag that is being completed. For ex. 'div.foo > s' returns
+ // 's', 'di' returns 'di' and likewise.
+ firstPart = (query.match(/[\s>+]?([a-zA-Z]*)$/) || ["", query])[1];
+ query = query.slice(0, query.length - firstPart.length);
+ } else if (state === this.States.CLASS) {
+ // gets the class that is being completed. For ex. '.foo.b' returns 'b'
+ firstPart = query.match(/\.([^\.]*)$/)[1];
+ query = query.slice(0, query.length - firstPart.length - 1);
+ } else if (state === this.States.ID) {
+ // gets the id that is being completed. For ex. '.foo#b' returns 'b'
+ firstPart = query.match(/#([^#]*)$/)[1];
+ query = query.slice(0, query.length - firstPart.length - 1);
+ }
+ // TODO: implement some caching so that over the wire request is not made
+ // everytime.
+ if (/[\s+>~]$/.test(query)) {
+ query += "*";
+ }
+
+ let suggestionsPromise = this.walker.getSuggestionsForQuery(
+ query, firstPart, state);
+ this._lastQuery = suggestionsPromise.then(result => {
+ this.emit("processing-done");
+ if (result.query !== query) {
+ // This means that this response is for a previous request and the user
+ // as since typed something extra leading to a new request.
+ return promise.resolve(null);
+ }
+
+ if (state === this.States.CLASS) {
+ firstPart = "." + firstPart;
+ } else if (state === this.States.ID) {
+ firstPart = "#" + firstPart;
+ }
+
+ // If there is a single tag match and it's what the user typed, then
+ // don't need to show a popup.
+ if (result.suggestions.length === 1 &&
+ result.suggestions[0][0] === firstPart) {
+ result.suggestions = [];
+ }
+
+ // Wait for the autocomplete-popup to fire its popup-opened event, to make sure
+ // the autoSelect item has been selected.
+ return this._showPopup(result.suggestions, firstPart, state);
+ });
+
+ return;
+ }
+};
diff --git a/devtools/client/inspector/inspector.js b/devtools/client/inspector/inspector.js
new file mode 100644
index 000000000..c056c213f
--- /dev/null
+++ b/devtools/client/inspector/inspector.js
@@ -0,0 +1,1936 @@
+/* -*- 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/. */
+
+/* global window */
+
+"use strict";
+
+var Cu = Components.utils;
+var { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+var Services = require("Services");
+var promise = require("promise");
+var defer = require("devtools/shared/defer");
+var EventEmitter = require("devtools/shared/event-emitter");
+const {executeSoon} = require("devtools/shared/DevToolsUtils");
+var {KeyShortcuts} = require("devtools/client/shared/key-shortcuts");
+var {Task} = require("devtools/shared/task");
+const {initCssProperties} = require("devtools/shared/fronts/css-properties");
+const nodeConstants = require("devtools/shared/dom-node-constants");
+const Telemetry = require("devtools/client/shared/telemetry");
+
+const Menu = require("devtools/client/framework/menu");
+const MenuItem = require("devtools/client/framework/menu-item");
+
+const {CommandUtils} = require("devtools/client/shared/developer-toolbar");
+const {ComputedViewTool} = require("devtools/client/inspector/computed/computed");
+const {FontInspector} = require("devtools/client/inspector/fonts/fonts");
+const {HTMLBreadcrumbs} = require("devtools/client/inspector/breadcrumbs");
+const {InspectorSearch} = require("devtools/client/inspector/inspector-search");
+const MarkupView = require("devtools/client/inspector/markup/markup");
+const {RuleViewTool} = require("devtools/client/inspector/rules/rules");
+const {ToolSidebar} = require("devtools/client/inspector/toolsidebar");
+const {ViewHelpers} = require("devtools/client/shared/widgets/view-helpers");
+const clipboardHelper = require("devtools/shared/platform/clipboard");
+
+const {LocalizationHelper, localizeMarkup} = require("devtools/shared/l10n");
+const INSPECTOR_L10N =
+ new LocalizationHelper("devtools/client/locales/inspector.properties");
+const TOOLBOX_L10N = new LocalizationHelper("devtools/client/locales/toolbox.properties");
+
+// Sidebar dimensions
+const INITIAL_SIDEBAR_SIZE = 350;
+
+// If the toolbox width is smaller than given amount of pixels,
+// the sidebar automatically switches from 'landscape' to 'portrait' mode.
+const PORTRAIT_MODE_WIDTH = 700;
+
+/**
+ * Represents an open instance of the Inspector for a tab.
+ * The inspector controls the breadcrumbs, the markup view, and the sidebar
+ * (computed view, rule view, font view and animation inspector).
+ *
+ * Events:
+ * - ready
+ * Fired when the inspector panel is opened for the first time and ready to
+ * use
+ * - new-root
+ * Fired after a new root (navigation to a new page) event was fired by
+ * the walker, and taken into account by the inspector (after the markup
+ * view has been reloaded)
+ * - markuploaded
+ * Fired when the markup-view frame has loaded
+ * - breadcrumbs-updated
+ * Fired when the breadcrumb widget updates to a new node
+ * - boxmodel-view-updated
+ * Fired when the box model updates to a new node
+ * - markupmutation
+ * Fired after markup mutations have been processed by the markup-view
+ * - computed-view-refreshed
+ * Fired when the computed rules view updates to a new node
+ * - computed-view-property-expanded
+ * Fired when a property is expanded in the computed rules view
+ * - computed-view-property-collapsed
+ * Fired when a property is collapsed in the computed rules view
+ * - computed-view-sourcelinks-updated
+ * Fired when the stylesheet source links have been updated (when switching
+ * to source-mapped files)
+ * - computed-view-filtered
+ * Fired when the computed rules view is filtered
+ * - rule-view-refreshed
+ * Fired when the rule view updates to a new node
+ * - rule-view-sourcelinks-updated
+ * Fired when the stylesheet source links have been updated (when switching
+ * to source-mapped files)
+ */
+function Inspector(toolbox) {
+ this._toolbox = toolbox;
+ this._target = toolbox.target;
+ this.panelDoc = window.document;
+ this.panelWin = window;
+ this.panelWin.inspector = this;
+
+ this.telemetry = new Telemetry();
+
+ this.nodeMenuTriggerInfo = null;
+
+ this._handleRejectionIfNotDestroyed = this._handleRejectionIfNotDestroyed.bind(this);
+ this._onBeforeNavigate = this._onBeforeNavigate.bind(this);
+ this.onNewRoot = this.onNewRoot.bind(this);
+ this._onContextMenu = this._onContextMenu.bind(this);
+ this.onTextBoxContextMenu = this.onTextBoxContextMenu.bind(this);
+ this._updateSearchResultsLabel = this._updateSearchResultsLabel.bind(this);
+ this.onNewSelection = this.onNewSelection.bind(this);
+ this.onDetached = this.onDetached.bind(this);
+ this.onPaneToggleButtonClicked = this.onPaneToggleButtonClicked.bind(this);
+ this._onMarkupFrameLoad = this._onMarkupFrameLoad.bind(this);
+ this.onPanelWindowResize = this.onPanelWindowResize.bind(this);
+ this.onSidebarShown = this.onSidebarShown.bind(this);
+ this.onSidebarHidden = this.onSidebarHidden.bind(this);
+
+ this._target.on("will-navigate", this._onBeforeNavigate);
+ this._detectingActorFeatures = this._detectActorFeatures();
+
+ EventEmitter.decorate(this);
+}
+
+Inspector.prototype = {
+ /**
+ * open is effectively an asynchronous constructor
+ */
+ init: Task.async(function* () {
+ // Localize all the nodes containing a data-localization attribute.
+ localizeMarkup(this.panelDoc);
+
+ this._cssPropertiesLoaded = initCssProperties(this.toolbox);
+ yield this._cssPropertiesLoaded;
+ yield this.target.makeRemote();
+ yield this._getPageStyle();
+
+ // This may throw if the document is still loading and we are
+ // refering to a dead about:blank document
+ let defaultSelection = yield this._getDefaultNodeForSelection()
+ .catch(this._handleRejectionIfNotDestroyed);
+
+ return yield this._deferredOpen(defaultSelection);
+ }),
+
+ get toolbox() {
+ return this._toolbox;
+ },
+
+ get inspector() {
+ return this._toolbox.inspector;
+ },
+
+ get walker() {
+ return this._toolbox.walker;
+ },
+
+ get selection() {
+ return this._toolbox.selection;
+ },
+
+ get highlighter() {
+ return this._toolbox.highlighter;
+ },
+
+ get isOuterHTMLEditable() {
+ return this._target.client.traits.editOuterHTML;
+ },
+
+ get hasUrlToImageDataResolver() {
+ return this._target.client.traits.urlToImageDataResolver;
+ },
+
+ get canGetUniqueSelector() {
+ return this._target.client.traits.getUniqueSelector;
+ },
+
+ get canGetUsedFontFaces() {
+ return this._target.client.traits.getUsedFontFaces;
+ },
+
+ get canPasteInnerOrAdjacentHTML() {
+ return this._target.client.traits.pasteHTML;
+ },
+
+ /**
+ * Handle promise rejections for various asynchronous actions, and only log errors if
+ * the inspector panel still exists.
+ * This is useful to silence useless errors that happen when the inspector is closed
+ * while still initializing (and making protocol requests).
+ */
+ _handleRejectionIfNotDestroyed: function (e) {
+ if (!this._panelDestroyer) {
+ console.error(e);
+ }
+ },
+
+ /**
+ * Figure out what features the backend supports
+ */
+ _detectActorFeatures: function () {
+ this._supportsDuplicateNode = false;
+ this._supportsScrollIntoView = false;
+ this._supportsResolveRelativeURL = false;
+
+ // Use getActorDescription first so that all actorHasMethod calls use
+ // a cached response from the server.
+ return this._target.getActorDescription("domwalker").then(desc => {
+ return promise.all([
+ this._target.actorHasMethod("domwalker", "duplicateNode").then(value => {
+ this._supportsDuplicateNode = value;
+ }).catch(e => console.error(e)),
+ this._target.actorHasMethod("domnode", "scrollIntoView").then(value => {
+ this._supportsScrollIntoView = value;
+ }).catch(e => console.error(e)),
+ this._target.actorHasMethod("inspector", "resolveRelativeURL").then(value => {
+ this._supportsResolveRelativeURL = value;
+ }).catch(e => console.error(e)),
+ ]);
+ });
+ },
+
+ _deferredOpen: function (defaultSelection) {
+ let deferred = defer();
+
+ this.breadcrumbs = new HTMLBreadcrumbs(this);
+
+ this.walker.on("new-root", this.onNewRoot);
+
+ this.selection.on("new-node-front", this.onNewSelection);
+ this.selection.on("detached-front", this.onDetached);
+
+ if (this.target.isLocalTab) {
+ // Show a warning when the debugger is paused.
+ // We show the warning only when the inspector
+ // is selected.
+ this.updateDebuggerPausedWarning = () => {
+ let notificationBox = this._toolbox.getNotificationBox();
+ let notification =
+ notificationBox.getNotificationWithValue("inspector-script-paused");
+ if (!notification && this._toolbox.currentToolId == "inspector" &&
+ this._toolbox.threadClient.paused) {
+ let message = INSPECTOR_L10N.getStr("debuggerPausedWarning.message");
+ notificationBox.appendNotification(message,
+ "inspector-script-paused", "", notificationBox.PRIORITY_WARNING_HIGH);
+ }
+
+ if (notification && this._toolbox.currentToolId != "inspector") {
+ notificationBox.removeNotification(notification);
+ }
+
+ if (notification && !this._toolbox.threadClient.paused) {
+ notificationBox.removeNotification(notification);
+ }
+ };
+ this.target.on("thread-paused", this.updateDebuggerPausedWarning);
+ this.target.on("thread-resumed", this.updateDebuggerPausedWarning);
+ this._toolbox.on("select", this.updateDebuggerPausedWarning);
+ this.updateDebuggerPausedWarning();
+ }
+
+ this._initMarkup();
+ this.isReady = false;
+
+ this.once("markuploaded", () => {
+ this.isReady = true;
+
+ // All the components are initialized. Let's select a node.
+ if (defaultSelection) {
+ this.selection.setNodeFront(defaultSelection, "inspector-open");
+ this.markup.expandNode(this.selection.nodeFront);
+ }
+
+ // And setup the toolbar only now because it may depend on the document.
+ this.setupToolbar();
+
+ this.emit("ready");
+ deferred.resolve(this);
+ });
+
+ this.setupSearchBox();
+ this.setupSidebar();
+
+ return deferred.promise;
+ },
+
+ _onBeforeNavigate: function () {
+ this._defaultNode = null;
+ this.selection.setNodeFront(null);
+ this._destroyMarkup();
+ this.isDirty = false;
+ this._pendingSelection = null;
+ },
+
+ _getPageStyle: function () {
+ return this.inspector.getPageStyle().then(pageStyle => {
+ this.pageStyle = pageStyle;
+ }, this._handleRejectionIfNotDestroyed);
+ },
+
+ /**
+ * Return a promise that will resolve to the default node for selection.
+ */
+ _getDefaultNodeForSelection: function () {
+ if (this._defaultNode) {
+ return this._defaultNode;
+ }
+ let walker = this.walker;
+ let rootNode = null;
+ let pendingSelection = this._pendingSelection;
+
+ // A helper to tell if the target has or is about to navigate.
+ // this._pendingSelection changes on "will-navigate" and "new-root" events.
+ let hasNavigated = () => pendingSelection !== this._pendingSelection;
+
+ // If available, set either the previously selected node or the body
+ // as default selected, else set documentElement
+ return walker.getRootNode().then(node => {
+ if (hasNavigated()) {
+ return promise.reject("navigated; resolution of _defaultNode aborted");
+ }
+
+ rootNode = node;
+ if (this.selectionCssSelector) {
+ return walker.querySelector(rootNode, this.selectionCssSelector);
+ }
+ return null;
+ }).then(front => {
+ if (hasNavigated()) {
+ return promise.reject("navigated; resolution of _defaultNode aborted");
+ }
+
+ if (front) {
+ return front;
+ }
+ return walker.querySelector(rootNode, "body");
+ }).then(front => {
+ if (hasNavigated()) {
+ return promise.reject("navigated; resolution of _defaultNode aborted");
+ }
+
+ if (front) {
+ return front;
+ }
+ return this.walker.documentElement();
+ }).then(node => {
+ if (hasNavigated()) {
+ return promise.reject("navigated; resolution of _defaultNode aborted");
+ }
+ this._defaultNode = node;
+ return node;
+ });
+ },
+
+ /**
+ * Target getter.
+ */
+ get target() {
+ return this._target;
+ },
+
+ /**
+ * Target setter.
+ */
+ set target(value) {
+ this._target = value;
+ },
+
+ /**
+ * Indicate that a tool has modified the state of the page. Used to
+ * decide whether to show the "are you sure you want to navigate"
+ * notification.
+ */
+ markDirty: function () {
+ this.isDirty = true;
+ },
+
+ /**
+ * Hooks the searchbar to show result and auto completion suggestions.
+ */
+ setupSearchBox: function () {
+ this.searchBox = this.panelDoc.getElementById("inspector-searchbox");
+ this.searchClearButton = this.panelDoc.getElementById("inspector-searchinput-clear");
+ this.searchResultsLabel = this.panelDoc.getElementById("inspector-searchlabel");
+
+ this.search = new InspectorSearch(this, this.searchBox, this.searchClearButton);
+ this.search.on("search-cleared", this._updateSearchResultsLabel);
+ this.search.on("search-result", this._updateSearchResultsLabel);
+
+ let shortcuts = new KeyShortcuts({
+ window: this.panelDoc.defaultView,
+ });
+ let key = INSPECTOR_L10N.getStr("inspector.searchHTML.key");
+ shortcuts.on(key, (name, event) => {
+ // Prevent overriding same shortcut from the computed/rule views
+ if (event.target.closest("#sidebar-panel-ruleview") ||
+ event.target.closest("#sidebar-panel-computedview")) {
+ return;
+ }
+ event.preventDefault();
+ this.searchBox.focus();
+ });
+ },
+
+ get searchSuggestions() {
+ return this.search.autocompleter;
+ },
+
+ _updateSearchResultsLabel: function (event, result) {
+ let str = "";
+ if (event !== "search-cleared") {
+ if (result) {
+ str = INSPECTOR_L10N.getFormatStr(
+ "inspector.searchResultsCount2", result.resultsIndex + 1, result.resultsLength);
+ } else {
+ str = INSPECTOR_L10N.getStr("inspector.searchResultsNone");
+ }
+ }
+
+ this.searchResultsLabel.textContent = str;
+ },
+
+ get React() {
+ return this._toolbox.React;
+ },
+
+ get ReactDOM() {
+ return this._toolbox.ReactDOM;
+ },
+
+ get ReactRedux() {
+ return this._toolbox.ReactRedux;
+ },
+
+ get browserRequire() {
+ return this._toolbox.browserRequire;
+ },
+
+ get InspectorTabPanel() {
+ if (!this._InspectorTabPanel) {
+ this._InspectorTabPanel =
+ this.React.createFactory(this.browserRequire(
+ "devtools/client/inspector/components/inspector-tab-panel"));
+ }
+ return this._InspectorTabPanel;
+ },
+
+ /**
+ * Check if the inspector should use the landscape mode.
+ *
+ * @return {Boolean} true if the inspector should be in landscape mode.
+ */
+ useLandscapeMode: function () {
+ let { clientWidth } = this.panelDoc.getElementById("inspector-splitter-box");
+ return clientWidth > PORTRAIT_MODE_WIDTH;
+ },
+
+ /**
+ * Build Splitter located between the main and side area of
+ * the Inspector panel.
+ */
+ setupSplitter: function () {
+ let SplitBox = this.React.createFactory(this.browserRequire(
+ "devtools/client/shared/components/splitter/split-box"));
+
+ let splitter = SplitBox({
+ className: "inspector-sidebar-splitter",
+ initialWidth: INITIAL_SIDEBAR_SIZE,
+ initialHeight: INITIAL_SIDEBAR_SIZE,
+ splitterSize: 1,
+ endPanelControl: true,
+ startPanel: this.InspectorTabPanel({
+ id: "inspector-main-content"
+ }),
+ endPanel: this.InspectorTabPanel({
+ id: "inspector-sidebar-container"
+ }),
+ vert: this.useLandscapeMode(),
+ });
+
+ this._splitter = this.ReactDOM.render(splitter,
+ this.panelDoc.getElementById("inspector-splitter-box"));
+
+ this.panelWin.addEventListener("resize", this.onPanelWindowResize, true);
+
+ // Persist splitter state in preferences.
+ this.sidebar.on("show", this.onSidebarShown);
+ this.sidebar.on("hide", this.onSidebarHidden);
+ this.sidebar.on("destroy", this.onSidebarHidden);
+ },
+
+ /**
+ * Splitter clean up.
+ */
+ teardownSplitter: function () {
+ this.panelWin.removeEventListener("resize", this.onPanelWindowResize, true);
+
+ this.sidebar.off("show", this.onSidebarShown);
+ this.sidebar.off("hide", this.onSidebarHidden);
+ this.sidebar.off("destroy", this.onSidebarHidden);
+ },
+
+ /**
+ * If Toolbox width is less than 600 px, the splitter changes its mode
+ * to `horizontal` to support portrait view.
+ */
+ onPanelWindowResize: function () {
+ this._splitter.setState({
+ vert: this.useLandscapeMode(),
+ });
+ },
+
+ onSidebarShown: function () {
+ let width;
+ let height;
+
+ // Initialize splitter size from preferences.
+ try {
+ width = Services.prefs.getIntPref("devtools.toolsidebar-width.inspector");
+ height = Services.prefs.getIntPref("devtools.toolsidebar-height.inspector");
+ } catch (e) {
+ // Set width and height of the splitter. Only one
+ // value is really useful at a time depending on the current
+ // orientation (vertical/horizontal).
+ // Having both is supported by the splitter component.
+ width = INITIAL_SIDEBAR_SIZE;
+ height = INITIAL_SIDEBAR_SIZE;
+ }
+
+ this._splitter.setState({width, height});
+ },
+
+ onSidebarHidden: function () {
+ // Store the current splitter size to preferences.
+ let state = this._splitter.state;
+ Services.prefs.setIntPref("devtools.toolsidebar-width.inspector", state.width);
+ Services.prefs.setIntPref("devtools.toolsidebar-height.inspector", state.height);
+ },
+
+ /**
+ * Build the sidebar.
+ */
+ setupSidebar: function () {
+ let tabbox = this.panelDoc.querySelector("#inspector-sidebar");
+ this.sidebar = new ToolSidebar(tabbox, this, "inspector", {
+ showAllTabsMenu: true
+ });
+
+ let defaultTab = Services.prefs.getCharPref("devtools.inspector.activeSidebar");
+
+ this._setDefaultSidebar = (event, toolId) => {
+ Services.prefs.setCharPref("devtools.inspector.activeSidebar", toolId);
+ };
+
+ this.sidebar.on("select", this._setDefaultSidebar);
+
+ if (!Services.prefs.getBoolPref("devtools.fontinspector.enabled") &&
+ defaultTab == "fontinspector") {
+ defaultTab = "ruleview";
+ }
+
+ // Append all side panels
+ this.sidebar.addExistingTab(
+ "ruleview",
+ INSPECTOR_L10N.getStr("inspector.sidebar.ruleViewTitle"),
+ defaultTab == "ruleview");
+
+ this.sidebar.addExistingTab(
+ "computedview",
+ INSPECTOR_L10N.getStr("inspector.sidebar.computedViewTitle"),
+ defaultTab == "computedview");
+
+ this.ruleview = new RuleViewTool(this, this.panelWin);
+ this.computedview = new ComputedViewTool(this, this.panelWin);
+
+ if (Services.prefs.getBoolPref("devtools.layoutview.enabled")) {
+ const {LayoutView} = this.browserRequire("devtools/client/inspector/layout/layout");
+ this.layoutview = new LayoutView(this, this.panelWin);
+ }
+
+ if (this.target.form.animationsActor) {
+ this.sidebar.addFrameTab(
+ "animationinspector",
+ INSPECTOR_L10N.getStr("inspector.sidebar.animationInspectorTitle"),
+ "chrome://devtools/content/animationinspector/animation-inspector.xhtml",
+ defaultTab == "animationinspector");
+ }
+
+ if (Services.prefs.getBoolPref("devtools.fontinspector.enabled") &&
+ this.canGetUsedFontFaces) {
+ this.sidebar.addExistingTab(
+ "fontinspector",
+ INSPECTOR_L10N.getStr("inspector.sidebar.fontInspectorTitle"),
+ defaultTab == "fontinspector");
+
+ this.fontInspector = new FontInspector(this, this.panelWin);
+ this.sidebar.toggleTab(true, "fontinspector");
+ }
+
+ // Setup the splitter before the sidebar is displayed so,
+ // we don't miss any events.
+ this.setupSplitter();
+
+ this.sidebar.show(defaultTab);
+ },
+
+ /**
+ * Register a side-panel tab. This API can be used outside of
+ * DevTools (e.g. from an extension) as well as by DevTools
+ * code base.
+ *
+ * @param {string} tab uniq id
+ * @param {string} title tab title
+ * @param {React.Component} panel component. See `InspectorPanelTab` as an example.
+ * @param {boolean} selected true if the panel should be selected
+ */
+ addSidebarTab: function (id, title, panel, selected) {
+ this.sidebar.addTab(id, title, panel, selected);
+ },
+
+ setupToolbar: function () {
+ this.teardownToolbar();
+
+ // Setup the sidebar toggle button.
+ let SidebarToggle = this.React.createFactory(this.browserRequire(
+ "devtools/client/shared/components/sidebar-toggle"));
+
+ let sidebarToggle = SidebarToggle({
+ onClick: this.onPaneToggleButtonClicked,
+ collapsed: false,
+ expandPaneTitle: INSPECTOR_L10N.getStr("inspector.expandPane"),
+ collapsePaneTitle: INSPECTOR_L10N.getStr("inspector.collapsePane"),
+ });
+
+ let parentBox = this.panelDoc.getElementById("inspector-sidebar-toggle-box");
+ this._sidebarToggle = this.ReactDOM.render(sidebarToggle, parentBox);
+
+ // Setup the add-node button.
+ this.addNode = this.addNode.bind(this);
+ this.addNodeButton = this.panelDoc.getElementById("inspector-element-add-button");
+ this.addNodeButton.addEventListener("click", this.addNode);
+
+ // Setup the eye-dropper icon if we're in an HTML document and we have actor support.
+ if (this.selection.nodeFront && this.selection.nodeFront.isInHTMLDocument) {
+ this.target.actorHasMethod("inspector", "pickColorFromPage").then(value => {
+ if (!value) {
+ return;
+ }
+
+ this.onEyeDropperDone = this.onEyeDropperDone.bind(this);
+ this.onEyeDropperButtonClicked = this.onEyeDropperButtonClicked.bind(this);
+ this.eyeDropperButton = this.panelDoc
+ .getElementById("inspector-eyedropper-toggle");
+ this.eyeDropperButton.disabled = false;
+ this.eyeDropperButton.title = INSPECTOR_L10N.getStr("inspector.eyedropper.label");
+ this.eyeDropperButton.addEventListener("click", this.onEyeDropperButtonClicked);
+ }, e => console.error(e));
+ } else {
+ let eyeDropperButton = this.panelDoc.getElementById("inspector-eyedropper-toggle");
+ eyeDropperButton.disabled = true;
+ eyeDropperButton.title = INSPECTOR_L10N.getStr("eyedropper.disabled.title");
+ }
+ },
+
+ teardownToolbar: function () {
+ this._sidebarToggle = null;
+
+ if (this.addNodeButton) {
+ this.addNodeButton.removeEventListener("click", this.addNode);
+ this.addNodeButton = null;
+ }
+
+ if (this.eyeDropperButton) {
+ this.eyeDropperButton.removeEventListener("click", this.onEyeDropperButtonClicked);
+ this.eyeDropperButton = null;
+ }
+ },
+
+ /**
+ * Reset the inspector on new root mutation.
+ */
+ onNewRoot: function () {
+ this._defaultNode = null;
+ this.selection.setNodeFront(null);
+ this._destroyMarkup();
+ this.isDirty = false;
+
+ let onNodeSelected = defaultNode => {
+ // Cancel this promise resolution as a new one had
+ // been queued up.
+ if (this._pendingSelection != onNodeSelected) {
+ return;
+ }
+ this._pendingSelection = null;
+ this.selection.setNodeFront(defaultNode, "navigateaway");
+
+ this._initMarkup();
+ this.once("markuploaded", () => {
+ if (!this.markup) {
+ return;
+ }
+ this.markup.expandNode(this.selection.nodeFront);
+ this.emit("new-root");
+ });
+
+ // Setup the toolbar again, since its content may depend on the current document.
+ this.setupToolbar();
+ };
+ this._pendingSelection = onNodeSelected;
+ this._getDefaultNodeForSelection()
+ .then(onNodeSelected, this._handleRejectionIfNotDestroyed);
+ },
+
+ _selectionCssSelector: null,
+
+ /**
+ * Set the currently selected node unique css selector.
+ * Will store the current target url along with it to allow pre-selection at
+ * reload
+ */
+ set selectionCssSelector(cssSelector = null) {
+ if (this._panelDestroyer) {
+ return;
+ }
+
+ this._selectionCssSelector = {
+ selector: cssSelector,
+ url: this._target.url
+ };
+ },
+
+ /**
+ * Get the current selection unique css selector if any, that is, if a node
+ * is actually selected and that node has been selected while on the same url
+ */
+ get selectionCssSelector() {
+ if (this._selectionCssSelector &&
+ this._selectionCssSelector.url === this._target.url) {
+ return this._selectionCssSelector.selector;
+ }
+ return null;
+ },
+
+ /**
+ * Can a new HTML element be inserted into the currently selected element?
+ * @return {Boolean}
+ */
+ canAddHTMLChild: function () {
+ let selection = this.selection;
+
+ // Don't allow to insert an element into these elements. This should only
+ // contain elements where walker.insertAdjacentHTML has no effect.
+ let invalidTagNames = ["html", "iframe"];
+
+ return selection.isHTMLNode() &&
+ selection.isElementNode() &&
+ !selection.isPseudoElementNode() &&
+ !selection.isAnonymousNode() &&
+ invalidTagNames.indexOf(
+ selection.nodeFront.nodeName.toLowerCase()) === -1;
+ },
+
+ /**
+ * When a new node is selected.
+ */
+ onNewSelection: function (event, value, reason) {
+ if (reason === "selection-destroy") {
+ return;
+ }
+
+ // Wait for all the known tools to finish updating and then let the
+ // client know.
+ let selection = this.selection.nodeFront;
+
+ // Update the state of the add button in the toolbar depending on the
+ // current selection.
+ let btn = this.panelDoc.querySelector("#inspector-element-add-button");
+ if (this.canAddHTMLChild()) {
+ btn.removeAttribute("disabled");
+ } else {
+ btn.setAttribute("disabled", "true");
+ }
+
+ // On any new selection made by the user, store the unique css selector
+ // of the selected node so it can be restored after reload of the same page
+ if (this.canGetUniqueSelector &&
+ this.selection.isElementNode()) {
+ selection.getUniqueSelector().then(selector => {
+ this.selectionCssSelector = selector;
+ }, this._handleRejectionIfNotDestroyed);
+ }
+
+ let selfUpdate = this.updating("inspector-panel");
+ executeSoon(() => {
+ try {
+ selfUpdate(selection);
+ } catch (ex) {
+ console.error(ex);
+ }
+ });
+ },
+
+ /**
+ * Delay the "inspector-updated" notification while a tool
+ * is updating itself. Returns a function that must be
+ * invoked when the tool is done updating with the node
+ * that the tool is viewing.
+ */
+ updating: function (name) {
+ if (this._updateProgress && this._updateProgress.node != this.selection.nodeFront) {
+ this.cancelUpdate();
+ }
+
+ if (!this._updateProgress) {
+ // Start an update in progress.
+ let self = this;
+ this._updateProgress = {
+ node: this.selection.nodeFront,
+ outstanding: new Set(),
+ checkDone: function () {
+ if (this !== self._updateProgress) {
+ return;
+ }
+ // Cancel update if there is no `selection` anymore.
+ // It can happen if the inspector panel is already destroyed.
+ if (!self.selection || (this.node !== self.selection.nodeFront)) {
+ self.cancelUpdate();
+ return;
+ }
+ if (this.outstanding.size !== 0) {
+ return;
+ }
+
+ self._updateProgress = null;
+ self.emit("inspector-updated", name);
+ },
+ };
+ }
+
+ let progress = this._updateProgress;
+ let done = function () {
+ progress.outstanding.delete(done);
+ progress.checkDone();
+ };
+ progress.outstanding.add(done);
+ return done;
+ },
+
+ /**
+ * Cancel notification of inspector updates.
+ */
+ cancelUpdate: function () {
+ this._updateProgress = null;
+ },
+
+ /**
+ * When a node is deleted, select its parent node or the defaultNode if no
+ * parent is found (may happen when deleting an iframe inside which the
+ * node was selected).
+ */
+ onDetached: function (event, parentNode) {
+ this.breadcrumbs.cutAfter(this.breadcrumbs.indexOf(parentNode));
+ this.selection.setNodeFront(parentNode ? parentNode : this._defaultNode, "detached");
+ },
+
+ /**
+ * Destroy the inspector.
+ */
+ destroy: function () {
+ if (this._panelDestroyer) {
+ return this._panelDestroyer;
+ }
+
+ if (this.walker) {
+ this.walker.off("new-root", this.onNewRoot);
+ this.pageStyle = null;
+ }
+
+ this.cancelUpdate();
+
+ this.target.off("will-navigate", this._onBeforeNavigate);
+
+ this.target.off("thread-paused", this.updateDebuggerPausedWarning);
+ this.target.off("thread-resumed", this.updateDebuggerPausedWarning);
+ this._toolbox.off("select", this.updateDebuggerPausedWarning);
+
+ if (this.ruleview) {
+ this.ruleview.destroy();
+ }
+
+ if (this.computedview) {
+ this.computedview.destroy();
+ }
+
+ if (this.layoutview) {
+ this.layoutview.destroy();
+ }
+
+ if (this.fontInspector) {
+ this.fontInspector.destroy();
+ }
+
+ let cssPropertiesDestroyer = this._cssPropertiesLoaded.then(({front}) => {
+ if (front) {
+ front.destroy();
+ }
+ });
+
+ this.sidebar.off("select", this._setDefaultSidebar);
+ let sidebarDestroyer = this.sidebar.destroy();
+
+ this.teardownSplitter();
+
+ this.sidebar = null;
+
+ this.teardownToolbar();
+ this.breadcrumbs.destroy();
+ this.selection.off("new-node-front", this.onNewSelection);
+ this.selection.off("detached-front", this.onDetached);
+ let markupDestroyer = this._destroyMarkup();
+ this.panelWin.inspector = null;
+ this.target = null;
+ this.panelDoc = null;
+ this.panelWin = null;
+ this.breadcrumbs = null;
+ this._toolbox = null;
+ this.search.destroy();
+ this.search = null;
+ this.searchBox = null;
+
+ this._panelDestroyer = promise.all([
+ sidebarDestroyer,
+ markupDestroyer,
+ cssPropertiesDestroyer
+ ]);
+
+ return this._panelDestroyer;
+ },
+
+ /**
+ * Returns the clipboard content if it is appropriate for pasting
+ * into the current node's outer HTML, otherwise returns null.
+ */
+ _getClipboardContentForPaste: function () {
+ let flavors = clipboardHelper.getCurrentFlavors();
+ if (flavors.indexOf("text") != -1 ||
+ (flavors.indexOf("html") != -1 && flavors.indexOf("image") == -1)) {
+ let content = clipboardHelper.getData();
+ if (content && content.trim().length > 0) {
+ return content;
+ }
+ }
+ return null;
+ },
+
+ _onContextMenu: function (e) {
+ e.preventDefault();
+ this._openMenu({
+ screenX: e.screenX,
+ screenY: e.screenY,
+ target: e.target,
+ });
+ },
+
+ /**
+ * This is meant to be called by all the search, filter, inplace text boxes in the
+ * inspector, and just calls through to the toolbox openTextBoxContextMenu helper.
+ * @param {DOMEvent} e
+ */
+ onTextBoxContextMenu: function (e) {
+ e.stopPropagation();
+ e.preventDefault();
+ this.toolbox.openTextBoxContextMenu(e.screenX, e.screenY);
+ },
+
+ _openMenu: function ({ target, screenX = 0, screenY = 0 } = { }) {
+ let markupContainer = this.markup.getContainer(this.selection.nodeFront);
+
+ this.contextMenuTarget = target;
+ this.nodeMenuTriggerInfo = markupContainer &&
+ markupContainer.editor.getInfoAtNode(target);
+
+ let isSelectionElement = this.selection.isElementNode() &&
+ !this.selection.isPseudoElementNode();
+ let isEditableElement = isSelectionElement &&
+ !this.selection.isAnonymousNode();
+ let isDuplicatableElement = isSelectionElement &&
+ !this.selection.isAnonymousNode() &&
+ !this.selection.isRoot();
+ let isScreenshotable = isSelectionElement &&
+ this.canGetUniqueSelector &&
+ this.selection.nodeFront.isTreeDisplayed;
+
+ let menu = new Menu();
+ menu.append(new MenuItem({
+ id: "node-menu-edithtml",
+ label: INSPECTOR_L10N.getStr("inspectorHTMLEdit.label"),
+ accesskey: INSPECTOR_L10N.getStr("inspectorHTMLEdit.accesskey"),
+ disabled: !isEditableElement || !this.isOuterHTMLEditable,
+ click: () => this.editHTML(),
+ }));
+ menu.append(new MenuItem({
+ id: "node-menu-add",
+ label: INSPECTOR_L10N.getStr("inspectorAddNode.label"),
+ accesskey: INSPECTOR_L10N.getStr("inspectorAddNode.accesskey"),
+ disabled: !this.canAddHTMLChild(),
+ click: () => this.addNode(),
+ }));
+ menu.append(new MenuItem({
+ id: "node-menu-duplicatenode",
+ label: INSPECTOR_L10N.getStr("inspectorDuplicateNode.label"),
+ hidden: !this._supportsDuplicateNode,
+ disabled: !isDuplicatableElement,
+ click: () => this.duplicateNode(),
+ }));
+ menu.append(new MenuItem({
+ id: "node-menu-delete",
+ label: INSPECTOR_L10N.getStr("inspectorHTMLDelete.label"),
+ accesskey: INSPECTOR_L10N.getStr("inspectorHTMLDelete.accesskey"),
+ disabled: !isEditableElement,
+ click: () => this.deleteNode(),
+ }));
+
+ menu.append(new MenuItem({
+ label: INSPECTOR_L10N.getStr("inspectorAttributesSubmenu.label"),
+ accesskey:
+ INSPECTOR_L10N.getStr("inspectorAttributesSubmenu.accesskey"),
+ submenu: this._getAttributesSubmenu(isEditableElement),
+ }));
+
+ menu.append(new MenuItem({
+ type: "separator",
+ }));
+
+ // Set the pseudo classes
+ for (let name of ["hover", "active", "focus"]) {
+ let menuitem = new MenuItem({
+ id: "node-menu-pseudo-" + name,
+ label: name,
+ type: "checkbox",
+ click: this.togglePseudoClass.bind(this, ":" + name),
+ });
+
+ if (isSelectionElement) {
+ let checked = this.selection.nodeFront.hasPseudoClassLock(":" + name);
+ menuitem.checked = checked;
+ } else {
+ menuitem.disabled = true;
+ }
+
+ menu.append(menuitem);
+ }
+
+ menu.append(new MenuItem({
+ type: "separator",
+ }));
+
+ let copySubmenu = new Menu();
+ copySubmenu.append(new MenuItem({
+ id: "node-menu-copyinner",
+ label: INSPECTOR_L10N.getStr("inspectorCopyInnerHTML.label"),
+ accesskey: INSPECTOR_L10N.getStr("inspectorCopyInnerHTML.accesskey"),
+ disabled: !isSelectionElement,
+ click: () => this.copyInnerHTML(),
+ }));
+ copySubmenu.append(new MenuItem({
+ id: "node-menu-copyouter",
+ label: INSPECTOR_L10N.getStr("inspectorCopyOuterHTML.label"),
+ accesskey: INSPECTOR_L10N.getStr("inspectorCopyOuterHTML.accesskey"),
+ disabled: !isSelectionElement,
+ click: () => this.copyOuterHTML(),
+ }));
+ copySubmenu.append(new MenuItem({
+ id: "node-menu-copyuniqueselector",
+ label: INSPECTOR_L10N.getStr("inspectorCopyCSSSelector.label"),
+ accesskey:
+ INSPECTOR_L10N.getStr("inspectorCopyCSSSelector.accesskey"),
+ disabled: !isSelectionElement,
+ hidden: !this.canGetUniqueSelector,
+ click: () => this.copyUniqueSelector(),
+ }));
+ copySubmenu.append(new MenuItem({
+ id: "node-menu-copyimagedatauri",
+ label: INSPECTOR_L10N.getStr("inspectorImageDataUri.label"),
+ disabled: !isSelectionElement || !markupContainer ||
+ !markupContainer.isPreviewable(),
+ click: () => this.copyImageDataUri(),
+ }));
+
+ menu.append(new MenuItem({
+ label: INSPECTOR_L10N.getStr("inspectorCopyHTMLSubmenu.label"),
+ submenu: copySubmenu,
+ }));
+
+ menu.append(new MenuItem({
+ label: INSPECTOR_L10N.getStr("inspectorPasteHTMLSubmenu.label"),
+ submenu: this._getPasteSubmenu(isEditableElement),
+ }));
+
+ menu.append(new MenuItem({
+ type: "separator",
+ }));
+
+ let isNodeWithChildren = this.selection.isNode() &&
+ markupContainer.hasChildren;
+ menu.append(new MenuItem({
+ id: "node-menu-expand",
+ label: INSPECTOR_L10N.getStr("inspectorExpandNode.label"),
+ disabled: !isNodeWithChildren,
+ click: () => this.expandNode(),
+ }));
+ menu.append(new MenuItem({
+ id: "node-menu-collapse",
+ label: INSPECTOR_L10N.getStr("inspectorCollapseNode.label"),
+ disabled: !isNodeWithChildren || !markupContainer.expanded,
+ click: () => this.collapseNode(),
+ }));
+
+ menu.append(new MenuItem({
+ type: "separator",
+ }));
+
+ menu.append(new MenuItem({
+ id: "node-menu-scrollnodeintoview",
+ label: INSPECTOR_L10N.getStr("inspectorScrollNodeIntoView.label"),
+ accesskey:
+ INSPECTOR_L10N.getStr("inspectorScrollNodeIntoView.accesskey"),
+ hidden: !this._supportsScrollIntoView,
+ disabled: !isSelectionElement,
+ click: () => this.scrollNodeIntoView(),
+ }));
+ menu.append(new MenuItem({
+ id: "node-menu-screenshotnode",
+ label: INSPECTOR_L10N.getStr("inspectorScreenshotNode.label"),
+ disabled: !isScreenshotable,
+ click: () => this.screenshotNode(),
+ }));
+ menu.append(new MenuItem({
+ id: "node-menu-useinconsole",
+ label: INSPECTOR_L10N.getStr("inspectorUseInConsole.label"),
+ click: () => this.useInConsole(),
+ }));
+ menu.append(new MenuItem({
+ id: "node-menu-showdomproperties",
+ label: INSPECTOR_L10N.getStr("inspectorShowDOMProperties.label"),
+ click: () => this.showDOMProperties(),
+ }));
+
+ let nodeLinkMenuItems = this._getNodeLinkMenuItems();
+ if (nodeLinkMenuItems.filter(item => item.visible).length > 0) {
+ menu.append(new MenuItem({
+ id: "node-menu-link-separator",
+ type: "separator",
+ }));
+ }
+
+ for (let menuitem of nodeLinkMenuItems) {
+ menu.append(menuitem);
+ }
+
+ menu.popup(screenX, screenY, this._toolbox);
+ return menu;
+ },
+
+ _getPasteSubmenu: function (isEditableElement) {
+ let isPasteable = isEditableElement && this._getClipboardContentForPaste();
+ let disableAdjacentPaste = !isPasteable ||
+ !this.canPasteInnerOrAdjacentHTML || this.selection.isRoot() ||
+ this.selection.isBodyNode() || this.selection.isHeadNode();
+ let disableFirstLastPaste = !isPasteable ||
+ !this.canPasteInnerOrAdjacentHTML || (this.selection.isHTMLNode() &&
+ this.selection.isRoot());
+
+ let pasteSubmenu = new Menu();
+ pasteSubmenu.append(new MenuItem({
+ id: "node-menu-pasteinnerhtml",
+ label: INSPECTOR_L10N.getStr("inspectorPasteInnerHTML.label"),
+ accesskey: INSPECTOR_L10N.getStr("inspectorPasteInnerHTML.accesskey"),
+ disabled: !isPasteable || !this.canPasteInnerOrAdjacentHTML,
+ click: () => this.pasteInnerHTML(),
+ }));
+ pasteSubmenu.append(new MenuItem({
+ id: "node-menu-pasteouterhtml",
+ label: INSPECTOR_L10N.getStr("inspectorPasteOuterHTML.label"),
+ accesskey: INSPECTOR_L10N.getStr("inspectorPasteOuterHTML.accesskey"),
+ disabled: !isPasteable || !this.isOuterHTMLEditable,
+ click: () => this.pasteOuterHTML(),
+ }));
+ pasteSubmenu.append(new MenuItem({
+ id: "node-menu-pastebefore",
+ label: INSPECTOR_L10N.getStr("inspectorHTMLPasteBefore.label"),
+ accesskey:
+ INSPECTOR_L10N.getStr("inspectorHTMLPasteBefore.accesskey"),
+ disabled: disableAdjacentPaste,
+ click: () => this.pasteAdjacentHTML("beforeBegin"),
+ }));
+ pasteSubmenu.append(new MenuItem({
+ id: "node-menu-pasteafter",
+ label: INSPECTOR_L10N.getStr("inspectorHTMLPasteAfter.label"),
+ accesskey:
+ INSPECTOR_L10N.getStr("inspectorHTMLPasteAfter.accesskey"),
+ disabled: disableAdjacentPaste,
+ click: () => this.pasteAdjacentHTML("afterEnd"),
+ }));
+ pasteSubmenu.append(new MenuItem({
+ id: "node-menu-pastefirstchild",
+ label: INSPECTOR_L10N.getStr("inspectorHTMLPasteFirstChild.label"),
+ accesskey:
+ INSPECTOR_L10N.getStr("inspectorHTMLPasteFirstChild.accesskey"),
+ disabled: disableFirstLastPaste,
+ click: () => this.pasteAdjacentHTML("afterBegin"),
+ }));
+ pasteSubmenu.append(new MenuItem({
+ id: "node-menu-pastelastchild",
+ label: INSPECTOR_L10N.getStr("inspectorHTMLPasteLastChild.label"),
+ accesskey:
+ INSPECTOR_L10N.getStr("inspectorHTMLPasteLastChild.accesskey"),
+ disabled: disableFirstLastPaste,
+ click: () => this.pasteAdjacentHTML("beforeEnd"),
+ }));
+
+ return pasteSubmenu;
+ },
+
+ _getAttributesSubmenu: function (isEditableElement) {
+ let attributesSubmenu = new Menu();
+ let nodeInfo = this.nodeMenuTriggerInfo;
+ let isAttributeClicked = isEditableElement && nodeInfo &&
+ nodeInfo.type === "attribute";
+
+ attributesSubmenu.append(new MenuItem({
+ id: "node-menu-add-attribute",
+ label: INSPECTOR_L10N.getStr("inspectorAddAttribute.label"),
+ accesskey: INSPECTOR_L10N.getStr("inspectorAddAttribute.accesskey"),
+ disabled: !isEditableElement,
+ click: () => this.onAddAttribute(),
+ }));
+ attributesSubmenu.append(new MenuItem({
+ id: "node-menu-edit-attribute",
+ label: INSPECTOR_L10N.getFormatStr("inspectorEditAttribute.label",
+ isAttributeClicked ? `"${nodeInfo.name}"` : ""),
+ accesskey: INSPECTOR_L10N.getStr("inspectorEditAttribute.accesskey"),
+ disabled: !isAttributeClicked,
+ click: () => this.onEditAttribute(),
+ }));
+
+ attributesSubmenu.append(new MenuItem({
+ id: "node-menu-remove-attribute",
+ label: INSPECTOR_L10N.getFormatStr("inspectorRemoveAttribute.label",
+ isAttributeClicked ? `"${nodeInfo.name}"` : ""),
+ accesskey:
+ INSPECTOR_L10N.getStr("inspectorRemoveAttribute.accesskey"),
+ disabled: !isAttributeClicked,
+ click: () => this.onRemoveAttribute(),
+ }));
+
+ return attributesSubmenu;
+ },
+
+ /**
+ * Link menu items can be shown or hidden depending on the context and
+ * selected node, and their labels can vary.
+ *
+ * @return {Array} list of visible menu items related to links.
+ */
+ _getNodeLinkMenuItems: function () {
+ let linkFollow = new MenuItem({
+ id: "node-menu-link-follow",
+ visible: false,
+ click: () => this.onFollowLink(),
+ });
+ let linkCopy = new MenuItem({
+ id: "node-menu-link-copy",
+ visible: false,
+ click: () => this.onCopyLink(),
+ });
+
+ // Get information about the right-clicked node.
+ let popupNode = this.contextMenuTarget;
+ if (!popupNode || !popupNode.classList.contains("link")) {
+ return [linkFollow, linkCopy];
+ }
+
+ let type = popupNode.dataset.type;
+ if (this._supportsResolveRelativeURL &&
+ (type === "uri" || type === "cssresource" || type === "jsresource")) {
+ // Links can't be opened in new tabs in the browser toolbox.
+ if (type === "uri" && !this.target.chrome) {
+ linkFollow.visible = true;
+ linkFollow.label = INSPECTOR_L10N.getStr(
+ "inspector.menu.openUrlInNewTab.label");
+ } else if (type === "cssresource") {
+ linkFollow.visible = true;
+ linkFollow.label = TOOLBOX_L10N.getStr(
+ "toolbox.viewCssSourceInStyleEditor.label");
+ } else if (type === "jsresource") {
+ linkFollow.visible = true;
+ linkFollow.label = TOOLBOX_L10N.getStr(
+ "toolbox.viewJsSourceInDebugger.label");
+ }
+
+ linkCopy.visible = true;
+ linkCopy.label = INSPECTOR_L10N.getStr(
+ "inspector.menu.copyUrlToClipboard.label");
+ } else if (type === "idref") {
+ linkFollow.visible = true;
+ linkFollow.label = INSPECTOR_L10N.getFormatStr(
+ "inspector.menu.selectElement.label", popupNode.dataset.link);
+ }
+
+ return [linkFollow, linkCopy];
+ },
+
+ _initMarkup: function () {
+ let doc = this.panelDoc;
+
+ this._markupBox = doc.getElementById("markup-box");
+
+ // create tool iframe
+ this._markupFrame = doc.createElement("iframe");
+ this._markupFrame.setAttribute("flex", "1");
+ this._markupFrame.setAttribute("tooltip", "aHTMLTooltip");
+ this._markupFrame.addEventListener("contextmenu", this._onContextMenu);
+
+ // This is needed to enable tooltips inside the iframe document.
+ this._markupFrame.addEventListener("load", this._onMarkupFrameLoad, true);
+
+ this._markupBox.setAttribute("collapsed", true);
+ this._markupBox.appendChild(this._markupFrame);
+ this._markupFrame.setAttribute("src", "chrome://devtools/content/inspector/markup/markup.xhtml");
+ this._markupFrame.setAttribute("aria-label",
+ INSPECTOR_L10N.getStr("inspector.panelLabel.markupView"));
+ },
+
+ _onMarkupFrameLoad: function () {
+ this._markupFrame.removeEventListener("load", this._onMarkupFrameLoad, true);
+
+ this._markupFrame.contentWindow.focus();
+
+ this._markupBox.removeAttribute("collapsed");
+
+ this.markup = new MarkupView(this, this._markupFrame, this._toolbox.win);
+
+ this.emit("markuploaded");
+ },
+
+ _destroyMarkup: function () {
+ let destroyPromise;
+
+ if (this._markupFrame) {
+ this._markupFrame.removeEventListener("load", this._onMarkupFrameLoad, true);
+ this._markupFrame.removeEventListener("contextmenu", this._onContextMenu);
+ }
+
+ if (this.markup) {
+ destroyPromise = this.markup.destroy();
+ this.markup = null;
+ } else {
+ destroyPromise = promise.resolve();
+ }
+
+ if (this._markupFrame) {
+ this._markupFrame.parentNode.removeChild(this._markupFrame);
+ this._markupFrame = null;
+ }
+
+ this._markupBox = null;
+
+ return destroyPromise;
+ },
+
+ /**
+ * When the pane toggle button is clicked or pressed, toggle the pane, change the button
+ * state and tooltip.
+ */
+ onPaneToggleButtonClicked: function (e) {
+ let sidePaneContainer = this.panelDoc.querySelector(
+ "#inspector-splitter-box .controlled");
+ let isVisible = !this._sidebarToggle.state.collapsed;
+
+ // Make sure the sidebar has width and height attributes before collapsing
+ // because ViewHelpers needs it.
+ if (isVisible) {
+ let rect = sidePaneContainer.getBoundingClientRect();
+ if (!sidePaneContainer.hasAttribute("width")) {
+ sidePaneContainer.setAttribute("width", rect.width);
+ }
+ // always refresh the height attribute before collapsing, it could have
+ // been modified by resizing the container.
+ sidePaneContainer.setAttribute("height", rect.height);
+ }
+
+ let onAnimationDone = () => {
+ if (isVisible) {
+ this._sidebarToggle.setState({collapsed: true});
+ } else {
+ this._sidebarToggle.setState({collapsed: false});
+ }
+ };
+
+ ViewHelpers.togglePane({
+ visible: !isVisible,
+ animated: true,
+ delayed: true,
+ callback: onAnimationDone
+ }, sidePaneContainer);
+ },
+
+ onEyeDropperButtonClicked: function () {
+ this.eyeDropperButton.hasAttribute("checked")
+ ? this.hideEyeDropper()
+ : this.showEyeDropper();
+ },
+
+ startEyeDropperListeners: function () {
+ this.inspector.once("color-pick-canceled", this.onEyeDropperDone);
+ this.inspector.once("color-picked", this.onEyeDropperDone);
+ this.walker.once("new-root", this.onEyeDropperDone);
+ },
+
+ stopEyeDropperListeners: function () {
+ this.inspector.off("color-pick-canceled", this.onEyeDropperDone);
+ this.inspector.off("color-picked", this.onEyeDropperDone);
+ this.walker.off("new-root", this.onEyeDropperDone);
+ },
+
+ onEyeDropperDone: function () {
+ this.eyeDropperButton.removeAttribute("checked");
+ this.stopEyeDropperListeners();
+ },
+
+ /**
+ * Show the eyedropper on the page.
+ * @return {Promise} resolves when the eyedropper is visible.
+ */
+ showEyeDropper: function () {
+ // The eyedropper button doesn't exist, most probably because the actor doesn't
+ // support the pickColorFromPage, or because the page isn't HTML.
+ if (!this.eyeDropperButton) {
+ return null;
+ }
+
+ this.telemetry.toolOpened("toolbareyedropper");
+ this.eyeDropperButton.setAttribute("checked", "true");
+ this.startEyeDropperListeners();
+ return this.inspector.pickColorFromPage(this.toolbox, {copyOnSelect: true})
+ .catch(e => console.error(e));
+ },
+
+ /**
+ * Hide the eyedropper.
+ * @return {Promise} resolves when the eyedropper is hidden.
+ */
+ hideEyeDropper: function () {
+ // The eyedropper button doesn't exist, most probably because the actor doesn't
+ // support the pickColorFromPage, or because the page isn't HTML.
+ if (!this.eyeDropperButton) {
+ return null;
+ }
+
+ this.eyeDropperButton.removeAttribute("checked");
+ this.stopEyeDropperListeners();
+ return this.inspector.cancelPickColorFromPage()
+ .catch(e => console.error(e));
+ },
+
+ /**
+ * Create a new node as the last child of the current selection, expand the
+ * parent and select the new node.
+ */
+ addNode: Task.async(function* () {
+ if (!this.canAddHTMLChild()) {
+ return;
+ }
+
+ let html = "<div></div>";
+
+ // Insert the html and expect a childList markup mutation.
+ let onMutations = this.once("markupmutation");
+ let {nodes} = yield this.walker.insertAdjacentHTML(this.selection.nodeFront,
+ "beforeEnd", html);
+ yield onMutations;
+
+ // Select the new node (this will auto-expand its parent).
+ this.selection.setNodeFront(nodes[0], "node-inserted");
+ }),
+
+ /**
+ * Toggle a pseudo class.
+ */
+ togglePseudoClass: function (pseudo) {
+ if (this.selection.isElementNode()) {
+ let node = this.selection.nodeFront;
+ if (node.hasPseudoClassLock(pseudo)) {
+ return this.walker.removePseudoClassLock(node, pseudo, {parents: true});
+ }
+
+ let hierarchical = pseudo == ":hover" || pseudo == ":active";
+ return this.walker.addPseudoClassLock(node, pseudo, {parents: hierarchical});
+ }
+ return promise.resolve();
+ },
+
+ /**
+ * Show DOM properties
+ */
+ showDOMProperties: function () {
+ this._toolbox.openSplitConsole().then(() => {
+ let panel = this._toolbox.getPanel("webconsole");
+ let jsterm = panel.hud.jsterm;
+
+ jsterm.execute("inspect($0)");
+ jsterm.focus();
+ });
+ },
+
+ /**
+ * Use in Console.
+ *
+ * Takes the currently selected node in the inspector and assigns it to a
+ * temp variable on the content window. Also opens the split console and
+ * autofills it with the temp variable.
+ */
+ useInConsole: function () {
+ this._toolbox.openSplitConsole().then(() => {
+ let panel = this._toolbox.getPanel("webconsole");
+ let jsterm = panel.hud.jsterm;
+
+ let evalString = `{ let i = 0;
+ while (window.hasOwnProperty("temp" + i) && i < 1000) {
+ i++;
+ }
+ window["temp" + i] = $0;
+ "temp" + i;
+ }`;
+
+ let options = {
+ selectedNodeActor: this.selection.nodeFront.actorID,
+ };
+ jsterm.requestEvaluation(evalString, options).then((res) => {
+ jsterm.setInputValue(res.result);
+ this.emit("console-var-ready");
+ });
+ });
+ },
+
+ /**
+ * Edit the outerHTML of the selected Node.
+ */
+ editHTML: function () {
+ if (!this.selection.isNode()) {
+ return;
+ }
+ if (this.markup) {
+ this.markup.beginEditingOuterHTML(this.selection.nodeFront);
+ }
+ },
+
+ /**
+ * Paste the contents of the clipboard into the selected Node's outer HTML.
+ */
+ pasteOuterHTML: function () {
+ let content = this._getClipboardContentForPaste();
+ if (!content) {
+ return promise.reject("No clipboard content for paste");
+ }
+
+ let node = this.selection.nodeFront;
+ return this.markup.getNodeOuterHTML(node).then(oldContent => {
+ this.markup.updateNodeOuterHTML(node, content, oldContent);
+ });
+ },
+
+ /**
+ * Paste the contents of the clipboard into the selected Node's inner HTML.
+ */
+ pasteInnerHTML: function () {
+ let content = this._getClipboardContentForPaste();
+ if (!content) {
+ return promise.reject("No clipboard content for paste");
+ }
+
+ let node = this.selection.nodeFront;
+ return this.markup.getNodeInnerHTML(node).then(oldContent => {
+ this.markup.updateNodeInnerHTML(node, content, oldContent);
+ });
+ },
+
+ /**
+ * Paste the contents of the clipboard as adjacent HTML to the selected Node.
+ * @param position
+ * The position as specified for Element.insertAdjacentHTML
+ * (i.e. "beforeBegin", "afterBegin", "beforeEnd", "afterEnd").
+ */
+ pasteAdjacentHTML: function (position) {
+ let content = this._getClipboardContentForPaste();
+ if (!content) {
+ return promise.reject("No clipboard content for paste");
+ }
+
+ let node = this.selection.nodeFront;
+ return this.markup.insertAdjacentHTMLToNode(node, position, content);
+ },
+
+ /**
+ * Copy the innerHTML of the selected Node to the clipboard.
+ */
+ copyInnerHTML: function () {
+ if (!this.selection.isNode()) {
+ return;
+ }
+ this._copyLongString(this.walker.innerHTML(this.selection.nodeFront));
+ },
+
+ /**
+ * Copy the outerHTML of the selected Node to the clipboard.
+ */
+ copyOuterHTML: function () {
+ if (!this.selection.isNode()) {
+ return;
+ }
+ let node = this.selection.nodeFront;
+
+ switch (node.nodeType) {
+ case nodeConstants.ELEMENT_NODE :
+ this._copyLongString(this.walker.outerHTML(node));
+ break;
+ case nodeConstants.COMMENT_NODE :
+ this._getLongString(node.getNodeValue()).then(comment => {
+ clipboardHelper.copyString("<!--" + comment + "-->");
+ });
+ break;
+ case nodeConstants.DOCUMENT_TYPE_NODE :
+ clipboardHelper.copyString(node.doctypeString);
+ break;
+ }
+ },
+
+ /**
+ * Copy the data-uri for the currently selected image in the clipboard.
+ */
+ copyImageDataUri: function () {
+ let container = this.markup.getContainer(this.selection.nodeFront);
+ if (container && container.isPreviewable()) {
+ container.copyImageDataUri();
+ }
+ },
+
+ /**
+ * Copy the content of a longString (via a promise resolving a
+ * LongStringActor) to the clipboard
+ * @param {Promise} longStringActorPromise
+ * promise expected to resolve a LongStringActor instance
+ * @return {Promise} promise resolving (with no argument) when the
+ * string is sent to the clipboard
+ */
+ _copyLongString: function (longStringActorPromise) {
+ return this._getLongString(longStringActorPromise).then(string => {
+ clipboardHelper.copyString(string);
+ }).catch(e => console.error(e));
+ },
+
+ /**
+ * Retrieve the content of a longString (via a promise resolving a LongStringActor)
+ * @param {Promise} longStringActorPromise
+ * promise expected to resolve a LongStringActor instance
+ * @return {Promise} promise resolving with the retrieved string as argument
+ */
+ _getLongString: function (longStringActorPromise) {
+ return longStringActorPromise.then(longStringActor => {
+ return longStringActor.string().then(string => {
+ longStringActor.release().catch(e => console.error(e));
+ return string;
+ });
+ }).catch(e => console.error(e));
+ },
+
+ /**
+ * Copy a unique selector of the selected Node to the clipboard.
+ */
+ copyUniqueSelector: function () {
+ if (!this.selection.isNode()) {
+ return;
+ }
+
+ this.selection.nodeFront.getUniqueSelector().then((selector) => {
+ clipboardHelper.copyString(selector);
+ }).then(null, console.error);
+ },
+
+ /**
+ * Initiate gcli screenshot command on selected node
+ */
+ screenshotNode: function () {
+ CommandUtils.createRequisition(this._target, {
+ environment: CommandUtils.createEnvironment(this, "_target")
+ }).then(requisition => {
+ // Bug 1180314 - CssSelector might contain white space so need to make sure it is
+ // passed to screenshot as a single parameter. More work *might* be needed if
+ // CssSelector could contain escaped single- or double-quotes, backslashes, etc.
+ requisition.updateExec("screenshot --selector '" + this.selectionCssSelector + "'");
+ });
+ },
+
+ /**
+ * Scroll the node into view.
+ */
+ scrollNodeIntoView: function () {
+ if (!this.selection.isNode()) {
+ return;
+ }
+
+ this.selection.nodeFront.scrollIntoView();
+ },
+
+ /**
+ * Duplicate the selected node
+ */
+ duplicateNode: function () {
+ let selection = this.selection;
+ if (!selection.isElementNode() ||
+ selection.isRoot() ||
+ selection.isAnonymousNode() ||
+ selection.isPseudoElementNode()) {
+ return;
+ }
+ this.walker.duplicateNode(selection.nodeFront).catch(e => console.error(e));
+ },
+
+ /**
+ * Delete the selected node.
+ */
+ deleteNode: function () {
+ if (!this.selection.isNode() ||
+ this.selection.isRoot()) {
+ return;
+ }
+
+ // If the markup panel is active, use the markup panel to delete
+ // the node, making this an undoable action.
+ if (this.markup) {
+ this.markup.deleteNode(this.selection.nodeFront);
+ } else {
+ // remove the node from content
+ this.walker.removeNode(this.selection.nodeFront);
+ }
+ },
+
+ /**
+ * Add attribute to node.
+ * Used for node context menu and shouldn't be called directly.
+ */
+ onAddAttribute: function () {
+ let container = this.markup.getContainer(this.selection.nodeFront);
+ container.addAttribute();
+ },
+
+ /**
+ * Edit attribute for node.
+ * Used for node context menu and shouldn't be called directly.
+ */
+ onEditAttribute: function () {
+ let container = this.markup.getContainer(this.selection.nodeFront);
+ container.editAttribute(this.nodeMenuTriggerInfo.name);
+ },
+
+ /**
+ * Remove attribute from node.
+ * Used for node context menu and shouldn't be called directly.
+ */
+ onRemoveAttribute: function () {
+ let container = this.markup.getContainer(this.selection.nodeFront);
+ container.removeAttribute(this.nodeMenuTriggerInfo.name);
+ },
+
+ expandNode: function () {
+ this.markup.expandAll(this.selection.nodeFront);
+ },
+
+ collapseNode: function () {
+ this.markup.collapseNode(this.selection.nodeFront);
+ },
+
+ /**
+ * This method is here for the benefit of the node-menu-link-follow menu item
+ * in the inspector contextual-menu.
+ */
+ onFollowLink: function () {
+ let type = this.contextMenuTarget.dataset.type;
+ let link = this.contextMenuTarget.dataset.link;
+
+ this.followAttributeLink(type, link);
+ },
+
+ /**
+ * Given a type and link found in a node's attribute in the markup-view,
+ * attempt to follow that link (which may result in opening a new tab, the
+ * style editor or debugger).
+ */
+ followAttributeLink: function (type, link) {
+ if (!type || !link) {
+ return;
+ }
+
+ if (type === "uri" || type === "cssresource" || type === "jsresource") {
+ // Open link in a new tab.
+ // When the inspector menu was setup on click (see _getNodeLinkMenuItems), we
+ // already checked that resolveRelativeURL existed.
+ this.inspector.resolveRelativeURL(
+ link, this.selection.nodeFront).then(url => {
+ if (type === "uri") {
+ let browserWin = this.target.tab.ownerDocument.defaultView;
+ browserWin.openUILinkIn(url, "tab");
+ } else if (type === "cssresource") {
+ return this.toolbox.viewSourceInStyleEditor(url);
+ } else if (type === "jsresource") {
+ return this.toolbox.viewSourceInDebugger(url);
+ }
+ return null;
+ }).catch(e => console.error(e));
+ } else if (type == "idref") {
+ // Select the node in the same document.
+ this.walker.document(this.selection.nodeFront).then(doc => {
+ return this.walker.querySelector(doc, "#" + CSS.escape(link)).then(node => {
+ if (!node) {
+ this.emit("idref-attribute-link-failed");
+ return;
+ }
+ this.selection.setNodeFront(node);
+ });
+ }).catch(e => console.error(e));
+ }
+ },
+
+ /**
+ * This method is here for the benefit of the node-menu-link-copy menu item
+ * in the inspector contextual-menu.
+ */
+ onCopyLink: function () {
+ let link = this.contextMenuTarget.dataset.link;
+
+ this.copyAttributeLink(link);
+ },
+
+ /**
+ * This method is here for the benefit of copying links.
+ */
+ copyAttributeLink: function (link) {
+ // When the inspector menu was setup on click (see _getNodeLinkMenuItems), we
+ // already checked that resolveRelativeURL existed.
+ this.inspector.resolveRelativeURL(link, this.selection.nodeFront).then(url => {
+ clipboardHelper.copyString(url);
+ }, console.error);
+ }
+};
+
+// URL constructor doesn't support chrome: scheme
+let href = window.location.href.replace(/chrome:/, "http://");
+let url = new window.URL(href);
+
+// Only use this method to attach the toolbox if some query parameters are given
+if (url.search.length > 1) {
+ const { targetFromURL } = require("devtools/client/framework/target-from-url");
+ const { attachThread } = require("devtools/client/framework/attach-thread");
+ const { BrowserLoader } =
+ Cu.import("resource://devtools/client/shared/browser-loader.js", {});
+
+ const { Selection } = require("devtools/client/framework/selection");
+ const { InspectorFront } = require("devtools/shared/fronts/inspector");
+ const { getHighlighterUtils } = require("devtools/client/framework/toolbox-highlighter-utils");
+
+ Task.spawn(function* () {
+ let target = yield targetFromURL(url);
+
+ let notImplemented = function () {
+ throw new Error("Not implemented in a tab");
+ };
+ let fakeToolbox = {
+ target,
+ hostType: "bottom",
+ doc: window.document,
+ win: window,
+ on() {}, emit() {}, off() {},
+ initInspector() {},
+ browserRequire: BrowserLoader({
+ window: window,
+ useOnlyShared: true
+ }).require,
+ get React() {
+ return this.browserRequire("devtools/client/shared/vendor/react");
+ },
+ get ReactDOM() {
+ return this.browserRequire("devtools/client/shared/vendor/react-dom");
+ },
+ isToolRegistered() {
+ return false;
+ },
+ currentToolId: "inspector",
+ getCurrentPanel() {
+ return "inspector";
+ },
+ get textboxContextMenuPopup() {
+ notImplemented();
+ },
+ getPanel: notImplemented,
+ openSplitConsole: notImplemented,
+ viewCssSourceInStyleEditor: notImplemented,
+ viewJsSourceInDebugger: notImplemented,
+ viewSource: notImplemented,
+ viewSourceInDebugger: notImplemented,
+ viewSourceInStyleEditor: notImplemented,
+
+ // For attachThread:
+ highlightTool() {},
+ unhighlightTool() {},
+ selectTool() {},
+ raise() {},
+ getNotificationBox() {}
+ };
+
+ // attachThread also expect a toolbox as argument
+ fakeToolbox.threadClient = yield attachThread(fakeToolbox);
+
+ let inspector = InspectorFront(target.client, target.form);
+ let showAllAnonymousContent =
+ Services.prefs.getBoolPref("devtools.inspector.showAllAnonymousContent");
+ let walker = yield inspector.getWalker({ showAllAnonymousContent });
+ let selection = new Selection(walker);
+ let highlighter = yield inspector.getHighlighter(false);
+
+ fakeToolbox.inspector = inspector;
+ fakeToolbox.walker = walker;
+ fakeToolbox.selection = selection;
+ fakeToolbox.highlighter = highlighter;
+ fakeToolbox.highlighterUtils = getHighlighterUtils(fakeToolbox);
+
+ let inspectorUI = new Inspector(fakeToolbox);
+ inspectorUI.init();
+ }).then(null, e => {
+ window.alert("Unable to start the inspector:" + e.message + "\n" + e.stack);
+ });
+}
diff --git a/devtools/client/inspector/inspector.xhtml b/devtools/client/inspector/inspector.xhtml
new file mode 100644
index 000000000..f43f6bb8c
--- /dev/null
+++ b/devtools/client/inspector/inspector.xhtml
@@ -0,0 +1,231 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+<!DOCTYPE html>
+
+<html xmlns="http://www.w3.org/1999/xhtml" dir="">
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+
+ <link rel="stylesheet" href="chrome://devtools/skin/widgets.css"/>
+ <link rel="stylesheet" href="chrome://devtools/skin/inspector.css"/>
+ <link rel="stylesheet" href="chrome://devtools/skin/rules.css"/>
+ <link rel="stylesheet" href="chrome://devtools/skin/computed.css"/>
+ <link rel="stylesheet" href="chrome://devtools/skin/fonts.css"/>
+ <link rel="stylesheet" href="chrome://devtools/skin/boxmodel.css"/>
+ <link rel="stylesheet" href="chrome://devtools/skin/layout.css"/>
+ <link rel="stylesheet" href="chrome://devtools/skin/animationinspector.css"/>
+ <link rel="stylesheet" href="resource://devtools/client/shared/components/sidebar-toggle.css"/>
+ <link rel="stylesheet" href="resource://devtools/client/shared/components/tabs/tabs.css"/>
+ <link rel="stylesheet" href="resource://devtools/client/shared/components/tabs/tabbar.css"/>
+ <link rel="stylesheet" href="resource://devtools/client/inspector/components/inspector-tab-panel.css"/>
+ <link rel="stylesheet" href="resource://devtools/client/shared/components/splitter/split-box.css"/>
+ <link rel="stylesheet" href="resource://devtools/client/inspector/layout/components/Accordion.css"/>
+
+ <script type="application/javascript;version=1.8"
+ src="chrome://devtools/content/shared/theme-switching.js"></script>
+ <script type="application/javascript;version=1.8" src="inspector.js" defer="true"></script>
+</head>
+<body class="theme-body" role="application">
+ <div class="inspector-responsive-container theme-body inspector">
+
+ <!-- Main Panel Content -->
+ <div id="inspector-main-content" class="devtools-main-content">
+ <div id="inspector-toolbar" class="devtools-toolbar" nowindowdrag="true"
+ data-localization-bundle="devtools/client/locales/inspector.properties">
+ <button id="inspector-element-add-button" class="devtools-button"
+ data-localization="title=inspectorAddNode.label"></button>
+ <div class="devtools-toolbar-spacer"></div>
+ <span id="inspector-searchlabel"></span>
+ <div id="inspector-search" class="devtools-searchbox has-clear-btn">
+ <input id="inspector-searchbox" class="devtools-searchinput"
+ type="search"
+ data-localization="placeholder=inspectorSearchHTML.label3"/>
+ <button id="inspector-searchinput-clear" class="devtools-searchinput-clear" tabindex="-1"></button>
+ </div>
+ <button id="inspector-eyedropper-toggle"
+ class="devtools-button command-button-invertable"></button>
+ <div id="inspector-sidebar-toggle-box"></div>
+ </div>
+ <div id="markup-box"></div>
+ <div id="inspector-breadcrumbs-toolbar" class="devtools-toolbar">
+ <div id="inspector-breadcrumbs" class="breadcrumbs-widget-container"
+ role="group" data-localization="aria-label=inspector.breadcrumbs.label" tabindex="0"></div>
+ </div>
+ </div>
+
+ <!-- Splitter -->
+ <div
+ xmlns="http://www.w3.org/1999/xhtml"
+ id="inspector-splitter-box">
+ </div>
+
+ <!-- Sidebar Container -->
+ <div id="inspector-sidebar-container">
+ <div
+ xmlns="http://www.w3.org/1999/xhtml"
+ id="inspector-sidebar"
+ hidden="true"></div>
+ </div>
+
+ <!-- Sidebar panel definitions -->
+ <div id="tabpanels" style="visibility:collapse">
+ <div id="sidebar-panel-ruleview" class="devtools-monospace theme-sidebar inspector-tabpanel"
+ data-localization-bundle="devtools/client/locales/inspector.properties">
+ <div id="ruleview-toolbar-container" class="devtools-toolbar">
+ <div id="ruleview-toolbar">
+ <div class="devtools-searchbox has-clear-btn">
+ <input id="ruleview-searchbox"
+ class="devtools-filterinput devtools-rule-searchbox"
+ type="search"
+ data-localization="placeholder=inspector.filterStyles.placeholder"/>
+ <button id="ruleview-searchinput-clear" class="devtools-searchinput-clear"></button>
+ </div>
+ <div id="ruleview-command-toolbar">
+ <button id="ruleview-add-rule-button" data-localization="title=inspector.addRule.tooltip" class="devtools-button"></button>
+ <button id="pseudo-class-panel-toggle" data-localization="title=inspector.togglePseudo.tooltip" class="devtools-button"></button>
+ </div>
+ </div>
+ <div id="pseudo-class-panel" hidden="true">
+ <label><input id="pseudo-hover-toggle" type="checkbox" value=":hover" tabindex="-1" />:hover</label>
+ <label><input id="pseudo-active-toggle" type="checkbox" value=":active" tabindex="-1" />:active</label>
+ <label><input id="pseudo-focus-toggle" type="checkbox" value=":focus" tabindex="-1" />:focus</label>
+ </div>
+ </div>
+
+ <div id="ruleview-container" class="ruleview">
+ <div id="ruleview-container-focusable" tabindex="-1">
+ </div>
+ </div>
+ </div>
+
+ <div id="sidebar-panel-computedview" class="devtools-monospace theme-sidebar inspector-tabpanel"
+ data-localization-bundle="devtools/client/locales/inspector.properties">
+ <div id="computedview-toolbar" class="devtools-toolbar">
+ <div class="devtools-searchbox has-clear-btn">
+ <input id="computedview-searchbox"
+ class="devtools-filterinput devtools-rule-searchbox"
+ type="search"
+ data-localization="placeholder=inspector.filterStyles.placeholder"/>
+ <button id="computedview-searchinput-clear" class="devtools-searchinput-clear"></button>
+ </div>
+ <input id="browser-style-checkbox"
+ type="checkbox"
+ class="includebrowserstyles"/>
+ <label id="browser-style-checkbox-label" for="browser-style-checkbox"
+ data-localization="content=inspector.browserStyles.label"></label>
+ </div>
+
+ <div id="computedview-container">
+ <div id="computedview-container-focusable" tabindex="-1">
+ <div id="boxmodel-wrapper" tabindex="0"
+ data-localization-bundle="devtools/client/locales/boxmodel.properties">
+ <div id="boxmodel-header">
+ <div id="boxmodel-expander" class="expander theme-twisty expandable" open=""></div>
+ <span data-localization="content=boxmodel.title"></span>
+ </div>
+
+ <div id="boxmodel-container">
+ <div id="boxmodel-main">
+ <span class="boxmodel-legend" data-box="margin" data-localization="content=boxmodel.margin;title=boxmodel.margin"></span>
+ <div id="boxmodel-margins" data-box="margin" data-localization="title=boxmodel.margin">
+ <span class="boxmodel-legend" data-box="border" data-localization="content=boxmodel.border;title=boxmodel.border"></span>
+ <div id="boxmodel-borders" data-box="border" data-localization="title=boxmodel.border">
+ <span class="boxmodel-legend" data-box="padding" data-localization="content=boxmodel.padding;title=boxmodel.padding"></span>
+ <div id="boxmodel-padding" data-box="padding" data-localization="title=boxmodel.padding">
+ <div id="boxmodel-content" data-box="content" data-localization="title=boxmodel.content">
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <p class="boxmodel-margin boxmodel-top"><span data-box="margin" class="boxmodel-editable" title="margin-top"></span></p>
+ <p class="boxmodel-margin boxmodel-right"><span data-box="margin" class="boxmodel-editable" title="margin-right"></span></p>
+ <p class="boxmodel-margin boxmodel-bottom"><span data-box="margin" class="boxmodel-editable" title="margin-bottom"></span></p>
+ <p class="boxmodel-margin boxmodel-left"><span data-box="margin" class="boxmodel-editable" title="margin-left"></span></p>
+
+ <p class="boxmodel-border boxmodel-top"><span data-box="border" class="boxmodel-editable" title="border-top"></span></p>
+ <p class="boxmodel-border boxmodel-right"><span data-box="border" class="boxmodel-editable" title="border-right"></span></p>
+ <p class="boxmodel-border boxmodel-bottom"><span data-box="border" class="boxmodel-editable" title="border-bottom"></span></p>
+ <p class="boxmodel-border boxmodel-left"><span data-box="border" class="boxmodel-editable" title="border-left"></span></p>
+
+ <p class="boxmodel-padding boxmodel-top"><span data-box="padding" class="boxmodel-editable" title="padding-top"></span></p>
+ <p class="boxmodel-padding boxmodel-right"><span data-box="padding" class="boxmodel-editable" title="padding-right"></span></p>
+ <p class="boxmodel-padding boxmodel-bottom"><span data-box="padding" class="boxmodel-editable" title="padding-bottom"></span></p>
+ <p class="boxmodel-padding boxmodel-left"><span data-box="padding" class="boxmodel-editable" title="padding-left"></span></p>
+
+ <p class="boxmodel-size">
+ <span data-box="content" data-localization="title=boxmodel.content"></span>
+ </p>
+ </div>
+
+ <div id="boxmodel-info">
+ <span id="boxmodel-element-size"></span>
+ <section id="boxmodel-position-group">
+ <button class="devtools-button" id="layout-geometry-editor"
+ data-localization="title=boxmodel.geometryButton.tooltip"></button>
+ <span id="boxmodel-element-position"></span>
+ </section>
+ </div>
+
+ <div style="display: none">
+ <p id="boxmodel-dummy"></p>
+ </div>
+ </div>
+ </div>
+
+ <div id="propertyContainer" class="theme-separator" tabindex="0">
+ </div>
+
+ <div id="computedview-no-results" hidden="" data-localization="content=inspector.noProperties"></div>
+ </div>
+ </div>
+ </div>
+
+ <div id="sidebar-panel-fontinspector" class="devtools-monospace theme-sidebar inspector-tabpanel"
+ data-localization-bundle="devtools/client/locales/font-inspector.properties">
+ <div class="devtools-toolbar">
+ <div class="devtools-searchbox">
+ <input id="font-preview-text-input" class="devtools-textinput" type="search"
+ data-localization="placeholder=fontinspector.previewText"/>
+ </div>
+ <label id="font-showall" class="theme-link"
+ data-localization="content=fontinspector.seeAll;
+ title=fontinspector.seeAll.tooltip"></label>
+ </div>
+
+ <div id="font-container">
+ <ul id="all-fonts"></ul>
+ </div>
+
+ <div id="font-template">
+ <section class="font">
+ <div class="font-preview-container">
+ <img class="font-preview"></img>
+ </div>
+ <div class="font-info">
+ <h1 class="font-name"></h1>
+ <span class="font-is-local" data-localization="content=fontinspector.system"></span>
+ <span class="font-is-remote" data-localization="content=fontinspector.remote"></span>
+ <p class="font-format-url">
+ <input readonly="readonly" class="font-url"></input>
+ <span class="font-format"></span>
+ </p>
+ <p class="font-css">
+ <span data-localization="content=fontinspector.usedAs"></span> "<span class="font-css-name"></span>"
+ </p>
+ <pre class="font-css-code"></pre>
+ </div>
+ </section>
+ </div>
+ </div>
+
+ <div id="sidebar-panel-animationinspector" class="devtools-monospace theme-sidebar inspector-tabpanel">
+ <iframe class="devtools-inspector-tab-frame"></iframe>
+ </div>
+ </div>
+
+ </div>
+</body>
+</html>
diff --git a/devtools/client/inspector/layout/actions/index.js b/devtools/client/inspector/layout/actions/index.js
new file mode 100644
index 000000000..66735cddb
--- /dev/null
+++ b/devtools/client/inspector/layout/actions/index.js
@@ -0,0 +1,5 @@
+/* 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";
diff --git a/devtools/client/inspector/layout/actions/moz.build b/devtools/client/inspector/layout/actions/moz.build
new file mode 100644
index 000000000..568f361a5
--- /dev/null
+++ b/devtools/client/inspector/layout/actions/moz.build
@@ -0,0 +1,5 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
diff --git a/devtools/client/inspector/layout/components/Accordion.css b/devtools/client/inspector/layout/components/Accordion.css
new file mode 100644
index 000000000..4076d30fa
--- /dev/null
+++ b/devtools/client/inspector/layout/components/Accordion.css
@@ -0,0 +1,42 @@
+/* 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/. */
+
+/**
+ * This file should not be modified and is a duplicate from the debugger.html project.
+ * Any changes to this file should be imported from the upstream debugger.html project.
+ */
+
+.accordion {
+ background-color: var(--theme-body-background);
+ width: 100%;
+}
+
+.accordion ._header {
+ background-color: var(--theme-toolbar-background);
+ border-bottom: 1px solid var(--theme-splitter-color);
+ cursor: pointer;
+ font-size: 11px;
+ padding: 5px;
+ transition: all 0.25s ease;
+ width: 100%;
+ -moz-user-select: none;
+}
+
+.accordion ._header:hover {
+ background-color: var(--theme-selection-color);
+}
+
+.accordion ._header:hover svg {
+ fill: var(--theme-comment-alt);
+}
+
+.accordion ._content {
+ border-bottom: 1px solid var(--theme-splitter-color);
+ font-size: 11px;
+}
+
+.arrow {
+ vertical-align: middle;
+ display: inline-block;
+}
diff --git a/devtools/client/inspector/layout/components/Accordion.js b/devtools/client/inspector/layout/components/Accordion.js
new file mode 100644
index 000000000..d69dc3c7e
--- /dev/null
+++ b/devtools/client/inspector/layout/components/Accordion.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/. */
+
+/**
+ * This file should not be modified and is a duplicate from the debugger.html project.
+ * Any changes to this file should be imported from the upstream debugger.html project.
+ */
+
+"use strict";
+
+const React = require("devtools/client/shared/vendor/react");
+const { DOM: dom, PropTypes } = React;
+
+const { div, span } = dom;
+
+const Accordion = React.createClass({
+ displayName: "Accordion",
+
+ propTypes: {
+ items: PropTypes.array
+ },
+
+ getInitialState: function () {
+ return { opened: this.props.items.map(item => item.opened),
+ created: [] };
+ },
+
+ handleHeaderClick: function (i) {
+ const opened = [...this.state.opened];
+ const created = [...this.state.created];
+ const item = this.props.items[i];
+
+ opened[i] = !opened[i];
+ created[i] = true;
+
+ if (opened[i] && item.onOpened) {
+ item.onOpened();
+ }
+
+ this.setState({ opened, created });
+ },
+
+ renderContainer: function (item, i) {
+ const { opened, created } = this.state;
+ const containerClassName =
+ item.header.toLowerCase().replace(/\s/g, "-") + "-pane";
+ let arrowClassName = "arrow theme-twisty";
+ if (opened[i]) {
+ arrowClassName += " open";
+ }
+
+ return div(
+ { className: containerClassName, key: i },
+
+ div(
+ { className: "_header",
+ onClick: () => this.handleHeaderClick(i) },
+ span({ className: arrowClassName }),
+ item.header
+ ),
+
+ (created[i] || opened[i]) ?
+ div(
+ { className: "_content",
+ style: { display: opened[i] ? "block" : "none" }
+ },
+ React.createElement(item.component, item.componentProps || {})
+ ) :
+ null
+ );
+ },
+
+ render: function () {
+ return div(
+ { className: "accordion" },
+ this.props.items.map(this.renderContainer)
+ );
+ }
+});
+
+module.exports = Accordion;
diff --git a/devtools/client/inspector/layout/components/App.js b/devtools/client/inspector/layout/components/App.js
new file mode 100644
index 000000000..887175b99
--- /dev/null
+++ b/devtools/client/inspector/layout/components/App.js
@@ -0,0 +1,35 @@
+/* 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 { getStr } = require("../utils/l10n");
+const { DOM: dom, createClass, createFactory } = require("devtools/client/shared/vendor/react");
+const { connect } = require("devtools/client/shared/vendor/react-redux");
+
+const Accordion = createFactory(require("./Accordion"));
+const Grid = createFactory(require("./Grid"));
+
+const App = createClass({
+
+ displayName: "App",
+
+ render() {
+ return dom.div(
+ {
+ id: "layoutview-container",
+ },
+ Accordion({
+ items: [
+ { header: getStr("layout.header"),
+ component: Grid,
+ opened: true }
+ ]
+ })
+ );
+ },
+
+});
+
+module.exports = connect(state => state)(App);
diff --git a/devtools/client/inspector/layout/components/Grid.js b/devtools/client/inspector/layout/components/Grid.js
new file mode 100644
index 000000000..8081ce9ef
--- /dev/null
+++ b/devtools/client/inspector/layout/components/Grid.js
@@ -0,0 +1,30 @@
+/* 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 { getStr } = require("../utils/l10n");
+const { DOM: dom, createClass } = require("devtools/client/shared/vendor/react");
+
+const Grid = createClass({
+
+ displayName: "Grid",
+
+ render() {
+ return dom.div(
+ {
+ id: "layoutview-grid-container",
+ },
+ dom.div(
+ {
+ className: "layoutview-no-grids"
+ },
+ getStr("layout.noGrids")
+ )
+ );
+ },
+
+});
+
+module.exports = Grid;
diff --git a/devtools/client/inspector/layout/components/moz.build b/devtools/client/inspector/layout/components/moz.build
new file mode 100644
index 000000000..0ae19f4f6
--- /dev/null
+++ b/devtools/client/inspector/layout/components/moz.build
@@ -0,0 +1,12 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'Accordion.css',
+ 'Accordion.js',
+ 'App.js',
+ 'Grid.js',
+)
diff --git a/devtools/client/inspector/layout/layout.js b/devtools/client/inspector/layout/layout.js
new file mode 100644
index 000000000..cf0955b27
--- /dev/null
+++ b/devtools/client/inspector/layout/layout.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/. */
+
+"use strict";
+
+const Services = require("Services");
+const { createFactory, createElement } = require("devtools/client/shared/vendor/react");
+const { Provider } = require("devtools/client/shared/vendor/react-redux");
+
+const App = createFactory(require("./components/App"));
+const Store = require("./store");
+
+const { LocalizationHelper } = require("devtools/shared/l10n");
+const INSPECTOR_L10N =
+ new LocalizationHelper("devtools/client/locales/inspector.properties");
+
+function LayoutView(inspector, window) {
+ this.inspector = inspector;
+ this.document = window.document;
+ this.store = null;
+
+ this.init();
+}
+
+LayoutView.prototype = {
+
+ init() {
+ let store = this.store = Store();
+ let provider = createElement(Provider, {
+ store,
+ id: "layoutview",
+ title: INSPECTOR_L10N.getStr("inspector.sidebar.layoutViewTitle"),
+ key: "layoutview",
+ }, App());
+
+ let defaultTab = Services.prefs.getCharPref("devtools.inspector.activeSidebar");
+
+ this.inspector.addSidebarTab(
+ "layoutview",
+ INSPECTOR_L10N.getStr("inspector.sidebar.layoutViewTitle"),
+ provider,
+ defaultTab == "layoutview"
+ );
+ },
+
+ destroy() {
+ this.inspector = null;
+ this.document = null;
+ this.store = null;
+ },
+};
+
+exports.LayoutView = LayoutView;
+
diff --git a/devtools/client/inspector/layout/moz.build b/devtools/client/inspector/layout/moz.build
new file mode 100644
index 000000000..8575deedf
--- /dev/null
+++ b/devtools/client/inspector/layout/moz.build
@@ -0,0 +1,18 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DIRS += [
+ 'actions',
+ 'components',
+ 'reducers',
+ 'utils',
+]
+
+DevToolsModules(
+ 'layout.js',
+ 'store.js',
+ 'types.js',
+)
diff --git a/devtools/client/inspector/layout/reducers/grids.js b/devtools/client/inspector/layout/reducers/grids.js
new file mode 100644
index 000000000..3a1c26fd4
--- /dev/null
+++ b/devtools/client/inspector/layout/reducers/grids.js
@@ -0,0 +1,21 @@
+/* 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 INITIAL_GRIDS = {
+
+};
+
+let reducers = {
+
+};
+
+module.exports = function (grids = INITIAL_GRIDS, action) {
+ let reducer = reducers[action.type];
+ if (!reducer) {
+ return grids;
+ }
+ return reducer(grids, action);
+};
diff --git a/devtools/client/inspector/layout/reducers/index.js b/devtools/client/inspector/layout/reducers/index.js
new file mode 100644
index 000000000..3fed406d7
--- /dev/null
+++ b/devtools/client/inspector/layout/reducers/index.js
@@ -0,0 +1,7 @@
+/* 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";
+
+exports.grids = require("./grids");
diff --git a/devtools/client/inspector/layout/reducers/moz.build b/devtools/client/inspector/layout/reducers/moz.build
new file mode 100644
index 000000000..7c6955914
--- /dev/null
+++ b/devtools/client/inspector/layout/reducers/moz.build
@@ -0,0 +1,10 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'grids.js',
+ 'index.js',
+)
diff --git a/devtools/client/inspector/layout/store.js b/devtools/client/inspector/layout/store.js
new file mode 100644
index 000000000..5069dda26
--- /dev/null
+++ b/devtools/client/inspector/layout/store.js
@@ -0,0 +1,33 @@
+/* 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 { combineReducers } = require("devtools/client/shared/vendor/redux");
+const createStore = require("devtools/client/shared/redux/create-store");
+const reducers = require("./reducers/index");
+const flags = require("devtools/shared/flags");
+
+module.exports = function () {
+ let shouldLog = false;
+ let history;
+
+ // If testing, store the action history in an array
+ // we'll later attach to the store
+ if (flags.testing) {
+ history = [];
+ shouldLog = true;
+ }
+
+ let store = createStore({
+ log: shouldLog,
+ history
+ })(combineReducers(reducers), {});
+
+ if (history) {
+ store.history = history;
+ }
+
+ return store;
+};
diff --git a/devtools/client/inspector/layout/types.js b/devtools/client/inspector/layout/types.js
new file mode 100644
index 000000000..66735cddb
--- /dev/null
+++ b/devtools/client/inspector/layout/types.js
@@ -0,0 +1,5 @@
+/* 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";
diff --git a/devtools/client/inspector/layout/utils/l10n.js b/devtools/client/inspector/layout/utils/l10n.js
new file mode 100644
index 000000000..96ab21768
--- /dev/null
+++ b/devtools/client/inspector/layout/utils/l10n.js
@@ -0,0 +1,15 @@
+/* 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 { LocalizationHelper } = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper("devtools/client/locales/layout.properties");
+
+module.exports = {
+ getStr: (...args) => L10N.getStr(...args),
+ getFormatStr: (...args) => L10N.getFormatStr(...args),
+ getFormatStrWithNumbers: (...args) => L10N.getFormatStrWithNumbers(...args),
+ numberWithDecimals: (...args) => L10N.numberWithDecimals(...args),
+};
diff --git a/devtools/client/inspector/layout/utils/moz.build b/devtools/client/inspector/layout/utils/moz.build
new file mode 100644
index 000000000..e3053b63f
--- /dev/null
+++ b/devtools/client/inspector/layout/utils/moz.build
@@ -0,0 +1,9 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'l10n.js',
+)
diff --git a/devtools/client/inspector/markup/markup.js b/devtools/client/inspector/markup/markup.js
new file mode 100644
index 000000000..d6e9f8c11
--- /dev/null
+++ b/devtools/client/inspector/markup/markup.js
@@ -0,0 +1,1878 @@
+/* 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 Services = require("Services");
+const defer = require("devtools/shared/defer");
+const {Task} = require("devtools/shared/task");
+const nodeConstants = require("devtools/shared/dom-node-constants");
+const nodeFilterConstants = require("devtools/shared/dom-node-filter-constants");
+const EventEmitter = require("devtools/shared/event-emitter");
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const {PluralForm} = require("devtools/shared/plural-form");
+const {template} = require("devtools/shared/gcli/templater");
+const {AutocompletePopup} = require("devtools/client/shared/autocomplete-popup");
+const {KeyShortcuts} = require("devtools/client/shared/key-shortcuts");
+const {scrollIntoViewIfNeeded} = require("devtools/client/shared/scroll");
+const {UndoStack} = require("devtools/client/shared/undo");
+const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+const {PrefObserver} = require("devtools/client/styleeditor/utils");
+const HTMLEditor = require("devtools/client/inspector/markup/views/html-editor");
+const MarkupElementContainer = require("devtools/client/inspector/markup/views/element-container");
+const MarkupReadOnlyContainer = require("devtools/client/inspector/markup/views/read-only-container");
+const MarkupTextContainer = require("devtools/client/inspector/markup/views/text-container");
+const RootContainer = require("devtools/client/inspector/markup/views/root-container");
+
+const INSPECTOR_L10N =
+ new LocalizationHelper("devtools/client/locales/inspector.properties");
+
+// Page size for pageup/pagedown
+const PAGE_SIZE = 10;
+const DEFAULT_MAX_CHILDREN = 100;
+const NEW_SELECTION_HIGHLIGHTER_TIMER = 1000;
+const DRAG_DROP_AUTOSCROLL_EDGE_MAX_DISTANCE = 50;
+const DRAG_DROP_AUTOSCROLL_EDGE_RATIO = 0.1;
+const DRAG_DROP_MIN_AUTOSCROLL_SPEED = 2;
+const DRAG_DROP_MAX_AUTOSCROLL_SPEED = 8;
+const DRAG_DROP_HEIGHT_TO_SPEED = 500;
+const DRAG_DROP_HEIGHT_TO_SPEED_MIN = 0.5;
+const DRAG_DROP_HEIGHT_TO_SPEED_MAX = 1;
+const ATTR_COLLAPSE_ENABLED_PREF = "devtools.markup.collapseAttributes";
+const ATTR_COLLAPSE_LENGTH_PREF = "devtools.markup.collapseAttributeLength";
+
+/**
+ * Vocabulary for the purposes of this file:
+ *
+ * MarkupContainer - the structure that holds an editor and its
+ * immediate children in the markup panel.
+ * - MarkupElementContainer: markup container for element nodes
+ * - MarkupTextContainer: markup container for text / comment nodes
+ * - MarkupReadonlyContainer: markup container for other nodes
+ * Node - A content node.
+ * object.elt - A UI element in the markup panel.
+ */
+
+/**
+ * The markup tree. Manages the mapping of nodes to MarkupContainers,
+ * updating based on mutations, and the undo/redo bindings.
+ *
+ * @param {Inspector} inspector
+ * The inspector we're watching.
+ * @param {iframe} frame
+ * An iframe in which the caller has kindly loaded markup.xhtml.
+ */
+function MarkupView(inspector, frame, controllerWindow) {
+ this.inspector = inspector;
+ this.walker = this.inspector.walker;
+ this._frame = frame;
+ this.win = this._frame.contentWindow;
+ this.doc = this._frame.contentDocument;
+ this._elt = this.doc.querySelector("#root");
+ this.htmlEditor = new HTMLEditor(this.doc);
+
+ try {
+ this.maxChildren = Services.prefs.getIntPref("devtools.markup.pagesize");
+ } catch (ex) {
+ this.maxChildren = DEFAULT_MAX_CHILDREN;
+ }
+
+ this.collapseAttributes =
+ Services.prefs.getBoolPref(ATTR_COLLAPSE_ENABLED_PREF);
+ this.collapseAttributeLength =
+ Services.prefs.getIntPref(ATTR_COLLAPSE_LENGTH_PREF);
+
+ // Creating the popup to be used to show CSS suggestions.
+ // The popup will be attached to the toolbox document.
+ this.popup = new AutocompletePopup(inspector.toolbox.doc, {
+ autoSelect: true,
+ theme: "auto",
+ });
+
+ this.undo = new UndoStack();
+ this.undo.installController(controllerWindow);
+
+ this._containers = new Map();
+
+ // Binding functions that need to be called in scope.
+ this._handleRejectionIfNotDestroyed = this._handleRejectionIfNotDestroyed.bind(this);
+ this._mutationObserver = this._mutationObserver.bind(this);
+ this._onDisplayChange = this._onDisplayChange.bind(this);
+ this._onMouseClick = this._onMouseClick.bind(this);
+ this._onMouseUp = this._onMouseUp.bind(this);
+ this._onNewSelection = this._onNewSelection.bind(this);
+ this._onCopy = this._onCopy.bind(this);
+ this._onFocus = this._onFocus.bind(this);
+ this._onMouseMove = this._onMouseMove.bind(this);
+ this._onMouseOut = this._onMouseOut.bind(this);
+ this._onToolboxPickerCanceled = this._onToolboxPickerCanceled.bind(this);
+ this._onToolboxPickerHover = this._onToolboxPickerHover.bind(this);
+ this._onCollapseAttributesPrefChange =
+ this._onCollapseAttributesPrefChange.bind(this);
+ this._isImagePreviewTarget = this._isImagePreviewTarget.bind(this);
+ this._onBlur = this._onBlur.bind(this);
+
+ EventEmitter.decorate(this);
+
+ // Listening to various events.
+ this._elt.addEventListener("click", this._onMouseClick, false);
+ this._elt.addEventListener("mousemove", this._onMouseMove, false);
+ this._elt.addEventListener("mouseout", this._onMouseOut, false);
+ this._elt.addEventListener("blur", this._onBlur, true);
+ this.win.addEventListener("mouseup", this._onMouseUp);
+ this.win.addEventListener("copy", this._onCopy);
+ this._frame.addEventListener("focus", this._onFocus, false);
+ this.walker.on("mutations", this._mutationObserver);
+ this.walker.on("display-change", this._onDisplayChange);
+ this.inspector.selection.on("new-node-front", this._onNewSelection);
+ this.toolbox.on("picker-canceled", this._onToolboxPickerCanceled);
+ this.toolbox.on("picker-node-hovered", this._onToolboxPickerHover);
+
+ this._onNewSelection();
+ this._initTooltips();
+
+ this._prefObserver = new PrefObserver("devtools.markup");
+ this._prefObserver.on(ATTR_COLLAPSE_ENABLED_PREF,
+ this._onCollapseAttributesPrefChange);
+ this._prefObserver.on(ATTR_COLLAPSE_LENGTH_PREF,
+ this._onCollapseAttributesPrefChange);
+
+ this._initShortcuts();
+}
+
+MarkupView.prototype = {
+ /**
+ * How long does a node flash when it mutates (in ms).
+ */
+ CONTAINER_FLASHING_DURATION: 500,
+
+ _selectedContainer: null,
+
+ get toolbox() {
+ return this.inspector.toolbox;
+ },
+
+ /**
+ * Handle promise rejections for various asynchronous actions, and only log errors if
+ * the markup view still exists.
+ * This is useful to silence useless errors that happen when the markup view is
+ * destroyed while still initializing (and making protocol requests).
+ */
+ _handleRejectionIfNotDestroyed: function (e) {
+ if (!this._destroyer) {
+ console.error(e);
+ }
+ },
+
+ _initTooltips: function () {
+ // The tooltips will be attached to the toolbox document.
+ this.eventDetailsTooltip = new HTMLTooltip(this.toolbox.doc,
+ {type: "arrow"});
+ this.imagePreviewTooltip = new HTMLTooltip(this.toolbox.doc,
+ {type: "arrow", useXulWrapper: "true"});
+ this._enableImagePreviewTooltip();
+ },
+
+ _enableImagePreviewTooltip: function () {
+ this.imagePreviewTooltip.startTogglingOnHover(this._elt,
+ this._isImagePreviewTarget);
+ },
+
+ _disableImagePreviewTooltip: function () {
+ this.imagePreviewTooltip.stopTogglingOnHover();
+ },
+
+ _onToolboxPickerHover: function (event, nodeFront) {
+ this.showNode(nodeFront).then(() => {
+ this._showContainerAsHovered(nodeFront);
+ }, e => console.error(e));
+ },
+
+ /**
+ * If the element picker gets canceled, make sure and re-center the view on the
+ * current selected element.
+ */
+ _onToolboxPickerCanceled: function () {
+ if (this._selectedContainer) {
+ scrollIntoViewIfNeeded(this._selectedContainer.editor.elt);
+ }
+ },
+
+ isDragging: false,
+
+ _onMouseMove: function (event) {
+ let target = event.target;
+
+ // Auto-scroll if we're dragging.
+ if (this.isDragging) {
+ event.preventDefault();
+ this._autoScroll(event);
+ return;
+ }
+
+ // Show the current container as hovered and highlight it.
+ // This requires finding the current MarkupContainer (walking up the DOM).
+ while (!target.container) {
+ if (target.tagName.toLowerCase() === "body") {
+ return;
+ }
+ target = target.parentNode;
+ }
+
+ let container = target.container;
+ if (this._hoveredNode !== container.node) {
+ this._showBoxModel(container.node);
+ }
+ this._showContainerAsHovered(container.node);
+
+ this.emit("node-hover");
+ },
+
+ /**
+ * If focus is moved outside of the markup view document and there is a
+ * selected container, make its contents not focusable by a keyboard.
+ */
+ _onBlur: function (event) {
+ if (!this._selectedContainer) {
+ return;
+ }
+
+ let {relatedTarget} = event;
+ if (relatedTarget && relatedTarget.ownerDocument === this.doc) {
+ return;
+ }
+
+ if (this._selectedContainer) {
+ this._selectedContainer.clearFocus();
+ }
+ },
+
+ /**
+ * Executed on each mouse-move while a node is being dragged in the view.
+ * Auto-scrolls the view to reveal nodes below the fold to drop the dragged
+ * node in.
+ */
+ _autoScroll: function (event) {
+ let docEl = this.doc.documentElement;
+
+ if (this._autoScrollAnimationFrame) {
+ this.win.cancelAnimationFrame(this._autoScrollAnimationFrame);
+ }
+
+ // Auto-scroll when the mouse approaches top/bottom edge.
+ let fromBottom = docEl.clientHeight - event.pageY + this.win.scrollY;
+ let fromTop = event.pageY - this.win.scrollY;
+ let edgeDistance = Math.min(DRAG_DROP_AUTOSCROLL_EDGE_MAX_DISTANCE,
+ docEl.clientHeight * DRAG_DROP_AUTOSCROLL_EDGE_RATIO);
+
+ // The smaller the screen, the slower the movement.
+ let heightToSpeedRatio =
+ Math.max(DRAG_DROP_HEIGHT_TO_SPEED_MIN,
+ Math.min(DRAG_DROP_HEIGHT_TO_SPEED_MAX,
+ docEl.clientHeight / DRAG_DROP_HEIGHT_TO_SPEED));
+
+ if (fromBottom <= edgeDistance) {
+ // Map our distance range to a speed range so that the speed is not too
+ // fast or too slow.
+ let speed = map(
+ fromBottom,
+ 0, edgeDistance,
+ DRAG_DROP_MIN_AUTOSCROLL_SPEED, DRAG_DROP_MAX_AUTOSCROLL_SPEED);
+
+ this._runUpdateLoop(() => {
+ docEl.scrollTop -= heightToSpeedRatio *
+ (speed - DRAG_DROP_MAX_AUTOSCROLL_SPEED);
+ });
+ }
+
+ if (fromTop <= edgeDistance) {
+ let speed = map(
+ fromTop,
+ 0, edgeDistance,
+ DRAG_DROP_MIN_AUTOSCROLL_SPEED, DRAG_DROP_MAX_AUTOSCROLL_SPEED);
+
+ this._runUpdateLoop(() => {
+ docEl.scrollTop += heightToSpeedRatio *
+ (speed - DRAG_DROP_MAX_AUTOSCROLL_SPEED);
+ });
+ }
+ },
+
+ /**
+ * Run a loop on the requestAnimationFrame.
+ */
+ _runUpdateLoop: function (update) {
+ let loop = () => {
+ update();
+ this._autoScrollAnimationFrame = this.win.requestAnimationFrame(loop);
+ };
+ loop();
+ },
+
+ _onMouseClick: function (event) {
+ // From the target passed here, let's find the parent MarkupContainer
+ // and ask it if the tooltip should be shown
+ let parentNode = event.target;
+ let container;
+ while (parentNode !== this.doc.body) {
+ if (parentNode.container) {
+ container = parentNode.container;
+ break;
+ }
+ parentNode = parentNode.parentNode;
+ }
+
+ if (container instanceof MarkupElementContainer) {
+ // With the newly found container, delegate the tooltip content creation
+ // and decision to show or not the tooltip
+ container._buildEventTooltipContent(event.target,
+ this.eventDetailsTooltip);
+ }
+ },
+
+ _onMouseUp: function () {
+ this.indicateDropTarget(null);
+ this.indicateDragTarget(null);
+ if (this._autoScrollAnimationFrame) {
+ this.win.cancelAnimationFrame(this._autoScrollAnimationFrame);
+ }
+ },
+
+ _onCollapseAttributesPrefChange: function () {
+ this.collapseAttributes =
+ Services.prefs.getBoolPref(ATTR_COLLAPSE_ENABLED_PREF);
+ this.collapseAttributeLength =
+ Services.prefs.getIntPref(ATTR_COLLAPSE_LENGTH_PREF);
+ this.update();
+ },
+
+ cancelDragging: function () {
+ if (!this.isDragging) {
+ return;
+ }
+
+ for (let [, container] of this._containers) {
+ if (container.isDragging) {
+ container.cancelDragging();
+ break;
+ }
+ }
+
+ this.indicateDropTarget(null);
+ this.indicateDragTarget(null);
+ if (this._autoScrollAnimationFrame) {
+ this.win.cancelAnimationFrame(this._autoScrollAnimationFrame);
+ }
+ },
+
+ _hoveredNode: null,
+
+ /**
+ * Show a NodeFront's container as being hovered
+ *
+ * @param {NodeFront} nodeFront
+ * The node to show as hovered
+ */
+ _showContainerAsHovered: function (nodeFront) {
+ if (this._hoveredNode === nodeFront) {
+ return;
+ }
+
+ if (this._hoveredNode) {
+ this.getContainer(this._hoveredNode).hovered = false;
+ }
+
+ this.getContainer(nodeFront).hovered = true;
+ this._hoveredNode = nodeFront;
+ // Emit an event that the container view is actually hovered now, as this function
+ // can be called by an asynchronous caller.
+ this.emit("showcontainerhovered");
+ },
+
+ _onMouseOut: function (event) {
+ // Emulate mouseleave by skipping any relatedTarget inside the markup-view.
+ if (this._elt.contains(event.relatedTarget)) {
+ return;
+ }
+
+ if (this._autoScrollAnimationFrame) {
+ this.win.cancelAnimationFrame(this._autoScrollAnimationFrame);
+ }
+ if (this.isDragging) {
+ return;
+ }
+
+ this._hideBoxModel(true);
+ if (this._hoveredNode) {
+ this.getContainer(this._hoveredNode).hovered = false;
+ }
+ this._hoveredNode = null;
+
+ this.emit("leave");
+ },
+
+ /**
+ * Show the box model highlighter on a given node front
+ *
+ * @param {NodeFront} nodeFront
+ * The node to show the highlighter for
+ * @return {Promise} Resolves when the highlighter for this nodeFront is
+ * shown, taking into account that there could already be highlighter
+ * requests queued up
+ */
+ _showBoxModel: function (nodeFront) {
+ return this.toolbox.highlighterUtils.highlightNodeFront(nodeFront);
+ },
+
+ /**
+ * Hide the box model highlighter on a given node front
+ *
+ * @param {Boolean} forceHide
+ * See toolbox-highlighter-utils/unhighlight
+ * @return {Promise} Resolves when the highlighter for this nodeFront is
+ * hidden, taking into account that there could already be highlighter
+ * requests queued up
+ */
+ _hideBoxModel: function (forceHide) {
+ return this.toolbox.highlighterUtils.unhighlight(forceHide);
+ },
+
+ _briefBoxModelTimer: null,
+
+ _clearBriefBoxModelTimer: function () {
+ if (this._briefBoxModelTimer) {
+ clearTimeout(this._briefBoxModelTimer);
+ this._briefBoxModelPromise.resolve();
+ this._briefBoxModelPromise = null;
+ this._briefBoxModelTimer = null;
+ }
+ },
+
+ _brieflyShowBoxModel: function (nodeFront) {
+ this._clearBriefBoxModelTimer();
+ let onShown = this._showBoxModel(nodeFront);
+ this._briefBoxModelPromise = defer();
+
+ this._briefBoxModelTimer = setTimeout(() => {
+ this._hideBoxModel()
+ .then(this._briefBoxModelPromise.resolve,
+ this._briefBoxModelPromise.resolve);
+ }, NEW_SELECTION_HIGHLIGHTER_TIMER);
+
+ return promise.all([onShown, this._briefBoxModelPromise.promise]);
+ },
+
+ template: function (name, dest, options = {stack: "markup.xhtml"}) {
+ let node = this.doc.getElementById("template-" + name).cloneNode(true);
+ node.removeAttribute("id");
+ template(node, dest, options);
+ return node;
+ },
+
+ /**
+ * Get the MarkupContainer object for a given node, or undefined if
+ * none exists.
+ */
+ getContainer: function (node) {
+ return this._containers.get(node);
+ },
+
+ update: function () {
+ let updateChildren = (node) => {
+ this.getContainer(node).update();
+ for (let child of node.treeChildren()) {
+ updateChildren(child);
+ }
+ };
+
+ // Start with the documentElement
+ let documentElement;
+ for (let node of this._rootNode.treeChildren()) {
+ if (node.isDocumentElement === true) {
+ documentElement = node;
+ break;
+ }
+ }
+
+ // Recursively update each node starting with documentElement.
+ updateChildren(documentElement);
+ },
+
+ /**
+ * Executed when the mouse hovers over a target in the markup-view and is used
+ * to decide whether this target should be used to display an image preview
+ * tooltip.
+ * Delegates the actual decision to the corresponding MarkupContainer instance
+ * if one is found.
+ *
+ * @return {Promise} the promise returned by
+ * MarkupElementContainer._isImagePreviewTarget
+ */
+ _isImagePreviewTarget: Task.async(function* (target) {
+ // From the target passed here, let's find the parent MarkupContainer
+ // and ask it if the tooltip should be shown
+ if (this.isDragging) {
+ return false;
+ }
+
+ let parent = target, container;
+ while (parent !== this.doc.body) {
+ if (parent.container) {
+ container = parent.container;
+ break;
+ }
+ parent = parent.parentNode;
+ }
+
+ if (container instanceof MarkupElementContainer) {
+ // With the newly found container, delegate the tooltip content creation
+ // and decision to show or not the tooltip
+ return container.isImagePreviewTarget(target, this.imagePreviewTooltip);
+ }
+
+ return false;
+ }),
+
+ /**
+ * Given the known reason, should the current selection be briefly highlighted
+ * In a few cases, we don't want to highlight the node:
+ * - If the reason is null (used to reset the selection),
+ * - if it's "inspector-open" (when the inspector opens up, let's not
+ * highlight the default node)
+ * - if it's "navigateaway" (since the page is being navigated away from)
+ * - if it's "test" (this is a special case for mochitest. In tests, we often
+ * need to select elements but don't necessarily want the highlighter to come
+ * and go after a delay as this might break test scenarios)
+ * We also do not want to start a brief highlight timeout if the node is
+ * already being hovered over, since in that case it will already be
+ * highlighted.
+ */
+ _shouldNewSelectionBeHighlighted: function () {
+ let reason = this.inspector.selection.reason;
+ let unwantedReasons = [
+ "inspector-open",
+ "navigateaway",
+ "nodeselected",
+ "test"
+ ];
+ let isHighlight = this._hoveredNode === this.inspector.selection.nodeFront;
+ return !isHighlight && reason && unwantedReasons.indexOf(reason) === -1;
+ },
+
+ /**
+ * React to new-node-front selection events.
+ * Highlights the node if needed, and make sure it is shown and selected in
+ * the view.
+ */
+ _onNewSelection: function () {
+ let selection = this.inspector.selection;
+
+ this.htmlEditor.hide();
+ if (this._hoveredNode && this._hoveredNode !== selection.nodeFront) {
+ this.getContainer(this._hoveredNode).hovered = false;
+ this._hoveredNode = null;
+ }
+
+ if (!selection.isNode()) {
+ this.unmarkSelectedNode();
+ return;
+ }
+
+ let done = this.inspector.updating("markup-view");
+ let onShowBoxModel, onShow;
+
+ // Highlight the element briefly if needed.
+ if (this._shouldNewSelectionBeHighlighted()) {
+ onShowBoxModel = this._brieflyShowBoxModel(selection.nodeFront);
+ }
+
+ onShow = this.showNode(selection.nodeFront).then(() => {
+ // We could be destroyed by now.
+ if (this._destroyer) {
+ return promise.reject("markupview destroyed");
+ }
+
+ // Mark the node as selected.
+ this.markNodeAsSelected(selection.nodeFront);
+
+ // Make sure the new selection is navigated to.
+ this.maybeNavigateToNewSelection();
+ return undefined;
+ }).catch(this._handleRejectionIfNotDestroyed);
+
+ promise.all([onShowBoxModel, onShow]).then(done);
+ },
+
+ /**
+ * Maybe make selected the current node selection's MarkupContainer depending
+ * on why the current node got selected.
+ */
+ maybeNavigateToNewSelection: function () {
+ let {reason, nodeFront} = this.inspector.selection;
+
+ // The list of reasons that should lead to navigating to the node.
+ let reasonsToNavigate = [
+ // If the user picked an element with the element picker.
+ "picker-node-picked",
+ // If the user shift-clicked (previewed) an element.
+ "picker-node-previewed",
+ // If the user selected an element with the browser context menu.
+ "browser-context-menu",
+ // If the user added a new node by clicking in the inspector toolbar.
+ "node-inserted"
+ ];
+
+ if (reasonsToNavigate.includes(reason)) {
+ this.getContainer(this._rootNode).elt.focus();
+ this.navigate(this.getContainer(nodeFront));
+ }
+ },
+
+ /**
+ * Create a TreeWalker to find the next/previous
+ * node for selection.
+ */
+ _selectionWalker: function (start) {
+ let walker = this.doc.createTreeWalker(
+ start || this._elt,
+ nodeFilterConstants.SHOW_ELEMENT,
+ function (element) {
+ if (element.container &&
+ element.container.elt === element &&
+ element.container.visible) {
+ return nodeFilterConstants.FILTER_ACCEPT;
+ }
+ return nodeFilterConstants.FILTER_SKIP;
+ }
+ );
+ walker.currentNode = this._selectedContainer.elt;
+ return walker;
+ },
+
+ _onCopy: function (evt) {
+ // Ignore copy events from editors
+ if (this._isInputOrTextarea(evt.target)) {
+ return;
+ }
+
+ let selection = this.inspector.selection;
+ if (selection.isNode()) {
+ this.inspector.copyOuterHTML();
+ }
+ evt.stopPropagation();
+ evt.preventDefault();
+ },
+
+ /**
+ * Register all key shortcuts.
+ */
+ _initShortcuts: function () {
+ let shortcuts = new KeyShortcuts({
+ window: this.win,
+ });
+
+ this._onShortcut = this._onShortcut.bind(this);
+
+ // Process localizable keys
+ ["markupView.hide.key",
+ "markupView.edit.key",
+ "markupView.scrollInto.key"].forEach(name => {
+ let key = INSPECTOR_L10N.getStr(name);
+ shortcuts.on(key, (_, event) => this._onShortcut(name, event));
+ });
+
+ // Process generic keys:
+ ["Delete", "Backspace", "Home", "Left", "Right", "Up", "Down", "PageUp",
+ "PageDown", "Esc", "Enter", "Space"].forEach(key => {
+ shortcuts.on(key, this._onShortcut);
+ });
+ },
+
+ /**
+ * Key shortcut listener.
+ */
+ _onShortcut(name, event) {
+ if (this._isInputOrTextarea(event.target)) {
+ return;
+ }
+ switch (name) {
+ // Localizable keys
+ case "markupView.hide.key": {
+ let node = this._selectedContainer.node;
+ if (node.hidden) {
+ this.walker.unhideNode(node);
+ } else {
+ this.walker.hideNode(node);
+ }
+ break;
+ }
+ case "markupView.edit.key": {
+ this.beginEditingOuterHTML(this._selectedContainer.node);
+ break;
+ }
+ case "markupView.scrollInto.key": {
+ let selection = this._selectedContainer.node;
+ this.inspector.scrollNodeIntoView(selection);
+ break;
+ }
+ // Generic keys
+ case "Delete": {
+ this.deleteNodeOrAttribute();
+ break;
+ }
+ case "Backspace": {
+ this.deleteNodeOrAttribute(true);
+ break;
+ }
+ case "Home": {
+ let rootContainer = this.getContainer(this._rootNode);
+ this.navigate(rootContainer.children.firstChild.container);
+ break;
+ }
+ case "Left": {
+ if (this._selectedContainer.expanded) {
+ this.collapseNode(this._selectedContainer.node);
+ } else {
+ let parent = this._selectionWalker().parentNode();
+ if (parent) {
+ this.navigate(parent.container);
+ }
+ }
+ break;
+ }
+ case "Right": {
+ if (!this._selectedContainer.expanded &&
+ this._selectedContainer.hasChildren) {
+ this._expandContainer(this._selectedContainer);
+ } else {
+ let next = this._selectionWalker().nextNode();
+ if (next) {
+ this.navigate(next.container);
+ }
+ }
+ break;
+ }
+ case "Up": {
+ let previousNode = this._selectionWalker().previousNode();
+ if (previousNode) {
+ this.navigate(previousNode.container);
+ }
+ break;
+ }
+ case "Down": {
+ let nextNode = this._selectionWalker().nextNode();
+ if (nextNode) {
+ this.navigate(nextNode.container);
+ }
+ break;
+ }
+ case "PageUp": {
+ let walker = this._selectionWalker();
+ let selection = this._selectedContainer;
+ for (let i = 0; i < PAGE_SIZE; i++) {
+ let previousNode = walker.previousNode();
+ if (!previousNode) {
+ break;
+ }
+ selection = previousNode.container;
+ }
+ this.navigate(selection);
+ break;
+ }
+ case "PageDown": {
+ let walker = this._selectionWalker();
+ let selection = this._selectedContainer;
+ for (let i = 0; i < PAGE_SIZE; i++) {
+ let nextNode = walker.nextNode();
+ if (!nextNode) {
+ break;
+ }
+ selection = nextNode.container;
+ }
+ this.navigate(selection);
+ break;
+ }
+ case "Enter":
+ case "Space": {
+ if (!this._selectedContainer.canFocus) {
+ this._selectedContainer.canFocus = true;
+ this._selectedContainer.focus();
+ } else {
+ // Return early to prevent cancelling the event.
+ return;
+ }
+ break;
+ }
+ case "Esc": {
+ if (this.isDragging) {
+ this.cancelDragging();
+ } else {
+ // Return early to prevent cancelling the event when not
+ // dragging, to allow the split console to be toggled.
+ return;
+ }
+ break;
+ }
+ default:
+ console.error("Unexpected markup-view key shortcut", name);
+ return;
+ }
+ // Prevent default for this action
+ event.stopPropagation();
+ event.preventDefault();
+ },
+
+ /**
+ * Check if a node is an input or textarea
+ */
+ _isInputOrTextarea: function (element) {
+ let name = element.tagName.toLowerCase();
+ return name === "input" || name === "textarea";
+ },
+
+ /**
+ * If there's an attribute on the current node that's currently focused, then
+ * delete this attribute, otherwise delete the node itself.
+ *
+ * @param {Boolean} moveBackward
+ * If set to true and if we're deleting the node, focus the previous
+ * sibling after deletion, otherwise the next one.
+ */
+ deleteNodeOrAttribute: function (moveBackward) {
+ let focusedAttribute = this.doc.activeElement
+ ? this.doc.activeElement.closest(".attreditor")
+ : null;
+ if (focusedAttribute) {
+ // The focused attribute might not be in the current selected container.
+ let container = focusedAttribute.closest("li.child").container;
+ container.removeAttribute(focusedAttribute.dataset.attr);
+ } else {
+ this.deleteNode(this._selectedContainer.node, moveBackward);
+ }
+ },
+
+ /**
+ * Delete a node from the DOM.
+ * This is an undoable action.
+ *
+ * @param {NodeFront} node
+ * The node to remove.
+ * @param {Boolean} moveBackward
+ * If set to true, focus the previous sibling, otherwise the next one.
+ */
+ deleteNode: function (node, moveBackward) {
+ if (node.isDocumentElement ||
+ node.nodeType == nodeConstants.DOCUMENT_TYPE_NODE ||
+ node.isAnonymous) {
+ return;
+ }
+
+ let container = this.getContainer(node);
+
+ // Retain the node so we can undo this...
+ this.walker.retainNode(node).then(() => {
+ let parent = node.parentNode();
+ let nextSibling = null;
+ this.undo.do(() => {
+ this.walker.removeNode(node).then(siblings => {
+ nextSibling = siblings.nextSibling;
+ let prevSibling = siblings.previousSibling;
+ let focusNode = moveBackward ? prevSibling : nextSibling;
+
+ // If we can't move as the user wants, we move to the other direction.
+ // If there is no sibling elements anymore, move to the parent node.
+ if (!focusNode) {
+ focusNode = nextSibling || prevSibling || parent;
+ }
+
+ let isNextSiblingText = nextSibling ?
+ nextSibling.nodeType === nodeConstants.TEXT_NODE : false;
+ let isPrevSiblingText = prevSibling ?
+ prevSibling.nodeType === nodeConstants.TEXT_NODE : false;
+
+ // If the parent had two children and the next or previous sibling
+ // is a text node, then it now has only a single text node, is about
+ // to be in-lined; and focus should move to the parent.
+ if (parent.numChildren === 2
+ && (isNextSiblingText || isPrevSiblingText)) {
+ focusNode = parent;
+ }
+
+ if (container.selected) {
+ this.navigate(this.getContainer(focusNode));
+ }
+ });
+ }, () => {
+ let isValidSibling = nextSibling && !nextSibling.isPseudoElement;
+ nextSibling = isValidSibling ? nextSibling : null;
+ this.walker.insertBefore(node, parent, nextSibling);
+ });
+ }).then(null, console.error);
+ },
+
+ /**
+ * If an editable item is focused, select its container.
+ */
+ _onFocus: function (event) {
+ let parent = event.target;
+ while (!parent.container) {
+ parent = parent.parentNode;
+ }
+ if (parent) {
+ this.navigate(parent.container);
+ }
+ },
+
+ /**
+ * Handle a user-requested navigation to a given MarkupContainer,
+ * updating the inspector's currently-selected node.
+ *
+ * @param {MarkupContainer} container
+ * The container we're navigating to.
+ */
+ navigate: function (container) {
+ if (!container) {
+ return;
+ }
+
+ let node = container.node;
+ this.markNodeAsSelected(node, "treepanel");
+ },
+
+ /**
+ * Make sure a node is included in the markup tool.
+ *
+ * @param {NodeFront} node
+ * The node in the content document.
+ * @param {Boolean} flashNode
+ * Whether the newly imported node should be flashed
+ * @return {MarkupContainer} The MarkupContainer object for this element.
+ */
+ importNode: function (node, flashNode) {
+ if (!node) {
+ return null;
+ }
+
+ if (this._containers.has(node)) {
+ return this.getContainer(node);
+ }
+
+ let container;
+ let {nodeType, isPseudoElement} = node;
+ if (node === this.walker.rootNode) {
+ container = new RootContainer(this, node);
+ this._elt.appendChild(container.elt);
+ this._rootNode = node;
+ } else if (nodeType == nodeConstants.ELEMENT_NODE && !isPseudoElement) {
+ container = new MarkupElementContainer(this, node, this.inspector);
+ } else if (nodeType == nodeConstants.COMMENT_NODE ||
+ nodeType == nodeConstants.TEXT_NODE) {
+ container = new MarkupTextContainer(this, node, this.inspector);
+ } else {
+ container = new MarkupReadOnlyContainer(this, node, this.inspector);
+ }
+
+ if (flashNode) {
+ container.flashMutation();
+ }
+
+ this._containers.set(node, container);
+ container.childrenDirty = true;
+
+ this._updateChildren(container);
+
+ this.inspector.emit("container-created", container);
+
+ return container;
+ },
+
+ /**
+ * Mutation observer used for included nodes.
+ */
+ _mutationObserver: function (mutations) {
+ for (let mutation of mutations) {
+ let type = mutation.type;
+ let target = mutation.target;
+
+ if (mutation.type === "documentUnload") {
+ // Treat this as a childList change of the child (maybe the protocol
+ // should do this).
+ type = "childList";
+ target = mutation.targetParent;
+ if (!target) {
+ continue;
+ }
+ }
+
+ let container = this.getContainer(target);
+ if (!container) {
+ // Container might not exist if this came from a load event for a node
+ // we're not viewing.
+ continue;
+ }
+
+ if (type === "attributes" && mutation.attributeName === "class") {
+ container.updateIsDisplayed();
+ }
+ if (type === "attributes" || type === "characterData"
+ || type === "events" || type === "pseudoClassLock") {
+ container.update();
+ } else if (type === "childList" || type === "nativeAnonymousChildList") {
+ container.childrenDirty = true;
+ // Update the children to take care of changes in the markup view DOM
+ // and update container (and its subtree) DOM tree depth level for
+ // accessibility where necessary.
+ this._updateChildren(container, {flash: true}).then(() =>
+ container.updateLevel());
+ } else if (type === "inlineTextChild") {
+ container.childrenDirty = true;
+ this._updateChildren(container, {flash: true});
+ container.update();
+ }
+ }
+
+ this._waitForChildren().then(() => {
+ if (this._destroyer) {
+ // Could not fully update after markup mutations, the markup-view was destroyed
+ // while waiting for children. Bail out silently.
+ return;
+ }
+ this._flashMutatedNodes(mutations);
+ this.inspector.emit("markupmutation", mutations);
+
+ // Since the htmlEditor is absolutely positioned, a mutation may change
+ // the location in which it should be shown.
+ this.htmlEditor.refresh();
+ });
+ },
+
+ /**
+ * React to display-change events from the walker
+ *
+ * @param {Array} nodes
+ * An array of nodeFronts
+ */
+ _onDisplayChange: function (nodes) {
+ for (let node of nodes) {
+ let container = this.getContainer(node);
+ if (container) {
+ container.updateIsDisplayed();
+ }
+ }
+ },
+
+ /**
+ * Given a list of mutations returned by the mutation observer, flash the
+ * corresponding containers to attract attention.
+ */
+ _flashMutatedNodes: function (mutations) {
+ let addedOrEditedContainers = new Set();
+ let removedContainers = new Set();
+
+ for (let {type, target, added, removed, newValue} of mutations) {
+ let container = this.getContainer(target);
+
+ if (container) {
+ if (type === "characterData") {
+ addedOrEditedContainers.add(container);
+ } else if (type === "attributes" && newValue === null) {
+ // Removed attributes should flash the entire node.
+ // New or changed attributes will flash the attribute itself
+ // in ElementEditor.flashAttribute.
+ addedOrEditedContainers.add(container);
+ } else if (type === "childList") {
+ // If there has been removals, flash the parent
+ if (removed.length) {
+ removedContainers.add(container);
+ }
+
+ // If there has been additions, flash the nodes if their associated
+ // container exist (so if their parent is expanded in the inspector).
+ added.forEach(node => {
+ let addedContainer = this.getContainer(node);
+ if (addedContainer) {
+ addedOrEditedContainers.add(addedContainer);
+
+ // The node may be added as a result of an append, in which case
+ // it will have been removed from another container first, but in
+ // these cases we don't want to flash both the removal and the
+ // addition
+ removedContainers.delete(container);
+ }
+ });
+ }
+ }
+ }
+
+ for (let container of removedContainers) {
+ container.flashMutation();
+ }
+ for (let container of addedOrEditedContainers) {
+ container.flashMutation();
+ }
+ },
+
+ /**
+ * Make sure the given node's parents are expanded and the
+ * node is scrolled on to screen.
+ */
+ showNode: function (node, centered = true) {
+ let parent = node;
+
+ this.importNode(node);
+
+ while ((parent = parent.parentNode())) {
+ this.importNode(parent);
+ this.expandNode(parent);
+ }
+
+ return this._waitForChildren().then(() => {
+ if (this._destroyer) {
+ return promise.reject("markupview destroyed");
+ }
+ return this._ensureVisible(node);
+ }).then(() => {
+ scrollIntoViewIfNeeded(this.getContainer(node).editor.elt, centered);
+ }, this._handleRejectionIfNotDestroyed);
+ },
+
+ /**
+ * Expand the container's children.
+ */
+ _expandContainer: function (container) {
+ return this._updateChildren(container, {expand: true}).then(() => {
+ if (this._destroyer) {
+ // Could not expand the node, the markup-view was destroyed in the meantime. Just
+ // silently give up.
+ return;
+ }
+ container.setExpanded(true);
+ });
+ },
+
+ /**
+ * Expand the node's children.
+ */
+ expandNode: function (node) {
+ let container = this.getContainer(node);
+ this._expandContainer(container);
+ },
+
+ /**
+ * Expand the entire tree beneath a container.
+ *
+ * @param {MarkupContainer} container
+ * The container to expand.
+ */
+ _expandAll: function (container) {
+ return this._expandContainer(container).then(() => {
+ let child = container.children.firstChild;
+ let promises = [];
+ while (child) {
+ promises.push(this._expandAll(child.container));
+ child = child.nextSibling;
+ }
+ return promise.all(promises);
+ }).then(null, console.error);
+ },
+
+ /**
+ * Expand the entire tree beneath a node.
+ *
+ * @param {DOMNode} node
+ * The node to expand, or null to start from the top.
+ */
+ expandAll: function (node) {
+ node = node || this._rootNode;
+ return this._expandAll(this.getContainer(node));
+ },
+
+ /**
+ * Collapse the node's children.
+ */
+ collapseNode: function (node) {
+ let container = this.getContainer(node);
+ container.setExpanded(false);
+ },
+
+ /**
+ * Returns either the innerHTML or the outerHTML for a remote node.
+ *
+ * @param {NodeFront} node
+ * The NodeFront to get the outerHTML / innerHTML for.
+ * @param {Boolean} isOuter
+ * If true, makes the function return the outerHTML,
+ * otherwise the innerHTML.
+ * @return {Promise} that will be resolved with the outerHTML / innerHTML.
+ */
+ _getNodeHTML: function (node, isOuter) {
+ let walkerPromise = null;
+
+ if (isOuter) {
+ walkerPromise = this.walker.outerHTML(node);
+ } else {
+ walkerPromise = this.walker.innerHTML(node);
+ }
+
+ return walkerPromise.then(longstr => {
+ return longstr.string().then(html => {
+ longstr.release().then(null, console.error);
+ return html;
+ });
+ });
+ },
+
+ /**
+ * Retrieve the outerHTML for a remote node.
+ *
+ * @param {NodeFront} node
+ * The NodeFront to get the outerHTML for.
+ * @return {Promise} that will be resolved with the outerHTML.
+ */
+ getNodeOuterHTML: function (node) {
+ return this._getNodeHTML(node, true);
+ },
+
+ /**
+ * Retrieve the innerHTML for a remote node.
+ *
+ * @param {NodeFront} node
+ * The NodeFront to get the innerHTML for.
+ * @return {Promise} that will be resolved with the innerHTML.
+ */
+ getNodeInnerHTML: function (node) {
+ return this._getNodeHTML(node);
+ },
+
+ /**
+ * Listen to mutations, expect a given node to be removed and try and select
+ * the node that sits at the same place instead.
+ * This is useful when changing the outerHTML or the tag name so that the
+ * newly inserted node gets selected instead of the one that just got removed.
+ */
+ reselectOnRemoved: function (removedNode, reason) {
+ // Only allow one removed node reselection at a time, so that when there are
+ // more than 1 request in parallel, the last one wins.
+ this.cancelReselectOnRemoved();
+
+ // Get the removedNode index in its parent node to reselect the right node.
+ let isHTMLTag = removedNode.tagName.toLowerCase() === "html";
+ let oldContainer = this.getContainer(removedNode);
+ let parentContainer = this.getContainer(removedNode.parentNode());
+ let childIndex = parentContainer.getChildContainers().indexOf(oldContainer);
+
+ let onMutations = this._removedNodeObserver = (e, mutations) => {
+ let isNodeRemovalMutation = false;
+ for (let mutation of mutations) {
+ let containsRemovedNode = mutation.removed &&
+ mutation.removed.some(n => n === removedNode);
+ if (mutation.type === "childList" &&
+ (containsRemovedNode || isHTMLTag)) {
+ isNodeRemovalMutation = true;
+ break;
+ }
+ }
+ if (!isNodeRemovalMutation) {
+ return;
+ }
+
+ this.inspector.off("markupmutation", onMutations);
+ this._removedNodeObserver = null;
+
+ // Don't select the new node if the user has already changed the current
+ // selection.
+ if (this.inspector.selection.nodeFront === parentContainer.node ||
+ (this.inspector.selection.nodeFront === removedNode && isHTMLTag)) {
+ let childContainers = parentContainer.getChildContainers();
+ if (childContainers && childContainers[childIndex]) {
+ this.markNodeAsSelected(childContainers[childIndex].node, reason);
+ if (childContainers[childIndex].hasChildren) {
+ this.expandNode(childContainers[childIndex].node);
+ }
+ this.emit("reselectedonremoved");
+ }
+ }
+ };
+
+ // Start listening for mutations until we find a childList change that has
+ // removedNode removed.
+ this.inspector.on("markupmutation", onMutations);
+ },
+
+ /**
+ * Make sure to stop listening for node removal markupmutations and not
+ * reselect the corresponding node when that happens.
+ * Useful when the outerHTML/tagname edition failed.
+ */
+ cancelReselectOnRemoved: function () {
+ if (this._removedNodeObserver) {
+ this.inspector.off("markupmutation", this._removedNodeObserver);
+ this._removedNodeObserver = null;
+ this.emit("canceledreselectonremoved");
+ }
+ },
+
+ /**
+ * Replace the outerHTML of any node displayed in the inspector with
+ * some other HTML code
+ *
+ * @param {NodeFront} node
+ * Node which outerHTML will be replaced.
+ * @param {String} newValue
+ * The new outerHTML to set on the node.
+ * @param {String} oldValue
+ * The old outerHTML that will be used if the user undoes the update.
+ * @return {Promise} that will resolve when the outer HTML has been updated.
+ */
+ updateNodeOuterHTML: function (node, newValue) {
+ let container = this.getContainer(node);
+ if (!container) {
+ return promise.reject();
+ }
+
+ // Changing the outerHTML removes the node which outerHTML was changed.
+ // Listen to this removal to reselect the right node afterwards.
+ this.reselectOnRemoved(node, "outerhtml");
+ return this.walker.setOuterHTML(node, newValue).then(null, () => {
+ this.cancelReselectOnRemoved();
+ });
+ },
+
+ /**
+ * Replace the innerHTML of any node displayed in the inspector with
+ * some other HTML code
+ * @param {Node} node
+ * node which innerHTML will be replaced.
+ * @param {String} newValue
+ * The new innerHTML to set on the node.
+ * @param {String} oldValue
+ * The old innerHTML that will be used if the user undoes the update.
+ * @return {Promise} that will resolve when the inner HTML has been updated.
+ */
+ updateNodeInnerHTML: function (node, newValue, oldValue) {
+ let container = this.getContainer(node);
+ if (!container) {
+ return promise.reject();
+ }
+
+ let def = defer();
+
+ container.undo.do(() => {
+ this.walker.setInnerHTML(node, newValue).then(def.resolve, def.reject);
+ }, () => {
+ this.walker.setInnerHTML(node, oldValue);
+ });
+
+ return def.promise;
+ },
+
+ /**
+ * Insert adjacent HTML to any node displayed in the inspector.
+ *
+ * @param {NodeFront} node
+ * The reference node.
+ * @param {String} position
+ * The position as specified for Element.insertAdjacentHTML
+ * (i.e. "beforeBegin", "afterBegin", "beforeEnd", "afterEnd").
+ * @param {String} newValue
+ * The adjacent HTML.
+ * @return {Promise} that will resolve when the adjacent HTML has
+ * been inserted.
+ */
+ insertAdjacentHTMLToNode: function (node, position, value) {
+ let container = this.getContainer(node);
+ if (!container) {
+ return promise.reject();
+ }
+
+ let def = defer();
+
+ let injectedNodes = [];
+ container.undo.do(() => {
+ this.walker.insertAdjacentHTML(node, position, value).then(nodeArray => {
+ injectedNodes = nodeArray.nodes;
+ return nodeArray;
+ }).then(def.resolve, def.reject);
+ }, () => {
+ this.walker.removeNodes(injectedNodes);
+ });
+
+ return def.promise;
+ },
+
+ /**
+ * Open an editor in the UI to allow editing of a node's outerHTML.
+ *
+ * @param {NodeFront} node
+ * The NodeFront to edit.
+ */
+ beginEditingOuterHTML: function (node) {
+ this.getNodeOuterHTML(node).then(oldValue => {
+ let container = this.getContainer(node);
+ if (!container) {
+ return;
+ }
+ this.htmlEditor.show(container.tagLine, oldValue);
+ this.htmlEditor.once("popuphidden", (e, commit, value) => {
+ // Need to focus the <html> element instead of the frame / window
+ // in order to give keyboard focus back to doc (from editor).
+ this.doc.documentElement.focus();
+
+ if (commit) {
+ this.updateNodeOuterHTML(node, value, oldValue);
+ }
+ });
+ });
+ },
+
+ /**
+ * Mark the given node expanded.
+ *
+ * @param {NodeFront} node
+ * The NodeFront to mark as expanded.
+ * @param {Boolean} expanded
+ * Whether the expand or collapse.
+ * @param {Boolean} expandDescendants
+ * Whether to expand all descendants too
+ */
+ setNodeExpanded: function (node, expanded, expandDescendants) {
+ if (expanded) {
+ if (expandDescendants) {
+ this.expandAll(node);
+ } else {
+ this.expandNode(node);
+ }
+ } else {
+ this.collapseNode(node);
+ }
+ },
+
+ /**
+ * Mark the given node selected, and update the inspector.selection
+ * object's NodeFront to keep consistent state between UI and selection.
+ *
+ * @param {NodeFront} aNode
+ * The NodeFront to mark as selected.
+ * @param {String} reason
+ * The reason for marking the node as selected.
+ * @return {Boolean} False if the node is already marked as selected, true
+ * otherwise.
+ */
+ markNodeAsSelected: function (node, reason) {
+ let container = this.getContainer(node);
+
+ if (this._selectedContainer === container) {
+ return false;
+ }
+
+ // Un-select and remove focus from the previous container.
+ if (this._selectedContainer) {
+ this._selectedContainer.selected = false;
+ this._selectedContainer.clearFocus();
+ }
+
+ // Select the new container.
+ this._selectedContainer = container;
+ if (node) {
+ this._selectedContainer.selected = true;
+ }
+
+ // Change the current selection if needed.
+ if (this.inspector.selection.nodeFront !== node) {
+ this.inspector.selection.setNodeFront(node, reason || "nodeselected");
+ }
+
+ return true;
+ },
+
+ /**
+ * Make sure that every ancestor of the selection are updated
+ * and included in the list of visible children.
+ */
+ _ensureVisible: function (node) {
+ while (node) {
+ let container = this.getContainer(node);
+ let parent = node.parentNode();
+ if (!container.elt.parentNode) {
+ let parentContainer = this.getContainer(parent);
+ if (parentContainer) {
+ parentContainer.childrenDirty = true;
+ this._updateChildren(parentContainer, {expand: true});
+ }
+ }
+
+ node = parent;
+ }
+ return this._waitForChildren();
+ },
+
+ /**
+ * Unmark selected node (no node selected).
+ */
+ unmarkSelectedNode: function () {
+ if (this._selectedContainer) {
+ this._selectedContainer.selected = false;
+ this._selectedContainer = null;
+ }
+ },
+
+ /**
+ * Check if the current selection is a descendent of the container.
+ * if so, make sure it's among the visible set for the container,
+ * and set the dirty flag if needed.
+ *
+ * @return The node that should be made visible, if any.
+ */
+ _checkSelectionVisible: function (container) {
+ let centered = null;
+ let node = this.inspector.selection.nodeFront;
+ while (node) {
+ if (node.parentNode() === container.node) {
+ centered = node;
+ break;
+ }
+ node = node.parentNode();
+ }
+
+ return centered;
+ },
+
+ /**
+ * Make sure all children of the given container's node are
+ * imported and attached to the container in the right order.
+ *
+ * Children need to be updated only in the following circumstances:
+ * a) We just imported this node and have never seen its children.
+ * container.childrenDirty will be set by importNode in this case.
+ * b) We received a childList mutation on the node.
+ * container.childrenDirty will be set in that case too.
+ * c) We have changed the selection, and the path to that selection
+ * wasn't loaded in a previous children request (because we only
+ * grab a subset).
+ * container.childrenDirty should be set in that case too!
+ *
+ * @param {MarkupContainer} container
+ * The markup container whose children need updating
+ * @param {Object} options
+ * Options are {expand:boolean,flash:boolean}
+ * @return {Promise} that will be resolved when the children are ready
+ * (which may be immediately).
+ */
+ _updateChildren: function (container, options) {
+ let expand = options && options.expand;
+ let flash = options && options.flash;
+
+ container.hasChildren = container.node.hasChildren;
+ // Accessibility should either ignore empty children or semantically
+ // consider them a group.
+ container.setChildrenRole();
+
+ if (!this._queuedChildUpdates) {
+ this._queuedChildUpdates = new Map();
+ }
+
+ if (this._queuedChildUpdates.has(container)) {
+ return this._queuedChildUpdates.get(container);
+ }
+
+ if (!container.childrenDirty) {
+ return promise.resolve(container);
+ }
+
+ if (container.inlineTextChild
+ && container.inlineTextChild != container.node.inlineTextChild) {
+ // This container was doing double duty as a container for a single
+ // text child, back that out.
+ this._containers.delete(container.inlineTextChild);
+ container.clearInlineTextChild();
+
+ if (container.hasChildren && container.selected) {
+ container.setExpanded(true);
+ }
+ }
+
+ if (container.node.inlineTextChild) {
+ container.setExpanded(false);
+ // this container will do double duty as the container for the single
+ // text child.
+ while (container.children.firstChild) {
+ container.children.removeChild(container.children.firstChild);
+ }
+
+ container.setInlineTextChild(container.node.inlineTextChild);
+
+ this._containers.set(container.node.inlineTextChild, container);
+ container.childrenDirty = false;
+ return promise.resolve(container);
+ }
+
+ if (!container.hasChildren) {
+ while (container.children.firstChild) {
+ container.children.removeChild(container.children.firstChild);
+ }
+ container.childrenDirty = false;
+ container.setExpanded(false);
+ return promise.resolve(container);
+ }
+
+ // If we're not expanded (or asked to update anyway), we're done for
+ // now. Note that this will leave the childrenDirty flag set, so when
+ // expanded we'll refresh the child list.
+ if (!(container.expanded || expand)) {
+ return promise.resolve(container);
+ }
+
+ // We're going to issue a children request, make sure it includes the
+ // centered node.
+ let centered = this._checkSelectionVisible(container);
+
+ // Children aren't updated yet, but clear the childrenDirty flag anyway.
+ // If the dirty flag is re-set while we're fetching we'll need to fetch
+ // again.
+ container.childrenDirty = false;
+ let updatePromise =
+ this._getVisibleChildren(container, centered).then(children => {
+ if (!this._containers) {
+ return promise.reject("markup view destroyed");
+ }
+ this._queuedChildUpdates.delete(container);
+
+ // If children are dirty, we got a change notification for this node
+ // while the request was in progress, we need to do it again.
+ if (container.childrenDirty) {
+ return this._updateChildren(container, {expand: centered});
+ }
+
+ let fragment = this.doc.createDocumentFragment();
+
+ for (let child of children.nodes) {
+ let childContainer = this.importNode(child, flash);
+ fragment.appendChild(childContainer.elt);
+ }
+
+ while (container.children.firstChild) {
+ container.children.removeChild(container.children.firstChild);
+ }
+
+ if (!(children.hasFirst && children.hasLast)) {
+ let nodesCount = container.node.numChildren;
+ let showAllString = PluralForm.get(nodesCount,
+ INSPECTOR_L10N.getStr("markupView.more.showAll2"));
+ let data = {
+ showing: INSPECTOR_L10N.getStr("markupView.more.showing"),
+ showAll: showAllString.replace("#1", nodesCount),
+ allButtonClick: () => {
+ container.maxChildren = -1;
+ container.childrenDirty = true;
+ this._updateChildren(container);
+ }
+ };
+
+ if (!children.hasFirst) {
+ let span = this.template("more-nodes", data);
+ fragment.insertBefore(span, fragment.firstChild);
+ }
+ if (!children.hasLast) {
+ let span = this.template("more-nodes", data);
+ fragment.appendChild(span);
+ }
+ }
+
+ container.children.appendChild(fragment);
+ return container;
+ }).catch(this._handleRejectionIfNotDestroyed);
+ this._queuedChildUpdates.set(container, updatePromise);
+ return updatePromise;
+ },
+
+ _waitForChildren: function () {
+ if (!this._queuedChildUpdates) {
+ return promise.resolve(undefined);
+ }
+
+ return promise.all([...this._queuedChildUpdates.values()]);
+ },
+
+ /**
+ * Return a list of the children to display for this container.
+ */
+ _getVisibleChildren: function (container, centered) {
+ let maxChildren = container.maxChildren || this.maxChildren;
+ if (maxChildren == -1) {
+ maxChildren = undefined;
+ }
+
+ return this.walker.children(container.node, {
+ maxNodes: maxChildren,
+ center: centered
+ });
+ },
+
+ /**
+ * Tear down the markup panel.
+ */
+ destroy: function () {
+ if (this._destroyer) {
+ return this._destroyer;
+ }
+
+ this._destroyer = promise.resolve();
+
+ this._clearBriefBoxModelTimer();
+
+ this._hoveredNode = null;
+
+ this.htmlEditor.destroy();
+ this.htmlEditor = null;
+
+ this.undo.destroy();
+ this.undo = null;
+
+ this.popup.destroy();
+ this.popup = null;
+
+ this._elt.removeEventListener("click", this._onMouseClick, false);
+ this._elt.removeEventListener("mousemove", this._onMouseMove, false);
+ this._elt.removeEventListener("mouseout", this._onMouseOut, false);
+ this._elt.removeEventListener("blur", this._onBlur, true);
+ this.win.removeEventListener("mouseup", this._onMouseUp);
+ this.win.removeEventListener("copy", this._onCopy);
+ this._frame.removeEventListener("focus", this._onFocus, false);
+ this.walker.off("mutations", this._mutationObserver);
+ this.walker.off("display-change", this._onDisplayChange);
+ this.inspector.selection.off("new-node-front", this._onNewSelection);
+ this.toolbox.off("picker-node-hovered",
+ this._onToolboxPickerHover);
+
+ this._prefObserver.off(ATTR_COLLAPSE_ENABLED_PREF,
+ this._onCollapseAttributesPrefChange);
+ this._prefObserver.off(ATTR_COLLAPSE_LENGTH_PREF,
+ this._onCollapseAttributesPrefChange);
+ this._prefObserver.destroy();
+
+ this._elt = null;
+
+ for (let [, container] of this._containers) {
+ container.destroy();
+ }
+ this._containers = null;
+
+ this.eventDetailsTooltip.destroy();
+ this.eventDetailsTooltip = null;
+
+ this.imagePreviewTooltip.destroy();
+ this.imagePreviewTooltip = null;
+
+ this.win = null;
+ this.doc = null;
+
+ this._lastDropTarget = null;
+ this._lastDragTarget = null;
+
+ return this._destroyer;
+ },
+
+ /**
+ * Find the closest element with class tag-line. These are used to indicate
+ * drag and drop targets.
+ *
+ * @param {DOMNode} el
+ * @return {DOMNode}
+ */
+ findClosestDragDropTarget: function (el) {
+ return el.classList.contains("tag-line")
+ ? el
+ : el.querySelector(".tag-line") || el.closest(".tag-line");
+ },
+
+ /**
+ * Takes an element as it's only argument and marks the element
+ * as the drop target
+ */
+ indicateDropTarget: function (el) {
+ if (this._lastDropTarget) {
+ this._lastDropTarget.classList.remove("drop-target");
+ }
+
+ if (!el) {
+ return;
+ }
+
+ let target = this.findClosestDragDropTarget(el);
+ if (target) {
+ target.classList.add("drop-target");
+ this._lastDropTarget = target;
+ }
+ },
+
+ /**
+ * Takes an element to mark it as indicator of dragging target's initial place
+ */
+ indicateDragTarget: function (el) {
+ if (this._lastDragTarget) {
+ this._lastDragTarget.classList.remove("drag-target");
+ }
+
+ if (!el) {
+ return;
+ }
+
+ let target = this.findClosestDragDropTarget(el);
+ if (target) {
+ target.classList.add("drag-target");
+ this._lastDragTarget = target;
+ }
+ },
+
+ /**
+ * Used to get the nodes required to modify the markup after dragging the
+ * element (parent/nextSibling).
+ */
+ get dropTargetNodes() {
+ let target = this._lastDropTarget;
+
+ if (!target) {
+ return null;
+ }
+
+ let parent, nextSibling;
+
+ if (target.previousElementSibling &&
+ target.previousElementSibling.nodeName.toLowerCase() === "ul") {
+ parent = target.parentNode.container.node;
+ nextSibling = null;
+ } else {
+ parent = target.parentNode.container.node.parentNode();
+ nextSibling = target.parentNode.container.node;
+ }
+
+ if (nextSibling && nextSibling.isBeforePseudoElement) {
+ nextSibling = target.parentNode.parentNode.children[1].container.node;
+ }
+ if (nextSibling && nextSibling.isAfterPseudoElement) {
+ parent = target.parentNode.container.node.parentNode();
+ nextSibling = null;
+ }
+
+ if (parent.nodeType !== nodeConstants.ELEMENT_NODE) {
+ return null;
+ }
+
+ return {parent, nextSibling};
+ }
+};
+
+/**
+ * Map a number from one range to another.
+ */
+function map(value, oldMin, oldMax, newMin, newMax) {
+ let ratio = oldMax - oldMin;
+ if (ratio == 0) {
+ return value;
+ }
+ return newMin + (newMax - newMin) * ((value - oldMin) / ratio);
+}
+
+module.exports = MarkupView;
diff --git a/devtools/client/inspector/markup/markup.xhtml b/devtools/client/inspector/markup/markup.xhtml
new file mode 100644
index 000000000..88b06aadd
--- /dev/null
+++ b/devtools/client/inspector/markup/markup.xhtml
@@ -0,0 +1,105 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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/. -->
+<!DOCTYPE html>
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <link rel="stylesheet" href="chrome://devtools/skin/markup.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://devtools/content/sourceeditor/codemirror/lib/codemirror.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://devtools/content/sourceeditor/codemirror/addon/dialog/dialog.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://devtools/content/sourceeditor/codemirror/mozilla.css" type="text/css"/>
+
+ <script type="application/javascript;version=1.8"
+ src="chrome://devtools/content/shared/theme-switching.js"></script>
+ <script type="application/javascript;version=1.8"
+ src="chrome://devtools/content/sourceeditor/codemirror/codemirror.bundle.js"></script>
+
+</head>
+<body class="theme-body devtools-monospace" role="application">
+
+<!-- NOTE THAT WE MAKE EXTENSIVE USE OF HTML COMMENTS IN THIS FILE IN ORDER -->
+<!-- TO MAKE SPANS READABLE WHILST AVOIDING SIGNIFICANT WHITESPACE -->
+
+ <div id="root-wrapper" role="presentation">
+ <div id="root" role="presentation"></div>
+ </div>
+ <div id="templates" style="display:none">
+
+ <ul class="children">
+ <li id="template-elementcontainer" save="${elt}" class="child collapsed" role="presentation">
+ <div save="${tagLine}" id="${id}" class="tag-line" role="treeitem" aria-level="${level}" aria-grabbed="${isDragging}"><!--
+ --><span save="${tagState}" class="tag-state" role="presentation"></span><!--
+ --><span save="${expander}" class="theme-twisty expander" role="presentation"></span><!--
+ --></div>
+ <ul save="${children}" class="children" role="group"></ul>
+ </li>
+
+ <li id="template-textcontainer" save="${elt}" class="child collapsed" role="presentation">
+ <div save="${tagLine}" id="${id}" class="tag-line" role="treeitem" aria-level="${level}" aria-grabbed="${isDragging}"><span save="${tagState}" class="tag-state" role="presentation"></span></div>
+ <ul save="${children}" class="children" role="group"></ul>
+ </li>
+
+ <li id="template-readonlycontainer" save="${elt}" class="child collapsed" role="presentation">
+ <div save="${tagLine}" id="${id}" class="tag-line" role="treeitem" aria-level="${level}" aria-grabbed="${isDragging}"><!--
+ --><span save="${tagState}" class="tag-state" role="presentation"></span><!--
+ --><span save="${expander}" class="theme-twisty expander" role="presentation"></span><!--
+ --></div>
+ <ul save="${children}" class="children" role="group"></ul>
+ </li>
+
+ <li id="template-more-nodes"
+ class="more-nodes devtools-class-comment"
+ save="${elt}"><!--
+ --><span>${showing}</span> <!--
+ --><button href="#" onclick="${allButtonClick}">${showAll}</button>
+ </li>
+ </ul>
+
+ <span id="template-generic" save="${elt}" class="editor"><span save="${tag}" class="tag"></span></span>
+
+ <span id="template-element" save="${elt}" class="editor"><!--
+ --><span class="open">&lt;<!--
+ --><span save="${tag}" class="tag theme-fg-color3" tabindex="-1"></span><!--
+ --><span save="${attrList}"></span><!--
+ --><span save="${newAttr}" class="newattr" tabindex="-1"></span><!--
+ --><span class="closing-bracket">&gt;</span><!--
+ --></span><!--
+ --><span class="close">&lt;/<!--
+ --><span save="${closeTag}" class="tag theme-fg-color3"></span><!--
+ -->&gt;<!--
+ --></span><!--
+ --><div save="${eventNode}" class="markupview-events" data-event="true">ev</div><!--
+ --></span>
+
+ <span id="template-attribute"
+ save="${attr}"
+ data-attr="${attrName}"
+ data-value="${attrValue}"
+ class="attreditor"
+ style="display:none"> <!--
+ --><span class="editable" save="${inner}" tabindex="${tabindex}"><!--
+ --><span save="${name}" class="attr-name theme-fg-color2"></span><!--
+ -->=&quot;<!--
+ --><span save="${val}" class="attr-value theme-fg-color6"></span><!--
+ -->&quot;<!--
+ --></span><!--
+ --></span>
+
+ <span id="template-text" save="${elt}" class="editor text"><!--
+ --><pre save="${value}" style="display:inline-block; white-space: normal;" tabindex="-1"></pre><!--
+ --></span>
+
+ <span id="template-comment"
+ save="${elt}"
+ class="editor comment theme-comment"><!--
+ --><span>&lt;!--</span><!--
+ --><pre save="${value}" style="display:inline-block; white-space: normal;" tabindex="-1"></pre><!--
+ --><span>--&gt;</span><!--
+ --></span>
+
+ </div>
+</body>
+</html>
diff --git a/devtools/client/inspector/markup/moz.build b/devtools/client/inspector/markup/moz.build
new file mode 100644
index 000000000..4d721cc3c
--- /dev/null
+++ b/devtools/client/inspector/markup/moz.build
@@ -0,0 +1,16 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DIRS += [
+ 'views',
+]
+
+DevToolsModules(
+ 'markup.js',
+ 'utils.js',
+)
+
+BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
diff --git a/devtools/client/inspector/markup/test/.eslintrc.js b/devtools/client/inspector/markup/test/.eslintrc.js
new file mode 100644
index 000000000..698ae9181
--- /dev/null
+++ b/devtools/client/inspector/markup/test/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../../../.eslintrc.mochitests.js"
+};
diff --git a/devtools/client/inspector/markup/test/actor_events_form.js b/devtools/client/inspector/markup/test/actor_events_form.js
new file mode 100644
index 000000000..bd1b1e91a
--- /dev/null
+++ b/devtools/client/inspector/markup/test/actor_events_form.js
@@ -0,0 +1,62 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// This test actor is used for testing the addition of custom form data
+// on NodeActor. Custom form property is set when 'form' event is sent
+// by NodeActor actor (see 'onNodeActorForm' method).
+
+const Events = require("sdk/event/core");
+const {ActorClassWithSpec, Actor, FrontClassWithSpec, Front, generateActorSpec} =
+ require("devtools/shared/protocol");
+
+const {NodeActor} = require("devtools/server/actors/inspector");
+
+var eventsSpec = generateActorSpec({
+ typeName: "eventsFormActor",
+
+ methods: {
+ attach: {
+ request: {},
+ response: {}
+ },
+ detach: {
+ request: {},
+ response: {}
+ }
+ }
+});
+
+var EventsFormActor = ActorClassWithSpec(eventsSpec, {
+ initialize: function () {
+ Actor.prototype.initialize.apply(this, arguments);
+ },
+
+ attach: function () {
+ Events.on(NodeActor, "form", this.onNodeActorForm);
+ },
+
+ detach: function () {
+ Events.off(NodeActor, "form", this.onNodeActorForm);
+ },
+
+ onNodeActorForm: function (event) {
+ let nodeActor = event.target;
+ if (nodeActor.rawNode.id == "container") {
+ let form = event.data;
+ form.setFormProperty("test-property", "test-value");
+ }
+ }
+});
+
+var EventsFormFront = FrontClassWithSpec(eventsSpec, {
+ initialize: function (client, form) {
+ Front.prototype.initialize.apply(this, arguments);
+
+ this.actorID = form[EventsFormActor.prototype.typeName];
+ this.manage(this);
+ }
+});
+
+exports.EventsFormFront = EventsFormFront;
diff --git a/devtools/client/inspector/markup/test/browser.ini b/devtools/client/inspector/markup/test/browser.ini
new file mode 100644
index 000000000..3116e4beb
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser.ini
@@ -0,0 +1,155 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ actor_events_form.js
+ doc_markup_anonymous.html
+ doc_markup_dragdrop.html
+ doc_markup_dragdrop_autoscroll_01.html
+ doc_markup_dragdrop_autoscroll_02.html
+ doc_markup_edit.html
+ doc_markup_events1.html
+ doc_markup_events2.html
+ doc_markup_events3.html
+ doc_markup_events_form.html
+ doc_markup_events_jquery.html
+ doc_markup_events-overflow.html
+ doc_markup_flashing.html
+ doc_markup_html_mixed_case.html
+ doc_markup_image_and_canvas.html
+ doc_markup_image_and_canvas_2.html
+ doc_markup_links.html
+ doc_markup_mutation.html
+ doc_markup_navigation.html
+ doc_markup_not_displayed.html
+ doc_markup_pagesize_01.html
+ doc_markup_pagesize_02.html
+ doc_markup_search.html
+ doc_markup_svg_attributes.html
+ doc_markup_toggle.html
+ doc_markup_tooltip.png
+ doc_markup_void_elements.html
+ doc_markup_void_elements.xhtml
+ doc_markup_whitespace.html
+ doc_markup_xul.xul
+ head.js
+ helper_attributes_test_runner.js
+ helper_events_test_runner.js
+ helper_markup_accessibility_navigation.js
+ helper_outerhtml_test_runner.js
+ helper_style_attr_test_runner.js
+ lib_jquery_1.0.js
+ lib_jquery_1.1.js
+ lib_jquery_1.2_min.js
+ lib_jquery_1.3_min.js
+ lib_jquery_1.4_min.js
+ lib_jquery_1.6_min.js
+ lib_jquery_1.7_min.js
+ lib_jquery_1.11.1_min.js
+ lib_jquery_2.1.1_min.js
+ !/devtools/client/commandline/test/helpers.js
+ !/devtools/client/framework/test/shared-head.js
+ !/devtools/client/inspector/test/head.js
+ !/devtools/client/inspector/test/shared-head.js
+ !/devtools/client/shared/test/test-actor.js
+ !/devtools/client/shared/test/test-actor-registry.js
+
+[browser_markup_accessibility_focus_blur.js]
+skip-if = os == "mac" # Full keyboard navigation on OSX only works if Full Keyboard Access setting is set to All Control in System Keyboard Preferences
+[browser_markup_accessibility_navigation.js]
+skip-if = os == "mac" # Full keyboard navigation on OSX only works if Full Keyboard Access setting is set to All Control in System Keyboard Preferences
+[browser_markup_accessibility_navigation_after_edit.js]
+skip-if = os == "mac" # Full keyboard navigation on OSX only works if Full Keyboard Access setting is set to All Control in System Keyboard Preferences
+[browser_markup_accessibility_semantics.js]
+[browser_markup_anonymous_01.js]
+[browser_markup_anonymous_02.js]
+skip-if = e10s # scratchpad.xul is not loading in e10s window
+[browser_markup_anonymous_03.js]
+[browser_markup_anonymous_04.js]
+[browser_markup_copy_image_data.js]
+subsuite = clipboard
+[browser_markup_css_completion_style_attribute_01.js]
+[browser_markup_css_completion_style_attribute_02.js]
+[browser_markup_css_completion_style_attribute_03.js]
+[browser_markup_dragdrop_autoscroll_01.js]
+[browser_markup_dragdrop_autoscroll_02.js]
+[browser_markup_dragdrop_distance.js]
+[browser_markup_dragdrop_draggable.js]
+[browser_markup_dragdrop_dragRootNode.js]
+[browser_markup_dragdrop_escapeKeyPress.js]
+[browser_markup_dragdrop_invalidNodes.js]
+[browser_markup_dragdrop_reorder.js]
+[browser_markup_dragdrop_tooltip.js]
+[browser_markup_events1.js]
+[browser_markup_events2.js]
+[browser_markup_events3.js]
+[browser_markup_events_form.js]
+[browser_markup_events_jquery_1.0.js]
+[browser_markup_events_jquery_1.1.js]
+[browser_markup_events_jquery_1.2.js]
+[browser_markup_events_jquery_1.3.js]
+[browser_markup_events_jquery_1.4.js]
+[browser_markup_events_jquery_1.6.js]
+[browser_markup_events_jquery_1.7.js]
+[browser_markup_events_jquery_1.11.1.js]
+[browser_markup_events_jquery_2.1.1.js]
+[browser_markup_events-overflow.js]
+skip-if = true # Bug 1177550
+[browser_markup_events-windowed-host.js]
+[browser_markup_links_01.js]
+[browser_markup_links_02.js]
+[browser_markup_links_03.js]
+[browser_markup_links_04.js]
+subsuite = clipboard
+[browser_markup_links_05.js]
+[browser_markup_links_06.js]
+[browser_markup_links_07.js]
+[browser_markup_load_01.js]
+[browser_markup_html_edit_01.js]
+[browser_markup_html_edit_02.js]
+[browser_markup_html_edit_03.js]
+[browser_markup_image_tooltip.js]
+[browser_markup_image_tooltip_mutations.js]
+[browser_markup_keybindings_01.js]
+[browser_markup_keybindings_02.js]
+[browser_markup_keybindings_03.js]
+[browser_markup_keybindings_04.js]
+[browser_markup_keybindings_delete_attributes.js]
+[browser_markup_keybindings_scrolltonode.js]
+[browser_markup_mutation_01.js]
+[browser_markup_mutation_02.js]
+[browser_markup_navigation.js]
+[browser_markup_node_names.js]
+[browser_markup_node_names_namespaced.js]
+[browser_markup_node_not_displayed_01.js]
+[browser_markup_node_not_displayed_02.js]
+[browser_markup_pagesize_01.js]
+[browser_markup_pagesize_02.js]
+[browser_markup_remove_xul_attributes.js]
+skip-if = e10s # Bug 1036409 - The last selected node isn't reselected
+[browser_markup_search_01.js]
+[browser_markup_tag_edit_01.js]
+[browser_markup_tag_edit_02.js]
+[browser_markup_tag_edit_03.js]
+[browser_markup_tag_edit_04-backspace.js]
+[browser_markup_tag_edit_04-delete.js]
+[browser_markup_tag_edit_05.js]
+[browser_markup_tag_edit_06.js]
+[browser_markup_tag_edit_07.js]
+[browser_markup_tag_edit_08.js]
+[browser_markup_tag_edit_09.js]
+[browser_markup_tag_edit_10.js]
+[browser_markup_tag_edit_11.js]
+[browser_markup_tag_edit_12.js]
+[browser_markup_tag_edit_13-other.js]
+[browser_markup_tag_edit_long-classname.js]
+[browser_markup_textcontent_display.js]
+[browser_markup_textcontent_edit_01.js]
+[browser_markup_textcontent_edit_02.js]
+[browser_markup_toggle_01.js]
+[browser_markup_toggle_02.js]
+[browser_markup_toggle_03.js]
+[browser_markup_update-on-navigtion.js]
+[browser_markup_void_elements_html.js]
+[browser_markup_void_elements_xhtml.js]
+[browser_markup_whitespace.js]
diff --git a/devtools/client/inspector/markup/test/browser_markup_accessibility_focus_blur.js b/devtools/client/inspector/markup/test/browser_markup_accessibility_focus_blur.js
new file mode 100644
index 000000000..7e94669c0
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_accessibility_focus_blur.js
@@ -0,0 +1,59 @@
+/* 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";
+
+// Test inspector markup view handling focus and blur when moving between markup
+// view, its root and other containers, and other parts of inspector.
+
+add_task(function* () {
+ let {inspector, testActor} = yield openInspectorForURL(
+ "data:text/html;charset=utf-8,<h1>foo</h1><span>bar</span>");
+ let markup = inspector.markup;
+ let doc = markup.doc;
+ let win = doc.defaultView;
+
+ let spanContainer = yield getContainerForSelector("span", inspector);
+ let rootContainer = markup.getContainer(markup._rootNode);
+
+ is(doc.activeElement, doc.body,
+ "Keyboard focus by default is on document body");
+
+ yield selectNode("span", inspector);
+
+ is(doc.activeElement, doc.body,
+ "Keyboard focus is still on document body");
+
+ info("Focusing on the test span node using 'Return' key");
+ // Focus on the tree element.
+ rootContainer.elt.focus();
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+
+ is(doc.activeElement, spanContainer.editor.tag,
+ "Keyboard focus should be on tag element of focused container");
+
+ info("Focusing on search box, external to markup view document");
+ yield focusSearchBoxUsingShortcut(inspector.panelWin);
+
+ is(doc.activeElement, doc.body,
+ "Keyboard focus should be removed from focused container");
+
+ info("Selecting the test span node again");
+ yield selectNode("span", inspector);
+
+ is(doc.activeElement, doc.body,
+ "Keyboard focus should again be on document body");
+
+ info("Focusing on the test span node using 'Space' key");
+ // Focus on the tree element.
+ rootContainer.elt.focus();
+ EventUtils.synthesizeKey("VK_SPACE", {}, win);
+
+ is(doc.activeElement, spanContainer.editor.tag,
+ "Keyboard focus should again be on tag element of focused container");
+
+ yield clickOnInspectMenuItem(testActor, "h1");
+ is(doc.activeElement, rootContainer.elt,
+ "When inspect menu item is used keyboard focus should move to tree.");
+});
diff --git a/devtools/client/inspector/markup/test/browser_markup_accessibility_navigation.js b/devtools/client/inspector/markup/test/browser_markup_accessibility_navigation.js
new file mode 100644
index 000000000..41e35afef
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_accessibility_navigation.js
@@ -0,0 +1,277 @@
+/* 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/. */
+/* import-globals-from helper_markup_accessibility_navigation.js */
+
+"use strict";
+
+// Test keyboard navigation accessibility of inspector's markup view.
+
+loadHelperScript("helper_markup_accessibility_navigation.js");
+
+/**
+ * Test data has the format of:
+ * {
+ * desc {String} description for better logging
+ * key {String} key event's key
+ * options {?Object} optional event data such as shiftKey, etc
+ * focused {String} path to expected focused element relative to
+ * its container
+ * activedescendant {String} path to expected aria-activedescendant element
+ * relative to its container
+ * waitFor {String} optional event to wait for if keyboard actions
+ * result in asynchronous updates
+ * }
+ */
+const TESTS = [
+ {
+ desc: "Collapse body container",
+ focused: "root.elt",
+ activedescendant: "body.tagLine",
+ key: "VK_LEFT",
+ options: { },
+ waitFor: "collapsed"
+ },
+ {
+ desc: "Expand body container",
+ focused: "root.elt",
+ activedescendant: "body.tagLine",
+ key: "VK_RIGHT",
+ options: { },
+ waitFor: "expanded"
+ },
+ {
+ desc: "Select header container",
+ focused: "root.elt",
+ activedescendant: "header.tagLine",
+ key: "VK_DOWN",
+ options: { },
+ waitFor: "inspector-updated"
+ },
+ {
+ desc: "Expand header container",
+ focused: "root.elt",
+ activedescendant: "header.tagLine",
+ key: "VK_RIGHT",
+ options: { },
+ waitFor: "expanded"
+ },
+ {
+ desc: "Select text container",
+ focused: "root.elt",
+ activedescendant: "container-0.tagLine",
+ key: "VK_DOWN",
+ options: { },
+ waitFor: "inspector-updated"
+ },
+ {
+ desc: "Select header container again",
+ focused: "root.elt",
+ activedescendant: "header.tagLine",
+ key: "VK_UP",
+ options: { },
+ waitFor: "inspector-updated"
+ },
+ {
+ desc: "Collapse header container",
+ focused: "root.elt",
+ activedescendant: "header.tagLine",
+ key: "VK_LEFT",
+ options: { },
+ waitFor: "collapsed"
+ },
+ {
+ desc: "Focus on header container tag",
+ focused: "header.focusableElms.0",
+ activedescendant: "header.tagLine",
+ key: "VK_RETURN",
+ options: { }
+ },
+ {
+ desc: "Remove focus from header container tag",
+ focused: "root.elt",
+ activedescendant: "header.tagLine",
+ key: "VK_ESCAPE",
+ options: { }
+ },
+ {
+ desc: "Focus on header container tag again",
+ focused: "header.focusableElms.0",
+ activedescendant: "header.tagLine",
+ key: "VK_SPACE",
+ options: { }
+ },
+ {
+ desc: "Focus on header id attribute",
+ focused: "header.focusableElms.1",
+ activedescendant: "header.tagLine",
+ key: "VK_TAB",
+ options: { }
+ },
+ {
+ desc: "Focus on header class attribute",
+ focused: "header.focusableElms.2",
+ activedescendant: "header.tagLine",
+ key: "VK_TAB",
+ options: { }
+ },
+ {
+ desc: "Focus on header new attribute",
+ focused: "header.focusableElms.3",
+ activedescendant: "header.tagLine",
+ key: "VK_TAB",
+ options: { }
+ },
+ {
+ desc: "Circle back and focus on header tag again",
+ focused: "header.focusableElms.0",
+ activedescendant: "header.tagLine",
+ key: "VK_TAB",
+ options: { }
+ },
+ {
+ desc: "Circle back and focus on header new attribute again",
+ focused: "header.focusableElms.3",
+ activedescendant: "header.tagLine",
+ key: "VK_TAB",
+ options: { shiftKey: true }
+ },
+ {
+ desc: "Tab back and focus on header class attribute",
+ focused: "header.focusableElms.2",
+ activedescendant: "header.tagLine",
+ key: "VK_TAB",
+ options: { shiftKey: true }
+ },
+ {
+ desc: "Tab back and focus on header id attribute",
+ focused: "header.focusableElms.1",
+ activedescendant: "header.tagLine",
+ key: "VK_TAB",
+ options: { shiftKey: true }
+ },
+ {
+ desc: "Tab back and focus on header tag",
+ focused: "header.focusableElms.0",
+ activedescendant: "header.tagLine",
+ key: "VK_TAB",
+ options: { shiftKey: true }
+ },
+ {
+ desc: "Expand header container, ensure that focus is still on header tag",
+ focused: "header.focusableElms.0",
+ activedescendant: "header.tagLine",
+ key: "VK_RIGHT",
+ options: { },
+ waitFor: "expanded"
+ },
+ {
+ desc: "Activate header tag editor",
+ focused: "header.editor.tag.inplaceEditor.input",
+ activedescendant: "header.tagLine",
+ key: "VK_RETURN",
+ options: { }
+ },
+ {
+ desc: "Activate header id attribute editor",
+ focused: "header.editor.attrList.children.0.children.1.inplaceEditor.input",
+ activedescendant: "header.tagLine",
+ key: "VK_TAB",
+ options: { }
+ },
+ {
+ desc: "Deselect text in header id attribute editor",
+ focused: "header.editor.attrList.children.0.children.1.inplaceEditor.input",
+ activedescendant: "header.tagLine",
+ key: "VK_TAB",
+ options: { }
+ },
+ {
+ desc: "Activate header class attribute editor",
+ focused: "header.editor.attrList.children.1.children.1.inplaceEditor.input",
+ activedescendant: "header.tagLine",
+ key: "VK_TAB",
+ options: { }
+ },
+ {
+ desc: "Deselect text in header class attribute editor",
+ focused: "header.editor.attrList.children.1.children.1.inplaceEditor.input",
+ activedescendant: "header.tagLine",
+ key: "VK_TAB",
+ options: { }
+ },
+ {
+ desc: "Activate header new attribute editor",
+ focused: "header.editor.newAttr.inplaceEditor.input",
+ activedescendant: "header.tagLine",
+ key: "VK_TAB",
+ options: { }
+ },
+ {
+ desc: "Circle back and activate header tag editor again",
+ focused: "header.editor.tag.inplaceEditor.input",
+ activedescendant: "header.tagLine",
+ key: "VK_TAB",
+ options: { }
+ },
+ {
+ desc: "Circle back and activate header new attribute editor again",
+ focused: "header.editor.newAttr.inplaceEditor.input",
+ activedescendant: "header.tagLine",
+ key: "VK_TAB",
+ options: { shiftKey: true }
+ },
+ {
+ desc: "Exit edit mode and keep focus on header new attribute",
+ focused: "header.focusableElms.3",
+ activedescendant: "header.tagLine",
+ key: "VK_ESCAPE",
+ options: { }
+ },
+ {
+ desc: "Move the selection to body and reset focus to container tree",
+ focused: "docBody",
+ activedescendant: "body.tagLine",
+ key: "VK_UP",
+ options: { },
+ waitFor: "inspector-updated"
+ },
+];
+
+let containerID = 0;
+let elms = {};
+
+add_task(function* () {
+ let { inspector } = yield openInspectorForURL(`data:text/html;charset=utf-8,
+ <h1 id="some-id" class="some-class">foo<span>Child span<span></h1>`);
+
+ // Record containers that are created after inspector is initialized to be
+ // useful in testing.
+ inspector.on("container-created", memorizeContainer);
+ registerCleanupFunction(() => {
+ inspector.off("container-created", memorizeContainer);
+ });
+
+ elms.docBody = inspector.markup.doc.body;
+ elms.root = inspector.markup.getContainer(inspector.markup._rootNode);
+ elms.header = yield getContainerForSelector("h1", inspector);
+ elms.body = yield getContainerForSelector("body", inspector);
+
+ // Initial focus is on root element and active descendant should be set on
+ // body tag line.
+ testNavigationState(inspector, elms, elms.docBody, elms.body.tagLine);
+
+ // Focus on the tree element.
+ elms.root.elt.focus();
+
+ for (let testData of TESTS) {
+ yield runAccessibilityNavigationTest(inspector, elms, testData);
+ }
+
+ elms = null;
+});
+
+// Record all containers that are created dynamically into elms object.
+function memorizeContainer(event, container) {
+ elms[`container-${containerID++}`] = container;
+}
diff --git a/devtools/client/inspector/markup/test/browser_markup_accessibility_navigation_after_edit.js b/devtools/client/inspector/markup/test/browser_markup_accessibility_navigation_after_edit.js
new file mode 100644
index 000000000..ec217db09
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_accessibility_navigation_after_edit.js
@@ -0,0 +1,126 @@
+/* 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/. */
+/* import-globals-from helper_markup_accessibility_navigation.js */
+
+"use strict";
+
+// Test keyboard navigation accessibility is preserved after editing attributes.
+
+loadHelperScript("helper_markup_accessibility_navigation.js");
+
+const TEST_URI = '<div id="some-id" class="some-class"></div>';
+
+/**
+ * Test data has the format of:
+ * {
+ * desc {String} description for better logging
+ * key {String} key event's key
+ * options {?Object} optional event data such as shiftKey, etc
+ * focused {String} path to expected focused element relative to
+ * its container
+ * activedescendant {String} path to expected aria-activedescendant element
+ * relative to its container
+ * waitFor {String} optional event to wait for if keyboard actions
+ * result in asynchronous updates
+ * }
+ */
+const TESTS = [
+ {
+ desc: "Select header container",
+ focused: "root.elt",
+ activedescendant: "div.tagLine",
+ key: "VK_DOWN",
+ options: { },
+ waitFor: "inspector-updated"
+ },
+ {
+ desc: "Focus on header tag",
+ focused: "div.focusableElms.0",
+ activedescendant: "div.tagLine",
+ key: "VK_RETURN",
+ options: { }
+ },
+ {
+ desc: "Activate header tag editor",
+ focused: "div.editor.tag.inplaceEditor.input",
+ activedescendant: "div.tagLine",
+ key: "VK_RETURN",
+ options: { }
+ },
+ {
+ desc: "Activate header id attribute editor",
+ focused: "div.editor.attrList.children.0.children.1.inplaceEditor.input",
+ activedescendant: "div.tagLine",
+ key: "VK_TAB",
+ options: { }
+ },
+ {
+ desc: "Deselect text in header id attribute editor",
+ focused: "div.editor.attrList.children.0.children.1.inplaceEditor.input",
+ activedescendant: "div.tagLine",
+ key: "VK_TAB",
+ options: { }
+ },
+ {
+ desc: "Move the cursor to the left",
+ focused: "div.editor.attrList.children.0.children.1.inplaceEditor.input",
+ activedescendant: "div.tagLine",
+ key: "VK_LEFT",
+ options: { }
+ },
+ {
+ desc: "Modify the attribute",
+ focused: "div.editor.attrList.children.0.children.1.inplaceEditor.input",
+ activedescendant: "div.tagLine",
+ key: "A",
+ options: { }
+ },
+ {
+ desc: "Commit the attribute change",
+ focused: "div.focusableElms.1",
+ activedescendant: "div.tagLine",
+ key: "VK_RETURN",
+ options: { },
+ waitFor: "inspector-updated"
+ },
+ {
+ desc: "Tab and focus on header class attribute",
+ focused: "div.focusableElms.2",
+ activedescendant: "div.tagLine",
+ key: "VK_TAB",
+ options: { }
+ },
+ {
+ desc: "Tab and focus on header new attribute node",
+ focused: "div.focusableElms.3",
+ activedescendant: "div.tagLine",
+ key: "VK_TAB",
+ options: { }
+ },
+];
+
+let elms = {};
+
+add_task(function* () {
+ let url = `data:text/html;charset=utf-8,${TEST_URI}`;
+ let { inspector } = yield openInspectorForURL(url);
+
+ elms.docBody = inspector.markup.doc.body;
+ elms.root = inspector.markup.getContainer(inspector.markup._rootNode);
+ elms.div = yield getContainerForSelector("div", inspector);
+ elms.body = yield getContainerForSelector("body", inspector);
+
+ // Initial focus is on root element and active descendant should be set on
+ // body tag line.
+ testNavigationState(inspector, elms, elms.docBody, elms.body.tagLine);
+
+ // Focus on the tree element.
+ elms.root.elt.focus();
+
+ for (let testData of TESTS) {
+ yield runAccessibilityNavigationTest(inspector, elms, testData);
+ }
+
+ elms = null;
+});
diff --git a/devtools/client/inspector/markup/test/browser_markup_accessibility_semantics.js b/devtools/client/inspector/markup/test/browser_markup_accessibility_semantics.js
new file mode 100644
index 000000000..b38a68c10
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_accessibility_semantics.js
@@ -0,0 +1,100 @@
+/* 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";
+
+// Test that inspector markup view has all expected ARIA properties set and
+// updated.
+
+const TOP_CONTAINER_LEVEL = 3;
+
+add_task(function* () {
+ let {inspector} = yield openInspectorForURL(`
+ data:text/html;charset=utf-8,
+ <h1>foo</h1>
+ <span>bar</span>
+ <ul>
+ <li></li>
+ </ul>`);
+ let markup = inspector.markup;
+ let doc = markup.doc;
+ let win = doc.defaultView;
+
+ let rootElt = markup.getContainer(markup._rootNode).elt;
+ let bodyContainer = yield getContainerForSelector("body", inspector);
+ let spanContainer = yield getContainerForSelector("span", inspector);
+ let headerContainer = yield getContainerForSelector("h1", inspector);
+ let listContainer = yield getContainerForSelector("ul", inspector);
+
+ // Focus on the tree element.
+ rootElt.focus();
+
+ // Test tree related semantics
+ is(rootElt.getAttribute("role"), "tree",
+ "Root container should have tree semantics");
+ is(rootElt.getAttribute("aria-dropeffect"), "none",
+ "By default root container's drop effect should be set to none");
+ is(rootElt.getAttribute("aria-activedescendant"),
+ bodyContainer.tagLine.getAttribute("id"),
+ "Default active descendant should be set to body");
+ is(bodyContainer.tagLine.getAttribute("aria-level"), TOP_CONTAINER_LEVEL - 1,
+ "Body container tagLine should have nested level up to date");
+ [spanContainer, headerContainer, listContainer].forEach(container => {
+ let treeitem = container.tagLine;
+ is(treeitem.getAttribute("role"), "treeitem",
+ "Child container tagLine elements should have tree item semantics");
+ is(treeitem.getAttribute("aria-level"), TOP_CONTAINER_LEVEL,
+ "Child container tagLine should have nested level up to date");
+ is(treeitem.getAttribute("aria-grabbed"), "false",
+ "Child container should be draggable but not grabbed by default");
+ is(container.children.getAttribute("role"), "group",
+ "Container with children should have its children element have group " +
+ "semantics");
+ ok(treeitem.id, "Tree item should have id assigned");
+ if (container.closeTagLine) {
+ is(container.closeTagLine.getAttribute("role"), "presentation",
+ "Ignore closing tag");
+ }
+ if (container.expander) {
+ is(container.expander.getAttribute("role"), "presentation",
+ "Ignore expander");
+ }
+ });
+
+ // Test expanding/expandable semantics
+ ok(!spanContainer.tagLine.hasAttribute("aria-expanded"),
+ "Non expandable tree items should not have aria-expanded attribute");
+ ok(!headerContainer.tagLine.hasAttribute("aria-expanded"),
+ "Non expandable tree items should not have aria-expanded attribute");
+ is(listContainer.tagLine.getAttribute("aria-expanded"), "false",
+ "Closed tree item should have aria-expanded unset");
+
+ info("Selecting and expanding list container");
+ let updated = waitForMultipleChildrenUpdates(inspector);
+ yield selectNode("ul", inspector);
+ EventUtils.synthesizeKey("VK_RIGHT", {}, win);
+ yield updated;
+
+ is(rootElt.getAttribute("aria-activedescendant"),
+ listContainer.tagLine.getAttribute("id"),
+ "Active descendant should not be set to list container tagLine");
+ is(listContainer.tagLine.getAttribute("aria-expanded"), "true",
+ "Open tree item should have aria-expanded set");
+ let listItemContainer = yield getContainerForSelector("li", inspector);
+ is(listItemContainer.tagLine.getAttribute("aria-level"),
+ TOP_CONTAINER_LEVEL + 1,
+ "Grand child container tagLine should have nested level up to date");
+ is(listItemContainer.children.getAttribute("role"), "presentation",
+ "Container with no children should have its children element ignored by " +
+ "accessibility");
+
+ info("Collapsing list container");
+ updated = waitForMultipleChildrenUpdates(inspector);
+ EventUtils.synthesizeKey("VK_LEFT", {}, win);
+ yield updated;
+
+ is(listContainer.tagLine.getAttribute("aria-expanded"), "false",
+ "Closed tree item should have aria-expanded unset");
+});
+
diff --git a/devtools/client/inspector/markup/test/browser_markup_anonymous_01.js b/devtools/client/inspector/markup/test/browser_markup_anonymous_01.js
new file mode 100644
index 000000000..fd32251d0
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_anonymous_01.js
@@ -0,0 +1,44 @@
+/* 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";
+
+// Test native anonymous content in the markupview.
+const TEST_URL = URL_ROOT + "doc_markup_anonymous.html";
+
+add_task(function* () {
+ let {inspector} = yield openInspectorForURL(TEST_URL);
+
+ let pseudo = yield getNodeFront("#pseudo", inspector);
+
+ // Markup looks like: <div><::before /><span /><::after /></div>
+ let children = yield inspector.walker.children(pseudo);
+ is(children.nodes.length, 3, "Children returned from walker");
+
+ info("Checking the ::before pseudo element");
+ let before = children.nodes[0];
+ yield isEditingMenuDisabled(before, inspector);
+
+ info("Checking the normal child element");
+ let span = children.nodes[1];
+ yield isEditingMenuEnabled(span, inspector);
+
+ info("Checking the ::after pseudo element");
+ let after = children.nodes[2];
+ yield isEditingMenuDisabled(after, inspector);
+
+ let native = yield getNodeFront("#native", inspector);
+
+ // Markup looks like: <div><video controls /></div>
+ let nativeChildren = yield inspector.walker.children(native);
+ is(nativeChildren.nodes.length, 1, "Children returned from walker");
+
+ info("Checking the video element");
+ let video = nativeChildren.nodes[0];
+ ok(!video.isAnonymous, "<video> is not anonymous");
+
+ let videoChildren = yield inspector.walker.children(video);
+ is(videoChildren.nodes.length, 0,
+ "No native children returned from walker for <video> by default");
+});
diff --git a/devtools/client/inspector/markup/test/browser_markup_anonymous_02.js b/devtools/client/inspector/markup/test/browser_markup_anonymous_02.js
new file mode 100644
index 000000000..b6221c5c3
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_anonymous_02.js
@@ -0,0 +1,31 @@
+/* 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";
+
+requestLongerTimeout(2);
+
+// Test XBL anonymous content in the markupview
+const TEST_URL = "chrome://devtools/content/scratchpad/scratchpad.xul";
+
+add_task(function* () {
+ let {inspector} = yield openInspectorForURL(TEST_URL);
+
+ let toolbarbutton = yield getNodeFront("toolbarbutton", inspector);
+ let children = yield inspector.walker.children(toolbarbutton);
+
+ is(toolbarbutton.numChildren, 3, "Correct number of children");
+ is(children.nodes.length, 3, "Children returned from walker");
+
+ is(toolbarbutton.isAnonymous, false, "Toolbarbutton is not anonymous");
+ yield isEditingMenuEnabled(toolbarbutton, inspector);
+
+ for (let node of children.nodes) {
+ ok(node.isAnonymous, "Child is anonymous");
+ ok(node._form.isXBLAnonymous, "Child is XBL anonymous");
+ ok(!node._form.isShadowAnonymous, "Child is not shadow anonymous");
+ ok(!node._form.isNativeAnonymous, "Child is not native anonymous");
+ yield isEditingMenuDisabled(node, inspector);
+ }
+});
diff --git a/devtools/client/inspector/markup/test/browser_markup_anonymous_03.js b/devtools/client/inspector/markup/test/browser_markup_anonymous_03.js
new file mode 100644
index 000000000..010ce06e0
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_anonymous_03.js
@@ -0,0 +1,34 @@
+/* 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";
+
+// Test shadow DOM content in the markupview.
+// Note that many features are not yet enabled, but basic listing
+// of elements should be working.
+const TEST_URL = URL_ROOT + "doc_markup_anonymous.html";
+
+add_task(function* () {
+ Services.prefs.setBoolPref("dom.webcomponents.enabled", true);
+
+ let {inspector} = yield openInspectorForURL(TEST_URL);
+
+ let shadow = yield getNodeFront("#shadow", inspector.markup);
+ let children = yield inspector.walker.children(shadow);
+
+ is(shadow.numChildren, 3, "Children of the shadow root are counted");
+ is(children.nodes.length, 3, "Children returned from walker");
+
+ info("Checking the ::before pseudo element");
+ let before = children.nodes[0];
+ yield isEditingMenuDisabled(before, inspector);
+
+ info("Checking the <h3> shadow element");
+ let shadowChild1 = children.nodes[1];
+ yield isEditingMenuDisabled(shadowChild1, inspector);
+
+ info("Checking the <select> shadow element");
+ let shadowChild2 = children.nodes[2];
+ yield isEditingMenuDisabled(shadowChild2, inspector);
+});
diff --git a/devtools/client/inspector/markup/test/browser_markup_anonymous_04.js b/devtools/client/inspector/markup/test/browser_markup_anonymous_04.js
new file mode 100644
index 000000000..da5e4567d
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_anonymous_04.js
@@ -0,0 +1,37 @@
+/* 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";
+
+// Test native anonymous content in the markupview with
+// devtools.inspector.showAllAnonymousContent set to true
+const TEST_URL = URL_ROOT + "doc_markup_anonymous.html";
+const PREF = "devtools.inspector.showAllAnonymousContent";
+
+add_task(function* () {
+ Services.prefs.setBoolPref(PREF, true);
+
+ let {inspector} = yield openInspectorForURL(TEST_URL);
+
+ let native = yield getNodeFront("#native", inspector);
+
+ // Markup looks like: <div><video controls /></div>
+ let nativeChildren = yield inspector.walker.children(native);
+ is(nativeChildren.nodes.length, 1, "Children returned from walker");
+
+ info("Checking the video element");
+ let video = nativeChildren.nodes[0];
+ ok(!video.isAnonymous, "<video> is not anonymous");
+
+ let videoChildren = yield inspector.walker.children(video);
+ is(videoChildren.nodes.length, 3, "<video> has native anonymous children");
+
+ for (let node of videoChildren.nodes) {
+ ok(node.isAnonymous, "Child is anonymous");
+ ok(!node._form.isXBLAnonymous, "Child is not XBL anonymous");
+ ok(!node._form.isShadowAnonymous, "Child is not shadow anonymous");
+ ok(node._form.isNativeAnonymous, "Child is native anonymous");
+ yield isEditingMenuDisabled(node, inspector);
+ }
+});
diff --git a/devtools/client/inspector/markup/test/browser_markup_copy_image_data.js b/devtools/client/inspector/markup/test/browser_markup_copy_image_data.js
new file mode 100644
index 000000000..275bff0b7
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_copy_image_data.js
@@ -0,0 +1,67 @@
+/* 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";
+
+// Test that image nodes have the "copy data-uri" contextual menu item enabled
+// and that clicking it puts the image data into the clipboard
+
+add_task(function* () {
+ yield addTab(URL_ROOT + "doc_markup_image_and_canvas.html");
+ let {inspector, testActor} = yield openInspector();
+
+ yield selectNode("div", inspector);
+ yield assertCopyImageDataNotAvailable(inspector);
+
+ yield selectNode("img", inspector);
+ yield assertCopyImageDataAvailable(inspector);
+ let expectedSrc = yield testActor.getAttribute("img", "src");
+ yield triggerCopyImageUrlAndWaitForClipboard(expectedSrc, inspector);
+
+ yield selectNode("canvas", inspector);
+ yield assertCopyImageDataAvailable(inspector);
+ let expectedURL = yield testActor.eval(`
+ content.document.querySelector(".canvas").toDataURL();`);
+ yield triggerCopyImageUrlAndWaitForClipboard(expectedURL, inspector);
+
+ // Check again that the menu isn't available on the DIV (to make sure our
+ // menu updating mechanism works)
+ yield selectNode("div", inspector);
+ yield assertCopyImageDataNotAvailable(inspector);
+});
+
+function* assertCopyImageDataNotAvailable(inspector) {
+ let allMenuItems = openContextMenuAndGetAllItems(inspector);
+ let item = allMenuItems.find(i => i.id === "node-menu-copyimagedatauri");
+
+ ok(item, "The menu item was found in the contextual menu");
+ ok(item.disabled, "The menu item is disabled");
+}
+
+function* assertCopyImageDataAvailable(inspector) {
+ let allMenuItems = openContextMenuAndGetAllItems(inspector);
+ let item = allMenuItems.find(i => i.id === "node-menu-copyimagedatauri");
+
+ ok(item, "The menu item was found in the contextual menu");
+ ok(!item.disabled, "The menu item is enabled");
+}
+
+function triggerCopyImageUrlAndWaitForClipboard(expected, inspector) {
+ let def = defer();
+
+ SimpleTest.waitForClipboard(expected, () => {
+ inspector.markup.getContainer(inspector.selection.nodeFront)
+ .copyImageDataUri();
+ }, () => {
+ ok(true, "The clipboard contains the expected value " +
+ expected.substring(0, 50) + "...");
+ def.resolve();
+ }, () => {
+ ok(false, "The clipboard doesn't contain the expected value " +
+ expected.substring(0, 50) + "...");
+ def.resolve();
+ });
+
+ return def.promise;
+}
diff --git a/devtools/client/inspector/markup/test/browser_markup_css_completion_style_attribute_01.js b/devtools/client/inspector/markup/test/browser_markup_css_completion_style_attribute_01.js
new file mode 100644
index 000000000..f860456d1
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_css_completion_style_attribute_01.js
@@ -0,0 +1,76 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_style_attr_test_runner.js */
+
+"use strict";
+
+// Test CSS state is correctly determined and the corresponding suggestions are
+// displayed. i.e. CSS property suggestions are shown when cursor is like:
+// ```style="di|"``` where | is the cursor; And CSS value suggestion is
+// displayed when the cursor is like: ```style="display:n|"``` properly. No
+// suggestions should ever appear when the attribute is not a style attribute.
+// The correctness and cycling of the suggestions is covered in the ruleview
+// tests.
+
+loadHelperScript("helper_style_attr_test_runner.js");
+
+const TEST_URL = URL_ROOT + "doc_markup_edit.html";
+
+// test data format :
+// [
+// what key to press,
+// expected input box value after keypress,
+// expected input.selectionStart,
+// expected input.selectionEnd,
+// is popup expected to be open ?
+// ]
+const TEST_DATA = [
+ ["s", "s", 1, 1, false],
+ ["t", "st", 2, 2, false],
+ ["y", "sty", 3, 3, false],
+ ["l", "styl", 4, 4, false],
+ ["e", "style", 5, 5, false],
+ ["=", "style=", 6, 6, false],
+ ["\"", "style=\"", 7, 7, false],
+ ["d", "style=\"display", 8, 14, true],
+ ["VK_TAB", "style=\"display", 14, 14, true],
+ ["VK_TAB", "style=\"dominant-baseline", 24, 24, true],
+ ["VK_TAB", "style=\"direction", 16, 16, true],
+ ["click_1", "style=\"display", 14, 14, false],
+ [":", "style=\"display:block", 15, 20, true],
+ ["n", "style=\"display:none", 16, 19, false],
+ ["VK_BACK_SPACE", "style=\"display:n", 16, 16, false],
+ ["VK_BACK_SPACE", "style=\"display:", 15, 15, false],
+ [" ", "style=\"display: block", 16, 21, true],
+ [" ", "style=\"display: block", 17, 22, true],
+ ["i", "style=\"display: inherit", 18, 24, true],
+ ["VK_RIGHT", "style=\"display: inherit", 24, 24, false],
+ [";", "style=\"display: inherit;", 25, 25, false],
+ [" ", "style=\"display: inherit; ", 26, 26, false],
+ [" ", "style=\"display: inherit; ", 27, 27, false],
+ ["VK_LEFT", "style=\"display: inherit; ", 26, 26, false],
+ ["c", "style=\"display: inherit; color ", 27, 31, true],
+ ["VK_RIGHT", "style=\"display: inherit; color ", 31, 31, false],
+ [" ", "style=\"display: inherit; color ", 32, 32, false],
+ ["c", "style=\"display: inherit; color c ", 33, 33, false],
+ ["VK_BACK_SPACE", "style=\"display: inherit; color ", 32, 32, false],
+ [":", "style=\"display: inherit; color :aliceblue ", 33, 42, true],
+ ["c", "style=\"display: inherit; color :cadetblue ", 34, 42, true],
+ ["VK_DOWN", "style=\"display: inherit; color :chartreuse ", 34, 43, true],
+ ["VK_RIGHT", "style=\"display: inherit; color :chartreuse ", 43, 43, false],
+ [" ", "style=\"display: inherit; color :chartreuse aliceblue ",
+ 44, 53, true],
+ ["!", "style=\"display: inherit; color :chartreuse !important; ",
+ 45, 55, false],
+ ["VK_RIGHT", "style=\"display: inherit; color :chartreuse !important; ",
+ 55, 55, false],
+ ["VK_RETURN", "style=\"display: inherit; color :chartreuse !important;\"",
+ -1, -1, false]
+];
+
+add_task(function* () {
+ let {inspector} = yield openInspectorForURL(TEST_URL);
+
+ yield runStyleAttributeAutocompleteTests(inspector, TEST_DATA);
+});
diff --git a/devtools/client/inspector/markup/test/browser_markup_css_completion_style_attribute_02.js b/devtools/client/inspector/markup/test/browser_markup_css_completion_style_attribute_02.js
new file mode 100644
index 000000000..345ee4866
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_css_completion_style_attribute_02.js
@@ -0,0 +1,106 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_style_attr_test_runner.js */
+
+"use strict";
+
+// Test CSS autocompletion of the style attributes stops after closing the
+// attribute using a matching quote.
+
+loadHelperScript("helper_style_attr_test_runner.js");
+
+const TEST_URL = URL_ROOT + "doc_markup_edit.html";
+
+// test data format :
+// [
+// what key to press,
+// expected input box value after keypress,
+// expected input.selectionStart,
+// expected input.selectionEnd,
+// is popup expected to be open ?
+// ]
+const TEST_DATA_DOUBLE = [
+ ["s", "s", 1, 1, false],
+ ["t", "st", 2, 2, false],
+ ["y", "sty", 3, 3, false],
+ ["l", "styl", 4, 4, false],
+ ["e", "style", 5, 5, false],
+ ["=", "style=", 6, 6, false],
+ ["\"", "style=\"", 7, 7, false],
+ ["c", "style=\"color", 8, 12, true],
+ ["VK_RIGHT", "style=\"color", 12, 12, false],
+ [":", "style=\"color:aliceblue", 13, 22, true],
+ ["b", "style=\"color:beige", 14, 18, true],
+ ["VK_RIGHT", "style=\"color:beige", 18, 18, false],
+ ["\"", "style=\"color:beige\"", 19, 19, false],
+ [" ", "style=\"color:beige\" ", 20, 20, false],
+ ["d", "style=\"color:beige\" d", 21, 21, false],
+ ["a", "style=\"color:beige\" da", 22, 22, false],
+ ["t", "style=\"color:beige\" dat", 23, 23, false],
+ ["a", "style=\"color:beige\" data", 24, 24, false],
+ ["VK_RETURN", "style=\"color:beige\"",
+ -1, -1, false]
+];
+
+// Check that single quote attribute is also supported
+const TEST_DATA_SINGLE = [
+ ["s", "s", 1, 1, false],
+ ["t", "st", 2, 2, false],
+ ["y", "sty", 3, 3, false],
+ ["l", "styl", 4, 4, false],
+ ["e", "style", 5, 5, false],
+ ["=", "style=", 6, 6, false],
+ ["'", "style='", 7, 7, false],
+ ["c", "style='color", 8, 12, true],
+ ["VK_RIGHT", "style='color", 12, 12, false],
+ [":", "style='color:aliceblue", 13, 22, true],
+ ["b", "style='color:beige", 14, 18, true],
+ ["VK_RIGHT", "style='color:beige", 18, 18, false],
+ ["'", "style='color:beige'", 19, 19, false],
+ [" ", "style='color:beige' ", 20, 20, false],
+ ["d", "style='color:beige' d", 21, 21, false],
+ ["a", "style='color:beige' da", 22, 22, false],
+ ["t", "style='color:beige' dat", 23, 23, false],
+ ["a", "style='color:beige' data", 24, 24, false],
+ ["VK_RETURN", "style=\"color:beige\"",
+ -1, -1, false]
+];
+
+// Check that autocompletion is still enabled after using url('1)
+const TEST_DATA_INNER = [
+ ["s", "s", 1, 1, false],
+ ["t", "st", 2, 2, false],
+ ["y", "sty", 3, 3, false],
+ ["l", "styl", 4, 4, false],
+ ["e", "style", 5, 5, false],
+ ["=", "style=", 6, 6, false],
+ ["\"", "style=\"", 7, 7, false],
+ ["b", "style=\"border", 8, 13, true],
+ ["a", "style=\"background", 9, 17, true],
+ ["VK_RIGHT", "style=\"background", 17, 17, false],
+ [":", "style=\"background:aliceblue", 18, 27, true],
+ ["u", "style=\"background:unset", 19, 23, true],
+ ["r", "style=\"background:url", 20, 21, false],
+ ["l", "style=\"background:url", 21, 21, false],
+ ["(", "style=\"background:url(", 22, 22, false],
+ ["'", "style=\"background:url('", 23, 23, false],
+ ["1", "style=\"background:url('1", 24, 24, false],
+ ["'", "style=\"background:url('1'", 25, 25, false],
+ [")", "style=\"background:url('1')", 26, 26, false],
+ [";", "style=\"background:url('1');", 27, 27, false],
+ [" ", "style=\"background:url('1'); ", 28, 28, false],
+ ["c", "style=\"background:url('1'); color", 29, 33, true],
+ ["VK_RIGHT", "style=\"background:url('1'); color", 33, 33, false],
+ [":", "style=\"background:url('1'); color:aliceblue", 34, 43, true],
+ ["b", "style=\"background:url('1'); color:beige", 35, 39, true],
+ ["VK_RETURN", "style=\"background:url('1'); color:beige\"", -1, -1, false]
+];
+
+add_task(function* () {
+ let {inspector} = yield openInspectorForURL(TEST_URL);
+
+ yield runStyleAttributeAutocompleteTests(inspector, TEST_DATA_DOUBLE);
+ yield runStyleAttributeAutocompleteTests(inspector, TEST_DATA_SINGLE);
+ yield runStyleAttributeAutocompleteTests(inspector, TEST_DATA_INNER);
+});
diff --git a/devtools/client/inspector/markup/test/browser_markup_css_completion_style_attribute_03.js b/devtools/client/inspector/markup/test/browser_markup_css_completion_style_attribute_03.js
new file mode 100644
index 000000000..3dbc3e6b2
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_css_completion_style_attribute_03.js
@@ -0,0 +1,54 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_style_attr_test_runner.js */
+
+"use strict";
+
+// Test CSS autocompletion of the style attribute can be triggered when the
+// caret is before a non-word character.
+
+loadHelperScript("helper_style_attr_test_runner.js");
+
+const TEST_URL = URL_ROOT + "doc_markup_edit.html";
+
+// test data format :
+// [
+// what key to press,
+// expected input box value after keypress,
+// expected input.selectionStart,
+// expected input.selectionEnd,
+// is popup expected to be open ?
+// ]
+const TEST_DATA = [
+ ["s", "s", 1, 1, false],
+ ["t", "st", 2, 2, false],
+ ["y", "sty", 3, 3, false],
+ ["l", "styl", 4, 4, false],
+ ["e", "style", 5, 5, false],
+ ["=", "style=", 6, 6, false],
+ ["\"", "style=\"", 7, 7, false],
+ ["\"", "style=\"\"", 8, 8, false],
+ ["VK_LEFT", "style=\"\"", 7, 7, false],
+ ["c", "style=\"color\"", 8, 12, true],
+ ["o", "style=\"color\"", 9, 12, true],
+ ["VK_RIGHT", "style=\"color\"", 12, 12, false],
+ [":", "style=\"color:aliceblue\"", 13, 22, true],
+ ["b", "style=\"color:beige\"", 14, 18, true],
+ ["VK_RIGHT", "style=\"color:beige\"", 18, 18, false],
+ [";", "style=\"color:beige;\"", 19, 19, false],
+ [";", "style=\"color:beige;;\"", 20, 20, false],
+ ["VK_LEFT", "style=\"color:beige;;\"", 19, 19, false],
+ ["p", "style=\"color:beige;padding;\"", 20, 26, true],
+ ["VK_RIGHT", "style=\"color:beige;padding;\"", 26, 26, false],
+ [":", "style=\"color:beige;padding:calc;\"", 27, 31, true],
+ ["0", "style=\"color:beige;padding:0;\"", 28, 28, false],
+ ["VK_RETURN", "style=\"color:beige;padding:0;\"",
+ -1, -1, false]
+];
+
+add_task(function* () {
+ let {inspector} = yield openInspectorForURL(TEST_URL);
+
+ yield runStyleAttributeAutocompleteTests(inspector, TEST_DATA);
+});
diff --git a/devtools/client/inspector/markup/test/browser_markup_dragdrop_autoscroll_01.js b/devtools/client/inspector/markup/test/browser_markup_dragdrop_autoscroll_01.js
new file mode 100644
index 000000000..0c25e2fc6
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_dragdrop_autoscroll_01.js
@@ -0,0 +1,51 @@
+/* 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";
+
+// Test that dragging a node near the top or bottom edge of the markup-view
+// auto-scrolls the view on a large toolbox.
+
+const TEST_URL = URL_ROOT + "doc_markup_dragdrop_autoscroll_01.html";
+
+add_task(function* () {
+ // Set the toolbox as large as it would get. The toolbox automatically shrinks
+ // to not overflow to window.
+ yield pushPref("devtools.toolbox.footer.height", 10000);
+
+ let {inspector} = yield openInspectorForURL(TEST_URL);
+ let markup = inspector.markup;
+ let viewHeight = markup.doc.documentElement.clientHeight;
+
+ info("Pretend the markup-view is dragging");
+ markup.isDragging = true;
+
+ info("Simulate a mousemove on the view, at the bottom, and expect scrolling");
+ let onScrolled = waitForScrollStop(markup.doc);
+
+ markup._onMouseMove({
+ preventDefault: () => {},
+ target: markup.doc.body,
+ pageY: viewHeight + markup.doc.defaultView.scrollY
+ });
+
+ let bottomScrollPos = yield onScrolled;
+ ok(bottomScrollPos > 0, "The view was scrolled down");
+
+ info("Simulate a mousemove at the top and expect more scrolling");
+ onScrolled = waitForScrollStop(markup.doc);
+
+ markup._onMouseMove({
+ preventDefault: () => {},
+ target: markup.doc.body,
+ pageY: markup.doc.defaultView.scrollY
+ });
+
+ let topScrollPos = yield onScrolled;
+ ok(topScrollPos < bottomScrollPos, "The view was scrolled up");
+ is(topScrollPos, 0, "The view was scrolled up to the top");
+
+ info("Simulate a mouseup to stop dragging");
+ markup._onMouseUp();
+});
diff --git a/devtools/client/inspector/markup/test/browser_markup_dragdrop_autoscroll_02.js b/devtools/client/inspector/markup/test/browser_markup_dragdrop_autoscroll_02.js
new file mode 100644
index 000000000..4aca6f424
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_dragdrop_autoscroll_02.js
@@ -0,0 +1,49 @@
+/* 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";
+
+// Test that dragging a node near the top or bottom edge of the markup-view
+// auto-scrolls the view on a small toolbox.
+
+const TEST_URL = URL_ROOT + "doc_markup_dragdrop_autoscroll_02.html";
+
+add_task(function* () {
+ // Set the toolbox to very small in size.
+ yield pushPref("devtools.toolbox.footer.height", 150);
+
+ let {inspector} = yield openInspectorForURL(TEST_URL);
+ let markup = inspector.markup;
+ let viewHeight = markup.doc.documentElement.clientHeight;
+
+ info("Pretend the markup-view is dragging");
+ markup.isDragging = true;
+
+ info("Simulate a mousemove on the view, at the bottom, and expect scrolling");
+ let onScrolled = waitForScrollStop(markup.doc);
+
+ markup._onMouseMove({
+ preventDefault: () => {},
+ target: markup.doc.body,
+ pageY: viewHeight + markup.doc.defaultView.scrollY
+ });
+
+ let bottomScrollPos = yield onScrolled;
+ ok(bottomScrollPos > 0, "The view was scrolled down");
+ info("Simulate a mousemove at the top and expect more scrolling");
+ onScrolled = waitForScrollStop(markup.doc);
+
+ markup._onMouseMove({
+ preventDefault: () => {},
+ target: markup.doc.body,
+ pageY: markup.doc.defaultView.scrollY
+ });
+
+ let topScrollPos = yield onScrolled;
+ ok(topScrollPos < bottomScrollPos, "The view was scrolled up");
+ is(topScrollPos, 0, "The view was scrolled up to the top");
+
+ info("Simulate a mouseup to stop dragging");
+ markup._onMouseUp();
+});
diff --git a/devtools/client/inspector/markup/test/browser_markup_dragdrop_distance.js b/devtools/client/inspector/markup/test/browser_markup_dragdrop_distance.js
new file mode 100644
index 000000000..e94b02191
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_dragdrop_distance.js
@@ -0,0 +1,49 @@
+/* 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";
+
+// Test that nodes don't start dragging before the mouse has moved by at least
+// the minimum vertical distance defined in markup-view.js by
+// DRAG_DROP_MIN_INITIAL_DISTANCE.
+
+const TEST_URL = URL_ROOT + "doc_markup_dragdrop.html";
+const TEST_NODE = "#test";
+
+// Keep this in sync with DRAG_DROP_MIN_INITIAL_DISTANCE in markup-view.js
+const MIN_DISTANCE = 10;
+
+add_task(function* () {
+ let {inspector} = yield openInspectorForURL(TEST_URL);
+
+ info("Drag the test node by half of the minimum distance");
+ yield simulateNodeDrag(inspector, TEST_NODE, 0, MIN_DISTANCE / 2);
+ yield checkIsDragging(inspector, TEST_NODE, false);
+
+ info("Drag the test node by exactly the minimum distance");
+ yield simulateNodeDrag(inspector, TEST_NODE, 0, MIN_DISTANCE);
+ yield checkIsDragging(inspector, TEST_NODE, true);
+ inspector.markup.cancelDragging();
+
+ info("Drag the test node by more than the minimum distance");
+ yield simulateNodeDrag(inspector, TEST_NODE, 0, MIN_DISTANCE * 2);
+ yield checkIsDragging(inspector, TEST_NODE, true);
+ inspector.markup.cancelDragging();
+
+ info("Drag the test node by minus the minimum distance");
+ yield simulateNodeDrag(inspector, TEST_NODE, 0, MIN_DISTANCE * -1);
+ yield checkIsDragging(inspector, TEST_NODE, true);
+ inspector.markup.cancelDragging();
+});
+
+function* checkIsDragging(inspector, selector, isDragging) {
+ let container = yield getContainerForSelector(selector, inspector);
+ if (isDragging) {
+ ok(container.isDragging, "The container is being dragged");
+ ok(inspector.markup.isDragging, "And the markup-view knows it");
+ } else {
+ ok(!container.isDragging, "The container hasn't been marked as dragging");
+ ok(!inspector.markup.isDragging, "And the markup-view either");
+ }
+}
diff --git a/devtools/client/inspector/markup/test/browser_markup_dragdrop_dragRootNode.js b/devtools/client/inspector/markup/test/browser_markup_dragdrop_dragRootNode.js
new file mode 100644
index 000000000..8bb4779d5
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_dragdrop_dragRootNode.js
@@ -0,0 +1,22 @@
+/* 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";
+
+// Test that the root node isn't draggable (as well as head and body).
+
+const TEST_URL = URL_ROOT + "doc_markup_dragdrop.html";
+const TEST_DATA = ["html", "head", "body"];
+
+add_task(function* () {
+ let {inspector} = yield openInspectorForURL(TEST_URL);
+
+ for (let selector of TEST_DATA) {
+ info("Try to drag/drop node " + selector);
+ yield simulateNodeDrag(inspector, selector);
+
+ let container = yield getContainerForSelector(selector, inspector);
+ ok(!container.isDragging, "The container hasn't been marked as dragging");
+ }
+});
diff --git a/devtools/client/inspector/markup/test/browser_markup_dragdrop_draggable.js b/devtools/client/inspector/markup/test/browser_markup_dragdrop_draggable.js
new file mode 100644
index 000000000..1853ab4f7
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_dragdrop_draggable.js
@@ -0,0 +1,63 @@
+/* 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";
+
+// Test which nodes are consider draggable by the markup-view.
+
+const TEST_URL = URL_ROOT + "doc_markup_dragdrop.html";
+
+// Test cases should be objects with the following properties:
+// - node {String|Function} A CSS selector that uniquely identifies the node to
+// be tested. Or a generator function called in a Task that should return the
+// corresponding MarkupContainer object to be tested.
+// - draggable {Boolean} Whether or not the node should be draggable.
+const TEST_DATA = [
+ { node: "head", draggable: false },
+ { node: "body", draggable: false },
+ { node: "html", draggable: false },
+ { node: "style", draggable: true },
+ { node: "a", draggable: true },
+ { node: "p", draggable: true },
+ { node: "input", draggable: true },
+ { node: "div", draggable: true },
+ {
+ node: function* (inspector) {
+ let parentFront = yield getNodeFront("#before", inspector);
+ let {nodes} = yield inspector.walker.children(parentFront);
+ // Getting the comment node.
+ return getContainerForNodeFront(nodes[1], inspector);
+ },
+ draggable: true
+ },
+ {
+ node: function* (inspector) {
+ let parentFront = yield getNodeFront("#test", inspector);
+ let {nodes} = yield inspector.walker.children(parentFront);
+ // Getting the ::before pseudo element.
+ return getContainerForNodeFront(nodes[0], inspector);
+ },
+ draggable: false
+ }
+];
+
+add_task(function* () {
+ let {inspector} = yield openInspectorForURL(TEST_URL);
+ yield inspector.markup.expandAll();
+
+ for (let {node, draggable} of TEST_DATA) {
+ let container;
+ let name;
+ if (typeof node === "string") {
+ container = yield getContainerForSelector(node, inspector);
+ name = node;
+ } else {
+ container = yield node(inspector);
+ name = container.toString();
+ }
+
+ let status = draggable ? "draggable" : "not draggable";
+ info(`Testing ${name}, expecting it to be ${status}`);
+ is(container.isDraggable(), draggable, `The node is ${status}`);
+ }
+});
diff --git a/devtools/client/inspector/markup/test/browser_markup_dragdrop_escapeKeyPress.js b/devtools/client/inspector/markup/test/browser_markup_dragdrop_escapeKeyPress.js
new file mode 100644
index 000000000..075d14352
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_dragdrop_escapeKeyPress.js
@@ -0,0 +1,34 @@
+/* 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";
+
+// Test whether ESCAPE keypress cancels dragging of an element.
+
+const TEST_URL = URL_ROOT + "doc_markup_dragdrop.html";
+
+add_task(function* () {
+ let {inspector} = yield openInspectorForURL(TEST_URL);
+ let {markup} = inspector;
+
+ info("Get a test container");
+ yield selectNode("#test", inspector);
+ let container = yield getContainerForSelector("#test", inspector);
+
+ info("Simulate a drag/drop on this container");
+ yield simulateNodeDrag(inspector, "#test");
+
+ ok(container.isDragging && markup.isDragging,
+ "The container is being dragged");
+ ok(markup.doc.body.classList.contains("dragging"),
+ "The dragging css class was added");
+
+ info("Simulate ESCAPE keypress");
+ EventUtils.sendKey("escape", inspector.panelWin);
+
+ ok(!container.isDragging && !markup.isDragging,
+ "The dragging has stopped");
+ ok(!markup.doc.body.classList.contains("dragging"),
+ "The dragging css class was removed");
+});
diff --git a/devtools/client/inspector/markup/test/browser_markup_dragdrop_invalidNodes.js b/devtools/client/inspector/markup/test/browser_markup_dragdrop_invalidNodes.js
new file mode 100644
index 000000000..9eea6a102
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_dragdrop_invalidNodes.js
@@ -0,0 +1,48 @@
+/* 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";
+
+// Check that pseudo-elements and anonymous nodes are not draggable.
+
+const TEST_URL = URL_ROOT + "doc_markup_dragdrop.html";
+const PREF = "devtools.inspector.showAllAnonymousContent";
+
+add_task(function* () {
+ Services.prefs.setBoolPref(PREF, true);
+
+ let {inspector} = yield openInspectorForURL(TEST_URL);
+
+ info("Expanding nodes below #test");
+ let parentFront = yield getNodeFront("#test", inspector);
+ yield inspector.markup.expandNode(parentFront);
+ yield waitForMultipleChildrenUpdates(inspector);
+
+ info("Getting the ::before pseudo element and selecting it");
+ let parentContainer = yield getContainerForNodeFront(parentFront, inspector);
+ let beforePseudo = parentContainer.elt.children[1].firstChild.container;
+ parentContainer.elt.scrollIntoView(true);
+ yield selectNode(beforePseudo.node, inspector);
+
+ info("Simulate dragging the ::before pseudo element");
+ yield simulateNodeDrag(inspector, beforePseudo);
+
+ ok(!beforePseudo.isDragging, "::before pseudo element isn't dragging");
+
+ info("Expanding nodes below #anonymousParent");
+ let inputFront = yield getNodeFront("#anonymousParent", inspector);
+ yield inspector.markup.expandNode(inputFront);
+ yield waitForMultipleChildrenUpdates(inspector);
+
+ info("Getting the anonymous node and selecting it");
+ let inputContainer = yield getContainerForNodeFront(inputFront, inspector);
+ let anonymousDiv = inputContainer.elt.children[1].firstChild.container;
+ inputContainer.elt.scrollIntoView(true);
+ yield selectNode(anonymousDiv.node, inspector);
+
+ info("Simulate dragging the anonymous node");
+ yield simulateNodeDrag(inspector, anonymousDiv);
+
+ ok(!anonymousDiv.isDragging, "anonymous node isn't dragging");
+});
diff --git a/devtools/client/inspector/markup/test/browser_markup_dragdrop_reorder.js b/devtools/client/inspector/markup/test/browser_markup_dragdrop_reorder.js
new file mode 100644
index 000000000..f74b50147
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_dragdrop_reorder.js
@@ -0,0 +1,109 @@
+/* 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";
+
+requestLongerTimeout(2);
+
+// Test different kinds of drag and drop node re-ordering.
+
+const TEST_URL = URL_ROOT + "doc_markup_dragdrop.html";
+
+add_task(function* () {
+ let {inspector} = yield openInspectorForURL(TEST_URL);
+ let ids;
+
+ info("Expand #test node");
+ let parentFront = yield getNodeFront("#test", inspector);
+ yield inspector.markup.expandNode(parentFront);
+ yield waitForMultipleChildrenUpdates(inspector);
+
+ info("Scroll #test into view");
+ let parentContainer = yield getContainerForNodeFront(parentFront, inspector);
+ parentContainer.elt.scrollIntoView(true);
+
+ info("Test putting an element back at its original place");
+ yield dragElementToOriginalLocation("#firstChild", inspector);
+ ids = yield getChildrenIDsOf(parentFront, inspector);
+ is(ids[0], "firstChild",
+ "#firstChild is still the first child of #test");
+ is(ids[1], "middleChild",
+ "#middleChild is still the second child of #test");
+
+ info("Testing switching elements inside their parent");
+ yield moveElementDown("#firstChild", "#middleChild", inspector);
+ ids = yield getChildrenIDsOf(parentFront, inspector);
+ is(ids[0], "middleChild",
+ "#firstChild is now the second child of #test");
+ is(ids[1], "firstChild",
+ "#middleChild is now the first child of #test");
+
+ info("Testing switching elements with a last child");
+ yield moveElementDown("#firstChild", "#lastChild", inspector);
+ ids = yield getChildrenIDsOf(parentFront, inspector);
+ is(ids[1], "lastChild",
+ "#lastChild is now the second child of #test");
+ is(ids[2], "firstChild",
+ "#firstChild is now the last child of #test");
+
+ info("Testing appending element to a parent");
+ yield moveElementDown("#before", "#test", inspector);
+ ids = yield getChildrenIDsOf(parentFront, inspector);
+ is(ids.length, 4,
+ "New element appended to #test");
+ is(ids[0], "before",
+ "New element is appended at the right place (currently first child)");
+
+ info("Testing moving element to after it's parent");
+ yield moveElementDown("#firstChild", "#test", inspector);
+ ids = yield getChildrenIDsOf(parentFront, inspector);
+ is(ids.length, 3,
+ "#firstChild is no longer #test's child");
+ let siblingFront = yield inspector.walker.nextSibling(parentFront);
+ is(siblingFront.id, "firstChild",
+ "#firstChild is now #test's nextElementSibling");
+});
+
+function* dragElementToOriginalLocation(selector, inspector) {
+ info("Picking up and putting back down " + selector);
+
+ function onMutation() {
+ ok(false, "Mutation received from dragging a node back to its location");
+ }
+ inspector.on("markupmutation", onMutation);
+ yield simulateNodeDragAndDrop(inspector, selector, 0, 0);
+
+ // Wait a bit to make sure the event never fires.
+ // This doesn't need to catch *all* cases, since the mutation
+ // will cause failure later in the test when it checks element ordering.
+ yield wait(500);
+ inspector.off("markupmutation", onMutation);
+}
+
+function* moveElementDown(selector, next, inspector) {
+ info("Switching " + selector + " with " + next);
+
+ let container = yield getContainerForSelector(next, inspector);
+ let height = container.tagLine.getBoundingClientRect().height;
+
+ let onMutated = inspector.once("markupmutation");
+ let uiUpdate = inspector.once("inspector-updated");
+
+ yield simulateNodeDragAndDrop(inspector, selector, 0, Math.round(height) + 2);
+
+ let mutations = yield onMutated;
+ yield uiUpdate;
+
+ is(mutations.length, 2, "2 mutations were received");
+}
+
+function* getChildrenIDsOf(parentFront, {walker}) {
+ let {nodes} = yield walker.children(parentFront);
+ // Filter out non-element nodes since children also returns pseudo-elements.
+ return nodes.filter(node => {
+ return !node.isPseudoElement;
+ }).map(node => {
+ return node.id;
+ });
+}
diff --git a/devtools/client/inspector/markup/test/browser_markup_dragdrop_tooltip.js b/devtools/client/inspector/markup/test/browser_markup_dragdrop_tooltip.js
new file mode 100644
index 000000000..77472800e
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_dragdrop_tooltip.js
@@ -0,0 +1,35 @@
+/* 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";
+
+// Test that tooltips don't appear when dragging over tooltip targets.
+
+const TEST_URL = "data:text/html;charset=utf8,<img src=\"about:logo\" /><div>";
+
+add_task(function* () {
+ let {inspector} = yield openInspectorForURL(TEST_URL);
+ let {markup} = inspector;
+
+ info("Get the tooltip target element for the image's src attribute");
+ let img = yield getContainerForSelector("img", inspector);
+ let target = img.editor.getAttributeElement("src").querySelector(".link");
+
+ info("Check that the src attribute of the image is a valid tooltip target");
+ let isValid = yield isHoverTooltipTarget(markup.imagePreviewTooltip, target);
+ ok(isValid, "The element is a valid tooltip target");
+
+ info("Start dragging the test div");
+ yield simulateNodeDrag(inspector, "div");
+
+ info("Now check that the src attribute of the image isn't a valid target");
+ isValid = yield isHoverTooltipTarget(markup.imagePreviewTooltip, target);
+ ok(!isValid, "The element is not a valid tooltip target");
+
+ info("Stop dragging the test div");
+ yield simulateNodeDrop(inspector, "div");
+
+ info("Check again the src attribute of the image");
+ isValid = yield isHoverTooltipTarget(markup.imagePreviewTooltip, target);
+ ok(isValid, "The element is a valid tooltip target");
+});
diff --git a/devtools/client/inspector/markup/test/browser_markup_events-overflow.js b/devtools/client/inspector/markup/test/browser_markup_events-overflow.js
new file mode 100644
index 000000000..3e73921f4
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_events-overflow.js
@@ -0,0 +1,91 @@
+/* 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 TEST_URL = URL_ROOT + "doc_markup_events-overflow.html";
+const TEST_DATA = [
+ {
+ desc: "editor overflows container",
+ // scroll to bottom
+ initialScrollTop: -1,
+ // last header
+ headerToClick: 49,
+ alignBottom: true,
+ alignTop: false,
+ },
+ {
+ desc: "header overflows the container",
+ initialScrollTop: 2,
+ headerToClick: 0,
+ alignBottom: false,
+ alignTop: true,
+ },
+ {
+ desc: "neither header nor editor overflows the container",
+ initialScrollTop: 2,
+ headerToClick: 5,
+ alignBottom: false,
+ alignTop: false,
+ },
+];
+
+add_task(function* () {
+ let { inspector } = yield openInspectorForURL(TEST_URL);
+
+ let markupContainer = yield getContainerForSelector("#events", inspector);
+ let evHolder = markupContainer.elt.querySelector(".markupview-events");
+ let tooltip = inspector.markup.eventDetailsTooltip;
+
+ info("Clicking to open event tooltip.");
+ EventUtils.synthesizeMouseAtCenter(evHolder, {},
+ inspector.markup.doc.defaultView);
+ yield tooltip.once("shown");
+ info("EventTooltip visible.");
+
+ let container = tooltip.panel;
+ let containerRect = container.getBoundingClientRect();
+ let headers = container.querySelectorAll(".event-header");
+
+ for (let data of TEST_DATA) {
+ info("Testing scrolling when " + data.desc);
+
+ if (data.initialScrollTop < 0) {
+ info("Scrolling container to the bottom.");
+ let newScrollTop = container.scrollHeight - container.clientHeight;
+ data.initialScrollTop = container.scrollTop = newScrollTop;
+ } else {
+ info("Scrolling container by " + data.initialScrollTop + "px");
+ container.scrollTop = data.initialScrollTop;
+ }
+
+ is(container.scrollTop, data.initialScrollTop, "Container scrolled.");
+
+ info("Clicking on header #" + data.headerToClick);
+ let header = headers[data.headerToClick];
+
+ let ready = tooltip.once("event-tooltip-ready");
+ EventUtils.synthesizeMouseAtCenter(header, {}, header.ownerGlobal);
+ yield ready;
+
+ info("Event handler expanded.");
+
+ // Wait for any scrolling to finish.
+ yield promiseNextTick();
+
+ if (data.alignTop) {
+ let headerRect = header.getBoundingClientRect();
+
+ is(Math.round(headerRect.top), Math.round(containerRect.top),
+ "Clicked header is aligned with the container top.");
+ } else if (data.alignBottom) {
+ let editorRect = header.nextElementSibling.getBoundingClientRect();
+
+ is(Math.round(editorRect.bottom), Math.round(containerRect.bottom),
+ "Clicked event handler code is aligned with the container bottom.");
+ } else {
+ is(container.scrollTop, data.initialScrollTop,
+ "Container did not scroll, as expected.");
+ }
+ }
+});
diff --git a/devtools/client/inspector/markup/test/browser_markup_events-windowed-host.js b/devtools/client/inspector/markup/test/browser_markup_events-windowed-host.js
new file mode 100644
index 000000000..cfcb0a8ab
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_events-windowed-host.js
@@ -0,0 +1,61 @@
+/* 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";
+
+/*
+ * Test that the event details tooltip can be hidden by clicking outside of the tooltip
+ * after switching hosts.
+ */
+
+const TEST_URL = URL_ROOT + "doc_markup_events-overflow.html";
+
+registerCleanupFunction(() => {
+ // Restore the default Toolbox host position after the test.
+ Services.prefs.clearUserPref("devtools.toolbox.host");
+});
+
+add_task(function* () {
+ let { inspector, toolbox } = yield openInspectorForURL(TEST_URL);
+ yield runTests(inspector);
+
+ yield toolbox.switchHost("window");
+ yield runTests(inspector);
+
+ yield toolbox.switchHost("bottom");
+ yield runTests(inspector);
+
+ yield toolbox.destroy();
+});
+
+function* runTests(inspector) {
+ let markupContainer = yield getContainerForSelector("#events", inspector);
+ let evHolder = markupContainer.elt.querySelector(".markupview-events");
+ let tooltip = inspector.markup.eventDetailsTooltip;
+
+ info("Clicking to open event tooltip.");
+
+ let onInspectorUpdated = inspector.once("inspector-updated");
+ let onTooltipShown = tooltip.once("shown");
+ EventUtils.synthesizeMouseAtCenter(evHolder, {}, inspector.markup.doc.defaultView);
+
+ yield onTooltipShown;
+ // New node is selected when clicking on the events bubble, wait for inspector-updated.
+ yield onInspectorUpdated;
+
+ ok(tooltip.isVisible(), "EventTooltip visible.");
+
+ onInspectorUpdated = inspector.once("inspector-updated");
+ let onTooltipHidden = tooltip.once("hidden");
+
+ info("Click on another tag to hide the event tooltip");
+ let h1 = yield getContainerForSelector("h1", inspector);
+ let tag = h1.elt.querySelector(".tag");
+ EventUtils.synthesizeMouseAtCenter(tag, {}, inspector.markup.doc.defaultView);
+
+ yield onTooltipHidden;
+ // New node is selected, wait for inspector-updated.
+ yield onInspectorUpdated;
+
+ ok(!tooltip.isVisible(), "EventTooltip hidden.");
+}
diff --git a/devtools/client/inspector/markup/test/browser_markup_events1.js b/devtools/client/inspector/markup/test/browser_markup_events1.js
new file mode 100644
index 000000000..dbfd4a5c3
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_events1.js
@@ -0,0 +1,149 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_events_test_runner.js */
+
+"use strict";
+
+// Test that markup view event bubbles show the correct event info for DOM
+// events.
+
+const TEST_URL = URL_ROOT + "doc_markup_events1.html";
+
+loadHelperScript("helper_events_test_runner.js");
+
+const TEST_DATA = [ // eslint-disable-line
+ {
+ selector: "html",
+ expected: [
+ {
+ type: "load",
+ filename: TEST_URL,
+ attributes: [
+ "Bubbling",
+ "DOM0"
+ ],
+ handler: "init();"
+ }
+ ]
+ },
+ {
+ selector: "#container",
+ expected: [
+ {
+ type: "mouseover",
+ filename: TEST_URL + ":45",
+ attributes: [
+ "Capturing",
+ "DOM2"
+ ],
+ handler: "function mouseoverHandler(event) {\n" +
+ " if (event.target.id !== \"container\") {\n" +
+ " let output = document.getElementById(\"output\");\n" +
+ " output.textContent = event.target.textContent;\n" +
+ " }\n" +
+ "}"
+ }
+ ]
+ },
+ {
+ selector: "#multiple",
+ expected: [
+ {
+ type: "click",
+ filename: TEST_URL + ":52",
+ attributes: [
+ "Bubbling",
+ "DOM2"
+ ],
+ handler: "function clickHandler(event) {\n" +
+ " let output = document.getElementById(\"output\");\n" +
+ " output.textContent = \"click\";\n" +
+ "}"
+ },
+ {
+ type: "mouseup",
+ filename: TEST_URL + ":57",
+ attributes: [
+ "Bubbling",
+ "DOM2"
+ ],
+ handler: "function mouseupHandler(event) {\n" +
+ " let output = document.getElementById(\"output\");\n" +
+ " output.textContent = \"mouseup\";\n" +
+ "}"
+ }
+ ]
+ },
+ // #noevents tests check that dynamically added events are properly displayed
+ // in the markupview
+ {
+ selector: "#noevents",
+ expected: []
+ },
+ {
+ selector: "#noevents",
+ beforeTest: function* (inspector, testActor) {
+ let nodeMutated = inspector.once("markupmutation");
+ yield testActor.eval("window.wrappedJSObject.addNoeventsClickHandler();");
+ yield nodeMutated;
+ },
+ expected: [
+ {
+ type: "click",
+ filename: TEST_URL + ":72",
+ attributes: [
+ "Bubbling",
+ "DOM2"
+ ],
+ handler: "function noeventsClickHandler(event) {\n" +
+ " alert(\"noevents has an event listener\");\n" +
+ "}"
+ }
+ ]
+ },
+ {
+ selector: "#noevents",
+ beforeTest: function* (inspector, testActor) {
+ let nodeMutated = inspector.once("markupmutation");
+ yield testActor.eval(
+ "window.wrappedJSObject.removeNoeventsClickHandler();");
+ yield nodeMutated;
+ },
+ expected: []
+ },
+ {
+ selector: "#DOM0",
+ expected: [
+ {
+ type: "click",
+ filename: TEST_URL,
+ attributes: [
+ "Bubbling",
+ "DOM0"
+ ],
+ handler: "alert('DOM0')"
+ }
+ ]
+ },
+ {
+ selector: "#handleevent",
+ expected: [
+ {
+ type: "click",
+ filename: TEST_URL + ":67",
+ attributes: [
+ "Bubbling",
+ "DOM2"
+ ],
+ handler: "handleEvent: function(blah) {\n" +
+ " alert(\"handleEvent\");\n" +
+ "}"
+ }
+ ]
+ }
+];
+
+add_task(function* () {
+ yield runEventPopupTests(TEST_URL, TEST_DATA);
+});
diff --git a/devtools/client/inspector/markup/test/browser_markup_events2.js b/devtools/client/inspector/markup/test/browser_markup_events2.js
new file mode 100644
index 000000000..3e741cf1f
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_events2.js
@@ -0,0 +1,163 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_events_test_runner.js */
+
+"use strict";
+
+// Test that markup view event bubbles show the correct event info for DOM
+// events.
+
+const TEST_URL = URL_ROOT + "doc_markup_events2.html";
+
+loadHelperScript("helper_events_test_runner.js");
+
+const TEST_DATA = [ // eslint-disable-line
+ {
+ selector: "#fatarrow",
+ expected: [
+ {
+ type: "click",
+ filename: TEST_URL + ":39",
+ attributes: [
+ "Bubbling",
+ "DOM2"
+ ],
+ handler: "() => {\n" +
+ " alert(\"Fat arrow without params!\");\n" +
+ "}"
+ },
+ {
+ type: "click",
+ filename: TEST_URL + ":43",
+ attributes: [
+ "Bubbling",
+ "DOM2"
+ ],
+ handler: "event => {\n" +
+ " alert(\"Fat arrow with 1 param!\");\n" +
+ "}"
+ },
+ {
+ type: "click",
+ filename: TEST_URL + ":47",
+ attributes: [
+ "Bubbling",
+ "DOM2"
+ ],
+ handler: "(event, foo, bar) => {\n" +
+ " alert(\"Fat arrow with 3 params!\");\n" +
+ "}"
+ },
+ {
+ type: "click",
+ filename: TEST_URL + ":51",
+ attributes: [
+ "Bubbling",
+ "DOM2"
+ ],
+ handler: "b => b"
+ }
+ ]
+ },
+ {
+ selector: "#bound",
+ expected: [
+ {
+ type: "click",
+ filename: TEST_URL + ":62",
+ attributes: [
+ "Bubbling",
+ "DOM2"
+ ],
+ handler: "function boundClickHandler(event) {\n" +
+ " alert(\"Bound event\");\n" +
+ "}"
+ }
+ ]
+ },
+ {
+ selector: "#boundhe",
+ expected: [
+ {
+ type: "click",
+ filename: TEST_URL + ":85",
+ attributes: [
+ "Bubbling",
+ "DOM2"
+ ],
+ handler: "handleEvent: function() {\n" +
+ " alert(\"boundHandleEvent\");\n" +
+ "}"
+ }
+ ]
+ },
+ {
+ selector: "#comment-inline",
+ expected: [
+ {
+ type: "click",
+ filename: TEST_URL + ":91",
+ attributes: [
+ "Bubbling",
+ "DOM2"
+ ],
+ handler: "function functionProceededByInlineComment() {\n" +
+ " alert(\"comment-inline\");\n" +
+ "}"
+ }
+ ]
+ },
+ {
+ selector: "#comment-streaming",
+ expected: [
+ {
+ type: "click",
+ filename: TEST_URL + ":96",
+ attributes: [
+ "Bubbling",
+ "DOM2"
+ ],
+ handler: "function functionProceededByStreamingComment() {\n" +
+ " alert(\"comment-streaming\");\n" +
+ "}"
+ }
+ ]
+ },
+ {
+ selector: "#anon-object-method",
+ expected: [
+ {
+ type: "click",
+ filename: TEST_URL + ":71",
+ attributes: [
+ "Bubbling",
+ "DOM2"
+ ],
+ handler: "anonObjectMethod: function() {\n" +
+ " alert(\"obj.anonObjectMethod\");\n" +
+ "}"
+ }
+ ]
+ },
+ {
+ selector: "#object-method",
+ expected: [
+ {
+ type: "click",
+ filename: TEST_URL + ":75",
+ attributes: [
+ "Bubbling",
+ "DOM2"
+ ],
+ handler: "objectMethod: function kay() {\n" +
+ " alert(\"obj.objectMethod\");\n" +
+ "}"
+ }
+ ]
+ }
+];
+
+add_task(function* () {
+ yield runEventPopupTests(TEST_URL, TEST_DATA);
+});
diff --git a/devtools/client/inspector/markup/test/browser_markup_events3.js b/devtools/client/inspector/markup/test/browser_markup_events3.js
new file mode 100644
index 000000000..a9dc2a499
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_events3.js
@@ -0,0 +1,161 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_events_test_runner.js */
+
+"use strict";
+
+// Test that markup view event bubbles show the correct event info for DOM
+// events.
+
+const TEST_URL = URL_ROOT + "doc_markup_events3.html";
+
+loadHelperScript("helper_events_test_runner.js");
+
+const TEST_DATA = [ // eslint-disable-line
+ {
+ selector: "#es6-method",
+ expected: [
+ {
+ type: "click",
+ filename: TEST_URL + ":91",
+ attributes: [
+ "Bubbling",
+ "DOM2"
+ ],
+ handler: "es6Method() {\n" +
+ " alert(\"obj.es6Method\");\n" +
+ "}"
+ }
+ ]
+ },
+ {
+ selector: "#generator",
+ expected: [
+ {
+ type: "click",
+ filename: TEST_URL + ":96",
+ attributes: [
+ "Bubbling",
+ "DOM2"
+ ],
+ handler: "function* generator() {\n" +
+ " alert(\"generator\");\n" +
+ "}"
+ }
+ ]
+ },
+ {
+ selector: "#anon-generator",
+ expected: [
+ {
+ type: "click",
+ filename: TEST_URL + ":55",
+ attributes: [
+ "Bubbling",
+ "DOM2"
+ ],
+ handler: "function*() {\n" +
+ " alert(\"anonGenerator\");\n" +
+ "}"
+ }
+ ]
+ },
+ {
+ selector: "#named-function-expression",
+ expected: [
+ {
+ type: "click",
+ filename: TEST_URL + ":23",
+ attributes: [
+ "Bubbling",
+ "DOM2"
+ ],
+ handler: "let namedFunctionExpression =\n" +
+ " function foo() {\n" +
+ " alert(\"namedFunctionExpression\");\n" +
+ " }"
+ }
+ ]
+ },
+ {
+ selector: "#anon-function-expression",
+ expected: [
+ {
+ type: "click",
+ filename: TEST_URL + ":27",
+ attributes: [
+ "Bubbling",
+ "DOM2"
+ ],
+ handler: "let anonFunctionExpression = function() {\n" +
+ " alert(\"anonFunctionExpression\");\n" +
+ "}"
+ }
+ ]
+ },
+ {
+ selector: "#returned-function",
+ expected: [
+ {
+ type: "click",
+ filename: TEST_URL + ":32",
+ attributes: [
+ "Bubbling",
+ "DOM2"
+ ],
+ handler: "function bar() {\n" +
+ " alert(\"returnedFunction\");\n" +
+ "}"
+ }
+ ]
+ },
+ {
+ selector: "#constructed-function",
+ expected: [
+ {
+ type: "click",
+ filename: TEST_URL + ":1",
+ attributes: [
+ "Bubbling",
+ "DOM2"
+ ],
+ handler: ""
+ }
+ ]
+ },
+ {
+ selector: "#constructed-function-with-body-string",
+ expected: [
+ {
+ type: "click",
+ filename: TEST_URL + ":1",
+ attributes: [
+ "Bubbling",
+ "DOM2"
+ ],
+ handler: "alert(\"constructedFuncWithBodyString\");"
+ }
+ ]
+ },
+ {
+ selector: "#multiple-assignment",
+ expected: [
+ {
+ type: "click",
+ filename: TEST_URL + ":42",
+ attributes: [
+ "Bubbling",
+ "DOM2"
+ ],
+ handler: "let multipleAssignment = foo = bar = function multi() {\n" +
+ " alert(\"multipleAssignment\");\n" +
+ "}"
+ }
+ ]
+ },
+];
+
+add_task(function* () {
+ yield runEventPopupTests(TEST_URL, TEST_DATA);
+});
diff --git a/devtools/client/inspector/markup/test/browser_markup_events_form.js b/devtools/client/inspector/markup/test/browser_markup_events_form.js
new file mode 100644
index 000000000..ab029720c
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_events_form.js
@@ -0,0 +1,61 @@
+/* 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";
+
+// Testing the feature whereby custom registered actors can listen to
+// 'form' events sent by the NodeActor to hook custom data to it.
+// The test registers one backend actor providing custom form data
+// and checks that the value is properly sent to the client (NodeFront).
+
+const TEST_PAGE_URL = URL_ROOT + "doc_markup_events_form.html";
+const TEST_ACTOR_URL = CHROME_URL_ROOT + "actor_events_form.js";
+
+var {EventsFormFront} = require(TEST_ACTOR_URL);
+
+add_task(function* () {
+ info("Opening the Toolbox");
+ let tab = yield addTab(TEST_PAGE_URL);
+ let toolbox = yield openToolboxForTab(tab, "webconsole");
+
+ info("Registering test actor");
+ let {registrar, front} = yield registerTestActor(toolbox);
+
+ info("Selecting the Inspector panel");
+ let inspector = yield toolbox.selectTool("inspector");
+ let container = yield getContainerForSelector("#container", inspector);
+ isnot(container, null, "There must be requested container");
+
+ let nodeFront = container.node;
+ let value = nodeFront.getFormProperty("test-property");
+ is(value, "test-value", "There must be custom property");
+
+ info("Unregistering actor");
+ yield unregisterActor(registrar, front);
+});
+
+function registerTestActor(toolbox) {
+ let deferred = defer();
+
+ let options = {
+ prefix: "eventsFormActor",
+ actorClass: "EventsFormActor",
+ moduleUrl: TEST_ACTOR_URL,
+ };
+
+ // Register as a tab actor
+ let client = toolbox.target.client;
+ registerTabActor(client, options).then(({registrar, form}) => {
+ // Attach to the registered actor
+ let front = EventsFormFront(client, form);
+ front.attach().then(() => {
+ deferred.resolve({
+ front: front,
+ registrar: registrar,
+ });
+ });
+ });
+
+ return deferred.promise;
+}
diff --git a/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.0.js b/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.0.js
new file mode 100644
index 000000000..7413ea660
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.0.js
@@ -0,0 +1,237 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_events_test_runner.js */
+"use strict";
+
+// Test that markup view event bubbles show the correct event info for jQuery
+// and jQuery Live events (jQuery version 1.0).
+
+const TEST_LIB = "lib_jquery_1.0.js";
+const TEST_URL = URL_ROOT + "doc_markup_events_jquery.html?" + TEST_LIB;
+
+loadHelperScript("helper_events_test_runner.js");
+
+/*eslint-disable */
+const TEST_DATA = [
+ {
+ selector: "html",
+ expected: [
+ {
+ type: "load",
+ filename: URL_ROOT + TEST_LIB,
+ attributes: [
+ "jQuery"
+ ],
+ handler: "ready: function() {\n" +
+ " // Make sure that the DOM is not already loaded\n" +
+ " if (!jQuery.isReady) {\n" +
+ " // Remember that the DOM is ready\n" +
+ " jQuery.isReady = true;\n" +
+ "\n" +
+ " // If there are functions bound, to execute\n" +
+ " if (jQuery.readyList) {\n" +
+ " // Execute all of them\n" +
+ " for (var i = 0; i < jQuery.readyList.length; i++)\n" +
+ " jQuery.readyList[i].apply(document);\n" +
+ "\n" +
+ " // Reset the list of functions\n" +
+ " jQuery.readyList = null;\n" +
+ " }\n" +
+ " }\n" +
+ "}"
+ },
+ {
+ type: "load",
+ filename: TEST_URL,
+ attributes: [
+ "Bubbling",
+ "DOM0"
+ ],
+ handler: "() => {\n" +
+ " var handler1 = function liveDivDblClick() {\n" +
+ " alert(1);\n" +
+ " };\n" +
+ " var handler2 = function liveDivDragStart() {\n" +
+ " alert(2);\n" +
+ " };\n" +
+ " var handler3 = function liveDivDragLeave() {\n" +
+ " alert(3);\n" +
+ " };\n" +
+ " var handler4 = function liveDivDragEnd() {\n" +
+ " alert(4);\n" +
+ " };\n" +
+ " var handler5 = function liveDivDrop() {\n" +
+ " alert(5);\n" +
+ " };\n" +
+ " var handler6 = function liveDivDragOver() {\n" +
+ " alert(6);\n" +
+ " };\n" +
+ " var handler7 = function divClick1() {\n" +
+ " alert(7);\n" +
+ " };\n" +
+ " var handler8 = function divClick2() {\n" +
+ " alert(8);\n" +
+ " };\n" +
+ " var handler9 = function divKeyDown() {\n" +
+ " alert(9);\n" +
+ " };\n" +
+ " var handler10 = function divDragOut() {\n" +
+ " alert(10);\n" +
+ " };\n" +
+ "\n" +
+ " if ($(\"#livediv\").live) {\n" +
+ " $(\"#livediv\").live(\"dblclick\", handler1);\n" +
+ " $(\"#livediv\").live(\"dragstart\", handler2);\n" +
+ " }\n" +
+ "\n" +
+ " if ($(\"#livediv\").delegate) {\n" +
+ " $(document).delegate(\"#livediv\", \"dragleave\", handler3);\n" +
+ " $(document).delegate(\"#livediv\", \"dragend\", handler4);\n" +
+ " }\n" +
+ "\n" +
+ " if ($(\"#livediv\").on) {\n" +
+ " $(document).on(\"drop\", \"#livediv\", handler5);\n" +
+ " $(document).on(\"dragover\", \"#livediv\", handler6);\n" +
+ " $(document).on(\"dragout\", \"#livediv:xxxxx\", handler10);\n" +
+ " }\n" +
+ "\n" +
+ " var div = $(\"div\")[0];\n" +
+ " $(div).click(handler7);\n" +
+ " $(div).click(handler8);\n" +
+ " $(div).keydown(handler9);\n" +
+ "}"
+ },
+ {
+ type: "load",
+ filename: URL_ROOT + TEST_LIB,
+ attributes: [
+ "Bubbling",
+ "DOM0"
+ ],
+ handler: "handle: function(event) {\n" +
+ " if (typeof jQuery == \"undefined\") return;\n" +
+ "\n" +
+ " event = event || jQuery.event.fix(window.event);\n" +
+ "\n" +
+ " // If no correct event was found, fail\n" +
+ " if (!event) return;\n" +
+ "\n" +
+ " var returnValue = true;\n" +
+ "\n" +
+ " var c = this.events[event.type];\n" +
+ "\n" +
+ " for (var j in c) {\n" +
+ " if (c[j].apply(this, [event]) === false) {\n" +
+ " event.preventDefault();\n" +
+ " event.stopPropagation();\n" +
+ " returnValue = false;\n" +
+ " }\n" +
+ " }\n" +
+ "\n" +
+ " return returnValue;\n" +
+ "}"
+ }
+ ]
+ },
+ {
+ selector: "#testdiv",
+ expected: [
+ {
+ type: "click",
+ filename: TEST_URL + ":34",
+ attributes: [
+ "jQuery"
+ ],
+ handler: "var handler7 = function divClick1() {\n" +
+ " alert(7);\n" +
+ "}"
+ },
+ {
+ type: "click",
+ filename: TEST_URL + ":35",
+ attributes: [
+ "jQuery"
+ ],
+ handler: "var handler8 = function divClick2() {\n" +
+ " alert(8);\n" +
+ "}"
+ },
+ {
+ type: "click",
+ filename: URL_ROOT + TEST_LIB + ":894",
+ attributes: [
+ "Bubbling",
+ "DOM2"
+ ],
+ handler: "handle: function(event) {\n" +
+ " if (typeof jQuery == \"undefined\") return;\n" +
+ "\n" +
+ " event = event || jQuery.event.fix(window.event);\n" +
+ "\n" +
+ " // If no correct event was found, fail\n" +
+ " if (!event) return;\n" +
+ "\n" +
+ " var returnValue = true;\n" +
+ "\n" +
+ " var c = this.events[event.type];\n" +
+ "\n" +
+ " for (var j in c) {\n" +
+ " if (c[j].apply(this, [event]) === false) {\n" +
+ " event.preventDefault();\n" +
+ " event.stopPropagation();\n" +
+ " returnValue = false;\n" +
+ " }\n" +
+ " }\n" +
+ "\n" +
+ " return returnValue;\n" +
+ "}"
+ },
+ {
+ type: "keydown",
+ filename: TEST_URL + ":36",
+ attributes: [
+ "jQuery"
+ ],
+ handler: "var handler9 = function divKeyDown() {\n" +
+ " alert(9);\n" +
+ "}"
+ },
+ {
+ type: "keydown",
+ filename: URL_ROOT + TEST_LIB + ":894",
+ attributes: [
+ "Bubbling",
+ "DOM2"
+ ],
+ handler: "handle: function(event) {\n" +
+ " if (typeof jQuery == \"undefined\") return;\n" +
+ "\n" +
+ " event = event || jQuery.event.fix(window.event);\n" +
+ "\n" +
+ " // If no correct event was found, fail\n" +
+ " if (!event) return;\n" +
+ "\n" +
+ " var returnValue = true;\n" +
+ "\n" +
+ " var c = this.events[event.type];\n" +
+ "\n" +
+ " for (var j in c) {\n" +
+ " if (c[j].apply(this, [event]) === false) {\n" +
+ " event.preventDefault();\n" +
+ " event.stopPropagation();\n" +
+ " returnValue = false;\n" +
+ " }\n" +
+ " }\n" +
+ "\n" +
+ " return returnValue;\n" +
+ "}"
+ }
+ ]
+ },
+];
+/*eslint-enable */
+
+add_task(function* () {
+ yield runEventPopupTests(TEST_URL, TEST_DATA);
+});
diff --git a/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.1.js b/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.1.js
new file mode 100644
index 000000000..e5e995a87
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.1.js
@@ -0,0 +1,271 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_events_test_runner.js */
+"use strict";
+
+// Test that markup view event bubbles show the correct event info for jQuery
+// and jQuery Live events (jQuery version 1.1).
+
+const TEST_LIB = "lib_jquery_1.1.js";
+const TEST_URL = URL_ROOT + "doc_markup_events_jquery.html?" + TEST_LIB;
+
+loadHelperScript("helper_events_test_runner.js");
+
+/*eslint-disable */
+const TEST_DATA = [
+ {
+ selector: "html",
+ expected: [
+ {
+ type: "load",
+ filename: URL_ROOT + TEST_LIB,
+ attributes: [
+ "jQuery"
+ ],
+ handler: "ready: function() {\n" +
+ " // Make sure that the DOM is not already loaded\n" +
+ " if (!jQuery.isReady) {\n" +
+ " // Remember that the DOM is ready\n" +
+ " jQuery.isReady = true;\n" +
+ "\n" +
+ " // If there are functions bound, to execute\n" +
+ " if (jQuery.readyList) {\n" +
+ " // Execute all of them\n" +
+ " jQuery.each(jQuery.readyList, function() {\n" +
+ " this.apply(document);\n" +
+ " });\n" +
+ "\n" +
+ " // Reset the list of functions\n" +
+ " jQuery.readyList = null;\n" +
+ " }\n" +
+ " // Remove event lisenter to avoid memory leak\n" +
+ " if (jQuery.browser.mozilla || jQuery.browser.opera)\n" +
+ " document.removeEventListener(\"DOMContentLoaded\", jQuery.ready, false);\n" +
+ " }\n" +
+ "}"
+ },
+ {
+ type: "load",
+ filename: TEST_URL,
+ attributes: [
+ "Bubbling",
+ "DOM0"
+ ],
+ handler: "() => {\n" +
+ " var handler1 = function liveDivDblClick() {\n" +
+ " alert(1);\n" +
+ " };\n" +
+ " var handler2 = function liveDivDragStart() {\n" +
+ " alert(2);\n" +
+ " };\n" +
+ " var handler3 = function liveDivDragLeave() {\n" +
+ " alert(3);\n" +
+ " };\n" +
+ " var handler4 = function liveDivDragEnd() {\n" +
+ " alert(4);\n" +
+ " };\n" +
+ " var handler5 = function liveDivDrop() {\n" +
+ " alert(5);\n" +
+ " };\n" +
+ " var handler6 = function liveDivDragOver() {\n" +
+ " alert(6);\n" +
+ " };\n" +
+ " var handler7 = function divClick1() {\n" +
+ " alert(7);\n" +
+ " };\n" +
+ " var handler8 = function divClick2() {\n" +
+ " alert(8);\n" +
+ " };\n" +
+ " var handler9 = function divKeyDown() {\n" +
+ " alert(9);\n" +
+ " };\n" +
+ " var handler10 = function divDragOut() {\n" +
+ " alert(10);\n" +
+ " };\n" +
+ "\n" +
+ " if ($(\"#livediv\").live) {\n" +
+ " $(\"#livediv\").live(\"dblclick\", handler1);\n" +
+ " $(\"#livediv\").live(\"dragstart\", handler2);\n" +
+ " }\n" +
+ "\n" +
+ " if ($(\"#livediv\").delegate) {\n" +
+ " $(document).delegate(\"#livediv\", \"dragleave\", handler3);\n" +
+ " $(document).delegate(\"#livediv\", \"dragend\", handler4);\n" +
+ " }\n" +
+ "\n" +
+ " if ($(\"#livediv\").on) {\n" +
+ " $(document).on(\"drop\", \"#livediv\", handler5);\n" +
+ " $(document).on(\"dragover\", \"#livediv\", handler6);\n" +
+ " $(document).on(\"dragout\", \"#livediv:xxxxx\", handler10);\n" +
+ " }\n" +
+ "\n" +
+ " var div = $(\"div\")[0];\n" +
+ " $(div).click(handler7);\n" +
+ " $(div).click(handler8);\n" +
+ " $(div).keydown(handler9);\n" +
+ "}"
+ },
+ {
+ type: "load",
+ filename: URL_ROOT + TEST_LIB,
+ attributes: [
+ "Bubbling",
+ "DOM0"
+ ],
+ handler: "handle: function(event) {\n" +
+ " if (typeof jQuery == \"undefined\") return false;\n" +
+ "\n" +
+ " // Empty object is for triggered events with no data\n" +
+ " event = jQuery.event.fix(event || window.event || {});\n" +
+ "\n" +
+ " // returned undefined or false\n" +
+ " var returnValue;\n" +
+ "\n" +
+ " var c = this.events[event.type];\n" +
+ "\n" +
+ " var args = [].slice.call(arguments, 1);\n" +
+ " args.unshift(event);\n" +
+ "\n" +
+ " for (var j in c) {\n" +
+ " // Pass in a reference to the handler function itself\n" +
+ " // So that we can later remove it\n" +
+ " args[0].handler = c[j];\n" +
+ " args[0].data = c[j].data;\n" +
+ "\n" +
+ " if (c[j].apply(this, args) === false) {\n" +
+ " event.preventDefault();\n" +
+ " event.stopPropagation();\n" +
+ " returnValue = false;\n" +
+ " }\n" +
+ " }\n" +
+ "\n" +
+ " // Clean up added properties in IE to prevent memory leak\n" +
+ " if (jQuery.browser.msie) event.target = event.preventDefault = event.stopPropagation = event.handler = event.data = null;\n" +
+ "\n" +
+ " return returnValue;\n" +
+ "}"
+ }
+ ]
+ },
+ {
+ selector: "#testdiv",
+ expected: [
+ {
+ type: "click",
+ filename: TEST_URL + ":34",
+ attributes: [
+ "jQuery"
+ ],
+ handler: "var handler7 = function divClick1() {\n" +
+ " alert(7);\n" +
+ "}"
+ },
+ {
+ type: "click",
+ filename: TEST_URL + ":35",
+ attributes: [
+ "jQuery"
+ ],
+ handler: "var handler8 = function divClick2() {\n" +
+ " alert(8);\n" +
+ "}"
+ },
+ {
+ type: "click",
+ filename: URL_ROOT + TEST_LIB + ":1224",
+ attributes: [
+ "Bubbling",
+ "DOM2"
+ ],
+ handler: "handle: function(event) {\n" +
+ " if (typeof jQuery == \"undefined\") return false;\n" +
+ "\n" +
+ " // Empty object is for triggered events with no data\n" +
+ " event = jQuery.event.fix(event || window.event || {});\n" +
+ "\n" +
+ " // returned undefined or false\n" +
+ " var returnValue;\n" +
+ "\n" +
+ " var c = this.events[event.type];\n" +
+ "\n" +
+ " var args = [].slice.call(arguments, 1);\n" +
+ " args.unshift(event);\n" +
+ "\n" +
+ " for (var j in c) {\n" +
+ " // Pass in a reference to the handler function itself\n" +
+ " // So that we can later remove it\n" +
+ " args[0].handler = c[j];\n" +
+ " args[0].data = c[j].data;\n" +
+ "\n" +
+ " if (c[j].apply(this, args) === false) {\n" +
+ " event.preventDefault();\n" +
+ " event.stopPropagation();\n" +
+ " returnValue = false;\n" +
+ " }\n" +
+ " }\n" +
+ "\n" +
+ " // Clean up added properties in IE to prevent memory leak\n" +
+ " if (jQuery.browser.msie) event.target = event.preventDefault = event.stopPropagation = event.handler = event.data = null;\n" +
+ "\n" +
+ " return returnValue;\n" +
+ "}"
+ },
+ {
+ type: "keydown",
+ filename: TEST_URL + ":36",
+ attributes: [
+ "jQuery"
+ ],
+ handler: "var handler9 = function divKeyDown() {\n" +
+ " alert(9);\n" +
+ "}"
+ },
+ {
+ type: "keydown",
+ filename: URL_ROOT + TEST_LIB + ":1224",
+ attributes: [
+ "Bubbling",
+ "DOM2"
+ ],
+ handler: "handle: function(event) {\n" +
+ " if (typeof jQuery == \"undefined\") return false;\n" +
+ "\n" +
+ " // Empty object is for triggered events with no data\n" +
+ " event = jQuery.event.fix(event || window.event || {});\n" +
+ "\n" +
+ " // returned undefined or false\n" +
+ " var returnValue;\n" +
+ "\n" +
+ " var c = this.events[event.type];\n" +
+ "\n" +
+ " var args = [].slice.call(arguments, 1);\n" +
+ " args.unshift(event);\n" +
+ "\n" +
+ " for (var j in c) {\n" +
+ " // Pass in a reference to the handler function itself\n" +
+ " // So that we can later remove it\n" +
+ " args[0].handler = c[j];\n" +
+ " args[0].data = c[j].data;\n" +
+ "\n" +
+ " if (c[j].apply(this, args) === false) {\n" +
+ " event.preventDefault();\n" +
+ " event.stopPropagation();\n" +
+ " returnValue = false;\n" +
+ " }\n" +
+ " }\n" +
+ "\n" +
+ " // Clean up added properties in IE to prevent memory leak\n" +
+ " if (jQuery.browser.msie) event.target = event.preventDefault = event.stopPropagation = event.handler = event.data = null;\n" +
+ "\n" +
+ " return returnValue;\n" +
+ "}"
+ }
+ ]
+ }
+];
+/*eslint-enable */
+
+add_task(function* () {
+ yield runEventPopupTests(TEST_URL, TEST_DATA);
+});
diff --git a/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.11.1.js b/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.11.1.js
new file mode 100644
index 000000000..17d59a317
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.11.1.js
@@ -0,0 +1,196 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_events_test_runner.js */
+"use strict";
+
+// Test that markup view event bubbles show the correct event info for jQuery
+// and jQuery Live events (jQuery version 1.11.1).
+
+const TEST_LIB = "lib_jquery_1.11.1_min.js";
+const TEST_URL = URL_ROOT + "doc_markup_events_jquery.html?" + TEST_LIB;
+
+loadHelperScript("helper_events_test_runner.js");
+
+/*eslint-disable */
+const TEST_DATA = [
+ {
+ selector: "html",
+ expected: [
+ {
+ type: "load",
+ filename: TEST_URL + ":27",
+ attributes: [
+ "Bubbling",
+ "DOM2"
+ ],
+ handler: "() => {\n" +
+ " var handler1 = function liveDivDblClick() {\n" +
+ " alert(1);\n" +
+ " };\n" +
+ " var handler2 = function liveDivDragStart() {\n" +
+ " alert(2);\n" +
+ " };\n" +
+ " var handler3 = function liveDivDragLeave() {\n" +
+ " alert(3);\n" +
+ " };\n" +
+ " var handler4 = function liveDivDragEnd() {\n" +
+ " alert(4);\n" +
+ " };\n" +
+ " var handler5 = function liveDivDrop() {\n" +
+ " alert(5);\n" +
+ " };\n" +
+ " var handler6 = function liveDivDragOver() {\n" +
+ " alert(6);\n" +
+ " };\n" +
+ " var handler7 = function divClick1() {\n" +
+ " alert(7);\n" +
+ " };\n" +
+ " var handler8 = function divClick2() {\n" +
+ " alert(8);\n" +
+ " };\n" +
+ " var handler9 = function divKeyDown() {\n" +
+ " alert(9);\n" +
+ " };\n" +
+ " var handler10 = function divDragOut() {\n" +
+ " alert(10);\n" +
+ " };\n" +
+ "\n" +
+ " if ($(\"#livediv\").live) {\n" +
+ " $(\"#livediv\").live(\"dblclick\", handler1);\n" +
+ " $(\"#livediv\").live(\"dragstart\", handler2);\n" +
+ " }\n" +
+ "\n" +
+ " if ($(\"#livediv\").delegate) {\n" +
+ " $(document).delegate(\"#livediv\", \"dragleave\", handler3);\n" +
+ " $(document).delegate(\"#livediv\", \"dragend\", handler4);\n" +
+ " }\n" +
+ "\n" +
+ " if ($(\"#livediv\").on) {\n" +
+ " $(document).on(\"drop\", \"#livediv\", handler5);\n" +
+ " $(document).on(\"dragover\", \"#livediv\", handler6);\n" +
+ " $(document).on(\"dragout\", \"#livediv:xxxxx\", handler10);\n" +
+ " }\n" +
+ "\n" +
+ " var div = $(\"div\")[0];\n" +
+ " $(div).click(handler7);\n" +
+ " $(div).click(handler8);\n" +
+ " $(div).keydown(handler9);\n" +
+ "}"
+ }
+ ]
+ },
+
+ {
+ selector: "#testdiv",
+ expected: [
+ {
+ type: "click",
+ filename: TEST_URL + ":34",
+ attributes: [
+ "jQuery"
+ ],
+ handler: "var handler7 = function divClick1() {\n" +
+ " alert(7);\n" +
+ "}"
+ },
+ {
+ type: "click",
+ filename: TEST_URL + ":35",
+ attributes: [
+ "jQuery"
+ ],
+ handler: "var handler8 = function divClick2() {\n" +
+ " alert(8);\n" +
+ "}"
+ },
+ {
+ type: "click",
+ filename: URL_ROOT + TEST_LIB + ":3",
+ attributes: [
+ "Bubbling",
+ "DOM2"
+ ],
+ handler: "k = r.handle = function(a) {\n" +
+ " return typeof m === K || a && m.event.triggered === a.type ? void 0 : m.event.dispatch.apply(k.elem, arguments)\n" +
+ "}"
+ },
+ {
+ type: "keydown",
+ filename: TEST_URL + ":36",
+ attributes: [
+ "jQuery"
+ ],
+ handler: "var handler9 = function divKeyDown() {\n" +
+ " alert(9);\n" +
+ "}"
+ },
+ {
+ type: "keydown",
+ filename: URL_ROOT + TEST_LIB + ":3",
+ attributes: [
+ "Bubbling",
+ "DOM2"
+ ],
+ handler: "k = r.handle = function(a) {\n" +
+ " return typeof m === K || a && m.event.triggered === a.type ? void 0 : m.event.dispatch.apply(k.elem, arguments)\n" +
+ "}"
+ }
+ ]
+ },
+
+ {
+ selector: "#livediv",
+ expected: [
+ {
+ type: "dragend",
+ filename: TEST_URL + ":31",
+ attributes: [
+ "jQuery",
+ "Live"
+ ],
+ handler: "var handler4 = function liveDivDragEnd() {\n" +
+ " alert(4);\n" +
+ "}"
+ },
+ {
+ type: "dragleave",
+ filename: TEST_URL + ":30",
+ attributes: [
+ "jQuery",
+ "Live"
+ ],
+ handler: "var handler3 = function liveDivDragLeave() {\n" +
+ " alert(3);\n" +
+ "}"
+ },
+ {
+ type: "dragover",
+ filename: TEST_URL + ":33",
+ attributes: [
+ "jQuery",
+ "Live"
+ ],
+ handler: "var handler6 = function liveDivDragOver() {\n" +
+ " alert(6);\n" +
+ "}"
+ },
+ {
+ type: "drop",
+ filename: TEST_URL + ":32",
+ attributes: [
+ "jQuery",
+ "Live"
+ ],
+ handler: "var handler5 = function liveDivDrop() {\n" +
+ " alert(5);\n" +
+ "}"
+ }
+ ]
+ },
+];
+/*eslint-enable */
+
+add_task(function* () {
+ yield runEventPopupTests(TEST_URL, TEST_DATA);
+});
diff --git a/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.2.js b/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.2.js
new file mode 100644
index 000000000..c26a14d66
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.2.js
@@ -0,0 +1,191 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_events_test_runner.js */
+"use strict";
+
+// Test that markup view event bubbles show the correct event info for jQuery
+// and jQuery Live events (jQuery version 1.2).
+
+const TEST_LIB = "lib_jquery_1.2_min.js";
+const TEST_URL = URL_ROOT + "doc_markup_events_jquery.html?" + TEST_LIB;
+
+loadHelperScript("helper_events_test_runner.js");
+
+/*eslint-disable */
+const TEST_DATA = [
+ {
+ selector: "html",
+ expected: [
+ {
+ type: "load",
+ filename: TEST_URL + ":27",
+ attributes: [
+ "Bubbling",
+ "DOM2"
+ ],
+ handler: "() => {\n" +
+ " var handler1 = function liveDivDblClick() {\n" +
+ " alert(1);\n" +
+ " };\n" +
+ " var handler2 = function liveDivDragStart() {\n" +
+ " alert(2);\n" +
+ " };\n" +
+ " var handler3 = function liveDivDragLeave() {\n" +
+ " alert(3);\n" +
+ " };\n" +
+ " var handler4 = function liveDivDragEnd() {\n" +
+ " alert(4);\n" +
+ " };\n" +
+ " var handler5 = function liveDivDrop() {\n" +
+ " alert(5);\n" +
+ " };\n" +
+ " var handler6 = function liveDivDragOver() {\n" +
+ " alert(6);\n" +
+ " };\n" +
+ " var handler7 = function divClick1() {\n" +
+ " alert(7);\n" +
+ " };\n" +
+ " var handler8 = function divClick2() {\n" +
+ " alert(8);\n" +
+ " };\n" +
+ " var handler9 = function divKeyDown() {\n" +
+ " alert(9);\n" +
+ " };\n" +
+ " var handler10 = function divDragOut() {\n" +
+ " alert(10);\n" +
+ " };\n" +
+ "\n" +
+ " if ($(\"#livediv\").live) {\n" +
+ " $(\"#livediv\").live(\"dblclick\", handler1);\n" +
+ " $(\"#livediv\").live(\"dragstart\", handler2);\n" +
+ " }\n" +
+ "\n" +
+ " if ($(\"#livediv\").delegate) {\n" +
+ " $(document).delegate(\"#livediv\", \"dragleave\", handler3);\n" +
+ " $(document).delegate(\"#livediv\", \"dragend\", handler4);\n" +
+ " }\n" +
+ "\n" +
+ " if ($(\"#livediv\").on) {\n" +
+ " $(document).on(\"drop\", \"#livediv\", handler5);\n" +
+ " $(document).on(\"dragover\", \"#livediv\", handler6);\n" +
+ " $(document).on(\"dragout\", \"#livediv:xxxxx\", handler10);\n" +
+ " }\n" +
+ "\n" +
+ " var div = $(\"div\")[0];\n" +
+ " $(div).click(handler7);\n" +
+ " $(div).click(handler8);\n" +
+ " $(div).keydown(handler9);\n" +
+ "}"
+ },
+ {
+ type: "load",
+ filename: URL_ROOT + TEST_LIB,
+ attributes: [
+ "Bubbling",
+ "DOM0"
+ ],
+ handler: "handle: function(event) {\n" +
+ " if (typeof jQuery == \"undefined\") return false;\n" +
+ "\n" +
+ " // Empty object is for triggered events with no data\n" +
+ " event = jQuery.event.fix(event || window.event || {});\n" +
+ "\n" +
+ " // returned undefined or false\n" +
+ " var returnValue;\n" +
+ "\n" +
+ " var c = this.events[event.type];\n" +
+ "\n" +
+ " var args = [].slice.call(arguments, 1);\n" +
+ " args.unshift(event);\n" +
+ "\n" +
+ " for (var j in c) {\n" +
+ " // Pass in a reference to the handler function itself\n" +
+ " // So that we can later remove it\n" +
+ " args[0].handler = c[j];\n" +
+ " args[0].data = c[j].data;\n" +
+ "\n" +
+ " if (c[j].apply(this, args) === false) {\n" +
+ " event.preventDefault();\n" +
+ " event.stopPropagation();\n" +
+ " returnValue = false;\n" +
+ " }\n" +
+ " }\n" +
+ "\n" +
+ " // Clean up added properties in IE to prevent memory leak\n" +
+ " if (jQuery.browser.msie) event.target = event.preventDefault = event.stopPropagation = event.handler = event.data = null;\n" +
+ "\n" +
+ " return returnValue;\n" +
+ "}"
+ }
+ ]
+ },
+ {
+ selector: "#testdiv",
+ expected: [
+ {
+ type: "click",
+ filename: TEST_URL + ":34",
+ attributes: [
+ "jQuery"
+ ],
+ handler: "var handler7 = function divClick1() {\n" +
+ " alert(7);\n" +
+ "}"
+ },
+ {
+ type: "click",
+ filename: TEST_URL + ":35",
+ attributes: [
+ "jQuery"
+ ],
+ handler: "var handler8 = function divClick2() {\n" +
+ " alert(8);\n" +
+ "}"
+ },
+ {
+ type: "click",
+ filename: URL_ROOT + TEST_LIB + ":24",
+ attributes: [
+ "Bubbling",
+ "DOM2"
+ ],
+ handler: "function() {\n" +
+ " var val;\n" +
+ " if (typeof jQuery == \"undefined\" || jQuery.event.triggered) return val;\n" +
+ " val = jQuery.event.handle.apply(element, arguments);\n" +
+ " return val;\n" +
+ "}"
+ },
+ {
+ type: "keydown",
+ filename: TEST_URL + ":36",
+ attributes: [
+ "jQuery"
+ ],
+ handler: "var handler9 = function divKeyDown() {\n" +
+ " alert(9);\n" +
+ "}"
+ },
+ {
+ type: "keydown",
+ filename: URL_ROOT + TEST_LIB + ":24",
+ attributes: [
+ "Bubbling",
+ "DOM2"
+ ],
+ handler: "function() {\n" +
+ " var val;\n" +
+ " if (typeof jQuery == \"undefined\" || jQuery.event.triggered) return val;\n" +
+ " val = jQuery.event.handle.apply(element, arguments);\n" +
+ " return val;\n" +
+ "}"
+ }
+ ]
+ },
+];
+/*eslint-enable */
+
+add_task(function* () {
+ yield runEventPopupTests(TEST_URL, TEST_DATA);
+});
diff --git a/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.3.js b/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.3.js
new file mode 100644
index 000000000..e0bdab2fd
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.3.js
@@ -0,0 +1,224 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_events_test_runner.js */
+"use strict";
+
+// Test that markup view event bubbles show the correct event info for jQuery
+// and jQuery Live events (jQuery version 1.3).
+
+const TEST_LIB = "lib_jquery_1.3_min.js";
+const TEST_URL = URL_ROOT + "doc_markup_events_jquery.html?" + TEST_LIB;
+
+loadHelperScript("helper_events_test_runner.js");
+
+/*eslint-disable */
+const TEST_DATA = [
+ {
+ selector: "html",
+ expected: [
+ {
+ type: "load",
+ filename: URL_ROOT + TEST_LIB + ":19",
+ attributes: [
+ "jQuery"
+ ],
+ handler: "ready: function() {\n" +
+ " if (!n.isReady) {\n" +
+ " n.isReady = true;\n" +
+ " if (n.readyList) {\n" +
+ " n.each(n.readyList, function() {\n" +
+ " this.call(document, n)\n" +
+ " });\n" +
+ " n.readyList = null\n" +
+ " }\n" +
+ " n(document).triggerHandler(\"ready\")\n" +
+ " }\n" +
+ "}"
+ },
+ {
+ type: "load",
+ filename: TEST_URL + ":27",
+ attributes: [
+ "Bubbling",
+ "DOM2"
+ ],
+ handler: "() => {\n" +
+ " var handler1 = function liveDivDblClick() {\n" +
+ " alert(1);\n" +
+ " };\n" +
+ " var handler2 = function liveDivDragStart() {\n" +
+ " alert(2);\n" +
+ " };\n" +
+ " var handler3 = function liveDivDragLeave() {\n" +
+ " alert(3);\n" +
+ " };\n" +
+ " var handler4 = function liveDivDragEnd() {\n" +
+ " alert(4);\n" +
+ " };\n" +
+ " var handler5 = function liveDivDrop() {\n" +
+ " alert(5);\n" +
+ " };\n" +
+ " var handler6 = function liveDivDragOver() {\n" +
+ " alert(6);\n" +
+ " };\n" +
+ " var handler7 = function divClick1() {\n" +
+ " alert(7);\n" +
+ " };\n" +
+ " var handler8 = function divClick2() {\n" +
+ " alert(8);\n" +
+ " };\n" +
+ " var handler9 = function divKeyDown() {\n" +
+ " alert(9);\n" +
+ " };\n" +
+ " var handler10 = function divDragOut() {\n" +
+ " alert(10);\n" +
+ " };\n" +
+ "\n" +
+ " if ($(\"#livediv\").live) {\n" +
+ " $(\"#livediv\").live(\"dblclick\", handler1);\n" +
+ " $(\"#livediv\").live(\"dragstart\", handler2);\n" +
+ " }\n" +
+ "\n" +
+ " if ($(\"#livediv\").delegate) {\n" +
+ " $(document).delegate(\"#livediv\", \"dragleave\", handler3);\n" +
+ " $(document).delegate(\"#livediv\", \"dragend\", handler4);\n" +
+ " }\n" +
+ "\n" +
+ " if ($(\"#livediv\").on) {\n" +
+ " $(document).on(\"drop\", \"#livediv\", handler5);\n" +
+ " $(document).on(\"dragover\", \"#livediv\", handler6);\n" +
+ " $(document).on(\"dragout\", \"#livediv:xxxxx\", handler10);\n" +
+ " }\n" +
+ "\n" +
+ " var div = $(\"div\")[0];\n" +
+ " $(div).click(handler7);\n" +
+ " $(div).click(handler8);\n" +
+ " $(div).keydown(handler9);\n" +
+ "}"
+ },
+ {
+ type: "load",
+ filename: URL_ROOT + TEST_LIB + ":19",
+ attributes: [
+ "Bubbling",
+ "DOM2"
+ ],
+ handler: "function() {\n" +
+ " return typeof n !== \"undefined\" && !n.event.triggered ? n.event.handle.apply(arguments.callee.elem, arguments) : g\n" +
+ "}"
+ },
+ {
+ type: "unload",
+ filename: URL_ROOT + TEST_LIB + ":19",
+ attributes: [
+ "jQuery"
+ ],
+ handler: "function(H) {\n" +
+ " n(this).unbind(H, D);\n" +
+ " return (E || G).apply(this, arguments)\n" +
+ "}"
+ },
+ {
+ type: "unload",
+ filename: URL_ROOT + TEST_LIB + ":19",
+ attributes: [
+ "Bubbling",
+ "DOM2"
+ ],
+ handler: "function() {\n" +
+ " return typeof n !== \"undefined\" && !n.event.triggered ? n.event.handle.apply(arguments.callee.elem, arguments) : g\n" +
+ "}"
+ }
+ ]
+ },
+ {
+ selector: "#testdiv",
+ expected: [
+ {
+ type: "click",
+ filename: TEST_URL + ":34",
+ attributes: [
+ "jQuery"
+ ],
+ handler: "var handler7 = function divClick1() {\n" +
+ " alert(7);\n" +
+ "}"
+ },
+ {
+ type: "click",
+ filename: TEST_URL + ":35",
+ attributes: [
+ "jQuery"
+ ],
+ handler: "var handler8 = function divClick2() {\n" +
+ " alert(8);\n" +
+ "}"
+ },
+ {
+ type: "click",
+ filename: URL_ROOT + TEST_LIB + ":19",
+ attributes: [
+ "Bubbling",
+ "DOM2"
+ ],
+ handler: "function() {\n" +
+ " return typeof n !== \"undefined\" && !n.event.triggered ? n.event.handle.apply(arguments.callee.elem, arguments) : g\n" +
+ "}"
+ },
+ {
+ type: "keydown",
+ filename: TEST_URL + ":36",
+ attributes: [
+ "jQuery"
+ ],
+ handler: "var handler9 = function divKeyDown() {\n" +
+ " alert(9);\n" +
+ "}"
+ },
+ {
+ type: "keydown",
+ filename: URL_ROOT + TEST_LIB + ":19",
+ attributes: [
+ "Bubbling",
+ "DOM2"
+ ],
+ handler: "function() {\n" +
+ " return typeof n !== \"undefined\" && !n.event.triggered ? n.event.handle.apply(arguments.callee.elem, arguments) : g\n" +
+ "}"
+ }
+ ]
+ },
+ {
+ selector: "#livediv",
+ expected: [
+ {
+ type: "dblclick",
+ filename: TEST_URL + ":28",
+ attributes: [
+ "jQuery",
+ "Live"
+ ],
+ handler: "var handler1 = function liveDivDblClick() {\n" +
+ " alert(1);\n" +
+ "}"
+ },
+ {
+ type: "dragstart",
+ filename: TEST_URL + ":29",
+ attributes: [
+ "jQuery",
+ "Live"
+ ],
+ handler: "var handler2 = function liveDivDragStart() {\n" +
+ " alert(2);\n" +
+ "}"
+ }
+ ]
+ },
+];
+/*eslint-enable */
+
+add_task(function* () {
+ yield runEventPopupTests(TEST_URL, TEST_DATA);
+});
diff --git a/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.4.js b/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.4.js
new file mode 100644
index 000000000..9f7d9e241
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.4.js
@@ -0,0 +1,287 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_events_test_runner.js */
+"use strict";
+
+// Test that markup view event bubbles show the correct event info for jQuery
+// and jQuery Live events (jQuery version 1.4).
+
+const TEST_LIB = "lib_jquery_1.4_min.js";
+const TEST_URL = URL_ROOT + "doc_markup_events_jquery.html?" + TEST_LIB;
+
+loadHelperScript("helper_events_test_runner.js");
+
+/*eslint-disable */
+const TEST_DATA = [
+ {
+ selector: "html",
+ expected: [
+ {
+ type: "load",
+ filename: TEST_URL + ":27",
+ attributes: [
+ "Bubbling",
+ "DOM2"
+ ],
+ handler: "() => {\n" +
+ " var handler1 = function liveDivDblClick() {\n" +
+ " alert(1);\n" +
+ " };\n" +
+ " var handler2 = function liveDivDragStart() {\n" +
+ " alert(2);\n" +
+ " };\n" +
+ " var handler3 = function liveDivDragLeave() {\n" +
+ " alert(3);\n" +
+ " };\n" +
+ " var handler4 = function liveDivDragEnd() {\n" +
+ " alert(4);\n" +
+ " };\n" +
+ " var handler5 = function liveDivDrop() {\n" +
+ " alert(5);\n" +
+ " };\n" +
+ " var handler6 = function liveDivDragOver() {\n" +
+ " alert(6);\n" +
+ " };\n" +
+ " var handler7 = function divClick1() {\n" +
+ " alert(7);\n" +
+ " };\n" +
+ " var handler8 = function divClick2() {\n" +
+ " alert(8);\n" +
+ " };\n" +
+ " var handler9 = function divKeyDown() {\n" +
+ " alert(9);\n" +
+ " };\n" +
+ " var handler10 = function divDragOut() {\n" +
+ " alert(10);\n" +
+ " };\n" +
+ "\n" +
+ " if ($(\"#livediv\").live) {\n" +
+ " $(\"#livediv\").live(\"dblclick\", handler1);\n" +
+ " $(\"#livediv\").live(\"dragstart\", handler2);\n" +
+ " }\n" +
+ "\n" +
+ " if ($(\"#livediv\").delegate) {\n" +
+ " $(document).delegate(\"#livediv\", \"dragleave\", handler3);\n" +
+ " $(document).delegate(\"#livediv\", \"dragend\", handler4);\n" +
+ " }\n" +
+ "\n" +
+ " if ($(\"#livediv\").on) {\n" +
+ " $(document).on(\"drop\", \"#livediv\", handler5);\n" +
+ " $(document).on(\"dragover\", \"#livediv\", handler6);\n" +
+ " $(document).on(\"dragout\", \"#livediv:xxxxx\", handler10);\n" +
+ " }\n" +
+ "\n" +
+ " var div = $(\"div\")[0];\n" +
+ " $(div).click(handler7);\n" +
+ " $(div).click(handler8);\n" +
+ " $(div).keydown(handler9);\n" +
+ "}"
+ },
+ {
+ type: "load",
+ filename: URL_ROOT + TEST_LIB + ":26",
+ attributes: [
+ "Bubbling",
+ "DOM2"
+ ],
+ handler: "ready: function() {\n" +
+ " if (!c.isReady) {\n" +
+ " if (!s.body) return setTimeout(c.ready, 13);\n" +
+ " c.isReady = true;\n" +
+ " if (Q) {\n" +
+ " for (var a, b = 0; a = Q[b++];) a.call(s, c);\n" +
+ " Q = null\n" +
+ " }\n" +
+ " c.fn.triggerHandler && c(s).triggerHandler(\"ready\")\n" +
+ " }\n" +
+ "}"
+ }
+ ]
+ },
+ {
+ selector: "#testdiv",
+ expected: [
+ {
+ type: "click",
+ filename: TEST_URL + ":34",
+ attributes: [
+ "jQuery"
+ ],
+ handler: "var handler7 = function divClick1() {\n" +
+ " alert(7);\n" +
+ "}"
+ },
+ {
+ type: "click",
+ filename: TEST_URL + ":35",
+ attributes: [
+ "jQuery"
+ ],
+ handler: "var handler8 = function divClick2() {\n" +
+ " alert(8);\n" +
+ "}"
+ },
+ {
+ type: "click",
+ filename: URL_ROOT + TEST_LIB + ":48",
+ attributes: [
+ "Bubbling",
+ "DOM2"
+ ],
+ handler: "j = function() {\n" +
+ " return typeof c !== \"undefined\" && !c.event.triggered ? c.event.handle.apply(j.elem, arguments) : w\n" +
+ "}"
+ },
+ {
+ type: "keydown",
+ filename: TEST_URL + ":36",
+ attributes: [
+ "jQuery"
+ ],
+ handler: "var handler9 = function divKeyDown() {\n" +
+ " alert(9);\n" +
+ "}"
+ },
+ {
+ type: "keydown",
+ filename: URL_ROOT + TEST_LIB + ":48",
+ attributes: [
+ "Bubbling",
+ "DOM2"
+ ],
+ handler: "j = function() {\n" +
+ " return typeof c !== \"undefined\" && !c.event.triggered ? c.event.handle.apply(j.elem, arguments) : w\n" +
+ "}"
+ }
+ ]
+ },
+ {
+ selector: "#livediv",
+ expected: [
+ {
+ type: "dblclick",
+ filename: TEST_URL + ":28",
+ attributes: [
+ "jQuery",
+ "Live"
+ ],
+ handler: "var handler1 = function liveDivDblClick() {\n" +
+ " alert(1);\n" +
+ "}"
+ },
+ {
+ type: "dblclick",
+ filename: URL_ROOT + TEST_LIB + ":17",
+ attributes: [
+ "jQuery",
+ "Live"
+ ],
+ handler: "function qa(a) {\n" +
+ " var b = true,\n" +
+ " d = [],\n" +
+ " f = [],\n" +
+ " e = arguments,\n" +
+ " i, j, o, p, n, t = c.extend({}, c.data(this, \"events\").live);\n" +
+ " for (p in t) {\n" +
+ " j = t[p];\n" +
+ " if (j.live === a.type || j.altLive && c.inArray(a.type, j.altLive) > -1) {\n" +
+ " i = j.data;\n" +
+ " i.beforeFilter && i.beforeFilter[a.type] && !i.beforeFilter[a.type](a) || f.push(j.selector)\n" +
+ " } else delete t[p]\n" +
+ " }\n" +
+ " i = c(a.target).closest(f, a.currentTarget);\n" +
+ " n = 0;\n" +
+ " for (l = i.length; n < l; n++)\n" +
+ " for (p in t) {\n" +
+ " j = t[p];\n" +
+ " o = i[n].elem;\n" +
+ " f = null;\n" +
+ " if (i[n].selector === j.selector) {\n" +
+ " if (j.live === \"mouseenter\" || j.live === \"mouseleave\") f = c(a.relatedTarget).closest(j.selector)[0];\n" +
+ " if (!f || f !== o) d.push({\n" +
+ " elem: o,\n" +
+ " fn: j\n" +
+ " })\n" +
+ " }\n" +
+ " }\n" +
+ " n = 0;\n" +
+ " for (l = d.length; n < l; n++) {\n" +
+ " i = d[n];\n" +
+ " a.currentTarget = i.elem;\n" +
+ " a.data = i.fn.data;\n" +
+ " if (i.fn.apply(i.elem, e) === false) {\n" +
+ " b = false;\n" +
+ " break\n" +
+ " }\n" +
+ " }\n" +
+ " return b\n" +
+ "}"
+ },
+ {
+ type: "dragstart",
+ filename: TEST_URL + ":29",
+ attributes: [
+ "jQuery",
+ "Live"
+ ],
+ handler: "var handler2 = function liveDivDragStart() {\n" +
+ " alert(2);\n" +
+ "}"
+ },
+ {
+ type: "dragstart",
+ filename: URL_ROOT + TEST_LIB + ":17",
+ attributes: [
+ "jQuery",
+ "Live"
+ ],
+ handler: "function qa(a) {\n" +
+ " var b = true,\n" +
+ " d = [],\n" +
+ " f = [],\n" +
+ " e = arguments,\n" +
+ " i, j, o, p, n, t = c.extend({}, c.data(this, \"events\").live);\n" +
+ " for (p in t) {\n" +
+ " j = t[p];\n" +
+ " if (j.live === a.type || j.altLive && c.inArray(a.type, j.altLive) > -1) {\n" +
+ " i = j.data;\n" +
+ " i.beforeFilter && i.beforeFilter[a.type] && !i.beforeFilter[a.type](a) || f.push(j.selector)\n" +
+ " } else delete t[p]\n" +
+ " }\n" +
+ " i = c(a.target).closest(f, a.currentTarget);\n" +
+ " n = 0;\n" +
+ " for (l = i.length; n < l; n++)\n" +
+ " for (p in t) {\n" +
+ " j = t[p];\n" +
+ " o = i[n].elem;\n" +
+ " f = null;\n" +
+ " if (i[n].selector === j.selector) {\n" +
+ " if (j.live === \"mouseenter\" || j.live === \"mouseleave\") f = c(a.relatedTarget).closest(j.selector)[0];\n" +
+ " if (!f || f !== o) d.push({\n" +
+ " elem: o,\n" +
+ " fn: j\n" +
+ " })\n" +
+ " }\n" +
+ " }\n" +
+ " n = 0;\n" +
+ " for (l = d.length; n < l; n++) {\n" +
+ " i = d[n];\n" +
+ " a.currentTarget = i.elem;\n" +
+ " a.data = i.fn.data;\n" +
+ " if (i.fn.apply(i.elem, e) === false) {\n" +
+ " b = false;\n" +
+ " break\n" +
+ " }\n" +
+ " }\n" +
+ " return b\n" +
+ "}"
+ }
+ ]
+ },
+];
+/*eslint-enable */
+
+add_task(function* () {
+ yield runEventPopupTests(TEST_URL, TEST_DATA);
+});
diff --git a/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.6.js b/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.6.js
new file mode 100644
index 000000000..f89bd0740
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.6.js
@@ -0,0 +1,388 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_events_test_runner.js */
+"use strict";
+
+requestLongerTimeout(2);
+
+// Test that markup view event bubbles show the correct event info for jQuery
+// and jQuery Live events (jQuery version 1.6).
+
+const TEST_LIB = "lib_jquery_1.6_min.js";
+const TEST_URL = URL_ROOT + "doc_markup_events_jquery.html?" + TEST_LIB;
+
+loadHelperScript("helper_events_test_runner.js");
+
+/*eslint-disable */
+const TEST_DATA = [
+ {
+ selector: "html",
+ expected: [
+ {
+ type: "load",
+ filename: TEST_URL + ":27",
+ attributes: [
+ "Bubbling",
+ "DOM2"
+ ],
+ handler: "() => {\n" +
+ " var handler1 = function liveDivDblClick() {\n" +
+ " alert(1);\n" +
+ " };\n" +
+ " var handler2 = function liveDivDragStart() {\n" +
+ " alert(2);\n" +
+ " };\n" +
+ " var handler3 = function liveDivDragLeave() {\n" +
+ " alert(3);\n" +
+ " };\n" +
+ " var handler4 = function liveDivDragEnd() {\n" +
+ " alert(4);\n" +
+ " };\n" +
+ " var handler5 = function liveDivDrop() {\n" +
+ " alert(5);\n" +
+ " };\n" +
+ " var handler6 = function liveDivDragOver() {\n" +
+ " alert(6);\n" +
+ " };\n" +
+ " var handler7 = function divClick1() {\n" +
+ " alert(7);\n" +
+ " };\n" +
+ " var handler8 = function divClick2() {\n" +
+ " alert(8);\n" +
+ " };\n" +
+ " var handler9 = function divKeyDown() {\n" +
+ " alert(9);\n" +
+ " };\n" +
+ " var handler10 = function divDragOut() {\n" +
+ " alert(10);\n" +
+ " };\n" +
+ "\n" +
+ " if ($(\"#livediv\").live) {\n" +
+ " $(\"#livediv\").live(\"dblclick\", handler1);\n" +
+ " $(\"#livediv\").live(\"dragstart\", handler2);\n" +
+ " }\n" +
+ "\n" +
+ " if ($(\"#livediv\").delegate) {\n" +
+ " $(document).delegate(\"#livediv\", \"dragleave\", handler3);\n" +
+ " $(document).delegate(\"#livediv\", \"dragend\", handler4);\n" +
+ " }\n" +
+ "\n" +
+ " if ($(\"#livediv\").on) {\n" +
+ " $(document).on(\"drop\", \"#livediv\", handler5);\n" +
+ " $(document).on(\"dragover\", \"#livediv\", handler6);\n" +
+ " $(document).on(\"dragout\", \"#livediv:xxxxx\", handler10);\n" +
+ " }\n" +
+ "\n" +
+ " var div = $(\"div\")[0];\n" +
+ " $(div).click(handler7);\n" +
+ " $(div).click(handler8);\n" +
+ " $(div).keydown(handler9);\n" +
+ "}"
+ },
+ {
+ type: "load",
+ filename: URL_ROOT + TEST_LIB + ":16",
+ attributes: [
+ "Bubbling",
+ "DOM2"
+ ],
+ handler: "ready: function(a) {\n" +
+ " if (a === !0 && !--e.readyWait || a !== !0 && !e.isReady) {\n" +
+ " if (!c.body) return setTimeout(e.ready, 1);\n" +
+ " e.isReady = !0;\n" +
+ " if (a !== !0 && --e.readyWait > 0) return;\n" +
+ " y.resolveWith(c, [e]), e.fn.trigger && e(c).trigger(\"ready\").unbind(\"ready\")\n" +
+ " }\n" +
+ "}"
+ }
+ ]
+ },
+ {
+ selector: "#testdiv",
+ expected: [
+ {
+ type: "click",
+ filename: TEST_URL + ":34",
+ attributes: [
+ "jQuery"
+ ],
+ handler: "var handler7 = function divClick1() {\n" +
+ " alert(7);\n" +
+ "}"
+ },
+ {
+ type: "click",
+ filename: TEST_URL + ":35",
+ attributes: [
+ "jQuery"
+ ],
+ handler: "var handler8 = function divClick2() {\n" +
+ " alert(8);\n" +
+ "}"
+ },
+ {
+ type: "click",
+ filename: URL_ROOT + TEST_LIB + ":16",
+ attributes: [
+ "Bubbling",
+ "DOM2"
+ ],
+ handler: "i.handle = k = function(a) {\n" +
+ " return typeof f != \"undefined\" && (!a || f.event.triggered !== a.type) ? f.event.handle.apply(k.elem, arguments) : b\n" +
+ "}"
+ },
+ {
+ type: "keydown",
+ filename: TEST_URL + ":36",
+ attributes: [
+ "jQuery"
+ ],
+ handler: "var handler9 = function divKeyDown() {\n" +
+ " alert(9);\n" +
+ "}"
+ },
+ {
+ type: "keydown",
+ filename: URL_ROOT + TEST_LIB + ":16",
+ attributes: [
+ "Bubbling",
+ "DOM2"
+ ],
+ handler: "i.handle = k = function(a) {\n" +
+ " return typeof f != \"undefined\" && (!a || f.event.triggered !== a.type) ? f.event.handle.apply(k.elem, arguments) : b\n" +
+ "}"
+ }
+ ]
+ },
+ {
+ selector: "#livediv",
+ expected: [
+ {
+ type: "dblclick",
+ filename: TEST_URL + ":28",
+ attributes: [
+ "jQuery",
+ "Live"
+ ],
+ handler: "var handler1 = function liveDivDblClick() {\n" +
+ " alert(1);\n" +
+ "}"
+ },
+ {
+ type: "dblclick",
+ filename: URL_ROOT + TEST_LIB + ":16",
+ attributes: [
+ "jQuery",
+ "Live"
+ ],
+ handler: "function M(a) {\n" +
+ " var b, c, d, e, g, h, i, j, k, l, m, n, o, p = [],\n" +
+ " q = [],\n" +
+ " r = f._data(this, \"events\");\n" +
+ " if (!(a.liveFired === this || !r || !r.live || a.target.disabled || a.button && a.type === \"click\")) {\n" +
+ " a.namespace && (n = new RegExp(\"(^|\\\\.)\" + a.namespace.split(\".\").join(\"\\\\.(?:.*\\\\.)?\") + \"(\\\\.|$)\")), a.liveFired = this;\n" +
+ " var s = r.live.slice(0);\n" +
+ " for (i = 0; i < s.length; i++) g = s[i], g.origType.replace(x, \"\") === a.type ? q.push(g.selector) : s.splice(i--, 1);\n" +
+ " e = f(a.target).closest(q, a.currentTarget);\n" +
+ " for (j = 0, k = e.length; j < k; j++) {\n" +
+ " m = e[j];\n" +
+ " for (i = 0; i < s.length; i++) {\n" +
+ " g = s[i];\n" +
+ " if (m.selector === g.selector && (!n || n.test(g.namespace)) && !m.elem.disabled) {\n" +
+ " h = m.elem, d = null;\n" +
+ " if (g.preType === \"mouseenter\" || g.preType === \"mouseleave\") a.type = g.preType, d = f(a.relatedTarget).closest(g.selector)[0], d && f.contains(h, d) && (d = h);\n" +
+ " (!d || d !== h) && p.push({\n" +
+ " elem: h,\n" +
+ " handleObj: g,\n" +
+ " level: m.level\n" +
+ " })\n" +
+ " }\n" +
+ " }\n" +
+ " }\n" +
+ " for (j = 0, k = p.length; j < k; j++) {\n" +
+ " e = p[j];\n" +
+ " if (c && e.level > c) break;\n" +
+ " a.currentTarget = e.elem, a.data = e.handleObj.data, a.handleObj = e.handleObj, o = e.handleObj.origHandler.apply(e.elem, arguments);\n" +
+ " if (o === !1 || a.isPropagationStopped()) {\n" +
+ " c = e.level, o === !1 && (b = !1);\n" +
+ " if (a.isImmediatePropagationStopped()) break\n" +
+ " }\n" +
+ " }\n" +
+ " return b\n" +
+ " }\n" +
+ "}"
+ },
+ {
+ type: "dragend",
+ filename: TEST_URL + ":31",
+ attributes: [
+ "jQuery",
+ "Live"
+ ],
+ handler: "var handler4 = function liveDivDragEnd() {\n" +
+ " alert(4);\n" +
+ "}"
+ },
+ {
+ type: "dragend",
+ filename: URL_ROOT + TEST_LIB + ":16",
+ attributes: [
+ "jQuery",
+ "Live"
+ ],
+ handler: "function M(a) {\n" +
+ " var b, c, d, e, g, h, i, j, k, l, m, n, o, p = [],\n" +
+ " q = [],\n" +
+ " r = f._data(this, \"events\");\n" +
+ " if (!(a.liveFired === this || !r || !r.live || a.target.disabled || a.button && a.type === \"click\")) {\n" +
+ " a.namespace && (n = new RegExp(\"(^|\\\\.)\" + a.namespace.split(\".\").join(\"\\\\.(?:.*\\\\.)?\") + \"(\\\\.|$)\")), a.liveFired = this;\n" +
+ " var s = r.live.slice(0);\n" +
+ " for (i = 0; i < s.length; i++) g = s[i], g.origType.replace(x, \"\") === a.type ? q.push(g.selector) : s.splice(i--, 1);\n" +
+ " e = f(a.target).closest(q, a.currentTarget);\n" +
+ " for (j = 0, k = e.length; j < k; j++) {\n" +
+ " m = e[j];\n" +
+ " for (i = 0; i < s.length; i++) {\n" +
+ " g = s[i];\n" +
+ " if (m.selector === g.selector && (!n || n.test(g.namespace)) && !m.elem.disabled) {\n" +
+ " h = m.elem, d = null;\n" +
+ " if (g.preType === \"mouseenter\" || g.preType === \"mouseleave\") a.type = g.preType, d = f(a.relatedTarget).closest(g.selector)[0], d && f.contains(h, d) && (d = h);\n" +
+ " (!d || d !== h) && p.push({\n" +
+ " elem: h,\n" +
+ " handleObj: g,\n" +
+ " level: m.level\n" +
+ " })\n" +
+ " }\n" +
+ " }\n" +
+ " }\n" +
+ " for (j = 0, k = p.length; j < k; j++) {\n" +
+ " e = p[j];\n" +
+ " if (c && e.level > c) break;\n" +
+ " a.currentTarget = e.elem, a.data = e.handleObj.data, a.handleObj = e.handleObj, o = e.handleObj.origHandler.apply(e.elem, arguments);\n" +
+ " if (o === !1 || a.isPropagationStopped()) {\n" +
+ " c = e.level, o === !1 && (b = !1);\n" +
+ " if (a.isImmediatePropagationStopped()) break\n" +
+ " }\n" +
+ " }\n" +
+ " return b\n" +
+ " }\n" +
+ "}"
+ },
+ {
+ type: "dragleave",
+ filename: TEST_URL + ":30",
+ attributes: [
+ "jQuery",
+ "Live"
+ ],
+ handler: "var handler3 = function liveDivDragLeave() {\n" +
+ " alert(3);\n" +
+ "}"
+ },
+ {
+ type: "dragleave",
+ filename: URL_ROOT + TEST_LIB + ":16",
+ attributes: [
+ "jQuery",
+ "Live"
+ ],
+ handler: "function M(a) {\n" +
+ " var b, c, d, e, g, h, i, j, k, l, m, n, o, p = [],\n" +
+ " q = [],\n" +
+ " r = f._data(this, \"events\");\n" +
+ " if (!(a.liveFired === this || !r || !r.live || a.target.disabled || a.button && a.type === \"click\")) {\n" +
+ " a.namespace && (n = new RegExp(\"(^|\\\\.)\" + a.namespace.split(\".\").join(\"\\\\.(?:.*\\\\.)?\") + \"(\\\\.|$)\")), a.liveFired = this;\n" +
+ " var s = r.live.slice(0);\n" +
+ " for (i = 0; i < s.length; i++) g = s[i], g.origType.replace(x, \"\") === a.type ? q.push(g.selector) : s.splice(i--, 1);\n" +
+ " e = f(a.target).closest(q, a.currentTarget);\n" +
+ " for (j = 0, k = e.length; j < k; j++) {\n" +
+ " m = e[j];\n" +
+ " for (i = 0; i < s.length; i++) {\n" +
+ " g = s[i];\n" +
+ " if (m.selector === g.selector && (!n || n.test(g.namespace)) && !m.elem.disabled) {\n" +
+ " h = m.elem, d = null;\n" +
+ " if (g.preType === \"mouseenter\" || g.preType === \"mouseleave\") a.type = g.preType, d = f(a.relatedTarget).closest(g.selector)[0], d && f.contains(h, d) && (d = h);\n" +
+ " (!d || d !== h) && p.push({\n" +
+ " elem: h,\n" +
+ " handleObj: g,\n" +
+ " level: m.level\n" +
+ " })\n" +
+ " }\n" +
+ " }\n" +
+ " }\n" +
+ " for (j = 0, k = p.length; j < k; j++) {\n" +
+ " e = p[j];\n" +
+ " if (c && e.level > c) break;\n" +
+ " a.currentTarget = e.elem, a.data = e.handleObj.data, a.handleObj = e.handleObj, o = e.handleObj.origHandler.apply(e.elem, arguments);\n" +
+ " if (o === !1 || a.isPropagationStopped()) {\n" +
+ " c = e.level, o === !1 && (b = !1);\n" +
+ " if (a.isImmediatePropagationStopped()) break\n" +
+ " }\n" +
+ " }\n" +
+ " return b\n" +
+ " }\n" +
+ "}"
+ },
+ {
+ type: "dragstart",
+ filename: TEST_URL + ":29",
+ attributes: [
+ "jQuery",
+ "Live"
+ ],
+ handler: "var handler2 = function liveDivDragStart() {\n" +
+ " alert(2);\n" +
+ "}"
+ },
+ {
+ type: "dragstart",
+ filename: URL_ROOT + TEST_LIB + ":16",
+ attributes: [
+ "jQuery",
+ "Live"
+ ],
+ handler: "function M(a) {\n" +
+ " var b, c, d, e, g, h, i, j, k, l, m, n, o, p = [],\n" +
+ " q = [],\n" +
+ " r = f._data(this, \"events\");\n" +
+ " if (!(a.liveFired === this || !r || !r.live || a.target.disabled || a.button && a.type === \"click\")) {\n" +
+ " a.namespace && (n = new RegExp(\"(^|\\\\.)\" + a.namespace.split(\".\").join(\"\\\\.(?:.*\\\\.)?\") + \"(\\\\.|$)\")), a.liveFired = this;\n" +
+ " var s = r.live.slice(0);\n" +
+ " for (i = 0; i < s.length; i++) g = s[i], g.origType.replace(x, \"\") === a.type ? q.push(g.selector) : s.splice(i--, 1);\n" +
+ " e = f(a.target).closest(q, a.currentTarget);\n" +
+ " for (j = 0, k = e.length; j < k; j++) {\n" +
+ " m = e[j];\n" +
+ " for (i = 0; i < s.length; i++) {\n" +
+ " g = s[i];\n" +
+ " if (m.selector === g.selector && (!n || n.test(g.namespace)) && !m.elem.disabled) {\n" +
+ " h = m.elem, d = null;\n" +
+ " if (g.preType === \"mouseenter\" || g.preType === \"mouseleave\") a.type = g.preType, d = f(a.relatedTarget).closest(g.selector)[0], d && f.contains(h, d) && (d = h);\n" +
+ " (!d || d !== h) && p.push({\n" +
+ " elem: h,\n" +
+ " handleObj: g,\n" +
+ " level: m.level\n" +
+ " })\n" +
+ " }\n" +
+ " }\n" +
+ " }\n" +
+ " for (j = 0, k = p.length; j < k; j++) {\n" +
+ " e = p[j];\n" +
+ " if (c && e.level > c) break;\n" +
+ " a.currentTarget = e.elem, a.data = e.handleObj.data, a.handleObj = e.handleObj, o = e.handleObj.origHandler.apply(e.elem, arguments);\n" +
+ " if (o === !1 || a.isPropagationStopped()) {\n" +
+ " c = e.level, o === !1 && (b = !1);\n" +
+ " if (a.isImmediatePropagationStopped()) break\n" +
+ " }\n" +
+ " }\n" +
+ " return b\n" +
+ " }\n" +
+ "}"
+ }
+ ]
+ },
+];
+/*eslint-enable */
+
+add_task(function* () {
+ yield runEventPopupTests(TEST_URL, TEST_DATA);
+});
diff --git a/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.7.js b/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.7.js
new file mode 100644
index 000000000..39f1d54e2
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.7.js
@@ -0,0 +1,234 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_events_test_runner.js */
+"use strict";
+
+requestLongerTimeout(2);
+
+// Test that markup view event bubbles show the correct event info for jQuery
+// and jQuery Live events (jQuery version 1.7).
+
+const TEST_LIB = "lib_jquery_1.7_min.js";
+const TEST_URL = URL_ROOT + "doc_markup_events_jquery.html?" + TEST_LIB;
+
+loadHelperScript("helper_events_test_runner.js");
+
+/*eslint-disable */
+const TEST_DATA = [
+ {
+ selector: "html",
+ expected: [
+ {
+ type: "load",
+ filename: TEST_URL + ":27",
+ attributes: [
+ "Bubbling",
+ "DOM2"
+ ],
+ handler: "() => {\n" +
+ " var handler1 = function liveDivDblClick() {\n" +
+ " alert(1);\n" +
+ " };\n" +
+ " var handler2 = function liveDivDragStart() {\n" +
+ " alert(2);\n" +
+ " };\n" +
+ " var handler3 = function liveDivDragLeave() {\n" +
+ " alert(3);\n" +
+ " };\n" +
+ " var handler4 = function liveDivDragEnd() {\n" +
+ " alert(4);\n" +
+ " };\n" +
+ " var handler5 = function liveDivDrop() {\n" +
+ " alert(5);\n" +
+ " };\n" +
+ " var handler6 = function liveDivDragOver() {\n" +
+ " alert(6);\n" +
+ " };\n" +
+ " var handler7 = function divClick1() {\n" +
+ " alert(7);\n" +
+ " };\n" +
+ " var handler8 = function divClick2() {\n" +
+ " alert(8);\n" +
+ " };\n" +
+ " var handler9 = function divKeyDown() {\n" +
+ " alert(9);\n" +
+ " };\n" +
+ " var handler10 = function divDragOut() {\n" +
+ " alert(10);\n" +
+ " };\n" +
+ "\n" +
+ " if ($(\"#livediv\").live) {\n" +
+ " $(\"#livediv\").live(\"dblclick\", handler1);\n" +
+ " $(\"#livediv\").live(\"dragstart\", handler2);\n" +
+ " }\n" +
+ "\n" +
+ " if ($(\"#livediv\").delegate) {\n" +
+ " $(document).delegate(\"#livediv\", \"dragleave\", handler3);\n" +
+ " $(document).delegate(\"#livediv\", \"dragend\", handler4);\n" +
+ " }\n" +
+ "\n" +
+ " if ($(\"#livediv\").on) {\n" +
+ " $(document).on(\"drop\", \"#livediv\", handler5);\n" +
+ " $(document).on(\"dragover\", \"#livediv\", handler6);\n" +
+ " $(document).on(\"dragout\", \"#livediv:xxxxx\", handler10);\n" +
+ " }\n" +
+ "\n" +
+ " var div = $(\"div\")[0];\n" +
+ " $(div).click(handler7);\n" +
+ " $(div).click(handler8);\n" +
+ " $(div).keydown(handler9);\n" +
+ "}"
+ },
+ {
+ type: "load",
+ filename: URL_ROOT + TEST_LIB + ":2",
+ attributes: [
+ "Bubbling",
+ "DOM2"
+ ],
+ handler: "ready: function(a) {\n" +
+ " if (a === !0 && !--e.readyWait || a !== !0 && !e.isReady) {\n" +
+ " if (!c.body) return setTimeout(e.ready, 1);\n" +
+ " e.isReady = !0;\n" +
+ " if (a !== !0 && --e.readyWait > 0) return;\n" +
+ " B.fireWith(c, [e]), e.fn.trigger && e(c).trigger(\"ready\").unbind(\"ready\")\n" +
+ " }\n" +
+ "}"
+ }
+ ]
+ },
+ {
+ selector: "#testdiv",
+ expected: [
+ {
+ type: "click",
+ filename: TEST_URL + ":34",
+ attributes: [
+ "jQuery"
+ ],
+ handler: "var handler7 = function divClick1() {\n" +
+ " alert(7);\n" +
+ "}"
+ },
+ {
+ type: "click",
+ filename: TEST_URL + ":35",
+ attributes: [
+ "jQuery"
+ ],
+ handler: "var handler8 = function divClick2() {\n" +
+ " alert(8);\n" +
+ "}"
+ },
+ {
+ type: "click",
+ filename: URL_ROOT + TEST_LIB + ":3",
+ attributes: [
+ "Bubbling",
+ "DOM2"
+ ],
+ handler: "h.handle = i = function(a) {\n" +
+ " return typeof f != \"undefined\" && (!a || f.event.triggered !== a.type) ? f.event.dispatch.apply(i.elem, arguments) : b\n" +
+ "}"
+ },
+ {
+ type: "keydown",
+ filename: TEST_URL + ":36",
+ attributes: [
+ "jQuery"
+ ],
+ handler: "var handler9 = function divKeyDown() {\n" +
+ " alert(9);\n" +
+ "}"
+ },
+ {
+ type: "keydown",
+ filename: URL_ROOT + TEST_LIB + ":3",
+ attributes: [
+ "Bubbling",
+ "DOM2"
+ ],
+ handler: "h.handle = i = function(a) {\n" +
+ " return typeof f != \"undefined\" && (!a || f.event.triggered !== a.type) ? f.event.dispatch.apply(i.elem, arguments) : b\n" +
+ "}"
+ }
+ ]
+ },
+ {
+ selector: "#livediv",
+ expected: [
+ {
+ type: "dblclick",
+ filename: TEST_URL + ":28",
+ attributes: [
+ "jQuery",
+ "Live"
+ ],
+ handler: "var handler1 = function liveDivDblClick() {\n" +
+ " alert(1);\n" +
+ "}"
+ },
+ {
+ type: "dragend",
+ filename: TEST_URL + ":31",
+ attributes: [
+ "jQuery",
+ "Live"
+ ],
+ handler: "var handler4 = function liveDivDragEnd() {\n" +
+ " alert(4);\n" +
+ "}"
+ },
+ {
+ type: "dragleave",
+ filename: TEST_URL + ":30",
+ attributes: [
+ "jQuery",
+ "Live"
+ ],
+ handler: "var handler3 = function liveDivDragLeave() {\n" +
+ " alert(3);\n" +
+ "}"
+ },
+ {
+ type: "dragover",
+ filename: TEST_URL + ":33",
+ attributes: [
+ "jQuery",
+ "Live"
+ ],
+ handler: "var handler6 = function liveDivDragOver() {\n" +
+ " alert(6);\n" +
+ "}"
+ },
+ {
+ type: "dragstart",
+ filename: TEST_URL + ":29",
+ attributes: [
+ "jQuery",
+ "Live"
+ ],
+ handler: "var handler2 = function liveDivDragStart() {\n" +
+ " alert(2);\n" +
+ "}"
+ },
+ {
+ type: "drop",
+ filename: TEST_URL + ":32",
+ attributes: [
+ "jQuery",
+ "Live"
+ ],
+ handler: "var handler5 = function liveDivDrop() {\n" +
+ " alert(5);\n" +
+ "}"
+ }
+ ]
+ },
+];
+/*eslint-enable */
+
+add_task(function* () {
+ yield runEventPopupTests(TEST_URL, TEST_DATA);
+});
diff --git a/devtools/client/inspector/markup/test/browser_markup_events_jquery_2.1.1.js b/devtools/client/inspector/markup/test/browser_markup_events_jquery_2.1.1.js
new file mode 100644
index 000000000..c6a6642ea
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_events_jquery_2.1.1.js
@@ -0,0 +1,196 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_events_test_runner.js */
+"use strict";
+
+requestLongerTimeout(2);
+
+// Test that markup view event bubbles show the correct event info for jQuery
+// and jQuery Live events (jQuery version 2.1.1).
+
+const TEST_LIB = "lib_jquery_2.1.1_min.js";
+const TEST_URL = URL_ROOT + "doc_markup_events_jquery.html?" + TEST_LIB;
+
+loadHelperScript("helper_events_test_runner.js");
+
+/*eslint-disable */
+const TEST_DATA = [
+ {
+ selector: "html",
+ expected: [
+ {
+ type: "load",
+ filename: TEST_URL + ":27",
+ attributes: [
+ "Bubbling",
+ "DOM2"
+ ],
+ handler: "() => {\n" +
+ " var handler1 = function liveDivDblClick() {\n" +
+ " alert(1);\n" +
+ " };\n" +
+ " var handler2 = function liveDivDragStart() {\n" +
+ " alert(2);\n" +
+ " };\n" +
+ " var handler3 = function liveDivDragLeave() {\n" +
+ " alert(3);\n" +
+ " };\n" +
+ " var handler4 = function liveDivDragEnd() {\n" +
+ " alert(4);\n" +
+ " };\n" +
+ " var handler5 = function liveDivDrop() {\n" +
+ " alert(5);\n" +
+ " };\n" +
+ " var handler6 = function liveDivDragOver() {\n" +
+ " alert(6);\n" +
+ " };\n" +
+ " var handler7 = function divClick1() {\n" +
+ " alert(7);\n" +
+ " };\n" +
+ " var handler8 = function divClick2() {\n" +
+ " alert(8);\n" +
+ " };\n" +
+ " var handler9 = function divKeyDown() {\n" +
+ " alert(9);\n" +
+ " };\n" +
+ " var handler10 = function divDragOut() {\n" +
+ " alert(10);\n" +
+ " };\n" +
+ "\n" +
+ " if ($(\"#livediv\").live) {\n" +
+ " $(\"#livediv\").live(\"dblclick\", handler1);\n" +
+ " $(\"#livediv\").live(\"dragstart\", handler2);\n" +
+ " }\n" +
+ "\n" +
+ " if ($(\"#livediv\").delegate) {\n" +
+ " $(document).delegate(\"#livediv\", \"dragleave\", handler3);\n" +
+ " $(document).delegate(\"#livediv\", \"dragend\", handler4);\n" +
+ " }\n" +
+ "\n" +
+ " if ($(\"#livediv\").on) {\n" +
+ " $(document).on(\"drop\", \"#livediv\", handler5);\n" +
+ " $(document).on(\"dragover\", \"#livediv\", handler6);\n" +
+ " $(document).on(\"dragout\", \"#livediv:xxxxx\", handler10);\n" +
+ " }\n" +
+ "\n" +
+ " var div = $(\"div\")[0];\n" +
+ " $(div).click(handler7);\n" +
+ " $(div).click(handler8);\n" +
+ " $(div).keydown(handler9);\n" +
+ "}"
+ }
+ ]
+ },
+ {
+ selector: "#testdiv",
+ expected: [
+ {
+ type: "click",
+ filename: TEST_URL + ":34",
+ attributes: [
+ "jQuery"
+ ],
+ handler: "var handler7 = function divClick1() {\n" +
+ " alert(7);\n" +
+ "}"
+ },
+ {
+ type: "click",
+ filename: TEST_URL + ":35",
+ attributes: [
+ "jQuery"
+ ],
+ handler: "var handler8 = function divClick2() {\n" +
+ " alert(8);\n" +
+ "}"
+ },
+ {
+ type: "click",
+ filename: URL_ROOT + TEST_LIB + ":3",
+ attributes: [
+ "Bubbling",
+ "DOM2"
+ ],
+ handler: "g = r.handle = function(b) {\n" +
+ " return typeof n !== U && n.event.triggered !== b.type ? n.event.dispatch.apply(a, arguments) : void 0\n" +
+ "}"
+ },
+ {
+ type: "keydown",
+ filename: TEST_URL + ":36",
+ attributes: [
+ "jQuery"
+ ],
+ handler: "var handler9 = function divKeyDown() {\n" +
+ " alert(9);\n" +
+ "}"
+ },
+ {
+ type: "keydown",
+ filename: URL_ROOT + TEST_LIB + ":3",
+ attributes: [
+ "Bubbling",
+ "DOM2"
+ ],
+ handler: "g = r.handle = function(b) {\n" +
+ " return typeof n !== U && n.event.triggered !== b.type ? n.event.dispatch.apply(a, arguments) : void 0\n" +
+ "}"
+ }
+ ]
+ },
+ {
+ selector: "#livediv",
+ expected: [
+ {
+ type: "dragend",
+ filename: TEST_URL + ":31",
+ attributes: [
+ "jQuery",
+ "Live"
+ ],
+ handler: "var handler4 = function liveDivDragEnd() {\n" +
+ " alert(4);\n" +
+ "}"
+ },
+ {
+ type: "dragleave",
+ filename: TEST_URL + ":30",
+ attributes: [
+ "jQuery",
+ "Live"
+ ],
+ handler: "var handler3 = function liveDivDragLeave() {\n" +
+ " alert(3);\n" +
+ "}"
+ },
+ {
+ type: "dragover",
+ filename: TEST_URL + ":33",
+ attributes: [
+ "jQuery",
+ "Live"
+ ],
+ handler: "var handler6 = function liveDivDragOver() {\n" +
+ " alert(6);\n" +
+ "}"
+ },
+ {
+ type: "drop",
+ filename: TEST_URL + ":32",
+ attributes: [
+ "jQuery",
+ "Live"
+ ],
+ handler: "var handler5 = function liveDivDrop() {\n" +
+ " alert(5);\n" +
+ "}"
+ }
+ ]
+ },
+];
+/*eslint-enable */
+
+add_task(function* () {
+ yield runEventPopupTests(TEST_URL, TEST_DATA);
+});
diff --git a/devtools/client/inspector/markup/test/browser_markup_html_edit_01.js b/devtools/client/inspector/markup/test/browser_markup_html_edit_01.js
new file mode 100644
index 000000000..e4c271498
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_html_edit_01.js
@@ -0,0 +1,84 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_outerhtml_test_runner.js */
+"use strict";
+
+// Test outerHTML edition via the markup-view
+
+requestLongerTimeout(2);
+
+loadHelperScript("helper_outerhtml_test_runner.js");
+
+const TEST_DATA = [{
+ selector: "#one",
+ oldHTML: '<div id="one">First <em>Div</em></div>',
+ newHTML: '<div id="one">First Div</div>',
+ validate: function* ({pageNodeFront, selectedNodeFront, testActor}) {
+ let text = yield testActor.getProperty("#one", "textContent");
+ is(text, "First Div", "New div has expected text content");
+ let num = yield testActor.getNumberOfElementMatches("#one em");
+ is(num, 0, "No em remaining");
+ }
+}, {
+ selector: "#removedChildren",
+ oldHTML: "<div id=\"removedChildren\">removedChild " +
+ "<i>Italic <b>Bold <u>Underline</u></b></i> Normal</div>",
+ newHTML: "<div id=\"removedChildren\">removedChild</div>"
+}, {
+ selector: "#addedChildren",
+ oldHTML: '<div id="addedChildren">addedChildren</div>',
+ newHTML: "<div id=\"addedChildren\">addedChildren " +
+ "<i>Italic <b>Bold <u>Underline</u></b></i> Normal</div>"
+}, {
+ selector: "#addedAttribute",
+ oldHTML: '<div id="addedAttribute">addedAttribute</div>',
+ newHTML: "<div id=\"addedAttribute\" class=\"important\" disabled checked>" +
+ "addedAttribute</div>",
+ validate: function* ({pageNodeFront, selectedNodeFront, testActor}) {
+ is(pageNodeFront, selectedNodeFront, "Original element is selected");
+ let html = yield testActor.getProperty("#addedAttribute", "outerHTML");
+ is(html, "<div id=\"addedAttribute\" class=\"important\" disabled=\"\" " +
+ "checked=\"\">addedAttribute</div>", "Attributes have been added");
+ }
+}, {
+ selector: "#changedTag",
+ oldHTML: '<div id="changedTag">changedTag</div>',
+ newHTML: '<p id="changedTag" class="important">changedTag</p>'
+}, {
+ selector: "#siblings",
+ oldHTML: '<div id="siblings">siblings</div>',
+ newHTML: '<div id="siblings-before-sibling">before sibling</div>' +
+ '<div id="siblings">siblings (updated)</div>' +
+ '<div id="siblings-after-sibling">after sibling</div>',
+ validate: function* ({selectedNodeFront, inspector, testActor}) {
+ let beforeSiblingFront = yield getNodeFront("#siblings-before-sibling",
+ inspector);
+ is(beforeSiblingFront, selectedNodeFront, "Sibling has been selected");
+
+ let text = yield testActor.getProperty("#siblings", "textContent");
+ is(text, "siblings (updated)", "New div has expected text content");
+
+ let beforeText = yield testActor.getProperty("#siblings-before-sibling",
+ "textContent");
+ is(beforeText, "before sibling", "Sibling has been inserted");
+
+ let afterText = yield testActor.getProperty("#siblings-after-sibling",
+ "textContent");
+ is(afterText, "after sibling", "Sibling has been inserted");
+ }
+}];
+
+const TEST_URL = "data:text/html," +
+ "<!DOCTYPE html>" +
+ "<head><meta charset='utf-8' /></head>" +
+ "<body>" +
+ TEST_DATA.map(outer => outer.oldHTML).join("\n") +
+ "</body>" +
+ "</html>";
+
+add_task(function* () {
+ let {inspector, testActor} = yield openInspectorForURL(TEST_URL);
+ inspector.markup._frame.focus();
+ yield runEditOuterHTMLTests(TEST_DATA, inspector, testActor);
+});
diff --git a/devtools/client/inspector/markup/test/browser_markup_html_edit_02.js b/devtools/client/inspector/markup/test/browser_markup_html_edit_02.js
new file mode 100644
index 000000000..8f6d0fd14
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_html_edit_02.js
@@ -0,0 +1,119 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_outerhtml_test_runner.js */
+"use strict";
+
+// Test outerHTML edition via the markup-view
+
+loadHelperScript("helper_outerhtml_test_runner.js");
+requestLongerTimeout(2);
+
+const TEST_DATA = [
+ {
+ selector: "#badMarkup1",
+ oldHTML: "<div id=\"badMarkup1\">badMarkup1</div>",
+ newHTML: "<div id=\"badMarkup1\">badMarkup1</div> hanging</div>",
+ validate: function* ({pageNodeFront, selectedNodeFront, testActor}) {
+ is(pageNodeFront, selectedNodeFront, "Original element is selected");
+
+ let textNodeName = yield testActor.eval(`
+ content.document.querySelector("#badMarkup1").nextSibling.nodeName
+ `);
+ let textNodeData = yield testActor.eval(`
+ content.document.querySelector("#badMarkup1").nextSibling.data
+ `);
+ is(textNodeName, "#text", "Sibling is a text element");
+ is(textNodeData, " hanging", "New text node has expected text content");
+ }
+ },
+ {
+ selector: "#badMarkup2",
+ oldHTML: "<div id=\"badMarkup2\">badMarkup2</div>",
+ newHTML: "<div id=\"badMarkup2\">badMarkup2</div> hanging<div></div>" +
+ "</div></div></body>",
+ validate: function* ({pageNodeFront, selectedNodeFront, testActor}) {
+ is(pageNodeFront, selectedNodeFront, "Original element is selected");
+
+ let textNodeName = yield testActor.eval(`
+ content.document.querySelector("#badMarkup2").nextSibling.nodeName
+ `);
+ let textNodeData = yield testActor.eval(`
+ content.document.querySelector("#badMarkup2").nextSibling.data
+ `);
+ is(textNodeName, "#text", "Sibling is a text element");
+ is(textNodeData, " hanging", "New text node has expected text content");
+ }
+ },
+ {
+ selector: "#badMarkup3",
+ oldHTML: "<div id=\"badMarkup3\">badMarkup3</div>",
+ newHTML: "<div id=\"badMarkup3\">badMarkup3 <em>Emphasized <strong> " +
+ "and strong</div>",
+ validate: function* ({pageNodeFront, selectedNodeFront, testActor}) {
+ is(pageNodeFront, selectedNodeFront, "Original element is selected");
+
+ let emText = yield testActor.getProperty("#badMarkup3 em", "textContent");
+ let strongText = yield testActor.getProperty("#badMarkup3 strong",
+ "textContent");
+ is(emText, "Emphasized and strong", "<em> was auto created");
+ is(strongText, " and strong", "<strong> was auto created");
+ }
+ },
+ {
+ selector: "#badMarkup4",
+ oldHTML: "<div id=\"badMarkup4\">badMarkup4</div>",
+ newHTML: "<div id=\"badMarkup4\">badMarkup4</p>",
+ validate: function* ({pageNodeFront, selectedNodeFront, testActor}) {
+ is(pageNodeFront, selectedNodeFront, "Original element is selected");
+
+ let divText = yield testActor.getProperty("#badMarkup4", "textContent");
+ let divTag = yield testActor.getProperty("#badMarkup4", "tagName");
+
+ let pText = yield testActor.getProperty("#badMarkup4 p", "textContent");
+ let pTag = yield testActor.getProperty("#badMarkup4 p", "tagName");
+
+ is(divText, "badMarkup4", "textContent is correct");
+ is(divTag, "DIV", "did not change to <p> tag");
+ is(pText, "", "The <p> tag has no children");
+ is(pTag, "P", "Created an empty <p> tag");
+ }
+ },
+ {
+ selector: "#badMarkup5",
+ oldHTML: "<p id=\"badMarkup5\">badMarkup5</p>",
+ newHTML: "<p id=\"badMarkup5\">badMarkup5 <div>with a nested div</div></p>",
+ validate: function* ({pageNodeFront, selectedNodeFront, testActor}) {
+ is(pageNodeFront, selectedNodeFront, "Original element is selected");
+
+ let num = yield testActor.getNumberOfElementMatches("#badMarkup5 div");
+
+ let pText = yield testActor.getProperty("#badMarkup5", "textContent");
+ let pTag = yield testActor.getProperty("#badMarkup5", "tagName");
+
+ let divText = yield testActor.getProperty("#badMarkup5 ~ div",
+ "textContent");
+ let divTag = yield testActor.getProperty("#badMarkup5 ~ div", "tagName");
+
+ is(num, 0, "The invalid markup got created as a sibling");
+ is(pText, "badMarkup5 ", "The p tag does not take in the div content");
+ is(pTag, "P", "Did not change to a <div> tag");
+ is(divText, "with a nested div", "textContent is correct");
+ is(divTag, "DIV", "Did not change to <p> tag");
+ }
+ }
+];
+
+const TEST_URL = "data:text/html," +
+ "<!DOCTYPE html>" +
+ "<head><meta charset='utf-8' /></head>" +
+ "<body>" +
+ TEST_DATA.map(outer => outer.oldHTML).join("\n") +
+ "</body>" +
+ "</html>";
+
+add_task(function* () {
+ let {inspector, testActor} = yield openInspectorForURL(TEST_URL);
+ inspector.markup._frame.focus();
+ yield runEditOuterHTMLTests(TEST_DATA, inspector, testActor);
+});
diff --git a/devtools/client/inspector/markup/test/browser_markup_html_edit_03.js b/devtools/client/inspector/markup/test/browser_markup_html_edit_03.js
new file mode 100644
index 000000000..d72bd1f1d
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_html_edit_03.js
@@ -0,0 +1,200 @@
+/* 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";
+
+// Test that outerHTML editing keybindings work as expected and that *special*
+// elements like <html>, <body> and <head> can be edited correctly.
+
+const TEST_URL = "data:text/html," +
+ "<!DOCTYPE html>" +
+ "<head><meta charset='utf-8' /></head>" +
+ "<body>" +
+ "<div id=\"keyboard\"></div>" +
+ "</body>" +
+ "</html>";
+const SELECTOR = "#keyboard";
+const OLD_HTML = '<div id="keyboard"></div>';
+const NEW_HTML = '<div id="keyboard">Edited</div>';
+
+requestLongerTimeout(2);
+
+add_task(function* () {
+ let {inspector, testActor} = yield openInspectorForURL(TEST_URL);
+
+ inspector.markup._frame.focus();
+
+ info("Check that pressing escape cancels edits");
+ yield testEscapeCancels(inspector, testActor);
+
+ info("Check that pressing F2 commits edits");
+ yield testF2Commits(inspector, testActor);
+
+ info("Check that editing the <body> element works like other nodes");
+ yield testBody(inspector, testActor);
+
+ info("Check that editing the <head> element works like other nodes");
+ yield testHead(inspector, testActor);
+
+ info("Check that editing the <html> element works like other nodes");
+ yield testDocumentElement(inspector, testActor);
+
+ info("Check (again) that editing the <html> element works like other nodes");
+ yield testDocumentElement2(inspector, testActor);
+});
+
+function* testEscapeCancels(inspector, testActor) {
+ yield selectNode(SELECTOR, inspector);
+
+ let onEditorShown = once(inspector.markup.htmlEditor, "popupshown");
+ EventUtils.sendKey("F2", inspector.markup._frame.contentWindow);
+ yield onEditorShown;
+ ok(inspector.markup.htmlEditor._visible, "HTML Editor is visible");
+
+ is((yield testActor.getProperty(SELECTOR, "outerHTML")), OLD_HTML,
+ "The node is starting with old HTML.");
+
+ inspector.markup.htmlEditor.editor.setText(NEW_HTML);
+
+ let onEditorHiddem = once(inspector.markup.htmlEditor, "popuphidden");
+ EventUtils.sendKey("ESCAPE", inspector.markup.htmlEditor.doc.defaultView);
+ yield onEditorHiddem;
+ ok(!inspector.markup.htmlEditor._visible, "HTML Editor is not visible");
+
+ is((yield testActor.getProperty(SELECTOR, "outerHTML")), OLD_HTML,
+ "Escape cancels edits");
+}
+
+function* testF2Commits(inspector, testActor) {
+ let onEditorShown = once(inspector.markup.htmlEditor, "popupshown");
+ inspector.markup._frame.contentDocument.documentElement.focus();
+ EventUtils.sendKey("F2", inspector.markup._frame.contentWindow);
+ yield onEditorShown;
+ ok(inspector.markup.htmlEditor._visible, "HTML Editor is visible");
+
+ is((yield testActor.getProperty(SELECTOR, "outerHTML")), OLD_HTML,
+ "The node is starting with old HTML.");
+
+ let onMutations = inspector.once("markupmutation");
+ inspector.markup.htmlEditor.editor.setText(NEW_HTML);
+ EventUtils.sendKey("F2", inspector.markup._frame.contentWindow);
+ yield onMutations;
+
+ ok(!inspector.markup.htmlEditor._visible, "HTML Editor is not visible");
+
+ is((yield testActor.getProperty(SELECTOR, "outerHTML")), NEW_HTML,
+ "F2 commits edits - the node has new HTML.");
+}
+
+function* testBody(inspector, testActor) {
+ let currentBodyHTML = yield testActor.getProperty("body", "outerHTML");
+ let bodyHTML = '<body id="updated"><p></p></body>';
+ let bodyFront = yield getNodeFront("body", inspector);
+
+ let onUpdated = inspector.once("inspector-updated");
+ let onReselected = inspector.markup.once("reselectedonremoved");
+ yield inspector.markup.updateNodeOuterHTML(bodyFront, bodyHTML,
+ currentBodyHTML);
+ yield onReselected;
+ yield onUpdated;
+
+ let newBodyHTML = yield testActor.getProperty("body", "outerHTML");
+ is(newBodyHTML, bodyHTML, "<body> HTML has been updated");
+
+ let headsNum = yield testActor.getNumberOfElementMatches("head");
+ is(headsNum, 1, "no extra <head>s have been added");
+}
+
+function* testHead(inspector, testActor) {
+ yield selectNode("head", inspector);
+
+ let currentHeadHTML = yield testActor.getProperty("head", "outerHTML");
+ let headHTML = "<head id=\"updated\"><title>New Title</title>" +
+ "<script>window.foo=\"bar\";</script></head>";
+ let headFront = yield getNodeFront("head", inspector);
+
+ let onUpdated = inspector.once("inspector-updated");
+ let onReselected = inspector.markup.once("reselectedonremoved");
+ yield inspector.markup.updateNodeOuterHTML(headFront, headHTML,
+ currentHeadHTML);
+ yield onReselected;
+ yield onUpdated;
+
+ is((yield testActor.eval("content.document.title")), "New Title",
+ "New title has been added");
+ is((yield testActor.eval("content.foo")), undefined,
+ "Script has not been executed");
+ is((yield testActor.getProperty("head", "outerHTML")), headHTML,
+ "<head> HTML has been updated");
+ is((yield testActor.getNumberOfElementMatches("body")), 1,
+ "no extra <body>s have been added");
+}
+
+function* testDocumentElement(inspector, testActor) {
+ let currentDocElementOuterHMTL = yield testActor.eval(
+ "content.document.documentElement.outerHMTL");
+ let docElementHTML = "<html id=\"updated\" foo=\"bar\"><head>" +
+ "<title>Updated from document element</title>" +
+ "<script>window.foo=\"bar\";</script></head><body>" +
+ "<p>Hello</p></body></html>";
+ let docElementFront = yield inspector.markup.walker.documentElement();
+
+ let onReselected = inspector.markup.once("reselectedonremoved");
+ yield inspector.markup.updateNodeOuterHTML(docElementFront, docElementHTML,
+ currentDocElementOuterHMTL);
+ yield onReselected;
+
+ is((yield testActor.eval("content.document.title")),
+ "Updated from document element", "New title has been added");
+ is((yield testActor.eval("content.foo")),
+ undefined, "Script has not been executed");
+ is((yield testActor.getAttribute("html", "id")),
+ "updated", "<html> ID has been updated");
+ is((yield testActor.getAttribute("html", "class")),
+ null, "<html> class has been updated");
+ is((yield testActor.getAttribute("html", "foo")),
+ "bar", "<html> attribute has been updated");
+ is((yield testActor.getProperty("html", "outerHTML")),
+ docElementHTML, "<html> HTML has been updated");
+ is((yield testActor.getNumberOfElementMatches("head")),
+ 1, "no extra <head>s have been added");
+ is((yield testActor.getNumberOfElementMatches("body")),
+ 1, "no extra <body>s have been added");
+ is((yield testActor.getProperty("body", "textContent")),
+ "Hello", "document.body.textContent has been updated");
+}
+
+function* testDocumentElement2(inspector, testActor) {
+ let currentDocElementOuterHMTL = yield testActor.eval(
+ "content.document.documentElement.outerHMTL");
+ let docElementHTML = "<html id=\"somethingelse\" class=\"updated\"><head>" +
+ "<title>Updated again from document element</title>" +
+ "<script>window.foo=\"bar\";</script></head><body>" +
+ "<p>Hello again</p></body></html>";
+ let docElementFront = yield inspector.markup.walker.documentElement();
+
+ let onReselected = inspector.markup.once("reselectedonremoved");
+ inspector.markup.updateNodeOuterHTML(docElementFront, docElementHTML,
+ currentDocElementOuterHMTL);
+ yield onReselected;
+
+ is((yield testActor.eval("content.document.title")),
+ "Updated again from document element", "New title has been added");
+ is((yield testActor.eval("content.foo")),
+ undefined, "Script has not been executed");
+ is((yield testActor.getAttribute("html", "id")),
+ "somethingelse", "<html> ID has been updated");
+ is((yield testActor.getAttribute("html", "class")),
+ "updated", "<html> class has been updated");
+ is((yield testActor.getAttribute("html", "foo")),
+ null, "<html> attribute has been removed");
+ is((yield testActor.getProperty("html", "outerHTML")),
+ docElementHTML, "<html> HTML has been updated");
+ is((yield testActor.getNumberOfElementMatches("head")),
+ 1, "no extra <head>s have been added");
+ is((yield testActor.getNumberOfElementMatches("body")),
+ 1, "no extra <body>s have been added");
+ is((yield testActor.getProperty("body", "textContent")),
+ "Hello again", "document.body.textContent has been updated");
+}
diff --git a/devtools/client/inspector/markup/test/browser_markup_image_tooltip.js b/devtools/client/inspector/markup/test/browser_markup_image_tooltip.js
new file mode 100644
index 000000000..7b1611acd
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_image_tooltip.js
@@ -0,0 +1,60 @@
+/* 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";
+
+// Test that image preview tooltips are shown on img and canvas tags in the
+// markup-view and that the tooltip actually contains an image and shows the
+// right dimension label
+
+const TEST_NODES = [
+ {selector: "img.local", size: "192" + " \u00D7 " + "192"},
+ {selector: "img.data", size: "64" + " \u00D7 " + "64"},
+ {selector: "img.remote", size: "22" + " \u00D7 " + "23"},
+ {selector: ".canvas", size: "600" + " \u00D7 " + "600"}
+];
+
+add_task(function* () {
+ yield addTab(URL_ROOT + "doc_markup_image_and_canvas_2.html");
+ let {inspector} = yield openInspector();
+
+ info("Selecting the first <img> tag");
+ yield selectNode("img", inspector);
+
+ for (let testNode of TEST_NODES) {
+ let target = yield getImageTooltipTarget(testNode, inspector);
+ yield assertTooltipShownOn(target, inspector);
+ checkImageTooltip(testNode, inspector);
+ }
+});
+
+function* getImageTooltipTarget({selector}, inspector) {
+ let nodeFront = yield getNodeFront(selector, inspector);
+ let isImg = nodeFront.tagName.toLowerCase() === "img";
+
+ let container = getContainerForNodeFront(nodeFront, inspector);
+
+ let target = container.editor.tag;
+ if (isImg) {
+ target = container.editor.getAttributeElement("src").querySelector(".link");
+ }
+ return target;
+}
+
+function* assertTooltipShownOn(element, {markup}) {
+ info("Is the element a valid hover target");
+ let isValid = yield isHoverTooltipTarget(markup.imagePreviewTooltip, element);
+ ok(isValid, "The element is a valid hover target for the image tooltip");
+}
+
+function checkImageTooltip({selector, size}, {markup}) {
+ let panel = markup.imagePreviewTooltip.panel;
+ let images = panel.getElementsByTagName("img");
+ is(images.length, 1, "Tooltip for [" + selector + "] contains an image");
+
+ let label = panel.querySelector(".devtools-tooltip-caption");
+ is(label.textContent, size,
+ "Tooltip label for [" + selector + "] displays the right image size");
+
+ markup.imagePreviewTooltip.hide();
+}
diff --git a/devtools/client/inspector/markup/test/browser_markup_image_tooltip_mutations.js b/devtools/client/inspector/markup/test/browser_markup_image_tooltip_mutations.js
new file mode 100644
index 000000000..bc1af2c2a
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_image_tooltip_mutations.js
@@ -0,0 +1,83 @@
+/* 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";
+
+// Test that image preview tooltip shows updated content when the image src
+// changes.
+
+/*eslint-disable */
+const INITIAL_SRC = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAADI5JREFUeNrsWwuQFNUVPf1m5z87szv7HWSWj8CigBFMEFZKiQsB1PgJwUAZg1HBpIQsKmokEhNjWUnFVPnDWBT+KolJYbRMoqUVq0yCClpqiX8sCchPWFwVlt2db7+X93pez7zu6Vn2NxsVWh8987p7pu+9555z7+tZjTGGY3kjOMa34w447oBjfKsY7i/UNM3Y8eFSAkD50Plgw03K5P9gvGv7U5ieeR3PszeREiPNX3/0DL4hjslzhm8THh+OITfXk3dhiv4GDtGPVzCaeJmPLYzuu5qJuWfuw2QTlcN1X9pwQU7LhdZ/ZAseD45cOh9hHvDkc/yAF/DNhdb5Mrr3PvBMaAYW8fMSIi2G497IMEK/YutGtAYr6+ej+nxu/NN8Ks3N7AR6HgcLz0Eg1Ljg1UcxZzi5qewIkMYLRweTr2Kzp+nmyXAd5pS3XQDd+N/4h4zgu9FI7brlXf90nMEnuwQxlvv+hosE3TuexmWeysmT4W+WxkMaLzf9Y8ATgjcUn7T9H1gqrpFq8eV1gMn6t16NhngjfoX6q4DUP032Rd4LJgpSLwJ1yzFqBG69eRkah0MVyo0Acfe+yy9AG4nMiYCkeM53KKFXncBLAXqEm+wCqZwaueq7WCmuLTcKSJmj737ol2hurA9eq9VdyiO8yWa3NNyog+SB5CZodSsQq/dfu34tJpYbBaTMzvVddDZu16q5smXf4G8zEvqm4cyaAmJPuTJk3oJWdS4WzcVtfMZbThSQckb/pYfRGgo3zNOqZnEHbJPGK4abaDCQIIsT8V/qTaBqHkLh6LzXH8XZQhbLhYKyyCC/WeHYcNdmvOgfe8skzbWL270/T3wf7tSx/lGCbTu8xlzzmCSWLc5iwmgikcCHi3Mga0Ry913vBFvQwg90l6M4ImWKfsWOp7DSWxmfpPlCFuPFfsNfKrCnPYpQKIRgqBK7D0SxYaNHwkEiJMtl0ReDp3Lc5D3PGoTo/sKngCl7a5chFqvBatKwjBd7WwqIlzB/78NcoUcp5VSgGxm+7b8eqQRGnHMO634epO4S1EZww09/iFg5UmGoESDuznP1xVhTUX1WWHPzjpd25wyH0hRxI3LGM75nxmuNEEUVpAN0XgxmPoKralakbQnWlIMQyVBD/w+3orkq4lvualjKyWwzt4MaxqspQHVhPOWG64bxYuhZXSFGWhipbSDVragOu5Y9eAsmDDUKyBA703vemVhHoueD6e9wAzJK1WfmN0Umk5GGM4kEMZcuIECqgjm0nldAqmbjwtm4VxZH5AvlADP6mx9Eqy9Q0+KqW8Ch+47FaMMYmnNGfY1iPMshoC6qFxme4wQ+0p+ARE6H3+9veWEDWgUhDhUKyFARn4jM5BNxT0XsMg7bfymGK1ov3wtjDfhL4w0HVGUVBEjDaaE+QNdrcNWch1PG4W6xrjBUXECGivg++Cva3JUT4iQUz3V2RsSVaKLwOuDT89A3HdBQoxhNC+fnVm74ual2EG893P6G+PuP4SfiO4cCBWQooL9qCWKNXPbcI37Aa/lnlZxXRt4RFONGwSDCPAHqOuqjWct1QiEMw5mChM5X4K47FyNqcd3aK9AwFH0CGYLoe1ctxk2eWi57rg5JfGp9rzC6ggCdFlAgHBDw5Yxlcg6G8SyHCjMlsgmDD9zhSeHlF+JnAgWDTQUy2NxfdwOao1UVV3pi3+bE97YSbWpLAbn6zefHNQkp1PMpIBwwvslKgIYTKM2nEpNzrGcH3FXTEal0L38kJ4uDQgEZbO4vnI173LXf5NHZaiUxtaCxyZuo/rK6LpUg54yg3zTWRAArvDcRIPZ6BqzrQ1REpmL+DNw32OKIDCb3X1qPVn8wNNMT4w2bvs+q4bAZrqBh2skaL3yyhhIIZ4i6oHkUK0RckcB8GigEyRIH4A6Mgc8fatl0/+BkkQxC9gIT4ljna1rIZW9rEdNbjJcNjsnoYj7LHWCUwpITzEgzRQKZ3XAFHbTzA3hrz8TEUUZxFBhoKpABQt/97p+w0hMZG68I8R6FtlsJT3FELndZntjM+VMnylKYq8GJI3UZaRMpquGSGFVOEfv0YZBMNzz+uvjbfzS6xQERIhlI9FcvQWNdFVb7x1zCb+QNK8vb9NsiifmI5hBgVoOCBC1sb0ab5RomqENxLO3eA1/0NDRU47q2RQNbRCUDIb7lF2CNL3ZGxEV4n08TVvZWYG4pZyV0zUdS45tyCBByOHWiyvZmxFXDCyRo1ge5+Sy0TA+8lWMiP/6O0S32exGV9Jf4fr8azdUR3zL/CZz4MtvzdX5uOYs6NDOmpkuj5Huh+7qUQSYl0ThHzw0YQzcGo6bhzEqoYq5rN3yRiYiG3Vfe2Ybm/qKA9NNZ3nNm4F7/yDkg9AN+U1mHiBcXP8zuDN76jj8hg1QyiWQigalj02BJPhK8I0zxijAjhp5zhlpLUDvS+BCy2HMAvvB4XDgL9/SXC0g/ou/5+6/xLX8w0uJrOIkXfPvyhY0F6gr7M8H0KWFYikcqAXakB+xwD9CdREBLoau7Gz3cAdSIdLFxFtJTCqRChSjnutvhDcREtzjz2Tswtz+yeNRFUeXZXtWux7C1fuoVcbd3J//ipDX3uZZDLGrwweS+UBLL5TDliVBnF8P7H+XI8aRRGsIBJg/Zlslt1+W+D1JWoSyi+kD9jfhs78t7mhZhSl+fLfY1Bdyv3I8V/qpY3B1McgN7ZFT5/vNO0I5DPLLdPBIJA8qc4h2I0QplYfDpJwHT+aj0246r5S8rToG8OjCle8wk4OLvvYGa+Ovr84uo2qBSwJS9G5egoZFLTfiEqWDtbwGfHgKOdPHcS+ai7XDzMPW/FJRLGGcxnBbK4YJC2K+h+T6Bdu5CqHqCWERd3bawb7JI+iJ735+LNaHaprBLLHBm08U3XxShEsdt+f3eTh3v7aC95Dct4RCWL5OZWh/oXBZThxAIxyOXLzBk8aiEWJID8rK3CpPOmeHaGpvCS+7EHv5FujVHUSJPLXvIFeHcNc+9xrB2gws9KZdxuLFax/WLM5gzzSm/lTXF/OdAcapyvjxPqxqHjr2v4ckX2bS2dRBrc5lSdpKjEJ9/9tdwX2WMd53ZQ2IVo3RES+UwVSpCPvYepNx4gmTGDUKIMQ4eduPnD7mx9xOn/KZKOlFbStjONxHTtR+BYAPmnoZ1Zp8wkBRwP/EL3u0F/C2hGl7vpz7vW37T3vP7if8wroKuoh8ribknX9BK5rcF+mo1qKaKyRPJTgTDjbzY8szcuLb3bpH00u35T47j7prRpwDJTxzyG0dHgxPp5bPG8VdkpfPbUg3SgoOo2mwVukb98D5EqpswZTTulCggTk4gpYhv0++wIhCJxr0+Hq1sondis0SE2oxQe3qWXwWyO4DSQg9gJ8Iiw1VFcGqXxet0N9xE4ygIxv/9W6wo9WyROEX/R+eiobYSq2vHTOR631Eiv2lRfh9dvxkumkXh92Qsx8XrAJ+7YGbWuhxOi/U+31NQmzyqNYG8N/3wfo6CRtRHcN01FzkvojohwLu0VVvDa56IS/xcj2b7nN+O+m0jqpE1wMPXZxAN9iCVThtDvH7gmiRGRpU8Lspv1Uhq4wIVdQoyuGSLNYPKUCS8+CzNURbzMmjK3i8u0U793lmuV0ef9nWQ5MGC/DiUqEUSaCtXna9RJEspZS1lrXINK/pcq+SpT50t98QKMq1FRmDfx3vxty102k0PM4ssEnvuz5+G26Ij4yDpz6z9fV8bkyIkqBFkhej0Ib+ZQ34XJK9AfozaiimqIoX3Jp3tiISrcfYpuN2+iFph/02P36PNC9fVcCnp6H9jYouKyfaWufz5Tp9tVxcUniw7IohZv4dZz81/ns67z3AYPrc2n0+Ix2q8k0PWjgBy88XaibnfK9A+5LdDY2Ivhy36fbT8Zv3Lb1U1qLqUxorXEEXIs0mjjrtxoTZWtdvigNs2sgPiujTv6DIZLld6b/V5742JZV3fUsUVFy5gdsNtKWFzUCEVbNepD1MkSMVbsb6SZm7jI3/zODtQKgUMsOw8wDZ63t5xcV1TnaEAxoc6wrqY+Fj+N4DsqOnhOIdicrQSm1MPYCPlIqHn5bbHg8/bj2D3QfZnCX3mpAICDZV8jH5kpbZqTD0W+DxaA74CWzLN2nd14OlL72J38Lf7+TjC7dadZFDoZJQPrtaIKL/G0L6ktptPZVJ8fMqHYPZOKYPMyQGadIJfDvdXwAFiZOTvDBPydf5vk4rWA+RfdhBlaF/yDDBRoMu9pfnSjv/p7DG+HXfAcQcc49v/BBgAcFAO4DmB2GQAAAAASUVORK5CYII=";
+/*eslint-enable */
+
+const UPDATED_SRC = URL_ROOT + "doc_markup_tooltip.png";
+
+const INITIAL_SRC_SIZE = "64" + " \u00D7 " + "64";
+const UPDATED_SRC_SIZE = "22" + " \u00D7 " + "23";
+
+add_task(function* () {
+ let { inspector } = yield openInspectorForURL(
+ "data:text/html,<p>markup view tooltip test</p><img>");
+
+ info("Retrieving NodeFront for the <img> element.");
+ let img = yield getNodeFront("img", inspector);
+
+ info("Selecting the <img> element");
+ yield selectNode(img, inspector);
+
+ info("Adding src attribute to the image.");
+ yield updateImageSrc(img, INITIAL_SRC, inspector);
+
+ let container = getContainerForNodeFront(img, inspector);
+ ok(container, "Found markup container for the image.");
+
+ let target = container.editor.getAttributeElement("src")
+ .querySelector(".link");
+ ok(target, "Found the src attribute in the markup view.");
+
+ info("Showing tooltip on the src link.");
+ yield isHoverTooltipTarget(inspector.markup.imagePreviewTooltip, target);
+
+ checkImageTooltip(INITIAL_SRC_SIZE, inspector);
+
+ info("Updating the image src.");
+ yield updateImageSrc(img, UPDATED_SRC, inspector);
+
+ target = container.editor.getAttributeElement("src").querySelector(".link");
+ ok(target, "Found the src attribute in the markup view after mutation.");
+
+ info("Showing tooltip on the src link.");
+ yield isHoverTooltipTarget(inspector.markup.imagePreviewTooltip, target);
+
+ info("Checking that the new image was shown.");
+ checkImageTooltip(UPDATED_SRC_SIZE, inspector);
+});
+
+/**
+ * Updates the src attribute of the image. Return a Promise.
+ */
+function updateImageSrc(img, newSrc, inspector) {
+ let onMutated = inspector.once("markupmutation");
+ let onModified = img.modifyAttributes([{
+ attributeName: "src",
+ newValue: newSrc
+ }]);
+
+ return Promise.all([onMutated, onModified]);
+}
+
+/**
+ * Checks that the markup view tooltip contains an image element with the given
+ * size.
+ */
+function checkImageTooltip(size, {markup}) {
+ let panel = markup.imagePreviewTooltip.panel;
+ let images = panel.getElementsByTagName("img");
+ is(images.length, 1, "Tooltip contains an image");
+
+ let label = panel.querySelector(".devtools-tooltip-caption");
+ is(label.textContent, size, "Tooltip label displays the right image size");
+
+ markup.imagePreviewTooltip.hide();
+}
diff --git a/devtools/client/inspector/markup/test/browser_markup_keybindings_01.js b/devtools/client/inspector/markup/test/browser_markup_keybindings_01.js
new file mode 100644
index 000000000..58eccc173
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_keybindings_01.js
@@ -0,0 +1,49 @@
+/* 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";
+
+requestLongerTimeout(2);
+
+// Tests tabbing through attributes on a node
+
+const TEST_URL = "data:text/html;charset=utf8,<div id='test' a b c d e></div>";
+
+add_task(function* () {
+ let {inspector} = yield openInspectorForURL(TEST_URL);
+
+ info("Focusing the tag editor of the test element");
+ let {editor} = yield focusNode("div", inspector);
+ editor.tag.focus();
+
+ info("Pressing tab and expecting to focus the ID attribute, always first");
+ EventUtils.sendKey("tab", inspector.panelWin);
+ checkFocusedAttribute("id");
+
+ info("Hit enter to turn the attribute to edit mode");
+ EventUtils.sendKey("return", inspector.panelWin);
+ checkFocusedAttribute("id", true);
+
+ // Check the order of the other attributes in the DOM to the check they appear
+ // correctly in the markup-view
+ let attributes = (yield getAttributesFromEditor("div", inspector)).slice(1);
+
+ info("Tabbing forward through attributes in edit mode");
+ for (let attribute of attributes) {
+ collapseSelectionAndTab(inspector);
+ checkFocusedAttribute(attribute, true);
+ }
+
+ info("Tabbing backward through attributes in edit mode");
+
+ // Just reverse the attributes other than id and remove the first one since
+ // it's already focused now.
+ let reverseAttributes = attributes.reverse();
+ reverseAttributes.shift();
+
+ for (let attribute of reverseAttributes) {
+ collapseSelectionAndShiftTab(inspector);
+ checkFocusedAttribute(attribute, true);
+ }
+});
diff --git a/devtools/client/inspector/markup/test/browser_markup_keybindings_02.js b/devtools/client/inspector/markup/test/browser_markup_keybindings_02.js
new file mode 100644
index 000000000..0e4b8a802
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_keybindings_02.js
@@ -0,0 +1,32 @@
+/* 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";
+
+// Tests that pressing ESC when a node in the markup-view is focused toggles
+// the split-console (see bug 988278)
+
+const TEST_URL = "data:text/html;charset=utf8,<div></div>";
+
+add_task(function* () {
+ let {inspector, toolbox} = yield openInspectorForURL(TEST_URL);
+
+ info("Focusing the tag editor of the test element");
+ let {editor} = yield getContainerForSelector("div", inspector);
+ editor.tag.focus();
+
+ info("Pressing ESC and wait for the split-console to open");
+ let onSplitConsole = toolbox.once("split-console");
+ let onConsoleReady = toolbox.once("webconsole-ready");
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, inspector.panelWin);
+ yield onSplitConsole;
+ yield onConsoleReady;
+ ok(toolbox.splitConsole, "The split console is shown.");
+
+ info("Pressing ESC again and wait for the split-console to close");
+ onSplitConsole = toolbox.once("split-console");
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, inspector.panelWin);
+ yield onSplitConsole;
+ ok(!toolbox.splitConsole, "The split console is hidden.");
+});
diff --git a/devtools/client/inspector/markup/test/browser_markup_keybindings_03.js b/devtools/client/inspector/markup/test/browser_markup_keybindings_03.js
new file mode 100644
index 000000000..1a94c9270
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_keybindings_03.js
@@ -0,0 +1,50 @@
+/* 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";
+
+// Tests that selecting a node with the mouse (by clicking on the line) focuses
+// the first focusable element in the corresponding MarkupContainer so that the
+// keyboard can be used immediately.
+
+const TEST_URL = `data:text/html;charset=utf8,
+ <div class='test-class'></div>Text node`;
+
+add_task(function* () {
+ let {inspector} = yield openInspectorForURL(TEST_URL);
+ let {walker} = inspector;
+
+ info("Select the test node to have the 2 test containers visible");
+ yield selectNode("div", inspector);
+
+ let divFront = yield walker.querySelector(walker.rootNode, "div");
+ let textFront = yield walker.nextSibling(divFront);
+
+ info("Click on the MarkupContainer element for the text node");
+ yield clickContainer(textFront, inspector);
+ is(inspector.markup.doc.activeElement,
+ getContainerForNodeFront(textFront, inspector).editor.value,
+ "The currently focused element is the node's text content");
+
+ info("Click on the MarkupContainer element for the <div> node");
+ yield clickContainer(divFront, inspector);
+ is(inspector.markup.doc.activeElement,
+ getContainerForNodeFront(divFront, inspector).editor.tag,
+ "The currently focused element is the div's tagname");
+
+ info("Click on the test-class attribute, to make sure it gets focused");
+ let editor = getContainerForNodeFront(divFront, inspector).editor;
+ let attributeEditor = editor.attrElements.get("class")
+ .querySelector(".editable");
+
+ let onFocus = once(attributeEditor, "focus");
+ EventUtils.synthesizeMouseAtCenter(attributeEditor, {type: "mousedown"},
+ inspector.markup.doc.defaultView);
+ EventUtils.synthesizeMouseAtCenter(attributeEditor, {type: "mouseup"},
+ inspector.markup.doc.defaultView);
+ yield onFocus;
+
+ is(inspector.markup.doc.activeElement, attributeEditor,
+ "The currently focused element is the div's class attribute");
+});
diff --git a/devtools/client/inspector/markup/test/browser_markup_keybindings_04.js b/devtools/client/inspector/markup/test/browser_markup_keybindings_04.js
new file mode 100644
index 000000000..3b6f8bfb3
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_keybindings_04.js
@@ -0,0 +1,58 @@
+/* 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";
+
+requestLongerTimeout(2);
+
+// Tests that selecting a node using the browser context menu (inspect element)
+// or the element picker focuses that node so that the keyboard can be used
+// immediately.
+
+const TEST_URL = "data:text/html;charset=utf8,<div>test element</div>";
+
+add_task(function* () {
+ let {inspector, testActor} = yield openInspectorForURL(TEST_URL);
+
+ info("Select the test node with the browser ctx menu");
+ yield clickOnInspectMenuItem(testActor, "div");
+ assertNodeSelected(inspector, "div");
+
+ info("Press arrowUp to focus <body> " +
+ "(which works if the node was focused properly)");
+ yield selectPreviousNodeWithArrowUp(inspector);
+ assertNodeSelected(inspector, "body");
+
+ info("Select the test node with the element picker");
+ yield selectWithElementPicker(inspector, testActor);
+ assertNodeSelected(inspector, "div");
+
+ info("Press arrowUp to focus <body> " +
+ "(which works if the node was focused properly)");
+ yield selectPreviousNodeWithArrowUp(inspector);
+ assertNodeSelected(inspector, "body");
+});
+
+function assertNodeSelected(inspector, tagName) {
+ is(inspector.selection.nodeFront.tagName.toLowerCase(), tagName,
+ `The <${tagName}> node is selected`);
+}
+
+function selectPreviousNodeWithArrowUp(inspector) {
+ let onNodeHighlighted = inspector.toolbox.once("node-highlight");
+ let onUpdated = inspector.once("inspector-updated");
+ EventUtils.synthesizeKey("VK_UP", {});
+ return Promise.all([onUpdated, onNodeHighlighted]);
+}
+
+function* selectWithElementPicker(inspector, testActor) {
+ yield startPicker(inspector.toolbox);
+
+ yield BrowserTestUtils.synthesizeMouseAtCenter("div", {
+ type: "mousemove",
+ }, gBrowser.selectedBrowser);
+
+ yield testActor.synthesizeKey({key: "VK_RETURN", options: {}});
+ yield inspector.once("inspector-updated");
+}
diff --git a/devtools/client/inspector/markup/test/browser_markup_keybindings_delete_attributes.js b/devtools/client/inspector/markup/test/browser_markup_keybindings_delete_attributes.js
new file mode 100644
index 000000000..a4c121360
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_keybindings_delete_attributes.js
@@ -0,0 +1,63 @@
+/* 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";
+
+// Tests that attributes can be deleted from the markup-view with the delete key
+// when they are focused.
+
+const HTML = '<div id="id" class="class" data-id="id"></div>';
+const TEST_URL = "data:text/html;charset=utf-8," + encodeURIComponent(HTML);
+
+// List of all the test cases. Each item is an object with the following props:
+// - selector: the css selector of the node that should be selected
+// - attribute: the name of the attribute that should be focused. Do not
+// specify an attribute that would make it impossible to find the node using
+// selector.
+// Note that after each test case, undo is called.
+const TEST_DATA = [{
+ selector: "#id",
+ attribute: "class"
+}, {
+ selector: "#id",
+ attribute: "data-id"
+}];
+
+add_task(function* () {
+ let {inspector} = yield openInspectorForURL(TEST_URL);
+ let {walker} = inspector;
+
+ for (let {selector, attribute} of TEST_DATA) {
+ info("Get the container for node " + selector);
+ let {editor} = yield getContainerForSelector(selector, inspector);
+
+ info("Focus attribute " + attribute);
+ let attr = editor.attrElements.get(attribute).querySelector(".editable");
+ attr.focus();
+
+ info("Delete the attribute by pressing delete");
+ let mutated = inspector.once("markupmutation");
+ EventUtils.sendKey("delete", inspector.panelWin);
+ yield mutated;
+
+ info("Check that the node is still here");
+ let node = yield walker.querySelector(walker.rootNode, selector);
+ ok(node, "The node hasn't been deleted");
+
+ info("Check that the attribute has been deleted");
+ node = yield walker.querySelector(walker.rootNode,
+ selector + "[" + attribute + "]");
+ ok(!node, "The attribute does not exist anymore in the DOM");
+ ok(!editor.attrElements.get(attribute),
+ "The attribute has been removed from the container");
+
+ info("Undo the change");
+ yield undoChange(inspector);
+ node = yield walker.querySelector(walker.rootNode,
+ selector + "[" + attribute + "]");
+ ok(node, "The attribute is back in the DOM");
+ ok(editor.attrElements.get(attribute),
+ "The attribute is back on the container");
+ }
+});
diff --git a/devtools/client/inspector/markup/test/browser_markup_keybindings_scrolltonode.js b/devtools/client/inspector/markup/test/browser_markup_keybindings_scrolltonode.js
new file mode 100644
index 000000000..7b129fc42
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_keybindings_scrolltonode.js
@@ -0,0 +1,87 @@
+/* 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";
+
+// Test the keyboard shortcut "S" used to scroll to the selected node.
+
+const HTML =
+ `<div style="width: 300px; height: 3000px; position:relative;">
+ <div id="scroll-top"
+ style="height: 50px; top: 0; position:absolute;">
+ TOP</div>
+ <div id="scroll-bottom"
+ style="height: 50px; bottom: 0; position:absolute;">
+ BOTTOM</div>
+ </div>`;
+const TEST_URL = "data:text/html;charset=utf-8," + encodeURIComponent(HTML);
+
+add_task(function* () {
+ let { inspector, testActor } = yield openInspectorForURL(TEST_URL);
+
+ info("Make sure the markup frame has the focus");
+ inspector.markup._frame.focus();
+
+ info("Before test starts, #scroll-top is visible, #scroll-bottom is hidden");
+ yield checkElementIsInViewport("#scroll-top", true, testActor);
+ yield checkElementIsInViewport("#scroll-bottom", false, testActor);
+
+ info("Select the #scroll-bottom node");
+ yield selectNode("#scroll-bottom", inspector);
+ info("Press S to scroll to the bottom node");
+ let waitForScroll = testActor.waitForEventOnNode("scroll");
+ yield EventUtils.synthesizeKey("S", {}, inspector.panelWin);
+ yield waitForScroll;
+ ok(true, "Scroll event received");
+
+ info("#scroll-top should be scrolled out, #scroll-bottom should be visible");
+ yield checkElementIsInViewport("#scroll-top", false, testActor);
+ yield checkElementIsInViewport("#scroll-bottom", true, testActor);
+
+ info("Select the #scroll-top node");
+ yield selectNode("#scroll-top", inspector);
+ info("Press S to scroll to the top node");
+ waitForScroll = testActor.waitForEventOnNode("scroll");
+ yield EventUtils.synthesizeKey("S", {}, inspector.panelWin);
+ yield waitForScroll;
+ ok(true, "Scroll event received");
+
+ info("#scroll-top should be visible, #scroll-bottom should be scrolled out");
+ yield checkElementIsInViewport("#scroll-top", true, testActor);
+ yield checkElementIsInViewport("#scroll-bottom", false, testActor);
+
+ info("Select #scroll-bottom node");
+ yield selectNode("#scroll-bottom", inspector);
+ info("Press shift + S, nothing should happen due to the modifier");
+ yield EventUtils.synthesizeKey("S", {shiftKey: true}, inspector.panelWin);
+
+ info("Same state, #scroll-top is visible, #scroll-bottom is scrolled out");
+ yield checkElementIsInViewport("#scroll-top", true, testActor);
+ yield checkElementIsInViewport("#scroll-bottom", false, testActor);
+});
+
+/**
+ * Verify that the element matching the provided selector is either in or out
+ * of the viewport, depending on the provided "expected" argument.
+ * Returns a promise that will resolve when the test has been performed.
+ *
+ * @param {String} selector
+ * css selector for the element to test
+ * @param {Boolean} expected
+ * true if the element is expected to be in the viewport, false otherwise
+ * @param {TestActor} testActor
+ * current test actor
+ * @return {Promise} promise
+ */
+function* checkElementIsInViewport(selector, expected, testActor) {
+ let isInViewport = yield testActor.eval(`
+ let node = content.document.querySelector("${selector}");
+ let rect = node.getBoundingClientRect();
+ rect.bottom >= 0 && rect.right >= 0 &&
+ rect.top <= content.innerHeight && rect.left <= content.innerWidth;
+ `);
+
+ is(isInViewport, expected,
+ selector + " in the viewport: expected to be " + expected);
+}
diff --git a/devtools/client/inspector/markup/test/browser_markup_links_01.js b/devtools/client/inspector/markup/test/browser_markup_links_01.js
new file mode 100644
index 000000000..4ef3ba4b9
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_links_01.js
@@ -0,0 +1,128 @@
+/* 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";
+
+// Tests that links are shown in attributes when the values (or part of the
+// values) are URIs or pointers to IDs.
+
+const TEST_URL = URL_ROOT + "doc_markup_links.html";
+
+const TEST_DATA = [{
+ selector: "link",
+ attributes: [{
+ attributeName: "href",
+ links: [{type: "cssresource", value: "style.css"}]
+ }]
+}, {
+ selector: "link[rel=icon]",
+ attributes: [{
+ attributeName: "href",
+ links: [{type: "uri",
+ value: "/media/img/firefox/favicon-196.223e1bcaf067.png"}]
+ }]
+}, {
+ selector: "form",
+ attributes: [{
+ attributeName: "action",
+ links: [{type: "uri", value: "/post_message"}]
+ }]
+}, {
+ selector: "label[for=name]",
+ attributes: [{
+ attributeName: "for",
+ links: [{type: "idref", value: "name"}]
+ }]
+}, {
+ selector: "label[for=message]",
+ attributes: [{
+ attributeName: "for",
+ links: [{type: "idref", value: "message"}]
+ }]
+}, {
+ selector: "output",
+ attributes: [{
+ attributeName: "form",
+ links: [{type: "idref", value: "message-form"}]
+ }, {
+ attributeName: "for",
+ links: [
+ {type: "idref", value: "name"},
+ {type: "idref", value: "message"},
+ {type: "idref", value: "invalid"}
+ ]
+ }]
+}, {
+ selector: "a",
+ attributes: [{
+ attributeName: "href",
+ links: [{type: "uri", value: "/go/somewhere/else"}]
+ }, {
+ attributeName: "ping",
+ links: [
+ {type: "uri", value: "/analytics?page=pageA"},
+ {type: "uri", value: "/analytics?user=test"}
+ ]
+ }]
+}, {
+ selector: "li[contextmenu=menu1]",
+ attributes: [{
+ attributeName: "contextmenu",
+ links: [{type: "idref", value: "menu1"}]
+ }]
+}, {
+ selector: "li[contextmenu=menu2]",
+ attributes: [{
+ attributeName: "contextmenu",
+ links: [{type: "idref", value: "menu2"}]
+ }]
+}, {
+ selector: "li[contextmenu=menu3]",
+ attributes: [{
+ attributeName: "contextmenu",
+ links: [{type: "idref", value: "menu3"}]
+ }]
+}, {
+ selector: "video",
+ attributes: [{
+ attributeName: "poster",
+ links: [{type: "uri", value: "doc_markup_tooltip.png"}]
+ }, {
+ attributeName: "src",
+ links: [{type: "uri", value: "code-rush.mp4"}]
+ }]
+}, {
+ selector: "script",
+ attributes: [{
+ attributeName: "src",
+ links: [{type: "jsresource", value: "lib_jquery_1.0.js"}]
+ }]
+}];
+
+requestLongerTimeout(2);
+
+add_task(function* () {
+ let {inspector} = yield openInspectorForURL(TEST_URL);
+
+ for (let {selector, attributes} of TEST_DATA) {
+ info("Testing attributes on node " + selector);
+ yield selectNode(selector, inspector);
+ let {editor} = yield getContainerForSelector(selector, inspector);
+
+ for (let {attributeName, links} of attributes) {
+ info("Testing attribute " + attributeName);
+ let linkEls = editor.attrElements.get(attributeName)
+ .querySelectorAll(".link");
+
+ is(linkEls.length, links.length, "The right number of links were found");
+
+ for (let i = 0; i < links.length; i++) {
+ is(linkEls[i].dataset.type, links[i].type,
+ `Link ${i} has the right type`);
+ is(linkEls[i].textContent, links[i].value,
+ `Link ${i} has the right value`);
+ }
+ }
+ }
+});
diff --git a/devtools/client/inspector/markup/test/browser_markup_links_02.js b/devtools/client/inspector/markup/test/browser_markup_links_02.js
new file mode 100644
index 000000000..83893281c
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_links_02.js
@@ -0,0 +1,38 @@
+/* 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";
+
+// Tests that attributes are linkified correctly when attributes are updated
+// and created.
+
+const TEST_URL = URL_ROOT + "doc_markup_links.html";
+
+add_task(function* () {
+ let {inspector} = yield openInspectorForURL(TEST_URL);
+
+ info("Adding a contextmenu attribute to the body node");
+ yield addNewAttributes("body", "contextmenu=\"menu1\"", inspector);
+
+ info("Checking for links in the new attribute");
+ let {editor} = yield getContainerForSelector("body", inspector);
+ let linkEls = editor.attrElements.get("contextmenu")
+ .querySelectorAll(".link");
+ is(linkEls.length, 1, "There is one link in the contextmenu attribute");
+ is(linkEls[0].dataset.type, "idref", "The link has the right type");
+ is(linkEls[0].textContent, "menu1", "The link has the right value");
+
+ info("Editing the contextmenu attribute on the body node");
+ let nodeMutated = inspector.once("markupmutation");
+ let attr = editor.attrElements.get("contextmenu").querySelector(".editable");
+ setEditableFieldValue(attr, "contextmenu=\"menu2\"", inspector);
+ yield nodeMutated;
+
+ info("Checking for links in the updated attribute");
+ ({editor} = yield getContainerForSelector("body", inspector));
+ linkEls = editor.attrElements.get("contextmenu").querySelectorAll(".link");
+ is(linkEls.length, 1, "There is one link in the contextmenu attribute");
+ is(linkEls[0].dataset.type, "idref", "The link has the right type");
+ is(linkEls[0].textContent, "menu2", "The link has the right value");
+});
diff --git a/devtools/client/inspector/markup/test/browser_markup_links_03.js b/devtools/client/inspector/markup/test/browser_markup_links_03.js
new file mode 100644
index 000000000..a54ccb498
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_links_03.js
@@ -0,0 +1,38 @@
+/* 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";
+
+// Tests that links appear correctly in attributes created in content.
+
+const TEST_URL = URL_ROOT + "doc_markup_links.html";
+
+add_task(function* () {
+ let {inspector, testActor} = yield openInspectorForURL(TEST_URL);
+
+ info("Adding a contextmenu attribute to the body node via the content");
+ let onMutated = inspector.once("markupmutation");
+ yield testActor.setAttribute("body", "contextmenu", "menu1");
+ yield onMutated;
+
+ info("Checking for links in the new attribute");
+ let {editor} = yield getContainerForSelector("body", inspector);
+ let linkEls = editor.attrElements.get("contextmenu")
+ .querySelectorAll(".link");
+ is(linkEls.length, 1, "There is one link in the contextmenu attribute");
+ is(linkEls[0].dataset.type, "idref", "The link has the right type");
+ is(linkEls[0].textContent, "menu1", "The link has the right value");
+
+ info("Editing the contextmenu attribute on the body node");
+ onMutated = inspector.once("markupmutation");
+ yield testActor.setAttribute("body", "contextmenu", "menu2");
+ yield onMutated;
+
+ info("Checking for links in the updated attribute");
+ ({editor} = yield getContainerForSelector("body", inspector));
+ linkEls = editor.attrElements.get("contextmenu").querySelectorAll(".link");
+ is(linkEls.length, 1, "There is one link in the contextmenu attribute");
+ is(linkEls[0].dataset.type, "idref", "The link has the right type");
+ is(linkEls[0].textContent, "menu2", "The link has the right value");
+});
diff --git a/devtools/client/inspector/markup/test/browser_markup_links_04.js b/devtools/client/inspector/markup/test/browser_markup_links_04.js
new file mode 100644
index 000000000..f21afd8d2
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_links_04.js
@@ -0,0 +1,116 @@
+/* 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";
+
+// Tests that the contextual menu shows the right items when clicking on a link
+// in an attribute.
+
+const TEST_URL = URL_ROOT + "doc_markup_links.html";
+
+const TOOLBOX_L10N = new LocalizationHelper("devtools/client/locales/toolbox.properties");
+
+// The test case array contains objects with the following properties:
+// - selector: css selector for the node to select in the inspector
+// - attributeName: name of the attribute to test
+// - popupNodeSelector: css selector for the element inside the attribute
+// element to use as the contextual menu anchor
+// - isLinkFollowItemVisible: is the follow-link item expected to be displayed
+// - isLinkCopyItemVisible: is the copy-link item expected to be displayed
+// - linkFollowItemLabel: the expected label of the follow-link item
+// - linkCopyItemLabel: the expected label of the copy-link item
+const TEST_DATA = [{
+ selector: "link",
+ attributeName: "href",
+ popupNodeSelector: ".link",
+ isLinkFollowItemVisible: true,
+ isLinkCopyItemVisible: true,
+ linkFollowItemLabel: TOOLBOX_L10N.getStr(
+ "toolbox.viewCssSourceInStyleEditor.label"),
+ linkCopyItemLabel: INSPECTOR_L10N.getStr(
+ "inspector.menu.copyUrlToClipboard.label")
+}, {
+ selector: "link[rel=icon]",
+ attributeName: "href",
+ popupNodeSelector: ".link",
+ isLinkFollowItemVisible: true,
+ isLinkCopyItemVisible: true,
+ linkFollowItemLabel: INSPECTOR_L10N.getStr(
+ "inspector.menu.openUrlInNewTab.label"),
+ linkCopyItemLabel: INSPECTOR_L10N.getStr(
+ "inspector.menu.copyUrlToClipboard.label")
+}, {
+ selector: "link",
+ attributeName: "rel",
+ popupNodeSelector: ".attr-value",
+ isLinkFollowItemVisible: false,
+ isLinkCopyItemVisible: false
+}, {
+ selector: "output",
+ attributeName: "for",
+ popupNodeSelector: ".link",
+ isLinkFollowItemVisible: true,
+ isLinkCopyItemVisible: false,
+ linkFollowItemLabel: INSPECTOR_L10N.getFormatStr(
+ "inspector.menu.selectElement.label", "name")
+}, {
+ selector: "script",
+ attributeName: "src",
+ popupNodeSelector: ".link",
+ isLinkFollowItemVisible: true,
+ isLinkCopyItemVisible: true,
+ linkFollowItemLabel: TOOLBOX_L10N.getStr(
+ "toolbox.viewJsSourceInDebugger.label"),
+ linkCopyItemLabel: INSPECTOR_L10N.getStr(
+ "inspector.menu.copyUrlToClipboard.label")
+}, {
+ selector: "p[for]",
+ attributeName: "for",
+ popupNodeSelector: ".attr-value",
+ isLinkFollowItemVisible: false,
+ isLinkCopyItemVisible: false
+}];
+
+add_task(function* () {
+ let {inspector} = yield openInspectorForURL(TEST_URL);
+
+ for (let test of TEST_DATA) {
+ info("Selecting test node " + test.selector);
+ yield selectNode(test.selector, inspector);
+
+ info("Finding the popupNode to anchor the context-menu to");
+ let {editor} = yield getContainerForSelector(test.selector, inspector);
+ let popupNode = editor.attrElements.get(test.attributeName)
+ .querySelector(test.popupNodeSelector);
+ ok(popupNode, "Found the popupNode in attribute " + test.attributeName);
+
+ info("Simulating a context click on the popupNode");
+ let allMenuItems = openContextMenuAndGetAllItems(inspector, {
+ target: popupNode,
+ });
+
+ let linkFollow = allMenuItems.find(i => i.id === "node-menu-link-follow");
+ let linkCopy = allMenuItems.find(i => i.id === "node-menu-link-copy");
+
+ // The contextual menu setup is async, because it needs to know if the
+ // inspector has the resolveRelativeURL method first. So call actorHasMethod
+ // here too to make sure the first call resolves first and the menu is
+ // properly setup.
+ yield inspector.target.actorHasMethod("inspector", "resolveRelativeURL");
+
+ is(linkFollow.visible, test.isLinkFollowItemVisible,
+ "The follow-link item display is correct");
+ is(linkCopy.visible, test.isLinkCopyItemVisible,
+ "The copy-link item display is correct");
+
+ if (test.isLinkFollowItemVisible) {
+ is(linkFollow.label, test.linkFollowItemLabel,
+ "the follow-link label is correct");
+ }
+ if (test.isLinkCopyItemVisible) {
+ is(linkCopy.label, test.linkCopyItemLabel,
+ "the copy-link label is correct");
+ }
+ }
+});
diff --git a/devtools/client/inspector/markup/test/browser_markup_links_05.js b/devtools/client/inspector/markup/test/browser_markup_links_05.js
new file mode 100644
index 000000000..feaf257a8
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_links_05.js
@@ -0,0 +1,69 @@
+/* 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";
+
+// Tests that the contextual menu items shown when clicking on links in
+// attributes actually do the right things.
+
+const TEST_URL = URL_ROOT + "doc_markup_links.html";
+
+add_task(function* () {
+ let {inspector} = yield openInspectorForURL(TEST_URL);
+
+ info("Select a node with a URI attribute");
+ yield selectNode("video", inspector);
+
+ info("Set the popupNode to the node that contains the uri");
+ let {editor} = yield getContainerForSelector("video", inspector);
+ openContextMenuAndGetAllItems(inspector, {
+ target: editor.attrElements.get("poster").querySelector(".link"),
+ });
+
+ info("Follow the link and wait for the new tab to open");
+ let onTabOpened = once(gBrowser.tabContainer, "TabOpen");
+ inspector.onFollowLink();
+ let {target: tab} = yield onTabOpened;
+ yield BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+ ok(true, "A new tab opened");
+ is(tab.linkedBrowser.currentURI.spec, URL_ROOT + "doc_markup_tooltip.png",
+ "The URL for the new tab is correct");
+ gBrowser.removeTab(tab);
+
+ info("Select a node with a IDREF attribute");
+ yield selectNode("label", inspector);
+
+ info("Set the popupNode to the node that contains the ref");
+ ({editor} = yield getContainerForSelector("label", inspector));
+ openContextMenuAndGetAllItems(inspector, {
+ target: editor.attrElements.get("for").querySelector(".link"),
+ });
+
+ info("Follow the link and wait for the new node to be selected");
+ let onSelection = inspector.selection.once("new-node-front");
+ inspector.onFollowLink();
+ yield onSelection;
+
+ ok(true, "A new node was selected");
+ is(inspector.selection.nodeFront.id, "name", "The right node was selected");
+
+ info("Select a node with an invalid IDREF attribute");
+ yield selectNode("output", inspector);
+
+ info("Set the popupNode to the node that contains the ref");
+ ({editor} = yield getContainerForSelector("output", inspector));
+ openContextMenuAndGetAllItems(inspector, {
+ target: editor.attrElements.get("for").querySelectorAll(".link")[2],
+ });
+
+ info("Try to follow the link and check that no new node were selected");
+ let onFailed = inspector.once("idref-attribute-link-failed");
+ inspector.onFollowLink();
+ yield onFailed;
+
+ ok(true, "The node selection failed");
+ is(inspector.selection.nodeFront.tagName.toLowerCase(), "output",
+ "The <output> node is still selected");
+});
diff --git a/devtools/client/inspector/markup/test/browser_markup_links_06.js b/devtools/client/inspector/markup/test/browser_markup_links_06.js
new file mode 100644
index 000000000..452fa9eca
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_links_06.js
@@ -0,0 +1,53 @@
+/* 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";
+
+// Tests that the contextual menu items shown when clicking on linked attributes
+// for <script> and <link> tags actually open the right tools.
+
+const TEST_URL = URL_ROOT + "doc_markup_links.html";
+
+add_task(function* () {
+ let {toolbox, inspector} = yield openInspectorForURL(TEST_URL);
+
+ info("Select a node with a cssresource attribute");
+ yield selectNode("link", inspector);
+
+ info("Set the popupNode to the node that contains the uri");
+ let {editor} = yield getContainerForSelector("link", inspector);
+ openContextMenuAndGetAllItems(inspector, {
+ target: editor.attrElements.get("href").querySelector(".link"),
+ });
+
+ info("Follow the link and wait for the style-editor to open");
+ let onStyleEditorReady = toolbox.once("styleeditor-ready");
+ inspector.onFollowLink();
+ yield onStyleEditorReady;
+
+ // No real need to test that the editor opened on the right file here as this
+ // is already tested in /framework/test/browser_toolbox_view_source_*
+ ok(true, "The style-editor was open");
+
+ info("Switch back to the inspector");
+ yield toolbox.selectTool("inspector");
+
+ info("Select a node with a jsresource attribute");
+ yield selectNode("script", inspector);
+
+ info("Set the popupNode to the node that contains the uri");
+ ({editor} = yield getContainerForSelector("script", inspector));
+ openContextMenuAndGetAllItems(inspector, {
+ target: editor.attrElements.get("src").querySelector(".link"),
+ });
+
+ info("Follow the link and wait for the debugger to open");
+ let onDebuggerReady = toolbox.once("jsdebugger-ready");
+ inspector.onFollowLink();
+ yield onDebuggerReady;
+
+ // No real need to test that the debugger opened on the right file here as
+ // this is already tested in /framework/test/browser_toolbox_view_source_*
+ ok(true, "The debugger was open");
+});
diff --git a/devtools/client/inspector/markup/test/browser_markup_links_07.js b/devtools/client/inspector/markup/test/browser_markup_links_07.js
new file mode 100644
index 000000000..793c1ee90
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_links_07.js
@@ -0,0 +1,109 @@
+/* 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";
+
+// Tests that a middle-click or meta/ctrl-click on links in attributes actually
+// do follows the link.
+
+const TEST_URL = URL_ROOT + "doc_markup_links.html";
+
+add_task(function* () {
+ let {inspector} = yield openInspectorForURL(TEST_URL);
+
+ info("Select a node with a URI attribute");
+ yield selectNode("video", inspector);
+
+ info("Find the link element from the markup-view");
+ let {editor} = yield getContainerForSelector("video", inspector);
+ let linkEl = editor.attrElements.get("poster").querySelector(".link");
+
+ info("Follow the link with middle-click and wait for the new tab to open");
+ yield followLinkWaitForTab(linkEl, false,
+ URL_ROOT + "doc_markup_tooltip.png");
+
+ info("Follow the link with meta/ctrl-click and wait for the new tab to open");
+ yield followLinkWaitForTab(linkEl, true,
+ URL_ROOT + "doc_markup_tooltip.png");
+
+ info("Select a node with a IDREF attribute");
+ yield selectNode("label", inspector);
+
+ info("Find the link element from the markup-view that contains the ref");
+ ({editor} = yield getContainerForSelector("label", inspector));
+ linkEl = editor.attrElements.get("for").querySelector(".link");
+
+ info("Follow link with middle-click, wait for new node to be selected.");
+ yield followLinkWaitForNewNode(linkEl, false, inspector);
+
+ // We have to re-select the label as the link switched the currently selected
+ // node.
+ yield selectNode("label", inspector);
+
+ info("Follow link with ctrl/meta-click, wait for new node to be selected.");
+ yield followLinkWaitForNewNode(linkEl, true, inspector);
+
+ info("Select a node with an invalid IDREF attribute");
+ yield selectNode("output", inspector);
+
+ info("Find the link element from the markup-view that contains the ref");
+ ({editor} = yield getContainerForSelector("output", inspector));
+ linkEl = editor.attrElements.get("for").querySelectorAll(".link")[2];
+
+ info("Try to follow link wiith middle-click, check no new node selected");
+ yield followLinkNoNewNode(linkEl, false, inspector);
+
+ info("Try to follow link wiith meta/ctrl-click, check no new node selected");
+ yield followLinkNoNewNode(linkEl, true, inspector);
+});
+
+function performMouseDown(linkEl, metactrl) {
+ let evt = linkEl.ownerDocument.createEvent("MouseEvents");
+
+ let button = -1;
+
+ if (metactrl) {
+ info("Performing Meta/Ctrl+Left Click");
+ button = 0;
+ } else {
+ info("Performing Middle Click");
+ button = 1;
+ }
+
+ evt.initMouseEvent("mousedown", true, true,
+ linkEl.ownerDocument.defaultView, 1, 0, 0, 0, 0, metactrl,
+ false, false, metactrl, button, null);
+
+ linkEl.dispatchEvent(evt);
+}
+
+function* followLinkWaitForTab(linkEl, isMetaClick, expectedTabURI) {
+ let onTabOpened = once(gBrowser.tabContainer, "TabOpen");
+ performMouseDown(linkEl, isMetaClick);
+ let {target} = yield onTabOpened;
+ yield BrowserTestUtils.browserLoaded(target.linkedBrowser);
+ ok(true, "A new tab opened");
+ is(target.linkedBrowser.currentURI.spec, expectedTabURI,
+ "The URL for the new tab is correct");
+ gBrowser.removeTab(target);
+}
+
+function* followLinkWaitForNewNode(linkEl, isMetaClick, inspector) {
+ let onSelection = inspector.selection.once("new-node-front");
+ performMouseDown(linkEl, isMetaClick);
+ yield onSelection;
+
+ ok(true, "A new node was selected");
+ is(inspector.selection.nodeFront.id, "name", "The right node was selected");
+}
+
+function* followLinkNoNewNode(linkEl, isMetaClick, inspector) {
+ let onFailed = inspector.once("idref-attribute-link-failed");
+ performMouseDown(linkEl, isMetaClick);
+ yield onFailed;
+
+ ok(true, "The node selection failed");
+ is(inspector.selection.nodeFront.tagName.toLowerCase(), "output",
+ "The <output> node is still selected");
+}
diff --git a/devtools/client/inspector/markup/test/browser_markup_load_01.js b/devtools/client/inspector/markup/test/browser_markup_load_01.js
new file mode 100644
index 000000000..9c8f4ed2c
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_load_01.js
@@ -0,0 +1,71 @@
+/* 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";
+
+// Tests that selecting an element with the 'Inspect Element' context
+// menu during a page reload doesn't cause the markup view to become empty.
+// See https://bugzilla.mozilla.org/show_bug.cgi?id=1036324
+
+const server = createTestHTTPServer();
+
+// Register a slow image handler so we can simulate a long time between
+// a reload and the load event firing.
+server.registerContentType("gif", "image/gif");
+server.registerPathHandler("/slow.gif", function (metadata, response) {
+ info("Image has been requested");
+ response.processAsync();
+ setTimeout(() => {
+ info("Image is responding");
+ response.finish();
+ }, 500);
+});
+
+// Test page load events.
+const TEST_URL = "data:text/html," +
+ "<!DOCTYPE html>" +
+ "<head><meta charset='utf-8' /></head>" +
+ "<body>" +
+ "<p>Slow script</p>" +
+ "<img src='http://localhost:" + server.identity.primaryPort + "/slow.gif' /></script>" +
+ "</body>" +
+ "</html>";
+
+add_task(function* () {
+ let {inspector, testActor, tab} = yield openInspectorForURL(TEST_URL);
+ let domContentLoaded = waitForLinkedBrowserEvent(tab, "DOMContentLoaded");
+ let pageLoaded = waitForLinkedBrowserEvent(tab, "load");
+
+ ok(inspector.markup, "There is a markup view");
+
+ // Select an element while the tab is in the middle of a slow reload.
+ testActor.eval("location.reload()");
+ yield domContentLoaded;
+ yield chooseWithInspectElementContextMenu("img", testActor);
+ yield pageLoaded;
+
+ yield inspector.once("markuploaded");
+ yield waitForMultipleChildrenUpdates(inspector);
+
+ ok(inspector.markup, "There is a markup view");
+ is(inspector.markup._elt.children.length, 1, "The markup view is rendering");
+});
+
+function* chooseWithInspectElementContextMenu(selector, testActor) {
+ yield BrowserTestUtils.synthesizeMouseAtCenter(selector, {
+ type: "contextmenu",
+ button: 2
+ }, gBrowser.selectedBrowser);
+
+ yield EventUtils.synthesizeKey("Q", {});
+}
+
+function waitForLinkedBrowserEvent(tab, event) {
+ let def = defer();
+ tab.linkedBrowser.addEventListener(event, function cb() {
+ tab.linkedBrowser.removeEventListener(event, cb, true);
+ def.resolve();
+ }, true);
+ return def.promise;
+}
diff --git a/devtools/client/inspector/markup/test/browser_markup_mutation_01.js b/devtools/client/inspector/markup/test/browser_markup_mutation_01.js
new file mode 100644
index 000000000..1e4cfb9b0
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_mutation_01.js
@@ -0,0 +1,340 @@
+/* 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";
+
+// Tests that various mutations to the dom update the markup view correctly.
+
+const TEST_URL = URL_ROOT + "doc_markup_mutation.html";
+
+// Mutation tests. Each entry in the array has the following properties:
+// - desc: for logging only
+// - numMutations: how many mutations are expected to come happen due to the
+// test case. Defaults to 1 if not set.
+// - test: a function supposed to mutate the DOM
+// - check: a function supposed to test that the mutation was handled
+const TEST_DATA = [
+ {
+ desc: "Adding an attribute",
+ test: function* (testActor) {
+ yield testActor.setAttribute("#node1", "newattr", "newattrval");
+ },
+ check: function* (inspector) {
+ let {editor} = yield getContainerForSelector("#node1", inspector);
+ ok([...editor.attrList.querySelectorAll(".attreditor")].some(attr => {
+ return attr.textContent.trim() === "newattr=\"newattrval\""
+ && attr.dataset.value === "newattrval"
+ && attr.dataset.attr === "newattr";
+ }), "newattr attribute found");
+ }
+ },
+ {
+ desc: "Removing an attribute",
+ test: function* (testActor) {
+ yield testActor.removeAttribute("#node1", "newattr");
+ },
+ check: function* (inspector) {
+ let {editor} = yield getContainerForSelector("#node1", inspector);
+ ok(![...editor.attrList.querySelectorAll(".attreditor")].some(attr => {
+ return attr.textContent.trim() === "newattr=\"newattrval\"";
+ }), "newattr attribute removed");
+ }
+ },
+ {
+ desc: "Re-adding an attribute",
+ test: function* (testActor) {
+ yield testActor.setAttribute("#node1", "newattr", "newattrval");
+ },
+ check: function* (inspector) {
+ let {editor} = yield getContainerForSelector("#node1", inspector);
+ ok([...editor.attrList.querySelectorAll(".attreditor")].some(attr => {
+ return attr.textContent.trim() === "newattr=\"newattrval\""
+ && attr.dataset.value === "newattrval"
+ && attr.dataset.attr === "newattr";
+ }), "newattr attribute found");
+ }
+ },
+ {
+ desc: "Changing an attribute",
+ test: function* (testActor) {
+ yield testActor.setAttribute("#node1", "newattr", "newattrchanged");
+ },
+ check: function* (inspector) {
+ let {editor} = yield getContainerForSelector("#node1", inspector);
+ ok([...editor.attrList.querySelectorAll(".attreditor")].some(attr => {
+ return attr.textContent.trim() === "newattr=\"newattrchanged\""
+ && attr.dataset.value === "newattrchanged"
+ && attr.dataset.attr === "newattr";
+ }), "newattr attribute found");
+ }
+ },
+ {
+ desc: "Adding another attribute does not rerender unchanged attributes",
+ test: function* (testActor, inspector) {
+ let {editor} = yield getContainerForSelector("#node1", inspector);
+
+ // This test checks the impact on the markup-view nodes after setting attributes on
+ // content nodes.
+ info("Expect attribute-container for 'new-attr' from the previous test");
+ let attributeContainer = editor.attrList.querySelector("[data-attr=newattr]");
+ ok(attributeContainer, "attribute-container for 'newattr' found");
+
+ info("Set a flag on the attribute-container to check after the mutation");
+ attributeContainer.beforeMutationFlag = true;
+
+ info("Add the attribute 'otherattr' on the content node to trigger the mutation");
+ yield testActor.setAttribute("#node1", "otherattr", "othervalue");
+ },
+ check: function* (inspector) {
+ let {editor} = yield getContainerForSelector("#node1", inspector);
+
+ info("Check the attribute-container for the new attribute mutation was created");
+ let otherAttrContainer = editor.attrList.querySelector("[data-attr=otherattr]");
+ ok(otherAttrContainer, "attribute-container for 'otherattr' found");
+
+ info("Check the attribute-container for 'new-attr' is the same node as earlier.");
+ let newAttrContainer = editor.attrList.querySelector("[data-attr=newattr]");
+ ok(newAttrContainer, "attribute-container for 'newattr' found");
+ ok(newAttrContainer.beforeMutationFlag, "attribute-container same as earlier");
+ }
+ },
+ {
+ desc: "Adding ::after element",
+ numMutations: 2,
+ test: function* (testActor) {
+ yield testActor.eval(`
+ let node1 = content.document.querySelector("#node1");
+ node1.classList.add("pseudo");
+ `);
+ },
+ check: function* (inspector) {
+ let {children} = yield getContainerForSelector("#node1", inspector);
+ is(children.childNodes.length, 2,
+ "Node1 now has 2 children (text child and ::after");
+ }
+ },
+ {
+ desc: "Removing ::after element",
+ numMutations: 2,
+ test: function* (testActor) {
+ yield testActor.eval(`
+ let node1 = content.document.querySelector("#node1");
+ node1.classList.remove("pseudo");
+ `);
+ },
+ check: function* (inspector) {
+ let container = yield getContainerForSelector("#node1", inspector);
+ ok(container.inlineTextChild, "Has single text child.");
+ }
+ },
+ {
+ desc: "Updating the text-content",
+ test: function* (testActor) {
+ yield testActor.setProperty("#node1", "textContent", "newtext");
+ },
+ check: function* (inspector) {
+ let container = yield getContainerForSelector("#node1", inspector);
+ ok(container.inlineTextChild, "Has single text child.");
+ ok(!container.canExpand, "Can't expand container with inlineTextChild.");
+ ok(!container.inlineTextChild.canExpand, "Can't expand inlineTextChild.");
+ is(container.editor.elt.querySelector(".text").textContent.trim(),
+ "newtext", "Single text child editor updated.");
+ }
+ },
+ {
+ desc: "Adding a second text child",
+ test: function* (testActor) {
+ yield testActor.eval(`
+ let node1 = content.document.querySelector("#node1");
+ let newText = node1.ownerDocument.createTextNode("more");
+ node1.appendChild(newText);
+ `);
+ },
+ check: function* (inspector) {
+ let container = yield getContainerForSelector("#node1", inspector);
+ ok(!container.inlineTextChild, "Does not have single text child.");
+ ok(container.canExpand, "Can expand container with child nodes.");
+ ok(container.editor.elt.querySelector(".text") == null,
+ "Single text child editor removed.");
+ },
+ },
+ {
+ desc: "Go from 2 to 1 text child",
+ test: function* (testActor) {
+ yield testActor.setProperty("#node1", "textContent", "newtext");
+ },
+ check: function* (inspector) {
+ let container = yield getContainerForSelector("#node1", inspector);
+ ok(container.inlineTextChild, "Has single text child.");
+ ok(!container.canExpand, "Can't expand container with inlineTextChild.");
+ ok(!container.inlineTextChild.canExpand, "Can't expand inlineTextChild.");
+ ok(container.editor.elt.querySelector(".text").textContent.trim(),
+ "newtext", "Single text child editor updated.");
+ },
+ },
+ {
+ desc: "Removing an only text child",
+ test: function* (testActor) {
+ yield testActor.setProperty("#node1", "innerHTML", "");
+ },
+ check: function* (inspector) {
+ let container = yield getContainerForSelector("#node1", inspector);
+ ok(!container.inlineTextChild, "Does not have single text child.");
+ ok(!container.canExpand, "Can't expand empty container.");
+ ok(container.editor.elt.querySelector(".text") == null,
+ "Single text child editor removed.");
+ },
+ },
+ {
+ desc: "Go from 0 to 1 text child",
+ test: function* (testActor) {
+ yield testActor.setProperty("#node1", "textContent", "newtext");
+ },
+ check: function* (inspector) {
+ let container = yield getContainerForSelector("#node1", inspector);
+ ok(container.inlineTextChild, "Has single text child.");
+ ok(!container.canExpand, "Can't expand container with inlineTextChild.");
+ ok(!container.inlineTextChild.canExpand, "Can't expand inlineTextChild.");
+ ok(container.editor.elt.querySelector(".text").textContent.trim(),
+ "newtext", "Single text child editor updated.");
+ },
+ },
+
+ {
+ desc: "Updating the innerHTML",
+ test: function* (testActor) {
+ yield testActor.setProperty("#node2", "innerHTML",
+ "<div><span>foo</span></div>");
+ },
+ check: function* (inspector) {
+ let container = yield getContainerForSelector("#node2", inspector);
+
+ let openTags = container.children.querySelectorAll(".open .tag");
+ is(openTags.length, 2, "There are 2 tags in node2");
+ is(openTags[0].textContent.trim(), "div", "The first tag is a div");
+ is(openTags[1].textContent.trim(), "span", "The second tag is a span");
+
+ is(container.children.querySelector(".text").textContent.trim(), "foo",
+ "The span's textcontent is correct");
+ }
+ },
+ {
+ desc: "Removing child nodes",
+ test: function* (testActor) {
+ yield testActor.eval(`
+ let node4 = content.document.querySelector("#node4");
+ while (node4.firstChild) {
+ node4.removeChild(node4.firstChild);
+ }
+ `);
+ },
+ check: function* (inspector) {
+ let {children} = yield getContainerForSelector("#node4", inspector);
+ is(children.innerHTML, "", "Children have been removed");
+ }
+ },
+ {
+ desc: "Appending a child to a different parent",
+ test: function* (testActor) {
+ yield testActor.eval(`
+ let node17 = content.document.querySelector("#node17");
+ let node2 = content.document.querySelector("#node2");
+ node2.appendChild(node17);
+ `);
+ },
+ check: function* (inspector) {
+ let {children} = yield getContainerForSelector("#node16", inspector);
+ is(children.innerHTML, "",
+ "Node17 has been removed from its node16 parent");
+
+ let container = yield getContainerForSelector("#node2", inspector);
+ let openTags = container.children.querySelectorAll(".open .tag");
+ is(openTags.length, 3, "There are now 3 tags in node2");
+ is(openTags[2].textContent.trim(), "p", "The third tag is node17");
+ }
+ },
+ {
+ desc: "Swapping a parent and child element, putting them in the same tree",
+ // body
+ // node1
+ // node18
+ // node19
+ // node20
+ // node21
+ // will become:
+ // body
+ // node1
+ // node20
+ // node21
+ // node18
+ // node19
+ test: function* (testActor) {
+ yield testActor.eval(`
+ let node18 = content.document.querySelector("#node18");
+ let node20 = content.document.querySelector("#node20");
+ let node1 = content.document.querySelector("#node1");
+ node1.appendChild(node20);
+ node20.appendChild(node18);
+ `);
+ },
+ check: function* (inspector) {
+ yield inspector.markup.expandAll();
+
+ let {children} = yield getContainerForSelector("#node1", inspector);
+ is(children.childNodes.length, 2,
+ "Node1 now has 2 children (textnode and node20)");
+
+ let node20 = children.childNodes[1];
+ let node20Children = node20.container.children;
+ is(node20Children.childNodes.length, 2,
+ "Node20 has 2 children (21 and 18)");
+
+ let node21 = node20Children.childNodes[0];
+ is(node21.container.editor.elt.querySelector(".text").textContent.trim(),
+ "line21", "Node21 has a single text child");
+
+ let node18 = node20Children.childNodes[1];
+ is(node18.querySelector(".open .attreditor .attr-value")
+ .textContent.trim(),
+ "node18", "Node20's second child is indeed node18");
+ }
+ }
+];
+
+add_task(function* () {
+ let {inspector, testActor} = yield openInspectorForURL(TEST_URL);
+
+ info("Expanding all markup-view nodes");
+ yield inspector.markup.expandAll();
+
+ for (let {desc, test, check, numMutations} of TEST_DATA) {
+ info("Starting test: " + desc);
+
+ numMutations = numMutations || 1;
+
+ info("Executing the test markup mutation");
+
+ // If a test expects more than one mutation it may come through in a single
+ // event or possibly in multiples.
+ let def = defer();
+ let seenMutations = 0;
+ inspector.on("markupmutation", function onmutation(e, mutations) {
+ seenMutations += mutations.length;
+ info("Receieved " + seenMutations +
+ " mutations, expecting at least " + numMutations);
+ if (seenMutations >= numMutations) {
+ inspector.off("markupmutation", onmutation);
+ def.resolve();
+ }
+ });
+ yield test(testActor, inspector);
+ yield def.promise;
+
+ info("Expanding all markup-view nodes to make sure new nodes are imported");
+ yield inspector.markup.expandAll();
+
+ info("Checking the markup-view content");
+ yield check(inspector);
+ }
+});
diff --git a/devtools/client/inspector/markup/test/browser_markup_mutation_02.js b/devtools/client/inspector/markup/test/browser_markup_mutation_02.js
new file mode 100644
index 000000000..eb69b4201
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_mutation_02.js
@@ -0,0 +1,159 @@
+/* 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";
+
+// Test that markup-containers in the markup-view do flash when their
+// corresponding DOM nodes mutate
+
+// Have to use the same timer functions used by the inspector.
+const {clearTimeout} = Cu.import("resource://gre/modules/Timer.jsm", {});
+
+const TEST_URL = URL_ROOT + "doc_markup_flashing.html";
+
+// The test data contains a list of mutations to test.
+// Each item is an object:
+// - desc: a description of the test step, for better logging
+// - mutate: a generator function that should make changes to the content DOM
+// - attribute: if set, the test will expect the corresponding attribute to
+// flash instead of the whole node
+// - flashedNode: [optional] the css selector of the node that is expected to
+// flash in the markup-view as a result of the mutation.
+// If missing, the rootNode (".list") will be expected to flash
+const TEST_DATA = [{
+ desc: "Adding a new node should flash the new node",
+ mutate: function* (testActor) {
+ yield testActor.eval(`
+ let newLi = content.document.createElement("LI");
+ newLi.textContent = "new list item";
+ content.document.querySelector(".list").appendChild(newLi);
+ `);
+ },
+ flashedNode: ".list li:nth-child(3)"
+}, {
+ desc: "Removing a node should flash its parent",
+ mutate: function* (testActor) {
+ yield testActor.eval(`
+ let root = content.document.querySelector(".list");
+ root.removeChild(root.lastElementChild);
+ `);
+ }
+}, {
+ desc: "Re-appending an existing node should only flash this node",
+ mutate: function* (testActor) {
+ yield testActor.eval(`
+ let root = content.document.querySelector(".list");
+ root.appendChild(root.firstElementChild);
+ `);
+ },
+ flashedNode: ".list .item:last-child"
+}, {
+ desc: "Adding an attribute should flash the attribute",
+ attribute: "test-name",
+ mutate: function* (testActor) {
+ yield testActor.setAttribute(".list", "test-name", "value-" + Date.now());
+ }
+}, {
+ desc: "Adding an attribute with css reserved characters should flash the " +
+ "attribute",
+ attribute: "one:two",
+ mutate: function* (testActor) {
+ yield testActor.setAttribute(".list", "one:two", "value-" + Date.now());
+ }
+}, {
+ desc: "Editing an attribute should flash the attribute",
+ attribute: "class",
+ mutate: function* (testActor) {
+ yield testActor.setAttribute(".list", "class", "list value-" + Date.now());
+ }
+}, {
+ desc: "Multiple changes to an attribute should flash the attribute",
+ attribute: "class",
+ mutate: function* (testActor) {
+ yield testActor.eval(`
+ let root = content.document.querySelector(".list");
+ root.removeAttribute("class");
+ root.setAttribute("class", "list value-" + Date.now());
+ root.setAttribute("class", "list value-" + Date.now());
+ root.removeAttribute("class");
+ root.setAttribute("class", "list value-" + Date.now());
+ root.setAttribute("class", "list value-" + Date.now());
+ `);
+ }
+}, {
+ desc: "Removing an attribute should flash the node",
+ mutate: function* (testActor) {
+ yield testActor.eval(`
+ let root = content.document.querySelector(".list");
+ root.removeAttribute("class");
+ `);
+ }
+}];
+
+add_task(function* () {
+ let {inspector, testActor} = yield openInspectorForURL(TEST_URL);
+
+ // Make sure mutated nodes flash for a very long time so we can more easily
+ // assert they do
+ inspector.markup.CONTAINER_FLASHING_DURATION = 1000 * 60 * 60;
+
+ info("Getting the <ul.list> root node to test mutations on");
+ let rootNodeFront = yield getNodeFront(".list", inspector);
+
+ info("Selecting the last element of the root node before starting");
+ yield selectNode(".list .item:nth-child(2)", inspector);
+
+ for (let {mutate, flashedNode, desc, attribute} of TEST_DATA) {
+ info("Starting test: " + desc);
+
+ info("Mutating the DOM and listening for markupmutation event");
+ let onMutation = inspector.once("markupmutation");
+ yield mutate(testActor);
+ let mutations = yield onMutation;
+
+ info("Wait for the breadcrumbs widget to update if it needs to");
+ if (inspector.breadcrumbs._hasInterestingMutations(mutations)) {
+ yield inspector.once("breadcrumbs-updated");
+ }
+
+ info("Asserting that the correct markup-container is flashing");
+ let flashingNodeFront = rootNodeFront;
+ if (flashedNode) {
+ flashingNodeFront = yield getNodeFront(flashedNode, inspector);
+ }
+
+ if (attribute) {
+ yield assertAttributeFlashing(flashingNodeFront, attribute, inspector);
+ } else {
+ yield assertNodeFlashing(flashingNodeFront, inspector);
+ }
+ }
+});
+
+function* assertNodeFlashing(nodeFront, inspector) {
+ let container = getContainerForNodeFront(nodeFront, inspector);
+ ok(container, "Markup container for node found");
+ ok(container.tagState.classList.contains("theme-bg-contrast"),
+ "Markup container for node is flashing");
+
+ // Clear the mutation flashing timeout now that we checked the node was
+ // flashing.
+ clearTimeout(container._flashMutationTimer);
+ container._flashMutationTimer = null;
+ container.tagState.classList.remove("theme-bg-contrast");
+}
+
+function* assertAttributeFlashing(nodeFront, attribute, inspector) {
+ let container = getContainerForNodeFront(nodeFront, inspector);
+ ok(container, "Markup container for node found");
+ ok(container.editor.attrElements.get(attribute),
+ "Attribute exists on editor");
+
+ let attributeElement = container.editor.getAttributeElement(attribute);
+
+ ok(attributeElement.classList.contains("theme-bg-contrast"),
+ "Element for " + attribute + " attribute is flashing");
+
+ attributeElement.classList.remove("theme-bg-contrast");
+}
diff --git a/devtools/client/inspector/markup/test/browser_markup_navigation.js b/devtools/client/inspector/markup/test/browser_markup_navigation.js
new file mode 100644
index 000000000..5bfd9719f
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_navigation.js
@@ -0,0 +1,147 @@
+/* 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";
+
+// Test that the markup-view nodes can be navigated to with the keyboard
+
+const TEST_URL = URL_ROOT + "doc_markup_navigation.html";
+const TEST_DATA = [
+ ["pageup", "*doctype*"],
+ ["down", "html"],
+ ["down", "head"],
+ ["down", "body"],
+ ["down", "node0"],
+ ["right", "node0"],
+ ["down", "node1"],
+ ["down", "node2"],
+ ["down", "node3"],
+ ["down", "*comment*"],
+ ["down", "node4"],
+ ["right", "node4"],
+ ["down", "*text*"],
+ ["down", "node5"],
+ ["down", "*text*"],
+ ["down", "node6"],
+ ["down", "*text*"],
+ ["down", "*comment*"],
+ ["down", "node7"],
+ ["right", "node7"],
+ ["down", "*text*"],
+ ["down", "node8"],
+ ["left", "node7"],
+ ["left", "node7"],
+ ["right", "node7"],
+ ["right", "*text*"],
+ ["down", "node8"],
+ ["down", "*text*"],
+ ["down", "node9"],
+ ["down", "*text*"],
+ ["down", "node10"],
+ ["down", "*text*"],
+ ["down", "node11"],
+ ["down", "*text*"],
+ ["down", "node12"],
+ ["right", "node12"],
+ ["down", "*text*"],
+ ["down", "node13"],
+ ["down", "node14"],
+ ["down", "node15"],
+ ["down", "node15"],
+ ["down", "node15"],
+ ["up", "node14"],
+ ["up", "node13"],
+ ["up", "*text*"],
+ ["up", "node12"],
+ ["left", "node12"],
+ ["down", "node14"],
+ ["home", "*doctype*"],
+ ["pagedown", "*text*"],
+ ["down", "node5"],
+ ["down", "*text*"],
+ ["down", "node6"],
+ ["down", "*text*"],
+ ["down", "*comment*"],
+ ["down", "node7"],
+ ["left", "node7"],
+ ["down", "*text*"],
+ ["down", "node9"],
+ ["down", "*text*"],
+ ["down", "node10"],
+ ["pageup", "*text*"],
+ ["pageup", "*doctype*"],
+ ["down", "html"],
+ ["left", "html"],
+ ["down", "head"]
+];
+
+add_task(function* () {
+ let {inspector} = yield openInspectorForURL(TEST_URL);
+
+ info("Making sure the markup-view frame is focused");
+ inspector.markup._frame.focus();
+
+ info("Starting to iterate through the test data");
+ for (let [key, className] of TEST_DATA) {
+ info("Testing step: " + key + " to navigate to " + className);
+ pressKey(key);
+
+ info("Making sure markup-view children get updated");
+ yield waitForChildrenUpdated(inspector);
+
+ info("Checking the right node is selected");
+ checkSelectedNode(key, className, inspector);
+ }
+
+ // In theory, we should wait for the inspector-updated event at each iteration
+ // of the previous loop where we expect the current node to change (because
+ // changing the current node ends up refreshing the rule-view, breadcrumbs,
+ // ...), but this would make this test a *lot* slower. Instead, having a final
+ // catch-all event works too.
+ yield inspector.once("inspector-updated");
+});
+
+function pressKey(key) {
+ switch (key) {
+ case "right":
+ EventUtils.synthesizeKey("VK_RIGHT", {});
+ break;
+ case "down":
+ EventUtils.synthesizeKey("VK_DOWN", {});
+ break;
+ case "left":
+ EventUtils.synthesizeKey("VK_LEFT", {});
+ break;
+ case "up":
+ EventUtils.synthesizeKey("VK_UP", {});
+ break;
+ case "pageup":
+ EventUtils.synthesizeKey("VK_PAGE_UP", {});
+ break;
+ case "pagedown":
+ EventUtils.synthesizeKey("VK_PAGE_DOWN", {});
+ break;
+ case "home":
+ EventUtils.synthesizeKey("VK_HOME", {});
+ break;
+ }
+}
+
+function checkSelectedNode(key, className, inspector) {
+ let node = inspector.selection.nodeFront;
+
+ if (className == "*comment*") {
+ is(node.nodeType, Node.COMMENT_NODE,
+ "Found a comment after pressing " + key);
+ } else if (className == "*text*") {
+ is(node.nodeType, Node.TEXT_NODE,
+ "Found text after pressing " + key);
+ } else if (className == "*doctype*") {
+ is(node.nodeType, Node.DOCUMENT_TYPE_NODE,
+ "Found the doctype after pressing " + key);
+ } else {
+ is(node.className, className,
+ "Found node: " + className + " after pressing " + key);
+ }
+}
diff --git a/devtools/client/inspector/markup/test/browser_markup_node_names.js b/devtools/client/inspector/markup/test/browser_markup_node_names.js
new file mode 100644
index 000000000..a8afad5e9
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_node_names.js
@@ -0,0 +1,28 @@
+/* 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";
+
+// Test element node name in the markupview
+const TEST_URL = URL_ROOT + "doc_markup_html_mixed_case.html";
+
+add_task(function* () {
+ let {inspector} = yield openInspectorForURL(TEST_URL);
+
+ // Get and open the svg element to show its children
+ let svgNodeFront = yield getNodeFront("svg", inspector);
+ yield inspector.markup.expandNode(svgNodeFront);
+ yield waitForMultipleChildrenUpdates(inspector);
+
+ let clipPathContainer = yield getContainerForSelector("clipPath", inspector);
+ info("Checking the clipPath element");
+ ok(clipPathContainer.editor.tag.textContent === "clipPath",
+ "clipPath node name is not lowercased");
+
+ let divContainer = yield getContainerForSelector("div", inspector);
+
+ info("Checking the div element");
+ ok(divContainer.editor.tag.textContent === "div",
+ "div node name is lowercased");
+});
diff --git a/devtools/client/inspector/markup/test/browser_markup_node_names_namespaced.js b/devtools/client/inspector/markup/test/browser_markup_node_names_namespaced.js
new file mode 100644
index 000000000..261176f94
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_node_names_namespaced.js
@@ -0,0 +1,43 @@
+/* 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";
+
+// Test namespaced element node names in the markupview.
+
+const XHTML = `
+ <!DOCTYPE html>
+ <html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:svg="http://www.w3.org/2000/svg">
+ <body>
+ <svg:svg width="100" height="100">
+ <svg:clipPath id="clip">
+ <svg:rect id="rectangle" x="0" y="0" width="10" height="5"></svg:rect>
+ </svg:clipPath>
+ <svg:circle cx="0" cy="0" r="5"></svg:circle>
+ </svg:svg>
+ </body>
+ </html>
+`;
+
+const TEST_URI = "data:application/xhtml+xml;charset=utf-8," + encodeURI(XHTML);
+
+add_task(function* () {
+ let {inspector} = yield openInspectorForURL(TEST_URI);
+
+ // Get and open the svg element to show its children.
+ let svgNodeFront = yield getNodeFront("svg", inspector);
+ yield inspector.markup.expandNode(svgNodeFront);
+ yield waitForMultipleChildrenUpdates(inspector);
+
+ let clipPathContainer = yield getContainerForSelector("clipPath", inspector);
+ info("Checking the clipPath element");
+ ok(clipPathContainer.editor.tag.textContent === "svg:clipPath",
+ "svg:clipPath node is correctly displayed");
+
+ let circlePathContainer = yield getContainerForSelector("circle", inspector);
+ info("Checking the circle element");
+ ok(circlePathContainer.editor.tag.textContent === "svg:circle",
+ "svg:circle node is correctly displayed");
+});
diff --git a/devtools/client/inspector/markup/test/browser_markup_node_not_displayed_01.js b/devtools/client/inspector/markup/test/browser_markup_node_not_displayed_01.js
new file mode 100644
index 000000000..ea4ecdfd0
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_node_not_displayed_01.js
@@ -0,0 +1,35 @@
+/* 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";
+
+// Tests that nodes that are not displayed appear differently in the markup-view
+// when these nodes are imported in the view.
+
+// Note that nodes inside a display:none parent are obviously not displayed too
+// but the markup-view uses css inheritance to mark those as hidden instead of
+// having to visit each and every child of a hidden node. So there's no sense
+// testing children nodes.
+
+const TEST_URL = URL_ROOT + "doc_markup_not_displayed.html";
+const TEST_DATA = [
+ {selector: "#normal-div", isDisplayed: true},
+ {selector: "head", isDisplayed: false},
+ {selector: "#display-none", isDisplayed: false},
+ {selector: "#hidden-true", isDisplayed: false},
+ {selector: "#visibility-hidden", isDisplayed: true},
+ {selector: "#hidden-via-hide-shortcut", isDisplayed: false},
+];
+
+add_task(function* () {
+ let {inspector} = yield openInspectorForURL(TEST_URL);
+
+ for (let {selector, isDisplayed} of TEST_DATA) {
+ info("Getting node " + selector);
+ let nodeFront = yield getNodeFront(selector, inspector);
+ let container = getContainerForNodeFront(nodeFront, inspector);
+ is(!container.elt.classList.contains("not-displayed"), isDisplayed,
+ `The container for ${selector} is marked as displayed ${isDisplayed}`);
+ }
+});
diff --git a/devtools/client/inspector/markup/test/browser_markup_node_not_displayed_02.js b/devtools/client/inspector/markup/test/browser_markup_node_not_displayed_02.js
new file mode 100644
index 000000000..b0423d2e6
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_node_not_displayed_02.js
@@ -0,0 +1,150 @@
+/* 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";
+
+// Tests that nodes are marked as displayed and not-displayed dynamically, when
+// their display changes
+
+const TEST_URL = URL_ROOT + "doc_markup_not_displayed.html";
+const TEST_DATA = [
+ {
+ desc: "Hiding a node by creating a new stylesheet",
+ selector: "#normal-div",
+ before: true,
+ changeStyle: function* (testActor) {
+ yield testActor.eval(`
+ let div = content.document.createElement("div");
+ div.id = "new-style";
+ div.innerHTML = "<style>#normal-div {display:none;}</style>";
+ content.document.body.appendChild(div);
+ `);
+ },
+ after: false
+ },
+ {
+ desc: "Showing a node by deleting an existing stylesheet",
+ selector: "#normal-div",
+ before: false,
+ changeStyle: function* (testActor) {
+ yield testActor.eval(`
+ content.document.getElementById("new-style").remove();
+ `);
+ },
+ after: true
+ },
+ {
+ desc: "Hiding a node by changing its style property",
+ selector: "#display-none",
+ before: false,
+ changeStyle: function* (testActor) {
+ yield testActor.eval(`
+ let node = content.document.querySelector("#display-none");
+ node.style.display = "block";
+ `);
+ },
+ after: true
+ },
+ {
+ desc: "Showing a node by removing its hidden attribute",
+ selector: "#hidden-true",
+ before: false,
+ changeStyle: function* (testActor) {
+ yield testActor.eval(`
+ content.document.querySelector("#hidden-true")
+ .removeAttribute("hidden");
+ `);
+ },
+ after: true
+ },
+ {
+ desc: "Hiding a node by adding a hidden attribute",
+ selector: "#hidden-true",
+ before: true,
+ changeStyle: function* (testActor) {
+ yield testActor.setAttribute("#hidden-true", "hidden", "true");
+ },
+ after: false
+ },
+ {
+ desc: "Showing a node by changin a stylesheet's rule",
+ selector: "#hidden-via-stylesheet",
+ before: false,
+ changeStyle: function* (testActor) {
+ yield testActor.eval(`
+ content.document.styleSheets[0]
+ .cssRules[0].style
+ .setProperty("display", "inline");
+ `);
+ },
+ after: true
+ },
+ {
+ desc: "Hiding a node by adding a new rule to a stylesheet",
+ selector: "#hidden-via-stylesheet",
+ before: true,
+ changeStyle: function* (testActor) {
+ yield testActor.eval(`
+ content.document.styleSheets[0].insertRule(
+ "#hidden-via-stylesheet {display: none;}", 1);
+ `);
+ },
+ after: false
+ },
+ {
+ desc: "Hiding a node by adding a class that matches an existing rule",
+ selector: "#normal-div",
+ before: true,
+ changeStyle: function* (testActor) {
+ yield testActor.eval(`
+ content.document.styleSheets[0].insertRule(
+ ".a-new-class {display: none;}", 2);
+ content.document.querySelector("#normal-div")
+ .classList.add("a-new-class");
+ `);
+ },
+ after: false
+ }
+];
+
+add_task(function* () {
+ let {inspector, testActor} = yield openInspectorForURL(TEST_URL);
+
+ for (let data of TEST_DATA) {
+ info("Running test case: " + data.desc);
+ yield runTestData(inspector, testActor, data);
+ }
+});
+
+function* runTestData(inspector, testActor,
+ {selector, before, changeStyle, after}) {
+ info("Getting the " + selector + " test node");
+ let nodeFront = yield getNodeFront(selector, inspector);
+ let container = getContainerForNodeFront(nodeFront, inspector);
+ is(!container.elt.classList.contains("not-displayed"), before,
+ "The container is marked as " + (before ? "shown" : "hidden"));
+
+ info("Listening for the display-change event");
+ let onDisplayChanged = defer();
+ inspector.markup.walker.once("display-change", onDisplayChanged.resolve);
+
+ info("Making style changes");
+ yield changeStyle(testActor);
+ let nodes = yield onDisplayChanged.promise;
+
+ info("Verifying that the list of changed nodes include our container");
+
+ ok(nodes.length, "The display-change event was received with a nodes");
+ let foundContainer = false;
+ for (let node of nodes) {
+ if (getContainerForNodeFront(node, inspector) === container) {
+ foundContainer = true;
+ break;
+ }
+ }
+ ok(foundContainer, "Container is part of the list of changed nodes");
+
+ is(!container.elt.classList.contains("not-displayed"), after,
+ "The container is marked as " + (after ? "shown" : "hidden"));
+}
diff --git a/devtools/client/inspector/markup/test/browser_markup_pagesize_01.js b/devtools/client/inspector/markup/test/browser_markup_pagesize_01.js
new file mode 100644
index 000000000..a9ba9fc05
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_pagesize_01.js
@@ -0,0 +1,86 @@
+/* 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";
+
+// Tests that the markup view loads only as many nodes as specified by the
+// devtools.markup.pagesize preference.
+
+Services.prefs.setIntPref("devtools.markup.pagesize", 5);
+
+const TEST_URL = URL_ROOT + "doc_markup_pagesize_01.html";
+const TEST_DATA = [{
+ desc: "Select the last item",
+ selector: "#z",
+ expected: "*more*vwxyz"
+}, {
+ desc: "Select the first item",
+ selector: "#a",
+ expected: "abcde*more*"
+}, {
+ desc: "Select the last item",
+ selector: "#z",
+ expected: "*more*vwxyz"
+}, {
+ desc: "Select an already-visible item",
+ selector: "#v",
+ // Because "v" was already visible, we shouldn't have loaded
+ // a different page.
+ expected: "*more*vwxyz"
+}, {
+ desc: "Verify childrenDirty reloads the page",
+ selector: "#w",
+ forceReload: true,
+ // But now that we don't already have a loaded page, selecting
+ // w should center around w.
+ expected: "*more*uvwxy*more*"
+}];
+
+add_task(function* () {
+ let {inspector} = yield openInspectorForURL(TEST_URL);
+
+ info("Start iterating through the test data");
+ for (let step of TEST_DATA) {
+ info("Start test: " + step.desc);
+
+ if (step.forceReload) {
+ yield forceReload(inspector);
+ }
+ info("Selecting the node that corresponds to " + step.selector);
+ yield selectNode(step.selector, inspector);
+
+ info("Checking that the right nodes are shwon");
+ yield assertChildren(step.expected, inspector);
+ }
+
+ info("Checking that clicking the more button loads everything");
+ yield clickShowMoreNodes(inspector);
+ yield inspector.markup._waitForChildren();
+ yield assertChildren("abcdefghijklmnopqrstuvwxyz", inspector);
+});
+
+function* assertChildren(expected, inspector) {
+ let container = yield getContainerForSelector("body", inspector);
+ let found = "";
+ for (let child of container.children.children) {
+ if (child.classList.contains("more-nodes")) {
+ found += "*more*";
+ } else {
+ found += child.container.node.getAttribute("id");
+ }
+ }
+ is(found, expected, "Got the expected children.");
+}
+
+function* forceReload(inspector) {
+ let container = yield getContainerForSelector("body", inspector);
+ container.childrenDirty = true;
+}
+
+function* clickShowMoreNodes(inspector) {
+ let container = yield getContainerForSelector("body", inspector);
+ let button = container.elt.querySelector("button");
+ let win = button.ownerDocument.defaultView;
+ EventUtils.sendMouseEvent({type: "click"}, button, win);
+}
diff --git a/devtools/client/inspector/markup/test/browser_markup_pagesize_02.js b/devtools/client/inspector/markup/test/browser_markup_pagesize_02.js
new file mode 100644
index 000000000..549a36b0d
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_pagesize_02.js
@@ -0,0 +1,47 @@
+/* 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";
+
+// Tests that the markup view loads only as many nodes as specified
+// by the devtools.markup.pagesize preference and that pressing the "show all
+// nodes" actually shows the nodes
+
+const TEST_URL = URL_ROOT + "doc_markup_pagesize_02.html";
+
+// Make sure nodes are hidden when there are more than 5 in a row
+Services.prefs.setIntPref("devtools.markup.pagesize", 5);
+
+add_task(function* () {
+ let {inspector, testActor} = yield openInspectorForURL(TEST_URL);
+
+ info("Selecting the UL node");
+ yield clickContainer("ul", inspector);
+ info("Reloading the page with the UL node selected will expand its children");
+ yield reloadPage(inspector, testActor);
+ yield inspector.markup._waitForChildren();
+
+ info("Click on the 'show all nodes' button in the UL's list of children");
+ yield showAllNodes(inspector);
+
+ yield assertAllNodesAreVisible(inspector, testActor);
+});
+
+function* showAllNodes(inspector) {
+ let container = yield getContainerForSelector("ul", inspector);
+ let button = container.elt.querySelector("button");
+ ok(button, "All nodes button is here");
+ let win = button.ownerDocument.defaultView;
+
+ EventUtils.sendMouseEvent({type: "click"}, button, win);
+ yield inspector.markup._waitForChildren();
+}
+
+function* assertAllNodesAreVisible(inspector, testActor) {
+ let container = yield getContainerForSelector("ul", inspector);
+ ok(!container.elt.querySelector("button"),
+ "All nodes button isn't here anymore");
+ let numItems = yield testActor.getNumberOfElementMatches("ul > *");
+ is(container.children.childNodes.length, numItems);
+}
diff --git a/devtools/client/inspector/markup/test/browser_markup_remove_xul_attributes.js b/devtools/client/inspector/markup/test/browser_markup_remove_xul_attributes.js
new file mode 100644
index 000000000..b7065c683
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_remove_xul_attributes.js
@@ -0,0 +1,28 @@
+/* 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";
+
+// Test confirms that XUL attributes don't show up as empty
+// attributes after being deleted
+
+const TEST_URL = URL_ROOT + "doc_markup_xul.xul";
+
+add_task(function* () {
+ let {inspector, testActor} = yield openInspectorForURL(TEST_URL);
+
+ let panelFront = yield getNodeFront("#test", inspector);
+ ok(panelFront.hasAttribute("id"),
+ "panelFront has id attribute in the beginning");
+
+ info("Removing panel's id attribute");
+ let onMutation = inspector.once("markupmutation");
+ yield testActor.removeAttribute("#test", "id");
+
+ info("Waiting for markupmutation");
+ yield onMutation;
+
+ is(panelFront.hasAttribute("id"), false,
+ "panelFront doesn't have id attribute anymore");
+});
diff --git a/devtools/client/inspector/markup/test/browser_markup_search_01.js b/devtools/client/inspector/markup/test/browser_markup_search_01.js
new file mode 100644
index 000000000..68f0c04db
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_search_01.js
@@ -0,0 +1,51 @@
+/* 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";
+
+// Test that searching for nodes using the selector-search input expands and
+// selects the right nodes in the markup-view, even when those nodes are deeply
+// nested (and therefore not attached yet when the markup-view is initialized).
+
+const TEST_URL = URL_ROOT + "doc_markup_search.html";
+
+add_task(function* () {
+ let {inspector} = yield openInspectorForURL(TEST_URL);
+
+ let container = yield getContainerForSelector("em", inspector);
+ ok(!container, "The <em> tag isn't present yet in the markup-view");
+
+ // Searching for the innermost element first makes sure that the inspector
+ // back-end is able to attach the resulting node to the tree it knows at the
+ // moment. When the inspector is started, the <body> is the default selected
+ // node, and only the parents up to the ROOT are known, and its direct
+ // children.
+ info("searching for the innermost child: <em>");
+ yield searchFor("em", inspector);
+
+ container = yield getContainerForSelector("em", inspector);
+ ok(container, "The <em> tag is now imported in the markup-view");
+
+ let nodeFront = yield getNodeFront("em", inspector);
+ is(inspector.selection.nodeFront, nodeFront,
+ "The <em> tag is the currently selected node");
+
+ info("searching for other nodes too");
+ for (let node of ["span", "li", "ul"]) {
+ yield searchFor(node, inspector);
+
+ nodeFront = yield getNodeFront(node, inspector);
+ is(inspector.selection.nodeFront, nodeFront,
+ "The <" + node + "> tag is the currently selected node");
+ }
+});
+
+function* searchFor(selector, inspector) {
+ let onNewNodeFront = inspector.selection.once("new-node-front");
+
+ searchUsingSelectorSearch(selector, inspector);
+
+ yield onNewNodeFront;
+ yield inspector.once("inspector-updated");
+}
diff --git a/devtools/client/inspector/markup/test/browser_markup_tag_edit_01.js b/devtools/client/inspector/markup/test/browser_markup_tag_edit_01.js
new file mode 100644
index 000000000..b1b4f7115
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_tag_edit_01.js
@@ -0,0 +1,68 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_attributes_test_runner.js */
+"use strict";
+
+// Test editing various markup-containers' attribute fields
+
+loadHelperScript("helper_attributes_test_runner.js");
+
+const TEST_URL = URL_ROOT + "doc_markup_edit.html";
+var TEST_DATA = [{
+ desc: "Change an attribute",
+ node: "#node1",
+ originalAttributes: {
+ id: "node1",
+ class: "node1"
+ },
+ name: "class",
+ value: 'class="changednode1"',
+ expectedAttributes: {
+ id: "node1",
+ class: "changednode1"
+ }
+}, {
+ desc: "Try changing an attribute to a quote (\") - this should result " +
+ "in it being set to an empty string",
+ node: "#node22",
+ originalAttributes: {
+ id: "node22",
+ class: "unchanged"
+ },
+ name: "class",
+ value: 'class="""',
+ expectedAttributes: {
+ id: "node22",
+ class: ""
+ }
+}, {
+ desc: "Remove an attribute",
+ node: "#node4",
+ originalAttributes: {
+ id: "node4",
+ class: "node4"
+ },
+ name: "class",
+ value: "",
+ expectedAttributes: {
+ id: "node4"
+ }
+}, {
+ desc: "Try add attributes by adding to an existing attribute's entry",
+ node: "#node24",
+ originalAttributes: {
+ id: "node24"
+ },
+ name: "id",
+ value: 'id="node24" class="""',
+ expectedAttributes: {
+ id: "node24",
+ class: ""
+ }
+}];
+
+add_task(function* () {
+ let {inspector, testActor} = yield openInspectorForURL(TEST_URL);
+ yield runEditAttributesTests(TEST_DATA, inspector, testActor);
+});
diff --git a/devtools/client/inspector/markup/test/browser_markup_tag_edit_02.js b/devtools/client/inspector/markup/test/browser_markup_tag_edit_02.js
new file mode 100644
index 000000000..1e32d783a
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_tag_edit_02.js
@@ -0,0 +1,44 @@
+/* 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";
+
+// Tests that an existing attribute can be modified
+
+const TEST_URL = `data:text/html,
+ <div id='test-div'>Test modifying my ID attribute</div>`;
+
+add_task(function* () {
+ info("Opening the inspector on the test page");
+ let {inspector, testActor} = yield openInspectorForURL(TEST_URL);
+
+ info("Selecting the test node");
+ yield focusNode("#test-div", inspector);
+
+ info("Verify attributes, only ID should be there for now");
+ yield assertAttributes("#test-div", {
+ id: "test-div"
+ }, testActor);
+
+ info("Focus the ID attribute and change its content");
+ let {editor} = yield getContainerForSelector("#test-div", inspector);
+ let attr = editor.attrElements.get("id").querySelector(".editable");
+ let mutated = inspector.once("markupmutation");
+ setEditableFieldValue(attr,
+ attr.textContent + ' class="newclass" style="color:green"', inspector);
+ yield mutated;
+
+ info("Verify attributes, should have ID, class and style");
+ yield assertAttributes("#test-div", {
+ id: "test-div",
+ class: "newclass",
+ style: "color:green"
+ }, testActor);
+
+ info("Trying to undo the change");
+ yield undoChange(inspector);
+ yield assertAttributes("#test-div", {
+ id: "test-div"
+ }, testActor);
+});
diff --git a/devtools/client/inspector/markup/test/browser_markup_tag_edit_03.js b/devtools/client/inspector/markup/test/browser_markup_tag_edit_03.js
new file mode 100644
index 000000000..cdbdc72b6
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_tag_edit_03.js
@@ -0,0 +1,51 @@
+/* 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";
+
+// Tests that a node's tagname can be edited in the markup-view
+
+const TEST_URL = `data:text/html;charset=utf-8,
+ <div id='retag-me'><div id='retag-me-2'></div></div>`;
+
+add_task(function* () {
+ let {inspector, testActor} = yield openInspectorForURL(TEST_URL);
+
+ yield inspector.markup.expandAll();
+
+ info("Selecting the test node");
+ yield focusNode("#retag-me", inspector);
+
+ info("Getting the markup-container for the test node");
+ let container = yield getContainerForSelector("#retag-me", inspector);
+ ok(container.expanded, "The container is expanded");
+
+ let parentInfo = yield testActor.getNodeInfo("#retag-me");
+ is(parentInfo.tagName.toLowerCase(), "div",
+ "We've got #retag-me element, it's a DIV");
+ is(parentInfo.numChildren, 1, "#retag-me has one child");
+ let childInfo = yield testActor.getNodeInfo("#retag-me > *");
+ is(childInfo.attributes[0].value, "retag-me-2",
+ "#retag-me's only child is #retag-me-2");
+
+ info("Changing #retag-me's tagname in the markup-view");
+ let mutated = inspector.once("markupmutation");
+ let tagEditor = container.editor.tag;
+ setEditableFieldValue(tagEditor, "p", inspector);
+ yield mutated;
+
+ info("Checking that the markup-container exists and is correct");
+ container = yield getContainerForSelector("#retag-me", inspector);
+ ok(container.expanded, "The container is still expanded");
+ ok(container.selected, "The container is still selected");
+
+ info("Checking that the tagname change was done");
+ parentInfo = yield testActor.getNodeInfo("#retag-me");
+ is(parentInfo.tagName.toLowerCase(), "p",
+ "The #retag-me element is now a P");
+ is(parentInfo.numChildren, 1, "#retag-me still has one child");
+ childInfo = yield testActor.getNodeInfo("#retag-me > *");
+ is(childInfo.attributes[0].value, "retag-me-2",
+ "#retag-me's only child is #retag-me-2");
+});
diff --git a/devtools/client/inspector/markup/test/browser_markup_tag_edit_04-backspace.js b/devtools/client/inspector/markup/test/browser_markup_tag_edit_04-backspace.js
new file mode 100644
index 000000000..dbe718f45
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_tag_edit_04-backspace.js
@@ -0,0 +1,59 @@
+/* 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";
+
+// Tests that a node can be deleted from the markup-view with the backspace key.
+// Also checks that after deletion the correct element is highlighted.
+// The previous sibling is preferred, but the parent is a fallback.
+
+const HTML = `<style type="text/css">
+ #pseudo::before { content: 'before'; }
+ #pseudo::after { content: 'after'; }
+ </style>
+ <div id="parent">
+ <div id="first"></div>
+ <div id="second"></div>
+ <div id="third"></div>
+ </div>
+ <div id="only-child">
+ <div id="fourth"></div>
+ </div>
+ <div id="pseudo">
+ <div id="fifth"></div>
+ </div>`;
+const TEST_URL = "data:text/html;charset=utf-8," + encodeURIComponent(HTML);
+
+// List of all the test cases. Each item is an object with the following props:
+// - selector: the css selector of the node that should be selected
+// - focusedSelector: the css selector of the node we expect to be selected as
+// a result of the deletion
+// - pseudo: (optional) if the focused node is actually supposed to be a pseudo element
+// of the specified selector.
+// Note that after each test case, undo is called.
+const TEST_DATA = [{
+ selector: "#first",
+ focusedSelector: "#second"
+}, {
+ selector: "#second",
+ focusedSelector: "#first"
+}, {
+ selector: "#third",
+ focusedSelector: "#second"
+}, {
+ selector: "#fourth",
+ focusedSelector: "#only-child"
+}, {
+ selector: "#fifth",
+ focusedSelector: "#pseudo",
+ pseudo: "before"
+}];
+
+add_task(function* () {
+ let {inspector} = yield openInspectorForURL(TEST_URL);
+
+ for (let data of TEST_DATA) {
+ yield checkDeleteAndSelection(inspector, "back_space", data);
+ }
+});
diff --git a/devtools/client/inspector/markup/test/browser_markup_tag_edit_04-delete.js b/devtools/client/inspector/markup/test/browser_markup_tag_edit_04-delete.js
new file mode 100644
index 000000000..1446eba30
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_tag_edit_04-delete.js
@@ -0,0 +1,59 @@
+/* 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";
+
+// Tests that a node can be deleted from the markup-view with the delete key.
+// Also checks that after deletion the correct element is highlighted.
+// The next sibling is preferred, but the parent is a fallback.
+
+const HTML = `<style type="text/css">
+ #pseudo::before { content: 'before'; }
+ #pseudo::after { content: 'after'; }
+ </style>
+ <div id="parent">
+ <div id="first"></div>
+ <div id="second"></div>
+ <div id="third"></div>
+ </div>
+ <div id="only-child">
+ <div id="fourth"></div>
+ </div>
+ <div id="pseudo">
+ <div id="fifth"></div>
+ </div>`;
+const TEST_URL = "data:text/html;charset=utf-8," + encodeURIComponent(HTML);
+
+// List of all the test cases. Each item is an object with the following props:
+// - selector: the css selector of the node that should be selected
+// - focusedSelector: the css selector of the node we expect to be selected as
+// a result of the deletion
+// - pseudo: (optional) if the focused node is actually supposed to be a pseudo element
+// of the specified selector.
+// Note that after each test case, undo is called.
+const TEST_DATA = [{
+ selector: "#first",
+ focusedSelector: "#second"
+}, {
+ selector: "#second",
+ focusedSelector: "#third"
+}, {
+ selector: "#third",
+ focusedSelector: "#second"
+}, {
+ selector: "#fourth",
+ focusedSelector: "#only-child"
+}, {
+ selector: "#fifth",
+ focusedSelector: "#pseudo",
+ pseudo: "after"
+}];
+
+add_task(function* () {
+ let {inspector} = yield openInspectorForURL(TEST_URL);
+
+ for (let data of TEST_DATA) {
+ yield checkDeleteAndSelection(inspector, "delete", data);
+ }
+});
diff --git a/devtools/client/inspector/markup/test/browser_markup_tag_edit_05.js b/devtools/client/inspector/markup/test/browser_markup_tag_edit_05.js
new file mode 100644
index 000000000..54a1dab44
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_tag_edit_05.js
@@ -0,0 +1,77 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_attributes_test_runner.js */
+"use strict";
+
+// Tests that adding various types of attributes to nodes in the markup-view
+// works as expected. Also checks that the changes are properly undoable and
+// redoable. For each step in the test, we:
+// - Create a new DIV
+// - Make the change, check that the change was made as we expect
+// - Undo the change, check that the node is back in its original state
+// - Redo the change, check that the node change was made again correctly.
+
+loadHelperScript("helper_attributes_test_runner.js");
+
+var TEST_URL = "data:text/html,<div>markup-view attributes addition test</div>";
+var TEST_DATA = [{
+ desc: "Add an attribute value without closing \"",
+ text: 'style="display: block;',
+ expectedAttributes: {
+ style: "display: block;"
+ }
+}, {
+ desc: "Add an attribute value without closing '",
+ text: "style='display: inline;",
+ expectedAttributes: {
+ style: "display: inline;"
+ }
+}, {
+ desc: "Add an attribute wrapped with with double quotes double quote in it",
+ text: 'style="display: "inline',
+ expectedAttributes: {
+ style: "display: ",
+ inline: ""
+ }
+}, {
+ desc: "Add an attribute wrapped with single quotes with single quote in it",
+ text: "style='display: 'inline",
+ expectedAttributes: {
+ style: "display: ",
+ inline: ""
+ }
+}, {
+ desc: "Add an attribute with no value",
+ text: "disabled",
+ expectedAttributes: {
+ disabled: ""
+ }
+}, {
+ desc: "Add multiple attributes with no value",
+ text: "disabled autofocus",
+ expectedAttributes: {
+ disabled: "",
+ autofocus: ""
+ }
+}, {
+ desc: "Add multiple attributes with no value, and some with value",
+ text: "disabled name='name' data-test='test' autofocus",
+ expectedAttributes: {
+ disabled: "",
+ autofocus: "",
+ name: "name",
+ "data-test": "test"
+ }
+}, {
+ desc: "Add attribute with xmlns",
+ text: "xmlns:edi='http://ecommerce.example.org/schema'",
+ expectedAttributes: {
+ "xmlns:edi": "http://ecommerce.example.org/schema"
+ }
+}];
+
+add_task(function* () {
+ let {inspector, testActor} = yield openInspectorForURL(TEST_URL);
+ yield runAddAttributesTests(TEST_DATA, "div", inspector, testActor);
+});
diff --git a/devtools/client/inspector/markup/test/browser_markup_tag_edit_06.js b/devtools/client/inspector/markup/test/browser_markup_tag_edit_06.js
new file mode 100644
index 000000000..8202bd0a2
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_tag_edit_06.js
@@ -0,0 +1,85 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_attributes_test_runner.js */
+"use strict";
+
+// Tests that adding various types of attributes to nodes in the markup-view
+// works as expected. Also checks that the changes are properly undoable and
+// redoable. For each step in the test, we:
+// - Create a new DIV
+// - Make the change, check that the change was made as we expect
+// - Undo the change, check that the node is back in its original state
+// - Redo the change, check that the node change was made again correctly.
+
+loadHelperScript("helper_attributes_test_runner.js");
+
+var TEST_URL = "data:text/html,<div>markup-view attributes addition test</div>";
+var TEST_DATA = [{
+ desc: "Mixed single and double quotes",
+ text: "name=\"hi\" maxlength='not a number'",
+ expectedAttributes: {
+ maxlength: "not a number",
+ name: "hi"
+ }
+}, {
+ desc: "Invalid attribute name",
+ text: "x='y' <why-would-you-do-this>=\"???\"",
+ expectedAttributes: {
+ x: "y"
+ }
+}, {
+ desc: "Double quote wrapped in single quotes",
+ text: "x='h\"i'",
+ expectedAttributes: {
+ x: "h\"i"
+ }
+}, {
+ desc: "Single quote wrapped in double quotes",
+ text: "x=\"h'i\"",
+ expectedAttributes: {
+ x: "h'i"
+ }
+}, {
+ desc: "No quote wrapping",
+ text: "a=b x=y data-test=Some spaced data",
+ expectedAttributes: {
+ a: "b",
+ x: "y",
+ "data-test": "Some",
+ spaced: "",
+ data: ""
+ }
+}, {
+ desc: "Duplicate Attributes",
+ text: "a=b a='c' a=\"d\"",
+ expectedAttributes: {
+ a: "b"
+ }
+}, {
+ desc: "Inline styles",
+ text: "style=\"font-family: 'Lucida Grande', sans-serif; font-size: 75%;\"",
+ expectedAttributes: {
+ style: "font-family: 'Lucida Grande', sans-serif; font-size: 75%;"
+ }
+}, {
+ desc: "Object attribute names",
+ text: "toString=\"true\" hasOwnProperty=\"false\"",
+ expectedAttributes: {
+ tostring: "true",
+ hasownproperty: "false"
+ }
+}, {
+ desc: "Add event handlers",
+ text: "onclick=\"javascript: throw new Error('wont fire');\" " +
+ "onload=\"alert('here');\"",
+ expectedAttributes: {
+ onclick: "javascript: throw new Error('wont fire');",
+ onload: "alert('here');"
+ }
+}];
+
+add_task(function* () {
+ let {inspector, testActor} = yield openInspectorForURL(TEST_URL);
+ yield runAddAttributesTests(TEST_DATA, "div", inspector, testActor);
+});
diff --git a/devtools/client/inspector/markup/test/browser_markup_tag_edit_07.js b/devtools/client/inspector/markup/test/browser_markup_tag_edit_07.js
new file mode 100644
index 000000000..fffdc99cc
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_tag_edit_07.js
@@ -0,0 +1,135 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_attributes_test_runner.js */
+"use strict";
+
+// One more test testing various add-attributes configurations
+// Some of the test data below asserts that long attributes get collapsed
+
+loadHelperScript("helper_attributes_test_runner.js");
+
+/*eslint-disable */
+const LONG_ATTRIBUTE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ-ABCDEFGHIJKLMNOPQRSTUVWXYZ-ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZ-ABCDEFGHIJKLMNOPQRSTUVWXYZ-ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+const LONG_ATTRIBUTE_COLLAPSED = "ABCDEFGHIJKLMNOPQRSTUVWXYZ-ABCDEFGHIJKLMNOPQRSTUVWXYZ-ABCDEF\u2026UVWXYZ-ABCDEFGHIJKLMNOPQRSTUVWXYZ-ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+const DATA_URL_INLINE_STYLE='color: red; background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAABlBMVEUAAAD///+l2Z/dAAAAM0lEQVR4nGP4/5/h/1+G/58ZDrAz3D/McH8yw83NDDeNGe4Ug9C9zwz3gVLMDA/A6P9/AFGGFyjOXZtQAAAAAElFTkSuQmCC");';
+const DATA_URL_INLINE_STYLE_COLLAPSED='color: red; background: url("data:image/png;base64,iVBORw0KG\u2026NDDeNGe4Ug9C9zwz3gVLMDA/A6P9/AFGGFyjOXZtQAAAAAElFTkSuQmCC");';
+const DATA_URL_ATTRIBUTE = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAABlBMVEUAAAD///+l2Z/dAAAAM0lEQVR4nGP4/5/h/1+G/58ZDrAz3D/McH8yw83NDDeNGe4Ug9C9zwz3gVLMDA/A6P9/AFGGFyjOXZtQAAAAAElFTkSuQmCC";
+const DATA_URL_ATTRIBUTE_COLLAPSED = "data:image/png;base64,iVBORw0K\u20269/AFGGFyjOXZtQAAAAAElFTkSuQmCC";
+/*eslint-enable */
+
+var TEST_URL = "data:text/html,<div>markup-view attributes addition test</div>";
+var TEST_DATA = [{
+ desc: "Add an attribute value containing < > &uuml; \" & '",
+ text: 'src="somefile.html?param1=<a>&param2=&uuml;&param3=\'&quot;\'"',
+ expectedAttributes: {
+ src: "somefile.html?param1=<a>&param2=\xfc&param3='\"'"
+ }
+}, {
+ desc: "Add an attribute by clicking the empty space after a node",
+ text: 'class="newclass" style="color:green"',
+ expectedAttributes: {
+ class: "newclass",
+ style: "color:green"
+ }
+}, {
+ desc: "Try add an attribute containing a quote (\") attribute by " +
+ "clicking the empty space after a node - this should result " +
+ "in it being set to an empty string",
+ text: 'class="newclass" style="""',
+ expectedAttributes: {
+ class: "newclass",
+ style: ""
+ }
+}, {
+ desc: "Try to add long data URL to make sure it is collapsed in attribute " +
+ "editor.",
+ text: `style='${DATA_URL_INLINE_STYLE}'`,
+ expectedAttributes: {
+ "style": DATA_URL_INLINE_STYLE
+ },
+ validate: (container, inspector) => {
+ let editor = container.editor;
+ let visibleAttrText = editor.attrElements.get("style")
+ .querySelector(".attr-value")
+ .textContent;
+ is(visibleAttrText, DATA_URL_INLINE_STYLE_COLLAPSED);
+ }
+}, {
+ desc: "Try to add long attribute to make sure it is collapsed in attribute " +
+ "editor.",
+ text: `data-long="${LONG_ATTRIBUTE}"`,
+ expectedAttributes: {
+ "data-long": LONG_ATTRIBUTE
+ },
+ validate: (container, inspector) => {
+ let editor = container.editor;
+ let visibleAttrText = editor.attrElements.get("data-long")
+ .querySelector(".attr-value")
+ .textContent;
+ is(visibleAttrText, LONG_ATTRIBUTE_COLLAPSED);
+ }
+}, {
+ desc: "Try to add long data URL to make sure it is collapsed in attribute " +
+ "editor.",
+ text: `src="${DATA_URL_ATTRIBUTE}"`,
+ expectedAttributes: {
+ "src": DATA_URL_ATTRIBUTE
+ },
+ validate: (container, inspector) => {
+ let editor = container.editor;
+ let visibleAttrText = editor.attrElements.get("src")
+ .querySelector(".attr-value").textContent;
+ is(visibleAttrText, DATA_URL_ATTRIBUTE_COLLAPSED);
+ }
+}, {
+ desc: "Try to add long attribute with collapseAttributes == false" +
+ "to make sure it isn't collapsed in attribute editor.",
+ text: `data-long="${LONG_ATTRIBUTE}"`,
+ expectedAttributes: {
+ "data-long": LONG_ATTRIBUTE
+ },
+ setUp: function (inspector) {
+ Services.prefs.setBoolPref("devtools.markup.collapseAttributes", false);
+ },
+ validate: (container, inspector) => {
+ let editor = container.editor;
+ let visibleAttrText = editor.attrElements
+ .get("data-long")
+ .querySelector(".attr-value")
+ .textContent;
+ is(visibleAttrText, LONG_ATTRIBUTE);
+ },
+ tearDown: function (inspector) {
+ Services.prefs.clearUserPref("devtools.markup.collapseAttributes");
+ }
+}, {
+ desc: "Try to collapse attributes with collapseAttributeLength == 5",
+ text: `data-long="${LONG_ATTRIBUTE}"`,
+ expectedAttributes: {
+ "data-long": LONG_ATTRIBUTE
+ },
+ setUp: function (inspector) {
+ Services.prefs.setIntPref("devtools.markup.collapseAttributeLength", 2);
+ },
+ validate: (container, inspector) => {
+ let firstChar = LONG_ATTRIBUTE[0];
+ let lastChar = LONG_ATTRIBUTE[LONG_ATTRIBUTE.length - 1];
+ let collapsed = firstChar + "\u2026" + lastChar;
+ let editor = container.editor;
+ let visibleAttrText = editor.attrElements
+ .get("data-long")
+ .querySelector(".attr-value")
+ .textContent;
+ is(visibleAttrText, collapsed);
+ },
+ tearDown: function (inspector) {
+ Services.prefs.clearUserPref("devtools.markup.collapseAttributeLength");
+ }
+}];
+
+add_task(function* () {
+ let {inspector, testActor} = yield openInspectorForURL(TEST_URL);
+ yield runAddAttributesTests(TEST_DATA, "div", inspector, testActor);
+});
+
diff --git a/devtools/client/inspector/markup/test/browser_markup_tag_edit_08.js b/devtools/client/inspector/markup/test/browser_markup_tag_edit_08.js
new file mode 100644
index 000000000..238e59c52
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_tag_edit_08.js
@@ -0,0 +1,132 @@
+/* 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";
+
+// Test editing various markup-containers' attribute fields, in particular
+// attributes with long values and quotes
+
+const TEST_URL = URL_ROOT + "doc_markup_edit.html";
+/*eslint-disable */
+const LONG_ATTRIBUTE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ-ABCDEFGHIJKLMNOPQRSTUVWXYZ-ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZ-ABCDEFGHIJKLMNOPQRSTUVWXYZ-ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+const LONG_ATTRIBUTE_COLLAPSED = "ABCDEFGHIJKLMNOPQRSTUVWXYZ-ABCDEFGHIJKLMNOPQRSTUVWXYZ-ABCDEF\u2026UVWXYZ-ABCDEFGHIJKLMNOPQRSTUVWXYZ-ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+/*eslint-enable */
+
+add_task(function* () {
+ let {inspector, testActor} = yield openInspectorForURL(TEST_URL);
+
+ yield inspector.markup.expandAll();
+ yield testCollapsedLongAttribute(inspector, testActor);
+ yield testModifyInlineStyleWithQuotes(inspector, testActor);
+ yield testEditingAttributeWithMixedQuotes(inspector, testActor);
+});
+
+function* testCollapsedLongAttribute(inspector, testActor) {
+ info("Try to modify the collapsed long attribute, making sure it expands.");
+
+ info("Adding test attributes to the node");
+ let onMutated = inspector.once("markupmutation");
+ yield testActor.setAttribute("#node24", "class", "");
+ yield testActor.setAttribute("#node24", "data-long", LONG_ATTRIBUTE);
+ yield onMutated;
+
+ yield assertAttributes("#node24", {
+ id: "node24",
+ "class": "",
+ "data-long": LONG_ATTRIBUTE
+ }, testActor);
+
+ let {editor} = yield focusNode("#node24", inspector);
+ let attr = editor.attrElements.get("data-long").querySelector(".editable");
+
+ // Check to make sure it has expanded after focus
+ attr.focus();
+ EventUtils.sendKey("return", inspector.panelWin);
+ let input = inplaceEditor(attr).input;
+ is(input.value, `data-long="${LONG_ATTRIBUTE}"`);
+ EventUtils.sendKey("escape", inspector.panelWin);
+
+ setEditableFieldValue(attr, input.value + ' data-short="ABC"', inspector);
+ yield inspector.once("markupmutation");
+
+ let visibleAttrText = editor.attrElements.get("data-long")
+ .querySelector(".attr-value").textContent;
+ is(visibleAttrText, LONG_ATTRIBUTE_COLLAPSED);
+
+ yield assertAttributes("#node24", {
+ id: "node24",
+ class: "",
+ "data-long": LONG_ATTRIBUTE,
+ "data-short": "ABC"
+ }, testActor);
+}
+
+function* testModifyInlineStyleWithQuotes(inspector, testActor) {
+ info("Modify inline style containing \"");
+
+ yield assertAttributes("#node26", {
+ id: "node26",
+ style: 'background-image: url("moz-page-thumb://thumbnail?url=http%3A%2F%2Fwww.mozilla.org%2F");'
+ }, testActor);
+
+ let onMutated = inspector.once("markupmutation");
+ let {editor} = yield focusNode("#node26", inspector);
+ let attr = editor.attrElements.get("style").querySelector(".editable");
+
+ attr.focus();
+ EventUtils.sendKey("return", inspector.panelWin);
+
+ let input = inplaceEditor(attr).input;
+ let value = input.value;
+
+ is(value,
+ "style='background-image: url(\"moz-page-thumb://thumbnail?url=http%3A%2F%2Fwww.mozilla.org%2F\");'",
+ "Value contains actual double quotes"
+ );
+
+ value = value.replace(/mozilla\.org/, "mozilla.com");
+ input.value = value;
+
+ EventUtils.sendKey("return", inspector.panelWin);
+
+ yield onMutated;
+
+ yield assertAttributes("#node26", {
+ id: "node26",
+ style: 'background-image: url("moz-page-thumb://thumbnail?url=http%3A%2F%2Fwww.mozilla.com%2F");'
+ }, testActor);
+}
+
+function* testEditingAttributeWithMixedQuotes(inspector, testActor) {
+ info("Modify class containing \" and \'");
+
+ yield assertAttributes("#node27", {
+ "id": "node27",
+ "class": 'Double " and single \''
+ }, testActor);
+
+ let onMutated = inspector.once("markupmutation");
+ let {editor} = yield focusNode("#node27", inspector);
+ let attr = editor.attrElements.get("class").querySelector(".editable");
+
+ attr.focus();
+ EventUtils.sendKey("return", inspector.panelWin);
+
+ let input = inplaceEditor(attr).input;
+ let value = input.value;
+
+ is(value, "class=\"Double &quot; and single '\"", "Value contains &quot;");
+
+ value = value.replace(/Double/, "&quot;").replace(/single/, "'");
+ input.value = value;
+
+ EventUtils.sendKey("return", inspector.panelWin);
+
+ yield onMutated;
+
+ yield assertAttributes("#node27", {
+ id: "node27",
+ class: '" " and \' \''
+ }, testActor);
+}
diff --git a/devtools/client/inspector/markup/test/browser_markup_tag_edit_09.js b/devtools/client/inspector/markup/test/browser_markup_tag_edit_09.js
new file mode 100644
index 000000000..8680ab9f5
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_tag_edit_09.js
@@ -0,0 +1,71 @@
+/* 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";
+
+// Test that editing a mixed-case attribute preserves the case
+
+const TEST_URL = URL_ROOT + "doc_markup_svg_attributes.html";
+
+add_task(function* () {
+ let {inspector, testActor} = yield openInspectorForURL(TEST_URL);
+
+ yield inspector.markup.expandAll();
+ yield selectNode("svg", inspector);
+
+ yield testWellformedMixedCase(inspector, testActor);
+ yield testMalformedMixedCase(inspector, testActor);
+});
+
+function* testWellformedMixedCase(inspector, testActor) {
+ info("Modifying a mixed-case attribute, " +
+ "expecting the attribute's case to be preserved");
+
+ info("Listening to markup mutations");
+ let onMutated = inspector.once("markupmutation");
+
+ info("Focusing the viewBox attribute editor");
+ let {editor} = yield focusNode("svg", inspector);
+ let attr = editor.attrElements.get("viewBox").querySelector(".editable");
+ attr.focus();
+ EventUtils.sendKey("return", inspector.panelWin);
+
+ info("Editing the attribute value and waiting for the mutation event");
+ let input = inplaceEditor(attr).input;
+ input.value = "viewBox=\"0 0 1 1\"";
+ EventUtils.sendKey("return", inspector.panelWin);
+ yield onMutated;
+
+ yield assertAttributes("svg", {
+ "viewBox": "0 0 1 1",
+ "width": "200",
+ "height": "200"
+ }, testActor);
+}
+
+function* testMalformedMixedCase(inspector, testActor) {
+ info("Modifying a malformed, mixed-case attribute, " +
+ "expecting the attribute's case to be preserved");
+
+ info("Listening to markup mutations");
+ let onMutated = inspector.once("markupmutation");
+
+ info("Focusing the viewBox attribute editor");
+ let {editor} = yield focusNode("svg", inspector);
+ let attr = editor.attrElements.get("viewBox").querySelector(".editable");
+ attr.focus();
+ EventUtils.sendKey("return", inspector.panelWin);
+
+ info("Editing the attribute value and waiting for the mutation event");
+ let input = inplaceEditor(attr).input;
+ input.value = "viewBox=\"<>\"";
+ EventUtils.sendKey("return", inspector.panelWin);
+ yield onMutated;
+
+ yield assertAttributes("svg", {
+ "viewBox": "<>",
+ "width": "200",
+ "height": "200"
+ }, testActor);
+}
diff --git a/devtools/client/inspector/markup/test/browser_markup_tag_edit_10.js b/devtools/client/inspector/markup/test/browser_markup_tag_edit_10.js
new file mode 100644
index 000000000..8d27c5468
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_tag_edit_10.js
@@ -0,0 +1,34 @@
+/* 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";
+
+// Tests that invalid tagname updates are handled correctly
+
+const TEST_URL = "data:text/html;charset=utf-8,<div></div>";
+
+add_task(function* () {
+ let {inspector} = yield openInspectorForURL(TEST_URL);
+ yield inspector.markup.expandAll();
+
+ info("Updating the DIV tagname to an invalid value");
+ let container = yield focusNode("div", inspector);
+ let onCancelReselect = inspector.markup.once("canceledreselectonremoved");
+ let tagEditor = container.editor.tag;
+ setEditableFieldValue(tagEditor, "<<<", inspector);
+ yield onCancelReselect;
+ ok(true, "The markup-view emitted the canceledreselectonremoved event");
+ is(inspector.selection.nodeFront, container.node,
+ "The test DIV is still selected");
+
+ info("Updating the DIV tagname to a valid value this time");
+ let onReselect = inspector.markup.once("reselectedonremoved");
+ setEditableFieldValue(tagEditor, "span", inspector);
+ yield onReselect;
+ ok(true, "The markup-view emitted the reselectedonremoved event");
+
+ let spanFront = yield getNodeFront("span", inspector);
+ is(inspector.selection.nodeFront, spanFront,
+ "The selected node is now the SPAN");
+});
diff --git a/devtools/client/inspector/markup/test/browser_markup_tag_edit_11.js b/devtools/client/inspector/markup/test/browser_markup_tag_edit_11.js
new file mode 100644
index 000000000..906c4aced
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_tag_edit_11.js
@@ -0,0 +1,38 @@
+/* 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";
+
+// Bug 1090874 - Tests that a node is not recreated when it's tagname editor
+// is blurred and no changes were done.
+
+const TEST_URL = "data:text/html;charset=utf-8,<div></div>";
+
+add_task(function* () {
+ let isEditTagNameCalled = false;
+
+ let {inspector} = yield openInspectorForURL(TEST_URL);
+
+ // Overriding the editTagName walkerActor method here to check that it isn't
+ // called when blurring the tagname field.
+ inspector.walker.editTagName = function () {
+ isEditTagNameCalled = true;
+ };
+
+ let container = yield focusNode("div", inspector);
+ let tagEditor = container.editor.tag;
+
+ info("Blurring the tagname field");
+ tagEditor.blur();
+ is(isEditTagNameCalled, false, "The editTagName method wasn't called");
+
+ info("Updating the tagname to uppercase");
+ yield focusNode("div", inspector);
+ setEditableFieldValue(tagEditor, "DIV", inspector);
+ is(isEditTagNameCalled, false, "The editTagName method wasn't called");
+
+ info("Updating the tagname to a different value");
+ setEditableFieldValue(tagEditor, "SPAN", inspector);
+ is(isEditTagNameCalled, true, "The editTagName method was called");
+});
diff --git a/devtools/client/inspector/markup/test/browser_markup_tag_edit_12.js b/devtools/client/inspector/markup/test/browser_markup_tag_edit_12.js
new file mode 100644
index 000000000..4fcf3dd66
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_tag_edit_12.js
@@ -0,0 +1,98 @@
+/* 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";
+
+// Tests that focus position is correct when tabbing through and editing
+// attributes.
+
+const TEST_URL = "data:text/html;charset=utf8," +
+ "<div id='attr' a='1' b='2' c='3'></div>" +
+ "<div id='delattr' tobeinvalid='1' last='2'></div>";
+
+add_task(function* () {
+ let {inspector} = yield openInspectorForURL(TEST_URL);
+
+ yield testAttributeEditing(inspector);
+ yield testAttributeDeletion(inspector);
+});
+
+function* testAttributeEditing(inspector) {
+ info("Testing focus position after attribute editing");
+
+ info("Setting the first non-id attribute in edit mode");
+ // focuses id
+ yield activateFirstAttribute("#attr", inspector);
+ // focuses the first attr after id
+ collapseSelectionAndTab(inspector);
+
+ let attrs = yield getAttributesFromEditor("#attr", inspector);
+
+ info("Editing this attribute, keeping the same name, " +
+ "and tabbing to the next");
+ yield editAttributeAndTab(attrs[1] + '="99"', inspector);
+ checkFocusedAttribute(attrs[2], true);
+
+ info("Editing the new focused attribute, keeping the name, " +
+ "and tabbing to the previous");
+ yield editAttributeAndTab(attrs[2] + '="99"', inspector, true);
+ checkFocusedAttribute(attrs[1], true);
+
+ info("Editing attribute name, changes attribute order");
+ yield editAttributeAndTab("d='4'", inspector);
+ checkFocusedAttribute("id", true);
+
+ // Escape of the currently focused field for the next test
+ EventUtils.sendKey("escape", inspector.panelWin);
+}
+
+function* testAttributeDeletion(inspector) {
+ info("Testing focus position after attribute deletion");
+
+ info("Setting the first non-id attribute in edit mode");
+ // focuses id
+ yield activateFirstAttribute("#delattr", inspector);
+ // focuses the first attr after id
+ collapseSelectionAndTab(inspector);
+
+ let attrs = yield getAttributesFromEditor("#delattr", inspector);
+
+ info("Entering an invalid attribute to delete the attribute");
+ yield editAttributeAndTab('"', inspector);
+ checkFocusedAttribute(attrs[2], true);
+
+ info("Deleting the last attribute");
+ yield editAttributeAndTab(" ", inspector);
+
+ // Check we're on the newattr element
+ let focusedAttr = Services.focus.focusedElement;
+ ok(focusedAttr.classList.contains("styleinspector-propertyeditor"),
+ "in newattr");
+ is(focusedAttr.tagName, "textarea", "newattr is active");
+}
+
+function* editAttributeAndTab(newValue, inspector, goPrevious) {
+ let onEditMutation = inspector.markup.once("refocusedonedit");
+ inspector.markup.doc.activeElement.value = newValue;
+ if (goPrevious) {
+ EventUtils.synthesizeKey("VK_TAB", { shiftKey: true },
+ inspector.panelWin);
+ } else {
+ EventUtils.sendKey("tab", inspector.panelWin);
+ }
+ yield onEditMutation;
+}
+
+/**
+ * Given a markup container, focus and turn in edit mode its first attribute
+ * field.
+ */
+function* activateFirstAttribute(container, inspector) {
+ let {editor} = yield focusNode(container, inspector);
+ editor.tag.focus();
+
+ // Go to "id" attribute and trigger edit mode.
+ EventUtils.sendKey("tab", inspector.panelWin);
+ EventUtils.sendKey("return", inspector.panelWin);
+}
diff --git a/devtools/client/inspector/markup/test/browser_markup_tag_edit_13-other.js b/devtools/client/inspector/markup/test/browser_markup_tag_edit_13-other.js
new file mode 100644
index 000000000..188e12cbc
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_tag_edit_13-other.js
@@ -0,0 +1,38 @@
+/* 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";
+
+// Tests that doesn't fit into any specific category.
+
+const TEST_URL = `data:text/html;charset=utf8,
+ <div a b id='order' c class></div>`;
+
+add_task(function* () {
+ let {inspector, testActor} = yield openInspectorForURL(TEST_URL);
+
+ yield testOriginalAttributesOrder(inspector);
+ yield testOrderAfterAttributeChange(inspector, testActor);
+});
+
+function* testOriginalAttributesOrder(inspector) {
+ info("Testing order of attributes on initial node render");
+
+ let attributes = yield getAttributesFromEditor("#order", inspector);
+ ok(isEqual(attributes, ["id", "class", "a", "b", "c"]), "ordered correctly");
+}
+
+function* testOrderAfterAttributeChange(inspector, testActor) {
+ info("Testing order of attributes after attribute is change by setAttribute");
+
+ yield testActor.setAttribute("#order", "a", "changed");
+
+ let attributes = yield getAttributesFromEditor("#order", inspector);
+ ok(isEqual(attributes, ["id", "class", "a", "b", "c"]),
+ "order isn't changed");
+}
+
+function isEqual(a, b) {
+ return a.toString() === b.toString();
+}
diff --git a/devtools/client/inspector/markup/test/browser_markup_tag_edit_long-classname.js b/devtools/client/inspector/markup/test/browser_markup_tag_edit_long-classname.js
new file mode 100644
index 000000000..7615ed691
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_tag_edit_long-classname.js
@@ -0,0 +1,41 @@
+/* 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";
+
+// Test that editing long classnames shows the whole class attribute without scrollbars.
+
+const classname = "this-long-class-attribute-should-be-displayed " +
+ "without-overflow-when-switching-to-edit-mode " +
+ "AAAAAAAAAAAA-BBBBBBBBBBBBB-CCCCCCCCCCCCC-DDDDDDDDDDDDDD-EEEEEEEEEEEEE";
+const TEST_URL = `data:text/html;charset=utf8, <div class="${classname}"></div>`;
+
+add_task(function* () {
+ let {inspector} = yield openInspectorForURL(TEST_URL);
+
+ yield selectNode("div", inspector);
+ yield clickContainer("div", inspector);
+
+ let container = yield focusNode("div", inspector);
+ ok(container && container.editor, "The markup-container was found");
+
+ info("Listening for the markupmutation event");
+ let nodeMutated = inspector.once("markupmutation");
+ let attr = container.editor.attrElements.get("class").querySelector(".editable");
+
+ attr.focus();
+ EventUtils.sendKey("return", inspector.panelWin);
+ let input = inplaceEditor(attr).input;
+ ok(input, "Found editable field for class attribute");
+
+ is(input.scrollHeight, input.clientHeight, "input should not have vertical scrollbars");
+ is(input.scrollWidth, input.clientWidth, "input should not have horizontal scrollbars");
+ input.value = "class=\"other value\"";
+
+ info("Commit the new class value");
+ EventUtils.sendKey("return", inspector.panelWin);
+
+ info("Wait for the markup-mutation event");
+ yield nodeMutated;
+});
diff --git a/devtools/client/inspector/markup/test/browser_markup_textcontent_display.js b/devtools/client/inspector/markup/test/browser_markup_textcontent_display.js
new file mode 100644
index 000000000..7513e4e18
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_textcontent_display.js
@@ -0,0 +1,89 @@
+/* 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";
+
+// Test the rendering of text nodes in the markup view.
+
+const LONG_VALUE = "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do " +
+ "eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam.";
+const SCHEMA = "data:text/html;charset=UTF-8,";
+const TEST_URL = `${SCHEMA}<!DOCTYPE html>
+ <html>
+ <body>
+ <div id="shorttext">Short text</div>
+ <div id="longtext">${LONG_VALUE}</div>
+ <div id="shortcomment"><!--Short comment--></div>
+ <div id="longcomment"><!--${LONG_VALUE}--></div>
+ <div id="shorttext-and-node">Short text<span>Other element</span></div>
+ <div id="longtext-and-node">${LONG_VALUE}<span>Other element</span></div>
+ </body>
+ </html>`;
+
+const TEST_DATA = [{
+ desc: "Test node containing a short text, short text nodes can be inlined.",
+ selector: "#shorttext",
+ inline: true,
+ value: "Short text",
+}, {
+ desc: "Test node containing a long text, long text nodes are not inlined.",
+ selector: "#longtext",
+ inline: false,
+ value: LONG_VALUE,
+}, {
+ desc: "Test node containing a short comment, comments are not inlined.",
+ selector: "#shortcomment",
+ inline: false,
+ value: "Short comment",
+}, {
+ desc: "Test node containing a long comment, comments are not inlined.",
+ selector: "#longcomment",
+ inline: false,
+ value: LONG_VALUE,
+}, {
+ desc: "Test node containing a short text and a span.",
+ selector: "#shorttext-and-node",
+ inline: false,
+ value: "Short text",
+}, {
+ desc: "Test node containing a long text and a span.",
+ selector: "#longtext-and-node",
+ inline: false,
+ value: LONG_VALUE,
+}];
+
+add_task(function* () {
+ let {inspector, testActor} = yield openInspectorForURL(TEST_URL);
+
+ for (let data of TEST_DATA) {
+ yield checkNode(inspector, testActor, data);
+ }
+});
+
+function* checkNode(inspector, testActor, {desc, selector, inline, value}) {
+ info(desc);
+
+ let container = yield getContainerForSelector(selector, inspector);
+ let nodeValue = yield getFirstChildNodeValue(selector, testActor);
+ is(nodeValue, value, "The test node's text content is correct");
+
+ is(!!container.inlineTextChild, inline, "Container inlineTextChild is as expected");
+ is(!container.canExpand, inline, "Container canExpand property is as expected");
+
+ let textContainer;
+ if (inline) {
+ textContainer = container.elt.querySelector("pre");
+ ok(!!textContainer, "Text container is already rendered for inline text elements");
+ } else {
+ textContainer = container.elt.querySelector("pre");
+ ok(!textContainer, "Text container is not rendered for collapsed text nodes");
+ yield inspector.markup.expandNode(container.node);
+ yield waitForMultipleChildrenUpdates(inspector);
+
+ textContainer = container.elt.querySelector("pre");
+ ok(!!textContainer, "Text container is rendered after expanding the container");
+ }
+
+ is(textContainer.textContent, value, "The complete text node is rendered.");
+}
diff --git a/devtools/client/inspector/markup/test/browser_markup_textcontent_edit_01.js b/devtools/client/inspector/markup/test/browser_markup_textcontent_edit_01.js
new file mode 100644
index 000000000..f27b56647
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_textcontent_edit_01.js
@@ -0,0 +1,84 @@
+/* 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";
+
+// Test editing a node's text content
+
+const TEST_URL = URL_ROOT + "doc_markup_edit.html";
+const {DEFAULT_VALUE_SUMMARY_LENGTH} = require("devtools/server/actors/inspector");
+
+add_task(function* () {
+ let {inspector, testActor} = yield openInspectorForURL(TEST_URL);
+
+ info("Expanding all nodes");
+ yield inspector.markup.expandAll();
+ yield waitForMultipleChildrenUpdates(inspector);
+
+ yield editContainer(inspector, testActor, {
+ selector: ".node6",
+ newValue: "New text",
+ oldValue: "line6"
+ });
+
+ yield editContainer(inspector, testActor, {
+ selector: "#node17",
+ newValue: "LOREM IPSUM DOLOR SIT AMET, CONSECTETUR ADIPISCING ELIT. " +
+ "DONEC POSUERE PLACERAT MAGNA ET IMPERDIET.",
+ oldValue: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " +
+ "Donec posuere placerat magna et imperdiet."
+ });
+
+ yield editContainer(inspector, testActor, {
+ selector: "#node17",
+ newValue: "New value",
+ oldValue: "LOREM IPSUM DOLOR SIT AMET, CONSECTETUR ADIPISCING ELIT. " +
+ "DONEC POSUERE PLACERAT MAGNA ET IMPERDIET."
+ });
+
+ yield editContainer(inspector, testActor, {
+ selector: "#node17",
+ newValue: "LOREM IPSUM DOLOR SIT AMET, CONSECTETUR ADIPISCING ELIT. " +
+ "DONEC POSUERE PLACERAT MAGNA ET IMPERDIET.",
+ oldValue: "New value"
+ });
+});
+
+function* editContainer(inspector, testActor,
+ {selector, newValue, oldValue}) {
+ let nodeValue = yield getFirstChildNodeValue(selector, testActor);
+ is(nodeValue, oldValue, "The test node's text content is correct");
+
+ info("Changing the text content");
+ let onMutated = inspector.once("markupmutation");
+ let container = yield focusNode(selector, inspector);
+
+ let isOldValueInline = oldValue.length <= DEFAULT_VALUE_SUMMARY_LENGTH;
+ is(!!container.inlineTextChild, isOldValueInline, "inlineTextChild is as expected");
+ is(!container.canExpand, isOldValueInline, "canExpand property is as expected");
+
+ let field = container.elt.querySelector("pre");
+ is(field.textContent, oldValue,
+ "The text node has the correct original value after selecting");
+ setEditableFieldValue(field, newValue, inspector);
+
+ info("Listening to the markupmutation event");
+ yield onMutated;
+
+ nodeValue = yield getFirstChildNodeValue(selector, testActor);
+ is(nodeValue, newValue, "The test node's text content has changed");
+
+ let isNewValueInline = newValue.length <= DEFAULT_VALUE_SUMMARY_LENGTH;
+ is(!!container.inlineTextChild, isNewValueInline, "inlineTextChild is as expected");
+ is(!container.canExpand, isNewValueInline, "canExpand property is as expected");
+
+ if (isOldValueInline != isNewValueInline) {
+ is(container.expanded, !isNewValueInline,
+ "Container was automatically expanded/collapsed");
+ }
+
+ info("Selecting the <body> to reset the selection");
+ let bodyContainer = yield getContainerForSelector("body", inspector);
+ inspector.markup.markNodeAsSelected(bodyContainer.node);
+}
diff --git a/devtools/client/inspector/markup/test/browser_markup_textcontent_edit_02.js b/devtools/client/inspector/markup/test/browser_markup_textcontent_edit_02.js
new file mode 100644
index 000000000..04825f2d4
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_textcontent_edit_02.js
@@ -0,0 +1,116 @@
+/* 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";
+
+// Test that using UP/DOWN next to a number when editing a text node does not
+// increment or decrement but simply navigates inside the editable field.
+
+const TEST_URL = URL_ROOT + "doc_markup_edit.html";
+const SELECTOR = ".node6";
+
+add_task(function* () {
+ let {inspector, testActor} = yield openInspectorForURL(TEST_URL);
+
+ info("Expanding all nodes");
+ yield inspector.markup.expandAll();
+ yield waitForMultipleChildrenUpdates(inspector);
+
+ let nodeValue = yield getFirstChildNodeValue(SELECTOR, testActor);
+ let expectedValue = "line6";
+ is(nodeValue, expectedValue, "The test node's text content is correct");
+
+ info("Open editable field for .node6");
+ let container = yield focusNode(SELECTOR, inspector);
+ let field = container.elt.querySelector("pre");
+ field.focus();
+ EventUtils.sendKey("return", inspector.panelWin);
+ let editor = inplaceEditor(field);
+
+ info("Initially, all the input content should be selected");
+ checkSelectionPositions(editor, 0, expectedValue.length);
+
+ info("Navigate using 'RIGHT': move the caret to the end");
+ yield sendKey("VK_RIGHT", {}, editor, inspector.panelWin);
+ is(editor.input.value, expectedValue, "Value should not have changed");
+ checkSelectionPositions(editor, expectedValue.length, expectedValue.length);
+
+ info("Navigate using 'DOWN': no effect, already at the end");
+ yield sendKey("VK_DOWN", {}, editor, inspector.panelWin);
+ is(editor.input.value, expectedValue, "Value should not have changed");
+ checkSelectionPositions(editor, expectedValue.length, expectedValue.length);
+
+ info("Navigate using 'UP': move to the start");
+ yield sendKey("VK_UP", {}, editor, inspector.panelWin);
+ is(editor.input.value, expectedValue, "Value should not have changed");
+ checkSelectionPositions(editor, 0, 0);
+
+ info("Navigate using 'DOWN': move to the end");
+ yield sendKey("VK_DOWN", {}, editor, inspector.panelWin);
+ is(editor.input.value, expectedValue, "Value should not have changed");
+ checkSelectionPositions(editor, expectedValue.length, expectedValue.length);
+
+ info("Type 'b' in the editable field");
+ yield sendKey("b", {}, editor, inspector.panelWin);
+ expectedValue += "b";
+ is(editor.input.value, expectedValue, "Value should be updated");
+
+ info("Type 'a' in the editable field");
+ yield sendKey("a", {}, editor, inspector.panelWin);
+ expectedValue += "a";
+ is(editor.input.value, expectedValue, "Value should be updated");
+
+ info("Create a new line using shift+RETURN");
+ yield sendKey("VK_RETURN", {shiftKey: true}, editor, inspector.panelWin);
+ expectedValue += "\n";
+ is(editor.input.value, expectedValue, "Value should have a new line");
+ checkSelectionPositions(editor, expectedValue.length, expectedValue.length);
+
+ info("Type '1' in the editable field");
+ yield sendKey("1", {}, editor, inspector.panelWin);
+ expectedValue += "1";
+ is(editor.input.value, expectedValue, "Value should be updated");
+ checkSelectionPositions(editor, expectedValue.length, expectedValue.length);
+
+ info("Navigate using 'UP': move back to the first line");
+ yield sendKey("VK_UP", {}, editor, inspector.panelWin);
+ is(editor.input.value, expectedValue, "Value should not have changed");
+ info("Caret should be back on the first line");
+ checkSelectionPositions(editor, 1, 1);
+
+ info("Commit the new value with RETURN, wait for the markupmutation event");
+ let onMutated = inspector.once("markupmutation");
+ yield sendKey("VK_RETURN", {}, editor, inspector.panelWin);
+ yield onMutated;
+
+ nodeValue = yield getFirstChildNodeValue(SELECTOR, testActor);
+ is(nodeValue, expectedValue, "The test node's text content is correct");
+});
+
+/**
+ * Check that the editor selection is at the expected positions.
+ */
+function checkSelectionPositions(editor, expectedStart, expectedEnd) {
+ is(editor.input.selectionStart, expectedStart,
+ "Selection should start at " + expectedStart);
+ is(editor.input.selectionEnd, expectedEnd,
+ "Selection should end at " + expectedEnd);
+}
+
+/**
+ * Send a key and expect to receive a keypress event on the editor's input.
+ */
+function sendKey(key, options, editor, win) {
+ return new Promise(resolve => {
+ info("Adding event listener for down|left|right|back_space|return keys");
+ editor.input.addEventListener("keypress", function onKeypress() {
+ if (editor.input) {
+ editor.input.removeEventListener("keypress", onKeypress);
+ }
+ executeSoon(resolve);
+ });
+
+ EventUtils.synthesizeKey(key, options, win);
+ });
+}
diff --git a/devtools/client/inspector/markup/test/browser_markup_toggle_01.js b/devtools/client/inspector/markup/test/browser_markup_toggle_01.js
new file mode 100644
index 000000000..a481f685e
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_toggle_01.js
@@ -0,0 +1,58 @@
+/* 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";
+
+// Test toggling (expand/collapse) elements by clicking on twisties
+
+const TEST_URL = URL_ROOT + "doc_markup_toggle.html";
+
+add_task(function* () {
+ let {inspector, testActor} = yield openInspectorForURL(TEST_URL);
+
+ info("Getting the container for the html element");
+ let container = yield getContainerForSelector("html", inspector);
+ ok(container.mustExpand, "HTML element mustExpand");
+ ok(container.canExpand, "HTML element canExpand");
+ is(container.expander.style.visibility, "hidden", "HTML twisty is hidden");
+
+ info("Getting the container for the UL parent element");
+ container = yield getContainerForSelector("ul", inspector);
+ ok(!container.mustExpand, "UL element !mustExpand");
+ ok(container.canExpand, "UL element canExpand");
+ is(container.expander.style.visibility, "visible", "HTML twisty is visible");
+
+ info("Clicking on the UL parent expander, and waiting for children");
+ let onChildren = waitForChildrenUpdated(inspector);
+ let onUpdated = inspector.once("inspector-updated");
+ EventUtils.synthesizeMouseAtCenter(container.expander, {},
+ inspector.markup.doc.defaultView);
+ yield onChildren;
+ yield onUpdated;
+
+ info("Checking that child LI elements have been created");
+ let numLi = yield testActor.getNumberOfElementMatches("li");
+ for (let i = 0; i < numLi; i++) {
+ let liContainer = yield getContainerForSelector(
+ `li:nth-child(${i + 1})`, inspector);
+ ok(liContainer, "A container for the child LI element was created");
+ }
+ ok(container.expanded, "Parent UL container is expanded");
+
+ info("Clicking again on the UL expander");
+ // No need to wait, this is a local, synchronous operation where nodes are
+ // only hidden from the view, not destroyed
+ EventUtils.synthesizeMouseAtCenter(container.expander, {},
+ inspector.markup.doc.defaultView);
+
+ info("Checking that child LI elements have been hidden");
+ numLi = yield testActor.getNumberOfElementMatches("li");
+ for (let i = 0; i < numLi; i++) {
+ let liContainer = yield getContainerForSelector(
+ `li:nth-child(${i + 1})`, inspector);
+ is(liContainer.elt.getClientRects().length, 0,
+ "The container for the child LI element was hidden");
+ }
+ ok(!container.expanded, "Parent UL container is collapsed");
+});
diff --git a/devtools/client/inspector/markup/test/browser_markup_toggle_02.js b/devtools/client/inspector/markup/test/browser_markup_toggle_02.js
new file mode 100644
index 000000000..481f0bf58
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_toggle_02.js
@@ -0,0 +1,49 @@
+/* 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";
+
+// Test toggling (expand/collapse) elements by dbl-clicking on tag lines
+
+const TEST_URL = URL_ROOT + "doc_markup_toggle.html";
+
+add_task(function* () {
+ let {inspector, testActor} = yield openInspectorForURL(TEST_URL);
+
+ info("Getting the container for the UL parent element");
+ let container = yield getContainerForSelector("ul", inspector);
+
+ info("Dbl-clicking on the UL parent expander, and waiting for children");
+ let onChildren = waitForChildrenUpdated(inspector);
+ let onUpdated = inspector.once("inspector-updated");
+ EventUtils.synthesizeMouseAtCenter(container.tagLine, {clickCount: 2},
+ inspector.markup.doc.defaultView);
+ yield onChildren;
+ yield onUpdated;
+
+ info("Checking that child LI elements have been created");
+ let numLi = yield testActor.getNumberOfElementMatches("li");
+ for (let i = 0; i < numLi; i++) {
+ let liContainer = yield getContainerForSelector(
+ "li:nth-child(" + (i + 1) + ")", inspector);
+ ok(liContainer, "A container for the child LI element was created");
+ }
+ ok(container.expanded, "Parent UL container is expanded");
+
+ info("Dbl-clicking again on the UL expander");
+ // No need to wait, this is a local, synchronous operation where nodes are
+ // only hidden from the view, not destroyed
+ EventUtils.synthesizeMouseAtCenter(container.tagLine, {clickCount: 2},
+ inspector.markup.doc.defaultView);
+
+ info("Checking that child LI elements have been hidden");
+ numLi = yield testActor.getNumberOfElementMatches("li");
+ for (let i = 0; i < numLi; i++) {
+ let liContainer = yield getContainerForSelector(
+ "li:nth-child(" + (i + 1) + ")", inspector);
+ is(liContainer.elt.getClientRects().length, 0,
+ "The container for the child LI element was hidden");
+ }
+ ok(!container.expanded, "Parent UL container is collapsed");
+});
diff --git a/devtools/client/inspector/markup/test/browser_markup_toggle_03.js b/devtools/client/inspector/markup/test/browser_markup_toggle_03.js
new file mode 100644
index 000000000..fb3529c8e
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_toggle_03.js
@@ -0,0 +1,35 @@
+/* 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";
+
+// Test toggling (expand/collapse) elements by alt-clicking on twisties, which
+// should expand all the descendants
+
+const TEST_URL = URL_ROOT + "doc_markup_toggle.html";
+
+add_task(function* () {
+ let {inspector} = yield openInspectorForURL(TEST_URL);
+
+ info("Getting the container for the UL parent element");
+ let container = yield getContainerForSelector("ul", inspector);
+
+ info("Alt-clicking on the UL parent expander, and waiting for children");
+ let onUpdated = inspector.once("inspector-updated");
+ EventUtils.synthesizeMouseAtCenter(container.expander, {altKey: true},
+ inspector.markup.doc.defaultView);
+ yield onUpdated;
+ yield waitForMultipleChildrenUpdates(inspector);
+
+ info("Checking that all nodes exist and are expanded");
+ let nodeList = yield inspector.walker.querySelectorAll(
+ inspector.walker.rootNode, "ul, li, span, em");
+ let nodeFronts = yield nodeList.items();
+ for (let nodeFront of nodeFronts) {
+ let nodeContainer = getContainerForNodeFront(nodeFront, inspector);
+ ok(nodeContainer, "Container for node " + nodeFront.tagName + " exists");
+ ok(nodeContainer.expanded,
+ "Container for node " + nodeFront.tagName + " is expanded");
+ }
+});
diff --git a/devtools/client/inspector/markup/test/browser_markup_update-on-navigtion.js b/devtools/client/inspector/markup/test/browser_markup_update-on-navigtion.js
new file mode 100644
index 000000000..241cea672
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_update-on-navigtion.js
@@ -0,0 +1,44 @@
+/* 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";
+
+// Test that markup view handles page navigation correctly.
+
+const SCHEMA = "data:text/html;charset=UTF-8,";
+const URL_1 = SCHEMA + "<div id='one' style='color:red;'>ONE</div>";
+const URL_2 = SCHEMA + "<div id='two' style='color:green;'>TWO</div>";
+
+add_task(function* () {
+ let {inspector, testActor} = yield openInspectorForURL(URL_1);
+
+ assertMarkupViewIsLoaded();
+ yield selectNode("#one", inspector);
+
+ let willNavigate = inspector.target.once("will-navigate");
+ yield testActor.eval(`content.location = "${URL_2}"`);
+
+ info("Waiting for will-navigate");
+ yield willNavigate;
+
+ info("Navigation to page 2 has started, the inspector should be empty");
+ assertMarkupViewIsEmpty();
+
+ info("Waiting for new-root");
+ yield inspector.once("new-root");
+
+ info("Navigation to page 2 was done, the inspector should be back up");
+ assertMarkupViewIsLoaded();
+
+ yield selectNode("#two", inspector);
+
+ function assertMarkupViewIsLoaded() {
+ let markupViewBox = inspector.panelDoc.getElementById("markup-box");
+ is(markupViewBox.childNodes.length, 1, "The markup-view is loaded");
+ }
+
+ function assertMarkupViewIsEmpty() {
+ let markupViewBox = inspector.panelDoc.getElementById("markup-box");
+ is(markupViewBox.childNodes.length, 0, "The markup-view is unloaded");
+ }
+});
diff --git a/devtools/client/inspector/markup/test/browser_markup_void_elements_html.js b/devtools/client/inspector/markup/test/browser_markup_void_elements_html.js
new file mode 100644
index 000000000..60330a144
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_void_elements_html.js
@@ -0,0 +1,44 @@
+/* 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";
+
+// Test void element display in the markupview.
+const TEST_URL = URL_ROOT + "doc_markup_void_elements.html";
+
+add_task(function* () {
+ let {inspector} = yield openInspectorForURL(TEST_URL);
+ let {win} = inspector.markup;
+
+ info("check non-void element closing tag is displayed");
+ let {editor} = yield getContainerForSelector("h1", inspector);
+ ok(!editor.elt.classList.contains("void-element"),
+ "h1 element does not have void-element class");
+ ok(!editor.elt.querySelector(".close").style.display !== "none",
+ "h1 element tag is not hidden");
+
+ info("check void element closing tag is hidden in HTML document");
+ let container = yield getContainerForSelector("img", inspector);
+ ok(container.editor.elt.classList.contains("void-element"),
+ "img element has the expected class");
+ let closeElement = container.editor.elt.querySelector(".close");
+ let computedStyle = win.getComputedStyle(closeElement, null);
+ ok(computedStyle.display === "none", "img closing tag is hidden");
+
+ info("check void element with pseudo element");
+ let hrNodeFront = yield getNodeFront("hr.before", inspector);
+ container = getContainerForNodeFront(hrNodeFront, inspector);
+ ok(container.editor.elt.classList.contains("void-element"),
+ "hr element has the expected class");
+ closeElement = container.editor.elt.querySelector(".close");
+ computedStyle = win.getComputedStyle(closeElement, null);
+ ok(computedStyle.display === "none", "hr closing tag is hidden");
+
+ info("check expanded void element closing tag is not hidden");
+ yield inspector.markup.expandNode(hrNodeFront);
+ yield waitForMultipleChildrenUpdates(inspector);
+ ok(container.expanded, "hr container is expanded");
+ computedStyle = win.getComputedStyle(closeElement, null);
+ ok(computedStyle.display === "none", "hr closing tag is not hidden anymore");
+});
diff --git a/devtools/client/inspector/markup/test/browser_markup_void_elements_xhtml.js b/devtools/client/inspector/markup/test/browser_markup_void_elements_xhtml.js
new file mode 100644
index 000000000..0cccf54d4
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_void_elements_xhtml.js
@@ -0,0 +1,28 @@
+/* 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";
+
+// Test void element display in the markupview.
+const TEST_URL = URL_ROOT + "doc_markup_void_elements.xhtml";
+
+add_task(function* () {
+ let {inspector} = yield openInspectorForURL(TEST_URL);
+ let {win} = inspector.markup;
+
+ info("check non-void element closing tag is displayed");
+ let {editor} = yield getContainerForSelector("h1", inspector);
+ ok(!editor.elt.classList.contains("void-element"),
+ "h1 element does not have void-element class");
+ ok(!editor.elt.querySelector(".close").style.display !== "none",
+ "h1 element tag is not hidden");
+
+ info("check void element closing tag is not hidden in XHTML document");
+ let container = yield getContainerForSelector("br", inspector);
+ ok(!container.editor.elt.classList.contains("void-element"),
+ "br element does not have void-element class");
+ let closeElement = container.editor.elt.querySelector(".close");
+ let computedStyle = win.getComputedStyle(closeElement, null);
+ ok(computedStyle.display !== "none", "br closing tag is not hidden");
+});
diff --git a/devtools/client/inspector/markup/test/browser_markup_whitespace.js b/devtools/client/inspector/markup/test/browser_markup_whitespace.js
new file mode 100644
index 000000000..63a0d0467
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_whitespace.js
@@ -0,0 +1,66 @@
+/* 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";
+
+// Test that whitespace text nodes do show up in the markup-view when needed.
+
+const TEST_URL = URL_ROOT + "doc_markup_whitespace.html";
+
+add_task(function* () {
+ let {inspector, testActor} = yield openInspectorForURL(TEST_URL);
+ let {markup} = inspector;
+
+ yield markup.expandAll();
+
+ info("Verify the number of child nodes and child elements in body");
+
+ // Body has 5 element children, but there are 6 text nodes in there too, they come from
+ // the HTML file formatting (spaces and carriage returns).
+ let {numNodes, numChildren} = yield testActor.getNodeInfo("body");
+ is(numNodes, 11, "The body node has 11 child nodes (includes text nodes)");
+ is(numChildren, 5, "The body node has 5 child elements (only element nodes)");
+
+ // In body, there are only block-level elements, so whitespace text nodes do not have
+ // layout, so they should be skipped in the markup-view.
+ info("Check that the body's whitespace text node children aren't shown");
+ let bodyContainer = markup.getContainer(inspector.selection.nodeFront);
+ let childContainers = bodyContainer.getChildContainers();
+ is(childContainers.length, 5,
+ "Only the element nodes are shown in the markup view");
+
+ // div#inline has 3 element children, but there are 4 text nodes in there too, like in
+ // body, they come from spaces and carriage returns in the HTML file.
+ info("Verify the number of child nodes and child elements in div#inline");
+ ({numNodes, numChildren} = yield testActor.getNodeInfo("#inline"));
+ is(numNodes, 7, "The div#inline node has 7 child nodes (includes text nodes)");
+ is(numChildren, 3, "The div#inline node has 3 child elements (only element nodes)");
+
+ // Within the inline formatting context in div#inline, the whitespace text nodes between
+ // the images have layout, so they should appear in the markup-view.
+ info("Check that the div#inline's whitespace text node children are shown");
+ yield selectNode("#inline", inspector);
+ let divContainer = markup.getContainer(inspector.selection.nodeFront);
+ childContainers = divContainer.getChildContainers();
+ is(childContainers.length, 5,
+ "Both the element nodes and some text nodes are shown in the markup view");
+
+ // div#pre has 2 element children, but there are 3 text nodes in there too, like in
+ // div#inline, they come from spaces and carriage returns in the HTML file.
+ info("Verify the number of child nodes and child elements in div#pre");
+ ({numNodes, numChildren} = yield testActor.getNodeInfo("#pre"));
+ is(numNodes, 5, "The div#pre node has 5 child nodes (includes text nodes)");
+ is(numChildren, 2, "The div#pre node has 2 child elements (only element nodes)");
+
+ // Within the inline formatting context in div#pre, the whitespace text nodes between
+ // the images have layout, so they should appear in the markup-view, but since
+ // white-space is set to pre, then the whitespace text nodes before and after the first
+ // and last image should also appear.
+ info("Check that the div#pre's whitespace text node children are shown");
+ yield selectNode("#pre", inspector);
+ divContainer = markup.getContainer(inspector.selection.nodeFront);
+ childContainers = divContainer.getChildContainers();
+ is(childContainers.length, 5,
+ "Both the element nodes and all text nodes are shown in the markup view");
+});
diff --git a/devtools/client/inspector/markup/test/doc_markup_anonymous.html b/devtools/client/inspector/markup/test/doc_markup_anonymous.html
new file mode 100644
index 000000000..0ede3ca5f
--- /dev/null
+++ b/devtools/client/inspector/markup/test/doc_markup_anonymous.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <title>Anonymous content test</title>
+ <style type="text/css">
+ #pseudo::before {
+ content: "before";
+ }
+ #pseudo::after {
+ content: "after";
+ }
+ #shadow::before {
+ content: "Testing ::before on a shadow host";
+ }
+ </style>
+</head>
+<body>
+ <div id="pseudo"><span>middle</span></div>
+
+ <div id="shadow">light dom</div>
+
+ <div id="native"><video controls></video></div>
+
+ <script>
+ "use strict";
+ var host = document.querySelector("#shadow");
+ if (host.createShadowRoot) {
+ var root = host.createShadowRoot();
+ root.innerHTML = "<h3>Shadow DOM</h3><select multiple></select>";
+ }
+ </script>
+</body>
+</html> \ No newline at end of file
diff --git a/devtools/client/inspector/markup/test/doc_markup_dragdrop.html b/devtools/client/inspector/markup/test/doc_markup_dragdrop.html
new file mode 100644
index 000000000..f45c26065
--- /dev/null
+++ b/devtools/client/inspector/markup/test/doc_markup_dragdrop.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=858038
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 858038</title>
+ <style>
+ #test::before {
+ content: 'This should not be draggable';
+ }
+ </style>
+</head>
+<body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=858038">Mozilla Bug 858038</a>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <input id="anonymousParent" /><span id="before">Before<!-- Force not-inline --></span>
+ <pre id="test"><span id="firstChild">First</span><span id="middleChild">Middle</span><span id="lastChild">Last</span></pre> <span id="after">After</span>
+</body>
+</html>
diff --git a/devtools/client/inspector/markup/test/doc_markup_dragdrop_autoscroll_01.html b/devtools/client/inspector/markup/test/doc_markup_dragdrop_autoscroll_01.html
new file mode 100644
index 000000000..35f3b5f31
--- /dev/null
+++ b/devtools/client/inspector/markup/test/doc_markup_dragdrop_autoscroll_01.html
@@ -0,0 +1,87 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=858038
+https://bugzilla.mozilla.org/show_bug.cgi?id=1226898
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 858038 and 1226898 - Autoscroll</title>
+</head>
+<body>
+ <div id="first"></div>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=858038">Mozilla Bug 858038</a>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1226898">Mozilla Bug 1226898</a>
+ <p id="display">Test</p>
+ <div id="content" style="display: none">
+
+ </div>
+
+ <!-- Make sure the markup-view has enough nodes shown by default that it has a scrollbar -->
+
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+</body>
+</html>
diff --git a/devtools/client/inspector/markup/test/doc_markup_dragdrop_autoscroll_02.html b/devtools/client/inspector/markup/test/doc_markup_dragdrop_autoscroll_02.html
new file mode 100644
index 000000000..9e4d92cf3
--- /dev/null
+++ b/devtools/client/inspector/markup/test/doc_markup_dragdrop_autoscroll_02.html
@@ -0,0 +1,40 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=858038
+https://bugzilla.mozilla.org/show_bug.cgi?id=1226898
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 858038 and 1226898 - Autoscroll</title>
+</head>
+<body>
+ <div id="first"></div>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=858038">Mozilla Bug 858038</a>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1226898">Mozilla Bug 1226898</a>
+ <p id="display">Test</p>
+ <div id="content" style="display: none">
+
+ </div>
+
+ <!-- Make sure the markup-view has enough nodes shown by default that it has a scrollbar -->
+
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+</body>
+</html>
diff --git a/devtools/client/inspector/markup/test/doc_markup_edit.html b/devtools/client/inspector/markup/test/doc_markup_edit.html
new file mode 100644
index 000000000..ddefd1d87
--- /dev/null
+++ b/devtools/client/inspector/markup/test/doc_markup_edit.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+
+<html class="html">
+
+ <body class="body">
+ <div class="node0">
+ <div id="node1" class="node1">line1</div>
+ <div id="node2" class="node2">line2</div>
+ <p class="node3">line3</p>
+ <!-- A comment -->
+ <p id="node4" class="node4">line4
+ <span class="node5">line5</span>
+ <span class="node6">line6</span>
+ <!-- A comment -->
+ <a class="node7">line7<span class="node8">line8</span></a>
+ <span class="node9">line9</span>
+ <span class="node10">line10</span>
+ <span class="node11">line11</span>
+ <a class="node12">line12<span class="node13">line13</span></a>
+ </p>
+ <p id="node14">line14</p>
+ <p class="node15">line15</p>
+ </div>
+ <div id="node16">
+ <p id="node17">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec posuere placerat magna et imperdiet.</p>
+ </div>
+ <div id="node18">
+ <div id="node19">
+ <div id="node20">
+ <div id="node21">
+ line21
+ </div>
+ </div>
+ </div>
+ </div>
+ <div id="node22" class="unchanged"></div>
+ <div id="node23"></div>
+ <div id="node24"></div>
+ <div id="retag-me">
+ <div id="retag-me-2"></div>
+ </div>
+ <div id="node25"></div>
+ <div id="node26" style='background-image: url("moz-page-thumb://thumbnail?url=http%3A%2F%2Fwww.mozilla.org%2F");'></div>
+ <div id="node27" class="Double &quot; and single &apos;"></div>
+ <img id="node-data-url" />
+ <div id="node-data-url-style"></div>
+ </body>
+</html>
diff --git a/devtools/client/inspector/markup/test/doc_markup_events-overflow.html b/devtools/client/inspector/markup/test/doc_markup_events-overflow.html
new file mode 100644
index 000000000..d604245fe
--- /dev/null
+++ b/devtools/client/inspector/markup/test/doc_markup_events-overflow.html
@@ -0,0 +1,19 @@
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>doc_markup_events-overflow.html</title>
+</head>
+<body>
+ <h1>doc_markup_events-overflow.html</h1>
+ <span id="events">Inspect me!</span>
+ <script>
+ "use strict";
+ var el = document.getElementById("events");
+ for (var i = 50; i > 0; i--) {
+ el.addEventListener("click", function onClick() {
+ alert("click");
+ });
+ }
+ </script>
+</body>
+</html>
diff --git a/devtools/client/inspector/markup/test/doc_markup_events1.html b/devtools/client/inspector/markup/test/doc_markup_events1.html
new file mode 100644
index 000000000..0955289e2
--- /dev/null
+++ b/devtools/client/inspector/markup/test/doc_markup_events1.html
@@ -0,0 +1,113 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <style>
+ #container {
+ border: 1px solid #000;
+ width: 200px;
+ height: 85px;
+ }
+
+ #container > div {
+ border: 1px solid #000;
+ display: inline-block;
+ margin: 2px;
+ }
+
+ #output,
+ #noevents,
+ #DOM0,
+ #handleevent,
+ #output,
+ #noevents {
+ cursor: auto;
+ }
+
+ #output {
+ min-height: 1.5em;
+ }
+ </style>
+ <script type="application/javascript;version=1.8">
+ function init() {
+ let container = document.getElementById("container");
+ let multiple = document.getElementById("multiple");
+
+ container.addEventListener("mouseover", mouseoverHandler, true);
+ multiple.addEventListener("click", clickHandler, false);
+ multiple.addEventListener("mouseup", mouseupHandler, false);
+
+ let he = new handleEventClick();
+ let handleevent = document.getElementById("handleevent");
+ handleevent.addEventListener("click", he);
+ }
+
+ function mouseoverHandler(event) {
+ if (event.target.id !== "container") {
+ let output = document.getElementById("output");
+ output.textContent = event.target.textContent;
+ }
+ }
+
+ function clickHandler(event) {
+ let output = document.getElementById("output");
+ output.textContent = "click";
+ }
+
+ function mouseupHandler(event) {
+ let output = document.getElementById("output");
+ output.textContent = "mouseup";
+ }
+
+ function handleEventClick(hehe) {
+
+ }
+
+ handleEventClick.prototype = {
+ handleEvent: function(blah) {
+ alert("handleEvent");
+ }
+ };
+
+ function noeventsClickHandler(event) {
+ alert("noevents has an event listener");
+ }
+
+ function addNoeventsClickHandler() {
+ let noevents = document.getElementById("noevents");
+ noevents.addEventListener("click", noeventsClickHandler);
+ }
+
+ function removeNoeventsClickHandler() {
+ let noevents = document.getElementById("noevents");
+ noevents.removeEventListener("click", noeventsClickHandler);
+ }
+ </script>
+ </head>
+ <body onload="init();">
+ <h1>Events test 1</h1>
+ <div id="container">
+ <div>1</div>
+ <div>2</div>
+ <div>3</div>
+ <div>4</div>
+ <div>5</div>
+ <div>6</div>
+ <div>7</div>
+ <div>8</div>
+ <div>9</div>
+ <div>10</div>
+ <div>11</div>
+ <div>12</div>
+ <div>13</div>
+ <div>14</div>
+ <div>15</div>
+ <div>16</div>
+ <div id="multiple">multiple</div>
+ </div>
+ <div id="output"></div>
+ <div id="noevents">noevents</div>
+ <div id="DOM0" onclick="alert('DOM0')">DOM0 event here</div>
+ <div id="handleevent">handleEvent</div>
+ </body>
+</html>
diff --git a/devtools/client/inspector/markup/test/doc_markup_events2.html b/devtools/client/inspector/markup/test/doc_markup_events2.html
new file mode 100644
index 000000000..ddc17537d
--- /dev/null
+++ b/devtools/client/inspector/markup/test/doc_markup_events2.html
@@ -0,0 +1,111 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <style>
+ #fatarrow,
+ #bound,
+ #boundhe,
+ #comment-inline,
+ #comment-streaming,
+ #anon-object-method,
+ #object-method {
+ border: 1px solid #000;
+ width: 200px;
+ min-height: 1em;
+ cursor: pointer;
+ }
+ </style>
+ <script type="application/javascript;version=1.8">
+ function init() {
+ let fatarrow = document.getElementById("fatarrow");
+
+ let he = new handleEventClick();
+ let anonObjectMethod = document.getElementById("anon-object-method");
+ anonObjectMethod.addEventListener("click", he.anonObjectMethod);
+
+ let objectMethod = document.getElementById("object-method");
+ objectMethod.addEventListener("click", he.objectMethod);
+
+ let bhe = new boundHandleEventClick();
+ let boundheNode = document.getElementById("boundhe");
+ bhe.handleEvent = bhe.handleEvent.bind(bhe);
+ boundheNode.addEventListener("click", bhe);
+
+ let boundNode = document.getElementById("bound");
+ boundClickHandler = boundClickHandler.bind(this);
+ boundNode.addEventListener("click", boundClickHandler);
+
+ fatarrow.addEventListener("click", () => {
+ alert("Fat arrow without params!");
+ });
+
+ fatarrow.addEventListener("click", event => {
+ alert("Fat arrow with 1 param!");
+ });
+
+ fatarrow.addEventListener("click", (event, foo, bar) => {
+ alert("Fat arrow with 3 params!");
+ });
+
+ fatarrow.addEventListener("click", b => b);
+
+ let inlineCommentNode = document.getElementById("comment-inline");
+ inlineCommentNode
+ .addEventListener("click", functionProceededByInlineComment);
+
+ let streamingCommentNode = document.getElementById("comment-streaming");
+ streamingCommentNode
+ .addEventListener("click", functionProceededByStreamingComment);
+ }
+
+ function boundClickHandler(event) {
+ alert("Bound event");
+ }
+
+ function handleEventClick(hehe) {
+
+ }
+
+ handleEventClick.prototype = {
+ anonObjectMethod: function() {
+ alert("obj.anonObjectMethod");
+ },
+
+ objectMethod: function kay() {
+ alert("obj.objectMethod");
+ },
+ };
+
+ function boundHandleEventClick() {
+
+ }
+
+ boundHandleEventClick.prototype = {
+ handleEvent: function() {
+ alert("boundHandleEvent");
+ }
+ };
+
+ // A function proceeded with an inline comment
+ function functionProceededByInlineComment() {
+ alert("comment-inline");
+ }
+
+ /* A function proceeded with a streaming comment */
+ function functionProceededByStreamingComment() {
+ alert("comment-streaming");
+ }
+ </script>
+ </head>
+ <body onload="init();">
+ <h1>Events test 2</h1>
+ <div id="fatarrow">Fat arrows</div>
+ <div id="boundhe">Bound handleEvent</div>
+ <div id="bound">Bound event</div>
+ <div id="comment-inline">Event proceeded by an inline comment</div>
+ <div id="comment-streaming">Event proceeded by a streaming comment</div>
+ <div id="anon-object-method">Anonymous object method</div>
+ <div id="object-method">Object method</div>
+ </body>
+</html>
diff --git a/devtools/client/inspector/markup/test/doc_markup_events3.html b/devtools/client/inspector/markup/test/doc_markup_events3.html
new file mode 100644
index 000000000..af4decc40
--- /dev/null
+++ b/devtools/client/inspector/markup/test/doc_markup_events3.html
@@ -0,0 +1,115 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <style>
+ #es6-method,
+ #generator,
+ #anon-generator,
+ #named-function-expression,
+ #anon-function-expression,
+ #returned-function,
+ #constructed-function,
+ #constructed-function-with-body-string,
+ #multiple-assignment {
+ border: 1px solid #000;
+ width: 200px;
+ min-height: 1em;
+ cursor: pointer;
+ }
+ </style>
+ <script type="application/javascript;version=1.8">
+ let namedFunctionExpression =
+ function foo() {
+ alert("namedFunctionExpression");
+ }
+
+ let anonFunctionExpression = function() {
+ alert("anonFunctionExpression");
+ };
+
+ let returnedFunction = (function() {
+ return function bar() {
+ alert("returnedFunction");
+ }
+ })();
+
+ let constructedFunc = new Function();
+
+ let constructedFuncWithBodyString =
+ new Function('a', 'b', 'c', 'alert("constructedFuncWithBodyString");');
+
+ let multipleAssignment = foo = bar = function multi() {
+ alert("multipleAssignment");
+ }
+
+ function init() {
+ let he = new handleEventClick();
+ let es6Method = document.getElementById("es6-method");
+ es6Method.addEventListener("click", he.es6Method);
+
+ let generatorNode = document.getElementById("generator");
+ generatorNode.addEventListener("click", generator);
+
+ let anonGenerator = document.getElementById("anon-generator");
+ anonGenerator.addEventListener("click", function* () {
+ alert("anonGenerator");
+ });
+
+ let namedFunctionExpressionNode =
+ document.getElementById("named-function-expression");
+ namedFunctionExpressionNode.addEventListener("click",
+ namedFunctionExpression);
+
+ let anonFunctionExpressionNode =
+ document.getElementById("anon-function-expression");
+ anonFunctionExpressionNode.addEventListener("click",
+ anonFunctionExpression);
+
+ let returnedFunctionNode = document.getElementById("returned-function");
+ returnedFunctionNode.addEventListener("click", returnedFunction);
+
+ let constructedFunctionNode =
+ document.getElementById("constructed-function");
+ constructedFunctionNode.addEventListener("click", constructedFunc);
+
+ let constructedFunctionWithBodyStringNode =
+ document.getElementById("constructed-function-with-body-string");
+ constructedFunctionWithBodyStringNode
+ .addEventListener("click", constructedFuncWithBodyString);
+
+ let multipleAssignmentNode =
+ document.getElementById("multiple-assignment");
+ multipleAssignmentNode.addEventListener("click", multipleAssignment);
+ }
+
+ function handleEventClick(hehe) {
+
+ }
+
+ handleEventClick.prototype = {
+ es6Method() {
+ alert("obj.es6Method");
+ }
+ };
+
+ function* generator() {
+ alert("generator");
+ }
+ </script>
+ </head>
+ <body onload="init();">
+ <h1>Events test 3</h1>
+ <div id="es6-method">ES6 method</div>
+ <div id="generator">Generator</div>
+ <div id="anon-generator">Anonymous Generator</div>
+ <div id="named-function-expression">Named Function Expression</div>
+ <div id="anon-function-expression">Anonymous Function Expression</div>
+ <div id="returned-function">Returned Function</div>
+ <div id="constructed-function">Constructed Function</div>
+ <div id="constructed-function-with-body-string">
+ Constructed Function with body string
+ </div>
+ <div id="multiple-assignment">Multiple Assignment</div>
+ </body>
+</html>
diff --git a/devtools/client/inspector/markup/test/doc_markup_events_form.html b/devtools/client/inspector/markup/test/doc_markup_events_form.html
new file mode 100644
index 000000000..b4ddff4aa
--- /dev/null
+++ b/devtools/client/inspector/markup/test/doc_markup_events_form.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <style>
+ </style>
+ </head>
+ <body>
+ <div id="container">
+ </body>
+</html>
diff --git a/devtools/client/inspector/markup/test/doc_markup_events_jquery.html b/devtools/client/inspector/markup/test/doc_markup_events_jquery.html
new file mode 100644
index 000000000..5f8caff27
--- /dev/null
+++ b/devtools/client/inspector/markup/test/doc_markup_events_jquery.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+
+ <style>
+ input {
+ margin: 5px 3px 10px 10px;
+ }
+
+ div {
+ width: 100px;
+ height: 100px;
+ border: 1px solid #000;
+ }
+ </style>
+
+ <script type="application/javascript;version=1.8">
+ let jq = document.location.search.substr(1);
+
+ let script = document.createElement("script");
+ script.setAttribute("type", "text/javascript");
+ script.setAttribute("src", jq);
+
+ document.head.appendChild(script);
+
+ window.addEventListener("load", () => {
+ var handler1 = function liveDivDblClick() { alert(1); };
+ var handler2 = function liveDivDragStart() { alert(2); };
+ var handler3 = function liveDivDragLeave() { alert(3); };
+ var handler4 = function liveDivDragEnd() { alert(4); };
+ var handler5 = function liveDivDrop() { alert(5); };
+ var handler6 = function liveDivDragOver() { alert(6); };
+ var handler7 = function divClick1() { alert(7); };
+ var handler8 = function divClick2() { alert(8); };
+ var handler9 = function divKeyDown() { alert(9); };
+ var handler10 = function divDragOut() { alert(10); };
+
+ if ($("#livediv").live) {
+ $("#livediv").live( "dblclick", handler1);
+ $("#livediv").live( "dragstart", handler2);
+ }
+
+ if ($("#livediv").delegate) {
+ $(document).delegate( "#livediv", "dragleave", handler3);
+ $(document).delegate( "#livediv", "dragend", handler4);
+ }
+
+ if ($("#livediv").on) {
+ $(document).on( "drop", "#livediv", handler5);
+ $(document).on( "dragover", "#livediv", handler6);
+ $(document).on( "dragout", "#livediv:xxxxx", handler10);
+ }
+
+ var div = $("div")[0];
+ $(div).click(handler7);
+ $(div).click(handler8);
+ $(div).keydown(handler9);
+ });
+ </script>
+ </head>
+ <body>
+ <div id="testdiv"></div>
+ <br>
+ <div id="livediv"></div>
+ </body>
+</html>
diff --git a/devtools/client/inspector/markup/test/doc_markup_flashing.html b/devtools/client/inspector/markup/test/doc_markup_flashing.html
new file mode 100644
index 000000000..3bb8cf1d2
--- /dev/null
+++ b/devtools/client/inspector/markup/test/doc_markup_flashing.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <title>mutation flashing test</title>
+</head>
+<body>
+ <div id="root">
+ <ul class="list">
+ <li class="item">item</li>
+ <li class="item">item</li>
+ </ul>
+ </div>
+</body>
+</html> \ No newline at end of file
diff --git a/devtools/client/inspector/markup/test/doc_markup_html_mixed_case.html b/devtools/client/inspector/markup/test/doc_markup_html_mixed_case.html
new file mode 100644
index 000000000..ab26005e1
--- /dev/null
+++ b/devtools/client/inspector/markup/test/doc_markup_html_mixed_case.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <svg viewBox="0 0 2 2" width=200 height=200>
+ <clipPath>
+ <rect x=0 y=0 width=1 height=1 />
+ </clipPath>
+ <circle cx=1 cy=1 r=1 fill=lime />
+ </svg>
+ <DIV></DIV>
+ </body>
+</html>
diff --git a/devtools/client/inspector/markup/test/doc_markup_image_and_canvas.html b/devtools/client/inspector/markup/test/doc_markup_image_and_canvas.html
new file mode 100644
index 000000000..0b8a8bb80
--- /dev/null
+++ b/devtools/client/inspector/markup/test/doc_markup_image_and_canvas.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html class="html">
+ <head class="head">
+ <meta charset=utf-8 />
+ <title>Image and Canvas markup-view test</title>
+ </head>
+ <body>
+ <div></div>
+ <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAdYElEQVRogVWYZ3QV5pWuz6y5694pKzNzZ26SyWQyccpkJo5jx8GOHXcbgzHYgOkgJECIYoQAIQkhoYoaoIKEQEK9HUnnqJxedXpvOk3l6OiogkB0XOLkzpo/z/2BV9a6P971re/ffvbe6/32twVB3QFC+oMEdcmMa/cT0uxnwpBCwnaEZc8J7npPMDO2n8Dop3iHNhIc2URUtoVp1XYS2iSiol3E5UeZ12YRNxSz6G1idXaU5Xk9E5MqEvNO4vMu4gk7iYSVpYSRlYSelVk1t+Mqlue1LM1pWE5ouJPQcG9WxaNZNU/ier6aNfF1wspKUM29yTGeLNq4k9CyEJdxa0HC4vwwgoDmWeBBXTJBXTIRXQqTxgPELanM2Y8QUe4krNhBWLadSfVO5saSWLaksGw5yLLpEM62j/F17SQ6cpQ5UzHLgSZuTw1yZ17L6oqd+TkLc3M25uYsLM2ZuTVnYGVOy524gltxOUuzapa+hVlNqFidkbASEbHg6SZubWfJPcgXcT1P5wysTCm5HVdxO6FkcVbK8oIMQXjsAOGxZCL6Z5mfNB5gynSQKUMKE/r9hBW7mBnbz5L1MCvOYzzwZ/A0coZH/gxuW45gbHwfbe372Dt2sWAu5PFkK6tTvdyeFnN3QcPctJK5mIaFuI7lhI47CQ2rCRX3EjJW43LuJLTcndPxYE7Dw1kJtyO9TBirMfZnMXw9FU3nGR5MDPD1kpqliJg7cQWri3rmZ+QsJlQIosYDhPT7CWn3EdYlEf0WJqzZS1C5i1lLKnc86Tzwn+Zh4AxfTeTyf+OFfBHMYslwGHvLR0grX2O44i08A4e467vC41gHD2N93J8Rczcu4e6snNWE4tk5K+VefJT7iVHuz0pYjal4mNDxJKHg3kQ/cVstNtFpBmu30pj3FvXn3sQnO8+TmX4ezo6yGpdzb2GM5cQYs1MqBCHtXsbVuwmodhHU7GFyLJmY6SAzpoPEjAdYdBznrv80D8fP8iScw59mCvnvuVIe+M8wpUjC2voR0srf0XX+VwxVvoVPdJhV7yW+jnfwzVwff1gU8/XiCE/nh3g0K+Z+TMj9mJBHs4M8mRvlaVzJFzNK7keExM3VeIcz0bXtRVSzlo7S16n8/Gf0XfmIeXcNf1yRc2d6hDszWh4uu0hM6hFMGJKZNKYwbT5IwnGEW9507oUyeRTN5slELg9CWTyOnufrqQL+OFPMfycq+GaqmAXTcdx9W9DUv81Yw7vorr6HqvY9LC1biclPcNtSyC17Kfd81dwPNHA/dIN7oSZWI83cn2zl6Ww3f1gU86c5GV9MiJg31WHvO4mi/jNGqtcjrnobYflr1Gf9B/U5L+KVneWb5WFWJgZZmlDwZMXP0owdwe1AOndDp3gYzebJVC5Pp/P4IpbPV7ELfD1TxNPJfL6YKuQP08V8Eyvhy2gRK85MIrJkHN2bMTV/yHjfZ0wO7SEo3IG/ZxvBvt2M9+/D07OH8FAaE9J0YqpsYrrzzBgKmDEXMeeoYNlTx21nPXFtOY7uEwxXfUpf4bsIC9+iO+8VmjKf50bWC9RkPI+u7QD3ox3cnxnh9qSCe3M2bsWsCJ7O5PFVooA/LZTwx/kSvojlcz+cw11/Jnd8Z7njzWbFk8WqJ4dVTw5L1jNMqtIIDCUREO1mSpJEXJbEzGgS0cGdBPs+I9K7g1D3Nvxdn+Fu34qvdzfjAwfwDR7E2ZeCqXs/hu5kjN0HMLYmoW7YzmDJOjpy3qAr5036z79N26k1XD74U+qOP09t+ouM1O5i3tXI10sq7ie0LExoWJwyIPhDoogvYvk8iGRzJ5DJHd8ZbnvOMG89ybT+CBOawwTlKbjFe3EM7MErSiYqP0pMk05ce4JF/XFmZSnMjO5lQrQDb9sGXE3r8bVuxNvyMYGOLQR7dxAS7iHQv4/AwAG8gwcxd+9B1bSZ0cvv0p3/Mo0n/pO2U2sQ5a2j9cQbFG/5MWfXfpfinT+jIf0N2gs2ccvdzJ9uabgbk3JrSsXqnAnBnUAmi650pg1pjCuScIl3Yuz+BG3bBlTNH6Fs2oD65kZ0bVuwCfcQlR9nwZjDsuU8C4ZMFvWfMzm6j1D/dvzdm3E0rcPRtA5/2yYC7Z/8GSLQvQ1vzw68vbvx9e/F0bcHc8dmdNffo6/wBerS/pXGY88zkv8xA2c3UrrleY68+vec3/RTrqW/R/uFzcwYr/J4ZoT7szLufWu/AlPPZnTtH6Ns/hBp43uIqt+kp/wVhFW/Z7T+QzQ3t+AWHWBae5o5Uw4LljwWrfksWPJYMGUzrzvB+MBObG0bsTStx3DtA6w31hPo2MJ451Y8LZvwdmzB27UVZ8dmHJ1bcXZvw927A3ffZ7h6N6C6+hqd2c/Tdvq3DJ/fRFf6enLX/Zyk5/+G8xv/k9asjfSU7CSivsTdSB8P5xQ8Xh5jZVaNoLvsVTpL19BT/jsGLr/JwOW3GbzyDpqbW/ANpzJvPseKq5i7nlKWrIXMGfNYMF9g0VLEoukccdVxHJ2b0TWuRX9tHWON67Hc3ISneye+rp14O3fg796Bt2cH7q5t2Ns+wdb+Cc6uLbh7P8E3sBFX90eMNa5HUfkRPZlrKdz0Cw6++B32/fKvqNy3hv6i7Ygu7WXSeIXVqQEezct5tKTndlyNoLf8Tfoq3mLw8vtI6jcgb9yEvm07gdFjLFrzeTR+mRVnKdOaLIKSDKY1OdxxlbPqqeKW9QLR0cOYbm5EffVDjDc3Y23fhqNzN57e/bi69+HuScLTm4Svbx/e3t24u7bhaP8UZ+eneHo+wdmzDo9wIz7hLhyt++jN/oBT7/4Te3/5Fxx74x+4fvL3DJR9ysDlLSSc1TydH+L+nJQ7CS0rCQOCgUsfILryISO1G5E1bEbXsgvXwGFi2mxu2UtZtpVw113BirOcOWMB86ZCVpzl3HaUMTeWi1e4D8219agbPsLevQfvYCo+0RE8omM4+9OwdR3A1p2EszcJr3AfPuEuvD3b8PdsIdC/FVffx7iFn+Lr38t4fxr6ut00HH6Zwi0/omr/v9N+7jW6S9+h4+K7xB3lfLE4xOqslNszelbnHQgGK9YxWr0JWd1mVNe2Ye05yLQmhxVn2bMs2y+ybCtlWn0en/gkvuEMprV5TGvzCEoyMLVtR167FnXjJ/jFaUQVmUQUZwkpsghIz2DpOYyxIxlzxx6cvfvwC/fi79tBULiNkGg740Pb8Yl34RXuIzhwmPDgCaxN+xkpW8dA8Rt0Fb1MZ+nLXM//NaGxHB7O9nF3RsbtmJG7CS+Ckas7kTfvR30zBU37IbySLG7763g61cLDyA2eTLZi78+g5vRrFCT/gq6SjwiMZjKlycc1cBRd0xZGrnyA5sZWxkczmNYVMKkvIaorIawpwiLMQN+Vhr4tCWvXPrzCffj7dhEU7iAs3sO4eA+h0RR8/Ul4e/YzLUknLs3A27kLzdUPGCx7icHy33A99z/wyT9nNXqT+zMj3IsbuBO3I1D3ZyPtPcXYUC4uXRlhey1LU518uSLlm1U1y+EeJC0nObPnBfa9+48UpLyAtH4fPvEZxofSsXbuxtmbhLM/jZD0HFO6SsZVldiHi3FKL+JXlSFpPkTf5c3Im7Zj696Lo2snnp5dTAynMC1NIyRKxte3j/GBZKZHj5CQHWV6aD+Rvm0Ya99EXPBLbp5+DkXtem45qngSE7I6KebxvA6BS3+Vcdt1pv3tTI23MR1sZ3VxlG8em/jqgYFEsA9x82kObvwJ7/xCQMZnP8HYcYw5XQkB8edMKdLxDBwgLDuDazCToKKCwfpj1Odv51TSGmovfIq84wRm0SlGr+9AXLMBW/deIsNp+Pr2MTFyhLA4lcDAAcZFKUREB5kaSmF6KIkZ0R58TeuQFf+G5s//lcHi3xNT5fAo0sKDSSGPZyUIJpxdxP19zIWETHrbiAU6eHJbxX89sfD0loqovZmumlRO7vwVJz/7KS0X1uEZPMmMOocJaTqhkTSiss+JyrMZrt1NXdZ6Sk+8T27aW+z44F9Y/+pfk536G7qufMbojd0om3dh7UkhIErDN3gQ/+BhxsVHCAwdwSc6hE+YQqB/P5PiZOKjKUwKd6C/8hbN6c/Rnv0ibuERVgPXeRzr52F8CMFiYJhZ3yAJ/wDz4X7uJST81xMLf3pgYnlyEK+2Bml7JoP1aZj6zjKlLWbOWMCCMZcVWx7zY2eJqU/RX7GB0sO/Zucb36Hy9Hoy9r7CKz8V8G/fEfDRa/+Tyqw30fQewz2agV2YhnvwKFFFJh7RMXzD6fhHTuId/hxnfyouYTLBwRQmRw4wM5KCrWkDfXkv0XHuZYztB1jxNvBlQszjxAiChEvElK2HeZ+I+3EVX67o+eaegYcLSmb83QT0DUxYbjBtbSA6Vsm0oYQlewl3nBdYsmSzZDnHtDaToZrNXDn9Ojvf/nuyU15l59of8fPvCnj9lwLOH/s9yr7TeFT5uGXnCCrPManJJ6o6z7g0h4D8HOOKXMYVufhkZ/BJ0hkfPsr4UCqRoUO4OrejrF6LuPw99C37WbBX82VCxOPEEIJZp5gpWx/zPjF3p+Xcm1XwaEnL09tjPF7UPhucwoPMutuIWa8SM1UQtxSTMOWSMGYxqc4gMHqMsDwHVUsa9ec/ISv5d5xLe4czh96grngX6sF8Io6rRGxXmLBUseC9yoKzhrCmiLDuIuGxMsJjZUQM5USMpUR0+YSVZwlJTzI+dBhPbxKW1u2oGj5F25LEtOEij2NCHsYHEMy6RcRdgyyGRrk7o+LRkp6v7jl4esfK4pQcn7EVnfgS8p5CrKNlhMYuMWmsJGEv5+74ZRZthbhEaXiGzmAWZuJXViHryEHdV4isJx+nthab6hJOXSUTrgaWwm0sh9tIOK8RNVwmarpCxFxD1FJLxFpL2FJN1FxJ1FDM5Nh5IvJT+ESHcfcdwNi2B21LEhFNIQ+mOng0I0RgkdbhUjcy5e7j9rSCp3esPL5jxWHqoK7qBAf3vs3xlLWkJ39AZupaGor3YhoqZtbVyIKnjthYHvGxfILK8/ikBfiVVbiV1ZhHKhkbqcAgKcesrMRjqCbqamTGd5O4t5mY8zqTtgaCxjqClnrCtusEbQ34zTUEzJeZsFQxY6tgxpBPUJKOR3QES3cyutZ9hFR5PJhs5VG8G4F3rBGLsppJfw8L0xLik6PcWTYh7C7jxz/6H/zz9/6CH37vL3nuB/+L//jx3/DRmz/jUn4ybt0NVqJ9rASuEjcWElTmE9JWELM0MmVrIWxqxme4jt/UiM/cQMBylbCjkZi3hYXxThaDPSwGe5gPdBHzthFxNBO03yBov07IXk/EWkPUXEbMXMSMMY+gJB2H8BDOgaN4JZnMOi7z1XwfAr2kFKu2hrmJQW7NK1iaU3P3roP+/su89OIP+NEP/44ffP9v+bu/+Qv++i8FPP+zf+JCVgpOXSfLkRHmnHXEzaVMGcqYNtcQdzQRd3Uw6+lhbryfCVcLEVczEed1JtxNxLwtxP0dzHo7iHvambQ3E7U3E3G0EHG2Eva0EHXfJOqsZ8JeRcx2kVlTAWHFKdyDabhERwlITzHnqOCPCz0I1EMFeK0N3JodZnlOTiIuZ3JSTnNzIW+8+Ut+/vN/4SfP/ZDvffd/8w/f+St+++K/U5yXjlbajlPbzIztKtPGMqJj5UyZa5mxNzPjbCXu7mTG20XM18GUp5VJVxMTzhtM2q8zYbtB1NxE2HidgK6OkOkaEUcLk54Oor42JjwtRF0NTDmqmTSVEjNdIKw4hWvwEM7+VHwjJ5izFPGHRCuCMUkREed1bsXELM4MMxESYTC0UFl5ik2fvMWPf/x9nvvxD/n5z37CmpdeYN+uT7l5rRyjug+T4joR4xX8qgu4ZBfwa8qZsFwn5mghYm7Cq69j2tPKlLuZCWcjE/YGIpYGwsYGxvUNBLX1BDTVhAz1TNqbmXC1EHY2M25rZNxSTdhcybgmn7AmG9/Icay9+7D0JOEcOEhMn8vTyXoE5pEiwuZ6liL9rMyMMhMeQiW9SnnJMVKSNvL273/Db196nt+teZFPN3zA2ZMHab9RhlrajF5ag01aiEOSi1NSgFdZTshQz4TlBiHTNXy6WqLWa0Rt9USsV4mY6wibrhIx1BPWXyM61kBUX8uksZ4Jy3XC5mt4xmpxaKtwaC7i0RThkmbhlWZgF6ZgbN+OqX0ntr4kosoM7geqEFjFxUQM9dwO93MvLmUuNIxe1kBF4VGSdnzA5g1v8e7rv+GDN9ewb8cGcs+kcKMum6Gei8j7CzGIc3HJ8vCqSvEoynDIKnArL+PT1RIYq8OprMClvohXU45XU45fXYlffYmgppaI9ioxQx3TY7VE9LX4NNXYVVVY5Bexygqxy/NwSs7iHD6OqWs3uptbMLZvx967l7DkGLccFxA4RaXMmJt4MCHmXnSEOe8gfkMnzdVZ7N38Fq/9+ie8+IsfsuZX/8bH773E8ZR11JSlIu7OQ9F/HllXOsquE8g70pG2ZqDszsE0VIJDVoFTUY55pACrpACXvAiXvAi3rBSPrJyA4gphZTWT2ktEVOX4FeW45eXY5WVYFaXYFUXYFbm4ZJk4xUfRtW5Ddf1jjK1bsXbvxic+yKz+DAK7uJSEo5VHk6MsBQZIeIXcisrQiGo5vv9D3lvzHK/88ru88JO/5YXn/ooPXvk+pw69w7WKJNpqD9JxeTeNhZu4nPUBV/M3IWo8jm2kFI+iAvNIATZpMU5FCV5VKT51GT5VBT5VFUFNLVHdVYKKMvyyYpySQhySYhyKctyaCtzqUjyqAryybByDR1E3f4a8YQP65k8xt2/D1beXCdlRBDPeDhbDQpajAyxEBliKDjEXHMCuuoqwKYuTyW+x/cOf8eGr/4cNr3+fjW/8Mx+/9k9sf/+HHN3+K5LX/wtJa7/LiW0/p+7cBmQ3T2AduoBjtACHtBC3ogSPshSPugKf9jI+TTVedTVuZTUuxSWc0ou4FSX4VBUE9JcJGqoJj1UzrinHryzGJz2HuecI2pu70d/cjq5pM/obH+Pu3cGs5giCh0sKHt9SsjonYWGin4WJfpYmBog6b2KWVtJ86QAlp9ZzOvlVsg6+TvbBNzm171VOJ/2OC0feoezE+1SdfJemC5tRNB/HMXwBt7QQ23Ae1qHzeJSluBUlOBUXcSkrcKku4VZfwaupw6utwaWqxKOtxK+7RMRQw4S5hgnjFSL6CsKaEkLyPKy9R9E070J/czvG5q0YbmzE1b2VKWkKgq/v6vjDPS2PFuUsRoXMjnexEO4l7u8gbG7ENFKKuOkkHVcOIqw7grjxJH21R+m6nEpfzWFGrh9H3nQMU28WflkxAdVFnKN5mMXZWIZycEgLcMqK/gzgUV3Gq67Gq63Bq615VhVtFaGxS0SMV5gwXmHCUPHsS6rOJyjNxNyVgvb6VsZubMbU/Anm5o24OrcQGdqL4NHCCF+syHmyJOH2VB+J8TYS/hamnU2EjM9szyEpxTiYj22kGLe0AvtwKebBQmziQhyiPOyiXJxDebhH8rAP52IaOINZnI1DkodtJBeHtACPshSftgq/7go+TTUu1SUcikq8mkt4NZUEtBUEtRWEtKWE1AUE5Nn4JRk4+1MxtG5H2/gxumvP9kfGxg9xtW4i0r8LwYPZAb64LeHr21Lux/tZDLWR8DYx7Wggaqpm1tXIhLGaoLaScU0FfsVFnCMF2MR5uIYu4B7JwyY6i0l4CmNfBmN9GRgHMrANZeOS5eFRXMCtKMSjLMGruohHXYFHXYVbVYlb9cxSfaoK/KqL+FSFBBR5+GRn8AwdxTV4AHP7NgzNH6O/thZ9/Xuor7yJ6vIb2BrXEe7ZjuDx3CBf3hriyyUxd6c6SfgaiTuvErPXErPVELPVMGW6TERfQUBVglt6AYf4HHZRDo6hbBxDZ7EOnsbcfxJz/0msg6ewDWfilOTgluUS0pcQ0BTjVhTikBZgkxbiUpbh01YxPnaFce0V/OpKfMpSvIo8vJJMXENHsAuTsHZvx9iyAUPzWgzX3kNT8zry8jXIyn6Lqe49Au2bETyZFfJkXsjj2R5uhZqIOaqZNFcxZakkZq0iMlZKVFfyrKyKPHyyXHzSc7hHsnCIT2EXZWAXZWAbPIllMB2r6CSO4dO4ZefwKs4T1BUT0BTjURXgkhfgVBbjUZcR0FcRNFwmqLvMuKYCn6IIjyQH99Dn2Pr3Y+78DEPbBvQ33kV77Q00Nb9DWfky0osvoSx/BWvDWsbbtyJ4ONPF49keHs50sBxsZMZ+mQlzGZOmZ5oyXGRirIigOo+AModxxTO5Rk5iEqZhEx3HLv4cm+g4xt4jjPWmYRlMxyXNYlyTj0t+Do8ij4CmmHF9KaGxCgJjlfj1FQS05QS1lQTUZXhk+ThHzmAfTMPcvRt960Z0N95H0/B7lNVrkFa+iPTir5GXr0F35S1czRsJ9+xCcH+ylccznTyYamPRV8+0pYKooZSorpiw9gJRfSFhbR5BVTYBxVnGZafxSzLwjqTjHvocu/gYdvExbKKjWAePYRk8hlV0Aos4A+vQKbyK8/jVBYT0F4mYKoiYKgjqy/GpS/AoS/DIi3FK87ENn8UuSsc+mIq1dy/mjs0YW9ahqHmNweL/pPf8TxkqeQH1lTcxNqzF1bKVUO9eBKvRVh5MtXF/spVFXz0xaxUTxotE9YWENPmENOcJqnMYV2bil58mIMvAJ0nHPXwch+gIloHDmPtTMQkPYew7xFhvKoa+wxiFxxkTHieoLSSkKyakv0hIf5FxfSk+TRFuRSFO2QXc8gIcklxsw5k4hk7gFKdh69uHsW0z2utrUda+wXDFbxkpexlt3TvYmjfh6tzOuHA/U5JjCFZCLaxGW1mNtrIcuMas4wpT5nIiY0UEtRcIqnMJKLMIKM4QUJxhXH6KgCwd7+hx3MNHcQ0dwSlOwypK/RbmMKb+I5j6P8c8cAK37BxuWS5ueT5ueT5OeR4O2XlsknNYR3Oe3SU5OEazcA2fwj18HFtfMvqWrSivrkVy5R1GL72Bsu49rK1b8PTtJTBwgNDIEaZVpxEs+2+yEmrhbqSNlVAzi54G4vbLTJrKiIwVEVDm4JWdwSM5iWvkBJ6Rz/+/wJ3iVByiQ1hFqZj7UzH3H8YsPPYMoP8kqs4jaLqPoRdmYBw8g3k4C8tI9p9ll+bglOXgkmbhGj2NY/A4xq5k9Dd3or62GVndRygbNmJs3Y6nP4XA0BECI58TVpxhSn8OwYKvmeXxFu5EOrgbbWcl1MK851krTZrKCChzcEtOYR08gqH3ILqu/Wg7k9B37kXXsQdN2w40bTtQt+5E1bILRctulC37UbUfRNWWymhzCrKWw6i70zEMZGIdPod99FkFbJJszMNncEjP4pJl4hjOwNSdhq41hbHWZMztB7B1peLqO4p/OJ2wPPPZukWdQ1Sfx5SlCMGcu5kFXzMr4Q7uT/Vwf6Kb5UATs/aaZxCGYsZV53AOp2PoPYi6fS+Kll0omrchb9rCSMNGRq5tYKThE0avbWH0+jZkTXuRtySjbE1F232Csb4MTKIsbCO5OKX5uOUFuBT5uBTnMY+cwiY9jVOWiXXoFKa+4xi7jmHvO4l3MJOINI+o4gJT6iIm9UVEdAUE9QWExgqJmIsQxJxNxN1N3Brv4GFMyMNYH7eCN5l11jJtrWLKXE5YX4hPnol9+Dgm4SH03cmoO3ajaH0GIW/+FOXN7ahbd6Pt3I+hJ+3bR+00Lsl5XJJ8XJICXJKCPwN4FIV4VPk4FWdxKTNxK7LwyHPwSXIJSi8QlRcxpSojrqsiPnaJ2NglpgwVhPQl+PRFeHUFuLTnEUzaGpl23GDJ38bDmJBHM/3fAlxl2lpFSFdMQJ2HT56FS5KOXXwMc38q+p4kNJ270HXsQt+1E2PPfqz9qdjFx/BKMgko8gipCwmoSvApinBJCrAPfzsbSfJwywvwqi8QNFzAqz+HX5NLSFdEzFTBnLWaJUs9C6Y65k1XmbPUM2e5SsxSTdR0iYCxFLe2AKs6l/8HXK32/y5m8HIAAAAASUVORK5CYII=" />
+ <canvas class="canvas" width="600" height="600"></canvas>
+ <script type="text/javascript">
+ "use strict";
+
+ let context = document.querySelector(".canvas").getContext("2d");
+ context.beginPath();
+ context.moveTo(300, 0);
+ context.lineTo(600, 600);
+ context.lineTo(0, 600);
+ context.closePath();
+ context.fillStyle = "#ffc821";
+ context.fill();
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/inspector/markup/test/doc_markup_image_and_canvas_2.html b/devtools/client/inspector/markup/test/doc_markup_image_and_canvas_2.html
new file mode 100644
index 000000000..adae9ce21
--- /dev/null
+++ b/devtools/client/inspector/markup/test/doc_markup_image_and_canvas_2.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html class="html">
+ <head class="head">
+ <meta charset=utf-8 />
+ <title>Image and Canvas markup-view test</title>
+ </head>
+ <body>
+ <img class="local" src="chrome://branding/content/about-logo.png" />
+ <img class="data" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAADI5JREFUeNrsWwuQFNUVPf1m5z87szv7HWSWj8CigBFMEFZKiQsB1PgJwUAZg1HBpIQsKmokEhNjWUnFVPnDWBT+KolJYbRMoqUVq0yCClpqiX8sCchPWFwVlt2db7+X93pez7zu6Vn2NxsVWh8987p7pu+9555z7+tZjTGGY3kjOMa34w447oBjfKsY7i/UNM3Y8eFSAkD50Plgw03K5P9gvGv7U5ieeR3PszeREiPNX3/0DL4hjslzhm8THh+OITfXk3dhiv4GDtGPVzCaeJmPLYzuu5qJuWfuw2QTlcN1X9pwQU7LhdZ/ZAseD45cOh9hHvDkc/yAF/DNhdb5Mrr3PvBMaAYW8fMSIi2G497IMEK/YutGtAYr6+ej+nxu/NN8Ks3N7AR6HgcLz0Eg1Ljg1UcxZzi5qewIkMYLRweTr2Kzp+nmyXAd5pS3XQDd+N/4h4zgu9FI7brlXf90nMEnuwQxlvv+hosE3TuexmWeysmT4W+WxkMaLzf9Y8ATgjcUn7T9H1gqrpFq8eV1gMn6t16NhngjfoX6q4DUP032Rd4LJgpSLwJ1yzFqBG69eRkah0MVyo0Acfe+yy9AG4nMiYCkeM53KKFXncBLAXqEm+wCqZwaueq7WCmuLTcKSJmj737ol2hurA9eq9VdyiO8yWa3NNyog+SB5CZodSsQq/dfu34tJpYbBaTMzvVddDZu16q5smXf4G8zEvqm4cyaAmJPuTJk3oJWdS4WzcVtfMZbThSQckb/pYfRGgo3zNOqZnEHbJPGK4abaDCQIIsT8V/qTaBqHkLh6LzXH8XZQhbLhYKyyCC/WeHYcNdmvOgfe8skzbWL270/T3wf7tSx/lGCbTu8xlzzmCSWLc5iwmgikcCHi3Mga0Ry913vBFvQwg90l6M4ImWKfsWOp7DSWxmfpPlCFuPFfsNfKrCnPYpQKIRgqBK7D0SxYaNHwkEiJMtl0ReDp3Lc5D3PGoTo/sKngCl7a5chFqvBatKwjBd7WwqIlzB/78NcoUcp5VSgGxm+7b8eqQRGnHMO634epO4S1EZww09/iFg5UmGoESDuznP1xVhTUX1WWHPzjpd25wyH0hRxI3LGM75nxmuNEEUVpAN0XgxmPoKralakbQnWlIMQyVBD/w+3orkq4lvualjKyWwzt4MaxqspQHVhPOWG64bxYuhZXSFGWhipbSDVragOu5Y9eAsmDDUKyBA703vemVhHoueD6e9wAzJK1WfmN0Umk5GGM4kEMZcuIECqgjm0nldAqmbjwtm4VxZH5AvlADP6mx9Eqy9Q0+KqW8Ch+47FaMMYmnNGfY1iPMshoC6qFxme4wQ+0p+ARE6H3+9veWEDWgUhDhUKyFARn4jM5BNxT0XsMg7bfymGK1ov3wtjDfhL4w0HVGUVBEjDaaE+QNdrcNWch1PG4W6xrjBUXECGivg++Cva3JUT4iQUz3V2RsSVaKLwOuDT89A3HdBQoxhNC+fnVm74ual2EG893P6G+PuP4SfiO4cCBWQooL9qCWKNXPbcI37Aa/lnlZxXRt4RFONGwSDCPAHqOuqjWct1QiEMw5mChM5X4K47FyNqcd3aK9AwFH0CGYLoe1ctxk2eWi57rg5JfGp9rzC6ggCdFlAgHBDw5Yxlcg6G8SyHCjMlsgmDD9zhSeHlF+JnAgWDTQUy2NxfdwOao1UVV3pi3+bE97YSbWpLAbn6zefHNQkp1PMpIBwwvslKgIYTKM2nEpNzrGcH3FXTEal0L38kJ4uDQgEZbO4vnI173LXf5NHZaiUxtaCxyZuo/rK6LpUg54yg3zTWRAArvDcRIPZ6BqzrQ1REpmL+DNw32OKIDCb3X1qPVn8wNNMT4w2bvs+q4bAZrqBh2skaL3yyhhIIZ4i6oHkUK0RckcB8GigEyRIH4A6Mgc8fatl0/+BkkQxC9gIT4ljna1rIZW9rEdNbjJcNjsnoYj7LHWCUwpITzEgzRQKZ3XAFHbTzA3hrz8TEUUZxFBhoKpABQt/97p+w0hMZG68I8R6FtlsJT3FELndZntjM+VMnylKYq8GJI3UZaRMpquGSGFVOEfv0YZBMNzz+uvjbfzS6xQERIhlI9FcvQWNdFVb7x1zCb+QNK8vb9NsiifmI5hBgVoOCBC1sb0ab5RomqENxLO3eA1/0NDRU47q2RQNbRCUDIb7lF2CNL3ZGxEV4n08TVvZWYG4pZyV0zUdS45tyCBByOHWiyvZmxFXDCyRo1ge5+Sy0TA+8lWMiP/6O0S32exGV9Jf4fr8azdUR3zL/CZz4MtvzdX5uOYs6NDOmpkuj5Huh+7qUQSYl0ThHzw0YQzcGo6bhzEqoYq5rN3yRiYiG3Vfe2Ybm/qKA9NNZ3nNm4F7/yDkg9AN+U1mHiBcXP8zuDN76jj8hg1QyiWQigalj02BJPhK8I0zxijAjhp5zhlpLUDvS+BCy2HMAvvB4XDgL9/SXC0g/ou/5+6/xLX8w0uJrOIkXfPvyhY0F6gr7M8H0KWFYikcqAXakB+xwD9CdREBLoau7Gz3cAdSIdLFxFtJTCqRChSjnutvhDcREtzjz2Tswtz+yeNRFUeXZXtWux7C1fuoVcbd3J//ipDX3uZZDLGrwweS+UBLL5TDliVBnF8P7H+XI8aRRGsIBJg/Zlslt1+W+D1JWoSyi+kD9jfhs78t7mhZhSl+fLfY1Bdyv3I8V/qpY3B1McgN7ZFT5/vNO0I5DPLLdPBIJA8qc4h2I0QplYfDpJwHT+aj0246r5S8rToG8OjCle8wk4OLvvYGa+Ovr84uo2qBSwJS9G5egoZFLTfiEqWDtbwGfHgKOdPHcS+ai7XDzMPW/FJRLGGcxnBbK4YJC2K+h+T6Bdu5CqHqCWERd3bawb7JI+iJ735+LNaHaprBLLHBm08U3XxShEsdt+f3eTh3v7aC95Dct4RCWL5OZWh/oXBZThxAIxyOXLzBk8aiEWJID8rK3CpPOmeHaGpvCS+7EHv5FujVHUSJPLXvIFeHcNc+9xrB2gws9KZdxuLFax/WLM5gzzSm/lTXF/OdAcapyvjxPqxqHjr2v4ckX2bS2dRBrc5lSdpKjEJ9/9tdwX2WMd53ZQ2IVo3RES+UwVSpCPvYepNx4gmTGDUKIMQ4eduPnD7mx9xOn/KZKOlFbStjONxHTtR+BYAPmnoZ1Zp8wkBRwP/EL3u0F/C2hGl7vpz7vW37T3vP7if8wroKuoh8ribknX9BK5rcF+mo1qKaKyRPJTgTDjbzY8szcuLb3bpH00u35T47j7prRpwDJTxzyG0dHgxPp5bPG8VdkpfPbUg3SgoOo2mwVukb98D5EqpswZTTulCggTk4gpYhv0++wIhCJxr0+Hq1sondis0SE2oxQe3qWXwWyO4DSQg9gJ8Iiw1VFcGqXxet0N9xE4ygIxv/9W6wo9WyROEX/R+eiobYSq2vHTOR631Eiv2lRfh9dvxkumkXh92Qsx8XrAJ+7YGbWuhxOi/U+31NQmzyqNYG8N/3wfo6CRtRHcN01FzkvojohwLu0VVvDa56IS/xcj2b7nN+O+m0jqpE1wMPXZxAN9iCVThtDvH7gmiRGRpU8Lspv1Uhq4wIVdQoyuGSLNYPKUCS8+CzNURbzMmjK3i8u0U793lmuV0ef9nWQ5MGC/DiUqEUSaCtXna9RJEspZS1lrXINK/pcq+SpT50t98QKMq1FRmDfx3vxty102k0PM4ssEnvuz5+G26Ij4yDpz6z9fV8bkyIkqBFkhej0Ib+ZQ34XJK9AfozaiimqIoX3Jp3tiISrcfYpuN2+iFph/02P36PNC9fVcCnp6H9jYouKyfaWufz5Tp9tVxcUniw7IohZv4dZz81/ns67z3AYPrc2n0+Ix2q8k0PWjgBy88XaibnfK9A+5LdDY2Ivhy36fbT8Zv3Lb1U1qLqUxorXEEXIs0mjjrtxoTZWtdvigNs2sgPiujTv6DIZLld6b/V5742JZV3fUsUVFy5gdsNtKWFzUCEVbNepD1MkSMVbsb6SZm7jI3/zODtQKgUMsOw8wDZ63t5xcV1TnaEAxoc6wrqY+Fj+N4DsqOnhOIdicrQSm1MPYCPlIqHn5bbHg8/bj2D3QfZnCX3mpAICDZV8jH5kpbZqTD0W+DxaA74CWzLN2nd14OlL72J38Lf7+TjC7dadZFDoZJQPrtaIKL/G0L6ktptPZVJ8fMqHYPZOKYPMyQGadIJfDvdXwAFiZOTvDBPydf5vk4rWA+RfdhBlaF/yDDBRoMu9pfnSjv/p7DG+HXfAcQcc49v/BBgAcFAO4DmB2GQAAAAASUVORK5CYII=" />
+ <img class="remote" src="http://example.com/browser/devtools/client/inspector/markup/test/doc_markup_tooltip.png" />
+ <canvas class="canvas" width="600" height="600"></canvas>
+ <script type="text/javascript">
+ "use strict";
+
+ let context = document.querySelector(".canvas").getContext("2d");
+ context.beginPath();
+ context.moveTo(300, 0);
+ context.lineTo(600, 600);
+ context.lineTo(0, 600);
+ context.closePath();
+ context.fillStyle = "#ffc821";
+ context.fill();
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/inspector/markup/test/doc_markup_links.html b/devtools/client/inspector/markup/test/doc_markup_links.html
new file mode 100644
index 000000000..f393319f8
--- /dev/null
+++ b/devtools/client/inspector/markup/test/doc_markup_links.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Markup-view links</title>
+ <link rel="stylesheet" type="text/css" href="style.css">
+ <link rel="icon" type="image/png" sizes="196x196" href="/media/img/firefox/favicon-196.223e1bcaf067.png">
+ </head>
+ <body>
+ <form id="message-form" method="post" action="/post_message">
+ <p for="invalid-idref">
+ <label for="name">Name</label>
+ <input id="name" type="text" />
+ </p>
+ <p>
+ <label for="message">Message</label>
+ <input id="message" type="text" />
+ </p>
+ <p>
+ <button>Send message</button>
+ </p>
+ <output form="message-form" for="name message invalid">Thank you for your message!</output>
+ </form>
+ <a href="/go/somewhere/else" ping="/analytics?page=pageA /analytics?user=test">Click me, I'm a link</a>
+ <ul>
+ <li contextmenu="menu1">Item 1</li>
+ <li contextmenu="menu2">Item 2</li>
+ <li contextmenu="menu3">Item 3</li>
+ </ul>
+ <menu type="context" id="menu1">
+ <menuitem label="custom menu 1"></menuitem>
+ </menu>
+ <menu type="context" id="menu2">
+ <menuitem label="custom menu 2"></menuitem>
+ </menu>
+ <menu type="context" id="menu3">
+ <menuitem label="custom menu 3"></menuitem>
+ </menu>
+ <video controls poster="doc_markup_tooltip.png" src="code-rush.mp4"></video>
+ <script type="text/javascript" src="lib_jquery_1.0.js"></script>
+ </body>
+</html>
diff --git a/devtools/client/inspector/markup/test/doc_markup_mutation.html b/devtools/client/inspector/markup/test/doc_markup_mutation.html
new file mode 100644
index 000000000..f021c9fcf
--- /dev/null
+++ b/devtools/client/inspector/markup/test/doc_markup_mutation.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+
+<html class="html">
+ <style type="text/css">
+ #node1.pseudo::after {
+ content: "after";
+ }
+ </style>
+
+ <body class="body">
+ <div class="node0">
+ <div id="node1" class="node1">line1</div>
+ <div id="node2" class="node2">line2</div>
+ <p class="node3">line3</p>
+ <!-- A comment -->
+ <p id="node4" class="node4">line4
+ <span class="node5">line5</span>
+ <span class="node6">line6</span>
+ <!-- A comment -->
+ <a class="node7">line7<span class="node8">line8</span></a>
+ <span class="node9">line9</span>
+ <span class="node10">line10</span>
+ <span class="node11">line11</span>
+ <a class="node12">line12<span class="node13">line13</span></a>
+ </p>
+ <p id="node14">line14</p>
+ <p class="node15">line15</p>
+ </div>
+ <div id="node16">
+ <p id="node17">line17</p>
+ </div>
+ <div id="node18">
+ <div id="node19">
+ <div id="node20">
+ <div id="node21">
+ line21
+ </div>
+ </div>
+ </div>
+ </div>
+ </body>
+</html>
diff --git a/devtools/client/inspector/markup/test/doc_markup_navigation.html b/devtools/client/inspector/markup/test/doc_markup_navigation.html
new file mode 100644
index 000000000..9633052e1
--- /dev/null
+++ b/devtools/client/inspector/markup/test/doc_markup_navigation.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+
+<html class="html">
+ <head class="head">
+ <meta charset=utf-8 />
+ </head>
+
+ <body class="body">
+ <div class="node0">
+ <p class="node1">line1</p>
+ <p class="node2">line2</p>
+ <p class="node3">line3</p>
+ <!-- A comment -->
+ <p class="node4">line4
+ <span class="node5">line5</span>
+ <span class="node6">line6</span>
+ <!-- A comment -->
+ <a class="node7">line7<span class="node8">line8</span></a>
+ <span class="node9">line9</span>
+ <span class="node10">line10</span>
+ <span class="node11">line11</span>
+ <a class="node12">line12<span class="node13">line13</span></a>
+ </p>
+ <p class="node14">line14</p>
+ <p class="node15">line15</p>
+ </div>
+ </body>
+</html>
diff --git a/devtools/client/inspector/markup/test/doc_markup_not_displayed.html b/devtools/client/inspector/markup/test/doc_markup_not_displayed.html
new file mode 100644
index 000000000..20a4b9415
--- /dev/null
+++ b/devtools/client/inspector/markup/test/doc_markup_not_displayed.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <style>
+ #hidden-via-stylesheet {
+ display: none;
+ }
+ </style>
+</head>
+<body>
+ <div id="normal-div"></div>
+ <div id="display-none" style="display:none;"></div>
+ <div id="hidden-true" hidden="true"></div>
+ <div id="hidden-via-hide-shortcut" class="__fx-devtools-hide-shortcut__"></div>
+ <div id="visibility-hidden" style="visibility:hidden;"></div>
+ <div id="hidden-via-stylesheet"></div>
+</body>
+</html>
diff --git a/devtools/client/inspector/markup/test/doc_markup_pagesize_01.html b/devtools/client/inspector/markup/test/doc_markup_pagesize_01.html
new file mode 100644
index 000000000..8323f0b2e
--- /dev/null
+++ b/devtools/client/inspector/markup/test/doc_markup_pagesize_01.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+
+<html class="html">
+ <body class="body">
+ <div id="a"></div>
+ <div id="b"></div>
+ <div id="c"></div>
+ <div id="d"></div>
+ <div id="e"></div>
+ <div id="f"></div>
+ <div id="g"></div>
+ <div id="h"></div>
+ <div id="i"></div>
+ <div id="j"></div>
+ <div id="k"></div>
+ <div id="l"></div>
+ <div id="m"></div>
+ <div id="n"></div>
+ <div id="o"></div>
+ <div id="p"></div>
+ <div id="q"></div>
+ <div id="r"></div>
+ <div id="s"></div>
+ <div id="t"></div>
+ <div id="u"></div>
+ <div id="v"></div>
+ <div id="w"></div>
+ <div id="x"></div>
+ <div id="y"></div>
+ <div id="z"></div>
+ </body>
+</html>
diff --git a/devtools/client/inspector/markup/test/doc_markup_pagesize_02.html b/devtools/client/inspector/markup/test/doc_markup_pagesize_02.html
new file mode 100644
index 000000000..db2502c89
--- /dev/null
+++ b/devtools/client/inspector/markup/test/doc_markup_pagesize_02.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+
+<html class="html">
+ <body class="body">
+ <ul>
+ <li>some content</li>
+ <li>some content</li>
+ <li>some content</li>
+ <li>some content</li>
+ <li>some content</li>
+ <li>some content</li>
+ <li>some content</li>
+ <li>some content</li>
+ <li>some content</li>
+ <li>some content</li>
+ <li>some content</li>
+ <li>some content</li>
+ <li>some content</li>
+ <li>some content</li>
+ <li>some content</li>
+ <li>some content</li>
+ <li>some content</li>
+ <li>some content</li>
+ <li>some content</li>
+ <li>some content</li>
+ <li>some content</li>
+ <li>some content</li>
+ <li>some content</li>
+ <li>some content</li>
+ <li>some content</li>
+ </ul>
+ </body>
+</html>
diff --git a/devtools/client/inspector/markup/test/doc_markup_search.html b/devtools/client/inspector/markup/test/doc_markup_search.html
new file mode 100644
index 000000000..08c047bcc
--- /dev/null
+++ b/devtools/client/inspector/markup/test/doc_markup_search.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<head></head>
+<body>
+ <ul>
+ <li>
+ <span>this is an <em>important</em> node</span>
+ </li>
+ </ul>
+</body>
+</html> \ No newline at end of file
diff --git a/devtools/client/inspector/markup/test/doc_markup_svg_attributes.html b/devtools/client/inspector/markup/test/doc_markup_svg_attributes.html
new file mode 100644
index 000000000..04b699be7
--- /dev/null
+++ b/devtools/client/inspector/markup/test/doc_markup_svg_attributes.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <svg viewBox="0 0 2 2" width=200 height=200>
+ <circle cx=1 cy=1 r=1 fill=lime />
+ </svg>
+ </body>
+</html>
diff --git a/devtools/client/inspector/markup/test/doc_markup_toggle.html b/devtools/client/inspector/markup/test/doc_markup_toggle.html
new file mode 100644
index 000000000..521db100c
--- /dev/null
+++ b/devtools/client/inspector/markup/test/doc_markup_toggle.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <title>Expanding and collapsing markup-view containers</title>
+</head>
+<body>
+ <ul>
+ <li>
+ <span>list <em>item<!-- force expand --></em></span>
+ </li>
+ <li>
+ <span>list <em>item<!-- force expand --></em></span>
+ </li>
+ <li>
+ <span>list <em>item<!-- force expand --></em></span>
+ </li>
+ <li>
+ <span>list <em>item<!-- force expand --></em></span>
+ </li>
+ <li>
+ <span>list <em>item<!-- force expand --></em></span>
+ </li>
+ <li>
+ <span>list <em>item<!-- force expand --></em></span>
+ </li>
+ </ul>
+</body>
+</html>
diff --git a/devtools/client/inspector/markup/test/doc_markup_tooltip.png b/devtools/client/inspector/markup/test/doc_markup_tooltip.png
new file mode 100644
index 000000000..699ef7940
--- /dev/null
+++ b/devtools/client/inspector/markup/test/doc_markup_tooltip.png
Binary files differ
diff --git a/devtools/client/inspector/markup/test/doc_markup_void_elements.html b/devtools/client/inspector/markup/test/doc_markup_void_elements.html
new file mode 100644
index 000000000..72a937980
--- /dev/null
+++ b/devtools/client/inspector/markup/test/doc_markup_void_elements.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html class="html">
+ <head class="head">
+ <meta charset=utf-8 />
+ <style>
+ .before:before {
+ content: "before";
+ }
+ </style>
+ </head>
+ <body class="body">
+ <h1>Test void elements in HTML document</h1>
+ <img>
+ <hr>
+ <hr class="before">
+ <br>
+ </body>
+</html>
diff --git a/devtools/client/inspector/markup/test/doc_markup_void_elements.xhtml b/devtools/client/inspector/markup/test/doc_markup_void_elements.xhtml
new file mode 100644
index 000000000..331346b24
--- /dev/null
+++ b/devtools/client/inspector/markup/test/doc_markup_void_elements.xhtml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html
+ PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+ <head class="head">
+ <meta charset="utf-8" />
+ <style>
+ .before:before {
+ content: "before";
+ }
+ </style>
+ </head>
+ <body class="body">
+ <h1>Test void elements in XHTML document</h1>
+ <hr class="before" />
+ <img />
+ <hr />
+ <br />
+ </body>
+</html>
diff --git a/devtools/client/inspector/markup/test/doc_markup_whitespace.html b/devtools/client/inspector/markup/test/doc_markup_whitespace.html
new file mode 100644
index 000000000..9071c802d
--- /dev/null
+++ b/devtools/client/inspector/markup/test/doc_markup_whitespace.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <style>
+ #pre {
+ white-space: pre;
+ }
+ </style>
+ </head>
+ <body>
+ <div>div 1</div>
+ <div>div 2</div>
+ <div>div 3</div>
+ <div id="inline">
+ <img src="chrome://branding/content/about-logo.png" />
+ <img src="chrome://branding/content/about-logo.png" />
+ <img src="chrome://branding/content/about-logo.png" />
+ </div>
+ <div id="pre">
+ <img src="chrome://branding/content/about-logo.png" />
+ <img src="chrome://branding/content/about-logo.png" />
+ </div>
+ </body>
+</html>
diff --git a/devtools/client/inspector/markup/test/doc_markup_xul.xul b/devtools/client/inspector/markup/test/doc_markup_xul.xul
new file mode 100644
index 000000000..34f13dae0
--- /dev/null
+++ b/devtools/client/inspector/markup/test/doc_markup_xul.xul
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<xul:window xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Test Bug 984442">
+
+ <xul:panel id="test"></xul:panel>
+
+</xul:window>
diff --git a/devtools/client/inspector/markup/test/head.js b/devtools/client/inspector/markup/test/head.js
new file mode 100644
index 000000000..f7d55a272
--- /dev/null
+++ b/devtools/client/inspector/markup/test/head.js
@@ -0,0 +1,653 @@
+/* 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/. */
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+/* import-globals-from ../../test/head.js */
+"use strict";
+
+// Import the inspector's head.js first (which itself imports shared-head.js).
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/inspector/test/head.js",
+ this);
+
+var {getInplaceEditorForSpan: inplaceEditor} = require("devtools/client/shared/inplace-editor");
+var clipboard = require("sdk/clipboard");
+var {ActorRegistryFront} = require("devtools/shared/fronts/actor-registry");
+
+// If a test times out we want to see the complete log and not just the last few
+// lines.
+SimpleTest.requestCompleteLog();
+
+// Set the testing flag on DevToolsUtils and reset it when the test ends
+flags.testing = true;
+registerCleanupFunction(() => {
+ flags.testing = false;
+});
+
+// Clear preferences that may be set during the course of tests.
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("devtools.inspector.htmlPanelOpen");
+ Services.prefs.clearUserPref("devtools.inspector.sidebarOpen");
+ Services.prefs.clearUserPref("devtools.markup.pagesize");
+ Services.prefs.clearUserPref("dom.webcomponents.enabled");
+ Services.prefs.clearUserPref("devtools.inspector.showAllAnonymousContent");
+});
+
+/**
+ * Some tests may need to import one or more of the test helper scripts.
+ * A test helper script is simply a js file that contains common test code that
+ * is either not common-enough to be in head.js, or that is located in a
+ * separate directory.
+ * The script will be loaded synchronously and in the test's scope.
+ * @param {String} filePath The file path, relative to the current directory.
+ * Examples:
+ * - "helper_attributes_test_runner.js"
+ * - "../../../commandline/test/helpers.js"
+ */
+function loadHelperScript(filePath) {
+ let testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/"));
+ Services.scriptloader.loadSubScript(testDir + "/" + filePath, this);
+}
+
+/**
+ * Reload the current page
+ * @return a promise that resolves when the inspector has emitted the event
+ * new-root
+ */
+function reloadPage(inspector, testActor) {
+ info("Reloading the page");
+ let newRoot = inspector.once("new-root");
+ testActor.reload();
+ return newRoot;
+}
+
+/**
+ * Get the MarkupContainer object instance that corresponds to the given
+ * NodeFront
+ * @param {NodeFront} nodeFront
+ * @param {InspectorPanel} inspector The instance of InspectorPanel currently
+ * loaded in the toolbox
+ * @return {MarkupContainer}
+ */
+function getContainerForNodeFront(nodeFront, {markup}) {
+ return markup.getContainer(nodeFront);
+}
+
+/**
+ * Get the MarkupContainer object instance that corresponds to the given
+ * selector
+ * @param {String|NodeFront} selector
+ * @param {InspectorPanel} inspector The instance of InspectorPanel currently
+ * loaded in the toolbox
+ * @return {MarkupContainer}
+ */
+var getContainerForSelector = Task.async(function* (selector, inspector) {
+ info("Getting the markup-container for node " + selector);
+ let nodeFront = yield getNodeFront(selector, inspector);
+ let container = getContainerForNodeFront(nodeFront, inspector);
+ info("Found markup-container " + container);
+ return container;
+});
+
+/**
+ * Retrieve the nodeValue for the firstChild of a provided selector on the content page.
+ *
+ * @param {String} selector
+ * @param {TestActorFront} testActor The current TestActorFront instance.
+ * @return {String} the nodeValue of the first
+ */
+function* getFirstChildNodeValue(selector, testActor) {
+ let nodeValue = yield testActor.eval(`
+ content.document.querySelector("${selector}").firstChild.nodeValue;
+ `);
+ return nodeValue;
+}
+
+/**
+ * Using the markupview's _waitForChildren function, wait for all queued
+ * children updates to be handled.
+ * @param {InspectorPanel} inspector The instance of InspectorPanel currently
+ * loaded in the toolbox
+ * @return a promise that resolves when all queued children updates have been
+ * handled
+ */
+function waitForChildrenUpdated({markup}) {
+ info("Waiting for queued children updates to be handled");
+ let def = defer();
+ markup._waitForChildren().then(() => {
+ executeSoon(def.resolve);
+ });
+ return def.promise;
+}
+
+/**
+ * Simulate a click on the markup-container (a line in the markup-view)
+ * that corresponds to the selector passed.
+ * @param {String|NodeFront} selector
+ * @param {InspectorPanel} inspector The instance of InspectorPanel currently
+ * loaded in the toolbox
+ * @return {Promise} Resolves when the node has been selected.
+ */
+var clickContainer = Task.async(function* (selector, inspector) {
+ info("Clicking on the markup-container for node " + selector);
+
+ let nodeFront = yield getNodeFront(selector, inspector);
+ let container = getContainerForNodeFront(nodeFront, inspector);
+
+ let updated = container.selected
+ ? promise.resolve()
+ : inspector.once("inspector-updated");
+ EventUtils.synthesizeMouseAtCenter(container.tagLine, {type: "mousedown"},
+ inspector.markup.doc.defaultView);
+ EventUtils.synthesizeMouseAtCenter(container.tagLine, {type: "mouseup"},
+ inspector.markup.doc.defaultView);
+ return updated;
+});
+
+/**
+ * Focus a given editable element, enter edit mode, set value, and commit
+ * @param {DOMNode} field The element that gets editable after receiving focus
+ * and <ENTER> keypress
+ * @param {String} value The string value to be set into the edited field
+ * @param {InspectorPanel} inspector The instance of InspectorPanel currently
+ * loaded in the toolbox
+ */
+function setEditableFieldValue(field, value, inspector) {
+ field.focus();
+ EventUtils.sendKey("return", inspector.panelWin);
+ let input = inplaceEditor(field).input;
+ ok(input, "Found editable field for setting value: " + value);
+ input.value = value;
+ EventUtils.sendKey("return", inspector.panelWin);
+}
+
+/**
+ * Focus the new-attribute inplace-editor field of a node's markup container
+ * and enters the given text, then wait for it to be applied and the for the
+ * node to mutates (when new attribute(s) is(are) created)
+ * @param {String} selector The selector for the node to edit.
+ * @param {String} text The new attribute text to be entered (e.g. "id='test'")
+ * @param {InspectorPanel} inspector The instance of InspectorPanel currently
+ * loaded in the toolbox
+ * @return a promise that resolves when the node has mutated
+ */
+var addNewAttributes = Task.async(function* (selector, text, inspector) {
+ info(`Entering text "${text}" in new attribute field for node ${selector}`);
+
+ let container = yield focusNode(selector, inspector);
+ ok(container, "The container for '" + selector + "' was found");
+
+ info("Listening for the markupmutation event");
+ let nodeMutated = inspector.once("markupmutation");
+ setEditableFieldValue(container.editor.newAttr, text, inspector);
+ yield nodeMutated;
+});
+
+/**
+ * Checks that a node has the given attributes.
+ *
+ * @param {String} selector The selector for the node to check.
+ * @param {Object} expected An object containing the attributes to check.
+ * e.g. {id: "id1", class: "someclass"}
+ * @param {TestActorFront} testActor The current TestActorFront instance.
+ *
+ * Note that node.getAttribute() returns attribute values provided by the HTML
+ * parser. The parser only provides unescaped entities so &amp; will return &.
+ */
+var assertAttributes = Task.async(function* (selector, expected, testActor) {
+ let {attributes: actual} = yield testActor.getNodeInfo(selector);
+
+ is(actual.length, Object.keys(expected).length,
+ "The node " + selector + " has the expected number of attributes.");
+ for (let attr in expected) {
+ let foundAttr = actual.find(({name}) => name === attr);
+ let foundValue = foundAttr ? foundAttr.value : undefined;
+ ok(foundAttr, "The node " + selector + " has the attribute " + attr);
+ is(foundValue, expected[attr],
+ "The node " + selector + " has the correct " + attr + " attribute value");
+ }
+});
+
+/**
+ * Undo the last markup-view action and wait for the corresponding mutation to
+ * occur
+ * @param {InspectorPanel} inspector The instance of InspectorPanel currently
+ * loaded in the toolbox
+ * @return a promise that resolves when the markup-mutation has been treated or
+ * rejects if no undo action is possible
+ */
+function undoChange(inspector) {
+ let canUndo = inspector.markup.undo.canUndo();
+ ok(canUndo, "The last change in the markup-view can be undone");
+ if (!canUndo) {
+ return promise.reject();
+ }
+
+ let mutated = inspector.once("markupmutation");
+ inspector.markup.undo.undo();
+ return mutated;
+}
+
+/**
+ * Redo the last markup-view action and wait for the corresponding mutation to
+ * occur
+ * @param {InspectorPanel} inspector The instance of InspectorPanel currently
+ * loaded in the toolbox
+ * @return a promise that resolves when the markup-mutation has been treated or
+ * rejects if no redo action is possible
+ */
+function redoChange(inspector) {
+ let canRedo = inspector.markup.undo.canRedo();
+ ok(canRedo, "The last change in the markup-view can be redone");
+ if (!canRedo) {
+ return promise.reject();
+ }
+
+ let mutated = inspector.once("markupmutation");
+ inspector.markup.undo.redo();
+ return mutated;
+}
+
+/**
+ * Get the selector-search input box from the inspector panel
+ * @return {DOMNode}
+ */
+function getSelectorSearchBox(inspector) {
+ return inspector.panelWin.document.getElementById("inspector-searchbox");
+}
+
+/**
+ * Using the inspector panel's selector search box, search for a given selector.
+ * The selector input string will be entered in the input field and the <ENTER>
+ * keypress will be simulated.
+ * This function won't wait for any events and is not async. It's up to callers
+ * to subscribe to events and react accordingly.
+ */
+function searchUsingSelectorSearch(selector, inspector) {
+ info("Entering \"" + selector + "\" into the selector-search input field");
+ let field = getSelectorSearchBox(inspector);
+ field.focus();
+ field.value = selector;
+ EventUtils.sendKey("return", inspector.panelWin);
+}
+
+/**
+ * Check to see if the inspector menu items for editing are disabled.
+ * Things like Edit As HTML, Delete Node, etc.
+ * @param {NodeFront} nodeFront
+ * @param {InspectorPanel} inspector
+ * @param {Boolean} assert Should this function run assertions inline.
+ * @return A promise that resolves with a boolean indicating whether
+ * the menu items are disabled once the menu has been checked.
+ */
+var isEditingMenuDisabled = Task.async(
+function* (nodeFront, inspector, assert = true) {
+ // To ensure clipboard contains something to paste.
+ clipboard.set("<p>test</p>", "html");
+
+ yield selectNode(nodeFront, inspector);
+ let allMenuItems = openContextMenuAndGetAllItems(inspector);
+
+ let deleteMenuItem = allMenuItems.find(i => i.id === "node-menu-delete");
+ let editHTMLMenuItem = allMenuItems.find(i => i.id === "node-menu-edithtml");
+ let pasteHTMLMenuItem = allMenuItems.find(i => i.id === "node-menu-pasteouterhtml");
+
+ if (assert) {
+ ok(deleteMenuItem.disabled, "Delete menu item is disabled");
+ ok(editHTMLMenuItem.disabled, "Edit HTML menu item is disabled");
+ ok(pasteHTMLMenuItem.disabled, "Paste HTML menu item is disabled");
+ }
+
+ return deleteMenuItem.disabled &&
+ editHTMLMenuItem.disabled &&
+ pasteHTMLMenuItem.disabled;
+});
+
+/**
+ * Check to see if the inspector menu items for editing are enabled.
+ * Things like Edit As HTML, Delete Node, etc.
+ * @param {NodeFront} nodeFront
+ * @param {InspectorPanel} inspector
+ * @param {Boolean} assert Should this function run assertions inline.
+ * @return A promise that resolves with a boolean indicating whether
+ * the menu items are enabled once the menu has been checked.
+ */
+var isEditingMenuEnabled = Task.async(
+function* (nodeFront, inspector, assert = true) {
+ // To ensure clipboard contains something to paste.
+ clipboard.set("<p>test</p>", "html");
+
+ yield selectNode(nodeFront, inspector);
+ let allMenuItems = openContextMenuAndGetAllItems(inspector);
+
+ let deleteMenuItem = allMenuItems.find(i => i.id === "node-menu-delete");
+ let editHTMLMenuItem = allMenuItems.find(i => i.id === "node-menu-edithtml");
+ let pasteHTMLMenuItem = allMenuItems.find(i => i.id === "node-menu-pasteouterhtml");
+
+ if (assert) {
+ ok(!deleteMenuItem.disabled, "Delete menu item is enabled");
+ ok(!editHTMLMenuItem.disabled, "Edit HTML menu item is enabled");
+ ok(!pasteHTMLMenuItem.disabled, "Paste HTML menu item is enabled");
+ }
+
+ return !deleteMenuItem.disabled &&
+ !editHTMLMenuItem.disabled &&
+ !pasteHTMLMenuItem.disabled;
+});
+
+/**
+ * Wait for all current promises to be resolved. See this as executeSoon that
+ * can be used with yield.
+ */
+function promiseNextTick() {
+ let deferred = defer();
+ executeSoon(deferred.resolve);
+ return deferred.promise;
+}
+
+/**
+ * Collapses the current text selection in an input field and tabs to the next
+ * field.
+ */
+function collapseSelectionAndTab(inspector) {
+ // collapse selection and move caret to end
+ EventUtils.sendKey("tab", inspector.panelWin);
+ // next element
+ EventUtils.sendKey("tab", inspector.panelWin);
+}
+
+/**
+ * Collapses the current text selection in an input field and tabs to the
+ * previous field.
+ */
+function collapseSelectionAndShiftTab(inspector) {
+ // collapse selection and move caret to end
+ EventUtils.synthesizeKey("VK_TAB", { shiftKey: true },
+ inspector.panelWin);
+ // previous element
+ EventUtils.synthesizeKey("VK_TAB", { shiftKey: true },
+ inspector.panelWin);
+}
+
+/**
+ * Check that the current focused element is an attribute element in the markup
+ * view.
+ * @param {String} attrName The attribute name expected to be found
+ * @param {Boolean} editMode Whether or not the attribute should be in edit mode
+ */
+function checkFocusedAttribute(attrName, editMode) {
+ let focusedAttr = Services.focus.focusedElement;
+ ok(focusedAttr, "Has a focused element");
+
+ let dataAttr = focusedAttr.parentNode.dataset.attr;
+ is(dataAttr, attrName, attrName + " attribute editor is currently focused.");
+ if (editMode) {
+ // Using a multiline editor for attributes, the focused element should be a textarea.
+ is(focusedAttr.tagName, "textarea", attrName + "is in edit mode");
+ } else {
+ is(focusedAttr.tagName, "span", attrName + "is not in edit mode");
+ }
+}
+
+/**
+ * Get attributes for node as how they are represented in editor.
+ *
+ * @param {String} selector
+ * @param {InspectorPanel} inspector
+ * @return {Promise}
+ * A promise that resolves with an array of attribute names
+ * (e.g. ["id", "class", "href"])
+ */
+var getAttributesFromEditor = Task.async(function* (selector, inspector) {
+ let nodeList = (yield getContainerForSelector(selector, inspector))
+ .tagLine.querySelectorAll("[data-attr]");
+
+ return [...nodeList].map(node => node.getAttribute("data-attr"));
+});
+
+// The expand all operation of the markup-view calls itself recursively and
+// there's not one event we can wait for to know when it's done so use this
+// helper function to wait until all recursive children updates are done.
+function* waitForMultipleChildrenUpdates(inspector) {
+ // As long as child updates are queued up while we wait for an update already
+ // wait again
+ if (inspector.markup._queuedChildUpdates &&
+ inspector.markup._queuedChildUpdates.size) {
+ yield waitForChildrenUpdated(inspector);
+ return yield waitForMultipleChildrenUpdates(inspector);
+ }
+ return undefined;
+}
+
+/**
+ * Create an HTTP server that can be used to simulate custom requests within
+ * a test. It is automatically cleaned up when the test ends, so no need to
+ * call `destroy`.
+ *
+ * See https://developer.mozilla.org/en-US/docs/Httpd.js/HTTP_server_for_unit_tests
+ * for more information about how to register handlers.
+ *
+ * The server can be accessed like:
+ *
+ * const server = createTestHTTPServer();
+ * let url = "http://localhost: " + server.identity.primaryPort + "/path";
+ *
+ * @returns {HttpServer}
+ */
+function createTestHTTPServer() {
+ const {HttpServer} = Cu.import("resource://testing-common/httpd.js", {});
+ let server = new HttpServer();
+
+ registerCleanupFunction(function* cleanup() {
+ let destroyed = defer();
+ server.stop(() => {
+ destroyed.resolve();
+ });
+ yield destroyed.promise;
+ });
+
+ server.start(-1);
+ return server;
+}
+
+/**
+ * Registers new backend tab actor.
+ *
+ * @param {DebuggerClient} client RDP client object (toolbox.target.client)
+ * @param {Object} options Configuration object with the following options:
+ *
+ * - moduleUrl {String}: URL of the module that contains actor implementation.
+ * - prefix {String}: prefix of the actor.
+ * - actorClass {ActorClassWithSpec}: Constructor object for the actor.
+ * - frontClass {FrontClassWithSpec}: Constructor object for the front part
+ * of the registered actor.
+ *
+ * @returns {Promise} A promise that is resolved when the actor is registered.
+ * The resolved value has two properties:
+ *
+ * - registrar {ActorActor}: A handle to the registered actor that allows
+ * unregistration.
+ * - form {Object}: The JSON actor form provided by the server.
+ */
+function registerTabActor(client, options) {
+ let moduleUrl = options.moduleUrl;
+
+ return client.listTabs().then(response => {
+ let config = {
+ prefix: options.prefix,
+ constructor: options.actorClass,
+ type: { tab: true },
+ };
+
+ // Register the custom actor on the backend.
+ let registry = ActorRegistryFront(client, response);
+ return registry.registerActor(moduleUrl, config).then(registrar => {
+ return client.getTab().then(tabResponse => ({
+ registrar: registrar,
+ form: tabResponse.tab
+ }));
+ });
+ });
+}
+
+/**
+ * A helper for unregistering an existing backend actor.
+ *
+ * @param {ActorActor} registrar A handle to the registered actor
+ * that has been received after registration.
+ * @param {Front} Corresponding front object.
+ *
+ * @returns A promise that is resolved when the unregistration
+ * has finished.
+ */
+function unregisterActor(registrar, front) {
+ return front.detach().then(() => {
+ return registrar.unregister();
+ });
+}
+
+/**
+ * Simulate dragging a MarkupContainer by calling its mousedown and mousemove
+ * handlers.
+ * @param {InspectorPanel} inspector The current inspector-panel instance.
+ * @param {String|MarkupContainer} selector The selector to identify the node or
+ * the MarkupContainer for this node.
+ * @param {Number} xOffset Optional x offset to drag by.
+ * @param {Number} yOffset Optional y offset to drag by.
+ */
+function* simulateNodeDrag(inspector, selector, xOffset = 10, yOffset = 10) {
+ let container = typeof selector === "string"
+ ? yield getContainerForSelector(selector, inspector)
+ : selector;
+ let rect = container.tagLine.getBoundingClientRect();
+ let scrollX = inspector.markup.doc.documentElement.scrollLeft;
+ let scrollY = inspector.markup.doc.documentElement.scrollTop;
+
+ info("Simulate mouseDown on element " + selector);
+ container._onMouseDown({
+ target: container.tagLine,
+ button: 0,
+ pageX: scrollX + rect.x,
+ pageY: scrollY + rect.y,
+ stopPropagation: () => {},
+ preventDefault: () => {}
+ });
+
+ // _onMouseDown selects the node, so make sure to wait for the
+ // inspector-updated event if the current selection was different.
+ if (inspector.selection.nodeFront !== container.node) {
+ yield inspector.once("inspector-updated");
+ }
+
+ info("Simulate mouseMove on element " + selector);
+ container._onMouseMove({
+ pageX: scrollX + rect.x + xOffset,
+ pageY: scrollY + rect.y + yOffset
+ });
+}
+
+/**
+ * Simulate dropping a MarkupContainer by calling its mouseup handler. This is
+ * meant to be called after simulateNodeDrag has been called.
+ * @param {InspectorPanel} inspector The current inspector-panel instance.
+ * @param {String|MarkupContainer} selector The selector to identify the node or
+ * the MarkupContainer for this node.
+ */
+function* simulateNodeDrop(inspector, selector) {
+ info("Simulate mouseUp on element " + selector);
+ let container = typeof selector === "string"
+ ? yield getContainerForSelector(selector, inspector)
+ : selector;
+ container._onMouseUp();
+ inspector.markup._onMouseUp();
+}
+
+/**
+ * Simulate drag'n'dropping a MarkupContainer by calling its mousedown,
+ * mousemove and mouseup handlers.
+ * @param {InspectorPanel} inspector The current inspector-panel instance.
+ * @param {String|MarkupContainer} selector The selector to identify the node or
+ * the MarkupContainer for this node.
+ * @param {Number} xOffset Optional x offset to drag by.
+ * @param {Number} yOffset Optional y offset to drag by.
+ */
+function* simulateNodeDragAndDrop(inspector, selector, xOffset, yOffset) {
+ yield simulateNodeDrag(inspector, selector, xOffset, yOffset);
+ yield simulateNodeDrop(inspector, selector);
+}
+
+/**
+ * Waits until the element has not scrolled for 30 consecutive frames.
+ */
+function* waitForScrollStop(doc) {
+ let el = doc.documentElement;
+ let win = doc.defaultView;
+ let lastScrollTop = el.scrollTop;
+ let stopFrameCount = 0;
+ while (stopFrameCount < 30) {
+ // Wait for a frame.
+ yield new Promise(resolve => win.requestAnimationFrame(resolve));
+
+ // Check if the element has scrolled.
+ if (lastScrollTop == el.scrollTop) {
+ // No scrolling since the last frame.
+ stopFrameCount++;
+ } else {
+ // The element has scrolled. Reset the frame counter.
+ stopFrameCount = 0;
+ lastScrollTop = el.scrollTop;
+ }
+ }
+
+ return lastScrollTop;
+}
+
+/**
+ * Select a node in the inspector and try to delete it using the provided key. After that,
+ * check that the expected element is focused.
+ *
+ * @param {InspectorPanel} inspector
+ * The current inspector-panel instance.
+ * @param {String} key
+ * The key to simulate to delete the node
+ * @param {Object}
+ * - {String} selector: selector of the element to delete.
+ * - {String} focusedSelector: selector of the element that should be selected
+ * after deleting the node.
+ * - {String} pseudo: optional, "before" or "after" if the element focused after
+ * deleting the node is supposed to be a before/after pseudo-element.
+ */
+function* checkDeleteAndSelection(inspector, key, {selector, focusedSelector, pseudo}) {
+ info("Test deleting node " + selector + " with " + key + ", " +
+ "expecting " + focusedSelector + " to be focused");
+
+ info("Select node " + selector + " and make sure it is focused");
+ yield selectNode(selector, inspector);
+ yield clickContainer(selector, inspector);
+
+ info("Delete the node with: " + key);
+ let mutated = inspector.once("markupmutation");
+ EventUtils.sendKey(key, inspector.panelWin);
+ yield Promise.all([mutated, inspector.once("inspector-updated")]);
+
+ let nodeFront = yield getNodeFront(focusedSelector, inspector);
+ if (pseudo) {
+ // Update the selector for logging in case of failure.
+ focusedSelector = focusedSelector + "::" + pseudo;
+ // Retrieve the :before or :after pseudo element of the nodeFront.
+ let {nodes} = yield inspector.walker.children(nodeFront);
+ nodeFront = pseudo === "before" ? nodes[0] : nodes[nodes.length - 1];
+ }
+
+ is(inspector.selection.nodeFront, nodeFront,
+ focusedSelector + " is selected after deletion");
+
+ info("Check that the node was really removed");
+ let node = yield getNodeFront(selector, inspector);
+ ok(!node, "The node can't be found in the page anymore");
+
+ info("Undo the deletion to restore the original markup");
+ yield undoChange(inspector);
+ node = yield getNodeFront(selector, inspector);
+ ok(node, "The node is back");
+}
diff --git a/devtools/client/inspector/markup/test/helper_attributes_test_runner.js b/devtools/client/inspector/markup/test/helper_attributes_test_runner.js
new file mode 100644
index 000000000..20446d3d1
--- /dev/null
+++ b/devtools/client/inspector/markup/test/helper_attributes_test_runner.js
@@ -0,0 +1,160 @@
+/* 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/. */
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+/* import-globals-from head.js */
+"use strict";
+
+/**
+ * Run a series of add-attributes tests.
+ * This function will iterate over the provided tests array and run each test.
+ * Each test's goal is to provide some text to be entered into the test node's
+ * new-attribute field and check that the given attributes have been created.
+ * After each test has run, the markup-view's undo command will be called and
+ * the test runner will check if all the new attributes are gone.
+ * @param {Array} tests See runAddAttributesTest for the structure
+ * @param {DOMNode|String} nodeOrSelector The node or node selector
+ * corresponding to an element on the current test page that has *no attributes*
+ * when the test starts. It will be used to add and remove attributes.
+ * @param {InspectorPanel} inspector The instance of InspectorPanel currently
+ * opened
+ * @param {TestActorFront} testActor The current TestActorFront instance.
+ * @return a promise that resolves when the tests have run
+ */
+function runAddAttributesTests(tests, nodeOrSelector, inspector, testActor) {
+ info("Running " + tests.length + " add-attributes tests");
+ return Task.spawn(function* () {
+ info("Selecting the test node");
+ yield selectNode("div", inspector);
+
+ for (let test of tests) {
+ yield runAddAttributesTest(test, "div", inspector, testActor);
+ }
+ });
+}
+
+/**
+ * Run a single add-attribute test.
+ * See runAddAttributesTests for a description.
+ * @param {Object} test A test object should contain the following properties:
+ * - desc {String} a textual description for that test, to help when
+ * reading logs
+ * - text {String} the string to be inserted into the new attribute field
+ * - expectedAttributes {Object} a key/value pair object that will be
+ * used to check the attributes on the test element
+ * - validate {Function} optional extra function that will be called
+ * after the attributes have been added and which should be used to
+ * assert some more things this test runner might not be checking. The
+ * function will be called with the following arguments:
+ * - {DOMNode} The element being tested
+ * - {MarkupContainer} The corresponding container in the markup-view
+ * - {InspectorPanel} The instance of the InspectorPanel opened
+ * @param {String} selector The node selector corresponding to the test element
+ * @param {InspectorPanel} inspector The instance of InspectorPanel currently
+ * @param {TestActorFront} testActor The current TestActorFront instance.
+ * opened
+ */
+function* runAddAttributesTest(test, selector, inspector, testActor) {
+ if (test.setUp) {
+ test.setUp(inspector);
+ }
+
+ info("Starting add-attribute test: " + test.desc);
+ yield addNewAttributes(selector, test.text, inspector);
+
+ info("Assert that the attribute(s) has/have been applied correctly");
+ yield assertAttributes(selector, test.expectedAttributes, testActor);
+
+ if (test.validate) {
+ let container = yield getContainerForSelector(selector, inspector);
+ test.validate(container, inspector);
+ }
+
+ info("Undo the change");
+ yield undoChange(inspector);
+
+ info("Assert that the attribute(s) has/have been removed correctly");
+ yield assertAttributes(selector, {}, testActor);
+ if (test.tearDown) {
+ test.tearDown(inspector);
+ }
+}
+
+/**
+ * Run a series of edit-attributes tests.
+ * This function will iterate over the provided tests array and run each test.
+ * Each test's goal is to locate a given element on the current test page,
+ * assert its current attributes, then provide the name of one of them and a
+ * value to be set into it, and then check if the new attributes are correct.
+ * After each test has run, the markup-view's undo and redo commands will be
+ * called and the test runner will assert again that the attributes are correct.
+ * @param {Array} tests See runEditAttributesTest for the structure
+ * @param {InspectorPanel} inspector The instance of InspectorPanel currently
+ * opened
+ * @param {TestActorFront} testActor The current TestActorFront instance.
+ * @return a promise that resolves when the tests have run
+ */
+function runEditAttributesTests(tests, inspector, testActor) {
+ info("Running " + tests.length + " edit-attributes tests");
+ return Task.spawn(function* () {
+ info("Expanding all nodes in the markup-view");
+ yield inspector.markup.expandAll();
+
+ for (let test of tests) {
+ yield runEditAttributesTest(test, inspector, testActor);
+ }
+ });
+}
+
+/**
+ * Run a single edit-attribute test.
+ * See runEditAttributesTests for a description.
+ * @param {Object} test A test object should contain the following properties:
+ * - desc {String} a textual description for that test, to help when
+ * reading logs
+ * - node {String} a css selector that will be used to select the node
+ * which will be tested during this iteration
+ * - originalAttributes {Object} a key/value pair object that will be
+ * used to check the attributes of the node before the test runs
+ * - name {String} the name of the attribute to focus the editor for
+ * - value {String} the new value to be typed in the focused editor
+ * - expectedAttributes {Object} a key/value pair object that will be
+ * used to check the attributes on the test element
+ * @param {InspectorPanel} inspector The instance of InspectorPanel currently
+ * @param {TestActorFront} testActor The current TestActorFront instance.
+ * opened
+ */
+function* runEditAttributesTest(test, inspector, testActor) {
+ info("Starting edit-attribute test: " + test.desc);
+
+ info("Selecting the test node " + test.node);
+ yield selectNode(test.node, inspector);
+
+ info("Asserting that the node has the right attributes to start with");
+ yield assertAttributes(test.node, test.originalAttributes, testActor);
+
+ info("Editing attribute " + test.name + " with value " + test.value);
+
+ let container = yield focusNode(test.node, inspector);
+ ok(container && container.editor, "The markup-container for " + test.node +
+ " was found");
+
+ info("Listening for the markupmutation event");
+ let nodeMutated = inspector.once("markupmutation");
+ let attr = container.editor.attrElements.get(test.name)
+ .querySelector(".editable");
+ setEditableFieldValue(attr, test.value, inspector);
+ yield nodeMutated;
+
+ info("Asserting the new attributes after edition");
+ yield assertAttributes(test.node, test.expectedAttributes, testActor);
+
+ info("Undo the change and assert that the attributes have been changed back");
+ yield undoChange(inspector);
+ yield assertAttributes(test.node, test.originalAttributes, testActor);
+
+ info("Redo the change and assert that the attributes have been changed " +
+ "again");
+ yield redoChange(inspector);
+ yield assertAttributes(test.node, test.expectedAttributes, testActor);
+}
diff --git a/devtools/client/inspector/markup/test/helper_events_test_runner.js b/devtools/client/inspector/markup/test/helper_events_test_runner.js
new file mode 100644
index 000000000..acef334fb
--- /dev/null
+++ b/devtools/client/inspector/markup/test/helper_events_test_runner.js
@@ -0,0 +1,111 @@
+/* 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/. */
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+/* import-globals-from head.js */
+"use strict";
+
+/**
+ * Generator function that runs checkEventsForNode() for each object in the
+ * TEST_DATA array.
+ */
+function* runEventPopupTests(url, tests) {
+ let {inspector, testActor} = yield openInspectorForURL(url);
+
+ yield inspector.markup.expandAll();
+
+ for (let test of tests) {
+ yield checkEventsForNode(test, inspector, testActor);
+ }
+
+ // Wait for promises to avoid leaks when running this as a single test.
+ // We need to do this because we have opened a bunch of popups and don't them
+ // to affect other test runs when they are GCd.
+ yield promiseNextTick();
+}
+
+/**
+ * Generator function that takes a selector and expected results and returns
+ * the event info.
+ *
+ * @param {Object} test
+ * A test object should contain the following properties:
+ * - selector {String} a css selector targeting the node to edit
+ * - expected {Array} array of expected event objects
+ * - type {String} event type
+ * - filename {String} filename:line where the evt handler is defined
+ * - attributes {Array} array of event attributes ({String})
+ * - handler {String} string representation of the handler
+ * - beforeTest {Function} (optional) a function to execute on the page
+ * before running the test
+ * @param {InspectorPanel} inspector The instance of InspectorPanel currently
+ * opened
+ * @param {TestActorFront} testActor
+ */
+function* checkEventsForNode(test, inspector, testActor) {
+ let {selector, expected, beforeTest} = test;
+ let container = yield getContainerForSelector(selector, inspector);
+
+ if (typeof beforeTest === "function") {
+ yield beforeTest(inspector, testActor);
+ }
+
+ let evHolder = container.elt.querySelector(".markupview-events");
+
+ if (expected.length === 0) {
+ // if no event is expected, simply check that the event bubble is hidden
+ is(evHolder.style.display, "none", "event bubble should be hidden");
+ return;
+ }
+
+ let tooltip = inspector.markup.eventDetailsTooltip;
+
+ yield selectNode(selector, inspector);
+
+ // Click button to show tooltip
+ info("Clicking evHolder");
+ EventUtils.synthesizeMouseAtCenter(evHolder, {},
+ inspector.markup.doc.defaultView);
+ yield tooltip.once("shown");
+ info("tooltip shown");
+
+ // Check values
+ let headers = tooltip.panel.querySelectorAll(".event-header");
+ let nodeFront = container.node;
+ let cssSelector = nodeFront.nodeName + "#" + nodeFront.id;
+
+ for (let i = 0; i < headers.length; i++) {
+ info("Processing header[" + i + "] for " + cssSelector);
+
+ let header = headers[i];
+ let type = header.querySelector(".event-tooltip-event-type");
+ let filename = header.querySelector(".event-tooltip-filename");
+ let attributes = header.querySelectorAll(".event-tooltip-attributes");
+ let contentBox = header.nextElementSibling;
+
+ is(type.textContent, expected[i].type,
+ "type matches for " + cssSelector);
+ is(filename.textContent, expected[i].filename,
+ "filename matches for " + cssSelector);
+
+ is(attributes.length, expected[i].attributes.length,
+ "we have the correct number of attributes");
+
+ for (let j = 0; j < expected[i].attributes.length; j++) {
+ is(attributes[j].textContent, expected[i].attributes[j],
+ "attribute[" + j + "] matches for " + cssSelector);
+ }
+
+ // Make sure the header is not hidden by scrollbars before clicking.
+ header.scrollIntoView();
+
+ EventUtils.synthesizeMouseAtCenter(header, {}, type.ownerGlobal);
+ yield tooltip.once("event-tooltip-ready");
+
+ let editor = tooltip.eventTooltip._eventEditors.get(contentBox).editor;
+ is(editor.getText(), expected[i].handler,
+ "handler matches for " + cssSelector);
+ }
+
+ tooltip.hide();
+}
diff --git a/devtools/client/inspector/markup/test/helper_markup_accessibility_navigation.js b/devtools/client/inspector/markup/test/helper_markup_accessibility_navigation.js
new file mode 100644
index 000000000..a49f1e7ba
--- /dev/null
+++ b/devtools/client/inspector/markup/test/helper_markup_accessibility_navigation.js
@@ -0,0 +1,70 @@
+/* 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/. */
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+/* import-globals-from head.js */
+"use strict";
+
+/**
+ * Execute a keyboard event and check that the state is as expected (focused element, aria
+ * attribute etc...).
+ *
+ * @param {InspectorPanel} inspector
+ * Current instance of the inspector being tested.
+ * @param {Object} elms
+ * Map of elements that will be used to retrieve live references to children
+ * elements
+ * @param {Element} focused
+ * Element expected to be focused
+ * @param {Element} activedescendant
+ * Element expected to be the aria activedescendant of the root node
+ */
+function testNavigationState(inspector, elms, focused, activedescendant) {
+ let doc = inspector.markup.doc;
+ let id = activedescendant.getAttribute("id");
+ is(doc.activeElement, focused, `Keyboard focus should be set to ${focused}`);
+ is(elms.root.elt.getAttribute("aria-activedescendant"), id,
+ `Active descendant should be set to ${id}`);
+}
+
+/**
+ * Execute a keyboard event and check that the state is as expected (focused element, aria
+ * attribute etc...).
+ *
+ * @param {InspectorPanel} inspector
+ * Current instance of the inspector being tested.
+ * @param {Object} elms
+ * MarkupContainers/Elements that will be used to retrieve references to other
+ * elements based on objects' paths.
+ * @param {Object} testData
+ * - {String} desc: description for better logging.
+ * - {String} key: keyboard event's key.
+ * - {Object} options, optional: event data such as shiftKey, etc.
+ * - {String} focused: path to expected focused element in elms map.
+ * - {String} activedescendant: path to expected aria-activedescendant element in
+ * elms map.
+ * - {String} waitFor, optional: markupview event to wait for if keyboard actions
+ * result in async updates. Also accepts the inspector event "inspector-updated".
+ */
+function* runAccessibilityNavigationTest(inspector, elms,
+ {desc, key, options, focused, activedescendant, waitFor}) {
+ info(desc);
+
+ let markup = inspector.markup;
+ let doc = markup.doc;
+ let win = doc.defaultView;
+
+ let updated;
+ if (waitFor) {
+ updated = waitFor === "inspector-updated" ?
+ inspector.once(waitFor) : markup.once(waitFor);
+ } else {
+ updated = Promise.resolve();
+ }
+ EventUtils.synthesizeKey(key, options, win);
+ yield updated;
+
+ let focusedElement = lookupPath(elms, focused);
+ let activeDescendantElement = lookupPath(elms, activedescendant);
+ testNavigationState(inspector, elms, focusedElement, activeDescendantElement);
+}
diff --git a/devtools/client/inspector/markup/test/helper_outerhtml_test_runner.js b/devtools/client/inspector/markup/test/helper_outerhtml_test_runner.js
new file mode 100644
index 000000000..f2de0876f
--- /dev/null
+++ b/devtools/client/inspector/markup/test/helper_outerhtml_test_runner.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/. */
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+/* import-globals-from head.js */
+"use strict";
+
+/**
+ * Run a series of edit-outer-html tests.
+ * This function will iterate over the provided tests array and run each test.
+ * Each test's goal is to provide a node (a selector) and a new outer-HTML to be
+ * inserted in place of the current one for that node.
+ * This test runner will wait for the mutation event to be fired and will check
+ * a few things. Each test may also provide its own validate function to perform
+ * assertions and verify that the new outer html is correct.
+ * @param {Array} tests See runEditOuterHTMLTest for the structure
+ * @param {InspectorPanel} inspector The instance of InspectorPanel currently
+ * opened
+ * @param {TestActorFront} testActor The current TestActorFront instance
+ * @return a promise that resolves when the tests have run
+ */
+function runEditOuterHTMLTests(tests, inspector, testActor) {
+ info("Running " + tests.length + " edit-outer-html tests");
+ return Task.spawn(function* () {
+ for (let step of tests) {
+ yield runEditOuterHTMLTest(step, inspector, testActor);
+ }
+ });
+}
+
+/**
+ * Run a single edit-outer-html test.
+ * See runEditOuterHTMLTests for a description.
+ * @param {Object} test A test object should contain the following properties:
+ * - selector {String} a css selector targeting the node to edit
+ * - oldHTML {String}
+ * - newHTML {String}
+ * - validate {Function} will be executed when the edition test is done,
+ * after the new outer-html has been inserted. Should be used to verify
+ * the actual DOM, see if it corresponds to the newHTML string provided
+ * @param {InspectorPanel} inspector The instance of InspectorPanel currently
+ * @param {TestActorFront} testActor The current TestActorFront instance
+ * opened
+ */
+function* runEditOuterHTMLTest(test, inspector, testActor) {
+ info("Running an edit outerHTML test on '" + test.selector + "'");
+ yield selectNode(test.selector, inspector);
+
+ let onUpdated = inspector.once("inspector-updated");
+
+ info("Listen for reselectedonremoved and edit the outerHTML");
+ let onReselected = inspector.markup.once("reselectedonremoved");
+ yield inspector.markup.updateNodeOuterHTML(inspector.selection.nodeFront,
+ test.newHTML, test.oldHTML);
+ yield onReselected;
+
+ // Typically selectedNode will === pageNode, but if a new element has been
+ // injected in front of it, this will not be the case. If this happens.
+ let selectedNodeFront = inspector.selection.nodeFront;
+ let pageNodeFront = yield inspector.walker.querySelector(
+ inspector.walker.rootNode, test.selector);
+
+ if (test.validate) {
+ yield test.validate({pageNodeFront, selectedNodeFront,
+ inspector, testActor});
+ } else {
+ is(pageNodeFront, selectedNodeFront,
+ "Original node (grabbed by selector) is selected");
+ let {outerHTML} = yield testActor.getNodeInfo(test.selector);
+ is(outerHTML, test.newHTML, "Outer HTML has been updated");
+ }
+
+ // Wait for the inspector to be fully updated to avoid causing errors by
+ // abruptly closing hanging requests when the test ends
+ yield onUpdated;
+
+ let closeTagLine = inspector.markup.getContainer(pageNodeFront).closeTagLine;
+ if (closeTagLine) {
+ is(closeTagLine.querySelectorAll(".theme-fg-contrast").length, 0,
+ "No contrast class");
+ }
+}
diff --git a/devtools/client/inspector/markup/test/helper_style_attr_test_runner.js b/devtools/client/inspector/markup/test/helper_style_attr_test_runner.js
new file mode 100644
index 000000000..f884a8181
--- /dev/null
+++ b/devtools/client/inspector/markup/test/helper_style_attr_test_runner.js
@@ -0,0 +1,132 @@
+/* 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/. */
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+/* import-globals-from head.js */
+"use strict";
+
+/**
+ * Perform an style attribute edition and autocompletion test in the test
+ * url, for #node14. Test data should be an
+ * array of arrays structured as follows :
+ * [
+ * what key to press,
+ * expected input box value after keypress,
+ * expected input.selectionStart,
+ * expected input.selectionEnd,
+ * is popup expected to be open ?
+ * ]
+ *
+ * The test will start by adding a new attribute on the node, and then send each
+ * key specified in the testData. The last item of this array should leave the
+ * new attribute editor, either by committing or cancelling the edit.
+ *
+ * @param {InspectorPanel} inspector
+ * @param {Array} testData
+ * Array of arrays representing the characters to type for the new
+ * attribute as well as the expected state at each step
+ */
+function* runStyleAttributeAutocompleteTests(inspector, testData) {
+ info("Expand all markup nodes");
+ yield inspector.markup.expandAll();
+
+ info("Select #node14");
+ let container = yield focusNode("#node14", inspector);
+
+ info("Focus and open the new attribute inplace-editor");
+ let attr = container.editor.newAttr;
+ attr.focus();
+ EventUtils.sendKey("return", inspector.panelWin);
+ let editor = inplaceEditor(attr);
+
+ for (let i = 0; i < testData.length; i++) {
+ let data = testData[i];
+
+ // Expect a markupmutation event at the last iteration since that's when the
+ // attribute is actually created.
+ let onMutation = i === testData.length - 1
+ ? inspector.once("markupmutation") : null;
+
+ info(`Entering test data ${i}: ${data[0]}, expecting: [${data[1]}]`);
+ yield enterData(data, editor, inspector);
+
+ info(`Test data ${i} entered. Checking state.`);
+ yield checkData(data, editor, inspector);
+
+ yield onMutation;
+ }
+
+ // Undoing the action will remove the new attribute, so make sure to wait for
+ // the markupmutation event here again.
+ let onMutation = inspector.once("markupmutation");
+ while (inspector.markup.undo.canUndo()) {
+ yield undoChange(inspector);
+ }
+ yield onMutation;
+}
+
+/**
+ * Process a test data entry.
+ * @param {Array} data
+ * test data - click or key - to enter
+ * @param {InplaceEditor} editor
+ * @param {InspectorPanel} inspector
+ * @return {Promise} promise that will resolve when the test data has been
+ * applied
+ */
+function enterData(data, editor, inspector) {
+ let key = data[0];
+
+ if (/^click_[0-9]+$/.test(key)) {
+ let suggestionIndex = parseInt(key.split("_")[1], 10);
+ return clickOnSuggestion(suggestionIndex, editor);
+ }
+
+ return sendKey(key, editor, inspector);
+}
+
+function clickOnSuggestion(index, editor) {
+ return new Promise(resolve => {
+ info("Clicking on item " + index + " in the list");
+ editor.once("after-suggest", () => executeSoon(resolve));
+ editor.popup._list.childNodes[index].click();
+ });
+}
+
+function sendKey(key, editor, inspector) {
+ return new Promise(resolve => {
+ if (/(down|left|right|back_space|return)/ig.test(key)) {
+ info("Adding event listener for down|left|right|back_space|return keys");
+ editor.input.addEventListener("keypress", function onKeypress() {
+ if (editor.input) {
+ editor.input.removeEventListener("keypress", onKeypress);
+ }
+ executeSoon(resolve);
+ });
+ } else {
+ editor.once("after-suggest", () => executeSoon(resolve));
+ }
+
+ EventUtils.synthesizeKey(key, {}, inspector.panelWin);
+ });
+}
+
+/**
+ * Verify that the inplace editor is in the expected state for the provided
+ * test data.
+ */
+function* checkData(data, editor, inspector) {
+ let [, completion, selStart, selEnd, popupOpen] = data;
+
+ if (selEnd != -1) {
+ is(editor.input.value, completion, "Completed value is correct");
+ is(editor.input.selectionStart, selStart, "Selection start position is correct");
+ is(editor.input.selectionEnd, selEnd, "Selection end position is correct");
+ is(editor.popup.isOpen, popupOpen, "Popup is " + (popupOpen ? "open" : "closed"));
+ } else {
+ let nodeFront = yield getNodeFront("#node14", inspector);
+ let container = getContainerForNodeFront(nodeFront, inspector);
+ let attr = container.editor.attrElements.get("style").querySelector(".editable");
+ is(attr.textContent, completion, "Correct value is persisted after pressing Enter");
+ }
+}
diff --git a/devtools/client/inspector/markup/test/lib_jquery_1.0.js b/devtools/client/inspector/markup/test/lib_jquery_1.0.js
new file mode 100644
index 000000000..564361282
--- /dev/null
+++ b/devtools/client/inspector/markup/test/lib_jquery_1.0.js
@@ -0,0 +1,1814 @@
+/*
+ * jQuery - New Wave Javascript
+ *
+ * Copyright (c) 2006 John Resig (jquery.com)
+ * Dual licensed under the MIT (MIT-LICENSE.txt)
+ * and GPL (GPL-LICENSE.txt) licenses.
+ *
+ * $Date: 2006-10-27 23:14:48 -0400 (Fri, 27 Oct 2006) $
+ * $Rev: 509 $
+ */
+
+// Global undefined variable
+window.undefined = window.undefined;
+function jQuery(a,c) {
+
+ // Shortcut for document ready (because $(document).each() is silly)
+ if ( a && a.constructor == Function && jQuery.fn.ready )
+ return jQuery(document).ready(a);
+
+ // Make sure that a selection was provided
+ a = a || jQuery.context || document;
+
+ // Watch for when a jQuery object is passed as the selector
+ if ( a.jquery )
+ return $( jQuery.merge( a, [] ) );
+
+ // Watch for when a jQuery object is passed at the context
+ if ( c && c.jquery )
+ return $( c ).find(a);
+
+ // If the context is global, return a new object
+ if ( window == this )
+ return new jQuery(a,c);
+
+ // Handle HTML strings
+ var m = /^[^<]*(<.+>)[^>]*$/.exec(a);
+ if ( m ) a = jQuery.clean( [ m[1] ] );
+
+ // Watch for when an array is passed in
+ this.get( a.constructor == Array || a.length && !a.nodeType && a[0] != undefined && a[0].nodeType ?
+ // Assume that it is an array of DOM Elements
+ jQuery.merge( a, [] ) :
+
+ // Find the matching elements and save them for later
+ jQuery.find( a, c ) );
+
+ // See if an extra function was provided
+ var fn = arguments[ arguments.length - 1 ];
+
+ // If so, execute it in context
+ if ( fn && fn.constructor == Function )
+ this.each(fn);
+}
+
+// Map over the $ in case of overwrite
+if ( $ )
+ jQuery._$ = $;
+
+// Map the jQuery namespace to the '$' one
+var $ = jQuery;
+
+jQuery.fn = jQuery.prototype = {
+ jquery: "$Rev: 509 $",
+
+ size: function() {
+ return this.length;
+ },
+
+ get: function( num ) {
+ // Watch for when an array (of elements) is passed in
+ if ( num && num.constructor == Array ) {
+
+ // Use a tricky hack to make the jQuery object
+ // look and feel like an array
+ this.length = 0;
+ [].push.apply( this, num );
+
+ return this;
+ } else
+ return num == undefined ?
+
+ // Return a 'clean' array
+ jQuery.map( this, function(a){ return a } ) :
+
+ // Return just the object
+ this[num];
+ },
+ each: function( fn, args ) {
+ return jQuery.each( this, fn, args );
+ },
+
+ index: function( obj ) {
+ var pos = -1;
+ this.each(function(i){
+ if ( this == obj ) pos = i;
+ });
+ return pos;
+ },
+
+ attr: function( key, value, type ) {
+ // Check to see if we're setting style values
+ return key.constructor != String || value != undefined ?
+ this.each(function(){
+ // See if we're setting a hash of styles
+ if ( value == undefined )
+ // Set all the styles
+ for ( var prop in key )
+ jQuery.attr(
+ type ? this.style : this,
+ prop, key[prop]
+ );
+
+ // See if we're setting a single key/value style
+ else
+ jQuery.attr(
+ type ? this.style : this,
+ key, value
+ );
+ }) :
+
+ // Look for the case where we're accessing a style value
+ jQuery[ type || "attr" ]( this[0], key );
+ },
+
+ css: function( key, value ) {
+ return this.attr( key, value, "curCSS" );
+ },
+ text: function(e) {
+ e = e || this;
+ var t = "";
+ for ( var j = 0; j < e.length; j++ ) {
+ var r = e[j].childNodes;
+ for ( var i = 0; i < r.length; i++ )
+ t += r[i].nodeType != 1 ?
+ r[i].nodeValue : jQuery.fn.text([ r[i] ]);
+ }
+ return t;
+ },
+ wrap: function() {
+ // The elements to wrap the target around
+ var a = jQuery.clean(arguments);
+
+ // Wrap each of the matched elements individually
+ return this.each(function(){
+ // Clone the structure that we're using to wrap
+ var b = a[0].cloneNode(true);
+
+ // Insert it before the element to be wrapped
+ this.parentNode.insertBefore( b, this );
+
+ // Find he deepest point in the wrap structure
+ while ( b.firstChild )
+ b = b.firstChild;
+
+ // Move the matched element to within the wrap structure
+ b.appendChild( this );
+ });
+ },
+ append: function() {
+ return this.domManip(arguments, true, 1, function(a){
+ this.appendChild( a );
+ });
+ },
+ prepend: function() {
+ return this.domManip(arguments, true, -1, function(a){
+ this.insertBefore( a, this.firstChild );
+ });
+ },
+ before: function() {
+ return this.domManip(arguments, false, 1, function(a){
+ this.parentNode.insertBefore( a, this );
+ });
+ },
+ after: function() {
+ return this.domManip(arguments, false, -1, function(a){
+ this.parentNode.insertBefore( a, this.nextSibling );
+ });
+ },
+ end: function() {
+ return this.get( this.stack.pop() );
+ },
+ find: function(t) {
+ return this.pushStack( jQuery.map( this, function(a){
+ return jQuery.find(t,a);
+ }), arguments );
+ },
+
+ clone: function(deep) {
+ return this.pushStack( jQuery.map( this, function(a){
+ return a.cloneNode( deep != undefined ? deep : true );
+ }), arguments );
+ },
+
+ filter: function(t) {
+ return this.pushStack(
+ t.constructor == Array &&
+ jQuery.map(this,function(a){
+ for ( var i = 0; i < t.length; i++ )
+ if ( jQuery.filter(t[i],[a]).r.length )
+ return a;
+ }) ||
+
+ t.constructor == Boolean &&
+ ( t ? this.get() : [] ) ||
+
+ t.constructor == Function &&
+ jQuery.grep( this, t ) ||
+
+ jQuery.filter(t,this).r, arguments );
+ },
+
+ not: function(t) {
+ return this.pushStack( t.constructor == String ?
+ jQuery.filter(t,this,false).r :
+ jQuery.grep(this,function(a){ return a != t; }), arguments );
+ },
+
+ add: function(t) {
+ return this.pushStack( jQuery.merge( this, t.constructor == String ?
+ jQuery.find(t) : t.constructor == Array ? t : [t] ), arguments );
+ },
+ is: function(expr) {
+ return expr ? jQuery.filter(expr,this).r.length > 0 : this.length > 0;
+ },
+ domManip: function(args, table, dir, fn){
+ var clone = this.size() > 1;
+ var a = jQuery.clean(args);
+
+ return this.each(function(){
+ var obj = this;
+
+ if ( table && this.nodeName == "TABLE" && a[0].nodeName != "THEAD" ) {
+ var tbody = this.getElementsByTagName("tbody");
+
+ if ( !tbody.length ) {
+ obj = document.createElement("tbody");
+ this.appendChild( obj );
+ } else
+ obj = tbody[0];
+ }
+
+ for ( var i = ( dir < 0 ? a.length - 1 : 0 );
+ i != ( dir < 0 ? dir : a.length ); i += dir ) {
+ fn.apply( obj, [ clone ? a[i].cloneNode(true) : a[i] ] );
+ }
+ });
+ },
+ pushStack: function(a,args) {
+ var fn = args && args[args.length-1];
+
+ if ( !fn || fn.constructor != Function ) {
+ if ( !this.stack ) this.stack = [];
+ this.stack.push( this.get() );
+ this.get( a );
+ } else {
+ var old = this.get();
+ this.get( a );
+ if ( fn.constructor == Function )
+ return this.each( fn );
+ this.get( old );
+ }
+
+ return this;
+ }
+};
+
+jQuery.extend = jQuery.fn.extend = function(obj,prop) {
+ if ( !prop ) { prop = obj; obj = this; }
+ for ( var i in prop ) obj[i] = prop[i];
+ return obj;
+};
+
+jQuery.extend({
+ init: function(){
+ jQuery.initDone = true;
+
+ jQuery.each( jQuery.macros.axis, function(i,n){
+ jQuery.fn[ i ] = function(a) {
+ var ret = jQuery.map(this,n);
+ if ( a && a.constructor == String )
+ ret = jQuery.filter(a,ret).r;
+ return this.pushStack( ret, arguments );
+ };
+ });
+
+ jQuery.each( jQuery.macros.to, function(i,n){
+ jQuery.fn[ i ] = function(){
+ var a = arguments;
+ return this.each(function(){
+ for ( var j = 0; j < a.length; j++ )
+ $(a[j])[n]( this );
+ });
+ };
+ });
+
+ jQuery.each( jQuery.macros.each, function(i,n){
+ jQuery.fn[ i ] = function() {
+ return this.each( n, arguments );
+ };
+ });
+
+ jQuery.each( jQuery.macros.filter, function(i,n){
+ jQuery.fn[ n ] = function(num,fn) {
+ return this.filter( ":" + n + "(" + num + ")", fn );
+ };
+ });
+
+ jQuery.each( jQuery.macros.attr, function(i,n){
+ n = n || i;
+ jQuery.fn[ i ] = function(h) {
+ return h == undefined ?
+ this.length ? this[0][n] : null :
+ this.attr( n, h );
+ };
+ });
+
+ jQuery.each( jQuery.macros.css, function(i,n){
+ jQuery.fn[ n ] = function(h) {
+ return h == undefined ?
+ ( this.length ? jQuery.css( this[0], n ) : null ) :
+ this.css( n, h );
+ };
+ });
+
+ },
+ each: function( obj, fn, args ) {
+ if ( obj.length == undefined )
+ for ( var i in obj )
+ fn.apply( obj[i], args || [i, obj[i]] );
+ else
+ for ( var i = 0; i < obj.length; i++ )
+ fn.apply( obj[i], args || [i, obj[i]] );
+ return obj;
+ },
+
+ className: {
+ add: function(o,c){
+ if (jQuery.className.has(o,c)) return;
+ o.className += ( o.className ? " " : "" ) + c;
+ },
+ remove: function(o,c){
+ o.className = !c ? "" :
+ o.className.replace(
+ new RegExp("(^|\\s*\\b[^-])"+c+"($|\\b(?=[^-]))", "g"), "");
+ },
+ has: function(e,a) {
+ if ( e.className != undefined )
+ e = e.className;
+ return new RegExp("(^|\\s)" + a + "(\\s|$)").test(e);
+ }
+ },
+ swap: function(e,o,f) {
+ for ( var i in o ) {
+ e.style["old"+i] = e.style[i];
+ e.style[i] = o[i];
+ }
+ f.apply( e, [] );
+ for ( var i in o )
+ e.style[i] = e.style["old"+i];
+ },
+
+ css: function(e,p) {
+ if ( p == "height" || p == "width" ) {
+ var old = {}, oHeight, oWidth, d = ["Top","Bottom","Right","Left"];
+
+ for ( var i in d ) {
+ old["padding" + d[i]] = 0;
+ old["border" + d[i] + "Width"] = 0;
+ }
+
+ jQuery.swap( e, old, function() {
+ if (jQuery.css(e,"display") != "none") {
+ oHeight = e.offsetHeight;
+ oWidth = e.offsetWidth;
+ } else {
+ e = $(e.cloneNode(true)).css({
+ visibility: "hidden", position: "absolute", display: "block"
+ }).prependTo("body")[0];
+
+ oHeight = e.clientHeight;
+ oWidth = e.clientWidth;
+
+ e.parentNode.removeChild(e);
+ }
+ });
+
+ return p == "height" ? oHeight : oWidth;
+ } else if ( p == "opacity" && jQuery.browser.msie )
+ return parseFloat( jQuery.curCSS(e,"filter").replace(/[^0-9.]/,"") ) || 1;
+
+ return jQuery.curCSS( e, p );
+ },
+
+ curCSS: function(elem, prop, force) {
+ var ret;
+
+ if (!force && elem.style[prop]) {
+
+ ret = elem.style[prop];
+
+ } else if (elem.currentStyle) {
+
+ var newProp = prop.replace(/\-(\w)/g,function(m,c){return c.toUpperCase()});
+ ret = elem.currentStyle[prop] || elem.currentStyle[newProp];
+
+ } else if (document.defaultView && document.defaultView.getComputedStyle) {
+
+ prop = prop.replace(/([A-Z])/g,"-$1").toLowerCase();
+ var cur = document.defaultView.getComputedStyle(elem, null);
+
+ if ( cur )
+ ret = cur.getPropertyValue(prop);
+ else if ( prop == 'display' )
+ ret = 'none';
+ else
+ jQuery.swap(elem, { display: 'block' }, function() {
+ ret = document.defaultView.getComputedStyle(this,null).getPropertyValue(prop);
+ });
+
+ }
+
+ return ret;
+ },
+
+ clean: function(a) {
+ var r = [];
+ for ( var i = 0; i < a.length; i++ ) {
+ if ( a[i].constructor == String ) {
+
+ var table = "";
+
+ if ( !a[i].indexOf("<thead") || !a[i].indexOf("<tbody") ) {
+ table = "thead";
+ a[i] = "<table>" + a[i] + "</table>";
+ } else if ( !a[i].indexOf("<tr") ) {
+ table = "tr";
+ a[i] = "<table>" + a[i] + "</table>";
+ } else if ( !a[i].indexOf("<td") || !a[i].indexOf("<th") ) {
+ table = "td";
+ a[i] = "<table><tbody><tr>" + a[i] + "</tr></tbody></table>";
+ }
+
+ var div = document.createElement("div");
+ div.innerHTML = a[i];
+
+ if ( table ) {
+ div = div.firstChild;
+ if ( table != "thead" ) div = div.firstChild;
+ if ( table == "td" ) div = div.firstChild;
+ }
+
+ for ( var j = 0; j < div.childNodes.length; j++ )
+ r.push( div.childNodes[j] );
+ } else if ( a[i].jquery || a[i].length && !a[i].nodeType )
+ for ( var k = 0; k < a[i].length; k++ )
+ r.push( a[i][k] );
+ else if ( a[i] !== null )
+ r.push( a[i].nodeType ? a[i] : document.createTextNode(a[i].toString()) );
+ }
+ return r;
+ },
+
+ expr: {
+ "": "m[2]== '*'||a.nodeName.toUpperCase()==m[2].toUpperCase()",
+ "#": "a.getAttribute('id')&&a.getAttribute('id')==m[2]",
+ ":": {
+ // Position Checks
+ lt: "i<m[3]-0",
+ gt: "i>m[3]-0",
+ nth: "m[3]-0==i",
+ eq: "m[3]-0==i",
+ first: "i==0",
+ last: "i==r.length-1",
+ even: "i%2==0",
+ odd: "i%2",
+
+ // Child Checks
+ "first-child": "jQuery.sibling(a,0).cur",
+ "last-child": "jQuery.sibling(a,0).last",
+ "only-child": "jQuery.sibling(a).length==1",
+
+ // Parent Checks
+ parent: "a.childNodes.length",
+ empty: "!a.childNodes.length",
+
+ // Text Check
+ contains: "(a.innerText||a.innerHTML).indexOf(m[3])>=0",
+
+ // Visibility
+ visible: "a.type!='hidden'&&jQuery.css(a,'display')!='none'&&jQuery.css(a,'visibility')!='hidden'",
+ hidden: "a.type=='hidden'||jQuery.css(a,'display')=='none'||jQuery.css(a,'visibility')=='hidden'",
+
+ // Form elements
+ enabled: "!a.disabled",
+ disabled: "a.disabled",
+ checked: "a.checked",
+ selected: "a.selected"
+ },
+ ".": "jQuery.className.has(a,m[2])",
+ "@": {
+ "=": "z==m[4]",
+ "!=": "z!=m[4]",
+ "^=": "!z.indexOf(m[4])",
+ "$=": "z.substr(z.length - m[4].length,m[4].length)==m[4]",
+ "*=": "z.indexOf(m[4])>=0",
+ "": "z"
+ },
+ "[": "jQuery.find(m[2],a).length"
+ },
+
+ token: [
+ "\\.\\.|/\\.\\.", "a.parentNode",
+ ">|/", "jQuery.sibling(a.firstChild)",
+ "\\+", "jQuery.sibling(a).next",
+ "~", function(a){
+ var r = [];
+ var s = jQuery.sibling(a);
+ if ( s.n > 0 )
+ for ( var i = s.n; i < s.length; i++ )
+ r.push( s[i] );
+ return r;
+ }
+ ],
+ find: function( t, context ) {
+ // Make sure that the context is a DOM Element
+ if ( context && context.nodeType == undefined )
+ context = null;
+
+ // Set the correct context (if none is provided)
+ context = context || jQuery.context || document;
+
+ if ( t.constructor != String ) return [t];
+
+ if ( !t.indexOf("//") ) {
+ context = context.documentElement;
+ t = t.substr(2,t.length);
+ } else if ( !t.indexOf("/") ) {
+ context = context.documentElement;
+ t = t.substr(1,t.length);
+ // FIX Assume the root element is right :(
+ if ( t.indexOf("/") >= 1 )
+ t = t.substr(t.indexOf("/"),t.length);
+ }
+
+ var ret = [context];
+ var done = [];
+ var last = null;
+
+ while ( t.length > 0 && last != t ) {
+ var r = [];
+ last = t;
+
+ t = jQuery.trim(t).replace( /^\/\//i, "" );
+
+ var foundToken = false;
+
+ for ( var i = 0; i < jQuery.token.length; i += 2 ) {
+ var re = new RegExp("^(" + jQuery.token[i] + ")");
+ var m = re.exec(t);
+
+ if ( m ) {
+ r = ret = jQuery.map( ret, jQuery.token[i+1] );
+ t = jQuery.trim( t.replace( re, "" ) );
+ foundToken = true;
+ }
+ }
+
+ if ( !foundToken ) {
+ if ( !t.indexOf(",") || !t.indexOf("|") ) {
+ if ( ret[0] == context ) ret.shift();
+ done = jQuery.merge( done, ret );
+ r = ret = [context];
+ t = " " + t.substr(1,t.length);
+ } else {
+ var re2 = /^([#.]?)([a-z0-9\\*_-]*)/i;
+ var m = re2.exec(t);
+
+ if ( m[1] == "#" ) {
+ // Ummm, should make this work in all XML docs
+ var oid = document.getElementById(m[2]);
+ r = ret = oid ? [oid] : [];
+ t = t.replace( re2, "" );
+ } else {
+ if ( !m[2] || m[1] == "." ) m[2] = "*";
+
+ for ( var i = 0; i < ret.length; i++ )
+ r = jQuery.merge( r,
+ m[2] == "*" ?
+ jQuery.getAll(ret[i]) :
+ ret[i].getElementsByTagName(m[2])
+ );
+ }
+ }
+ }
+
+ if ( t ) {
+ var val = jQuery.filter(t,r);
+ ret = r = val.r;
+ t = jQuery.trim(val.t);
+ }
+ }
+
+ if ( ret && ret[0] == context ) ret.shift();
+ done = jQuery.merge( done, ret );
+
+ return done;
+ },
+
+ getAll: function(o,r) {
+ r = r || [];
+ var s = o.childNodes;
+ for ( var i = 0; i < s.length; i++ )
+ if ( s[i].nodeType == 1 ) {
+ r.push( s[i] );
+ jQuery.getAll( s[i], r );
+ }
+ return r;
+ },
+
+ attr: function(elem, name, value){
+ var fix = {
+ "for": "htmlFor",
+ "class": "className",
+ "float": "cssFloat",
+ innerHTML: "innerHTML",
+ className: "className"
+ };
+
+ if ( fix[name] ) {
+ if ( value != undefined ) elem[fix[name]] = value;
+ return elem[fix[name]];
+ } else if ( elem.getAttribute ) {
+ if ( value != undefined ) elem.setAttribute( name, value );
+ return elem.getAttribute( name, 2 );
+ } else {
+ name = name.replace(/-([a-z])/ig,function(z,b){return b.toUpperCase();});
+ if ( value != undefined ) elem[name] = value;
+ return elem[name];
+ }
+ },
+
+ // The regular expressions that power the parsing engine
+ parse: [
+ // Match: [@value='test'], [@foo]
+ [ "\\[ *(@)S *([!*$^=]*) *Q\\]", 1 ],
+
+ // Match: [div], [div p]
+ [ "(\\[)Q\\]", 0 ],
+
+ // Match: :contains('foo')
+ [ "(:)S\\(Q\\)", 0 ],
+
+ // Match: :even, :last-chlid
+ [ "([:.#]*)S", 0 ]
+ ],
+
+ filter: function(t,r,not) {
+ // Figure out if we're doing regular, or inverse, filtering
+ var g = not !== false ? jQuery.grep :
+ function(a,f) {return jQuery.grep(a,f,true);};
+
+ while ( t && /^[a-z[({<*:.#]/i.test(t) ) {
+
+ var p = jQuery.parse;
+
+ for ( var i = 0; i < p.length; i++ ) {
+ var re = new RegExp( "^" + p[i][0]
+
+ // Look for a string-like sequence
+ .replace( 'S', "([a-z*_-][a-z0-9_-]*)" )
+
+ // Look for something (optionally) enclosed with quotes
+ .replace( 'Q', " *'?\"?([^'\"]*?)'?\"? *" ), "i" );
+
+ var m = re.exec( t );
+
+ if ( m ) {
+ // Re-organize the match
+ if ( p[i][1] )
+ m = ["", m[1], m[3], m[2], m[4]];
+
+ // Remove what we just matched
+ t = t.replace( re, "" );
+
+ break;
+ }
+ }
+
+ // :not() is a special case that can be optomized by
+ // keeping it out of the expression list
+ if ( m[1] == ":" && m[2] == "not" )
+ r = jQuery.filter(m[3],r,false).r;
+
+ // Otherwise, find the expression to execute
+ else {
+ var f = jQuery.expr[m[1]];
+ if ( f.constructor != String )
+ f = jQuery.expr[m[1]][m[2]];
+
+ // Build a custom macro to enclose it
+ eval("f = function(a,i){" +
+ ( m[1] == "@" ? "z=jQuery.attr(a,m[3]);" : "" ) +
+ "return " + f + "}");
+
+ // Execute it against the current filter
+ r = g( r, f );
+ }
+ }
+
+ // Return an array of filtered elements (r)
+ // and the modified expression string (t)
+ return { r: r, t: t };
+ },
+ trim: function(t){
+ return t.replace(/^\s+|\s+$/g, "");
+ },
+ parents: function( elem ){
+ var matched = [];
+ var cur = elem.parentNode;
+ while ( cur && cur != document ) {
+ matched.push( cur );
+ cur = cur.parentNode;
+ }
+ return matched;
+ },
+ sibling: function(elem, pos, not) {
+ var elems = [];
+
+ var siblings = elem.parentNode.childNodes;
+ for ( var i = 0; i < siblings.length; i++ ) {
+ if ( not === true && siblings[i] == elem ) continue;
+
+ if ( siblings[i].nodeType == 1 )
+ elems.push( siblings[i] );
+ if ( siblings[i] == elem )
+ elems.n = elems.length - 1;
+ }
+
+ return jQuery.extend( elems, {
+ last: elems.n == elems.length - 1,
+ cur: pos == "even" && elems.n % 2 == 0 || pos == "odd" && elems.n % 2 || elems[pos] == elem,
+ prev: elems[elems.n - 1],
+ next: elems[elems.n + 1]
+ });
+ },
+ merge: function(first, second) {
+ var result = [];
+
+ // Move b over to the new array (this helps to avoid
+ // StaticNodeList instances)
+ for ( var k = 0; k < first.length; k++ )
+ result[k] = first[k];
+
+ // Now check for duplicates between a and b and only
+ // add the unique items
+ for ( var i = 0; i < second.length; i++ ) {
+ var noCollision = true;
+
+ // The collision-checking process
+ for ( var j = 0; j < first.length; j++ )
+ if ( second[i] == first[j] )
+ noCollision = false;
+
+ // If the item is unique, add it
+ if ( noCollision )
+ result.push( second[i] );
+ }
+
+ return result;
+ },
+ grep: function(elems, fn, inv) {
+ // If a string is passed in for the function, make a function
+ // for it (a handy shortcut)
+ if ( fn.constructor == String )
+ fn = new Function("a","i","return " + fn);
+
+ var result = [];
+
+ // Go through the array, only saving the items
+ // that pass the validator function
+ for ( var i = 0; i < elems.length; i++ )
+ if ( !inv && fn(elems[i],i) || inv && !fn(elems[i],i) )
+ result.push( elems[i] );
+
+ return result;
+ },
+ map: function(elems, fn) {
+ // If a string is passed in for the function, make a function
+ // for it (a handy shortcut)
+ if ( fn.constructor == String )
+ fn = new Function("a","return " + fn);
+
+ var result = [];
+
+ // Go through the array, translating each of the items to their
+ // new value (or values).
+ for ( var i = 0; i < elems.length; i++ ) {
+ var val = fn(elems[i],i);
+
+ if ( val !== null && val != undefined ) {
+ if ( val.constructor != Array ) val = [val];
+ result = jQuery.merge( result, val );
+ }
+ }
+
+ return result;
+ },
+
+ /*
+ * A number of helper functions used for managing events.
+ * Many of the ideas behind this code orignated from Dean Edwards' addEvent library.
+ */
+ event: {
+
+ // Bind an event to an element
+ // Original by Dean Edwards
+ add: function(element, type, handler) {
+ // For whatever reason, IE has trouble passing the window object
+ // around, causing it to be cloned in the process
+ if ( jQuery.browser.msie && element.setInterval != undefined )
+ element = window;
+
+ // Make sure that the function being executed has a unique ID
+ if ( !handler.guid )
+ handler.guid = this.guid++;
+
+ // Init the element's event structure
+ if (!element.events)
+ element.events = {};
+
+ // Get the current list of functions bound to this event
+ var handlers = element.events[type];
+
+ // If it hasn't been initialized yet
+ if (!handlers) {
+ // Init the event handler queue
+ handlers = element.events[type] = {};
+
+ // Remember an existing handler, if it's already there
+ if (element["on" + type])
+ handlers[0] = element["on" + type];
+ }
+
+ // Add the function to the element's handler list
+ handlers[handler.guid] = handler;
+
+ // And bind the global event handler to the element
+ element["on" + type] = this.handle;
+
+ // Remember the function in a global list (for triggering)
+ if (!this.global[type])
+ this.global[type] = [];
+ this.global[type].push( element );
+ },
+
+ guid: 1,
+ global: {},
+
+ // Detach an event or set of events from an element
+ remove: function(element, type, handler) {
+ if (element.events)
+ if (type && element.events[type])
+ if ( handler )
+ delete element.events[type][handler.guid];
+ else
+ for ( var i in element.events[type] )
+ delete element.events[type][i];
+ else
+ for ( var j in element.events )
+ this.remove( element, j );
+ },
+
+ trigger: function(type,data,element) {
+ // Touch up the incoming data
+ data = data || [];
+
+ // Handle a global trigger
+ if ( !element ) {
+ var g = this.global[type];
+ if ( g )
+ for ( var i = 0; i < g.length; i++ )
+ this.trigger( type, data, g[i] );
+
+ // Handle triggering a single element
+ } else if ( element["on" + type] ) {
+ // Pass along a fake event
+ data.unshift( this.fix({ type: type, target: element }) );
+
+ // Trigger the event
+ element["on" + type].apply( element, data );
+ }
+ },
+
+ handle: function(event) {
+ if ( typeof jQuery == "undefined" ) return;
+
+ event = event || jQuery.event.fix( window.event );
+
+ // If no correct event was found, fail
+ if ( !event ) return;
+
+ var returnValue = true;
+
+ var c = this.events[event.type];
+
+ for ( var j in c ) {
+ if ( c[j].apply( this, [event] ) === false ) {
+ event.preventDefault();
+ event.stopPropagation();
+ returnValue = false;
+ }
+ }
+
+ return returnValue;
+ },
+
+ fix: function(event) {
+ if ( event ) {
+ event.preventDefault = function() {
+ this.returnValue = false;
+ };
+
+ event.stopPropagation = function() {
+ this.cancelBubble = true;
+ };
+ }
+
+ return event;
+ }
+
+ }
+});
+
+new function() {
+ var b = navigator.userAgent.toLowerCase();
+
+ // Figure out what browser is being used
+ jQuery.browser = {
+ safari: /webkit/.test(b),
+ opera: /opera/.test(b),
+ msie: /msie/.test(b) && !/opera/.test(b),
+ mozilla: /mozilla/.test(b) && !/compatible/.test(b)
+ };
+
+ // Check to see if the W3C box model is being used
+ jQuery.boxModel = !jQuery.browser.msie || document.compatMode == "CSS1Compat";
+};
+
+jQuery.macros = {
+ to: {
+ appendTo: "append",
+ prependTo: "prepend",
+ insertBefore: "before",
+ insertAfter: "after"
+ },
+
+
+ css: "width,height,top,left,position,float,overflow,color,background".split(","),
+
+ filter: [ "eq", "lt", "gt", "contains" ],
+
+ attr: {
+
+ val: "value",
+
+ html: "innerHTML",
+
+ id: null,
+
+ title: null,
+
+ name: null,
+
+ href: null,
+
+ src: null,
+
+ rel: null
+ },
+
+ axis: {
+
+ parent: "a.parentNode",
+
+ ancestors: jQuery.parents,
+
+ parents: jQuery.parents,
+
+ next: "jQuery.sibling(a).next",
+
+ prev: "jQuery.sibling(a).prev",
+
+ siblings: jQuery.sibling,
+
+ children: "a.childNodes"
+ },
+
+ each: {
+
+ removeAttr: function( key ) {
+ this.removeAttribute( key );
+ },
+ show: function(){
+ this.style.display = this.oldblock ? this.oldblock : "";
+ if ( jQuery.css(this,"display") == "none" )
+ this.style.display = "block";
+ },
+ hide: function(){
+ this.oldblock = this.oldblock || jQuery.css(this,"display");
+ if ( this.oldblock == "none" )
+ this.oldblock = "block";
+ this.style.display = "none";
+ },
+ toggle: function(){
+ $(this)[ $(this).is(":hidden") ? "show" : "hide" ].apply( $(this), arguments );
+ },
+ addClass: function(c){
+ jQuery.className.add(this,c);
+ },
+ removeClass: function(c){
+ jQuery.className.remove(this,c);
+ },
+ toggleClass: function( c ){
+ jQuery.className[ jQuery.className.has(this,c) ? "remove" : "add" ](this,c);
+ },
+
+ remove: function(a){
+ if ( !a || jQuery.filter( [this], a ).r )
+ this.parentNode.removeChild( this );
+ },
+ empty: function(){
+ while ( this.firstChild )
+ this.removeChild( this.firstChild );
+ },
+ bind: function( type, fn ) {
+ if ( fn.constructor == String )
+ fn = new Function("e", ( !fn.indexOf(".") ? "$(this)" : "return " ) + fn);
+ jQuery.event.add( this, type, fn );
+ },
+
+ unbind: function( type, fn ) {
+ jQuery.event.remove( this, type, fn );
+ },
+ trigger: function( type, data ) {
+ jQuery.event.trigger( type, data, this );
+ }
+ }
+};
+
+jQuery.init();jQuery.fn.extend({
+
+ // We're overriding the old toggle function, so
+ // remember it for later
+ _toggle: jQuery.fn.toggle,
+ toggle: function(a,b) {
+ // If two functions are passed in, we're
+ // toggling on a click
+ return a && b && a.constructor == Function && b.constructor == Function ? this.click(function(e){
+ // Figure out which function to execute
+ this.last = this.last == a ? b : a;
+
+ // Make sure that clicks stop
+ e.preventDefault();
+
+ // and execute the function
+ return this.last.apply( this, [e] ) || false;
+ }) :
+
+ // Otherwise, execute the old toggle function
+ this._toggle.apply( this, arguments );
+ },
+
+ hover: function(f,g) {
+
+ // A private function for haandling mouse 'hovering'
+ function handleHover(e) {
+ // Check if mouse(over|out) are still within the same parent element
+ var p = (e.type == "mouseover" ? e.fromElement : e.toElement) || e.relatedTarget;
+
+ // Traverse up the tree
+ while ( p && p != this ) p = p.parentNode;
+
+ // If we actually just moused on to a sub-element, ignore it
+ if ( p == this ) return false;
+
+ // Execute the right function
+ return (e.type == "mouseover" ? f : g).apply(this, [e]);
+ }
+
+ // Bind the function to the two event listeners
+ return this.mouseover(handleHover).mouseout(handleHover);
+ },
+ ready: function(f) {
+ // If the DOM is already ready
+ if ( jQuery.isReady )
+ // Execute the function immediately
+ f.apply( document );
+
+ // Otherwise, remember the function for later
+ else {
+ // Add the function to the wait list
+ jQuery.readyList.push( f );
+ }
+
+ return this;
+ }
+});
+
+jQuery.extend({
+ /*
+ * All the code that makes DOM Ready work nicely.
+ */
+ isReady: false,
+ readyList: [],
+
+ // Handle when the DOM is ready
+ ready: function() {
+ // Make sure that the DOM is not already loaded
+ if ( !jQuery.isReady ) {
+ // Remember that the DOM is ready
+ jQuery.isReady = true;
+
+ // If there are functions bound, to execute
+ if ( jQuery.readyList ) {
+ // Execute all of them
+ for ( var i = 0; i < jQuery.readyList.length; i++ )
+ jQuery.readyList[i].apply( document );
+
+ // Reset the list of functions
+ jQuery.readyList = null;
+ }
+ }
+ }
+});
+
+new function(){
+
+ var e = ("blur,focus,load,resize,scroll,unload,click,dblclick," +
+ "mousedown,mouseup,mousemove,mouseover,mouseout,change,reset,select," +
+ "submit,keydown,keypress,keyup,error").split(",");
+
+ // Go through all the event names, but make sure that
+ // it is enclosed properly
+ for ( var i = 0; i < e.length; i++ ) new function(){
+
+ var o = e[i];
+
+ // Handle event binding
+ jQuery.fn[o] = function(f){
+ return f ? this.bind(o, f) : this.trigger(o);
+ };
+
+ // Handle event unbinding
+ jQuery.fn["un"+o] = function(f){ return this.unbind(o, f); };
+
+ // Finally, handle events that only fire once
+ jQuery.fn["one"+o] = function(f){
+ // Attach the event listener
+ return this.each(function(){
+
+ var count = 0;
+
+ // Add the event
+ jQuery.event.add( this, o, function(e){
+ // If this function has already been executed, stop
+ if ( count++ ) return;
+
+ // And execute the bound function
+ return f.apply(this, [e]);
+ });
+ });
+ };
+
+ };
+
+ // If Mozilla is used
+ if ( jQuery.browser.mozilla || jQuery.browser.opera ) {
+ // Use the handy event callback
+ document.addEventListener( "DOMContentLoaded", jQuery.ready, false );
+
+ // If IE is used, use the excellent hack by Matthias Miller
+ // http://www.outofhanwell.com/blog/index.php?title=the_window_onload_problem_revisited
+ } else if ( jQuery.browser.msie ) {
+
+ // Only works if you document.write() it
+ document.write("<scr" + "ipt id=__ie_init defer=true " +
+ "src=//:><\/script>");
+
+ // Use the defer script hack
+ var script = document.getElementById("__ie_init");
+ script.onreadystatechange = function() {
+ if ( this.readyState == "complete" )
+ jQuery.ready();
+ };
+
+ // Clear from memory
+ script = null;
+
+ // If Safari is used
+ } else if ( jQuery.browser.safari ) {
+ // Continually check to see if the document.readyState is valid
+ jQuery.safariTimer = setInterval(function(){
+ // loaded and complete are both valid states
+ if ( document.readyState == "loaded" ||
+ document.readyState == "complete" ) {
+
+ // If either one are found, remove the timer
+ clearInterval( jQuery.safariTimer );
+ jQuery.safariTimer = null;
+
+ // and execute any waiting functions
+ jQuery.ready();
+ }
+ }, 10);
+ }
+
+ // A fallback to window.onload, that will always work
+ jQuery.event.add( window, "load", jQuery.ready );
+
+};
+jQuery.fn.extend({
+
+ // overwrite the old show method
+ _show: jQuery.fn.show,
+
+ show: function(speed,callback){
+ return speed ? this.animate({
+ height: "show", width: "show", opacity: "show"
+ }, speed, callback) : this._show();
+ },
+
+ // Overwrite the old hide method
+ _hide: jQuery.fn.hide,
+
+ hide: function(speed,callback){
+ return speed ? this.animate({
+ height: "hide", width: "hide", opacity: "hide"
+ }, speed, callback) : this._hide();
+ },
+
+ slideDown: function(speed,callback){
+ return this.animate({height: "show"}, speed, callback);
+ },
+
+ slideUp: function(speed,callback){
+ return this.animate({height: "hide"}, speed, callback);
+ },
+
+ slideToggle: function(speed,callback){
+ return this.each(function(){
+ var state = $(this).is(":hidden") ? "show" : "hide";
+ $(this).animate({height: state}, speed, callback);
+ });
+ },
+
+ fadeIn: function(speed,callback){
+ return this.animate({opacity: "show"}, speed, callback);
+ },
+
+ fadeOut: function(speed,callback){
+ return this.animate({opacity: "hide"}, speed, callback);
+ },
+
+ fadeTo: function(speed,to,callback){
+ return this.animate({opacity: to}, speed, callback);
+ },
+ animate: function(prop,speed,callback) {
+ return this.queue(function(){
+
+ this.curAnim = prop;
+
+ for ( var p in prop ) {
+ var e = new jQuery.fx( this, jQuery.speed(speed,callback), p );
+ if ( prop[p].constructor == Number )
+ e.custom( e.cur(), prop[p] );
+ else
+ e[ prop[p] ]( prop );
+ }
+
+ });
+ },
+ queue: function(type,fn){
+ if ( !fn ) {
+ fn = type;
+ type = "fx";
+ }
+
+ return this.each(function(){
+ if ( !this.queue )
+ this.queue = {};
+
+ if ( !this.queue[type] )
+ this.queue[type] = [];
+
+ this.queue[type].push( fn );
+
+ if ( this.queue[type].length == 1 )
+ fn.apply(this);
+ });
+ }
+
+});
+
+jQuery.extend({
+
+ setAuto: function(e,p) {
+ if ( e.notAuto ) return;
+
+ if ( p == "height" && e.scrollHeight != parseInt(jQuery.curCSS(e,p)) ) return;
+ if ( p == "width" && e.scrollWidth != parseInt(jQuery.curCSS(e,p)) ) return;
+
+ // Remember the original height
+ var a = e.style[p];
+
+ // Figure out the size of the height right now
+ var o = jQuery.curCSS(e,p,1);
+
+ if ( p == "height" && e.scrollHeight != o ||
+ p == "width" && e.scrollWidth != o ) return;
+
+ // Set the height to auto
+ e.style[p] = e.currentStyle ? "" : "auto";
+
+ // See what the size of "auto" is
+ var n = jQuery.curCSS(e,p,1);
+
+ // Revert back to the original size
+ if ( o != n && n != "auto" ) {
+ e.style[p] = a;
+ e.notAuto = true;
+ }
+ },
+
+ speed: function(s,o) {
+ o = o || {};
+
+ if ( o.constructor == Function )
+ o = { complete: o };
+
+ var ss = { slow: 600, fast: 200 };
+ o.duration = (s && s.constructor == Number ? s : ss[s]) || 400;
+
+ // Queueing
+ o.oldComplete = o.complete;
+ o.complete = function(){
+ jQuery.dequeue(this, "fx");
+ if ( o.oldComplete && o.oldComplete.constructor == Function )
+ o.oldComplete.apply( this );
+ };
+
+ return o;
+ },
+
+ queue: {},
+
+ dequeue: function(elem,type){
+ type = type || "fx";
+
+ if ( elem.queue && elem.queue[type] ) {
+ // Remove self
+ elem.queue[type].shift();
+
+ // Get next function
+ var f = elem.queue[type][0];
+
+ if ( f ) f.apply( elem );
+ }
+ },
+
+ /*
+ * I originally wrote fx() as a clone of moo.fx and in the process
+ * of making it small in size the code became illegible to sane
+ * people. You've been warned.
+ */
+
+ fx: function( elem, options, prop ){
+
+ var z = this;
+
+ // The users options
+ z.o = {
+ duration: options.duration || 400,
+ complete: options.complete,
+ step: options.step
+ };
+
+ // The element
+ z.el = elem;
+
+ // The styles
+ var y = z.el.style;
+
+ // Simple function for setting a style value
+ z.a = function(){
+ if ( options.step )
+ options.step.apply( elem, [ z.now ] );
+
+ if ( prop == "opacity" ) {
+ if (z.now == 1) z.now = 0.9999;
+ if (window.ActiveXObject)
+ y.filter = "alpha(opacity=" + z.now*100 + ")";
+ else
+ y.opacity = z.now;
+
+ // My hate for IE will never die
+ } else if ( parseInt(z.now) )
+ y[prop] = parseInt(z.now) + "px";
+
+ y.display = "block";
+ };
+
+ // Figure out the maximum number to run to
+ z.max = function(){
+ return parseFloat( jQuery.css(z.el,prop) );
+ };
+
+ // Get the current size
+ z.cur = function(){
+ var r = parseFloat( jQuery.curCSS(z.el, prop) );
+ return r && r > -10000 ? r : z.max();
+ };
+
+ // Start an animation from one number to another
+ z.custom = function(from,to){
+ z.startTime = (new Date()).getTime();
+ z.now = from;
+ z.a();
+
+ z.timer = setInterval(function(){
+ z.step(from, to);
+ }, 13);
+ };
+
+ // Simple 'show' function
+ z.show = function( p ){
+ if ( !z.el.orig ) z.el.orig = {};
+
+ // Remember where we started, so that we can go back to it later
+ z.el.orig[prop] = this.cur();
+
+ z.custom( 0, z.el.orig[prop] );
+
+ // Stupid IE, look what you made me do
+ if ( prop != "opacity" )
+ y[prop] = "1px";
+ };
+
+ // Simple 'hide' function
+ z.hide = function(){
+ if ( !z.el.orig ) z.el.orig = {};
+
+ // Remember where we started, so that we can go back to it later
+ z.el.orig[prop] = this.cur();
+
+ z.o.hide = true;
+
+ // Begin the animation
+ z.custom(z.el.orig[prop], 0);
+ };
+
+ // IE has trouble with opacity if it does not have layout
+ if ( jQuery.browser.msie && !z.el.currentStyle.hasLayout )
+ y.zoom = "1";
+
+ // Remember the overflow of the element
+ if ( !z.el.oldOverlay )
+ z.el.oldOverflow = jQuery.css( z.el, "overflow" );
+
+ // Make sure that nothing sneaks out
+ y.overflow = "hidden";
+
+ // Each step of an animation
+ z.step = function(firstNum, lastNum){
+ var t = (new Date()).getTime();
+
+ if (t > z.o.duration + z.startTime) {
+ // Stop the timer
+ clearInterval(z.timer);
+ z.timer = null;
+
+ z.now = lastNum;
+ z.a();
+
+ z.el.curAnim[ prop ] = true;
+
+ var done = true;
+ for ( var i in z.el.curAnim )
+ if ( z.el.curAnim[i] !== true )
+ done = false;
+
+ if ( done ) {
+ // Reset the overflow
+ y.overflow = z.el.oldOverflow;
+
+ // Hide the element if the "hide" operation was done
+ if ( z.o.hide )
+ y.display = 'none';
+
+ // Reset the property, if the item has been hidden
+ if ( z.o.hide ) {
+ for ( var p in z.el.curAnim ) {
+ y[ p ] = z.el.orig[p] + ( p == "opacity" ? "" : "px" );
+
+ // set its height and/or width to auto
+ if ( p == 'height' || p == 'width' )
+ jQuery.setAuto( z.el, p );
+ }
+ }
+ }
+
+ // If a callback was provided, execute it
+ if( done && z.o.complete && z.o.complete.constructor == Function )
+ // Execute the complete function
+ z.o.complete.apply( z.el );
+ } else {
+ // Figure out where in the animation we are and set the number
+ var p = (t - this.startTime) / z.o.duration;
+ z.now = ((-Math.cos(p*Math.PI)/2) + 0.5) * (lastNum-firstNum) + firstNum;
+
+ // Perform the next step of the animation
+ z.a();
+ }
+ };
+
+ }
+
+});
+// AJAX Plugin
+// Docs Here:
+// http://jquery.com/docs/ajax/
+jQuery.fn.loadIfModified = function( url, params, callback ) {
+ this.load( url, params, callback, 1 );
+};
+
+jQuery.fn.load = function( url, params, callback, ifModified ) {
+ if ( url.constructor == Function )
+ return this.bind("load", url);
+
+ callback = callback || function(){};
+
+ // Default to a GET request
+ var type = "GET";
+
+ // If the second parameter was provided
+ if ( params ) {
+ // If it's a function
+ if ( params.constructor == Function ) {
+ // We assume that it's the callback
+ callback = params;
+ params = null;
+
+ // Otherwise, build a param string
+ } else {
+ params = jQuery.param( params );
+ type = "POST";
+ }
+ }
+
+ var self = this;
+
+ // Request the remote document
+ jQuery.ajax( type, url, params,function(res, status){
+
+ if ( status == "success" || !ifModified && status == "notmodified" ) {
+ // Inject the HTML into all the matched elements
+ self.html(res.responseText).each( callback, [res.responseText, status] );
+
+ // Execute all the scripts inside of the newly-injected HTML
+ $("script", self).each(function(){
+ if ( this.src )
+ $.getScript( this.src );
+ else
+ eval.call( window, this.text || this.textContent || this.innerHTML || "" );
+ });
+ } else
+ callback.apply( self, [res.responseText, status] );
+
+ }, ifModified);
+
+ return this;
+};
+
+// If IE is used, create a wrapper for the XMLHttpRequest object
+if ( jQuery.browser.msie )
+ XMLHttpRequest = function(){
+ return new ActiveXObject(
+ navigator.userAgent.indexOf("MSIE 5") >= 0 ?
+ "Microsoft.XMLHTTP" : "Msxml2.XMLHTTP"
+ );
+ };
+
+// Attach a bunch of functions for handling common AJAX events
+new function(){
+ var e = "ajaxStart,ajaxStop,ajaxComplete,ajaxError,ajaxSuccess".split(',');
+
+ for ( var i = 0; i < e.length; i++ ) new function(){
+ var o = e[i];
+ jQuery.fn[o] = function(f){
+ return this.bind(o, f);
+ };
+ };
+};
+
+jQuery.extend({
+ get: function( url, data, callback, type, ifModified ) {
+ if ( data.constructor == Function ) {
+ type = callback;
+ callback = data;
+ data = null;
+ }
+
+ if ( data ) url += "?" + jQuery.param(data);
+
+ // Build and start the HTTP Request
+ jQuery.ajax( "GET", url, null, function(r, status) {
+ if ( callback ) callback( jQuery.httpData(r,type), status );
+ }, ifModified);
+ },
+
+ getIfModified: function( url, data, callback, type ) {
+ jQuery.get(url, data, callback, type, 1);
+ },
+
+ getScript: function( url, data, callback ) {
+ jQuery.get(url, data, callback, "script");
+ },
+ post: function( url, data, callback, type ) {
+ // Build and start the HTTP Request
+ jQuery.ajax( "POST", url, jQuery.param(data), function(r, status) {
+ if ( callback ) callback( jQuery.httpData(r,type), status );
+ });
+ },
+
+ // timeout (ms)
+ timeout: 0,
+
+ ajaxTimeout: function(timeout) {
+ jQuery.timeout = timeout;
+ },
+
+ // Last-Modified header cache for next request
+ lastModified: {},
+ ajax: function( type, url, data, ret, ifModified ) {
+ // If only a single argument was passed in,
+ // assume that it is a object of key/value pairs
+ if ( !url ) {
+ ret = type.complete;
+ var success = type.success;
+ var error = type.error;
+ data = type.data;
+ url = type.url;
+ type = type.type;
+ }
+
+ // Watch for a new set of requests
+ if ( ! jQuery.active++ )
+ jQuery.event.trigger( "ajaxStart" );
+
+ var requestDone = false;
+
+ // Create the request object
+ var xml = new XMLHttpRequest();
+
+ // Open the socket
+ xml.open(type || "GET", url, true);
+
+ // Set the correct header, if data is being sent
+ if ( data )
+ xml.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
+
+ // Set the If-Modified-Since header, if ifModified mode.
+ if ( ifModified )
+ xml.setRequestHeader("If-Modified-Since",
+ jQuery.lastModified[url] || "Thu, 01 Jan 1970 00:00:00 GMT" );
+
+ // Set header so calling script knows that it's an XMLHttpRequest
+ xml.setRequestHeader("X-Requested-With", "XMLHttpRequest");
+
+ // Make sure the browser sends the right content length
+ if ( xml.overrideMimeType )
+ xml.setRequestHeader("Connection", "close");
+
+ // Wait for a response to come back
+ var onreadystatechange = function(istimeout){
+ // The transfer is complete and the data is available, or the request timed out
+ if ( xml && (xml.readyState == 4 || istimeout == "timeout") ) {
+ requestDone = true;
+
+ var status = jQuery.httpSuccess( xml ) && istimeout != "timeout" ?
+ ifModified && jQuery.httpNotModified( xml, url ) ? "notmodified" : "success" : "error";
+
+ // Make sure that the request was successful or notmodified
+ if ( status != "error" ) {
+ // Cache Last-Modified header, if ifModified mode.
+ var modRes = xml.getResponseHeader("Last-Modified");
+ if ( ifModified && modRes ) jQuery.lastModified[url] = modRes;
+
+ // If a local callback was specified, fire it
+ if ( success ) success( xml, status );
+
+ // Fire the global callback
+ jQuery.event.trigger( "ajaxSuccess" );
+
+ // Otherwise, the request was not successful
+ } else {
+ // If a local callback was specified, fire it
+ if ( error ) error( xml, status );
+
+ // Fire the global callback
+ jQuery.event.trigger( "ajaxError" );
+ }
+
+ // The request was completed
+ jQuery.event.trigger( "ajaxComplete" );
+
+ // Handle the global AJAX counter
+ if ( ! --jQuery.active )
+ jQuery.event.trigger( "ajaxStop" );
+
+ // Process result
+ if ( ret ) ret(xml, status);
+
+ // Stop memory leaks
+ xml.onreadystatechange = function(){};
+ xml = null;
+
+ }
+ };
+ xml.onreadystatechange = onreadystatechange;
+
+ // Timeout checker
+ if(jQuery.timeout > 0)
+ setTimeout(function(){
+ // Check to see if the request is still happening
+ if (xml) {
+ // Cancel the request
+ xml.abort();
+
+ if ( !requestDone ) onreadystatechange( "timeout" );
+
+ // Clear from memory
+ xml = null;
+ }
+ }, jQuery.timeout);
+
+ // Send the data
+ xml.send(data);
+ },
+
+ // Counter for holding the number of active queries
+ active: 0,
+
+ // Determines if an XMLHttpRequest was successful or not
+ httpSuccess: function(r) {
+ try {
+ return !r.status && location.protocol == "file:" ||
+ ( r.status >= 200 && r.status < 300 ) || r.status == 304 ||
+ jQuery.browser.safari && r.status == undefined;
+ } catch(e){}
+
+ return false;
+ },
+
+ // Determines if an XMLHttpRequest returns NotModified
+ httpNotModified: function(xml, url) {
+ try {
+ var xmlRes = xml.getResponseHeader("Last-Modified");
+
+ // Firefox always returns 200. check Last-Modified date
+ return xml.status == 304 || xmlRes == jQuery.lastModified[url] ||
+ jQuery.browser.safari && xml.status == undefined;
+ } catch(e){}
+
+ return false;
+ },
+
+ // Get the data out of an XMLHttpRequest.
+ // Return parsed XML if content-type header is "xml" and type is "xml" or omitted,
+ // otherwise return plain text.
+ httpData: function(r,type) {
+ var ct = r.getResponseHeader("content-type");
+ var data = !type && ct && ct.indexOf("xml") >= 0;
+ data = type == "xml" || data ? r.responseXML : r.responseText;
+
+ // If the type is "script", eval it
+ if ( type == "script" ) eval.call( window, data );
+
+ return data;
+ },
+
+ // Serialize an array of form elements or a set of
+ // key/values into a query string
+ param: function(a) {
+ var s = [];
+
+ // If an array was passed in, assume that it is an array
+ // of form elements
+ if ( a.constructor == Array ) {
+ // Serialize the form elements
+ for ( var i = 0; i < a.length; i++ )
+ s.push( a[i].name + "=" + encodeURIComponent( a[i].value ) );
+
+ // Otherwise, assume that it's an object of key/value pairs
+ } else {
+ // Serialize the key/values
+ for ( var j in a )
+ s.push( j + "=" + encodeURIComponent( a[j] ) );
+ }
+
+ // Return the resulting serialization
+ return s.join("&");
+ }
+
+});
diff --git a/devtools/client/inspector/markup/test/lib_jquery_1.1.js b/devtools/client/inspector/markup/test/lib_jquery_1.1.js
new file mode 100644
index 000000000..981a3bdc1
--- /dev/null
+++ b/devtools/client/inspector/markup/test/lib_jquery_1.1.js
@@ -0,0 +1,2172 @@
+/* prevent execution of jQuery if included more than once */
+if(typeof window.jQuery == "undefined") {
+/*
+ * jQuery 1.1 - New Wave Javascript
+ *
+ * Copyright (c) 2007 John Resig (jquery.com)
+ * Dual licensed under the MIT (MIT-LICENSE.txt)
+ * and GPL (GPL-LICENSE.txt) licenses.
+ *
+ * $Date: 2007-01-14 17:37:33 -0500 (Sun, 14 Jan 2007) $
+ * $Rev: 1073 $
+ */
+
+// Global undefined variable
+window.undefined = window.undefined;
+var jQuery = function(a,c) {
+ // If the context is global, return a new object
+ if ( window == this )
+ return new jQuery(a,c);
+
+ // Make sure that a selection was provided
+ a = a || document;
+
+ // HANDLE: $(function)
+ // Shortcut for document ready
+ // Safari reports typeof on DOM NodeLists as a function
+ if ( jQuery.isFunction(a) && !a.nodeType && a[0] == undefined )
+ return new jQuery(document)[ jQuery.fn.ready ? "ready" : "load" ]( a );
+
+ // Handle HTML strings
+ if ( typeof a == "string" ) {
+ var m = /^[^<]*(<.+>)[^>]*$/.exec(a);
+
+ a = m ?
+ // HANDLE: $(html) -> $(array)
+ jQuery.clean( [ m[1] ] ) :
+
+ // HANDLE: $(expr)
+ jQuery.find( a, c );
+ }
+
+ return this.setArray(
+ // HANDLE: $(array)
+ a.constructor == Array && a ||
+
+ // HANDLE: $(arraylike)
+ // Watch for when an array-like object is passed as the selector
+ (a.jquery || a.length && a != window && !a.nodeType && a[0] != undefined && a[0].nodeType) && jQuery.makeArray( a ) ||
+
+ // HANDLE: $(*)
+ [ a ] );
+};
+
+// Map over the $ in case of overwrite
+if ( typeof $ != "undefined" )
+ jQuery._$ = $;
+
+// Map the jQuery namespace to the '$' one
+var $ = jQuery;
+
+jQuery.fn = jQuery.prototype = {
+ jquery: "1.1",
+
+ size: function() {
+ return this.length;
+ },
+
+ length: 0,
+
+ get: function( num ) {
+ return num == undefined ?
+
+ // Return a 'clean' array
+ jQuery.makeArray( this ) :
+
+ // Return just the object
+ this[num];
+ },
+ pushStack: function( a ) {
+ var ret = jQuery(this);
+ ret.prevObject = this;
+ return ret.setArray( a );
+ },
+ setArray: function( a ) {
+ this.length = 0;
+ [].push.apply( this, a );
+ return this;
+ },
+ each: function( fn, args ) {
+ return jQuery.each( this, fn, args );
+ },
+ index: function( obj ) {
+ var pos = -1;
+ this.each(function(i){
+ if ( this == obj ) pos = i;
+ });
+ return pos;
+ },
+
+ attr: function( key, value, type ) {
+ var obj = key;
+
+ // Look for the case where we're accessing a style value
+ if ( key.constructor == String )
+ if ( value == undefined )
+ return jQuery[ type || "attr" ]( this[0], key );
+ else {
+ obj = {};
+ obj[ key ] = value;
+ }
+
+ // Check to see if we're setting style values
+ return this.each(function(){
+ // Set all the styles
+ for ( var prop in obj )
+ jQuery.attr(
+ type ? this.style : this,
+ prop, jQuery.prop(this, obj[prop], type)
+ );
+ });
+ },
+
+ css: function( key, value ) {
+ return this.attr( key, value, "curCSS" );
+ },
+
+ text: function(e) {
+ if ( typeof e == "string" )
+ return this.empty().append( document.createTextNode( e ) );
+
+ var t = "";
+ jQuery.each( e || this, function(){
+ jQuery.each( this.childNodes, function(){
+ if ( this.nodeType != 8 )
+ t += this.nodeType != 1 ?
+ this.nodeValue : jQuery.fn.text([ this ]);
+ });
+ });
+ return t;
+ },
+
+ wrap: function() {
+ // The elements to wrap the target around
+ var a = jQuery.clean(arguments);
+
+ // Wrap each of the matched elements individually
+ return this.each(function(){
+ // Clone the structure that we're using to wrap
+ var b = a[0].cloneNode(true);
+
+ // Insert it before the element to be wrapped
+ this.parentNode.insertBefore( b, this );
+
+ // Find the deepest point in the wrap structure
+ while ( b.firstChild )
+ b = b.firstChild;
+
+ // Move the matched element to within the wrap structure
+ b.appendChild( this );
+ });
+ },
+ append: function() {
+ return this.domManip(arguments, true, 1, function(a){
+ this.appendChild( a );
+ });
+ },
+ prepend: function() {
+ return this.domManip(arguments, true, -1, function(a){
+ this.insertBefore( a, this.firstChild );
+ });
+ },
+ before: function() {
+ return this.domManip(arguments, false, 1, function(a){
+ this.parentNode.insertBefore( a, this );
+ });
+ },
+ after: function() {
+ return this.domManip(arguments, false, -1, function(a){
+ this.parentNode.insertBefore( a, this.nextSibling );
+ });
+ },
+ end: function() {
+ return this.prevObject || jQuery([]);
+ },
+ find: function(t) {
+ return this.pushStack( jQuery.map( this, function(a){
+ return jQuery.find(t,a);
+ }) );
+ },
+ clone: function(deep) {
+ return this.pushStack( jQuery.map( this, function(a){
+ return a.cloneNode( deep != undefined ? deep : true );
+ }) );
+ },
+
+ filter: function(t) {
+ return this.pushStack(
+ jQuery.isFunction( t ) &&
+ jQuery.grep(this, function(el, index){
+ return t.apply(el, [index])
+ }) ||
+
+ jQuery.multiFilter(t,this) );
+ },
+
+ not: function(t) {
+ return this.pushStack(
+ t.constructor == String &&
+ jQuery.multiFilter(t,this,true) ||
+
+ jQuery.grep(this,function(a){
+ if ( t.constructor == Array || t.jquery )
+ return jQuery.inArray( t, a ) < 0;
+ else
+ return a != t;
+ }) );
+ },
+
+ add: function(t) {
+ return this.pushStack( jQuery.merge(
+ this.get(),
+ typeof t == "string" ? jQuery(t).get() : t )
+ );
+ },
+ is: function(expr) {
+ return expr ? jQuery.filter(expr,this).r.length > 0 : false;
+ },
+
+ val: function( val ) {
+ return val == undefined ?
+ ( this.length ? this[0].value : null ) :
+ this.attr( "value", val );
+ },
+
+ html: function( val ) {
+ return val == undefined ?
+ ( this.length ? this[0].innerHTML : null ) :
+ this.empty().append( val );
+ },
+ domManip: function(args, table, dir, fn){
+ var clone = this.length > 1;
+ var a = jQuery.clean(args);
+ if ( dir < 0 )
+ a.reverse();
+
+ return this.each(function(){
+ var obj = this;
+
+ if ( table && this.nodeName.toUpperCase() == "TABLE" && a[0].nodeName.toUpperCase() == "TR" )
+ obj = this.getElementsByTagName("tbody")[0] || this.appendChild(document.createElement("tbody"));
+
+ jQuery.each( a, function(){
+ fn.apply( obj, [ clone ? this.cloneNode(true) : this ] );
+ });
+
+ });
+ }
+};
+
+jQuery.extend = jQuery.fn.extend = function() {
+ // copy reference to target object
+ var target = arguments[0],
+ a = 1;
+
+ // extend jQuery itself if only one argument is passed
+ if ( arguments.length == 1 ) {
+ target = this;
+ a = 0;
+ }
+ var prop;
+ while (prop = arguments[a++])
+ // Extend the base object
+ for ( var i in prop ) target[i] = prop[i];
+
+ // Return the modified object
+ return target;
+};
+
+jQuery.extend({
+ noConflict: function() {
+ if ( jQuery._$ )
+ $ = jQuery._$;
+ },
+
+ isFunction: function( fn ) {
+ return fn && typeof fn == "function";
+ },
+ // args is for internal usage only
+ each: function( obj, fn, args ) {
+ if ( obj.length == undefined )
+ for ( var i in obj )
+ fn.apply( obj[i], args || [i, obj[i]] );
+ else
+ for ( var i = 0, ol = obj.length; i < ol; i++ )
+ if ( fn.apply( obj[i], args || [i, obj[i]] ) === false ) break;
+ return obj;
+ },
+
+ prop: function(elem, value, type){
+ // Handle executable functions
+ if ( jQuery.isFunction( value ) )
+ return value.call( elem );
+
+ // Handle passing in a number to a CSS property
+ if ( value.constructor == Number && type == "curCSS" )
+ return value + "px";
+
+ return value;
+ },
+
+ className: {
+ // internal only, use addClass("class")
+ add: function( elem, c ){
+ jQuery.each( c.split(/\s+/), function(i, cur){
+ if ( !jQuery.className.has( elem.className, cur ) )
+ elem.className += ( elem.className ? " " : "" ) + cur;
+ });
+ },
+
+ // internal only, use removeClass("class")
+ remove: function( elem, c ){
+ elem.className = c ?
+ jQuery.grep( elem.className.split(/\s+/), function(cur){
+ return !jQuery.className.has( c, cur );
+ }).join(" ") : "";
+ },
+
+ // internal only, use is(".class")
+ has: function( t, c ) {
+ t = t.className || t;
+ return t && new RegExp("(^|\\s)" + c + "(\\s|$)").test( t );
+ }
+ },
+ swap: function(e,o,f) {
+ for ( var i in o ) {
+ e.style["old"+i] = e.style[i];
+ e.style[i] = o[i];
+ }
+ f.apply( e, [] );
+ for ( var i in o )
+ e.style[i] = e.style["old"+i];
+ },
+
+ css: function(e,p) {
+ if ( p == "height" || p == "width" ) {
+ var old = {}, oHeight, oWidth, d = ["Top","Bottom","Right","Left"];
+
+ jQuery.each( d, function(){
+ old["padding" + this] = 0;
+ old["border" + this + "Width"] = 0;
+ });
+
+ jQuery.swap( e, old, function() {
+ if (jQuery.css(e,"display") != "none") {
+ oHeight = e.offsetHeight;
+ oWidth = e.offsetWidth;
+ } else {
+ e = jQuery(e.cloneNode(true))
+ .find(":radio").removeAttr("checked").end()
+ .css({
+ visibility: "hidden", position: "absolute", display: "block", right: "0", left: "0"
+ }).appendTo(e.parentNode)[0];
+
+ var parPos = jQuery.css(e.parentNode,"position");
+ if ( parPos == "" || parPos == "static" )
+ e.parentNode.style.position = "relative";
+
+ oHeight = e.clientHeight;
+ oWidth = e.clientWidth;
+
+ if ( parPos == "" || parPos == "static" )
+ e.parentNode.style.position = "static";
+
+ e.parentNode.removeChild(e);
+ }
+ });
+
+ return p == "height" ? oHeight : oWidth;
+ }
+
+ return jQuery.curCSS( e, p );
+ },
+
+ curCSS: function(elem, prop, force) {
+ var ret;
+
+ if (prop == "opacity" && jQuery.browser.msie)
+ return jQuery.attr(elem.style, "opacity");
+
+ if (prop == "float" || prop == "cssFloat")
+ prop = jQuery.browser.msie ? "styleFloat" : "cssFloat";
+
+ if (!force && elem.style[prop])
+ ret = elem.style[prop];
+
+ else if (document.defaultView && document.defaultView.getComputedStyle) {
+
+ if (prop == "cssFloat" || prop == "styleFloat")
+ prop = "float";
+
+ prop = prop.replace(/([A-Z])/g,"-$1").toLowerCase();
+ var cur = document.defaultView.getComputedStyle(elem, null);
+
+ if ( cur )
+ ret = cur.getPropertyValue(prop);
+ else if ( prop == "display" )
+ ret = "none";
+ else
+ jQuery.swap(elem, { display: "block" }, function() {
+ var c = document.defaultView.getComputedStyle(this, "");
+ ret = c && c.getPropertyValue(prop) || "";
+ });
+
+ } else if (elem.currentStyle) {
+
+ var newProp = prop.replace(/\-(\w)/g,function(m,c){return c.toUpperCase();});
+ ret = elem.currentStyle[prop] || elem.currentStyle[newProp];
+
+ }
+
+ return ret;
+ },
+
+ clean: function(a) {
+ var r = [];
+
+ jQuery.each( a, function(i,arg){
+ if ( !arg ) return;
+
+ if ( arg.constructor == Number )
+ arg = arg.toString();
+
+ // Convert html string into DOM nodes
+ if ( typeof arg == "string" ) {
+ // Trim whitespace, otherwise indexOf won't work as expected
+ var s = jQuery.trim(arg), div = document.createElement("div"), tb = [];
+
+ var wrap =
+ // option or optgroup
+ !s.indexOf("<opt") &&
+ [1, "<select>", "</select>"] ||
+
+ (!s.indexOf("<thead") || !s.indexOf("<tbody") || !s.indexOf("<tfoot")) &&
+ [1, "<table>", "</table>"] ||
+
+ !s.indexOf("<tr") &&
+ [2, "<table><tbody>", "</tbody></table>"] ||
+
+ // <thead> matched above
+ (!s.indexOf("<td") || !s.indexOf("<th")) &&
+ [3, "<table><tbody><tr>", "</tr></tbody></table>"] ||
+
+ [0,"",""];
+
+ // Go to html and back, then peel off extra wrappers
+ div.innerHTML = wrap[1] + s + wrap[2];
+
+ // Move to the right depth
+ while ( wrap[0]-- )
+ div = div.firstChild;
+
+ // Remove IE's autoinserted <tbody> from table fragments
+ if ( jQuery.browser.msie ) {
+
+ // String was a <table>, *may* have spurious <tbody>
+ if ( !s.indexOf("<table") && s.indexOf("<tbody") < 0 )
+ tb = div.firstChild && div.firstChild.childNodes;
+
+ // String was a bare <thead> or <tfoot>
+ else if ( wrap[1] == "<table>" && s.indexOf("<tbody") < 0 )
+ tb = div.childNodes;
+
+ for ( var n = tb.length-1; n >= 0 ; --n )
+ if ( tb[n].nodeName.toUpperCase() == "TBODY" && !tb[n].childNodes.length )
+ tb[n].parentNode.removeChild(tb[n]);
+
+ }
+
+ arg = div.childNodes;
+ }
+
+ if ( arg.length === 0 )
+ return;
+
+ if ( arg[0] == undefined )
+ r.push( arg );
+ else
+ r = jQuery.merge( r, arg );
+
+ });
+
+ return r;
+ },
+
+ attr: function(elem, name, value){
+ var fix = {
+ "for": "htmlFor",
+ "class": "className",
+ "float": jQuery.browser.msie ? "styleFloat" : "cssFloat",
+ cssFloat: jQuery.browser.msie ? "styleFloat" : "cssFloat",
+ innerHTML: "innerHTML",
+ className: "className",
+ value: "value",
+ disabled: "disabled",
+ checked: "checked",
+ readonly: "readOnly",
+ selected: "selected"
+ };
+
+ // IE actually uses filters for opacity ... elem is actually elem.style
+ if ( name == "opacity" && jQuery.browser.msie && value != undefined ) {
+ // IE has trouble with opacity if it does not have layout
+ // Force it by setting the zoom level
+ elem.zoom = 1;
+
+ // Set the alpha filter to set the opacity
+ return elem.filter = elem.filter.replace(/alpha\([^\)]*\)/gi,"") +
+ ( value == 1 ? "" : "alpha(opacity=" + value * 100 + ")" );
+
+ } else if ( name == "opacity" && jQuery.browser.msie )
+ return elem.filter ?
+ parseFloat( elem.filter.match(/alpha\(opacity=(.*)\)/)[1] ) / 100 : 1;
+
+ // Mozilla doesn't play well with opacity 1
+ if ( name == "opacity" && jQuery.browser.mozilla && value == 1 )
+ value = 0.9999;
+
+ // Certain attributes only work when accessed via the old DOM 0 way
+ if ( fix[name] ) {
+ if ( value != undefined ) elem[fix[name]] = value;
+ return elem[fix[name]];
+
+ } else if ( value == undefined && jQuery.browser.msie && elem.nodeName && elem.nodeName.toUpperCase() == "FORM" && (name == "action" || name == "method") )
+ return elem.getAttributeNode(name).nodeValue;
+
+ // IE elem.getAttribute passes even for style
+ else if ( elem.tagName ) {
+ if ( value != undefined ) elem.setAttribute( name, value );
+ return elem.getAttribute( name );
+
+ } else {
+ name = name.replace(/-([a-z])/ig,function(z,b){return b.toUpperCase();});
+ if ( value != undefined ) elem[name] = value;
+ return elem[name];
+ }
+ },
+ trim: function(t){
+ return t.replace(/^\s+|\s+$/g, "");
+ },
+
+ makeArray: function( a ) {
+ var r = [];
+
+ if ( a.constructor != Array )
+ for ( var i = 0, al = a.length; i < al; i++ )
+ r.push( a[i] );
+ else
+ r = a.slice( 0 );
+
+ return r;
+ },
+
+ inArray: function( b, a ) {
+ for ( var i = 0, al = a.length; i < al; i++ )
+ if ( a[i] == b )
+ return i;
+ return -1;
+ },
+ merge: function(first, second) {
+ var r = [].slice.call( first, 0 );
+
+ // Now check for duplicates between the two arrays
+ // and only add the unique items
+ for ( var i = 0, sl = second.length; i < sl; i++ )
+ // Check for duplicates
+ if ( jQuery.inArray( second[i], r ) == -1 )
+ // The item is unique, add it
+ first.push( second[i] );
+
+ return first;
+ },
+ grep: function(elems, fn, inv) {
+ // If a string is passed in for the function, make a function
+ // for it (a handy shortcut)
+ if ( typeof fn == "string" )
+ fn = new Function("a","i","return " + fn);
+
+ var result = [];
+
+ // Go through the array, only saving the items
+ // that pass the validator function
+ for ( var i = 0, el = elems.length; i < el; i++ )
+ if ( !inv && fn(elems[i],i) || inv && !fn(elems[i],i) )
+ result.push( elems[i] );
+
+ return result;
+ },
+ map: function(elems, fn) {
+ // If a string is passed in for the function, make a function
+ // for it (a handy shortcut)
+ if ( typeof fn == "string" )
+ fn = new Function("a","return " + fn);
+
+ var result = [], r = [];
+
+ // Go through the array, translating each of the items to their
+ // new value (or values).
+ for ( var i = 0, el = elems.length; i < el; i++ ) {
+ var val = fn(elems[i],i);
+
+ if ( val !== null && val != undefined ) {
+ if ( val.constructor != Array ) val = [val];
+ result = result.concat( val );
+ }
+ }
+
+ var r = result.length ? [ result[0] ] : [];
+
+ check: for ( var i = 1, rl = result.length; i < rl; i++ ) {
+ for ( var j = 0; j < i; j++ )
+ if ( result[i] == r[j] )
+ continue check;
+
+ r.push( result[i] );
+ }
+
+ return r;
+ }
+});
+
+/*
+ * Whether the W3C compliant box model is being used.
+ *
+ * @property
+ * @name $.boxModel
+ * @type Boolean
+ * @cat JavaScript
+ */
+new function() {
+ var b = navigator.userAgent.toLowerCase();
+
+ // Figure out what browser is being used
+ jQuery.browser = {
+ safari: /webkit/.test(b),
+ opera: /opera/.test(b),
+ msie: /msie/.test(b) && !/opera/.test(b),
+ mozilla: /mozilla/.test(b) && !/(compatible|webkit)/.test(b)
+ };
+
+ // Check to see if the W3C box model is being used
+ jQuery.boxModel = !jQuery.browser.msie || document.compatMode == "CSS1Compat";
+};
+
+jQuery.each({
+ parent: "a.parentNode",
+ parents: "jQuery.parents(a)",
+ next: "jQuery.nth(a,2,'nextSibling')",
+ prev: "jQuery.nth(a,2,'previousSibling')",
+ siblings: "jQuery.sibling(a.parentNode.firstChild,a)",
+ children: "jQuery.sibling(a.firstChild)"
+}, function(i,n){
+ jQuery.fn[ i ] = function(a) {
+ var ret = jQuery.map(this,n);
+ if ( a && typeof a == "string" )
+ ret = jQuery.multiFilter(a,ret);
+ return this.pushStack( ret );
+ };
+});
+
+jQuery.each({
+ appendTo: "append",
+ prependTo: "prepend",
+ insertBefore: "before",
+ insertAfter: "after"
+}, function(i,n){
+ jQuery.fn[ i ] = function(){
+ var a = arguments;
+ return this.each(function(){
+ for ( var j = 0, al = a.length; j < al; j++ )
+ jQuery(a[j])[n]( this );
+ });
+ };
+});
+
+jQuery.each( {
+ removeAttr: function( key ) {
+ jQuery.attr( this, key, "" );
+ this.removeAttribute( key );
+ },
+ addClass: function(c){
+ jQuery.className.add(this,c);
+ },
+ removeClass: function(c){
+ jQuery.className.remove(this,c);
+ },
+ toggleClass: function( c ){
+ jQuery.className[ jQuery.className.has(this,c) ? "remove" : "add" ](this, c);
+ },
+ remove: function(a){
+ if ( !a || jQuery.filter( a, [this] ).r.length )
+ this.parentNode.removeChild( this );
+ },
+ empty: function() {
+ while ( this.firstChild )
+ this.removeChild( this.firstChild );
+ }
+}, function(i,n){
+ jQuery.fn[ i ] = function() {
+ return this.each( n, arguments );
+ };
+});
+
+jQuery.each( [ "eq", "lt", "gt", "contains" ], function(i,n){
+ jQuery.fn[ n ] = function(num,fn) {
+ return this.filter( ":" + n + "(" + num + ")", fn );
+ };
+});
+
+jQuery.each( [ "height", "width" ], function(i,n){
+ jQuery.fn[ n ] = function(h) {
+ return h == undefined ?
+ ( this.length ? jQuery.css( this[0], n ) : null ) :
+ this.css( n, h.constructor == String ? h : h + "px" );
+ };
+});
+jQuery.extend({
+ expr: {
+ "": "m[2]=='*'||a.nodeName.toUpperCase()==m[2].toUpperCase()",
+ "#": "a.getAttribute('id')==m[2]",
+ ":": {
+ // Position Checks
+ lt: "i<m[3]-0",
+ gt: "i>m[3]-0",
+ nth: "m[3]-0==i",
+ eq: "m[3]-0==i",
+ first: "i==0",
+ last: "i==r.length-1",
+ even: "i%2==0",
+ odd: "i%2",
+
+ // Child Checks
+ "nth-child": "jQuery.nth(a.parentNode.firstChild,m[3],'nextSibling',a)==a",
+ "first-child": "jQuery.nth(a.parentNode.firstChild,1,'nextSibling')==a",
+ "last-child": "jQuery.nth(a.parentNode.lastChild,1,'previousSibling')==a",
+ "only-child": "jQuery.sibling(a.parentNode.firstChild).length==1",
+
+ // Parent Checks
+ parent: "a.firstChild",
+ empty: "!a.firstChild",
+
+ // Text Check
+ contains: "jQuery.fn.text.apply([a]).indexOf(m[3])>=0",
+
+ // Visibility
+ visible: 'a.type!="hidden"&&jQuery.css(a,"display")!="none"&&jQuery.css(a,"visibility")!="hidden"',
+ hidden: 'a.type=="hidden"||jQuery.css(a,"display")=="none"||jQuery.css(a,"visibility")=="hidden"',
+
+ // Form attributes
+ enabled: "!a.disabled",
+ disabled: "a.disabled",
+ checked: "a.checked",
+ selected: "a.selected||jQuery.attr(a,'selected')",
+
+ // Form elements
+ text: "a.type=='text'",
+ radio: "a.type=='radio'",
+ checkbox: "a.type=='checkbox'",
+ file: "a.type=='file'",
+ password: "a.type=='password'",
+ submit: "a.type=='submit'",
+ image: "a.type=='image'",
+ reset: "a.type=='reset'",
+ button: 'a.type=="button"||a.nodeName=="BUTTON"',
+ input: "/input|select|textarea|button/i.test(a.nodeName)"
+ },
+ ".": "jQuery.className.has(a,m[2])",
+ "@": {
+ "=": "z==m[4]",
+ "!=": "z!=m[4]",
+ "^=": "z&&!z.indexOf(m[4])",
+ "$=": "z&&z.substr(z.length - m[4].length,m[4].length)==m[4]",
+ "*=": "z&&z.indexOf(m[4])>=0",
+ "": "z",
+ _resort: function(m){
+ return ["", m[1], m[3], m[2], m[5]];
+ },
+ _prefix: "z=a[m[3]]||jQuery.attr(a,m[3]);"
+ },
+ "[": "jQuery.find(m[2],a).length"
+ },
+
+ // The regular expressions that power the parsing engine
+ parse: [
+ // Match: [@value='test'], [@foo]
+ /^\[ *(@)([a-z0-9_-]*) *([!*$^=]*) *('?"?)(.*?)\4 *\]/i,
+
+ // Match: [div], [div p]
+ /^(\[)\s*(.*?(\[.*?\])?[^[]*?)\s*\]/,
+
+ // Match: :contains('foo')
+ /^(:)([a-z0-9_-]*)\("?'?(.*?(\(.*?\))?[^(]*?)"?'?\)/i,
+
+ // Match: :even, :last-chlid
+ /^([:.#]*)([a-z0-9_*-]*)/i
+ ],
+
+ token: [
+ /^(\/?\.\.)/, "a.parentNode",
+ /^(>|\/)/, "jQuery.sibling(a.firstChild)",
+ /^(\+)/, "jQuery.nth(a,2,'nextSibling')",
+ /^(~)/, function(a){
+ var s = jQuery.sibling(a.parentNode.firstChild);
+ return s.slice(0, jQuery.inArray(a,s));
+ }
+ ],
+
+ multiFilter: function( expr, elems, not ) {
+ var old, cur = [];
+
+ while ( expr && expr != old ) {
+ old = expr;
+ var f = jQuery.filter( expr, elems, not );
+ expr = f.t.replace(/^\s*,\s*/, "" );
+ cur = not ? elems = f.r : jQuery.merge( cur, f.r );
+ }
+
+ return cur;
+ },
+ find: function( t, context ) {
+ // Quickly handle non-string expressions
+ if ( typeof t != "string" )
+ return [ t ];
+
+ // Make sure that the context is a DOM Element
+ if ( context && !context.nodeType )
+ context = null;
+
+ // Set the correct context (if none is provided)
+ context = context || document;
+
+ // Handle the common XPath // expression
+ if ( !t.indexOf("//") ) {
+ context = context.documentElement;
+ t = t.substr(2,t.length);
+
+ // And the / root expression
+ } else if ( !t.indexOf("/") ) {
+ context = context.documentElement;
+ t = t.substr(1,t.length);
+ if ( t.indexOf("/") >= 1 )
+ t = t.substr(t.indexOf("/"),t.length);
+ }
+
+ // Initialize the search
+ var ret = [context], done = [], last = null;
+
+ // Continue while a selector expression exists, and while
+ // we're no longer looping upon ourselves
+ while ( t && last != t ) {
+ var r = [];
+ last = t;
+
+ t = jQuery.trim(t).replace( /^\/\//i, "" );
+
+ var foundToken = false;
+
+ // An attempt at speeding up child selectors that
+ // point to a specific element tag
+ var re = /^[\/>]\s*([a-z0-9*-]+)/i;
+ var m = re.exec(t);
+
+ if ( m ) {
+ // Perform our own iteration and filter
+ jQuery.each( ret, function(){
+ for ( var c = this.firstChild; c; c = c.nextSibling )
+ if ( c.nodeType == 1 && ( c.nodeName == m[1].toUpperCase() || m[1] == "*" ) )
+ r.push( c );
+ });
+
+ ret = r;
+ t = jQuery.trim( t.replace( re, "" ) );
+ foundToken = true;
+ } else {
+ // Look for pre-defined expression tokens
+ for ( var i = 0; i < jQuery.token.length; i += 2 ) {
+ // Attempt to match each, individual, token in
+ // the specified order
+ var re = jQuery.token[i];
+ var m = re.exec(t);
+
+ // If the token match was found
+ if ( m ) {
+ // Map it against the token's handler
+ r = ret = jQuery.map( ret, jQuery.isFunction( jQuery.token[i+1] ) ?
+ jQuery.token[i+1] :
+ function(a){ return eval(jQuery.token[i+1]); });
+
+ // And remove the token
+ t = jQuery.trim( t.replace( re, "" ) );
+ foundToken = true;
+ break;
+ }
+ }
+ }
+
+ // See if there's still an expression, and that we haven't already
+ // matched a token
+ if ( t && !foundToken ) {
+ // Handle multiple expressions
+ if ( !t.indexOf(",") ) {
+ // Clean the result set
+ if ( ret[0] == context ) ret.shift();
+
+ // Merge the result sets
+ jQuery.merge( done, ret );
+
+ // Reset the context
+ r = ret = [context];
+
+ // Touch up the selector string
+ t = " " + t.substr(1,t.length);
+
+ } else {
+ // Optomize for the case nodeName#idName
+ var re2 = /^([a-z0-9_-]+)(#)([a-z0-9\\*_-]*)/i;
+ var m = re2.exec(t);
+
+ // Re-organize the results, so that they're consistent
+ if ( m ) {
+ m = [ 0, m[2], m[3], m[1] ];
+
+ } else {
+ // Otherwise, do a traditional filter check for
+ // ID, class, and element selectors
+ re2 = /^([#.]?)([a-z0-9\\*_-]*)/i;
+ m = re2.exec(t);
+ }
+
+ // Try to do a global search by ID, where we can
+ if ( m[1] == "#" && ret[ret.length-1].getElementById ) {
+ // Optimization for HTML document case
+ var oid = ret[ret.length-1].getElementById(m[2]);
+
+ // Do a quick check for node name (where applicable) so
+ // that div#foo searches will be really fast
+ ret = r = oid &&
+ (!m[3] || oid.nodeName == m[3].toUpperCase()) ? [oid] : [];
+
+ } else {
+ // Pre-compile a regular expression to handle class searches
+ if ( m[1] == "." )
+ var rec = new RegExp("(^|\\s)" + m[2] + "(\\s|$)");
+
+ // We need to find all descendant elements, it is more
+ // efficient to use getAll() when we are already further down
+ // the tree - we try to recognize that here
+ jQuery.each( ret, function(){
+ // Grab the tag name being searched for
+ var tag = m[1] != "" || m[0] == "" ? "*" : m[2];
+
+ // Handle IE7 being really dumb about <object>s
+ if ( this.nodeName.toUpperCase() == "OBJECT" && tag == "*" )
+ tag = "param";
+
+ jQuery.merge( r,
+ m[1] != "" && ret.length != 1 ?
+ jQuery.getAll( this, [], m[1], m[2], rec ) :
+ this.getElementsByTagName( tag )
+ );
+ });
+
+ // It's faster to filter by class and be done with it
+ if ( m[1] == "." && ret.length == 1 )
+ r = jQuery.grep( r, function(e) {
+ return rec.test(e.className);
+ });
+
+ // Same with ID filtering
+ if ( m[1] == "#" && ret.length == 1 ) {
+ // Remember, then wipe out, the result set
+ var tmp = r;
+ r = [];
+
+ // Then try to find the element with the ID
+ jQuery.each( tmp, function(){
+ if ( this.getAttribute("id") == m[2] ) {
+ r = [ this ];
+ return false;
+ }
+ });
+ }
+
+ ret = r;
+ }
+
+ t = t.replace( re2, "" );
+ }
+
+ }
+
+ // If a selector string still exists
+ if ( t ) {
+ // Attempt to filter it
+ var val = jQuery.filter(t,r);
+ ret = r = val.r;
+ t = jQuery.trim(val.t);
+ }
+ }
+
+ // Remove the root context
+ if ( ret && ret[0] == context ) ret.shift();
+
+ // And combine the results
+ jQuery.merge( done, ret );
+
+ return done;
+ },
+
+ filter: function(t,r,not) {
+ // Look for common filter expressions
+ while ( t && /^[a-z[({<*:.#]/i.test(t) ) {
+
+ var p = jQuery.parse, m;
+
+ jQuery.each( p, function(i,re){
+
+ // Look for, and replace, string-like sequences
+ // and finally build a regexp out of it
+ m = re.exec( t );
+
+ if ( m ) {
+ // Remove what we just matched
+ t = t.substring( m[0].length );
+
+ // Re-organize the first match
+ if ( jQuery.expr[ m[1] ]._resort )
+ m = jQuery.expr[ m[1] ]._resort( m );
+
+ return false;
+ }
+ });
+
+ // :not() is a special case that can be optimized by
+ // keeping it out of the expression list
+ if ( m[1] == ":" && m[2] == "not" )
+ r = jQuery.filter(m[3], r, true).r;
+
+ // Handle classes as a special case (this will help to
+ // improve the speed, as the regexp will only be compiled once)
+ else if ( m[1] == "." ) {
+
+ var re = new RegExp("(^|\\s)" + m[2] + "(\\s|$)");
+ r = jQuery.grep( r, function(e){
+ return re.test(e.className || "");
+ }, not);
+
+ // Otherwise, find the expression to execute
+ } else {
+ var f = jQuery.expr[m[1]];
+ if ( typeof f != "string" )
+ f = jQuery.expr[m[1]][m[2]];
+
+ // Build a custom macro to enclose it
+ eval("f = function(a,i){" +
+ ( jQuery.expr[ m[1] ]._prefix || "" ) +
+ "return " + f + "}");
+
+ // Execute it against the current filter
+ r = jQuery.grep( r, f, not );
+ }
+ }
+
+ // Return an array of filtered elements (r)
+ // and the modified expression string (t)
+ return { r: r, t: t };
+ },
+
+ getAll: function( o, r, token, name, re ) {
+ for ( var s = o.firstChild; s; s = s.nextSibling )
+ if ( s.nodeType == 1 ) {
+ var add = true;
+
+ if ( token == "." )
+ add = s.className && re.test(s.className);
+ else if ( token == "#" )
+ add = s.getAttribute("id") == name;
+
+ if ( add )
+ r.push( s );
+
+ if ( token == "#" && r.length ) break;
+
+ if ( s.firstChild )
+ jQuery.getAll( s, r, token, name, re );
+ }
+
+ return r;
+ },
+ parents: function( elem ){
+ var matched = [];
+ var cur = elem.parentNode;
+ while ( cur && cur != document ) {
+ matched.push( cur );
+ cur = cur.parentNode;
+ }
+ return matched;
+ },
+ nth: function(cur,result,dir,elem){
+ result = result || 1;
+ var num = 0;
+ for ( ; cur; cur = cur[dir] ) {
+ if ( cur.nodeType == 1 ) num++;
+ if ( num == result || result == "even" && num % 2 == 0 && num > 1 && cur == elem ||
+ result == "odd" && num % 2 == 1 && cur == elem ) return cur;
+ }
+ },
+ sibling: function( n, elem ) {
+ var r = [];
+
+ for ( ; n; n = n.nextSibling ) {
+ if ( n.nodeType == 1 && (!elem || n != elem) )
+ r.push( n );
+ }
+
+ return r;
+ }
+});
+/*
+ * A number of helper functions used for managing events.
+ * Many of the ideas behind this code orignated from
+ * Dean Edwards' addEvent library.
+ */
+jQuery.event = {
+
+ // Bind an event to an element
+ // Original by Dean Edwards
+ add: function(element, type, handler, data) {
+ // For whatever reason, IE has trouble passing the window object
+ // around, causing it to be cloned in the process
+ if ( jQuery.browser.msie && element.setInterval != undefined )
+ element = window;
+
+ // if data is passed, bind to handler
+ if( data )
+ handler.data = data;
+
+ // Make sure that the function being executed has a unique ID
+ if ( !handler.guid )
+ handler.guid = this.guid++;
+
+ // Init the element's event structure
+ if (!element.events)
+ element.events = {};
+
+ // Get the current list of functions bound to this event
+ var handlers = element.events[type];
+
+ // If it hasn't been initialized yet
+ if (!handlers) {
+ // Init the event handler queue
+ handlers = element.events[type] = {};
+
+ // Remember an existing handler, if it's already there
+ if (element["on" + type])
+ handlers[0] = element["on" + type];
+ }
+
+ // Add the function to the element's handler list
+ handlers[handler.guid] = handler;
+
+ // And bind the global event handler to the element
+ element["on" + type] = this.handle;
+
+ // Remember the function in a global list (for triggering)
+ if (!this.global[type])
+ this.global[type] = [];
+ this.global[type].push( element );
+ },
+
+ guid: 1,
+ global: {},
+
+ // Detach an event or set of events from an element
+ remove: function(element, type, handler) {
+ if (element.events)
+ if ( type && type.type )
+ delete element.events[ type.type ][ type.handler.guid ];
+ else if (type && element.events[type])
+ if ( handler )
+ delete element.events[type][handler.guid];
+ else
+ for ( var i in element.events[type] )
+ delete element.events[type][i];
+ else
+ for ( var j in element.events )
+ this.remove( element, j );
+ },
+
+ trigger: function(type,data,element) {
+ // Clone the incoming data, if any
+ data = jQuery.makeArray(data || []);
+
+ // Handle a global trigger
+ if ( !element ) {
+ var g = this.global[type];
+ if ( g )
+ jQuery.each( g, function(){
+ jQuery.event.trigger( type, data, this );
+ });
+
+ // Handle triggering a single element
+ } else if ( element["on" + type] ) {
+ // Pass along a fake event
+ data.unshift( this.fix({ type: type, target: element }) );
+
+ // Trigger the event
+ var val = element["on" + type].apply( element, data );
+
+ if ( val !== false && jQuery.isFunction( element[ type ] ) )
+ element[ type ]();
+ }
+ },
+
+ handle: function(event) {
+ if ( typeof jQuery == "undefined" ) return false;
+
+ // Empty object is for triggered events with no data
+ event = jQuery.event.fix( event || window.event || {} );
+
+ // returned undefined or false
+ var returnValue;
+
+ var c = this.events[event.type];
+
+ var args = [].slice.call( arguments, 1 );
+ args.unshift( event );
+
+ for ( var j in c ) {
+ // Pass in a reference to the handler function itself
+ // So that we can later remove it
+ args[0].handler = c[j];
+ args[0].data = c[j].data;
+
+ if ( c[j].apply( this, args ) === false ) {
+ event.preventDefault();
+ event.stopPropagation();
+ returnValue = false;
+ }
+ }
+
+ // Clean up added properties in IE to prevent memory leak
+ if (jQuery.browser.msie) event.target = event.preventDefault = event.stopPropagation = event.handler = event.data = null;
+
+ return returnValue;
+ },
+
+ fix: function(event) {
+ // Fix target property, if necessary
+ if ( !event.target && event.srcElement )
+ event.target = event.srcElement;
+
+ // Calculate pageX/Y if missing and clientX/Y available
+ if ( event.pageX == undefined && event.clientX != undefined ) {
+ var e = document.documentElement, b = document.body;
+ event.pageX = event.clientX + (e.scrollLeft || b.scrollLeft);
+ event.pageY = event.clientY + (e.scrollTop || b.scrollTop);
+ }
+
+ // check if target is a textnode (safari)
+ if (jQuery.browser.safari && event.target.nodeType == 3) {
+ // store a copy of the original event object
+ // and clone because target is read only
+ var originalEvent = event;
+ event = jQuery.extend({}, originalEvent);
+
+ // get parentnode from textnode
+ event.target = originalEvent.target.parentNode;
+
+ // add preventDefault and stopPropagation since
+ // they will not work on the clone
+ event.preventDefault = function() {
+ return originalEvent.preventDefault();
+ };
+ event.stopPropagation = function() {
+ return originalEvent.stopPropagation();
+ };
+ }
+
+ // fix preventDefault and stopPropagation
+ if (!event.preventDefault)
+ event.preventDefault = function() {
+ this.returnValue = false;
+ };
+
+ if (!event.stopPropagation)
+ event.stopPropagation = function() {
+ this.cancelBubble = true;
+ };
+
+ return event;
+ }
+};
+
+jQuery.fn.extend({
+ bind: function( type, data, fn ) {
+ return this.each(function(){
+ jQuery.event.add( this, type, fn || data, data );
+ });
+ },
+ one: function( type, data, fn ) {
+ return this.each(function(){
+ jQuery.event.add( this, type, function(event) {
+ jQuery(this).unbind(event);
+ return (fn || data).apply( this, arguments);
+ }, data);
+ });
+ },
+ unbind: function( type, fn ) {
+ return this.each(function(){
+ jQuery.event.remove( this, type, fn );
+ });
+ },
+ trigger: function( type, data ) {
+ return this.each(function(){
+ jQuery.event.trigger( type, data, this );
+ });
+ },
+ toggle: function() {
+ // Save reference to arguments for access in closure
+ var a = arguments;
+
+ return this.click(function(e) {
+ // Figure out which function to execute
+ this.lastToggle = this.lastToggle == 0 ? 1 : 0;
+
+ // Make sure that clicks stop
+ e.preventDefault();
+
+ // and execute the function
+ return a[this.lastToggle].apply( this, [e] ) || false;
+ });
+ },
+ hover: function(f,g) {
+
+ // A private function for handling mouse 'hovering'
+ function handleHover(e) {
+ // Check if mouse(over|out) are still within the same parent element
+ var p = (e.type == "mouseover" ? e.fromElement : e.toElement) || e.relatedTarget;
+
+ // Traverse up the tree
+ while ( p && p != this ) try { p = p.parentNode } catch(e) { p = this; };
+
+ // If we actually just moused on to a sub-element, ignore it
+ if ( p == this ) return false;
+
+ // Execute the right function
+ return (e.type == "mouseover" ? f : g).apply(this, [e]);
+ }
+
+ // Bind the function to the two event listeners
+ return this.mouseover(handleHover).mouseout(handleHover);
+ },
+ ready: function(f) {
+ // If the DOM is already ready
+ if ( jQuery.isReady )
+ // Execute the function immediately
+ f.apply( document, [jQuery] );
+
+ // Otherwise, remember the function for later
+ else {
+ // Add the function to the wait list
+ jQuery.readyList.push( function() { return f.apply(this, [jQuery]) } );
+ }
+
+ return this;
+ }
+});
+
+jQuery.extend({
+ /*
+ * All the code that makes DOM Ready work nicely.
+ */
+ isReady: false,
+ readyList: [],
+
+ // Handle when the DOM is ready
+ ready: function() {
+ // Make sure that the DOM is not already loaded
+ if ( !jQuery.isReady ) {
+ // Remember that the DOM is ready
+ jQuery.isReady = true;
+
+ // If there are functions bound, to execute
+ if ( jQuery.readyList ) {
+ // Execute all of them
+ jQuery.each( jQuery.readyList, function(){
+ this.apply( document );
+ });
+
+ // Reset the list of functions
+ jQuery.readyList = null;
+ }
+ // Remove event lisenter to avoid memory leak
+ if ( jQuery.browser.mozilla || jQuery.browser.opera )
+ document.removeEventListener( "DOMContentLoaded", jQuery.ready, false );
+ }
+ }
+});
+
+new function(){
+
+ jQuery.each( ("blur,focus,load,resize,scroll,unload,click,dblclick," +
+ "mousedown,mouseup,mousemove,mouseover,mouseout,change,select," +
+ "submit,keydown,keypress,keyup,error").split(","), function(i,o){
+
+ // Handle event binding
+ jQuery.fn[o] = function(f){
+ return f ? this.bind(o, f) : this.trigger(o);
+ };
+
+ });
+
+ // If Mozilla is used
+ if ( jQuery.browser.mozilla || jQuery.browser.opera )
+ // Use the handy event callback
+ document.addEventListener( "DOMContentLoaded", jQuery.ready, false );
+
+ // If IE is used, use the excellent hack by Matthias Miller
+ // http://www.outofhanwell.com/blog/index.php?title=the_window_onload_problem_revisited
+ else if ( jQuery.browser.msie ) {
+
+ // Only works if you document.write() it
+ document.write("<scr" + "ipt id=__ie_init defer=true " +
+ "src=//:><\/script>");
+
+ // Use the defer script hack
+ var script = document.getElementById("__ie_init");
+
+ // script does not exist if jQuery is loaded dynamically
+ if ( script )
+ script.onreadystatechange = function() {
+ if ( this.readyState != "complete" ) return;
+ this.parentNode.removeChild( this );
+ jQuery.ready();
+ };
+
+ // Clear from memory
+ script = null;
+
+ // If Safari is used
+ } else if ( jQuery.browser.safari )
+ // Continually check to see if the document.readyState is valid
+ jQuery.safariTimer = setInterval(function(){
+ // loaded and complete are both valid states
+ if ( document.readyState == "loaded" ||
+ document.readyState == "complete" ) {
+
+ // If either one are found, remove the timer
+ clearInterval( jQuery.safariTimer );
+ jQuery.safariTimer = null;
+
+ // and execute any waiting functions
+ jQuery.ready();
+ }
+ }, 10);
+
+ // A fallback to window.onload, that will always work
+ jQuery.event.add( window, "load", jQuery.ready );
+
+};
+
+// Clean up after IE to avoid memory leaks
+if (jQuery.browser.msie)
+ jQuery(window).one("unload", function() {
+ var global = jQuery.event.global;
+ for ( var type in global ) {
+ var els = global[type], i = els.length;
+ if ( i && type != 'unload' )
+ do
+ jQuery.event.remove(els[i-1], type);
+ while (--i);
+ }
+ });
+jQuery.fn.extend({
+
+ show: function(speed,callback){
+ var hidden = this.filter(":hidden");
+ return speed ?
+ hidden.animate({
+ height: "show", width: "show", opacity: "show"
+ }, speed, callback) :
+
+ hidden.each(function(){
+ this.style.display = this.oldblock ? this.oldblock : "";
+ if ( jQuery.css(this,"display") == "none" )
+ this.style.display = "block";
+ });
+ },
+
+ hide: function(speed,callback){
+ var visible = this.filter(":visible");
+ return speed ?
+ visible.animate({
+ height: "hide", width: "hide", opacity: "hide"
+ }, speed, callback) :
+
+ visible.each(function(){
+ this.oldblock = this.oldblock || jQuery.css(this,"display");
+ if ( this.oldblock == "none" )
+ this.oldblock = "block";
+ this.style.display = "none";
+ });
+ },
+
+ // Save the old toggle function
+ _toggle: jQuery.fn.toggle,
+ toggle: function( fn, fn2 ){
+ var args = arguments;
+ return jQuery.isFunction(fn) && jQuery.isFunction(fn2) ?
+ this._toggle( fn, fn2 ) :
+ this.each(function(){
+ jQuery(this)[ jQuery(this).is(":hidden") ? "show" : "hide" ]
+ .apply( jQuery(this), args );
+ });
+ },
+ slideDown: function(speed,callback){
+ return this.animate({height: "show"}, speed, callback);
+ },
+ slideUp: function(speed,callback){
+ return this.animate({height: "hide"}, speed, callback);
+ },
+ slideToggle: function(speed, callback){
+ return this.each(function(){
+ var state = jQuery(this).is(":hidden") ? "show" : "hide";
+ jQuery(this).animate({height: state}, speed, callback);
+ });
+ },
+ fadeIn: function(speed, callback){
+ return this.animate({opacity: "show"}, speed, callback);
+ },
+ fadeOut: function(speed, callback){
+ return this.animate({opacity: "hide"}, speed, callback);
+ },
+ fadeTo: function(speed,to,callback){
+ return this.animate({opacity: to}, speed, callback);
+ },
+ animate: function( prop, speed, easing, callback ) {
+ return this.queue(function(){
+
+ this.curAnim = jQuery.extend({}, prop);
+ var opt = jQuery.speed(speed, easing, callback);
+
+ for ( var p in prop ) {
+ var e = new jQuery.fx( this, opt, p );
+ if ( prop[p].constructor == Number )
+ e.custom( e.cur(), prop[p] );
+ else
+ e[ prop[p] ]( prop );
+ }
+
+ });
+ },
+ queue: function(type,fn){
+ if ( !fn ) {
+ fn = type;
+ type = "fx";
+ }
+
+ return this.each(function(){
+ if ( !this.queue )
+ this.queue = {};
+
+ if ( !this.queue[type] )
+ this.queue[type] = [];
+
+ this.queue[type].push( fn );
+
+ if ( this.queue[type].length == 1 )
+ fn.apply(this);
+ });
+ }
+
+});
+
+jQuery.extend({
+
+ speed: function(speed, easing, fn) {
+ var opt = speed && speed.constructor == Object ? speed : {
+ complete: fn || !fn && easing ||
+ jQuery.isFunction( speed ) && speed,
+ duration: speed,
+ easing: fn && easing || easing && easing.constructor != Function && easing
+ };
+
+ opt.duration = (opt.duration && opt.duration.constructor == Number ?
+ opt.duration :
+ { slow: 600, fast: 200 }[opt.duration]) || 400;
+
+ // Queueing
+ opt.old = opt.complete;
+ opt.complete = function(){
+ jQuery.dequeue(this, "fx");
+ if ( jQuery.isFunction( opt.old ) )
+ opt.old.apply( this );
+ };
+
+ return opt;
+ },
+
+ easing: {},
+
+ queue: {},
+
+ dequeue: function(elem,type){
+ type = type || "fx";
+
+ if ( elem.queue && elem.queue[type] ) {
+ // Remove self
+ elem.queue[type].shift();
+
+ // Get next function
+ var f = elem.queue[type][0];
+
+ if ( f ) f.apply( elem );
+ }
+ },
+
+ /*
+ * I originally wrote fx() as a clone of moo.fx and in the process
+ * of making it small in size the code became illegible to sane
+ * people. You've been warned.
+ */
+
+ fx: function( elem, options, prop ){
+
+ var z = this;
+
+ // The styles
+ var y = elem.style;
+
+ // Store display property
+ var oldDisplay = jQuery.css(elem, "display");
+
+ // Set display property to block for animation
+ y.display = "block";
+
+ // Make sure that nothing sneaks out
+ y.overflow = "hidden";
+
+ // Simple function for setting a style value
+ z.a = function(){
+ if ( options.step )
+ options.step.apply( elem, [ z.now ] );
+
+ if ( prop == "opacity" )
+ jQuery.attr(y, "opacity", z.now); // Let attr handle opacity
+ else if ( parseInt(z.now) ) // My hate for IE will never die
+ y[prop] = parseInt(z.now) + "px";
+ };
+
+ // Figure out the maximum number to run to
+ z.max = function(){
+ return parseFloat( jQuery.css(elem,prop) );
+ };
+
+ // Get the current size
+ z.cur = function(){
+ var r = parseFloat( jQuery.curCSS(elem, prop) );
+ return r && r > -10000 ? r : z.max();
+ };
+
+ // Start an animation from one number to another
+ z.custom = function(from,to){
+ z.startTime = (new Date()).getTime();
+ z.now = from;
+ z.a();
+
+ z.timer = setInterval(function(){
+ z.step(from, to);
+ }, 13);
+ };
+
+ // Simple 'show' function
+ z.show = function(){
+ if ( !elem.orig ) elem.orig = {};
+
+ // Remember where we started, so that we can go back to it later
+ elem.orig[prop] = this.cur();
+
+ options.show = true;
+
+ // Begin the animation
+ z.custom(0, elem.orig[prop]);
+
+ // Stupid IE, look what you made me do
+ if ( prop != "opacity" )
+ y[prop] = "1px";
+ };
+
+ // Simple 'hide' function
+ z.hide = function(){
+ if ( !elem.orig ) elem.orig = {};
+
+ // Remember where we started, so that we can go back to it later
+ elem.orig[prop] = this.cur();
+
+ options.hide = true;
+
+ // Begin the animation
+ z.custom(elem.orig[prop], 0);
+ };
+
+ //Simple 'toggle' function
+ z.toggle = function() {
+ if ( !elem.orig ) elem.orig = {};
+
+ // Remember where we started, so that we can go back to it later
+ elem.orig[prop] = this.cur();
+
+ if(oldDisplay == "none") {
+ options.show = true;
+
+ // Stupid IE, look what you made me do
+ if ( prop != "opacity" )
+ y[prop] = "1px";
+
+ // Begin the animation
+ z.custom(0, elem.orig[prop]);
+ } else {
+ options.hide = true;
+
+ // Begin the animation
+ z.custom(elem.orig[prop], 0);
+ }
+ };
+
+ // Each step of an animation
+ z.step = function(firstNum, lastNum){
+ var t = (new Date()).getTime();
+
+ if (t > options.duration + z.startTime) {
+ // Stop the timer
+ clearInterval(z.timer);
+ z.timer = null;
+
+ z.now = lastNum;
+ z.a();
+
+ if (elem.curAnim) elem.curAnim[ prop ] = true;
+
+ var done = true;
+ for ( var i in elem.curAnim )
+ if ( elem.curAnim[i] !== true )
+ done = false;
+
+ if ( done ) {
+ // Reset the overflow
+ y.overflow = "";
+
+ // Reset the display
+ y.display = oldDisplay;
+ if (jQuery.css(elem, "display") == "none")
+ y.display = "block";
+
+ // Hide the element if the "hide" operation was done
+ if ( options.hide )
+ y.display = "none";
+
+ // Reset the properties, if the item has been hidden or shown
+ if ( options.hide || options.show )
+ for ( var p in elem.curAnim )
+ if (p == "opacity")
+ jQuery.attr(y, p, elem.orig[p]);
+ else
+ y[p] = "";
+ }
+
+ // If a callback was provided, execute it
+ if ( done && jQuery.isFunction( options.complete ) )
+ // Execute the complete function
+ options.complete.apply( elem );
+ } else {
+ var n = t - this.startTime;
+ // Figure out where in the animation we are and set the number
+ var p = n / options.duration;
+
+ // If the easing function exists, then use it
+ z.now = options.easing && jQuery.easing[options.easing] ?
+ jQuery.easing[options.easing](p, n, firstNum, (lastNum-firstNum), options.duration) :
+ // else use default linear easing
+ ((-Math.cos(p*Math.PI)/2) + 0.5) * (lastNum-firstNum) + firstNum;
+
+ // Perform the next step of the animation
+ z.a();
+ }
+ };
+
+ }
+});
+jQuery.fn.extend({
+ loadIfModified: function( url, params, callback ) {
+ this.load( url, params, callback, 1 );
+ },
+ load: function( url, params, callback, ifModified ) {
+ if ( jQuery.isFunction( url ) )
+ return this.bind("load", url);
+
+ callback = callback || function(){};
+
+ // Default to a GET request
+ var type = "GET";
+
+ // If the second parameter was provided
+ if ( params )
+ // If it's a function
+ if ( jQuery.isFunction( params.constructor ) ) {
+ // We assume that it's the callback
+ callback = params;
+ params = null;
+
+ // Otherwise, build a param string
+ } else {
+ params = jQuery.param( params );
+ type = "POST";
+ }
+
+ var self = this;
+
+ // Request the remote document
+ jQuery.ajax({
+ url: url,
+ type: type,
+ data: params,
+ ifModified: ifModified,
+ complete: function(res, status){
+ if ( status == "success" || !ifModified && status == "notmodified" )
+ // Inject the HTML into all the matched elements
+ self.attr("innerHTML", res.responseText)
+ // Execute all the scripts inside of the newly-injected HTML
+ .evalScripts()
+ // Execute callback
+ .each( callback, [res.responseText, status, res] );
+ else
+ callback.apply( self, [res.responseText, status, res] );
+ }
+ });
+ return this;
+ },
+ serialize: function() {
+ return jQuery.param( this );
+ },
+ evalScripts: function() {
+ return this.find("script").each(function(){
+ if ( this.src )
+ jQuery.getScript( this.src );
+ else
+ jQuery.globalEval( this.text || this.textContent || this.innerHTML || "" );
+ }).end();
+ }
+
+});
+
+// If IE is used, create a wrapper for the XMLHttpRequest object
+if ( jQuery.browser.msie && typeof XMLHttpRequest == "undefined" )
+ XMLHttpRequest = function(){
+ return new ActiveXObject("Microsoft.XMLHTTP");
+ };
+
+// Attach a bunch of functions for handling common AJAX events
+
+jQuery.each( "ajaxStart,ajaxStop,ajaxComplete,ajaxError,ajaxSuccess,ajaxSend".split(","), function(i,o){
+ jQuery.fn[o] = function(f){
+ return this.bind(o, f);
+ };
+});
+
+jQuery.extend({
+ get: function( url, data, callback, type, ifModified ) {
+ // shift arguments if data argument was ommited
+ if ( jQuery.isFunction( data ) ) {
+ callback = data;
+ data = null;
+ }
+
+ return jQuery.ajax({
+ url: url,
+ data: data,
+ success: callback,
+ dataType: type,
+ ifModified: ifModified
+ });
+ },
+ getIfModified: function( url, data, callback, type ) {
+ return jQuery.get(url, data, callback, type, 1);
+ },
+ getScript: function( url, callback ) {
+ return jQuery.get(url, null, callback, "script");
+ },
+ getJSON: function( url, data, callback ) {
+ return jQuery.get(url, data, callback, "json");
+ },
+ post: function( url, data, callback, type ) {
+ return jQuery.ajax({
+ type: "POST",
+ url: url,
+ data: data,
+ success: callback,
+ dataType: type
+ });
+ },
+
+ // timeout (ms)
+ //timeout: 0,
+ ajaxTimeout: function( timeout ) {
+ jQuery.ajaxSettings.timeout = timeout;
+ },
+ ajaxSetup: function( settings ) {
+ jQuery.extend( jQuery.ajaxSettings, settings );
+ },
+
+ ajaxSettings: {
+ global: true,
+ type: "GET",
+ timeout: 0,
+ contentType: "application/x-www-form-urlencoded",
+ processData: true,
+ async: true,
+ data: null
+ },
+
+ // Last-Modified header cache for next request
+ lastModified: {},
+ ajax: function( s ) {
+ // TODO introduce global settings, allowing the client to modify them for all requests, not only timeout
+ s = jQuery.extend({}, jQuery.ajaxSettings, s);
+
+ // if data available
+ if ( s.data ) {
+ // convert data if not already a string
+ if (s.processData && typeof s.data != "string")
+ s.data = jQuery.param(s.data);
+ // append data to url for get requests
+ if( s.type.toLowerCase() == "get" )
+ // "?" + data or "&" + data (in case there are already params)
+ s.url += ((s.url.indexOf("?") > -1) ? "&" : "?") + s.data;
+ }
+
+ // Watch for a new set of requests
+ if ( s.global && ! jQuery.active++ )
+ jQuery.event.trigger( "ajaxStart" );
+
+ var requestDone = false;
+
+ // Create the request object
+ var xml = new XMLHttpRequest();
+
+ // Open the socket
+ xml.open(s.type, s.url, s.async);
+
+ // Set the correct header, if data is being sent
+ if ( s.data )
+ xml.setRequestHeader("Content-Type", s.contentType);
+
+ // Set the If-Modified-Since header, if ifModified mode.
+ if ( s.ifModified )
+ xml.setRequestHeader("If-Modified-Since",
+ jQuery.lastModified[s.url] || "Thu, 01 Jan 1970 00:00:00 GMT" );
+
+ // Set header so the called script knows that it's an XMLHttpRequest
+ xml.setRequestHeader("X-Requested-With", "XMLHttpRequest");
+
+ // Make sure the browser sends the right content length
+ if ( xml.overrideMimeType )
+ xml.setRequestHeader("Connection", "close");
+
+ // Allow custom headers/mimetypes
+ if( s.beforeSend )
+ s.beforeSend(xml);
+
+ if ( s.global )
+ jQuery.event.trigger("ajaxSend", [xml, s]);
+
+ // Wait for a response to come back
+ var onreadystatechange = function(isTimeout){
+ // The transfer is complete and the data is available, or the request timed out
+ if ( xml && (xml.readyState == 4 || isTimeout == "timeout") ) {
+ requestDone = true;
+ var status;
+ try {
+ status = jQuery.httpSuccess( xml ) && isTimeout != "timeout" ?
+ s.ifModified && jQuery.httpNotModified( xml, s.url ) ? "notmodified" : "success" : "error";
+ // Make sure that the request was successful or notmodified
+ if ( status != "error" ) {
+ // Cache Last-Modified header, if ifModified mode.
+ var modRes;
+ try {
+ modRes = xml.getResponseHeader("Last-Modified");
+ } catch(e) {} // swallow exception thrown by FF if header is not available
+
+ if ( s.ifModified && modRes )
+ jQuery.lastModified[s.url] = modRes;
+
+ // process the data (runs the xml through httpData regardless of callback)
+ var data = jQuery.httpData( xml, s.dataType );
+
+ // If a local callback was specified, fire it and pass it the data
+ if ( s.success )
+ s.success( data, status );
+
+ // Fire the global callback
+ if( s.global )
+ jQuery.event.trigger( "ajaxSuccess", [xml, s] );
+ } else
+ jQuery.handleError(s, xml, status);
+ } catch(e) {
+ status = "error";
+ jQuery.handleError(s, xml, status, e);
+ }
+
+ // The request was completed
+ if( s.global )
+ jQuery.event.trigger( "ajaxComplete", [xml, s] );
+
+ // Handle the global AJAX counter
+ if ( s.global && ! --jQuery.active )
+ jQuery.event.trigger( "ajaxStop" );
+
+ // Process result
+ if ( s.complete )
+ s.complete(xml, status);
+
+ // Stop memory leaks
+ xml.onreadystatechange = function(){};
+ xml = null;
+ }
+ };
+ xml.onreadystatechange = onreadystatechange;
+
+ // Timeout checker
+ if ( s.timeout > 0 )
+ setTimeout(function(){
+ // Check to see if the request is still happening
+ if ( xml ) {
+ // Cancel the request
+ xml.abort();
+
+ if( !requestDone )
+ onreadystatechange( "timeout" );
+ }
+ }, s.timeout);
+
+ // save non-leaking reference
+ var xml2 = xml;
+
+ // Send the data
+ try {
+ xml2.send(s.data);
+ } catch(e) {
+ jQuery.handleError(s, xml, null, e);
+ }
+
+ // firefox 1.5 doesn't fire statechange for sync requests
+ if ( !s.async )
+ onreadystatechange();
+
+ // return XMLHttpRequest to allow aborting the request etc.
+ return xml2;
+ },
+
+ handleError: function( s, xml, status, e ) {
+ // If a local callback was specified, fire it
+ if ( s.error ) s.error( xml, status, e );
+
+ // Fire the global callback
+ if ( s.global )
+ jQuery.event.trigger( "ajaxError", [xml, s, e] );
+ },
+
+ // Counter for holding the number of active queries
+ active: 0,
+
+ // Determines if an XMLHttpRequest was successful or not
+ httpSuccess: function( r ) {
+ try {
+ return !r.status && location.protocol == "file:" ||
+ ( r.status >= 200 && r.status < 300 ) || r.status == 304 ||
+ jQuery.browser.safari && r.status == undefined;
+ } catch(e){}
+ return false;
+ },
+
+ // Determines if an XMLHttpRequest returns NotModified
+ httpNotModified: function( xml, url ) {
+ try {
+ var xmlRes = xml.getResponseHeader("Last-Modified");
+
+ // Firefox always returns 200. check Last-Modified date
+ return xml.status == 304 || xmlRes == jQuery.lastModified[url] ||
+ jQuery.browser.safari && xml.status == undefined;
+ } catch(e){}
+ return false;
+ },
+
+ /* Get the data out of an XMLHttpRequest.
+ * Return parsed XML if content-type header is "xml" and type is "xml" or omitted,
+ * otherwise return plain text.
+ * (String) data - The type of data that you're expecting back,
+ * (e.g. "xml", "html", "script")
+ */
+ httpData: function( r, type ) {
+ var ct = r.getResponseHeader("content-type");
+ var data = !type && ct && ct.indexOf("xml") >= 0;
+ data = type == "xml" || data ? r.responseXML : r.responseText;
+
+ // If the type is "script", eval it in global context
+ if ( type == "script" )
+ jQuery.globalEval( data );
+
+ // Get the JavaScript object, if JSON is used.
+ if ( type == "json" )
+ eval( "data = " + data );
+
+ // evaluate scripts within html
+ if ( type == "html" )
+ jQuery("<div>").html(data).evalScripts();
+
+ return data;
+ },
+
+ // Serialize an array of form elements or a set of
+ // key/values into a query string
+ param: function( a ) {
+ var s = [];
+
+ // If an array was passed in, assume that it is an array
+ // of form elements
+ if ( a.constructor == Array || a.jquery )
+ // Serialize the form elements
+ jQuery.each( a, function(){
+ s.push( encodeURIComponent(this.name) + "=" + encodeURIComponent( this.value ) );
+ });
+
+ // Otherwise, assume that it's an object of key/value pairs
+ else
+ // Serialize the key/values
+ for ( var j in a )
+ // If the value is an array then the key names need to be repeated
+ if ( a[j].constructor == Array )
+ jQuery.each( a[j], function(){
+ s.push( encodeURIComponent(j) + "=" + encodeURIComponent( this ) );
+ });
+ else
+ s.push( encodeURIComponent(j) + "=" + encodeURIComponent( a[j] ) );
+
+ // Return the resulting serialization
+ return s.join("&");
+ },
+
+ // evalulates a script in global context
+ // not reliable for safari
+ globalEval: function( data ) {
+ if ( window.execScript )
+ window.execScript( data );
+ else if ( jQuery.browser.safari )
+ // safari doesn't provide a synchronous global eval
+ window.setTimeout( data, 0 );
+ else
+ eval.call( window, data );
+ }
+
+});
+}
diff --git a/devtools/client/inspector/markup/test/lib_jquery_1.11.1_min.js b/devtools/client/inspector/markup/test/lib_jquery_1.11.1_min.js
new file mode 100644
index 000000000..ab28a2472
--- /dev/null
+++ b/devtools/client/inspector/markup/test/lib_jquery_1.11.1_min.js
@@ -0,0 +1,4 @@
+/*! jQuery v1.11.1 | (c) 2005, 2014 jQuery Foundation, Inc. | jquery.org/license */
+!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=c.slice,e=c.concat,f=c.push,g=c.indexOf,h={},i=h.toString,j=h.hasOwnProperty,k={},l="1.11.1",m=function(a,b){return new m.fn.init(a,b)},n=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,o=/^-ms-/,p=/-([\da-z])/gi,q=function(a,b){return b.toUpperCase()};m.fn=m.prototype={jquery:l,constructor:m,selector:"",length:0,toArray:function(){return d.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:d.call(this)},pushStack:function(a){var b=m.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return m.each(this,a,b)},map:function(a){return this.pushStack(m.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:c.sort,splice:c.splice},m.extend=m.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||m.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(e=arguments[h]))for(d in e)a=g[d],c=e[d],g!==c&&(j&&c&&(m.isPlainObject(c)||(b=m.isArray(c)))?(b?(b=!1,f=a&&m.isArray(a)?a:[]):f=a&&m.isPlainObject(a)?a:{},g[d]=m.extend(j,f,c)):void 0!==c&&(g[d]=c));return g},m.extend({expando:"jQuery"+(l+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===m.type(a)},isArray:Array.isArray||function(a){return"array"===m.type(a)},isWindow:function(a){return null!=a&&a==a.window},isNumeric:function(a){return!m.isArray(a)&&a-parseFloat(a)>=0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},isPlainObject:function(a){var b;if(!a||"object"!==m.type(a)||a.nodeType||m.isWindow(a))return!1;try{if(a.constructor&&!j.call(a,"constructor")&&!j.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}if(k.ownLast)for(b in a)return j.call(a,b);for(b in a);return void 0===b||j.call(a,b)},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(b){b&&m.trim(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(o,"ms-").replace(p,q)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b,c){var d,e=0,f=a.length,g=r(a);if(c){if(g){for(;f>e;e++)if(d=b.apply(a[e],c),d===!1)break}else for(e in a)if(d=b.apply(a[e],c),d===!1)break}else if(g){for(;f>e;e++)if(d=b.call(a[e],e,a[e]),d===!1)break}else for(e in a)if(d=b.call(a[e],e,a[e]),d===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(n,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(r(Object(a))?m.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){var d;if(b){if(g)return g.call(b,a,c);for(d=b.length,c=c?0>c?Math.max(0,d+c):c:0;d>c;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,b){var c=+b.length,d=0,e=a.length;while(c>d)a[e++]=b[d++];if(c!==c)while(void 0!==b[d])a[e++]=b[d++];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,f=0,g=a.length,h=r(a),i=[];if(h)for(;g>f;f++)d=b(a[f],f,c),null!=d&&i.push(d);else for(f in a)d=b(a[f],f,c),null!=d&&i.push(d);return e.apply([],i)},guid:1,proxy:function(a,b){var c,e,f;return"string"==typeof b&&(f=a[b],b=a,a=f),m.isFunction(a)?(c=d.call(arguments,2),e=function(){return a.apply(b||this,c.concat(d.call(arguments)))},e.guid=a.guid=a.guid||m.guid++,e):void 0},now:function(){return+new Date},support:k}),m.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function r(a){var b=a.length,c=m.type(a);return"function"===c||m.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var s=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+-new Date,v=a.document,w=0,x=0,y=gb(),z=gb(),A=gb(),B=function(a,b){return a===b&&(l=!0),0},C="undefined",D=1<<31,E={}.hasOwnProperty,F=[],G=F.pop,H=F.push,I=F.push,J=F.slice,K=F.indexOf||function(a){for(var b=0,c=this.length;c>b;b++)if(this[b]===a)return b;return-1},L="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",M="[\\x20\\t\\r\\n\\f]",N="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",O=N.replace("w","w#"),P="\\["+M+"*("+N+")(?:"+M+"*([*^$|!~]?=)"+M+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+O+"))|)"+M+"*\\]",Q=":("+N+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+P+")*)|.*)\\)|)",R=new RegExp("^"+M+"+|((?:^|[^\\\\])(?:\\\\.)*)"+M+"+$","g"),S=new RegExp("^"+M+"*,"+M+"*"),T=new RegExp("^"+M+"*([>+~]|"+M+")"+M+"*"),U=new RegExp("="+M+"*([^\\]'\"]*?)"+M+"*\\]","g"),V=new RegExp(Q),W=new RegExp("^"+O+"$"),X={ID:new RegExp("^#("+N+")"),CLASS:new RegExp("^\\.("+N+")"),TAG:new RegExp("^("+N.replace("w","w*")+")"),ATTR:new RegExp("^"+P),PSEUDO:new RegExp("^"+Q),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+L+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/^(?:input|select|textarea|button)$/i,Z=/^h\d$/i,$=/^[^{]+\{\s*\[native \w/,_=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ab=/[+~]/,bb=/'|\\/g,cb=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),db=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)};try{I.apply(F=J.call(v.childNodes),v.childNodes),F[v.childNodes.length].nodeType}catch(eb){I={apply:F.length?function(a,b){H.apply(a,J.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function fb(a,b,d,e){var f,h,j,k,l,o,r,s,w,x;if((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,d=d||[],!a||"string"!=typeof a)return d;if(1!==(k=b.nodeType)&&9!==k)return[];if(p&&!e){if(f=_.exec(a))if(j=f[1]){if(9===k){if(h=b.getElementById(j),!h||!h.parentNode)return d;if(h.id===j)return d.push(h),d}else if(b.ownerDocument&&(h=b.ownerDocument.getElementById(j))&&t(b,h)&&h.id===j)return d.push(h),d}else{if(f[2])return I.apply(d,b.getElementsByTagName(a)),d;if((j=f[3])&&c.getElementsByClassName&&b.getElementsByClassName)return I.apply(d,b.getElementsByClassName(j)),d}if(c.qsa&&(!q||!q.test(a))){if(s=r=u,w=b,x=9===k&&a,1===k&&"object"!==b.nodeName.toLowerCase()){o=g(a),(r=b.getAttribute("id"))?s=r.replace(bb,"\\$&"):b.setAttribute("id",s),s="[id='"+s+"'] ",l=o.length;while(l--)o[l]=s+qb(o[l]);w=ab.test(a)&&ob(b.parentNode)||b,x=o.join(",")}if(x)try{return I.apply(d,w.querySelectorAll(x)),d}catch(y){}finally{r||b.removeAttribute("id")}}}return i(a.replace(R,"$1"),b,d,e)}function gb(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function hb(a){return a[u]=!0,a}function ib(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function jb(a,b){var c=a.split("|"),e=a.length;while(e--)d.attrHandle[c[e]]=b}function kb(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||D)-(~a.sourceIndex||D);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function lb(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function mb(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function nb(a){return hb(function(b){return b=+b,hb(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function ob(a){return a&&typeof a.getElementsByTagName!==C&&a}c=fb.support={},f=fb.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=fb.setDocument=function(a){var b,e=a?a.ownerDocument||a:v,g=e.defaultView;return e!==n&&9===e.nodeType&&e.documentElement?(n=e,o=e.documentElement,p=!f(e),g&&g!==g.top&&(g.addEventListener?g.addEventListener("unload",function(){m()},!1):g.attachEvent&&g.attachEvent("onunload",function(){m()})),c.attributes=ib(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ib(function(a){return a.appendChild(e.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=$.test(e.getElementsByClassName)&&ib(function(a){return a.innerHTML="<div class='a'></div><div class='a i'></div>",a.firstChild.className="i",2===a.getElementsByClassName("i").length}),c.getById=ib(function(a){return o.appendChild(a).id=u,!e.getElementsByName||!e.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if(typeof b.getElementById!==C&&p){var c=b.getElementById(a);return c&&c.parentNode?[c]:[]}},d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){var c=typeof a.getAttributeNode!==C&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return typeof b.getElementsByTagName!==C?b.getElementsByTagName(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return typeof b.getElementsByClassName!==C&&p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=$.test(e.querySelectorAll))&&(ib(function(a){a.innerHTML="<select msallowclip=''><option selected=''></option></select>",a.querySelectorAll("[msallowclip^='']").length&&q.push("[*^$]="+M+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+M+"*(?:value|"+L+")"),a.querySelectorAll(":checked").length||q.push(":checked")}),ib(function(a){var b=e.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+M+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=$.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ib(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",Q)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=$.test(o.compareDocumentPosition),t=b||$.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===e||a.ownerDocument===v&&t(v,a)?-1:b===e||b.ownerDocument===v&&t(v,b)?1:k?K.call(k,a)-K.call(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,f=a.parentNode,g=b.parentNode,h=[a],i=[b];if(!f||!g)return a===e?-1:b===e?1:f?-1:g?1:k?K.call(k,a)-K.call(k,b):0;if(f===g)return kb(a,b);c=a;while(c=c.parentNode)h.unshift(c);c=b;while(c=c.parentNode)i.unshift(c);while(h[d]===i[d])d++;return d?kb(h[d],i[d]):h[d]===v?-1:i[d]===v?1:0},e):n},fb.matches=function(a,b){return fb(a,null,null,b)},fb.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(U,"='$1']"),!(!c.matchesSelector||!p||r&&r.test(b)||q&&q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return fb(b,n,null,[a]).length>0},fb.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},fb.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&E.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},fb.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},fb.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=fb.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=fb.selectors={cacheLength:50,createPseudo:hb,match:X,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(cb,db),a[3]=(a[3]||a[4]||a[5]||"").replace(cb,db),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||fb.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&fb.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return X.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&V.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(cb,db).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+M+")"+a+"("+M+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||typeof a.getAttribute!==C&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=fb.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h;if(q){if(f){while(p){l=b;while(l=l[p])if(h?l.nodeName.toLowerCase()===r:1===l.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){k=q[u]||(q[u]={}),j=k[a]||[],n=j[0]===w&&j[1],m=j[0]===w&&j[2],l=n&&q.childNodes[n];while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if(1===l.nodeType&&++m&&l===b){k[a]=[w,n,m];break}}else if(s&&(j=(b[u]||(b[u]={}))[a])&&j[0]===w)m=j[1];else while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if((h?l.nodeName.toLowerCase()===r:1===l.nodeType)&&++m&&(s&&((l[u]||(l[u]={}))[a]=[w,m]),l===b))break;return m-=e,m===d||m%d===0&&m/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||fb.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?hb(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=K.call(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:hb(function(a){var b=[],c=[],d=h(a.replace(R,"$1"));return d[u]?hb(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),!c.pop()}}),has:hb(function(a){return function(b){return fb(a,b).length>0}}),contains:hb(function(a){return function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:hb(function(a){return W.test(a||"")||fb.error("unsupported lang: "+a),a=a.replace(cb,db).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Z.test(a.nodeName)},input:function(a){return Y.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:nb(function(){return[0]}),last:nb(function(a,b){return[b-1]}),eq:nb(function(a,b,c){return[0>c?c+b:c]}),even:nb(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:nb(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:nb(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:nb(function(a,b,c){for(var d=0>c?c+b:c;++d<b;)a.push(d);return a})}},d.pseudos.nth=d.pseudos.eq;for(b in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})d.pseudos[b]=lb(b);for(b in{submit:!0,reset:!0})d.pseudos[b]=mb(b);function pb(){}pb.prototype=d.filters=d.pseudos,d.setFilters=new pb,g=fb.tokenize=function(a,b){var c,e,f,g,h,i,j,k=z[a+" "];if(k)return b?0:k.slice(0);h=a,i=[],j=d.preFilter;while(h){(!c||(e=S.exec(h)))&&(e&&(h=h.slice(e[0].length)||h),i.push(f=[])),c=!1,(e=T.exec(h))&&(c=e.shift(),f.push({value:c,type:e[0].replace(R," ")}),h=h.slice(c.length));for(g in d.filter)!(e=X[g].exec(h))||j[g]&&!(e=j[g](e))||(c=e.shift(),f.push({value:c,type:g,matches:e}),h=h.slice(c.length));if(!c)break}return b?h.length:h?fb.error(a):z(a,i).slice(0)};function qb(a){for(var b=0,c=a.length,d="";c>b;b++)d+=a[b].value;return d}function rb(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(i=b[u]||(b[u]={}),(h=i[d])&&h[0]===w&&h[1]===f)return j[2]=h[2];if(i[d]=j,j[2]=a(b,c,g))return!0}}}function sb(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function tb(a,b,c){for(var d=0,e=b.length;e>d;d++)fb(a,b[d],c);return c}function ub(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(!c||c(f,d,e))&&(g.push(f),j&&b.push(h));return g}function vb(a,b,c,d,e,f){return d&&!d[u]&&(d=vb(d)),e&&!e[u]&&(e=vb(e,f)),hb(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||tb(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:ub(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=ub(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?K.call(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=ub(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):I.apply(g,r)})}function wb(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=rb(function(a){return a===b},h,!0),l=rb(function(a){return K.call(b,a)>-1},h,!0),m=[function(a,c,d){return!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d))}];f>i;i++)if(c=d.relative[a[i].type])m=[rb(sb(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return vb(i>1&&sb(m),i>1&&qb(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(R,"$1"),c,e>i&&wb(a.slice(i,e)),f>e&&wb(a=a.slice(e)),f>e&&qb(a))}m.push(c)}return sb(m)}function xb(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,m,o,p=0,q="0",r=f&&[],s=[],t=j,u=f||e&&d.find.TAG("*",k),v=w+=null==t?1:Math.random()||.1,x=u.length;for(k&&(j=g!==n&&g);q!==x&&null!=(l=u[q]);q++){if(e&&l){m=0;while(o=a[m++])if(o(l,g,h)){i.push(l);break}k&&(w=v)}c&&((l=!o&&l)&&p--,f&&r.push(l))}if(p+=q,c&&q!==p){m=0;while(o=b[m++])o(r,s,g,h);if(f){if(p>0)while(q--)r[q]||s[q]||(s[q]=G.call(i));s=ub(s)}I.apply(i,s),k&&!f&&s.length>0&&p+b.length>1&&fb.uniqueSort(i)}return k&&(w=v,j=t),r};return c?hb(f):f}return h=fb.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=wb(b[c]),f[u]?d.push(f):e.push(f);f=A(a,xb(e,d)),f.selector=a}return f},i=fb.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(cb,db),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=X.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(cb,db),ab.test(j[0].type)&&ob(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&qb(j),!a)return I.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,ab.test(a)&&ob(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ib(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ib(function(a){return a.innerHTML="<a href='#'></a>","#"===a.firstChild.getAttribute("href")})||jb("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ib(function(a){return a.innerHTML="<input/>",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||jb("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ib(function(a){return null==a.getAttribute("disabled")})||jb(L,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),fb}(a);m.find=s,m.expr=s.selectors,m.expr[":"]=m.expr.pseudos,m.unique=s.uniqueSort,m.text=s.getText,m.isXMLDoc=s.isXML,m.contains=s.contains;var t=m.expr.match.needsContext,u=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,v=/^.[^:#\[\.,]*$/;function w(a,b,c){if(m.isFunction(b))return m.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return m.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(v.test(b))return m.filter(b,a,c);b=m.filter(b,a)}return m.grep(a,function(a){return m.inArray(a,b)>=0!==c})}m.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?m.find.matchesSelector(d,a)?[d]:[]:m.find.matches(a,m.grep(b,function(a){return 1===a.nodeType}))},m.fn.extend({find:function(a){var b,c=[],d=this,e=d.length;if("string"!=typeof a)return this.pushStack(m(a).filter(function(){for(b=0;e>b;b++)if(m.contains(d[b],this))return!0}));for(b=0;e>b;b++)m.find(a,d[b],c);return c=this.pushStack(e>1?m.unique(c):c),c.selector=this.selector?this.selector+" "+a:a,c},filter:function(a){return this.pushStack(w(this,a||[],!1))},not:function(a){return this.pushStack(w(this,a||[],!0))},is:function(a){return!!w(this,"string"==typeof a&&t.test(a)?m(a):a||[],!1).length}});var x,y=a.document,z=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,A=m.fn.init=function(a,b){var c,d;if(!a)return this;if("string"==typeof a){if(c="<"===a.charAt(0)&&">"===a.charAt(a.length-1)&&a.length>=3?[null,a,null]:z.exec(a),!c||!c[1]&&b)return!b||b.jquery?(b||x).find(a):this.constructor(b).find(a);if(c[1]){if(b=b instanceof m?b[0]:b,m.merge(this,m.parseHTML(c[1],b&&b.nodeType?b.ownerDocument||b:y,!0)),u.test(c[1])&&m.isPlainObject(b))for(c in b)m.isFunction(this[c])?this[c](b[c]):this.attr(c,b[c]);return this}if(d=y.getElementById(c[2]),d&&d.parentNode){if(d.id!==c[2])return x.find(a);this.length=1,this[0]=d}return this.context=y,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):m.isFunction(a)?"undefined"!=typeof x.ready?x.ready(a):a(m):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),m.makeArray(a,this))};A.prototype=m.fn,x=m(y);var B=/^(?:parents|prev(?:Until|All))/,C={children:!0,contents:!0,next:!0,prev:!0};m.extend({dir:function(a,b,c){var d=[],e=a[b];while(e&&9!==e.nodeType&&(void 0===c||1!==e.nodeType||!m(e).is(c)))1===e.nodeType&&d.push(e),e=e[b];return d},sibling:function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c}}),m.fn.extend({has:function(a){var b,c=m(a,this),d=c.length;return this.filter(function(){for(b=0;d>b;b++)if(m.contains(this,c[b]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=t.test(a)||"string"!=typeof a?m(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&m.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?m.unique(f):f)},index:function(a){return a?"string"==typeof a?m.inArray(this[0],m(a)):m.inArray(a.jquery?a[0]:a,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(m.unique(m.merge(this.get(),m(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function D(a,b){do a=a[b];while(a&&1!==a.nodeType);return a}m.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return m.dir(a,"parentNode")},parentsUntil:function(a,b,c){return m.dir(a,"parentNode",c)},next:function(a){return D(a,"nextSibling")},prev:function(a){return D(a,"previousSibling")},nextAll:function(a){return m.dir(a,"nextSibling")},prevAll:function(a){return m.dir(a,"previousSibling")},nextUntil:function(a,b,c){return m.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return m.dir(a,"previousSibling",c)},siblings:function(a){return m.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return m.sibling(a.firstChild)},contents:function(a){return m.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:m.merge([],a.childNodes)}},function(a,b){m.fn[a]=function(c,d){var e=m.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=m.filter(d,e)),this.length>1&&(C[a]||(e=m.unique(e)),B.test(a)&&(e=e.reverse())),this.pushStack(e)}});var E=/\S+/g,F={};function G(a){var b=F[a]={};return m.each(a.match(E)||[],function(a,c){b[c]=!0}),b}m.Callbacks=function(a){a="string"==typeof a?F[a]||G(a):m.extend({},a);var b,c,d,e,f,g,h=[],i=!a.once&&[],j=function(l){for(c=a.memory&&l,d=!0,f=g||0,g=0,e=h.length,b=!0;h&&e>f;f++)if(h[f].apply(l[0],l[1])===!1&&a.stopOnFalse){c=!1;break}b=!1,h&&(i?i.length&&j(i.shift()):c?h=[]:k.disable())},k={add:function(){if(h){var d=h.length;!function f(b){m.each(b,function(b,c){var d=m.type(c);"function"===d?a.unique&&k.has(c)||h.push(c):c&&c.length&&"string"!==d&&f(c)})}(arguments),b?e=h.length:c&&(g=d,j(c))}return this},remove:function(){return h&&m.each(arguments,function(a,c){var d;while((d=m.inArray(c,h,d))>-1)h.splice(d,1),b&&(e>=d&&e--,f>=d&&f--)}),this},has:function(a){return a?m.inArray(a,h)>-1:!(!h||!h.length)},empty:function(){return h=[],e=0,this},disable:function(){return h=i=c=void 0,this},disabled:function(){return!h},lock:function(){return i=void 0,c||k.disable(),this},locked:function(){return!i},fireWith:function(a,c){return!h||d&&!i||(c=c||[],c=[a,c.slice?c.slice():c],b?i.push(c):j(c)),this},fire:function(){return k.fireWith(this,arguments),this},fired:function(){return!!d}};return k},m.extend({Deferred:function(a){var b=[["resolve","done",m.Callbacks("once memory"),"resolved"],["reject","fail",m.Callbacks("once memory"),"rejected"],["notify","progress",m.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return m.Deferred(function(c){m.each(b,function(b,f){var g=m.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&m.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?m.extend(a,d):d}},e={};return d.pipe=d.then,m.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=d.call(arguments),e=c.length,f=1!==e||a&&m.isFunction(a.promise)?e:0,g=1===f?a:m.Deferred(),h=function(a,b,c){return function(e){b[a]=this,c[a]=arguments.length>1?d.call(arguments):e,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(e>1)for(i=new Array(e),j=new Array(e),k=new Array(e);e>b;b++)c[b]&&m.isFunction(c[b].promise)?c[b].promise().done(h(b,k,c)).fail(g.reject).progress(h(b,j,i)):--f;return f||g.resolveWith(k,c),g.promise()}});var H;m.fn.ready=function(a){return m.ready.promise().done(a),this},m.extend({isReady:!1,readyWait:1,holdReady:function(a){a?m.readyWait++:m.ready(!0)},ready:function(a){if(a===!0?!--m.readyWait:!m.isReady){if(!y.body)return setTimeout(m.ready);m.isReady=!0,a!==!0&&--m.readyWait>0||(H.resolveWith(y,[m]),m.fn.triggerHandler&&(m(y).triggerHandler("ready"),m(y).off("ready")))}}});function I(){y.addEventListener?(y.removeEventListener("DOMContentLoaded",J,!1),a.removeEventListener("load",J,!1)):(y.detachEvent("onreadystatechange",J),a.detachEvent("onload",J))}function J(){(y.addEventListener||"load"===event.type||"complete"===y.readyState)&&(I(),m.ready())}m.ready.promise=function(b){if(!H)if(H=m.Deferred(),"complete"===y.readyState)setTimeout(m.ready);else if(y.addEventListener)y.addEventListener("DOMContentLoaded",J,!1),a.addEventListener("load",J,!1);else{y.attachEvent("onreadystatechange",J),a.attachEvent("onload",J);var c=!1;try{c=null==a.frameElement&&y.documentElement}catch(d){}c&&c.doScroll&&!function e(){if(!m.isReady){try{c.doScroll("left")}catch(a){return setTimeout(e,50)}I(),m.ready()}}()}return H.promise(b)};var K="undefined",L;for(L in m(k))break;k.ownLast="0"!==L,k.inlineBlockNeedsLayout=!1,m(function(){var a,b,c,d;c=y.getElementsByTagName("body")[0],c&&c.style&&(b=y.createElement("div"),d=y.createElement("div"),d.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",c.appendChild(d).appendChild(b),typeof b.style.zoom!==K&&(b.style.cssText="display:inline;margin:0;border:0;padding:1px;width:1px;zoom:1",k.inlineBlockNeedsLayout=a=3===b.offsetWidth,a&&(c.style.zoom=1)),c.removeChild(d))}),function(){var a=y.createElement("div");if(null==k.deleteExpando){k.deleteExpando=!0;try{delete a.test}catch(b){k.deleteExpando=!1}}a=null}(),m.acceptData=function(a){var b=m.noData[(a.nodeName+" ").toLowerCase()],c=+a.nodeType||1;return 1!==c&&9!==c?!1:!b||b!==!0&&a.getAttribute("classid")===b};var M=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,N=/([A-Z])/g;function O(a,b,c){if(void 0===c&&1===a.nodeType){var d="data-"+b.replace(N,"-$1").toLowerCase();if(c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:M.test(c)?m.parseJSON(c):c}catch(e){}m.data(a,b,c)}else c=void 0}return c}function P(a){var b;for(b in a)if(("data"!==b||!m.isEmptyObject(a[b]))&&"toJSON"!==b)return!1;return!0}function Q(a,b,d,e){if(m.acceptData(a)){var f,g,h=m.expando,i=a.nodeType,j=i?m.cache:a,k=i?a[h]:a[h]&&h;
+if(k&&j[k]&&(e||j[k].data)||void 0!==d||"string"!=typeof b)return k||(k=i?a[h]=c.pop()||m.guid++:h),j[k]||(j[k]=i?{}:{toJSON:m.noop}),("object"==typeof b||"function"==typeof b)&&(e?j[k]=m.extend(j[k],b):j[k].data=m.extend(j[k].data,b)),g=j[k],e||(g.data||(g.data={}),g=g.data),void 0!==d&&(g[m.camelCase(b)]=d),"string"==typeof b?(f=g[b],null==f&&(f=g[m.camelCase(b)])):f=g,f}}function R(a,b,c){if(m.acceptData(a)){var d,e,f=a.nodeType,g=f?m.cache:a,h=f?a[m.expando]:m.expando;if(g[h]){if(b&&(d=c?g[h]:g[h].data)){m.isArray(b)?b=b.concat(m.map(b,m.camelCase)):b in d?b=[b]:(b=m.camelCase(b),b=b in d?[b]:b.split(" ")),e=b.length;while(e--)delete d[b[e]];if(c?!P(d):!m.isEmptyObject(d))return}(c||(delete g[h].data,P(g[h])))&&(f?m.cleanData([a],!0):k.deleteExpando||g!=g.window?delete g[h]:g[h]=null)}}}m.extend({cache:{},noData:{"applet ":!0,"embed ":!0,"object ":"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(a){return a=a.nodeType?m.cache[a[m.expando]]:a[m.expando],!!a&&!P(a)},data:function(a,b,c){return Q(a,b,c)},removeData:function(a,b){return R(a,b)},_data:function(a,b,c){return Q(a,b,c,!0)},_removeData:function(a,b){return R(a,b,!0)}}),m.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=m.data(f),1===f.nodeType&&!m._data(f,"parsedAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=m.camelCase(d.slice(5)),O(f,d,e[d])));m._data(f,"parsedAttrs",!0)}return e}return"object"==typeof a?this.each(function(){m.data(this,a)}):arguments.length>1?this.each(function(){m.data(this,a,b)}):f?O(f,a,m.data(f,a)):void 0},removeData:function(a){return this.each(function(){m.removeData(this,a)})}}),m.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=m._data(a,b),c&&(!d||m.isArray(c)?d=m._data(a,b,m.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=m.queue(a,b),d=c.length,e=c.shift(),f=m._queueHooks(a,b),g=function(){m.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return m._data(a,c)||m._data(a,c,{empty:m.Callbacks("once memory").add(function(){m._removeData(a,b+"queue"),m._removeData(a,c)})})}}),m.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length<c?m.queue(this[0],a):void 0===b?this:this.each(function(){var c=m.queue(this,a,b);m._queueHooks(this,a),"fx"===a&&"inprogress"!==c[0]&&m.dequeue(this,a)})},dequeue:function(a){return this.each(function(){m.dequeue(this,a)})},clearQueue:function(a){return this.queue(a||"fx",[])},promise:function(a,b){var c,d=1,e=m.Deferred(),f=this,g=this.length,h=function(){--d||e.resolveWith(f,[f])};"string"!=typeof a&&(b=a,a=void 0),a=a||"fx";while(g--)c=m._data(f[g],a+"queueHooks"),c&&c.empty&&(d++,c.empty.add(h));return h(),e.promise(b)}});var S=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,T=["Top","Right","Bottom","Left"],U=function(a,b){return a=b||a,"none"===m.css(a,"display")||!m.contains(a.ownerDocument,a)},V=m.access=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===m.type(c)){e=!0;for(h in c)m.access(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,m.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(m(a),c)})),b))for(;i>h;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},W=/^(?:checkbox|radio)$/i;!function(){var a=y.createElement("input"),b=y.createElement("div"),c=y.createDocumentFragment();if(b.innerHTML=" <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",k.leadingWhitespace=3===b.firstChild.nodeType,k.tbody=!b.getElementsByTagName("tbody").length,k.htmlSerialize=!!b.getElementsByTagName("link").length,k.html5Clone="<:nav></:nav>"!==y.createElement("nav").cloneNode(!0).outerHTML,a.type="checkbox",a.checked=!0,c.appendChild(a),k.appendChecked=a.checked,b.innerHTML="<textarea>x</textarea>",k.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue,c.appendChild(b),b.innerHTML="<input type='radio' checked='checked' name='t'/>",k.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,k.noCloneEvent=!0,b.attachEvent&&(b.attachEvent("onclick",function(){k.noCloneEvent=!1}),b.cloneNode(!0).click()),null==k.deleteExpando){k.deleteExpando=!0;try{delete b.test}catch(d){k.deleteExpando=!1}}}(),function(){var b,c,d=y.createElement("div");for(b in{submit:!0,change:!0,focusin:!0})c="on"+b,(k[b+"Bubbles"]=c in a)||(d.setAttribute(c,"t"),k[b+"Bubbles"]=d.attributes[c].expando===!1);d=null}();var X=/^(?:input|select|textarea)$/i,Y=/^key/,Z=/^(?:mouse|pointer|contextmenu)|click/,$=/^(?:focusinfocus|focusoutblur)$/,_=/^([^.]*)(?:\.(.+)|)$/;function ab(){return!0}function bb(){return!1}function cb(){try{return y.activeElement}catch(a){}}m.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,n,o,p,q,r=m._data(a);if(r){c.handler&&(i=c,c=i.handler,e=i.selector),c.guid||(c.guid=m.guid++),(g=r.events)||(g=r.events={}),(k=r.handle)||(k=r.handle=function(a){return typeof m===K||a&&m.event.triggered===a.type?void 0:m.event.dispatch.apply(k.elem,arguments)},k.elem=a),b=(b||"").match(E)||[""],h=b.length;while(h--)f=_.exec(b[h])||[],o=q=f[1],p=(f[2]||"").split(".").sort(),o&&(j=m.event.special[o]||{},o=(e?j.delegateType:j.bindType)||o,j=m.event.special[o]||{},l=m.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&m.expr.match.needsContext.test(e),namespace:p.join(".")},i),(n=g[o])||(n=g[o]=[],n.delegateCount=0,j.setup&&j.setup.call(a,d,p,k)!==!1||(a.addEventListener?a.addEventListener(o,k,!1):a.attachEvent&&a.attachEvent("on"+o,k))),j.add&&(j.add.call(a,l),l.handler.guid||(l.handler.guid=c.guid)),e?n.splice(n.delegateCount++,0,l):n.push(l),m.event.global[o]=!0);a=null}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,n,o,p,q,r=m.hasData(a)&&m._data(a);if(r&&(k=r.events)){b=(b||"").match(E)||[""],j=b.length;while(j--)if(h=_.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=m.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,n=k[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),i=f=n.length;while(f--)g=n[f],!e&&q!==g.origType||c&&c.guid!==g.guid||h&&!h.test(g.namespace)||d&&d!==g.selector&&("**"!==d||!g.selector)||(n.splice(f,1),g.selector&&n.delegateCount--,l.remove&&l.remove.call(a,g));i&&!n.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||m.removeEvent(a,o,r.handle),delete k[o])}else for(o in k)m.event.remove(a,o+b[j],c,d,!0);m.isEmptyObject(k)&&(delete r.handle,m._removeData(a,"events"))}},trigger:function(b,c,d,e){var f,g,h,i,k,l,n,o=[d||y],p=j.call(b,"type")?b.type:b,q=j.call(b,"namespace")?b.namespace.split("."):[];if(h=l=d=d||y,3!==d.nodeType&&8!==d.nodeType&&!$.test(p+m.event.triggered)&&(p.indexOf(".")>=0&&(q=p.split("."),p=q.shift(),q.sort()),g=p.indexOf(":")<0&&"on"+p,b=b[m.expando]?b:new m.Event(p,"object"==typeof b&&b),b.isTrigger=e?2:3,b.namespace=q.join("."),b.namespace_re=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=d),c=null==c?[b]:m.makeArray(c,[b]),k=m.event.special[p]||{},e||!k.trigger||k.trigger.apply(d,c)!==!1)){if(!e&&!k.noBubble&&!m.isWindow(d)){for(i=k.delegateType||p,$.test(i+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),l=h;l===(d.ownerDocument||y)&&o.push(l.defaultView||l.parentWindow||a)}n=0;while((h=o[n++])&&!b.isPropagationStopped())b.type=n>1?i:k.bindType||p,f=(m._data(h,"events")||{})[b.type]&&m._data(h,"handle"),f&&f.apply(h,c),f=g&&h[g],f&&f.apply&&m.acceptData(h)&&(b.result=f.apply(h,c),b.result===!1&&b.preventDefault());if(b.type=p,!e&&!b.isDefaultPrevented()&&(!k._default||k._default.apply(o.pop(),c)===!1)&&m.acceptData(d)&&g&&d[p]&&!m.isWindow(d)){l=d[g],l&&(d[g]=null),m.event.triggered=p;try{d[p]()}catch(r){}m.event.triggered=void 0,l&&(d[g]=l)}return b.result}},dispatch:function(a){a=m.event.fix(a);var b,c,e,f,g,h=[],i=d.call(arguments),j=(m._data(this,"events")||{})[a.type]||[],k=m.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=m.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,g=0;while((e=f.handlers[g++])&&!a.isImmediatePropagationStopped())(!a.namespace_re||a.namespace_re.test(e.namespace))&&(a.handleObj=e,a.data=e.data,c=((m.event.special[e.origType]||{}).handle||e.handler).apply(f.elem,i),void 0!==c&&(a.result=c)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&(!a.button||"click"!==a.type))for(;i!=this;i=i.parentNode||this)if(1===i.nodeType&&(i.disabled!==!0||"click"!==a.type)){for(e=[],f=0;h>f;f++)d=b[f],c=d.selector+" ",void 0===e[c]&&(e[c]=d.needsContext?m(c,this).index(i)>=0:m.find(c,this,null,[i]).length),e[c]&&e.push(d);e.length&&g.push({elem:i,handlers:e})}return h<b.length&&g.push({elem:this,handlers:b.slice(h)}),g},fix:function(a){if(a[m.expando])return a;var b,c,d,e=a.type,f=a,g=this.fixHooks[e];g||(this.fixHooks[e]=g=Z.test(e)?this.mouseHooks:Y.test(e)?this.keyHooks:{}),d=g.props?this.props.concat(g.props):this.props,a=new m.Event(f),b=d.length;while(b--)c=d[b],a[c]=f[c];return a.target||(a.target=f.srcElement||y),3===a.target.nodeType&&(a.target=a.target.parentNode),a.metaKey=!!a.metaKey,g.filter?g.filter(a,f):a},props:"altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(a,b){return null==a.which&&(a.which=null!=b.charCode?b.charCode:b.keyCode),a}},mouseHooks:{props:"button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(a,b){var c,d,e,f=b.button,g=b.fromElement;return null==a.pageX&&null!=b.clientX&&(d=a.target.ownerDocument||y,e=d.documentElement,c=d.body,a.pageX=b.clientX+(e&&e.scrollLeft||c&&c.scrollLeft||0)-(e&&e.clientLeft||c&&c.clientLeft||0),a.pageY=b.clientY+(e&&e.scrollTop||c&&c.scrollTop||0)-(e&&e.clientTop||c&&c.clientTop||0)),!a.relatedTarget&&g&&(a.relatedTarget=g===a.target?b.toElement:g),a.which||void 0===f||(a.which=1&f?1:2&f?3:4&f?2:0),a}},special:{load:{noBubble:!0},focus:{trigger:function(){if(this!==cb()&&this.focus)try{return this.focus(),!1}catch(a){}},delegateType:"focusin"},blur:{trigger:function(){return this===cb()&&this.blur?(this.blur(),!1):void 0},delegateType:"focusout"},click:{trigger:function(){return m.nodeName(this,"input")&&"checkbox"===this.type&&this.click?(this.click(),!1):void 0},_default:function(a){return m.nodeName(a.target,"a")}},beforeunload:{postDispatch:function(a){void 0!==a.result&&a.originalEvent&&(a.originalEvent.returnValue=a.result)}}},simulate:function(a,b,c,d){var e=m.extend(new m.Event,c,{type:a,isSimulated:!0,originalEvent:{}});d?m.event.trigger(e,null,b):m.event.dispatch.call(b,e),e.isDefaultPrevented()&&c.preventDefault()}},m.removeEvent=y.removeEventListener?function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c,!1)}:function(a,b,c){var d="on"+b;a.detachEvent&&(typeof a[d]===K&&(a[d]=null),a.detachEvent(d,c))},m.Event=function(a,b){return this instanceof m.Event?(a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||void 0===a.defaultPrevented&&a.returnValue===!1?ab:bb):this.type=a,b&&m.extend(this,b),this.timeStamp=a&&a.timeStamp||m.now(),void(this[m.expando]=!0)):new m.Event(a,b)},m.Event.prototype={isDefaultPrevented:bb,isPropagationStopped:bb,isImmediatePropagationStopped:bb,preventDefault:function(){var a=this.originalEvent;this.isDefaultPrevented=ab,a&&(a.preventDefault?a.preventDefault():a.returnValue=!1)},stopPropagation:function(){var a=this.originalEvent;this.isPropagationStopped=ab,a&&(a.stopPropagation&&a.stopPropagation(),a.cancelBubble=!0)},stopImmediatePropagation:function(){var a=this.originalEvent;this.isImmediatePropagationStopped=ab,a&&a.stopImmediatePropagation&&a.stopImmediatePropagation(),this.stopPropagation()}},m.each({mouseenter:"mouseover",mouseleave:"mouseout",pointerenter:"pointerover",pointerleave:"pointerout"},function(a,b){m.event.special[a]={delegateType:b,bindType:b,handle:function(a){var c,d=this,e=a.relatedTarget,f=a.handleObj;return(!e||e!==d&&!m.contains(d,e))&&(a.type=f.origType,c=f.handler.apply(this,arguments),a.type=b),c}}}),k.submitBubbles||(m.event.special.submit={setup:function(){return m.nodeName(this,"form")?!1:void m.event.add(this,"click._submit keypress._submit",function(a){var b=a.target,c=m.nodeName(b,"input")||m.nodeName(b,"button")?b.form:void 0;c&&!m._data(c,"submitBubbles")&&(m.event.add(c,"submit._submit",function(a){a._submit_bubble=!0}),m._data(c,"submitBubbles",!0))})},postDispatch:function(a){a._submit_bubble&&(delete a._submit_bubble,this.parentNode&&!a.isTrigger&&m.event.simulate("submit",this.parentNode,a,!0))},teardown:function(){return m.nodeName(this,"form")?!1:void m.event.remove(this,"._submit")}}),k.changeBubbles||(m.event.special.change={setup:function(){return X.test(this.nodeName)?(("checkbox"===this.type||"radio"===this.type)&&(m.event.add(this,"propertychange._change",function(a){"checked"===a.originalEvent.propertyName&&(this._just_changed=!0)}),m.event.add(this,"click._change",function(a){this._just_changed&&!a.isTrigger&&(this._just_changed=!1),m.event.simulate("change",this,a,!0)})),!1):void m.event.add(this,"beforeactivate._change",function(a){var b=a.target;X.test(b.nodeName)&&!m._data(b,"changeBubbles")&&(m.event.add(b,"change._change",function(a){!this.parentNode||a.isSimulated||a.isTrigger||m.event.simulate("change",this.parentNode,a,!0)}),m._data(b,"changeBubbles",!0))})},handle:function(a){var b=a.target;return this!==b||a.isSimulated||a.isTrigger||"radio"!==b.type&&"checkbox"!==b.type?a.handleObj.handler.apply(this,arguments):void 0},teardown:function(){return m.event.remove(this,"._change"),!X.test(this.nodeName)}}),k.focusinBubbles||m.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){m.event.simulate(b,a.target,m.event.fix(a),!0)};m.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=m._data(d,b);e||d.addEventListener(a,c,!0),m._data(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=m._data(d,b)-1;e?m._data(d,b,e):(d.removeEventListener(a,c,!0),m._removeData(d,b))}}}),m.fn.extend({on:function(a,b,c,d,e){var f,g;if("object"==typeof a){"string"!=typeof b&&(c=c||b,b=void 0);for(f in a)this.on(f,b,c,a[f],e);return this}if(null==c&&null==d?(d=b,c=b=void 0):null==d&&("string"==typeof b?(d=c,c=void 0):(d=c,c=b,b=void 0)),d===!1)d=bb;else if(!d)return this;return 1===e&&(g=d,d=function(a){return m().off(a),g.apply(this,arguments)},d.guid=g.guid||(g.guid=m.guid++)),this.each(function(){m.event.add(this,a,d,c,b)})},one:function(a,b,c,d){return this.on(a,b,c,d,1)},off:function(a,b,c){var d,e;if(a&&a.preventDefault&&a.handleObj)return d=a.handleObj,m(a.delegateTarget).off(d.namespace?d.origType+"."+d.namespace:d.origType,d.selector,d.handler),this;if("object"==typeof a){for(e in a)this.off(e,b,a[e]);return this}return(b===!1||"function"==typeof b)&&(c=b,b=void 0),c===!1&&(c=bb),this.each(function(){m.event.remove(this,a,c,b)})},trigger:function(a,b){return this.each(function(){m.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];return c?m.event.trigger(a,b,c,!0):void 0}});function db(a){var b=eb.split("|"),c=a.createDocumentFragment();if(c.createElement)while(b.length)c.createElement(b.pop());return c}var eb="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",fb=/ jQuery\d+="(?:null|\d+)"/g,gb=new RegExp("<(?:"+eb+")[\\s/>]","i"),hb=/^\s+/,ib=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,jb=/<([\w:]+)/,kb=/<tbody/i,lb=/<|&#?\w+;/,mb=/<(?:script|style|link)/i,nb=/checked\s*(?:[^=]|=\s*.checked.)/i,ob=/^$|\/(?:java|ecma)script/i,pb=/^true\/(.*)/,qb=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g,rb={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],area:[1,"<map>","</map>"],param:[1,"<object>","</object>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:k.htmlSerialize?[0,"",""]:[1,"X<div>","</div>"]},sb=db(y),tb=sb.appendChild(y.createElement("div"));rb.optgroup=rb.option,rb.tbody=rb.tfoot=rb.colgroup=rb.caption=rb.thead,rb.th=rb.td;function ub(a,b){var c,d,e=0,f=typeof a.getElementsByTagName!==K?a.getElementsByTagName(b||"*"):typeof a.querySelectorAll!==K?a.querySelectorAll(b||"*"):void 0;if(!f)for(f=[],c=a.childNodes||a;null!=(d=c[e]);e++)!b||m.nodeName(d,b)?f.push(d):m.merge(f,ub(d,b));return void 0===b||b&&m.nodeName(a,b)?m.merge([a],f):f}function vb(a){W.test(a.type)&&(a.defaultChecked=a.checked)}function wb(a,b){return m.nodeName(a,"table")&&m.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function xb(a){return a.type=(null!==m.find.attr(a,"type"))+"/"+a.type,a}function yb(a){var b=pb.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function zb(a,b){for(var c,d=0;null!=(c=a[d]);d++)m._data(c,"globalEval",!b||m._data(b[d],"globalEval"))}function Ab(a,b){if(1===b.nodeType&&m.hasData(a)){var c,d,e,f=m._data(a),g=m._data(b,f),h=f.events;if(h){delete g.handle,g.events={};for(c in h)for(d=0,e=h[c].length;e>d;d++)m.event.add(b,c,h[c][d])}g.data&&(g.data=m.extend({},g.data))}}function Bb(a,b){var c,d,e;if(1===b.nodeType){if(c=b.nodeName.toLowerCase(),!k.noCloneEvent&&b[m.expando]){e=m._data(b);for(d in e.events)m.removeEvent(b,d,e.handle);b.removeAttribute(m.expando)}"script"===c&&b.text!==a.text?(xb(b).text=a.text,yb(b)):"object"===c?(b.parentNode&&(b.outerHTML=a.outerHTML),k.html5Clone&&a.innerHTML&&!m.trim(b.innerHTML)&&(b.innerHTML=a.innerHTML)):"input"===c&&W.test(a.type)?(b.defaultChecked=b.checked=a.checked,b.value!==a.value&&(b.value=a.value)):"option"===c?b.defaultSelected=b.selected=a.defaultSelected:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}}m.extend({clone:function(a,b,c){var d,e,f,g,h,i=m.contains(a.ownerDocument,a);if(k.html5Clone||m.isXMLDoc(a)||!gb.test("<"+a.nodeName+">")?f=a.cloneNode(!0):(tb.innerHTML=a.outerHTML,tb.removeChild(f=tb.firstChild)),!(k.noCloneEvent&&k.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||m.isXMLDoc(a)))for(d=ub(f),h=ub(a),g=0;null!=(e=h[g]);++g)d[g]&&Bb(e,d[g]);if(b)if(c)for(h=h||ub(a),d=d||ub(f),g=0;null!=(e=h[g]);g++)Ab(e,d[g]);else Ab(a,f);return d=ub(f,"script"),d.length>0&&zb(d,!i&&ub(a,"script")),d=h=e=null,f},buildFragment:function(a,b,c,d){for(var e,f,g,h,i,j,l,n=a.length,o=db(b),p=[],q=0;n>q;q++)if(f=a[q],f||0===f)if("object"===m.type(f))m.merge(p,f.nodeType?[f]:f);else if(lb.test(f)){h=h||o.appendChild(b.createElement("div")),i=(jb.exec(f)||["",""])[1].toLowerCase(),l=rb[i]||rb._default,h.innerHTML=l[1]+f.replace(ib,"<$1></$2>")+l[2],e=l[0];while(e--)h=h.lastChild;if(!k.leadingWhitespace&&hb.test(f)&&p.push(b.createTextNode(hb.exec(f)[0])),!k.tbody){f="table"!==i||kb.test(f)?"<table>"!==l[1]||kb.test(f)?0:h:h.firstChild,e=f&&f.childNodes.length;while(e--)m.nodeName(j=f.childNodes[e],"tbody")&&!j.childNodes.length&&f.removeChild(j)}m.merge(p,h.childNodes),h.textContent="";while(h.firstChild)h.removeChild(h.firstChild);h=o.lastChild}else p.push(b.createTextNode(f));h&&o.removeChild(h),k.appendChecked||m.grep(ub(p,"input"),vb),q=0;while(f=p[q++])if((!d||-1===m.inArray(f,d))&&(g=m.contains(f.ownerDocument,f),h=ub(o.appendChild(f),"script"),g&&zb(h),c)){e=0;while(f=h[e++])ob.test(f.type||"")&&c.push(f)}return h=null,o},cleanData:function(a,b){for(var d,e,f,g,h=0,i=m.expando,j=m.cache,l=k.deleteExpando,n=m.event.special;null!=(d=a[h]);h++)if((b||m.acceptData(d))&&(f=d[i],g=f&&j[f])){if(g.events)for(e in g.events)n[e]?m.event.remove(d,e):m.removeEvent(d,e,g.handle);j[f]&&(delete j[f],l?delete d[i]:typeof d.removeAttribute!==K?d.removeAttribute(i):d[i]=null,c.push(f))}}}),m.fn.extend({text:function(a){return V(this,function(a){return void 0===a?m.text(this):this.empty().append((this[0]&&this[0].ownerDocument||y).createTextNode(a))},null,a,arguments.length)},append:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=wb(this,a);b.appendChild(a)}})},prepend:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=wb(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},remove:function(a,b){for(var c,d=a?m.filter(a,this):this,e=0;null!=(c=d[e]);e++)b||1!==c.nodeType||m.cleanData(ub(c)),c.parentNode&&(b&&m.contains(c.ownerDocument,c)&&zb(ub(c,"script")),c.parentNode.removeChild(c));return this},empty:function(){for(var a,b=0;null!=(a=this[b]);b++){1===a.nodeType&&m.cleanData(ub(a,!1));while(a.firstChild)a.removeChild(a.firstChild);a.options&&m.nodeName(a,"select")&&(a.options.length=0)}return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return m.clone(this,a,b)})},html:function(a){return V(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a)return 1===b.nodeType?b.innerHTML.replace(fb,""):void 0;if(!("string"!=typeof a||mb.test(a)||!k.htmlSerialize&&gb.test(a)||!k.leadingWhitespace&&hb.test(a)||rb[(jb.exec(a)||["",""])[1].toLowerCase()])){a=a.replace(ib,"<$1></$2>");try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(m.cleanData(ub(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=arguments[0];return this.domManip(arguments,function(b){a=this.parentNode,m.cleanData(ub(this)),a&&a.replaceChild(b,this)}),a&&(a.length||a.nodeType)?this:this.remove()},detach:function(a){return this.remove(a,!0)},domManip:function(a,b){a=e.apply([],a);var c,d,f,g,h,i,j=0,l=this.length,n=this,o=l-1,p=a[0],q=m.isFunction(p);if(q||l>1&&"string"==typeof p&&!k.checkClone&&nb.test(p))return this.each(function(c){var d=n.eq(c);q&&(a[0]=p.call(this,c,d.html())),d.domManip(a,b)});if(l&&(i=m.buildFragment(a,this[0].ownerDocument,!1,this),c=i.firstChild,1===i.childNodes.length&&(i=c),c)){for(g=m.map(ub(i,"script"),xb),f=g.length;l>j;j++)d=i,j!==o&&(d=m.clone(d,!0,!0),f&&m.merge(g,ub(d,"script"))),b.call(this[j],d,j);if(f)for(h=g[g.length-1].ownerDocument,m.map(g,yb),j=0;f>j;j++)d=g[j],ob.test(d.type||"")&&!m._data(d,"globalEval")&&m.contains(h,d)&&(d.src?m._evalUrl&&m._evalUrl(d.src):m.globalEval((d.text||d.textContent||d.innerHTML||"").replace(qb,"")));i=c=null}return this}}),m.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){m.fn[a]=function(a){for(var c,d=0,e=[],g=m(a),h=g.length-1;h>=d;d++)c=d===h?this:this.clone(!0),m(g[d])[b](c),f.apply(e,c.get());return this.pushStack(e)}});var Cb,Db={};function Eb(b,c){var d,e=m(c.createElement(b)).appendTo(c.body),f=a.getDefaultComputedStyle&&(d=a.getDefaultComputedStyle(e[0]))?d.display:m.css(e[0],"display");return e.detach(),f}function Fb(a){var b=y,c=Db[a];return c||(c=Eb(a,b),"none"!==c&&c||(Cb=(Cb||m("<iframe frameborder='0' width='0' height='0'/>")).appendTo(b.documentElement),b=(Cb[0].contentWindow||Cb[0].contentDocument).document,b.write(),b.close(),c=Eb(a,b),Cb.detach()),Db[a]=c),c}!function(){var a;k.shrinkWrapBlocks=function(){if(null!=a)return a;a=!1;var b,c,d;return c=y.getElementsByTagName("body")[0],c&&c.style?(b=y.createElement("div"),d=y.createElement("div"),d.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",c.appendChild(d).appendChild(b),typeof b.style.zoom!==K&&(b.style.cssText="-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;display:block;margin:0;border:0;padding:1px;width:1px;zoom:1",b.appendChild(y.createElement("div")).style.width="5px",a=3!==b.offsetWidth),c.removeChild(d),a):void 0}}();var Gb=/^margin/,Hb=new RegExp("^("+S+")(?!px)[a-z%]+$","i"),Ib,Jb,Kb=/^(top|right|bottom|left)$/;a.getComputedStyle?(Ib=function(a){return a.ownerDocument.defaultView.getComputedStyle(a,null)},Jb=function(a,b,c){var d,e,f,g,h=a.style;return c=c||Ib(a),g=c?c.getPropertyValue(b)||c[b]:void 0,c&&(""!==g||m.contains(a.ownerDocument,a)||(g=m.style(a,b)),Hb.test(g)&&Gb.test(b)&&(d=h.width,e=h.minWidth,f=h.maxWidth,h.minWidth=h.maxWidth=h.width=g,g=c.width,h.width=d,h.minWidth=e,h.maxWidth=f)),void 0===g?g:g+""}):y.documentElement.currentStyle&&(Ib=function(a){return a.currentStyle},Jb=function(a,b,c){var d,e,f,g,h=a.style;return c=c||Ib(a),g=c?c[b]:void 0,null==g&&h&&h[b]&&(g=h[b]),Hb.test(g)&&!Kb.test(b)&&(d=h.left,e=a.runtimeStyle,f=e&&e.left,f&&(e.left=a.currentStyle.left),h.left="fontSize"===b?"1em":g,g=h.pixelLeft+"px",h.left=d,f&&(e.left=f)),void 0===g?g:g+""||"auto"});function Lb(a,b){return{get:function(){var c=a();if(null!=c)return c?void delete this.get:(this.get=b).apply(this,arguments)}}}!function(){var b,c,d,e,f,g,h;if(b=y.createElement("div"),b.innerHTML=" <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",d=b.getElementsByTagName("a")[0],c=d&&d.style){c.cssText="float:left;opacity:.5",k.opacity="0.5"===c.opacity,k.cssFloat=!!c.cssFloat,b.style.backgroundClip="content-box",b.cloneNode(!0).style.backgroundClip="",k.clearCloneStyle="content-box"===b.style.backgroundClip,k.boxSizing=""===c.boxSizing||""===c.MozBoxSizing||""===c.WebkitBoxSizing,m.extend(k,{reliableHiddenOffsets:function(){return null==g&&i(),g},boxSizingReliable:function(){return null==f&&i(),f},pixelPosition:function(){return null==e&&i(),e},reliableMarginRight:function(){return null==h&&i(),h}});function i(){var b,c,d,i;c=y.getElementsByTagName("body")[0],c&&c.style&&(b=y.createElement("div"),d=y.createElement("div"),d.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",c.appendChild(d).appendChild(b),b.style.cssText="-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;display:block;margin-top:1%;top:1%;border:1px;padding:1px;width:4px;position:absolute",e=f=!1,h=!0,a.getComputedStyle&&(e="1%"!==(a.getComputedStyle(b,null)||{}).top,f="4px"===(a.getComputedStyle(b,null)||{width:"4px"}).width,i=b.appendChild(y.createElement("div")),i.style.cssText=b.style.cssText="-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;display:block;margin:0;border:0;padding:0",i.style.marginRight=i.style.width="0",b.style.width="1px",h=!parseFloat((a.getComputedStyle(i,null)||{}).marginRight)),b.innerHTML="<table><tr><td></td><td>t</td></tr></table>",i=b.getElementsByTagName("td"),i[0].style.cssText="margin:0;border:0;padding:0;display:none",g=0===i[0].offsetHeight,g&&(i[0].style.display="",i[1].style.display="none",g=0===i[0].offsetHeight),c.removeChild(d))}}}(),m.swap=function(a,b,c,d){var e,f,g={};for(f in b)g[f]=a.style[f],a.style[f]=b[f];e=c.apply(a,d||[]);for(f in b)a.style[f]=g[f];return e};var Mb=/alpha\([^)]*\)/i,Nb=/opacity\s*=\s*([^)]*)/,Ob=/^(none|table(?!-c[ea]).+)/,Pb=new RegExp("^("+S+")(.*)$","i"),Qb=new RegExp("^([+-])=("+S+")","i"),Rb={position:"absolute",visibility:"hidden",display:"block"},Sb={letterSpacing:"0",fontWeight:"400"},Tb=["Webkit","O","Moz","ms"];function Ub(a,b){if(b in a)return b;var c=b.charAt(0).toUpperCase()+b.slice(1),d=b,e=Tb.length;while(e--)if(b=Tb[e]+c,b in a)return b;return d}function Vb(a,b){for(var c,d,e,f=[],g=0,h=a.length;h>g;g++)d=a[g],d.style&&(f[g]=m._data(d,"olddisplay"),c=d.style.display,b?(f[g]||"none"!==c||(d.style.display=""),""===d.style.display&&U(d)&&(f[g]=m._data(d,"olddisplay",Fb(d.nodeName)))):(e=U(d),(c&&"none"!==c||!e)&&m._data(d,"olddisplay",e?c:m.css(d,"display"))));for(g=0;h>g;g++)d=a[g],d.style&&(b&&"none"!==d.style.display&&""!==d.style.display||(d.style.display=b?f[g]||"":"none"));return a}function Wb(a,b,c){var d=Pb.exec(b);return d?Math.max(0,d[1]-(c||0))+(d[2]||"px"):b}function Xb(a,b,c,d,e){for(var f=c===(d?"border":"content")?4:"width"===b?1:0,g=0;4>f;f+=2)"margin"===c&&(g+=m.css(a,c+T[f],!0,e)),d?("content"===c&&(g-=m.css(a,"padding"+T[f],!0,e)),"margin"!==c&&(g-=m.css(a,"border"+T[f]+"Width",!0,e))):(g+=m.css(a,"padding"+T[f],!0,e),"padding"!==c&&(g+=m.css(a,"border"+T[f]+"Width",!0,e)));return g}function Yb(a,b,c){var d=!0,e="width"===b?a.offsetWidth:a.offsetHeight,f=Ib(a),g=k.boxSizing&&"border-box"===m.css(a,"boxSizing",!1,f);if(0>=e||null==e){if(e=Jb(a,b,f),(0>e||null==e)&&(e=a.style[b]),Hb.test(e))return e;d=g&&(k.boxSizingReliable()||e===a.style[b]),e=parseFloat(e)||0}return e+Xb(a,b,c||(g?"border":"content"),d,f)+"px"}m.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=Jb(a,"opacity");return""===c?"1":c}}}},cssNumber:{columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":k.cssFloat?"cssFloat":"styleFloat"},style:function(a,b,c,d){if(a&&3!==a.nodeType&&8!==a.nodeType&&a.style){var e,f,g,h=m.camelCase(b),i=a.style;if(b=m.cssProps[h]||(m.cssProps[h]=Ub(i,h)),g=m.cssHooks[b]||m.cssHooks[h],void 0===c)return g&&"get"in g&&void 0!==(e=g.get(a,!1,d))?e:i[b];if(f=typeof c,"string"===f&&(e=Qb.exec(c))&&(c=(e[1]+1)*e[2]+parseFloat(m.css(a,b)),f="number"),null!=c&&c===c&&("number"!==f||m.cssNumber[h]||(c+="px"),k.clearCloneStyle||""!==c||0!==b.indexOf("background")||(i[b]="inherit"),!(g&&"set"in g&&void 0===(c=g.set(a,c,d)))))try{i[b]=c}catch(j){}}},css:function(a,b,c,d){var e,f,g,h=m.camelCase(b);return b=m.cssProps[h]||(m.cssProps[h]=Ub(a.style,h)),g=m.cssHooks[b]||m.cssHooks[h],g&&"get"in g&&(f=g.get(a,!0,c)),void 0===f&&(f=Jb(a,b,d)),"normal"===f&&b in Sb&&(f=Sb[b]),""===c||c?(e=parseFloat(f),c===!0||m.isNumeric(e)?e||0:f):f}}),m.each(["height","width"],function(a,b){m.cssHooks[b]={get:function(a,c,d){return c?Ob.test(m.css(a,"display"))&&0===a.offsetWidth?m.swap(a,Rb,function(){return Yb(a,b,d)}):Yb(a,b,d):void 0},set:function(a,c,d){var e=d&&Ib(a);return Wb(a,c,d?Xb(a,b,d,k.boxSizing&&"border-box"===m.css(a,"boxSizing",!1,e),e):0)}}}),k.opacity||(m.cssHooks.opacity={get:function(a,b){return Nb.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?.01*parseFloat(RegExp.$1)+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=m.isNumeric(b)?"alpha(opacity="+100*b+")":"",f=d&&d.filter||c.filter||"";c.zoom=1,(b>=1||""===b)&&""===m.trim(f.replace(Mb,""))&&c.removeAttribute&&(c.removeAttribute("filter"),""===b||d&&!d.filter)||(c.filter=Mb.test(f)?f.replace(Mb,e):f+" "+e)}}),m.cssHooks.marginRight=Lb(k.reliableMarginRight,function(a,b){return b?m.swap(a,{display:"inline-block"},Jb,[a,"marginRight"]):void 0}),m.each({margin:"",padding:"",border:"Width"},function(a,b){m.cssHooks[a+b]={expand:function(c){for(var d=0,e={},f="string"==typeof c?c.split(" "):[c];4>d;d++)e[a+T[d]+b]=f[d]||f[d-2]||f[0];return e}},Gb.test(a)||(m.cssHooks[a+b].set=Wb)}),m.fn.extend({css:function(a,b){return V(this,function(a,b,c){var d,e,f={},g=0;if(m.isArray(b)){for(d=Ib(a),e=b.length;e>g;g++)f[b[g]]=m.css(a,b[g],!1,d);return f}return void 0!==c?m.style(a,b,c):m.css(a,b)},a,b,arguments.length>1)},show:function(){return Vb(this,!0)},hide:function(){return Vb(this)},toggle:function(a){return"boolean"==typeof a?a?this.show():this.hide():this.each(function(){U(this)?m(this).show():m(this).hide()})}});function Zb(a,b,c,d,e){return new Zb.prototype.init(a,b,c,d,e)}m.Tween=Zb,Zb.prototype={constructor:Zb,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||"swing",this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(m.cssNumber[c]?"":"px")
+},cur:function(){var a=Zb.propHooks[this.prop];return a&&a.get?a.get(this):Zb.propHooks._default.get(this)},run:function(a){var b,c=Zb.propHooks[this.prop];return this.pos=b=this.options.duration?m.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):Zb.propHooks._default.set(this),this}},Zb.prototype.init.prototype=Zb.prototype,Zb.propHooks={_default:{get:function(a){var b;return null==a.elem[a.prop]||a.elem.style&&null!=a.elem.style[a.prop]?(b=m.css(a.elem,a.prop,""),b&&"auto"!==b?b:0):a.elem[a.prop]},set:function(a){m.fx.step[a.prop]?m.fx.step[a.prop](a):a.elem.style&&(null!=a.elem.style[m.cssProps[a.prop]]||m.cssHooks[a.prop])?m.style(a.elem,a.prop,a.now+a.unit):a.elem[a.prop]=a.now}}},Zb.propHooks.scrollTop=Zb.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},m.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2}},m.fx=Zb.prototype.init,m.fx.step={};var $b,_b,ac=/^(?:toggle|show|hide)$/,bc=new RegExp("^(?:([+-])=|)("+S+")([a-z%]*)$","i"),cc=/queueHooks$/,dc=[ic],ec={"*":[function(a,b){var c=this.createTween(a,b),d=c.cur(),e=bc.exec(b),f=e&&e[3]||(m.cssNumber[a]?"":"px"),g=(m.cssNumber[a]||"px"!==f&&+d)&&bc.exec(m.css(c.elem,a)),h=1,i=20;if(g&&g[3]!==f){f=f||g[3],e=e||[],g=+d||1;do h=h||".5",g/=h,m.style(c.elem,a,g+f);while(h!==(h=c.cur()/d)&&1!==h&&--i)}return e&&(g=c.start=+g||+d||0,c.unit=f,c.end=e[1]?g+(e[1]+1)*e[2]:+e[2]),c}]};function fc(){return setTimeout(function(){$b=void 0}),$b=m.now()}function gc(a,b){var c,d={height:a},e=0;for(b=b?1:0;4>e;e+=2-b)c=T[e],d["margin"+c]=d["padding"+c]=a;return b&&(d.opacity=d.width=a),d}function hc(a,b,c){for(var d,e=(ec[b]||[]).concat(ec["*"]),f=0,g=e.length;g>f;f++)if(d=e[f].call(c,b,a))return d}function ic(a,b,c){var d,e,f,g,h,i,j,l,n=this,o={},p=a.style,q=a.nodeType&&U(a),r=m._data(a,"fxshow");c.queue||(h=m._queueHooks(a,"fx"),null==h.unqueued&&(h.unqueued=0,i=h.empty.fire,h.empty.fire=function(){h.unqueued||i()}),h.unqueued++,n.always(function(){n.always(function(){h.unqueued--,m.queue(a,"fx").length||h.empty.fire()})})),1===a.nodeType&&("height"in b||"width"in b)&&(c.overflow=[p.overflow,p.overflowX,p.overflowY],j=m.css(a,"display"),l="none"===j?m._data(a,"olddisplay")||Fb(a.nodeName):j,"inline"===l&&"none"===m.css(a,"float")&&(k.inlineBlockNeedsLayout&&"inline"!==Fb(a.nodeName)?p.zoom=1:p.display="inline-block")),c.overflow&&(p.overflow="hidden",k.shrinkWrapBlocks()||n.always(function(){p.overflow=c.overflow[0],p.overflowX=c.overflow[1],p.overflowY=c.overflow[2]}));for(d in b)if(e=b[d],ac.exec(e)){if(delete b[d],f=f||"toggle"===e,e===(q?"hide":"show")){if("show"!==e||!r||void 0===r[d])continue;q=!0}o[d]=r&&r[d]||m.style(a,d)}else j=void 0;if(m.isEmptyObject(o))"inline"===("none"===j?Fb(a.nodeName):j)&&(p.display=j);else{r?"hidden"in r&&(q=r.hidden):r=m._data(a,"fxshow",{}),f&&(r.hidden=!q),q?m(a).show():n.done(function(){m(a).hide()}),n.done(function(){var b;m._removeData(a,"fxshow");for(b in o)m.style(a,b,o[b])});for(d in o)g=hc(q?r[d]:0,d,n),d in r||(r[d]=g.start,q&&(g.end=g.start,g.start="width"===d||"height"===d?1:0))}}function jc(a,b){var c,d,e,f,g;for(c in a)if(d=m.camelCase(c),e=b[d],f=a[c],m.isArray(f)&&(e=f[1],f=a[c]=f[0]),c!==d&&(a[d]=f,delete a[c]),g=m.cssHooks[d],g&&"expand"in g){f=g.expand(f),delete a[d];for(c in f)c in a||(a[c]=f[c],b[c]=e)}else b[d]=e}function kc(a,b,c){var d,e,f=0,g=dc.length,h=m.Deferred().always(function(){delete i.elem}),i=function(){if(e)return!1;for(var b=$b||fc(),c=Math.max(0,j.startTime+j.duration-b),d=c/j.duration||0,f=1-d,g=0,i=j.tweens.length;i>g;g++)j.tweens[g].run(f);return h.notifyWith(a,[j,f,c]),1>f&&i?c:(h.resolveWith(a,[j]),!1)},j=h.promise({elem:a,props:m.extend({},b),opts:m.extend(!0,{specialEasing:{}},c),originalProperties:b,originalOptions:c,startTime:$b||fc(),duration:c.duration,tweens:[],createTween:function(b,c){var d=m.Tween(a,j.opts,b,c,j.opts.specialEasing[b]||j.opts.easing);return j.tweens.push(d),d},stop:function(b){var c=0,d=b?j.tweens.length:0;if(e)return this;for(e=!0;d>c;c++)j.tweens[c].run(1);return b?h.resolveWith(a,[j,b]):h.rejectWith(a,[j,b]),this}}),k=j.props;for(jc(k,j.opts.specialEasing);g>f;f++)if(d=dc[f].call(j,a,k,j.opts))return d;return m.map(k,hc,j),m.isFunction(j.opts.start)&&j.opts.start.call(a,j),m.fx.timer(m.extend(i,{elem:a,anim:j,queue:j.opts.queue})),j.progress(j.opts.progress).done(j.opts.done,j.opts.complete).fail(j.opts.fail).always(j.opts.always)}m.Animation=m.extend(kc,{tweener:function(a,b){m.isFunction(a)?(b=a,a=["*"]):a=a.split(" ");for(var c,d=0,e=a.length;e>d;d++)c=a[d],ec[c]=ec[c]||[],ec[c].unshift(b)},prefilter:function(a,b){b?dc.unshift(a):dc.push(a)}}),m.speed=function(a,b,c){var d=a&&"object"==typeof a?m.extend({},a):{complete:c||!c&&b||m.isFunction(a)&&a,duration:a,easing:c&&b||b&&!m.isFunction(b)&&b};return d.duration=m.fx.off?0:"number"==typeof d.duration?d.duration:d.duration in m.fx.speeds?m.fx.speeds[d.duration]:m.fx.speeds._default,(null==d.queue||d.queue===!0)&&(d.queue="fx"),d.old=d.complete,d.complete=function(){m.isFunction(d.old)&&d.old.call(this),d.queue&&m.dequeue(this,d.queue)},d},m.fn.extend({fadeTo:function(a,b,c,d){return this.filter(U).css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){var e=m.isEmptyObject(a),f=m.speed(b,c,d),g=function(){var b=kc(this,m.extend({},a),f);(e||m._data(this,"finish"))&&b.stop(!0)};return g.finish=g,e||f.queue===!1?this.each(g):this.queue(f.queue,g)},stop:function(a,b,c){var d=function(a){var b=a.stop;delete a.stop,b(c)};return"string"!=typeof a&&(c=b,b=a,a=void 0),b&&a!==!1&&this.queue(a||"fx",[]),this.each(function(){var b=!0,e=null!=a&&a+"queueHooks",f=m.timers,g=m._data(this);if(e)g[e]&&g[e].stop&&d(g[e]);else for(e in g)g[e]&&g[e].stop&&cc.test(e)&&d(g[e]);for(e=f.length;e--;)f[e].elem!==this||null!=a&&f[e].queue!==a||(f[e].anim.stop(c),b=!1,f.splice(e,1));(b||!c)&&m.dequeue(this,a)})},finish:function(a){return a!==!1&&(a=a||"fx"),this.each(function(){var b,c=m._data(this),d=c[a+"queue"],e=c[a+"queueHooks"],f=m.timers,g=d?d.length:0;for(c.finish=!0,m.queue(this,a,[]),e&&e.stop&&e.stop.call(this,!0),b=f.length;b--;)f[b].elem===this&&f[b].queue===a&&(f[b].anim.stop(!0),f.splice(b,1));for(b=0;g>b;b++)d[b]&&d[b].finish&&d[b].finish.call(this);delete c.finish})}}),m.each(["toggle","show","hide"],function(a,b){var c=m.fn[b];m.fn[b]=function(a,d,e){return null==a||"boolean"==typeof a?c.apply(this,arguments):this.animate(gc(b,!0),a,d,e)}}),m.each({slideDown:gc("show"),slideUp:gc("hide"),slideToggle:gc("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){m.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),m.timers=[],m.fx.tick=function(){var a,b=m.timers,c=0;for($b=m.now();c<b.length;c++)a=b[c],a()||b[c]!==a||b.splice(c--,1);b.length||m.fx.stop(),$b=void 0},m.fx.timer=function(a){m.timers.push(a),a()?m.fx.start():m.timers.pop()},m.fx.interval=13,m.fx.start=function(){_b||(_b=setInterval(m.fx.tick,m.fx.interval))},m.fx.stop=function(){clearInterval(_b),_b=null},m.fx.speeds={slow:600,fast:200,_default:400},m.fn.delay=function(a,b){return a=m.fx?m.fx.speeds[a]||a:a,b=b||"fx",this.queue(b,function(b,c){var d=setTimeout(b,a);c.stop=function(){clearTimeout(d)}})},function(){var a,b,c,d,e;b=y.createElement("div"),b.setAttribute("className","t"),b.innerHTML=" <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",d=b.getElementsByTagName("a")[0],c=y.createElement("select"),e=c.appendChild(y.createElement("option")),a=b.getElementsByTagName("input")[0],d.style.cssText="top:1px",k.getSetAttribute="t"!==b.className,k.style=/top/.test(d.getAttribute("style")),k.hrefNormalized="/a"===d.getAttribute("href"),k.checkOn=!!a.value,k.optSelected=e.selected,k.enctype=!!y.createElement("form").enctype,c.disabled=!0,k.optDisabled=!e.disabled,a=y.createElement("input"),a.setAttribute("value",""),k.input=""===a.getAttribute("value"),a.value="t",a.setAttribute("type","radio"),k.radioValue="t"===a.value}();var lc=/\r/g;m.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=m.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,m(this).val()):a,null==e?e="":"number"==typeof e?e+="":m.isArray(e)&&(e=m.map(e,function(a){return null==a?"":a+""})),b=m.valHooks[this.type]||m.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=m.valHooks[e.type]||m.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(lc,""):null==c?"":c)}}}),m.extend({valHooks:{option:{get:function(a){var b=m.find.attr(a,"value");return null!=b?b:m.trim(m.text(a))}},select:{get:function(a){for(var b,c,d=a.options,e=a.selectedIndex,f="select-one"===a.type||0>e,g=f?null:[],h=f?e+1:d.length,i=0>e?h:f?e:0;h>i;i++)if(c=d[i],!(!c.selected&&i!==e||(k.optDisabled?c.disabled:null!==c.getAttribute("disabled"))||c.parentNode.disabled&&m.nodeName(c.parentNode,"optgroup"))){if(b=m(c).val(),f)return b;g.push(b)}return g},set:function(a,b){var c,d,e=a.options,f=m.makeArray(b),g=e.length;while(g--)if(d=e[g],m.inArray(m.valHooks.option.get(d),f)>=0)try{d.selected=c=!0}catch(h){d.scrollHeight}else d.selected=!1;return c||(a.selectedIndex=-1),e}}}}),m.each(["radio","checkbox"],function(){m.valHooks[this]={set:function(a,b){return m.isArray(b)?a.checked=m.inArray(m(a).val(),b)>=0:void 0}},k.checkOn||(m.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})});var mc,nc,oc=m.expr.attrHandle,pc=/^(?:checked|selected)$/i,qc=k.getSetAttribute,rc=k.input;m.fn.extend({attr:function(a,b){return V(this,m.attr,a,b,arguments.length>1)},removeAttr:function(a){return this.each(function(){m.removeAttr(this,a)})}}),m.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(a&&3!==f&&8!==f&&2!==f)return typeof a.getAttribute===K?m.prop(a,b,c):(1===f&&m.isXMLDoc(a)||(b=b.toLowerCase(),d=m.attrHooks[b]||(m.expr.match.bool.test(b)?nc:mc)),void 0===c?d&&"get"in d&&null!==(e=d.get(a,b))?e:(e=m.find.attr(a,b),null==e?void 0:e):null!==c?d&&"set"in d&&void 0!==(e=d.set(a,c,b))?e:(a.setAttribute(b,c+""),c):void m.removeAttr(a,b))},removeAttr:function(a,b){var c,d,e=0,f=b&&b.match(E);if(f&&1===a.nodeType)while(c=f[e++])d=m.propFix[c]||c,m.expr.match.bool.test(c)?rc&&qc||!pc.test(c)?a[d]=!1:a[m.camelCase("default-"+c)]=a[d]=!1:m.attr(a,c,""),a.removeAttribute(qc?c:d)},attrHooks:{type:{set:function(a,b){if(!k.radioValue&&"radio"===b&&m.nodeName(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}}}),nc={set:function(a,b,c){return b===!1?m.removeAttr(a,c):rc&&qc||!pc.test(c)?a.setAttribute(!qc&&m.propFix[c]||c,c):a[m.camelCase("default-"+c)]=a[c]=!0,c}},m.each(m.expr.match.bool.source.match(/\w+/g),function(a,b){var c=oc[b]||m.find.attr;oc[b]=rc&&qc||!pc.test(b)?function(a,b,d){var e,f;return d||(f=oc[b],oc[b]=e,e=null!=c(a,b,d)?b.toLowerCase():null,oc[b]=f),e}:function(a,b,c){return c?void 0:a[m.camelCase("default-"+b)]?b.toLowerCase():null}}),rc&&qc||(m.attrHooks.value={set:function(a,b,c){return m.nodeName(a,"input")?void(a.defaultValue=b):mc&&mc.set(a,b,c)}}),qc||(mc={set:function(a,b,c){var d=a.getAttributeNode(c);return d||a.setAttributeNode(d=a.ownerDocument.createAttribute(c)),d.value=b+="","value"===c||b===a.getAttribute(c)?b:void 0}},oc.id=oc.name=oc.coords=function(a,b,c){var d;return c?void 0:(d=a.getAttributeNode(b))&&""!==d.value?d.value:null},m.valHooks.button={get:function(a,b){var c=a.getAttributeNode(b);return c&&c.specified?c.value:void 0},set:mc.set},m.attrHooks.contenteditable={set:function(a,b,c){mc.set(a,""===b?!1:b,c)}},m.each(["width","height"],function(a,b){m.attrHooks[b]={set:function(a,c){return""===c?(a.setAttribute(b,"auto"),c):void 0}}})),k.style||(m.attrHooks.style={get:function(a){return a.style.cssText||void 0},set:function(a,b){return a.style.cssText=b+""}});var sc=/^(?:input|select|textarea|button|object)$/i,tc=/^(?:a|area)$/i;m.fn.extend({prop:function(a,b){return V(this,m.prop,a,b,arguments.length>1)},removeProp:function(a){return a=m.propFix[a]||a,this.each(function(){try{this[a]=void 0,delete this[a]}catch(b){}})}}),m.extend({propFix:{"for":"htmlFor","class":"className"},prop:function(a,b,c){var d,e,f,g=a.nodeType;if(a&&3!==g&&8!==g&&2!==g)return f=1!==g||!m.isXMLDoc(a),f&&(b=m.propFix[b]||b,e=m.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){var b=m.find.attr(a,"tabindex");return b?parseInt(b,10):sc.test(a.nodeName)||tc.test(a.nodeName)&&a.href?0:-1}}}}),k.hrefNormalized||m.each(["href","src"],function(a,b){m.propHooks[b]={get:function(a){return a.getAttribute(b,4)}}}),k.optSelected||(m.propHooks.selected={get:function(a){var b=a.parentNode;return b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex),null}}),m.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){m.propFix[this.toLowerCase()]=this}),k.enctype||(m.propFix.enctype="encoding");var uc=/[\t\r\n\f]/g;m.fn.extend({addClass:function(a){var b,c,d,e,f,g,h=0,i=this.length,j="string"==typeof a&&a;if(m.isFunction(a))return this.each(function(b){m(this).addClass(a.call(this,b,this.className))});if(j)for(b=(a||"").match(E)||[];i>h;h++)if(c=this[h],d=1===c.nodeType&&(c.className?(" "+c.className+" ").replace(uc," "):" ")){f=0;while(e=b[f++])d.indexOf(" "+e+" ")<0&&(d+=e+" ");g=m.trim(d),c.className!==g&&(c.className=g)}return this},removeClass:function(a){var b,c,d,e,f,g,h=0,i=this.length,j=0===arguments.length||"string"==typeof a&&a;if(m.isFunction(a))return this.each(function(b){m(this).removeClass(a.call(this,b,this.className))});if(j)for(b=(a||"").match(E)||[];i>h;h++)if(c=this[h],d=1===c.nodeType&&(c.className?(" "+c.className+" ").replace(uc," "):"")){f=0;while(e=b[f++])while(d.indexOf(" "+e+" ")>=0)d=d.replace(" "+e+" "," ");g=a?m.trim(d):"",c.className!==g&&(c.className=g)}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):this.each(m.isFunction(a)?function(c){m(this).toggleClass(a.call(this,c,this.className,b),b)}:function(){if("string"===c){var b,d=0,e=m(this),f=a.match(E)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else(c===K||"boolean"===c)&&(this.className&&m._data(this,"__className__",this.className),this.className=this.className||a===!1?"":m._data(this,"__className__")||"")})},hasClass:function(a){for(var b=" "+a+" ",c=0,d=this.length;d>c;c++)if(1===this[c].nodeType&&(" "+this[c].className+" ").replace(uc," ").indexOf(b)>=0)return!0;return!1}}),m.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(a,b){m.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),m.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)},bind:function(a,b,c){return this.on(a,null,b,c)},unbind:function(a,b){return this.off(a,null,b)},delegate:function(a,b,c,d){return this.on(b,a,c,d)},undelegate:function(a,b,c){return 1===arguments.length?this.off(a,"**"):this.off(b,a||"**",c)}});var vc=m.now(),wc=/\?/,xc=/(,)|(\[|{)|(}|])|"(?:[^"\\\r\n]|\\["\\\/bfnrt]|\\u[\da-fA-F]{4})*"\s*:?|true|false|null|-?(?!0\d)\d+(?:\.\d+|)(?:[eE][+-]?\d+|)/g;m.parseJSON=function(b){if(a.JSON&&a.JSON.parse)return a.JSON.parse(b+"");var c,d=null,e=m.trim(b+"");return e&&!m.trim(e.replace(xc,function(a,b,e,f){return c&&b&&(d=0),0===d?a:(c=e||b,d+=!f-!e,"")}))?Function("return "+e)():m.error("Invalid JSON: "+b)},m.parseXML=function(b){var c,d;if(!b||"string"!=typeof b)return null;try{a.DOMParser?(d=new DOMParser,c=d.parseFromString(b,"text/xml")):(c=new ActiveXObject("Microsoft.XMLDOM"),c.async="false",c.loadXML(b))}catch(e){c=void 0}return c&&c.documentElement&&!c.getElementsByTagName("parsererror").length||m.error("Invalid XML: "+b),c};var yc,zc,Ac=/#.*$/,Bc=/([?&])_=[^&]*/,Cc=/^(.*?):[ \t]*([^\r\n]*)\r?$/gm,Dc=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Ec=/^(?:GET|HEAD)$/,Fc=/^\/\//,Gc=/^([\w.+-]+:)(?:\/\/(?:[^\/?#]*@|)([^\/?#:]*)(?::(\d+)|)|)/,Hc={},Ic={},Jc="*/".concat("*");try{zc=location.href}catch(Kc){zc=y.createElement("a"),zc.href="",zc=zc.href}yc=Gc.exec(zc.toLowerCase())||[];function Lc(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(E)||[];if(m.isFunction(c))while(d=f[e++])"+"===d.charAt(0)?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function Mc(a,b,c,d){var e={},f=a===Ic;function g(h){var i;return e[h]=!0,m.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function Nc(a,b){var c,d,e=m.ajaxSettings.flatOptions||{};for(d in b)void 0!==b[d]&&((e[d]?a:c||(c={}))[d]=b[d]);return c&&m.extend(!0,a,c),a}function Oc(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===e&&(e=a.mimeType||b.getResponseHeader("Content-Type"));if(e)for(g in h)if(h[g]&&h[g].test(e)){i.unshift(g);break}if(i[0]in c)f=i[0];else{for(g in c){if(!i[0]||a.converters[g+" "+i[0]]){f=g;break}d||(d=g)}f=f||d}return f?(f!==i[0]&&i.unshift(f),c[f]):void 0}function Pc(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}m.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:zc,type:"GET",isLocal:Dc.test(yc[1]),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Jc,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":m.parseJSON,"text xml":m.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?Nc(Nc(a,m.ajaxSettings),b):Nc(m.ajaxSettings,a)},ajaxPrefilter:Lc(Hc),ajaxTransport:Lc(Ic),ajax:function(a,b){"object"==typeof a&&(b=a,a=void 0),b=b||{};var c,d,e,f,g,h,i,j,k=m.ajaxSetup({},b),l=k.context||k,n=k.context&&(l.nodeType||l.jquery)?m(l):m.event,o=m.Deferred(),p=m.Callbacks("once memory"),q=k.statusCode||{},r={},s={},t=0,u="canceled",v={readyState:0,getResponseHeader:function(a){var b;if(2===t){if(!j){j={};while(b=Cc.exec(f))j[b[1].toLowerCase()]=b[2]}b=j[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return 2===t?f:null},setRequestHeader:function(a,b){var c=a.toLowerCase();return t||(a=s[c]=s[c]||a,r[a]=b),this},overrideMimeType:function(a){return t||(k.mimeType=a),this},statusCode:function(a){var b;if(a)if(2>t)for(b in a)q[b]=[q[b],a[b]];else v.always(a[v.status]);return this},abort:function(a){var b=a||u;return i&&i.abort(b),x(0,b),this}};if(o.promise(v).complete=p.add,v.success=v.done,v.error=v.fail,k.url=((a||k.url||zc)+"").replace(Ac,"").replace(Fc,yc[1]+"//"),k.type=b.method||b.type||k.method||k.type,k.dataTypes=m.trim(k.dataType||"*").toLowerCase().match(E)||[""],null==k.crossDomain&&(c=Gc.exec(k.url.toLowerCase()),k.crossDomain=!(!c||c[1]===yc[1]&&c[2]===yc[2]&&(c[3]||("http:"===c[1]?"80":"443"))===(yc[3]||("http:"===yc[1]?"80":"443")))),k.data&&k.processData&&"string"!=typeof k.data&&(k.data=m.param(k.data,k.traditional)),Mc(Hc,k,b,v),2===t)return v;h=k.global,h&&0===m.active++&&m.event.trigger("ajaxStart"),k.type=k.type.toUpperCase(),k.hasContent=!Ec.test(k.type),e=k.url,k.hasContent||(k.data&&(e=k.url+=(wc.test(e)?"&":"?")+k.data,delete k.data),k.cache===!1&&(k.url=Bc.test(e)?e.replace(Bc,"$1_="+vc++):e+(wc.test(e)?"&":"?")+"_="+vc++)),k.ifModified&&(m.lastModified[e]&&v.setRequestHeader("If-Modified-Since",m.lastModified[e]),m.etag[e]&&v.setRequestHeader("If-None-Match",m.etag[e])),(k.data&&k.hasContent&&k.contentType!==!1||b.contentType)&&v.setRequestHeader("Content-Type",k.contentType),v.setRequestHeader("Accept",k.dataTypes[0]&&k.accepts[k.dataTypes[0]]?k.accepts[k.dataTypes[0]]+("*"!==k.dataTypes[0]?", "+Jc+"; q=0.01":""):k.accepts["*"]);for(d in k.headers)v.setRequestHeader(d,k.headers[d]);if(k.beforeSend&&(k.beforeSend.call(l,v,k)===!1||2===t))return v.abort();u="abort";for(d in{success:1,error:1,complete:1})v[d](k[d]);if(i=Mc(Ic,k,b,v)){v.readyState=1,h&&n.trigger("ajaxSend",[v,k]),k.async&&k.timeout>0&&(g=setTimeout(function(){v.abort("timeout")},k.timeout));try{t=1,i.send(r,x)}catch(w){if(!(2>t))throw w;x(-1,w)}}else x(-1,"No Transport");function x(a,b,c,d){var j,r,s,u,w,x=b;2!==t&&(t=2,g&&clearTimeout(g),i=void 0,f=d||"",v.readyState=a>0?4:0,j=a>=200&&300>a||304===a,c&&(u=Oc(k,v,c)),u=Pc(k,u,v,j),j?(k.ifModified&&(w=v.getResponseHeader("Last-Modified"),w&&(m.lastModified[e]=w),w=v.getResponseHeader("etag"),w&&(m.etag[e]=w)),204===a||"HEAD"===k.type?x="nocontent":304===a?x="notmodified":(x=u.state,r=u.data,s=u.error,j=!s)):(s=x,(a||!x)&&(x="error",0>a&&(a=0))),v.status=a,v.statusText=(b||x)+"",j?o.resolveWith(l,[r,x,v]):o.rejectWith(l,[v,x,s]),v.statusCode(q),q=void 0,h&&n.trigger(j?"ajaxSuccess":"ajaxError",[v,k,j?r:s]),p.fireWith(l,[v,x]),h&&(n.trigger("ajaxComplete",[v,k]),--m.active||m.event.trigger("ajaxStop")))}return v},getJSON:function(a,b,c){return m.get(a,b,c,"json")},getScript:function(a,b){return m.get(a,void 0,b,"script")}}),m.each(["get","post"],function(a,b){m[b]=function(a,c,d,e){return m.isFunction(c)&&(e=e||d,d=c,c=void 0),m.ajax({url:a,type:b,dataType:e,data:c,success:d})}}),m.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(a,b){m.fn[b]=function(a){return this.on(b,a)}}),m._evalUrl=function(a){return m.ajax({url:a,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0})},m.fn.extend({wrapAll:function(a){if(m.isFunction(a))return this.each(function(b){m(this).wrapAll(a.call(this,b))});if(this[0]){var b=m(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&1===a.firstChild.nodeType)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){return this.each(m.isFunction(a)?function(b){m(this).wrapInner(a.call(this,b))}:function(){var b=m(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=m.isFunction(a);return this.each(function(c){m(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){m.nodeName(this,"body")||m(this).replaceWith(this.childNodes)}).end()}}),m.expr.filters.hidden=function(a){return a.offsetWidth<=0&&a.offsetHeight<=0||!k.reliableHiddenOffsets()&&"none"===(a.style&&a.style.display||m.css(a,"display"))},m.expr.filters.visible=function(a){return!m.expr.filters.hidden(a)};var Qc=/%20/g,Rc=/\[\]$/,Sc=/\r?\n/g,Tc=/^(?:submit|button|image|reset|file)$/i,Uc=/^(?:input|select|textarea|keygen)/i;function Vc(a,b,c,d){var e;if(m.isArray(b))m.each(b,function(b,e){c||Rc.test(a)?d(a,e):Vc(a+"["+("object"==typeof e?b:"")+"]",e,c,d)});else if(c||"object"!==m.type(b))d(a,b);else for(e in b)Vc(a+"["+e+"]",b[e],c,d)}m.param=function(a,b){var c,d=[],e=function(a,b){b=m.isFunction(b)?b():null==b?"":b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};if(void 0===b&&(b=m.ajaxSettings&&m.ajaxSettings.traditional),m.isArray(a)||a.jquery&&!m.isPlainObject(a))m.each(a,function(){e(this.name,this.value)});else for(c in a)Vc(c,a[c],b,e);return d.join("&").replace(Qc,"+")},m.fn.extend({serialize:function(){return m.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=m.prop(this,"elements");return a?m.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!m(this).is(":disabled")&&Uc.test(this.nodeName)&&!Tc.test(a)&&(this.checked||!W.test(a))}).map(function(a,b){var c=m(this).val();return null==c?null:m.isArray(c)?m.map(c,function(a){return{name:b.name,value:a.replace(Sc,"\r\n")}}):{name:b.name,value:c.replace(Sc,"\r\n")}}).get()}}),m.ajaxSettings.xhr=void 0!==a.ActiveXObject?function(){return!this.isLocal&&/^(get|post|head|put|delete|options)$/i.test(this.type)&&Zc()||$c()}:Zc;var Wc=0,Xc={},Yc=m.ajaxSettings.xhr();a.ActiveXObject&&m(a).on("unload",function(){for(var a in Xc)Xc[a](void 0,!0)}),k.cors=!!Yc&&"withCredentials"in Yc,Yc=k.ajax=!!Yc,Yc&&m.ajaxTransport(function(a){if(!a.crossDomain||k.cors){var b;return{send:function(c,d){var e,f=a.xhr(),g=++Wc;if(f.open(a.type,a.url,a.async,a.username,a.password),a.xhrFields)for(e in a.xhrFields)f[e]=a.xhrFields[e];a.mimeType&&f.overrideMimeType&&f.overrideMimeType(a.mimeType),a.crossDomain||c["X-Requested-With"]||(c["X-Requested-With"]="XMLHttpRequest");for(e in c)void 0!==c[e]&&f.setRequestHeader(e,c[e]+"");f.send(a.hasContent&&a.data||null),b=function(c,e){var h,i,j;if(b&&(e||4===f.readyState))if(delete Xc[g],b=void 0,f.onreadystatechange=m.noop,e)4!==f.readyState&&f.abort();else{j={},h=f.status,"string"==typeof f.responseText&&(j.text=f.responseText);try{i=f.statusText}catch(k){i=""}h||!a.isLocal||a.crossDomain?1223===h&&(h=204):h=j.text?200:404}j&&d(h,i,j,f.getAllResponseHeaders())},a.async?4===f.readyState?setTimeout(b):f.onreadystatechange=Xc[g]=b:b()},abort:function(){b&&b(void 0,!0)}}}});function Zc(){try{return new a.XMLHttpRequest}catch(b){}}function $c(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}m.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/(?:java|ecma)script/},converters:{"text script":function(a){return m.globalEval(a),a}}}),m.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),m.ajaxTransport("script",function(a){if(a.crossDomain){var b,c=y.head||m("head")[0]||y.documentElement;return{send:function(d,e){b=y.createElement("script"),b.async=!0,a.scriptCharset&&(b.charset=a.scriptCharset),b.src=a.url,b.onload=b.onreadystatechange=function(a,c){(c||!b.readyState||/loaded|complete/.test(b.readyState))&&(b.onload=b.onreadystatechange=null,b.parentNode&&b.parentNode.removeChild(b),b=null,c||e(200,"success"))},c.insertBefore(b,c.firstChild)},abort:function(){b&&b.onload(void 0,!0)}}}});var _c=[],ad=/(=)\?(?=&|$)|\?\?/;m.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var a=_c.pop()||m.expando+"_"+vc++;return this[a]=!0,a}}),m.ajaxPrefilter("json jsonp",function(b,c,d){var e,f,g,h=b.jsonp!==!1&&(ad.test(b.url)?"url":"string"==typeof b.data&&!(b.contentType||"").indexOf("application/x-www-form-urlencoded")&&ad.test(b.data)&&"data");return h||"jsonp"===b.dataTypes[0]?(e=b.jsonpCallback=m.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,h?b[h]=b[h].replace(ad,"$1"+e):b.jsonp!==!1&&(b.url+=(wc.test(b.url)?"&":"?")+b.jsonp+"="+e),b.converters["script json"]=function(){return g||m.error(e+" was not called"),g[0]},b.dataTypes[0]="json",f=a[e],a[e]=function(){g=arguments},d.always(function(){a[e]=f,b[e]&&(b.jsonpCallback=c.jsonpCallback,_c.push(e)),g&&m.isFunction(f)&&f(g[0]),g=f=void 0}),"script"):void 0}),m.parseHTML=function(a,b,c){if(!a||"string"!=typeof a)return null;"boolean"==typeof b&&(c=b,b=!1),b=b||y;var d=u.exec(a),e=!c&&[];return d?[b.createElement(d[1])]:(d=m.buildFragment([a],b,e),e&&e.length&&m(e).remove(),m.merge([],d.childNodes))};var bd=m.fn.load;m.fn.load=function(a,b,c){if("string"!=typeof a&&bd)return bd.apply(this,arguments);var d,e,f,g=this,h=a.indexOf(" ");return h>=0&&(d=m.trim(a.slice(h,a.length)),a=a.slice(0,h)),m.isFunction(b)?(c=b,b=void 0):b&&"object"==typeof b&&(f="POST"),g.length>0&&m.ajax({url:a,type:f,dataType:"html",data:b}).done(function(a){e=arguments,g.html(d?m("<div>").append(m.parseHTML(a)).find(d):a)}).complete(c&&function(a,b){g.each(c,e||[a.responseText,b,a])}),this},m.expr.filters.animated=function(a){return m.grep(m.timers,function(b){return a===b.elem}).length};var cd=a.document.documentElement;function dd(a){return m.isWindow(a)?a:9===a.nodeType?a.defaultView||a.parentWindow:!1}m.offset={setOffset:function(a,b,c){var d,e,f,g,h,i,j,k=m.css(a,"position"),l=m(a),n={};"static"===k&&(a.style.position="relative"),h=l.offset(),f=m.css(a,"top"),i=m.css(a,"left"),j=("absolute"===k||"fixed"===k)&&m.inArray("auto",[f,i])>-1,j?(d=l.position(),g=d.top,e=d.left):(g=parseFloat(f)||0,e=parseFloat(i)||0),m.isFunction(b)&&(b=b.call(a,c,h)),null!=b.top&&(n.top=b.top-h.top+g),null!=b.left&&(n.left=b.left-h.left+e),"using"in b?b.using.call(a,n):l.css(n)}},m.fn.extend({offset:function(a){if(arguments.length)return void 0===a?this:this.each(function(b){m.offset.setOffset(this,a,b)});var b,c,d={top:0,left:0},e=this[0],f=e&&e.ownerDocument;if(f)return b=f.documentElement,m.contains(b,e)?(typeof e.getBoundingClientRect!==K&&(d=e.getBoundingClientRect()),c=dd(f),{top:d.top+(c.pageYOffset||b.scrollTop)-(b.clientTop||0),left:d.left+(c.pageXOffset||b.scrollLeft)-(b.clientLeft||0)}):d},position:function(){if(this[0]){var a,b,c={top:0,left:0},d=this[0];return"fixed"===m.css(d,"position")?b=d.getBoundingClientRect():(a=this.offsetParent(),b=this.offset(),m.nodeName(a[0],"html")||(c=a.offset()),c.top+=m.css(a[0],"borderTopWidth",!0),c.left+=m.css(a[0],"borderLeftWidth",!0)),{top:b.top-c.top-m.css(d,"marginTop",!0),left:b.left-c.left-m.css(d,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||cd;while(a&&!m.nodeName(a,"html")&&"static"===m.css(a,"position"))a=a.offsetParent;return a||cd})}}),m.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(a,b){var c=/Y/.test(b);m.fn[a]=function(d){return V(this,function(a,d,e){var f=dd(a);return void 0===e?f?b in f?f[b]:f.document.documentElement[d]:a[d]:void(f?f.scrollTo(c?m(f).scrollLeft():e,c?e:m(f).scrollTop()):a[d]=e)},a,d,arguments.length,null)}}),m.each(["top","left"],function(a,b){m.cssHooks[b]=Lb(k.pixelPosition,function(a,c){return c?(c=Jb(a,b),Hb.test(c)?m(a).position()[b]+"px":c):void 0})}),m.each({Height:"height",Width:"width"},function(a,b){m.each({padding:"inner"+a,content:b,"":"outer"+a},function(c,d){m.fn[d]=function(d,e){var f=arguments.length&&(c||"boolean"!=typeof d),g=c||(d===!0||e===!0?"margin":"border");return V(this,function(b,c,d){var e;return m.isWindow(b)?b.document.documentElement["client"+a]:9===b.nodeType?(e=b.documentElement,Math.max(b.body["scroll"+a],e["scroll"+a],b.body["offset"+a],e["offset"+a],e["client"+a])):void 0===d?m.css(b,c,g):m.style(b,c,d,g)},b,f?d:void 0,f,null)}})}),m.fn.size=function(){return this.length},m.fn.andSelf=m.fn.addBack,"function"==typeof define&&define.amd&&define("jquery",[],function(){return m});var ed=a.jQuery,fd=a.$;return m.noConflict=function(b){return a.$===m&&(a.$=fd),b&&a.jQuery===m&&(a.jQuery=ed),m},typeof b===K&&(a.jQuery=a.$=m),m});
diff --git a/devtools/client/inspector/markup/test/lib_jquery_1.2_min.js b/devtools/client/inspector/markup/test/lib_jquery_1.2_min.js
new file mode 100644
index 000000000..f10d4943f
--- /dev/null
+++ b/devtools/client/inspector/markup/test/lib_jquery_1.2_min.js
@@ -0,0 +1,32 @@
+/*
+ * jQuery 1.2 - New Wave Javascript
+ *
+ * Copyright (c) 2007 John Resig (jquery.com)
+ * Dual licensed under the MIT (MIT-LICENSE.txt)
+ * and GPL (GPL-LICENSE.txt) licenses.
+ *
+ * $Date: 2007-09-10 15:45:49 -0400 (Mon, 10 Sep 2007) $
+ * $Rev: 3219 $
+ */
+(function(){if(typeof jQuery!="undefined")var _jQuery=jQuery;var jQuery=window.jQuery=function(a,c){if(window==this||!this.init)return new jQuery(a,c);return this.init(a,c);};if(typeof $!="undefined")var _$=$;window.$=jQuery;var quickExpr=/^[^<]*(<(.|\s)+>)[^>]*$|^#(\w+)$/;jQuery.fn=jQuery.prototype={init:function(a,c){a=a||document;if(typeof a=="string"){var m=quickExpr.exec(a);if(m&&(m[1]||!c)){if(m[1])a=jQuery.clean([m[1]],c);else{var tmp=document.getElementById(m[3]);if(tmp)if(tmp.id!=m[3])return jQuery().find(a);else{this[0]=tmp;this.length=1;return this;}else
+a=[];}}else
+return new jQuery(c).find(a);}else if(jQuery.isFunction(a))return new jQuery(document)[jQuery.fn.ready?"ready":"load"](a);return this.setArray(a.constructor==Array&&a||(a.jquery||a.length&&a!=window&&!a.nodeType&&a[0]!=undefined&&a[0].nodeType)&&jQuery.makeArray(a)||[a]);},jquery:"1.2",size:function(){return this.length;},length:0,get:function(num){return num==undefined?jQuery.makeArray(this):this[num];},pushStack:function(a){var ret=jQuery(a);ret.prevObject=this;return ret;},setArray:function(a){this.length=0;Array.prototype.push.apply(this,a);return this;},each:function(fn,args){return jQuery.each(this,fn,args);},index:function(obj){var pos=-1;this.each(function(i){if(this==obj)pos=i;});return pos;},attr:function(key,value,type){var obj=key;if(key.constructor==String)if(value==undefined)return this.length&&jQuery[type||"attr"](this[0],key)||undefined;else{obj={};obj[key]=value;}return this.each(function(index){for(var prop in obj)jQuery.attr(type?this.style:this,prop,jQuery.prop(this,obj[prop],type,index,prop));});},css:function(key,value){return this.attr(key,value,"curCSS");},text:function(e){if(typeof e!="object"&&e!=null)return this.empty().append(document.createTextNode(e));var t="";jQuery.each(e||this,function(){jQuery.each(this.childNodes,function(){if(this.nodeType!=8)t+=this.nodeType!=1?this.nodeValue:jQuery.fn.text([this]);});});return t;},wrapAll:function(html){if(this[0])jQuery(html,this[0].ownerDocument).clone().insertBefore(this[0]).map(function(){var elem=this;while(elem.firstChild)elem=elem.firstChild;return elem;}).append(this);return this;},wrapInner:function(html){return this.each(function(){jQuery(this).contents().wrapAll(html);});},wrap:function(html){return this.each(function(){jQuery(this).wrapAll(html);});},append:function(){return this.domManip(arguments,true,1,function(a){this.appendChild(a);});},prepend:function(){return this.domManip(arguments,true,-1,function(a){this.insertBefore(a,this.firstChild);});},before:function(){return this.domManip(arguments,false,1,function(a){this.parentNode.insertBefore(a,this);});},after:function(){return this.domManip(arguments,false,-1,function(a){this.parentNode.insertBefore(a,this.nextSibling);});},end:function(){return this.prevObject||jQuery([]);},find:function(t){var data=jQuery.map(this,function(a){return jQuery.find(t,a);});return this.pushStack(/[^+>] [^+>]/.test(t)||t.indexOf("..")>-1?jQuery.unique(data):data);},clone:function(events){var ret=this.map(function(){return this.outerHTML?jQuery(this.outerHTML)[0]:this.cloneNode(true);});if(events===true){var clone=ret.find("*").andSelf();this.find("*").andSelf().each(function(i){var events=jQuery.data(this,"events");for(var type in events)for(var handler in events[type])jQuery.event.add(clone[i],type,events[type][handler],events[type][handler].data);});}return ret;},filter:function(t){return this.pushStack(jQuery.isFunction(t)&&jQuery.grep(this,function(el,index){return t.apply(el,[index]);})||jQuery.multiFilter(t,this));},not:function(t){return this.pushStack(t.constructor==String&&jQuery.multiFilter(t,this,true)||jQuery.grep(this,function(a){return(t.constructor==Array||t.jquery)?jQuery.inArray(a,t)<0:a!=t;}));},add:function(t){return this.pushStack(jQuery.merge(this.get(),t.constructor==String?jQuery(t).get():t.length!=undefined&&(!t.nodeName||t.nodeName=="FORM")?t:[t]));},is:function(expr){return expr?jQuery.multiFilter(expr,this).length>0:false;},hasClass:function(expr){return this.is("."+expr);},val:function(val){if(val==undefined){if(this.length){var elem=this[0];if(jQuery.nodeName(elem,"select")){var index=elem.selectedIndex,a=[],options=elem.options,one=elem.type=="select-one";if(index<0)return null;for(var i=one?index:0,max=one?index+1:options.length;i<max;i++){var option=options[i];if(option.selected){var val=jQuery.browser.msie&&!option.attributes["value"].specified?option.text:option.value;if(one)return val;a.push(val);}}return a;}else
+return this[0].value.replace(/\r/g,"");}}else
+return this.each(function(){if(val.constructor==Array&&/radio|checkbox/.test(this.type))this.checked=(jQuery.inArray(this.value,val)>=0||jQuery.inArray(this.name,val)>=0);else if(jQuery.nodeName(this,"select")){var tmp=val.constructor==Array?val:[val];jQuery("option",this).each(function(){this.selected=(jQuery.inArray(this.value,tmp)>=0||jQuery.inArray(this.text,tmp)>=0);});if(!tmp.length)this.selectedIndex=-1;}else
+this.value=val;});},html:function(val){return val==undefined?(this.length?this[0].innerHTML:null):this.empty().append(val);},replaceWith:function(val){return this.after(val).remove();},slice:function(){return this.pushStack(Array.prototype.slice.apply(this,arguments));},map:function(fn){return this.pushStack(jQuery.map(this,function(elem,i){return fn.call(elem,i,elem);}));},andSelf:function(){return this.add(this.prevObject);},domManip:function(args,table,dir,fn){var clone=this.length>1,a;return this.each(function(){if(!a){a=jQuery.clean(args,this.ownerDocument);if(dir<0)a.reverse();}var obj=this;if(table&&jQuery.nodeName(this,"table")&&jQuery.nodeName(a[0],"tr"))obj=this.getElementsByTagName("tbody")[0]||this.appendChild(document.createElement("tbody"));jQuery.each(a,function(){if(jQuery.nodeName(this,"script")){if(this.src)jQuery.ajax({url:this.src,async:false,dataType:"script"});else
+jQuery.globalEval(this.text||this.textContent||this.innerHTML||"");}else
+fn.apply(obj,[clone?this.cloneNode(true):this]);});});}};jQuery.extend=jQuery.fn.extend=function(){var target=arguments[0]||{},a=1,al=arguments.length,deep=false;if(target.constructor==Boolean){deep=target;target=arguments[1]||{};}if(al==1){target=this;a=0;}var prop;for(;a<al;a++)if((prop=arguments[a])!=null)for(var i in prop){if(target==prop[i])continue;if(deep&&typeof prop[i]=='object'&&target[i])jQuery.extend(target[i],prop[i]);else if(prop[i]!=undefined)target[i]=prop[i];}return target;};var expando="jQuery"+(new Date()).getTime(),uuid=0,win={};jQuery.extend({noConflict:function(deep){window.$=_$;if(deep)window.jQuery=_jQuery;return jQuery;},isFunction:function(fn){return!!fn&&typeof fn!="string"&&!fn.nodeName&&fn.constructor!=Array&&/function/i.test(fn+"");},isXMLDoc:function(elem){return elem.documentElement&&!elem.body||elem.tagName&&elem.ownerDocument&&!elem.ownerDocument.body;},globalEval:function(data){data=jQuery.trim(data);if(data){if(window.execScript)window.execScript(data);else if(jQuery.browser.safari)window.setTimeout(data,0);else
+eval.call(window,data);}},nodeName:function(elem,name){return elem.nodeName&&elem.nodeName.toUpperCase()==name.toUpperCase();},cache:{},data:function(elem,name,data){elem=elem==window?win:elem;var id=elem[expando];if(!id)id=elem[expando]=++uuid;if(name&&!jQuery.cache[id])jQuery.cache[id]={};if(data!=undefined)jQuery.cache[id][name]=data;return name?jQuery.cache[id][name]:id;},removeData:function(elem,name){elem=elem==window?win:elem;var id=elem[expando];if(name){if(jQuery.cache[id]){delete jQuery.cache[id][name];name="";for(name in jQuery.cache[id])break;if(!name)jQuery.removeData(elem);}}else{try{delete elem[expando];}catch(e){if(elem.removeAttribute)elem.removeAttribute(expando);}delete jQuery.cache[id];}},each:function(obj,fn,args){if(args){if(obj.length==undefined)for(var i in obj)fn.apply(obj[i],args);else
+for(var i=0,ol=obj.length;i<ol;i++)if(fn.apply(obj[i],args)===false)break;}else{if(obj.length==undefined)for(var i in obj)fn.call(obj[i],i,obj[i]);else
+for(var i=0,ol=obj.length,val=obj[0];i<ol&&fn.call(val,i,val)!==false;val=obj[++i]){}}return obj;},prop:function(elem,value,type,index,prop){if(jQuery.isFunction(value))value=value.call(elem,[index]);var exclude=/z-?index|font-?weight|opacity|zoom|line-?height/i;return value&&value.constructor==Number&&type=="curCSS"&&!exclude.test(prop)?value+"px":value;},className:{add:function(elem,c){jQuery.each((c||"").split(/\s+/),function(i,cur){if(!jQuery.className.has(elem.className,cur))elem.className+=(elem.className?" ":"")+cur;});},remove:function(elem,c){elem.className=c!=undefined?jQuery.grep(elem.className.split(/\s+/),function(cur){return!jQuery.className.has(c,cur);}).join(" "):"";},has:function(t,c){return jQuery.inArray(c,(t.className||t).toString().split(/\s+/))>-1;}},swap:function(e,o,f){for(var i in o){e.style["old"+i]=e.style[i];e.style[i]=o[i];}f.apply(e,[]);for(var i in o)e.style[i]=e.style["old"+i];},css:function(e,p){if(p=="height"||p=="width"){var old={},oHeight,oWidth,d=["Top","Bottom","Right","Left"];jQuery.each(d,function(){old["padding"+this]=0;old["border"+this+"Width"]=0;});jQuery.swap(e,old,function(){if(jQuery(e).is(':visible')){oHeight=e.offsetHeight;oWidth=e.offsetWidth;}else{e=jQuery(e.cloneNode(true)).find(":radio").removeAttr("checked").end().css({visibility:"hidden",position:"absolute",display:"block",right:"0",left:"0"}).appendTo(e.parentNode)[0];var parPos=jQuery.css(e.parentNode,"position")||"static";if(parPos=="static")e.parentNode.style.position="relative";oHeight=e.clientHeight;oWidth=e.clientWidth;if(parPos=="static")e.parentNode.style.position="static";e.parentNode.removeChild(e);}});return p=="height"?oHeight:oWidth;}return jQuery.curCSS(e,p);},curCSS:function(elem,prop,force){var ret,stack=[],swap=[];function color(a){if(!jQuery.browser.safari)return false;var ret=document.defaultView.getComputedStyle(a,null);return!ret||ret.getPropertyValue("color")=="";}if(prop=="opacity"&&jQuery.browser.msie){ret=jQuery.attr(elem.style,"opacity");return ret==""?"1":ret;}if(prop.match(/float/i))prop=styleFloat;if(!force&&elem.style[prop])ret=elem.style[prop];else if(document.defaultView&&document.defaultView.getComputedStyle){if(prop.match(/float/i))prop="float";prop=prop.replace(/([A-Z])/g,"-$1").toLowerCase();var cur=document.defaultView.getComputedStyle(elem,null);if(cur&&!color(elem))ret=cur.getPropertyValue(prop);else{for(var a=elem;a&&color(a);a=a.parentNode)stack.unshift(a);for(a=0;a<stack.length;a++)if(color(stack[a])){swap[a]=stack[a].style.display;stack[a].style.display="block";}ret=prop=="display"&&swap[stack.length-1]!=null?"none":document.defaultView.getComputedStyle(elem,null).getPropertyValue(prop)||"";for(a=0;a<swap.length;a++)if(swap[a]!=null)stack[a].style.display=swap[a];}if(prop=="opacity"&&ret=="")ret="1";}else if(elem.currentStyle){var newProp=prop.replace(/\-(\w)/g,function(m,c){return c.toUpperCase();});ret=elem.currentStyle[prop]||elem.currentStyle[newProp];if(!/^\d+(px)?$/i.test(ret)&&/^\d/.test(ret)){var style=elem.style.left;var runtimeStyle=elem.runtimeStyle.left;elem.runtimeStyle.left=elem.currentStyle.left;elem.style.left=ret||0;ret=elem.style.pixelLeft+"px";elem.style.left=style;elem.runtimeStyle.left=runtimeStyle;}}return ret;},clean:function(a,doc){var r=[];doc=doc||document;jQuery.each(a,function(i,arg){if(!arg)return;if(arg.constructor==Number)arg=arg.toString();if(typeof arg=="string"){arg=arg.replace(/(<(\w+)[^>]*?)\/>/g,function(m,all,tag){return tag.match(/^(abbr|br|col|img|input|link|meta|param|hr|area)$/i)?m:all+"></"+tag+">";});var s=jQuery.trim(arg).toLowerCase(),div=doc.createElement("div"),tb=[];var wrap=!s.indexOf("<opt")&&[1,"<select>","</select>"]||!s.indexOf("<leg")&&[1,"<fieldset>","</fieldset>"]||s.match(/^<(thead|tbody|tfoot|colg|cap)/)&&[1,"<table>","</table>"]||!s.indexOf("<tr")&&[2,"<table><tbody>","</tbody></table>"]||(!s.indexOf("<td")||!s.indexOf("<th"))&&[3,"<table><tbody><tr>","</tr></tbody></table>"]||!s.indexOf("<col")&&[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"]||jQuery.browser.msie&&[1,"div<div>","</div>"]||[0,"",""];div.innerHTML=wrap[1]+arg+wrap[2];while(wrap[0]--)div=div.lastChild;if(jQuery.browser.msie){if(!s.indexOf("<table")&&s.indexOf("<tbody")<0)tb=div.firstChild&&div.firstChild.childNodes;else if(wrap[1]=="<table>"&&s.indexOf("<tbody")<0)tb=div.childNodes;for(var n=tb.length-1;n>=0;--n)if(jQuery.nodeName(tb[n],"tbody")&&!tb[n].childNodes.length)tb[n].parentNode.removeChild(tb[n]);if(/^\s/.test(arg))div.insertBefore(doc.createTextNode(arg.match(/^\s*/)[0]),div.firstChild);}arg=jQuery.makeArray(div.childNodes);}if(0===arg.length&&(!jQuery.nodeName(arg,"form")&&!jQuery.nodeName(arg,"select")))return;if(arg[0]==undefined||jQuery.nodeName(arg,"form")||arg.options)r.push(arg);else
+r=jQuery.merge(r,arg);});return r;},attr:function(elem,name,value){var fix=jQuery.isXMLDoc(elem)?{}:jQuery.props;if(name=="selected"&&jQuery.browser.safari)elem.parentNode.selectedIndex;if(fix[name]){if(value!=undefined)elem[fix[name]]=value;return elem[fix[name]];}else if(jQuery.browser.msie&&name=="style")return jQuery.attr(elem.style,"cssText",value);else if(value==undefined&&jQuery.browser.msie&&jQuery.nodeName(elem,"form")&&(name=="action"||name=="method"))return elem.getAttributeNode(name).nodeValue;else if(elem.tagName){if(value!=undefined){if(name=="type"&&jQuery.nodeName(elem,"input")&&elem.parentNode)throw"type property can't be changed";elem.setAttribute(name,value);}if(jQuery.browser.msie&&/href|src/.test(name)&&!jQuery.isXMLDoc(elem))return elem.getAttribute(name,2);return elem.getAttribute(name);}else{if(name=="opacity"&&jQuery.browser.msie){if(value!=undefined){elem.zoom=1;elem.filter=(elem.filter||"").replace(/alpha\([^)]*\)/,"")+(parseFloat(value).toString()=="NaN"?"":"alpha(opacity="+value*100+")");}return elem.filter?(parseFloat(elem.filter.match(/opacity=([^)]*)/)[1])/100).toString():"";}name=name.replace(/-([a-z])/ig,function(z,b){return b.toUpperCase();});if(value!=undefined)elem[name]=value;return elem[name];}},trim:function(t){return(t||"").replace(/^\s+|\s+$/g,"");},makeArray:function(a){var r=[];if(typeof a!="array")for(var i=0,al=a.length;i<al;i++)r.push(a[i]);else
+r=a.slice(0);return r;},inArray:function(b,a){for(var i=0,al=a.length;i<al;i++)if(a[i]==b)return i;return-1;},merge:function(first,second){if(jQuery.browser.msie){for(var i=0;second[i];i++)if(second[i].nodeType!=8)first.push(second[i]);}else
+for(var i=0;second[i];i++)first.push(second[i]);return first;},unique:function(first){var r=[],done={};try{for(var i=0,fl=first.length;i<fl;i++){var id=jQuery.data(first[i]);if(!done[id]){done[id]=true;r.push(first[i]);}}}catch(e){r=first;}return r;},grep:function(elems,fn,inv){if(typeof fn=="string")fn=eval("false||function(a,i){return "+fn+"}");var result=[];for(var i=0,el=elems.length;i<el;i++)if(!inv&&fn(elems[i],i)||inv&&!fn(elems[i],i))result.push(elems[i]);return result;},map:function(elems,fn){if(typeof fn=="string")fn=eval("false||function(a){return "+fn+"}");var result=[];for(var i=0,el=elems.length;i<el;i++){var val=fn(elems[i],i);if(val!==null&&val!=undefined){if(val.constructor!=Array)val=[val];result=result.concat(val);}}return result;}});var userAgent=navigator.userAgent.toLowerCase();jQuery.browser={version:(userAgent.match(/.+(?:rv|it|ra|ie)[\/: ]([\d.]+)/)||[])[1],safari:/webkit/.test(userAgent),opera:/opera/.test(userAgent),msie:/msie/.test(userAgent)&&!/opera/.test(userAgent),mozilla:/mozilla/.test(userAgent)&&!/(compatible|webkit)/.test(userAgent)};var styleFloat=jQuery.browser.msie?"styleFloat":"cssFloat";jQuery.extend({boxModel:!jQuery.browser.msie||document.compatMode=="CSS1Compat",styleFloat:jQuery.browser.msie?"styleFloat":"cssFloat",props:{"for":"htmlFor","class":"className","float":styleFloat,cssFloat:styleFloat,styleFloat:styleFloat,innerHTML:"innerHTML",className:"className",value:"value",disabled:"disabled",checked:"checked",readonly:"readOnly",selected:"selected",maxlength:"maxLength"}});jQuery.each({parent:"a.parentNode",parents:"jQuery.dir(a,'parentNode')",next:"jQuery.nth(a,2,'nextSibling')",prev:"jQuery.nth(a,2,'previousSibling')",nextAll:"jQuery.dir(a,'nextSibling')",prevAll:"jQuery.dir(a,'previousSibling')",siblings:"jQuery.sibling(a.parentNode.firstChild,a)",children:"jQuery.sibling(a.firstChild)",contents:"jQuery.nodeName(a,'iframe')?a.contentDocument||a.contentWindow.document:jQuery.makeArray(a.childNodes)"},function(i,n){jQuery.fn[i]=function(a){var ret=jQuery.map(this,n);if(a&&typeof a=="string")ret=jQuery.multiFilter(a,ret);return this.pushStack(jQuery.unique(ret));};});jQuery.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(i,n){jQuery.fn[i]=function(){var a=arguments;return this.each(function(){for(var j=0,al=a.length;j<al;j++)jQuery(a[j])[n](this);});};});jQuery.each({removeAttr:function(key){jQuery.attr(this,key,"");this.removeAttribute(key);},addClass:function(c){jQuery.className.add(this,c);},removeClass:function(c){jQuery.className.remove(this,c);},toggleClass:function(c){jQuery.className[jQuery.className.has(this,c)?"remove":"add"](this,c);},remove:function(a){if(!a||jQuery.filter(a,[this]).r.length){jQuery.removeData(this);this.parentNode.removeChild(this);}},empty:function(){jQuery("*",this).each(function(){jQuery.removeData(this);});while(this.firstChild)this.removeChild(this.firstChild);}},function(i,n){jQuery.fn[i]=function(){return this.each(n,arguments);};});jQuery.each(["Height","Width"],function(i,name){var n=name.toLowerCase();jQuery.fn[n]=function(h){return this[0]==window?jQuery.browser.safari&&self["inner"+name]||jQuery.boxModel&&Math.max(document.documentElement["client"+name],document.body["client"+name])||document.body["client"+name]:this[0]==document?Math.max(document.body["scroll"+name],document.body["offset"+name]):h==undefined?(this.length?jQuery.css(this[0],n):null):this.css(n,h.constructor==String?h:h+"px");};});var chars=jQuery.browser.safari&&parseInt(jQuery.browser.version)<417?"(?:[\\w*_-]|\\\\.)":"(?:[\\w\u0128-\uFFFF*_-]|\\\\.)",quickChild=new RegExp("^>\\s*("+chars+"+)"),quickID=new RegExp("^("+chars+"+)(#)("+chars+"+)"),quickClass=new RegExp("^([#.]?)("+chars+"*)");jQuery.extend({expr:{"":"m[2]=='*'||jQuery.nodeName(a,m[2])","#":"a.getAttribute('id')==m[2]",":":{lt:"i<m[3]-0",gt:"i>m[3]-0",nth:"m[3]-0==i",eq:"m[3]-0==i",first:"i==0",last:"i==r.length-1",even:"i%2==0",odd:"i%2","first-child":"a.parentNode.getElementsByTagName('*')[0]==a","last-child":"jQuery.nth(a.parentNode.lastChild,1,'previousSibling')==a","only-child":"!jQuery.nth(a.parentNode.lastChild,2,'previousSibling')",parent:"a.firstChild",empty:"!a.firstChild",contains:"(a.textContent||a.innerText||'').indexOf(m[3])>=0",visible:'"hidden"!=a.type&&jQuery.css(a,"display")!="none"&&jQuery.css(a,"visibility")!="hidden"',hidden:'"hidden"==a.type||jQuery.css(a,"display")=="none"||jQuery.css(a,"visibility")=="hidden"',enabled:"!a.disabled",disabled:"a.disabled",checked:"a.checked",selected:"a.selected||jQuery.attr(a,'selected')",text:"'text'==a.type",radio:"'radio'==a.type",checkbox:"'checkbox'==a.type",file:"'file'==a.type",password:"'password'==a.type",submit:"'submit'==a.type",image:"'image'==a.type",reset:"'reset'==a.type",button:'"button"==a.type||jQuery.nodeName(a,"button")',input:"/input|select|textarea|button/i.test(a.nodeName)",has:"jQuery.find(m[3],a).length",header:"/h\\d/i.test(a.nodeName)",animated:"jQuery.grep(jQuery.timers,function(fn){return a==fn.elem;}).length"}},parse:[/^(\[) *@?([\w-]+) *([!*$^~=]*) *('?"?)(.*?)\4 *\]/,/^(:)([\w-]+)\("?'?(.*?(\(.*?\))?[^(]*?)"?'?\)/,new RegExp("^([:.#]*)("+chars+"+)")],multiFilter:function(expr,elems,not){var old,cur=[];while(expr&&expr!=old){old=expr;var f=jQuery.filter(expr,elems,not);expr=f.t.replace(/^\s*,\s*/,"");cur=not?elems=f.r:jQuery.merge(cur,f.r);}return cur;},find:function(t,context){if(typeof t!="string")return[t];if(context&&!context.nodeType)context=null;context=context||document;var ret=[context],done=[],last;while(t&&last!=t){var r=[];last=t;t=jQuery.trim(t);var foundToken=false;var re=quickChild;var m=re.exec(t);if(m){var nodeName=m[1].toUpperCase();for(var i=0;ret[i];i++)for(var c=ret[i].firstChild;c;c=c.nextSibling)if(c.nodeType==1&&(nodeName=="*"||c.nodeName.toUpperCase()==nodeName.toUpperCase()))r.push(c);ret=r;t=t.replace(re,"");if(t.indexOf(" ")==0)continue;foundToken=true;}else{re=/^([>+~])\s*(\w*)/i;if((m=re.exec(t))!=null){r=[];var nodeName=m[2],merge={};m=m[1];for(var j=0,rl=ret.length;j<rl;j++){var n=m=="~"||m=="+"?ret[j].nextSibling:ret[j].firstChild;for(;n;n=n.nextSibling)if(n.nodeType==1){var id=jQuery.data(n);if(m=="~"&&merge[id])break;if(!nodeName||n.nodeName.toUpperCase()==nodeName.toUpperCase()){if(m=="~")merge[id]=true;r.push(n);}if(m=="+")break;}}ret=r;t=jQuery.trim(t.replace(re,""));foundToken=true;}}if(t&&!foundToken){if(!t.indexOf(",")){if(context==ret[0])ret.shift();done=jQuery.merge(done,ret);r=ret=[context];t=" "+t.substr(1,t.length);}else{var re2=quickID;var m=re2.exec(t);if(m){m=[0,m[2],m[3],m[1]];}else{re2=quickClass;m=re2.exec(t);}m[2]=m[2].replace(/\\/g,"");var elem=ret[ret.length-1];if(m[1]=="#"&&elem&&elem.getElementById&&!jQuery.isXMLDoc(elem)){var oid=elem.getElementById(m[2]);if((jQuery.browser.msie||jQuery.browser.opera)&&oid&&typeof oid.id=="string"&&oid.id!=m[2])oid=jQuery('[@id="'+m[2]+'"]',elem)[0];ret=r=oid&&(!m[3]||jQuery.nodeName(oid,m[3]))?[oid]:[];}else{for(var i=0;ret[i];i++){var tag=m[1]=="#"&&m[3]?m[3]:m[1]!=""||m[0]==""?"*":m[2];if(tag=="*"&&ret[i].nodeName.toLowerCase()=="object")tag="param";r=jQuery.merge(r,ret[i].getElementsByTagName(tag));}if(m[1]==".")r=jQuery.classFilter(r,m[2]);if(m[1]=="#"){var tmp=[];for(var i=0;r[i];i++)if(r[i].getAttribute("id")==m[2]){tmp=[r[i]];break;}r=tmp;}ret=r;}t=t.replace(re2,"");}}if(t){var val=jQuery.filter(t,r);ret=r=val.r;t=jQuery.trim(val.t);}}if(t)ret=[];if(ret&&context==ret[0])ret.shift();done=jQuery.merge(done,ret);return done;},classFilter:function(r,m,not){m=" "+m+" ";var tmp=[];for(var i=0;r[i];i++){var pass=(" "+r[i].className+" ").indexOf(m)>=0;if(!not&&pass||not&&!pass)tmp.push(r[i]);}return tmp;},filter:function(t,r,not){var last;while(t&&t!=last){last=t;var p=jQuery.parse,m;for(var i=0;p[i];i++){m=p[i].exec(t);if(m){t=t.substring(m[0].length);m[2]=m[2].replace(/\\/g,"");break;}}if(!m)break;if(m[1]==":"&&m[2]=="not")r=jQuery.filter(m[3],r,true).r;else if(m[1]==".")r=jQuery.classFilter(r,m[2],not);else if(m[1]=="["){var tmp=[],type=m[3];for(var i=0,rl=r.length;i<rl;i++){var a=r[i],z=a[jQuery.props[m[2]]||m[2]];if(z==null||/href|src|selected/.test(m[2]))z=jQuery.attr(a,m[2])||'';if((type==""&&!!z||type=="="&&z==m[5]||type=="!="&&z!=m[5]||type=="^="&&z&&!z.indexOf(m[5])||type=="$="&&z.substr(z.length-m[5].length)==m[5]||(type=="*="||type=="~=")&&z.indexOf(m[5])>=0)^not)tmp.push(a);}r=tmp;}else if(m[1]==":"&&m[2]=="nth-child"){var merge={},tmp=[],test=/(\d*)n\+?(\d*)/.exec(m[3]=="even"&&"2n"||m[3]=="odd"&&"2n+1"||!/\D/.test(m[3])&&"n+"+m[3]||m[3]),first=(test[1]||1)-0,last=test[2]-0;for(var i=0,rl=r.length;i<rl;i++){var node=r[i],parentNode=node.parentNode,id=jQuery.data(parentNode);if(!merge[id]){var c=1;for(var n=parentNode.firstChild;n;n=n.nextSibling)if(n.nodeType==1)n.nodeIndex=c++;merge[id]=true;}var add=false;if(first==1){if(last==0||node.nodeIndex==last)add=true;}else if((node.nodeIndex+last)%first==0)add=true;if(add^not)tmp.push(node);}r=tmp;}else{var f=jQuery.expr[m[1]];if(typeof f!="string")f=jQuery.expr[m[1]][m[2]];f=eval("false||function(a,i){return "+f+"}");r=jQuery.grep(r,f,not);}}return{r:r,t:t};},dir:function(elem,dir){var matched=[];var cur=elem[dir];while(cur&&cur!=document){if(cur.nodeType==1)matched.push(cur);cur=cur[dir];}return matched;},nth:function(cur,result,dir,elem){result=result||1;var num=0;for(;cur;cur=cur[dir])if(cur.nodeType==1&&++num==result)break;return cur;},sibling:function(n,elem){var r=[];for(;n;n=n.nextSibling){if(n.nodeType==1&&(!elem||n!=elem))r.push(n);}return r;}});jQuery.event={add:function(element,type,handler,data){if(jQuery.browser.msie&&element.setInterval!=undefined)element=window;if(!handler.guid)handler.guid=this.guid++;if(data!=undefined){var fn=handler;handler=function(){return fn.apply(this,arguments);};handler.data=data;handler.guid=fn.guid;}var parts=type.split(".");type=parts[0];handler.type=parts[1];var events=jQuery.data(element,"events")||jQuery.data(element,"events",{});var handle=jQuery.data(element,"handle",function(){var val;if(typeof jQuery=="undefined"||jQuery.event.triggered)return val;val=jQuery.event.handle.apply(element,arguments);return val;});var handlers=events[type];if(!handlers){handlers=events[type]={};if(element.addEventListener)element.addEventListener(type,handle,false);else
+element.attachEvent("on"+type,handle);}handlers[handler.guid]=handler;this.global[type]=true;},guid:1,global:{},remove:function(element,type,handler){var events=jQuery.data(element,"events"),ret,index;if(typeof type=="string"){var parts=type.split(".");type=parts[0];}if(events){if(type&&type.type){handler=type.handler;type=type.type;}if(!type){for(type in events)this.remove(element,type);}else if(events[type]){if(handler)delete events[type][handler.guid];else
+for(handler in events[type])if(!parts[1]||events[type][handler].type==parts[1])delete events[type][handler];for(ret in events[type])break;if(!ret){if(element.removeEventListener)element.removeEventListener(type,jQuery.data(element,"handle"),false);else
+element.detachEvent("on"+type,jQuery.data(element,"handle"));ret=null;delete events[type];}}for(ret in events)break;if(!ret){jQuery.removeData(element,"events");jQuery.removeData(element,"handle");}}},trigger:function(type,data,element,donative,extra){data=jQuery.makeArray(data||[]);if(!element){if(this.global[type])jQuery("*").add([window,document]).trigger(type,data);}else{var val,ret,fn=jQuery.isFunction(element[type]||null),evt=!data[0]||!data[0].preventDefault;if(evt)data.unshift(this.fix({type:type,target:element}));if(jQuery.isFunction(jQuery.data(element,"handle")))val=jQuery.data(element,"handle").apply(element,data);if(!fn&&element["on"+type]&&element["on"+type].apply(element,data)===false)val=false;if(evt)data.shift();if(extra&&extra.apply(element,data)===false)val=false;if(fn&&donative!==false&&val!==false&&!(jQuery.nodeName(element,'a')&&type=="click")){this.triggered=true;element[type]();}this.triggered=false;}return val;},handle:function(event){var val;event=jQuery.event.fix(event||window.event||{});var parts=event.type.split(".");event.type=parts[0];var c=jQuery.data(this,"events")&&jQuery.data(this,"events")[event.type],args=Array.prototype.slice.call(arguments,1);args.unshift(event);for(var j in c){args[0].handler=c[j];args[0].data=c[j].data;if(!parts[1]||c[j].type==parts[1]){var tmp=c[j].apply(this,args);if(val!==false)val=tmp;if(tmp===false){event.preventDefault();event.stopPropagation();}}}if(jQuery.browser.msie)event.target=event.preventDefault=event.stopPropagation=event.handler=event.data=null;return val;},fix:function(event){var originalEvent=event;event=jQuery.extend({},originalEvent);event.preventDefault=function(){if(originalEvent.preventDefault)originalEvent.preventDefault();originalEvent.returnValue=false;};event.stopPropagation=function(){if(originalEvent.stopPropagation)originalEvent.stopPropagation();originalEvent.cancelBubble=true;};if(!event.target&&event.srcElement)event.target=event.srcElement;if(jQuery.browser.safari&&event.target.nodeType==3)event.target=originalEvent.target.parentNode;if(!event.relatedTarget&&event.fromElement)event.relatedTarget=event.fromElement==event.target?event.toElement:event.fromElement;if(event.pageX==null&&event.clientX!=null){var e=document.documentElement,b=document.body;event.pageX=event.clientX+(e&&e.scrollLeft||b.scrollLeft||0);event.pageY=event.clientY+(e&&e.scrollTop||b.scrollTop||0);}if(!event.which&&(event.charCode||event.keyCode))event.which=event.charCode||event.keyCode;if(!event.metaKey&&event.ctrlKey)event.metaKey=event.ctrlKey;if(!event.which&&event.button)event.which=(event.button&1?1:(event.button&2?3:(event.button&4?2:0)));return event;}};jQuery.fn.extend({bind:function(type,data,fn){return type=="unload"?this.one(type,data,fn):this.each(function(){jQuery.event.add(this,type,fn||data,fn&&data);});},one:function(type,data,fn){return this.each(function(){jQuery.event.add(this,type,function(event){jQuery(this).unbind(event);return(fn||data).apply(this,arguments);},fn&&data);});},unbind:function(type,fn){return this.each(function(){jQuery.event.remove(this,type,fn);});},trigger:function(type,data,fn){return this.each(function(){jQuery.event.trigger(type,data,this,true,fn);});},triggerHandler:function(type,data,fn){if(this[0])return jQuery.event.trigger(type,data,this[0],false,fn);},toggle:function(){var a=arguments;return this.click(function(e){this.lastToggle=0==this.lastToggle?1:0;e.preventDefault();return a[this.lastToggle].apply(this,[e])||false;});},hover:function(f,g){function handleHover(e){var p=e.relatedTarget;while(p&&p!=this)try{p=p.parentNode;}catch(e){p=this;};if(p==this)return false;return(e.type=="mouseover"?f:g).apply(this,[e]);}return this.mouseover(handleHover).mouseout(handleHover);},ready:function(f){bindReady();if(jQuery.isReady)f.apply(document,[jQuery]);else
+jQuery.readyList.push(function(){return f.apply(this,[jQuery]);});return this;}});jQuery.extend({isReady:false,readyList:[],ready:function(){if(!jQuery.isReady){jQuery.isReady=true;if(jQuery.readyList){jQuery.each(jQuery.readyList,function(){this.apply(document);});jQuery.readyList=null;}if(jQuery.browser.mozilla||jQuery.browser.opera)document.removeEventListener("DOMContentLoaded",jQuery.ready,false);if(!window.frames.length)jQuery(window).load(function(){jQuery("#__ie_init").remove();});}}});jQuery.each(("blur,focus,load,resize,scroll,unload,click,dblclick,"+"mousedown,mouseup,mousemove,mouseover,mouseout,change,select,"+"submit,keydown,keypress,keyup,error").split(","),function(i,o){jQuery.fn[o]=function(f){return f?this.bind(o,f):this.trigger(o);};});var readyBound=false;function bindReady(){if(readyBound)return;readyBound=true;if(jQuery.browser.mozilla||jQuery.browser.opera)document.addEventListener("DOMContentLoaded",jQuery.ready,false);else if(jQuery.browser.msie){document.write("<scr"+"ipt id=__ie_init defer=true "+"src=//:><\/script>");var script=document.getElementById("__ie_init");if(script)script.onreadystatechange=function(){if(this.readyState!="complete")return;jQuery.ready();};script=null;}else if(jQuery.browser.safari)jQuery.safariTimer=setInterval(function(){if(document.readyState=="loaded"||document.readyState=="complete"){clearInterval(jQuery.safariTimer);jQuery.safariTimer=null;jQuery.ready();}},10);jQuery.event.add(window,"load",jQuery.ready);}jQuery.fn.extend({load:function(url,params,callback){if(jQuery.isFunction(url))return this.bind("load",url);var off=url.indexOf(" ");if(off>=0){var selector=url.slice(off,url.length);url=url.slice(0,off);}callback=callback||function(){};var type="GET";if(params)if(jQuery.isFunction(params)){callback=params;params=null;}else{params=jQuery.param(params);type="POST";}var self=this;jQuery.ajax({url:url,type:type,data:params,complete:function(res,status){if(status=="success"||status=="notmodified")self.html(selector?jQuery("<div/>").append(res.responseText.replace(/<script(.|\s)*?\/script>/g,"")).find(selector):res.responseText);setTimeout(function(){self.each(callback,[res.responseText,status,res]);},13);}});return this;},serialize:function(){return jQuery.param(this.serializeArray());},serializeArray:function(){return this.map(function(){return jQuery.nodeName(this,"form")?jQuery.makeArray(this.elements):this;}).filter(function(){return this.name&&!this.disabled&&(this.checked||/select|textarea/i.test(this.nodeName)||/text|hidden|password/i.test(this.type));}).map(function(i,elem){var val=jQuery(this).val();return val==null?null:val.constructor==Array?jQuery.map(val,function(i,val){return{name:elem.name,value:val};}):{name:elem.name,value:val};}).get();}});jQuery.each("ajaxStart,ajaxStop,ajaxComplete,ajaxError,ajaxSuccess,ajaxSend".split(","),function(i,o){jQuery.fn[o]=function(f){return this.bind(o,f);};});var jsc=(new Date).getTime();jQuery.extend({get:function(url,data,callback,type){if(jQuery.isFunction(data)){callback=data;data=null;}return jQuery.ajax({type:"GET",url:url,data:data,success:callback,dataType:type});},getScript:function(url,callback){return jQuery.get(url,null,callback,"script");},getJSON:function(url,data,callback){return jQuery.get(url,data,callback,"json");},post:function(url,data,callback,type){if(jQuery.isFunction(data)){callback=data;data={};}return jQuery.ajax({type:"POST",url:url,data:data,success:callback,dataType:type});},ajaxSetup:function(settings){jQuery.extend(jQuery.ajaxSettings,settings);},ajaxSettings:{global:true,type:"GET",timeout:0,contentType:"application/x-www-form-urlencoded",processData:true,async:true,data:null},lastModified:{},ajax:function(s){var jsonp,jsre=/=(\?|%3F)/g,status,data;s=jQuery.extend(true,s,jQuery.extend(true,{},jQuery.ajaxSettings,s));if(s.data&&s.processData&&typeof s.data!="string")s.data=jQuery.param(s.data);var q=s.url.indexOf("?");if(q>-1){s.data=(s.data?s.data+"&":"")+s.url.slice(q+1);s.url=s.url.slice(0,q);}if(s.dataType=="jsonp"){if(!s.data||!s.data.match(jsre))s.data=(s.data?s.data+"&":"")+(s.jsonp||"callback")+"=?";s.dataType="json";}if(s.dataType=="json"&&s.data&&s.data.match(jsre)){jsonp="jsonp"+jsc++;s.data=s.data.replace(jsre,"="+jsonp);s.dataType="script";window[jsonp]=function(tmp){data=tmp;success();window[jsonp]=undefined;try{delete window[jsonp];}catch(e){}};}if(s.dataType=="script"&&s.cache==null)s.cache=false;if(s.cache===false&&s.type.toLowerCase()=="get")s.data=(s.data?s.data+"&":"")+"_="+(new Date()).getTime();if(s.data&&s.type.toLowerCase()=="get"){s.url+="?"+s.data;s.data=null;}if(s.global&&!jQuery.active++)jQuery.event.trigger("ajaxStart");if(!s.url.indexOf("http")&&s.dataType=="script"){var head=document.getElementsByTagName("head")[0];var script=document.createElement("script");script.src=s.url;if(!jsonp&&(s.success||s.complete)){var done=false;script.onload=script.onreadystatechange=function(){if(!done&&(!this.readyState||this.readyState=="loaded"||this.readyState=="complete")){done=true;success();complete();head.removeChild(script);}};}head.appendChild(script);return;}var requestDone=false;var xml=window.ActiveXObject?new ActiveXObject("Microsoft.XMLHTTP"):new XMLHttpRequest();xml.open(s.type,s.url,s.async);if(s.data)xml.setRequestHeader("Content-Type",s.contentType);if(s.ifModified)xml.setRequestHeader("If-Modified-Since",jQuery.lastModified[s.url]||"Thu, 01 Jan 1970 00:00:00 GMT");xml.setRequestHeader("X-Requested-With","XMLHttpRequest");if(s.beforeSend)s.beforeSend(xml);if(s.global)jQuery.event.trigger("ajaxSend",[xml,s]);var onreadystatechange=function(isTimeout){if(!requestDone&&xml&&(xml.readyState==4||isTimeout=="timeout")){requestDone=true;if(ival){clearInterval(ival);ival=null;}status=isTimeout=="timeout"&&"timeout"||!jQuery.httpSuccess(xml)&&"error"||s.ifModified&&jQuery.httpNotModified(xml,s.url)&&"notmodified"||"success";if(status=="success"){try{data=jQuery.httpData(xml,s.dataType);}catch(e){status="parsererror";}}if(status=="success"){var modRes;try{modRes=xml.getResponseHeader("Last-Modified");}catch(e){}if(s.ifModified&&modRes)jQuery.lastModified[s.url]=modRes;if(!jsonp)success();}else
+jQuery.handleError(s,xml,status);complete();if(s.async)xml=null;}};if(s.async){var ival=setInterval(onreadystatechange,13);if(s.timeout>0)setTimeout(function(){if(xml){xml.abort();if(!requestDone)onreadystatechange("timeout");}},s.timeout);}try{xml.send(s.data);}catch(e){jQuery.handleError(s,xml,null,e);}if(!s.async)onreadystatechange();return xml;function success(){if(s.success)s.success(data,status);if(s.global)jQuery.event.trigger("ajaxSuccess",[xml,s]);}function complete(){if(s.complete)s.complete(xml,status);if(s.global)jQuery.event.trigger("ajaxComplete",[xml,s]);if(s.global&&!--jQuery.active)jQuery.event.trigger("ajaxStop");}},handleError:function(s,xml,status,e){if(s.error)s.error(xml,status,e);if(s.global)jQuery.event.trigger("ajaxError",[xml,s,e]);},active:0,httpSuccess:function(r){try{return!r.status&&location.protocol=="file:"||(r.status>=200&&r.status<300)||r.status==304||jQuery.browser.safari&&r.status==undefined;}catch(e){}return false;},httpNotModified:function(xml,url){try{var xmlRes=xml.getResponseHeader("Last-Modified");return xml.status==304||xmlRes==jQuery.lastModified[url]||jQuery.browser.safari&&xml.status==undefined;}catch(e){}return false;},httpData:function(r,type){var ct=r.getResponseHeader("content-type");var xml=type=="xml"||!type&&ct&&ct.indexOf("xml")>=0;var data=xml?r.responseXML:r.responseText;if(xml&&data.documentElement.tagName=="parsererror")throw"parsererror";if(type=="script")jQuery.globalEval(data);if(type=="json")data=eval("("+data+")");return data;},param:function(a){var s=[];if(a.constructor==Array||a.jquery)jQuery.each(a,function(){s.push(encodeURIComponent(this.name)+"="+encodeURIComponent(this.value));});else
+for(var j in a)if(a[j]&&a[j].constructor==Array)jQuery.each(a[j],function(){s.push(encodeURIComponent(j)+"="+encodeURIComponent(this));});else
+s.push(encodeURIComponent(j)+"="+encodeURIComponent(a[j]));return s.join("&").replace(/%20/g,"+");}});jQuery.fn.extend({show:function(speed,callback){return speed?this.animate({height:"show",width:"show",opacity:"show"},speed,callback):this.filter(":hidden").each(function(){this.style.display=this.oldblock?this.oldblock:"";if(jQuery.css(this,"display")=="none")this.style.display="block";}).end();},hide:function(speed,callback){return speed?this.animate({height:"hide",width:"hide",opacity:"hide"},speed,callback):this.filter(":visible").each(function(){this.oldblock=this.oldblock||jQuery.css(this,"display");if(this.oldblock=="none")this.oldblock="block";this.style.display="none";}).end();},_toggle:jQuery.fn.toggle,toggle:function(fn,fn2){return jQuery.isFunction(fn)&&jQuery.isFunction(fn2)?this._toggle(fn,fn2):fn?this.animate({height:"toggle",width:"toggle",opacity:"toggle"},fn,fn2):this.each(function(){jQuery(this)[jQuery(this).is(":hidden")?"show":"hide"]();});},slideDown:function(speed,callback){return this.animate({height:"show"},speed,callback);},slideUp:function(speed,callback){return this.animate({height:"hide"},speed,callback);},slideToggle:function(speed,callback){return this.animate({height:"toggle"},speed,callback);},fadeIn:function(speed,callback){return this.animate({opacity:"show"},speed,callback);},fadeOut:function(speed,callback){return this.animate({opacity:"hide"},speed,callback);},fadeTo:function(speed,to,callback){return this.animate({opacity:to},speed,callback);},animate:function(prop,speed,easing,callback){var opt=jQuery.speed(speed,easing,callback);return this[opt.queue===false?"each":"queue"](function(){opt=jQuery.extend({},opt);var hidden=jQuery(this).is(":hidden"),self=this;for(var p in prop){if(prop[p]=="hide"&&hidden||prop[p]=="show"&&!hidden)return jQuery.isFunction(opt.complete)&&opt.complete.apply(this);if(p=="height"||p=="width"){opt.display=jQuery.css(this,"display");opt.overflow=this.style.overflow;}}if(opt.overflow!=null)this.style.overflow="hidden";opt.curAnim=jQuery.extend({},prop);jQuery.each(prop,function(name,val){var e=new jQuery.fx(self,opt,name);if(/toggle|show|hide/.test(val))e[val=="toggle"?hidden?"show":"hide":val](prop);else{var parts=val.toString().match(/^([+-]?)([\d.]+)(.*)$/),start=e.cur(true)||0;if(parts){end=parseFloat(parts[2]),unit=parts[3]||"px";if(unit!="px"){self.style[name]=end+unit;start=(end/e.cur(true))*start;self.style[name]=start+unit;}if(parts[1])end=((parts[1]=="-"?-1:1)*end)+start;e.custom(start,end,unit);}else
+e.custom(start,val,"");}});return true;});},queue:function(type,fn){if(!fn){fn=type;type="fx";}if(!arguments.length)return queue(this[0],type);return this.each(function(){if(fn.constructor==Array)queue(this,type,fn);else{queue(this,type).push(fn);if(queue(this,type).length==1)fn.apply(this);}});},stop:function(){var timers=jQuery.timers;return this.each(function(){for(var i=0;i<timers.length;i++)if(timers[i].elem==this)timers.splice(i--,1);}).dequeue();}});var queue=function(elem,type,array){if(!elem)return;var q=jQuery.data(elem,type+"queue");if(!q||array)q=jQuery.data(elem,type+"queue",array?jQuery.makeArray(array):[]);return q;};jQuery.fn.dequeue=function(type){type=type||"fx";return this.each(function(){var q=queue(this,type);q.shift();if(q.length)q[0].apply(this);});};jQuery.extend({speed:function(speed,easing,fn){var opt=speed&&speed.constructor==Object?speed:{complete:fn||!fn&&easing||jQuery.isFunction(speed)&&speed,duration:speed,easing:fn&&easing||easing&&easing.constructor!=Function&&easing};opt.duration=(opt.duration&&opt.duration.constructor==Number?opt.duration:{slow:600,fast:200}[opt.duration])||400;opt.old=opt.complete;opt.complete=function(){jQuery(this).dequeue();if(jQuery.isFunction(opt.old))opt.old.apply(this);};return opt;},easing:{linear:function(p,n,firstNum,diff){return firstNum+diff*p;},swing:function(p,n,firstNum,diff){return((-Math.cos(p*Math.PI)/2)+0.5)*diff+firstNum;}},timers:[],fx:function(elem,options,prop){this.options=options;this.elem=elem;this.prop=prop;if(!options.orig)options.orig={};}});jQuery.fx.prototype={update:function(){if(this.options.step)this.options.step.apply(this.elem,[this.now,this]);(jQuery.fx.step[this.prop]||jQuery.fx.step._default)(this);if(this.prop=="height"||this.prop=="width")this.elem.style.display="block";},cur:function(force){if(this.elem[this.prop]!=null&&this.elem.style[this.prop]==null)return this.elem[this.prop];var r=parseFloat(jQuery.curCSS(this.elem,this.prop,force));return r&&r>-10000?r:parseFloat(jQuery.css(this.elem,this.prop))||0;},custom:function(from,to,unit){this.startTime=(new Date()).getTime();this.start=from;this.end=to;this.unit=unit||this.unit||"px";this.now=this.start;this.pos=this.state=0;this.update();var self=this;function t(){return self.step();}t.elem=this.elem;jQuery.timers.push(t);if(jQuery.timers.length==1){var timer=setInterval(function(){var timers=jQuery.timers;for(var i=0;i<timers.length;i++)if(!timers[i]())timers.splice(i--,1);if(!timers.length)clearInterval(timer);},13);}},show:function(){this.options.orig[this.prop]=jQuery.attr(this.elem.style,this.prop);this.options.show=true;this.custom(0,this.cur());if(this.prop=="width"||this.prop=="height")this.elem.style[this.prop]="1px";jQuery(this.elem).show();},hide:function(){this.options.orig[this.prop]=jQuery.attr(this.elem.style,this.prop);this.options.hide=true;this.custom(this.cur(),0);},step:function(){var t=(new Date()).getTime();if(t>this.options.duration+this.startTime){this.now=this.end;this.pos=this.state=1;this.update();this.options.curAnim[this.prop]=true;var done=true;for(var i in this.options.curAnim)if(this.options.curAnim[i]!==true)done=false;if(done){if(this.options.display!=null){this.elem.style.overflow=this.options.overflow;this.elem.style.display=this.options.display;if(jQuery.css(this.elem,"display")=="none")this.elem.style.display="block";}if(this.options.hide)this.elem.style.display="none";if(this.options.hide||this.options.show)for(var p in this.options.curAnim)jQuery.attr(this.elem.style,p,this.options.orig[p]);}if(done&&jQuery.isFunction(this.options.complete))this.options.complete.apply(this.elem);return false;}else{var n=t-this.startTime;this.state=n/this.options.duration;this.pos=jQuery.easing[this.options.easing||(jQuery.easing.swing?"swing":"linear")](this.state,n,0,1,this.options.duration);this.now=this.start+((this.end-this.start)*this.pos);this.update();}return true;}};jQuery.fx.step={scrollLeft:function(fx){fx.elem.scrollLeft=fx.now;},scrollTop:function(fx){fx.elem.scrollTop=fx.now;},opacity:function(fx){jQuery.attr(fx.elem.style,"opacity",fx.now);},_default:function(fx){fx.elem.style[fx.prop]=fx.now+fx.unit;}};jQuery.fn.offset=function(){var left=0,top=0,elem=this[0],results;if(elem)with(jQuery.browser){var absolute=jQuery.css(elem,"position")=="absolute",parent=elem.parentNode,offsetParent=elem.offsetParent,doc=elem.ownerDocument,safari2=safari&&!absolute&&parseInt(version)<522;if(elem.getBoundingClientRect){box=elem.getBoundingClientRect();add(box.left+Math.max(doc.documentElement.scrollLeft,doc.body.scrollLeft),box.top+Math.max(doc.documentElement.scrollTop,doc.body.scrollTop));if(msie){var border=jQuery("html").css("borderWidth");border=(border=="medium"||jQuery.boxModel&&parseInt(version)>=7)&&2||border;add(-border,-border);}}else{add(elem.offsetLeft,elem.offsetTop);while(offsetParent){add(offsetParent.offsetLeft,offsetParent.offsetTop);if(mozilla&&/^t[d|h]$/i.test(parent.tagName)||!safari2)border(offsetParent);if(safari2&&!absolute&&jQuery.css(offsetParent,"position")=="absolute")absolute=true;offsetParent=offsetParent.offsetParent;}while(parent.tagName&&/^body|html$/i.test(parent.tagName)){if(/^inline|table-row.*$/i.test(jQuery.css(parent,"display")))add(-parent.scrollLeft,-parent.scrollTop);if(mozilla&&jQuery.css(parent,"overflow")!="visible")border(parent);parent=parent.parentNode;}if(safari&&absolute)add(-doc.body.offsetLeft,-doc.body.offsetTop);}results={top:top,left:left};}return results;function border(elem){add(jQuery.css(elem,"borderLeftWidth"),jQuery.css(elem,"borderTopWidth"));}function add(l,t){left+=parseInt(l)||0;top+=parseInt(t)||0;}};})(); \ No newline at end of file
diff --git a/devtools/client/inspector/markup/test/lib_jquery_1.3_min.js b/devtools/client/inspector/markup/test/lib_jquery_1.3_min.js
new file mode 100644
index 000000000..378f94376
--- /dev/null
+++ b/devtools/client/inspector/markup/test/lib_jquery_1.3_min.js
@@ -0,0 +1,19 @@
+/*
+ * jQuery JavaScript Library v1.3
+ * http://jquery.com/
+ *
+ * Copyright (c) 2009 John Resig
+ * Dual licensed under the MIT and GPL licenses.
+ * http://docs.jquery.com/License
+ *
+ * Date: 2009-01-13 12:50:31 -0500 (Tue, 13 Jan 2009)
+ * Revision: 6104
+ */
+(function(){var l=this,g,x=l.jQuery,o=l.$,n=l.jQuery=l.$=function(D,E){return new n.fn.init(D,E)},C=/^[^<]*(<(.|\s)+>)[^>]*$|^#([\w-]+)$/,f=/^.[^:#\[\.,]*$/;n.fn=n.prototype={init:function(D,G){D=D||document;if(D.nodeType){this[0]=D;this.length=1;this.context=D;return this}if(typeof D==="string"){var F=C.exec(D);if(F&&(F[1]||!G)){if(F[1]){D=n.clean([F[1]],G)}else{var H=document.getElementById(F[3]);if(H){if(H.id!=F[3]){return n().find(D)}var E=n(H);E.context=document;E.selector=D;return E}D=[]}}else{return n(G).find(D)}}else{if(n.isFunction(D)){return n(document).ready(D)}}if(D.selector&&D.context){this.selector=D.selector;this.context=D.context}return this.setArray(n.makeArray(D))},selector:"",jquery:"1.3",size:function(){return this.length},get:function(D){return D===g?n.makeArray(this):this[D]},pushStack:function(E,G,D){var F=n(E);F.prevObject=this;F.context=this.context;if(G==="find"){F.selector=this.selector+(this.selector?" ":"")+D}else{if(G){F.selector=this.selector+"."+G+"("+D+")"}}return F},setArray:function(D){this.length=0;Array.prototype.push.apply(this,D);return this},each:function(E,D){return n.each(this,E,D)},index:function(D){return n.inArray(D&&D.jquery?D[0]:D,this)},attr:function(E,G,F){var D=E;if(typeof E==="string"){if(G===g){return this[0]&&n[F||"attr"](this[0],E)}else{D={};D[E]=G}}return this.each(function(H){for(E in D){n.attr(F?this.style:this,E,n.prop(this,D[E],F,H,E))}})},css:function(D,E){if((D=="width"||D=="height")&&parseFloat(E)<0){E=g}return this.attr(D,E,"curCSS")},text:function(E){if(typeof E!=="object"&&E!=null){return this.empty().append((this[0]&&this[0].ownerDocument||document).createTextNode(E))}var D="";n.each(E||this,function(){n.each(this.childNodes,function(){if(this.nodeType!=8){D+=this.nodeType!=1?this.nodeValue:n.fn.text([this])}})});return D},wrapAll:function(D){if(this[0]){var E=n(D,this[0].ownerDocument).clone();if(this[0].parentNode){E.insertBefore(this[0])}E.map(function(){var F=this;while(F.firstChild){F=F.firstChild}return F}).append(this)}return this},wrapInner:function(D){return this.each(function(){n(this).contents().wrapAll(D)})},wrap:function(D){return this.each(function(){n(this).wrapAll(D)})},append:function(){return this.domManip(arguments,true,function(D){if(this.nodeType==1){this.appendChild(D)}})},prepend:function(){return this.domManip(arguments,true,function(D){if(this.nodeType==1){this.insertBefore(D,this.firstChild)}})},before:function(){return this.domManip(arguments,false,function(D){this.parentNode.insertBefore(D,this)})},after:function(){return this.domManip(arguments,false,function(D){this.parentNode.insertBefore(D,this.nextSibling)})},end:function(){return this.prevObject||n([])},push:[].push,find:function(D){if(this.length===1&&!/,/.test(D)){var F=this.pushStack([],"find",D);F.length=0;n.find(D,this[0],F);return F}else{var E=n.map(this,function(G){return n.find(D,G)});return this.pushStack(/[^+>] [^+>]/.test(D)?n.unique(E):E,"find",D)}},clone:function(E){var D=this.map(function(){if(!n.support.noCloneEvent&&!n.isXMLDoc(this)){var H=this.cloneNode(true),G=document.createElement("div");G.appendChild(H);return n.clean([G.innerHTML])[0]}else{return this.cloneNode(true)}});var F=D.find("*").andSelf().each(function(){if(this[h]!==g){this[h]=null}});if(E===true){this.find("*").andSelf().each(function(H){if(this.nodeType==3){return}var G=n.data(this,"events");for(var J in G){for(var I in G[J]){n.event.add(F[H],J,G[J][I],G[J][I].data)}}})}return D},filter:function(D){return this.pushStack(n.isFunction(D)&&n.grep(this,function(F,E){return D.call(F,E)})||n.multiFilter(D,n.grep(this,function(E){return E.nodeType===1})),"filter",D)},closest:function(D){var E=n.expr.match.POS.test(D)?n(D):null;return this.map(function(){var F=this;while(F&&F.ownerDocument){if(E?E.index(F)>-1:n(F).is(D)){return F}F=F.parentNode}})},not:function(D){if(typeof D==="string"){if(f.test(D)){return this.pushStack(n.multiFilter(D,this,true),"not",D)}else{D=n.multiFilter(D,this)}}var E=D.length&&D[D.length-1]!==g&&!D.nodeType;return this.filter(function(){return E?n.inArray(this,D)<0:this!=D})},add:function(D){return this.pushStack(n.unique(n.merge(this.get(),typeof D==="string"?n(D):n.makeArray(D))))},is:function(D){return !!D&&n.multiFilter(D,this).length>0},hasClass:function(D){return !!D&&this.is("."+D)},val:function(J){if(J===g){var D=this[0];if(D){if(n.nodeName(D,"option")){return(D.attributes.value||{}).specified?D.value:D.text}if(n.nodeName(D,"select")){var H=D.selectedIndex,K=[],L=D.options,G=D.type=="select-one";if(H<0){return null}for(var E=G?H:0,I=G?H+1:L.length;E<I;E++){var F=L[E];if(F.selected){J=n(F).val();if(G){return J}K.push(J)}}return K}return(D.value||"").replace(/\r/g,"")}return g}if(typeof J==="number"){J+=""}return this.each(function(){if(this.nodeType!=1){return}if(n.isArray(J)&&/radio|checkbox/.test(this.type)){this.checked=(n.inArray(this.value,J)>=0||n.inArray(this.name,J)>=0)}else{if(n.nodeName(this,"select")){var M=n.makeArray(J);n("option",this).each(function(){this.selected=(n.inArray(this.value,M)>=0||n.inArray(this.text,M)>=0)});if(!M.length){this.selectedIndex=-1}}else{this.value=J}}})},html:function(D){return D===g?(this[0]?this[0].innerHTML:null):this.empty().append(D)},replaceWith:function(D){return this.after(D).remove()},eq:function(D){return this.slice(D,+D+1)},slice:function(){return this.pushStack(Array.prototype.slice.apply(this,arguments),"slice",Array.prototype.slice.call(arguments).join(","))},map:function(D){return this.pushStack(n.map(this,function(F,E){return D.call(F,E,F)}))},andSelf:function(){return this.add(this.prevObject)},domManip:function(J,M,L){if(this[0]){var I=(this[0].ownerDocument||this[0]).createDocumentFragment(),F=n.clean(J,(this[0].ownerDocument||this[0]),I),H=I.firstChild,D=this.length>1?I.cloneNode(true):I;if(H){for(var G=0,E=this.length;G<E;G++){L.call(K(this[G],H),G>0?D.cloneNode(true):I)}}if(F){n.each(F,y)}}return this;function K(N,O){return M&&n.nodeName(N,"table")&&n.nodeName(O,"tr")?(N.getElementsByTagName("tbody")[0]||N.appendChild(N.ownerDocument.createElement("tbody"))):N}}};n.fn.init.prototype=n.fn;function y(D,E){if(E.src){n.ajax({url:E.src,async:false,dataType:"script"})}else{n.globalEval(E.text||E.textContent||E.innerHTML||"")}if(E.parentNode){E.parentNode.removeChild(E)}}function e(){return +new Date}n.extend=n.fn.extend=function(){var I=arguments[0]||{},G=1,H=arguments.length,D=false,F;if(typeof I==="boolean"){D=I;I=arguments[1]||{};G=2}if(typeof I!=="object"&&!n.isFunction(I)){I={}}if(H==G){I=this;--G}for(;G<H;G++){if((F=arguments[G])!=null){for(var E in F){var J=I[E],K=F[E];if(I===K){continue}if(D&&K&&typeof K==="object"&&!K.nodeType){I[E]=n.extend(D,J||(K.length!=null?[]:{}),K)}else{if(K!==g){I[E]=K}}}}}return I};var b=/z-?index|font-?weight|opacity|zoom|line-?height/i,p=document.defaultView||{},r=Object.prototype.toString;n.extend({noConflict:function(D){l.$=o;if(D){l.jQuery=x}return n},isFunction:function(D){return r.call(D)==="[object Function]"},isArray:function(D){return r.call(D)==="[object Array]"},isXMLDoc:function(D){return D.documentElement&&!D.body||D.tagName&&D.ownerDocument&&!D.ownerDocument.body},globalEval:function(F){F=n.trim(F);if(F){var E=document.getElementsByTagName("head")[0]||document.documentElement,D=document.createElement("script");D.type="text/javascript";if(n.support.scriptEval){D.appendChild(document.createTextNode(F))}else{D.text=F}E.insertBefore(D,E.firstChild);E.removeChild(D)}},nodeName:function(E,D){return E.nodeName&&E.nodeName.toUpperCase()==D.toUpperCase()},each:function(F,J,E){var D,G=0,H=F.length;if(E){if(H===g){for(D in F){if(J.apply(F[D],E)===false){break}}}else{for(;G<H;){if(J.apply(F[G++],E)===false){break}}}}else{if(H===g){for(D in F){if(J.call(F[D],D,F[D])===false){break}}}else{for(var I=F[0];G<H&&J.call(I,G,I)!==false;I=F[++G]){}}}return F},prop:function(G,H,F,E,D){if(n.isFunction(H)){H=H.call(G,E)}return typeof H==="number"&&F=="curCSS"&&!b.test(D)?H+"px":H},className:{add:function(D,E){n.each((E||"").split(/\s+/),function(F,G){if(D.nodeType==1&&!n.className.has(D.className,G)){D.className+=(D.className?" ":"")+G}})},remove:function(D,E){if(D.nodeType==1){D.className=E!==g?n.grep(D.className.split(/\s+/),function(F){return !n.className.has(E,F)}).join(" "):""}},has:function(E,D){return n.inArray(D,(E.className||E).toString().split(/\s+/))>-1}},swap:function(G,F,H){var D={};for(var E in F){D[E]=G.style[E];G.style[E]=F[E]}H.call(G);for(var E in F){G.style[E]=D[E]}},css:function(F,D,H){if(D=="width"||D=="height"){var J,E={position:"absolute",visibility:"hidden",display:"block"},I=D=="width"?["Left","Right"]:["Top","Bottom"];function G(){J=D=="width"?F.offsetWidth:F.offsetHeight;var L=0,K=0;n.each(I,function(){L+=parseFloat(n.curCSS(F,"padding"+this,true))||0;K+=parseFloat(n.curCSS(F,"border"+this+"Width",true))||0});J-=Math.round(L+K)}if(n(F).is(":visible")){G()}else{n.swap(F,E,G)}return Math.max(0,J)}return n.curCSS(F,D,H)},curCSS:function(H,E,F){var K,D=H.style;if(E=="opacity"&&!n.support.opacity){K=n.attr(D,"opacity");return K==""?"1":K}if(E.match(/float/i)){E=v}if(!F&&D&&D[E]){K=D[E]}else{if(p.getComputedStyle){if(E.match(/float/i)){E="float"}E=E.replace(/([A-Z])/g,"-$1").toLowerCase();var L=p.getComputedStyle(H,null);if(L){K=L.getPropertyValue(E)}if(E=="opacity"&&K==""){K="1"}}else{if(H.currentStyle){var I=E.replace(/\-(\w)/g,function(M,N){return N.toUpperCase()});K=H.currentStyle[E]||H.currentStyle[I];if(!/^\d+(px)?$/i.test(K)&&/^\d/.test(K)){var G=D.left,J=H.runtimeStyle.left;H.runtimeStyle.left=H.currentStyle.left;D.left=K||0;K=D.pixelLeft+"px";D.left=G;H.runtimeStyle.left=J}}}}return K},clean:function(E,J,H){J=J||document;if(typeof J.createElement==="undefined"){J=J.ownerDocument||J[0]&&J[0].ownerDocument||document}if(!H&&E.length===1&&typeof E[0]==="string"){var G=/^<(\w+)\s*\/?>$/.exec(E[0]);if(G){return[J.createElement(G[1])]}}var F=[],D=[],K=J.createElement("div");n.each(E,function(O,Q){if(typeof Q==="number"){Q+=""}if(!Q){return}if(typeof Q==="string"){Q=Q.replace(/(<(\w+)[^>]*?)\/>/g,function(S,T,R){return R.match(/^(abbr|br|col|img|input|link|meta|param|hr|area|embed)$/i)?S:T+"></"+R+">"});var N=n.trim(Q).toLowerCase();var P=!N.indexOf("<opt")&&[1,"<select multiple='multiple'>","</select>"]||!N.indexOf("<leg")&&[1,"<fieldset>","</fieldset>"]||N.match(/^<(thead|tbody|tfoot|colg|cap)/)&&[1,"<table>","</table>"]||!N.indexOf("<tr")&&[2,"<table><tbody>","</tbody></table>"]||(!N.indexOf("<td")||!N.indexOf("<th"))&&[3,"<table><tbody><tr>","</tr></tbody></table>"]||!N.indexOf("<col")&&[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"]||!n.support.htmlSerialize&&[1,"div<div>","</div>"]||[0,"",""];K.innerHTML=P[1]+Q+P[2];while(P[0]--){K=K.lastChild}if(!n.support.tbody){var M=!N.indexOf("<table")&&N.indexOf("<tbody")<0?K.firstChild&&K.firstChild.childNodes:P[1]=="<table>"&&N.indexOf("<tbody")<0?K.childNodes:[];for(var L=M.length-1;L>=0;--L){if(n.nodeName(M[L],"tbody")&&!M[L].childNodes.length){M[L].parentNode.removeChild(M[L])}}}if(!n.support.leadingWhitespace&&/^\s/.test(Q)){K.insertBefore(J.createTextNode(Q.match(/^\s*/)[0]),K.firstChild)}Q=n.makeArray(K.childNodes)}if(Q.nodeType){F.push(Q)}else{F=n.merge(F,Q)}});if(H){for(var I=0;F[I];I++){if(n.nodeName(F[I],"script")&&(!F[I].type||F[I].type.toLowerCase()==="text/javascript")){D.push(F[I].parentNode?F[I].parentNode.removeChild(F[I]):F[I])}else{if(F[I].nodeType===1){F.splice.apply(F,[I+1,0].concat(n.makeArray(F[I].getElementsByTagName("script"))))}H.appendChild(F[I])}}return D}return F},attr:function(I,F,J){if(!I||I.nodeType==3||I.nodeType==8){return g}var G=!n.isXMLDoc(I),K=J!==g;F=G&&n.props[F]||F;if(I.tagName){var E=/href|src|style/.test(F);if(F=="selected"&&I.parentNode){I.parentNode.selectedIndex}if(F in I&&G&&!E){if(K){if(F=="type"&&n.nodeName(I,"input")&&I.parentNode){throw"type property can't be changed"}I[F]=J}if(n.nodeName(I,"form")&&I.getAttributeNode(F)){return I.getAttributeNode(F).nodeValue}if(F=="tabIndex"){var H=I.getAttributeNode("tabIndex");return H&&H.specified?H.value:I.nodeName.match(/^(a|area|button|input|object|select|textarea)$/i)?0:g}return I[F]}if(!n.support.style&&G&&F=="style"){return n.attr(I.style,"cssText",J)}if(K){I.setAttribute(F,""+J)}var D=!n.support.hrefNormalized&&G&&E?I.getAttribute(F,2):I.getAttribute(F);return D===null?g:D}if(!n.support.opacity&&F=="opacity"){if(K){I.zoom=1;I.filter=(I.filter||"").replace(/alpha\([^)]*\)/,"")+(parseInt(J)+""=="NaN"?"":"alpha(opacity="+J*100+")")}return I.filter&&I.filter.indexOf("opacity=")>=0?(parseFloat(I.filter.match(/opacity=([^)]*)/)[1])/100)+"":""}F=F.replace(/-([a-z])/ig,function(L,M){return M.toUpperCase()});if(K){I[F]=J}return I[F]},trim:function(D){return(D||"").replace(/^\s+|\s+$/g,"")},makeArray:function(F){var D=[];if(F!=null){var E=F.length;if(E==null||typeof F==="string"||n.isFunction(F)||F.setInterval){D[0]=F}else{while(E){D[--E]=F[E]}}}return D},inArray:function(F,G){for(var D=0,E=G.length;D<E;D++){if(G[D]===F){return D}}return -1},merge:function(G,D){var E=0,F,H=G.length;if(!n.support.getAll){while((F=D[E++])!=null){if(F.nodeType!=8){G[H++]=F}}}else{while((F=D[E++])!=null){G[H++]=F}}return G},unique:function(J){var E=[],D={};try{for(var F=0,G=J.length;F<G;F++){var I=n.data(J[F]);if(!D[I]){D[I]=true;E.push(J[F])}}}catch(H){E=J}return E},grep:function(E,I,D){var F=[];for(var G=0,H=E.length;G<H;G++){if(!D!=!I(E[G],G)){F.push(E[G])}}return F},map:function(D,I){var E=[];for(var F=0,G=D.length;F<G;F++){var H=I(D[F],F);if(H!=null){E[E.length]=H}}return E.concat.apply([],E)}});var B=navigator.userAgent.toLowerCase();n.browser={version:(B.match(/.+(?:rv|it|ra|ie)[\/: ]([\d.]+)/)||[0,"0"])[1],safari:/webkit/.test(B),opera:/opera/.test(B),msie:/msie/.test(B)&&!/opera/.test(B),mozilla:/mozilla/.test(B)&&!/(compatible|webkit)/.test(B)};n.each({parent:function(D){return D.parentNode},parents:function(D){return n.dir(D,"parentNode")},next:function(D){return n.nth(D,2,"nextSibling")},prev:function(D){return n.nth(D,2,"previousSibling")},nextAll:function(D){return n.dir(D,"nextSibling")},prevAll:function(D){return n.dir(D,"previousSibling")},siblings:function(D){return n.sibling(D.parentNode.firstChild,D)},children:function(D){return n.sibling(D.firstChild)},contents:function(D){return n.nodeName(D,"iframe")?D.contentDocument||D.contentWindow.document:n.makeArray(D.childNodes)}},function(D,E){n.fn[D]=function(F){var G=n.map(this,E);if(F&&typeof F=="string"){G=n.multiFilter(F,G)}return this.pushStack(n.unique(G),D,F)}});n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(D,E){n.fn[D]=function(){var F=arguments;return this.each(function(){for(var G=0,H=F.length;G<H;G++){n(F[G])[E](this)}})}});n.each({removeAttr:function(D){n.attr(this,D,"");if(this.nodeType==1){this.removeAttribute(D)}},addClass:function(D){n.className.add(this,D)},removeClass:function(D){n.className.remove(this,D)},toggleClass:function(E,D){if(typeof D!=="boolean"){D=!n.className.has(this,E)}n.className[D?"add":"remove"](this,E)},remove:function(D){if(!D||n.filter(D,[this]).length){n("*",this).add([this]).each(function(){n.event.remove(this);n.removeData(this)});if(this.parentNode){this.parentNode.removeChild(this)}}},empty:function(){n(">*",this).remove();while(this.firstChild){this.removeChild(this.firstChild)}}},function(D,E){n.fn[D]=function(){return this.each(E,arguments)}});function j(D,E){return D[0]&&parseInt(n.curCSS(D[0],E,true),10)||0}var h="jQuery"+e(),u=0,z={};n.extend({cache:{},data:function(E,D,F){E=E==l?z:E;var G=E[h];if(!G){G=E[h]=++u}if(D&&!n.cache[G]){n.cache[G]={}}if(F!==g){n.cache[G][D]=F}return D?n.cache[G][D]:G},removeData:function(E,D){E=E==l?z:E;var G=E[h];if(D){if(n.cache[G]){delete n.cache[G][D];D="";for(D in n.cache[G]){break}if(!D){n.removeData(E)}}}else{try{delete E[h]}catch(F){if(E.removeAttribute){E.removeAttribute(h)}}delete n.cache[G]}},queue:function(E,D,G){if(E){D=(D||"fx")+"queue";var F=n.data(E,D);if(!F||n.isArray(G)){F=n.data(E,D,n.makeArray(G))}else{if(G){F.push(G)}}}return F},dequeue:function(G,F){var D=n.queue(G,F),E=D.shift();if(!F||F==="fx"){E=D[0]}if(E!==g){E.call(G)}}});n.fn.extend({data:function(D,F){var G=D.split(".");G[1]=G[1]?"."+G[1]:"";if(F===g){var E=this.triggerHandler("getData"+G[1]+"!",[G[0]]);if(E===g&&this.length){E=n.data(this[0],D)}return E===g&&G[1]?this.data(G[0]):E}else{return this.trigger("setData"+G[1]+"!",[G[0],F]).each(function(){n.data(this,D,F)})}},removeData:function(D){return this.each(function(){n.removeData(this,D)})},queue:function(D,E){if(typeof D!=="string"){E=D;D="fx"}if(E===g){return n.queue(this[0],D)}return this.each(function(){var F=n.queue(this,D,E);if(D=="fx"&&F.length==1){F[0].call(this)}})},dequeue:function(D){return this.each(function(){n.dequeue(this,D)})}});
+/*
+ * Sizzle CSS Selector Engine - v0.9.1
+ * Copyright 2009, The Dojo Foundation
+ * Released under the MIT, BSD, and GPL Licenses.
+ * More information: http://sizzlejs.com/
+ */
+(function(){var N=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^[\]]*\]|[^[\]]+)+\]|\\.|[^ >+~,(\[]+)+|[>+~])(\s*,\s*)?/g,I=0,F=Object.prototype.toString;var E=function(ae,S,aa,V){aa=aa||[];S=S||document;if(S.nodeType!==1&&S.nodeType!==9){return[]}if(!ae||typeof ae!=="string"){return aa}var ab=[],ac,Y,ah,ag,Z,R,Q=true;N.lastIndex=0;while((ac=N.exec(ae))!==null){ab.push(ac[1]);if(ac[2]){R=RegExp.rightContext;break}}if(ab.length>1&&G.match.POS.exec(ae)){if(ab.length===2&&G.relative[ab[0]]){var U="",X;while((X=G.match.POS.exec(ae))){U+=X[0];ae=ae.replace(G.match.POS,"")}Y=E.filter(U,E(/\s$/.test(ae)?ae+"*":ae,S))}else{Y=G.relative[ab[0]]?[S]:E(ab.shift(),S);while(ab.length){var P=[];ae=ab.shift();if(G.relative[ae]){ae+=ab.shift()}for(var af=0,ad=Y.length;af<ad;af++){E(ae,Y[af],P)}Y=P}}}else{var ai=V?{expr:ab.pop(),set:D(V)}:E.find(ab.pop(),ab.length===1&&S.parentNode?S.parentNode:S);Y=E.filter(ai.expr,ai.set);if(ab.length>0){ah=D(Y)}else{Q=false}while(ab.length){var T=ab.pop(),W=T;if(!G.relative[T]){T=""}else{W=ab.pop()}if(W==null){W=S}G.relative[T](ah,W,M(S))}}if(!ah){ah=Y}if(!ah){throw"Syntax error, unrecognized expression: "+(T||ae)}if(F.call(ah)==="[object Array]"){if(!Q){aa.push.apply(aa,ah)}else{if(S.nodeType===1){for(var af=0;ah[af]!=null;af++){if(ah[af]&&(ah[af]===true||ah[af].nodeType===1&&H(S,ah[af]))){aa.push(Y[af])}}}else{for(var af=0;ah[af]!=null;af++){if(ah[af]&&ah[af].nodeType===1){aa.push(Y[af])}}}}}else{D(ah,aa)}if(R){E(R,S,aa,V)}return aa};E.matches=function(P,Q){return E(P,null,null,Q)};E.find=function(V,S){var W,Q;if(!V){return[]}for(var R=0,P=G.order.length;R<P;R++){var T=G.order[R],Q;if((Q=G.match[T].exec(V))){var U=RegExp.leftContext;if(U.substr(U.length-1)!=="\\"){Q[1]=(Q[1]||"").replace(/\\/g,"");W=G.find[T](Q,S);if(W!=null){V=V.replace(G.match[T],"");break}}}}if(!W){W=S.getElementsByTagName("*")}return{set:W,expr:V}};E.filter=function(S,ac,ad,T){var Q=S,Y=[],ah=ac,V,ab;while(S&&ac.length){for(var U in G.filter){if((V=G.match[U].exec(S))!=null){var Z=G.filter[U],R=null,X=0,aa,ag;ab=false;if(ah==Y){Y=[]}if(G.preFilter[U]){V=G.preFilter[U](V,ah,ad,Y,T);if(!V){ab=aa=true}else{if(V===true){continue}else{if(V[0]===true){R=[];var W=null,af;for(var ae=0;(af=ah[ae])!==g;ae++){if(af&&W!==af){R.push(af);W=af}}}}}}if(V){for(var ae=0;(ag=ah[ae])!==g;ae++){if(ag){if(R&&ag!=R[X]){X++}aa=Z(ag,V,X,R);var P=T^!!aa;if(ad&&aa!=null){if(P){ab=true}else{ah[ae]=false}}else{if(P){Y.push(ag);ab=true}}}}}if(aa!==g){if(!ad){ah=Y}S=S.replace(G.match[U],"");if(!ab){return[]}break}}}S=S.replace(/\s*,\s*/,"");if(S==Q){if(ab==null){throw"Syntax error, unrecognized expression: "+S}else{break}}Q=S}return ah};var G=E.selectors={order:["ID","NAME","TAG"],match:{ID:/#((?:[\w\u00c0-\uFFFF_-]|\\.)+)/,CLASS:/\.((?:[\w\u00c0-\uFFFF_-]|\\.)+)/,NAME:/\[name=['"]*((?:[\w\u00c0-\uFFFF_-]|\\.)+)['"]*\]/,ATTR:/\[\s*((?:[\w\u00c0-\uFFFF_-]|\\.)+)\s*(?:(\S?=)\s*(['"]*)(.*?)\3|)\s*\]/,TAG:/^((?:[\w\u00c0-\uFFFF\*_-]|\\.)+)/,CHILD:/:(only|nth|last|first)-child(?:\((even|odd|[\dn+-]*)\))?/,POS:/:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^-]|$)/,PSEUDO:/:((?:[\w\u00c0-\uFFFF_-]|\\.)+)(?:\((['"]*)((?:\([^\)]+\)|[^\2\(\)]*)+)\2\))?/},attrMap:{"class":"className","for":"htmlFor"},attrHandle:{href:function(P){return P.getAttribute("href")}},relative:{"+":function(T,Q){for(var R=0,P=T.length;R<P;R++){var S=T[R];if(S){var U=S.previousSibling;while(U&&U.nodeType!==1){U=U.previousSibling}T[R]=typeof Q==="string"?U||false:U===Q}}if(typeof Q==="string"){E.filter(Q,T,true)}},">":function(U,Q,V){if(typeof Q==="string"&&!/\W/.test(Q)){Q=V?Q:Q.toUpperCase();for(var R=0,P=U.length;R<P;R++){var T=U[R];if(T){var S=T.parentNode;U[R]=S.nodeName===Q?S:false}}}else{for(var R=0,P=U.length;R<P;R++){var T=U[R];if(T){U[R]=typeof Q==="string"?T.parentNode:T.parentNode===Q}}if(typeof Q==="string"){E.filter(Q,U,true)}}},"":function(S,Q,U){var R="done"+(I++),P=O;if(!Q.match(/\W/)){var T=Q=U?Q:Q.toUpperCase();P=L}P("parentNode",Q,R,S,T,U)},"~":function(S,Q,U){var R="done"+(I++),P=O;if(typeof Q==="string"&&!Q.match(/\W/)){var T=Q=U?Q:Q.toUpperCase();P=L}P("previousSibling",Q,R,S,T,U)}},find:{ID:function(Q,R){if(R.getElementById){var P=R.getElementById(Q[1]);return P?[P]:[]}},NAME:function(P,Q){return Q.getElementsByName?Q.getElementsByName(P[1]):null},TAG:function(P,Q){return Q.getElementsByTagName(P[1])}},preFilter:{CLASS:function(S,Q,R,P,U){S=" "+S[1].replace(/\\/g,"")+" ";for(var T=0;Q[T];T++){if(U^(" "+Q[T].className+" ").indexOf(S)>=0){if(!R){P.push(Q[T])}}else{if(R){Q[T]=false}}}return false},ID:function(P){return P[1].replace(/\\/g,"")},TAG:function(Q,P){for(var R=0;!P[R];R++){}return M(P[R])?Q[1]:Q[1].toUpperCase()},CHILD:function(P){if(P[1]=="nth"){var Q=/(-?)(\d*)n((?:\+|-)?\d*)/.exec(P[2]=="even"&&"2n"||P[2]=="odd"&&"2n+1"||!/\D/.test(P[2])&&"0n+"+P[2]||P[2]);P[2]=(Q[1]+(Q[2]||1))-0;P[3]=Q[3]-0}P[0]="done"+(I++);return P},ATTR:function(Q){var P=Q[1];if(G.attrMap[P]){Q[1]=G.attrMap[P]}if(Q[2]==="~="){Q[4]=" "+Q[4]+" "}return Q},PSEUDO:function(T,Q,R,P,U){if(T[1]==="not"){if(T[3].match(N).length>1){T[3]=E(T[3],null,null,Q)}else{var S=E.filter(T[3],Q,R,true^U);if(!R){P.push.apply(P,S)}return false}}else{if(G.match.POS.test(T[0])){return true}}return T},POS:function(P){P.unshift(true);return P}},filters:{enabled:function(P){return P.disabled===false&&P.type!=="hidden"},disabled:function(P){return P.disabled===true},checked:function(P){return P.checked===true},selected:function(P){P.parentNode.selectedIndex;return P.selected===true},parent:function(P){return !!P.firstChild},empty:function(P){return !P.firstChild},has:function(R,Q,P){return !!E(P[3],R).length},header:function(P){return/h\d/i.test(P.nodeName)},text:function(P){return"text"===P.type},radio:function(P){return"radio"===P.type},checkbox:function(P){return"checkbox"===P.type},file:function(P){return"file"===P.type},password:function(P){return"password"===P.type},submit:function(P){return"submit"===P.type},image:function(P){return"image"===P.type},reset:function(P){return"reset"===P.type},button:function(P){return"button"===P.type||P.nodeName.toUpperCase()==="BUTTON"},input:function(P){return/input|select|textarea|button/i.test(P.nodeName)}},setFilters:{first:function(Q,P){return P===0},last:function(R,Q,P,S){return Q===S.length-1},even:function(Q,P){return P%2===0},odd:function(Q,P){return P%2===1},lt:function(R,Q,P){return Q<P[3]-0},gt:function(R,Q,P){return Q>P[3]-0},nth:function(R,Q,P){return P[3]-0==Q},eq:function(R,Q,P){return P[3]-0==Q}},filter:{CHILD:function(P,S){var V=S[1],W=P.parentNode;var U="child"+W.childNodes.length;if(W&&(!W[U]||!P.nodeIndex)){var T=1;for(var Q=W.firstChild;Q;Q=Q.nextSibling){if(Q.nodeType==1){Q.nodeIndex=T++}}W[U]=T-1}if(V=="first"){return P.nodeIndex==1}else{if(V=="last"){return P.nodeIndex==W[U]}else{if(V=="only"){return W[U]==1}else{if(V=="nth"){var Y=false,R=S[2],X=S[3];if(R==1&&X==0){return true}if(R==0){if(P.nodeIndex==X){Y=true}}else{if((P.nodeIndex-X)%R==0&&(P.nodeIndex-X)/R>=0){Y=true}}return Y}}}}},PSEUDO:function(V,R,S,W){var Q=R[1],T=G.filters[Q];if(T){return T(V,S,R,W)}else{if(Q==="contains"){return(V.textContent||V.innerText||"").indexOf(R[3])>=0}else{if(Q==="not"){var U=R[3];for(var S=0,P=U.length;S<P;S++){if(U[S]===V){return false}}return true}}}},ID:function(Q,P){return Q.nodeType===1&&Q.getAttribute("id")===P},TAG:function(Q,P){return(P==="*"&&Q.nodeType===1)||Q.nodeName===P},CLASS:function(Q,P){return P.test(Q.className)},ATTR:function(T,R){var P=G.attrHandle[R[1]]?G.attrHandle[R[1]](T):T[R[1]]||T.getAttribute(R[1]),U=P+"",S=R[2],Q=R[4];return P==null?false:S==="="?U===Q:S==="*="?U.indexOf(Q)>=0:S==="~="?(" "+U+" ").indexOf(Q)>=0:!R[4]?P:S==="!="?U!=Q:S==="^="?U.indexOf(Q)===0:S==="$="?U.substr(U.length-Q.length)===Q:S==="|="?U===Q||U.substr(0,Q.length+1)===Q+"-":false},POS:function(T,Q,R,U){var P=Q[2],S=G.setFilters[P];if(S){return S(T,R,Q,U)}}}};for(var K in G.match){G.match[K]=RegExp(G.match[K].source+/(?![^\[]*\])(?![^\(]*\))/.source)}var D=function(Q,P){Q=Array.prototype.slice.call(Q);if(P){P.push.apply(P,Q);return P}return Q};try{Array.prototype.slice.call(document.documentElement.childNodes)}catch(J){D=function(T,S){var Q=S||[];if(F.call(T)==="[object Array]"){Array.prototype.push.apply(Q,T)}else{if(typeof T.length==="number"){for(var R=0,P=T.length;R<P;R++){Q.push(T[R])}}else{for(var R=0;T[R];R++){Q.push(T[R])}}}return Q}}(function(){var Q=document.createElement("form"),R="script"+(new Date).getTime();Q.innerHTML="<input name='"+R+"'/>";var P=document.documentElement;P.insertBefore(Q,P.firstChild);if(!!document.getElementById(R)){G.find.ID=function(T,U){if(U.getElementById){var S=U.getElementById(T[1]);return S?S.id===T[1]||S.getAttributeNode&&S.getAttributeNode("id").nodeValue===T[1]?[S]:g:[]}};G.filter.ID=function(U,S){var T=U.getAttributeNode&&U.getAttributeNode("id");return U.nodeType===1&&T&&T.nodeValue===S}}P.removeChild(Q)})();(function(){var P=document.createElement("div");P.appendChild(document.createComment(""));if(P.getElementsByTagName("*").length>0){G.find.TAG=function(Q,U){var T=U.getElementsByTagName(Q[1]);if(Q[1]==="*"){var S=[];for(var R=0;T[R];R++){if(T[R].nodeType===1){S.push(T[R])}}T=S}return T}}P.innerHTML="<a href='#'></a>";if(P.firstChild.getAttribute("href")!=="#"){G.attrHandle.href=function(Q){return Q.getAttribute("href",2)}}})();if(document.querySelectorAll){(function(){var P=E;E=function(T,S,Q,R){S=S||document;if(!R&&S.nodeType===9){try{return D(S.querySelectorAll(T),Q)}catch(U){}}return P(T,S,Q,R)};E.find=P.find;E.filter=P.filter;E.selectors=P.selectors;E.matches=P.matches})()}if(document.documentElement.getElementsByClassName){G.order.splice(1,0,"CLASS");G.find.CLASS=function(P,Q){return Q.getElementsByClassName(P[1])}}function L(Q,W,V,Z,X,Y){for(var T=0,R=Z.length;T<R;T++){var P=Z[T];if(P){P=P[Q];var U=false;while(P&&P.nodeType){var S=P[V];if(S){U=Z[S];break}if(P.nodeType===1&&!Y){P[V]=T}if(P.nodeName===W){U=P;break}P=P[Q]}Z[T]=U}}}function O(Q,V,U,Y,W,X){for(var S=0,R=Y.length;S<R;S++){var P=Y[S];if(P){P=P[Q];var T=false;while(P&&P.nodeType){if(P[U]){T=Y[P[U]];break}if(P.nodeType===1){if(!X){P[U]=S}if(typeof V!=="string"){if(P===V){T=true;break}}else{if(E.filter(V,[P]).length>0){T=P;break}}}P=P[Q]}Y[S]=T}}}var H=document.compareDocumentPosition?function(Q,P){return Q.compareDocumentPosition(P)&16}:function(Q,P){return Q!==P&&(Q.contains?Q.contains(P):true)};var M=function(P){return P.documentElement&&!P.body||P.tagName&&P.ownerDocument&&!P.ownerDocument.body};n.find=E;n.filter=E.filter;n.expr=E.selectors;n.expr[":"]=n.expr.filters;E.selectors.filters.hidden=function(P){return"hidden"===P.type||n.css(P,"display")==="none"||n.css(P,"visibility")==="hidden"};E.selectors.filters.visible=function(P){return"hidden"!==P.type&&n.css(P,"display")!=="none"&&n.css(P,"visibility")!=="hidden"};E.selectors.filters.animated=function(P){return n.grep(n.timers,function(Q){return P===Q.elem}).length};n.multiFilter=function(R,P,Q){if(Q){R=":not("+R+")"}return E.matches(R,P)};n.dir=function(R,Q){var P=[],S=R[Q];while(S&&S!=document){if(S.nodeType==1){P.push(S)}S=S[Q]}return P};n.nth=function(T,P,R,S){P=P||1;var Q=0;for(;T;T=T[R]){if(T.nodeType==1&&++Q==P){break}}return T};n.sibling=function(R,Q){var P=[];for(;R;R=R.nextSibling){if(R.nodeType==1&&R!=Q){P.push(R)}}return P};return;l.Sizzle=E})();n.event={add:function(H,E,G,J){if(H.nodeType==3||H.nodeType==8){return}if(H.setInterval&&H!=l){H=l}if(!G.guid){G.guid=this.guid++}if(J!==g){var F=G;G=this.proxy(F);G.data=J}var D=n.data(H,"events")||n.data(H,"events",{}),I=n.data(H,"handle")||n.data(H,"handle",function(){return typeof n!=="undefined"&&!n.event.triggered?n.event.handle.apply(arguments.callee.elem,arguments):g});I.elem=H;n.each(E.split(/\s+/),function(L,M){var N=M.split(".");M=N.shift();G.type=N.slice().sort().join(".");var K=D[M];if(n.event.specialAll[M]){n.event.specialAll[M].setup.call(H,J,N)}if(!K){K=D[M]={};if(!n.event.special[M]||n.event.special[M].setup.call(H,J,N)===false){if(H.addEventListener){H.addEventListener(M,I,false)}else{if(H.attachEvent){H.attachEvent("on"+M,I)}}}}K[G.guid]=G;n.event.global[M]=true});H=null},guid:1,global:{},remove:function(J,G,I){if(J.nodeType==3||J.nodeType==8){return}var F=n.data(J,"events"),E,D;if(F){if(G===g||(typeof G==="string"&&G.charAt(0)==".")){for(var H in F){this.remove(J,H+(G||""))}}else{if(G.type){I=G.handler;G=G.type}n.each(G.split(/\s+/),function(L,N){var P=N.split(".");N=P.shift();var M=RegExp("(^|\\.)"+P.slice().sort().join(".*\\.")+"(\\.|$)");if(F[N]){if(I){delete F[N][I.guid]}else{for(var O in F[N]){if(M.test(F[N][O].type)){delete F[N][O]}}}if(n.event.specialAll[N]){n.event.specialAll[N].teardown.call(J,P)}for(E in F[N]){break}if(!E){if(!n.event.special[N]||n.event.special[N].teardown.call(J,P)===false){if(J.removeEventListener){J.removeEventListener(N,n.data(J,"handle"),false)}else{if(J.detachEvent){J.detachEvent("on"+N,n.data(J,"handle"))}}}E=null;delete F[N]}}})}for(E in F){break}if(!E){var K=n.data(J,"handle");if(K){K.elem=null}n.removeData(J,"events");n.removeData(J,"handle")}}},trigger:function(H,J,G,D){var F=H.type||H;if(!D){H=typeof H==="object"?H[h]?H:n.extend(n.Event(F),H):n.Event(F);if(F.indexOf("!")>=0){H.type=F=F.slice(0,-1);H.exclusive=true}if(!G){H.stopPropagation();if(this.global[F]){n.each(n.cache,function(){if(this.events&&this.events[F]){n.event.trigger(H,J,this.handle.elem)}})}}if(!G||G.nodeType==3||G.nodeType==8){return g}H.result=g;H.target=G;J=n.makeArray(J);J.unshift(H)}H.currentTarget=G;var I=n.data(G,"handle");if(I){I.apply(G,J)}if((!G[F]||(n.nodeName(G,"a")&&F=="click"))&&G["on"+F]&&G["on"+F].apply(G,J)===false){H.result=false}if(!D&&G[F]&&!H.isDefaultPrevented()&&!(n.nodeName(G,"a")&&F=="click")){this.triggered=true;try{G[F]()}catch(K){}}this.triggered=false;if(!H.isPropagationStopped()){var E=G.parentNode||G.ownerDocument;if(E){n.event.trigger(H,J,E,true)}}},handle:function(J){var I,D;J=arguments[0]=n.event.fix(J||l.event);var K=J.type.split(".");J.type=K.shift();I=!K.length&&!J.exclusive;var H=RegExp("(^|\\.)"+K.slice().sort().join(".*\\.")+"(\\.|$)");D=(n.data(this,"events")||{})[J.type];for(var F in D){var G=D[F];if(I||H.test(G.type)){J.handler=G;J.data=G.data;var E=G.apply(this,arguments);if(E!==g){J.result=E;if(E===false){J.preventDefault();J.stopPropagation()}}if(J.isImmediatePropagationStopped()){break}}}},props:"altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode metaKey newValue originalTarget pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "),fix:function(G){if(G[h]){return G}var E=G;G=n.Event(E);for(var F=this.props.length,I;F;){I=this.props[--F];G[I]=E[I]}if(!G.target){G.target=G.srcElement||document}if(G.target.nodeType==3){G.target=G.target.parentNode}if(!G.relatedTarget&&G.fromElement){G.relatedTarget=G.fromElement==G.target?G.toElement:G.fromElement}if(G.pageX==null&&G.clientX!=null){var H=document.documentElement,D=document.body;G.pageX=G.clientX+(H&&H.scrollLeft||D&&D.scrollLeft||0)-(H.clientLeft||0);G.pageY=G.clientY+(H&&H.scrollTop||D&&D.scrollTop||0)-(H.clientTop||0)}if(!G.which&&((G.charCode||G.charCode===0)?G.charCode:G.keyCode)){G.which=G.charCode||G.keyCode}if(!G.metaKey&&G.ctrlKey){G.metaKey=G.ctrlKey}if(!G.which&&G.button){G.which=(G.button&1?1:(G.button&2?3:(G.button&4?2:0)))}return G},proxy:function(E,D){D=D||function(){return E.apply(this,arguments)};D.guid=E.guid=E.guid||D.guid||this.guid++;return D},special:{ready:{setup:A,teardown:function(){}}},specialAll:{live:{setup:function(D,E){n.event.add(this,E[0],c)},teardown:function(F){if(F.length){var D=0,E=RegExp("(^|\\.)"+F[0]+"(\\.|$)");n.each((n.data(this,"events").live||{}),function(){if(E.test(this.type)){D++}});if(D<1){n.event.remove(this,F[0],c)}}}}}};n.Event=function(D){if(!this.preventDefault){return new n.Event(D)}if(D&&D.type){this.originalEvent=D;this.type=D.type;this.timeStamp=D.timeStamp}else{this.type=D}if(!this.timeStamp){this.timeStamp=e()}this[h]=true};function k(){return false}function t(){return true}n.Event.prototype={preventDefault:function(){this.isDefaultPrevented=t;var D=this.originalEvent;if(!D){return}if(D.preventDefault){D.preventDefault()}D.returnValue=false},stopPropagation:function(){this.isPropagationStopped=t;var D=this.originalEvent;if(!D){return}if(D.stopPropagation){D.stopPropagation()}D.cancelBubble=true},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=t;this.stopPropagation()},isDefaultPrevented:k,isPropagationStopped:k,isImmediatePropagationStopped:k};var a=function(E){var D=E.relatedTarget;while(D&&D!=this){try{D=D.parentNode}catch(F){D=this}}if(D!=this){E.type=E.data;n.event.handle.apply(this,arguments)}};n.each({mouseover:"mouseenter",mouseout:"mouseleave"},function(E,D){n.event.special[D]={setup:function(){n.event.add(this,E,a,D)},teardown:function(){n.event.remove(this,E,a)}}});n.fn.extend({bind:function(E,F,D){return E=="unload"?this.one(E,F,D):this.each(function(){n.event.add(this,E,D||F,D&&F)})},one:function(F,G,E){var D=n.event.proxy(E||G,function(H){n(this).unbind(H,D);return(E||G).apply(this,arguments)});return this.each(function(){n.event.add(this,F,D,E&&G)})},unbind:function(E,D){return this.each(function(){n.event.remove(this,E,D)})},trigger:function(D,E){return this.each(function(){n.event.trigger(D,E,this)})},triggerHandler:function(D,F){if(this[0]){var E=n.Event(D);E.preventDefault();E.stopPropagation();n.event.trigger(E,F,this[0]);return E.result}},toggle:function(F){var D=arguments,E=1;while(E<D.length){n.event.proxy(F,D[E++])}return this.click(n.event.proxy(F,function(G){this.lastToggle=(this.lastToggle||0)%E;G.preventDefault();return D[this.lastToggle++].apply(this,arguments)||false}))},hover:function(D,E){return this.mouseenter(D).mouseleave(E)},ready:function(D){A();if(n.isReady){D.call(document,n)}else{n.readyList.push(D)}return this},live:function(F,E){var D=n.event.proxy(E);D.guid+=this.selector+F;n(document).bind(i(F,this.selector),this.selector,D);return this},die:function(E,D){n(document).unbind(i(E,this.selector),D?{guid:D.guid+this.selector+E}:null);return this}});function c(G){var D=RegExp("(^|\\.)"+G.type+"(\\.|$)"),F=true,E=[];n.each(n.data(this,"events").live||[],function(H,I){if(D.test(I.type)){var J=n(G.target).closest(I.data)[0];if(J){E.push({elem:J,fn:I})}}});n.each(E,function(){if(!G.isImmediatePropagationStopped()&&this.fn.call(this.elem,G,this.fn.data)===false){F=false}});return F}function i(E,D){return["live",E,D.replace(/\./g,"`").replace(/ /g,"|")].join(".")}n.extend({isReady:false,readyList:[],ready:function(){if(!n.isReady){n.isReady=true;if(n.readyList){n.each(n.readyList,function(){this.call(document,n)});n.readyList=null}n(document).triggerHandler("ready")}}});var w=false;function A(){if(w){return}w=true;if(document.addEventListener){document.addEventListener("DOMContentLoaded",function(){document.removeEventListener("DOMContentLoaded",arguments.callee,false);n.ready()},false)}else{if(document.attachEvent){document.attachEvent("onreadystatechange",function(){if(document.readyState==="complete"){document.detachEvent("onreadystatechange",arguments.callee);n.ready()}});if(document.documentElement.doScroll&&!l.frameElement){(function(){if(n.isReady){return}try{document.documentElement.doScroll("left")}catch(D){setTimeout(arguments.callee,0);return}n.ready()})()}}}n.event.add(l,"load",n.ready)}n.each(("blur,focus,load,resize,scroll,unload,click,dblclick,mousedown,mouseup,mousemove,mouseover,mouseout,mouseenter,mouseleave,change,select,submit,keydown,keypress,keyup,error").split(","),function(E,D){n.fn[D]=function(F){return F?this.bind(D,F):this.trigger(D)}});n(l).bind("unload",function(){for(var D in n.cache){if(D!=1&&n.cache[D].handle){n.event.remove(n.cache[D].handle.elem)}}});(function(){n.support={};var E=document.documentElement,F=document.createElement("script"),J=document.createElement("div"),I="script"+(new Date).getTime();J.style.display="none";J.innerHTML=' <link/><table></table><a href="/a" style="color:red;float:left;opacity:.5;">a</a><select><option>text</option></select><object><param/></object>';var G=J.getElementsByTagName("*"),D=J.getElementsByTagName("a")[0];if(!G||!G.length||!D){return}n.support={leadingWhitespace:J.firstChild.nodeType==3,tbody:!J.getElementsByTagName("tbody").length,objectAll:!!J.getElementsByTagName("object")[0].getElementsByTagName("*").length,htmlSerialize:!!J.getElementsByTagName("link").length,style:/red/.test(D.getAttribute("style")),hrefNormalized:D.getAttribute("href")==="/a",opacity:D.style.opacity==="0.5",cssFloat:!!D.style.cssFloat,scriptEval:false,noCloneEvent:true,boxModel:null};F.type="text/javascript";try{F.appendChild(document.createTextNode("window."+I+"=1;"))}catch(H){}E.insertBefore(F,E.firstChild);if(l[I]){n.support.scriptEval=true;delete l[I]}E.removeChild(F);if(J.attachEvent&&J.fireEvent){J.attachEvent("onclick",function(){n.support.noCloneEvent=false;J.detachEvent("onclick",arguments.callee)});J.cloneNode(true).fireEvent("onclick")}n(function(){var K=document.createElement("div");K.style.width="1px";K.style.paddingLeft="1px";document.body.appendChild(K);n.boxModel=n.support.boxModel=K.offsetWidth===2;document.body.removeChild(K)})})();var v=n.support.cssFloat?"cssFloat":"styleFloat";n.props={"for":"htmlFor","class":"className","float":v,cssFloat:v,styleFloat:v,readonly:"readOnly",maxlength:"maxLength",cellspacing:"cellSpacing",rowspan:"rowSpan",tabindex:"tabIndex"};n.fn.extend({_load:n.fn.load,load:function(F,I,J){if(typeof F!=="string"){return this._load(F)}var H=F.indexOf(" ");if(H>=0){var D=F.slice(H,F.length);F=F.slice(0,H)}var G="GET";if(I){if(n.isFunction(I)){J=I;I=null}else{if(typeof I==="object"){I=n.param(I);G="POST"}}}var E=this;n.ajax({url:F,type:G,dataType:"html",data:I,complete:function(L,K){if(K=="success"||K=="notmodified"){E.html(D?n("<div/>").append(L.responseText.replace(/<script(.|\s)*?\/script>/g,"")).find(D):L.responseText)}if(J){E.each(J,[L.responseText,K,L])}}});return this},serialize:function(){return n.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?n.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||/select|textarea/i.test(this.nodeName)||/text|hidden|password/i.test(this.type))}).map(function(D,E){var F=n(this).val();return F==null?null:n.isArray(F)?n.map(F,function(H,G){return{name:E.name,value:H}}):{name:E.name,value:F}}).get()}});n.each("ajaxStart,ajaxStop,ajaxComplete,ajaxError,ajaxSuccess,ajaxSend".split(","),function(D,E){n.fn[E]=function(F){return this.bind(E,F)}});var q=e();n.extend({get:function(D,F,G,E){if(n.isFunction(F)){G=F;F=null}return n.ajax({type:"GET",url:D,data:F,success:G,dataType:E})},getScript:function(D,E){return n.get(D,null,E,"script")},getJSON:function(D,E,F){return n.get(D,E,F,"json")},post:function(D,F,G,E){if(n.isFunction(F)){G=F;F={}}return n.ajax({type:"POST",url:D,data:F,success:G,dataType:E})},ajaxSetup:function(D){n.extend(n.ajaxSettings,D)},ajaxSettings:{url:location.href,global:true,type:"GET",contentType:"application/x-www-form-urlencoded",processData:true,async:true,xhr:function(){return l.ActiveXObject?new ActiveXObject("Microsoft.XMLHTTP"):new XMLHttpRequest()},accepts:{xml:"application/xml, text/xml",html:"text/html",script:"text/javascript, application/javascript",json:"application/json, text/javascript",text:"text/plain",_default:"*/*"}},lastModified:{},ajax:function(L){L=n.extend(true,L,n.extend(true,{},n.ajaxSettings,L));var V,E=/=\?(&|$)/g,Q,U,F=L.type.toUpperCase();if(L.data&&L.processData&&typeof L.data!=="string"){L.data=n.param(L.data)}if(L.dataType=="jsonp"){if(F=="GET"){if(!L.url.match(E)){L.url+=(L.url.match(/\?/)?"&":"?")+(L.jsonp||"callback")+"=?"}}else{if(!L.data||!L.data.match(E)){L.data=(L.data?L.data+"&":"")+(L.jsonp||"callback")+"=?"}}L.dataType="json"}if(L.dataType=="json"&&(L.data&&L.data.match(E)||L.url.match(E))){V="jsonp"+q++;if(L.data){L.data=(L.data+"").replace(E,"="+V+"$1")}L.url=L.url.replace(E,"="+V+"$1");L.dataType="script";l[V]=function(W){U=W;H();K();l[V]=g;try{delete l[V]}catch(X){}if(G){G.removeChild(S)}}}if(L.dataType=="script"&&L.cache==null){L.cache=false}if(L.cache===false&&F=="GET"){var D=e();var T=L.url.replace(/(\?|&)_=.*?(&|$)/,"$1_="+D+"$2");L.url=T+((T==L.url)?(L.url.match(/\?/)?"&":"?")+"_="+D:"")}if(L.data&&F=="GET"){L.url+=(L.url.match(/\?/)?"&":"?")+L.data;L.data=null}if(L.global&&!n.active++){n.event.trigger("ajaxStart")}var P=/^(\w+:)?\/\/([^\/?#]+)/.exec(L.url);if(L.dataType=="script"&&F=="GET"&&P&&(P[1]&&P[1]!=location.protocol||P[2]!=location.host)){var G=document.getElementsByTagName("head")[0];var S=document.createElement("script");S.src=L.url;if(L.scriptCharset){S.charset=L.scriptCharset}if(!V){var N=false;S.onload=S.onreadystatechange=function(){if(!N&&(!this.readyState||this.readyState=="loaded"||this.readyState=="complete")){N=true;H();K();G.removeChild(S)}}}G.appendChild(S);return g}var J=false;var I=L.xhr();if(L.username){I.open(F,L.url,L.async,L.username,L.password)}else{I.open(F,L.url,L.async)}try{if(L.data){I.setRequestHeader("Content-Type",L.contentType)}if(L.ifModified){I.setRequestHeader("If-Modified-Since",n.lastModified[L.url]||"Thu, 01 Jan 1970 00:00:00 GMT")}I.setRequestHeader("X-Requested-With","XMLHttpRequest");I.setRequestHeader("Accept",L.dataType&&L.accepts[L.dataType]?L.accepts[L.dataType]+", */*":L.accepts._default)}catch(R){}if(L.beforeSend&&L.beforeSend(I,L)===false){if(L.global&&!--n.active){n.event.trigger("ajaxStop")}I.abort();return false}if(L.global){n.event.trigger("ajaxSend",[I,L])}var M=function(W){if(I.readyState==0){if(O){clearInterval(O);O=null;if(L.global&&!--n.active){n.event.trigger("ajaxStop")}}}else{if(!J&&I&&(I.readyState==4||W=="timeout")){J=true;if(O){clearInterval(O);O=null}Q=W=="timeout"?"timeout":!n.httpSuccess(I)?"error":L.ifModified&&n.httpNotModified(I,L.url)?"notmodified":"success";if(Q=="success"){try{U=n.httpData(I,L.dataType,L)}catch(Y){Q="parsererror"}}if(Q=="success"){var X;try{X=I.getResponseHeader("Last-Modified")}catch(Y){}if(L.ifModified&&X){n.lastModified[L.url]=X}if(!V){H()}}else{n.handleError(L,I,Q)}K();if(L.async){I=null}}}};if(L.async){var O=setInterval(M,13);if(L.timeout>0){setTimeout(function(){if(I){if(!J){M("timeout")}if(I){I.abort()}}},L.timeout)}}try{I.send(L.data)}catch(R){n.handleError(L,I,null,R)}if(!L.async){M()}function H(){if(L.success){L.success(U,Q)}if(L.global){n.event.trigger("ajaxSuccess",[I,L])}}function K(){if(L.complete){L.complete(I,Q)}if(L.global){n.event.trigger("ajaxComplete",[I,L])}if(L.global&&!--n.active){n.event.trigger("ajaxStop")}}return I},handleError:function(E,G,D,F){if(E.error){E.error(G,D,F)}if(E.global){n.event.trigger("ajaxError",[G,E,F])}},active:0,httpSuccess:function(E){try{return !E.status&&location.protocol=="file:"||(E.status>=200&&E.status<300)||E.status==304||E.status==1223}catch(D){}return false},httpNotModified:function(F,D){try{var G=F.getResponseHeader("Last-Modified");return F.status==304||G==n.lastModified[D]}catch(E){}return false},httpData:function(I,G,F){var E=I.getResponseHeader("content-type"),D=G=="xml"||!G&&E&&E.indexOf("xml")>=0,H=D?I.responseXML:I.responseText;if(D&&H.documentElement.tagName=="parsererror"){throw"parsererror"}if(F&&F.dataFilter){H=F.dataFilter(H,G)}if(typeof H==="string"){if(G=="script"){n.globalEval(H)}if(G=="json"){H=l["eval"]("("+H+")")}}return H},param:function(D){var F=[];function G(H,I){F[F.length]=encodeURIComponent(H)+"="+encodeURIComponent(I)}if(n.isArray(D)||D.jquery){n.each(D,function(){G(this.name,this.value)})}else{for(var E in D){if(n.isArray(D[E])){n.each(D[E],function(){G(E,this)})}else{G(E,n.isFunction(D[E])?D[E]():D[E])}}}return F.join("&").replace(/%20/g,"+")}});var m={},d=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]];function s(E,D){var F={};n.each(d.concat.apply([],d.slice(0,D)),function(){F[this]=E});return F}n.fn.extend({show:function(I,K){if(I){return this.animate(s("show",3),I,K)}else{for(var G=0,E=this.length;G<E;G++){var D=n.data(this[G],"olddisplay");this[G].style.display=D||"";if(n.css(this[G],"display")==="none"){var F=this[G].tagName,J;if(m[F]){J=m[F]}else{var H=n("<"+F+" />").appendTo("body");J=H.css("display");if(J==="none"){J="block"}H.remove();m[F]=J}this[G].style.display=n.data(this[G],"olddisplay",J)}}return this}},hide:function(G,H){if(G){return this.animate(s("hide",3),G,H)}else{for(var F=0,E=this.length;F<E;F++){var D=n.data(this[F],"olddisplay");if(!D&&D!=="none"){n.data(this[F],"olddisplay",n.css(this[F],"display"))}this[F].style.display="none"}return this}},_toggle:n.fn.toggle,toggle:function(F,E){var D=typeof F==="boolean";return n.isFunction(F)&&n.isFunction(E)?this._toggle.apply(this,arguments):F==null||D?this.each(function(){var G=D?F:n(this).is(":hidden");n(this)[G?"show":"hide"]()}):this.animate(s("toggle",3),F,E)},fadeTo:function(D,F,E){return this.animate({opacity:F},D,E)},animate:function(H,E,G,F){var D=n.speed(E,G,F);return this[D.queue===false?"each":"queue"](function(){var J=n.extend({},D),L,K=this.nodeType==1&&n(this).is(":hidden"),I=this;for(L in H){if(H[L]=="hide"&&K||H[L]=="show"&&!K){return J.complete.call(this)}if((L=="height"||L=="width")&&this.style){J.display=n.css(this,"display");J.overflow=this.style.overflow}}if(J.overflow!=null){this.style.overflow="hidden"}J.curAnim=n.extend({},H);n.each(H,function(N,R){var Q=new n.fx(I,J,N);if(/toggle|show|hide/.test(R)){Q[R=="toggle"?K?"show":"hide":R](H)}else{var P=R.toString().match(/^([+-]=)?([\d+-.]+)(.*)$/),S=Q.cur(true)||0;if(P){var M=parseFloat(P[2]),O=P[3]||"px";if(O!="px"){I.style[N]=(M||1)+O;S=((M||1)/Q.cur(true))*S;I.style[N]=S+O}if(P[1]){M=((P[1]=="-="?-1:1)*M)+S}Q.custom(S,M,O)}else{Q.custom(S,R,"")}}});return true})},stop:function(E,D){var F=n.timers;if(E){this.queue([])}this.each(function(){for(var G=F.length-1;G>=0;G--){if(F[G].elem==this){if(D){F[G](true)}F.splice(G,1)}}});if(!D){this.dequeue()}return this}});n.each({slideDown:s("show",1),slideUp:s("hide",1),slideToggle:s("toggle",1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"}},function(D,E){n.fn[D]=function(F,G){return this.animate(E,F,G)}});n.extend({speed:function(F,G,E){var D=typeof F==="object"?F:{complete:E||!E&&G||n.isFunction(F)&&F,duration:F,easing:E&&G||G&&!n.isFunction(G)&&G};D.duration=n.fx.off?0:typeof D.duration==="number"?D.duration:n.fx.speeds[D.duration]||n.fx.speeds._default;D.old=D.complete;D.complete=function(){if(D.queue!==false){n(this).dequeue()}if(n.isFunction(D.old)){D.old.call(this)}};return D},easing:{linear:function(F,G,D,E){return D+E*F},swing:function(F,G,D,E){return((-Math.cos(F*Math.PI)/2)+0.5)*E+D}},timers:[],timerId:null,fx:function(E,D,F){this.options=D;this.elem=E;this.prop=F;if(!D.orig){D.orig={}}}});n.fx.prototype={update:function(){if(this.options.step){this.options.step.call(this.elem,this.now,this)}(n.fx.step[this.prop]||n.fx.step._default)(this);if((this.prop=="height"||this.prop=="width")&&this.elem.style){this.elem.style.display="block"}},cur:function(E){if(this.elem[this.prop]!=null&&(!this.elem.style||this.elem.style[this.prop]==null)){return this.elem[this.prop]}var D=parseFloat(n.css(this.elem,this.prop,E));return D&&D>-10000?D:parseFloat(n.curCSS(this.elem,this.prop))||0},custom:function(H,G,F){this.startTime=e();this.start=H;this.end=G;this.unit=F||this.unit||"px";this.now=this.start;this.pos=this.state=0;var D=this;function E(I){return D.step(I)}E.elem=this.elem;n.timers.push(E);if(E()&&n.timerId==null){n.timerId=setInterval(function(){var J=n.timers;for(var I=0;I<J.length;I++){if(!J[I]()){J.splice(I--,1)}}if(!J.length){clearInterval(n.timerId);n.timerId=null}},13)}},show:function(){this.options.orig[this.prop]=n.attr(this.elem.style,this.prop);this.options.show=true;this.custom(this.prop=="width"||this.prop=="height"?1:0,this.cur());n(this.elem).show()},hide:function(){this.options.orig[this.prop]=n.attr(this.elem.style,this.prop);this.options.hide=true;this.custom(this.cur(),0)},step:function(G){var F=e();if(G||F>=this.options.duration+this.startTime){this.now=this.end;this.pos=this.state=1;this.update();this.options.curAnim[this.prop]=true;var D=true;for(var E in this.options.curAnim){if(this.options.curAnim[E]!==true){D=false}}if(D){if(this.options.display!=null){this.elem.style.overflow=this.options.overflow;this.elem.style.display=this.options.display;if(n.css(this.elem,"display")=="none"){this.elem.style.display="block"}}if(this.options.hide){n(this.elem).hide()}if(this.options.hide||this.options.show){for(var H in this.options.curAnim){n.attr(this.elem.style,H,this.options.orig[H])}}}if(D){this.options.complete.call(this.elem)}return false}else{var I=F-this.startTime;this.state=I/this.options.duration;this.pos=n.easing[this.options.easing||(n.easing.swing?"swing":"linear")](this.state,I,0,1,this.options.duration);this.now=this.start+((this.end-this.start)*this.pos);this.update()}return true}};n.extend(n.fx,{speeds:{slow:600,fast:200,_default:400},step:{opacity:function(D){n.attr(D.elem.style,"opacity",D.now)},_default:function(D){if(D.elem.style&&D.elem.style[D.prop]!=null){D.elem.style[D.prop]=D.now+D.unit}else{D.elem[D.prop]=D.now}}}});if(document.documentElement.getBoundingClientRect){n.fn.offset=function(){if(!this[0]){return{top:0,left:0}}if(this[0]===this[0].ownerDocument.body){return n.offset.bodyOffset(this[0])}var F=this[0].getBoundingClientRect(),I=this[0].ownerDocument,E=I.body,D=I.documentElement,K=D.clientTop||E.clientTop||0,J=D.clientLeft||E.clientLeft||0,H=F.top+(self.pageYOffset||n.boxModel&&D.scrollTop||E.scrollTop)-K,G=F.left+(self.pageXOffset||n.boxModel&&D.scrollLeft||E.scrollLeft)-J;return{top:H,left:G}}}else{n.fn.offset=function(){if(!this[0]){return{top:0,left:0}}if(this[0]===this[0].ownerDocument.body){return n.offset.bodyOffset(this[0])}n.offset.initialized||n.offset.initialize();var I=this[0],F=I.offsetParent,E=I,N=I.ownerDocument,L,G=N.documentElement,J=N.body,K=N.defaultView,D=K.getComputedStyle(I,null),M=I.offsetTop,H=I.offsetLeft;while((I=I.parentNode)&&I!==J&&I!==G){L=K.getComputedStyle(I,null);M-=I.scrollTop,H-=I.scrollLeft;if(I===F){M+=I.offsetTop,H+=I.offsetLeft;if(n.offset.doesNotAddBorder&&!(n.offset.doesAddBorderForTableAndCells&&/^t(able|d|h)$/i.test(I.tagName))){M+=parseInt(L.borderTopWidth,10)||0,H+=parseInt(L.borderLeftWidth,10)||0}E=F,F=I.offsetParent}if(n.offset.subtractsBorderForOverflowNotVisible&&L.overflow!=="visible"){M+=parseInt(L.borderTopWidth,10)||0,H+=parseInt(L.borderLeftWidth,10)||0}D=L}if(D.position==="relative"||D.position==="static"){M+=J.offsetTop,H+=J.offsetLeft}if(D.position==="fixed"){M+=Math.max(G.scrollTop,J.scrollTop),H+=Math.max(G.scrollLeft,J.scrollLeft)}return{top:M,left:H}}}n.offset={initialize:function(){if(this.initialized){return}var K=document.body,E=document.createElement("div"),G,F,M,H,L,D,I=K.style.marginTop,J='<div style="position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;"><div></div></div><table style="position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;"cellpadding="0"cellspacing="0"><tr><td></td></tr></table>';L={position:"absolute",top:0,left:0,margin:0,border:0,width:"1px",height:"1px",visibility:"hidden"};for(D in L){E.style[D]=L[D]}E.innerHTML=J;K.insertBefore(E,K.firstChild);G=E.firstChild,F=G.firstChild,H=G.nextSibling.firstChild.firstChild;this.doesNotAddBorder=(F.offsetTop!==5);this.doesAddBorderForTableAndCells=(H.offsetTop===5);G.style.overflow="hidden",G.style.position="relative";this.subtractsBorderForOverflowNotVisible=(F.offsetTop===-5);K.style.marginTop="1px";this.doesNotIncludeMarginInBodyOffset=(K.offsetTop===0);K.style.marginTop=I;K.removeChild(E);this.initialized=true},bodyOffset:function(D){n.offset.initialized||n.offset.initialize();var F=D.offsetTop,E=D.offsetLeft;if(n.offset.doesNotIncludeMarginInBodyOffset){F+=parseInt(n.curCSS(D,"marginTop",true),10)||0,E+=parseInt(n.curCSS(D,"marginLeft",true),10)||0}return{top:F,left:E}}};n.fn.extend({position:function(){var H=0,G=0,E;if(this[0]){var F=this.offsetParent(),I=this.offset(),D=/^body|html$/i.test(F[0].tagName)?{top:0,left:0}:F.offset();I.top-=j(this,"marginTop");I.left-=j(this,"marginLeft");D.top+=j(F,"borderTopWidth");D.left+=j(F,"borderLeftWidth");E={top:I.top-D.top,left:I.left-D.left}}return E},offsetParent:function(){var D=this[0].offsetParent||document.body;while(D&&(!/^body|html$/i.test(D.tagName)&&n.css(D,"position")=="static")){D=D.offsetParent}return n(D)}});n.each(["Left","Top"],function(E,D){var F="scroll"+D;n.fn[F]=function(G){if(!this[0]){return null}return G!==g?this.each(function(){this==l||this==document?l.scrollTo(!E?G:n(l).scrollLeft(),E?G:n(l).scrollTop()):this[F]=G}):this[0]==l||this[0]==document?self[E?"pageYOffset":"pageXOffset"]||n.boxModel&&document.documentElement[F]||document.body[F]:this[0][F]}});n.each(["Height","Width"],function(G,E){var D=G?"Left":"Top",F=G?"Right":"Bottom";n.fn["inner"+E]=function(){return this[E.toLowerCase()]()+j(this,"padding"+D)+j(this,"padding"+F)};n.fn["outer"+E]=function(I){return this["inner"+E]()+j(this,"border"+D+"Width")+j(this,"border"+F+"Width")+(I?j(this,"margin"+D)+j(this,"margin"+F):0)};var H=E.toLowerCase();n.fn[H]=function(I){return this[0]==l?document.compatMode=="CSS1Compat"&&document.documentElement["client"+E]||document.body["client"+E]:this[0]==document?Math.max(document.documentElement["client"+E],document.body["scroll"+E],document.documentElement["scroll"+E],document.body["offset"+E],document.documentElement["offset"+E]):I===g?(this.length?n.css(this[0],H):null):this.css(H,typeof I==="string"?I:I+"px")}})})();
diff --git a/devtools/client/inspector/markup/test/lib_jquery_1.4_min.js b/devtools/client/inspector/markup/test/lib_jquery_1.4_min.js
new file mode 100644
index 000000000..5c70e4c5f
--- /dev/null
+++ b/devtools/client/inspector/markup/test/lib_jquery_1.4_min.js
@@ -0,0 +1,151 @@
+/*!
+ * jQuery JavaScript Library v1.4
+ * http://jquery.com/
+ *
+ * Copyright 2010, John Resig
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://docs.jquery.com/License
+ *
+ * Includes Sizzle.js
+ * http://sizzlejs.com/
+ * Copyright 2010, The Dojo Foundation
+ * Released under the MIT, BSD, and GPL Licenses.
+ *
+ * Date: Wed Jan 13 15:23:05 2010 -0500
+ */
+(function(A,w){function oa(){if(!c.isReady){try{s.documentElement.doScroll("left")}catch(a){setTimeout(oa,1);return}c.ready()}}function La(a,b){b.src?c.ajax({url:b.src,async:false,dataType:"script"}):c.globalEval(b.text||b.textContent||b.innerHTML||"");b.parentNode&&b.parentNode.removeChild(b)}function $(a,b,d,f,e,i){var j=a.length;if(typeof b==="object"){for(var o in b)$(a,o,b[o],f,e,d);return a}if(d!==w){f=!i&&f&&c.isFunction(d);for(o=0;o<j;o++)e(a[o],b,f?d.call(a[o],o,e(a[o],b)):d,i);return a}return j?
+e(a[0],b):null}function K(){return(new Date).getTime()}function aa(){return false}function ba(){return true}function pa(a,b,d){d[0].type=a;return c.event.handle.apply(b,d)}function qa(a){var b=true,d=[],f=[],e=arguments,i,j,o,p,n,t=c.extend({},c.data(this,"events").live);for(p in t){j=t[p];if(j.live===a.type||j.altLive&&c.inArray(a.type,j.altLive)>-1){i=j.data;i.beforeFilter&&i.beforeFilter[a.type]&&!i.beforeFilter[a.type](a)||f.push(j.selector)}else delete t[p]}i=c(a.target).closest(f,a.currentTarget);
+n=0;for(l=i.length;n<l;n++)for(p in t){j=t[p];o=i[n].elem;f=null;if(i[n].selector===j.selector){if(j.live==="mouseenter"||j.live==="mouseleave")f=c(a.relatedTarget).closest(j.selector)[0];if(!f||f!==o)d.push({elem:o,fn:j})}}n=0;for(l=d.length;n<l;n++){i=d[n];a.currentTarget=i.elem;a.data=i.fn.data;if(i.fn.apply(i.elem,e)===false){b=false;break}}return b}function ra(a,b){return["live",a,b.replace(/\./g,"`").replace(/ /g,"&")].join(".")}function sa(a){return!a||!a.parentNode||a.parentNode.nodeType===
+11}function ta(a,b){var d=0;b.each(function(){if(this.nodeName===(a[d]&&a[d].nodeName)){var f=c.data(a[d++]),e=c.data(this,f);if(f=f&&f.events){delete e.handle;e.events={};for(var i in f)for(var j in f[i])c.event.add(this,i,f[i][j],f[i][j].data)}}})}function ua(a,b,d){var f,e,i;if(a.length===1&&typeof a[0]==="string"&&a[0].length<512&&a[0].indexOf("<option")<0){e=true;if(i=c.fragments[a[0]])if(i!==1)f=i}if(!f){b=b&&b[0]?b[0].ownerDocument||b[0]:s;f=b.createDocumentFragment();c.clean(a,b,f,d)}if(e)c.fragments[a[0]]=
+i?f:1;return{fragment:f,cacheable:e}}function T(a){for(var b=0,d,f;(d=a[b])!=null;b++)if(!c.noData[d.nodeName.toLowerCase()]&&(f=d[H]))delete c.cache[f]}function L(a,b){var d={};c.each(va.concat.apply([],va.slice(0,b)),function(){d[this]=a});return d}function wa(a){return"scrollTo"in a&&a.document?a:a.nodeType===9?a.defaultView||a.parentWindow:false}var c=function(a,b){return new c.fn.init(a,b)},Ma=A.jQuery,Na=A.$,s=A.document,U,Oa=/^[^<]*(<[\w\W]+>)[^>]*$|^#([\w-]+)$/,Pa=/^.[^:#\[\.,]*$/,Qa=/\S/,
+Ra=/^(\s|\u00A0)+|(\s|\u00A0)+$/g,Sa=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,P=navigator.userAgent,xa=false,Q=[],M,ca=Object.prototype.toString,da=Object.prototype.hasOwnProperty,ea=Array.prototype.push,R=Array.prototype.slice,V=Array.prototype.indexOf;c.fn=c.prototype={init:function(a,b){var d,f;if(!a)return this;if(a.nodeType){this.context=this[0]=a;this.length=1;return this}if(typeof a==="string")if((d=Oa.exec(a))&&(d[1]||!b))if(d[1]){f=b?b.ownerDocument||b:s;if(a=Sa.exec(a))if(c.isPlainObject(b)){a=[s.createElement(a[1])];
+c.fn.attr.call(a,b,true)}else a=[f.createElement(a[1])];else{a=ua([d[1]],[f]);a=(a.cacheable?a.fragment.cloneNode(true):a.fragment).childNodes}}else{if(b=s.getElementById(d[2])){if(b.id!==d[2])return U.find(a);this.length=1;this[0]=b}this.context=s;this.selector=a;return this}else if(!b&&/^\w+$/.test(a)){this.selector=a;this.context=s;a=s.getElementsByTagName(a)}else return!b||b.jquery?(b||U).find(a):c(b).find(a);else if(c.isFunction(a))return U.ready(a);if(a.selector!==w){this.selector=a.selector;
+this.context=a.context}return c.isArray(a)?this.setArray(a):c.makeArray(a,this)},selector:"",jquery:"1.4",length:0,size:function(){return this.length},toArray:function(){return R.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this.slice(a)[0]:this[a]},pushStack:function(a,b,d){a=c(a||null);a.prevObject=this;a.context=this.context;if(b==="find")a.selector=this.selector+(this.selector?" ":"")+d;else if(b)a.selector=this.selector+"."+b+"("+d+")";return a},setArray:function(a){this.length=
+0;ea.apply(this,a);return this},each:function(a,b){return c.each(this,a,b)},ready:function(a){c.bindReady();if(c.isReady)a.call(s,c);else Q&&Q.push(a);return this},eq:function(a){return a===-1?this.slice(a):this.slice(a,+a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(R.apply(this,arguments),"slice",R.call(arguments).join(","))},map:function(a){return this.pushStack(c.map(this,function(b,d){return a.call(b,d,b)}))},end:function(){return this.prevObject||
+c(null)},push:ea,sort:[].sort,splice:[].splice};c.fn.init.prototype=c.fn;c.extend=c.fn.extend=function(){var a=arguments[0]||{},b=1,d=arguments.length,f=false,e,i,j,o;if(typeof a==="boolean"){f=a;a=arguments[1]||{};b=2}if(typeof a!=="object"&&!c.isFunction(a))a={};if(d===b){a=this;--b}for(;b<d;b++)if((e=arguments[b])!=null)for(i in e){j=a[i];o=e[i];if(a!==o)if(f&&o&&(c.isPlainObject(o)||c.isArray(o))){j=j&&(c.isPlainObject(j)||c.isArray(j))?j:c.isArray(o)?[]:{};a[i]=c.extend(f,j,o)}else if(o!==w)a[i]=
+o}return a};c.extend({noConflict:function(a){A.$=Na;if(a)A.jQuery=Ma;return c},isReady:false,ready:function(){if(!c.isReady){if(!s.body)return setTimeout(c.ready,13);c.isReady=true;if(Q){for(var a,b=0;a=Q[b++];)a.call(s,c);Q=null}c.fn.triggerHandler&&c(s).triggerHandler("ready")}},bindReady:function(){if(!xa){xa=true;if(s.readyState==="complete")return c.ready();if(s.addEventListener){s.addEventListener("DOMContentLoaded",M,false);A.addEventListener("load",c.ready,false)}else if(s.attachEvent){s.attachEvent("onreadystatechange",
+M);A.attachEvent("onload",c.ready);var a=false;try{a=A.frameElement==null}catch(b){}s.documentElement.doScroll&&a&&oa()}}},isFunction:function(a){return ca.call(a)==="[object Function]"},isArray:function(a){return ca.call(a)==="[object Array]"},isPlainObject:function(a){if(!a||ca.call(a)!=="[object Object]"||a.nodeType||a.setInterval)return false;if(a.constructor&&!da.call(a,"constructor")&&!da.call(a.constructor.prototype,"isPrototypeOf"))return false;var b;for(b in a);return b===w||da.call(a,b)},
+isEmptyObject:function(a){for(var b in a)return false;return true},noop:function(){},globalEval:function(a){if(a&&Qa.test(a)){var b=s.getElementsByTagName("head")[0]||s.documentElement,d=s.createElement("script");d.type="text/javascript";if(c.support.scriptEval)d.appendChild(s.createTextNode(a));else d.text=a;b.insertBefore(d,b.firstChild);b.removeChild(d)}},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,b,d){var f,e=0,i=a.length,j=i===w||c.isFunction(a);
+if(d)if(j)for(f in a){if(b.apply(a[f],d)===false)break}else for(;e<i;){if(b.apply(a[e++],d)===false)break}else if(j)for(f in a){if(b.call(a[f],f,a[f])===false)break}else for(d=a[0];e<i&&b.call(d,e,d)!==false;d=a[++e]);return a},trim:function(a){return(a||"").replace(Ra,"")},makeArray:function(a,b){b=b||[];if(a!=null)a.length==null||typeof a==="string"||c.isFunction(a)||typeof a!=="function"&&a.setInterval?ea.call(b,a):c.merge(b,a);return b},inArray:function(a,b){if(b.indexOf)return b.indexOf(a);for(var d=
+0,f=b.length;d<f;d++)if(b[d]===a)return d;return-1},merge:function(a,b){var d=a.length,f=0;if(typeof b.length==="number")for(var e=b.length;f<e;f++)a[d++]=b[f];else for(;b[f]!==w;)a[d++]=b[f++];a.length=d;return a},grep:function(a,b,d){for(var f=[],e=0,i=a.length;e<i;e++)!d!==!b(a[e],e)&&f.push(a[e]);return f},map:function(a,b,d){for(var f=[],e,i=0,j=a.length;i<j;i++){e=b(a[i],i,d);if(e!=null)f[f.length]=e}return f.concat.apply([],f)},guid:1,proxy:function(a,b,d){if(arguments.length===2)if(typeof b===
+"string"){d=a;a=d[b];b=w}else if(b&&!c.isFunction(b)){d=b;b=w}if(!b&&a)b=function(){return a.apply(d||this,arguments)};if(a)b.guid=a.guid=a.guid||b.guid||c.guid++;return b},uaMatch:function(a){var b={browser:""};a=a.toLowerCase();if(/webkit/.test(a))b={browser:"webkit",version:/webkit[\/ ]([\w.]+)/};else if(/opera/.test(a))b={browser:"opera",version:/version/.test(a)?/version[\/ ]([\w.]+)/:/opera[\/ ]([\w.]+)/};else if(/msie/.test(a))b={browser:"msie",version:/msie ([\w.]+)/};else if(/mozilla/.test(a)&&
+!/compatible/.test(a))b={browser:"mozilla",version:/rv:([\w.]+)/};b.version=(b.version&&b.version.exec(a)||[0,"0"])[1];return b},browser:{}});P=c.uaMatch(P);if(P.browser){c.browser[P.browser]=true;c.browser.version=P.version}if(c.browser.webkit)c.browser.safari=true;if(V)c.inArray=function(a,b){return V.call(b,a)};U=c(s);if(s.addEventListener)M=function(){s.removeEventListener("DOMContentLoaded",M,false);c.ready()};else if(s.attachEvent)M=function(){if(s.readyState==="complete"){s.detachEvent("onreadystatechange",
+M);c.ready()}};if(V)c.inArray=function(a,b){return V.call(b,a)};(function(){c.support={};var a=s.documentElement,b=s.createElement("script"),d=s.createElement("div"),f="script"+K();d.style.display="none";d.innerHTML=" <link/><table></table><a href='/a' style='color:red;float:left;opacity:.55;'>a</a><input type='checkbox'/>";var e=d.getElementsByTagName("*"),i=d.getElementsByTagName("a")[0];if(!(!e||!e.length||!i)){c.support={leadingWhitespace:d.firstChild.nodeType===3,tbody:!d.getElementsByTagName("tbody").length,
+htmlSerialize:!!d.getElementsByTagName("link").length,style:/red/.test(i.getAttribute("style")),hrefNormalized:i.getAttribute("href")==="/a",opacity:/^0.55$/.test(i.style.opacity),cssFloat:!!i.style.cssFloat,checkOn:d.getElementsByTagName("input")[0].value==="on",optSelected:s.createElement("select").appendChild(s.createElement("option")).selected,scriptEval:false,noCloneEvent:true,boxModel:null};b.type="text/javascript";try{b.appendChild(s.createTextNode("window."+f+"=1;"))}catch(j){}a.insertBefore(b,
+a.firstChild);if(A[f]){c.support.scriptEval=true;delete A[f]}a.removeChild(b);if(d.attachEvent&&d.fireEvent){d.attachEvent("onclick",function o(){c.support.noCloneEvent=false;d.detachEvent("onclick",o)});d.cloneNode(true).fireEvent("onclick")}c(function(){var o=s.createElement("div");o.style.width=o.style.paddingLeft="1px";s.body.appendChild(o);c.boxModel=c.support.boxModel=o.offsetWidth===2;s.body.removeChild(o).style.display="none"});a=function(o){var p=s.createElement("div");o="on"+o;var n=o in
+p;if(!n){p.setAttribute(o,"return;");n=typeof p[o]==="function"}return n};c.support.submitBubbles=a("submit");c.support.changeBubbles=a("change");a=b=d=e=i=null}})();c.props={"for":"htmlFor","class":"className",readonly:"readOnly",maxlength:"maxLength",cellspacing:"cellSpacing",rowspan:"rowSpan",colspan:"colSpan",tabindex:"tabIndex",usemap:"useMap",frameborder:"frameBorder"};var H="jQuery"+K(),Ta=0,ya={},Ua={};c.extend({cache:{},expando:H,noData:{embed:true,object:true,applet:true},data:function(a,
+b,d){if(!(a.nodeName&&c.noData[a.nodeName.toLowerCase()])){a=a==A?ya:a;var f=a[H],e=c.cache;if(!b&&!f)return null;f||(f=++Ta);if(typeof b==="object"){a[H]=f;e=e[f]=c.extend(true,{},b)}else e=e[f]?e[f]:typeof d==="undefined"?Ua:(e[f]={});if(d!==w){a[H]=f;e[b]=d}return typeof b==="string"?e[b]:e}},removeData:function(a,b){if(!(a.nodeName&&c.noData[a.nodeName.toLowerCase()])){a=a==A?ya:a;var d=a[H],f=c.cache,e=f[d];if(b){if(e){delete e[b];c.isEmptyObject(e)&&c.removeData(a)}}else{try{delete a[H]}catch(i){a.removeAttribute&&
+a.removeAttribute(H)}delete f[d]}}}});c.fn.extend({data:function(a,b){if(typeof a==="undefined"&&this.length)return c.data(this[0]);else if(typeof a==="object")return this.each(function(){c.data(this,a)});var d=a.split(".");d[1]=d[1]?"."+d[1]:"";if(b===w){var f=this.triggerHandler("getData"+d[1]+"!",[d[0]]);if(f===w&&this.length)f=c.data(this[0],a);return f===w&&d[1]?this.data(d[0]):f}else return this.trigger("setData"+d[1]+"!",[d[0],b]).each(function(){c.data(this,a,b)})},removeData:function(a){return this.each(function(){c.removeData(this,
+a)})}});c.extend({queue:function(a,b,d){if(a){b=(b||"fx")+"queue";var f=c.data(a,b);if(!d)return f||[];if(!f||c.isArray(d))f=c.data(a,b,c.makeArray(d));else f.push(d);return f}},dequeue:function(a,b){b=b||"fx";var d=c.queue(a,b),f=d.shift();if(f==="inprogress")f=d.shift();if(f){b==="fx"&&d.unshift("inprogress");f.call(a,function(){c.dequeue(a,b)})}}});c.fn.extend({queue:function(a,b){if(typeof a!=="string"){b=a;a="fx"}if(b===w)return c.queue(this[0],a);return this.each(function(){var d=c.queue(this,
+a,b);a==="fx"&&d[0]!=="inprogress"&&c.dequeue(this,a)})},dequeue:function(a){return this.each(function(){c.dequeue(this,a)})},delay:function(a,b){a=c.fx?c.fx.speeds[a]||a:a;b=b||"fx";return this.queue(b,function(){var d=this;setTimeout(function(){c.dequeue(d,b)},a)})},clearQueue:function(a){return this.queue(a||"fx",[])}});var za=/[\n\t]/g,fa=/\s+/,Va=/\r/g,Wa=/href|src|style/,Xa=/(button|input)/i,Ya=/(button|input|object|select|textarea)/i,Za=/^(a|area)$/i,Aa=/radio|checkbox/;c.fn.extend({attr:function(a,
+b){return $(this,a,b,true,c.attr)},removeAttr:function(a){return this.each(function(){c.attr(this,a,"");this.nodeType===1&&this.removeAttribute(a)})},addClass:function(a){if(c.isFunction(a))return this.each(function(p){var n=c(this);n.addClass(a.call(this,p,n.attr("class")))});if(a&&typeof a==="string")for(var b=(a||"").split(fa),d=0,f=this.length;d<f;d++){var e=this[d];if(e.nodeType===1)if(e.className)for(var i=" "+e.className+" ",j=0,o=b.length;j<o;j++){if(i.indexOf(" "+b[j]+" ")<0)e.className+=
+" "+b[j]}else e.className=a}return this},removeClass:function(a){if(c.isFunction(a))return this.each(function(p){var n=c(this);n.removeClass(a.call(this,p,n.attr("class")))});if(a&&typeof a==="string"||a===w)for(var b=(a||"").split(fa),d=0,f=this.length;d<f;d++){var e=this[d];if(e.nodeType===1&&e.className)if(a){for(var i=(" "+e.className+" ").replace(za," "),j=0,o=b.length;j<o;j++)i=i.replace(" "+b[j]+" "," ");e.className=i.substring(1,i.length-1)}else e.className=""}return this},toggleClass:function(a,
+b){var d=typeof a,f=typeof b==="boolean";if(c.isFunction(a))return this.each(function(e){var i=c(this);i.toggleClass(a.call(this,e,i.attr("class"),b),b)});return this.each(function(){if(d==="string")for(var e,i=0,j=c(this),o=b,p=a.split(fa);e=p[i++];){o=f?o:!j.hasClass(e);j[o?"addClass":"removeClass"](e)}else if(d==="undefined"||d==="boolean"){this.className&&c.data(this,"__className__",this.className);this.className=this.className||a===false?"":c.data(this,"__className__")||""}})},hasClass:function(a){a=
+" "+a+" ";for(var b=0,d=this.length;b<d;b++)if((" "+this[b].className+" ").replace(za," ").indexOf(a)>-1)return true;return false},val:function(a){if(a===w){var b=this[0];if(b){if(c.nodeName(b,"option"))return(b.attributes.value||{}).specified?b.value:b.text;if(c.nodeName(b,"select")){var d=b.selectedIndex,f=[],e=b.options;b=b.type==="select-one";if(d<0)return null;var i=b?d:0;for(d=b?d+1:e.length;i<d;i++){var j=e[i];if(j.selected){a=c(j).val();if(b)return a;f.push(a)}}return f}if(Aa.test(b.type)&&
+!c.support.checkOn)return b.getAttribute("value")===null?"on":b.value;return(b.value||"").replace(Va,"")}return w}var o=c.isFunction(a);return this.each(function(p){var n=c(this),t=a;if(this.nodeType===1){if(o)t=a.call(this,p,n.val());if(typeof t==="number")t+="";if(c.isArray(t)&&Aa.test(this.type))this.checked=c.inArray(n.val(),t)>=0;else if(c.nodeName(this,"select")){var z=c.makeArray(t);c("option",this).each(function(){this.selected=c.inArray(c(this).val(),z)>=0});if(!z.length)this.selectedIndex=
+-1}else this.value=t}})}});c.extend({attrFn:{val:true,css:true,html:true,text:true,data:true,width:true,height:true,offset:true},attr:function(a,b,d,f){if(!a||a.nodeType===3||a.nodeType===8)return w;if(f&&b in c.attrFn)return c(a)[b](d);f=a.nodeType!==1||!c.isXMLDoc(a);var e=d!==w;b=f&&c.props[b]||b;if(a.nodeType===1){var i=Wa.test(b);if(b in a&&f&&!i){if(e){if(b==="type"&&Xa.test(a.nodeName)&&a.parentNode)throw"type property can't be changed";a[b]=d}if(c.nodeName(a,"form")&&a.getAttributeNode(b))return a.getAttributeNode(b).nodeValue;
+if(b==="tabIndex")return(b=a.getAttributeNode("tabIndex"))&&b.specified?b.value:Ya.test(a.nodeName)||Za.test(a.nodeName)&&a.href?0:w;return a[b]}if(!c.support.style&&f&&b==="style"){if(e)a.style.cssText=""+d;return a.style.cssText}e&&a.setAttribute(b,""+d);a=!c.support.hrefNormalized&&f&&i?a.getAttribute(b,2):a.getAttribute(b);return a===null?w:a}return c.style(a,b,d)}});var $a=function(a){return a.replace(/[^\w\s\.\|`]/g,function(b){return"\\"+b})};c.event={add:function(a,b,d,f){if(!(a.nodeType===
+3||a.nodeType===8)){if(a.setInterval&&a!==A&&!a.frameElement)a=A;if(!d.guid)d.guid=c.guid++;if(f!==w){d=c.proxy(d);d.data=f}var e=c.data(a,"events")||c.data(a,"events",{}),i=c.data(a,"handle"),j;if(!i){j=function(){return typeof c!=="undefined"&&!c.event.triggered?c.event.handle.apply(j.elem,arguments):w};i=c.data(a,"handle",j)}if(i){i.elem=a;b=b.split(/\s+/);for(var o,p=0;o=b[p++];){var n=o.split(".");o=n.shift();d.type=n.slice(0).sort().join(".");var t=e[o],z=this.special[o]||{};if(!t){t=e[o]={};
+if(!z.setup||z.setup.call(a,f,n,d)===false)if(a.addEventListener)a.addEventListener(o,i,false);else a.attachEvent&&a.attachEvent("on"+o,i)}if(z.add)if((n=z.add.call(a,d,f,n,t))&&c.isFunction(n)){n.guid=n.guid||d.guid;d=n}t[d.guid]=d;this.global[o]=true}a=null}}},global:{},remove:function(a,b,d){if(!(a.nodeType===3||a.nodeType===8)){var f=c.data(a,"events"),e,i,j;if(f){if(b===w||typeof b==="string"&&b.charAt(0)===".")for(i in f)this.remove(a,i+(b||""));else{if(b.type){d=b.handler;b=b.type}b=b.split(/\s+/);
+for(var o=0;i=b[o++];){var p=i.split(".");i=p.shift();var n=!p.length,t=c.map(p.slice(0).sort(),$a);t=new RegExp("(^|\\.)"+t.join("\\.(?:.*\\.)?")+"(\\.|$)");var z=this.special[i]||{};if(f[i]){if(d){j=f[i][d.guid];delete f[i][d.guid]}else for(var B in f[i])if(n||t.test(f[i][B].type))delete f[i][B];z.remove&&z.remove.call(a,p,j);for(e in f[i])break;if(!e){if(!z.teardown||z.teardown.call(a,p)===false)if(a.removeEventListener)a.removeEventListener(i,c.data(a,"handle"),false);else a.detachEvent&&a.detachEvent("on"+
+i,c.data(a,"handle"));e=null;delete f[i]}}}}for(e in f)break;if(!e){if(B=c.data(a,"handle"))B.elem=null;c.removeData(a,"events");c.removeData(a,"handle")}}}},trigger:function(a,b,d,f){var e=a.type||a;if(!f){a=typeof a==="object"?a[H]?a:c.extend(c.Event(e),a):c.Event(e);if(e.indexOf("!")>=0){a.type=e=e.slice(0,-1);a.exclusive=true}if(!d){a.stopPropagation();this.global[e]&&c.each(c.cache,function(){this.events&&this.events[e]&&c.event.trigger(a,b,this.handle.elem)})}if(!d||d.nodeType===3||d.nodeType===
+8)return w;a.result=w;a.target=d;b=c.makeArray(b);b.unshift(a)}a.currentTarget=d;var i=c.data(d,"handle");i&&i.apply(d,b);var j,o;try{if(!(d&&d.nodeName&&c.noData[d.nodeName.toLowerCase()])){j=d[e];o=d["on"+e]}}catch(p){}i=c.nodeName(d,"a")&&e==="click";if(!f&&j&&!a.isDefaultPrevented()&&!i){this.triggered=true;try{d[e]()}catch(n){}}else if(o&&d["on"+e].apply(d,b)===false)a.result=false;this.triggered=false;if(!a.isPropagationStopped())(d=d.parentNode||d.ownerDocument)&&c.event.trigger(a,b,d,true)},
+handle:function(a){var b,d;a=arguments[0]=c.event.fix(a||A.event);a.currentTarget=this;d=a.type.split(".");a.type=d.shift();b=!d.length&&!a.exclusive;var f=new RegExp("(^|\\.)"+d.slice(0).sort().join("\\.(?:.*\\.)?")+"(\\.|$)");d=(c.data(this,"events")||{})[a.type];for(var e in d){var i=d[e];if(b||f.test(i.type)){a.handler=i;a.data=i.data;i=i.apply(this,arguments);if(i!==w){a.result=i;if(i===false){a.preventDefault();a.stopPropagation()}}if(a.isImmediatePropagationStopped())break}}return a.result},
+props:"altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode layerX layerY metaKey newValue offsetX offsetY originalTarget pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "),fix:function(a){if(a[H])return a;var b=a;a=c.Event(b);for(var d=this.props.length,f;d;){f=this.props[--d];a[f]=b[f]}if(!a.target)a.target=a.srcElement||
+s;if(a.target.nodeType===3)a.target=a.target.parentNode;if(!a.relatedTarget&&a.fromElement)a.relatedTarget=a.fromElement===a.target?a.toElement:a.fromElement;if(a.pageX==null&&a.clientX!=null){b=s.documentElement;d=s.body;a.pageX=a.clientX+(b&&b.scrollLeft||d&&d.scrollLeft||0)-(b&&b.clientLeft||d&&d.clientLeft||0);a.pageY=a.clientY+(b&&b.scrollTop||d&&d.scrollTop||0)-(b&&b.clientTop||d&&d.clientTop||0)}if(!a.which&&(a.charCode||a.charCode===0?a.charCode:a.keyCode))a.which=a.charCode||a.keyCode;if(!a.metaKey&&
+a.ctrlKey)a.metaKey=a.ctrlKey;if(!a.which&&a.button!==w)a.which=a.button&1?1:a.button&2?3:a.button&4?2:0;return a},guid:1E8,proxy:c.proxy,special:{ready:{setup:c.bindReady,teardown:c.noop},live:{add:function(a,b){c.extend(a,b||{});a.guid+=b.selector+b.live;c.event.add(this,b.live,qa,b)},remove:function(a){if(a.length){var b=0,d=new RegExp("(^|\\.)"+a[0]+"(\\.|$)");c.each(c.data(this,"events").live||{},function(){d.test(this.type)&&b++});b<1&&c.event.remove(this,a[0],qa)}},special:{}},beforeunload:{setup:function(a,
+b,d){if(this.setInterval)this.onbeforeunload=d;return false},teardown:function(a,b){if(this.onbeforeunload===b)this.onbeforeunload=null}}}};c.Event=function(a){if(!this.preventDefault)return new c.Event(a);if(a&&a.type){this.originalEvent=a;this.type=a.type}else this.type=a;this.timeStamp=K();this[H]=true};c.Event.prototype={preventDefault:function(){this.isDefaultPrevented=ba;var a=this.originalEvent;if(a){a.preventDefault&&a.preventDefault();a.returnValue=false}},stopPropagation:function(){this.isPropagationStopped=
+ba;var a=this.originalEvent;if(a){a.stopPropagation&&a.stopPropagation();a.cancelBubble=true}},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=ba;this.stopPropagation()},isDefaultPrevented:aa,isPropagationStopped:aa,isImmediatePropagationStopped:aa};var Ba=function(a){for(var b=a.relatedTarget;b&&b!==this;)try{b=b.parentNode}catch(d){break}if(b!==this){a.type=a.data;c.event.handle.apply(this,arguments)}},Ca=function(a){a.type=a.data;c.event.handle.apply(this,arguments)};c.each({mouseenter:"mouseover",
+mouseleave:"mouseout"},function(a,b){c.event.special[a]={setup:function(d){c.event.add(this,b,d&&d.selector?Ca:Ba,a)},teardown:function(d){c.event.remove(this,b,d&&d.selector?Ca:Ba)}}});if(!c.support.submitBubbles)c.event.special.submit={setup:function(a,b,d){if(this.nodeName.toLowerCase()!=="form"){c.event.add(this,"click.specialSubmit."+d.guid,function(f){var e=f.target,i=e.type;if((i==="submit"||i==="image")&&c(e).closest("form").length)return pa("submit",this,arguments)});c.event.add(this,"keypress.specialSubmit."+
+d.guid,function(f){var e=f.target,i=e.type;if((i==="text"||i==="password")&&c(e).closest("form").length&&f.keyCode===13)return pa("submit",this,arguments)})}else return false},remove:function(a,b){c.event.remove(this,"click.specialSubmit"+(b?"."+b.guid:""));c.event.remove(this,"keypress.specialSubmit"+(b?"."+b.guid:""))}};if(!c.support.changeBubbles){var ga=/textarea|input|select/i;function Da(a){var b=a.type,d=a.value;if(b==="radio"||b==="checkbox")d=a.checked;else if(b==="select-multiple")d=a.selectedIndex>
+-1?c.map(a.options,function(f){return f.selected}).join("-"):"";else if(a.nodeName.toLowerCase()==="select")d=a.selectedIndex;return d}function ha(a,b){var d=a.target,f,e;if(!(!ga.test(d.nodeName)||d.readOnly)){f=c.data(d,"_change_data");e=Da(d);if(e!==f){if(a.type!=="focusout"||d.type!=="radio")c.data(d,"_change_data",e);if(d.type!=="select"&&(f!=null||e)){a.type="change";return c.event.trigger(a,b,this)}}}}c.event.special.change={filters:{focusout:ha,click:function(a){var b=a.target,d=b.type;if(d===
+"radio"||d==="checkbox"||b.nodeName.toLowerCase()==="select")return ha.call(this,a)},keydown:function(a){var b=a.target,d=b.type;if(a.keyCode===13&&b.nodeName.toLowerCase()!=="textarea"||a.keyCode===32&&(d==="checkbox"||d==="radio")||d==="select-multiple")return ha.call(this,a)},beforeactivate:function(a){a=a.target;a.nodeName.toLowerCase()==="input"&&a.type==="radio"&&c.data(a,"_change_data",Da(a))}},setup:function(a,b,d){for(var f in W)c.event.add(this,f+".specialChange."+d.guid,W[f]);return ga.test(this.nodeName)},
+remove:function(a,b){for(var d in W)c.event.remove(this,d+".specialChange"+(b?"."+b.guid:""),W[d]);return ga.test(this.nodeName)}};var W=c.event.special.change.filters}s.addEventListener&&c.each({focus:"focusin",blur:"focusout"},function(a,b){function d(f){f=c.event.fix(f);f.type=b;return c.event.handle.call(this,f)}c.event.special[b]={setup:function(){this.addEventListener(a,d,true)},teardown:function(){this.removeEventListener(a,d,true)}}});c.each(["bind","one"],function(a,b){c.fn[b]=function(d,
+f,e){if(typeof d==="object"){for(var i in d)this[b](i,f,d[i],e);return this}if(c.isFunction(f)){thisObject=e;e=f;f=w}var j=b==="one"?c.proxy(e,function(o){c(this).unbind(o,j);return e.apply(this,arguments)}):e;return d==="unload"&&b!=="one"?this.one(d,f,e,thisObject):this.each(function(){c.event.add(this,d,j,f)})}});c.fn.extend({unbind:function(a,b){if(typeof a==="object"&&!a.preventDefault){for(var d in a)this.unbind(d,a[d]);return this}return this.each(function(){c.event.remove(this,a,b)})},trigger:function(a,
+b){return this.each(function(){c.event.trigger(a,b,this)})},triggerHandler:function(a,b){if(this[0]){a=c.Event(a);a.preventDefault();a.stopPropagation();c.event.trigger(a,b,this[0]);return a.result}},toggle:function(a){for(var b=arguments,d=1;d<b.length;)c.proxy(a,b[d++]);return this.click(c.proxy(a,function(f){var e=(c.data(this,"lastToggle"+a.guid)||0)%d;c.data(this,"lastToggle"+a.guid,e+1);f.preventDefault();return b[e].apply(this,arguments)||false}))},hover:function(a,b){return this.mouseenter(a).mouseleave(b||
+a)},live:function(a,b,d){if(c.isFunction(b)){d=b;b=w}c(this.context).bind(ra(a,this.selector),{data:b,selector:this.selector,live:a},d);return this},die:function(a,b){c(this.context).unbind(ra(a,this.selector),b?{guid:b.guid+this.selector+a}:null);return this}});c.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error".split(" "),function(a,b){c.fn[b]=function(d){return d?
+this.bind(b,d):this.trigger(b)};if(c.attrFn)c.attrFn[b]=true});A.attachEvent&&!A.addEventListener&&A.attachEvent("onunload",function(){for(var a in c.cache)if(c.cache[a].handle)try{c.event.remove(c.cache[a].handle.elem)}catch(b){}});(function(){function a(g){for(var h="",k,m=0;g[m];m++){k=g[m];if(k.nodeType===3||k.nodeType===4)h+=k.nodeValue;else if(k.nodeType!==8)h+=a(k.childNodes)}return h}function b(g,h,k,m,r,q){r=0;for(var v=m.length;r<v;r++){var u=m[r];if(u){u=u[g];for(var y=false;u;){if(u.sizcache===
+k){y=m[u.sizset];break}if(u.nodeType===1&&!q){u.sizcache=k;u.sizset=r}if(u.nodeName.toLowerCase()===h){y=u;break}u=u[g]}m[r]=y}}}function d(g,h,k,m,r,q){r=0;for(var v=m.length;r<v;r++){var u=m[r];if(u){u=u[g];for(var y=false;u;){if(u.sizcache===k){y=m[u.sizset];break}if(u.nodeType===1){if(!q){u.sizcache=k;u.sizset=r}if(typeof h!=="string"){if(u===h){y=true;break}}else if(p.filter(h,[u]).length>0){y=u;break}}u=u[g]}m[r]=y}}}var f=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^[\]]*\]|['"][^'"]*['"]|[^[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,
+e=0,i=Object.prototype.toString,j=false,o=true;[0,0].sort(function(){o=false;return 0});var p=function(g,h,k,m){k=k||[];var r=h=h||s;if(h.nodeType!==1&&h.nodeType!==9)return[];if(!g||typeof g!=="string")return k;for(var q=[],v,u,y,S,I=true,N=x(h),J=g;(f.exec(""),v=f.exec(J))!==null;){J=v[3];q.push(v[1]);if(v[2]){S=v[3];break}}if(q.length>1&&t.exec(g))if(q.length===2&&n.relative[q[0]])u=ia(q[0]+q[1],h);else for(u=n.relative[q[0]]?[h]:p(q.shift(),h);q.length;){g=q.shift();if(n.relative[g])g+=q.shift();
+u=ia(g,u)}else{if(!m&&q.length>1&&h.nodeType===9&&!N&&n.match.ID.test(q[0])&&!n.match.ID.test(q[q.length-1])){v=p.find(q.shift(),h,N);h=v.expr?p.filter(v.expr,v.set)[0]:v.set[0]}if(h){v=m?{expr:q.pop(),set:B(m)}:p.find(q.pop(),q.length===1&&(q[0]==="~"||q[0]==="+")&&h.parentNode?h.parentNode:h,N);u=v.expr?p.filter(v.expr,v.set):v.set;if(q.length>0)y=B(u);else I=false;for(;q.length;){var E=q.pop();v=E;if(n.relative[E])v=q.pop();else E="";if(v==null)v=h;n.relative[E](y,v,N)}}else y=[]}y||(y=u);if(!y)throw"Syntax error, unrecognized expression: "+
+(E||g);if(i.call(y)==="[object Array]")if(I)if(h&&h.nodeType===1)for(g=0;y[g]!=null;g++){if(y[g]&&(y[g]===true||y[g].nodeType===1&&F(h,y[g])))k.push(u[g])}else for(g=0;y[g]!=null;g++)y[g]&&y[g].nodeType===1&&k.push(u[g]);else k.push.apply(k,y);else B(y,k);if(S){p(S,r,k,m);p.uniqueSort(k)}return k};p.uniqueSort=function(g){if(D){j=o;g.sort(D);if(j)for(var h=1;h<g.length;h++)g[h]===g[h-1]&&g.splice(h--,1)}return g};p.matches=function(g,h){return p(g,null,null,h)};p.find=function(g,h,k){var m,r;if(!g)return[];
+for(var q=0,v=n.order.length;q<v;q++){var u=n.order[q];if(r=n.leftMatch[u].exec(g)){var y=r[1];r.splice(1,1);if(y.substr(y.length-1)!=="\\"){r[1]=(r[1]||"").replace(/\\/g,"");m=n.find[u](r,h,k);if(m!=null){g=g.replace(n.match[u],"");break}}}}m||(m=h.getElementsByTagName("*"));return{set:m,expr:g}};p.filter=function(g,h,k,m){for(var r=g,q=[],v=h,u,y,S=h&&h[0]&&x(h[0]);g&&h.length;){for(var I in n.filter)if((u=n.leftMatch[I].exec(g))!=null&&u[2]){var N=n.filter[I],J,E;E=u[1];y=false;u.splice(1,1);if(E.substr(E.length-
+1)!=="\\"){if(v===q)q=[];if(n.preFilter[I])if(u=n.preFilter[I](u,v,k,q,m,S)){if(u===true)continue}else y=J=true;if(u)for(var X=0;(E=v[X])!=null;X++)if(E){J=N(E,u,X,v);var Ea=m^!!J;if(k&&J!=null)if(Ea)y=true;else v[X]=false;else if(Ea){q.push(E);y=true}}if(J!==w){k||(v=q);g=g.replace(n.match[I],"");if(!y)return[];break}}}if(g===r)if(y==null)throw"Syntax error, unrecognized expression: "+g;else break;r=g}return v};var n=p.selectors={order:["ID","NAME","TAG"],match:{ID:/#((?:[\w\u00c0-\uFFFF-]|\\.)+)/,
+CLASS:/\.((?:[\w\u00c0-\uFFFF-]|\\.)+)/,NAME:/\[name=['"]*((?:[\w\u00c0-\uFFFF-]|\\.)+)['"]*\]/,ATTR:/\[\s*((?:[\w\u00c0-\uFFFF-]|\\.)+)\s*(?:(\S?=)\s*(['"]*)(.*?)\3|)\s*\]/,TAG:/^((?:[\w\u00c0-\uFFFF\*-]|\\.)+)/,CHILD:/:(only|nth|last|first)-child(?:\((even|odd|[\dn+-]*)\))?/,POS:/:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^-]|$)/,PSEUDO:/:((?:[\w\u00c0-\uFFFF-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/},leftMatch:{},attrMap:{"class":"className","for":"htmlFor"},attrHandle:{href:function(g){return g.getAttribute("href")}},
+relative:{"+":function(g,h){var k=typeof h==="string",m=k&&!/\W/.test(h);k=k&&!m;if(m)h=h.toLowerCase();m=0;for(var r=g.length,q;m<r;m++)if(q=g[m]){for(;(q=q.previousSibling)&&q.nodeType!==1;);g[m]=k||q&&q.nodeName.toLowerCase()===h?q||false:q===h}k&&p.filter(h,g,true)},">":function(g,h){var k=typeof h==="string";if(k&&!/\W/.test(h)){h=h.toLowerCase();for(var m=0,r=g.length;m<r;m++){var q=g[m];if(q){k=q.parentNode;g[m]=k.nodeName.toLowerCase()===h?k:false}}}else{m=0;for(r=g.length;m<r;m++)if(q=g[m])g[m]=
+k?q.parentNode:q.parentNode===h;k&&p.filter(h,g,true)}},"":function(g,h,k){var m=e++,r=d;if(typeof h==="string"&&!/\W/.test(h)){var q=h=h.toLowerCase();r=b}r("parentNode",h,m,g,q,k)},"~":function(g,h,k){var m=e++,r=d;if(typeof h==="string"&&!/\W/.test(h)){var q=h=h.toLowerCase();r=b}r("previousSibling",h,m,g,q,k)}},find:{ID:function(g,h,k){if(typeof h.getElementById!=="undefined"&&!k)return(g=h.getElementById(g[1]))?[g]:[]},NAME:function(g,h){if(typeof h.getElementsByName!=="undefined"){var k=[];
+h=h.getElementsByName(g[1]);for(var m=0,r=h.length;m<r;m++)h[m].getAttribute("name")===g[1]&&k.push(h[m]);return k.length===0?null:k}},TAG:function(g,h){return h.getElementsByTagName(g[1])}},preFilter:{CLASS:function(g,h,k,m,r,q){g=" "+g[1].replace(/\\/g,"")+" ";if(q)return g;q=0;for(var v;(v=h[q])!=null;q++)if(v)if(r^(v.className&&(" "+v.className+" ").replace(/[\t\n]/g," ").indexOf(g)>=0))k||m.push(v);else if(k)h[q]=false;return false},ID:function(g){return g[1].replace(/\\/g,"")},TAG:function(g){return g[1].toLowerCase()},
+CHILD:function(g){if(g[1]==="nth"){var h=/(-?)(\d*)n((?:\+|-)?\d*)/.exec(g[2]==="even"&&"2n"||g[2]==="odd"&&"2n+1"||!/\D/.test(g[2])&&"0n+"+g[2]||g[2]);g[2]=h[1]+(h[2]||1)-0;g[3]=h[3]-0}g[0]=e++;return g},ATTR:function(g,h,k,m,r,q){h=g[1].replace(/\\/g,"");if(!q&&n.attrMap[h])g[1]=n.attrMap[h];if(g[2]==="~=")g[4]=" "+g[4]+" ";return g},PSEUDO:function(g,h,k,m,r){if(g[1]==="not")if((f.exec(g[3])||"").length>1||/^\w/.test(g[3]))g[3]=p(g[3],null,null,h);else{g=p.filter(g[3],h,k,true^r);k||m.push.apply(m,
+g);return false}else if(n.match.POS.test(g[0])||n.match.CHILD.test(g[0]))return true;return g},POS:function(g){g.unshift(true);return g}},filters:{enabled:function(g){return g.disabled===false&&g.type!=="hidden"},disabled:function(g){return g.disabled===true},checked:function(g){return g.checked===true},selected:function(g){return g.selected===true},parent:function(g){return!!g.firstChild},empty:function(g){return!g.firstChild},has:function(g,h,k){return!!p(k[3],g).length},header:function(g){return/h\d/i.test(g.nodeName)},
+text:function(g){return"text"===g.type},radio:function(g){return"radio"===g.type},checkbox:function(g){return"checkbox"===g.type},file:function(g){return"file"===g.type},password:function(g){return"password"===g.type},submit:function(g){return"submit"===g.type},image:function(g){return"image"===g.type},reset:function(g){return"reset"===g.type},button:function(g){return"button"===g.type||g.nodeName.toLowerCase()==="button"},input:function(g){return/input|select|textarea|button/i.test(g.nodeName)}},
+setFilters:{first:function(g,h){return h===0},last:function(g,h,k,m){return h===m.length-1},even:function(g,h){return h%2===0},odd:function(g,h){return h%2===1},lt:function(g,h,k){return h<k[3]-0},gt:function(g,h,k){return h>k[3]-0},nth:function(g,h,k){return k[3]-0===h},eq:function(g,h,k){return k[3]-0===h}},filter:{PSEUDO:function(g,h,k,m){var r=h[1],q=n.filters[r];if(q)return q(g,k,h,m);else if(r==="contains")return(g.textContent||g.innerText||a([g])||"").indexOf(h[3])>=0;else if(r==="not"){h=
+h[3];k=0;for(m=h.length;k<m;k++)if(h[k]===g)return false;return true}else throw"Syntax error, unrecognized expression: "+r;},CHILD:function(g,h){var k=h[1],m=g;switch(k){case "only":case "first":for(;m=m.previousSibling;)if(m.nodeType===1)return false;if(k==="first")return true;m=g;case "last":for(;m=m.nextSibling;)if(m.nodeType===1)return false;return true;case "nth":k=h[2];var r=h[3];if(k===1&&r===0)return true;h=h[0];var q=g.parentNode;if(q&&(q.sizcache!==h||!g.nodeIndex)){var v=0;for(m=q.firstChild;m;m=
+m.nextSibling)if(m.nodeType===1)m.nodeIndex=++v;q.sizcache=h}g=g.nodeIndex-r;return k===0?g===0:g%k===0&&g/k>=0}},ID:function(g,h){return g.nodeType===1&&g.getAttribute("id")===h},TAG:function(g,h){return h==="*"&&g.nodeType===1||g.nodeName.toLowerCase()===h},CLASS:function(g,h){return(" "+(g.className||g.getAttribute("class"))+" ").indexOf(h)>-1},ATTR:function(g,h){var k=h[1];g=n.attrHandle[k]?n.attrHandle[k](g):g[k]!=null?g[k]:g.getAttribute(k);k=g+"";var m=h[2];h=h[4];return g==null?m==="!=":m===
+"="?k===h:m==="*="?k.indexOf(h)>=0:m==="~="?(" "+k+" ").indexOf(h)>=0:!h?k&&g!==false:m==="!="?k!==h:m==="^="?k.indexOf(h)===0:m==="$="?k.substr(k.length-h.length)===h:m==="|="?k===h||k.substr(0,h.length+1)===h+"-":false},POS:function(g,h,k,m){var r=n.setFilters[h[2]];if(r)return r(g,k,h,m)}}},t=n.match.POS;for(var z in n.match){n.match[z]=new RegExp(n.match[z].source+/(?![^\[]*\])(?![^\(]*\))/.source);n.leftMatch[z]=new RegExp(/(^(?:.|\r|\n)*?)/.source+n.match[z].source.replace(/\\(\d+)/g,function(g,
+h){return"\\"+(h-0+1)}))}var B=function(g,h){g=Array.prototype.slice.call(g,0);if(h){h.push.apply(h,g);return h}return g};try{Array.prototype.slice.call(s.documentElement.childNodes,0)}catch(C){B=function(g,h){h=h||[];if(i.call(g)==="[object Array]")Array.prototype.push.apply(h,g);else if(typeof g.length==="number")for(var k=0,m=g.length;k<m;k++)h.push(g[k]);else for(k=0;g[k];k++)h.push(g[k]);return h}}var D;if(s.documentElement.compareDocumentPosition)D=function(g,h){if(!g.compareDocumentPosition||
+!h.compareDocumentPosition){if(g==h)j=true;return g.compareDocumentPosition?-1:1}g=g.compareDocumentPosition(h)&4?-1:g===h?0:1;if(g===0)j=true;return g};else if("sourceIndex"in s.documentElement)D=function(g,h){if(!g.sourceIndex||!h.sourceIndex){if(g==h)j=true;return g.sourceIndex?-1:1}g=g.sourceIndex-h.sourceIndex;if(g===0)j=true;return g};else if(s.createRange)D=function(g,h){if(!g.ownerDocument||!h.ownerDocument){if(g==h)j=true;return g.ownerDocument?-1:1}var k=g.ownerDocument.createRange(),m=
+h.ownerDocument.createRange();k.setStart(g,0);k.setEnd(g,0);m.setStart(h,0);m.setEnd(h,0);g=k.compareBoundaryPoints(Range.START_TO_END,m);if(g===0)j=true;return g};(function(){var g=s.createElement("div"),h="script"+(new Date).getTime();g.innerHTML="<a name='"+h+"'/>";var k=s.documentElement;k.insertBefore(g,k.firstChild);if(s.getElementById(h)){n.find.ID=function(m,r,q){if(typeof r.getElementById!=="undefined"&&!q)return(r=r.getElementById(m[1]))?r.id===m[1]||typeof r.getAttributeNode!=="undefined"&&
+r.getAttributeNode("id").nodeValue===m[1]?[r]:w:[]};n.filter.ID=function(m,r){var q=typeof m.getAttributeNode!=="undefined"&&m.getAttributeNode("id");return m.nodeType===1&&q&&q.nodeValue===r}}k.removeChild(g);k=g=null})();(function(){var g=s.createElement("div");g.appendChild(s.createComment(""));if(g.getElementsByTagName("*").length>0)n.find.TAG=function(h,k){k=k.getElementsByTagName(h[1]);if(h[1]==="*"){h=[];for(var m=0;k[m];m++)k[m].nodeType===1&&h.push(k[m]);k=h}return k};g.innerHTML="<a href='#'></a>";
+if(g.firstChild&&typeof g.firstChild.getAttribute!=="undefined"&&g.firstChild.getAttribute("href")!=="#")n.attrHandle.href=function(h){return h.getAttribute("href",2)};g=null})();s.querySelectorAll&&function(){var g=p,h=s.createElement("div");h.innerHTML="<p class='TEST'></p>";if(!(h.querySelectorAll&&h.querySelectorAll(".TEST").length===0)){p=function(m,r,q,v){r=r||s;if(!v&&r.nodeType===9&&!x(r))try{return B(r.querySelectorAll(m),q)}catch(u){}return g(m,r,q,v)};for(var k in g)p[k]=g[k];h=null}}();
+(function(){var g=s.createElement("div");g.innerHTML="<div class='test e'></div><div class='test'></div>";if(!(!g.getElementsByClassName||g.getElementsByClassName("e").length===0)){g.lastChild.className="e";if(g.getElementsByClassName("e").length!==1){n.order.splice(1,0,"CLASS");n.find.CLASS=function(h,k,m){if(typeof k.getElementsByClassName!=="undefined"&&!m)return k.getElementsByClassName(h[1])};g=null}}})();var F=s.compareDocumentPosition?function(g,h){return g.compareDocumentPosition(h)&16}:function(g,
+h){return g!==h&&(g.contains?g.contains(h):true)},x=function(g){return(g=(g?g.ownerDocument||g:0).documentElement)?g.nodeName!=="HTML":false},ia=function(g,h){var k=[],m="",r;for(h=h.nodeType?[h]:h;r=n.match.PSEUDO.exec(g);){m+=r[0];g=g.replace(n.match.PSEUDO,"")}g=n.relative[g]?g+"*":g;r=0;for(var q=h.length;r<q;r++)p(g,h[r],k);return p.filter(m,k)};c.find=p;c.expr=p.selectors;c.expr[":"]=c.expr.filters;c.unique=p.uniqueSort;c.getText=a;c.isXMLDoc=x;c.contains=F})();var ab=/Until$/,bb=/^(?:parents|prevUntil|prevAll)/,
+cb=/,/;R=Array.prototype.slice;var Fa=function(a,b,d){if(c.isFunction(b))return c.grep(a,function(e,i){return!!b.call(e,i,e)===d});else if(b.nodeType)return c.grep(a,function(e){return e===b===d});else if(typeof b==="string"){var f=c.grep(a,function(e){return e.nodeType===1});if(Pa.test(b))return c.filter(b,f,!d);else b=c.filter(b,a)}return c.grep(a,function(e){return c.inArray(e,b)>=0===d})};c.fn.extend({find:function(a){for(var b=this.pushStack("","find",a),d=0,f=0,e=this.length;f<e;f++){d=b.length;
+c.find(a,this[f],b);if(f>0)for(var i=d;i<b.length;i++)for(var j=0;j<d;j++)if(b[j]===b[i]){b.splice(i--,1);break}}return b},has:function(a){var b=c(a);return this.filter(function(){for(var d=0,f=b.length;d<f;d++)if(c.contains(this,b[d]))return true})},not:function(a){return this.pushStack(Fa(this,a,false),"not",a)},filter:function(a){return this.pushStack(Fa(this,a,true),"filter",a)},is:function(a){return!!a&&c.filter(a,this).length>0},closest:function(a,b){if(c.isArray(a)){var d=[],f=this[0],e,i=
+{},j;if(f&&a.length){e=0;for(var o=a.length;e<o;e++){j=a[e];i[j]||(i[j]=c.expr.match.POS.test(j)?c(j,b||this.context):j)}for(;f&&f.ownerDocument&&f!==b;){for(j in i){e=i[j];if(e.jquery?e.index(f)>-1:c(f).is(e)){d.push({selector:j,elem:f});delete i[j]}}f=f.parentNode}}return d}var p=c.expr.match.POS.test(a)?c(a,b||this.context):null;return this.map(function(n,t){for(;t&&t.ownerDocument&&t!==b;){if(p?p.index(t)>-1:c(t).is(a))return t;t=t.parentNode}return null})},index:function(a){if(!a||typeof a===
+"string")return c.inArray(this[0],a?c(a):this.parent().children());return c.inArray(a.jquery?a[0]:a,this)},add:function(a,b){a=typeof a==="string"?c(a,b||this.context):c.makeArray(a);b=c.merge(this.get(),a);return this.pushStack(sa(a[0])||sa(b[0])?b:c.unique(b))},andSelf:function(){return this.add(this.prevObject)}});c.each({parent:function(a){return(a=a.parentNode)&&a.nodeType!==11?a:null},parents:function(a){return c.dir(a,"parentNode")},parentsUntil:function(a,b,d){return c.dir(a,"parentNode",
+d)},next:function(a){return c.nth(a,2,"nextSibling")},prev:function(a){return c.nth(a,2,"previousSibling")},nextAll:function(a){return c.dir(a,"nextSibling")},prevAll:function(a){return c.dir(a,"previousSibling")},nextUntil:function(a,b,d){return c.dir(a,"nextSibling",d)},prevUntil:function(a,b,d){return c.dir(a,"previousSibling",d)},siblings:function(a){return c.sibling(a.parentNode.firstChild,a)},children:function(a){return c.sibling(a.firstChild)},contents:function(a){return c.nodeName(a,"iframe")?
+a.contentDocument||a.contentWindow.document:c.makeArray(a.childNodes)}},function(a,b){c.fn[a]=function(d,f){var e=c.map(this,b,d);ab.test(a)||(f=d);if(f&&typeof f==="string")e=c.filter(f,e);e=this.length>1?c.unique(e):e;if((this.length>1||cb.test(f))&&bb.test(a))e=e.reverse();return this.pushStack(e,a,R.call(arguments).join(","))}});c.extend({filter:function(a,b,d){if(d)a=":not("+a+")";return c.find.matches(a,b)},dir:function(a,b,d){var f=[];for(a=a[b];a&&a.nodeType!==9&&(d===w||!c(a).is(d));){a.nodeType===
+1&&f.push(a);a=a[b]}return f},nth:function(a,b,d){b=b||1;for(var f=0;a;a=a[d])if(a.nodeType===1&&++f===b)break;return a},sibling:function(a,b){for(var d=[];a;a=a.nextSibling)a.nodeType===1&&a!==b&&d.push(a);return d}});var Ga=/ jQuery\d+="(?:\d+|null)"/g,Y=/^\s+/,db=/(<([\w:]+)[^>]*?)\/>/g,eb=/^(?:area|br|col|embed|hr|img|input|link|meta|param)$/i,Ha=/<([\w:]+)/,fb=/<tbody/i,gb=/<|&\w+;/,hb=function(a,b,d){return eb.test(d)?a:b+"></"+d+">"},G={option:[1,"<select multiple='multiple'>","</select>"],
+legend:[1,"<fieldset>","</fieldset>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],area:[1,"<map>","</map>"],_default:[0,"",""]};G.optgroup=G.option;G.tbody=G.tfoot=G.colgroup=G.caption=G.thead;G.th=G.td;if(!c.support.htmlSerialize)G._default=[1,"div<div>","</div>"];c.fn.extend({text:function(a){if(c.isFunction(a))return this.each(function(b){var d=c(this);
+return d.text(a.call(this,b,d.text()))});if(typeof a!=="object"&&a!==w)return this.empty().append((this[0]&&this[0].ownerDocument||s).createTextNode(a));return c.getText(this)},wrapAll:function(a){if(c.isFunction(a))return this.each(function(d){c(this).wrapAll(a.call(this,d))});if(this[0]){var b=c(a,this[0].ownerDocument).eq(0).clone(true);this[0].parentNode&&b.insertBefore(this[0]);b.map(function(){for(var d=this;d.firstChild&&d.firstChild.nodeType===1;)d=d.firstChild;return d}).append(this)}return this},
+wrapInner:function(a){return this.each(function(){var b=c(this),d=b.contents();d.length?d.wrapAll(a):b.append(a)})},wrap:function(a){return this.each(function(){c(this).wrapAll(a)})},unwrap:function(){return this.parent().each(function(){c.nodeName(this,"body")||c(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,true,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,true,function(a){this.nodeType===1&&
+this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,false,function(b){this.parentNode.insertBefore(b,this)});else if(arguments.length){var a=c(arguments[0]);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,false,function(b){this.parentNode.insertBefore(b,this.nextSibling)});else if(arguments.length){var a=this.pushStack(this,
+"after",arguments);a.push.apply(a,c(arguments[0]).toArray());return a}},clone:function(a){var b=this.map(function(){if(!c.support.noCloneEvent&&!c.isXMLDoc(this)){var d=this.outerHTML,f=this.ownerDocument;if(!d){d=f.createElement("div");d.appendChild(this.cloneNode(true));d=d.innerHTML}return c.clean([d.replace(Ga,"").replace(Y,"")],f)[0]}else return this.cloneNode(true)});if(a===true){ta(this,b);ta(this.find("*"),b.find("*"))}return b},html:function(a){if(a===w)return this[0]&&this[0].nodeType===
+1?this[0].innerHTML.replace(Ga,""):null;else if(typeof a==="string"&&!/<script/i.test(a)&&(c.support.leadingWhitespace||!Y.test(a))&&!G[(Ha.exec(a)||["",""])[1].toLowerCase()])try{for(var b=0,d=this.length;b<d;b++)if(this[b].nodeType===1){T(this[b].getElementsByTagName("*"));this[b].innerHTML=a}}catch(f){this.empty().append(a)}else c.isFunction(a)?this.each(function(e){var i=c(this),j=i.html();i.empty().append(function(){return a.call(this,e,j)})}):this.empty().append(a);return this},replaceWith:function(a){if(this[0]&&
+this[0].parentNode){c.isFunction(a)||(a=c(a).detach());return this.each(function(){var b=this.nextSibling,d=this.parentNode;c(this).remove();b?c(b).before(a):c(d).append(a)})}else return this.pushStack(c(c.isFunction(a)?a():a),"replaceWith",a)},detach:function(a){return this.remove(a,true)},domManip:function(a,b,d){function f(t){return c.nodeName(t,"table")?t.getElementsByTagName("tbody")[0]||t.appendChild(t.ownerDocument.createElement("tbody")):t}var e,i,j=a[0],o=[];if(c.isFunction(j))return this.each(function(t){var z=
+c(this);a[0]=j.call(this,t,b?z.html():w);return z.domManip(a,b,d)});if(this[0]){e=a[0]&&a[0].parentNode&&a[0].parentNode.nodeType===11?{fragment:a[0].parentNode}:ua(a,this,o);if(i=e.fragment.firstChild){b=b&&c.nodeName(i,"tr");for(var p=0,n=this.length;p<n;p++)d.call(b?f(this[p],i):this[p],e.cacheable||this.length>1||p>0?e.fragment.cloneNode(true):e.fragment)}o&&c.each(o,La)}return this}});c.fragments={};c.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},
+function(a,b){c.fn[a]=function(d){var f=[];d=c(d);for(var e=0,i=d.length;e<i;e++){var j=(e>0?this.clone(true):this).get();c.fn[b].apply(c(d[e]),j);f=f.concat(j)}return this.pushStack(f,a,d.selector)}});c.each({remove:function(a,b){if(!a||c.filter(a,[this]).length){if(!b&&this.nodeType===1){T(this.getElementsByTagName("*"));T([this])}this.parentNode&&this.parentNode.removeChild(this)}},empty:function(){for(this.nodeType===1&&T(this.getElementsByTagName("*"));this.firstChild;)this.removeChild(this.firstChild)}},
+function(a,b){c.fn[a]=function(){return this.each(b,arguments)}});c.extend({clean:function(a,b,d,f){b=b||s;if(typeof b.createElement==="undefined")b=b.ownerDocument||b[0]&&b[0].ownerDocument||s;var e=[];c.each(a,function(i,j){if(typeof j==="number")j+="";if(j){if(typeof j==="string"&&!gb.test(j))j=b.createTextNode(j);else if(typeof j==="string"){j=j.replace(db,hb);var o=(Ha.exec(j)||["",""])[1].toLowerCase(),p=G[o]||G._default,n=p[0];i=b.createElement("div");for(i.innerHTML=p[1]+j+p[2];n--;)i=i.lastChild;
+if(!c.support.tbody){n=fb.test(j);o=o==="table"&&!n?i.firstChild&&i.firstChild.childNodes:p[1]==="<table>"&&!n?i.childNodes:[];for(p=o.length-1;p>=0;--p)c.nodeName(o[p],"tbody")&&!o[p].childNodes.length&&o[p].parentNode.removeChild(o[p])}!c.support.leadingWhitespace&&Y.test(j)&&i.insertBefore(b.createTextNode(Y.exec(j)[0]),i.firstChild);j=c.makeArray(i.childNodes)}if(j.nodeType)e.push(j);else e=c.merge(e,j)}});if(d)for(a=0;e[a];a++)if(f&&c.nodeName(e[a],"script")&&(!e[a].type||e[a].type.toLowerCase()===
+"text/javascript"))f.push(e[a].parentNode?e[a].parentNode.removeChild(e[a]):e[a]);else{e[a].nodeType===1&&e.splice.apply(e,[a+1,0].concat(c.makeArray(e[a].getElementsByTagName("script"))));d.appendChild(e[a])}return e}});var ib=/z-?index|font-?weight|opacity|zoom|line-?height/i,Ia=/alpha\([^)]*\)/,Ja=/opacity=([^)]*)/,ja=/float/i,ka=/-([a-z])/ig,jb=/([A-Z])/g,kb=/^-?\d+(?:px)?$/i,lb=/^-?\d/,mb={position:"absolute",visibility:"hidden",display:"block"},nb=["Left","Right"],ob=["Top","Bottom"],pb=s.defaultView&&
+s.defaultView.getComputedStyle,Ka=c.support.cssFloat?"cssFloat":"styleFloat",la=function(a,b){return b.toUpperCase()};c.fn.css=function(a,b){return $(this,a,b,true,function(d,f,e){if(e===w)return c.curCSS(d,f);if(typeof e==="number"&&!ib.test(f))e+="px";c.style(d,f,e)})};c.extend({style:function(a,b,d){if(!a||a.nodeType===3||a.nodeType===8)return w;if((b==="width"||b==="height")&&parseFloat(d)<0)d=w;var f=a.style||a,e=d!==w;if(!c.support.opacity&&b==="opacity"){if(e){f.zoom=1;b=parseInt(d,10)+""===
+"NaN"?"":"alpha(opacity="+d*100+")";a=f.filter||c.curCSS(a,"filter")||"";f.filter=Ia.test(a)?a.replace(Ia,b):b}return f.filter&&f.filter.indexOf("opacity=")>=0?parseFloat(Ja.exec(f.filter)[1])/100+"":""}if(ja.test(b))b=Ka;b=b.replace(ka,la);if(e)f[b]=d;return f[b]},css:function(a,b,d,f){if(b==="width"||b==="height"){var e,i=b==="width"?nb:ob;function j(){e=b==="width"?a.offsetWidth:a.offsetHeight;f!=="border"&&c.each(i,function(){f||(e-=parseFloat(c.curCSS(a,"padding"+this,true))||0);if(f==="margin")e+=
+parseFloat(c.curCSS(a,"margin"+this,true))||0;else e-=parseFloat(c.curCSS(a,"border"+this+"Width",true))||0})}a.offsetWidth!==0?j():c.swap(a,mb,j);return Math.max(0,Math.round(e))}return c.curCSS(a,b,d)},curCSS:function(a,b,d){var f,e=a.style;if(!c.support.opacity&&b==="opacity"&&a.currentStyle){f=Ja.test(a.currentStyle.filter||"")?parseFloat(RegExp.$1)/100+"":"";return f===""?"1":f}if(ja.test(b))b=Ka;if(!d&&e&&e[b])f=e[b];else if(pb){if(ja.test(b))b="float";b=b.replace(jb,"-$1").toLowerCase();e=
+a.ownerDocument.defaultView;if(!e)return null;if(a=e.getComputedStyle(a,null))f=a.getPropertyValue(b);if(b==="opacity"&&f==="")f="1"}else if(a.currentStyle){d=b.replace(ka,la);f=a.currentStyle[b]||a.currentStyle[d];if(!kb.test(f)&&lb.test(f)){b=e.left;var i=a.runtimeStyle.left;a.runtimeStyle.left=a.currentStyle.left;e.left=d==="fontSize"?"1em":f||0;f=e.pixelLeft+"px";e.left=b;a.runtimeStyle.left=i}}return f},swap:function(a,b,d){var f={};for(var e in b){f[e]=a.style[e];a.style[e]=b[e]}d.call(a);for(e in b)a.style[e]=
+f[e]}});if(c.expr&&c.expr.filters){c.expr.filters.hidden=function(a){var b=a.offsetWidth,d=a.offsetHeight,f=a.nodeName.toLowerCase()==="tr";return b===0&&d===0&&!f?true:b>0&&d>0&&!f?false:c.curCSS(a,"display")==="none"};c.expr.filters.visible=function(a){return!c.expr.filters.hidden(a)}}var qb=K(),rb=/<script(.|\s)*?\/script>/gi,sb=/select|textarea/i,tb=/color|date|datetime|email|hidden|month|number|password|range|search|tel|text|time|url|week/i,O=/=\?(&|$)/,ma=/\?/,ub=/(\?|&)_=.*?(&|$)/,vb=/^(\w+:)?\/\/([^\/?#]+)/,
+wb=/%20/g;c.fn.extend({_load:c.fn.load,load:function(a,b,d){if(typeof a!=="string")return this._load(a);else if(!this.length)return this;var f=a.indexOf(" ");if(f>=0){var e=a.slice(f,a.length);a=a.slice(0,f)}f="GET";if(b)if(c.isFunction(b)){d=b;b=null}else if(typeof b==="object"){b=c.param(b,c.ajaxSettings.traditional);f="POST"}c.ajax({url:a,type:f,dataType:"html",data:b,context:this,complete:function(i,j){if(j==="success"||j==="notmodified")this.html(e?c("<div />").append(i.responseText.replace(rb,
+"")).find(e):i.responseText);d&&this.each(d,[i.responseText,j,i])}});return this},serialize:function(){return c.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?c.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||sb.test(this.nodeName)||tb.test(this.type))}).map(function(a,b){a=c(this).val();return a==null?null:c.isArray(a)?c.map(a,function(d){return{name:b.name,value:d}}):{name:b.name,value:a}}).get()}});
+c.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){c.fn[b]=function(d){return this.bind(b,d)}});c.extend({get:function(a,b,d,f){if(c.isFunction(b)){f=f||d;d=b;b=null}return c.ajax({type:"GET",url:a,data:b,success:d,dataType:f})},getScript:function(a,b){return c.get(a,null,b,"script")},getJSON:function(a,b,d){return c.get(a,b,d,"json")},post:function(a,b,d,f){if(c.isFunction(b)){f=f||d;d=b;b={}}return c.ajax({type:"POST",url:a,data:b,success:d,dataType:f})},
+ajaxSetup:function(a){c.extend(c.ajaxSettings,a)},ajaxSettings:{url:location.href,global:true,type:"GET",contentType:"application/x-www-form-urlencoded",processData:true,async:true,xhr:A.XMLHttpRequest&&(A.location.protocol!=="file:"||!A.ActiveXObject)?function(){return new A.XMLHttpRequest}:function(){try{return new A.ActiveXObject("Microsoft.XMLHTTP")}catch(a){}},accepts:{xml:"application/xml, text/xml",html:"text/html",script:"text/javascript, application/javascript",json:"application/json, text/javascript",
+text:"text/plain",_default:"*/*"}},lastModified:{},etag:{},ajax:function(a){function b(){e.success&&e.success.call(p,o,j,x);e.global&&f("ajaxSuccess",[x,e])}function d(){e.complete&&e.complete.call(p,x,j);e.global&&f("ajaxComplete",[x,e]);e.global&&!--c.active&&c.event.trigger("ajaxStop")}function f(r,q){(e.context?c(e.context):c.event).trigger(r,q)}var e=c.extend(true,{},c.ajaxSettings,a),i,j,o,p=e.context||e,n=e.type.toUpperCase();if(e.data&&e.processData&&typeof e.data!=="string")e.data=c.param(e.data,
+e.traditional);if(e.dataType==="jsonp"){if(n==="GET")O.test(e.url)||(e.url+=(ma.test(e.url)?"&":"?")+(e.jsonp||"callback")+"=?");else if(!e.data||!O.test(e.data))e.data=(e.data?e.data+"&":"")+(e.jsonp||"callback")+"=?";e.dataType="json"}if(e.dataType==="json"&&(e.data&&O.test(e.data)||O.test(e.url))){i=e.jsonpCallback||"jsonp"+qb++;if(e.data)e.data=(e.data+"").replace(O,"="+i+"$1");e.url=e.url.replace(O,"="+i+"$1");e.dataType="script";A[i]=A[i]||function(r){o=r;b();d();A[i]=w;try{delete A[i]}catch(q){}B&&
+B.removeChild(C)}}if(e.dataType==="script"&&e.cache===null)e.cache=false;if(e.cache===false&&n==="GET"){var t=K(),z=e.url.replace(ub,"$1_="+t+"$2");e.url=z+(z===e.url?(ma.test(e.url)?"&":"?")+"_="+t:"")}if(e.data&&n==="GET")e.url+=(ma.test(e.url)?"&":"?")+e.data;e.global&&!c.active++&&c.event.trigger("ajaxStart");t=(t=vb.exec(e.url))&&(t[1]&&t[1]!==location.protocol||t[2]!==location.host);if(e.dataType==="script"&&n==="GET"&&t){var B=s.getElementsByTagName("head")[0]||s.documentElement,C=s.createElement("script");
+C.src=e.url;if(e.scriptCharset)C.charset=e.scriptCharset;if(!i){var D=false;C.onload=C.onreadystatechange=function(){if(!D&&(!this.readyState||this.readyState==="loaded"||this.readyState==="complete")){D=true;b();d();C.onload=C.onreadystatechange=null;B&&C.parentNode&&B.removeChild(C)}}}B.insertBefore(C,B.firstChild);return w}var F=false,x=e.xhr();if(x){e.username?x.open(n,e.url,e.async,e.username,e.password):x.open(n,e.url,e.async);try{if(e.data||a&&a.contentType)x.setRequestHeader("Content-Type",
+e.contentType);if(e.ifModified){c.lastModified[e.url]&&x.setRequestHeader("If-Modified-Since",c.lastModified[e.url]);c.etag[e.url]&&x.setRequestHeader("If-None-Match",c.etag[e.url])}t||x.setRequestHeader("X-Requested-With","XMLHttpRequest");x.setRequestHeader("Accept",e.dataType&&e.accepts[e.dataType]?e.accepts[e.dataType]+", */*":e.accepts._default)}catch(ia){}if(e.beforeSend&&e.beforeSend.call(p,x,e)===false){e.global&&!--c.active&&c.event.trigger("ajaxStop");x.abort();return false}e.global&&f("ajaxSend",
+[x,e]);var g=x.onreadystatechange=function(r){if(!x||x.readyState===0){F||d();F=true;if(x)x.onreadystatechange=c.noop}else if(!F&&x&&(x.readyState===4||r==="timeout")){F=true;x.onreadystatechange=c.noop;j=r==="timeout"?"timeout":!c.httpSuccess(x)?"error":e.ifModified&&c.httpNotModified(x,e.url)?"notmodified":"success";if(j==="success")try{o=c.httpData(x,e.dataType,e)}catch(q){j="parsererror"}if(j==="success"||j==="notmodified")i||b();else c.handleError(e,x,j);d();r==="timeout"&&x.abort();if(e.async)x=
+null}};try{var h=x.abort;x.abort=function(){if(x){h.call(x);if(x)x.readyState=0}g()}}catch(k){}e.async&&e.timeout>0&&setTimeout(function(){x&&!F&&g("timeout")},e.timeout);try{x.send(n==="POST"||n==="PUT"||n==="DELETE"?e.data:null)}catch(m){c.handleError(e,x,null,m);d()}e.async||g();return x}},handleError:function(a,b,d,f){if(a.error)a.error.call(a.context||A,b,d,f);if(a.global)(a.context?c(a.context):c.event).trigger("ajaxError",[b,a,f])},active:0,httpSuccess:function(a){try{return!a.status&&location.protocol===
+"file:"||a.status>=200&&a.status<300||a.status===304||a.status===1223||a.status===0}catch(b){}return false},httpNotModified:function(a,b){var d=a.getResponseHeader("Last-Modified"),f=a.getResponseHeader("Etag");if(d)c.lastModified[b]=d;if(f)c.etag[b]=f;return a.status===304||a.status===0},httpData:function(a,b,d){var f=a.getResponseHeader("content-type")||"",e=b==="xml"||!b&&f.indexOf("xml")>=0;a=e?a.responseXML:a.responseText;if(e&&a.documentElement.nodeName==="parsererror")throw"parsererror";if(d&&
+d.dataFilter)a=d.dataFilter(a,b);if(typeof a==="string")if(b==="json"||!b&&f.indexOf("json")>=0)if(/^[\],:{}\s]*$/.test(a.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,"@").replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,"]").replace(/(?:^|:|,)(?:\s*\[)+/g,"")))a=A.JSON&&A.JSON.parse?A.JSON.parse(a):(new Function("return "+a))();else throw"Invalid JSON: "+a;else if(b==="script"||!b&&f.indexOf("javascript")>=0)c.globalEval(a);return a},param:function(a,b){function d(e,i){i=
+c.isFunction(i)?i():i;f[f.length]=encodeURIComponent(e)+"="+encodeURIComponent(i)}var f=[];if(b===w)b=c.ajaxSettings.traditional;c.isArray(a)||a.jquery?c.each(a,function(){d(this.name,this.value)}):c.each(a,function e(i,j){if(c.isArray(j))c.each(j,function(o,p){b?d(i,p):e(i+"["+(typeof p==="object"||c.isArray(p)?o:"")+"]",p)});else!b&&j!=null&&typeof j==="object"?c.each(j,function(o,p){e(i+"["+o+"]",p)}):d(i,j)});return f.join("&").replace(wb,"+")}});var na={},xb=/toggle|show|hide/,yb=/^([+-]=)?([\d+-.]+)(.*)$/,
+Z,va=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]];c.fn.extend({show:function(a,b){if(a!=null)return this.animate(L("show",3),a,b);else{a=0;for(b=this.length;a<b;a++){var d=c.data(this[a],"olddisplay");this[a].style.display=d||"";if(c.css(this[a],"display")==="none"){d=this[a].nodeName;var f;if(na[d])f=na[d];else{var e=c("<"+d+" />").appendTo("body");f=e.css("display");if(f==="none")f="block";e.remove();
+na[d]=f}c.data(this[a],"olddisplay",f)}}a=0;for(b=this.length;a<b;a++)this[a].style.display=c.data(this[a],"olddisplay")||"";return this}},hide:function(a,b){if(a!=null)return this.animate(L("hide",3),a,b);else{a=0;for(b=this.length;a<b;a++){var d=c.data(this[a],"olddisplay");!d&&d!=="none"&&c.data(this[a],"olddisplay",c.css(this[a],"display"))}a=0;for(b=this.length;a<b;a++)this[a].style.display="none";return this}},_toggle:c.fn.toggle,toggle:function(a,b){var d=typeof a==="boolean";if(c.isFunction(a)&&
+c.isFunction(b))this._toggle.apply(this,arguments);else a==null||d?this.each(function(){var f=d?a:c(this).is(":hidden");c(this)[f?"show":"hide"]()}):this.animate(L("toggle",3),a,b);return this},fadeTo:function(a,b,d){return this.filter(":hidden").css("opacity",0).show().end().animate({opacity:b},a,d)},animate:function(a,b,d,f){var e=c.speed(b,d,f);if(c.isEmptyObject(a))return this.each(e.complete);return this[e.queue===false?"each":"queue"](function(){var i=c.extend({},e),j,o=this.nodeType===1&&c(this).is(":hidden"),
+p=this;for(j in a){var n=j.replace(ka,la);if(j!==n){a[n]=a[j];delete a[j];j=n}if(a[j]==="hide"&&o||a[j]==="show"&&!o)return i.complete.call(this);if((j==="height"||j==="width")&&this.style){i.display=c.css(this,"display");i.overflow=this.style.overflow}if(c.isArray(a[j])){(i.specialEasing=i.specialEasing||{})[j]=a[j][1];a[j]=a[j][0]}}if(i.overflow!=null)this.style.overflow="hidden";i.curAnim=c.extend({},a);c.each(a,function(t,z){var B=new c.fx(p,i,t);if(xb.test(z))B[z==="toggle"?o?"show":"hide":z](a);
+else{var C=yb.exec(z),D=B.cur(true)||0;if(C){z=parseFloat(C[2]);var F=C[3]||"px";if(F!=="px"){p.style[t]=(z||1)+F;D=(z||1)/B.cur(true)*D;p.style[t]=D+F}if(C[1])z=(C[1]==="-="?-1:1)*z+D;B.custom(D,z,F)}else B.custom(D,z,"")}});return true})},stop:function(a,b){var d=c.timers;a&&this.queue([]);this.each(function(){for(var f=d.length-1;f>=0;f--)if(d[f].elem===this){b&&d[f](true);d.splice(f,1)}});b||this.dequeue();return this}});c.each({slideDown:L("show",1),slideUp:L("hide",1),slideToggle:L("toggle",
+1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"}},function(a,b){c.fn[a]=function(d,f){return this.animate(b,d,f)}});c.extend({speed:function(a,b,d){var f=a&&typeof a==="object"?a:{complete:d||!d&&b||c.isFunction(a)&&a,duration:a,easing:d&&b||b&&!c.isFunction(b)&&b};f.duration=c.fx.off?0:typeof f.duration==="number"?f.duration:c.fx.speeds[f.duration]||c.fx.speeds._default;f.old=f.complete;f.complete=function(){f.queue!==false&&c(this).dequeue();c.isFunction(f.old)&&f.old.call(this)};return f},easing:{linear:function(a,
+b,d,f){return d+f*a},swing:function(a,b,d,f){return(-Math.cos(a*Math.PI)/2+0.5)*f+d}},timers:[],fx:function(a,b,d){this.options=b;this.elem=a;this.prop=d;if(!b.orig)b.orig={}}});c.fx.prototype={update:function(){this.options.step&&this.options.step.call(this.elem,this.now,this);(c.fx.step[this.prop]||c.fx.step._default)(this);if((this.prop==="height"||this.prop==="width")&&this.elem.style)this.elem.style.display="block"},cur:function(a){if(this.elem[this.prop]!=null&&(!this.elem.style||this.elem.style[this.prop]==
+null))return this.elem[this.prop];return(a=parseFloat(c.css(this.elem,this.prop,a)))&&a>-10000?a:parseFloat(c.curCSS(this.elem,this.prop))||0},custom:function(a,b,d){function f(i){return e.step(i)}this.startTime=K();this.start=a;this.end=b;this.unit=d||this.unit||"px";this.now=this.start;this.pos=this.state=0;var e=this;f.elem=this.elem;if(f()&&c.timers.push(f)&&!Z)Z=setInterval(c.fx.tick,13)},show:function(){this.options.orig[this.prop]=c.style(this.elem,this.prop);this.options.show=true;this.custom(this.prop===
+"width"||this.prop==="height"?1:0,this.cur());c(this.elem).show()},hide:function(){this.options.orig[this.prop]=c.style(this.elem,this.prop);this.options.hide=true;this.custom(this.cur(),0)},step:function(a){var b=K(),d=true;if(a||b>=this.options.duration+this.startTime){this.now=this.end;this.pos=this.state=1;this.update();this.options.curAnim[this.prop]=true;for(var f in this.options.curAnim)if(this.options.curAnim[f]!==true)d=false;if(d){if(this.options.display!=null){this.elem.style.overflow=
+this.options.overflow;a=c.data(this.elem,"olddisplay");this.elem.style.display=a?a:this.options.display;if(c.css(this.elem,"display")==="none")this.elem.style.display="block"}this.options.hide&&c(this.elem).hide();if(this.options.hide||this.options.show)for(var e in this.options.curAnim)c.style(this.elem,e,this.options.orig[e]);this.options.complete.call(this.elem)}return false}else{e=b-this.startTime;this.state=e/this.options.duration;a=this.options.easing||(c.easing.swing?"swing":"linear");this.pos=
+c.easing[this.options.specialEasing&&this.options.specialEasing[this.prop]||a](this.state,e,0,1,this.options.duration);this.now=this.start+(this.end-this.start)*this.pos;this.update()}return true}};c.extend(c.fx,{tick:function(){for(var a=c.timers,b=0;b<a.length;b++)a[b]()||a.splice(b--,1);a.length||c.fx.stop()},stop:function(){clearInterval(Z);Z=null},speeds:{slow:600,fast:200,_default:400},step:{opacity:function(a){c.style(a.elem,"opacity",a.now)},_default:function(a){if(a.elem.style&&a.elem.style[a.prop]!=
+null)a.elem.style[a.prop]=(a.prop==="width"||a.prop==="height"?Math.max(0,a.now):a.now)+a.unit;else a.elem[a.prop]=a.now}}});if(c.expr&&c.expr.filters)c.expr.filters.animated=function(a){return c.grep(c.timers,function(b){return a===b.elem}).length};c.fn.offset="getBoundingClientRect"in s.documentElement?function(a){var b=this[0];if(!b||!b.ownerDocument)return null;if(a)return this.each(function(e){c.offset.setOffset(this,a,e)});if(b===b.ownerDocument.body)return c.offset.bodyOffset(b);var d=b.getBoundingClientRect(),
+f=b.ownerDocument;b=f.body;f=f.documentElement;return{top:d.top+(self.pageYOffset||c.support.boxModel&&f.scrollTop||b.scrollTop)-(f.clientTop||b.clientTop||0),left:d.left+(self.pageXOffset||c.support.boxModel&&f.scrollLeft||b.scrollLeft)-(f.clientLeft||b.clientLeft||0)}}:function(a){var b=this[0];if(!b||!b.ownerDocument)return null;if(a)return this.each(function(t){c.offset.setOffset(this,a,t)});if(b===b.ownerDocument.body)return c.offset.bodyOffset(b);c.offset.initialize();var d=b.offsetParent,f=
+b,e=b.ownerDocument,i,j=e.documentElement,o=e.body;f=(e=e.defaultView)?e.getComputedStyle(b,null):b.currentStyle;for(var p=b.offsetTop,n=b.offsetLeft;(b=b.parentNode)&&b!==o&&b!==j;){if(c.offset.supportsFixedPosition&&f.position==="fixed")break;i=e?e.getComputedStyle(b,null):b.currentStyle;p-=b.scrollTop;n-=b.scrollLeft;if(b===d){p+=b.offsetTop;n+=b.offsetLeft;if(c.offset.doesNotAddBorder&&!(c.offset.doesAddBorderForTableAndCells&&/^t(able|d|h)$/i.test(b.nodeName))){p+=parseFloat(i.borderTopWidth)||
+0;n+=parseFloat(i.borderLeftWidth)||0}f=d;d=b.offsetParent}if(c.offset.subtractsBorderForOverflowNotVisible&&i.overflow!=="visible"){p+=parseFloat(i.borderTopWidth)||0;n+=parseFloat(i.borderLeftWidth)||0}f=i}if(f.position==="relative"||f.position==="static"){p+=o.offsetTop;n+=o.offsetLeft}if(c.offset.supportsFixedPosition&&f.position==="fixed"){p+=Math.max(j.scrollTop,o.scrollTop);n+=Math.max(j.scrollLeft,o.scrollLeft)}return{top:p,left:n}};c.offset={initialize:function(){var a=s.body,b=s.createElement("div"),
+d,f,e,i=parseFloat(c.curCSS(a,"marginTop",true))||0;c.extend(b.style,{position:"absolute",top:0,left:0,margin:0,border:0,width:"1px",height:"1px",visibility:"hidden"});b.innerHTML="<div style='position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;'><div></div></div><table style='position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;' cellpadding='0' cellspacing='0'><tr><td></td></tr></table>";a.insertBefore(b,a.firstChild);
+d=b.firstChild;f=d.firstChild;e=d.nextSibling.firstChild.firstChild;this.doesNotAddBorder=f.offsetTop!==5;this.doesAddBorderForTableAndCells=e.offsetTop===5;f.style.position="fixed";f.style.top="20px";this.supportsFixedPosition=f.offsetTop===20||f.offsetTop===15;f.style.position=f.style.top="";d.style.overflow="hidden";d.style.position="relative";this.subtractsBorderForOverflowNotVisible=f.offsetTop===-5;this.doesNotIncludeMarginInBodyOffset=a.offsetTop!==i;a.removeChild(b);c.offset.initialize=c.noop},
+bodyOffset:function(a){var b=a.offsetTop,d=a.offsetLeft;c.offset.initialize();if(c.offset.doesNotIncludeMarginInBodyOffset){b+=parseFloat(c.curCSS(a,"marginTop",true))||0;d+=parseFloat(c.curCSS(a,"marginLeft",true))||0}return{top:b,left:d}},setOffset:function(a,b,d){if(/static/.test(c.curCSS(a,"position")))a.style.position="relative";var f=c(a),e=f.offset(),i=parseInt(c.curCSS(a,"top",true),10)||0,j=parseInt(c.curCSS(a,"left",true),10)||0;if(c.isFunction(b))b=b.call(a,d,e);d={top:b.top-e.top+i,left:b.left-
+e.left+j};"using"in b?b.using.call(a,d):f.css(d)}};c.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),d=this.offset(),f=/^body|html$/i.test(b[0].nodeName)?{top:0,left:0}:b.offset();d.top-=parseFloat(c.curCSS(a,"marginTop",true))||0;d.left-=parseFloat(c.curCSS(a,"marginLeft",true))||0;f.top+=parseFloat(c.curCSS(b[0],"borderTopWidth",true))||0;f.left+=parseFloat(c.curCSS(b[0],"borderLeftWidth",true))||0;return{top:d.top-f.top,left:d.left-f.left}},offsetParent:function(){return this.map(function(){for(var a=
+this.offsetParent||s.body;a&&!/^body|html$/i.test(a.nodeName)&&c.css(a,"position")==="static";)a=a.offsetParent;return a})}});c.each(["Left","Top"],function(a,b){var d="scroll"+b;c.fn[d]=function(f){var e=this[0],i;if(!e)return null;if(f!==w)return this.each(function(){if(i=wa(this))i.scrollTo(!a?f:c(i).scrollLeft(),a?f:c(i).scrollTop());else this[d]=f});else return(i=wa(e))?"pageXOffset"in i?i[a?"pageYOffset":"pageXOffset"]:c.support.boxModel&&i.document.documentElement[d]||i.document.body[d]:e[d]}});
+c.each(["Height","Width"],function(a,b){var d=b.toLowerCase();c.fn["inner"+b]=function(){return this[0]?c.css(this[0],d,false,"padding"):null};c.fn["outer"+b]=function(f){return this[0]?c.css(this[0],d,false,f?"margin":"border"):null};c.fn[d]=function(f){var e=this[0];if(!e)return f==null?null:this;return"scrollTo"in e&&e.document?e.document.compatMode==="CSS1Compat"&&e.document.documentElement["client"+b]||e.document.body["client"+b]:e.nodeType===9?Math.max(e.documentElement["client"+b],e.body["scroll"+
+b],e.documentElement["scroll"+b],e.body["offset"+b],e.documentElement["offset"+b]):f===w?c.css(e,d):this.css(d,typeof f==="string"?f:f+"px")}});A.jQuery=A.$=c})(window);
diff --git a/devtools/client/inspector/markup/test/lib_jquery_1.6_min.js b/devtools/client/inspector/markup/test/lib_jquery_1.6_min.js
new file mode 100644
index 000000000..c72011dfa
--- /dev/null
+++ b/devtools/client/inspector/markup/test/lib_jquery_1.6_min.js
@@ -0,0 +1,16 @@
+/*!
+ * jQuery JavaScript Library v1.6
+ * http://jquery.com/
+ *
+ * Copyright 2011, John Resig
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * Includes Sizzle.js
+ * http://sizzlejs.com/
+ * Copyright 2011, The Dojo Foundation
+ * Released under the MIT, BSD, and GPL Licenses.
+ *
+ * Date: Mon May 2 13:50:00 2011 -0400
+ */
+(function(a,b){function cw(a){return f.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:!1}function ct(a){if(!ch[a]){var b=f("<"+a+">").appendTo("body"),d=b.css("display");b.remove();if(d==="none"||d===""){ci||(ci=c.createElement("iframe"),ci.frameBorder=ci.width=ci.height=0),c.body.appendChild(ci);if(!cj||!ci.createElement)cj=(ci.contentWindow||ci.contentDocument).document,cj.write("<!doctype><html><body></body></html>");b=cj.createElement(a),cj.body.appendChild(b),d=f.css(b,"display"),c.body.removeChild(ci)}ch[a]=d}return ch[a]}function cs(a,b){var c={};f.each(cn.concat.apply([],cn.slice(0,b)),function(){c[this]=a});return c}function cr(){co=b}function cq(){setTimeout(cr,0);return co=f.now()}function cg(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}function cf(){try{return new a.XMLHttpRequest}catch(b){}}function b_(a,c){a.dataFilter&&(c=a.dataFilter(c,a.dataType));var d=a.dataTypes,e={},g,h,i=d.length,j,k=d[0],l,m,n,o,p;for(g=1;g<i;g++){if(g===1)for(h in a.converters)typeof h=="string"&&(e[h.toLowerCase()]=a.converters[h]);l=k,k=d[g];if(k==="*")k=l;else if(l!=="*"&&l!==k){m=l+" "+k,n=e[m]||e["* "+k];if(!n){p=b;for(o in e){j=o.split(" ");if(j[0]===l||j[0]==="*"){p=e[j[1]+" "+k];if(p){o=e[o],o===!0?n=p:p===!0&&(n=o);break}}}}!n&&!p&&f.error("No conversion from "+m.replace(" "," to ")),n!==!0&&(c=n?n(c):p(o(c)))}}return c}function b$(a,c,d){var e=a.contents,f=a.dataTypes,g=a.responseFields,h,i,j,k;for(i in g)i in d&&(c[g[i]]=d[i]);while(f[0]==="*")f.shift(),h===b&&(h=a.mimeType||c.getResponseHeader("content-type"));if(h)for(i in e)if(e[i]&&e[i].test(h)){f.unshift(i);break}if(f[0]in d)j=f[0];else{for(i in d){if(!f[0]||a.converters[i+" "+f[0]]){j=i;break}k||(k=i)}j=j||k}if(j){j!==f[0]&&f.unshift(j);return d[j]}}function bZ(a,b,c,d){if(f.isArray(b))f.each(b,function(b,e){c||bD.test(a)?d(a,e):bZ(a+"["+(typeof e=="object"||f.isArray(e)?b:"")+"]",e,c,d)});else if(!c&&b!=null&&typeof b=="object")for(var e in b)bZ(a+"["+e+"]",b[e],c,d);else d(a,b)}function bY(a,c,d,e,f,g){f=f||c.dataTypes[0],g=g||{},g[f]=!0;var h=a[f],i=0,j=h?h.length:0,k=a===bS,l;for(;i<j&&(k||!l);i++)l=h[i](c,d,e),typeof l=="string"&&(!k||g[l]?l=b:(c.dataTypes.unshift(l),l=bY(a,c,d,e,l,g)));(k||!l)&&!g["*"]&&(l=bY(a,c,d,e,"*",g));return l}function bX(a){return function(b,c){typeof b!="string"&&(c=b,b="*");if(f.isFunction(c)){var d=b.toLowerCase().split(bO),e=0,g=d.length,h,i,j;for(;e<g;e++)h=d[e],j=/^\+/.test(h),j&&(h=h.substr(1)||"*"),i=a[h]=a[h]||[],i[j?"unshift":"push"](c)}}}function bB(a,b,c){var d=b==="width"?bv:bw,e=b==="width"?a.offsetWidth:a.offsetHeight;if(c==="border")return e;f.each(d,function(){c||(e-=parseFloat(f.css(a,"padding"+this))||0),c==="margin"?e+=parseFloat(f.css(a,"margin"+this))||0:e-=parseFloat(f.css(a,"border"+this+"Width"))||0});return e}function bl(a,b){b.src?f.ajax({url:b.src,async:!1,dataType:"script"}):f.globalEval(b.text||b.textContent||b.innerHTML||""),b.parentNode&&b.parentNode.removeChild(b)}function bk(a){f.nodeName(a,"input")?bj(a):a.getElementsByTagName&&f.grep(a.getElementsByTagName("input"),bj)}function bj(a){if(a.type==="checkbox"||a.type==="radio")a.defaultChecked=a.checked}function bi(a){return"getElementsByTagName"in a?a.getElementsByTagName("*"):"querySelectorAll"in a?a.querySelectorAll("*"):[]}function bh(a,b){var c;if(b.nodeType===1){b.clearAttributes&&b.clearAttributes(),b.mergeAttributes&&b.mergeAttributes(a),c=b.nodeName.toLowerCase();if(c==="object")b.outerHTML=a.outerHTML;else if(c!=="input"||a.type!=="checkbox"&&a.type!=="radio"){if(c==="option")b.selected=a.defaultSelected;else if(c==="input"||c==="textarea")b.defaultValue=a.defaultValue}else a.checked&&(b.defaultChecked=b.checked=a.checked),b.value!==a.value&&(b.value=a.value);b.removeAttribute(f.expando)}}function bg(a,b){if(b.nodeType===1&&!!f.hasData(a)){var c=f.expando,d=f.data(a),e=f.data(b,d);if(d=d[c]){var g=d.events;e=e[c]=f.extend({},d);if(g){delete e.handle,e.events={};for(var h in g)for(var i=0,j=g[h].length;i<j;i++)f.event.add(b,h+(g[h][i].namespace?".":"")+g[h][i].namespace,g[h][i],g[h][i].data)}}}}function bf(a,b){return f.nodeName(a,"table")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function W(a,b,c){b=b||0;if(f.isFunction(b))return f.grep(a,function(a,d){var e=!!b.call(a,d,a);return e===c});if(b.nodeType)return f.grep(a,function(a,d){return a===b===c});if(typeof b=="string"){var d=f.grep(a,function(a){return a.nodeType===1});if(R.test(b))return f.filter(b,d,!c);b=f.filter(b,d)}return f.grep(a,function(a,d){return f.inArray(a,b)>=0===c})}function V(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function N(a,b){return(a&&a!=="*"?a+".":"")+b.replace(z,"`").replace(A,"&")}function M(a){var b,c,d,e,g,h,i,j,k,l,m,n,o,p=[],q=[],r=f._data(this,"events");if(!(a.liveFired===this||!r||!r.live||a.target.disabled||a.button&&a.type==="click")){a.namespace&&(n=new RegExp("(^|\\.)"+a.namespace.split(".").join("\\.(?:.*\\.)?")+"(\\.|$)")),a.liveFired=this;var s=r.live.slice(0);for(i=0;i<s.length;i++)g=s[i],g.origType.replace(x,"")===a.type?q.push(g.selector):s.splice(i--,1);e=f(a.target).closest(q,a.currentTarget);for(j=0,k=e.length;j<k;j++){m=e[j];for(i=0;i<s.length;i++){g=s[i];if(m.selector===g.selector&&(!n||n.test(g.namespace))&&!m.elem.disabled){h=m.elem,d=null;if(g.preType==="mouseenter"||g.preType==="mouseleave")a.type=g.preType,d=f(a.relatedTarget).closest(g.selector)[0],d&&f.contains(h,d)&&(d=h);(!d||d!==h)&&p.push({elem:h,handleObj:g,level:m.level})}}}for(j=0,k=p.length;j<k;j++){e=p[j];if(c&&e.level>c)break;a.currentTarget=e.elem,a.data=e.handleObj.data,a.handleObj=e.handleObj,o=e.handleObj.origHandler.apply(e.elem,arguments);if(o===!1||a.isPropagationStopped()){c=e.level,o===!1&&(b=!1);if(a.isImmediatePropagationStopped())break}}return b}}function K(a,c,d){var e=f.extend({},d[0]);e.type=a,e.originalEvent={},e.liveFired=b,f.event.handle.call(c,e),e.isDefaultPrevented()&&d[0].preventDefault()}function E(){return!0}function D(){return!1}function m(a,c,d){var e=c+"defer",g=c+"queue",h=c+"mark",i=f.data(a,e,b,!0);i&&(d==="queue"||!f.data(a,g,b,!0))&&(d==="mark"||!f.data(a,h,b,!0))&&setTimeout(function(){!f.data(a,g,b,!0)&&!f.data(a,h,b,!0)&&(f.removeData(a,e,!0),i.resolve())},0)}function l(a){for(var b in a)if(b!=="toJSON")return!1;return!0}function k(a,c,d){if(d===b&&a.nodeType===1){name="data-"+c.replace(j,"$1-$2").toLowerCase(),d=a.getAttribute(name);if(typeof d=="string"){try{d=d==="true"?!0:d==="false"?!1:d==="null"?null:f.isNaN(d)?i.test(d)?f.parseJSON(d):d:parseFloat(d)}catch(e){}f.data(a,c,d)}else d=b}return d}var c=a.document,d=a.navigator,e=a.location,f=function(){function H(){if(!e.isReady){try{c.documentElement.doScroll("left")}catch(a){setTimeout(H,1);return}e.ready()}}var e=function(a,b){return new e.fn.init(a,b,h)},f=a.jQuery,g=a.$,h,i=/^(?:[^<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/,j=/\S/,k=/^\s+/,l=/\s+$/,m=/\d/,n=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,o=/^[\],:{}\s]*$/,p=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,q=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,r=/(?:^|:|,)(?:\s*\[)+/g,s=/(webkit)[ \/]([\w.]+)/,t=/(opera)(?:.*version)?[ \/]([\w.]+)/,u=/(msie) ([\w.]+)/,v=/(mozilla)(?:.*? rv:([\w.]+))?/,w=d.userAgent,x,y,z,A=Object.prototype.toString,B=Object.prototype.hasOwnProperty,C=Array.prototype.push,D=Array.prototype.slice,E=String.prototype.trim,F=Array.prototype.indexOf,G={};e.fn=e.prototype={constructor:e,init:function(a,d,f){var g,h,j,k;if(!a)return this;if(a.nodeType){this.context=this[0]=a,this.length=1;return this}if(a==="body"&&!d&&c.body){this.context=c,this[0]=c.body,this.selector=a,this.length=1;return this}if(typeof a=="string"){a.charAt(0)==="<"&&a.charAt(a.length-1)===">"&&a.length>=3?g=[null,a,null]:g=i.exec(a);if(g&&(g[1]||!d)){if(g[1]){d=d instanceof e?d[0]:d,k=d?d.ownerDocument||d:c,j=n.exec(a),j?e.isPlainObject(d)?(a=[c.createElement(j[1])],e.fn.attr.call(a,d,!0)):a=[k.createElement(j[1])]:(j=e.buildFragment([g[1]],[k]),a=(j.cacheable?e.clone(j.fragment):j.fragment).childNodes);return e.merge(this,a)}h=c.getElementById(g[2]);if(h&&h.parentNode){if(h.id!==g[2])return f.find(a);this.length=1,this[0]=h}this.context=c,this.selector=a;return this}return!d||d.jquery?(d||f).find(a):this.constructor(d).find(a)}if(e.isFunction(a))return f.ready(a);a.selector!==b&&(this.selector=a.selector,this.context=a.context);return e.makeArray(a,this)},selector:"",jquery:"1.6",length:0,size:function(){return this.length},toArray:function(){return D.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var d=this.constructor();e.isArray(a)?C.apply(d,a):e.merge(d,a),d.prevObject=this,d.context=this.context,b==="find"?d.selector=this.selector+(this.selector?" ":"")+c:b&&(d.selector=this.selector+"."+b+"("+c+")");return d},each:function(a,b){return e.each(this,a,b)},ready:function(a){e.bindReady(),y.done(a);return this},eq:function(a){return a===-1?this.slice(a):this.slice(a,+a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(D.apply(this,arguments),"slice",D.call(arguments).join(","))},map:function(a){return this.pushStack(e.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:C,sort:[].sort,splice:[].splice},e.fn.init.prototype=e.fn,e.extend=e.fn.extend=function(){var a,c,d,f,g,h,i=arguments[0]||{},j=1,k=arguments.length,l=!1;typeof i=="boolean"&&(l=i,i=arguments[1]||{},j=2),typeof i!="object"&&!e.isFunction(i)&&(i={}),k===j&&(i=this,--j);for(;j<k;j++)if((a=arguments[j])!=null)for(c in a){d=i[c],f=a[c];if(i===f)continue;l&&f&&(e.isPlainObject(f)||(g=e.isArray(f)))?(g?(g=!1,h=d&&e.isArray(d)?d:[]):h=d&&e.isPlainObject(d)?d:{},i[c]=e.extend(l,h,f)):f!==b&&(i[c]=f)}return i},e.extend({noConflict:function(b){a.$===e&&(a.$=g),b&&a.jQuery===e&&(a.jQuery=f);return e},isReady:!1,readyWait:1,holdReady:function(a){a?e.readyWait++:e.ready(!0)},ready:function(a){if(a===!0&&!--e.readyWait||a!==!0&&!e.isReady){if(!c.body)return setTimeout(e.ready,1);e.isReady=!0;if(a!==!0&&--e.readyWait>0)return;y.resolveWith(c,[e]),e.fn.trigger&&e(c).trigger("ready").unbind("ready")}},bindReady:function(){if(!y){y=e._Deferred();if(c.readyState==="complete")return setTimeout(e.ready,1);if(c.addEventListener)c.addEventListener("DOMContentLoaded",z,!1),a.addEventListener("load",e.ready,!1);else if(c.attachEvent){c.attachEvent("onreadystatechange",z),a.attachEvent("onload",e.ready);var b=!1;try{b=a.frameElement==null}catch(d){}c.documentElement.doScroll&&b&&H()}}},isFunction:function(a){return e.type(a)==="function"},isArray:Array.isArray||function(a){return e.type(a)==="array"},isWindow:function(a){return a&&typeof a=="object"&&"setInterval"in a},isNaN:function(a){return a==null||!m.test(a)||isNaN(a)},type:function(a){return a==null?String(a):G[A.call(a)]||"object"},isPlainObject:function(a){if(!a||e.type(a)!=="object"||a.nodeType||e.isWindow(a))return!1;if(a.constructor&&!B.call(a,"constructor")&&!B.call(a.constructor.prototype,"isPrototypeOf"))return!1;var c;for(c in a);return c===b||B.call(a,c)},isEmptyObject:function(a){for(var b in a)return!1;return!0},error:function(a){throw a},parseJSON:function(b){if(typeof b!="string"||!b)return null;b=e.trim(b);if(a.JSON&&a.JSON.parse)return a.JSON.parse(b);if(o.test(b.replace(p,"@").replace(q,"]").replace(r,"")))return(new Function("return "+b))();e.error("Invalid JSON: "+b)},parseXML:function(b,c,d){a.DOMParser?(d=new DOMParser,c=d.parseFromString(b,"text/xml")):(c=new ActiveXObject("Microsoft.XMLDOM"),c.async="false",c.loadXML(b)),d=c.documentElement,(!d||!d.nodeName||d.nodeName==="parsererror")&&e.error("Invalid XML: "+b);return c},noop:function(){},globalEval:function(b){b&&j.test(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,c,d){var f,g=0,h=a.length,i=h===b||e.isFunction(a);if(d){if(i){for(f in a)if(c.apply(a[f],d)===!1)break}else for(;g<h;)if(c.apply(a[g++],d)===!1)break}else if(i){for(f in a)if(c.call(a[f],f,a[f])===!1)break}else for(;g<h;)if(c.call(a[g],g,a[g++])===!1)break;return a},trim:E?function(a){return a==null?"":E.call(a)}:function(a){return a==null?"":(a+"").replace(k,"").replace(l,"")},makeArray:function(a,b){var c=b||[];if(a!=null){var d=e.type(a);a.length==null||d==="string"||d==="function"||d==="regexp"||e.isWindow(a)?C.call(c,a):e.merge(c,a)}return c},inArray:function(a,b){if(F)return F.call(b,a);for(var c=0,d=b.length;c<d;c++)if(b[c]===a)return c;return-1},merge:function(a,c){var d=a.length,e=0;if(typeof c.length=="number")for(var f=c.length;e<f;e++)a[d++]=c[e];else while(c[e]!==b)a[d++]=c[e++];a.length=d;return a},grep:function(a,b,c){var d=[],e;c=!!c;for(var f=0,g=a.length;f<g;f++)e=!!b(a[f],f),c!==e&&d.push(a[f]);return d},map:function(a,c,d){var f,g,h=[],i=0,j=a.length,k=a instanceof e||j!==b&&typeof j=="number"&&(j>0&&a[0]&&a[j-1]||j===0||e.isArray(a));if(k)for(;i<j;i++)f=c(a[i],i,d),f!=null&&(h[h.length]=f);else for(g in a)f=c(a[g],g,d),f!=null&&(h[h.length]=f);return h.concat.apply([],h)},guid:1,proxy:function(a,c){if(typeof c=="string"){var d=a[c];c=a,a=d}if(!e.isFunction(a))return b;var f=D.call(arguments,2),g=function(){return a.apply(c,f.concat(D.call(arguments)))};g.guid=a.guid=a.guid||g.guid||e.guid++;return g},access:function(a,c,d,f,g,h){var i=a.length;if(typeof c=="object"){for(var j in c)e.access(a,j,c[j],f,g,d);return a}if(d!==b){f=!h&&f&&e.isFunction(d);for(var k=0;k<i;k++)g(a[k],c,f?d.call(a[k],k,g(a[k],c)):d,h);return a}return i?g(a[0],c):b},now:function(){return(new Date).getTime()},uaMatch:function(a){a=a.toLowerCase();var b=s.exec(a)||t.exec(a)||u.exec(a)||a.indexOf("compatible")<0&&v.exec(a)||[];return{browser:b[1]||"",version:b[2]||"0"}},sub:function(){function a(b,c){return new a.fn.init(b,c)}e.extend(!0,a,this),a.superclass=this,a.fn=a.prototype=this(),a.fn.constructor=a,a.sub=this.sub,a.fn.init=function(c,d){d&&d instanceof e&&!(d instanceof a)&&(d=a(d));return e.fn.init.call(this,c,d,b)},a.fn.init.prototype=a.fn;var b=a(c);return a},browser:{}}),e.each("Boolean Number String Function Array Date RegExp Object".split(" "),function(a,b){G["[object "+b+"]"]=b.toLowerCase()}),x=e.uaMatch(w),x.browser&&(e.browser[x.browser]=!0,e.browser.version=x.version),e.browser.webkit&&(e.browser.safari=!0),j.test(" ")&&(k=/^[\s\xA0]+/,l=/[\s\xA0]+$/),h=e(c),c.addEventListener?z=function(){c.removeEventListener("DOMContentLoaded",z,!1),e.ready()}:c.attachEvent&&(z=function(){c.readyState==="complete"&&(c.detachEvent("onreadystatechange",z),e.ready())});return e}(),g="done fail isResolved isRejected promise then always pipe".split(" "),h=[].slice;f.extend({_Deferred:function(){var a=[],b,c,d,e={done:function(){if(!d){var c=arguments,g,h,i,j,k;b&&(k=b,b=0);for(g=0,h=c.length;g<h;g++)i=c[g],j=f.type(i),j==="array"?e.done.apply(e,i):j==="function"&&a.push(i);k&&e.resolveWith(k[0],k[1])}return this},resolveWith:function(e,f){if(!d&&!b&&!c){f=f||[],c=1;try{while(a[0])a.shift().apply(e,f)}finally{b=[e,f],c=0}}return this},resolve:function(){e.resolveWith(this,arguments);return this},isResolved:function(){return!!c||!!b},cancel:function(){d=1,a=[];return this}};return e},Deferred:function(a){var b=f._Deferred(),c=f._Deferred(),d;f.extend(b,{then:function(a,c){b.done(a).fail(c);return this},always:function(){return b.done.apply(b,arguments).fail.apply(this,arguments)},fail:c.done,rejectWith:c.resolveWith,reject:c.resolve,isRejected:c.isResolved,pipe:function(a,c){return f.Deferred(function(d){f.each({done:[a,"resolve"],fail:[c,"reject"]},function(a,c){var e=c[0],g=c[1],h;f.isFunction(e)?b[a](function(){h=e.apply(this,arguments),f.isFunction(h.promise)?h.promise().then(d.resolve,d.reject):d[g](h)}):b[a](d[g])})}).promise()},promise:function(a){if(a==null){if(d)return d;d=a={}}var c=g.length;while(c--)a[g[c]]=b[g[c]];return a}}),b.done(c.cancel).fail(b.cancel),delete b.cancel,a&&a.call(b,b);return b},when:function(a){function i(a){return function(c){b[a]=arguments.length>1?h.call(arguments,0):c,--e||g.resolveWith(g,h.call(b,0))}}var b=arguments,c=0,d=b.length,e=d,g=d<=1&&a&&f.isFunction(a.promise)?a:f.Deferred();if(d>1){for(;c<d;c++)b[c]&&f.isFunction(b[c].promise)?b[c].promise().then(i(c),g.reject):--e;e||g.resolveWith(g,b)}else g!==a&&g.resolveWith(g,d?[a]:[]);return g.promise()}}),f.support=function(){var a=c.createElement("div"),b,d,e,f,g,h,i,j,k,l,m,n,o,p,q;a.setAttribute("className","t"),a.innerHTML=" <link/><table></table><a href='/a' style='top:1px;float:left;opacity:.55;'>a</a><input type='checkbox'/>",b=a.getElementsByTagName("*"),d=a.getElementsByTagName("a")[0];if(!b||!b.length||!d)return{};e=c.createElement("select"),f=e.appendChild(c.createElement("option")),g=a.getElementsByTagName("input")[0],i={leadingWhitespace:a.firstChild.nodeType===3,tbody:!a.getElementsByTagName("tbody").length,htmlSerialize:!!a.getElementsByTagName("link").length,style:/top/.test(d.getAttribute("style")),hrefNormalized:d.getAttribute("href")==="/a",opacity:/^0.55$/.test(d.style.opacity),cssFloat:!!d.style.cssFloat,checkOn:g.value==="on",optSelected:f.selected,getSetAttribute:a.className!=="t",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0},g.checked=!0,i.noCloneChecked=g.cloneNode(!0).checked,e.disabled=!0,i.optDisabled=!f.disabled;try{delete a.test}catch(r){i.deleteExpando=!1}!a.addEventListener&&a.attachEvent&&a.fireEvent&&(a.attachEvent("onclick",function click(){i.noCloneEvent=!1,a.detachEvent("onclick",click)}),a.cloneNode(!0).fireEvent("onclick")),g=c.createElement("input"),g.value="t",g.setAttribute("type","radio"),i.radioValue=g.value==="t",g.setAttribute("checked","checked"),a.appendChild(g),j=c.createDocumentFragment(),j.appendChild(a.firstChild),i.checkClone=j.cloneNode(!0).cloneNode(!0).lastChild.checked,a.innerHTML="",a.style.width=a.style.paddingLeft="1px",k=c.createElement("body"),l={visibility:"hidden",width:0,height:0,border:0,margin:0,background:"none"};for(p in l)k.style[p]=l[p];k.appendChild(a),c.documentElement.appendChild(k),i.appendChecked=g.checked,i.boxModel=a.offsetWidth===2,"zoom"in a.style&&(a.style.display="inline",a.style.zoom=1,i.inlineBlockNeedsLayout=a.offsetWidth===2,a.style.display="",a.innerHTML="<div style='width:4px;'></div>",i.shrinkWrapBlocks=a.offsetWidth!==2),a.innerHTML="<table><tr><td style='padding:0;border:0;display:none'></td><td>t</td></tr></table>",m=a.getElementsByTagName("td"),q=m[0].offsetHeight===0,m[0].style.display="",m[1].style.display="none",i.reliableHiddenOffsets=q&&m[0].offsetHeight===0,a.innerHTML="",c.defaultView&&c.defaultView.getComputedStyle&&(h=c.createElement("div"),h.style.width="0",h.style.marginRight="0",a.appendChild(h),i.reliableMarginRight=(parseInt(c.defaultView.getComputedStyle(h,null).marginRight,10)||0)===0),k.innerHTML="",c.documentElement.removeChild(k);if(a.attachEvent)for(p in{submit:1,change:1,focusin:1})o="on"+p,q=o in a,q||(a.setAttribute(o,"return;"),q=typeof a[o]=="function"),i[p+"Bubbles"]=q;return i}(),f.boxModel=f.support.boxModel;var i=/^(?:\{.*\}|\[.*\])$/,j=/([a-z])([A-Z])/g;f.extend({cache:{},uuid:0,expando:"jQuery"+(f.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){a=a.nodeType?f.cache[a[f.expando]]:a[f.expando];return!!a&&!l(a)},data:function(a,c,d,e){if(!!f.acceptData(a)){var g=f.expando,h=typeof c=="string",i,j=a.nodeType,k=j?f.cache:a,l=j?a[f.expando]:a[f.expando]&&f.expando;if((!l||e&&l&&!k[l][g])&&h&&d===b)return;l||(j?a[f.expando]=l=++f.uuid:l=f.expando),k[l]||(k[l]={},j||(k[l].toJSON=f.noop));if(typeof c=="object"||typeof c=="function")e?k[l][g]=f.extend(k[l][g],c):k[l]=f.extend(k[l],c);i=k[l],e&&(i[g]||(i[g]={}),i=i[g]),d!==b&&(i[c]=d);if(c==="events"&&!i[c])return i[g]&&i[g].events;return h?i[c]:i}},removeData:function(b,c,d){if(!!f.acceptData(b)){var e=f.expando,g=b.nodeType,h=g?f.cache:b,i=g?b[f.expando]:f.expando;if(!h[i])return;if(c){var j=d?h[i][e]:h[i];if(j){delete j[c];if(!l(j))return}}if(d){delete h[i][e];if(!l(h[i]))return}var k=h[i][e];f.support.deleteExpando||h!=a?delete h[i]:h[i]=null,k?(h[i]={},g||(h[i].toJSON=f.noop),h[i][e]=k):g&&(f.support.deleteExpando?delete b[f.expando]:b.removeAttribute?b.removeAttribute(f.expando):b[f.expando]=null)}},_data:function(a,b,c){return f.data(a,b,c,!0)},acceptData:function(a){if(a.nodeName){var b=f.noData[a.nodeName.toLowerCase()];if(b)return b!==!0&&a.getAttribute("classid")===b}return!0}}),f.fn.extend({data:function(a,c){var d=null;if(typeof a=="undefined"){if(this.length){d=f.data(this[0]);if(this[0].nodeType===1){var e=this[0].attributes,g;for(var h=0,i=e.length;h<i;h++)g=e[h].name,g.indexOf("data-")===0&&(g=f.camelCase(g.substring(5)),k(this[0],g,d[g]))}}return d}if(typeof a=="object")return this.each(function(){f.data(this,a)});var j=a.split(".");j[1]=j[1]?"."+j[1]:"";if(c===b){d=this.triggerHandler("getData"+j[1]+"!",[j[0]]),d===b&&this.length&&(d=f.data(this[0],a),d=k(this[0],a,d));return d===b&&j[1]?this.data(j[0]):d}return this.each(function(){var b=f(this),d=[j[0],c];b.triggerHandler("setData"+j[1]+"!",d),f.data(this,a,c),b.triggerHandler("changeData"+j[1]+"!",d)})},removeData:function(a){return this.each(function(){f.removeData(this,a)})}}),f.extend({_mark:function(a,c){a&&(c=(c||"fx")+"mark",f.data(a,c,(f.data(a,c,b,!0)||0)+1,!0))},_unmark:function(a,c,d){a!==!0&&(d=c,c=a,a=!1);if(c){d=d||"fx";var e=d+"mark",g=a?0:(f.data(c,e,b,!0)||1)-1;g?f.data(c,e,g,!0):(f.removeData(c,e,!0),m(c,d,"mark"))}},queue:function(a,c,d){if(a){c=(c||"fx")+"queue";var e=f.data(a,c,b,!0);d&&(!e||f.isArray(d)?e=f.data(a,c,f.makeArray(d),!0):e.push(d));return e||[]}},dequeue:function(a,b){b=b||"fx";var c=f.queue(a,b),d=c.shift(),e;d==="inprogress"&&(d=c.shift()),d&&(b==="fx"&&c.unshift("inprogress"),d.call(a,function(){f.dequeue(a,b)})),c.length||(f.removeData(a,b+"queue",!0),m(a,b,"queue"))}}),f.fn.extend({queue:function(a,c){typeof a!="string"&&(c=a,a="fx");if(c===b)return f.queue(this[0],a);return this.each(function(){var b=f.queue(this,a,c);a==="fx"&&b[0]!=="inprogress"&&f.dequeue(this,a)})},dequeue:function(a){return this.each(function(){f.dequeue(this,a)})},delay:function(a,b){a=f.fx?f.fx.speeds[a]||a:a,b=b||"fx";return this.queue(b,function(){var c=this;setTimeout(function(){f.dequeue(c,b)},a)})},clearQueue:function(a){return this.queue(a||"fx",[])},promise:function(a,c){function l(){--h||d.resolveWith(e,[e])}typeof a!="string"&&(c=a,a=b),a=a||"fx";var d=f.Deferred(),e=this,g=e.length,h=1,i=a+"defer",j=a+"queue",k=a+"mark";while(g--)if(tmp=f.data(e[g],i,b,!0)||(f.data(e[g],j,b,!0)||f.data(e[g],k,b,!0))&&f.data(e[g],i,f._Deferred(),!0))h++,tmp.done(l);l();return d.promise()}});var n=/[\n\t\r]/g,o=/\s+/,p=/\r/g,q=/^(?:button|input)$/i,r=/^(?:button|input|object|select|textarea)$/i,s=/^a(?:rea)?$/i,t=/^(?:data-|aria-)/,u=/\:/,v;f.fn.extend({attr:function(a,b){return f.access(this,a,b,!0,f.attr)},removeAttr:function(a){return this.each(function(){f.removeAttr(this,a)})},prop:function(a,b){return f.access(this,a,b,!0,f.prop)},removeProp:function(a){return this.each(function(){try{this[a]=b,delete this[a]}catch(c){}})},addClass:function(a){if(f.isFunction(a))return this.each(function(b){var c=f(this);c.addClass(a.call(this,b,c.attr("class")||""))});if(a&&typeof a=="string"){var b=(a||"").split(o);for(var c=0,d=this.length;c<d;c++){var e=this[c];if(e.nodeType===1)if(!e.className)e.className=a;else{var g=" "+e.className+" ",h=e.className;for(var i=0,j=b.length;i<j;i++)g.indexOf(" "+b[i]+" ")<0&&(h+=" "+b[i]);e.className=f.trim(h)}}}return this},removeClass:function(a){if(f.isFunction(a))return this.each(function(b){var c=f(this);c.removeClass(a.call(this,b,c.attr("class")))});if(a&&typeof a=="string"||a===b){var c=(a||"").split(o);for(var d=0,e=this.length;d<e;d++){var g=this[d];if(g.nodeType===1&&g.className)if(a){var h=(" "+g.className+" ").replace(n," ");for(var i=0,j=c.length;i<j;i++)h=h.replace(" "+c[i]+" "," ");g.className=f.trim(h)}else g.className=""}}return this},toggleClass:function(a,b){var c=typeof a,d=typeof b=="boolean";if(f.isFunction(a))return this.each(function(c){var d=f(this);d.toggleClass(a.call(this,c,d.attr("class"),b),b)});return this.each(function(){if(c==="string"){var e,g=0,h=f(this),i=b,j=a.split(o);while(e=j[g++])i=d?i:!h.hasClass(e),h[i?"addClass":"removeClass"](e)}else if(c==="undefined"||c==="boolean")this.className&&f._data(this,"__className__",this.className),this.className=this.className||a===!1?"":f._data(this,"__className__")||""})},hasClass:function(a){var b=" "+a+" ";for(var c=0,d=this.length;c<d;c++)if((" "+this[c].className+" ").replace(n," ").indexOf(b)>-1)return!0;return!1},val:function(a){var c,d,e=this[0];if(!arguments.length){if(e){c=f.valHooks[e.nodeName.toLowerCase()]||f.valHooks[e.type];if(c&&"get"in c&&(d=c.get(e,"value"))!==b)return d;return(e.value||"").replace(p,"")}return b}var g=f.isFunction(a);return this.each(function(d){var e=f(this),h;if(this.nodeType===1){g?h=a.call(this,d,e.val()):h=a,h==null?h="":typeof h=="number"?h+="":f.isArray(h)&&(h=f.map(h,function(a){return a==null?"":a+""})),c=f.valHooks[this.nodeName.toLowerCase()]||f.valHooks[this.type];if(!c||"set"in c&&c.set(this,h,"value")===b)this.value=h}})}}),f.extend({valHooks:{option:{get:function(a){var b=a.attributes.value;return!b||b.specified?a.value:a.text}},select:{get:function(a){var b=a.selectedIndex,c=[],d=a.options,e=a.type==="select-one";if(b<0)return null;for(var g=e?b:0,h=e?b+1:d.length;g<h;g++){var i=d[g];if(i.selected&&(f.support.optDisabled?!i.disabled:i.getAttribute("disabled")===null)&&(!i.parentNode.disabled||!f.nodeName(i.parentNode,"optgroup"))){value=f(i).val();if(e)return value;c.push(value)}}if(e&&!c.length&&d.length)return f(d[b]).val();return c},set:function(a,b){var c=f.makeArray(b);f(a).find("option").each(function(){this.selected=f.inArray(f(this).val(),c)>=0}),c.length||(a.selectedIndex=-1);return c}}},attrFn:{val:!0,css:!0,html:!0,text:!0,data:!0,width:!0,height:!0,offset:!0},attrFix:{tabindex:"tabIndex",readonly:"readOnly"},attr:function(a,c,d,e){var g=a.nodeType;if(!a||g===3||g===8||g===2)return b;if(e&&c in f.attrFn)return f(a)[c](d);var h,i,j=g!==1||!f.isXMLDoc(a);c=j&&f.attrFix[c]||c,i=f.attrHooks[c]||(v&&(f.nodeName(a,"form")||u.test(c))?v:b);if(d!==b){if(d===null||d===!1&&!t.test(c)){f.removeAttr(a,c);return b}if(i&&"set"in i&&j&&(h=i.set(a,d,c))!==b)return h;d===!0&&!t.test(c)&&(d=c),a.setAttribute(c,""+d);return d}if(i&&"get"in i&&j)return i.get(a,c);h=a.getAttribute(c);return h===null?b:h},removeAttr:function(a,b){a.nodeType===1&&(b=f.attrFix[b]||b,f.support.getSetAttribute?a.removeAttribute(b):(f.attr(a,b,""),a.removeAttributeNode(a.getAttributeNode(b))))},attrHooks:{type:{set:function(a,b){if(q.test(a.nodeName)&&a.parentNode)f.error("type property can't be changed");else if(!f.support.radioValue&&b==="radio"&&f.nodeName(a,"input")){var c=a.getAttribute("value");a.setAttribute("type",b),c&&(a.value=c);return b}}},tabIndex:{get:function(a){var c=a.getAttributeNode("tabIndex");return c&&c.specified?parseInt(c.value,10):r.test(a.nodeName)||s.test(a.nodeName)&&a.href?0:b}}},propFix:{},prop:function(a,c,d){var e=a.nodeType;if(!a||e===3||e===8||e===2)return b;var g,h,i=e!==1||!f.isXMLDoc(a);c=i&&f.propFix[c]||c,h=f.propHooks[c];return d!==b?h&&"set"in h&&(g=h.set(a,d,c))!==b?g:a[c]=d:h&&"get"in h&&(g=h.get(a,c))!==b?g:a[c]},propHooks:{}}),f.support.getSetAttribute||(f.attrFix=f.extend(f.attrFix,{"for":"htmlFor","class":"className",maxlength:"maxLength",cellspacing:"cellSpacing",cellpadding:"cellPadding",rowspan:"rowSpan",colspan:"colSpan",usemap:"useMap",frameborder:"frameBorder"}),v=f.attrHooks.name=f.attrHooks.value=f.valHooks.button={get:function(a,c){var d;if(c==="value"&&!f.nodeName(a,"button"))return a.getAttribute(c);d=a.getAttributeNode(c);return d&&d.specified?d.nodeValue:b},set:function(a,b,c){var d=a.getAttributeNode(c);if(d){d.nodeValue=b;return b}}},f.each(["width","height"],function(a,b){f.attrHooks[b]=f.extend(f.attrHooks[b],{set:function(a,c){if(c===""){a.setAttribute(b,"auto");return c}}})})),f.support.hrefNormalized||f.each(["href","src","width","height"],function(a,c){f.attrHooks[c]=f.extend(f.attrHooks[c],{get:function(a){var d=a.getAttribute(c,2);return d===null?b:d}})}),f.support.style||(f.attrHooks.style={get:function(a){return a.style.cssText.toLowerCase()||b},set:function(a,b){return a.style.cssText=""+b}}),f.support.optSelected||(f.propHooks.selected=f.extend(f.propHooks.selected,{get:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}})),f.support.checkOn||f.each(["radio","checkbox"],function(){f.valHooks[this]={get:function(a){return a.getAttribute("value")===null?"on":a.value}}}),f.each(["radio","checkbox"],function(){f.valHooks[this]=f.extend(f.valHooks[this],{set:function(a,b){if(f.isArray(b))return a.checked=f.inArray(f(a).val(),b)>=0}})});var w=Object.prototype.hasOwnProperty,x=/\.(.*)$/,y=/^(?:textarea|input|select)$/i,z=/\./g,A=/ /g,B=/[^\w\s.|`]/g,C=function(a){return a.replace(B,"\\$&")};f.event={add:function(a,c,d,e){if(a.nodeType!==3&&a.nodeType!==8){if(d===!1)d=D;else if(!d)return;var g,h;d.handler&&(g=d,d=g.handler),d.guid||(d.guid=f.guid++);var i=f._data(a);if(!i)return;var j=i.events,k=i.handle;j||(i.events=j={}),k||(i.handle=k=function(a){return typeof f!="undefined"&&(!a||f.event.triggered!==a.type)?f.event.handle.apply(k.elem,arguments):b}),k.elem=a,c=c.split(" ");var l,m=0,n;while(l=c[m++]){h=g?f.extend({},g):{handler:d,data:e},l.indexOf(".")>-1?(n=l.split("."),l=n.shift(),h.namespace=n.slice(0).sort().join(".")):(n=[],h.namespace=""),h.type=l,h.guid||(h.guid=d.guid);var o=j[l],p=f.event.special[l]||{};if(!o){o=j[l]=[];if(!p.setup||p.setup.call(a,e,n,k)===!1)a.addEventListener?a.addEventListener(l,k,!1):a.attachEvent&&a.attachEvent("on"+l,k)}p.add&&(p.add.call(a,h),h.handler.guid||(h.handler.guid=d.guid)),o.push(h),f.event.global[l]=!0}a=null}},global:{},remove:function(a,c,d,e){if(a.nodeType!==3&&a.nodeType!==8){d===!1&&(d=D);var g,h,i,j,k=0,l,m,n,o,p,q,r,s=f.hasData(a)&&f._data(a),t=s&&s.events;if(!s||!t)return;c&&c.type&&(d=c.handler,c=c.type);if(!c||typeof c=="string"&&c.charAt(0)==="."){c=c||"";for(h in t)f.event.remove(a,h+c);return}c=c.split(" ");while(h=c[k++]){r=h,q=null,l=h.indexOf(".")<0,m=[],l||(m=h.split("."),h=m.shift(),n=new RegExp("(^|\\.)"+f.map(m.slice(0).sort(),C).join("\\.(?:.*\\.)?")+"(\\.|$)")),p=t[h];if(!p)continue;if(!d){for(j=0;j<p.length;j++){q=p[j];if(l||n.test(q.namespace))f.event.remove(a,r,q.handler,j),p.splice(j--,1)}continue}o=f.event.special[h]||{};for(j=e||0;j<p.length;j++){q=p[j];if(d.guid===q.guid){if(l||n.test(q.namespace))e==null&&p.splice(j--,1),o.remove&&o.remove.call(a,q);if(e!=null)break}}if(p.length===0||e!=null&&p.length===1)(!o.teardown||o.teardown.call(a,m)===!1)&&f.removeEvent(a,h,s.handle),g=null,delete t[h]}if(f.isEmptyObject(t)){var u=s.handle;u&&(u.elem=null),delete s.events,delete s.handle,f.isEmptyObject(s)&&f.removeData(a,b,!0)}}},customEvent:{getData:!0,setData:!0,changeData:!0},trigger:function(c,d,e,g){var h=c.type||c,i=[],j;h.indexOf("!")>=0&&(h=h.slice(0,-1),j=!0),h.indexOf(".")>=0&&(i=h.split("."),h=i.shift(),i.sort());if(!!e&&!f.event.customEvent[h]||!!f.event.global[h]){c=typeof c=="object"?c[f.expando]?c:new f.Event(h,c):new f.Event(h),c.type=h,c.exclusive=j,c.namespace=i.join("."),c.namespace_re=new RegExp("(^|\\.)"+i.join("\\.(?:.*\\.)?")+"(\\.|$)");if(g||!e)c.preventDefault(),c.stopPropagation();if(!e){f.each(f.cache,function(){var a=f.expando,b=this[a];b&&b.events&&b.events[h]&&f.event.trigger(c,d,b.handle.elem)});return}if(e.nodeType===3||e.nodeType===8)return;c.result=b,c.target=e,d=d?f.makeArray(d):[],d.unshift(c);var k=e,l=h.indexOf(":")<0?"on"+h:"";do{var m=f._data(k,"handle");c.currentTarget=k,m&&m.apply(k,d),l&&f.acceptData(k)&&k[l]&&k[l].apply(k,d)===!1&&(c.result=!1,c.preventDefault()),k=k.parentNode||k.ownerDocument||k===c.target.ownerDocument&&a}while(k&&!c.isPropagationStopped());if(!c.isDefaultPrevented()){var n,o=f.event.special[h]||{};if((!o._default||o._default.call(e.ownerDocument,c)===!1)&&(h!=="click"||!f.nodeName(e,"a"))&&f.acceptData(e)){try{l&&e[h]&&(n=e[l],n&&(e[l]=null),f.event.triggered=h,e[h]())}catch(p){}n&&(e[l]=n),f.event.triggered=b}}return c.result}},handle:function(c){c=f.event.fix(c||a.event);var d=((f._data(this,"events")||{})[c.type]||[]).slice(0),e=!c.exclusive&&!c.namespace,g=Array.prototype.slice.call(arguments,0);g[0]=c,c.currentTarget=this;for(var h=0,i=d.length;h<i;h++){var j=d[h];if(e||c.namespace_re.test(j.namespace)){c.handler=j.handler,c.data=j.data,c.handleObj=j;var k=j.handler.apply(this,g);k!==b&&(c.result=k,k===!1&&(c.preventDefault(),c.stopPropagation()));if(c.isImmediatePropagationStopped())break}}return c.result},props:"altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode layerX layerY metaKey newValue offsetX offsetY pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "),fix:function(a){if(a[f.expando])return a;var d=a;a=f.Event(d);for(var e=this.props.length,g;e;)g=this.props[--e],a[g]=d[g];a.target||(a.target=a.srcElement||c),a.target.nodeType===3&&(a.target=a.target.parentNode),!a.relatedTarget&&a.fromElement&&(a.relatedTarget=a.fromElement===a.target?a.toElement:a.fromElement);if(a.pageX==null&&a.clientX!=null){var h=a.target.ownerDocument||c,i=h.documentElement,j=h.body;a.pageX=a.clientX+(i&&i.scrollLeft||j&&j.scrollLeft||0)-(i&&i.clientLeft||j&&j.clientLeft||0),a.pageY=a.clientY+(i&&i.scrollTop||j&&j.scrollTop||0)-(i&&i.clientTop||j&&j.clientTop||0)}a.which==null&&(a.charCode!=null||a.keyCode!=null)&&(a.which=a.charCode!=null?a.charCode:a.keyCode),!a.metaKey&&a.ctrlKey&&(a.metaKey=a.ctrlKey),!a.which&&a.button!==b&&(a.which=a.button&1?1:a.button&2?3:a.button&4?2:0);return a},guid:1e8,proxy:f.proxy,special:{ready:{setup:f.bindReady,teardown:f.noop},live:{add:function(a){f.event.add(this,N(a.origType,a.selector),f.extend({},a,{handler:M,guid:a.handler.guid}))},remove:function(a){f.event.remove(this,N(a.origType,a.selector),a)}},beforeunload:{setup:function(a,b,c){f.isWindow(this)&&(this.onbeforeunload=c)},teardown:function(a,b){this.onbeforeunload===b&&(this.onbeforeunload=null)}}}},f.removeEvent=c.removeEventListener?function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c,!1)}:function(a,b,c){a.detachEvent&&a.detachEvent("on"+b,c)},f.Event=function(a,b){if(!this.preventDefault)return new f.Event(a,b);a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||a.returnValue===!1||a.getPreventDefault&&a.getPreventDefault()?E:D):this.type=a,b&&f.extend(this,b),this.timeStamp=f.now(),this[f.expando]=!0},f.Event.prototype={preventDefault:function(){this.isDefaultPrevented=E;var a=this.originalEvent;!a||(a.preventDefault?a.preventDefault():a.returnValue=!1)},stopPropagation:function(){this.isPropagationStopped=E;var a=this.originalEvent;!a||(a.stopPropagation&&a.stopPropagation(),a.cancelBubble=!0)},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=E,this.stopPropagation()},isDefaultPrevented:D,isPropagationStopped:D,isImmediatePropagationStopped:D};var F=function(a){var b=a.relatedTarget;try{if(b&&b!==c&&!b.parentNode)return;while(b&&b!==this)b=b.parentNode;b!==this&&(a.type=a.data,f.event.handle.apply(this,arguments))}catch(d){}},G=function(a){a.type=a.data,f.event.handle.apply(this,arguments)};f.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(a,b){f.event.special[a]={setup:function(c){f.event.add(this,b,c&&c.selector?G:F,a)},teardown:function(a){f.event.remove(this,b,a&&a.selector?G:F)}}}),f.support.submitBubbles||(f.event.special.submit={setup:function(a,b){if(!f.nodeName(this,"form"))f.event.add(this,"click.specialSubmit",function(a){var b=a.target,c=b.type;(c==="submit"||c==="image")&&f(b).closest("form").length&&K("submit",this,arguments)}),f.event.add(this,"keypress.specialSubmit",function(a){var b=a.target,c=b.type;(c==="text"||c==="password")&&f(b).closest("form").length&&a.keyCode===13&&K("submit",this,arguments)});else return!1},teardown:function(a){f.event.remove(this,".specialSubmit")}});if(!f.support.changeBubbles){var H,I=function(a){var b=a.type,c=a.value;b==="radio"||b==="checkbox"?c=a.checked:b==="select-multiple"?c=a.selectedIndex>-1?f.map(a.options,function(a){return a.selected}).join("-"):"":f.nodeName(a,"select")&&(c=a.selectedIndex);return c},J=function J(a){var c=a.target,d,e;if(!!y.test(c.nodeName)&&!c.readOnly){d=f._data(c,"_change_data"),e=I(c),(a.type!=="focusout"||c.type!=="radio")&&f._data(c,"_change_data",e);if(d===b||e===d)return;if(d!=null||e)a.type="change",a.liveFired=b,f.event.trigger(a,arguments[1],c)}};f.event.special.change={filters:{focusout:J,beforedeactivate:J,click:function(a){var b=a.target,c=f.nodeName(b,"input")?b.type:"";(c==="radio"||c==="checkbox"||f.nodeName(b,"select"))&&J.call(this,a)},keydown:function(a){var b=a.target,c=f.nodeName(b,"input")?b.type:"";(a.keyCode===13&&!f.nodeName(b,"textarea")||a.keyCode===32&&(c==="checkbox"||c==="radio")||c==="select-multiple")&&J.call(this,a)},beforeactivate:function(a){var b=a.target;f._data(b,"_change_data",I(b))}},setup:function(a,b){if(this.type==="file")return!1;for(var c in H)f.event.add(this,c+".specialChange",H[c]);return y.test(this.nodeName)},teardown:function(a){f.event.remove(this,".specialChange");return y.test(this.nodeName)}},H=f.event.special.change.filters,H.focus=H.beforeactivate}f.support.focusinBubbles||f.each({focus:"focusin",blur:"focusout"},function(a,b){function e(a){var c=f.event.fix(a);c.type=b,c.originalEvent={},f.event.trigger(c,null,c.target),c.isDefaultPrevented()&&a.preventDefault()}var d=0;f.event.special[b]={setup:function(){d++===0&&c.addEventListener(a,e,!0)},teardown:function(){--d===0&&c.removeEventListener(a,e,!0)}}}),f.each(["bind","one"],function(a,c){f.fn[c]=function(a,d,e){var g;if(typeof a=="object"){for(var h in a)this[c](h,d,a[h],e);return this}if(arguments.length===2||d===!1)e=d,d=b;c==="one"?(g=function(a){f(this).unbind(a,g);return e.apply(this,arguments)},g.guid=e.guid||f.guid++):g=e;if(a==="unload"&&c!=="one")this.one(a,d,e);else for(var i=0,j=this.length;i<j;i++)f.event.add(this[i],a,g,d);return this}}),f.fn.extend({unbind:function(a,b){if(typeof a=="object"&&!a.preventDefault)for(var c in a)this.unbind(c,a[c]);else for(var d=0,e=this.length;d<e;d++)f.event.remove(this[d],a,b);return this},delegate:function(a,b,c,d){return this.live(b,c,d,a)},undelegate:function(a,b,c){return arguments.length===0?this.unbind("live"):this.die(b,null,c,a)},trigger:function(a,b){return this.each(function(){f.event.trigger(a,b,this)})},triggerHandler:function(a,b){if(this[0])return f.event.trigger(a,b,this[0],!0)},toggle:function(a){var b=arguments,c=a.guid||f.guid++,d=0,e=function(c){var e=(f.data(this,"lastToggle"+a.guid)||0)%d;f.data(this,"lastToggle"+a.guid,e+1),c.preventDefault();return b[e].apply(this,arguments)||!1};e.guid=c;while(d<b.length)b[d++].guid=c;return this.click(e)},hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}});var L={focus:"focusin",blur:"focusout",mouseenter:"mouseover",mouseleave:"mouseout"};f.each(["live","die"],function(a,c){f.fn[c]=function(a,d,e,g){var h,i=0,j,k,l,m=g||this.selector,n=g?this:f(this.context);if(typeof a=="object"&&!a.preventDefault){for(var o in a)n[c](o,d,a[o],m);return this}if(c==="die"&&!a&&g&&g.charAt(0)==="."){n.unbind(g);return this}if(d===!1||f.isFunction(d))e=d||D,d=b;a=(a||"").split(" ");while((h=a[i++])!=null){j=x.exec(h),k="",j&&(k=j[0],h=h.replace(x,""));if(h==="hover"){a.push("mouseenter"+k,"mouseleave"+k);continue}l=h,L[h]?(a.push(L[h]+k),h=h+k):h=(L[h]||h)+k;if(c==="live")for(var p=0,q=n.length;p<q;p++)f.event.add(n[p],"live."+N(h,m),{data:d,selector:m,handler:e,origType:h,origHandler:e,preType:l});else n.unbind("live."+N(h,m),e)}return this}}),f.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error".split(" "),function(a,b){f.fn[b]=function(a,c){c==null&&(c=a,a=null);return arguments.length>0?this.bind(b,a,c):this.trigger(b)},f.attrFn&&(f.attrFn[b]=!0)}),function(){function u(a,b,c,d,e,f){for(var g=0,h=d.length;g<h;g++){var i=d[g];if(i){var j=!1;i=i[a];while(i){if(i.sizcache===c){j=d[i.sizset];break}if(i.nodeType===1){f||(i.sizcache=c,i.sizset=g);if(typeof b!="string"){if(i===b){j=!0;break}}else if(k.filter(b,[i]).length>0){j=i;break}}i=i[a]}d[g]=j}}}function t(a,b,c,d,e,f){for(var g=0,h=d.length;g<h;g++){var i=d[g];if(i){var j=!1;i=i[a];while(i){if(i.sizcache===c){j=d[i.sizset];break}i.nodeType===1&&!f&&(i.sizcache=c,i.sizset=g);if(i.nodeName.toLowerCase()===b){j=i;break}i=i[a]}d[g]=j}}}var a=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,d=0,e=Object.prototype.toString,g=!1,h=!0,i=/\\/g,j=/\W/;[0,0].sort(function(){h=!1;return 0});var k=function(b,d,f,g){f=f||[],d=d||c;var h=d;if(d.nodeType!==1&&d.nodeType!==9)return[];if(!b||typeof b!="string")return f;var i,j,n,o,q,r,s,t,u=!0,w=k.isXML(d),x=[],y=b;do{a.exec(""),i=a.exec(y);if(i){y=i[3],x.push(i[1]);if(i[2]){o=i[3];break}}}while(i);if(x.length>1&&m.exec(b))if(x.length===2&&l.relative[x[0]])j=v(x[0]+x[1],d);else{j=l.relative[x[0]]?[d]:k(x.shift(),d);while(x.length)b=x.shift(),l.relative[b]&&(b+=x.shift()),j=v(b,j)}else{!g&&x.length>1&&d.nodeType===9&&!w&&l.match.ID.test(x[0])&&!l.match.ID.test(x[x.length-1])&&(q=k.find(x.shift(),d,w),d=q.expr?k.filter(q.expr,q.set)[0]:q.set[0]);if(d){q=g?{expr:x.pop(),set:p(g)}:k.find(x.pop(),x.length===1&&(x[0]==="~"||x[0]==="+")&&d.parentNode?d.parentNode:d,w),j=q.expr?k.filter(q.expr,q.set):q.set,x.length>0?n=p(j):u=!1;while(x.length)r=x.pop(),s=r,l.relative[r]?s=x.pop():r="",s==null&&(s=d),l.relative[r](n,s,w)}else n=x=[]}n||(n=j),n||k.error(r||b);if(e.call(n)==="[object Array]")if(!u)f.push.apply(f,n);else if(d&&d.nodeType===1)for(t=0;n[t]!=null;t++)n[t]&&(n[t]===!0||n[t].nodeType===1&&k.contains(d,n[t]))&&f.push(j[t]);else for(t=0;n[t]!=null;t++)n[t]&&n[t].nodeType===1&&f.push(j[t]);else p(n,f);o&&(k(o,h,f,g),k.uniqueSort(f));return f};k.uniqueSort=function(a){if(r){g=h,a.sort(r);if(g)for(var b=1;b<a.length;b++)a[b]===a[b-1]&&a.splice(b--,1)}return a},k.matches=function(a,b){return k(a,null,null,b)},k.matchesSelector=function(a,b){return k(b,null,null,[a]).length>0},k.find=function(a,b,c){var d;if(!a)return[];for(var e=0,f=l.order.length;e<f;e++){var g,h=l.order[e];if(g=l.leftMatch[h].exec(a)){var j=g[1];g.splice(1,1);if(j.substr(j.length-1)!=="\\"){g[1]=(g[1]||"").replace(i,""),d=l.find[h](g,b,c);if(d!=null){a=a.replace(l.match[h],"");break}}}}d||(d=typeof b.getElementsByTagName!="undefined"?b.getElementsByTagName("*"):[]);return{set:d,expr:a}},k.filter=function(a,c,d,e){var f,g,h=a,i=[],j=c,m=c&&c[0]&&k.isXML(c[0]);while(a&&c.length){for(var n in l.filter)if((f=l.leftMatch[n].exec(a))!=null&&f[2]){var o,p,q=l.filter[n],r=f[1];g=!1,f.splice(1,1);if(r.substr(r.length-1)==="\\")continue;j===i&&(i=[]);if(l.preFilter[n]){f=l.preFilter[n](f,j,d,i,e,m);if(!f)g=o=!0;else if(f===!0)continue}if(f)for(var s=0;(p=j[s])!=null;s++)if(p){o=q(p,f,s,j);var t=e^!!o;d&&o!=null?t?g=!0:j[s]=!1:t&&(i.push(p),g=!0)}if(o!==b){d||(j=i),a=a.replace(l.match[n],"");if(!g)return[];break}}if(a===h)if(g==null)k.error(a);else break;h=a}return j},k.error=function(a){throw"Syntax error, unrecognized expression: "+a};var l=k.selectors={order:["ID","NAME","TAG"],match:{ID:/#((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,CLASS:/\.((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,NAME:/\[name=['"]*((?:[\w\u00c0-\uFFFF\-]|\\.)+)['"]*\]/,ATTR:/\[\s*((?:[\w\u00c0-\uFFFF\-]|\\.)+)\s*(?:(\S?=)\s*(?:(['"])(.*?)\3|(#?(?:[\w\u00c0-\uFFFF\-]|\\.)*)|)|)\s*\]/,TAG:/^((?:[\w\u00c0-\uFFFF\*\-]|\\.)+)/,CHILD:/:(only|nth|last|first)-child(?:\(\s*(even|odd|(?:[+\-]?\d+|(?:[+\-]?\d*)?n\s*(?:[+\-]\s*\d+)?))\s*\))?/,POS:/:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^\-]|$)/,PSEUDO:/:((?:[\w\u00c0-\uFFFF\-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/},leftMatch:{},attrMap:{"class":"className","for":"htmlFor"},attrHandle:{href:function(a){return a.getAttribute("href")},type:function(a){return a.getAttribute("type")}},relative:{"+":function(a,b){var c=typeof b=="string",d=c&&!j.test(b),e=c&&!d;d&&(b=b.toLowerCase());for(var f=0,g=a.length,h;f<g;f++)if(h=a[f]){while((h=h.previousSibling)&&h.nodeType!==1);a[f]=e||h&&h.nodeName.toLowerCase()===b?h||!1:h===b}e&&k.filter(b,a,!0)},">":function(a,b){var c,d=typeof b=="string",e=0,f=a.length;if(d&&!j.test(b)){b=b.toLowerCase();for(;e<f;e++){c=a[e];if(c){var g=c.parentNode;a[e]=g.nodeName.toLowerCase()===b?g:!1}}}else{for(;e<f;e++)c=a[e],c&&(a[e]=d?c.parentNode:c.parentNode===b);d&&k.filter(b,a,!0)}},"":function(a,b,c){var e,f=d++,g=u;typeof b=="string"&&!j.test(b)&&(b=b.toLowerCase(),e=b,g=t),g("parentNode",b,f,a,e,c)},"~":function(a,b,c){var e,f=d++,g=u;typeof b=="string"&&!j.test(b)&&(b=b.toLowerCase(),e=b,g=t),g("previousSibling",b,f,a,e,c)}},find:{ID:function(a,b,c){if(typeof b.getElementById!="undefined"&&!c){var d=b.getElementById(a[1]);return d&&d.parentNode?[d]:[]}},NAME:function(a,b){if(typeof b.getElementsByName!="undefined"){var c=[],d=b.getElementsByName(a[1]);for(var e=0,f=d.length;e<f;e++)d[e].getAttribute("name")===a[1]&&c.push(d[e]);return c.length===0?null:c}},TAG:function(a,b){if(typeof b.getElementsByTagName!="undefined")return b.getElementsByTagName(a[1])}},preFilter:{CLASS:function(a,b,c,d,e,f){a=" "+a[1].replace(i,"")+" ";if(f)return a;for(var g=0,h;(h=b[g])!=null;g++)h&&(e^(h.className&&(" "+h.className+" ").replace(/[\t\n\r]/g," ").indexOf(a)>=0)?c||d.push(h):c&&(b[g]=!1));return!1},ID:function(a){return a[1].replace(i,"")},TAG:function(a,b){return a[1].replace(i,"").toLowerCase()},CHILD:function(a){if(a[1]==="nth"){a[2]||k.error(a[0]),a[2]=a[2].replace(/^\+|\s*/g,"");var b=/(-?)(\d*)(?:n([+\-]?\d*))?/.exec(a[2]==="even"&&"2n"||a[2]==="odd"&&"2n+1"||!/\D/.test(a[2])&&"0n+"+a[2]||a[2]);a[2]=b[1]+(b[2]||1)-0,a[3]=b[3]-0}else a[2]&&k.error(a[0]);a[0]=d++;return a},ATTR:function(a,b,c,d,e,f){var g=a[1]=a[1].replace(i,"");!f&&l.attrMap[g]&&(a[1]=l.attrMap[g]),a[4]=(a[4]||a[5]||"").replace(i,""),a[2]==="~="&&(a[4]=" "+a[4]+" ");return a},PSEUDO:function(b,c,d,e,f){if(b[1]==="not")if((a.exec(b[3])||"").length>1||/^\w/.test(b[3]))b[3]=k(b[3],null,null,c);else{var g=k.filter(b[3],c,d,!0^f);d||e.push.apply(e,g);return!1}else if(l.match.POS.test(b[0])||l.match.CHILD.test(b[0]))return!0;return b},POS:function(a){a.unshift(!0);return a}},filters:{enabled:function(a){return a.disabled===!1&&a.type!=="hidden"},disabled:function(a){return a.disabled===!0},checked:function(a){return a.checked===!0},selected:function(a){a.parentNode&&a.parentNode.selectedIndex;return a.selected===!0},parent:function(a){return!!a.firstChild},empty:function(a){return!a.firstChild},has:function(a,b,c){return!!k(c[3],a).length},header:function(a){return/h\d/i.test(a.nodeName)},text:function(a){var b=a.getAttribute("type"),c=a.type;return a.nodeName.toLowerCase()==="input"&&"text"===c&&(b===c||b===null)},radio:function(a){return a.nodeName.toLowerCase()==="input"&&"radio"===a.type},checkbox:function(a){return a.nodeName.toLowerCase()==="input"&&"checkbox"===a.type},file:function(a){return a.nodeName.toLowerCase()==="input"&&"file"===a.type},password:function(a){return a.nodeName.toLowerCase()==="input"&&"password"===a.type},submit:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"submit"===a.type},image:function(a){return a.nodeName.toLowerCase()==="input"&&"image"===a.type},reset:function(a){return a.nodeName.toLowerCase()==="input"&&"reset"===a.type},button:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&"button"===a.type||b==="button"},input:function(a){return/input|select|textarea|button/i.test(a.nodeName)},focus:function(a){return a===a.ownerDocument.activeElement}},setFilters:{first:function(a,b){return b===0},last:function(a,b,c,d){return b===d.length-1},even:function(a,b){return b%2===0},odd:function(a,b){return b%2===1},lt:function(a,b,c){return b<c[3]-0},gt:function(a,b,c){return b>c[3]-0},nth:function(a,b,c){return c[3]-0===b},eq:function(a,b,c){return c[3]-0===b}},filter:{PSEUDO:function(a,b,c,d){var e=b[1],f=l.filters[e];if(f)return f(a,c,b,d);if(e==="contains")return(a.textContent||a.innerText||k.getText([a])||"").indexOf(b[3])>=0;if(e==="not"){var g=b[3];for(var h=0,i=g.length;h<i;h++)if(g[h]===a)return!1;return!0}k.error(e)},CHILD:function(a,b){var c=b[1],d=a;switch(c){case"only":case"first":while(d=d.previousSibling)if(d.nodeType===1)return!1;if(c==="first")return!0;d=a;case"last":while(d=d.nextSibling)if(d.nodeType===1)return!1;return!0;case"nth":var e=b[2],f=b[3];if(e===1&&f===0)return!0;var g=b[0],h=a.parentNode;if(h&&(h.sizcache!==g||!a.nodeIndex)){var i=0;for(d=h.firstChild;d;d=d.nextSibling)d.nodeType===1&&(d.nodeIndex=++i);h.sizcache=g}var j=a.nodeIndex-f;return e===0?j===0:j%e===0&&j/e>=0}},ID:function(a,b){return a.nodeType===1&&a.getAttribute("id")===b},TAG:function(a,b){return b==="*"&&a.nodeType===1||a.nodeName.toLowerCase()===b},CLASS:function(a,b){return(" "+(a.className||a.getAttribute("class"))+" ").indexOf(b)>-1},ATTR:function(a,b){var c=b[1],d=l.attrHandle[c]?l.attrHandle[c](a):a[c]!=null?a[c]:a.getAttribute(c),e=d+"",f=b[2],g=b[4];return d==null?f==="!=":f==="="?e===g:f==="*="?e.indexOf(g)>=0:f==="~="?(" "+e+" ").indexOf(g)>=0:g?f==="!="?e!==g:f==="^="?e.indexOf(g)===0:f==="$="?e.substr(e.length-g.length)===g:f==="|="?e===g||e.substr(0,g.length+1)===g+"-":!1:e&&d!==!1},POS:function(a,b,c,d){var e=b[2],f=l.setFilters[e];if(f)return f(a,c,b,d)}}},m=l.match.POS,n=function(a,b){return"\\"+(b-0+1)};for(var o in l.match)l.match[o]=new RegExp(l.match[o].source+/(?![^\[]*\])(?![^\(]*\))/.source),l.leftMatch[o]=new RegExp(/(^(?:.|\r|\n)*?)/.source+l.match[o].source.replace(/\\(\d+)/g,n));var p=function(a,b){a=Array.prototype.slice.call(a,0);if(b){b.push.apply(b,a);return b}return a};try{Array.prototype.slice.call(c.documentElement.childNodes,0)[0].nodeType}catch(q){p=function(a,b){var c=0,d=b||[];if(e.call(a)==="[object Array]")Array.prototype.push.apply(d,a);else if(typeof a.length=="number")for(var f=a.length;c<f;c++)d.push(a[c]);else for(;a[c];c++)d.push(a[c]);return d}}var r,s;c.documentElement.compareDocumentPosition?r=function(a,b){if(a===b){g=!0;return 0}if(!a.compareDocumentPosition||!b.compareDocumentPosition)return a.compareDocumentPosition?-1:1;return a.compareDocumentPosition(b)&4?-1:1}:(r=function(a,b){var c,d,e=[],f=[],h=a.parentNode,i=b.parentNode,j=h;if(a===b){g=!0;return 0}if(h===i)return s(a,b);if(!h)return-1;if(!i)return 1;while(j)e.unshift(j),j=j.parentNode;j=i;while(j)f.unshift(j),j=j.parentNode;c=e.length,d=f.length;for(var k=0;k<c&&k<d;k++)if(e[k]!==f[k])return s(e[k],f[k]);return k===c?s(a,f[k],-1):s(e[k],b,1)},s=function(a,b,c){if(a===b)return c;var d=a.nextSibling;while(d){if(d===b)return-1;d=d.nextSibling}return 1}),k.getText=function(a){var b="",c;for(var d=0;a[d];d++)c=a[d],c.nodeType===3||c.nodeType===4?b+=c.nodeValue:c.nodeType!==8&&(b+=k.getText(c.childNodes));return b},function(){var a=c.createElement("div"),d="script"+(new Date).getTime(),e=c.documentElement;a.innerHTML="<a name='"+d+"'/>",e.insertBefore(a,e.firstChild),c.getElementById(d)&&(l.find.ID=function(a,c,d){if(typeof c.getElementById!="undefined"&&!d){var e=c.getElementById(a[1]);return e?e.id===a[1]||typeof e.getAttributeNode!="undefined"&&e.getAttributeNode("id").nodeValue===a[1]?[e]:b:[]}},l.filter.ID=function(a,b){var c=typeof a.getAttributeNode!="undefined"&&a.getAttributeNode("id");return a.nodeType===1&&c&&c.nodeValue===b}),e.removeChild(a),e=a=null}(),function(){var a=c.createElement("div");a.appendChild(c.createComment("")),a.getElementsByTagName("*").length>0&&(l.find.TAG=function(a,b){var c=b.getElementsByTagName(a[1]);if(a[1]==="*"){var d=[];for(var e=0;c[e];e++)c[e].nodeType===1&&d.push(c[e]);c=d}return c}),a.innerHTML="<a href='#'></a>",a.firstChild&&typeof a.firstChild.getAttribute!="undefined"&&a.firstChild.getAttribute("href")!=="#"&&(l.attrHandle.href=function(a){return a.getAttribute("href",2)}),a=null}(),c.querySelectorAll&&function(){var a=k,b=c.createElement("div"),d="__sizzle__";b.innerHTML="<p class='TEST'></p>";if(!b.querySelectorAll||b.querySelectorAll(".TEST").length!==0){k=function(b,e,f,g){e=e||c;if(!g&&!k.isXML(e)){var h=/^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec(b);if(h&&(e.nodeType===1||e.nodeType===9)){if(h[1])return p(e.getElementsByTagName(b),f);if(h[2]&&l.find.CLASS&&e.getElementsByClassName)return p(e.getElementsByClassName(h[2]),f)}if(e.nodeType===9){if(b==="body"&&e.body)return p([e.body],f);if(h&&h[3]){var i=e.getElementById(h[3]);if(!i||!i.parentNode)return p([],f);if(i.id===h[3])return p([i],f)}try{return p(e.querySelectorAll(b),f)}catch(j){}}else if(e.nodeType===1&&e.nodeName.toLowerCase()!=="object"){var m=e,n=e.getAttribute("id"),o=n||d,q=e.parentNode,r=/^\s*[+~]/.test(b);n?o=o.replace(/'/g,"\\$&"):e.setAttribute("id",o),r&&q&&(e=e.parentNode);try{if(!r||q)return p(e.querySelectorAll("[id='"+o+"'] "+b),f)}catch(s){}finally{n||m.removeAttribute("id")}}}return a(b,e,f,g)};for(var e in a)k[e]=a[e];b=null}}(),function(){var a=c.documentElement,b=a.matchesSelector||a.mozMatchesSelector||a.webkitMatchesSelector||a.msMatchesSelector;if(b){var d=!b.call(c.createElement("div"),"div"),e=!1;try{b.call(c.documentElement,"[test!='']:sizzle")}catch(f){e=!0}k.matchesSelector=function(a,c){c=c.replace(/\=\s*([^'"\]]*)\s*\]/g,"='$1']");if(!k.isXML(a))try{if(e||!l.match.PSEUDO.test(c)&&!/!=/.test(c)){var f=b.call(a,c);if(f||!d||a.document&&a.document.nodeType!==11)return f}}catch(g){}return k(c,null,null,[a]).length>0}}}(),function(){var a=c.createElement("div");a.innerHTML="<div class='test e'></div><div class='test'></div>";if(!!a.getElementsByClassName&&a.getElementsByClassName("e").length!==0){a.lastChild.className="e";if(a.getElementsByClassName("e").length===1)return;l.order.splice(1,0,"CLASS"),l.find.CLASS=function(a,b,c){if(typeof b.getElementsByClassName!="undefined"&&!c)return b.getElementsByClassName(a[1])},a=null}}(),c.documentElement.contains?k.contains=function(a,b){return a!==b&&(a.contains?a.contains(b):!0)}:c.documentElement.compareDocumentPosition?k.contains=function(a,b){return!!(a.compareDocumentPosition(b)&16)}:k.contains=function(){return!1},k.isXML=function(a){var b=(a?a.ownerDocument||a:0).documentElement;return b?b.nodeName!=="HTML":!1};var v=function(a,b){var c,d=[],e="",f=b.nodeType?[b]:b;while(c=l.match.PSEUDO.exec(a))e+=c[0],a=a.replace(l.match.PSEUDO,"");a=l.relative[a]?a+"*":a;for(var g=0,h=f.length;g<h;g++)k(a,f[g],d);return k.filter(e,d)};f.find=k,f.expr=k.selectors,f.expr[":"]=f.expr.filters,f.unique=k.uniqueSort,f.text=k.getText,f.isXMLDoc=k.isXML,f.contains=k.contains}();var O=/Until$/,P=/^(?:parents|prevUntil|prevAll)/,Q=/,/,R=/^.[^:#\[\.,]*$/,S=Array.prototype.slice,T=f.expr.match.POS,U={children:!0,contents:!0,next:!0,prev:!0};f.fn.extend({find:function(a){var b=this,c,d;if(typeof a!="string")return f(a).filter(function(){for(c=0,d=b.length;c<d;c++)if(f.contains(b[c],this))return!0});var e=this.pushStack("","find",a),g,h,i;for(c=0,d=this.length;c<d;c++){g=e.length,f.find(a,this[c],e);if(c>0)for(h=g;h<e.length;h++)for(i=0;i<g;i++)if(e[i]===e[h]){e.splice(h--,1);break}}return e},has:function(a){var b=f(a);return this.filter(function(){for(var a=0,c=b.length;a<c;a++)if(f.contains(this,b[a]))return!0})},not:function(a){return this.pushStack(W(this,a,!1),"not",a)},filter:function(a){return this.pushStack(W(this,a,!0),"filter",a)},is:function(a){return!!a&&(typeof a=="string"?f.filter(a,this).length>0:this.filter(a).length>0)},closest:function(a,b){var c=[],d,e,g=this[0];if(f.isArray(a)){var h,i,j={},k=1;if(g&&a.length){for(d=0,e=a.length;d<e;d++)i=a[d],j[i]||(j[i]=T.test(i)?f(i,b||this.context):i);while(g&&g.ownerDocument&&g!==b){for(i in j)h=j[i],(h.jquery?h.index(g)>-1:f(g).is(h))&&c.push({selector:i,elem:g,level:k});g=g.parentNode,k++}}return c}var l=T.test(a)||typeof a!="string"?f(a,b||this.context):0;for(d=0,e=this.length;d<e;d++){g=this[d];while(g){if(l?l.index(g)>-1:f.find.matchesSelector(g,a)){c.push(g);break}g=g.parentNode;if(!g||!g.ownerDocument||g===b||g.nodeType===11)break}}c=c.length>1?f.unique(c):c;return this.pushStack(c,"closest",a)},index:function(a){if(!a||typeof a=="string")return f.inArray(this[0],a?f(a):this.parent().children());return f.inArray(a.jquery?a[0]:a,this)},add:function(a,b){var c=typeof a=="string"?f(a,b):f.makeArray(a&&a.nodeType?[a]:a),d=f.merge(this.get(),c);return this.pushStack(V(c[0])||V(d[0])?d:f.unique(d))},andSelf:function(){return this.add(this.prevObject)}}),f.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return f.dir(a,"parentNode")},parentsUntil:function(a,b,c){return f.dir(a,"parentNode",c)},next:function(a){return f.nth(a,2,"nextSibling")},prev:function(a){return f.nth(a,2,"previousSibling")},nextAll:function(a){return f.dir(a,"nextSibling")},prevAll:function(a){return f.dir(a,"previousSibling")},nextUntil:function(a,b,c){return f.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return f.dir(a,"previousSibling",c)},siblings:function(a){return f.sibling(a.parentNode.firstChild,a)},children:function(a){return f.sibling(a.firstChild)},contents:function(a){return f.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:f.makeArray(a.childNodes)}},function(a,b){f.fn[a]=function(c,d){var e=f.map(this,b,c),g=S.call(arguments);O.test(a)||(d=c),d&&typeof d=="string"&&(e=f.filter(d,e)),e=this.length>1&&!U[a]?f.unique(e):e,(this.length>1||Q.test(d))&&P.test(a)&&(e=e.reverse());return this.pushStack(e,a,g.join(","))}}),f.extend({filter:function(a,b,c){c&&(a=":not("+a+")");return b.length===1?f.find.matchesSelector(b[0],a)?[b[0]]:[]:f.find.matches(a,b)},dir:function(a,c,d){var e=[],g=a[c];while(g&&g.nodeType!==9&&(d===b||g.nodeType!==1||!f(g).is(d)))g.nodeType===1&&e.push(g),g=g[c];return e},nth:function(a,b,c,d){b=b||1;var e=0;for(;a;a=a[c])if(a.nodeType===1&&++e===b)break;return a},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var X=/ jQuery\d+="(?:\d+|null)"/g,Y=/^\s+/,Z=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,$=/<([\w:]+)/,_=/<tbody/i,ba=/<|&#?\w+;/,bb=/<(?:script|object|embed|option|style)/i,bc=/checked\s*(?:[^=]|=\s*.checked.)/i,bd=/\/(java|ecma)script/i,be={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],area:[1,"<map>","</map>"],_default:[0,"",""]};be.optgroup=be.option,be.tbody=be.tfoot=be.colgroup=be.caption=be.thead,be.th=be.td,f.support.htmlSerialize||(be._default=[1,"div<div>","</div>"]),f.fn.extend({text:function(a){if(f.isFunction(a))return this.each(function(b){var c=f(this);c.text(a.call(this,b,c.text()))});if(typeof a!="object"&&a!==b)return this.empty().append((this[0]&&this[0].ownerDocument||c).createTextNode(a));return f.text(this)},wrapAll:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapAll(a.call(this,b))});if(this[0]){var b=f(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapInner(a.call(this,b))});return this.each(function(){var b=f(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){return this.each(function(){f(this).wrapAll(a)})},unwrap:function(){return this.parent().each(function(){f.nodeName(this,"body")||f(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=f(arguments[0]);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,f(arguments[0]).toArray());return a}},remove:function(a,b){for(var c=0,d;(d=this[c])!=null;c++)if(!a||f.filter(a,[d]).length)!b&&d.nodeType===1&&(f.cleanData(d.getElementsByTagName("*")),f.cleanData([d])),d.parentNode&&d.parentNode.removeChild(d);return this},empty:function(){for(var a=0,b;(b=this[a])!=null;a++){b.nodeType===1&&f.cleanData(b.getElementsByTagName("*"));while(b.firstChild)b.removeChild(b.firstChild)}return this},clone:function(a,b){a=a==null?!1:a,b=b==null?a:b;return this.map(function(){return f.clone(this,a,b)})},html:function(a){if(a===b)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(X,""):null;if(typeof a=="string"&&!bb.test(a)&&(f.support.leadingWhitespace||!Y.test(a))&&!be[($.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(Z,"<$1></$2>");try{for(var c=0,d=this.length;c<d;c++)this[c].nodeType===1&&(f.cleanData(this[c].getElementsByTagName("*")),this[c].innerHTML=a)}catch(e){this.empty().append(a)}}else f.isFunction(a)?this.each(function(b){var c=f(this);c.html(a.call(this,b,c.html()))}):this.empty().append(a);return this},replaceWith:function(a){if(this[0]&&this[0].parentNode){if(f.isFunction(a))return this.each(function(b){var c=f(this),d=c.html();c.replaceWith(a.call(this,b,d))});typeof a!="string"&&(a=f(a).detach());return this.each(function(){var b=this.nextSibling,c=this.parentNode;f(this).remove(),b?f(b).before(a):f(c).append(a)})}return this.length?this.pushStack(f(f.isFunction(a)?a():a),"replaceWith",a):this},detach:function(a){return this.remove(a,!0)},domManip:function(a,c,d){var e,g,h,i,j=a[0],k=[];if(!f.support.checkClone&&arguments.length===3&&typeof j=="string"&&bc.test(j))return this.each(function(){f(this).domManip(a,c,d,!0)});if(f.isFunction(j))return this.each(function(e){var g=f(this);a[0]=j.call(this,e,c?g.html():b),g.domManip(a,c,d)});if(this[0]){i=j&&j.parentNode,f.support.parentNode&&i&&i.nodeType===11&&i.childNodes.length===this.length?e={fragment:i}:e=f.buildFragment(a,this,k),h=e.fragment,h.childNodes.length===1?g=h=h.firstChild:g=h.firstChild;if(g){c=c&&f.nodeName(g,"tr");for(var l=0,m=this.length,n=m-1;l<m;l++)d.call(c?bf(this[l],g):this[l],e.cacheable||m>1&&l<n?f.clone(h,!0,!0):h)}k.length&&f.each(k,bl)}return this}}),f.buildFragment=function(a,b,d){var e,g,h,i=b&&b[0]?b[0].ownerDocument||b[0]:c;a.length===1&&typeof a[0]=="string"&&a[0].length<512&&i===c&&a[0].charAt(0)==="<"&&!bb.test(a[0])&&(f.support.checkClone||!bc.test(a[0]))&&(g=!0,h=f.fragments[a[0]],h&&h!==1&&(e=h)),e||(e=i.createDocumentFragment(),f.clean(a,i,e,d)),g&&(f.fragments[a[0]]=h?e:1);return{fragment:e,cacheable:g}},f.fragments={},f.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){f.fn[a]=function(c){var d=[],e=f(c),g=this.length===1&&this[0].parentNode;if(g&&g.nodeType===11&&g.childNodes.length===1&&e.length===1){e[b](this[0]);return this}for(var h=0,i=e.length;h<i;h++){var j=(h>0?this.clone(!0):this).get();f(e[h])[b](j),d=d.concat(j)}return this.pushStack(d,a,e.selector)}}),f.extend({clone:function(a,b,c){var d=a.cloneNode(!0),e,g,h;if((!f.support.noCloneEvent||!f.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!f.isXMLDoc(a)){bh(a,d),e=bi(a),g=bi(d);for(h=0;e[h];++h)bh(e[h],g[h])}if(b){bg(a,d);if(c){e=bi(a),g=bi(d);for(h=0;e[h];++h)bg(e[h],g[h])}}return d},clean:function(a,b,d,e){var g;b=b||c,typeof b.createElement=="undefined"&&(b=b.ownerDocument||b[0]&&b[0].ownerDocument||c);var h=[];for(var i=0,j;(j=a[i])!=null;i++){typeof j=="number"&&(j+="");if(!j)continue;if(typeof j=="string")if(!ba.test(j))j=b.createTextNode(j);else{j=j.replace(Z,"<$1></$2>");var k=($.exec(j)||["",""])[1].toLowerCase(),l=be[k]||be._default,m=l[0],n=b.createElement("div");n.innerHTML=l[1]+j+l[2];while(m--)n=n.lastChild;if(!f.support.tbody){var o=_.test(j),p=k==="table"&&!o?n.firstChild&&n.firstChild.childNodes:l[1]==="<table>"&&!o?n.childNodes:[];for(var q=p.length-1;q>=0;--q)f.nodeName(p[q],"tbody")&&!p[q].childNodes.length&&p[q].parentNode.removeChild(p[q])}!f.support.leadingWhitespace&&Y.test(j)&&n.insertBefore(b.createTextNode(Y.exec(j)[0]),n.firstChild),j=n.childNodes}var r;if(!f.support.appendChecked)if(j[0]&&typeof (r=j.length)=="number")for(i=0;i<r;i++)bk(j[i]);else bk(j);j.nodeType?h.push(j):h=f.merge(h,j)}if(d){g=function(a){return!a.type||bd.test(a.type)};for(i=0;h[i];i++)if(e&&f.nodeName(h[i],"script")&&(!h[i].type||h[i].type.toLowerCase()==="text/javascript"))e.push(h[i].parentNode?h[i].parentNode.removeChild(h[i]):h[i]);else{if(h[i].nodeType===1){var s=f.grep(h[i].getElementsByTagName("script"),g);h.splice.apply(h,[i+1,0].concat(s))}d.appendChild(h[i])}}return h},cleanData:function(a){var b,c,d=f.cache,e=f.expando,g=f.event.special,h=f.support.deleteExpando;for(var i=0,j;(j=a[i])!=null;i++){if(j.nodeName&&f.noData[j.nodeName.toLowerCase()])continue;c=j[f.expando];if(c){b=d[c]&&d[c][e];if(b&&b.events){for(var k in b.events)g[k]?f.event.remove(j,k):f.removeEvent(j,k,b.handle);b.handle&&(b.handle.elem=null)}h?delete j[f.expando]:j.removeAttribute&&j.removeAttribute(f.expando),delete d[c]}}}});var bm=/alpha\([^)]*\)/i,bn=/opacity=([^)]*)/,bo=/-([a-z])/ig,bp=/([A-Z]|^ms)/g,bq=/^-?\d+(?:px)?$/i,br=/^-?\d/,bs=/^[+\-]=/,bt=/[^+\-\.\de]+/g,bu={position:"absolute",visibility:"hidden",display:"block"},bv=["Left","Right"],bw=["Top","Bottom"],bx,by,bz,bA=function(a,b){return b.toUpperCase()};f.fn.css=function(a,c){if(arguments.length===2&&c===b)return this;return f.access(this,a,c,!0,function(a,c,d){return d!==b?f.style(a,c,d):f.css(a,c)})},f.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=bx(a,"opacity","opacity");return c===""?"1":c}return a.style.opacity}}},cssNumber:{zIndex:!0,fontWeight:!0,opacity:!0,zoom:!0,lineHeight:!0,widows:!0,orphans:!0},cssProps:{"float":f.support.cssFloat?"cssFloat":"styleFloat"},style:function(a,c,d,e){if(!!a&&a.nodeType!==3&&a.nodeType!==8&&!!a.style){var g,h,i=f.camelCase(c),j=a.style,k=f.cssHooks[i];c=f.cssProps[i]||i;if(d===b){if(k&&"get"in k&&(g=k.get(a,!1,e))!==b)return g;return j[c]}h=typeof d;if(h==="number"&&isNaN(d)||d==null)return;h==="string"&&bs.test(d)&&(d=+d.replace(bt,"")+parseFloat(f.css(a,c))),h==="number"&&!f.cssNumber[i]&&(d+="px");if(!k||!("set"in k)||(d=k.set(a,d))!==b)try{j[c]=d}catch(l){}}},css:function(a,c,d){var e,g;c=f.camelCase(c),g=f.cssHooks[c],c=f.cssProps[c]||c,c==="cssFloat"&&(c="float");if(g&&"get"in g&&(e=g.get(a,!0,d))!==b)return e;if(bx)return bx(a,c)},swap:function(a,b,c){var d={};for(var e in b)d[e]=a.style[e],a.style[e]=b[e];c.call(a);for(e in b)a.style[e]=d[e]},camelCase:function(a){return a.replace(bo,bA)}}),f.curCSS=f.css,f.each(["height","width"],function(a,b){f.cssHooks[b]={get:function(a,c,d){var e;if(c){a.offsetWidth!==0?e=bB(a,b,d):f.swap(a,bu,function(){e=bB(a,b,d)});if(e<=0){e=bx(a,b,b),e==="0px"&&bz&&(e=bz(a,b,b));if(e!=null)return e===""||e==="auto"?"0px":e}if(e<0||e==null){e=a.style[b];return e===""||e==="auto"?"0px":e}return typeof e=="string"?e:e+"px"}},set:function(a,b){if(!bq.test(b))return b;b=parseFloat(b);if(b>=0)return b+"px"}}}),f.support.opacity||(f.cssHooks.opacity={get:function(a,b){return bn.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle;c.zoom=1;var e=f.isNaN(b)?"":"alpha(opacity="+b*100+")",g=d&&d.filter||c.filter||"";c.filter=bm.test(g)?g.replace(bm,e):g+" "+e}}),f(function(){f.support.reliableMarginRight||(f.cssHooks.marginRight={get:function(a,b){var c;f.swap(a,{display:"inline-block"},function(){b?c=bx(a,"margin-right","marginRight"):c=a.style.marginRight});return c}})}),c.defaultView&&c.defaultView.getComputedStyle&&(by=function(a,c){var d,e,g;c=c.replace(bp,"-$1").toLowerCase();if(!(e=a.ownerDocument.defaultView))return b;if(g=e.getComputedStyle(a,null))d=g.getPropertyValue(c),d===""&&!f.contains(a.ownerDocument.documentElement,a)&&(d=f.style(a,c));return d}),c.documentElement.currentStyle&&(bz=function(a,b){var c,d=a.currentStyle&&a.currentStyle[b],e=a.runtimeStyle&&a.runtimeStyle[b],f=a.style;!bq.test(d)&&br.test(d)&&(c=f.left,e&&(a.runtimeStyle.left=a.currentStyle.left),f.left=b==="fontSize"?"1em":d||0,d=f.pixelLeft+"px",f.left=c,e&&(a.runtimeStyle.left=e));return d===""?"auto":d}),bx=by||bz,f.expr&&f.expr.filters&&(f.expr.filters.hidden=function(a){var b=a.offsetWidth,c=a.offsetHeight;return b===0&&c===0||!f.support.reliableHiddenOffsets&&(a.style.display||f.css(a,"display"))==="none"},f.expr.filters.visible=function(a){return!f.expr.filters.hidden(a)});var bC=/%20/g,bD=/\[\]$/,bE=/\r?\n/g,bF=/#.*$/,bG=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,bH=/^(?:color|date|datetime|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,bI=/^(?:about|app|app\-storage|.+\-extension|file|widget):$/,bJ=/^(?:GET|HEAD)$/,bK=/^\/\//,bL=/\?/,bM=/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,bN=/^(?:select|textarea)/i,bO=/\s+/,bP=/([?&])_=[^&]*/,bQ=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/,bR=f.fn.load,bS={},bT={},bU,bV;try{bU=e.href}catch(bW){bU=c.createElement("a"),bU.href="",bU=bU.href}bV=bQ.exec(bU.toLowerCase())||[],f.fn.extend({load:function(a,c,d){if(typeof a!="string"&&bR)return bR.apply(this,arguments);if(!this.length)return this;var e=a.indexOf(" ");if(e>=0){var g=a.slice(e,a.length);a=a.slice(0,e)}var h="GET";c&&(f.isFunction(c)?(d=c,c=b):typeof c=="object"&&(c=f.param(c,f.ajaxSettings.traditional),h="POST"));var i=this;f.ajax({url:a,type:h,dataType:"html",data:c,complete:function(a,b,c){c=a.responseText,a.isResolved()&&(a.done(function(a){c=a}),i.html(g?f("<div>").append(c.replace(bM,"")).find(g):c)),d&&i.each(d,[c,b,a])}});return this},serialize:function(){return f.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?f.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||bN.test(this.nodeName)||bH.test(this.type))}).map(function(a,b){var c=f(this).val();return c==null?null:f.isArray(c)?f.map(c,function(a,c){return{name:b.name,value:a.replace(bE,"\r\n")}}):{name:b.name,value:c.replace(bE,"\r\n")}}).get()}}),f.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){f.fn[b]=function(a){return this.bind(b,a)}}),f.each(["get","post"],function(a,c){f[c]=function(a,d,e,g){f.isFunction(d)&&(g=g||e,e=d,d=b);return f.ajax({type:c,url:a,data:d,success:e,dataType:g})}}),f.extend({getScript:function(a,c){return f.get(a,b,c,"script")},getJSON:function(a,b,c){return f.get(a,b,c,"json")},ajaxSetup:function(a,b){b?f.extend(!0,a,f.ajaxSettings,b):(b=a,a=f.extend(!0,f.ajaxSettings,b));for(var c in{context:1,url:1})c in b?a[c]=b[c]:c in f.ajaxSettings&&(a[c]=f.ajaxSettings[c]);return a},ajaxSettings:{url:bU,isLocal:bI.test(bV[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":"*/*"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":a.String,"text html":!0,"text json":f.parseJSON,"text xml":f.parseXML}},ajaxPrefilter:bX(bS),ajaxTransport:bX(bT),ajax:function(a,c){function w(a,c,l,m){if(s!==2){s=2,q&&clearTimeout(q),p=b,n=m||"",v.readyState=a?4:0;var o,r,u,w=l?b$(d,v,l):b,x,y;if(a>=200&&a<300||a===304){if(d.ifModified){if(x=v.getResponseHeader("Last-Modified"))f.lastModified[k]=x;if(y=v.getResponseHeader("Etag"))f.etag[k]=y}if(a===304)c="notmodified",o=!0;else try{r=b_(d,w),c="success",o=!0}catch(z){c="parsererror",u=z}}else{u=c;if(!c||a)c="error",a<0&&(a=0)}v.status=a,v.statusText=c,o?h.resolveWith(e,[r,c,v]):h.rejectWith(e,[v,c,u]),v.statusCode(j),j=b,t&&g.trigger("ajax"+(o?"Success":"Error"),[v,d,o?r:u]),i.resolveWith(e,[v,c]),t&&(g.trigger("ajaxComplete",[v,d]),--f.active||f.event.trigger("ajaxStop"))}}typeof a=="object"&&(c=a,a=b),c=c||{};var d=f.ajaxSetup({},c),e=d.context||d,g=e!==d&&(e.nodeType||e instanceof f)?f(e):f.event,h=f.Deferred(),i=f._Deferred(),j=d.statusCode||{},k,l={},m={},n,o,p,q,r,s=0,t,u,v={readyState:0,setRequestHeader:function(a,b){if(!s){var c=a.toLowerCase();a=m[c]=m[c]||a,l[a]=b}return this},getAllResponseHeaders:function(){return s===2?n:null},getResponseHeader:function(a){var c;if(s===2){if(!o){o={};while(c=bG.exec(n))o[c[1].toLowerCase()]=c[2]}c=o[a.toLowerCase()]}return c===b?null:c},overrideMimeType:function(a){s||(d.mimeType=a);return this},abort:function(a){a=a||"abort",p&&p.abort(a),w(0,a);return this}};h.promise(v),v.success=v.done,v.error=v.fail,v.complete=i.done,v.statusCode=function(a){if(a){var b;if(s<2)for(b in a)j[b]=[j[b],a[b]];else b=a[v.status],v.then(b,b)}return this},d.url=((a||d.url)+"").replace(bF,"").replace(bK,bV[1]+"//"),d.dataTypes=f.trim(d.dataType||"*").toLowerCase().split(bO),d.crossDomain==null&&(r=bQ.exec(d.url.toLowerCase()),d.crossDomain=!(!r||r[1]==bV[1]&&r[2]==bV[2]&&(r[3]||(r[1]==="http:"?80:443))==(bV[3]||(bV[1]==="http:"?80:443)))),d.data&&d.processData&&typeof d.data!="string"&&(d.data=f.param(d.data,d.traditional)),bY(bS,d,c,v);if(s===2)return!1;t=d.global,d.type=d.type.toUpperCase(),d.hasContent=!bJ.test(d.type),t&&f.active++===0&&f.event.trigger("ajaxStart");if(!d.hasContent){d.data&&(d.url+=(bL.test(d.url)?"&":"?")+d.data),k=d.url;if(d.cache===!1){var x=f.now(),y=d.url.replace(bP,"$1_="+x);d.url=y+(y===d.url?(bL.test(d.url)?"&":"?")+"_="+x:"")}}(d.data&&d.hasContent&&d.contentType!==!1||c.contentType)&&v.setRequestHeader("Content-Type",d.contentType),d.ifModified&&(k=k||d.url,f.lastModified[k]&&v.setRequestHeader("If-Modified-Since",f.lastModified[k]),f.etag[k]&&v.setRequestHeader("If-None-Match",f.etag[k])),v.setRequestHeader("Accept",d.dataTypes[0]&&d.accepts[d.dataTypes[0]]?d.accepts[d.dataTypes[0]]+(d.dataTypes[0]!=="*"?", */*; q=0.01":""):d.accepts["*"]);for(u in d.headers)v.setRequestHeader(u,d.headers[u]);if(d.beforeSend&&(d.beforeSend.call(e,v,d)===!1||s===2)){v.abort();return!1}for(u in{success:1,error:1,complete:1})v[u](d[u]);p=bY(bT,d,c,v);if(!p)w(-1,"No Transport");else{v.readyState=1,t&&g.trigger("ajaxSend",[v,d]),d.async&&d.timeout>0&&(q=setTimeout(function(){v.abort("timeout")},d.timeout));try{s=1,p.send(l,w)}catch(z){status<2?w(-1,z):f.error(z)}}return v},param:function(a,c){var d=[],e=function(a,b){b=f.isFunction(b)?b():b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=f.ajaxSettings.traditional);if(f.isArray(a)||a.jquery&&!f.isPlainObject(a))f.each(a,function(){e(this.name,this.value)});else for(var g in a)bZ(g,a[g],c,e);return d.join("&").replace(bC,"+")}}),f.extend({active:0,lastModified:{},etag:{}});var ca=f.now(),cb=/(\=)\?(&|$)|\?\?/i;f.ajaxSetup({jsonp:"callback",jsonpCallback:function(){return f.expando+"_"+ca++}}),f.ajaxPrefilter("json jsonp",function(b,c,d){var e=b.contentType==="application/x-www-form-urlencoded"&&typeof b.data=="string";if(b.dataTypes[0]==="jsonp"||b.jsonp!==!1&&(cb.test(b.url)||e&&cb.test(b.data))){var g,h=b.jsonpCallback=f.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,i=a[h],j=b.url,k=b.data,l="$1"+h+"$2";b.jsonp!==!1&&(j=j.replace(cb,l),b.url===j&&(e&&(k=k.replace(cb,l)),b.data===k&&(j+=(/\?/.test(j)?"&":"?")+b.jsonp+"="+h))),b.url=j,b.data=k,a[h]=function(a){g=[a]},d.always(function(){a[h]=i,g&&f.isFunction(i)&&a[h](g[0])}),b.converters["script json"]=function(){g||f.error(h+" was not called");return g[0]},b.dataTypes[0]="json";return"script"}}),f.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(a){f.globalEval(a);return a}}}),f.ajaxPrefilter("script",function(a){a.cache===b&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),f.ajaxTransport("script",function(a){if(a.crossDomain){var d,e=c.head||c.getElementsByTagName("head")[0]||c.documentElement;return{send:function(f,g){d=c.createElement("script"),d.async="async",a.scriptCharset&&(d.charset=a.scriptCharset),d.src=a.url,d.onload=d.onreadystatechange=function(a,c){if(c||!d.readyState||/loaded|complete/.test(d.readyState))d.onload=d.onreadystatechange=null,e&&d.parentNode&&e.removeChild(d),d=b,c||g(200,"success")},e.insertBefore(d,e.firstChild)},abort:function(){d&&d.onload(0,1)}}}});var cc=a.ActiveXObject?function(){for(var a in ce)ce[a](0,1)}:!1,cd=0,ce;f.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&cf()||cg()}:cf,function(a){f.extend(f.support,{ajax:!!a,cors:!!a&&"withCredentials"in a})}(f.ajaxSettings.xhr()),f.support.ajax&&f.ajaxTransport(function(c){if(!c.crossDomain||f.support.cors){var d;return{send:function(e,g){var h=c.xhr(),i,j;c.username?h.open(c.type,c.url,c.async,c.username,c.password):h.open(c.type,c.url,c.async);if(c.xhrFields)for(j in c.xhrFields)h[j]=c.xhrFields[j];c.mimeType&&h.overrideMimeType&&h.overrideMimeType(c.mimeType),!c.crossDomain&&!e["X-Requested-With"]&&(e["X-Requested-With"]="XMLHttpRequest");try{for(j in e)h.setRequestHeader(j,e[j])}catch(k){}h.send(c.hasContent&&c.data||null),d=function(a,e){var j,k,l,m,n;try{if(d&&(e||h.readyState===4)){d=b,i&&(h.onreadystatechange=f.noop,cc&&delete ce[i]);if(e)h.readyState!==4&&h.abort();else{j=h.status,l=h.getAllResponseHeaders(),m={},n=h.responseXML,n&&n.documentElement&&(m.xml=n),m.text=h.responseText;try{k=h.statusText}catch(o){k=""}!j&&c.isLocal&&!c.crossDomain?j=m.text?200:404:j===1223&&(j=204)}}}catch(p){e||g(-1,p)}m&&g(j,k,m,l)},!c.async||h.readyState===4?d():(i=++cd,cc&&(ce||(ce={},f(a).unload(cc)),ce[i]=d),h.onreadystatechange=d)},abort:function(){d&&d(0,1)}}}});var ch={},ci,cj,ck=/^(?:toggle|show|hide)$/,cl=/^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i,cm,cn=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]],co,cp=a.webkitRequestAnimationFrame||a.mozRequestAnimationFrame||a.oRequestAnimationFrame;f.fn.extend({show:function(a,b,c){var d,e;if(a||a===0)return this.animate(cs("show",3),a,b,c);for(var g=0,h=this.length;g<h;g++)d=this[g],d.style&&(e=d.style.display,!f._data(d,"olddisplay")&&e==="none"&&(e=d.style.display=""),e===""&&f.css(d,"display")==="none"&&f._data(d,"olddisplay",ct(d.nodeName)));for(g=0;g<h;g++){d=this[g];if(d.style){e=d.style.display;if(e===""||e==="none")d.style.display=f._data(d,"olddisplay")||""}}return this},hide:function(a,b,c){if(a||a===0)return this.animate(cs("hide",3),a,b,c);for(var d=0,e=this.length;d<e;d++)if(this[d].style){var g=f.css(this[d],"display");g!=="none"&&!f._data(this[d],"olddisplay")&&f._data(this[d],"olddisplay",g)}for(d=0;d<e;d++)this[d].style&&(this[d].style.display="none");return this},_toggle:f.fn.toggle,toggle:function(a,b,c){var d=typeof a=="boolean";f.isFunction(a)&&f.isFunction(b)?this._toggle.apply(this,arguments):a==null||d?this.each(function(){var b=d?a:f(this).is(":hidden");f(this)[b?"show":"hide"]()}):this.animate(cs("toggle",3),a,b,c);return this},fadeTo:function(a,b,c,d){return this.filter(":hidden").css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){var e=f.speed(b,c,d);if(f.isEmptyObject(a))return this.each(e.complete,[!1]);return this[e.queue===!1?"each":"queue"](function(){e.queue===!1&&f._mark(this);var b=f.extend({},e),c=this.nodeType===1,d=c&&f(this).is(":hidden"),g,h,i,j,k,l,m,n,o;b.animatedProperties={};for(i in a){g=f.camelCase(i),i!==g&&(a[g]=a[i],delete a[i]),h=a[g];if(h==="hide"&&d||h==="show"&&!d)return b.complete.call(this);c&&(g==="height"||g==="width")&&(b.overflow=[this.style.overflow,this.style.overflowX,this.style.overflowY],f.css(this,"display")==="inline"&&f.css(this,"float")==="none"&&(f.support.inlineBlockNeedsLayout?(j=ct(this.nodeName),j==="inline"?this.style.display="inline-block":(this.style.display="inline",this.style.zoom=1)):this.style.display="inline-block")),b.animatedProperties[g]=f.isArray(h)?h[1]:b.specialEasing&&b.specialEasing[g]||b.easing||"swing"}b.overflow!=null&&(this.style.overflow="hidden");for(i in a)k=new f.fx(this,b,i),h=a[i],ck.test(h)?k[h==="toggle"?d?"show":"hide":h]():(l=cl.exec(h),m=k.cur(),l?(n=parseFloat(l[2]),o=l[3]||(f.cssNumber[g]?"":"px"),o!=="px"&&(f.style(this,i,(n||1)+o),m=(n||1)/k.cur()*m,f.style(this,i,m+o)),l[1]&&(n=(l[1]==="-="?-1:1)*n+m),k.custom(m,n,o)):k.custom(m,h,""));return!0})},stop:function(a,b){a&&this.queue([]),this.each(function(){var a=f.timers,c=a.length;b||f._unmark(!0,this);while(c--)a[c].elem===this&&(b&&a[c](!0),a.splice(c,1))}),b||this.dequeue();return this}}),f.each({slideDown:cs("show",1),slideUp:cs("hide",1),slideToggle:cs("toggle",1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){f.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),f.extend({speed:function(a,b,c){var d=a&&typeof a=="object"?f.extend({},a):{complete:c||!c&&b||f.isFunction(a)&&a,duration:a,easing:c&&b||b&&!f.isFunction(b)&&b};d.duration=f.fx.off?0:typeof d.duration=="number"?d.duration:d.duration in f.fx.speeds?f.fx.speeds[d.duration]:f.fx.speeds._default,d.old=d.complete,d.complete=function(a){d.queue!==!1?f.dequeue(this):a!==!1&&f._unmark(this),f.isFunction(d.old)&&d.old.call(this)};return d},easing:{linear:function(a,b,c,d){return c+d*a},swing:function(a,b,c,d){return(-Math.cos(a*Math.PI)/2+.5)*d+c}},timers:[],fx:function(a,b,c){this.options=b,this.elem=a,this.prop=c,b.orig=b.orig||{}}}),f.fx.prototype={update:function(){this.options.step&&this.options.step.call(this.elem,this.now,this),(f.fx.step[this.prop]||f.fx.step._default)(this)},cur:function(){if(this.elem[this.prop]!=null&&(!this.elem.style||this.elem.style[this.prop]==null))return this.elem[this.prop];var a,b=f.css(this.elem,this.prop);return isNaN(a=parseFloat(b))?!b||b==="auto"?0:b:a},custom:function(a,b,c){function h(a){return d.step(a)}var d=this,e=f.fx,g;this.startTime=co||cq(),this.start=a,this.end=b,this.unit=c||this.unit||(f.cssNumber[this.prop]?"":"px"),this.now=this.start,this.pos=this.state=0,h.elem=this.elem,h()&&f.timers.push(h)&&!cm&&(cp?(cm=1,g=function(){cm&&(cp(g),e.tick())},cp(g)):cm=setInterval(e.tick,e.interval))},show:function(){this.options.orig[this.prop]=f.style(this.elem,this.prop),this.options.show=!0,this.custom(this.prop==="width"||this.prop==="height"?1:0,this.cur()),f(this.elem).show()},hide:function(){this.options.orig[this.prop]=f.style(this.elem,this.prop),this.options.hide=!0,this.custom(this.cur(),0)},step:function(a){var b=co||cq(),c=!0,d=this.elem,e=this.options,g,h;if(a||b>=e.duration+this.startTime){this.now=this.end,this.pos=this.state=1,this.update(),e.animatedProperties[this.prop]=!0;for(g in e.animatedProperties)e.animatedProperties[g]!==!0&&(c=!1);if(c){e.overflow!=null&&!f.support.shrinkWrapBlocks&&f.each(["","X","Y"],function(a,b){d.style["overflow"+b]=e.overflow[a]}),e.hide&&f(d).hide();if(e.hide||e.show)for(var i in e.animatedProperties)f.style(d,i,e.orig[i]);e.complete.call(d)}return!1}e.duration==Infinity?this.now=b:(h=b-this.startTime,this.state=h/e.duration,this.pos=f.easing[e.animatedProperties[this.prop]](this.state,h,0,1,e.duration),this.now=this.start+(this.end-this.start)*this.pos),this.update();return!0}},f.extend(f.fx,{tick:function(){var a=f.timers,b=a.length;while(b--)a[b]()||a.splice(b,1);a.length||f.fx.stop()},interval:13,stop:function(){clearInterval(cm),cm=null},speeds:{slow:600,fast:200,_default:400},step:{opacity:function(a){f.style(a.elem,"opacity",a.now)},_default:function(a){a.elem.style&&a.elem.style[a.prop]!=null?a.elem.style[a.prop]=(a.prop==="width"||a.prop==="height"?Math.max(0,a.now):a.now)+a.unit:a.elem[a.prop]=a.now}}}),f.expr&&f.expr.filters&&(f.expr.filters.animated=function(a){return f.grep(f.timers,function(b){return a===b.elem}).length});var cu=/^t(?:able|d|h)$/i,cv=/^(?:body|html)$/i;"getBoundingClientRect"in c.documentElement?f.fn.offset=function(a){var b=this[0],c;if(a)return this.each(function(b){f.offset.setOffset(this,a,b)});if(!b||!b.ownerDocument)return null;if(b===b.ownerDocument.body)return f.offset.bodyOffset(b);try{c=b.getBoundingClientRect()}catch(d){}var e=b.ownerDocument,g=e.documentElement;if(!c||!f.contains(g,b))return c?{top:c.top,left:c.left}:{top:0,left:0};var h=e.body,i=cw(e),j=g.clientTop||h.clientTop||0,k=g.clientLeft||h.clientLeft||0,l=i.pageYOffset||f.support.boxModel&&g.scrollTop||h.scrollTop,m=i.pageXOffset||f.support.boxModel&&g.scrollLeft||h.scrollLeft,n=c.top+l-j,o=c.left+m-k;return{top:n,left:o}}:f.fn.offset=function(a){var b=this[0];if(a)return this.each(function(b){f.offset.setOffset(this,a,b)});if(!b||!b.ownerDocument)return null;if(b===b.ownerDocument.body)return f.offset.bodyOffset(b);f.offset.initialize();var c,d=b.offsetParent,e=b,g=b.ownerDocument,h=g.documentElement,i=g.body,j=g.defaultView,k=j?j.getComputedStyle(b,null):b.currentStyle,l=b.offsetTop,m=b.offsetLeft;while((b=b.parentNode)&&b!==i&&b!==h){if(f.offset.supportsFixedPosition&&k.position==="fixed")break;c=j?j.getComputedStyle(b,null):b.currentStyle,l-=b.scrollTop,m-=b.scrollLeft,b===d&&(l+=b.offsetTop,m+=b.offsetLeft,f.offset.doesNotAddBorder&&(!f.offset.doesAddBorderForTableAndCells||!cu.test(b.nodeName))&&(l+=parseFloat(c.borderTopWidth)||0,m+=parseFloat(c.borderLeftWidth)||0),e=d,d=b.offsetParent),f.offset.subtractsBorderForOverflowNotVisible&&c.overflow!=="visible"&&(l+=parseFloat(c.borderTopWidth)||0,m+=parseFloat(c.borderLeftWidth)||0),k=c}if(k.position==="relative"||k.position==="static")l+=i.offsetTop,m+=i.offsetLeft;f.offset.supportsFixedPosition&&k.position==="fixed"&&(l+=Math.max(h.scrollTop,i.scrollTop),m+=Math.max(h.scrollLeft,i.scrollLeft));return{top:l,left:m}},f.offset={initialize:function(){var a=c.body,b=c.createElement("div"),d,e,g,h,i=parseFloat(f.css(a,"marginTop"))||0,j="<div style='position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;'><div></div></div><table style='position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;' cellpadding='0' cellspacing='0'><tr><td></td></tr></table>";f.extend(b.style,{position:"absolute",top:0,left:0,margin:0,border:0,width:"1px",height:"1px",visibility:"hidden"}),b.innerHTML=j,a.insertBefore(b,a.firstChild),d=b.firstChild,e=d.firstChild,h=d.nextSibling.firstChild.firstChild,this.doesNotAddBorder=e.offsetTop!==5,this.doesAddBorderForTableAndCells=h.offsetTop===5,e.style.position="fixed",e.style.top="20px",this.supportsFixedPosition=e.offsetTop===20||e.offsetTop===15,e.style.position=e.style.top="",d.style.overflow="hidden",d.style.position="relative",this.subtractsBorderForOverflowNotVisible=e.offsetTop===-5,this.doesNotIncludeMarginInBodyOffset=a.offsetTop!==i,a.removeChild(b),f.offset.initialize=f.noop},bodyOffset:function(a){var b=a.offsetTop,c=a.offsetLeft;f.offset.initialize(),f.offset.doesNotIncludeMarginInBodyOffset&&(b+=parseFloat(f.css(a,"marginTop"))||0,c+=parseFloat(f.css(a,"marginLeft"))||0);return{top:b,left:c}},setOffset:function(a,b,c){var d=f.css(a,"position");d==="static"&&(a.style.position="relative");var e=f(a),g=e.offset(),h=f.css(a,"top"),i=f.css(a,"left"),j=(d==="absolute"||d==="fixed")&&f.inArray("auto",[h,i])>-1,k={},l={},m,n;j?(l=e.position(),m=l.top,n=l.left):(m=parseFloat(h)||0,n=parseFloat(i)||0),f.isFunction(b)&&(b=b.call(a,c,g)),b.top!=null&&(k.top=b.top-g.top+m),b.left!=null&&(k.left=b.left-g.left+n),"using"in b?b.using.call(a,k):e.css(k)}},f.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),c=this.offset(),d=cv.test(b[0].nodeName)?{top:0,left:0}:b.offset();c.top-=parseFloat(f.css(a,"marginTop"))||0,c.left-=parseFloat(f.css(a,"marginLeft"))||0,d.top+=parseFloat(f.css(b[0],"borderTopWidth"))||0,d.left+=parseFloat(f.css(b[0],"borderLeftWidth"))||0;return{top:c.top-d.top,left:c.left-d.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||c.body;while(a&&!cv.test(a.nodeName)&&f.css(a,"position")==="static")a=a.offsetParent;return a})}}),f.each(["Left","Top"],function(a,c){var d="scroll"+c;f.fn[d]=function(c){var e,g;if(c===b){e=this[0];if(!e)return null;g=cw(e);return g?"pageXOffset"in g?g[a?"pageYOffset":"pageXOffset"]:f.support.boxModel&&g.document.documentElement[d]||g.document.body[d]:e[d]}return this.each(function(){g=cw(this),g?g.scrollTo(a?f(g).scrollLeft():c,a?c:f(g).scrollTop()):this[d]=c})}}),f.each(["Height","Width"],function(a,c){var d=c.toLowerCase();f.fn["inner"+c]=function(){return this[0]?parseFloat(f.css(this[0],d,"padding")):null},f.fn["outer"+c]=function(a){return this[0]?parseFloat(f.css(this[0],d,a?"margin":"border")):null},f.fn[d]=function(a){var e=this[0];if(!e)return a==null?null:this;if(f.isFunction(a))return this.each(function(b){var c=f(this);c[d](a.call(this,b,c[d]()))});if(f.isWindow(e)){var g=e.document.documentElement["client"+c];return e.document.compatMode==="CSS1Compat"&&g||e.document.body["client"+c]||g}if(e.nodeType===9)return Math.max(e.documentElement["client"+c],e.body["scroll"+c],e.documentElement["scroll"+c],e.body["offset"+c],e.documentElement["offset"+c]);if(a===b){var h=f.css(e,d),i=parseFloat(h);return f.isNaN(i)?h:i}return this.css(d,typeof a=="string"?a:a+"px")}}),a.jQuery=a.$=f})(window); \ No newline at end of file
diff --git a/devtools/client/inspector/markup/test/lib_jquery_1.7_min.js b/devtools/client/inspector/markup/test/lib_jquery_1.7_min.js
new file mode 100644
index 000000000..3ca5e0f5d
--- /dev/null
+++ b/devtools/client/inspector/markup/test/lib_jquery_1.7_min.js
@@ -0,0 +1,4 @@
+/*! jQuery v1.7 jquery.com | jquery.org/license */
+(function(a,b){function cA(a){return f.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:!1}function cx(a){if(!cm[a]){var b=c.body,d=f("<"+a+">").appendTo(b),e=d.css("display");d.remove();if(e==="none"||e===""){cn||(cn=c.createElement("iframe"),cn.frameBorder=cn.width=cn.height=0),b.appendChild(cn);if(!co||!cn.createElement)co=(cn.contentWindow||cn.contentDocument).document,co.write((c.compatMode==="CSS1Compat"?"<!doctype html>":"")+"<html><body>"),co.close();d=co.createElement(a),co.body.appendChild(d),e=f.css(d,"display"),b.removeChild(cn)}cm[a]=e}return cm[a]}function cw(a,b){var c={};f.each(cs.concat.apply([],cs.slice(0,b)),function(){c[this]=a});return c}function cv(){ct=b}function cu(){setTimeout(cv,0);return ct=f.now()}function cl(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}function ck(){try{return new a.XMLHttpRequest}catch(b){}}function ce(a,c){a.dataFilter&&(c=a.dataFilter(c,a.dataType));var d=a.dataTypes,e={},g,h,i=d.length,j,k=d[0],l,m,n,o,p;for(g=1;g<i;g++){if(g===1)for(h in a.converters)typeof h=="string"&&(e[h.toLowerCase()]=a.converters[h]);l=k,k=d[g];if(k==="*")k=l;else if(l!=="*"&&l!==k){m=l+" "+k,n=e[m]||e["* "+k];if(!n){p=b;for(o in e){j=o.split(" ");if(j[0]===l||j[0]==="*"){p=e[j[1]+" "+k];if(p){o=e[o],o===!0?n=p:p===!0&&(n=o);break}}}}!n&&!p&&f.error("No conversion from "+m.replace(" "," to ")),n!==!0&&(c=n?n(c):p(o(c)))}}return c}function cd(a,c,d){var e=a.contents,f=a.dataTypes,g=a.responseFields,h,i,j,k;for(i in g)i in d&&(c[g[i]]=d[i]);while(f[0]==="*")f.shift(),h===b&&(h=a.mimeType||c.getResponseHeader("content-type"));if(h)for(i in e)if(e[i]&&e[i].test(h)){f.unshift(i);break}if(f[0]in d)j=f[0];else{for(i in d){if(!f[0]||a.converters[i+" "+f[0]]){j=i;break}k||(k=i)}j=j||k}if(j){j!==f[0]&&f.unshift(j);return d[j]}}function cc(a,b,c,d){if(f.isArray(b))f.each(b,function(b,e){c||bG.test(a)?d(a,e):cc(a+"["+(typeof e=="object"||f.isArray(e)?b:"")+"]",e,c,d)});else if(!c&&b!=null&&typeof b=="object")for(var e in b)cc(a+"["+e+"]",b[e],c,d);else d(a,b)}function cb(a,c){var d,e,g=f.ajaxSettings.flatOptions||{};for(d in c)c[d]!==b&&((g[d]?a:e||(e={}))[d]=c[d]);e&&f.extend(!0,a,e)}function ca(a,c,d,e,f,g){f=f||c.dataTypes[0],g=g||{},g[f]=!0;var h=a[f],i=0,j=h?h.length:0,k=a===bV,l;for(;i<j&&(k||!l);i++)l=h[i](c,d,e),typeof l=="string"&&(!k||g[l]?l=b:(c.dataTypes.unshift(l),l=ca(a,c,d,e,l,g)));(k||!l)&&!g["*"]&&(l=ca(a,c,d,e,"*",g));return l}function b_(a){return function(b,c){typeof b!="string"&&(c=b,b="*");if(f.isFunction(c)){var d=b.toLowerCase().split(bR),e=0,g=d.length,h,i,j;for(;e<g;e++)h=d[e],j=/^\+/.test(h),j&&(h=h.substr(1)||"*"),i=a[h]=a[h]||[],i[j?"unshift":"push"](c)}}}function bE(a,b,c){var d=b==="width"?a.offsetWidth:a.offsetHeight,e=b==="width"?bz:bA;if(d>0){c!=="border"&&f.each(e,function(){c||(d-=parseFloat(f.css(a,"padding"+this))||0),c==="margin"?d+=parseFloat(f.css(a,c+this))||0:d-=parseFloat(f.css(a,"border"+this+"Width"))||0});return d+"px"}d=bB(a,b,b);if(d<0||d==null)d=a.style[b]||0;d=parseFloat(d)||0,c&&f.each(e,function(){d+=parseFloat(f.css(a,"padding"+this))||0,c!=="padding"&&(d+=parseFloat(f.css(a,"border"+this+"Width"))||0),c==="margin"&&(d+=parseFloat(f.css(a,c+this))||0)});return d+"px"}function br(a,b){b.src?f.ajax({url:b.src,async:!1,dataType:"script"}):f.globalEval((b.text||b.textContent||b.innerHTML||"").replace(bi,"/*$0*/")),b.parentNode&&b.parentNode.removeChild(b)}function bq(a){var b=(a.nodeName||"").toLowerCase();b==="input"?bp(a):b!=="script"&&typeof a.getElementsByTagName!="undefined"&&f.grep(a.getElementsByTagName("input"),bp)}function bp(a){if(a.type==="checkbox"||a.type==="radio")a.defaultChecked=a.checked}function bo(a){return typeof a.getElementsByTagName!="undefined"?a.getElementsByTagName("*"):typeof a.querySelectorAll!="undefined"?a.querySelectorAll("*"):[]}function bn(a,b){var c;if(b.nodeType===1){b.clearAttributes&&b.clearAttributes(),b.mergeAttributes&&b.mergeAttributes(a),c=b.nodeName.toLowerCase();if(c==="object")b.outerHTML=a.outerHTML;else if(c!=="input"||a.type!=="checkbox"&&a.type!=="radio"){if(c==="option")b.selected=a.defaultSelected;else if(c==="input"||c==="textarea")b.defaultValue=a.defaultValue}else a.checked&&(b.defaultChecked=b.checked=a.checked),b.value!==a.value&&(b.value=a.value);b.removeAttribute(f.expando)}}function bm(a,b){if(b.nodeType===1&&!!f.hasData(a)){var c,d,e,g=f._data(a),h=f._data(b,g),i=g.events;if(i){delete h.handle,h.events={};for(c in i)for(d=0,e=i[c].length;d<e;d++)f.event.add(b,c+(i[c][d].namespace?".":"")+i[c][d].namespace,i[c][d],i[c][d].data)}h.data&&(h.data=f.extend({},h.data))}}function bl(a,b){return f.nodeName(a,"table")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function X(a){var b=Y.split(" "),c=a.createDocumentFragment();if(c.createElement)while(b.length)c.createElement(b.pop());return c}function W(a,b,c){b=b||0;if(f.isFunction(b))return f.grep(a,function(a,d){var e=!!b.call(a,d,a);return e===c});if(b.nodeType)return f.grep(a,function(a,d){return a===b===c});if(typeof b=="string"){var d=f.grep(a,function(a){return a.nodeType===1});if(R.test(b))return f.filter(b,d,!c);b=f.filter(b,d)}return f.grep(a,function(a,d){return f.inArray(a,b)>=0===c})}function V(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function N(){return!0}function M(){return!1}function n(a,b,c){var d=b+"defer",e=b+"queue",g=b+"mark",h=f._data(a,d);h&&(c==="queue"||!f._data(a,e))&&(c==="mark"||!f._data(a,g))&&setTimeout(function(){!f._data(a,e)&&!f._data(a,g)&&(f.removeData(a,d,!0),h.fire())},0)}function m(a){for(var b in a){if(b==="data"&&f.isEmptyObject(a[b]))continue;if(b!=="toJSON")return!1}return!0}function l(a,c,d){if(d===b&&a.nodeType===1){var e="data-"+c.replace(k,"-$1").toLowerCase();d=a.getAttribute(e);if(typeof d=="string"){try{d=d==="true"?!0:d==="false"?!1:d==="null"?null:f.isNumeric(d)?parseFloat(d):j.test(d)?f.parseJSON(d):d}catch(g){}f.data(a,c,d)}else d=b}return d}function h(a){var b=g[a]={},c,d;a=a.split(/\s+/);for(c=0,d=a.length;c<d;c++)b[a[c]]=!0;return b}var c=a.document,d=a.navigator,e=a.location,f=function(){function K(){if(!e.isReady){try{c.documentElement.doScroll("left")}catch(a){setTimeout(K,1);return}e.ready()}}var e=function(a,b){return new e.fn.init(a,b,h)},f=a.jQuery,g=a.$,h,i=/^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/,j=/\S/,k=/^\s+/,l=/\s+$/,m=/\d/,n=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,o=/^[\],:{}\s]*$/,p=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,q=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,r=/(?:^|:|,)(?:\s*\[)+/g,s=/(webkit)[ \/]([\w.]+)/,t=/(opera)(?:.*version)?[ \/]([\w.]+)/,u=/(msie) ([\w.]+)/,v=/(mozilla)(?:.*? rv:([\w.]+))?/,w=/-([a-z]|[0-9])/ig,x=/^-ms-/,y=function(a,b){return(b+"").toUpperCase()},z=d.userAgent,A,B,C,D=Object.prototype.toString,E=Object.prototype.hasOwnProperty,F=Array.prototype.push,G=Array.prototype.slice,H=String.prototype.trim,I=Array.prototype.indexOf,J={};e.fn=e.prototype={constructor:e,init:function(a,d,f){var g,h,j,k;if(!a)return this;if(a.nodeType){this.context=this[0]=a,this.length=1;return this}if(a==="body"&&!d&&c.body){this.context=c,this[0]=c.body,this.selector=a,this.length=1;return this}if(typeof a=="string"){a.charAt(0)!=="<"||a.charAt(a.length-1)!==">"||a.length<3?g=i.exec(a):g=[null,a,null];if(g&&(g[1]||!d)){if(g[1]){d=d instanceof e?d[0]:d,k=d?d.ownerDocument||d:c,j=n.exec(a),j?e.isPlainObject(d)?(a=[c.createElement(j[1])],e.fn.attr.call(a,d,!0)):a=[k.createElement(j[1])]:(j=e.buildFragment([g[1]],[k]),a=(j.cacheable?e.clone(j.fragment):j.fragment).childNodes);return e.merge(this,a)}h=c.getElementById(g[2]);if(h&&h.parentNode){if(h.id!==g[2])return f.find(a);this.length=1,this[0]=h}this.context=c,this.selector=a;return this}return!d||d.jquery?(d||f).find(a):this.constructor(d).find(a)}if(e.isFunction(a))return f.ready(a);a.selector!==b&&(this.selector=a.selector,this.context=a.context);return e.makeArray(a,this)},selector:"",jquery:"1.7",length:0,size:function(){return this.length},toArray:function(){return G.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var d=this.constructor();e.isArray(a)?F.apply(d,a):e.merge(d,a),d.prevObject=this,d.context=this.context,b==="find"?d.selector=this.selector+(this.selector?" ":"")+c:b&&(d.selector=this.selector+"."+b+"("+c+")");return d},each:function(a,b){return e.each(this,a,b)},ready:function(a){e.bindReady(),B.add(a);return this},eq:function(a){return a===-1?this.slice(a):this.slice(a,+a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(G.apply(this,arguments),"slice",G.call(arguments).join(","))},map:function(a){return this.pushStack(e.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:F,sort:[].sort,splice:[].splice},e.fn.init.prototype=e.fn,e.extend=e.fn.extend=function(){var a,c,d,f,g,h,i=arguments[0]||{},j=1,k=arguments.length,l=!1;typeof i=="boolean"&&(l=i,i=arguments[1]||{},j=2),typeof i!="object"&&!e.isFunction(i)&&(i={}),k===j&&(i=this,--j);for(;j<k;j++)if((a=arguments[j])!=null)for(c in a){d=i[c],f=a[c];if(i===f)continue;l&&f&&(e.isPlainObject(f)||(g=e.isArray(f)))?(g?(g=!1,h=d&&e.isArray(d)?d:[]):h=d&&e.isPlainObject(d)?d:{},i[c]=e.extend(l,h,f)):f!==b&&(i[c]=f)}return i},e.extend({noConflict:function(b){a.$===e&&(a.$=g),b&&a.jQuery===e&&(a.jQuery=f);return e},isReady:!1,readyWait:1,holdReady:function(a){a?e.readyWait++:e.ready(!0)},ready:function(a){if(a===!0&&!--e.readyWait||a!==!0&&!e.isReady){if(!c.body)return setTimeout(e.ready,1);e.isReady=!0;if(a!==!0&&--e.readyWait>0)return;B.fireWith(c,[e]),e.fn.trigger&&e(c).trigger("ready").unbind("ready")}},bindReady:function(){if(!B){B=e.Callbacks("once memory");if(c.readyState==="complete")return setTimeout(e.ready,1);if(c.addEventListener)c.addEventListener("DOMContentLoaded",C,!1),a.addEventListener("load",e.ready,!1);else if(c.attachEvent){c.attachEvent("onreadystatechange",C),a.attachEvent("onload",e.ready);var b=!1;try{b=a.frameElement==null}catch(d){}c.documentElement.doScroll&&b&&K()}}},isFunction:function(a){return e.type(a)==="function"},isArray:Array.isArray||function(a){return e.type(a)==="array"},isWindow:function(a){return a&&typeof a=="object"&&"setInterval"in a},isNumeric:function(a){return a!=null&&m.test(a)&&!isNaN(a)},type:function(a){return a==null?String(a):J[D.call(a)]||"object"},isPlainObject:function(a){if(!a||e.type(a)!=="object"||a.nodeType||e.isWindow(a))return!1;try{if(a.constructor&&!E.call(a,"constructor")&&!E.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}var d;for(d in a);return d===b||E.call(a,d)},isEmptyObject:function(a){for(var b in a)return!1;return!0},error:function(a){throw a},parseJSON:function(b){if(typeof b!="string"||!b)return null;b=e.trim(b);if(a.JSON&&a.JSON.parse)return a.JSON.parse(b);if(o.test(b.replace(p,"@").replace(q,"]").replace(r,"")))return(new Function("return "+b))();e.error("Invalid JSON: "+b)},parseXML:function(c){var d,f;try{a.DOMParser?(f=new DOMParser,d=f.parseFromString(c,"text/xml")):(d=new ActiveXObject("Microsoft.XMLDOM"),d.async="false",d.loadXML(c))}catch(g){d=b}(!d||!d.documentElement||d.getElementsByTagName("parsererror").length)&&e.error("Invalid XML: "+c);return d},noop:function(){},globalEval:function(b){b&&j.test(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(x,"ms-").replace(w,y)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,c,d){var f,g=0,h=a.length,i=h===b||e.isFunction(a);if(d){if(i){for(f in a)if(c.apply(a[f],d)===!1)break}else for(;g<h;)if(c.apply(a[g++],d)===!1)break}else if(i){for(f in a)if(c.call(a[f],f,a[f])===!1)break}else for(;g<h;)if(c.call(a[g],g,a[g++])===!1)break;return a},trim:H?function(a){return a==null?"":H.call(a)}:function(a){return a==null?"":(a+"").replace(k,"").replace(l,"")},makeArray:function(a,b){var c=b||[];if(a!=null){var d=e.type(a);a.length==null||d==="string"||d==="function"||d==="regexp"||e.isWindow(a)?F.call(c,a):e.merge(c,a)}return c},inArray:function(a,b,c){var d;if(b){if(I)return I.call(b,a,c);d=b.length,c=c?c<0?Math.max(0,d+c):c:0;for(;c<d;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,c){var d=a.length,e=0;if(typeof c.length=="number")for(var f=c.length;e<f;e++)a[d++]=c[e];else while(c[e]!==b)a[d++]=c[e++];a.length=d;return a},grep:function(a,b,c){var d=[],e;c=!!c;for(var f=0,g=a.length;f<g;f++)e=!!b(a[f],f),c!==e&&d.push(a[f]);return d},map:function(a,c,d){var f,g,h=[],i=0,j=a.length,k=a instanceof e||j!==b&&typeof j=="number"&&(j>0&&a[0]&&a[j-1]||j===0||e.isArray(a));if(k)for(;i<j;i++)f=c(a[i],i,d),f!=null&&(h[h.length]=f);else for(g in a)f=c(a[g],g,d),f!=null&&(h[h.length]=f);return h.concat.apply([],h)},guid:1,proxy:function(a,c){if(typeof c=="string"){var d=a[c];c=a,a=d}if(!e.isFunction(a))return b;var f=G.call(arguments,2),g=function(){return a.apply(c,f.concat(G.call(arguments)))};g.guid=a.guid=a.guid||g.guid||e.guid++;return g},access:function(a,c,d,f,g,h){var i=a.length;if(typeof c=="object"){for(var j in c)e.access(a,j,c[j],f,g,d);return a}if(d!==b){f=!h&&f&&e.isFunction(d);for(var k=0;k<i;k++)g(a[k],c,f?d.call(a[k],k,g(a[k],c)):d,h);return a}return i?g(a[0],c):b},now:function(){return(new Date).getTime()},uaMatch:function(a){a=a.toLowerCase();var b=s.exec(a)||t.exec(a)||u.exec(a)||a.indexOf("compatible")<0&&v.exec(a)||[];return{browser:b[1]||"",version:b[2]||"0"}},sub:function(){function a(b,c){return new a.fn.init(b,c)}e.extend(!0,a,this),a.superclass=this,a.fn=a.prototype=this(),a.fn.constructor=a,a.sub=this.sub,a.fn.init=function(d,f){f&&f instanceof e&&!(f instanceof a)&&(f=a(f));return e.fn.init.call(this,d,f,b)},a.fn.init.prototype=a.fn;var b=a(c);return a},browser:{}}),e.each("Boolean Number String Function Array Date RegExp Object".split(" "),function(a,b){J["[object "+b+"]"]=b.toLowerCase()}),A=e.uaMatch(z),A.browser&&(e.browser[A.browser]=!0,e.browser.version=A.version),e.browser.webkit&&(e.browser.safari=!0),j.test(" ")&&(k=/^[\s\xA0]+/,l=/[\s\xA0]+$/),h=e(c),c.addEventListener?C=function(){c.removeEventListener("DOMContentLoaded",C,!1),e.ready()}:c.attachEvent&&(C=function(){c.readyState==="complete"&&(c.detachEvent("onreadystatechange",C),e.ready())}),typeof define=="function"&&define.amd&&define.amd.jQuery&&define("jquery",[],function(){return e});return e}(),g={};f.Callbacks=function(a){a=a?g[a]||h(a):{};var c=[],d=[],e,i,j,k,l,m=function(b){var d,e,g,h,i;for(d=0,e=b.length;d<e;d++)g=b[d],h=f.type(g),h==="array"?m(g):h==="function"&&(!a.unique||!o.has(g))&&c.push(g)},n=function(b,f){f=f||[],e=!a.memory||[b,f],i=!0,l=j||0,j=0,k=c.length;for(;c&&l<k;l++)if(c[l].apply(b,f)===!1&&a.stopOnFalse){e=!0;break}i=!1,c&&(a.once?e===!0?o.disable():c=[]:d&&d.length&&(e=d.shift(),o.fireWith(e[0],e[1])))},o={add:function(){if(c){var a=c.length;m(arguments),i?k=c.length:e&&e!==!0&&(j=a,n(e[0],e[1]))}return this},remove:function(){if(c){var b=arguments,d=0,e=b.length;for(;d<e;d++)for(var f=0;f<c.length;f++)if(b[d]===c[f]){i&&f<=k&&(k--,f<=l&&l--),c.splice(f--,1);if(a.unique)break}}return this},has:function(a){if(c){var b=0,d=c.length;for(;b<d;b++)if(a===c[b])return!0}return!1},empty:function(){c=[];return this},disable:function(){c=d=e=b;return this},disabled:function(){return!c},lock:function(){d=b,(!e||e===!0)&&o.disable();return this},locked:function(){return!d},fireWith:function(b,c){d&&(i?a.once||d.push([b,c]):(!a.once||!e)&&n(b,c));return this},fire:function(){o.fireWith(this,arguments);return this},fired:function(){return!!e}};return o};var i=[].slice;f.extend({Deferred:function(a){var b=f.Callbacks("once memory"),c=f.Callbacks("once memory"),d=f.Callbacks("memory"),e="pending",g={resolve:b,reject:c,notify:d},h={done:b.add,fail:c.add,progress:d.add,state:function(){return e},isResolved:b.fired,isRejected:c.fired,then:function(a,b,c){i.done(a).fail(b).progress(c);return this},always:function(){return i.done.apply(i,arguments).fail.apply(i,arguments)},pipe:function(a,b,c){return f.Deferred(function(d){f.each({done:[a,"resolve"],fail:[b,"reject"],progress:[c,"notify"]},function(a,b){var c=b[0],e=b[1],g;f.isFunction(c)?i[a](function(){g=c.apply(this,arguments),g&&f.isFunction(g.promise)?g.promise().then(d.resolve,d.reject,d.notify):d[e+"With"](this===i?d:this,[g])}):i[a](d[e])})}).promise()},promise:function(a){if(a==null)a=h;else for(var b in h)a[b]=h[b];return a}},i=h.promise({}),j;for(j in g)i[j]=g[j].fire,i[j+"With"]=g[j].fireWith;i.done(function(){e="resolved"},c.disable,d.lock).fail(function(){e="rejected"},b.disable,d.lock),a&&a.call(i,i);return i},when:function(a){function m(a){return function(b){e[a]=arguments.length>1?i.call(arguments,0):b,j.notifyWith(k,e)}}function l(a){return function(c){b[a]=arguments.length>1?i.call(arguments,0):c,--g||j.resolveWith(j,b)}}var b=i.call(arguments,0),c=0,d=b.length,e=Array(d),g=d,h=d,j=d<=1&&a&&f.isFunction(a.promise)?a:f.Deferred(),k=j.promise();if(d>1){for(;c<d;c++)b[c]&&b[c].promise&&f.isFunction(b[c].promise)?b[c].promise().then(l(c),j.reject,m(c)):--g;g||j.resolveWith(j,b)}else j!==a&&j.resolveWith(j,d?[a]:[]);return k}}),f.support=function(){var a=c.createElement("div"),b=c.documentElement,d,e,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u;a.setAttribute("className","t"),a.innerHTML=" <link/><table></table><a href='/a' style='top:1px;float:left;opacity:.55;'>a</a><input type='checkbox'/><nav></nav>",d=a.getElementsByTagName("*"),e=a.getElementsByTagName("a")[0];if(!d||!d.length||!e)return{};g=c.createElement("select"),h=g.appendChild(c.createElement("option")),i=a.getElementsByTagName("input")[0],k={leadingWhitespace:a.firstChild.nodeType===3,tbody:!a.getElementsByTagName("tbody").length,htmlSerialize:!!a.getElementsByTagName("link").length,style:/top/.test(e.getAttribute("style")),hrefNormalized:e.getAttribute("href")==="/a",opacity:/^0.55/.test(e.style.opacity),cssFloat:!!e.style.cssFloat,unknownElems:!!a.getElementsByTagName("nav").length,checkOn:i.value==="on",optSelected:h.selected,getSetAttribute:a.className!=="t",enctype:!!c.createElement("form").enctype,submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0},i.checked=!0,k.noCloneChecked=i.cloneNode(!0).checked,g.disabled=!0,k.optDisabled=!h.disabled;try{delete a.test}catch(v){k.deleteExpando=!1}!a.addEventListener&&a.attachEvent&&a.fireEvent&&(a.attachEvent("onclick",function(){k.noCloneEvent=!1}),a.cloneNode(!0).fireEvent("onclick")),i=c.createElement("input"),i.value="t",i.setAttribute("type","radio"),k.radioValue=i.value==="t",i.setAttribute("checked","checked"),a.appendChild(i),l=c.createDocumentFragment(),l.appendChild(a.lastChild),k.checkClone=l.cloneNode(!0).cloneNode(!0).lastChild.checked,a.innerHTML="",a.style.width=a.style.paddingLeft="1px",m=c.getElementsByTagName("body")[0],o=c.createElement(m?"div":"body"),p={visibility:"hidden",width:0,height:0,border:0,margin:0,background:"none"},m&&f.extend(p,{position:"absolute",left:"-999px",top:"-999px"});for(t in p)o.style[t]=p[t];o.appendChild(a),n=m||b,n.insertBefore(o,n.firstChild),k.appendChecked=i.checked,k.boxModel=a.offsetWidth===2,"zoom"in a.style&&(a.style.display="inline",a.style.zoom=1,k.inlineBlockNeedsLayout=a.offsetWidth===2,a.style.display="",a.innerHTML="<div style='width:4px;'></div>",k.shrinkWrapBlocks=a.offsetWidth!==2),a.innerHTML="<table><tr><td style='padding:0;border:0;display:none'></td><td>t</td></tr></table>",q=a.getElementsByTagName("td"),u=q[0].offsetHeight===0,q[0].style.display="",q[1].style.display="none",k.reliableHiddenOffsets=u&&q[0].offsetHeight===0,a.innerHTML="",c.defaultView&&c.defaultView.getComputedStyle&&(j=c.createElement("div"),j.style.width="0",j.style.marginRight="0",a.appendChild(j),k.reliableMarginRight=(parseInt((c.defaultView.getComputedStyle(j,null)||{marginRight:0}).marginRight,10)||0)===0);if(a.attachEvent)for(t in{submit:1,change:1,focusin:1})s="on"+t,u=s in a,u||(a.setAttribute(s,"return;"),u=typeof a[s]=="function"),k[t+"Bubbles"]=u;f(function(){var a,b,d,e,g,h,i=1,j="position:absolute;top:0;left:0;width:1px;height:1px;margin:0;",l="visibility:hidden;border:0;",n="style='"+j+"border:5px solid #000;padding:0;'",p="<div "+n+"><div></div></div>"+"<table "+n+" cellpadding='0' cellspacing='0'>"+"<tr><td></td></tr></table>";m=c.getElementsByTagName("body")[0];!m||(a=c.createElement("div"),a.style.cssText=l+"width:0;height:0;position:static;top:0;margin-top:"+i+"px",m.insertBefore(a,m.firstChild),o=c.createElement("div"),o.style.cssText=j+l,o.innerHTML=p,a.appendChild(o),b=o.firstChild,d=b.firstChild,g=b.nextSibling.firstChild.firstChild,h={doesNotAddBorder:d.offsetTop!==5,doesAddBorderForTableAndCells:g.offsetTop===5},d.style.position="fixed",d.style.top="20px",h.fixedPosition=d.offsetTop===20||d.offsetTop===15,d.style.position=d.style.top="",b.style.overflow="hidden",b.style.position="relative",h.subtractsBorderForOverflowNotVisible=d.offsetTop===-5,h.doesNotIncludeMarginInBodyOffset=m.offsetTop!==i,m.removeChild(a),o=a=null,f.extend(k,h))}),o.innerHTML="",n.removeChild(o),o=l=g=h=m=j=a=i=null;return k}(),f.boxModel=f.support.boxModel;var j=/^(?:\{.*\}|\[.*\])$/,k=/([A-Z])/g;f.extend({cache:{},uuid:0,expando:"jQuery"+(f.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){a=a.nodeType?f.cache[a[f.expando]]:a[f.expando];return!!a&&!m(a)},data:function(a,c,d,e){if(!!f.acceptData(a)){var g,h,i,j=f.expando,k=typeof c=="string",l=a.nodeType,m=l?f.cache:a,n=l?a[f.expando]:a[f.expando]&&f.expando,o=c==="events";if((!n||!m[n]||!o&&!e&&!m[n].data)&&k&&d===b)return;n||(l?a[f.expando]=n=++f.uuid:n=f.expando),m[n]||(m[n]={},l||(m[n].toJSON=f.noop));if(typeof c=="object"||typeof c=="function")e?m[n]=f.extend(m[n],c):m[n].data=f.extend(m[n].data,c);g=h=m[n],e||(h.data||(h.data={}),h=h.data),d!==b&&(h[f.camelCase(c)]=d);if(o&&!h[c])return g.events;k?(i=h[c],i==null&&(i=h[f.camelCase(c)])):i=h;return i}},removeData:function(a,b,c){if(!!f.acceptData(a)){var d,e,g,h=f.expando,i=a.nodeType,j=i?f.cache:a,k=i?a[f.expando]:f.expando;if(!j[k])return;if(b){d=c?j[k]:j[k].data;if(d){f.isArray(b)?b=b:b in d?b=[b]:(b=f.camelCase(b),b in d?b=[b]:b=b.split(" "));for(e=0,g=b.length;e<g;e++)delete d[b[e]];if(!(c?m:f.isEmptyObject)(d))return}}if(!c){delete j[k].data;if(!m(j[k]))return}f.support.deleteExpando||!j.setInterval?delete j[k]:j[k]=null,i&&(f.support.deleteExpando?delete a[f.expando]:a.removeAttribute?a.removeAttribute(f.expando):a[f.expando]=null)}},_data:function(a,b,c){return f.data(a,b,c,!0)},acceptData:function(a){if(a.nodeName){var b=f.noData[a.nodeName.toLowerCase()];if(b)return b!==!0&&a.getAttribute("classid")===b}return!0}}),f.fn.extend({data:function(a,c){var d,e,g,h=null;if(typeof a=="undefined"){if(this.length){h=f.data(this[0]);if(this[0].nodeType===1&&!f._data(this[0],"parsedAttrs")){e=this[0].attributes;for(var i=0,j=e.length;i<j;i++)g=e[i].name,g.indexOf("data-")===0&&(g=f.camelCase(g.substring(5)),l(this[0],g,h[g]));f._data(this[0],"parsedAttrs",!0)}}return h}if(typeof a=="object")return this.each(function(){f.data(this,a)});d=a.split("."),d[1]=d[1]?"."+d[1]:"";if(c===b){h=this.triggerHandler("getData"+d[1]+"!",[d[0]]),h===b&&this.length&&(h=f.data(this[0],a),h=l(this[0],a,h));return h===b&&d[1]?this.data(d[0]):h}return this.each(function(){var b=f(this),e=[d[0],c];b.triggerHandler("setData"+d[1]+"!",e),f.data(this,a,c),b.triggerHandler("changeData"+d[1]+"!",e)})},removeData:function(a){return this.each(function(){f.removeData(this,a)})}}),f.extend({_mark:function(a,b){a&&(b=(b||"fx")+"mark",f._data(a,b,(f._data(a,b)||0)+1))},_unmark:function(a,b,c){a!==!0&&(c=b,b=a,a=!1);if(b){c=c||"fx";var d=c+"mark",e=a?0:(f._data(b,d)||1)-1;e?f._data(b,d,e):(f.removeData(b,d,!0),n(b,c,"mark"))}},queue:function(a,b,c){var d;if(a){b=(b||"fx")+"queue",d=f._data(a,b),c&&(!d||f.isArray(c)?d=f._data(a,b,f.makeArray(c)):d.push(c));return d||[]}},dequeue:function(a,b){b=b||"fx";var c=f.queue(a,b),d=c.shift(),e={};d==="inprogress"&&(d=c.shift()),d&&(b==="fx"&&c.unshift("inprogress"),f._data(a,b+".run",e),d.call(a,function(){f.dequeue(a,b)},e)),c.length||(f.removeData(a,b+"queue "+b+".run",!0),n(a,b,"queue"))}}),f.fn.extend({queue:function(a,c){typeof a!="string"&&(c=a,a="fx");if(c===b)return f.queue(this[0],a);return this.each(function(){var b=f.queue(this,a,c);a==="fx"&&b[0]!=="inprogress"&&f.dequeue(this,a)})},dequeue:function(a){return this.each(function(){f.dequeue(this,a)})},delay:function(a,b){a=f.fx?f.fx.speeds[a]||a:a,b=b||"fx";return this.queue(b,function(b,c){var d=setTimeout(b,a);c.stop=function(){clearTimeout(d)}})},clearQueue:function(a){return this.queue(a||"fx",[])},promise:function(a,c){function m(){--h||d.resolveWith(e,[e])}typeof a!="string"&&(c=a,a=b),a=a||"fx";var d=f.Deferred(),e=this,g=e.length,h=1,i=a+"defer",j=a+"queue",k=a+"mark",l;while(g--)if(l=f.data(e[g],i,b,!0)||(f.data(e[g],j,b,!0)||f.data(e[g],k,b,!0))&&f.data(e[g],i,f.Callbacks("once memory"),!0))h++,l.add(m);m();return d.promise()}});var o=/[\n\t\r]/g,p=/\s+/,q=/\r/g,r=/^(?:button|input)$/i,s=/^(?:button|input|object|select|textarea)$/i,t=/^a(?:rea)?$/i,u=/^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i,v=f.support.getSetAttribute,w,x,y;f.fn.extend({attr:function(a,b){return f.access(this,a,b,!0,f.attr)},removeAttr:function(a){return this.each(function(){f.removeAttr(this,a)})},prop:function(a,b){return f.access(this,a,b,!0,f.prop)},removeProp:function(a){a=f.propFix[a]||a;return this.each(function(){try{this[a]=b,delete this[a]}catch(c){}})},addClass:function(a){var b,c,d,e,g,h,i;if(f.isFunction(a))return this.each(function(b){f(this).addClass(a.call(this,b,this.className))});if(a&&typeof a=="string"){b=a.split(p);for(c=0,d=this.length;c<d;c++){e=this[c];if(e.nodeType===1)if(!e.className&&b.length===1)e.className=a;else{g=" "+e.className+" ";for(h=0,i=b.length;h<i;h++)~g.indexOf(" "+b[h]+" ")||(g+=b[h]+" ");e.className=f.trim(g)}}}return this},removeClass:function(a){var c,d,e,g,h,i,j;if(f.isFunction(a))return this.each(function(b){f(this).removeClass(a.call(this,b,this.className))});if(a&&typeof a=="string"||a===b){c=(a||"").split(p);for(d=0,e=this.length;d<e;d++){g=this[d];if(g.nodeType===1&&g.className)if(a){h=(" "+g.className+" ").replace(o," ");for(i=0,j=c.length;i<j;i++)h=h.replace(" "+c[i]+" "," ");g.className=f.trim(h)}else g.className=""}}return this},toggleClass:function(a,b){var c=typeof a,d=typeof b=="boolean";if(f.isFunction(a))return this.each(function(c){f(this).toggleClass(a.call(this,c,this.className,b),b)});return this.each(function(){if(c==="string"){var e,g=0,h=f(this),i=b,j=a.split(p);while(e=j[g++])i=d?i:!h.hasClass(e),h[i?"addClass":"removeClass"](e)}else if(c==="undefined"||c==="boolean")this.className&&f._data(this,"__className__",this.className),this.className=this.className||a===!1?"":f._data(this,"__className__")||""})},hasClass:function(a){var b=" "+a+" ",c=0,d=this.length;for(;c<d;c++)if(this[c].nodeType===1&&(" "+this[c].className+" ").replace(o," ").indexOf(b)>-1)return!0;return!1},val:function(a){var c,d,e,g=this[0];if(!arguments.length){if(g){c=f.valHooks[g.nodeName.toLowerCase()]||f.valHooks[g.type];if(c&&"get"in c&&(d=c.get(g,"value"))!==b)return d;d=g.value;return typeof d=="string"?d.replace(q,""):d==null?"":d}return b}e=f.isFunction(a);return this.each(function(d){var g=f(this),h;if(this.nodeType===1){e?h=a.call(this,d,g.val()):h=a,h==null?h="":typeof h=="number"?h+="":f.isArray(h)&&(h=f.map(h,function(a){return a==null?"":a+""})),c=f.valHooks[this.nodeName.toLowerCase()]||f.valHooks[this.type];if(!c||!("set"in c)||c.set(this,h,"value")===b)this.value=h}})}}),f.extend({valHooks:{option:{get:function(a){var b=a.attributes.value;return!b||b.specified?a.value:a.text}},select:{get:function(a){var b,c,d,e,g=a.selectedIndex,h=[],i=a.options,j=a.type==="select-one";if(g<0)return null;c=j?g:0,d=j?g+1:i.length;for(;c<d;c++){e=i[c];if(e.selected&&(f.support.optDisabled?!e.disabled:e.getAttribute("disabled")===null)&&(!e.parentNode.disabled||!f.nodeName(e.parentNode,"optgroup"))){b=f(e).val();if(j)return b;h.push(b)}}if(j&&!h.length&&i.length)return f(i[g]).val();return h},set:function(a,b){var c=f.makeArray(b);f(a).find("option").each(function(){this.selected=f.inArray(f(this).val(),c)>=0}),c.length||(a.selectedIndex=-1);return c}}},attrFn:{val:!0,css:!0,html:!0,text:!0,data:!0,width:!0,height:!0,offset:!0},attr:function(a,c,d,e){var g,h,i,j=a.nodeType;if(!a||j===3||j===8||j===2)return b;if(e&&c in f.attrFn)return f(a)[c](d);if(!("getAttribute"in a))return f.prop(a,c,d);i=j!==1||!f.isXMLDoc(a),i&&(c=c.toLowerCase(),h=f.attrHooks[c]||(u.test(c)?x:w));if(d!==b){if(d===null){f.removeAttr(a,c);return b}if(h&&"set"in h&&i&&(g=h.set(a,d,c))!==b)return g;a.setAttribute(c,""+d);return d}if(h&&"get"in h&&i&&(g=h.get(a,c))!==null)return g;g=a.getAttribute(c);return g===null?b:g},removeAttr:function(a,b){var c,d,e,g,h=0;if(a.nodeType===1){d=(b||"").split(p),g=d.length;for(;h<g;h++)e=d[h].toLowerCase(),c=f.propFix[e]||e,f.attr(a,e,""),a.removeAttribute(v?e:c),u.test(e)&&c in a&&(a[c]=!1)}},attrHooks:{type:{set:function(a,b){if(r.test(a.nodeName)&&a.parentNode)f.error("type property can't be changed");else if(!f.support.radioValue&&b==="radio"&&f.nodeName(a,"input")){var c=a.value;a.setAttribute("type",b),c&&(a.value=c);return b}}},value:{get:function(a,b){if(w&&f.nodeName(a,"button"))return w.get(a,b);return b in a?a.value:null},set:function(a,b,c){if(w&&f.nodeName(a,"button"))return w.set(a,b,c);a.value=b}}},propFix:{tabindex:"tabIndex",readonly:"readOnly","for":"htmlFor","class":"className",maxlength:"maxLength",cellspacing:"cellSpacing",cellpadding:"cellPadding",rowspan:"rowSpan",colspan:"colSpan",usemap:"useMap",frameborder:"frameBorder",contenteditable:"contentEditable"},prop:function(a,c,d){var e,g,h,i=a.nodeType;if(!a||i===3||i===8||i===2)return b;h=i!==1||!f.isXMLDoc(a),h&&(c=f.propFix[c]||c,g=f.propHooks[c]);return d!==b?g&&"set"in g&&(e=g.set(a,d,c))!==b?e:a[c]=d:g&&"get"in g&&(e=g.get(a,c))!==null?e:a[c]},propHooks:{tabIndex:{get:function(a){var c=a.getAttributeNode("tabindex");return c&&c.specified?parseInt(c.value,10):s.test(a.nodeName)||t.test(a.nodeName)&&a.href?0:b}}}}),f.attrHooks.tabindex=f.propHooks.tabIndex,x={get:function(a,c){var d,e=f.prop(a,c);return e===!0||typeof e!="boolean"&&(d=a.getAttributeNode(c))&&d.nodeValue!==!1?c.toLowerCase():b},set:function(a,b,c){var d;b===!1?f.removeAttr(a,c):(d=f.propFix[c]||c,d in a&&(a[d]=!0),a.setAttribute(c,c.toLowerCase()));return c}},v||(y={name:!0,id:!0},w=f.valHooks.button={get:function(a,c){var d;d=a.getAttributeNode(c);return d&&(y[c]?d.nodeValue!=="":d.specified)?d.nodeValue:b},set:function(a,b,d){var e=a.getAttributeNode(d);e||(e=c.createAttribute(d),a.setAttributeNode(e));return e.nodeValue=b+""}},f.attrHooks.tabindex.set=w.set,f.each(["width","height"],function(a,b){f.attrHooks[b]=f.extend(f.attrHooks[b],{set:function(a,c){if(c===""){a.setAttribute(b,"auto");return c}}})}),f.attrHooks.contenteditable={get:w.get,set:function(a,b,c){b===""&&(b="false"),w.set(a,b,c)}}),f.support.hrefNormalized||f.each(["href","src","width","height"],function(a,c){f.attrHooks[c]=f.extend(f.attrHooks[c],{get:function(a){var d=a.getAttribute(c,2);return d===null?b:d}})}),f.support.style||(f.attrHooks.style={get:function(a){return a.style.cssText.toLowerCase()||b},set:function(a,b){return a.style.cssText=""+b}}),f.support.optSelected||(f.propHooks.selected=f.extend(f.propHooks.selected,{get:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex);return null}})),f.support.enctype||(f.propFix.enctype="encoding"),f.support.checkOn||f.each(["radio","checkbox"],function(){f.valHooks[this]={get:function(a){return a.getAttribute("value")===null?"on":a.value}}}),f.each(["radio","checkbox"],function(){f.valHooks[this]=f.extend(f.valHooks[this],{set:function(a,b){if(f.isArray(b))return a.checked=f.inArray(f(a).val(),b)>=0}})});var z=/\.(.*)$/,A=/^(?:textarea|input|select)$/i,B=/\./g,C=/ /g,D=/[^\w\s.|`]/g,E=/^([^\.]*)?(?:\.(.+))?$/,F=/\bhover(\.\S+)?/,G=/^key/,H=/^(?:mouse|contextmenu)|click/,I=/^(\w*)(?:#([\w\-]+))?(?:\.([\w\-]+))?$/,J=function(a){var b=I.exec(a);b&&
+(b[1]=(b[1]||"").toLowerCase(),b[3]=b[3]&&new RegExp("(?:^|\\s)"+b[3]+"(?:\\s|$)"));return b},K=function(a,b){return(!b[1]||a.nodeName.toLowerCase()===b[1])&&(!b[2]||a.id===b[2])&&(!b[3]||b[3].test(a.className))},L=function(a){return f.event.special.hover?a:a.replace(F,"mouseenter$1 mouseleave$1")};f.event={add:function(a,c,d,e,g){var h,i,j,k,l,m,n,o,p,q,r,s;if(!(a.nodeType===3||a.nodeType===8||!c||!d||!(h=f._data(a)))){d.handler&&(p=d,d=p.handler),d.guid||(d.guid=f.guid++),j=h.events,j||(h.events=j={}),i=h.handle,i||(h.handle=i=function(a){return typeof f!="undefined"&&(!a||f.event.triggered!==a.type)?f.event.dispatch.apply(i.elem,arguments):b},i.elem=a),c=L(c).split(" ");for(k=0;k<c.length;k++){l=E.exec(c[k])||[],m=l[1],n=(l[2]||"").split(".").sort(),s=f.event.special[m]||{},m=(g?s.delegateType:s.bindType)||m,s=f.event.special[m]||{},o=f.extend({type:m,origType:l[1],data:e,handler:d,guid:d.guid,selector:g,namespace:n.join(".")},p),g&&(o.quick=J(g),!o.quick&&f.expr.match.POS.test(g)&&(o.isPositional=!0)),r=j[m];if(!r){r=j[m]=[],r.delegateCount=0;if(!s.setup||s.setup.call(a,e,n,i)===!1)a.addEventListener?a.addEventListener(m,i,!1):a.attachEvent&&a.attachEvent("on"+m,i)}s.add&&(s.add.call(a,o),o.handler.guid||(o.handler.guid=d.guid)),g?r.splice(r.delegateCount++,0,o):r.push(o),f.event.global[m]=!0}a=null}},global:{},remove:function(a,b,c,d){var e=f.hasData(a)&&f._data(a),g,h,i,j,k,l,m,n,o,p,q;if(!!e&&!!(m=e.events)){b=L(b||"").split(" ");for(g=0;g<b.length;g++){h=E.exec(b[g])||[],i=h[1],j=h[2];if(!i){j=j?"."+j:"";for(l in m)f.event.remove(a,l+j,c,d);return}n=f.event.special[i]||{},i=(d?n.delegateType:n.bindType)||i,p=m[i]||[],k=p.length,j=j?new RegExp("(^|\\.)"+j.split(".").sort().join("\\.(?:.*\\.)?")+"(\\.|$)"):null;if(c||j||d||n.remove)for(l=0;l<p.length;l++){q=p[l];if(!c||c.guid===q.guid)if(!j||j.test(q.namespace))if(!d||d===q.selector||d==="**"&&q.selector)p.splice(l--,1),q.selector&&p.delegateCount--,n.remove&&n.remove.call(a,q)}else p.length=0;p.length===0&&k!==p.length&&((!n.teardown||n.teardown.call(a,j)===!1)&&f.removeEvent(a,i,e.handle),delete m[i])}f.isEmptyObject(m)&&(o=e.handle,o&&(o.elem=null),f.removeData(a,["events","handle"],!0))}},customEvent:{getData:!0,setData:!0,changeData:!0},trigger:function(c,d,e,g){if(!e||e.nodeType!==3&&e.nodeType!==8){var h=c.type||c,i=[],j,k,l,m,n,o,p,q,r,s;h.indexOf("!")>=0&&(h=h.slice(0,-1),k=!0),h.indexOf(".")>=0&&(i=h.split("."),h=i.shift(),i.sort());if((!e||f.event.customEvent[h])&&!f.event.global[h])return;c=typeof c=="object"?c[f.expando]?c:new f.Event(h,c):new f.Event(h),c.type=h,c.isTrigger=!0,c.exclusive=k,c.namespace=i.join("."),c.namespace_re=c.namespace?new RegExp("(^|\\.)"+i.join("\\.(?:.*\\.)?")+"(\\.|$)"):null,o=h.indexOf(":")<0?"on"+h:"",(g||!e)&&c.preventDefault();if(!e){j=f.cache;for(l in j)j[l].events&&j[l].events[h]&&f.event.trigger(c,d,j[l].handle.elem,!0);return}c.result=b,c.target||(c.target=e),d=d!=null?f.makeArray(d):[],d.unshift(c),p=f.event.special[h]||{};if(p.trigger&&p.trigger.apply(e,d)===!1)return;r=[[e,p.bindType||h]];if(!g&&!p.noBubble&&!f.isWindow(e)){s=p.delegateType||h,n=null;for(m=e.parentNode;m;m=m.parentNode)r.push([m,s]),n=m;n&&n===e.ownerDocument&&r.push([n.defaultView||n.parentWindow||a,s])}for(l=0;l<r.length;l++){m=r[l][0],c.type=r[l][1],q=(f._data(m,"events")||{})[c.type]&&f._data(m,"handle"),q&&q.apply(m,d),q=o&&m[o],q&&f.acceptData(m)&&q.apply(m,d);if(c.isPropagationStopped())break}c.type=h,c.isDefaultPrevented()||(!p._default||p._default.apply(e.ownerDocument,d)===!1)&&(h!=="click"||!f.nodeName(e,"a"))&&f.acceptData(e)&&o&&e[h]&&(h!=="focus"&&h!=="blur"||c.target.offsetWidth!==0)&&!f.isWindow(e)&&(n=e[o],n&&(e[o]=null),f.event.triggered=h,e[h](),f.event.triggered=b,n&&(e[o]=n));return c.result}},dispatch:function(c){c=f.event.fix(c||a.event);var d=(f._data(this,"events")||{})[c.type]||[],e=d.delegateCount,g=[].slice.call(arguments,0),h=!c.exclusive&&!c.namespace,i=(f.event.special[c.type]||{}).handle,j=[],k,l,m,n,o,p,q,r,s,t,u;g[0]=c,c.delegateTarget=this;if(e&&!c.target.disabled&&(!c.button||c.type!=="click"))for(m=c.target;m!=this;m=m.parentNode||this){o={},q=[];for(k=0;k<e;k++)r=d[k],s=r.selector,t=o[s],r.isPositional?t=(t||(o[s]=f(s))).index(m)>=0:t===b&&(t=o[s]=r.quick?K(m,r.quick):f(m).is(s)),t&&q.push(r);q.length&&j.push({elem:m,matches:q})}d.length>e&&j.push({elem:this,matches:d.slice(e)});for(k=0;k<j.length&&!c.isPropagationStopped();k++){p=j[k],c.currentTarget=p.elem;for(l=0;l<p.matches.length&&!c.isImmediatePropagationStopped();l++){r=p.matches[l];if(h||!c.namespace&&!r.namespace||c.namespace_re&&c.namespace_re.test(r.namespace))c.data=r.data,c.handleObj=r,n=(i||r.handler).apply(p.elem,g),n!==b&&(c.result=n,n===!1&&(c.preventDefault(),c.stopPropagation()))}}return c.result},props:"attrChange attrName relatedNode srcElement altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(a,b){a.which==null&&(a.which=b.charCode!=null?b.charCode:b.keyCode);return a}},mouseHooks:{props:"button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement wheelDelta".split(" "),filter:function(a,d){var e,f,g,h=d.button,i=d.fromElement;a.pageX==null&&d.clientX!=null&&(e=a.target.ownerDocument||c,f=e.documentElement,g=e.body,a.pageX=d.clientX+(f&&f.scrollLeft||g&&g.scrollLeft||0)-(f&&f.clientLeft||g&&g.clientLeft||0),a.pageY=d.clientY+(f&&f.scrollTop||g&&g.scrollTop||0)-(f&&f.clientTop||g&&g.clientTop||0)),!a.relatedTarget&&i&&(a.relatedTarget=i===a.target?d.toElement:i),!a.which&&h!==b&&(a.which=h&1?1:h&2?3:h&4?2:0);return a}},fix:function(a){if(a[f.expando])return a;var d,e,g=a,h=f.event.fixHooks[a.type]||{},i=h.props?this.props.concat(h.props):this.props;a=f.Event(g);for(d=i.length;d;)e=i[--d],a[e]=g[e];a.target||(a.target=g.srcElement||c),a.target.nodeType===3&&(a.target=a.target.parentNode),a.metaKey===b&&(a.metaKey=a.ctrlKey);return h.filter?h.filter(a,g):a},special:{ready:{setup:f.bindReady},focus:{delegateType:"focusin",noBubble:!0},blur:{delegateType:"focusout",noBubble:!0},beforeunload:{setup:function(a,b,c){f.isWindow(this)&&(this.onbeforeunload=c)},teardown:function(a,b){this.onbeforeunload===b&&(this.onbeforeunload=null)}}},simulate:function(a,b,c,d){var e=f.extend(new f.Event,c,{type:a,isSimulated:!0,originalEvent:{}});d?f.event.trigger(e,null,b):f.event.dispatch.call(b,e),e.isDefaultPrevented()&&c.preventDefault()}},f.event.handle=f.event.dispatch,f.removeEvent=c.removeEventListener?function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c,!1)}:function(a,b,c){a.detachEvent&&a.detachEvent("on"+b,c)},f.Event=function(a,b){if(!(this instanceof f.Event))return new f.Event(a,b);a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||a.returnValue===!1||a.getPreventDefault&&a.getPreventDefault()?N:M):this.type=a,b&&f.extend(this,b),this.timeStamp=a&&a.timeStamp||f.now(),this[f.expando]=!0},f.Event.prototype={preventDefault:function(){this.isDefaultPrevented=N;var a=this.originalEvent;!a||(a.preventDefault?a.preventDefault():a.returnValue=!1)},stopPropagation:function(){this.isPropagationStopped=N;var a=this.originalEvent;!a||(a.stopPropagation&&a.stopPropagation(),a.cancelBubble=!0)},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=N,this.stopPropagation()},isDefaultPrevented:M,isPropagationStopped:M,isImmediatePropagationStopped:M},f.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(a,b){f.event.special[a]=f.event.special[b]={delegateType:b,bindType:b,handle:function(a){var b=this,c=a.relatedTarget,d=a.handleObj,e=d.selector,g,h;if(!c||d.origType===a.type||c!==b&&!f.contains(b,c))g=a.type,a.type=d.origType,h=d.handler.apply(this,arguments),a.type=g;return h}}}),f.support.submitBubbles||(f.event.special.submit={setup:function(){if(f.nodeName(this,"form"))return!1;f.event.add(this,"click._submit keypress._submit",function(a){var c=a.target,d=f.nodeName(c,"input")||f.nodeName(c,"button")?c.form:b;d&&!d._submit_attached&&(f.event.add(d,"submit._submit",function(a){this.parentNode&&f.event.simulate("submit",this.parentNode,a,!0)}),d._submit_attached=!0)})},teardown:function(){if(f.nodeName(this,"form"))return!1;f.event.remove(this,"._submit")}}),f.support.changeBubbles||(f.event.special.change={setup:function(){if(A.test(this.nodeName)){if(this.type==="checkbox"||this.type==="radio")f.event.add(this,"propertychange._change",function(a){a.originalEvent.propertyName==="checked"&&(this._just_changed=!0)}),f.event.add(this,"click._change",function(a){this._just_changed&&(this._just_changed=!1,f.event.simulate("change",this,a,!0))});return!1}f.event.add(this,"beforeactivate._change",function(a){var b=a.target;A.test(b.nodeName)&&!b._change_attached&&(f.event.add(b,"change._change",function(a){this.parentNode&&!a.isSimulated&&f.event.simulate("change",this.parentNode,a,!0)}),b._change_attached=!0)})},handle:function(a){var b=a.target;if(this!==b||a.isSimulated||a.isTrigger||b.type!=="radio"&&b.type!=="checkbox")return a.handleObj.handler.apply(this,arguments)},teardown:function(){f.event.remove(this,"._change");return A.test(this.nodeName)}}),f.support.focusinBubbles||f.each({focus:"focusin",blur:"focusout"},function(a,b){var d=0,e=function(a){f.event.simulate(b,a.target,f.event.fix(a),!0)};f.event.special[b]={setup:function(){d++===0&&c.addEventListener(a,e,!0)},teardown:function(){--d===0&&c.removeEventListener(a,e,!0)}}}),f.fn.extend({on:function(a,c,d,e,g){var h,i;if(typeof a=="object"){typeof c!="string"&&(d=c,c=b);for(i in a)this.on(i,c,d,a[i],g);return this}d==null&&e==null?(e=c,d=c=b):e==null&&(typeof c=="string"?(e=d,d=b):(e=d,d=c,c=b));if(e===!1)e=M;else if(!e)return this;g===1&&(h=e,e=function(a){f().off(a);return h.apply(this,arguments)},e.guid=h.guid||(h.guid=f.guid++));return this.each(function(){f.event.add(this,a,e,d,c)})},one:function(a,b,c,d){return this.on.call(this,a,b,c,d,1)},off:function(a,c,d){if(a&&a.preventDefault&&a.handleObj){var e=a.handleObj;f(a.delegateTarget).off(e.namespace?e.type+"."+e.namespace:e.type,e.selector,e.handler);return this}if(typeof a=="object"){for(var g in a)this.off(g,c,a[g]);return this}if(c===!1||typeof c=="function")d=c,c=b;d===!1&&(d=M);return this.each(function(){f.event.remove(this,a,d,c)})},bind:function(a,b,c){return this.on(a,null,b,c)},unbind:function(a,b){return this.off(a,null,b)},live:function(a,b,c){f(this.context).on(a,this.selector,b,c);return this},die:function(a,b){f(this.context).off(a,this.selector||"**",b);return this},delegate:function(a,b,c,d){return this.on(b,a,c,d)},undelegate:function(a,b,c){return arguments.length==1?this.off(a,"**"):this.off(b,a,c)},trigger:function(a,b){return this.each(function(){f.event.trigger(a,b,this)})},triggerHandler:function(a,b){if(this[0])return f.event.trigger(a,b,this[0],!0)},toggle:function(a){var b=arguments,c=a.guid||f.guid++,d=0,e=function(c){var e=(f._data(this,"lastToggle"+a.guid)||0)%d;f._data(this,"lastToggle"+a.guid,e+1),c.preventDefault();return b[e].apply(this,arguments)||!1};e.guid=c;while(d<b.length)b[d++].guid=c;return this.click(e)},hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),f.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(a,b){f.fn[b]=function(a,c){c==null&&(c=a,a=null);return arguments.length>0?this.bind(b,a,c):this.trigger(b)},f.attrFn&&(f.attrFn[b]=!0),G.test(b)&&(f.event.fixHooks[b]=f.event.keyHooks),H.test(b)&&(f.event.fixHooks[b]=f.event.mouseHooks)}),function(){function x(a,b,c,e,f,g){for(var h=0,i=e.length;h<i;h++){var j=e[h];if(j){var k=!1;j=j[a];while(j){if(j[d]===c){k=e[j.sizset];break}if(j.nodeType===1){g||(j[d]=c,j.sizset=h);if(typeof b!="string"){if(j===b){k=!0;break}}else if(m.filter(b,[j]).length>0){k=j;break}}j=j[a]}e[h]=k}}}function w(a,b,c,e,f,g){for(var h=0,i=e.length;h<i;h++){var j=e[h];if(j){var k=!1;j=j[a];while(j){if(j[d]===c){k=e[j.sizset];break}j.nodeType===1&&!g&&(j[d]=c,j.sizset=h);if(j.nodeName.toLowerCase()===b){k=j;break}j=j[a]}e[h]=k}}}var a=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,d="sizcache"+(Math.random()+"").replace(".",""),e=0,g=Object.prototype.toString,h=!1,i=!0,j=/\\/g,k=/\r\n/g,l=/\W/;[0,0].sort(function(){i=!1;return 0});var m=function(b,d,e,f){e=e||[],d=d||c;var h=d;if(d.nodeType!==1&&d.nodeType!==9)return[];if(!b||typeof b!="string")return e;var i,j,k,l,n,q,r,t,u=!0,v=m.isXML(d),w=[],x=b;do{a.exec(""),i=a.exec(x);if(i){x=i[3],w.push(i[1]);if(i[2]){l=i[3];break}}}while(i);if(w.length>1&&p.exec(b))if(w.length===2&&o.relative[w[0]])j=y(w[0]+w[1],d,f);else{j=o.relative[w[0]]?[d]:m(w.shift(),d);while(w.length)b=w.shift(),o.relative[b]&&(b+=w.shift()),j=y(b,j,f)}else{!f&&w.length>1&&d.nodeType===9&&!v&&o.match.ID.test(w[0])&&!o.match.ID.test(w[w.length-1])&&(n=m.find(w.shift(),d,v),d=n.expr?m.filter(n.expr,n.set)[0]:n.set[0]);if(d){n=f?{expr:w.pop(),set:s(f)}:m.find(w.pop(),w.length===1&&(w[0]==="~"||w[0]==="+")&&d.parentNode?d.parentNode:d,v),j=n.expr?m.filter(n.expr,n.set):n.set,w.length>0?k=s(j):u=!1;while(w.length)q=w.pop(),r=q,o.relative[q]?r=w.pop():q="",r==null&&(r=d),o.relative[q](k,r,v)}else k=w=[]}k||(k=j),k||m.error(q||b);if(g.call(k)==="[object Array]")if(!u)e.push.apply(e,k);else if(d&&d.nodeType===1)for(t=0;k[t]!=null;t++)k[t]&&(k[t]===!0||k[t].nodeType===1&&m.contains(d,k[t]))&&e.push(j[t]);else for(t=0;k[t]!=null;t++)k[t]&&k[t].nodeType===1&&e.push(j[t]);else s(k,e);l&&(m(l,h,e,f),m.uniqueSort(e));return e};m.uniqueSort=function(a){if(u){h=i,a.sort(u);if(h)for(var b=1;b<a.length;b++)a[b]===a[b-1]&&a.splice(b--,1)}return a},m.matches=function(a,b){return m(a,null,null,b)},m.matchesSelector=function(a,b){return m(b,null,null,[a]).length>0},m.find=function(a,b,c){var d,e,f,g,h,i;if(!a)return[];for(e=0,f=o.order.length;e<f;e++){h=o.order[e];if(g=o.leftMatch[h].exec(a)){i=g[1],g.splice(1,1);if(i.substr(i.length-1)!=="\\"){g[1]=(g[1]||"").replace(j,""),d=o.find[h](g,b,c);if(d!=null){a=a.replace(o.match[h],"");break}}}}d||(d=typeof b.getElementsByTagName!="undefined"?b.getElementsByTagName("*"):[]);return{set:d,expr:a}},m.filter=function(a,c,d,e){var f,g,h,i,j,k,l,n,p,q=a,r=[],s=c,t=c&&c[0]&&m.isXML(c[0]);while(a&&c.length){for(h in o.filter)if((f=o.leftMatch[h].exec(a))!=null&&f[2]){k=o.filter[h],l=f[1],g=!1,f.splice(1,1);if(l.substr(l.length-1)==="\\")continue;s===r&&(r=[]);if(o.preFilter[h]){f=o.preFilter[h](f,s,d,r,e,t);if(!f)g=i=!0;else if(f===!0)continue}if(f)for(n=0;(j=s[n])!=null;n++)j&&(i=k(j,f,n,s),p=e^i,d&&i!=null?p?g=!0:s[n]=!1:p&&(r.push(j),g=!0));if(i!==b){d||(s=r),a=a.replace(o.match[h],"");if(!g)return[];break}}if(a===q)if(g==null)m.error(a);else break;q=a}return s},m.error=function(a){throw"Syntax error, unrecognized expression: "+a};var n=m.getText=function(a){var b,c,d=a.nodeType,e="";if(d){if(d===1){if(typeof a.textContent=="string")return a.textContent;if(typeof a.innerText=="string")return a.innerText.replace(k,"");for(a=a.firstChild;a;a=a.nextSibling)e+=n(a)}else if(d===3||d===4)return a.nodeValue}else for(b=0;c=a[b];b++)c.nodeType!==8&&(e+=n(c));return e},o=m.selectors={order:["ID","NAME","TAG"],match:{ID:/#((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,CLASS:/\.((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,NAME:/\[name=['"]*((?:[\w\u00c0-\uFFFF\-]|\\.)+)['"]*\]/,ATTR:/\[\s*((?:[\w\u00c0-\uFFFF\-]|\\.)+)\s*(?:(\S?=)\s*(?:(['"])(.*?)\3|(#?(?:[\w\u00c0-\uFFFF\-]|\\.)*)|)|)\s*\]/,TAG:/^((?:[\w\u00c0-\uFFFF\*\-]|\\.)+)/,CHILD:/:(only|nth|last|first)-child(?:\(\s*(even|odd|(?:[+\-]?\d+|(?:[+\-]?\d*)?n\s*(?:[+\-]\s*\d+)?))\s*\))?/,POS:/:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^\-]|$)/,PSEUDO:/:((?:[\w\u00c0-\uFFFF\-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/},leftMatch:{},attrMap:{"class":"className","for":"htmlFor"},attrHandle:{href:function(a){return a.getAttribute("href")},type:function(a){return a.getAttribute("type")}},relative:{"+":function(a,b){var c=typeof b=="string",d=c&&!l.test(b),e=c&&!d;d&&(b=b.toLowerCase());for(var f=0,g=a.length,h;f<g;f++)if(h=a[f]){while((h=h.previousSibling)&&h.nodeType!==1);a[f]=e||h&&h.nodeName.toLowerCase()===b?h||!1:h===b}e&&m.filter(b,a,!0)},">":function(a,b){var c,d=typeof b=="string",e=0,f=a.length;if(d&&!l.test(b)){b=b.toLowerCase();for(;e<f;e++){c=a[e];if(c){var g=c.parentNode;a[e]=g.nodeName.toLowerCase()===b?g:!1}}}else{for(;e<f;e++)c=a[e],c&&(a[e]=d?c.parentNode:c.parentNode===b);d&&m.filter(b,a,!0)}},"":function(a,b,c){var d,f=e++,g=x;typeof b=="string"&&!l.test(b)&&(b=b.toLowerCase(),d=b,g=w),g("parentNode",b,f,a,d,c)},"~":function(a,b,c){var d,f=e++,g=x;typeof b=="string"&&!l.test(b)&&(b=b.toLowerCase(),d=b,g=w),g("previousSibling",b,f,a,d,c)}},find:{ID:function(a,b,c){if(typeof b.getElementById!="undefined"&&!c){var d=b.getElementById(a[1]);return d&&d.parentNode?[d]:[]}},NAME:function(a,b){if(typeof b.getElementsByName!="undefined"){var c=[],d=b.getElementsByName(a[1]);for(var e=0,f=d.length;e<f;e++)d[e].getAttribute("name")===a[1]&&c.push(d[e]);return c.length===0?null:c}},TAG:function(a,b){if(typeof b.getElementsByTagName!="undefined")return b.getElementsByTagName(a[1])}},preFilter:{CLASS:function(a,b,c,d,e,f){a=" "+a[1].replace(j,"")+" ";if(f)return a;for(var g=0,h;(h=b[g])!=null;g++)h&&(e^(h.className&&(" "+h.className+" ").replace(/[\t\n\r]/g," ").indexOf(a)>=0)?c||d.push(h):c&&(b[g]=!1));return!1},ID:function(a){return a[1].replace(j,"")},TAG:function(a,b){return a[1].replace(j,"").toLowerCase()},CHILD:function(a){if(a[1]==="nth"){a[2]||m.error(a[0]),a[2]=a[2].replace(/^\+|\s*/g,"");var b=/(-?)(\d*)(?:n([+\-]?\d*))?/.exec(a[2]==="even"&&"2n"||a[2]==="odd"&&"2n+1"||!/\D/.test(a[2])&&"0n+"+a[2]||a[2]);a[2]=b[1]+(b[2]||1)-0,a[3]=b[3]-0}else a[2]&&m.error(a[0]);a[0]=e++;return a},ATTR:function(a,b,c,d,e,f){var g=a[1]=a[1].replace(j,"");!f&&o.attrMap[g]&&(a[1]=o.attrMap[g]),a[4]=(a[4]||a[5]||"").replace(j,""),a[2]==="~="&&(a[4]=" "+a[4]+" ");return a},PSEUDO:function(b,c,d,e,f){if(b[1]==="not")if((a.exec(b[3])||"").length>1||/^\w/.test(b[3]))b[3]=m(b[3],null,null,c);else{var g=m.filter(b[3],c,d,!0^f);d||e.push.apply(e,g);return!1}else if(o.match.POS.test(b[0])||o.match.CHILD.test(b[0]))return!0;return b},POS:function(a){a.unshift(!0);return a}},filters:{enabled:function(a){return a.disabled===!1&&a.type!=="hidden"},disabled:function(a){return a.disabled===!0},checked:function(a){return a.checked===!0},selected:function(a){a.parentNode&&a.parentNode.selectedIndex;return a.selected===!0},parent:function(a){return!!a.firstChild},empty:function(a){return!a.firstChild},has:function(a,b,c){return!!m(c[3],a).length},header:function(a){return/h\d/i.test(a.nodeName)},text:function(a){var b=a.getAttribute("type"),c=a.type;return a.nodeName.toLowerCase()==="input"&&"text"===c&&(b===c||b===null)},radio:function(a){return a.nodeName.toLowerCase()==="input"&&"radio"===a.type},checkbox:function(a){return a.nodeName.toLowerCase()==="input"&&"checkbox"===a.type},file:function(a){return a.nodeName.toLowerCase()==="input"&&"file"===a.type},password:function(a){return a.nodeName.toLowerCase()==="input"&&"password"===a.type},submit:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"submit"===a.type},image:function(a){return a.nodeName.toLowerCase()==="input"&&"image"===a.type},reset:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"reset"===a.type},button:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&"button"===a.type||b==="button"},input:function(a){return/input|select|textarea|button/i.test(a.nodeName)},focus:function(a){return a===a.ownerDocument.activeElement}},setFilters:{first:function(a,b){return b===0},last:function(a,b,c,d){return b===d.length-1},even:function(a,b){return b%2===0},odd:function(a,b){return b%2===1},lt:function(a,b,c){return b<c[3]-0},gt:function(a,b,c){return b>c[3]-0},nth:function(a,b,c){return c[3]-0===b},eq:function(a,b,c){return c[3]-0===b}},filter:{PSEUDO:function(a,b,c,d){var e=b[1],f=o.filters[e];if(f)return f(a,c,b,d);if(e==="contains")return(a.textContent||a.innerText||n([a])||"").indexOf(b[3])>=0;if(e==="not"){var g=b[3];for(var h=0,i=g.length;h<i;h++)if(g[h]===a)return!1;return!0}m.error(e)},CHILD:function(a,b){var c,e,f,g,h,i,j,k=b[1],l=a;switch(k){case"only":case"first":while(l=l.previousSibling)if(l.nodeType===1)return!1;if(k==="first")return!0;l=a;case"last":while(l=l.nextSibling)if(l.nodeType===1)return!1;return!0;case"nth":c=b[2],e=b[3];if(c===1&&e===0)return!0;f=b[0],g=a.parentNode;if(g&&(g[d]!==f||!a.nodeIndex)){i=0;for(l=g.firstChild;l;l=l.nextSibling)l.nodeType===1&&(l.nodeIndex=++i);g[d]=f}j=a.nodeIndex-e;return c===0?j===0:j%c===0&&j/c>=0}},ID:function(a,b){return a.nodeType===1&&a.getAttribute("id")===b},TAG:function(a,b){return b==="*"&&a.nodeType===1||!!a.nodeName&&a.nodeName.toLowerCase()===b},CLASS:function(a,b){return(" "+(a.className||a.getAttribute("class"))+" ").indexOf(b)>-1},ATTR:function(a,b){var c=b[1],d=m.attr?m.attr(a,c):o.attrHandle[c]?o.attrHandle[c](a):a[c]!=null?a[c]:a.getAttribute(c),e=d+"",f=b[2],g=b[4];return d==null?f==="!=":!f&&m.attr?d!=null:f==="="?e===g:f==="*="?e.indexOf(g)>=0:f==="~="?(" "+e+" ").indexOf(g)>=0:g?f==="!="?e!==g:f==="^="?e.indexOf(g)===0:f==="$="?e.substr(e.length-g.length)===g:f==="|="?e===g||e.substr(0,g.length+1)===g+"-":!1:e&&d!==!1},POS:function(a,b,c,d){var e=b[2],f=o.setFilters[e];if(f)return f(a,c,b,d)}}},p=o.match.POS,q=function(a,b){return"\\"+(b-0+1)};for(var r in o.match)o.match[r]=new RegExp(o.match[r].source+/(?![^\[]*\])(?![^\(]*\))/.source),o.leftMatch[r]=new RegExp(/(^(?:.|\r|\n)*?)/.source+o.match[r].source.replace(/\\(\d+)/g,q));var s=function(a,b){a=Array.prototype.slice.call(a,0);if(b){b.push.apply(b,a);return b}return a};try{Array.prototype.slice.call(c.documentElement.childNodes,0)[0].nodeType}catch(t){s=function(a,b){var c=0,d=b||[];if(g.call(a)==="[object Array]")Array.prototype.push.apply(d,a);else if(typeof a.length=="number")for(var e=a.length;c<e;c++)d.push(a[c]);else for(;a[c];c++)d.push(a[c]);return d}}var u,v;c.documentElement.compareDocumentPosition?u=function(a,b){if(a===b){h=!0;return 0}if(!a.compareDocumentPosition||!b.compareDocumentPosition)return a.compareDocumentPosition?-1:1;return a.compareDocumentPosition(b)&4?-1:1}:(u=function(a,b){if(a===b){h=!0;return 0}if(a.sourceIndex&&b.sourceIndex)return a.sourceIndex-b.sourceIndex;var c,d,e=[],f=[],g=a.parentNode,i=b.parentNode,j=g;if(g===i)return v(a,b);if(!g)return-1;if(!i)return 1;while(j)e.unshift(j),j=j.parentNode;j=i;while(j)f.unshift(j),j=j.parentNode;c=e.length,d=f.length;for(var k=0;k<c&&k<d;k++)if(e[k]!==f[k])return v(e[k],f[k]);return k===c?v(a,f[k],-1):v(e[k],b,1)},v=function(a,b,c){if(a===b)return c;var d=a.nextSibling;while(d){if(d===b)return-1;d=d.nextSibling}return 1}),function(){var a=c.createElement("div"),d="script"+(new Date).getTime(),e=c.documentElement;a.innerHTML="<a name='"+d+"'/>",e.insertBefore(a,e.firstChild),c.getElementById(d)&&(o.find.ID=function(a,c,d){if(typeof c.getElementById!="undefined"&&!d){var e=c.getElementById(a[1]);return e?e.id===a[1]||typeof e.getAttributeNode!="undefined"&&e.getAttributeNode("id").nodeValue===a[1]?[e]:b:[]}},o.filter.ID=function(a,b){var c=typeof a.getAttributeNode!="undefined"&&a.getAttributeNode("id");return a.nodeType===1&&c&&c.nodeValue===b}),e.removeChild(a),e=a=null}(),function(){var a=c.createElement("div");a.appendChild(c.createComment("")),a.getElementsByTagName("*").length>0&&(o.find.TAG=function(a,b){var c=b.getElementsByTagName(a[1]);if(a[1]==="*"){var d=[];for(var e=0;c[e];e++)c[e].nodeType===1&&d.push(c[e]);c=d}return c}),a.innerHTML="<a href='#'></a>",a.firstChild&&typeof a.firstChild.getAttribute!="undefined"&&a.firstChild.getAttribute("href")!=="#"&&(o.attrHandle.href=function(a){return a.getAttribute("href",2)}),a=null}(),c.querySelectorAll&&function(){var a=m,b=c.createElement("div"),d="__sizzle__";b.innerHTML="<p class='TEST'></p>";if(!b.querySelectorAll||b.querySelectorAll(".TEST").length!==0){m=function(b,e,f,g){e=e||c;if(!g&&!m.isXML(e)){var h=/^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec(b);if(h&&(e.nodeType===1||e.nodeType===9)){if(h[1])return s(e.getElementsByTagName(b),f);if(h[2]&&o.find.CLASS&&e.getElementsByClassName)return s(e.getElementsByClassName(h[2]),f)}if(e.nodeType===9){if(b==="body"&&e.body)return s([e.body],f);if(h&&h[3]){var i=e.getElementById(h[3]);if(!i||!i.parentNode)return s([],f);if(i.id===h[3])return s([i],f)}try{return s(e.querySelectorAll(b),f)}catch(j){}}else if(e.nodeType===1&&e.nodeName.toLowerCase()!=="object"){var k=e,l=e.getAttribute("id"),n=l||d,p=e.parentNode,q=/^\s*[+~]/.test(b);l?n=n.replace(/'/g,"\\$&"):e.setAttribute("id",n),q&&p&&(e=e.parentNode);try{if(!q||p)return s(e.querySelectorAll("[id='"+n+"'] "+b),f)}catch(r){}finally{l||k.removeAttribute("id")}}}return a(b,e,f,g)};for(var e in a)m[e]=a[e];b=null}}(),function(){var a=c.documentElement,b=a.matchesSelector||a.mozMatchesSelector||a.webkitMatchesSelector||a.msMatchesSelector;if(b){var d=!b.call(c.createElement("div"),"div"),e=!1;try{b.call(c.documentElement,"[test!='']:sizzle")}catch(f){e=!0}m.matchesSelector=function(a,c){c=c.replace(/\=\s*([^'"\]]*)\s*\]/g,"='$1']");if(!m.isXML(a))try{if(e||!o.match.PSEUDO.test(c)&&!/!=/.test(c)){var f=b.call(a,c);if(f||!d||a.document&&a.document.nodeType!==11)return f}}catch(g){}return m(c,null,null,[a]).length>0}}}(),function(){var a=c.createElement("div");a.innerHTML="<div class='test e'></div><div class='test'></div>";if(!!a.getElementsByClassName&&a.getElementsByClassName("e").length!==0){a.lastChild.className="e";if(a.getElementsByClassName("e").length===1)return;o.order.splice(1,0,"CLASS"),o.find.CLASS=function(a,b,c){if(typeof b.getElementsByClassName!="undefined"&&!c)return b.getElementsByClassName(a[1])},a=null}}(),c.documentElement.contains?m.contains=function(a,b){return a!==b&&(a.contains?a.contains(b):!0)}:c.documentElement.compareDocumentPosition?m.contains=function(a,b){return!!(a.compareDocumentPosition(b)&16)}:m.contains=function(){return!1},m.isXML=function(a){var b=(a?a.ownerDocument||a:0).documentElement;return b?b.nodeName!=="HTML":!1};var y=function(a,b,c){var d,e=[],f="",g=b.nodeType?[b]:b;while(d=o.match.PSEUDO.exec(a))f+=d[0],a=a.replace(o.match.PSEUDO,"");a=o.relative[a]?a+"*":a;for(var h=0,i=g.length;h<i;h++)m(a,g[h],e,c);return m.filter(f,e)};m.attr=f.attr,m.selectors.attrMap={},f.find=m,f.expr=m.selectors,f.expr[":"]=f.expr.filters,f.unique=m.uniqueSort,f.text=m.getText,f.isXMLDoc=m.isXML,f.contains=m.contains}();var O=/Until$/,P=/^(?:parents|prevUntil|prevAll)/,Q=/,/,R=/^.[^:#\[\.,]*$/,S=Array.prototype.slice,T=f.expr.match.POS,U={children:!0,contents:!0,next:!0,prev:!0};f.fn.extend({find:function(a){var b=this,c,d;if(typeof a!="string")return f(a).filter(function(){for(c=0,d=b.length;c<d;c++)if(f.contains(b[c],this))return!0});var e=this.pushStack("","find",a),g,h,i;for(c=0,d=this.length;c<d;c++){g=e.length,f.find(a,this[c],e);if(c>0)for(h=g;h<e.length;h++)for(i=0;i<g;i++)if(e[i]===e[h]){e.splice(h--,1);break}}return e},has:function(a){var b=f(a);return this.filter(function(){for(var a=0,c=b.length;a<c;a++)if(f.contains(this,b[a]))return!0})},not:function(a){return this.pushStack(W(this,a,!1),"not",a)},filter:function(a){return this.pushStack(W(this,a,!0),"filter",a)},is:function(a){return!!a&&(typeof a=="string"?T.test(a)?f(a,this.context).index(this[0])>=0:f.filter(a,this).length>0:this.filter(a).length>0)},closest:function(a,b){var c=[],d,e,g=this[0];if(f.isArray(a)){var h=1;while(g&&g.ownerDocument&&g!==b){for(d=0;d<a.length;d++)f(g).is(a[d])&&c.push({selector:a[d],elem:g,level:h});g=g.parentNode,h++}return c}var i=T.test(a)||typeof a!="string"?f(a,b||this.context):0;for(d=0,e=this.length;d<e;d++){g=this[d];while(g){if(i?i.index(g)>-1:f.find.matchesSelector(g,a)){c.push(g);break}g=g.parentNode;if(!g||!g.ownerDocument||g===b||g.nodeType===11)break}}c=c.length>1?f.unique(c):c;return this.pushStack(c,"closest",a)},index:function(a){if(!a)return this[0]&&this[0].parentNode?this.prevAll().length:-1;if(typeof a=="string")return f.inArray(this[0],f(a));return f.inArray(a.jquery?a[0]:a,this)},add:function(a,b){var c=typeof a=="string"?f(a,b):f.makeArray(a&&a.nodeType?[a]:a),d=f.merge(this.get(),c);return this.pushStack(V(c[0])||V(d[0])?d:f.unique(d))},andSelf:function(){return this.add(this.prevObject)}}),f.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return f.dir(a,"parentNode")},parentsUntil:function(a,b,c){return f.dir(a,"parentNode",c)},next:function(a){return f.nth(a,2,"nextSibling")},prev:function(a){return f.nth(a,2,"previousSibling")},nextAll:function(a){return f.dir(a,"nextSibling")},prevAll:function(a){return f.dir(a,"previousSibling")},nextUntil:function(a,b,c){return f.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return f.dir(a,"previousSibling",c)},siblings:function(a){return f.sibling(a.parentNode.firstChild,a)},children:function(a){return f.sibling(a.firstChild)},contents:function(a){return f.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:f.makeArray(a.childNodes)}},function(a,b){f.fn[a]=function(c,d){var e=f.map(this,b,c),g=S.call(arguments);O.test(a)||(d=c),d&&typeof d=="string"&&(e=f.filter(d,e)),e=this.length>1&&!U[a]?f.unique(e):e,(this.length>1||Q.test(d))&&P.test(a)&&(e=e.reverse());return this.pushStack(e,a,g.join(","))}}),f.extend({filter:function(a,b,c){c&&(a=":not("+a+")");return b.length===1?f.find.matchesSelector(b[0],a)?[b[0]]:[]:f.find.matches(a,b)},dir:function(a,c,d){var e=[],g=a[c];while(g&&g.nodeType!==9&&(d===b||g.nodeType!==1||!f(g).is(d)))g.nodeType===1&&e.push(g),g=g[c];return e},nth:function(a,b,c,d){b=b||1;var e=0;for(;a;a=a[c])if(a.nodeType===1&&++e===b)break;return a},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var Y="abbr article aside audio canvas datalist details figcaption figure footer header hgroup mark meter nav output progress section summary time video",Z=/ jQuery\d+="(?:\d+|null)"/g,$=/^\s+/,_=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,ba=/<([\w:]+)/,bb=/<tbody/i,bc=/<|&#?\w+;/,bd=/<(?:script|style)/i,be=/<(?:script|object|embed|option|style)/i,bf=new RegExp("<(?:"+Y.replace(" ","|")+")","i"),bg=/checked\s*(?:[^=]|=\s*.checked.)/i,bh=/\/(java|ecma)script/i,bi=/^\s*<!(?:\[CDATA\[|\-\-)/,bj={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],area:[1,"<map>","</map>"],_default:[0,"",""]},bk=X(c);bj.optgroup=bj.option,bj.tbody=bj.tfoot=bj.colgroup=bj.caption=bj.thead,bj.th=bj.td,f.support.htmlSerialize||(bj._default=[1,"div<div>","</div>"]),f.fn.extend({text:function(a){if(f.isFunction(a))return this.each(function(b){var c=f(this);c.text(a.call(this,b,c.text()))});if(typeof a!="object"&&a!==b)return this.empty().append((this[0]&&this[0].ownerDocument||c).createTextNode(a));return f.text(this)},wrapAll:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapAll(a.call(this,b))});if(this[0]){var b=f(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapInner(a.call(this,b))});return this.each(function(){var b=f(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){return this.each(function(){f(this).wrapAll(a)})},unwrap:function(){return this.parent().each(function(){f.nodeName(this,"body")||f(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=f(arguments[0]);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=this.pushStack(this,"after"
+,arguments);a.push.apply(a,f(arguments[0]).toArray());return a}},remove:function(a,b){for(var c=0,d;(d=this[c])!=null;c++)if(!a||f.filter(a,[d]).length)!b&&d.nodeType===1&&(f.cleanData(d.getElementsByTagName("*")),f.cleanData([d])),d.parentNode&&d.parentNode.removeChild(d);return this},empty:function(){for(var a=0,b;(b=this[a])!=null;a++){b.nodeType===1&&f.cleanData(b.getElementsByTagName("*"));while(b.firstChild)b.removeChild(b.firstChild)}return this},clone:function(a,b){a=a==null?!1:a,b=b==null?a:b;return this.map(function(){return f.clone(this,a,b)})},html:function(a){if(a===b)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(Z,""):null;if(typeof a=="string"&&!bd.test(a)&&(f.support.leadingWhitespace||!$.test(a))&&!bj[(ba.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(_,"<$1></$2>");try{for(var c=0,d=this.length;c<d;c++)this[c].nodeType===1&&(f.cleanData(this[c].getElementsByTagName("*")),this[c].innerHTML=a)}catch(e){this.empty().append(a)}}else f.isFunction(a)?this.each(function(b){var c=f(this);c.html(a.call(this,b,c.html()))}):this.empty().append(a);return this},replaceWith:function(a){if(this[0]&&this[0].parentNode){if(f.isFunction(a))return this.each(function(b){var c=f(this),d=c.html();c.replaceWith(a.call(this,b,d))});typeof a!="string"&&(a=f(a).detach());return this.each(function(){var b=this.nextSibling,c=this.parentNode;f(this).remove(),b?f(b).before(a):f(c).append(a)})}return this.length?this.pushStack(f(f.isFunction(a)?a():a),"replaceWith",a):this},detach:function(a){return this.remove(a,!0)},domManip:function(a,c,d){var e,g,h,i,j=a[0],k=[];if(!f.support.checkClone&&arguments.length===3&&typeof j=="string"&&bg.test(j))return this.each(function(){f(this).domManip(a,c,d,!0)});if(f.isFunction(j))return this.each(function(e){var g=f(this);a[0]=j.call(this,e,c?g.html():b),g.domManip(a,c,d)});if(this[0]){i=j&&j.parentNode,f.support.parentNode&&i&&i.nodeType===11&&i.childNodes.length===this.length?e={fragment:i}:e=f.buildFragment(a,this,k),h=e.fragment,h.childNodes.length===1?g=h=h.firstChild:g=h.firstChild;if(g){c=c&&f.nodeName(g,"tr");for(var l=0,m=this.length,n=m-1;l<m;l++)d.call(c?bl(this[l],g):this[l],e.cacheable||m>1&&l<n?f.clone(h,!0,!0):h)}k.length&&f.each(k,br)}return this}}),f.buildFragment=function(a,b,d){var e,g,h,i,j=a[0];b&&b[0]&&(i=b[0].ownerDocument||b[0]),i.createDocumentFragment||(i=c),a.length===1&&typeof j=="string"&&j.length<512&&i===c&&j.charAt(0)==="<"&&!be.test(j)&&(f.support.checkClone||!bg.test(j))&&!f.support.unknownElems&&bf.test(j)&&(g=!0,h=f.fragments[j],h&&h!==1&&(e=h)),e||(e=i.createDocumentFragment(),f.clean(a,i,e,d)),g&&(f.fragments[j]=h?e:1);return{fragment:e,cacheable:g}},f.fragments={},f.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){f.fn[a]=function(c){var d=[],e=f(c),g=this.length===1&&this[0].parentNode;if(g&&g.nodeType===11&&g.childNodes.length===1&&e.length===1){e[b](this[0]);return this}for(var h=0,i=e.length;h<i;h++){var j=(h>0?this.clone(!0):this).get();f(e[h])[b](j),d=d.concat(j)}return this.pushStack(d,a,e.selector)}}),f.extend({clone:function(a,b,c){var d=a.cloneNode(!0),e,g,h;if((!f.support.noCloneEvent||!f.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!f.isXMLDoc(a)){bn(a,d),e=bo(a),g=bo(d);for(h=0;e[h];++h)g[h]&&bn(e[h],g[h])}if(b){bm(a,d);if(c){e=bo(a),g=bo(d);for(h=0;e[h];++h)bm(e[h],g[h])}}e=g=null;return d},clean:function(a,b,d,e){var g;b=b||c,typeof b.createElement=="undefined"&&(b=b.ownerDocument||b[0]&&b[0].ownerDocument||c);var h=[],i;for(var j=0,k;(k=a[j])!=null;j++){typeof k=="number"&&(k+="");if(!k)continue;if(typeof k=="string")if(!bc.test(k))k=b.createTextNode(k);else{k=k.replace(_,"<$1></$2>");var l=(ba.exec(k)||["",""])[1].toLowerCase(),m=bj[l]||bj._default,n=m[0],o=b.createElement("div");b===c?bk.appendChild(o):X(b).appendChild(o),o.innerHTML=m[1]+k+m[2];while(n--)o=o.lastChild;if(!f.support.tbody){var p=bb.test(k),q=l==="table"&&!p?o.firstChild&&o.firstChild.childNodes:m[1]==="<table>"&&!p?o.childNodes:[];for(i=q.length-1;i>=0;--i)f.nodeName(q[i],"tbody")&&!q[i].childNodes.length&&q[i].parentNode.removeChild(q[i])}!f.support.leadingWhitespace&&$.test(k)&&o.insertBefore(b.createTextNode($.exec(k)[0]),o.firstChild),k=o.childNodes}var r;if(!f.support.appendChecked)if(k[0]&&typeof (r=k.length)=="number")for(i=0;i<r;i++)bq(k[i]);else bq(k);k.nodeType?h.push(k):h=f.merge(h,k)}if(d){g=function(a){return!a.type||bh.test(a.type)};for(j=0;h[j];j++)if(e&&f.nodeName(h[j],"script")&&(!h[j].type||h[j].type.toLowerCase()==="text/javascript"))e.push(h[j].parentNode?h[j].parentNode.removeChild(h[j]):h[j]);else{if(h[j].nodeType===1){var s=f.grep(h[j].getElementsByTagName("script"),g);h.splice.apply(h,[j+1,0].concat(s))}d.appendChild(h[j])}}return h},cleanData:function(a){var b,c,d=f.cache,e=f.event.special,g=f.support.deleteExpando;for(var h=0,i;(i=a[h])!=null;h++){if(i.nodeName&&f.noData[i.nodeName.toLowerCase()])continue;c=i[f.expando];if(c){b=d[c];if(b&&b.events){for(var j in b.events)e[j]?f.event.remove(i,j):f.removeEvent(i,j,b.handle);b.handle&&(b.handle.elem=null)}g?delete i[f.expando]:i.removeAttribute&&i.removeAttribute(f.expando),delete d[c]}}}});var bs=/alpha\([^)]*\)/i,bt=/opacity=([^)]*)/,bu=/([A-Z]|^ms)/g,bv=/^-?\d+(?:px)?$/i,bw=/^-?\d/,bx=/^([\-+])=([\-+.\de]+)/,by={position:"absolute",visibility:"hidden",display:"block"},bz=["Left","Right"],bA=["Top","Bottom"],bB,bC,bD;f.fn.css=function(a,c){if(arguments.length===2&&c===b)return this;return f.access(this,a,c,!0,function(a,c,d){return d!==b?f.style(a,c,d):f.css(a,c)})},f.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=bB(a,"opacity","opacity");return c===""?"1":c}return a.style.opacity}}},cssNumber:{fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":f.support.cssFloat?"cssFloat":"styleFloat"},style:function(a,c,d,e){if(!!a&&a.nodeType!==3&&a.nodeType!==8&&!!a.style){var g,h,i=f.camelCase(c),j=a.style,k=f.cssHooks[i];c=f.cssProps[i]||i;if(d===b){if(k&&"get"in k&&(g=k.get(a,!1,e))!==b)return g;return j[c]}h=typeof d,h==="string"&&(g=bx.exec(d))&&(d=+(g[1]+1)*+g[2]+parseFloat(f.css(a,c)),h="number");if(d==null||h==="number"&&isNaN(d))return;h==="number"&&!f.cssNumber[i]&&(d+="px");if(!k||!("set"in k)||(d=k.set(a,d))!==b)try{j[c]=d}catch(l){}}},css:function(a,c,d){var e,g;c=f.camelCase(c),g=f.cssHooks[c],c=f.cssProps[c]||c,c==="cssFloat"&&(c="float");if(g&&"get"in g&&(e=g.get(a,!0,d))!==b)return e;if(bB)return bB(a,c)},swap:function(a,b,c){var d={};for(var e in b)d[e]=a.style[e],a.style[e]=b[e];c.call(a);for(e in b)a.style[e]=d[e]}}),f.curCSS=f.css,f.each(["height","width"],function(a,b){f.cssHooks[b]={get:function(a,c,d){var e;if(c){if(a.offsetWidth!==0)return bE(a,b,d);f.swap(a,by,function(){e=bE(a,b,d)});return e}},set:function(a,b){if(!bv.test(b))return b;b=parseFloat(b);if(b>=0)return b+"px"}}}),f.support.opacity||(f.cssHooks.opacity={get:function(a,b){return bt.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=f.isNumeric(b)?"alpha(opacity="+b*100+")":"",g=d&&d.filter||c.filter||"";c.zoom=1;if(b>=1&&f.trim(g.replace(bs,""))===""){c.removeAttribute("filter");if(d&&!d.filter)return}c.filter=bs.test(g)?g.replace(bs,e):g+" "+e}}),f(function(){f.support.reliableMarginRight||(f.cssHooks.marginRight={get:function(a,b){var c;f.swap(a,{display:"inline-block"},function(){b?c=bB(a,"margin-right","marginRight"):c=a.style.marginRight});return c}})}),c.defaultView&&c.defaultView.getComputedStyle&&(bC=function(a,c){var d,e,g;c=c.replace(bu,"-$1").toLowerCase();if(!(e=a.ownerDocument.defaultView))return b;if(g=e.getComputedStyle(a,null))d=g.getPropertyValue(c),d===""&&!f.contains(a.ownerDocument.documentElement,a)&&(d=f.style(a,c));return d}),c.documentElement.currentStyle&&(bD=function(a,b){var c,d,e,f=a.currentStyle&&a.currentStyle[b],g=a.style;f===null&&g&&(e=g[b])&&(f=e),!bv.test(f)&&bw.test(f)&&(c=g.left,d=a.runtimeStyle&&a.runtimeStyle.left,d&&(a.runtimeStyle.left=a.currentStyle.left),g.left=b==="fontSize"?"1em":f||0,f=g.pixelLeft+"px",g.left=c,d&&(a.runtimeStyle.left=d));return f===""?"auto":f}),bB=bC||bD,f.expr&&f.expr.filters&&(f.expr.filters.hidden=function(a){var b=a.offsetWidth,c=a.offsetHeight;return b===0&&c===0||!f.support.reliableHiddenOffsets&&(a.style&&a.style.display||f.css(a,"display"))==="none"},f.expr.filters.visible=function(a){return!f.expr.filters.hidden(a)});var bF=/%20/g,bG=/\[\]$/,bH=/\r?\n/g,bI=/#.*$/,bJ=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,bK=/^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,bL=/^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,bM=/^(?:GET|HEAD)$/,bN=/^\/\//,bO=/\?/,bP=/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,bQ=/^(?:select|textarea)/i,bR=/\s+/,bS=/([?&])_=[^&]*/,bT=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/,bU=f.fn.load,bV={},bW={},bX,bY,bZ=["*/"]+["*"];try{bX=e.href}catch(b$){bX=c.createElement("a"),bX.href="",bX=bX.href}bY=bT.exec(bX.toLowerCase())||[],f.fn.extend({load:function(a,c,d){if(typeof a!="string"&&bU)return bU.apply(this,arguments);if(!this.length)return this;var e=a.indexOf(" ");if(e>=0){var g=a.slice(e,a.length);a=a.slice(0,e)}var h="GET";c&&(f.isFunction(c)?(d=c,c=b):typeof c=="object"&&(c=f.param(c,f.ajaxSettings.traditional),h="POST"));var i=this;f.ajax({url:a,type:h,dataType:"html",data:c,complete:function(a,b,c){c=a.responseText,a.isResolved()&&(a.done(function(a){c=a}),i.html(g?f("<div>").append(c.replace(bP,"")).find(g):c)),d&&i.each(d,[c,b,a])}});return this},serialize:function(){return f.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?f.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||bQ.test(this.nodeName)||bK.test(this.type))}).map(function(a,b){var c=f(this).val();return c==null?null:f.isArray(c)?f.map(c,function(a,c){return{name:b.name,value:a.replace(bH,"\r\n")}}):{name:b.name,value:c.replace(bH,"\r\n")}}).get()}}),f.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){f.fn[b]=function(a){return this.bind(b,a)}}),f.each(["get","post"],function(a,c){f[c]=function(a,d,e,g){f.isFunction(d)&&(g=g||e,e=d,d=b);return f.ajax({type:c,url:a,data:d,success:e,dataType:g})}}),f.extend({getScript:function(a,c){return f.get(a,b,c,"script")},getJSON:function(a,b,c){return f.get(a,b,c,"json")},ajaxSetup:function(a,b){b?cb(a,f.ajaxSettings):(b=a,a=f.ajaxSettings),cb(a,b);return a},ajaxSettings:{url:bX,isLocal:bL.test(bY[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":bZ},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":a.String,"text html":!0,"text json":f.parseJSON,"text xml":f.parseXML},flatOptions:{context:!0,url:!0}},ajaxPrefilter:b_(bV),ajaxTransport:b_(bW),ajax:function(a,c){function w(a,c,l,m){if(s!==2){s=2,q&&clearTimeout(q),p=b,n=m||"",v.readyState=a>0?4:0;var o,r,u,w=c,x=l?cd(d,v,l):b,y,z;if(a>=200&&a<300||a===304){if(d.ifModified){if(y=v.getResponseHeader("Last-Modified"))f.lastModified[k]=y;if(z=v.getResponseHeader("Etag"))f.etag[k]=z}if(a===304)w="notmodified",o=!0;else try{r=ce(d,x),w="success",o=!0}catch(A){w="parsererror",u=A}}else{u=w;if(!w||a)w="error",a<0&&(a=0)}v.status=a,v.statusText=""+(c||w),o?h.resolveWith(e,[r,w,v]):h.rejectWith(e,[v,w,u]),v.statusCode(j),j=b,t&&g.trigger("ajax"+(o?"Success":"Error"),[v,d,o?r:u]),i.fireWith(e,[v,w]),t&&(g.trigger("ajaxComplete",[v,d]),--f.active||f.event.trigger("ajaxStop"))}}typeof a=="object"&&(c=a,a=b),c=c||{};var d=f.ajaxSetup({},c),e=d.context||d,g=e!==d&&(e.nodeType||e instanceof f)?f(e):f.event,h=f.Deferred(),i=f.Callbacks("once memory"),j=d.statusCode||{},k,l={},m={},n,o,p,q,r,s=0,t,u,v={readyState:0,setRequestHeader:function(a,b){if(!s){var c=a.toLowerCase();a=m[c]=m[c]||a,l[a]=b}return this},getAllResponseHeaders:function(){return s===2?n:null},getResponseHeader:function(a){var c;if(s===2){if(!o){o={};while(c=bJ.exec(n))o[c[1].toLowerCase()]=c[2]}c=o[a.toLowerCase()]}return c===b?null:c},overrideMimeType:function(a){s||(d.mimeType=a);return this},abort:function(a){a=a||"abort",p&&p.abort(a),w(0,a);return this}};h.promise(v),v.success=v.done,v.error=v.fail,v.complete=i.add,v.statusCode=function(a){if(a){var b;if(s<2)for(b in a)j[b]=[j[b],a[b]];else b=a[v.status],v.then(b,b)}return this},d.url=((a||d.url)+"").replace(bI,"").replace(bN,bY[1]+"//"),d.dataTypes=f.trim(d.dataType||"*").toLowerCase().split(bR),d.crossDomain==null&&(r=bT.exec(d.url.toLowerCase()),d.crossDomain=!(!r||r[1]==bY[1]&&r[2]==bY[2]&&(r[3]||(r[1]==="http:"?80:443))==(bY[3]||(bY[1]==="http:"?80:443)))),d.data&&d.processData&&typeof d.data!="string"&&(d.data=f.param(d.data,d.traditional)),ca(bV,d,c,v);if(s===2)return!1;t=d.global,d.type=d.type.toUpperCase(),d.hasContent=!bM.test(d.type),t&&f.active++===0&&f.event.trigger("ajaxStart");if(!d.hasContent){d.data&&(d.url+=(bO.test(d.url)?"&":"?")+d.data,delete d.data),k=d.url;if(d.cache===!1){var x=f.now(),y=d.url.replace(bS,"$1_="+x);d.url=y+(y===d.url?(bO.test(d.url)?"&":"?")+"_="+x:"")}}(d.data&&d.hasContent&&d.contentType!==!1||c.contentType)&&v.setRequestHeader("Content-Type",d.contentType),d.ifModified&&(k=k||d.url,f.lastModified[k]&&v.setRequestHeader("If-Modified-Since",f.lastModified[k]),f.etag[k]&&v.setRequestHeader("If-None-Match",f.etag[k])),v.setRequestHeader("Accept",d.dataTypes[0]&&d.accepts[d.dataTypes[0]]?d.accepts[d.dataTypes[0]]+(d.dataTypes[0]!=="*"?", "+bZ+"; q=0.01":""):d.accepts["*"]);for(u in d.headers)v.setRequestHeader(u,d.headers[u]);if(d.beforeSend&&(d.beforeSend.call(e,v,d)===!1||s===2)){v.abort();return!1}for(u in{success:1,error:1,complete:1})v[u](d[u]);p=ca(bW,d,c,v);if(!p)w(-1,"No Transport");else{v.readyState=1,t&&g.trigger("ajaxSend",[v,d]),d.async&&d.timeout>0&&(q=setTimeout(function(){v.abort("timeout")},d.timeout));try{s=1,p.send(l,w)}catch(z){s<2?w(-1,z):f.error(z)}}return v},param:function(a,c){var d=[],e=function(a,b){b=f.isFunction(b)?b():b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=f.ajaxSettings.traditional);if(f.isArray(a)||a.jquery&&!f.isPlainObject(a))f.each(a,function(){e(this.name,this.value)});else for(var g in a)cc(g,a[g],c,e);return d.join("&").replace(bF,"+")}}),f.extend({active:0,lastModified:{},etag:{}});var cf=f.now(),cg=/(\=)\?(&|$)|\?\?/i;f.ajaxSetup({jsonp:"callback",jsonpCallback:function(){return f.expando+"_"+cf++}}),f.ajaxPrefilter("json jsonp",function(b,c,d){var e=b.contentType==="application/x-www-form-urlencoded"&&typeof b.data=="string";if(b.dataTypes[0]==="jsonp"||b.jsonp!==!1&&(cg.test(b.url)||e&&cg.test(b.data))){var g,h=b.jsonpCallback=f.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,i=a[h],j=b.url,k=b.data,l="$1"+h+"$2";b.jsonp!==!1&&(j=j.replace(cg,l),b.url===j&&(e&&(k=k.replace(cg,l)),b.data===k&&(j+=(/\?/.test(j)?"&":"?")+b.jsonp+"="+h))),b.url=j,b.data=k,a[h]=function(a){g=[a]},d.always(function(){a[h]=i,g&&f.isFunction(i)&&a[h](g[0])}),b.converters["script json"]=function(){g||f.error(h+" was not called");return g[0]},b.dataTypes[0]="json";return"script"}}),f.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(a){f.globalEval(a);return a}}}),f.ajaxPrefilter("script",function(a){a.cache===b&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),f.ajaxTransport("script",function(a){if(a.crossDomain){var d,e=c.head||c.getElementsByTagName("head")[0]||c.documentElement;return{send:function(f,g){d=c.createElement("script"),d.async="async",a.scriptCharset&&(d.charset=a.scriptCharset),d.src=a.url,d.onload=d.onreadystatechange=function(a,c){if(c||!d.readyState||/loaded|complete/.test(d.readyState))d.onload=d.onreadystatechange=null,e&&d.parentNode&&e.removeChild(d),d=b,c||g(200,"success")},e.insertBefore(d,e.firstChild)},abort:function(){d&&d.onload(0,1)}}}});var ch=a.ActiveXObject?function(){for(var a in cj)cj[a](0,1)}:!1,ci=0,cj;f.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&ck()||cl()}:ck,function(a){f.extend(f.support,{ajax:!!a,cors:!!a&&"withCredentials"in a})}(f.ajaxSettings.xhr()),f.support.ajax&&f.ajaxTransport(function(c){if(!c.crossDomain||f.support.cors){var d;return{send:function(e,g){var h=c.xhr(),i,j;c.username?h.open(c.type,c.url,c.async,c.username,c.password):h.open(c.type,c.url,c.async);if(c.xhrFields)for(j in c.xhrFields)h[j]=c.xhrFields[j];c.mimeType&&h.overrideMimeType&&h.overrideMimeType(c.mimeType),!c.crossDomain&&!e["X-Requested-With"]&&(e["X-Requested-With"]="XMLHttpRequest");try{for(j in e)h.setRequestHeader(j,e[j])}catch(k){}h.send(c.hasContent&&c.data||null),d=function(a,e){var j,k,l,m,n;try{if(d&&(e||h.readyState===4)){d=b,i&&(h.onreadystatechange=f.noop,ch&&delete cj[i]);if(e)h.readyState!==4&&h.abort();else{j=h.status,l=h.getAllResponseHeaders(),m={},n=h.responseXML,n&&n.documentElement&&(m.xml=n),m.text=h.responseText;try{k=h.statusText}catch(o){k=""}!j&&c.isLocal&&!c.crossDomain?j=m.text?200:404:j===1223&&(j=204)}}}catch(p){e||g(-1,p)}m&&g(j,k,m,l)},!c.async||h.readyState===4?d():(i=++ci,ch&&(cj||(cj={},f(a).unload(ch)),cj[i]=d),h.onreadystatechange=d)},abort:function(){d&&d(0,1)}}}});var cm={},cn,co,cp=/^(?:toggle|show|hide)$/,cq=/^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i,cr,cs=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]],ct;f.fn.extend({show:function(a,b,c){var d,e;if(a||a===0)return this.animate(cw("show",3),a,b,c);for(var g=0,h=this.length;g<h;g++)d=this[g],d.style&&(e=d.style.display,!f._data(d,"olddisplay")&&e==="none"&&(e=d.style.display=""),e===""&&f.css(d,"display")==="none"&&f._data(d,"olddisplay",cx(d.nodeName)));for(g=0;g<h;g++){d=this[g];if(d.style){e=d.style.display;if(e===""||e==="none")d.style.display=f._data(d,"olddisplay")||""}}return this},hide:function(a,b,c){if(a||a===0)return this.animate(cw("hide",3),a,b,c);var d,e,g=0,h=this.length;for(;g<h;g++)d=this[g],d.style&&(e=f.css(d,"display"),e!=="none"&&!f._data(d,"olddisplay")&&f._data(d,"olddisplay",e));for(g=0;g<h;g++)this[g].style&&(this[g].style.display="none");return this},_toggle:f.fn.toggle,toggle:function(a,b,c){var d=typeof a=="boolean";f.isFunction(a)&&f.isFunction(b)?this._toggle.apply(this,arguments):a==null||d?this.each(function(){var b=d?a:f(this).is(":hidden");f(this)[b?"show":"hide"]()}):this.animate(cw("toggle",3),a,b,c);return this},fadeTo:function(a,b,c,d){return this.filter(":hidden").css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){function g(){e.queue===!1&&f._mark(this);var b=f.extend({},e),c=this.nodeType===1,d=c&&f(this).is(":hidden"),g,h,i,j,k,l,m,n,o;b.animatedProperties={};for(i in a){g=f.camelCase(i),i!==g&&(a[g]=a[i],delete a[i]),h=a[g],f.isArray(h)?(b.animatedProperties[g]=h[1],h=a[g]=h[0]):b.animatedProperties[g]=b.specialEasing&&b.specialEasing[g]||b.easing||"swing";if(h==="hide"&&d||h==="show"&&!d)return b.complete.call(this);c&&(g==="height"||g==="width")&&(b.overflow=[this.style.overflow,this.style.overflowX,this.style.overflowY],f.css(this,"display")==="inline"&&f.css(this,"float")==="none"&&(!f.support.inlineBlockNeedsLayout||cx(this.nodeName)==="inline"?this.style.display="inline-block":this.style.zoom=1))}b.overflow!=null&&(this.style.overflow="hidden");for(i in a)j=new f.fx(this,b,i),h=a[i],cp.test(h)?(o=f._data(this,"toggle"+i)||(h==="toggle"?d?"show":"hide":0),o?(f._data(this,"toggle"+i,o==="show"?"hide":"show"),j[o]()):j[h]()):(k=cq.exec(h),l=j.cur(),k?(m=parseFloat(k[2]),n=k[3]||(f.cssNumber[i]?"":"px"),n!=="px"&&(f.style(this,i,(m||1)+n),l=(m||1)/j.cur()*l,f.style(this,i,l+n)),k[1]&&(m=(k[1]==="-="?-1:1)*m+l),j.custom(l,m,n)):j.custom(l,h,""));return!0}var e=f.speed(b,c,d);if(f.isEmptyObject(a))return this.each(e.complete,[!1]);a=f.extend({},a);return e.queue===!1?this.each(g):this.queue(e.queue,g)},stop:function(a,c,d){typeof a!="string"&&(d=c,c=a,a=b),c&&a!==!1&&this.queue(a||"fx",[]);return this.each(function(){function h(a,b,c){var e=b[c];f.removeData(a,c,!0),e.stop(d)}var b,c=!1,e=f.timers,g=f._data(this);d||f._unmark(!0,this);if(a==null)for(b in g)g[b].stop&&b.indexOf(".run")===b.length-4&&h(this,g,b);else g[b=a+".run"]&&g[b].stop&&h(this,g,b);for(b=e.length;b--;)e[b].elem===this&&(a==null||e[b].queue===a)&&(d?e[b](!0):e[b].saveState(),c=!0,e.splice(b,1));(!d||!c)&&f.dequeue(this,a)})}}),f.each({slideDown:cw("show",1),slideUp:cw("hide",1),slideToggle:cw("toggle",1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){f.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),f.extend({speed:function(a,b,c){var d=a&&typeof a=="object"?f.extend({},a):{complete:c||!c&&b||f.isFunction(a)&&a,duration:a,easing:c&&b||b&&!f.isFunction(b)&&b};d.duration=f.fx.off?0:typeof d.duration=="number"?d.duration:d.duration in f.fx.speeds?f.fx.speeds[d.duration]:f.fx.speeds._default;if(d.queue==null||d.queue===!0)d.queue="fx";d.old=d.complete,d.complete=function(a){f.isFunction(d.old)&&d.old.call(this),d.queue?f.dequeue(this,d.queue):a!==!1&&f._unmark(this)};return d},easing:{linear:function(a,b,c,d){return c+d*a},swing:function(a,b,c,d){return(-Math.cos(a*Math.PI)/2+.5)*d+c}},timers:[],fx:function(a,b,c){this.options=b,this.elem=a,this.prop=c,b.orig=b.orig||{}}}),f.fx.prototype={update:function(){this.options.step&&this.options.step.call(this.elem,this.now,this),(f.fx.step[this.prop]||f.fx.step._default)(this)},cur:function(){if(this.elem[this.prop]!=null&&(!this.elem.style||this.elem.style[this.prop]==null))return this.elem[this.prop];var a,b=f.css(this.elem,this.prop);return isNaN(a=parseFloat(b))?!b||b==="auto"?0:b:a},custom:function(a,c,d){function h(a){return e.step(a)}var e=this,g=f.fx;this.startTime=ct||cu(),this.end=c,this.now=this.start=a,this.pos=this.state=0,this.unit=d||this.unit||(f.cssNumber[this.prop]?"":"px"),h.queue=this.options.queue,h.elem=this.elem,h.saveState=function(){e.options.hide&&f._data(e.elem,"fxshow"+e.prop)===b&&f._data(e.elem,"fxshow"+e.prop,e.start)},h()&&f.timers.push(h)&&!cr&&(cr=setInterval(g.tick,g.interval))},show:function(){var a=f._data(this.elem,"fxshow"+this.prop);this.options.orig[this.prop]=a||f.style(this.elem,this.prop),this.options.show=!0,a!==b?this.custom(this.cur(),a):this.custom(this.prop==="width"||this.prop==="height"?1:0,this.cur()),f(this.elem).show()},hide:function(){this.options.orig[this.prop]=f._data(this.elem,"fxshow"+this.prop)||f.style(this.elem,this.prop),this.options.hide=!0,this.custom(this.cur(),0)},step:function(a){var b,c,d,e=ct||cu(),g=!0,h=this.elem,i=this.options;if(a||e>=i.duration+this.startTime){this.now=this.end,this.pos=this.state=1,this.update(),i.animatedProperties[this.prop]=!0;for(b in i.animatedProperties)i.animatedProperties[b]!==!0&&(g=!1);if(g){i.overflow!=null&&!f.support.shrinkWrapBlocks&&f.each(["","X","Y"],function(a,b){h.style["overflow"+b]=i.overflow[a]}),i.hide&&f(h).hide();if(i.hide||i.show)for(b in i.animatedProperties)f.style(h,b,i.orig[b]),f.removeData(h,"fxshow"+b,!0),f.removeData(h,"toggle"+b,!0);d=i.complete,d&&(i.complete=!1,d.call(h))}return!1}i.duration==Infinity?this.now=e:(c=e-this.startTime,this.state=c/i.duration,this.pos=f.easing[i.animatedProperties[this.prop]](this.state,c,0,1,i.duration),this.now=this.start+(this.end-this.start)*this.pos),this.update();return!0}},f.extend(f.fx,{tick:function(){var a,b=f.timers,c=0;for(;c<b.length;c++)a=b[c],!a()&&b[c]===a&&b.splice(c--,1);b.length||f.fx.stop()},interval:13,stop:function(){clearInterval(cr),cr=null},speeds:{slow:600,fast:200,_default:400},step:{opacity:function(a){f.style(a.elem,"opacity",a.now)},_default:function(a){a.elem.style&&a.elem.style[a.prop]!=null?a.elem.style[a.prop]=a.now+a.unit:a.elem[a.prop]=a.now}}}),f.each(["width","height"],function(a,b){f.fx.step[b]=function(a){f.style(a.elem,b,Math.max(0,a.now))}}),f.expr&&f.expr.filters&&(f.expr.filters.animated=function(a){return f.grep(f.timers,function(b){return a===b.elem}).length});var cy=/^t(?:able|d|h)$/i,cz=/^(?:body|html)$/i;"getBoundingClientRect"in c.documentElement?f.fn.offset=function(a){var b=this[0],c;if(a)return this.each(function(b){f.offset.setOffset(this,a,b)});if(!b||!b.ownerDocument)return null;if(b===b.ownerDocument.body)return f.offset.bodyOffset(b);try{c=b.getBoundingClientRect()}catch(d){}var e=b.ownerDocument,g=e.documentElement;if(!c||!f.contains(g,b))return c?{top:c.top,left:c.left}:{top:0,left:0};var h=e.body,i=cA(e),j=g.clientTop||h.clientTop||0,k=g.clientLeft||h.clientLeft||0,l=i.pageYOffset||f.support.boxModel&&g.scrollTop||h.scrollTop,m=i.pageXOffset||f.support.boxModel&&g.scrollLeft||h.scrollLeft,n=c.top+l-j,o=c.left+m-k;return{top:n,left:o}}:f.fn.offset=function(a){var b=this[0];if(a)return this.each(function(b){f.offset.setOffset(this,a,b)});if(!b||!b.ownerDocument)return null;if(b===b.ownerDocument.body)return f.offset.bodyOffset(b);var c,d=b.offsetParent,e=b,g=b.ownerDocument,h=g.documentElement,i=g.body,j=g.defaultView,k=j?j.getComputedStyle(b,null):b.currentStyle,l=b.offsetTop,m=b.offsetLeft;while((b=b.parentNode)&&b!==i&&b!==h){if(f.support.fixedPosition&&k.position==="fixed")break;c=j?j.getComputedStyle(b,null):b.currentStyle,l-=b.scrollTop,m-=b.scrollLeft,b===d&&(l+=b.offsetTop,m+=b.offsetLeft,f.support.doesNotAddBorder&&(!f.support.doesAddBorderForTableAndCells||!cy.test(b.nodeName))&&(l+=parseFloat(c.borderTopWidth)||0,m+=parseFloat(c.borderLeftWidth)||0),e=d,d=b.offsetParent),f.support.subtractsBorderForOverflowNotVisible&&c.overflow!=="visible"&&(l+=parseFloat(c.borderTopWidth)||0,m+=parseFloat(c.borderLeftWidth)||0),k=c}if(k.position==="relative"||k.position==="static")l+=i.offsetTop,m+=i.offsetLeft;f.support.fixedPosition&&k.position==="fixed"&&(l+=Math.max(h.scrollTop,i.scrollTop),m+=Math.max(h.scrollLeft,i.scrollLeft));return{top:l,left:m}},f.offset={bodyOffset:function(a){var b=a.offsetTop,c=a.offsetLeft;f.support.doesNotIncludeMarginInBodyOffset&&(b+=parseFloat(f.css(a,"marginTop"))||0,c+=parseFloat(f.css(a,"marginLeft"))||0);return{top:b,left:c}},setOffset:function(a,b,c){var d=f.css(a,"position");d==="static"&&(a.style.position="relative");var e=f(a),g=e.offset(),h=f.css(a,"top"),i=f.css(a,"left"),j=(d==="absolute"||d==="fixed")&&f.inArray("auto",[h,i])>-1,k={},l={},m,n;j?(l=e.position(),m=l.top,n=l.left):(m=parseFloat(h)||0,n=parseFloat(i)||0),f.isFunction(b)&&(b=b.call(a,c,g)),b.top!=null&&(k.top=b.top-g.top+m),b.left!=null&&(k.left=b.left-g.left+n),"using"in b?b.using.call(a,k):e.css(k)}},f.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),c=this.offset(),d=cz.test(b[0].nodeName)?{top:0,left:0}:b.offset();c.top-=parseFloat(f.css(a,"marginTop"))||0,c.left-=parseFloat(f.css(a,"marginLeft"))||0,d.top+=parseFloat(f.css(b[0],"borderTopWidth"))||0,d.left+=parseFloat(f.css(b[0],"borderLeftWidth"))||0;return{top:c.top-d.top,left:c.left-d.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||c.body;while(a&&!cz.test(a.nodeName)&&f.css(a,"position")==="static")a=a.offsetParent;return a})}}),f.each(["Left","Top"],function(a,c){var d="scroll"+c;f.fn[d]=function(c){var e,g;if(c===b){e=this[0];if(!e)return null;g=cA(e);return g?"pageXOffset"in g?g[a?"pageYOffset":"pageXOffset"]:f.support.boxModel&&g.document.documentElement[d]||g.document.body[d]:e[d]}return this.each(function(){g=cA(this),g?g.scrollTo(a?f(g).scrollLeft():c,a?c:f(g).scrollTop()):this[d]=c})}}),f.each(["Height","Width"],function(a,c){var d=c.toLowerCase();f.fn["inner"+c]=function(){var a=this[0];return a?a.style?parseFloat(f.css(a,d,"padding")):this[d]():null},f.fn["outer"+c]=function(a){var b=this[0];return b?b.style?parseFloat(f.css(b,d,a?"margin":"border")):this[d]():null},f.fn[d]=function(a){var e=this[0];if(!e)return a==null?null:this;if(f.isFunction(a))return this.each(function(b){var c=f(this);c[d](a.call(this,b,c[d]()))});if(f.isWindow(e)){var g=e.document.documentElement["client"+c],h=e.document.body;return e.document.compatMode==="CSS1Compat"&&g||h&&h["client"+c]||g}if(e.nodeType===9)return Math.max(e.documentElement["client"+c],e.body["scroll"+c],e.documentElement["scroll"+c],e.body["offset"+c],e.documentElement["offset"+c]);if(a===b){var i=f.css(e,d),j=parseFloat(i);return f.isNumeric(j)?j:i}return this.css(d,typeof a=="string"?a:a+"px")}}),a.jQuery=a.$=f})(window); \ No newline at end of file
diff --git a/devtools/client/inspector/markup/test/lib_jquery_2.1.1_min.js b/devtools/client/inspector/markup/test/lib_jquery_2.1.1_min.js
new file mode 100644
index 000000000..e5ace116b
--- /dev/null
+++ b/devtools/client/inspector/markup/test/lib_jquery_2.1.1_min.js
@@ -0,0 +1,4 @@
+/*! jQuery v2.1.1 | (c) 2005, 2014 jQuery Foundation, Inc. | jquery.org/license */
+!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=c.slice,e=c.concat,f=c.push,g=c.indexOf,h={},i=h.toString,j=h.hasOwnProperty,k={},l=a.document,m="2.1.1",n=function(a,b){return new n.fn.init(a,b)},o=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,p=/^-ms-/,q=/-([\da-z])/gi,r=function(a,b){return b.toUpperCase()};n.fn=n.prototype={jquery:m,constructor:n,selector:"",length:0,toArray:function(){return d.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:d.call(this)},pushStack:function(a){var b=n.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return n.each(this,a,b)},map:function(a){return this.pushStack(n.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:c.sort,splice:c.splice},n.extend=n.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||n.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(a=arguments[h]))for(b in a)c=g[b],d=a[b],g!==d&&(j&&d&&(n.isPlainObject(d)||(e=n.isArray(d)))?(e?(e=!1,f=c&&n.isArray(c)?c:[]):f=c&&n.isPlainObject(c)?c:{},g[b]=n.extend(j,f,d)):void 0!==d&&(g[b]=d));return g},n.extend({expando:"jQuery"+(m+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===n.type(a)},isArray:Array.isArray,isWindow:function(a){return null!=a&&a===a.window},isNumeric:function(a){return!n.isArray(a)&&a-parseFloat(a)>=0},isPlainObject:function(a){return"object"!==n.type(a)||a.nodeType||n.isWindow(a)?!1:a.constructor&&!j.call(a.constructor.prototype,"isPrototypeOf")?!1:!0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(a){var b,c=eval;a=n.trim(a),a&&(1===a.indexOf("use strict")?(b=l.createElement("script"),b.text=a,l.head.appendChild(b).parentNode.removeChild(b)):c(a))},camelCase:function(a){return a.replace(p,"ms-").replace(q,r)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b,c){var d,e=0,f=a.length,g=s(a);if(c){if(g){for(;f>e;e++)if(d=b.apply(a[e],c),d===!1)break}else for(e in a)if(d=b.apply(a[e],c),d===!1)break}else if(g){for(;f>e;e++)if(d=b.call(a[e],e,a[e]),d===!1)break}else for(e in a)if(d=b.call(a[e],e,a[e]),d===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(o,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(s(Object(a))?n.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){return null==b?-1:g.call(b,a,c)},merge:function(a,b){for(var c=+b.length,d=0,e=a.length;c>d;d++)a[e++]=b[d];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,f=0,g=a.length,h=s(a),i=[];if(h)for(;g>f;f++)d=b(a[f],f,c),null!=d&&i.push(d);else for(f in a)d=b(a[f],f,c),null!=d&&i.push(d);return e.apply([],i)},guid:1,proxy:function(a,b){var c,e,f;return"string"==typeof b&&(c=a[b],b=a,a=c),n.isFunction(a)?(e=d.call(arguments,2),f=function(){return a.apply(b||this,e.concat(d.call(arguments)))},f.guid=a.guid=a.guid||n.guid++,f):void 0},now:Date.now,support:k}),n.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function s(a){var b=a.length,c=n.type(a);return"function"===c||n.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var t=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+-new Date,v=a.document,w=0,x=0,y=gb(),z=gb(),A=gb(),B=function(a,b){return a===b&&(l=!0),0},C="undefined",D=1<<31,E={}.hasOwnProperty,F=[],G=F.pop,H=F.push,I=F.push,J=F.slice,K=F.indexOf||function(a){for(var b=0,c=this.length;c>b;b++)if(this[b]===a)return b;return-1},L="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",M="[\\x20\\t\\r\\n\\f]",N="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",O=N.replace("w","w#"),P="\\["+M+"*("+N+")(?:"+M+"*([*^$|!~]?=)"+M+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+O+"))|)"+M+"*\\]",Q=":("+N+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+P+")*)|.*)\\)|)",R=new RegExp("^"+M+"+|((?:^|[^\\\\])(?:\\\\.)*)"+M+"+$","g"),S=new RegExp("^"+M+"*,"+M+"*"),T=new RegExp("^"+M+"*([>+~]|"+M+")"+M+"*"),U=new RegExp("="+M+"*([^\\]'\"]*?)"+M+"*\\]","g"),V=new RegExp(Q),W=new RegExp("^"+O+"$"),X={ID:new RegExp("^#("+N+")"),CLASS:new RegExp("^\\.("+N+")"),TAG:new RegExp("^("+N.replace("w","w*")+")"),ATTR:new RegExp("^"+P),PSEUDO:new RegExp("^"+Q),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+L+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/^(?:input|select|textarea|button)$/i,Z=/^h\d$/i,$=/^[^{]+\{\s*\[native \w/,_=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ab=/[+~]/,bb=/'|\\/g,cb=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),db=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)};try{I.apply(F=J.call(v.childNodes),v.childNodes),F[v.childNodes.length].nodeType}catch(eb){I={apply:F.length?function(a,b){H.apply(a,J.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function fb(a,b,d,e){var f,h,j,k,l,o,r,s,w,x;if((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,d=d||[],!a||"string"!=typeof a)return d;if(1!==(k=b.nodeType)&&9!==k)return[];if(p&&!e){if(f=_.exec(a))if(j=f[1]){if(9===k){if(h=b.getElementById(j),!h||!h.parentNode)return d;if(h.id===j)return d.push(h),d}else if(b.ownerDocument&&(h=b.ownerDocument.getElementById(j))&&t(b,h)&&h.id===j)return d.push(h),d}else{if(f[2])return I.apply(d,b.getElementsByTagName(a)),d;if((j=f[3])&&c.getElementsByClassName&&b.getElementsByClassName)return I.apply(d,b.getElementsByClassName(j)),d}if(c.qsa&&(!q||!q.test(a))){if(s=r=u,w=b,x=9===k&&a,1===k&&"object"!==b.nodeName.toLowerCase()){o=g(a),(r=b.getAttribute("id"))?s=r.replace(bb,"\\$&"):b.setAttribute("id",s),s="[id='"+s+"'] ",l=o.length;while(l--)o[l]=s+qb(o[l]);w=ab.test(a)&&ob(b.parentNode)||b,x=o.join(",")}if(x)try{return I.apply(d,w.querySelectorAll(x)),d}catch(y){}finally{r||b.removeAttribute("id")}}}return i(a.replace(R,"$1"),b,d,e)}function gb(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function hb(a){return a[u]=!0,a}function ib(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function jb(a,b){var c=a.split("|"),e=a.length;while(e--)d.attrHandle[c[e]]=b}function kb(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||D)-(~a.sourceIndex||D);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function lb(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function mb(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function nb(a){return hb(function(b){return b=+b,hb(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function ob(a){return a&&typeof a.getElementsByTagName!==C&&a}c=fb.support={},f=fb.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=fb.setDocument=function(a){var b,e=a?a.ownerDocument||a:v,g=e.defaultView;return e!==n&&9===e.nodeType&&e.documentElement?(n=e,o=e.documentElement,p=!f(e),g&&g!==g.top&&(g.addEventListener?g.addEventListener("unload",function(){m()},!1):g.attachEvent&&g.attachEvent("onunload",function(){m()})),c.attributes=ib(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ib(function(a){return a.appendChild(e.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=$.test(e.getElementsByClassName)&&ib(function(a){return a.innerHTML="<div class='a'></div><div class='a i'></div>",a.firstChild.className="i",2===a.getElementsByClassName("i").length}),c.getById=ib(function(a){return o.appendChild(a).id=u,!e.getElementsByName||!e.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if(typeof b.getElementById!==C&&p){var c=b.getElementById(a);return c&&c.parentNode?[c]:[]}},d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){var c=typeof a.getAttributeNode!==C&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return typeof b.getElementsByTagName!==C?b.getElementsByTagName(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return typeof b.getElementsByClassName!==C&&p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=$.test(e.querySelectorAll))&&(ib(function(a){a.innerHTML="<select msallowclip=''><option selected=''></option></select>",a.querySelectorAll("[msallowclip^='']").length&&q.push("[*^$]="+M+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+M+"*(?:value|"+L+")"),a.querySelectorAll(":checked").length||q.push(":checked")}),ib(function(a){var b=e.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+M+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=$.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ib(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",Q)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=$.test(o.compareDocumentPosition),t=b||$.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===e||a.ownerDocument===v&&t(v,a)?-1:b===e||b.ownerDocument===v&&t(v,b)?1:k?K.call(k,a)-K.call(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,f=a.parentNode,g=b.parentNode,h=[a],i=[b];if(!f||!g)return a===e?-1:b===e?1:f?-1:g?1:k?K.call(k,a)-K.call(k,b):0;if(f===g)return kb(a,b);c=a;while(c=c.parentNode)h.unshift(c);c=b;while(c=c.parentNode)i.unshift(c);while(h[d]===i[d])d++;return d?kb(h[d],i[d]):h[d]===v?-1:i[d]===v?1:0},e):n},fb.matches=function(a,b){return fb(a,null,null,b)},fb.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(U,"='$1']"),!(!c.matchesSelector||!p||r&&r.test(b)||q&&q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return fb(b,n,null,[a]).length>0},fb.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},fb.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&E.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},fb.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},fb.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=fb.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=fb.selectors={cacheLength:50,createPseudo:hb,match:X,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(cb,db),a[3]=(a[3]||a[4]||a[5]||"").replace(cb,db),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||fb.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&fb.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return X.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&V.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(cb,db).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+M+")"+a+"("+M+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||typeof a.getAttribute!==C&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=fb.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h;if(q){if(f){while(p){l=b;while(l=l[p])if(h?l.nodeName.toLowerCase()===r:1===l.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){k=q[u]||(q[u]={}),j=k[a]||[],n=j[0]===w&&j[1],m=j[0]===w&&j[2],l=n&&q.childNodes[n];while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if(1===l.nodeType&&++m&&l===b){k[a]=[w,n,m];break}}else if(s&&(j=(b[u]||(b[u]={}))[a])&&j[0]===w)m=j[1];else while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if((h?l.nodeName.toLowerCase()===r:1===l.nodeType)&&++m&&(s&&((l[u]||(l[u]={}))[a]=[w,m]),l===b))break;return m-=e,m===d||m%d===0&&m/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||fb.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?hb(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=K.call(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:hb(function(a){var b=[],c=[],d=h(a.replace(R,"$1"));return d[u]?hb(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),!c.pop()}}),has:hb(function(a){return function(b){return fb(a,b).length>0}}),contains:hb(function(a){return function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:hb(function(a){return W.test(a||"")||fb.error("unsupported lang: "+a),a=a.replace(cb,db).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Z.test(a.nodeName)},input:function(a){return Y.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:nb(function(){return[0]}),last:nb(function(a,b){return[b-1]}),eq:nb(function(a,b,c){return[0>c?c+b:c]}),even:nb(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:nb(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:nb(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:nb(function(a,b,c){for(var d=0>c?c+b:c;++d<b;)a.push(d);return a})}},d.pseudos.nth=d.pseudos.eq;for(b in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})d.pseudos[b]=lb(b);for(b in{submit:!0,reset:!0})d.pseudos[b]=mb(b);function pb(){}pb.prototype=d.filters=d.pseudos,d.setFilters=new pb,g=fb.tokenize=function(a,b){var c,e,f,g,h,i,j,k=z[a+" "];if(k)return b?0:k.slice(0);h=a,i=[],j=d.preFilter;while(h){(!c||(e=S.exec(h)))&&(e&&(h=h.slice(e[0].length)||h),i.push(f=[])),c=!1,(e=T.exec(h))&&(c=e.shift(),f.push({value:c,type:e[0].replace(R," ")}),h=h.slice(c.length));for(g in d.filter)!(e=X[g].exec(h))||j[g]&&!(e=j[g](e))||(c=e.shift(),f.push({value:c,type:g,matches:e}),h=h.slice(c.length));if(!c)break}return b?h.length:h?fb.error(a):z(a,i).slice(0)};function qb(a){for(var b=0,c=a.length,d="";c>b;b++)d+=a[b].value;return d}function rb(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(i=b[u]||(b[u]={}),(h=i[d])&&h[0]===w&&h[1]===f)return j[2]=h[2];if(i[d]=j,j[2]=a(b,c,g))return!0}}}function sb(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function tb(a,b,c){for(var d=0,e=b.length;e>d;d++)fb(a,b[d],c);return c}function ub(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(!c||c(f,d,e))&&(g.push(f),j&&b.push(h));return g}function vb(a,b,c,d,e,f){return d&&!d[u]&&(d=vb(d)),e&&!e[u]&&(e=vb(e,f)),hb(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||tb(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:ub(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=ub(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?K.call(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=ub(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):I.apply(g,r)})}function wb(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=rb(function(a){return a===b},h,!0),l=rb(function(a){return K.call(b,a)>-1},h,!0),m=[function(a,c,d){return!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d))}];f>i;i++)if(c=d.relative[a[i].type])m=[rb(sb(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return vb(i>1&&sb(m),i>1&&qb(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(R,"$1"),c,e>i&&wb(a.slice(i,e)),f>e&&wb(a=a.slice(e)),f>e&&qb(a))}m.push(c)}return sb(m)}function xb(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,m,o,p=0,q="0",r=f&&[],s=[],t=j,u=f||e&&d.find.TAG("*",k),v=w+=null==t?1:Math.random()||.1,x=u.length;for(k&&(j=g!==n&&g);q!==x&&null!=(l=u[q]);q++){if(e&&l){m=0;while(o=a[m++])if(o(l,g,h)){i.push(l);break}k&&(w=v)}c&&((l=!o&&l)&&p--,f&&r.push(l))}if(p+=q,c&&q!==p){m=0;while(o=b[m++])o(r,s,g,h);if(f){if(p>0)while(q--)r[q]||s[q]||(s[q]=G.call(i));s=ub(s)}I.apply(i,s),k&&!f&&s.length>0&&p+b.length>1&&fb.uniqueSort(i)}return k&&(w=v,j=t),r};return c?hb(f):f}return h=fb.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=wb(b[c]),f[u]?d.push(f):e.push(f);f=A(a,xb(e,d)),f.selector=a}return f},i=fb.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(cb,db),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=X.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(cb,db),ab.test(j[0].type)&&ob(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&qb(j),!a)return I.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,ab.test(a)&&ob(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ib(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ib(function(a){return a.innerHTML="<a href='#'></a>","#"===a.firstChild.getAttribute("href")})||jb("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ib(function(a){return a.innerHTML="<input/>",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||jb("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ib(function(a){return null==a.getAttribute("disabled")})||jb(L,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),fb}(a);n.find=t,n.expr=t.selectors,n.expr[":"]=n.expr.pseudos,n.unique=t.uniqueSort,n.text=t.getText,n.isXMLDoc=t.isXML,n.contains=t.contains;var u=n.expr.match.needsContext,v=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,w=/^.[^:#\[\.,]*$/;function x(a,b,c){if(n.isFunction(b))return n.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return n.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(w.test(b))return n.filter(b,a,c);b=n.filter(b,a)}return n.grep(a,function(a){return g.call(b,a)>=0!==c})}n.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?n.find.matchesSelector(d,a)?[d]:[]:n.find.matches(a,n.grep(b,function(a){return 1===a.nodeType}))},n.fn.extend({find:function(a){var b,c=this.length,d=[],e=this;if("string"!=typeof a)return this.pushStack(n(a).filter(function(){for(b=0;c>b;b++)if(n.contains(e[b],this))return!0}));for(b=0;c>b;b++)n.find(a,e[b],d);return d=this.pushStack(c>1?n.unique(d):d),d.selector=this.selector?this.selector+" "+a:a,d},filter:function(a){return this.pushStack(x(this,a||[],!1))},not:function(a){return this.pushStack(x(this,a||[],!0))},is:function(a){return!!x(this,"string"==typeof a&&u.test(a)?n(a):a||[],!1).length}});var y,z=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,A=n.fn.init=function(a,b){var c,d;if(!a)return this;if("string"==typeof a){if(c="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:z.exec(a),!c||!c[1]&&b)return!b||b.jquery?(b||y).find(a):this.constructor(b).find(a);if(c[1]){if(b=b instanceof n?b[0]:b,n.merge(this,n.parseHTML(c[1],b&&b.nodeType?b.ownerDocument||b:l,!0)),v.test(c[1])&&n.isPlainObject(b))for(c in b)n.isFunction(this[c])?this[c](b[c]):this.attr(c,b[c]);return this}return d=l.getElementById(c[2]),d&&d.parentNode&&(this.length=1,this[0]=d),this.context=l,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):n.isFunction(a)?"undefined"!=typeof y.ready?y.ready(a):a(n):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),n.makeArray(a,this))};A.prototype=n.fn,y=n(l);var B=/^(?:parents|prev(?:Until|All))/,C={children:!0,contents:!0,next:!0,prev:!0};n.extend({dir:function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&n(a).is(c))break;d.push(a)}return d},sibling:function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c}}),n.fn.extend({has:function(a){var b=n(a,this),c=b.length;return this.filter(function(){for(var a=0;c>a;a++)if(n.contains(this,b[a]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=u.test(a)||"string"!=typeof a?n(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&n.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?n.unique(f):f)},index:function(a){return a?"string"==typeof a?g.call(n(a),this[0]):g.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(n.unique(n.merge(this.get(),n(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function D(a,b){while((a=a[b])&&1!==a.nodeType);return a}n.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return n.dir(a,"parentNode")},parentsUntil:function(a,b,c){return n.dir(a,"parentNode",c)},next:function(a){return D(a,"nextSibling")},prev:function(a){return D(a,"previousSibling")},nextAll:function(a){return n.dir(a,"nextSibling")},prevAll:function(a){return n.dir(a,"previousSibling")},nextUntil:function(a,b,c){return n.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return n.dir(a,"previousSibling",c)},siblings:function(a){return n.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return n.sibling(a.firstChild)},contents:function(a){return a.contentDocument||n.merge([],a.childNodes)}},function(a,b){n.fn[a]=function(c,d){var e=n.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=n.filter(d,e)),this.length>1&&(C[a]||n.unique(e),B.test(a)&&e.reverse()),this.pushStack(e)}});var E=/\S+/g,F={};function G(a){var b=F[a]={};return n.each(a.match(E)||[],function(a,c){b[c]=!0}),b}n.Callbacks=function(a){a="string"==typeof a?F[a]||G(a):n.extend({},a);var b,c,d,e,f,g,h=[],i=!a.once&&[],j=function(l){for(b=a.memory&&l,c=!0,g=e||0,e=0,f=h.length,d=!0;h&&f>g;g++)if(h[g].apply(l[0],l[1])===!1&&a.stopOnFalse){b=!1;break}d=!1,h&&(i?i.length&&j(i.shift()):b?h=[]:k.disable())},k={add:function(){if(h){var c=h.length;!function g(b){n.each(b,function(b,c){var d=n.type(c);"function"===d?a.unique&&k.has(c)||h.push(c):c&&c.length&&"string"!==d&&g(c)})}(arguments),d?f=h.length:b&&(e=c,j(b))}return this},remove:function(){return h&&n.each(arguments,function(a,b){var c;while((c=n.inArray(b,h,c))>-1)h.splice(c,1),d&&(f>=c&&f--,g>=c&&g--)}),this},has:function(a){return a?n.inArray(a,h)>-1:!(!h||!h.length)},empty:function(){return h=[],f=0,this},disable:function(){return h=i=b=void 0,this},disabled:function(){return!h},lock:function(){return i=void 0,b||k.disable(),this},locked:function(){return!i},fireWith:function(a,b){return!h||c&&!i||(b=b||[],b=[a,b.slice?b.slice():b],d?i.push(b):j(b)),this},fire:function(){return k.fireWith(this,arguments),this},fired:function(){return!!c}};return k},n.extend({Deferred:function(a){var b=[["resolve","done",n.Callbacks("once memory"),"resolved"],["reject","fail",n.Callbacks("once memory"),"rejected"],["notify","progress",n.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return n.Deferred(function(c){n.each(b,function(b,f){var g=n.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&n.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?n.extend(a,d):d}},e={};return d.pipe=d.then,n.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=d.call(arguments),e=c.length,f=1!==e||a&&n.isFunction(a.promise)?e:0,g=1===f?a:n.Deferred(),h=function(a,b,c){return function(e){b[a]=this,c[a]=arguments.length>1?d.call(arguments):e,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(e>1)for(i=new Array(e),j=new Array(e),k=new Array(e);e>b;b++)c[b]&&n.isFunction(c[b].promise)?c[b].promise().done(h(b,k,c)).fail(g.reject).progress(h(b,j,i)):--f;return f||g.resolveWith(k,c),g.promise()}});var H;n.fn.ready=function(a){return n.ready.promise().done(a),this},n.extend({isReady:!1,readyWait:1,holdReady:function(a){a?n.readyWait++:n.ready(!0)},ready:function(a){(a===!0?--n.readyWait:n.isReady)||(n.isReady=!0,a!==!0&&--n.readyWait>0||(H.resolveWith(l,[n]),n.fn.triggerHandler&&(n(l).triggerHandler("ready"),n(l).off("ready"))))}});function I(){l.removeEventListener("DOMContentLoaded",I,!1),a.removeEventListener("load",I,!1),n.ready()}n.ready.promise=function(b){return H||(H=n.Deferred(),"complete"===l.readyState?setTimeout(n.ready):(l.addEventListener("DOMContentLoaded",I,!1),a.addEventListener("load",I,!1))),H.promise(b)},n.ready.promise();var J=n.access=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===n.type(c)){e=!0;for(h in c)n.access(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,n.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(n(a),c)})),b))for(;i>h;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f};n.acceptData=function(a){return 1===a.nodeType||9===a.nodeType||!+a.nodeType};function K(){Object.defineProperty(this.cache={},0,{get:function(){return{}}}),this.expando=n.expando+Math.random()}K.uid=1,K.accepts=n.acceptData,K.prototype={key:function(a){if(!K.accepts(a))return 0;var b={},c=a[this.expando];if(!c){c=K.uid++;try{b[this.expando]={value:c},Object.defineProperties(a,b)}catch(d){b[this.expando]=c,n.extend(a,b)}}return this.cache[c]||(this.cache[c]={}),c},set:function(a,b,c){var d,e=this.key(a),f=this.cache[e];if("string"==typeof b)f[b]=c;else if(n.isEmptyObject(f))n.extend(this.cache[e],b);else for(d in b)f[d]=b[d];return f},get:function(a,b){var c=this.cache[this.key(a)];return void 0===b?c:c[b]},access:function(a,b,c){var d;return void 0===b||b&&"string"==typeof b&&void 0===c?(d=this.get(a,b),void 0!==d?d:this.get(a,n.camelCase(b))):(this.set(a,b,c),void 0!==c?c:b)},remove:function(a,b){var c,d,e,f=this.key(a),g=this.cache[f];if(void 0===b)this.cache[f]={};else{n.isArray(b)?d=b.concat(b.map(n.camelCase)):(e=n.camelCase(b),b in g?d=[b,e]:(d=e,d=d in g?[d]:d.match(E)||[])),c=d.length;while(c--)delete g[d[c]]}},hasData:function(a){return!n.isEmptyObject(this.cache[a[this.expando]]||{})},discard:function(a){a[this.expando]&&delete this.cache[a[this.expando]]}};var L=new K,M=new K,N=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,O=/([A-Z])/g;function P(a,b,c){var d;if(void 0===c&&1===a.nodeType)if(d="data-"+b.replace(O,"-$1").toLowerCase(),c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:N.test(c)?n.parseJSON(c):c}catch(e){}M.set(a,b,c)}else c=void 0;return c}n.extend({hasData:function(a){return M.hasData(a)||L.hasData(a)},data:function(a,b,c){return M.access(a,b,c)},removeData:function(a,b){M.remove(a,b)
+},_data:function(a,b,c){return L.access(a,b,c)},_removeData:function(a,b){L.remove(a,b)}}),n.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=M.get(f),1===f.nodeType&&!L.get(f,"hasDataAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=n.camelCase(d.slice(5)),P(f,d,e[d])));L.set(f,"hasDataAttrs",!0)}return e}return"object"==typeof a?this.each(function(){M.set(this,a)}):J(this,function(b){var c,d=n.camelCase(a);if(f&&void 0===b){if(c=M.get(f,a),void 0!==c)return c;if(c=M.get(f,d),void 0!==c)return c;if(c=P(f,d,void 0),void 0!==c)return c}else this.each(function(){var c=M.get(this,d);M.set(this,d,b),-1!==a.indexOf("-")&&void 0!==c&&M.set(this,a,b)})},null,b,arguments.length>1,null,!0)},removeData:function(a){return this.each(function(){M.remove(this,a)})}}),n.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=L.get(a,b),c&&(!d||n.isArray(c)?d=L.access(a,b,n.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=n.queue(a,b),d=c.length,e=c.shift(),f=n._queueHooks(a,b),g=function(){n.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return L.get(a,c)||L.access(a,c,{empty:n.Callbacks("once memory").add(function(){L.remove(a,[b+"queue",c])})})}}),n.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length<c?n.queue(this[0],a):void 0===b?this:this.each(function(){var c=n.queue(this,a,b);n._queueHooks(this,a),"fx"===a&&"inprogress"!==c[0]&&n.dequeue(this,a)})},dequeue:function(a){return this.each(function(){n.dequeue(this,a)})},clearQueue:function(a){return this.queue(a||"fx",[])},promise:function(a,b){var c,d=1,e=n.Deferred(),f=this,g=this.length,h=function(){--d||e.resolveWith(f,[f])};"string"!=typeof a&&(b=a,a=void 0),a=a||"fx";while(g--)c=L.get(f[g],a+"queueHooks"),c&&c.empty&&(d++,c.empty.add(h));return h(),e.promise(b)}});var Q=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,R=["Top","Right","Bottom","Left"],S=function(a,b){return a=b||a,"none"===n.css(a,"display")||!n.contains(a.ownerDocument,a)},T=/^(?:checkbox|radio)$/i;!function(){var a=l.createDocumentFragment(),b=a.appendChild(l.createElement("div")),c=l.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),k.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="<textarea>x</textarea>",k.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var U="undefined";k.focusinBubbles="onfocusin"in a;var V=/^key/,W=/^(?:mouse|pointer|contextmenu)|click/,X=/^(?:focusinfocus|focusoutblur)$/,Y=/^([^.]*)(?:\.(.+)|)$/;function Z(){return!0}function $(){return!1}function _(){try{return l.activeElement}catch(a){}}n.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=L.get(a);if(r){c.handler&&(f=c,c=f.handler,e=f.selector),c.guid||(c.guid=n.guid++),(i=r.events)||(i=r.events={}),(g=r.handle)||(g=r.handle=function(b){return typeof n!==U&&n.event.triggered!==b.type?n.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(E)||[""],j=b.length;while(j--)h=Y.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o&&(l=n.event.special[o]||{},o=(e?l.delegateType:l.bindType)||o,l=n.event.special[o]||{},k=n.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&n.expr.match.needsContext.test(e),namespace:p.join(".")},f),(m=i[o])||(m=i[o]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,p,g)!==!1||a.addEventListener&&a.addEventListener(o,g,!1)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),n.event.global[o]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=L.hasData(a)&&L.get(a);if(r&&(i=r.events)){b=(b||"").match(E)||[""],j=b.length;while(j--)if(h=Y.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=n.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,m=i[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&q!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||n.removeEvent(a,o,r.handle),delete i[o])}else for(o in i)n.event.remove(a,o+b[j],c,d,!0);n.isEmptyObject(i)&&(delete r.handle,L.remove(a,"events"))}},trigger:function(b,c,d,e){var f,g,h,i,k,m,o,p=[d||l],q=j.call(b,"type")?b.type:b,r=j.call(b,"namespace")?b.namespace.split("."):[];if(g=h=d=d||l,3!==d.nodeType&&8!==d.nodeType&&!X.test(q+n.event.triggered)&&(q.indexOf(".")>=0&&(r=q.split("."),q=r.shift(),r.sort()),k=q.indexOf(":")<0&&"on"+q,b=b[n.expando]?b:new n.Event(q,"object"==typeof b&&b),b.isTrigger=e?2:3,b.namespace=r.join("."),b.namespace_re=b.namespace?new RegExp("(^|\\.)"+r.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=d),c=null==c?[b]:n.makeArray(c,[b]),o=n.event.special[q]||{},e||!o.trigger||o.trigger.apply(d,c)!==!1)){if(!e&&!o.noBubble&&!n.isWindow(d)){for(i=o.delegateType||q,X.test(i+q)||(g=g.parentNode);g;g=g.parentNode)p.push(g),h=g;h===(d.ownerDocument||l)&&p.push(h.defaultView||h.parentWindow||a)}f=0;while((g=p[f++])&&!b.isPropagationStopped())b.type=f>1?i:o.bindType||q,m=(L.get(g,"events")||{})[b.type]&&L.get(g,"handle"),m&&m.apply(g,c),m=k&&g[k],m&&m.apply&&n.acceptData(g)&&(b.result=m.apply(g,c),b.result===!1&&b.preventDefault());return b.type=q,e||b.isDefaultPrevented()||o._default&&o._default.apply(p.pop(),c)!==!1||!n.acceptData(d)||k&&n.isFunction(d[q])&&!n.isWindow(d)&&(h=d[k],h&&(d[k]=null),n.event.triggered=q,d[q](),n.event.triggered=void 0,h&&(d[k]=h)),b.result}},dispatch:function(a){a=n.event.fix(a);var b,c,e,f,g,h=[],i=d.call(arguments),j=(L.get(this,"events")||{})[a.type]||[],k=n.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=n.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,c=0;while((g=f.handlers[c++])&&!a.isImmediatePropagationStopped())(!a.namespace_re||a.namespace_re.test(g.namespace))&&(a.handleObj=g,a.data=g.data,e=((n.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==e&&(a.result=e)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&(!a.button||"click"!==a.type))for(;i!==this;i=i.parentNode||this)if(i.disabled!==!0||"click"!==a.type){for(d=[],c=0;h>c;c++)f=b[c],e=f.selector+" ",void 0===d[e]&&(d[e]=f.needsContext?n(e,this).index(i)>=0:n.find(e,this,null,[i]).length),d[e]&&d.push(f);d.length&&g.push({elem:i,handlers:d})}return h<b.length&&g.push({elem:this,handlers:b.slice(h)}),g},props:"altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(a,b){return null==a.which&&(a.which=null!=b.charCode?b.charCode:b.keyCode),a}},mouseHooks:{props:"button buttons clientX clientY offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(a,b){var c,d,e,f=b.button;return null==a.pageX&&null!=b.clientX&&(c=a.target.ownerDocument||l,d=c.documentElement,e=c.body,a.pageX=b.clientX+(d&&d.scrollLeft||e&&e.scrollLeft||0)-(d&&d.clientLeft||e&&e.clientLeft||0),a.pageY=b.clientY+(d&&d.scrollTop||e&&e.scrollTop||0)-(d&&d.clientTop||e&&e.clientTop||0)),a.which||void 0===f||(a.which=1&f?1:2&f?3:4&f?2:0),a}},fix:function(a){if(a[n.expando])return a;var b,c,d,e=a.type,f=a,g=this.fixHooks[e];g||(this.fixHooks[e]=g=W.test(e)?this.mouseHooks:V.test(e)?this.keyHooks:{}),d=g.props?this.props.concat(g.props):this.props,a=new n.Event(f),b=d.length;while(b--)c=d[b],a[c]=f[c];return a.target||(a.target=l),3===a.target.nodeType&&(a.target=a.target.parentNode),g.filter?g.filter(a,f):a},special:{load:{noBubble:!0},focus:{trigger:function(){return this!==_()&&this.focus?(this.focus(),!1):void 0},delegateType:"focusin"},blur:{trigger:function(){return this===_()&&this.blur?(this.blur(),!1):void 0},delegateType:"focusout"},click:{trigger:function(){return"checkbox"===this.type&&this.click&&n.nodeName(this,"input")?(this.click(),!1):void 0},_default:function(a){return n.nodeName(a.target,"a")}},beforeunload:{postDispatch:function(a){void 0!==a.result&&a.originalEvent&&(a.originalEvent.returnValue=a.result)}}},simulate:function(a,b,c,d){var e=n.extend(new n.Event,c,{type:a,isSimulated:!0,originalEvent:{}});d?n.event.trigger(e,null,b):n.event.dispatch.call(b,e),e.isDefaultPrevented()&&c.preventDefault()}},n.removeEvent=function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c,!1)},n.Event=function(a,b){return this instanceof n.Event?(a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||void 0===a.defaultPrevented&&a.returnValue===!1?Z:$):this.type=a,b&&n.extend(this,b),this.timeStamp=a&&a.timeStamp||n.now(),void(this[n.expando]=!0)):new n.Event(a,b)},n.Event.prototype={isDefaultPrevented:$,isPropagationStopped:$,isImmediatePropagationStopped:$,preventDefault:function(){var a=this.originalEvent;this.isDefaultPrevented=Z,a&&a.preventDefault&&a.preventDefault()},stopPropagation:function(){var a=this.originalEvent;this.isPropagationStopped=Z,a&&a.stopPropagation&&a.stopPropagation()},stopImmediatePropagation:function(){var a=this.originalEvent;this.isImmediatePropagationStopped=Z,a&&a.stopImmediatePropagation&&a.stopImmediatePropagation(),this.stopPropagation()}},n.each({mouseenter:"mouseover",mouseleave:"mouseout",pointerenter:"pointerover",pointerleave:"pointerout"},function(a,b){n.event.special[a]={delegateType:b,bindType:b,handle:function(a){var c,d=this,e=a.relatedTarget,f=a.handleObj;return(!e||e!==d&&!n.contains(d,e))&&(a.type=f.origType,c=f.handler.apply(this,arguments),a.type=b),c}}}),k.focusinBubbles||n.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){n.event.simulate(b,a.target,n.event.fix(a),!0)};n.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=L.access(d,b);e||d.addEventListener(a,c,!0),L.access(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=L.access(d,b)-1;e?L.access(d,b,e):(d.removeEventListener(a,c,!0),L.remove(d,b))}}}),n.fn.extend({on:function(a,b,c,d,e){var f,g;if("object"==typeof a){"string"!=typeof b&&(c=c||b,b=void 0);for(g in a)this.on(g,b,c,a[g],e);return this}if(null==c&&null==d?(d=b,c=b=void 0):null==d&&("string"==typeof b?(d=c,c=void 0):(d=c,c=b,b=void 0)),d===!1)d=$;else if(!d)return this;return 1===e&&(f=d,d=function(a){return n().off(a),f.apply(this,arguments)},d.guid=f.guid||(f.guid=n.guid++)),this.each(function(){n.event.add(this,a,d,c,b)})},one:function(a,b,c,d){return this.on(a,b,c,d,1)},off:function(a,b,c){var d,e;if(a&&a.preventDefault&&a.handleObj)return d=a.handleObj,n(a.delegateTarget).off(d.namespace?d.origType+"."+d.namespace:d.origType,d.selector,d.handler),this;if("object"==typeof a){for(e in a)this.off(e,b,a[e]);return this}return(b===!1||"function"==typeof b)&&(c=b,b=void 0),c===!1&&(c=$),this.each(function(){n.event.remove(this,a,c,b)})},trigger:function(a,b){return this.each(function(){n.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];return c?n.event.trigger(a,b,c,!0):void 0}});var ab=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,bb=/<([\w:]+)/,cb=/<|&#?\w+;/,db=/<(?:script|style|link)/i,eb=/checked\s*(?:[^=]|=\s*.checked.)/i,fb=/^$|\/(?:java|ecma)script/i,gb=/^true\/(.*)/,hb=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g,ib={option:[1,"<select multiple='multiple'>","</select>"],thead:[1,"<table>","</table>"],col:[2,"<table><colgroup>","</colgroup></table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:[0,"",""]};ib.optgroup=ib.option,ib.tbody=ib.tfoot=ib.colgroup=ib.caption=ib.thead,ib.th=ib.td;function jb(a,b){return n.nodeName(a,"table")&&n.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function kb(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function lb(a){var b=gb.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function mb(a,b){for(var c=0,d=a.length;d>c;c++)L.set(a[c],"globalEval",!b||L.get(b[c],"globalEval"))}function nb(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(L.hasData(a)&&(f=L.access(a),g=L.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;d>c;c++)n.event.add(b,e,j[e][c])}M.hasData(a)&&(h=M.access(a),i=n.extend({},h),M.set(b,i))}}function ob(a,b){var c=a.getElementsByTagName?a.getElementsByTagName(b||"*"):a.querySelectorAll?a.querySelectorAll(b||"*"):[];return void 0===b||b&&n.nodeName(a,b)?n.merge([a],c):c}function pb(a,b){var c=b.nodeName.toLowerCase();"input"===c&&T.test(a.type)?b.checked=a.checked:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}n.extend({clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=n.contains(a.ownerDocument,a);if(!(k.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||n.isXMLDoc(a)))for(g=ob(h),f=ob(a),d=0,e=f.length;e>d;d++)pb(f[d],g[d]);if(b)if(c)for(f=f||ob(a),g=g||ob(h),d=0,e=f.length;e>d;d++)nb(f[d],g[d]);else nb(a,h);return g=ob(h,"script"),g.length>0&&mb(g,!i&&ob(a,"script")),h},buildFragment:function(a,b,c,d){for(var e,f,g,h,i,j,k=b.createDocumentFragment(),l=[],m=0,o=a.length;o>m;m++)if(e=a[m],e||0===e)if("object"===n.type(e))n.merge(l,e.nodeType?[e]:e);else if(cb.test(e)){f=f||k.appendChild(b.createElement("div")),g=(bb.exec(e)||["",""])[1].toLowerCase(),h=ib[g]||ib._default,f.innerHTML=h[1]+e.replace(ab,"<$1></$2>")+h[2],j=h[0];while(j--)f=f.lastChild;n.merge(l,f.childNodes),f=k.firstChild,f.textContent=""}else l.push(b.createTextNode(e));k.textContent="",m=0;while(e=l[m++])if((!d||-1===n.inArray(e,d))&&(i=n.contains(e.ownerDocument,e),f=ob(k.appendChild(e),"script"),i&&mb(f),c)){j=0;while(e=f[j++])fb.test(e.type||"")&&c.push(e)}return k},cleanData:function(a){for(var b,c,d,e,f=n.event.special,g=0;void 0!==(c=a[g]);g++){if(n.acceptData(c)&&(e=c[L.expando],e&&(b=L.cache[e]))){if(b.events)for(d in b.events)f[d]?n.event.remove(c,d):n.removeEvent(c,d,b.handle);L.cache[e]&&delete L.cache[e]}delete M.cache[c[M.expando]]}}}),n.fn.extend({text:function(a){return J(this,function(a){return void 0===a?n.text(this):this.empty().each(function(){(1===this.nodeType||11===this.nodeType||9===this.nodeType)&&(this.textContent=a)})},null,a,arguments.length)},append:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=jb(this,a);b.appendChild(a)}})},prepend:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=jb(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},remove:function(a,b){for(var c,d=a?n.filter(a,this):this,e=0;null!=(c=d[e]);e++)b||1!==c.nodeType||n.cleanData(ob(c)),c.parentNode&&(b&&n.contains(c.ownerDocument,c)&&mb(ob(c,"script")),c.parentNode.removeChild(c));return this},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(n.cleanData(ob(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return n.clone(this,a,b)})},html:function(a){return J(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!db.test(a)&&!ib[(bb.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(ab,"<$1></$2>");try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(n.cleanData(ob(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=arguments[0];return this.domManip(arguments,function(b){a=this.parentNode,n.cleanData(ob(this)),a&&a.replaceChild(b,this)}),a&&(a.length||a.nodeType)?this:this.remove()},detach:function(a){return this.remove(a,!0)},domManip:function(a,b){a=e.apply([],a);var c,d,f,g,h,i,j=0,l=this.length,m=this,o=l-1,p=a[0],q=n.isFunction(p);if(q||l>1&&"string"==typeof p&&!k.checkClone&&eb.test(p))return this.each(function(c){var d=m.eq(c);q&&(a[0]=p.call(this,c,d.html())),d.domManip(a,b)});if(l&&(c=n.buildFragment(a,this[0].ownerDocument,!1,this),d=c.firstChild,1===c.childNodes.length&&(c=d),d)){for(f=n.map(ob(c,"script"),kb),g=f.length;l>j;j++)h=c,j!==o&&(h=n.clone(h,!0,!0),g&&n.merge(f,ob(h,"script"))),b.call(this[j],h,j);if(g)for(i=f[f.length-1].ownerDocument,n.map(f,lb),j=0;g>j;j++)h=f[j],fb.test(h.type||"")&&!L.access(h,"globalEval")&&n.contains(i,h)&&(h.src?n._evalUrl&&n._evalUrl(h.src):n.globalEval(h.textContent.replace(hb,"")))}return this}}),n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){n.fn[a]=function(a){for(var c,d=[],e=n(a),g=e.length-1,h=0;g>=h;h++)c=h===g?this:this.clone(!0),n(e[h])[b](c),f.apply(d,c.get());return this.pushStack(d)}});var qb,rb={};function sb(b,c){var d,e=n(c.createElement(b)).appendTo(c.body),f=a.getDefaultComputedStyle&&(d=a.getDefaultComputedStyle(e[0]))?d.display:n.css(e[0],"display");return e.detach(),f}function tb(a){var b=l,c=rb[a];return c||(c=sb(a,b),"none"!==c&&c||(qb=(qb||n("<iframe frameborder='0' width='0' height='0'/>")).appendTo(b.documentElement),b=qb[0].contentDocument,b.write(),b.close(),c=sb(a,b),qb.detach()),rb[a]=c),c}var ub=/^margin/,vb=new RegExp("^("+Q+")(?!px)[a-z%]+$","i"),wb=function(a){return a.ownerDocument.defaultView.getComputedStyle(a,null)};function xb(a,b,c){var d,e,f,g,h=a.style;return c=c||wb(a),c&&(g=c.getPropertyValue(b)||c[b]),c&&(""!==g||n.contains(a.ownerDocument,a)||(g=n.style(a,b)),vb.test(g)&&ub.test(b)&&(d=h.width,e=h.minWidth,f=h.maxWidth,h.minWidth=h.maxWidth=h.width=g,g=c.width,h.width=d,h.minWidth=e,h.maxWidth=f)),void 0!==g?g+"":g}function yb(a,b){return{get:function(){return a()?void delete this.get:(this.get=b).apply(this,arguments)}}}!function(){var b,c,d=l.documentElement,e=l.createElement("div"),f=l.createElement("div");if(f.style){f.style.backgroundClip="content-box",f.cloneNode(!0).style.backgroundClip="",k.clearCloneStyle="content-box"===f.style.backgroundClip,e.style.cssText="border:0;width:0;height:0;top:0;left:-9999px;margin-top:1px;position:absolute",e.appendChild(f);function g(){f.style.cssText="-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;display:block;margin-top:1%;top:1%;border:1px;padding:1px;width:4px;position:absolute",f.innerHTML="",d.appendChild(e);var g=a.getComputedStyle(f,null);b="1%"!==g.top,c="4px"===g.width,d.removeChild(e)}a.getComputedStyle&&n.extend(k,{pixelPosition:function(){return g(),b},boxSizingReliable:function(){return null==c&&g(),c},reliableMarginRight:function(){var b,c=f.appendChild(l.createElement("div"));return c.style.cssText=f.style.cssText="-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;display:block;margin:0;border:0;padding:0",c.style.marginRight=c.style.width="0",f.style.width="1px",d.appendChild(e),b=!parseFloat(a.getComputedStyle(c,null).marginRight),d.removeChild(e),b}})}}(),n.swap=function(a,b,c,d){var e,f,g={};for(f in b)g[f]=a.style[f],a.style[f]=b[f];e=c.apply(a,d||[]);for(f in b)a.style[f]=g[f];return e};var zb=/^(none|table(?!-c[ea]).+)/,Ab=new RegExp("^("+Q+")(.*)$","i"),Bb=new RegExp("^([+-])=("+Q+")","i"),Cb={position:"absolute",visibility:"hidden",display:"block"},Db={letterSpacing:"0",fontWeight:"400"},Eb=["Webkit","O","Moz","ms"];function Fb(a,b){if(b in a)return b;var c=b[0].toUpperCase()+b.slice(1),d=b,e=Eb.length;while(e--)if(b=Eb[e]+c,b in a)return b;return d}function Gb(a,b,c){var d=Ab.exec(b);return d?Math.max(0,d[1]-(c||0))+(d[2]||"px"):b}function Hb(a,b,c,d,e){for(var f=c===(d?"border":"content")?4:"width"===b?1:0,g=0;4>f;f+=2)"margin"===c&&(g+=n.css(a,c+R[f],!0,e)),d?("content"===c&&(g-=n.css(a,"padding"+R[f],!0,e)),"margin"!==c&&(g-=n.css(a,"border"+R[f]+"Width",!0,e))):(g+=n.css(a,"padding"+R[f],!0,e),"padding"!==c&&(g+=n.css(a,"border"+R[f]+"Width",!0,e)));return g}function Ib(a,b,c){var d=!0,e="width"===b?a.offsetWidth:a.offsetHeight,f=wb(a),g="border-box"===n.css(a,"boxSizing",!1,f);if(0>=e||null==e){if(e=xb(a,b,f),(0>e||null==e)&&(e=a.style[b]),vb.test(e))return e;d=g&&(k.boxSizingReliable()||e===a.style[b]),e=parseFloat(e)||0}return e+Hb(a,b,c||(g?"border":"content"),d,f)+"px"}function Jb(a,b){for(var c,d,e,f=[],g=0,h=a.length;h>g;g++)d=a[g],d.style&&(f[g]=L.get(d,"olddisplay"),c=d.style.display,b?(f[g]||"none"!==c||(d.style.display=""),""===d.style.display&&S(d)&&(f[g]=L.access(d,"olddisplay",tb(d.nodeName)))):(e=S(d),"none"===c&&e||L.set(d,"olddisplay",e?c:n.css(d,"display"))));for(g=0;h>g;g++)d=a[g],d.style&&(b&&"none"!==d.style.display&&""!==d.style.display||(d.style.display=b?f[g]||"":"none"));return a}n.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=xb(a,"opacity");return""===c?"1":c}}}},cssNumber:{columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":"cssFloat"},style:function(a,b,c,d){if(a&&3!==a.nodeType&&8!==a.nodeType&&a.style){var e,f,g,h=n.camelCase(b),i=a.style;return b=n.cssProps[h]||(n.cssProps[h]=Fb(i,h)),g=n.cssHooks[b]||n.cssHooks[h],void 0===c?g&&"get"in g&&void 0!==(e=g.get(a,!1,d))?e:i[b]:(f=typeof c,"string"===f&&(e=Bb.exec(c))&&(c=(e[1]+1)*e[2]+parseFloat(n.css(a,b)),f="number"),null!=c&&c===c&&("number"!==f||n.cssNumber[h]||(c+="px"),k.clearCloneStyle||""!==c||0!==b.indexOf("background")||(i[b]="inherit"),g&&"set"in g&&void 0===(c=g.set(a,c,d))||(i[b]=c)),void 0)}},css:function(a,b,c,d){var e,f,g,h=n.camelCase(b);return b=n.cssProps[h]||(n.cssProps[h]=Fb(a.style,h)),g=n.cssHooks[b]||n.cssHooks[h],g&&"get"in g&&(e=g.get(a,!0,c)),void 0===e&&(e=xb(a,b,d)),"normal"===e&&b in Db&&(e=Db[b]),""===c||c?(f=parseFloat(e),c===!0||n.isNumeric(f)?f||0:e):e}}),n.each(["height","width"],function(a,b){n.cssHooks[b]={get:function(a,c,d){return c?zb.test(n.css(a,"display"))&&0===a.offsetWidth?n.swap(a,Cb,function(){return Ib(a,b,d)}):Ib(a,b,d):void 0},set:function(a,c,d){var e=d&&wb(a);return Gb(a,c,d?Hb(a,b,d,"border-box"===n.css(a,"boxSizing",!1,e),e):0)}}}),n.cssHooks.marginRight=yb(k.reliableMarginRight,function(a,b){return b?n.swap(a,{display:"inline-block"},xb,[a,"marginRight"]):void 0}),n.each({margin:"",padding:"",border:"Width"},function(a,b){n.cssHooks[a+b]={expand:function(c){for(var d=0,e={},f="string"==typeof c?c.split(" "):[c];4>d;d++)e[a+R[d]+b]=f[d]||f[d-2]||f[0];return e}},ub.test(a)||(n.cssHooks[a+b].set=Gb)}),n.fn.extend({css:function(a,b){return J(this,function(a,b,c){var d,e,f={},g=0;if(n.isArray(b)){for(d=wb(a),e=b.length;e>g;g++)f[b[g]]=n.css(a,b[g],!1,d);return f}return void 0!==c?n.style(a,b,c):n.css(a,b)},a,b,arguments.length>1)},show:function(){return Jb(this,!0)},hide:function(){return Jb(this)},toggle:function(a){return"boolean"==typeof a?a?this.show():this.hide():this.each(function(){S(this)?n(this).show():n(this).hide()})}});function Kb(a,b,c,d,e){return new Kb.prototype.init(a,b,c,d,e)}n.Tween=Kb,Kb.prototype={constructor:Kb,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||"swing",this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(n.cssNumber[c]?"":"px")},cur:function(){var a=Kb.propHooks[this.prop];return a&&a.get?a.get(this):Kb.propHooks._default.get(this)},run:function(a){var b,c=Kb.propHooks[this.prop];return this.pos=b=this.options.duration?n.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):Kb.propHooks._default.set(this),this}},Kb.prototype.init.prototype=Kb.prototype,Kb.propHooks={_default:{get:function(a){var b;return null==a.elem[a.prop]||a.elem.style&&null!=a.elem.style[a.prop]?(b=n.css(a.elem,a.prop,""),b&&"auto"!==b?b:0):a.elem[a.prop]},set:function(a){n.fx.step[a.prop]?n.fx.step[a.prop](a):a.elem.style&&(null!=a.elem.style[n.cssProps[a.prop]]||n.cssHooks[a.prop])?n.style(a.elem,a.prop,a.now+a.unit):a.elem[a.prop]=a.now}}},Kb.propHooks.scrollTop=Kb.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},n.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2}},n.fx=Kb.prototype.init,n.fx.step={};var Lb,Mb,Nb=/^(?:toggle|show|hide)$/,Ob=new RegExp("^(?:([+-])=|)("+Q+")([a-z%]*)$","i"),Pb=/queueHooks$/,Qb=[Vb],Rb={"*":[function(a,b){var c=this.createTween(a,b),d=c.cur(),e=Ob.exec(b),f=e&&e[3]||(n.cssNumber[a]?"":"px"),g=(n.cssNumber[a]||"px"!==f&&+d)&&Ob.exec(n.css(c.elem,a)),h=1,i=20;if(g&&g[3]!==f){f=f||g[3],e=e||[],g=+d||1;do h=h||".5",g/=h,n.style(c.elem,a,g+f);while(h!==(h=c.cur()/d)&&1!==h&&--i)}return e&&(g=c.start=+g||+d||0,c.unit=f,c.end=e[1]?g+(e[1]+1)*e[2]:+e[2]),c}]};function Sb(){return setTimeout(function(){Lb=void 0}),Lb=n.now()}function Tb(a,b){var c,d=0,e={height:a};for(b=b?1:0;4>d;d+=2-b)c=R[d],e["margin"+c]=e["padding"+c]=a;return b&&(e.opacity=e.width=a),e}function Ub(a,b,c){for(var d,e=(Rb[b]||[]).concat(Rb["*"]),f=0,g=e.length;g>f;f++)if(d=e[f].call(c,b,a))return d}function Vb(a,b,c){var d,e,f,g,h,i,j,k,l=this,m={},o=a.style,p=a.nodeType&&S(a),q=L.get(a,"fxshow");c.queue||(h=n._queueHooks(a,"fx"),null==h.unqueued&&(h.unqueued=0,i=h.empty.fire,h.empty.fire=function(){h.unqueued||i()}),h.unqueued++,l.always(function(){l.always(function(){h.unqueued--,n.queue(a,"fx").length||h.empty.fire()})})),1===a.nodeType&&("height"in b||"width"in b)&&(c.overflow=[o.overflow,o.overflowX,o.overflowY],j=n.css(a,"display"),k="none"===j?L.get(a,"olddisplay")||tb(a.nodeName):j,"inline"===k&&"none"===n.css(a,"float")&&(o.display="inline-block")),c.overflow&&(o.overflow="hidden",l.always(function(){o.overflow=c.overflow[0],o.overflowX=c.overflow[1],o.overflowY=c.overflow[2]}));for(d in b)if(e=b[d],Nb.exec(e)){if(delete b[d],f=f||"toggle"===e,e===(p?"hide":"show")){if("show"!==e||!q||void 0===q[d])continue;p=!0}m[d]=q&&q[d]||n.style(a,d)}else j=void 0;if(n.isEmptyObject(m))"inline"===("none"===j?tb(a.nodeName):j)&&(o.display=j);else{q?"hidden"in q&&(p=q.hidden):q=L.access(a,"fxshow",{}),f&&(q.hidden=!p),p?n(a).show():l.done(function(){n(a).hide()}),l.done(function(){var b;L.remove(a,"fxshow");for(b in m)n.style(a,b,m[b])});for(d in m)g=Ub(p?q[d]:0,d,l),d in q||(q[d]=g.start,p&&(g.end=g.start,g.start="width"===d||"height"===d?1:0))}}function Wb(a,b){var c,d,e,f,g;for(c in a)if(d=n.camelCase(c),e=b[d],f=a[c],n.isArray(f)&&(e=f[1],f=a[c]=f[0]),c!==d&&(a[d]=f,delete a[c]),g=n.cssHooks[d],g&&"expand"in g){f=g.expand(f),delete a[d];for(c in f)c in a||(a[c]=f[c],b[c]=e)}else b[d]=e}function Xb(a,b,c){var d,e,f=0,g=Qb.length,h=n.Deferred().always(function(){delete i.elem}),i=function(){if(e)return!1;for(var b=Lb||Sb(),c=Math.max(0,j.startTime+j.duration-b),d=c/j.duration||0,f=1-d,g=0,i=j.tweens.length;i>g;g++)j.tweens[g].run(f);return h.notifyWith(a,[j,f,c]),1>f&&i?c:(h.resolveWith(a,[j]),!1)},j=h.promise({elem:a,props:n.extend({},b),opts:n.extend(!0,{specialEasing:{}},c),originalProperties:b,originalOptions:c,startTime:Lb||Sb(),duration:c.duration,tweens:[],createTween:function(b,c){var d=n.Tween(a,j.opts,b,c,j.opts.specialEasing[b]||j.opts.easing);return j.tweens.push(d),d},stop:function(b){var c=0,d=b?j.tweens.length:0;if(e)return this;for(e=!0;d>c;c++)j.tweens[c].run(1);return b?h.resolveWith(a,[j,b]):h.rejectWith(a,[j,b]),this}}),k=j.props;for(Wb(k,j.opts.specialEasing);g>f;f++)if(d=Qb[f].call(j,a,k,j.opts))return d;return n.map(k,Ub,j),n.isFunction(j.opts.start)&&j.opts.start.call(a,j),n.fx.timer(n.extend(i,{elem:a,anim:j,queue:j.opts.queue})),j.progress(j.opts.progress).done(j.opts.done,j.opts.complete).fail(j.opts.fail).always(j.opts.always)}n.Animation=n.extend(Xb,{tweener:function(a,b){n.isFunction(a)?(b=a,a=["*"]):a=a.split(" ");for(var c,d=0,e=a.length;e>d;d++)c=a[d],Rb[c]=Rb[c]||[],Rb[c].unshift(b)},prefilter:function(a,b){b?Qb.unshift(a):Qb.push(a)}}),n.speed=function(a,b,c){var d=a&&"object"==typeof a?n.extend({},a):{complete:c||!c&&b||n.isFunction(a)&&a,duration:a,easing:c&&b||b&&!n.isFunction(b)&&b};return d.duration=n.fx.off?0:"number"==typeof d.duration?d.duration:d.duration in n.fx.speeds?n.fx.speeds[d.duration]:n.fx.speeds._default,(null==d.queue||d.queue===!0)&&(d.queue="fx"),d.old=d.complete,d.complete=function(){n.isFunction(d.old)&&d.old.call(this),d.queue&&n.dequeue(this,d.queue)},d},n.fn.extend({fadeTo:function(a,b,c,d){return this.filter(S).css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){var e=n.isEmptyObject(a),f=n.speed(b,c,d),g=function(){var b=Xb(this,n.extend({},a),f);(e||L.get(this,"finish"))&&b.stop(!0)};return g.finish=g,e||f.queue===!1?this.each(g):this.queue(f.queue,g)},stop:function(a,b,c){var d=function(a){var b=a.stop;delete a.stop,b(c)};return"string"!=typeof a&&(c=b,b=a,a=void 0),b&&a!==!1&&this.queue(a||"fx",[]),this.each(function(){var b=!0,e=null!=a&&a+"queueHooks",f=n.timers,g=L.get(this);if(e)g[e]&&g[e].stop&&d(g[e]);else for(e in g)g[e]&&g[e].stop&&Pb.test(e)&&d(g[e]);for(e=f.length;e--;)f[e].elem!==this||null!=a&&f[e].queue!==a||(f[e].anim.stop(c),b=!1,f.splice(e,1));(b||!c)&&n.dequeue(this,a)})},finish:function(a){return a!==!1&&(a=a||"fx"),this.each(function(){var b,c=L.get(this),d=c[a+"queue"],e=c[a+"queueHooks"],f=n.timers,g=d?d.length:0;for(c.finish=!0,n.queue(this,a,[]),e&&e.stop&&e.stop.call(this,!0),b=f.length;b--;)f[b].elem===this&&f[b].queue===a&&(f[b].anim.stop(!0),f.splice(b,1));for(b=0;g>b;b++)d[b]&&d[b].finish&&d[b].finish.call(this);delete c.finish})}}),n.each(["toggle","show","hide"],function(a,b){var c=n.fn[b];n.fn[b]=function(a,d,e){return null==a||"boolean"==typeof a?c.apply(this,arguments):this.animate(Tb(b,!0),a,d,e)}}),n.each({slideDown:Tb("show"),slideUp:Tb("hide"),slideToggle:Tb("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){n.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),n.timers=[],n.fx.tick=function(){var a,b=0,c=n.timers;for(Lb=n.now();b<c.length;b++)a=c[b],a()||c[b]!==a||c.splice(b--,1);c.length||n.fx.stop(),Lb=void 0},n.fx.timer=function(a){n.timers.push(a),a()?n.fx.start():n.timers.pop()},n.fx.interval=13,n.fx.start=function(){Mb||(Mb=setInterval(n.fx.tick,n.fx.interval))},n.fx.stop=function(){clearInterval(Mb),Mb=null},n.fx.speeds={slow:600,fast:200,_default:400},n.fn.delay=function(a,b){return a=n.fx?n.fx.speeds[a]||a:a,b=b||"fx",this.queue(b,function(b,c){var d=setTimeout(b,a);c.stop=function(){clearTimeout(d)}})},function(){var a=l.createElement("input"),b=l.createElement("select"),c=b.appendChild(l.createElement("option"));a.type="checkbox",k.checkOn=""!==a.value,k.optSelected=c.selected,b.disabled=!0,k.optDisabled=!c.disabled,a=l.createElement("input"),a.value="t",a.type="radio",k.radioValue="t"===a.value}();var Yb,Zb,$b=n.expr.attrHandle;n.fn.extend({attr:function(a,b){return J(this,n.attr,a,b,arguments.length>1)},removeAttr:function(a){return this.each(function(){n.removeAttr(this,a)})}}),n.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(a&&3!==f&&8!==f&&2!==f)return typeof a.getAttribute===U?n.prop(a,b,c):(1===f&&n.isXMLDoc(a)||(b=b.toLowerCase(),d=n.attrHooks[b]||(n.expr.match.bool.test(b)?Zb:Yb)),void 0===c?d&&"get"in d&&null!==(e=d.get(a,b))?e:(e=n.find.attr(a,b),null==e?void 0:e):null!==c?d&&"set"in d&&void 0!==(e=d.set(a,c,b))?e:(a.setAttribute(b,c+""),c):void n.removeAttr(a,b))
+},removeAttr:function(a,b){var c,d,e=0,f=b&&b.match(E);if(f&&1===a.nodeType)while(c=f[e++])d=n.propFix[c]||c,n.expr.match.bool.test(c)&&(a[d]=!1),a.removeAttribute(c)},attrHooks:{type:{set:function(a,b){if(!k.radioValue&&"radio"===b&&n.nodeName(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}}}),Zb={set:function(a,b,c){return b===!1?n.removeAttr(a,c):a.setAttribute(c,c),c}},n.each(n.expr.match.bool.source.match(/\w+/g),function(a,b){var c=$b[b]||n.find.attr;$b[b]=function(a,b,d){var e,f;return d||(f=$b[b],$b[b]=e,e=null!=c(a,b,d)?b.toLowerCase():null,$b[b]=f),e}});var _b=/^(?:input|select|textarea|button)$/i;n.fn.extend({prop:function(a,b){return J(this,n.prop,a,b,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[n.propFix[a]||a]})}}),n.extend({propFix:{"for":"htmlFor","class":"className"},prop:function(a,b,c){var d,e,f,g=a.nodeType;if(a&&3!==g&&8!==g&&2!==g)return f=1!==g||!n.isXMLDoc(a),f&&(b=n.propFix[b]||b,e=n.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){return a.hasAttribute("tabindex")||_b.test(a.nodeName)||a.href?a.tabIndex:-1}}}}),k.optSelected||(n.propHooks.selected={get:function(a){var b=a.parentNode;return b&&b.parentNode&&b.parentNode.selectedIndex,null}}),n.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){n.propFix[this.toLowerCase()]=this});var ac=/[\t\r\n\f]/g;n.fn.extend({addClass:function(a){var b,c,d,e,f,g,h="string"==typeof a&&a,i=0,j=this.length;if(n.isFunction(a))return this.each(function(b){n(this).addClass(a.call(this,b,this.className))});if(h)for(b=(a||"").match(E)||[];j>i;i++)if(c=this[i],d=1===c.nodeType&&(c.className?(" "+c.className+" ").replace(ac," "):" ")){f=0;while(e=b[f++])d.indexOf(" "+e+" ")<0&&(d+=e+" ");g=n.trim(d),c.className!==g&&(c.className=g)}return this},removeClass:function(a){var b,c,d,e,f,g,h=0===arguments.length||"string"==typeof a&&a,i=0,j=this.length;if(n.isFunction(a))return this.each(function(b){n(this).removeClass(a.call(this,b,this.className))});if(h)for(b=(a||"").match(E)||[];j>i;i++)if(c=this[i],d=1===c.nodeType&&(c.className?(" "+c.className+" ").replace(ac," "):"")){f=0;while(e=b[f++])while(d.indexOf(" "+e+" ")>=0)d=d.replace(" "+e+" "," ");g=a?n.trim(d):"",c.className!==g&&(c.className=g)}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):this.each(n.isFunction(a)?function(c){n(this).toggleClass(a.call(this,c,this.className,b),b)}:function(){if("string"===c){var b,d=0,e=n(this),f=a.match(E)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else(c===U||"boolean"===c)&&(this.className&&L.set(this,"__className__",this.className),this.className=this.className||a===!1?"":L.get(this,"__className__")||"")})},hasClass:function(a){for(var b=" "+a+" ",c=0,d=this.length;d>c;c++)if(1===this[c].nodeType&&(" "+this[c].className+" ").replace(ac," ").indexOf(b)>=0)return!0;return!1}});var bc=/\r/g;n.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=n.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,n(this).val()):a,null==e?e="":"number"==typeof e?e+="":n.isArray(e)&&(e=n.map(e,function(a){return null==a?"":a+""})),b=n.valHooks[this.type]||n.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=n.valHooks[e.type]||n.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(bc,""):null==c?"":c)}}}),n.extend({valHooks:{option:{get:function(a){var b=n.find.attr(a,"value");return null!=b?b:n.trim(n.text(a))}},select:{get:function(a){for(var b,c,d=a.options,e=a.selectedIndex,f="select-one"===a.type||0>e,g=f?null:[],h=f?e+1:d.length,i=0>e?h:f?e:0;h>i;i++)if(c=d[i],!(!c.selected&&i!==e||(k.optDisabled?c.disabled:null!==c.getAttribute("disabled"))||c.parentNode.disabled&&n.nodeName(c.parentNode,"optgroup"))){if(b=n(c).val(),f)return b;g.push(b)}return g},set:function(a,b){var c,d,e=a.options,f=n.makeArray(b),g=e.length;while(g--)d=e[g],(d.selected=n.inArray(d.value,f)>=0)&&(c=!0);return c||(a.selectedIndex=-1),f}}}}),n.each(["radio","checkbox"],function(){n.valHooks[this]={set:function(a,b){return n.isArray(b)?a.checked=n.inArray(n(a).val(),b)>=0:void 0}},k.checkOn||(n.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})}),n.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(a,b){n.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),n.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)},bind:function(a,b,c){return this.on(a,null,b,c)},unbind:function(a,b){return this.off(a,null,b)},delegate:function(a,b,c,d){return this.on(b,a,c,d)},undelegate:function(a,b,c){return 1===arguments.length?this.off(a,"**"):this.off(b,a||"**",c)}});var cc=n.now(),dc=/\?/;n.parseJSON=function(a){return JSON.parse(a+"")},n.parseXML=function(a){var b,c;if(!a||"string"!=typeof a)return null;try{c=new DOMParser,b=c.parseFromString(a,"text/xml")}catch(d){b=void 0}return(!b||b.getElementsByTagName("parsererror").length)&&n.error("Invalid XML: "+a),b};var ec,fc,gc=/#.*$/,hc=/([?&])_=[^&]*/,ic=/^(.*?):[ \t]*([^\r\n]*)$/gm,jc=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,kc=/^(?:GET|HEAD)$/,lc=/^\/\//,mc=/^([\w.+-]+:)(?:\/\/(?:[^\/?#]*@|)([^\/?#:]*)(?::(\d+)|)|)/,nc={},oc={},pc="*/".concat("*");try{fc=location.href}catch(qc){fc=l.createElement("a"),fc.href="",fc=fc.href}ec=mc.exec(fc.toLowerCase())||[];function rc(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(E)||[];if(n.isFunction(c))while(d=f[e++])"+"===d[0]?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function sc(a,b,c,d){var e={},f=a===oc;function g(h){var i;return e[h]=!0,n.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function tc(a,b){var c,d,e=n.ajaxSettings.flatOptions||{};for(c in b)void 0!==b[c]&&((e[c]?a:d||(d={}))[c]=b[c]);return d&&n.extend(!0,a,d),a}function uc(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===d&&(d=a.mimeType||b.getResponseHeader("Content-Type"));if(d)for(e in h)if(h[e]&&h[e].test(d)){i.unshift(e);break}if(i[0]in c)f=i[0];else{for(e in c){if(!i[0]||a.converters[e+" "+i[0]]){f=e;break}g||(g=e)}f=f||g}return f?(f!==i[0]&&i.unshift(f),c[f]):void 0}function vc(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}n.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:fc,type:"GET",isLocal:jc.test(ec[1]),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":pc,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":n.parseJSON,"text xml":n.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?tc(tc(a,n.ajaxSettings),b):tc(n.ajaxSettings,a)},ajaxPrefilter:rc(nc),ajaxTransport:rc(oc),ajax:function(a,b){"object"==typeof a&&(b=a,a=void 0),b=b||{};var c,d,e,f,g,h,i,j,k=n.ajaxSetup({},b),l=k.context||k,m=k.context&&(l.nodeType||l.jquery)?n(l):n.event,o=n.Deferred(),p=n.Callbacks("once memory"),q=k.statusCode||{},r={},s={},t=0,u="canceled",v={readyState:0,getResponseHeader:function(a){var b;if(2===t){if(!f){f={};while(b=ic.exec(e))f[b[1].toLowerCase()]=b[2]}b=f[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return 2===t?e:null},setRequestHeader:function(a,b){var c=a.toLowerCase();return t||(a=s[c]=s[c]||a,r[a]=b),this},overrideMimeType:function(a){return t||(k.mimeType=a),this},statusCode:function(a){var b;if(a)if(2>t)for(b in a)q[b]=[q[b],a[b]];else v.always(a[v.status]);return this},abort:function(a){var b=a||u;return c&&c.abort(b),x(0,b),this}};if(o.promise(v).complete=p.add,v.success=v.done,v.error=v.fail,k.url=((a||k.url||fc)+"").replace(gc,"").replace(lc,ec[1]+"//"),k.type=b.method||b.type||k.method||k.type,k.dataTypes=n.trim(k.dataType||"*").toLowerCase().match(E)||[""],null==k.crossDomain&&(h=mc.exec(k.url.toLowerCase()),k.crossDomain=!(!h||h[1]===ec[1]&&h[2]===ec[2]&&(h[3]||("http:"===h[1]?"80":"443"))===(ec[3]||("http:"===ec[1]?"80":"443")))),k.data&&k.processData&&"string"!=typeof k.data&&(k.data=n.param(k.data,k.traditional)),sc(nc,k,b,v),2===t)return v;i=k.global,i&&0===n.active++&&n.event.trigger("ajaxStart"),k.type=k.type.toUpperCase(),k.hasContent=!kc.test(k.type),d=k.url,k.hasContent||(k.data&&(d=k.url+=(dc.test(d)?"&":"?")+k.data,delete k.data),k.cache===!1&&(k.url=hc.test(d)?d.replace(hc,"$1_="+cc++):d+(dc.test(d)?"&":"?")+"_="+cc++)),k.ifModified&&(n.lastModified[d]&&v.setRequestHeader("If-Modified-Since",n.lastModified[d]),n.etag[d]&&v.setRequestHeader("If-None-Match",n.etag[d])),(k.data&&k.hasContent&&k.contentType!==!1||b.contentType)&&v.setRequestHeader("Content-Type",k.contentType),v.setRequestHeader("Accept",k.dataTypes[0]&&k.accepts[k.dataTypes[0]]?k.accepts[k.dataTypes[0]]+("*"!==k.dataTypes[0]?", "+pc+"; q=0.01":""):k.accepts["*"]);for(j in k.headers)v.setRequestHeader(j,k.headers[j]);if(k.beforeSend&&(k.beforeSend.call(l,v,k)===!1||2===t))return v.abort();u="abort";for(j in{success:1,error:1,complete:1})v[j](k[j]);if(c=sc(oc,k,b,v)){v.readyState=1,i&&m.trigger("ajaxSend",[v,k]),k.async&&k.timeout>0&&(g=setTimeout(function(){v.abort("timeout")},k.timeout));try{t=1,c.send(r,x)}catch(w){if(!(2>t))throw w;x(-1,w)}}else x(-1,"No Transport");function x(a,b,f,h){var j,r,s,u,w,x=b;2!==t&&(t=2,g&&clearTimeout(g),c=void 0,e=h||"",v.readyState=a>0?4:0,j=a>=200&&300>a||304===a,f&&(u=uc(k,v,f)),u=vc(k,u,v,j),j?(k.ifModified&&(w=v.getResponseHeader("Last-Modified"),w&&(n.lastModified[d]=w),w=v.getResponseHeader("etag"),w&&(n.etag[d]=w)),204===a||"HEAD"===k.type?x="nocontent":304===a?x="notmodified":(x=u.state,r=u.data,s=u.error,j=!s)):(s=x,(a||!x)&&(x="error",0>a&&(a=0))),v.status=a,v.statusText=(b||x)+"",j?o.resolveWith(l,[r,x,v]):o.rejectWith(l,[v,x,s]),v.statusCode(q),q=void 0,i&&m.trigger(j?"ajaxSuccess":"ajaxError",[v,k,j?r:s]),p.fireWith(l,[v,x]),i&&(m.trigger("ajaxComplete",[v,k]),--n.active||n.event.trigger("ajaxStop")))}return v},getJSON:function(a,b,c){return n.get(a,b,c,"json")},getScript:function(a,b){return n.get(a,void 0,b,"script")}}),n.each(["get","post"],function(a,b){n[b]=function(a,c,d,e){return n.isFunction(c)&&(e=e||d,d=c,c=void 0),n.ajax({url:a,type:b,dataType:e,data:c,success:d})}}),n.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(a,b){n.fn[b]=function(a){return this.on(b,a)}}),n._evalUrl=function(a){return n.ajax({url:a,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0})},n.fn.extend({wrapAll:function(a){var b;return n.isFunction(a)?this.each(function(b){n(this).wrapAll(a.call(this,b))}):(this[0]&&(b=n(a,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstElementChild)a=a.firstElementChild;return a}).append(this)),this)},wrapInner:function(a){return this.each(n.isFunction(a)?function(b){n(this).wrapInner(a.call(this,b))}:function(){var b=n(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=n.isFunction(a);return this.each(function(c){n(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){n.nodeName(this,"body")||n(this).replaceWith(this.childNodes)}).end()}}),n.expr.filters.hidden=function(a){return a.offsetWidth<=0&&a.offsetHeight<=0},n.expr.filters.visible=function(a){return!n.expr.filters.hidden(a)};var wc=/%20/g,xc=/\[\]$/,yc=/\r?\n/g,zc=/^(?:submit|button|image|reset|file)$/i,Ac=/^(?:input|select|textarea|keygen)/i;function Bc(a,b,c,d){var e;if(n.isArray(b))n.each(b,function(b,e){c||xc.test(a)?d(a,e):Bc(a+"["+("object"==typeof e?b:"")+"]",e,c,d)});else if(c||"object"!==n.type(b))d(a,b);else for(e in b)Bc(a+"["+e+"]",b[e],c,d)}n.param=function(a,b){var c,d=[],e=function(a,b){b=n.isFunction(b)?b():null==b?"":b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};if(void 0===b&&(b=n.ajaxSettings&&n.ajaxSettings.traditional),n.isArray(a)||a.jquery&&!n.isPlainObject(a))n.each(a,function(){e(this.name,this.value)});else for(c in a)Bc(c,a[c],b,e);return d.join("&").replace(wc,"+")},n.fn.extend({serialize:function(){return n.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=n.prop(this,"elements");return a?n.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!n(this).is(":disabled")&&Ac.test(this.nodeName)&&!zc.test(a)&&(this.checked||!T.test(a))}).map(function(a,b){var c=n(this).val();return null==c?null:n.isArray(c)?n.map(c,function(a){return{name:b.name,value:a.replace(yc,"\r\n")}}):{name:b.name,value:c.replace(yc,"\r\n")}}).get()}}),n.ajaxSettings.xhr=function(){try{return new XMLHttpRequest}catch(a){}};var Cc=0,Dc={},Ec={0:200,1223:204},Fc=n.ajaxSettings.xhr();a.ActiveXObject&&n(a).on("unload",function(){for(var a in Dc)Dc[a]()}),k.cors=!!Fc&&"withCredentials"in Fc,k.ajax=Fc=!!Fc,n.ajaxTransport(function(a){var b;return k.cors||Fc&&!a.crossDomain?{send:function(c,d){var e,f=a.xhr(),g=++Cc;if(f.open(a.type,a.url,a.async,a.username,a.password),a.xhrFields)for(e in a.xhrFields)f[e]=a.xhrFields[e];a.mimeType&&f.overrideMimeType&&f.overrideMimeType(a.mimeType),a.crossDomain||c["X-Requested-With"]||(c["X-Requested-With"]="XMLHttpRequest");for(e in c)f.setRequestHeader(e,c[e]);b=function(a){return function(){b&&(delete Dc[g],b=f.onload=f.onerror=null,"abort"===a?f.abort():"error"===a?d(f.status,f.statusText):d(Ec[f.status]||f.status,f.statusText,"string"==typeof f.responseText?{text:f.responseText}:void 0,f.getAllResponseHeaders()))}},f.onload=b(),f.onerror=b("error"),b=Dc[g]=b("abort");try{f.send(a.hasContent&&a.data||null)}catch(h){if(b)throw h}},abort:function(){b&&b()}}:void 0}),n.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/(?:java|ecma)script/},converters:{"text script":function(a){return n.globalEval(a),a}}}),n.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET")}),n.ajaxTransport("script",function(a){if(a.crossDomain){var b,c;return{send:function(d,e){b=n("<script>").prop({async:!0,charset:a.scriptCharset,src:a.url}).on("load error",c=function(a){b.remove(),c=null,a&&e("error"===a.type?404:200,a.type)}),l.head.appendChild(b[0])},abort:function(){c&&c()}}}});var Gc=[],Hc=/(=)\?(?=&|$)|\?\?/;n.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var a=Gc.pop()||n.expando+"_"+cc++;return this[a]=!0,a}}),n.ajaxPrefilter("json jsonp",function(b,c,d){var e,f,g,h=b.jsonp!==!1&&(Hc.test(b.url)?"url":"string"==typeof b.data&&!(b.contentType||"").indexOf("application/x-www-form-urlencoded")&&Hc.test(b.data)&&"data");return h||"jsonp"===b.dataTypes[0]?(e=b.jsonpCallback=n.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,h?b[h]=b[h].replace(Hc,"$1"+e):b.jsonp!==!1&&(b.url+=(dc.test(b.url)?"&":"?")+b.jsonp+"="+e),b.converters["script json"]=function(){return g||n.error(e+" was not called"),g[0]},b.dataTypes[0]="json",f=a[e],a[e]=function(){g=arguments},d.always(function(){a[e]=f,b[e]&&(b.jsonpCallback=c.jsonpCallback,Gc.push(e)),g&&n.isFunction(f)&&f(g[0]),g=f=void 0}),"script"):void 0}),n.parseHTML=function(a,b,c){if(!a||"string"!=typeof a)return null;"boolean"==typeof b&&(c=b,b=!1),b=b||l;var d=v.exec(a),e=!c&&[];return d?[b.createElement(d[1])]:(d=n.buildFragment([a],b,e),e&&e.length&&n(e).remove(),n.merge([],d.childNodes))};var Ic=n.fn.load;n.fn.load=function(a,b,c){if("string"!=typeof a&&Ic)return Ic.apply(this,arguments);var d,e,f,g=this,h=a.indexOf(" ");return h>=0&&(d=n.trim(a.slice(h)),a=a.slice(0,h)),n.isFunction(b)?(c=b,b=void 0):b&&"object"==typeof b&&(e="POST"),g.length>0&&n.ajax({url:a,type:e,dataType:"html",data:b}).done(function(a){f=arguments,g.html(d?n("<div>").append(n.parseHTML(a)).find(d):a)}).complete(c&&function(a,b){g.each(c,f||[a.responseText,b,a])}),this},n.expr.filters.animated=function(a){return n.grep(n.timers,function(b){return a===b.elem}).length};var Jc=a.document.documentElement;function Kc(a){return n.isWindow(a)?a:9===a.nodeType&&a.defaultView}n.offset={setOffset:function(a,b,c){var d,e,f,g,h,i,j,k=n.css(a,"position"),l=n(a),m={};"static"===k&&(a.style.position="relative"),h=l.offset(),f=n.css(a,"top"),i=n.css(a,"left"),j=("absolute"===k||"fixed"===k)&&(f+i).indexOf("auto")>-1,j?(d=l.position(),g=d.top,e=d.left):(g=parseFloat(f)||0,e=parseFloat(i)||0),n.isFunction(b)&&(b=b.call(a,c,h)),null!=b.top&&(m.top=b.top-h.top+g),null!=b.left&&(m.left=b.left-h.left+e),"using"in b?b.using.call(a,m):l.css(m)}},n.fn.extend({offset:function(a){if(arguments.length)return void 0===a?this:this.each(function(b){n.offset.setOffset(this,a,b)});var b,c,d=this[0],e={top:0,left:0},f=d&&d.ownerDocument;if(f)return b=f.documentElement,n.contains(b,d)?(typeof d.getBoundingClientRect!==U&&(e=d.getBoundingClientRect()),c=Kc(f),{top:e.top+c.pageYOffset-b.clientTop,left:e.left+c.pageXOffset-b.clientLeft}):e},position:function(){if(this[0]){var a,b,c=this[0],d={top:0,left:0};return"fixed"===n.css(c,"position")?b=c.getBoundingClientRect():(a=this.offsetParent(),b=this.offset(),n.nodeName(a[0],"html")||(d=a.offset()),d.top+=n.css(a[0],"borderTopWidth",!0),d.left+=n.css(a[0],"borderLeftWidth",!0)),{top:b.top-d.top-n.css(c,"marginTop",!0),left:b.left-d.left-n.css(c,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||Jc;while(a&&!n.nodeName(a,"html")&&"static"===n.css(a,"position"))a=a.offsetParent;return a||Jc})}}),n.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(b,c){var d="pageYOffset"===c;n.fn[b]=function(e){return J(this,function(b,e,f){var g=Kc(b);return void 0===f?g?g[c]:b[e]:void(g?g.scrollTo(d?a.pageXOffset:f,d?f:a.pageYOffset):b[e]=f)},b,e,arguments.length,null)}}),n.each(["top","left"],function(a,b){n.cssHooks[b]=yb(k.pixelPosition,function(a,c){return c?(c=xb(a,b),vb.test(c)?n(a).position()[b]+"px":c):void 0})}),n.each({Height:"height",Width:"width"},function(a,b){n.each({padding:"inner"+a,content:b,"":"outer"+a},function(c,d){n.fn[d]=function(d,e){var f=arguments.length&&(c||"boolean"!=typeof d),g=c||(d===!0||e===!0?"margin":"border");return J(this,function(b,c,d){var e;return n.isWindow(b)?b.document.documentElement["client"+a]:9===b.nodeType?(e=b.documentElement,Math.max(b.body["scroll"+a],e["scroll"+a],b.body["offset"+a],e["offset"+a],e["client"+a])):void 0===d?n.css(b,c,g):n.style(b,c,d,g)},b,f?d:void 0,f,null)}})}),n.fn.size=function(){return this.length},n.fn.andSelf=n.fn.addBack,"function"==typeof define&&define.amd&&define("jquery",[],function(){return n});var Lc=a.jQuery,Mc=a.$;return n.noConflict=function(b){return a.$===n&&(a.$=Mc),b&&a.jQuery===n&&(a.jQuery=Lc),n},typeof b===U&&(a.jQuery=a.$=n),n});
diff --git a/devtools/client/inspector/markup/utils.js b/devtools/client/inspector/markup/utils.js
new file mode 100644
index 000000000..8fab9d963
--- /dev/null
+++ b/devtools/client/inspector/markup/utils.js
@@ -0,0 +1,135 @@
+/* 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";
+
+/**
+ * Apply a 'flashed' background and foreground color to elements. Intended
+ * to be used with flashElementOff as a way of drawing attention to an element.
+ *
+ * @param {Node} backgroundElt
+ * The element to set the highlighted background color on.
+ * @param {Node} foregroundElt
+ * The element to set the matching foreground color on.
+ * Optional. This will equal backgroundElt if not set.
+ */
+function flashElementOn(backgroundElt, foregroundElt = backgroundElt) {
+ if (!backgroundElt || !foregroundElt) {
+ return;
+ }
+
+ // Make sure the animation class is not here
+ backgroundElt.classList.remove("flash-out");
+
+ // Change the background
+ backgroundElt.classList.add("theme-bg-contrast");
+
+ foregroundElt.classList.add("theme-fg-contrast");
+ [].forEach.call(
+ foregroundElt.querySelectorAll("[class*=theme-fg-color]"),
+ span => span.classList.add("theme-fg-contrast")
+ );
+}
+
+/**
+ * Remove a 'flashed' background and foreground color to elements.
+ * See flashElementOn.
+ *
+ * @param {Node} backgroundElt
+ * The element to reomve the highlighted background color on.
+ * @param {Node} foregroundElt
+ * The element to remove the matching foreground color on.
+ * Optional. This will equal backgroundElt if not set.
+ */
+function flashElementOff(backgroundElt, foregroundElt = backgroundElt) {
+ if (!backgroundElt || !foregroundElt) {
+ return;
+ }
+
+ // Add the animation class to smoothly remove the background
+ backgroundElt.classList.add("flash-out");
+
+ // Remove the background
+ backgroundElt.classList.remove("theme-bg-contrast");
+
+ foregroundElt.classList.remove("theme-fg-contrast");
+ [].forEach.call(
+ foregroundElt.querySelectorAll("[class*=theme-fg-color]"),
+ span => span.classList.remove("theme-fg-contrast")
+ );
+}
+
+/**
+ * Retrieve the available width between a provided element left edge and a container right
+ * edge. This used can be used as a max-width for inplace-editor (autocomplete) widgets
+ * replacing Editor elements of the the markup-view;
+ */
+function getAutocompleteMaxWidth(element, container) {
+ let elementRect = element.getBoundingClientRect();
+ let containerRect = container.getBoundingClientRect();
+ return containerRect.right - elementRect.left - 2;
+}
+
+/**
+ * Parse attribute names and values from a string.
+ *
+ * @param {String} attr
+ * The input string for which names/values are to be parsed.
+ * @param {HTMLDocument} doc
+ * A document that can be used to test valid attributes.
+ * @return {Array}
+ * An array of attribute names and their values.
+ */
+function parseAttributeValues(attr, doc) {
+ attr = attr.trim();
+
+ let parseAndGetNode = str => {
+ return new DOMParser().parseFromString(str, "text/html").body.childNodes[0];
+ };
+
+ // Handle bad user inputs by appending a " or ' if it fails to parse without
+ // them. Also note that a SVG tag is used to make sure the HTML parser
+ // preserves mixed-case attributes
+ let el = parseAndGetNode("<svg " + attr + "></svg>") ||
+ parseAndGetNode("<svg " + attr + "\"></svg>") ||
+ parseAndGetNode("<svg " + attr + "'></svg>");
+
+ let div = doc.createElement("div");
+ let attributes = [];
+ for (let {name, value} of el.attributes) {
+ // Try to set on an element in the document, throws exception on bad input.
+ // Prevents InvalidCharacterError - "String contains an invalid character".
+ try {
+ div.setAttribute(name, value);
+ attributes.push({ name, value });
+ } catch (e) {
+ // This may throw exceptions on bad input.
+ // Prevents InvalidCharacterError - "String contains an invalid
+ // character".
+ }
+ }
+
+ return attributes;
+}
+
+/**
+ * Truncate the string and add ellipsis to the middle of the string.
+ */
+function truncateString(str, maxLength) {
+ if (!str || str.length <= maxLength) {
+ return str;
+ }
+
+ return str.substring(0, Math.ceil(maxLength / 2)) +
+ "…" +
+ str.substring(str.length - Math.floor(maxLength / 2));
+}
+
+module.exports = {
+ flashElementOn,
+ flashElementOff,
+ getAutocompleteMaxWidth,
+ parseAttributeValues,
+ truncateString,
+};
diff --git a/devtools/client/inspector/markup/views/element-container.js b/devtools/client/inspector/markup/views/element-container.js
new file mode 100644
index 000000000..851a803cb
--- /dev/null
+++ b/devtools/client/inspector/markup/views/element-container.js
@@ -0,0 +1,193 @@
+/* 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 PREVIEW_MAX_DIM_PREF = "devtools.inspector.imagePreviewTooltipSize";
+
+const promise = require("promise");
+const Services = require("Services");
+const Heritage = require("sdk/core/heritage");
+const {Task} = require("devtools/shared/task");
+const nodeConstants = require("devtools/shared/dom-node-constants");
+const clipboardHelper = require("devtools/shared/platform/clipboard");
+const {setImageTooltip, setBrokenImageTooltip} =
+ require("devtools/client/shared/widgets/tooltip/ImageTooltipHelper");
+const {setEventTooltip} = require("devtools/client/shared/widgets/tooltip/EventTooltipHelper");
+const MarkupContainer = require("devtools/client/inspector/markup/views/markup-container");
+const ElementEditor = require("devtools/client/inspector/markup/views/element-editor");
+
+/**
+ * An implementation of MarkupContainer for Elements that can contain
+ * child nodes.
+ * Allows editing of tag name, attributes, expanding / collapsing.
+ *
+ * @param {MarkupView} markupView
+ * The markup view that owns this container.
+ * @param {NodeFront} node
+ * The node to display.
+ */
+function MarkupElementContainer(markupView, node) {
+ MarkupContainer.prototype.initialize.call(this, markupView, node,
+ "elementcontainer");
+
+ if (node.nodeType === nodeConstants.ELEMENT_NODE) {
+ this.editor = new ElementEditor(this, node);
+ } else {
+ throw new Error("Invalid node for MarkupElementContainer");
+ }
+
+ this.tagLine.appendChild(this.editor.elt);
+}
+
+MarkupElementContainer.prototype = Heritage.extend(MarkupContainer.prototype, {
+ _buildEventTooltipContent: Task.async(function* (target, tooltip) {
+ if (target.hasAttribute("data-event")) {
+ yield tooltip.hide();
+
+ let listenerInfo = yield this.node.getEventListenerInfo();
+
+ let toolbox = this.markup.toolbox;
+ setEventTooltip(tooltip, listenerInfo, toolbox);
+ // Disable the image preview tooltip while we display the event details
+ this.markup._disableImagePreviewTooltip();
+ tooltip.once("hidden", () => {
+ // Enable the image preview tooltip after closing the event details
+ this.markup._enableImagePreviewTooltip();
+ });
+ tooltip.show(target);
+ }
+ }),
+
+ /**
+ * Generates the an image preview for this Element. The element must be an
+ * image or canvas (@see isPreviewable).
+ *
+ * @return {Promise} that is resolved with an object of form
+ * { data, size: { naturalWidth, naturalHeight, resizeRatio } } where
+ * - data is the data-uri for the image preview.
+ * - size contains information about the original image size and if
+ * the preview has been resized.
+ *
+ * If this element is not previewable or the preview cannot be generated for
+ * some reason, the Promise is rejected.
+ */
+ _getPreview: function () {
+ if (!this.isPreviewable()) {
+ return promise.reject("_getPreview called on a non-previewable element.");
+ }
+
+ if (this.tooltipDataPromise) {
+ // A preview request is already pending. Re-use that request.
+ return this.tooltipDataPromise;
+ }
+
+ // Fetch the preview from the server.
+ this.tooltipDataPromise = Task.spawn(function* () {
+ let maxDim = Services.prefs.getIntPref(PREVIEW_MAX_DIM_PREF);
+ let preview = yield this.node.getImageData(maxDim);
+ let data = yield preview.data.string();
+
+ // Clear the pending preview request. We can't reuse the results later as
+ // the preview contents might have changed.
+ this.tooltipDataPromise = null;
+ return { data, size: preview.size };
+ }.bind(this));
+
+ return this.tooltipDataPromise;
+ },
+
+ /**
+ * Executed by MarkupView._isImagePreviewTarget which is itself called when
+ * the mouse hovers over a target in the markup-view.
+ * Checks if the target is indeed something we want to have an image tooltip
+ * preview over and, if so, inserts content into the tooltip.
+ *
+ * @return {Promise} that resolves when the tooltip content is ready. Resolves
+ * true if the tooltip should be displayed, false otherwise.
+ */
+ isImagePreviewTarget: Task.async(function* (target, tooltip) {
+ // Is this Element previewable.
+ if (!this.isPreviewable()) {
+ return false;
+ }
+
+ // If the Element has an src attribute, the tooltip is shown when hovering
+ // over the src url. If not, the tooltip is shown when hovering over the tag
+ // name.
+ let src = this.editor.getAttributeElement("src");
+ let expectedTarget = src ? src.querySelector(".link") : this.editor.tag;
+ if (target !== expectedTarget) {
+ return false;
+ }
+
+ try {
+ let { data, size } = yield this._getPreview();
+ // The preview is ready.
+ let options = {
+ naturalWidth: size.naturalWidth,
+ naturalHeight: size.naturalHeight,
+ maxDim: Services.prefs.getIntPref(PREVIEW_MAX_DIM_PREF)
+ };
+
+ setImageTooltip(tooltip, this.markup.doc, data, options);
+ } catch (e) {
+ // Indicate the failure but show the tooltip anyway.
+ setBrokenImageTooltip(tooltip, this.markup.doc);
+ }
+ return true;
+ }),
+
+ copyImageDataUri: function () {
+ // We need to send again a request to gettooltipData even if one was sent
+ // for the tooltip, because we want the full-size image
+ this.node.getImageData().then(data => {
+ data.data.string().then(str => {
+ clipboardHelper.copyString(str);
+ });
+ });
+ },
+
+ setInlineTextChild: function (inlineTextChild) {
+ this.inlineTextChild = inlineTextChild;
+ this.editor.updateTextEditor();
+ },
+
+ clearInlineTextChild: function () {
+ this.inlineTextChild = undefined;
+ this.editor.updateTextEditor();
+ },
+
+ /**
+ * Trigger new attribute field for input.
+ */
+ addAttribute: function () {
+ this.editor.newAttr.editMode();
+ },
+
+ /**
+ * Trigger attribute field for editing.
+ */
+ editAttribute: function (attrName) {
+ this.editor.attrElements.get(attrName).editMode();
+ },
+
+ /**
+ * Remove attribute from container.
+ * This is an undoable action.
+ */
+ removeAttribute: function (attrName) {
+ let doMods = this.editor._startModifyingAttributes();
+ let undoMods = this.editor._startModifyingAttributes();
+ this.editor._saveAttribute(attrName, undoMods);
+ doMods.removeAttribute(attrName);
+ this.undo.do(() => {
+ doMods.apply();
+ }, () => {
+ undoMods.apply();
+ });
+ }
+});
+
+module.exports = MarkupElementContainer;
diff --git a/devtools/client/inspector/markup/views/element-editor.js b/devtools/client/inspector/markup/views/element-editor.js
new file mode 100644
index 000000000..3149086eb
--- /dev/null
+++ b/devtools/client/inspector/markup/views/element-editor.js
@@ -0,0 +1,560 @@
+/* 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 Services = require("Services");
+const TextEditor = require("devtools/client/inspector/markup/views/text-editor");
+const {
+ getAutocompleteMaxWidth,
+ flashElementOn,
+ flashElementOff,
+ parseAttributeValues,
+ truncateString,
+} = require("devtools/client/inspector/markup/utils");
+const {editableField, InplaceEditor} =
+ require("devtools/client/shared/inplace-editor");
+const {parseAttribute} =
+ require("devtools/client/shared/node-attribute-parser");
+const {getCssProperties} = require("devtools/shared/fronts/css-properties");
+
+// Page size for pageup/pagedown
+const COLLAPSE_DATA_URL_REGEX = /^data.+base64/;
+const COLLAPSE_DATA_URL_LENGTH = 60;
+
+// Contains only void (without end tag) HTML elements
+const HTML_VOID_ELEMENTS = [ "area", "base", "br", "col", "command", "embed",
+ "hr", "img", "input", "keygen", "link", "meta", "param", "source",
+ "track", "wbr" ];
+
+/**
+ * Creates an editor for an Element node.
+ *
+ * @param {MarkupContainer} container
+ * The container owning this editor.
+ * @param {Element} node
+ * The node being edited.
+ */
+function ElementEditor(container, node) {
+ this.container = container;
+ this.node = node;
+ this.markup = this.container.markup;
+ this.template = this.markup.template.bind(this.markup);
+ this.doc = this.markup.doc;
+ this._cssProperties = getCssProperties(this.markup.toolbox);
+
+ this.attrElements = new Map();
+ this.animationTimers = {};
+
+ // The templates will fill the following properties
+ this.elt = null;
+ this.tag = null;
+ this.closeTag = null;
+ this.attrList = null;
+ this.newAttr = null;
+ this.closeElt = null;
+
+ // Create the main editor
+ this.template("element", this);
+
+ // Make the tag name editable (unless this is a remote node or
+ // a document element)
+ if (!node.isDocumentElement) {
+ // Make the tag optionally tabbable but not by default.
+ this.tag.setAttribute("tabindex", "-1");
+ editableField({
+ element: this.tag,
+ multiline: true,
+ maxWidth: () => getAutocompleteMaxWidth(this.tag, this.container.elt),
+ trigger: "dblclick",
+ stopOnReturn: true,
+ done: this.onTagEdit.bind(this),
+ contextMenu: this.markup.inspector.onTextBoxContextMenu,
+ cssProperties: this._cssProperties
+ });
+ }
+
+ // Make the new attribute space editable.
+ this.newAttr.editMode = editableField({
+ element: this.newAttr,
+ multiline: true,
+ maxWidth: () => getAutocompleteMaxWidth(this.newAttr, this.container.elt),
+ trigger: "dblclick",
+ stopOnReturn: true,
+ contentType: InplaceEditor.CONTENT_TYPES.CSS_MIXED,
+ popup: this.markup.popup,
+ done: (val, commit) => {
+ if (!commit) {
+ return;
+ }
+
+ let doMods = this._startModifyingAttributes();
+ let undoMods = this._startModifyingAttributes();
+ this._applyAttributes(val, null, doMods, undoMods);
+ this.container.undo.do(() => {
+ doMods.apply();
+ }, function () {
+ undoMods.apply();
+ });
+ },
+ contextMenu: this.markup.inspector.onTextBoxContextMenu,
+ cssProperties: this._cssProperties
+ });
+
+ let displayName = this.node.displayName;
+ this.tag.textContent = displayName;
+ this.closeTag.textContent = displayName;
+
+ let isVoidElement = HTML_VOID_ELEMENTS.includes(displayName);
+ if (node.isInHTMLDocument && isVoidElement) {
+ this.elt.classList.add("void-element");
+ }
+
+ this.update();
+ this.initialized = true;
+}
+
+ElementEditor.prototype = {
+ set selected(value) {
+ if (this.textEditor) {
+ this.textEditor.selected = value;
+ }
+ },
+
+ flashAttribute: function (attrName) {
+ if (this.animationTimers[attrName]) {
+ clearTimeout(this.animationTimers[attrName]);
+ }
+
+ flashElementOn(this.getAttributeElement(attrName));
+
+ this.animationTimers[attrName] = setTimeout(() => {
+ flashElementOff(this.getAttributeElement(attrName));
+ }, this.markup.CONTAINER_FLASHING_DURATION);
+ },
+
+ /**
+ * Returns information about node in the editor.
+ *
+ * @param {DOMNode} node
+ * The node to get information from.
+ * @return {Object} An object literal with the following information:
+ * {type: "attribute", name: "rel", value: "index", el: node}
+ */
+ getInfoAtNode: function (node) {
+ if (!node) {
+ return null;
+ }
+
+ let type = null;
+ let name = null;
+ let value = null;
+
+ // Attribute
+ let attribute = node.closest(".attreditor");
+ if (attribute) {
+ type = "attribute";
+ name = attribute.querySelector(".attr-name").textContent;
+ value = attribute.querySelector(".attr-value").textContent;
+ }
+
+ return {type, name, value, el: node};
+ },
+
+ /**
+ * Update the state of the editor from the node.
+ */
+ update: function () {
+ let nodeAttributes = this.node.attributes || [];
+
+ // Keep the data model in sync with attributes on the node.
+ let currentAttributes = new Set(nodeAttributes.map(a => a.name));
+ for (let name of this.attrElements.keys()) {
+ if (!currentAttributes.has(name)) {
+ this.removeAttribute(name);
+ }
+ }
+
+ // Only loop through the current attributes on the node. Missing
+ // attributes have already been removed at this point.
+ for (let attr of nodeAttributes) {
+ let el = this.attrElements.get(attr.name);
+ let valueChanged = el &&
+ el.dataset.value !== attr.value;
+ let isEditing = el && el.querySelector(".editable").inplaceEditor;
+ let canSimplyShowEditor = el && (!valueChanged || isEditing);
+
+ if (canSimplyShowEditor) {
+ // Element already exists and doesn't need to be recreated.
+ // Just show it (it's hidden by default due to the template).
+ el.style.removeProperty("display");
+ } else {
+ // Create a new editor, because the value of an existing attribute
+ // has changed.
+ let attribute = this._createAttribute(attr, el);
+ attribute.style.removeProperty("display");
+
+ // Temporarily flash the attribute to highlight the change.
+ // But not if this is the first time the editor instance has
+ // been created.
+ if (this.initialized) {
+ this.flashAttribute(attr.name);
+ }
+ }
+ }
+
+ // Update the event bubble display
+ this.eventNode.style.display = this.node.hasEventListeners ?
+ "inline-block" : "none";
+
+ this.updateTextEditor();
+ },
+
+ /**
+ * Update the inline text editor in case of a single text child node.
+ */
+ updateTextEditor: function () {
+ let node = this.node.inlineTextChild;
+
+ if (this.textEditor && this.textEditor.node != node) {
+ this.elt.removeChild(this.textEditor.elt);
+ this.textEditor = null;
+ }
+
+ if (node && !this.textEditor) {
+ // Create a text editor added to this editor.
+ // This editor won't receive an update automatically, so we rely on
+ // child text editors to let us know that we need updating.
+ this.textEditor = new TextEditor(this.container, node, "text");
+ this.elt.insertBefore(this.textEditor.elt,
+ this.elt.firstChild.nextSibling.nextSibling);
+ }
+
+ if (this.textEditor) {
+ this.textEditor.update();
+ }
+ },
+
+ _startModifyingAttributes: function () {
+ return this.node.startModifyingAttributes();
+ },
+
+ /**
+ * Get the element used for one of the attributes of this element.
+ *
+ * @param {String} attrName
+ * The name of the attribute to get the element for
+ * @return {DOMNode}
+ */
+ getAttributeElement: function (attrName) {
+ return this.attrList.querySelector(
+ ".attreditor[data-attr=" + CSS.escape(attrName) + "] .attr-value");
+ },
+
+ /**
+ * Remove an attribute from the attrElements object and the DOM.
+ *
+ * @param {String} attrName
+ * The name of the attribute to remove
+ */
+ removeAttribute: function (attrName) {
+ let attr = this.attrElements.get(attrName);
+ if (attr) {
+ this.attrElements.delete(attrName);
+ attr.remove();
+ }
+ },
+
+ _createAttribute: function (attribute, before = null) {
+ // Create the template editor, which will save some variables here.
+ let data = {
+ attrName: attribute.name,
+ attrValue: attribute.value,
+ tabindex: this.container.canFocus ? "0" : "-1",
+ };
+ this.template("attribute", data);
+ let {attr, inner, name, val} = data;
+
+ // Double quotes need to be handled specially to prevent DOMParser failing.
+ // name="v"a"l"u"e" when editing -> name='v"a"l"u"e"'
+ // name="v'a"l'u"e" when editing -> name="v'a&quot;l'u&quot;e"
+ let editValueDisplayed = attribute.value || "";
+ let hasDoubleQuote = editValueDisplayed.includes('"');
+ let hasSingleQuote = editValueDisplayed.includes("'");
+ let initial = attribute.name + '="' + editValueDisplayed + '"';
+
+ // Can't just wrap value with ' since the value contains both " and '.
+ if (hasDoubleQuote && hasSingleQuote) {
+ editValueDisplayed = editValueDisplayed.replace(/\"/g, "&quot;");
+ initial = attribute.name + '="' + editValueDisplayed + '"';
+ }
+
+ // Wrap with ' since there are no single quotes in the attribute value.
+ if (hasDoubleQuote && !hasSingleQuote) {
+ initial = attribute.name + "='" + editValueDisplayed + "'";
+ }
+
+ // Make the attribute editable.
+ attr.editMode = editableField({
+ element: inner,
+ trigger: "dblclick",
+ stopOnReturn: true,
+ selectAll: false,
+ initial: initial,
+ multiline: true,
+ maxWidth: () => getAutocompleteMaxWidth(inner, this.container.elt),
+ contentType: InplaceEditor.CONTENT_TYPES.CSS_MIXED,
+ popup: this.markup.popup,
+ start: (editor, event) => {
+ // If the editing was started inside the name or value areas,
+ // select accordingly.
+ if (event && event.target === name) {
+ editor.input.setSelectionRange(0, name.textContent.length);
+ } else if (event && event.target.closest(".attr-value") === val) {
+ let length = editValueDisplayed.length;
+ let editorLength = editor.input.value.length;
+ let start = editorLength - (length + 1);
+ editor.input.setSelectionRange(start, start + length);
+ } else {
+ editor.input.select();
+ }
+ },
+ done: (newValue, commit, direction) => {
+ if (!commit || newValue === initial) {
+ return;
+ }
+
+ let doMods = this._startModifyingAttributes();
+ let undoMods = this._startModifyingAttributes();
+
+ // Remove the attribute stored in this editor and re-add any attributes
+ // parsed out of the input element. Restore original attribute if
+ // parsing fails.
+ this.refocusOnEdit(attribute.name, attr, direction);
+ this._saveAttribute(attribute.name, undoMods);
+ doMods.removeAttribute(attribute.name);
+ this._applyAttributes(newValue, attr, doMods, undoMods);
+ this.container.undo.do(() => {
+ doMods.apply();
+ }, () => {
+ undoMods.apply();
+ });
+ },
+ contextMenu: this.markup.inspector.onTextBoxContextMenu,
+ cssProperties: this._cssProperties
+ });
+
+ // Figure out where we should place the attribute.
+ if (attribute.name == "id") {
+ before = this.attrList.firstChild;
+ } else if (attribute.name == "class") {
+ let idNode = this.attrElements.get("id");
+ before = idNode ? idNode.nextSibling : this.attrList.firstChild;
+ }
+ this.attrList.insertBefore(attr, before);
+
+ this.removeAttribute(attribute.name);
+ this.attrElements.set(attribute.name, attr);
+
+ // Parse the attribute value to detect whether there are linkable parts in
+ // it (make sure to pass a complete list of existing attributes to the
+ // parseAttribute function, by concatenating attribute, because this could
+ // be a newly added attribute not yet on this.node).
+ let attributes = this.node.attributes.filter(existingAttribute => {
+ return existingAttribute.name !== attribute.name;
+ });
+ attributes.push(attribute);
+ let parsedLinksData = parseAttribute(this.node.namespaceURI,
+ this.node.tagName, attributes, attribute.name);
+
+ // Create links in the attribute value, and collapse long attributes if
+ // needed.
+ let collapse = value => {
+ if (value && value.match(COLLAPSE_DATA_URL_REGEX)) {
+ return truncateString(value, COLLAPSE_DATA_URL_LENGTH);
+ }
+ return this.markup.collapseAttributes
+ ? truncateString(value, this.markup.collapseAttributeLength)
+ : value;
+ };
+
+ val.innerHTML = "";
+ for (let token of parsedLinksData) {
+ if (token.type === "string") {
+ val.appendChild(this.doc.createTextNode(collapse(token.value)));
+ } else {
+ let link = this.doc.createElement("span");
+ link.classList.add("link");
+ link.setAttribute("data-type", token.type);
+ link.setAttribute("data-link", token.value);
+ link.textContent = collapse(token.value);
+ val.appendChild(link);
+ }
+ }
+
+ name.textContent = attribute.name;
+
+ return attr;
+ },
+
+ /**
+ * Parse a user-entered attribute string and apply the resulting
+ * attributes to the node. This operation is undoable.
+ *
+ * @param {String} value
+ * The user-entered value.
+ * @param {DOMNode} attrNode
+ * The attribute editor that created this
+ * set of attributes, used to place new attributes where the
+ * user put them.
+ */
+ _applyAttributes: function (value, attrNode, doMods, undoMods) {
+ let attrs = parseAttributeValues(value, this.doc);
+ for (let attr of attrs) {
+ // Create an attribute editor next to the current attribute if needed.
+ this._createAttribute(attr, attrNode ? attrNode.nextSibling : null);
+ this._saveAttribute(attr.name, undoMods);
+ doMods.setAttribute(attr.name, attr.value);
+ }
+ },
+
+ /**
+ * Saves the current state of the given attribute into an attribute
+ * modification list.
+ */
+ _saveAttribute: function (name, undoMods) {
+ let node = this.node;
+ if (node.hasAttribute(name)) {
+ let oldValue = node.getAttribute(name);
+ undoMods.setAttribute(name, oldValue);
+ } else {
+ undoMods.removeAttribute(name);
+ }
+ },
+
+ /**
+ * Listen to mutations, and when the attribute list is regenerated
+ * try to focus on the attribute after the one that's being edited now.
+ * If the attribute order changes, go to the beginning of the attribute list.
+ */
+ refocusOnEdit: function (attrName, attrNode, direction) {
+ // Only allow one refocus on attribute change at a time, so when there's
+ // more than 1 request in parallel, the last one wins.
+ if (this._editedAttributeObserver) {
+ this.markup.inspector.off("markupmutation", this._editedAttributeObserver);
+ this._editedAttributeObserver = null;
+ }
+
+ let container = this.markup.getContainer(this.node);
+
+ let activeAttrs = [...this.attrList.childNodes]
+ .filter(el => el.style.display != "none");
+ let attributeIndex = activeAttrs.indexOf(attrNode);
+
+ let onMutations = this._editedAttributeObserver = (e, mutations) => {
+ let isDeletedAttribute = false;
+ let isNewAttribute = false;
+
+ for (let mutation of mutations) {
+ let inContainer =
+ this.markup.getContainer(mutation.target) === container;
+ if (!inContainer) {
+ continue;
+ }
+
+ let isOriginalAttribute = mutation.attributeName === attrName;
+
+ isDeletedAttribute = isDeletedAttribute || isOriginalAttribute &&
+ mutation.newValue === null;
+ isNewAttribute = isNewAttribute || mutation.attributeName !== attrName;
+ }
+
+ let isModifiedOrder = isDeletedAttribute && isNewAttribute;
+ this._editedAttributeObserver = null;
+
+ // "Deleted" attributes are merely hidden, so filter them out.
+ let visibleAttrs = [...this.attrList.childNodes]
+ .filter(el => el.style.display != "none");
+ let activeEditor;
+ if (visibleAttrs.length > 0) {
+ if (!direction) {
+ // No direction was given; stay on current attribute.
+ activeEditor = visibleAttrs[attributeIndex];
+ } else if (isModifiedOrder) {
+ // The attribute was renamed, reordering the existing attributes.
+ // So let's go to the beginning of the attribute list for consistency.
+ activeEditor = visibleAttrs[0];
+ } else {
+ let newAttributeIndex;
+ if (isDeletedAttribute) {
+ newAttributeIndex = attributeIndex;
+ } else if (direction == Services.focus.MOVEFOCUS_FORWARD) {
+ newAttributeIndex = attributeIndex + 1;
+ } else if (direction == Services.focus.MOVEFOCUS_BACKWARD) {
+ newAttributeIndex = attributeIndex - 1;
+ }
+
+ // The number of attributes changed (deleted), or we moved through
+ // the array so check we're still within bounds.
+ if (newAttributeIndex >= 0 &&
+ newAttributeIndex <= visibleAttrs.length - 1) {
+ activeEditor = visibleAttrs[newAttributeIndex];
+ }
+ }
+ }
+
+ // Either we have no attributes left,
+ // or we just edited the last attribute and want to move on.
+ if (!activeEditor) {
+ activeEditor = this.newAttr;
+ }
+
+ // Refocus was triggered by tab or shift-tab.
+ // Continue in edit mode.
+ if (direction) {
+ activeEditor.editMode();
+ } else {
+ // Refocus was triggered by enter.
+ // Exit edit mode (but restore focus).
+ let editable = activeEditor === this.newAttr ?
+ activeEditor : activeEditor.querySelector(".editable");
+ editable.focus();
+ }
+
+ this.markup.emit("refocusedonedit");
+ };
+
+ // Start listening for mutations until we find an attributes change
+ // that modifies this attribute.
+ this.markup.inspector.once("markupmutation", onMutations);
+ },
+
+ /**
+ * Called when the tag name editor has is done editing.
+ */
+ onTagEdit: function (newTagName, isCommit) {
+ if (!isCommit ||
+ newTagName.toLowerCase() === this.node.tagName.toLowerCase() ||
+ !("editTagName" in this.markup.walker)) {
+ return;
+ }
+
+ // Changing the tagName removes the node. Make sure the replacing node gets
+ // selected afterwards.
+ this.markup.reselectOnRemoved(this.node, "edittagname");
+ this.markup.walker.editTagName(this.node, newTagName).then(null, () => {
+ // Failed to edit the tag name, cancel the reselection.
+ this.markup.cancelReselectOnRemoved();
+ });
+ },
+
+ destroy: function () {
+ for (let key in this.animationTimers) {
+ clearTimeout(this.animationTimers[key]);
+ }
+ this.animationTimers = null;
+ }
+};
+
+module.exports = ElementEditor;
diff --git a/devtools/client/inspector/markup/views/html-editor.js b/devtools/client/inspector/markup/views/html-editor.js
new file mode 100644
index 000000000..6f99391b6
--- /dev/null
+++ b/devtools/client/inspector/markup/views/html-editor.js
@@ -0,0 +1,180 @@
+/* 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 Editor = require("devtools/client/sourceeditor/editor");
+const Services = require("Services");
+const EventEmitter = require("devtools/shared/event-emitter");
+
+/**
+ * A wrapper around the Editor component, that allows editing of HTML.
+ *
+ * The main functionality this provides around the Editor is the ability
+ * to show/hide/position an editor inplace. It only appends once to the
+ * body, and uses CSS to position the editor. The reason it is done this
+ * way is that the editor is loaded in an iframe, and calling appendChild
+ * causes it to reload.
+ *
+ * Meant to be embedded inside of an HTML page, as in markup.xhtml.
+ *
+ * @param {HTMLDocument} htmlDocument
+ * The document to attach the editor to. Will also use this
+ * document as a basis for listening resize events.
+ */
+function HTMLEditor(htmlDocument) {
+ this.doc = htmlDocument;
+ this.container = this.doc.createElement("div");
+ this.container.className = "html-editor theme-body";
+ this.container.style.display = "none";
+ this.editorInner = this.doc.createElement("div");
+ this.editorInner.className = "html-editor-inner";
+ this.container.appendChild(this.editorInner);
+
+ this.doc.body.appendChild(this.container);
+ this.hide = this.hide.bind(this);
+ this.refresh = this.refresh.bind(this);
+
+ EventEmitter.decorate(this);
+
+ this.doc.defaultView.addEventListener("resize",
+ this.refresh, true);
+
+ let config = {
+ mode: Editor.modes.html,
+ lineWrapping: true,
+ styleActiveLine: false,
+ extraKeys: {},
+ theme: "mozilla markup-view"
+ };
+
+ config.extraKeys[ctrl("Enter")] = this.hide;
+ config.extraKeys.F2 = this.hide;
+ config.extraKeys.Esc = this.hide.bind(this, false);
+
+ this.container.addEventListener("click", this.hide, false);
+ this.editorInner.addEventListener("click", stopPropagation, false);
+ this.editor = new Editor(config);
+
+ this.editor.appendToLocalElement(this.editorInner);
+ this.hide(false);
+}
+
+HTMLEditor.prototype = {
+
+ /**
+ * Need to refresh position by manually setting CSS values, so this will
+ * need to be called on resizes and other sizing changes.
+ */
+ refresh: function () {
+ let element = this._attachedElement;
+
+ if (element) {
+ this.container.style.top = element.offsetTop + "px";
+ this.container.style.left = element.offsetLeft + "px";
+ this.container.style.width = element.offsetWidth + "px";
+ this.container.style.height = element.parentNode.offsetHeight + "px";
+ this.editor.refresh();
+ }
+ },
+
+ /**
+ * Anchor the editor to a particular element.
+ *
+ * @param {DOMNode} element
+ * The element that the editor will be anchored to.
+ * Should belong to the HTMLDocument passed into the constructor.
+ */
+ _attach: function (element) {
+ this._detach();
+ this._attachedElement = element;
+ element.classList.add("html-editor-container");
+ this.refresh();
+ },
+
+ /**
+ * Unanchor the editor from an element.
+ */
+ _detach: function () {
+ if (this._attachedElement) {
+ this._attachedElement.classList.remove("html-editor-container");
+ this._attachedElement = undefined;
+ }
+ },
+
+ /**
+ * Anchor the editor to a particular element, and show the editor.
+ *
+ * @param {DOMNode} element
+ * The element that the editor will be anchored to.
+ * Should belong to the HTMLDocument passed into the constructor.
+ * @param {String} text
+ * Value to set the contents of the editor to
+ * @param {Function} cb
+ * The function to call when hiding
+ */
+ show: function (element, text) {
+ if (this._visible) {
+ return;
+ }
+
+ this._originalValue = text;
+ this.editor.setText(text);
+ this._attach(element);
+ this.container.style.display = "flex";
+ this._visible = true;
+
+ this.editor.refresh();
+ this.editor.focus();
+
+ this.emit("popupshown");
+ },
+
+ /**
+ * Hide the editor, optionally committing the changes
+ *
+ * @param {Boolean} shouldCommit
+ * A change will be committed by default. If this param
+ * strictly equals false, no change will occur.
+ */
+ hide: function (shouldCommit) {
+ if (!this._visible) {
+ return;
+ }
+
+ this.container.style.display = "none";
+ this._detach();
+
+ let newValue = this.editor.getText();
+ let valueHasChanged = this._originalValue !== newValue;
+ let preventCommit = shouldCommit === false || !valueHasChanged;
+ this._originalValue = undefined;
+ this._visible = undefined;
+ this.emit("popuphidden", !preventCommit, newValue);
+ },
+
+ /**
+ * Destroy this object and unbind all event handlers
+ */
+ destroy: function () {
+ this.doc.defaultView.removeEventListener("resize",
+ this.refresh, true);
+ this.container.removeEventListener("click", this.hide, false);
+ this.editorInner.removeEventListener("click", stopPropagation, false);
+
+ this.hide(false);
+ this.container.remove();
+ this.editor.destroy();
+ }
+};
+
+function ctrl(k) {
+ return (Services.appinfo.OS == "Darwin" ? "Cmd-" : "Ctrl-") + k;
+}
+
+function stopPropagation(e) {
+ e.stopPropagation();
+}
+
+module.exports = HTMLEditor;
diff --git a/devtools/client/inspector/markup/views/markup-container.js b/devtools/client/inspector/markup/views/markup-container.js
new file mode 100644
index 000000000..b54157242
--- /dev/null
+++ b/devtools/client/inspector/markup/views/markup-container.js
@@ -0,0 +1,720 @@
+/* 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 {Task} = require("devtools/shared/task");
+const {KeyCodes} = require("devtools/client/shared/keycodes");
+const {flashElementOn, flashElementOff} =
+ require("devtools/client/inspector/markup/utils");
+
+const DRAG_DROP_MIN_INITIAL_DISTANCE = 10;
+
+/**
+ * The main structure for storing a document node in the markup
+ * tree. Manages creation of the editor for the node and
+ * a <ul> for placing child elements, and expansion/collapsing
+ * of the element.
+ *
+ * This should not be instantiated directly, instead use one of:
+ * MarkupReadOnlyContainer
+ * MarkupTextContainer
+ * MarkupElementContainer
+ */
+function MarkupContainer() { }
+
+/**
+ * Unique identifier used to set markup container node id.
+ * @type {Number}
+ */
+let markupContainerID = 0;
+
+MarkupContainer.prototype = {
+ /*
+ * Initialize the MarkupContainer. Should be called while one
+ * of the other contain classes is instantiated.
+ *
+ * @param {MarkupView} markupView
+ * The markup view that owns this container.
+ * @param {NodeFront} node
+ * The node to display.
+ * @param {String} templateID
+ * Which template to render for this container
+ */
+ initialize: function (markupView, node, templateID) {
+ this.markup = markupView;
+ this.node = node;
+ this.undo = this.markup.undo;
+ this.win = this.markup._frame.contentWindow;
+ this.id = "treeitem-" + markupContainerID++;
+ this.htmlElt = this.win.document.documentElement;
+
+ // The template will fill the following properties
+ this.elt = null;
+ this.expander = null;
+ this.tagState = null;
+ this.tagLine = null;
+ this.children = null;
+ this.markup.template(templateID, this);
+ this.elt.container = this;
+
+ this._onMouseDown = this._onMouseDown.bind(this);
+ this._onToggle = this._onToggle.bind(this);
+ this._onMouseUp = this._onMouseUp.bind(this);
+ this._onMouseMove = this._onMouseMove.bind(this);
+ this._onKeyDown = this._onKeyDown.bind(this);
+
+ // Binding event listeners
+ this.elt.addEventListener("mousedown", this._onMouseDown, false);
+ this.win.addEventListener("mouseup", this._onMouseUp, true);
+ this.win.addEventListener("mousemove", this._onMouseMove, true);
+ this.elt.addEventListener("dblclick", this._onToggle, false);
+ if (this.expander) {
+ this.expander.addEventListener("click", this._onToggle, false);
+ }
+
+ // Marking the node as shown or hidden
+ this.updateIsDisplayed();
+ },
+
+ toString: function () {
+ return "[MarkupContainer for " + this.node + "]";
+ },
+
+ isPreviewable: function () {
+ if (this.node.tagName && !this.node.isPseudoElement) {
+ let tagName = this.node.tagName.toLowerCase();
+ let srcAttr = this.editor.getAttributeElement("src");
+ let isImage = tagName === "img" && srcAttr;
+ let isCanvas = tagName === "canvas";
+
+ return isImage || isCanvas;
+ }
+
+ return false;
+ },
+
+ /**
+ * Show whether the element is displayed or not
+ * If an element has the attribute `display: none` or has been hidden with
+ * the H key, it is not displayed (faded in markup view).
+ * Otherwise, it is displayed.
+ */
+ updateIsDisplayed: function () {
+ this.elt.classList.remove("not-displayed");
+ if (!this.node.isDisplayed || this.node.hidden) {
+ this.elt.classList.add("not-displayed");
+ }
+ },
+
+ /**
+ * True if the current node has children. The MarkupView
+ * will set this attribute for the MarkupContainer.
+ */
+ _hasChildren: false,
+
+ get hasChildren() {
+ return this._hasChildren;
+ },
+
+ set hasChildren(value) {
+ this._hasChildren = value;
+ this.updateExpander();
+ },
+
+ /**
+ * A list of all elements with tabindex that are not in container's children.
+ */
+ get focusableElms() {
+ return [...this.tagLine.querySelectorAll("[tabindex]")];
+ },
+
+ /**
+ * An indicator that the container internals are focusable.
+ */
+ get canFocus() {
+ return this._canFocus;
+ },
+
+ /**
+ * Toggle focusable state for container internals.
+ */
+ set canFocus(value) {
+ if (this._canFocus === value) {
+ return;
+ }
+
+ this._canFocus = value;
+
+ if (value) {
+ this.tagLine.addEventListener("keydown", this._onKeyDown, true);
+ this.focusableElms.forEach(elm => elm.setAttribute("tabindex", "0"));
+ } else {
+ this.tagLine.removeEventListener("keydown", this._onKeyDown, true);
+ // Exclude from tab order.
+ this.focusableElms.forEach(elm => elm.setAttribute("tabindex", "-1"));
+ }
+ },
+
+ /**
+ * If conatiner and its contents are focusable, exclude them from tab order,
+ * and, if necessary, remove focus.
+ */
+ clearFocus: function () {
+ if (!this.canFocus) {
+ return;
+ }
+
+ this.canFocus = false;
+ let doc = this.markup.doc;
+
+ if (!doc.activeElement || doc.activeElement === doc.body) {
+ return;
+ }
+
+ let parent = doc.activeElement;
+
+ while (parent && parent !== this.elt) {
+ parent = parent.parentNode;
+ }
+
+ if (parent) {
+ doc.activeElement.blur();
+ }
+ },
+
+ /**
+ * True if the current node can be expanded.
+ */
+ get canExpand() {
+ return this._hasChildren && !this.node.inlineTextChild;
+ },
+
+ /**
+ * True if this is the root <html> element and can't be collapsed.
+ */
+ get mustExpand() {
+ return this.node._parent === this.markup.walker.rootNode;
+ },
+
+ /**
+ * True if current node can be expanded and collapsed.
+ */
+ get showExpander() {
+ return this.canExpand && !this.mustExpand;
+ },
+
+ updateExpander: function () {
+ if (!this.expander) {
+ return;
+ }
+
+ if (this.showExpander) {
+ this.expander.style.visibility = "visible";
+ // Update accessibility expanded state.
+ this.tagLine.setAttribute("aria-expanded", this.expanded);
+ } else {
+ this.expander.style.visibility = "hidden";
+ // No need for accessible expanded state indicator when expander is not
+ // shown.
+ this.tagLine.removeAttribute("aria-expanded");
+ }
+ },
+
+ /**
+ * If current node has no children, ignore them. Otherwise, consider them a
+ * group from the accessibility point of view.
+ */
+ setChildrenRole: function () {
+ this.children.setAttribute("role",
+ this.hasChildren ? "group" : "presentation");
+ },
+
+ /**
+ * Set an appropriate DOM tree depth level for a node and its subtree.
+ */
+ updateLevel: function () {
+ // ARIA level should already be set when container template is rendered.
+ let currentLevel = this.tagLine.getAttribute("aria-level");
+ let newLevel = this.level;
+ if (currentLevel === newLevel) {
+ // If level did not change, ignore this node and its subtree.
+ return;
+ }
+
+ this.tagLine.setAttribute("aria-level", newLevel);
+ let childContainers = this.getChildContainers();
+ if (childContainers) {
+ childContainers.forEach(container => container.updateLevel());
+ }
+ },
+
+ /**
+ * If the node has children, return the list of containers for all these
+ * children.
+ */
+ getChildContainers: function () {
+ if (!this.hasChildren) {
+ return null;
+ }
+
+ return [...this.children.children].filter(node => node.container)
+ .map(node => node.container);
+ },
+
+ /**
+ * True if the node has been visually expanded in the tree.
+ */
+ get expanded() {
+ return !this.elt.classList.contains("collapsed");
+ },
+
+ setExpanded: function (value) {
+ if (!this.expander) {
+ return;
+ }
+
+ if (!this.canExpand) {
+ value = false;
+ }
+ if (this.mustExpand) {
+ value = true;
+ }
+
+ if (value && this.elt.classList.contains("collapsed")) {
+ // Expanding a node means cloning its "inline" closing tag into a new
+ // tag-line that the user can interact with and showing the children.
+ let closingTag = this.elt.querySelector(".close");
+ if (closingTag) {
+ if (!this.closeTagLine) {
+ let line = this.markup.doc.createElement("div");
+ line.classList.add("tag-line");
+ // Closing tag is not important for accessibility.
+ line.setAttribute("role", "presentation");
+
+ let tagState = this.markup.doc.createElement("div");
+ tagState.classList.add("tag-state");
+ line.appendChild(tagState);
+
+ line.appendChild(closingTag.cloneNode(true));
+
+ flashElementOff(line);
+ this.closeTagLine = line;
+ }
+ this.elt.appendChild(this.closeTagLine);
+ }
+
+ this.elt.classList.remove("collapsed");
+ this.expander.setAttribute("open", "");
+ this.hovered = false;
+ this.markup.emit("expanded");
+ } else if (!value) {
+ if (this.closeTagLine) {
+ this.elt.removeChild(this.closeTagLine);
+ this.closeTagLine = undefined;
+ }
+ this.elt.classList.add("collapsed");
+ this.expander.removeAttribute("open");
+ this.markup.emit("collapsed");
+ }
+ if (this.showExpander) {
+ this.tagLine.setAttribute("aria-expanded", this.expanded);
+ }
+ },
+
+ parentContainer: function () {
+ return this.elt.parentNode ? this.elt.parentNode.container : null;
+ },
+
+ /**
+ * Determine tree depth level of a given node. This is used to specify ARIA
+ * level for node tree items and to give them better semantic context.
+ */
+ get level() {
+ let level = 1;
+ let parent = this.node.parentNode();
+ while (parent && parent !== this.markup.walker.rootNode) {
+ level++;
+ parent = parent.parentNode();
+ }
+ return level;
+ },
+
+ _isDragging: false,
+ _dragStartY: 0,
+
+ set isDragging(isDragging) {
+ let rootElt = this.markup.getContainer(this.markup._rootNode).elt;
+ this._isDragging = isDragging;
+ this.markup.isDragging = isDragging;
+ this.tagLine.setAttribute("aria-grabbed", isDragging);
+
+ if (isDragging) {
+ this.htmlElt.classList.add("dragging");
+ this.elt.classList.add("dragging");
+ this.markup.doc.body.classList.add("dragging");
+ rootElt.setAttribute("aria-dropeffect", "move");
+ } else {
+ this.htmlElt.classList.remove("dragging");
+ this.elt.classList.remove("dragging");
+ this.markup.doc.body.classList.remove("dragging");
+ rootElt.setAttribute("aria-dropeffect", "none");
+ }
+ },
+
+ get isDragging() {
+ return this._isDragging;
+ },
+
+ /**
+ * Check if element is draggable.
+ */
+ isDraggable: function () {
+ let tagName = this.node.tagName && this.node.tagName.toLowerCase();
+
+ return !this.node.isPseudoElement &&
+ !this.node.isAnonymous &&
+ !this.node.isDocumentElement &&
+ tagName !== "body" &&
+ tagName !== "head" &&
+ this.win.getSelection().isCollapsed &&
+ this.node.parentNode().tagName !== null;
+ },
+
+ /**
+ * Move keyboard focus to a next/previous focusable element inside container
+ * that is not part of its children (only if current focus is on first or last
+ * element).
+ *
+ * @param {DOMNode} current currently focused element
+ * @param {Boolean} back direction
+ * @return {DOMNode} newly focused element if any
+ */
+ _wrapMoveFocus: function (current, back) {
+ let elms = this.focusableElms;
+ let next;
+ if (back) {
+ if (elms.indexOf(current) === 0) {
+ next = elms[elms.length - 1];
+ next.focus();
+ }
+ } else if (elms.indexOf(current) === elms.length - 1) {
+ next = elms[0];
+ next.focus();
+ }
+ return next;
+ },
+
+ _onKeyDown: function (event) {
+ let {target, keyCode, shiftKey} = event;
+ let isInput = this.markup._isInputOrTextarea(target);
+
+ // Ignore all keystrokes that originated in editors except for when 'Tab' is
+ // pressed.
+ if (isInput && keyCode !== KeyCodes.DOM_VK_TAB) {
+ return;
+ }
+
+ switch (keyCode) {
+ case KeyCodes.DOM_VK_TAB:
+ // Only handle 'Tab' if tabbable element is on the edge (first or last).
+ if (isInput) {
+ // Corresponding tabbable element is editor's next sibling.
+ let next = this._wrapMoveFocus(target.nextSibling, shiftKey);
+ if (next) {
+ event.preventDefault();
+ // Keep the editing state if possible.
+ if (next._editable) {
+ let e = this.markup.doc.createEvent("Event");
+ e.initEvent(next._trigger, true, true);
+ next.dispatchEvent(e);
+ }
+ }
+ } else {
+ let next = this._wrapMoveFocus(target, shiftKey);
+ if (next) {
+ event.preventDefault();
+ }
+ }
+ break;
+ case KeyCodes.DOM_VK_ESCAPE:
+ this.clearFocus();
+ this.markup.getContainer(this.markup._rootNode).elt.focus();
+ if (this.isDragging) {
+ // Escape when dragging is handled by markup view itself.
+ return;
+ }
+ event.preventDefault();
+ break;
+ default:
+ return;
+ }
+ event.stopPropagation();
+ },
+
+ _onMouseDown: function (event) {
+ let {target, button, metaKey, ctrlKey} = event;
+ let isLeftClick = button === 0;
+ let isMiddleClick = button === 1;
+ let isMetaClick = isLeftClick && (metaKey || ctrlKey);
+
+ // The "show more nodes" button already has its onclick, so early return.
+ if (target.nodeName === "button") {
+ return;
+ }
+
+ // target is the MarkupContainer itself.
+ this.hovered = false;
+ this.markup.navigate(this);
+ // Make container tabbable descendants tabbable and focus in.
+ this.canFocus = true;
+ this.focus();
+ event.stopPropagation();
+
+ // Preventing the default behavior will avoid the body to gain focus on
+ // mouseup (through bubbling) when clicking on a non focusable node in the
+ // line. So, if the click happened outside of a focusable element, do
+ // prevent the default behavior, so that the tagname or textcontent gains
+ // focus.
+ if (!target.closest(".editor [tabindex]")) {
+ event.preventDefault();
+ }
+
+ // Follow attribute links if middle or meta click.
+ if (isMiddleClick || isMetaClick) {
+ let link = target.dataset.link;
+ let type = target.dataset.type;
+ // Make container tabbable descendants not tabbable (by default).
+ this.canFocus = false;
+ this.markup.inspector.followAttributeLink(type, link);
+ return;
+ }
+
+ // Start node drag & drop (if the mouse moved, see _onMouseMove).
+ if (isLeftClick && this.isDraggable()) {
+ this._isPreDragging = true;
+ this._dragStartY = event.pageY;
+ }
+ },
+
+ /**
+ * On mouse up, stop dragging.
+ */
+ _onMouseUp: Task.async(function* () {
+ this._isPreDragging = false;
+
+ if (this.isDragging) {
+ this.cancelDragging();
+
+ let dropTargetNodes = this.markup.dropTargetNodes;
+
+ if (!dropTargetNodes) {
+ return;
+ }
+
+ yield this.markup.walker.insertBefore(this.node, dropTargetNodes.parent,
+ dropTargetNodes.nextSibling);
+ this.markup.emit("drop-completed");
+ }
+ }),
+
+ /**
+ * On mouse move, move the dragged element and indicate the drop target.
+ */
+ _onMouseMove: function (event) {
+ // If this is the first move after mousedown, only start dragging after the
+ // mouse has travelled a few pixels and then indicate the start position.
+ let initialDiff = Math.abs(event.pageY - this._dragStartY);
+ if (this._isPreDragging && initialDiff >= DRAG_DROP_MIN_INITIAL_DISTANCE) {
+ this._isPreDragging = false;
+ this.isDragging = true;
+
+ // If this is the last child, use the closing <div.tag-line> of parent as
+ // indicator.
+ let position = this.elt.nextElementSibling ||
+ this.markup.getContainer(this.node.parentNode())
+ .closeTagLine;
+ this.markup.indicateDragTarget(position);
+ }
+
+ if (this.isDragging) {
+ let x = 0;
+ let y = event.pageY - this.win.scrollY;
+
+ // Ensure we keep the dragged element within the markup view.
+ if (y < 0) {
+ y = 0;
+ } else if (y >= this.markup.doc.body.offsetHeight - this.win.scrollY) {
+ y = this.markup.doc.body.offsetHeight - this.win.scrollY - 1;
+ }
+
+ let diff = y - this._dragStartY + this.win.scrollY;
+ this.elt.style.top = diff + "px";
+
+ let el = this.markup.doc.elementFromPoint(x, y);
+ this.markup.indicateDropTarget(el);
+ }
+ },
+
+ cancelDragging: function () {
+ if (!this.isDragging) {
+ return;
+ }
+
+ this._isPreDragging = false;
+ this.isDragging = false;
+ this.elt.style.removeProperty("top");
+ },
+
+ /**
+ * Temporarily flash the container to attract attention.
+ * Used for markup mutations.
+ */
+ flashMutation: function () {
+ if (!this.selected) {
+ flashElementOn(this.tagState, this.editor.elt);
+ if (this._flashMutationTimer) {
+ clearTimeout(this._flashMutationTimer);
+ this._flashMutationTimer = null;
+ }
+ this._flashMutationTimer = setTimeout(() => {
+ flashElementOff(this.tagState, this.editor.elt);
+ }, this.markup.CONTAINER_FLASHING_DURATION);
+ }
+ },
+
+ _hovered: false,
+
+ /**
+ * Highlight the currently hovered tag + its closing tag if necessary
+ * (that is if the tag is expanded)
+ */
+ set hovered(value) {
+ this.tagState.classList.remove("flash-out");
+ this._hovered = value;
+ if (value) {
+ if (!this.selected) {
+ this.tagState.classList.add("theme-bg-darker");
+ }
+ if (this.closeTagLine) {
+ this.closeTagLine.querySelector(".tag-state").classList.add(
+ "theme-bg-darker");
+ }
+ } else {
+ this.tagState.classList.remove("theme-bg-darker");
+ if (this.closeTagLine) {
+ this.closeTagLine.querySelector(".tag-state").classList.remove(
+ "theme-bg-darker");
+ }
+ }
+ },
+
+ /**
+ * True if the container is visible in the markup tree.
+ */
+ get visible() {
+ return this.elt.getBoundingClientRect().height > 0;
+ },
+
+ /**
+ * True if the container is currently selected.
+ */
+ _selected: false,
+
+ get selected() {
+ return this._selected;
+ },
+
+ set selected(value) {
+ this.tagState.classList.remove("flash-out");
+ this._selected = value;
+ this.editor.selected = value;
+ // Markup tree item should have accessible selected state.
+ this.tagLine.setAttribute("aria-selected", value);
+ if (this._selected) {
+ let container = this.markup.getContainer(this.markup._rootNode);
+ if (container) {
+ container.elt.setAttribute("aria-activedescendant", this.id);
+ }
+ this.tagLine.setAttribute("selected", "");
+ this.tagState.classList.add("theme-selected");
+ } else {
+ this.tagLine.removeAttribute("selected");
+ this.tagState.classList.remove("theme-selected");
+ }
+ },
+
+ /**
+ * Update the container's editor to the current state of the
+ * viewed node.
+ */
+ update: function () {
+ if (this.node.pseudoClassLocks.length) {
+ this.elt.classList.add("pseudoclass-locked");
+ } else {
+ this.elt.classList.remove("pseudoclass-locked");
+ }
+
+ if (this.editor.update) {
+ this.editor.update();
+ }
+ },
+
+ /**
+ * Try to put keyboard focus on the current editor.
+ */
+ focus: function () {
+ // Elements with tabindex of -1 are not focusable.
+ let focusable = this.editor.elt.querySelector("[tabindex='0']");
+ if (focusable) {
+ focusable.focus();
+ }
+ },
+
+ _onToggle: function (event) {
+ this.markup.navigate(this);
+ if (this.hasChildren) {
+ this.markup.setNodeExpanded(this.node, !this.expanded, event.altKey);
+ }
+ event.stopPropagation();
+ },
+
+ /**
+ * Get rid of event listeners and references, when the container is no longer
+ * needed
+ */
+ destroy: function () {
+ // Remove event listeners
+ this.elt.removeEventListener("mousedown", this._onMouseDown, false);
+ this.elt.removeEventListener("dblclick", this._onToggle, false);
+ this.tagLine.removeEventListener("keydown", this._onKeyDown, true);
+ if (this.win) {
+ this.win.removeEventListener("mouseup", this._onMouseUp, true);
+ this.win.removeEventListener("mousemove", this._onMouseMove, true);
+ }
+
+ this.win = null;
+ this.htmlElt = null;
+
+ if (this.expander) {
+ this.expander.removeEventListener("click", this._onToggle, false);
+ }
+
+ // Recursively destroy children containers
+ let firstChild = this.children.firstChild;
+ while (firstChild) {
+ // Not all children of a container are containers themselves
+ // ("show more nodes" button is one example)
+ if (firstChild.container) {
+ firstChild.container.destroy();
+ }
+ this.children.removeChild(firstChild);
+ firstChild = this.children.firstChild;
+ }
+
+ this.editor.destroy();
+ }
+};
+
+module.exports = MarkupContainer;
diff --git a/devtools/client/inspector/markup/views/moz.build b/devtools/client/inspector/markup/views/moz.build
new file mode 100644
index 000000000..846bc6a84
--- /dev/null
+++ b/devtools/client/inspector/markup/views/moz.build
@@ -0,0 +1,17 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'element-container.js',
+ 'element-editor.js',
+ 'html-editor.js',
+ 'markup-container.js',
+ 'read-only-container.js',
+ 'read-only-editor.js',
+ 'root-container.js',
+ 'text-container.js',
+ 'text-editor.js',
+)
diff --git a/devtools/client/inspector/markup/views/read-only-container.js b/devtools/client/inspector/markup/views/read-only-container.js
new file mode 100644
index 000000000..fd645baac
--- /dev/null
+++ b/devtools/client/inspector/markup/views/read-only-container.js
@@ -0,0 +1,33 @@
+/* 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 Heritage = require("sdk/core/heritage");
+const ReadOnlyEditor = require("devtools/client/inspector/markup/views/read-only-editor");
+const MarkupContainer = require("devtools/client/inspector/markup/views/markup-container");
+
+/**
+ * An implementation of MarkupContainer for Pseudo Elements,
+ * Doctype nodes, or any other type generic node that doesn't
+ * fit for other editors.
+ * Does not allow any editing, just viewing / selecting.
+ *
+ * @param {MarkupView} markupView
+ * The markup view that owns this container.
+ * @param {NodeFront} node
+ * The node to display.
+ */
+function MarkupReadOnlyContainer(markupView, node) {
+ MarkupContainer.prototype.initialize.call(this, markupView, node,
+ "readonlycontainer");
+
+ this.editor = new ReadOnlyEditor(this, node);
+ this.tagLine.appendChild(this.editor.elt);
+}
+
+MarkupReadOnlyContainer.prototype =
+ Heritage.extend(MarkupContainer.prototype, {});
+
+module.exports = MarkupReadOnlyContainer;
diff --git a/devtools/client/inspector/markup/views/read-only-editor.js b/devtools/client/inspector/markup/views/read-only-editor.js
new file mode 100644
index 000000000..dbc39eeb7
--- /dev/null
+++ b/devtools/client/inspector/markup/views/read-only-editor.js
@@ -0,0 +1,43 @@
+/* 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 nodeConstants = require("devtools/shared/dom-node-constants");
+
+/**
+ * Creates an editor for non-editable nodes.
+ */
+function ReadOnlyEditor(container, node) {
+ this.container = container;
+ this.markup = this.container.markup;
+ this.template = this.markup.template.bind(this.markup);
+ this.elt = null;
+ this.template("generic", this);
+
+ if (node.isPseudoElement) {
+ this.tag.classList.add("theme-fg-color5");
+ this.tag.textContent = node.isBeforePseudoElement ? "::before" : "::after";
+ } else if (node.nodeType == nodeConstants.DOCUMENT_TYPE_NODE) {
+ this.elt.classList.add("comment");
+ this.tag.textContent = node.doctypeString;
+ } else {
+ this.tag.textContent = node.nodeName;
+ }
+}
+
+ReadOnlyEditor.prototype = {
+ destroy: function () {
+ this.elt.remove();
+ },
+
+ /**
+ * Stub method for consistency with ElementEditor.
+ */
+ getInfoAtNode: function () {
+ return null;
+ }
+};
+
+module.exports = ReadOnlyEditor;
diff --git a/devtools/client/inspector/markup/views/root-container.js b/devtools/client/inspector/markup/views/root-container.js
new file mode 100644
index 000000000..ccc918fca
--- /dev/null
+++ b/devtools/client/inspector/markup/views/root-container.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/. */
+
+"use strict";
+
+/**
+ * Dummy container node used for the root document element.
+ */
+function RootContainer(markupView, node) {
+ this.doc = markupView.doc;
+ this.elt = this.doc.createElement("ul");
+ // Root container has tree semantics for accessibility.
+ this.elt.setAttribute("role", "tree");
+ this.elt.setAttribute("tabindex", "0");
+ this.elt.setAttribute("aria-dropeffect", "none");
+ this.elt.container = this;
+ this.children = this.elt;
+ this.node = node;
+ this.toString = () => "[root container]";
+}
+
+RootContainer.prototype = {
+ hasChildren: true,
+ expanded: true,
+ update: function () {},
+ destroy: function () {},
+
+ /**
+ * If the node has children, return the list of containers for all these children.
+ * @return {Array} An array of child containers or null.
+ */
+ getChildContainers: function () {
+ return [...this.children.children].filter(node => node.container)
+ .map(node => node.container);
+ },
+
+ /**
+ * Set the expanded state of the container node.
+ * @param {Boolean} value
+ */
+ setExpanded: function () {},
+
+ /**
+ * Set an appropriate role of the container's children node.
+ */
+ setChildrenRole: function () {},
+
+ /**
+ * Set an appropriate DOM tree depth level for a node and its subtree.
+ */
+ updateLevel: function () {}
+};
+
+module.exports = RootContainer;
diff --git a/devtools/client/inspector/markup/views/text-container.js b/devtools/client/inspector/markup/views/text-container.js
new file mode 100644
index 000000000..357f17778
--- /dev/null
+++ b/devtools/client/inspector/markup/views/text-container.js
@@ -0,0 +1,40 @@
+/* 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 Heritage = require("sdk/core/heritage");
+const nodeConstants = require("devtools/shared/dom-node-constants");
+const TextEditor = require("devtools/client/inspector/markup/views/text-editor");
+const MarkupContainer = require("devtools/client/inspector/markup/views/markup-container");
+
+/**
+ * An implementation of MarkupContainer for text node and comment nodes.
+ * Allows basic text editing in a textarea.
+ *
+ * @param {MarkupView} markupView
+ * The markup view that owns this container.
+ * @param {NodeFront} node
+ * The node to display.
+ * @param {Inspector} inspector
+ * The inspector tool container the markup-view
+ */
+function MarkupTextContainer(markupView, node) {
+ MarkupContainer.prototype.initialize.call(this, markupView, node,
+ "textcontainer");
+
+ if (node.nodeType == nodeConstants.TEXT_NODE) {
+ this.editor = new TextEditor(this, node, "text");
+ } else if (node.nodeType == nodeConstants.COMMENT_NODE) {
+ this.editor = new TextEditor(this, node, "comment");
+ } else {
+ throw new Error("Invalid node for MarkupTextContainer");
+ }
+
+ this.tagLine.appendChild(this.editor.elt);
+}
+
+MarkupTextContainer.prototype = Heritage.extend(MarkupContainer.prototype, {});
+
+module.exports = MarkupTextContainer;
diff --git a/devtools/client/inspector/markup/views/text-editor.js b/devtools/client/inspector/markup/views/text-editor.js
new file mode 100644
index 000000000..f3c83ca87
--- /dev/null
+++ b/devtools/client/inspector/markup/views/text-editor.js
@@ -0,0 +1,109 @@
+/* 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 {getAutocompleteMaxWidth} = require("devtools/client/inspector/markup/utils");
+const {editableField} = require("devtools/client/shared/inplace-editor");
+const {getCssProperties} = require("devtools/shared/fronts/css-properties");
+const {LocalizationHelper} = require("devtools/shared/l10n");
+
+const INSPECTOR_L10N =
+ new LocalizationHelper("devtools/client/locales/inspector.properties");
+
+/**
+ * Creates a simple text editor node, used for TEXT and COMMENT
+ * nodes.
+ *
+ * @param {MarkupContainer} container
+ * The container owning this editor.
+ * @param {DOMNode} node
+ * The node being edited.
+ * @param {String} templateId
+ * The template id to use to build the editor.
+ */
+function TextEditor(container, node, templateId) {
+ this.container = container;
+ this.markup = this.container.markup;
+ this.node = node;
+ this.template = this.markup.template.bind(templateId);
+ this._selected = false;
+
+ this.markup.template(templateId, this);
+
+ editableField({
+ element: this.value,
+ stopOnReturn: true,
+ trigger: "dblclick",
+ multiline: true,
+ maxWidth: () => getAutocompleteMaxWidth(this.value, this.container.elt),
+ trimOutput: false,
+ done: (val, commit) => {
+ if (!commit) {
+ return;
+ }
+ this.node.getNodeValue().then(longstr => {
+ longstr.string().then(oldValue => {
+ longstr.release().then(null, console.error);
+
+ this.container.undo.do(() => {
+ this.node.setNodeValue(val);
+ }, () => {
+ this.node.setNodeValue(oldValue);
+ });
+ });
+ });
+ },
+ cssProperties: getCssProperties(this.markup.toolbox),
+ contextMenu: this.markup.inspector.onTextBoxContextMenu
+ });
+
+ this.update();
+}
+
+TextEditor.prototype = {
+ get selected() {
+ return this._selected;
+ },
+
+ set selected(value) {
+ if (value === this._selected) {
+ return;
+ }
+ this._selected = value;
+ this.update();
+ },
+
+ update: function () {
+ let longstr = null;
+ this.node.getNodeValue().then(ret => {
+ longstr = ret;
+ return longstr.string();
+ }).then(str => {
+ longstr.release().then(null, console.error);
+ this.value.textContent = str;
+
+ let isWhitespace = !/[^\s]/.exec(str);
+ this.value.classList.toggle("whitespace", isWhitespace);
+
+ let chars = str.replace(/\n/g, "âŽ")
+ .replace(/\t/g, "⇥")
+ .replace(/ /g, "â—¦");
+ this.value.setAttribute("title", isWhitespace
+ ? INSPECTOR_L10N.getFormatStr("markupView.whitespaceOnly", chars)
+ : "");
+ }).then(null, console.error);
+ },
+
+ destroy: function () {},
+
+ /**
+ * Stub method for consistency with ElementEditor.
+ */
+ getInfoAtNode: function () {
+ return null;
+ }
+};
+
+module.exports = TextEditor;
diff --git a/devtools/client/inspector/moz.build b/devtools/client/inspector/moz.build
new file mode 100644
index 000000000..bdf3e3887
--- /dev/null
+++ b/devtools/client/inspector/moz.build
@@ -0,0 +1,23 @@
+# 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/.
+
+DIRS += [
+ 'components',
+ 'computed',
+ 'fonts',
+ 'layout',
+ 'markup',
+ 'rules',
+ 'shared'
+]
+
+DevToolsModules(
+ 'breadcrumbs.js',
+ 'inspector-commands.js',
+ 'inspector-search.js',
+ 'panel.js',
+ 'toolsidebar.js',
+)
+
+BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
diff --git a/devtools/client/inspector/panel.js b/devtools/client/inspector/panel.js
new file mode 100644
index 000000000..7f733491b
--- /dev/null
+++ b/devtools/client/inspector/panel.js
@@ -0,0 +1,19 @@
+/* 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";
+
+function InspectorPanel(iframeWindow, toolbox) {
+ this._inspector = new iframeWindow.Inspector(toolbox);
+}
+InspectorPanel.prototype = {
+ open() {
+ return this._inspector.init();
+ },
+
+ destroy() {
+ return this._inspector.destroy();
+ }
+};
+exports.InspectorPanel = InspectorPanel;
diff --git a/devtools/client/inspector/rules/models/element-style.js b/devtools/client/inspector/rules/models/element-style.js
new file mode 100644
index 000000000..7f015ba08
--- /dev/null
+++ b/devtools/client/inspector/rules/models/element-style.js
@@ -0,0 +1,412 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set 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 {Rule} = require("devtools/client/inspector/rules/models/rule");
+const {promiseWarn} = require("devtools/client/inspector/shared/utils");
+const {ELEMENT_STYLE} = require("devtools/shared/specs/styles");
+const {getCssProperties} = require("devtools/shared/fronts/css-properties");
+
+/**
+ * ElementStyle is responsible for the following:
+ * Keeps track of which properties are overridden.
+ * Maintains a list of Rule objects for a given element.
+ *
+ * @param {Element} element
+ * The element whose style we are viewing.
+ * @param {CssRuleView} ruleView
+ * The instance of the rule-view panel.
+ * @param {Object} store
+ * The ElementStyle can use this object to store metadata
+ * that might outlast the rule view, particularly the current
+ * set of disabled properties.
+ * @param {PageStyleFront} pageStyle
+ * Front for the page style actor that will be providing
+ * the style information.
+ * @param {Boolean} showUserAgentStyles
+ * Should user agent styles be inspected?
+ */
+function ElementStyle(element, ruleView, store, pageStyle,
+ showUserAgentStyles) {
+ this.element = element;
+ this.ruleView = ruleView;
+ this.store = store || {};
+ this.pageStyle = pageStyle;
+ this.showUserAgentStyles = showUserAgentStyles;
+ this.rules = [];
+ this.cssProperties = getCssProperties(this.ruleView.inspector.toolbox);
+
+ // We don't want to overwrite this.store.userProperties so we only create it
+ // if it doesn't already exist.
+ if (!("userProperties" in this.store)) {
+ this.store.userProperties = new UserProperties();
+ }
+
+ if (!("disabled" in this.store)) {
+ this.store.disabled = new WeakMap();
+ }
+}
+
+ElementStyle.prototype = {
+ // The element we're looking at.
+ element: null,
+
+ destroy: function () {
+ if (this.destroyed) {
+ return;
+ }
+ this.destroyed = true;
+
+ for (let rule of this.rules) {
+ if (rule.editor) {
+ rule.editor.destroy();
+ }
+ }
+ },
+
+ /**
+ * Called by the Rule object when it has been changed through the
+ * setProperty* methods.
+ */
+ _changed: function () {
+ if (this.onChanged) {
+ this.onChanged();
+ }
+ },
+
+ /**
+ * Refresh the list of rules to be displayed for the active element.
+ * Upon completion, this.rules[] will hold a list of Rule objects.
+ *
+ * Returns a promise that will be resolved when the elementStyle is
+ * ready.
+ */
+ populate: function () {
+ let populated = this.pageStyle.getApplied(this.element, {
+ inherited: true,
+ matchedSelectors: true,
+ filter: this.showUserAgentStyles ? "ua" : undefined,
+ }).then(entries => {
+ if (this.destroyed) {
+ return promise.resolve(undefined);
+ }
+
+ if (this.populated !== populated) {
+ // Don't care anymore.
+ return promise.resolve(undefined);
+ }
+
+ // Store the current list of rules (if any) during the population
+ // process. They will be reused if possible.
+ let existingRules = this.rules;
+
+ this.rules = [];
+
+ for (let entry of entries) {
+ this._maybeAddRule(entry, existingRules);
+ }
+
+ // Mark overridden computed styles.
+ this.markOverriddenAll();
+
+ this._sortRulesForPseudoElement();
+
+ // We're done with the previous list of rules.
+ for (let r of existingRules) {
+ if (r && r.editor) {
+ r.editor.destroy();
+ }
+ }
+
+ return undefined;
+ }).then(null, e => {
+ // populate is often called after a setTimeout,
+ // the connection may already be closed.
+ if (this.destroyed) {
+ return promise.resolve(undefined);
+ }
+ return promiseWarn(e);
+ });
+ this.populated = populated;
+ return this.populated;
+ },
+
+ /**
+ * Put pseudo elements in front of others.
+ */
+ _sortRulesForPseudoElement: function () {
+ this.rules = this.rules.sort((a, b) => {
+ return (a.pseudoElement || "z") > (b.pseudoElement || "z");
+ });
+ },
+
+ /**
+ * Add a rule if it's one we care about. Filters out duplicates and
+ * inherited styles with no inherited properties.
+ *
+ * @param {Object} options
+ * Options for creating the Rule, see the Rule constructor.
+ * @param {Array} existingRules
+ * Rules to reuse if possible. If a rule is reused, then it
+ * it will be deleted from this array.
+ * @return {Boolean} true if we added the rule.
+ */
+ _maybeAddRule: function (options, existingRules) {
+ // If we've already included this domRule (for example, when a
+ // common selector is inherited), ignore it.
+ if (options.rule &&
+ this.rules.some(rule => rule.domRule === options.rule)) {
+ return false;
+ }
+
+ if (options.system) {
+ return false;
+ }
+
+ let rule = null;
+
+ // If we're refreshing and the rule previously existed, reuse the
+ // Rule object.
+ if (existingRules) {
+ let ruleIndex = existingRules.findIndex((r) => r.matches(options));
+ if (ruleIndex >= 0) {
+ rule = existingRules[ruleIndex];
+ rule.refresh(options);
+ existingRules.splice(ruleIndex, 1);
+ }
+ }
+
+ // If this is a new rule, create its Rule object.
+ if (!rule) {
+ rule = new Rule(this, options);
+ }
+
+ // Ignore inherited rules with no visible properties.
+ if (options.inherited && !rule.hasAnyVisibleProperties()) {
+ return false;
+ }
+
+ this.rules.push(rule);
+ return true;
+ },
+
+ /**
+ * Calls markOverridden with all supported pseudo elements
+ */
+ markOverriddenAll: function () {
+ this.markOverridden();
+ for (let pseudo of this.cssProperties.pseudoElements) {
+ this.markOverridden(pseudo);
+ }
+ },
+
+ /**
+ * Mark the properties listed in this.rules for a given pseudo element
+ * with an overridden flag if an earlier property overrides it.
+ *
+ * @param {String} pseudo
+ * Which pseudo element to flag as overridden.
+ * Empty string or undefined will default to no pseudo element.
+ */
+ markOverridden: function (pseudo = "") {
+ // Gather all the text properties applied by these rules, ordered
+ // from more- to less-specific. Text properties from keyframes rule are
+ // excluded from being marked as overridden since a number of criteria such
+ // as time, and animation overlay are required to be check in order to
+ // determine if the property is overridden.
+ let textProps = [];
+ for (let rule of this.rules) {
+ if ((rule.matchedSelectors.length > 0 ||
+ rule.domRule.type === ELEMENT_STYLE) &&
+ rule.pseudoElement === pseudo && !rule.keyframes) {
+ for (let textProp of rule.textProps.slice(0).reverse()) {
+ if (textProp.enabled) {
+ textProps.push(textProp);
+ }
+ }
+ }
+ }
+
+ // Gather all the computed properties applied by those text
+ // properties.
+ let computedProps = [];
+ for (let textProp of textProps) {
+ computedProps = computedProps.concat(textProp.computed);
+ }
+
+ // Walk over the computed properties. As we see a property name
+ // for the first time, mark that property's name as taken by this
+ // property.
+ //
+ // If we come across a property whose name is already taken, check
+ // its priority against the property that was found first:
+ //
+ // If the new property is a higher priority, mark the old
+ // property overridden and mark the property name as taken by
+ // the new property.
+ //
+ // If the new property is a lower or equal priority, mark it as
+ // overridden.
+ //
+ // _overriddenDirty will be set on each prop, indicating whether its
+ // dirty status changed during this pass.
+ let taken = {};
+ for (let computedProp of computedProps) {
+ let earlier = taken[computedProp.name];
+
+ // Prevent -webkit-gradient from being selected after unchecking
+ // linear-gradient in this case:
+ // -moz-linear-gradient: ...;
+ // -webkit-linear-gradient: ...;
+ // linear-gradient: ...;
+ if (!computedProp.textProp.isValid()) {
+ computedProp.overridden = true;
+ continue;
+ }
+ let overridden;
+ if (earlier &&
+ computedProp.priority === "important" &&
+ earlier.priority !== "important" &&
+ (earlier.textProp.rule.inherited ||
+ !computedProp.textProp.rule.inherited)) {
+ // New property is higher priority. Mark the earlier property
+ // overridden (which will reverse its dirty state).
+ earlier._overriddenDirty = !earlier._overriddenDirty;
+ earlier.overridden = true;
+ overridden = false;
+ } else {
+ overridden = !!earlier;
+ }
+
+ computedProp._overriddenDirty =
+ (!!computedProp.overridden !== overridden);
+ computedProp.overridden = overridden;
+ if (!computedProp.overridden && computedProp.textProp.enabled) {
+ taken[computedProp.name] = computedProp;
+ }
+ }
+
+ // For each TextProperty, mark it overridden if all of its
+ // computed properties are marked overridden. Update the text
+ // property's associated editor, if any. This will clear the
+ // _overriddenDirty state on all computed properties.
+ for (let textProp of textProps) {
+ // _updatePropertyOverridden will return true if the
+ // overridden state has changed for the text property.
+ if (this._updatePropertyOverridden(textProp)) {
+ textProp.updateEditor();
+ }
+ }
+ },
+
+ /**
+ * Mark a given TextProperty as overridden or not depending on the
+ * state of its computed properties. Clears the _overriddenDirty state
+ * on all computed properties.
+ *
+ * @param {TextProperty} prop
+ * The text property to update.
+ * @return {Boolean} true if the TextProperty's overridden state (or any of
+ * its computed properties overridden state) changed.
+ */
+ _updatePropertyOverridden: function (prop) {
+ let overridden = true;
+ let dirty = false;
+ for (let computedProp of prop.computed) {
+ if (!computedProp.overridden) {
+ overridden = false;
+ }
+ dirty = computedProp._overriddenDirty || dirty;
+ delete computedProp._overriddenDirty;
+ }
+
+ dirty = (!!prop.overridden !== overridden) || dirty;
+ prop.overridden = overridden;
+ return dirty;
+ }
+};
+
+/**
+ * Store of CSSStyleDeclarations mapped to properties that have been changed by
+ * the user.
+ */
+function UserProperties() {
+ this.map = new Map();
+}
+
+UserProperties.prototype = {
+ /**
+ * Get a named property for a given CSSStyleDeclaration.
+ *
+ * @param {CSSStyleDeclaration} style
+ * The CSSStyleDeclaration against which the property is mapped.
+ * @param {String} name
+ * The name of the property to get.
+ * @param {String} value
+ * Default value.
+ * @return {String}
+ * The property value if it has previously been set by the user, null
+ * otherwise.
+ */
+ getProperty: function (style, name, value) {
+ let key = this.getKey(style);
+ let entry = this.map.get(key, null);
+
+ if (entry && name in entry) {
+ return entry[name];
+ }
+ return value;
+ },
+
+ /**
+ * Set a named property for a given CSSStyleDeclaration.
+ *
+ * @param {CSSStyleDeclaration} style
+ * The CSSStyleDeclaration against which the property is to be mapped.
+ * @param {String} bame
+ * The name of the property to set.
+ * @param {String} userValue
+ * The value of the property to set.
+ */
+ setProperty: function (style, bame, userValue) {
+ let key = this.getKey(style, bame);
+ let entry = this.map.get(key, null);
+
+ if (entry) {
+ entry[bame] = userValue;
+ } else {
+ let props = {};
+ props[bame] = userValue;
+ this.map.set(key, props);
+ }
+ },
+
+ /**
+ * Check whether a named property for a given CSSStyleDeclaration is stored.
+ *
+ * @param {CSSStyleDeclaration} style
+ * The CSSStyleDeclaration against which the property would be mapped.
+ * @param {String} name
+ * The name of the property to check.
+ */
+ contains: function (style, name) {
+ let key = this.getKey(style, name);
+ let entry = this.map.get(key, null);
+ return !!entry && name in entry;
+ },
+
+ getKey: function (style, name) {
+ return style.actorID + ":" + name;
+ },
+
+ clear: function () {
+ this.map.clear();
+ }
+};
+
+exports.ElementStyle = ElementStyle;
diff --git a/devtools/client/inspector/rules/models/moz.build b/devtools/client/inspector/rules/models/moz.build
new file mode 100644
index 000000000..1c5c0f89f
--- /dev/null
+++ b/devtools/client/inspector/rules/models/moz.build
@@ -0,0 +1,11 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'element-style.js',
+ 'rule.js',
+ 'text-property.js',
+)
diff --git a/devtools/client/inspector/rules/models/rule.js b/devtools/client/inspector/rules/models/rule.js
new file mode 100644
index 000000000..1a3fa057a
--- /dev/null
+++ b/devtools/client/inspector/rules/models/rule.js
@@ -0,0 +1,686 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set 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 CssLogic = require("devtools/shared/inspector/css-logic");
+const {ELEMENT_STYLE} = require("devtools/shared/specs/styles");
+const {TextProperty} =
+ require("devtools/client/inspector/rules/models/text-property");
+const {promiseWarn} = require("devtools/client/inspector/shared/utils");
+const {parseDeclarations} = require("devtools/shared/css/parsing-utils");
+const Services = require("Services");
+
+const STYLE_INSPECTOR_PROPERTIES = "devtools/shared/locales/styleinspector.properties";
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const STYLE_INSPECTOR_L10N = new LocalizationHelper(STYLE_INSPECTOR_PROPERTIES);
+
+/**
+ * Rule is responsible for the following:
+ * Manages a single style declaration or rule.
+ * Applies changes to the properties in a rule.
+ * Maintains a list of TextProperty objects.
+ *
+ * @param {ElementStyle} elementStyle
+ * The ElementStyle to which this rule belongs.
+ * @param {Object} options
+ * The information used to construct this rule. Properties include:
+ * rule: A StyleRuleActor
+ * inherited: An element this rule was inherited from. If omitted,
+ * the rule applies directly to the current element.
+ * isSystem: Is this a user agent style?
+ * isUnmatched: True if the rule does not match the current selected
+ * element, otherwise, false.
+ */
+function Rule(elementStyle, options) {
+ this.elementStyle = elementStyle;
+ this.domRule = options.rule || null;
+ this.style = options.rule;
+ this.matchedSelectors = options.matchedSelectors || [];
+ this.pseudoElement = options.pseudoElement || "";
+
+ this.isSystem = options.isSystem;
+ this.isUnmatched = options.isUnmatched || false;
+ this.inherited = options.inherited || null;
+ this.keyframes = options.keyframes || null;
+ this._modificationDepth = 0;
+
+ if (this.domRule && this.domRule.mediaText) {
+ this.mediaText = this.domRule.mediaText;
+ }
+
+ this.cssProperties = this.elementStyle.ruleView.cssProperties;
+
+ // Populate the text properties with the style's current authoredText
+ // value, and add in any disabled properties from the store.
+ this.textProps = this._getTextProperties();
+ this.textProps = this.textProps.concat(this._getDisabledProperties());
+}
+
+Rule.prototype = {
+ mediaText: "",
+
+ get title() {
+ let title = CssLogic.shortSource(this.sheet);
+ if (this.domRule.type !== ELEMENT_STYLE && this.ruleLine > 0) {
+ title += ":" + this.ruleLine;
+ }
+
+ return title + (this.mediaText ? " @media " + this.mediaText : "");
+ },
+
+ get inheritedSource() {
+ if (this._inheritedSource) {
+ return this._inheritedSource;
+ }
+ this._inheritedSource = "";
+ if (this.inherited) {
+ let eltText = this.inherited.displayName;
+ if (this.inherited.id) {
+ eltText += "#" + this.inherited.id;
+ }
+ this._inheritedSource =
+ STYLE_INSPECTOR_L10N.getFormatStr("rule.inheritedFrom", eltText);
+ }
+ return this._inheritedSource;
+ },
+
+ get keyframesName() {
+ if (this._keyframesName) {
+ return this._keyframesName;
+ }
+ this._keyframesName = "";
+ if (this.keyframes) {
+ this._keyframesName =
+ STYLE_INSPECTOR_L10N.getFormatStr("rule.keyframe", this.keyframes.name);
+ }
+ return this._keyframesName;
+ },
+
+ get selectorText() {
+ return this.domRule.selectors ? this.domRule.selectors.join(", ") :
+ CssLogic.l10n("rule.sourceElement");
+ },
+
+ /**
+ * The rule's stylesheet.
+ */
+ get sheet() {
+ return this.domRule ? this.domRule.parentStyleSheet : null;
+ },
+
+ /**
+ * The rule's line within a stylesheet
+ */
+ get ruleLine() {
+ return this.domRule ? this.domRule.line : "";
+ },
+
+ /**
+ * The rule's column within a stylesheet
+ */
+ get ruleColumn() {
+ return this.domRule ? this.domRule.column : null;
+ },
+
+ /**
+ * Get display name for this rule based on the original source
+ * for this rule's style sheet.
+ *
+ * @return {Promise}
+ * Promise which resolves with location as an object containing
+ * both the full and short version of the source string.
+ */
+ getOriginalSourceStrings: function () {
+ return this.domRule.getOriginalLocation().then(({href,
+ line, mediaText}) => {
+ let mediaString = mediaText ? " @" + mediaText : "";
+ let linePart = line > 0 ? (":" + line) : "";
+
+ let sourceStrings = {
+ full: (href || CssLogic.l10n("rule.sourceInline")) + linePart +
+ mediaString,
+ short: CssLogic.shortSource({href: href}) + linePart + mediaString
+ };
+
+ return sourceStrings;
+ });
+ },
+
+ /**
+ * Returns true if the rule matches the creation options
+ * specified.
+ *
+ * @param {Object} options
+ * Creation options. See the Rule constructor for documentation.
+ */
+ matches: function (options) {
+ return this.style === options.rule;
+ },
+
+ /**
+ * Create a new TextProperty to include in the rule.
+ *
+ * @param {String} name
+ * The text property name (such as "background" or "border-top").
+ * @param {String} value
+ * The property's value (not including priority).
+ * @param {String} priority
+ * The property's priority (either "important" or an empty string).
+ * @param {Boolean} enabled
+ * True if the property should be enabled.
+ * @param {TextProperty} siblingProp
+ * Optional, property next to which the new property will be added.
+ */
+ createProperty: function (name, value, priority, enabled, siblingProp) {
+ let prop = new TextProperty(this, name, value, priority, enabled);
+
+ let ind;
+ if (siblingProp) {
+ ind = this.textProps.indexOf(siblingProp) + 1;
+ this.textProps.splice(ind, 0, prop);
+ } else {
+ ind = this.textProps.length;
+ this.textProps.push(prop);
+ }
+
+ this.applyProperties((modifications) => {
+ modifications.createProperty(ind, name, value, priority, enabled);
+ // Now that the rule has been updated, the server might have given us data
+ // that changes the state of the property. Update it now.
+ prop.updateEditor();
+ });
+
+ return prop;
+ },
+
+ /**
+ * Helper function for applyProperties that is called when the actor
+ * does not support as-authored styles. Store disabled properties
+ * in the element style's store.
+ */
+ _applyPropertiesNoAuthored: function (modifications) {
+ this.elementStyle.markOverriddenAll();
+
+ let disabledProps = [];
+
+ for (let prop of this.textProps) {
+ if (prop.invisible) {
+ continue;
+ }
+ if (!prop.enabled) {
+ disabledProps.push({
+ name: prop.name,
+ value: prop.value,
+ priority: prop.priority
+ });
+ continue;
+ }
+ if (prop.value.trim() === "") {
+ continue;
+ }
+
+ modifications.setProperty(-1, prop.name, prop.value, prop.priority);
+
+ prop.updateComputed();
+ }
+
+ // Store disabled properties in the disabled store.
+ let disabled = this.elementStyle.store.disabled;
+ if (disabledProps.length > 0) {
+ disabled.set(this.style, disabledProps);
+ } else {
+ disabled.delete(this.style);
+ }
+
+ return modifications.apply().then(() => {
+ let cssProps = {};
+ // Note that even though StyleRuleActors normally provide parsed
+ // declarations already, _applyPropertiesNoAuthored is only used when
+ // connected to older backend that do not provide them. So parse here.
+ for (let cssProp of parseDeclarations(this.cssProperties.isKnown,
+ this.style.authoredText)) {
+ cssProps[cssProp.name] = cssProp;
+ }
+
+ for (let textProp of this.textProps) {
+ if (!textProp.enabled) {
+ continue;
+ }
+ let cssProp = cssProps[textProp.name];
+
+ if (!cssProp) {
+ cssProp = {
+ name: textProp.name,
+ value: "",
+ priority: ""
+ };
+ }
+
+ textProp.priority = cssProp.priority;
+ }
+ });
+ },
+
+ /**
+ * A helper for applyProperties that applies properties in the "as
+ * authored" case; that is, when the StyleRuleActor supports
+ * setRuleText.
+ */
+ _applyPropertiesAuthored: function (modifications) {
+ return modifications.apply().then(() => {
+ // The rewriting may have required some other property values to
+ // change, e.g., to insert some needed terminators. Update the
+ // relevant properties here.
+ for (let index in modifications.changedDeclarations) {
+ let newValue = modifications.changedDeclarations[index];
+ this.textProps[index].noticeNewValue(newValue);
+ }
+ // Recompute and redisplay the computed properties.
+ for (let prop of this.textProps) {
+ if (!prop.invisible && prop.enabled) {
+ prop.updateComputed();
+ prop.updateEditor();
+ }
+ }
+ });
+ },
+
+ /**
+ * Reapply all the properties in this rule, and update their
+ * computed styles. Will re-mark overridden properties. Sets the
+ * |_applyingModifications| property to a promise which will resolve
+ * when the edit has completed.
+ *
+ * @param {Function} modifier a function that takes a RuleModificationList
+ * (or RuleRewriter) as an argument and that modifies it
+ * to apply the desired edit
+ * @return {Promise} a promise which will resolve when the edit
+ * is complete
+ */
+ applyProperties: function (modifier) {
+ // If there is already a pending modification, we have to wait
+ // until it settles before applying the next modification.
+ let resultPromise =
+ promise.resolve(this._applyingModifications).then(() => {
+ let modifications = this.style.startModifyingProperties(
+ this.cssProperties);
+ modifier(modifications);
+ if (this.style.canSetRuleText) {
+ return this._applyPropertiesAuthored(modifications);
+ }
+ return this._applyPropertiesNoAuthored(modifications);
+ }).then(() => {
+ this.elementStyle.markOverriddenAll();
+
+ if (resultPromise === this._applyingModifications) {
+ this._applyingModifications = null;
+ this.elementStyle._changed();
+ }
+ }).catch(promiseWarn);
+
+ this._applyingModifications = resultPromise;
+ return resultPromise;
+ },
+
+ /**
+ * Renames a property.
+ *
+ * @param {TextProperty} property
+ * The property to rename.
+ * @param {String} name
+ * The new property name (such as "background" or "border-top").
+ */
+ setPropertyName: function (property, name) {
+ if (name === property.name) {
+ return;
+ }
+
+ let oldName = property.name;
+ property.name = name;
+ let index = this.textProps.indexOf(property);
+ this.applyProperties((modifications) => {
+ modifications.renameProperty(index, oldName, name);
+ });
+ },
+
+ /**
+ * Sets the value and priority of a property, then reapply all properties.
+ *
+ * @param {TextProperty} property
+ * The property to manipulate.
+ * @param {String} value
+ * The property's value (not including priority).
+ * @param {String} priority
+ * The property's priority (either "important" or an empty string).
+ */
+ setPropertyValue: function (property, value, priority) {
+ if (value === property.value && priority === property.priority) {
+ return;
+ }
+
+ property.value = value;
+ property.priority = priority;
+
+ let index = this.textProps.indexOf(property);
+ this.applyProperties((modifications) => {
+ modifications.setProperty(index, property.name, value, priority);
+ });
+ },
+
+ /**
+ * Just sets the value and priority of a property, in order to preview its
+ * effect on the content document.
+ *
+ * @param {TextProperty} property
+ * The property which value will be previewed
+ * @param {String} value
+ * The value to be used for the preview
+ * @param {String} priority
+ * The property's priority (either "important" or an empty string).
+ */
+ previewPropertyValue: function (property, value, priority) {
+ let modifications = this.style.startModifyingProperties(this.cssProperties);
+ modifications.setProperty(this.textProps.indexOf(property),
+ property.name, value, priority);
+ modifications.apply().then(() => {
+ // Ensure dispatching a ruleview-changed event
+ // also for previews
+ this.elementStyle._changed();
+ });
+ },
+
+ /**
+ * Disables or enables given TextProperty.
+ *
+ * @param {TextProperty} property
+ * The property to enable/disable
+ * @param {Boolean} value
+ */
+ setPropertyEnabled: function (property, value) {
+ if (property.enabled === !!value) {
+ return;
+ }
+ property.enabled = !!value;
+ let index = this.textProps.indexOf(property);
+ this.applyProperties((modifications) => {
+ modifications.setPropertyEnabled(index, property.name, property.enabled);
+ });
+ },
+
+ /**
+ * Remove a given TextProperty from the rule and update the rule
+ * accordingly.
+ *
+ * @param {TextProperty} property
+ * The property to be removed
+ */
+ removeProperty: function (property) {
+ let index = this.textProps.indexOf(property);
+ this.textProps.splice(index, 1);
+ // Need to re-apply properties in case removing this TextProperty
+ // exposes another one.
+ this.applyProperties((modifications) => {
+ modifications.removeProperty(index, property.name);
+ });
+ },
+
+ /**
+ * Get the list of TextProperties from the style. Needs
+ * to parse the style's authoredText.
+ */
+ _getTextProperties: function () {
+ let textProps = [];
+ let store = this.elementStyle.store;
+
+ // Starting with FF49, StyleRuleActors provide parsed declarations.
+ let props = this.style.declarations;
+ if (!props.length) {
+ props = parseDeclarations(this.cssProperties.isKnown,
+ this.style.authoredText, true);
+ }
+
+ for (let prop of props) {
+ let name = prop.name;
+ // If the authored text has an invalid property, it will show up
+ // as nameless. Skip these as we don't currently have a good
+ // way to display them.
+ if (!name) {
+ continue;
+ }
+ // In an inherited rule, we only show inherited properties.
+ // However, we must keep all properties in order for rule
+ // rewriting to work properly. So, compute the "invisible"
+ // property here.
+ let invisible = this.inherited && !this.cssProperties.isInherited(name);
+ let value = store.userProperties.getProperty(this.style, name,
+ prop.value);
+ let textProp = new TextProperty(this, name, value, prop.priority,
+ !("commentOffsets" in prop),
+ invisible);
+ textProps.push(textProp);
+ }
+
+ return textProps;
+ },
+
+ /**
+ * Return the list of disabled properties from the store for this rule.
+ */
+ _getDisabledProperties: function () {
+ let store = this.elementStyle.store;
+
+ // Include properties from the disabled property store, if any.
+ let disabledProps = store.disabled.get(this.style);
+ if (!disabledProps) {
+ return [];
+ }
+
+ let textProps = [];
+
+ for (let prop of disabledProps) {
+ let value = store.userProperties.getProperty(this.style, prop.name,
+ prop.value);
+ let textProp = new TextProperty(this, prop.name, value, prop.priority);
+ textProp.enabled = false;
+ textProps.push(textProp);
+ }
+
+ return textProps;
+ },
+
+ /**
+ * Reread the current state of the rules and rebuild text
+ * properties as needed.
+ */
+ refresh: function (options) {
+ this.matchedSelectors = options.matchedSelectors || [];
+ let newTextProps = this._getTextProperties();
+
+ // Update current properties for each property present on the style.
+ // This will mark any touched properties with _visited so we
+ // can detect properties that weren't touched (because they were
+ // removed from the style).
+ // Also keep track of properties that didn't exist in the current set
+ // of properties.
+ let brandNewProps = [];
+ for (let newProp of newTextProps) {
+ if (!this._updateTextProperty(newProp)) {
+ brandNewProps.push(newProp);
+ }
+ }
+
+ // Refresh editors and disabled state for all the properties that
+ // were updated.
+ for (let prop of this.textProps) {
+ // Properties that weren't touched during the update
+ // process must no longer exist on the node. Mark them disabled.
+ if (!prop._visited) {
+ prop.enabled = false;
+ prop.updateEditor();
+ } else {
+ delete prop._visited;
+ }
+ }
+
+ // Add brand new properties.
+ this.textProps = this.textProps.concat(brandNewProps);
+
+ // Refresh the editor if one already exists.
+ if (this.editor) {
+ this.editor.populate();
+ }
+ },
+
+ /**
+ * Update the current TextProperties that match a given property
+ * from the authoredText. Will choose one existing TextProperty to update
+ * with the new property's value, and will disable all others.
+ *
+ * When choosing the best match to reuse, properties will be chosen
+ * by assigning a rank and choosing the highest-ranked property:
+ * Name, value, and priority match, enabled. (6)
+ * Name, value, and priority match, disabled. (5)
+ * Name and value match, enabled. (4)
+ * Name and value match, disabled. (3)
+ * Name matches, enabled. (2)
+ * Name matches, disabled. (1)
+ *
+ * If no existing properties match the property, nothing happens.
+ *
+ * @param {TextProperty} newProp
+ * The current version of the property, as parsed from the
+ * authoredText in Rule._getTextProperties().
+ * @return {Boolean} true if a property was updated, false if no properties
+ * were updated.
+ */
+ _updateTextProperty: function (newProp) {
+ let match = { rank: 0, prop: null };
+
+ for (let prop of this.textProps) {
+ if (prop.name !== newProp.name) {
+ continue;
+ }
+
+ // Mark this property visited.
+ prop._visited = true;
+
+ // Start at rank 1 for matching name.
+ let rank = 1;
+
+ // Value and Priority matches add 2 to the rank.
+ // Being enabled adds 1. This ranks better matches higher,
+ // with priority breaking ties.
+ if (prop.value === newProp.value) {
+ rank += 2;
+ if (prop.priority === newProp.priority) {
+ rank += 2;
+ }
+ }
+
+ if (prop.enabled) {
+ rank += 1;
+ }
+
+ if (rank > match.rank) {
+ if (match.prop) {
+ // We outrank a previous match, disable it.
+ match.prop.enabled = false;
+ match.prop.updateEditor();
+ }
+ match.rank = rank;
+ match.prop = prop;
+ } else if (rank) {
+ // A previous match outranks us, disable ourself.
+ prop.enabled = false;
+ prop.updateEditor();
+ }
+ }
+
+ // If we found a match, update its value with the new text property
+ // value.
+ if (match.prop) {
+ match.prop.set(newProp);
+ return true;
+ }
+
+ return false;
+ },
+
+ /**
+ * Jump between editable properties in the UI. If the focus direction is
+ * forward, begin editing the next property name if available or focus the
+ * new property editor otherwise. If the focus direction is backward,
+ * begin editing the previous property value or focus the selector editor if
+ * this is the first element in the property list.
+ *
+ * @param {TextProperty} textProperty
+ * The text property that will be left to focus on a sibling.
+ * @param {Number} direction
+ * The move focus direction number.
+ */
+ editClosestTextProperty: function (textProperty, direction) {
+ let index = this.textProps.indexOf(textProperty);
+
+ if (direction === Services.focus.MOVEFOCUS_FORWARD) {
+ for (++index; index < this.textProps.length; ++index) {
+ if (!this.textProps[index].invisible) {
+ break;
+ }
+ }
+ if (index === this.textProps.length) {
+ textProperty.rule.editor.closeBrace.click();
+ } else {
+ this.textProps[index].editor.nameSpan.click();
+ }
+ } else if (direction === Services.focus.MOVEFOCUS_BACKWARD) {
+ for (--index; index >= 0; --index) {
+ if (!this.textProps[index].invisible) {
+ break;
+ }
+ }
+ if (index < 0) {
+ textProperty.editor.ruleEditor.selectorText.click();
+ } else {
+ this.textProps[index].editor.valueSpan.click();
+ }
+ }
+ },
+
+ /**
+ * Return a string representation of the rule.
+ */
+ stringifyRule: function () {
+ let selectorText = this.selectorText;
+ let cssText = "";
+ let terminator = Services.appinfo.OS === "WINNT" ? "\r\n" : "\n";
+
+ for (let textProp of this.textProps) {
+ if (!textProp.invisible) {
+ cssText += "\t" + textProp.stringifyProperty() + terminator;
+ }
+ }
+
+ return selectorText + " {" + terminator + cssText + "}";
+ },
+
+ /**
+ * See whether this rule has any non-invisible properties.
+ * @return {Boolean} true if there is any visible property, or false
+ * if all properties are invisible
+ */
+ hasAnyVisibleProperties: function () {
+ for (let prop of this.textProps) {
+ if (!prop.invisible) {
+ return true;
+ }
+ }
+ return false;
+ }
+};
+
+exports.Rule = Rule;
diff --git a/devtools/client/inspector/rules/models/text-property.js b/devtools/client/inspector/rules/models/text-property.js
new file mode 100644
index 000000000..3bbe6e91d
--- /dev/null
+++ b/devtools/client/inspector/rules/models/text-property.js
@@ -0,0 +1,215 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set 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 {escapeCSSComment} = require("devtools/shared/css/parsing-utils");
+const {getCssProperties} = require("devtools/shared/fronts/css-properties");
+
+/**
+ * TextProperty is responsible for the following:
+ * Manages a single property from the authoredText attribute of the
+ * relevant declaration.
+ * Maintains a list of computed properties that come from this
+ * property declaration.
+ * Changes to the TextProperty are sent to its related Rule for
+ * application.
+ *
+ * @param {Rule} rule
+ * The rule this TextProperty came from.
+ * @param {String} name
+ * The text property name (such as "background" or "border-top").
+ * @param {String} value
+ * The property's value (not including priority).
+ * @param {String} priority
+ * The property's priority (either "important" or an empty string).
+ * @param {Boolean} enabled
+ * Whether the property is enabled.
+ * @param {Boolean} invisible
+ * Whether the property is invisible. An invisible property
+ * does not show up in the UI; these are needed so that the
+ * index of a property in Rule.textProps is the same as the index
+ * coming from parseDeclarations.
+ */
+function TextProperty(rule, name, value, priority, enabled = true,
+ invisible = false) {
+ this.rule = rule;
+ this.name = name;
+ this.value = value;
+ this.priority = priority;
+ this.enabled = !!enabled;
+ this.invisible = invisible;
+ this.panelDoc = this.rule.elementStyle.ruleView.inspector.panelDoc;
+
+ const toolbox = this.rule.elementStyle.ruleView.inspector.toolbox;
+ this.cssProperties = getCssProperties(toolbox);
+
+ this.updateComputed();
+}
+
+TextProperty.prototype = {
+ /**
+ * Update the editor associated with this text property,
+ * if any.
+ */
+ updateEditor: function () {
+ if (this.editor) {
+ this.editor.update();
+ }
+ },
+
+ /**
+ * Update the list of computed properties for this text property.
+ */
+ updateComputed: function () {
+ if (!this.name) {
+ return;
+ }
+
+ // This is a bit funky. To get the list of computed properties
+ // for this text property, we'll set the property on a dummy element
+ // and see what the computed style looks like.
+ let dummyElement = this.rule.elementStyle.ruleView.dummyElement;
+ let dummyStyle = dummyElement.style;
+ dummyStyle.cssText = "";
+ dummyStyle.setProperty(this.name, this.value, this.priority);
+
+ this.computed = [];
+
+ // Manually get all the properties that are set when setting a value on
+ // this.name and check the computed style on dummyElement for each one.
+ // If we just read dummyStyle, it would skip properties when value === "".
+ let subProps = this.cssProperties.getSubproperties(this.name);
+
+ for (let prop of subProps) {
+ this.computed.push({
+ textProp: this,
+ name: prop,
+ value: dummyStyle.getPropertyValue(prop),
+ priority: dummyStyle.getPropertyPriority(prop),
+ });
+ }
+ },
+
+ /**
+ * Set all the values from another TextProperty instance into
+ * this TextProperty instance.
+ *
+ * @param {TextProperty} prop
+ * The other TextProperty instance.
+ */
+ set: function (prop) {
+ let changed = false;
+ for (let item of ["name", "value", "priority", "enabled"]) {
+ if (this[item] !== prop[item]) {
+ this[item] = prop[item];
+ changed = true;
+ }
+ }
+
+ if (changed) {
+ this.updateEditor();
+ }
+ },
+
+ setValue: function (value, priority, force = false) {
+ let store = this.rule.elementStyle.store;
+
+ if (this.editor && value !== this.editor.committed.value || force) {
+ store.userProperties.setProperty(this.rule.style, this.name, value);
+ }
+
+ this.rule.setPropertyValue(this, value, priority);
+ this.updateEditor();
+ },
+
+ /**
+ * Called when the property's value has been updated externally, and
+ * the property and editor should update.
+ */
+ noticeNewValue: function (value) {
+ if (value !== this.value) {
+ this.value = value;
+ this.updateEditor();
+ }
+ },
+
+ setName: function (name) {
+ let store = this.rule.elementStyle.store;
+
+ if (name !== this.name) {
+ store.userProperties.setProperty(this.rule.style, name,
+ this.editor.committed.value);
+ }
+
+ this.rule.setPropertyName(this, name);
+ this.updateEditor();
+ },
+
+ setEnabled: function (value) {
+ this.rule.setPropertyEnabled(this, value);
+ this.updateEditor();
+ },
+
+ remove: function () {
+ this.rule.removeProperty(this);
+ },
+
+ /**
+ * Return a string representation of the rule property.
+ */
+ stringifyProperty: function () {
+ // Get the displayed property value
+ let declaration = this.name + ": " + this.editor.valueSpan.textContent +
+ ";";
+
+ // Comment out property declarations that are not enabled
+ if (!this.enabled) {
+ declaration = "/* " + escapeCSSComment(declaration) + " */";
+ }
+
+ return declaration;
+ },
+
+ /**
+ * See whether this property's name is known.
+ *
+ * @return {Boolean} true if the property name is known, false otherwise.
+ */
+ isKnownProperty: function () {
+ return this.cssProperties.isKnown(this.name);
+ },
+
+ /**
+ * Validate this property. Does it make sense for this value to be assigned
+ * to this property name?
+ *
+ * @return {Boolean} true if the property value is valid, false otherwise.
+ */
+ isValid: function () {
+ // Starting with FF49, StyleRuleActors provide a list of parsed
+ // declarations, with data about their validity, but if we don't have this,
+ // compute validity locally (which might not be correct, but better than
+ // nothing).
+ if (!this.rule.domRule.declarations) {
+ return this.cssProperties.isValidOnClient(this.name, this.value, this.panelDoc);
+ }
+
+ let selfIndex = this.rule.textProps.indexOf(this);
+
+ // When adding a new property in the rule-view, the TextProperty object is
+ // created right away before the rule gets updated on the server, so we're
+ // not going to find the corresponding declaration object yet. Default to
+ // true.
+ if (!this.rule.domRule.declarations[selfIndex]) {
+ return true;
+ }
+
+ return this.rule.domRule.declarations[selfIndex].isValid;
+ }
+};
+
+exports.TextProperty = TextProperty;
diff --git a/devtools/client/inspector/rules/moz.build b/devtools/client/inspector/rules/moz.build
new file mode 100644
index 000000000..e826c1414
--- /dev/null
+++ b/devtools/client/inspector/rules/moz.build
@@ -0,0 +1,16 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DIRS += [
+ 'models',
+ 'views',
+]
+
+DevToolsModules(
+ 'rules.js',
+)
+
+BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
diff --git a/devtools/client/inspector/rules/rules.js b/devtools/client/inspector/rules/rules.js
new file mode 100644
index 000000000..8c5ec7617
--- /dev/null
+++ b/devtools/client/inspector/rules/rules.js
@@ -0,0 +1,1673 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set 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 Services = require("Services");
+const {Task} = require("devtools/shared/task");
+const {Tools} = require("devtools/client/definitions");
+const {l10n} = require("devtools/shared/inspector/css-logic");
+const {ELEMENT_STYLE} = require("devtools/shared/specs/styles");
+const {OutputParser} = require("devtools/client/shared/output-parser");
+const {PrefObserver, PREF_ORIG_SOURCES} = require("devtools/client/styleeditor/utils");
+const {ElementStyle} = require("devtools/client/inspector/rules/models/element-style");
+const {Rule} = require("devtools/client/inspector/rules/models/rule");
+const {RuleEditor} = require("devtools/client/inspector/rules/views/rule-editor");
+const {gDevTools} = require("devtools/client/framework/devtools");
+const {getCssProperties} = require("devtools/shared/fronts/css-properties");
+const HighlightersOverlay = require("devtools/client/inspector/shared/highlighters-overlay");
+const {
+ VIEW_NODE_SELECTOR_TYPE,
+ VIEW_NODE_PROPERTY_TYPE,
+ VIEW_NODE_VALUE_TYPE,
+ VIEW_NODE_IMAGE_URL_TYPE,
+ VIEW_NODE_LOCATION_TYPE,
+} = require("devtools/client/inspector/shared/node-types");
+const StyleInspectorMenu = require("devtools/client/inspector/shared/style-inspector-menu");
+const TooltipsOverlay = require("devtools/client/inspector/shared/tooltips-overlay");
+const {createChild, promiseWarn, throttle} = require("devtools/client/inspector/shared/utils");
+const EventEmitter = require("devtools/shared/event-emitter");
+const {KeyShortcuts} = require("devtools/client/shared/key-shortcuts");
+const clipboardHelper = require("devtools/shared/platform/clipboard");
+const {AutocompletePopup} = require("devtools/client/shared/autocomplete-popup");
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const PREF_UA_STYLES = "devtools.inspector.showUserAgentStyles";
+const PREF_DEFAULT_COLOR_UNIT = "devtools.defaultColorUnit";
+const PREF_ENABLE_MDN_DOCS_TOOLTIP =
+ "devtools.inspector.mdnDocsTooltip.enabled";
+const FILTER_CHANGED_TIMEOUT = 150;
+
+// This is used to parse user input when filtering.
+const FILTER_PROP_RE = /\s*([^:\s]*)\s*:\s*(.*?)\s*;?$/;
+// This is used to parse the filter search value to see if the filter
+// should be strict or not
+const FILTER_STRICT_RE = /\s*`(.*?)`\s*$/;
+
+/**
+ * Our model looks like this:
+ *
+ * ElementStyle:
+ * Responsible for keeping track of which properties are overridden.
+ * Maintains a list of Rule objects that apply to the element.
+ * Rule:
+ * Manages a single style declaration or rule.
+ * Responsible for applying changes to the properties in a rule.
+ * Maintains a list of TextProperty objects.
+ * TextProperty:
+ * Manages a single property from the authoredText attribute of the
+ * relevant declaration.
+ * Maintains a list of computed properties that come from this
+ * property declaration.
+ * Changes to the TextProperty are sent to its related Rule for
+ * application.
+ *
+ * View hierarchy mostly follows the model hierarchy.
+ *
+ * CssRuleView:
+ * Owns an ElementStyle and creates a list of RuleEditors for its
+ * Rules.
+ * RuleEditor:
+ * Owns a Rule object and creates a list of TextPropertyEditors
+ * for its TextProperties.
+ * Manages creation of new text properties.
+ * TextPropertyEditor:
+ * Owns a TextProperty object.
+ * Manages changes to the TextProperty.
+ * Can be expanded to display computed properties.
+ * Can mark a property disabled or enabled.
+ */
+
+/**
+ * CssRuleView is a view of the style rules and declarations that
+ * apply to a given element. After construction, the 'element'
+ * property will be available with the user interface.
+ *
+ * @param {Inspector} inspector
+ * Inspector toolbox panel
+ * @param {Document} document
+ * The document that will contain the rule view.
+ * @param {Object} store
+ * The CSS rule view can use this object to store metadata
+ * that might outlast the rule view, particularly the current
+ * set of disabled properties.
+ * @param {PageStyleFront} pageStyle
+ * The PageStyleFront for communicating with the remote server.
+ */
+function CssRuleView(inspector, document, store, pageStyle) {
+ this.inspector = inspector;
+ this.styleDocument = document;
+ this.styleWindow = this.styleDocument.defaultView;
+ this.store = store || {};
+ this.pageStyle = pageStyle;
+
+ // Allow tests to override throttling behavior, as this can cause intermittents.
+ this.throttle = throttle;
+
+ this.cssProperties = getCssProperties(inspector.toolbox);
+
+ this._outputParser = new OutputParser(document, this.cssProperties);
+
+ this._onAddRule = this._onAddRule.bind(this);
+ this._onContextMenu = this._onContextMenu.bind(this);
+ this._onCopy = this._onCopy.bind(this);
+ this._onFilterStyles = this._onFilterStyles.bind(this);
+ this._onClearSearch = this._onClearSearch.bind(this);
+ this._onTogglePseudoClassPanel = this._onTogglePseudoClassPanel.bind(this);
+ this._onTogglePseudoClass = this._onTogglePseudoClass.bind(this);
+
+ let doc = this.styleDocument;
+ this.element = doc.getElementById("ruleview-container-focusable");
+ this.addRuleButton = doc.getElementById("ruleview-add-rule-button");
+ this.searchField = doc.getElementById("ruleview-searchbox");
+ this.searchClearButton = doc.getElementById("ruleview-searchinput-clear");
+ this.pseudoClassPanel = doc.getElementById("pseudo-class-panel");
+ this.pseudoClassToggle = doc.getElementById("pseudo-class-panel-toggle");
+ this.hoverCheckbox = doc.getElementById("pseudo-hover-toggle");
+ this.activeCheckbox = doc.getElementById("pseudo-active-toggle");
+ this.focusCheckbox = doc.getElementById("pseudo-focus-toggle");
+
+ this.searchClearButton.hidden = true;
+
+ this.shortcuts = new KeyShortcuts({ window: this.styleWindow });
+ this._onShortcut = this._onShortcut.bind(this);
+ this.shortcuts.on("Escape", this._onShortcut);
+ this.shortcuts.on("Return", this._onShortcut);
+ this.shortcuts.on("Space", this._onShortcut);
+ this.shortcuts.on("CmdOrCtrl+F", this._onShortcut);
+ this.element.addEventListener("copy", this._onCopy);
+ this.element.addEventListener("contextmenu", this._onContextMenu);
+ this.addRuleButton.addEventListener("click", this._onAddRule);
+ this.searchField.addEventListener("input", this._onFilterStyles);
+ this.searchField.addEventListener("contextmenu", this.inspector.onTextBoxContextMenu);
+ this.searchClearButton.addEventListener("click", this._onClearSearch);
+ this.pseudoClassToggle.addEventListener("click",
+ this._onTogglePseudoClassPanel);
+ this.hoverCheckbox.addEventListener("click", this._onTogglePseudoClass);
+ this.activeCheckbox.addEventListener("click", this._onTogglePseudoClass);
+ this.focusCheckbox.addEventListener("click", this._onTogglePseudoClass);
+
+ this._handlePrefChange = this._handlePrefChange.bind(this);
+ this._onSourcePrefChanged = this._onSourcePrefChanged.bind(this);
+
+ this._prefObserver = new PrefObserver("devtools.");
+ this._prefObserver.on(PREF_ORIG_SOURCES, this._onSourcePrefChanged);
+ this._prefObserver.on(PREF_UA_STYLES, this._handlePrefChange);
+ this._prefObserver.on(PREF_DEFAULT_COLOR_UNIT, this._handlePrefChange);
+ this._prefObserver.on(PREF_ENABLE_MDN_DOCS_TOOLTIP, this._handlePrefChange);
+
+ this.showUserAgentStyles = Services.prefs.getBoolPref(PREF_UA_STYLES);
+ this.enableMdnDocsTooltip =
+ Services.prefs.getBoolPref(PREF_ENABLE_MDN_DOCS_TOOLTIP);
+
+ // The popup will be attached to the toolbox document.
+ this.popup = new AutocompletePopup(inspector._toolbox.doc, {
+ autoSelect: true,
+ theme: "auto"
+ });
+
+ this._showEmpty();
+
+ this._contextmenu = new StyleInspectorMenu(this, { isRuleView: true });
+
+ // Add the tooltips and highlighters to the view
+ this.tooltips = new TooltipsOverlay(this);
+ this.tooltips.addToView();
+ this.highlighters = new HighlightersOverlay(this);
+ this.highlighters.addToView();
+
+ EventEmitter.decorate(this);
+}
+
+CssRuleView.prototype = {
+ // The element that we're inspecting.
+ _viewedElement: null,
+
+ // Used for cancelling timeouts in the style filter.
+ _filterChangedTimeout: null,
+
+ // Empty, unconnected element of the same type as this node, used
+ // to figure out how shorthand properties will be parsed.
+ _dummyElement: null,
+
+ // Get the dummy elemenet.
+ get dummyElement() {
+ return this._dummyElement;
+ },
+
+ // Get the filter search value.
+ get searchValue() {
+ return this.searchField.value.toLowerCase();
+ },
+
+ /**
+ * Get an instance of SelectorHighlighter (used to highlight nodes that match
+ * selectors in the rule-view). A new instance is only created the first time
+ * this function is called. The same instance will then be returned.
+ *
+ * @return {Promise} Resolves to the instance of the highlighter.
+ */
+ getSelectorHighlighter: Task.async(function* () {
+ let utils = this.inspector.toolbox.highlighterUtils;
+ if (!utils.supportsCustomHighlighters()) {
+ return null;
+ }
+
+ if (this.selectorHighlighter) {
+ return this.selectorHighlighter;
+ }
+
+ try {
+ let h = yield utils.getHighlighterByType("SelectorHighlighter");
+ this.selectorHighlighter = h;
+ return h;
+ } catch (e) {
+ // The SelectorHighlighter type could not be created in the
+ // current target. It could be an older server, or a XUL page.
+ return null;
+ }
+ }),
+
+ /**
+ * Highlight/unhighlight all the nodes that match a given set of selectors
+ * inside the document of the current selected node.
+ * Only one selector can be highlighted at a time, so calling the method a
+ * second time with a different selector will first unhighlight the previously
+ * highlighted nodes.
+ * Calling the method a second time with the same selector will just
+ * unhighlight the highlighted nodes.
+ *
+ * @param {DOMNode} selectorIcon
+ * The icon that was clicked to toggle the selector. The
+ * class 'highlighted' will be added when the selector is
+ * highlighted.
+ * @param {String} selector
+ * The selector used to find nodes in the page.
+ */
+ toggleSelectorHighlighter: function (selectorIcon, selector) {
+ if (this.lastSelectorIcon) {
+ this.lastSelectorIcon.classList.remove("highlighted");
+ }
+ selectorIcon.classList.remove("highlighted");
+
+ this.unhighlightSelector().then(() => {
+ if (selector !== this.highlighters.selectorHighlighterShown) {
+ this.highlighters.selectorHighlighterShown = selector;
+ selectorIcon.classList.add("highlighted");
+ this.lastSelectorIcon = selectorIcon;
+ this.highlightSelector(selector).then(() => {
+ this.emit("ruleview-selectorhighlighter-toggled", true);
+ }, e => console.error(e));
+ } else {
+ this.highlighters.selectorHighlighterShown = null;
+ this.emit("ruleview-selectorhighlighter-toggled", false);
+ }
+ }, e => console.error(e));
+ },
+
+ highlightSelector: Task.async(function* (selector) {
+ let node = this.inspector.selection.nodeFront;
+
+ let highlighter = yield this.getSelectorHighlighter();
+ if (!highlighter) {
+ return;
+ }
+
+ yield highlighter.show(node, {
+ hideInfoBar: true,
+ hideGuides: true,
+ selector
+ });
+ }),
+
+ unhighlightSelector: Task.async(function* () {
+ let highlighter = yield this.getSelectorHighlighter();
+ if (!highlighter) {
+ return;
+ }
+
+ yield highlighter.hide();
+ }),
+
+ /**
+ * Get the type of a given node in the rule-view
+ *
+ * @param {DOMNode} node
+ * The node which we want information about
+ * @return {Object} The type information object contains the following props:
+ * - type {String} One of the VIEW_NODE_XXX_TYPE const in
+ * client/inspector/shared/node-types
+ * - value {Object} Depends on the type of the node
+ * returns null of the node isn't anything we care about
+ */
+ getNodeInfo: function (node) {
+ if (!node) {
+ return null;
+ }
+
+ let type, value;
+ let classes = node.classList;
+ let prop = getParentTextProperty(node);
+
+ if (classes.contains("ruleview-propertyname") && prop) {
+ type = VIEW_NODE_PROPERTY_TYPE;
+ value = {
+ property: node.textContent,
+ value: getPropertyNameAndValue(node).value,
+ enabled: prop.enabled,
+ overridden: prop.overridden,
+ pseudoElement: prop.rule.pseudoElement,
+ sheetHref: prop.rule.domRule.href,
+ textProperty: prop
+ };
+ } else if (classes.contains("ruleview-propertyvalue") && prop) {
+ type = VIEW_NODE_VALUE_TYPE;
+ value = {
+ property: getPropertyNameAndValue(node).name,
+ value: node.textContent,
+ enabled: prop.enabled,
+ overridden: prop.overridden,
+ pseudoElement: prop.rule.pseudoElement,
+ sheetHref: prop.rule.domRule.href,
+ textProperty: prop
+ };
+ } else if (classes.contains("theme-link") &&
+ !classes.contains("ruleview-rule-source") && prop) {
+ type = VIEW_NODE_IMAGE_URL_TYPE;
+ value = {
+ property: getPropertyNameAndValue(node).name,
+ value: node.parentNode.textContent,
+ url: node.href,
+ enabled: prop.enabled,
+ overridden: prop.overridden,
+ pseudoElement: prop.rule.pseudoElement,
+ sheetHref: prop.rule.domRule.href,
+ textProperty: prop
+ };
+ } else if (classes.contains("ruleview-selector-unmatched") ||
+ classes.contains("ruleview-selector-matched") ||
+ classes.contains("ruleview-selectorcontainer") ||
+ classes.contains("ruleview-selector") ||
+ classes.contains("ruleview-selector-attribute") ||
+ classes.contains("ruleview-selector-pseudo-class") ||
+ classes.contains("ruleview-selector-pseudo-class-lock")) {
+ type = VIEW_NODE_SELECTOR_TYPE;
+ value = this._getRuleEditorForNode(node).selectorText.textContent;
+ } else if (classes.contains("ruleview-rule-source") ||
+ classes.contains("ruleview-rule-source-label")) {
+ type = VIEW_NODE_LOCATION_TYPE;
+ let rule = this._getRuleEditorForNode(node).rule;
+ value = (rule.sheet && rule.sheet.href) ? rule.sheet.href : rule.title;
+ } else {
+ return null;
+ }
+
+ return {type, value};
+ },
+
+ /**
+ * Retrieve the RuleEditor instance that should be stored on
+ * the offset parent of the node
+ */
+ _getRuleEditorForNode: function (node) {
+ if (!node.offsetParent) {
+ // some nodes don't have an offsetParent, but their parentNode does
+ node = node.parentNode;
+ }
+ return node.offsetParent._ruleEditor;
+ },
+
+ /**
+ * Context menu handler.
+ */
+ _onContextMenu: function (event) {
+ this._contextmenu.show(event);
+ },
+
+ /**
+ * Callback for copy event. Copy the selected text.
+ *
+ * @param {Event} event
+ * copy event object.
+ */
+ _onCopy: function (event) {
+ if (event) {
+ this.copySelection(event.target);
+ event.preventDefault();
+ }
+ },
+
+ /**
+ * Copy the current selection. The current target is necessary
+ * if the selection is inside an input or a textarea
+ *
+ * @param {DOMNode} target
+ * DOMNode target of the copy action
+ */
+ copySelection: function (target) {
+ try {
+ let text = "";
+
+ let nodeName = target && target.nodeName;
+ if (nodeName === "input" || nodeName == "textarea") {
+ let start = Math.min(target.selectionStart, target.selectionEnd);
+ let end = Math.max(target.selectionStart, target.selectionEnd);
+ let count = end - start;
+ text = target.value.substr(start, count);
+ } else {
+ text = this.styleWindow.getSelection().toString();
+
+ // Remove any double newlines.
+ text = text.replace(/(\r?\n)\r?\n/g, "$1");
+ }
+
+ clipboardHelper.copyString(text);
+ } catch (e) {
+ console.error(e);
+ }
+ },
+
+ /**
+ * A helper for _onAddRule that handles the case where the actor
+ * does not support as-authored styles.
+ */
+ _onAddNewRuleNonAuthored: function () {
+ let elementStyle = this._elementStyle;
+ let element = elementStyle.element;
+ let rules = elementStyle.rules;
+ let pseudoClasses = element.pseudoClassLocks;
+
+ this.pageStyle.addNewRule(element, pseudoClasses).then(options => {
+ let newRule = new Rule(elementStyle, options);
+ rules.push(newRule);
+ let editor = new RuleEditor(this, newRule);
+ newRule.editor = editor;
+
+ // Insert the new rule editor after the inline element rule
+ if (rules.length <= 1) {
+ this.element.appendChild(editor.element);
+ } else {
+ for (let rule of rules) {
+ if (rule.domRule.type === ELEMENT_STYLE) {
+ let referenceElement = rule.editor.element.nextSibling;
+ this.element.insertBefore(editor.element, referenceElement);
+ break;
+ }
+ }
+ }
+
+ // Focus and make the new rule's selector editable
+ editor.selectorText.click();
+ elementStyle._changed();
+ });
+ },
+
+ /**
+ * Add a new rule to the current element.
+ */
+ _onAddRule: function () {
+ let elementStyle = this._elementStyle;
+ let element = elementStyle.element;
+ let client = this.inspector.target.client;
+ let pseudoClasses = element.pseudoClassLocks;
+
+ if (!client.traits.addNewRule) {
+ return;
+ }
+
+ if (!this.pageStyle.supportsAuthoredStyles) {
+ // We're talking to an old server.
+ this._onAddNewRuleNonAuthored();
+ return;
+ }
+
+ // Adding a new rule with authored styles will cause the actor to
+ // emit an event, which will in turn cause the rule view to be
+ // updated. So, we wait for this update and for the rule creation
+ // request to complete, and then focus the new rule's selector.
+ let eventPromise = this.once("ruleview-refreshed");
+ let newRulePromise = this.pageStyle.addNewRule(element, pseudoClasses);
+ promise.all([eventPromise, newRulePromise]).then((values) => {
+ let options = values[1];
+ // Be sure the reference the correct |rules| here.
+ for (let rule of this._elementStyle.rules) {
+ if (options.rule === rule.domRule) {
+ rule.editor.selectorText.click();
+ elementStyle._changed();
+ break;
+ }
+ }
+ });
+ },
+
+ /**
+ * Disables add rule button when needed
+ */
+ refreshAddRuleButtonState: function () {
+ let shouldBeDisabled = !this._viewedElement ||
+ !this.inspector.selection.isElementNode() ||
+ this.inspector.selection.isAnonymousNode();
+ this.addRuleButton.disabled = shouldBeDisabled;
+ },
+
+ setPageStyle: function (pageStyle) {
+ this.pageStyle = pageStyle;
+ },
+
+ /**
+ * Return {Boolean} true if the rule view currently has an input
+ * editor visible.
+ */
+ get isEditing() {
+ return this.tooltips.isEditing ||
+ this.element.querySelectorAll(".styleinspector-propertyeditor")
+ .length > 0;
+ },
+
+ _handlePrefChange: function (pref) {
+ if (pref === PREF_UA_STYLES) {
+ this.showUserAgentStyles = Services.prefs.getBoolPref(pref);
+ }
+
+ // Reselect the currently selected element
+ let refreshOnPrefs = [PREF_UA_STYLES, PREF_DEFAULT_COLOR_UNIT];
+ if (refreshOnPrefs.indexOf(pref) > -1) {
+ this.selectElement(this._viewedElement, true);
+ }
+ },
+
+ /**
+ * Update source links when pref for showing original sources changes
+ */
+ _onSourcePrefChanged: function () {
+ if (this._elementStyle && this._elementStyle.rules) {
+ for (let rule of this._elementStyle.rules) {
+ if (rule.editor) {
+ rule.editor.updateSourceLink();
+ }
+ }
+ this.inspector.emit("rule-view-sourcelinks-updated");
+ }
+ },
+
+ /**
+ * Set the filter style search value.
+ * @param {String} value
+ * The search value.
+ */
+ setFilterStyles: function (value = "") {
+ this.searchField.value = value;
+ this.searchField.focus();
+ this._onFilterStyles();
+ },
+
+ /**
+ * Called when the user enters a search term in the filter style search box.
+ */
+ _onFilterStyles: function () {
+ if (this._filterChangedTimeout) {
+ clearTimeout(this._filterChangedTimeout);
+ }
+
+ let filterTimeout = (this.searchValue.length > 0) ?
+ FILTER_CHANGED_TIMEOUT : 0;
+ this.searchClearButton.hidden = this.searchValue.length === 0;
+
+ this._filterChangedTimeout = setTimeout(() => {
+ if (this.searchField.value.length > 0) {
+ this.searchField.setAttribute("filled", true);
+ } else {
+ this.searchField.removeAttribute("filled");
+ }
+
+ this.searchData = {
+ searchPropertyMatch: FILTER_PROP_RE.exec(this.searchValue),
+ searchPropertyName: this.searchValue,
+ searchPropertyValue: this.searchValue,
+ strictSearchValue: "",
+ strictSearchPropertyName: false,
+ strictSearchPropertyValue: false,
+ strictSearchAllValues: false
+ };
+
+ if (this.searchData.searchPropertyMatch) {
+ // Parse search value as a single property line and extract the
+ // property name and value. If the parsed property name or value is
+ // contained in backquotes (`), extract the value within the backquotes
+ // and set the corresponding strict search for the property to true.
+ if (FILTER_STRICT_RE.test(this.searchData.searchPropertyMatch[1])) {
+ this.searchData.strictSearchPropertyName = true;
+ this.searchData.searchPropertyName =
+ FILTER_STRICT_RE.exec(this.searchData.searchPropertyMatch[1])[1];
+ } else {
+ this.searchData.searchPropertyName =
+ this.searchData.searchPropertyMatch[1];
+ }
+
+ if (FILTER_STRICT_RE.test(this.searchData.searchPropertyMatch[2])) {
+ this.searchData.strictSearchPropertyValue = true;
+ this.searchData.searchPropertyValue =
+ FILTER_STRICT_RE.exec(this.searchData.searchPropertyMatch[2])[1];
+ } else {
+ this.searchData.searchPropertyValue =
+ this.searchData.searchPropertyMatch[2];
+ }
+
+ // Strict search for stylesheets will match the property line regex.
+ // Extract the search value within the backquotes to be used
+ // in the strict search for stylesheets in _highlightStyleSheet.
+ if (FILTER_STRICT_RE.test(this.searchValue)) {
+ this.searchData.strictSearchValue =
+ FILTER_STRICT_RE.exec(this.searchValue)[1];
+ }
+ } else if (FILTER_STRICT_RE.test(this.searchValue)) {
+ // If the search value does not correspond to a property line and
+ // is contained in backquotes, extract the search value within the
+ // backquotes and set the flag to perform a strict search for all
+ // the values (selector, stylesheet, property and computed values).
+ let searchValue = FILTER_STRICT_RE.exec(this.searchValue)[1];
+ this.searchData.strictSearchAllValues = true;
+ this.searchData.searchPropertyName = searchValue;
+ this.searchData.searchPropertyValue = searchValue;
+ this.searchData.strictSearchValue = searchValue;
+ }
+
+ this._clearHighlight(this.element);
+ this._clearRules();
+ this._createEditors();
+
+ this.inspector.emit("ruleview-filtered");
+
+ this._filterChangeTimeout = null;
+ }, filterTimeout);
+ },
+
+ /**
+ * Called when the user clicks on the clear button in the filter style search
+ * box. Returns true if the search box is cleared and false otherwise.
+ */
+ _onClearSearch: function () {
+ if (this.searchField.value) {
+ this.setFilterStyles("");
+ return true;
+ }
+
+ return false;
+ },
+
+ destroy: function () {
+ this.isDestroyed = true;
+ this.clear();
+
+ this._dummyElement = null;
+
+ this._prefObserver.off(PREF_ORIG_SOURCES, this._onSourcePrefChanged);
+ this._prefObserver.off(PREF_UA_STYLES, this._handlePrefChange);
+ this._prefObserver.off(PREF_DEFAULT_COLOR_UNIT, this._handlePrefChange);
+ this._prefObserver.destroy();
+
+ this._outputParser = null;
+
+ // Remove context menu
+ if (this._contextmenu) {
+ this._contextmenu.destroy();
+ this._contextmenu = null;
+ }
+
+ this.tooltips.destroy();
+ this.highlighters.destroy();
+
+ // Remove bound listeners
+ this.shortcuts.destroy();
+ this.element.removeEventListener("copy", this._onCopy);
+ this.element.removeEventListener("contextmenu", this._onContextMenu);
+ this.addRuleButton.removeEventListener("click", this._onAddRule);
+ this.searchField.removeEventListener("input", this._onFilterStyles);
+ this.searchField.removeEventListener("contextmenu",
+ this.inspector.onTextBoxContextMenu);
+ this.searchClearButton.removeEventListener("click", this._onClearSearch);
+ this.pseudoClassToggle.removeEventListener("click",
+ this._onTogglePseudoClassPanel);
+ this.hoverCheckbox.removeEventListener("click", this._onTogglePseudoClass);
+ this.activeCheckbox.removeEventListener("click", this._onTogglePseudoClass);
+ this.focusCheckbox.removeEventListener("click", this._onTogglePseudoClass);
+
+ this.searchField = null;
+ this.searchClearButton = null;
+ this.pseudoClassPanel = null;
+ this.pseudoClassToggle = null;
+ this.hoverCheckbox = null;
+ this.activeCheckbox = null;
+ this.focusCheckbox = null;
+
+ this.inspector = null;
+ this.styleDocument = null;
+ this.styleWindow = null;
+
+ if (this.element.parentNode) {
+ this.element.parentNode.removeChild(this.element);
+ }
+
+ if (this._elementStyle) {
+ this._elementStyle.destroy();
+ }
+
+ this.popup.destroy();
+ },
+
+ /**
+ * Mark the view as selecting an element, disabling all interaction, and
+ * visually clearing the view after a few milliseconds to avoid confusion
+ * about which element's styles the rule view shows.
+ */
+ _startSelectingElement: function () {
+ this.element.classList.add("non-interactive");
+ },
+
+ /**
+ * Mark the view as no longer selecting an element, re-enabling interaction.
+ */
+ _stopSelectingElement: function () {
+ this.element.classList.remove("non-interactive");
+ },
+
+ /**
+ * Update the view with a new selected element.
+ *
+ * @param {NodeActor} element
+ * The node whose style rules we'll inspect.
+ * @param {Boolean} allowRefresh
+ * Update the view even if the element is the same as last time.
+ */
+ selectElement: function (element, allowRefresh = false) {
+ let refresh = (this._viewedElement === element);
+ if (refresh && !allowRefresh) {
+ return promise.resolve(undefined);
+ }
+
+ if (this.popup.isOpen) {
+ this.popup.hidePopup();
+ }
+
+ this.clear(false);
+ this._viewedElement = element;
+
+ this.clearPseudoClassPanel();
+ this.refreshAddRuleButtonState();
+
+ if (!this._viewedElement) {
+ this._stopSelectingElement();
+ this._clearRules();
+ this._showEmpty();
+ this.refreshPseudoClassPanel();
+ return promise.resolve(undefined);
+ }
+
+ // To figure out how shorthand properties are interpreted by the
+ // engine, we will set properties on a dummy element and observe
+ // how their .style attribute reflects them as computed values.
+ let dummyElementPromise = promise.resolve(this.styleDocument).then(document => {
+ // ::before and ::after do not have a namespaceURI
+ let namespaceURI = this.element.namespaceURI ||
+ document.documentElement.namespaceURI;
+ this._dummyElement = document.createElementNS(namespaceURI,
+ this.element.tagName);
+ }).then(null, promiseWarn);
+
+ let elementStyle = new ElementStyle(element, this, this.store,
+ this.pageStyle, this.showUserAgentStyles);
+ this._elementStyle = elementStyle;
+
+ this._startSelectingElement();
+
+ return dummyElementPromise.then(() => {
+ if (this._elementStyle === elementStyle) {
+ return this._populate();
+ }
+ return undefined;
+ }).then(() => {
+ if (this._elementStyle === elementStyle) {
+ if (!refresh) {
+ this.element.scrollTop = 0;
+ }
+ this._stopSelectingElement();
+ this._elementStyle.onChanged = () => {
+ this._changed();
+ };
+ }
+ }).then(null, e => {
+ if (this._elementStyle === elementStyle) {
+ this._stopSelectingElement();
+ this._clearRules();
+ }
+ console.error(e);
+ });
+ },
+
+ /**
+ * Update the rules for the currently highlighted element.
+ */
+ refreshPanel: function () {
+ // Ignore refreshes during editing or when no element is selected.
+ if (this.isEditing || !this._elementStyle) {
+ return promise.resolve(undefined);
+ }
+
+ // Repopulate the element style once the current modifications are done.
+ let promises = [];
+ for (let rule of this._elementStyle.rules) {
+ if (rule._applyingModifications) {
+ promises.push(rule._applyingModifications);
+ }
+ }
+
+ return promise.all(promises).then(() => {
+ return this._populate();
+ });
+ },
+
+ /**
+ * Clear the pseudo class options panel by removing the checked and disabled
+ * attributes for each checkbox.
+ */
+ clearPseudoClassPanel: function () {
+ this.hoverCheckbox.checked = this.hoverCheckbox.disabled = false;
+ this.activeCheckbox.checked = this.activeCheckbox.disabled = false;
+ this.focusCheckbox.checked = this.focusCheckbox.disabled = false;
+ },
+
+ /**
+ * Update the pseudo class options for the currently highlighted element.
+ */
+ refreshPseudoClassPanel: function () {
+ if (!this._elementStyle || !this.inspector.selection.isElementNode()) {
+ this.hoverCheckbox.disabled = true;
+ this.activeCheckbox.disabled = true;
+ this.focusCheckbox.disabled = true;
+ return;
+ }
+
+ for (let pseudoClassLock of this._elementStyle.element.pseudoClassLocks) {
+ switch (pseudoClassLock) {
+ case ":hover": {
+ this.hoverCheckbox.checked = true;
+ break;
+ }
+ case ":active": {
+ this.activeCheckbox.checked = true;
+ break;
+ }
+ case ":focus": {
+ this.focusCheckbox.checked = true;
+ break;
+ }
+ }
+ }
+ },
+
+ _populate: function () {
+ let elementStyle = this._elementStyle;
+ return this._elementStyle.populate().then(() => {
+ if (this._elementStyle !== elementStyle || this.isDestroyed) {
+ return null;
+ }
+
+ this._clearRules();
+ let onEditorsReady = this._createEditors();
+ this.refreshPseudoClassPanel();
+
+ // Notify anyone that cares that we refreshed.
+ return onEditorsReady.then(() => {
+ this.emit("ruleview-refreshed");
+ }, e => console.error(e));
+ }).then(null, promiseWarn);
+ },
+
+ /**
+ * Show the user that the rule view has no node selected.
+ */
+ _showEmpty: function () {
+ if (this.styleDocument.getElementById("ruleview-no-results")) {
+ return;
+ }
+
+ createChild(this.element, "div", {
+ id: "ruleview-no-results",
+ textContent: l10n("rule.empty")
+ });
+ },
+
+ /**
+ * Clear the rules.
+ */
+ _clearRules: function () {
+ this.element.innerHTML = "";
+ },
+
+ /**
+ * Clear the rule view.
+ */
+ clear: function (clearDom = true) {
+ this.lastSelectorIcon = null;
+
+ if (clearDom) {
+ this._clearRules();
+ }
+ this._viewedElement = null;
+
+ if (this._elementStyle) {
+ this._elementStyle.destroy();
+ this._elementStyle = null;
+ }
+ },
+
+ /**
+ * Called when the user has made changes to the ElementStyle.
+ * Emits an event that clients can listen to.
+ */
+ _changed: function () {
+ this.emit("ruleview-changed");
+ },
+
+ /**
+ * Text for header that shows above rules for this element
+ */
+ get selectedElementLabel() {
+ if (this._selectedElementLabel) {
+ return this._selectedElementLabel;
+ }
+ this._selectedElementLabel = l10n("rule.selectedElement");
+ return this._selectedElementLabel;
+ },
+
+ /**
+ * Text for header that shows above rules for pseudo elements
+ */
+ get pseudoElementLabel() {
+ if (this._pseudoElementLabel) {
+ return this._pseudoElementLabel;
+ }
+ this._pseudoElementLabel = l10n("rule.pseudoElement");
+ return this._pseudoElementLabel;
+ },
+
+ get showPseudoElements() {
+ if (this._showPseudoElements === undefined) {
+ this._showPseudoElements =
+ Services.prefs.getBoolPref("devtools.inspector.show_pseudo_elements");
+ }
+ return this._showPseudoElements;
+ },
+
+ /**
+ * Creates an expandable container in the rule view
+ *
+ * @param {String} label
+ * The label for the container header
+ * @param {Boolean} isPseudo
+ * Whether or not the container will hold pseudo element rules
+ * @return {DOMNode} The container element
+ */
+ createExpandableContainer: function (label, isPseudo = false) {
+ let header = this.styleDocument.createElementNS(HTML_NS, "div");
+ header.className = this._getRuleViewHeaderClassName(true);
+ header.textContent = label;
+
+ let twisty = this.styleDocument.createElementNS(HTML_NS, "span");
+ twisty.className = "ruleview-expander theme-twisty";
+ twisty.setAttribute("open", "true");
+
+ header.insertBefore(twisty, header.firstChild);
+ this.element.appendChild(header);
+
+ let container = this.styleDocument.createElementNS(HTML_NS, "div");
+ container.classList.add("ruleview-expandable-container");
+ container.hidden = false;
+ this.element.appendChild(container);
+
+ header.addEventListener("dblclick", () => {
+ this._toggleContainerVisibility(twisty, container, isPseudo,
+ !this.showPseudoElements);
+ }, false);
+
+ twisty.addEventListener("click", () => {
+ this._toggleContainerVisibility(twisty, container, isPseudo,
+ !this.showPseudoElements);
+ }, false);
+
+ if (isPseudo) {
+ this._toggleContainerVisibility(twisty, container, isPseudo,
+ this.showPseudoElements);
+ }
+
+ return container;
+ },
+
+ /**
+ * Toggle the visibility of an expandable container
+ *
+ * @param {DOMNode} twisty
+ * Clickable toggle DOM Node
+ * @param {DOMNode} container
+ * Expandable container DOM Node
+ * @param {Boolean} isPseudo
+ * Whether or not the container will hold pseudo element rules
+ * @param {Boolean} showPseudo
+ * Whether or not pseudo element rules should be displayed
+ */
+ _toggleContainerVisibility: function (twisty, container, isPseudo,
+ showPseudo) {
+ let isOpen = twisty.getAttribute("open");
+
+ if (isPseudo) {
+ this._showPseudoElements = !!showPseudo;
+
+ Services.prefs.setBoolPref("devtools.inspector.show_pseudo_elements",
+ this.showPseudoElements);
+
+ container.hidden = !this.showPseudoElements;
+ isOpen = !this.showPseudoElements;
+ } else {
+ container.hidden = !container.hidden;
+ }
+
+ if (isOpen) {
+ twisty.removeAttribute("open");
+ } else {
+ twisty.setAttribute("open", "true");
+ }
+ },
+
+ _getRuleViewHeaderClassName: function (isPseudo) {
+ let baseClassName = "theme-gutter ruleview-header";
+ return isPseudo ? baseClassName + " ruleview-expandable-header" :
+ baseClassName;
+ },
+
+ /**
+ * Creates editor UI for each of the rules in _elementStyle.
+ */
+ _createEditors: function () {
+ // Run through the current list of rules, attaching
+ // their editors in order. Create editors if needed.
+ let lastInheritedSource = "";
+ let lastKeyframes = null;
+ let seenPseudoElement = false;
+ let seenNormalElement = false;
+ let seenSearchTerm = false;
+ let container = null;
+
+ if (!this._elementStyle.rules) {
+ return promise.resolve();
+ }
+
+ let editorReadyPromises = [];
+ for (let rule of this._elementStyle.rules) {
+ if (rule.domRule.system) {
+ continue;
+ }
+
+ // Initialize rule editor
+ if (!rule.editor) {
+ rule.editor = new RuleEditor(this, rule);
+ editorReadyPromises.push(rule.editor.once("source-link-updated"));
+ }
+
+ // Filter the rules and highlight any matches if there is a search input
+ if (this.searchValue && this.searchData) {
+ if (this.highlightRule(rule)) {
+ seenSearchTerm = true;
+ } else if (rule.domRule.type !== ELEMENT_STYLE) {
+ continue;
+ }
+ }
+
+ // Only print header for this element if there are pseudo elements
+ if (seenPseudoElement && !seenNormalElement && !rule.pseudoElement) {
+ seenNormalElement = true;
+ let div = this.styleDocument.createElementNS(HTML_NS, "div");
+ div.className = this._getRuleViewHeaderClassName();
+ div.textContent = this.selectedElementLabel;
+ this.element.appendChild(div);
+ }
+
+ let inheritedSource = rule.inheritedSource;
+ if (inheritedSource && inheritedSource !== lastInheritedSource) {
+ let div = this.styleDocument.createElementNS(HTML_NS, "div");
+ div.className = this._getRuleViewHeaderClassName();
+ div.textContent = inheritedSource;
+ lastInheritedSource = inheritedSource;
+ this.element.appendChild(div);
+ }
+
+ if (!seenPseudoElement && rule.pseudoElement) {
+ seenPseudoElement = true;
+ container = this.createExpandableContainer(this.pseudoElementLabel,
+ true);
+ }
+
+ let keyframes = rule.keyframes;
+ if (keyframes && keyframes !== lastKeyframes) {
+ lastKeyframes = keyframes;
+ container = this.createExpandableContainer(rule.keyframesName);
+ }
+
+ if (container && (rule.pseudoElement || keyframes)) {
+ container.appendChild(rule.editor.element);
+ } else {
+ this.element.appendChild(rule.editor.element);
+ }
+ }
+
+ if (this.searchValue && !seenSearchTerm) {
+ this.searchField.classList.add("devtools-style-searchbox-no-match");
+ } else {
+ this.searchField.classList.remove("devtools-style-searchbox-no-match");
+ }
+
+ return promise.all(editorReadyPromises);
+ },
+
+ /**
+ * Highlight rules that matches the filter search value and returns a
+ * boolean indicating whether or not rules were highlighted.
+ *
+ * @param {Rule} rule
+ * The rule object we're highlighting if its rule selectors or
+ * property values match the search value.
+ * @return {Boolean} true if the rule was highlighted, false otherwise.
+ */
+ highlightRule: function (rule) {
+ let isRuleSelectorHighlighted = this._highlightRuleSelector(rule);
+ let isStyleSheetHighlighted = this._highlightStyleSheet(rule);
+ let isHighlighted = isRuleSelectorHighlighted || isStyleSheetHighlighted;
+
+ // Highlight search matches in the rule properties
+ for (let textProp of rule.textProps) {
+ if (!textProp.invisible && this._highlightProperty(textProp.editor)) {
+ isHighlighted = true;
+ }
+ }
+
+ return isHighlighted;
+ },
+
+ /**
+ * Highlights the rule selector that matches the filter search value and
+ * returns a boolean indicating whether or not the selector was highlighted.
+ *
+ * @param {Rule} rule
+ * The Rule object.
+ * @return {Boolean} true if the rule selector was highlighted,
+ * false otherwise.
+ */
+ _highlightRuleSelector: function (rule) {
+ let isSelectorHighlighted = false;
+
+ let selectorNodes = [...rule.editor.selectorText.childNodes];
+ if (rule.domRule.type === CSSRule.KEYFRAME_RULE) {
+ selectorNodes = [rule.editor.selectorText];
+ } else if (rule.domRule.type === ELEMENT_STYLE) {
+ selectorNodes = [];
+ }
+
+ // Highlight search matches in the rule selectors
+ for (let selectorNode of selectorNodes) {
+ let selector = selectorNode.textContent.toLowerCase();
+ if ((this.searchData.strictSearchAllValues &&
+ selector === this.searchData.strictSearchValue) ||
+ (!this.searchData.strictSearchAllValues &&
+ selector.includes(this.searchValue))) {
+ selectorNode.classList.add("ruleview-highlight");
+ isSelectorHighlighted = true;
+ }
+ }
+
+ return isSelectorHighlighted;
+ },
+
+ /**
+ * Highlights the stylesheet source that matches the filter search value and
+ * returns a boolean indicating whether or not the stylesheet source was
+ * highlighted.
+ *
+ * @return {Boolean} true if the stylesheet source was highlighted, false
+ * otherwise.
+ */
+ _highlightStyleSheet: function (rule) {
+ let styleSheetSource = rule.title.toLowerCase();
+ let isStyleSheetHighlighted = this.searchData.strictSearchValue ?
+ styleSheetSource === this.searchData.strictSearchValue :
+ styleSheetSource.includes(this.searchValue);
+
+ if (isStyleSheetHighlighted) {
+ rule.editor.source.classList.add("ruleview-highlight");
+ }
+
+ return isStyleSheetHighlighted;
+ },
+
+ /**
+ * Highlights the rule properties and computed properties that match the
+ * filter search value and returns a boolean indicating whether or not the
+ * property or computed property was highlighted.
+ *
+ * @param {TextPropertyEditor} editor
+ * The rule property TextPropertyEditor object.
+ * @return {Boolean} true if the property or computed property was
+ * highlighted, false otherwise.
+ */
+ _highlightProperty: function (editor) {
+ let isPropertyHighlighted = this._highlightRuleProperty(editor);
+ let isComputedHighlighted = this._highlightComputedProperty(editor);
+
+ // Expand the computed list if a computed property is highlighted and the
+ // property rule is not highlighted
+ if (!isPropertyHighlighted && isComputedHighlighted &&
+ !editor.computed.hasAttribute("user-open")) {
+ editor.expandForFilter();
+ }
+
+ return isPropertyHighlighted || isComputedHighlighted;
+ },
+
+ /**
+ * Called when TextPropertyEditor is updated and updates the rule property
+ * highlight.
+ *
+ * @param {TextPropertyEditor} editor
+ * The rule property TextPropertyEditor object.
+ */
+ _updatePropertyHighlight: function (editor) {
+ if (!this.searchValue || !this.searchData) {
+ return;
+ }
+
+ this._clearHighlight(editor.element);
+
+ if (this._highlightProperty(editor)) {
+ this.searchField.classList.remove("devtools-style-searchbox-no-match");
+ }
+ },
+
+ /**
+ * Highlights the rule property that matches the filter search value
+ * and returns a boolean indicating whether or not the property was
+ * highlighted.
+ *
+ * @param {TextPropertyEditor} editor
+ * The rule property TextPropertyEditor object.
+ * @return {Boolean} true if the rule property was highlighted,
+ * false otherwise.
+ */
+ _highlightRuleProperty: function (editor) {
+ // Get the actual property value displayed in the rule view
+ let propertyName = editor.prop.name.toLowerCase();
+ let propertyValue = editor.valueSpan.textContent.toLowerCase();
+
+ return this._highlightMatches(editor.container, propertyName,
+ propertyValue);
+ },
+
+ /**
+ * Highlights the computed property that matches the filter search value and
+ * returns a boolean indicating whether or not the computed property was
+ * highlighted.
+ *
+ * @param {TextPropertyEditor} editor
+ * The rule property TextPropertyEditor object.
+ * @return {Boolean} true if the computed property was highlighted, false
+ * otherwise.
+ */
+ _highlightComputedProperty: function (editor) {
+ let isComputedHighlighted = false;
+
+ // Highlight search matches in the computed list of properties
+ editor._populateComputed();
+ for (let computed of editor.prop.computed) {
+ if (computed.element) {
+ // Get the actual property value displayed in the computed list
+ let computedName = computed.name.toLowerCase();
+ let computedValue = computed.parsedValue.toLowerCase();
+
+ isComputedHighlighted = this._highlightMatches(computed.element,
+ computedName, computedValue) ? true : isComputedHighlighted;
+ }
+ }
+
+ return isComputedHighlighted;
+ },
+
+ /**
+ * Helper function for highlightRules that carries out highlighting the given
+ * element if the search terms match the property, and returns a boolean
+ * indicating whether or not the search terms match.
+ *
+ * @param {DOMNode} element
+ * The node to highlight if search terms match
+ * @param {String} propertyName
+ * The property name of a rule
+ * @param {String} propertyValue
+ * The property value of a rule
+ * @return {Boolean} true if the given search terms match the property, false
+ * otherwise.
+ */
+ _highlightMatches: function (element, propertyName, propertyValue) {
+ let {
+ searchPropertyName,
+ searchPropertyValue,
+ searchPropertyMatch,
+ strictSearchPropertyName,
+ strictSearchPropertyValue,
+ strictSearchAllValues,
+ } = this.searchData;
+ let matches = false;
+
+ // If the inputted search value matches a property line like
+ // `font-family: arial`, then check to make sure the name and value match.
+ // Otherwise, just compare the inputted search string directly against the
+ // name and value of the rule property.
+ let hasNameAndValue = searchPropertyMatch &&
+ searchPropertyName &&
+ searchPropertyValue;
+ let isMatch = (value, query, isStrict) => {
+ return isStrict ? value === query : query && value.includes(query);
+ };
+
+ if (hasNameAndValue) {
+ matches =
+ isMatch(propertyName, searchPropertyName, strictSearchPropertyName) &&
+ isMatch(propertyValue, searchPropertyValue, strictSearchPropertyValue);
+ } else {
+ matches =
+ isMatch(propertyName, searchPropertyName,
+ strictSearchPropertyName || strictSearchAllValues) ||
+ isMatch(propertyValue, searchPropertyValue,
+ strictSearchPropertyValue || strictSearchAllValues);
+ }
+
+ if (matches) {
+ element.classList.add("ruleview-highlight");
+ }
+
+ return matches;
+ },
+
+ /**
+ * Clear all search filter highlights in the panel, and close the computed
+ * list if toggled opened
+ */
+ _clearHighlight: function (element) {
+ for (let el of element.querySelectorAll(".ruleview-highlight")) {
+ el.classList.remove("ruleview-highlight");
+ }
+
+ for (let computed of element.querySelectorAll(
+ ".ruleview-computedlist[filter-open]")) {
+ computed.parentNode._textPropertyEditor.collapseForFilter();
+ }
+ },
+
+ /**
+ * Called when the pseudo class panel button is clicked and toggles
+ * the display of the pseudo class panel.
+ */
+ _onTogglePseudoClassPanel: function () {
+ if (this.pseudoClassPanel.hidden) {
+ this.pseudoClassToggle.setAttribute("checked", "true");
+ this.hoverCheckbox.setAttribute("tabindex", "0");
+ this.activeCheckbox.setAttribute("tabindex", "0");
+ this.focusCheckbox.setAttribute("tabindex", "0");
+ } else {
+ this.pseudoClassToggle.removeAttribute("checked");
+ this.hoverCheckbox.setAttribute("tabindex", "-1");
+ this.activeCheckbox.setAttribute("tabindex", "-1");
+ this.focusCheckbox.setAttribute("tabindex", "-1");
+ }
+
+ this.pseudoClassPanel.hidden = !this.pseudoClassPanel.hidden;
+ },
+
+ /**
+ * Called when a pseudo class checkbox is clicked and toggles
+ * the pseudo class for the current selected element.
+ */
+ _onTogglePseudoClass: function (event) {
+ let target = event.currentTarget;
+ this.inspector.togglePseudoClass(target.value);
+ },
+
+ /**
+ * Handle the keypress event in the rule view.
+ */
+ _onShortcut: function (name, event) {
+ if (!event.target.closest("#sidebar-panel-ruleview")) {
+ return;
+ }
+
+ if (name === "CmdOrCtrl+F") {
+ this.searchField.focus();
+ event.preventDefault();
+ } else if ((name === "Return" || name === "Space") &&
+ this.element.classList.contains("non-interactive")) {
+ event.preventDefault();
+ } else if (name === "Escape" &&
+ event.target === this.searchField &&
+ this._onClearSearch()) {
+ // Handle the search box's keypress event. If the escape key is pressed,
+ // clear the search box field.
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ }
+
+};
+
+/**
+ * Helper functions
+ */
+
+/**
+ * Walk up the DOM from a given node until a parent property holder is found.
+ * For elements inside the computed property list, the non-computed parent
+ * property holder will be returned
+ *
+ * @param {DOMNode} node
+ * The node to start from
+ * @return {DOMNode} The parent property holder node, or null if not found
+ */
+function getParentTextPropertyHolder(node) {
+ while (true) {
+ if (!node || !node.classList) {
+ return null;
+ }
+ if (node.classList.contains("ruleview-property")) {
+ return node;
+ }
+ node = node.parentNode;
+ }
+}
+
+/**
+ * For any given node, find the TextProperty it is in if any
+ * @param {DOMNode} node
+ * The node to start from
+ * @return {TextProperty}
+ */
+function getParentTextProperty(node) {
+ let parent = getParentTextPropertyHolder(node);
+ if (!parent) {
+ return null;
+ }
+
+ let propValue = parent.querySelector(".ruleview-propertyvalue");
+ if (!propValue) {
+ return null;
+ }
+
+ return propValue.textProperty;
+}
+
+/**
+ * Walker up the DOM from a given node until a parent property holder is found,
+ * and return the textContent for the name and value nodes.
+ * Stops at the first property found, so if node is inside the computed property
+ * list, the computed property will be returned
+ *
+ * @param {DOMNode} node
+ * The node to start from
+ * @return {Object} {name, value}
+ */
+function getPropertyNameAndValue(node) {
+ while (true) {
+ if (!node || !node.classList) {
+ return null;
+ }
+ // Check first for ruleview-computed since it's the deepest
+ if (node.classList.contains("ruleview-computed") ||
+ node.classList.contains("ruleview-property")) {
+ return {
+ name: node.querySelector(".ruleview-propertyname").textContent,
+ value: node.querySelector(".ruleview-propertyvalue").textContent
+ };
+ }
+ node = node.parentNode;
+ }
+}
+
+function RuleViewTool(inspector, window) {
+ this.inspector = inspector;
+ this.document = window.document;
+
+ this.view = new CssRuleView(this.inspector, this.document);
+
+ this.clearUserProperties = this.clearUserProperties.bind(this);
+ this.refresh = this.refresh.bind(this);
+ this.onLinkClicked = this.onLinkClicked.bind(this);
+ this.onMutations = this.onMutations.bind(this);
+ this.onPanelSelected = this.onPanelSelected.bind(this);
+ this.onPropertyChanged = this.onPropertyChanged.bind(this);
+ this.onResized = this.onResized.bind(this);
+ this.onSelected = this.onSelected.bind(this);
+ this.onViewRefreshed = this.onViewRefreshed.bind(this);
+
+ this.view.on("ruleview-changed", this.onPropertyChanged);
+ this.view.on("ruleview-refreshed", this.onViewRefreshed);
+ this.view.on("ruleview-linked-clicked", this.onLinkClicked);
+
+ this.inspector.selection.on("detached-front", this.onSelected);
+ this.inspector.selection.on("new-node-front", this.onSelected);
+ this.inspector.selection.on("pseudoclass", this.refresh);
+ this.inspector.target.on("navigate", this.clearUserProperties);
+ this.inspector.sidebar.on("ruleview-selected", this.onPanelSelected);
+ this.inspector.pageStyle.on("stylesheet-updated", this.refresh);
+ this.inspector.walker.on("mutations", this.onMutations);
+ this.inspector.walker.on("resize", this.onResized);
+
+ this.onSelected();
+}
+
+RuleViewTool.prototype = {
+ isSidebarActive: function () {
+ if (!this.view) {
+ return false;
+ }
+ return this.inspector.sidebar.getCurrentTabID() == "ruleview";
+ },
+
+ onSelected: function (event) {
+ // Ignore the event if the view has been destroyed, or if it's inactive.
+ // But only if the current selection isn't null. If it's been set to null,
+ // let the update go through as this is needed to empty the view on
+ // navigation.
+ if (!this.view) {
+ return;
+ }
+
+ let isInactive = !this.isSidebarActive() &&
+ this.inspector.selection.nodeFront;
+ if (isInactive) {
+ return;
+ }
+
+ this.view.setPageStyle(this.inspector.pageStyle);
+
+ if (!this.inspector.selection.isConnected() ||
+ !this.inspector.selection.isElementNode()) {
+ this.view.selectElement(null);
+ return;
+ }
+
+ if (!event || event == "new-node-front") {
+ let done = this.inspector.updating("rule-view");
+ this.view.selectElement(this.inspector.selection.nodeFront)
+ .then(done, done);
+ }
+ },
+
+ refresh: function () {
+ if (this.isSidebarActive()) {
+ this.view.refreshPanel();
+ }
+ },
+
+ clearUserProperties: function () {
+ if (this.view && this.view.store && this.view.store.userProperties) {
+ this.view.store.userProperties.clear();
+ }
+ },
+
+ onPanelSelected: function () {
+ if (this.inspector.selection.nodeFront === this.view._viewedElement) {
+ this.refresh();
+ } else {
+ this.onSelected();
+ }
+ },
+
+ onLinkClicked: function (e, rule) {
+ let sheet = rule.parentStyleSheet;
+
+ // Chrome stylesheets are not listed in the style editor, so show
+ // these sheets in the view source window instead.
+ if (!sheet || sheet.isSystem) {
+ let href = rule.nodeHref || rule.href;
+ let toolbox = gDevTools.getToolbox(this.inspector.target);
+ toolbox.viewSource(href, rule.line);
+ return;
+ }
+
+ let location = promise.resolve(rule.location);
+ if (Services.prefs.getBoolPref(PREF_ORIG_SOURCES)) {
+ location = rule.getOriginalLocation();
+ }
+ location.then(({ source, href, line, column }) => {
+ let target = this.inspector.target;
+ if (Tools.styleEditor.isTargetSupported(target)) {
+ gDevTools.showToolbox(target, "styleeditor").then(function (toolbox) {
+ let url = source || href;
+ toolbox.getCurrentPanel().selectStyleSheet(url, line, column);
+ });
+ }
+ return;
+ });
+ },
+
+ onPropertyChanged: function () {
+ this.inspector.markDirty();
+ },
+
+ onViewRefreshed: function () {
+ this.inspector.emit("rule-view-refreshed");
+ },
+
+ /**
+ * When markup mutations occur, if an attribute of the selected node changes,
+ * we need to refresh the view as that might change the node's styles.
+ */
+ onMutations: function (mutations) {
+ for (let {type, target} of mutations) {
+ if (target === this.inspector.selection.nodeFront &&
+ type === "attributes") {
+ this.refresh();
+ break;
+ }
+ }
+ },
+
+ /**
+ * When the window gets resized, this may cause media-queries to match, and
+ * therefore, different styles may apply.
+ */
+ onResized: function () {
+ this.refresh();
+ },
+
+ destroy: function () {
+ this.inspector.walker.off("mutations", this.onMutations);
+ this.inspector.walker.off("resize", this.onResized);
+ this.inspector.selection.off("detached-front", this.onSelected);
+ this.inspector.selection.off("pseudoclass", this.refresh);
+ this.inspector.selection.off("new-node-front", this.onSelected);
+ this.inspector.target.off("navigate", this.clearUserProperties);
+ this.inspector.sidebar.off("ruleview-selected", this.onPanelSelected);
+ if (this.inspector.pageStyle) {
+ this.inspector.pageStyle.off("stylesheet-updated", this.refresh);
+ }
+
+ this.view.off("ruleview-linked-clicked", this.onLinkClicked);
+ this.view.off("ruleview-changed", this.onPropertyChanged);
+ this.view.off("ruleview-refreshed", this.onViewRefreshed);
+
+ this.view.destroy();
+
+ this.view = this.document = this.inspector = null;
+ }
+};
+
+exports.CssRuleView = CssRuleView;
+exports.RuleViewTool = RuleViewTool;
diff --git a/devtools/client/inspector/rules/test/.eslintrc.js b/devtools/client/inspector/rules/test/.eslintrc.js
new file mode 100644
index 000000000..698ae9181
--- /dev/null
+++ b/devtools/client/inspector/rules/test/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../../../.eslintrc.mochitests.js"
+};
diff --git a/devtools/client/inspector/rules/test/browser.ini b/devtools/client/inspector/rules/test/browser.ini
new file mode 100644
index 000000000..2c11219fb
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser.ini
@@ -0,0 +1,221 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ doc_author-sheet.html
+ doc_blob_stylesheet.html
+ doc_content_stylesheet.html
+ doc_content_stylesheet_imported.css
+ doc_content_stylesheet_imported2.css
+ doc_content_stylesheet_linked.css
+ doc_content_stylesheet_script.css
+ doc_copystyles.css
+ doc_copystyles.html
+ doc_cssom.html
+ doc_custom.html
+ doc_filter.html
+ doc_frame_script.js
+ doc_inline_sourcemap.html
+ doc_invalid_sourcemap.css
+ doc_invalid_sourcemap.html
+ doc_keyframeanimation.css
+ doc_keyframeanimation.html
+ doc_keyframeLineNumbers.html
+ doc_media_queries.html
+ doc_pseudoelement.html
+ doc_ruleLineNumbers.html
+ doc_sourcemaps.css
+ doc_sourcemaps.css.map
+ doc_sourcemaps.html
+ doc_sourcemaps.scss
+ doc_style_editor_link.css
+ doc_test_image.png
+ doc_urls_clickable.css
+ doc_urls_clickable.html
+ head.js
+ !/devtools/client/commandline/test/helpers.js
+ !/devtools/client/framework/test/shared-head.js
+ !/devtools/client/inspector/test/head.js
+ !/devtools/client/inspector/test/shared-head.js
+ !/devtools/client/shared/test/test-actor.js
+ !/devtools/client/shared/test/test-actor-registry.js
+
+[browser_rules_add-property-and-reselect.js]
+[browser_rules_add-property-cancel_01.js]
+[browser_rules_add-property-cancel_02.js]
+[browser_rules_add-property-cancel_03.js]
+[browser_rules_add-property-commented.js]
+[browser_rules_add-property_01.js]
+[browser_rules_add-property_02.js]
+[browser_rules_add-property-svg.js]
+[browser_rules_add-rule-and-property.js]
+[browser_rules_add-rule-button-state.js]
+[browser_rules_add-rule-edit-selector.js]
+[browser_rules_add-rule-iframes.js]
+[browser_rules_add-rule-namespace-elements.js]
+[browser_rules_add-rule-pseudo-class.js]
+[browser_rules_add-rule-then-property-edit-selector.js]
+[browser_rules_add-rule-with-menu.js]
+[browser_rules_add-rule.js]
+[browser_rules_authored.js]
+[browser_rules_authored_color.js]
+[browser_rules_authored_override.js]
+[browser_rules_blob_stylesheet.js]
+[browser_rules_colorpicker-and-image-tooltip_01.js]
+[browser_rules_colorpicker-and-image-tooltip_02.js]
+[browser_rules_colorpicker-appears-on-swatch-click.js]
+[browser_rules_colorpicker-commit-on-ENTER.js]
+[browser_rules_colorpicker-edit-gradient.js]
+[browser_rules_colorpicker-hides-on-tooltip.js]
+[browser_rules_colorpicker-multiple-changes.js]
+[browser_rules_colorpicker-release-outside-frame.js]
+[browser_rules_colorpicker-revert-on-ESC.js]
+[browser_rules_colorpicker-swatch-displayed.js]
+[browser_rules_colorUnit.js]
+[browser_rules_completion-existing-property_01.js]
+[browser_rules_completion-existing-property_02.js]
+[browser_rules_completion-new-property_01.js]
+[browser_rules_completion-new-property_02.js]
+[browser_rules_completion-new-property_03.js]
+[browser_rules_completion-new-property_04.js]
+[browser_rules_completion-new-property_multiline.js]
+[browser_rules_computed-lists_01.js]
+[browser_rules_computed-lists_02.js]
+[browser_rules_completion-popup-hidden-after-navigation.js]
+[browser_rules_content_01.js]
+[browser_rules_content_02.js]
+skip-if = e10s && debug # Bug 1250058 - Docshell leak on debug e10s
+[browser_rules_context-menu-show-mdn-docs-01.js]
+[browser_rules_context-menu-show-mdn-docs-02.js]
+[browser_rules_context-menu-show-mdn-docs-03.js]
+[browser_rules_copy_styles.js]
+subsuite = clipboard
+[browser_rules_cssom.js]
+[browser_rules_cubicbezier-appears-on-swatch-click.js]
+[browser_rules_cubicbezier-commit-on-ENTER.js]
+[browser_rules_cubicbezier-revert-on-ESC.js]
+[browser_rules_custom.js]
+[browser_rules_cycle-angle.js]
+[browser_rules_cycle-color.js]
+[browser_rules_edit-display-grid-property.js]
+[browser_rules_edit-property-cancel.js]
+[browser_rules_edit-property-click.js]
+[browser_rules_edit-property-commit.js]
+[browser_rules_edit-property-computed.js]
+[browser_rules_edit-property-increments.js]
+[browser_rules_edit-property-order.js]
+[browser_rules_edit-property-remove_01.js]
+[browser_rules_edit-property-remove_02.js]
+[browser_rules_edit-property-remove_03.js]
+[browser_rules_edit-property_01.js]
+[browser_rules_edit-property_02.js]
+[browser_rules_edit-property_03.js]
+[browser_rules_edit-property_04.js]
+[browser_rules_edit-property_05.js]
+[browser_rules_edit-property_06.js]
+[browser_rules_edit-property_07.js]
+[browser_rules_edit-property_08.js]
+[browser_rules_edit-property_09.js]
+[browser_rules_edit-selector-click.js]
+[browser_rules_edit-selector-click-on-scrollbar.js]
+skip-if = os == "mac" # Bug 1245996 : click on scrollbar not working on OSX
+[browser_rules_edit-selector-commit.js]
+[browser_rules_edit-selector_01.js]
+[browser_rules_edit-selector_02.js]
+[browser_rules_edit-selector_03.js]
+[browser_rules_edit-selector_04.js]
+[browser_rules_edit-selector_05.js]
+[browser_rules_edit-selector_06.js]
+[browser_rules_edit-selector_07.js]
+[browser_rules_edit-selector_08.js]
+[browser_rules_edit-selector_09.js]
+[browser_rules_edit-selector_10.js]
+[browser_rules_edit-selector_11.js]
+[browser_rules_edit-value-after-name_01.js]
+[browser_rules_edit-value-after-name_02.js]
+[browser_rules_edit-value-after-name_03.js]
+[browser_rules_edit-value-after-name_04.js]
+[browser_rules_editable-field-focus_01.js]
+[browser_rules_editable-field-focus_02.js]
+[browser_rules_eyedropper.js]
+[browser_rules_filtereditor-appears-on-swatch-click.js]
+[browser_rules_filtereditor-commit-on-ENTER.js]
+[browser_rules_filtereditor-revert-on-ESC.js]
+skip-if = (os == "win" && debug) # bug 963492: win.
+[browser_rules_grid-highlighter-on-navigate.js]
+[browser_rules_grid-highlighter-on-reload.js]
+[browser_rules_grid-toggle_01.js]
+[browser_rules_grid-toggle_02.js]
+[browser_rules_grid-toggle_03.js]
+[browser_rules_guessIndentation.js]
+[browser_rules_inherited-properties_01.js]
+[browser_rules_inherited-properties_02.js]
+[browser_rules_inherited-properties_03.js]
+[browser_rules_inline-source-map.js]
+[browser_rules_invalid.js]
+[browser_rules_invalid-source-map.js]
+[browser_rules_keybindings.js]
+[browser_rules_keyframes-rule_01.js]
+[browser_rules_keyframes-rule_02.js]
+[browser_rules_keyframeLineNumbers.js]
+[browser_rules_lineNumbers.js]
+[browser_rules_livepreview.js]
+[browser_rules_mark_overridden_01.js]
+[browser_rules_mark_overridden_02.js]
+[browser_rules_mark_overridden_03.js]
+[browser_rules_mark_overridden_04.js]
+[browser_rules_mark_overridden_05.js]
+[browser_rules_mark_overridden_06.js]
+[browser_rules_mark_overridden_07.js]
+[browser_rules_mathml-element.js]
+[browser_rules_media-queries.js]
+[browser_rules_multiple-properties-duplicates.js]
+[browser_rules_multiple-properties-priority.js]
+[browser_rules_multiple-properties-unfinished_01.js]
+[browser_rules_multiple-properties-unfinished_02.js]
+[browser_rules_multiple_properties_01.js]
+[browser_rules_multiple_properties_02.js]
+[browser_rules_original-source-link.js]
+[browser_rules_pseudo-element_01.js]
+[browser_rules_pseudo-element_02.js]
+[browser_rules_pseudo_lock_options.js]
+[browser_rules_refresh-no-flicker.js]
+[browser_rules_refresh-on-attribute-change_01.js]
+[browser_rules_refresh-on-attribute-change_02.js]
+[browser_rules_refresh-on-style-change.js]
+[browser_rules_search-filter-computed-list_01.js]
+[browser_rules_search-filter-computed-list_02.js]
+[browser_rules_search-filter-computed-list_03.js]
+[browser_rules_search-filter-computed-list_04.js]
+[browser_rules_search-filter-computed-list_expander.js]
+[browser_rules_search-filter-overridden-property.js]
+[browser_rules_search-filter_01.js]
+[browser_rules_search-filter_02.js]
+[browser_rules_search-filter_03.js]
+[browser_rules_search-filter_04.js]
+[browser_rules_search-filter_05.js]
+[browser_rules_search-filter_06.js]
+[browser_rules_search-filter_07.js]
+[browser_rules_search-filter_08.js]
+[browser_rules_search-filter_09.js]
+[browser_rules_search-filter_10.js]
+[browser_rules_search-filter_context-menu.js]
+subsuite = clipboard
+[browser_rules_search-filter_escape-keypress.js]
+[browser_rules_select-and-copy-styles.js]
+subsuite = clipboard
+[browser_rules_selector-highlighter-on-navigate.js]
+[browser_rules_selector-highlighter_01.js]
+[browser_rules_selector-highlighter_02.js]
+[browser_rules_selector-highlighter_03.js]
+[browser_rules_selector-highlighter_04.js]
+[browser_rules_selector_highlight.js]
+[browser_rules_strict-search-filter-computed-list_01.js]
+[browser_rules_strict-search-filter_01.js]
+[browser_rules_strict-search-filter_02.js]
+[browser_rules_strict-search-filter_03.js]
+[browser_rules_style-editor-link.js]
+[browser_rules_urls-clickable.js]
+[browser_rules_user-agent-styles.js]
+[browser_rules_user-agent-styles-uneditable.js]
+[browser_rules_user-property-reset.js]
diff --git a/devtools/client/inspector/rules/test/browser_rules_add-property-and-reselect.js b/devtools/client/inspector/rules/test/browser_rules_add-property-and-reselect.js
new file mode 100644
index 000000000..492739abe
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_add-property-and-reselect.js
@@ -0,0 +1,44 @@
+/* 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";
+
+// Tests that adding properties to rules work and reselecting the element still
+// show them.
+
+const TEST_URI = URL_ROOT + "doc_content_stylesheet.html";
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#target", inspector);
+
+ info("Setting a font-weight property on all rules");
+ yield setPropertyOnAllRules(view);
+
+ info("Reselecting the element");
+ yield selectNode("body", inspector);
+ yield selectNode("#target", inspector);
+
+ checkPropertyOnAllRules(view);
+});
+
+function* setPropertyOnAllRules(view) {
+ // Wait for the properties to be properly created on the backend and for the
+ // view to be updated.
+ let onRefreshed = view.once("ruleview-refreshed");
+ for (let rule of view._elementStyle.rules) {
+ rule.editor.addProperty("font-weight", "bold", "", true);
+ }
+ yield onRefreshed;
+}
+
+function checkPropertyOnAllRules(view) {
+ for (let rule of view._elementStyle.rules) {
+ let lastRule = rule.textProps[rule.textProps.length - 1];
+
+ is(lastRule.name, "font-weight", "Last rule name is font-weight");
+ is(lastRule.value, "bold", "Last rule value is bold");
+ }
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_add-property-cancel_01.js b/devtools/client/inspector/rules/test/browser_rules_add-property-cancel_01.js
new file mode 100644
index 000000000..78b3a4c91
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_add-property-cancel_01.js
@@ -0,0 +1,44 @@
+/* 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";
+
+// Tests adding a new property and escapes the new empty property name editor.
+
+const TEST_URI = `
+ <style type="text/css">
+ #testid {
+ background-color: blue;
+ }
+ .testclass {
+ background-color: green;
+ }
+ </style>
+ <div id='testid' class='testclass'>Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ let elementRuleEditor = getRuleViewRuleEditor(view, 0);
+ let editor = yield focusNewRuleViewProperty(elementRuleEditor);
+ is(inplaceEditor(elementRuleEditor.newPropSpan), editor,
+ "The new property editor got focused");
+
+ info("Escape the new property editor");
+ let onBlur = once(editor.input, "blur");
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, view.styleWindow);
+ yield onBlur;
+
+ info("Checking the state of cancelling a new property name editor");
+ is(elementRuleEditor.rule.textProps.length, 0,
+ "Should have cancelled creating a new text property.");
+ ok(!elementRuleEditor.propertyList.hasChildNodes(),
+ "Should not have any properties.");
+
+ is(view.styleDocument.activeElement, view.styleDocument.body,
+ "Correct element has focus");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_add-property-cancel_02.js b/devtools/client/inspector/rules/test/browser_rules_add-property-cancel_02.js
new file mode 100644
index 000000000..7f4d1564c
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_add-property-cancel_02.js
@@ -0,0 +1,34 @@
+/* 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";
+
+// Tests adding a new property and escapes the new empty property value editor.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #testid {
+ background-color: blue;
+ }
+ </style>
+ <div id='testid'>Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ info("Test creating a new property and escaping");
+ yield addProperty(view, 1, "color", "red", "VK_ESCAPE", false);
+
+ is(view.styleDocument.activeElement, view.styleDocument.body,
+ "Correct element has focus");
+
+ let elementRuleEditor = getRuleViewRuleEditor(view, 1);
+ is(elementRuleEditor.rule.textProps.length, 1,
+ "Removed the new text property.");
+ is(elementRuleEditor.propertyList.children.length, 1,
+ "Removed the property editor.");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_add-property-cancel_03.js b/devtools/client/inspector/rules/test/browser_rules_add-property-cancel_03.js
new file mode 100644
index 000000000..4f8b42009
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_add-property-cancel_03.js
@@ -0,0 +1,43 @@
+/* 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";
+
+// Tests adding a new property and escapes the property name editor with a
+// value.
+
+const TEST_URI = `
+ <style type='text/css'>
+ div {
+ background-color: blue;
+ }
+ </style>
+ <div>Test node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("div", inspector);
+
+ // Add a property to the element's style declaration, add some text,
+ // then press escape.
+
+ let elementRuleEditor = getRuleViewRuleEditor(view, 1);
+ let editor = yield focusNewRuleViewProperty(elementRuleEditor);
+
+ is(inplaceEditor(elementRuleEditor.newPropSpan), editor,
+ "Next focused editor should be the new property editor.");
+
+ EventUtils.sendString("background", view.styleWindow);
+
+ let onBlur = once(editor.input, "blur");
+ EventUtils.synthesizeKey("VK_ESCAPE", {});
+ yield onBlur;
+
+ is(elementRuleEditor.rule.textProps.length, 1,
+ "Should have canceled creating a new text property.");
+ is(view.styleDocument.activeElement, view.styleDocument.body,
+ "Correct element has focus");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_add-property-commented.js b/devtools/client/inspector/rules/test/browser_rules_add-property-commented.js
new file mode 100644
index 000000000..eacf5db5a
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_add-property-commented.js
@@ -0,0 +1,47 @@
+/* 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 commented properties can be added and are disabled.
+
+const TEST_URI = "<div id='testid'></div>";
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+ yield testCreateNewSetOfCommentedAndUncommentedProperties(view);
+});
+
+function* testCreateNewSetOfCommentedAndUncommentedProperties(view) {
+ info("Test creating a new set of commented and uncommented properties");
+
+ info("Focusing a new property name in the rule-view");
+ let ruleEditor = getRuleViewRuleEditor(view, 0);
+ let editor = yield focusEditableField(view, ruleEditor.closeBrace);
+ is(inplaceEditor(ruleEditor.newPropSpan), editor,
+ "The new property editor has focus");
+
+ info(
+ "Entering a commented property/value pair into the property name editor");
+ let input = editor.input;
+ input.value = `color: blue;
+ /* background-color: yellow; */
+ width: 200px;
+ height: 100px;
+ /* padding-bottom: 1px; */`;
+
+ info("Pressing return to commit and focus the new value field");
+ let onModifications = view.once("ruleview-changed");
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
+ yield onModifications;
+
+ let textProps = ruleEditor.rule.textProps;
+ ok(textProps[0].enabled, "The 'color' property is enabled.");
+ ok(!textProps[1].enabled, "The 'background-color' property is disabled.");
+ ok(textProps[2].enabled, "The 'width' property is enabled.");
+ ok(textProps[3].enabled, "The 'height' property is enabled.");
+ ok(!textProps[4].enabled, "The 'padding-bottom' property is disabled.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_add-property-svg.js b/devtools/client/inspector/rules/test/browser_rules_add-property-svg.js
new file mode 100644
index 000000000..a53421db3
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_add-property-svg.js
@@ -0,0 +1,22 @@
+/* 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";
+
+// Tests editing SVG styles using the rules view.
+
+var TEST_URL = "chrome://global/skin/icons/warning.svg";
+var TEST_SELECTOR = "path";
+
+add_task(function* () {
+ yield addTab(TEST_URL);
+ let {inspector, view} = yield openRuleView();
+ yield selectNode(TEST_SELECTOR, inspector);
+
+ info("Test creating a new property");
+ yield addProperty(view, 0, "fill", "red");
+
+ is((yield getComputedStyleProperty(TEST_SELECTOR, null, "fill")),
+ "rgb(255, 0, 0)", "The fill was changed to red");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_add-property_01.js b/devtools/client/inspector/rules/test/browser_rules_add-property_01.js
new file mode 100644
index 000000000..1d7068d54
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_add-property_01.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";
+
+// Test adding an invalid property.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #testid {
+ background-color: blue;
+ }
+ .testclass {
+ background-color: green;
+ }
+ </style>
+ <div id='testid' class='testclass'>Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ info("Test creating a new property");
+ let textProp = yield addProperty(view, 0, "background-color", "#XYZ");
+
+ is(textProp.value, "#XYZ", "Text prop should have been changed.");
+ is(textProp.overridden, true, "Property should be overridden");
+ is(textProp.editor.isValid(), false, "#XYZ should not be a valid entry");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_add-property_02.js b/devtools/client/inspector/rules/test/browser_rules_add-property_02.js
new file mode 100644
index 000000000..6f6bef0f7
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_add-property_02.js
@@ -0,0 +1,65 @@
+/* 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 adding a valid property to a CSS rule, and navigating through the fields
+// by pressing ENTER.
+
+const TEST_URI = `
+ <style type="text/css">
+ #testid {
+ color: blue;
+ }
+ </style>
+ <div id='testid'>Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ info("Focus the new property name field");
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+ let editor = yield focusNewRuleViewProperty(ruleEditor);
+ let input = editor.input;
+
+ is(inplaceEditor(ruleEditor.newPropSpan), editor,
+ "Next focused editor should be the new property editor.");
+ ok(input.selectionStart === 0 && input.selectionEnd === input.value.length,
+ "Editor contents are selected.");
+
+ // Try clicking on the editor's input again, shouldn't cause trouble
+ // (see bug 761665).
+ EventUtils.synthesizeMouse(input, 1, 1, {}, view.styleWindow);
+ input.select();
+
+ info("Entering the property name");
+ editor.input.value = "background-color";
+
+ info("Pressing RETURN and waiting for the value field focus");
+ let onNameAdded = view.once("ruleview-changed");
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
+
+ yield onNameAdded;
+
+ editor = inplaceEditor(view.styleDocument.activeElement);
+
+ is(ruleEditor.rule.textProps.length, 2,
+ "Should have created a new text property.");
+ is(ruleEditor.propertyList.children.length, 2,
+ "Should have created a property editor.");
+ let textProp = ruleEditor.rule.textProps[1];
+ is(editor, inplaceEditor(textProp.editor.valueSpan),
+ "Should be editing the value span now.");
+
+ info("Entering the property value");
+ let onValueAdded = view.once("ruleview-changed");
+ editor.input.value = "purple";
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
+ yield onValueAdded;
+
+ is(textProp.value, "purple", "Text prop should have been changed.");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_add-rule-and-property.js b/devtools/client/inspector/rules/test/browser_rules_add-rule-and-property.js
new file mode 100644
index 000000000..1cf04a275
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_add-rule-and-property.js
@@ -0,0 +1,30 @@
+/* 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";
+
+// Tests adding a new rule and a new property in this rule.
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8,<div id='testid'>Styled Node</div>");
+ let {inspector, view} = yield openRuleView();
+
+ info("Selecting the test node");
+ yield selectNode("#testid", inspector);
+
+ info("Adding a new rule for this node and blurring the new selector field");
+ yield addNewRuleAndDismissEditor(inspector, view, "#testid", 1);
+
+ info("Adding a new property for this rule");
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+
+ let onRuleViewChanged = view.once("ruleview-changed");
+ ruleEditor.addProperty("font-weight", "bold", "", true);
+ yield onRuleViewChanged;
+
+ let textProps = ruleEditor.rule.textProps;
+ let prop = textProps[textProps.length - 1];
+ is(prop.name, "font-weight", "The last property name is font-weight");
+ is(prop.value, "bold", "The last property value is bold");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_add-rule-button-state.js b/devtools/client/inspector/rules/test/browser_rules_add-rule-button-state.js
new file mode 100644
index 000000000..1441213b3
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_add-rule-button-state.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";
+
+// Tests if the `Add rule` button disables itself properly for non-element nodes
+// and anonymous element.
+
+const TEST_URI = `
+ <style type="text/css">
+ #pseudo::before {
+ content: "before";
+ }
+ </style>
+ <div id="pseudo"></div>
+ <div id="testid">Test Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield testDisabledButton(inspector, view);
+});
+
+function* testDisabledButton(inspector, view) {
+ let node = "#testid";
+
+ info("Selecting a real element");
+ yield selectNode(node, inspector);
+ ok(!view.addRuleButton.disabled, "Add rule button should be enabled");
+
+ info("Select a null element");
+ yield view.selectElement(null);
+ ok(view.addRuleButton.disabled, "Add rule button should be disabled");
+
+ info("Selecting a real element");
+ yield selectNode(node, inspector);
+ ok(!view.addRuleButton.disabled, "Add rule button should be enabled");
+
+ info("Selecting a pseudo element");
+ let pseudo = yield getNodeFront("#pseudo", inspector);
+ let children = yield inspector.walker.children(pseudo);
+ let before = children.nodes[0];
+ yield selectNode(before, inspector);
+ ok(view.addRuleButton.disabled, "Add rule button should be disabled");
+
+ info("Selecting a real element");
+ yield selectNode(node, inspector);
+ ok(!view.addRuleButton.disabled, "Add rule button should be enabled");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_add-rule-edit-selector.js b/devtools/client/inspector/rules/test/browser_rules_add-rule-edit-selector.js
new file mode 100644
index 000000000..b59f317a5
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_add-rule-edit-selector.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";
+
+// Tests the behaviour of adding a new rule to the rule view and editing
+// its selector.
+
+const TEST_URI = `
+ <style type="text/css">
+ #testid {
+ text-align: center;
+ }
+ </style>
+ <div id="testid">Styled Node</div>
+ <span>This is a span</span>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ yield addNewRule(inspector, view);
+ yield testEditSelector(view, "span");
+
+ info("Selecting the modified element with the new rule");
+ yield selectNode("span", inspector);
+ yield checkModifiedElement(view, "span");
+});
+
+function* testEditSelector(view, name) {
+ info("Test editing existing selector field");
+ let idRuleEditor = getRuleViewRuleEditor(view, 1);
+ let editor = idRuleEditor.selectorText.ownerDocument.activeElement;
+
+ info("Entering a new selector name and committing");
+ editor.value = name;
+
+ info("Waiting for rule view to update");
+ let onRuleViewChanged = once(view, "ruleview-changed");
+
+ info("Entering the commit key");
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ yield onRuleViewChanged;
+
+ is(view._elementStyle.rules.length, 3, "Should have 3 rules.");
+ ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists.");
+}
+
+function* checkModifiedElement(view, name) {
+ is(view._elementStyle.rules.length, 2, "Should have 2 rules.");
+ ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_add-rule-iframes.js b/devtools/client/inspector/rules/test/browser_rules_add-rule-iframes.js
new file mode 100644
index 000000000..7b0ba7812
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_add-rule-iframes.js
@@ -0,0 +1,57 @@
+/* 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";
+
+// Tests adding a rule on elements nested in iframes.
+
+const TEST_URI =
+ `<div>outer</div>
+ <iframe id="frame1" src="data:text/html;charset=utf-8,<div>inner1</div>">
+ </iframe>
+ <iframe id="frame2" src="data:text/html;charset=utf-8,<div>inner2</div>">
+ </iframe>`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("div", inspector);
+ yield addNewRuleAndDismissEditor(inspector, view, "div", 1);
+ yield addNewProperty(view, 1, "color", "red");
+
+ let innerFrameDiv1 = yield getNodeFrontInFrame("div", "#frame1", inspector);
+ yield selectNode(innerFrameDiv1, inspector);
+ yield addNewRuleAndDismissEditor(inspector, view, "div", 1);
+ yield addNewProperty(view, 1, "color", "blue");
+
+ let innerFrameDiv2 = yield getNodeFrontInFrame("div", "#frame2", inspector);
+ yield selectNode(innerFrameDiv2, inspector);
+ yield addNewRuleAndDismissEditor(inspector, view, "div", 1);
+ yield addNewProperty(view, 1, "color", "green");
+});
+
+/**
+ * Add a new property in the rule at the provided index in the rule view.
+ *
+ * @param {RuleView} view
+ * @param {Number} index
+ * The index of the rule in which we should add a new property.
+ * @param {String} name
+ * The name of the new property.
+ * @param {String} value
+ * The value of the new property.
+ */
+function* addNewProperty(view, index, name, value) {
+ let idRuleEditor = getRuleViewRuleEditor(view, index);
+ info(`Adding new property "${name}: ${value};"`);
+
+ let onRuleViewChanged = view.once("ruleview-changed");
+ idRuleEditor.addProperty(name, value, "", true);
+ yield onRuleViewChanged;
+
+ let textProps = idRuleEditor.rule.textProps;
+ let lastProperty = textProps[textProps.length - 1];
+ is(lastProperty.name, name, "Last property has the expected name");
+ is(lastProperty.value, value, "Last property has the expected value");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_add-rule-namespace-elements.js b/devtools/client/inspector/rules/test/browser_rules_add-rule-namespace-elements.js
new file mode 100644
index 000000000..98e34e69f
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_add-rule-namespace-elements.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";
+
+// Tests the behaviour of adding a new rule using the add rule button
+// on namespaced elements.
+
+const XHTML = `
+ <!DOCTYPE html>
+ <html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:svg="http://www.w3.org/2000/svg">
+ <body>
+ <svg:svg width="100" height="100">
+ <svg:clipPath>
+ <svg:rect x="0" y="0" width="10" height="5"></svg:rect>
+ </svg:clipPath>
+ <svg:circle cx="0" cy="0" r="5"></svg:circle>
+ </svg:svg>
+ </body>
+ </html>
+`;
+const TEST_URI = "data:application/xhtml+xml;charset=utf-8," + encodeURI(XHTML);
+
+const TEST_DATA = [
+ { node: "clipPath", expected: "clipPath" },
+ { node: "rect", expected: "rect" },
+ { node: "circle", expected: "circle" }
+];
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let {inspector, view} = yield openRuleView();
+
+ for (let data of TEST_DATA) {
+ let {node, expected} = data;
+ yield selectNode(node, inspector);
+ yield addNewRuleAndDismissEditor(inspector, view, expected, 1);
+ }
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_add-rule-pseudo-class.js b/devtools/client/inspector/rules/test/browser_rules_add-rule-pseudo-class.js
new file mode 100644
index 000000000..39f773c13
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_add-rule-pseudo-class.js
@@ -0,0 +1,82 @@
+/* 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";
+
+// Tests adding a rule with pseudo class locks on.
+
+const TEST_URI = "<p id='element'>Test element</p>";
+
+const EXPECTED_SELECTOR = "#element";
+const TEST_DATA = [
+ [],
+ [":hover"],
+ [":hover", ":active"],
+ [":hover", ":active", ":focus"],
+ [":active"],
+ [":active", ":focus"],
+ [":focus"]
+];
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#element", inspector);
+
+ for (let data of TEST_DATA) {
+ yield runTestData(inspector, view, data);
+ }
+});
+
+function* runTestData(inspector, view, pseudoClasses) {
+ yield setPseudoLocks(inspector, view, pseudoClasses);
+
+ let expected = EXPECTED_SELECTOR + pseudoClasses.join("");
+ yield addNewRuleAndDismissEditor(inspector, view, expected, 1);
+
+ yield resetPseudoLocks(inspector, view);
+}
+
+function* setPseudoLocks(inspector, view, pseudoClasses) {
+ if (pseudoClasses.length == 0) {
+ return;
+ }
+
+ for (let pseudoClass of pseudoClasses) {
+ switch (pseudoClass) {
+ case ":hover":
+ view.hoverCheckbox.click();
+ yield inspector.once("rule-view-refreshed");
+ break;
+ case ":active":
+ view.activeCheckbox.click();
+ yield inspector.once("rule-view-refreshed");
+ break;
+ case ":focus":
+ view.focusCheckbox.click();
+ yield inspector.once("rule-view-refreshed");
+ break;
+ }
+ }
+}
+
+function* resetPseudoLocks(inspector, view) {
+ if (!view.hoverCheckbox.checked &&
+ !view.activeCheckbox.checked &&
+ !view.focusCheckbox.checked) {
+ return;
+ }
+ if (view.hoverCheckbox.checked) {
+ view.hoverCheckbox.click();
+ yield inspector.once("rule-view-refreshed");
+ }
+ if (view.activeCheckbox.checked) {
+ view.activeCheckbox.click();
+ yield inspector.once("rule-view-refreshed");
+ }
+ if (view.focusCheckbox.checked) {
+ view.focusCheckbox.click();
+ yield inspector.once("rule-view-refreshed");
+ }
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_add-rule-then-property-edit-selector.js b/devtools/client/inspector/rules/test/browser_rules_add-rule-then-property-edit-selector.js
new file mode 100644
index 000000000..294eb67e4
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_add-rule-then-property-edit-selector.js
@@ -0,0 +1,80 @@
+/* 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";
+
+// Tests the behaviour of adding a new rule to the rule view, adding a new
+// property and editing the selector.
+
+const TEST_URI = `
+ <style type="text/css">
+ #testid {
+ text-align: center;
+ }
+ </style>
+ <div id="testid">Styled Node</div>
+ <span>This is a span</span>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ yield addNewRuleAndDismissEditor(inspector, view, "#testid", 1);
+
+ info("Adding a new property to the new rule");
+ yield testAddingProperty(view, 1);
+
+ info("Editing existing selector field");
+ yield testEditSelector(view, "span");
+
+ info("Selecting the modified element");
+ yield selectNode("span", inspector);
+
+ info("Check new rule and property exist in the modified element");
+ yield checkModifiedElement(view, "span", 1);
+});
+
+function* testAddingProperty(view, index) {
+ let ruleEditor = getRuleViewRuleEditor(view, index);
+ ruleEditor.addProperty("font-weight", "bold", "", true);
+ let textProps = ruleEditor.rule.textProps;
+ let lastRule = textProps[textProps.length - 1];
+ is(lastRule.name, "font-weight", "Last rule name is font-weight");
+ is(lastRule.value, "bold", "Last rule value is bold");
+}
+
+function* testEditSelector(view, name) {
+ let idRuleEditor = getRuleViewRuleEditor(view, 1);
+
+ info("Focusing an existing selector name in the rule-view");
+ let editor = yield focusEditableField(view, idRuleEditor.selectorText);
+
+ is(inplaceEditor(idRuleEditor.selectorText), editor,
+ "The selector editor got focused");
+
+ info("Entering a new selector name: " + name);
+ editor.input.value = name;
+
+ info("Waiting for rule view to update");
+ let onRuleViewChanged = once(view, "ruleview-changed");
+
+ info("Entering the commit key");
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ yield onRuleViewChanged;
+
+ is(view._elementStyle.rules.length, 3, "Should have 3 rules.");
+}
+
+function* checkModifiedElement(view, name, index) {
+ is(view._elementStyle.rules.length, 2, "Should have 2 rules.");
+ ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists.");
+
+ let idRuleEditor = getRuleViewRuleEditor(view, index);
+ let textProps = idRuleEditor.rule.textProps;
+ let lastRule = textProps[textProps.length - 1];
+ is(lastRule.name, "font-weight", "Last rule name is font-weight");
+ is(lastRule.value, "bold", "Last rule value is bold");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_add-rule-with-menu.js b/devtools/client/inspector/rules/test/browser_rules_add-rule-with-menu.js
new file mode 100644
index 000000000..976fc9643
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_add-rule-with-menu.js
@@ -0,0 +1,42 @@
+/* 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";
+
+// Tests the a new CSS rule can be added using the context menu.
+
+const TEST_URI = '<div id="testid">Test Node</div>';
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+
+ yield selectNode("#testid", inspector);
+ yield addNewRuleFromContextMenu(inspector, view);
+ yield testNewRule(view);
+});
+
+function* addNewRuleFromContextMenu(inspector, view) {
+ info("Waiting for context menu to be shown");
+
+ let allMenuItems = openStyleContextMenuAndGetAllItems(view, view.element);
+ let menuitemAddRule = allMenuItems.find(item => item.label ===
+ STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.addNewRule"));
+
+ ok(menuitemAddRule.visible, "Add rule is visible");
+
+ info("Adding the new rule and expecting a ruleview-changed event");
+ let onRuleViewChanged = view.once("ruleview-changed");
+ menuitemAddRule.click();
+ yield onRuleViewChanged;
+}
+
+function* testNewRule(view) {
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+ let editor = ruleEditor.selectorText.ownerDocument.activeElement;
+ is(editor.value, "#testid", "Selector editor value is as expected");
+
+ info("Escaping from the selector field the change");
+ EventUtils.synthesizeKey("VK_ESCAPE", {});
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_add-rule.js b/devtools/client/inspector/rules/test/browser_rules_add-rule.js
new file mode 100644
index 000000000..296105c85
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_add-rule.js
@@ -0,0 +1,47 @@
+/* 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";
+
+// Tests adding a new rule using the add rule button.
+
+const TEST_URI = `
+ <style type="text/css">
+ .testclass {
+ text-align: center;
+ }
+ </style>
+ <div id="testid" class="testclass">Styled Node</div>
+ <span class="testclass2">This is a span</span>
+ <span class="class1 class2">Multiple classes</span>
+ <span class="class3 class4">Multiple classes</span>
+ <p>Empty<p>
+ <h1 class="asd@@@@a!!!!:::@asd">Invalid characters in class</h1>
+ <h2 id="asd@@@a!!2a">Invalid characters in id</h2>
+ <svg viewBox="0 0 10 10">
+ <circle cx="5" cy="5" r="5" fill="blue"></circle>
+ </svg>
+`;
+
+const TEST_DATA = [
+ { node: "#testid", expected: "#testid" },
+ { node: ".testclass2", expected: ".testclass2" },
+ { node: ".class1.class2", expected: ".class1.class2" },
+ { node: ".class3.class4", expected: ".class3.class4" },
+ { node: "p", expected: "p" },
+ { node: "h1", expected: ".asd\\@\\@\\@\\@a\\!\\!\\!\\!\\:\\:\\:\\@asd" },
+ { node: "h2", expected: "#asd\\@\\@\\@a\\!\\!2a" },
+ { node: "circle", expected: "circle" }
+];
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+
+ for (let data of TEST_DATA) {
+ let {node, expected} = data;
+ yield selectNode(node, inspector);
+ yield addNewRuleAndDismissEditor(inspector, view, expected, 1);
+ }
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_authored.js b/devtools/client/inspector/rules/test/browser_rules_authored.js
new file mode 100644
index 000000000..cb0dd1186
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_authored.js
@@ -0,0 +1,49 @@
+/* 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 for as-authored styles.
+
+function* createTestContent(style) {
+ let html = `<style type="text/css">
+ ${style}
+ </style>
+ <div id="testid" class="testclass">Styled Node</div>`;
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(html));
+
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+ return view;
+}
+
+add_task(function* () {
+ let view = yield createTestContent("#testid {" +
+ // Invalid property.
+ " something: random;" +
+ // Invalid value.
+ " color: orang;" +
+ // Override.
+ " background-color: blue;" +
+ " background-color: #f0c;" +
+ "} ");
+
+ let elementStyle = view._elementStyle;
+
+ let expected = [
+ {name: "something", overridden: true},
+ {name: "color", overridden: true},
+ {name: "background-color", overridden: true},
+ {name: "background-color", overridden: false}
+ ];
+
+ let rule = elementStyle.rules[1];
+
+ for (let i = 0; i < expected.length; ++i) {
+ let prop = rule.textProps[i];
+ is(prop.name, expected[i].name, "test name for prop " + i);
+ is(prop.overridden, expected[i].overridden,
+ "test overridden for prop " + i);
+ }
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_authored_color.js b/devtools/client/inspector/rules/test/browser_rules_authored_color.js
new file mode 100644
index 000000000..4c5cab206
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_authored_color.js
@@ -0,0 +1,67 @@
+/* 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 for as-authored color styles.
+
+/**
+ * Array of test color objects:
+ * {String} name: name of the used & expected color format.
+ * {String} id: id of the element that will be created to test this color.
+ * {String} color: initial value of the color property applied to the test element.
+ * {String} result: expected value of the color property after edition.
+ */
+const colors = [
+ {name: "hex", id: "test1", color: "#f0c", result: "#0f0"},
+ {name: "rgb", id: "test2", color: "rgb(0,128,250)", result: "rgb(0, 255, 0)"},
+ // Test case preservation.
+ {name: "hex", id: "test3", color: "#F0C", result: "#0F0"},
+];
+
+add_task(function* () {
+ Services.prefs.setCharPref("devtools.defaultColorUnit", "authored");
+
+ let html = "";
+ for (let {color, id} of colors) {
+ html += `<div id="${id}" style="color: ${color}">Styled Node</div>`;
+ }
+
+ let tab = yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(html));
+
+ let {inspector, view} = yield openRuleView();
+
+ for (let color of colors) {
+ let cPicker = view.tooltips.colorPicker;
+ let selector = "#" + color.id;
+ yield selectNode(selector, inspector);
+
+ let swatch = getRuleViewProperty(view, "element", "color").valueSpan
+ .querySelector(".ruleview-colorswatch");
+ let onColorPickerReady = cPicker.once("ready");
+ swatch.click();
+ yield onColorPickerReady;
+
+ yield simulateColorPickerChange(view, cPicker, [0, 255, 0, 1], {
+ selector,
+ name: "color",
+ value: "rgb(0, 255, 0)"
+ });
+
+ let spectrum = cPicker.spectrum;
+ let onHidden = cPicker.tooltip.once("hidden");
+ // Validating the color change ends up updating the rule view twice
+ let onRuleViewChanged = waitForNEvents(view, "ruleview-changed", 2);
+ focusAndSendKey(spectrum.element.ownerDocument.defaultView, "RETURN");
+ yield onHidden;
+ yield onRuleViewChanged;
+
+ is(getRuleViewPropertyValue(view, "element", "color"), color.result,
+ "changing the color preserved the unit for " + color.name);
+ }
+
+ let target = TargetFactory.forTab(tab);
+ yield gDevTools.closeToolbox(target);
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_authored_override.js b/devtools/client/inspector/rules/test/browser_rules_authored_override.js
new file mode 100644
index 000000000..7305e5712
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_authored_override.js
@@ -0,0 +1,53 @@
+/* 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 for as-authored styles.
+
+function* createTestContent(style) {
+ let html = `<style type="text/css">
+ ${style}
+ </style>
+ <div id="testid" class="testclass">Styled Node</div>`;
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(html));
+
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+ return view;
+}
+
+add_task(function* () {
+ let gradientText1 = "(orange, blue);";
+ let gradientText2 = "(pink, teal);";
+
+ let view =
+ yield createTestContent("#testid {" +
+ " background-image: linear-gradient" +
+ gradientText1 +
+ " background-image: -ms-linear-gradient" +
+ gradientText2 +
+ " background-image: linear-gradient" +
+ gradientText2 +
+ "} ");
+
+ let elementStyle = view._elementStyle;
+ let rule = elementStyle.rules[1];
+
+ // Initially the last property should be active.
+ for (let i = 0; i < 3; ++i) {
+ let prop = rule.textProps[i];
+ is(prop.name, "background-image", "check the property name");
+ is(prop.overridden, i !== 2, "check overridden for " + i);
+ }
+
+ yield togglePropStatus(view, rule.textProps[2]);
+
+ // Now the first property should be active.
+ for (let i = 0; i < 3; ++i) {
+ let prop = rule.textProps[i];
+ is(prop.overridden || !prop.enabled, i !== 0,
+ "post-change check overridden for " + i);
+ }
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_blob_stylesheet.js b/devtools/client/inspector/rules/test/browser_rules_blob_stylesheet.js
new file mode 100644
index 000000000..adc8eb2ee
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_blob_stylesheet.js
@@ -0,0 +1,20 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the rule-view content is correct for stylesheet generated
+// with createObjectURL(cssBlob)
+const TEST_URL = URL_ROOT + "doc_blob_stylesheet.html";
+
+add_task(function* () {
+ yield addTab(TEST_URL);
+ let {inspector, view} = yield openRuleView();
+
+ yield selectNode("h1", inspector);
+ is(view.element.querySelectorAll("#noResults").length, 0,
+ "The no-results element is not displayed");
+
+ is(view.element.querySelectorAll(".ruleview-rule").length, 2,
+ "There are 2 displayed rules");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_colorUnit.js b/devtools/client/inspector/rules/test/browser_rules_colorUnit.js
new file mode 100644
index 000000000..138f68365
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_colorUnit.js
@@ -0,0 +1,65 @@
+/* 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 color selection respects the user pref.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #testid {
+ color: blue;
+ }
+ </style>
+ <div id='testid' class='testclass'>Styled Node</div>
+`;
+
+add_task(function* () {
+ let TESTS = [
+ {name: "hex", result: "#0f0"},
+ {name: "rgb", result: "rgb(0, 255, 0)"}
+ ];
+
+ for (let {name, result} of TESTS) {
+ info("starting test for " + name);
+ Services.prefs.setCharPref("devtools.defaultColorUnit", name);
+
+ let tab = yield addTab("data:text/html;charset=utf-8," +
+ encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+
+ yield selectNode("#testid", inspector);
+ yield basicTest(view, name, result);
+
+ let target = TargetFactory.forTab(tab);
+ yield gDevTools.closeToolbox(target);
+ gBrowser.removeCurrentTab();
+ }
+});
+
+function* basicTest(view, name, result) {
+ let cPicker = view.tooltips.colorPicker;
+ let swatch = getRuleViewProperty(view, "#testid", "color").valueSpan
+ .querySelector(".ruleview-colorswatch");
+ let onColorPickerReady = cPicker.once("ready");
+ swatch.click();
+ yield onColorPickerReady;
+
+ yield simulateColorPickerChange(view, cPicker, [0, 255, 0, 1], {
+ selector: "#testid",
+ name: "color",
+ value: "rgb(0, 255, 0)"
+ });
+
+ let spectrum = cPicker.spectrum;
+ let onHidden = cPicker.tooltip.once("hidden");
+ // Validating the color change ends up updating the rule view twice
+ let onRuleViewChanged = waitForNEvents(view, "ruleview-changed", 2);
+ focusAndSendKey(spectrum.element.ownerDocument.defaultView, "RETURN");
+ yield onHidden;
+ yield onRuleViewChanged;
+
+ is(getRuleViewPropertyValue(view, "#testid", "color"), result,
+ "changing the color used the " + name + " unit");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_colorpicker-and-image-tooltip_01.js b/devtools/client/inspector/rules/test/browser_rules_colorpicker-and-image-tooltip_01.js
new file mode 100644
index 000000000..a8d2fd5f1
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_colorpicker-and-image-tooltip_01.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";
+
+// Tests that after a color change, the image preview tooltip in the same
+// property is displayed and positioned correctly.
+// See bug 979292
+
+const TEST_URI = `
+ <style type="text/css">
+ body {
+ background: url("chrome://global/skin/icons/warning-64.png"), linear-gradient(white, #F06 400px);
+ }
+ </style>
+ Testing the color picker tooltip!
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {view} = yield openRuleView();
+ let value = getRuleViewProperty(view, "body", "background").valueSpan;
+ let swatch = value.querySelectorAll(".ruleview-colorswatch")[0];
+ let url = value.querySelector(".theme-link");
+ yield testImageTooltipAfterColorChange(swatch, url, view);
+});
+
+function* testImageTooltipAfterColorChange(swatch, url, ruleView) {
+ info("First, verify that the image preview tooltip works");
+ let anchor = yield isHoverTooltipTarget(ruleView.tooltips.previewTooltip,
+ url);
+ ok(anchor, "The image preview tooltip is shown on the url span");
+ is(anchor, url, "The anchor returned by the showOnHover callback is correct");
+
+ info("Open the color picker tooltip and change the color");
+ let picker = ruleView.tooltips.colorPicker;
+ let onColorPickerReady = picker.once("ready");
+ swatch.click();
+ yield onColorPickerReady;
+
+ yield simulateColorPickerChange(ruleView, picker, [0, 0, 0, 1], {
+ selector: "body",
+ name: "background-image",
+ value: 'url("chrome://global/skin/icons/warning-64.png"), linear-gradient(rgb(0, 0, 0), rgb(255, 0, 102) 400px)'
+ });
+
+ let spectrum = picker.spectrum;
+ let onHidden = picker.tooltip.once("hidden");
+ let onModifications = ruleView.once("ruleview-changed");
+ focusAndSendKey(spectrum.element.ownerDocument.defaultView, "RETURN");
+ yield onHidden;
+ yield onModifications;
+
+ info("Verify again that the image preview tooltip works");
+ // After a color change, the property is re-populated, we need to get the new
+ // dom node
+ url = getRuleViewProperty(ruleView, "body", "background").valueSpan
+ .querySelector(".theme-link");
+ anchor = yield isHoverTooltipTarget(ruleView.tooltips.previewTooltip, url);
+ ok(anchor, "The image preview tooltip is shown on the url span");
+ is(anchor, url, "The anchor returned by the showOnHover callback is correct");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_colorpicker-and-image-tooltip_02.js b/devtools/client/inspector/rules/test/browser_rules_colorpicker-and-image-tooltip_02.js
new file mode 100644
index 000000000..743ad5180
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_colorpicker-and-image-tooltip_02.js
@@ -0,0 +1,66 @@
+/* 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";
+
+// Tests that after a color change, opening another tooltip, like the image
+// preview doesn't revert the color change in the rule view.
+// This used to happen when the activeSwatch wasn't reset when the colorpicker
+// would hide.
+// See bug 979292
+
+const TEST_URI = `
+ <style type="text/css">
+ body {
+ background: red url("chrome://global/skin/icons/warning-64.png")
+ no-repeat center center;
+ }
+ </style>
+ Testing the color picker tooltip!
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {view} = yield openRuleView();
+ yield testColorChangeIsntRevertedWhenOtherTooltipIsShown(view);
+});
+
+function* testColorChangeIsntRevertedWhenOtherTooltipIsShown(ruleView) {
+ let swatch = getRuleViewProperty(ruleView, "body", "background").valueSpan
+ .querySelector(".ruleview-colorswatch");
+
+ info("Open the color picker tooltip and change the color");
+ let picker = ruleView.tooltips.colorPicker;
+ let onColorPickerReady = picker.once("ready");
+ swatch.click();
+ yield onColorPickerReady;
+
+ yield simulateColorPickerChange(ruleView, picker, [0, 0, 0, 1], {
+ selector: "body",
+ name: "background-color",
+ value: "rgb(0, 0, 0)"
+ });
+
+ let spectrum = picker.spectrum;
+
+ let onModifications = waitForNEvents(ruleView, "ruleview-changed", 2);
+ let onHidden = picker.tooltip.once("hidden");
+ focusAndSendKey(spectrum.element.ownerDocument.defaultView, "RETURN");
+ yield onHidden;
+ yield onModifications;
+
+ info("Open the image preview tooltip");
+ let value = getRuleViewProperty(ruleView, "body", "background").valueSpan;
+ let url = value.querySelector(".theme-link");
+ let onShown = ruleView.tooltips.previewTooltip.once("shown");
+ let anchor = yield isHoverTooltipTarget(ruleView.tooltips.previewTooltip, url);
+ ruleView.tooltips.previewTooltip.show(anchor);
+ yield onShown;
+
+ info("Image tooltip is shown, verify that the swatch is still correct");
+ swatch = value.querySelector(".ruleview-colorswatch");
+ is(swatch.style.backgroundColor, "black",
+ "The swatch's color is correct");
+ is(swatch.nextSibling.textContent, "black", "The color name is correct");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_colorpicker-appears-on-swatch-click.js b/devtools/client/inspector/rules/test/browser_rules_colorpicker-appears-on-swatch-click.js
new file mode 100644
index 000000000..383ffed6c
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_colorpicker-appears-on-swatch-click.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";
+
+// Tests that color pickers appear when clicking on color swatches.
+
+const TEST_URI = `
+ <style type="text/css">
+ body {
+ color: red;
+ background-color: #ededed;
+ background-image: url(chrome://global/skin/icons/warning-64.png);
+ border: 2em solid rgba(120, 120, 120, .5);
+ }
+ </style>
+ Testing the color picker tooltip!
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {view} = yield openRuleView();
+
+ let propertiesToTest = ["color", "background-color", "border"];
+
+ for (let property of propertiesToTest) {
+ info("Testing that the colorpicker appears on swatch click");
+ let value = getRuleViewProperty(view, "body", property).valueSpan;
+ let swatch = value.querySelector(".ruleview-colorswatch");
+ yield testColorPickerAppearsOnColorSwatchClick(view, swatch);
+ }
+});
+
+function* testColorPickerAppearsOnColorSwatchClick(view, swatch) {
+ let cPicker = view.tooltips.colorPicker;
+ ok(cPicker, "The rule-view has the expected colorPicker property");
+
+ let cPickerPanel = cPicker.tooltip.panel;
+ ok(cPickerPanel, "The XUL panel for the color picker exists");
+
+ let onColorPickerReady = cPicker.once("ready");
+ swatch.click();
+ yield onColorPickerReady;
+
+ ok(true, "The color picker was shown on click of the color swatch");
+ ok(!inplaceEditor(swatch.parentNode),
+ "The inplace editor wasn't shown as a result of the color swatch click");
+
+ yield hideTooltipAndWaitForRuleViewChanged(cPicker, view);
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_colorpicker-commit-on-ENTER.js b/devtools/client/inspector/rules/test/browser_rules_colorpicker-commit-on-ENTER.js
new file mode 100644
index 000000000..129e8f245
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_colorpicker-commit-on-ENTER.js
@@ -0,0 +1,61 @@
+/* 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";
+
+// Tests that a color change in the color picker is committed when ENTER is
+// pressed.
+
+const TEST_URI = `
+ <style type="text/css">
+ body {
+ border: 2em solid rgba(120, 120, 120, .5);
+ }
+ </style>
+ Testing the color picker tooltip!
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {view} = yield openRuleView();
+
+ let swatch = getRuleViewProperty(view, "body", "border").valueSpan
+ .querySelector(".ruleview-colorswatch");
+ yield testPressingEnterCommitsChanges(swatch, view);
+});
+
+function* testPressingEnterCommitsChanges(swatch, ruleView) {
+ let cPicker = ruleView.tooltips.colorPicker;
+
+ let onColorPickerReady = cPicker.once("ready");
+ swatch.click();
+ yield onColorPickerReady;
+
+ yield simulateColorPickerChange(ruleView, cPicker, [0, 255, 0, .5], {
+ selector: "body",
+ name: "border-left-color",
+ value: "rgba(0, 255, 0, 0.5)"
+ });
+
+ is(swatch.style.backgroundColor, "rgba(0, 255, 0, 0.5)",
+ "The color swatch's background was updated");
+ is(getRuleViewProperty(ruleView, "body", "border").valueSpan.textContent,
+ "2em solid rgba(0, 255, 0, 0.5)",
+ "The text of the border css property was updated");
+
+ let onModified = ruleView.once("ruleview-changed");
+ let spectrum = cPicker.spectrum;
+ let onHidden = cPicker.tooltip.once("hidden");
+ focusAndSendKey(spectrum.element.ownerDocument.defaultView, "RETURN");
+ yield onHidden;
+ yield onModified;
+
+ is((yield getComputedStyleProperty("body", null, "border-left-color")),
+ "rgba(0, 255, 0, 0.5)", "The element's border was kept after RETURN");
+ is(swatch.style.backgroundColor, "rgba(0, 255, 0, 0.5)",
+ "The color swatch's background was kept after RETURN");
+ is(getRuleViewProperty(ruleView, "body", "border").valueSpan.textContent,
+ "2em solid rgba(0, 255, 0, 0.5)",
+ "The text of the border css property was kept after RETURN");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_colorpicker-edit-gradient.js b/devtools/client/inspector/rules/test/browser_rules_colorpicker-edit-gradient.js
new file mode 100644
index 000000000..71ceb14c3
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_colorpicker-edit-gradient.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";
+
+// Tests that changing a color in a gradient css declaration using the tooltip
+// color picker works.
+
+const TEST_URI = `
+ <style type="text/css">
+ body {
+ background-image: linear-gradient(to left, #f06 25%, #333 95%, #000 100%);
+ }
+ </style>
+ Updating a gradient declaration with the color picker tooltip
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {view} = yield openRuleView();
+
+ info("Testing that the colors in gradient properties are parsed correctly");
+ testColorParsing(view);
+
+ info("Testing that changing one of the colors of a gradient property works");
+ yield testPickingNewColor(view);
+});
+
+function testColorParsing(view) {
+ let ruleEl = getRuleViewProperty(view, "body", "background-image");
+ ok(ruleEl, "The background-image gradient declaration was found");
+
+ let swatchEls = ruleEl.valueSpan.querySelectorAll(".ruleview-colorswatch");
+ ok(swatchEls, "The color swatch elements were found");
+ is(swatchEls.length, 3, "There are 3 color swatches");
+
+ let colorEls = ruleEl.valueSpan.querySelectorAll(".ruleview-color");
+ ok(colorEls, "The color elements were found");
+ is(colorEls.length, 3, "There are 3 color values");
+
+ let colors = ["#f06", "#333", "#000"];
+ for (let i = 0; i < colors.length; i++) {
+ is(colorEls[i].textContent, colors[i], "The right color value was found");
+ }
+}
+
+function* testPickingNewColor(view) {
+ // Grab the first color swatch and color in the gradient
+ let ruleEl = getRuleViewProperty(view, "body", "background-image");
+ let swatchEl = ruleEl.valueSpan.querySelector(".ruleview-colorswatch");
+ let colorEl = ruleEl.valueSpan.querySelector(".ruleview-color");
+
+ info("Get the color picker tooltip and clicking on the swatch to show it");
+ let cPicker = view.tooltips.colorPicker;
+ let onColorPickerReady = cPicker.once("ready");
+ swatchEl.click();
+ yield onColorPickerReady;
+
+ let change = {
+ selector: "body",
+ name: "background-image",
+ value: "linear-gradient(to left, rgb(1, 1, 1) 25%, " +
+ "rgb(51, 51, 51) 95%, rgb(0, 0, 0) 100%)"
+ };
+ yield simulateColorPickerChange(view, cPicker, [1, 1, 1, 1], change);
+
+ is(swatchEl.style.backgroundColor, "rgb(1, 1, 1)",
+ "The color swatch's background was updated");
+ is(colorEl.textContent, "#010101", "The color text was updated");
+ is((yield getComputedStyleProperty("body", null, "background-image")),
+ "linear-gradient(to left, rgb(1, 1, 1) 25%, rgb(51, 51, 51) 95%, " +
+ "rgb(0, 0, 0) 100%)",
+ "The gradient has been updated correctly");
+
+ yield hideTooltipAndWaitForRuleViewChanged(cPicker, view);
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_colorpicker-hides-on-tooltip.js b/devtools/client/inspector/rules/test/browser_rules_colorpicker-hides-on-tooltip.js
new file mode 100644
index 000000000..b50c63605
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_colorpicker-hides-on-tooltip.js
@@ -0,0 +1,46 @@
+/* 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";
+
+// Tests that the color picker tooltip hides when an image tooltip appears.
+
+const TEST_URI = `
+ <style type="text/css">
+ body {
+ color: red;
+ background-color: #ededed;
+ background-image: url(chrome://global/skin/icons/warning-64.png);
+ border: 2em solid rgba(120, 120, 120, .5);
+ }
+ </style>
+ Testing the color picker tooltip!
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {view} = yield openRuleView();
+
+ let swatch = getRuleViewProperty(view, "body", "color").valueSpan
+ .querySelector(".ruleview-colorswatch");
+
+ let bgImageSpan = getRuleViewProperty(view, "body", "background-image").valueSpan;
+ let uriSpan = bgImageSpan.querySelector(".theme-link");
+
+ let colorPicker = view.tooltips.colorPicker;
+ info("Showing the color picker tooltip by clicking on the color swatch");
+ let onColorPickerReady = colorPicker.once("ready");
+ swatch.click();
+ yield onColorPickerReady;
+
+ info("Now showing the image preview tooltip to hide the color picker");
+ let onHidden = colorPicker.tooltip.once("hidden");
+ // Hiding the color picker refreshes the value.
+ let onRuleViewChanged = view.once("ruleview-changed");
+ yield assertHoverTooltipOn(view.tooltips.previewTooltip, uriSpan);
+ yield onHidden;
+ yield onRuleViewChanged;
+
+ ok(true, "The color picker closed when the image preview tooltip appeared");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_colorpicker-multiple-changes.js b/devtools/client/inspector/rules/test/browser_rules_colorpicker-multiple-changes.js
new file mode 100644
index 000000000..06fab72d6
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_colorpicker-multiple-changes.js
@@ -0,0 +1,124 @@
+/* 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";
+
+// Tests that the color in the colorpicker tooltip can be changed several times.
+// without causing error in various cases:
+// - simple single-color property (color)
+// - color and image property (background-image)
+// - overridden property
+// See bug 979292 and bug 980225
+
+const TEST_URI = `
+ <style type="text/css">
+ body {
+ color: green;
+ background: red url("chrome://global/skin/icons/warning-64.png")
+ no-repeat center center;
+ }
+ p {
+ color: blue;
+ }
+ </style>
+ <p>Testing the color picker tooltip!</p>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+
+ yield testSimpleMultipleColorChanges(inspector, view);
+ yield testComplexMultipleColorChanges(inspector, view);
+ yield testOverriddenMultipleColorChanges(inspector, view);
+});
+
+function* testSimpleMultipleColorChanges(inspector, ruleView) {
+ yield selectNode("p", inspector);
+
+ info("Getting the <p> tag's color property");
+ let swatch = getRuleViewProperty(ruleView, "p", "color").valueSpan
+ .querySelector(".ruleview-colorswatch");
+
+ info("Opening the color picker");
+ let picker = ruleView.tooltips.colorPicker;
+ let onColorPickerReady = picker.once("ready");
+ swatch.click();
+ yield onColorPickerReady;
+
+ info("Changing the color several times");
+ let colors = [
+ {rgba: [0, 0, 0, 1], computed: "rgb(0, 0, 0)"},
+ {rgba: [100, 100, 100, 1], computed: "rgb(100, 100, 100)"},
+ {rgba: [200, 200, 200, 1], computed: "rgb(200, 200, 200)"}
+ ];
+ for (let {rgba, computed} of colors) {
+ yield simulateColorPickerChange(ruleView, picker, rgba, {
+ selector: "p",
+ name: "color",
+ value: computed
+ });
+ }
+}
+
+function* testComplexMultipleColorChanges(inspector, ruleView) {
+ yield selectNode("body", inspector);
+
+ info("Getting the <body> tag's color property");
+ let swatch = getRuleViewProperty(ruleView, "body", "background").valueSpan
+ .querySelector(".ruleview-colorswatch");
+
+ info("Opening the color picker");
+ let picker = ruleView.tooltips.colorPicker;
+ let onColorPickerReady = picker.once("ready");
+ swatch.click();
+ yield onColorPickerReady;
+
+ info("Changing the color several times");
+ let colors = [
+ {rgba: [0, 0, 0, 1], computed: "rgb(0, 0, 0)"},
+ {rgba: [100, 100, 100, 1], computed: "rgb(100, 100, 100)"},
+ {rgba: [200, 200, 200, 1], computed: "rgb(200, 200, 200)"}
+ ];
+ for (let {rgba, computed} of colors) {
+ yield simulateColorPickerChange(ruleView, picker, rgba, {
+ selector: "body",
+ name: "background-color",
+ value: computed
+ });
+ }
+
+ info("Closing the color picker");
+ yield hideTooltipAndWaitForRuleViewChanged(picker, ruleView);
+}
+
+function* testOverriddenMultipleColorChanges(inspector, ruleView) {
+ yield selectNode("p", inspector);
+
+ info("Getting the <body> tag's color property");
+ let swatch = getRuleViewProperty(ruleView, "body", "color").valueSpan
+ .querySelector(".ruleview-colorswatch");
+
+ info("Opening the color picker");
+ let picker = ruleView.tooltips.colorPicker;
+ let onColorPickerReady = picker.once("ready");
+ swatch.click();
+ yield onColorPickerReady;
+
+ info("Changing the color several times");
+ let colors = [
+ {rgba: [0, 0, 0, 1], computed: "rgb(0, 0, 0)"},
+ {rgba: [100, 100, 100, 1], computed: "rgb(100, 100, 100)"},
+ {rgba: [200, 200, 200, 1], computed: "rgb(200, 200, 200)"}
+ ];
+ for (let {rgba, computed} of colors) {
+ yield simulateColorPickerChange(ruleView, picker, rgba, {
+ selector: "body",
+ name: "color",
+ value: computed
+ });
+ is((yield getComputedStyleProperty("p", null, "color")),
+ "rgb(200, 200, 200)", "The color of the P tag is still correct");
+ }
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_colorpicker-release-outside-frame.js b/devtools/client/inspector/rules/test/browser_rules_colorpicker-release-outside-frame.js
new file mode 100644
index 000000000..ef6ca02b1
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_colorpicker-release-outside-frame.js
@@ -0,0 +1,67 @@
+/* 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";
+
+// Tests that color pickers stops following the pointer if the pointer is
+// released outside the tooltip frame (bug 1160720).
+
+const TEST_URI = "<body style='color: red'>Test page for bug 1160720";
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {view} = yield openRuleView();
+
+ let cSwatch = getRuleViewProperty(view, "element", "color").valueSpan
+ .querySelector(".ruleview-colorswatch");
+
+ let picker = yield openColorPickerForSwatch(cSwatch, view);
+ let spectrum = picker.spectrum;
+ let change = spectrum.once("changed");
+
+ info("Pressing mouse down over color picker.");
+ let onRuleViewChanged = view.once("ruleview-changed");
+ EventUtils.synthesizeMouseAtCenter(spectrum.dragger, {
+ type: "mousedown",
+ }, spectrum.dragger.ownerDocument.defaultView);
+ yield onRuleViewChanged;
+
+ let value = yield change;
+ info(`Color changed to ${value} on mousedown.`);
+
+ // If the mousemove below fails to detect that the button is no longer pressed
+ // the spectrum will update and emit changed event synchronously after calling
+ // synthesizeMouse so this handler is executed before the test ends.
+ spectrum.once("changed", (event, newValue) => {
+ is(newValue, value, "Value changed on mousemove without a button pressed.");
+ });
+
+ // Releasing the button pressed by mousedown above on top of a different frame
+ // does not make sense in this test as EventUtils doesn't preserve the context
+ // i.e. the buttons that were pressed down between events.
+
+ info("Moving mouse over color picker without any buttons pressed.");
+
+ EventUtils.synthesizeMouse(spectrum.dragger, 10, 10, {
+ // -1 = no buttons are pressed down
+ button: -1,
+ type: "mousemove",
+ }, spectrum.dragger.ownerDocument.defaultView);
+});
+
+function* openColorPickerForSwatch(swatch, view) {
+ let cPicker = view.tooltips.colorPicker;
+ ok(cPicker, "The rule-view has the expected colorPicker property");
+
+ let cPickerPanel = cPicker.tooltip.panel;
+ ok(cPickerPanel, "The XUL panel for the color picker exists");
+
+ let onColorPickerReady = cPicker.once("ready");
+ swatch.click();
+ yield onColorPickerReady;
+
+ ok(true, "The color picker was shown on click of the color swatch");
+
+ return cPicker;
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_colorpicker-revert-on-ESC.js b/devtools/client/inspector/rules/test/browser_rules_colorpicker-revert-on-ESC.js
new file mode 100644
index 000000000..e244d429c
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_colorpicker-revert-on-ESC.js
@@ -0,0 +1,109 @@
+/* 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";
+
+// Tests that a color change in the color picker is reverted when ESC is
+// pressed.
+
+const TEST_URI = `
+ <style type="text/css">
+ body {
+ background-color: #EDEDED;
+ }
+ </style>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {view} = yield openRuleView();
+ yield testPressingEscapeRevertsChanges(view);
+ yield testPressingEscapeRevertsChangesAndDisables(view);
+});
+
+function* testPressingEscapeRevertsChanges(view) {
+ let {swatch, propEditor, cPicker} = yield openColorPickerAndSelectColor(view,
+ 1, 0, [0, 0, 0, 1], {
+ selector: "body",
+ name: "background-color",
+ value: "rgb(0, 0, 0)"
+ });
+
+ is(swatch.style.backgroundColor, "rgb(0, 0, 0)",
+ "The color swatch's background was updated");
+ is(propEditor.valueSpan.textContent, "#000",
+ "The text of the background-color css property was updated");
+
+ let spectrum = cPicker.spectrum;
+
+ info("Pressing ESCAPE to close the tooltip");
+ let onHidden = cPicker.tooltip.once("hidden");
+ let onModifications = view.once("ruleview-changed");
+ EventUtils.sendKey("ESCAPE", spectrum.element.ownerDocument.defaultView);
+ yield onHidden;
+ yield onModifications;
+
+ yield waitForComputedStyleProperty("body", null, "background-color",
+ "rgb(237, 237, 237)");
+ is(propEditor.valueSpan.textContent, "#EDEDED",
+ "Got expected property value.");
+}
+
+function* testPressingEscapeRevertsChangesAndDisables(view) {
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+
+ info("Disabling background-color property");
+ let textProp = ruleEditor.rule.textProps[0];
+ yield togglePropStatus(view, textProp);
+
+ ok(textProp.editor.element.classList.contains("ruleview-overridden"),
+ "property is overridden.");
+ is(textProp.editor.enable.style.visibility, "visible",
+ "property enable checkbox is visible.");
+ ok(!textProp.editor.enable.getAttribute("checked"),
+ "property enable checkbox is not checked.");
+ ok(!textProp.editor.prop.enabled,
+ "background-color property is disabled.");
+ let newValue = yield getRulePropertyValue("background-color");
+ is(newValue, "", "background-color should have been unset.");
+
+ let {cPicker} = yield openColorPickerAndSelectColor(view,
+ 1, 0, [0, 0, 0, 1]);
+
+ ok(!textProp.editor.element.classList.contains("ruleview-overridden"),
+ "property overridden is not displayed.");
+ is(textProp.editor.enable.style.visibility, "hidden",
+ "property enable checkbox is hidden.");
+
+ let spectrum = cPicker.spectrum;
+
+ info("Pressing ESCAPE to close the tooltip");
+ let onHidden = cPicker.tooltip.once("hidden");
+ let onModifications = view.once("ruleview-changed");
+ EventUtils.sendKey("ESCAPE", spectrum.element.ownerDocument.defaultView);
+ yield onHidden;
+ yield onModifications;
+
+ ok(textProp.editor.element.classList.contains("ruleview-overridden"),
+ "property is overridden.");
+ is(textProp.editor.enable.style.visibility, "visible",
+ "property enable checkbox is visible.");
+ ok(!textProp.editor.enable.getAttribute("checked"),
+ "property enable checkbox is not checked.");
+ ok(!textProp.editor.prop.enabled,
+ "background-color property is disabled.");
+ newValue = yield getRulePropertyValue("background-color");
+ is(newValue, "", "background-color should have been unset.");
+ is(textProp.editor.valueSpan.textContent, "#EDEDED",
+ "Got expected property value.");
+}
+
+function* getRulePropertyValue(name) {
+ let propValue = yield executeInContent("Test:GetRulePropertyValue", {
+ styleSheetIndex: 0,
+ ruleIndex: 0,
+ name: name
+ });
+ return propValue;
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_colorpicker-swatch-displayed.js b/devtools/client/inspector/rules/test/browser_rules_colorpicker-swatch-displayed.js
new file mode 100644
index 000000000..b06ff37df
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_colorpicker-swatch-displayed.js
@@ -0,0 +1,73 @@
+/* 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";
+
+// Tests that color swatches are displayed next to colors in the rule-view.
+
+const TEST_URI = `
+ <style type="text/css">
+ body {
+ color: red;
+ background-color: #ededed;
+ background-image: url(chrome://global/skin/icons/warning-64.png);
+ border: 2em solid rgba(120, 120, 120, .5);
+ }
+ * {
+ color: blue;
+ background: linear-gradient(
+ to right,
+ #f00,
+ #f008,
+ #00ff00,
+ #00ff0080,
+ rgb(31,170,217),
+ rgba(31,170,217,.5),
+ hsl(5, 5%, 5%),
+ hsla(5, 5%, 5%, 0.25),
+ #F00,
+ #F008,
+ #00FF00,
+ #00FF0080,
+ RGB(31,170,217),
+ RGBA(31,170,217,.5),
+ HSL(5, 5%, 5%),
+ HSLA(5, 5%, 5%, 0.25));
+ box-shadow: inset 0 0 2px 20px red, inset 0 0 2px 40px blue;
+ }
+ </style>
+ Testing the color picker tooltip!
+`;
+
+// Tests that properties in the rule-view contain color swatches.
+// Each entry in the test array should contain:
+// {
+// selector: the rule-view selector to look for the property in
+// propertyName: the property to test
+// nb: the number of color swatches this property should have
+// }
+const TESTS = [
+ {selector: "body", propertyName: "color", nb: 1},
+ {selector: "body", propertyName: "background-color", nb: 1},
+ {selector: "body", propertyName: "border", nb: 1},
+ {selector: "*", propertyName: "color", nb: 1},
+ {selector: "*", propertyName: "background", nb: 16},
+ {selector: "*", propertyName: "box-shadow", nb: 2},
+];
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {view} = yield openRuleView();
+
+ for (let {selector, propertyName, nb} of TESTS) {
+ info("Looking for color swatches in property " + propertyName +
+ " in selector " + selector);
+
+ let prop = getRuleViewProperty(view, selector, propertyName).valueSpan;
+ let swatches = prop.querySelectorAll(".ruleview-colorswatch");
+
+ ok(swatches.length, "Swatches found in the property");
+ is(swatches.length, nb, "Correct number of swatches found in the property");
+ }
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_completion-existing-property_01.js b/devtools/client/inspector/rules/test/browser_rules_completion-existing-property_01.js
new file mode 100644
index 000000000..566bae259
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_completion-existing-property_01.js
@@ -0,0 +1,139 @@
+/* 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";
+
+// Tests that CSS property names are autocompleted and cycled correctly when
+// editing an existing property in the rule view.
+
+// format :
+// [
+// what key to press,
+// expected input box value after keypress,
+// is the popup open,
+// is a suggestion selected in the popup,
+// ]
+
+const OPEN = true, SELECTED = true;
+var testData = [
+ ["VK_RIGHT", "font", !OPEN, !SELECTED],
+ ["-", "font-size", OPEN, SELECTED],
+ ["f", "font-family", OPEN, SELECTED],
+ ["VK_BACK_SPACE", "font-f", !OPEN, !SELECTED],
+ ["VK_BACK_SPACE", "font-", !OPEN, !SELECTED],
+ ["VK_BACK_SPACE", "font", !OPEN, !SELECTED],
+ ["VK_BACK_SPACE", "fon", !OPEN, !SELECTED],
+ ["VK_BACK_SPACE", "fo", !OPEN, !SELECTED],
+ ["VK_BACK_SPACE", "f", !OPEN, !SELECTED],
+ ["VK_BACK_SPACE", "", !OPEN, !SELECTED],
+ ["d", "display", OPEN, SELECTED],
+ ["VK_DOWN", "dominant-baseline", OPEN, SELECTED],
+ ["VK_DOWN", "direction", OPEN, SELECTED],
+ ["VK_DOWN", "display", OPEN, SELECTED],
+ ["VK_UP", "direction", OPEN, SELECTED],
+ ["VK_UP", "dominant-baseline", OPEN, SELECTED],
+ ["VK_UP", "display", OPEN, SELECTED],
+ ["VK_BACK_SPACE", "d", !OPEN, !SELECTED],
+ ["i", "display", OPEN, SELECTED],
+ ["s", "display", !OPEN, !SELECTED],
+ ["VK_BACK_SPACE", "dis", !OPEN, !SELECTED],
+ ["VK_BACK_SPACE", "di", !OPEN, !SELECTED],
+ ["VK_BACK_SPACE", "d", !OPEN, !SELECTED],
+ ["VK_BACK_SPACE", "", !OPEN, !SELECTED],
+ ["VK_HOME", "", !OPEN, !SELECTED],
+ ["VK_END", "", !OPEN, !SELECTED],
+ ["VK_PAGE_UP", "", !OPEN, !SELECTED],
+ ["VK_PAGE_DOWN", "", !OPEN, !SELECTED],
+ ["d", "display", OPEN, SELECTED],
+ ["VK_HOME", "display", !OPEN, !SELECTED],
+ ["VK_END", "display", !OPEN, !SELECTED],
+ // Press right key to ensure caret move to end of the input on Mac OS since
+ // Mac OS doesn't move caret after pressing HOME / END.
+ ["VK_RIGHT", "display", !OPEN, !SELECTED],
+ ["VK_BACK_SPACE", "displa", !OPEN, !SELECTED],
+ ["VK_BACK_SPACE", "displ", !OPEN, !SELECTED],
+ ["VK_BACK_SPACE", "disp", !OPEN, !SELECTED],
+ ["VK_BACK_SPACE", "dis", !OPEN, !SELECTED],
+ ["VK_BACK_SPACE", "di", !OPEN, !SELECTED],
+ ["VK_BACK_SPACE", "d", !OPEN, !SELECTED],
+ ["VK_BACK_SPACE", "", !OPEN, !SELECTED],
+ ["f", "font-size", OPEN, SELECTED],
+ ["i", "filter", OPEN, SELECTED],
+ ["VK_LEFT", "filter", !OPEN, !SELECTED],
+ ["VK_LEFT", "filter", !OPEN, !SELECTED],
+ ["i", "fiilter", !OPEN, !SELECTED],
+ ["VK_ESCAPE", null, !OPEN, !SELECTED],
+];
+
+const TEST_URI = "<h1 style='font: 24px serif'>Header</h1>";
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {toolbox, inspector, view, testActor} = yield openRuleView();
+
+ info("Test autocompletion after 1st page load");
+ yield runAutocompletionTest(toolbox, inspector, view);
+
+ info("Test autocompletion after page navigation");
+ yield reloadPage(inspector, testActor);
+ yield runAutocompletionTest(toolbox, inspector, view);
+});
+
+function* runAutocompletionTest(toolbox, inspector, view) {
+ info("Selecting the test node");
+ yield selectNode("h1", inspector);
+
+ info("Focusing the css property editable field");
+ let propertyName = view.styleDocument.querySelectorAll(".ruleview-propertyname")[0];
+ let editor = yield focusEditableField(view, propertyName);
+
+ info("Starting to test for css property completion");
+ for (let i = 0; i < testData.length; i++) {
+ yield testCompletion(testData[i], editor, view);
+ }
+}
+
+function* testCompletion([key, completion, open, selected],
+ editor, view) {
+ info("Pressing key " + key);
+ info("Expecting " + completion);
+ info("Is popup opened: " + open);
+ info("Is item selected: " + selected);
+
+ // Listening for the right event that will tell us when the key has been
+ // entered and processed.
+ let onSuggest;
+ if (/(left|right|back_space|escape|home|end|page_up|page_down)/ig.test(key)) {
+ info("Adding event listener for " +
+ "left|right|back_space|escape|home|end|page_up|page_down keys");
+ onSuggest = once(editor.input, "keypress");
+ } else {
+ info("Waiting for after-suggest event on the editor");
+ onSuggest = editor.once("after-suggest");
+ }
+
+ // Also listening for popup opened/closed events if needed.
+ let popupEvent = open ? "popup-opened" : "popup-closed";
+ let onPopupEvent = editor.popup.isOpen !== open ? once(editor.popup, popupEvent) : null;
+
+ info("Synthesizing key " + key);
+ EventUtils.synthesizeKey(key, {}, view.styleWindow);
+
+ // Flush the throttle for the preview text.
+ view.throttle.flush();
+
+ yield onSuggest;
+ yield onPopupEvent;
+
+ info("Checking the state");
+ if (completion !== null) {
+ is(editor.input.value, completion, "Correct value is autocompleted");
+ }
+ if (!open) {
+ ok(!(editor.popup && editor.popup.isOpen), "Popup is closed");
+ } else {
+ ok(editor.popup.isOpen, "Popup is open");
+ is(editor.popup.selectedIndex !== -1, selected, "An item is selected");
+ }
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_completion-existing-property_02.js b/devtools/client/inspector/rules/test/browser_rules_completion-existing-property_02.js
new file mode 100644
index 000000000..fde8f5d12
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_completion-existing-property_02.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";
+
+// Tests that CSS property names and values are autocompleted and cycled
+// correctly when editing existing properties in the rule view.
+
+// format :
+// [
+// what key to press,
+// modifers,
+// expected input box value after keypress,
+// is the popup open,
+// is a suggestion selected in the popup,
+// expect ruleview-changed,
+// ]
+
+const OPEN = true, SELECTED = true, CHANGE = true;
+var testData = [
+ ["b", {}, "beige", OPEN, SELECTED, CHANGE],
+ ["l", {}, "black", OPEN, SELECTED, CHANGE],
+ ["VK_DOWN", {}, "blanchedalmond", OPEN, SELECTED, CHANGE],
+ ["VK_DOWN", {}, "blue", OPEN, SELECTED, CHANGE],
+ ["VK_RIGHT", {}, "blue", !OPEN, !SELECTED, !CHANGE],
+ [" ", {}, "blue aliceblue", OPEN, SELECTED, CHANGE],
+ ["!", {}, "blue !important", !OPEN, !SELECTED, CHANGE],
+ ["VK_BACK_SPACE", {}, "blue !", !OPEN, !SELECTED, CHANGE],
+ ["VK_BACK_SPACE", {}, "blue ", !OPEN, !SELECTED, CHANGE],
+ ["VK_BACK_SPACE", {}, "blue", !OPEN, !SELECTED, CHANGE],
+ ["VK_TAB", {shiftKey: true}, "color", !OPEN, !SELECTED, CHANGE],
+ ["VK_BACK_SPACE", {}, "", !OPEN, !SELECTED, !CHANGE],
+ ["d", {}, "display", OPEN, SELECTED, !CHANGE],
+ ["VK_TAB", {}, "blue", !OPEN, !SELECTED, CHANGE],
+ ["n", {}, "none", !OPEN, !SELECTED, CHANGE],
+ ["VK_RETURN", {}, null, !OPEN, !SELECTED, CHANGE]
+];
+
+const TEST_URI = "<h1 style='color: red'>Header</h1>";
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {toolbox, inspector, view, testActor} = yield openRuleView();
+
+ info("Test autocompletion after 1st page load");
+ yield runAutocompletionTest(toolbox, inspector, view);
+
+ info("Test autocompletion after page navigation");
+ yield reloadPage(inspector, testActor);
+ yield runAutocompletionTest(toolbox, inspector, view);
+});
+
+function* runAutocompletionTest(toolbox, inspector, view) {
+ info("Selecting the test node");
+ yield selectNode("h1", inspector);
+
+ let rule = getRuleViewRuleEditor(view, 0).rule;
+ let prop = rule.textProps[0];
+
+ info("Focusing the css property editable value");
+ let editor = yield focusEditableField(view, prop.editor.valueSpan);
+
+ info("Starting to test for css property completion");
+ for (let i = 0; i < testData.length; i++) {
+ // Re-define the editor at each iteration, because the focus may have moved
+ // from property to value and back
+ editor = inplaceEditor(view.styleDocument.activeElement);
+ yield testCompletion(testData[i], editor, view);
+ }
+}
+
+function* testCompletion([key, modifiers, completion, open, selected, change],
+ editor, view) {
+ info("Pressing key " + key);
+ info("Expecting " + completion);
+ info("Is popup opened: " + open);
+ info("Is item selected: " + selected);
+
+ let onDone;
+ if (change) {
+ // If the key triggers a ruleview-changed, wait for that event, it will
+ // always be the last to be triggered and tells us when the preview has
+ // been done.
+ onDone = view.once("ruleview-changed");
+ } else {
+ // Otherwise, expect an after-suggest event (except if the popup gets
+ // closed).
+ onDone = key !== "VK_RIGHT" && key !== "VK_BACK_SPACE"
+ ? editor.once("after-suggest")
+ : null;
+ }
+
+ info("Synthesizing key " + key + ", modifiers: " + Object.keys(modifiers));
+
+ // Also listening for popup opened/closed events if needed.
+ let popupEvent = open ? "popup-opened" : "popup-closed";
+ let onPopupEvent = editor.popup.isOpen !== open ? once(editor.popup, popupEvent) : null;
+
+ EventUtils.synthesizeKey(key, modifiers, view.styleWindow);
+
+ // Flush the throttle for the preview text.
+ view.throttle.flush();
+
+ yield onDone;
+ yield onPopupEvent;
+
+ // The key might have been a TAB or shift-TAB, in which case the editor will
+ // be a new one
+ editor = inplaceEditor(view.styleDocument.activeElement);
+
+ info("Checking the state");
+ if (completion !== null) {
+ is(editor.input.value, completion, "Correct value is autocompleted");
+ }
+
+ if (!open) {
+ ok(!(editor.popup && editor.popup.isOpen), "Popup is closed");
+ } else {
+ ok(editor.popup.isOpen, "Popup is open");
+ is(editor.popup.selectedIndex !== -1, selected, "An item is selected");
+ }
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_completion-new-property_01.js b/devtools/client/inspector/rules/test/browser_rules_completion-new-property_01.js
new file mode 100644
index 000000000..86ff9ca03
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_completion-new-property_01.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";
+
+// Tests that CSS property names are autocompleted and cycled correctly when
+// creating a new property in the rule view.
+
+// format :
+// [
+// what key to press,
+// expected input box value after keypress,
+// is the popup open,
+// is a suggestion selected in the popup,
+// ]
+const OPEN = true, SELECTED = true;
+var testData = [
+ ["d", "display", OPEN, SELECTED],
+ ["VK_DOWN", "dominant-baseline", OPEN, SELECTED],
+ ["VK_DOWN", "direction", OPEN, SELECTED],
+ ["VK_DOWN", "display", OPEN, SELECTED],
+ ["VK_UP", "direction", OPEN, SELECTED],
+ ["VK_UP", "dominant-baseline", OPEN, SELECTED],
+ ["VK_UP", "display", OPEN, SELECTED],
+ ["VK_BACK_SPACE", "d", !OPEN, !SELECTED],
+ ["i", "display", OPEN, SELECTED],
+ ["s", "display", !OPEN, !SELECTED],
+ ["VK_BACK_SPACE", "dis", !OPEN, !SELECTED],
+ ["VK_BACK_SPACE", "di", !OPEN, !SELECTED],
+ ["VK_BACK_SPACE", "d", !OPEN, !SELECTED],
+ ["VK_BACK_SPACE", "", !OPEN, !SELECTED],
+ ["f", "font-size", OPEN, SELECTED],
+ ["i", "filter", OPEN, SELECTED],
+ ["VK_ESCAPE", null, !OPEN, !SELECTED],
+];
+
+const TEST_URI = "<h1 style='border: 1px solid red'>Header</h1>";
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {toolbox, inspector, view, testActor} = yield openRuleView();
+
+ info("Test autocompletion after 1st page load");
+ yield runAutocompletionTest(toolbox, inspector, view);
+
+ info("Test autocompletion after page navigation");
+ yield reloadPage(inspector, testActor);
+ yield runAutocompletionTest(toolbox, inspector, view);
+});
+
+function* runAutocompletionTest(toolbox, inspector, view) {
+ info("Selecting the test node");
+ yield selectNode("h1", inspector);
+
+ info("Focusing the css property editable field");
+ let ruleEditor = getRuleViewRuleEditor(view, 0);
+ let editor = yield focusNewRuleViewProperty(ruleEditor);
+
+ info("Starting to test for css property completion");
+ for (let i = 0; i < testData.length; i++) {
+ yield testCompletion(testData[i], editor, view);
+ }
+}
+
+function* testCompletion([key, completion, open, isSelected], editor, view) {
+ info("Pressing key " + key);
+ info("Expecting " + completion);
+ info("Is popup opened: " + open);
+ info("Is item selected: " + isSelected);
+
+ let onSuggest;
+
+ if (/(right|back_space|escape)/ig.test(key)) {
+ info("Adding event listener for right|back_space|escape keys");
+ onSuggest = once(editor.input, "keypress");
+ } else {
+ info("Waiting for after-suggest event on the editor");
+ onSuggest = editor.once("after-suggest");
+ }
+
+ // Also listening for popup opened/closed events if needed.
+ let popupEvent = open ? "popup-opened" : "popup-closed";
+ let onPopupEvent = editor.popup.isOpen !== open ? once(editor.popup, popupEvent) : null;
+
+ info("Synthesizing key " + key);
+ EventUtils.synthesizeKey(key, {}, view.styleWindow);
+
+ yield onSuggest;
+ yield onPopupEvent;
+
+ info("Checking the state");
+ if (completion !== null) {
+ is(editor.input.value, completion, "Correct value is autocompleted");
+ }
+ if (!open) {
+ ok(!(editor.popup && editor.popup.isOpen), "Popup is closed");
+ } else {
+ ok(editor.popup.isOpen, "Popup is open");
+ is(editor.popup.selectedIndex !== -1, isSelected, "An item is selected");
+ }
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_completion-new-property_02.js b/devtools/client/inspector/rules/test/browser_rules_completion-new-property_02.js
new file mode 100644
index 000000000..d89e5129d
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_completion-new-property_02.js
@@ -0,0 +1,129 @@
+/* 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";
+
+// Tests that CSS property names and values are autocompleted and cycled
+// correctly when editing new properties in the rule view.
+
+// format :
+// [
+// what key to press,
+// modifers,
+// expected input box value after keypress,
+// is the popup open,
+// is a suggestion selected in the popup,
+// expect ruleview-changed,
+// ]
+
+const OPEN = true, SELECTED = true, CHANGE = true;
+const testData = [
+ ["d", {}, "display", OPEN, SELECTED, !CHANGE],
+ ["VK_TAB", {}, "", OPEN, !SELECTED, CHANGE],
+ ["VK_DOWN", {}, "block", OPEN, SELECTED, CHANGE],
+ ["n", {}, "none", !OPEN, !SELECTED, CHANGE],
+ ["VK_TAB", {shiftKey: true}, "display", !OPEN, !SELECTED, CHANGE],
+ ["VK_BACK_SPACE", {}, "", !OPEN, !SELECTED, !CHANGE],
+ ["o", {}, "overflow", OPEN, SELECTED, !CHANGE],
+ ["u", {}, "outline", OPEN, SELECTED, !CHANGE],
+ ["VK_DOWN", {}, "outline-color", OPEN, SELECTED, !CHANGE],
+ ["VK_TAB", {}, "none", !OPEN, !SELECTED, CHANGE],
+ ["r", {}, "rebeccapurple", OPEN, SELECTED, CHANGE],
+ ["VK_DOWN", {}, "red", OPEN, SELECTED, CHANGE],
+ ["VK_DOWN", {}, "rgb", OPEN, SELECTED, CHANGE],
+ ["VK_DOWN", {}, "rgba", OPEN, SELECTED, CHANGE],
+ ["VK_DOWN", {}, "rosybrown", OPEN, SELECTED, CHANGE],
+ ["VK_DOWN", {}, "royalblue", OPEN, SELECTED, CHANGE],
+ ["VK_RIGHT", {}, "royalblue", !OPEN, !SELECTED, !CHANGE],
+ [" ", {}, "royalblue aliceblue", OPEN, SELECTED, CHANGE],
+ ["!", {}, "royalblue !important", !OPEN, !SELECTED, CHANGE],
+ ["VK_ESCAPE", {}, null, !OPEN, !SELECTED, CHANGE]
+];
+
+const TEST_URI = `
+ <style type="text/css">
+ h1 {
+ border: 1px solid red;
+ }
+ </style>
+ <h1>Test element</h1>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {toolbox, inspector, view, testActor} = yield openRuleView();
+
+ info("Test autocompletion after 1st page load");
+ yield runAutocompletionTest(toolbox, inspector, view);
+
+ info("Test autocompletion after page navigation");
+ yield reloadPage(inspector, testActor);
+ yield runAutocompletionTest(toolbox, inspector, view);
+});
+
+function* runAutocompletionTest(toolbox, inspector, view) {
+ info("Selecting the test node");
+ yield selectNode("h1", inspector);
+
+ info("Focusing a new css property editable property");
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+ let editor = yield focusNewRuleViewProperty(ruleEditor);
+
+ info("Starting to test for css property completion");
+ for (let i = 0; i < testData.length; i++) {
+ // Re-define the editor at each iteration, because the focus may have moved
+ // from property to value and back
+ editor = inplaceEditor(view.styleDocument.activeElement);
+ yield testCompletion(testData[i], editor, view);
+ }
+}
+
+function* testCompletion([key, modifiers, completion, open, selected, change],
+ editor, view) {
+ info("Pressing key " + key);
+ info("Expecting " + completion);
+ info("Is popup opened: " + open);
+ info("Is item selected: " + selected);
+
+ let onDone;
+ if (change) {
+ // If the key triggers a ruleview-changed, wait for that event, it will
+ // always be the last to be triggered and tells us when the preview has
+ // been done.
+ onDone = view.once("ruleview-changed");
+ } else {
+ // Otherwise, expect an after-suggest event (except if the popup gets
+ // closed).
+ onDone = key !== "VK_RIGHT" && key !== "VK_BACK_SPACE"
+ ? editor.once("after-suggest")
+ : null;
+ }
+
+ // Also listening for popup opened/closed events if needed.
+ let popupEvent = open ? "popup-opened" : "popup-closed";
+ let onPopupEvent = editor.popup.isOpen !== open ? once(editor.popup, popupEvent) : null;
+
+ info("Synthesizing key " + key + ", modifiers: " + Object.keys(modifiers));
+ EventUtils.synthesizeKey(key, modifiers, view.styleWindow);
+
+ // Flush the throttle for the preview text.
+ view.throttle.flush();
+
+ yield onDone;
+ yield onPopupEvent;
+
+ info("Checking the state");
+ if (completion !== null) {
+ // The key might have been a TAB or shift-TAB, in which case the editor will
+ // be a new one
+ editor = inplaceEditor(view.styleDocument.activeElement);
+ is(editor.input.value, completion, "Correct value is autocompleted");
+ }
+ if (!open) {
+ ok(!(editor.popup && editor.popup.isOpen), "Popup is closed");
+ } else {
+ ok(editor.popup.isOpen, "Popup is open");
+ is(editor.popup.selectedIndex !== -1, selected, "An item is selected");
+ }
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_completion-new-property_03.js b/devtools/client/inspector/rules/test/browser_rules_completion-new-property_03.js
new file mode 100644
index 000000000..a5072429c
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_completion-new-property_03.js
@@ -0,0 +1,47 @@
+/* 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";
+
+// Regression test for a case where completing gave the wrong answer.
+// See bug 1179318.
+
+const TEST_URI = "<h1 style='color: red'>Header</h1>";
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {toolbox, inspector, view} = yield openRuleView();
+
+ info("Test autocompletion for background-color");
+ yield runAutocompletionTest(toolbox, inspector, view);
+});
+
+function* runAutocompletionTest(toolbox, inspector, view) {
+ info("Selecting the test node");
+ yield selectNode("h1", inspector);
+
+ info("Focusing the new property editable field");
+ let ruleEditor = getRuleViewRuleEditor(view, 0);
+ let editor = yield focusNewRuleViewProperty(ruleEditor);
+
+ info("Sending \"background\" to the editable field");
+ for (let key of "background") {
+ let onSuggest = editor.once("after-suggest");
+ EventUtils.synthesizeKey(key, {}, view.styleWindow);
+ yield onSuggest;
+ }
+
+ const itemIndex = 4;
+
+ let bgcItem = editor.popup.getItemAtIndex(itemIndex);
+ is(bgcItem.label, "background-color",
+ "check the expected completion element");
+
+ editor.popup.selectedIndex = itemIndex;
+
+ let node = editor.popup._list.childNodes[itemIndex];
+ EventUtils.synthesizeMouseAtCenter(node, {}, editor.popup._window);
+
+ is(editor.input.value, "background-color", "Correct value is autocompleted");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_completion-new-property_04.js b/devtools/client/inspector/rules/test/browser_rules_completion-new-property_04.js
new file mode 100644
index 000000000..e19794e1b
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_completion-new-property_04.js
@@ -0,0 +1,73 @@
+/* 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 new property editor supports the following flow:
+// - type first character of property name
+// - select an autocomplete suggestion !!with a mouse click!!
+// - press RETURN to move to the property value
+// - blur the input to commit
+
+const TEST_URI = "<style>.title {color: red;}</style>" +
+ "<h1 class=title>Header</h1>";
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let { inspector, view} = yield openRuleView();
+
+ info("Selecting the test node");
+ yield selectNode("h1", inspector);
+
+ info("Focusing the new property editable field");
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+ let editor = yield focusNewRuleViewProperty(ruleEditor);
+
+ info("Sending \"background\" to the editable field.");
+ for (let key of "background") {
+ let onSuggest = editor.once("after-suggest");
+ EventUtils.synthesizeKey(key, {}, view.styleWindow);
+ yield onSuggest;
+ }
+
+ const itemIndex = 4;
+ let bgcItem = editor.popup.getItemAtIndex(itemIndex);
+ is(bgcItem.label, "background-color",
+ "Check the expected completion element is background-color.");
+ editor.popup.selectedIndex = itemIndex;
+
+ info("Select the background-color suggestion with a mouse click.");
+ let onSuggest = editor.once("after-suggest");
+ let node = editor.popup.elements.get(bgcItem);
+ EventUtils.synthesizeMouseAtCenter(node, {}, editor.popup._window);
+
+ yield onSuggest;
+ is(editor.input.value, "background-color", "Correct value is autocompleted");
+
+ info("Press RETURN to move the focus to a property value editor.");
+ let onModifications = view.once("ruleview-changed");
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
+
+ yield onModifications;
+
+ // Getting the new value editor after focus
+ editor = inplaceEditor(view.styleDocument.activeElement);
+ let textProp = ruleEditor.rule.textProps[1];
+
+ is(ruleEditor.rule.textProps.length, 2,
+ "Created a new text property.");
+ is(ruleEditor.propertyList.children.length, 2,
+ "Created a property editor.");
+ is(editor, inplaceEditor(textProp.editor.valueSpan),
+ "Editing the value span now.");
+
+ info("Entering a value and blurring the field to expect a rule change");
+ editor.input.value = "#F00";
+
+ onModifications = view.once("ruleview-changed");
+ editor.input.blur();
+ yield onModifications;
+
+ is(textProp.value, "#F00", "Text prop should have been changed.");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_completion-new-property_multiline.js b/devtools/client/inspector/rules/test/browser_rules_completion-new-property_multiline.js
new file mode 100644
index 000000000..ec939eafc
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_completion-new-property_multiline.js
@@ -0,0 +1,131 @@
+/* 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 behaviour of the CSS autocomplete for CSS value displayed on
+// multiple lines. Expected behavior is:
+// - UP/DOWN should navigate in the input and not increment/decrement numbers
+// - typing a new value should still trigger the autocomplete
+// - UP/DOWN when the autocomplete popup is displayed should cycle through
+// suggestions
+
+const LONG_CSS_VALUE =
+ "transparent linear-gradient(0deg, blue 0%, white 5%, red 10%, blue 15%, " +
+ "white 20%, red 25%, blue 30%, white 35%, red 40%, blue 45%, white 50%, " +
+ "red 55%, blue 60%, white 65%, red 70%, blue 75%, white 80%, red 85%, " +
+ "blue 90%, white 95% ) repeat scroll 0% 0%";
+
+const EXPECTED_CSS_VALUE = LONG_CSS_VALUE.replace("95%", "95%, red");
+
+const TEST_URI =
+ `<style>
+ .title {
+ background: ${LONG_CSS_VALUE};
+ }
+ </style>
+ <h1 class=title>Header</h1>`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let { inspector, view} = yield openRuleView();
+
+ info("Selecting the test node");
+ yield selectNode("h1", inspector);
+
+ info("Focusing the property editable field");
+ let rule = getRuleViewRuleEditor(view, 1).rule;
+ let prop = rule.textProps[0];
+
+ // Calculate offsets to click in the middle of the first box quad.
+ let rect = prop.editor.valueSpan.getBoundingClientRect();
+ let firstQuad = prop.editor.valueSpan.getBoxQuads()[0];
+ // For a multiline value, the first quad left edge is not aligned with the
+ // bounding rect left edge. The offsets expected by focusEditableField are
+ // relative to the bouding rectangle, so we need to translate the x-offset.
+ let x = firstQuad.bounds.left - rect.left + firstQuad.bounds.width / 2;
+ // The first quad top edge is aligned with the bounding top edge, no
+ // translation needed here.
+ let y = firstQuad.bounds.height / 2;
+
+ info("Focusing the css property editable value");
+ let editor = yield focusEditableField(view, prop.editor.valueSpan, x, y);
+
+ info("Moving the caret next to a number");
+ let pos = editor.input.value.indexOf("0deg") + 1;
+ editor.input.setSelectionRange(pos, pos);
+ is(editor.input.value[editor.input.selectionStart - 1], "0",
+ "Input caret is after a 0");
+
+ info("Check that UP/DOWN navigates in the input, even when next to a number");
+ EventUtils.synthesizeKey("VK_DOWN", {}, view.styleWindow);
+ ok(editor.input.selectionStart !== pos, "Input caret moved");
+ is(editor.input.value, LONG_CSS_VALUE, "Input value was not decremented.");
+
+ info("Move the caret to the end of the gradient definition.");
+ pos = editor.input.value.indexOf("95%") + 3;
+ editor.input.setSelectionRange(pos, pos);
+
+ info("Sending \", re\" to the editable field.");
+ for (let key of ", re") {
+ yield synthesizeKeyForAutocomplete(key, editor, view.styleWindow);
+ }
+
+ info("Check the autocomplete can still be displayed.");
+ ok(editor.popup && editor.popup.isOpen, "Autocomplete popup is displayed.");
+ is(editor.popup.selectedIndex, 0,
+ "Autocomplete has an item selected by default");
+
+ let item = editor.popup.getItemAtIndex(editor.popup.selectedIndex);
+ is(item.label, "rebeccapurple",
+ "Check autocomplete displays expected value.");
+
+ info("Check autocomplete suggestions can be cycled using UP/DOWN arrows.");
+
+ yield synthesizeKeyForAutocomplete("VK_DOWN", editor, view.styleWindow);
+ ok(editor.popup.selectedIndex, 1, "Using DOWN cycles autocomplete values.");
+ yield synthesizeKeyForAutocomplete("VK_DOWN", editor, view.styleWindow);
+ ok(editor.popup.selectedIndex, 2, "Using DOWN cycles autocomplete values.");
+ yield synthesizeKeyForAutocomplete("VK_UP", editor, view.styleWindow);
+ is(editor.popup.selectedIndex, 1, "Using UP cycles autocomplete values.");
+ item = editor.popup.getItemAtIndex(editor.popup.selectedIndex);
+ is(item.label, "red", "Check autocomplete displays expected value.");
+
+ info("Select the background-color suggestion with a mouse click.");
+ let onRuleviewChanged = view.once("ruleview-changed");
+ let onSuggest = editor.once("after-suggest");
+
+ let node = editor.popup._list.childNodes[editor.popup.selectedIndex];
+ EventUtils.synthesizeMouseAtCenter(node, {}, editor.popup._window);
+
+ view.throttle.flush();
+ yield onSuggest;
+ yield onRuleviewChanged;
+
+ is(editor.input.value, EXPECTED_CSS_VALUE,
+ "Input value correctly autocompleted");
+
+ info("Press ESCAPE to leave the input.");
+ onRuleviewChanged = view.once("ruleview-changed");
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, view.styleWindow);
+ yield onRuleviewChanged;
+});
+
+/**
+ * Send the provided key to the currently focused input of the provided window.
+ * Wait for the editor to emit "after-suggest" to make sure the autocompletion
+ * process is finished.
+ *
+ * @param {String} key
+ * The key to send to the input.
+ * @param {InplaceEditor} editor
+ * The inplace editor which owns the focused input.
+ * @param {Window} win
+ * Window in which the key event will be dispatched.
+ */
+function* synthesizeKeyForAutocomplete(key, editor, win) {
+ let onSuggest = editor.once("after-suggest");
+ EventUtils.synthesizeKey(key, {}, win);
+ yield onSuggest;
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_completion-popup-hidden-after-navigation.js b/devtools/client/inspector/rules/test/browser_rules_completion-popup-hidden-after-navigation.js
new file mode 100644
index 000000000..84f119606
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_completion-popup-hidden-after-navigation.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";
+
+// Tests that the ruleview autocomplete popup is hidden after page navigation.
+
+const TEST_URI = "<h1 style='font: 24px serif'></h1>";
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view, testActor} = yield openRuleView();
+
+ info("Test autocompletion popup is hidden after page navigation");
+
+ info("Selecting the test node");
+ yield selectNode("h1", inspector);
+
+ info("Focusing the css property editable field");
+ let propertyName = view.styleDocument
+ .querySelectorAll(".ruleview-propertyname")[0];
+ let editor = yield focusEditableField(view, propertyName);
+
+ info("Pressing key VK_DOWN");
+ let onSuggest = once(editor.input, "keypress");
+ let onPopupOpened = once(editor.popup, "popup-opened");
+
+ EventUtils.synthesizeKey("VK_DOWN", {}, view.styleWindow);
+
+ info("Waiting for autocomplete popup to be displayed");
+ yield onSuggest;
+ yield onPopupOpened;
+
+ ok(view.popup && view.popup.isOpen, "Popup should be opened");
+
+ info("Reloading the page");
+ yield reloadPage(inspector, testActor);
+
+ ok(!(view.popup && view.popup.isOpen), "Popup should be closed");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_computed-lists_01.js b/devtools/client/inspector/rules/test/browser_rules_computed-lists_01.js
new file mode 100644
index 000000000..5acebd562
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_computed-lists_01.js
@@ -0,0 +1,47 @@
+/* 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";
+
+// Tests that the rule view shows expanders for properties with computed lists.
+
+var TEST_URI = `
+ <style type="text/css">
+ #testid {
+ margin: 4px;
+ top: 0px;
+ }
+ </style>
+ <h1 id="testid">Styled Node</h1>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+ yield testExpandersShown(inspector, view);
+});
+
+function* testExpandersShown(inspector, view) {
+ let rule = getRuleViewRuleEditor(view, 1).rule;
+
+ info("Check that the correct rules are visible");
+ is(rule.selectorText, "#testid", "Second rule is #testid.");
+ is(rule.textProps[0].name, "margin", "First property is margin.");
+ is(rule.textProps[1].name, "top", "Second property is top.");
+
+ info("Check that the expanders are shown correctly");
+ is(rule.textProps[0].editor.expander.style.visibility, "visible",
+ "margin expander is visible.");
+ is(rule.textProps[1].editor.expander.style.visibility, "hidden",
+ "top expander is hidden.");
+ ok(!rule.textProps[0].editor.expander.hasAttribute("open"),
+ "margin computed list is closed.");
+ ok(!rule.textProps[1].editor.expander.hasAttribute("open"),
+ "top computed list is closed.");
+ ok(!rule.textProps[0].editor.computed.hasChildNodes(),
+ "margin computed list is empty before opening.");
+ ok(!rule.textProps[1].editor.computed.hasChildNodes(),
+ "top computed list is empty.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_computed-lists_02.js b/devtools/client/inspector/rules/test/browser_rules_computed-lists_02.js
new file mode 100644
index 000000000..d6dc82d5f
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_computed-lists_02.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";
+
+// Tests that the rule view computed lists can be expanded/collapsed,
+// and contain the right subproperties.
+
+var TEST_URI = `
+ <style type="text/css">
+ #testid {
+ margin: 0px 1px 2px 3px;
+ top: 0px;
+ }
+ </style>
+ <h1 id="testid">Styled Node</h1>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+ yield testComputedList(inspector, view);
+});
+
+function* testComputedList(inspector, view) {
+ let rule = getRuleViewRuleEditor(view, 1).rule;
+ let propEditor = rule.textProps[0].editor;
+ let expander = propEditor.expander;
+
+ ok(!expander.hasAttribute("open"), "margin computed list is closed");
+
+ info("Opening the computed list of margin property");
+ expander.click();
+ ok(expander.hasAttribute("open"), "margin computed list is open");
+
+ let computed = propEditor.prop.computed;
+ let computedDom = propEditor.computed;
+ let propNames = [
+ "margin-top",
+ "margin-right",
+ "margin-bottom",
+ "margin-left"
+ ];
+
+ is(computed.length, propNames.length, "There should be 4 computed values");
+ is(computedDom.children.length, propNames.length,
+ "There should be 4 nodes in the DOM");
+
+ propNames.forEach((propName, i) => {
+ let propValue = i + "px";
+ is(computed[i].name, propName,
+ "Computed property #" + i + " has name " + propName);
+ is(computed[i].value, propValue,
+ "Computed property #" + i + " has value " + propValue);
+ is(computedDom.querySelectorAll(".ruleview-propertyname")[i].textContent,
+ propName,
+ "Computed property #" + i + " in DOM has correct name");
+ is(computedDom.querySelectorAll(".ruleview-propertyvalue")[i].textContent,
+ propValue,
+ "Computed property #" + i + " in DOM has correct value");
+ });
+
+ info("Closing the computed list of margin property");
+ expander.click();
+ ok(!expander.hasAttribute("open"), "margin computed list is closed");
+
+ info("Opening the computed list of margin property");
+ expander.click();
+ ok(expander.hasAttribute("open"), "margin computed list is open");
+ is(computed.length, propNames.length, "Still 4 computed values");
+ is(computedDom.children.length, propNames.length, "Still 4 nodes in the DOM");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_content_01.js b/devtools/client/inspector/rules/test/browser_rules_content_01.js
new file mode 100644
index 000000000..8695d9b8d
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_content_01.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";
+
+// Test that the rule-view content is correct
+
+const TEST_URI = `
+ <style type="text/css">
+ @media screen and (min-width: 10px) {
+ #testid {
+ background-color: blue;
+ }
+ }
+ .testclass, .unmatched {
+ background-color: green;
+ }
+ </style>
+ <div id="testid" class="testclass">Styled Node</div>
+ <div id="testid2">Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+
+ yield selectNode("#testid", inspector);
+ is(view.element.querySelectorAll("#ruleview-no-results").length, 0,
+ "After a highlight, no longer has a no-results element.");
+
+ yield clearCurrentNodeSelection(inspector);
+ is(view.element.querySelectorAll("#ruleview-no-results").length, 1,
+ "After highlighting null, has a no-results element again.");
+
+ yield selectNode("#testid", inspector);
+
+ let linkText = getRuleViewLinkTextByIndex(view, 1);
+ is(linkText, "inline:3 @screen and (min-width: 10px)",
+ "link text at index 1 contains media query text.");
+
+ linkText = getRuleViewLinkTextByIndex(view, 2);
+ is(linkText, "inline:7",
+ "link text at index 2 contains no media query text.");
+
+ let selector = getRuleViewRuleEditor(view, 2).selectorText;
+ is(selector.querySelector(".ruleview-selector-matched").textContent,
+ ".testclass", ".textclass should be matched.");
+ is(selector.querySelector(".ruleview-selector-unmatched").textContent,
+ ".unmatched", ".unmatched should not be matched.");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_content_02.js b/devtools/client/inspector/rules/test/browser_rules_content_02.js
new file mode 100644
index 000000000..253f374b4
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_content_02.js
@@ -0,0 +1,60 @@
+/* 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/ */
+/* globals getTestActorWithoutToolbox */
+"use strict";
+
+// Test the rule-view content when the inspector gets opened via the page
+// ctx-menu "inspect element"
+
+const CONTENT = `
+ <body style="color:red;">
+ <div style="color:blue;">
+ <p style="color:green;">
+ <span style="color:yellow;">test element</span>
+ </p>
+ </div>
+ </body>
+`;
+
+add_task(function* () {
+ let tab = yield addTab("data:text/html;charset=utf-8," + CONTENT);
+
+ let testActor = yield getTestActorWithoutToolbox(tab);
+ let inspector = yield clickOnInspectMenuItem(testActor, "span");
+
+ checkRuleViewContent(inspector.ruleview.view);
+});
+
+function checkRuleViewContent({styleDocument}) {
+ info("Making sure the rule-view contains the expected content");
+
+ let headers = [...styleDocument.querySelectorAll(".ruleview-header")];
+ is(headers.length, 3, "There are 3 headers for inherited rules");
+
+ is(headers[0].textContent,
+ STYLE_INSPECTOR_L10N.getFormatStr("rule.inheritedFrom", "p"),
+ "The first header is correct");
+ is(headers[1].textContent,
+ STYLE_INSPECTOR_L10N.getFormatStr("rule.inheritedFrom", "div"),
+ "The second header is correct");
+ is(headers[2].textContent,
+ STYLE_INSPECTOR_L10N.getFormatStr("rule.inheritedFrom", "body"),
+ "The third header is correct");
+
+ let rules = styleDocument.querySelectorAll(".ruleview-rule");
+ is(rules.length, 4, "There are 4 rules in the view");
+
+ for (let rule of rules) {
+ let selector = rule.querySelector(".ruleview-selectorcontainer");
+ is(selector.textContent, STYLE_INSPECTOR_L10N.getStr("rule.sourceElement"),
+ "The rule's selector is correct");
+
+ let propertyNames = [...rule.querySelectorAll(".ruleview-propertyname")];
+ is(propertyNames.length, 1, "There's only one property name, as expected");
+
+ let propertyValues = [...rule.querySelectorAll(".ruleview-propertyvalue")];
+ is(propertyValues.length, 1, "There's only one property value, as expected");
+ }
+}
+
diff --git a/devtools/client/inspector/rules/test/browser_rules_context-menu-show-mdn-docs-01.js b/devtools/client/inspector/rules/test/browser_rules_context-menu-show-mdn-docs-01.js
new file mode 100644
index 000000000..b81bb8013
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_context-menu-show-mdn-docs-01.js
@@ -0,0 +1,96 @@
+/* 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/ */
+
+/**
+ * This file tests the code that integrates the Style Inspector's rule view
+ * with the MDN docs tooltip.
+ *
+ * If you display the context click on a property name in the rule view, you
+ * should see a menu item "Show MDN Docs". If you click that item, the MDN
+ * docs tooltip should be shown, containing docs from MDN for that property.
+ *
+ * This file tests that the context menu item is shown when it should be
+ * shown and hidden when it should be hidden.
+ */
+
+"use strict";
+
+/**
+ * The test document tries to confuse the context menu
+ * code by having a tag called "padding" and a property
+ * value called "margin".
+ */
+const TEST_URI = `
+ <html>
+ <head>
+ <style>
+ padding {font-family: margin;}
+ </style>
+ </head>
+
+ <body>
+ <padding>MDN tooltip testing</padding>
+ </body>
+ </html>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("padding", inspector);
+ yield testMdnContextMenuItemVisibility(view);
+});
+
+/**
+ * Tests that the MDN context menu item is shown when it should be,
+ * and hidden when it should be.
+ * - iterate through every node in the rule view
+ * - set that node as popupNode (the node that the context menu
+ * is shown for)
+ * - update the context menu's state
+ * - test that the MDN context menu item is hidden, or not,
+ * depending on popupNode
+ */
+function* testMdnContextMenuItemVisibility(view) {
+ info("Test that MDN context menu item is shown only when it should be.");
+
+ let root = rootElement(view);
+ for (let node of iterateNodes(root)) {
+ info("Setting " + node + " as popupNode");
+ info("Creating context menu with " + node + " as popupNode");
+ let allMenuItems = openStyleContextMenuAndGetAllItems(view, node);
+ let menuitemShowMdnDocs = allMenuItems.find(item => item.label ===
+ STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.showMdnDocs"));
+
+ let isVisible = menuitemShowMdnDocs.visible;
+ let shouldBeVisible = isPropertyNameNode(node);
+ let message = shouldBeVisible ? "shown" : "hidden";
+ is(isVisible, shouldBeVisible,
+ "The MDN context menu item is " + message + " ; content : " +
+ node.textContent + " ; type : " + node.nodeType);
+ }
+}
+
+/**
+ * Check if a node is a property name.
+ */
+function isPropertyNameNode(node) {
+ return node.textContent === "font-family";
+}
+
+/**
+ * A generator that iterates recursively through all child nodes of baseNode.
+ */
+function* iterateNodes(baseNode) {
+ yield baseNode;
+
+ for (let child of baseNode.childNodes) {
+ yield* iterateNodes(child);
+ }
+}
+
+/**
+ * Returns the root element for the rule view.
+ */
+var rootElement = view => (view.element) ? view.element : view.styleDocument;
diff --git a/devtools/client/inspector/rules/test/browser_rules_context-menu-show-mdn-docs-02.js b/devtools/client/inspector/rules/test/browser_rules_context-menu-show-mdn-docs-02.js
new file mode 100644
index 000000000..e0d08d28a
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_context-menu-show-mdn-docs-02.js
@@ -0,0 +1,61 @@
+/* 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/ */
+
+/**
+ * This file tests the code that integrates the Style Inspector's rule view
+ * with the MDN docs tooltip.
+ *
+ * If you display the context click on a property name in the rule view, you
+ * should see a menu item "Show MDN Docs". If you click that item, the MDN
+ * docs tooltip should be shown, containing docs from MDN for that property.
+ *
+ * This file tests that:
+ * - clicking the context menu item shows the tooltip
+ * - the tooltip content matches the property name for which the context menu was opened
+ */
+
+"use strict";
+
+const {setBaseCssDocsUrl} =
+ require("devtools/client/shared/widgets/MdnDocsWidget");
+
+const PROPERTYNAME = "color";
+
+const TEST_DOC = `
+ <html>
+ <body>
+ <div style="color: red">
+ Test "Show MDN Docs" context menu option
+ </div>
+ </body>
+ </html>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf8," + encodeURIComponent(TEST_DOC));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("div", inspector);
+
+ setBaseCssDocsUrl(URL_ROOT);
+
+ info("Setting the popupNode for the MDN docs tooltip");
+
+ let {nameSpan} = getRuleViewProperty(view, "element", PROPERTYNAME);
+
+ let allMenuItems = openStyleContextMenuAndGetAllItems(view, nameSpan.firstChild);
+ let menuitemShowMdnDocs = allMenuItems.find(item => item.label ===
+ STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.showMdnDocs"));
+
+ let cssDocs = view.tooltips.cssDocs;
+
+ info("Showing the MDN docs tooltip");
+ let onShown = cssDocs.tooltip.once("shown");
+ menuitemShowMdnDocs.click();
+ yield onShown;
+ ok(true, "The MDN docs tooltip was shown");
+
+ info("Quick check that the tooltip contents are set");
+ let h1 = cssDocs.tooltip.container.querySelector(".mdn-property-name");
+ is(h1.textContent, PROPERTYNAME, "The MDN docs tooltip h1 is correct");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_context-menu-show-mdn-docs-03.js b/devtools/client/inspector/rules/test/browser_rules_context-menu-show-mdn-docs-03.js
new file mode 100644
index 000000000..d1089fcf6
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_context-menu-show-mdn-docs-03.js
@@ -0,0 +1,118 @@
+/* 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/ */
+
+/**
+ * This file tests the "devtools.inspector.mdnDocsTooltip.enabled" preference,
+ * that we use to enable/disable the MDN tooltip in the Inspector.
+ *
+ * The desired behavior is:
+ * - if the preference is true, show the "Show MDN Docs" context menu item
+ * - if the preference is false, don't show the item
+ * - listen for changes to the pref, so we can show/hide the item dynamically
+ */
+
+"use strict";
+
+const { PrefObserver } = require("devtools/client/styleeditor/utils");
+const PREF_ENABLE_MDN_DOCS_TOOLTIP =
+ "devtools.inspector.mdnDocsTooltip.enabled";
+const PROPERTY_NAME_CLASS = "ruleview-propertyname";
+
+const TEST_DOC = `
+ <html>
+ <body>
+ <div style="color: red">
+ Test the pref to enable/disable the "Show MDN Docs" context menu option
+ </div>
+ </body>
+ </html>
+`;
+
+add_task(function* () {
+ info("Ensure the pref is true to begin with");
+ let initial = Services.prefs.getBoolPref(PREF_ENABLE_MDN_DOCS_TOOLTIP);
+ if (initial != true) {
+ setBooleanPref(PREF_ENABLE_MDN_DOCS_TOOLTIP, true);
+ }
+
+ yield addTab("data:text/html;charset=utf8," + encodeURIComponent(TEST_DOC));
+
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("div", inspector);
+ yield testMdnContextMenuItemVisibility(view, true);
+
+ yield setBooleanPref(PREF_ENABLE_MDN_DOCS_TOOLTIP, false);
+ yield testMdnContextMenuItemVisibility(view, false);
+
+ info("Close the Inspector");
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ yield gDevTools.closeToolbox(target);
+
+ ({inspector, view} = yield openRuleView());
+ yield selectNode("div", inspector);
+ yield testMdnContextMenuItemVisibility(view, false);
+
+ yield setBooleanPref(PREF_ENABLE_MDN_DOCS_TOOLTIP, true);
+ yield testMdnContextMenuItemVisibility(view, true);
+
+ info("Ensure the pref is reset to its initial value");
+ let eventual = Services.prefs.getBoolPref(PREF_ENABLE_MDN_DOCS_TOOLTIP);
+ if (eventual != initial) {
+ setBooleanPref(PREF_ENABLE_MDN_DOCS_TOOLTIP, initial);
+ }
+});
+
+/**
+ * Set a boolean pref, and wait for the pref observer to
+ * trigger, so that code listening for the pref change
+ * has had a chance to update itself.
+ *
+ * @param pref {string} Name of the pref to change
+ * @param state {boolean} Desired value of the pref.
+ *
+ * Note that if the pref already has the value in `state`,
+ * then the prefObserver will not trigger. So you should only
+ * call this function if you know the pref's current value is
+ * not `state`.
+ */
+function* setBooleanPref(pref, state) {
+ let oncePrefChanged = defer();
+ let prefObserver = new PrefObserver("devtools.");
+ prefObserver.on(pref, oncePrefChanged.resolve);
+
+ info("Set the pref " + pref + " to: " + state);
+ Services.prefs.setBoolPref(pref, state);
+
+ info("Wait for prefObserver to call back so the UI can update");
+ yield oncePrefChanged.promise;
+ prefObserver.off(pref, oncePrefChanged.resolve);
+}
+
+/**
+ * Test whether the MDN tooltip context menu item is visible when it should be.
+ *
+ * @param view The rule view
+ * @param shouldBeVisible {boolean} Whether we expect the context
+ * menu item to be visible or not.
+ */
+function* testMdnContextMenuItemVisibility(view, shouldBeVisible) {
+ let message = shouldBeVisible ? "shown" : "hidden";
+ info("Test that MDN context menu item is " + message);
+
+ info("Set a CSS property name as popupNode");
+ let root = rootElement(view);
+ let node = root.querySelector("." + PROPERTY_NAME_CLASS).firstChild;
+ let allMenuItems = openStyleContextMenuAndGetAllItems(view, node);
+ let menuitemShowMdnDocs = allMenuItems.find(item => item.label ===
+ STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.showMdnDocs"));
+
+ let isVisible = menuitemShowMdnDocs.visible;
+ is(isVisible, shouldBeVisible,
+ "The MDN context menu item is " + message);
+}
+
+/**
+ * Returns the root element for the rule view.
+ */
+var rootElement = view => (view.element) ? view.element : view.styleDocument;
diff --git a/devtools/client/inspector/rules/test/browser_rules_copy_styles.js b/devtools/client/inspector/rules/test/browser_rules_copy_styles.js
new file mode 100644
index 000000000..a6f991a60
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_copy_styles.js
@@ -0,0 +1,307 @@
+/* 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";
+
+/**
+ * Tests the behaviour of the copy styles context menu items in the rule
+ * view.
+ */
+
+const osString = Services.appinfo.OS;
+
+const TEST_URI = URL_ROOT + "doc_copystyles.html";
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let { inspector, view } = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+
+ let data = [
+ {
+ desc: "Test Copy Property Name",
+ node: ruleEditor.rule.textProps[0].editor.nameSpan,
+ menuItemLabel: "styleinspector.contextmenu.copyPropertyName",
+ expectedPattern: "color",
+ visible: {
+ copyLocation: false,
+ copyPropertyDeclaration: true,
+ copyPropertyName: true,
+ copyPropertyValue: false,
+ copySelector: false,
+ copyRule: true
+ }
+ },
+ {
+ desc: "Test Copy Property Value",
+ node: ruleEditor.rule.textProps[2].editor.valueSpan,
+ menuItemLabel: "styleinspector.contextmenu.copyPropertyValue",
+ expectedPattern: "12px",
+ visible: {
+ copyLocation: false,
+ copyPropertyDeclaration: true,
+ copyPropertyName: false,
+ copyPropertyValue: true,
+ copySelector: false,
+ copyRule: true
+ }
+ },
+ {
+ desc: "Test Copy Property Value with Priority",
+ node: ruleEditor.rule.textProps[3].editor.valueSpan,
+ menuItemLabel: "styleinspector.contextmenu.copyPropertyValue",
+ expectedPattern: "#00F !important",
+ visible: {
+ copyLocation: false,
+ copyPropertyDeclaration: true,
+ copyPropertyName: false,
+ copyPropertyValue: true,
+ copySelector: false,
+ copyRule: true
+ }
+ },
+ {
+ desc: "Test Copy Property Declaration",
+ node: ruleEditor.rule.textProps[2].editor.nameSpan,
+ menuItemLabel: "styleinspector.contextmenu.copyPropertyDeclaration",
+ expectedPattern: "font-size: 12px;",
+ visible: {
+ copyLocation: false,
+ copyPropertyDeclaration: true,
+ copyPropertyName: true,
+ copyPropertyValue: false,
+ copySelector: false,
+ copyRule: true
+ }
+ },
+ {
+ desc: "Test Copy Property Declaration with Priority",
+ node: ruleEditor.rule.textProps[3].editor.nameSpan,
+ menuItemLabel: "styleinspector.contextmenu.copyPropertyDeclaration",
+ expectedPattern: "border-color: #00F !important;",
+ visible: {
+ copyLocation: false,
+ copyPropertyDeclaration: true,
+ copyPropertyName: true,
+ copyPropertyValue: false,
+ copySelector: false,
+ copyRule: true
+ }
+ },
+ {
+ desc: "Test Copy Rule",
+ node: ruleEditor.rule.textProps[2].editor.nameSpan,
+ menuItemLabel: "styleinspector.contextmenu.copyRule",
+ expectedPattern: "#testid {[\\r\\n]+" +
+ "\tcolor: #F00;[\\r\\n]+" +
+ "\tbackground-color: #00F;[\\r\\n]+" +
+ "\tfont-size: 12px;[\\r\\n]+" +
+ "\tborder-color: #00F !important;[\\r\\n]+" +
+ "\t--var: \"\\*/\";[\\r\\n]+" +
+ "}",
+ visible: {
+ copyLocation: false,
+ copyPropertyDeclaration: true,
+ copyPropertyName: true,
+ copyPropertyValue: false,
+ copySelector: false,
+ copyRule: true
+ }
+ },
+ {
+ desc: "Test Copy Selector",
+ node: ruleEditor.selectorText,
+ menuItemLabel: "styleinspector.contextmenu.copySelector",
+ expectedPattern: "html, body, #testid",
+ visible: {
+ copyLocation: false,
+ copyPropertyDeclaration: false,
+ copyPropertyName: false,
+ copyPropertyValue: false,
+ copySelector: true,
+ copyRule: true
+ }
+ },
+ {
+ desc: "Test Copy Location",
+ node: ruleEditor.source,
+ menuItemLabel: "styleinspector.contextmenu.copyLocation",
+ expectedPattern: "http://example.com/browser/devtools/client/" +
+ "inspector/rules/test/doc_copystyles.css",
+ visible: {
+ copyLocation: true,
+ copyPropertyDeclaration: false,
+ copyPropertyName: false,
+ copyPropertyValue: false,
+ copySelector: false,
+ copyRule: true
+ }
+ },
+ {
+ setup: function* () {
+ yield disableProperty(view, 0);
+ },
+ desc: "Test Copy Rule with Disabled Property",
+ node: ruleEditor.rule.textProps[2].editor.nameSpan,
+ menuItemLabel: "styleinspector.contextmenu.copyRule",
+ expectedPattern: "#testid {[\\r\\n]+" +
+ "\t\/\\* color: #F00; \\*\/[\\r\\n]+" +
+ "\tbackground-color: #00F;[\\r\\n]+" +
+ "\tfont-size: 12px;[\\r\\n]+" +
+ "\tborder-color: #00F !important;[\\r\\n]+" +
+ "\t--var: \"\\*/\";[\\r\\n]+" +
+ "}",
+ visible: {
+ copyLocation: false,
+ copyPropertyDeclaration: true,
+ copyPropertyName: true,
+ copyPropertyValue: false,
+ copySelector: false,
+ copyRule: true
+ }
+ },
+ {
+ setup: function* () {
+ yield disableProperty(view, 4);
+ },
+ desc: "Test Copy Rule with Disabled Property with Comment",
+ node: ruleEditor.rule.textProps[2].editor.nameSpan,
+ menuItemLabel: "styleinspector.contextmenu.copyRule",
+ expectedPattern: "#testid {[\\r\\n]+" +
+ "\t\/\\* color: #F00; \\*\/[\\r\\n]+" +
+ "\tbackground-color: #00F;[\\r\\n]+" +
+ "\tfont-size: 12px;[\\r\\n]+" +
+ "\tborder-color: #00F !important;[\\r\\n]+" +
+ "\t/\\* --var: \"\\*\\\\\/\"; \\*\/[\\r\\n]+" +
+ "}",
+ visible: {
+ copyLocation: false,
+ copyPropertyDeclaration: true,
+ copyPropertyName: true,
+ copyPropertyValue: false,
+ copySelector: false,
+ copyRule: true
+ }
+ },
+ {
+ desc: "Test Copy Property Declaration with Disabled Property",
+ node: ruleEditor.rule.textProps[0].editor.nameSpan,
+ menuItemLabel: "styleinspector.contextmenu.copyPropertyDeclaration",
+ expectedPattern: "\/\\* color: #F00; \\*\/",
+ visible: {
+ copyLocation: false,
+ copyPropertyDeclaration: true,
+ copyPropertyName: true,
+ copyPropertyValue: false,
+ copySelector: false,
+ copyRule: true
+ }
+ },
+ ];
+
+ for (let { setup, desc, node, menuItemLabel, expectedPattern, visible } of data) {
+ if (setup) {
+ yield setup();
+ }
+
+ info(desc);
+ yield checkCopyStyle(view, node, menuItemLabel, expectedPattern, visible);
+ }
+});
+
+function* checkCopyStyle(view, node, menuItemLabel, expectedPattern, visible) {
+ let allMenuItems = openStyleContextMenuAndGetAllItems(view, node);
+ let menuItem = allMenuItems.find(item =>
+ item.label === STYLE_INSPECTOR_L10N.getStr(menuItemLabel));
+ let menuitemCopy = allMenuItems.find(item => item.label ===
+ STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copy"));
+ let menuitemCopyLocation = allMenuItems.find(item => item.label ===
+ STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyLocation"));
+ let menuitemCopyPropertyDeclaration = allMenuItems.find(item => item.label ===
+ STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyPropertyDeclaration"));
+ let menuitemCopyPropertyName = allMenuItems.find(item => item.label ===
+ STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyPropertyName"));
+ let menuitemCopyPropertyValue = allMenuItems.find(item => item.label ===
+ STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyPropertyValue"));
+ let menuitemCopySelector = allMenuItems.find(item => item.label ===
+ STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copySelector"));
+ let menuitemCopyRule = allMenuItems.find(item => item.label ===
+ STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyRule"));
+
+ ok(menuitemCopy.disabled,
+ "Copy disabled is as expected: true");
+ ok(menuitemCopy.visible,
+ "Copy visible is as expected: true");
+
+ is(menuitemCopyLocation.visible,
+ visible.copyLocation,
+ "Copy Location visible attribute is as expected: " +
+ visible.copyLocation);
+
+ is(menuitemCopyPropertyDeclaration.visible,
+ visible.copyPropertyDeclaration,
+ "Copy Property Declaration visible attribute is as expected: " +
+ visible.copyPropertyDeclaration);
+
+ is(menuitemCopyPropertyName.visible,
+ visible.copyPropertyName,
+ "Copy Property Name visible attribute is as expected: " +
+ visible.copyPropertyName);
+
+ is(menuitemCopyPropertyValue.visible,
+ visible.copyPropertyValue,
+ "Copy Property Value visible attribute is as expected: " +
+ visible.copyPropertyValue);
+
+ is(menuitemCopySelector.visible,
+ visible.copySelector,
+ "Copy Selector visible attribute is as expected: " +
+ visible.copySelector);
+
+ is(menuitemCopyRule.visible,
+ visible.copyRule,
+ "Copy Rule visible attribute is as expected: " +
+ visible.copyRule);
+
+ try {
+ yield waitForClipboardPromise(() => menuItem.click(),
+ () => checkClipboardData(expectedPattern));
+ } catch (e) {
+ failedClipboard(expectedPattern);
+ }
+}
+
+function* disableProperty(view, index) {
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+ let textProp = ruleEditor.rule.textProps[index];
+ yield togglePropStatus(view, textProp);
+}
+
+function checkClipboardData(expectedPattern) {
+ let actual = SpecialPowers.getClipboardData("text/unicode");
+ let expectedRegExp = new RegExp(expectedPattern, "g");
+ return expectedRegExp.test(actual);
+}
+
+function failedClipboard(expectedPattern) {
+ // Format expected text for comparison
+ let terminator = osString == "WINNT" ? "\r\n" : "\n";
+ expectedPattern = expectedPattern.replace(/\[\\r\\n\][+*]/g, terminator);
+ expectedPattern = expectedPattern.replace(/\\\(/g, "(");
+ expectedPattern = expectedPattern.replace(/\\\)/g, ")");
+
+ let actual = SpecialPowers.getClipboardData("text/unicode");
+
+ // Trim the right hand side of our strings. This is because expectedPattern
+ // accounts for windows sometimes adding a newline to our copied data.
+ expectedPattern = expectedPattern.trimRight();
+ actual = actual.trimRight();
+
+ ok(false, "Clipboard text does not match expected " +
+ "results (escaped for accurate comparison):\n");
+ info("Actual: " + escape(actual));
+ info("Expected: " + escape(expectedPattern));
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_css-docs-tooltip_closes-on-escape.js b/devtools/client/inspector/rules/test/browser_rules_css-docs-tooltip_closes-on-escape.js
new file mode 100644
index 000000000..f386f45b4
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_css-docs-tooltip_closes-on-escape.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/ */
+
+/**
+ * Test that the CssDocs tooltip of the ruleview can be closed when pressing the Escape
+ * key.
+ */
+
+"use strict";
+
+const {setBaseCssDocsUrl} =
+ require("devtools/client/shared/widgets/MdnDocsWidget");
+
+const PROPERTYNAME = "color";
+
+const TEST_URI = `
+ <html>
+ <body>
+ <div style="color: red">
+ Test "Show MDN Docs" closes on escape
+ </div>
+ </body>
+ </html>
+`;
+
+/**
+ * Test that the tooltip is hidden when we press Escape
+ */
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("div", inspector);
+
+ setBaseCssDocsUrl(URL_ROOT);
+
+ info("Retrieve a valid anchor for the CssDocs tooltip");
+ let {nameSpan} = getRuleViewProperty(view, "element", PROPERTYNAME);
+
+ info("Showing the MDN docs tooltip");
+ let onShown = view.tooltips.cssDocs.tooltip.once("shown");
+ view.tooltips.cssDocs.show(nameSpan, PROPERTYNAME);
+ yield onShown;
+ ok(true, "The MDN docs tooltip was shown");
+
+ info("Simulate pressing the 'Escape' key");
+ let onHidden = view.tooltips.cssDocs.tooltip.once("hidden");
+ EventUtils.sendKey("escape");
+ yield onHidden;
+ ok(true, "The MDN docs tooltip was hidden on pressing 'escape'");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_cssom.js b/devtools/client/inspector/rules/test/browser_rules_cssom.js
new file mode 100644
index 000000000..d20e85192
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_cssom.js
@@ -0,0 +1,22 @@
+/* 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 to ensure that CSSOM doesn't make the rule view blow up.
+// https://bugzilla.mozilla.org/show_bug.cgi?id=1224121
+
+const TEST_URI = URL_ROOT + "doc_cssom.html";
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#target", inspector);
+
+ let elementStyle = view._elementStyle;
+ let rule = elementStyle.rules[1];
+
+ is(rule.textProps.length, 1, "rule should have one property");
+ is(rule.textProps[0].name, "color", "the property should be 'color'");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_cubicbezier-appears-on-swatch-click.js b/devtools/client/inspector/rules/test/browser_rules_cubicbezier-appears-on-swatch-click.js
new file mode 100644
index 000000000..18099894b
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_cubicbezier-appears-on-swatch-click.js
@@ -0,0 +1,70 @@
+/* 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";
+
+// Tests that cubic-bezier pickers appear when clicking on cubic-bezier
+// swatches.
+
+const TEST_URI = `
+ <style type="text/css">
+ div {
+ animation: move 3s linear;
+ transition: top 4s cubic-bezier(.1, 1.45, 1, -1.2);
+ }
+ .test {
+ animation-timing-function: ease-in-out;
+ transition-timing-function: ease-out;
+ }
+ </style>
+ <div class="test">Testing the cubic-bezier tooltip!</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("div", inspector);
+
+ let swatches = [];
+ swatches.push(
+ getRuleViewProperty(view, "div", "animation").valueSpan
+ .querySelector(".ruleview-bezierswatch")
+ );
+ swatches.push(
+ getRuleViewProperty(view, "div", "transition").valueSpan
+ .querySelector(".ruleview-bezierswatch")
+ );
+ swatches.push(
+ getRuleViewProperty(view, ".test", "animation-timing-function").valueSpan
+ .querySelector(".ruleview-bezierswatch")
+ );
+ swatches.push(
+ getRuleViewProperty(view, ".test", "transition-timing-function").valueSpan
+ .querySelector(".ruleview-bezierswatch")
+ );
+
+ for (let swatch of swatches) {
+ info("Testing that the cubic-bezier appears on cubicswatch click");
+ yield testAppears(view, swatch);
+ }
+});
+
+function* testAppears(view, swatch) {
+ ok(swatch, "The cubic-swatch exists");
+
+ let bezier = view.tooltips.cubicBezier;
+ ok(bezier, "The rule-view has the expected cubicBezier property");
+
+ let bezierPanel = bezier.tooltip.panel;
+ ok(bezierPanel, "The XUL panel for the cubic-bezier tooltip exists");
+
+ let onBezierWidgetReady = bezier.once("ready");
+ swatch.click();
+ yield onBezierWidgetReady;
+
+ ok(true, "The cubic-bezier tooltip was shown on click of the cibuc swatch");
+ ok(!inplaceEditor(swatch.parentNode),
+ "The inplace editor wasn't shown as a result of the cibuc swatch click");
+ yield hideTooltipAndWaitForRuleViewChanged(bezier, view);
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_cubicbezier-commit-on-ENTER.js b/devtools/client/inspector/rules/test/browser_rules_cubicbezier-commit-on-ENTER.js
new file mode 100644
index 000000000..5dc43d1c9
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_cubicbezier-commit-on-ENTER.js
@@ -0,0 +1,66 @@
+/* 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";
+
+// Tests that a curve change in the cubic-bezier tooltip is committed when ENTER
+// is pressed.
+
+const TEST_URI = `
+ <style type="text/css">
+ body {
+ transition: top 2s linear;
+ }
+ </style>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {view} = yield openRuleView();
+
+ info("Getting the bezier swatch element");
+ let swatch = getRuleViewProperty(view, "body", "transition").valueSpan
+ .querySelector(".ruleview-bezierswatch");
+
+ yield testPressingEnterCommitsChanges(swatch, view);
+});
+
+function* testPressingEnterCommitsChanges(swatch, ruleView) {
+ let bezierTooltip = ruleView.tooltips.cubicBezier;
+
+ info("Showing the tooltip");
+ let onBezierWidgetReady = bezierTooltip.once("ready");
+ swatch.click();
+ yield onBezierWidgetReady;
+
+ let widget = yield bezierTooltip.widget;
+ info("Simulating a change of curve in the widget");
+ widget.coordinates = [0.1, 2, 0.9, -1];
+ let expected = "cubic-bezier(0.1, 2, 0.9, -1)";
+
+ yield waitForSuccess(function* () {
+ let func = yield getComputedStyleProperty("body", null,
+ "transition-timing-function");
+ return func === expected;
+ }, "Waiting for the change to be previewed on the element");
+
+ ok(getRuleViewProperty(ruleView, "body", "transition").valueSpan.textContent
+ .indexOf("cubic-bezier(") !== -1,
+ "The text of the timing-function was updated");
+
+ info("Sending RETURN key within the tooltip document");
+ // Pressing RETURN ends up doing 2 rule-view updates, one for the preview and
+ // one for the commit when the tooltip closes.
+ let onRuleViewChanged = waitForNEvents(ruleView, "ruleview-changed", 2);
+ focusAndSendKey(widget.parent.ownerDocument.defaultView, "RETURN");
+ yield onRuleViewChanged;
+
+ let style = yield getComputedStyleProperty("body", null,
+ "transition-timing-function");
+ is(style, expected, "The element's timing-function was kept after RETURN");
+
+ let ruleViewStyle = getRuleViewProperty(ruleView, "body", "transition")
+ .valueSpan.textContent.indexOf("cubic-bezier(") !== -1;
+ ok(ruleViewStyle, "The text of the timing-function was kept after RETURN");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_cubicbezier-revert-on-ESC.js b/devtools/client/inspector/rules/test/browser_rules_cubicbezier-revert-on-ESC.js
new file mode 100644
index 000000000..826d8a5aa
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_cubicbezier-revert-on-ESC.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";
+
+// Tests that changes made to the cubic-bezier timing-function in the
+// cubic-bezier tooltip are reverted when ESC is pressed.
+
+const TEST_URI = `
+ <style type='text/css'>
+ body {
+ animation-timing-function: linear;
+ }
+ </style>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {view} = yield openRuleView();
+ yield testPressingEscapeRevertsChanges(view);
+ yield testPressingEscapeRevertsChangesAndDisables(view);
+});
+
+function* testPressingEscapeRevertsChanges(view) {
+ let {propEditor} = yield openCubicBezierAndChangeCoords(view, 1, 0,
+ [0.1, 2, 0.9, -1], {
+ selector: "body",
+ name: "animation-timing-function",
+ value: "cubic-bezier(0.1, 2, 0.9, -1)"
+ });
+
+ is(propEditor.valueSpan.textContent, "cubic-bezier(.1,2,.9,-1)",
+ "Got expected property value.");
+
+ yield escapeTooltip(view);
+
+ yield waitForComputedStyleProperty("body", null, "animation-timing-function",
+ "linear");
+ is(propEditor.valueSpan.textContent, "linear",
+ "Got expected property value.");
+}
+
+function* testPressingEscapeRevertsChangesAndDisables(view) {
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+ let textProp = ruleEditor.rule.textProps[0];
+ let propEditor = textProp.editor;
+
+ info("Disabling animation-timing-function property");
+ yield togglePropStatus(view, textProp);
+
+ ok(propEditor.element.classList.contains("ruleview-overridden"),
+ "property is overridden.");
+ is(propEditor.enable.style.visibility, "visible",
+ "property enable checkbox is visible.");
+ ok(!propEditor.enable.getAttribute("checked"),
+ "property enable checkbox is not checked.");
+ ok(!propEditor.prop.enabled,
+ "animation-timing-function property is disabled.");
+ let newValue = yield getRulePropertyValue("animation-timing-function");
+ is(newValue, "", "animation-timing-function should have been unset.");
+
+ yield openCubicBezierAndChangeCoords(view, 1, 0, [0.1, 2, 0.9, -1]);
+
+ yield escapeTooltip(view);
+
+ ok(propEditor.element.classList.contains("ruleview-overridden"),
+ "property is overridden.");
+ is(propEditor.enable.style.visibility, "visible",
+ "property enable checkbox is visible.");
+ ok(!propEditor.enable.getAttribute("checked"),
+ "property enable checkbox is not checked.");
+ ok(!propEditor.prop.enabled,
+ "animation-timing-function property is disabled.");
+ newValue = yield getRulePropertyValue("animation-timing-function");
+ is(newValue, "", "animation-timing-function should have been unset.");
+ is(propEditor.valueSpan.textContent, "linear",
+ "Got expected property value.");
+}
+
+function* getRulePropertyValue(name) {
+ let propValue = yield executeInContent("Test:GetRulePropertyValue", {
+ styleSheetIndex: 0,
+ ruleIndex: 0,
+ name: name
+ });
+ return propValue;
+}
+
+function* escapeTooltip(view) {
+ info("Pressing ESCAPE to close the tooltip");
+
+ let bezierTooltip = view.tooltips.cubicBezier;
+ let widget = yield bezierTooltip.widget;
+ let onHidden = bezierTooltip.tooltip.once("hidden");
+ let onModifications = view.once("ruleview-changed");
+ focusAndSendKey(widget.parent.ownerDocument.defaultView, "ESCAPE");
+ yield onHidden;
+ yield onModifications;
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_custom.js b/devtools/client/inspector/rules/test/browser_rules_custom.js
new file mode 100644
index 000000000..7c941af6f
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_custom.js
@@ -0,0 +1,72 @@
+/* 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 TEST_URI = URL_ROOT + "doc_custom.html";
+
+// Tests the display of custom declarations in the rule-view.
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let {inspector, view} = yield openRuleView();
+
+ yield simpleCustomOverride(inspector, view);
+ yield importantCustomOverride(inspector, view);
+ yield disableCustomOverride(inspector, view);
+});
+
+function* simpleCustomOverride(inspector, view) {
+ yield selectNode("#testidSimple", inspector);
+
+ let idRule = getRuleViewRuleEditor(view, 1).rule;
+ let idRuleProp = idRule.textProps[0];
+
+ is(idRuleProp.name, "--background-color",
+ "First ID prop should be --background-color");
+ ok(!idRuleProp.overridden, "ID prop should not be overridden.");
+
+ let classRule = getRuleViewRuleEditor(view, 2).rule;
+ let classRuleProp = classRule.textProps[0];
+
+ is(classRuleProp.name, "--background-color",
+ "First class prop should be --background-color");
+ ok(classRuleProp.overridden, "Class property should be overridden.");
+
+ // Override --background-color by changing the element style.
+ let elementProp = yield addProperty(view, 0, "--background-color", "purple");
+
+ is(classRuleProp.name, "--background-color",
+ "First element prop should now be --background-color");
+ ok(!elementProp.overridden,
+ "Element style property should not be overridden");
+ ok(idRuleProp.overridden, "ID property should be overridden");
+ ok(classRuleProp.overridden, "Class property should be overridden");
+}
+
+function* importantCustomOverride(inspector, view) {
+ yield selectNode("#testidImportant", inspector);
+
+ let idRule = getRuleViewRuleEditor(view, 1).rule;
+ let idRuleProp = idRule.textProps[0];
+ ok(idRuleProp.overridden, "Not-important rule should be overridden.");
+
+ let classRule = getRuleViewRuleEditor(view, 2).rule;
+ let classRuleProp = classRule.textProps[0];
+ ok(!classRuleProp.overridden, "Important rule should not be overridden.");
+}
+
+function* disableCustomOverride(inspector, view) {
+ yield selectNode("#testidDisable", inspector);
+
+ let idRule = getRuleViewRuleEditor(view, 1).rule;
+ let idRuleProp = idRule.textProps[0];
+
+ yield togglePropStatus(view, idRuleProp);
+
+ let classRule = getRuleViewRuleEditor(view, 2).rule;
+ let classRuleProp = classRule.textProps[0];
+ ok(!classRuleProp.overridden,
+ "Class prop should not be overridden after id prop was disabled.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_cycle-angle.js b/devtools/client/inspector/rules/test/browser_rules_cycle-angle.js
new file mode 100644
index 000000000..fa135f937
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_cycle-angle.js
@@ -0,0 +1,93 @@
+/* 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 cycling angle units in the rule view.
+
+const TEST_URI = `
+ <style type="text/css">
+ body {
+ image-orientation: 1turn;
+ }
+ div {
+ image-orientation: 180deg;
+ }
+ </style>
+ <body><div>Test</div>cycling angle units in the rule view!</body>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ let container = getRuleViewProperty(
+ view, "body", "image-orientation").valueSpan;
+ yield checkAngleCycling(container, view);
+ yield checkAngleCyclingPersist(inspector, view);
+});
+
+function* checkAngleCycling(container, view) {
+ let valueNode = container.querySelector(".ruleview-angle");
+ let win = view.styleWindow;
+
+ // turn
+ is(valueNode.textContent, "1turn", "Angle displayed as a turn value.");
+
+ let tests = [{
+ value: "360deg",
+ comment: "Angle displayed as a degree value."
+ }, {
+ value: `${Math.round(Math.PI * 2 * 10000) / 10000}rad`,
+ comment: "Angle displayed as a radian value."
+ }, {
+ value: "400grad",
+ comment: "Angle displayed as a gradian value."
+ }, {
+ value: "1turn",
+ comment: "Angle displayed as a turn value again."
+ }];
+
+ for (let test of tests) {
+ yield checkSwatchShiftClick(container, win, test.value, test.comment);
+ }
+}
+
+function* checkAngleCyclingPersist(inspector, view) {
+ yield selectNode("div", inspector);
+ let container = getRuleViewProperty(
+ view, "div", "image-orientation").valueSpan;
+ let valueNode = container.querySelector(".ruleview-angle");
+ let win = view.styleWindow;
+
+ is(valueNode.textContent, "180deg", "Angle displayed as a degree value.");
+
+ yield checkSwatchShiftClick(container, win,
+ `${Math.round(Math.PI * 10000) / 10000}rad`,
+ "Angle displayed as a radian value.");
+
+ // Select the body and reselect the div to see
+ // if the new angle unit persisted
+ yield selectNode("body", inspector);
+ yield selectNode("div", inspector);
+
+ // We have to query for the container and the swatch because
+ // they've been re-generated
+ container = getRuleViewProperty(view, "div", "image-orientation").valueSpan;
+ valueNode = container.querySelector(".ruleview-angle");
+ is(valueNode.textContent, `${Math.round(Math.PI * 10000) / 10000}rad`,
+ "Angle still displayed as a radian value.");
+}
+
+function* checkSwatchShiftClick(container, win, expectedValue, comment) {
+ let swatch = container.querySelector(".ruleview-angleswatch");
+ let valueNode = container.querySelector(".ruleview-angle");
+
+ let onUnitChange = swatch.once("unit-change");
+ EventUtils.synthesizeMouseAtCenter(swatch, {
+ type: "mousedown",
+ shiftKey: true
+ }, win);
+ yield onUnitChange;
+ is(valueNode.textContent, expectedValue, comment);
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_cycle-color.js b/devtools/client/inspector/rules/test/browser_rules_cycle-color.js
new file mode 100644
index 000000000..e31ffa133
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_cycle-color.js
@@ -0,0 +1,120 @@
+/* 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 cycling color types in the rule view.
+
+const TEST_URI = `
+ <style type="text/css">
+ body {
+ color: #f00;
+ }
+ span {
+ color: blue;
+ border-color: #ff000080;
+ }
+ </style>
+ <body><span>Test</span> cycling color types in the rule view!</body>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ let container = getRuleViewProperty(view, "body", "color").valueSpan;
+ yield checkColorCycling(container, view);
+ yield checkAlphaColorCycling(inspector, view);
+ yield checkColorCyclingPersist(inspector, view);
+});
+
+function* checkColorCycling(container, view) {
+ let valueNode = container.querySelector(".ruleview-color");
+ let win = view.styleWindow;
+
+ // Hex
+ is(valueNode.textContent, "#f00", "Color displayed as a hex value.");
+
+ let tests = [{
+ value: "hsl(0, 100%, 50%)",
+ comment: "Color displayed as an HSL value."
+ }, {
+ value: "rgb(255, 0, 0)",
+ comment: "Color displayed as an RGB value."
+ }, {
+ value: "red",
+ comment: "Color displayed as a color name."
+ }, {
+ value: "#f00",
+ comment: "Color displayed as an authored value."
+ }, {
+ value: "hsl(0, 100%, 50%)",
+ comment: "Color displayed as an HSL value again."
+ }];
+
+ for (let test of tests) {
+ yield checkSwatchShiftClick(container, win, test.value, test.comment);
+ }
+}
+
+function* checkAlphaColorCycling(inspector, view) {
+ yield selectNode("span", inspector);
+ let container = getRuleViewProperty(view, "span", "border-color").valueSpan;
+ let valueNode = container.querySelector(".ruleview-color");
+ let win = view.styleWindow;
+
+ is(valueNode.textContent, "#ff000080",
+ "Color displayed as an alpha hex value.");
+
+ let tests = [{
+ value: "hsla(0, 100%, 50%, 0.5)",
+ comment: "Color displayed as an HSLa value."
+ }, {
+ value: "rgba(255, 0, 0, 0.5)",
+ comment: "Color displayed as an RGBa value."
+ }, {
+ value: "#ff000080",
+ comment: "Color displayed as an alpha hex value again."
+ }];
+
+ for (let test of tests) {
+ yield checkSwatchShiftClick(container, win, test.value, test.comment);
+ }
+}
+
+function* checkColorCyclingPersist(inspector, view) {
+ yield selectNode("span", inspector);
+ let container = getRuleViewProperty(view, "span", "color").valueSpan;
+ let valueNode = container.querySelector(".ruleview-color");
+ let win = view.styleWindow;
+
+ is(valueNode.textContent, "blue", "Color displayed as a color name.");
+
+ yield checkSwatchShiftClick(container, win, "#00f",
+ "Color displayed as a hex value.");
+
+ // Select the body and reselect the span to see
+ // if the new color unit persisted
+ yield selectNode("body", inspector);
+ yield selectNode("span", inspector);
+
+ // We have to query for the container and the swatch because
+ // they've been re-generated
+ container = getRuleViewProperty(view, "span", "color").valueSpan;
+ valueNode = container.querySelector(".ruleview-color");
+ is(valueNode.textContent, "#00f",
+ "Color is still displayed as a hex value.");
+}
+
+function* checkSwatchShiftClick(container, win, expectedValue, comment) {
+ let swatch = container.querySelector(".ruleview-colorswatch");
+ let valueNode = container.querySelector(".ruleview-color");
+
+ let onUnitChange = swatch.once("unit-change");
+ EventUtils.synthesizeMouseAtCenter(swatch, {
+ type: "mousedown",
+ shiftKey: true
+ }, win);
+ yield onUnitChange;
+ is(valueNode.textContent, expectedValue, comment);
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-display-grid-property.js b/devtools/client/inspector/rules/test/browser_rules_edit-display-grid-property.js
new file mode 100644
index 000000000..18522b527
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-display-grid-property.js
@@ -0,0 +1,49 @@
+/* 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 toggling the grid highlighter in the rule view and modifying the 'display: grid'
+// declaration.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #grid {
+ display: grid;
+ }
+ </style>
+ <div id="grid">
+ <div id="cell1">cell1</div>
+ <div id="cell2">cell2</div>
+ </div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ let highlighters = view.highlighters;
+
+ yield selectNode("#grid", inspector);
+ let container = getRuleViewProperty(view, "#grid", "display").valueSpan;
+ let gridToggle = container.querySelector(".ruleview-grid");
+
+ info("Toggling ON the CSS grid highlighter from the rule-view.");
+ let onHighlighterShown = highlighters.once("highlighter-shown");
+ gridToggle.click();
+ yield onHighlighterShown;
+
+ info("Edit the 'grid' property value to 'block'.");
+ let editor = yield focusEditableField(view, container);
+ let onHighlighterHidden = highlighters.once("highlighter-hidden");
+ let onDone = view.once("ruleview-changed");
+ editor.input.value = "block;";
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
+ yield onHighlighterHidden;
+ yield onDone;
+
+ info("Check the grid highlighter and grid toggle button are hidden.");
+ gridToggle = container.querySelector(".ruleview-grid");
+ ok(!gridToggle, "Grid highlighter toggle is not visible.");
+ ok(!highlighters.gridHighlighterShown, "No CSS grid highlighter is shown.");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property-cancel.js b/devtools/client/inspector/rules/test/browser_rules_edit-property-cancel.js
new file mode 100644
index 000000000..af1a6fbc0
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property-cancel.js
@@ -0,0 +1,46 @@
+/* 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";
+
+// Tests editing a property name or value and escaping will revert the
+// changes and restore the original value.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #testid {
+ background-color: #00F;
+ }
+ </style>
+ <div id='testid'>Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+ let propEditor = ruleEditor.rule.textProps[0].editor;
+
+ yield focusEditableField(view, propEditor.nameSpan);
+ yield sendKeysAndWaitForFocus(view, ruleEditor.element,
+ ["DELETE", "ESCAPE"]);
+
+ is(propEditor.nameSpan.textContent, "background-color",
+ "'background-color' property name is correctly set.");
+ is((yield getComputedStyleProperty("#testid", null, "background-color")),
+ "rgb(0, 0, 255)", "#00F background color is set.");
+
+ yield focusEditableField(view, propEditor.valueSpan);
+ let onValueDeleted = view.once("ruleview-changed");
+ yield sendKeysAndWaitForFocus(view, ruleEditor.element,
+ ["DELETE", "ESCAPE"]);
+ yield onValueDeleted;
+
+ is(propEditor.valueSpan.textContent, "#00F",
+ "'#00F' property value is correctly set.");
+ is((yield getComputedStyleProperty("#testid", null, "background-color")),
+ "rgb(0, 0, 255)", "#00F background color is set.");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property-click.js b/devtools/client/inspector/rules/test/browser_rules_edit-property-click.js
new file mode 100644
index 000000000..08a5ee786
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property-click.js
@@ -0,0 +1,61 @@
+/* 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";
+
+// Tests that the property name and value editors can be triggered when
+// clicking on the property-name, the property-value, the colon or semicolon.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #testid {
+ margin: 0;
+ }
+ </style>
+ <div id='testid'>Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+ yield testEditPropertyAndCancel(inspector, view);
+});
+
+function* testEditPropertyAndCancel(inspector, view) {
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+ let propEditor = ruleEditor.rule.textProps[0].editor;
+
+ info("Test editor is created when clicking on property name");
+ yield focusEditableField(view, propEditor.nameSpan);
+ ok(propEditor.nameSpan.inplaceEditor, "Editor created for property name");
+ yield sendKeysAndWaitForFocus(view, ruleEditor.element, ["ESCAPE"]);
+
+ info("Test editor is created when clicking on ':' next to property name");
+ let nameRect = propEditor.nameSpan.getBoundingClientRect();
+ yield focusEditableField(view, propEditor.nameSpan, nameRect.width + 1);
+ ok(propEditor.nameSpan.inplaceEditor, "Editor created for property name");
+ yield sendKeysAndWaitForFocus(view, ruleEditor.element, ["ESCAPE"]);
+
+ info("Test editor is created when clicking on property value");
+ yield focusEditableField(view, propEditor.valueSpan);
+ ok(propEditor.valueSpan.inplaceEditor, "Editor created for property value");
+ // When cancelling a value edition, the text-property-editor will trigger
+ // a modification to make sure the property is back to its original value
+ // => need to wait on "ruleview-changed" to avoid unhandled promises
+ let onRuleviewChanged = view.once("ruleview-changed");
+ yield sendKeysAndWaitForFocus(view, ruleEditor.element, ["ESCAPE"]);
+ yield onRuleviewChanged;
+
+ info("Test editor is created when clicking on ';' next to property value");
+ let valueRect = propEditor.valueSpan.getBoundingClientRect();
+ yield focusEditableField(view, propEditor.valueSpan, valueRect.width + 1);
+ ok(propEditor.valueSpan.inplaceEditor, "Editor created for property value");
+ // When cancelling a value edition, the text-property-editor will trigger
+ // a modification to make sure the property is back to its original value
+ // => need to wait on "ruleview-changed" to avoid unhandled promises
+ onRuleviewChanged = view.once("ruleview-changed");
+ yield sendKeysAndWaitForFocus(view, ruleEditor.element, ["ESCAPE"]);
+ yield onRuleviewChanged;
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property-commit.js b/devtools/client/inspector/rules/test/browser_rules_edit-property-commit.js
new file mode 100644
index 000000000..8e16601c7
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property-commit.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";
+
+// Test original value is correctly displayed when ESCaping out of the
+// inplace editor in the style inspector.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #testid {
+ color: #00F;
+ }
+ </style>
+ <div id='testid'>Styled Node</div>
+`;
+
+// Test data format
+// {
+// value: what char sequence to type,
+// commitKey: what key to type to "commit" the change,
+// modifiers: commitKey modifiers,
+// expected: what value is expected as a result
+// }
+const testData = [
+ {
+ value: "red",
+ commitKey: "VK_ESCAPE",
+ modifiers: {},
+ expected: "#00F"
+ },
+ {
+ value: "red",
+ commitKey: "VK_RETURN",
+ modifiers: {},
+ expected: "red"
+ },
+ {
+ value: "invalid",
+ commitKey: "VK_RETURN",
+ modifiers: {},
+ expected: "invalid"
+ },
+ {
+ value: "blue",
+ commitKey: "VK_TAB", modifiers: {shiftKey: true},
+ expected: "blue"
+ }
+];
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ for (let data of testData) {
+ yield runTestData(view, data);
+ }
+});
+
+function* runTestData(view, {value, commitKey, modifiers, expected}) {
+ let idRuleEditor = getRuleViewRuleEditor(view, 1);
+ let propEditor = idRuleEditor.rule.textProps[0].editor;
+
+ info("Focusing the inplace editor field");
+
+ let editor = yield focusEditableField(view, propEditor.valueSpan);
+ is(inplaceEditor(propEditor.valueSpan), editor,
+ "Focused editor should be the value span.");
+
+ info("Entering test data " + value);
+ let onRuleViewChanged = view.once("ruleview-changed");
+ EventUtils.sendString(value, view.styleWindow);
+ view.throttle.flush();
+ yield onRuleViewChanged;
+
+ info("Entering the commit key " + commitKey + " " + modifiers);
+ onRuleViewChanged = view.once("ruleview-changed");
+ let onBlur = once(editor.input, "blur");
+ EventUtils.synthesizeKey(commitKey, modifiers);
+ yield onBlur;
+ yield onRuleViewChanged;
+
+ if (commitKey === "VK_ESCAPE") {
+ is(propEditor.valueSpan.textContent, expected,
+ "Value is as expected: " + expected);
+ } else {
+ is(propEditor.valueSpan.textContent, expected,
+ "Value is as expected: " + expected);
+ }
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property-computed.js b/devtools/client/inspector/rules/test/browser_rules_edit-property-computed.js
new file mode 100644
index 000000000..ee0a1fa74
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property-computed.js
@@ -0,0 +1,89 @@
+/* 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 computed values of a style (the shorthand expansion) are
+// properly updated after the style is changed.
+
+const TEST_URI = `
+ <style type="text/css">
+ #testid {
+ padding: 10px;
+ }
+ </style>
+ <div id="testid">Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+ yield editAndCheck(view);
+});
+
+function* editAndCheck(view) {
+ let idRuleEditor = getRuleViewRuleEditor(view, 1);
+ let prop = idRuleEditor.rule.textProps[0];
+ let propEditor = prop.editor;
+ let newPaddingValue = "20px";
+
+ info("Focusing the inplace editor field");
+ let editor = yield focusEditableField(view, propEditor.valueSpan);
+ is(inplaceEditor(propEditor.valueSpan), editor,
+ "Focused editor should be the value span.");
+
+ let onPropertyChange = waitForComputedStyleProperty("#testid", null,
+ "padding-top", newPaddingValue);
+ let onRefreshAfterPreview = once(view, "ruleview-changed");
+
+ info("Entering a new value");
+ EventUtils.sendString(newPaddingValue, view.styleWindow);
+
+ info("Waiting for the throttled previewValue to apply the " +
+ "changes to document");
+
+ view.throttle.flush();
+ yield onPropertyChange;
+
+ info("Waiting for ruleview-refreshed after previewValue was applied.");
+ yield onRefreshAfterPreview;
+
+ let onBlur = once(editor.input, "blur");
+
+ info("Entering the commit key and finishing edit");
+ EventUtils.synthesizeKey("VK_RETURN", {});
+
+ info("Waiting for blur on the field");
+ yield onBlur;
+
+ info("Waiting for the style changes to be applied");
+ yield once(view, "ruleview-changed");
+
+ let computed = prop.computed;
+ let propNames = [
+ "padding-top",
+ "padding-right",
+ "padding-bottom",
+ "padding-left"
+ ];
+
+ is(computed.length, propNames.length, "There should be 4 computed values");
+ propNames.forEach((propName, i) => {
+ is(computed[i].name, propName,
+ "Computed property #" + i + " has name " + propName);
+ is(computed[i].value, newPaddingValue,
+ "Computed value of " + propName + " is as expected");
+ });
+
+ propEditor.expander.click();
+ let computedDom = propEditor.computed;
+ is(computedDom.children.length, propNames.length,
+ "There should be 4 nodes in the DOM");
+ propNames.forEach((propName, i) => {
+ is(computedDom.getElementsByClassName("ruleview-propertyvalue")[i]
+ .textContent, newPaddingValue,
+ "Computed value of " + propName + " in DOM is as expected");
+ });
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property-increments.js b/devtools/client/inspector/rules/test/browser_rules_edit-property-increments.js
new file mode 100644
index 000000000..ca63cedcc
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property-increments.js
@@ -0,0 +1,280 @@
+/* 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 increasing/decreasing values in rule view using
+// arrow keys works correctly.
+
+// Bug 1275446 - This test happen to hit the default timeout on linux32
+requestLongerTimeout(2);
+
+const TEST_URI = `
+ <style>
+ #test {
+ margin-top: 0px;
+ padding-top: 0px;
+ color: #000000;
+ background-color: #000000;
+ background: none;
+ transition: initial;
+ z-index: 0;
+ }
+ </style>
+ <div id="test"></div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#test", inspector);
+
+ yield testMarginIncrements(view);
+ yield testVariousUnitIncrements(view);
+ yield testHexIncrements(view);
+ yield testAlphaHexIncrements(view);
+ yield testRgbIncrements(view);
+ yield testShorthandIncrements(view);
+ yield testOddCases(view);
+ yield testZeroValueIncrements(view);
+});
+
+function* testMarginIncrements(view) {
+ info("Testing keyboard increments on the margin property");
+
+ let idRuleEditor = getRuleViewRuleEditor(view, 1);
+ let marginPropEditor = idRuleEditor.rule.textProps[0].editor;
+
+ yield runIncrementTest(marginPropEditor, view, {
+ 1: {alt: true, start: "0px", end: "0.1px", selectAll: true},
+ 2: {start: "0px", end: "1px", selectAll: true},
+ 3: {shift: true, start: "0px", end: "10px", selectAll: true},
+ 4: {down: true, alt: true, start: "0.1px", end: "0px", selectAll: true},
+ 5: {down: true, start: "0px", end: "-1px", selectAll: true},
+ 6: {down: true, shift: true, start: "0px", end: "-10px", selectAll: true},
+ 7: {pageUp: true, shift: true, start: "0px", end: "100px", selectAll: true},
+ 8: {pageDown: true, shift: true, start: "0px", end: "-100px",
+ selectAll: true},
+ 9: {start: "0", end: "1px", selectAll: true},
+ 10: {down: true, start: "0", end: "-1px", selectAll: true},
+ });
+}
+
+function* testVariousUnitIncrements(view) {
+ info("Testing keyboard increments on values with various units");
+
+ let idRuleEditor = getRuleViewRuleEditor(view, 1);
+ let paddingPropEditor = idRuleEditor.rule.textProps[1].editor;
+
+ yield runIncrementTest(paddingPropEditor, view, {
+ 1: {start: "0px", end: "1px", selectAll: true},
+ 2: {start: "0pt", end: "1pt", selectAll: true},
+ 3: {start: "0pc", end: "1pc", selectAll: true},
+ 4: {start: "0em", end: "1em", selectAll: true},
+ 5: {start: "0%", end: "1%", selectAll: true},
+ 6: {start: "0in", end: "1in", selectAll: true},
+ 7: {start: "0cm", end: "1cm", selectAll: true},
+ 8: {start: "0mm", end: "1mm", selectAll: true},
+ 9: {start: "0ex", end: "1ex", selectAll: true},
+ 10: {start: "0", end: "1px", selectAll: true},
+ 11: {down: true, start: "0", end: "-1px", selectAll: true},
+ });
+}
+
+function* testHexIncrements(view) {
+ info("Testing keyboard increments with hex colors");
+
+ let idRuleEditor = getRuleViewRuleEditor(view, 1);
+ let hexColorPropEditor = idRuleEditor.rule.textProps[2].editor;
+
+ yield runIncrementTest(hexColorPropEditor, view, {
+ 1: {start: "#CCCCCC", end: "#CDCDCD", selectAll: true},
+ 2: {shift: true, start: "#CCCCCC", end: "#DCDCDC", selectAll: true},
+ 3: {start: "#CCCCCC", end: "#CDCCCC", selection: [1, 3]},
+ 4: {shift: true, start: "#CCCCCC", end: "#DCCCCC", selection: [1, 3]},
+ 5: {start: "#FFFFFF", end: "#FFFFFF", selectAll: true},
+ 6: {down: true, shift: true, start: "#000000", end: "#000000",
+ selectAll: true}
+ });
+}
+
+function* testAlphaHexIncrements(view) {
+ info("Testing keyboard increments with alpha hex colors");
+
+ let idRuleEditor = getRuleViewRuleEditor(view, 1);
+ let hexColorPropEditor = idRuleEditor.rule.textProps[2].editor;
+
+ yield runIncrementTest(hexColorPropEditor, view, {
+ 1: {start: "#CCCCCCAA", end: "#CDCDCDAB", selectAll: true},
+ 2: {shift: true, start: "#CCCCCCAA", end: "#DCDCDCBA", selectAll: true},
+ 3: {start: "#CCCCCCAA", end: "#CDCCCCAA", selection: [1, 3]},
+ 4: {shift: true, start: "#CCCCCCAA", end: "#DCCCCCAA", selection: [1, 3]},
+ 5: {start: "#FFFFFFFF", end: "#FFFFFFFF", selectAll: true},
+ 6: {down: true, shift: true, start: "#00000000", end: "#00000000",
+ selectAll: true}
+ });
+}
+
+function* testRgbIncrements(view) {
+ info("Testing keyboard increments with rgb colors");
+
+ let idRuleEditor = getRuleViewRuleEditor(view, 1);
+ let rgbColorPropEditor = idRuleEditor.rule.textProps[3].editor;
+
+ yield runIncrementTest(rgbColorPropEditor, view, {
+ 1: {start: "rgb(0,0,0)", end: "rgb(0,1,0)", selection: [6, 7]},
+ 2: {shift: true, start: "rgb(0,0,0)", end: "rgb(0,10,0)",
+ selection: [6, 7]},
+ 3: {start: "rgb(0,255,0)", end: "rgb(0,255,0)", selection: [6, 9]},
+ 4: {shift: true, start: "rgb(0,250,0)", end: "rgb(0,255,0)",
+ selection: [6, 9]},
+ 5: {down: true, start: "rgb(0,0,0)", end: "rgb(0,0,0)", selection: [6, 7]},
+ 6: {down: true, shift: true, start: "rgb(0,5,0)", end: "rgb(0,0,0)",
+ selection: [6, 7]}
+ });
+}
+
+function* testShorthandIncrements(view) {
+ info("Testing keyboard increments within shorthand values");
+
+ let idRuleEditor = getRuleViewRuleEditor(view, 1);
+ let paddingPropEditor = idRuleEditor.rule.textProps[1].editor;
+
+ yield runIncrementTest(paddingPropEditor, view, {
+ 1: {start: "0px 0px 0px 0px", end: "0px 1px 0px 0px", selection: [4, 7]},
+ 2: {shift: true, start: "0px 0px 0px 0px", end: "0px 10px 0px 0px",
+ selection: [4, 7]},
+ 3: {start: "0px 0px 0px 0px", end: "1px 0px 0px 0px", selectAll: true},
+ 4: {shift: true, start: "0px 0px 0px 0px", end: "10px 0px 0px 0px",
+ selectAll: true},
+ 5: {down: true, start: "0px 0px 0px 0px", end: "0px 0px -1px 0px",
+ selection: [8, 11]},
+ 6: {down: true, shift: true, start: "0px 0px 0px 0px",
+ end: "-10px 0px 0px 0px", selectAll: true},
+ 7: {up: true, start: "0.1em .1em 0em 0em", end: "0.1em 1.1em 0em 0em",
+ selection: [6, 9]},
+ 8: {up: true, alt: true, start: "0.1em .9em 0em 0em",
+ end: "0.1em 1em 0em 0em", selection: [6, 9]},
+ 9: {up: true, shift: true, start: "0.2em .2em 0em 0em",
+ end: "0.2em 10.2em 0em 0em", selection: [6, 9]}
+ });
+}
+
+function* testOddCases(view) {
+ info("Testing some more odd cases");
+
+ let idRuleEditor = getRuleViewRuleEditor(view, 1);
+ let marginPropEditor = idRuleEditor.rule.textProps[0].editor;
+
+ yield runIncrementTest(marginPropEditor, view, {
+ 1: {start: "98.7%", end: "99.7%", selection: [3, 3]},
+ 2: {alt: true, start: "98.7%", end: "98.8%", selection: [3, 3]},
+ 3: {start: "0", end: "1px"},
+ 4: {down: true, start: "0", end: "-1px"},
+ 5: {start: "'a=-1'", end: "'a=0'", selection: [4, 4]},
+ 6: {start: "0 -1px", end: "0 0px", selection: [2, 2]},
+ 7: {start: "url(-1)", end: "url(-1)", selection: [4, 4]},
+ 8: {start: "url('test1.1.png')", end: "url('test1.2.png')",
+ selection: [11, 11]},
+ 9: {start: "url('test1.png')", end: "url('test2.png')", selection: [9, 9]},
+ 10: {shift: true, start: "url('test1.1.png')", end: "url('test11.1.png')",
+ selection: [9, 9]},
+ 11: {down: true, start: "url('test-1.png')", end: "url('test-2.png')",
+ selection: [9, 11]},
+ 12: {start: "url('test1.1.png')", end: "url('test1.2.png')",
+ selection: [11, 12]},
+ 13: {down: true, alt: true, start: "url('test-0.png')",
+ end: "url('test--0.1.png')", selection: [10, 11]},
+ 14: {alt: true, start: "url('test--0.1.png')", end: "url('test-0.png')",
+ selection: [10, 14]}
+ });
+}
+
+function* testZeroValueIncrements(view) {
+ info("Testing a valid unit is added when incrementing from 0");
+
+ let idRuleEditor = getRuleViewRuleEditor(view, 1);
+ let backgroundPropEditor = idRuleEditor.rule.textProps[4].editor;
+ yield runIncrementTest(backgroundPropEditor, view, {
+ 1: { start: "url(test-0.png) no-repeat 0 0",
+ end: "url(test-0.png) no-repeat 1px 0", selection: [26, 26] },
+ 2: { start: "url(test-0.png) no-repeat 0 0",
+ end: "url(test-0.png) no-repeat 0 1px", selection: [28, 28] },
+ 3: { start: "url(test-0.png) no-repeat center/0",
+ end: "url(test-0.png) no-repeat center/1px", selection: [34, 34] },
+ 4: { start: "url(test-0.png) no-repeat 0 0",
+ end: "url(test-1.png) no-repeat 0 0", selection: [10, 10] },
+ 5: { start: "linear-gradient(0, red 0, blue 0)",
+ end: "linear-gradient(1deg, red 0, blue 0)", selection: [17, 17] },
+ 6: { start: "linear-gradient(1deg, red 0, blue 0)",
+ end: "linear-gradient(1deg, red 1px, blue 0)", selection: [27, 27] },
+ 7: { start: "linear-gradient(1deg, red 0, blue 0)",
+ end: "linear-gradient(1deg, red 0, blue 1px)", selection: [35, 35] },
+ });
+
+ let transitionPropEditor = idRuleEditor.rule.textProps[5].editor;
+ yield runIncrementTest(transitionPropEditor, view, {
+ 1: { start: "all 0 ease-out", end: "all 1s ease-out", selection: [5, 5] },
+ 2: { start: "margin 4s, color 0",
+ end: "margin 4s, color 1s", selection: [18, 18] },
+ });
+
+ let zIndexPropEditor = idRuleEditor.rule.textProps[6].editor;
+ yield runIncrementTest(zIndexPropEditor, view, {
+ 1: {start: "0", end: "1", selection: [1, 1]},
+ });
+}
+
+function* runIncrementTest(propertyEditor, view, tests) {
+ let editor = yield focusEditableField(view, propertyEditor.valueSpan);
+
+ for (let test in tests) {
+ yield testIncrement(editor, tests[test], view, propertyEditor);
+ }
+
+ // Blur the field to put back the UI in its initial state (and avoid pending
+ // requests when the test ends).
+ let onRuleViewChanged = view.once("ruleview-changed");
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, view.styleWindow);
+ view.throttle.flush();
+ yield onRuleViewChanged;
+}
+
+function* testIncrement(editor, options, view) {
+ editor.input.value = options.start;
+ let input = editor.input;
+
+ if (options.selectAll) {
+ input.select();
+ } else if (options.selection) {
+ input.setSelectionRange(options.selection[0], options.selection[1]);
+ }
+
+ is(input.value, options.start, "Value initialized at " + options.start);
+
+ let onRuleViewChanged = view.once("ruleview-changed");
+ let onKeyUp = once(input, "keyup");
+
+ let key;
+ key = options.down ? "VK_DOWN" : "VK_UP";
+ if (options.pageDown) {
+ key = "VK_PAGE_DOWN";
+ } else if (options.pageUp) {
+ key = "VK_PAGE_UP";
+ }
+
+ EventUtils.synthesizeKey(key, {altKey: options.alt, shiftKey: options.shift},
+ view.styleWindow);
+
+ yield onKeyUp;
+
+ // Only expect a change if the value actually changed!
+ if (options.start !== options.end) {
+ view.throttle.flush();
+ yield onRuleViewChanged;
+ }
+
+ is(input.value, options.end, "Value changed to " + options.end);
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property-order.js b/devtools/client/inspector/rules/test/browser_rules_edit-property-order.js
new file mode 100644
index 000000000..b4a86c194
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property-order.js
@@ -0,0 +1,89 @@
+/* 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";
+
+// Checking properties orders and overrides in the rule-view.
+
+const TEST_URI = "<style>#testid {}</style><div id='testid'>Styled Node</div>";
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ let elementStyle = view._elementStyle;
+ let elementRule = elementStyle.rules[1];
+
+ info("Checking rules insertion order and checking the applied style");
+ let firstProp = yield addProperty(view, 1, "background-color", "green");
+ let secondProp = yield addProperty(view, 1, "background-color", "blue");
+
+ is(elementRule.textProps[0], firstProp,
+ "Rules should be in addition order.");
+ is(elementRule.textProps[1], secondProp,
+ "Rules should be in addition order.");
+
+ // rgb(0, 0, 255) = blue
+ is((yield getValue("#testid", "background-color")), "rgb(0, 0, 255)",
+ "Second property should have been used.");
+
+ info("Removing the second property and checking the applied style again");
+ yield removeProperty(view, secondProp);
+ // rgb(0, 128, 0) = green
+ is((yield getValue("#testid", "background-color")), "rgb(0, 128, 0)",
+ "After deleting second property, first should be used.");
+
+ info("Creating a new second property and checking that the insertion order " +
+ "is still the same");
+
+ secondProp = yield addProperty(view, 1, "background-color", "blue");
+
+ is((yield getValue("#testid", "background-color")), "rgb(0, 0, 255)",
+ "New property should be used.");
+ is(elementRule.textProps[0], firstProp,
+ "Rules shouldn't have switched places.");
+ is(elementRule.textProps[1], secondProp,
+ "Rules shouldn't have switched places.");
+
+ info("Disabling the second property and checking the applied style");
+ yield togglePropStatus(view, secondProp);
+
+ is((yield getValue("#testid", "background-color")), "rgb(0, 128, 0)",
+ "After disabling second property, first value should be used");
+
+ info("Disabling the first property too and checking the applied style");
+ yield togglePropStatus(view, firstProp);
+
+ is((yield getValue("#testid", "background-color")), "transparent",
+ "After disabling both properties, value should be empty.");
+
+ info("Re-enabling the second propertyt and checking the applied style");
+ yield togglePropStatus(view, secondProp);
+
+ is((yield getValue("#testid", "background-color")), "rgb(0, 0, 255)",
+ "Value should be set correctly after re-enabling");
+
+ info("Re-enabling the first property and checking the insertion order " +
+ "is still respected");
+ yield togglePropStatus(view, firstProp);
+
+ is((yield getValue("#testid", "background-color")), "rgb(0, 0, 255)",
+ "Re-enabling an earlier property shouldn't make it override " +
+ "a later property.");
+ is(elementRule.textProps[0], firstProp,
+ "Rules shouldn't have switched places.");
+ is(elementRule.textProps[1], secondProp,
+ "Rules shouldn't have switched places.");
+ info("Modifying the first property and checking the applied style");
+ yield setProperty(view, firstProp, "purple");
+
+ is((yield getValue("#testid", "background-color")), "rgb(0, 0, 255)",
+ "Modifying an earlier property shouldn't override a later property.");
+});
+
+function* getValue(selector, propName) {
+ let value = yield getComputedStyleProperty(selector, null, propName);
+ return value;
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property-remove_01.js b/devtools/client/inspector/rules/test/browser_rules_edit-property-remove_01.js
new file mode 100644
index 000000000..0aed2f5c8
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property-remove_01.js
@@ -0,0 +1,67 @@
+/* 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";
+
+// Tests removing a property by clearing the property name and pressing the
+// return key, and checks if the focus is moved to the appropriate editable
+// field.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #testid {
+ background-color: #00F;
+ color: #00F;
+ }
+ </style>
+ <div id='testid'>Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ info("Getting the first property in the #testid rule");
+ let rule = getRuleViewRuleEditor(view, 1).rule;
+ let prop = rule.textProps[0];
+
+ info("Deleting the name of that property to remove the property");
+ yield removeProperty(view, prop, false);
+
+ let newValue = yield executeInContent("Test:GetRulePropertyValue", {
+ styleSheetIndex: 0,
+ ruleIndex: 0,
+ name: "background-color"
+ });
+ is(newValue, "", "background-color should have been unset.");
+
+ info("Getting the new first property in the rule");
+ prop = rule.textProps[0];
+
+ let editor = inplaceEditor(view.styleDocument.activeElement);
+ is(inplaceEditor(prop.editor.nameSpan), editor,
+ "Focus should have moved to the next property name");
+
+ info("Deleting the name of that property to remove the property");
+ view.styleDocument.activeElement.blur();
+ yield removeProperty(view, prop, false);
+
+ newValue = yield executeInContent("Test:GetRulePropertyValue", {
+ styleSheetIndex: 0,
+ ruleIndex: 0,
+ name: "color"
+ });
+ is(newValue, "", "color should have been unset.");
+
+ editor = inplaceEditor(view.styleDocument.activeElement);
+ is(inplaceEditor(rule.editor.newPropSpan), editor,
+ "Focus should have moved to the new property span");
+ is(rule.textProps.length, 0,
+ "All properties should have been removed.");
+ is(rule.editor.propertyList.children.length, 1,
+ "Should have the new property span.");
+
+ view.styleDocument.activeElement.blur();
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property-remove_02.js b/devtools/client/inspector/rules/test/browser_rules_edit-property-remove_02.js
new file mode 100644
index 000000000..5690e7c2d
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property-remove_02.js
@@ -0,0 +1,67 @@
+/* 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";
+
+// Tests removing a property by clearing the property value and pressing the
+// return key, and checks if the focus is moved to the appropriate editable
+// field.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #testid {
+ background-color: #00F;
+ color: #00F;
+ }
+ </style>
+ <div id='testid'>Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ info("Getting the first property in the rule");
+ let rule = getRuleViewRuleEditor(view, 1).rule;
+ let prop = rule.textProps[0];
+
+ info("Clearing the property value");
+ yield setProperty(view, prop, null, false);
+
+ let newValue = yield executeInContent("Test:GetRulePropertyValue", {
+ styleSheetIndex: 0,
+ ruleIndex: 0,
+ name: "background-color"
+ });
+ is(newValue, "", "background-color should have been unset.");
+
+ info("Getting the new first property in the rule");
+ prop = rule.textProps[0];
+
+ let editor = inplaceEditor(view.styleDocument.activeElement);
+ is(inplaceEditor(prop.editor.nameSpan), editor,
+ "Focus should have moved to the next property name");
+ view.styleDocument.activeElement.blur();
+
+ info("Clearing the property value");
+ yield setProperty(view, prop, null, false);
+
+ newValue = yield executeInContent("Test:GetRulePropertyValue", {
+ styleSheetIndex: 0,
+ ruleIndex: 0,
+ name: "color"
+ });
+ is(newValue, "", "color should have been unset.");
+
+ editor = inplaceEditor(view.styleDocument.activeElement);
+ is(inplaceEditor(rule.editor.newPropSpan), editor,
+ "Focus should have moved to the new property span");
+ is(rule.textProps.length, 0,
+ "All properties should have been removed.");
+ is(rule.editor.propertyList.children.length, 1,
+ "Should have the new property span.");
+
+ view.styleDocument.activeElement.blur();
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property-remove_03.js b/devtools/client/inspector/rules/test/browser_rules_edit-property-remove_03.js
new file mode 100644
index 000000000..21a1063c2
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property-remove_03.js
@@ -0,0 +1,83 @@
+/* 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";
+
+// Tests removing a property by clearing the property name and pressing shift
+// and tab keys, and checks if the focus is moved to the appropriate editable
+// field.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #testid {
+ background-color: #00F;
+ color: #00F;
+ }
+ </style>
+ <div id='testid'>Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ info("Getting the second property in the rule");
+ let rule = getRuleViewRuleEditor(view, 1).rule;
+ let prop = rule.textProps[1];
+
+ info("Clearing the property value and pressing shift-tab");
+ let editor = yield focusEditableField(view, prop.editor.valueSpan);
+ let onValueDone = view.once("ruleview-changed");
+ editor.input.value = "";
+ EventUtils.synthesizeKey("VK_TAB", {shiftKey: true}, view.styleWindow);
+ yield onValueDone;
+
+ let newValue = yield executeInContent("Test:GetRulePropertyValue", {
+ styleSheetIndex: 0,
+ ruleIndex: 0,
+ name: "color"
+ });
+ is(newValue, "", "color should have been unset.");
+ is(prop.editor.valueSpan.textContent, "",
+ "'' property value is correctly set.");
+
+ info("Pressing shift-tab again to focus the previous property value");
+ let onValueFocused = view.once("ruleview-changed");
+ EventUtils.synthesizeKey("VK_TAB", {shiftKey: true}, view.styleWindow);
+ yield onValueFocused;
+
+ info("Getting the first property in the rule");
+ prop = rule.textProps[0];
+
+ editor = inplaceEditor(view.styleDocument.activeElement);
+ is(inplaceEditor(prop.editor.valueSpan), editor,
+ "Focus should have moved to the previous property value");
+
+ info("Pressing shift-tab again to focus the property name");
+ let onNameFocused = view.once("ruleview-changed");
+ EventUtils.synthesizeKey("VK_TAB", {shiftKey: true}, view.styleWindow);
+ yield onNameFocused;
+
+ info("Removing the name and pressing shift-tab to focus the selector");
+ let onNameDeleted = view.once("ruleview-changed");
+ EventUtils.synthesizeKey("VK_DELETE", {}, view.styleWindow);
+ EventUtils.synthesizeKey("VK_TAB", {shiftKey: true}, view.styleWindow);
+ yield onNameDeleted;
+
+ newValue = yield executeInContent("Test:GetRulePropertyValue", {
+ styleSheetIndex: 0,
+ ruleIndex: 0,
+ name: "background-color"
+ });
+ is(newValue, "", "background-color should have been unset.");
+
+ editor = inplaceEditor(view.styleDocument.activeElement);
+ is(inplaceEditor(rule.editor.selectorText), editor,
+ "Focus should have moved to the selector text.");
+ is(rule.textProps.length, 0,
+ "All properties should have been removed.");
+ ok(!rule.editor.propertyList.hasChildNodes(),
+ "Should not have any properties.");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property_01.js b/devtools/client/inspector/rules/test/browser_rules_edit-property_01.js
new file mode 100644
index 000000000..6f4c49e20
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property_01.js
@@ -0,0 +1,93 @@
+/* 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";
+
+// Testing adding new properties via the inplace-editors in the rule
+// view.
+// FIXME: some of the inplace-editor focus/blur/commit/revert stuff
+// should be factored out in head.js
+
+const TEST_URI = `
+ <style type="text/css">
+ #testid {
+ color: red;
+ background-color: blue;
+ }
+ .testclass, .unmatched {
+ background-color: green;
+ }
+ </style>
+ <div id="testid" class="testclass">Styled Node</div>
+ <div id="testid2">Styled Node</div>
+`;
+
+var BACKGROUND_IMAGE_URL = 'url("' + URL_ROOT + 'doc_test_image.png")';
+
+var TEST_DATA = [
+ { name: "border-color", value: "red", isValid: true },
+ { name: "background-image", value: BACKGROUND_IMAGE_URL, isValid: true },
+ { name: "border", value: "solid 1px foo", isValid: false },
+];
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ let rule = getRuleViewRuleEditor(view, 1).rule;
+ for (let {name, value, isValid} of TEST_DATA) {
+ yield testEditProperty(view, rule, name, value, isValid);
+ }
+});
+
+function* testEditProperty(view, rule, name, value, isValid) {
+ info("Test editing existing property name/value fields");
+
+ let doc = rule.editor.doc;
+ let prop = rule.textProps[0];
+
+ info("Focusing an existing property name in the rule-view");
+ let editor = yield focusEditableField(view, prop.editor.nameSpan, 32, 1);
+
+ is(inplaceEditor(prop.editor.nameSpan), editor,
+ "The property name editor got focused");
+ let input = editor.input;
+
+ info("Entering a new property name, including : to commit and " +
+ "focus the value");
+ let onValueFocus = once(rule.editor.element, "focus", true);
+ let onNameDone = view.once("ruleview-changed");
+ EventUtils.sendString(name + ":", doc.defaultView);
+ yield onValueFocus;
+ yield onNameDone;
+
+ // Getting the value editor after focus
+ editor = inplaceEditor(doc.activeElement);
+ input = editor.input;
+ is(inplaceEditor(prop.editor.valueSpan), editor, "Focus moved to the value.");
+
+ info("Entering a new value, including ; to commit and blur the value");
+ let onValueDone = view.once("ruleview-changed");
+ let onBlur = once(input, "blur");
+ EventUtils.sendString(value + ";", doc.defaultView);
+ yield onBlur;
+ yield onValueDone;
+
+ is(prop.editor.isValid(), isValid,
+ value + " is " + isValid ? "valid" : "invalid");
+
+ info("Checking that the style property was changed on the content page");
+ let propValue = yield executeInContent("Test:GetRulePropertyValue", {
+ styleSheetIndex: 0,
+ ruleIndex: 0,
+ name
+ });
+
+ if (isValid) {
+ is(propValue, value, name + " should have been set.");
+ } else {
+ isnot(propValue, value, name + " shouldn't have been set.");
+ }
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property_02.js b/devtools/client/inspector/rules/test/browser_rules_edit-property_02.js
new file mode 100644
index 000000000..7e6315236
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property_02.js
@@ -0,0 +1,133 @@
+/* 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 several types of rule-view property edition
+
+const TEST_URI = `
+ <style type="text/css">
+ #testid {
+ background-color: blue;
+ }
+ .testclass, .unmatched {
+ background-color: green;
+ }
+ </style>
+ <div id="testid" class="testclass">Styled Node</div>
+ <div id="testid2">Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ yield testEditProperty(inspector, view);
+ yield testDisableProperty(inspector, view);
+ yield testPropertyStillMarkedDirty(inspector, view);
+});
+
+function* testEditProperty(inspector, ruleView) {
+ let idRule = getRuleViewRuleEditor(ruleView, 1).rule;
+ let prop = idRule.textProps[0];
+
+ let editor = yield focusEditableField(ruleView, prop.editor.nameSpan);
+ let input = editor.input;
+ is(inplaceEditor(prop.editor.nameSpan), editor,
+ "Next focused editor should be the name editor.");
+
+ ok(input.selectionStart === 0 && input.selectionEnd === input.value.length,
+ "Editor contents are selected.");
+
+ // Try clicking on the editor's input again, shouldn't cause trouble
+ // (see bug 761665).
+ EventUtils.synthesizeMouse(input, 1, 1, {}, ruleView.styleWindow);
+ input.select();
+
+ info("Entering property name \"border-color\" followed by a colon to " +
+ "focus the value");
+ let onNameDone = ruleView.once("ruleview-changed");
+ let onFocus = once(idRule.editor.element, "focus", true);
+ EventUtils.sendString("border-color:", ruleView.styleWindow);
+ yield onFocus;
+ yield onNameDone;
+
+ info("Verifying that the focused field is the valueSpan");
+ editor = inplaceEditor(ruleView.styleDocument.activeElement);
+ input = editor.input;
+ is(inplaceEditor(prop.editor.valueSpan), editor,
+ "Focus should have moved to the value.");
+ ok(input.selectionStart === 0 && input.selectionEnd === input.value.length,
+ "Editor contents are selected.");
+
+ info("Entering a value following by a semi-colon to commit it");
+ let onBlur = once(editor.input, "blur");
+ // Use sendChar() to pass each character as a string so that we can test
+ // prop.editor.warning.hidden after each character.
+ for (let ch of "red;") {
+ let onPreviewDone = ruleView.once("ruleview-changed");
+ EventUtils.sendChar(ch, ruleView.styleWindow);
+ ruleView.throttle.flush();
+ yield onPreviewDone;
+ is(prop.editor.warning.hidden, true,
+ "warning triangle is hidden or shown as appropriate");
+ }
+ yield onBlur;
+
+ let newValue = yield executeInContent("Test:GetRulePropertyValue", {
+ styleSheetIndex: 0,
+ ruleIndex: 0,
+ name: "border-color"
+ });
+ is(newValue, "red", "border-color should have been set.");
+
+ ruleView.styleDocument.activeElement.blur();
+ yield addProperty(ruleView, 1, "color", "red", ";");
+
+ let props = ruleView.element.querySelectorAll(".ruleview-property");
+ for (let i = 0; i < props.length; i++) {
+ is(props[i].hasAttribute("dirty"), i <= 1,
+ "props[" + i + "] marked dirty as appropriate");
+ }
+}
+
+function* testDisableProperty(inspector, ruleView) {
+ let idRule = getRuleViewRuleEditor(ruleView, 1).rule;
+ let prop = idRule.textProps[0];
+
+ info("Disabling a property");
+ yield togglePropStatus(ruleView, prop);
+
+ let newValue = yield executeInContent("Test:GetRulePropertyValue", {
+ styleSheetIndex: 0,
+ ruleIndex: 0,
+ name: "border-color"
+ });
+ is(newValue, "", "Border-color should have been unset.");
+
+ info("Enabling the property again");
+ yield togglePropStatus(ruleView, prop);
+
+ newValue = yield executeInContent("Test:GetRulePropertyValue", {
+ styleSheetIndex: 0,
+ ruleIndex: 0,
+ name: "border-color"
+ });
+ is(newValue, "red", "Border-color should have been reset.");
+}
+
+function* testPropertyStillMarkedDirty(inspector, ruleView) {
+ // Select an unstyled node.
+ yield selectNode("#testid2", inspector);
+
+ // Select the original node again.
+ yield selectNode("#testid", inspector);
+
+ let props = ruleView.element.querySelectorAll(".ruleview-property");
+ for (let i = 0; i < props.length; i++) {
+ is(props[i].hasAttribute("dirty"), i <= 1,
+ "props[" + i + "] marked dirty as appropriate");
+ }
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property_03.js b/devtools/client/inspector/rules/test/browser_rules_edit-property_03.js
new file mode 100644
index 000000000..a5771b41e
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property_03.js
@@ -0,0 +1,50 @@
+/* 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";
+
+// Tests that emptying out an existing value removes the property and
+// doesn't cause any other issues. See also Bug 1150780.
+
+const TEST_URI = `
+ <style type="text/css">
+ #testid {
+ color: red;
+ background-color: blue;
+ font-size: 12px;
+ }
+ .testclass, .unmatched {
+ background-color: green;
+ }
+ </style>
+ <div id="testid" class="testclass">Styled Node</div>
+ <div id="testid2">Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+ let propEditor = ruleEditor.rule.textProps[1].editor;
+
+ yield focusEditableField(view, propEditor.valueSpan);
+
+ info("Deleting all the text out of a value field");
+ let onRuleViewChanged = view.once("ruleview-changed");
+ yield sendKeysAndWaitForFocus(view, ruleEditor.element,
+ ["DELETE", "RETURN"]);
+ yield onRuleViewChanged;
+
+ info("Pressing enter a couple times to cycle through editors");
+ yield sendKeysAndWaitForFocus(view, ruleEditor.element, ["RETURN"]);
+ onRuleViewChanged = view.once("ruleview-changed");
+ yield sendKeysAndWaitForFocus(view, ruleEditor.element, ["RETURN"]);
+ yield onRuleViewChanged;
+
+ isnot(ruleEditor.rule.textProps[1].editor.nameSpan.style.display, "none",
+ "The name span is visible");
+ is(ruleEditor.rule.textProps.length, 2, "Correct number of props");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property_04.js b/devtools/client/inspector/rules/test/browser_rules_edit-property_04.js
new file mode 100644
index 000000000..7460db4cd
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property_04.js
@@ -0,0 +1,85 @@
+/* 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";
+
+// Tests that a disabled property remains disabled when the escaping out of
+// the property editor.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #testid {
+ background-color: blue;
+ }
+ </style>
+ <div id='testid'>Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ let rule = getRuleViewRuleEditor(view, 1).rule;
+ let prop = rule.textProps[0];
+
+ info("Disabling a property");
+ yield togglePropStatus(view, prop);
+
+ let newValue = yield executeInContent("Test:GetRulePropertyValue", {
+ styleSheetIndex: 0,
+ ruleIndex: 0,
+ name: "background-color"
+ });
+ is(newValue, "", "background-color should have been unset.");
+
+ yield testEditDisableProperty(view, rule, prop, "name", "VK_ESCAPE");
+ yield testEditDisableProperty(view, rule, prop, "value", "VK_ESCAPE");
+ yield testEditDisableProperty(view, rule, prop, "value", "VK_TAB");
+ yield testEditDisableProperty(view, rule, prop, "value", "VK_RETURN");
+});
+
+function* testEditDisableProperty(view, rule, prop, fieldType, commitKey) {
+ let field = fieldType === "name" ? prop.editor.nameSpan
+ : prop.editor.valueSpan;
+
+ let editor = yield focusEditableField(view, field);
+
+ ok(!prop.editor.element.classList.contains("ruleview-overridden"),
+ "property is not overridden.");
+ is(prop.editor.enable.style.visibility, "hidden",
+ "property enable checkbox is hidden.");
+
+ let newValue = yield executeInContent("Test:GetRulePropertyValue", {
+ styleSheetIndex: 0,
+ ruleIndex: 0,
+ name: "background-color"
+ });
+ is(newValue, "", "background-color should remain unset.");
+
+ let onChangeDone;
+ if (fieldType === "value") {
+ onChangeDone = view.once("ruleview-changed");
+ }
+
+ let onBlur = once(editor.input, "blur");
+ EventUtils.synthesizeKey(commitKey, {}, view.styleWindow);
+ yield onBlur;
+ yield onChangeDone;
+
+ ok(!prop.enabled, "property is disabled.");
+ ok(prop.editor.element.classList.contains("ruleview-overridden"),
+ "property is overridden.");
+ is(prop.editor.enable.style.visibility, "visible",
+ "property enable checkbox is visible.");
+ ok(!prop.editor.enable.getAttribute("checked"),
+ "property enable checkbox is not checked.");
+
+ newValue = yield executeInContent("Test:GetRulePropertyValue", {
+ styleSheetIndex: 0,
+ ruleIndex: 0,
+ name: "background-color"
+ });
+ is(newValue, "", "background-color should remain unset.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property_05.js b/devtools/client/inspector/rules/test/browser_rules_edit-property_05.js
new file mode 100644
index 000000000..3d37c81d5
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property_05.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";
+
+// Tests that a disabled property is re-enabled if the property name or value is
+// modified
+
+const TEST_URI = `
+ <style type='text/css'>
+ #testid {
+ background-color: blue;
+ }
+ </style>
+ <div id='testid'>Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ let rule = getRuleViewRuleEditor(view, 1).rule;
+ let prop = rule.textProps[0];
+
+ info("Disabling background-color property");
+ yield togglePropStatus(view, prop);
+
+ let newValue = yield getRulePropertyValue("background-color");
+ is(newValue, "", "background-color should have been unset.");
+
+ info("Entering a new property name, including : to commit and " +
+ "focus the value");
+
+ yield focusEditableField(view, prop.editor.nameSpan);
+ let onNameDone = view.once("ruleview-changed");
+ EventUtils.sendString("border-color:", view.styleWindow);
+ yield onNameDone;
+
+ info("Escape editing the property value");
+ let onValueDone = view.once("ruleview-changed");
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, view.styleWindow);
+ yield onValueDone;
+
+ newValue = yield getRulePropertyValue("border-color");
+ is(newValue, "blue", "border-color should have been set.");
+
+ ok(prop.enabled, "border-color property is enabled.");
+ ok(!prop.editor.element.classList.contains("ruleview-overridden"),
+ "border-color is not overridden");
+
+ info("Disabling border-color property");
+ yield togglePropStatus(view, prop);
+
+ newValue = yield getRulePropertyValue("border-color");
+ is(newValue, "", "border-color should have been unset.");
+
+ info("Enter a new property value for the border-color property");
+ yield setProperty(view, prop, "red");
+
+ newValue = yield getRulePropertyValue("border-color");
+ is(newValue, "red", "new border-color should have been set.");
+
+ ok(prop.enabled, "border-color property is enabled.");
+ ok(!prop.editor.element.classList.contains("ruleview-overridden"),
+ "border-color is not overridden");
+});
+
+function* getRulePropertyValue(name) {
+ let propValue = yield executeInContent("Test:GetRulePropertyValue", {
+ styleSheetIndex: 0,
+ ruleIndex: 0,
+ name: name
+ });
+ return propValue;
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property_06.js b/devtools/client/inspector/rules/test/browser_rules_edit-property_06.js
new file mode 100644
index 000000000..95211f1d0
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property_06.js
@@ -0,0 +1,52 @@
+/* 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";
+
+// Tests that editing a property's priority is behaving correctly, and disabling
+// and editing the property will re-enable the property.
+
+const TEST_URI = `
+ <style type='text/css'>
+ body {
+ background-color: green !important;
+ }
+ body {
+ background-color: red;
+ }
+ </style>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("body", inspector);
+
+ let rule = getRuleViewRuleEditor(view, 1).rule;
+ let prop = rule.textProps[0];
+
+ is((yield getComputedStyleProperty("body", null, "background-color")),
+ "rgb(0, 128, 0)", "green background color is set.");
+
+ yield setProperty(view, prop, "red !important");
+
+ is(prop.editor.valueSpan.textContent, "red !important",
+ "'red !important' property value is correctly set.");
+ is((yield getComputedStyleProperty("body", null, "background-color")),
+ "rgb(255, 0, 0)", "red background color is set.");
+
+ info("Disabling red background color property");
+ yield togglePropStatus(view, prop);
+
+ is((yield getComputedStyleProperty("body", null, "background-color")),
+ "rgb(0, 128, 0)", "green background color is set.");
+
+ yield setProperty(view, prop, "red");
+
+ is(prop.editor.valueSpan.textContent, "red",
+ "'red' property value is correctly set.");
+ ok(prop.enabled, "red background-color property is enabled.");
+ is((yield getComputedStyleProperty("body", null, "background-color")),
+ "rgb(0, 128, 0)", "green background color is set.");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property_07.js b/devtools/client/inspector/rules/test/browser_rules_edit-property_07.js
new file mode 100644
index 000000000..40314819f
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property_07.js
@@ -0,0 +1,50 @@
+/* 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";
+
+// Tests that adding multiple values will enable the property even if the
+// property does not change, and that the extra values are added correctly.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #testid {
+ background-color: #f00;
+ }
+ </style>
+ <div id='testid'>Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ let rule = getRuleViewRuleEditor(view, 1).rule;
+ let prop = rule.textProps[0];
+
+ info("Disabling red background color property");
+ yield togglePropStatus(view, prop);
+ ok(!prop.enabled, "red background-color property is disabled.");
+
+ let editor = yield focusEditableField(view, prop.editor.valueSpan);
+ let onDone = view.once("ruleview-changed");
+ editor.input.value = "red; color: red;";
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
+ yield onDone;
+
+ is(prop.editor.valueSpan.textContent, "red",
+ "'red' property value is correctly set.");
+ ok(prop.enabled, "red background-color property is enabled.");
+ is((yield getComputedStyleProperty("#testid", null, "background-color")),
+ "rgb(255, 0, 0)", "red background color is set.");
+
+ let propEditor = rule.textProps[1].editor;
+ is(propEditor.nameSpan.textContent, "color",
+ "new 'color' property name is correctly set.");
+ is(propEditor.valueSpan.textContent, "red",
+ "new 'red' property value is correctly set.");
+ is((yield getComputedStyleProperty("#testid", null, "color")),
+ "rgb(255, 0, 0)", "red color is set.");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property_08.js b/devtools/client/inspector/rules/test/browser_rules_edit-property_08.js
new file mode 100644
index 000000000..1becd40d9
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property_08.js
@@ -0,0 +1,57 @@
+/* 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 renaming a property works.
+
+const TEST_URI = `
+ <style type="text/css">
+ #testid {
+ color: #FFF;
+ }
+ </style>
+ <div style='color: red' id='testid'>Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ info("Get the color property editor");
+ let ruleEditor = getRuleViewRuleEditor(view, 0);
+ let propEditor = ruleEditor.rule.textProps[0].editor;
+ is(ruleEditor.rule.textProps[0].name, "color");
+
+ info("Focus the property name field");
+ yield focusEditableField(ruleEditor.ruleView, propEditor.nameSpan, 32, 1);
+
+ info("Rename the property to background-color");
+ // Expect 3 events: the value editor being focused, the ruleview-changed event
+ // which signals that the new value has been previewed (fires once when the
+ // value gets focused), and the markupmutation event since we're modifying an
+ // inline style.
+ let onValueFocus = once(ruleEditor.element, "focus", true);
+ let onRuleViewChanged = ruleEditor.ruleView.once("ruleview-changed");
+ let onMutation = inspector.once("markupmutation");
+ EventUtils.sendString("background-color:", ruleEditor.doc.defaultView);
+ yield onValueFocus;
+ yield onRuleViewChanged;
+ yield onMutation;
+
+ is(ruleEditor.rule.textProps[0].name, "background-color");
+ yield waitForComputedStyleProperty("#testid", null, "background-color",
+ "rgb(255, 0, 0)");
+
+ is((yield getComputedStyleProperty("#testid", null, "color")),
+ "rgb(255, 255, 255)", "color is white");
+
+ // The value field is still focused. Blur it now and wait for the
+ // ruleview-changed event to avoid pending requests.
+ onRuleViewChanged = view.once("ruleview-changed");
+ EventUtils.synthesizeKey("VK_ESCAPE", {});
+ yield onRuleViewChanged;
+});
+
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property_09.js b/devtools/client/inspector/rules/test/browser_rules_edit-property_09.js
new file mode 100644
index 000000000..51f714021
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property_09.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 a newProperty editor is only created if no other editor was
+// previously displayed.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #testid {
+ background-color: blue;
+ }
+ </style>
+ <div id='testid'>Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+ yield testClickOnEmptyAreaToCloseEditor(inspector, view);
+});
+
+function synthesizeMouseOnEmptyArea(ruleEditor, view) {
+ // any text property editor will do
+ let propEditor = ruleEditor.rule.textProps[0].editor;
+ let valueContainer = propEditor.valueContainer;
+ let valueRect = valueContainer.getBoundingClientRect();
+ // click right next to the ";" at the end of valueContainer
+ EventUtils.synthesizeMouse(valueContainer, valueRect.width + 1, 1, {},
+ view.styleWindow);
+}
+
+function* testClickOnEmptyAreaToCloseEditor(inspector, view) {
+ // Start at the beginning: start to add a rule to the element's style
+ // declaration, add some text, then press escape.
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+ let propEditor = ruleEditor.rule.textProps[0].editor;
+
+ info("Create a property value editor");
+ let editor = yield focusEditableField(view, propEditor.valueSpan);
+ ok(editor.input, "The inplace-editor field is ready");
+
+ info("Close the property value editor by clicking on an empty area " +
+ "in the rule editor");
+ let onRuleViewChanged = view.once("ruleview-changed");
+ let onBlur = once(editor.input, "blur");
+ synthesizeMouseOnEmptyArea(ruleEditor, view);
+ yield onBlur;
+ yield onRuleViewChanged;
+ ok(!view.isEditing, "No inplace editor should be displayed in the ruleview");
+
+ info("Create new newProperty editor by clicking again on the empty area");
+ let onFocus = once(ruleEditor.element, "focus", true);
+ synthesizeMouseOnEmptyArea(ruleEditor, view);
+ yield onFocus;
+ editor = inplaceEditor(ruleEditor.element.ownerDocument.activeElement);
+ is(inplaceEditor(ruleEditor.newPropSpan), editor,
+ "New property editor was created");
+
+ info("Close the newProperty editor by clicking again on the empty area");
+ onBlur = once(editor.input, "blur");
+ synthesizeMouseOnEmptyArea(ruleEditor, view);
+ yield onBlur;
+
+ ok(!view.isEditing, "No inplace editor should be displayed in the ruleview");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-selector-click-on-scrollbar.js b/devtools/client/inspector/rules/test/browser_rules_edit-selector-click-on-scrollbar.js
new file mode 100644
index 000000000..1846df60d
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector-click-on-scrollbar.js
@@ -0,0 +1,88 @@
+/* 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";
+
+// Testing ruleview inplace-editor is not blurred when clicking on the ruleview
+// container scrollbar.
+
+const TEST_URI = `
+ <style type="text/css">
+ div.testclass {
+ color: black;
+ }
+ .a {
+ color: #aaa;
+ }
+ .b {
+ color: #bbb;
+ }
+ .c {
+ color: #ccc;
+ }
+ .d {
+ color: #ddd;
+ }
+ .e {
+ color: #eee;
+ }
+ .f {
+ color: #fff;
+ }
+ </style>
+ <div class="testclass a b c d e f">Styled Node</div>
+`;
+
+add_task(function* () {
+ info("Toolbox height should be small enough to force scrollbars to appear");
+ yield new Promise(done => {
+ let options = {"set": [
+ ["devtools.toolbox.footer.height", 200],
+ ]};
+ SpecialPowers.pushPrefEnv(options, done);
+ });
+
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode(".testclass", inspector);
+
+ info("Check we have an overflow on the ruleview container.");
+ let container = view.element;
+ let hasScrollbar = container.offsetHeight < container.scrollHeight;
+ ok(hasScrollbar, "The rule view container should have a vertical scrollbar.");
+
+ info("Focusing an existing selector name in the rule-view.");
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+ let editor = yield focusEditableField(view, ruleEditor.selectorText);
+ is(inplaceEditor(ruleEditor.selectorText), editor,
+ "The selector editor is focused.");
+
+ info("Click on the scrollbar element.");
+ yield clickOnRuleviewScrollbar(view);
+
+ is(editor.input, view.styleDocument.activeElement,
+ "The editor input should still be focused.");
+
+ info("Check a new value can still be committed in the editable field");
+ let newValue = ".testclass.a.b.c.d.e.f";
+ let onRuleViewChanged = once(view, "ruleview-changed");
+
+ info("Enter new value and commit.");
+ editor.input.value = newValue;
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ yield onRuleViewChanged;
+ ok(getRuleViewRule(view, newValue), "Rule with '" + newValue + " 'exists.");
+});
+
+function* clickOnRuleviewScrollbar(view) {
+ let container = view.element.parentNode;
+ let onScroll = once(container, "scroll");
+ let rect = container.getBoundingClientRect();
+ // click 5 pixels before the bottom-right corner should hit the scrollbar
+ EventUtils.synthesizeMouse(container, rect.width - 5, rect.height - 5,
+ {}, view.styleWindow);
+ yield onScroll;
+
+ ok(true, "The rule view container scrolled after clicking on the scrollbar.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-selector-click.js b/devtools/client/inspector/rules/test/browser_rules_edit-selector-click.js
new file mode 100644
index 000000000..7a3b6d467
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector-click.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";
+
+// Testing selector inplace-editor remains available and focused after clicking
+// in its input.
+
+const TEST_URI = `
+ <style type="text/css">
+ .testclass {
+ text-align: center;
+ }
+ </style>
+ <div class="testclass">Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode(".testclass", inspector);
+ yield testClickOnSelectorEditorInput(view);
+});
+
+function* testClickOnSelectorEditorInput(view) {
+ info("Test clicking inside the selector editor input");
+
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+
+ info("Focusing an existing selector name in the rule-view");
+ let editor = yield focusEditableField(view, ruleEditor.selectorText);
+ let editorInput = editor.input;
+ is(inplaceEditor(ruleEditor.selectorText), editor,
+ "The selector editor got focused");
+
+ info("Click inside the editor input");
+ let onClick = once(editorInput, "click");
+ EventUtils.synthesizeMouse(editor.input, 2, 1, {}, view.styleWindow);
+ yield onClick;
+ is(editor.input, view.styleDocument.activeElement,
+ "The editor input should still be focused");
+ ok(!ruleEditor.newPropSpan, "No newProperty editor was created");
+
+ info("Doubleclick inside the editor input");
+ let onDoubleClick = once(editorInput, "dblclick");
+ EventUtils.synthesizeMouse(editor.input, 2, 1, { clickCount: 2 },
+ view.styleWindow);
+ yield onDoubleClick;
+ is(editor.input, view.styleDocument.activeElement,
+ "The editor input should still be focused");
+ ok(!ruleEditor.newPropSpan, "No newProperty editor was created");
+
+ info("Click outside the editor input");
+ let onBlur = once(editorInput, "blur");
+ let rect = editorInput.getBoundingClientRect();
+ EventUtils.synthesizeMouse(editorInput, rect.width + 5, rect.height / 2, {},
+ view.styleWindow);
+ yield onBlur;
+
+ isnot(editorInput, view.styleDocument.activeElement,
+ "The editor input should no longer be focused");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-selector-commit.js b/devtools/client/inspector/rules/test/browser_rules_edit-selector-commit.js
new file mode 100644
index 000000000..f7058371f
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector-commit.js
@@ -0,0 +1,117 @@
+/* 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 selector value is correctly displayed when committing the inplace editor
+// with ENTER, ESC, SHIFT+TAB and TAB
+
+const TEST_URI = `
+ <style type='text/css'>
+ #testid1 {
+ text-align: center;
+ }
+ #testid2 {
+ text-align: center;
+ }
+ #testid3 {
+ }
+ </style>
+ <div id='testid1'>Styled Node</div>
+ <div id='testid2'>Styled Node</div>
+ <div id='testid3'>Styled Node</div>
+`;
+
+const TEST_DATA = [
+ {
+ node: "#testid1",
+ value: ".testclass",
+ commitKey: "VK_ESCAPE",
+ modifiers: {},
+ expected: "#testid1",
+
+ },
+ {
+ node: "#testid1",
+ value: ".testclass1",
+ commitKey: "VK_RETURN",
+ modifiers: {},
+ expected: ".testclass1"
+ },
+ {
+ node: "#testid2",
+ value: ".testclass2",
+ commitKey: "VK_TAB",
+ modifiers: {},
+ expected: ".testclass2"
+ },
+ {
+ node: "#testid3",
+ value: ".testclass3",
+ commitKey: "VK_TAB",
+ modifiers: {shiftKey: true},
+ expected: ".testclass3"
+ }
+];
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let { inspector, view } = yield openRuleView();
+
+ for (let data of TEST_DATA) {
+ yield runTestData(inspector, view, data);
+ }
+});
+
+function* runTestData(inspector, view, data) {
+ let {node, value, commitKey, modifiers, expected} = data;
+
+ info("Updating " + node + " to " + value + " and committing with " +
+ commitKey + ". Expecting: " + expected);
+
+ info("Selecting the test element");
+ yield selectNode(node, inspector);
+
+ let idRuleEditor = getRuleViewRuleEditor(view, 1);
+
+ info("Focusing an existing selector name in the rule-view");
+ let editor = yield focusEditableField(view, idRuleEditor.selectorText);
+ is(inplaceEditor(idRuleEditor.selectorText), editor,
+ "The selector editor got focused");
+
+ info("Enter the new selector value: " + value);
+ editor.input.value = value;
+
+ info("Entering the commit key " + commitKey + " " + modifiers);
+ EventUtils.synthesizeKey(commitKey, modifiers);
+
+ let activeElement = view.styleDocument.activeElement;
+
+ if (commitKey === "VK_ESCAPE") {
+ is(idRuleEditor.rule.selectorText, expected,
+ "Value is as expected: " + expected);
+ is(idRuleEditor.isEditing, false, "Selector is not being edited.");
+ is(idRuleEditor.selectorText, activeElement,
+ "Focus is on selector span.");
+ return;
+ }
+
+ yield once(view, "ruleview-changed");
+
+ ok(getRuleViewRule(view, expected),
+ "Rule with " + expected + " selector exists.");
+
+ if (modifiers.shiftKey) {
+ idRuleEditor = getRuleViewRuleEditor(view, 0);
+ }
+
+ let rule = idRuleEditor.rule;
+ if (rule.textProps.length > 0) {
+ is(inplaceEditor(rule.textProps[0].editor.nameSpan).input, activeElement,
+ "Focus is on the first property name span.");
+ } else {
+ is(inplaceEditor(idRuleEditor.newPropSpan).input, activeElement,
+ "Focus is on the new property span.");
+ }
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-selector_01.js b/devtools/client/inspector/rules/test/browser_rules_edit-selector_01.js
new file mode 100644
index 000000000..af228094b
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector_01.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";
+
+// Testing selector inplace-editor behaviors in the rule-view
+
+const TEST_URI = `
+ <style type="text/css">
+ .testclass {
+ text-align: center;
+ }
+ </style>
+ <div id="testid" class="testclass">Styled Node</div>
+ <span>This is a span</span>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+
+ info("Selecting the test element");
+ yield selectNode("#testid", inspector);
+ yield testEditSelector(view, "span");
+
+ info("Selecting the modified element with the new rule");
+ yield selectNode("span", inspector);
+ yield checkModifiedElement(view, "span");
+});
+
+function* testEditSelector(view, name) {
+ info("Test editing existing selector fields");
+
+ let idRuleEditor = getRuleViewRuleEditor(view, 1);
+
+ info("Focusing an existing selector name in the rule-view");
+ let editor = yield focusEditableField(view, idRuleEditor.selectorText);
+
+ is(inplaceEditor(idRuleEditor.selectorText), editor,
+ "The selector editor got focused");
+
+ info("Entering a new selector name and committing");
+ editor.input.value = name;
+
+ info("Waiting for rule view to update");
+ let onRuleViewChanged = once(view, "ruleview-changed");
+
+ info("Entering the commit key");
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ yield onRuleViewChanged;
+
+ is(view._elementStyle.rules.length, 2, "Should have 2 rules.");
+ ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists.");
+ ok(getRuleViewRuleEditor(view, 1).element.getAttribute("unmatched"),
+ "Rule with " + name + " does not match the current element.");
+}
+
+function* checkModifiedElement(view, name) {
+ is(view._elementStyle.rules.length, 2, "Should have 2 rules.");
+ ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-selector_02.js b/devtools/client/inspector/rules/test/browser_rules_edit-selector_02.js
new file mode 100644
index 000000000..503f91efa
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector_02.js
@@ -0,0 +1,88 @@
+/* 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";
+
+// Testing selector inplace-editor behaviors in the rule-view with pseudo
+// classes.
+
+const TEST_URI = `
+ <style type="text/css">
+ .testclass {
+ text-align: center;
+ }
+ #testid3::first-letter {
+ text-decoration: "italic"
+ }
+ </style>
+ <div id="testid">Styled Node</div>
+ <span class="testclass">This is a span</span>
+ <div class="testclass2">A</div>
+ <div id="testid3">B</div>
+`;
+
+const PSEUDO_PREF = "devtools.inspector.show_pseudo_elements";
+
+add_task(function* () {
+ // Expand the pseudo-elements section by default.
+ Services.prefs.setBoolPref(PSEUDO_PREF, true);
+
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+
+ info("Selecting the test element");
+ yield selectNode(".testclass", inspector);
+ yield testEditSelector(view, "div:nth-child(1)");
+
+ info("Selecting the modified element");
+ yield selectNode("#testid", inspector);
+ yield checkModifiedElement(view, "div:nth-child(1)");
+
+ info("Selecting the test element");
+ yield selectNode("#testid3", inspector);
+ yield testEditSelector(view, ".testclass2::first-letter");
+
+ info("Selecting the modified element");
+ yield selectNode(".testclass2", inspector);
+ yield checkModifiedElement(view, ".testclass2::first-letter");
+
+ // Reset the pseudo-elements section pref to its default value.
+ Services.prefs.clearUserPref(PSEUDO_PREF);
+});
+
+function* testEditSelector(view, name) {
+ info("Test editing existing selector fields");
+
+ let idRuleEditor = getRuleViewRuleEditor(view, 1) ||
+ getRuleViewRuleEditor(view, 1, 0);
+
+ info("Focusing an existing selector name in the rule-view");
+ let editor = yield focusEditableField(view, idRuleEditor.selectorText);
+
+ is(inplaceEditor(idRuleEditor.selectorText), editor,
+ "The selector editor got focused");
+
+ info("Entering a new selector name: " + name);
+ editor.input.value = name;
+
+ info("Waiting for rule view to update");
+ let onRuleViewChanged = once(view, "ruleview-changed");
+
+ info("Entering the commit key");
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ yield onRuleViewChanged;
+
+ is(view._elementStyle.rules.length, 2, "Should have 2 rule.");
+ ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists.");
+
+ let newRuleEditor = getRuleViewRuleEditor(view, 1) ||
+ getRuleViewRuleEditor(view, 1, 0);
+ ok(newRuleEditor.element.getAttribute("unmatched"),
+ "Rule with " + name + " does not match the current element.");
+}
+
+function* checkModifiedElement(view, name) {
+ is(view._elementStyle.rules.length, 2, "Should have 2 rules.");
+ ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-selector_03.js b/devtools/client/inspector/rules/test/browser_rules_edit-selector_03.js
new file mode 100644
index 000000000..c6834f6ee
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector_03.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";
+
+// Testing selector inplace-editor behaviors in the rule-view with invalid
+// selectors
+
+const TEST_URI = `
+ <style type="text/css">
+ .testclass {
+ text-align: center;
+ }
+ </style>
+ <div class="testclass">Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode(".testclass", inspector);
+ yield testEditSelector(view, "asd@:::!");
+});
+
+function* testEditSelector(view, name) {
+ info("Test editing existing selector fields");
+
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+
+ info("Focusing an existing selector name in the rule-view");
+ let editor = yield focusEditableField(view, ruleEditor.selectorText);
+
+ is(inplaceEditor(ruleEditor.selectorText), editor,
+ "The selector editor got focused");
+
+ info("Entering a new selector name and committing");
+ editor.input.value = name;
+ let onRuleViewChanged = once(view, "ruleview-invalid-selector");
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ yield onRuleViewChanged;
+
+ is(view._elementStyle.rules.length, 2, "Should have 2 rules.");
+ is(getRuleViewRule(view, name), undefined,
+ "Rule with " + name + " selector should not exist.");
+ ok(getRuleViewRule(view, ".testclass"),
+ "Rule with .testclass selector exists.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-selector_04.js b/devtools/client/inspector/rules/test/browser_rules_edit-selector_04.js
new file mode 100644
index 000000000..09b6ad841
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector_04.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";
+
+// Tests that the selector highlighter is removed when modifying a selector and
+// the selector highlighter works for the newly added unmatched rule.
+
+const TEST_URI = `
+ <style type="text/css">
+ p {
+ background: red;
+ }
+ </style>
+ <p>Test the selector highlighter</p>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("p", inspector);
+
+ ok(!view.selectorHighlighter,
+ "No selectorhighlighter exist in the rule-view");
+
+ yield testSelectorHighlight(view, "p");
+ yield testEditSelector(view, "body");
+ yield testSelectorHighlight(view, "body");
+});
+
+function* testSelectorHighlight(view, name) {
+ info("Test creating selector highlighter");
+
+ info("Clicking on a selector icon");
+ let icon = getRuleViewSelectorHighlighterIcon(view, name);
+
+ let onToggled = view.once("ruleview-selectorhighlighter-toggled");
+ EventUtils.synthesizeMouseAtCenter(icon, {}, view.styleWindow);
+ let isVisible = yield onToggled;
+
+ ok(view.selectorHighlighter, "The selectorhighlighter instance was created");
+ ok(isVisible, "The toggle event says the highlighter is visible");
+}
+
+function* testEditSelector(view, name) {
+ info("Test editing existing selector fields");
+
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+
+ info("Focusing an existing selector name in the rule-view");
+ let editor = yield focusEditableField(view, ruleEditor.selectorText);
+
+ is(inplaceEditor(ruleEditor.selectorText), editor,
+ "The selector editor got focused");
+
+ info("Waiting for rule view to update");
+ let onToggled = view.once("ruleview-selectorhighlighter-toggled");
+
+ info("Entering a new selector name and committing");
+ editor.input.value = name;
+ EventUtils.synthesizeKey("VK_RETURN", {});
+
+ let isVisible = yield onToggled;
+
+ ok(!view.highlighters.selectorHighlighterShown,
+ "The selectorHighlighterShown instance was removed");
+ ok(!isVisible, "The toggle event says the highlighter is not visible");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-selector_05.js b/devtools/client/inspector/rules/test/browser_rules_edit-selector_05.js
new file mode 100644
index 000000000..cd996b4b0
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector_05.js
@@ -0,0 +1,78 @@
+/* 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";
+
+// Tests that adding a new property of an unmatched rule works properly.
+
+const TEST_URI = `
+ <style type="text/css">
+ #testid {
+ }
+ .testclass {
+ background-color: white;
+ }
+ </style>
+ <div id="testid">Styled Node</div>
+ <span class="testclass">This is a span</span>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+
+ info("Selecting the test element");
+ yield selectNode("#testid", inspector);
+ yield testEditSelector(view, "span");
+ yield testAddProperty(view);
+
+ info("Selecting the modified element with the new rule");
+ yield selectNode("span", inspector);
+ yield checkModifiedElement(view, "span");
+});
+
+function* testEditSelector(view, name) {
+ info("Test editing existing selector fields");
+
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+
+ info("Focusing an existing selector name in the rule-view");
+ let editor = yield focusEditableField(view, ruleEditor.selectorText);
+
+ is(inplaceEditor(ruleEditor.selectorText), editor,
+ "The selector editor got focused");
+
+ info("Entering a new selector name and committing");
+ editor.input.value = name;
+
+ info("Waiting for rule view to update");
+ let onRuleViewChanged = once(view, "ruleview-changed");
+
+ info("Entering the commit key");
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ yield onRuleViewChanged;
+
+ is(view._elementStyle.rules.length, 2, "Should have 2 rules.");
+ ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists.");
+ ok(getRuleViewRuleEditor(view, 1).element.getAttribute("unmatched"),
+ "Rule with " + name + " does not match the current element.");
+
+ // Escape the new property editor after editing the selector
+ let onBlur = once(view.styleDocument.activeElement, "blur");
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, view.styleWindow);
+ yield onBlur;
+}
+
+function* checkModifiedElement(view, name) {
+ is(view._elementStyle.rules.length, 3, "Should have 3 rules.");
+ ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists.");
+}
+
+function* testAddProperty(view) {
+ info("Test creating a new property");
+ let textProp = yield addProperty(view, 1, "text-align", "center");
+
+ is(textProp.value, "center", "Text prop should have been changed.");
+ ok(!textProp.overridden, "Property should not be overridden");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-selector_06.js b/devtools/client/inspector/rules/test/browser_rules_edit-selector_06.js
new file mode 100644
index 000000000..7d782a309
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector_06.js
@@ -0,0 +1,76 @@
+/* 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";
+
+// Testing selector inplace-editor behaviors in the rule-view with unmatched
+// selectors
+
+const TEST_URI = `
+ <style type="text/css">
+ .testclass {
+ text-align: center;
+ }
+ div {
+ }
+ </style>
+ <div class="testclass">Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode(".testclass", inspector);
+ yield testEditClassSelector(view);
+ yield testEditDivSelector(view);
+});
+
+function* testEditClassSelector(view) {
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+ let editor = yield focusEditableField(view, ruleEditor.selectorText);
+
+ editor.input.value = "body";
+ let onRuleViewChanged = once(view, "ruleview-changed");
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ yield onRuleViewChanged;
+
+ // Get the new rule editor that replaced the original
+ ruleEditor = getRuleViewRuleEditor(view, 1);
+ let propEditor = ruleEditor.rule.textProps[0].editor;
+
+ info("Check that the correct rules are visible");
+ is(view._elementStyle.rules.length, 3, "Should have 3 rules.");
+ ok(ruleEditor.element.getAttribute("unmatched"), "Rule editor is unmatched.");
+ is(getRuleViewRule(view, ".testclass"), undefined,
+ "Rule with .testclass selector should not exist.");
+ ok(getRuleViewRule(view, "body"),
+ "Rule with body selector exists.");
+ is(inplaceEditor(propEditor.nameSpan),
+ inplaceEditor(view.styleDocument.activeElement),
+ "Focus should have moved to the property name.");
+}
+
+function* testEditDivSelector(view) {
+ let ruleEditor = getRuleViewRuleEditor(view, 2);
+ let editor = yield focusEditableField(view, ruleEditor.selectorText);
+
+ editor.input.value = "asdf";
+ let onRuleViewChanged = once(view, "ruleview-changed");
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ yield onRuleViewChanged;
+
+ // Get the new rule editor that replaced the original
+ ruleEditor = getRuleViewRuleEditor(view, 2);
+
+ info("Check that the correct rules are visible");
+ is(view._elementStyle.rules.length, 3, "Should have 3 rules.");
+ ok(ruleEditor.element.getAttribute("unmatched"), "Rule editor is unmatched.");
+ is(getRuleViewRule(view, "div"), undefined,
+ "Rule with div selector should not exist.");
+ ok(getRuleViewRule(view, "asdf"),
+ "Rule with asdf selector exists.");
+ is(inplaceEditor(ruleEditor.newPropSpan),
+ inplaceEditor(view.styleDocument.activeElement),
+ "Focus should have moved to the property name.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-selector_07.js b/devtools/client/inspector/rules/test/browser_rules_edit-selector_07.js
new file mode 100644
index 000000000..81c7aad72
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector_07.js
@@ -0,0 +1,62 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the rule view overridden search filter does not appear for an
+// unmatched rule.
+
+const TEST_URI = `
+ <style type="text/css">
+ div {
+ height: 0px;
+ }
+ #testid {
+ height: 1px;
+ }
+ .testclass {
+ height: 10px;
+ }
+ </style>
+ <div id="testid">Styled Node</div>
+ <span class="testclass">This is a span</span>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+
+ yield selectNode("#testid", inspector);
+ yield testEditSelector(view, "span");
+});
+
+function* testEditSelector(view, name) {
+ info("Test editing existing selector fields");
+
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+
+ info("Focusing an existing selector name in the rule-view");
+ let editor = yield focusEditableField(view, ruleEditor.selectorText);
+
+ is(inplaceEditor(ruleEditor.selectorText), editor,
+ "The selector editor got focused");
+
+ info("Entering a new selector name and committing");
+ editor.input.value = name;
+
+ info("Entering the commit key");
+ let onRuleViewChanged = once(view, "ruleview-changed");
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ yield onRuleViewChanged;
+
+ // Get the new rule editor that replaced the original
+ ruleEditor = getRuleViewRuleEditor(view, 1);
+ let rule = ruleEditor.rule;
+ let textPropEditor = rule.textProps[0].editor;
+
+ is(view._elementStyle.rules.length, 3, "Should have 3 rules.");
+ ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists.");
+ ok(ruleEditor.element.getAttribute("unmatched"),
+ "Rule with " + name + " does not match the current element.");
+ ok(textPropEditor.filterProperty.hidden, "Overridden search is hidden.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-selector_08.js b/devtools/client/inspector/rules/test/browser_rules_edit-selector_08.js
new file mode 100644
index 000000000..33382e0de
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector_08.js
@@ -0,0 +1,71 @@
+/* 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 reverting a selector edit does the right thing.
+// Bug 1241046.
+
+const TEST_URI = `
+ <style type="text/css">
+ span {
+ color: chartreuse;
+ }
+ </style>
+ <span>
+ <div id="testid" class="testclass">Styled Node</div>
+ </span>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+
+ info("Selecting the test element");
+ yield selectNode("#testid", inspector);
+
+ let idRuleEditor = getRuleViewRuleEditor(view, 2);
+
+ info("Focusing an existing selector name in the rule-view");
+ let editor = yield focusEditableField(view, idRuleEditor.selectorText);
+
+ is(inplaceEditor(idRuleEditor.selectorText), editor,
+ "The selector editor got focused");
+
+ info("Entering a new selector name and committing");
+ editor.input.value = "pre";
+
+ info("Waiting for rule view to update");
+ let onRuleViewChanged = once(view, "ruleview-changed");
+
+ info("Entering the commit key");
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ yield onRuleViewChanged;
+
+ info("Re-focusing the selector name in the rule-view");
+ idRuleEditor = getRuleViewRuleEditor(view, 2);
+ editor = yield focusEditableField(view, idRuleEditor.selectorText);
+
+ is(view._elementStyle.rules.length, 2, "Should have 2 rules.");
+ ok(getRuleViewRule(view, "pre"), "Rule with pre selector exists.");
+ is(getRuleViewRuleEditor(view, 2).element.getAttribute("unmatched"),
+ "true",
+ "Rule with pre does not match the current element.");
+
+ // Now change it back.
+ info("Re-entering original selector name and committing");
+ editor.input.value = "span";
+
+ info("Waiting for rule view to update");
+ onRuleViewChanged = once(view, "ruleview-changed");
+
+ info("Entering the commit key");
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ yield onRuleViewChanged;
+
+ is(view._elementStyle.rules.length, 2, "Should have 2 rules.");
+ ok(getRuleViewRule(view, "span"), "Rule with span selector exists.");
+ is(getRuleViewRuleEditor(view, 2).element.getAttribute("unmatched"),
+ "false", "Rule with span matches the current element.");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-selector_09.js b/devtools/client/inspector/rules/test/browser_rules_edit-selector_09.js
new file mode 100644
index 000000000..a18ddc5ef
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector_09.js
@@ -0,0 +1,110 @@
+/* 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";
+
+// Tests that editing a selector to an unmatched rule does set up the correct
+// property on the rule, and that settings property in said rule does not
+// lead to overriding properties from matched rules.
+// Test that having a rule with both matched and unmatched selectors does work
+// correctly.
+
+const TEST_URI = `
+ <style type="text/css">
+ #testid {
+ color: black;
+ }
+ .testclass {
+ background-color: white;
+ }
+ </style>
+ <div id="testid">Styled Node</div>
+ <span class="testclass">This is a span</span>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+
+ yield selectNode("#testid", inspector);
+ yield testEditSelector(view, "span");
+ yield testAddImportantProperty(view);
+ yield testAddMatchedRule(view, "span, div");
+});
+
+function* testEditSelector(view, name) {
+ info("Test editing existing selector fields");
+
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+
+ info("Focusing an existing selector name in the rule-view");
+ let editor = yield focusEditableField(view, ruleEditor.selectorText);
+
+ is(inplaceEditor(ruleEditor.selectorText), editor,
+ "The selector editor got focused");
+
+ info("Entering a new selector name and committing");
+ editor.input.value = name;
+
+ info("Waiting for rule view to update");
+ let onRuleViewChanged = once(view, "ruleview-changed");
+
+ info("Entering the commit key");
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ yield onRuleViewChanged;
+
+ ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists.");
+ ok(getRuleViewRuleEditor(view, 1).element.getAttribute("unmatched"),
+ "Rule with " + name + " does not match the current element.");
+
+ // Escape the new property editor after editing the selector
+ let onBlur = once(view.styleDocument.activeElement, "blur");
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, view.styleWindow);
+ yield onBlur;
+}
+
+function* testAddImportantProperty(view) {
+ info("Test creating a new property with !important");
+ let textProp = yield addProperty(view, 1, "color", "red !important");
+
+ is(textProp.value, "red", "Text prop should have been changed.");
+ is(textProp.priority, "important",
+ "Text prop has an \"important\" priority.");
+ ok(!textProp.overridden, "Property should not be overridden");
+
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+ let prop = ruleEditor.rule.textProps[0];
+ ok(!prop.overridden,
+ "Existing property on matched rule should not be overridden");
+}
+
+function* testAddMatchedRule(view, name) {
+ info("Test adding a matching selector");
+
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+
+ info("Focusing an existing selector name in the rule-view");
+ let editor = yield focusEditableField(view, ruleEditor.selectorText);
+
+ is(inplaceEditor(ruleEditor.selectorText), editor,
+ "The selector editor got focused");
+
+ info("Entering a new selector name and committing");
+ editor.input.value = name;
+
+ info("Waiting for rule view to update");
+ let onRuleViewChanged = once(view, "ruleview-changed");
+
+ info("Entering the commit key");
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ yield onRuleViewChanged;
+
+ is(getRuleViewRuleEditor(view, 1).element.getAttribute("unmatched"), "false",
+ "Rule with " + name + " does match the current element.");
+
+ // Escape the new property editor after editing the selector
+ let onBlur = once(view.styleDocument.activeElement, "blur");
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, view.styleWindow);
+ yield onBlur;
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-selector_10.js b/devtools/client/inspector/rules/test/browser_rules_edit-selector_10.js
new file mode 100644
index 000000000..d878dd516
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector_10.js
@@ -0,0 +1,64 @@
+/* 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";
+
+// Regression test for bug 1293616: make sure that editing a selector
+// keeps the rule in the proper position.
+
+const TEST_URI = `
+ <style type="text/css">
+ #testid span, #testid p {
+ background: aqua;
+ }
+ span {
+ background: fuchsia;
+ }
+ </style>
+ <div id="testid">
+ <span class="pickme">
+ Styled Node
+ </span>
+ </div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode(".pickme", inspector);
+ yield testEditSelector(view);
+});
+
+function* testEditSelector(view) {
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+ let editor = yield focusEditableField(view, ruleEditor.selectorText);
+
+ editor.input.value = "#testid span";
+ let onRuleViewChanged = once(view, "ruleview-changed");
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ yield onRuleViewChanged;
+
+ // Escape the new property editor after editing the selector
+ let onBlur = once(view.styleDocument.activeElement, "blur");
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, view.styleWindow);
+ yield onBlur;
+
+ // Get the new rule editor that replaced the original
+ ruleEditor = getRuleViewRuleEditor(view, 1);
+
+ info("Check that the correct rules are visible");
+ is(view._elementStyle.rules.length, 3, "Should have 3 rules.");
+ is(ruleEditor.element.getAttribute("unmatched"), "false", "Rule editor is matched.");
+
+ let props = ruleEditor.rule.textProps;
+ is(props.length, 1, "Rule has correct number of properties");
+ is(props[0].name, "background", "Found background property");
+ ok(!props[0].overridden, "Background property is not overridden");
+
+ ruleEditor = getRuleViewRuleEditor(view, 2);
+ props = ruleEditor.rule.textProps;
+ is(props.length, 1, "Rule has correct number of properties");
+ is(props[0].name, "background", "Found background property");
+ ok(props[0].overridden, "Background property is overridden");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-selector_11.js b/devtools/client/inspector/rules/test/browser_rules_edit-selector_11.js
new file mode 100644
index 000000000..9a1bdc8fa
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector_11.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";
+
+// Regression test for bug 1293616, where editing a selector should
+// change the relative priority of the rule.
+
+const TEST_URI = `
+ <style type="text/css">
+ #testid {
+ background: aqua;
+ }
+ .pickme {
+ background: seagreen;
+ }
+ span {
+ background: fuchsia;
+ }
+ </style>
+ <div>
+ <span id="testid" class="pickme">
+ Styled Node
+ </span>
+ </div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode(".pickme", inspector);
+ yield testEditSelector(view);
+});
+
+function* testEditSelector(view) {
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+ let editor = yield focusEditableField(view, ruleEditor.selectorText);
+
+ editor.input.value = ".pickme";
+ let onRuleViewChanged = once(view, "ruleview-changed");
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ yield onRuleViewChanged;
+
+ // Escape the new property editor after editing the selector
+ let onBlur = once(view.styleDocument.activeElement, "blur");
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, view.styleWindow);
+ yield onBlur;
+
+ // Get the new rule editor that replaced the original
+ ruleEditor = getRuleViewRuleEditor(view, 1);
+
+ info("Check that the correct rules are visible");
+ is(view._elementStyle.rules.length, 4, "Should have 4 rules.");
+ is(ruleEditor.element.getAttribute("unmatched"), "false", "Rule editor is matched.");
+
+ let props = ruleEditor.rule.textProps;
+ is(props.length, 1, "Rule has correct number of properties");
+ is(props[0].name, "background", "Found background property");
+ is(props[0].value, "aqua", "Background property is aqua");
+ ok(props[0].overridden, "Background property is overridden");
+
+ ruleEditor = getRuleViewRuleEditor(view, 2);
+ props = ruleEditor.rule.textProps;
+ is(props.length, 1, "Rule has correct number of properties");
+ is(props[0].name, "background", "Found background property");
+ is(props[0].value, "seagreen", "Background property is seagreen");
+ ok(!props[0].overridden, "Background property is not overridden");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_01.js b/devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_01.js
new file mode 100644
index 000000000..dbf59cba9
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_01.js
@@ -0,0 +1,107 @@
+/* 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";
+
+// Tests that clicking on swatch-preceeded value while editing the property name
+// will result in editing the property value. Also tests that the value span is updated
+// only if the property name has changed. See also Bug 1248274.
+
+const TEST_URI = `
+ <style type="text/css">
+ #testid {
+ color: red;
+ }
+ </style>
+ <div id="testid">Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+
+ yield selectNode("#testid", inspector);
+
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+ let propEditor = ruleEditor.rule.textProps[0].editor;
+
+ yield testColorValueSpanClickWithoutNameChange(propEditor, view);
+ yield testColorValueSpanClickAfterNameChange(propEditor, view);
+});
+
+function* testColorValueSpanClickWithoutNameChange(propEditor, view) {
+ info("Test click on color span while focusing property name editor");
+ let colorSpan = propEditor.valueSpan.querySelector(".ruleview-color");
+
+ info("Focus the color name span");
+ yield focusEditableField(view, propEditor.nameSpan);
+ let editor = inplaceEditor(propEditor.doc.activeElement);
+
+ // We add a click event to make sure the color span won't be cleared
+ // on nameSpan blur (which would lead to the click event not being triggered)
+ let onColorSpanClick = once(colorSpan, "click");
+
+ // The property-value-updated is emitted when the valueSpan markup is being
+ // re-populated, which should not be the case when not modifying the property name
+ let onPropertyValueUpdated = function () {
+ ok(false, "The \"property-value-updated\" should not be emitted");
+ };
+ view.on("property-value-updated", onPropertyValueUpdated);
+
+ info("blur propEditor.nameSpan by clicking on the color span");
+ EventUtils.synthesizeMouse(colorSpan, 1, 1, {}, propEditor.doc.defaultView);
+
+ info("wait for the click event on the color span");
+ yield onColorSpanClick;
+ ok(true, "Expected click event was emitted");
+
+ editor = inplaceEditor(propEditor.doc.activeElement);
+ is(inplaceEditor(propEditor.valueSpan), editor,
+ "The property value editor got focused");
+
+ // We remove this listener in order to not cause unwanted conflict in the next test
+ view.off("property-value-updated", onPropertyValueUpdated);
+
+ info("blur valueSpan editor to trigger ruleview-changed event and prevent " +
+ "having pending request");
+ let onRuleViewChanged = view.once("ruleview-changed");
+ editor.input.blur();
+ yield onRuleViewChanged;
+}
+
+function* testColorValueSpanClickAfterNameChange(propEditor, view) {
+ info("Test click on color span after property name change");
+ let colorSpan = propEditor.valueSpan.querySelector(".ruleview-color");
+
+ info("Focus the color name span");
+ yield focusEditableField(view, propEditor.nameSpan);
+ let editor = inplaceEditor(propEditor.doc.activeElement);
+
+ info("Modify the property to border-color to trigger the " +
+ "property-value-updated event");
+ editor.input.value = "border-color";
+
+ let onRuleViewChanged = view.once("ruleview-changed");
+ let onPropertyValueUpdate = view.once("property-value-updated");
+
+ info("blur propEditor.nameSpan by clicking on the color span");
+ EventUtils.synthesizeMouse(colorSpan, 1, 1, {}, propEditor.doc.defaultView);
+
+ info("wait for ruleview-changed event to be triggered to prevent pending requests");
+ yield onRuleViewChanged;
+
+ info("wait for the property value to be updated");
+ yield onPropertyValueUpdate;
+ ok(true, "Expected \"property-value-updated\" event was emitted");
+
+ editor = inplaceEditor(propEditor.doc.activeElement);
+ is(inplaceEditor(propEditor.valueSpan), editor,
+ "The property value editor got focused");
+
+ info("blur valueSpan editor to trigger ruleview-changed event and prevent " +
+ "having pending request");
+ onRuleViewChanged = view.once("ruleview-changed");
+ editor.input.blur();
+ yield onRuleViewChanged;
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_02.js b/devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_02.js
new file mode 100644
index 000000000..372ed7477
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_02.js
@@ -0,0 +1,65 @@
+/* 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";
+
+// Tests that hitting shift + click on color swatch while editing the property
+// name will only change the color unit and not lead to edit the property value.
+// See also Bug 1248274.
+
+const TEST_URI = `
+ <style type="text/css">
+ #testid {
+ color: red;
+ background: linear-gradient(
+ 90deg,
+ rgb(183,222,237),
+ rgb(33,180,226),
+ rgb(31,170,217),
+ rgba(200,170,140,0.5));
+ }
+ </style>
+ <div id="testid">Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+
+ info("Test shift + click on color swatch while editing property name");
+
+ yield selectNode("#testid", inspector);
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+ let propEditor = ruleEditor.rule.textProps[1].editor;
+ let swatchSpan = propEditor.valueSpan.querySelectorAll(".ruleview-colorswatch")[2];
+
+ info("Focus the background name span");
+ yield focusEditableField(view, propEditor.nameSpan);
+ let editor = inplaceEditor(propEditor.doc.activeElement);
+
+ info("Modify the property to background-image to trigger the " +
+ "property-value-updated event");
+ editor.input.value = "background-image";
+
+ let onPropertyValueUpdate = view.once("property-value-updated");
+ let onSwatchUnitChange = swatchSpan.once("unit-change");
+ let onRuleViewChanged = view.once("ruleview-changed");
+
+ info("blur propEditor.nameSpan by clicking on the color swatch");
+ EventUtils.synthesizeMouseAtCenter(swatchSpan, {shiftKey: true},
+ propEditor.doc.defaultView);
+
+ info("wait for ruleview-changed event to be triggered to prevent pending requests");
+ yield onRuleViewChanged;
+
+ info("wait for the color unit to change");
+ yield onSwatchUnitChange;
+ ok(true, "the color unit was changed");
+
+ info("wait for the property value to be updated");
+ yield onPropertyValueUpdate;
+
+ ok(!inplaceEditor(propEditor.valueSpan), "The inplace editor wasn't shown " +
+ "as a result of the color swatch shift + click");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_03.js b/devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_03.js
new file mode 100644
index 000000000..041a45a3e
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_03.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";
+
+// Tests that clicking on color swatch while editing the property name
+// will show the color tooltip with the correct value. See also Bug 1248274.
+
+const TEST_URI = `
+ <style type="text/css">
+ #testid {
+ color: red;
+ background: linear-gradient(
+ 90deg,
+ rgb(183,222,237),
+ rgb(33,180,226),
+ rgb(31,170,217),
+ rgba(200,170,140,0.5));
+ }
+ </style>
+ <div id="testid">Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+
+ info("Test click on color swatch while editing property name");
+
+ yield selectNode("#testid", inspector);
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+ let propEditor = ruleEditor.rule.textProps[1].editor;
+ let swatchSpan = propEditor.valueSpan.querySelectorAll(
+ ".ruleview-colorswatch")[3];
+ let colorPicker = view.tooltips.colorPicker;
+
+ info("Focus the background name span");
+ yield focusEditableField(view, propEditor.nameSpan);
+ let editor = inplaceEditor(propEditor.doc.activeElement);
+
+ info("Modify the background property to background-image to trigger the " +
+ "property-value-updated event");
+ editor.input.value = "background-image";
+
+ let onRuleViewChanged = view.once("ruleview-changed");
+ let onPropertyValueUpdate = view.once("property-value-updated");
+ let onReady = colorPicker.once("ready");
+
+ info("blur propEditor.nameSpan by clicking on the color swatch");
+ EventUtils.synthesizeMouseAtCenter(swatchSpan, {},
+ propEditor.doc.defaultView);
+
+ info("wait for ruleview-changed event to be triggered to prevent pending requests");
+ yield onRuleViewChanged;
+
+ info("wait for the property value to be updated");
+ yield onPropertyValueUpdate;
+
+ info("wait for the color picker to be shown");
+ yield onReady;
+
+ ok(true, "The color picker was shown on click of the color swatch");
+ ok(!inplaceEditor(propEditor.valueSpan),
+ "The inplace editor wasn't shown as a result of the color swatch click");
+
+ let spectrum = colorPicker.spectrum;
+ is(spectrum.rgb, "200,170,140,0.5", "The correct color picker was shown");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_04.js b/devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_04.js
new file mode 100644
index 000000000..fa4d8e6e2
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_04.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";
+
+// Tests that clicking on a property's value URL while editing the property name
+// will open the link in a new tab. See also Bug 1248274.
+
+const TEST_URI = `
+ <style type="text/css">
+ #testid {
+ background: url("chrome://global/skin/icons/warning-64.png"), linear-gradient(white, #F06 400px);
+ }
+ </style>
+ <div id="testid">Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+
+ info("Test click on background-image url while editing property name");
+
+ yield selectNode("#testid", inspector);
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+ let propEditor = ruleEditor.rule.textProps[0].editor;
+ let anchor = propEditor.valueSpan.querySelector(".ruleview-propertyvalue .theme-link");
+
+ info("Focus the background name span");
+ yield focusEditableField(view, propEditor.nameSpan);
+ let editor = inplaceEditor(propEditor.doc.activeElement);
+
+ info("Modify the property to background to trigger the " +
+ "property-value-updated event");
+ editor.input.value = "background-image";
+
+ let onRuleViewChanged = view.once("ruleview-changed");
+ let onPropertyValueUpdate = view.once("property-value-updated");
+ let onTabOpened = waitForTab();
+
+ info("blur propEditor.nameSpan by clicking on the link");
+ // The url can be wrapped across multiple lines, and so we click the lower left corner
+ // of the anchor to make sure to target the link.
+ let rect = anchor.getBoundingClientRect();
+ EventUtils.synthesizeMouse(anchor, 2, rect.height - 2, {}, propEditor.doc.defaultView);
+
+ info("wait for ruleview-changed event to be triggered to prevent pending requests");
+ yield onRuleViewChanged;
+
+ info("wait for the property value to be updated");
+ yield onPropertyValueUpdate;
+
+ info("wait for the image to be open in a new tab");
+ let tab = yield onTabOpened;
+ ok(true, "A new tab opened");
+
+ is(tab.linkedBrowser.currentURI.spec, anchor.href,
+ "The URL for the new tab is correct");
+
+ gBrowser.removeTab(tab);
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_editable-field-focus_01.js b/devtools/client/inspector/rules/test/browser_rules_editable-field-focus_01.js
new file mode 100644
index 000000000..c9c7cd3d2
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_editable-field-focus_01.js
@@ -0,0 +1,94 @@
+/* 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";
+
+// Tests that the correct editable fields are focused when tabbing and entering
+// through the rule view.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #testid {
+ background-color: blue;
+ color: red;
+ margin: 0;
+ padding: 0;
+ }
+ div {
+ border-color: red
+ }
+ </style>
+ <div id='testid'>Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+ yield testEditableFieldFocus(inspector, view, "VK_RETURN");
+ yield testEditableFieldFocus(inspector, view, "VK_TAB");
+});
+
+function* testEditableFieldFocus(inspector, view, commitKey) {
+ info("Click on the selector of the inline style ('element')");
+ let ruleEditor = getRuleViewRuleEditor(view, 0);
+ let onFocus = once(ruleEditor.element, "focus", true);
+ ruleEditor.selectorText.click();
+ yield onFocus;
+ assertEditor(view, ruleEditor.newPropSpan,
+ "Focus should be in the element property span");
+
+ info("Focus the next field with " + commitKey);
+ ruleEditor = getRuleViewRuleEditor(view, 1);
+ yield focusNextEditableField(view, ruleEditor, commitKey);
+ assertEditor(view, ruleEditor.selectorText,
+ "Focus should have moved to the next rule selector");
+
+ for (let i = 0; i < ruleEditor.rule.textProps.length; i++) {
+ let textProp = ruleEditor.rule.textProps[i];
+ let propEditor = textProp.editor;
+
+ info("Focus the next field with " + commitKey);
+ // Expect a ruleview-changed event if we are moving from a property value
+ // to the next property name (which occurs after the first iteration, as for
+ // i=0, the previous field is the selector).
+ let onRuleViewChanged = i > 0 ? view.once("ruleview-changed") : null;
+ yield focusNextEditableField(view, ruleEditor, commitKey);
+ yield onRuleViewChanged;
+ assertEditor(view, propEditor.nameSpan,
+ "Focus should have moved to the property name");
+
+ info("Focus the next field with " + commitKey);
+ yield focusNextEditableField(view, ruleEditor, commitKey);
+ assertEditor(view, propEditor.valueSpan,
+ "Focus should have moved to the property value");
+ }
+
+ // Expect a ruleview-changed event again as we're bluring a property value.
+ let onRuleViewChanged = view.once("ruleview-changed");
+ yield focusNextEditableField(view, ruleEditor, commitKey);
+ yield onRuleViewChanged;
+ assertEditor(view, ruleEditor.newPropSpan,
+ "Focus should have moved to the new property span");
+
+ ruleEditor = getRuleViewRuleEditor(view, 2);
+
+ yield focusNextEditableField(view, ruleEditor, commitKey);
+ assertEditor(view, ruleEditor.selectorText,
+ "Focus should have moved to the next rule selector");
+
+ info("Blur the selector field");
+ EventUtils.synthesizeKey("VK_ESCAPE", {});
+}
+
+function* focusNextEditableField(view, ruleEditor, commitKey) {
+ let onFocus = once(ruleEditor.element, "focus", true);
+ EventUtils.synthesizeKey(commitKey, {}, view.styleWindow);
+ yield onFocus;
+}
+
+function assertEditor(view, element, message) {
+ let editor = inplaceEditor(view.styleDocument.activeElement);
+ is(inplaceEditor(element), editor, message);
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_editable-field-focus_02.js b/devtools/client/inspector/rules/test/browser_rules_editable-field-focus_02.js
new file mode 100644
index 000000000..13ad221f0
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_editable-field-focus_02.js
@@ -0,0 +1,84 @@
+/* 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";
+
+// Tests that the correct editable fields are focused when shift tabbing
+// through the rule view.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #testid {
+ background-color: blue;
+ color: red;
+ margin: 0;
+ padding: 0;
+ }
+ div {
+ border-color: red
+ }
+ </style>
+ <div id='testid'>Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+ yield testEditableFieldFocus(inspector, view, "VK_TAB", { shiftKey: true });
+});
+
+function* testEditableFieldFocus(inspector, view, commitKey, options = {}) {
+ let ruleEditor = getRuleViewRuleEditor(view, 2);
+ let editor = yield focusEditableField(view, ruleEditor.selectorText);
+ is(inplaceEditor(ruleEditor.selectorText), editor,
+ "Focus should be in the 'div' rule selector");
+
+ ruleEditor = getRuleViewRuleEditor(view, 1);
+
+ yield focusNextField(view, ruleEditor, commitKey, options);
+ assertEditor(view, ruleEditor.newPropSpan,
+ "Focus should have moved to the new property span");
+
+ for (let textProp of ruleEditor.rule.textProps.slice(0).reverse()) {
+ let propEditor = textProp.editor;
+
+ yield focusNextField(view, ruleEditor, commitKey, options);
+ yield assertEditor(view, propEditor.valueSpan,
+ "Focus should have moved to the property value");
+
+ yield focusNextFieldAndExpectChange(view, ruleEditor, commitKey, options);
+ yield assertEditor(view, propEditor.nameSpan,
+ "Focus should have moved to the property name");
+ }
+
+ ruleEditor = getRuleViewRuleEditor(view, 1);
+
+ yield focusNextField(view, ruleEditor, commitKey, options);
+ yield assertEditor(view, ruleEditor.selectorText,
+ "Focus should have moved to the '#testid' rule selector");
+
+ ruleEditor = getRuleViewRuleEditor(view, 0);
+
+ yield focusNextField(view, ruleEditor, commitKey, options);
+ assertEditor(view, ruleEditor.newPropSpan,
+ "Focus should have moved to the new property span");
+}
+
+function* focusNextFieldAndExpectChange(view, ruleEditor, commitKey, options) {
+ let onRuleViewChanged = view.once("ruleview-changed");
+ yield focusNextField(view, ruleEditor, commitKey, options);
+ yield onRuleViewChanged;
+}
+
+function* focusNextField(view, ruleEditor, commitKey, options) {
+ let onFocus = once(ruleEditor.element, "focus", true);
+ EventUtils.synthesizeKey(commitKey, options, view.styleWindow);
+ yield onFocus;
+}
+
+function* assertEditor(view, element, message) {
+ let editor = inplaceEditor(view.styleDocument.activeElement);
+ is(inplaceEditor(element), editor, message);
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_eyedropper.js b/devtools/client/inspector/rules/test/browser_rules_eyedropper.js
new file mode 100644
index 000000000..0762066e3
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_eyedropper.js
@@ -0,0 +1,123 @@
+/* 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";
+
+// Test opening the eyedropper from the color picker. Pressing escape to close it, and
+// clicking the page to select a color.
+
+const TEST_URI = `
+ <style type="text/css">
+ body {
+ background-color: white;
+ padding: 0px
+ }
+
+ #div1 {
+ background-color: #ff5;
+ width: 20px;
+ height: 20px;
+ }
+
+ #div2 {
+ margin-left: 20px;
+ width: 20px;
+ height: 20px;
+ background-color: #f09;
+ }
+ </style>
+ <body><div id="div1"></div><div id="div2"></div></body>
+`;
+
+// #f09
+const ORIGINAL_COLOR = "rgb(255, 0, 153)";
+// #ff5
+const EXPECTED_COLOR = "rgb(255, 255, 85)";
+
+add_task(function* () {
+ info("Add the test tab, open the rule-view and select the test node");
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {testActor, inspector, view} = yield openRuleView();
+ yield selectNode("#div2", inspector);
+
+ info("Get the background-color property from the rule-view");
+ let property = getRuleViewProperty(view, "#div2", "background-color");
+ let swatch = property.valueSpan.querySelector(".ruleview-colorswatch");
+ ok(swatch, "Color swatch is displayed for the bg-color property");
+
+ info("Open the eyedropper from the colorpicker tooltip");
+ yield openEyedropper(view, swatch);
+
+ let tooltip = view.tooltips.colorPicker.tooltip;
+ ok(!tooltip.isVisible(), "color picker tooltip is closed after opening eyedropper");
+
+ info("Test that pressing escape dismisses the eyedropper");
+ yield testESC(swatch, inspector, testActor);
+
+ info("Open the eyedropper again");
+ yield openEyedropper(view, swatch);
+
+ info("Test that a color can be selected with the eyedropper");
+ yield testSelect(view, swatch, inspector, testActor);
+
+ let onHidden = tooltip.once("hidden");
+ tooltip.hide();
+ yield onHidden;
+ ok(!tooltip.isVisible(), "color picker tooltip is closed");
+
+ yield waitForTick();
+});
+
+function* testESC(swatch, inspector, testActor) {
+ info("Press escape");
+ let onCanceled = new Promise(resolve => {
+ inspector.inspector.once("color-pick-canceled", resolve);
+ });
+ yield testActor.synthesizeKey({key: "VK_ESCAPE", options: {}});
+ yield onCanceled;
+
+ let color = swatch.style.backgroundColor;
+ is(color, ORIGINAL_COLOR, "swatch didn't change after pressing ESC");
+}
+
+function* testSelect(view, swatch, inspector, testActor) {
+ info("Click at x:10px y:10px");
+ let onPicked = new Promise(resolve => {
+ inspector.inspector.once("color-picked", resolve);
+ });
+ // The change to the content is done async after rule view change
+ let onRuleViewChanged = view.once("ruleview-changed");
+
+ yield testActor.synthesizeMouse({selector: "html", x: 10, y: 10,
+ options: {type: "mousemove"}});
+ yield testActor.synthesizeMouse({selector: "html", x: 10, y: 10,
+ options: {type: "mousedown"}});
+ yield testActor.synthesizeMouse({selector: "html", x: 10, y: 10,
+ options: {type: "mouseup"}});
+
+ yield onPicked;
+ yield onRuleViewChanged;
+
+ let color = swatch.style.backgroundColor;
+ is(color, EXPECTED_COLOR, "swatch changed colors");
+
+ is((yield getComputedStyleProperty("div", null, "background-color")),
+ EXPECTED_COLOR,
+ "div's color set to body color after dropper");
+}
+
+function* openEyedropper(view, swatch) {
+ let tooltip = view.tooltips.colorPicker.tooltip;
+
+ info("Click on the swatch");
+ let onColorPickerReady = view.tooltips.colorPicker.once("ready");
+ swatch.click();
+ yield onColorPickerReady;
+
+ let dropperButton = tooltip.doc.querySelector("#eyedropper-button");
+
+ info("Click on the eyedropper icon");
+ let onOpened = tooltip.once("eyedropper-opened");
+ dropperButton.click();
+ yield onOpened;
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_filtereditor-appears-on-swatch-click.js b/devtools/client/inspector/rules/test/browser_rules_filtereditor-appears-on-swatch-click.js
new file mode 100644
index 000000000..21eeebb36
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_filtereditor-appears-on-swatch-click.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests the that Filter Editor Tooltip opens by clicking on filter swatches
+
+const TEST_URL = URL_ROOT + "doc_filter.html";
+
+add_task(function* () {
+ yield addTab(TEST_URL);
+
+ let {view} = yield openRuleView();
+
+ info("Getting the filter swatch element");
+ let swatch = getRuleViewProperty(view, "body", "filter").valueSpan
+ .querySelector(".ruleview-filterswatch");
+
+ let filterTooltip = view.tooltips.filterEditor;
+ // Clicking on a cssfilter swatch sets the current filter value in the tooltip
+ // which, in turn, makes the FilterWidget emit an "updated" event that causes
+ // the rule-view to refresh. So we must wait for the ruleview-changed event.
+ let onRuleViewChanged = view.once("ruleview-changed");
+ swatch.click();
+ yield onRuleViewChanged;
+
+ ok(true, "The shown event was emitted after clicking on swatch");
+ ok(!inplaceEditor(swatch.parentNode),
+ "The inplace editor wasn't shown as a result of the filter swatch click");
+
+ yield hideTooltipAndWaitForRuleViewChanged(filterTooltip, view);
+
+ yield waitForTick();
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_filtereditor-commit-on-ENTER.js b/devtools/client/inspector/rules/test/browser_rules_filtereditor-commit-on-ENTER.js
new file mode 100644
index 000000000..127a20843
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_filtereditor-commit-on-ENTER.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests the Filter Editor Tooltip committing changes on ENTER
+
+const TEST_URL = URL_ROOT + "doc_filter.html";
+
+add_task(function* () {
+ yield addTab(TEST_URL);
+ let {view} = yield openRuleView();
+
+ info("Get the filter swatch element");
+ let swatch = getRuleViewProperty(view, "body", "filter").valueSpan
+ .querySelector(".ruleview-filterswatch");
+
+ info("Click on the filter swatch element");
+ // Clicking on a cssfilter swatch sets the current filter value in the tooltip
+ // which, in turn, makes the FilterWidget emit an "updated" event that causes
+ // the rule-view to refresh. So we must wait for the ruleview-changed event.
+ let onRuleViewChanged = view.once("ruleview-changed");
+ swatch.click();
+ yield onRuleViewChanged;
+
+ info("Get the cssfilter widget instance");
+ let filterTooltip = view.tooltips.filterEditor;
+ let widget = filterTooltip.widget;
+
+ info("Set a new value in the cssfilter widget");
+ onRuleViewChanged = view.once("ruleview-changed");
+ widget.setCssValue("blur(2px)");
+ yield waitForComputedStyleProperty("body", null, "filter", "blur(2px)");
+ yield onRuleViewChanged;
+ ok(true, "Changes previewed on the element");
+
+ info("Press RETURN to commit changes");
+ // Pressing return in the cssfilter tooltip triggeres 2 ruleview-changed
+ onRuleViewChanged = waitForNEvents(view, "ruleview-changed", 2);
+ EventUtils.sendKey("RETURN", widget.styleWindow);
+ yield onRuleViewChanged;
+
+ is((yield getComputedStyleProperty("body", null, "filter")), "blur(2px)",
+ "The elemenet's filter was kept after RETURN");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_filtereditor-revert-on-ESC.js b/devtools/client/inspector/rules/test/browser_rules_filtereditor-revert-on-ESC.js
new file mode 100644
index 000000000..0302f40a9
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_filtereditor-revert-on-ESC.js
@@ -0,0 +1,118 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that changes made to the Filter Editor Tooltip are reverted when
+// ESC is pressed
+
+const TEST_URL = URL_ROOT + "doc_filter.html";
+
+add_task(function* () {
+ yield addTab(TEST_URL);
+ let {view} = yield openRuleView();
+ yield testPressingEscapeRevertsChanges(view);
+ yield testPressingEscapeRevertsChangesAndDisables(view);
+});
+
+function* testPressingEscapeRevertsChanges(view) {
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+ let propEditor = ruleEditor.rule.textProps[0].editor;
+ let swatch = propEditor.valueSpan.querySelector(".ruleview-filterswatch");
+
+ yield clickOnFilterSwatch(swatch, view);
+ yield setValueInFilterWidget("blur(2px)", view);
+
+ yield waitForComputedStyleProperty("body", null, "filter", "blur(2px)");
+ is(propEditor.valueSpan.textContent, "blur(2px)",
+ "Got expected property value.");
+
+ yield pressEscapeToCloseTooltip(view);
+
+ yield waitForComputedStyleProperty("body", null, "filter",
+ "blur(2px) contrast(2)");
+ is(propEditor.valueSpan.textContent, "blur(2px) contrast(2)",
+ "Got expected property value.");
+}
+
+function* testPressingEscapeRevertsChangesAndDisables(view) {
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+ let propEditor = ruleEditor.rule.textProps[0].editor;
+
+ info("Disabling filter property");
+ let onRuleViewChanged = view.once("ruleview-changed");
+ propEditor.enable.click();
+ yield onRuleViewChanged;
+
+ ok(propEditor.element.classList.contains("ruleview-overridden"),
+ "property is overridden.");
+ is(propEditor.enable.style.visibility, "visible",
+ "property enable checkbox is visible.");
+ ok(!propEditor.enable.getAttribute("checked"),
+ "property enable checkbox is not checked.");
+ ok(!propEditor.prop.enabled,
+ "filter property is disabled.");
+ let newValue = yield getRulePropertyValue("filter");
+ is(newValue, "", "filter should have been unset.");
+
+ let swatch = propEditor.valueSpan.querySelector(".ruleview-filterswatch");
+ yield clickOnFilterSwatch(swatch, view);
+
+ ok(!propEditor.element.classList.contains("ruleview-overridden"),
+ "property overridden is not displayed.");
+ is(propEditor.enable.style.visibility, "hidden",
+ "property enable checkbox is hidden.");
+
+ yield setValueInFilterWidget("blur(2px)", view);
+ yield pressEscapeToCloseTooltip(view);
+
+ ok(propEditor.element.classList.contains("ruleview-overridden"),
+ "property is overridden.");
+ is(propEditor.enable.style.visibility, "visible",
+ "property enable checkbox is visible.");
+ ok(!propEditor.enable.getAttribute("checked"),
+ "property enable checkbox is not checked.");
+ ok(!propEditor.prop.enabled, "filter property is disabled.");
+ newValue = yield getRulePropertyValue("filter");
+ is(newValue, "", "filter should have been unset.");
+ is(propEditor.valueSpan.textContent, "blur(2px) contrast(2)",
+ "Got expected property value.");
+}
+
+function* getRulePropertyValue(name) {
+ let propValue = yield executeInContent("Test:GetRulePropertyValue", {
+ styleSheetIndex: 0,
+ ruleIndex: 0,
+ name: name
+ });
+ return propValue;
+}
+
+function* clickOnFilterSwatch(swatch, view) {
+ info("Clicking on a css filter swatch to open the tooltip");
+
+ // Clicking on a cssfilter swatch sets the current filter value in the tooltip
+ // which, in turn, makes the FilterWidget emit an "updated" event that causes
+ // the rule-view to refresh. So we must wait for the ruleview-changed event.
+ let onRuleViewChanged = view.once("ruleview-changed");
+ swatch.click();
+ yield onRuleViewChanged;
+}
+
+function* setValueInFilterWidget(value, view) {
+ info("Setting the CSS filter value in the tooltip");
+
+ let filterTooltip = view.tooltips.filterEditor;
+ let onRuleViewChanged = view.once("ruleview-changed");
+ filterTooltip.widget.setCssValue(value);
+ yield onRuleViewChanged;
+}
+
+function* pressEscapeToCloseTooltip(view) {
+ info("Pressing ESCAPE to close the tooltip");
+
+ let filterTooltip = view.tooltips.filterEditor;
+ let onRuleViewChanged = view.once("ruleview-changed");
+ EventUtils.sendKey("ESCAPE", filterTooltip.widget.styleWindow);
+ yield onRuleViewChanged;
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_grid-highlighter-on-navigate.js b/devtools/client/inspector/rules/test/browser_rules_grid-highlighter-on-navigate.js
new file mode 100644
index 000000000..617eb00da
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_grid-highlighter-on-navigate.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 that grid highlighter is hidden on page navigation.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #grid {
+ display: grid;
+ }
+ </style>
+ <div id="grid">
+ <div id="cell1">cell1</div>
+ <div id="cell2">cell2</div>
+ </div>
+`;
+
+const TEST_URI_2 = "data:text/html,<html><body>test</body></html>";
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ let highlighters = view.highlighters;
+
+ yield selectNode("#grid", inspector);
+ let container = getRuleViewProperty(view, "#grid", "display").valueSpan;
+ let gridToggle = container.querySelector(".ruleview-grid");
+
+ info("Toggling ON the CSS grid highlighter from the rule-view.");
+ let onHighlighterShown = highlighters.once("highlighter-shown");
+ gridToggle.click();
+ yield onHighlighterShown;
+
+ ok(highlighters.gridHighlighterShown, "CSS grid highlighter is shown.");
+
+ yield navigateTo(inspector, TEST_URI_2);
+ ok(!highlighters.gridHighlighterShown, "CSS grid highlighter is hidden.");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_grid-highlighter-on-reload.js b/devtools/client/inspector/rules/test/browser_rules_grid-highlighter-on-reload.js
new file mode 100644
index 000000000..a6780a94a
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_grid-highlighter-on-reload.js
@@ -0,0 +1,53 @@
+/* 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 grid highlighter showing grid gaps can be displayed after reloading the
+// page (Bug 1342051).
+
+const TEST_URI = `
+ <style type='text/css'>
+ #grid {
+ display: grid;
+ grid-gap: 10px;
+ }
+ </style>
+ <div id="grid">
+ <div id="cell1">cell1</div>
+ <div id="cell2">cell2</div>
+ </div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+
+ info("Check that the grid highlighter can be displayed");
+ yield checkGridHighlighter();
+
+ info("Close the toolbox before reloading the tab");
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ yield gDevTools.closeToolbox(target);
+
+ yield refreshTab(gBrowser.selectedTab);
+
+ info("Check that the grid highlighter can be displayed after reloading the page");
+ yield checkGridHighlighter();
+});
+
+function* checkGridHighlighter() {
+ let {inspector, view} = yield openRuleView();
+ let {highlighters} = view;
+
+ yield selectNode("#grid", inspector);
+ let container = getRuleViewProperty(view, "#grid", "display").valueSpan;
+ let gridToggle = container.querySelector(".ruleview-grid");
+
+ info("Toggling ON the CSS grid highlighter from the rule-view.");
+ let onHighlighterShown = highlighters.once("highlighter-shown");
+ gridToggle.click();
+ yield onHighlighterShown;
+
+ ok(highlighters.gridHighlighterShown, "CSS grid highlighter is shown.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_grid-toggle_01.js b/devtools/client/inspector/rules/test/browser_rules_grid-toggle_01.js
new file mode 100644
index 000000000..04534522b
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_grid-toggle_01.js
@@ -0,0 +1,64 @@
+/* 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 toggling the grid highlighter in the rule view and the display of the
+// grid highlighter.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #grid {
+ display: grid;
+ }
+ </style>
+ <div id="grid">
+ <div id="cell1">cell1</div>
+ <div id="cell2">cell2</div>
+ </div>
+`;
+
+const HIGHLIGHTER_TYPE = "CssGridHighlighter";
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ let highlighters = view.highlighters;
+
+ yield selectNode("#grid", inspector);
+ let container = getRuleViewProperty(view, "#grid", "display").valueSpan;
+ let gridToggle = container.querySelector(".ruleview-grid");
+
+ info("Checking the initial state of the CSS grid toggle in the rule-view.");
+ ok(gridToggle, "Grid highlighter toggle is visible.");
+ ok(!gridToggle.classList.contains("active"),
+ "Grid highlighter toggle button is not active.");
+ ok(!highlighters.highlighters[HIGHLIGHTER_TYPE],
+ "No CSS grid highlighter exists in the rule-view.");
+ ok(!highlighters.gridHighlighterShown, "No CSS grid highlighter is shown.");
+
+ info("Toggling ON the CSS grid highlighter from the rule-view.");
+ let onHighlighterShown = highlighters.once("highlighter-shown");
+ gridToggle.click();
+ yield onHighlighterShown;
+
+ info("Checking the CSS grid highlighter is created and toggle button is active in " +
+ "the rule-view.");
+ ok(gridToggle.classList.contains("active"),
+ "Grid highlighter toggle is active.");
+ ok(highlighters.highlighters[HIGHLIGHTER_TYPE],
+ "CSS grid highlighter created in the rule-view.");
+ ok(highlighters.gridHighlighterShown, "CSS grid highlighter is shown.");
+
+ info("Toggling OFF the CSS grid highlighter from the rule-view.");
+ let onHighlighterHidden = highlighters.once("highlighter-hidden");
+ gridToggle.click();
+ yield onHighlighterHidden;
+
+ info("Checking the CSS grid highlighter is not shown and toggle button is not active " +
+ "in the rule-view.");
+ ok(!gridToggle.classList.contains("active"),
+ "Grid highlighter toggle button is not active.");
+ ok(!highlighters.gridHighlighterShown, "No CSS grid highlighter is shown.");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_grid-toggle_02.js b/devtools/client/inspector/rules/test/browser_rules_grid-toggle_02.js
new file mode 100644
index 000000000..5c339e892
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_grid-toggle_02.js
@@ -0,0 +1,73 @@
+/* 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 toggling the grid highlighter in the rule view from an overridden 'display: grid'
+// declaration.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #grid {
+ display: grid;
+ }
+ div, ul {
+ display: grid;
+ }
+ </style>
+ <ul id="grid">
+ <li id="cell1">cell1</li>
+ <li id="cell2">cell2</li>
+ </ul>
+`;
+
+const HIGHLIGHTER_TYPE = "CssGridHighlighter";
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ let highlighters = view.highlighters;
+
+ yield selectNode("#grid", inspector);
+ let container = getRuleViewProperty(view, "#grid", "display").valueSpan;
+ let gridToggle = container.querySelector(".ruleview-grid");
+ let overriddenContainer = getRuleViewProperty(view, "div, ul", "display").valueSpan;
+ let overriddenGridToggle = overriddenContainer.querySelector(".ruleview-grid");
+
+ info("Checking the initial state of the CSS grid toggle in the rule-view.");
+ ok(gridToggle && overriddenGridToggle, "Grid highlighter toggles are visible.");
+ ok(!gridToggle.classList.contains("active") &&
+ !overriddenGridToggle.classList.contains("active"),
+ "Grid highlighter toggle buttons are not active.");
+ ok(!highlighters.highlighters[HIGHLIGHTER_TYPE],
+ "No CSS grid highlighter exists in the rule-view.");
+ ok(!highlighters.gridHighlighterShown, "No CSS grid highlighter is shown.");
+
+ info("Toggling ON the CSS grid highlighter from the overridden rule in the rule-view.");
+ let onHighlighterShown = highlighters.once("highlighter-shown");
+ overriddenGridToggle.click();
+ yield onHighlighterShown;
+
+ info("Checking the CSS grid highlighter is created and toggle buttons are active in " +
+ "the rule-view.");
+ ok(gridToggle.classList.contains("active") &&
+ overriddenGridToggle.classList.contains("active"),
+ "Grid highlighter toggle is active.");
+ ok(highlighters.highlighters[HIGHLIGHTER_TYPE],
+ "CSS grid highlighter created in the rule-view.");
+ ok(highlighters.gridHighlighterShown, "CSS grid highlighter is shown.");
+
+ info("Toggling off the CSS grid highlighter from the normal grid declaration in the " +
+ "rule-view.");
+ let onHighlighterHidden = highlighters.once("highlighter-hidden");
+ gridToggle.click();
+ yield onHighlighterHidden;
+
+ info("Checking the CSS grid highlighter is not shown and toggle buttons are not " +
+ "active in the rule-view.");
+ ok(!gridToggle.classList.contains("active") &&
+ !overriddenGridToggle.classList.contains("active"),
+ "Grid highlighter toggle buttons are not active.");
+ ok(!highlighters.gridHighlighterShown, "No CSS grid highlighter is shown.");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_grid-toggle_03.js b/devtools/client/inspector/rules/test/browser_rules_grid-toggle_03.js
new file mode 100644
index 000000000..a908d6a97
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_grid-toggle_03.js
@@ -0,0 +1,96 @@
+/* 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 toggling the grid highlighter in the rule view with multiple grids in the page.
+
+const TEST_URI = `
+ <style type='text/css'>
+ .grid {
+ display: grid;
+ }
+ </style>
+ <div id="grid1" class="grid">
+ <div class="cell1">cell1</div>
+ <div class="cell2">cell2</div>
+ </div>
+ <div id="grid2" class="grid">
+ <div class="cell1">cell1</div>
+ <div class="cell2">cell2</div>
+ </div>
+`;
+
+const HIGHLIGHTER_TYPE = "CssGridHighlighter";
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ let highlighters = view.highlighters;
+
+ info("Selecting the first grid container.");
+ yield selectNode("#grid1", inspector);
+ let container = getRuleViewProperty(view, ".grid", "display").valueSpan;
+ let gridToggle = container.querySelector(".ruleview-grid");
+
+ info("Checking the state of the CSS grid toggle for the first grid container in the " +
+ "rule-view.");
+ ok(gridToggle, "Grid highlighter toggle is visible.");
+ ok(!gridToggle.classList.contains("active"),
+ "Grid highlighter toggle button is not active.");
+ ok(!highlighters.highlighters[HIGHLIGHTER_TYPE],
+ "No CSS grid highlighter exists in the rule-view.");
+ ok(!highlighters.gridHighlighterShown, "No CSS grid highlighter is shown.");
+
+ info("Toggling ON the CSS grid highlighter for the first grid container from the " +
+ "rule-view.");
+ let onHighlighterShown = highlighters.once("highlighter-shown");
+ gridToggle.click();
+ yield onHighlighterShown;
+
+ info("Checking the CSS grid highlighter is created and toggle button is active in " +
+ "the rule-view.");
+ ok(gridToggle.classList.contains("active"),
+ "Grid highlighter toggle is active.");
+ ok(highlighters.highlighters[HIGHLIGHTER_TYPE],
+ "CSS grid highlighter created in the rule-view.");
+ ok(highlighters.gridHighlighterShown, "CSS grid highlighter is shown.");
+
+ info("Selecting the second grid container.");
+ yield selectNode("#grid2", inspector);
+ let firstGridHighterShown = highlighters.gridHighlighterShown;
+ container = getRuleViewProperty(view, ".grid", "display").valueSpan;
+ gridToggle = container.querySelector(".ruleview-grid");
+
+ info("Checking the state of the CSS grid toggle for the second grid container in the " +
+ "rule-view.");
+ ok(gridToggle, "Grid highlighter toggle is visible.");
+ ok(!gridToggle.classList.contains("active"),
+ "Grid highlighter toggle button is not active.");
+ ok(highlighters.gridHighlighterShown, "CSS grid highlighter is still shown.");
+
+ info("Toggling ON the CSS grid highlighter for the second grid container from the " +
+ "rule-view.");
+ onHighlighterShown = highlighters.once("highlighter-shown");
+ gridToggle.click();
+ yield onHighlighterShown;
+
+ info("Checking the CSS grid highlighter is created for the second grid container and " +
+ "toggle button is active in the rule-view.");
+ ok(gridToggle.classList.contains("active"),
+ "Grid highlighter toggle is active.");
+ ok(highlighters.gridHighlighterShown != firstGridHighterShown,
+ "Grid highlighter for the second grid container is shown.");
+
+ info("Selecting the first grid container.");
+ yield selectNode("#grid1", inspector);
+ container = getRuleViewProperty(view, ".grid", "display").valueSpan;
+ gridToggle = container.querySelector(".ruleview-grid");
+
+ info("Checking the state of the CSS grid toggle for the first grid container in the " +
+ "rule-view.");
+ ok(gridToggle, "Grid highlighter toggle is visible.");
+ ok(!gridToggle.classList.contains("active"),
+ "Grid highlighter toggle button is not active.");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_guessIndentation.js b/devtools/client/inspector/rules/test/browser_rules_guessIndentation.js
new file mode 100644
index 000000000..ba2a1d7fb
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_guessIndentation.js
@@ -0,0 +1,47 @@
+/* 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";
+
+// Tests that we can guess indentation from a style sheet, not just a
+// rule.
+
+// Use a weird indentation depth to avoid accidental success.
+const TEST_URI = `
+ <style type='text/css'>
+div {
+ background-color: blue;
+}
+
+* {
+}
+</style>
+ <div id='testid' class='testclass'>Styled Node</div>
+`;
+
+const expectedText = `
+div {
+ background-color: blue;
+}
+
+* {
+ color: chartreuse;
+}
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {toolbox, inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ info("Add a new property in the rule-view");
+ yield addProperty(view, 2, "color", "chartreuse");
+
+ info("Switch to the style-editor");
+ let { UI } = yield toolbox.selectTool("styleeditor");
+
+ let styleEditor = yield UI.editors[0].getSourceEditor();
+ let text = styleEditor.sourceEditor.getText();
+ is(text, expectedText, "style inspector changes are synced");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_inherited-properties_01.js b/devtools/client/inspector/rules/test/browser_rules_inherited-properties_01.js
new file mode 100644
index 000000000..d1f6d7f45
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_inherited-properties_01.js
@@ -0,0 +1,47 @@
+/* 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 inherited properties appear for a nested element in the
+// rule view.
+
+const TEST_URI = `
+ <style type="text/css">
+ #test2 {
+ background-color: green;
+ color: purple;
+ }
+ </style>
+ <div id="test2"><div id="test1">Styled Node</div></div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#test1", inspector);
+ yield simpleInherit(inspector, view);
+});
+
+function* simpleInherit(inspector, view) {
+ let elementStyle = view._elementStyle;
+ is(elementStyle.rules.length, 2, "Should have 2 rules.");
+
+ let elementRule = elementStyle.rules[0];
+ ok(!elementRule.inherited,
+ "Element style attribute should not consider itself inherited.");
+
+ let inheritRule = elementStyle.rules[1];
+ is(inheritRule.selectorText, "#test2",
+ "Inherited rule should be the one that includes inheritable properties.");
+ ok(!!inheritRule.inherited, "Rule should consider itself inherited.");
+ is(inheritRule.textProps.length, 2,
+ "Rule should have two styles");
+ let bgcProp = inheritRule.textProps[0];
+ is(bgcProp.name, "background-color",
+ "background-color property should exist");
+ ok(bgcProp.invisible, "background-color property should be invisible");
+ let inheritProp = inheritRule.textProps[1];
+ is(inheritProp.name, "color", "color should have been inherited.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_inherited-properties_02.js b/devtools/client/inspector/rules/test/browser_rules_inherited-properties_02.js
new file mode 100644
index 000000000..db9662eee
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_inherited-properties_02.js
@@ -0,0 +1,34 @@
+/* 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 no inherited properties appear when the property does not apply
+// to the nested element.
+
+const TEST_URI = `
+ <style type="text/css">
+ #test2 {
+ background-color: green;
+ }
+ </style>
+ <div id="test2"><div id="test1">Styled Node</div></div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#test1", inspector);
+ yield emptyInherit(inspector, view);
+});
+
+function* emptyInherit(inspector, view) {
+ // No inheritable styles, this rule shouldn't show up.
+ let elementStyle = view._elementStyle;
+ is(elementStyle.rules.length, 1, "Should have 1 rule.");
+
+ let elementRule = elementStyle.rules[0];
+ ok(!elementRule.inherited,
+ "Element style attribute should not consider itself inherited.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_inherited-properties_03.js b/devtools/client/inspector/rules/test/browser_rules_inherited-properties_03.js
new file mode 100644
index 000000000..d6075f6f4
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_inherited-properties_03.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";
+
+// Check that inline inherited properties appear in the nested element.
+
+var {ELEMENT_STYLE} = require("devtools/shared/specs/styles");
+
+const TEST_URI = `
+ <div id="test2" style="color: red">
+ <div id="test1">Styled Node</div>
+ </div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#test1", inspector);
+ yield elementStyleInherit(inspector, view);
+});
+
+function* elementStyleInherit(inspector, view) {
+ let elementStyle = view._elementStyle;
+ is(elementStyle.rules.length, 2, "Should have 2 rules.");
+
+ let elementRule = elementStyle.rules[0];
+ ok(!elementRule.inherited,
+ "Element style attribute should not consider itself inherited.");
+
+ let inheritRule = elementStyle.rules[1];
+ is(inheritRule.domRule.type, ELEMENT_STYLE,
+ "Inherited rule should be an element style, not a rule.");
+ ok(!!inheritRule.inherited, "Rule should consider itself inherited.");
+ is(inheritRule.textProps.length, 1,
+ "Should only display one inherited style");
+ let inheritProp = inheritRule.textProps[0];
+ is(inheritProp.name, "color", "color should have been inherited.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_inline-source-map.js b/devtools/client/inspector/rules/test/browser_rules_inline-source-map.js
new file mode 100644
index 000000000..05109d8c6
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_inline-source-map.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 when a source map comment appears in an inline stylesheet, the
+// rule-view still appears correctly.
+// Bug 1255787.
+
+const TESTCASE_URI = URL_ROOT + "doc_inline_sourcemap.html";
+const PREF = "devtools.styleeditor.source-maps-enabled";
+
+add_task(function* () {
+ Services.prefs.setBoolPref(PREF, true);
+
+ yield addTab(TESTCASE_URI);
+ let {inspector, view} = yield openRuleView();
+
+ yield selectNode("div", inspector);
+
+ let ruleEl = getRuleViewRule(view, "div");
+ ok(ruleEl, "The 'div' rule exists in the rule-view");
+
+ Services.prefs.clearUserPref(PREF);
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_invalid-source-map.js b/devtools/client/inspector/rules/test/browser_rules_invalid-source-map.js
new file mode 100644
index 000000000..825f48a96
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_invalid-source-map.js
@@ -0,0 +1,44 @@
+/* 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 when a source map is missing/invalid, the rule view still loads
+// correctly.
+
+const TESTCASE_URI = URL_ROOT + "doc_invalid_sourcemap.html";
+const PREF = "devtools.styleeditor.source-maps-enabled";
+const CSS_LOC = "doc_invalid_sourcemap.css:1";
+
+add_task(function* () {
+ Services.prefs.setBoolPref(PREF, true);
+
+ yield addTab(TESTCASE_URI);
+ let {inspector, view} = yield openRuleView();
+
+ yield selectNode("div", inspector);
+
+ let ruleEl = getRuleViewRule(view, "div");
+ ok(ruleEl, "The 'div' rule exists in the rule-view");
+
+ let prop = getRuleViewProperty(view, "div", "color");
+ ok(prop, "The 'color' property exists in this rule");
+
+ let value = getRuleViewPropertyValue(view, "div", "color");
+ is(value, "gold", "The 'color' property has the right value");
+
+ yield verifyLinkText(view, CSS_LOC);
+
+ Services.prefs.clearUserPref(PREF);
+});
+
+function verifyLinkText(view, text) {
+ info("Verifying that the rule-view stylesheet link is " + text);
+ let label = getRuleViewLinkByIndex(view, 1)
+ .querySelector(".ruleview-rule-source-label");
+ return waitForSuccess(
+ () => label.textContent == text,
+ "Link text changed to display correct location: " + text
+ );
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_invalid.js b/devtools/client/inspector/rules/test/browser_rules_invalid.js
new file mode 100644
index 000000000..e664f68ac
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_invalid.js
@@ -0,0 +1,33 @@
+/* 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 an invalid property still lets us display the rule view
+// Bug 1235603.
+
+const TEST_URI = `
+ <style>
+ div {
+ background: #fff;
+ font-family: sans-serif;
+ url(display-table.min.htc);
+ }
+ </style>
+ <body>
+ <div id="testid" class="testclass">Styled Node</div>
+ </body>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ is(view._elementStyle.rules.length, 2, "Should have 2 rules.");
+ // Have to actually get the rule in order to ensure that the
+ // elements were created.
+ ok(getRuleViewRule(view, "div"), "Rule with div selector exists");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_keybindings.js b/devtools/client/inspector/rules/test/browser_rules_keybindings.js
new file mode 100644
index 000000000..84fdeff85
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_keybindings.js
@@ -0,0 +1,49 @@
+/* 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 focus doesn't leave the style editor when adding a property
+// (bug 719916)
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8,<h1>Some header text</h1>");
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("h1", inspector);
+
+ info("Getting the ruleclose brace element");
+ let brace = view.styleDocument.querySelector(".ruleview-ruleclose");
+
+ info("Focus the new property editable field to create a color property");
+ let ruleEditor = getRuleViewRuleEditor(view, 0);
+ let editor = yield focusNewRuleViewProperty(ruleEditor);
+ editor.input.value = "color";
+
+ info("Typing ENTER to focus the next field: property value");
+ let onFocus = once(brace.parentNode, "focus", true);
+ let onRuleViewChanged = view.once("ruleview-changed");
+
+ EventUtils.sendKey("return");
+
+ yield onFocus;
+ yield onRuleViewChanged;
+ ok(true, "The value field was focused");
+
+ info("Entering a property value");
+ editor = getCurrentInplaceEditor(view);
+ editor.input.value = "green";
+
+ info("Typing ENTER again should focus a new property name");
+ onFocus = once(brace.parentNode, "focus", true);
+ onRuleViewChanged = view.once("ruleview-changed");
+ EventUtils.sendKey("return");
+ yield onFocus;
+ yield onRuleViewChanged;
+ ok(true, "The new property name field was focused");
+ getCurrentInplaceEditor(view).input.blur();
+});
+
+function getCurrentInplaceEditor(view) {
+ return inplaceEditor(view.styleDocument.activeElement);
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_keyframeLineNumbers.js b/devtools/client/inspector/rules/test/browser_rules_keyframeLineNumbers.js
new file mode 100644
index 000000000..ebbde08ac
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_keyframeLineNumbers.js
@@ -0,0 +1,25 @@
+/* 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 editing a rule will update the line numbers of subsequent
+// rules in the rule view.
+
+const TESTCASE_URI = URL_ROOT + "doc_keyframeLineNumbers.html";
+
+add_task(function* () {
+ yield addTab(TESTCASE_URI);
+ let { inspector, view } = yield openRuleView();
+ yield selectNode("#outer", inspector);
+
+ info("Insert a new property, which will affect the line numbers");
+ yield addProperty(view, 1, "font-size", "72px");
+
+ yield selectNode("#inner", inspector);
+
+ let value = getRuleViewLinkTextByIndex(view, 3);
+ // Note that this is relative to the <style>.
+ is(value.slice(-3), ":27", "rule line number is 27");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_keyframes-rule_01.js b/devtools/client/inspector/rules/test/browser_rules_keyframes-rule_01.js
new file mode 100644
index 000000000..8d4b436c5
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_keyframes-rule_01.js
@@ -0,0 +1,106 @@
+/* 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 keyframe rules and gutters are displayed correctly in the
+// rule view.
+
+const TEST_URI = URL_ROOT + "doc_keyframeanimation.html";
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let {inspector, view} = yield openRuleView();
+ yield testPacman(inspector, view);
+ yield testBoxy(inspector, view);
+ yield testMoxy(inspector, view);
+});
+
+function* testPacman(inspector, view) {
+ info("Test content and gutter in the keyframes rule of #pacman");
+
+ yield assertKeyframeRules("#pacman", inspector, view, {
+ elementRulesNb: 2,
+ keyframeRulesNb: 2,
+ keyframesRules: ["pacman", "pacman"],
+ keyframeRules: ["100%", "100%"]
+ });
+
+ assertGutters(view, {
+ guttersNbs: 2,
+ gutterHeading: ["Keyframes pacman", "Keyframes pacman"]
+ });
+}
+
+function* testBoxy(inspector, view) {
+ info("Test content and gutter in the keyframes rule of #boxy");
+
+ yield assertKeyframeRules("#boxy", inspector, view, {
+ elementRulesNb: 3,
+ keyframeRulesNb: 3,
+ keyframesRules: ["boxy", "boxy", "boxy"],
+ keyframeRules: ["10%", "20%", "100%"]
+ });
+
+ assertGutters(view, {
+ guttersNbs: 1,
+ gutterHeading: ["Keyframes boxy"]
+ });
+}
+
+function* testMoxy(inspector, view) {
+ info("Test content and gutter in the keyframes rule of #moxy");
+
+ yield assertKeyframeRules("#moxy", inspector, view, {
+ elementRulesNb: 3,
+ keyframeRulesNb: 4,
+ keyframesRules: ["boxy", "boxy", "boxy", "moxy"],
+ keyframeRules: ["10%", "20%", "100%", "100%"]
+ });
+
+ assertGutters(view, {
+ guttersNbs: 2,
+ gutterHeading: ["Keyframes boxy", "Keyframes moxy"]
+ });
+}
+
+function* assertKeyframeRules(selector, inspector, view, expected) {
+ yield selectNode(selector, inspector);
+ let elementStyle = view._elementStyle;
+
+ let rules = {
+ elementRules: elementStyle.rules.filter(rule => !rule.keyframes),
+ keyframeRules: elementStyle.rules.filter(rule => rule.keyframes)
+ };
+
+ is(rules.elementRules.length, expected.elementRulesNb, selector +
+ " has the correct number of non keyframe element rules");
+ is(rules.keyframeRules.length, expected.keyframeRulesNb, selector +
+ " has the correct number of keyframe rules");
+
+ let i = 0;
+ for (let keyframeRule of rules.keyframeRules) {
+ ok(keyframeRule.keyframes.name == expected.keyframesRules[i],
+ keyframeRule.keyframes.name + " has the correct keyframes name");
+ ok(keyframeRule.domRule.keyText == expected.keyframeRules[i],
+ keyframeRule.domRule.keyText + " selector heading is correct");
+ i++;
+ }
+}
+
+function assertGutters(view, expected) {
+ let gutters = view.element.querySelectorAll(".theme-gutter");
+
+ is(gutters.length, expected.guttersNbs,
+ "There are " + gutters.length + " gutter headings");
+
+ let i = 0;
+ for (let gutter of gutters) {
+ is(gutter.textContent, expected.gutterHeading[i],
+ "Correct " + gutter.textContent + " gutter headings");
+ i++;
+ }
+
+ return gutters;
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_keyframes-rule_02.js b/devtools/client/inspector/rules/test/browser_rules_keyframes-rule_02.js
new file mode 100644
index 000000000..b7652ecaa
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_keyframes-rule_02.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";
+
+// Test that verifies the content of the keyframes rule and property changes
+// to keyframe rules.
+
+const TEST_URI = URL_ROOT + "doc_keyframeanimation.html";
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let {inspector, view} = yield openRuleView();
+ yield testPacman(inspector, view);
+ yield testBoxy(inspector, view);
+});
+
+function* testPacman(inspector, view) {
+ info("Test content in the keyframes rule of #pacman");
+
+ let rules = yield getKeyframeRules("#pacman", inspector, view);
+
+ info("Test text properties for Keyframes #pacman");
+
+ is(convertTextPropsToString(rules.keyframeRules[0].textProps),
+ "left: 750px",
+ "Keyframe pacman (100%) property is correct"
+ );
+
+ // Dynamic changes test disabled because of Bug 1050940
+ // If this part of the test is ever enabled again, it should be changed to
+ // use addProperty (in head.js) and stop using _applyingModifications
+
+ // info("Test dynamic changes to keyframe rule for #pacman");
+
+ // let defaultView = element.ownerDocument.defaultView;
+ // let ruleEditor = view.element.children[5].childNodes[0]._ruleEditor;
+ // ruleEditor.addProperty("opacity", "0", true);
+
+ // yield ruleEditor._applyingModifications;
+ // yield once(element, "animationend");
+
+ // is
+ // (
+ // convertTextPropsToString(rules.keyframeRules[1].textProps),
+ // "left: 750px; opacity: 0",
+ // "Keyframe pacman (100%) property is correct"
+ // );
+
+ // is(defaultView.getComputedStyle(element).getPropertyValue("opacity"), "0",
+ // "Added opacity property should have been used.");
+}
+
+function* testBoxy(inspector, view) {
+ info("Test content in the keyframes rule of #boxy");
+
+ let rules = yield getKeyframeRules("#boxy", inspector, view);
+
+ info("Test text properties for Keyframes #boxy");
+
+ is(convertTextPropsToString(rules.keyframeRules[0].textProps),
+ "background-color: blue",
+ "Keyframe boxy (10%) property is correct"
+ );
+
+ is(convertTextPropsToString(rules.keyframeRules[1].textProps),
+ "background-color: green",
+ "Keyframe boxy (20%) property is correct"
+ );
+
+ is(convertTextPropsToString(rules.keyframeRules[2].textProps),
+ "opacity: 0",
+ "Keyframe boxy (100%) property is correct"
+ );
+}
+
+function convertTextPropsToString(textProps) {
+ return textProps.map(t => t.name + ": " + t.value).join("; ");
+}
+
+function* getKeyframeRules(selector, inspector, view) {
+ yield selectNode(selector, inspector);
+ let elementStyle = view._elementStyle;
+
+ let rules = {
+ elementRules: elementStyle.rules.filter(rule => !rule.keyframes),
+ keyframeRules: elementStyle.rules.filter(rule => rule.keyframes)
+ };
+
+ return rules;
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_lineNumbers.js b/devtools/client/inspector/rules/test/browser_rules_lineNumbers.js
new file mode 100644
index 000000000..3b09209f5
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_lineNumbers.js
@@ -0,0 +1,29 @@
+/* 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 editing a rule will update the line numbers of subsequent
+// rules in the rule view.
+
+const TESTCASE_URI = URL_ROOT + "doc_ruleLineNumbers.html";
+
+add_task(function* () {
+ yield addTab(TESTCASE_URI);
+ let { inspector, view } = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ let bodyRuleEditor = getRuleViewRuleEditor(view, 3);
+ let value = getRuleViewLinkTextByIndex(view, 2);
+ // Note that this is relative to the <style>.
+ is(value.slice(-2), ":6", "initial rule line number is 6");
+
+ let onLocationChanged = once(bodyRuleEditor.rule.domRule, "location-changed");
+ yield addProperty(view, 1, "font-size", "23px");
+ yield onLocationChanged;
+
+ let newBodyTitle = getRuleViewLinkTextByIndex(view, 2);
+ // Note that this is relative to the <style>.
+ is(newBodyTitle.slice(-2), ":7", "updated rule line number is 7");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_livepreview.js b/devtools/client/inspector/rules/test/browser_rules_livepreview.js
new file mode 100644
index 000000000..1f1302a70
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_livepreview.js
@@ -0,0 +1,72 @@
+/* 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 changes are previewed when editing a property value.
+
+const TEST_URI = `
+ <style type="text/css">
+ #testid {
+ display:block;
+ }
+ </style>
+ <div id="testid">Styled Node</div><span>inline element</span>
+`;
+
+// Format
+// {
+// value : what to type in the field
+// expected : expected computed style on the targeted element
+// }
+const TEST_DATA = [
+ {value: "inline", expected: "inline"},
+ {value: "inline-block", expected: "inline-block"},
+
+ // Invalid property values should not apply, and should fall back to default
+ {value: "red", expected: "block"},
+ {value: "something", expected: "block"},
+
+ {escape: true, value: "inline", expected: "block"}
+];
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ for (let data of TEST_DATA) {
+ yield testLivePreviewData(data, view, "#testid");
+ }
+});
+
+function* testLivePreviewData(data, ruleView, selector) {
+ let rule = getRuleViewRuleEditor(ruleView, 1).rule;
+ let propEditor = rule.textProps[0].editor;
+
+ info("Focusing the property value inplace-editor");
+ let editor = yield focusEditableField(ruleView, propEditor.valueSpan);
+ is(inplaceEditor(propEditor.valueSpan), editor,
+ "The focused editor is the value");
+
+ info("Entering value in the editor: " + data.value);
+ let onPreviewDone = ruleView.once("ruleview-changed");
+ EventUtils.sendString(data.value, ruleView.styleWindow);
+ ruleView.throttle.flush();
+ yield onPreviewDone;
+
+ let onValueDone = ruleView.once("ruleview-changed");
+ if (data.escape) {
+ EventUtils.synthesizeKey("VK_ESCAPE", {});
+ } else {
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ }
+ yield onValueDone;
+
+ // While the editor is still focused in, the display should have
+ // changed already
+ is((yield getComputedStyleProperty(selector, null, "display")),
+ data.expected,
+ "Element should be previewed as " + data.expected);
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_mark_overridden_01.js b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_01.js
new file mode 100644
index 000000000..ab10fadfe
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_01.js
@@ -0,0 +1,56 @@
+/* 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";
+
+// Tests that the rule view marks overridden rules correctly based on the
+// specificity of the rule.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #testid {
+ background-color: blue;
+ }
+ .testclass {
+ background-color: green;
+ }
+ </style>
+ <div id='testid' class='testclass'>Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ let idRule = getRuleViewRuleEditor(view, 1).rule;
+ let idProp = idRule.textProps[0];
+ is(idProp.name, "background-color",
+ "First ID property should be background-color");
+ is(idProp.value, "blue", "First ID property value should be blue");
+ ok(!idProp.overridden, "ID prop should not be overridden.");
+ ok(!idProp.editor.element.classList.contains("ruleview-overridden"),
+ "ID property editor should not have ruleview-overridden class");
+
+ let classRule = getRuleViewRuleEditor(view, 2).rule;
+ let classProp = classRule.textProps[0];
+ is(classProp.name, "background-color",
+ "First class prop should be background-color");
+ is(classProp.value, "green", "First class property value should be green");
+ ok(classProp.overridden, "Class property should be overridden.");
+ ok(classProp.editor.element.classList.contains("ruleview-overridden"),
+ "Class property editor should have ruleview-overridden class");
+
+ // Override background-color by changing the element style.
+ let elementProp = yield addProperty(view, 0, "background-color", "purple");
+
+ ok(!elementProp.overridden,
+ "Element style property should not be overridden");
+ ok(idProp.overridden, "ID property should be overridden");
+ ok(idProp.editor.element.classList.contains("ruleview-overridden"),
+ "ID property editor should have ruleview-overridden class");
+ ok(classProp.overridden, "Class property should be overridden");
+ ok(classProp.editor.element.classList.contains("ruleview-overridden"),
+ "Class property editor should have ruleview-overridden class");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_mark_overridden_02.js b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_02.js
new file mode 100644
index 000000000..c71fc7211
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_02.js
@@ -0,0 +1,45 @@
+/* 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";
+
+// Tests that the rule view marks overridden rules correctly for short hand
+// properties and the computed list properties
+
+const TEST_URI = `
+ <style type='text/css'>
+ #testid {
+ margin-left: 1px;
+ }
+ .testclass {
+ margin: 2px;
+ }
+ </style>
+ <div id='testid' class='testclass'>Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+ yield testMarkOverridden(inspector, view);
+});
+
+function* testMarkOverridden(inspector, view) {
+ let elementStyle = view._elementStyle;
+
+ let classRule = elementStyle.rules[2];
+ let classProp = classRule.textProps[0];
+ ok(!classProp.overridden,
+ "Class prop shouldn't be overridden, some props are still being used.");
+
+ for (let computed of classProp.computed) {
+ if (computed.name.indexOf("margin-left") == 0) {
+ ok(computed.overridden, "margin-left props should be overridden.");
+ } else {
+ ok(!computed.overridden,
+ "Non-margin-left props should not be overridden.");
+ }
+ }
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_mark_overridden_03.js b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_03.js
new file mode 100644
index 000000000..b99bab8b4
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_03.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";
+
+// Tests that the rule view marks overridden rules correctly based on the
+// priority for the rule
+
+const TEST_URI = `
+ <style type='text/css'>
+ #testid {
+ background-color: blue;
+ }
+ .testclass {
+ background-color: green !important;
+ }
+ </style>
+ <div id='testid' class='testclass'>Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ let idRule = getRuleViewRuleEditor(view, 1).rule;
+ let idProp = idRule.textProps[0];
+ ok(idProp.overridden, "Not-important rule should be overridden.");
+
+ let classRule = getRuleViewRuleEditor(view, 2).rule;
+ let classProp = classRule.textProps[0];
+ ok(!classProp.overridden, "Important rule should not be overridden.");
+
+ ok(idProp.overridden, "ID property should be overridden.");
+
+ // FIXME: re-enable these 2 assertions when bug 1247737 is fixed.
+ // let elementProp = yield addProperty(view, 0, "background-color", "purple");
+ // ok(!elementProp.overridden, "New important prop should not be overriden.");
+ // ok(classProp.overridden, "Class property should be overridden.");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_mark_overridden_04.js b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_04.js
new file mode 100644
index 000000000..fbce1ebf4
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_04.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";
+
+// Tests that the rule view marks overridden rules correctly if a property gets
+// disabled
+
+const TEST_URI = `
+ <style type='text/css'>
+ #testid {
+ background-color: blue;
+ }
+ .testclass {
+ background-color: green;
+ }
+ </style>
+ <div id='testid' class='testclass'>Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ let idRule = getRuleViewRuleEditor(view, 1).rule;
+ let idProp = idRule.textProps[0];
+
+ yield togglePropStatus(view, idProp);
+
+ let classRule = getRuleViewRuleEditor(view, 2).rule;
+ let classProp = classRule.textProps[0];
+ ok(!classProp.overridden,
+ "Class prop should not be overridden after id prop was disabled.");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_mark_overridden_05.js b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_05.js
new file mode 100644
index 000000000..11ecd72ff
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_05.js
@@ -0,0 +1,33 @@
+/* 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";
+
+// Tests that the rule view marks overridden rules correctly based on the
+// order of the property.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #testid {
+ background-color: green;
+ }
+ </style>
+ <div id='testid' class='testclass'>Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ let rule = getRuleViewRuleEditor(view, 1).rule;
+
+ yield addProperty(view, 1, "background-color", "red");
+
+ let firstProp = rule.textProps[0];
+ let secondProp = rule.textProps[1];
+
+ ok(firstProp.overridden, "First property should be overridden.");
+ ok(!secondProp.overridden, "Second property should not be overridden.");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_mark_overridden_06.js b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_06.js
new file mode 100644
index 000000000..c2e71fe49
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_06.js
@@ -0,0 +1,60 @@
+/* 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";
+
+// Tests that the rule view marks overridden rules correctly after
+// editing the selector.
+
+const TEST_URI = `
+ <style type='text/css'>
+ div {
+ background-color: blue;
+ background-color: chartreuse;
+ }
+ </style>
+ <div id='testid' class='testclass'>Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+ yield testMarkOverridden(inspector, view);
+});
+
+function* testMarkOverridden(inspector, view) {
+ let elementStyle = view._elementStyle;
+ let rule = elementStyle.rules[1];
+ checkProperties(rule);
+
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+ info("Focusing an existing selector name in the rule-view");
+ let editor = yield focusEditableField(view, ruleEditor.selectorText);
+
+ info("Entering a new selector name and committing");
+ editor.input.value = "div[class]";
+
+ let onRuleViewChanged = once(view, "ruleview-changed");
+ info("Entering the commit key");
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ yield onRuleViewChanged;
+
+ view.searchField.focus();
+ checkProperties(rule);
+}
+
+// A helper to perform a repeated set of checks.
+function checkProperties(rule) {
+ let prop = rule.textProps[0];
+ is(prop.name, "background-color",
+ "First property should be background-color");
+ is(prop.value, "blue", "First property value should be blue");
+ ok(prop.overridden, "prop should be overridden.");
+ prop = rule.textProps[1];
+ is(prop.name, "background-color",
+ "Second property should be background-color");
+ is(prop.value, "chartreuse", "First property value should be chartreuse");
+ ok(!prop.overridden, "prop should not be overridden.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_mark_overridden_07.js b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_07.js
new file mode 100644
index 000000000..9480ddd47
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_07.js
@@ -0,0 +1,72 @@
+/* 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";
+
+// Tests that the rule view marks overridden rules correctly based on the
+// specificity of the rule.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #testid {
+ margin-left: 23px;
+ }
+
+ div {
+ margin-right: 23px;
+ margin-left: 1px !important;
+ }
+
+ body {
+ margin-right: 1px !important;
+ font-size: 79px;
+ }
+
+ span {
+ font-size: 12px;
+ }
+ </style>
+ <body>
+ <span>
+ <div id='testid' class='testclass'>Styled Node</div>
+ </span>
+ </body>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+ yield testMarkOverridden(inspector, view);
+});
+
+function* testMarkOverridden(inspector, view) {
+ let elementStyle = view._elementStyle;
+
+ let RESULTS = [
+ // We skip the first element
+ [],
+ [{name: "margin-left", value: "23px", overridden: true}],
+ [{name: "margin-right", value: "23px", overridden: false},
+ {name: "margin-left", value: "1px", overridden: false}],
+ [{name: "font-size", value: "12px", overridden: false}],
+ [{name: "margin-right", value: "1px", overridden: true},
+ {name: "font-size", value: "79px", overridden: true}]
+ ];
+
+ for (let i = 1; i < RESULTS.length; ++i) {
+ let idRule = elementStyle.rules[i];
+
+ for (let propIndex in RESULTS[i]) {
+ let expected = RESULTS[i][propIndex];
+ let prop = idRule.textProps[propIndex];
+
+ info("Checking rule " + i + ", property " + propIndex);
+
+ is(prop.name, expected.name, "check property name");
+ is(prop.value, expected.value, "check property value");
+ is(prop.overridden, expected.overridden, "check property overridden");
+ }
+ }
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_mathml-element.js b/devtools/client/inspector/rules/test/browser_rules_mathml-element.js
new file mode 100644
index 000000000..f8a1e8572
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_mathml-element.js
@@ -0,0 +1,53 @@
+/* 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";
+
+// Tests that the rule-view displays correctly on MathML elements.
+
+const TEST_URI = `
+ <div>
+ <math xmlns=\http://www.w3.org/1998/Math/MathML\>
+ <mfrac>
+ <msubsup>
+ <mi>a</mi>
+ <mi>i</mi>
+ <mi>j</mi>
+ </msubsup>
+ <msub>
+ <mi>x</mi>
+ <mn>0</mn>
+ </msub>
+ </mfrac>
+ </math>
+ </div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+
+ info("Select the DIV node and verify the rule-view shows rules");
+ yield selectNode("div", inspector);
+ ok(view.element.querySelectorAll(".ruleview-rule").length,
+ "The rule-view shows rules for the div element");
+
+ info("Select various MathML nodes and verify the rule-view is empty");
+ yield selectNode("math", inspector);
+ ok(!view.element.querySelectorAll(".ruleview-rule").length,
+ "The rule-view is empty for the math element");
+
+ yield selectNode("msubsup", inspector);
+ ok(!view.element.querySelectorAll(".ruleview-rule").length,
+ "The rule-view is empty for the msubsup element");
+
+ yield selectNode("mn", inspector);
+ ok(!view.element.querySelectorAll(".ruleview-rule").length,
+ "The rule-view is empty for the mn element");
+
+ info("Select again the DIV node and verify the rule-view shows rules");
+ yield selectNode("div", inspector);
+ ok(view.element.querySelectorAll(".ruleview-rule").length,
+ "The rule-view shows rules for the div element");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_media-queries.js b/devtools/client/inspector/rules/test/browser_rules_media-queries.js
new file mode 100644
index 000000000..57ab19163
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_media-queries.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";
+
+// Tests that we correctly display appropriate media query titles in the
+// rule view.
+
+const TEST_URI = URL_ROOT + "doc_media_queries.html";
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("div", inspector);
+
+ let elementStyle = view._elementStyle;
+
+ let inline = STYLE_INSPECTOR_L10N.getStr("rule.sourceInline");
+
+ is(elementStyle.rules.length, 3, "Should have 3 rules.");
+ is(elementStyle.rules[0].title, inline, "check rule 0 title");
+ is(elementStyle.rules[1].title, inline +
+ ":9 @media screen and (min-width: 1px)", "check rule 1 title");
+ is(elementStyle.rules[2].title, inline + ":2", "check rule 2 title");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_multiple-properties-duplicates.js b/devtools/client/inspector/rules/test/browser_rules_multiple-properties-duplicates.js
new file mode 100644
index 000000000..c820dd73f
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_multiple-properties-duplicates.js
@@ -0,0 +1,68 @@
+/* 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 rule-view behaves correctly when entering mutliple and/or
+// unfinished properties/values in inplace-editors
+
+const TEST_URI = "<div>Test Element</div>";
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("div", inspector);
+
+ let ruleEditor = getRuleViewRuleEditor(view, 0);
+ // Note that we wait for a markup mutation here because this new rule will end
+ // up creating a style attribute on the node shown in the markup-view.
+ // (we also wait for the rule-view to refresh).
+ let onMutation = inspector.once("markupmutation");
+ let onRuleViewChanged = view.once("ruleview-changed");
+ yield createNewRuleViewProperty(ruleEditor,
+ "color:red;color:orange;color:yellow;color:green;color:blue;color:indigo;" +
+ "color:violet;");
+ yield onMutation;
+ yield onRuleViewChanged;
+
+ is(ruleEditor.rule.textProps.length, 7,
+ "Should have created new text properties.");
+ is(ruleEditor.propertyList.children.length, 8,
+ "Should have created new property editors.");
+
+ is(ruleEditor.rule.textProps[0].name, "color",
+ "Should have correct property name");
+ is(ruleEditor.rule.textProps[0].value, "red",
+ "Should have correct property value");
+
+ is(ruleEditor.rule.textProps[1].name, "color",
+ "Should have correct property name");
+ is(ruleEditor.rule.textProps[1].value, "orange",
+ "Should have correct property value");
+
+ is(ruleEditor.rule.textProps[2].name, "color",
+ "Should have correct property name");
+ is(ruleEditor.rule.textProps[2].value, "yellow",
+ "Should have correct property value");
+
+ is(ruleEditor.rule.textProps[3].name, "color",
+ "Should have correct property name");
+ is(ruleEditor.rule.textProps[3].value, "green",
+ "Should have correct property value");
+
+ is(ruleEditor.rule.textProps[4].name, "color",
+ "Should have correct property name");
+ is(ruleEditor.rule.textProps[4].value, "blue",
+ "Should have correct property value");
+
+ is(ruleEditor.rule.textProps[5].name, "color",
+ "Should have correct property name");
+ is(ruleEditor.rule.textProps[5].value, "indigo",
+ "Should have correct property value");
+
+ is(ruleEditor.rule.textProps[6].name, "color",
+ "Should have correct property name");
+ is(ruleEditor.rule.textProps[6].value, "violet",
+ "Should have correct property value");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_multiple-properties-priority.js b/devtools/client/inspector/rules/test/browser_rules_multiple-properties-priority.js
new file mode 100644
index 000000000..f7d98b768
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_multiple-properties-priority.js
@@ -0,0 +1,47 @@
+/* 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 rule-view behaves correctly when entering mutliple and/or
+// unfinished properties/values in inplace-editors.
+
+const TEST_URI = "<div>Test Element</div>";
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("div", inspector);
+
+ let ruleEditor = getRuleViewRuleEditor(view, 0);
+ // Note that we wait for a markup mutation here because this new rule will end
+ // up creating a style attribute on the node shown in the markup-view.
+ // (we also wait for the rule-view to refresh).
+ let onMutation = inspector.once("markupmutation");
+ let onRuleViewChanged = view.once("ruleview-changed");
+ yield createNewRuleViewProperty(ruleEditor,
+ "color:red;width:100px;height: 100px;");
+ yield onMutation;
+ yield onRuleViewChanged;
+
+ is(ruleEditor.rule.textProps.length, 3,
+ "Should have created new text properties.");
+ is(ruleEditor.propertyList.children.length, 4,
+ "Should have created new property editors.");
+
+ is(ruleEditor.rule.textProps[0].name, "color",
+ "Should have correct property name");
+ is(ruleEditor.rule.textProps[0].value, "red",
+ "Should have correct property value");
+
+ is(ruleEditor.rule.textProps[1].name, "width",
+ "Should have correct property name");
+ is(ruleEditor.rule.textProps[1].value, "100px",
+ "Should have correct property value");
+
+ is(ruleEditor.rule.textProps[2].name, "height",
+ "Should have correct property name");
+ is(ruleEditor.rule.textProps[2].value, "100px",
+ "Should have correct property value");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_multiple-properties-unfinished_01.js b/devtools/client/inspector/rules/test/browser_rules_multiple-properties-unfinished_01.js
new file mode 100644
index 000000000..deaf16029
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_multiple-properties-unfinished_01.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 rule-view behaves correctly when entering multiple and/or
+// unfinished properties/values in inplace-editors
+
+const TEST_URI = "<div>Test Element</div>";
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("div", inspector);
+ yield testCreateNewMultiUnfinished(inspector, view);
+});
+
+function* testCreateNewMultiUnfinished(inspector, view) {
+ let ruleEditor = getRuleViewRuleEditor(view, 0);
+ let onMutation = inspector.once("markupmutation");
+ let onRuleViewChanged = view.once("ruleview-changed");
+ yield createNewRuleViewProperty(ruleEditor,
+ "color:blue;background : orange ; text-align:center; border-color: ");
+ yield onMutation;
+ yield onRuleViewChanged;
+
+ is(ruleEditor.rule.textProps.length, 4,
+ "Should have created new text properties.");
+ is(ruleEditor.propertyList.children.length, 4,
+ "Should have created property editors.");
+
+ EventUtils.sendString("red", view.styleWindow);
+ onRuleViewChanged = view.once("ruleview-changed");
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
+ yield onRuleViewChanged;
+
+ is(ruleEditor.rule.textProps.length, 4,
+ "Should have the same number of text properties.");
+ is(ruleEditor.propertyList.children.length, 5,
+ "Should have added the changed value editor.");
+
+ is(ruleEditor.rule.textProps[0].name, "color",
+ "Should have correct property name");
+ is(ruleEditor.rule.textProps[0].value, "blue",
+ "Should have correct property value");
+
+ is(ruleEditor.rule.textProps[1].name, "background",
+ "Should have correct property name");
+ is(ruleEditor.rule.textProps[1].value, "orange",
+ "Should have correct property value");
+
+ is(ruleEditor.rule.textProps[2].name, "text-align",
+ "Should have correct property name");
+ is(ruleEditor.rule.textProps[2].value, "center",
+ "Should have correct property value");
+
+ is(ruleEditor.rule.textProps[3].name, "border-color",
+ "Should have correct property name");
+ is(ruleEditor.rule.textProps[3].value, "red",
+ "Should have correct property value");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_multiple-properties-unfinished_02.js b/devtools/client/inspector/rules/test/browser_rules_multiple-properties-unfinished_02.js
new file mode 100644
index 000000000..dd1360b96
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_multiple-properties-unfinished_02.js
@@ -0,0 +1,71 @@
+/* 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 rule-view behaves correctly when entering mutliple and/or
+// unfinished properties/values in inplace-editors
+
+const TEST_URI = "<div>Test Element</div>";
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+
+ // Turn off throttling, which can cause intermittents. Throttling is used by
+ // the TextPropertyEditor.
+ view.throttle = () => {};
+
+ yield selectNode("div", inspector);
+
+ let ruleEditor = getRuleViewRuleEditor(view, 0);
+ // Note that we wait for a markup mutation here because this new rule will end
+ // up creating a style attribute on the node shown in the markup-view.
+ // (we also wait for the rule-view to refresh).
+ let onMutation = inspector.once("markupmutation");
+ let onRuleViewChanged = view.once("ruleview-changed");
+ yield createNewRuleViewProperty(ruleEditor, "width: 100px; heig");
+ yield onMutation;
+ yield onRuleViewChanged;
+
+ is(ruleEditor.rule.textProps.length, 2,
+ "Should have created a new text property.");
+ is(ruleEditor.propertyList.children.length, 2,
+ "Should have created a property editor.");
+
+ // Value is focused, lets add multiple rules here and make sure they get added
+ onMutation = inspector.once("markupmutation");
+ onRuleViewChanged = view.once("ruleview-changed");
+ let valueEditor = ruleEditor.propertyList.children[1]
+ .querySelector(".styleinspector-propertyeditor");
+ valueEditor.value = "10px;background:orangered;color: black;";
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
+ yield onMutation;
+ yield onRuleViewChanged;
+
+ is(ruleEditor.rule.textProps.length, 4,
+ "Should have added the changed value.");
+ is(ruleEditor.propertyList.children.length, 5,
+ "Should have added the changed value editor.");
+
+ is(ruleEditor.rule.textProps[0].name, "width",
+ "Should have correct property name");
+ is(ruleEditor.rule.textProps[0].value, "100px",
+ "Should have correct property value");
+
+ is(ruleEditor.rule.textProps[1].name, "heig",
+ "Should have correct property name");
+ is(ruleEditor.rule.textProps[1].value, "10px",
+ "Should have correct property value");
+
+ is(ruleEditor.rule.textProps[2].name, "background",
+ "Should have correct property name");
+ is(ruleEditor.rule.textProps[2].value, "orangered",
+ "Should have correct property value");
+
+ is(ruleEditor.rule.textProps[3].name, "color",
+ "Should have correct property name");
+ is(ruleEditor.rule.textProps[3].value, "black",
+ "Should have correct property value");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_multiple_properties_01.js b/devtools/client/inspector/rules/test/browser_rules_multiple_properties_01.js
new file mode 100644
index 000000000..2801df652
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_multiple_properties_01.js
@@ -0,0 +1,53 @@
+/* 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 rule-view behaves correctly when entering mutliple and/or
+// unfinished properties/values in inplace-editors.
+
+const TEST_URI = "<div>Test Element</div>";
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("div", inspector);
+
+ let ruleEditor = getRuleViewRuleEditor(view, 0);
+ // Note that we wait for a markup mutation here because this new rule will end
+ // up creating a style attribute on the node shown in the markup-view.
+ // (we also wait for the rule-view to refresh).
+ let onMutation = inspector.once("markupmutation");
+ let onRuleViewChanged = view.once("ruleview-changed");
+ yield createNewRuleViewProperty(ruleEditor,
+ "color:blue;background : orange ; text-align:center; " +
+ "border-color: green;");
+ yield onMutation;
+ yield onRuleViewChanged;
+
+ is(ruleEditor.rule.textProps.length, 4,
+ "Should have created a new text property.");
+ is(ruleEditor.propertyList.children.length, 5,
+ "Should have created a new property editor.");
+
+ is(ruleEditor.rule.textProps[0].name, "color",
+ "Should have correct property name");
+ is(ruleEditor.rule.textProps[0].value, "blue",
+ "Should have correct property value");
+
+ is(ruleEditor.rule.textProps[1].name, "background",
+ "Should have correct property name");
+ is(ruleEditor.rule.textProps[1].value, "orange",
+ "Should have correct property value");
+
+ is(ruleEditor.rule.textProps[2].name, "text-align",
+ "Should have correct property name");
+ is(ruleEditor.rule.textProps[2].value, "center",
+ "Should have correct property value");
+
+ is(ruleEditor.rule.textProps[3].name, "border-color",
+ "Should have correct property name");
+ is(ruleEditor.rule.textProps[3].value, "green",
+ "Should have correct property value");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_multiple_properties_02.js b/devtools/client/inspector/rules/test/browser_rules_multiple_properties_02.js
new file mode 100644
index 000000000..ce6f1909f
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_multiple_properties_02.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 rule-view behaves correctly when entering mutliple and/or
+// unfinished properties/values in inplace-editors
+
+const TEST_URI = "<div>Test Element</div>";
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("div", inspector);
+
+ let ruleEditor = getRuleViewRuleEditor(view, 0);
+ let onDone = view.once("ruleview-changed");
+ yield createNewRuleViewProperty(ruleEditor, "width:");
+ yield onDone;
+
+ is(ruleEditor.rule.textProps.length, 1,
+ "Should have created a new text property.");
+ is(ruleEditor.propertyList.children.length, 1,
+ "Should have created a property editor.");
+
+ // Value is focused, lets add multiple rules here and make sure they get added
+ onDone = view.once("ruleview-changed");
+ let onMutation = inspector.once("markupmutation");
+ let input = view.styleDocument.activeElement;
+ input.value = "height: 10px;color:blue";
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
+ yield onMutation;
+ yield onDone;
+
+ is(ruleEditor.rule.textProps.length, 2,
+ "Should have added the changed value.");
+ is(ruleEditor.propertyList.children.length, 3,
+ "Should have added the changed value editor.");
+
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, view.styleWindow);
+ is(ruleEditor.propertyList.children.length, 2,
+ "Should have removed the value editor.");
+
+ is(ruleEditor.rule.textProps[0].name, "width",
+ "Should have correct property name");
+ is(ruleEditor.rule.textProps[0].value, "height: 10px",
+ "Should have correct property value");
+
+ is(ruleEditor.rule.textProps[1].name, "color",
+ "Should have correct property name");
+ is(ruleEditor.rule.textProps[1].value, "blue",
+ "Should have correct property value");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_original-source-link.js b/devtools/client/inspector/rules/test/browser_rules_original-source-link.js
new file mode 100644
index 000000000..09dad9a86
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_original-source-link.js
@@ -0,0 +1,85 @@
+/* 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 stylesheet links in the rule view are correct when source maps
+// are involved.
+
+const TESTCASE_URI = URL_ROOT + "doc_sourcemaps.html";
+const PREF = "devtools.styleeditor.source-maps-enabled";
+const SCSS_LOC = "doc_sourcemaps.scss:4";
+const CSS_LOC = "doc_sourcemaps.css:1";
+
+add_task(function* () {
+ info("Setting the " + PREF + " pref to true");
+ Services.prefs.setBoolPref(PREF, true);
+
+ yield addTab(TESTCASE_URI);
+ let {toolbox, inspector, view} = yield openRuleView();
+
+ info("Selecting the test node");
+ yield selectNode("div", inspector);
+
+ yield verifyLinkText(SCSS_LOC, view);
+
+ info("Setting the " + PREF + " pref to false");
+ Services.prefs.setBoolPref(PREF, false);
+ yield verifyLinkText(CSS_LOC, view);
+
+ info("Setting the " + PREF + " pref to true again");
+ Services.prefs.setBoolPref(PREF, true);
+
+ yield testClickingLink(toolbox, view);
+ yield checkDisplayedStylesheet(toolbox);
+
+ info("Clearing the " + PREF + " pref");
+ Services.prefs.clearUserPref(PREF);
+});
+
+function* testClickingLink(toolbox, view) {
+ info("Listening for switch to the style editor");
+ let onStyleEditorReady = toolbox.once("styleeditor-ready");
+
+ info("Finding the stylesheet link and clicking it");
+ let link = getRuleViewLinkByIndex(view, 1);
+ link.scrollIntoView();
+ link.click();
+ yield onStyleEditorReady;
+}
+
+function checkDisplayedStylesheet(toolbox) {
+ let def = defer();
+
+ let panel = toolbox.getCurrentPanel();
+ panel.UI.on("editor-selected", (event, editor) => {
+ // The style editor selects the first sheet at first load before
+ // selecting the desired sheet.
+ if (editor.styleSheet.href.endsWith("scss")) {
+ info("Original source editor selected");
+ editor.getSourceEditor().then(editorSelected)
+ .then(def.resolve, def.reject);
+ }
+ });
+
+ return def.promise;
+}
+
+function editorSelected(editor) {
+ let href = editor.styleSheet.href;
+ ok(href.endsWith("doc_sourcemaps.scss"),
+ "selected stylesheet is correct one");
+
+ let {line} = editor.sourceEditor.getCursor();
+ is(line, 3, "cursor is at correct line number in original source");
+}
+
+function verifyLinkText(text, view) {
+ info("Verifying that the rule-view stylesheet link is " + text);
+ let label = getRuleViewLinkByIndex(view, 1)
+ .querySelector(".ruleview-rule-source-label");
+ return waitForSuccess(function* () {
+ return label.textContent == text;
+ }, "Link text changed to display correct location: " + text);
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_pseudo-element_01.js b/devtools/client/inspector/rules/test/browser_rules_pseudo-element_01.js
new file mode 100644
index 000000000..e98b5437c
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_pseudo-element_01.js
@@ -0,0 +1,260 @@
+/* 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 pseudoelements are displayed correctly in the rule view
+
+const TEST_URI = URL_ROOT + "doc_pseudoelement.html";
+const PSEUDO_PREF = "devtools.inspector.show_pseudo_elements";
+
+add_task(function* () {
+ yield pushPref(PSEUDO_PREF, true);
+
+ yield addTab(TEST_URI);
+ let {inspector, view} = yield openRuleView();
+
+ yield testTopLeft(inspector, view);
+ yield testTopRight(inspector, view);
+ yield testBottomRight(inspector, view);
+ yield testBottomLeft(inspector, view);
+ yield testParagraph(inspector, view);
+ yield testBody(inspector, view);
+});
+
+function* testTopLeft(inspector, view) {
+ let id = "#topleft";
+ let rules = yield assertPseudoElementRulesNumbers(id,
+ inspector, view, {
+ elementRulesNb: 4,
+ firstLineRulesNb: 2,
+ firstLetterRulesNb: 1,
+ selectionRulesNb: 0,
+ afterRulesNb: 1,
+ beforeRulesNb: 2
+ }
+ );
+
+ let gutters = assertGutters(view);
+
+ info("Make sure that clicking on the twisty hides pseudo elements");
+ let expander = gutters[0].querySelector(".ruleview-expander");
+ ok(!view.element.children[1].hidden, "Pseudo Elements are expanded");
+
+ expander.click();
+ ok(view.element.children[1].hidden,
+ "Pseudo Elements are collapsed by twisty");
+
+ expander.click();
+ ok(!view.element.children[1].hidden, "Pseudo Elements are expanded again");
+
+ info("Make sure that dblclicking on the header container also toggles " +
+ "the pseudo elements");
+ EventUtils.synthesizeMouseAtCenter(gutters[0], {clickCount: 2},
+ view.styleWindow);
+ ok(view.element.children[1].hidden,
+ "Pseudo Elements are collapsed by dblclicking");
+
+ let elementRuleView = getRuleViewRuleEditor(view, 3);
+
+ let elementFirstLineRule = rules.firstLineRules[0];
+ let elementFirstLineRuleView =
+ [...view.element.children[1].children].filter(e => {
+ return e._ruleEditor && e._ruleEditor.rule === elementFirstLineRule;
+ })[0]._ruleEditor;
+
+ is(convertTextPropsToString(elementFirstLineRule.textProps),
+ "color: orange",
+ "TopLeft firstLine properties are correct");
+
+ let onAdded = view.once("ruleview-changed");
+ let firstProp = elementFirstLineRuleView.addProperty("background-color",
+ "rgb(0, 255, 0)", "", true);
+ yield onAdded;
+
+ onAdded = view.once("ruleview-changed");
+ let secondProp = elementFirstLineRuleView.addProperty("font-style",
+ "italic", "", true);
+ yield onAdded;
+
+ is(firstProp,
+ elementFirstLineRule.textProps[elementFirstLineRule.textProps.length - 2],
+ "First added property is on back of array");
+ is(secondProp,
+ elementFirstLineRule.textProps[elementFirstLineRule.textProps.length - 1],
+ "Second added property is on back of array");
+
+ is((yield getComputedStyleProperty(id, ":first-line", "background-color")),
+ "rgb(0, 255, 0)", "Added property should have been used.");
+ is((yield getComputedStyleProperty(id, ":first-line", "font-style")),
+ "italic", "Added property should have been used.");
+ is((yield getComputedStyleProperty(id, null, "text-decoration")),
+ "none", "Added property should not apply to element");
+
+ yield togglePropStatus(view, firstProp);
+
+ is((yield getComputedStyleProperty(id, ":first-line", "background-color")),
+ "rgb(255, 0, 0)", "Disabled property should now have been used.");
+ is((yield getComputedStyleProperty(id, null, "background-color")),
+ "rgb(221, 221, 221)", "Added property should not apply to element");
+
+ yield togglePropStatus(view, firstProp);
+
+ is((yield getComputedStyleProperty(id, ":first-line", "background-color")),
+ "rgb(0, 255, 0)", "Added property should have been used.");
+ is((yield getComputedStyleProperty(id, null, "text-decoration")),
+ "none", "Added property should not apply to element");
+
+ onAdded = view.once("ruleview-changed");
+ firstProp = elementRuleView.addProperty("background-color",
+ "rgb(0, 0, 255)", "", true);
+ yield onAdded;
+
+ is((yield getComputedStyleProperty(id, null, "background-color")),
+ "rgb(0, 0, 255)", "Added property should have been used.");
+ is((yield getComputedStyleProperty(id, ":first-line", "background-color")),
+ "rgb(0, 255, 0)", "Added prop does not apply to pseudo");
+}
+
+function* testTopRight(inspector, view) {
+ yield assertPseudoElementRulesNumbers("#topright", inspector, view, {
+ elementRulesNb: 4,
+ firstLineRulesNb: 1,
+ firstLetterRulesNb: 1,
+ selectionRulesNb: 0,
+ beforeRulesNb: 2,
+ afterRulesNb: 1
+ });
+
+ let gutters = assertGutters(view);
+
+ let expander = gutters[0].querySelector(".ruleview-expander");
+ ok(!view.element.firstChild.classList.contains("show-expandable-container"),
+ "Pseudo Elements remain collapsed after switching element");
+
+ expander.scrollIntoView();
+ expander.click();
+ ok(!view.element.children[1].hidden,
+ "Pseudo Elements are shown again after clicking twisty");
+}
+
+function* testBottomRight(inspector, view) {
+ yield assertPseudoElementRulesNumbers("#bottomright", inspector, view, {
+ elementRulesNb: 4,
+ firstLineRulesNb: 1,
+ firstLetterRulesNb: 1,
+ selectionRulesNb: 0,
+ beforeRulesNb: 3,
+ afterRulesNb: 1
+ });
+}
+
+function* testBottomLeft(inspector, view) {
+ yield assertPseudoElementRulesNumbers("#bottomleft", inspector, view, {
+ elementRulesNb: 4,
+ firstLineRulesNb: 1,
+ firstLetterRulesNb: 1,
+ selectionRulesNb: 0,
+ beforeRulesNb: 2,
+ afterRulesNb: 1
+ });
+}
+
+function* testParagraph(inspector, view) {
+ let rules =
+ yield assertPseudoElementRulesNumbers("#bottomleft p", inspector, view, {
+ elementRulesNb: 3,
+ firstLineRulesNb: 1,
+ firstLetterRulesNb: 1,
+ selectionRulesNb: 1,
+ beforeRulesNb: 0,
+ afterRulesNb: 0
+ });
+
+ assertGutters(view);
+
+ let elementFirstLineRule = rules.firstLineRules[0];
+ is(convertTextPropsToString(elementFirstLineRule.textProps),
+ "background: blue",
+ "Paragraph first-line properties are correct");
+
+ let elementFirstLetterRule = rules.firstLetterRules[0];
+ is(convertTextPropsToString(elementFirstLetterRule.textProps),
+ "color: red; font-size: 130%",
+ "Paragraph first-letter properties are correct");
+
+ let elementSelectionRule = rules.selectionRules[0];
+ is(convertTextPropsToString(elementSelectionRule.textProps),
+ "color: white; background: black",
+ "Paragraph first-letter properties are correct");
+}
+
+function* testBody(inspector, view) {
+ yield testNode("body", inspector, view);
+
+ let gutters = getGutters(view);
+ is(gutters.length, 0, "There are no gutter headings");
+}
+
+function convertTextPropsToString(textProps) {
+ return textProps.map(t => t.name + ": " + t.value).join("; ");
+}
+
+function* testNode(selector, inspector, view) {
+ yield selectNode(selector, inspector);
+ let elementStyle = view._elementStyle;
+ return elementStyle;
+}
+
+function* assertPseudoElementRulesNumbers(selector, inspector, view, ruleNbs) {
+ let elementStyle = yield testNode(selector, inspector, view);
+
+ let rules = {
+ elementRules: elementStyle.rules.filter(rule => !rule.pseudoElement),
+ firstLineRules: elementStyle.rules.filter(rule =>
+ rule.pseudoElement === ":first-line"),
+ firstLetterRules: elementStyle.rules.filter(rule =>
+ rule.pseudoElement === ":first-letter"),
+ selectionRules: elementStyle.rules.filter(rule =>
+ rule.pseudoElement === ":-moz-selection"),
+ beforeRules: elementStyle.rules.filter(rule =>
+ rule.pseudoElement === ":before"),
+ afterRules: elementStyle.rules.filter(rule =>
+ rule.pseudoElement === ":after"),
+ };
+
+ is(rules.elementRules.length, ruleNbs.elementRulesNb,
+ selector + " has the correct number of non pseudo element rules");
+ is(rules.firstLineRules.length, ruleNbs.firstLineRulesNb,
+ selector + " has the correct number of :first-line rules");
+ is(rules.firstLetterRules.length, ruleNbs.firstLetterRulesNb,
+ selector + " has the correct number of :first-letter rules");
+ is(rules.selectionRules.length, ruleNbs.selectionRulesNb,
+ selector + " has the correct number of :selection rules");
+ is(rules.beforeRules.length, ruleNbs.beforeRulesNb,
+ selector + " has the correct number of :before rules");
+ is(rules.afterRules.length, ruleNbs.afterRulesNb,
+ selector + " has the correct number of :after rules");
+
+ return rules;
+}
+
+function getGutters(view) {
+ return view.element.querySelectorAll(".theme-gutter");
+}
+
+function assertGutters(view) {
+ let gutters = getGutters(view);
+
+ is(gutters.length, 3,
+ "There are 3 gutter headings");
+ is(gutters[0].textContent, "Pseudo-elements",
+ "Gutter heading is correct");
+ is(gutters[1].textContent, "This Element",
+ "Gutter heading is correct");
+ is(gutters[2].textContent, "Inherited from body",
+ "Gutter heading is correct");
+
+ return gutters;
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_pseudo-element_02.js b/devtools/client/inspector/rules/test/browser_rules_pseudo-element_02.js
new file mode 100644
index 000000000..f69c328db
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_pseudo-element_02.js
@@ -0,0 +1,29 @@
+/* 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 pseudoelements are displayed correctly in the markup view.
+
+const TEST_URI = URL_ROOT + "doc_pseudoelement.html";
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let {inspector} = yield openRuleView();
+
+ let node = yield getNodeFront("#topleft", inspector);
+ let children = yield inspector.markup.walker.children(node);
+
+ is(children.nodes.length, 3, "Element has correct number of children");
+
+ let beforeElement = children.nodes[0];
+ is(beforeElement.tagName, "_moz_generated_content_before",
+ "tag name is correct");
+ yield selectNode(beforeElement, inspector);
+
+ let afterElement = children.nodes[children.nodes.length - 1];
+ is(afterElement.tagName, "_moz_generated_content_after",
+ "tag name is correct");
+ yield selectNode(afterElement, inspector);
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_pseudo_lock_options.js b/devtools/client/inspector/rules/test/browser_rules_pseudo_lock_options.js
new file mode 100644
index 000000000..d795ba5f3
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_pseudo_lock_options.js
@@ -0,0 +1,131 @@
+/* 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";
+
+// Tests that the rule view pseudo lock options work properly.
+
+const TEST_URI = `
+ <style type='text/css'>
+ div {
+ color: red;
+ }
+ div:hover {
+ color: blue;
+ }
+ div:active {
+ color: yellow;
+ }
+ div:focus {
+ color: green;
+ }
+ </style>
+ <div>test div</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("div", inspector);
+
+ yield assertPseudoPanelClosed(view);
+
+ info("Toggle the pseudo class panel open");
+ view.pseudoClassToggle.click();
+ yield assertPseudoPanelOpened(view);
+
+ info("Toggle each pseudo lock and check that the pseudo lock is added");
+ yield togglePseudoClass(inspector, view.hoverCheckbox);
+ yield assertPseudoAdded(inspector, view, ":hover", 3, 1);
+ yield togglePseudoClass(inspector, view.hoverCheckbox);
+ yield assertPseudoRemoved(inspector, view, 2);
+
+ yield togglePseudoClass(inspector, view.activeCheckbox);
+ yield assertPseudoAdded(inspector, view, ":active", 3, 1);
+ yield togglePseudoClass(inspector, view.activeCheckbox);
+ yield assertPseudoRemoved(inspector, view, 2);
+
+ yield togglePseudoClass(inspector, view.focusCheckbox);
+ yield assertPseudoAdded(inspector, view, ":focus", 3, 1);
+ yield togglePseudoClass(inspector, view.focusCheckbox);
+ yield assertPseudoRemoved(inspector, view, 2);
+
+ info("Toggle all pseudo lock and check that the pseudo lock is added");
+ yield togglePseudoClass(inspector, view.hoverCheckbox);
+ yield togglePseudoClass(inspector, view.activeCheckbox);
+ yield togglePseudoClass(inspector, view.focusCheckbox);
+ yield assertPseudoAdded(inspector, view, ":focus", 5, 1);
+ yield assertPseudoAdded(inspector, view, ":active", 5, 2);
+ yield assertPseudoAdded(inspector, view, ":hover", 5, 3);
+ yield togglePseudoClass(inspector, view.hoverCheckbox);
+ yield togglePseudoClass(inspector, view.activeCheckbox);
+ yield togglePseudoClass(inspector, view.focusCheckbox);
+ yield assertPseudoRemoved(inspector, view, 2);
+
+ info("Select a null element");
+ yield view.selectElement(null);
+ ok(!view.hoverCheckbox.checked && view.hoverCheckbox.disabled,
+ ":hover checkbox is unchecked and disabled");
+ ok(!view.activeCheckbox.checked && view.activeCheckbox.disabled,
+ ":active checkbox is unchecked and disabled");
+ ok(!view.focusCheckbox.checked && view.focusCheckbox.disabled,
+ ":focus checkbox is unchecked and disabled");
+
+ info("Toggle the pseudo class panel close");
+ view.pseudoClassToggle.click();
+ yield assertPseudoPanelClosed(view);
+});
+
+function* togglePseudoClass(inspector, pseudoClassOption) {
+ info("Toggle the pseudoclass, wait for it to be applied");
+ let onRefresh = inspector.once("rule-view-refreshed");
+ pseudoClassOption.click();
+ yield onRefresh;
+}
+
+function* assertPseudoAdded(inspector, view, pseudoClass, numRules,
+ childIndex) {
+ info("Check that the ruleview contains the pseudo-class rule");
+ is(view.element.children.length, numRules,
+ "Should have " + numRules + " rules.");
+ is(getRuleViewRuleEditor(view, childIndex).rule.selectorText,
+ "div" + pseudoClass, "rule view is showing " + pseudoClass + " rule");
+}
+
+function* assertPseudoRemoved(inspector, view, numRules) {
+ info("Check that the ruleview no longer contains the pseudo-class rule");
+ is(view.element.children.length, numRules,
+ "Should have " + numRules + " rules.");
+ is(getRuleViewRuleEditor(view, 1).rule.selectorText, "div",
+ "Second rule is div");
+}
+
+function* assertPseudoPanelOpened(view) {
+ info("Check the opened state of the pseudo class panel");
+
+ ok(!view.pseudoClassPanel.hidden, "Pseudo Class Panel Opened");
+ ok(!view.hoverCheckbox.disabled, ":hover checkbox is not disabled");
+ ok(!view.activeCheckbox.disabled, ":active checkbox is not disabled");
+ ok(!view.focusCheckbox.disabled, ":focus checkbox is not disabled");
+
+ is(view.hoverCheckbox.getAttribute("tabindex"), "0",
+ ":hover checkbox has a tabindex of 0");
+ is(view.activeCheckbox.getAttribute("tabindex"), "0",
+ ":active checkbox has a tabindex of 0");
+ is(view.focusCheckbox.getAttribute("tabindex"), "0",
+ ":focus checkbox has a tabindex of 0");
+}
+
+function* assertPseudoPanelClosed(view) {
+ info("Check the closed state of the pseudo clas panel");
+
+ ok(view.pseudoClassPanel.hidden, "Pseudo Class Panel Hidden");
+
+ is(view.hoverCheckbox.getAttribute("tabindex"), "-1",
+ ":hover checkbox has a tabindex of -1");
+ is(view.activeCheckbox.getAttribute("tabindex"), "-1",
+ ":active checkbox has a tabindex of -1");
+ is(view.focusCheckbox.getAttribute("tabindex"), "-1",
+ ":focus checkbox has a tabindex of -1");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_refresh-no-flicker.js b/devtools/client/inspector/rules/test/browser_rules_refresh-no-flicker.js
new file mode 100644
index 000000000..25ea3d972
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_refresh-no-flicker.js
@@ -0,0 +1,39 @@
+/* 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 rule view does not go blank while selecting a new node.
+
+const TESTCASE_URI = "data:text/html;charset=utf-8," +
+ "<div id=\"testdiv\" style=\"font-size:10px;\">" +
+ "Test div!</div>";
+
+add_task(function* () {
+ yield addTab(TESTCASE_URI);
+
+ info("Opening the rule view and selecting the test node");
+ let {inspector, view} = yield openRuleView();
+ let testdiv = yield getNodeFront("#testdiv", inspector);
+ yield selectNode(testdiv, inspector);
+
+ let htmlBefore = view.element.innerHTML;
+ ok(htmlBefore.indexOf("font-size") > -1,
+ "The rule view should contain a font-size property.");
+
+ // Do the selectNode call manually, because otherwise it's hard to guarantee
+ // that we can make the below checks at a reasonable time.
+ info("refreshing the node");
+ let p = view.selectElement(testdiv, true);
+ is(view.element.innerHTML, htmlBefore,
+ "The rule view is unchanged during selection.");
+ ok(view.element.classList.contains("non-interactive"),
+ "The rule view is marked non-interactive.");
+ yield p;
+
+ info("node refreshed");
+ ok(!view.element.classList.contains("non-interactive"),
+ "The rule view is marked interactive again.");
+});
+
diff --git a/devtools/client/inspector/rules/test/browser_rules_refresh-on-attribute-change_01.js b/devtools/client/inspector/rules/test/browser_rules_refresh-on-attribute-change_01.js
new file mode 100644
index 000000000..381a6bda2
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_refresh-on-attribute-change_01.js
@@ -0,0 +1,61 @@
+/* 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 changing the current element's attributes refreshes the rule-view
+
+const TEST_URI = `
+ <style type="text/css">
+ #testid {
+ background-color: blue;
+ }
+ .testclass {
+ background-color: green;
+ }
+ </style>
+ <div id="testid" class="testclass" style="margin-top: 1px; padding-top: 5px;">
+ Styled Node
+ </div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view, testActor} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ info("Checking that the rule-view has the element, #testid and " +
+ ".testclass selectors");
+ checkRuleViewContent(view, ["element", "#testid", ".testclass"]);
+
+ info("Changing the node's ID attribute and waiting for the " +
+ "rule-view refresh");
+ let ruleViewRefreshed = inspector.once("rule-view-refreshed");
+ yield testActor.setAttribute("#testid", "id", "differentid");
+ yield ruleViewRefreshed;
+
+ info("Checking that the rule-view doesn't have the #testid selector anymore");
+ checkRuleViewContent(view, ["element", ".testclass"]);
+
+ info("Reverting the ID attribute change");
+ ruleViewRefreshed = inspector.once("rule-view-refreshed");
+ yield testActor.setAttribute("#differentid", "id", "testid");
+ yield ruleViewRefreshed;
+
+ info("Checking that the rule-view has all the selectors again");
+ checkRuleViewContent(view, ["element", "#testid", ".testclass"]);
+});
+
+function checkRuleViewContent(view, expectedSelectors) {
+ let selectors = view.styleDocument
+ .querySelectorAll(".ruleview-selectorcontainer");
+
+ is(selectors.length, expectedSelectors.length,
+ expectedSelectors.length + " selectors are displayed");
+
+ for (let i = 0; i < expectedSelectors.length; i++) {
+ is(selectors[i].textContent.indexOf(expectedSelectors[i]), 0,
+ "Selector " + (i + 1) + " is " + expectedSelectors[i]);
+ }
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_refresh-on-attribute-change_02.js b/devtools/client/inspector/rules/test/browser_rules_refresh-on-attribute-change_02.js
new file mode 100644
index 000000000..6ee385faa
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_refresh-on-attribute-change_02.js
@@ -0,0 +1,153 @@
+/* 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 changing the current element's style attribute refreshes the
+// rule-view
+
+const TEST_URI = `
+ <div id="testid" class="testclass" style="margin-top: 1px; padding-top: 5px;">
+ Styled Node
+ </div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view, testActor} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ yield testPropertyChanges(inspector, view);
+ yield testPropertyChange0(inspector, view, "#testid", testActor);
+ yield testPropertyChange1(inspector, view, "#testid", testActor);
+ yield testPropertyChange2(inspector, view, "#testid", testActor);
+ yield testPropertyChange3(inspector, view, "#testid", testActor);
+ yield testPropertyChange4(inspector, view, "#testid", testActor);
+ yield testPropertyChange5(inspector, view, "#testid", testActor);
+ yield testPropertyChange6(inspector, view, "#testid", testActor);
+});
+
+function* testPropertyChanges(inspector, ruleView) {
+ info("Adding a second margin-top value in the element selector");
+ let ruleEditor = ruleView._elementStyle.rules[0].editor;
+ let onRefreshed = inspector.once("rule-view-refreshed");
+ ruleEditor.addProperty("margin-top", "5px", "", true);
+ yield onRefreshed;
+
+ let rule = ruleView._elementStyle.rules[0];
+ validateTextProp(rule.textProps[0], false, "margin-top", "1px",
+ "Original margin property active");
+}
+
+function* testPropertyChange0(inspector, ruleView, selector, testActor) {
+ yield changeElementStyle(selector, "margin-top: 1px; padding-top: 5px",
+ inspector, testActor);
+
+ let rule = ruleView._elementStyle.rules[0];
+ is(rule.editor.element.querySelectorAll(".ruleview-property").length, 3,
+ "Correct number of properties");
+ validateTextProp(rule.textProps[0], true, "margin-top", "1px",
+ "First margin property re-enabled");
+ validateTextProp(rule.textProps[2], false, "margin-top", "5px",
+ "Second margin property disabled");
+}
+
+function* testPropertyChange1(inspector, ruleView, selector, testActor) {
+ info("Now set it back to 5px, the 5px value should be re-enabled.");
+ yield changeElementStyle(selector, "margin-top: 5px; padding-top: 5px;",
+ inspector, testActor);
+
+ let rule = ruleView._elementStyle.rules[0];
+ is(rule.editor.element.querySelectorAll(".ruleview-property").length, 3,
+ "Correct number of properties");
+ validateTextProp(rule.textProps[0], false, "margin-top", "1px",
+ "First margin property re-enabled");
+ validateTextProp(rule.textProps[2], true, "margin-top", "5px",
+ "Second margin property disabled");
+}
+
+function* testPropertyChange2(inspector, ruleView, selector, testActor) {
+ info("Set the margin property to a value that doesn't exist in the editor.");
+ info("Should reuse the currently-enabled element (the second one.)");
+ yield changeElementStyle(selector, "margin-top: 15px; padding-top: 5px;",
+ inspector, testActor);
+
+ let rule = ruleView._elementStyle.rules[0];
+ is(rule.editor.element.querySelectorAll(".ruleview-property").length, 3,
+ "Correct number of properties");
+ validateTextProp(rule.textProps[0], false, "margin-top", "1px",
+ "First margin property re-enabled");
+ validateTextProp(rule.textProps[2], true, "margin-top", "15px",
+ "Second margin property disabled");
+}
+
+function* testPropertyChange3(inspector, ruleView, selector, testActor) {
+ info("Remove the padding-top attribute. Should disable the padding " +
+ "property but not remove it.");
+ yield changeElementStyle(selector, "margin-top: 5px;", inspector, testActor);
+
+ let rule = ruleView._elementStyle.rules[0];
+ is(rule.editor.element.querySelectorAll(".ruleview-property").length, 3,
+ "Correct number of properties");
+ validateTextProp(rule.textProps[1], false, "padding-top", "5px",
+ "Padding property disabled");
+}
+
+function* testPropertyChange4(inspector, ruleView, selector, testActor) {
+ info("Put the padding-top attribute back in, should re-enable the " +
+ "padding property.");
+ yield changeElementStyle(selector, "margin-top: 5px; padding-top: 25px",
+ inspector, testActor);
+
+ let rule = ruleView._elementStyle.rules[0];
+ is(rule.editor.element.querySelectorAll(".ruleview-property").length, 3,
+ "Correct number of properties");
+ validateTextProp(rule.textProps[1], true, "padding-top", "25px",
+ "Padding property enabled");
+}
+
+function* testPropertyChange5(inspector, ruleView, selector, testActor) {
+ info("Add an entirely new property");
+ yield changeElementStyle(selector,
+ "margin-top: 5px; padding-top: 25px; padding-left: 20px;",
+ inspector, testActor);
+
+ let rule = ruleView._elementStyle.rules[0];
+ is(rule.editor.element.querySelectorAll(".ruleview-property").length, 4,
+ "Added a property");
+ validateTextProp(rule.textProps[3], true, "padding-left", "20px",
+ "Padding property enabled");
+}
+
+function* testPropertyChange6(inspector, ruleView, selector, testActor) {
+ info("Add an entirely new property again");
+ yield changeElementStyle(selector, "background: red " +
+ "url(\"chrome://branding/content/about-logo.png\") repeat scroll 0% 0%",
+ inspector, testActor);
+
+ let rule = ruleView._elementStyle.rules[0];
+ is(rule.editor.element.querySelectorAll(".ruleview-property").length, 5,
+ "Added a property");
+ validateTextProp(rule.textProps[4], true, "background",
+ "red url(\"chrome://branding/content/about-logo.png\") repeat scroll 0% 0%",
+ "shortcut property correctly set");
+}
+
+function* changeElementStyle(selector, style, inspector, testActor) {
+ let onRefreshed = inspector.once("rule-view-refreshed");
+ yield testActor.setAttribute(selector, "style", style);
+ yield onRefreshed;
+}
+
+function validateTextProp(prop, enabled, name, value, desc) {
+ is(prop.enabled, enabled, desc + ": enabled.");
+ is(prop.name, name, desc + ": name.");
+ is(prop.value, value, desc + ": value.");
+
+ is(prop.editor.enable.hasAttribute("checked"), enabled,
+ desc + ": enabled checkbox.");
+ is(prop.editor.nameSpan.textContent, name, desc + ": name span.");
+ is(prop.editor.valueSpan.textContent,
+ value, desc + ": value span.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_refresh-on-style-change.js b/devtools/client/inspector/rules/test/browser_rules_refresh-on-style-change.js
new file mode 100644
index 000000000..81ff9d4d5
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_refresh-on-style-change.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 the rule view refreshes when the current node has its style
+// changed
+
+const TEST_URI = "<div id='testdiv' style='font-size: 10px;''>Test div!</div>";
+
+add_task(function* () {
+ Services.prefs.setCharPref("devtools.defaultColorUnit", "name");
+
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view, testActor} = yield openRuleView();
+ yield selectNode("#testdiv", inspector);
+
+ let fontSize = getRuleViewPropertyValue(view, "element", "font-size");
+ is(fontSize, "10px", "The rule view shows the right font-size");
+
+ info("Changing the node's style and waiting for the update");
+ let onUpdated = inspector.once("rule-view-refreshed");
+ yield testActor.setAttribute("#testdiv", "style",
+ "font-size: 3em; color: lightgoldenrodyellow; " +
+ "text-align: right; text-transform: uppercase");
+ yield onUpdated;
+
+ let textAlign = getRuleViewPropertyValue(view, "element", "text-align");
+ is(textAlign, "right", "The rule view shows the new text align.");
+ let color = getRuleViewPropertyValue(view, "element", "color");
+ is(color, "lightgoldenrodyellow", "The rule view shows the new color.");
+ fontSize = getRuleViewPropertyValue(view, "element", "font-size");
+ is(fontSize, "3em", "The rule view shows the new font size.");
+ let textTransform = getRuleViewPropertyValue(view, "element",
+ "text-transform");
+ is(textTransform, "uppercase", "The rule view shows the new text transform.");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_01.js b/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_01.js
new file mode 100644
index 000000000..f4c47bba0
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_01.js
@@ -0,0 +1,156 @@
+/* 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";
+
+// Tests that the rule view search filter and clear button works properly in
+// the computed list.
+
+const TEST_URI = `
+ <style type="text/css">
+ #testid {
+ margin: 4px 0px;
+ }
+ .testclass {
+ background-color: red;
+ }
+ </style>
+ <h1 id="testid" class="testclass">Styled Node</h1>
+`;
+
+const TEST_DATA = [
+ {
+ desc: "Tests that the search filter works properly in the computed list " +
+ "for property names",
+ search: "margin",
+ isExpanderOpen: false,
+ isFilterOpen: false,
+ isMarginHighlighted: true,
+ isMarginTopHighlighted: true,
+ isMarginRightHighlighted: true,
+ isMarginBottomHighlighted: true,
+ isMarginLeftHighlighted: true
+ },
+ {
+ desc: "Tests that the search filter works properly in the computed list " +
+ "for property values",
+ search: "0px",
+ isExpanderOpen: false,
+ isFilterOpen: false,
+ isMarginHighlighted: true,
+ isMarginTopHighlighted: false,
+ isMarginRightHighlighted: true,
+ isMarginBottomHighlighted: false,
+ isMarginLeftHighlighted: true
+ },
+ {
+ desc: "Tests that the search filter works properly in the computed list " +
+ "for property line input",
+ search: "margin-top:4px",
+ isExpanderOpen: true,
+ isFilterOpen: true,
+ isMarginHighlighted: false,
+ isMarginTopHighlighted: true,
+ isMarginRightHighlighted: false,
+ isMarginBottomHighlighted: false,
+ isMarginLeftHighlighted: false
+ },
+ {
+ desc: "Tests that the search filter works properly in the computed list " +
+ "for parsed name",
+ search: "margin-top:",
+ isExpanderOpen: true,
+ isFilterOpen: true,
+ isMarginHighlighted: false,
+ isMarginTopHighlighted: true,
+ isMarginRightHighlighted: false,
+ isMarginBottomHighlighted: false,
+ isMarginLeftHighlighted: false
+ },
+ {
+ desc: "Tests that the search filter works properly in the computed list " +
+ "for parsed property value",
+ search: ":4px",
+ isExpanderOpen: false,
+ isFilterOpen: false,
+ isMarginHighlighted: true,
+ isMarginTopHighlighted: true,
+ isMarginRightHighlighted: false,
+ isMarginBottomHighlighted: true,
+ isMarginLeftHighlighted: false
+ }
+];
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+ yield testAddTextInFilter(inspector, view);
+});
+
+function* testAddTextInFilter(inspector, view) {
+ for (let data of TEST_DATA) {
+ info(data.desc);
+ yield setSearchFilter(view, data.search);
+ yield checkRules(view, data);
+ yield clearSearchAndCheckRules(view);
+ }
+}
+
+function* checkRules(view, data) {
+ info("Check that the correct rules are visible");
+ is(view.element.children.length, 2, "Should have 2 rules.");
+ is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element",
+ "First rule is inline element.");
+
+ let rule = getRuleViewRuleEditor(view, 1).rule;
+ let textPropEditor = rule.textProps[0].editor;
+ let computed = textPropEditor.computed;
+
+ is(rule.selectorText, "#testid", "Second rule is #testid.");
+ is(!!textPropEditor.expander.getAttribute("open"), data.isExpanderOpen,
+ "Got correct expander state.");
+ is(computed.hasAttribute("filter-open"), data.isFilterOpen,
+ "Got correct expanded state for margin computed list.");
+ is(textPropEditor.container.classList.contains("ruleview-highlight"),
+ data.isMarginHighlighted,
+ "Got correct highlight for margin text property.");
+
+ is(computed.children[0].classList.contains("ruleview-highlight"),
+ data.isMarginTopHighlighted,
+ "Got correct highlight for margin-top computed property.");
+ is(computed.children[1].classList.contains("ruleview-highlight"),
+ data.isMarginRightHighlighted,
+ "Got correct highlight for margin-right computed property.");
+ is(computed.children[2].classList.contains("ruleview-highlight"),
+ data.isMarginBottomHighlighted,
+ "Got correct highlight for margin-bottom computed property.");
+ is(computed.children[3].classList.contains("ruleview-highlight"),
+ data.isMarginLeftHighlighted,
+ "Got correct highlight for margin-left computed property.");
+}
+
+function* clearSearchAndCheckRules(view) {
+ let win = view.styleWindow;
+ let searchField = view.searchField;
+ let searchClearButton = view.searchClearButton;
+
+ let rule = getRuleViewRuleEditor(view, 1).rule;
+ let textPropEditor = rule.textProps[0].editor;
+ let computed = textPropEditor.computed;
+
+ info("Clearing the search filter");
+ EventUtils.synthesizeMouseAtCenter(searchClearButton, {}, win);
+ yield view.inspector.once("ruleview-filtered");
+
+ info("Check the search filter is cleared and no rules are highlighted");
+ is(view.element.children.length, 3, "Should have 3 rules.");
+ ok(!searchField.value, "Search filter is cleared");
+ ok(!view.styleDocument.querySelectorAll(".ruleview-highlight").length,
+ "No rules are higlighted");
+
+ ok(!textPropEditor.expander.getAttribute("open"), "Expander is closed.");
+ ok(!computed.hasAttribute("filter-open"),
+ "margin computed list is closed.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_02.js b/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_02.js
new file mode 100644
index 000000000..911f09ff3
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_02.js
@@ -0,0 +1,93 @@
+/* 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";
+
+// Tests that the rule view search filter works properly in the computed list
+// when modifying the existing search filter value
+
+const SEARCH = "margin-";
+
+const TEST_URI = `
+ <style type="text/css">
+ #testid {
+ margin: 4px 0px;
+ }
+ .testclass {
+ background-color: red;
+ }
+ </style>
+ <h1 id="testid" class="testclass">Styled Node</h1>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+ yield testAddTextInFilter(inspector, view);
+ yield testRemoveTextInFilter(inspector, view);
+});
+
+function* testAddTextInFilter(inspector, view) {
+ yield setSearchFilter(view, SEARCH);
+
+ info("Check that the correct rules are visible");
+ is(view.element.children.length, 2, "Should have 2 rules.");
+ is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element",
+ "First rule is inline element.");
+
+ let rule = getRuleViewRuleEditor(view, 1).rule;
+ let ruleEditor = rule.textProps[0].editor;
+ let computed = ruleEditor.computed;
+
+ is(rule.selectorText, "#testid", "Second rule is #testid.");
+ ok(ruleEditor.expander.getAttribute("open"), "Expander is open.");
+ ok(!ruleEditor.container.classList.contains("ruleview-highlight"),
+ "margin text property is not highlighted.");
+ ok(computed.hasAttribute("filter-open"), "margin computed list is open.");
+
+ ok(computed.children[0].classList.contains("ruleview-highlight"),
+ "margin-top computed property is correctly highlighted.");
+ ok(computed.children[1].classList.contains("ruleview-highlight"),
+ "margin-right computed property is correctly highlighted.");
+ ok(computed.children[2].classList.contains("ruleview-highlight"),
+ "margin-bottom computed property is correctly highlighted.");
+ ok(computed.children[3].classList.contains("ruleview-highlight"),
+ "margin-left computed property is correctly highlighted.");
+}
+
+function* testRemoveTextInFilter(inspector, view) {
+ info("Press backspace and set filter text to \"margin\"");
+
+ let win = view.styleWindow;
+ let searchField = view.searchField;
+
+ searchField.focus();
+ EventUtils.synthesizeKey("VK_BACK_SPACE", {}, win);
+ yield inspector.once("ruleview-filtered");
+
+ info("Check that the correct rules are visible");
+ is(view.element.children.length, 2, "Should have 2 rules.");
+ is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element",
+ "First rule is inline element.");
+
+ let rule = getRuleViewRuleEditor(view, 1).rule;
+ let ruleEditor = rule.textProps[0].editor;
+ let computed = ruleEditor.computed;
+
+ is(rule.selectorText, "#testid", "Second rule is #testid.");
+ ok(!ruleEditor.expander.getAttribute("open"), "Expander is closed.");
+ ok(ruleEditor.container.classList.contains("ruleview-highlight"),
+ "margin text property is correctly highlighted.");
+ ok(!computed.hasAttribute("filter-open"), "margin computed list is closed.");
+
+ ok(computed.children[0].classList.contains("ruleview-highlight"),
+ "margin-top computed property is correctly highlighted.");
+ ok(computed.children[1].classList.contains("ruleview-highlight"),
+ "margin-right computed property is correctly highlighted.");
+ ok(computed.children[2].classList.contains("ruleview-highlight"),
+ "margin-bottom computed property is correctly highlighted.");
+ ok(computed.children[3].classList.contains("ruleview-highlight"),
+ "margin-left computed property is correctly highlighted.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_03.js b/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_03.js
new file mode 100644
index 000000000..1d8063419
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_03.js
@@ -0,0 +1,49 @@
+/* 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";
+
+// Tests that the rule view search filter works properly in the computed list
+// for color values.
+
+// The color format here is chosen to match the default returned by
+// CssColor.toString.
+const SEARCH = "background-color: rgb(243, 243, 243)";
+
+const TEST_URI = `
+ <style type="text/css">
+ .testclass {
+ background: rgb(243, 243, 243) none repeat scroll 0% 0%;
+ }
+ </style>
+ <div class="testclass">Styled Node</h1>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode(".testclass", inspector);
+ yield testAddTextInFilter(inspector, view);
+});
+
+function* testAddTextInFilter(inspector, view) {
+ yield setSearchFilter(view, SEARCH);
+
+ info("Check that the correct rules are visible");
+ is(view.element.children.length, 2, "Should have 2 rules.");
+ is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element",
+ "First rule is inline element.");
+
+ let rule = getRuleViewRuleEditor(view, 1).rule;
+ let ruleEditor = rule.textProps[0].editor;
+ let computed = ruleEditor.computed;
+
+ is(rule.selectorText, ".testclass", "Second rule is .testclass.");
+ ok(ruleEditor.expander.getAttribute("open"), "Expander is open.");
+ ok(!ruleEditor.container.classList.contains("ruleview-highlight"),
+ "background property is not highlighted.");
+ ok(computed.hasAttribute("filter-open"), "background computed list is open.");
+ ok(computed.children[0].classList.contains("ruleview-highlight"),
+ "background-color computed property is highlighted.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_04.js b/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_04.js
new file mode 100644
index 000000000..05b8b01eb
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_04.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";
+
+// Tests that the rule view search filter works properly in the computed list
+// for newly modified property values.
+
+const SEARCH = "0px";
+
+const TEST_URI = `
+ <style type='text/css'>
+ #testid {
+ margin: 4px;
+ top: 0px;
+ }
+ </style>
+ <h1 id='testid'>Styled Node</h1>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+ yield testModifyPropertyValueFilter(inspector, view);
+});
+
+function* testModifyPropertyValueFilter(inspector, view) {
+ yield setSearchFilter(view, SEARCH);
+
+ let rule = getRuleViewRuleEditor(view, 1).rule;
+ let propEditor = rule.textProps[0].editor;
+ let computed = propEditor.computed;
+ let editor = yield focusEditableField(view, propEditor.valueSpan);
+
+ info("Check that the correct rules are visible");
+ is(rule.selectorText, "#testid", "Second rule is #testid.");
+ ok(!propEditor.container.classList.contains("ruleview-highlight"),
+ "margin text property is not highlighted.");
+ ok(rule.textProps[1].editor.container.classList
+ .contains("ruleview-highlight"),
+ "top text property is correctly highlighted.");
+
+ let onBlur = once(editor.input, "blur");
+ let onModification = view.once("ruleview-changed");
+ EventUtils.sendString("4px 0px", view.styleWindow);
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ yield onBlur;
+ yield onModification;
+
+ ok(propEditor.container.classList.contains("ruleview-highlight"),
+ "margin text property is correctly highlighted.");
+ ok(!computed.hasAttribute("filter-open"), "margin computed list is closed.");
+ ok(!computed.children[0].classList.contains("ruleview-highlight"),
+ "margin-top computed property is not highlighted.");
+ ok(computed.children[1].classList.contains("ruleview-highlight"),
+ "margin-right computed property is correctly highlighted.");
+ ok(!computed.children[2].classList.contains("ruleview-highlight"),
+ "margin-bottom computed property is not highlighted.");
+ ok(computed.children[3].classList.contains("ruleview-highlight"),
+ "margin-left computed property is correctly highlighted.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_expander.js b/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_expander.js
new file mode 100644
index 000000000..c8b1e0869
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_expander.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";
+
+// Tests that the expanded computed list for a property remains open after
+// clearing the rule view search filter.
+
+const SEARCH = "0px";
+
+const TEST_URI = `
+ <style type="text/css">
+ #testid {
+ margin: 4px 0px;
+ }
+ .testclass {
+ background-color: red;
+ }
+ </style>
+ <h1 id="testid" class="testclass">Styled Node</h1>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+ yield testOpenExpanderAndAddTextInFilter(inspector, view);
+ yield testClearSearchFilter(inspector, view);
+});
+
+function* testOpenExpanderAndAddTextInFilter(inspector, view) {
+ let rule = getRuleViewRuleEditor(view, 1).rule;
+ let ruleEditor = rule.textProps[0].editor;
+ let computed = ruleEditor.computed;
+
+ info("Opening the computed list of margin property");
+ ruleEditor.expander.click();
+
+ yield setSearchFilter(view, SEARCH);
+
+ info("Check that the correct rules are visible");
+ is(view.element.children.length, 2, "Should have 2 rules.");
+ is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element",
+ "First rule is inline element.");
+
+ is(rule.selectorText, "#testid", "Second rule is #testid.");
+ ok(ruleEditor.expander.getAttribute("open"), "Expander is open.");
+ ok(ruleEditor.container.classList.contains("ruleview-highlight"),
+ "margin text property is correctly highlighted.");
+ ok(!computed.hasAttribute("filter-open"),
+ "margin computed list does not contain filter-open class.");
+ ok(computed.hasAttribute("user-open"),
+ "margin computed list contains user-open attribute.");
+
+ ok(!computed.children[0].classList.contains("ruleview-highlight"),
+ "margin-top computed property is not highlighted.");
+ ok(computed.children[1].classList.contains("ruleview-highlight"),
+ "margin-right computed property is correctly highlighted.");
+ ok(!computed.children[2].classList.contains("ruleview-highlight"),
+ "margin-bottom computed property is not highlighted.");
+ ok(computed.children[3].classList.contains("ruleview-highlight"),
+ "margin-left computed property is correctly highlighted.");
+}
+
+function* testClearSearchFilter(inspector, view) {
+ info("Clearing the search filter");
+
+ let searchField = view.searchField;
+ let searchClearButton = view.searchClearButton;
+ let onRuleViewFiltered = inspector.once("ruleview-filtered");
+
+ EventUtils.synthesizeMouseAtCenter(searchClearButton, {},
+ view.styleWindow);
+
+ yield onRuleViewFiltered;
+
+ info("Check the search filter is cleared and no rules are highlighted");
+ is(view.element.children.length, 3, "Should have 3 rules.");
+ ok(!searchField.value, "Search filter is cleared");
+ ok(!view.styleDocument.querySelectorAll(".ruleview-highlight").length,
+ "No rules are higlighted");
+
+ let ruleEditor = getRuleViewRuleEditor(view, 1).rule.textProps[0].editor;
+ let computed = ruleEditor.computed;
+
+ ok(ruleEditor.expander.getAttribute("open"), "Expander is open.");
+ ok(!computed.hasAttribute("filter-open"),
+ "margin computed list does not contain filter-open class.");
+ ok(computed.hasAttribute("user-open"),
+ "margin computed list contains user-open attribute.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter-overridden-property.js b/devtools/client/inspector/rules/test/browser_rules_search-filter-overridden-property.js
new file mode 100644
index 000000000..3e634b76e
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_search-filter-overridden-property.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";
+
+// Tests that the rule view overriden search filter works properly for
+// overridden properties.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #testid {
+ width: 100%;
+ }
+ h1 {
+ width: 50%;
+ }
+ </style>
+ <h1 id='testid' class='testclass'>Styled Node</h1>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+ yield testFilterOverriddenProperty(inspector, view);
+});
+
+function* testFilterOverriddenProperty(inspector, ruleView) {
+ info("Check that the correct rules are visible");
+ is(ruleView.element.children.length, 3, "Should have 3 rules.");
+
+ let rule = getRuleViewRuleEditor(ruleView, 1).rule;
+ let textPropEditor = rule.textProps[0].editor;
+ is(rule.selectorText, "#testid", "Second rule is #testid.");
+ ok(!textPropEditor.element.classList.contains("ruleview-overridden"),
+ "width property is not overridden.");
+ ok(textPropEditor.filterProperty.hidden,
+ "Overridden search button is hidden.");
+
+ rule = getRuleViewRuleEditor(ruleView, 2).rule;
+ textPropEditor = rule.textProps[0].editor;
+ is(rule.selectorText, "h1", "Third rule is h1.");
+ ok(textPropEditor.element.classList.contains("ruleview-overridden"),
+ "width property is overridden.");
+ ok(!textPropEditor.filterProperty.hidden,
+ "Overridden search button is not hidden.");
+
+ let searchField = ruleView.searchField;
+ let onRuleViewFiltered = inspector.once("ruleview-filtered");
+
+ info("Click the overridden search");
+ textPropEditor.filterProperty.click();
+ yield onRuleViewFiltered;
+
+ info("Check that the overridden search is applied");
+ is(searchField.value, "`width`", "The search field value is width.");
+
+ rule = getRuleViewRuleEditor(ruleView, 1).rule;
+ textPropEditor = rule.textProps[0].editor;
+ is(rule.selectorText, "#testid", "Second rule is #testid.");
+ ok(textPropEditor.container.classList.contains("ruleview-highlight"),
+ "width property is correctly highlighted.");
+
+ rule = getRuleViewRuleEditor(ruleView, 2).rule;
+ textPropEditor = rule.textProps[0].editor;
+ is(rule.selectorText, "h1", "Third rule is h1.");
+ ok(textPropEditor.container.classList.contains("ruleview-highlight"),
+ "width property is correctly highlighted.");
+ ok(textPropEditor.element.classList.contains("ruleview-overridden"),
+ "width property is overridden.");
+ ok(!textPropEditor.filterProperty.hidden,
+ "Overridden search button is not hidden.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter_01.js b/devtools/client/inspector/rules/test/browser_rules_search-filter_01.js
new file mode 100644
index 000000000..4dd1c951d
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_01.js
@@ -0,0 +1,91 @@
+/* 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";
+
+// Tests that the rule view search filter and clear button works properly.
+
+const TEST_URI = `
+ <style type="text/css">
+ #testid, h1 {
+ background-color: #00F !important;
+ }
+ .testclass {
+ width: 100%;
+ }
+ </style>
+ <h1 id="testid" class="testclass">Styled Node</h1>
+`;
+
+const TEST_DATA = [
+ {
+ desc: "Tests that the search filter works properly for property names",
+ search: "color"
+ },
+ {
+ desc: "Tests that the search filter works properly for property values",
+ search: "00F"
+ },
+ {
+ desc: "Tests that the search filter works properly for property line input",
+ search: "background-color:#00F"
+ },
+ {
+ desc: "Tests that the search filter works properly for parsed property " +
+ "names",
+ search: "background:"
+ },
+ {
+ desc: "Tests that the search filter works properly for parsed property " +
+ "values",
+ search: ":00F"
+ },
+];
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+ yield testAddTextInFilter(inspector, view);
+});
+
+function* testAddTextInFilter(inspector, view) {
+ for (let data of TEST_DATA) {
+ info(data.desc);
+ yield setSearchFilter(view, data.search);
+ yield checkRules(view);
+ yield clearSearchAndCheckRules(view);
+ }
+}
+
+function* checkRules(view) {
+ info("Check that the correct rules are visible");
+ is(view.element.children.length, 2, "Should have 2 rules.");
+ is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element",
+ "First rule is inline element.");
+
+ let rule = getRuleViewRuleEditor(view, 1).rule;
+
+ is(rule.selectorText, "#testid, h1", "Second rule is #testid, h1.");
+ ok(rule.textProps[0].editor.container.classList
+ .contains("ruleview-highlight"),
+ "background-color text property is correctly highlighted.");
+}
+
+function* clearSearchAndCheckRules(view) {
+ let doc = view.styleDocument;
+ let win = view.styleWindow;
+ let searchField = view.searchField;
+ let searchClearButton = view.searchClearButton;
+
+ info("Clearing the search filter");
+ EventUtils.synthesizeMouseAtCenter(searchClearButton, {}, win);
+ yield view.inspector.once("ruleview-filtered");
+
+ info("Check the search filter is cleared and no rules are highlighted");
+ is(view.element.children.length, 3, "Should have 3 rules.");
+ ok(!searchField.value, "Search filter is cleared.");
+ ok(!doc.querySelectorAll(".ruleview-highlight").length,
+ "No rules are higlighted.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter_02.js b/devtools/client/inspector/rules/test/browser_rules_search-filter_02.js
new file mode 100644
index 000000000..c23e7be62
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_02.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";
+
+// Tests that the rule view search filter works properly for keyframe rule
+// selectors.
+
+const SEARCH = "20%";
+const TEST_URI = URL_ROOT + "doc_keyframeanimation.html";
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#boxy", inspector);
+ yield testAddTextInFilter(inspector, view);
+});
+
+function* testAddTextInFilter(inspector, view) {
+ yield setSearchFilter(view, SEARCH);
+
+ info("Check that the correct rules are visible");
+ is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element",
+ "First rule is inline element.");
+
+ let ruleEditor = getRuleViewRuleEditor(view, 2, 0);
+
+ is(ruleEditor.rule.domRule.keyText, "20%", "Second rule is 20%.");
+ ok(ruleEditor.selectorText.classList.contains("ruleview-highlight"),
+ "20% selector is highlighted.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter_03.js b/devtools/client/inspector/rules/test/browser_rules_search-filter_03.js
new file mode 100644
index 000000000..89280f0eb
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_03.js
@@ -0,0 +1,39 @@
+/* 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";
+
+// Tests that the rule view search filter works properly for inline styles.
+
+const SEARCH = "color";
+
+const TEST_URI = `
+ <style type="text/css">
+ #testid {
+ width: 100%;
+ }
+ </style>
+ <div id="testid" style="background-color:aliceblue">Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+ yield testAddTextInFilter(inspector, view);
+});
+
+function* testAddTextInFilter(inspector, view) {
+ yield setSearchFilter(view, SEARCH);
+
+ info("Check that the correct rules are visible");
+ is(view.element.children.length, 1, "Should have 1 rule.");
+
+ let rule = getRuleViewRuleEditor(view, 0).rule;
+
+ is(rule.selectorText, "element", "First rule is inline element.");
+ ok(rule.textProps[0].editor.container.classList
+ .contains("ruleview-highlight"),
+ "background-color text property is correctly highlighted.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter_04.js b/devtools/client/inspector/rules/test/browser_rules_search-filter_04.js
new file mode 100644
index 000000000..5804d74ac
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_04.js
@@ -0,0 +1,76 @@
+/* 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";
+
+// Tests that the rule view search filter works properly when modifying the
+// existing search filter value.
+
+const SEARCH = "00F";
+
+const TEST_URI = `
+ <style type="text/css">
+ #testid {
+ background-color: #00F;
+ }
+ .testclass {
+ width: 100%;
+ }
+ </style>
+ <div id="testid" class="testclass">Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+ yield testAddTextInFilter(inspector, view);
+ yield testRemoveTextInFilter(inspector, view);
+});
+
+function* testAddTextInFilter(inspector, view) {
+ yield setSearchFilter(view, SEARCH);
+
+ info("Check that the correct rules are visible");
+ is(view.element.children.length, 2, "Should have 2 rules.");
+ is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element",
+ "First rule is inline element.");
+
+ let rule = getRuleViewRuleEditor(view, 1).rule;
+
+ is(rule.selectorText, "#testid", "Second rule is #testid.");
+ ok(rule.textProps[0].editor.container.classList
+ .contains("ruleview-highlight"),
+ "background-color text property is correctly highlighted.");
+}
+
+function* testRemoveTextInFilter(inspector, view) {
+ info("Press backspace and set filter text to \"00\"");
+
+ let win = view.styleWindow;
+ let searchField = view.searchField;
+
+ searchField.focus();
+ EventUtils.synthesizeKey("VK_BACK_SPACE", {}, win);
+ yield inspector.once("ruleview-filtered");
+
+ info("Check that the correct rules are visible");
+ is(view.element.children.length, 3, "Should have 3 rules.");
+ is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element",
+ "First rule is inline element.");
+
+ let rule = getRuleViewRuleEditor(view, 1).rule;
+
+ is(rule.selectorText, "#testid", "Second rule is #testid.");
+ ok(rule.textProps[0].editor.container.classList
+ .contains("ruleview-highlight"),
+ "background-color text property is correctly highlighted.");
+
+ rule = getRuleViewRuleEditor(view, 2).rule;
+
+ is(rule.selectorText, ".testclass", "Second rule is .testclass.");
+ ok(rule.textProps[0].editor.container.classList
+ .contains("ruleview-highlight"),
+ "width text property is correctly highlighted.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter_05.js b/devtools/client/inspector/rules/test/browser_rules_search-filter_05.js
new file mode 100644
index 000000000..9388dd47e
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_05.js
@@ -0,0 +1,33 @@
+/* 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";
+
+// Tests that the rule view search filter works properly for stylesheet source.
+
+const SEARCH = "doc_urls_clickable.css";
+const TEST_URI = URL_ROOT + "doc_urls_clickable.html";
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let {inspector, view} = yield openRuleView();
+ yield selectNode(".relative1", inspector);
+ yield testAddTextInFilter(inspector, view);
+});
+
+function* testAddTextInFilter(inspector, view) {
+ yield setSearchFilter(view, SEARCH);
+
+ info("Check that the correct rules are visible");
+ is(view.element.children.length, 2, "Should have 2 rules.");
+ is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element",
+ "First rule is inline element.");
+
+ let rule = getRuleViewRuleEditor(view, 1).rule;
+ let source = rule.textProps[0].editor.ruleEditor.source;
+
+ is(rule.selectorText, ".relative1", "Second rule is .relative1.");
+ ok(source.classList.contains("ruleview-highlight"),
+ "stylesheet source is correctly highlighted.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter_06.js b/devtools/client/inspector/rules/test/browser_rules_search-filter_06.js
new file mode 100644
index 000000000..67b02ab73
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_06.js
@@ -0,0 +1,27 @@
+/* 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";
+
+// Tests that the rule view search filter does not highlight the source with
+// input that could be parsed as a property line.
+
+const SEARCH = "doc_urls_clickable.css: url";
+const TEST_URI = URL_ROOT + "doc_urls_clickable.html";
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let {inspector, view} = yield openRuleView();
+ yield selectNode(".relative1", inspector);
+ yield testAddTextInFilter(inspector, view);
+});
+
+function* testAddTextInFilter(inspector, view) {
+ yield setSearchFilter(view, SEARCH);
+
+ info("Check that the correct rules are visible");
+ is(view.element.children.length, 1, "Should have 1 rules.");
+ is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element",
+ "First rule is inline element.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter_07.js b/devtools/client/inspector/rules/test/browser_rules_search-filter_07.js
new file mode 100644
index 000000000..16b047d8d
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_07.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";
+
+// Tests that the rule view search filter works properly for newly modified
+// property name.
+
+const SEARCH = "e";
+
+const TEST_URI = `
+ <style type='text/css'>
+ #testid {
+ width: 100%;
+ height: 50%;
+ }
+ </style>
+ <h1 id='testid'>Styled Node</h1>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ info("Enter the test value in the search filter");
+ yield setSearchFilter(view, SEARCH);
+
+ info("Focus the width property name");
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+ let rule = ruleEditor.rule;
+ let propEditor = rule.textProps[0].editor;
+ yield focusEditableField(view, propEditor.nameSpan);
+
+ info("Check that the correct rules are visible");
+ is(view.element.children.length, 2, "Should have 2 rules.");
+ is(rule.selectorText, "#testid", "Second rule is #testid.");
+ ok(!propEditor.container.classList.contains("ruleview-highlight"),
+ "width text property is not highlighted.");
+ ok(rule.textProps[1].editor.container.classList
+ .contains("ruleview-highlight"),
+ "height text property is correctly highlighted.");
+
+ info("Change the width property to margin-left");
+ EventUtils.sendString("margin-left", view.styleWindow);
+
+ info("Submit the change");
+ let onRuleViewChanged = view.once("ruleview-changed");
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ yield onRuleViewChanged;
+
+ ok(propEditor.container.classList.contains("ruleview-highlight"),
+ "margin-left text property is correctly highlighted.");
+
+ // After pressing return on the property name, the value has been focused
+ // automatically. Blur it now and wait for the rule-view to refresh to avoid
+ // pending requests.
+ onRuleViewChanged = view.once("ruleview-changed");
+ EventUtils.synthesizeKey("VK_ESCAPE", {});
+ yield onRuleViewChanged;
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter_08.js b/devtools/client/inspector/rules/test/browser_rules_search-filter_08.js
new file mode 100644
index 000000000..1a3c0de59
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_08.js
@@ -0,0 +1,53 @@
+/* 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";
+
+// Tests that the rule view search filter works properly for newly modified
+// property value.
+
+const SEARCH = "100%";
+
+const TEST_URI = `
+ <style type='text/css'>
+ #testid {
+ width: 100%;
+ height: 50%;
+ }
+ </style>
+ <h1 id='testid'>Styled Node</h1>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ info("Enter the test value in the search filter");
+ yield setSearchFilter(view, SEARCH);
+
+ info("Focus the height property value");
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+ let rule = ruleEditor.rule;
+ let propEditor = rule.textProps[1].editor;
+ yield focusEditableField(view, propEditor.valueSpan);
+
+ info("Check that the correct rules are visible");
+ is(view.element.children.length, 2, "Should have 2 rules.");
+ is(rule.selectorText, "#testid", "Second rule is #testid.");
+ ok(rule.textProps[0].editor.container.classList
+ .contains("ruleview-highlight"),
+ "width text property is correctly highlighted.");
+ ok(!propEditor.container.classList.contains("ruleview-highlight"),
+ "height text property is not highlighted.");
+
+ info("Change the height property value to 100%");
+ let onRuleViewChanged = view.once("ruleview-changed");
+ EventUtils.sendString("100%", view.styleWindow);
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ yield onRuleViewChanged;
+
+ ok(propEditor.container.classList.contains("ruleview-highlight"),
+ "height text property is correctly highlighted.");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter_09.js b/devtools/client/inspector/rules/test/browser_rules_search-filter_09.js
new file mode 100644
index 000000000..620e5d336
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_09.js
@@ -0,0 +1,73 @@
+/* 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";
+
+// Tests that the rule view search filter works properly for newly added
+// property.
+
+const SEARCH = "100%";
+
+const TEST_URI = `
+ <style type='text/css'>
+ #testid {
+ width: 100%;
+ height: 50%;
+ }
+ </style>
+ <h1 id='testid'>Styled Node</h1>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ info("Enter the test value in the search filter");
+ yield setSearchFilter(view, SEARCH);
+
+ info("Start entering a new property in the rule");
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+ let rule = ruleEditor.rule;
+ let editor = yield focusNewRuleViewProperty(ruleEditor);
+
+ info("Check that the correct rules are visible");
+ is(view.element.children.length, 2, "Should have 2 rules.");
+ is(rule.selectorText, "#testid", "Second rule is #testid.");
+ ok(rule.textProps[0].editor.container.classList
+ .contains("ruleview-highlight"),
+ "width text property is correctly highlighted.");
+ ok(!rule.textProps[1].editor.container.classList
+ .contains("ruleview-highlight"),
+ "height text property is not highlighted.");
+
+ info("Test creating a new property");
+
+ info("Entering margin-left in the property name editor");
+ // Changing the value doesn't cause a rule-view refresh, no need to wait for
+ // ruleview-changed here.
+ editor.input.value = "margin-left";
+
+ info("Pressing return to commit and focus the new value field");
+ let onRuleViewChanged = view.once("ruleview-changed");
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
+ yield onRuleViewChanged;
+
+ // Getting the new value editor after focus
+ editor = inplaceEditor(view.styleDocument.activeElement);
+ let propEditor = ruleEditor.rule.textProps[2].editor;
+
+ info("Entering a value and bluring the field to expect a rule change");
+ onRuleViewChanged = view.once("ruleview-changed");
+ editor.input.value = "100%";
+ view.throttle.flush();
+ yield onRuleViewChanged;
+
+ onRuleViewChanged = view.once("ruleview-changed");
+ editor.input.blur();
+ yield onRuleViewChanged;
+
+ ok(propEditor.container.classList.contains("ruleview-highlight"),
+ "margin-left text property is correctly highlighted.");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter_10.js b/devtools/client/inspector/rules/test/browser_rules_search-filter_10.js
new file mode 100644
index 000000000..ac336591d
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_10.js
@@ -0,0 +1,84 @@
+/* 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";
+
+// Tests that the rule view search filter works properly for rule selectors.
+
+const TEST_URI = `
+ <style type="text/css">
+ html, body, div {
+ background-color: #00F;
+ }
+ #testid {
+ width: 100%;
+ }
+ </style>
+ <div id="testid" class="testclass">Styled Node</div>
+`;
+
+const TEST_DATA = [
+ {
+ desc: "Tests that the search filter works properly for a single rule " +
+ "selector",
+ search: "#test",
+ selectorText: "#testid",
+ index: 0
+ },
+ {
+ desc: "Tests that the search filter works properly for multiple rule " +
+ "selectors",
+ search: "body",
+ selectorText: "html, body, div",
+ index: 2
+ }
+];
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+ yield testAddTextInFilter(inspector, view);
+});
+
+function* testAddTextInFilter(inspector, view) {
+ for (let data of TEST_DATA) {
+ info(data.desc);
+ yield setSearchFilter(view, data.search);
+ yield checkRules(view, data);
+ yield clearSearchAndCheckRules(view);
+ }
+}
+
+function* checkRules(view, data) {
+ info("Check that the correct rules are visible");
+ is(view.element.children.length, 2, "Should have 2 rules.");
+ is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element",
+ "First rule is inline element.");
+
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+
+ is(ruleEditor.rule.selectorText, data.selectorText,
+ "Second rule is " + data.selectorText + ".");
+ ok(ruleEditor.selectorText.children[data.index].classList
+ .contains("ruleview-highlight"),
+ data.selectorText + " selector is highlighted.");
+}
+
+function* clearSearchAndCheckRules(view) {
+ let doc = view.styleDocument;
+ let win = view.styleWindow;
+ let searchField = view.searchField;
+ let searchClearButton = view.searchClearButton;
+
+ info("Clearing the search filter");
+ EventUtils.synthesizeMouseAtCenter(searchClearButton, {}, win);
+ yield view.inspector.once("ruleview-filtered");
+
+ info("Check the search filter is cleared and no rules are highlighted");
+ is(view.element.children.length, 3, "Should have 3 rules.");
+ ok(!searchField.value, "Search filter is cleared.");
+ ok(!doc.querySelectorAll(".ruleview-highlight").length,
+ "No rules are higlighted.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter_context-menu.js b/devtools/client/inspector/rules/test/browser_rules_search-filter_context-menu.js
new file mode 100644
index 000000000..349f1b9b3
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_context-menu.js
@@ -0,0 +1,83 @@
+/* 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 rule view search filter context menu works properly.
+
+const TEST_INPUT = "h1";
+const TEST_URI = "<h1>test filter context menu</h1>";
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {toolbox, inspector, view} = yield openRuleView();
+ yield selectNode("h1", inspector);
+
+ let win = view.styleWindow;
+ let searchField = view.searchField;
+ let searchContextMenu = toolbox.textBoxContextMenuPopup;
+ ok(searchContextMenu,
+ "The search filter context menu is loaded in the rule view");
+
+ let cmdUndo = searchContextMenu.querySelector("[command=cmd_undo]");
+ let cmdDelete = searchContextMenu.querySelector("[command=cmd_delete]");
+ let cmdSelectAll = searchContextMenu.querySelector("[command=cmd_selectAll]");
+ let cmdCut = searchContextMenu.querySelector("[command=cmd_cut]");
+ let cmdCopy = searchContextMenu.querySelector("[command=cmd_copy]");
+ let cmdPaste = searchContextMenu.querySelector("[command=cmd_paste]");
+
+ info("Opening context menu");
+
+ emptyClipboard();
+
+ let onFocus = once(searchField, "focus");
+ searchField.focus();
+ yield onFocus;
+
+ let onContextMenuPopup = once(searchContextMenu, "popupshowing");
+ EventUtils.synthesizeMouse(searchField, 2, 2,
+ {type: "contextmenu", button: 2}, win);
+ yield onContextMenuPopup;
+
+ is(cmdUndo.getAttribute("disabled"), "true", "cmdUndo is disabled");
+ is(cmdDelete.getAttribute("disabled"), "true", "cmdDelete is disabled");
+ is(cmdSelectAll.getAttribute("disabled"), "true", "cmdSelectAll is disabled");
+
+ // Cut/Copy items are enabled in context menu even if there
+ // is no selection. See also Bug 1303033
+ is(cmdCut.getAttribute("disabled"), "", "cmdCut is enabled");
+ is(cmdCopy.getAttribute("disabled"), "", "cmdCopy is enabled");
+
+ if (isWindows()) {
+ // emptyClipboard only works on Windows (666254), assert paste only for this OS.
+ is(cmdPaste.getAttribute("disabled"), "true", "cmdPaste is disabled");
+ }
+
+ info("Closing context menu");
+ let onContextMenuHidden = once(searchContextMenu, "popuphidden");
+ searchContextMenu.hidePopup();
+ yield onContextMenuHidden;
+
+ info("Copy text in search field using the context menu");
+ searchField.value = TEST_INPUT;
+ searchField.select();
+ EventUtils.synthesizeMouse(searchField, 2, 2,
+ {type: "contextmenu", button: 2}, win);
+ yield onContextMenuPopup;
+ yield waitForClipboardPromise(() => cmdCopy.click(), TEST_INPUT);
+ searchContextMenu.hidePopup();
+ yield onContextMenuHidden;
+
+ info("Reopen context menu and check command properties");
+ EventUtils.synthesizeMouse(searchField, 2, 2,
+ {type: "contextmenu", button: 2}, win);
+ yield onContextMenuPopup;
+
+ is(cmdUndo.getAttribute("disabled"), "", "cmdUndo is enabled");
+ is(cmdDelete.getAttribute("disabled"), "", "cmdDelete is enabled");
+ is(cmdSelectAll.getAttribute("disabled"), "", "cmdSelectAll is enabled");
+ is(cmdCut.getAttribute("disabled"), "", "cmdCut is enabled");
+ is(cmdCopy.getAttribute("disabled"), "", "cmdCopy is enabled");
+ is(cmdPaste.getAttribute("disabled"), "", "cmdPaste is enabled");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter_escape-keypress.js b/devtools/client/inspector/rules/test/browser_rules_search-filter_escape-keypress.js
new file mode 100644
index 000000000..21848dce8
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_escape-keypress.js
@@ -0,0 +1,65 @@
+/* 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";
+
+// Tests that the rule view search filter escape keypress will clear the search
+// field.
+
+const SEARCH = "00F";
+
+const TEST_URI = `
+ <style type="text/css">
+ #testid {
+ background-color: #00F;
+ }
+ .testclass {
+ width: 100%;
+ }
+ </style>
+ <div id="testid" class="testclass">Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+ yield testAddTextInFilter(inspector, view);
+ yield testEscapeKeypress(inspector, view);
+});
+
+function* testAddTextInFilter(inspector, view) {
+ yield setSearchFilter(view, SEARCH);
+
+ info("Check that the correct rules are visible");
+ is(view.element.children.length, 2, "Should have 2 rules.");
+ is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element",
+ "First rule is inline element.");
+
+ let rule = getRuleViewRuleEditor(view, 1).rule;
+
+ is(rule.selectorText, "#testid", "Second rule is #testid.");
+ ok(rule.textProps[0].editor.container.classList
+ .contains("ruleview-highlight"),
+ "background-color text property is correctly highlighted.");
+}
+
+function* testEscapeKeypress(inspector, view) {
+ info("Pressing the escape key on search filter");
+
+ let doc = view.styleDocument;
+ let win = view.styleWindow;
+ let searchField = view.searchField;
+ let onRuleViewFiltered = inspector.once("ruleview-filtered");
+
+ searchField.focus();
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, win);
+ yield onRuleViewFiltered;
+
+ info("Check the search filter is cleared and no rules are highlighted");
+ is(view.element.children.length, 3, "Should have 3 rules.");
+ ok(!searchField.value, "Search filter is cleared");
+ ok(!doc.querySelectorAll(".ruleview-highlight").length,
+ "No rules are higlighted");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_select-and-copy-styles.js b/devtools/client/inspector/rules/test/browser_rules_select-and-copy-styles.js
new file mode 100644
index 000000000..b3f4ef364
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_select-and-copy-styles.js
@@ -0,0 +1,171 @@
+/* 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";
+
+// Tests that properties can be selected and copied from the rule view
+
+const osString = Services.appinfo.OS;
+
+const TEST_URI = `
+ <style type="text/css">
+ html {
+ color: #000000;
+ }
+ span {
+ font-variant: small-caps; color: #000000;
+ }
+ .nomatches {
+ color: #ff0000;
+ }
+ </style>
+ <div id="first" style="margin: 10em;
+ font-size: 14pt; font-family: helvetica, sans-serif; color: #AAA">
+ <h1>Some header text</h1>
+ <p id="salutation" style="font-size: 12pt">hi.</p>
+ <p id="body" style="font-size: 12pt">I am a test-case. This text exists
+ solely to provide some things to <span style="color: yellow">
+ highlight</span> and <span style="font-weight: bold">count</span>
+ style list-items in the box at right. If you are reading this,
+ you should go do something else instead. Maybe read a book. Or better
+ yet, write some test-cases for another bit of code.
+ <span style="font-style: italic">some text</span></p>
+ <p id="closing">more text</p>
+ <p>even more text</p>
+ </div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("div", inspector);
+ yield checkCopySelection(view);
+ yield checkSelectAll(view);
+ yield checkCopyEditorValue(view);
+});
+
+function* checkCopySelection(view) {
+ info("Testing selection copy");
+
+ let contentDoc = view.styleDocument;
+ let win = view.styleWindow;
+ let prop = contentDoc.querySelector(".ruleview-property");
+ let values = contentDoc.querySelectorAll(".ruleview-propertyvaluecontainer");
+
+ let range = contentDoc.createRange();
+ range.setStart(prop, 0);
+ range.setEnd(values[4], 2);
+ win.getSelection().addRange(range);
+ info("Checking that _Copy() returns the correct clipboard value");
+
+ let expectedPattern = " margin: 10em;[\\r\\n]+" +
+ " font-size: 14pt;[\\r\\n]+" +
+ " font-family: helvetica, sans-serif;[\\r\\n]+" +
+ " color: #AAA;[\\r\\n]+" +
+ "}[\\r\\n]+" +
+ "html {[\\r\\n]+" +
+ " color: #000000;[\\r\\n]*";
+
+ let allMenuItems = openStyleContextMenuAndGetAllItems(view, prop);
+ let menuitemCopy = allMenuItems.find(item => item.label ===
+ STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copy"));
+
+ ok(menuitemCopy.visible,
+ "Copy menu item is displayed as expected");
+
+ try {
+ yield waitForClipboardPromise(() => menuitemCopy.click(),
+ () => checkClipboardData(expectedPattern));
+ } catch (e) {
+ failedClipboard(expectedPattern);
+ }
+}
+
+function* checkSelectAll(view) {
+ info("Testing select-all copy");
+
+ let contentDoc = view.styleDocument;
+ let prop = contentDoc.querySelector(".ruleview-property");
+
+ info("Checking that _SelectAll() then copy returns the correct " +
+ "clipboard value");
+ view._contextmenu._onSelectAll();
+ let expectedPattern = "element {[\\r\\n]+" +
+ " margin: 10em;[\\r\\n]+" +
+ " font-size: 14pt;[\\r\\n]+" +
+ " font-family: helvetica, sans-serif;[\\r\\n]+" +
+ " color: #AAA;[\\r\\n]+" +
+ "}[\\r\\n]+" +
+ "html {[\\r\\n]+" +
+ " color: #000000;[\\r\\n]+" +
+ "}[\\r\\n]*";
+
+ let allMenuItems = openStyleContextMenuAndGetAllItems(view, prop);
+ let menuitemCopy = allMenuItems.find(item => item.label ===
+ STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copy"));
+
+ ok(menuitemCopy.visible,
+ "Copy menu item is displayed as expected");
+
+ try {
+ yield waitForClipboardPromise(() => menuitemCopy.click(),
+ () => checkClipboardData(expectedPattern));
+ } catch (e) {
+ failedClipboard(expectedPattern);
+ }
+}
+
+function* checkCopyEditorValue(view) {
+ info("Testing CSS property editor value copy");
+
+ let ruleEditor = getRuleViewRuleEditor(view, 0);
+ let propEditor = ruleEditor.rule.textProps[0].editor;
+
+ let editor = yield focusEditableField(view, propEditor.valueSpan);
+
+ info("Checking that copying a css property value editor returns the correct" +
+ " clipboard value");
+
+ let expectedPattern = "10em";
+
+ let allMenuItems = openStyleContextMenuAndGetAllItems(view, editor.input);
+ let menuitemCopy = allMenuItems.find(item => item.label ===
+ STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copy"));
+
+ ok(menuitemCopy.visible,
+ "Copy menu item is displayed as expected");
+
+ try {
+ yield waitForClipboardPromise(() => menuitemCopy.click(),
+ () => checkClipboardData(expectedPattern));
+ } catch (e) {
+ failedClipboard(expectedPattern);
+ }
+}
+
+function checkClipboardData(expectedPattern) {
+ let actual = SpecialPowers.getClipboardData("text/unicode");
+ let expectedRegExp = new RegExp(expectedPattern, "g");
+ return expectedRegExp.test(actual);
+}
+
+function failedClipboard(expectedPattern) {
+ // Format expected text for comparison
+ let terminator = osString == "WINNT" ? "\r\n" : "\n";
+ expectedPattern = expectedPattern.replace(/\[\\r\\n\][+*]/g, terminator);
+ expectedPattern = expectedPattern.replace(/\\\(/g, "(");
+ expectedPattern = expectedPattern.replace(/\\\)/g, ")");
+
+ let actual = SpecialPowers.getClipboardData("text/unicode");
+
+ // Trim the right hand side of our strings. This is because expectedPattern
+ // accounts for windows sometimes adding a newline to our copied data.
+ expectedPattern = expectedPattern.trimRight();
+ actual = actual.trimRight();
+
+ dump("TEST-UNEXPECTED-FAIL | Clipboard text does not match expected ... " +
+ "results (escaped for accurate comparison):\n");
+ info("Actual: " + escape(actual));
+ info("Expected: " + escape(expectedPattern));
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_selector-highlighter-on-navigate.js b/devtools/client/inspector/rules/test/browser_rules_selector-highlighter-on-navigate.js
new file mode 100644
index 000000000..54e25c399
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_selector-highlighter-on-navigate.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 the selector highlighter is hidden on page navigation.
+
+const TEST_URI = `
+ <style type="text/css">
+ body, p, td {
+ background: red;
+ }
+ </style>
+ Test the selector highlighter
+`;
+
+const TEST_URI_2 = "data:text/html,<html><body>test</body></html>";
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ let highlighters = view.highlighters;
+
+ info("Clicking on a selector icon");
+ let icon = getRuleViewSelectorHighlighterIcon(view, "body, p, td");
+
+ let onToggled = view.once("ruleview-selectorhighlighter-toggled");
+ EventUtils.synthesizeMouseAtCenter(icon, {}, view.styleWindow);
+ let isVisible = yield onToggled;
+
+ ok(highlighters.selectorHighlighterShown, "The selectorHighlighterShown is set.");
+ ok(view.selectorHighlighter, "The selectorhighlighter instance was created");
+ ok(isVisible, "The toggle event says the highlighter is visible");
+
+ yield navigateTo(inspector, TEST_URI_2);
+ ok(!highlighters.selectorHighlighterShown, "The selectorHighlighterShown is unset.");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_selector-highlighter_01.js b/devtools/client/inspector/rules/test/browser_rules_selector-highlighter_01.js
new file mode 100644
index 000000000..4c8853e02
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_selector-highlighter_01.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";
+
+// Test that the selector highlighter is created when clicking on a selector
+// icon in the rule view.
+
+const TEST_URI = `
+ <style type="text/css">
+ body, p, td {
+ background: red;
+ }
+ </style>
+ Test the selector highlighter
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {view} = yield openRuleView();
+
+ ok(!view.selectorHighlighter,
+ "No selectorhighlighter exist in the rule-view");
+
+ info("Clicking on a selector icon");
+ let icon = getRuleViewSelectorHighlighterIcon(view, "body, p, td");
+
+ let onToggled = view.once("ruleview-selectorhighlighter-toggled");
+ EventUtils.synthesizeMouseAtCenter(icon, {}, view.styleWindow);
+ let isVisible = yield onToggled;
+
+ ok(view.selectorHighlighter, "The selectorhighlighter instance was created");
+ ok(isVisible, "The toggle event says the highlighter is visible");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_selector-highlighter_02.js b/devtools/client/inspector/rules/test/browser_rules_selector-highlighter_02.js
new file mode 100644
index 000000000..33f73e587
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_selector-highlighter_02.js
@@ -0,0 +1,78 @@
+/* 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 selector highlighter is shown when clicking on a selector icon
+// in the rule-view
+
+// Note that in this test, we mock the highlighter front, merely testing the
+// behavior of the style-inspector UI for now
+
+const TEST_URI = `
+ <style type="text/css">
+ body {
+ background: red;
+ }
+ p {
+ color: white;
+ }
+ </style>
+ <p>Testing the selector highlighter</p>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+
+ // Mock the highlighter front to get the reference of the NodeFront
+ let HighlighterFront = {
+ isShown: false,
+ nodeFront: null,
+ options: null,
+ show: function (nodeFront, options) {
+ this.nodeFront = nodeFront;
+ this.options = options;
+ this.isShown = true;
+ },
+ hide: function () {
+ this.nodeFront = null;
+ this.options = null;
+ this.isShown = false;
+ }
+ };
+
+ // Inject the mock highlighter in the rule-view
+ view.selectorHighlighter = HighlighterFront;
+
+ let icon = getRuleViewSelectorHighlighterIcon(view, "body");
+
+ info("Checking that the HighlighterFront's show/hide methods are called");
+
+ info("Clicking once on the body selector highlighter icon");
+ yield clickSelectorIcon(icon, view);
+ ok(HighlighterFront.isShown, "The highlighter is shown");
+
+ info("Clicking once again on the body selector highlighter icon");
+ yield clickSelectorIcon(icon, view);
+ ok(!HighlighterFront.isShown, "The highlighter is hidden");
+
+ info("Checking that the right NodeFront reference and options are passed");
+ yield selectNode("p", inspector);
+ icon = getRuleViewSelectorHighlighterIcon(view, "p");
+
+ yield clickSelectorIcon(icon, view);
+ is(HighlighterFront.nodeFront.tagName, "P",
+ "The right NodeFront is passed to the highlighter (1)");
+ is(HighlighterFront.options.selector, "p",
+ "The right selector option is passed to the highlighter (1)");
+
+ yield selectNode("body", inspector);
+ icon = getRuleViewSelectorHighlighterIcon(view, "body");
+ yield clickSelectorIcon(icon, view);
+ is(HighlighterFront.nodeFront.tagName, "BODY",
+ "The right NodeFront is passed to the highlighter (2)");
+ is(HighlighterFront.options.selector, "body",
+ "The right selector option is passed to the highlighter (2)");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_selector-highlighter_03.js b/devtools/client/inspector/rules/test/browser_rules_selector-highlighter_03.js
new file mode 100644
index 000000000..1ffbac012
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_selector-highlighter_03.js
@@ -0,0 +1,78 @@
+/* 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 selector highlighter toggling mechanism works correctly.
+
+// Note that in this test, we mock the highlighter front, merely testing the
+// behavior of the style-inspector UI for now
+
+const TEST_URI = `
+ <style type="text/css">
+ div {text-decoration: underline;}
+ .node-1 {color: red;}
+ .node-2 {color: green;}
+ </style>
+ <div class="node-1">Node 1</div>
+ <div class="node-2">Node 2</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+
+ // Mock the highlighter front.
+ let HighlighterFront = {
+ isShown: false,
+ show: function () {
+ this.isShown = true;
+ },
+ hide: function () {
+ this.isShown = false;
+ }
+ };
+
+ // Inject the mock highlighter in the rule-view
+ view.selectorHighlighter = HighlighterFront;
+
+ info("Select .node-1 and click on the .node-1 selector icon");
+ yield selectNode(".node-1", inspector);
+ let icon = getRuleViewSelectorHighlighterIcon(view, ".node-1");
+ yield clickSelectorIcon(icon, view);
+ ok(HighlighterFront.isShown, "The highlighter is shown");
+
+ info("With .node-1 still selected, click again on the .node-1 selector icon");
+ yield clickSelectorIcon(icon, view);
+ ok(!HighlighterFront.isShown, "The highlighter is now hidden");
+
+ info("With .node-1 still selected, click on the div selector icon");
+ icon = getRuleViewSelectorHighlighterIcon(view, "div");
+ yield clickSelectorIcon(icon, view);
+ ok(HighlighterFront.isShown, "The highlighter is shown again");
+
+ info("With .node-1 still selected, click again on the .node-1 selector icon");
+ icon = getRuleViewSelectorHighlighterIcon(view, ".node-1");
+ yield clickSelectorIcon(icon, view);
+ ok(HighlighterFront.isShown,
+ "The highlighter is shown again since the clicked selector was different");
+
+ info("Selecting .node-2");
+ yield selectNode(".node-2", inspector);
+ ok(HighlighterFront.isShown,
+ "The highlighter is still shown after selection");
+
+ info("With .node-2 selected, click on the div selector icon");
+ icon = getRuleViewSelectorHighlighterIcon(view, "div");
+ yield clickSelectorIcon(icon, view);
+ ok(HighlighterFront.isShown,
+ "The highlighter is shown still since the selected was different");
+
+ info("Switching back to .node-1 and clicking on the div selector");
+ yield selectNode(".node-1", inspector);
+ icon = getRuleViewSelectorHighlighterIcon(view, "div");
+ yield clickSelectorIcon(icon, view);
+ ok(!HighlighterFront.isShown,
+ "The highlighter is hidden now that the same selector was clicked");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_selector-highlighter_04.js b/devtools/client/inspector/rules/test/browser_rules_selector-highlighter_04.js
new file mode 100644
index 000000000..b770f8127
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_selector-highlighter_04.js
@@ -0,0 +1,53 @@
+/* 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 selector highlighter is shown when clicking on a selector icon
+// for the 'element {}' rule
+
+// Note that in this test, we mock the highlighter front, merely testing the
+// behavior of the style-inspector UI for now
+
+const TEST_URI = `
+<p>Testing the selector highlighter for the 'element {}' rule</p>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+
+ // Mock the highlighter front to get the reference of the NodeFront
+ let HighlighterFront = {
+ isShown: false,
+ nodeFront: null,
+ options: null,
+ show: function (nodeFront, options) {
+ this.nodeFront = nodeFront;
+ this.options = options;
+ this.isShown = true;
+ },
+ hide: function () {
+ this.nodeFront = null;
+ this.options = null;
+ this.isShown = false;
+ }
+ };
+ // Inject the mock highlighter in the rule-view
+ view.selectorHighlighter = HighlighterFront;
+
+ info("Checking that the right NodeFront reference and options are passed");
+ yield selectNode("p", inspector);
+ let icon = getRuleViewSelectorHighlighterIcon(view, "element");
+
+ yield clickSelectorIcon(icon, view);
+ is(HighlighterFront.nodeFront.tagName, "P",
+ "The right NodeFront is passed to the highlighter (1)");
+ is(HighlighterFront.options.selector, "body > p:nth-child(1)",
+ "The right selector option is passed to the highlighter (1)");
+ ok(HighlighterFront.isShown, "The toggle event says the highlighter is visible");
+
+ yield clickSelectorIcon(icon, view);
+ ok(!HighlighterFront.isShown, "The toggle event says the highlighter is not visible");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_selector_highlight.js b/devtools/client/inspector/rules/test/browser_rules_selector_highlight.js
new file mode 100644
index 000000000..91422d57a
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_selector_highlight.js
@@ -0,0 +1,144 @@
+/* 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";
+
+// Tests that the rule view selector text is highlighted correctly according
+// to the components of the selector.
+
+const TEST_URI = [
+ "<style type='text/css'>",
+ " h1 {}",
+ " h1#testid {}",
+ " h1 + p {}",
+ " div[hidden=\"true\"] {}",
+ " div[title=\"test\"][checked=true] {}",
+ " p:empty {}",
+ " p:lang(en) {}",
+ " .testclass:active {}",
+ " .testclass:focus {}",
+ " .testclass:hover {}",
+ "</style>",
+ "<h1>Styled Node</h1>",
+ "<p>Paragraph</p>",
+ "<h1 id=\"testid\">Styled Node</h1>",
+ "<div hidden=\"true\"></div>",
+ "<div title=\"test\" checked=\"true\"></div>",
+ "<p></p>",
+ "<p lang=\"en\">Paragraph<p>",
+ "<div class=\"testclass\">Styled Node</div>"
+].join("\n");
+
+const SELECTOR_ATTRIBUTE = "ruleview-selector-attribute";
+const SELECTOR_ELEMENT = "ruleview-selector";
+const SELECTOR_PSEUDO_CLASS = "ruleview-selector-pseudo-class";
+const SELECTOR_PSEUDO_CLASS_LOCK = "ruleview-selector-pseudo-class-lock";
+
+const TEST_DATA = [
+ {
+ node: "h1",
+ expected: [
+ { value: "h1", class: SELECTOR_ELEMENT }
+ ]
+ },
+ {
+ node: "h1 + p",
+ expected: [
+ { value: "h1 + p", class: SELECTOR_ELEMENT }
+ ]
+ },
+ {
+ node: "h1#testid",
+ expected: [
+ { value: "h1#testid", class: SELECTOR_ELEMENT }
+ ]
+ },
+ {
+ node: "div[hidden='true']",
+ expected: [
+ { value: "div", class: SELECTOR_ELEMENT },
+ { value: "[hidden=\"true\"]", class: SELECTOR_ATTRIBUTE }
+ ]
+ },
+ {
+ node: "div[title=\"test\"][checked=\"true\"]",
+ expected: [
+ { value: "div", class: SELECTOR_ELEMENT },
+ { value: "[title=\"test\"]", class: SELECTOR_ATTRIBUTE },
+ { value: "[checked=\"true\"]", class: SELECTOR_ATTRIBUTE }
+ ]
+ },
+ {
+ node: "p:empty",
+ expected: [
+ { value: "p", class: SELECTOR_ELEMENT },
+ { value: ":empty", class: SELECTOR_PSEUDO_CLASS }
+ ]
+ },
+ {
+ node: "p:lang(en)",
+ expected: [
+ { value: "p", class: SELECTOR_ELEMENT },
+ { value: ":lang(en)", class: SELECTOR_PSEUDO_CLASS }
+ ]
+ },
+ {
+ node: ".testclass",
+ pseudoClass: ":active",
+ expected: [
+ { value: ".testclass", class: SELECTOR_ELEMENT },
+ { value: ":active", class: SELECTOR_PSEUDO_CLASS_LOCK }
+ ]
+ },
+ {
+ node: ".testclass",
+ pseudoClass: ":focus",
+ expected: [
+ { value: ".testclass", class: SELECTOR_ELEMENT },
+ { value: ":focus", class: SELECTOR_PSEUDO_CLASS_LOCK }
+ ]
+ },
+ {
+ node: ".testclass",
+ pseudoClass: ":hover",
+ expected: [
+ { value: ".testclass", class: SELECTOR_ELEMENT },
+ { value: ":hover", class: SELECTOR_PSEUDO_CLASS_LOCK }
+ ]
+ },
+];
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+
+ for (let {node, pseudoClass, expected} of TEST_DATA) {
+ yield selectNode(node, inspector);
+
+ if (pseudoClass) {
+ let onRefresh = inspector.once("rule-view-refreshed");
+ inspector.togglePseudoClass(pseudoClass);
+ yield onRefresh;
+ }
+
+ let selectorContainer =
+ getRuleViewRuleEditor(view, 1).selectorText.firstChild;
+
+ if (selectorContainer.children.length === expected.length) {
+ for (let i = 0; i < expected.length; i++) {
+ is(expected[i].value, selectorContainer.children[i].textContent,
+ "Got expected selector value: " + expected[i].value + " == " +
+ selectorContainer.children[i].textContent);
+ is(expected[i].class, selectorContainer.children[i].className,
+ "Got expected class name: " + expected[i].class + " == " +
+ selectorContainer.children[i].className);
+ }
+ } else {
+ for (let selector of selectorContainer.children) {
+ info("Actual selector components: { value: " + selector.textContent +
+ ", class: " + selector.className + " }\n");
+ }
+ }
+ }
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_strict-search-filter-computed-list_01.js b/devtools/client/inspector/rules/test/browser_rules_strict-search-filter-computed-list_01.js
new file mode 100644
index 000000000..dea9fff32
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_strict-search-filter-computed-list_01.js
@@ -0,0 +1,182 @@
+/* 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";
+
+// Tests that the rule view strict search filter and clear button works properly
+// in the computed list
+
+const TEST_URI = `
+ <style type="text/css">
+ #testid {
+ margin: 4px 0px 10px 44px;
+ }
+ .testclass {
+ background-color: red;
+ }
+ </style>
+ <h1 id="testid" class="testclass">Styled Node</h1>
+`;
+
+const TEST_DATA = [
+ {
+ desc: "Tests that the strict search filter works properly in the " +
+ "computed list for property names",
+ search: "`margin-left`",
+ isExpanderOpen: true,
+ isFilterOpen: true,
+ isMarginHighlighted: false,
+ isMarginTopHighlighted: false,
+ isMarginRightHighlighted: false,
+ isMarginBottomHighlighted: false,
+ isMarginLeftHighlighted: true
+ },
+ {
+ desc: "Tests that the strict search filter works properly in the " +
+ "computed list for property values",
+ search: "`0px`",
+ isExpanderOpen: true,
+ isFilterOpen: true,
+ isMarginHighlighted: false,
+ isMarginTopHighlighted: false,
+ isMarginRightHighlighted: true,
+ isMarginBottomHighlighted: false,
+ isMarginLeftHighlighted: false
+ },
+ {
+ desc: "Tests that the strict search filter works properly in the " +
+ "computed list for parsed property names",
+ search: "`margin-left`:",
+ isExpanderOpen: true,
+ isFilterOpen: true,
+ isMarginHighlighted: false,
+ isMarginTopHighlighted: false,
+ isMarginRightHighlighted: false,
+ isMarginBottomHighlighted: false,
+ isMarginLeftHighlighted: true
+ },
+ {
+ desc: "Tests that the strict search filter works properly in the " +
+ "computed list for parsed property values",
+ search: ":`4px`",
+ isExpanderOpen: true,
+ isFilterOpen: true,
+ isMarginHighlighted: false,
+ isMarginTopHighlighted: true,
+ isMarginRightHighlighted: false,
+ isMarginBottomHighlighted: false,
+ isMarginLeftHighlighted: false
+ },
+ {
+ desc: "Tests that the strict search filter works properly in the " +
+ "computed list for property line input",
+ search: "`margin-top`:`4px`",
+ isExpanderOpen: true,
+ isFilterOpen: true,
+ isMarginHighlighted: false,
+ isMarginTopHighlighted: true,
+ isMarginRightHighlighted: false,
+ isMarginBottomHighlighted: false,
+ isMarginLeftHighlighted: false
+ },
+ {
+ desc: "Tests that the strict search filter works properly in the " +
+ "computed list for a parsed strict property name and non-strict " +
+ "property value",
+ search: "`margin-top`:4px",
+ isExpanderOpen: true,
+ isFilterOpen: true,
+ isMarginHighlighted: false,
+ isMarginTopHighlighted: true,
+ isMarginRightHighlighted: false,
+ isMarginBottomHighlighted: false,
+ isMarginLeftHighlighted: false
+ },
+ {
+ desc: "Tests that the strict search filter works properly in the " +
+ "computed list for a parsed strict property value and non-strict " +
+ "property name",
+ search: "i:`4px`",
+ isExpanderOpen: true,
+ isFilterOpen: true,
+ isMarginHighlighted: false,
+ isMarginTopHighlighted: true,
+ isMarginRightHighlighted: false,
+ isMarginBottomHighlighted: false,
+ isMarginLeftHighlighted: false
+ },
+];
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+ yield testAddTextInFilter(inspector, view);
+});
+
+function* testAddTextInFilter(inspector, view) {
+ for (let data of TEST_DATA) {
+ info(data.desc);
+ yield setSearchFilter(view, data.search);
+ yield checkRules(view, data);
+ yield clearSearchAndCheckRules(view);
+ }
+}
+
+function* checkRules(view, data) {
+ info("Check that the correct rules are visible");
+ is(view.element.children.length, 2, "Should have 2 rules.");
+ is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element",
+ "First rule is inline element.");
+
+ let rule = getRuleViewRuleEditor(view, 1).rule;
+ let textPropEditor = rule.textProps[0].editor;
+ let computed = textPropEditor.computed;
+
+ is(rule.selectorText, "#testid", "Second rule is #testid.");
+ is(!!textPropEditor.expander.getAttribute("open"), data.isExpanderOpen,
+ "Got correct expander state.");
+ is(computed.hasAttribute("filter-open"), data.isFilterOpen,
+ "Got correct expanded state for margin computed list.");
+ is(textPropEditor.container.classList.contains("ruleview-highlight"),
+ data.isMarginHighlighted,
+ "Got correct highlight for margin text property.");
+
+ is(computed.children[0].classList.contains("ruleview-highlight"),
+ data.isMarginTopHighlighted,
+ "Got correct highlight for margin-top computed property.");
+ is(computed.children[1].classList.contains("ruleview-highlight"),
+ data.isMarginRightHighlighted,
+ "Got correct highlight for margin-right computed property.");
+ is(computed.children[2].classList.contains("ruleview-highlight"),
+ data.isMarginBottomHighlighted,
+ "Got correct highlight for margin-bottom computed property.");
+ is(computed.children[3].classList.contains("ruleview-highlight"),
+ data.isMarginLeftHighlighted,
+ "Got correct highlight for margin-left computed property.");
+}
+
+function* clearSearchAndCheckRules(view) {
+ let win = view.styleWindow;
+ let searchField = view.searchField;
+ let searchClearButton = view.searchClearButton;
+
+ let rule = getRuleViewRuleEditor(view, 1).rule;
+ let textPropEditor = rule.textProps[0].editor;
+ let computed = textPropEditor.computed;
+
+ info("Clearing the search filter");
+ EventUtils.synthesizeMouseAtCenter(searchClearButton, {}, win);
+ yield view.inspector.once("ruleview-filtered");
+
+ info("Check the search filter is cleared and no rules are highlighted");
+ is(view.element.children.length, 3, "Should have 3 rules.");
+ ok(!searchField.value, "Search filter is cleared");
+ ok(!view.styleDocument.querySelectorAll(".ruleview-highlight").length,
+ "No rules are higlighted");
+
+ ok(!textPropEditor.expander.getAttribute("open"), "Expander is closed.");
+ ok(!computed.hasAttribute("filter-open"),
+ "margin computed list is closed.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_strict-search-filter_01.js b/devtools/client/inspector/rules/test/browser_rules_strict-search-filter_01.js
new file mode 100644
index 000000000..50948e174
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_strict-search-filter_01.js
@@ -0,0 +1,130 @@
+/* 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";
+
+// Tests that the rule view strict search filter works properly for property
+// names.
+
+const TEST_URI = `
+ <style type="text/css">
+ #testid {
+ width: 2%;
+ color: red;
+ }
+ .testclass {
+ width: 22%;
+ background-color: #00F;
+ }
+ </style>
+ <h1 id="testid" class="testclass">Styled Node</h1>
+`;
+
+const TEST_DATA = [
+ {
+ desc: "Tests that the strict search filter works properly for property " +
+ "names",
+ search: "`color`",
+ ruleCount: 2,
+ propertyIndex: 1
+ },
+ {
+ desc: "Tests that the strict search filter works properly for property " +
+ "values",
+ search: "`2%`",
+ ruleCount: 2,
+ propertyIndex: 0
+ },
+ {
+ desc: "Tests that the strict search filter works properly for parsed " +
+ "property names",
+ search: "`color`:",
+ ruleCount: 2,
+ propertyIndex: 1
+ },
+ {
+ desc: "Tests that the strict search filter works properly for parsed " +
+ "property values",
+ search: ":`2%`",
+ ruleCount: 2,
+ propertyIndex: 0
+ },
+ {
+ desc: "Tests that the strict search filter works properly for property " +
+ "line input",
+ search: "`width`:`2%`",
+ ruleCount: 2,
+ propertyIndex: 0
+ },
+ {
+ desc: "Tests that the search filter works properly for a parsed strict " +
+ "property name and non-strict property value.",
+ search: "`width`:2%",
+ ruleCount: 3,
+ propertyIndex: 0
+ },
+ {
+ desc: "Tests that the search filter works properly for a parsed strict " +
+ "property value and non-strict property name.",
+ search: "i:`2%`",
+ ruleCount: 2,
+ propertyIndex: 0
+ }
+];
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+ yield testAddTextInFilter(inspector, view);
+});
+
+function* testAddTextInFilter(inspector, view) {
+ for (let data of TEST_DATA) {
+ info(data.desc);
+ yield setSearchFilter(view, data.search);
+ yield checkRules(view, data);
+ yield clearSearchAndCheckRules(view);
+ }
+}
+
+function* checkRules(view, data) {
+ info("Check that the correct rules are visible");
+ is(view.element.children.length, data.ruleCount,
+ "Should have " + data.ruleCount + " rules.");
+ is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element",
+ "First rule is inline element.");
+
+ let rule = getRuleViewRuleEditor(view, 1).rule;
+
+ is(rule.selectorText, "#testid", "Second rule is #testid.");
+ ok(rule.textProps[data.propertyIndex].editor.container.classList
+ .contains("ruleview-highlight"),
+ "Text property is correctly highlighted.");
+
+ if (data.ruleCount > 2) {
+ rule = getRuleViewRuleEditor(view, 2).rule;
+ is(rule.selectorText, ".testclass", "Third rule is .testclass.");
+ ok(rule.textProps[data.propertyIndex].editor.container.classList
+ .contains("ruleview-highlight"),
+ "Text property is correctly highlighted.");
+ }
+}
+
+function* clearSearchAndCheckRules(view) {
+ let doc = view.styleDocument;
+ let win = view.styleWindow;
+ let searchField = view.searchField;
+ let searchClearButton = view.searchClearButton;
+
+ info("Clearing the search filter");
+ EventUtils.synthesizeMouseAtCenter(searchClearButton, {}, win);
+ yield view.inspector.once("ruleview-filtered");
+
+ info("Check the search filter is cleared and no rules are highlighted");
+ is(view.element.children.length, 3, "Should have 3 rules.");
+ ok(!searchField.value, "Search filter is cleared.");
+ ok(!doc.querySelectorAll(".ruleview-highlight").length,
+ "No rules are higlighted.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_strict-search-filter_02.js b/devtools/client/inspector/rules/test/browser_rules_strict-search-filter_02.js
new file mode 100644
index 000000000..0c76f0518
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_strict-search-filter_02.js
@@ -0,0 +1,34 @@
+/* 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";
+
+// Tests that the rule view strict search filter works properly for stylesheet
+// source.
+
+const SEARCH = "`doc_urls_clickable.css:1`";
+const TEST_URI = URL_ROOT + "doc_urls_clickable.html";
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let {inspector, view} = yield openRuleView();
+ yield selectNode(".relative1", inspector);
+ yield testAddTextInFilter(inspector, view);
+});
+
+function* testAddTextInFilter(inspector, view) {
+ yield setSearchFilter(view, SEARCH);
+
+ info("Check that the correct rules are visible");
+ is(view.element.children.length, 2, "Should have 2 rules.");
+ is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element",
+ "First rule is inline element.");
+
+ let rule = getRuleViewRuleEditor(view, 1).rule;
+ let source = rule.textProps[0].editor.ruleEditor.source;
+
+ is(rule.selectorText, ".relative1", "Second rule is .relative1.");
+ ok(source.classList.contains("ruleview-highlight"),
+ "stylesheet source is correctly highlighted.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_strict-search-filter_03.js b/devtools/client/inspector/rules/test/browser_rules_strict-search-filter_03.js
new file mode 100644
index 000000000..0326b0e9c
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_strict-search-filter_03.js
@@ -0,0 +1,44 @@
+/* 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";
+
+// Tests that the rule view strict search filter works properly for selector
+// values.
+
+const SEARCH = "`.testclass`";
+
+const TEST_URI = `
+ <style type="text/css">
+ .testclass1 {
+ background-color: #00F;
+ }
+ .testclass {
+ color: red;
+ }
+ </style>
+ <h1 id="testid" class="testclass testclass1">Styled Node</h1>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+ yield testAddTextInFilter(inspector, view);
+});
+
+function* testAddTextInFilter(inspector, view) {
+ yield setSearchFilter(view, SEARCH);
+
+ info("Check that the correct rules are visible");
+ is(view.element.children.length, 2, "Should have 2 rules.");
+ is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element",
+ "First rule is inline element.");
+
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+
+ is(ruleEditor.rule.selectorText, ".testclass", "Second rule is .testclass.");
+ ok(ruleEditor.selectorText.children[0].classList
+ .contains("ruleview-highlight"), ".testclass selector is highlighted.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_style-editor-link.js b/devtools/client/inspector/rules/test/browser_rules_style-editor-link.js
new file mode 100644
index 000000000..927deb8ce
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_style-editor-link.js
@@ -0,0 +1,203 @@
+/* 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";
+
+// FIXME: Whitelisting this test.
+// As part of bug 1077403, the leaking uncaught rejection should be fixed.
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Unknown sheet source");
+
+// Test the links from the rule-view to the styleeditor
+
+const STYLESHEET_URL = "data:text/css," + encodeURIComponent(
+ ["#first {",
+ "color: blue",
+ "}"].join("\n"));
+
+const EXTERNAL_STYLESHEET_FILE_NAME = "doc_style_editor_link.css";
+const EXTERNAL_STYLESHEET_URL = URL_ROOT + EXTERNAL_STYLESHEET_FILE_NAME;
+
+const DOCUMENT_URL = "data:text/html;charset=utf-8," + encodeURIComponent(`
+ <html>
+ <head>
+ <title>Rule view style editor link test</title>
+ <style type="text/css">
+ html { color: #000000; }
+ div { font-variant: small-caps; color: #000000; }
+ .nomatches {color: #ff0000;}</style> <div id="first" style="margin: 10em;
+ font-size: 14pt; font-family: helvetica, sans-serif; color: #AAA">
+ </style>
+ <style>
+ div { font-weight: bold; }
+ </style>
+ <link rel="stylesheet" type="text/css" href="${STYLESHEET_URL}">
+ <link rel="stylesheet" type="text/css" href="${EXTERNAL_STYLESHEET_URL}">
+ </head>
+ <body>
+ <h1>Some header text</h1>
+ <p id="salutation" style="font-size: 12pt">hi.</p>
+ <p id="body" style="font-size: 12pt">I am a test-case. This text exists
+ solely to provide some things to
+ <span style="color: yellow" class="highlight">
+ highlight</span> and <span style="font-weight: bold">count</span>
+ style list-items in the box at right. If you are reading this,
+ you should go do something else instead. Maybe read a book. Or better
+ yet, write some test-cases for another bit of code.
+ <span style="font-style: italic">some text</span></p>
+ <p id="closing">more text</p>
+ <p>even more text</p>
+ </div>
+ </body>
+ </html>
+`);
+
+add_task(function* () {
+ yield addTab(DOCUMENT_URL);
+ let {toolbox, inspector, view, testActor} = yield openRuleView();
+ yield selectNode("div", inspector);
+
+ yield testInlineStyle(view);
+ yield testFirstInlineStyleSheet(view, toolbox, testActor);
+ yield testSecondInlineStyleSheet(view, toolbox, testActor);
+ yield testExternalStyleSheet(view, toolbox, testActor);
+ yield testDisabledStyleEditor(view, toolbox);
+});
+
+function* testInlineStyle(view) {
+ info("Testing inline style");
+
+ let onTab = waitForTab();
+ info("Clicking on the first link in the rule-view");
+ clickLinkByIndex(view, 0);
+
+ let tab = yield onTab;
+
+ let tabURI = tab.linkedBrowser.documentURI.spec;
+ ok(tabURI.startsWith("view-source:"), "View source tab is open");
+ info("Closing tab");
+ gBrowser.removeTab(tab);
+}
+
+function* testFirstInlineStyleSheet(view, toolbox, testActor) {
+ info("Testing inline stylesheet");
+
+ info("Listening for toolbox switch to the styleeditor");
+ let onSwitch = waitForStyleEditor(toolbox);
+
+ info("Clicking an inline stylesheet");
+ clickLinkByIndex(view, 4);
+ let editor = yield onSwitch;
+
+ ok(true, "Switched to the style-editor panel in the toolbox");
+
+ yield validateStyleEditorSheet(editor, 0, testActor);
+}
+
+function* testSecondInlineStyleSheet(view, toolbox, testActor) {
+ info("Testing second inline stylesheet");
+
+ info("Waiting for the stylesheet editor to be selected");
+ let panel = toolbox.getCurrentPanel();
+ let onSelected = panel.UI.once("editor-selected");
+
+ info("Switching back to the inspector panel in the toolbox");
+ yield toolbox.selectTool("inspector");
+
+ info("Clicking on second inline stylesheet link");
+ testRuleViewLinkLabel(view);
+ clickLinkByIndex(view, 3);
+ let editor = yield onSelected;
+
+ is(toolbox.currentToolId, "styleeditor",
+ "The style editor is selected again");
+ yield validateStyleEditorSheet(editor, 1, testActor);
+}
+
+function* testExternalStyleSheet(view, toolbox, testActor) {
+ info("Testing external stylesheet");
+
+ info("Waiting for the stylesheet editor to be selected");
+ let panel = toolbox.getCurrentPanel();
+ let onSelected = panel.UI.once("editor-selected");
+
+ info("Switching back to the inspector panel in the toolbox");
+ yield toolbox.selectTool("inspector");
+
+ info("Clicking on an external stylesheet link");
+ testRuleViewLinkLabel(view);
+ clickLinkByIndex(view, 1);
+ let editor = yield onSelected;
+
+ is(toolbox.currentToolId, "styleeditor",
+ "The style editor is selected again");
+ yield validateStyleEditorSheet(editor, 2, testActor);
+}
+
+function* validateStyleEditorSheet(editor, expectedSheetIndex, testActor) {
+ info("validating style editor stylesheet");
+ is(editor.styleSheet.styleSheetIndex, expectedSheetIndex,
+ "loaded stylesheet index matches document stylesheet");
+
+ let href = editor.styleSheet.href || editor.styleSheet.nodeHref;
+
+ let expectedHref = yield testActor.eval(
+ `content.document.styleSheets[${expectedSheetIndex}].href ||
+ content.document.location.href`);
+
+ is(href, expectedHref, "loaded stylesheet href matches document stylesheet");
+}
+
+function* testDisabledStyleEditor(view, toolbox) {
+ info("Testing with the style editor disabled");
+
+ info("Switching to the inspector panel in the toolbox");
+ yield toolbox.selectTool("inspector");
+
+ info("Disabling the style editor");
+ Services.prefs.setBoolPref("devtools.styleeditor.enabled", false);
+ gDevTools.emit("tool-unregistered", "styleeditor");
+
+ info("Clicking on a link");
+ testUnselectableRuleViewLink(view, 1);
+ clickLinkByIndex(view, 1);
+
+ is(toolbox.currentToolId, "inspector", "The click should have no effect");
+
+ info("Enabling the style editor");
+ Services.prefs.setBoolPref("devtools.styleeditor.enabled", true);
+ gDevTools.emit("tool-registered", "styleeditor");
+
+ info("Clicking on a link");
+ let onStyleEditorSelected = toolbox.once("styleeditor-selected");
+ clickLinkByIndex(view, 1);
+ yield onStyleEditorSelected;
+ is(toolbox.currentToolId, "styleeditor", "Style Editor should be selected");
+
+ Services.prefs.clearUserPref("devtools.styleeditor.enabled");
+}
+
+function testRuleViewLinkLabel(view) {
+ let link = getRuleViewLinkByIndex(view, 2);
+ let labelElem = link.querySelector(".ruleview-rule-source-label");
+ let value = labelElem.textContent;
+ let tooltipText = labelElem.getAttribute("title");
+
+ is(value, EXTERNAL_STYLESHEET_FILE_NAME + ":1",
+ "rule view stylesheet display value matches filename and line number");
+ is(tooltipText, EXTERNAL_STYLESHEET_URL + ":1",
+ "rule view stylesheet tooltip text matches the full URI path");
+}
+
+function testUnselectableRuleViewLink(view, index) {
+ let link = getRuleViewLinkByIndex(view, index);
+ let unselectable = link.hasAttribute("unselectable");
+
+ ok(unselectable, "Rule view is unselectable");
+}
+
+function clickLinkByIndex(view, index) {
+ let link = getRuleViewLinkByIndex(view, index);
+ link.scrollIntoView();
+ link.click();
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_urls-clickable.js b/devtools/client/inspector/rules/test/browser_rules_urls-clickable.js
new file mode 100644
index 000000000..fb1211e3c
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_urls-clickable.js
@@ -0,0 +1,70 @@
+/* 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";
+
+// Tests to make sure that URLs are clickable in the rule view
+
+const TEST_URI = URL_ROOT + "doc_urls_clickable.html";
+const TEST_IMAGE = URL_ROOT + "doc_test_image.png";
+const BASE_64_URL = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAA" +
+ "FCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAA" +
+ "BJRU5ErkJggg==";
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let {inspector, view} = yield openRuleView();
+ yield selectNodes(inspector, view);
+});
+
+function* selectNodes(inspector, ruleView) {
+ let relative1 = ".relative1";
+ let relative2 = ".relative2";
+ let absolute = ".absolute";
+ let inline = ".inline";
+ let base64 = ".base64";
+ let noimage = ".noimage";
+ let inlineresolved = ".inline-resolved";
+
+ yield selectNode(relative1, inspector);
+ let relativeLink = ruleView.styleDocument
+ .querySelector(".ruleview-propertyvaluecontainer a");
+ ok(relativeLink, "Link exists for relative1 node");
+ is(relativeLink.getAttribute("href"), TEST_IMAGE, "href matches");
+
+ yield selectNode(relative2, inspector);
+ relativeLink = ruleView.styleDocument
+ .querySelector(".ruleview-propertyvaluecontainer a");
+ ok(relativeLink, "Link exists for relative2 node");
+ is(relativeLink.getAttribute("href"), TEST_IMAGE, "href matches");
+
+ yield selectNode(absolute, inspector);
+ let absoluteLink = ruleView.styleDocument
+ .querySelector(".ruleview-propertyvaluecontainer a");
+ ok(absoluteLink, "Link exists for absolute node");
+ is(absoluteLink.getAttribute("href"), TEST_IMAGE, "href matches");
+
+ yield selectNode(inline, inspector);
+ let inlineLink = ruleView.styleDocument
+ .querySelector(".ruleview-propertyvaluecontainer a");
+ ok(inlineLink, "Link exists for inline node");
+ is(inlineLink.getAttribute("href"), TEST_IMAGE, "href matches");
+
+ yield selectNode(base64, inspector);
+ let base64Link = ruleView.styleDocument
+ .querySelector(".ruleview-propertyvaluecontainer a");
+ ok(base64Link, "Link exists for base64 node");
+ is(base64Link.getAttribute("href"), BASE_64_URL, "href matches");
+
+ yield selectNode(inlineresolved, inspector);
+ let inlineResolvedLink = ruleView.styleDocument
+ .querySelector(".ruleview-propertyvaluecontainer a");
+ ok(inlineResolvedLink, "Link exists for style tag node");
+ is(inlineResolvedLink.getAttribute("href"), TEST_IMAGE, "href matches");
+
+ yield selectNode(noimage, inspector);
+ let noimageLink = ruleView.styleDocument
+ .querySelector(".ruleview-propertyvaluecontainer a");
+ ok(!noimageLink, "There is no link for the node with no background image");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_user-agent-styles-uneditable.js b/devtools/client/inspector/rules/test/browser_rules_user-agent-styles-uneditable.js
new file mode 100644
index 000000000..e1bafff9b
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_user-agent-styles-uneditable.js
@@ -0,0 +1,58 @@
+/* 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 user agent styles are never editable via
+// the UI
+
+const TEST_URI = `
+ <blockquote type=cite>
+ <pre _moz_quote=true>
+ inspect <a href='foo' style='color:orange'>user agent</a> styles
+ </pre>
+ </blockquote>
+`;
+
+var PREF_UA_STYLES = "devtools.inspector.showUserAgentStyles";
+
+add_task(function* () {
+ info("Starting the test with the pref set to true before toolbox is opened");
+ Services.prefs.setBoolPref(PREF_UA_STYLES, true);
+
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+
+ yield userAgentStylesUneditable(inspector, view);
+
+ info("Resetting " + PREF_UA_STYLES);
+ Services.prefs.clearUserPref(PREF_UA_STYLES);
+});
+
+function* userAgentStylesUneditable(inspector, view) {
+ info("Making sure that UI is not editable for user agent styles");
+
+ yield selectNode("a", inspector);
+ let uaRules = view._elementStyle.rules.filter(rule=>!rule.editor.isEditable);
+
+ for (let rule of uaRules) {
+ ok(rule.editor.element.hasAttribute("uneditable"),
+ "UA rules have uneditable attribute");
+
+ let firstProp = rule.textProps.filter(p => !p.invisible)[0];
+
+ ok(!firstProp.editor.nameSpan._editable,
+ "nameSpan is not editable");
+ ok(!firstProp.editor.valueSpan._editable,
+ "valueSpan is not editable");
+ ok(!rule.editor.closeBrace._editable, "closeBrace is not editable");
+
+ let colorswatch = rule.editor.element
+ .querySelector(".ruleview-colorswatch");
+ if (colorswatch) {
+ ok(!view.tooltips.colorPicker.swatches.has(colorswatch),
+ "The swatch is not editable");
+ }
+ }
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_user-agent-styles.js b/devtools/client/inspector/rules/test/browser_rules_user-agent-styles.js
new file mode 100644
index 000000000..6852e3c03
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_user-agent-styles.js
@@ -0,0 +1,183 @@
+/* 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 user agent styles are inspectable via rule view if
+// it is preffed on.
+
+var PREF_UA_STYLES = "devtools.inspector.showUserAgentStyles";
+const { PrefObserver } = require("devtools/client/styleeditor/utils");
+
+const TEST_URI = URL_ROOT + "doc_author-sheet.html";
+
+const TEST_DATA = [
+ {
+ selector: "blockquote",
+ numUserRules: 1,
+ numUARules: 0
+ },
+ {
+ selector: "pre",
+ numUserRules: 1,
+ numUARules: 0
+ },
+ {
+ selector: "input[type=range]",
+ numUserRules: 1,
+ numUARules: 0
+ },
+ {
+ selector: "input[type=number]",
+ numUserRules: 1,
+ numUARules: 0
+ },
+ {
+ selector: "input[type=color]",
+ numUserRules: 1,
+ numUARules: 0
+ },
+ {
+ selector: "input[type=text]",
+ numUserRules: 1,
+ numUARules: 0
+ },
+ {
+ selector: "progress",
+ numUserRules: 1,
+ numUARules: 0
+ },
+ // Note that some tests below assume that the "a" selector is the
+ // last test in TEST_DATA.
+ {
+ selector: "a",
+ numUserRules: 3,
+ numUARules: 0
+ }
+];
+
+add_task(function* () {
+ requestLongerTimeout(4);
+
+ info("Starting the test with the pref set to true before toolbox is opened");
+ yield setUserAgentStylesPref(true);
+
+ yield addTab(TEST_URI);
+ let {inspector, view} = yield openRuleView();
+
+ info("Making sure that UA styles are visible on initial load");
+ yield userAgentStylesVisible(inspector, view);
+
+ info("Making sure that setting the pref to false hides UA styles");
+ yield setUserAgentStylesPref(false);
+ yield userAgentStylesNotVisible(inspector, view);
+
+ info("Making sure that resetting the pref to true shows UA styles again");
+ yield setUserAgentStylesPref(true);
+ yield userAgentStylesVisible(inspector, view);
+
+ info("Resetting " + PREF_UA_STYLES);
+ Services.prefs.clearUserPref(PREF_UA_STYLES);
+});
+
+function* setUserAgentStylesPref(val) {
+ info("Setting the pref " + PREF_UA_STYLES + " to: " + val);
+
+ // Reset the pref and wait for PrefObserver to callback so UI
+ // has a chance to get updated.
+ let oncePrefChanged = defer();
+ let prefObserver = new PrefObserver("devtools.");
+ prefObserver.on(PREF_UA_STYLES, oncePrefChanged.resolve);
+ Services.prefs.setBoolPref(PREF_UA_STYLES, val);
+ yield oncePrefChanged.promise;
+ prefObserver.off(PREF_UA_STYLES, oncePrefChanged.resolve);
+}
+
+function* userAgentStylesVisible(inspector, view) {
+ info("Making sure that user agent styles are currently visible");
+
+ let userRules;
+ let uaRules;
+
+ for (let data of TEST_DATA) {
+ yield selectNode(data.selector, inspector);
+ yield compareAppliedStylesWithUI(inspector, view, "ua");
+
+ userRules = view._elementStyle.rules.filter(rule=>rule.editor.isEditable);
+ uaRules = view._elementStyle.rules.filter(rule=>!rule.editor.isEditable);
+ is(userRules.length, data.numUserRules, "Correct number of user rules");
+ ok(uaRules.length > data.numUARules, "Has UA rules");
+ }
+
+ ok(userRules.some(rule => rule.matchedSelectors.length === 1),
+ "There is an inline style for element in user styles");
+
+ // These tests rely on the "a" selector being the last test in
+ // TEST_DATA.
+ ok(uaRules.some(rule => {
+ return rule.matchedSelectors.indexOf(":any-link") !== -1;
+ }), "There is a rule for :any-link");
+ ok(uaRules.some(rule => {
+ return rule.matchedSelectors.indexOf("*|*:link") !== -1;
+ }), "There is a rule for *|*:link");
+ ok(uaRules.some(rule => {
+ return rule.matchedSelectors.length === 1;
+ }), "Inline styles for ua styles");
+}
+
+function* userAgentStylesNotVisible(inspector, view) {
+ info("Making sure that user agent styles are not currently visible");
+
+ let userRules;
+ let uaRules;
+
+ for (let data of TEST_DATA) {
+ yield selectNode(data.selector, inspector);
+ yield compareAppliedStylesWithUI(inspector, view);
+
+ userRules = view._elementStyle.rules.filter(rule=>rule.editor.isEditable);
+ uaRules = view._elementStyle.rules.filter(rule=>!rule.editor.isEditable);
+ is(userRules.length, data.numUserRules, "Correct number of user rules");
+ is(uaRules.length, data.numUARules, "No UA rules");
+ }
+}
+
+function* compareAppliedStylesWithUI(inspector, view, filter) {
+ info("Making sure that UI is consistent with pageStyle.getApplied");
+
+ let entries = yield inspector.pageStyle.getApplied(
+ inspector.selection.nodeFront,
+ {
+ inherited: true,
+ matchedSelectors: true,
+ filter: filter
+ }
+ );
+
+ // We may see multiple entries that map to a given rule; filter the
+ // duplicates here to match what the UI does.
+ let entryMap = new Map();
+ for (let entry of entries) {
+ entryMap.set(entry.rule, entry);
+ }
+ entries = [...entryMap.values()];
+
+ let elementStyle = view._elementStyle;
+ is(elementStyle.rules.length, entries.length,
+ "Should have correct number of rules (" + entries.length + ")");
+
+ entries = entries.sort((a, b) => {
+ return (a.pseudoElement || "z") > (b.pseudoElement || "z");
+ });
+
+ entries.forEach((entry, i) => {
+ let elementStyleRule = elementStyle.rules[i];
+ is(elementStyleRule.inherited, entry.inherited,
+ "Same inherited (" + entry.inherited + ")");
+ is(elementStyleRule.isSystem, entry.isSystem,
+ "Same isSystem (" + entry.isSystem + ")");
+ is(elementStyleRule.editor.isEditable, !entry.isSystem,
+ "Editor isEditable opposite of UA (" + entry.isSystem + ")");
+ });
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_user-property-reset.js b/devtools/client/inspector/rules/test/browser_rules_user-property-reset.js
new file mode 100644
index 000000000..62b1d927c
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_user-property-reset.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";
+
+// Test that user set style properties can be changed from the markup-view and
+// don't survive page reload
+
+const TEST_URI = `
+ <p id='id1' style='width:200px;'>element 1</p>
+ <p id='id2' style='width:100px;'>element 2</p>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view, testActor} = yield openRuleView();
+
+ yield selectNode("#id1", inspector);
+ yield modifyRuleViewWidth("300px", view, inspector);
+ yield assertRuleAndMarkupViewWidth("id1", "300px", view, inspector);
+
+ yield selectNode("#id2", inspector);
+ yield assertRuleAndMarkupViewWidth("id2", "100px", view, inspector);
+ yield modifyRuleViewWidth("50px", view, inspector);
+ yield assertRuleAndMarkupViewWidth("id2", "50px", view, inspector);
+
+ yield reloadPage(inspector, testActor);
+
+ yield selectNode("#id1", inspector);
+ yield assertRuleAndMarkupViewWidth("id1", "200px", view, inspector);
+ yield selectNode("#id2", inspector);
+ yield assertRuleAndMarkupViewWidth("id2", "100px", view, inspector);
+});
+
+function getStyleRule(ruleView) {
+ return ruleView.styleDocument.querySelector(".ruleview-rule");
+}
+
+function* modifyRuleViewWidth(value, ruleView, inspector) {
+ info("Getting the property value element");
+ let valueSpan = getStyleRule(ruleView)
+ .querySelector(".ruleview-propertyvalue");
+
+ info("Focusing the property value to set it to edit mode");
+ let editor = yield focusEditableField(ruleView, valueSpan.parentNode);
+
+ ok(editor.input, "The inplace-editor field is ready");
+ info("Setting the new value");
+ editor.input.value = value;
+
+ info("Pressing return and waiting for the field to blur and for the " +
+ "markup-view to show the mutation");
+ let onBlur = once(editor.input, "blur", true);
+ let onStyleChanged = waitForStyleModification(inspector);
+ EventUtils.sendKey("return");
+ yield onBlur;
+ yield onStyleChanged;
+
+ info("Escaping out of the new property field that has been created after " +
+ "the value was edited");
+ let onNewFieldBlur = once(ruleView.styleDocument.activeElement, "blur", true);
+ EventUtils.sendKey("escape");
+ yield onNewFieldBlur;
+}
+
+function* getContainerStyleAttrValue(id, {walker, markup}) {
+ let front = yield walker.querySelector(walker.rootNode, "#" + id);
+ let container = markup.getContainer(front);
+
+ let attrIndex = 0;
+ for (let attrName of container.elt.querySelectorAll(".attr-name")) {
+ if (attrName.textContent === "style") {
+ return container.elt.querySelectorAll(".attr-value")[attrIndex];
+ }
+ attrIndex++;
+ }
+ return undefined;
+}
+
+function* assertRuleAndMarkupViewWidth(id, value, ruleView, inspector) {
+ let valueSpan = getStyleRule(ruleView)
+ .querySelector(".ruleview-propertyvalue");
+ is(valueSpan.textContent, value,
+ "Rule-view style width is " + value + " as expected");
+
+ let attr = yield getContainerStyleAttrValue(id, inspector);
+ is(attr.textContent.replace(/\s/g, ""),
+ "width:" + value + ";", "Markup-view style attribute width is " + value);
+}
diff --git a/devtools/client/inspector/rules/test/doc_author-sheet.html b/devtools/client/inspector/rules/test/doc_author-sheet.html
new file mode 100644
index 000000000..f8c2eadd5
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_author-sheet.html
@@ -0,0 +1,39 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<html>
+<head>
+ <title>authored sheet test</title>
+
+ <style>
+ pre a {
+ color: orange;
+ }
+ </style>
+
+ <script>
+ "use strict";
+ var gIOService = SpecialPowers.Cc["@mozilla.org/network/io-service;1"]
+ .getService(SpecialPowers.Ci.nsIIOService);
+
+ var style = "data:text/css,a { background-color: seagreen; }";
+ var uri = gIOService.newURI(style, null, null);
+ var windowUtils = SpecialPowers.wrap(window)
+ .QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor)
+ .getInterface(SpecialPowers.Ci.nsIDOMWindowUtils);
+ windowUtils.loadSheet(uri, windowUtils.AUTHOR_SHEET);
+ </script>
+
+</head>
+<body>
+ <input type=text placeholder=test></input>
+ <input type=color></input>
+ <input type=range></input>
+ <input type=number></input>
+ <progress></progress>
+ <blockquote type=cite>
+ <pre _moz_quote=true>
+ inspect <a href="foo">user agent</a> styles
+ </pre>
+ </blockquote>
+</body>
+</html>
diff --git a/devtools/client/inspector/rules/test/doc_blob_stylesheet.html b/devtools/client/inspector/rules/test/doc_blob_stylesheet.html
new file mode 100644
index 000000000..c9973993b
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_blob_stylesheet.html
@@ -0,0 +1,39 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+</html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Blob stylesheet sourcemap</title>
+</head>
+<body>
+<h1>Test</h1>
+<script>
+"use strict";
+
+var cssContent = `body {
+ background-color: black;
+}
+body > h1 {
+ color: white;
+}
+` +
+"/*# sourceMappingURL=data:application/json;base64,ewoidmVyc2lvbiI6IDMsCiJtYX" +
+"BwaW5ncyI6ICJBQUFBLElBQUs7RUFDSCxnQkFBZ0IsRUFBRSxLQUFLOztBQUN2QixTQUFPO0VBQ0" +
+"wsS0FBSyxFQUFFLEtBQUsiLAoic291cmNlcyI6IFsidGVzdC5zY3NzIl0sCiJzb3VyY2VzQ29udG" +
+"VudCI6IFsiYm9keSB7XG4gIGJhY2tncm91bmQtY29sb3I6IGJsYWNrO1xuICAmID4gaDEge1xuIC" +
+"AgIGNvbG9yOiB3aGl0ZTsgIFxuICB9XG59XG4iXSwKIm5hbWVzIjogW10sCiJmaWxlIjogInRlc3" +
+"QuY3NzIgp9Cg== */";
+var cssBlob = new Blob([cssContent], {type: "text/css"});
+var url = URL.createObjectURL(cssBlob);
+
+var head = document.querySelector("head");
+var link = document.createElement("link");
+link.rel = "stylesheet";
+link.type = "text/css";
+link.href = url;
+head.appendChild(link);
+</script>
+</body>
+</html>
diff --git a/devtools/client/inspector/rules/test/doc_content_stylesheet.html b/devtools/client/inspector/rules/test/doc_content_stylesheet.html
new file mode 100644
index 000000000..3ea65f606
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_content_stylesheet.html
@@ -0,0 +1,35 @@
+<html>
+<head>
+ <title>test</title>
+
+ <link href="./doc_content_stylesheet_linked.css" rel="stylesheet" type="text/css">
+
+ <script>
+ /* eslint no-unused-vars: [2, {"vars": "local"}] */
+ "use strict";
+ // Load script.css
+ function loadCSS() {
+ let link = document.createElement("link");
+ link.rel = "stylesheet";
+ link.type = "text/css";
+ link.href = "./doc_content_stylesheet_script.css";
+ document.getElementsByTagName("head")[0].appendChild(link);
+ }
+ </script>
+
+ <style>
+ table {
+ border: 1px solid #000;
+ }
+ </style>
+</head>
+<body onload="loadCSS();">
+ <table id="target">
+ <tr>
+ <td>
+ <h3>Simple test</h3>
+ </td>
+ </tr>
+ </table>
+</body>
+</html>
diff --git a/devtools/client/inspector/rules/test/doc_content_stylesheet_imported.css b/devtools/client/inspector/rules/test/doc_content_stylesheet_imported.css
new file mode 100644
index 000000000..ea1a3d986
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_content_stylesheet_imported.css
@@ -0,0 +1,5 @@
+@import url("./doc_content_stylesheet_imported2.css");
+
+#target {
+ text-decoration: underline;
+}
diff --git a/devtools/client/inspector/rules/test/doc_content_stylesheet_imported2.css b/devtools/client/inspector/rules/test/doc_content_stylesheet_imported2.css
new file mode 100644
index 000000000..77c73299e
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_content_stylesheet_imported2.css
@@ -0,0 +1,3 @@
+#target {
+ text-decoration: underline;
+}
diff --git a/devtools/client/inspector/rules/test/doc_content_stylesheet_linked.css b/devtools/client/inspector/rules/test/doc_content_stylesheet_linked.css
new file mode 100644
index 000000000..712ba78fb
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_content_stylesheet_linked.css
@@ -0,0 +1,3 @@
+table {
+ border-collapse: collapse;
+}
diff --git a/devtools/client/inspector/rules/test/doc_content_stylesheet_script.css b/devtools/client/inspector/rules/test/doc_content_stylesheet_script.css
new file mode 100644
index 000000000..5aa5e2c6c
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_content_stylesheet_script.css
@@ -0,0 +1,5 @@
+@import url("./doc_content_stylesheet_imported.css");
+
+table {
+ opacity: 1;
+}
diff --git a/devtools/client/inspector/rules/test/doc_copystyles.css b/devtools/client/inspector/rules/test/doc_copystyles.css
new file mode 100644
index 000000000..83f0c87b1
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_copystyles.css
@@ -0,0 +1,11 @@
+/* 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/. */
+
+html, body, #testid {
+ color: #F00;
+ background-color: #00F;
+ font-size: 12px;
+ border-color: #00F !important;
+ --var: "*/";
+}
diff --git a/devtools/client/inspector/rules/test/doc_copystyles.html b/devtools/client/inspector/rules/test/doc_copystyles.html
new file mode 100644
index 000000000..da1b4c0b3
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_copystyles.html
@@ -0,0 +1,11 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<html>
+ <head>
+ <title>Test case for copying stylesheet in rule-view</title>
+ <link rel="stylesheet" type="text/css" href="doc_copystyles.css"/>
+ </head>
+ <body>
+ <div id='testid'>Styled Node</div>
+ </body>
+</html>
diff --git a/devtools/client/inspector/rules/test/doc_cssom.html b/devtools/client/inspector/rules/test/doc_cssom.html
new file mode 100644
index 000000000..28de66d7d
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_cssom.html
@@ -0,0 +1,22 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<html>
+<head>
+ <title>CSSOM test</title>
+
+ <script>
+ "use strict";
+ window.onload = function () {
+ let x = document.styleSheets[0];
+ x.insertRule("div { color: seagreen; }", 1);
+ };
+ </script>
+
+ <style>
+ span { }
+ </style>
+</head>
+<body>
+ <div id="target"> the ocean </div>
+</body>
+</html>
diff --git a/devtools/client/inspector/rules/test/doc_custom.html b/devtools/client/inspector/rules/test/doc_custom.html
new file mode 100644
index 000000000..09bf501d5
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_custom.html
@@ -0,0 +1,33 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<html>
+ <head>
+ <style>
+ #testidSimple {
+ --background-color: blue;
+ }
+ .testclassSimple {
+ --background-color: green;
+ }
+
+ .testclassImportant {
+ --background-color: green !important;
+ }
+ #testidImportant {
+ --background-color: blue;
+ }
+
+ #testidDisable {
+ --background-color: blue;
+ }
+ .testclassDisable {
+ --background-color: green;
+ }
+ </style>
+ </head>
+ <body>
+ <div id="testidSimple" class="testclassSimple">Styled Node</div>
+ <div id="testidImportant" class="testclassImportant">Styled Node</div>
+ <div id="testidDisable" class="testclassDisable">Styled Node</div>
+ </body>
+</html>
diff --git a/devtools/client/inspector/rules/test/doc_filter.html b/devtools/client/inspector/rules/test/doc_filter.html
new file mode 100644
index 000000000..cb2df9feb
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_filter.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<!-- 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/. -->
+<html>
+<head>
+ <title>Bug 1055181 - CSS Filter Editor Widget</title>
+ <style>
+ body {
+ filter: blur(2px) contrast(2);
+ }
+ </style>
+</head>
diff --git a/devtools/client/inspector/rules/test/doc_frame_script.js b/devtools/client/inspector/rules/test/doc_frame_script.js
new file mode 100644
index 000000000..88da043f1
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_frame_script.js
@@ -0,0 +1,113 @@
+/* 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/ */
+/* globals addMessageListener, sendAsyncMessage */
+
+"use strict";
+
+// A helper frame-script for brower/devtools/styleinspector tests.
+//
+// Most listeners in the script expect "Test:"-namespaced messages from chrome,
+// then execute code upon receiving, and immediately send back a message.
+// This is so that chrome test code can execute code in content and wait for a
+// response this way:
+// let response = yield executeInContent(browser, "Test:msgName", data, true);
+// The response message should have the same name "Test:msgName"
+//
+// Some listeners do not send a response message back.
+
+var {utils: Cu} = Components;
+
+var {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+var defer = require("devtools/shared/defer");
+
+/**
+ * Get a value for a given property name in a css rule in a stylesheet, given
+ * their indexes
+ * @param {Object} data Expects a data object with the following properties
+ * - {Number} styleSheetIndex
+ * - {Number} ruleIndex
+ * - {String} name
+ * @return {String} The value, if found, null otherwise
+ */
+addMessageListener("Test:GetRulePropertyValue", function (msg) {
+ let {name, styleSheetIndex, ruleIndex} = msg.data;
+ let value = null;
+
+ dumpn("Getting the value for property name " + name + " in sheet " +
+ styleSheetIndex + " and rule " + ruleIndex);
+
+ let sheet = content.document.styleSheets[styleSheetIndex];
+ if (sheet) {
+ let rule = sheet.cssRules[ruleIndex];
+ if (rule) {
+ value = rule.style.getPropertyValue(name);
+ }
+ }
+
+ sendAsyncMessage("Test:GetRulePropertyValue", value);
+});
+
+/**
+ * Get the property value from the computed style for an element.
+ * @param {Object} data Expects a data object with the following properties
+ * - {String} selector: The selector used to obtain the element.
+ * - {String} pseudo: pseudo id to query, or null.
+ * - {String} name: name of the property
+ * @return {String} The value, if found, null otherwise
+ */
+addMessageListener("Test:GetComputedStylePropertyValue", function (msg) {
+ let {selector, pseudo, name} = msg.data;
+ let element = content.document.querySelector(selector);
+ let value = content.document.defaultView.getComputedStyle(element, pseudo)
+ .getPropertyValue(name);
+ sendAsyncMessage("Test:GetComputedStylePropertyValue", value);
+});
+
+/**
+ * Wait the property value from the computed style for an element and
+ * compare it with the expected value
+ * @param {Object} data Expects a data object with the following properties
+ * - {String} selector: The selector used to obtain the element.
+ * - {String} pseudo: pseudo id to query, or null.
+ * - {String} name: name of the property
+ * - {String} expected: the expected value for property
+ */
+addMessageListener("Test:WaitForComputedStylePropertyValue", function (msg) {
+ let {selector, pseudo, name, expected} = msg.data;
+ let element = content.document.querySelector(selector);
+ waitForSuccess(() => {
+ let value = content.document.defaultView.getComputedStyle(element, pseudo)
+ .getPropertyValue(name);
+
+ return value === expected;
+ }).then(() => {
+ sendAsyncMessage("Test:WaitForComputedStylePropertyValue");
+ });
+});
+
+var dumpn = msg => dump(msg + "\n");
+
+/**
+ * Polls a given function waiting for it to return true.
+ *
+ * @param {Function} validatorFn A validator function that returns a boolean.
+ * This is called every few milliseconds to check if the result is true. When
+ * it is true, the promise resolves.
+ * @return a promise that resolves when the function returned true or rejects
+ * if the timeout is reached
+ */
+function waitForSuccess(validatorFn) {
+ let def = defer();
+
+ function wait(fn) {
+ if (fn()) {
+ def.resolve();
+ } else {
+ setTimeout(() => wait(fn), 200);
+ }
+ }
+ wait(validatorFn);
+
+ return def.promise;
+}
diff --git a/devtools/client/inspector/rules/test/doc_inline_sourcemap.html b/devtools/client/inspector/rules/test/doc_inline_sourcemap.html
new file mode 100644
index 000000000..cb107d424
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_inline_sourcemap.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<html>
+<head>
+ <title>CSS source maps in inline stylesheets</title>
+</head>
+<body>
+ <div>CSS source maps in inline stylesheets</div>
+ <style>
+div {
+ color: #ff0066; }
+
+span {
+ background-color: #EEE; }
+
+/*# sourceMappingURL=doc_sourcemaps.css.map */
+ </style>
+</body>
+</html>
diff --git a/devtools/client/inspector/rules/test/doc_invalid_sourcemap.css b/devtools/client/inspector/rules/test/doc_invalid_sourcemap.css
new file mode 100644
index 000000000..ff96a6b54
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_invalid_sourcemap.css
@@ -0,0 +1,3 @@
+div { color: gold; }
+
+/*# sourceMappingURL=this-source-map-does-not-exist.css.map */ \ No newline at end of file
diff --git a/devtools/client/inspector/rules/test/doc_invalid_sourcemap.html b/devtools/client/inspector/rules/test/doc_invalid_sourcemap.html
new file mode 100644
index 000000000..2e6422bec
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_invalid_sourcemap.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Invalid source map</title>
+ <link rel="stylesheet" type="text/css" href="doc_invalid_sourcemap.css">
+</head>
+<body>
+ <div>invalid source map</div>
+</body>
+</html>
diff --git a/devtools/client/inspector/rules/test/doc_keyframeLineNumbers.html b/devtools/client/inspector/rules/test/doc_keyframeLineNumbers.html
new file mode 100644
index 000000000..8fce04584
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_keyframeLineNumbers.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>keyframe line numbers test</title>
+ <style type="text/css">
+div {
+ animation-duration: 1s;
+ animation-iteration-count: infinite;
+ animation-direction: alternate;
+ animation-name: CC;
+}
+
+span {
+ animation-duration: 3s;
+ animation-iteration-count: infinite;
+ animation-direction: alternate;
+ animation-name: DD;
+}
+
+@keyframes CC {
+ from {
+ background: #ffffff;
+ }
+ to {
+ background: #f0c;
+ }
+}
+
+@keyframes DD {
+ from {
+ background: seagreen;
+ }
+ to {
+ background: chartreuse;
+ }
+}
+ </style>
+</head>
+<body>
+ <div id="outer">
+ <span id="inner">lizards</div>
+ </div>
+</body>
+</html>
diff --git a/devtools/client/inspector/rules/test/doc_keyframeanimation.css b/devtools/client/inspector/rules/test/doc_keyframeanimation.css
new file mode 100644
index 000000000..64582ed35
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_keyframeanimation.css
@@ -0,0 +1,84 @@
+/* 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/. */
+
+.box {
+ height: 50px;
+ width: 50px;
+}
+
+.circle {
+ width: 20px;
+ height: 20px;
+ border-radius: 10px;
+ background-color: #FFCB01;
+}
+
+#pacman {
+ width: 0px;
+ height: 0px;
+ border-right: 60px solid transparent;
+ border-top: 60px solid #FFCB01;
+ border-left: 60px solid #FFCB01;
+ border-bottom: 60px solid #FFCB01;
+ border-top-left-radius: 60px;
+ border-bottom-left-radius: 60px;
+ border-top-right-radius: 60px;
+ border-bottom-right-radius: 60px;
+ top: 120px;
+ left: 150px;
+ position: absolute;
+ animation-name: pacman;
+ animation-fill-mode: forwards;
+ animation-timing-function: linear;
+ animation-duration: 15s;
+}
+
+#boxy {
+ top: 170px;
+ left: 450px;
+ position: absolute;
+ animation: 4s linear 0s normal none infinite boxy;
+}
+
+
+#moxy {
+ animation-name: moxy, boxy;
+ animation-delay: 3.5s;
+ animation-duration: 2s;
+ top: 170px;
+ left: 650px;
+ position: absolute;
+}
+
+@-moz-keyframes pacman {
+ 100% {
+ left: 750px;
+ }
+}
+
+@keyframes pacman {
+ 100% {
+ left: 750px;
+ }
+}
+
+@keyframes boxy {
+ 10% {
+ background-color: blue;
+ }
+
+ 20% {
+ background-color: green;
+ }
+
+ 100% {
+ opacity: 0;
+ }
+}
+
+@keyframes moxy {
+ to {
+ opacity: 0;
+ }
+}
diff --git a/devtools/client/inspector/rules/test/doc_keyframeanimation.html b/devtools/client/inspector/rules/test/doc_keyframeanimation.html
new file mode 100644
index 000000000..4e02c32f0
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_keyframeanimation.html
@@ -0,0 +1,13 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<html>
+ <head>
+ <title>test case for keyframes rule in rule-view</title>
+ <link rel="stylesheet" type="text/css" href="doc_keyframeanimation.css"/>
+ </head>
+ <body>
+ <div id="pacman"></div>
+ <div id="boxy" class="circle"></div>
+ <div id="moxy" class="circle"></div>
+ </body>
+</html>
diff --git a/devtools/client/inspector/rules/test/doc_media_queries.html b/devtools/client/inspector/rules/test/doc_media_queries.html
new file mode 100644
index 000000000..1adb8bc7a
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_media_queries.html
@@ -0,0 +1,24 @@
+<html>
+<head>
+ <title>test</title>
+ <script type="application/javascript;version=1.7">
+
+ </script>
+ <style>
+ div {
+ width: 1000px;
+ height: 100px;
+ background-color: #f00;
+ }
+
+ @media screen and (min-width: 1px) {
+ div {
+ width: 200px;
+ }
+ }
+ </style>
+</head>
+<body>
+<div></div>
+</body>
+</html>
diff --git a/devtools/client/inspector/rules/test/doc_pseudoelement.html b/devtools/client/inspector/rules/test/doc_pseudoelement.html
new file mode 100644
index 000000000..6145d4bf1
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_pseudoelement.html
@@ -0,0 +1,131 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<html>
+ <head>
+ <style>
+
+body {
+ color: #333;
+}
+
+.box {
+ float:left;
+ width: 128px;
+ height: 128px;
+ background: #ddd;
+ padding: 32px;
+ margin: 32px;
+ position:relative;
+}
+
+.box:first-line {
+ color: orange;
+ background: red;
+}
+
+.box:first-letter {
+ color: green;
+}
+
+* {
+ cursor: default;
+}
+
+nothing {
+ cursor: pointer;
+}
+
+p::-moz-selection {
+ color: white;
+ background: black;
+}
+p::selection {
+ color: white;
+ background: black;
+}
+
+p:first-line {
+ background: blue;
+}
+p:first-letter {
+ color: red;
+ font-size: 130%;
+}
+
+.box:before {
+ background: green;
+ content: " ";
+ position: absolute;
+ height:32px;
+ width:32px;
+}
+
+.box:after {
+ background: red;
+ content: " ";
+ position: absolute;
+ border-radius: 50%;
+ height:32px;
+ width:32px;
+ top: 50%;
+ left: 50%;
+ margin-top: -16px;
+ margin-left: -16px;
+}
+
+.topleft:before {
+ top:0;
+ left:0;
+}
+
+.topleft:first-line {
+ color: orange;
+}
+.topleft::selection {
+ color: orange;
+}
+
+.topright:before {
+ top:0;
+ right:0;
+}
+
+.bottomright:before {
+ bottom:10px;
+ right:10px;
+ color: red;
+}
+
+.bottomright:before {
+ bottom:0;
+ right:0;
+}
+
+.bottomleft:before {
+ bottom:0;
+ left:0;
+}
+
+ </style>
+ </head>
+ <body>
+ <h1>ruleview pseudoelement($("test"));</h1>
+
+ <div id="topleft" class="box topleft">
+ <p>Top Left<br />Position</p>
+ </div>
+
+ <div id="topright" class="box topright">
+ <p>Top Right<br />Position</p>
+ </div>
+
+ <div id="bottomright" class="box bottomright">
+ <p>Bottom Right<br />Position</p>
+ </div>
+
+ <div id="bottomleft" class="box bottomleft">
+ <p>Bottom Left<br />Position</p>
+ </div>
+
+ </body>
+</html>
diff --git a/devtools/client/inspector/rules/test/doc_ruleLineNumbers.html b/devtools/client/inspector/rules/test/doc_ruleLineNumbers.html
new file mode 100644
index 000000000..5a157f384
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_ruleLineNumbers.html
@@ -0,0 +1,19 @@
+<!doctype html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>simple testcase</title>
+ <style type="text/css">
+ #testid {
+ background-color: seagreen;
+ }
+
+ body {
+ color: chartreuse;
+ }
+ </style>
+</head>
+<body>
+ <div id="testid">simple testcase</div>
+</body>
+</html>
diff --git a/devtools/client/inspector/rules/test/doc_sourcemaps.css b/devtools/client/inspector/rules/test/doc_sourcemaps.css
new file mode 100644
index 000000000..a9b437a40
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_sourcemaps.css
@@ -0,0 +1,7 @@
+div {
+ color: #ff0066; }
+
+span {
+ background-color: #EEE; }
+
+/*# sourceMappingURL=doc_sourcemaps.css.map */ \ No newline at end of file
diff --git a/devtools/client/inspector/rules/test/doc_sourcemaps.css.map b/devtools/client/inspector/rules/test/doc_sourcemaps.css.map
new file mode 100644
index 000000000..0f7486fd9
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_sourcemaps.css.map
@@ -0,0 +1,7 @@
+{
+"version": 3,
+"mappings": "AAGA,GAAI;EACF,KAAK,EAHU,OAAI;;AAMrB,IAAK;EACH,gBAAgB,EAAE,IAAI",
+"sources": ["doc_sourcemaps.scss"],
+"names": [],
+"file": "doc_sourcemaps.css"
+}
diff --git a/devtools/client/inspector/rules/test/doc_sourcemaps.html b/devtools/client/inspector/rules/test/doc_sourcemaps.html
new file mode 100644
index 000000000..0014e55fe
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_sourcemaps.html
@@ -0,0 +1,11 @@
+<!doctype html>
+<html>
+<head>
+ <title>testcase for testing CSS source maps</title>
+ <link rel="stylesheet" type="text/css" href="simple.css"/>
+ <link rel="stylesheet" type="text/css" href="doc_sourcemaps.css"/>
+</head>
+<body>
+ <div>source maps <span>testcase</span></div>
+</body>
+</html>
diff --git a/devtools/client/inspector/rules/test/doc_sourcemaps.scss b/devtools/client/inspector/rules/test/doc_sourcemaps.scss
new file mode 100644
index 000000000..0ff6c471b
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_sourcemaps.scss
@@ -0,0 +1,10 @@
+
+$paulrougetpink: #f06;
+
+div {
+ color: $paulrougetpink;
+}
+
+span {
+ background-color: #EEE;
+} \ No newline at end of file
diff --git a/devtools/client/inspector/rules/test/doc_style_editor_link.css b/devtools/client/inspector/rules/test/doc_style_editor_link.css
new file mode 100644
index 000000000..e49e1f587
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_style_editor_link.css
@@ -0,0 +1,3 @@
+div {
+ opacity: 1;
+} \ No newline at end of file
diff --git a/devtools/client/inspector/rules/test/doc_test_image.png b/devtools/client/inspector/rules/test/doc_test_image.png
new file mode 100644
index 000000000..769c63634
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_test_image.png
Binary files differ
diff --git a/devtools/client/inspector/rules/test/doc_urls_clickable.css b/devtools/client/inspector/rules/test/doc_urls_clickable.css
new file mode 100644
index 000000000..04315b2c3
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_urls_clickable.css
@@ -0,0 +1,9 @@
+.relative1 {
+ background-image: url(./doc_test_image.png);
+}
+.absolute {
+ background: url("http://example.com/browser/devtools/client/inspector/rules/test/doc_test_image.png");
+}
+.base64 {
+ background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==');
+}
diff --git a/devtools/client/inspector/rules/test/doc_urls_clickable.html b/devtools/client/inspector/rules/test/doc_urls_clickable.html
new file mode 100644
index 000000000..b0265a703
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_urls_clickable.html
@@ -0,0 +1,30 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<html>
+ <head>
+
+ <link href="./doc_urls_clickable.css" rel="stylesheet" type="text/css">
+
+ <style>
+ .relative2 {
+ background-image: url(doc_test_image.png);
+ }
+ </style>
+ </head>
+ <body>
+
+ <div class="relative1">Background image #1 with relative path (loaded from external css)</div>
+
+ <div class="relative2">Background image #2 with relative path (loaded from style tag)</div>
+
+ <div class="absolute">Background image with absolute path (loaded from external css)</div>
+
+ <div class="base64">Background image with base64 url (loaded from external css)</div>
+
+ <div class="inline" style="background: url(doc_test_image.png);">Background image with relative path (loaded from style attribute)</div>
+
+ <div class="inline-resolved" style="background-image: url(./doc_test_image.png)">Background image with resolved relative path (loaded from style attribute)</div>
+
+ <div class="noimage">No background image :(</div>
+ </body>
+</html>
diff --git a/devtools/client/inspector/rules/test/head.js b/devtools/client/inspector/rules/test/head.js
new file mode 100644
index 000000000..5e5ede09b
--- /dev/null
+++ b/devtools/client/inspector/rules/test/head.js
@@ -0,0 +1,840 @@
+/* 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/ */
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+/* import-globals-from ../../test/head.js */
+"use strict";
+
+// Import the inspector's head.js first (which itself imports shared-head.js).
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/inspector/test/head.js",
+ this);
+
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("devtools.defaultColorUnit");
+});
+
+var {getInplaceEditorForSpan: inplaceEditor} =
+ require("devtools/client/shared/inplace-editor");
+
+const ROOT_TEST_DIR = getRootDirectory(gTestPath);
+const FRAME_SCRIPT_URL = ROOT_TEST_DIR + "doc_frame_script.js";
+
+const STYLE_INSPECTOR_L10N
+ = new LocalizationHelper("devtools/shared/locales/styleinspector.properties");
+
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("devtools.defaultColorUnit");
+});
+
+/**
+ * The rule-view tests rely on a frame-script to be injected in the content test
+ * page. So override the shared-head's addTab to load the frame script after the
+ * tab was added.
+ * FIXME: Refactor the rule-view tests to use the testActor instead of a frame
+ * script, so they can run on remote targets too.
+ */
+var _addTab = addTab;
+addTab = function (url) {
+ return _addTab(url).then(tab => {
+ info("Loading the helper frame script " + FRAME_SCRIPT_URL);
+ let browser = tab.linkedBrowser;
+ browser.messageManager.loadFrameScript(FRAME_SCRIPT_URL, false);
+ return tab;
+ });
+};
+
+/**
+ * Wait for a content -> chrome message on the message manager (the window
+ * messagemanager is used).
+ *
+ * @param {String} name
+ * The message name
+ * @return {Promise} A promise that resolves to the response data when the
+ * message has been received
+ */
+function waitForContentMessage(name) {
+ info("Expecting message " + name + " from content");
+
+ let mm = gBrowser.selectedBrowser.messageManager;
+
+ let def = defer();
+ mm.addMessageListener(name, function onMessage(msg) {
+ mm.removeMessageListener(name, onMessage);
+ def.resolve(msg.data);
+ });
+ return def.promise;
+}
+
+/**
+ * Send an async message to the frame script (chrome -> content) and wait for a
+ * response message with the same name (content -> chrome).
+ *
+ * @param {String} name
+ * The message name. Should be one of the messages defined
+ * in doc_frame_script.js
+ * @param {Object} data
+ * Optional data to send along
+ * @param {Object} objects
+ * Optional CPOW objects to send along
+ * @param {Boolean} expectResponse
+ * If set to false, don't wait for a response with the same name
+ * from the content script. Defaults to true.
+ * @return {Promise} Resolves to the response data if a response is expected,
+ * immediately resolves otherwise
+ */
+function executeInContent(name, data = {}, objects = {},
+ expectResponse = true) {
+ info("Sending message " + name + " to content");
+ let mm = gBrowser.selectedBrowser.messageManager;
+
+ mm.sendAsyncMessage(name, data, objects);
+ if (expectResponse) {
+ return waitForContentMessage(name);
+ }
+
+ return promise.resolve();
+}
+
+/**
+ * Send an async message to the frame script and get back the requested
+ * computed style property.
+ *
+ * @param {String} selector
+ * The selector used to obtain the element.
+ * @param {String} pseudo
+ * pseudo id to query, or null.
+ * @param {String} name
+ * name of the property.
+ */
+function* getComputedStyleProperty(selector, pseudo, propName) {
+ return yield executeInContent("Test:GetComputedStylePropertyValue",
+ {selector,
+ pseudo,
+ name: propName});
+}
+
+/**
+ * Get an element's inline style property value.
+ * @param {TestActor} testActor
+ * @param {String} selector
+ * The selector used to obtain the element.
+ * @param {String} name
+ * name of the property.
+ */
+function getStyle(testActor, selector, propName) {
+ return testActor.eval(`
+ content.document.querySelector("${selector}")
+ .style.getPropertyValue("${propName}");
+ `);
+}
+
+/**
+ * Send an async message to the frame script and wait until the requested
+ * computed style property has the expected value.
+ *
+ * @param {String} selector
+ * The selector used to obtain the element.
+ * @param {String} pseudo
+ * pseudo id to query, or null.
+ * @param {String} prop
+ * name of the property.
+ * @param {String} expected
+ * expected value of property
+ * @param {String} name
+ * the name used in test message
+ */
+function* waitForComputedStyleProperty(selector, pseudo, name, expected) {
+ return yield executeInContent("Test:WaitForComputedStylePropertyValue",
+ {selector,
+ pseudo,
+ expected,
+ name});
+}
+
+/**
+ * Given an inplace editable element, click to switch it to edit mode, wait for
+ * focus
+ *
+ * @return a promise that resolves to the inplace-editor element when ready
+ */
+var focusEditableField = Task.async(function* (ruleView, editable, xOffset = 1,
+ yOffset = 1, options = {}) {
+ let onFocus = once(editable.parentNode, "focus", true);
+ info("Clicking on editable field to turn to edit mode");
+ EventUtils.synthesizeMouse(editable, xOffset, yOffset, options,
+ editable.ownerDocument.defaultView);
+ yield onFocus;
+
+ info("Editable field gained focus, returning the input field now");
+ let onEdit = inplaceEditor(editable.ownerDocument.activeElement);
+
+ return onEdit;
+});
+
+/**
+ * When a tooltip is closed, this ends up "commiting" the value changed within
+ * the tooltip (e.g. the color in case of a colorpicker) which, in turn, ends up
+ * setting the value of the corresponding css property in the rule-view.
+ * Use this function to close the tooltip and make sure the test waits for the
+ * ruleview-changed event.
+ * @param {SwatchBasedEditorTooltip} editorTooltip
+ * @param {CSSRuleView} view
+ */
+function* hideTooltipAndWaitForRuleViewChanged(editorTooltip, view) {
+ let onModified = view.once("ruleview-changed");
+ let onHidden = editorTooltip.tooltip.once("hidden");
+ editorTooltip.hide();
+ yield onModified;
+ yield onHidden;
+}
+
+/**
+ * Polls a given generator function waiting for it to return true.
+ *
+ * @param {Function} validatorFn
+ * A validator generator function that returns a boolean.
+ * This is called every few milliseconds to check if the result is true.
+ * When it is true, the promise resolves.
+ * @param {String} name
+ * Optional name of the test. This is used to generate
+ * the success and failure messages.
+ * @return a promise that resolves when the function returned true or rejects
+ * if the timeout is reached
+ */
+var waitForSuccess = Task.async(function* (validatorFn, desc = "untitled") {
+ let i = 0;
+ while (true) {
+ info("Checking: " + desc);
+ if (yield validatorFn()) {
+ ok(true, "Success: " + desc);
+ break;
+ }
+ i++;
+ if (i > 10) {
+ ok(false, "Failure: " + desc);
+ break;
+ }
+ yield new Promise(r => setTimeout(r, 200));
+ }
+});
+
+/**
+ * Get the DOMNode for a css rule in the rule-view that corresponds to the given
+ * selector
+ *
+ * @param {CssRuleView} view
+ * The instance of the rule-view panel
+ * @param {String} selectorText
+ * The selector in the rule-view for which the rule
+ * object is wanted
+ * @return {DOMNode}
+ */
+function getRuleViewRule(view, selectorText) {
+ let rule;
+ for (let r of view.styleDocument.querySelectorAll(".ruleview-rule")) {
+ let selector = r.querySelector(".ruleview-selectorcontainer, " +
+ ".ruleview-selector-matched");
+ if (selector && selector.textContent === selectorText) {
+ rule = r;
+ break;
+ }
+ }
+
+ return rule;
+}
+
+/**
+ * Get references to the name and value span nodes corresponding to a given
+ * selector and property name in the rule-view
+ *
+ * @param {CssRuleView} view
+ * The instance of the rule-view panel
+ * @param {String} selectorText
+ * The selector in the rule-view to look for the property in
+ * @param {String} propertyName
+ * The name of the property
+ * @return {Object} An object like {nameSpan: DOMNode, valueSpan: DOMNode}
+ */
+function getRuleViewProperty(view, selectorText, propertyName) {
+ let prop;
+
+ let rule = getRuleViewRule(view, selectorText);
+ if (rule) {
+ // Look for the propertyName in that rule element
+ for (let p of rule.querySelectorAll(".ruleview-property")) {
+ let nameSpan = p.querySelector(".ruleview-propertyname");
+ let valueSpan = p.querySelector(".ruleview-propertyvalue");
+
+ if (nameSpan.textContent === propertyName) {
+ prop = {nameSpan: nameSpan, valueSpan: valueSpan};
+ break;
+ }
+ }
+ }
+ return prop;
+}
+
+/**
+ * Get the text value of the property corresponding to a given selector and name
+ * in the rule-view
+ *
+ * @param {CssRuleView} view
+ * The instance of the rule-view panel
+ * @param {String} selectorText
+ * The selector in the rule-view to look for the property in
+ * @param {String} propertyName
+ * The name of the property
+ * @return {String} The property value
+ */
+function getRuleViewPropertyValue(view, selectorText, propertyName) {
+ return getRuleViewProperty(view, selectorText, propertyName)
+ .valueSpan.textContent;
+}
+
+/**
+ * Get a reference to the selector DOM element corresponding to a given selector
+ * in the rule-view
+ *
+ * @param {CssRuleView} view
+ * The instance of the rule-view panel
+ * @param {String} selectorText
+ * The selector in the rule-view to look for
+ * @return {DOMNode} The selector DOM element
+ */
+function getRuleViewSelector(view, selectorText) {
+ let rule = getRuleViewRule(view, selectorText);
+ return rule.querySelector(".ruleview-selector, .ruleview-selector-matched");
+}
+
+/**
+ * Get a reference to the selectorhighlighter icon DOM element corresponding to
+ * a given selector in the rule-view
+ *
+ * @param {CssRuleView} view
+ * The instance of the rule-view panel
+ * @param {String} selectorText
+ * The selector in the rule-view to look for
+ * @return {DOMNode} The selectorhighlighter icon DOM element
+ */
+function getRuleViewSelectorHighlighterIcon(view, selectorText) {
+ let rule = getRuleViewRule(view, selectorText);
+ return rule.querySelector(".ruleview-selectorhighlighter");
+}
+
+/**
+ * Simulate a color change in a given color picker tooltip, and optionally wait
+ * for a given element in the page to have its style changed as a result.
+ * Note that this function assumes that the colorpicker popup is already open
+ * and it won't close it after having selected the new color.
+ *
+ * @param {RuleView} ruleView
+ * The related rule view instance
+ * @param {SwatchColorPickerTooltip} colorPicker
+ * @param {Array} newRgba
+ * The new color to be set [r, g, b, a]
+ * @param {Object} expectedChange
+ * Optional object that needs the following props:
+ * - {String} selector The selector to the element in the page that
+ * will have its style changed.
+ * - {String} name The style name that will be changed
+ * - {String} value The expected style value
+ * The style will be checked like so: getComputedStyle(element)[name] === value
+ */
+var simulateColorPickerChange = Task.async(function* (ruleView, colorPicker,
+ newRgba, expectedChange) {
+ let onComputedStyleChanged;
+ if (expectedChange) {
+ let {selector, name, value} = expectedChange;
+ onComputedStyleChanged = waitForComputedStyleProperty(selector, null, name, value);
+ }
+ let onRuleViewChanged = ruleView.once("ruleview-changed");
+ info("Getting the spectrum colorpicker object");
+ let spectrum = colorPicker.spectrum;
+ info("Setting the new color");
+ spectrum.rgb = newRgba;
+ info("Applying the change");
+ spectrum.updateUI();
+ spectrum.onChange();
+ info("Waiting for rule-view to update");
+ yield onRuleViewChanged;
+
+ if (expectedChange) {
+ info("Waiting for the style to be applied on the page");
+ yield onComputedStyleChanged;
+ }
+});
+
+/**
+ * Open the color picker popup for a given property in a given rule and
+ * simulate a color change. Optionally wait for a given element in the page to
+ * have its style changed as a result.
+ *
+ * @param {RuleView} view
+ * The related rule view instance
+ * @param {Number} ruleIndex
+ * Which rule to target in the rule view
+ * @param {Number} propIndex
+ * Which property to target in the rule
+ * @param {Array} newRgba
+ * The new color to be set [r, g, b, a]
+ * @param {Object} expectedChange
+ * Optional object that needs the following props:
+ * - {String} selector The selector to the element in the page that
+ * will have its style changed.
+ * - {String} name The style name that will be changed
+ * - {String} value The expected style value
+ * The style will be checked like so: getComputedStyle(element)[name] === value
+ */
+var openColorPickerAndSelectColor = Task.async(function* (view, ruleIndex,
+ propIndex, newRgba, expectedChange) {
+ let ruleEditor = getRuleViewRuleEditor(view, ruleIndex);
+ let propEditor = ruleEditor.rule.textProps[propIndex].editor;
+ let swatch = propEditor.valueSpan.querySelector(".ruleview-colorswatch");
+ let cPicker = view.tooltips.colorPicker;
+
+ info("Opening the colorpicker by clicking the color swatch");
+ let onColorPickerReady = cPicker.once("ready");
+ swatch.click();
+ yield onColorPickerReady;
+
+ yield simulateColorPickerChange(view, cPicker, newRgba, expectedChange);
+
+ return {propEditor, swatch, cPicker};
+});
+
+/**
+ * Open the cubicbezier popup for a given property in a given rule and
+ * simulate a curve change. Optionally wait for a given element in the page to
+ * have its style changed as a result.
+ *
+ * @param {RuleView} view
+ * The related rule view instance
+ * @param {Number} ruleIndex
+ * Which rule to target in the rule view
+ * @param {Number} propIndex
+ * Which property to target in the rule
+ * @param {Array} coords
+ * The new coordinates to be used, e.g. [0.1, 2, 0.9, -1]
+ * @param {Object} expectedChange
+ * Optional object that needs the following props:
+ * - {String} selector The selector to the element in the page that
+ * will have its style changed.
+ * - {String} name The style name that will be changed
+ * - {String} value The expected style value
+ * The style will be checked like so: getComputedStyle(element)[name] === value
+ */
+var openCubicBezierAndChangeCoords = Task.async(function* (view, ruleIndex,
+ propIndex, coords, expectedChange) {
+ let ruleEditor = getRuleViewRuleEditor(view, ruleIndex);
+ let propEditor = ruleEditor.rule.textProps[propIndex].editor;
+ let swatch = propEditor.valueSpan.querySelector(".ruleview-bezierswatch");
+ let bezierTooltip = view.tooltips.cubicBezier;
+
+ info("Opening the cubicBezier by clicking the swatch");
+ let onBezierWidgetReady = bezierTooltip.once("ready");
+ swatch.click();
+ yield onBezierWidgetReady;
+
+ let widget = yield bezierTooltip.widget;
+
+ info("Simulating a change of curve in the widget");
+ let onRuleViewChanged = view.once("ruleview-changed");
+ widget.coordinates = coords;
+ yield onRuleViewChanged;
+
+ if (expectedChange) {
+ info("Waiting for the style to be applied on the page");
+ let {selector, name, value} = expectedChange;
+ yield waitForComputedStyleProperty(selector, null, name, value);
+ }
+
+ return {propEditor, swatch, bezierTooltip};
+});
+
+/**
+ * Get a rule-link from the rule-view given its index
+ *
+ * @param {CssRuleView} view
+ * The instance of the rule-view panel
+ * @param {Number} index
+ * The index of the link to get
+ * @return {DOMNode} The link if any at this index
+ */
+function getRuleViewLinkByIndex(view, index) {
+ let links = view.styleDocument.querySelectorAll(".ruleview-rule-source");
+ return links[index];
+}
+
+/**
+ * Get rule-link text from the rule-view given its index
+ *
+ * @param {CssRuleView} view
+ * The instance of the rule-view panel
+ * @param {Number} index
+ * The index of the link to get
+ * @return {String} The string at this index
+ */
+function getRuleViewLinkTextByIndex(view, index) {
+ let link = getRuleViewLinkByIndex(view, index);
+ return link.querySelector(".ruleview-rule-source-label").textContent;
+}
+
+/**
+ * Simulate adding a new property in an existing rule in the rule-view.
+ *
+ * @param {CssRuleView} view
+ * The instance of the rule-view panel
+ * @param {Number} ruleIndex
+ * The index of the rule to use. Note that if ruleIndex is 0, you might
+ * want to also listen to markupmutation events in your test since
+ * that's going to change the style attribute of the selected node.
+ * @param {String} name
+ * The name for the new property
+ * @param {String} value
+ * The value for the new property
+ * @param {String} commitValueWith
+ * Which key should be used to commit the new value. VK_RETURN is used by
+ * default, but tests might want to use another key to test cancelling
+ * for exemple.
+ * @param {Boolean} blurNewProperty
+ * After the new value has been added, a new property would have been
+ * focused. This parameter is true by default, and that causes the new
+ * property to be blurred. Set to false if you don't want this.
+ * @return {TextProperty} The instance of the TextProperty that was added
+ */
+var addProperty = Task.async(function* (view, ruleIndex, name, value,
+ commitValueWith = "VK_RETURN",
+ blurNewProperty = true) {
+ info("Adding new property " + name + ":" + value + " to rule " + ruleIndex);
+
+ let ruleEditor = getRuleViewRuleEditor(view, ruleIndex);
+ let editor = yield focusNewRuleViewProperty(ruleEditor);
+ let numOfProps = ruleEditor.rule.textProps.length;
+
+ info("Adding name " + name);
+ editor.input.value = name;
+ let onNameAdded = view.once("ruleview-changed");
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
+ yield onNameAdded;
+
+ // Focus has moved to the value inplace-editor automatically.
+ editor = inplaceEditor(view.styleDocument.activeElement);
+ let textProps = ruleEditor.rule.textProps;
+ let textProp = textProps[textProps.length - 1];
+
+ is(ruleEditor.rule.textProps.length, numOfProps + 1,
+ "A new test property was added");
+ is(editor, inplaceEditor(textProp.editor.valueSpan),
+ "The inplace editor appeared for the value");
+
+ info("Adding value " + value);
+ // Setting the input value schedules a preview to be shown in 10ms which
+ // triggers a ruleview-changed event (see bug 1209295).
+ let onPreview = view.once("ruleview-changed");
+ editor.input.value = value;
+ view.throttle.flush();
+ yield onPreview;
+
+ let onValueAdded = view.once("ruleview-changed");
+ EventUtils.synthesizeKey(commitValueWith, {}, view.styleWindow);
+ yield onValueAdded;
+
+ if (blurNewProperty) {
+ view.styleDocument.activeElement.blur();
+ }
+
+ return textProp;
+});
+
+/**
+ * Simulate changing the value of a property in a rule in the rule-view.
+ *
+ * @param {CssRuleView} view
+ * The instance of the rule-view panel
+ * @param {TextProperty} textProp
+ * The instance of the TextProperty to be changed
+ * @param {String} value
+ * The new value to be used. If null is passed, then the value will be
+ * deleted
+ * @param {Boolean} blurNewProperty
+ * After the value has been changed, a new property would have been
+ * focused. This parameter is true by default, and that causes the new
+ * property to be blurred. Set to false if you don't want this.
+ */
+var setProperty = Task.async(function* (view, textProp, value,
+ blurNewProperty = true) {
+ yield focusEditableField(view, textProp.editor.valueSpan);
+
+ let onPreview = view.once("ruleview-changed");
+ if (value === null) {
+ EventUtils.synthesizeKey("VK_DELETE", {}, view.styleWindow);
+ } else {
+ EventUtils.sendString(value, view.styleWindow);
+ }
+ view.throttle.flush();
+ yield onPreview;
+
+ let onValueDone = view.once("ruleview-changed");
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
+ yield onValueDone;
+
+ if (blurNewProperty) {
+ view.styleDocument.activeElement.blur();
+ }
+});
+
+/**
+ * Simulate removing a property from an existing rule in the rule-view.
+ *
+ * @param {CssRuleView} view
+ * The instance of the rule-view panel
+ * @param {TextProperty} textProp
+ * The instance of the TextProperty to be removed
+ * @param {Boolean} blurNewProperty
+ * After the property has been removed, a new property would have been
+ * focused. This parameter is true by default, and that causes the new
+ * property to be blurred. Set to false if you don't want this.
+ */
+var removeProperty = Task.async(function* (view, textProp,
+ blurNewProperty = true) {
+ yield focusEditableField(view, textProp.editor.nameSpan);
+
+ let onModifications = view.once("ruleview-changed");
+ info("Deleting the property name now");
+ EventUtils.synthesizeKey("VK_DELETE", {}, view.styleWindow);
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
+ yield onModifications;
+
+ if (blurNewProperty) {
+ view.styleDocument.activeElement.blur();
+ }
+});
+
+/**
+ * Simulate clicking the enable/disable checkbox next to a property in a rule.
+ *
+ * @param {CssRuleView} view
+ * The instance of the rule-view panel
+ * @param {TextProperty} textProp
+ * The instance of the TextProperty to be enabled/disabled
+ */
+var togglePropStatus = Task.async(function* (view, textProp) {
+ let onRuleViewRefreshed = view.once("ruleview-changed");
+ textProp.editor.enable.click();
+ yield onRuleViewRefreshed;
+});
+
+/**
+ * Click on a rule-view's close brace to focus a new property name editor
+ *
+ * @param {RuleEditor} ruleEditor
+ * An instance of RuleEditor that will receive the new property
+ * @return a promise that resolves to the newly created editor when ready and
+ * focused
+ */
+var focusNewRuleViewProperty = Task.async(function* (ruleEditor) {
+ info("Clicking on a close ruleEditor brace to start editing a new property");
+
+ // Use bottom alignment to avoid scrolling out of the parent element area.
+ ruleEditor.closeBrace.scrollIntoView(false);
+ let editor = yield focusEditableField(ruleEditor.ruleView,
+ ruleEditor.closeBrace);
+
+ is(inplaceEditor(ruleEditor.newPropSpan), editor,
+ "Focused editor is the new property editor.");
+
+ return editor;
+});
+
+/**
+ * Create a new property name in the rule-view, focusing a new property editor
+ * by clicking on the close brace, and then entering the given text.
+ * Keep in mind that the rule-view knows how to handle strings with multiple
+ * properties, so the input text may be like: "p1:v1;p2:v2;p3:v3".
+ *
+ * @param {RuleEditor} ruleEditor
+ * The instance of RuleEditor that will receive the new property(ies)
+ * @param {String} inputValue
+ * The text to be entered in the new property name field
+ * @return a promise that resolves when the new property name has been entered
+ * and once the value field is focused
+ */
+var createNewRuleViewProperty = Task.async(function* (ruleEditor, inputValue) {
+ info("Creating a new property editor");
+ let editor = yield focusNewRuleViewProperty(ruleEditor);
+
+ info("Entering the value " + inputValue);
+ editor.input.value = inputValue;
+
+ info("Submitting the new value and waiting for value field focus");
+ let onFocus = once(ruleEditor.element, "focus", true);
+ EventUtils.synthesizeKey("VK_RETURN", {},
+ ruleEditor.element.ownerDocument.defaultView);
+ yield onFocus;
+});
+
+/**
+ * Set the search value for the rule-view filter styles search box.
+ *
+ * @param {CssRuleView} view
+ * The instance of the rule-view panel
+ * @param {String} searchValue
+ * The filter search value
+ * @return a promise that resolves when the rule-view is filtered for the
+ * search term
+ */
+var setSearchFilter = Task.async(function* (view, searchValue) {
+ info("Setting filter text to \"" + searchValue + "\"");
+ let win = view.styleWindow;
+ let searchField = view.searchField;
+ searchField.focus();
+ synthesizeKeys(searchValue, win);
+ yield view.inspector.once("ruleview-filtered");
+});
+
+/**
+ * Reload the current page and wait for the inspector to be initialized after
+ * the navigation
+ *
+ * @param {InspectorPanel} inspector
+ * The instance of InspectorPanel currently loaded in the toolbox
+ * @param {TestActor} testActor
+ * The current instance of the TestActor
+ */
+function* reloadPage(inspector, testActor) {
+ let onNewRoot = inspector.once("new-root");
+ yield testActor.reload();
+ yield onNewRoot;
+ yield inspector.markup._waitForChildren();
+}
+
+/**
+ * Create a new rule by clicking on the "add rule" button.
+ * This will leave the selector inplace-editor active.
+ *
+ * @param {InspectorPanel} inspector
+ * The instance of InspectorPanel currently loaded in the toolbox
+ * @param {CssRuleView} view
+ * The instance of the rule-view panel
+ * @return a promise that resolves after the rule has been added
+ */
+function* addNewRule(inspector, view) {
+ info("Adding the new rule using the button");
+ view.addRuleButton.click();
+
+ info("Waiting for rule view to change");
+ yield view.once("ruleview-changed");
+}
+
+/**
+ * Create a new rule by clicking on the "add rule" button, dismiss the editor field and
+ * verify that the selector is correct.
+ *
+ * @param {InspectorPanel} inspector
+ * The instance of InspectorPanel currently loaded in the toolbox
+ * @param {CssRuleView} view
+ * The instance of the rule-view panel
+ * @param {String} expectedSelector
+ * The value we expect the selector to have
+ * @param {Number} expectedIndex
+ * The index we expect the rule to have in the rule-view
+ * @return a promise that resolves after the rule has been added
+ */
+function* addNewRuleAndDismissEditor(inspector, view, expectedSelector, expectedIndex) {
+ yield addNewRule(inspector, view);
+
+ info("Getting the new rule at index " + expectedIndex);
+ let ruleEditor = getRuleViewRuleEditor(view, expectedIndex);
+ let editor = ruleEditor.selectorText.ownerDocument.activeElement;
+ is(editor.value, expectedSelector,
+ "The editor for the new selector has the correct value: " + expectedSelector);
+
+ info("Pressing escape to leave the editor");
+ EventUtils.synthesizeKey("VK_ESCAPE", {});
+
+ is(ruleEditor.selectorText.textContent, expectedSelector,
+ "The new selector has the correct text: " + expectedSelector);
+}
+
+/**
+ * Simulate a sequence of non-character keys (return, escape, tab) and wait for
+ * a given element to receive the focus.
+ *
+ * @param {CssRuleView} view
+ * The instance of the rule-view panel
+ * @param {DOMNode} element
+ * The element that should be focused
+ * @param {Array} keys
+ * Array of non-character keys, the part that comes after "DOM_VK_" eg.
+ * "RETURN", "ESCAPE"
+ * @return a promise that resolves after the element received the focus
+ */
+function* sendKeysAndWaitForFocus(view, element, keys) {
+ let onFocus = once(element, "focus", true);
+ for (let key of keys) {
+ EventUtils.sendKey(key, view.styleWindow);
+ }
+ yield onFocus;
+}
+
+/**
+ * Open the style editor context menu and return all of it's items in a flat array
+ * @param {CssRuleView} view
+ * The instance of the rule-view panel
+ * @return An array of MenuItems
+ */
+function openStyleContextMenuAndGetAllItems(view, target) {
+ let menu = view._contextmenu._openMenu({target: target});
+
+ // Flatten all menu items into a single array to make searching through it easier
+ let allItems = [].concat.apply([], menu.items.map(function addItem(item) {
+ if (item.submenu) {
+ return addItem(item.submenu.items);
+ }
+ return item;
+ }));
+
+ return allItems;
+}
+
+/**
+ * Wait for a markupmutation event on the inspector that is for a style modification.
+ * @param {InspectorPanel} inspector
+ * @return {Promise}
+ */
+function waitForStyleModification(inspector) {
+ return new Promise(function (resolve) {
+ function checkForStyleModification(name, mutations) {
+ for (let mutation of mutations) {
+ if (mutation.type === "attributes" && mutation.attributeName === "style") {
+ inspector.off("markupmutation", checkForStyleModification);
+ resolve();
+ return;
+ }
+ }
+ }
+ inspector.on("markupmutation", checkForStyleModification);
+ });
+}
+
+/**
+ * Click on the selector icon
+ * @param {DOMNode} icon
+ * @param {CSSRuleView} view
+ */
+function* clickSelectorIcon(icon, view) {
+ let onToggled = view.once("ruleview-selectorhighlighter-toggled");
+ EventUtils.synthesizeMouseAtCenter(icon, {}, view.styleWindow);
+ yield onToggled;
+}
+
+/**
+ * Make sure window is properly focused before sending a key event.
+ * @param {Window} win
+ * @param {Event} key
+ */
+function focusAndSendKey(win, key) {
+ win.document.documentElement.focus();
+ EventUtils.sendKey(key, win);
+}
diff --git a/devtools/client/inspector/rules/views/moz.build b/devtools/client/inspector/rules/views/moz.build
new file mode 100644
index 000000000..ac0a24d76
--- /dev/null
+++ b/devtools/client/inspector/rules/views/moz.build
@@ -0,0 +1,8 @@
+# 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/.
+
+DevToolsModules(
+ 'rule-editor.js',
+ 'text-property-editor.js',
+)
diff --git a/devtools/client/inspector/rules/views/rule-editor.js b/devtools/client/inspector/rules/views/rule-editor.js
new file mode 100644
index 000000000..2587bf19c
--- /dev/null
+++ b/devtools/client/inspector/rules/views/rule-editor.js
@@ -0,0 +1,620 @@
+/* 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 {l10n} = require("devtools/shared/inspector/css-logic");
+const {ELEMENT_STYLE} = require("devtools/shared/specs/styles");
+const {PREF_ORIG_SOURCES} = require("devtools/client/styleeditor/utils");
+const {Rule} = require("devtools/client/inspector/rules/models/rule");
+const {InplaceEditor, editableField, editableItem} =
+ require("devtools/client/shared/inplace-editor");
+const {TextPropertyEditor} =
+ require("devtools/client/inspector/rules/views/text-property-editor");
+const {
+ createChild,
+ blurOnMultipleProperties,
+ promiseWarn
+} = require("devtools/client/inspector/shared/utils");
+const {
+ parseDeclarations,
+ parsePseudoClassesAndAttributes,
+ SELECTOR_ATTRIBUTE,
+ SELECTOR_ELEMENT,
+ SELECTOR_PSEUDO_CLASS
+} = require("devtools/shared/css/parsing-utils");
+const promise = require("promise");
+const Services = require("Services");
+const EventEmitter = require("devtools/shared/event-emitter");
+const {Task} = require("devtools/shared/task");
+
+const STYLE_INSPECTOR_PROPERTIES = "devtools/shared/locales/styleinspector.properties";
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const STYLE_INSPECTOR_L10N = new LocalizationHelper(STYLE_INSPECTOR_PROPERTIES);
+
+/**
+ * RuleEditor is responsible for the following:
+ * Owns a Rule object and creates a list of TextPropertyEditors
+ * for its TextProperties.
+ * Manages creation of new text properties.
+ *
+ * One step of a RuleEditor's instantiation is figuring out what's the original
+ * source link to the parent stylesheet (in case of source maps). This step is
+ * asynchronous and is triggered as soon as the RuleEditor is instantiated (see
+ * updateSourceLink). If you need to know when the RuleEditor is done with this,
+ * you need to listen to the source-link-updated event.
+ *
+ * @param {CssRuleView} ruleView
+ * The CssRuleView containg the document holding this rule editor.
+ * @param {Rule} rule
+ * The Rule object we're editing.
+ */
+function RuleEditor(ruleView, rule) {
+ EventEmitter.decorate(this);
+
+ this.ruleView = ruleView;
+ this.doc = this.ruleView.styleDocument;
+ this.toolbox = this.ruleView.inspector.toolbox;
+ this.rule = rule;
+
+ this.isEditable = !rule.isSystem;
+ // Flag that blocks updates of the selector and properties when it is
+ // being edited
+ this.isEditing = false;
+
+ this._onNewProperty = this._onNewProperty.bind(this);
+ this._newPropertyDestroy = this._newPropertyDestroy.bind(this);
+ this._onSelectorDone = this._onSelectorDone.bind(this);
+ this._locationChanged = this._locationChanged.bind(this);
+ this.updateSourceLink = this.updateSourceLink.bind(this);
+
+ this.rule.domRule.on("location-changed", this._locationChanged);
+ this.toolbox.on("tool-registered", this.updateSourceLink);
+ this.toolbox.on("tool-unregistered", this.updateSourceLink);
+
+ this._create();
+}
+
+RuleEditor.prototype = {
+ destroy: function () {
+ this.rule.domRule.off("location-changed");
+ this.toolbox.off("tool-registered", this.updateSourceLink);
+ this.toolbox.off("tool-unregistered", this.updateSourceLink);
+ },
+
+ get isSelectorEditable() {
+ let trait = this.isEditable &&
+ this.ruleView.inspector.target.client.traits.selectorEditable &&
+ this.rule.domRule.type !== ELEMENT_STYLE &&
+ this.rule.domRule.type !== CSSRule.KEYFRAME_RULE;
+
+ // Do not allow editing anonymousselectors until we can
+ // detect mutations on pseudo elements in Bug 1034110.
+ return trait && !this.rule.elementStyle.element.isAnonymous;
+ },
+
+ _create: function () {
+ this.element = this.doc.createElement("div");
+ this.element.className = "ruleview-rule theme-separator";
+ this.element.setAttribute("uneditable", !this.isEditable);
+ this.element.setAttribute("unmatched", this.rule.isUnmatched);
+ this.element._ruleEditor = this;
+
+ // Give a relative position for the inplace editor's measurement
+ // span to be placed absolutely against.
+ this.element.style.position = "relative";
+
+ // Add the source link.
+ this.source = createChild(this.element, "div", {
+ class: "ruleview-rule-source theme-link"
+ });
+ this.source.addEventListener("click", function () {
+ if (this.source.hasAttribute("unselectable")) {
+ return;
+ }
+ let rule = this.rule.domRule;
+ this.ruleView.emit("ruleview-linked-clicked", rule);
+ }.bind(this));
+ let sourceLabel = this.doc.createElement("span");
+ sourceLabel.classList.add("ruleview-rule-source-label");
+ this.source.appendChild(sourceLabel);
+
+ this.updateSourceLink();
+
+ let code = createChild(this.element, "div", {
+ class: "ruleview-code"
+ });
+
+ let header = createChild(code, "div", {});
+
+ this.selectorText = createChild(header, "span", {
+ class: "ruleview-selectorcontainer theme-fg-color3",
+ tabindex: this.isSelectorEditable ? "0" : "-1",
+ });
+
+ if (this.isSelectorEditable) {
+ this.selectorText.addEventListener("click", event => {
+ // Clicks within the selector shouldn't propagate any further.
+ event.stopPropagation();
+ }, false);
+
+ editableField({
+ element: this.selectorText,
+ done: this._onSelectorDone,
+ cssProperties: this.rule.cssProperties,
+ contextMenu: this.ruleView.inspector.onTextBoxContextMenu
+ });
+ }
+
+ if (this.rule.domRule.type !== CSSRule.KEYFRAME_RULE) {
+ let selector = this.rule.domRule.selectors
+ ? this.rule.domRule.selectors.join(", ")
+ : this.ruleView.inspector.selectionCssSelector;
+
+ let selectorHighlighter = createChild(header, "span", {
+ class: "ruleview-selectorhighlighter" +
+ (this.ruleView.highlighters.selectorHighlighterShown === selector ?
+ " highlighted" : ""),
+ title: l10n("rule.selectorHighlighter.tooltip")
+ });
+ selectorHighlighter.addEventListener("click", () => {
+ this.ruleView.toggleSelectorHighlighter(selectorHighlighter, selector);
+ });
+ }
+
+ this.openBrace = createChild(header, "span", {
+ class: "ruleview-ruleopen",
+ textContent: " {"
+ });
+
+ this.propertyList = createChild(code, "ul", {
+ class: "ruleview-propertylist"
+ });
+
+ this.populate();
+
+ this.closeBrace = createChild(code, "div", {
+ class: "ruleview-ruleclose",
+ tabindex: this.isEditable ? "0" : "-1",
+ textContent: "}"
+ });
+
+ if (this.isEditable) {
+ // A newProperty editor should only be created when no editor was
+ // previously displayed. Since the editors are cleared on blur,
+ // check this.ruleview.isEditing on mousedown
+ this._ruleViewIsEditing = false;
+
+ code.addEventListener("mousedown", () => {
+ this._ruleViewIsEditing = this.ruleView.isEditing;
+ });
+
+ code.addEventListener("click", () => {
+ let selection = this.doc.defaultView.getSelection();
+ if (selection.isCollapsed && !this._ruleViewIsEditing) {
+ this.newProperty();
+ }
+ // Cleanup the _ruleViewIsEditing flag
+ this._ruleViewIsEditing = false;
+ }, false);
+
+ this.element.addEventListener("mousedown", () => {
+ this.doc.defaultView.focus();
+ }, false);
+
+ // Create a property editor when the close brace is clicked.
+ editableItem({ element: this.closeBrace }, () => {
+ this.newProperty();
+ });
+ }
+ },
+
+ /**
+ * Event handler called when a property changes on the
+ * StyleRuleActor.
+ */
+ _locationChanged: function () {
+ this.updateSourceLink();
+ },
+
+ updateSourceLink: function () {
+ let sourceLabel = this.element.querySelector(".ruleview-rule-source-label");
+ let title = this.rule.title;
+ let sourceHref = (this.rule.sheet && this.rule.sheet.href) ?
+ this.rule.sheet.href : title;
+ let sourceLine = this.rule.ruleLine > 0 ? ":" + this.rule.ruleLine : "";
+
+ sourceLabel.setAttribute("title", sourceHref + sourceLine);
+
+ if (this.toolbox.isToolRegistered("styleeditor")) {
+ this.source.removeAttribute("unselectable");
+ } else {
+ this.source.setAttribute("unselectable", true);
+ }
+
+ if (this.rule.isSystem) {
+ let uaLabel = STYLE_INSPECTOR_L10N.getStr("rule.userAgentStyles");
+ sourceLabel.textContent = uaLabel + " " + title;
+
+ // Special case about:PreferenceStyleSheet, as it is generated on the
+ // fly and the URI is not registered with the about: handler.
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=935803#c37
+ if (sourceHref === "about:PreferenceStyleSheet") {
+ this.source.setAttribute("unselectable", "true");
+ sourceLabel.textContent = uaLabel;
+ sourceLabel.removeAttribute("title");
+ }
+ } else {
+ sourceLabel.textContent = title;
+ if (this.rule.ruleLine === -1 && this.rule.domRule.parentStyleSheet) {
+ this.source.setAttribute("unselectable", "true");
+ }
+ }
+
+ let showOrig = Services.prefs.getBoolPref(PREF_ORIG_SOURCES);
+ if (showOrig && !this.rule.isSystem &&
+ this.rule.domRule.type !== ELEMENT_STYLE) {
+ // Only get the original source link if the right pref is set, if the rule
+ // isn't a system rule and if it isn't an inline rule.
+ this.rule.getOriginalSourceStrings().then((strings) => {
+ sourceLabel.textContent = strings.short;
+ sourceLabel.setAttribute("title", strings.full);
+ }, e => console.error(e)).then(() => {
+ this.emit("source-link-updated");
+ });
+ } else {
+ // If we're not getting the original source link, then we can emit the
+ // event immediately (but still asynchronously to give consumers a chance
+ // to register it after having instantiated the RuleEditor).
+ promise.resolve().then(() => {
+ this.emit("source-link-updated");
+ });
+ }
+ },
+
+ /**
+ * Update the rule editor with the contents of the rule.
+ */
+ populate: function () {
+ // Clear out existing viewers.
+ while (this.selectorText.hasChildNodes()) {
+ this.selectorText.removeChild(this.selectorText.lastChild);
+ }
+
+ // If selector text comes from a css rule, highlight selectors that
+ // actually match. For custom selector text (such as for the 'element'
+ // style, just show the text directly.
+ if (this.rule.domRule.type === ELEMENT_STYLE) {
+ this.selectorText.textContent = this.rule.selectorText;
+ } else if (this.rule.domRule.type === CSSRule.KEYFRAME_RULE) {
+ this.selectorText.textContent = this.rule.domRule.keyText;
+ } else {
+ this.rule.domRule.selectors.forEach((selector, i) => {
+ if (i !== 0) {
+ createChild(this.selectorText, "span", {
+ class: "ruleview-selector-separator",
+ textContent: ", "
+ });
+ }
+
+ let containerClass =
+ (this.rule.matchedSelectors.indexOf(selector) > -1) ?
+ "ruleview-selector-matched" : "ruleview-selector-unmatched";
+ let selectorContainer = createChild(this.selectorText, "span", {
+ class: containerClass
+ });
+
+ let parsedSelector = parsePseudoClassesAndAttributes(selector);
+
+ for (let selectorText of parsedSelector) {
+ let selectorClass = "";
+
+ switch (selectorText.type) {
+ case SELECTOR_ATTRIBUTE:
+ selectorClass = "ruleview-selector-attribute";
+ break;
+ case SELECTOR_ELEMENT:
+ selectorClass = "ruleview-selector";
+ break;
+ case SELECTOR_PSEUDO_CLASS:
+ selectorClass = [":active", ":focus", ":hover"].some(
+ pseudo => selectorText.value === pseudo) ?
+ "ruleview-selector-pseudo-class-lock" :
+ "ruleview-selector-pseudo-class";
+ break;
+ default:
+ break;
+ }
+
+ createChild(selectorContainer, "span", {
+ textContent: selectorText.value,
+ class: selectorClass
+ });
+ }
+ });
+ }
+
+ for (let prop of this.rule.textProps) {
+ if (!prop.editor && !prop.invisible) {
+ let editor = new TextPropertyEditor(this, prop);
+ this.propertyList.appendChild(editor.element);
+ }
+ }
+ },
+
+ /**
+ * Programatically add a new property to the rule.
+ *
+ * @param {String} name
+ * Property name.
+ * @param {String} value
+ * Property value.
+ * @param {String} priority
+ * Property priority.
+ * @param {Boolean} enabled
+ * True if the property should be enabled.
+ * @param {TextProperty} siblingProp
+ * Optional, property next to which the new property will be added.
+ * @return {TextProperty}
+ * The new property
+ */
+ addProperty: function (name, value, priority, enabled, siblingProp) {
+ let prop = this.rule.createProperty(name, value, priority, enabled,
+ siblingProp);
+ let index = this.rule.textProps.indexOf(prop);
+ let editor = new TextPropertyEditor(this, prop);
+
+ // Insert this node before the DOM node that is currently at its new index
+ // in the property list. There is currently one less node in the DOM than
+ // in the property list, so this causes it to appear after siblingProp.
+ // If there is no node at its index, as is the case where this is the last
+ // node being inserted, then this behaves as appendChild.
+ this.propertyList.insertBefore(editor.element,
+ this.propertyList.children[index]);
+
+ return prop;
+ },
+
+ /**
+ * Programatically add a list of new properties to the rule. Focus the UI
+ * to the proper location after adding (either focus the value on the
+ * last property if it is empty, or create a new property and focus it).
+ *
+ * @param {Array} properties
+ * Array of properties, which are objects with this signature:
+ * {
+ * name: {string},
+ * value: {string},
+ * priority: {string}
+ * }
+ * @param {TextProperty} siblingProp
+ * Optional, the property next to which all new props should be added.
+ */
+ addProperties: function (properties, siblingProp) {
+ if (!properties || !properties.length) {
+ return;
+ }
+
+ let lastProp = siblingProp;
+ for (let p of properties) {
+ let isCommented = Boolean(p.commentOffsets);
+ let enabled = !isCommented;
+ lastProp = this.addProperty(p.name, p.value, p.priority, enabled,
+ lastProp);
+ }
+
+ // Either focus on the last value if incomplete, or start a new one.
+ if (lastProp && lastProp.value.trim() === "") {
+ lastProp.editor.valueSpan.click();
+ } else {
+ this.newProperty();
+ }
+ },
+
+ /**
+ * Create a text input for a property name. If a non-empty property
+ * name is given, we'll create a real TextProperty and add it to the
+ * rule.
+ */
+ newProperty: function () {
+ // If we're already creating a new property, ignore this.
+ if (!this.closeBrace.hasAttribute("tabindex")) {
+ return;
+ }
+
+ // While we're editing a new property, it doesn't make sense to
+ // start a second new property editor, so disable focusing the
+ // close brace for now.
+ this.closeBrace.removeAttribute("tabindex");
+
+ this.newPropItem = createChild(this.propertyList, "li", {
+ class: "ruleview-property ruleview-newproperty",
+ });
+
+ this.newPropSpan = createChild(this.newPropItem, "span", {
+ class: "ruleview-propertyname",
+ tabindex: "0"
+ });
+
+ this.multipleAddedProperties = null;
+
+ this.editor = new InplaceEditor({
+ element: this.newPropSpan,
+ done: this._onNewProperty,
+ destroy: this._newPropertyDestroy,
+ advanceChars: ":",
+ contentType: InplaceEditor.CONTENT_TYPES.CSS_PROPERTY,
+ popup: this.ruleView.popup,
+ cssProperties: this.rule.cssProperties,
+ contextMenu: this.ruleView.inspector.onTextBoxContextMenu
+ });
+
+ // Auto-close the input if multiple rules get pasted into new property.
+ this.editor.input.addEventListener("paste",
+ blurOnMultipleProperties(this.rule.cssProperties), false);
+ },
+
+ /**
+ * Called when the new property input has been dismissed.
+ *
+ * @param {String} value
+ * The value in the editor.
+ * @param {Boolean} commit
+ * True if the value should be committed.
+ */
+ _onNewProperty: function (value, commit) {
+ if (!value || !commit) {
+ return;
+ }
+
+ // parseDeclarations allows for name-less declarations, but in the present
+ // case, we're creating a new declaration, it doesn't make sense to accept
+ // these entries
+ this.multipleAddedProperties =
+ parseDeclarations(this.rule.cssProperties.isKnown, value, true)
+ .filter(d => d.name);
+
+ // Blur the editor field now and deal with adding declarations later when
+ // the field gets destroyed (see _newPropertyDestroy)
+ this.editor.input.blur();
+ },
+
+ /**
+ * Called when the new property editor is destroyed.
+ * This is where the properties (type TextProperty) are actually being
+ * added, since we want to wait until after the inplace editor `destroy`
+ * event has been fired to keep consistent UI state.
+ */
+ _newPropertyDestroy: function () {
+ // We're done, make the close brace focusable again.
+ this.closeBrace.setAttribute("tabindex", "0");
+
+ this.propertyList.removeChild(this.newPropItem);
+ delete this.newPropItem;
+ delete this.newPropSpan;
+
+ // If properties were added, we want to focus the proper element.
+ // If the last new property has no value, focus the value on it.
+ // Otherwise, start a new property and focus that field.
+ if (this.multipleAddedProperties && this.multipleAddedProperties.length) {
+ this.addProperties(this.multipleAddedProperties);
+ }
+ },
+
+ /**
+ * Called when the selector's inplace editor is closed.
+ * Ignores the change if the user pressed escape, otherwise
+ * commits it.
+ *
+ * @param {String} value
+ * The value contained in the editor.
+ * @param {Boolean} commit
+ * True if the change should be applied.
+ * @param {Number} direction
+ * The move focus direction number.
+ */
+ _onSelectorDone: Task.async(function* (value, commit, direction) {
+ if (!commit || this.isEditing || value === "" ||
+ value === this.rule.selectorText) {
+ return;
+ }
+
+ let ruleView = this.ruleView;
+ let elementStyle = ruleView._elementStyle;
+ let element = elementStyle.element;
+ let supportsUnmatchedRules =
+ this.rule.domRule.supportsModifySelectorUnmatched;
+
+ this.isEditing = true;
+
+ try {
+ let response = yield this.rule.domRule.modifySelector(element, value);
+
+ if (!supportsUnmatchedRules) {
+ this.isEditing = false;
+
+ if (response) {
+ this.ruleView.refreshPanel();
+ }
+ return;
+ }
+
+ // We recompute the list of applied styles, because editing a
+ // selector might cause this rule's position to change.
+ let applied = yield elementStyle.pageStyle.getApplied(element, {
+ inherited: true,
+ matchedSelectors: true,
+ filter: elementStyle.showUserAgentStyles ? "ua" : undefined
+ });
+
+ this.isEditing = false;
+
+ let {ruleProps, isMatching} = response;
+ if (!ruleProps) {
+ // Notify for changes, even when nothing changes,
+ // just to allow tests being able to track end of this request.
+ ruleView.emit("ruleview-invalid-selector");
+ return;
+ }
+
+ ruleProps.isUnmatched = !isMatching;
+ let newRule = new Rule(elementStyle, ruleProps);
+ let editor = new RuleEditor(ruleView, newRule);
+ let rules = elementStyle.rules;
+
+ let newRuleIndex = applied.findIndex((r) => r.rule == ruleProps.rule);
+ let oldIndex = rules.indexOf(this.rule);
+
+ // If the selector no longer matches, then we leave the rule in
+ // the same relative position.
+ if (newRuleIndex === -1) {
+ newRuleIndex = oldIndex;
+ }
+
+ // Remove the old rule and insert the new rule.
+ rules.splice(oldIndex, 1);
+ rules.splice(newRuleIndex, 0, newRule);
+ elementStyle._changed();
+ elementStyle.markOverriddenAll();
+
+ // We install the new editor in place of the old -- you might
+ // think we would replicate the list-modification logic above,
+ // but that is complicated due to the way the UI installs
+ // pseudo-element rules and the like.
+ this.element.parentNode.replaceChild(editor.element, this.element);
+
+ // Remove highlight for modified selector
+ if (ruleView.highlighters.selectorHighlighterShown) {
+ ruleView.toggleSelectorHighlighter(ruleView.lastSelectorIcon,
+ ruleView.highlighters.selectorHighlighterShown);
+ }
+
+ editor._moveSelectorFocus(direction);
+ } catch (err) {
+ this.isEditing = false;
+ promiseWarn(err);
+ }
+ }),
+
+ /**
+ * Handle moving the focus change after a tab or return keypress in the
+ * selector inplace editor.
+ *
+ * @param {Number} direction
+ * The move focus direction number.
+ */
+ _moveSelectorFocus: function (direction) {
+ if (!direction || direction === Services.focus.MOVEFOCUS_BACKWARD) {
+ return;
+ }
+
+ if (this.rule.textProps.length > 0) {
+ this.rule.textProps[0].editor.nameSpan.click();
+ } else {
+ this.propertyList.click();
+ }
+ }
+};
+
+exports.RuleEditor = RuleEditor;
diff --git a/devtools/client/inspector/rules/views/text-property-editor.js b/devtools/client/inspector/rules/views/text-property-editor.js
new file mode 100644
index 000000000..d3015f931
--- /dev/null
+++ b/devtools/client/inspector/rules/views/text-property-editor.js
@@ -0,0 +1,880 @@
+/* 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 {l10n} = require("devtools/shared/inspector/css-logic");
+const {getCssProperties} = require("devtools/shared/fronts/css-properties");
+const {InplaceEditor, editableField} =
+ require("devtools/client/shared/inplace-editor");
+const {
+ createChild,
+ appendText,
+ advanceValidate,
+ blurOnMultipleProperties
+} = require("devtools/client/inspector/shared/utils");
+const {
+ parseDeclarations,
+ parseSingleValue,
+} = require("devtools/shared/css/parsing-utils");
+const Services = require("Services");
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+
+const SHARED_SWATCH_CLASS = "ruleview-swatch";
+const COLOR_SWATCH_CLASS = "ruleview-colorswatch";
+const BEZIER_SWATCH_CLASS = "ruleview-bezierswatch";
+const FILTER_SWATCH_CLASS = "ruleview-filterswatch";
+const ANGLE_SWATCH_CLASS = "ruleview-angleswatch";
+
+/*
+ * An actionable element is an element which on click triggers a specific action
+ * (e.g. shows a color tooltip, opens a link, …).
+ */
+const ACTIONABLE_ELEMENTS_SELECTORS = [
+ `.${COLOR_SWATCH_CLASS}`,
+ `.${BEZIER_SWATCH_CLASS}`,
+ `.${FILTER_SWATCH_CLASS}`,
+ `.${ANGLE_SWATCH_CLASS}`,
+ "a"
+];
+
+/**
+ * TextPropertyEditor is responsible for the following:
+ * Owns a TextProperty object.
+ * Manages changes to the TextProperty.
+ * Can be expanded to display computed properties.
+ * Can mark a property disabled or enabled.
+ *
+ * @param {RuleEditor} ruleEditor
+ * The rule editor that owns this TextPropertyEditor.
+ * @param {TextProperty} property
+ * The text property to edit.
+ */
+function TextPropertyEditor(ruleEditor, property) {
+ this.ruleEditor = ruleEditor;
+ this.ruleView = this.ruleEditor.ruleView;
+ this.doc = this.ruleEditor.doc;
+ this.popup = this.ruleView.popup;
+ this.prop = property;
+ this.prop.editor = this;
+ this.browserWindow = this.doc.defaultView.top;
+ this._populatedComputed = false;
+ this._hasPendingClick = false;
+ this._clickedElementOptions = null;
+
+ const toolbox = this.ruleView.inspector.toolbox;
+ this.cssProperties = getCssProperties(toolbox);
+
+ this._onEnableClicked = this._onEnableClicked.bind(this);
+ this._onExpandClicked = this._onExpandClicked.bind(this);
+ this._onStartEditing = this._onStartEditing.bind(this);
+ this._onNameDone = this._onNameDone.bind(this);
+ this._onValueDone = this._onValueDone.bind(this);
+ this._onSwatchCommit = this._onSwatchCommit.bind(this);
+ this._onSwatchPreview = this._onSwatchPreview.bind(this);
+ this._onSwatchRevert = this._onSwatchRevert.bind(this);
+ this._onValidate = this.ruleView.throttle(this._previewValue, 10, this);
+ this.update = this.update.bind(this);
+ this.updatePropertyState = this.updatePropertyState.bind(this);
+
+ this._create();
+ this.update();
+}
+
+TextPropertyEditor.prototype = {
+ /**
+ * Boolean indicating if the name or value is being currently edited.
+ */
+ get editing() {
+ return !!(this.nameSpan.inplaceEditor || this.valueSpan.inplaceEditor ||
+ this.ruleView.tooltips.isEditing) || this.popup.isOpen;
+ },
+
+ /**
+ * Get the rule to the current text property
+ */
+ get rule() {
+ return this.prop.rule;
+ },
+
+ /**
+ * Create the property editor's DOM.
+ */
+ _create: function () {
+ this.element = this.doc.createElementNS(HTML_NS, "li");
+ this.element.classList.add("ruleview-property");
+ this.element._textPropertyEditor = this;
+
+ this.container = createChild(this.element, "div", {
+ class: "ruleview-propertycontainer"
+ });
+
+ // The enable checkbox will disable or enable the rule.
+ this.enable = createChild(this.container, "div", {
+ class: "ruleview-enableproperty theme-checkbox",
+ tabindex: "-1"
+ });
+
+ // Click to expand the computed properties of the text property.
+ this.expander = createChild(this.container, "span", {
+ class: "ruleview-expander theme-twisty"
+ });
+ this.expander.addEventListener("click", this._onExpandClicked, true);
+
+ this.nameContainer = createChild(this.container, "span", {
+ class: "ruleview-namecontainer"
+ });
+
+ // Property name, editable when focused. Property name
+ // is committed when the editor is unfocused.
+ this.nameSpan = createChild(this.nameContainer, "span", {
+ class: "ruleview-propertyname theme-fg-color5",
+ tabindex: this.ruleEditor.isEditable ? "0" : "-1",
+ });
+
+ appendText(this.nameContainer, ": ");
+
+ // Create a span that will hold the property and semicolon.
+ // Use this span to create a slightly larger click target
+ // for the value.
+ this.valueContainer = createChild(this.container, "span", {
+ class: "ruleview-propertyvaluecontainer"
+ });
+
+ // Property value, editable when focused. Changes to the
+ // property value are applied as they are typed, and reverted
+ // if the user presses escape.
+ this.valueSpan = createChild(this.valueContainer, "span", {
+ class: "ruleview-propertyvalue theme-fg-color1",
+ tabindex: this.ruleEditor.isEditable ? "0" : "-1",
+ });
+
+ // Storing the TextProperty on the elements for easy access
+ // (for instance by the tooltip)
+ this.valueSpan.textProperty = this.prop;
+ this.nameSpan.textProperty = this.prop;
+
+ // If the value is a color property we need to put it through the parser
+ // so that colors can be coerced into the default color type. This prevents
+ // us from thinking that when colors are coerced they have been changed by
+ // the user.
+ let outputParser = this.ruleView._outputParser;
+ let frag = outputParser.parseCssProperty(this.prop.name, this.prop.value);
+ let parsedValue = frag.textContent;
+
+ // Save the initial value as the last committed value,
+ // for restoring after pressing escape.
+ this.committed = { name: this.prop.name,
+ value: parsedValue,
+ priority: this.prop.priority };
+
+ appendText(this.valueContainer, ";");
+
+ this.warning = createChild(this.container, "div", {
+ class: "ruleview-warning",
+ hidden: "",
+ title: l10n("rule.warning.title"),
+ });
+
+ // Filter button that filters for the current property name and is
+ // displayed when the property is overridden by another rule.
+ this.filterProperty = createChild(this.container, "div", {
+ class: "ruleview-overridden-rule-filter",
+ hidden: "",
+ title: l10n("rule.filterProperty.title"),
+ });
+
+ this.filterProperty.addEventListener("click", event => {
+ this.ruleEditor.ruleView.setFilterStyles("`" + this.prop.name + "`");
+ event.stopPropagation();
+ }, false);
+
+ // Holds the viewers for the computed properties.
+ // will be populated in |_updateComputed|.
+ this.computed = createChild(this.element, "ul", {
+ class: "ruleview-computedlist",
+ });
+
+ // Only bind event handlers if the rule is editable.
+ if (this.ruleEditor.isEditable) {
+ this.enable.addEventListener("click", this._onEnableClicked, true);
+
+ this.nameContainer.addEventListener("click", (event) => {
+ // Clicks within the name shouldn't propagate any further.
+ event.stopPropagation();
+
+ // Forward clicks on nameContainer to the editable nameSpan
+ if (event.target === this.nameContainer) {
+ this.nameSpan.click();
+ }
+ }, false);
+
+ editableField({
+ start: this._onStartEditing,
+ element: this.nameSpan,
+ done: this._onNameDone,
+ destroy: this.updatePropertyState,
+ advanceChars: ":",
+ contentType: InplaceEditor.CONTENT_TYPES.CSS_PROPERTY,
+ popup: this.popup,
+ cssProperties: this.cssProperties,
+ contextMenu: this.ruleView.inspector.onTextBoxContextMenu
+ });
+
+ // Auto blur name field on multiple CSS rules get pasted in.
+ this.nameContainer.addEventListener("paste",
+ blurOnMultipleProperties(this.cssProperties), false);
+
+ this.valueContainer.addEventListener("click", (event) => {
+ // Clicks within the value shouldn't propagate any further.
+ event.stopPropagation();
+
+ // Forward clicks on valueContainer to the editable valueSpan
+ if (event.target === this.valueContainer) {
+ this.valueSpan.click();
+ }
+ }, false);
+
+ // The mousedown event could trigger a blur event on nameContainer, which
+ // will trigger a call to the update function. The update function clears
+ // valueSpan's markup. Thus the regular click event does not bubble up, and
+ // listener's callbacks are not called.
+ // So we need to remember where the user clicks in order to re-trigger the click
+ // after the valueSpan's markup is re-populated. We only need to track this for
+ // valueSpan's child elements, because direct click on valueSpan will always
+ // trigger a click event.
+ this.valueSpan.addEventListener("mousedown", (event) => {
+ let clickedEl = event.target;
+ if (clickedEl === this.valueSpan) {
+ return;
+ }
+ this._hasPendingClick = true;
+
+ let matchedSelector = ACTIONABLE_ELEMENTS_SELECTORS.find(
+ (selector) => clickedEl.matches(selector));
+ if (matchedSelector) {
+ let similarElements = [...this.valueSpan.querySelectorAll(matchedSelector)];
+ this._clickedElementOptions = {
+ selector: matchedSelector,
+ index: similarElements.indexOf(clickedEl)
+ };
+ }
+ }, false);
+
+ this.valueSpan.addEventListener("mouseup", (event) => {
+ this._clickedElementOptions = null;
+ this._hasPendingClick = false;
+ }, false);
+
+ this.valueSpan.addEventListener("click", (event) => {
+ let target = event.target;
+
+ if (target.nodeName === "a") {
+ event.stopPropagation();
+ event.preventDefault();
+ this.browserWindow.openUILinkIn(target.href, "tab");
+ }
+ }, false);
+
+ editableField({
+ start: this._onStartEditing,
+ element: this.valueSpan,
+ done: this._onValueDone,
+ destroy: this.update,
+ validate: this._onValidate,
+ advanceChars: advanceValidate,
+ contentType: InplaceEditor.CONTENT_TYPES.CSS_VALUE,
+ property: this.prop,
+ popup: this.popup,
+ multiline: true,
+ maxWidth: () => this.container.getBoundingClientRect().width,
+ cssProperties: this.cssProperties,
+ contextMenu: this.ruleView.inspector.onTextBoxContextMenu
+ });
+ }
+ },
+
+ /**
+ * Get the path from which to resolve requests for this
+ * rule's stylesheet.
+ *
+ * @return {String} the stylesheet's href.
+ */
+ get sheetHref() {
+ let domRule = this.rule.domRule;
+ if (domRule) {
+ return domRule.href || domRule.nodeHref;
+ }
+ return undefined;
+ },
+
+ /**
+ * Populate the span based on changes to the TextProperty.
+ */
+ update: function () {
+ if (this.ruleView.isDestroyed) {
+ return;
+ }
+
+ this.updatePropertyState();
+
+ let name = this.prop.name;
+ this.nameSpan.textContent = name;
+
+ // Combine the property's value and priority into one string for
+ // the value.
+ let store = this.rule.elementStyle.store;
+ let val = store.userProperties.getProperty(this.rule.style, name,
+ this.prop.value);
+ if (this.prop.priority) {
+ val += " !" + this.prop.priority;
+ }
+
+ let propDirty = store.userProperties.contains(this.rule.style, name);
+
+ if (propDirty) {
+ this.element.setAttribute("dirty", "");
+ } else {
+ this.element.removeAttribute("dirty");
+ }
+
+ let outputParser = this.ruleView._outputParser;
+ let parserOptions = {
+ angleClass: "ruleview-angle",
+ angleSwatchClass: SHARED_SWATCH_CLASS + " " + ANGLE_SWATCH_CLASS,
+ bezierClass: "ruleview-bezier",
+ bezierSwatchClass: SHARED_SWATCH_CLASS + " " + BEZIER_SWATCH_CLASS,
+ colorClass: "ruleview-color",
+ colorSwatchClass: SHARED_SWATCH_CLASS + " " + COLOR_SWATCH_CLASS,
+ filterClass: "ruleview-filter",
+ filterSwatchClass: SHARED_SWATCH_CLASS + " " + FILTER_SWATCH_CLASS,
+ gridClass: "ruleview-grid",
+ defaultColorType: !propDirty,
+ urlClass: "theme-link",
+ baseURI: this.sheetHref
+ };
+ let frag = outputParser.parseCssProperty(name, val, parserOptions);
+ this.valueSpan.innerHTML = "";
+ this.valueSpan.appendChild(frag);
+
+ this.ruleView.emit("property-value-updated", this.valueSpan);
+
+ // Attach the color picker tooltip to the color swatches
+ this._colorSwatchSpans =
+ this.valueSpan.querySelectorAll("." + COLOR_SWATCH_CLASS);
+ if (this.ruleEditor.isEditable) {
+ for (let span of this._colorSwatchSpans) {
+ // Adding this swatch to the list of swatches our colorpicker
+ // knows about
+ this.ruleView.tooltips.colorPicker.addSwatch(span, {
+ onShow: this._onStartEditing,
+ onPreview: this._onSwatchPreview,
+ onCommit: this._onSwatchCommit,
+ onRevert: this._onSwatchRevert
+ });
+ span.on("unit-change", this._onSwatchCommit);
+ let title = l10n("rule.colorSwatch.tooltip");
+ span.setAttribute("title", title);
+ }
+ }
+
+ // Attach the cubic-bezier tooltip to the bezier swatches
+ this._bezierSwatchSpans =
+ this.valueSpan.querySelectorAll("." + BEZIER_SWATCH_CLASS);
+ if (this.ruleEditor.isEditable) {
+ for (let span of this._bezierSwatchSpans) {
+ // Adding this swatch to the list of swatches our colorpicker
+ // knows about
+ this.ruleView.tooltips.cubicBezier.addSwatch(span, {
+ onShow: this._onStartEditing,
+ onPreview: this._onSwatchPreview,
+ onCommit: this._onSwatchCommit,
+ onRevert: this._onSwatchRevert
+ });
+ let title = l10n("rule.bezierSwatch.tooltip");
+ span.setAttribute("title", title);
+ }
+ }
+
+ // Attach the filter editor tooltip to the filter swatch
+ let span = this.valueSpan.querySelector("." + FILTER_SWATCH_CLASS);
+ if (this.ruleEditor.isEditable) {
+ if (span) {
+ parserOptions.filterSwatch = true;
+
+ this.ruleView.tooltips.filterEditor.addSwatch(span, {
+ onShow: this._onStartEditing,
+ onPreview: this._onSwatchPreview,
+ onCommit: this._onSwatchCommit,
+ onRevert: this._onSwatchRevert
+ }, outputParser, parserOptions);
+ let title = l10n("rule.filterSwatch.tooltip");
+ span.setAttribute("title", title);
+ }
+ }
+
+ this.angleSwatchSpans =
+ this.valueSpan.querySelectorAll("." + ANGLE_SWATCH_CLASS);
+ if (this.ruleEditor.isEditable) {
+ for (let angleSpan of this.angleSwatchSpans) {
+ angleSpan.on("unit-change", this._onSwatchCommit);
+ let title = l10n("rule.angleSwatch.tooltip");
+ angleSpan.setAttribute("title", title);
+ }
+ }
+
+ let gridToggle = this.valueSpan.querySelector(".ruleview-grid");
+ if (gridToggle) {
+ gridToggle.setAttribute("title", l10n("rule.gridToggle.tooltip"));
+ if (this.ruleView.highlighters.gridHighlighterShown ===
+ this.ruleView.inspector.selection.nodeFront) {
+ gridToggle.classList.add("active");
+ }
+ }
+
+ // Now that we have updated the property's value, we might have a pending
+ // click on the value container. If we do, we have to trigger a click event
+ // on the right element.
+ if (this._hasPendingClick) {
+ this._hasPendingClick = false;
+ let elToClick;
+
+ if (this._clickedElementOptions !== null) {
+ let {selector, index} = this._clickedElementOptions;
+ elToClick = this.valueSpan.querySelectorAll(selector)[index];
+
+ this._clickedElementOptions = null;
+ }
+
+ if (!elToClick) {
+ elToClick = this.valueSpan;
+ }
+ elToClick.click();
+ }
+
+ // Populate the computed styles.
+ this._updateComputed();
+
+ // Update the rule property highlight.
+ this.ruleView._updatePropertyHighlight(this);
+ },
+
+ _onStartEditing: function () {
+ this.element.classList.remove("ruleview-overridden");
+ this.filterProperty.hidden = true;
+ this.enable.style.visibility = "hidden";
+ },
+
+ /**
+ * Update the visibility of the enable checkbox, the warning indicator and
+ * the filter property, as well as the overriden state of the property.
+ */
+ updatePropertyState: function () {
+ if (this.prop.enabled) {
+ this.enable.style.removeProperty("visibility");
+ this.enable.setAttribute("checked", "");
+ } else {
+ this.enable.style.visibility = "visible";
+ this.enable.removeAttribute("checked");
+ }
+
+ this.warning.hidden = this.editing || this.isValid();
+ this.filterProperty.hidden = this.editing ||
+ !this.isValid() ||
+ !this.prop.overridden ||
+ this.ruleEditor.rule.isUnmatched;
+
+ if (!this.editing &&
+ (this.prop.overridden || !this.prop.enabled ||
+ !this.prop.isKnownProperty())) {
+ this.element.classList.add("ruleview-overridden");
+ } else {
+ this.element.classList.remove("ruleview-overridden");
+ }
+ },
+
+ /**
+ * Update the indicator for computed styles. The computed styles themselves
+ * are populated on demand, when they become visible.
+ */
+ _updateComputed: function () {
+ this.computed.innerHTML = "";
+
+ let showExpander = this.prop.computed.some(c => c.name !== this.prop.name);
+ this.expander.style.visibility = showExpander ? "visible" : "hidden";
+
+ this._populatedComputed = false;
+ if (this.expander.hasAttribute("open")) {
+ this._populateComputed();
+ }
+ },
+
+ /**
+ * Populate the list of computed styles.
+ */
+ _populateComputed: function () {
+ if (this._populatedComputed) {
+ return;
+ }
+ this._populatedComputed = true;
+
+ for (let computed of this.prop.computed) {
+ // Don't bother to duplicate information already
+ // shown in the text property.
+ if (computed.name === this.prop.name) {
+ continue;
+ }
+
+ let li = createChild(this.computed, "li", {
+ class: "ruleview-computed"
+ });
+
+ if (computed.overridden) {
+ li.classList.add("ruleview-overridden");
+ }
+
+ createChild(li, "span", {
+ class: "ruleview-propertyname theme-fg-color5",
+ textContent: computed.name
+ });
+ appendText(li, ": ");
+
+ let outputParser = this.ruleView._outputParser;
+ let frag = outputParser.parseCssProperty(
+ computed.name, computed.value, {
+ colorSwatchClass: "ruleview-swatch ruleview-colorswatch",
+ urlClass: "theme-link",
+ baseURI: this.sheetHref
+ }
+ );
+
+ // Store the computed property value that was parsed for output
+ computed.parsedValue = frag.textContent;
+
+ createChild(li, "span", {
+ class: "ruleview-propertyvalue theme-fg-color1",
+ child: frag
+ });
+
+ appendText(li, ";");
+
+ // Store the computed style element for easy access when highlighting
+ // styles
+ computed.element = li;
+ }
+ },
+
+ /**
+ * Handles clicks on the disabled property.
+ */
+ _onEnableClicked: function (event) {
+ let checked = this.enable.hasAttribute("checked");
+ if (checked) {
+ this.enable.removeAttribute("checked");
+ } else {
+ this.enable.setAttribute("checked", "");
+ }
+ this.prop.setEnabled(!checked);
+ event.stopPropagation();
+ },
+
+ /**
+ * Handles clicks on the computed property expander. If the computed list is
+ * open due to user expanding or style filtering, collapse the computed list
+ * and close the expander. Otherwise, add user-open attribute which is used to
+ * expand the computed list and tracks whether or not the computed list is
+ * expanded by manually by the user.
+ */
+ _onExpandClicked: function (event) {
+ if (this.computed.hasAttribute("filter-open") ||
+ this.computed.hasAttribute("user-open")) {
+ this.expander.removeAttribute("open");
+ this.computed.removeAttribute("filter-open");
+ this.computed.removeAttribute("user-open");
+ } else {
+ this.expander.setAttribute("open", "true");
+ this.computed.setAttribute("user-open", "");
+ this._populateComputed();
+ }
+
+ event.stopPropagation();
+ },
+
+ /**
+ * Expands the computed list when a computed property is matched by the style
+ * filtering. The filter-open attribute is used to track whether or not the
+ * computed list was toggled opened by the filter.
+ */
+ expandForFilter: function () {
+ if (!this.computed.hasAttribute("user-open")) {
+ this.expander.setAttribute("open", "true");
+ this.computed.setAttribute("filter-open", "");
+ this._populateComputed();
+ }
+ },
+
+ /**
+ * Collapses the computed list that was expanded by style filtering.
+ */
+ collapseForFilter: function () {
+ this.computed.removeAttribute("filter-open");
+
+ if (!this.computed.hasAttribute("user-open")) {
+ this.expander.removeAttribute("open");
+ }
+ },
+
+ /**
+ * Called when the property name's inplace editor is closed.
+ * Ignores the change if the user pressed escape, otherwise
+ * commits it.
+ *
+ * @param {String} value
+ * The value contained in the editor.
+ * @param {Boolean} commit
+ * True if the change should be applied.
+ * @param {Number} direction
+ * The move focus direction number.
+ */
+ _onNameDone: function (value, commit, direction) {
+ let isNameUnchanged = (!commit && !this.ruleEditor.isEditing) ||
+ this.committed.name === value;
+ if (this.prop.value && isNameUnchanged) {
+ return;
+ }
+
+ // Remove a property if the name is empty
+ if (!value.trim()) {
+ this.remove(direction);
+ return;
+ }
+
+ // Remove a property if the property value is empty and the property
+ // value is not about to be focused
+ if (!this.prop.value &&
+ direction !== Services.focus.MOVEFOCUS_FORWARD) {
+ this.remove(direction);
+ return;
+ }
+
+ // Adding multiple rules inside of name field overwrites the current
+ // property with the first, then adds any more onto the property list.
+ let properties = parseDeclarations(this.cssProperties.isKnown, value);
+
+ if (properties.length) {
+ this.prop.setName(properties[0].name);
+ this.committed.name = this.prop.name;
+
+ if (!this.prop.enabled) {
+ this.prop.setEnabled(true);
+ }
+
+ if (properties.length > 1) {
+ this.prop.setValue(properties[0].value, properties[0].priority);
+ this.ruleEditor.addProperties(properties.slice(1), this.prop);
+ }
+ }
+ },
+
+ /**
+ * Remove property from style and the editors from DOM.
+ * Begin editing next or previous available property given the focus
+ * direction.
+ *
+ * @param {Number} direction
+ * The move focus direction number.
+ */
+ remove: function (direction) {
+ if (this._colorSwatchSpans && this._colorSwatchSpans.length) {
+ for (let span of this._colorSwatchSpans) {
+ this.ruleView.tooltips.colorPicker.removeSwatch(span);
+ span.off("unit-change", this._onSwatchCommit);
+ }
+ }
+
+ if (this.angleSwatchSpans && this.angleSwatchSpans.length) {
+ for (let span of this.angleSwatchSpans) {
+ span.off("unit-change", this._onSwatchCommit);
+ }
+ }
+
+ this.element.parentNode.removeChild(this.element);
+ this.ruleEditor.rule.editClosestTextProperty(this.prop, direction);
+ this.nameSpan.textProperty = null;
+ this.valueSpan.textProperty = null;
+ this.prop.remove();
+ },
+
+ /**
+ * Called when a value editor closes. If the user pressed escape,
+ * revert to the value this property had before editing.
+ *
+ * @param {String} value
+ * The value contained in the editor.
+ * @param {Boolean} commit
+ * True if the change should be applied.
+ * @param {Number} direction
+ * The move focus direction number.
+ */
+ _onValueDone: function (value = "", commit, direction) {
+ let parsedProperties = this._getValueAndExtraProperties(value);
+ let val = parseSingleValue(this.cssProperties.isKnown,
+ parsedProperties.firstValue);
+ let isValueUnchanged = (!commit && !this.ruleEditor.isEditing) ||
+ !parsedProperties.propertiesToAdd.length &&
+ this.committed.value === val.value &&
+ this.committed.priority === val.priority;
+ // If the value is not empty and unchanged, revert the property back to
+ // its original value and enabled or disabled state
+ if (value.trim() && isValueUnchanged) {
+ this.ruleEditor.rule.previewPropertyValue(this.prop, val.value,
+ val.priority);
+ this.rule.setPropertyEnabled(this.prop, this.prop.enabled);
+ return;
+ }
+
+ if (this.isDisplayGrid()) {
+ this.ruleView.highlighters._hideGridHighlighter();
+ }
+
+ // First, set this property value (common case, only modified a property)
+ this.prop.setValue(val.value, val.priority);
+
+ if (!this.prop.enabled) {
+ this.prop.setEnabled(true);
+ }
+
+ this.committed.value = this.prop.value;
+ this.committed.priority = this.prop.priority;
+
+ // If needed, add any new properties after this.prop.
+ this.ruleEditor.addProperties(parsedProperties.propertiesToAdd, this.prop);
+
+ // If the input value is empty and the focus is moving forward to the next
+ // editable field, then remove the whole property.
+ // A timeout is used here to accurately check the state, since the inplace
+ // editor `done` and `destroy` events fire before the next editor
+ // is focused.
+ if (!value.trim() && direction !== Services.focus.MOVEFOCUS_BACKWARD) {
+ setTimeout(() => {
+ if (!this.editing) {
+ this.remove(direction);
+ }
+ }, 0);
+ }
+ },
+
+ /**
+ * Called when the swatch editor wants to commit a value change.
+ */
+ _onSwatchCommit: function () {
+ this._onValueDone(this.valueSpan.textContent, true);
+ this.update();
+ },
+
+ /**
+ * Called when the swatch editor wants to preview a value change.
+ */
+ _onSwatchPreview: function () {
+ this._previewValue(this.valueSpan.textContent);
+ },
+
+ /**
+ * Called when the swatch editor closes from an ESC. Revert to the original
+ * value of this property before editing.
+ */
+ _onSwatchRevert: function () {
+ this._previewValue(this.prop.value, true);
+ this.update();
+ },
+
+ /**
+ * Parse a value string and break it into pieces, starting with the
+ * first value, and into an array of additional properties (if any).
+ *
+ * Example: Calling with "red; width: 100px" would return
+ * { firstValue: "red", propertiesToAdd: [{ name: "width", value: "100px" }] }
+ *
+ * @param {String} value
+ * The string to parse
+ * @return {Object} An object with the following properties:
+ * firstValue: A string containing a simple value, like
+ * "red" or "100px!important"
+ * propertiesToAdd: An array with additional properties, following the
+ * parseDeclarations format of {name,value,priority}
+ */
+ _getValueAndExtraProperties: function (value) {
+ // The inplace editor will prevent manual typing of multiple properties,
+ // but we need to deal with the case during a paste event.
+ // Adding multiple properties inside of value editor sets value with the
+ // first, then adds any more onto the property list (below this property).
+ let firstValue = value;
+ let propertiesToAdd = [];
+
+ let properties = parseDeclarations(this.cssProperties.isKnown, value);
+
+ // Check to see if the input string can be parsed as multiple properties
+ if (properties.length) {
+ // Get the first property value (if any), and any remaining
+ // properties (if any)
+ if (!properties[0].name && properties[0].value) {
+ firstValue = properties[0].value;
+ propertiesToAdd = properties.slice(1);
+ } else if (properties[0].name && properties[0].value) {
+ // In some cases, the value could be a property:value pair
+ // itself. Join them as one value string and append
+ // potentially following properties
+ firstValue = properties[0].name + ": " + properties[0].value;
+ propertiesToAdd = properties.slice(1);
+ }
+ }
+
+ return {
+ propertiesToAdd: propertiesToAdd,
+ firstValue: firstValue
+ };
+ },
+
+ /**
+ * Live preview this property, without committing changes.
+ *
+ * @param {String} value
+ * The value to set the current property to.
+ * @param {Boolean} reverting
+ * True if we're reverting the previously previewed value
+ */
+ _previewValue: function (value, reverting = false) {
+ // Since function call is throttled, we need to make sure we are still
+ // editing, and any selector modifications have been completed
+ if (!reverting && (!this.editing || this.ruleEditor.isEditing)) {
+ return;
+ }
+
+ let val = parseSingleValue(this.cssProperties.isKnown, value);
+ this.ruleEditor.rule.previewPropertyValue(this.prop, val.value,
+ val.priority);
+ },
+
+ /**
+ * Validate this property. Does it make sense for this value to be assigned
+ * to this property name? This does not apply the property value
+ *
+ * @return {Boolean} true if the property value is valid, false otherwise.
+ */
+ isValid: function () {
+ return this.prop.isValid();
+ },
+
+ /**
+ * Returns true if the property is a `display: grid` declaration.
+ *
+ * @return {Boolean} true if the property is a `display: grid` declaration.
+ */
+ isDisplayGrid: function () {
+ return this.prop.name === "display" && this.prop.value === "grid";
+ }
+};
+
+exports.TextPropertyEditor = TextPropertyEditor;
diff --git a/devtools/client/inspector/shared/dom-node-preview.js b/devtools/client/inspector/shared/dom-node-preview.js
new file mode 100644
index 000000000..d96384785
--- /dev/null
+++ b/devtools/client/inspector/shared/dom-node-preview.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";
+
+const {Task} = require("devtools/shared/task");
+const EventEmitter = require("devtools/shared/event-emitter");
+const {createNode} = require("devtools/client/animationinspector/utils");
+const { LocalizationHelper } = require("devtools/shared/l10n");
+
+const STRINGS_URI = "devtools/client/locales/inspector.properties";
+const L10N = new LocalizationHelper(STRINGS_URI);
+
+/**
+ * UI component responsible for displaying a preview of a dom node.
+ * @param {InspectorPanel} inspector Requires a reference to the inspector-panel
+ * to highlight and select the node, as well as refresh it when there are
+ * mutations.
+ * @param {Object} options Supported properties are:
+ * - compact {Boolean} Defaults to false.
+ * By default, nodes are previewed like <tag id="id" class="class">
+ * If true, nodes will be previewed like tag#id.class instead.
+ */
+function DomNodePreview(inspector, options = {}) {
+ this.inspector = inspector;
+ this.options = options;
+
+ this.onPreviewMouseOver = this.onPreviewMouseOver.bind(this);
+ this.onPreviewMouseOut = this.onPreviewMouseOut.bind(this);
+ this.onSelectElClick = this.onSelectElClick.bind(this);
+ this.onMarkupMutations = this.onMarkupMutations.bind(this);
+ this.onHighlightElClick = this.onHighlightElClick.bind(this);
+ this.onHighlighterLocked = this.onHighlighterLocked.bind(this);
+
+ EventEmitter.decorate(this);
+}
+
+exports.DomNodePreview = DomNodePreview;
+
+DomNodePreview.prototype = {
+ init: function (containerEl) {
+ let document = containerEl.ownerDocument;
+
+ // Init the markup for displaying the target node.
+ this.el = createNode({
+ parent: containerEl,
+ attributes: {
+ "class": "animation-target"
+ }
+ });
+
+ // Icon to select the node in the inspector.
+ this.highlightNodeEl = createNode({
+ parent: this.el,
+ nodeType: "span",
+ attributes: {
+ "class": "node-highlighter",
+ "title": L10N.getStr("inspector.nodePreview.highlightNodeLabel")
+ }
+ });
+
+ // Wrapper used for mouseover/out event handling.
+ this.previewEl = createNode({
+ parent: this.el,
+ nodeType: "span",
+ attributes: {
+ "title": L10N.getStr("inspector.nodePreview.selectNodeLabel")
+ }
+ });
+
+ if (!this.options.compact) {
+ this.previewEl.appendChild(document.createTextNode("<"));
+ }
+
+ // Only used for ::before and ::after pseudo-elements.
+ this.pseudoEl = createNode({
+ parent: this.previewEl,
+ nodeType: "span",
+ attributes: {
+ "class": "pseudo-element theme-fg-color5"
+ }
+ });
+
+ // Tag name.
+ this.tagNameEl = createNode({
+ parent: this.previewEl,
+ nodeType: "span",
+ attributes: {
+ "class": "tag-name theme-fg-color3"
+ }
+ });
+
+ // Id attribute container.
+ this.idEl = createNode({
+ parent: this.previewEl,
+ nodeType: "span"
+ });
+
+ if (!this.options.compact) {
+ createNode({
+ parent: this.idEl,
+ nodeType: "span",
+ attributes: {
+ "class": "attribute-name theme-fg-color2"
+ },
+ textContent: "id"
+ });
+ this.idEl.appendChild(document.createTextNode("=\""));
+ } else {
+ createNode({
+ parent: this.idEl,
+ nodeType: "span",
+ attributes: {
+ "class": "theme-fg-color6"
+ },
+ textContent: "#"
+ });
+ }
+
+ createNode({
+ parent: this.idEl,
+ nodeType: "span",
+ attributes: {
+ "class": "attribute-value theme-fg-color6"
+ }
+ });
+
+ if (!this.options.compact) {
+ this.idEl.appendChild(document.createTextNode("\""));
+ }
+
+ // Class attribute container.
+ this.classEl = createNode({
+ parent: this.previewEl,
+ nodeType: "span"
+ });
+
+ if (!this.options.compact) {
+ createNode({
+ parent: this.classEl,
+ nodeType: "span",
+ attributes: {
+ "class": "attribute-name theme-fg-color2"
+ },
+ textContent: "class"
+ });
+ this.classEl.appendChild(document.createTextNode("=\""));
+ } else {
+ createNode({
+ parent: this.classEl,
+ nodeType: "span",
+ attributes: {
+ "class": "theme-fg-color6"
+ },
+ textContent: "."
+ });
+ }
+
+ createNode({
+ parent: this.classEl,
+ nodeType: "span",
+ attributes: {
+ "class": "attribute-value theme-fg-color6"
+ }
+ });
+
+ if (!this.options.compact) {
+ this.classEl.appendChild(document.createTextNode("\""));
+ this.previewEl.appendChild(document.createTextNode(">"));
+ }
+
+ this.startListeners();
+ },
+
+ startListeners: function () {
+ // Init events for highlighting and selecting the node.
+ this.previewEl.addEventListener("mouseover", this.onPreviewMouseOver);
+ this.previewEl.addEventListener("mouseout", this.onPreviewMouseOut);
+ this.previewEl.addEventListener("click", this.onSelectElClick);
+ this.highlightNodeEl.addEventListener("click", this.onHighlightElClick);
+
+ // Start to listen for markupmutation events.
+ this.inspector.on("markupmutation", this.onMarkupMutations);
+
+ // Listen to the target node highlighter.
+ HighlighterLock.on("highlighted", this.onHighlighterLocked);
+ },
+
+ stopListeners: function () {
+ HighlighterLock.off("highlighted", this.onHighlighterLocked);
+ this.inspector.off("markupmutation", this.onMarkupMutations);
+ this.previewEl.removeEventListener("mouseover", this.onPreviewMouseOver);
+ this.previewEl.removeEventListener("mouseout", this.onPreviewMouseOut);
+ this.previewEl.removeEventListener("click", this.onSelectElClick);
+ this.highlightNodeEl.removeEventListener("click", this.onHighlightElClick);
+ },
+
+ destroy: function () {
+ HighlighterLock.unhighlight().catch(e => console.error(e));
+
+ this.stopListeners();
+
+ this.el.remove();
+ this.el = this.tagNameEl = this.idEl = this.classEl = this.pseudoEl = null;
+ this.highlightNodeEl = this.previewEl = null;
+ this.nodeFront = this.inspector = null;
+ },
+
+ get highlighterUtils() {
+ if (this.inspector && this.inspector.toolbox) {
+ return this.inspector.toolbox.highlighterUtils;
+ }
+ return null;
+ },
+
+ onPreviewMouseOver: function () {
+ if (!this.nodeFront || !this.highlighterUtils) {
+ return;
+ }
+ this.highlighterUtils.highlightNodeFront(this.nodeFront)
+ .catch(e => console.error(e));
+ },
+
+ onPreviewMouseOut: function () {
+ if (!this.nodeFront || !this.highlighterUtils) {
+ return;
+ }
+ this.highlighterUtils.unhighlight()
+ .catch(e => console.error(e));
+ },
+
+ onSelectElClick: function () {
+ if (!this.nodeFront) {
+ return;
+ }
+ this.inspector.selection.setNodeFront(this.nodeFront, "dom-node-preview");
+ },
+
+ onHighlightElClick: function (e) {
+ e.stopPropagation();
+
+ let classList = this.highlightNodeEl.classList;
+ let isHighlighted = classList.contains("selected");
+
+ if (isHighlighted) {
+ classList.remove("selected");
+ HighlighterLock.unhighlight().then(() => {
+ this.emit("target-highlighter-unlocked");
+ }, error => console.error(error));
+ } else {
+ classList.add("selected");
+ HighlighterLock.highlight(this).then(() => {
+ this.emit("target-highlighter-locked");
+ }, error => console.error(error));
+ }
+ },
+
+ onHighlighterLocked: function (e, domNodePreview) {
+ if (domNodePreview !== this) {
+ this.highlightNodeEl.classList.remove("selected");
+ }
+ },
+
+ onMarkupMutations: function (e, mutations) {
+ if (!this.nodeFront) {
+ return;
+ }
+
+ for (let {target} of mutations) {
+ if (target === this.nodeFront) {
+ // Re-render with the same nodeFront to update the output.
+ this.render(this.nodeFront);
+ break;
+ }
+ }
+ },
+
+ render: function (nodeFront) {
+ this.nodeFront = nodeFront;
+ let {displayName, attributes} = nodeFront;
+
+ if (nodeFront.isPseudoElement) {
+ this.pseudoEl.textContent = nodeFront.isBeforePseudoElement
+ ? "::before"
+ : "::after";
+ this.pseudoEl.style.display = "inline";
+ this.tagNameEl.style.display = "none";
+ } else {
+ this.tagNameEl.textContent = displayName;
+ this.pseudoEl.style.display = "none";
+ this.tagNameEl.style.display = "inline";
+ }
+
+ let idIndex = attributes.findIndex(({name}) => name === "id");
+ if (idIndex > -1 && attributes[idIndex].value) {
+ this.idEl.querySelector(".attribute-value").textContent =
+ attributes[idIndex].value;
+ this.idEl.style.display = "inline";
+ } else {
+ this.idEl.style.display = "none";
+ }
+
+ let classIndex = attributes.findIndex(({name}) => name === "class");
+ if (classIndex > -1 && attributes[classIndex].value) {
+ let value = attributes[classIndex].value;
+ if (this.options.compact) {
+ value = value.split(" ").join(".");
+ }
+
+ this.classEl.querySelector(".attribute-value").textContent = value;
+ this.classEl.style.display = "inline";
+ } else {
+ this.classEl.style.display = "none";
+ }
+ }
+};
+
+/**
+ * HighlighterLock is a helper used to lock the highlighter on DOM nodes in the
+ * page.
+ * It instantiates a new highlighter that is then shared amongst all instances
+ * of DomNodePreview. This is useful because that means showing the highlighter
+ * on one node will unhighlight the previously highlighted one, but will not
+ * interfere with the default inspector highlighter.
+ */
+var HighlighterLock = {
+ highlighter: null,
+ isShown: false,
+
+ highlight: Task.async(function* (animationTargetNode) {
+ if (!this.highlighter) {
+ let util = animationTargetNode.inspector.toolbox.highlighterUtils;
+ this.highlighter = yield util.getHighlighterByType("BoxModelHighlighter");
+ }
+
+ yield this.highlighter.show(animationTargetNode.nodeFront);
+ this.isShown = true;
+ this.emit("highlighted", animationTargetNode);
+ }),
+
+ unhighlight: Task.async(function* () {
+ if (!this.highlighter || !this.isShown) {
+ return;
+ }
+
+ yield this.highlighter.hide();
+ this.isShown = false;
+ this.emit("unhighlighted");
+ })
+};
+
+EventEmitter.decorate(HighlighterLock);
diff --git a/devtools/client/inspector/shared/highlighters-overlay.js b/devtools/client/inspector/shared/highlighters-overlay.js
new file mode 100644
index 000000000..c054c72af
--- /dev/null
+++ b/devtools/client/inspector/shared/highlighters-overlay.js
@@ -0,0 +1,315 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set 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";
+
+/**
+ * The highlighter overlays are in-content highlighters that appear when hovering over
+ * property values.
+ */
+
+const promise = require("promise");
+const EventEmitter = require("devtools/shared/event-emitter");
+const { VIEW_NODE_VALUE_TYPE } = require("devtools/client/inspector/shared/node-types");
+
+/**
+ * Manages all highlighters in the style-inspector.
+ *
+ * @param {CssRuleView|CssComputedView} view
+ * Either the rule-view or computed-view panel
+ */
+function HighlightersOverlay(view) {
+ this.view = view;
+
+ let {CssRuleView} = require("devtools/client/inspector/rules/rules");
+ this.isRuleView = view instanceof CssRuleView;
+
+ this.highlighters = {};
+
+ // NodeFront of the grid container that is highlighted.
+ this.gridHighlighterShown = null;
+ // Name of the highlighter shown on mouse hover.
+ this.hoveredHighlighterShown = null;
+ // Name of the selector highlighter shown.
+ this.selectorHighlighterShown = null;
+
+ this.highlighterUtils = this.view.inspector.toolbox.highlighterUtils;
+
+ // Only initialize the overlay if at least one of the highlighter types is
+ // supported.
+ this.supportsHighlighters =
+ this.highlighterUtils.supportsCustomHighlighters();
+
+ this._onClick = this._onClick.bind(this);
+ this._onMouseMove = this._onMouseMove.bind(this);
+ this._onMouseOut = this._onMouseOut.bind(this);
+ this._onWillNavigate = this._onWillNavigate.bind(this);
+
+ EventEmitter.decorate(this);
+}
+
+HighlightersOverlay.prototype = {
+ /**
+ * Add the highlighters overlay to the view. This will start tracking mouse
+ * movements and display highlighters when needed.
+ */
+ addToView: function () {
+ if (!this.supportsHighlighters || this._isStarted || this._isDestroyed) {
+ return;
+ }
+
+ let el = this.view.element;
+ el.addEventListener("click", this._onClick, true);
+ el.addEventListener("mousemove", this._onMouseMove, false);
+ el.addEventListener("mouseout", this._onMouseOut, false);
+ el.ownerDocument.defaultView.addEventListener("mouseout", this._onMouseOut, false);
+
+ if (this.isRuleView) {
+ this.view.inspector.target.on("will-navigate", this._onWillNavigate);
+ }
+
+ this._isStarted = true;
+ },
+
+ /**
+ * Remove the overlay from the current view. This will stop tracking mouse
+ * movement and showing highlighters.
+ */
+ removeFromView: function () {
+ if (!this.supportsHighlighters || !this._isStarted || this._isDestroyed) {
+ return;
+ }
+
+ let el = this.view.element;
+ el.removeEventListener("click", this._onClick, true);
+ el.removeEventListener("mousemove", this._onMouseMove, false);
+ el.removeEventListener("mouseout", this._onMouseOut, false);
+
+ if (this.isRuleView) {
+ this.view.inspector.target.off("will-navigate", this._onWillNavigate);
+ }
+
+ this._isStarted = false;
+ },
+
+ _onClick: function (event) {
+ // Bail out if the target is not a grid property value.
+ if (!this._isDisplayGridValue(event.target)) {
+ return;
+ }
+
+ event.stopPropagation();
+
+ this._getHighlighter("CssGridHighlighter").then(highlighter => {
+ let node = this.view.inspector.selection.nodeFront;
+
+ // Toggle off the grid highlighter if the grid highlighter toggle is clicked
+ // for the current highlighted grid.
+ if (node === this.gridHighlighterShown) {
+ return highlighter.hide();
+ }
+
+ return highlighter.show(node);
+ }).then(isGridShown => {
+ // Toggle all the grid icons in the current rule view.
+ for (let gridIcon of this.view.element.querySelectorAll(".ruleview-grid")) {
+ gridIcon.classList.toggle("active", isGridShown);
+ }
+
+ if (isGridShown) {
+ this.gridHighlighterShown = this.view.inspector.selection.nodeFront;
+ this.emit("highlighter-shown");
+ } else {
+ this.gridHighlighterShown = null;
+ this.emit("highlighter-hidden");
+ }
+ }).catch(e => console.error(e));
+ },
+
+ _onMouseMove: function (event) {
+ // Bail out if the target is the same as for the last mousemove.
+ if (event.target === this._lastHovered) {
+ return;
+ }
+
+ // Only one highlighter can be displayed at a time, hide the currently shown.
+ this._hideHoveredHighlighter();
+
+ this._lastHovered = event.target;
+
+ let nodeInfo = this.view.getNodeInfo(event.target);
+ if (!nodeInfo) {
+ return;
+ }
+
+ // Choose the type of highlighter required for the hovered node.
+ let type;
+ if (this._isRuleViewTransform(nodeInfo) ||
+ this._isComputedViewTransform(nodeInfo)) {
+ type = "CssTransformHighlighter";
+ }
+
+ if (type) {
+ this.hoveredHighlighterShown = type;
+ let node = this.view.inspector.selection.nodeFront;
+ this._getHighlighter(type)
+ .then(highlighter => highlighter.show(node))
+ .then(shown => {
+ if (shown) {
+ this.emit("highlighter-shown");
+ }
+ });
+ }
+ },
+
+ _onMouseOut: function (event) {
+ // Only hide the highlighter if the mouse leaves the currently hovered node.
+ if (!this._lastHovered ||
+ (event && this._lastHovered.contains(event.relatedTarget))) {
+ return;
+ }
+
+ // Otherwise, hide the highlighter.
+ this._lastHovered = null;
+ this._hideHoveredHighlighter();
+ },
+
+ /**
+ * Clear saved highlighter shown properties on will-navigate.
+ */
+ _onWillNavigate: function () {
+ this.gridHighlighterShown = null;
+ this.hoveredHighlighterShown = null;
+ this.selectorHighlighterShown = null;
+ },
+
+ /**
+ * Is the current hovered node a css transform property value in the rule-view.
+ *
+ * @param {Object} nodeInfo
+ * @return {Boolean}
+ */
+ _isRuleViewTransform: function (nodeInfo) {
+ let isTransform = nodeInfo.type === VIEW_NODE_VALUE_TYPE &&
+ nodeInfo.value.property === "transform";
+ let isEnabled = nodeInfo.value.enabled &&
+ !nodeInfo.value.overridden &&
+ !nodeInfo.value.pseudoElement;
+ return this.isRuleView && isTransform && isEnabled;
+ },
+
+ /**
+ * Is the current hovered node a css transform property value in the
+ * computed-view.
+ *
+ * @param {Object} nodeInfo
+ * @return {Boolean}
+ */
+ _isComputedViewTransform: function (nodeInfo) {
+ let isTransform = nodeInfo.type === VIEW_NODE_VALUE_TYPE &&
+ nodeInfo.value.property === "transform";
+ return !this.isRuleView && isTransform;
+ },
+
+ /**
+ * Is the current clicked node a grid display property value in the
+ * rule-view.
+ *
+ * @param {DOMNode} node
+ * @return {Boolean}
+ */
+ _isDisplayGridValue: function (node) {
+ return this.isRuleView && node.classList.contains("ruleview-grid");
+ },
+
+ /**
+ * Hide the currently shown grid highlighter.
+ */
+ _hideGridHighlighter: function () {
+ if (!this.gridHighlighterShown || !this.highlighters.CssGridHighlighter) {
+ return;
+ }
+
+ let onHidden = this.highlighters.CssGridHighlighter.hide();
+ if (onHidden) {
+ onHidden.then(null, e => console.error(e));
+ }
+
+ this.gridHighlighterShown = null;
+ this.emit("highlighter-hidden");
+ },
+
+ /**
+ * Hide the currently shown hovered highlighter.
+ */
+ _hideHoveredHighlighter: function () {
+ if (!this.hoveredHighlighterShown ||
+ !this.highlighters[this.hoveredHighlighterShown]) {
+ return;
+ }
+
+ // For some reason, the call to highlighter.hide doesn't always return a
+ // promise. This causes some tests to fail when trying to install a
+ // rejection handler on the result of the call. To avoid this, check
+ // whether the result is truthy before installing the handler.
+ let onHidden = this.highlighters[this.hoveredHighlighterShown].hide();
+ if (onHidden) {
+ onHidden.then(null, e => console.error(e));
+ }
+
+ this.hoveredHighlighterShown = null;
+ this.emit("highlighter-hidden");
+ },
+
+ /**
+ * Get a highlighter front given a type. It will only be initialized once.
+ *
+ * @param {String} type
+ * The highlighter type. One of this.highlighters.
+ * @return {Promise} that resolves to the highlighter
+ */
+ _getHighlighter: function (type) {
+ let utils = this.highlighterUtils;
+
+ if (this.highlighters[type]) {
+ return promise.resolve(this.highlighters[type]);
+ }
+
+ return utils.getHighlighterByType(type).then(highlighter => {
+ this.highlighters[type] = highlighter;
+ return highlighter;
+ });
+ },
+
+ /**
+ * Destroy this overlay instance, removing it from the view and destroying
+ * all initialized highlighters.
+ */
+ destroy: function () {
+ this.removeFromView();
+
+ for (let type in this.highlighters) {
+ if (this.highlighters[type]) {
+ this.highlighters[type].finalize();
+ this.highlighters[type] = null;
+ }
+ }
+
+ this.highlighters = null;
+
+ this.gridHighlighterShown = null;
+ this.hoveredHighlighterShown = null;
+ this.selectorHighlighterShown = null;
+
+ this.highlighterUtils = null;
+ this.isRuleView = null;
+ this.view = null;
+
+ this._isDestroyed = true;
+ }
+};
+
+module.exports = HighlightersOverlay;
diff --git a/devtools/client/inspector/shared/moz.build b/devtools/client/inspector/shared/moz.build
new file mode 100644
index 000000000..fd2239b60
--- /dev/null
+++ b/devtools/client/inspector/shared/moz.build
@@ -0,0 +1,16 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'dom-node-preview.js',
+ 'highlighters-overlay.js',
+ 'node-types.js',
+ 'style-inspector-menu.js',
+ 'tooltips-overlay.js',
+ 'utils.js'
+)
+
+BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
diff --git a/devtools/client/inspector/shared/node-types.js b/devtools/client/inspector/shared/node-types.js
new file mode 100644
index 000000000..4f31ee9fe
--- /dev/null
+++ b/devtools/client/inspector/shared/node-types.js
@@ -0,0 +1,17 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set 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";
+
+/**
+ * Types of nodes used in the rule and omputed view.
+ */
+
+exports.VIEW_NODE_SELECTOR_TYPE = 1;
+exports.VIEW_NODE_PROPERTY_TYPE = 2;
+exports.VIEW_NODE_VALUE_TYPE = 3;
+exports.VIEW_NODE_IMAGE_URL_TYPE = 4;
+exports.VIEW_NODE_LOCATION_TYPE = 5;
diff --git a/devtools/client/inspector/shared/style-inspector-menu.js b/devtools/client/inspector/shared/style-inspector-menu.js
new file mode 100644
index 000000000..975074609
--- /dev/null
+++ b/devtools/client/inspector/shared/style-inspector-menu.js
@@ -0,0 +1,510 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set 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 {PREF_ORIG_SOURCES} = require("devtools/client/styleeditor/utils");
+const Services = require("Services");
+const {Task} = require("devtools/shared/task");
+
+const Menu = require("devtools/client/framework/menu");
+const MenuItem = require("devtools/client/framework/menu-item");
+
+const {
+ VIEW_NODE_SELECTOR_TYPE,
+ VIEW_NODE_PROPERTY_TYPE,
+ VIEW_NODE_VALUE_TYPE,
+ VIEW_NODE_IMAGE_URL_TYPE,
+ VIEW_NODE_LOCATION_TYPE,
+} = require("devtools/client/inspector/shared/node-types");
+const clipboardHelper = require("devtools/shared/platform/clipboard");
+
+const STYLE_INSPECTOR_PROPERTIES = "devtools/shared/locales/styleinspector.properties";
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const STYLE_INSPECTOR_L10N = new LocalizationHelper(STYLE_INSPECTOR_PROPERTIES);
+
+const PREF_ENABLE_MDN_DOCS_TOOLTIP =
+ "devtools.inspector.mdnDocsTooltip.enabled";
+
+/**
+ * Style inspector context menu
+ *
+ * @param {RuleView|ComputedView} view
+ * RuleView or ComputedView instance controlling this menu
+ * @param {Object} options
+ * Option menu configuration
+ */
+function StyleInspectorMenu(view, options) {
+ this.view = view;
+ this.inspector = this.view.inspector;
+ this.styleDocument = this.view.styleDocument;
+ this.styleWindow = this.view.styleWindow;
+
+ this.isRuleView = options.isRuleView;
+
+ this._onAddNewRule = this._onAddNewRule.bind(this);
+ this._onCopy = this._onCopy.bind(this);
+ this._onCopyColor = this._onCopyColor.bind(this);
+ this._onCopyImageDataUrl = this._onCopyImageDataUrl.bind(this);
+ this._onCopyLocation = this._onCopyLocation.bind(this);
+ this._onCopyPropertyDeclaration = this._onCopyPropertyDeclaration.bind(this);
+ this._onCopyPropertyName = this._onCopyPropertyName.bind(this);
+ this._onCopyPropertyValue = this._onCopyPropertyValue.bind(this);
+ this._onCopyRule = this._onCopyRule.bind(this);
+ this._onCopySelector = this._onCopySelector.bind(this);
+ this._onCopyUrl = this._onCopyUrl.bind(this);
+ this._onSelectAll = this._onSelectAll.bind(this);
+ this._onShowMdnDocs = this._onShowMdnDocs.bind(this);
+ this._onToggleOrigSources = this._onToggleOrigSources.bind(this);
+}
+
+module.exports = StyleInspectorMenu;
+
+StyleInspectorMenu.prototype = {
+ /**
+ * Display the style inspector context menu
+ */
+ show: function (event) {
+ try {
+ this._openMenu({
+ target: event.explicitOriginalTarget,
+ screenX: event.screenX,
+ screenY: event.screenY,
+ });
+ } catch (e) {
+ console.error(e);
+ }
+ },
+
+ _openMenu: function ({ target, screenX = 0, screenY = 0 } = { }) {
+ // In the sidebar we do not have this.styleDocument.popupNode
+ // so we need to save the node ourselves.
+ this.styleDocument.popupNode = target;
+ this.styleWindow.focus();
+
+ let menu = new Menu();
+
+ let menuitemCopy = new MenuItem({
+ label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copy"),
+ accesskey: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copy.accessKey"),
+ click: () => {
+ this._onCopy();
+ },
+ disabled: !this._hasTextSelected(),
+ });
+ let menuitemCopyLocation = new MenuItem({
+ label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyLocation"),
+ click: () => {
+ this._onCopyLocation();
+ },
+ visible: false,
+ });
+ let menuitemCopyRule = new MenuItem({
+ label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyRule"),
+ click: () => {
+ this._onCopyRule();
+ },
+ visible: this.isRuleView,
+ });
+ let copyColorAccessKey = "styleinspector.contextmenu.copyColor.accessKey";
+ let menuitemCopyColor = new MenuItem({
+ label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyColor"),
+ accesskey: STYLE_INSPECTOR_L10N.getStr(copyColorAccessKey),
+ click: () => {
+ this._onCopyColor();
+ },
+ visible: this._isColorPopup(),
+ });
+ let copyUrlAccessKey = "styleinspector.contextmenu.copyUrl.accessKey";
+ let menuitemCopyUrl = new MenuItem({
+ label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyUrl"),
+ accesskey: STYLE_INSPECTOR_L10N.getStr(copyUrlAccessKey),
+ click: () => {
+ this._onCopyUrl();
+ },
+ visible: this._isImageUrl(),
+ });
+ let copyImageAccessKey = "styleinspector.contextmenu.copyImageDataUrl.accessKey";
+ let menuitemCopyImageDataUrl = new MenuItem({
+ label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyImageDataUrl"),
+ accesskey: STYLE_INSPECTOR_L10N.getStr(copyImageAccessKey),
+ click: () => {
+ this._onCopyImageDataUrl();
+ },
+ visible: this._isImageUrl(),
+ });
+ let copyPropDeclarationLabel = "styleinspector.contextmenu.copyPropertyDeclaration";
+ let menuitemCopyPropertyDeclaration = new MenuItem({
+ label: STYLE_INSPECTOR_L10N.getStr(copyPropDeclarationLabel),
+ click: () => {
+ this._onCopyPropertyDeclaration();
+ },
+ visible: false,
+ });
+ let menuitemCopyPropertyName = new MenuItem({
+ label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyPropertyName"),
+ click: () => {
+ this._onCopyPropertyName();
+ },
+ visible: false,
+ });
+ let menuitemCopyPropertyValue = new MenuItem({
+ label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyPropertyValue"),
+ click: () => {
+ this._onCopyPropertyValue();
+ },
+ visible: false,
+ });
+ let menuitemCopySelector = new MenuItem({
+ label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copySelector"),
+ click: () => {
+ this._onCopySelector();
+ },
+ visible: false,
+ });
+
+ this._clickedNodeInfo = this._getClickedNodeInfo();
+ if (this.isRuleView && this._clickedNodeInfo) {
+ switch (this._clickedNodeInfo.type) {
+ case VIEW_NODE_PROPERTY_TYPE :
+ menuitemCopyPropertyDeclaration.visible = true;
+ menuitemCopyPropertyName.visible = true;
+ break;
+ case VIEW_NODE_VALUE_TYPE :
+ menuitemCopyPropertyDeclaration.visible = true;
+ menuitemCopyPropertyValue.visible = true;
+ break;
+ case VIEW_NODE_SELECTOR_TYPE :
+ menuitemCopySelector.visible = true;
+ break;
+ case VIEW_NODE_LOCATION_TYPE :
+ menuitemCopyLocation.visible = true;
+ break;
+ }
+ }
+
+ menu.append(menuitemCopy);
+ menu.append(menuitemCopyLocation);
+ menu.append(menuitemCopyRule);
+ menu.append(menuitemCopyColor);
+ menu.append(menuitemCopyUrl);
+ menu.append(menuitemCopyImageDataUrl);
+ menu.append(menuitemCopyPropertyDeclaration);
+ menu.append(menuitemCopyPropertyName);
+ menu.append(menuitemCopyPropertyValue);
+ menu.append(menuitemCopySelector);
+
+ menu.append(new MenuItem({
+ type: "separator",
+ }));
+
+ // Select All
+ let selectAllAccessKey = "styleinspector.contextmenu.selectAll.accessKey";
+ let menuitemSelectAll = new MenuItem({
+ label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.selectAll"),
+ accesskey: STYLE_INSPECTOR_L10N.getStr(selectAllAccessKey),
+ click: () => {
+ this._onSelectAll();
+ },
+ });
+ menu.append(menuitemSelectAll);
+
+ menu.append(new MenuItem({
+ type: "separator",
+ }));
+
+ // Add new rule
+ let addRuleAccessKey = "styleinspector.contextmenu.addNewRule.accessKey";
+ let menuitemAddRule = new MenuItem({
+ label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.addNewRule"),
+ accesskey: STYLE_INSPECTOR_L10N.getStr(addRuleAccessKey),
+ click: () => {
+ this._onAddNewRule();
+ },
+ visible: this.isRuleView,
+ disabled: !this.isRuleView ||
+ this.inspector.selection.isAnonymousNode(),
+ });
+ menu.append(menuitemAddRule);
+
+ // Show MDN Docs
+ let mdnDocsAccessKey = "styleinspector.contextmenu.showMdnDocs.accessKey";
+ let menuitemShowMdnDocs = new MenuItem({
+ label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.showMdnDocs"),
+ accesskey: STYLE_INSPECTOR_L10N.getStr(mdnDocsAccessKey),
+ click: () => {
+ this._onShowMdnDocs();
+ },
+ visible: (Services.prefs.getBoolPref(PREF_ENABLE_MDN_DOCS_TOOLTIP) &&
+ this._isPropertyName()),
+ });
+ menu.append(menuitemShowMdnDocs);
+
+ // Show Original Sources
+ let sourcesAccessKey = "styleinspector.contextmenu.toggleOrigSources.accessKey";
+ let menuitemSources = new MenuItem({
+ label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.toggleOrigSources"),
+ accesskey: STYLE_INSPECTOR_L10N.getStr(sourcesAccessKey),
+ click: () => {
+ this._onToggleOrigSources();
+ },
+ type: "checkbox",
+ checked: Services.prefs.getBoolPref(PREF_ORIG_SOURCES),
+ });
+ menu.append(menuitemSources);
+
+ menu.popup(screenX, screenY, this.inspector._toolbox);
+ return menu;
+ },
+
+ _hasTextSelected: function () {
+ let hasTextSelected;
+ let selection = this.styleWindow.getSelection();
+
+ let node = this._getClickedNode();
+ if (node.nodeName == "input" || node.nodeName == "textarea") {
+ let { selectionStart, selectionEnd } = node;
+ hasTextSelected = isFinite(selectionStart) && isFinite(selectionEnd)
+ && selectionStart !== selectionEnd;
+ } else {
+ hasTextSelected = selection.toString() && !selection.isCollapsed;
+ }
+
+ return hasTextSelected;
+ },
+
+ /**
+ * Get the type of the currently clicked node
+ */
+ _getClickedNodeInfo: function () {
+ let node = this._getClickedNode();
+ return this.view.getNodeInfo(node);
+ },
+
+ /**
+ * A helper that determines if the popup was opened with a click to a color
+ * value and saves the color to this._colorToCopy.
+ *
+ * @return {Boolean}
+ * true if click on color opened the popup, false otherwise.
+ */
+ _isColorPopup: function () {
+ this._colorToCopy = "";
+
+ let container = this._getClickedNode();
+ if (!container) {
+ return false;
+ }
+
+ let isColorNode = el => el.dataset && "color" in el.dataset;
+
+ while (!isColorNode(container)) {
+ container = container.parentNode;
+ if (!container) {
+ return false;
+ }
+ }
+
+ this._colorToCopy = container.dataset.color;
+ return true;
+ },
+
+ _isPropertyName: function () {
+ let nodeInfo = this._getClickedNodeInfo();
+ if (!nodeInfo) {
+ return false;
+ }
+ return nodeInfo.type == VIEW_NODE_PROPERTY_TYPE;
+ },
+
+ /**
+ * Check if the current node (clicked node) is an image URL
+ *
+ * @return {Boolean} true if the node is an image url
+ */
+ _isImageUrl: function () {
+ let nodeInfo = this._getClickedNodeInfo();
+ if (!nodeInfo) {
+ return false;
+ }
+ return nodeInfo.type == VIEW_NODE_IMAGE_URL_TYPE;
+ },
+
+ /**
+ * Get the DOM Node container for the current popupNode.
+ * If popupNode is a textNode, return the parent node, otherwise return
+ * popupNode itself.
+ *
+ * @return {DOMNode}
+ */
+ _getClickedNode: function () {
+ let container = null;
+ let node = this.styleDocument.popupNode;
+
+ if (node) {
+ let isTextNode = node.nodeType == node.TEXT_NODE;
+ container = isTextNode ? node.parentElement : node;
+ }
+
+ return container;
+ },
+
+ /**
+ * Select all text.
+ */
+ _onSelectAll: function () {
+ let selection = this.styleWindow.getSelection();
+ selection.selectAllChildren(this.view.element);
+ },
+
+ /**
+ * Copy the most recently selected color value to clipboard.
+ */
+ _onCopy: function () {
+ this.view.copySelection(this.styleDocument.popupNode);
+ },
+
+ /**
+ * Copy the most recently selected color value to clipboard.
+ */
+ _onCopyColor: function () {
+ clipboardHelper.copyString(this._colorToCopy);
+ },
+
+ /*
+ * Retrieve the url for the selected image and copy it to the clipboard
+ */
+ _onCopyUrl: function () {
+ if (!this._clickedNodeInfo) {
+ return;
+ }
+
+ clipboardHelper.copyString(this._clickedNodeInfo.value.url);
+ },
+
+ /**
+ * Retrieve the image data for the selected image url and copy it to the
+ * clipboard
+ */
+ _onCopyImageDataUrl: Task.async(function* () {
+ if (!this._clickedNodeInfo) {
+ return;
+ }
+
+ let message;
+ try {
+ let inspectorFront = this.inspector.inspector;
+ let imageUrl = this._clickedNodeInfo.value.url;
+ let data = yield inspectorFront.getImageDataFromURL(imageUrl);
+ message = yield data.data.string();
+ } catch (e) {
+ message =
+ STYLE_INSPECTOR_L10N.getStr("styleinspector.copyImageDataUrlError");
+ }
+
+ clipboardHelper.copyString(message);
+ }),
+
+ /**
+ * Show docs from MDN for a CSS property.
+ */
+ _onShowMdnDocs: function () {
+ let cssPropertyName = this.styleDocument.popupNode.textContent;
+ let anchor = this.styleDocument.popupNode.parentNode;
+ let cssDocsTooltip = this.view.tooltips.cssDocs;
+ cssDocsTooltip.show(anchor, cssPropertyName);
+ },
+
+ /**
+ * Add a new rule to the current element.
+ */
+ _onAddNewRule: function () {
+ this.view._onAddRule();
+ },
+
+ /**
+ * Copy the rule source location of the current clicked node.
+ */
+ _onCopyLocation: function () {
+ if (!this._clickedNodeInfo) {
+ return;
+ }
+
+ clipboardHelper.copyString(this._clickedNodeInfo.value);
+ },
+
+ /**
+ * Copy the rule property declaration of the current clicked node.
+ */
+ _onCopyPropertyDeclaration: function () {
+ if (!this._clickedNodeInfo) {
+ return;
+ }
+
+ let textProp = this._clickedNodeInfo.value.textProperty;
+ clipboardHelper.copyString(textProp.stringifyProperty());
+ },
+
+ /**
+ * Copy the rule property name of the current clicked node.
+ */
+ _onCopyPropertyName: function () {
+ if (!this._clickedNodeInfo) {
+ return;
+ }
+
+ clipboardHelper.copyString(this._clickedNodeInfo.value.property);
+ },
+
+ /**
+ * Copy the rule property value of the current clicked node.
+ */
+ _onCopyPropertyValue: function () {
+ if (!this._clickedNodeInfo) {
+ return;
+ }
+
+ clipboardHelper.copyString(this._clickedNodeInfo.value.value);
+ },
+
+ /**
+ * Copy the rule of the current clicked node.
+ */
+ _onCopyRule: function () {
+ let ruleEditor =
+ this.styleDocument.popupNode.parentNode.offsetParent._ruleEditor;
+ let rule = ruleEditor.rule;
+ clipboardHelper.copyString(rule.stringifyRule());
+ },
+
+ /**
+ * Copy the rule selector of the current clicked node.
+ */
+ _onCopySelector: function () {
+ if (!this._clickedNodeInfo) {
+ return;
+ }
+
+ clipboardHelper.copyString(this._clickedNodeInfo.value);
+ },
+
+ /**
+ * Toggle the original sources pref.
+ */
+ _onToggleOrigSources: function () {
+ let isEnabled = Services.prefs.getBoolPref(PREF_ORIG_SOURCES);
+ Services.prefs.setBoolPref(PREF_ORIG_SOURCES, !isEnabled);
+ },
+
+ destroy: function () {
+ this.popupNode = null;
+ this.styleDocument.popupNode = null;
+ this.view = null;
+ this.inspector = null;
+ this.styleDocument = null;
+ this.styleWindow = null;
+ }
+};
diff --git a/devtools/client/inspector/shared/test/.eslintrc.js b/devtools/client/inspector/shared/test/.eslintrc.js
new file mode 100644
index 000000000..698ae9181
--- /dev/null
+++ b/devtools/client/inspector/shared/test/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../../../.eslintrc.mochitests.js"
+};
diff --git a/devtools/client/inspector/shared/test/browser.ini b/devtools/client/inspector/shared/test/browser.ini
new file mode 100644
index 000000000..ce85ee80e
--- /dev/null
+++ b/devtools/client/inspector/shared/test/browser.ini
@@ -0,0 +1,41 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ doc_author-sheet.html
+ doc_content_stylesheet.html
+ doc_content_stylesheet.xul
+ doc_content_stylesheet_imported.css
+ doc_content_stylesheet_imported2.css
+ doc_content_stylesheet_linked.css
+ doc_content_stylesheet_script.css
+ doc_content_stylesheet_xul.css
+ doc_frame_script.js
+ head.js
+ !/devtools/client/commandline/test/helpers.js
+ !/devtools/client/framework/test/shared-head.js
+ !/devtools/client/inspector/test/head.js
+ !/devtools/client/inspector/test/shared-head.js
+ !/devtools/client/shared/test/test-actor.js
+ !/devtools/client/shared/test/test-actor-registry.js
+
+[browser_styleinspector_context-menu-copy-color_01.js]
+[browser_styleinspector_context-menu-copy-color_02.js]
+subsuite = clipboard
+[browser_styleinspector_context-menu-copy-urls.js]
+subsuite = clipboard
+[browser_styleinspector_csslogic-content-stylesheets.js]
+skip-if = e10s && debug # Bug 1250058 (docshell leak when opening 2 toolboxes)
+[browser_styleinspector_output-parser.js]
+[browser_styleinspector_refresh_when_active.js]
+[browser_styleinspector_tooltip-background-image.js]
+[browser_styleinspector_tooltip-closes-on-new-selection.js]
+skip-if = e10s # Bug 1111546 (e10s)
+[browser_styleinspector_tooltip-longhand-fontfamily.js]
+[browser_styleinspector_tooltip-multiple-background-images.js]
+[browser_styleinspector_tooltip-shorthand-fontfamily.js]
+[browser_styleinspector_tooltip-size.js]
+[browser_styleinspector_transform-highlighter-01.js]
+[browser_styleinspector_transform-highlighter-02.js]
+[browser_styleinspector_transform-highlighter-03.js]
+[browser_styleinspector_transform-highlighter-04.js]
diff --git a/devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-color_01.js b/devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-color_01.js
new file mode 100644
index 000000000..5a27edf16
--- /dev/null
+++ b/devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-color_01.js
@@ -0,0 +1,118 @@
+/* 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 "Copy color" item of the context menu #1: Test _isColorPopup.
+
+const TEST_URI = `
+ <div style="color:rgb(18, 58, 188);margin:0px;background:span[data-color];">
+ Test "Copy color" context menu option
+ </div>
+`;
+
+add_task(function* () {
+ // Test is slow on Linux EC2 instances - Bug 1137765
+ requestLongerTimeout(2);
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector} = yield openInspector();
+ yield testView("ruleview", inspector);
+ yield testView("computedview", inspector);
+});
+
+function* testView(viewId, inspector) {
+ info("Testing " + viewId);
+
+ yield inspector.sidebar.select(viewId);
+ let view = inspector[viewId].view || inspector[viewId].computedView;
+ yield selectNode("div", inspector);
+
+ testIsColorValueNode(view);
+ testIsColorPopupOnAllNodes(view);
+ yield clearCurrentNodeSelection(inspector);
+}
+
+/**
+ * A function testing that isColorValueNode correctly detects nodes part of
+ * color values.
+ */
+function testIsColorValueNode(view) {
+ info("Testing that child nodes of color nodes are detected.");
+ let root = rootElement(view);
+ let colorNode = root.querySelector("span[data-color]");
+
+ ok(colorNode, "Color node found");
+ for (let node of iterateNodes(colorNode)) {
+ ok(isColorValueNode(node), "Node is part of color value.");
+ }
+}
+
+/**
+ * A function testing that _isColorPopup returns a correct value for all nodes
+ * in the view.
+ */
+function testIsColorPopupOnAllNodes(view) {
+ let root = rootElement(view);
+ for (let node of iterateNodes(root)) {
+ testIsColorPopupOnNode(view, node);
+ }
+}
+
+/**
+ * Test result of _isColorPopup with given node.
+ * @param object view
+ * A CSSRuleView or CssComputedView instance.
+ * @param Node node
+ * A node to check.
+ */
+function testIsColorPopupOnNode(view, node) {
+ info("Testing node " + node);
+ view.styleDocument.popupNode = node;
+ view._contextmenu._colorToCopy = "";
+
+ let result = view._contextmenu._isColorPopup();
+ let correct = isColorValueNode(node);
+
+ is(result, correct, "_isColorPopup returned the expected value " + correct);
+ is(view._contextmenu._colorToCopy, (correct) ? "rgb(18, 58, 188)" : "",
+ "_colorToCopy was set to the expected value");
+}
+
+/**
+ * Check if a node is part of color value i.e. it has parent with a 'data-color'
+ * attribute.
+ */
+function isColorValueNode(node) {
+ let container = (node.nodeType == node.TEXT_NODE) ?
+ node.parentElement : node;
+
+ let isColorNode = el => el.dataset && "color" in el.dataset;
+
+ while (!isColorNode(container)) {
+ container = container.parentNode;
+ if (!container) {
+ info("No color. Node is not part of color value.");
+ return false;
+ }
+ }
+
+ info("Found a color. Node is part of color value.");
+
+ return true;
+}
+
+/**
+ * A generator that iterates recursively trough all child nodes of baseNode.
+ */
+function* iterateNodes(baseNode) {
+ yield baseNode;
+
+ for (let child of baseNode.childNodes) {
+ yield* iterateNodes(child);
+ }
+}
+
+/**
+ * Returns the root element for the given view, rule or computed.
+ */
+var rootElement = view => (view.element) ? view.element : view.styleDocument;
diff --git a/devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-color_02.js b/devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-color_02.js
new file mode 100644
index 000000000..afae7a2b6
--- /dev/null
+++ b/devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-color_02.js
@@ -0,0 +1,99 @@
+/* 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 "Copy color" item of the context menu #2: Test that correct color is
+// copied if the color changes.
+
+const TEST_URI = `
+ <style type="text/css">
+ div {
+ color: #123ABC;
+ }
+ </style>
+ <div>Testing the color picker tooltip!</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+
+ let {inspector, view} = yield openRuleView();
+
+ yield testCopyToClipboard(inspector, view);
+ yield testManualEdit(inspector, view);
+ yield testColorPickerEdit(inspector, view);
+});
+
+function* testCopyToClipboard(inspector, view) {
+ info("Testing that color is copied to clipboard");
+
+ yield selectNode("div", inspector);
+
+ let element = getRuleViewProperty(view, "div", "color").valueSpan
+ .querySelector(".ruleview-colorswatch");
+
+ let allMenuItems = openStyleContextMenuAndGetAllItems(view, element);
+ let menuitemCopyColor = allMenuItems.find(item => item.label ===
+ STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyColor"));
+
+ ok(menuitemCopyColor.visible, "Copy color is visible");
+
+ yield waitForClipboardPromise(() => menuitemCopyColor.click(),
+ "#123ABC");
+
+ EventUtils.synthesizeKey("VK_ESCAPE", { });
+}
+
+function* testManualEdit(inspector, view) {
+ info("Testing manually edited colors");
+ yield selectNode("div", inspector);
+
+ let {valueSpan} = getRuleViewProperty(view, "div", "color");
+
+ let newColor = "#C9184E";
+ let editor = yield focusEditableField(view, valueSpan);
+
+ info("Typing new value");
+ let input = editor.input;
+ let onBlur = once(input, "blur");
+ EventUtils.sendString(newColor + ";", view.styleWindow);
+ yield onBlur;
+ yield wait(1);
+
+ let colorValueElement = getRuleViewProperty(view, "div", "color")
+ .valueSpan.firstChild;
+ is(colorValueElement.dataset.color, newColor, "data-color was updated");
+
+ view.styleDocument.popupNode = colorValueElement;
+
+ let contextMenu = view._contextmenu;
+ contextMenu._isColorPopup();
+ is(contextMenu._colorToCopy, newColor, "_colorToCopy has the new value");
+}
+
+function* testColorPickerEdit(inspector, view) {
+ info("Testing colors edited via color picker");
+ yield selectNode("div", inspector);
+
+ let swatchElement = getRuleViewProperty(view, "div", "color").valueSpan
+ .querySelector(".ruleview-colorswatch");
+
+ info("Opening the color picker");
+ let picker = view.tooltips.colorPicker;
+ let onColorPickerReady = picker.once("ready");
+ swatchElement.click();
+ yield onColorPickerReady;
+
+ let rgbaColor = [83, 183, 89, 1];
+ let rgbaColorText = "rgba(83, 183, 89, 1)";
+ yield simulateColorPickerChange(view, picker, rgbaColor);
+
+ is(swatchElement.parentNode.dataset.color, rgbaColorText,
+ "data-color was updated");
+ view.styleDocument.popupNode = swatchElement;
+
+ let contextMenu = view._contextmenu;
+ contextMenu._isColorPopup();
+ is(contextMenu._colorToCopy, rgbaColorText, "_colorToCopy has the new value");
+}
diff --git a/devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-urls.js b/devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-urls.js
new file mode 100644
index 000000000..412137825
--- /dev/null
+++ b/devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-urls.js
@@ -0,0 +1,109 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* Tests both Copy URL and Copy Data URL context menu items */
+
+const TEST_DATA_URI = "data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=";
+
+// Invalid URL still needs to be reachable otherwise getImageDataUrl will
+// timeout. DevTools chrome:// URLs aren't content accessible, so use some
+// random resource:// URL here.
+const INVALID_IMAGE_URI = "resource://devtools/client/definitions.js";
+const ERROR_MESSAGE = STYLE_INSPECTOR_L10N.getStr("styleinspector.copyImageDataUrlError");
+
+add_task(function* () {
+ const TEST_URI = `<style type="text/css">
+ .valid-background {
+ background-image: url(${TEST_DATA_URI});
+ }
+ .invalid-background {
+ background-image: url(${INVALID_IMAGE_URI});
+ }
+ </style>
+ <div class="valid-background">Valid background image</div>
+ <div class="invalid-background">Invalid background image</div>`;
+
+ yield addTab("data:text/html;charset=utf8," + encodeURIComponent(TEST_URI));
+
+ yield startTest();
+});
+
+function* startTest() {
+ info("Opening rule view");
+ let {inspector, view} = yield openRuleView();
+
+ info("Test valid background image URL in rule view");
+ yield testCopyUrlToClipboard({view, inspector}, "data-uri",
+ ".valid-background", TEST_DATA_URI);
+ yield testCopyUrlToClipboard({view, inspector}, "url",
+ ".valid-background", TEST_DATA_URI);
+
+ info("Test invalid background image URL in rue view");
+ yield testCopyUrlToClipboard({view, inspector}, "data-uri",
+ ".invalid-background", ERROR_MESSAGE);
+ yield testCopyUrlToClipboard({view, inspector}, "url",
+ ".invalid-background", INVALID_IMAGE_URI);
+
+ info("Opening computed view");
+ view = selectComputedView(inspector);
+
+ info("Test valid background image URL in computed view");
+ yield testCopyUrlToClipboard({view, inspector}, "data-uri",
+ ".valid-background", TEST_DATA_URI);
+ yield testCopyUrlToClipboard({view, inspector}, "url",
+ ".valid-background", TEST_DATA_URI);
+
+ info("Test invalid background image URL in computed view");
+ yield testCopyUrlToClipboard({view, inspector}, "data-uri",
+ ".invalid-background", ERROR_MESSAGE);
+ yield testCopyUrlToClipboard({view, inspector}, "url",
+ ".invalid-background", INVALID_IMAGE_URI);
+}
+
+function* testCopyUrlToClipboard({view, inspector}, type, selector, expected) {
+ info("Select node in inspector panel");
+ yield selectNode(selector, inspector);
+
+ info("Retrieve background-image link for selected node in current " +
+ "styleinspector view");
+ let property = getBackgroundImageProperty(view, selector);
+ let imageLink = property.valueSpan.querySelector(".theme-link");
+ ok(imageLink, "Background-image link element found");
+
+ info("Simulate right click on the background-image URL");
+ let allMenuItems = openStyleContextMenuAndGetAllItems(view, imageLink);
+ let menuitemCopyUrl = allMenuItems.find(item => item.label ===
+ STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyUrl"));
+ let menuitemCopyImageDataUrl = allMenuItems.find(item => item.label ===
+ STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyImageDataUrl"));
+
+ info("Context menu is displayed");
+ ok(menuitemCopyUrl.visible,
+ "\"Copy URL\" menu entry is displayed");
+ ok(menuitemCopyImageDataUrl.visible,
+ "\"Copy Image Data-URL\" menu entry is displayed");
+
+ if (type == "data-uri") {
+ info("Click Copy Data URI and wait for clipboard");
+ yield waitForClipboardPromise(() => {
+ return menuitemCopyImageDataUrl.click();
+ }, expected);
+ } else {
+ info("Click Copy URL and wait for clipboard");
+ yield waitForClipboardPromise(() => {
+ return menuitemCopyUrl.click();
+ }, expected);
+ }
+
+ info("Hide context menu");
+}
+
+function getBackgroundImageProperty(view, selector) {
+ let isRuleView = view instanceof CssRuleView;
+ if (isRuleView) {
+ return getRuleViewProperty(view, selector, "background-image");
+ }
+ return getComputedViewProperty(view, "background-image");
+}
diff --git a/devtools/client/inspector/shared/test/browser_styleinspector_csslogic-content-stylesheets.js b/devtools/client/inspector/shared/test/browser_styleinspector_csslogic-content-stylesheets.js
new file mode 100644
index 000000000..421a2bb47
--- /dev/null
+++ b/devtools/client/inspector/shared/test/browser_styleinspector_csslogic-content-stylesheets.js
@@ -0,0 +1,82 @@
+/* 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 stylesheets on HMTL and XUL document
+
+// FIXME: this test opens the devtools for nothing, it should be changed into a
+// devtools/server/tests/mochitest/test_css-logic-...something...html
+// test
+
+const TEST_URI_HTML = TEST_URL_ROOT + "doc_content_stylesheet.html";
+const TEST_URI_AUTHOR = TEST_URL_ROOT + "doc_author-sheet.html";
+const TEST_URI_XUL = TEST_URL_ROOT + "doc_content_stylesheet.xul";
+const XUL_URI = Cc["@mozilla.org/network/io-service;1"]
+ .getService(Ci.nsIIOService)
+ .newURI(TEST_URI_XUL, null, null);
+var ssm = Components.classes["@mozilla.org/scriptsecuritymanager;1"]
+ .getService(Ci.nsIScriptSecurityManager);
+const XUL_PRINCIPAL = ssm.createCodebasePrincipal(XUL_URI, {});
+
+add_task(function* () {
+ requestLongerTimeout(2);
+
+ info("Checking stylesheets on HTML document");
+ yield addTab(TEST_URI_HTML);
+
+ let {inspector, testActor} = yield openInspector();
+ yield selectNode("#target", inspector);
+
+ info("Checking stylesheets");
+ yield checkSheets("#target", testActor);
+
+ info("Checking authored stylesheets");
+ yield addTab(TEST_URI_AUTHOR);
+
+ ({inspector} = yield openInspector());
+ yield selectNode("#target", inspector);
+ yield checkSheets("#target", testActor);
+
+ info("Checking stylesheets on XUL document");
+ info("Allowing XUL content");
+ allowXUL();
+ yield addTab(TEST_URI_XUL);
+
+ ({inspector} = yield openInspector());
+ yield selectNode("#target", inspector);
+
+ yield checkSheets("#target", testActor);
+ info("Disallowing XUL content");
+ disallowXUL();
+});
+
+function allowXUL() {
+ Cc["@mozilla.org/permissionmanager;1"].getService(Ci.nsIPermissionManager)
+ .addFromPrincipal(XUL_PRINCIPAL, "allowXULXBL",
+ Ci.nsIPermissionManager.ALLOW_ACTION);
+}
+
+function disallowXUL() {
+ Cc["@mozilla.org/permissionmanager;1"].getService(Ci.nsIPermissionManager)
+ .addFromPrincipal(XUL_PRINCIPAL, "allowXULXBL",
+ Ci.nsIPermissionManager.DENY_ACTION);
+}
+
+function* checkSheets(targetSelector, testActor) {
+ let sheets = yield testActor.getStyleSheetsInfoForNode(targetSelector);
+
+ for (let sheet of sheets) {
+ if (!sheet.href ||
+ /doc_content_stylesheet_/.test(sheet.href) ||
+ // For the "authored" case.
+ /^data:.*seagreen/.test(sheet.href)) {
+ ok(sheet.isContentSheet,
+ sheet.href + " identified as content stylesheet");
+ } else {
+ ok(!sheet.isContentSheet,
+ sheet.href + " identified as non-content stylesheet");
+ }
+ }
+}
diff --git a/devtools/client/inspector/shared/test/browser_styleinspector_output-parser.js b/devtools/client/inspector/shared/test/browser_styleinspector_output-parser.js
new file mode 100644
index 000000000..f1f846f5d
--- /dev/null
+++ b/devtools/client/inspector/shared/test/browser_styleinspector_output-parser.js
@@ -0,0 +1,341 @@
+/* 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 expected outputs of the output-parser's parseCssProperty function.
+
+// This is more of a unit test than a mochitest-browser test, but can't be
+// tested with an xpcshell test as the output-parser requires the DOM to work.
+
+const {OutputParser} = require("devtools/client/shared/output-parser");
+const {initCssProperties, getCssProperties} = require("devtools/shared/fronts/css-properties");
+
+const COLOR_CLASS = "color-class";
+const URL_CLASS = "url-class";
+const CUBIC_BEZIER_CLASS = "bezier-class";
+const ANGLE_CLASS = "angle-class";
+
+const TEST_DATA = [
+ {
+ name: "width",
+ value: "100%",
+ test: fragment => {
+ is(countAll(fragment), 0);
+ is(fragment.textContent, "100%");
+ }
+ },
+ {
+ name: "width",
+ value: "blue",
+ test: fragment => {
+ is(countAll(fragment), 0);
+ }
+ },
+ {
+ name: "content",
+ value: "'red url(test.png) repeat top left'",
+ test: fragment => {
+ is(countAll(fragment), 0);
+ }
+ },
+ {
+ name: "content",
+ value: "\"blue\"",
+ test: fragment => {
+ is(countAll(fragment), 0);
+ }
+ },
+ {
+ name: "margin-left",
+ value: "url(something.jpg)",
+ test: fragment => {
+ is(countAll(fragment), 0);
+ }
+ },
+ {
+ name: "background-color",
+ value: "transparent",
+ test: fragment => {
+ is(countAll(fragment), 2);
+ is(countColors(fragment), 1);
+ is(fragment.textContent, "transparent");
+ }
+ },
+ {
+ name: "color",
+ value: "red",
+ test: fragment => {
+ is(countColors(fragment), 1);
+ is(fragment.textContent, "red");
+ }
+ },
+ {
+ name: "color",
+ value: "#F06",
+ test: fragment => {
+ is(countColors(fragment), 1);
+ is(fragment.textContent, "#F06");
+ }
+ },
+ {
+ name: "border",
+ value: "80em dotted pink",
+ test: fragment => {
+ is(countAll(fragment), 2);
+ is(countColors(fragment), 1);
+ is(getColor(fragment), "pink");
+ }
+ },
+ {
+ name: "color",
+ value: "red !important",
+ test: fragment => {
+ is(countColors(fragment), 1);
+ is(fragment.textContent, "red !important");
+ }
+ },
+ {
+ name: "background",
+ value: "red url(test.png) repeat top left",
+ test: fragment => {
+ is(countColors(fragment), 1);
+ is(countUrls(fragment), 1);
+ is(getColor(fragment), "red");
+ is(getUrl(fragment), "test.png");
+ is(countAll(fragment), 3);
+ }
+ },
+ {
+ name: "background",
+ value: "blue url(test.png) repeat top left !important",
+ test: fragment => {
+ is(countColors(fragment), 1);
+ is(countUrls(fragment), 1);
+ is(getColor(fragment), "blue");
+ is(getUrl(fragment), "test.png");
+ is(countAll(fragment), 3);
+ }
+ },
+ {
+ name: "list-style-image",
+ value: "url(\"images/arrow.gif\")",
+ test: fragment => {
+ is(countAll(fragment), 1);
+ is(getUrl(fragment), "images/arrow.gif");
+ }
+ },
+ {
+ name: "list-style-image",
+ value: "url(\"images/arrow.gif\")!important",
+ test: fragment => {
+ is(countAll(fragment), 1);
+ is(getUrl(fragment), "images/arrow.gif");
+ is(fragment.textContent, "url(\"images/arrow.gif\")!important");
+ }
+ },
+ {
+ name: "-moz-binding",
+ value: "url(http://somesite.com/path/to/binding.xml#someid)",
+ test: fragment => {
+ is(countAll(fragment), 1);
+ is(countUrls(fragment), 1);
+ is(getUrl(fragment), "http://somesite.com/path/to/binding.xml#someid");
+ }
+ },
+ {
+ name: "background",
+ value: "linear-gradient(to right, rgba(183,222,237,1) 0%, " +
+ "rgba(33,180,226,1) 30%, rgba(31,170,217,.5) 44%, " +
+ "#F06 75%, red 100%)",
+ test: fragment => {
+ is(countAll(fragment), 10);
+ let allSwatches = fragment.querySelectorAll("." + COLOR_CLASS);
+ is(allSwatches.length, 5);
+ is(allSwatches[0].textContent, "rgba(183,222,237,1)");
+ is(allSwatches[1].textContent, "rgba(33,180,226,1)");
+ is(allSwatches[2].textContent, "rgba(31,170,217,.5)");
+ is(allSwatches[3].textContent, "#F06");
+ is(allSwatches[4].textContent, "red");
+ }
+ },
+ {
+ name: "background",
+ value: "-moz-radial-gradient(center 45deg, circle closest-side, " +
+ "orange 0%, red 100%)",
+ test: fragment => {
+ is(countAll(fragment), 6);
+ let colorSwatches = fragment.querySelectorAll("." + COLOR_CLASS);
+ is(colorSwatches.length, 2);
+ is(colorSwatches[0].textContent, "orange");
+ is(colorSwatches[1].textContent, "red");
+ let angleSwatches = fragment.querySelectorAll("." + ANGLE_CLASS);
+ is(angleSwatches.length, 1);
+ is(angleSwatches[0].textContent, "45deg");
+ }
+ },
+ {
+ name: "background",
+ value: "white url(http://test.com/wow_such_image.png) no-repeat top left",
+ test: fragment => {
+ is(countAll(fragment), 3);
+ is(countUrls(fragment), 1);
+ is(countColors(fragment), 1);
+ }
+ },
+ {
+ name: "background",
+ value: "url(\"http://test.com/wow_such_(oh-noes)image.png?testid=1&color=red#w00t\")",
+ test: fragment => {
+ is(countAll(fragment), 1);
+ is(getUrl(fragment), "http://test.com/wow_such_(oh-noes)image.png?testid=1&color=red#w00t");
+ }
+ },
+ {
+ name: "background-image",
+ value: "url(this-is-an-incredible-image.jpeg)",
+ test: fragment => {
+ is(countAll(fragment), 1);
+ is(getUrl(fragment), "this-is-an-incredible-image.jpeg");
+ }
+ },
+ {
+ name: "background",
+ value: "red url( \"http://wow.com/cool/../../../you're(doingit)wrong\" ) repeat center",
+ test: fragment => {
+ is(countAll(fragment), 3);
+ is(countColors(fragment), 1);
+ is(getUrl(fragment), "http://wow.com/cool/../../../you're(doingit)wrong");
+ }
+ },
+ {
+ name: "background-image",
+ value: "url(../../../look/at/this/folder/structure/../" +
+ "../red.blue.green.svg )",
+ test: fragment => {
+ is(countAll(fragment), 1);
+ is(getUrl(fragment), "../../../look/at/this/folder/structure/../" +
+ "../red.blue.green.svg");
+ }
+ },
+ {
+ name: "transition-timing-function",
+ value: "linear",
+ test: fragment => {
+ is(countCubicBeziers(fragment), 1);
+ is(getCubicBezier(fragment), "linear");
+ }
+ },
+ {
+ name: "animation-timing-function",
+ value: "ease-in-out",
+ test: fragment => {
+ is(countCubicBeziers(fragment), 1);
+ is(getCubicBezier(fragment), "ease-in-out");
+ }
+ },
+ {
+ name: "animation-timing-function",
+ value: "cubic-bezier(.1, 0.55, .9, -3.45)",
+ test: fragment => {
+ is(countCubicBeziers(fragment), 1);
+ is(getCubicBezier(fragment), "cubic-bezier(.1, 0.55, .9, -3.45)");
+ }
+ },
+ {
+ name: "animation",
+ value: "move 3s cubic-bezier(.1, 0.55, .9, -3.45)",
+ test: fragment => {
+ is(countCubicBeziers(fragment), 1);
+ is(getCubicBezier(fragment), "cubic-bezier(.1, 0.55, .9, -3.45)");
+ }
+ },
+ {
+ name: "transition",
+ value: "top 1s ease-in",
+ test: fragment => {
+ is(countCubicBeziers(fragment), 1);
+ is(getCubicBezier(fragment), "ease-in");
+ }
+ },
+ {
+ name: "transition",
+ value: "top 3s steps(4, end)",
+ test: fragment => {
+ is(countAll(fragment), 0);
+ }
+ },
+ {
+ name: "transition",
+ value: "top 3s step-start",
+ test: fragment => {
+ is(countAll(fragment), 0);
+ }
+ },
+ {
+ name: "transition",
+ value: "top 3s step-end",
+ test: fragment => {
+ is(countAll(fragment), 0);
+ }
+ },
+ {
+ name: "background",
+ value: "rgb(255, var(--g-value), 192)",
+ test: fragment => {
+ is(fragment.textContent, "rgb(255, var(--g-value), 192)");
+ }
+ },
+ {
+ name: "background",
+ value: "rgb(255, var(--g-value, 0), 192)",
+ test: fragment => {
+ is(fragment.textContent, "rgb(255, var(--g-value, 0), 192)");
+ }
+ }
+];
+
+add_task(function* () {
+ // Mock the toolbox that initCssProperties expect so we get the fallback css properties.
+ let toolbox = {target: {client: {}, hasActor: () => false}};
+ yield initCssProperties(toolbox);
+ let cssProperties = getCssProperties(toolbox);
+
+ let parser = new OutputParser(document, cssProperties);
+ for (let i = 0; i < TEST_DATA.length; i++) {
+ let data = TEST_DATA[i];
+ info("Output-parser test data " + i + ". {" + data.name + " : " +
+ data.value + ";}");
+ data.test(parser.parseCssProperty(data.name, data.value, {
+ colorClass: COLOR_CLASS,
+ urlClass: URL_CLASS,
+ bezierClass: CUBIC_BEZIER_CLASS,
+ angleClass: ANGLE_CLASS,
+ defaultColorType: false
+ }));
+ }
+});
+
+function countAll(fragment) {
+ return fragment.querySelectorAll("*").length;
+}
+function countColors(fragment) {
+ return fragment.querySelectorAll("." + COLOR_CLASS).length;
+}
+function countUrls(fragment) {
+ return fragment.querySelectorAll("." + URL_CLASS).length;
+}
+function countCubicBeziers(fragment) {
+ return fragment.querySelectorAll("." + CUBIC_BEZIER_CLASS).length;
+}
+function getColor(fragment, index) {
+ return fragment.querySelectorAll("." + COLOR_CLASS)[index||0].textContent;
+}
+function getUrl(fragment, index) {
+ return fragment.querySelectorAll("." + URL_CLASS)[index||0].textContent;
+}
+function getCubicBezier(fragment, index) {
+ return fragment.querySelectorAll("." + CUBIC_BEZIER_CLASS)[index||0]
+ .textContent;
+}
diff --git a/devtools/client/inspector/shared/test/browser_styleinspector_refresh_when_active.js b/devtools/client/inspector/shared/test/browser_styleinspector_refresh_when_active.js
new file mode 100644
index 000000000..942fe05e2
--- /dev/null
+++ b/devtools/client/inspector/shared/test/browser_styleinspector_refresh_when_active.js
@@ -0,0 +1,43 @@
+/* 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 style-inspector views only refresh when they are active.
+
+const TEST_URI = `
+ <div id="one" style="color:red;">one</div>
+ <div id="two" style="color:blue;">two</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+
+ yield selectNode("#one", inspector);
+
+ is(getRuleViewPropertyValue(view, "element", "color"), "red",
+ "The rule-view shows the properties for test node one");
+
+ let cView = inspector.computedview.computedView;
+ let prop = getComputedViewProperty(cView, "color");
+ ok(!prop, "The computed-view doesn't show the properties for test node one");
+
+ info("Switching to the computed-view");
+ let onComputedViewReady = inspector.once("computed-view-refreshed");
+ selectComputedView(inspector);
+ yield onComputedViewReady;
+
+ ok(getComputedViewPropertyValue(cView, "color"), "#F00",
+ "The computed-view shows the properties for test node one");
+
+ info("Selecting test node two");
+ yield selectNode("#two", inspector);
+
+ ok(getComputedViewPropertyValue(cView, "color"), "#00F",
+ "The computed-view shows the properties for test node two");
+
+ is(getRuleViewPropertyValue(view, "element", "color"), "red",
+ "The rule-view doesn't the properties for test node two");
+});
diff --git a/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-background-image.js b/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-background-image.js
new file mode 100644
index 000000000..bd467b800
--- /dev/null
+++ b/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-background-image.js
@@ -0,0 +1,125 @@
+/* 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 background-image URLs have image preview tooltips in the rule-view
+// and computed-view
+
+const TEST_URI = `
+ <style type="text/css">
+ body {
+ padding: 1em;
+ background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAADI5JREFUeNrsWwuQFNUVPf1m5z87szv7HWSWj8CigBFMEFZKiQsB1PgJwUAZg1HBpIQsKmokEhNjWUnFVPnDWBT+KolJYbRMoqUVq0yCClpqiX8sCchPWFwVlt2db7+X93pez7zu6Vn2NxsVWh8987p7pu+9555z7+tZjTGGY3kjOMa34w447oBjfKsY7i/UNM3Y8eFSAkD50Plgw03K5P9gvGv7U5ieeR3PszeREiPNX3/0DL4hjslzhm8THh+OITfXk3dhiv4GDtGPVzCaeJmPLYzuu5qJuWfuw2QTlcN1X9pwQU7LhdZ/ZAseD45cOh9hHvDkc/yAF/DNhdb5Mrr3PvBMaAYW8fMSIi2G497IMEK/YutGtAYr6+ej+nxu/NN8Ks3N7AR6HgcLz0Eg1Ljg1UcxZzi5qewIkMYLRweTr2Kzp+nmyXAd5pS3XQDd+N/4h4zgu9FI7brlXf90nMEnuwQxlvv+hosE3TuexmWeysmT4W+WxkMaLzf9Y8ATgjcUn7T9H1gqrpFq8eV1gMn6t16NhngjfoX6q4DUP032Rd4LJgpSLwJ1yzFqBG69eRkah0MVyo0Acfe+yy9AG4nMiYCkeM53KKFXncBLAXqEm+wCqZwaueq7WCmuLTcKSJmj737ol2hurA9eq9VdyiO8yWa3NNyog+SB5CZodSsQq/dfu34tJpYbBaTMzvVddDZu16q5smXf4G8zEvqm4cyaAmJPuTJk3oJWdS4WzcVtfMZbThSQckb/pYfRGgo3zNOqZnEHbJPGK4abaDCQIIsT8V/qTaBqHkLh6LzXH8XZQhbLhYKyyCC/WeHYcNdmvOgfe8skzbWL270/T3wf7tSx/lGCbTu8xlzzmCSWLc5iwmgikcCHi3Mga0Ry913vBFvQwg90l6M4ImWKfsWOp7DSWxmfpPlCFuPFfsNfKrCnPYpQKIRgqBK7D0SxYaNHwkEiJMtl0ReDp3Lc5D3PGoTo/sKngCl7a5chFqvBatKwjBd7WwqIlzB/78NcoUcp5VSgGxm+7b8eqQRGnHMO634epO4S1EZww09/iFg5UmGoESDuznP1xVhTUX1WWHPzjpd25wyH0hRxI3LGM75nxmuNEEUVpAN0XgxmPoKralakbQnWlIMQyVBD/w+3orkq4lvualjKyWwzt4MaxqspQHVhPOWG64bxYuhZXSFGWhipbSDVragOu5Y9eAsmDDUKyBA703vemVhHoueD6e9wAzJK1WfmN0Umk5GGM4kEMZcuIECqgjm0nldAqmbjwtm4VxZH5AvlADP6mx9Eqy9Q0+KqW8Ch+47FaMMYmnNGfY1iPMshoC6qFxme4wQ+0p+ARE6H3+9veWEDWgUhDhUKyFARn4jM5BNxT0XsMg7bfymGK1ov3wtjDfhL4w0HVGUVBEjDaaE+QNdrcNWch1PG4W6xrjBUXECGivg++Cva3JUT4iQUz3V2RsSVaKLwOuDT89A3HdBQoxhNC+fnVm74ual2EG893P6G+PuP4SfiO4cCBWQooL9qCWKNXPbcI37Aa/lnlZxXRt4RFONGwSDCPAHqOuqjWct1QiEMw5mChM5X4K47FyNqcd3aK9AwFH0CGYLoe1ctxk2eWi57rg5JfGp9rzC6ggCdFlAgHBDw5Yxlcg6G8SyHCjMlsgmDD9zhSeHlF+JnAgWDTQUy2NxfdwOao1UVV3pi3+bE97YSbWpLAbn6zefHNQkp1PMpIBwwvslKgIYTKM2nEpNzrGcH3FXTEal0L38kJ4uDQgEZbO4vnI173LXf5NHZaiUxtaCxyZuo/rK6LpUg54yg3zTWRAArvDcRIPZ6BqzrQ1REpmL+DNw32OKIDCb3X1qPVn8wNNMT4w2bvs+q4bAZrqBh2skaL3yyhhIIZ4i6oHkUK0RckcB8GigEyRIH4A6Mgc8fatl0/+BkkQxC9gIT4ljna1rIZW9rEdNbjJcNjsnoYj7LHWCUwpITzEgzRQKZ3XAFHbTzA3hrz8TEUUZxFBhoKpABQt/97p+w0hMZG68I8R6FtlsJT3FELndZntjM+VMnylKYq8GJI3UZaRMpquGSGFVOEfv0YZBMNzz+uvjbfzS6xQERIhlI9FcvQWNdFVb7x1zCb+QNK8vb9NsiifmI5hBgVoOCBC1sb0ab5RomqENxLO3eA1/0NDRU47q2RQNbRCUDIb7lF2CNL3ZGxEV4n08TVvZWYG4pZyV0zUdS45tyCBByOHWiyvZmxFXDCyRo1ge5+Sy0TA+8lWMiP/6O0S32exGV9Jf4fr8azdUR3zL/CZz4MtvzdX5uOYs6NDOmpkuj5Huh+7qUQSYl0ThHzw0YQzcGo6bhzEqoYq5rN3yRiYiG3Vfe2Ybm/qKA9NNZ3nNm4F7/yDkg9AN+U1mHiBcXP8zuDN76jj8hg1QyiWQigalj02BJPhK8I0zxijAjhp5zhlpLUDvS+BCy2HMAvvB4XDgL9/SXC0g/ou/5+6/xLX8w0uJrOIkXfPvyhY0F6gr7M8H0KWFYikcqAXakB+xwD9CdREBLoau7Gz3cAdSIdLFxFtJTCqRChSjnutvhDcREtzjz2Tswtz+yeNRFUeXZXtWux7C1fuoVcbd3J//ipDX3uZZDLGrwweS+UBLL5TDliVBnF8P7H+XI8aRRGsIBJg/Zlslt1+W+D1JWoSyi+kD9jfhs78t7mhZhSl+fLfY1Bdyv3I8V/qpY3B1McgN7ZFT5/vNO0I5DPLLdPBIJA8qc4h2I0QplYfDpJwHT+aj0246r5S8rToG8OjCle8wk4OLvvYGa+Ovr84uo2qBSwJS9G5egoZFLTfiEqWDtbwGfHgKOdPHcS+ai7XDzMPW/FJRLGGcxnBbK4YJC2K+h+T6Bdu5CqHqCWERd3bawb7JI+iJ735+LNaHaprBLLHBm08U3XxShEsdt+f3eTh3v7aC95Dct4RCWL5OZWh/oXBZThxAIxyOXLzBk8aiEWJID8rK3CpPOmeHaGpvCS+7EHv5FujVHUSJPLXvIFeHcNc+9xrB2gws9KZdxuLFax/WLM5gzzSm/lTXF/OdAcapyvjxPqxqHjr2v4ckX2bS2dRBrc5lSdpKjEJ9/9tdwX2WMd53ZQ2IVo3RES+UwVSpCPvYepNx4gmTGDUKIMQ4eduPnD7mx9xOn/KZKOlFbStjONxHTtR+BYAPmnoZ1Zp8wkBRwP/EL3u0F/C2hGl7vpz7vW37T3vP7if8wroKuoh8ribknX9BK5rcF+mo1qKaKyRPJTgTDjbzY8szcuLb3bpH00u35T47j7prRpwDJTxzyG0dHgxPp5bPG8VdkpfPbUg3SgoOo2mwVukb98D5EqpswZTTulCggTk4gpYhv0++wIhCJxr0+Hq1sondis0SE2oxQe3qWXwWyO4DSQg9gJ8Iiw1VFcGqXxet0N9xE4ygIxv/9W6wo9WyROEX/R+eiobYSq2vHTOR631Eiv2lRfh9dvxkumkXh92Qsx8XrAJ+7YGbWuhxOi/U+31NQmzyqNYG8N/3wfo6CRtRHcN01FzkvojohwLu0VVvDa56IS/xcj2b7nN+O+m0jqpE1wMPXZxAN9iCVThtDvH7gmiRGRpU8Lspv1Uhq4wIVdQoyuGSLNYPKUCS8+CzNURbzMmjK3i8u0U793lmuV0ef9nWQ5MGC/DiUqEUSaCtXna9RJEspZS1lrXINK/pcq+SpT50t98QKMq1FRmDfx3vxty102k0PM4ssEnvuz5+G26Ij4yDpz6z9fV8bkyIkqBFkhej0Ib+ZQ34XJK9AfozaiimqIoX3Jp3tiISrcfYpuN2+iFph/02P36PNC9fVcCnp6H9jYouKyfaWufz5Tp9tVxcUniw7IohZv4dZz81/ns67z3AYPrc2n0+Ix2q8k0PWjgBy88XaibnfK9A+5LdDY2Ivhy36fbT8Zv3Lb1U1qLqUxorXEEXIs0mjjrtxoTZWtdvigNs2sgPiujTv6DIZLld6b/V5742JZV3fUsUVFy5gdsNtKWFzUCEVbNepD1MkSMVbsb6SZm7jI3/zODtQKgUMsOw8wDZ63t5xcV1TnaEAxoc6wrqY+Fj+N4DsqOnhOIdicrQSm1MPYCPlIqHn5bbHg8/bj2D3QfZnCX3mpAICDZV8jH5kpbZqTD0W+DxaA74CWzLN2nd14OlL72J38Lf7+TjC7dadZFDoZJQPrtaIKL/G0L6ktptPZVJ8fMqHYPZOKYPMyQGadIJfDvdXwAFiZOTvDBPydf5vk4rWA+RfdhBlaF/yDDBRoMu9pfnSjv/p7DG+HXfAcQcc49v/BBgAcFAO4DmB2GQAAAAASUVORK5CYII=);
+ background-repeat: repeat-y;
+ background-position: right top;
+ }
+ .test-element {
+ font-family: verdana;
+ color: #333;
+ background: url(chrome://global/skin/icons/warning-64.png) no-repeat left center;
+ padding-left: 70px;
+ }
+ </style>
+ <div class="test-element">test element</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+
+ info("Testing the background-image property on the body rule");
+ yield testBodyRuleView(view);
+
+ info("Selecting the test div node");
+ yield selectNode(".test-element", inspector);
+ info("Testing the the background property on the .test-element rule");
+ yield testDivRuleView(view);
+
+ info("Testing that image preview tooltips show even when there are " +
+ "fields being edited");
+ yield testTooltipAppearsEvenInEditMode(view);
+
+ info("Switching over to the computed-view");
+ let onComputedViewReady = inspector.once("computed-view-refreshed");
+ view = selectComputedView(inspector);
+ yield onComputedViewReady;
+
+ info("Testing that the background-image computed style has a tooltip too");
+ yield testComputedView(view);
+});
+
+function* testBodyRuleView(view) {
+ info("Testing tooltips in the rule view");
+ let panel = view.tooltips.previewTooltip.panel;
+
+ // Check that the rule view has a tooltip and that a XUL panel has
+ // been created
+ ok(view.tooltips.previewTooltip, "Tooltip instance exists");
+ ok(panel, "XUL panel exists");
+
+ // Get the background-image property inside the rule view
+ let {valueSpan} = getRuleViewProperty(view, "body", "background-image");
+ let uriSpan = valueSpan.querySelector(".theme-link");
+
+ yield assertHoverTooltipOn(view.tooltips.previewTooltip, uriSpan);
+
+ let images = panel.getElementsByTagName("img");
+ is(images.length, 1, "Tooltip contains an image");
+ ok(images[0].getAttribute("src")
+ .indexOf("iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHe") !== -1,
+ "The image URL seems fine");
+}
+
+function* testDivRuleView(view) {
+ let panel = view.tooltips.previewTooltip.panel;
+
+ // Get the background property inside the rule view
+ let {valueSpan} = getRuleViewProperty(view, ".test-element", "background");
+ let uriSpan = valueSpan.querySelector(".theme-link");
+
+ yield assertHoverTooltipOn(view.tooltips.previewTooltip, uriSpan);
+
+ let images = panel.getElementsByTagName("img");
+ is(images.length, 1, "Tooltip contains an image");
+ ok(images[0].getAttribute("src").startsWith("data:"),
+ "Tooltip contains a data-uri image as expected");
+}
+
+function* testTooltipAppearsEvenInEditMode(view) {
+ info("Switching to edit mode in the rule view");
+ let editor = yield turnToEditMode(view);
+
+ info("Now trying to show the preview tooltip");
+ let {valueSpan} = getRuleViewProperty(view, ".test-element", "background");
+ let uriSpan = valueSpan.querySelector(".theme-link");
+ yield assertHoverTooltipOn(view.tooltips.previewTooltip, uriSpan);
+
+ is(view.styleDocument.activeElement, editor.input,
+ "Tooltip was shown in edit mode, and inplace-editor still focused");
+}
+
+function turnToEditMode(ruleView) {
+ let brace = ruleView.styleDocument.querySelector(".ruleview-ruleclose");
+ return focusEditableField(ruleView, brace);
+}
+
+function* testComputedView(view) {
+ let tooltip = view.tooltips.previewTooltip;
+ ok(tooltip, "The computed-view has a tooltip defined");
+
+ let panel = tooltip.panel;
+ ok(panel, "The computed-view tooltip has a XUL panel");
+
+ let {valueSpan} = getComputedViewProperty(view, "background-image");
+ let uriSpan = valueSpan.querySelector(".theme-link");
+
+ yield assertHoverTooltipOn(view.tooltips.previewTooltip, uriSpan);
+
+ let images = panel.getElementsByTagName("img");
+ is(images.length, 1, "Tooltip contains an image");
+
+ ok(images[0].getAttribute("src").startsWith("data:"),
+ "Tooltip contains a data-uri in the computed-view too");
+}
diff --git a/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-closes-on-new-selection.js b/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-closes-on-new-selection.js
new file mode 100644
index 000000000..7f15d4fbe
--- /dev/null
+++ b/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-closes-on-new-selection.js
@@ -0,0 +1,73 @@
+/* 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 if a tooltip is visible when a new selection is made, it closes
+
+const TEST_URI = "<div class='one'>el 1</div><div class='two'>el 2</div>";
+const XHTML_NS = "http://www.w3.org/1999/xhtml";
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode(".one", inspector);
+
+ info("Testing rule view tooltip closes on new selection");
+ yield testRuleView(view, inspector);
+
+ info("Testing computed view tooltip closes on new selection");
+ view = selectComputedView(inspector);
+ yield testComputedView(view, inspector);
+});
+
+function* testRuleView(ruleView, inspector) {
+ info("Showing the tooltip");
+
+ let tooltip = ruleView.tooltips.previewTooltip;
+ let tooltipContent = ruleView.styleDocument.createElementNS(XHTML_NS, "div");
+ yield tooltip.setContent(tooltipContent, {width: 100, height: 30});
+
+ // Stop listening for mouse movements because it's not needed for this test,
+ // and causes intermittent failures on Linux. When this test runs in the suite
+ // sometimes a mouseleave event is dispatched at the start, which causes the
+ // tooltip to hide in the middle of being shown, which causes timeouts later.
+ tooltip.stopTogglingOnHover();
+
+ let onShown = tooltip.once("shown");
+ tooltip.show(ruleView.styleDocument.firstElementChild);
+ yield onShown;
+
+ info("Selecting a new node");
+ let onHidden = tooltip.once("hidden");
+ yield selectNode(".two", inspector);
+ yield onHidden;
+
+ ok(true, "Rule view tooltip closed after a new node got selected");
+}
+
+function* testComputedView(computedView, inspector) {
+ info("Showing the tooltip");
+
+ let tooltip = computedView.tooltips.previewTooltip;
+ let tooltipContent = computedView.styleDocument.createElementNS(XHTML_NS, "div");
+ yield tooltip.setContent(tooltipContent, {width: 100, height: 30});
+
+ // Stop listening for mouse movements because it's not needed for this test,
+ // and causes intermittent failures on Linux. When this test runs in the suite
+ // sometimes a mouseleave event is dispatched at the start, which causes the
+ // tooltip to hide in the middle of being shown, which causes timeouts later.
+ tooltip.stopTogglingOnHover();
+
+ let onShown = tooltip.once("shown");
+ tooltip.show(computedView.styleDocument.firstElementChild);
+ yield onShown;
+
+ info("Selecting a new node");
+ let onHidden = tooltip.once("hidden");
+ yield selectNode(".one", inspector);
+ yield onHidden;
+
+ ok(true, "Computed view tooltip closed after a new node got selected");
+}
diff --git a/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-longhand-fontfamily.js b/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-longhand-fontfamily.js
new file mode 100644
index 000000000..6bce367ae
--- /dev/null
+++ b/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-longhand-fontfamily.js
@@ -0,0 +1,120 @@
+/* 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 fontfamily tooltip on longhand properties
+
+const TEST_URI = `
+ <style type="text/css">
+ #testElement {
+ font-family: cursive;
+ color: #333;
+ padding-left: 70px;
+ }
+ </style>
+ <div id="testElement">test element</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testElement", inspector);
+ yield testRuleView(view, inspector.selection.nodeFront);
+
+ info("Opening the computed view");
+ let onComputedViewReady = inspector.once("computed-view-refreshed");
+ view = selectComputedView(inspector);
+ yield onComputedViewReady;
+
+ yield testComputedView(view, inspector.selection.nodeFront);
+
+ yield testExpandedComputedViewProperty(view, inspector.selection.nodeFront);
+});
+
+function* testRuleView(ruleView, nodeFront) {
+ info("Testing font-family tooltips in the rule view");
+
+ let tooltip = ruleView.tooltips.previewTooltip;
+ let panel = tooltip.panel;
+
+ // Check that the rule view has a tooltip and that a XUL panel has
+ // been created
+ ok(tooltip, "Tooltip instance exists");
+ ok(panel, "XUL panel exists");
+
+ // Get the font family property inside the rule view
+ let {valueSpan} = getRuleViewProperty(ruleView, "#testElement",
+ "font-family");
+
+ // And verify that the tooltip gets shown on this property
+ yield assertHoverTooltipOn(tooltip, valueSpan);
+
+ let images = panel.getElementsByTagName("img");
+ is(images.length, 1, "Tooltip contains an image");
+ ok(images[0].getAttribute("src").startsWith("data:"),
+ "Tooltip contains a data-uri image as expected");
+
+ let dataURL = yield getFontFamilyDataURL(valueSpan.textContent, nodeFront);
+ is(images[0].getAttribute("src"), dataURL,
+ "Tooltip contains the correct data-uri image");
+}
+
+function* testComputedView(computedView, nodeFront) {
+ info("Testing font-family tooltips in the computed view");
+
+ let tooltip = computedView.tooltips.previewTooltip;
+ let panel = tooltip.panel;
+ let {valueSpan} = getComputedViewProperty(computedView, "font-family");
+
+ yield assertHoverTooltipOn(tooltip, valueSpan);
+
+ let images = panel.getElementsByTagName("img");
+ is(images.length, 1, "Tooltip contains an image");
+ ok(images[0].getAttribute("src").startsWith("data:"),
+ "Tooltip contains a data-uri image as expected");
+
+ let dataURL = yield getFontFamilyDataURL(valueSpan.textContent, nodeFront);
+ is(images[0].getAttribute("src"), dataURL,
+ "Tooltip contains the correct data-uri image");
+}
+
+function* testExpandedComputedViewProperty(computedView, nodeFront) {
+ info("Testing font-family tooltips in expanded properties of the " +
+ "computed view");
+
+ info("Expanding the font-family property to reveal matched selectors");
+ let propertyView = getPropertyView(computedView, "font-family");
+ propertyView.matchedExpanded = true;
+ yield propertyView.refreshMatchedSelectors();
+
+ let valueSpan = propertyView.matchedSelectorsContainer
+ .querySelector(".bestmatch .other-property-value");
+
+ let tooltip = computedView.tooltips.previewTooltip;
+ let panel = tooltip.panel;
+
+ yield assertHoverTooltipOn(tooltip, valueSpan);
+
+ let images = panel.getElementsByTagName("img");
+ is(images.length, 1, "Tooltip contains an image");
+ ok(images[0].getAttribute("src").startsWith("data:"),
+ "Tooltip contains a data-uri image as expected");
+
+ let dataURL = yield getFontFamilyDataURL(valueSpan.textContent, nodeFront);
+ is(images[0].getAttribute("src"), dataURL,
+ "Tooltip contains the correct data-uri image");
+}
+
+function getPropertyView(computedView, name) {
+ let propertyView = null;
+ computedView.propertyViews.some(function (view) {
+ if (view.name == name) {
+ propertyView = view;
+ return true;
+ }
+ return false;
+ });
+ return propertyView;
+}
diff --git a/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-multiple-background-images.js b/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-multiple-background-images.js
new file mode 100644
index 000000000..60d747a45
--- /dev/null
+++ b/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-multiple-background-images.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 for bug 1026921: Ensure the URL of hovered url() node is used instead
+// of the first found from the declaration as there might be multiple urls.
+
+const YELLOW_DOT = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gYcDCwCr0o5ngAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAAANSURBVAjXY/j/n6EeAAd9An7Z55GEAAAAAElFTkSuQmCC";
+const BLUE_DOT = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gYcDCwlCkCM9QAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAAANSURBVAjXY2Bg+F8PAAKCAX/tPkrkAAAAAElFTkSuQmCC";
+const TEST_STYLE = `h1 {background: url(${YELLOW_DOT}), url(${BLUE_DOT});}`;
+const TEST_URI = `<style>${TEST_STYLE}</style><h1>test element</h1>`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector} = yield openInspector();
+
+ yield testRuleViewUrls(inspector);
+ yield testComputedViewUrls(inspector);
+});
+
+function* testRuleViewUrls(inspector) {
+ info("Testing tooltips in the rule view");
+ let view = selectRuleView(inspector);
+ yield selectNode("h1", inspector);
+
+ let {valueSpan} = getRuleViewProperty(view, "h1", "background");
+ yield performChecks(view, valueSpan);
+}
+
+function* testComputedViewUrls(inspector) {
+ info("Testing tooltips in the computed view");
+
+ let onComputedViewReady = inspector.once("computed-view-refreshed");
+ let view = selectComputedView(inspector);
+ yield onComputedViewReady;
+
+ let {valueSpan} = getComputedViewProperty(view, "background-image");
+
+ yield performChecks(view, valueSpan);
+}
+
+/**
+ * A helper that checks url() tooltips contain correct images
+ */
+function* performChecks(view, propertyValue) {
+ function checkTooltip(panel, imageSrc) {
+ let images = panel.getElementsByTagName("img");
+ is(images.length, 1, "Tooltip contains an image");
+ is(images[0].getAttribute("src"), imageSrc, "The image URL is correct");
+ }
+
+ let links = propertyValue.querySelectorAll(".theme-link");
+ let panel = view.tooltips.previewTooltip.panel;
+
+ info("Checking first link tooltip");
+ yield assertHoverTooltipOn(view.tooltips.previewTooltip, links[0]);
+ checkTooltip(panel, YELLOW_DOT);
+
+ info("Checking second link tooltip");
+ yield assertHoverTooltipOn(view.tooltips.previewTooltip, links[1]);
+ checkTooltip(panel, BLUE_DOT);
+}
diff --git a/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-shorthand-fontfamily.js b/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-shorthand-fontfamily.js
new file mode 100644
index 000000000..bb851ec92
--- /dev/null
+++ b/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-shorthand-fontfamily.js
@@ -0,0 +1,58 @@
+/* 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 fontfamily tooltip on shorthand properties
+
+const TEST_URI = `
+ <style type="text/css">
+ #testElement {
+ font: italic bold .8em/1.2 Arial;
+ }
+ </style>
+ <div id="testElement">test element</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+
+ yield selectNode("#testElement", inspector);
+ yield testRuleView(view, inspector.selection.nodeFront);
+});
+
+function* testRuleView(ruleView, nodeFront) {
+ info("Testing font-family tooltips in the rule view");
+
+ let tooltip = ruleView.tooltips.previewTooltip;
+ let panel = tooltip.panel;
+
+ // Check that the rule view has a tooltip and that a XUL panel has
+ // been created
+ ok(tooltip, "Tooltip instance exists");
+ ok(panel, "XUL panel exists");
+
+ // Get the computed font family property inside the font rule view
+ let propertyList = ruleView.element
+ .querySelectorAll(".ruleview-propertylist");
+ let fontExpander = propertyList[1].querySelectorAll(".ruleview-expander")[0];
+ fontExpander.click();
+
+ let rule = getRuleViewRule(ruleView, "#testElement");
+ let valueSpan = rule
+ .querySelector(".ruleview-computed .ruleview-propertyvalue");
+
+ // And verify that the tooltip gets shown on this property
+ yield assertHoverTooltipOn(tooltip, valueSpan);
+
+ let images = panel.getElementsByTagName("img");
+ is(images.length, 1, "Tooltip contains an image");
+ ok(images[0].getAttribute("src")
+ .startsWith("data:"), "Tooltip contains a data-uri image as expected");
+
+ let dataURL = yield getFontFamilyDataURL(valueSpan.textContent, nodeFront);
+ is(images[0].getAttribute("src"), dataURL,
+ "Tooltip contains the correct data-uri image");
+}
diff --git a/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-size.js b/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-size.js
new file mode 100644
index 000000000..b231fe1b1
--- /dev/null
+++ b/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-size.js
@@ -0,0 +1,86 @@
+/* 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";
+
+// Checking tooltips dimensions, to make sure their big enough to display their
+// content
+
+const TEST_URI = `
+ <style type="text/css">
+ div {
+ width: 300px;height: 300px;border-radius: 50%;
+ background: red url(chrome://global/skin/icons/warning-64.png);
+ }
+ </style>
+ <div></div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("div", inspector);
+ yield testImageDimension(view);
+ yield testPickerDimension(view);
+});
+
+function* testImageDimension(ruleView) {
+ info("Testing background-image tooltip dimensions");
+
+ let tooltip = ruleView.tooltips.previewTooltip;
+ let panel = tooltip.panel;
+ let {valueSpan} = getRuleViewProperty(ruleView, "div", "background");
+ let uriSpan = valueSpan.querySelector(".theme-link");
+
+ // Make sure there is a hover tooltip for this property, this also will fill
+ // the tooltip with its content
+ yield assertHoverTooltipOn(tooltip, uriSpan);
+
+ info("Showing the tooltip");
+ let onShown = tooltip.once("shown");
+ tooltip.show(uriSpan);
+ yield onShown;
+
+ // Let's not test for a specific size, but instead let's make sure it's at
+ // least as big as the image
+ let imageRect = panel.querySelector("img").getBoundingClientRect();
+ let panelRect = panel.getBoundingClientRect();
+
+ ok(panelRect.width >= imageRect.width,
+ "The panel is wide enough to show the image");
+ ok(panelRect.height >= imageRect.height,
+ "The panel is high enough to show the image");
+
+ let onHidden = tooltip.once("hidden");
+ tooltip.hide();
+ yield onHidden;
+}
+
+function* testPickerDimension(ruleView) {
+ info("Testing color-picker tooltip dimensions");
+
+ let {valueSpan} = getRuleViewProperty(ruleView, "div", "background");
+ let swatch = valueSpan.querySelector(".ruleview-colorswatch");
+ let cPicker = ruleView.tooltips.colorPicker;
+
+ let onReady = cPicker.once("ready");
+ swatch.click();
+ yield onReady;
+
+ // The colorpicker spectrum's iframe has a fixed width height, so let's
+ // make sure the tooltip is at least as big as that
+ let spectrumRect = cPicker.spectrum.element.getBoundingClientRect();
+ let panelRect = cPicker.tooltip.container.getBoundingClientRect();
+
+ ok(panelRect.width >= spectrumRect.width,
+ "The panel is wide enough to show the picker");
+ ok(panelRect.height >= spectrumRect.height,
+ "The panel is high enough to show the picker");
+
+ let onHidden = cPicker.tooltip.once("hidden");
+ let onRuleViewChanged = ruleView.once("ruleview-changed");
+ cPicker.hide();
+ yield onHidden;
+ yield onRuleViewChanged;
+}
diff --git a/devtools/client/inspector/shared/test/browser_styleinspector_transform-highlighter-01.js b/devtools/client/inspector/shared/test/browser_styleinspector_transform-highlighter-01.js
new file mode 100644
index 000000000..68a91ff95
--- /dev/null
+++ b/devtools/client/inspector/shared/test/browser_styleinspector_transform-highlighter-01.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 css transform highlighter is created only when asked
+
+const TEST_URI = `
+ <style type="text/css">
+ body {
+ transform: skew(16deg);
+ }
+ </style>
+ Test the css transform highlighter
+`;
+
+const TYPE = "CssTransformHighlighter";
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+
+ let overlay = view.highlighters;
+
+ ok(!overlay.highlighters[TYPE], "No highlighter exists in the rule-view");
+ let h = yield overlay._getHighlighter(TYPE);
+ ok(overlay.highlighters[TYPE],
+ "The highlighter has been created in the rule-view");
+ is(h, overlay.highlighters[TYPE], "The right highlighter has been created");
+ let h2 = yield overlay._getHighlighter(TYPE);
+ is(h, h2,
+ "The same instance of highlighter is returned everytime in the rule-view");
+
+ let onComputedViewReady = inspector.once("computed-view-refreshed");
+ let cView = selectComputedView(inspector);
+ yield onComputedViewReady;
+ overlay = cView.highlighters;
+
+ ok(!overlay.highlighters[TYPE], "No highlighter exists in the computed-view");
+ h = yield overlay._getHighlighter(TYPE);
+ ok(overlay.highlighters[TYPE],
+ "The highlighter has been created in the computed-view");
+ is(h, overlay.highlighters[TYPE], "The right highlighter has been created");
+ h2 = yield overlay._getHighlighter(TYPE);
+ is(h, h2, "The same instance of highlighter is returned everytime " +
+ "in the computed-view");
+});
diff --git a/devtools/client/inspector/shared/test/browser_styleinspector_transform-highlighter-02.js b/devtools/client/inspector/shared/test/browser_styleinspector_transform-highlighter-02.js
new file mode 100644
index 000000000..a44a31422
--- /dev/null
+++ b/devtools/client/inspector/shared/test/browser_styleinspector_transform-highlighter-02.js
@@ -0,0 +1,57 @@
+/* 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 css transform highlighter is created when hovering over a
+// transform property
+
+const TEST_URI = `
+ <style type="text/css">
+ body {
+ transform: skew(16deg);
+ color: yellow;
+ }
+ </style>
+ Test the css transform highlighter
+`;
+
+var TYPE = "CssTransformHighlighter";
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ let hs = view.highlighters;
+
+ ok(!hs.highlighters[TYPE], "No highlighter exists in the rule-view (1)");
+
+ info("Faking a mousemove on a non-transform property");
+ let {valueSpan} = getRuleViewProperty(view, "body", "color");
+ hs._onMouseMove({target: valueSpan});
+ ok(!hs.highlighters[TYPE], "No highlighter exists in the rule-view (2)");
+
+ info("Faking a mousemove on a transform property");
+ ({valueSpan} = getRuleViewProperty(view, "body", "transform"));
+ let onHighlighterShown = hs.once("highlighter-shown");
+ hs._onMouseMove({target: valueSpan});
+ yield onHighlighterShown;
+
+ let onComputedViewReady = inspector.once("computed-view-refreshed");
+ let cView = selectComputedView(inspector);
+ yield onComputedViewReady;
+ hs = cView.highlighters;
+
+ ok(!hs.highlighters[TYPE], "No highlighter exists in the computed-view (1)");
+
+ info("Faking a mousemove on a non-transform property");
+ ({valueSpan} = getComputedViewProperty(cView, "color"));
+ hs._onMouseMove({target: valueSpan});
+ ok(!hs.highlighters[TYPE], "No highlighter exists in the computed-view (2)");
+
+ info("Faking a mousemove on a transform property");
+ ({valueSpan} = getComputedViewProperty(cView, "transform"));
+ onHighlighterShown = hs.once("highlighter-shown");
+ hs._onMouseMove({target: valueSpan});
+ yield onHighlighterShown;
+});
diff --git a/devtools/client/inspector/shared/test/browser_styleinspector_transform-highlighter-03.js b/devtools/client/inspector/shared/test/browser_styleinspector_transform-highlighter-03.js
new file mode 100644
index 000000000..1ecdf279e
--- /dev/null
+++ b/devtools/client/inspector/shared/test/browser_styleinspector_transform-highlighter-03.js
@@ -0,0 +1,103 @@
+/* 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 css transform highlighter is shown when hovering over transform
+// properties
+
+// Note that in this test, we mock the highlighter front, merely testing the
+// behavior of the style-inspector UI for now
+
+const TEST_URI = `
+ <style type="text/css">
+ html {
+ transform: scale(.9);
+ }
+ body {
+ transform: skew(16deg);
+ color: purple;
+ }
+ </style>
+ Test the css transform highlighter
+`;
+
+const TYPE = "CssTransformHighlighter";
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+
+ // Mock the highlighter front to get the reference of the NodeFront
+ let HighlighterFront = {
+ isShown: false,
+ nodeFront: null,
+ nbOfTimesShown: 0,
+ show: function (nodeFront) {
+ this.nodeFront = nodeFront;
+ this.isShown = true;
+ this.nbOfTimesShown ++;
+ return promise.resolve(true);
+ },
+ hide: function () {
+ this.nodeFront = null;
+ this.isShown = false;
+ return promise.resolve();
+ },
+ finalize: function () {}
+ };
+
+ // Inject the mock highlighter in the rule-view
+ let hs = view.highlighters;
+ hs.highlighters[TYPE] = HighlighterFront;
+
+ let {valueSpan} = getRuleViewProperty(view, "body", "transform");
+
+ info("Checking that the HighlighterFront's show/hide methods are called");
+ let onHighlighterShown = hs.once("highlighter-shown");
+ hs._onMouseMove({target: valueSpan});
+ yield onHighlighterShown;
+ ok(HighlighterFront.isShown, "The highlighter is shown");
+ let onHighlighterHidden = hs.once("highlighter-hidden");
+ hs._onMouseOut();
+ yield onHighlighterHidden;
+ ok(!HighlighterFront.isShown, "The highlighter is hidden");
+
+ info("Checking that hovering several times over the same property doesn't" +
+ " show the highlighter several times");
+ let nb = HighlighterFront.nbOfTimesShown;
+ onHighlighterShown = hs.once("highlighter-shown");
+ hs._onMouseMove({target: valueSpan});
+ yield onHighlighterShown;
+ is(HighlighterFront.nbOfTimesShown, nb + 1, "The highlighter was shown once");
+ hs._onMouseMove({target: valueSpan});
+ hs._onMouseMove({target: valueSpan});
+ is(HighlighterFront.nbOfTimesShown, nb + 1,
+ "The highlighter was shown once, after several mousemove");
+
+ info("Checking that the right NodeFront reference is passed");
+ yield selectNode("html", inspector);
+ ({valueSpan} = getRuleViewProperty(view, "html", "transform"));
+ onHighlighterShown = hs.once("highlighter-shown");
+ hs._onMouseMove({target: valueSpan});
+ yield onHighlighterShown;
+ is(HighlighterFront.nodeFront.tagName, "HTML",
+ "The right NodeFront is passed to the highlighter (1)");
+
+ yield selectNode("body", inspector);
+ ({valueSpan} = getRuleViewProperty(view, "body", "transform"));
+ onHighlighterShown = hs.once("highlighter-shown");
+ hs._onMouseMove({target: valueSpan});
+ yield onHighlighterShown;
+ is(HighlighterFront.nodeFront.tagName, "BODY",
+ "The right NodeFront is passed to the highlighter (2)");
+
+ info("Checking that the highlighter gets hidden when hovering a " +
+ "non-transform property");
+ ({valueSpan} = getRuleViewProperty(view, "body", "color"));
+ onHighlighterHidden = hs.once("highlighter-hidden");
+ hs._onMouseMove({target: valueSpan});
+ yield onHighlighterHidden;
+ ok(!HighlighterFront.isShown, "The highlighter is hidden");
+});
diff --git a/devtools/client/inspector/shared/test/browser_styleinspector_transform-highlighter-04.js b/devtools/client/inspector/shared/test/browser_styleinspector_transform-highlighter-04.js
new file mode 100644
index 000000000..9d81e2649
--- /dev/null
+++ b/devtools/client/inspector/shared/test/browser_styleinspector_transform-highlighter-04.js
@@ -0,0 +1,60 @@
+/* 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 css transform highlighter is shown only when hovering over a
+// transform declaration that isn't overriden or disabled
+
+// Note that unlike the other browser_styleinspector_transform-highlighter-N.js
+// tests, this one only tests the rule-view as only this view features disabled
+// and overriden properties
+
+const TEST_URI = `
+ <style type="text/css">
+ div {
+ background: purple;
+ width:300px;height:300px;
+ transform: rotate(16deg);
+ }
+ .test {
+ transform: skew(25deg);
+ }
+ </style>
+ <div class="test"></div>
+`;
+
+const TYPE = "CssTransformHighlighter";
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode(".test", inspector);
+
+ let hs = view.highlighters;
+
+ info("Faking a mousemove on the overriden property");
+ let {valueSpan} = getRuleViewProperty(view, "div", "transform");
+ hs._onMouseMove({target: valueSpan});
+ ok(!hs.highlighters[TYPE],
+ "No highlighter was created for the overriden property");
+
+ info("Disabling the applied property");
+ let classRuleEditor = getRuleViewRuleEditor(view, 1);
+ let propEditor = classRuleEditor.rule.textProps[0].editor;
+ propEditor.enable.click();
+ yield classRuleEditor.rule._applyingModifications;
+
+ info("Faking a mousemove on the disabled property");
+ ({valueSpan} = getRuleViewProperty(view, ".test", "transform"));
+ hs._onMouseMove({target: valueSpan});
+ ok(!hs.highlighters[TYPE],
+ "No highlighter was created for the disabled property");
+
+ info("Faking a mousemove on the now unoverriden property");
+ ({valueSpan} = getRuleViewProperty(view, "div", "transform"));
+ let onHighlighterShown = hs.once("highlighter-shown");
+ hs._onMouseMove({target: valueSpan});
+ yield onHighlighterShown;
+});
diff --git a/devtools/client/inspector/shared/test/doc_author-sheet.html b/devtools/client/inspector/shared/test/doc_author-sheet.html
new file mode 100644
index 000000000..d611bb387
--- /dev/null
+++ b/devtools/client/inspector/shared/test/doc_author-sheet.html
@@ -0,0 +1,37 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<html>
+<head>
+ <title>authored sheet test</title>
+ <style>
+ #target {
+ color: chartreuse;
+ }
+ </style>
+ <script>
+ "use strict";
+ var gIOService = SpecialPowers.Cc["@mozilla.org/network/io-service;1"]
+ .getService(SpecialPowers.Ci.nsIIOService);
+
+ var style = "data:text/css,div { background-color: seagreen; }";
+ var uri = gIOService.newURI(style, null, null);
+ var windowUtils = SpecialPowers.wrap(window)
+ .QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor)
+ .getInterface(SpecialPowers.Ci.nsIDOMWindowUtils);
+ windowUtils.loadSheet(uri, windowUtils.AUTHOR_SHEET);
+ </script>
+</head>
+<body>
+ <div id="target"> the ocean </div>
+ <input type=text placeholder=test></input>
+ <input type=color></input>
+ <input type=range></input>
+ <input type=number></input>
+ <progress></progress>
+ <blockquote type=cite>
+ <pre _moz_quote=true>
+ inspect <a href="foo">user agent</a> styles
+ </pre>
+ </blockquote>
+</body>
+</html>
diff --git a/devtools/client/inspector/shared/test/doc_content_stylesheet.html b/devtools/client/inspector/shared/test/doc_content_stylesheet.html
new file mode 100644
index 000000000..f9b52f78d
--- /dev/null
+++ b/devtools/client/inspector/shared/test/doc_content_stylesheet.html
@@ -0,0 +1,32 @@
+<html>
+<head>
+ <title>test</title>
+ <link href="./doc_content_stylesheet_linked.css" rel="stylesheet" type="text/css">
+ <script>
+ /* exported loadCSS */
+ "use strict";
+ // Load script.css
+ function loadCSS() {
+ let link = document.createElement("link");
+ link.rel = "stylesheet";
+ link.type = "text/css";
+ link.href = "./doc_content_stylesheet_script.css";
+ document.getElementsByTagName("head")[0].appendChild(link);
+ }
+ </script>
+ <style>
+ table {
+ border: 1px solid #000;
+ }
+ </style>
+</head>
+<body onload="loadCSS();">
+ <table id="target">
+ <tr>
+ <td>
+ <h3>Simple test</h3>
+ </td>
+ </tr>
+ </table>
+</body>
+</html>
diff --git a/devtools/client/inspector/shared/test/doc_content_stylesheet.xul b/devtools/client/inspector/shared/test/doc_content_stylesheet.xul
new file mode 100644
index 000000000..efd53815d
--- /dev/null
+++ b/devtools/client/inspector/shared/test/doc_content_stylesheet.xul
@@ -0,0 +1,9 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/xul.css" type="text/css"?>
+<?xml-stylesheet href="./doc_content_stylesheet_xul.css"
+ type="text/css"?>
+<!DOCTYPE window>
+<window id="testwindow" xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <label id="target" value="Simple XUL document" />
+</window> \ No newline at end of file
diff --git a/devtools/client/inspector/shared/test/doc_content_stylesheet_imported.css b/devtools/client/inspector/shared/test/doc_content_stylesheet_imported.css
new file mode 100644
index 000000000..ea1a3d986
--- /dev/null
+++ b/devtools/client/inspector/shared/test/doc_content_stylesheet_imported.css
@@ -0,0 +1,5 @@
+@import url("./doc_content_stylesheet_imported2.css");
+
+#target {
+ text-decoration: underline;
+}
diff --git a/devtools/client/inspector/shared/test/doc_content_stylesheet_imported2.css b/devtools/client/inspector/shared/test/doc_content_stylesheet_imported2.css
new file mode 100644
index 000000000..77c73299e
--- /dev/null
+++ b/devtools/client/inspector/shared/test/doc_content_stylesheet_imported2.css
@@ -0,0 +1,3 @@
+#target {
+ text-decoration: underline;
+}
diff --git a/devtools/client/inspector/shared/test/doc_content_stylesheet_linked.css b/devtools/client/inspector/shared/test/doc_content_stylesheet_linked.css
new file mode 100644
index 000000000..712ba78fb
--- /dev/null
+++ b/devtools/client/inspector/shared/test/doc_content_stylesheet_linked.css
@@ -0,0 +1,3 @@
+table {
+ border-collapse: collapse;
+}
diff --git a/devtools/client/inspector/shared/test/doc_content_stylesheet_script.css b/devtools/client/inspector/shared/test/doc_content_stylesheet_script.css
new file mode 100644
index 000000000..5aa5e2c6c
--- /dev/null
+++ b/devtools/client/inspector/shared/test/doc_content_stylesheet_script.css
@@ -0,0 +1,5 @@
+@import url("./doc_content_stylesheet_imported.css");
+
+table {
+ opacity: 1;
+}
diff --git a/devtools/client/inspector/shared/test/doc_content_stylesheet_xul.css b/devtools/client/inspector/shared/test/doc_content_stylesheet_xul.css
new file mode 100644
index 000000000..a14ae7f6f
--- /dev/null
+++ b/devtools/client/inspector/shared/test/doc_content_stylesheet_xul.css
@@ -0,0 +1,3 @@
+#target {
+ font-size: 200px;
+}
diff --git a/devtools/client/inspector/shared/test/doc_frame_script.js b/devtools/client/inspector/shared/test/doc_frame_script.js
new file mode 100644
index 000000000..aeb73a115
--- /dev/null
+++ b/devtools/client/inspector/shared/test/doc_frame_script.js
@@ -0,0 +1,115 @@
+/* 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/ */
+/* globals addMessageListener, sendAsyncMessage */
+
+"use strict";
+
+// A helper frame-script for brower/devtools/styleinspector tests.
+//
+// Most listeners in the script expect "Test:"-namespaced messages from chrome,
+// then execute code upon receiving, and immediately send back a message.
+// This is so that chrome test code can execute code in content and wait for a
+// response this way:
+// let response = yield executeInContent(browser, "Test:MsgName", data, true);
+// The response message should have the same name "Test:MsgName"
+//
+// Some listeners do not send a response message back.
+
+var {utils: Cu} = Components;
+var {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+var defer = require("devtools/shared/defer");
+
+/**
+ * Get a value for a given property name in a css rule in a stylesheet, given
+ * their indexes
+ * @param {Object} data Expects a data object with the following properties
+ * - {Number} styleSheetIndex
+ * - {Number} ruleIndex
+ * - {String} name
+ * @return {String} The value, if found, null otherwise
+ */
+addMessageListener("Test:GetRulePropertyValue", function (msg) {
+ let {name, styleSheetIndex, ruleIndex} = msg.data;
+ let value = null;
+
+ dumpn("Getting the value for property name " + name + " in sheet " +
+ styleSheetIndex + " and rule " + ruleIndex);
+
+ let sheet = content.document.styleSheets[styleSheetIndex];
+ if (sheet) {
+ let rule = sheet.cssRules[ruleIndex];
+ if (rule) {
+ value = rule.style.getPropertyValue(name);
+ }
+ }
+
+ sendAsyncMessage("Test:GetRulePropertyValue", value);
+});
+
+/**
+ * Get the property value from the computed style for an element.
+ * @param {Object} data Expects a data object with the following properties
+ * - {String} selector: The selector used to obtain the element.
+ * - {String} pseudo: pseudo id to query, or null.
+ * - {String} name: name of the property
+ * @return {String} The value, if found, null otherwise
+ */
+addMessageListener("Test:GetComputedStylePropertyValue", function (msg) {
+ let {selector, pseudo, name} = msg.data;
+ let doc = content.document;
+
+ let element = doc.querySelector(selector);
+ let value = content.getComputedStyle(element, pseudo).getPropertyValue(name);
+ sendAsyncMessage("Test:GetComputedStylePropertyValue", value);
+});
+
+/**
+ * Wait the property value from the computed style for an element and
+ * compare it with the expected value
+ * @param {Object} data Expects a data object with the following properties
+ * - {String} selector: The selector used to obtain the element.
+ * - {String} pseudo: pseudo id to query, or null.
+ * - {String} name: name of the property
+ * - {String} expected: the expected value for property
+ */
+addMessageListener("Test:WaitForComputedStylePropertyValue", function (msg) {
+ let {selector, pseudo, name, expected} = msg.data;
+ let element = content.document.querySelector(selector);
+ waitForSuccess(() => {
+ let value = content.document.defaultView.getComputedStyle(element, pseudo)
+ .getPropertyValue(name);
+
+ return value === expected;
+ }).then(() => {
+ sendAsyncMessage("Test:WaitForComputedStylePropertyValue");
+ });
+});
+
+var dumpn = msg => dump(msg + "\n");
+
+/**
+ * Polls a given function waiting for it to return true.
+ *
+ * @param {Function} validatorFn A validator function that returns a boolean.
+ * This is called every few milliseconds to check if the result is true. When
+ * it is true, the promise resolves.
+ * @param {String} name Optional name of the test. This is used to generate
+ * the success and failure messages.
+ * @return a promise that resolves when the function returned true or rejects
+ * if the timeout is reached
+ */
+function waitForSuccess(validatorFn, name = "untitled") {
+ let def = defer();
+
+ function wait(fn) {
+ if (fn()) {
+ def.resolve();
+ } else {
+ setTimeout(() => wait(fn), 200);
+ }
+ }
+ wait(validatorFn);
+
+ return def.promise;
+}
diff --git a/devtools/client/inspector/shared/test/head.js b/devtools/client/inspector/shared/test/head.js
new file mode 100644
index 000000000..bcc2ec2c7
--- /dev/null
+++ b/devtools/client/inspector/shared/test/head.js
@@ -0,0 +1,557 @@
+/* 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/ */
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+/* import-globals-from ../../test/head.js */
+"use strict";
+
+// Import the inspector's head.js first (which itself imports shared-head.js).
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/inspector/test/head.js",
+ this);
+
+var {CssRuleView} = require("devtools/client/inspector/rules/rules");
+var {getInplaceEditorForSpan: inplaceEditor} =
+ require("devtools/client/shared/inplace-editor");
+const {getColor: getThemeColor} = require("devtools/client/shared/theme");
+
+const TEST_URL_ROOT =
+ "http://example.com/browser/devtools/client/inspector/shared/test/";
+const TEST_URL_ROOT_SSL =
+ "https://example.com/browser/devtools/client/inspector/shared/test/";
+const ROOT_TEST_DIR = getRootDirectory(gTestPath);
+const FRAME_SCRIPT_URL = ROOT_TEST_DIR + "doc_frame_script.js";
+const STYLE_INSPECTOR_L10N =
+ new LocalizationHelper("devtools/shared/locales/styleinspector.properties");
+
+// Clean-up all prefs that might have been changed during a test run
+// (safer here because if the test fails, then the pref is never reverted)
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("devtools.defaultColorUnit");
+});
+
+/**
+ * The functions found below are here to ease test development and maintenance.
+ * Most of these functions are stateless and will require some form of context
+ * (the instance of the current toolbox, or inspector panel for instance).
+ *
+ * Most of these functions are async too and return promises.
+ *
+ * All tests should follow the following pattern:
+ *
+ * add_task(function*() {
+ * yield addTab(TEST_URI);
+ * let {toolbox, inspector} = yield openInspector();
+ * inspector.sidebar.select(viewId);
+ * let view = inspector[viewId].view;
+ * yield selectNode("#test", inspector);
+ * yield someAsyncTestFunction(view);
+ * });
+ *
+ * add_task is the way to define the testcase in the test file. It accepts
+ * a single generator-function argument.
+ * The generator function should yield any async call.
+ *
+ * There is no need to clean tabs up at the end of a test as this is done
+ * automatically.
+ *
+ * It is advised not to store any references on the global scope. There
+ * shouldn't be a need to anyway. Thanks to add_task, test steps, even
+ * though asynchronous, can be described in a nice flat way, and
+ * if/for/while/... control flow can be used as in sync code, making it
+ * possible to write the outline of the test case all in add_task, and delegate
+ * actual processing and assertions to other functions.
+ */
+
+/* *********************************************
+ * UTILS
+ * *********************************************
+ * General test utilities.
+ * Add new tabs, open the toolbox and switch to the various panels, select
+ * nodes, get node references, ...
+ */
+
+/**
+ * The rule-view tests rely on a frame-script to be injected in the content test
+ * page. So override the shared-head's addTab to load the frame script after the
+ * tab was added.
+ * FIXME: Refactor the rule-view tests to use the testActor instead of a frame
+ * script, so they can run on remote targets too.
+ */
+var _addTab = addTab;
+addTab = function (url) {
+ return _addTab(url).then(tab => {
+ info("Loading the helper frame script " + FRAME_SCRIPT_URL);
+ let browser = tab.linkedBrowser;
+ browser.messageManager.loadFrameScript(FRAME_SCRIPT_URL, false);
+ return tab;
+ });
+};
+
+/**
+ * Wait for a content -> chrome message on the message manager (the window
+ * messagemanager is used).
+ *
+ * @param {String} name
+ * The message name
+ * @return {Promise} A promise that resolves to the response data when the
+ * message has been received
+ */
+function waitForContentMessage(name) {
+ info("Expecting message " + name + " from content");
+
+ let mm = gBrowser.selectedBrowser.messageManager;
+
+ let def = defer();
+ mm.addMessageListener(name, function onMessage(msg) {
+ mm.removeMessageListener(name, onMessage);
+ def.resolve(msg.data);
+ });
+ return def.promise;
+}
+
+/**
+ * Send an async message to the frame script (chrome -> content) and wait for a
+ * response message with the same name (content -> chrome).
+ *
+ * @param {String} name
+ * The message name. Should be one of the messages defined
+ * in doc_frame_script.js
+ * @param {Object} data
+ * Optional data to send along
+ * @param {Object} objects
+ * Optional CPOW objects to send along
+ * @param {Boolean} expectResponse
+ * If set to false, don't wait for a response with the same name
+ * from the content script. Defaults to true.
+ * @return {Promise} Resolves to the response data if a response is expected,
+ * immediately resolves otherwise
+ */
+function executeInContent(name, data = {}, objects = {},
+ expectResponse = true) {
+ info("Sending message " + name + " to content");
+ let mm = gBrowser.selectedBrowser.messageManager;
+
+ mm.sendAsyncMessage(name, data, objects);
+ if (expectResponse) {
+ return waitForContentMessage(name);
+ }
+
+ return promise.resolve();
+}
+
+/**
+ * Send an async message to the frame script and get back the requested
+ * computed style property.
+ *
+ * @param {String} selector
+ * The selector used to obtain the element.
+ * @param {String} pseudo
+ * pseudo id to query, or null.
+ * @param {String} name
+ * name of the property.
+ */
+function* getComputedStyleProperty(selector, pseudo, propName) {
+ return yield executeInContent("Test:GetComputedStylePropertyValue",
+ {selector,
+ pseudo,
+ name: propName});
+}
+
+/**
+ * Send an async message to the frame script and wait until the requested
+ * computed style property has the expected value.
+ *
+ * @param {String} selector
+ * The selector used to obtain the element.
+ * @param {String} pseudo
+ * pseudo id to query, or null.
+ * @param {String} prop
+ * name of the property.
+ * @param {String} expected
+ * expected value of property
+ * @param {String} name
+ * the name used in test message
+ */
+function* waitForComputedStyleProperty(selector, pseudo, name, expected) {
+ return yield executeInContent("Test:WaitForComputedStylePropertyValue",
+ {selector,
+ pseudo,
+ expected,
+ name});
+}
+
+/**
+ * Given an inplace editable element, click to switch it to edit mode, wait for
+ * focus
+ *
+ * @return a promise that resolves to the inplace-editor element when ready
+ */
+var focusEditableField = Task.async(function* (ruleView, editable, xOffset = 1,
+ yOffset = 1, options = {}) {
+ let onFocus = once(editable.parentNode, "focus", true);
+ info("Clicking on editable field to turn to edit mode");
+ EventUtils.synthesizeMouse(editable, xOffset, yOffset, options,
+ editable.ownerDocument.defaultView);
+ yield onFocus;
+
+ info("Editable field gained focus, returning the input field now");
+ let onEdit = inplaceEditor(editable.ownerDocument.activeElement);
+
+ return onEdit;
+});
+
+/**
+ * Polls a given function waiting for it to return true.
+ *
+ * @param {Function} validatorFn
+ * A validator function that returns a boolean.
+ * This is called every few milliseconds to check if the result is true.
+ * When it is true, the promise resolves.
+ * @param {String} name
+ * Optional name of the test. This is used to generate
+ * the success and failure messages.
+ * @return a promise that resolves when the function returned true or rejects
+ * if the timeout is reached
+ */
+function waitForSuccess(validatorFn, name = "untitled") {
+ let def = defer();
+
+ function wait(validator) {
+ if (validator()) {
+ ok(true, "Validator function " + name + " returned true");
+ def.resolve();
+ } else {
+ setTimeout(() => wait(validator), 200);
+ }
+ }
+ wait(validatorFn);
+
+ return def.promise;
+}
+
+/**
+ * Get the dataURL for the font family tooltip.
+ *
+ * @param {String} font
+ * The font family value.
+ * @param {object} nodeFront
+ * The NodeActor that will used to retrieve the dataURL for the
+ * font family tooltip contents.
+ */
+var getFontFamilyDataURL = Task.async(function* (font, nodeFront) {
+ let fillStyle = getThemeColor("body-color");
+
+ let {data} = yield nodeFront.getFontFamilyDataURL(font, fillStyle);
+ let dataURL = yield data.string();
+ return dataURL;
+});
+
+/* *********************************************
+ * RULE-VIEW
+ * *********************************************
+ * Rule-view related test utility functions
+ * This object contains functions to get rules, get properties, ...
+ */
+
+/**
+ * Get the DOMNode for a css rule in the rule-view that corresponds to the given
+ * selector
+ *
+ * @param {CssRuleView} view
+ * The instance of the rule-view panel
+ * @param {String} selectorText
+ * The selector in the rule-view for which the rule
+ * object is wanted
+ * @return {DOMNode}
+ */
+function getRuleViewRule(view, selectorText) {
+ let rule;
+ for (let r of view.styleDocument.querySelectorAll(".ruleview-rule")) {
+ let selector = r.querySelector(".ruleview-selectorcontainer, " +
+ ".ruleview-selector-matched");
+ if (selector && selector.textContent === selectorText) {
+ rule = r;
+ break;
+ }
+ }
+
+ return rule;
+}
+
+/**
+ * Get references to the name and value span nodes corresponding to a given
+ * selector and property name in the rule-view
+ *
+ * @param {CssRuleView} view
+ * The instance of the rule-view panel
+ * @param {String} selectorText
+ * The selector in the rule-view to look for the property in
+ * @param {String} propertyName
+ * The name of the property
+ * @return {Object} An object like {nameSpan: DOMNode, valueSpan: DOMNode}
+ */
+function getRuleViewProperty(view, selectorText, propertyName) {
+ let prop;
+
+ let rule = getRuleViewRule(view, selectorText);
+ if (rule) {
+ // Look for the propertyName in that rule element
+ for (let p of rule.querySelectorAll(".ruleview-property")) {
+ let nameSpan = p.querySelector(".ruleview-propertyname");
+ let valueSpan = p.querySelector(".ruleview-propertyvalue");
+
+ if (nameSpan.textContent === propertyName) {
+ prop = {nameSpan: nameSpan, valueSpan: valueSpan};
+ break;
+ }
+ }
+ }
+ return prop;
+}
+
+/**
+ * Get the text value of the property corresponding to a given selector and name
+ * in the rule-view
+ *
+ * @param {CssRuleView} view
+ * The instance of the rule-view panel
+ * @param {String} selectorText
+ * The selector in the rule-view to look for the property in
+ * @param {String} propertyName
+ * The name of the property
+ * @return {String} The property value
+ */
+function getRuleViewPropertyValue(view, selectorText, propertyName) {
+ return getRuleViewProperty(view, selectorText, propertyName)
+ .valueSpan.textContent;
+}
+
+/**
+ * Get a reference to the selector DOM element corresponding to a given selector
+ * in the rule-view
+ *
+ * @param {CssRuleView} view
+ * The instance of the rule-view panel
+ * @param {String} selectorText
+ * The selector in the rule-view to look for
+ * @return {DOMNode} The selector DOM element
+ */
+function getRuleViewSelector(view, selectorText) {
+ let rule = getRuleViewRule(view, selectorText);
+ return rule.querySelector(".ruleview-selector, .ruleview-selector-matched");
+}
+
+/**
+ * Get a reference to the selectorhighlighter icon DOM element corresponding to
+ * a given selector in the rule-view
+ *
+ * @param {CssRuleView} view
+ * The instance of the rule-view panel
+ * @param {String} selectorText
+ * The selector in the rule-view to look for
+ * @return {DOMNode} The selectorhighlighter icon DOM element
+ */
+function getRuleViewSelectorHighlighterIcon(view, selectorText) {
+ let rule = getRuleViewRule(view, selectorText);
+ return rule.querySelector(".ruleview-selectorhighlighter");
+}
+
+/**
+ * Simulate a color change in a given color picker tooltip, and optionally wait
+ * for a given element in the page to have its style changed as a result
+ *
+ * @param {RuleView} ruleView
+ * The related rule view instance
+ * @param {SwatchColorPickerTooltip} colorPicker
+ * @param {Array} newRgba
+ * The new color to be set [r, g, b, a]
+ * @param {Object} expectedChange
+ * Optional object that needs the following props:
+ * - {DOMNode} element The element in the page that will have its
+ * style changed.
+ * - {String} name The style name that will be changed
+ * - {String} value The expected style value
+ * The style will be checked like so: getComputedStyle(element)[name] === value
+ */
+var simulateColorPickerChange = Task.async(function* (ruleView, colorPicker,
+ newRgba, expectedChange) {
+ let onRuleViewChanged = ruleView.once("ruleview-changed");
+ info("Getting the spectrum colorpicker object");
+ let spectrum = yield colorPicker.spectrum;
+ info("Setting the new color");
+ spectrum.rgb = newRgba;
+ info("Applying the change");
+ spectrum.updateUI();
+ spectrum.onChange();
+ info("Waiting for rule-view to update");
+ yield onRuleViewChanged;
+
+ if (expectedChange) {
+ info("Waiting for the style to be applied on the page");
+ yield waitForSuccess(() => {
+ let {element, name, value} = expectedChange;
+ return content.getComputedStyle(element)[name] === value;
+ }, "Color picker change applied on the page");
+ }
+});
+
+/**
+ * Get a rule-link from the rule-view given its index
+ *
+ * @param {CssRuleView} view
+ * The instance of the rule-view panel
+ * @param {Number} index
+ * The index of the link to get
+ * @return {DOMNode} The link if any at this index
+ */
+function getRuleViewLinkByIndex(view, index) {
+ let links = view.styleDocument.querySelectorAll(".ruleview-rule-source");
+ return links[index];
+}
+
+/**
+ * Get rule-link text from the rule-view given its index
+ *
+ * @param {CssRuleView} view
+ * The instance of the rule-view panel
+ * @param {Number} index
+ * The index of the link to get
+ * @return {String} The string at this index
+ */
+function getRuleViewLinkTextByIndex(view, index) {
+ let link = getRuleViewLinkByIndex(view, index);
+ return link.querySelector(".ruleview-rule-source-label").textContent;
+}
+
+/**
+ * Click on a rule-view's close brace to focus a new property name editor
+ *
+ * @param {RuleEditor} ruleEditor
+ * An instance of RuleEditor that will receive the new property
+ * @return a promise that resolves to the newly created editor when ready and
+ * focused
+ */
+var focusNewRuleViewProperty = Task.async(function* (ruleEditor) {
+ info("Clicking on a close ruleEditor brace to start editing a new property");
+ ruleEditor.closeBrace.scrollIntoView();
+ let editor = yield focusEditableField(ruleEditor.ruleView,
+ ruleEditor.closeBrace);
+
+ is(inplaceEditor(ruleEditor.newPropSpan), editor,
+ "Focused editor is the new property editor.");
+
+ return editor;
+});
+
+/**
+ * Create a new property name in the rule-view, focusing a new property editor
+ * by clicking on the close brace, and then entering the given text.
+ * Keep in mind that the rule-view knows how to handle strings with multiple
+ * properties, so the input text may be like: "p1:v1;p2:v2;p3:v3".
+ *
+ * @param {RuleEditor} ruleEditor
+ * The instance of RuleEditor that will receive the new property(ies)
+ * @param {String} inputValue
+ * The text to be entered in the new property name field
+ * @return a promise that resolves when the new property name has been entered
+ * and once the value field is focused
+ */
+var createNewRuleViewProperty = Task.async(function* (ruleEditor, inputValue) {
+ info("Creating a new property editor");
+ let editor = yield focusNewRuleViewProperty(ruleEditor);
+
+ info("Entering the value " + inputValue);
+ editor.input.value = inputValue;
+
+ info("Submitting the new value and waiting for value field focus");
+ let onFocus = once(ruleEditor.element, "focus", true);
+ EventUtils.synthesizeKey("VK_RETURN", {},
+ ruleEditor.element.ownerDocument.defaultView);
+ yield onFocus;
+});
+
+/**
+ * Set the search value for the rule-view filter styles search box.
+ *
+ * @param {CssRuleView} view
+ * The instance of the rule-view panel
+ * @param {String} searchValue
+ * The filter search value
+ * @return a promise that resolves when the rule-view is filtered for the
+ * search term
+ */
+var setSearchFilter = Task.async(function* (view, searchValue) {
+ info("Setting filter text to \"" + searchValue + "\"");
+ let win = view.styleWindow;
+ let searchField = view.searchField;
+ searchField.focus();
+ synthesizeKeys(searchValue, win);
+ yield view.inspector.once("ruleview-filtered");
+});
+
+/* *********************************************
+ * COMPUTED-VIEW
+ * *********************************************
+ * Computed-view related utility functions.
+ * Allows to get properties, links, expand properties, ...
+ */
+
+/**
+ * Get references to the name and value span nodes corresponding to a given
+ * property name in the computed-view
+ *
+ * @param {CssComputedView} view
+ * The instance of the computed view panel
+ * @param {String} name
+ * The name of the property to retrieve
+ * @return an object {nameSpan, valueSpan}
+ */
+function getComputedViewProperty(view, name) {
+ let prop;
+ for (let property of view.styleDocument.querySelectorAll(".property-view")) {
+ let nameSpan = property.querySelector(".property-name");
+ let valueSpan = property.querySelector(".property-value");
+
+ if (nameSpan.textContent === name) {
+ prop = {nameSpan: nameSpan, valueSpan: valueSpan};
+ break;
+ }
+ }
+ return prop;
+}
+
+/**
+ * Get the text value of the property corresponding to a given name in the
+ * computed-view
+ *
+ * @param {CssComputedView} view
+ * The instance of the computed view panel
+ * @param {String} name
+ * The name of the property to retrieve
+ * @return {String} The property value
+ */
+function getComputedViewPropertyValue(view, name, propertyName) {
+ return getComputedViewProperty(view, name, propertyName)
+ .valueSpan.textContent;
+}
+
+/**
+ * Open the style editor context menu and return all of it's items in a flat array
+ * @param {CssRuleView} view
+ * The instance of the rule-view panel
+ * @return An array of MenuItems
+ */
+function openStyleContextMenuAndGetAllItems(view, target) {
+ let menu = view._contextmenu._openMenu({target: target});
+
+ // Flatten all menu items into a single array to make searching through it easier
+ let allItems = [].concat.apply([], menu.items.map(function addItem(item) {
+ if (item.submenu) {
+ return addItem(item.submenu.items);
+ }
+ return item;
+ }));
+
+ return allItems;
+}
diff --git a/devtools/client/inspector/shared/tooltips-overlay.js b/devtools/client/inspector/shared/tooltips-overlay.js
new file mode 100644
index 000000000..8a02d7e3d
--- /dev/null
+++ b/devtools/client/inspector/shared/tooltips-overlay.js
@@ -0,0 +1,319 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set 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";
+
+/**
+ * The tooltip overlays are tooltips that appear when hovering over property values and
+ * editor tooltips that appear when clicking swatch based editors.
+ */
+
+const { Task } = require("devtools/shared/task");
+const Services = require("Services");
+const {
+ VIEW_NODE_VALUE_TYPE,
+ VIEW_NODE_IMAGE_URL_TYPE,
+} = require("devtools/client/inspector/shared/node-types");
+const { getColor } = require("devtools/client/shared/theme");
+const { getCssProperties } = require("devtools/shared/fronts/css-properties");
+const CssDocsTooltip = require("devtools/client/shared/widgets/tooltip/CssDocsTooltip");
+const { HTMLTooltip } = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+const {
+ getImageDimensions,
+ setImageTooltip,
+ setBrokenImageTooltip,
+} = require("devtools/client/shared/widgets/tooltip/ImageTooltipHelper");
+const SwatchColorPickerTooltip = require("devtools/client/shared/widgets/tooltip/SwatchColorPickerTooltip");
+const SwatchCubicBezierTooltip = require("devtools/client/shared/widgets/tooltip/SwatchCubicBezierTooltip");
+const SwatchFilterTooltip = require("devtools/client/shared/widgets/tooltip/SwatchFilterTooltip");
+
+const PREF_IMAGE_TOOLTIP_SIZE = "devtools.inspector.imagePreviewTooltipSize";
+
+// Types of existing tooltips
+const TOOLTIP_IMAGE_TYPE = "image";
+const TOOLTIP_FONTFAMILY_TYPE = "font-family";
+
+/**
+ * Manages all tooltips in the style-inspector.
+ *
+ * @param {CssRuleView|CssComputedView} view
+ * Either the rule-view or computed-view panel
+ */
+function TooltipsOverlay(view) {
+ this.view = view;
+
+ let {CssRuleView} = require("devtools/client/inspector/rules/rules");
+ this.isRuleView = view instanceof CssRuleView;
+ this._cssProperties = getCssProperties(this.view.inspector.toolbox);
+
+ this._onNewSelection = this._onNewSelection.bind(this);
+ this.view.inspector.selection.on("new-node-front", this._onNewSelection);
+}
+
+TooltipsOverlay.prototype = {
+ get isEditing() {
+ return this.colorPicker.tooltip.isVisible() ||
+ this.colorPicker.eyedropperOpen ||
+ this.cubicBezier.tooltip.isVisible() ||
+ this.filterEditor.tooltip.isVisible();
+ },
+
+ /**
+ * Add the tooltips overlay to the view. This will start tracking mouse
+ * movements and display tooltips when needed
+ */
+ addToView: function () {
+ if (this._isStarted || this._isDestroyed) {
+ return;
+ }
+
+ let { toolbox } = this.view.inspector;
+
+ // Initializing the different tooltips that are used in the inspector.
+ // These tooltips are attached to the toolbox document if they require a popup panel.
+ // Otherwise, it is attached to the inspector panel document if it is an inline
+ // editor.
+ this.previewTooltip = new HTMLTooltip(toolbox.doc, {
+ type: "arrow",
+ useXulWrapper: true
+ });
+ this.previewTooltip.startTogglingOnHover(this.view.element,
+ this._onPreviewTooltipTargetHover.bind(this));
+
+ // MDN CSS help tooltip
+ this.cssDocs = new CssDocsTooltip(toolbox.doc);
+
+ if (this.isRuleView) {
+ // Color picker tooltip
+ this.colorPicker = new SwatchColorPickerTooltip(toolbox.doc, this.view.inspector);
+ // Cubic bezier tooltip
+ this.cubicBezier = new SwatchCubicBezierTooltip(toolbox.doc);
+ // Filter editor tooltip
+ this.filterEditor = new SwatchFilterTooltip(toolbox.doc,
+ this._cssProperties.getValidityChecker(this.view.inspector.panelDoc));
+ }
+
+ this._isStarted = true;
+ },
+
+ /**
+ * Remove the tooltips overlay from the view. This will stop tracking mouse
+ * movements and displaying tooltips
+ */
+ removeFromView: function () {
+ if (!this._isStarted || this._isDestroyed) {
+ return;
+ }
+
+ this.previewTooltip.stopTogglingOnHover(this.view.element);
+ this.previewTooltip.destroy();
+
+ if (this.colorPicker) {
+ this.colorPicker.destroy();
+ }
+
+ if (this.cubicBezier) {
+ this.cubicBezier.destroy();
+ }
+
+ if (this.cssDocs) {
+ this.cssDocs.destroy();
+ }
+
+ if (this.filterEditor) {
+ this.filterEditor.destroy();
+ }
+
+ this._isStarted = false;
+ },
+
+ /**
+ * Given a hovered node info, find out which type of tooltip should be shown,
+ * if any
+ *
+ * @param {Object} nodeInfo
+ * @return {String} The tooltip type to be shown, or null
+ */
+ _getTooltipType: function ({type, value: prop}) {
+ let tooltipType = null;
+ let inspector = this.view.inspector;
+
+ // Image preview tooltip
+ if (type === VIEW_NODE_IMAGE_URL_TYPE &&
+ inspector.hasUrlToImageDataResolver) {
+ tooltipType = TOOLTIP_IMAGE_TYPE;
+ }
+
+ // Font preview tooltip
+ if (type === VIEW_NODE_VALUE_TYPE && prop.property === "font-family") {
+ let value = prop.value.toLowerCase();
+ if (value !== "inherit" && value !== "unset" && value !== "initial") {
+ tooltipType = TOOLTIP_FONTFAMILY_TYPE;
+ }
+ }
+
+ return tooltipType;
+ },
+
+ /**
+ * Executed by the tooltip when the pointer hovers over an element of the
+ * view. Used to decide whether the tooltip should be shown or not and to
+ * actually put content in it.
+ * Checks if the hovered target is a css value we support tooltips for.
+ *
+ * @param {DOMNode} target The currently hovered node
+ * @return {Promise}
+ */
+ _onPreviewTooltipTargetHover: Task.async(function* (target) {
+ let nodeInfo = this.view.getNodeInfo(target);
+ if (!nodeInfo) {
+ // The hovered node isn't something we care about
+ return false;
+ }
+
+ let type = this._getTooltipType(nodeInfo);
+ if (!type) {
+ // There is no tooltip type defined for the hovered node
+ return false;
+ }
+
+ if (this.isRuleView && this.colorPicker.tooltip.isVisible()) {
+ this.colorPicker.revert();
+ this.colorPicker.hide();
+ }
+
+ if (this.isRuleView && this.cubicBezier.tooltip.isVisible()) {
+ this.cubicBezier.revert();
+ this.cubicBezier.hide();
+ }
+
+ if (this.isRuleView && this.cssDocs.tooltip.isVisible()) {
+ this.cssDocs.hide();
+ }
+
+ if (this.isRuleView && this.filterEditor.tooltip.isVisible()) {
+ this.filterEditor.revert();
+ this.filterEdtior.hide();
+ }
+
+ let inspector = this.view.inspector;
+
+ if (type === TOOLTIP_IMAGE_TYPE) {
+ try {
+ yield this._setImagePreviewTooltip(nodeInfo.value.url);
+ } catch (e) {
+ yield setBrokenImageTooltip(this.previewTooltip, this.view.inspector.panelDoc);
+ }
+ return true;
+ }
+
+ if (type === TOOLTIP_FONTFAMILY_TYPE) {
+ let font = nodeInfo.value.value;
+ let nodeFront = inspector.selection.nodeFront;
+ yield this._setFontPreviewTooltip(font, nodeFront);
+ return true;
+ }
+
+ return false;
+ }),
+
+ /**
+ * Set the content of the preview tooltip to display an image preview. The image URL can
+ * be relative, a call will be made to the debuggee to retrieve the image content as an
+ * imageData URI.
+ *
+ * @param {String} imageUrl
+ * The image url value (may be relative or absolute).
+ * @return {Promise} A promise that resolves when the preview tooltip content is ready
+ */
+ _setImagePreviewTooltip: Task.async(function* (imageUrl) {
+ let doc = this.view.inspector.panelDoc;
+ let maxDim = Services.prefs.getIntPref(PREF_IMAGE_TOOLTIP_SIZE);
+
+ let naturalWidth, naturalHeight;
+ if (imageUrl.startsWith("data:")) {
+ // If the imageUrl already is a data-url, save ourselves a round-trip
+ let size = yield getImageDimensions(doc, imageUrl);
+ naturalWidth = size.naturalWidth;
+ naturalHeight = size.naturalHeight;
+ } else {
+ let inspectorFront = this.view.inspector.inspector;
+ let {data, size} = yield inspectorFront.getImageDataFromURL(imageUrl, maxDim);
+ imageUrl = yield data.string();
+ naturalWidth = size.naturalWidth;
+ naturalHeight = size.naturalHeight;
+ }
+
+ yield setImageTooltip(this.previewTooltip, doc, imageUrl,
+ {maxDim, naturalWidth, naturalHeight});
+ }),
+
+ /**
+ * Set the content of the preview tooltip to display a font family preview.
+ *
+ * @param {String} font
+ * The font family value.
+ * @param {object} nodeFront
+ * The NodeActor that will used to retrieve the dataURL for the font
+ * family tooltip contents.
+ * @return {Promise} A promise that resolves when the preview tooltip content is ready
+ */
+ _setFontPreviewTooltip: Task.async(function* (font, nodeFront) {
+ if (!font || !nodeFront || typeof nodeFront.getFontFamilyDataURL !== "function") {
+ throw new Error("Unable to create font preview tooltip content.");
+ }
+
+ font = font.replace(/"/g, "'");
+ font = font.replace("!important", "");
+ font = font.trim();
+
+ let fillStyle = getColor("body-color");
+ let {data, size: maxDim} = yield nodeFront.getFontFamilyDataURL(font, fillStyle);
+
+ let imageUrl = yield data.string();
+ let doc = this.view.inspector.panelDoc;
+ let {naturalWidth, naturalHeight} = yield getImageDimensions(doc, imageUrl);
+
+ yield setImageTooltip(this.previewTooltip, doc, imageUrl,
+ {hideDimensionLabel: true, maxDim, naturalWidth, naturalHeight});
+ }),
+
+ _onNewSelection: function () {
+ if (this.previewTooltip) {
+ this.previewTooltip.hide();
+ }
+
+ if (this.colorPicker) {
+ this.colorPicker.hide();
+ }
+
+ if (this.cubicBezier) {
+ this.cubicBezier.hide();
+ }
+
+ if (this.cssDocs) {
+ this.cssDocs.hide();
+ }
+
+ if (this.filterEditor) {
+ this.filterEditor.hide();
+ }
+ },
+
+ /**
+ * Destroy this overlay instance, removing it from the view
+ */
+ destroy: function () {
+ this.removeFromView();
+
+ this.view.inspector.selection.off("new-node-front", this._onNewSelection);
+ this.view = null;
+
+ this._isDestroyed = true;
+ }
+};
+
+module.exports = TooltipsOverlay;
diff --git a/devtools/client/inspector/shared/utils.js b/devtools/client/inspector/shared/utils.js
new file mode 100644
index 000000000..60dda914c
--- /dev/null
+++ b/devtools/client/inspector/shared/utils.js
@@ -0,0 +1,161 @@
+/* -*- 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 {parseDeclarations} = require("devtools/shared/css/parsing-utils");
+const promise = require("promise");
+const {getCSSLexer} = require("devtools/shared/css/lexer");
+const {KeyCodes} = require("devtools/client/shared/keycodes");
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+
+/**
+ * Create a child element with a set of attributes.
+ *
+ * @param {Element} parent
+ * The parent node.
+ * @param {string} tagName
+ * The tag name.
+ * @param {object} attributes
+ * A set of attributes to set on the node.
+ */
+function createChild(parent, tagName, attributes = {}) {
+ let elt = parent.ownerDocument.createElementNS(HTML_NS, tagName);
+ for (let attr in attributes) {
+ if (attributes.hasOwnProperty(attr)) {
+ if (attr === "textContent") {
+ elt.textContent = attributes[attr];
+ } else if (attr === "child") {
+ elt.appendChild(attributes[attr]);
+ } else {
+ elt.setAttribute(attr, attributes[attr]);
+ }
+ }
+ }
+ parent.appendChild(elt);
+ return elt;
+}
+
+exports.createChild = createChild;
+
+/**
+ * Append a text node to an element.
+ *
+ * @param {Element} parent
+ * The parent node.
+ * @param {string} text
+ * The text content for the text node.
+ */
+function appendText(parent, text) {
+ parent.appendChild(parent.ownerDocument.createTextNode(text));
+}
+
+exports.appendText = appendText;
+
+/**
+ * Called when a character is typed in a value editor. This decides
+ * whether to advance or not, first by checking to see if ";" was
+ * typed, and then by lexing the input and seeing whether the ";"
+ * would be a terminator at this point.
+ *
+ * @param {number} keyCode
+ * Key code to be checked.
+ * @param {string} aValue
+ * Current text editor value.
+ * @param {number} insertionPoint
+ * The index of the insertion point.
+ * @return {Boolean} True if the focus should advance; false if
+ * the character should be inserted.
+ */
+function advanceValidate(keyCode, value, insertionPoint) {
+ // Only ";" has special handling here.
+ if (keyCode !== KeyCodes.DOM_VK_SEMICOLON) {
+ return false;
+ }
+
+ // Insert the character provisionally and see what happens. If we
+ // end up with a ";" symbol token, then the semicolon terminates the
+ // value. Otherwise it's been inserted in some spot where it has a
+ // valid meaning, like a comment or string.
+ value = value.slice(0, insertionPoint) + ";" + value.slice(insertionPoint);
+ let lexer = getCSSLexer(value);
+ while (true) {
+ let token = lexer.nextToken();
+ if (token.endOffset > insertionPoint) {
+ if (token.tokenType === "symbol" && token.text === ";") {
+ // The ";" is a terminator.
+ return true;
+ }
+ // The ";" is not a terminator in this context.
+ break;
+ }
+ }
+ return false;
+}
+
+exports.advanceValidate = advanceValidate;
+
+/**
+ * Create a throttling function wrapper to regulate its frequency.
+ *
+ * @param {Function} func
+ * The function to throttle
+ * @param {number} wait
+ * The throttling period
+ * @param {Object} scope
+ * The scope to use for func
+ * @return {Function} The throttled function
+ */
+function throttle(func, wait, scope) {
+ let timer = null;
+
+ return function () {
+ if (timer) {
+ clearTimeout(timer);
+ }
+
+ let args = arguments;
+ timer = setTimeout(function () {
+ timer = null;
+ func.apply(scope, args);
+ }, wait);
+ };
+}
+
+exports.throttle = throttle;
+
+/**
+ * Event handler that causes a blur on the target if the input has
+ * multiple CSS properties as the value.
+ */
+function blurOnMultipleProperties(cssProperties) {
+ return (e) => {
+ setTimeout(() => {
+ let props = parseDeclarations(cssProperties.isKnown, e.target.value);
+ if (props.length > 1) {
+ e.target.blur();
+ }
+ }, 0);
+ };
+}
+
+exports.blurOnMultipleProperties = blurOnMultipleProperties;
+
+/**
+ * Log the provided error to the console and return a rejected Promise for
+ * this error.
+ *
+ * @param {Error} error
+ * The error to log
+ * @return {Promise} A rejected promise
+ */
+function promiseWarn(error) {
+ console.error(error);
+ return promise.reject(error);
+}
+
+exports.promiseWarn = promiseWarn;
diff --git a/devtools/client/inspector/test/.eslintrc.js b/devtools/client/inspector/test/.eslintrc.js
new file mode 100644
index 000000000..8d15a76d9
--- /dev/null
+++ b/devtools/client/inspector/test/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../../.eslintrc.mochitests.js"
+};
diff --git a/devtools/client/inspector/test/browser.ini b/devtools/client/inspector/test/browser.ini
new file mode 100644
index 000000000..65ad71c0c
--- /dev/null
+++ b/devtools/client/inspector/test/browser.ini
@@ -0,0 +1,172 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ doc_inspector_add_node.html
+ doc_inspector_breadcrumbs.html
+ doc_inspector_breadcrumbs_visibility.html
+ doc_inspector_csp.html
+ doc_inspector_csp.html^headers^
+ doc_inspector_delete-selected-node-01.html
+ doc_inspector_delete-selected-node-02.html
+ doc_inspector_embed.html
+ doc_inspector_gcli-inspect-command.html
+ doc_inspector_highlight_after_transition.html
+ doc_inspector_highlighter-comments.html
+ doc_inspector_highlighter-geometry_01.html
+ doc_inspector_highlighter-geometry_02.html
+ doc_inspector_highlighter_csstransform.html
+ doc_inspector_highlighter_dom.html
+ doc_inspector_highlighter_inline.html
+ doc_inspector_highlighter.html
+ doc_inspector_highlighter_rect.html
+ doc_inspector_highlighter_rect_iframe.html
+ doc_inspector_highlighter_xbl.xul
+ doc_inspector_infobar_01.html
+ doc_inspector_infobar_02.html
+ doc_inspector_infobar_03.html
+ doc_inspector_infobar_textnode.html
+ doc_inspector_long-divs.html
+ doc_inspector_menu.html
+ doc_inspector_outerhtml.html
+ doc_inspector_remove-iframe-during-load.html
+ doc_inspector_search.html
+ doc_inspector_search-reserved.html
+ doc_inspector_search-suggestions.html
+ doc_inspector_search-svg.html
+ doc_inspector_select-last-selected-01.html
+ doc_inspector_select-last-selected-02.html
+ doc_inspector_svg.svg
+ head.js
+ shared-head.js
+ !/devtools/client/commandline/test/helpers.js
+ !/devtools/client/framework/test/shared-head.js
+ !/devtools/client/shared/test/test-actor.js
+ !/devtools/client/shared/test/test-actor-registry.js
+
+[browser_inspector_addNode_01.js]
+[browser_inspector_addNode_02.js]
+[browser_inspector_addNode_03.js]
+[browser_inspector_addSidebarTab.js]
+[browser_inspector_breadcrumbs.js]
+[browser_inspector_breadcrumbs_highlight_hover.js]
+[browser_inspector_breadcrumbs_keybinding.js]
+[browser_inspector_breadcrumbs_keyboard_trap.js]
+skip-if = os == "mac" # Full keyboard navigation on OSX only works if Full Keyboard Access setting is set to All Control in System Keyboard Preferences
+[browser_inspector_breadcrumbs_mutations.js]
+[browser_inspector_breadcrumbs_namespaced.js]
+[browser_inspector_breadcrumbs_visibility.js]
+[browser_inspector_delete-selected-node-01.js]
+[browser_inspector_delete-selected-node-02.js]
+[browser_inspector_delete-selected-node-03.js]
+[browser_inspector_destroy-after-navigation.js]
+[browser_inspector_destroy-before-ready.js]
+[browser_inspector_expand-collapse.js]
+[browser_inspector_gcli-inspect-command.js]
+[browser_inspector_highlighter-01.js]
+[browser_inspector_highlighter-02.js]
+[browser_inspector_highlighter-03.js]
+[browser_inspector_highlighter-04.js]
+[browser_inspector_highlighter-by-type.js]
+[browser_inspector_highlighter-cancel.js]
+[browser_inspector_highlighter-comments.js]
+[browser_inspector_highlighter-cssgrid_01.js]
+[browser_inspector_highlighter-csstransform_01.js]
+[browser_inspector_highlighter-csstransform_02.js]
+[browser_inspector_highlighter-embed.js]
+[browser_inspector_highlighter-eyedropper-clipboard.js]
+subsuite = clipboard
+[browser_inspector_highlighter-eyedropper-csp.js]
+[browser_inspector_highlighter-eyedropper-events.js]
+[browser_inspector_highlighter-eyedropper-label.js]
+[browser_inspector_highlighter-eyedropper-show-hide.js]
+[browser_inspector_highlighter-eyedropper-xul.js]
+[browser_inspector_highlighter-geometry_01.js]
+[browser_inspector_highlighter-geometry_02.js]
+[browser_inspector_highlighter-geometry_03.js]
+[browser_inspector_highlighter-geometry_04.js]
+[browser_inspector_highlighter-geometry_05.js]
+[browser_inspector_highlighter-geometry_06.js]
+[browser_inspector_highlighter-hover_01.js]
+[browser_inspector_highlighter-hover_02.js]
+[browser_inspector_highlighter-hover_03.js]
+[browser_inspector_highlighter-iframes_01.js]
+[browser_inspector_highlighter-iframes_02.js]
+[browser_inspector_highlighter-inline.js]
+[browser_inspector_highlighter-keybinding_01.js]
+[browser_inspector_highlighter-keybinding_02.js]
+[browser_inspector_highlighter-keybinding_03.js]
+[browser_inspector_highlighter-keybinding_04.js]
+[browser_inspector_highlighter-measure_01.js]
+[browser_inspector_highlighter-measure_02.js]
+[browser_inspector_highlighter-options.js]
+[browser_inspector_highlighter-preview.js]
+[browser_inspector_highlighter-rect_01.js]
+[browser_inspector_highlighter-rect_02.js]
+[browser_inspector_highlighter-rulers_01.js]
+[browser_inspector_highlighter-rulers_02.js]
+[browser_inspector_highlighter-selector_01.js]
+[browser_inspector_highlighter-selector_02.js]
+[browser_inspector_highlighter-xbl.js]
+[browser_inspector_highlighter-zoom.js]
+[browser_inspector_iframe-navigation.js]
+[browser_inspector_infobar_01.js]
+[browser_inspector_infobar_02.js]
+[browser_inspector_infobar_03.js]
+[browser_inspector_infobar_textnode.js]
+[browser_inspector_initialization.js]
+skip-if = (e10s && debug) # Bug 1250058 - Docshell leak on debug e10s
+[browser_inspector_inspect-object-element.js]
+[browser_inspector_invalidate.js]
+[browser_inspector_keyboard-shortcuts-copy-outerhtml.js]
+subsuite = clipboard
+[browser_inspector_keyboard-shortcuts.js]
+[browser_inspector_menu-01-sensitivity.js]
+subsuite = clipboard
+[browser_inspector_menu-02-copy-items.js]
+subsuite = clipboard
+[browser_inspector_menu-03-paste-items.js]
+subsuite = clipboard
+[browser_inspector_menu-03-paste-items-svg.js]
+subsuite = clipboard
+[browser_inspector_menu-04-use-in-console.js]
+[browser_inspector_menu-05-attribute-items.js]
+[browser_inspector_menu-06-other.js]
+[browser_inspector_navigation.js]
+[browser_inspector_navigate_to_errors.js]
+[browser_inspector_open_on_neterror.js]
+[browser_inspector_pane-toggle-01.js]
+[browser_inspector_pane-toggle-02.js]
+[browser_inspector_pane-toggle-03.js]
+[browser_inspector_pane-toggle-05.js]
+skip-if = os == "mac" # Full keyboard navigation on OSX only works if Full Keyboard Access setting is set to All Control in System Keyboard
+[browser_inspector_picker-stop-on-destroy.js]
+[browser_inspector_picker-stop-on-tool-change.js]
+[browser_inspector_portrait_mode.js]
+[browser_inspector_pseudoclass-lock.js]
+[browser_inspector_pseudoclass-menu.js]
+[browser_inspector_reload-01.js]
+[browser_inspector_reload-02.js]
+[browser_inspector_remove-iframe-during-load.js]
+[browser_inspector_search-01.js]
+[browser_inspector_search-02.js]
+[browser_inspector_search-03.js]
+[browser_inspector_search-04.js]
+[browser_inspector_search-05.js]
+[browser_inspector_search-06.js]
+[browser_inspector_search-07.js]
+[browser_inspector_search-08.js]
+[browser_inspector_search-clear.js]
+[browser_inspector_search-filter_context-menu.js]
+subsuite = clipboard
+[browser_inspector_search_keyboard_trap.js]
+[browser_inspector_search-label.js]
+[browser_inspector_search-reserved.js]
+[browser_inspector_search-selection.js]
+[browser_inspector_search-sidebar.js]
+[browser_inspector_select-docshell.js]
+[browser_inspector_select-last-selected.js]
+[browser_inspector_search-navigation.js]
+[browser_inspector_sidebarstate.js]
+[browser_inspector_switch-to-inspector-on-pick.js]
+[browser_inspector_textbox-menu.js]
diff --git a/devtools/client/inspector/test/browser_inspector_addNode_01.js b/devtools/client/inspector/test/browser_inspector_addNode_01.js
new file mode 100644
index 000000000..f90cb6c5c
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_addNode_01.js
@@ -0,0 +1,22 @@
+/* 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";
+
+// Test that the add node button and context menu items are present in the UI.
+
+const TEST_URL = "data:text/html;charset=utf-8,<h1>Add node</h1>";
+
+add_task(function* () {
+ let {inspector} = yield openInspectorForURL(TEST_URL);
+ let {panelDoc} = inspector;
+
+ let allMenuItems = openContextMenuAndGetAllItems(inspector);
+ let menuItem = allMenuItems.find(item => item.id === "node-menu-add");
+ ok(menuItem, "The item is in the menu");
+
+ let toolbarButton =
+ panelDoc.querySelector("#inspector-toolbar #inspector-element-add-button");
+ ok(toolbarButton, "The add button is in the toolbar");
+});
diff --git a/devtools/client/inspector/test/browser_inspector_addNode_02.js b/devtools/client/inspector/test/browser_inspector_addNode_02.js
new file mode 100644
index 000000000..2421f9df3
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_addNode_02.js
@@ -0,0 +1,63 @@
+/* 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";
+
+// Test that the add node button and context menu items have the right state
+// depending on the current selection.
+
+const TEST_URL = URL_ROOT + "doc_inspector_add_node.html";
+
+add_task(function* () {
+ let {inspector} = yield openInspectorForURL(TEST_URL);
+
+ info("Select the DOCTYPE element");
+ let {nodes} = yield inspector.walker.children(inspector.walker.rootNode);
+ yield selectNode(nodes[0], inspector);
+ assertState(false, inspector,
+ "The button and item are disabled on DOCTYPE");
+
+ info("Select the ::before pseudo-element");
+ let body = yield getNodeFront("body", inspector);
+ ({nodes} = yield inspector.walker.children(body));
+ yield selectNode(nodes[0], inspector);
+ assertState(false, inspector,
+ "The button and item are disabled on a pseudo-element");
+
+ info("Select the svg element");
+ yield selectNode("svg", inspector);
+ assertState(false, inspector,
+ "The button and item are disabled on a SVG element");
+
+ info("Select the div#foo element");
+ yield selectNode("#foo", inspector);
+ assertState(true, inspector,
+ "The button and item are enabled on a DIV element");
+
+ info("Select the documentElement element (html)");
+ yield selectNode("html", inspector);
+ assertState(false, inspector,
+ "The button and item are disabled on the documentElement");
+
+ info("Select the iframe element");
+ yield selectNode("iframe", inspector);
+ assertState(false, inspector,
+ "The button and item are disabled on an IFRAME element");
+});
+
+function assertState(isEnabled, inspector, desc) {
+ let doc = inspector.panelDoc;
+ let btn = doc.querySelector("#inspector-element-add-button");
+
+ // Force an update of the context menu to make sure menu items are updated
+ // according to the current selection. This normally happens when the menu is
+ // opened, but for the sake of this test's simplicity, we directly call the
+ // private update function instead.
+ let allMenuItems = openContextMenuAndGetAllItems(inspector);
+ let menuItem = allMenuItems.find(item => item.id === "node-menu-add");
+ ok(menuItem, "The item is in the menu");
+ is(!menuItem.disabled, isEnabled, desc);
+
+ is(!btn.hasAttribute("disabled"), isEnabled, desc);
+}
diff --git a/devtools/client/inspector/test/browser_inspector_addNode_03.js b/devtools/client/inspector/test/browser_inspector_addNode_03.js
new file mode 100644
index 000000000..38a8369ec
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_addNode_03.js
@@ -0,0 +1,84 @@
+/* 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";
+
+// Test that adding nodes does work as expected: the parent gets expanded, the
+// new node gets selected.
+
+const TEST_URL = URL_ROOT + "doc_inspector_add_node.html";
+const PARENT_TREE_LEVEL = 3;
+
+add_task(function* () {
+ let {inspector} = yield openInspectorForURL(TEST_URL);
+
+ info("Adding in element that has no children and is collapsed");
+ let parentNode = yield getNodeFront("#foo", inspector);
+ yield selectNode(parentNode, inspector);
+ yield testAddNode(parentNode, inspector);
+
+ info("Adding in element with children but that has not been expanded yet");
+ parentNode = yield getNodeFront("#bar", inspector);
+ yield selectNode(parentNode, inspector);
+ yield testAddNode(parentNode, inspector);
+
+ info("Adding in element with children that has been expanded then collapsed");
+ // Select again #bar and collapse it.
+ parentNode = yield getNodeFront("#bar", inspector);
+ yield selectNode(parentNode, inspector);
+ collapseNode(parentNode, inspector);
+ yield testAddNode(parentNode, inspector);
+
+ info("Adding in element with children that is expanded");
+ parentNode = yield getNodeFront("#bar", inspector);
+ yield selectNode(parentNode, inspector);
+ yield testAddNode(parentNode, inspector);
+});
+
+function* testAddNode(parentNode, inspector) {
+ let btn = inspector.panelDoc.querySelector("#inspector-element-add-button");
+ let markupWindow = inspector.markup.win;
+ let parentContainer = inspector.markup.getContainer(parentNode);
+
+ is(parentContainer.tagLine.getAttribute("aria-level"), PARENT_TREE_LEVEL,
+ "Parent level should be up to date.");
+
+ info("Clicking 'add node' and expecting a markup mutation and focus event");
+ let onMutation = inspector.once("markupmutation");
+ btn.click();
+ let mutations = yield onMutation;
+
+ info("Expecting an inspector-updated event right after the mutation event " +
+ "to wait for the new node selection");
+ yield inspector.once("inspector-updated");
+
+ is(mutations.length, 1, "There is one mutation only");
+ is(mutations[0].added.length, 1, "There is one new node only");
+
+ let newNode = mutations[0].added[0];
+
+ is(newNode, inspector.selection.nodeFront,
+ "The new node is selected");
+
+ ok(parentContainer.expanded, "The parent node is now expanded");
+
+ is(inspector.selection.nodeFront.parentNode(), parentNode,
+ "The new node is inside the right parent");
+
+ let focusedElement = markupWindow.document.activeElement;
+ let focusedContainer = focusedElement.container;
+ let selectedContainer = inspector.markup._selectedContainer;
+ is(selectedContainer.tagLine.getAttribute("aria-level"),
+ PARENT_TREE_LEVEL + 1, "Added container level should be up to date.");
+ is(selectedContainer.node, inspector.selection.nodeFront,
+ "The right container is selected in the markup-view");
+ ok(selectedContainer.selected, "Selected container is set to selected");
+ is(focusedContainer.toString(), "[root container]",
+ "Root container is focused");
+}
+
+function collapseNode(node, inspector) {
+ let container = inspector.markup.getContainer(node);
+ container.setExpanded(false);
+}
diff --git a/devtools/client/inspector/test/browser_inspector_addSidebarTab.js b/devtools/client/inspector/test/browser_inspector_addSidebarTab.js
new file mode 100644
index 000000000..77dc2632e
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_addSidebarTab.js
@@ -0,0 +1,62 @@
+/* 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 TEST_URI = "data:text/html;charset=UTF-8," +
+ "<h1>browser_inspector_addtabbar.js</h1>";
+
+const CONTENT_TEXT = "Hello World!";
+
+/**
+ * Verify InspectorPanel.addSidebarTab() API that can be consumed
+ * by DevTools extensions as well as DevTools code base.
+ */
+add_task(function* () {
+ let { inspector } = yield openInspectorForURL(TEST_URI);
+
+ const React = inspector.React;
+ const { div } = React.DOM;
+
+ info("Adding custom panel.");
+
+ // Define custom side-panel.
+ let tabPanel = React.createFactory(React.createClass({
+ displayName: "myTabPanel",
+ render: function () {
+ return (
+ div({className: "my-tab-panel"},
+ CONTENT_TEXT
+ )
+ );
+ }
+ }));
+
+ // Append custom panel (tab) into the Inspector panel and
+ // make sure it's selected by default (the last arg = true).
+ inspector.addSidebarTab("myPanel", "My Panel", tabPanel, true);
+ is(inspector.sidebar.getCurrentTabID(), "myPanel",
+ "My Panel is selected by default");
+
+ // Define another custom side-panel.
+ tabPanel = React.createFactory(React.createClass({
+ displayName: "myTabPanel2",
+ render: function () {
+ return (
+ div({className: "my-tab-panel2"},
+ "Another Content"
+ )
+ );
+ }
+ }));
+
+ // Append second panel, but don't select it by default.
+ inspector.addSidebarTab("myPanel", "My Panel", tabPanel, false);
+ is(inspector.sidebar.getCurrentTabID(), "myPanel",
+ "My Panel is selected by default");
+
+ // Check the the panel content is properly rendered.
+ let tabPanelNode = inspector.panelDoc.querySelector(".my-tab-panel");
+ is(tabPanelNode.textContent, CONTENT_TEXT,
+ "Side panel content has been rendered.");
+});
diff --git a/devtools/client/inspector/test/browser_inspector_breadcrumbs.js b/devtools/client/inspector/test/browser_inspector_breadcrumbs.js
new file mode 100644
index 000000000..e5befff9e
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_breadcrumbs.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";
+
+// Test that the breadcrumbs widget content is correct.
+
+const TEST_URI = URL_ROOT + "doc_inspector_breadcrumbs.html";
+const NODES = [
+ {selector: "#i1111", ids: "i1 i11 i111 i1111", nodeName: "div",
+ title: "div#i1111"},
+ {selector: "#i22", ids: "i2 i22", nodeName: "div",
+ title: "div#i22"},
+ {selector: "#i2111", ids: "i2 i21 i211 i2111", nodeName: "div",
+ title: "div#i2111"},
+ {selector: "#i21", ids: "i2 i21 i211 i2111", nodeName: "div",
+ title: "div#i21"},
+ {selector: "#i22211", ids: "i2 i22 i222 i2221 i22211", nodeName: "div",
+ title: "div#i22211"},
+ {selector: "#i22", ids: "i2 i22 i222 i2221 i22211", nodeName: "div",
+ title: "div#i22"},
+ {selector: "#i3", ids: "i3", nodeName: "article",
+ title: "article#i3"},
+ {selector: "clipPath", ids: "vector clip", nodeName: "clipPath",
+ title: "clipPath#clip"},
+];
+
+add_task(function* () {
+ let { inspector } = yield openInspectorForURL(TEST_URI);
+ let breadcrumbs = inspector.panelDoc.getElementById("inspector-breadcrumbs");
+ let container = breadcrumbs.querySelector(".html-arrowscrollbox-inner");
+
+ for (let node of NODES) {
+ info("Testing node " + node.selector);
+
+ info("Selecting node and waiting for breadcrumbs to update");
+ let breadcrumbsUpdated = inspector.once("breadcrumbs-updated");
+ yield selectNode(node.selector, inspector);
+ yield breadcrumbsUpdated;
+
+ info("Performing checks for node " + node.selector);
+ let buttonsLabelIds = node.ids.split(" ");
+
+ // html > body > …
+ is(container.childNodes.length, buttonsLabelIds.length + 2,
+ "Node " + node.selector + ": Items count");
+
+ for (let i = 2; i < container.childNodes.length; i++) {
+ let expectedId = "#" + buttonsLabelIds[i - 2];
+ let button = container.childNodes[i];
+ let labelId = button.querySelector(".breadcrumbs-widget-item-id");
+ is(labelId.textContent, expectedId,
+ "Node " + node.selector + ": button " + i + " matches");
+ }
+
+ let checkedButton = container.querySelector("button[checked]");
+ let labelId = checkedButton.querySelector(".breadcrumbs-widget-item-id");
+ let id = inspector.selection.nodeFront.id;
+ is(labelId.textContent, "#" + id,
+ "Node " + node.selector + ": selection matches");
+
+ let labelTag = checkedButton.querySelector(".breadcrumbs-widget-item-tag");
+ is(labelTag.textContent, node.nodeName,
+ "Node " + node.selector + " has the expected tag name");
+
+ is(checkedButton.getAttribute("title"), node.title,
+ "Node " + node.selector + " has the expected tooltip");
+ }
+
+ yield testPseudoElements(inspector, container);
+ yield testComments(inspector, container);
+});
+
+function* testPseudoElements(inspector, container) {
+ info("Checking for pseudo elements");
+
+ let pseudoParent = yield getNodeFront("#pseudo-container", inspector);
+ let children = yield inspector.walker.children(pseudoParent);
+ is(children.nodes.length, 2, "Pseudo children returned from walker");
+
+ let beforeElement = children.nodes[0];
+ let breadcrumbsUpdated = inspector.once("breadcrumbs-updated");
+ yield selectNode(beforeElement, inspector);
+ yield breadcrumbsUpdated;
+ is(container.childNodes[3].textContent, "::before",
+ "::before shows up in breadcrumb");
+
+ let afterElement = children.nodes[1];
+ breadcrumbsUpdated = inspector.once("breadcrumbs-updated");
+ yield selectNode(afterElement, inspector);
+ yield breadcrumbsUpdated;
+ is(container.childNodes[3].textContent, "::after",
+ "::before shows up in breadcrumb");
+}
+
+function* testComments(inspector, container) {
+ info("Checking for comment elements");
+
+ let breadcrumbs = inspector.breadcrumbs;
+ let checkedButtonIndex = 2;
+ let button = container.childNodes[checkedButtonIndex];
+
+ let onBreadcrumbsUpdated = inspector.once("breadcrumbs-updated");
+ button.click();
+ yield onBreadcrumbsUpdated;
+
+ is(breadcrumbs.currentIndex, checkedButtonIndex, "New button is selected");
+ ok(breadcrumbs.outer.hasAttribute("aria-activedescendant"),
+ "Active descendant must be set");
+
+ let comment = [...inspector.markup._containers].find(([node]) =>
+ node.nodeType === Ci.nsIDOMNode.COMMENT_NODE)[0];
+
+ let onInspectorUpdated = inspector.once("inspector-updated");
+ inspector.selection.setNodeFront(comment);
+ yield onInspectorUpdated;
+
+ is(breadcrumbs.currentIndex, -1,
+ "When comment is selected no breadcrumb should be checked");
+ ok(!breadcrumbs.outer.hasAttribute("aria-activedescendant"),
+ "Active descendant must not be set");
+
+ onInspectorUpdated = inspector.once("inspector-updated");
+ onBreadcrumbsUpdated = inspector.once("breadcrumbs-updated");
+ button.click();
+ yield Promise.all([onInspectorUpdated, onBreadcrumbsUpdated]);
+
+ is(breadcrumbs.currentIndex, checkedButtonIndex,
+ "Same button is selected again");
+ ok(breadcrumbs.outer.hasAttribute("aria-activedescendant"),
+ "Active descendant must be set again");
+}
diff --git a/devtools/client/inspector/test/browser_inspector_breadcrumbs_highlight_hover.js b/devtools/client/inspector/test/browser_inspector_breadcrumbs_highlight_hover.js
new file mode 100644
index 000000000..6714ea35e
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_breadcrumbs_highlight_hover.js
@@ -0,0 +1,47 @@
+/* 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";
+
+// Test that hovering over nodes on the breadcrumb buttons in the inspector
+// shows the highlighter over those nodes
+add_task(function* () {
+ info("Loading the test document and opening the inspector");
+ let {toolbox, inspector, testActor} = yield openInspectorForURL(
+ "data:text/html;charset=utf-8,<h1>foo</h1><span>bar</span>");
+ info("Selecting the test node");
+ yield selectNode("span", inspector);
+ let bcButtons = inspector.breadcrumbs.container;
+
+ let onNodeHighlighted = toolbox.once("node-highlight");
+ let button = bcButtons.childNodes[1];
+ EventUtils.synthesizeMouseAtCenter(button, {type: "mousemove"},
+ button.ownerDocument.defaultView);
+ yield onNodeHighlighted;
+
+ let isVisible = yield testActor.isHighlighting();
+ ok(isVisible, "The highlighter is shown on a markup container hover");
+
+ ok((yield testActor.assertHighlightedNode("body")),
+ "The highlighter highlights the right node");
+
+ let onNodeUnhighlighted = toolbox.once("node-unhighlight");
+ // move outside of the breadcrumb trail to trigger unhighlight
+ EventUtils.synthesizeMouseAtCenter(inspector.addNodeButton,
+ {type: "mousemove"},
+ inspector.addNodeButton.ownerDocument.defaultView);
+ yield onNodeUnhighlighted;
+
+ onNodeHighlighted = toolbox.once("node-highlight");
+ button = bcButtons.childNodes[2];
+ EventUtils.synthesizeMouseAtCenter(button, {type: "mousemove"},
+ button.ownerDocument.defaultView);
+ yield onNodeHighlighted;
+
+ isVisible = yield testActor.isHighlighting();
+ ok(isVisible, "The highlighter is shown on a markup container hover");
+
+ ok((yield testActor.assertHighlightedNode("span")),
+ "The highlighter highlights the right node");
+});
diff --git a/devtools/client/inspector/test/browser_inspector_breadcrumbs_keybinding.js b/devtools/client/inspector/test/browser_inspector_breadcrumbs_keybinding.js
new file mode 100644
index 000000000..8e72a8bab
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_breadcrumbs_keybinding.js
@@ -0,0 +1,71 @@
+/* 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 breadcrumbs keybindings work.
+
+const TEST_URI = URL_ROOT + "doc_inspector_breadcrumbs.html";
+const TEST_DATA = [{
+ desc: "Pressing left should select the parent <body>",
+ key: "VK_LEFT",
+ newSelection: "body"
+}, {
+ desc: "Pressing left again should select the parent <html>",
+ key: "VK_LEFT",
+ newSelection: "html"
+}, {
+ desc: "Pressing left again should stay on <html>, it's the first element",
+ key: "VK_LEFT",
+ newSelection: "html"
+}, {
+ desc: "Pressing right should go to <body>",
+ key: "VK_RIGHT",
+ newSelection: "body"
+}, {
+ desc: "Pressing right again should go to #i2",
+ key: "VK_RIGHT",
+ newSelection: "#i2"
+}, {
+ desc: "Pressing right again should stay on #i2, it's the last element",
+ key: "VK_RIGHT",
+ newSelection: "#i2"
+}];
+
+add_task(function* () {
+ let {inspector} = yield openInspectorForURL(TEST_URI);
+
+ info("Selecting the test node");
+ yield selectNode("#i2", inspector);
+
+ info("Clicking on the corresponding breadcrumbs node to focus it");
+ let container = inspector.panelDoc.getElementById("inspector-breadcrumbs");
+
+ let button = container.querySelector("button[checked]");
+ button.click();
+
+ let currentSelection = "#id2";
+ for (let {desc, key, newSelection} of TEST_DATA) {
+ info(desc);
+
+ // If the selection will change, wait for the breadcrumb to update,
+ // otherwise continue.
+ let onUpdated = null;
+ if (newSelection !== currentSelection) {
+ info("Expecting a new node to be selected");
+ onUpdated = inspector.once("breadcrumbs-updated");
+ }
+
+ EventUtils.synthesizeKey(key, {});
+ yield onUpdated;
+
+ let newNodeFront = yield getNodeFront(newSelection, inspector);
+ is(newNodeFront, inspector.selection.nodeFront,
+ "The current selection is correct");
+ is(container.getAttribute("aria-activedescendant"),
+ container.querySelector("button[checked]").id,
+ "aria-activedescendant is set correctly");
+
+ currentSelection = newSelection;
+ }
+});
diff --git a/devtools/client/inspector/test/browser_inspector_breadcrumbs_keyboard_trap.js b/devtools/client/inspector/test/browser_inspector_breadcrumbs_keyboard_trap.js
new file mode 100644
index 000000000..16c70650b
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_breadcrumbs_keyboard_trap.js
@@ -0,0 +1,83 @@
+/* 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 ability to tab to and away from breadcrumbs using keyboard.
+
+const TEST_URL = URL_ROOT + "doc_inspector_breadcrumbs.html";
+
+/**
+ * Test data has the format of:
+ * {
+ * desc {String} description for better logging
+ * focused {Boolean} flag, indicating if breadcrumbs contain focus
+ * key {String} key event's key
+ * options {?Object} optional event data such as shiftKey, etc
+ * }
+ */
+const TEST_DATA = [
+ {
+ desc: "Move the focus away from breadcrumbs to a next focusable element",
+ focused: false,
+ key: "VK_TAB",
+ options: { }
+ },
+ {
+ desc: "Move the focus back to the breadcrumbs",
+ focused: true,
+ key: "VK_TAB",
+ options: { shiftKey: true }
+ },
+ {
+ desc: "Move the focus back away from breadcrumbs to a previous focusable " +
+ "element",
+ focused: false,
+ key: "VK_TAB",
+ options: { shiftKey: true }
+ },
+ {
+ desc: "Move the focus back to the breadcrumbs",
+ focused: true,
+ key: "VK_TAB",
+ options: { }
+ }
+];
+
+add_task(function* () {
+ let { toolbox, inspector } = yield openInspectorForURL(TEST_URL);
+ let doc = inspector.panelDoc;
+ let {breadcrumbs} = inspector;
+
+ yield selectNode("#i2", inspector);
+
+ info("Clicking on the corresponding breadcrumbs node to focus it");
+ let container = doc.getElementById("inspector-breadcrumbs");
+
+ let button = container.querySelector("button[checked]");
+ let onHighlight = toolbox.once("node-highlight");
+ button.click();
+ yield onHighlight;
+
+ // Ensure a breadcrumb is focused.
+ is(doc.activeElement, container, "Focus is on selected breadcrumb");
+ is(container.getAttribute("aria-activedescendant"), button.id,
+ "aria-activedescendant is set correctly");
+
+ for (let { desc, focused, key, options } of TEST_DATA) {
+ info(desc);
+
+ EventUtils.synthesizeKey(key, options);
+ // Wait until the keyPromise promise resolves.
+ yield breadcrumbs.keyPromise;
+
+ if (focused) {
+ is(doc.activeElement, container, "Focus is on selected breadcrumb");
+ } else {
+ ok(!containsFocus(doc, container), "Focus is outside of breadcrumbs");
+ }
+ is(container.getAttribute("aria-activedescendant"), button.id,
+ "aria-activedescendant is set correctly");
+ }
+});
diff --git a/devtools/client/inspector/test/browser_inspector_breadcrumbs_mutations.js b/devtools/client/inspector/test/browser_inspector_breadcrumbs_mutations.js
new file mode 100644
index 000000000..100ee275a
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_breadcrumbs_mutations.js
@@ -0,0 +1,212 @@
+/* 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 breadcrumbs widget refreshes correctly when there are markup
+// mutations (and that it doesn't refresh when those mutations don't change its
+// output).
+
+const TEST_URI = URL_ROOT + "doc_inspector_breadcrumbs.html";
+
+// Each item in the TEST_DATA array is a test case that should contain the
+// following properties:
+// - desc {String} A description of this test case (will be logged).
+// - setup {Function*} A generator function (can yield promises) that sets up
+// the test case. Useful for selecting a node before starting the test.
+// - run {Function*} A generator function (can yield promises) that runs the
+// actual test case, i.e, mutates the content DOM to cause the breadcrumbs
+// to refresh, or not.
+// - shouldRefresh {Boolean} Once the `run` function has completed, and the test
+// has detected that the page has changed, this boolean instructs the test to
+// verify if the breadcrumbs has refreshed or not.
+// - output {Array} A list of strings for the text that should be found in each
+// button after the test has run.
+const TEST_DATA = [{
+ desc: "Adding a child at the end of the chain shouldn't change anything",
+ setup: function* (inspector) {
+ yield selectNode("#i1111", inspector);
+ },
+ run: function* ({walker, selection}) {
+ yield walker.setInnerHTML(selection.nodeFront, "<b>test</b>");
+ },
+ shouldRefresh: false,
+ output: ["html", "body", "article#i1", "div#i11", "div#i111", "div#i1111"]
+}, {
+ desc: "Updating an ID to an displayed element should refresh",
+ setup: function* () {},
+ run: function* ({walker}) {
+ let node = yield walker.querySelector(walker.rootNode, "#i1");
+ yield node.modifyAttributes([{
+ attributeName: "id",
+ newValue: "i1-changed"
+ }]);
+ },
+ shouldRefresh: true,
+ output: ["html", "body", "article#i1-changed", "div#i11", "div#i111",
+ "div#i1111"]
+}, {
+ desc: "Updating an class to a displayed element should refresh",
+ setup: function* () {},
+ run: function* ({walker}) {
+ let node = yield walker.querySelector(walker.rootNode, "body");
+ yield node.modifyAttributes([{
+ attributeName: "class",
+ newValue: "test-class"
+ }]);
+ },
+ shouldRefresh: true,
+ output: ["html", "body.test-class", "article#i1-changed", "div#i11",
+ "div#i111", "div#i1111"]
+}, {
+ desc: "Updating a non id/class attribute to a displayed element should not " +
+ "refresh",
+ setup: function* () {},
+ run: function* ({walker}) {
+ let node = yield walker.querySelector(walker.rootNode, "#i11");
+ yield node.modifyAttributes([{
+ attributeName: "name",
+ newValue: "value"
+ }]);
+ },
+ shouldRefresh: false,
+ output: ["html", "body.test-class", "article#i1-changed", "div#i11",
+ "div#i111", "div#i1111"]
+}, {
+ desc: "Moving a child in an element that's not displayed should not refresh",
+ setup: function* () {},
+ run: function* ({walker}) {
+ // Re-append #i1211 as a last child of #i2.
+ let parent = yield walker.querySelector(walker.rootNode, "#i2");
+ let child = yield walker.querySelector(walker.rootNode, "#i211");
+ yield walker.insertBefore(child, parent);
+ },
+ shouldRefresh: false,
+ output: ["html", "body.test-class", "article#i1-changed", "div#i11",
+ "div#i111", "div#i1111"]
+}, {
+ desc: "Moving an undisplayed child in a displayed element should not refresh",
+ setup: function* () {},
+ run: function* ({walker}) {
+ // Re-append #i2 in body (move it to the end).
+ let parent = yield walker.querySelector(walker.rootNode, "body");
+ let child = yield walker.querySelector(walker.rootNode, "#i2");
+ yield walker.insertBefore(child, parent);
+ },
+ shouldRefresh: false,
+ output: ["html", "body.test-class", "article#i1-changed", "div#i11",
+ "div#i111", "div#i1111"]
+}, {
+ desc: "Updating attributes on an element that's not displayed should not " +
+ "refresh",
+ setup: function* () {},
+ run: function* ({walker}) {
+ let node = yield walker.querySelector(walker.rootNode, "#i2");
+ yield node.modifyAttributes([{
+ attributeName: "id",
+ newValue: "i2-changed"
+ }, {
+ attributeName: "class",
+ newValue: "test-class"
+ }]);
+ },
+ shouldRefresh: false,
+ output: ["html", "body.test-class", "article#i1-changed", "div#i11",
+ "div#i111", "div#i1111"]
+}, {
+ desc: "Removing the currently selected node should refresh",
+ setup: function* (inspector) {
+ yield selectNode("#i2-changed", inspector);
+ },
+ run: function* ({walker, selection}) {
+ yield walker.removeNode(selection.nodeFront);
+ },
+ shouldRefresh: true,
+ output: ["html", "body.test-class"]
+}, {
+ desc: "Changing the class of the currently selected node should refresh",
+ setup: function* () {},
+ run: function* ({selection}) {
+ yield selection.nodeFront.modifyAttributes([{
+ attributeName: "class",
+ newValue: "test-class-changed"
+ }]);
+ },
+ shouldRefresh: true,
+ output: ["html", "body.test-class-changed"]
+}, {
+ desc: "Changing the id of the currently selected node should refresh",
+ setup: function* () {},
+ run: function* ({selection}) {
+ yield selection.nodeFront.modifyAttributes([{
+ attributeName: "id",
+ newValue: "new-id"
+ }]);
+ },
+ shouldRefresh: true,
+ output: ["html", "body#new-id.test-class-changed"]
+}];
+
+add_task(function* () {
+ let {inspector} = yield openInspectorForURL(TEST_URI);
+ let breadcrumbs = inspector.panelDoc.getElementById("inspector-breadcrumbs");
+ let container = breadcrumbs.querySelector(".html-arrowscrollbox-inner");
+ let win = container.ownerDocument.defaultView;
+
+ for (let {desc, setup, run, shouldRefresh, output} of TEST_DATA) {
+ info("Running test case: " + desc);
+
+ info("Listen to markupmutation events from the inspector to know when a " +
+ "test case has completed");
+ let onContentMutation = inspector.once("markupmutation");
+
+ info("Running setup");
+ yield setup(inspector);
+
+ info("Listen to mutations on the breadcrumbs container");
+ let hasBreadcrumbsMutated = false;
+ let observer = new win.MutationObserver(mutations => {
+ // Only consider childList changes or tooltiptext/checked attributes
+ // changes. The rest may be mutations caused by the overflowing arrowbox.
+ for (let {type, attributeName} of mutations) {
+ let isChildList = type === "childList";
+ let isAttributes = type === "attributes" &&
+ (attributeName === "checked" ||
+ attributeName === "tooltiptext");
+ if (isChildList || isAttributes) {
+ hasBreadcrumbsMutated = true;
+ break;
+ }
+ }
+ });
+ observer.observe(container, {
+ attributes: true,
+ childList: true,
+ subtree: true
+ });
+
+ info("Running the test case");
+ yield run(inspector);
+
+ info("Wait until the page has mutated");
+ yield onContentMutation;
+
+ if (shouldRefresh) {
+ info("The breadcrumbs is expected to refresh, so wait for it");
+ yield inspector.once("inspector-updated");
+ } else {
+ ok(!inspector._updateProgress,
+ "The breadcrumbs widget is not currently updating");
+ }
+
+ is(shouldRefresh, hasBreadcrumbsMutated, "Has the breadcrumbs refreshed?");
+ observer.disconnect();
+
+ info("Check the output of the breadcrumbs widget");
+ is(container.childNodes.length, output.length, "Correct number of buttons");
+ for (let i = 0; i < container.childNodes.length; i++) {
+ is(output[i], container.childNodes[i].textContent,
+ "Text content for button " + i + " is correct");
+ }
+ }
+});
diff --git a/devtools/client/inspector/test/browser_inspector_breadcrumbs_namespaced.js b/devtools/client/inspector/test/browser_inspector_breadcrumbs_namespaced.js
new file mode 100644
index 000000000..0b14ef1b0
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_breadcrumbs_namespaced.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";
+
+// Test that the breadcrumbs widget content for namespaced elements is correct.
+
+const XHTML = `
+ <!DOCTYPE html>
+ <html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:svg="http://www.w3.org/2000/svg">
+ <body>
+ <svg:svg width="100" height="100">
+ <svg:clipPath id="clip">
+ <svg:rect id="rectangle" x="0" y="0" width="10" height="5"></svg:rect>
+ </svg:clipPath>
+ <svg:circle cx="0" cy="0" r="5"></svg:circle>
+ </svg:svg>
+ </body>
+ </html>
+`;
+
+const TEST_URI = "data:application/xhtml+xml;charset=utf-8," + encodeURI(XHTML);
+
+const NODES = [
+ {selector: "clipPath", nodes: ["svg:svg", "svg:clipPath"],
+ nodeName: "svg:clipPath", title: "svg:clipPath#clip"},
+ {selector: "circle", nodes: ["svg:svg", "svg:circle"],
+ nodeName: "svg:circle", title: "svg:circle"},
+];
+
+add_task(function* () {
+ let { inspector } = yield openInspectorForURL(TEST_URI);
+ let container = inspector.panelDoc.getElementById("inspector-breadcrumbs");
+
+ for (let node of NODES) {
+ info("Testing node " + node.selector);
+
+ info("Selecting node and waiting for breadcrumbs to update");
+ let breadcrumbsUpdated = inspector.once("breadcrumbs-updated");
+ yield selectNode(node.selector, inspector);
+ yield breadcrumbsUpdated;
+
+ info("Performing checks for node " + node.selector);
+
+ let checkedButton = container.querySelector("button[checked]");
+
+ let labelTag = checkedButton.querySelector(".breadcrumbs-widget-item-tag");
+ is(labelTag.textContent, node.nodeName,
+ "Node " + node.selector + " has the expected tag name");
+
+ is(checkedButton.getAttribute("title"), node.title,
+ "Node " + node.selector + " has the expected tooltip");
+ }
+});
diff --git a/devtools/client/inspector/test/browser_inspector_breadcrumbs_visibility.js b/devtools/client/inspector/test/browser_inspector_breadcrumbs_visibility.js
new file mode 100644
index 000000000..caee745c9
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_breadcrumbs_visibility.js
@@ -0,0 +1,110 @@
+/* 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 start and end buttons on the breadcrumb trail bring the right
+// crumbs into the visible area, for both LTR and RTL
+
+let { Toolbox } = require("devtools/client/framework/toolbox");
+
+const TEST_URI = URL_ROOT + "doc_inspector_breadcrumbs_visibility.html";
+const NODE_ONE = "div#aVeryLongIdToExceedTheBreadcrumbTruncationLimit";
+const NODE_TWO = "div#anotherVeryLongIdToExceedTheBreadcrumbTruncationLimit";
+const NODE_THREE = "div#aThirdVeryLongIdToExceedTheTruncationLimit";
+const NODE_FOUR = "div#aFourthOneToExceedTheTruncationLimit";
+const NODE_FIVE = "div#aFifthOneToExceedTheTruncationLimit";
+const NODE_SIX = "div#aSixthOneToExceedTheTruncationLimit";
+const NODE_SEVEN = "div#aSeventhOneToExceedTheTruncationLimit";
+
+const NODES = [
+ { action: "start", title: NODE_SIX },
+ { action: "start", title: NODE_FIVE },
+ { action: "start", title: NODE_FOUR },
+ { action: "start", title: NODE_THREE },
+ { action: "start", title: NODE_TWO },
+ { action: "start", title: NODE_ONE },
+ { action: "end", title: NODE_TWO },
+ { action: "end", title: NODE_THREE },
+ { action: "end", title: NODE_FOUR },
+ { action: "end", title: NODE_FIVE },
+ { action: "end", title: NODE_SIX }
+];
+
+add_task(function* () {
+ // This test needs specific initial size of the sidebar.
+ yield pushPref("devtools.toolsidebar-width.inspector", 350);
+ yield pushPref("devtools.toolsidebar-height.inspector", 150);
+
+ let { inspector, toolbox } = yield openInspectorForURL(TEST_URI);
+
+ // No way to wait for scrolling to end (Bug 1172171)
+ // Rather than wait a max time; limit test to instant scroll behavior
+ inspector.breadcrumbs.arrowScrollBox.scrollBehavior = "instant";
+
+ yield toolbox.switchHost(Toolbox.HostType.WINDOW);
+ let hostWindow = toolbox.win.parent;
+ let originalWidth = hostWindow.outerWidth;
+ let originalHeight = hostWindow.outerHeight;
+ hostWindow.resizeTo(640, 300);
+
+ info("Testing transitions ltr");
+ yield pushPref("intl.uidirection.en-US", "ltr");
+ yield testBreadcrumbTransitions(hostWindow, inspector);
+
+ info("Testing transitions rtl");
+ yield pushPref("intl.uidirection.en-US", "rtl");
+ yield testBreadcrumbTransitions(hostWindow, inspector);
+
+ hostWindow.resizeTo(originalWidth, originalHeight);
+});
+
+function* testBreadcrumbTransitions(hostWindow, inspector) {
+ let breadcrumbs = inspector.panelDoc.getElementById("inspector-breadcrumbs");
+ let startBtn = breadcrumbs.querySelector(".scrollbutton-up");
+ let endBtn = breadcrumbs.querySelector(".scrollbutton-down");
+ let container = breadcrumbs.querySelector(".html-arrowscrollbox-inner");
+ let breadcrumbsUpdated = inspector.once("breadcrumbs-updated");
+
+ info("Selecting initial node");
+ yield selectNode(NODE_SEVEN, inspector);
+
+ // So just need to wait for a duration
+ yield breadcrumbsUpdated;
+ let initialCrumb = container.querySelector("button[checked]");
+ is(isElementInViewport(hostWindow, initialCrumb), true,
+ "initial element was visible");
+
+ for (let node of NODES) {
+ info("Checking for visibility of crumb " + node.title);
+ if (node.action === "end") {
+ info("Simulating click of end button");
+ EventUtils.synthesizeMouseAtCenter(endBtn, {}, inspector.panelWin);
+ } else if (node.action === "start") {
+ info("Simulating click of start button");
+ EventUtils.synthesizeMouseAtCenter(startBtn, {}, inspector.panelWin);
+ }
+
+ yield breadcrumbsUpdated;
+ let selector = "button[title=\"" + node.title + "\"]";
+ let relevantCrumb = container.querySelector(selector);
+ is(isElementInViewport(hostWindow, relevantCrumb), true,
+ node.title + " crumb is visible");
+ }
+}
+
+function isElementInViewport(window, el) {
+ let rect = el.getBoundingClientRect();
+
+ return (
+ rect.top >= 0 &&
+ rect.left >= 0 &&
+ rect.bottom <= window.innerHeight &&
+ rect.right <= window.innerWidth
+ );
+}
+
+registerCleanupFunction(function () {
+ // Restore the host type for other tests.
+ Services.prefs.clearUserPref("devtools.toolbox.host");
+});
diff --git a/devtools/client/inspector/test/browser_inspector_delete-selected-node-01.js b/devtools/client/inspector/test/browser_inspector_delete-selected-node-01.js
new file mode 100644
index 000000000..3b5049e25
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_delete-selected-node-01.js
@@ -0,0 +1,24 @@
+/* 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 to ensure inspector handles deletion of selected node correctly.
+
+const TEST_URL = URL_ROOT + "doc_inspector_delete-selected-node-01.html";
+
+add_task(function* () {
+ let {inspector} = yield openInspectorForURL(TEST_URL);
+
+ let span = yield getNodeFrontInFrame("span", "iframe", inspector);
+ yield selectNode(span, inspector);
+
+ info("Removing selected <span> element.");
+ let parentNode = span.parentNode();
+ yield inspector.walker.removeNode(span);
+
+ // Wait for the inspector to process the mutation
+ yield inspector.once("inspector-updated");
+ is(inspector.selection.nodeFront, parentNode,
+ "Parent node of selected <span> got selected.");
+});
diff --git a/devtools/client/inspector/test/browser_inspector_delete-selected-node-02.js b/devtools/client/inspector/test/browser_inspector_delete-selected-node-02.js
new file mode 100644
index 000000000..fbd008a89
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_delete-selected-node-02.js
@@ -0,0 +1,154 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set 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";
+
+// Test that when nodes are being deleted in the page, the current selection
+// and therefore the markup view, css rule view, computed view, font view,
+// box model view, and breadcrumbs, reset accordingly to show the right node
+
+const TEST_PAGE = URL_ROOT +
+ "doc_inspector_delete-selected-node-02.html";
+
+add_task(function* () {
+ let { inspector } = yield openInspectorForURL(TEST_PAGE);
+
+ yield testManuallyDeleteSelectedNode();
+ yield testAutomaticallyDeleteSelectedNode();
+ yield testDeleteSelectedNodeContainerFrame();
+ yield testDeleteWithNonElementNode();
+
+ function* testManuallyDeleteSelectedNode() {
+ info("Selecting a node, deleting it via context menu and checking that " +
+ "its parent node is selected and breadcrumbs are updated.");
+
+ yield deleteNodeWithContextMenu("#deleteManually");
+
+ info("Performing checks.");
+ yield assertNodeSelectedAndPanelsUpdated("#selectedAfterDelete",
+ "li#selectedAfterDelete");
+ }
+
+ function* testAutomaticallyDeleteSelectedNode() {
+ info("Selecting a node, deleting it via javascript and checking that " +
+ "its parent node is selected and breadcrumbs are updated.");
+
+ let div = yield getNodeFront("#deleteAutomatically", inspector);
+ yield selectNode(div, inspector);
+
+ info("Deleting selected node via javascript.");
+ yield inspector.walker.removeNode(div);
+
+ info("Waiting for inspector to update.");
+ yield inspector.once("inspector-updated");
+
+ info("Inspector updated, performing checks.");
+ yield assertNodeSelectedAndPanelsUpdated("#deleteChildren",
+ "ul#deleteChildren");
+ }
+
+ function* testDeleteSelectedNodeContainerFrame() {
+ info("Selecting a node inside iframe, deleting the iframe via javascript " +
+ "and checking the parent node of the iframe is selected and " +
+ "breadcrumbs are updated.");
+
+ info("Selecting an element inside iframe.");
+ let iframe = yield getNodeFront("#deleteIframe", inspector);
+ let div = yield getNodeFrontInFrame("#deleteInIframe", iframe, inspector);
+ yield selectNode(div, inspector);
+
+ info("Deleting selected node via javascript.");
+ yield inspector.walker.removeNode(iframe);
+
+ info("Waiting for inspector to update.");
+ yield inspector.once("inspector-updated");
+
+ info("Inspector updated, performing checks.");
+ yield assertNodeSelectedAndPanelsUpdated("body", "body");
+ }
+
+ function* testDeleteWithNonElementNode() {
+ info("Selecting a node, deleting it via context menu and checking that " +
+ "its parent node is selected and breadcrumbs are updated " +
+ "when the node is followed by a non-element node");
+
+ yield deleteNodeWithContextMenu("#deleteWithNonElement");
+
+ let expectedCrumbs = ["html", "body", "div#deleteToMakeSingleTextNode"];
+ yield assertNodeSelectedAndCrumbsUpdated(expectedCrumbs,
+ Node.TEXT_NODE);
+
+ // Delete node with key, as cannot delete text node with
+ // context menu at this time.
+ inspector.markup._frame.focus();
+ EventUtils.synthesizeKey("VK_DELETE", {});
+ yield inspector.once("inspector-updated");
+
+ expectedCrumbs = ["html", "body", "div#deleteToMakeSingleTextNode"];
+ yield assertNodeSelectedAndCrumbsUpdated(expectedCrumbs,
+ Node.ELEMENT_NODE);
+ }
+
+ function* deleteNodeWithContextMenu(selector) {
+ yield selectNode(selector, inspector);
+ let nodeToBeDeleted = inspector.selection.nodeFront;
+
+ info("Getting the node container in the markup view.");
+ let container = yield getContainerForSelector(selector, inspector);
+
+ let allMenuItems = openContextMenuAndGetAllItems(inspector, {
+ target: container.tagLine,
+ });
+ let menuItem = allMenuItems.find(item => item.id === "node-menu-delete");
+
+ info("Clicking 'Delete Node' in the context menu.");
+ is(menuItem.disabled, false, "delete menu item is enabled");
+ menuItem.click();
+
+ // close the open context menu
+ EventUtils.synthesizeKey("VK_ESCAPE", {});
+
+ info("Waiting for inspector to update.");
+ yield inspector.once("inspector-updated");
+
+ // Since the mutations are sent asynchronously from the server, the
+ // inspector-updated event triggered by the deletion might happen before
+ // the mutation is received and the element is removed from the
+ // breadcrumbs. See bug 1284125.
+ if (inspector.breadcrumbs.indexOf(nodeToBeDeleted) > -1) {
+ info("Crumbs haven't seen deletion. Waiting for breadcrumbs-updated.");
+ yield inspector.once("breadcrumbs-updated");
+ }
+
+ return menuItem;
+ }
+
+ function* assertNodeSelectedAndCrumbsUpdated(expectedCrumbs,
+ expectedNodeType) {
+ info("Performing checks");
+ let actualNodeType = inspector.selection.nodeFront.nodeType;
+ is(actualNodeType, expectedNodeType, "The node has the right type");
+
+ let breadcrumbs = inspector.panelDoc.querySelectorAll(
+ "#inspector-breadcrumbs .html-arrowscrollbox-inner > *");
+ is(breadcrumbs.length, expectedCrumbs.length,
+ "Have the correct number of breadcrumbs");
+ for (let i = 0; i < breadcrumbs.length; i++) {
+ is(breadcrumbs[i].textContent, expectedCrumbs[i],
+ "Text content for button " + i + " is correct");
+ }
+ }
+
+ function* assertNodeSelectedAndPanelsUpdated(selector, crumbLabel) {
+ let nodeFront = yield getNodeFront(selector, inspector);
+ is(inspector.selection.nodeFront, nodeFront, "The right node is selected");
+
+ let breadcrumbs = inspector.panelDoc.querySelector(
+ "#inspector-breadcrumbs .html-arrowscrollbox-inner");
+ is(breadcrumbs.querySelector("button[checked=true]").textContent,
+ crumbLabel,
+ "The right breadcrumb is selected");
+ }
+});
diff --git a/devtools/client/inspector/test/browser_inspector_delete-selected-node-03.js b/devtools/client/inspector/test/browser_inspector_delete-selected-node-03.js
new file mode 100644
index 000000000..21057cdb6
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_delete-selected-node-03.js
@@ -0,0 +1,27 @@
+/* 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";
+
+// Test to ensure inspector can handle destruction of selected node inside an
+// iframe.
+
+const TEST_URL = URL_ROOT + "doc_inspector_delete-selected-node-01.html";
+
+add_task(function* () {
+ let { inspector } = yield openInspectorForURL(TEST_URL);
+
+ let iframe = yield getNodeFront("iframe", inspector);
+ let node = yield getNodeFrontInFrame("span", iframe, inspector);
+ yield selectNode(node, inspector);
+
+ info("Removing iframe.");
+ yield inspector.walker.removeNode(iframe);
+ yield inspector.selection.once("detached-front");
+
+ let body = yield getNodeFront("body", inspector);
+
+ is(inspector.selection.nodeFront, body, "Selection is now the body node");
+
+ yield inspector.once("inspector-updated");
+});
diff --git a/devtools/client/inspector/test/browser_inspector_destroy-after-navigation.js b/devtools/client/inspector/test/browser_inspector_destroy-after-navigation.js
new file mode 100644
index 000000000..5fcd5538b
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_destroy-after-navigation.js
@@ -0,0 +1,24 @@
+/* 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";
+
+// Testing that closing the inspector after navigating to a page doesn't fail.
+
+const URL_1 = "data:text/plain;charset=UTF-8,abcde";
+const URL_2 = "data:text/plain;charset=UTF-8,12345";
+
+add_task(function* () {
+ let { inspector, toolbox } = yield openInspectorForURL(URL_1);
+
+ yield navigateTo(inspector, URL_2);
+
+ info("Destroying toolbox");
+ try {
+ yield toolbox.destroy();
+ ok(true, "Toolbox destroyed");
+ } catch (e) {
+ ok(false, "An exception occured while destroying toolbox");
+ console.error(e);
+ }
+});
diff --git a/devtools/client/inspector/test/browser_inspector_destroy-before-ready.js b/devtools/client/inspector/test/browser_inspector_destroy-before-ready.js
new file mode 100644
index 000000000..ac8ad5d37
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_destroy-before-ready.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 switching to the inspector panel and not waiting for it to be fully
+// loaded doesn't fail the test with unhandled rejected promises.
+
+add_task(function* () {
+ // At least one assertion is needed to avoid failing the test, but really,
+ // what we're interested in is just having the test pass when switching to the
+ // inspector.
+ ok(true);
+
+ yield addTab("data:text/html;charset=utf-8,test inspector destroy");
+
+ info("Open the toolbox on the debugger panel");
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ let toolbox = yield gDevTools.showToolbox(target, "jsdebugger");
+
+ info("Switch to the inspector panel and immediately end the test");
+ let onInspectorSelected = toolbox.once("inspector-selected");
+ toolbox.selectTool("inspector");
+ yield onInspectorSelected;
+});
diff --git a/devtools/client/inspector/test/browser_inspector_expand-collapse.js b/devtools/client/inspector/test/browser_inspector_expand-collapse.js
new file mode 100644
index 000000000..3b1dcb6b2
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_expand-collapse.js
@@ -0,0 +1,64 @@
+/* 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";
+
+// Tests that context menu items exapnd all and collapse are shown properly.
+
+const TEST_URL = "data:text/html;charset=utf-8," +
+ "<div id='parent-node'><div id='child-node'></div></div>";
+
+add_task(function* () {
+ // Test is often exceeding time-out threshold, similar to Bug 1137765
+ requestLongerTimeout(2);
+
+ let {inspector} = yield openInspectorForURL(TEST_URL);
+
+ info("Selecting the parent node");
+
+ let front = yield getNodeFrontForSelector("#parent-node", inspector);
+
+ yield selectNode(front, inspector);
+
+ info("Simulating context menu click on the selected node container.");
+ let allMenuItems = openContextMenuAndGetAllItems(inspector, {
+ target: getContainerForNodeFront(front, inspector).tagLine,
+ });
+ let nodeMenuCollapseElement =
+ allMenuItems.find(item => item.id === "node-menu-collapse");
+ let nodeMenuExpandElement =
+ allMenuItems.find(item => item.id === "node-menu-expand");
+
+ ok(nodeMenuCollapseElement.disabled, "Collapse option is disabled");
+ ok(!nodeMenuExpandElement.disabled, "ExpandAll option is enabled");
+
+ info("Testing whether expansion works properly");
+ nodeMenuExpandElement.click();
+
+ info("Waiting for expansion to occur");
+ yield waitForMultipleChildrenUpdates(inspector);
+ let markUpContainer = getContainerForNodeFront(front, inspector);
+ ok(markUpContainer.expanded, "node has been successfully expanded");
+
+ // reselecting node after expansion
+ yield selectNode(front, inspector);
+
+ info("Testing whether collapse works properly");
+ info("Simulating context menu click on the selected node container.");
+ allMenuItems = openContextMenuAndGetAllItems(inspector, {
+ target: getContainerForNodeFront(front, inspector).tagLine,
+ });
+ nodeMenuCollapseElement =
+ allMenuItems.find(item => item.id === "node-menu-collapse");
+ nodeMenuExpandElement =
+ allMenuItems.find(item => item.id === "node-menu-expand");
+
+ ok(!nodeMenuCollapseElement.disabled, "Collapse option is enabled");
+ ok(!nodeMenuExpandElement.disabled, "ExpandAll option is enabled");
+ nodeMenuCollapseElement.click();
+
+ info("Waiting for collapse to occur");
+ yield waitForMultipleChildrenUpdates(inspector);
+ ok(!markUpContainer.expanded, "node has been successfully collapsed");
+});
diff --git a/devtools/client/inspector/test/browser_inspector_gcli-inspect-command.js b/devtools/client/inspector/test/browser_inspector_gcli-inspect-command.js
new file mode 100644
index 000000000..dca8167c4
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_gcli-inspect-command.js
@@ -0,0 +1,118 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint key-spacing: 0 */
+"use strict";
+
+// Testing that the gcli 'inspect' command works as it should.
+
+const TEST_URI = URL_ROOT + "doc_inspector_gcli-inspect-command.html";
+
+add_task(function* () {
+ return helpers.addTabWithToolbar(TEST_URI, Task.async(function* (options) {
+ let {inspector} = yield openInspector();
+
+ let checkSelection = Task.async(function* (selector) {
+ let node = yield getNodeFront(selector, inspector);
+ is(inspector.selection.nodeFront, node, "the current selection is correct");
+ });
+
+ yield helpers.audit(options, [
+ {
+ setup: "inspect",
+ check: {
+ input: "inspect",
+ hints: " <selector>",
+ markup: "VVVVVVV",
+ status: "ERROR",
+ args: {
+ selector: {
+ message: "Value required for \u2018selector\u2019."
+ },
+ }
+ },
+ },
+ {
+ setup: "inspect div",
+ check: {
+ input: "inspect div",
+ hints: "",
+ markup: "VVVVVVVVVVV",
+ status: "VALID",
+ args: {
+ selector: { message: "" },
+ }
+ },
+ exec: {},
+ post: () => checkSelection("div"),
+ },
+ {
+ setup: "inspect .someclass",
+ check: {
+ input: "inspect .someclass",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVVVVV",
+ status: "VALID",
+ args: {
+ selector: { message: "" },
+ }
+ },
+ exec: {},
+ post: () => checkSelection(".someclass"),
+ },
+ {
+ setup: "inspect #someid",
+ check: {
+ input: "inspect #someid",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVV",
+ status: "VALID",
+ args: {
+ selector: { message: "" },
+ }
+ },
+ exec: {},
+ post: () => checkSelection("#someid"),
+ },
+ {
+ setup: "inspect button[disabled]",
+ check: {
+ input: "inspect button[disabled]",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVVVVVVVVVVV",
+ status: "VALID",
+ args: {
+ selector: { message: "" },
+ }
+ },
+ exec: {},
+ post: () => checkSelection("button[disabled]"),
+ },
+ {
+ setup: "inspect p>strong",
+ check: {
+ input: "inspect p>strong",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVVV",
+ status: "VALID",
+ args: {
+ selector: { message: "" },
+ }
+ },
+ exec: {},
+ post: () => checkSelection("p>strong"),
+ },
+ {
+ setup: "inspect :root",
+ check: {
+ input: "inspect :root",
+ hints: "",
+ markup: "VVVVVVVVVVVVV",
+ status: "VALID"
+ },
+ exec: {},
+ post: () => checkSelection(":root"),
+ },
+ ]);
+ }));
+});
diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-01.js b/devtools/client/inspector/test/browser_inspector_highlighter-01.js
new file mode 100644
index 000000000..946b8c3c8
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-01.js
@@ -0,0 +1,31 @@
+/* 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";
+
+// Test that hovering over nodes in the markup-view shows the highlighter over
+// those nodes
+add_task(function* () {
+ info("Loading the test document and opening the inspector");
+ let {toolbox, inspector, testActor} = yield openInspectorForURL(
+ "data:text/html;charset=utf-8,<h1>foo</h1><span>bar</span>");
+
+ let isVisible = yield testActor.isHighlighting(toolbox);
+ ok(!isVisible, "The highlighter is hidden by default");
+
+ info("Selecting the test node");
+ yield selectNode("span", inspector);
+ let container = yield getContainerForSelector("h1", inspector);
+
+ let onHighlighterReady = toolbox.once("highlighter-ready");
+ EventUtils.synthesizeMouseAtCenter(container.tagLine, {type: "mousemove"},
+ inspector.markup.doc.defaultView);
+ yield onHighlighterReady;
+
+ isVisible = yield testActor.isHighlighting();
+ ok(isVisible, "The highlighter is shown on a markup container hover");
+
+ ok((yield testActor.assertHighlightedNode("h1")),
+ "The highlighter highlights the right node");
+});
diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-02.js b/devtools/client/inspector/test/browser_inspector_highlighter-02.js
new file mode 100644
index 000000000..37eb9389e
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-02.js
@@ -0,0 +1,39 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set 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";
+
+// Test that the highlighter is correctly displayed over a variety of elements
+
+const TEST_URI = URL_ROOT + "doc_inspector_highlighter.html";
+
+add_task(function* () {
+ let {inspector, testActor} = yield openInspectorForURL(TEST_URI);
+
+ info("Selecting the simple, non-transformed DIV");
+ yield selectAndHighlightNode("#simple-div", inspector);
+
+ let isVisible = yield testActor.isHighlighting();
+ ok(isVisible, "The highlighter is shown");
+ ok((yield testActor.assertHighlightedNode("#simple-div")),
+ "The highlighter's outline corresponds to the simple div");
+ yield testActor.isNodeCorrectlyHighlighted("#simple-div", is, "non-zoomed");
+
+ info("Selecting the rotated DIV");
+ yield selectAndHighlightNode("#rotated-div", inspector);
+
+ isVisible = yield testActor.isHighlighting();
+ ok(isVisible, "The highlighter is shown");
+ yield testActor.isNodeCorrectlyHighlighted("#rotated-div", is, "rotated");
+
+ info("Selecting the zero width height DIV");
+ yield selectAndHighlightNode("#widthHeightZero-div", inspector);
+
+ isVisible = yield testActor.isHighlighting();
+ ok(isVisible, "The highlighter is shown");
+ yield testActor.isNodeCorrectlyHighlighted("#widthHeightZero-div", is,
+ "zero width height");
+});
diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-03.js b/devtools/client/inspector/test/browser_inspector_highlighter-03.js
new file mode 100644
index 000000000..344b5c6c8
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-03.js
@@ -0,0 +1,70 @@
+/* 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 iframes are correctly highlighted.
+
+const IFRAME_SRC = "<style>" +
+ "body {" +
+ "margin:0;" +
+ "height:100%;" +
+ "background-color:red" +
+ "}" +
+ "</style><body>hello from iframe</body>";
+
+const DOCUMENT_SRC = "<style>" +
+ "iframe {" +
+ "height:200px;" +
+ "border: 11px solid black;" +
+ "padding: 13px;" +
+ "}" +
+ "body,iframe {" +
+ "margin:0" +
+ "}" +
+ "</style>" +
+ "<body>" +
+ "<iframe src='data:text/html;charset=utf-8," + IFRAME_SRC + "'></iframe>" +
+ "</body>";
+
+const TEST_URI = "data:text/html;charset=utf-8," + DOCUMENT_SRC;
+
+add_task(function* () {
+ let { inspector, toolbox, testActor } = yield openInspectorForURL(TEST_URI);
+
+ info("Waiting for box mode to show.");
+ let body = yield getNodeFront("body", inspector);
+ yield inspector.highlighter.showBoxModel(body);
+
+ info("Waiting for element picker to become active.");
+ yield startPicker(toolbox);
+
+ info("Moving mouse over iframe padding.");
+ yield moveMouseOver("iframe", 1, 1);
+
+ info("Performing checks");
+ yield testActor.isNodeCorrectlyHighlighted("iframe", is);
+
+ info("Scrolling the document");
+ yield testActor.setProperty("iframe", "style", "margin-bottom: 2000px");
+ yield testActor.eval("window.scrollBy(0, 40);");
+
+ // target the body within the iframe
+ let iframeBodySelector = ["iframe", "body"];
+
+ info("Moving mouse over iframe body");
+ yield moveMouseOver("iframe", 40, 40);
+
+ ok((yield testActor.assertHighlightedNode(iframeBodySelector)),
+ "highlighter shows the right node");
+ yield testActor.isNodeCorrectlyHighlighted(iframeBodySelector, is);
+
+ info("Waiting for the element picker to deactivate.");
+ yield inspector.toolbox.highlighterUtils.stopPicker();
+
+ function moveMouseOver(selector, x, y) {
+ info("Waiting for element " + selector + " to be highlighted");
+ testActor.synthesizeMouse({selector, x, y, options: {type: "mousemove"}});
+ return inspector.toolbox.once("picker-node-hovered");
+ }
+});
diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-04.js b/devtools/client/inspector/test/browser_inspector_highlighter-04.js
new file mode 100644
index 000000000..d87f20e94
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-04.js
@@ -0,0 +1,43 @@
+/* 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";
+
+// Check that various highlighter elements exist.
+
+const TEST_URL = "data:text/html;charset=utf-8,<div>test</div>";
+
+// IDs of all highlighter elements that we expect to find in the canvasFrame.
+const ELEMENTS = ["box-model-root",
+ "box-model-elements",
+ "box-model-margin",
+ "box-model-border",
+ "box-model-padding",
+ "box-model-content",
+ "box-model-guide-top",
+ "box-model-guide-right",
+ "box-model-guide-bottom",
+ "box-model-guide-left",
+ "box-model-infobar-container",
+ "box-model-infobar-tagname",
+ "box-model-infobar-id",
+ "box-model-infobar-classes",
+ "box-model-infobar-pseudo-classes",
+ "box-model-infobar-dimensions"];
+
+add_task(function* () {
+ let {inspector, testActor} = yield openInspectorForURL(TEST_URL);
+
+ info("Show the box-model highlighter");
+ let divFront = yield getNodeFront("div", inspector);
+ yield inspector.highlighter.showBoxModel(divFront);
+
+ for (let id of ELEMENTS) {
+ let foundId = yield testActor.getHighlighterNodeAttribute(id, "id");
+ is(foundId, id, "Element " + id + " found");
+ }
+
+ info("Hide the box-model highlighter");
+ yield inspector.highlighter.hideBoxModel();
+});
diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-by-type.js b/devtools/client/inspector/test/browser_inspector_highlighter-by-type.js
new file mode 100644
index 000000000..485d9db0e
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-by-type.js
@@ -0,0 +1,66 @@
+/* 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";
+
+// Check that custom highlighters can be retrieved by type and that they expose
+// the expected API.
+
+const TEST_URL = "data:text/html;charset=utf-8,custom highlighters";
+
+add_task(function* () {
+ let {inspector} = yield openInspectorForURL(TEST_URL);
+
+ yield onlyOneInstanceOfMainHighlighter(inspector);
+ yield manyInstancesOfCustomHighlighters(inspector);
+ yield showHideMethodsAreAvailable(inspector);
+ yield unknownHighlighterTypeShouldntBeAccepted(inspector);
+});
+
+function* onlyOneInstanceOfMainHighlighter({inspector}) {
+ info("Check that the inspector always sends back the same main highlighter");
+
+ let h1 = yield inspector.getHighlighter(false);
+ let h2 = yield inspector.getHighlighter(false);
+ is(h1, h2, "The same highlighter front was returned");
+
+ is(h1.typeName, "highlighter", "The right front type was returned");
+}
+
+function* manyInstancesOfCustomHighlighters({inspector}) {
+ let h1 = yield inspector.getHighlighterByType("BoxModelHighlighter");
+ let h2 = yield inspector.getHighlighterByType("BoxModelHighlighter");
+ ok(h1 !== h2, "getHighlighterByType returns new instances every time (1)");
+
+ let h3 = yield inspector.getHighlighterByType("CssTransformHighlighter");
+ let h4 = yield inspector.getHighlighterByType("CssTransformHighlighter");
+ ok(h3 !== h4, "getHighlighterByType returns new instances every time (2)");
+ ok(h3 !== h1 && h3 !== h2,
+ "getHighlighterByType returns new instances every time (3)");
+ ok(h4 !== h1 && h4 !== h2,
+ "getHighlighterByType returns new instances every time (4)");
+
+ yield h1.finalize();
+ yield h2.finalize();
+ yield h3.finalize();
+ yield h4.finalize();
+}
+
+function* showHideMethodsAreAvailable({inspector}) {
+ let h1 = yield inspector.getHighlighterByType("BoxModelHighlighter");
+ let h2 = yield inspector.getHighlighterByType("CssTransformHighlighter");
+
+ ok("show" in h1, "Show method is present on the front API");
+ ok("show" in h2, "Show method is present on the front API");
+ ok("hide" in h1, "Hide method is present on the front API");
+ ok("hide" in h2, "Hide method is present on the front API");
+
+ yield h1.finalize();
+ yield h2.finalize();
+}
+
+function* unknownHighlighterTypeShouldntBeAccepted({inspector}) {
+ let h = yield inspector.getHighlighterByType("whatever");
+ ok(!h, "No highlighter was returned for the invalid type");
+}
diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-cancel.js b/devtools/client/inspector/test/browser_inspector_highlighter-cancel.js
new file mode 100644
index 000000000..f1022bb50
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-cancel.js
@@ -0,0 +1,52 @@
+/* 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";
+
+// Test that canceling the element picker zooms back on the focused element. Bug 1224304.
+
+const TEST_URL = URL_ROOT + "doc_inspector_long-divs.html";
+
+add_task(function* () {
+ let {inspector, toolbox, testActor} = yield openInspectorForURL(TEST_URL);
+
+ yield selectAndHighlightNode("#focus-here", inspector);
+ ok((yield testActor.assertHighlightedNode("#focus-here")),
+ "The highlighter focuses on div#focus-here");
+ ok(isSelectedMarkupNodeInView(),
+ "The currently selected node is on the screen.");
+
+ // Start the picker but skip focusing manually focusing on the target, let the element
+ // picker do the focusing.
+ yield startPicker(toolbox, true);
+ yield moveMouseOver("#zoom-here");
+ ok(!isSelectedMarkupNodeInView(),
+ "The currently selected node is off the screen.");
+
+ yield cancelPickerByShortcut();
+ ok(isSelectedMarkupNodeInView(),
+ "The currently selected node is focused back on the screen.");
+
+ function cancelPickerByShortcut() {
+ info("Key pressed. Waiting for picker to be canceled.");
+ testActor.synthesizeKey({key: "VK_ESCAPE", options: {}});
+ return inspector.toolbox.once("picker-canceled");
+ }
+
+ function moveMouseOver(selector) {
+ info(`Waiting for element ${selector} to be hovered in the markup view`);
+ testActor.synthesizeMouse({
+ options: {type: "mousemove"},
+ center: true,
+ selector: selector
+ });
+ return inspector.markup.once("showcontainerhovered");
+ }
+
+ function isSelectedMarkupNodeInView() {
+ const selectedNodeContainer = inspector.markup._selectedContainer.elt;
+ const bounds = selectedNodeContainer.getBoundingClientRect();
+ return bounds.top > 0 && bounds.bottom > 0;
+ }
+});
diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-comments.js b/devtools/client/inspector/test/browser_inspector_highlighter-comments.js
new file mode 100644
index 000000000..104395227
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-comments.js
@@ -0,0 +1,105 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set 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";
+
+//
+// Whitelisting this test.
+// As part of bug 1077403, the leaking uncaught rejection should be fixed.
+//
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed("false");
+
+// Test that hovering over the markup-view's containers doesn't always show the
+// highlighter, depending on the type of node hovered over.
+
+const TEST_PAGE = URL_ROOT +
+ "doc_inspector_highlighter-comments.html";
+
+add_task(function* () {
+ let {inspector, testActor} = yield openInspectorForURL(TEST_PAGE);
+ let markupView = inspector.markup;
+ yield selectNode("p", inspector);
+
+ info("Hovering over #id1 and waiting for highlighter to appear.");
+ yield hoverElement("#id1");
+ yield assertHighlighterShownOn("#id1");
+
+ info("Hovering over comment node and ensuring highlighter doesn't appear.");
+ yield hoverComment();
+ yield assertHighlighterHidden();
+
+ info("Hovering over #id1 again and waiting for highlighter to appear.");
+ yield hoverElement("#id1");
+ yield assertHighlighterShownOn("#id1");
+
+ info("Hovering over #id2 and waiting for highlighter to appear.");
+ yield hoverElement("#id2");
+ yield assertHighlighterShownOn("#id2");
+
+ info("Hovering over <script> and ensuring highlighter doesn't appear.");
+ yield hoverElement("script");
+ yield assertHighlighterHidden();
+
+ info("Hovering over #id3 and waiting for highlighter to appear.");
+ yield hoverElement("#id3");
+ yield assertHighlighterShownOn("#id3");
+
+ info("Hovering over hidden #id4 and ensuring highlighter doesn't appear.");
+ yield hoverElement("#id4");
+ yield assertHighlighterHidden();
+
+ info("Hovering over a text node and waiting for highlighter to appear.");
+ yield hoverTextNode("Visible text node");
+ yield assertHighlighterShownOnTextNode("body", 14);
+
+ function hoverContainer(container) {
+ let promise = inspector.toolbox.once("node-highlight");
+
+ EventUtils.synthesizeMouse(container.tagLine, 2, 2, {type: "mousemove"},
+ markupView.doc.defaultView);
+
+ return promise;
+ }
+
+ function* hoverElement(selector) {
+ info(`Hovering node ${selector} in the markup view`);
+ let container = yield getContainerForSelector(selector, inspector);
+ return hoverContainer(container);
+ }
+
+ function hoverComment() {
+ info("Hovering the comment node in the markup view");
+ for (let [node, container] of markupView._containers) {
+ if (node.nodeType === Ci.nsIDOMNode.COMMENT_NODE) {
+ return hoverContainer(container);
+ }
+ }
+ return null;
+ }
+
+ function hoverTextNode(text) {
+ info(`Hovering the text node "${text}" in the markup view`);
+ let container = [...markupView._containers].filter(([nodeFront]) => {
+ return nodeFront.nodeType === Ci.nsIDOMNode.TEXT_NODE &&
+ nodeFront._form.nodeValue.trim() === text.trim();
+ })[0][1];
+ return hoverContainer(container);
+ }
+
+ function* assertHighlighterShownOn(selector) {
+ ok((yield testActor.assertHighlightedNode(selector)),
+ "Highlighter is shown on the right node: " + selector);
+ }
+
+ function* assertHighlighterShownOnTextNode(parentSelector, childNodeIndex) {
+ ok((yield testActor.assertHighlightedTextNode(parentSelector, childNodeIndex)),
+ "Highlighter is shown on the right text node");
+ }
+
+ function* assertHighlighterHidden() {
+ let isVisible = yield testActor.isHighlighting();
+ ok(!isVisible, "Highlighter is hidden");
+ }
+});
diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-cssgrid_01.js b/devtools/client/inspector/test/browser_inspector_highlighter-cssgrid_01.js
new file mode 100644
index 000000000..ef21b88c9
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-cssgrid_01.js
@@ -0,0 +1,77 @@
+/* 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";
+
+// Test the creation of the canvas highlighter element of the css grid highlighter.
+
+const TEST_URL = `
+ <style type='text/css'>
+ #grid {
+ display: grid;
+ }
+ #cell1 {
+ grid-column: 1;
+ grid-row: 1;
+ }
+ #cell2 {
+ grid-column: 2;
+ grid-row: 1;
+ }
+ #cell3 {
+ grid-column: 1;
+ grid-row: 2;
+ }
+ #cell4 {
+ grid-column: 2;
+ grid-row: 2;
+ }
+ </style>
+ <div id="grid">
+ <div id="cell1">cell1</div>
+ <div id="cell2">cell2</div>
+ <div id="cell3">cell3</div>
+ <div id="cell4">cell4</div>
+ </div>
+`;
+
+const HIGHLIGHTER_TYPE = "CssGridHighlighter";
+
+add_task(function* () {
+ let {inspector, testActor} = yield openInspectorForURL(
+ "data:text/html;charset=utf-8," + encodeURIComponent(TEST_URL));
+ let front = inspector.inspector;
+ let highlighter = yield front.getHighlighterByType(HIGHLIGHTER_TYPE);
+
+ yield isHiddenByDefault(testActor, highlighter);
+ yield isVisibleWhenShown(testActor, inspector, highlighter);
+
+ yield highlighter.finalize();
+});
+
+function* isHiddenByDefault(testActor, highlighterFront) {
+ info("Checking that the highlighter is hidden by default");
+
+ let hidden = yield testActor.getHighlighterNodeAttribute(
+ "css-grid-canvas", "hidden", highlighterFront);
+ ok(hidden, "The highlighter is hidden by default");
+}
+
+function* isVisibleWhenShown(testActor, inspector, highlighterFront) {
+ info("Asking to show the highlighter on the test node");
+
+ let node = yield getNodeFront("#grid", inspector);
+ yield highlighterFront.show(node);
+
+ let hidden = yield testActor.getHighlighterNodeAttribute(
+ "css-grid-canvas", "hidden", highlighterFront);
+ ok(!hidden, "The highlighter is visible");
+
+ info("Hiding the highlighter");
+ yield highlighterFront.hide();
+
+ hidden = yield testActor.getHighlighterNodeAttribute(
+ "css-grid-canvas", "hidden", highlighterFront);
+ ok(hidden, "The highlighter is hidden");
+}
diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-csstransform_01.js b/devtools/client/inspector/test/browser_inspector_highlighter-csstransform_01.js
new file mode 100644
index 000000000..f30d1b590
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-csstransform_01.js
@@ -0,0 +1,152 @@
+/* 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";
+
+// Test the creation of the SVG highlighter elements of the css transform
+// highlighter.
+
+const TEST_URL = `
+ <div id="transformed"
+ style="border:1px solid red;width:100px;height:100px;transform:skew(13deg);">
+ </div>
+ <div id="untransformed"
+ style="border:1px solid blue;width:100px;height:100px;">
+ </div>
+ <span id="inline"
+ style="transform:rotate(90deg);">this is an inline transformed element
+ </span>
+`;
+
+add_task(function* () {
+ let {inspector, testActor} = yield openInspectorForURL(
+ "data:text/html;charset=utf-8," + encodeURI(TEST_URL));
+ let front = inspector.inspector;
+
+ let highlighter = yield front.getHighlighterByType("CssTransformHighlighter");
+
+ yield isHiddenByDefault(testActor, highlighter);
+ yield has2PolygonsAnd4Lines(testActor, highlighter);
+ yield isNotShownForUntransformed(testActor, inspector, highlighter);
+ yield isNotShownForInline(testActor, inspector, highlighter);
+ yield isVisibleWhenShown(testActor, inspector, highlighter);
+ yield linesLinkThePolygons(testActor, inspector, highlighter);
+
+ yield highlighter.finalize();
+});
+
+function* isHiddenByDefault(testActor, highlighterFront) {
+ info("Checking that the highlighter is hidden by default");
+
+ let hidden = yield testActor.getHighlighterNodeAttribute(
+ "css-transform-elements", "hidden", highlighterFront);
+ ok(hidden, "The highlighter is hidden by default");
+}
+
+function* has2PolygonsAnd4Lines(testActor, highlighterFront) {
+ info("Checking that the highlighter is made up of 4 lines and 2 polygons");
+
+ let value = yield testActor.getHighlighterNodeAttribute(
+ "css-transform-untransformed", "class", highlighterFront);
+ is(value, "css-transform-untransformed", "The untransformed polygon exists");
+
+ value = yield testActor.getHighlighterNodeAttribute(
+ "css-transform-transformed", "class", highlighterFront);
+ is(value, "css-transform-transformed", "The transformed polygon exists");
+
+ for (let nb of ["1", "2", "3", "4"]) {
+ value = yield testActor.getHighlighterNodeAttribute(
+ "css-transform-line" + nb, "class", highlighterFront);
+ is(value, "css-transform-line", "The line " + nb + " exists");
+ }
+}
+
+function* isNotShownForUntransformed(testActor, inspector, highlighterFront) {
+ info("Asking to show the highlighter on the untransformed test node");
+
+ let node = yield getNodeFront("#untransformed", inspector);
+ yield highlighterFront.show(node);
+
+ let hidden = yield testActor.getHighlighterNodeAttribute(
+ "css-transform-elements", "hidden", highlighterFront);
+ ok(hidden, "The highlighter is still hidden");
+}
+
+function* isNotShownForInline(testActor, inspector, highlighterFront) {
+ info("Asking to show the highlighter on the inline test node");
+
+ let node = yield getNodeFront("#inline", inspector);
+ yield highlighterFront.show(node);
+
+ let hidden = yield testActor.getHighlighterNodeAttribute(
+ "css-transform-elements", "hidden", highlighterFront);
+ ok(hidden, "The highlighter is still hidden");
+}
+
+function* isVisibleWhenShown(testActor, inspector, highlighterFront) {
+ info("Asking to show the highlighter on the test node");
+
+ let node = yield getNodeFront("#transformed", inspector);
+ yield highlighterFront.show(node);
+
+ let hidden = yield testActor.getHighlighterNodeAttribute(
+ "css-transform-elements", "hidden", highlighterFront);
+ ok(!hidden, "The highlighter is visible");
+
+ info("Hiding the highlighter");
+ yield highlighterFront.hide();
+
+ hidden = yield testActor.getHighlighterNodeAttribute(
+ "css-transform-elements", "hidden", highlighterFront);
+ ok(hidden, "The highlighter is hidden");
+}
+
+function* linesLinkThePolygons(testActor, inspector, highlighterFront) {
+ info("Showing the highlighter on the transformed node");
+
+ let node = yield getNodeFront("#transformed", inspector);
+ yield highlighterFront.show(node);
+
+ info("Checking that the 4 lines do link the 2 shape's corners");
+
+ let lines = [];
+ for (let nb of ["1", "2", "3", "4"]) {
+ let x1 = yield testActor.getHighlighterNodeAttribute(
+ "css-transform-line" + nb, "x1", highlighterFront);
+ let y1 = yield testActor.getHighlighterNodeAttribute(
+ "css-transform-line" + nb, "y1", highlighterFront);
+ let x2 = yield testActor.getHighlighterNodeAttribute(
+ "css-transform-line" + nb, "x2", highlighterFront);
+ let y2 = yield testActor.getHighlighterNodeAttribute(
+ "css-transform-line" + nb, "y2", highlighterFront);
+ lines.push({x1, y1, x2, y2});
+ }
+
+ let points1 = yield testActor.getHighlighterNodeAttribute(
+ "css-transform-untransformed", "points", highlighterFront);
+ points1 = points1.split(" ");
+
+ let points2 = yield testActor.getHighlighterNodeAttribute(
+ "css-transform-transformed", "points", highlighterFront);
+ points2 = points2.split(" ");
+
+ for (let i = 0; i < lines.length; i++) {
+ info("Checking line nb " + i);
+ let line = lines[i];
+
+ let p1 = points1[i].split(",");
+ is(p1[0], line.x1,
+ "line " + i + "'s first point matches the untransformed x coordinate");
+ is(p1[1], line.y1,
+ "line " + i + "'s first point matches the untransformed y coordinate");
+
+ let p2 = points2[i].split(",");
+ is(p2[0], line.x2,
+ "line " + i + "'s first point matches the transformed x coordinate");
+ is(p2[1], line.y2,
+ "line " + i + "'s first point matches the transformed y coordinate");
+ }
+
+ yield highlighterFront.hide();
+}
diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-csstransform_02.js b/devtools/client/inspector/test/browser_inspector_highlighter-csstransform_02.js
new file mode 100644
index 000000000..52e3b0146
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-csstransform_02.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/. */
+
+"use strict";
+
+/*
+Bug 1014547 - CSS transforms highlighter
+Test that the highlighter elements created have the right size and coordinates.
+
+Note that instead of hard-coding values here, the assertions are made by
+comparing with the result of getAdjustedQuads.
+
+There's a separate test for checking that getAdjustedQuads actually returns
+sensible values
+(devtools/client/shared/test/browser_layoutHelpers-getBoxQuads.js),
+so the present test doesn't care about that, it just verifies that the css
+transform highlighter applies those values correctly to the SVG elements
+*/
+
+const TEST_URL = URL_ROOT + "doc_inspector_highlighter_csstransform.html";
+
+add_task(function* () {
+ let {inspector, testActor} = yield openInspectorForURL(TEST_URL);
+ let front = inspector.inspector;
+
+ let highlighter = yield front.getHighlighterByType("CssTransformHighlighter");
+
+ let nodeFront = yield getNodeFront("#test-node", inspector);
+
+ info("Displaying the transform highlighter on test node");
+ yield highlighter.show(nodeFront);
+
+ let data = yield testActor.getAllAdjustedQuads("#test-node");
+ let [expected] = data.border;
+
+ let points = yield testActor.getHighlighterNodeAttribute(
+ "css-transform-transformed", "points", highlighter);
+ let polygonPoints = points.split(" ").map(p => {
+ return {
+ x: +p.substring(0, p.indexOf(",")),
+ y: +p.substring(p.indexOf(",") + 1)
+ };
+ });
+
+ for (let i = 1; i < 5; i++) {
+ is(polygonPoints[i - 1].x, expected["p" + i].x,
+ "p" + i + " x coordinate is correct");
+ is(polygonPoints[i - 1].y, expected["p" + i].y,
+ "p" + i + " y coordinate is correct");
+ }
+
+ info("Hiding the transform highlighter");
+ yield highlighter.hide();
+ yield highlighter.finalize();
+});
diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-embed.js b/devtools/client/inspector/test/browser_inspector_highlighter-embed.js
new file mode 100644
index 000000000..23cd4332a
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-embed.js
@@ -0,0 +1,30 @@
+/* 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";
+
+// Test that the highlighter can go inside <embed> elements
+
+const TEST_URL = URL_ROOT + "doc_inspector_embed.html";
+
+add_task(function* () {
+ let {inspector} = yield openInspectorForURL(TEST_URL);
+
+ info("Get a node inside the <embed> element and select/highlight it");
+ let body = yield getEmbeddedBody(inspector);
+ yield selectAndHighlightNode(body, inspector);
+
+ let selectedNode = inspector.selection.nodeFront;
+ is(selectedNode.tagName.toLowerCase(), "body", "The selected node is <body>");
+ ok(selectedNode.baseURI.endsWith("doc_inspector_menu.html"),
+ "The selected node is the <body> node inside the <embed> element");
+});
+
+function* getEmbeddedBody({walker}) {
+ let embed = yield walker.querySelector(walker.rootNode, "embed");
+ let {nodes} = yield walker.children(embed);
+ let contentDoc = nodes[0];
+ let body = yield walker.querySelector(contentDoc, "body");
+ return body;
+}
diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-clipboard.js b/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-clipboard.js
new file mode 100644
index 000000000..2d91f81a7
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-clipboard.js
@@ -0,0 +1,39 @@
+/* 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";
+
+// Test that the eyedropper can copy colors to the clipboard
+
+const HIGHLIGHTER_TYPE = "EyeDropper";
+const ID = "eye-dropper-";
+const TEST_URI = "data:text/html;charset=utf-8,<style>html{background:red}</style>";
+
+add_task(function* () {
+ let helper = yield openInspectorForURL(TEST_URI)
+ .then(getHighlighterHelperFor(HIGHLIGHTER_TYPE));
+ helper.prefix = ID;
+
+ let {show, finalize,
+ waitForElementAttributeSet, waitForElementAttributeRemoved} = helper;
+
+ info("Show the eyedropper with the copyOnSelect option");
+ yield show("html", {copyOnSelect: true});
+
+ info("Make sure to wait until the eyedropper is done taking a screenshot of the page");
+ yield waitForElementAttributeSet("root", "drawn", helper);
+
+ yield waitForClipboardPromise(() => {
+ info("Activate the eyedropper so the background color is copied");
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ }, "#FF0000");
+
+ ok(true, "The clipboard contains the right value");
+
+ yield waitForElementAttributeRemoved("root", "drawn", helper);
+ yield waitForElementAttributeSet("root", "hidden", helper);
+ ok(true, "The eyedropper is now hidden");
+
+ finalize();
+});
+
diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-csp.js b/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-csp.js
new file mode 100644
index 000000000..0cd425b56
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-csp.js
@@ -0,0 +1,30 @@
+/* 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";
+
+// Test that the eyedropper opens correctly even when the page defines CSP headers.
+
+const HIGHLIGHTER_TYPE = "EyeDropper";
+const ID = "eye-dropper-";
+const TEST_URI = URL_ROOT + "doc_inspector_csp.html";
+
+add_task(function* () {
+ let helper = yield openInspectorForURL(TEST_URI)
+ .then(getHighlighterHelperFor(HIGHLIGHTER_TYPE));
+ helper.prefix = ID;
+ let {show, hide, finalize, isElementHidden, waitForElementAttributeSet} = helper;
+
+ info("Try to display the eyedropper");
+ yield show("html");
+
+ let hidden = yield isElementHidden("root");
+ ok(!hidden, "The eyedropper is now shown");
+
+ info("Wait until the eyedropper is done taking a screenshot of the page");
+ yield waitForElementAttributeSet("root", "drawn", helper);
+ ok(true, "The image data was retrieved successfully from the window");
+
+ yield hide();
+ finalize();
+});
diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-events.js b/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-events.js
new file mode 100644
index 000000000..49543b5ce
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-events.js
@@ -0,0 +1,141 @@
+/* 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";
+
+// Test the eyedropper mouse and keyboard handling.
+
+const HIGHLIGHTER_TYPE = "EyeDropper";
+const ID = "eye-dropper-";
+const TEST_URI = `
+<style>
+ html{width:100%;height:100%;}
+</style>
+<body>eye-dropper test</body>`;
+
+const MOVE_EVENTS_DATA = [
+ {type: "mouse", x: 200, y: 100, expected: {x: 200, y: 100}},
+ {type: "mouse", x: 100, y: 200, expected: {x: 100, y: 200}},
+ {type: "keyboard", key: "VK_LEFT", expected: {x: 99, y: 200}},
+ {type: "keyboard", key: "VK_LEFT", shift: true, expected: {x: 89, y: 200}},
+ {type: "keyboard", key: "VK_RIGHT", expected: {x: 90, y: 200}},
+ {type: "keyboard", key: "VK_RIGHT", shift: true, expected: {x: 100, y: 200}},
+ {type: "keyboard", key: "VK_DOWN", expected: {x: 100, y: 201}},
+ {type: "keyboard", key: "VK_DOWN", shift: true, expected: {x: 100, y: 211}},
+ {type: "keyboard", key: "VK_UP", expected: {x: 100, y: 210}},
+ {type: "keyboard", key: "VK_UP", shift: true, expected: {x: 100, y: 200}},
+ // Mouse initialization for left and top snapping
+ {type: "mouse", x: 7, y: 7, expected: {x: 7, y: 7}},
+ // Left Snapping
+ {type: "keyboard", key: "VK_LEFT", shift: true, expected: {x: 0, y: 7},
+ desc: "Left Snapping to x=0"},
+ // Top Snapping
+ {type: "keyboard", key: "VK_UP", shift: true, expected: {x: 0, y: 0},
+ desc: "Top Snapping to y=0"},
+ // Mouse initialization for right snapping
+ {
+ type: "mouse",
+ x: (width, height) => width - 5,
+ y: 0,
+ expected: {
+ x: (width, height) => width - 5,
+ y: 0
+ }
+ },
+ // Right snapping
+ {
+ type: "keyboard",
+ key: "VK_RIGHT",
+ shift: true,
+ expected: {
+ x: (width, height) => width,
+ y: 0
+ },
+ desc: "Right snapping to x=max window width available"
+ },
+ // Mouse initialization for bottom snapping
+ {
+ type: "mouse",
+ x: 0,
+ y: (width, height) => height - 5,
+ expected: {
+ x: 0,
+ y: (width, height) => height - 5
+ }
+ },
+ // Bottom snapping
+ {
+ type: "keyboard",
+ key: "VK_DOWN",
+ shift: true,
+ expected: {
+ x: 0,
+ y: (width, height) => height
+ },
+ desc: "Bottom snapping to y=max window height available"
+ },
+];
+
+add_task(function* () {
+ let {inspector, testActor} = yield openInspectorForURL(
+ "data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let helper = yield getHighlighterHelperFor(HIGHLIGHTER_TYPE)({inspector, testActor});
+
+ helper.prefix = ID;
+
+ yield helper.show("html");
+ yield respondsToMoveEvents(helper, testActor);
+ yield respondsToReturnAndEscape(helper);
+
+ helper.finalize();
+});
+
+function* respondsToMoveEvents(helper, testActor) {
+ info("Checking that the eyedropper responds to events from the mouse and keyboard");
+ let {mouse} = helper;
+ let {width, height} = yield testActor.getBoundingClientRect("html");
+
+ for (let {type, x, y, key, shift, expected, desc} of MOVE_EVENTS_DATA) {
+ x = typeof x === "function" ? x(width, height) : x;
+ y = typeof y === "function" ? y(width, height) : y;
+ expected.x = typeof expected.x === "function" ?
+ expected.x(width, height) : expected.x;
+ expected.y = typeof expected.y === "function" ?
+ expected.y(width, height) : expected.y;
+
+ if (typeof desc === "undefined") {
+ info(`Simulating a ${type} event to move to ${expected.x} ${expected.y}`);
+ } else {
+ info(`Simulating ${type} event: ${desc}`);
+ }
+
+ if (type === "mouse") {
+ yield mouse.move(x, y);
+ } else if (type === "keyboard") {
+ let options = shift ? {shiftKey: true} : {};
+ yield EventUtils.synthesizeKey(key, options);
+ }
+ yield checkPosition(expected, helper);
+ }
+}
+
+function* checkPosition({x, y}, {getElementAttribute}) {
+ let style = yield getElementAttribute("root", "style");
+ is(style, `top:${y}px;left:${x}px;`,
+ `The eyedropper is at the expected ${x} ${y} position`);
+}
+
+function* respondsToReturnAndEscape({isElementHidden, show}) {
+ info("Simulating return to select the color and hide the eyedropper");
+
+ yield EventUtils.synthesizeKey("VK_RETURN", {});
+ let hidden = yield isElementHidden("root");
+ ok(hidden, "The eyedropper has been hidden");
+
+ info("Showing the eyedropper again and simulating escape to hide it");
+
+ yield show("html");
+ yield EventUtils.synthesizeKey("VK_ESCAPE", {});
+ hidden = yield isElementHidden("root");
+ ok(hidden, "The eyedropper has been hidden again");
+}
diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-label.js b/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-label.js
new file mode 100644
index 000000000..02750761b
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-label.js
@@ -0,0 +1,115 @@
+/* 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";
+
+// Test the position of the eyedropper label.
+// It should move around when the eyedropper is close to the edges of the viewport so as
+// to always stay visible.
+
+const HIGHLIGHTER_TYPE = "EyeDropper";
+const ID = "eye-dropper-";
+
+const HTML = `
+<style>
+html, body {height: 100%; margin: 0;}
+body {background: linear-gradient(red, gold); display: flex; justify-content: center;
+ align-items: center;}
+</style>
+Eyedropper label position test
+`;
+const TEST_PAGE = "data:text/html;charset=utf-8," + encodeURI(HTML);
+
+const TEST_DATA = [{
+ desc: "Move the mouse to the center of the screen",
+ getCoordinates: (width, height) => {
+ return {x: width / 2, y: height / 2};
+ },
+ expectedPositions: {top: false, right: false, left: false}
+}, {
+ desc: "Move the mouse to the center left",
+ getCoordinates: (width, height) => {
+ return {x: 0, y: height / 2};
+ },
+ expectedPositions: {top: false, right: true, left: false}
+}, {
+ desc: "Move the mouse to the center right",
+ getCoordinates: (width, height) => {
+ return {x: width, y: height / 2};
+ },
+ expectedPositions: {top: false, right: false, left: true}
+}, {
+ desc: "Move the mouse to the bottom center",
+ getCoordinates: (width, height) => {
+ return {x: width / 2, y: height};
+ },
+ expectedPositions: {top: true, right: false, left: false}
+}, {
+ desc: "Move the mouse to the bottom left",
+ getCoordinates: (width, height) => {
+ return {x: 0, y: height};
+ },
+ expectedPositions: {top: true, right: true, left: false}
+}, {
+ desc: "Move the mouse to the bottom right",
+ getCoordinates: (width, height) => {
+ return {x: width, y: height};
+ },
+ expectedPositions: {top: true, right: false, left: true}
+}, {
+ desc: "Move the mouse to the top left",
+ getCoordinates: (width, height) => {
+ return {x: 0, y: 0};
+ },
+ expectedPositions: {top: false, right: true, left: false}
+}, {
+ desc: "Move the mouse to the top right",
+ getCoordinates: (width, height) => {
+ return {x: width, y: 0};
+ },
+ expectedPositions: {top: false, right: false, left: true}
+}];
+
+add_task(function* () {
+ let {inspector, testActor} = yield openInspectorForURL(TEST_PAGE);
+ let helper = yield getHighlighterHelperFor(HIGHLIGHTER_TYPE)({inspector, testActor});
+ helper.prefix = ID;
+
+ let {mouse, show, hide, finalize} = helper;
+ let {width, height} = yield testActor.getBoundingClientRect("html");
+
+ // This test fails in non-e10s windows if we use width and height. For some reasons, the
+ // mouse events can't be dispatched/handled properly when we try to move the eyedropper
+ // to the far right and/or bottom of the screen. So just removing 10px from each side
+ // fixes it.
+ width -= 10;
+ height -= 10;
+
+ info("Show the eyedropper on the page");
+ yield show("html");
+
+ info("Move the eyedropper around and check that the label appears at the right place");
+ for (let {desc, getCoordinates, expectedPositions} of TEST_DATA) {
+ info(desc);
+ let {x, y} = getCoordinates(width, height);
+ info(`Moving the mouse to ${x} ${y}`);
+ yield mouse.move(x, y);
+ yield checkLabelPositionAttributes(helper, expectedPositions);
+ }
+
+ info("Hide the eyedropper");
+ yield hide();
+ finalize();
+});
+
+function* checkLabelPositionAttributes(helper, positions) {
+ for (let position in positions) {
+ is((yield hasAttribute(helper, position)), positions[position],
+ `The label was ${positions[position] ? "" : "not "}moved to the ${position}`);
+ }
+}
+
+function* hasAttribute({getElementAttribute}, name) {
+ let value = yield getElementAttribute("root", name);
+ return value !== null;
+}
diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-show-hide.js b/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-show-hide.js
new file mode 100644
index 000000000..86f2ae83d
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-show-hide.js
@@ -0,0 +1,42 @@
+/* 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";
+
+// Test the basic structure of the eye-dropper highlighter.
+
+const HIGHLIGHTER_TYPE = "EyeDropper";
+const ID = "eye-dropper-";
+
+add_task(function* () {
+ let helper = yield openInspectorForURL("data:text/html;charset=utf-8,eye-dropper test")
+ .then(getHighlighterHelperFor(HIGHLIGHTER_TYPE));
+ helper.prefix = ID;
+
+ yield isInitiallyHidden(helper);
+ yield canBeShownAndHidden(helper);
+
+ helper.finalize();
+});
+
+function* isInitiallyHidden({isElementHidden}) {
+ info("Checking that the eyedropper is hidden by default");
+
+ let hidden = yield isElementHidden("root");
+ ok(hidden, "The eyedropper is hidden by default");
+}
+
+function* canBeShownAndHidden({show, hide, isElementHidden, getElementAttribute}) {
+ info("Asking to show and hide the highlighter actually works");
+
+ yield show("html");
+ let hidden = yield isElementHidden("root");
+ ok(!hidden, "The eyedropper is now shown");
+
+ let style = yield getElementAttribute("root", "style");
+ is(style, "top:100px;left:100px;", "The eyedropper is correctly positioned");
+
+ yield hide();
+ hidden = yield isElementHidden("root");
+ ok(hidden, "The eyedropper is now hidden again");
+}
diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-xul.js b/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-xul.js
new file mode 100644
index 000000000..7c44e7275
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-xul.js
@@ -0,0 +1,64 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the eyedropper icons in the toolbar and in the color picker aren't displayed
+// when the page isn't an HTML one.
+
+const TEST_URL = URL_ROOT + "doc_inspector_highlighter_xbl.xul";
+const TEST_URL_2 =
+ "data:text/html;charset=utf-8,<h1 style='color:red'>HTML test page</h1>";
+
+add_task(function* () {
+ let {inspector} = yield openInspectorForURL(TEST_URL);
+
+ info("Check the inspector toolbar");
+ let button = inspector.panelDoc.querySelector("#inspector-eyedropper-toggle");
+ ok(isDisabled(button), "The button is hidden in the toolbar");
+
+ info("Check the color picker");
+ yield selectNode("#scale", inspector);
+
+ // Find the color swatch in the rule-view.
+ let ruleView = inspector.ruleview.view;
+ let ruleViewDocument = ruleView.styleDocument;
+ let swatchEl = ruleViewDocument.querySelector(".ruleview-colorswatch");
+
+ info("Open the color picker");
+ let cPicker = ruleView.tooltips.colorPicker;
+ let onColorPickerReady = cPicker.once("ready");
+ swatchEl.click();
+ yield onColorPickerReady;
+
+ button = cPicker.tooltip.doc.querySelector("#eyedropper-button");
+ ok(isDisabled(button), "The button is disabled in the color picker");
+
+ info("Navigate to a HTML document");
+ yield navigateTo(inspector, TEST_URL_2);
+
+ info("Check the inspector toolbar in HTML document");
+ button = inspector.panelDoc.querySelector("#inspector-eyedropper-toggle");
+ ok(!isDisabled(button), "The button is enabled in the toolbar");
+
+ info("Check the color picker in HTML document");
+ // Find the color swatch in the rule-view.
+ yield selectNode("h1", inspector);
+
+ ruleView = inspector.ruleview.view;
+ ruleViewDocument = ruleView.styleDocument;
+ swatchEl = ruleViewDocument.querySelector(".ruleview-colorswatch");
+
+ info("Open the color picker in HTML document");
+ cPicker = ruleView.tooltips.colorPicker;
+ onColorPickerReady = cPicker.once("ready");
+ swatchEl.click();
+ yield onColorPickerReady;
+
+ button = cPicker.tooltip.doc.querySelector("#eyedropper-button");
+ ok(!isDisabled(button), "The button is enabled in the color picker");
+});
+
+function isDisabled(button) {
+ return button.disabled;
+}
diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-geometry_01.js b/devtools/client/inspector/test/browser_inspector_highlighter-geometry_01.js
new file mode 100644
index 000000000..28a20998c
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-geometry_01.js
@@ -0,0 +1,89 @@
+/* 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";
+
+// Test the creation of the geometry highlighter elements.
+
+const TEST_URL = `data:text/html;charset=utf-8,
+ <span id='inline'></span>
+ <div id='positioned' style='
+ background:yellow;
+ position:absolute;
+ left:5rem;
+ top:30px;
+ right:300px;
+ bottom:10em;'></div>
+ <div id='sized' style='
+ background:red;
+ width:5em;
+ height:50%;'></div>`;
+
+const HIGHLIGHTER_TYPE = "GeometryEditorHighlighter";
+
+const ID = "geometry-editor-";
+const SIDES = ["left", "right", "top", "bottom"];
+
+add_task(function* () {
+ let helper = yield openInspectorForURL(TEST_URL)
+ .then(getHighlighterHelperFor(HIGHLIGHTER_TYPE));
+
+ let { finalize } = helper;
+
+ helper.prefix = ID;
+
+ yield hasArrowsAndLabelsAndHandlers(helper);
+ yield isHiddenForNonPositionedNonSizedElement(helper);
+ yield sideArrowsAreDisplayedForPositionedNode(helper);
+
+ finalize();
+});
+
+function* hasArrowsAndLabelsAndHandlers({getElementAttribute}) {
+ info("Checking that the highlighter has the expected arrows and labels");
+
+ for (let name of [...SIDES]) {
+ let value = yield getElementAttribute("arrow-" + name, "class");
+ is(value, ID + "arrow " + name, "The " + name + " arrow exists");
+
+ value = yield getElementAttribute("label-text-" + name, "class");
+ is(value, ID + "label-text", "The " + name + " label exists");
+
+ value = yield getElementAttribute("handler-" + name, "class");
+ is(value, ID + "handler-" + name, "The " + name + " handler exists");
+ }
+}
+
+function* isHiddenForNonPositionedNonSizedElement(
+ {show, hide, isElementHidden}) {
+ info("Asking to show the highlighter on an inline, non p ositioned element");
+
+ yield show("#inline");
+
+ for (let name of [...SIDES]) {
+ let hidden = yield isElementHidden("arrow-" + name);
+ ok(hidden, "The " + name + " arrow is hidden");
+
+ hidden = yield isElementHidden("handler-" + name);
+ ok(hidden, "The " + name + " handler is hidden");
+ }
+}
+
+function* sideArrowsAreDisplayedForPositionedNode(
+ {show, hide, isElementHidden}) {
+ info("Asking to show the highlighter on the positioned node");
+
+ yield show("#positioned");
+
+ for (let name of SIDES) {
+ let hidden = yield isElementHidden("arrow-" + name);
+ ok(!hidden, "The " + name + " arrow is visible for the positioned node");
+
+ hidden = yield isElementHidden("handler-" + name);
+ ok(!hidden, "The " + name + " handler is visible for the positioned node");
+ }
+
+ info("Hiding the highlighter");
+ yield hide();
+}
diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-geometry_02.js b/devtools/client/inspector/test/browser_inspector_highlighter-geometry_02.js
new file mode 100644
index 000000000..e0681c6f9
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-geometry_02.js
@@ -0,0 +1,116 @@
+/* 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/. */
+
+/* Globals defined in: devtools/client/inspector/test/head.js */
+
+"use strict";
+
+// Test that the geometry highlighter labels are correct.
+
+const TEST_URL = `data:text/html;charset=utf-8,
+ <div id='positioned' style='
+ background:yellow;
+ position:absolute;
+ left:5rem;
+ top:30px;
+ right:300px;
+ bottom:10em;'></div>
+ <div id='positioned2' style='
+ background:blue;
+ position:absolute;
+ right:10%;
+ top:5vmin;'>test element</div>
+ <div id='relative' style='
+ background:green;
+ position:relative;
+ top:10px;
+ left:20px;
+ bottom:30px;
+ right:40px;
+ width:100px;
+ height:100px;'></div>
+ <div id='relative2' style='
+ background:grey;
+ position:relative;
+ top:0;bottom:-50px;
+ height:3em;'>relative</div>`;
+
+const ID = "geometry-editor-";
+const HIGHLIGHTER_TYPE = "GeometryEditorHighlighter";
+
+const POSITIONED_ELEMENT_TESTS = [{
+ selector: "#positioned",
+ expectedLabels: [
+ {side: "left", visible: true, label: "5rem"},
+ {side: "top", visible: true, label: "30px"},
+ {side: "right", visible: true, label: "300px"},
+ {side: "bottom", visible: true, label: "10em"}
+ ]
+}, {
+ selector: "#positioned2",
+ expectedLabels: [
+ {side: "left", visible: false},
+ {side: "top", visible: true, label: "5vmin"},
+ {side: "right", visible: true, label: "10%"},
+ {side: "bottom", visible: false}
+ ]
+}, {
+ selector: "#relative",
+ expectedLabels: [
+ {side: "left", visible: true, label: "20px"},
+ {side: "top", visible: true, label: "10px"},
+ {side: "right", visible: false},
+ {side: "bottom", visible: false}
+ ]
+}, {
+ selector: "#relative2",
+ expectedLabels: [
+ {side: "left", visible: false},
+ {side: "top", visible: true, label: "0px"},
+ {side: "right", visible: false},
+ {side: "bottom", visible: false}
+ ]
+}];
+
+add_task(function* () {
+ let helper = yield openInspectorForURL(TEST_URL)
+ .then(getHighlighterHelperFor(HIGHLIGHTER_TYPE));
+
+ helper.prefix = ID;
+
+ let { finalize } = helper;
+
+ yield positionLabelsAreCorrect(helper);
+
+ yield finalize();
+});
+
+function* positionLabelsAreCorrect(
+ {show, hide, isElementHidden, getElementTextContent}
+) {
+ info("Highlight nodes and check position labels");
+
+ for (let {selector, expectedLabels} of POSITIONED_ELEMENT_TESTS) {
+ info("Testing node " + selector);
+
+ yield show(selector);
+
+ for (let {side, visible, label} of expectedLabels) {
+ let id = "label-" + side;
+
+ let hidden = yield isElementHidden(id);
+ if (visible) {
+ ok(!hidden, "The " + side + " label is visible");
+
+ let value = yield getElementTextContent(id);
+ is(value, label, "The " + side + " label textcontent is correct");
+ } else {
+ ok(hidden, "The " + side + " label is hidden");
+ }
+ }
+
+ info("Hiding the highlighter");
+ yield hide();
+ }
+}
diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-geometry_03.js b/devtools/client/inspector/test/browser_inspector_highlighter-geometry_03.js
new file mode 100644
index 000000000..0fa7bb96b
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-geometry_03.js
@@ -0,0 +1,61 @@
+/* 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/. */
+
+/* Globals defined in: devtools/client/inspector/test/head.js */
+
+"use strict";
+
+// Test that the right arrows/labels are shown even when the css properties are
+// in several different css rules.
+
+const TEST_URL = URL_ROOT + "doc_inspector_highlighter-geometry_01.html";
+const ID = "geometry-editor-";
+const HIGHLIGHTER_TYPE = "GeometryEditorHighlighter";
+const PROPS = ["left", "right", "top", "bottom"];
+
+add_task(function* () {
+ let helper = yield openInspectorForURL(TEST_URL)
+ .then(getHighlighterHelperFor(HIGHLIGHTER_TYPE));
+
+ helper.prefix = ID;
+
+ let { finalize } = helper;
+
+ yield checkArrowsLabelsAndHandlers(
+ "#node2", ["top", "left", "bottom", "right"],
+ helper);
+
+ yield checkArrowsLabelsAndHandlers("#node3", ["top", "left"], helper);
+
+ yield finalize();
+});
+
+function* checkArrowsLabelsAndHandlers(selector, expectedProperties,
+ {show, hide, isElementHidden}
+) {
+ info("Getting node " + selector + " from the page");
+
+ yield show(selector);
+
+ for (let name of expectedProperties) {
+ let hidden = (yield isElementHidden("arrow-" + name)) &&
+ (yield isElementHidden("handler-" + name));
+ ok(!hidden,
+ "The " + name + " label/arrow & handler is visible for node " + selector);
+ }
+
+ // Testing that the other arrows are hidden
+ for (let name of PROPS) {
+ if (expectedProperties.indexOf(name) !== -1) {
+ continue;
+ }
+ let hidden = (yield isElementHidden("arrow-" + name)) &&
+ (yield isElementHidden("handler-" + name));
+ ok(hidden,
+ "The " + name + " arrow & handler is hidden for node " + selector);
+ }
+
+ info("Hiding the highlighter");
+ yield hide();
+}
diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-geometry_04.js b/devtools/client/inspector/test/browser_inspector_highlighter-geometry_04.js
new file mode 100644
index 000000000..7f198f6e3
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-geometry_04.js
@@ -0,0 +1,85 @@
+/* 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/. */
+
+ /* Globals defined in: devtools/client/inspector/test/head.js */
+
+"use strict";
+
+// Test that the arrows and handlers are positioned correctly and have the right
+// size.
+
+const TEST_URL = URL_ROOT + "doc_inspector_highlighter-geometry_01.html";
+const ID = "geometry-editor-";
+const HIGHLIGHTER_TYPE = "GeometryEditorHighlighter";
+
+const handlerMap = {
+ "top": {"cx": "x2", "cy": "y2"},
+ "bottom": {"cx": "x2", "cy": "y2"},
+ "left": {"cx": "x2", "cy": "y2"},
+ "right": {"cx": "x2", "cy": "y2"}
+};
+
+add_task(function* () {
+ let helper = yield openInspectorForURL(TEST_URL)
+ .then(getHighlighterHelperFor(HIGHLIGHTER_TYPE));
+
+ helper.prefix = ID;
+
+ let { hide, finalize } = helper;
+
+ yield checkArrowsAndHandlers(helper, ".absolute-all-4", {
+ "top": {x1: 506, y1: 51, x2: 506, y2: 61},
+ "bottom": {x1: 506, y1: 451, x2: 506, y2: 251},
+ "left": {x1: 401, y1: 156, x2: 411, y2: 156},
+ "right": {x1: 901, y1: 156, x2: 601, y2: 156}
+ });
+
+ yield checkArrowsAndHandlers(helper, ".relative", {
+ "top": {x1: 901, y1: 51, x2: 901, y2: 91},
+ "left": {x1: 401, y1: 97, x2: 651, y2: 97}
+ });
+
+ yield checkArrowsAndHandlers(helper, ".fixed", {
+ "top": {x1: 25, y1: 0, x2: 25, y2: 400},
+ "left": {x1: 0, y1: 425, x2: 0, y2: 425}
+ });
+
+ info("Hiding the highlighter");
+ yield hide();
+ yield finalize();
+});
+
+function* checkArrowsAndHandlers(helper, selector, arrows) {
+ info("Highlighting the test node " + selector);
+
+ yield helper.show(selector);
+
+ for (let side in arrows) {
+ yield checkArrowAndHandler(helper, side, arrows[side]);
+ }
+}
+
+function* checkArrowAndHandler({getElementAttribute}, name, expectedCoords) {
+ info("Checking " + name + "arrow and handler coordinates are correct");
+
+ let handlerX = yield getElementAttribute("handler-" + name, "cx");
+ let handlerY = yield getElementAttribute("handler-" + name, "cy");
+
+ let expectedHandlerX = yield getElementAttribute("arrow-" + name,
+ handlerMap[name].cx);
+ let expectedHandlerY = yield getElementAttribute("arrow-" + name,
+ handlerMap[name].cy);
+
+ is(handlerX, expectedHandlerX,
+ "coordinate X for handler " + name + " is correct.");
+ is(handlerY, expectedHandlerY,
+ "coordinate Y for handler " + name + " is correct.");
+
+ for (let coordinate in expectedCoords) {
+ let value = yield getElementAttribute("arrow-" + name, coordinate);
+
+ is(Math.floor(value), expectedCoords[coordinate],
+ coordinate + " coordinate for arrow " + name + " is correct");
+ }
+}
diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-geometry_05.js b/devtools/client/inspector/test/browser_inspector_highlighter-geometry_05.js
new file mode 100644
index 000000000..649a4be3b
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-geometry_05.js
@@ -0,0 +1,119 @@
+/* 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/. */
+
+ /* Globals defined in: devtools/client/inspector/test/head.js */
+
+"use strict";
+
+// Test that the arrows/handlers and offsetparent and currentnode elements of
+// the geometry highlighter only appear when needed.
+
+const TEST_URL = URL_ROOT + "doc_inspector_highlighter-geometry_02.html";
+const ID = "geometry-editor-";
+const HIGHLIGHTER_TYPE = "GeometryEditorHighlighter";
+
+const TEST_DATA = [{
+ selector: "body",
+ isOffsetParentVisible: false,
+ isCurrentNodeVisible: false,
+ hasVisibleArrowsAndHandlers: false
+}, {
+ selector: "h1",
+ isOffsetParentVisible: false,
+ isCurrentNodeVisible: false,
+ hasVisibleArrowsAndHandlers: false
+}, {
+ selector: ".absolute",
+ isOffsetParentVisible: false,
+ isCurrentNodeVisible: true,
+ hasVisibleArrowsAndHandlers: true
+}, {
+ selector: "#absolute-container",
+ isOffsetParentVisible: false,
+ isCurrentNodeVisible: true,
+ hasVisibleArrowsAndHandlers: false
+}, {
+ selector: ".absolute-bottom-right",
+ isOffsetParentVisible: true,
+ isCurrentNodeVisible: true,
+ hasVisibleArrowsAndHandlers: true
+}, {
+ selector: ".absolute-width-margin",
+ isOffsetParentVisible: true,
+ isCurrentNodeVisible: true,
+ hasVisibleArrowsAndHandlers: true
+}, {
+ selector: ".absolute-all-4",
+ isOffsetParentVisible: true,
+ isCurrentNodeVisible: true,
+ hasVisibleArrowsAndHandlers: true
+}, {
+ selector: ".relative",
+ isOffsetParentVisible: true,
+ isCurrentNodeVisible: true,
+ hasVisibleArrowsAndHandlers: true
+}, {
+ selector: ".static",
+ isOffsetParentVisible: false,
+ isCurrentNodeVisible: false,
+ hasVisibleArrowsAndHandlers: false
+}, {
+ selector: ".static-size",
+ isOffsetParentVisible: false,
+ isCurrentNodeVisible: true,
+ hasVisibleArrowsAndHandlers: false
+}, {
+ selector: ".fixed",
+ isOffsetParentVisible: false,
+ isCurrentNodeVisible: true,
+ hasVisibleArrowsAndHandlers: true
+}];
+
+add_task(function* () {
+ let helper = yield openInspectorForURL(TEST_URL)
+ .then(getHighlighterHelperFor(HIGHLIGHTER_TYPE));
+
+ helper.prefix = ID;
+
+ let { hide, finalize } = helper;
+
+ for (let data of TEST_DATA) {
+ yield testNode(helper, data);
+ }
+
+ info("Hiding the highlighter");
+ yield hide();
+ yield finalize();
+});
+
+function* testNode(helper, data) {
+ let { selector } = data;
+ yield helper.show(data.selector);
+
+ is((yield isOffsetParentVisible(helper)), data.isOffsetParentVisible,
+ "The offset-parent highlighter visibility is correct for node " + selector);
+ is((yield isCurrentNodeVisible(helper)), data.isCurrentNodeVisible,
+ "The current-node highlighter visibility is correct for node " + selector);
+ is((yield hasVisibleArrowsAndHandlers(helper)),
+ data.hasVisibleArrowsAndHandlers,
+ "The arrows visibility is correct for node " + selector);
+}
+
+function* isOffsetParentVisible({isElementHidden}) {
+ return !(yield isElementHidden("offset-parent"));
+}
+
+function* isCurrentNodeVisible({isElementHidden}) {
+ return !(yield isElementHidden("current-node"));
+}
+
+function* hasVisibleArrowsAndHandlers({isElementHidden}) {
+ for (let side of ["top", "left", "bottom", "right"]) {
+ let hidden = yield isElementHidden("arrow-" + side);
+ if (!hidden) {
+ return !(yield isElementHidden("handler-" + side));
+ }
+ }
+ return false;
+}
diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-geometry_06.js b/devtools/client/inspector/test/browser_inspector_highlighter-geometry_06.js
new file mode 100644
index 000000000..cc22473b7
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-geometry_06.js
@@ -0,0 +1,166 @@
+/* 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";
+
+// Test that the geometry editor resizes properly an element on all sides,
+// with different unit measures, and that arrow/handlers are updated correctly.
+
+const TEST_URL = URL_ROOT + "doc_inspector_highlighter-geometry_01.html";
+const ID = "geometry-editor-";
+const HIGHLIGHTER_TYPE = "GeometryEditorHighlighter";
+
+const SIDES = ["top", "right", "bottom", "left"];
+
+// The object below contains all the tests for this unit test.
+// The property's name is the test's description, that points to an
+// object contains the steps (what side of the geometry editor to drag,
+// the amount of pixels) and the expectation.
+const TESTS = {
+ "Drag top's handler along x and y, south-east direction": {
+ "expects": "Only y axis is used to updated the top's element value",
+ "drag": "top",
+ "by": {x: 10, y: 10}
+ },
+ "Drag right's handler along x and y, south-east direction": {
+ "expects": "Only x axis is used to updated the right's element value",
+ "drag": "right",
+ "by": {x: 10, y: 10}
+ },
+ "Drag bottom's handler along x and y, south-east direction": {
+ "expects": "Only y axis is used to updated the bottom's element value",
+ "drag": "bottom",
+ "by": {x: 10, y: 10}
+ },
+ "Drag left's handler along x and y, south-east direction": {
+ "expects": "Only y axis is used to updated the left's element value",
+ "drag": "left",
+ "by": {x: 10, y: 10}
+ },
+ "Drag top's handler along x and y, north-west direction": {
+ "expects": "Only y axis is used to updated the top's element value",
+ "drag": "top",
+ "by": {x: -20, y: -20}
+ },
+ "Drag right's handler along x and y, north-west direction": {
+ "expects": "Only x axis is used to updated the right's element value",
+ "drag": "right",
+ "by": {x: -20, y: -20}
+ },
+ "Drag bottom's handler along x and y, north-west direction": {
+ "expects": "Only y axis is used to updated the bottom's element value",
+ "drag": "bottom",
+ "by": {x: -20, y: -20}
+ },
+ "Drag left's handler along x and y, north-west direction": {
+ "expects": "Only y axis is used to updated the left's element value",
+ "drag": "left",
+ "by": {x: -20, y: -20}
+ }
+};
+
+add_task(function* () {
+ let inspector = yield openInspectorForURL(TEST_URL);
+ let helper = yield getHighlighterHelperFor(HIGHLIGHTER_TYPE)(inspector);
+
+ helper.prefix = ID;
+
+ let { show, hide, finalize } = helper;
+
+ info("Showing the highlighter");
+ yield show("#node2");
+
+ for (let desc in TESTS) {
+ yield executeTest(helper, desc, TESTS[desc]);
+ }
+
+ info("Hiding the highlighter");
+ yield hide();
+ yield finalize();
+});
+
+function* executeTest(helper, desc, data) {
+ info(desc);
+
+ ok((yield areElementAndHighlighterMovedCorrectly(
+ helper, data.drag, data.by)), data.expects);
+}
+
+function* areElementAndHighlighterMovedCorrectly(helper, side, by) {
+ let { mouse, reflow, highlightedNode } = helper;
+
+ let {x, y} = yield getHandlerCoords(helper, side);
+
+ let dx = x + by.x;
+ let dy = y + by.y;
+
+ let beforeDragStyle = yield highlightedNode.getComputedStyle();
+
+ // simulate drag & drop
+ yield mouse.down(x, y);
+ yield mouse.move(dx, dy);
+ yield mouse.up();
+
+ yield reflow();
+
+ info(`Checking ${side} handler is moved correctly`);
+ yield isHandlerPositionUpdated(helper, side, x, y, by);
+
+ let delta = (side === "left" || side === "right") ? by.x : by.y;
+ delta = delta * ((side === "right" || side === "bottom") ? -1 : 1);
+
+ info("Checking element's sides are correct after drag & drop");
+ return yield areElementSideValuesCorrect(highlightedNode, beforeDragStyle,
+ side, delta);
+}
+
+function* isHandlerPositionUpdated(helper, name, x, y, by) {
+ let {x: afterDragX, y: afterDragY} = yield getHandlerCoords(helper, name);
+
+ if (name === "left" || name === "right") {
+ is(afterDragX, x + by.x,
+ `${name} handler's x axis updated.`);
+ is(afterDragY, y,
+ `${name} handler's y axis unchanged.`);
+ } else {
+ is(afterDragX, x,
+ `${name} handler's x axis unchanged.`);
+ is(afterDragY, y + by.y,
+ `${name} handler's y axis updated.`);
+ }
+}
+
+function* areElementSideValuesCorrect(node, beforeDragStyle, name, delta) {
+ let afterDragStyle = yield node.getComputedStyle();
+ let isSideCorrect = true;
+
+ for (let side of SIDES) {
+ let afterValue = Math.round(parseFloat(afterDragStyle[side].value));
+ let beforeValue = Math.round(parseFloat(beforeDragStyle[side].value));
+
+ if (side === name) {
+ // `isSideCorrect` is used only as test's return value, not to perform
+ // the actual test, because with `is` instead of `ok` we gather more
+ // information in case of failure
+ isSideCorrect = isSideCorrect && (afterValue === beforeValue + delta);
+
+ is(afterValue, beforeValue + delta,
+ `${side} is updated.`);
+ } else {
+ isSideCorrect = isSideCorrect && (afterValue === beforeValue);
+
+ is(afterValue, beforeValue,
+ `${side} is unchaged.`);
+ }
+ }
+
+ return isSideCorrect;
+}
+
+function* getHandlerCoords({getElementAttribute}, side) {
+ return {
+ x: Math.round(yield getElementAttribute("handler-" + side, "cx")),
+ y: Math.round(yield getElementAttribute("handler-" + side, "cy"))
+ };
+}
diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-hover_01.js b/devtools/client/inspector/test/browser_inspector_highlighter-hover_01.js
new file mode 100644
index 000000000..85f897080
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-hover_01.js
@@ -0,0 +1,41 @@
+/* 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";
+
+// Test that when first hovering over a node and immediately after selecting it
+// by clicking on it leaves the highlighter visible for as long as the mouse is
+// over the node
+
+const TEST_URL = "data:text/html;charset=utf-8," +
+ "<p>It's going to be legen....</p>";
+
+add_task(function* () {
+ let {inspector, testActor} = yield openInspectorForURL(TEST_URL);
+
+ info("hovering over the <p> line in the markup-view");
+ yield hoverContainer("p", inspector);
+ let isVisible = yield testActor.isHighlighting();
+ ok(isVisible, "the highlighter is still visible");
+
+ info("selecting the <p> line by clicking in the markup-view");
+ yield clickContainer("p", inspector);
+
+ yield testActor.setProperty("p", "textContent", "wait for it ....");
+ info("wait and see if the highlighter stays visible even after the node " +
+ "was selected");
+ yield waitForTheBrieflyShowBoxModelTimeout();
+
+ yield testActor.setProperty("p", "textContent", "dary!!!!");
+ isVisible = yield testActor.isHighlighting();
+ ok(isVisible, "the highlighter is still visible");
+});
+
+function waitForTheBrieflyShowBoxModelTimeout() {
+ let deferred = defer();
+ // Note that the current timeout is 1 sec and is neither configurable nor
+ // exported anywhere we can access, so hard-coding the timeout
+ setTimeout(deferred.resolve, 1500);
+ return deferred.promise;
+}
diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-hover_02.js b/devtools/client/inspector/test/browser_inspector_highlighter-hover_02.js
new file mode 100644
index 000000000..e853b3963
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-hover_02.js
@@ -0,0 +1,38 @@
+/* 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";
+
+// Test that when after an element is selected and highlighted on hover, if the
+// mouse leaves the markup-view and comes back again on the same element, that
+// the highlighter is shown again on the node
+
+const TEST_URL = "data:text/html;charset=utf-8,<p>Select me!</p>";
+
+add_task(function* () {
+ let {inspector, testActor} = yield openInspectorForURL(TEST_URL);
+
+ info("hover over the <p> line in the markup-view so that it's the " +
+ "currently hovered node");
+ yield hoverContainer("p", inspector);
+
+ info("select the <p> markup-container line by clicking");
+ yield clickContainer("p", inspector);
+ let isVisible = yield testActor.isHighlighting();
+ ok(isVisible, "the highlighter is shown");
+
+ info("listen to the highlighter's hidden event");
+ let onHidden = testActor.waitForHighlighterEvent("hidden",
+ inspector.highlighter);
+ info("mouse-leave the markup-view");
+ yield mouseLeaveMarkupView(inspector);
+ yield onHidden;
+ isVisible = yield testActor.isHighlighting();
+ ok(!isVisible, "the highlighter is hidden after mouseleave");
+
+ info("hover over the <p> line again, which is still selected");
+ yield hoverContainer("p", inspector);
+ isVisible = yield testActor.isHighlighting();
+ ok(isVisible, "the highlighter is visible again");
+});
diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-hover_03.js b/devtools/client/inspector/test/browser_inspector_highlighter-hover_03.js
new file mode 100644
index 000000000..fcd88be7f
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-hover_03.js
@@ -0,0 +1,55 @@
+/* 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";
+
+// Test that once a node has been hovered over and marked as such, if it is
+// navigated away using the keyboard, the highlighter moves to the new node, and
+// if it is then navigated back to, it is briefly highlighted again
+
+const TEST_PAGE = "data:text/html;charset=utf-8," +
+ "<p id=\"one\">one</p><p id=\"two\">two</p>";
+
+add_task(function* () {
+ let {inspector} = yield openInspectorForURL(TEST_PAGE);
+
+ info("Making sure the markup-view frame is focused");
+ inspector.markup._frame.focus();
+
+ // Mock the highlighter to easily track which node gets highlighted.
+ // We don't need to test here that the highlighter is actually visible, we
+ // just care about whether the markup-view asks it to be shown
+ let highlightedNode = null;
+ inspector.toolbox._highlighter.showBoxModel = function (nodeFront) {
+ highlightedNode = nodeFront;
+ return promise.resolve();
+ };
+ inspector.toolbox._highlighter.hideBoxModel = function () {
+ return promise.resolve();
+ };
+
+ function* isHighlighting(selector, desc) {
+ let nodeFront = yield getNodeFront(selector, inspector);
+ is(highlightedNode, nodeFront, desc);
+ }
+
+ info("Hover over <p#one> line in the markup-view");
+ yield hoverContainer("#one", inspector);
+ yield isHighlighting("#one", "<p#one> is highlighted");
+
+ info("Navigate to <p#two> with the keyboard");
+ let onUpdated = inspector.once("inspector-updated");
+ EventUtils.synthesizeKey("VK_DOWN", {}, inspector.panelWin);
+ yield onUpdated;
+ onUpdated = inspector.once("inspector-updated");
+ EventUtils.synthesizeKey("VK_DOWN", {}, inspector.panelWin);
+ yield onUpdated;
+ yield isHighlighting("#two", "<p#two> is highlighted");
+
+ info("Navigate back to <p#one> with the keyboard");
+ onUpdated = inspector.once("inspector-updated");
+ EventUtils.synthesizeKey("VK_UP", {}, inspector.panelWin);
+ yield onUpdated;
+ yield isHighlighting("#one", "<p#one> is highlighted again");
+});
diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-iframes_01.js b/devtools/client/inspector/test/browser_inspector_highlighter-iframes_01.js
new file mode 100644
index 000000000..6475937c4
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-iframes_01.js
@@ -0,0 +1,64 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set 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";
+
+// Testing that moving the mouse over the document with the element picker
+// started highlights nodes
+
+const NESTED_FRAME_SRC = "data:text/html;charset=utf-8," +
+ "nested iframe<div>nested div</div>";
+
+const OUTER_FRAME_SRC = "data:text/html;charset=utf-8," +
+ "little frame<div>little div</div>" +
+ "<iframe src='" + NESTED_FRAME_SRC + "' />";
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "iframe tests for inspector" +
+ "<iframe src=\"" + OUTER_FRAME_SRC + "\" />";
+
+add_task(function* () {
+ let {inspector, testActor} = yield openInspectorForURL(TEST_URI);
+ let outerFrameDiv = ["iframe", "div"];
+ let innerFrameDiv = ["iframe", "iframe", "div"];
+
+ info("Waiting for element picker to activate.");
+ yield startPicker(inspector.toolbox);
+
+ info("Moving mouse over outerFrameDiv");
+ yield moveMouseOver(outerFrameDiv);
+ ok((yield testActor.assertHighlightedNode(outerFrameDiv)),
+ "outerFrameDiv is highlighted.");
+
+ info("Moving mouse over innerFrameDiv");
+ yield moveMouseOver(innerFrameDiv);
+ ok((yield testActor.assertHighlightedNode(innerFrameDiv)),
+ "innerFrameDiv is highlighted.");
+
+ info("Selecting root node");
+ yield selectNode(inspector.walker.rootNode, inspector);
+
+ info("Selecting an element from the nested iframe directly");
+ let innerFrameFront = yield getNodeFrontInFrame("iframe", "iframe",
+ inspector);
+ let innerFrameDivFront = yield getNodeFrontInFrame("div", innerFrameFront,
+ inspector);
+ yield selectNode(innerFrameDivFront, inspector);
+
+ is(inspector.breadcrumbs.nodeHierarchy.length, 9,
+ "Breadcrumbs have 9 items.");
+
+ info("Waiting for element picker to deactivate.");
+ yield inspector.toolbox.highlighterUtils.stopPicker();
+
+ function moveMouseOver(selector) {
+ info("Waiting for element " + selector + " to be highlighted");
+ testActor.synthesizeMouse({
+ selector: selector,
+ options: {type: "mousemove"},
+ center: true
+ }).then(() => inspector.toolbox.once("picker-node-hovered"));
+ }
+});
diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-iframes_02.js b/devtools/client/inspector/test/browser_inspector_highlighter-iframes_02.js
new file mode 100644
index 000000000..12f44ce32
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-iframes_02.js
@@ -0,0 +1,59 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set 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";
+
+// Test that the highlighter is correctly positioned when switching context
+// to an iframe that has an offset from the parent viewport (eg. 100px margin)
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<div id=\"outer\"></div>" +
+ "<iframe style='margin:100px' src='data:text/html," +
+ "<div id=\"inner\">Look I am here!</div>'>";
+
+add_task(function* () {
+ info("Enable command-button-frames preference setting");
+ Services.prefs.setBoolPref("devtools.command-button-frames.enabled", true);
+ let {inspector, toolbox, testActor} = yield openInspectorForURL(TEST_URI);
+
+ info("Switch to the iframe context.");
+ yield switchToFrameContext(1, toolbox, inspector);
+
+ info("Check navigation was successful.");
+ let hasOuterNode = yield testActor.hasNode("#outer");
+ ok(!hasOuterNode, "Check testActor has no access to outer element");
+ let hasTestNode = yield testActor.hasNode("#inner");
+ ok(hasTestNode, "Check testActor has access to inner element");
+
+ info("Check highlighting is correct after switching iframe context");
+ yield selectAndHighlightNode("#inner", inspector);
+ let isHighlightCorrect = yield testActor.assertHighlightedNode("#inner");
+ ok(isHighlightCorrect, "The selected node is properly highlighted.");
+
+ info("Cleanup command-button-frames preferences.");
+ Services.prefs.clearUserPref("devtools.command-button-frames.enabled");
+});
+
+/**
+ * Helper designed to switch context to another frame at the provided index.
+ * Returns a promise that will resolve when the navigation is complete.
+ * @return {Promise}
+ */
+function* switchToFrameContext(frameIndex, toolbox, inspector) {
+ // Open frame menu and wait till it's available on the screen.
+ let btn = toolbox.doc.getElementById("command-button-frames");
+ let menu = toolbox.showFramesMenu({target: btn});
+ yield once(menu, "open");
+
+ info("Select the iframe in the frame list.");
+ let newRoot = inspector.once("new-root");
+
+ menu.items[frameIndex].click();
+
+ yield newRoot;
+ yield inspector.once("inspector-updated");
+
+ info("Navigation to the iframe is done.");
+}
diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-inline.js b/devtools/client/inspector/test/browser_inspector_highlighter-inline.js
new file mode 100644
index 000000000..4e39a92f9
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-inline.js
@@ -0,0 +1,76 @@
+/* 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";
+
+requestLongerTimeout(2);
+
+// Test that highlighting various inline boxes displays the right number of
+// polygons in the page.
+
+const TEST_URL = URL_ROOT + "doc_inspector_highlighter_inline.html";
+const TEST_DATA = [
+ "body",
+ "h1",
+ "h2",
+ "h2 em",
+ "p",
+ "p span",
+ // The following test case used to fail. See bug 1139925.
+ "[dir=rtl] > span"
+];
+
+add_task(function* () {
+ info("Loading the test document and opening the inspector");
+ let {inspector, testActor} = yield openInspectorForURL(TEST_URL);
+
+ for (let selector of TEST_DATA) {
+ info("Selecting and highlighting node " + selector);
+ yield selectAndHighlightNode(selector, inspector);
+
+ info("Get all quads for this node");
+ let data = yield testActor.getAllAdjustedQuads(selector);
+
+ info("Iterate over the box-model regions and verify that the highlighter " +
+ "is correct");
+ for (let region of ["margin", "border", "padding", "content"]) {
+ let {points} = yield testActor.getHighlighterRegionPath(region);
+ is(points.length, data[region].length, "The highlighter's " + region +
+ " path defines the correct number of boxes");
+ }
+
+ info("Verify that the guides define a rectangle that contains all " +
+ "content boxes");
+
+ let expectedContentRect = {
+ p1: {x: Infinity, y: Infinity},
+ p2: {x: -Infinity, y: Infinity},
+ p3: {x: -Infinity, y: -Infinity},
+ p4: {x: Infinity, y: -Infinity}
+ };
+ for (let {p1, p2, p3, p4} of data.content) {
+ expectedContentRect.p1.x = Math.min(expectedContentRect.p1.x, p1.x);
+ expectedContentRect.p1.y = Math.min(expectedContentRect.p1.y, p1.y);
+ expectedContentRect.p2.x = Math.max(expectedContentRect.p2.x, p2.x);
+ expectedContentRect.p2.y = Math.min(expectedContentRect.p2.y, p2.y);
+ expectedContentRect.p3.x = Math.max(expectedContentRect.p3.x, p3.x);
+ expectedContentRect.p3.y = Math.max(expectedContentRect.p3.y, p3.y);
+ expectedContentRect.p4.x = Math.min(expectedContentRect.p4.x, p4.x);
+ expectedContentRect.p4.y = Math.max(expectedContentRect.p4.y, p4.y);
+ }
+
+ let contentRect = yield testActor.getGuidesRectangle();
+
+ for (let point of ["p1", "p2", "p3", "p4"]) {
+ is((contentRect[point].x),
+ (expectedContentRect[point].x),
+ "x coordinate of point " + point +
+ " of the content rectangle defined by the outer guides is correct");
+ is((contentRect[point].y),
+ (expectedContentRect[point].y),
+ "y coordinate of point " + point +
+ " of the content rectangle defined by the outer guides is correct");
+ }
+ }
+});
diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-keybinding_01.js b/devtools/client/inspector/test/browser_inspector_highlighter-keybinding_01.js
new file mode 100644
index 000000000..100dd1e6e
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-keybinding_01.js
@@ -0,0 +1,64 @@
+/* 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";
+
+// Test that the keybindings for Picker work alright
+
+const TEST_URL = URL_ROOT + "doc_inspector_highlighter_dom.html";
+
+add_task(function* () {
+ let {inspector, toolbox, testActor} = yield openInspectorForURL(TEST_URL);
+
+ yield startPicker(toolbox);
+
+ info("Selecting the simple-div1 DIV");
+ yield moveMouseOver("#simple-div1");
+
+ ok((yield testActor.assertHighlightedNode("#simple-div1")),
+ "The highlighter shows #simple-div1. OK.");
+
+ // First Child selection
+ info("Testing first-child selection.");
+
+ yield doKeyHover({key: "VK_RIGHT", options: {}});
+ ok((yield testActor.assertHighlightedNode("#useless-para")),
+ "The highlighter shows #useless-para. OK.");
+
+ info("Selecting the useful-para paragraph DIV");
+ yield moveMouseOver("#useful-para");
+ ok((yield testActor.assertHighlightedNode("#useful-para")),
+ "The highlighter shows #useful-para. OK.");
+
+ yield doKeyHover({key: "VK_RIGHT", options: {}});
+ ok((yield testActor.assertHighlightedNode("#bold")),
+ "The highlighter shows #bold. OK.");
+
+ info("Going back up to the simple-div1 DIV");
+ yield doKeyHover({key: "VK_LEFT", options: {}});
+ yield doKeyHover({key: "VK_LEFT", options: {}});
+ ok((yield testActor.assertHighlightedNode("#simple-div1")),
+ "The highlighter shows #simple-div1. OK.");
+
+ info("First child selection test Passed.");
+
+ info("Stopping the picker");
+ yield toolbox.highlighterUtils.stopPicker();
+
+ function doKeyHover(args) {
+ info("Key pressed. Waiting for element to be highlighted/hovered");
+ testActor.synthesizeKey(args);
+ return inspector.toolbox.once("picker-node-hovered");
+ }
+
+ function moveMouseOver(selector) {
+ info("Waiting for element " + selector + " to be highlighted");
+ testActor.synthesizeMouse({
+ options: {type: "mousemove"},
+ center: true,
+ selector: selector
+ });
+ return inspector.toolbox.once("picker-node-hovered");
+ }
+});
diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-keybinding_02.js b/devtools/client/inspector/test/browser_inspector_highlighter-keybinding_02.js
new file mode 100644
index 000000000..96d5449f9
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-keybinding_02.js
@@ -0,0 +1,64 @@
+/* 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";
+
+// Test that the keybindings for Picker work alright
+
+const TEST_URL = URL_ROOT + "doc_inspector_highlighter_dom.html";
+
+add_task(function* () {
+ let {inspector, toolbox, testActor} = yield openInspectorForURL(TEST_URL);
+
+ yield startPicker(toolbox);
+
+ // Previously chosen child memory
+ info("Testing whether previously chosen child is remembered");
+
+ info("Selecting the ahoy paragraph DIV");
+ yield moveMouseOver("#ahoy");
+
+ yield doKeyHover({key: "VK_LEFT", options: {}});
+ ok((yield testActor.assertHighlightedNode("#simple-div2")),
+ "The highlighter shows #simple-div2. OK.");
+
+ yield doKeyHover({key: "VK_RIGHT", options: {}});
+ ok((yield testActor.assertHighlightedNode("#ahoy")),
+ "The highlighter shows #ahoy. OK.");
+
+ info("Going back up to the complex-div DIV");
+ yield doKeyHover({key: "VK_LEFT", options: {}});
+ yield doKeyHover({key: "VK_LEFT", options: {}});
+ ok((yield testActor.assertHighlightedNode("#complex-div")),
+ "The highlighter shows #complex-div. OK.");
+
+ yield doKeyHover({key: "VK_RIGHT", options: {}});
+ ok((yield testActor.assertHighlightedNode("#simple-div2")),
+ "The highlighter shows #simple-div2. OK.");
+
+ info("Previously chosen child is remembered. Passed.");
+
+ info("Stopping the picker");
+ yield toolbox.highlighterUtils.stopPicker();
+
+ function doKeyHover(args) {
+ info("Key pressed. Waiting for element to be highlighted/hovered");
+ let onHighlighterReady = toolbox.once("highlighter-ready");
+ let onPickerNodeHovered = inspector.toolbox.once("picker-node-hovered");
+ testActor.synthesizeKey(args);
+ return promise.all([onHighlighterReady, onPickerNodeHovered]);
+ }
+
+ function moveMouseOver(selector) {
+ info("Waiting for element " + selector + " to be highlighted");
+ let onHighlighterReady = toolbox.once("highlighter-ready");
+ let onPickerNodeHovered = inspector.toolbox.once("picker-node-hovered");
+ testActor.synthesizeMouse({
+ options: {type: "mousemove"},
+ center: true,
+ selector: selector
+ });
+ return promise.all([onHighlighterReady, onPickerNodeHovered]);
+ }
+});
diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-keybinding_03.js b/devtools/client/inspector/test/browser_inspector_highlighter-keybinding_03.js
new file mode 100644
index 000000000..99a2316bb
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-keybinding_03.js
@@ -0,0 +1,71 @@
+/* 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";
+
+// Test that the keybindings for Picker work alright
+
+const IS_OSX = Services.appinfo.OS === "Darwin";
+const TEST_URL = URL_ROOT + "doc_inspector_highlighter_dom.html";
+
+add_task(function* () {
+ let {inspector, toolbox, testActor} = yield openInspectorForURL(TEST_URL);
+
+ yield startPicker(toolbox);
+ yield moveMouseOver("#another");
+
+ info("Testing enter/return key as pick-node command");
+ yield doKeyPick({key: "VK_RETURN", options: {}});
+ is(inspector.selection.nodeFront.id, "another",
+ "The #another node was selected. Passed.");
+
+ info("Testing escape key as cancel-picker command");
+ yield startPicker(toolbox);
+ yield moveMouseOver("#ahoy");
+ yield doKeyStop({key: "VK_ESCAPE", options: {}});
+ is(inspector.selection.nodeFront.id, "another",
+ "The #another DIV is still selected. Passed.");
+
+ info("Testing Ctrl+Shift+C shortcut as cancel-picker command");
+ yield startPicker(toolbox);
+ yield moveMouseOver("#ahoy");
+ let shortcutOpts = {key: "VK_C", options: {}};
+ if (IS_OSX) {
+ shortcutOpts.options.metaKey = true;
+ shortcutOpts.options.altKey = true;
+ } else {
+ shortcutOpts.options.ctrlKey = true;
+ shortcutOpts.options.shiftKey = true;
+ }
+ yield doKeyStop(shortcutOpts);
+ is(inspector.selection.nodeFront.id, "another",
+ "The #another DIV is still selected. Passed.");
+
+ function doKeyPick(args) {
+ info("Key pressed. Waiting for element to be picked");
+ testActor.synthesizeKey(args);
+ return promise.all([
+ inspector.selection.once("new-node-front"),
+ inspector.once("inspector-updated")
+ ]);
+ }
+
+ function doKeyStop(args) {
+ info("Key pressed. Waiting for picker to be canceled");
+ testActor.synthesizeKey(args);
+ return inspector.toolbox.once("picker-stopped");
+ }
+
+ function moveMouseOver(selector) {
+ info("Waiting for element " + selector + " to be highlighted");
+ let onHighlighterReady = toolbox.once("highlighter-ready");
+ let onPickerNodeHovered = inspector.toolbox.once("picker-node-hovered");
+ testActor.synthesizeMouse({
+ options: {type: "mousemove"},
+ center: true,
+ selector: selector
+ });
+ return promise.all([onHighlighterReady, onPickerNodeHovered]);
+ }
+});
diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-keybinding_04.js b/devtools/client/inspector/test/browser_inspector_highlighter-keybinding_04.js
new file mode 100644
index 000000000..f53ca8ee6
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-keybinding_04.js
@@ -0,0 +1,46 @@
+/* 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";
+
+// Tests that pressing ESC twice while in picker mode first stops the picker and
+// then opens the split-console (see bug 988278).
+
+const TEST_URL = "data:text/html;charset=utf8,<div></div>";
+
+add_task(function* () {
+ let {inspector, toolbox, testActor} = yield openInspectorForURL(TEST_URL);
+
+ yield startPicker(toolbox);
+
+ info("Start using the picker by hovering over nodes");
+ let onHover = toolbox.once("picker-node-hovered");
+ testActor.synthesizeMouse({
+ options: {type: "mousemove"},
+ center: true,
+ selector: "div"
+ });
+ yield onHover;
+
+ info("Press escape and wait for the picker to stop");
+ let onPickerStopped = toolbox.once("picker-stopped");
+ testActor.synthesizeKey({
+ key: "VK_ESCAPE",
+ options: {}
+ });
+ yield onPickerStopped;
+
+ info("Press escape again and wait for the split console to open");
+ let onSplitConsole = toolbox.once("split-console");
+ let onConsoleReady = toolbox.once("webconsole-ready");
+ // The escape key is synthesized in the main process, which is where the focus
+ // should be after the picker was stopped.
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, inspector.panelWin);
+ yield onSplitConsole;
+ yield onConsoleReady;
+ ok(toolbox.splitConsole, "The split console is shown.");
+
+ // Hide the split console.
+ yield toolbox.toggleSplitConsole();
+});
diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-measure_01.js b/devtools/client/inspector/test/browser_inspector_highlighter-measure_01.js
new file mode 100644
index 000000000..b7f20e2fb
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-measure_01.js
@@ -0,0 +1,88 @@
+/* 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 TEST_URL = `data:text/html;charset=utf-8,
+ <div style='
+ position:absolute;
+ left: 0;
+ top: 0;
+ width: 40000px;
+ height: 8000px'>
+ </div>`;
+
+const PREFIX = "measuring-tool-highlighter-";
+const HIGHLIGHTER_TYPE = "MeasuringToolHighlighter";
+
+const X = 32;
+const Y = 20;
+
+add_task(function* () {
+ let helper = yield openInspectorForURL(TEST_URL)
+ .then(getHighlighterHelperFor(HIGHLIGHTER_TYPE));
+
+ let { finalize } = helper;
+
+ helper.prefix = PREFIX;
+
+ yield isHiddenByDefault(helper);
+ yield areLabelsHiddenByDefaultWhenShows(helper);
+ yield areLabelsProperlyDisplayedWhenMouseMoved(helper);
+
+ yield finalize();
+});
+
+function* isHiddenByDefault({isElementHidden}) {
+ info("Checking the highlighter is hidden by default");
+
+ let hidden = yield isElementHidden("elements");
+ ok(hidden, "highlighter's root is hidden by default");
+
+ hidden = yield isElementHidden("label-size");
+ ok(hidden, "highlighter's label size is hidden by default");
+
+ hidden = yield isElementHidden("label-position");
+ ok(hidden, "highlighter's label position is hidden by default");
+}
+
+function* areLabelsHiddenByDefaultWhenShows({isElementHidden, show}) {
+ info("Checking the highlighter is displayed when asked");
+
+ yield show();
+
+ let hidden = yield isElementHidden("elements");
+ is(hidden, false, "highlighter is visible after show");
+
+ hidden = yield isElementHidden("label-size");
+ ok(hidden, "label's size still hidden");
+
+ hidden = yield isElementHidden("label-position");
+ ok(hidden, "label's position still hidden");
+}
+
+function* areLabelsProperlyDisplayedWhenMouseMoved({isElementHidden,
+ synthesizeMouse, getElementTextContent}) {
+ info("Checking labels are properly displayed when mouse moved");
+
+ yield synthesizeMouse({
+ selector: ":root",
+ options: {type: "mousemove"},
+ x: X,
+ y: Y
+ });
+
+ let hidden = yield isElementHidden("label-position");
+ is(hidden, false, "label's position is displayed after the mouse is moved");
+
+ hidden = yield isElementHidden("label-size");
+ ok(hidden, "label's size still hidden");
+
+ let text = yield getElementTextContent("label-position");
+
+ let [x, y] = text.replace(/ /g, "").split(/\n/);
+
+ is(+x, X, "label's position shows the proper X coord");
+ is(+y, Y, "label's position shows the proper Y coord");
+}
diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-measure_02.js b/devtools/client/inspector/test/browser_inspector_highlighter-measure_02.js
new file mode 100644
index 000000000..424cc183a
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-measure_02.js
@@ -0,0 +1,130 @@
+/* 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 TEST_URL = `data:text/html;charset=utf-8,
+ <div style='
+ position:absolute;
+ left: 0;
+ top: 0;
+ width: 40000px;
+ height: 8000px'>
+ </div>`;
+
+const PREFIX = "measuring-tool-highlighter-";
+const HIGHLIGHTER_TYPE = "MeasuringToolHighlighter";
+
+const SIDES = ["top", "right", "bottom", "left"];
+
+const X = 32;
+const Y = 20;
+const WIDTH = 160;
+const HEIGHT = 100;
+const HYPOTENUSE = Math.hypot(WIDTH, HEIGHT).toFixed(2);
+
+add_task(function* () {
+ let helper = yield openInspectorForURL(TEST_URL)
+ .then(getHighlighterHelperFor(HIGHLIGHTER_TYPE));
+
+ let { show, finalize } = helper;
+
+ helper.prefix = PREFIX;
+
+ yield show();
+
+ yield hasNoLabelsWhenStarts(helper);
+ yield hasSizeLabelWhenMoved(helper);
+ yield hasCorrectSizeLabelValue(helper);
+ yield hasSizeLabelAndGuidesWhenStops(helper);
+ yield hasCorrectSizeLabelValue(helper);
+
+ yield finalize();
+});
+
+function* hasNoLabelsWhenStarts({isElementHidden, synthesizeMouse}) {
+ info("Checking highlighter has no labels when we start to select");
+
+ yield synthesizeMouse({
+ selector: ":root",
+ options: {type: "mousedown"},
+ x: X,
+ y: Y
+ });
+
+ let hidden = yield isElementHidden("label-size");
+ ok(hidden, "label's size still hidden");
+
+ hidden = yield isElementHidden("label-position");
+ ok(hidden, "label's position still hidden");
+
+ info("Checking highlighter has no guides when we start to select");
+
+ let guidesHidden = true;
+ for (let side of SIDES) {
+ guidesHidden = guidesHidden && (yield isElementHidden("guide-" + side));
+ }
+
+ ok(guidesHidden, "guides are hidden during dragging");
+}
+
+function* hasSizeLabelWhenMoved({isElementHidden, synthesizeMouse}) {
+ info("Checking highlighter has size label when we select the area");
+
+ yield synthesizeMouse({
+ selector: ":root",
+ options: {type: "mousemove"},
+ x: X + WIDTH,
+ y: Y + HEIGHT
+ });
+
+ let hidden = yield isElementHidden("label-size");
+ is(hidden, false, "label's size is visible during selection");
+
+ hidden = yield isElementHidden("label-position");
+ ok(hidden, "label's position still hidden");
+
+ info("Checking highlighter has no guides when we select the area");
+
+ let guidesHidden = true;
+ for (let side of SIDES) {
+ guidesHidden = guidesHidden && (yield isElementHidden("guide-" + side));
+ }
+
+ ok(guidesHidden, "guides are hidden during selection");
+}
+
+function* hasSizeLabelAndGuidesWhenStops({isElementHidden, synthesizeMouse}) {
+ info("Checking highlighter has size label and guides when we stop");
+
+ yield synthesizeMouse({
+ selector: ":root",
+ options: {type: "mouseup"},
+ x: X + WIDTH,
+ y: Y + HEIGHT
+ });
+
+ let hidden = yield isElementHidden("label-size");
+ is(hidden, false, "label's size is visible when the selection is done");
+
+ hidden = yield isElementHidden("label-position");
+ ok(hidden, "label's position still hidden");
+
+ let guidesVisible = true;
+ for (let side of SIDES) {
+ guidesVisible = guidesVisible && !(yield isElementHidden("guide-" + side));
+ }
+
+ ok(guidesVisible, "guides are visible when the selection is done");
+}
+
+function* hasCorrectSizeLabelValue({getElementTextContent}) {
+ let text = yield getElementTextContent("label-size");
+
+ let [width, height, hypot] = text.match(/\d.*px/g);
+
+ is(parseFloat(width), WIDTH, "width on label's size is correct");
+ is(parseFloat(height), HEIGHT, "height on label's size is correct");
+ is(parseFloat(hypot), HYPOTENUSE, "hypotenuse on label's size is correct");
+}
diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-options.js b/devtools/client/inspector/test/browser_inspector_highlighter-options.js
new file mode 100644
index 000000000..65a6ec4b0
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-options.js
@@ -0,0 +1,204 @@
+/* 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";
+
+// Check that the box-model highlighter supports configuration options
+
+const TEST_URL = `
+ <body style="padding:2em;">
+ <div style="width:100px;height:100px;padding:2em;
+ border:.5em solid black;margin:1em;">test</div>
+ </body>
+`;
+
+// Test data format:
+// - desc: a string that will be output to the console.
+// - options: json object to be passed as options to the highlighter.
+// - checkHighlighter: a generator (async) function that should check the
+// highlighter is correct.
+const TEST_DATA = [
+ {
+ desc: "Guides and infobar should be shown by default",
+ options: {},
+ checkHighlighter: function* (testActor) {
+ let hidden = yield testActor.getHighlighterNodeAttribute(
+ "box-model-infobar-container", "hidden");
+ ok(!hidden, "Node infobar is visible");
+
+ hidden = yield testActor.getHighlighterNodeAttribute(
+ "box-model-elements", "hidden");
+ ok(!hidden, "SVG container is visible");
+
+ for (let side of ["top", "right", "bottom", "left"]) {
+ hidden = yield testActor.getHighlighterNodeAttribute(
+ "box-model-guide-" + side, "hidden");
+ ok(!hidden, side + " guide is visible");
+ }
+ }
+ },
+ {
+ desc: "All regions should be shown by default",
+ options: {},
+ checkHighlighter: function* (testActor) {
+ for (let region of ["margin", "border", "padding", "content"]) {
+ let {d} = yield testActor.getHighlighterRegionPath(region);
+ ok(d, "Region " + region + " has set coordinates");
+ }
+ }
+ },
+ {
+ desc: "Guides can be hidden",
+ options: {hideGuides: true},
+ checkHighlighter: function* (testActor) {
+ for (let side of ["top", "right", "bottom", "left"]) {
+ let hidden = yield testActor.getHighlighterNodeAttribute(
+ "box-model-guide-" + side, "hidden");
+ is(hidden, "true", side + " guide has been hidden");
+ }
+ }
+ },
+ {
+ desc: "Infobar can be hidden",
+ options: {hideInfoBar: true},
+ checkHighlighter: function* (testActor) {
+ let hidden = yield testActor.getHighlighterNodeAttribute(
+ "box-model-infobar-container", "hidden");
+ is(hidden, "true", "infobar has been hidden");
+ }
+ },
+ {
+ desc: "One region only can be shown (1)",
+ options: {showOnly: "content"},
+ checkHighlighter: function* (testActor) {
+ let {d} = yield testActor.getHighlighterRegionPath("margin");
+ ok(!d, "margin region is hidden");
+
+ ({d} = yield testActor.getHighlighterRegionPath("border"));
+ ok(!d, "border region is hidden");
+
+ ({d} = yield testActor.getHighlighterRegionPath("padding"));
+ ok(!d, "padding region is hidden");
+
+ ({d} = yield testActor.getHighlighterRegionPath("content"));
+ ok(d, "content region is shown");
+ }
+ },
+ {
+ desc: "One region only can be shown (2)",
+ options: {showOnly: "margin"},
+ checkHighlighter: function* (testActor) {
+ let {d} = yield testActor.getHighlighterRegionPath("margin");
+ ok(d, "margin region is shown");
+
+ ({d} = yield testActor.getHighlighterRegionPath("border"));
+ ok(!d, "border region is hidden");
+
+ ({d} = yield testActor.getHighlighterRegionPath("padding"));
+ ok(!d, "padding region is hidden");
+
+ ({d} = yield testActor.getHighlighterRegionPath("content"));
+ ok(!d, "content region is hidden");
+ }
+ },
+ {
+ desc: "Guides can be drawn around a given region (1)",
+ options: {region: "padding"},
+ checkHighlighter: function* (testActor) {
+ let topY1 = yield testActor.getHighlighterNodeAttribute(
+ "box-model-guide-top", "y1");
+ let rightX1 = yield testActor.getHighlighterNodeAttribute(
+ "box-model-guide-right", "x1");
+ let bottomY1 = yield testActor.getHighlighterNodeAttribute(
+ "box-model-guide-bottom", "y1");
+ let leftX1 = yield testActor.getHighlighterNodeAttribute(
+ "box-model-guide-left", "x1");
+
+ let {points} = yield testActor.getHighlighterRegionPath("padding");
+ points = points[0];
+
+ is(Math.ceil(topY1), points[0][1], "Top guide's y1 is correct");
+ is(Math.floor(rightX1), points[1][0], "Right guide's x1 is correct");
+ is(Math.floor(bottomY1), points[2][1], "Bottom guide's y1 is correct");
+ is(Math.ceil(leftX1), points[3][0], "Left guide's x1 is correct");
+ }
+ },
+ {
+ desc: "Guides can be drawn around a given region (2)",
+ options: {region: "margin"},
+ checkHighlighter: function* (testActor) {
+ let topY1 = yield testActor.getHighlighterNodeAttribute(
+ "box-model-guide-top", "y1");
+ let rightX1 = yield testActor.getHighlighterNodeAttribute(
+ "box-model-guide-right", "x1");
+ let bottomY1 = yield testActor.getHighlighterNodeAttribute(
+ "box-model-guide-bottom", "y1");
+ let leftX1 = yield testActor.getHighlighterNodeAttribute(
+ "box-model-guide-left", "x1");
+
+ let {points} = yield testActor.getHighlighterRegionPath("margin");
+ points = points[0];
+
+ is(Math.ceil(topY1), points[0][1], "Top guide's y1 is correct");
+ is(Math.floor(rightX1), points[1][0], "Right guide's x1 is correct");
+ is(Math.floor(bottomY1), points[2][1], "Bottom guide's y1 is correct");
+ is(Math.ceil(leftX1), points[3][0], "Left guide's x1 is correct");
+ }
+ },
+ {
+ desc: "When showOnly is used, other regions can be faded",
+ options: {showOnly: "margin", onlyRegionArea: true},
+ checkHighlighter: function* (testActor) {
+ for (let region of ["margin", "border", "padding", "content"]) {
+ let {d} = yield testActor.getHighlighterRegionPath(region);
+ ok(d, "Region " + region + " is shown (it has a d attribute)");
+
+ let faded = yield testActor.getHighlighterNodeAttribute(
+ "box-model-" + region, "faded");
+ if (region === "margin") {
+ ok(!faded, "The margin region is not faded");
+ } else {
+ is(faded, "true", "Region " + region + " is faded");
+ }
+ }
+ }
+ },
+ {
+ desc: "When showOnly is used, other regions can be faded (2)",
+ options: {showOnly: "padding", onlyRegionArea: true},
+ checkHighlighter: function* (testActor) {
+ for (let region of ["margin", "border", "padding", "content"]) {
+ let {d} = yield testActor.getHighlighterRegionPath(region);
+ ok(d, "Region " + region + " is shown (it has a d attribute)");
+
+ let faded = yield testActor.getHighlighterNodeAttribute(
+ "box-model-" + region, "faded");
+ if (region === "padding") {
+ ok(!faded, "The padding region is not faded");
+ } else {
+ is(faded, "true", "Region " + region + " is faded");
+ }
+ }
+ }
+ }
+];
+
+add_task(function* () {
+ let {inspector, testActor} = yield openInspectorForURL(
+ "data:text/html;charset=utf-8," + encodeURI(TEST_URL));
+
+ let divFront = yield getNodeFront("div", inspector);
+
+ for (let {desc, options, checkHighlighter} of TEST_DATA) {
+ info("Running test: " + desc);
+
+ info("Show the box-model highlighter with options " + options);
+ yield inspector.highlighter.showBoxModel(divFront, options);
+
+ yield checkHighlighter(testActor);
+
+ info("Hide the box-model highlighter");
+ yield inspector.highlighter.hideBoxModel();
+ }
+});
diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-preview.js b/devtools/client/inspector/test/browser_inspector_highlighter-preview.js
new file mode 100644
index 000000000..5f525786a
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-preview.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/. */
+
+"use strict";
+
+// Test that the highlighter is correctly displayed and picker mode is not stopped after
+// a shift-click (preview)
+
+const TEST_URI = `data:text/html;charset=utf-8,
+ <p id="one">one</p><p id="two">two</p><p id="three">three</p>`;
+
+add_task(function* () {
+ let {inspector, toolbox, testActor} = yield openInspectorForURL(TEST_URI);
+
+ let body = yield getNodeFront("body", inspector);
+ is(inspector.selection.nodeFront, body, "By default the body node is selected");
+
+ info("Start the element picker");
+ yield startPicker(toolbox);
+
+ info("Shift-clicking element #one should select it but keep the picker ON");
+ yield clickElement("#one", testActor, inspector, true);
+ yield checkElementSelected("#one", inspector);
+ checkPickerMode(toolbox, true);
+
+ info("Shift-clicking element #two should select it but keep the picker ON");
+ yield clickElement("#two", testActor, inspector, true);
+ yield checkElementSelected("#two", inspector);
+ checkPickerMode(toolbox, true);
+
+ info("Clicking element #three should select it and turn the picker OFF");
+ yield clickElement("#three", testActor, inspector, false);
+ yield checkElementSelected("#three", inspector);
+ checkPickerMode(toolbox, false);
+});
+
+function* clickElement(selector, testActor, inspector, isShift) {
+ let onSelectionChanged = inspector.once("inspector-updated");
+ yield testActor.synthesizeMouse({
+ selector: selector,
+ center: true,
+ options: { shiftKey: isShift }
+ });
+ yield onSelectionChanged;
+}
+
+function* checkElementSelected(selector, inspector) {
+ let el = yield getNodeFront(selector, inspector);
+ is(inspector.selection.nodeFront, el, `The element ${selector} is now selected`);
+}
+
+function checkPickerMode(toolbox, isOn) {
+ let pickerButton = toolbox.doc.querySelector("#command-button-pick");
+ is(pickerButton.hasAttribute("checked"), isOn, "The picker mode is correct");
+}
diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-rect_01.js b/devtools/client/inspector/test/browser_inspector_highlighter-rect_01.js
new file mode 100644
index 000000000..9645e25d9
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-rect_01.js
@@ -0,0 +1,121 @@
+/* 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";
+
+// Test that the custom rect highlighter provides the right API, ensures that
+// the input is valid and that it does create a box with the right dimensions,
+// at the right position.
+
+const TEST_URL = "data:text/html;charset=utf-8,Rect Highlighter Test";
+
+add_task(function* () {
+ let {inspector, testActor} = yield openInspectorForURL(TEST_URL);
+ let front = inspector.inspector;
+ let highlighter = yield front.getHighlighterByType("RectHighlighter");
+ let body = yield getNodeFront("body", inspector);
+
+ info("Make sure the highlighter returned is correct");
+
+ ok(highlighter, "The RectHighlighter custom type was created");
+ is(highlighter.typeName, "customhighlighter",
+ "The RectHighlighter has the right type");
+ ok(highlighter.show && highlighter.hide,
+ "The RectHighlighter has the expected show/hide methods");
+
+ info("Check that the highlighter is hidden by default");
+
+ let hidden = yield testActor.getHighlighterNodeAttribute(
+ "highlighted-rect", "hidden", highlighter);
+ is(hidden, "true", "The highlighter is hidden by default");
+
+ info("Check that nothing is shown if no rect is passed");
+
+ yield highlighter.show(body);
+ hidden = yield testActor.getHighlighterNodeAttribute(
+ "highlighted-rect", "hidden", highlighter);
+ is(hidden, "true", "The highlighter is hidden when no rect is passed");
+
+ info("Check that nothing is shown if rect is incomplete or invalid");
+
+ yield highlighter.show(body, {
+ rect: {x: 0, y: 0}
+ });
+ hidden = yield testActor.getHighlighterNodeAttribute(
+ "highlighted-rect", "hidden", highlighter);
+ is(hidden, "true", "The highlighter is hidden when the rect is incomplete");
+
+ yield highlighter.show(body, {
+ rect: {x: 0, y: 0, width: -Infinity, height: 0}
+ });
+ hidden = yield testActor.getHighlighterNodeAttribute(
+ "highlighted-rect", "hidden", highlighter);
+ is(hidden, "true", "The highlighter is hidden when the rect is invalid (1)");
+
+ yield highlighter.show(body, {
+ rect: {x: 0, y: 0, width: 5, height: -45}
+ });
+ hidden = yield testActor.getHighlighterNodeAttribute(
+ "highlighted-rect", "hidden", highlighter);
+ is(hidden, "true", "The highlighter is hidden when the rect is invalid (2)");
+
+ yield highlighter.show(body, {
+ rect: {x: "test", y: 0, width: 5, height: 5}
+ });
+ hidden = yield testActor.getHighlighterNodeAttribute(
+ "highlighted-rect", "hidden", highlighter);
+ is(hidden, "true", "The highlighter is hidden when the rect is invalid (3)");
+
+ info("Check that the highlighter is displayed when valid options are passed");
+
+ yield highlighter.show(body, {
+ rect: {x: 5, y: 5, width: 50, height: 50}
+ });
+ hidden = yield testActor.getHighlighterNodeAttribute(
+ "highlighted-rect", "hidden", highlighter);
+ ok(!hidden, "The highlighter is displayed");
+ let style = yield testActor.getHighlighterNodeAttribute(
+ "highlighted-rect", "style", highlighter);
+ is(style, "left:5px;top:5px;width:50px;height:50px;",
+ "The highlighter is positioned correctly");
+
+ info("Check that the highlighter can be displayed at x=0 y=0");
+
+ yield highlighter.show(body, {
+ rect: {x: 0, y: 0, width: 50, height: 50}
+ });
+ hidden = yield testActor.getHighlighterNodeAttribute(
+ "highlighted-rect", "hidden", highlighter);
+ ok(!hidden, "The highlighter is displayed when x=0 and y=0");
+ style = yield testActor.getHighlighterNodeAttribute(
+ "highlighted-rect", "style", highlighter);
+ is(style, "left:0px;top:0px;width:50px;height:50px;",
+ "The highlighter is positioned correctly");
+
+ info("Check that the highlighter is hidden when dimensions are 0");
+
+ yield highlighter.show(body, {
+ rect: {x: 0, y: 0, width: 0, height: 0}
+ });
+ hidden = yield testActor.getHighlighterNodeAttribute(
+ "highlighted-rect", "hidden", highlighter);
+ is(hidden, "true", "The highlighter is hidden width and height are 0");
+
+ info("Check that a fill color can be passed");
+
+ yield highlighter.show(body, {
+ rect: {x: 100, y: 200, width: 500, height: 200},
+ fill: "red"
+ });
+ hidden = yield testActor.getHighlighterNodeAttribute(
+ "highlighted-rect", "hidden", highlighter);
+ ok(!hidden, "The highlighter is displayed");
+ style = yield testActor.getHighlighterNodeAttribute(
+ "highlighted-rect", "style", highlighter);
+ is(style, "left:100px;top:200px;width:500px;height:200px;background:red;",
+ "The highlighter has the right background color");
+
+ yield highlighter.hide();
+ yield highlighter.finalize();
+});
diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-rect_02.js b/devtools/client/inspector/test/browser_inspector_highlighter-rect_02.js
new file mode 100644
index 000000000..716a5deda
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-rect_02.js
@@ -0,0 +1,37 @@
+/* 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";
+
+// Test that the custom rect highlighter positions the rectangle relative to the
+// viewport of the context node we pass to it.
+
+const TEST_URL = URL_ROOT + "doc_inspector_highlighter_rect.html";
+
+add_task(function* () {
+ let {inspector, testActor} = yield openInspectorForURL(TEST_URL);
+ let front = inspector.inspector;
+ let highlighter = yield front.getHighlighterByType("RectHighlighter");
+
+ info("Showing the rect highlighter in the context of the iframe");
+
+ // Get the reference to a context node inside the iframe
+ let childBody = yield getNodeFrontInFrame("body", "iframe", inspector);
+ yield highlighter.show(childBody, {
+ rect: {x: 50, y: 50, width: 100, height: 100}
+ });
+
+ let style = yield testActor.getHighlighterNodeAttribute("highlighted-rect",
+ "style", highlighter);
+
+ // The parent body has margin=50px and border=10px
+ // The parent iframe also has margin=50px and border=10px
+ // = 50 + 10 + 50 + 10 = 120px
+ // The rect is aat x=50 and y=50, so left and top should be 170px
+ is(style, "left:170px;top:170px;width:100px;height:100px;",
+ "The highlighter is correctly positioned");
+
+ yield highlighter.hide();
+ yield highlighter.finalize();
+});
diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-rulers_01.js b/devtools/client/inspector/test/browser_inspector_highlighter-rulers_01.js
new file mode 100644
index 000000000..f5deb0222
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-rulers_01.js
@@ -0,0 +1,76 @@
+/* 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";
+
+// Test the creation of the geometry highlighter elements.
+
+const TEST_URL = "data:text/html;charset=utf-8," +
+ "<div style='position:absolute;left: 0; top: 0; " +
+ "width: 40000px; height: 8000px'></div>";
+
+const ID = "rulers-highlighter-";
+
+// Maximum size, in pixel, for the horizontal ruler and vertical ruler
+// used by RulersHighlighter
+const RULERS_MAX_X_AXIS = 10000;
+const RULERS_MAX_Y_AXIS = 15000;
+// Number of steps after we add a text in RulersHighliter;
+// currently the unit is in pixel.
+const RULERS_TEXT_STEP = 100;
+
+add_task(function* () {
+ let { inspector, testActor } = yield openInspectorForURL(TEST_URL);
+ let front = inspector.inspector;
+
+ let highlighter = yield front.getHighlighterByType("RulersHighlighter");
+
+ yield isHiddenByDefault(highlighter, inspector, testActor);
+ yield hasRightLabelsContent(highlighter, inspector, testActor);
+
+ yield highlighter.finalize();
+});
+
+function* isHiddenByDefault(highlighterFront, inspector, testActor) {
+ info("Checking the highlighter is hidden by default");
+
+ let hidden = yield testActor.getHighlighterNodeAttribute(
+ ID + "elements", "hidden", highlighterFront);
+
+ is(hidden, "true", "highlighter is hidden by default");
+
+ info("Checking the highlighter is displayed when asked");
+ // the rulers doesn't need any node, but as highligher it seems mandatory
+ // ones, so the body is given
+ let body = yield getNodeFront("body", inspector);
+ yield highlighterFront.show(body);
+
+ hidden = yield testActor.getHighlighterNodeAttribute(
+ ID + "elements", "hidden", highlighterFront);
+
+ isnot(hidden, "true", "highlighter is visible after show");
+}
+
+function* hasRightLabelsContent(highlighterFront, inspector, testActor) {
+ info("Checking the rulers have the proper text, based on rulers' size");
+
+ let contentX = yield testActor.getHighlighterNodeTextContent(
+ `${ID}x-axis-text`, highlighterFront);
+ let contentY = yield testActor.getHighlighterNodeTextContent(
+ `${ID}y-axis-text`, highlighterFront);
+
+ let expectedX = "";
+ for (let i = RULERS_TEXT_STEP; i < RULERS_MAX_X_AXIS; i += RULERS_TEXT_STEP) {
+ expectedX += i;
+ }
+
+ is(contentX, expectedX, "x axis text content is correct");
+
+ let expectedY = "";
+ for (let i = RULERS_TEXT_STEP; i < RULERS_MAX_Y_AXIS; i += RULERS_TEXT_STEP) {
+ expectedY += i;
+ }
+
+ is(contentY, expectedY, "y axis text content is correct");
+}
diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-rulers_02.js b/devtools/client/inspector/test/browser_inspector_highlighter-rulers_02.js
new file mode 100644
index 000000000..fac1c801e
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-rulers_02.js
@@ -0,0 +1,103 @@
+/* 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";
+
+// Test the creation of the geometry highlighter elements.
+
+const TEST_URL = "data:text/html;charset=utf-8," +
+ "<div style='position:absolute;left: 0; top: 0; " +
+ "width: 40000px; height: 8000px'></div>";
+
+const ID = "rulers-highlighter-";
+
+add_task(function* () {
+ let { inspector, testActor } = yield openInspectorForURL(TEST_URL);
+ let front = inspector.inspector;
+
+ let highlighter = yield front.getHighlighterByType("RulersHighlighter");
+
+ // the rulers doesn't need any node, but as highligher it seems mandatory
+ // ones, so the body is given
+ let body = yield getNodeFront("body", inspector);
+ yield highlighter.show(body);
+
+ yield isUpdatedAfterScroll(highlighter, inspector, testActor);
+
+ yield highlighter.finalize();
+});
+
+function* isUpdatedAfterScroll(highlighterFront, inspector, testActor) {
+ info("Check the rulers' position by default");
+
+ let xAxisRulerTransform = yield testActor.getHighlighterNodeAttribute(
+ `${ID}x-axis-ruler`, "transform", highlighterFront);
+ let xAxisTextTransform = yield testActor.getHighlighterNodeAttribute(
+ `${ID}x-axis-text`, "transform", highlighterFront);
+ let yAxisRulerTransform = yield testActor.getHighlighterNodeAttribute(
+ `${ID}y-axis-ruler`, "transform", highlighterFront);
+ let yAxisTextTransform = yield testActor.getHighlighterNodeAttribute(
+ `${ID}y-axis-text`, "transform", highlighterFront);
+
+ is(xAxisRulerTransform, null, "x axis ruler is positioned properly");
+ is(xAxisTextTransform, null, "x axis text are positioned properly");
+ is(yAxisRulerTransform, null, "y axis ruler is positioned properly");
+ is(yAxisTextTransform, null, "y axis text are positioned properly");
+
+ info("Ask the content window to scroll to specific coords");
+
+ let x = 200, y = 300;
+
+ let data = yield testActor.scrollWindow(x, y);
+
+ is(data.x, x, "window scrolled properly horizontally");
+ is(data.y, y, "window scrolled properly vertically");
+
+ info("Check the rulers are properly positioned after the scrolling");
+
+ xAxisRulerTransform = yield testActor.getHighlighterNodeAttribute(
+ `${ID}x-axis-ruler`, "transform", highlighterFront);
+ xAxisTextTransform = yield testActor.getHighlighterNodeAttribute(
+ `${ID}x-axis-text`, "transform", highlighterFront);
+ yAxisRulerTransform = yield testActor.getHighlighterNodeAttribute(
+ `${ID}y-axis-ruler`, "transform", highlighterFront);
+ yAxisTextTransform = yield testActor.getHighlighterNodeAttribute(
+ `${ID}y-axis-text`, "transform", highlighterFront);
+
+ is(xAxisRulerTransform, `translate(-${x})`,
+ "x axis ruler is positioned properly");
+ is(xAxisTextTransform, `translate(-${x})`,
+ "x axis text are positioned properly");
+ is(yAxisRulerTransform, `translate(0, -${y})`,
+ "y axis ruler is positioned properly");
+ is(yAxisTextTransform, `translate(0, -${y})`,
+ "y axis text are positioned properly");
+
+ info("Ask the content window to scroll relative to the current position");
+
+ data = yield testActor.scrollWindow(-50, -60, true);
+
+ is(data.x, x - 50, "window scrolled properly horizontally");
+ is(data.y, y - 60, "window scrolled properly vertically");
+
+ info("Check the rulers are properly positioned after the relative scrolling");
+
+ xAxisRulerTransform = yield testActor.getHighlighterNodeAttribute(
+ `${ID}x-axis-ruler`, "transform", highlighterFront);
+ xAxisTextTransform = yield testActor.getHighlighterNodeAttribute(
+ `${ID}x-axis-text`, "transform", highlighterFront);
+ yAxisRulerTransform = yield testActor.getHighlighterNodeAttribute(
+ `${ID}y-axis-ruler`, "transform", highlighterFront);
+ yAxisTextTransform = yield testActor.getHighlighterNodeAttribute(
+ `${ID}y-axis-text`, "transform", highlighterFront);
+
+ is(xAxisRulerTransform, `translate(-${x - 50})`,
+ "x axis ruler is positioned properly");
+ is(xAxisTextTransform, `translate(-${x - 50})`,
+ "x axis text are positioned properly");
+ is(yAxisRulerTransform, `translate(0, -${y - 60})`,
+ "y axis ruler is positioned properly");
+ is(yAxisTextTransform, `translate(0, -${y - 60})`,
+ "y axis text are positioned properly");
+}
diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-selector_01.js b/devtools/client/inspector/test/browser_inspector_highlighter-selector_01.js
new file mode 100644
index 000000000..4edfe6051
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-selector_01.js
@@ -0,0 +1,63 @@
+/* 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";
+
+// Test that the custom selector highlighter creates as many box-model
+// highlighters as there are nodes that match the given selector
+
+const TEST_URL = "data:text/html;charset=utf-8," +
+ "<div id='test-node'>test node</div>" +
+ "<ul>" +
+ " <li class='item'>item</li>" +
+ " <li class='item'>item</li>" +
+ " <li class='item'>item</li>" +
+ " <li class='item'>item</li>" +
+ " <li class='item'>item</li>" +
+ "</ul>";
+
+const TEST_DATA = [{
+ selector: "#test-node",
+ containerCount: 1
+}, {
+ selector: null,
+ containerCount: 0,
+}, {
+ selector: undefined,
+ containerCount: 0,
+}, {
+ selector: ".invalid-class",
+ containerCount: 0
+}, {
+ selector: ".item",
+ containerCount: 5
+}, {
+ selector: "#test-node, ul, .item",
+ containerCount: 7
+}];
+
+requestLongerTimeout(5);
+
+add_task(function* () {
+ let {inspector, testActor} = yield openInspectorForURL(TEST_URL);
+ let front = inspector.inspector;
+ let highlighter = yield front.getHighlighterByType("SelectorHighlighter");
+
+ let contextNode = yield getNodeFront("body", inspector);
+
+ for (let {selector, containerCount} of TEST_DATA) {
+ info("Showing the highlighter on " + selector + ". Expecting " +
+ containerCount + " highlighter containers");
+
+ yield highlighter.show(contextNode, {selector});
+
+ let nb = yield testActor.getSelectorHighlighterBoxNb(highlighter.actorID);
+ ok(nb !== null, "The number of highlighters was retrieved");
+
+ is(nb, containerCount, "The correct number of highlighers were created");
+ yield highlighter.hide();
+ }
+
+ yield highlighter.finalize();
+});
diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-selector_02.js b/devtools/client/inspector/test/browser_inspector_highlighter-selector_02.js
new file mode 100644
index 000000000..85fcaeb1c
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-selector_02.js
@@ -0,0 +1,61 @@
+/* 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";
+
+// Test that the custom selector highlighter creates highlighters for nodes in
+// the right frame.
+
+const FRAME_SRC = "data:text/html;charset=utf-8," +
+ "<div class=sub-level-node></div>";
+
+const TEST_URL = "data:text/html;charset=utf-8," +
+ "<div class=root-level-node></div>" +
+ "<iframe src=\"" + FRAME_SRC + "\" />";
+
+const TEST_DATA = [{
+ selector: ".root-level-node",
+ containerCount: 1
+}, {
+ selector: ".sub-level-node",
+ containerCount: 0
+}, {
+ inIframe: true,
+ selector: ".root-level-node",
+ containerCount: 0
+}, {
+ inIframe: true,
+ selector: ".sub-level-node",
+ containerCount: 1
+}];
+
+requestLongerTimeout(5);
+
+add_task(function* () {
+ let {inspector, testActor} = yield openInspectorForURL(TEST_URL);
+ let front = inspector.inspector;
+ let highlighter = yield front.getHighlighterByType("SelectorHighlighter");
+
+ for (let {inIframe, selector, containerCount} of TEST_DATA) {
+ info("Showing the highlighter on " + selector + ". Expecting " +
+ containerCount + " highlighter containers");
+
+ let contextNode;
+ if (inIframe) {
+ contextNode = yield getNodeFrontInFrame("body", "iframe", inspector);
+ } else {
+ contextNode = yield getNodeFront("body", inspector);
+ }
+
+ yield highlighter.show(contextNode, {selector});
+
+ let nb = yield testActor.getSelectorHighlighterBoxNb(highlighter.actorID);
+ ok(nb !== null, "The number of highlighters was retrieved");
+
+ is(nb, containerCount, "The correct number of highlighers were created");
+ yield highlighter.hide();
+ }
+
+ yield highlighter.finalize();
+});
diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-xbl.js b/devtools/client/inspector/test/browser_inspector_highlighter-xbl.js
new file mode 100644
index 000000000..2421fd3f3
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-xbl.js
@@ -0,0 +1,39 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the picker works correctly with XBL anonymous nodes
+
+const TEST_URL = URL_ROOT + "doc_inspector_highlighter_xbl.xul";
+
+add_task(function* () {
+ let {inspector, toolbox, testActor} = yield openInspectorForURL(TEST_URL);
+
+ yield startPicker(toolbox);
+
+ info("Selecting the scale");
+ yield moveMouseOver("#scale");
+ yield doKeyPick({key: "VK_RETURN", options: {}});
+ is(inspector.selection.nodeFront.className, "scale-slider",
+ "The .scale-slider inside the scale was selected");
+
+ function doKeyPick(msg) {
+ info("Key pressed. Waiting for element to be picked");
+ testActor.synthesizeKey(msg);
+ return promise.all([
+ inspector.selection.once("new-node-front"),
+ inspector.once("inspector-updated")
+ ]);
+ }
+
+ function moveMouseOver(selector) {
+ info("Waiting for element " + selector + " to be highlighted");
+ testActor.synthesizeMouse({
+ options: {type: "mousemove"},
+ center: true,
+ selector: selector
+ });
+ return inspector.toolbox.once("picker-node-hovered");
+ }
+});
diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-zoom.js b/devtools/client/inspector/test/browser_inspector_highlighter-zoom.js
new file mode 100644
index 000000000..1919975ef
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-zoom.js
@@ -0,0 +1,72 @@
+/* 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";
+
+// Test that the highlighter stays correctly positioned and has the right aspect
+// ratio even when the page is zoomed in or out.
+
+const TEST_URL = "data:text/html;charset=utf-8,<div>zoom me</div>";
+
+// TEST_LEVELS entries should contain the following properties:
+// - level: the zoom level to test
+// - expected: the style attribute value to check for on the root highlighter
+// element.
+const TEST_LEVELS = [{
+ level: 2,
+ expected: "position:absolute;transform-origin:top left;" +
+ "transform:scale(0.5);width:200%;height:200%;"
+}, {
+ level: 1,
+ expected: "position:absolute;width:100%;height:100%;"
+}, {
+ level: .5,
+ expected: "position:absolute;transform-origin:top left;" +
+ "transform:scale(2);width:50%;height:50%;"
+}];
+
+add_task(function* () {
+ let {inspector, testActor} = yield openInspectorForURL(TEST_URL);
+
+ info("Highlighting the test node");
+
+ yield hoverElement("div", inspector);
+ let isVisible = yield testActor.isHighlighting();
+ ok(isVisible, "The highlighter is visible");
+
+ for (let {level, expected} of TEST_LEVELS) {
+ info("Zoom to level " + level +
+ " and check that the highlighter is correct");
+
+ yield testActor.zoomPageTo(level);
+ isVisible = yield testActor.isHighlighting();
+ ok(isVisible, "The highlighter is still visible at zoom level " + level);
+
+ yield testActor.isNodeCorrectlyHighlighted("div", is);
+
+ info("Check that the highlighter root wrapper node was scaled down");
+
+ let style = yield getRootNodeStyle(testActor);
+ is(style, expected, "The style attribute of the root element is correct");
+ }
+});
+
+function* hoverElement(selector, inspector) {
+ info("Hovering node " + selector + " in the markup view");
+ let container = yield getContainerForSelector(selector, inspector);
+ yield hoverContainer(container, inspector);
+}
+
+function* hoverContainer(container, inspector) {
+ let onHighlight = inspector.toolbox.once("node-highlight");
+ EventUtils.synthesizeMouse(container.tagLine, 2, 2, {type: "mousemove"},
+ inspector.markup.doc.defaultView);
+ yield onHighlight;
+}
+
+function* getRootNodeStyle(testActor) {
+ let value = yield testActor.getHighlighterNodeAttribute(
+ "box-model-root", "style");
+ return value;
+}
diff --git a/devtools/client/inspector/test/browser_inspector_iframe-navigation.js b/devtools/client/inspector/test/browser_inspector_iframe-navigation.js
new file mode 100644
index 000000000..df638b5cb
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_iframe-navigation.js
@@ -0,0 +1,43 @@
+/* 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 highlighter element picker still works through iframe
+// navigations.
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<p>bug 699308 - test iframe navigation</p>" +
+ "<iframe src='data:text/html;charset=utf-8,hello world'></iframe>";
+
+add_task(function* () {
+ let { toolbox, testActor } = yield openInspectorForURL(TEST_URI);
+
+ info("Starting element picker.");
+ yield startPicker(toolbox);
+
+ info("Waiting for highlighter to activate.");
+ let highlighterShowing = toolbox.once("highlighter-ready");
+ testActor.synthesizeMouse({
+ selector: "body",
+ options: {type: "mousemove"},
+ x: 1,
+ y: 1
+ });
+ yield highlighterShowing;
+
+ let isVisible = yield testActor.isHighlighting();
+ ok(isVisible, "Inspector is highlighting.");
+
+ yield testActor.reloadFrame("iframe");
+ info("Frame reloaded. Reloading again.");
+
+ yield testActor.reloadFrame("iframe");
+ info("Frame reloaded twice.");
+
+ isVisible = yield testActor.isHighlighting();
+ ok(isVisible, "Inspector is highlighting after iframe nav.");
+
+ info("Stopping element picker.");
+ yield toolbox.highlighterUtils.stopPicker();
+});
diff --git a/devtools/client/inspector/test/browser_inspector_infobar_01.js b/devtools/client/inspector/test/browser_inspector_infobar_01.js
new file mode 100644
index 000000000..441cd9e48
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_infobar_01.js
@@ -0,0 +1,89 @@
+/* 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";
+
+// Check the position and text content of the highlighter nodeinfo bar.
+
+const TEST_URI = URL_ROOT + "doc_inspector_infobar_01.html";
+
+add_task(function* () {
+ let {inspector, testActor} = yield openInspectorForURL(TEST_URI);
+
+ let testData = [
+ {
+ selector: "#top",
+ position: "bottom",
+ tag: "div",
+ id: "top",
+ classes: ".class1.class2",
+ dims: "500" + " \u00D7 " + "100"
+ },
+ {
+ selector: "#vertical",
+ position: "overlap",
+ tag: "div",
+ id: "vertical",
+ classes: ""
+ // No dims as they will vary between computers
+ },
+ {
+ selector: "#bottom",
+ position: "top",
+ tag: "div",
+ id: "bottom",
+ classes: "",
+ dims: "500" + " \u00D7 " + "100"
+ },
+ {
+ selector: "body",
+ position: "bottom",
+ tag: "body",
+ classes: ""
+ // No dims as they will vary between computers
+ },
+ {
+ selector: "clipPath",
+ position: "bottom",
+ tag: "clipPath",
+ id: "clip",
+ classes: ""
+ // No dims as element is not displayed and we just want to test tag name
+ },
+ ];
+
+ for (let currTest of testData) {
+ yield testPosition(currTest, inspector, testActor);
+ }
+});
+
+function* testPosition(test, inspector, testActor) {
+ info("Testing " + test.selector);
+
+ yield selectAndHighlightNode(test.selector, inspector);
+
+ let position = yield testActor.getHighlighterNodeAttribute(
+ "box-model-infobar-container", "position");
+ is(position, test.position, "Node " + test.selector + ": position matches");
+
+ let tag = yield testActor.getHighlighterNodeTextContent(
+ "box-model-infobar-tagname");
+ is(tag, test.tag, "node " + test.selector + ": tagName matches.");
+
+ if (test.id) {
+ let id = yield testActor.getHighlighterNodeTextContent(
+ "box-model-infobar-id");
+ is(id, "#" + test.id, "node " + test.selector + ": id matches.");
+ }
+
+ let classes = yield testActor.getHighlighterNodeTextContent(
+ "box-model-infobar-classes");
+ is(classes, test.classes, "node " + test.selector + ": classes match.");
+
+ if (test.dims) {
+ let dims = yield testActor.getHighlighterNodeTextContent(
+ "box-model-infobar-dimensions");
+ is(dims, test.dims, "node " + test.selector + ": dims match.");
+ }
+}
diff --git a/devtools/client/inspector/test/browser_inspector_infobar_02.js b/devtools/client/inspector/test/browser_inspector_infobar_02.js
new file mode 100644
index 000000000..1b5bd5edf
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_infobar_02.js
@@ -0,0 +1,50 @@
+/* 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";
+
+// Check the text content of the highlighter info bar for namespaced elements.
+
+const XHTML = `
+ <!DOCTYPE html>
+ <html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:svg="http://www.w3.org/2000/svg">
+ <body>
+ <svg:svg width="100" height="100">
+ <svg:circle cx="0" cy="0" r="5"></svg:circle>
+ </svg:svg>
+ </body>
+ </html>
+`;
+
+const TEST_URI = "data:application/xhtml+xml;charset=utf-8," + encodeURI(XHTML);
+
+add_task(function* () {
+ let {inspector, testActor} = yield openInspectorForURL(TEST_URI);
+
+ let testData = [
+ {
+ selector: "svg",
+ tag: "svg:svg"
+ },
+ {
+ selector: "circle",
+ tag: "svg:circle"
+ },
+ ];
+
+ for (let currTest of testData) {
+ yield testNode(currTest, inspector, testActor);
+ }
+});
+
+function* testNode(test, inspector, testActor) {
+ info("Testing " + test.selector);
+
+ yield selectAndHighlightNode(test.selector, inspector);
+
+ let tag = yield testActor.getHighlighterNodeTextContent(
+ "box-model-infobar-tagname");
+ is(tag, test.tag, "node " + test.selector + ": tagName matches.");
+}
diff --git a/devtools/client/inspector/test/browser_inspector_infobar_03.js b/devtools/client/inspector/test/browser_inspector_infobar_03.js
new file mode 100644
index 000000000..023d5bb38
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_infobar_03.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";
+
+// Bug 1102269 - Make sure info-bar never gets outside of visible area after scrolling
+
+const TEST_URI = URL_ROOT + "doc_inspector_infobar_03.html";
+
+add_task(function* () {
+ let {inspector, testActor} = yield openInspectorForURL(TEST_URI);
+
+ let testData = {
+ selector: "body",
+ position: "overlap",
+ style: "top:0px",
+ };
+
+ yield testPositionAndStyle(testData, inspector, testActor);
+});
+
+function* testPositionAndStyle(test, inspector, testActor) {
+ info("Testing " + test.selector);
+
+ yield selectAndHighlightNode(test.selector, inspector);
+
+ let style = yield testActor.getHighlighterNodeAttribute(
+ "box-model-infobar-container", "style");
+
+ is(style.split(";")[0], test.style,
+ "Infobar shows on top of the page when page isn't scrolled");
+
+ yield testActor.scrollWindow(0, 500);
+
+ style = yield testActor.getHighlighterNodeAttribute(
+ "box-model-infobar-container", "style");
+
+ is(style.split(";")[0], test.style,
+ "Infobar shows on top of the page even if the page is scrolled");
+}
diff --git a/devtools/client/inspector/test/browser_inspector_infobar_textnode.js b/devtools/client/inspector/test/browser_inspector_infobar_textnode.js
new file mode 100644
index 000000000..c9315eb9b
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_infobar_textnode.js
@@ -0,0 +1,46 @@
+/* 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";
+
+// Bug 1309212 - Make sure info-bar is displayed with dimensions for text nodes.
+
+const TEST_URI = URL_ROOT + "doc_inspector_infobar_textnode.html";
+
+add_task(function* () {
+ let { inspector, testActor } = yield openInspectorForURL(TEST_URI);
+ let { walker } = inspector;
+
+ info("Retrieve the children of #textnode-container");
+ let div = yield walker.querySelector(walker.rootNode, "#textnode-container");
+ let { nodes } = yield inspector.walker.children(div);
+
+ // Children 0, 2 and 4 are text nodes, for which we expect to see an infobar containing
+ // dimensions.
+
+ // Regular text node.
+ info("Select the first text node");
+ yield selectNode(nodes[0], inspector, "test-highlight");
+ yield checkTextNodeInfoBar(testActor);
+
+ // Whitespace-only text node.
+ info("Select the second text node");
+ yield selectNode(nodes[2], inspector, "test-highlight");
+ yield checkTextNodeInfoBar(testActor);
+
+ // Regular text node.
+ info("Select the third text node");
+ yield selectNode(nodes[4], inspector, "test-highlight");
+ yield checkTextNodeInfoBar(testActor);
+});
+
+function* checkTextNodeInfoBar(testActor) {
+ let tag = yield testActor.getHighlighterNodeTextContent(
+ "box-model-infobar-tagname");
+ is(tag, "#text", "node display name is #text");
+ let dims = yield testActor.getHighlighterNodeTextContent(
+ "box-model-infobar-dimensions");
+ // Do not assert dimensions as they might be platform specific.
+ ok(!!dims, "node has dims");
+}
diff --git a/devtools/client/inspector/test/browser_inspector_initialization.js b/devtools/client/inspector/test/browser_inspector_initialization.js
new file mode 100644
index 000000000..55db060f3
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_initialization.js
@@ -0,0 +1,112 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set 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/. */
+/* globals getTestActorWithoutToolbox */
+"use strict";
+
+// Tests for different ways to initialize the inspector.
+
+const HTML = `
+ <div id="first" style="margin: 10em; font-size: 14pt;
+ font-family: helvetica, sans-serif; color: gray">
+ <h1>Some header text</h1>
+ <p id="salutation" style="font-size: 12pt">hi.</p>
+ <p id="body" style="font-size: 12pt">I am a test-case. This text exists
+ solely to provide some things to test the inspector initialization.</p>
+ <p>If you are reading this, you should go do something else instead. Maybe
+ read a book. Or better yet, write some test-cases for another bit of code.
+ <span style="font-style: italic">Inspector's!</span>
+ </p>
+ <p id="closing">end transmission</p>
+ </div>
+`;
+
+const TEST_URI = "data:text/html;charset=utf-8," + encodeURI(HTML);
+
+add_task(function* () {
+ let tab = yield addTab(TEST_URI);
+ let testActor = yield getTestActorWithoutToolbox(tab);
+
+ yield testToolboxInitialization(testActor, tab);
+ yield testContextMenuInitialization(testActor);
+ yield testContextMenuInspectorAlreadyOpen(testActor);
+});
+
+function* testToolboxInitialization(testActor, tab) {
+ let target = TargetFactory.forTab(tab);
+
+ info("Opening inspector with gDevTools.");
+ let toolbox = yield gDevTools.showToolbox(target, "inspector");
+ let inspector = toolbox.getCurrentPanel();
+
+ ok(true, "Inspector started, and notification received.");
+ ok(inspector, "Inspector instance is accessible.");
+ ok(inspector.isReady, "Inspector instance is ready.");
+ is(inspector.target.tab, tab, "Valid target.");
+
+ yield selectNode("p", inspector);
+ yield testMarkupView("p", inspector);
+ yield testBreadcrumbs("p", inspector);
+
+ yield testActor.scrollIntoView("span");
+
+ yield selectNode("span", inspector);
+ yield testMarkupView("span", inspector);
+ yield testBreadcrumbs("span", inspector);
+
+ info("Destroying toolbox");
+ let destroyed = toolbox.once("destroyed");
+ toolbox.destroy();
+ yield destroyed;
+
+ ok("true", "'destroyed' notification received.");
+ ok(!gDevTools.getToolbox(target), "Toolbox destroyed.");
+}
+
+function* testContextMenuInitialization(testActor) {
+ info("Opening inspector by clicking on 'Inspect Element' context menu item");
+ yield clickOnInspectMenuItem(testActor, "#salutation");
+
+ info("Checking inspector state.");
+ yield testMarkupView("#salutation");
+ yield testBreadcrumbs("#salutation");
+}
+
+function* testContextMenuInspectorAlreadyOpen(testActor) {
+ info("Changing node by clicking on 'Inspect Element' context menu item");
+
+ let inspector = getActiveInspector();
+ ok(inspector, "Inspector is active");
+
+ yield clickOnInspectMenuItem(testActor, "#closing");
+
+ ok(true, "Inspector was updated when 'Inspect Element' was clicked.");
+ yield testMarkupView("#closing", inspector);
+ yield testBreadcrumbs("#closing", inspector);
+}
+
+function* testMarkupView(selector, inspector) {
+ inspector = inspector || getActiveInspector();
+ let nodeFront = yield getNodeFront(selector, inspector);
+ try {
+ is(inspector.selection.nodeFront, nodeFront,
+ "Right node is selected in the markup view");
+ } catch (ex) {
+ ok(false, "Got exception while resolving selected node of markup view.");
+ console.error(ex);
+ }
+}
+
+function* testBreadcrumbs(selector, inspector) {
+ inspector = inspector || getActiveInspector();
+ let nodeFront = yield getNodeFront(selector, inspector);
+
+ let b = inspector.breadcrumbs;
+ let expectedText = b.prettyPrintNodeAsText(nodeFront);
+ let button = b.container.querySelector("button[checked=true]");
+ ok(button, "A crumbs is checked=true");
+ is(button.getAttribute("title"), expectedText,
+ "Crumb refers to the right node");
+}
diff --git a/devtools/client/inspector/test/browser_inspector_inspect-object-element.js b/devtools/client/inspector/test/browser_inspector_inspect-object-element.js
new file mode 100644
index 000000000..ca646c506
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_inspect-object-element.js
@@ -0,0 +1,18 @@
+/* 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";
+
+// A regression test for bug 665880 to make sure elements inside <object> can
+// be inspected without exceptions.
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<object><p>browser_inspector_inspect-object-element.js</p></object>";
+
+add_task(function* () {
+ let { inspector } = yield openInspectorForURL(TEST_URI);
+
+ yield selectNode("object", inspector);
+
+ ok(true, "Selected <object> without throwing");
+});
diff --git a/devtools/client/inspector/test/browser_inspector_invalidate.js b/devtools/client/inspector/test/browser_inspector_invalidate.js
new file mode 100644
index 000000000..040bd1c1c
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_invalidate.js
@@ -0,0 +1,35 @@
+/* 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";
+
+// Test that highlighter handles geometry changes correctly.
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "browser_inspector_invalidate.js\n" +
+ "<div style=\"width: 100px; height: 100px; background:yellow;\"></div>";
+
+add_task(function* () {
+ let {inspector, testActor} = yield openInspectorForURL(TEST_URI);
+ let divFront = yield getNodeFront("div", inspector);
+
+ info("Waiting for highlighter to activate");
+ yield inspector.highlighter.showBoxModel(divFront);
+
+ let rect = yield testActor.getSimpleBorderRect();
+ is(rect.width, 100, "The highlighter has the right width.");
+
+ info("Changing the test element's size and waiting for the highlighter " +
+ "to update");
+ yield testActor.changeHighlightedNodeWaitForUpdate(
+ "style",
+ "width: 200px; height: 100px; background:yellow;"
+ );
+
+ rect = yield testActor.getSimpleBorderRect();
+ is(rect.width, 200, "The highlighter has the right width after update");
+
+ info("Waiting for highlighter to hide");
+ yield inspector.highlighter.hideBoxModel();
+});
diff --git a/devtools/client/inspector/test/browser_inspector_keyboard-shortcuts-copy-outerhtml.js b/devtools/client/inspector/test/browser_inspector_keyboard-shortcuts-copy-outerhtml.js
new file mode 100644
index 000000000..46b0ce5f5
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_keyboard-shortcuts-copy-outerhtml.js
@@ -0,0 +1,52 @@
+/* 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";
+
+// Test copy outer HTML from the keyboard/copy event
+
+const TEST_URL = URL_ROOT + "doc_inspector_outerhtml.html";
+
+add_task(function* () {
+ let { inspector } = yield openInspectorForURL(TEST_URL);
+ let root = inspector.markup._elt;
+
+ info("Test copy outerHTML for COMMENT node");
+ let comment = getElementByType(inspector, Ci.nsIDOMNode.COMMENT_NODE);
+ yield setSelectionNodeFront(comment, inspector);
+ yield checkClipboard("<!-- Comment -->", root);
+
+ info("Test copy outerHTML for DOCTYPE node");
+ let doctype = getElementByType(inspector, Ci.nsIDOMNode.DOCUMENT_TYPE_NODE);
+ yield setSelectionNodeFront(doctype, inspector);
+ yield checkClipboard("<!DOCTYPE html>", root);
+
+ info("Test copy outerHTML for ELEMENT node");
+ yield selectAndHighlightNode("div", inspector);
+ yield checkClipboard("<div><p>Test copy OuterHTML</p></div>", root);
+});
+
+function* setSelectionNodeFront(node, inspector) {
+ let updated = inspector.once("inspector-updated");
+ inspector.selection.setNodeFront(node);
+ yield updated;
+}
+
+function* checkClipboard(expectedText, node) {
+ try {
+ yield waitForClipboardPromise(() => fireCopyEvent(node), expectedText);
+ ok(true, "Clipboard successfully filled with : " + expectedText);
+ } catch (e) {
+ ok(false, "Clipboard could not be filled with the expected text : " +
+ expectedText);
+ }
+}
+
+function getElementByType(inspector, type) {
+ for (let [node] of inspector.markup._containers) {
+ if (node.nodeType === type) {
+ return node;
+ }
+ }
+ return null;
+}
diff --git a/devtools/client/inspector/test/browser_inspector_keyboard-shortcuts.js b/devtools/client/inspector/test/browser_inspector_keyboard-shortcuts.js
new file mode 100644
index 000000000..f03a33fd1
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_keyboard-shortcuts.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";
+
+// Tests that the keybindings for highlighting different elements work as
+// intended.
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<html><head><title>Test for the highlighter keybindings</title></head>" +
+ "<body><p><strong>Greetings, earthlings!</strong>" +
+ " I come in peace.</p></body></html>";
+
+const TEST_DATA = [
+ { key: "VK_LEFT", selectedNode: "p" },
+ { key: "VK_LEFT", selectedNode: "body" },
+ { key: "VK_LEFT", selectedNode: "html" },
+ { key: "VK_RIGHT", selectedNode: "body" },
+ { key: "VK_RIGHT", selectedNode: "p" },
+ { key: "VK_RIGHT", selectedNode: "strong" },
+];
+
+add_task(function* () {
+ let { inspector } = yield openInspectorForURL(TEST_URI);
+
+ info("Selecting the deepest element to start with");
+ yield selectNode("strong", inspector);
+
+ let nodeFront = yield getNodeFront("strong", inspector);
+ is(inspector.selection.nodeFront, nodeFront,
+ "<strong> should be selected initially");
+
+ info("Focusing the currently active breadcrumb button");
+ let bc = inspector.breadcrumbs;
+ bc.nodeHierarchy[bc.currentIndex].button.focus();
+
+ for (let { key, selectedNode } of TEST_DATA) {
+ info("Pressing " + key + " to select " + selectedNode);
+
+ let updated = inspector.once("inspector-updated");
+ EventUtils.synthesizeKey(key, {});
+ yield updated;
+
+ let selectedNodeFront = yield getNodeFront(selectedNode, inspector);
+ is(inspector.selection.nodeFront, selectedNodeFront,
+ selectedNode + " is selected.");
+ }
+});
diff --git a/devtools/client/inspector/test/browser_inspector_menu-01-sensitivity.js b/devtools/client/inspector/test/browser_inspector_menu-01-sensitivity.js
new file mode 100644
index 000000000..59dbbbcc0
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_menu-01-sensitivity.js
@@ -0,0 +1,278 @@
+/* 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";
+
+// Test that context menu items are enabled / disabled correctly.
+
+const TEST_URL = URL_ROOT + "doc_inspector_menu.html";
+
+const PASTE_MENU_ITEMS = [
+ "node-menu-pasteinnerhtml",
+ "node-menu-pasteouterhtml",
+ "node-menu-pastebefore",
+ "node-menu-pasteafter",
+ "node-menu-pastefirstchild",
+ "node-menu-pastelastchild",
+];
+
+const ACTIVE_ON_DOCTYPE_ITEMS = [
+ "node-menu-showdomproperties",
+ "node-menu-useinconsole"
+];
+
+const ALL_MENU_ITEMS = [
+ "node-menu-edithtml",
+ "node-menu-copyinner",
+ "node-menu-copyouter",
+ "node-menu-copyuniqueselector",
+ "node-menu-copyimagedatauri",
+ "node-menu-delete",
+ "node-menu-pseudo-hover",
+ "node-menu-pseudo-active",
+ "node-menu-pseudo-focus",
+ "node-menu-scrollnodeintoview",
+ "node-menu-screenshotnode",
+ "node-menu-add-attribute",
+ "node-menu-edit-attribute",
+ "node-menu-remove-attribute"
+].concat(PASTE_MENU_ITEMS, ACTIVE_ON_DOCTYPE_ITEMS);
+
+const INACTIVE_ON_DOCTYPE_ITEMS =
+ ALL_MENU_ITEMS.filter(item => ACTIVE_ON_DOCTYPE_ITEMS.indexOf(item) === -1);
+
+/**
+ * Test cases, each item of this array may define the following properties:
+ * desc: string that will be logged
+ * selector: selector of the node to be selected
+ * disabled: items that should have disabled state
+ * clipboardData: clipboard content
+ * clipboardDataType: clipboard content type
+ * attributeTrigger: attribute that will be used as context menu trigger
+ */
+const TEST_CASES = [
+ {
+ desc: "doctype node with empty clipboard",
+ selector: null,
+ disabled: INACTIVE_ON_DOCTYPE_ITEMS,
+ },
+ {
+ desc: "doctype node with html on clipboard",
+ clipboardData: "<p>some text</p>",
+ clipboardDataType: "html",
+ selector: null,
+ disabled: INACTIVE_ON_DOCTYPE_ITEMS,
+ },
+ {
+ desc: "element node HTML on the clipboard",
+ clipboardData: "<p>some text</p>",
+ clipboardDataType: "html",
+ disabled: [
+ "node-menu-copyimagedatauri",
+ "node-menu-edit-attribute",
+ "node-menu-remove-attribute"
+ ],
+ selector: "#sensitivity",
+ },
+ {
+ desc: "<html> element",
+ clipboardData: "<p>some text</p>",
+ clipboardDataType: "html",
+ selector: "html",
+ disabled: [
+ "node-menu-copyimagedatauri",
+ "node-menu-pastebefore",
+ "node-menu-pasteafter",
+ "node-menu-pastefirstchild",
+ "node-menu-pastelastchild",
+ "node-menu-edit-attribute",
+ "node-menu-remove-attribute"
+ ],
+ },
+ {
+ desc: "<body> with HTML on clipboard",
+ clipboardData: "<p>some text</p>",
+ clipboardDataType: "html",
+ selector: "body",
+ disabled: [
+ "node-menu-copyimagedatauri",
+ "node-menu-pastebefore",
+ "node-menu-pasteafter",
+ "node-menu-edit-attribute",
+ "node-menu-remove-attribute"
+ ]
+ },
+ {
+ desc: "<img> with HTML on clipboard",
+ clipboardData: "<p>some text</p>",
+ clipboardDataType: "html",
+ selector: "img",
+ disabled: [
+ "node-menu-edit-attribute",
+ "node-menu-remove-attribute"
+ ]
+ },
+ {
+ desc: "<head> with HTML on clipboard",
+ clipboardData: "<p>some text</p>",
+ clipboardDataType: "html",
+ selector: "head",
+ disabled: [
+ "node-menu-copyimagedatauri",
+ "node-menu-pastebefore",
+ "node-menu-pasteafter",
+ "node-menu-screenshotnode",
+ "node-menu-edit-attribute",
+ "node-menu-remove-attribute"
+ ],
+ },
+ {
+ desc: "<head> with no html on clipboard",
+ selector: "head",
+ disabled: PASTE_MENU_ITEMS.concat([
+ "node-menu-copyimagedatauri",
+ "node-menu-screenshotnode",
+ "node-menu-edit-attribute",
+ "node-menu-remove-attribute"
+ ]),
+ },
+ {
+ desc: "<element> with text on clipboard",
+ clipboardData: "some text",
+ clipboardDataType: undefined,
+ selector: "#paste-area",
+ disabled: [
+ "node-menu-copyimagedatauri",
+ "node-menu-edit-attribute",
+ "node-menu-remove-attribute"
+ ]
+ },
+ {
+ desc: "<element> with base64 encoded image data uri on clipboard",
+ clipboardData:
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABC" +
+ "AAAAAA6fptVAAAACklEQVQYV2P4DwABAQEAWk1v8QAAAABJRU5ErkJggg==",
+ clipboardDataType: undefined,
+ selector: "#paste-area",
+ disabled: PASTE_MENU_ITEMS.concat([
+ "node-menu-copyimagedatauri",
+ "node-menu-edit-attribute",
+ "node-menu-remove-attribute"
+ ]),
+ },
+ {
+ desc: "<element> with empty string on clipboard",
+ clipboardData: "",
+ clipboardDataType: undefined,
+ selector: "#paste-area",
+ disabled: PASTE_MENU_ITEMS.concat([
+ "node-menu-copyimagedatauri",
+ "node-menu-edit-attribute",
+ "node-menu-remove-attribute"
+ ]),
+ },
+ {
+ desc: "<element> with whitespace only on clipboard",
+ clipboardData: " \n\n\t\n\n \n",
+ clipboardDataType: undefined,
+ selector: "#paste-area",
+ disabled: PASTE_MENU_ITEMS.concat([
+ "node-menu-copyimagedatauri",
+ "node-menu-edit-attribute",
+ "node-menu-remove-attribute"
+ ]),
+ },
+ {
+ desc: "<element> that isn't visible on the page, empty clipboard",
+ selector: "#hiddenElement",
+ disabled: PASTE_MENU_ITEMS.concat([
+ "node-menu-copyimagedatauri",
+ "node-menu-screenshotnode",
+ "node-menu-edit-attribute",
+ "node-menu-remove-attribute"
+ ]),
+ },
+ {
+ desc: "<element> nested in another hidden element, empty clipboard",
+ selector: "#nestedHiddenElement",
+ disabled: PASTE_MENU_ITEMS.concat([
+ "node-menu-copyimagedatauri",
+ "node-menu-screenshotnode",
+ "node-menu-edit-attribute",
+ "node-menu-remove-attribute"
+ ]),
+ },
+ {
+ desc: "<element> with context menu triggered on attribute, empty clipboard",
+ selector: "#attributes",
+ disabled: PASTE_MENU_ITEMS.concat(["node-menu-copyimagedatauri"]),
+ attributeTrigger: "data-edit"
+ }
+];
+
+var clipboard = require("sdk/clipboard");
+registerCleanupFunction(() => {
+ clipboard = null;
+});
+
+add_task(function* () {
+ let { inspector } = yield openInspectorForURL(TEST_URL);
+ for (let test of TEST_CASES) {
+ let { desc, disabled, selector, attributeTrigger } = test;
+
+ info(`Test ${desc}`);
+ setupClipboard(test.clipboardData, test.clipboardDataType);
+
+ let front = yield getNodeFrontForSelector(selector, inspector);
+
+ info("Selecting the specified node.");
+ yield selectNode(front, inspector);
+
+ info("Simulating context menu click on the selected node container.");
+ let nodeFrontContainer = getContainerForNodeFront(front, inspector);
+ let contextMenuTrigger = attributeTrigger
+ ? nodeFrontContainer.tagLine.querySelector(
+ `[data-attr="${attributeTrigger}"]`)
+ : nodeFrontContainer.tagLine;
+
+ let allMenuItems = openContextMenuAndGetAllItems(inspector, {
+ target: contextMenuTrigger,
+ });
+
+ for (let id of ALL_MENU_ITEMS) {
+ let menuItem = allMenuItems.find(item => item.id === id);
+ let shouldBeDisabled = disabled.indexOf(id) !== -1;
+ is(menuItem.disabled, shouldBeDisabled,
+ `#${id} should be ${shouldBeDisabled ? "disabled" : "enabled"} `);
+ }
+ }
+});
+
+/**
+ * A helper that fetches a front for a node that matches the given selector or
+ * doctype node if the selector is falsy.
+ */
+function* getNodeFrontForSelector(selector, inspector) {
+ if (selector) {
+ info("Retrieving front for selector " + selector);
+ return getNodeFront(selector, inspector);
+ }
+
+ info("Retrieving front for doctype node");
+ let {nodes} = yield inspector.walker.children(inspector.walker.rootNode);
+ return nodes[0];
+}
+
+/**
+ * A helper that populates the clipboard with data of given type. Clears the
+ * clipboard if data is falsy.
+ */
+function setupClipboard(data, type) {
+ if (data) {
+ info("Populating clipboard with " + type + " data.");
+ clipboard.set(data, type);
+ } else {
+ info("Clearing clipboard.");
+ clipboard.set("", "text");
+ }
+}
diff --git a/devtools/client/inspector/test/browser_inspector_menu-02-copy-items.js b/devtools/client/inspector/test/browser_inspector_menu-02-copy-items.js
new file mode 100644
index 000000000..0c96e9bbe
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_menu-02-copy-items.js
@@ -0,0 +1,49 @@
+/* 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";
+
+// Test that the various copy items in the context menu works correctly.
+
+const TEST_URL = URL_ROOT + "doc_inspector_menu.html";
+const COPY_ITEMS_TEST_DATA = [
+ {
+ desc: "copy inner html",
+ id: "node-menu-copyinner",
+ selector: "[data-id=\"copy\"]",
+ text: "Paragraph for testing copy",
+ },
+ {
+ desc: "copy outer html",
+ id: "node-menu-copyouter",
+ selector: "[data-id=\"copy\"]",
+ text: "<p data-id=\"copy\">Paragraph for testing copy</p>",
+ },
+ {
+ desc: "copy unique selector",
+ id: "node-menu-copyuniqueselector",
+ selector: "[data-id=\"copy\"]",
+ text: "body > div:nth-child(1) > p:nth-child(2)",
+ },
+ {
+ desc: "copy image data uri",
+ id: "node-menu-copyimagedatauri",
+ selector: "#copyimage",
+ text: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABC" +
+ "AAAAAA6fptVAAAACklEQVQYV2P4DwABAQEAWk1v8QAAAABJRU5ErkJggg==",
+ },
+];
+
+add_task(function* () {
+ let { inspector } = yield openInspectorForURL(TEST_URL);
+ for (let {desc, id, selector, text} of COPY_ITEMS_TEST_DATA) {
+ info("Testing " + desc);
+ yield selectNode(selector, inspector);
+
+ let allMenuItems = openContextMenuAndGetAllItems(inspector);
+ let item = allMenuItems.find(i => i.id === id);
+ ok(item, "The popup has a " + desc + " menu item.");
+
+ yield waitForClipboardPromise(() => item.click(), text);
+ }
+});
diff --git a/devtools/client/inspector/test/browser_inspector_menu-03-paste-items-svg.js b/devtools/client/inspector/test/browser_inspector_menu-03-paste-items-svg.js
new file mode 100644
index 000000000..26ae3ff00
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_menu-03-paste-items-svg.js
@@ -0,0 +1,42 @@
+/* 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";
+
+// Test that HTML can be pasted in SVG elements.
+
+const TEST_URL = URL_ROOT + "doc_inspector_svg.svg";
+const PASTE_AS_FIRST_CHILD = '<circle xmlns="http://www.w3.org/2000/svg" cx="42" cy="42" r="5"/>';
+const PASTE_AS_LAST_CHILD = '<circle xmlns="http://www.w3.org/2000/svg" cx="42" cy="42" r="15"/>';
+
+add_task(function* () {
+ let clipboard = require("sdk/clipboard");
+
+ let { inspector, testActor } = yield openInspectorForURL(TEST_URL);
+
+ let refSelector = "svg";
+ let oldHTML = yield testActor.getProperty(refSelector, "innerHTML");
+ yield selectNode(refSelector, inspector);
+ let markupTagLine = getContainerForSelector(refSelector, inspector).tagLine;
+
+ yield pasteContent("node-menu-pastefirstchild", PASTE_AS_FIRST_CHILD);
+ yield pasteContent("node-menu-pastelastchild", PASTE_AS_LAST_CHILD);
+
+ let html = yield testActor.getProperty(refSelector, "innerHTML");
+ let expectedHtml = PASTE_AS_FIRST_CHILD + oldHTML + PASTE_AS_LAST_CHILD;
+ is(html, expectedHtml, "The innerHTML of the SVG node is correct");
+
+ // Helpers
+ function* pasteContent(menuId, clipboardData) {
+ let allMenuItems = openContextMenuAndGetAllItems(inspector, {
+ target: markupTagLine,
+ });
+ info(`Testing ${menuId} for ${clipboardData}`);
+ clipboard.set(clipboardData);
+
+ let onMutation = inspector.once("markupmutation");
+ allMenuItems.find(item => item.id === menuId).click();
+ info("Waiting for mutation to occur");
+ yield onMutation;
+ }
+});
diff --git a/devtools/client/inspector/test/browser_inspector_menu-03-paste-items.js b/devtools/client/inspector/test/browser_inspector_menu-03-paste-items.js
new file mode 100644
index 000000000..19e5742de
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_menu-03-paste-items.js
@@ -0,0 +1,128 @@
+/* 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";
+
+// Test that different paste items work in the context menu
+
+const TEST_URL = URL_ROOT + "doc_inspector_menu.html";
+const PASTE_ADJACENT_HTML_DATA = [
+ {
+ desc: "As First Child",
+ clipboardData: "2",
+ menuId: "node-menu-pastefirstchild",
+ },
+ {
+ desc: "As Last Child",
+ clipboardData: "4",
+ menuId: "node-menu-pastelastchild",
+ },
+ {
+ desc: "Before",
+ clipboardData: "1",
+ menuId: "node-menu-pastebefore",
+ },
+ {
+ desc: "After",
+ clipboardData: "<span>5</span>",
+ menuId: "node-menu-pasteafter",
+ },
+];
+
+var clipboard = require("sdk/clipboard");
+registerCleanupFunction(() => {
+ clipboard = null;
+});
+
+add_task(function* () {
+ let { inspector, testActor } = yield openInspectorForURL(TEST_URL);
+
+ yield testPasteOuterHTMLMenu();
+ yield testPasteInnerHTMLMenu();
+ yield testPasteAdjacentHTMLMenu();
+
+ function* testPasteOuterHTMLMenu() {
+ info("Testing that 'Paste Outer HTML' menu item works.");
+ clipboard.set("this was pasted (outerHTML)");
+ let outerHTMLSelector = "#paste-area h1";
+
+ let nodeFront = yield getNodeFront(outerHTMLSelector, inspector);
+ yield selectNode(nodeFront, inspector);
+
+ let allMenuItems = openContextMenuAndGetAllItems(inspector, {
+ target: getContainerForNodeFront(nodeFront, inspector).tagLine,
+ });
+
+ let onNodeReselected = inspector.markup.once("reselectedonremoved");
+ allMenuItems.find(item => item.id === "node-menu-pasteouterhtml").click();
+
+ info("Waiting for inspector selection to update");
+ yield onNodeReselected;
+
+ let outerHTML = yield testActor.getProperty("body", "outerHTML");
+ ok(outerHTML.includes(clipboard.get()),
+ "Clipboard content was pasted into the node's outer HTML.");
+ ok(!(yield testActor.hasNode(outerHTMLSelector)),
+ "The original node was removed.");
+ }
+
+ function* testPasteInnerHTMLMenu() {
+ info("Testing that 'Paste Inner HTML' menu item works.");
+ clipboard.set("this was pasted (innerHTML)");
+ let innerHTMLSelector = "#paste-area .inner";
+ let getInnerHTML = () => testActor.getProperty(innerHTMLSelector,
+ "innerHTML");
+ let origInnerHTML = yield getInnerHTML();
+
+ let nodeFront = yield getNodeFront(innerHTMLSelector, inspector);
+ yield selectNode(nodeFront, inspector);
+
+ let allMenuItems = openContextMenuAndGetAllItems(inspector, {
+ target: getContainerForNodeFront(nodeFront, inspector).tagLine,
+ });
+
+ let onMutation = inspector.once("markupmutation");
+ allMenuItems.find(item => item.id === "node-menu-pasteinnerhtml").click();
+ info("Waiting for mutation to occur");
+ yield onMutation;
+
+ ok((yield getInnerHTML()) === clipboard.get(),
+ "Clipboard content was pasted into the node's inner HTML.");
+ ok((yield testActor.hasNode(innerHTMLSelector)),
+ "The original node has been preserved.");
+ yield undoChange(inspector);
+ ok((yield getInnerHTML()) === origInnerHTML,
+ "Previous innerHTML has been restored after undo");
+ }
+
+ function* testPasteAdjacentHTMLMenu() {
+ let refSelector = "#paste-area .adjacent .ref";
+ let adjacentNodeSelector = "#paste-area .adjacent";
+ let nodeFront = yield getNodeFront(refSelector, inspector);
+ yield selectNode(nodeFront, inspector);
+ let markupTagLine = getContainerForNodeFront(nodeFront, inspector).tagLine;
+
+ for (let { clipboardData, menuId } of PASTE_ADJACENT_HTML_DATA) {
+ let allMenuItems = openContextMenuAndGetAllItems(inspector, {
+ target: markupTagLine,
+ });
+ info(`Testing ${menuId} for ${clipboardData}`);
+ clipboard.set(clipboardData);
+
+ let onMutation = inspector.once("markupmutation");
+ allMenuItems.find(item => item.id === menuId).click();
+ info("Waiting for mutation to occur");
+ yield onMutation;
+ }
+
+ let html = yield testActor.getProperty(adjacentNodeSelector, "innerHTML");
+ ok(html.trim() === "1<span class=\"ref\">234</span><span>5</span>",
+ "The Paste as Last Child / as First Child / Before / After worked as " +
+ "expected");
+ yield undoChange(inspector);
+
+ html = yield testActor.getProperty(adjacentNodeSelector, "innerHTML");
+ ok(html.trim() === "1<span class=\"ref\">234</span>",
+ "Undo works for paste adjacent HTML");
+ }
+});
diff --git a/devtools/client/inspector/test/browser_inspector_menu-04-use-in-console.js b/devtools/client/inspector/test/browser_inspector_menu-04-use-in-console.js
new file mode 100644
index 000000000..3908784f6
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_menu-04-use-in-console.js
@@ -0,0 +1,61 @@
+/* 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";
+
+// Tests "Use in Console" menu item
+
+const TEST_URL = URL_ROOT + "doc_inspector_menu.html";
+
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("devtools.toolbox.splitconsoleEnabled");
+});
+
+// Use the old webconsole since the node isn't being rendered as an HTML tag
+// in the new one (Bug 1304794)
+Services.prefs.setBoolPref("devtools.webconsole.new-frontend-enabled", false);
+registerCleanupFunction(function* () {
+ Services.prefs.clearUserPref("devtools.webconsole.new-frontend-enabled");
+});
+
+add_task(function* () {
+ let { inspector, toolbox } = yield openInspectorForURL(TEST_URL);
+
+ yield testUseInConsole();
+
+ function* testUseInConsole() {
+ info("Testing 'Use in Console' menu item.");
+
+ yield selectNode("#console-var", inspector);
+ let container = yield getContainerForSelector("#console-var", inspector);
+ let allMenuItems = openContextMenuAndGetAllItems(inspector, {
+ target: container.tagLine,
+ });
+ let menuItem = allMenuItems.find(i => i.id === "node-menu-useinconsole");
+ menuItem.click();
+
+ yield inspector.once("console-var-ready");
+
+ let hud = toolbox.getPanel("webconsole").hud;
+ let jsterm = hud.jsterm;
+
+ let jstermInput = jsterm.hud.document.querySelector(".jsterm-input-node");
+ is(jstermInput.value, "temp0", "first console variable is named temp0");
+
+ let result = yield jsterm.execute();
+ isnot(result.textContent.indexOf('<p id="console-var">'), -1,
+ "variable temp0 references correct node");
+
+ yield selectNode("#console-var-multi", inspector);
+ menuItem.click();
+ yield inspector.once("console-var-ready");
+
+ is(jstermInput.value, "temp1", "second console variable is named temp1");
+
+ result = yield jsterm.execute();
+ isnot(result.textContent.indexOf('<p id="console-var-multi">'), -1,
+ "variable temp1 references correct node");
+
+ jsterm.clearHistory();
+ }
+});
diff --git a/devtools/client/inspector/test/browser_inspector_menu-05-attribute-items.js b/devtools/client/inspector/test/browser_inspector_menu-05-attribute-items.js
new file mode 100644
index 000000000..df901f0a4
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_menu-05-attribute-items.js
@@ -0,0 +1,79 @@
+/* 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";
+
+// Test that attribute items work in the context menu
+
+const TEST_URL = URL_ROOT + "doc_inspector_menu.html";
+
+add_task(function* () {
+ let { inspector, testActor } = yield openInspectorForURL(TEST_URL);
+ yield selectNode("#attributes", inspector);
+
+ yield testAddAttribute();
+ yield testEditAttribute();
+ yield testRemoveAttribute();
+
+ function* testAddAttribute() {
+ info("Triggering 'Add Attribute' and waiting for mutation to occur");
+ let addAttribute = getMenuItem("node-menu-add-attribute");
+ addAttribute.click();
+
+ EventUtils.synthesizeKey('class="u-hidden"', {});
+ let onMutation = inspector.once("markupmutation");
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ yield onMutation;
+
+ let hasAttribute = testActor.hasNode("#attributes.u-hidden");
+ ok(hasAttribute, "attribute was successfully added");
+ }
+
+ function* testEditAttribute() {
+ info("Testing 'Edit Attribute' menu item");
+ let editAttribute = getMenuItem("node-menu-edit-attribute");
+
+ info("Triggering 'Edit Attribute' and waiting for mutation to occur");
+ inspector.nodeMenuTriggerInfo = {
+ type: "attribute",
+ name: "data-edit"
+ };
+ editAttribute.click();
+ EventUtils.synthesizeKey("data-edit='edited'", {});
+ let onMutation = inspector.once("markupmutation");
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ yield onMutation;
+
+ let isAttributeChanged =
+ yield testActor.hasNode("#attributes[data-edit='edited']");
+ ok(isAttributeChanged, "attribute was successfully edited");
+ }
+
+ function* testRemoveAttribute() {
+ info("Testing 'Remove Attribute' menu item");
+ let removeAttribute = getMenuItem("node-menu-remove-attribute");
+
+ info("Triggering 'Remove Attribute' and waiting for mutation to occur");
+ inspector.nodeMenuTriggerInfo = {
+ type: "attribute",
+ name: "data-remove"
+ };
+ let onMutation = inspector.once("markupmutation");
+ removeAttribute.click();
+ yield onMutation;
+
+ let hasAttribute = yield testActor.hasNode("#attributes[data-remove]");
+ ok(!hasAttribute, "attribute was successfully removed");
+ }
+
+ function getMenuItem(id) {
+ let allMenuItems = openContextMenuAndGetAllItems(inspector, {
+ target: getContainerForSelector("#attributes", inspector).tagLine,
+ });
+ let menuItem = allMenuItems.find(i => i.id === id);
+ ok(menuItem, "Menu item '" + id + "' found");
+ // Close the menu so synthesizing future keys won't select menu items.
+ EventUtils.synthesizeKey("VK_ESCAPE", {});
+ return menuItem;
+ }
+});
diff --git a/devtools/client/inspector/test/browser_inspector_menu-06-other.js b/devtools/client/inspector/test/browser_inspector_menu-06-other.js
new file mode 100644
index 000000000..9f4310121
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_menu-06-other.js
@@ -0,0 +1,95 @@
+/* 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";
+
+// Tests for menuitem functionality that doesn't fit into any specific category
+const TEST_URL = URL_ROOT + "doc_inspector_menu.html";
+add_task(function* () {
+ let { inspector, toolbox, testActor } = yield openInspectorForURL(TEST_URL);
+ yield testShowDOMProperties();
+ yield testDuplicateNode();
+ yield testDeleteNode();
+ yield testDeleteRootNode();
+ yield testScrollIntoView();
+ function* testShowDOMProperties() {
+ info("Testing 'Show DOM Properties' menu item.");
+ let allMenuItems = openContextMenuAndGetAllItems(inspector);
+ let showDOMPropertiesNode =
+ allMenuItems.find(item => item.id === "node-menu-showdomproperties");
+ ok(showDOMPropertiesNode, "the popup menu has a show dom properties item");
+
+ let consoleOpened = toolbox.once("webconsole-ready");
+
+ info("Triggering 'Show DOM Properties' and waiting for inspector open");
+ showDOMPropertiesNode.click();
+ yield consoleOpened;
+
+ let webconsoleUI = toolbox.getPanel("webconsole").hud.ui;
+ let messagesAdded = webconsoleUI.once("new-messages");
+ yield messagesAdded;
+ info("Checking if 'inspect($0)' was evaluated");
+ ok(webconsoleUI.jsterm.history[0] === "inspect($0)");
+ yield toolbox.toggleSplitConsole();
+ }
+ function* testDuplicateNode() {
+ info("Testing 'Duplicate Node' menu item for normal elements.");
+
+ yield selectNode(".duplicate", inspector);
+ is((yield testActor.getNumberOfElementMatches(".duplicate")), 1,
+ "There should initially be 1 .duplicate node");
+
+ let allMenuItems = openContextMenuAndGetAllItems(inspector);
+ let menuItem =
+ allMenuItems.find(item => item.id === "node-menu-duplicatenode");
+ ok(menuItem, "'Duplicate node' menu item should exist");
+
+ info("Triggering 'Duplicate Node' and waiting for inspector to update");
+ let updated = inspector.once("markupmutation");
+ menuItem.click();
+ yield updated;
+
+ is((yield testActor.getNumberOfElementMatches(".duplicate")), 2,
+ "The duplicated node should be in the markup.");
+
+ let container = yield getContainerForSelector(".duplicate + .duplicate",
+ inspector);
+ ok(container, "A MarkupContainer should be created for the new node");
+ }
+
+ function* testDeleteNode() {
+ info("Testing 'Delete Node' menu item for normal elements.");
+ yield selectNode("#delete", inspector);
+ let allMenuItems = openContextMenuAndGetAllItems(inspector);
+ let deleteNode = allMenuItems.find(item => item.id === "node-menu-delete");
+ ok(deleteNode, "the popup menu has a delete menu item");
+ let updated = inspector.once("inspector-updated");
+
+ info("Triggering 'Delete Node' and waiting for inspector to update");
+ deleteNode.click();
+ yield updated;
+
+ ok(!(yield testActor.hasNode("#delete")), "Node deleted");
+ }
+
+ function* testDeleteRootNode() {
+ info("Testing 'Delete Node' menu item does not delete root node.");
+ yield selectNode("html", inspector);
+
+ let allMenuItems = openContextMenuAndGetAllItems(inspector);
+ let deleteNode = allMenuItems.find(item => item.id === "node-menu-delete");
+ deleteNode.click();
+
+ let deferred = defer();
+ executeSoon(deferred.resolve);
+ yield deferred.promise;
+
+ ok((yield testActor.eval("!!content.document.documentElement")),
+ "Document element still alive.");
+ }
+
+ function* testScrollIntoView() {
+ // Follow up bug to add this test - https://bugzilla.mozilla.org/show_bug.cgi?id=1154107
+ todo(false, "Verify that node is scrolled into the viewport.");
+ }
+});
diff --git a/devtools/client/inspector/test/browser_inspector_navigate_to_errors.js b/devtools/client/inspector/test/browser_inspector_navigate_to_errors.js
new file mode 100644
index 000000000..c2266c852
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_navigate_to_errors.js
@@ -0,0 +1,50 @@
+/* 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";
+
+// Test that inspector works when navigating to error pages.
+
+const TEST_URL_1 = "data:text/html,<html><body id=\"test-doc-1\">page</body></html>";
+const TEST_URL_2 = "http://127.0.0.1:36325/";
+const TEST_URL_3 = "http://www.wronguri.wronguri/";
+const TEST_URL_4 = "data:text/html,<html><body>test-doc-4</body></html>";
+
+add_task(function* () {
+ // Open the inspector on a valid URL
+ let { inspector, testActor } = yield openInspectorForURL(TEST_URL_1);
+
+ info("Navigate to closed port");
+ yield navigateTo(inspector, TEST_URL_2);
+
+ let documentURI = yield testActor.eval("document.documentURI;");
+ ok(documentURI.startsWith("about:neterror"), "content is correct.");
+
+ let hasPage = yield getNodeFront("#test-doc-1", inspector);
+ ok(!hasPage, "Inspector actor is no longer able to reach previous page DOM node");
+
+ let hasNetErrorNode = yield getNodeFront("#errorShortDesc", inspector);
+ ok(hasNetErrorNode, "Inspector actor is able to reach error page DOM node");
+
+ let bundle = Services.strings.createBundle("chrome://global/locale/appstrings.properties");
+ let domain = TEST_URL_2.match(/^http:\/\/(.*)\/$/)[1];
+ let errorMsg = bundle.formatStringFromName("connectionFailure",
+ [domain], 1);
+ is(yield getDisplayedNodeTextContent("#errorShortDescText", inspector), errorMsg,
+ "Inpector really inspects the error page");
+
+ info("Navigate to unknown domain");
+ yield navigateTo(inspector, TEST_URL_3);
+
+ domain = TEST_URL_3.match(/^http:\/\/(.*)\/$/)[1];
+ errorMsg = bundle.formatStringFromName("dnsNotFound",
+ [domain], 1);
+ is(yield getDisplayedNodeTextContent("#errorShortDescText", inspector), errorMsg,
+ "Inspector really inspects the new error page");
+
+ info("Navigate to a valid url");
+ yield navigateTo(inspector, TEST_URL_4);
+
+ is(yield getDisplayedNodeTextContent("body", inspector), "test-doc-4",
+ "Inspector really inspects the valid url");
+});
diff --git a/devtools/client/inspector/test/browser_inspector_navigation.js b/devtools/client/inspector/test/browser_inspector_navigation.js
new file mode 100644
index 000000000..dab6f7007
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_navigation.js
@@ -0,0 +1,43 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set 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";
+
+// Test that inspector updates when page is navigated.
+
+const TEST_URL_FILE = "browser/devtools/client/inspector/test/" +
+ "doc_inspector_breadcrumbs.html";
+
+const TEST_URL_1 = "http://test1.example.org/" + TEST_URL_FILE;
+const TEST_URL_2 = "http://test2.example.org/" + TEST_URL_FILE;
+
+add_task(function* () {
+ let { inspector, testActor } = yield openInspectorForURL(TEST_URL_1);
+
+ yield selectNode("#i1", inspector);
+
+ info("Navigating to a different page.");
+ yield navigateTo(inspector, TEST_URL_2);
+
+ ok(true, "New page loaded");
+ yield selectNode("#i1", inspector);
+
+ let markuploaded = inspector.once("markuploaded");
+ let onUpdated = inspector.once("inspector-updated");
+
+ info("Going back in history");
+ yield testActor.eval("history.go(-1)");
+
+ info("Waiting for markup view to load after going back in history.");
+ yield markuploaded;
+
+ info("Check that the inspector updates");
+ yield onUpdated;
+
+ ok(true, "Old page loaded");
+ is((yield testActor.eval("location.href;")), TEST_URL_1, "URL is correct.");
+
+ yield selectNode("#i1", inspector);
+});
diff --git a/devtools/client/inspector/test/browser_inspector_open_on_neterror.js b/devtools/client/inspector/test/browser_inspector_open_on_neterror.js
new file mode 100644
index 000000000..01e065a1a
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_open_on_neterror.js
@@ -0,0 +1,37 @@
+/* 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";
+
+// Test that inspector works correctly when opened against a net error page
+
+const TEST_URL_1 = "http://127.0.0.1:36325/";
+const TEST_URL_2 = "data:text/html,<html><body>test-doc-2</body></html>";
+
+add_task(function* () {
+ // Unfortunately, net error page are not firing load event, so that we can't
+ // use addTab helper and have to do that:
+ let tab = gBrowser.selectedTab = gBrowser.addTab("data:text/html,empty");
+ yield BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ yield ContentTask.spawn(tab.linkedBrowser, { url: TEST_URL_1 }, function* ({ url }) {
+ // Also, the neterror being privileged, the DOMContentLoaded only fires on
+ // the chromeEventHandler.
+ let { chromeEventHandler } = docShell; // eslint-disable-line no-undef
+ let onDOMContentLoaded = ContentTaskUtils.waitForEvent(chromeEventHandler,
+ "DOMContentLoaded", true);
+ content.location = url;
+ yield onDOMContentLoaded;
+ });
+
+ let { inspector, testActor } = yield openInspector();
+ ok(true, "Inspector loaded on the already opened net error");
+
+ let documentURI = yield testActor.eval("document.documentURI;");
+ ok(documentURI.startsWith("about:neterror"), "content is really a net error page.");
+
+ info("Navigate to a valid url");
+ yield navigateTo(inspector, TEST_URL_2);
+
+ is(yield getDisplayedNodeTextContent("body", inspector), "test-doc-2",
+ "Inspector really inspects the valid url");
+});
diff --git a/devtools/client/inspector/test/browser_inspector_pane-toggle-01.js b/devtools/client/inspector/test/browser_inspector_pane-toggle-01.js
new file mode 100644
index 000000000..1ec95cec3
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_pane-toggle-01.js
@@ -0,0 +1,27 @@
+/* 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 inspector panel has a sidebar pane toggle button, and that
+// this button is visible both in BOTTOM and SIDE hosts.
+
+add_task(function* () {
+ info("Open the inspector in a bottom toolbox host");
+ let {toolbox, inspector} = yield openInspectorForURL("about:blank", "bottom");
+
+ let button = inspector.panelDoc.querySelector(".sidebar-toggle");
+ ok(button, "The toggle button exists in the DOM");
+ is(button.parentNode.id, "inspector-sidebar-toggle-box",
+ "The toggle button has the right parent");
+ ok(button.getAttribute("title"), "The tool tip has initial state");
+ ok(!button.classList.contains("pane-collapsed"), "The button is in expanded state");
+ ok(!!button.getClientRects().length, "The button is visible");
+
+ info("Switch the host to side type");
+ yield toolbox.switchHost("side");
+
+ ok(!!button.getClientRects().length, "The button is still visible");
+ ok(!button.classList.contains("pane-collapsed"),
+ "The button is still in expanded state");
+});
diff --git a/devtools/client/inspector/test/browser_inspector_pane-toggle-02.js b/devtools/client/inspector/test/browser_inspector_pane-toggle-02.js
new file mode 100644
index 000000000..54b68c655
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_pane-toggle-02.js
@@ -0,0 +1,43 @@
+/* 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 inspector toggled panel is visible by default, is hidden after
+// clicking on the toggle button and remains expanded/collapsed when switching
+// hosts.
+
+add_task(function* () {
+ info("Open the inspector in a side toolbox host");
+ let {toolbox, inspector} = yield openInspectorForURL("about:blank", "side");
+
+ let panel = inspector.panelDoc.querySelector("#inspector-splitter-box .controlled");
+
+ let button = inspector.panelDoc.querySelector(".sidebar-toggle");
+ ok(!panel.classList.contains("pane-collapsed"), "The panel is in expanded state");
+
+ info("Listen to the end of the animation on the sidebar panel");
+ let onTransitionEnd = once(panel, "transitionend");
+
+ info("Click on the toggle button");
+ EventUtils.synthesizeMouseAtCenter(button, {},
+ inspector.panelDoc.defaultView);
+
+ yield onTransitionEnd;
+ ok(panel.classList.contains("pane-collapsed"), "The panel is in collapsed state");
+ ok(!panel.hasAttribute("animated"),
+ "The collapsed panel will not perform unwanted animations");
+
+ info("Switch the host to bottom type");
+ yield toolbox.switchHost("bottom");
+ ok(panel.classList.contains("pane-collapsed"), "The panel is in collapsed state");
+
+ info("Click on the toggle button to expand the panel again");
+
+ onTransitionEnd = once(panel, "transitionend");
+ EventUtils.synthesizeMouseAtCenter(button, {},
+ inspector.panelDoc.defaultView);
+ yield onTransitionEnd;
+
+ ok(!panel.classList.contains("pane-collapsed"), "The panel is in expanded state");
+});
diff --git a/devtools/client/inspector/test/browser_inspector_pane-toggle-03.js b/devtools/client/inspector/test/browser_inspector_pane-toggle-03.js
new file mode 100644
index 000000000..02fffd995
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_pane-toggle-03.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 the toggle button can collapse and expand the inspector side/bottom
+// panel, and that the appropriate attributes are updated in the process.
+
+add_task(function* () {
+ let {inspector} = yield openInspectorForURL("about:blank");
+
+ let button = inspector.panelDoc.querySelector(".sidebar-toggle");
+ let panel = inspector.panelDoc.querySelector("#inspector-splitter-box .controlled");
+
+ ok(!button.classList.contains("pane-collapsed"), "The button is in expanded state");
+
+ info("Listen to the end of the animation on the sidebar panel");
+ let onTransitionEnd = once(panel, "transitionend");
+
+ info("Click on the toggle button");
+ EventUtils.synthesizeMouseAtCenter(button, {},
+ inspector.panelDoc.defaultView);
+
+ yield onTransitionEnd;
+ ok(button.classList.contains("pane-collapsed"), "The button is in collapsed state");
+ ok(panel.classList.contains("pane-collapsed"), "The panel is in collapsed state");
+
+ info("Listen again to the end of the animation on the sidebar panel");
+ onTransitionEnd = once(panel, "transitionend");
+
+ info("Click on the toggle button again");
+ EventUtils.synthesizeMouseAtCenter(button, {},
+ inspector.panelDoc.defaultView);
+
+ yield onTransitionEnd;
+ ok(!button.classList.contains("pane-collapsed"), "The button is in expanded state");
+ ok(!panel.classList.contains("pane-collapsed"), "The panel is in expanded state");
+});
diff --git a/devtools/client/inspector/test/browser_inspector_pane-toggle-05.js b/devtools/client/inspector/test/browser_inspector_pane-toggle-05.js
new file mode 100644
index 000000000..2a0c82037
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_pane-toggle-05.js
@@ -0,0 +1,33 @@
+/* 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 keyboard navigation for the pane toggle using
+* space and enter
+*/
+
+add_task(function* () {
+ let {inspector} = yield openInspectorForURL("about:blank", "side");
+ let panel = inspector.panelDoc.querySelector("#inspector-splitter-box .controlled");
+
+ let button = inspector.panelDoc.querySelector(".sidebar-toggle");
+
+ ok(!panel.classList.contains("pane-collapsed"), "The panel is in expanded state");
+
+ yield togglePane(button, "Press on the toggle button", panel, "VK_RETURN");
+ ok(panel.classList.contains("pane-collapsed"), "The panel is in collapsed state");
+
+ yield togglePane(button, "Press on the toggle button to expand the panel again",
+ panel, "VK_SPACE");
+ ok(!panel.classList.contains("pane-collapsed"), "The panel is in expanded state");
+});
+
+function* togglePane(button, message, panel, keycode) {
+ let onTransitionEnd = once(panel, "transitionend");
+ info(message);
+ button.focus();
+ EventUtils.synthesizeKey(keycode, {});
+ yield onTransitionEnd;
+}
diff --git a/devtools/client/inspector/test/browser_inspector_picker-stop-on-destroy.js b/devtools/client/inspector/test/browser_inspector_picker-stop-on-destroy.js
new file mode 100644
index 000000000..bc81b9661
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_picker-stop-on-destroy.js
@@ -0,0 +1,30 @@
+/* vim: set 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";
+
+// Test that the highlighter's picker should be stopped when the toolbox is
+// closed
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<p>testing the highlighter goes away on destroy</p>";
+
+add_task(function* () {
+ let { inspector, toolbox } = yield openInspectorForURL(TEST_URI);
+ let pickerStopped = toolbox.once("picker-stopped");
+
+ yield selectNode("p", inspector);
+
+ info("Inspector displayed and ready, starting the picker.");
+ yield startPicker(toolbox);
+
+ info("Destroying the toolbox.");
+ yield toolbox.destroy();
+
+ info("Waiting for the picker-stopped event that should be fired when the " +
+ "toolbox is destroyed.");
+ yield pickerStopped;
+
+ ok(true, "picker-stopped event fired after switch tools so picker is closed");
+});
diff --git a/devtools/client/inspector/test/browser_inspector_picker-stop-on-tool-change.js b/devtools/client/inspector/test/browser_inspector_picker-stop-on-tool-change.js
new file mode 100644
index 000000000..37dc82ec1
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_picker-stop-on-tool-change.js
@@ -0,0 +1,27 @@
+/* vim: set 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";
+
+// Test that the highlighter's picker is stopped when a different tool is
+// selected
+
+const TEST_URI = "data:text/html;charset=UTF-8," +
+ "testing the highlighter goes away on tool selection";
+
+add_task(function* () {
+ let { toolbox } = yield openInspectorForURL(TEST_URI);
+ let pickerStopped = toolbox.once("picker-stopped");
+
+ info("Starting the inspector picker");
+ yield startPicker(toolbox);
+
+ info("Selecting another tool than the inspector in the toolbox");
+ yield toolbox.selectNextTool();
+
+ info("Waiting for the picker-stopped event to be fired");
+ yield pickerStopped;
+
+ ok(true, "picker-stopped event fired after switch tools; picker is closed");
+});
diff --git a/devtools/client/inspector/test/browser_inspector_portrait_mode.js b/devtools/client/inspector/test/browser_inspector_portrait_mode.js
new file mode 100644
index 000000000..04fcc2b56
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_portrait_mode.js
@@ -0,0 +1,78 @@
+/* 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";
+
+// Test that the inspector splitter is properly initialized in horizontal mode if the
+// inspector starts in portrait mode.
+
+add_task(function* () {
+ let { inspector, toolbox } = yield openInspectorForURL(
+ "data:text/html;charset=utf-8,<h1>foo</h1><span>bar</span>", "window");
+
+ let hostWindow = toolbox.win.parent;
+ let originalWidth = hostWindow.outerWidth;
+ let originalHeight = hostWindow.outerHeight;
+
+ let splitter = inspector.panelDoc.querySelector(".inspector-sidebar-splitter");
+
+ // If the inspector is not already in landscape mode.
+ if (!splitter.classList.contains("vert")) {
+ info("Resize toolbox window to force inspector to landscape mode");
+ let onClassnameMutation = waitForClassMutation(splitter);
+ hostWindow.resizeTo(800, 500);
+ yield onClassnameMutation;
+
+ ok(splitter.classList.contains("vert"), "Splitter is in vertical mode");
+ }
+
+ info("Resize toolbox window to force inspector to portrait mode");
+ let onClassnameMutation = waitForClassMutation(splitter);
+ hostWindow.resizeTo(500, 500);
+ yield onClassnameMutation;
+
+ ok(splitter.classList.contains("horz"), "Splitter is in horizontal mode");
+
+ info("Close the inspector");
+ yield gDevTools.closeToolbox(toolbox.target);
+
+ info("Reopen inspector");
+ ({ inspector, toolbox } = yield openInspector("window"));
+
+ // Devtools window should still be 500px * 500px, inspector should still be in portrait.
+ splitter = inspector.panelDoc.querySelector(".inspector-sidebar-splitter");
+ ok(splitter.classList.contains("horz"), "Splitter is in horizontal mode");
+
+ info("Restore original window size");
+ toolbox.win.parent.resizeTo(originalWidth, originalHeight);
+});
+
+/**
+ * Helper waiting for a class attribute mutation on the provided target. Returns a
+ * promise.
+ *
+ * @param {Node} target
+ *   Node to observe
+ * @return {Promise} promise that will resolve upon receiving a mutation for the class
+ * attribute on the target.
+ */
+function waitForClassMutation(target) {
+ return new Promise(resolve => {
+ let observer = new MutationObserver((mutations) => {
+ for (let mutation of mutations) {
+ if (mutation.attributeName === "class") {
+ observer.disconnect();
+ resolve();
+ return;
+ }
+ }
+ });
+ observer.observe(target, { attributes: true });
+ });
+}
+
+registerCleanupFunction(function () {
+ // Restore the host type for other tests.
+ Services.prefs.clearUserPref("devtools.toolbox.host");
+});
diff --git a/devtools/client/inspector/test/browser_inspector_pseudoclass-lock.js b/devtools/client/inspector/test/browser_inspector_pseudoclass-lock.js
new file mode 100644
index 000000000..bd98bd58f
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_pseudoclass-lock.js
@@ -0,0 +1,160 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* globals getTestActorWithoutToolbox */
+"use strict";
+
+// Test that locking the pseudoclass displays correctly in the ruleview
+
+const PSEUDO = ":hover";
+const TEST_URL = "data:text/html;charset=UTF-8," +
+ "<head>" +
+ " <style>div {color:red;} div:hover {color:blue;}</style>" +
+ "</head>" +
+ "<body>" +
+ ' <div id="parent-div">' +
+ ' <div id="div-1">test div</div>' +
+ ' <div id="div-2">test div2</div>' +
+ " </div>" +
+ "</body>";
+
+add_task(function* () {
+ info("Creating the test tab and opening the rule-view");
+ let {toolbox, inspector, testActor} = yield openInspectorForURL(TEST_URL);
+
+ info("Selecting the ruleview sidebar");
+ inspector.sidebar.select("ruleview");
+
+ let view = inspector.ruleview.view;
+
+ info("Selecting the test node");
+ yield selectNode("#div-1", inspector);
+
+ yield togglePseudoClass(inspector);
+ yield assertPseudoAddedToNode(inspector, testActor, view, "#div-1");
+
+ yield togglePseudoClass(inspector);
+ yield assertPseudoRemovedFromNode(testActor, "#div-1");
+ yield assertPseudoRemovedFromView(inspector, testActor, view, "#div-1");
+
+ yield togglePseudoClass(inspector);
+ yield testNavigate(inspector, testActor, view);
+
+ info("Toggle pseudo on the parent and ensure everything is toggled off");
+ yield selectNode("#parent-div", inspector);
+ yield togglePseudoClass(inspector);
+ yield assertPseudoRemovedFromNode(testActor, "#div-1");
+ yield assertPseudoRemovedFromView(inspector, testActor, view, "#div-1");
+
+ yield togglePseudoClass(inspector);
+ info("Assert pseudo is dismissed when toggling it on a sibling node");
+ yield selectNode("#div-2", inspector);
+ yield togglePseudoClass(inspector);
+ yield assertPseudoAddedToNode(inspector, testActor, view, "#div-2");
+ let hasLock = yield testActor.hasPseudoClassLock("#div-1", PSEUDO);
+ ok(!hasLock, "pseudo-class lock has been removed for the previous locked node");
+
+ info("Destroying the toolbox");
+ let tab = toolbox.target.tab;
+ yield toolbox.destroy();
+
+ // As the toolbox get detroyed, we need to fetch a new test-actor
+ testActor = yield getTestActorWithoutToolbox(tab);
+
+ yield assertPseudoRemovedFromNode(testActor, "#div-1");
+ yield assertPseudoRemovedFromNode(testActor, "#div-2");
+});
+
+function* togglePseudoClass(inspector) {
+ info("Toggle the pseudoclass, wait for it to be applied");
+
+ // Give the inspector panels a chance to update when the pseudoclass changes
+ let onPseudo = inspector.selection.once("pseudoclass");
+ let onRefresh = inspector.once("rule-view-refreshed");
+
+ // Walker uses SDK-events so calling walker.once does not return a promise.
+ let onMutations = once(inspector.walker, "mutations");
+
+ yield inspector.togglePseudoClass(PSEUDO);
+
+ yield onPseudo;
+ yield onRefresh;
+ yield onMutations;
+}
+
+function* testNavigate(inspector, testActor, ruleview) {
+ yield selectNode("#parent-div", inspector);
+
+ info("Make sure the pseudoclass is still on after navigating to a parent");
+
+ ok((yield testActor.hasPseudoClassLock("#div-1", PSEUDO)),
+ "pseudo-class lock is still applied after inspecting ancestor");
+
+ yield selectNode("#div-2", inspector);
+
+ info("Make sure the pseudoclass is still set after navigating to a " +
+ "non-hierarchy node");
+ ok(yield testActor.hasPseudoClassLock("#div-1", PSEUDO),
+ "pseudo-class lock is still on after inspecting sibling node");
+
+ yield selectNode("#div-1", inspector);
+}
+
+function* showPickerOn(selector, inspector) {
+ let nodeFront = yield getNodeFront(selector, inspector);
+ yield inspector.highlighter.showBoxModel(nodeFront);
+}
+
+function* assertPseudoAddedToNode(inspector, testActor, ruleview, selector) {
+ info("Make sure the pseudoclass lock is applied to " + selector + " and its ancestors");
+
+ let hasLock = yield testActor.hasPseudoClassLock(selector, PSEUDO);
+ ok(hasLock, "pseudo-class lock has been applied");
+ hasLock = yield testActor.hasPseudoClassLock("#parent-div", PSEUDO);
+ ok(hasLock, "pseudo-class lock has been applied");
+ hasLock = yield testActor.hasPseudoClassLock("body", PSEUDO);
+ ok(hasLock, "pseudo-class lock has been applied");
+
+ info("Check that the ruleview contains the pseudo-class rule");
+ let rules = ruleview.element.querySelectorAll(
+ ".ruleview-rule.theme-separator");
+ is(rules.length, 3,
+ "rule view is showing 3 rules for pseudo-class locked div");
+ is(rules[1]._ruleEditor.rule.selectorText, "div:hover",
+ "rule view is showing " + PSEUDO + " rule");
+
+ info("Show the highlighter on " + selector);
+ yield showPickerOn(selector, inspector);
+
+ info("Check that the infobar selector contains the pseudo-class");
+ let value = yield testActor.getHighlighterNodeTextContent(
+ "box-model-infobar-pseudo-classes");
+ is(value, PSEUDO, "pseudo-class in infobar selector");
+ yield inspector.highlighter.hideBoxModel();
+}
+
+function* assertPseudoRemovedFromNode(testActor, selector) {
+ info("Make sure the pseudoclass lock is removed from #div-1 and its " +
+ "ancestors");
+
+ let hasLock = yield testActor.hasPseudoClassLock(selector, PSEUDO);
+ ok(!hasLock, "pseudo-class lock has been removed");
+ hasLock = yield testActor.hasPseudoClassLock("#parent-div", PSEUDO);
+ ok(!hasLock, "pseudo-class lock has been removed");
+ hasLock = yield testActor.hasPseudoClassLock("body", PSEUDO);
+ ok(!hasLock, "pseudo-class lock has been removed");
+}
+
+function* assertPseudoRemovedFromView(inspector, testActor, ruleview, selector) {
+ info("Check that the ruleview no longer contains the pseudo-class rule");
+ let rules = ruleview.element.querySelectorAll(
+ ".ruleview-rule.theme-separator");
+ is(rules.length, 2, "rule view is showing 2 rules after removing lock");
+
+ yield showPickerOn(selector, inspector);
+
+ let value = yield testActor.getHighlighterNodeTextContent(
+ "box-model-infobar-pseudo-classes");
+ is(value, "", "pseudo-class removed from infobar selector");
+ yield inspector.highlighter.hideBoxModel();
+}
diff --git a/devtools/client/inspector/test/browser_inspector_pseudoclass-menu.js b/devtools/client/inspector/test/browser_inspector_pseudoclass-menu.js
new file mode 100644
index 000000000..45bd82b76
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_pseudoclass-menu.js
@@ -0,0 +1,46 @@
+/* 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";
+
+// Test that the inspector has the correct pseudo-class locking menu items and
+// that these items actually work
+
+const TEST_URI = "data:text/html;charset=UTF-8," +
+ "pseudo-class lock node menu tests" +
+ "<div>test div</div>";
+const PSEUDOS = ["hover", "active", "focus"];
+
+add_task(function* () {
+ let {inspector, testActor} = yield openInspectorForURL(TEST_URI);
+ yield selectNode("div", inspector);
+
+ let allMenuItems = openContextMenuAndGetAllItems(inspector);
+
+ yield testMenuItems(testActor, allMenuItems, inspector);
+});
+
+function* testMenuItems(testActor, allMenuItems, inspector) {
+ for (let pseudo of PSEUDOS) {
+ let menuItem =
+ allMenuItems.find(item => item.id === "node-menu-pseudo-" + pseudo);
+ ok(menuItem, ":" + pseudo + " menuitem exists");
+ is(menuItem.disabled, false, ":" + pseudo + " menuitem is enabled");
+
+ // Give the inspector panels a chance to update when the pseudoclass changes
+ let onPseudo = inspector.selection.once("pseudoclass");
+ let onRefresh = inspector.once("rule-view-refreshed");
+
+ // Walker uses SDK-events so calling walker.once does not return a promise.
+ let onMutations = once(inspector.walker, "mutations");
+
+ menuItem.click();
+
+ yield onPseudo;
+ yield onRefresh;
+ yield onMutations;
+
+ let hasLock = yield testActor.hasPseudoClassLock("div", ":" + pseudo);
+ ok(hasLock, "pseudo-class lock has been applied");
+ }
+}
diff --git a/devtools/client/inspector/test/browser_inspector_reload-01.js b/devtools/client/inspector/test/browser_inspector_reload-01.js
new file mode 100644
index 000000000..61a1dde27
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_reload-01.js
@@ -0,0 +1,32 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set 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";
+
+// A test to ensure reloading a page doesn't break the inspector.
+
+// Reload should reselect the currently selected markup view element.
+// This should work even when an element whose selector needs escaping
+// is selected (bug 1002280).
+const TEST_URI = "data:text/html,<p id='1'>p</p>";
+
+add_task(function* () {
+ let { inspector, testActor } = yield openInspectorForURL(TEST_URI);
+ yield selectNode("p", inspector);
+
+ let markupLoaded = inspector.once("markuploaded");
+
+ info("Reloading page.");
+ yield testActor.eval("location.reload()");
+
+ info("Waiting for markupview to load after reload.");
+ yield markupLoaded;
+
+ let nodeFront = yield getNodeFront("p", inspector);
+ is(inspector.selection.nodeFront, nodeFront, "<p> selected after reload.");
+
+ info("Selecting a node to see that inspector still works.");
+ yield selectNode("body", inspector);
+});
diff --git a/devtools/client/inspector/test/browser_inspector_reload-02.js b/devtools/client/inspector/test/browser_inspector_reload-02.js
new file mode 100644
index 000000000..c9940a828
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_reload-02.js
@@ -0,0 +1,48 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set 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";
+
+// A test to ensure reloading a page doesn't break the inspector.
+
+// Reload should reselect the currently selected markup view element.
+// This should work even when an element whose selector is inaccessible
+// is selected (bug 1038651).
+const TEST_URI = 'data:text/xml,<?xml version="1.0" standalone="no"?>' +
+'<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"' +
+' "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">' +
+'<svg width="4cm" height="4cm" viewBox="0 0 400 400"' +
+' xmlns="http://www.w3.org/2000/svg" version="1.1">' +
+" <title>Example triangle01- simple example of a path</title>" +
+" <desc>A path that draws a triangle</desc>" +
+' <rect x="1" y="1" width="398" height="398"' +
+' fill="none" stroke="blue" />' +
+' <path d="M 100 100 L 300 100 L 200 300 z"' +
+' fill="red" stroke="blue" stroke-width="3" />' +
+"</svg>";
+
+add_task(function* () {
+ let { inspector, testActor } = yield openInspectorForURL(TEST_URI);
+
+ let markupLoaded = inspector.once("markuploaded");
+
+ info("Reloading page.");
+ yield testActor.eval("location.reload()");
+
+ info("Waiting for markupview to load after reload.");
+ yield markupLoaded;
+
+ let svgFront = yield getNodeFront("svg", inspector);
+ is(inspector.selection.nodeFront, svgFront, "<svg> selected after reload.");
+
+ info("Selecting a node to see that inspector still works.");
+ yield selectNode("rect", inspector);
+
+ info("Reloading page.");
+ yield testActor.eval("location.reload");
+
+ let rectFront = yield getNodeFront("rect", inspector);
+ is(inspector.selection.nodeFront, rectFront, "<rect> selected after reload.");
+});
diff --git a/devtools/client/inspector/test/browser_inspector_remove-iframe-during-load.js b/devtools/client/inspector/test/browser_inspector_remove-iframe-during-load.js
new file mode 100644
index 000000000..2058b85fa
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_remove-iframe-during-load.js
@@ -0,0 +1,48 @@
+/* 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";
+
+// Testing that the inspector doesn't go blank when navigating to a page that
+// deletes an iframe while loading.
+
+const TEST_URL = URL_ROOT + "doc_inspector_remove-iframe-during-load.html";
+
+add_task(function* () {
+ let {inspector, testActor} = yield openInspectorForURL("about:blank");
+ yield selectNode("body", inspector);
+
+ // We do not want to wait for the inspector to be fully ready before testing
+ // so we load TEST_URL and just wait for the content window to be done loading
+ yield testActor.loadAndWaitForCustomEvent(TEST_URL);
+
+ // The content doc contains a script that creates iframes and deletes them
+ // immediately after. It does this before the load event, after
+ // DOMContentLoaded and after load. This is what used to make the inspector go
+ // blank when navigating to that page.
+ // At this stage, there should be no iframes in the page anymore.
+ ok(!(yield testActor.hasNode("iframe")),
+ "Iframes added by the content page should have been removed");
+
+ // Create/remove an extra one now, after the load event.
+ info("Creating and removing an iframe.");
+ let onMarkupLoaded = inspector.once("markuploaded");
+ testActor.eval("new " + function () {
+ let iframe = document.createElement("iframe");
+ document.body.appendChild(iframe);
+ iframe.remove();
+ });
+
+ ok(!(yield testActor.hasNode("iframe")),
+ "The after-load iframe should have been removed.");
+
+ info("Waiting for markup-view to load.");
+ yield onMarkupLoaded;
+
+ // Assert that the markup-view is displayed and works
+ ok(!(yield testActor.hasNode("iframe")), "Iframe has been removed.");
+ is((yield testActor.getProperty("#yay", "textContent")), "load",
+ "Load event fired.");
+
+ yield selectNode("#yay", inspector);
+});
diff --git a/devtools/client/inspector/test/browser_inspector_search-01.js b/devtools/client/inspector/test/browser_inspector_search-01.js
new file mode 100644
index 000000000..a4fd4d424
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_search-01.js
@@ -0,0 +1,96 @@
+/* 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/ */
+/* eslint no-inline-comments: 0 */
+"use strict";
+
+requestLongerTimeout(2);
+
+// Test that searching for nodes in the search field actually selects those
+// nodes.
+
+const TEST_URL = URL_ROOT + "doc_inspector_search.html";
+
+// The various states of the inspector: [key, id, isValid]
+// [
+// what key to press,
+// what id should be selected after the keypress,
+// is the searched text valid selector
+// ]
+const KEY_STATES = [
+ ["#", "b1", true], // #
+ ["d", "b1", true], // #d
+ ["1", "b1", true], // #d1
+ ["VK_RETURN", "d1", true], // #d1
+ ["VK_BACK_SPACE", "d1", true], // #d
+ ["2", "d1", true], // #d2
+ ["VK_RETURN", "d2", true], // #d2
+ ["2", "d2", true], // #d22
+ ["VK_RETURN", "d2", false], // #d22
+ ["VK_BACK_SPACE", "d2", false], // #d2
+ ["VK_RETURN", "d2", true], // #d2
+ ["VK_BACK_SPACE", "d2", true], // #d
+ ["1", "d2", true], // #d1
+ ["VK_RETURN", "d1", true], // #d1
+ ["VK_BACK_SPACE", "d1", true], // #d
+ ["VK_BACK_SPACE", "d1", true], // #
+ ["VK_BACK_SPACE", "d1", true], //
+ ["d", "d1", true], // d
+ ["i", "d1", true], // di
+ ["v", "d1", true], // div
+ [".", "d1", true], // div.
+ ["c", "d1", true], // div.c
+ ["VK_UP", "d1", true], // div.c1
+ ["VK_TAB", "d1", true], // div.c1
+ ["VK_RETURN", "d2", true], // div.c1
+ ["VK_BACK_SPACE", "d2", true], // div.c
+ ["VK_BACK_SPACE", "d2", true], // div.
+ ["VK_BACK_SPACE", "d2", true], // div
+ ["VK_BACK_SPACE", "d2", true], // di
+ ["VK_BACK_SPACE", "d2", true], // d
+ ["VK_BACK_SPACE", "d2", true], //
+ [".", "d2", true], // .
+ ["c", "d2", true], // .c
+ ["1", "d2", true], // .c1
+ ["VK_RETURN", "d2", true], // .c1
+ ["VK_RETURN", "s2", true], // .c1
+ ["VK_RETURN", "p1", true], // .c1
+ ["P", "p1", true], // .c1P
+ ["VK_RETURN", "p1", false], // .c1P
+];
+
+add_task(function* () {
+ let { inspector } = yield openInspectorForURL(TEST_URL);
+ let { searchBox } = inspector;
+
+ yield selectNode("#b1", inspector);
+ yield focusSearchBoxUsingShortcut(inspector.panelWin);
+
+ let index = 0;
+ for (let [ key, id, isValid ] of KEY_STATES) {
+ info(index + ": Pressing key " + key + " to get id " + id + ".");
+ let done = inspector.searchSuggestions.once("processing-done");
+ EventUtils.synthesizeKey(key, {}, inspector.panelWin);
+ yield done;
+ info("Got processing-done event");
+
+ if (key === "VK_RETURN") {
+ info("Waiting for " + (isValid ? "NO " : "") + "results");
+ yield inspector.search.once("search-result");
+ }
+
+ info("Waiting for search query to complete");
+ yield inspector.searchSuggestions._lastQuery;
+
+ info(inspector.selection.nodeFront.id + " is selected with text " +
+ searchBox.value);
+ let nodeFront = yield getNodeFront("#" + id, inspector);
+ is(inspector.selection.nodeFront, nodeFront,
+ "Correct node is selected for state " + index);
+
+ is(!searchBox.classList.contains("devtools-style-searchbox-no-match"), isValid,
+ "Correct searchbox result state for state " + index);
+
+ index++;
+ }
+});
diff --git a/devtools/client/inspector/test/browser_inspector_search-02.js b/devtools/client/inspector/test/browser_inspector_search-02.js
new file mode 100644
index 000000000..5e75f5dd2
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_search-02.js
@@ -0,0 +1,169 @@
+/* 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";
+
+// Testing that searching for combining selectors using the inspector search
+// field produces correct suggestions.
+
+const TEST_URL = URL_ROOT + "doc_inspector_search-suggestions.html";
+
+// An array of (key, suggestions) pairs where key is a key to press and
+// suggestions is an array of suggestions that should be shown in the popup.
+// Suggestion is an object with label of the entry and optional count
+// (defaults to 1)
+const TEST_DATA = [
+ {
+ key: "d",
+ suggestions: [
+ {label: "div"},
+ {label: "#d1"},
+ {label: "#d2"}
+ ]
+ },
+ {
+ key: "i",
+ suggestions: [{label: "div"}]
+ },
+ {
+ key: "v",
+ suggestions: []
+ },
+ {
+ key: " ",
+ suggestions: [
+ {label: "div div"},
+ {label: "div span"}
+ ]
+ },
+ {
+ key: ">",
+ suggestions: [
+ {label: "div >div"},
+ {label: "div >span"}
+ ]
+ },
+ {
+ key: "VK_BACK_SPACE",
+ suggestions: [
+ {label: "div div"},
+ {label: "div span"}
+ ]
+ },
+ {
+ key: "+",
+ suggestions: [{label: "div +span"}]
+ },
+ {
+ key: "VK_BACK_SPACE",
+ suggestions: [
+ {label: "div div"},
+ {label: "div span"}
+ ]
+ },
+ {
+ key: "VK_BACK_SPACE",
+ suggestions: []
+ },
+ {
+ key: "VK_BACK_SPACE",
+ suggestions: [{label: "div"}]
+ },
+ {
+ key: "VK_BACK_SPACE",
+ suggestions: [
+ {label: "div"},
+ {label: "#d1"},
+ {label: "#d2"}
+ ]
+ },
+ {
+ key: "VK_BACK_SPACE",
+ suggestions: []
+ },
+ {
+ key: "p",
+ suggestions: [
+ {label: "p"},
+ {label: "#p1"},
+ {label: "#p2"},
+ {label: "#p3"},
+ ]
+ },
+ {
+ key: " ",
+ suggestions: [{label: "p strong"}]
+ },
+ {
+ key: "+",
+ suggestions: [
+ {label: "p +button" },
+ {label: "p +p"}
+ ]
+ },
+ {
+ key: "b",
+ suggestions: [{label: "p +button"}]
+ },
+ {
+ key: "u",
+ suggestions: [{label: "p +button"}]
+ },
+ {
+ key: "t",
+ suggestions: [{label: "p +button"}]
+ },
+ {
+ key: "t",
+ suggestions: [{label: "p +button"}]
+ },
+ {
+ key: "o",
+ suggestions: [{label: "p +button"}]
+ },
+ {
+ key: "n",
+ suggestions: []
+ },
+ {
+ key: "+",
+ suggestions: [{label: "p +button+p"}]
+ }
+];
+
+add_task(function* () {
+ let { inspector } = yield openInspectorForURL(TEST_URL);
+ let searchBox = inspector.searchBox;
+ let popup = inspector.searchSuggestions.searchPopup;
+
+ yield focusSearchBoxUsingShortcut(inspector.panelWin);
+
+ for (let { key, suggestions } of TEST_DATA) {
+ info("Pressing " + key + " to get " + formatSuggestions(suggestions));
+
+ let command = once(searchBox, "input");
+ EventUtils.synthesizeKey(key, {}, inspector.panelWin);
+ yield command;
+
+ info("Waiting for search query to complete");
+ yield inspector.searchSuggestions._lastQuery;
+
+ info("Query completed. Performing checks for input '" + searchBox.value +
+ "' - key pressed: " + key);
+ let actualSuggestions = popup.getItems().reverse();
+
+ is(popup.isOpen ? actualSuggestions.length : 0, suggestions.length,
+ "There are expected number of suggestions.");
+
+ for (let i = 0; i < suggestions.length; i++) {
+ is(actualSuggestions[i].label, suggestions[i].label,
+ "The suggestion at " + i + "th index is correct.");
+ }
+ }
+});
+
+function formatSuggestions(suggestions) {
+ return "[" + suggestions
+ .map(s => "'" + s.label + "'")
+ .join(", ") + "]";
+}
diff --git a/devtools/client/inspector/test/browser_inspector_search-03.js b/devtools/client/inspector/test/browser_inspector_search-03.js
new file mode 100644
index 000000000..215b536a6
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_search-03.js
@@ -0,0 +1,250 @@
+/* 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";
+
+// Testing that searching for elements using the inspector search field
+// produces correct suggestions.
+
+const TEST_URL = URL_ROOT + "doc_inspector_search.html";
+
+// An array of (key, suggestions) pairs where key is a key to press and
+// suggestions is an array of suggestions that should be shown in the popup.
+// Suggestion is an object with label of the entry and optional count
+// (defaults to 1)
+var TEST_DATA = [
+ {
+ key: "d",
+ suggestions: [
+ {label: "div"},
+ {label: "#d1"},
+ {label: "#d2"}
+ ]
+ },
+ {
+ key: "i",
+ suggestions: [{label: "div"}]
+ },
+ {
+ key: "v",
+ suggestions: []
+ },
+ {
+ key: ".",
+ suggestions: [{label: "div.c1"}]
+ },
+ {
+ key: "VK_BACK_SPACE",
+ suggestions: []
+ },
+ {
+ key: "#",
+ suggestions: [
+ {label: "div#d1"},
+ {label: "div#d2"}
+ ]
+ },
+ {
+ key: "VK_BACK_SPACE",
+ suggestions: []
+ },
+ {
+ key: "VK_BACK_SPACE",
+ suggestions: [{label: "div"}]
+ },
+ {
+ key: "VK_BACK_SPACE",
+ suggestions: [
+ {label: "div"},
+ {label: "#d1"},
+ {label: "#d2"}
+ ]
+ },
+ {
+ key: "VK_BACK_SPACE",
+ suggestions: []
+ },
+ {
+ key: ".",
+ suggestions: [
+ {label: ".c1"},
+ {label: ".c2"}
+ ]
+ },
+ {
+ key: "c",
+ suggestions: [
+ {label: ".c1"},
+ {label: ".c2"}
+ ]
+ },
+ {
+ key: "2",
+ suggestions: []
+ },
+ {
+ key: "VK_BACK_SPACE",
+ suggestions: [
+ {label: ".c1"},
+ {label: ".c2"}
+ ]
+ },
+ {
+ key: "1",
+ suggestions: []
+ },
+ {
+ key: "#",
+ suggestions: [
+ {label: "#d2"},
+ {label: "#p1"},
+ {label: "#s2"}
+ ]
+ },
+ {
+ key: "VK_BACK_SPACE",
+ suggestions: []
+ },
+ {
+ key: "VK_BACK_SPACE",
+ suggestions: [
+ {label: ".c1"},
+ {label: ".c2"}
+ ]
+ },
+ {
+ key: "VK_BACK_SPACE",
+ suggestions: [
+ {label: ".c1"},
+ {label: ".c2"}
+ ]
+ },
+ {
+ key: "VK_BACK_SPACE",
+ suggestions: []
+ },
+ {
+ key: "#",
+ suggestions: [
+ {label: "#b1"},
+ {label: "#d1"},
+ {label: "#d2"},
+ {label: "#p1"},
+ {label: "#p2"},
+ {label: "#p3"},
+ {label: "#s1"},
+ {label: "#s2"}
+ ]
+ },
+ {
+ key: "p",
+ suggestions: [
+ {label: "#p1"},
+ {label: "#p2"},
+ {label: "#p3"}
+ ]
+ },
+ {
+ key: "VK_BACK_SPACE",
+ suggestions: [
+ {label: "#b1"},
+ {label: "#d1"},
+ {label: "#d2"},
+ {label: "#p1"},
+ {label: "#p2"},
+ {label: "#p3"},
+ {label: "#s1"},
+ {label: "#s2"}
+ ]
+ },
+ {
+ key: "VK_BACK_SPACE",
+ suggestions: []
+ },
+ {
+ key: "p",
+ suggestions: [
+ {label: "p"},
+ {label: "#p1"},
+ {label: "#p2"},
+ {label: "#p3"}
+ ]
+ },
+ {
+ key: "[", suggestions: []
+ },
+ {
+ key: "i", suggestions: []
+ },
+ {
+ key: "d", suggestions: []
+ },
+ {
+ key: "*", suggestions: []
+ },
+ {
+ key: "=", suggestions: []
+ },
+ {
+ key: "p", suggestions: []
+ },
+ {
+ key: "]", suggestions: []
+ },
+ {
+ key: ".",
+ suggestions: [
+ {label: "p[id*=p].c1"},
+ {label: "p[id*=p].c2"}
+ ]
+ },
+ {
+ key: "VK_BACK_SPACE",
+ suggestions: []
+ },
+ {
+ key: "#",
+ suggestions: [
+ {label: "p[id*=p]#p1"},
+ {label: "p[id*=p]#p2"},
+ {label: "p[id*=p]#p3"}
+ ]
+ }
+];
+
+add_task(function* () {
+ let { inspector } = yield openInspectorForURL(TEST_URL);
+ let searchBox = inspector.searchBox;
+ let popup = inspector.searchSuggestions.searchPopup;
+
+ yield focusSearchBoxUsingShortcut(inspector.panelWin);
+
+ for (let { key, suggestions } of TEST_DATA) {
+ info("Pressing " + key + " to get " + formatSuggestions(suggestions));
+
+ let command = once(searchBox, "input");
+ EventUtils.synthesizeKey(key, {}, inspector.panelWin);
+ yield command;
+
+ info("Waiting for search query to complete");
+ yield inspector.searchSuggestions._lastQuery;
+
+ info("Query completed. Performing checks for input '" +
+ searchBox.value + "'");
+ let actualSuggestions = popup.getItems().reverse();
+
+ is(popup.isOpen ? actualSuggestions.length : 0, suggestions.length,
+ "There are expected number of suggestions.");
+
+ for (let i = 0; i < suggestions.length; i++) {
+ is(actualSuggestions[i].label, suggestions[i].label,
+ "The suggestion at " + i + "th index is correct.");
+ }
+ }
+});
+
+function formatSuggestions(suggestions) {
+ return "[" + suggestions
+ .map(s => "'" + s.label + "'")
+ .join(", ") + "]";
+}
diff --git a/devtools/client/inspector/test/browser_inspector_search-04.js b/devtools/client/inspector/test/browser_inspector_search-04.js
new file mode 100644
index 000000000..a5aee8156
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_search-04.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";
+
+// Testing that searching for elements inside iframes does work.
+
+const IFRAME_SRC = "doc_inspector_search.html";
+const TEST_URL = "data:text/html;charset=utf-8," +
+ "<div class=\"c1 c2\">" +
+ "<iframe src=\"" + URL_ROOT + IFRAME_SRC + "\"></iframe>" +
+ "<iframe src=\"" + URL_ROOT + IFRAME_SRC + "\"></iframe>";
+
+// An array of (key, suggestions) pairs where key is a key to press and
+// suggestions is an array of suggestions that should be shown in the popup.
+// Suggestion is an object with label of the entry and optional count
+// (defaults to 1)
+var TEST_DATA = [
+ {
+ key: "d",
+ suggestions: [
+ {label: "div"},
+ {label: "#d1"},
+ {label: "#d2"}
+ ]
+ },
+ {
+ key: "i",
+ suggestions: [{label: "div"}]
+ },
+ {
+ key: "v",
+ suggestions: []
+ },
+ {
+ key: "VK_BACK_SPACE",
+ suggestions: [{label: "div"}]
+ },
+ {
+ key: "VK_BACK_SPACE",
+ suggestions: [
+ {label: "div"},
+ {label: "#d1"},
+ {label: "#d2"}
+ ]
+ },
+ {
+ key: "VK_BACK_SPACE",
+ suggestions: []
+ },
+ {
+ key: ".",
+ suggestions: [
+ {label: ".c1"},
+ {label: ".c2"}
+ ]
+ },
+ {
+ key: "VK_BACK_SPACE",
+ suggestions: []
+ },
+ {
+ key: "#",
+ suggestions: [
+ {label: "#b1"},
+ {label: "#d1"},
+ {label: "#d2"},
+ {label: "#p1"},
+ {label: "#p2"},
+ {label: "#p3"},
+ {label: "#s1"},
+ {label: "#s2"}
+ ]
+ },
+];
+
+add_task(function* () {
+ let {inspector} = yield openInspectorForURL(TEST_URL);
+ let searchBox = inspector.searchBox;
+ let popup = inspector.searchSuggestions.searchPopup;
+
+ yield focusSearchBoxUsingShortcut(inspector.panelWin);
+
+ for (let {key, suggestions} of TEST_DATA) {
+ info("Pressing " + key + " to get " + formatSuggestions(suggestions));
+
+ let command = once(searchBox, "input");
+ EventUtils.synthesizeKey(key, {}, inspector.panelWin);
+ yield command;
+
+ info("Waiting for search query to complete");
+ yield inspector.searchSuggestions._lastQuery;
+
+ info("Query completed. Performing checks for input '" +
+ searchBox.value + "'");
+ let actualSuggestions = popup.getItems().reverse();
+
+ is(popup.isOpen ? actualSuggestions.length : 0, suggestions.length,
+ "There are expected number of suggestions.");
+
+ for (let i = 0; i < suggestions.length; i++) {
+ is(actualSuggestions[i].label, suggestions[i].label,
+ "The suggestion at " + i + "th index is correct.");
+ }
+ }
+});
+
+function formatSuggestions(suggestions) {
+ return "[" + suggestions
+ .map(s => "'" + s.label + "'")
+ .join(", ") + "]";
+}
diff --git a/devtools/client/inspector/test/browser_inspector_search-05.js b/devtools/client/inspector/test/browser_inspector_search-05.js
new file mode 100644
index 000000000..542d0ccc5
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_search-05.js
@@ -0,0 +1,93 @@
+/* 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";
+
+// Testing that when search results contain suggestions for nodes in other
+// frames, selecting these suggestions actually selects the right nodes.
+
+requestLongerTimeout(2);
+
+const IFRAME_SRC = "doc_inspector_search.html";
+const NESTED_IFRAME_SRC = `
+ <button id="b1">Nested button</button>
+ <iframe id="iframe-4" src="${URL_ROOT + IFRAME_SRC}"></iframe>
+`;
+const TEST_URL = `
+ <iframe id="iframe-1" src="${URL_ROOT + IFRAME_SRC}"></iframe>
+ <iframe id="iframe-2" src="${URL_ROOT + IFRAME_SRC}"></iframe>
+ <iframe id="iframe-3"
+ src="data:text/html;charset=utf-8,${encodeURI(NESTED_IFRAME_SRC)}">
+ </iframe>
+`;
+
+add_task(function* () {
+ let {inspector} = yield openInspectorForURL(
+ "data:text/html;charset=utf-8," + encodeURI(TEST_URL));
+
+ info("Focus the search box");
+ yield focusSearchBoxUsingShortcut(inspector.panelWin);
+
+ info("Enter # to search for all ids");
+ let processingDone = once(inspector.searchSuggestions, "processing-done");
+ EventUtils.synthesizeKey("#", {}, inspector.panelWin);
+ yield processingDone;
+
+ info("Wait for search query to complete");
+ yield inspector.searchSuggestions._lastQuery;
+
+ info("Press tab to fill the search input with the first suggestion");
+ processingDone = once(inspector.searchSuggestions, "processing-done");
+ EventUtils.synthesizeKey("VK_TAB", {}, inspector.panelWin);
+ yield processingDone;
+
+ info("Press enter and expect a new selection");
+ let onSelect = inspector.once("inspector-updated");
+ EventUtils.synthesizeKey("VK_RETURN", {}, inspector.panelWin);
+ yield onSelect;
+
+ yield checkCorrectButton(inspector, "#iframe-1");
+
+ info("Press enter to cycle through multiple nodes matching this suggestion");
+ onSelect = inspector.once("inspector-updated");
+ EventUtils.synthesizeKey("VK_RETURN", {}, inspector.panelWin);
+ yield onSelect;
+
+ yield checkCorrectButton(inspector, "#iframe-2");
+
+ info("Press enter to cycle through multiple nodes matching this suggestion");
+ onSelect = inspector.once("inspector-updated");
+ EventUtils.synthesizeKey("VK_RETURN", {}, inspector.panelWin);
+ yield onSelect;
+
+ yield checkCorrectButton(inspector, "#iframe-3");
+
+ info("Press enter to cycle through multiple nodes matching this suggestion");
+ onSelect = inspector.once("inspector-updated");
+ EventUtils.synthesizeKey("VK_RETURN", {}, inspector.panelWin);
+ yield onSelect;
+
+ yield checkCorrectButton(inspector, "#iframe-4");
+
+ info("Press enter to cycle through multiple nodes matching this suggestion");
+ onSelect = inspector.once("inspector-updated");
+ EventUtils.synthesizeKey("VK_RETURN", {}, inspector.panelWin);
+ yield onSelect;
+
+ yield checkCorrectButton(inspector, "#iframe-1");
+});
+
+let checkCorrectButton = Task.async(function* (inspector, frameSelector) {
+ let {walker} = inspector;
+ let node = inspector.selection.nodeFront;
+
+ ok(node.id, "b1", "The selected node is #b1");
+ ok(node.tagName.toLowerCase(), "button",
+ "The selected node is <button>");
+
+ let selectedNodeDoc = yield walker.document(node);
+ let iframe = yield walker.multiFrameQuerySelectorAll(frameSelector);
+ iframe = yield iframe.item(0);
+ let iframeDoc = (yield walker.children(iframe)).nodes[0];
+ is(selectedNodeDoc, iframeDoc, "The selected node is in " + frameSelector);
+});
diff --git a/devtools/client/inspector/test/browser_inspector_search-06.js b/devtools/client/inspector/test/browser_inspector_search-06.js
new file mode 100644
index 000000000..1b3950c00
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_search-06.js
@@ -0,0 +1,87 @@
+/* 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";
+
+// Check that searching again for nodes after they are removed or added from the
+// DOM works correctly.
+
+const TEST_URL = URL_ROOT + "doc_inspector_search.html";
+
+add_task(function* () {
+ let { inspector, testActor } = yield openInspectorForURL(TEST_URL);
+
+ info("Searching for test node #d1");
+ yield focusSearchBoxUsingShortcut(inspector.panelWin);
+ yield synthesizeKeys(["#", "d", "1", "VK_RETURN"], inspector);
+
+ yield inspector.search.once("search-result");
+ assertHasResult(inspector, true);
+
+ info("Removing node #d1");
+ // Expect an inspector-updated event here, because removing #d1 causes the
+ // breadcrumbs to update (since #d1 is displayed in it).
+ let onUpdated = inspector.once("inspector-updated");
+ yield mutatePage(inspector, testActor,
+ "document.getElementById(\"d1\").remove()");
+ yield onUpdated;
+
+ info("Pressing return button to search again for node #d1.");
+ yield synthesizeKeys("VK_RETURN", inspector);
+
+ yield inspector.search.once("search-result");
+ assertHasResult(inspector, false);
+
+ info("Emptying the field and searching for a node that doesn't exist: #d3");
+ let keys = ["VK_BACK_SPACE", "VK_BACK_SPACE", "VK_BACK_SPACE", "#", "d", "3",
+ "VK_RETURN"];
+ yield synthesizeKeys(keys, inspector);
+
+ yield inspector.search.once("search-result");
+ assertHasResult(inspector, false);
+
+ info("Create the #d3 node in the page");
+ // No need to expect an inspector-updated event here, Creating #d3 isn't going
+ // to update the breadcrumbs in any ways.
+ yield mutatePage(inspector, testActor,
+ `document.getElementById("d2").insertAdjacentHTML(
+ "afterend", "<div id=d3></div>")`);
+
+ info("Pressing return button to search again for node #d3.");
+ yield synthesizeKeys("VK_RETURN", inspector);
+
+ yield inspector.search.once("search-result");
+ assertHasResult(inspector, true);
+
+ // Catch-all event for remaining server requests when searching for the new
+ // node.
+ yield inspector.once("inspector-updated");
+});
+
+function* synthesizeKeys(keys, inspector) {
+ if (typeof keys === "string") {
+ keys = [keys];
+ }
+
+ for (let key of keys) {
+ info("Synthesizing key " + key + " in the search box");
+ let eventHandled = once(inspector.searchBox, "keypress", true);
+ EventUtils.synthesizeKey(key, {}, inspector.panelWin);
+ yield eventHandled;
+ info("Waiting for the search query to complete");
+ yield inspector.searchSuggestions._lastQuery;
+ }
+}
+
+function assertHasResult(inspector, expectResult) {
+ is(inspector.searchBox.classList.contains("devtools-style-searchbox-no-match"),
+ !expectResult,
+ "There are" + (expectResult ? "" : " no") + " search results");
+}
+
+function* mutatePage(inspector, testActor, expression) {
+ let onMutation = inspector.once("markupmutation");
+ yield testActor.eval(expression);
+ yield onMutation;
+}
diff --git a/devtools/client/inspector/test/browser_inspector_search-07.js b/devtools/client/inspector/test/browser_inspector_search-07.js
new file mode 100644
index 000000000..79e2021cd
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_search-07.js
@@ -0,0 +1,49 @@
+/* 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 searching for classes on SVG elements does work (see bug 1219920).
+
+const TEST_URL = URL_ROOT + "doc_inspector_search-svg.html";
+
+// An array of (key, suggestions) pairs where key is a key to press and
+// suggestions is an array of suggestions that should be shown in the popup.
+const TEST_DATA = [{
+ key: "c",
+ suggestions: ["circle", "clipPath", ".class1", ".class2"]
+}, {
+ key: "VK_BACK_SPACE",
+ suggestions: []
+}, {
+ key: ".",
+ suggestions: [".class1", ".class2"]
+}];
+
+add_task(function* () {
+ let {inspector} = yield openInspectorForURL(TEST_URL);
+ let {searchBox} = inspector;
+ let popup = inspector.searchSuggestions.searchPopup;
+
+ yield focusSearchBoxUsingShortcut(inspector.panelWin);
+
+ for (let {key, suggestions} of TEST_DATA) {
+ info("Pressing " + key + " to get " + suggestions);
+
+ let command = once(searchBox, "input");
+ EventUtils.synthesizeKey(key, {}, inspector.panelWin);
+ yield command;
+
+ info("Waiting for search query to complete and getting the suggestions");
+ yield inspector.searchSuggestions._lastQuery;
+ let actualSuggestions = popup.getItems().reverse();
+
+ is(popup.isOpen ? actualSuggestions.length : 0, suggestions.length,
+ "There are expected number of suggestions.");
+
+ for (let i = 0; i < suggestions.length; i++) {
+ is(actualSuggestions[i].label, suggestions[i],
+ "The suggestion at " + i + "th index is correct.");
+ }
+ }
+});
diff --git a/devtools/client/inspector/test/browser_inspector_search-08.js b/devtools/client/inspector/test/browser_inspector_search-08.js
new file mode 100644
index 000000000..f5c77fcac
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_search-08.js
@@ -0,0 +1,64 @@
+/* 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 searching for namespaced elements does work.
+
+const XHTML = `
+ <!DOCTYPE html>
+ <html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:svg="http://www.w3.org/2000/svg">
+ <body>
+ <svg:svg width="100" height="100">
+ <svg:clipPath>
+ <svg:rect x="0" y="0" width="10" height="5"></svg:rect>
+ </svg:clipPath>
+ <svg:circle cx="0" cy="0" r="5"></svg:circle>
+ </svg:svg>
+ </body>
+ </html>
+`;
+
+const TEST_URI = "data:application/xhtml+xml;charset=utf-8," + encodeURI(XHTML);
+
+// An array of (key, suggestions) pairs where key is a key to press and
+// suggestions is an array of suggestions that should be shown in the popup.
+const TEST_DATA = [{
+ key: "c",
+ suggestions: ["circle", "clipPath"]
+}, {
+ key: "VK_BACK_SPACE",
+ suggestions: []
+}, {
+ key: "s",
+ suggestions: ["svg"]
+}];
+
+add_task(function* () {
+ let {inspector} = yield openInspectorForURL(TEST_URI);
+ let {searchBox} = inspector;
+ let popup = inspector.searchSuggestions.searchPopup;
+
+ yield focusSearchBoxUsingShortcut(inspector.panelWin);
+
+ for (let {key, suggestions} of TEST_DATA) {
+ info("Pressing " + key + " to get " + suggestions.join(", "));
+
+ let command = once(searchBox, "input");
+ EventUtils.synthesizeKey(key, {}, inspector.panelWin);
+ yield command;
+
+ info("Waiting for search query to complete and getting the suggestions");
+ yield inspector.searchSuggestions._lastQuery;
+ let actualSuggestions = popup.getItems().reverse();
+
+ is(popup.isOpen ? actualSuggestions.length : 0, suggestions.length,
+ "There are expected number of suggestions.");
+
+ for (let i = 0; i < suggestions.length; i++) {
+ is(actualSuggestions[i].label, suggestions[i],
+ "The suggestion at " + i + "th index is correct.");
+ }
+ }
+});
diff --git a/devtools/client/inspector/test/browser_inspector_search-clear.js b/devtools/client/inspector/test/browser_inspector_search-clear.js
new file mode 100644
index 000000000..4388c70a6
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_search-clear.js
@@ -0,0 +1,52 @@
+/* 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";
+
+// Bug 1295081 Test searchbox clear button's display behavior is correct
+
+const XHTML = `
+ <!DOCTYPE html>
+ <html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:svg="http://www.w3.org/2000/svg">
+ <body>
+ <svg:svg width="100" height="100">
+ <svg:clipPath>
+ <svg:rect x="0" y="0" width="10" height="5"></svg:rect>
+ </svg:clipPath>
+ <svg:circle cx="0" cy="0" r="5"></svg:circle>
+ </svg:svg>
+ </body>
+ </html>
+`;
+
+const TEST_URI = "data:application/xhtml+xml;charset=utf-8," + encodeURI(XHTML);
+
+// Type "d" in inspector-searchbox, Enter [Back space] key and check if the
+// clear button is shown correctly
+add_task(function* () {
+ let {inspector} = yield openInspectorForURL(TEST_URI);
+ let {searchBox, searchClearButton} = inspector;
+
+ yield focusSearchBoxUsingShortcut(inspector.panelWin);
+
+ info("Type d and the clear button will be shown");
+
+ let command = once(searchBox, "input");
+ EventUtils.synthesizeKey("c", {}, inspector.panelWin);
+ yield command;
+
+ info("Waiting for search query to complete and getting the suggestions");
+ yield inspector.searchSuggestions._lastQuery;
+
+ ok(!searchClearButton.hidden,
+ "The clear button is shown when some word is in searchBox");
+
+ EventUtils.synthesizeKey("VK_BACK_SPACE", {}, inspector.panelWin);
+ yield command;
+
+ info("Waiting for search query to complete and getting the suggestions");
+ yield inspector.searchSuggestions._lastQuery;
+
+ ok(searchClearButton.hidden, "The clear button is hidden when no word is in searchBox");
+});
diff --git a/devtools/client/inspector/test/browser_inspector_search-filter_context-menu.js b/devtools/client/inspector/test/browser_inspector_search-filter_context-menu.js
new file mode 100644
index 000000000..137456468
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_search-filter_context-menu.js
@@ -0,0 +1,82 @@
+/* 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 inspector's markup view search filter context menu works properly.
+
+const TEST_INPUT = "h1";
+const TEST_URI = "<h1>test filter context menu</h1>";
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {toolbox, inspector} = yield openInspector();
+ let {searchBox} = inspector;
+ yield selectNode("h1", inspector);
+
+ let win = inspector.panelWin;
+ let searchContextMenu = toolbox.textBoxContextMenuPopup;
+ ok(searchContextMenu,
+ "The search filter context menu is loaded in the inspector");
+
+ let cmdUndo = searchContextMenu.querySelector("[command=cmd_undo]");
+ let cmdDelete = searchContextMenu.querySelector("[command=cmd_delete]");
+ let cmdSelectAll = searchContextMenu.querySelector("[command=cmd_selectAll]");
+ let cmdCut = searchContextMenu.querySelector("[command=cmd_cut]");
+ let cmdCopy = searchContextMenu.querySelector("[command=cmd_copy]");
+ let cmdPaste = searchContextMenu.querySelector("[command=cmd_paste]");
+
+ emptyClipboard();
+
+ info("Opening context menu");
+ let onFocus = once(searchBox, "focus");
+ searchBox.focus();
+ yield onFocus;
+
+ let onContextMenuPopup = once(searchContextMenu, "popupshowing");
+ EventUtils.synthesizeMouse(searchBox, 2, 2,
+ {type: "contextmenu", button: 2}, win);
+ yield onContextMenuPopup;
+
+ is(cmdUndo.getAttribute("disabled"), "true", "cmdUndo is disabled");
+ is(cmdDelete.getAttribute("disabled"), "true", "cmdDelete is disabled");
+ is(cmdSelectAll.getAttribute("disabled"), "true", "cmdSelectAll is disabled");
+
+ // Cut/Copy items are enabled in context menu even if there
+ // is no selection. See also Bug 1303033
+ is(cmdCut.getAttribute("disabled"), "", "cmdCut is enabled");
+ is(cmdCopy.getAttribute("disabled"), "", "cmdCopy is enabled");
+
+ if (isWindows()) {
+ // emptyClipboard only works on Windows (666254), assert paste only for this OS.
+ is(cmdPaste.getAttribute("disabled"), "true", "cmdPaste is disabled");
+ }
+
+ info("Closing context menu");
+ let onContextMenuHidden = once(searchContextMenu, "popuphidden");
+ searchContextMenu.hidePopup();
+ yield onContextMenuHidden;
+
+ info("Copy text in search field using the context menu");
+ searchBox.value = TEST_INPUT;
+ searchBox.select();
+ searchBox.focus();
+ EventUtils.synthesizeMouse(searchBox, 2, 2,
+ {type: "contextmenu", button: 2}, win);
+ yield onContextMenuPopup;
+ yield waitForClipboardPromise(() => cmdCopy.click(), TEST_INPUT);
+ searchContextMenu.hidePopup();
+ yield onContextMenuHidden;
+
+ info("Reopen context menu and check command properties");
+ EventUtils.synthesizeMouse(searchBox, 2, 2,
+ {type: "contextmenu", button: 2}, win);
+ yield onContextMenuPopup;
+
+ is(cmdUndo.getAttribute("disabled"), "", "cmdUndo is enabled");
+ is(cmdDelete.getAttribute("disabled"), "", "cmdDelete is enabled");
+ is(cmdSelectAll.getAttribute("disabled"), "", "cmdSelectAll is enabled");
+ is(cmdCut.getAttribute("disabled"), "", "cmdCut is enabled");
+ is(cmdCopy.getAttribute("disabled"), "", "cmdCopy is enabled");
+ is(cmdPaste.getAttribute("disabled"), "", "cmdPaste is enabled");
+});
diff --git a/devtools/client/inspector/test/browser_inspector_search-label.js b/devtools/client/inspector/test/browser_inspector_search-label.js
new file mode 100644
index 000000000..669ad79b8
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_search-label.js
@@ -0,0 +1,33 @@
+/* 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";
+
+// Check that search label updated correctcly based on the search result.
+
+const TEST_URL = URL_ROOT + "doc_inspector_search.html";
+
+add_task(function* () {
+ let { inspector } = yield openInspectorForURL(TEST_URL);
+ let { panelWin, searchResultsLabel } = inspector;
+
+ info("Searching for test node #d1");
+ // Expect the label shows 1 result
+ yield focusSearchBoxUsingShortcut(panelWin);
+ synthesizeKeys("#d1", panelWin);
+ EventUtils.synthesizeKey("VK_RETURN", {}, panelWin);
+
+ yield inspector.search.once("search-result");
+ is(searchResultsLabel.textContent, "1 of 1");
+
+ info("Click the clear button");
+ // Expect the label is cleared after clicking the clear button.
+
+ inspector.searchClearButton.click();
+ is(searchResultsLabel.textContent, "");
+
+ // Catch-all event for remaining server requests when searching for the new
+ // node.
+ yield inspector.once("inspector-updated");
+});
diff --git a/devtools/client/inspector/test/browser_inspector_search-navigation.js b/devtools/client/inspector/test/browser_inspector_search-navigation.js
new file mode 100644
index 000000000..bf409fcc7
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_search-navigation.js
@@ -0,0 +1,76 @@
+/* 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 searchbox value is correct when suggestions popup is navigated
+// with keyboard.
+
+// Test data as pairs of [key to press, expected content of searchbox].
+const KEY_STATES = [
+ ["d", "d"],
+ ["i", "di"],
+ ["v", "div"],
+ [".", "div."],
+ ["VK_UP", "div.c1"],
+ ["VK_DOWN", "div.l1"],
+ ["VK_BACK_SPACE", "div.l"],
+ ["VK_TAB", "div.l1"],
+ [" ", "div.l1 "],
+ ["VK_UP", "div.l1 div"],
+ ["VK_UP", "div.l1 span"],
+ ["VK_UP", "div.l1 div"],
+ [".", "div.l1 div."],
+ ["VK_TAB", "div.l1 div.c1"],
+ ["VK_BACK_SPACE", "div.l1 div.c"],
+ ["VK_BACK_SPACE", "div.l1 div."],
+ ["VK_BACK_SPACE", "div.l1 div"],
+ ["VK_BACK_SPACE", "div.l1 di"],
+ ["VK_BACK_SPACE", "div.l1 d"],
+ ["VK_BACK_SPACE", "div.l1 "],
+ ["VK_UP", "div.l1 div"],
+ ["VK_BACK_SPACE", "div.l1 di"],
+ ["VK_BACK_SPACE", "div.l1 d"],
+ ["VK_BACK_SPACE", "div.l1 "],
+ ["VK_UP", "div.l1 div"],
+ ["VK_UP", "div.l1 span"],
+ ["VK_UP", "div.l1 div"],
+ ["VK_TAB", "div.l1 div"],
+ ["VK_BACK_SPACE", "div.l1 di"],
+ ["VK_BACK_SPACE", "div.l1 d"],
+ ["VK_BACK_SPACE", "div.l1 "],
+ ["VK_DOWN", "div.l1 div"],
+ ["VK_DOWN", "div.l1 span"],
+ ["VK_BACK_SPACE", "div.l1 spa"],
+ ["VK_BACK_SPACE", "div.l1 sp"],
+ ["VK_BACK_SPACE", "div.l1 s"],
+ ["VK_BACK_SPACE", "div.l1 "],
+ ["VK_BACK_SPACE", "div.l1"],
+ ["VK_BACK_SPACE", "div.l"],
+ ["VK_BACK_SPACE", "div."],
+ ["VK_BACK_SPACE", "div"],
+ ["VK_BACK_SPACE", "di"],
+ ["VK_BACK_SPACE", "d"],
+ ["VK_BACK_SPACE", ""],
+];
+
+const TEST_URL = URL_ROOT +
+ "doc_inspector_search-suggestions.html";
+
+add_task(function* () {
+ let { inspector } = yield openInspectorForURL(TEST_URL);
+ yield focusSearchBoxUsingShortcut(inspector.panelWin);
+
+ for (let [key, query] of KEY_STATES) {
+ info("Pressing key " + key + " to get searchbox value as " + query);
+
+ let done = inspector.searchSuggestions.once("processing-done");
+ EventUtils.synthesizeKey(key, {}, inspector.panelWin);
+ yield done;
+
+ info("Waiting for search query to complete");
+ yield inspector.searchSuggestions._lastQuery;
+
+ is(inspector.searchBox.value, query, "The searchbox value is correct");
+ }
+});
diff --git a/devtools/client/inspector/test/browser_inspector_search-reserved.js b/devtools/client/inspector/test/browser_inspector_search-reserved.js
new file mode 100644
index 000000000..e8141eb08
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_search-reserved.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";
+
+// Testing searching for ids and classes that contain reserved characters.
+const TEST_URL = URL_ROOT + "doc_inspector_search-reserved.html";
+
+// An array of (key, suggestions) pairs where key is a key to press and
+// suggestions is an array of suggestions that should be shown in the popup.
+// Suggestion is an object with label of the entry and optional count
+// (defaults to 1)
+const TEST_DATA = [
+ {
+ key: "#",
+ suggestions: [{label: "#d1\\.d2"}]
+ },
+ {
+ key: "d",
+ suggestions: [{label: "#d1\\.d2"}]
+ },
+ {
+ key: "VK_BACK_SPACE",
+ suggestions: [{label: "#d1\\.d2"}]
+ },
+ {
+ key: "VK_BACK_SPACE",
+ suggestions: []
+ },
+ {
+ key: ".",
+ suggestions: [{label: ".c1\\.c2"}]
+ },
+ {
+ key: "c",
+ suggestions: [{label: ".c1\\.c2"}]
+ },
+ {
+ key: "VK_BACK_SPACE",
+ suggestions: [{label: ".c1\\.c2"}]
+ },
+ {
+ key: "VK_BACK_SPACE",
+ suggestions: []
+ },
+ {
+ key: "d",
+ suggestions: [{label: "div"},
+ {label: "#d1\\.d2"}]
+ },
+ {
+ key: "VK_BACK_SPACE",
+ suggestions: []
+ },
+ {
+ key: "c",
+ suggestions: [{label: ".c1\\.c2"}]
+ },
+ {
+ key: "VK_BACK_SPACE",
+ suggestions: []
+ },
+ {
+ key: "b",
+ suggestions: [{label: "body"}]
+ },
+ {
+ key: "o",
+ suggestions: [{label: "body"}]
+ },
+ {
+ key: "d",
+ suggestions: [{label: "body"}]
+ },
+ {
+ key: "y",
+ suggestions: []
+ },
+ {
+ key: " ",
+ suggestions: [{label: "body div"}]
+ },
+ {
+ key: ".",
+ suggestions: [{label: "body .c1\\.c2"}]
+ },
+ {
+ key: "VK_BACK_SPACE",
+ suggestions: [{label: "body div"}]
+ },
+ {
+ key: "#",
+ suggestions: [{label: "body #d1\\.d2"}]
+ }
+];
+
+add_task(function* () {
+ let { inspector } = yield openInspectorForURL(TEST_URL);
+ let searchBox = inspector.searchBox;
+ let popup = inspector.searchSuggestions.searchPopup;
+
+ yield focusSearchBoxUsingShortcut(inspector.panelWin);
+
+ for (let { key, suggestions } of TEST_DATA) {
+ info("Pressing " + key + " to get " + formatSuggestions(suggestions));
+
+ let command = once(searchBox, "input");
+ EventUtils.synthesizeKey(key, {}, inspector.panelWin);
+ yield command;
+
+ info("Waiting for search query to complete");
+ yield inspector.searchSuggestions._lastQuery;
+
+ info("Query completed. Performing checks for input '" +
+ searchBox.value + "'");
+ let actualSuggestions = popup.getItems().reverse();
+
+ is(popup.isOpen ? actualSuggestions.length : 0, suggestions.length,
+ "There are expected number of suggestions.");
+
+ for (let i = 0; i < suggestions.length; i++) {
+ is(suggestions[i].label, actualSuggestions[i].label,
+ "The suggestion at " + i + "th index is correct.");
+ }
+ }
+});
+
+function formatSuggestions(suggestions) {
+ return "[" + suggestions
+ .map(s => "'" + s.label + "'")
+ .join(", ") + "]";
+}
diff --git a/devtools/client/inspector/test/browser_inspector_search-selection.js b/devtools/client/inspector/test/browser_inspector_search-selection.js
new file mode 100644
index 000000000..99f1e34bb
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_search-selection.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";
+
+// Testing navigation between nodes in search results
+const {AppConstants} = require("resource://gre/modules/AppConstants.jsm");
+
+const TEST_URL = URL_ROOT + "doc_inspector_search.html";
+
+add_task(function* () {
+ let {inspector} = yield openInspectorForURL(TEST_URL);
+
+ info("Focus the search box");
+ yield focusSearchBoxUsingShortcut(inspector.panelWin);
+
+ info("Enter body > p to search");
+ let processingDone = once(inspector.searchSuggestions, "processing-done");
+ EventUtils.sendString("body > p", inspector.panelWin);
+ yield processingDone;
+
+ info("Wait for search query to complete");
+ yield inspector.searchSuggestions._lastQuery;
+
+ let msg = "Press enter and expect a new selection";
+ yield sendKeyAndCheck(inspector, msg, "VK_RETURN", {}, "#p1");
+
+ msg = "Press enter to cycle through multiple nodes";
+ yield sendKeyAndCheck(inspector, msg, "VK_RETURN", {}, "#p2");
+
+ msg = "Press shift-enter to select the previous node";
+ yield sendKeyAndCheck(inspector, msg, "VK_RETURN", { shiftKey: true }, "#p1");
+
+ if (AppConstants.platform === "macosx") {
+ msg = "Press meta-g to cycle through multiple nodes";
+ yield sendKeyAndCheck(inspector, msg, "VK_G", { metaKey: true }, "#p2");
+
+ msg = "Press shift+meta-g to select the previous node";
+ yield sendKeyAndCheck(inspector, msg, "VK_G",
+ { metaKey: true, shiftKey: true }, "#p1");
+ } else {
+ msg = "Press ctrl-g to cycle through multiple nodes";
+ yield sendKeyAndCheck(inspector, msg, "VK_G", { ctrlKey: true }, "#p2");
+
+ msg = "Press shift+ctrl-g to select the previous node";
+ yield sendKeyAndCheck(inspector, msg, "VK_G",
+ { ctrlKey: true, shiftKey: true }, "#p1");
+ }
+});
+
+let sendKeyAndCheck = Task.async(function* (inspector, description, key,
+ modifiers, expectedId) {
+ info(description);
+ let onSelect = inspector.once("inspector-updated");
+ EventUtils.synthesizeKey(key, modifiers, inspector.panelWin);
+ yield onSelect;
+
+ let selectedNode = inspector.selection.nodeFront;
+ info(selectedNode.id + " is selected with text " + inspector.searchBox.value);
+ let targetNode = yield getNodeFront(expectedId, inspector);
+ is(selectedNode, targetNode, "Correct node " + expectedId + " is selected");
+});
diff --git a/devtools/client/inspector/test/browser_inspector_search-sidebar.js b/devtools/client/inspector/test/browser_inspector_search-sidebar.js
new file mode 100644
index 000000000..d65a670ac
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_search-sidebar.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";
+
+// Test that depending where the user last clicked in the inspector, the right search
+// field is focused when ctrl+F is pressed.
+
+add_task(function* () {
+ let {inspector} = yield openInspectorForURL("data:text/html;charset=utf-8,Search!");
+
+ info("Check that by default, the inspector search field gets focused");
+ pressCtrlF();
+ isInInspectorSearchBox(inspector);
+
+ info("Click somewhere in the rule-view");
+ clickInRuleView(inspector);
+
+ info("Check that the rule-view search field gets focused");
+ pressCtrlF();
+ isInRuleViewSearchBox(inspector);
+
+ info("Click in the inspector again");
+ yield clickContainer("head", inspector);
+
+ info("Check that now we're back in the inspector, its search field gets focused");
+ pressCtrlF();
+ isInInspectorSearchBox(inspector);
+
+ info("Switch to the computed view, and click somewhere inside it");
+ selectComputedView(inspector);
+ clickInComputedView(inspector);
+
+ info("Check that the computed-view search field gets focused");
+ pressCtrlF();
+ isInComputedViewSearchBox(inspector);
+
+ info("Click in the inspector yet again");
+ yield clickContainer("body", inspector);
+
+ info("We're back in the inspector again, check the inspector search field focuses");
+ pressCtrlF();
+ isInInspectorSearchBox(inspector);
+});
+
+function pressCtrlF() {
+ EventUtils.synthesizeKey("f", {accelKey: true});
+}
+
+function clickInRuleView(inspector) {
+ let el = inspector.panelDoc.querySelector("#sidebar-panel-ruleview");
+ EventUtils.synthesizeMouseAtCenter(el, {}, inspector.panelDoc.defaultView);
+}
+
+function clickInComputedView(inspector) {
+ let el = inspector.panelDoc.querySelector("#sidebar-panel-computedview");
+ EventUtils.synthesizeMouseAtCenter(el, {}, inspector.panelDoc.defaultView);
+}
+
+function isInInspectorSearchBox(inspector) {
+ // Focus ends up in an anonymous child of the XUL textbox.
+ ok(inspector.panelDoc.activeElement.closest("#inspector-searchbox"),
+ "The inspector search field is focused when ctrl+F is pressed");
+}
+
+function isInRuleViewSearchBox(inspector) {
+ is(inspector.panelDoc.activeElement, inspector.ruleview.view.searchField,
+ "The rule-view search field is focused when ctrl+F is pressed");
+}
+
+function isInComputedViewSearchBox(inspector) {
+ is(inspector.panelDoc.activeElement, inspector.computedview.computedView.searchField,
+ "The computed-view search field is focused when ctrl+F is pressed");
+}
diff --git a/devtools/client/inspector/test/browser_inspector_search-suggests-ids-and-classes.js b/devtools/client/inspector/test/browser_inspector_search-suggests-ids-and-classes.js
new file mode 100644
index 000000000..b20c72342
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_search-suggests-ids-and-classes.js
@@ -0,0 +1,84 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that the selector-search input proposes ids and classes even when . and
+// # is missing, but that this only occurs when the query is one word (no
+// selector combination)
+
+// The various states of the inspector: [key, suggestions array]
+// [
+// what key to press,
+// suggestions array with count [
+// [suggestion1, count1], [suggestion2] ...
+// ] count can be left to represent 1
+// ]
+const KEY_STATES = [
+ ["s", [["span", 1], [".span", 1], ["#span", 1]]],
+ ["p", [["span", 1], [".span", 1], ["#span", 1]]],
+ ["a", [["span", 1], [".span", 1], ["#span", 1]]],
+ ["n", []],
+ [" ", [["span div", 1]]],
+ // mixed tag/class/id suggestions only work for the first word
+ ["d", [["span div", 1]]],
+ ["VK_BACK_SPACE", [["span div", 1]]],
+ ["VK_BACK_SPACE", []],
+ ["VK_BACK_SPACE", [["span", 1], [".span", 1], ["#span", 1]]],
+ ["VK_BACK_SPACE", [["span", 1], [".span", 1], ["#span", 1]]],
+ ["VK_BACK_SPACE", [["span", 1], [".span", 1], ["#span", 1]]],
+ ["VK_BACK_SPACE", []],
+ // Test that mixed tags, classes and ids are grouped by types, sorted by
+ // count and alphabetical order
+ ["b", [
+ ["button", 3],
+ ["body", 1],
+ [".bc", 3],
+ [".ba", 1],
+ [".bb", 1],
+ ["#ba", 1],
+ ["#bb", 1],
+ ["#bc", 1]
+ ]],
+];
+
+const TEST_URL = `<span class="span" id="span">
+ <div class="div" id="div"></div>
+ </span>
+ <button class="ba bc" id="bc"></button>
+ <button class="bb bc" id="bb"></button>
+ <button class="bc" id="ba"></button>`;
+
+add_task(function* () {
+ let {inspector} = yield openInspectorForURL("data:text/html;charset=utf-8," +
+ encodeURI(TEST_URL));
+
+ let searchBox = inspector.panelWin.document.getElementById(
+ "inspector-searchbox");
+ let popup = inspector.searchSuggestions.searchPopup;
+
+ yield focusSearchBoxUsingShortcut(inspector.panelWin);
+
+ for (let [key, expectedSuggestions] of KEY_STATES) {
+ info("pressing key " + key + " to get suggestions " +
+ JSON.stringify(expectedSuggestions));
+
+ let onCommand = once(searchBox, "input", true);
+ EventUtils.synthesizeKey(key, {}, inspector.panelWin);
+ yield onCommand;
+
+ info("Waiting for the suggestions to be retrieved");
+ yield inspector.searchSuggestions._lastQuery;
+
+ let actualSuggestions = popup.getItems();
+ is(popup.isOpen ? actualSuggestions.length : 0, expectedSuggestions.length,
+ "There are expected number of suggestions");
+ actualSuggestions.reverse();
+
+ for (let i = 0; i < expectedSuggestions.length; i++) {
+ is(expectedSuggestions[i][0], actualSuggestions[i].label,
+ "The suggestion at " + i + "th index is correct.");
+ is(expectedSuggestions[i][1] || 1, actualSuggestions[i].count,
+ "The count for suggestion at " + i + "th index is correct.");
+ }
+ }
+});
diff --git a/devtools/client/inspector/test/browser_inspector_search_keyboard_trap.js b/devtools/client/inspector/test/browser_inspector_search_keyboard_trap.js
new file mode 100644
index 000000000..391d812a2
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_search_keyboard_trap.js
@@ -0,0 +1,94 @@
+/* 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 ability to tab to and away from inspector search using keyboard.
+
+const TEST_URL = URL_ROOT + "doc_inspector_search.html";
+
+/**
+ * Test data has the format of:
+ * {
+ * desc {String} description for better logging
+ * focused {Boolean} flag, indicating if search box contains focus
+ * keys: {Array} list of keys that include key code and optional
+ * event data (shiftKey, etc)
+ * }
+ *
+ */
+const TEST_DATA = [
+ {
+ desc: "Move focus to a next focusable element",
+ focused: false,
+ keys: [
+ {
+ key: "VK_TAB",
+ options: { }
+ }
+ ]
+ },
+ {
+ desc: "Move focus back to searchbox",
+ focused: true,
+ keys: [
+ {
+ key: "VK_TAB",
+ options: { shiftKey: true }
+ }
+ ]
+ },
+ {
+ desc: "Open popup and then tab away (2 times) to the a next focusable " +
+ "element",
+ focused: false,
+ keys: [
+ {
+ key: "d",
+ options: { }
+ },
+ {
+ key: "VK_TAB",
+ options: { }
+ },
+ {
+ key: "VK_TAB",
+ options: { }
+ }
+ ]
+ },
+ {
+ desc: "Move focus back to searchbox",
+ focused: true,
+ keys: [
+ {
+ key: "VK_TAB",
+ options: { shiftKey: true }
+ }
+ ]
+ }
+];
+
+add_task(function* () {
+ let { inspector } = yield openInspectorForURL(TEST_URL);
+ let { searchBox } = inspector;
+ let doc = inspector.panelDoc;
+
+ yield selectNode("#b1", inspector);
+ yield focusSearchBoxUsingShortcut(inspector.panelWin);
+
+ // Ensure a searchbox is focused.
+ ok(containsFocus(doc, searchBox), "Focus is in a searchbox");
+
+ for (let { desc, focused, keys } of TEST_DATA) {
+ info(desc);
+ for (let { key, options } of keys) {
+ let done = !focused ?
+ inspector.searchSuggestions.once("processing-done") : Promise.resolve();
+ EventUtils.synthesizeKey(key, options);
+ yield done;
+ }
+ is(containsFocus(doc, searchBox), focused, "Focus is set correctly");
+ }
+});
diff --git a/devtools/client/inspector/test/browser_inspector_select-docshell.js b/devtools/client/inspector/test/browser_inspector_select-docshell.js
new file mode 100644
index 000000000..6a801fdea
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_select-docshell.js
@@ -0,0 +1,86 @@
+/* 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";
+
+// Test frame selection switching at toolbox level
+// when using the inspector
+
+const FrameURL = "data:text/html;charset=UTF-8," +
+ encodeURI("<div id=\"frame\">frame</div>");
+const URL = "data:text/html;charset=UTF-8," +
+ encodeURI('<iframe src="' + FrameURL +
+ '"></iframe><div id="top">top</div>');
+
+add_task(function* () {
+ Services.prefs.setBoolPref("devtools.command-button-frames.enabled", true);
+
+ let {inspector, toolbox, testActor} = yield openInspectorForURL(URL);
+
+ // Verify we are on the top level document
+ ok((yield testActor.hasNode("#top")),
+ "We have the test node on the top level document");
+
+ assertMarkupViewIsLoaded(inspector);
+
+ // Verify that the frame map button is empty at the moment.
+ let btn = toolbox.doc.getElementById("command-button-frames");
+ ok(!btn.firstChild, "The frame list button doesn't have any children");
+
+ // Open frame menu and wait till it's available on the screen.
+ let menu = toolbox.showFramesMenu({target: btn});
+ yield once(menu, "open");
+
+ // Verify that the menu is popuplated.
+ let frames = menu.items.slice();
+ is(frames.length, 2, "We have both frames in the menu");
+
+ frames.sort(function (a, b) {
+ return a.label.localeCompare(b.label);
+ });
+
+ is(frames[0].label, FrameURL, "Got top level document in the list");
+ is(frames[1].label, URL, "Got iframe document in the list");
+
+ // Listen to will-navigate to check if the view is empty
+ let willNavigate = toolbox.target.once("will-navigate").then(() => {
+ info("Navigation to the iframe has started, the inspector should be empty");
+ assertMarkupViewIsEmpty(inspector);
+ });
+
+ // Only select the iframe after we are able to select an element from the top
+ // level document.
+ let newRoot = inspector.once("new-root");
+ yield selectNode("#top", inspector);
+ info("Select the iframe");
+ frames[0].click();
+
+ yield willNavigate;
+ yield newRoot;
+
+ info("Navigation to the iframe is done, the inspector should be back up");
+
+ // Verify we are on page one
+ ok(!(yield testActor.hasNode("iframe")),
+ "We not longer have access to the top frame elements");
+ ok((yield testActor.hasNode("#frame")),
+ "But now have direct access to the iframe elements");
+
+ // On page 2 load, verify we have the right content
+ assertMarkupViewIsLoaded(inspector);
+
+ yield selectNode("#frame", inspector);
+
+ Services.prefs.clearUserPref("devtools.command-button-frames.enabled");
+});
+
+function assertMarkupViewIsLoaded(inspector) {
+ let markupViewBox = inspector.panelDoc.getElementById("markup-box");
+ is(markupViewBox.childNodes.length, 1, "The markup-view is loaded");
+}
+
+function assertMarkupViewIsEmpty(inspector) {
+ let markupViewBox = inspector.panelDoc.getElementById("markup-box");
+ is(markupViewBox.childNodes.length, 0, "The markup-view is unloaded");
+}
diff --git a/devtools/client/inspector/test/browser_inspector_select-last-selected.js b/devtools/client/inspector/test/browser_inspector_select-last-selected.js
new file mode 100644
index 000000000..0f2050327
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_select-last-selected.js
@@ -0,0 +1,95 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set 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";
+
+requestLongerTimeout(2);
+
+// Checks that the expected default node is selected after a page navigation or
+// a reload.
+var PAGE_1 = URL_ROOT + "doc_inspector_select-last-selected-01.html";
+var PAGE_2 = URL_ROOT + "doc_inspector_select-last-selected-02.html";
+
+// An array of test cases with following properties:
+// - url: URL to navigate to. If URL == content.location, reload instead.
+// - nodeToSelect: a selector for a node to select before navigation. If null,
+// whatever is selected stays selected.
+// - selectedNode: a selector for a node that is selected after navigation.
+var TEST_DATA = [
+ {
+ url: PAGE_1,
+ nodeToSelect: "#id1",
+ selectedNode: "#id1"
+ },
+ {
+ url: PAGE_1,
+ nodeToSelect: "#id2",
+ selectedNode: "#id2"
+ },
+ {
+ url: PAGE_1,
+ nodeToSelect: "#id3",
+ selectedNode: "#id3"
+ },
+ {
+ url: PAGE_1,
+ nodeToSelect: "#id4",
+ selectedNode: "#id4"
+ },
+ {
+ url: PAGE_2,
+ nodeToSelect: null,
+ selectedNode: "body"
+ },
+ {
+ url: PAGE_1,
+ nodeToSelect: "#id5",
+ selectedNode: "body"
+ },
+ {
+ url: PAGE_2,
+ nodeToSelect: null,
+ selectedNode: "body"
+ }
+];
+
+add_task(function* () {
+ let { inspector, toolbox, testActor } = yield openInspectorForURL(PAGE_1);
+
+ for (let { url, nodeToSelect, selectedNode } of TEST_DATA) {
+ if (nodeToSelect) {
+ info("Selecting node " + nodeToSelect + " before navigation.");
+ yield selectNode(nodeToSelect, inspector);
+ }
+
+ yield navigateToAndWaitForNewRoot(url);
+
+ let nodeFront = yield getNodeFront(selectedNode, inspector);
+ ok(nodeFront, "Got expected node front");
+ is(inspector.selection.nodeFront, nodeFront,
+ selectedNode + " is selected after navigation.");
+ }
+
+ function* navigateToAndWaitForNewRoot(url) {
+ info("Navigating and waiting for new-root event after navigation.");
+
+ let current = yield testActor.eval("location.href");
+ if (url == current) {
+ info("Reloading page.");
+ let markuploaded = inspector.once("markuploaded");
+ let onNewRoot = inspector.once("new-root");
+ let onUpdated = inspector.once("inspector-updated");
+
+ let activeTab = toolbox.target.activeTab;
+ yield activeTab.reload();
+ info("Waiting for inspector to be ready.");
+ yield markuploaded;
+ yield onNewRoot;
+ yield onUpdated;
+ } else {
+ yield navigateTo(inspector, url);
+ }
+ }
+});
diff --git a/devtools/client/inspector/test/browser_inspector_sidebarstate.js b/devtools/client/inspector/test/browser_inspector_sidebarstate.js
new file mode 100644
index 000000000..a2bb764c1
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_sidebarstate.js
@@ -0,0 +1,38 @@
+/* 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 TEST_URI = "data:text/html;charset=UTF-8," +
+ "<h1>browser_inspector_sidebarstate.js</h1>";
+
+add_task(function* () {
+ let { inspector, toolbox } = yield openInspectorForURL(TEST_URI);
+
+ info("Selecting ruleview.");
+ inspector.sidebar.select("ruleview");
+
+ is(inspector.sidebar.getCurrentTabID(), "ruleview",
+ "Rule View is selected by default");
+
+ info("Selecting computed view.");
+ inspector.sidebar.select("computedview");
+
+ // Finish initialization of the computed panel before
+ // destroying the toolbox.
+ yield waitForTick();
+
+ info("Closing inspector.");
+ yield toolbox.destroy();
+
+ info("Re-opening inspector.");
+ inspector = (yield openInspector()).inspector;
+
+ if (!inspector.sidebar.getCurrentTabID()) {
+ info("Default sidebar still to be selected, adding select listener.");
+ yield inspector.sidebar.once("select");
+ }
+
+ is(inspector.sidebar.getCurrentTabID(), "computedview",
+ "Computed view is selected by default.");
+});
diff --git a/devtools/client/inspector/test/browser_inspector_switch-to-inspector-on-pick.js b/devtools/client/inspector/test/browser_inspector_switch-to-inspector-on-pick.js
new file mode 100644
index 000000000..53b2892ac
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_switch-to-inspector-on-pick.js
@@ -0,0 +1,39 @@
+/* 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";
+
+// Testing that clicking the pick button switches the toolbox to the inspector
+// panel.
+
+const TEST_URI = "data:text/html;charset=UTF-8," +
+ "<p>Switch to inspector on pick</p>";
+
+add_task(function* () {
+ let tab = yield addTab(TEST_URI);
+ let toolbox = yield openToolbox(tab);
+
+ yield startPickerAndAssertSwitchToInspector(toolbox);
+
+ info("Stoppping element picker.");
+ yield toolbox.highlighterUtils.stopPicker();
+});
+
+function openToolbox(tab) {
+ info("Opening webconsole.");
+ let target = TargetFactory.forTab(tab);
+ return gDevTools.showToolbox(target, "webconsole");
+}
+
+function* startPickerAndAssertSwitchToInspector(toolbox) {
+ info("Clicking element picker button.");
+ let pickButton = toolbox.doc.querySelector("#command-button-pick");
+ pickButton.click();
+
+ info("Waiting for inspector to be selected.");
+ yield toolbox.once("inspector-selected");
+ is(toolbox.currentToolId, "inspector", "Switched to the inspector");
+
+ info("Waiting for inspector to update.");
+ yield toolbox.getCurrentPanel().once("inspector-updated");
+}
diff --git a/devtools/client/inspector/test/browser_inspector_textbox-menu.js b/devtools/client/inspector/test/browser_inspector_textbox-menu.js
new file mode 100644
index 000000000..74190229f
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_textbox-menu.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";
+
+// Test that when right-clicking on various text boxes throughout the inspector does use
+// the toolbox's context menu (copy/cut/paste/selectAll/Undo).
+
+add_task(function* () {
+ yield addTab(`data:text/html;charset=utf-8,
+ <style>h1 { color: red; }</style>
+ <h1 id="title">textbox context menu test</h1>`);
+ let {toolbox, inspector} = yield openInspector();
+ yield selectNode("h1", inspector);
+
+ info("Testing the markup-view tagname");
+ let container = yield focusNode("h1", inspector);
+ let tag = container.editor.tag;
+ tag.focus();
+ EventUtils.sendKey("return", inspector.panelWin);
+ yield checkTextBox(inspector.markup.doc.activeElement, toolbox);
+
+ info("Testing the markup-view attribute");
+ EventUtils.sendKey("tab", inspector.panelWin);
+ yield checkTextBox(inspector.markup.doc.activeElement, toolbox);
+
+ info("Testing the markup-view new attribute");
+ // It takes 2 tabs to focus the newAttr field, the first one just moves the cursor to
+ // the end of the field.
+ EventUtils.sendKey("tab", inspector.panelWin);
+ EventUtils.sendKey("tab", inspector.panelWin);
+ yield checkTextBox(inspector.markup.doc.activeElement, toolbox);
+
+ info("Testing the markup-view textcontent");
+ EventUtils.sendKey("tab", inspector.panelWin);
+ yield checkTextBox(inspector.markup.doc.activeElement, toolbox);
+ // Blur this last markup-view field, since we're moving on to the rule-view next.
+ EventUtils.sendKey("escape", inspector.panelWin);
+
+ info("Testing the rule-view selector");
+ let ruleView = inspector.ruleview.view;
+ let cssRuleEditor = getRuleViewRuleEditor(ruleView, 1);
+ EventUtils.synthesizeMouse(cssRuleEditor.selectorText, 0, 0, {}, inspector.panelWin);
+ yield checkTextBox(inspector.panelDoc.activeElement, toolbox);
+
+ info("Testing the rule-view property name");
+ EventUtils.sendKey("tab", inspector.panelWin);
+ yield checkTextBox(inspector.panelDoc.activeElement, toolbox);
+
+ info("Testing the rule-view property value");
+ EventUtils.sendKey("tab", inspector.panelWin);
+ yield checkTextBox(inspector.panelDoc.activeElement, toolbox);
+
+ info("Testing the rule-view new property");
+ // Tabbing out of the value field triggers a ruleview-changed event that we need to wait
+ // for.
+ let onRuleViewChanged = once(ruleView, "ruleview-changed");
+ EventUtils.sendKey("tab", inspector.panelWin);
+ yield onRuleViewChanged;
+ yield checkTextBox(inspector.panelDoc.activeElement, toolbox);
+
+ info("Switching to the computed-view");
+ let onComputedViewReady = inspector.once("boxmodel-view-updated");
+ selectComputedView(inspector);
+ yield onComputedViewReady;
+
+ info("Testing the box-model region");
+ let margin = inspector.panelDoc.querySelector(".boxmodel-margin.boxmodel-top > span");
+ EventUtils.synthesizeMouseAtCenter(margin, {}, inspector.panelWin);
+ yield checkTextBox(inspector.panelDoc.activeElement, toolbox);
+});
+
+function* checkTextBox(textBox, {textBoxContextMenuPopup}) {
+ is(textBoxContextMenuPopup.state, "closed", "The menu is closed");
+
+ info("Simulating context click on the textbox and expecting the menu to open");
+ let onContextMenu = once(textBoxContextMenuPopup, "popupshown");
+ EventUtils.synthesizeMouse(textBox, 2, 2, {type: "contextmenu", button: 2},
+ textBox.ownerDocument.defaultView);
+ yield onContextMenu;
+
+ is(textBoxContextMenuPopup.state, "open", "The menu is now visible");
+
+ info("Closing the menu");
+ let onContextMenuHidden = once(textBoxContextMenuPopup, "popuphidden");
+ textBoxContextMenuPopup.hidePopup();
+ yield onContextMenuHidden;
+
+ is(textBoxContextMenuPopup.state, "closed", "The menu is closed again");
+}
diff --git a/devtools/client/inspector/test/doc_inspector_add_node.html b/devtools/client/inspector/test/doc_inspector_add_node.html
new file mode 100644
index 000000000..d024b2a99
--- /dev/null
+++ b/devtools/client/inspector/test/doc_inspector_add_node.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <title>Add elements tests</title>
+ <style>
+ body::before {
+ content: "pseudo-element";
+ }
+ </style>
+</head>
+<body>
+ <div id="foo"></div>
+ <svg>
+ <rect x="0" y="0" width="100" height="50"></rect>
+ </svg>
+ <div id="bar">
+ <div id="baz"></div>
+ </div>
+ <iframe src="data:text/html;charset=utf-8,Test iframe content"></iframe>
+</body>
+</html>
diff --git a/devtools/client/inspector/test/doc_inspector_breadcrumbs.html b/devtools/client/inspector/test/doc_inspector_breadcrumbs.html
new file mode 100644
index 000000000..fee063611
--- /dev/null
+++ b/devtools/client/inspector/test/doc_inspector_breadcrumbs.html
@@ -0,0 +1,75 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <style>
+ div {
+ min-height: 10px; min-width: 10px;
+ border: 1px solid red;
+ margin: 10px;
+ }
+ #pseudo-container::before {
+ content: 'before';
+ }
+ #pseudo-container::after {
+ content: 'after';
+ }
+ </style>
+ </head>
+ <body>
+ <article id="i1">
+ <div id="i11">
+ <div id="i111">
+ <div id="i1111">
+ </div>
+ </div>
+ </div>
+ </article>
+ <article id="i2">
+ <div id="i21">
+ <div id="i211">
+ <div id="i2111">
+ </div>
+ </div>
+ </div>
+ <div id="i22">
+ <div id="i221">
+ </div>
+ <div id="i222">
+ <div id="i2221">
+ <div id="i22211">
+ </div>
+ </div>
+ </div>
+ </div>
+ </article>
+ <article id="i3">
+ <link id="i31" />
+ <link />
+ <link />
+ <link />
+ <link />
+ <link />
+ <link />
+ <link />
+ <link />
+ <link />
+ <link />
+ <link />
+ <link />
+ <link />
+ <link />
+ <link />
+ <link />
+ <link />
+ <link />
+ </article>
+ <div id='pseudo-container'></div>
+ <!-- This is a comment node -->
+ <svg id="vector" viewBox="0 0 10 10">
+ <clipPath id="clip">
+ <rect id="rectangle" x="0" y="0" width="10" height="5"></rect>
+ </clipPath>
+ <circle cx="5" cy="5" r="5" fill="blue" clip-path="url(#clip)"></circle>
+ </svg>
+ </body>
+</html>
diff --git a/devtools/client/inspector/test/doc_inspector_breadcrumbs_visibility.html b/devtools/client/inspector/test/doc_inspector_breadcrumbs_visibility.html
new file mode 100644
index 000000000..862f32407
--- /dev/null
+++ b/devtools/client/inspector/test/doc_inspector_breadcrumbs_visibility.html
@@ -0,0 +1,22 @@
+<html>
+ <head>
+ <meta http-equiv="content-type" content="text/html; charset=windows-1252">
+ </head>
+ <body>
+ <div id="aVeryLongIdToExceedTheBreadcrumbTruncationLimit">
+ <div id="anotherVeryLongIdToExceedTheBreadcrumbTruncationLimit">
+ <div id="aThirdVeryLongIdToExceedTheTruncationLimit">
+ <div id="aFourthOneToExceedTheTruncationLimit">
+ <div id="aFifthOneToExceedTheTruncationLimit">
+ <div id="aSixthOneToExceedTheTruncationLimit">
+ <div id="aSeventhOneToExceedTheTruncationLimit">
+ A text node at the end
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </body>
+</html>
diff --git a/devtools/client/inspector/test/doc_inspector_csp.html b/devtools/client/inspector/test/doc_inspector_csp.html
new file mode 100644
index 000000000..49af7e53b
--- /dev/null
+++ b/devtools/client/inspector/test/doc_inspector_csp.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Inspector CSP Test</title>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ This HTTP response has CSP headers.
+ </body>
+</html>
diff --git a/devtools/client/inspector/test/doc_inspector_csp.html^headers^ b/devtools/client/inspector/test/doc_inspector_csp.html^headers^
new file mode 100644
index 000000000..3345a82b8
--- /dev/null
+++ b/devtools/client/inspector/test/doc_inspector_csp.html^headers^
@@ -0,0 +1,2 @@
+Content-Type: text/html; charset=UTF-8
+content-security-policy: default-src 'self'; connect-src 'self'; script-src 'self'; style-src 'self';
diff --git a/devtools/client/inspector/test/doc_inspector_delete-selected-node-01.html b/devtools/client/inspector/test/doc_inspector_delete-selected-node-01.html
new file mode 100644
index 000000000..70edbd936
--- /dev/null
+++ b/devtools/client/inspector/test/doc_inspector_delete-selected-node-01.html
@@ -0,0 +1,4 @@
+<!DOCTYPE html>
+
+<h1>mop</h1>
+<iframe src="data:text/html;charset=utf-8,<!DOCTYPE HTML>%0D%0A<h1>kill me<span>.</span><%2Fh1>"></iframe>
diff --git a/devtools/client/inspector/test/doc_inspector_delete-selected-node-02.html b/devtools/client/inspector/test/doc_inspector_delete-selected-node-02.html
new file mode 100644
index 000000000..0749b064a
--- /dev/null
+++ b/devtools/client/inspector/test/doc_inspector_delete-selected-node-02.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <title>node delete - reset selection - test</title>
+</head>
+<body>
+ <ul id="deleteChildren">
+ <li id="deleteManually">Delete me via the inspector</li>
+ <li id="selectedAfterDelete">This node is selected after manual delete</li>
+ <li id="deleteAutomatically">Delete me via javascript</li>
+ </ul>
+ <iframe id="deleteIframe" src="data:text/html,%3C!DOCTYPE%20html%3E%3Chtml%20lang%3D%22en%22%3E%3Cbody%3E%3Cp%20id%3D%22deleteInIframe%22%3EDelete my container iframe%3C%2Fp%3E%3C%2Fbody%3E%3C%2Fhtml%3E"></iframe>
+ <div id="deleteToMakeSingleTextNode">
+ 1
+ <b id="deleteWithNonElement">Delete me and select the non-element node</b>
+ 2
+ </div>
+</body>
+</html>
diff --git a/devtools/client/inspector/test/doc_inspector_embed.html b/devtools/client/inspector/test/doc_inspector_embed.html
new file mode 100644
index 000000000..1d286ade0
--- /dev/null
+++ b/devtools/client/inspector/test/doc_inspector_embed.html
@@ -0,0 +1,6 @@
+<!doctype html><html><head><meta charset="UTF-8"></head><body>
+<object>
+ <embed src="doc_inspector_menu.html" type="application/html"
+ width="422" height="258"></embed>
+</object>
+</body></html>
diff --git a/devtools/client/inspector/test/doc_inspector_gcli-inspect-command.html b/devtools/client/inspector/test/doc_inspector_gcli-inspect-command.html
new file mode 100644
index 000000000..a7d28828c
--- /dev/null
+++ b/devtools/client/inspector/test/doc_inspector_gcli-inspect-command.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <title>GCLI inspect command test</title>
+</head>
+<body>
+
+ <!-- This is a list of 0 h1 elements -->
+
+ <!-- This is a list of 1 div elements -->
+ <div>Hello, I'm a div</div>
+
+ <!-- This is a list of 2 span elements -->
+ <span>Hello, I'm a span</span>
+ <span>And me</span>
+
+ <!-- This is a collection of various things that match only once -->
+ <p class="someclass">.someclass</p>
+ <p id="someid">#someid</p>
+ <button disabled>button[disabled]</button>
+ <p><strong>p&gt;strong</strong></p>
+
+</body>
+</html>
diff --git a/devtools/client/inspector/test/doc_inspector_highlight_after_transition.html b/devtools/client/inspector/test/doc_inspector_highlight_after_transition.html
new file mode 100644
index 000000000..b2ba0b066
--- /dev/null
+++ b/devtools/client/inspector/test/doc_inspector_highlight_after_transition.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+
+ <style>
+ div {
+ opacity: 0;
+ height: 0;
+ background: red;
+ border-top: 1px solid #888;
+ transition-property: height, opacity;
+ transition-duration: 3000ms;
+ transition-timing-function: ease-in-out, ease-in-out, linear;
+ }
+
+ div[visible] {
+ opacity: 1;
+ height: 200px;
+ }
+ </style>
+</head>
+<body>
+ <div></div>
+</body>
+</html>
diff --git a/devtools/client/inspector/test/doc_inspector_highlighter-comments.html b/devtools/client/inspector/test/doc_inspector_highlighter-comments.html
new file mode 100644
index 000000000..3dedc9f36
--- /dev/null
+++ b/devtools/client/inspector/test/doc_inspector_highlighter-comments.html
@@ -0,0 +1,19 @@
+<!doctype html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <title>Inspector Highlighter Test</title>
+</head>
+<body>
+ <p></p>
+ <div id="id1">Visible div 1</div>
+ <!-- Invisible comment node -->
+ <div id="id2">Visible div 2</div>
+ <script type="text/javascript">
+ /* Invisible script node */
+ </script>
+ <div id="id3">Visible div 3</div>
+ <div id="id4" style="display:none;">Invisible div node</div>
+ Visible text node
+</body>
+</html>
diff --git a/devtools/client/inspector/test/doc_inspector_highlighter-geometry_01.html b/devtools/client/inspector/test/doc_inspector_highlighter-geometry_01.html
new file mode 100644
index 000000000..f05f15deb
--- /dev/null
+++ b/devtools/client/inspector/test/doc_inspector_highlighter-geometry_01.html
@@ -0,0 +1,90 @@
+<!doctype html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <title>geometry highlighter test page</title>
+ <style type="text/css">
+ html, body {
+ margin: 0;
+ padding: 0;
+ }
+
+ .relative-sized-parent {
+ position: relative;
+ border: 2px solid black;
+ border-radius: 25px;
+ }
+ .size {
+ width: 300px;
+ height: 300px;
+ }
+
+ .positioned-child {
+ position: absolute;
+ background: #f06;
+ }
+ .pos-top-left {
+ top: 30px;
+ left: 25%;
+ }
+ .pos-bottom-right {
+ bottom: 10em;
+ right: -10px;
+ }
+
+ .inline-positioned {
+ background: yellow;
+ }
+
+ #absolute-container {
+ position: absolute;
+ top: 50px;
+ left: 400px;
+ width: 500px;
+ height: 400px;
+ border: 1px solid black;
+ }
+
+ .absolute-all-4 {
+ position: absolute;
+ top: 10px;
+ left: 10px;
+ bottom: 200px;
+ right: 300px;
+ border: 1px solid red;
+ }
+
+ .relative {
+ position: relative;
+ top: 10%;
+ left: 50%;
+ height: 10px;
+ border: 1px solid blue;
+ }
+
+ .fixed {
+ position: fixed;
+ top: 400px;
+ left: 0;
+ width: 50px;
+ height: 50px;
+ border-radius: 50%;
+ background: green;
+ }
+ </style>
+</head>
+<body>
+ <div id="node1" class="relative-sized-parent size">
+ <div id="node2" class="positioned-child pos-top-left pos-bottom-right">
+ <div id="node3" class="inline-positioned positioned-child pos-top-left" style="width:50px;height:50px;"></div>
+ </div>
+ </div>
+
+ <div id="absolute-container">
+ <div class="absolute-all-4"></div>
+ <div class="relative"></div>
+ </div>
+
+ <div class="fixed"></div>
+</body>
+</html>
diff --git a/devtools/client/inspector/test/doc_inspector_highlighter-geometry_02.html b/devtools/client/inspector/test/doc_inspector_highlighter-geometry_02.html
new file mode 100644
index 000000000..4392c9042
--- /dev/null
+++ b/devtools/client/inspector/test/doc_inspector_highlighter-geometry_02.html
@@ -0,0 +1,120 @@
+<!doctype html><html><head><meta charset="UTF-8"></head><body class="header">
+
+<style>
+.fixed { position: fixed; top: 40px; right: 20px; margin-top: 20px; background: #ccf; }
+.fixed-bottom-right { position: fixed; bottom: 4em; right: 25%; margin: 20px; background: #ccf; }
+
+#absolute-container { position: relative; height: 150px; margin: 20px; }
+.absolute { position: absolute; top: 20px; left: 400px; background: #fcc; }
+.absolute-bottom-right { position: absolute; bottom: 20px; right: 50px; background: #fcc; }
+.absolute-all-4 { position: absolute; top: 100px; bottom: 10px; left: 20px; right: 700px; background: #fcc; }
+.absolute-negative { position: absolute; bottom: -25px; background: #fcc; }
+.absolute-width-margin { position: absolute; top: 20px; right: 20px; width: 450px; margin: .3em; padding: 10px; border: 2px solid red; box-sizing: border-box; background: #fcc; }
+
+.relative { position: relative; top: 10px; left: 10px; background: #cfc;}
+.relative-inline { position: relative; top: 10px; left: 10px; display: inline; background: #cfc;}
+
+.static { position: static; top: 10px; left: 10px; background: #fcf; }
+.static-size { position: static; top: 10px; left: 10px; width: 300px; height: 100px; background: #fcf; }
+
+#sticky-container {
+ margin: 50px;
+ height: 400px;
+ width: 400px;
+ padding: 40px;
+ overflow: scroll;
+}
+#sticky-container dl {
+ margin: 0;
+ padding: 24px 0 0 0;
+}
+
+#sticky-container dt {
+ background: #ffc;
+ border-bottom: 1px solid #989EA4;
+ border-top: 1px solid #717D85;
+ color: #FFF;
+ font: bold 18px/21px Helvetica, Arial, sans-serif;
+ margin: 0;
+ padding: 2px 0 0 12px;
+ position: sticky;
+ width: 99%;
+ top: 0px;
+}
+
+#sticky-container dd {
+ font: bold 20px/45px Helvetica, Arial, sans-serif;
+ margin: 0;
+ padding: 0 0 0 12px;
+ white-space: nowrap;
+}
+
+#sticky-container dd + dd {
+ border-top: 1px solid #CCC
+}
+</style>
+
+<h1>Positioning playground</h1>
+<p>A demo of various positioning schemes: <a href="http://dev.w3.org/csswg/css-position/#pos-sch">http://dev.w3.org/csswg/css-position/#pos-sch</a>.</p>
+<p>absolute, static, fixed, relative, sticky</p>
+
+<h2>Absolute positioning</h2>
+<div class="absolute">
+ Absolute child with no relative parent
+</div>
+<div id="absolute-container">
+ <div class="absolute">
+ Absolute child with a relative parent
+ </div>
+ <div class="absolute-bottom-right">
+ Absolute child with a relative parent, positioned from the bottom right
+ </div>
+ <div class="absolute-all-4">
+ Absolute child with a relative parent, with all 4 positions
+ </div>
+ <div class="absolute-negative">
+ Absolute child with a relative parent, with negative positions
+ </div>
+ <div class="absolute-width-margin">
+ Absolute child with a relative parent, size, margin
+ </div>
+</div>
+
+<h2>Relative positioning</h2>
+<div id="relative-container">
+ <div class="relative">
+ Relative child
+ </div>
+ <div style="width: 100px;">
+ <div class="relative-inline">
+ Relative inline child, across multiple lines
+ </div>
+ </div>
+ <div style="position:relative;">
+ <div class="relative">
+ Relative child, in a positioned parent
+ </div>
+ </div>
+</div>
+
+<h2>Fixed positioning</h2>
+<div id="fixed-container">
+ <div class="fixed">
+ Fixed child
+ </div>
+ <div class="fixed-bottom-right">
+ Fixed child, bottom right
+ </div>
+</div>
+
+<h2>Static positioning</h2>
+<div id="static-container">
+ <div class="static">
+ Static child with no width/height
+ </div>
+ <div class="static-size">
+ Static child with width/height
+ </div>
+</div>
+
+</body></html> \ No newline at end of file
diff --git a/devtools/client/inspector/test/doc_inspector_highlighter.html b/devtools/client/inspector/test/doc_inspector_highlighter.html
new file mode 100644
index 000000000..376a9c714
--- /dev/null
+++ b/devtools/client/inspector/test/doc_inspector_highlighter.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <style>
+ div {
+ position:absolute;
+ }
+
+ #simple-div {
+ padding: 5px;
+ border: 7px solid red;
+ margin: 9px;
+ top: 30px;
+ left: 150px;
+ }
+
+ #rotated-div {
+ padding: 5px;
+ border: 7px solid red;
+ margin: 9px;
+ transform: rotate(45deg);
+ top: 30px;
+ left: 80px;
+ }
+
+ #widthHeightZero-div {
+ top: 30px;
+ left: 10px;
+ width: 0;
+ height: 0;
+ }
+ </style>
+ </head>
+ <body>
+ <div id="simple-div">Gort! Klaatu barada nikto!</div>
+ <div id="rotated-div"></div>
+ <div id="widthHeightZero-div">Width &amp; height = 0</div>
+ </body>
+</html>
diff --git a/devtools/client/inspector/test/doc_inspector_highlighter_csstransform.html b/devtools/client/inspector/test/doc_inspector_highlighter_csstransform.html
new file mode 100644
index 000000000..cfa2761d7
--- /dev/null
+++ b/devtools/client/inspector/test/doc_inspector_highlighter_csstransform.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>css transform highlighter test</title>
+ <style type="text/css">
+ #test-node {
+ position: absolute;
+ top: 0;
+ left: 0;
+
+ width: 300px;
+ height: 300px;
+
+ transform: rotate(90deg) skew(13deg) scale(.8) translateX(50px);
+ transform-origin: 50%;
+
+ background: linear-gradient(green, yellow);
+ }
+ </style>
+</head>
+<body>
+ <div id="test-node"></div>
+</body>
+</html>
diff --git a/devtools/client/inspector/test/doc_inspector_highlighter_dom.html b/devtools/client/inspector/test/doc_inspector_highlighter_dom.html
new file mode 100644
index 000000000..fab0c8803
--- /dev/null
+++ b/devtools/client/inspector/test/doc_inspector_highlighter_dom.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<body>
+
+<p>Hello World!</p>
+
+<div id="complex-div">
+ <div id="simple-div1">
+ <p id="useless-para">The DOM is very useful! <em>#useless-para</em></p>
+ <p id="useful-para">This example is <b id="bold">really</b> useful. <em>#useful-para</em></p>
+ </div>
+
+ <div id="simple-div2">
+ <p id="another">This is another node. You won't reach this in my test.</p>
+ <p id="ahoy">Ahoy! How you doin' Capn'? <em>#ahoy</em></p>
+ </div>
+</div>
+
+</body>
+</html>
diff --git a/devtools/client/inspector/test/doc_inspector_highlighter_inline.html b/devtools/client/inspector/test/doc_inspector_highlighter_inline.html
new file mode 100644
index 000000000..e1aa5bb1f
--- /dev/null
+++ b/devtools/client/inspector/test/doc_inspector_highlighter_inline.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <style>
+ html {
+ height: 100%;
+ background: #eee;
+ }
+ body {
+ margin: 0 auto;
+ padding: 1em;
+ box-sizing: border-box;
+ width: 500px;
+ height: 100%;
+ background: white;
+ font-family: Arial;
+ font-size: 15px;
+ line-height: 40px;
+ }
+ p span {
+ padding: 5px 0;
+ margin: 0 5px;
+ border: 5px solid #eee;
+ }
+ </style>
+ </head>
+ <body>
+ <h1>Lorem Ipsum</h1>
+ <h2>Lorem ipsum <em>dolor sit amet</em></h2>
+ <p><span>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed varius, nisl eget semper maximus, dui tellus tempor leo, at pharetra eros tortor sed odio. Nullam sagittis ex nec mi sagittis pulvinar. Pellentesque dapibus feugiat fermentum. Curabitur lacinia quis enim et tristique. Aliquam in semper massa. In ac vulputate nunc, at rutrum neque. Fusce condimentum, tellus quis placerat imperdiet, dolor tortor mattis erat, nec luctus magna diam pharetra mauris.</span></p>
+ <div dir="rtl">
+ <span><span></span>some ltr text in an rtl container</span>
+ </div>
+ </body>
+</html>
diff --git a/devtools/client/inspector/test/doc_inspector_highlighter_rect.html b/devtools/client/inspector/test/doc_inspector_highlighter_rect.html
new file mode 100644
index 000000000..4d23d52fd
--- /dev/null
+++ b/devtools/client/inspector/test/doc_inspector_highlighter_rect.html
@@ -0,0 +1,22 @@
+<!doctype html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <title>rect highlighter parent test page</title>
+ <style type="text/css">
+ body {
+ margin: 50px;
+ border: 10px solid red;
+ }
+
+ iframe {
+ border: 10px solid yellow;
+ padding: 0;
+ margin: 50px;
+ }
+ </style>
+</head>
+<body>
+ <iframe src="doc_inspector_highlighter_rect_iframe.html"></iframe>
+</body>
+</html>
diff --git a/devtools/client/inspector/test/doc_inspector_highlighter_rect_iframe.html b/devtools/client/inspector/test/doc_inspector_highlighter_rect_iframe.html
new file mode 100644
index 000000000..d59050f69
--- /dev/null
+++ b/devtools/client/inspector/test/doc_inspector_highlighter_rect_iframe.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <title>rect highlighter child test page</title>
+ <style type="text/css">
+ body {
+ margin: 0;
+ }
+ </style>
+</head>
+<body>
+
+</body>
+</html>
diff --git a/devtools/client/inspector/test/doc_inspector_highlighter_xbl.xul b/devtools/client/inspector/test/doc_inspector_highlighter_xbl.xul
new file mode 100644
index 000000000..8cbf990ea
--- /dev/null
+++ b/devtools/client/inspector/test/doc_inspector_highlighter_xbl.xul
@@ -0,0 +1,9 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<window title="Test that the picker works correctly with XBL anonymous nodes"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+<scale id="scale" style="background:red"/>
+
+</window>
diff --git a/devtools/client/inspector/test/doc_inspector_infobar.html b/devtools/client/inspector/test/doc_inspector_infobar.html
new file mode 100644
index 000000000..137b3487f
--- /dev/null
+++ b/devtools/client/inspector/test/doc_inspector_infobar.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+
+ <style>
+ body {
+ width: 100%;
+ height: 100%;
+ }
+
+ div {
+ position: absolute;
+ height: 100px;
+ width: 500px;
+ }
+
+ #bottom {
+ bottom: 0px;
+ }
+
+ #vertical {
+ height: 100%;
+ }
+
+ #farbottom {
+ top: 2000px;
+ background: red;
+ }
+
+ #abovetop {
+ top: -123px;
+ }";
+ </style>
+</head>
+<body>
+ <div id="abovetop"></div>
+ <div id="vertical"></div>
+ <div id="top" class="class1 class2"></div>
+ <div id="bottom"></div>
+ <div id="farbottom"></div>
+</body>
+</html>
diff --git a/devtools/client/inspector/test/doc_inspector_infobar_01.html b/devtools/client/inspector/test/doc_inspector_infobar_01.html
new file mode 100644
index 000000000..a0c42ee38
--- /dev/null
+++ b/devtools/client/inspector/test/doc_inspector_infobar_01.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+
+ <style>
+ body {
+ width: 100%;
+ height: 100%;
+ }
+ div {
+ position: absolute;
+ height: 100px;
+ width: 500px;
+ }
+
+ #bottom {
+ bottom: 0px;
+ background: blue;
+ }
+
+ #vertical {
+ height: 100%;
+ background: green;
+ }
+
+ svg {
+ width: 10px;
+ height: 10px;
+ }
+ </style>
+ </head>
+ <body>
+ <div id="vertical">Vertical</div>
+ <div id="top" class="class1 class2">Top</div>
+ <div id="bottom">Bottom</div>
+ <svg viewBox="0 0 10 10">
+ <clipPath id="clip">
+ <rect x="0" y="0" width="10" height="5"></rect>
+ </clipPath>
+ <circle cx="5" cy="5" r="5" fill="blue" clip-path="url(#clip)"></circle>
+ </svg>
+ </body>
+ </html>
diff --git a/devtools/client/inspector/test/doc_inspector_infobar_02.html b/devtools/client/inspector/test/doc_inspector_infobar_02.html
new file mode 100644
index 000000000..ed1843f8d
--- /dev/null
+++ b/devtools/client/inspector/test/doc_inspector_infobar_02.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+
+ <style>
+ body {
+ width: 100%;
+ height: 100%;
+ }
+
+ div {
+ position: absolute;
+ height: 100px;
+ width: 500px;
+ }
+
+ #below-bottom {
+ bottom: -200px;
+ background: red;
+ }
+
+ #above-top {
+ top: -200px;
+ background: black;
+ color: white;
+ }";
+ </style>
+</head>
+<body>
+ <div id="above-top">Above top</div>
+ <div id="below-bottom">Far bottom</div>
+</body>
+</html>
diff --git a/devtools/client/inspector/test/doc_inspector_infobar_03.html b/devtools/client/inspector/test/doc_inspector_infobar_03.html
new file mode 100644
index 000000000..a9aa05fa0
--- /dev/null
+++ b/devtools/client/inspector/test/doc_inspector_infobar_03.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+
+ <style>
+ body {
+ height: 300vh;
+ }
+ </style>
+ </head>
+ <body>
+ </body>
+ </html>
diff --git a/devtools/client/inspector/test/doc_inspector_infobar_textnode.html b/devtools/client/inspector/test/doc_inspector_infobar_textnode.html
new file mode 100644
index 000000000..2370708f4
--- /dev/null
+++ b/devtools/client/inspector/test/doc_inspector_infobar_textnode.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+</head>
+<body>
+ <div id="textnode-container">
+ text
+ <span>content</span>
+ <span>content</span>
+ text
+ </div>
+</body>
+</html>
diff --git a/devtools/client/inspector/test/doc_inspector_long-divs.html b/devtools/client/inspector/test/doc_inspector_long-divs.html
new file mode 100644
index 000000000..52d6343aa
--- /dev/null
+++ b/devtools/client/inspector/test/doc_inspector_long-divs.html
@@ -0,0 +1,104 @@
+<!doctype html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <title>Inspector Long Div Listing</title>
+ <style>
+ div {
+ background-color: #0002;
+ padding-left: 1em;
+ }
+ </style>
+</head>
+<body>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div id="focus-here">focus here</div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div>
+ <div>
+ <div>
+ <div>
+ <div id="zoom-here">zoom-here</div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+
+</body>
+</html>
diff --git a/devtools/client/inspector/test/doc_inspector_menu.html b/devtools/client/inspector/test/doc_inspector_menu.html
new file mode 100644
index 000000000..862a34579
--- /dev/null
+++ b/devtools/client/inspector/test/doc_inspector_menu.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Inspector Tree Menu Test</title>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ <div>
+ <div id="paste-area">
+ <h1>Inspector Tree Menu Test</h1>
+ <p class="inner">Unset</p>
+ <p class="adjacent">
+ <span class="ref">3</span>
+ </p>
+ </div>
+ <p data-id="copy">Paragraph for testing copy</p>
+ <p id="sensitivity">Paragraph for sensitivity</p>
+ <p class="duplicate">This will be duplicated</p>
+ <p id="delete">This has to be deleted</p>
+ <img id="copyimage" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVQYV2P4DwABAQEAWk1v8QAAAABJRU5ErkJggg==" />
+ <div id="hiddenElement" style="display: none;">
+ <p id="nestedHiddenElement">Visible element nested inside a non-visible element</p>
+ </div>
+ <p id="console-var">Paragraph for testing console variables</p>
+ <p id="console-var-multi">Paragraph for testing multiple console variables</p>
+ <p id="attributes" data-edit="original" data-remove="thing">Attributes are going to be changed here</p>
+ </div>
+ </body>
+</html>
diff --git a/devtools/client/inspector/test/doc_inspector_outerhtml.html b/devtools/client/inspector/test/doc_inspector_outerhtml.html
new file mode 100644
index 000000000..cc400674d
--- /dev/null
+++ b/devtools/client/inspector/test/doc_inspector_outerhtml.html
@@ -0,0 +1,11 @@
+<!doctype html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <title>Inspector Copy OuterHTML Test</title>
+</head>
+<body>
+ <!-- Comment -->
+ <div><p>Test copy OuterHTML</p></div>
+</body>
+</html>
diff --git a/devtools/client/inspector/test/doc_inspector_remove-iframe-during-load.html b/devtools/client/inspector/test/doc_inspector_remove-iframe-during-load.html
new file mode 100644
index 000000000..25454e122
--- /dev/null
+++ b/devtools/client/inspector/test/doc_inspector_remove-iframe-during-load.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <title>iframe creation/deletion test</title>
+</head>
+<body>
+ <div id="yay"></div>
+ <script type="text/javascript">
+ "use strict";
+
+ var yay = document.querySelector("#yay");
+ yay.textContent = "nothing";
+
+ // Create a custom event to let the test know when the window has finished
+ // loading.
+ var event = new Event("test-page-processing-done");
+
+ // Create/remove an iframe before load.
+ var iframe = document.createElement("iframe");
+ document.body.appendChild(iframe);
+ iframe.remove();
+ yay.textContent = "before events";
+
+ // Create/remove an iframe on DOMContentLoaded.
+ document.addEventListener("DOMContentLoaded", function () {
+ let newIframe = document.createElement("iframe");
+ document.body.appendChild(newIframe);
+ newIframe.remove();
+ yay.textContent = "DOMContentLoaded";
+ });
+
+ // Create/remove an iframe on window load.
+ window.addEventListener("load", function () {
+ let newIframe = document.createElement("iframe");
+ document.body.appendChild(newIframe);
+ newIframe.remove();
+ yay.textContent = "load";
+
+ // Dispatch the done event.
+ window.dispatchEvent(event);
+ });
+ </script>
+</body>
+</html>
diff --git a/devtools/client/inspector/test/doc_inspector_search-reserved.html b/devtools/client/inspector/test/doc_inspector_search-reserved.html
new file mode 100644
index 000000000..15cf8c3af
--- /dev/null
+++ b/devtools/client/inspector/test/doc_inspector_search-reserved.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <title>Inspector Search Box Reserved Character Test</title>
+</head>
+<body>
+ <div id="d1.d2">Hi, I'm an id that contains a CSS reserved character</div>
+ <div class="c1.c2">Hi, a class that contains a CSS reserved character</div>
+</body>
+</html>
diff --git a/devtools/client/inspector/test/doc_inspector_search-suggestions.html b/devtools/client/inspector/test/doc_inspector_search-suggestions.html
new file mode 100644
index 000000000..a84a2e3d4
--- /dev/null
+++ b/devtools/client/inspector/test/doc_inspector_search-suggestions.html
@@ -0,0 +1,27 @@
+<!doctype html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <title>Inspector Search Box Test</title>
+</head>
+<body>
+ <div id="d1">
+ <div class="l1">
+ <div id="d2" class="c1">Hello, I'm nested div</div>
+ </div>
+ </div>
+ <span id="s1">Hello, I'm a span
+ <div class="l1">
+ <span>Hi I am a nested span</span>
+ <span class="s4">Hi I am a nested classed span</span>
+ </div>
+ </span>
+ <span class="c1" id="s2">And me</span>
+
+ <p class="c1" id="p1">.someclass</p>
+ <p id="p2">#someid</p>
+ <button id="b1" disabled>button[disabled]</button>
+ <p id="p3" class="c2"><strong>p&gt;strong</strong></p>
+
+</body>
+</html>
diff --git a/devtools/client/inspector/test/doc_inspector_search-svg.html b/devtools/client/inspector/test/doc_inspector_search-svg.html
new file mode 100644
index 000000000..f762b2288
--- /dev/null
+++ b/devtools/client/inspector/test/doc_inspector_search-svg.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <title>Inspector SVG Search Box Test</title>
+</head>
+<body>
+ <div class="class1"></div>
+ <svg>
+ <clipPath>
+ <rect x="0" y="0" width="10" height="5"></rect>
+ </clipPath>
+ <circle cx="0" cy="0" r="50" class="class2" />
+ </svg>
+</body>
+</html>
diff --git a/devtools/client/inspector/test/doc_inspector_search.html b/devtools/client/inspector/test/doc_inspector_search.html
new file mode 100644
index 000000000..262eb0be6
--- /dev/null
+++ b/devtools/client/inspector/test/doc_inspector_search.html
@@ -0,0 +1,26 @@
+<!doctype html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <title>Inspector Search Box Test</title>
+</head>
+<body>
+
+ <!-- This is a list of 0 h1 elements -->
+
+ <!-- This is a list of 2 div elements -->
+ <div id="d1">Hello, I'm a div</div>
+ <div id="d2" class="c1">Hello, I'm another div</div>
+
+ <!-- This is a list of 2 span elements -->
+ <span id="s1">Hello, I'm a span</span>
+ <span class="c1" id="s2">And me</span>
+
+ <!-- This is a collection of various things that match only once -->
+ <p class="c1" id="p1">.someclass</p>
+ <p id="p2">#someid</p>
+ <button id="b1" disabled>button[disabled]</button>
+ <p id="p3" class="c2"><strong>p&gt;strong</strong></p>
+
+</body>
+</html>
diff --git a/devtools/client/inspector/test/doc_inspector_select-last-selected-01.html b/devtools/client/inspector/test/doc_inspector_select-last-selected-01.html
new file mode 100644
index 000000000..fbe1251cb
--- /dev/null
+++ b/devtools/client/inspector/test/doc_inspector_select-last-selected-01.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <title>select last selected test</title>
+ </head>
+ <body>
+ <div id="id1"></div>
+ <div id="id2"></div>
+ <div id="id3">
+ <ul class="aList">
+ <li class="item"></li>
+ <li class="item"></li>
+ <li class="item"></li>
+ <li class="item">
+ <span id="id4"></span>
+ </li>
+ </ul>
+ </div>
+ </body>
+</html> \ No newline at end of file
diff --git a/devtools/client/inspector/test/doc_inspector_select-last-selected-02.html b/devtools/client/inspector/test/doc_inspector_select-last-selected-02.html
new file mode 100644
index 000000000..2fbef312c
--- /dev/null
+++ b/devtools/client/inspector/test/doc_inspector_select-last-selected-02.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <title>select last selected test</title>
+ </head>
+ <body>
+ <div id="id5"></div>
+ </body>
+</html> \ No newline at end of file
diff --git a/devtools/client/inspector/test/doc_inspector_svg.svg b/devtools/client/inspector/test/doc_inspector_svg.svg
new file mode 100644
index 000000000..75154dcf3
--- /dev/null
+++ b/devtools/client/inspector/test/doc_inspector_svg.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg">
+ <circle r="5"/>
+</svg>
diff --git a/devtools/client/inspector/test/head.js b/devtools/client/inspector/test/head.js
new file mode 100644
index 000000000..f251568df
--- /dev/null
+++ b/devtools/client/inspector/test/head.js
@@ -0,0 +1,732 @@
+/* vim: set 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/. */
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+/* import-globals-from ../../framework/test/shared-head.js */
+/* import-globals-from ../../commandline/test/helpers.js */
+/* import-globals-from ../../shared/test/test-actor-registry.js */
+/* import-globals-from ../../inspector/test/shared-head.js */
+"use strict";
+
+// Load the shared-head file first.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js",
+ this);
+
+// Services.prefs.setBoolPref("devtools.debugger.log", true);
+// SimpleTest.registerCleanupFunction(() => {
+// Services.prefs.clearUserPref("devtools.debugger.log");
+// });
+
+// Import the GCLI test helper
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/commandline/test/helpers.js",
+ this);
+
+// Import helpers registering the test-actor in remote targets
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/shared/test/test-actor-registry.js",
+ this);
+
+// Import helpers for the inspector that are also shared with others
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/inspector/test/shared-head.js",
+ this);
+
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const INSPECTOR_L10N =
+ new LocalizationHelper("devtools/client/locales/inspector.properties");
+
+flags.testing = true;
+registerCleanupFunction(() => {
+ flags.testing = false;
+});
+
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("devtools.inspector.activeSidebar");
+});
+
+registerCleanupFunction(function* () {
+ // Move the mouse outside inspector. If the test happened fake a mouse event
+ // somewhere over inspector the pointer is considered to be there when the
+ // next test begins. This might cause unexpected events to be emitted when
+ // another test moves the mouse.
+ EventUtils.synthesizeMouseAtPoint(1, 1, {type: "mousemove"}, window);
+});
+
+var navigateTo = Task.async(function* (inspector, url) {
+ let markuploaded = inspector.once("markuploaded");
+ let onNewRoot = inspector.once("new-root");
+ let onUpdated = inspector.once("inspector-updated");
+
+ info("Navigating to: " + url);
+ let activeTab = inspector.toolbox.target.activeTab;
+ yield activeTab.navigateTo(url);
+
+ info("Waiting for markup view to load after navigation.");
+ yield markuploaded;
+
+ info("Waiting for new root.");
+ yield onNewRoot;
+
+ info("Waiting for inspector to update after new-root event.");
+ yield onUpdated;
+});
+
+/**
+ * Start the element picker and focus the content window.
+ * @param {Toolbox} toolbox
+ * @param {Boolean} skipFocus - Allow tests to bypass the focus event.
+ */
+var startPicker = Task.async(function* (toolbox, skipFocus) {
+ info("Start the element picker");
+ toolbox.win.focus();
+ yield toolbox.highlighterUtils.startPicker();
+ if (!skipFocus) {
+ // By default make sure the content window is focused since the picker may not focus
+ // the content window by default.
+ yield ContentTask.spawn(gBrowser.selectedBrowser, null, function* () {
+ content.focus();
+ });
+ }
+});
+
+/**
+ * Highlight a node and set the inspector's current selection to the node or
+ * the first match of the given css selector.
+ * @param {String|NodeFront} selector
+ * @param {InspectorPanel} inspector
+ * The instance of InspectorPanel currently loaded in the toolbox
+ * @return a promise that resolves when the inspector is updated with the new
+ * node
+ */
+function selectAndHighlightNode(selector, inspector) {
+ info("Highlighting and selecting the node " + selector);
+ return selectNode(selector, inspector, "test-highlight");
+}
+
+/**
+ * Select node for a given selector, make it focusable and set focus in its
+ * container element.
+ * @param {String|NodeFront} selector
+ * @param {InspectorPanel} inspector The current inspector-panel instance.
+ * @return {MarkupContainer}
+ */
+function* focusNode(selector, inspector) {
+ getContainerForNodeFront(inspector.walker.rootNode, inspector).elt.focus();
+ let nodeFront = yield getNodeFront(selector, inspector);
+ let container = getContainerForNodeFront(nodeFront, inspector);
+ yield selectNode(nodeFront, inspector);
+ EventUtils.sendKey("return", inspector.panelWin);
+ return container;
+}
+
+/**
+ * Set the inspector's current selection to null so that no node is selected
+ *
+ * @param {InspectorPanel} inspector
+ * The instance of InspectorPanel currently loaded in the toolbox
+ * @return a promise that resolves when the inspector is updated
+ */
+function clearCurrentNodeSelection(inspector) {
+ info("Clearing the current selection");
+ let updated = inspector.once("inspector-updated");
+ inspector.selection.setNodeFront(null);
+ return updated;
+}
+
+/**
+ * Open the inspector in a tab with given URL.
+ * @param {string} url The URL to open.
+ * @param {String} hostType Optional hostType, as defined in Toolbox.HostType
+ * @return A promise that is resolved once the tab and inspector have loaded
+ * with an object: { tab, toolbox, inspector }.
+ */
+var openInspectorForURL = Task.async(function* (url, hostType) {
+ let tab = yield addTab(url);
+ let { inspector, toolbox, testActor } = yield openInspector(hostType);
+ return { tab, inspector, toolbox, testActor };
+});
+
+function getActiveInspector() {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ return gDevTools.getToolbox(target).getPanel("inspector");
+}
+
+/**
+ * Right click on a node in the test page and click on the inspect menu item.
+ * @param {TestActor}
+ * @param {String} selector The selector for the node to click on in the page.
+ * @return {Promise} Resolves to the inspector when it has opened and is updated
+ */
+var clickOnInspectMenuItem = Task.async(function* (testActor, selector) {
+ info("Showing the contextual menu on node " + selector);
+ let contentAreaContextMenu = document.querySelector(
+ "#contentAreaContextMenu");
+ let contextOpened = once(contentAreaContextMenu, "popupshown");
+
+ yield testActor.synthesizeMouse({
+ selector: selector,
+ center: true,
+ options: {type: "contextmenu", button: 2}
+ });
+
+ yield contextOpened;
+
+ info("Triggering the inspect action");
+ yield gContextMenu.inspectNode();
+
+ info("Hiding the menu");
+ let contextClosed = once(contentAreaContextMenu, "popuphidden");
+ contentAreaContextMenu.hidePopup();
+ yield contextClosed;
+
+ return getActiveInspector();
+});
+
+/**
+ * Get the NodeFront for a node that matches a given css selector inside a
+ * given iframe.
+ * @param {String|NodeFront} selector
+ * @param {String|NodeFront} frameSelector A selector that matches the iframe
+ * the node is in
+ * @param {InspectorPanel} inspector The instance of InspectorPanel currently
+ * loaded in the toolbox
+ * @return {Promise} Resolves when the inspector is updated with the new node
+ */
+var getNodeFrontInFrame = Task.async(function* (selector, frameSelector,
+ inspector) {
+ let iframe = yield getNodeFront(frameSelector, inspector);
+ let {nodes} = yield inspector.walker.children(iframe);
+ return inspector.walker.querySelector(nodes[0], selector);
+});
+
+var focusSearchBoxUsingShortcut = Task.async(function* (panelWin, callback) {
+ info("Focusing search box");
+ let searchBox = panelWin.document.getElementById("inspector-searchbox");
+ let focused = once(searchBox, "focus");
+
+ panelWin.focus();
+
+ synthesizeKeyShortcut(INSPECTOR_L10N.getStr("inspector.searchHTML.key"));
+
+ yield focused;
+
+ if (callback) {
+ callback();
+ }
+});
+
+/**
+ * Get the MarkupContainer object instance that corresponds to the given
+ * NodeFront
+ * @param {NodeFront} nodeFront
+ * @param {InspectorPanel} inspector The instance of InspectorPanel currently
+ * loaded in the toolbox
+ * @return {MarkupContainer}
+ */
+function getContainerForNodeFront(nodeFront, {markup}) {
+ return markup.getContainer(nodeFront);
+}
+
+/**
+ * Get the MarkupContainer object instance that corresponds to the given
+ * selector
+ * @param {String|NodeFront} selector
+ * @param {InspectorPanel} inspector The instance of InspectorPanel currently
+ * loaded in the toolbox
+ * @return {MarkupContainer}
+ */
+var getContainerForSelector = Task.async(function* (selector, inspector) {
+ info("Getting the markup-container for node " + selector);
+ let nodeFront = yield getNodeFront(selector, inspector);
+ let container = getContainerForNodeFront(nodeFront, inspector);
+ info("Found markup-container " + container);
+ return container;
+});
+
+/**
+ * Simulate a mouse-over on the markup-container (a line in the markup-view)
+ * that corresponds to the selector passed.
+ * @param {String|NodeFront} selector
+ * @param {InspectorPanel} inspector The instance of InspectorPanel currently
+ * loaded in the toolbox
+ * @return {Promise} Resolves when the container is hovered and the higlighter
+ * is shown on the corresponding node
+ */
+var hoverContainer = Task.async(function* (selector, inspector) {
+ info("Hovering over the markup-container for node " + selector);
+
+ let nodeFront = yield getNodeFront(selector, inspector);
+ let container = getContainerForNodeFront(nodeFront, inspector);
+
+ let highlit = inspector.toolbox.once("node-highlight");
+ EventUtils.synthesizeMouseAtCenter(container.tagLine, {type: "mousemove"},
+ inspector.markup.doc.defaultView);
+ return highlit;
+});
+
+/**
+ * Simulate a click on the markup-container (a line in the markup-view)
+ * that corresponds to the selector passed.
+ * @param {String|NodeFront} selector
+ * @param {InspectorPanel} inspector The instance of InspectorPanel currently
+ * loaded in the toolbox
+ * @return {Promise} Resolves when the node has been selected.
+ */
+var clickContainer = Task.async(function* (selector, inspector) {
+ info("Clicking on the markup-container for node " + selector);
+
+ let nodeFront = yield getNodeFront(selector, inspector);
+ let container = getContainerForNodeFront(nodeFront, inspector);
+
+ let updated = inspector.once("inspector-updated");
+ EventUtils.synthesizeMouseAtCenter(container.tagLine, {type: "mousedown"},
+ inspector.markup.doc.defaultView);
+ EventUtils.synthesizeMouseAtCenter(container.tagLine, {type: "mouseup"},
+ inspector.markup.doc.defaultView);
+ return updated;
+});
+
+/**
+ * Simulate the mouse leaving the markup-view area
+ * @param {InspectorPanel} inspector The instance of InspectorPanel currently
+ * loaded in the toolbox
+ * @return a promise when done
+ */
+function mouseLeaveMarkupView(inspector) {
+ info("Leaving the markup-view area");
+ let def = defer();
+
+ // Find another element to mouseover over in order to leave the markup-view
+ let btn = inspector.toolbox.doc.querySelector("#toolbox-controls");
+
+ EventUtils.synthesizeMouseAtCenter(btn, {type: "mousemove"},
+ inspector.toolbox.win);
+ executeSoon(def.resolve);
+
+ return def.promise;
+}
+
+/**
+ * Dispatch the copy event on the given element
+ */
+function fireCopyEvent(element) {
+ let evt = element.ownerDocument.createEvent("Event");
+ evt.initEvent("copy", true, true);
+ element.dispatchEvent(evt);
+}
+
+/**
+ * Undo the last markup-view action and wait for the corresponding mutation to
+ * occur
+ * @param {InspectorPanel} inspector The instance of InspectorPanel currently
+ * loaded in the toolbox
+ * @return a promise that resolves when the markup-mutation has been treated or
+ * rejects if no undo action is possible
+ */
+function undoChange(inspector) {
+ let canUndo = inspector.markup.undo.canUndo();
+ ok(canUndo, "The last change in the markup-view can be undone");
+ if (!canUndo) {
+ return promise.reject();
+ }
+
+ let mutated = inspector.once("markupmutation");
+ inspector.markup.undo.undo();
+ return mutated;
+}
+
+/**
+ * Redo the last markup-view action and wait for the corresponding mutation to
+ * occur
+ * @param {InspectorPanel} inspector The instance of InspectorPanel currently
+ * loaded in the toolbox
+ * @return a promise that resolves when the markup-mutation has been treated or
+ * rejects if no redo action is possible
+ */
+function redoChange(inspector) {
+ let canRedo = inspector.markup.undo.canRedo();
+ ok(canRedo, "The last change in the markup-view can be redone");
+ if (!canRedo) {
+ return promise.reject();
+ }
+
+ let mutated = inspector.once("markupmutation");
+ inspector.markup.undo.redo();
+ return mutated;
+}
+
+/**
+ * A helper that fetches a front for a node that matches the given selector or
+ * doctype node if the selector is falsy.
+ */
+function* getNodeFrontForSelector(selector, inspector) {
+ if (selector) {
+ info("Retrieving front for selector " + selector);
+ return getNodeFront(selector, inspector);
+ }
+
+ info("Retrieving front for doctype node");
+ let {nodes} = yield inspector.walker.children(inspector.walker.rootNode);
+ return nodes[0];
+}
+
+/**
+ * A simple polling helper that executes a given function until it returns true.
+ * @param {Function} check A generator function that is expected to return true at some
+ * stage.
+ * @param {String} desc A text description to be displayed when the polling starts.
+ * @param {Number} attemptes Optional number of times we poll. Defaults to 10.
+ * @param {Number} timeBetweenAttempts Optional time to wait between each attempt.
+ * Defaults to 200ms.
+ */
+function* poll(check, desc, attempts = 10, timeBetweenAttempts = 200) {
+ info(desc);
+
+ for (let i = 0; i < attempts; i++) {
+ if (yield check()) {
+ return;
+ }
+ yield new Promise(resolve => setTimeout(resolve, timeBetweenAttempts));
+ }
+
+ throw new Error(`Timeout while: ${desc}`);
+}
+
+/**
+ * Encapsulate some common operations for highlighter's tests, to have
+ * the tests cleaner, without exposing directly `inspector`, `highlighter`, and
+ * `testActor` if not needed.
+ *
+ * @param {String}
+ * The highlighter's type
+ * @return
+ * A generator function that takes an object with `inspector` and `testActor`
+ * properties. (see `openInspector`)
+ */
+const getHighlighterHelperFor = (type) => Task.async(
+ function* ({inspector, testActor}) {
+ let front = inspector.inspector;
+ let highlighter = yield front.getHighlighterByType(type);
+
+ let prefix = "";
+
+ // Internals for mouse events
+ let prevX, prevY;
+
+ // Highlighted node
+ let highlightedNode = null;
+
+ return {
+ set prefix(value) {
+ prefix = value;
+ },
+
+ get highlightedNode() {
+ if (!highlightedNode) {
+ return null;
+ }
+
+ return {
+ getComputedStyle: function* (options = {}) {
+ return yield inspector.pageStyle.getComputed(
+ highlightedNode, options);
+ }
+ };
+ },
+
+ show: function* (selector = ":root", options) {
+ highlightedNode = yield getNodeFront(selector, inspector);
+ return yield highlighter.show(highlightedNode, options);
+ },
+
+ hide: function* () {
+ yield highlighter.hide();
+ },
+
+ isElementHidden: function* (id) {
+ return (yield testActor.getHighlighterNodeAttribute(
+ prefix + id, "hidden", highlighter)) === "true";
+ },
+
+ getElementTextContent: function* (id) {
+ return yield testActor.getHighlighterNodeTextContent(
+ prefix + id, highlighter);
+ },
+
+ getElementAttribute: function* (id, name) {
+ return yield testActor.getHighlighterNodeAttribute(
+ prefix + id, name, highlighter);
+ },
+
+ waitForElementAttributeSet: function* (id, name) {
+ yield poll(function* () {
+ let value = yield testActor.getHighlighterNodeAttribute(
+ prefix + id, name, highlighter);
+ return !!value;
+ }, `Waiting for element ${id} to have attribute ${name} set`);
+ },
+
+ waitForElementAttributeRemoved: function* (id, name) {
+ yield poll(function* () {
+ let value = yield testActor.getHighlighterNodeAttribute(
+ prefix + id, name, highlighter);
+ return !value;
+ }, `Waiting for element ${id} to have attribute ${name} removed`);
+ },
+
+ synthesizeMouse: function* (options) {
+ options = Object.assign({selector: ":root"}, options);
+ yield testActor.synthesizeMouse(options);
+ },
+
+ // This object will synthesize any "mouse" prefixed event to the
+ // `testActor`, using the name of method called as suffix for the
+ // event's name.
+ // If no x, y coords are given, the previous ones are used.
+ //
+ // For example:
+ // mouse.down(10, 20); // synthesize "mousedown" at 10,20
+ // mouse.move(20, 30); // synthesize "mousemove" at 20,30
+ // mouse.up(); // synthesize "mouseup" at 20,30
+ mouse: new Proxy({}, {
+ get: (target, name) =>
+ function* (x = prevX, y = prevY) {
+ prevX = x;
+ prevY = y;
+ yield testActor.synthesizeMouse({
+ selector: ":root", x, y, options: {type: "mouse" + name}});
+ }
+ }),
+
+ reflow: function* () {
+ yield testActor.reflow();
+ },
+
+ finalize: function* () {
+ highlightedNode = null;
+ yield highlighter.finalize();
+ }
+ };
+ }
+);
+
+// The expand all operation of the markup-view calls itself recursively and
+// there's not one event we can wait for to know when it's done so use this
+// helper function to wait until all recursive children updates are done.
+function* waitForMultipleChildrenUpdates(inspector) {
+ // As long as child updates are queued up while we wait for an update already
+ // wait again
+ if (inspector.markup._queuedChildUpdates &&
+ inspector.markup._queuedChildUpdates.size) {
+ yield waitForChildrenUpdated(inspector);
+ return yield waitForMultipleChildrenUpdates(inspector);
+ }
+ return null;
+}
+
+/**
+ * Using the markupview's _waitForChildren function, wait for all queued
+ * children updates to be handled.
+ * @param {InspectorPanel} inspector The instance of InspectorPanel currently
+ * loaded in the toolbox
+ * @return a promise that resolves when all queued children updates have been
+ * handled
+ */
+function waitForChildrenUpdated({markup}) {
+ info("Waiting for queued children updates to be handled");
+ let def = defer();
+ markup._waitForChildren().then(() => {
+ executeSoon(def.resolve);
+ });
+ return def.promise;
+}
+
+/**
+ * Wait for the toolbox to emit the styleeditor-selected event and when done
+ * wait for the stylesheet identified by href to be loaded in the stylesheet
+ * editor
+ *
+ * @param {Toolbox} toolbox
+ * @param {String} href
+ * Optional, if not provided, wait for the first editor to be ready
+ * @return a promise that resolves to the editor when the stylesheet editor is
+ * ready
+ */
+function waitForStyleEditor(toolbox, href) {
+ let def = defer();
+
+ info("Waiting for the toolbox to switch to the styleeditor");
+ toolbox.once("styleeditor-selected").then(() => {
+ let panel = toolbox.getCurrentPanel();
+ ok(panel && panel.UI, "Styleeditor panel switched to front");
+
+ // A helper that resolves the promise once it receives an editor that
+ // matches the expected href. Returns false if the editor was not correct.
+ let gotEditor = (event, editor) => {
+ let currentHref = editor.styleSheet.href;
+ if (!href || (href && currentHref.endsWith(href))) {
+ info("Stylesheet editor selected");
+ panel.UI.off("editor-selected", gotEditor);
+
+ editor.getSourceEditor().then(sourceEditor => {
+ info("Stylesheet editor fully loaded");
+ def.resolve(sourceEditor);
+ });
+
+ return true;
+ }
+
+ info("The editor was incorrect. Waiting for editor-selected event.");
+ return false;
+ };
+
+ // The expected editor may already be selected. Check the if the currently
+ // selected editor is the expected one and if not wait for an
+ // editor-selected event.
+ if (!gotEditor("styleeditor-selected", panel.UI.selectedEditor)) {
+ // The expected editor is not selected (yet). Wait for it.
+ panel.UI.on("editor-selected", gotEditor);
+ }
+ });
+
+ return def.promise;
+}
+
+/**
+ * Checks if document's active element is within the given element.
+ * @param {HTMLDocument} doc document with active element in question
+ * @param {DOMNode} container element tested on focus containment
+ * @return {Boolean}
+ */
+function containsFocus(doc, container) {
+ let elm = doc.activeElement;
+ while (elm) {
+ if (elm === container) {
+ return true;
+ }
+ elm = elm.parentNode;
+ }
+ return false;
+}
+
+/**
+ * Listen for a new tab to open and return a promise that resolves when one
+ * does and completes the load event.
+ *
+ * @return a promise that resolves to the tab object
+ */
+var waitForTab = Task.async(function* () {
+ info("Waiting for a tab to open");
+ yield once(gBrowser.tabContainer, "TabOpen");
+ let tab = gBrowser.selectedTab;
+ yield BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ info("The tab load completed");
+ return tab;
+});
+
+/**
+ * Simulate the key input for the given input in the window.
+ *
+ * @param {String} input
+ * The string value to input
+ * @param {Window} win
+ * The window containing the panel
+ */
+function synthesizeKeys(input, win) {
+ for (let key of input.split("")) {
+ EventUtils.synthesizeKey(key, {}, win);
+ }
+}
+
+/**
+ * Given a tooltip object instance (see Tooltip.js), checks if it is set to
+ * toggle and hover and if so, checks if the given target is a valid hover
+ * target. This won't actually show the tooltip (the less we interact with XUL
+ * panels during test runs, the better).
+ *
+ * @return a promise that resolves when the answer is known
+ */
+function isHoverTooltipTarget(tooltip, target) {
+ if (!tooltip._toggle._baseNode || !tooltip.panel) {
+ return promise.reject(new Error(
+ "The tooltip passed isn't set to toggle on hover or is not a tooltip"));
+ }
+ return tooltip._toggle.isValidHoverTarget(target);
+}
+
+/**
+ * Same as isHoverTooltipTarget except that it will fail the test if there is no
+ * tooltip defined on hover of the given element
+ *
+ * @return a promise
+ */
+function assertHoverTooltipOn(tooltip, element) {
+ return isHoverTooltipTarget(tooltip, element).then(() => {
+ ok(true, "A tooltip is defined on hover of the given element");
+ }, () => {
+ ok(false, "No tooltip is defined on hover of the given element");
+ });
+}
+
+/**
+ * Open the inspector menu and return all of it's items in a flat array
+ * @param {InspectorPanel} inspector
+ * @param {Object} options to pass into openMenu
+ * @return An array of MenuItems
+ */
+function openContextMenuAndGetAllItems(inspector, options) {
+ let menu = inspector._openMenu(options);
+
+ // Flatten all menu items into a single array to make searching through it easier
+ let allItems = [].concat.apply([], menu.items.map(function addItem(item) {
+ if (item.submenu) {
+ return addItem(item.submenu.items);
+ }
+ return item;
+ }));
+
+ return allItems;
+}
+
+/**
+ * Get the rule editor from the rule-view given its index
+ *
+ * @param {CssRuleView} view
+ * The instance of the rule-view panel
+ * @param {Number} childrenIndex
+ * The children index of the element to get
+ * @param {Number} nodeIndex
+ * The child node index of the element to get
+ * @return {DOMNode} The rule editor if any at this index
+ */
+function getRuleViewRuleEditor(view, childrenIndex, nodeIndex) {
+ return nodeIndex !== undefined ?
+ view.element.children[childrenIndex].childNodes[nodeIndex]._ruleEditor :
+ view.element.children[childrenIndex]._ruleEditor;
+}
+
+/**
+ * Get the text displayed for a given DOM Element's textContent within the
+ * markup view.
+ *
+ * @param {String} selector
+ * @param {InspectorPanel} inspector
+ * @return {String} The text displayed in the markup view
+ */
+function* getDisplayedNodeTextContent(selector, inspector) {
+ // We have to ensure that the textContent is displayed, for that the DOM
+ // Element has to be selected in the markup view and to be expanded.
+ yield selectNode(selector, inspector);
+
+ let container = yield getContainerForSelector(selector, inspector);
+ yield inspector.markup.expandNode(container.node);
+ yield waitForMultipleChildrenUpdates(inspector);
+ if (container) {
+ let textContainer = container.elt.querySelector("pre");
+ return textContainer.textContent;
+ }
+ return null;
+}
diff --git a/devtools/client/inspector/test/shared-head.js b/devtools/client/inspector/test/shared-head.js
new file mode 100644
index 000000000..13eeca0f7
--- /dev/null
+++ b/devtools/client/inspector/test/shared-head.js
@@ -0,0 +1,186 @@
+/* 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";
+
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+/* globals registerTestActor, getTestActor, Task, openToolboxForTab, gBrowser */
+
+// This file contains functions related to the inspector that are also of interest to
+// other test directores as well.
+
+/**
+ * Open the toolbox, with the inspector tool visible.
+ * @param {String} hostType Optional hostType, as defined in Toolbox.HostType
+ * @return a promise that resolves when the inspector is ready
+ */
+var openInspector = Task.async(function* (hostType) {
+ info("Opening the inspector");
+
+ let toolbox = yield openToolboxForTab(gBrowser.selectedTab, "inspector",
+ hostType);
+ let inspector = toolbox.getPanel("inspector");
+
+ if (inspector._updateProgress) {
+ info("Need to wait for the inspector to update");
+ yield inspector.once("inspector-updated");
+ }
+
+ info("Waiting for actor features to be detected");
+ yield inspector._detectingActorFeatures;
+
+ yield registerTestActor(toolbox.target.client);
+ let testActor = yield getTestActor(toolbox);
+
+ return {toolbox, inspector, testActor};
+});
+
+/**
+ * Open the toolbox, with the inspector tool visible, and the one of the sidebar
+ * tabs selected.
+ *
+ * @param {String} id
+ * The ID of the sidebar tab to be opened
+ * @return a promise that resolves when the inspector is ready and the tab is
+ * visible and ready
+ */
+var openInspectorSidebarTab = Task.async(function* (id) {
+ let {toolbox, inspector, testActor} = yield openInspector();
+
+ info("Selecting the " + id + " sidebar");
+ inspector.sidebar.select(id);
+
+ return {
+ toolbox,
+ inspector,
+ testActor
+ };
+});
+
+/**
+ * Open the toolbox, with the inspector tool visible, and the rule-view
+ * sidebar tab selected.
+ *
+ * @return a promise that resolves when the inspector is ready and the rule view
+ * is visible and ready
+ */
+function openRuleView() {
+ return openInspectorSidebarTab("ruleview").then(data => {
+ // Replace the view to use a custom throttle function that can be triggered manually
+ // through an additional ".flush()" property.
+ data.inspector.ruleview.view.throttle = manualThrottle();
+
+ return {
+ toolbox: data.toolbox,
+ inspector: data.inspector,
+ testActor: data.testActor,
+ view: data.inspector.ruleview.view
+ };
+ });
+}
+
+/**
+ * Open the toolbox, with the inspector tool visible, and the computed-view
+ * sidebar tab selected.
+ *
+ * @return a promise that resolves when the inspector is ready and the computed
+ * view is visible and ready
+ */
+function openComputedView() {
+ return openInspectorSidebarTab("computedview").then(data => {
+ return {
+ toolbox: data.toolbox,
+ inspector: data.inspector,
+ testActor: data.testActor,
+ view: data.inspector.computedview.computedView
+ };
+ });
+}
+
+/**
+ * Select the rule view sidebar tab on an already opened inspector panel.
+ *
+ * @param {InspectorPanel} inspector
+ * The opened inspector panel
+ * @return {CssRuleView} the rule view
+ */
+function selectRuleView(inspector) {
+ inspector.sidebar.select("ruleview");
+ return inspector.ruleview.view;
+}
+
+/**
+ * Select the computed view sidebar tab on an already opened inspector panel.
+ *
+ * @param {InspectorPanel} inspector
+ * The opened inspector panel
+ * @return {CssComputedView} the computed view
+ */
+function selectComputedView(inspector) {
+ inspector.sidebar.select("computedview");
+ return inspector.computedview.computedView;
+}
+
+/**
+ * Get the NodeFront for a node that matches a given css selector, via the
+ * protocol.
+ * @param {String|NodeFront} selector
+ * @param {InspectorPanel} inspector The instance of InspectorPanel currently
+ * loaded in the toolbox
+ * @return {Promise} Resolves to the NodeFront instance
+ */
+function getNodeFront(selector, {walker}) {
+ if (selector._form) {
+ return selector;
+ }
+ return walker.querySelector(walker.rootNode, selector);
+}
+
+/**
+ * Set the inspector's current selection to the first match of the given css
+ * selector
+ * @param {String|NodeFront} selector
+ * @param {InspectorPanel} inspector The instance of InspectorPanel currently
+ * loaded in the toolbox
+ * @param {String} reason Defaults to "test" which instructs the inspector not
+ * to highlight the node upon selection
+ * @return {Promise} Resolves when the inspector is updated with the new node
+ */
+var selectNode = Task.async(function* (selector, inspector, reason = "test") {
+ info("Selecting the node for '" + selector + "'");
+ let nodeFront = yield getNodeFront(selector, inspector);
+ let updated = inspector.once("inspector-updated");
+ inspector.selection.setNodeFront(nodeFront, reason);
+ yield updated;
+});
+
+/**
+ * Create a throttling function that can be manually "flushed". This is to replace the
+ * use of the `throttle` function from `devtools/client/inspector/shared/utils.js`, which
+ * has a setTimeout that can cause intermittents.
+ * @return {Function} This function has the same function signature as throttle, but
+ * the property `.flush()` has been added for flushing out any
+ * throttled calls.
+ */
+function manualThrottle() {
+ let calls = [];
+
+ function throttle(func, wait, scope) {
+ return function () {
+ let existingCall = calls.find(call => call.func === func);
+ if (existingCall) {
+ existingCall.args = arguments;
+ } else {
+ calls.push({ func, wait, scope, args: arguments });
+ }
+ };
+ }
+
+ throttle.flush = function () {
+ calls.forEach(({func, scope, args}) => func.apply(scope, args));
+ calls = [];
+ };
+
+ return throttle;
+}
diff --git a/devtools/client/inspector/toolsidebar.js b/devtools/client/inspector/toolsidebar.js
new file mode 100644
index 000000000..d013b7b84
--- /dev/null
+++ b/devtools/client/inspector/toolsidebar.js
@@ -0,0 +1,325 @@
+/* -*- 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";
+
+var EventEmitter = require("devtools/shared/event-emitter");
+var Telemetry = require("devtools/client/shared/telemetry");
+var { Task } = require("devtools/shared/task");
+
+/**
+ * This object represents replacement for ToolSidebar
+ * implemented in devtools/client/framework/sidebar.js module
+ *
+ * This new component is part of devtools.html aimed at
+ * removing XUL and use HTML for entire DevTools UI.
+ * There are currently two implementation of the side bar since
+ * the `sidebar.js` module (mentioned above) is still used by
+ * other panels.
+ * As soon as all panels are using this HTML based
+ * implementation it can be removed.
+ */
+function ToolSidebar(tabbox, panel, uid, options = {}) {
+ EventEmitter.decorate(this);
+
+ this._tabbox = tabbox;
+ this._uid = uid;
+ this._panelDoc = this._tabbox.ownerDocument;
+ this._toolPanel = panel;
+ this._options = options;
+
+ if (!options.disableTelemetry) {
+ this._telemetry = new Telemetry();
+ }
+
+ this._tabs = [];
+
+ if (this._options.hideTabstripe) {
+ this._tabbox.setAttribute("hidetabs", "true");
+ }
+
+ this.render();
+
+ this._toolPanel.emit("sidebar-created", this);
+}
+
+exports.ToolSidebar = ToolSidebar;
+
+ToolSidebar.prototype = {
+ TABPANEL_ID_PREFIX: "sidebar-panel-",
+
+ // React
+
+ get React() {
+ return this._toolPanel.React;
+ },
+
+ get ReactDOM() {
+ return this._toolPanel.ReactDOM;
+ },
+
+ get browserRequire() {
+ return this._toolPanel.browserRequire;
+ },
+
+ get InspectorTabPanel() {
+ return this._toolPanel.InspectorTabPanel;
+ },
+
+ // Rendering
+
+ render: function () {
+ let Tabbar = this.React.createFactory(this.browserRequire(
+ "devtools/client/shared/components/tabs/tabbar"));
+
+ let sidebar = Tabbar({
+ toolbox: this._toolPanel._toolbox,
+ showAllTabsMenu: true,
+ onSelect: this.handleSelectionChange.bind(this),
+ });
+
+ this._tabbar = this.ReactDOM.render(sidebar, this._tabbox);
+ },
+
+ /**
+ * Register a side-panel tab.
+ *
+ * @param {string} tab uniq id
+ * @param {string} title tab title
+ * @param {React.Component} panel component. See `InspectorPanelTab` as an example.
+ * @param {boolean} selected true if the panel should be selected
+ */
+ addTab: function (id, title, panel, selected) {
+ this._tabbar.addTab(id, title, selected, panel);
+ this.emit("new-tab-registered", id);
+ },
+
+ /**
+ * Helper API for adding side-panels that use existing DOM nodes
+ * (defined within inspector.xhtml) as the content.
+ *
+ * @param {string} tab uniq id
+ * @param {string} title tab title
+ * @param {boolean} selected true if the panel should be selected
+ */
+ addExistingTab: function (id, title, selected) {
+ let panel = this.InspectorTabPanel({
+ id: id,
+ idPrefix: this.TABPANEL_ID_PREFIX,
+ key: id,
+ title: title,
+ });
+
+ this.addTab(id, title, panel, selected);
+ },
+
+ /**
+ * Helper API for adding side-panels that use existing <iframe> nodes
+ * (defined within inspector.xhtml) as the content.
+ * The document must have a title, which will be used as the name of the tab.
+ *
+ * @param {string} tab uniq id
+ * @param {string} title tab title
+ * @param {string} url
+ * @param {boolean} selected true if the panel should be selected
+ */
+ addFrameTab: function (id, title, url, selected) {
+ let panel = this.InspectorTabPanel({
+ id: id,
+ idPrefix: this.TABPANEL_ID_PREFIX,
+ key: id,
+ title: title,
+ url: url,
+ onMount: this.onSidePanelMounted.bind(this),
+ });
+
+ this.addTab(id, title, panel, selected);
+ },
+
+ onSidePanelMounted: function (content, props) {
+ let iframe = content.querySelector("iframe");
+ if (!iframe || iframe.getAttribute("src")) {
+ return;
+ }
+
+ let onIFrameLoaded = (event) => {
+ iframe.removeEventListener("load", onIFrameLoaded, true);
+
+ let doc = event.target;
+ let win = doc.defaultView;
+ if ("setPanel" in win) {
+ win.setPanel(this._toolPanel, iframe);
+ }
+ this.emit(props.id + "-ready");
+ };
+
+ iframe.addEventListener("load", onIFrameLoaded, true);
+ iframe.setAttribute("src", props.url);
+ },
+
+ /**
+ * Remove an existing tab.
+ * @param {String} tabId The ID of the tab that was used to register it, or
+ * the tab id attribute value if the tab existed before the sidebar
+ * got created.
+ * @param {String} tabPanelId Optional. If provided, this ID will be used
+ * instead of the tabId to retrieve and remove the corresponding <tabpanel>
+ */
+ removeTab: Task.async(function* (tabId, tabPanelId) {
+ this._tabbar.removeTab(tabId);
+
+ let win = this.getWindowForTab(tabId);
+ if (win && ("destroy" in win)) {
+ yield win.destroy();
+ }
+
+ this.emit("tab-unregistered", tabId);
+ }),
+
+ /**
+ * Show or hide a specific tab.
+ * @param {Boolean} isVisible True to show the tab/tabpanel, False to hide it.
+ * @param {String} id The ID of the tab to be hidden.
+ */
+ toggleTab: function (isVisible, id) {
+ this._tabbar.toggleTab(id, isVisible);
+ },
+
+ /**
+ * Select a specific tab.
+ */
+ select: function (id) {
+ this._tabbar.select(id);
+ },
+
+ /**
+ * Return the id of the selected tab.
+ */
+ getCurrentTabID: function () {
+ return this._currentTool;
+ },
+
+ /**
+ * Returns the requested tab panel based on the id.
+ * @param {String} id
+ * @return {DOMNode}
+ */
+ getTabPanel: function (id) {
+ // Search with and without the ID prefix as there might have been existing
+ // tabpanels by the time the sidebar got created
+ return this._panelDoc.querySelector("#" +
+ this.TABPANEL_ID_PREFIX + id + ", #" + id);
+ },
+
+ /**
+ * Event handler.
+ */
+ handleSelectionChange: function (id) {
+ if (this._destroyed) {
+ return;
+ }
+
+ let previousTool = this._currentTool;
+ if (previousTool) {
+ if (this._telemetry) {
+ this._telemetry.toolClosed(previousTool);
+ }
+ this.emit(previousTool + "-unselected");
+ }
+
+ this._currentTool = id;
+
+ if (this._telemetry) {
+ this._telemetry.toolOpened(this._currentTool);
+ }
+
+ this.emit(this._currentTool + "-selected");
+ this.emit("select", this._currentTool);
+ },
+
+ /**
+ * Show the sidebar.
+ *
+ * @param {String} id
+ * The sidebar tab id to select.
+ */
+ show: function (id) {
+ this._tabbox.removeAttribute("hidden");
+
+ // If an id is given, select the corresponding sidebar tab and record the
+ // tool opened.
+ if (id) {
+ this._currentTool = id;
+
+ if (this._telemetry) {
+ this._telemetry.toolOpened(this._currentTool);
+ }
+ }
+
+ this.emit("show");
+ },
+
+ /**
+ * Show the sidebar.
+ */
+ hide: function () {
+ this._tabbox.setAttribute("hidden", "true");
+
+ this.emit("hide");
+ },
+
+ /**
+ * Return the window containing the tab content.
+ */
+ getWindowForTab: function (id) {
+ // Get the tabpanel and make sure it contains an iframe
+ let panel = this.getTabPanel(id);
+ if (!panel || !panel.firstElementChild || !panel.firstElementChild.contentWindow) {
+ return null;
+ }
+
+ return panel.firstElementChild.contentWindow;
+ },
+
+ /**
+ * Clean-up.
+ */
+ destroy: Task.async(function* () {
+ if (this._destroyed) {
+ return;
+ }
+ this._destroyed = true;
+
+ this.emit("destroy");
+
+ // Note that we check for the existence of this._tabbox.tabpanels at each
+ // step as the container window may have been closed by the time one of the
+ // panel's destroy promise resolves.
+ let tabpanels = [...this._tabbox.querySelectorAll(".tab-panel-box")];
+ for (let panel of tabpanels) {
+ let iframe = panel.querySelector("iframe");
+ if (!iframe) {
+ continue;
+ }
+ let win = iframe.contentWindow;
+ if (win && ("destroy" in win)) {
+ yield win.destroy();
+ }
+ panel.remove();
+ }
+
+ if (this._currentTool && this._telemetry) {
+ this._telemetry.toolClosed(this._currentTool);
+ }
+
+ this._toolPanel.emit("sidebar-destroyed", this);
+
+ this._tabs = null;
+ this._tabbox = null;
+ this._panelDoc = null;
+ this._toolPanel = null;
+ })
+};
diff --git a/devtools/client/jar.mn b/devtools/client/jar.mn
new file mode 100644
index 000000000..763a59fbd
--- /dev/null
+++ b/devtools/client/jar.mn
@@ -0,0 +1,351 @@
+# 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/.
+
+devtools.jar:
+% content devtools %content/
+ content/shared/vendor/d3.js (shared/vendor/d3.js)
+ content/shared/vendor/dagre-d3.js (shared/vendor/dagre-d3.js)
+ content/shared/widgets/widgets.css (shared/widgets/widgets.css)
+ content/shared/widgets/VariablesView.xul (shared/widgets/VariablesView.xul)
+ content/projecteditor/chrome/content/projecteditor.xul (projecteditor/chrome/content/projecteditor.xul)
+ content/projecteditor/lib/helpers/readdir.js (projecteditor/lib/helpers/readdir.js)
+ content/projecteditor/chrome/content/projecteditor-loader.xul (projecteditor/chrome/content/projecteditor-loader.xul)
+ content/projecteditor/chrome/content/projecteditor-test.xul (projecteditor/chrome/content/projecteditor-test.xul)
+ content/projecteditor/chrome/content/projecteditor-loader.js (projecteditor/chrome/content/projecteditor-loader.js)
+ content/netmonitor/netmonitor.xul (netmonitor/netmonitor.xul)
+ content/netmonitor/netmonitor-controller.js (netmonitor/netmonitor-controller.js)
+ content/netmonitor/netmonitor-view.js (netmonitor/netmonitor-view.js)
+ content/webconsole/webconsole.xul (webconsole/webconsole.xul)
+* content/scratchpad/scratchpad.xul (scratchpad/scratchpad.xul)
+ content/scratchpad/scratchpad.js (scratchpad/scratchpad.js)
+ content/shared/splitview.css (shared/splitview.css)
+ content/shared/theme-switching.js (shared/theme-switching.js)
+ content/shared/frame-script-utils.js (shared/frame-script-utils.js)
+ content/styleeditor/styleeditor.xul (styleeditor/styleeditor.xul)
+ content/storage/storage.xul (storage/storage.xul)
+ content/inspector/inspector.js (inspector/inspector.js)
+ content/inspector/markup/markup.xhtml (inspector/markup/markup.xhtml)
+ content/animationinspector/animation-controller.js (animationinspector/animation-controller.js)
+ content/animationinspector/animation-panel.js (animationinspector/animation-panel.js)
+ content/animationinspector/animation-inspector.xhtml (animationinspector/animation-inspector.xhtml)
+ content/sourceeditor/codemirror/addon/dialog/dialog.css (sourceeditor/codemirror/addon/dialog/dialog.css)
+ content/sourceeditor/codemirror/addon/hint/show-hint.js (sourceeditor/codemirror/addon/hint/show-hint.js)
+ content/sourceeditor/codemirror/addon/tern/tern.js (sourceeditor/codemirror/addon/tern/tern.js)
+ content/sourceeditor/codemirror/codemirror.bundle.js (sourceeditor/codemirror/codemirror.bundle.js)
+ content/sourceeditor/codemirror/lib/codemirror.css (sourceeditor/codemirror/lib/codemirror.css)
+ content/sourceeditor/codemirror/mozilla.css (sourceeditor/codemirror/mozilla.css)
+ content/debugger/new/index.html (debugger/new/index.html)
+ content/debugger/new/images/angle-brackets.svg (debugger/new/images/angle-brackets.svg)
+ content/debugger/new/images/arrow.svg (debugger/new/images/arrow.svg)
+ content/debugger/new/images/blackBox.svg (debugger/new/images/blackBox.svg)
+ content/debugger/new/images/breakpoint.svg (debugger/new/images/breakpoint.svg)
+ content/debugger/new/images/close.svg (debugger/new/images/close.svg)
+ content/debugger/new/images/disableBreakpoints.svg (debugger/new/images/disableBreakpoints.svg)
+ content/debugger/new/images/domain.svg (debugger/new/images/domain.svg)
+ content/debugger/new/images/file.svg (debugger/new/images/file.svg)
+ content/debugger/new/images/folder.svg (debugger/new/images/folder.svg)
+ content/debugger/new/images/globe.svg (debugger/new/images/globe.svg)
+ content/debugger/new/images/pause-circle.svg (debugger/new/images/pause-circle.svg)
+ content/debugger/new/images/pause.svg (debugger/new/images/pause.svg)
+ content/debugger/new/images/play.svg (debugger/new/images/play.svg)
+ content/debugger/new/images/prettyPrint.svg (debugger/new/images/prettyPrint.svg)
+ content/debugger/new/images/resume.svg (debugger/new/images/resume.svg)
+ content/debugger/new/images/stepIn.svg (debugger/new/images/stepIn.svg)
+ content/debugger/new/images/stepOut.svg (debugger/new/images/stepOut.svg)
+ content/debugger/new/images/stepOver.svg (debugger/new/images/stepOver.svg)
+ content/debugger/new/images/worker.svg (debugger/new/images/worker.svg)
+ content/debugger/new/images/settings.svg (debugger/new/images/settings.svg)
+ content/debugger/new/images/subSettings.svg (debugger/new/images/subSettings.svg)
+ content/debugger/debugger.xul (debugger/debugger.xul)
+ content/debugger/debugger.css (debugger/debugger.css)
+ content/debugger/debugger-controller.js (debugger/debugger-controller.js)
+ content/debugger/debugger-view.js (debugger/debugger-view.js)
+ content/debugger/views/workers-view.js (debugger/views/workers-view.js)
+ content/debugger/views/variable-bubble-view.js (debugger/views/variable-bubble-view.js)
+ content/debugger/views/watch-expressions-view.js (debugger/views/watch-expressions-view.js)
+ content/debugger/views/global-search-view.js (debugger/views/global-search-view.js)
+ content/debugger/views/toolbar-view.js (debugger/views/toolbar-view.js)
+ content/debugger/views/options-view.js (debugger/views/options-view.js)
+ content/debugger/views/stack-frames-view.js (debugger/views/stack-frames-view.js)
+ content/debugger/views/stack-frames-classic-view.js (debugger/views/stack-frames-classic-view.js)
+ content/debugger/views/filter-view.js (debugger/views/filter-view.js)
+ content/debugger/utils.js (debugger/utils.js)
+ content/shadereditor/shadereditor.xul (shadereditor/shadereditor.xul)
+ content/shadereditor/shadereditor.js (shadereditor/shadereditor.js)
+ content/canvasdebugger/canvasdebugger.xul (canvasdebugger/canvasdebugger.xul)
+ content/canvasdebugger/canvasdebugger.js (canvasdebugger/canvasdebugger.js)
+ content/canvasdebugger/snapshotslist.js (canvasdebugger/snapshotslist.js)
+ content/canvasdebugger/callslist.js (canvasdebugger/callslist.js)
+ content/webaudioeditor/webaudioeditor.xul (webaudioeditor/webaudioeditor.xul)
+ content/webaudioeditor/includes.js (webaudioeditor/includes.js)
+ content/webaudioeditor/models.js (webaudioeditor/models.js)
+ content/webaudioeditor/controller.js (webaudioeditor/controller.js)
+ content/webaudioeditor/views/utils.js (webaudioeditor/views/utils.js)
+ content/webaudioeditor/views/context.js (webaudioeditor/views/context.js)
+ content/webaudioeditor/views/inspector.js (webaudioeditor/views/inspector.js)
+ content/webaudioeditor/views/properties.js (webaudioeditor/views/properties.js)
+ content/webaudioeditor/views/automation.js (webaudioeditor/views/automation.js)
+ content/performance/performance.xul (performance/performance.xul)
+ content/performance/performance-controller.js (performance/performance-controller.js)
+ content/performance/performance-view.js (performance/performance-view.js)
+ content/performance/views/overview.js (performance/views/overview.js)
+ content/performance/views/toolbar.js (performance/views/toolbar.js)
+ content/performance/views/details.js (performance/views/details.js)
+ content/performance/views/details-abstract-subview.js (performance/views/details-abstract-subview.js)
+ content/performance/views/details-waterfall.js (performance/views/details-waterfall.js)
+ content/performance/views/details-js-call-tree.js (performance/views/details-js-call-tree.js)
+ content/performance/views/details-js-flamegraph.js (performance/views/details-js-flamegraph.js)
+ content/performance/views/details-memory-call-tree.js (performance/views/details-memory-call-tree.js)
+ content/performance/views/details-memory-flamegraph.js (performance/views/details-memory-flamegraph.js)
+ content/performance/views/recordings.js (performance/views/recordings.js)
+ content/memory/memory.xhtml (memory/memory.xhtml)
+ content/memory/initializer.js (memory/initializer.js)
+ content/commandline/commandline.css (commandline/commandline.css)
+ content/commandline/commandlineoutput.xhtml (commandline/commandlineoutput.xhtml)
+ content/commandline/commandlinetooltip.xhtml (commandline/commandlinetooltip.xhtml)
+ content/framework/toolbox-window.xul (framework/toolbox-window.xul)
+ content/framework/toolbox-options.xhtml (framework/toolbox-options.xhtml)
+ content/framework/toolbox.xul (framework/toolbox.xul)
+ content/framework/toolbox-init.js (framework/toolbox-init.js)
+ content/framework/options-panel.css (framework/options-panel.css)
+ content/framework/toolbox-process-window.xul (framework/toolbox-process-window.xul)
+* content/framework/toolbox-process-window.js (framework/toolbox-process-window.js)
+ content/framework/dev-edition-promo/dev-edition-promo.xul (framework/dev-edition-promo/dev-edition-promo.xul)
+* content/framework/dev-edition-promo/dev-edition-promo.css (framework/dev-edition-promo/dev-edition-promo.css)
+ content/framework/dev-edition-promo/dev-edition-logo.png (framework/dev-edition-promo/dev-edition-logo.png)
+ content/inspector/inspector.xhtml (inspector/inspector.xhtml)
+ content/framework/connect/connect.xhtml (framework/connect/connect.xhtml)
+ content/framework/connect/connect.css (framework/connect/connect.css)
+ content/framework/connect/connect.js (framework/connect/connect.js)
+ content/shared/widgets/graphs-frame.xhtml (shared/widgets/graphs-frame.xhtml)
+ content/shared/widgets/cubic-bezier.css (shared/widgets/cubic-bezier.css)
+ content/shared/widgets/mdn-docs.css (shared/widgets/mdn-docs.css)
+ content/shared/widgets/filter-widget.css (shared/widgets/filter-widget.css)
+ content/shared/widgets/spectrum.css (shared/widgets/spectrum.css)
+ content/aboutdebugging/aboutdebugging.xhtml (aboutdebugging/aboutdebugging.xhtml)
+ content/aboutdebugging/aboutdebugging.css (aboutdebugging/aboutdebugging.css)
+ content/aboutdebugging/initializer.js (aboutdebugging/initializer.js)
+ content/responsive.html/index.xhtml (responsive.html/index.xhtml)
+ content/responsive.html/index.js (responsive.html/index.js)
+ content/dom/dom.html (dom/dom.html)
+ content/dom/main.js (dom/main.js)
+% skin devtools classic/1.0 %skin/
+ skin/devtools-browser.css (themes/devtools-browser.css)
+ skin/dark-theme.css (themes/dark-theme.css)
+ skin/light-theme.css (themes/light-theme.css)
+ skin/firebug-theme.css (themes/firebug-theme.css)
+ skin/toolbars.css (themes/toolbars.css)
+ skin/toolbox.css (themes/toolbox.css)
+ skin/tooltips.css (themes/tooltips.css)
+ skin/images/add.svg (themes/images/add.svg)
+ skin/images/filters.svg (themes/images/filters.svg)
+ skin/images/filter-swatch.svg (themes/images/filter-swatch.svg)
+ skin/images/grid.svg (themes/images/grid.svg)
+ skin/images/angle-swatch.svg (themes/images/angle-swatch.svg)
+ skin/images/pseudo-class.svg (themes/images/pseudo-class.svg)
+ skin/images/controls.png (themes/images/controls.png)
+ skin/images/controls@2x.png (themes/images/controls@2x.png)
+ skin/images/animation-fast-track.svg (themes/images/animation-fast-track.svg)
+ skin/images/performance-icons.svg (themes/images/performance-icons.svg)
+ skin/widgets.css (themes/widgets.css)
+ skin/images/power.svg (themes/images/power.svg)
+ skin/images/filetypes/dir-close.svg (themes/images/filetypes/dir-close.svg)
+ skin/images/filetypes/dir-open.svg (themes/images/filetypes/dir-open.svg)
+ skin/images/filetypes/globe.svg (themes/images/filetypes/globe.svg)
+ skin/images/commandline-icon.svg (themes/images/commandline-icon.svg)
+ skin/images/alerticon-warning.png (themes/images/alerticon-warning.png)
+ skin/images/alerticon-warning@2x.png (themes/images/alerticon-warning@2x.png)
+ skin/rules.css (themes/rules.css)
+ skin/commandline.css (themes/commandline.css)
+ skin/images/command-paintflashing.svg (themes/images/command-paintflashing.svg)
+ skin/images/command-screenshot.svg (themes/images/command-screenshot.svg)
+ skin/images/command-responsivemode.svg (themes/images/command-responsivemode.svg)
+ skin/images/command-pick.svg (themes/images/command-pick.svg)
+ skin/images/command-frames.svg (themes/images/command-frames.svg)
+ skin/images/command-console.svg (themes/images/command-console.svg)
+ skin/images/command-eyedropper.svg (themes/images/command-eyedropper.svg)
+ skin/images/command-rulers.svg (themes/images/command-rulers.svg)
+ skin/images/command-measure.svg (themes/images/command-measure.svg)
+ skin/images/command-noautohide.svg (themes/images/command-noautohide.svg)
+ skin/markup.css (themes/markup.css)
+ skin/images/editor-error.png (themes/images/editor-error.png)
+ skin/images/breakpoint.svg (themes/images/breakpoint.svg)
+ skin/webconsole.css (themes/webconsole.css)
+ skin/images/webconsole.svg (themes/images/webconsole.svg)
+ skin/images/breadcrumbs-scrollbutton.png (themes/images/breadcrumbs-scrollbutton.png)
+ skin/images/breadcrumbs-scrollbutton@2x.png (themes/images/breadcrumbs-scrollbutton@2x.png)
+ skin/animationinspector.css (themes/animationinspector.css)
+ skin/canvasdebugger.css (themes/canvasdebugger.css)
+ skin/debugger.css (themes/debugger.css)
+ skin/netmonitor.css (themes/netmonitor.css)
+ skin/dom.css (themes/dom.css)
+ skin/performance.css (themes/performance.css)
+ skin/memory.css (themes/memory.css)
+ skin/scratchpad.css (themes/scratchpad.css)
+ skin/shadereditor.css (themes/shadereditor.css)
+ skin/storage.css (themes/storage.css)
+ skin/splitview.css (themes/splitview.css)
+ skin/styleeditor.css (themes/styleeditor.css)
+ skin/webaudioeditor.css (themes/webaudioeditor.css)
+ skin/components-frame.css (themes/components-frame.css)
+ skin/components-h-split-box.css (themes/components-h-split-box.css)
+ skin/jit-optimizations.css (themes/jit-optimizations.css)
+ skin/images/filter.svg (themes/images/filter.svg)
+ skin/images/search.svg (themes/images/search.svg)
+ skin/images/item-toggle.svg (themes/images/item-toggle.svg)
+ skin/images/item-arrow-dark-rtl.svg (themes/images/item-arrow-dark-rtl.svg)
+ skin/images/item-arrow-dark-ltr.svg (themes/images/item-arrow-dark-ltr.svg)
+ skin/images/item-arrow-rtl.svg (themes/images/item-arrow-rtl.svg)
+ skin/images/item-arrow-ltr.svg (themes/images/item-arrow-ltr.svg)
+ skin/images/noise.png (themes/images/noise.png)
+ skin/images/dropmarker.svg (themes/images/dropmarker.svg)
+ skin/boxmodel.css (themes/boxmodel.css)
+ skin/images/geometry-editor.svg (themes/images/geometry-editor.svg)
+ skin/images/pause.svg (themes/images/pause.svg)
+ skin/images/play.svg (themes/images/play.svg)
+ skin/images/fast-forward.svg (themes/images/fast-forward.svg)
+ skin/images/rewind.svg (themes/images/rewind.svg)
+ skin/images/debugger-step-in.svg (themes/images/debugger-step-in.svg)
+ skin/images/debugger-step-out.svg (themes/images/debugger-step-out.svg)
+ skin/images/debugger-step-over.svg (themes/images/debugger-step-over.svg)
+ skin/images/debugger-toggleBreakpoints.svg (themes/images/debugger-toggleBreakpoints.svg)
+ skin/images/tracer-icon.png (themes/images/tracer-icon.png)
+ skin/images/tracer-icon@2x.png (themes/images/tracer-icon@2x.png)
+ skin/images/responsivemode/responsive-se-resizer.png (themes/images/responsivemode/responsive-se-resizer.png)
+ skin/images/responsivemode/responsive-se-resizer@2x.png (themes/images/responsivemode/responsive-se-resizer@2x.png)
+ skin/images/responsivemode/responsive-vertical-resizer.png (themes/images/responsivemode/responsive-vertical-resizer.png)
+ skin/images/responsivemode/responsive-vertical-resizer@2x.png (themes/images/responsivemode/responsive-vertical-resizer@2x.png)
+ skin/images/responsivemode/responsive-horizontal-resizer.png (themes/images/responsivemode/responsive-horizontal-resizer.png)
+ skin/images/responsivemode/responsive-horizontal-resizer@2x.png (themes/images/responsivemode/responsive-horizontal-resizer@2x.png)
+ skin/images/responsivemode/responsiveui-rotate.png (themes/images/responsivemode/responsiveui-rotate.png)
+ skin/images/responsivemode/responsiveui-rotate@2x.png (themes/images/responsivemode/responsiveui-rotate@2x.png)
+ skin/images/responsivemode/responsiveui-touch.png (themes/images/responsivemode/responsiveui-touch.png)
+ skin/images/responsivemode/responsiveui-touch@2x.png (themes/images/responsivemode/responsiveui-touch@2x.png)
+ skin/images/responsivemode/responsiveui-screenshot.png (themes/images/responsivemode/responsiveui-screenshot.png)
+ skin/images/responsivemode/responsiveui-screenshot@2x.png (themes/images/responsivemode/responsiveui-screenshot@2x.png)
+ skin/images/responsivemode/responsiveui-home.png (themes/images/responsivemode/responsiveui-home.png)
+ skin/images/toggle-tools.png (themes/images/toggle-tools.png)
+ skin/images/toggle-tools@2x.png (themes/images/toggle-tools@2x.png)
+ skin/images/dock-bottom.svg (themes/images/dock-bottom.svg)
+ skin/images/dock-side.svg (themes/images/dock-side.svg)
+ skin/images/dock-undock.svg (themes/images/dock-undock.svg)
+ skin/floating-scrollbars-dark-theme.css (themes/floating-scrollbars-dark-theme.css)
+ skin/floating-scrollbars-responsive-design.css (themes/floating-scrollbars-responsive-design.css)
+ skin/inspector.css (themes/inspector.css)
+ skin/images/profiler-stopwatch.svg (themes/images/profiler-stopwatch.svg)
+ skin/images/emojis/emoji-command-pick.svg (themes/images/emojis/emoji-command-pick.svg)
+ skin/images/emojis/emoji-tool-webconsole.svg (themes/images/emojis/emoji-tool-webconsole.svg)
+ skin/images/emojis/emoji-tool-canvas.svg (themes/images/emojis/emoji-tool-canvas.svg)
+ skin/images/emojis/emoji-tool-debugger.svg (themes/images/emojis/emoji-tool-debugger.svg)
+ skin/images/emojis/emoji-tool-inspector.svg (themes/images/emojis/emoji-tool-inspector.svg)
+ skin/images/emojis/emoji-tool-shadereditor.svg (themes/images/emojis/emoji-tool-shadereditor.svg)
+ skin/images/emojis/emoji-tool-styleeditor.svg (themes/images/emojis/emoji-tool-styleeditor.svg)
+ skin/images/emojis/emoji-tool-storage.svg (themes/images/emojis/emoji-tool-storage.svg)
+ skin/images/emojis/emoji-tool-profiler.svg (themes/images/emojis/emoji-tool-profiler.svg)
+ skin/images/emojis/emoji-tool-network.svg (themes/images/emojis/emoji-tool-network.svg)
+ skin/images/emojis/emoji-tool-scratchpad.svg (themes/images/emojis/emoji-tool-scratchpad.svg)
+ skin/images/emojis/emoji-tool-webaudio.svg (themes/images/emojis/emoji-tool-webaudio.svg)
+ skin/images/emojis/emoji-tool-memory.svg (themes/images/emojis/emoji-tool-memory.svg)
+ skin/images/emojis/emoji-tool-dom.svg (themes/images/emojis/emoji-tool-dom.svg)
+ skin/images/debugging-addons.svg (themes/images/debugging-addons.svg)
+ skin/images/debugging-devices.svg (themes/images/debugging-devices.svg)
+ skin/images/debugging-tabs.svg (themes/images/debugging-tabs.svg)
+ skin/images/debugging-workers.svg (themes/images/debugging-workers.svg)
+ skin/images/globe.svg (themes/images/globe.svg)
+ skin/images/tool-options.svg (themes/images/tool-options.svg)
+ skin/images/tool-webconsole.svg (themes/images/tool-webconsole.svg)
+ skin/images/tool-canvas.svg (themes/images/tool-canvas.svg)
+ skin/images/tool-debugger.svg (themes/images/tool-debugger.svg)
+ skin/images/tool-debugger-paused.svg (themes/images/tool-debugger-paused.svg)
+ skin/images/tool-inspector.svg (themes/images/tool-inspector.svg)
+ skin/images/tool-shadereditor.svg (themes/images/tool-shadereditor.svg)
+ skin/images/tool-styleeditor.svg (themes/images/tool-styleeditor.svg)
+ skin/images/tool-storage.svg (themes/images/tool-storage.svg)
+ skin/images/tool-profiler.svg (themes/images/tool-profiler.svg)
+ skin/images/tool-profiler-active.svg (themes/images/tool-profiler-active.svg)
+ skin/images/tool-network.svg (themes/images/tool-network.svg)
+ skin/images/tool-scratchpad.svg (themes/images/tool-scratchpad.svg)
+ skin/images/tool-webaudio.svg (themes/images/tool-webaudio.svg)
+ skin/images/tool-memory.svg (themes/images/tool-memory.svg)
+ skin/images/tool-memory-active.svg (themes/images/tool-memory-active.svg)
+ skin/images/tool-dom.svg (themes/images/tool-dom.svg)
+ skin/images/close.svg (themes/images/close.svg)
+ skin/images/clear.svg (themes/images/clear.svg)
+ skin/images/vview-delete.png (themes/images/vview-delete.png)
+ skin/images/vview-delete@2x.png (themes/images/vview-delete@2x.png)
+ skin/images/vview-edit.png (themes/images/vview-edit.png)
+ skin/images/vview-edit@2x.png (themes/images/vview-edit@2x.png)
+ skin/images/vview-lock.png (themes/images/vview-lock.png)
+ skin/images/vview-lock@2x.png (themes/images/vview-lock@2x.png)
+ skin/images/vview-open-inspector.png (themes/images/vview-open-inspector.png)
+ skin/images/vview-open-inspector@2x.png (themes/images/vview-open-inspector@2x.png)
+ skin/images/sort-arrows.svg (themes/images/sort-arrows.svg)
+ skin/images/cubic-bezier-swatch.png (themes/images/cubic-bezier-swatch.png)
+ skin/images/cubic-bezier-swatch@2x.png (themes/images/cubic-bezier-swatch@2x.png)
+ skin/fonts.css (themes/fonts.css)
+ skin/computed.css (themes/computed.css)
+ skin/layout.css (themes/layout.css)
+ skin/images/arrow-e.png (themes/images/arrow-e.png)
+ skin/images/arrow-e@2x.png (themes/images/arrow-e@2x.png)
+ skin/projecteditor/projecteditor.css (themes/projecteditor/projecteditor.css)
+ skin/images/search-clear-failed.svg (themes/images/search-clear-failed.svg)
+ skin/images/search-clear-light.svg (themes/images/search-clear-light.svg)
+ skin/images/search-clear-dark.svg (themes/images/search-clear-dark.svg)
+ skin/tooltip/arrow-horizontal-dark.png (themes/tooltip/arrow-horizontal-dark.png)
+ skin/tooltip/arrow-horizontal-dark@2x.png (themes/tooltip/arrow-horizontal-dark@2x.png)
+ skin/tooltip/arrow-vertical-dark.png (themes/tooltip/arrow-vertical-dark.png)
+ skin/tooltip/arrow-vertical-dark@2x.png (themes/tooltip/arrow-vertical-dark@2x.png)
+ skin/tooltip/arrow-horizontal-light.png (themes/tooltip/arrow-horizontal-light.png)
+ skin/tooltip/arrow-horizontal-light@2x.png (themes/tooltip/arrow-horizontal-light@2x.png)
+ skin/tooltip/arrow-vertical-light.png (themes/tooltip/arrow-vertical-light.png)
+ skin/tooltip/arrow-vertical-light@2x.png (themes/tooltip/arrow-vertical-light@2x.png)
+ skin/images/reload.svg (themes/images/reload.svg)
+ skin/images/security-state-broken.svg (themes/images/security-state-broken.svg)
+ skin/images/security-state-insecure.svg (themes/images/security-state-insecure.svg)
+ skin/images/security-state-secure.svg (themes/images/security-state-secure.svg)
+ skin/images/security-state-weak.svg (themes/images/security-state-weak.svg)
+ skin/images/diff.svg (themes/images/diff.svg)
+ skin/images/import.svg (themes/images/import.svg)
+ skin/images/pane-collapse.svg (themes/images/pane-collapse.svg)
+ skin/images/pane-expand.svg (themes/images/pane-expand.svg)
+
+ # Firebug Theme
+ skin/images/firebug/read-only.svg (themes/images/firebug/read-only.svg)
+ skin/images/firebug/spinner.png (themes/images/firebug/spinner.png)
+ skin/images/firebug/twisty-closed-firebug.svg (themes/images/firebug/twisty-closed-firebug.svg)
+ skin/images/firebug/twisty-open-firebug.svg (themes/images/firebug/twisty-open-firebug.svg)
+ skin/images/firebug/arrow-down.svg (themes/images/firebug/arrow-down.svg)
+ skin/images/firebug/arrow-up.svg (themes/images/firebug/arrow-up.svg)
+ skin/images/firebug/close.svg (themes/images/firebug/close.svg)
+ skin/images/firebug/pause.svg (themes/images/firebug/pause.svg)
+ skin/images/firebug/play.svg (themes/images/firebug/play.svg)
+ skin/images/firebug/rewind.svg (themes/images/firebug/rewind.svg)
+ skin/images/firebug/disable.svg (themes/images/firebug/disable.svg)
+ skin/images/firebug/breadcrumbs-divider.svg (themes/images/firebug/breadcrumbs-divider.svg)
+ skin/images/firebug/breakpoint.svg (themes/images/firebug/breakpoint.svg)
+ skin/images/firebug/tool-options.svg (themes/images/firebug/tool-options.svg)
+ skin/images/firebug/debugger-step-in.svg (themes/images/firebug/debugger-step-in.svg)
+ skin/images/firebug/debugger-step-out.svg (themes/images/firebug/debugger-step-out.svg)
+ skin/images/firebug/debugger-step-over.svg (themes/images/firebug/debugger-step-over.svg)
+ skin/images/firebug/pane-collapse.svg (themes/images/firebug/pane-collapse.svg)
+ skin/images/firebug/pane-expand.svg (themes/images/firebug/pane-expand.svg)
+ skin/images/firebug/dock-undock.svg (themes/images/firebug/dock-undock.svg)
+ skin/images/firebug/dock-side.svg (themes/images/firebug/dock-side.svg)
+ skin/images/firebug/dock-bottom.svg (themes/images/firebug/dock-bottom.svg)
+ skin/images/firebug/commandline-icon.svg (themes/images/firebug/commandline-icon.svg)
+ skin/images/firebug/debugger-blackbox.svg (themes/images/firebug/debugger-blackbox.svg)
+ skin/images/firebug/debugger-prettyprint.svg (themes/images/firebug/debugger-prettyprint.svg)
+ skin/images/firebug/debugger-toggleBreakpoints.svg (themes/images/firebug/debugger-toggleBreakpoints.svg)
+ skin/images/firebug/tool-debugger-paused.svg (themes/images/firebug/tool-debugger-paused.svg)
+ skin/images/firebug/command-pick.svg (themes/images/firebug/command-pick.svg)
+ skin/images/firebug/command-console.svg (themes/images/firebug/command-console.svg)
+ skin/images/firebug/command-eyedropper.svg (themes/images/firebug/command-eyedropper.svg)
+ skin/images/firebug/command-frames.svg (themes/images/firebug/command-frames.svg)
+ skin/images/firebug/command-paintflashing.svg (themes/images/firebug/command-paintflashing.svg)
+ skin/images/firebug/command-responsivemode.svg (themes/images/firebug/command-responsivemode.svg)
+ skin/images/firebug/command-scratchpad.svg (themes/images/firebug/command-scratchpad.svg)
+ skin/images/firebug/command-screenshot.svg (themes/images/firebug/command-screenshot.svg)
+ skin/images/firebug/command-measure.svg (themes/images/firebug/command-measure.svg)
+ skin/images/firebug/command-rulers.svg (themes/images/firebug/command-rulers.svg)
+ skin/images/firebug/command-noautohide.svg (themes/images/firebug/command-noautohide.svg)
diff --git a/devtools/client/jsonview/.eslintrc.js b/devtools/client/jsonview/.eslintrc.js
new file mode 100644
index 000000000..bd1e31981
--- /dev/null
+++ b/devtools/client/jsonview/.eslintrc.js
@@ -0,0 +1,11 @@
+"use strict";
+
+module.exports = {
+ "globals": {
+ "define": true,
+ "document": true,
+ "window": true,
+ "CustomEvent": true,
+ "Locale": true
+ }
+};
diff --git a/devtools/client/jsonview/components/headers-panel.js b/devtools/client/jsonview/components/headers-panel.js
new file mode 100644
index 000000000..9229aaa01
--- /dev/null
+++ b/devtools/client/jsonview/components/headers-panel.js
@@ -0,0 +1,79 @@
+/* -*- 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";
+
+define(function (require, exports, module) {
+ const { DOM: dom, createFactory, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
+ const { createFactories } = require("devtools/client/shared/components/reps/rep-utils");
+ const { Headers } = createFactories(require("./headers"));
+ const { Toolbar, ToolbarButton } = createFactories(require("./reps/toolbar"));
+
+ const { div } = dom;
+
+ /**
+ * This template represents the 'Headers' panel
+ * s responsible for rendering its content.
+ */
+ let HeadersPanel = createClass({
+ displayName: "HeadersPanel",
+
+ propTypes: {
+ actions: PropTypes.object,
+ data: PropTypes.object,
+ },
+
+ getInitialState: function () {
+ return {
+ data: {}
+ };
+ },
+
+ render: function () {
+ let data = this.props.data;
+
+ return (
+ div({className: "headersPanelBox"},
+ HeadersToolbar({actions: this.props.actions}),
+ div({className: "panelContent"},
+ Headers({data: data})
+ )
+ )
+ );
+ }
+ });
+
+ /**
+ * This template is responsible for rendering a toolbar
+ * within the 'Headers' panel.
+ */
+ let HeadersToolbar = createFactory(createClass({
+ displayName: "HeadersToolbar",
+
+ propTypes: {
+ actions: PropTypes.object,
+ },
+
+ // Commands
+
+ onCopy: function (event) {
+ this.props.actions.onCopyHeaders();
+ },
+
+ render: function () {
+ return (
+ Toolbar({},
+ ToolbarButton({className: "btn copy", onClick: this.onCopy},
+ Locale.$STR("jsonViewer.Copy")
+ )
+ )
+ );
+ },
+ }));
+
+ // Exports from this module
+ exports.HeadersPanel = HeadersPanel;
+});
diff --git a/devtools/client/jsonview/components/headers.js b/devtools/client/jsonview/components/headers.js
new file mode 100644
index 000000000..38ac4051c
--- /dev/null
+++ b/devtools/client/jsonview/components/headers.js
@@ -0,0 +1,105 @@
+/* -*- 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";
+
+define(function (require, exports, module) {
+ const { DOM: dom, createFactory, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
+
+ const { div, span, table, tbody, tr, td, } = dom;
+
+ /**
+ * This template is responsible for rendering basic layout
+ * of the 'Headers' panel. It displays HTTP headers groups such as
+ * received or response headers.
+ */
+ let Headers = createClass({
+ displayName: "Headers",
+
+ propTypes: {
+ data: PropTypes.object,
+ },
+
+ getInitialState: function () {
+ return {};
+ },
+
+ render: function () {
+ let data = this.props.data;
+
+ return (
+ div({className: "netInfoHeadersTable"},
+ div({className: "netHeadersGroup"},
+ div({className: "netInfoHeadersGroup"},
+ Locale.$STR("jsonViewer.responseHeaders")
+ ),
+ table({cellPadding: 0, cellSpacing: 0},
+ HeaderList({headers: data.response})
+ )
+ ),
+ div({className: "netHeadersGroup"},
+ div({className: "netInfoHeadersGroup"},
+ Locale.$STR("jsonViewer.requestHeaders")
+ ),
+ table({cellPadding: 0, cellSpacing: 0},
+ HeaderList({headers: data.request})
+ )
+ )
+ )
+ );
+ }
+ });
+
+ /**
+ * This template renders headers list,
+ * name + value pairs.
+ */
+ let HeaderList = createFactory(createClass({
+ displayName: "HeaderList",
+
+ propTypes: {
+ headers: PropTypes.arrayOf(PropTypes.shape({
+ name: PropTypes.string,
+ value: PropTypes.string
+ }))
+ },
+
+ getInitialState: function () {
+ return {
+ headers: []
+ };
+ },
+
+ render: function () {
+ let headers = this.props.headers;
+
+ headers.sort(function (a, b) {
+ return a.name > b.name ? 1 : -1;
+ });
+
+ let rows = [];
+ headers.forEach(header => {
+ rows.push(
+ tr({key: header.name},
+ td({className: "netInfoParamName"},
+ span({title: header.name}, header.name)
+ ),
+ td({className: "netInfoParamValue"}, header.value)
+ )
+ );
+ });
+
+ return (
+ tbody({},
+ rows
+ )
+ );
+ }
+ }));
+
+ // Exports from this module
+ exports.Headers = Headers;
+});
diff --git a/devtools/client/jsonview/components/json-panel.js b/devtools/client/jsonview/components/json-panel.js
new file mode 100644
index 000000000..c7280a0b1
--- /dev/null
+++ b/devtools/client/jsonview/components/json-panel.js
@@ -0,0 +1,194 @@
+/* -*- 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";
+
+define(function (require, exports, module) {
+ const { DOM: dom, createFactory, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
+ const { createFactories } = require("devtools/client/shared/components/reps/rep-utils");
+ const TreeView = createFactory(require("devtools/client/shared/components/tree/tree-view"));
+ const { Rep } = createFactories(require("devtools/client/shared/components/reps/rep"));
+ const { SearchBox } = createFactories(require("./search-box"));
+ const { Toolbar, ToolbarButton } = createFactories(require("./reps/toolbar"));
+
+ const { div } = dom;
+ const AUTO_EXPAND_MAX_SIZE = 100 * 1024;
+ const AUTO_EXPAND_MAX_LEVEL = 7;
+
+ /**
+ * This template represents the 'JSON' panel. The panel is
+ * responsible for rendering an expandable tree that allows simple
+ * inspection of JSON structure.
+ */
+ let JsonPanel = createClass({
+ displayName: "JsonPanel",
+
+ propTypes: {
+ data: PropTypes.oneOfType([
+ PropTypes.string,
+ PropTypes.array,
+ PropTypes.object
+ ]),
+ jsonTextLength: PropTypes.number,
+ searchFilter: PropTypes.string,
+ actions: PropTypes.object,
+ },
+
+ getInitialState: function () {
+ return {};
+ },
+
+ componentDidMount: function () {
+ document.addEventListener("keypress", this.onKeyPress, true);
+ },
+
+ componentWillUnmount: function () {
+ document.removeEventListener("keypress", this.onKeyPress, true);
+ },
+
+ onKeyPress: function (e) {
+ // XXX shortcut for focusing the Filter field (see Bug 1178771).
+ },
+
+ onFilter: function (object) {
+ if (!this.props.searchFilter) {
+ return true;
+ }
+
+ let json = JSON.stringify(object).toLowerCase();
+ return json.indexOf(this.props.searchFilter.toLowerCase()) >= 0;
+ },
+
+ getExpandedNodes: function (object, path = "", level = 0) {
+ if (typeof object != "object") {
+ return null;
+ }
+
+ if (level > AUTO_EXPAND_MAX_LEVEL) {
+ return null;
+ }
+
+ let expandedNodes = new Set();
+ for (let prop in object) {
+ let nodePath = path + "/" + prop;
+ expandedNodes.add(nodePath);
+
+ let nodes = this.getExpandedNodes(object[prop], nodePath, level + 1);
+ if (nodes) {
+ expandedNodes = new Set([...expandedNodes, ...nodes]);
+ }
+ }
+ return expandedNodes;
+ },
+
+ renderValue: props => {
+ let member = props.member;
+
+ // Hide object summary when object is expanded (bug 1244912).
+ if (typeof member.value == "object" && member.open) {
+ return null;
+ }
+
+ // Render the value (summary) using Reps library.
+ return Rep(Object.assign({}, props, {
+ cropLimit: 50,
+ }));
+ },
+
+ renderTree: function () {
+ // Append custom column for displaying values. This column
+ // Take all available horizontal space.
+ let columns = [{
+ id: "value",
+ width: "100%"
+ }];
+
+ // Expand the document by default if its size isn't bigger than 100KB.
+ let expandedNodes = new Set();
+ if (this.props.jsonTextLength <= AUTO_EXPAND_MAX_SIZE) {
+ expandedNodes = this.getExpandedNodes(this.props.data);
+ }
+
+ // Render tree component.
+ return TreeView({
+ object: this.props.data,
+ mode: "tiny",
+ onFilter: this.onFilter,
+ columns: columns,
+ renderValue: this.renderValue,
+ expandedNodes: expandedNodes,
+ });
+ },
+
+ render: function () {
+ let content;
+ let data = this.props.data;
+
+ try {
+ if (typeof data == "object") {
+ content = this.renderTree();
+ } else {
+ content = div({className: "jsonParseError"},
+ data + ""
+ );
+ }
+ } catch (err) {
+ content = div({className: "jsonParseError"},
+ err + ""
+ );
+ }
+
+ return (
+ div({className: "jsonPanelBox"},
+ JsonToolbar({actions: this.props.actions}),
+ div({className: "panelContent"},
+ content
+ )
+ )
+ );
+ }
+ });
+
+ /**
+ * This template represents a toolbar within the 'JSON' panel.
+ */
+ let JsonToolbar = createFactory(createClass({
+ displayName: "JsonToolbar",
+
+ propTypes: {
+ actions: PropTypes.object,
+ },
+
+ // Commands
+
+ onSave: function (event) {
+ this.props.actions.onSaveJson();
+ },
+
+ onCopy: function (event) {
+ this.props.actions.onCopyJson();
+ },
+
+ render: function () {
+ return (
+ Toolbar({},
+ ToolbarButton({className: "btn save", onClick: this.onSave},
+ Locale.$STR("jsonViewer.Save")
+ ),
+ ToolbarButton({className: "btn copy", onClick: this.onCopy},
+ Locale.$STR("jsonViewer.Copy")
+ ),
+ SearchBox({
+ actions: this.props.actions
+ })
+ )
+ );
+ },
+ }));
+
+ // Exports from this module
+ exports.JsonPanel = JsonPanel;
+});
diff --git a/devtools/client/jsonview/components/main-tabbed-area.js b/devtools/client/jsonview/components/main-tabbed-area.js
new file mode 100644
index 000000000..ecba73807
--- /dev/null
+++ b/devtools/client/jsonview/components/main-tabbed-area.js
@@ -0,0 +1,89 @@
+/* -*- 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";
+
+define(function (require, exports, module) {
+ const { createClass, PropTypes } = require("devtools/client/shared/vendor/react");
+ const { createFactories } = require("devtools/client/shared/components/reps/rep-utils");
+ const { JsonPanel } = createFactories(require("./json-panel"));
+ const { TextPanel } = createFactories(require("./text-panel"));
+ const { HeadersPanel } = createFactories(require("./headers-panel"));
+ const { Tabs, TabPanel } = createFactories(require("devtools/client/shared/components/tabs/tabs"));
+
+ /**
+ * This object represents the root application template
+ * responsible for rendering the basic tab layout.
+ */
+ let MainTabbedArea = createClass({
+ displayName: "MainTabbedArea",
+
+ propTypes: {
+ jsonText: PropTypes.string,
+ tabActive: PropTypes.number,
+ actions: PropTypes.object,
+ headers: PropTypes.object,
+ searchFilter: PropTypes.string,
+ json: PropTypes.oneOfType([
+ PropTypes.string,
+ PropTypes.object,
+ PropTypes.array
+ ])
+ },
+
+ getInitialState: function () {
+ return {
+ json: {},
+ headers: {},
+ jsonText: this.props.jsonText,
+ tabActive: this.props.tabActive
+ };
+ },
+
+ onTabChanged: function (index) {
+ this.setState({tabActive: index});
+ },
+
+ render: function () {
+ return (
+ Tabs({
+ tabActive: this.state.tabActive,
+ onAfterChange: this.onTabChanged},
+ TabPanel({
+ className: "json",
+ title: Locale.$STR("jsonViewer.tab.JSON")},
+ JsonPanel({
+ data: this.props.json,
+ jsonTextLength: this.props.jsonText.length,
+ actions: this.props.actions,
+ searchFilter: this.state.searchFilter
+ })
+ ),
+ TabPanel({
+ className: "rawdata",
+ title: Locale.$STR("jsonViewer.tab.RawData")},
+ TextPanel({
+ data: this.state.jsonText,
+ actions: this.props.actions
+ })
+ ),
+ TabPanel({
+ className: "headers",
+ title: Locale.$STR("jsonViewer.tab.Headers")},
+ HeadersPanel({
+ data: this.props.headers,
+ actions: this.props.actions,
+ searchFilter: this.props.searchFilter
+ })
+ )
+ )
+ );
+ }
+ });
+
+ // Exports from this module
+ exports.MainTabbedArea = MainTabbedArea;
+});
diff --git a/devtools/client/jsonview/components/moz.build b/devtools/client/jsonview/components/moz.build
new file mode 100644
index 000000000..fa66c8709
--- /dev/null
+++ b/devtools/client/jsonview/components/moz.build
@@ -0,0 +1,18 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DIRS += [
+ 'reps'
+]
+
+DevToolsModules(
+ 'headers-panel.js',
+ 'headers.js',
+ 'json-panel.js',
+ 'main-tabbed-area.js',
+ 'search-box.js',
+ 'text-panel.js'
+)
diff --git a/devtools/client/jsonview/components/reps/moz.build b/devtools/client/jsonview/components/reps/moz.build
new file mode 100644
index 000000000..1d239b7bd
--- /dev/null
+++ b/devtools/client/jsonview/components/reps/moz.build
@@ -0,0 +1,9 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'toolbar.js',
+)
diff --git a/devtools/client/jsonview/components/reps/toolbar.js b/devtools/client/jsonview/components/reps/toolbar.js
new file mode 100644
index 000000000..52a35ffbe
--- /dev/null
+++ b/devtools/client/jsonview/components/reps/toolbar.js
@@ -0,0 +1,58 @@
+/* -*- 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";
+
+define(function (require, exports, module) {
+ const React = require("devtools/client/shared/vendor/react");
+ const DOM = React.DOM;
+
+ /**
+ * Renders a simple toolbar.
+ */
+ let Toolbar = React.createClass({
+ displayName: "Toolbar",
+
+ propTypes: {
+ children: React.PropTypes.oneOfType([
+ React.PropTypes.array,
+ React.PropTypes.element
+ ])
+ },
+
+ render: function () {
+ return (
+ DOM.div({className: "toolbar"},
+ this.props.children
+ )
+ );
+ }
+ });
+
+ /**
+ * Renders a simple toolbar button.
+ */
+ let ToolbarButton = React.createClass({
+ displayName: "ToolbarButton",
+
+ propTypes: {
+ active: React.PropTypes.bool,
+ disabled: React.PropTypes.bool,
+ children: React.PropTypes.string,
+ },
+
+ render: function () {
+ let props = Object.assign({className: "btn"}, this.props);
+ return (
+ DOM.button(props, this.props.children)
+ );
+ },
+ });
+
+ // Exports from this module
+ exports.Toolbar = Toolbar;
+ exports.ToolbarButton = ToolbarButton;
+});
diff --git a/devtools/client/jsonview/components/search-box.js b/devtools/client/jsonview/components/search-box.js
new file mode 100644
index 000000000..fc9bcbcb8
--- /dev/null
+++ b/devtools/client/jsonview/components/search-box.js
@@ -0,0 +1,55 @@
+/* -*- 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";
+
+define(function (require, exports, module) {
+ const { DOM: dom, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
+
+ const { input } = dom;
+
+ // For smooth incremental searching (in case the user is typing quickly).
+ const searchDelay = 250;
+
+ /**
+ * This object represents a search box located at the
+ * top right corner of the application.
+ */
+ let SearchBox = createClass({
+ displayName: "SearchBox",
+
+ propTypes: {
+ actions: PropTypes.object,
+ },
+
+ onSearch: function (event) {
+ let searchBox = event.target;
+ let win = searchBox.ownerDocument.defaultView;
+
+ if (this.searchTimeout) {
+ win.clearTimeout(this.searchTimeout);
+ }
+
+ let callback = this.doSearch.bind(this, searchBox);
+ this.searchTimeout = win.setTimeout(callback, searchDelay);
+ },
+
+ doSearch: function (searchBox) {
+ this.props.actions.onSearch(searchBox.value);
+ },
+
+ render: function () {
+ return (
+ input({className: "searchBox",
+ placeholder: Locale.$STR("jsonViewer.filterJSON"),
+ onChange: this.onSearch})
+ );
+ },
+ });
+
+ // Exports from this module
+ exports.SearchBox = SearchBox;
+});
diff --git a/devtools/client/jsonview/components/text-panel.js b/devtools/client/jsonview/components/text-panel.js
new file mode 100644
index 000000000..1df2e349d
--- /dev/null
+++ b/devtools/client/jsonview/components/text-panel.js
@@ -0,0 +1,95 @@
+/* -*- 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";
+
+define(function (require, exports, module) {
+ const { DOM: dom, createFactory, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
+ const { createFactories } = require("devtools/client/shared/components/reps/rep-utils");
+ const { Toolbar, ToolbarButton } = createFactories(require("./reps/toolbar"));
+ const { div, pre } = dom;
+
+ /**
+ * This template represents the 'Raw Data' panel displaying
+ * JSON as a text received from the server.
+ */
+ let TextPanel = createClass({
+ displayName: "TextPanel",
+
+ propTypes: {
+ actions: PropTypes.object,
+ data: PropTypes.string
+ },
+
+ getInitialState: function () {
+ return {};
+ },
+
+ render: function () {
+ return (
+ div({className: "textPanelBox"},
+ TextToolbar({actions: this.props.actions}),
+ div({className: "panelContent"},
+ pre({className: "data"},
+ this.props.data
+ )
+ )
+ )
+ );
+ }
+ });
+
+ /**
+ * This object represents a toolbar displayed within the
+ * 'Raw Data' panel.
+ */
+ let TextToolbar = createFactory(createClass({
+ displayName: "TextToolbar",
+
+ propTypes: {
+ actions: PropTypes.object,
+ },
+
+ // Commands
+
+ onPrettify: function (event) {
+ this.props.actions.onPrettify();
+ },
+
+ onSave: function (event) {
+ this.props.actions.onSaveJson();
+ },
+
+ onCopy: function (event) {
+ this.props.actions.onCopyJson();
+ },
+
+ render: function () {
+ return (
+ Toolbar({},
+ ToolbarButton({
+ className: "btn save",
+ onClick: this.onSave},
+ Locale.$STR("jsonViewer.Save")
+ ),
+ ToolbarButton({
+ className: "btn copy",
+ onClick: this.onCopy},
+ Locale.$STR("jsonViewer.Copy")
+ ),
+ ToolbarButton({
+ className: "btn prettyprint",
+ onClick: this.onPrettify},
+ Locale.$STR("jsonViewer.PrettyPrint")
+ )
+ )
+ );
+ },
+ }));
+
+ // Exports from this module
+ exports.TextPanel = TextPanel;
+});
diff --git a/devtools/client/jsonview/converter-child.js b/devtools/client/jsonview/converter-child.js
new file mode 100644
index 000000000..61aa0c9a3
--- /dev/null
+++ b/devtools/client/jsonview/converter-child.js
@@ -0,0 +1,345 @@
+/* -*- 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 {Cc, Ci, components} = require("chrome");
+const Services = require("Services");
+const {Class} = require("sdk/core/heritage");
+const {Unknown} = require("sdk/platform/xpcom");
+const xpcom = require("sdk/platform/xpcom");
+const Events = require("sdk/dom/events");
+const Clipboard = require("sdk/clipboard");
+
+loader.lazyRequireGetter(this, "NetworkHelper",
+ "devtools/shared/webconsole/network-helper");
+loader.lazyRequireGetter(this, "JsonViewUtils",
+ "devtools/client/jsonview/utils");
+
+const childProcessMessageManager =
+ Cc["@mozilla.org/childprocessmessagemanager;1"]
+ .getService(Ci.nsISyncMessageSender);
+
+// Amount of space that will be allocated for the stream's backing-store.
+// Must be power of 2. Used to copy the data stream in onStopRequest.
+const SEGMENT_SIZE = Math.pow(2, 17);
+
+const JSON_VIEW_MIME_TYPE = "application/vnd.mozilla.json.view";
+const CONTRACT_ID = "@mozilla.org/streamconv;1?from=" +
+ JSON_VIEW_MIME_TYPE + "&to=*/*";
+const CLASS_ID = "{d8c9acee-dec5-11e4-8c75-1681e6b88ec1}";
+
+// Localization
+let jsonViewStrings = Services.strings.createBundle(
+ "chrome://devtools/locale/jsonview.properties");
+
+/**
+ * This object detects 'application/vnd.mozilla.json.view' content type
+ * and converts it into a JSON Viewer application that allows simple
+ * JSON inspection.
+ *
+ * Inspired by JSON View: https://github.com/bhollis/jsonview/
+ */
+let Converter = Class({
+ extends: Unknown,
+
+ interfaces: [
+ "nsIStreamConverter",
+ "nsIStreamListener",
+ "nsIRequestObserver"
+ ],
+
+ get wrappedJSObject() {
+ return this;
+ },
+
+ /**
+ * This component works as such:
+ * 1. asyncConvertData captures the listener
+ * 2. onStartRequest fires, initializes stuff, modifies the listener
+ * to match our output type
+ * 3. onDataAvailable transcodes the data into a UTF-8 string
+ * 4. onStopRequest gets the collected data and converts it,
+ * spits it to the listener
+ * 5. convert does nothing, it's just the synchronous version
+ * of asyncConvertData
+ */
+ convert: function (fromStream, fromType, toType, ctx) {
+ return fromStream;
+ },
+
+ asyncConvertData: function (fromType, toType, listener, ctx) {
+ this.listener = listener;
+ },
+
+ onDataAvailable: function (request, context, inputStream, offset, count) {
+ // From https://developer.mozilla.org/en/Reading_textual_data
+ let is = Cc["@mozilla.org/intl/converter-input-stream;1"]
+ .createInstance(Ci.nsIConverterInputStream);
+ is.init(inputStream, this.charset, -1,
+ Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
+
+ // Seed it with something positive
+ while (count) {
+ let str = {};
+ let bytesRead = is.readString(count, str);
+ if (!bytesRead) {
+ break;
+ }
+ count -= bytesRead;
+ this.data += str.value;
+ }
+ },
+
+ onStartRequest: function (request, context) {
+ this.data = "";
+ this.uri = request.QueryInterface(Ci.nsIChannel).URI.spec;
+
+ // Sets the charset if it is available. (For documents loaded from the
+ // filesystem, this is not set.)
+ this.charset =
+ request.QueryInterface(Ci.nsIChannel).contentCharset || "UTF-8";
+
+ this.channel = request;
+ this.channel.contentType = "text/html";
+ this.channel.contentCharset = "UTF-8";
+ // Because content might still have a reference to this window,
+ // force setting it to a null principal to avoid it being same-
+ // origin with (other) content.
+ this.channel.loadInfo.resetPrincipalsToNullPrincipal();
+
+ this.listener.onStartRequest(this.channel, context);
+ },
+
+ /**
+ * This should go something like this:
+ * 1. Make sure we have a unicode string.
+ * 2. Convert it to a Javascript object.
+ * 2.1 Removes the callback
+ * 3. Convert that to HTML? Or XUL?
+ * 4. Spit it back out at the listener
+ */
+ onStopRequest: function (request, context, statusCode) {
+ let headers = {
+ response: [],
+ request: []
+ };
+
+ let win = NetworkHelper.getWindowForRequest(request);
+
+ let Locale = {
+ $STR: key => {
+ try {
+ return jsonViewStrings.GetStringFromName(key);
+ } catch (err) {
+ console.error(err);
+ return undefined;
+ }
+ }
+ };
+
+ JsonViewUtils.exportIntoContentScope(win, Locale, "Locale");
+
+ Events.once(win, "DOMContentLoaded", event => {
+ win.addEventListener("contentMessage",
+ this.onContentMessage.bind(this), false, true);
+ });
+
+ // The request doesn't have to be always nsIHttpChannel
+ // (e.g. in case of data: URLs)
+ if (request instanceof Ci.nsIHttpChannel) {
+ request.visitResponseHeaders({
+ visitHeader: function (name, value) {
+ headers.response.push({name: name, value: value});
+ }
+ });
+
+ request.visitRequestHeaders({
+ visitHeader: function (name, value) {
+ headers.request.push({name: name, value: value});
+ }
+ });
+ }
+
+ let outputDoc = "";
+
+ try {
+ headers = JSON.stringify(headers);
+ outputDoc = this.toHTML(this.data, headers, this.uri);
+ } catch (e) {
+ console.error("JSON Viewer ERROR " + e);
+ outputDoc = this.toErrorPage(e, this.data, this.uri);
+ }
+
+ let storage = Cc["@mozilla.org/storagestream;1"]
+ .createInstance(Ci.nsIStorageStream);
+
+ storage.init(SEGMENT_SIZE, 0xffffffff, null);
+ let out = storage.getOutputStream(0);
+
+ let binout = Cc["@mozilla.org/binaryoutputstream;1"]
+ .createInstance(Ci.nsIBinaryOutputStream);
+
+ binout.setOutputStream(out);
+ binout.writeUtf8Z(outputDoc);
+ binout.close();
+
+ // We need to trim 4 bytes off the front (this could be underlying bug).
+ let trunc = 4;
+ let instream = storage.newInputStream(trunc);
+
+ // Pass the data to the main content listener
+ this.listener.onDataAvailable(this.channel, context, instream, 0,
+ instream.available());
+
+ this.listener.onStopRequest(this.channel, context, statusCode);
+
+ this.listener = null;
+ },
+
+ htmlEncode: function (t) {
+ return t !== null ? t.toString()
+ .replace(/&/g, "&amp;")
+ .replace(/"/g, "&quot;")
+ .replace(/</g, "&lt;")
+ .replace(/>/g, "&gt;") : "";
+ },
+
+ toHTML: function (json, headers, title) {
+ let themeClassName = "theme-" + JsonViewUtils.getCurrentTheme();
+ let clientBaseUrl = "resource://devtools/client/";
+ let baseUrl = clientBaseUrl + "jsonview/";
+ let themeVarsUrl = clientBaseUrl + "themes/variables.css";
+ let commonUrl = clientBaseUrl + "themes/common.css";
+ let toolbarsUrl = clientBaseUrl + "themes/toolbars.css";
+
+ let os;
+ let platform = Services.appinfo.OS;
+ if (platform.startsWith("WINNT")) {
+ os = "win";
+ } else if (platform.startsWith("Darwin")) {
+ os = "mac";
+ } else {
+ os = "linux";
+ }
+
+ return "<!DOCTYPE html>\n" +
+ "<html platform=\"" + os + "\" class=\"" + themeClassName + "\">" +
+ "<head><title>" + this.htmlEncode(title) + "</title>" +
+ "<base href=\"" + this.htmlEncode(baseUrl) + "\">" +
+ "<link rel=\"stylesheet\" type=\"text/css\" href=\"" +
+ themeVarsUrl + "\">" +
+ "<link rel=\"stylesheet\" type=\"text/css\" href=\"" +
+ commonUrl + "\">" +
+ "<link rel=\"stylesheet\" type=\"text/css\" href=\"" +
+ toolbarsUrl + "\">" +
+ "<link rel=\"stylesheet\" type=\"text/css\" href=\"css/main.css\">" +
+ "<script data-main=\"viewer-config\" src=\"lib/require.js\"></script>" +
+ "</head><body>" +
+ "<div id=\"content\"></div>" +
+ "<div id=\"json\">" + this.htmlEncode(json) + "</div>" +
+ "<div id=\"headers\">" + this.htmlEncode(headers) + "</div>" +
+ "</body></html>";
+ },
+
+ toErrorPage: function (error, data, uri) {
+ // Escape unicode nulls
+ data = data.replace("\u0000", "\uFFFD");
+
+ let errorInfo = error + "";
+
+ let output = "<div id=\"error\">" + "error parsing";
+ if (errorInfo.message) {
+ output += "<div class=\"errormessage\">" + errorInfo.message + "</div>";
+ }
+
+ output += "</div><div id=\"json\">" + this.highlightError(data,
+ errorInfo.line, errorInfo.column) + "</div>";
+
+ return "<!DOCTYPE html>\n" +
+ "<html><head><title>" + this.htmlEncode(uri + " - Error") + "</title>" +
+ "<base href=\"" + this.htmlEncode(this.data.url()) + "\">" +
+ "</head><body>" +
+ output +
+ "</body></html>";
+ },
+
+ // Chrome <-> Content communication
+
+ onContentMessage: function (e) {
+ // Do not handle events from different documents.
+ let win = NetworkHelper.getWindowForRequest(this.channel);
+ if (win != e.target) {
+ return;
+ }
+
+ let value = e.detail.value;
+ switch (e.detail.type) {
+ case "copy":
+ Clipboard.set(value, "text");
+ break;
+
+ case "copy-headers":
+ this.copyHeaders(value);
+ break;
+
+ case "save":
+ childProcessMessageManager.sendAsyncMessage(
+ "devtools:jsonview:save", value);
+ }
+ },
+
+ copyHeaders: function (headers) {
+ let value = "";
+ let eol = (Services.appinfo.OS !== "WINNT") ? "\n" : "\r\n";
+
+ let responseHeaders = headers.response;
+ for (let i = 0; i < responseHeaders.length; i++) {
+ let header = responseHeaders[i];
+ value += header.name + ": " + header.value + eol;
+ }
+
+ value += eol;
+
+ let requestHeaders = headers.request;
+ for (let i = 0; i < requestHeaders.length; i++) {
+ let header = requestHeaders[i];
+ value += header.name + ": " + header.value + eol;
+ }
+
+ Clipboard.set(value, "text");
+ }
+});
+
+// Stream converter component definition
+let service = xpcom.Service({
+ id: components.ID(CLASS_ID),
+ contract: CONTRACT_ID,
+ Component: Converter,
+ register: false,
+ unregister: false
+});
+
+function register() {
+ if (!xpcom.isRegistered(service)) {
+ xpcom.register(service);
+ return true;
+ }
+ return false;
+}
+
+function unregister() {
+ if (xpcom.isRegistered(service)) {
+ xpcom.unregister(service);
+ return true;
+ }
+ return false;
+}
+
+exports.JsonViewService = {
+ register: register,
+ unregister: unregister
+};
diff --git a/devtools/client/jsonview/converter-observer.js b/devtools/client/jsonview/converter-observer.js
new file mode 100644
index 000000000..9b149c565
--- /dev/null
+++ b/devtools/client/jsonview/converter-observer.js
@@ -0,0 +1,97 @@
+/* -*- 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 Cu = Components.utils;
+
+const {XPCOMUtils} = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {});
+const {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
+
+// Load devtools module lazily.
+XPCOMUtils.defineLazyGetter(this, "devtools", function () {
+ const {devtools} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+ return devtools;
+});
+
+// Load JsonView services lazily.
+XPCOMUtils.defineLazyGetter(this, "JsonViewService", function () {
+ const {JsonViewService} = devtools.require("devtools/client/jsonview/converter-child");
+ return JsonViewService;
+});
+
+XPCOMUtils.defineLazyGetter(this, "JsonViewSniffer", function () {
+ const {JsonViewSniffer} = devtools.require("devtools/client/jsonview/converter-sniffer");
+ return JsonViewSniffer;
+});
+
+// Constants
+const JSON_VIEW_PREF = "devtools.jsonview.enabled";
+
+/**
+ * Listen for 'devtools.jsonview.enabled' preference changes and
+ * register/unregister the JSON View XPCOM services as appropriate.
+ */
+function ConverterObserver() {
+}
+
+ConverterObserver.prototype = {
+ initialize: function () {
+ // Only the DevEdition has this feature available by default.
+ // Users need to manually flip 'devtools.jsonview.enabled' preference
+ // to have it available in other distributions.
+ if (this.isEnabled()) {
+ this.register();
+ }
+
+ Services.prefs.addObserver(JSON_VIEW_PREF, this, false);
+ Services.obs.addObserver(this, "xpcom-shutdown", false);
+ },
+
+ observe: function (subject, topic, data) {
+ switch (topic) {
+ case "xpcom-shutdown":
+ this.onShutdown();
+ break;
+ case "nsPref:changed":
+ this.onPrefChanged();
+ break;
+ }
+ },
+
+ onShutdown: function () {
+ Services.prefs.removeObserver(JSON_VIEW_PREF, observer);
+ Services.obs.removeObserver(observer, "xpcom-shutdown");
+ },
+
+ onPrefChanged: function () {
+ if (this.isEnabled()) {
+ this.register();
+ } else {
+ this.unregister();
+ }
+ },
+
+ register: function () {
+ JsonViewSniffer.register();
+ JsonViewService.register();
+ },
+
+ unregister: function () {
+ JsonViewSniffer.unregister();
+ JsonViewService.unregister();
+ },
+
+ isEnabled: function () {
+ return Services.prefs.getBoolPref(JSON_VIEW_PREF);
+ },
+};
+
+// Listen to JSON View 'enable' pref and perform dynamic
+// registration or unregistration of the main application
+// component.
+var observer = new ConverterObserver();
+observer.initialize();
diff --git a/devtools/client/jsonview/converter-sniffer.js b/devtools/client/jsonview/converter-sniffer.js
new file mode 100644
index 000000000..65e5d2aad
--- /dev/null
+++ b/devtools/client/jsonview/converter-sniffer.js
@@ -0,0 +1,106 @@
+/* -*- 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 {Cc, Ci, components} = require("chrome");
+const xpcom = require("sdk/platform/xpcom");
+const {Unknown} = require("sdk/platform/xpcom");
+const {Class} = require("sdk/core/heritage");
+
+const categoryManager = Cc["@mozilla.org/categorymanager;1"]
+ .getService(Ci.nsICategoryManager);
+
+loader.lazyRequireGetter(this, "NetworkHelper",
+ "devtools/shared/webconsole/network-helper");
+
+// Constants
+const JSON_TYPE = "application/json";
+const CONTRACT_ID = "@mozilla.org/devtools/jsonview-sniffer;1";
+const CLASS_ID = "{4148c488-dca1-49fc-a621-2a0097a62422}";
+const JSON_VIEW_MIME_TYPE = "application/vnd.mozilla.json.view";
+const JSON_VIEW_TYPE = "JSON View";
+const CONTENT_SNIFFER_CATEGORY = "net-content-sniffers";
+
+/**
+ * This component represents a sniffer (implements nsIContentSniffer
+ * interface) responsible for changing top level 'application/json'
+ * document types to: 'application/vnd.mozilla.json.view'.
+ *
+ * This internal type is consequently rendered by JSON View component
+ * that represents the JSON through a viewer interface.
+ */
+var Sniffer = Class({
+ extends: Unknown,
+
+ interfaces: [
+ "nsIContentSniffer",
+ ],
+
+ get wrappedJSObject() {
+ return this;
+ },
+
+ getMIMETypeFromContent: function (request, data, length) {
+ // JSON View is enabled only for top level loads only.
+ if (!NetworkHelper.isTopLevelLoad(request)) {
+ return "";
+ }
+
+ if (request instanceof Ci.nsIChannel) {
+ try {
+ if (request.contentDisposition ==
+ Ci.nsIChannel.DISPOSITION_ATTACHMENT) {
+ return "";
+ }
+ } catch (e) {
+ // Channel doesn't support content dispositions
+ }
+
+ // Check the response content type and if it's application/json
+ // change it to new internal type consumed by JSON View.
+ if (request.contentType == JSON_TYPE) {
+ return JSON_VIEW_MIME_TYPE;
+ }
+ }
+
+ return "";
+ }
+});
+
+var service = xpcom.Service({
+ id: components.ID(CLASS_ID),
+ contract: CONTRACT_ID,
+ Component: Sniffer,
+ register: false,
+ unregister: false
+});
+
+function register() {
+ if (!xpcom.isRegistered(service)) {
+ xpcom.register(service);
+ categoryManager.addCategoryEntry(CONTENT_SNIFFER_CATEGORY, JSON_VIEW_TYPE,
+ CONTRACT_ID, false, false);
+ return true;
+ }
+
+ return false;
+}
+
+function unregister() {
+ if (xpcom.isRegistered(service)) {
+ categoryManager.deleteCategoryEntry(CONTENT_SNIFFER_CATEGORY,
+ JSON_VIEW_TYPE, false);
+ xpcom.unregister(service);
+ return true;
+ }
+ return false;
+}
+
+exports.JsonViewSniffer = {
+ register: register,
+ unregister: unregister
+};
diff --git a/devtools/client/jsonview/css/general.css b/devtools/client/jsonview/css/general.css
new file mode 100644
index 000000000..0c68d65e7
--- /dev/null
+++ b/devtools/client/jsonview/css/general.css
@@ -0,0 +1,46 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+/******************************************************************************/
+/* General */
+
+body {
+ color: var(--theme-body-color);
+ background-color: var(--theme-body-background);
+ padding: 0;
+ margin: 0;
+ overflow-x: hidden;
+}
+
+*:focus {
+ outline: none !important;
+}
+
+#content {
+ height: 100%;
+}
+
+pre {
+ background-color: white;
+ border: none;
+ font-family: var(--monospace-font-family);
+}
+
+#json,
+#headers {
+ display: none;
+}
+
+/******************************************************************************/
+/* Dark Theme */
+
+body.theme-dark {
+ color: var(--theme-body-color);
+ background-color: var(--theme-body-background);
+}
+
+.theme-dark pre {
+ background-color: var(--theme-body-background);
+}
diff --git a/devtools/client/jsonview/css/headers-panel.css b/devtools/client/jsonview/css/headers-panel.css
new file mode 100644
index 000000000..89cec46e0
--- /dev/null
+++ b/devtools/client/jsonview/css/headers-panel.css
@@ -0,0 +1,78 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+/******************************************************************************/
+/* Headers Panel */
+
+.headersPanelBox {
+ height: 100%;
+}
+
+.headersPanelBox .netInfoHeadersTable {
+ overflow: auto;
+ height: 100%;
+}
+
+.headersPanelBox .netHeadersGroup {
+ padding: 10px;
+}
+
+.headersPanelBox td {
+ vertical-align: bottom;
+}
+
+.headersPanelBox .netInfoHeadersGroup {
+ color: var(--theme-body-color-alt);
+ margin-bottom: 10px;
+ border-bottom: 1px solid var(--theme-splitter-color);
+ padding-top: 8px;
+ padding-bottom: 4px;
+ font-weight: bold;
+ -moz-user-select: none;
+}
+
+.headersPanelBox .netInfoParamValue {
+ word-wrap: break-word;
+}
+
+.headersPanelBox .netInfoParamName {
+ padding: 2px 10px 0 0;
+ font-weight: bold;
+ vertical-align: top;
+ text-align: right;
+ white-space: nowrap;
+}
+
+/******************************************************************************/
+/* Theme colors have been generated/copied from Network Panel's header view */
+
+/* Light Theme */
+.theme-light .netInfoParamName {
+ color: var(--theme-highlight-red);
+}
+
+.theme-light .netInfoParamValue {
+ color: var(--theme-highlight-purple);
+}
+
+/* Dark Theme */
+.theme-dark .netInfoParamName {
+ color: var(--theme-highlight-purple);
+}
+
+.theme-dark .netInfoParamValue {
+ color: var(--theme-highlight-gray);
+}
+
+/* Firebug Theme */
+.theme-firebug .netInfoHeadersTable {
+ font-family: Lucida Grande, Tahoma, sans-serif;
+ font-size: 11px;
+ line-height: 12px;
+}
+
+.theme-firebug .netInfoParamValue {
+ font-family: var(--monospace-font-family);
+}
diff --git a/devtools/client/jsonview/css/json-panel.css b/devtools/client/jsonview/css/json-panel.css
new file mode 100644
index 000000000..b107d34a0
--- /dev/null
+++ b/devtools/client/jsonview/css/json-panel.css
@@ -0,0 +1,16 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+/******************************************************************************/
+/* JSON Panel */
+
+.jsonParseError {
+ font-size: 12px;
+ font-family: Lucida Grande, Tahoma, sans-serif;
+ line-height: 15px;
+ width: 100%;
+ padding: 10px;
+ color: red;
+}
diff --git a/devtools/client/jsonview/css/main.css b/devtools/client/jsonview/css/main.css
new file mode 100644
index 000000000..04f3cb87c
--- /dev/null
+++ b/devtools/client/jsonview/css/main.css
@@ -0,0 +1,59 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+@import "resource://devtools/client/shared/components/reps/reps.css";
+@import "resource://devtools/client/shared/components/tree/tree-view.css";
+@import "resource://devtools/client/shared/components/tabs/tabs.css";
+
+@import "general.css";
+@import "search-box.css";
+@import "toolbar.css";
+@import "json-panel.css";
+@import "text-panel.css";
+@import "headers-panel.css";
+
+/******************************************************************************/
+/* Panel Content */
+
+.panelContent {
+ overflow-y: auto;
+ width: 100%;
+}
+
+/* The tree takes the entire horizontal space within the panel content. */
+.panelContent .treeTable {
+ width: 100%;
+ font-family: var(--monospace-font-family);
+}
+
+:root[platform="linux"] .treeTable {
+ font-size: 80%; /* To handle big monospace font */
+}
+
+/* Make sure there is a little space between label and value columns. */
+.panelContent .treeTable .treeLabelCell {
+ padding-right: 17px;
+}
+
+/******************************************************************************/
+/* Theme Firebug */
+
+.theme-firebug .panelContent {
+ height: calc(100% - 30px);
+}
+
+/* JSON View is using bigger font-size for the main tabs so,
+ let's overwrite the default value. */
+.theme-firebug .tabs .tabs-navigation {
+ font-size: 14px;
+}
+
+/******************************************************************************/
+/* Theme Light & Theme Dark*/
+
+.theme-dark .panelContent,
+.theme-light .panelContent {
+ height: calc(100% - 27px);
+}
diff --git a/devtools/client/jsonview/css/moz.build b/devtools/client/jsonview/css/moz.build
new file mode 100644
index 000000000..e881b3469
--- /dev/null
+++ b/devtools/client/jsonview/css/moz.build
@@ -0,0 +1,16 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'general.css',
+ 'headers-panel.css',
+ 'json-panel.css',
+ 'main.css',
+ 'search-box.css',
+ 'search.svg',
+ 'text-panel.css',
+ 'toolbar.css'
+)
diff --git a/devtools/client/jsonview/css/search-box.css b/devtools/client/jsonview/css/search-box.css
new file mode 100644
index 000000000..99615b648
--- /dev/null
+++ b/devtools/client/jsonview/css/search-box.css
@@ -0,0 +1,24 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+/******************************************************************************/
+/* Search Box */
+
+.searchBox {
+ height: 18px;
+ font: message-box;
+ background-color: var(--theme-body-background);
+ background-image: url("chrome://devtools/skin/images/filter.svg#filterinput");
+ background-repeat: no-repeat;
+ background-position: 2px center;
+ border: 1px solid var(--theme-splitter-color);
+ border-radius: 2px;
+ color: var(--theme-content-color1);
+ width: 200px;
+ margin-top: 0;
+ margin-right: 1px;
+ float: right;
+ padding-left: 20px;
+}
diff --git a/devtools/client/jsonview/css/search.svg b/devtools/client/jsonview/css/search.svg
new file mode 100644
index 000000000..53f2d3651
--- /dev/null
+++ b/devtools/client/jsonview/css/search.svg
@@ -0,0 +1,22 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <defs>
+ <linearGradient id="a">
+ <stop offset="0" stop-color="#427dc2"/>
+ <stop offset="1" stop-color="#5e9fce"/>
+ </linearGradient>
+ <linearGradient id="b">
+ <stop offset="0" stop-color="#2f5d93"/>
+ <stop offset="1" stop-color="#3a87bd"/>
+ </linearGradient>
+ <filter id="c" width="1.239" height="1.241" x="-.12" y="-.12" color-interpolation-filters="sRGB">
+ <feGaussianBlur stdDeviation=".637"/>
+ </filter>
+ <linearGradient id="d" x1="4.094" x2="4.094" y1="13.423" y2="2.743" xlink:href="#a" gradientUnits="userSpaceOnUse"/>
+ <linearGradient id="e" x1="8.711" x2="8.711" y1="13.58" y2="2.566" xlink:href="#b" gradientUnits="userSpaceOnUse"/>
+ </defs>
+ <path fill="#fff" stroke="#fff" stroke-width="1.5" d="M10.14 1.656c-2.35 0-4.25 1.9-4.25 4.25 0 .752.19 1.45.532 2.063L1.61 12.78l1.562 1.564 4.78-4.78c.64.384 1.387.592 2.19.592 2.35 0 4.25-1.9 4.25-4.25s-1.9-4.25-4.25-4.25zm0 1.532c1.504 0 2.72 1.214 2.72 2.718s-1.216 2.72-2.72 2.72c-1.503 0-2.718-1.216-2.718-2.72 0-1.504 1.215-2.718 2.72-2.718z" stroke-linejoin="round" filter="url(#c)"/>
+ <path fill="url(#d)" stroke="url(#e)" stroke-width=".6" d="M10 2C7.79 2 6 3.79 6 6c0 .828.256 1.612.688 2.25l-4.875 4.875 1.062 1.063L7.75 9.31C8.388 9.745 9.172 10 10 10c2.21 0 4-1.79 4-4s-1.79-4-4-4zm0 1c1.657 0 3 1.343 3 3s-1.343 3-3 3-3-1.343-3-3 1.343-3 3-3z" stroke-linejoin="round"/>
+</svg>
diff --git a/devtools/client/jsonview/css/text-panel.css b/devtools/client/jsonview/css/text-panel.css
new file mode 100644
index 000000000..99b238556
--- /dev/null
+++ b/devtools/client/jsonview/css/text-panel.css
@@ -0,0 +1,26 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+/******************************************************************************/
+/* Text Panel */
+
+.textPanelBox {
+ height: 100%;
+}
+
+.textPanelBox .data {
+ overflow: auto;
+ height: 100%;
+}
+
+.textPanelBox pre {
+ margin: 0;
+ font-family: var(--monospace-font-family);
+ color: var(--theme-content-color1);
+}
+
+:root[platform="linux"] .textPanelBox .data {
+ font-size: 80%; /* To handle big monospace font */
+}
diff --git a/devtools/client/jsonview/css/toolbar.css b/devtools/client/jsonview/css/toolbar.css
new file mode 100644
index 000000000..833b2119f
--- /dev/null
+++ b/devtools/client/jsonview/css/toolbar.css
@@ -0,0 +1,92 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+/******************************************************************************/
+/* Toolbar */
+
+.toolbar {
+ line-height: 20px;
+ height: 22px;
+ font: message-box;
+ padding: 4px 0 3px 0;
+}
+
+.toolbar .btn {
+ margin-left: 5px;
+ background-color: #E6E6E6;
+ border: 1px solid rgb(204, 204, 204);
+ text-decoration: none;
+ display: inline-block;
+ text-align: center;
+ white-space: nowrap;
+ vertical-align: middle;
+ cursor: pointer;
+ -moz-user-select: none;
+ padding: 0 2px;
+ border-radius: 2px;
+}
+
+.toolbar .btn::-moz-focus-inner {
+ border: 1px solid transparent;
+}
+
+/******************************************************************************/
+/* Firebug Theme */
+
+.theme-firebug .toolbar {
+ border-bottom: 1px solid rgb(170, 188, 207);
+ background-color: var(--theme-tab-toolbar-background) !important;
+ background-image: linear-gradient(rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0.2));
+}
+
+.theme-firebug .toolbar .btn {
+ border-radius: 2px;
+ color: #141414;
+ background-color: white;
+}
+
+.theme-firebug .toolbar .btn:hover {
+ color: #333;
+ background-color: #e6e6e6;
+ border-color: #adadad;
+}
+
+.theme-firebug .toolbar .btn:active {
+ background-image: none;
+ outline: 0;
+ box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
+}
+
+/******************************************************************************/
+/* Light Theme & Dark Theme*/
+
+.theme-dark .toolbar,
+.theme-light .toolbar {
+ background-color: var(--theme-toolbar-background);
+ border-bottom: 1px solid var(--theme-splitter-color);
+ padding: 1px;
+ padding-left: 2px;
+}
+
+.theme-dark .toolbar .btn,
+.theme-light .toolbar .btn {
+ min-height: 18px;
+ color: var(--theme-content-color1);
+ text-shadow: none;
+ margin: 1px 2px 1px 2px;
+ border: none;
+ background-color: rgba(170, 170, 170, .2); /* --toolbar-tab-hover */
+ transition: background 0.05s ease-in-out;
+}
+
+.theme-dark .toolbar .btn:hover,
+.theme-light .toolbar .btn:hover {
+ background: rgba(170, 170, 170, .3); /* Splitters */
+}
+
+.theme-dark .toolbar .btn:not([disabled]):hover:active,
+.theme-light .toolbar .btn:not([disabled]):hover:active {
+ background: rgba(170, 170, 170, .4); /* --toolbar-tab-hover-active */
+}
diff --git a/devtools/client/jsonview/json-viewer.js b/devtools/client/jsonview/json-viewer.js
new file mode 100644
index 000000000..d96081da2
--- /dev/null
+++ b/devtools/client/jsonview/json-viewer.js
@@ -0,0 +1,112 @@
+/* -*- 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";
+
+define(function (require, exports, module) {
+ const { render } = require("devtools/client/shared/vendor/react-dom");
+ const { createFactories } = require("devtools/client/shared/components/reps/rep-utils");
+ const { MainTabbedArea } = createFactories(require("./components/main-tabbed-area"));
+
+ const json = document.getElementById("json");
+ const headers = document.getElementById("headers");
+
+ let jsonData;
+
+ try {
+ jsonData = JSON.parse(json.textContent);
+ } catch (err) {
+ jsonData = err + "";
+ }
+
+ // Application state object.
+ let input = {
+ jsonText: json.textContent,
+ jsonPretty: null,
+ json: jsonData,
+ headers: JSON.parse(headers.textContent),
+ tabActive: 0,
+ prettified: false
+ };
+
+ json.remove();
+ headers.remove();
+
+ /**
+ * Application actions/commands. This list implements all commands
+ * available for the JSON viewer.
+ */
+ input.actions = {
+ onCopyJson: function () {
+ dispatchEvent("copy", input.prettified ? input.jsonPretty : input.jsonText);
+ },
+
+ onSaveJson: function () {
+ dispatchEvent("save", input.prettified ? input.jsonPretty : input.jsonText);
+ },
+
+ onCopyHeaders: function () {
+ dispatchEvent("copy-headers", input.headers);
+ },
+
+ onSearch: function (value) {
+ theApp.setState({searchFilter: value});
+ },
+
+ onPrettify: function (data) {
+ if (input.prettified) {
+ theApp.setState({jsonText: input.jsonText});
+ } else {
+ if (!input.jsonPretty) {
+ input.jsonPretty = JSON.stringify(jsonData, null, " ");
+ }
+ theApp.setState({jsonText: input.jsonPretty});
+ }
+
+ input.prettified = !input.prettified;
+ },
+ };
+
+ /**
+ * Helper for dispatching an event. It's handled in chrome scope.
+ *
+ * @param {String} type Event detail type
+ * @param {Object} value Event detail value
+ */
+ function dispatchEvent(type, value) {
+ let data = {
+ detail: {
+ type,
+ value,
+ }
+ };
+
+ let contentMessageEvent = new CustomEvent("contentMessage", data);
+ window.dispatchEvent(contentMessageEvent);
+ }
+
+ /**
+ * Render the main application component. It's the main tab bar displayed
+ * at the top of the window. This component also represents ReacJS root.
+ */
+ let content = document.getElementById("content");
+ let theApp = render(MainTabbedArea(input), content);
+
+ let onResize = event => {
+ window.document.body.style.height = window.innerHeight + "px";
+ window.document.body.style.width = window.innerWidth + "px";
+ };
+
+ window.addEventListener("resize", onResize);
+ onResize();
+
+ // Send notification event to the window. Can be useful for
+ // tests as well as extensions.
+ let event = new CustomEvent("JSONViewInitialized", {});
+ window.jsonViewInitialized = true;
+ window.dispatchEvent(event);
+});
+
diff --git a/devtools/client/jsonview/lib/moz.build b/devtools/client/jsonview/lib/moz.build
new file mode 100644
index 000000000..fff9a99f9
--- /dev/null
+++ b/devtools/client/jsonview/lib/moz.build
@@ -0,0 +1,9 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'require.js'
+)
diff --git a/devtools/client/jsonview/lib/require.js b/devtools/client/jsonview/lib/require.js
new file mode 100644
index 000000000..77a5bb1d3
--- /dev/null
+++ b/devtools/client/jsonview/lib/require.js
@@ -0,0 +1,2076 @@
+/** vim: et:ts=4:sw=4:sts=4
+ * @license RequireJS 2.1.15 Copyright (c) 2010-2014, The Dojo Foundation All Rights Reserved.
+ * Available via the MIT or new BSD license.
+ * see: http://github.com/jrburke/requirejs for details
+ */
+//Not using strict: uneven strict support in browsers, #392, and causes
+//problems with requirejs.exec()/transpiler plugins that may not be strict.
+/*jslint regexp: true, nomen: true, sloppy: true */
+/*global window, navigator, document, importScripts, setTimeout, opera */
+
+var requirejs, require, define;
+(function (global) {
+ var req, s, head, baseElement, dataMain, src,
+ interactiveScript, currentlyAddingScript, mainScript, subPath,
+ version = '2.1.15',
+ commentRegExp = /(\/\*([\s\S]*?)\*\/|([^:]|^)\/\/(.*)$)/mg,
+ cjsRequireRegExp = /[^.]\s*require\s*\(\s*["']([^'"\s]+)["']\s*\)/g,
+ jsSuffixRegExp = /\.js$/,
+ currDirRegExp = /^\.\//,
+ op = Object.prototype,
+ ostring = op.toString,
+ hasOwn = op.hasOwnProperty,
+ ap = Array.prototype,
+ apsp = ap.splice,
+ isBrowser = !!(typeof window !== 'undefined' && typeof navigator !== 'undefined' && window.document),
+ isWebWorker = !isBrowser && typeof importScripts !== 'undefined',
+ //PS3 indicates loaded and complete, but need to wait for complete
+ //specifically. Sequence is 'loading', 'loaded', execution,
+ // then 'complete'. The UA check is unfortunate, but not sure how
+ //to feature test w/o causing perf issues.
+ readyRegExp = isBrowser && navigator.platform === 'PLAYSTATION 3' ?
+ /^complete$/ : /^(complete|loaded)$/,
+ defContextName = '_',
+ //Oh the tragedy, detecting opera. See the usage of isOpera for reason.
+ isOpera = typeof opera !== 'undefined' && opera.toString() === '[object Opera]',
+ contexts = {},
+ cfg = {},
+ globalDefQueue = [],
+ useInteractive = false;
+
+ function isFunction(it) {
+ return ostring.call(it) === '[object Function]';
+ }
+
+ function isArray(it) {
+ return ostring.call(it) === '[object Array]';
+ }
+
+ /**
+ * Helper function for iterating over an array. If the func returns
+ * a true value, it will break out of the loop.
+ */
+ function each(ary, func) {
+ if (ary) {
+ var i;
+ for (i = 0; i < ary.length; i += 1) {
+ if (ary[i] && func(ary[i], i, ary)) {
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * Helper function for iterating over an array backwards. If the func
+ * returns a true value, it will break out of the loop.
+ */
+ function eachReverse(ary, func) {
+ if (ary) {
+ var i;
+ for (i = ary.length - 1; i > -1; i -= 1) {
+ if (ary[i] && func(ary[i], i, ary)) {
+ break;
+ }
+ }
+ }
+ }
+
+ function hasProp(obj, prop) {
+ return hasOwn.call(obj, prop);
+ }
+
+ function getOwn(obj, prop) {
+ return hasProp(obj, prop) && obj[prop];
+ }
+
+ /**
+ * Cycles over properties in an object and calls a function for each
+ * property value. If the function returns a truthy value, then the
+ * iteration is stopped.
+ */
+ function eachProp(obj, func) {
+ var prop;
+ for (prop in obj) {
+ if (hasProp(obj, prop)) {
+ if (func(obj[prop], prop)) {
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * Simple function to mix in properties from source into target,
+ * but only if target does not already have a property of the same name.
+ */
+ function mixin(target, source, force, deepStringMixin) {
+ if (source) {
+ eachProp(source, function (value, prop) {
+ if (force || !hasProp(target, prop)) {
+ if (deepStringMixin && typeof value === 'object' && value &&
+ !isArray(value) && !isFunction(value) &&
+ !(value instanceof RegExp)) {
+
+ if (!target[prop]) {
+ target[prop] = {};
+ }
+ mixin(target[prop], value, force, deepStringMixin);
+ } else {
+ target[prop] = value;
+ }
+ }
+ });
+ }
+ return target;
+ }
+
+ //Similar to Function.prototype.bind, but the 'this' object is specified
+ //first, since it is easier to read/figure out what 'this' will be.
+ function bind(obj, fn) {
+ return function () {
+ return fn.apply(obj, arguments);
+ };
+ }
+
+ function scripts() {
+ return document.getElementsByTagName('script');
+ }
+
+ function defaultOnError(err) {
+ throw err;
+ }
+
+ //Allow getting a global that is expressed in
+ //dot notation, like 'a.b.c'.
+ function getGlobal(value) {
+ if (!value) {
+ return value;
+ }
+ var g = global;
+ each(value.split('.'), function (part) {
+ g = g[part];
+ });
+ return g;
+ }
+
+ /**
+ * Constructs an error with a pointer to an URL with more information.
+ * @param {String} id the error ID that maps to an ID on a web page.
+ * @param {String} message human readable error.
+ * @param {Error} [err] the original error, if there is one.
+ *
+ * @returns {Error}
+ */
+ function makeError(id, msg, err, requireModules) {
+ var e = new Error(msg + '\nhttp://requirejs.org/docs/errors.html#' + id);
+ e.requireType = id;
+ e.requireModules = requireModules;
+ if (err) {
+ e.originalError = err;
+ }
+ return e;
+ }
+
+ if (typeof define !== 'undefined') {
+ //If a define is already in play via another AMD loader,
+ //do not overwrite.
+ return;
+ }
+
+ if (typeof requirejs !== 'undefined') {
+ if (isFunction(requirejs)) {
+ //Do not overwrite an existing requirejs instance.
+ return;
+ }
+ cfg = requirejs;
+ requirejs = undefined;
+ }
+
+ //Allow for a require config object
+ if (typeof require !== 'undefined' && !isFunction(require)) {
+ //assume it is a config object.
+ cfg = require;
+ require = undefined;
+ }
+
+ function newContext(contextName) {
+ var inCheckLoaded, Module, context, handlers,
+ checkLoadedTimeoutId,
+ config = {
+ //Defaults. Do not set a default for map
+ //config to speed up normalize(), which
+ //will run faster if there is no default.
+ waitSeconds: 7,
+ baseUrl: './',
+ paths: {},
+ bundles: {},
+ pkgs: {},
+ shim: {},
+ config: {}
+ },
+ registry = {},
+ //registry of just enabled modules, to speed
+ //cycle breaking code when lots of modules
+ //are registered, but not activated.
+ enabledRegistry = {},
+ undefEvents = {},
+ defQueue = [],
+ defined = {},
+ urlFetched = {},
+ bundlesMap = {},
+ requireCounter = 1,
+ unnormalizedCounter = 1;
+
+ /**
+ * Trims the . and .. from an array of path segments.
+ * It will keep a leading path segment if a .. will become
+ * the first path segment, to help with module name lookups,
+ * which act like paths, but can be remapped. But the end result,
+ * all paths that use this function should look normalized.
+ * NOTE: this method MODIFIES the input array.
+ * @param {Array} ary the array of path segments.
+ */
+ function trimDots(ary) {
+ var i, part;
+ for (i = 0; i < ary.length; i++) {
+ part = ary[i];
+ if (part === '.') {
+ ary.splice(i, 1);
+ i -= 1;
+ } else if (part === '..') {
+ // If at the start, or previous value is still ..,
+ // keep them so that when converted to a path it may
+ // still work when converted to a path, even though
+ // as an ID it is less than ideal. In larger point
+ // releases, may be better to just kick out an error.
+ if (i === 0 || (i == 1 && ary[2] === '..') || ary[i - 1] === '..') {
+ continue;
+ } else if (i > 0) {
+ ary.splice(i - 1, 2);
+ i -= 2;
+ }
+ }
+ }
+ }
+
+ /**
+ * Given a relative module name, like ./something, normalize it to
+ * a real name that can be mapped to a path.
+ * @param {String} name the relative name
+ * @param {String} baseName a real name that the name arg is relative
+ * to.
+ * @param {Boolean} applyMap apply the map config to the value. Should
+ * only be done if this normalization is for a dependency ID.
+ * @returns {String} normalized name
+ */
+ function normalize(name, baseName, applyMap) {
+ var pkgMain, mapValue, nameParts, i, j, nameSegment, lastIndex,
+ foundMap, foundI, foundStarMap, starI, normalizedBaseParts,
+ baseParts = (baseName && baseName.split('/')),
+ map = config.map,
+ starMap = map && map['*'];
+
+ //Adjust any relative paths.
+ if (name) {
+ name = name.split('/');
+ lastIndex = name.length - 1;
+
+ // If wanting node ID compatibility, strip .js from end
+ // of IDs. Have to do this here, and not in nameToUrl
+ // because node allows either .js or non .js to map
+ // to same file.
+ if (config.nodeIdCompat && jsSuffixRegExp.test(name[lastIndex])) {
+ name[lastIndex] = name[lastIndex].replace(jsSuffixRegExp, '');
+ }
+
+ // Starts with a '.' so need the baseName
+ if (name[0].charAt(0) === '.' && baseParts) {
+ //Convert baseName to array, and lop off the last part,
+ //so that . matches that 'directory' and not name of the baseName's
+ //module. For instance, baseName of 'one/two/three', maps to
+ //'one/two/three.js', but we want the directory, 'one/two' for
+ //this normalization.
+ normalizedBaseParts = baseParts.slice(0, baseParts.length - 1);
+ name = normalizedBaseParts.concat(name);
+ }
+
+ trimDots(name);
+ name = name.join('/');
+ }
+
+ //Apply map config if available.
+ if (applyMap && map && (baseParts || starMap)) {
+ nameParts = name.split('/');
+
+ outerLoop: for (i = nameParts.length; i > 0; i -= 1) {
+ nameSegment = nameParts.slice(0, i).join('/');
+
+ if (baseParts) {
+ //Find the longest baseName segment match in the config.
+ //So, do joins on the biggest to smallest lengths of baseParts.
+ for (j = baseParts.length; j > 0; j -= 1) {
+ mapValue = getOwn(map, baseParts.slice(0, j).join('/'));
+
+ //baseName segment has config, find if it has one for
+ //this name.
+ if (mapValue) {
+ mapValue = getOwn(mapValue, nameSegment);
+ if (mapValue) {
+ //Match, update name to the new value.
+ foundMap = mapValue;
+ foundI = i;
+ break outerLoop;
+ }
+ }
+ }
+ }
+
+ //Check for a star map match, but just hold on to it,
+ //if there is a shorter segment match later in a matching
+ //config, then favor over this star map.
+ if (!foundStarMap && starMap && getOwn(starMap, nameSegment)) {
+ foundStarMap = getOwn(starMap, nameSegment);
+ starI = i;
+ }
+ }
+
+ if (!foundMap && foundStarMap) {
+ foundMap = foundStarMap;
+ foundI = starI;
+ }
+
+ if (foundMap) {
+ nameParts.splice(0, foundI, foundMap);
+ name = nameParts.join('/');
+ }
+ }
+
+ // If the name points to a package's name, use
+ // the package main instead.
+ pkgMain = getOwn(config.pkgs, name);
+
+ return pkgMain ? pkgMain : name;
+ }
+
+ function removeScript(name) {
+ if (isBrowser) {
+ each(scripts(), function (scriptNode) {
+ if (scriptNode.getAttribute('data-requiremodule') === name &&
+ scriptNode.getAttribute('data-requirecontext') === context.contextName) {
+ scriptNode.parentNode.removeChild(scriptNode);
+ return true;
+ }
+ });
+ }
+ }
+
+ function hasPathFallback(id) {
+ var pathConfig = getOwn(config.paths, id);
+ if (pathConfig && isArray(pathConfig) && pathConfig.length > 1) {
+ //Pop off the first array value, since it failed, and
+ //retry
+ pathConfig.shift();
+ context.require.undef(id);
+
+ //Custom require that does not do map translation, since
+ //ID is "absolute", already mapped/resolved.
+ context.makeRequire(null, {
+ skipMap: true
+ })([id]);
+
+ return true;
+ }
+ }
+
+ //Turns a plugin!resource to [plugin, resource]
+ //with the plugin being undefined if the name
+ //did not have a plugin prefix.
+ function splitPrefix(name) {
+ var prefix,
+ index = name ? name.indexOf('!') : -1;
+ if (index > -1) {
+ prefix = name.substring(0, index);
+ name = name.substring(index + 1, name.length);
+ }
+ return [prefix, name];
+ }
+
+ /**
+ * Creates a module mapping that includes plugin prefix, module
+ * name, and path. If parentModuleMap is provided it will
+ * also normalize the name via require.normalize()
+ *
+ * @param {String} name the module name
+ * @param {String} [parentModuleMap] parent module map
+ * for the module name, used to resolve relative names.
+ * @param {Boolean} isNormalized: is the ID already normalized.
+ * This is true if this call is done for a define() module ID.
+ * @param {Boolean} applyMap: apply the map config to the ID.
+ * Should only be true if this map is for a dependency.
+ *
+ * @returns {Object}
+ */
+ function makeModuleMap(name, parentModuleMap, isNormalized, applyMap) {
+ var url, pluginModule, suffix, nameParts,
+ prefix = null,
+ parentName = parentModuleMap ? parentModuleMap.name : null,
+ originalName = name,
+ isDefine = true,
+ normalizedName = '';
+
+ //If no name, then it means it is a require call, generate an
+ //internal name.
+ if (!name) {
+ isDefine = false;
+ name = '_@r' + (requireCounter += 1);
+ }
+
+ nameParts = splitPrefix(name);
+ prefix = nameParts[0];
+ name = nameParts[1];
+
+ if (prefix) {
+ prefix = normalize(prefix, parentName, applyMap);
+ pluginModule = getOwn(defined, prefix);
+ }
+
+ //Account for relative paths if there is a base name.
+ if (name) {
+ if (prefix) {
+ if (pluginModule && pluginModule.normalize) {
+ //Plugin is loaded, use its normalize method.
+ normalizedName = pluginModule.normalize(name, function (name) {
+ return normalize(name, parentName, applyMap);
+ });
+ } else {
+ // If nested plugin references, then do not try to
+ // normalize, as it will not normalize correctly. This
+ // places a restriction on resourceIds, and the longer
+ // term solution is not to normalize until plugins are
+ // loaded and all normalizations to allow for async
+ // loading of a loader plugin. But for now, fixes the
+ // common uses. Details in #1131
+ normalizedName = name.indexOf('!') === -1 ?
+ normalize(name, parentName, applyMap) :
+ name;
+ }
+ } else {
+ //A regular module.
+ normalizedName = normalize(name, parentName, applyMap);
+
+ //Normalized name may be a plugin ID due to map config
+ //application in normalize. The map config values must
+ //already be normalized, so do not need to redo that part.
+ nameParts = splitPrefix(normalizedName);
+ prefix = nameParts[0];
+ normalizedName = nameParts[1];
+ isNormalized = true;
+
+ url = context.nameToUrl(normalizedName);
+ }
+ }
+
+ //If the id is a plugin id that cannot be determined if it needs
+ //normalization, stamp it with a unique ID so two matching relative
+ //ids that may conflict can be separate.
+ suffix = prefix && !pluginModule && !isNormalized ?
+ '_unnormalized' + (unnormalizedCounter += 1) :
+ '';
+
+ return {
+ prefix: prefix,
+ name: normalizedName,
+ parentMap: parentModuleMap,
+ unnormalized: !!suffix,
+ url: url,
+ originalName: originalName,
+ isDefine: isDefine,
+ id: (prefix ?
+ prefix + '!' + normalizedName :
+ normalizedName) + suffix
+ };
+ }
+
+ function getModule(depMap) {
+ var id = depMap.id,
+ mod = getOwn(registry, id);
+
+ if (!mod) {
+ mod = registry[id] = new context.Module(depMap);
+ }
+
+ return mod;
+ }
+
+ function on(depMap, name, fn) {
+ var id = depMap.id,
+ mod = getOwn(registry, id);
+
+ if (hasProp(defined, id) &&
+ (!mod || mod.defineEmitComplete)) {
+ if (name === 'defined') {
+ fn(defined[id]);
+ }
+ } else {
+ mod = getModule(depMap);
+ if (mod.error && name === 'error') {
+ fn(mod.error);
+ } else {
+ mod.on(name, fn);
+ }
+ }
+ }
+
+ function onError(err, errback) {
+ var ids = err.requireModules,
+ notified = false;
+
+ if (errback) {
+ errback(err);
+ } else {
+ each(ids, function (id) {
+ var mod = getOwn(registry, id);
+ if (mod) {
+ //Set error on module, so it skips timeout checks.
+ mod.error = err;
+ if (mod.events.error) {
+ notified = true;
+ mod.emit('error', err);
+ }
+ }
+ });
+
+ if (!notified) {
+ req.onError(err);
+ }
+ }
+ }
+
+ /**
+ * Internal method to transfer globalQueue items to this context's
+ * defQueue.
+ */
+ function takeGlobalQueue() {
+ //Push all the globalDefQueue items into the context's defQueue
+ if (globalDefQueue.length) {
+ //Array splice in the values since the context code has a
+ //local var ref to defQueue, so cannot just reassign the one
+ //on context.
+ apsp.apply(defQueue,
+ [defQueue.length, 0].concat(globalDefQueue));
+ globalDefQueue = [];
+ }
+ }
+
+ handlers = {
+ 'require': function (mod) {
+ if (mod.require) {
+ return mod.require;
+ } else {
+ return (mod.require = context.makeRequire(mod.map));
+ }
+ },
+ 'exports': function (mod) {
+ mod.usingExports = true;
+ if (mod.map.isDefine) {
+ if (mod.exports) {
+ return (defined[mod.map.id] = mod.exports);
+ } else {
+ return (mod.exports = defined[mod.map.id] = {});
+ }
+ }
+ },
+ 'module': function (mod) {
+ if (mod.module) {
+ return mod.module;
+ } else {
+ return (mod.module = {
+ id: mod.map.id,
+ uri: mod.map.url,
+ config: function () {
+ return getOwn(config.config, mod.map.id) || {};
+ },
+ exports: mod.exports || (mod.exports = {})
+ });
+ }
+ }
+ };
+
+ function cleanRegistry(id) {
+ //Clean up machinery used for waiting modules.
+ delete registry[id];
+ delete enabledRegistry[id];
+ }
+
+ function breakCycle(mod, traced, processed) {
+ var id = mod.map.id;
+
+ if (mod.error) {
+ mod.emit('error', mod.error);
+ } else {
+ traced[id] = true;
+ each(mod.depMaps, function (depMap, i) {
+ var depId = depMap.id,
+ dep = getOwn(registry, depId);
+
+ //Only force things that have not completed
+ //being defined, so still in the registry,
+ //and only if it has not been matched up
+ //in the module already.
+ if (dep && !mod.depMatched[i] && !processed[depId]) {
+ if (getOwn(traced, depId)) {
+ mod.defineDep(i, defined[depId]);
+ mod.check(); //pass false?
+ } else {
+ breakCycle(dep, traced, processed);
+ }
+ }
+ });
+ processed[id] = true;
+ }
+ }
+
+ function checkLoaded() {
+ var err, usingPathFallback,
+ waitInterval = config.waitSeconds * 1000,
+ //It is possible to disable the wait interval by using waitSeconds of 0.
+ expired = waitInterval && (context.startTime + waitInterval) < new Date().getTime(),
+ noLoads = [],
+ reqCalls = [],
+ stillLoading = false,
+ needCycleCheck = true;
+
+ //Do not bother if this call was a result of a cycle break.
+ if (inCheckLoaded) {
+ return;
+ }
+
+ inCheckLoaded = true;
+
+ //Figure out the state of all the modules.
+ eachProp(enabledRegistry, function (mod) {
+ var map = mod.map,
+ modId = map.id;
+
+ //Skip things that are not enabled or in error state.
+ if (!mod.enabled) {
+ return;
+ }
+
+ if (!map.isDefine) {
+ reqCalls.push(mod);
+ }
+
+ if (!mod.error) {
+ //If the module should be executed, and it has not
+ //been inited and time is up, remember it.
+ if (!mod.inited && expired) {
+ if (hasPathFallback(modId)) {
+ usingPathFallback = true;
+ stillLoading = true;
+ } else {
+ noLoads.push(modId);
+ removeScript(modId);
+ }
+ } else if (!mod.inited && mod.fetched && map.isDefine) {
+ stillLoading = true;
+ if (!map.prefix) {
+ //No reason to keep looking for unfinished
+ //loading. If the only stillLoading is a
+ //plugin resource though, keep going,
+ //because it may be that a plugin resource
+ //is waiting on a non-plugin cycle.
+ return (needCycleCheck = false);
+ }
+ }
+ }
+ });
+
+ if (expired && noLoads.length) {
+ //If wait time expired, throw error of unloaded modules.
+ err = makeError('timeout', 'Load timeout for modules: ' + noLoads, null, noLoads);
+ err.contextName = context.contextName;
+ return onError(err);
+ }
+
+ //Not expired, check for a cycle.
+ if (needCycleCheck) {
+ each(reqCalls, function (mod) {
+ breakCycle(mod, {}, {});
+ });
+ }
+
+ //If still waiting on loads, and the waiting load is something
+ //other than a plugin resource, or there are still outstanding
+ //scripts, then just try back later.
+ if ((!expired || usingPathFallback) && stillLoading) {
+ //Something is still waiting to load. Wait for it, but only
+ //if a timeout is not already in effect.
+ if ((isBrowser || isWebWorker) && !checkLoadedTimeoutId) {
+ checkLoadedTimeoutId = setTimeout(function () {
+ checkLoadedTimeoutId = 0;
+ checkLoaded();
+ }, 50);
+ }
+ }
+
+ inCheckLoaded = false;
+ }
+
+ Module = function (map) {
+ this.events = getOwn(undefEvents, map.id) || {};
+ this.map = map;
+ this.shim = getOwn(config.shim, map.id);
+ this.depExports = [];
+ this.depMaps = [];
+ this.depMatched = [];
+ this.pluginMaps = {};
+ this.depCount = 0;
+
+ /* this.exports this.factory
+ this.depMaps = [],
+ this.enabled, this.fetched
+ */
+ };
+
+ Module.prototype = {
+ init: function (depMaps, factory, errback, options) {
+ options = options || {};
+
+ //Do not do more inits if already done. Can happen if there
+ //are multiple define calls for the same module. That is not
+ //a normal, common case, but it is also not unexpected.
+ if (this.inited) {
+ return;
+ }
+
+ this.factory = factory;
+
+ if (errback) {
+ //Register for errors on this module.
+ this.on('error', errback);
+ } else if (this.events.error) {
+ //If no errback already, but there are error listeners
+ //on this module, set up an errback to pass to the deps.
+ errback = bind(this, function (err) {
+ this.emit('error', err);
+ });
+ }
+
+ //Do a copy of the dependency array, so that
+ //source inputs are not modified. For example
+ //"shim" deps are passed in here directly, and
+ //doing a direct modification of the depMaps array
+ //would affect that config.
+ this.depMaps = depMaps && depMaps.slice(0);
+
+ this.errback = errback;
+
+ //Indicate this module has be initialized
+ this.inited = true;
+
+ this.ignore = options.ignore;
+
+ //Could have option to init this module in enabled mode,
+ //or could have been previously marked as enabled. However,
+ //the dependencies are not known until init is called. So
+ //if enabled previously, now trigger dependencies as enabled.
+ if (options.enabled || this.enabled) {
+ //Enable this module and dependencies.
+ //Will call this.check()
+ this.enable();
+ } else {
+ this.check();
+ }
+ },
+
+ defineDep: function (i, depExports) {
+ //Because of cycles, defined callback for a given
+ //export can be called more than once.
+ if (!this.depMatched[i]) {
+ this.depMatched[i] = true;
+ this.depCount -= 1;
+ this.depExports[i] = depExports;
+ }
+ },
+
+ fetch: function () {
+ if (this.fetched) {
+ return;
+ }
+ this.fetched = true;
+
+ context.startTime = (new Date()).getTime();
+
+ var map = this.map;
+
+ //If the manager is for a plugin managed resource,
+ //ask the plugin to load it now.
+ if (this.shim) {
+ context.makeRequire(this.map, {
+ enableBuildCallback: true
+ })(this.shim.deps || [], bind(this, function () {
+ return map.prefix ? this.callPlugin() : this.load();
+ }));
+ } else {
+ //Regular dependency.
+ return map.prefix ? this.callPlugin() : this.load();
+ }
+ },
+
+ load: function () {
+ var url = this.map.url;
+
+ //Regular dependency.
+ if (!urlFetched[url]) {
+ urlFetched[url] = true;
+ context.load(this.map.id, url);
+ }
+ },
+
+ /**
+ * Checks if the module is ready to define itself, and if so,
+ * define it.
+ */
+ check: function () {
+ if (!this.enabled || this.enabling) {
+ return;
+ }
+
+ var err, cjsModule,
+ id = this.map.id,
+ depExports = this.depExports,
+ exports = this.exports,
+ factory = this.factory;
+
+ if (!this.inited) {
+ this.fetch();
+ } else if (this.error) {
+ this.emit('error', this.error);
+ } else if (!this.defining) {
+ //The factory could trigger another require call
+ //that would result in checking this module to
+ //define itself again. If already in the process
+ //of doing that, skip this work.
+ this.defining = true;
+
+ if (this.depCount < 1 && !this.defined) {
+ if (isFunction(factory)) {
+ //If there is an error listener, favor passing
+ //to that instead of throwing an error. However,
+ //only do it for define()'d modules. require
+ //errbacks should not be called for failures in
+ //their callbacks (#699). However if a global
+ //onError is set, use that.
+ if ((this.events.error && this.map.isDefine) ||
+ req.onError !== defaultOnError) {
+ try {
+ exports = context.execCb(id, factory, depExports, exports);
+ } catch (e) {
+ err = e;
+ }
+ } else {
+ exports = context.execCb(id, factory, depExports, exports);
+ }
+
+ // Favor return value over exports. If node/cjs in play,
+ // then will not have a return value anyway. Favor
+ // module.exports assignment over exports object.
+ if (this.map.isDefine && exports === undefined) {
+ cjsModule = this.module;
+ if (cjsModule) {
+ exports = cjsModule.exports;
+ } else if (this.usingExports) {
+ //exports already set the defined value.
+ exports = this.exports;
+ }
+ }
+
+ if (err) {
+ err.requireMap = this.map;
+ err.requireModules = this.map.isDefine ? [this.map.id] : null;
+ err.requireType = this.map.isDefine ? 'define' : 'require';
+ return onError((this.error = err));
+ }
+
+ } else {
+ //Just a literal value
+ exports = factory;
+ }
+
+ this.exports = exports;
+
+ if (this.map.isDefine && !this.ignore) {
+ defined[id] = exports;
+
+ if (req.onResourceLoad) {
+ req.onResourceLoad(context, this.map, this.depMaps);
+ }
+ }
+
+ //Clean up
+ cleanRegistry(id);
+
+ this.defined = true;
+ }
+
+ //Finished the define stage. Allow calling check again
+ //to allow define notifications below in the case of a
+ //cycle.
+ this.defining = false;
+
+ if (this.defined && !this.defineEmitted) {
+ this.defineEmitted = true;
+ this.emit('defined', this.exports);
+ this.defineEmitComplete = true;
+ }
+
+ }
+ },
+
+ callPlugin: function () {
+ var map = this.map,
+ id = map.id,
+ //Map already normalized the prefix.
+ pluginMap = makeModuleMap(map.prefix);
+
+ //Mark this as a dependency for this plugin, so it
+ //can be traced for cycles.
+ this.depMaps.push(pluginMap);
+
+ on(pluginMap, 'defined', bind(this, function (plugin) {
+ var load, normalizedMap, normalizedMod,
+ bundleId = getOwn(bundlesMap, this.map.id),
+ name = this.map.name,
+ parentName = this.map.parentMap ? this.map.parentMap.name : null,
+ localRequire = context.makeRequire(map.parentMap, {
+ enableBuildCallback: true
+ });
+
+ //If current map is not normalized, wait for that
+ //normalized name to load instead of continuing.
+ if (this.map.unnormalized) {
+ //Normalize the ID if the plugin allows it.
+ if (plugin.normalize) {
+ name = plugin.normalize(name, function (name) {
+ return normalize(name, parentName, true);
+ }) || '';
+ }
+
+ //prefix and name should already be normalized, no need
+ //for applying map config again either.
+ normalizedMap = makeModuleMap(map.prefix + '!' + name,
+ this.map.parentMap);
+ on(normalizedMap,
+ 'defined', bind(this, function (value) {
+ this.init([], function () { return value; }, null, {
+ enabled: true,
+ ignore: true
+ });
+ }));
+
+ normalizedMod = getOwn(registry, normalizedMap.id);
+ if (normalizedMod) {
+ //Mark this as a dependency for this plugin, so it
+ //can be traced for cycles.
+ this.depMaps.push(normalizedMap);
+
+ if (this.events.error) {
+ normalizedMod.on('error', bind(this, function (err) {
+ this.emit('error', err);
+ }));
+ }
+ normalizedMod.enable();
+ }
+
+ return;
+ }
+
+ //If a paths config, then just load that file instead to
+ //resolve the plugin, as it is built into that paths layer.
+ if (bundleId) {
+ this.map.url = context.nameToUrl(bundleId);
+ this.load();
+ return;
+ }
+
+ load = bind(this, function (value) {
+ this.init([], function () { return value; }, null, {
+ enabled: true
+ });
+ });
+
+ load.error = bind(this, function (err) {
+ this.inited = true;
+ this.error = err;
+ err.requireModules = [id];
+
+ //Remove temp unnormalized modules for this module,
+ //since they will never be resolved otherwise now.
+ eachProp(registry, function (mod) {
+ if (mod.map.id.indexOf(id + '_unnormalized') === 0) {
+ cleanRegistry(mod.map.id);
+ }
+ });
+
+ onError(err);
+ });
+
+ //Allow plugins to load other code without having to know the
+ //context or how to 'complete' the load.
+ load.fromText = bind(this, function (text, textAlt) {
+ /*jslint evil: true */
+ var moduleName = map.name,
+ moduleMap = makeModuleMap(moduleName),
+ hasInteractive = useInteractive;
+
+ //As of 2.1.0, support just passing the text, to reinforce
+ //fromText only being called once per resource. Still
+ //support old style of passing moduleName but discard
+ //that moduleName in favor of the internal ref.
+ if (textAlt) {
+ text = textAlt;
+ }
+
+ //Turn off interactive script matching for IE for any define
+ //calls in the text, then turn it back on at the end.
+ if (hasInteractive) {
+ useInteractive = false;
+ }
+
+ //Prime the system by creating a module instance for
+ //it.
+ getModule(moduleMap);
+
+ //Transfer any config to this other module.
+ if (hasProp(config.config, id)) {
+ config.config[moduleName] = config.config[id];
+ }
+
+ try {
+ req.exec(text);
+ } catch (e) {
+ return onError(makeError('fromtexteval',
+ 'fromText eval for ' + id +
+ ' failed: ' + e,
+ e,
+ [id]));
+ }
+
+ if (hasInteractive) {
+ useInteractive = true;
+ }
+
+ //Mark this as a dependency for the plugin
+ //resource
+ this.depMaps.push(moduleMap);
+
+ //Support anonymous modules.
+ context.completeLoad(moduleName);
+
+ //Bind the value of that module to the value for this
+ //resource ID.
+ localRequire([moduleName], load);
+ });
+
+ //Use parentName here since the plugin's name is not reliable,
+ //could be some weird string with no path that actually wants to
+ //reference the parentName's path.
+ plugin.load(map.name, localRequire, load, config);
+ }));
+
+ context.enable(pluginMap, this);
+ this.pluginMaps[pluginMap.id] = pluginMap;
+ },
+
+ enable: function () {
+ enabledRegistry[this.map.id] = this;
+ this.enabled = true;
+
+ //Set flag mentioning that the module is enabling,
+ //so that immediate calls to the defined callbacks
+ //for dependencies do not trigger inadvertent load
+ //with the depCount still being zero.
+ this.enabling = true;
+
+ //Enable each dependency
+ each(this.depMaps, bind(this, function (depMap, i) {
+ var id, mod, handler;
+
+ if (typeof depMap === 'string') {
+ //Dependency needs to be converted to a depMap
+ //and wired up to this module.
+ depMap = makeModuleMap(depMap,
+ (this.map.isDefine ? this.map : this.map.parentMap),
+ false,
+ !this.skipMap);
+ this.depMaps[i] = depMap;
+
+ handler = getOwn(handlers, depMap.id);
+
+ if (handler) {
+ this.depExports[i] = handler(this);
+ return;
+ }
+
+ this.depCount += 1;
+
+ on(depMap, 'defined', bind(this, function (depExports) {
+ this.defineDep(i, depExports);
+ this.check();
+ }));
+
+ if (this.errback) {
+ on(depMap, 'error', bind(this, this.errback));
+ }
+ }
+
+ id = depMap.id;
+ mod = registry[id];
+
+ //Skip special modules like 'require', 'exports', 'module'
+ //Also, don't call enable if it is already enabled,
+ //important in circular dependency cases.
+ if (!hasProp(handlers, id) && mod && !mod.enabled) {
+ context.enable(depMap, this);
+ }
+ }));
+
+ //Enable each plugin that is used in
+ //a dependency
+ eachProp(this.pluginMaps, bind(this, function (pluginMap) {
+ var mod = getOwn(registry, pluginMap.id);
+ if (mod && !mod.enabled) {
+ context.enable(pluginMap, this);
+ }
+ }));
+
+ this.enabling = false;
+
+ this.check();
+ },
+
+ on: function (name, cb) {
+ var cbs = this.events[name];
+ if (!cbs) {
+ cbs = this.events[name] = [];
+ }
+ cbs.push(cb);
+ },
+
+ emit: function (name, evt) {
+ each(this.events[name], function (cb) {
+ cb(evt);
+ });
+ if (name === 'error') {
+ //Now that the error handler was triggered, remove
+ //the listeners, since this broken Module instance
+ //can stay around for a while in the registry.
+ delete this.events[name];
+ }
+ }
+ };
+
+ function callGetModule(args) {
+ //Skip modules already defined.
+ if (!hasProp(defined, args[0])) {
+ getModule(makeModuleMap(args[0], null, true)).init(args[1], args[2]);
+ }
+ }
+
+ function removeListener(node, func, name, ieName) {
+ //Favor detachEvent because of IE9
+ //issue, see attachEvent/addEventListener comment elsewhere
+ //in this file.
+ if (node.detachEvent && !isOpera) {
+ //Probably IE. If not it will throw an error, which will be
+ //useful to know.
+ if (ieName) {
+ node.detachEvent(ieName, func);
+ }
+ } else {
+ node.removeEventListener(name, func, false);
+ }
+ }
+
+ /**
+ * Given an event from a script node, get the requirejs info from it,
+ * and then removes the event listeners on the node.
+ * @param {Event} evt
+ * @returns {Object}
+ */
+ function getScriptData(evt) {
+ //Using currentTarget instead of target for Firefox 2.0's sake. Not
+ //all old browsers will be supported, but this one was easy enough
+ //to support and still makes sense.
+ var node = evt.currentTarget || evt.srcElement;
+
+ //Remove the listeners once here.
+ removeListener(node, context.onScriptLoad, 'load', 'onreadystatechange');
+ removeListener(node, context.onScriptError, 'error');
+
+ return {
+ node: node,
+ id: node && node.getAttribute('data-requiremodule')
+ };
+ }
+
+ function intakeDefines() {
+ var args;
+
+ //Any defined modules in the global queue, intake them now.
+ takeGlobalQueue();
+
+ //Make sure any remaining defQueue items get properly processed.
+ while (defQueue.length) {
+ args = defQueue.shift();
+ if (args[0] === null) {
+ return onError(makeError('mismatch', 'Mismatched anonymous define() module: ' + args[args.length - 1]));
+ } else {
+ //args are id, deps, factory. Should be normalized by the
+ //define() function.
+ callGetModule(args);
+ }
+ }
+ }
+
+ context = {
+ config: config,
+ contextName: contextName,
+ registry: registry,
+ defined: defined,
+ urlFetched: urlFetched,
+ defQueue: defQueue,
+ Module: Module,
+ makeModuleMap: makeModuleMap,
+ nextTick: req.nextTick,
+ onError: onError,
+
+ /**
+ * Set a configuration for the context.
+ * @param {Object} cfg config object to integrate.
+ */
+ configure: function (cfg) {
+ //Make sure the baseUrl ends in a slash.
+ if (cfg.baseUrl) {
+ if (cfg.baseUrl.charAt(cfg.baseUrl.length - 1) !== '/') {
+ cfg.baseUrl += '/';
+ }
+ }
+
+ //Save off the paths since they require special processing,
+ //they are additive.
+ var shim = config.shim,
+ objs = {
+ paths: true,
+ bundles: true,
+ config: true,
+ map: true
+ };
+
+ eachProp(cfg, function (value, prop) {
+ if (objs[prop]) {
+ if (!config[prop]) {
+ config[prop] = {};
+ }
+ mixin(config[prop], value, true, true);
+ } else {
+ config[prop] = value;
+ }
+ });
+
+ //Reverse map the bundles
+ if (cfg.bundles) {
+ eachProp(cfg.bundles, function (value, prop) {
+ each(value, function (v) {
+ if (v !== prop) {
+ bundlesMap[v] = prop;
+ }
+ });
+ });
+ }
+
+ //Merge shim
+ if (cfg.shim) {
+ eachProp(cfg.shim, function (value, id) {
+ //Normalize the structure
+ if (isArray(value)) {
+ value = {
+ deps: value
+ };
+ }
+ if ((value.exports || value.init) && !value.exportsFn) {
+ value.exportsFn = context.makeShimExports(value);
+ }
+ shim[id] = value;
+ });
+ config.shim = shim;
+ }
+
+ //Adjust packages if necessary.
+ if (cfg.packages) {
+ each(cfg.packages, function (pkgObj) {
+ var location, name;
+
+ pkgObj = typeof pkgObj === 'string' ? { name: pkgObj } : pkgObj;
+
+ name = pkgObj.name;
+ location = pkgObj.location;
+ if (location) {
+ config.paths[name] = pkgObj.location;
+ }
+
+ //Save pointer to main module ID for pkg name.
+ //Remove leading dot in main, so main paths are normalized,
+ //and remove any trailing .js, since different package
+ //envs have different conventions: some use a module name,
+ //some use a file name.
+ config.pkgs[name] = pkgObj.name + '/' + (pkgObj.main || 'main')
+ .replace(currDirRegExp, '')
+ .replace(jsSuffixRegExp, '');
+ });
+ }
+
+ //If there are any "waiting to execute" modules in the registry,
+ //update the maps for them, since their info, like URLs to load,
+ //may have changed.
+ eachProp(registry, function (mod, id) {
+ //If module already has init called, since it is too
+ //late to modify them, and ignore unnormalized ones
+ //since they are transient.
+ if (!mod.inited && !mod.map.unnormalized) {
+ mod.map = makeModuleMap(id);
+ }
+ });
+
+ //If a deps array or a config callback is specified, then call
+ //require with those args. This is useful when require is defined as a
+ //config object before require.js is loaded.
+ if (cfg.deps || cfg.callback) {
+ context.require(cfg.deps || [], cfg.callback);
+ }
+ },
+
+ makeShimExports: function (value) {
+ function fn() {
+ var ret;
+ if (value.init) {
+ ret = value.init.apply(global, arguments);
+ }
+ return ret || (value.exports && getGlobal(value.exports));
+ }
+ return fn;
+ },
+
+ makeRequire: function (relMap, options) {
+ options = options || {};
+
+ function localRequire(deps, callback, errback) {
+ var id, map, requireMod;
+
+ if (options.enableBuildCallback && callback && isFunction(callback)) {
+ callback.__requireJsBuild = true;
+ }
+
+ if (typeof deps === 'string') {
+ if (isFunction(callback)) {
+ //Invalid call
+ return onError(makeError('requireargs', 'Invalid require call'), errback);
+ }
+
+ //If require|exports|module are requested, get the
+ //value for them from the special handlers. Caveat:
+ //this only works while module is being defined.
+ if (relMap && hasProp(handlers, deps)) {
+ return handlers[deps](registry[relMap.id]);
+ }
+
+ //Synchronous access to one module. If require.get is
+ //available (as in the Node adapter), prefer that.
+ if (req.get) {
+ return req.get(context, deps, relMap, localRequire);
+ }
+
+ //Normalize module name, if it contains . or ..
+ map = makeModuleMap(deps, relMap, false, true);
+ id = map.id;
+
+ if (!hasProp(defined, id)) {
+ return onError(makeError('notloaded', 'Module name "' +
+ id +
+ '" has not been loaded yet for context: ' +
+ contextName +
+ (relMap ? '' : '. Use require([])')));
+ }
+ return defined[id];
+ }
+
+ //Grab defines waiting in the global queue.
+ intakeDefines();
+
+ //Mark all the dependencies as needing to be loaded.
+ context.nextTick(function () {
+ //Some defines could have been added since the
+ //require call, collect them.
+ intakeDefines();
+
+ requireMod = getModule(makeModuleMap(null, relMap));
+
+ //Store if map config should be applied to this require
+ //call for dependencies.
+ requireMod.skipMap = options.skipMap;
+
+ requireMod.init(deps, callback, errback, {
+ enabled: true
+ });
+
+ checkLoaded();
+ });
+
+ return localRequire;
+ }
+
+ mixin(localRequire, {
+ isBrowser: isBrowser,
+
+ /**
+ * Converts a module name + .extension into an URL path.
+ * *Requires* the use of a module name. It does not support using
+ * plain URLs like nameToUrl.
+ */
+ toUrl: function (moduleNamePlusExt) {
+ var ext,
+ index = moduleNamePlusExt.lastIndexOf('.'),
+ segment = moduleNamePlusExt.split('/')[0],
+ isRelative = segment === '.' || segment === '..';
+
+ //Have a file extension alias, and it is not the
+ //dots from a relative path.
+ if (index !== -1 && (!isRelative || index > 1)) {
+ ext = moduleNamePlusExt.substring(index, moduleNamePlusExt.length);
+ moduleNamePlusExt = moduleNamePlusExt.substring(0, index);
+ }
+
+ return context.nameToUrl(normalize(moduleNamePlusExt,
+ relMap && relMap.id, true), ext, true);
+ },
+
+ defined: function (id) {
+ return hasProp(defined, makeModuleMap(id, relMap, false, true).id);
+ },
+
+ specified: function (id) {
+ id = makeModuleMap(id, relMap, false, true).id;
+ return hasProp(defined, id) || hasProp(registry, id);
+ }
+ });
+
+ //Only allow undef on top level require calls
+ if (!relMap) {
+ localRequire.undef = function (id) {
+ //Bind any waiting define() calls to this context,
+ //fix for #408
+ takeGlobalQueue();
+
+ var map = makeModuleMap(id, relMap, true),
+ mod = getOwn(registry, id);
+
+ removeScript(id);
+
+ delete defined[id];
+ delete urlFetched[map.url];
+ delete undefEvents[id];
+
+ //Clean queued defines too. Go backwards
+ //in array so that the splices do not
+ //mess up the iteration.
+ eachReverse(defQueue, function(args, i) {
+ if(args[0] === id) {
+ defQueue.splice(i, 1);
+ }
+ });
+
+ if (mod) {
+ //Hold on to listeners in case the
+ //module will be attempted to be reloaded
+ //using a different config.
+ if (mod.events.defined) {
+ undefEvents[id] = mod.events;
+ }
+
+ cleanRegistry(id);
+ }
+ };
+ }
+
+ return localRequire;
+ },
+
+ /**
+ * Called to enable a module if it is still in the registry
+ * awaiting enablement. A second arg, parent, the parent module,
+ * is passed in for context, when this method is overridden by
+ * the optimizer. Not shown here to keep code compact.
+ */
+ enable: function (depMap) {
+ var mod = getOwn(registry, depMap.id);
+ if (mod) {
+ getModule(depMap).enable();
+ }
+ },
+
+ /**
+ * Internal method used by environment adapters to complete a load event.
+ * A load event could be a script load or just a load pass from a synchronous
+ * load call.
+ * @param {String} moduleName the name of the module to potentially complete.
+ */
+ completeLoad: function (moduleName) {
+ var found, args, mod,
+ shim = getOwn(config.shim, moduleName) || {},
+ shExports = shim.exports;
+
+ takeGlobalQueue();
+
+ while (defQueue.length) {
+ args = defQueue.shift();
+ if (args[0] === null) {
+ args[0] = moduleName;
+ //If already found an anonymous module and bound it
+ //to this name, then this is some other anon module
+ //waiting for its completeLoad to fire.
+ if (found) {
+ break;
+ }
+ found = true;
+ } else if (args[0] === moduleName) {
+ //Found matching define call for this script!
+ found = true;
+ }
+
+ callGetModule(args);
+ }
+
+ //Do this after the cycle of callGetModule in case the result
+ //of those calls/init calls changes the registry.
+ mod = getOwn(registry, moduleName);
+
+ if (!found && !hasProp(defined, moduleName) && mod && !mod.inited) {
+ if (config.enforceDefine && (!shExports || !getGlobal(shExports))) {
+ if (hasPathFallback(moduleName)) {
+ return;
+ } else {
+ return onError(makeError('nodefine',
+ 'No define call for ' + moduleName,
+ null,
+ [moduleName]));
+ }
+ } else {
+ //A script that does not call define(), so just simulate
+ //the call for it.
+ callGetModule([moduleName, (shim.deps || []), shim.exportsFn]);
+ }
+ }
+
+ checkLoaded();
+ },
+
+ /**
+ * Converts a module name to a file path. Supports cases where
+ * moduleName may actually be just an URL.
+ * Note that it **does not** call normalize on the moduleName,
+ * it is assumed to have already been normalized. This is an
+ * internal API, not a public one. Use toUrl for the public API.
+ */
+ nameToUrl: function (moduleName, ext, skipExt) {
+ var paths, syms, i, parentModule, url,
+ parentPath, bundleId,
+ pkgMain = getOwn(config.pkgs, moduleName);
+
+ if (pkgMain) {
+ moduleName = pkgMain;
+ }
+
+ bundleId = getOwn(bundlesMap, moduleName);
+
+ if (bundleId) {
+ return context.nameToUrl(bundleId, ext, skipExt);
+ }
+
+ //If a colon is in the URL, it indicates a protocol is used and it is just
+ //an URL to a file, or if it starts with a slash, contains a query arg (i.e. ?)
+ //or ends with .js, then assume the user meant to use an url and not a module id.
+ //The slash is important for protocol-less URLs as well as full paths.
+ if (req.jsExtRegExp.test(moduleName)) {
+ //Just a plain path, not module name lookup, so just return it.
+ //Add extension if it is included. This is a bit wonky, only non-.js things pass
+ //an extension, this method probably needs to be reworked.
+ url = moduleName + (ext || '');
+ } else {
+ //A module that needs to be converted to a path.
+ paths = config.paths;
+
+ syms = moduleName.split('/');
+ //For each module name segment, see if there is a path
+ //registered for it. Start with most specific name
+ //and work up from it.
+ for (i = syms.length; i > 0; i -= 1) {
+ parentModule = syms.slice(0, i).join('/');
+
+ parentPath = getOwn(paths, parentModule);
+ if (parentPath) {
+ //If an array, it means there are a few choices,
+ //Choose the one that is desired
+ if (isArray(parentPath)) {
+ parentPath = parentPath[0];
+ }
+ syms.splice(0, i, parentPath);
+ break;
+ }
+ }
+
+ //Join the path parts together, then figure out if baseUrl is needed.
+ url = syms.join('/');
+ url += (ext || (/^data\:|\?/.test(url) || skipExt ? '' : '.js'));
+ url = (url.charAt(0) === '/' || url.match(/^[\w\+\.\-]+:/) ? '' : config.baseUrl) + url;
+ }
+
+ return config.urlArgs ? url +
+ ((url.indexOf('?') === -1 ? '?' : '&') +
+ config.urlArgs) : url;
+ },
+
+ //Delegates to req.load. Broken out as a separate function to
+ //allow overriding in the optimizer.
+ load: function (id, url) {
+ req.load(context, id, url);
+ },
+
+ /**
+ * Executes a module callback function. Broken out as a separate function
+ * solely to allow the build system to sequence the files in the built
+ * layer in the right sequence.
+ *
+ * @private
+ */
+ execCb: function (name, callback, args, exports) {
+ return callback.apply(exports, args);
+ },
+
+ /**
+ * callback for script loads, used to check status of loading.
+ *
+ * @param {Event} evt the event from the browser for the script
+ * that was loaded.
+ */
+ onScriptLoad: function (evt) {
+ //Using currentTarget instead of target for Firefox 2.0's sake. Not
+ //all old browsers will be supported, but this one was easy enough
+ //to support and still makes sense.
+ if (evt.type === 'load' ||
+ (readyRegExp.test((evt.currentTarget || evt.srcElement).readyState))) {
+ //Reset interactive script so a script node is not held onto for
+ //to long.
+ interactiveScript = null;
+
+ //Pull out the name of the module and the context.
+ var data = getScriptData(evt);
+ context.completeLoad(data.id);
+ }
+ },
+
+ /**
+ * Callback for script errors.
+ */
+ onScriptError: function (evt) {
+ var data = getScriptData(evt);
+ if (!hasPathFallback(data.id)) {
+ return onError(makeError('scripterror', 'Script error for: ' + data.id, evt, [data.id]));
+ }
+ }
+ };
+
+ context.require = context.makeRequire();
+ return context;
+ }
+
+ /**
+ * Main entry point.
+ *
+ * If the only argument to require is a string, then the module that
+ * is represented by that string is fetched for the appropriate context.
+ *
+ * If the first argument is an array, then it will be treated as an array
+ * of dependency string names to fetch. An optional function callback can
+ * be specified to execute when all of those dependencies are available.
+ *
+ * Make a local req variable to help Caja compliance (it assumes things
+ * on a require that are not standardized), and to give a short
+ * name for minification/local scope use.
+ */
+ req = requirejs = function (deps, callback, errback, optional) {
+
+ //Find the right context, use default
+ var context, config,
+ contextName = defContextName;
+
+ // Determine if have config object in the call.
+ if (!isArray(deps) && typeof deps !== 'string') {
+ // deps is a config object
+ config = deps;
+ if (isArray(callback)) {
+ // Adjust args if there are dependencies
+ deps = callback;
+ callback = errback;
+ errback = optional;
+ } else {
+ deps = [];
+ }
+ }
+
+ if (config && config.context) {
+ contextName = config.context;
+ }
+
+ context = getOwn(contexts, contextName);
+ if (!context) {
+ context = contexts[contextName] = req.s.newContext(contextName);
+ }
+
+ if (config) {
+ context.configure(config);
+ }
+
+ return context.require(deps, callback, errback);
+ };
+
+ /**
+ * Support require.config() to make it easier to cooperate with other
+ * AMD loaders on globally agreed names.
+ */
+ req.config = function (config) {
+ return req(config);
+ };
+
+ /**
+ * Execute something after the current tick
+ * of the event loop. Override for other envs
+ * that have a better solution than setTimeout.
+ * @param {Function} fn function to execute later.
+ */
+ req.nextTick = typeof setTimeout !== 'undefined' ? function (fn) {
+ setTimeout(fn, 4);
+ } : function (fn) { fn(); };
+
+ /**
+ * Export require as a global, but only if it does not already exist.
+ */
+ if (!require) {
+ require = req;
+ }
+
+ req.version = version;
+
+ //Used to filter out dependencies that are already paths.
+ req.jsExtRegExp = /^\/|:|\?|\.js$/;
+ req.isBrowser = isBrowser;
+ s = req.s = {
+ contexts: contexts,
+ newContext: newContext
+ };
+
+ //Create default context.
+ req({});
+
+ //Exports some context-sensitive methods on global require.
+ each([
+ 'toUrl',
+ 'undef',
+ 'defined',
+ 'specified'
+ ], function (prop) {
+ //Reference from contexts instead of early binding to default context,
+ //so that during builds, the latest instance of the default context
+ //with its config gets used.
+ req[prop] = function () {
+ var ctx = contexts[defContextName];
+ return ctx.require[prop].apply(ctx, arguments);
+ };
+ });
+
+ if (isBrowser) {
+ head = s.head = document.getElementsByTagName('head')[0];
+ //If BASE tag is in play, using appendChild is a problem for IE6.
+ //When that browser dies, this can be removed. Details in this jQuery bug:
+ //http://dev.jquery.com/ticket/2709
+ baseElement = document.getElementsByTagName('base')[0];
+ if (baseElement) {
+ head = s.head = baseElement.parentNode;
+ }
+ }
+
+ /**
+ * Any errors that require explicitly generates will be passed to this
+ * function. Intercept/override it if you want custom error handling.
+ * @param {Error} err the error object.
+ */
+ req.onError = defaultOnError;
+
+ /**
+ * Creates the node for the load command. Only used in browser envs.
+ */
+ req.createNode = function (config, moduleName, url) {
+ var node = config.xhtml ?
+ document.createElementNS('http://www.w3.org/1999/xhtml', 'html:script') :
+ document.createElement('script');
+ node.type = config.scriptType || 'text/javascript';
+ node.charset = 'utf-8';
+ node.async = true;
+ return node;
+ };
+
+ /**
+ * Does the request to load a module for the browser case.
+ * Make this a separate function to allow other environments
+ * to override it.
+ *
+ * @param {Object} context the require context to find state.
+ * @param {String} moduleName the name of the module.
+ * @param {Object} url the URL to the module.
+ */
+ req.load = function (context, moduleName, url) {
+ var config = (context && context.config) || {},
+ node;
+ if (isBrowser) {
+ //In the browser so use a script tag
+ node = req.createNode(config, moduleName, url);
+
+ node.setAttribute('data-requirecontext', context.contextName);
+ node.setAttribute('data-requiremodule', moduleName);
+
+ //Set up load listener. Test attachEvent first because IE9 has
+ //a subtle issue in its addEventListener and script onload firings
+ //that do not match the behavior of all other browsers with
+ //addEventListener support, which fire the onload event for a
+ //script right after the script execution. See:
+ //https://connect.microsoft.com/IE/feedback/details/648057/script-onload-event-is-not-fired-immediately-after-script-execution
+ //UNFORTUNATELY Opera implements attachEvent but does not follow the script
+ //script execution mode.
+ if (node.attachEvent &&
+ //Check if node.attachEvent is artificially added by custom script or
+ //natively supported by browser
+ //read https://github.com/jrburke/requirejs/issues/187
+ //if we can NOT find [native code] then it must NOT natively supported.
+ //in IE8, node.attachEvent does not have toString()
+ //Note the test for "[native code" with no closing brace, see:
+ //https://github.com/jrburke/requirejs/issues/273
+ !(node.attachEvent.toString && node.attachEvent.toString().indexOf('[native code') < 0) &&
+ !isOpera) {
+ //Probably IE. IE (at least 6-8) do not fire
+ //script onload right after executing the script, so
+ //we cannot tie the anonymous define call to a name.
+ //However, IE reports the script as being in 'interactive'
+ //readyState at the time of the define call.
+ useInteractive = true;
+
+ node.attachEvent('onreadystatechange', context.onScriptLoad);
+ //It would be great to add an error handler here to catch
+ //404s in IE9+. However, onreadystatechange will fire before
+ //the error handler, so that does not help. If addEventListener
+ //is used, then IE will fire error before load, but we cannot
+ //use that pathway given the connect.microsoft.com issue
+ //mentioned above about not doing the 'script execute,
+ //then fire the script load event listener before execute
+ //next script' that other browsers do.
+ //Best hope: IE10 fixes the issues,
+ //and then destroys all installs of IE 6-9.
+ //node.attachEvent('onerror', context.onScriptError);
+ } else {
+ node.addEventListener('load', context.onScriptLoad, false);
+ node.addEventListener('error', context.onScriptError, false);
+ }
+ node.src = url;
+
+ //For some cache cases in IE 6-8, the script executes before the end
+ //of the appendChild execution, so to tie an anonymous define
+ //call to the module name (which is stored on the node), hold on
+ //to a reference to this node, but clear after the DOM insertion.
+ currentlyAddingScript = node;
+ if (baseElement) {
+ head.insertBefore(node, baseElement);
+ } else {
+ head.appendChild(node);
+ }
+ currentlyAddingScript = null;
+
+ return node;
+ } else if (isWebWorker) {
+ try {
+ //In a web worker, use importScripts. This is not a very
+ //efficient use of importScripts, importScripts will block until
+ //its script is downloaded and evaluated. However, if web workers
+ //are in play, the expectation that a build has been done so that
+ //only one script needs to be loaded anyway. This may need to be
+ //reevaluated if other use cases become common.
+ importScripts(url);
+
+ //Account for anonymous modules
+ context.completeLoad(moduleName);
+ } catch (e) {
+ context.onError(makeError('importscripts',
+ 'importScripts failed for ' +
+ moduleName + ' at ' + url,
+ e,
+ [moduleName]));
+ }
+ }
+ };
+
+ function getInteractiveScript() {
+ if (interactiveScript && interactiveScript.readyState === 'interactive') {
+ return interactiveScript;
+ }
+
+ eachReverse(scripts(), function (script) {
+ if (script.readyState === 'interactive') {
+ return (interactiveScript = script);
+ }
+ });
+ return interactiveScript;
+ }
+
+ //Look for a data-main script attribute, which could also adjust the baseUrl.
+ if (isBrowser && !cfg.skipDataMain) {
+ //Figure out baseUrl. Get it from the script tag with require.js in it.
+ eachReverse(scripts(), function (script) {
+ //Set the 'head' where we can append children by
+ //using the script's parent.
+ if (!head) {
+ head = script.parentNode;
+ }
+
+ //Look for a data-main attribute to set main script for the page
+ //to load. If it is there, the path to data main becomes the
+ //baseUrl, if it is not already set.
+ dataMain = script.getAttribute('data-main');
+ if (dataMain) {
+ //Preserve dataMain in case it is a path (i.e. contains '?')
+ mainScript = dataMain;
+
+ //Set final baseUrl if there is not already an explicit one.
+ if (!cfg.baseUrl) {
+ //Pull off the directory of data-main for use as the
+ //baseUrl.
+ src = mainScript.split('/');
+ mainScript = src.pop();
+ subPath = src.length ? src.join('/') + '/' : './';
+
+ cfg.baseUrl = subPath;
+ }
+
+ //Strip off any trailing .js since mainScript is now
+ //like a module name.
+ mainScript = mainScript.replace(jsSuffixRegExp, '');
+
+ //If mainScript is still a path, fall back to dataMain
+ if (req.jsExtRegExp.test(mainScript)) {
+ mainScript = dataMain;
+ }
+
+ //Put the data-main script in the files to load.
+ cfg.deps = cfg.deps ? cfg.deps.concat(mainScript) : [mainScript];
+
+ return true;
+ }
+ });
+ }
+
+ /**
+ * The function that handles definitions of modules. Differs from
+ * require() in that a string for the module should be the first argument,
+ * and the function to execute after dependencies are loaded should
+ * return a value to define the module corresponding to the first argument's
+ * name.
+ */
+ define = function (name, deps, callback) {
+ var node, context;
+
+ //Allow for anonymous modules
+ if (typeof name !== 'string') {
+ //Adjust args appropriately
+ callback = deps;
+ deps = name;
+ name = null;
+ }
+
+ //This module may not have dependencies
+ if (!isArray(deps)) {
+ callback = deps;
+ deps = null;
+ }
+
+ //If no name, and callback is a function, then figure out if it a
+ //CommonJS thing with dependencies.
+ if (!deps && isFunction(callback)) {
+ deps = [];
+ //Remove comments from the callback string,
+ //look for require calls, and pull them into the dependencies,
+ //but only if there are function args.
+ if (callback.length) {
+ callback
+ .toString()
+ .replace(commentRegExp, '')
+ .replace(cjsRequireRegExp, function (match, dep) {
+ deps.push(dep);
+ });
+
+ //May be a CommonJS thing even without require calls, but still
+ //could use exports, and module. Avoid doing exports and module
+ //work though if it just needs require.
+ //REQUIRES the function to expect the CommonJS variables in the
+ //order listed below.
+ deps = (callback.length === 1 ? ['require'] : ['require', 'exports', 'module']).concat(deps);
+ }
+ }
+
+ //If in IE 6-8 and hit an anonymous define() call, do the interactive
+ //work.
+ if (useInteractive) {
+ node = currentlyAddingScript || getInteractiveScript();
+ if (node) {
+ if (!name) {
+ name = node.getAttribute('data-requiremodule');
+ }
+ context = contexts[node.getAttribute('data-requirecontext')];
+ }
+ }
+
+ //Always save off evaluating the def call until the script onload handler.
+ //This allows multiple modules to be in a file without prematurely
+ //tracing dependencies, and allows for anonymous module support,
+ //where the module name is not known until the script onload event
+ //occurs. If no context, use the global queue, and get it processed
+ //in the onscript load callback.
+ (context ? context.defQueue : globalDefQueue).push([name, deps, callback]);
+ };
+
+ define.amd = {
+ jQuery: true
+ };
+
+
+ /**
+ * Executes the text. Normally just uses eval, but can be modified
+ * to use a better, environment-specific call. Only used for transpiling
+ * loader plugins, not for plain JS modules.
+ * @param {String} text the text to execute/evaluate.
+ */
+ req.exec = function (text) {
+ /*jslint evil: true */
+ return eval(text);
+ };
+
+ //Set up with config info.
+ req(cfg);
+}(this));
diff --git a/devtools/client/jsonview/main.js b/devtools/client/jsonview/main.js
new file mode 100644
index 000000000..a438e2e34
--- /dev/null
+++ b/devtools/client/jsonview/main.js
@@ -0,0 +1,62 @@
+/* -*- 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/. */
+/* globals JsonViewUtils*/
+
+"use strict";
+
+const { Cu } = require("chrome");
+const Services = require("Services");
+
+const { XPCOMUtils } = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {});
+
+XPCOMUtils.defineLazyGetter(this, "JsonViewUtils", function () {
+ return require("devtools/client/jsonview/utils");
+});
+
+/**
+ * Singleton object that represents the JSON View in-content tool.
+ * It has the same lifetime as the browser. Initialization done by
+ * DevTools() object from devtools/client/framework/devtools.js
+ */
+var JsonView = {
+ initialize: function () {
+ // Load JSON converter module. This converter is responsible
+ // for handling 'application/json' documents and converting
+ // them into a simple web-app that allows easy inspection
+ // of the JSON data.
+ Services.ppmm.loadProcessScript(
+ "resource://devtools/client/jsonview/converter-observer.js",
+ true);
+
+ this.onSaveListener = this.onSave.bind(this);
+
+ // Register for messages coming from the child process.
+ Services.ppmm.addMessageListener(
+ "devtools:jsonview:save", this.onSaveListener);
+ },
+
+ destroy: function () {
+ Services.ppmm.removeMessageListener(
+ "devtools:jsonview:save", this.onSaveListener);
+ },
+
+ // Message handlers for events from child processes
+
+ /**
+ * Save JSON to a file needs to be implemented here
+ * in the parent process.
+ */
+ onSave: function (message) {
+ let value = message.data;
+ let file = JsonViewUtils.getTargetFile();
+ if (file) {
+ JsonViewUtils.saveToFile(file, value);
+ }
+ }
+};
+
+// Exports from this module
+module.exports.JsonView = JsonView;
diff --git a/devtools/client/jsonview/moz.build b/devtools/client/jsonview/moz.build
new file mode 100644
index 000000000..06a98ed9b
--- /dev/null
+++ b/devtools/client/jsonview/moz.build
@@ -0,0 +1,23 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DIRS += [
+ 'components',
+ 'css',
+ 'lib'
+]
+
+DevToolsModules(
+ 'converter-child.js',
+ 'converter-observer.js',
+ 'converter-sniffer.js',
+ 'json-viewer.js',
+ 'main.js',
+ 'utils.js',
+ 'viewer-config.js'
+)
+
+BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
diff --git a/devtools/client/jsonview/test/.eslintrc.js b/devtools/client/jsonview/test/.eslintrc.js
new file mode 100644
index 000000000..8d15a76d9
--- /dev/null
+++ b/devtools/client/jsonview/test/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../../.eslintrc.mochitests.js"
+};
diff --git a/devtools/client/jsonview/test/array_json.json b/devtools/client/jsonview/test/array_json.json
new file mode 100644
index 000000000..f91c3e08d
--- /dev/null
+++ b/devtools/client/jsonview/test/array_json.json
@@ -0,0 +1 @@
+[{"name": "jan"},{"name": "honza"},{"name": "odvarko"}]
diff --git a/devtools/client/jsonview/test/array_json.json^headers^ b/devtools/client/jsonview/test/array_json.json^headers^
new file mode 100644
index 000000000..6010bfd18
--- /dev/null
+++ b/devtools/client/jsonview/test/array_json.json^headers^
@@ -0,0 +1 @@
+Content-Type: application/json; charset=utf-8
diff --git a/devtools/client/jsonview/test/browser.ini b/devtools/client/jsonview/test/browser.ini
new file mode 100644
index 000000000..14f640c8c
--- /dev/null
+++ b/devtools/client/jsonview/test/browser.ini
@@ -0,0 +1,28 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ array_json.json
+ array_json.json^headers^
+ doc_frame_script.js
+ head.js
+ invalid_json.json
+ invalid_json.json^headers^
+ simple_json.json
+ simple_json.json^headers^
+ valid_json.json
+ valid_json.json^headers^
+ !/devtools/client/commandline/test/head.js
+ !/devtools/client/framework/test/head.js
+ !/devtools/client/framework/test/shared-head.js
+
+[browser_jsonview_copy_headers.js]
+subsuite = clipboard
+[browser_jsonview_copy_json.js]
+subsuite = clipboard
+[browser_jsonview_copy_rawdata.js]
+subsuite = clipboard
+[browser_jsonview_filter.js]
+[browser_jsonview_invalid_json.js]
+[browser_jsonview_valid_json.js]
+[browser_jsonview_save_json.js]
diff --git a/devtools/client/jsonview/test/browser_jsonview_copy_headers.js b/devtools/client/jsonview/test/browser_jsonview_copy_headers.js
new file mode 100644
index 000000000..1ffe9f8ca
--- /dev/null
+++ b/devtools/client/jsonview/test/browser_jsonview_copy_headers.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 TEST_JSON_URL = URL_ROOT + "valid_json.json";
+
+add_task(function* () {
+ info("Test valid JSON started");
+
+ yield addJsonViewTab(TEST_JSON_URL);
+
+ // Select the RawData tab
+ yield selectJsonViewContentTab("headers");
+
+ // Check displayed headers
+ let count = yield getElementCount(".headersPanelBox .netHeadersGroup");
+ is(count, 2, "There must be two header groups");
+
+ let text = yield getElementText(".headersPanelBox .netInfoHeadersTable");
+ isnot(text, "", "Headers text must not be empty");
+
+ let browser = gBrowser.selectedBrowser;
+
+ // Verify JSON copy into the clipboard.
+ yield waitForClipboardPromise(function setup() {
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ ".headersPanelBox .toolbar button.copy",
+ {}, browser);
+ }, function validator(value) {
+ return value.indexOf("application/json") > 0;
+ });
+});
diff --git a/devtools/client/jsonview/test/browser_jsonview_copy_json.js b/devtools/client/jsonview/test/browser_jsonview_copy_json.js
new file mode 100644
index 000000000..b4c08b843
--- /dev/null
+++ b/devtools/client/jsonview/test/browser_jsonview_copy_json.js
@@ -0,0 +1,31 @@
+/* -*- 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 TEST_JSON_URL = URL_ROOT + "simple_json.json";
+
+add_task(function* () {
+ info("Test copy JSON started");
+
+ yield addJsonViewTab(TEST_JSON_URL);
+
+ let countBefore = yield getElementCount(".jsonPanelBox .treeTable .treeRow");
+ ok(countBefore == 1, "There must be one row");
+
+ let text = yield getElementText(".jsonPanelBox .treeTable .treeRow");
+ is(text, "name\"value\"", "There must be proper JSON displayed");
+
+ // Verify JSON copy into the clipboard.
+ let value = "{\"name\": \"value\"}\n";
+ let browser = gBrowser.selectedBrowser;
+ let selector = ".jsonPanelBox .toolbar button.copy";
+ yield waitForClipboardPromise(function setup() {
+ BrowserTestUtils.synthesizeMouseAtCenter(selector, {}, browser);
+ }, function validator(result) {
+ let str = normalizeNewLines(result);
+ return str == value;
+ });
+});
diff --git a/devtools/client/jsonview/test/browser_jsonview_copy_rawdata.js b/devtools/client/jsonview/test/browser_jsonview_copy_rawdata.js
new file mode 100644
index 000000000..d2346ea42
--- /dev/null
+++ b/devtools/client/jsonview/test/browser_jsonview_copy_rawdata.js
@@ -0,0 +1,53 @@
+/* -*- 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 TEST_JSON_URL = URL_ROOT + "simple_json.json";
+
+let jsonText = "{\"name\": \"value\"}\n";
+let prettyJson = "{\n \"name\": \"value\"\n}";
+
+add_task(function* () {
+ info("Test copy raw data started");
+
+ yield addJsonViewTab(TEST_JSON_URL);
+
+ // Select the RawData tab
+ yield selectJsonViewContentTab("rawdata");
+
+ // Check displayed JSON
+ let text = yield getElementText(".textPanelBox .data");
+ is(text, jsonText, "Proper JSON must be displayed in DOM");
+
+ let browser = gBrowser.selectedBrowser;
+
+ // Verify JSON copy into the clipboard.
+ yield waitForClipboardPromise(function setup() {
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ ".textPanelBox .toolbar button.copy",
+ {}, browser);
+ }, jsonText);
+
+ // Click 'Pretty Print' button
+ yield BrowserTestUtils.synthesizeMouseAtCenter(
+ ".textPanelBox .toolbar button.prettyprint",
+ {}, browser);
+
+ let prettyText = yield getElementText(".textPanelBox .data");
+ prettyText = normalizeNewLines(prettyText);
+ ok(prettyText.startsWith(prettyJson),
+ "Pretty printed JSON must be displayed");
+
+ // Verify JSON copy into the clipboard.
+ yield waitForClipboardPromise(function setup() {
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ ".textPanelBox .toolbar button.copy",
+ {}, browser);
+ }, function validator(value) {
+ let str = normalizeNewLines(value);
+ return str == prettyJson;
+ });
+});
diff --git a/devtools/client/jsonview/test/browser_jsonview_filter.js b/devtools/client/jsonview/test/browser_jsonview_filter.js
new file mode 100644
index 000000000..5e87bb8ae
--- /dev/null
+++ b/devtools/client/jsonview/test/browser_jsonview_filter.js
@@ -0,0 +1,28 @@
+/* -*- 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 TEST_JSON_URL = URL_ROOT + "array_json.json";
+
+add_task(function* () {
+ info("Test valid JSON started");
+
+ yield addJsonViewTab(TEST_JSON_URL);
+
+ let count = yield getElementCount(".jsonPanelBox .treeTable .treeRow");
+ is(count, 6, "There must be expected number of rows");
+
+ // XXX use proper shortcut to focus the filter box
+ // as soon as bug Bug 1178771 is fixed.
+ yield sendString("h", ".jsonPanelBox .searchBox");
+
+ // The filtering is done asynchronously so, we need to wait.
+ yield waitForFilter();
+
+ let hiddenCount = yield getElementCount(
+ ".jsonPanelBox .treeTable .treeRow.hidden");
+ is(hiddenCount, 4, "There must be expected number of hidden rows");
+});
diff --git a/devtools/client/jsonview/test/browser_jsonview_invalid_json.js b/devtools/client/jsonview/test/browser_jsonview_invalid_json.js
new file mode 100644
index 000000000..de3cbd74d
--- /dev/null
+++ b/devtools/client/jsonview/test/browser_jsonview_invalid_json.js
@@ -0,0 +1,20 @@
+/* -*- 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 TEST_JSON_URL = URL_ROOT + "invalid_json.json";
+
+add_task(function* () {
+ info("Test invalid JSON started");
+
+ yield addJsonViewTab(TEST_JSON_URL);
+
+ let count = yield getElementCount(".jsonPanelBox .treeTable .treeRow");
+ ok(count == 0, "There must be no row");
+
+ let text = yield getElementText(".jsonPanelBox .jsonParseError");
+ ok(text, "There must be an error description");
+});
diff --git a/devtools/client/jsonview/test/browser_jsonview_save_json.js b/devtools/client/jsonview/test/browser_jsonview_save_json.js
new file mode 100644
index 000000000..4b95c563f
--- /dev/null
+++ b/devtools/client/jsonview/test/browser_jsonview_save_json.js
@@ -0,0 +1,38 @@
+/* -*- 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 TEST_JSON_URL = URL_ROOT + "valid_json.json";
+
+let { MockFilePicker } = SpecialPowers;
+
+MockFilePicker.init(window);
+MockFilePicker.returnValue = MockFilePicker.returnCancel;
+
+registerCleanupFunction(function () {
+ MockFilePicker.cleanup();
+});
+
+add_task(function* () {
+ info("Test save JSON started");
+
+ yield addJsonViewTab(TEST_JSON_URL);
+
+ let promise = new Promise((resolve) => {
+ MockFilePicker.showCallback = () => {
+ MockFilePicker.showCallback = null;
+ ok(true, "File picker was opened");
+ resolve();
+ };
+ });
+
+ let browser = gBrowser.selectedBrowser;
+ yield BrowserTestUtils.synthesizeMouseAtCenter(
+ ".jsonPanelBox button.save",
+ {}, browser);
+
+ yield promise;
+});
diff --git a/devtools/client/jsonview/test/browser_jsonview_valid_json.js b/devtools/client/jsonview/test/browser_jsonview_valid_json.js
new file mode 100644
index 000000000..83d0e1088
--- /dev/null
+++ b/devtools/client/jsonview/test/browser_jsonview_valid_json.js
@@ -0,0 +1,33 @@
+/* -*- 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 TEST_JSON_URL = URL_ROOT + "valid_json.json";
+
+add_task(function* () {
+ info("Test valid JSON started");
+
+ let tab = yield addJsonViewTab(TEST_JSON_URL);
+
+ ok(tab.linkedBrowser.contentPrincipal.isNullPrincipal, "Should have null principal");
+
+ let countBefore = yield getElementCount(".jsonPanelBox .treeTable .treeRow");
+ ok(countBefore == 3, "There must be three rows");
+
+ let objectCellCount = yield getElementCount(
+ ".jsonPanelBox .treeTable .objectCell");
+ ok(objectCellCount == 1, "There must be one object cell");
+
+ let objectCellText = yield getElementText(
+ ".jsonPanelBox .treeTable .objectCell");
+ ok(objectCellText == "", "The summary is hidden when object is expanded");
+
+ // Collapsed auto-expanded node.
+ yield clickJsonNode(".jsonPanelBox .treeTable .treeLabel");
+
+ let countAfter = yield getElementCount(".jsonPanelBox .treeTable .treeRow");
+ ok(countAfter == 1, "There must be one row");
+});
diff --git a/devtools/client/jsonview/test/doc_frame_script.js b/devtools/client/jsonview/test/doc_frame_script.js
new file mode 100644
index 000000000..3d19b3433
--- /dev/null
+++ b/devtools/client/jsonview/test/doc_frame_script.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";
+
+/* globals Services, sendAsyncMessage, addMessageListener */
+
+// XXX Some helper API could go to:
+// testing/mochitest/tests/SimpleTest/AsyncContentUtils.js
+// (or at least to share test API in devtools)
+
+// Set up a dummy environment so that EventUtils works. We need to be careful to
+// pass a window object into each EventUtils method we call rather than having
+// it rely on the |window| global.
+let EventUtils = {};
+EventUtils.window = content;
+EventUtils.parent = EventUtils.window;
+EventUtils._EU_Ci = Components.interfaces; // eslint-disable-line
+EventUtils._EU_Cc = Components.classes; // eslint-disable-line
+EventUtils.navigator = content.navigator;
+EventUtils.KeyboardEvent = content.KeyboardEvent;
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochikit/content/tests/SimpleTest/EventUtils.js", EventUtils);
+
+/**
+ * When the JSON View is done rendering it triggers custom event
+ * "JSONViewInitialized", then the Test:TestPageProcessingDone message
+ * will be sent to the parent process for tests to wait for this event
+ * if needed.
+ */
+content.addEventListener("JSONViewInitialized", () => {
+ sendAsyncMessage("Test:JsonView:JSONViewInitialized");
+}, false);
+
+addMessageListener("Test:JsonView:GetElementCount", function (msg) {
+ let {selector} = msg.data;
+ let nodeList = content.document.querySelectorAll(selector);
+ sendAsyncMessage(msg.name, {count: nodeList.length});
+});
+
+addMessageListener("Test:JsonView:GetElementText", function (msg) {
+ let {selector} = msg.data;
+ let element = content.document.querySelector(selector);
+ let text = element ? element.textContent : null;
+ sendAsyncMessage(msg.name, {text: text});
+});
+
+addMessageListener("Test:JsonView:FocusElement", function (msg) {
+ let {selector} = msg.data;
+ let element = content.document.querySelector(selector);
+ if (element) {
+ element.focus();
+ }
+ sendAsyncMessage(msg.name);
+});
+
+addMessageListener("Test:JsonView:SendString", function (msg) {
+ let {selector, str} = msg.data;
+ if (selector) {
+ let element = content.document.querySelector(selector);
+ if (element) {
+ element.focus();
+ }
+ }
+
+ EventUtils.sendString(str, content);
+
+ sendAsyncMessage(msg.name);
+});
+
+addMessageListener("Test:JsonView:WaitForFilter", function (msg) {
+ let firstRow = content.document.querySelector(
+ ".jsonPanelBox .treeTable .treeRow");
+
+ // Check if the filter is already set.
+ if (firstRow.classList.contains("hidden")) {
+ sendAsyncMessage(msg.name);
+ return;
+ }
+
+ // Wait till the first row has 'hidden' class set.
+ let observer = new content.MutationObserver(function (mutations) {
+ for (let i = 0; i < mutations.length; i++) {
+ let mutation = mutations[i];
+ if (mutation.attributeName == "class") {
+ if (firstRow.classList.contains("hidden")) {
+ observer.disconnect();
+ sendAsyncMessage(msg.name);
+ break;
+ }
+ }
+ }
+ });
+
+ observer.observe(firstRow, { attributes: true });
+});
diff --git a/devtools/client/jsonview/test/head.js b/devtools/client/jsonview/test/head.js
new file mode 100644
index 000000000..b71883e67
--- /dev/null
+++ b/devtools/client/jsonview/test/head.js
@@ -0,0 +1,145 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint no-unused-vars: [2, {"vars": "local", "args": "none"}] */
+/* import-globals-from ../../framework/test/shared-head.js */
+/* import-globals-from ../../framework/test/head.js */
+
+"use strict";
+
+// shared-head.js handles imports, constants, and utility functions
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/framework/test/head.js", this);
+
+const JSON_VIEW_PREF = "devtools.jsonview.enabled";
+
+// Enable JSON View for the test
+Services.prefs.setBoolPref(JSON_VIEW_PREF, true);
+
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref(JSON_VIEW_PREF);
+});
+
+// XXX move some API into devtools/framework/test/shared-head.js
+
+/**
+ * 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 tab object when the url is loaded
+ */
+function addJsonViewTab(url) {
+ info("Adding a new JSON tab with URL: '" + url + "'");
+
+ let deferred = promise.defer();
+ addTab(url).then(tab => {
+ let browser = tab.linkedBrowser;
+
+ // Load devtools/shared/frame-script-utils.js
+ getFrameScript();
+
+ // Load frame script with helpers for JSON View tests.
+ let rootDir = getRootDirectory(gTestPath);
+ let frameScriptUrl = rootDir + "doc_frame_script.js";
+ browser.messageManager.loadFrameScript(frameScriptUrl, false);
+
+ // Resolve if the JSONView is fully loaded or wait
+ // for an initialization event.
+ if (content.window.wrappedJSObject.jsonViewInitialized) {
+ deferred.resolve(tab);
+ } else {
+ waitForContentMessage("Test:JsonView:JSONViewInitialized").then(() => {
+ deferred.resolve(tab);
+ });
+ }
+ });
+
+ return deferred.promise;
+}
+
+/**
+ * Expanding a node in the JSON tree
+ */
+function clickJsonNode(selector) {
+ info("Expanding node: '" + selector + "'");
+
+ let browser = gBrowser.selectedBrowser;
+ return BrowserTestUtils.synthesizeMouseAtCenter(selector, {}, browser);
+}
+
+/**
+ * Select JSON View tab (in the content).
+ */
+function selectJsonViewContentTab(name) {
+ info("Selecting tab: '" + name + "'");
+
+ let browser = gBrowser.selectedBrowser;
+ let selector = ".tabs-menu .tabs-menu-item." + name + " a";
+ return BrowserTestUtils.synthesizeMouseAtCenter(selector, {}, browser);
+}
+
+function getElementCount(selector) {
+ info("Get element count: '" + selector + "'");
+
+ let data = {
+ selector: selector
+ };
+
+ return executeInContent("Test:JsonView:GetElementCount", data)
+ .then(result => {
+ return result.count;
+ });
+}
+
+function getElementText(selector) {
+ info("Get element text: '" + selector + "'");
+
+ let data = {
+ selector: selector
+ };
+
+ return executeInContent("Test:JsonView:GetElementText", data)
+ .then(result => {
+ return result.text;
+ });
+}
+
+function focusElement(selector) {
+ info("Focus element: '" + selector + "'");
+
+ let data = {
+ selector: selector
+ };
+
+ return executeInContent("Test:JsonView:FocusElement", data);
+}
+
+/**
+ * Send the string aStr to the focused element.
+ *
+ * For now this method only works for ASCII characters and emulates the shift
+ * key state on US keyboard layout.
+ */
+function sendString(str, selector) {
+ info("Send string: '" + str + "'");
+
+ let data = {
+ selector: selector,
+ str: str
+ };
+
+ return executeInContent("Test:JsonView:SendString", data);
+}
+
+function waitForTime(delay) {
+ let deferred = promise.defer();
+ setTimeout(deferred.resolve, delay);
+ return deferred.promise;
+}
+
+function waitForFilter() {
+ return executeInContent("Test:JsonView:WaitForFilter");
+}
+
+function normalizeNewLines(value) {
+ return value.replace("(\r\n|\n)", "\n");
+}
diff --git a/devtools/client/jsonview/test/invalid_json.json b/devtools/client/jsonview/test/invalid_json.json
new file mode 100644
index 000000000..004e1e203
--- /dev/null
+++ b/devtools/client/jsonview/test/invalid_json.json
@@ -0,0 +1 @@
+{,}
diff --git a/devtools/client/jsonview/test/invalid_json.json^headers^ b/devtools/client/jsonview/test/invalid_json.json^headers^
new file mode 100644
index 000000000..6010bfd18
--- /dev/null
+++ b/devtools/client/jsonview/test/invalid_json.json^headers^
@@ -0,0 +1 @@
+Content-Type: application/json; charset=utf-8
diff --git a/devtools/client/jsonview/test/simple_json.json b/devtools/client/jsonview/test/simple_json.json
new file mode 100644
index 000000000..831dfbcfb
--- /dev/null
+++ b/devtools/client/jsonview/test/simple_json.json
@@ -0,0 +1 @@
+{"name": "value"}
diff --git a/devtools/client/jsonview/test/simple_json.json^headers^ b/devtools/client/jsonview/test/simple_json.json^headers^
new file mode 100644
index 000000000..6010bfd18
--- /dev/null
+++ b/devtools/client/jsonview/test/simple_json.json^headers^
@@ -0,0 +1 @@
+Content-Type: application/json; charset=utf-8
diff --git a/devtools/client/jsonview/test/valid_json.json b/devtools/client/jsonview/test/valid_json.json
new file mode 100644
index 000000000..ca7356ccd
--- /dev/null
+++ b/devtools/client/jsonview/test/valid_json.json
@@ -0,0 +1,6 @@
+{
+ "family": {
+ "father": "John Doe",
+ "mother": "Alice Doe"
+ }
+}
diff --git a/devtools/client/jsonview/test/valid_json.json^headers^ b/devtools/client/jsonview/test/valid_json.json^headers^
new file mode 100644
index 000000000..6010bfd18
--- /dev/null
+++ b/devtools/client/jsonview/test/valid_json.json^headers^
@@ -0,0 +1 @@
+Content-Type: application/json; charset=utf-8
diff --git a/devtools/client/jsonview/utils.js b/devtools/client/jsonview/utils.js
new file mode 100644
index 000000000..a70afdc68
--- /dev/null
+++ b/devtools/client/jsonview/utils.js
@@ -0,0 +1,101 @@
+/* -*- 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 { Cu, Cc, Ci } = require("chrome");
+const Services = require("Services");
+const { getMostRecentBrowserWindow } = require("sdk/window/utils");
+
+const OPEN_FLAGS = {
+ RDONLY: parseInt("0x01", 16),
+ WRONLY: parseInt("0x02", 16),
+ CREATE_FILE: parseInt("0x08", 16),
+ APPEND: parseInt("0x10", 16),
+ TRUNCATE: parseInt("0x20", 16),
+ EXCL: parseInt("0x80", 16)
+};
+
+/**
+ * Open File Save As dialog and let the user to pick proper file location.
+ */
+exports.getTargetFile = function () {
+ let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+
+ let win = getMostRecentBrowserWindow();
+ fp.init(win, null, Ci.nsIFilePicker.modeSave);
+ fp.appendFilter("JSON Files", "*.json; *.jsonp;");
+ fp.appendFilters(Ci.nsIFilePicker.filterText);
+ fp.appendFilters(Ci.nsIFilePicker.filterAll);
+ fp.filterIndex = 0;
+
+ let rv = fp.show();
+ if (rv == Ci.nsIFilePicker.returnOK || rv == Ci.nsIFilePicker.returnReplace) {
+ return fp.file;
+ }
+
+ return null;
+};
+
+/**
+ * Save JSON to a file
+ */
+exports.saveToFile = function (file, jsonString) {
+ let foStream = Cc["@mozilla.org/network/file-output-stream;1"]
+ .createInstance(Ci.nsIFileOutputStream);
+
+ // write, create, truncate
+ let openFlags = OPEN_FLAGS.WRONLY | OPEN_FLAGS.CREATE_FILE |
+ OPEN_FLAGS.TRUNCATE;
+
+ let permFlags = parseInt("0666", 8);
+ foStream.init(file, openFlags, permFlags, 0);
+
+ let converter = Cc["@mozilla.org/intl/converter-output-stream;1"]
+ .createInstance(Ci.nsIConverterOutputStream);
+
+ converter.init(foStream, "UTF-8", 0, 0);
+
+ // The entire jsonString can be huge so, write the data in chunks.
+ let chunkLength = 1024 * 1204;
+ for (let i = 0; i <= jsonString.length; i++) {
+ let data = jsonString.substr(i, chunkLength + 1);
+ if (data) {
+ converter.writeString(data);
+ }
+ i = i + chunkLength;
+ }
+
+ // this closes foStream
+ converter.close();
+};
+
+/**
+ * Get the current theme from preferences.
+ */
+exports.getCurrentTheme = function () {
+ return Services.prefs.getCharPref("devtools.theme");
+};
+
+/**
+ * Export given object into the target window scope.
+ */
+exports.exportIntoContentScope = function (win, obj, defineAs) {
+ let clone = Cu.createObjectIn(win, {
+ defineAs: defineAs
+ });
+
+ let props = Object.getOwnPropertyNames(obj);
+ for (let i = 0; i < props.length; i++) {
+ let propName = props[i];
+ let propValue = obj[propName];
+ if (typeof propValue == "function") {
+ Cu.exportFunction(propValue, clone, {
+ defineAs: propName
+ });
+ }
+ }
+};
diff --git a/devtools/client/jsonview/viewer-config.js b/devtools/client/jsonview/viewer-config.js
new file mode 100644
index 000000000..b5ffbe34d
--- /dev/null
+++ b/devtools/client/jsonview/viewer-config.js
@@ -0,0 +1,39 @@
+/* -*- 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/. */
+/* global requirejs */
+
+"use strict";
+
+/**
+ * RequireJS configuration for JSON Viewer.
+ *
+ * ReactJS library is shared among DevTools. Both, the minified (production)
+ * version and developer versions of the library are available.
+ *
+ * In order to use the developer version you need to specify the following
+ * in your .mozconfig (see also bug 1181646):
+ * ac_add_options --enable-debug-js-modules
+ *
+ * The path mapping uses paths fallback (a feature supported by RequireJS)
+ * See also: http://requirejs.org/docs/api.html#pathsfallbacks
+ *
+ * React module ID is using exactly the same (relative) path as the rest
+ * of the code base, so it's consistent and modules can be easily reused.
+ */
+require.config({
+ baseUrl: ".",
+ paths: {
+ "devtools/client/shared": "resource://devtools/client/shared",
+ "devtools/shared": "resource://devtools/shared",
+ "devtools/client/shared/vendor/react": [
+ "resource://devtools/client/shared/vendor/react-dev",
+ "resource://devtools/client/shared/vendor/react"
+ ],
+ }
+});
+
+// Load the main panel module
+requirejs(["json-viewer"]);
diff --git a/devtools/client/locales/en-US/VariablesView.dtd b/devtools/client/locales/en-US/VariablesView.dtd
new file mode 100644
index 000000000..d9b34d5df
--- /dev/null
+++ b/devtools/client/locales/en-US/VariablesView.dtd
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+
+<!-- LOCALIZATION NOTE : FILE The correct localization of this file might be to
+ - keep it in English, or another language commonly spoken among web developers.
+ - You want to make that choice consistent across the developer tools.
+ - A good criteria is the language in which you'd find the best
+ - documentation on web development on the web. -->
+
+<!ENTITY PropertiesViewWindowTitle "Properties">
+
diff --git a/devtools/client/locales/en-US/aboutdebugging.dtd b/devtools/client/locales/en-US/aboutdebugging.dtd
new file mode 100644
index 000000000..1f38a9443
--- /dev/null
+++ b/devtools/client/locales/en-US/aboutdebugging.dtd
@@ -0,0 +1,5 @@
+<!-- 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/. -->
+
+<!ENTITY aboutDebugging.fullTitle "Debugging with Firefox Developer Tools">
diff --git a/devtools/client/locales/en-US/aboutdebugging.properties b/devtools/client/locales/en-US/aboutdebugging.properties
new file mode 100644
index 000000000..eab9ac7f1
--- /dev/null
+++ b/devtools/client/locales/en-US/aboutdebugging.properties
@@ -0,0 +1,105 @@
+# 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/.
+
+# LOCALIZATION NOTE (debug):
+# This string is displayed as a label of the button that starts
+# debugging a service worker.
+debug = Debug
+
+# LOCALIZATION NOTE (push):
+# This string is displayed as a label of the button that pushes a test payload
+# to a service worker.
+push = Push
+
+# LOCALIZATION NOTE (start):
+# This string is displayed as a label of the button that starts a service worker.
+start = Start
+
+scope = Scope
+unregister = unregister
+
+pushService = Push Service
+
+# LOCALIZATION NOTE (addons):
+# This string is displayed as a header of the about:debugging#addons page.
+addons = Add-ons
+
+# LOCALIZATION NOTE (addonDebugging.label):
+# This string is displayed next to a check box that enables the user to switch
+# addon debugging on/off.
+addonDebugging.label = Enable add-on debugging
+
+# LOCALIZATION NOTE (addonDebugging.tooltip):
+# This string is displayed in a tooltip that appears when hovering over a check
+# box that switches addon debugging on/off.
+addonDebugging.tooltip = Turning this on will allow you to debug add-ons and various other parts of the browser chrome
+
+# LOCALIZATION NOTE (moreInfo):
+# This string is displayed next to addonDebugging.label as a link to a page
+# with documentation.
+moreInfo = more info
+
+# LOCALIZATION NOTE (loadTemporaryAddon):
+# This string is displayed as a label of a button that allows the user to
+# load additional add-ons.
+loadTemporaryAddon = Load Temporary Add-on
+
+# LOCALIZATION NOTE (extensions):
+# This string is displayed as a header above the list of loaded add-ons.
+extensions = Extensions
+
+# LOCALIZATION NOTE (selectAddonFromFile2):
+# This string is displayed as the title of the file picker that appears when
+# the user clicks the 'Load Temporary Add-on' button
+selectAddonFromFile2 = Select Manifest File or Package (.xpi)
+
+# LOCALIZATION NOTE (reload):
+# This string is displayed as a label of the button that reloads a given addon.
+reload = Reload
+
+# LOCALIZATION NOTE (reloadDisabledTooltip):
+# This string is displayed in a tooltip that appears when hovering over a
+# disabled 'reload' button.
+reloadDisabledTooltip = Only temporarily installed add-ons can be reloaded
+
+# LOCALIZATION NOTE (workers):
+# This string is displayed as a header of the about:debugging#workers page.
+workers = Workers
+
+serviceWorkers = Service Workers
+sharedWorkers = Shared Workers
+otherWorkers = Other Workers
+
+# LOCALIZATION NOTE (running):
+# This string is displayed as the state of a service worker in RUNNING state.
+running = Running
+
+# LOCALIZATION NOTE (stopped):
+# This string is displayed as the state of a service worker in STOPPED state.
+stopped = Stopped
+
+# LOCALIZATION NOTE (registering):
+# This string is displayed as the state of a service worker for which no service worker
+# registration could be found yet. Only active registrations are visible from
+# about:debugging, so such service workers are considered as registering.
+registering = Registering
+
+# LOCALIZATION NOTE (tabs):
+# This string is displayed as a header of the about:debugging#tabs page.
+tabs = Tabs
+
+# LOCALIZATION NOTE (pageNotFound):
+# This string is displayed as the main message at any error/invalid page.
+pageNotFound = Page not found
+
+# LOCALIZATION NOTE (doesNotExist):
+# This string is displayed as an error message when navigating to an invalid page
+# %S will be replaced by the name of the page at run-time.
+doesNotExist = #%S does not exist!
+
+# LOCALIZATION NOTE (nothing):
+# This string is displayed when the list of workers is empty.
+nothing = Nothing yet.
+
+configurationIsNotCompatible = Your browser configuration is not compatible with Service Workers
diff --git a/devtools/client/locales/en-US/animationinspector.properties b/devtools/client/locales/en-US/animationinspector.properties
new file mode 100644
index 000000000..a61f07f57
--- /dev/null
+++ b/devtools/client/locales/en-US/animationinspector.properties
@@ -0,0 +1,173 @@
+# 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/.
+
+# LOCALIZATION NOTE These strings are used inside the Animation inspector
+# which is available as a sidebar panel in the Inspector.
+# The correct localization of this file might be to keep it in
+# English, or another language commonly spoken among web developers.
+# You want to make that choice consistent across the developer tools.
+# A good criteria is the language in which you'd find the best
+# documentation on web development on the web.
+
+# LOCALIZATION NOTE (panel.invalidElementSelected):
+# This is the label shown in the panel when an invalid node is currently
+# selected in the inspector (i.e. a non-element node or a node that is not
+# animated).
+panel.invalidElementSelected=No animations were found for the current element.
+
+# LOCALIZATION NOTE (panel.selectElement): This is the label shown in the panel
+# when an invalid node is currently selected in the inspector, to invite the
+# user to select a new node by clicking on the element-picker icon.
+panel.selectElement=Pick another element from the page.
+
+# LOCALIZATION NOTE (panel.allAnimations): This is the label shown at the bottom of
+# the panel, in a toolbar, to let the user know the toolbar applies to all
+# animations, not just the ones applying to the current element.
+panel.allAnimations=All animations
+
+# LOCALIZATION NOTE (player.animationDurationLabel):
+# This string is displayed in each animation player widget. It is the label
+# displayed before the animation duration.
+player.animationDurationLabel=Duration:
+
+# LOCALIZATION NOTE (player.animationDelayLabel):
+# This string is displayed in each animation player widget. It is the label
+# displayed before the animation delay.
+player.animationDelayLabel=Delay:
+
+# LOCALIZATION NOTE (player.animationEndDelayLabel):
+# This string is displayed in each animation player widget. It is the label
+# displayed before the animation endDelay.
+player.animationEndDelayLabel=End delay:
+
+# LOCALIZATION NOTE (player.animationRateLabel):
+# This string is displayed in each animation player widget. It is the label
+# displayed before the animation playback rate.
+player.animationRateLabel=Playback rate:
+
+# LOCALIZATION NOTE (player.animationIterationCountLabel):
+# This string is displayed in each animation player widget. It is the label
+# displayed before the number of times the animation is set to repeat.
+player.animationIterationCountLabel=Repeats:
+
+# LOCALIZATION NOTE (player.infiniteIterationCount):
+# In case the animation repeats infinitely, this string is displayed next to the
+# player.animationIterationCountLabel string, instead of a number.
+player.infiniteIterationCount=&#8734;
+
+# LOCALIZATION NOTE (player.infiniteIterationCountText):
+# See player.infiniteIterationCount for a description of what this is.
+# Unlike player.infiniteIterationCount, this string isn't used in HTML, but in
+# a tooltip.
+player.infiniteIterationCountText=∞
+
+# LOCALIZATION NOTE (player.animationIterationStartLabel):
+# This string is displayed in a tooltip that appears when hovering over
+# animations in the timeline. It is the label displayed before the animation
+# iterationStart value.
+# %1$S will be replaced by the original iteration start value
+# %2$S will be replaced by the actual time of iteration start
+player.animationIterationStartLabel=Iteration start: %1$S (%2$Ss)
+
+# LOCALIZATION NOTE (player.animationEasingLabel):
+# This string is displayed in a tooltip that appears when hovering over
+# animations in the timeline. It is the label displayed before the animation
+# easing value.
+player.animationEasingLabel=Easing:
+
+# LOCALIZATION NOTE (player.animationFillLabel):
+# This string is displayed in a tooltip that appears when hovering over
+# animations in the timeline. It is the label displayed before the animation
+# fill mode value.
+player.animationFillLabel=Fill:
+
+# LOCALIZATION NOTE (player.animationDirectionLabel):
+# This string is displayed in a tooltip that appears when hovering over
+# animations in the timeline. It is the label displayed before the animation
+# direction value.
+player.animationDirectionLabel=Direction:
+
+# LOCALIZATION NOTE (player.timeLabel):
+# This string is displayed in each animation player widget, to indicate either
+# how long (in seconds) the animation lasts, or what is the animation's current
+# time (in seconds too);
+player.timeLabel=%Ss
+
+# LOCALIZATION NOTE (player.playbackRateLabel):
+# This string is displayed in each animation player widget, as the label of
+# drop-down list items that can be used to change the rate at which the
+# animation runs (1× being the default, 2× being twice as fast).
+player.playbackRateLabel=%S×
+
+# LOCALIZATION NOTE (player.runningOnCompositorTooltip):
+# This string is displayed as a tooltip for the icon that indicates that the
+# animation is running on the compositor thread.
+player.runningOnCompositorTooltip=This animation is running on compositor thread
+
+# LOCALIZATION NOTE (player.allPropertiesOnCompositorTooltip):
+# This string is displayed as a tooltip for the icon that indicates that
+# all of animation is running on the compositor thread.
+player.allPropertiesOnCompositorTooltip=All animation properties are optimized
+
+# LOCALIZATION NOTE (player.somePropertiesOnCompositorTooltip):
+# This string is displayed as a tooltip for the icon that indicates that
+# all of animation is not running on the compositor thread.
+player.somePropertiesOnCompositorTooltip=Some animation properties are optimized
+
+# LOCALIZATION NOTE (timeline.rateSelectorTooltip):
+# This string is displayed in the timeline toolbar, as the tooltip of the
+# drop-down list that can be used to change the rate at which the animations
+# run.
+timeline.rateSelectorTooltip=Set the animations playback rates
+
+# LOCALIZATION NOTE (timeline.pauseResumeButtonTooltip):
+# This string is displayed in the timeline toolbar, as the tooltip of the
+# pause/resume button that can be used to pause or resume the animations
+timeline.pausedButtonTooltip=Resume the animations
+
+# LOCALIZATION NOTE (timeline.pauseResumeButtonTooltip):
+# This string is displayed in the timeline toolbar, as the tooltip of the
+# pause/resume button that can be used to pause or resume the animations
+timeline.resumedButtonTooltip=Pause the animations
+
+# LOCALIZATION NOTE (timeline.rewindButtonTooltip):
+# This string is displayed in the timeline toolbar, as the tooltip of the
+# rewind button that can be used to rewind the animations
+timeline.rewindButtonTooltip=Rewind the animations
+
+# LOCALIZATION NOTE (timeline.timeGraduationLabel):
+# This string is displayed at the top of the animation panel, next to each time
+# graduation, to indicate what duration (in milliseconds) this graduation
+# corresponds to.
+timeline.timeGraduationLabel=%Sms
+
+# LOCALIZATION NOTE (timeline.cssanimation.nameLabel):
+# This string is displayed in a tooltip of the animation panel that is shown
+# when hovering over the name of a CSS Animation in the timeline UI.
+# %S will be replaced by the name of the animation at run-time.
+timeline.cssanimation.nameLabel=%S - CSS Animation
+
+# LOCALIZATION NOTE (timeline.csstransition.nameLabel):
+# This string is displayed in a tooltip of the animation panel that is shown
+# when hovering over the name of a CSS Transition in the timeline UI.
+# %S will be replaced by the name of the transition at run-time.
+timeline.csstransition.nameLabel=%S - CSS Transition
+
+# LOCALIZATION NOTE (timeline.scriptanimation.nameLabel):
+# This string is displayed in a tooltip of the animation panel that is shown
+# when hovering over the name of a script-generated animation in the timeline UI.
+# %S will be replaced by the name of the animation at run-time.
+timeline.scriptanimation.nameLabel=%S - Script Animation
+
+# LOCALIZATION NOTE (timeline.scriptanimation.unnamedLabel):
+# This string is displayed in a tooltip of the animation panel that is shown
+# when hovering over an unnamed script-generated animation in the timeline UI.
+timeline.scriptanimation.unnamedLabel=Script Animation
+
+# LOCALIZATION NOTE (timeline.unknown.nameLabel):
+# This string is displayed in a tooltip of the animation panel that is shown
+# when hovering over the name of an unknown animation type in the timeline UI.
+# This can happen if devtools couldn't figure out the type of the animation.
+# %S will be replaced by the name of the transition at run-time.
+timeline.unknown.nameLabel=%S
diff --git a/devtools/client/locales/en-US/app-manager.properties b/devtools/client/locales/en-US/app-manager.properties
new file mode 100644
index 000000000..ab9548ae4
--- /dev/null
+++ b/devtools/client/locales/en-US/app-manager.properties
@@ -0,0 +1,29 @@
+# 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/.
+
+validator.nonExistingFolder=The project folder doesn’t exist
+validator.expectProjectFolder=The project folder ends up being a file
+validator.noManifestFile=A manifest file is required at project root folder, named either ‘manifest.webapp’ for packaged apps or ‘manifest.json’ for add-ons.
+validator.invalidManifestURL=Invalid manifest URL ‘%S’
+# LOCALIZATION NOTE (validator.invalidManifestJSON, validator.noAccessManifestURL):
+# %1$S is the error message, %2$S is the URI of the manifest.
+validator.invalidManifestJSON=The webapp manifest isn’t a valid JSON file: %1$S at: %2$S
+validator.noAccessManifestURL=Unable to read manifest file: %1$S at: %2$S
+# LOCALIZATION NOTE (validator.invalidHostedManifestURL): %1$S is the URI of
+# the manifest, %2$S is the error message.
+validator.invalidHostedManifestURL=Invalid hosted manifest URL ‘%1$S’: %2$S
+validator.invalidProjectType=Unknown project type ‘%S’
+# LOCALIZATION NOTE (validator.missNameManifestProperty, validator.missIconsManifestProperty):
+# don't translate 'icons' and 'name'.
+validator.missNameManifestProperty=Missing mandatory ‘name’ in Manifest.
+validator.missIconsManifestProperty=Missing ‘icons’ in Manifest.
+validator.missIconMarketplace2=app submission to the Marketplace requires a 128px icon
+validator.invalidAppType=Unknown app type: ‘%S’.
+validator.invalidHostedPriviledges=Hosted App can’t be type ‘%S’.
+validator.noCertifiedSupport=‘certified’ apps are not fully supported on the App manager.
+validator.nonAbsoluteLaunchPath=Launch path has to be an absolute path starting with ‘/’: ‘%S’
+validator.accessFailedLaunchPath=Unable to access the app starting document ‘%S’
+# LOCALIZATION NOTE (validator.accessFailedLaunchPathBadHttpCode): %1$S is the URI of
+# the launch document, %2$S is the http error code.
+validator.accessFailedLaunchPathBadHttpCode=Unable to access the app starting document ‘%1$S’, got HTTP code %2$S
diff --git a/devtools/client/locales/en-US/appcacheutils.properties b/devtools/client/locales/en-US/appcacheutils.properties
new file mode 100644
index 000000000..a900731d9
--- /dev/null
+++ b/devtools/client/locales/en-US/appcacheutils.properties
@@ -0,0 +1,119 @@
+# 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/.
+
+# LOCALIZATION NOTE These strings are used inside the Web Console
+# command line which is available from the Web Developer sub-menu
+# -> 'Web Console'.
+# These messages are displayed when an attempt is made to validate a
+# page or a cache manifest using AppCacheUtils.jsm
+
+# The correct localization of this file might be to keep it in
+# English, or another language commonly spoken among web developers.
+# You want to make that choice consistent across the developer tools.
+# A good criteria is the language in which you'd find the best
+# documentation on web development on the web.
+
+# LOCALIZATION NOTE (noManifest): the specified page has no cache manifest.
+noManifest=The specified page has no manifest.
+
+# LOCALIZATION NOTE (notUTF8): the associated cache manifest has a character
+# encoding that is not UTF-8. Parameters: %S is the current encoding.
+notUTF8=Manifest has a character encoding of %S. Manifests must have the utf-8 character encoding.
+
+# LOCALIZATION NOTE (badMimeType): the associated cache manifest has a
+# mimetype that is not text/cache-manifest. Parameters: %S is the current
+# mimetype.
+badMimeType=Manifest has a mimetype of %S. Manifests must have a mimetype of text/cache-manifest.
+
+# LOCALIZATION NOTE (duplicateURI): the associated cache manifest references
+# the same URI from multiple locations. Parameters: %1$S is the URI, %2$S is a
+# list of references to this URI.
+duplicateURI=URI %1$S is referenced in multiple locations. This is not allowed: %2$S.
+
+# LOCALIZATION NOTE (networkBlocksURI, fallbackBlocksURI): the associated
+# cache manifest references the same URI in the NETWORK (or FALLBACK) section
+# as it does in other sections. Parameters: %1$S is the line number, %2$S is
+# the resource name, %3$S is the line number, %4$S is the resource name, %5$S
+# is the section name.
+networkBlocksURI=NETWORK section line %1$S (%2$S) prevents caching of line %3$S (%4$S) in the %5$S section.
+fallbackBlocksURI=FALLBACK section line %1$S (%2$S) prevents caching of line %3$S (%4$S) in the %5$S section.
+
+# LOCALIZATION NOTE (fileChangedButNotManifest): the associated cache manifest
+# references a URI that has a file modified after the cache manifest.
+# Parameters: %1$S is the resource name, %2$S is the cache manifest, %3$S is
+# the line number.
+fileChangedButNotManifest=The file %1$S was modified after %2$S. Unless the text in the manifest file is changed the cached version will be used instead at line %3$S.
+
+# LOCALIZATION NOTE (cacheControlNoStore): the specified page has a header
+# preventing caching or storing information. Parameters: %1$S is the resource
+# name, %2$S is the line number.
+cacheControlNoStore=%1$S has cache-control set to no-store. This will prevent the application cache from storing the file at line %2$S.
+
+# LOCALIZATION NOTE (notAvailable): the specified resource is not available.
+# Parameters: %1$S is the resource name, %2$S is the line number.
+notAvailable=%1$S points to a resource that is not available at line %2$S.
+
+# LOCALIZATION NOTE (invalidURI): it's used when an invalid URI is passed to
+# the appcache.
+invalidURI=The URI passed to AppCacheUtils is invalid.
+
+# LOCALIZATION NOTE (noResults): it's used when a search returns no results.
+noResults=Your search returned no results.
+
+# LOCALIZATION NOTE (cacheDisabled): it's used when the cache is disabled and
+# an attempt is made to view offline data.
+cacheDisabled=Your disk cache is disabled. Please set browser.cache.disk.enable to true in about:config and try again.
+
+# LOCALIZATION NOTE (firstLineMustBeCacheManifest): the associated cache
+# manifest has a first line that is not "CACHE MANIFEST". Parameters: %S is
+# the line number.
+firstLineMustBeCacheManifest=The first line of the manifest must be “CACHE MANIFEST†at line %S.
+
+# LOCALIZATION NOTE (cacheManifestOnlyFirstLine2): the associated cache
+# manifest has "CACHE MANIFEST" on a line other than the first line.
+# Parameters: %S is the line number where "CACHE MANIFEST" appears.
+cacheManifestOnlyFirstLine2=“CACHE MANIFEST†is only valid on the first line but was found at line %S.
+
+# LOCALIZATION NOTE (asteriskInWrongSection2): the associated cache manifest
+# has an asterisk (*) in a section other than the NETWORK section. Parameters:
+# %1$S is the section name, %2$S is the line number.
+asteriskInWrongSection2=Asterisk (*) incorrectly used in the %1$S section at line %2$S. If a line in the NETWORK section contains only a single asterisk character, then any URI not listed in the manifest will be treated as if the URI was listed in the NETWORK section. Otherwise such URIs will be treated as unavailable. Other uses of the * character are prohibited.
+
+# LOCALIZATION NOTE (escapeSpaces): the associated cache manifest has a space
+# in a URI. Spaces must be replaced with %20. Parameters: %S is the line
+# number where this error occurs.
+escapeSpaces=Spaces in URIs need to be replaced with %20 at line %S.
+
+# LOCALIZATION NOTE (slashDotDotSlashBad): the associated cache manifest has a
+# URI containing /../, which is invalid. Parameters: %S is the line number
+# where this error occurs.
+slashDotDotSlashBad=/../ is not a valid URI prefix at line %S.
+
+# LOCALIZATION NOTE (tooManyDotDotSlashes): the associated cache manifest has
+# a URI containing too many ../ operators. Too many of these operators mean
+# that the file would be below the root of the site, which is not possible.
+# Parameters: %S is the line number where this error occurs.
+tooManyDotDotSlashes=Too many dot dot slash operators (../) at line %S.
+
+# LOCALIZATION NOTE (fallbackUseSpaces): the associated cache manifest has a
+# FALLBACK section containing more or less than the standard two URIs
+# separated by a single space. Parameters: %S is the line number where this
+# error occurs.
+fallbackUseSpaces=Only two URIs separated by spaces are allowed in the FALLBACK section at line %S.
+
+# LOCALIZATION NOTE (fallbackAsterisk2): the associated cache manifest has a
+# FALLBACK section that attempts to use an asterisk (*) as a wildcard. In this
+# section the URI is simply a path prefix. Parameters: %S is the line number
+# where this error occurs.
+fallbackAsterisk2=Asterisk (*) incorrectly used in the FALLBACK section at line %S. URIs in the FALLBACK section simply need to match a prefix of the request URI.
+
+# LOCALIZATION NOTE (settingsBadValue): the associated cache manifest has a
+# SETTINGS section containing something other than the valid "prefer-online"
+# or "fast". Parameters: %S is the line number where this error occurs.
+settingsBadValue=The SETTINGS section may only contain a single value, “prefer-online†or “fast†at line %S.
+
+# LOCALIZATION NOTE (invalidSectionName): the associated cache manifest
+# contains an invalid section name. Parameters: %1$S is the section name, %2$S
+# is the line number.
+invalidSectionName=Invalid section name (%1$S) at line %2$S.
diff --git a/devtools/client/locales/en-US/boxmodel.properties b/devtools/client/locales/en-US/boxmodel.properties
new file mode 100644
index 000000000..cea31ff05
--- /dev/null
+++ b/devtools/client/locales/en-US/boxmodel.properties
@@ -0,0 +1,37 @@
+# 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/.
+
+# LOCALIZATION NOTE : FILE This file contains the Layout View strings.
+# The Layout View is a panel displayed in the computed view tab of the Inspector sidebar.
+
+# LOCALIZATION NOTE : FILE The correct localization of this file might be to
+# keep it in English, or another language commonly spoken among web developers.
+# You want to make that choice consistent across the developer tools.
+# A good criteria is the language in which you'd find the best
+# documentation on web development on the web.
+
+# LOCALIZATION NOTE (boxmodel.title) This is the title of the box model panel and is
+# displayed as a label.
+boxmodel.title=Box Model
+
+# LOCALIZATION NOTE (boxmodel.margin) This refers to the margin in the box model and
+# might be displayed as a label or as a tooltip.
+boxmodel.margin=margin
+
+# LOCALIZATION NOTE (boxmodel.border) This refers to the border in the box model and
+# might be displayed as a label or as a tooltip.
+boxmodel.border=border
+
+# LOCALIZATION NOTE (boxmodel.padding) This refers to the padding in the box model and
+# might be displayed as a label or as a tooltip.
+boxmodel.padding=padding
+
+# LOCALIZATION NOTE (boxmodel.content) This refers to the content in the box model and
+# might be displayed as a label or as a tooltip.
+boxmodel.content=content
+
+# LOCALIZATION NOTE: (boxmodel.geometryButton.tooltip) This label is displayed as a
+# tooltip that appears when hovering over the button that allows users to edit the
+# position of an element in the page.
+boxmodel.geometryButton.tooltip=Edit position
diff --git a/devtools/client/locales/en-US/canvasdebugger.dtd b/devtools/client/locales/en-US/canvasdebugger.dtd
new file mode 100644
index 000000000..0682c30ce
--- /dev/null
+++ b/devtools/client/locales/en-US/canvasdebugger.dtd
@@ -0,0 +1,45 @@
+<!-- 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/. -->
+
+<!-- LOCALIZATION NOTE : FILE This file contains the Debugger strings -->
+<!-- LOCALIZATION NOTE : FILE Do not translate commandkey -->
+
+<!-- LOCALIZATION NOTE : FILE The correct localization of this file might be to
+ - keep it in English, or another language commonly spoken among web developers.
+ - You want to make that choice consistent across the developer tools.
+ - A good criteria is the language in which you'd find the best
+ - documentation on web development on the web. -->
+
+<!-- LOCALIZATION NOTE (canvasDebuggerUI.reloadNotice1): This is the label shown
+ - on the button that triggers a page refresh. -->
+<!ENTITY canvasDebuggerUI.reloadNotice1 "Reload">
+
+<!-- LOCALIZATION NOTE (canvasDebuggerUI.reloadNotice2): This is the label shown
+ - along with the button that triggers a page refresh. -->
+<!ENTITY canvasDebuggerUI.reloadNotice2 "the page to be able to debug &lt;canvas&gt; contexts.">
+
+<!-- LOCALIZATION NOTE (canvasDebuggerUI.emptyNotice1/2): This is the label shown
+ - in the call list view when empty. -->
+<!ENTITY canvasDebuggerUI.emptyNotice1 "Click on the">
+<!ENTITY canvasDebuggerUI.emptyNotice2 "button to record an animation frame’s call stack.">
+
+<!-- LOCALIZATION NOTE (canvasDebuggerUI.waitingNotice): This is the label shown
+ - in the call list view while recording a snapshot. -->
+<!ENTITY canvasDebuggerUI.waitingNotice "Recording an animation cycle…">
+
+<!-- LOCALIZATION NOTE (canvasDebuggerUI.recordSnapshot): This string is displayed
+ - on a button that starts a new snapshot. -->
+<!ENTITY canvasDebuggerUI.recordSnapshot.tooltip "Record the next frame in the animation loop.">
+
+<!-- LOCALIZATION NOTE (canvasDebuggerUI.importSnapshot): This string is displayed
+ - on a button that opens a dialog to import a saved snapshot data file. -->
+<!ENTITY canvasDebuggerUI.importSnapshot "Import…">
+
+<!-- LOCALIZATION NOTE (canvasDebuggerUI.clearSnapshots): This string is displayed
+ - on a button that remvoes all the snapshots. -->
+<!ENTITY canvasDebuggerUI.clearSnapshots "Clear">
+
+<!-- LOCALIZATION NOTE (canvasDebuggerUI.searchboxPlaceholder): This string is displayed
+ - as a placeholder of the search box that filters the calls list. -->
+<!ENTITY canvasDebuggerUI.searchboxPlaceholder "Filter calls">
diff --git a/devtools/client/locales/en-US/canvasdebugger.properties b/devtools/client/locales/en-US/canvasdebugger.properties
new file mode 100644
index 000000000..a5d2c6bf1
--- /dev/null
+++ b/devtools/client/locales/en-US/canvasdebugger.properties
@@ -0,0 +1,70 @@
+# 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/.
+
+# LOCALIZATION NOTE These strings are used inside the Canvas Debugger
+# which is available from the Web Developer sub-menu -> 'Canvas'.
+# The correct localization of this file might be to keep it in
+# English, or another language commonly spoken among web developers.
+# You want to make that choice consistent across the developer tools.
+# A good criteria is the language in which you'd find the best
+# documentation on web development on the web.
+
+# LOCALIZATION NOTE (noSnapshotsText): The text to display in the snapshots menu
+# when there are no recorded snapshots yet.
+noSnapshotsText=There are no snapshots yet.
+
+# LOCALIZATION NOTE (snapshotsList.itemLabel):
+# This string is displayed in the snapshots list of the Canvas Debugger,
+# identifying a set of function calls of a recorded animation frame.
+snapshotsList.itemLabel=Snapshot #%S
+
+# LOCALIZATION NOTE (snapshotsList.loadingLabel):
+# This string is displayed in the snapshots list of the Canvas Debugger,
+# for an item that has not finished loading.
+snapshotsList.loadingLabel=Loading…
+
+# LOCALIZATION NOTE (snapshotsList.saveLabel):
+# This string is displayed in the snapshots list of the Canvas Debugger,
+# for saving an item to disk.
+snapshotsList.saveLabel=Save
+
+# LOCALIZATION NOTE (snapshotsList.savingLabel):
+# This string is displayed in the snapshots list of the Canvas Debugger,
+# while saving an item to disk.
+snapshotsList.savingLabel=Saving…
+
+# LOCALIZATION NOTE (snapshotsList.loadedLabel):
+# This string is displayed in the snapshots list of the Canvas Debugger,
+# for an item which was loaded from disk
+snapshotsList.loadedLabel=Loaded from disk
+
+# LOCALIZATION NOTE (snapshotsList.saveDialogTitle):
+# This string is displayed as a title for saving a snapshot to disk.
+snapshotsList.saveDialogTitle=Save animation frame snapshot…
+
+# LOCALIZATION NOTE (snapshotsList.saveDialogJSONFilter):
+# This string is displayed as a filter for saving a snapshot to disk.
+snapshotsList.saveDialogJSONFilter=JSON Files
+
+# LOCALIZATION NOTE (snapshotsList.saveDialogAllFilter):
+# This string is displayed as a filter for saving a snapshot to disk.
+snapshotsList.saveDialogAllFilter=All Files
+
+# LOCALIZATION NOTE (snapshotsList.drawCallsLabel):
+# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
+# This string is displayed in the snapshots list of the Canvas Debugger,
+# as a generic description about how many draw calls were made.
+snapshotsList.drawCallsLabel=#1 draw;#1 draws
+
+# LOCALIZATION NOTE (snapshotsList.functionCallsLabel):
+# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
+# This string is displayed in the snapshots list of the Canvas Debugger,
+# as a generic description about how many function calls were made in total.
+snapshotsList.functionCallsLabel=#1 call;#1 calls
+
+# LOCALIZATION NOTE (recordingTimeoutFailure):
+# This notification alert is displayed when attempting to record a requestAnimationFrame
+# cycle in the Canvas Debugger and no cycles detected. This alerts the user that no
+# loops were found.
+recordingTimeoutFailure=Canvas Debugger could not find a requestAnimationFrame or setTimeout cycle.
diff --git a/devtools/client/locales/en-US/components.properties b/devtools/client/locales/en-US/components.properties
new file mode 100644
index 000000000..7237e40a2
--- /dev/null
+++ b/devtools/client/locales/en-US/components.properties
@@ -0,0 +1,19 @@
+# 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/.
+
+# LOCALIZATION NOTE These strings are used in the shared React components,
+# so files in `devtools/client/shared/components/*`.
+
+# LOCALIZATION NOTE (frame.unknownSource): When we do not know the source filename of
+# a frame, we use this string instead.
+frame.unknownSource=(unknown)
+
+# LOCALIZATION NOTE (viewsourceindebugger): The label for the tooltip when hovering over
+# a source link that links to the debugger.
+# %S represents the URL to match in the debugger.
+frame.viewsourceindebugger=View source in Debugger → %S
+
+# LOCALIZATION NOTE (notificationBox.closeTooltip): The content of a tooltip that
+# appears when hovering over the close button in a notification box.
+notificationBox.closeTooltip=Close this message
diff --git a/devtools/client/locales/en-US/connection-screen.dtd b/devtools/client/locales/en-US/connection-screen.dtd
new file mode 100644
index 000000000..674a408d5
--- /dev/null
+++ b/devtools/client/locales/en-US/connection-screen.dtd
@@ -0,0 +1,30 @@
+<!-- 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/. -->
+
+<!-- LOCALIZATION NOTE : FILE This file contains the Remote Connection strings.
+ - The Remote Connection window can reached from the "connect…" menuitem
+ - in the Web Developer menu.
+ - -->
+
+<!ENTITY title "Connect">
+<!ENTITY header "Connect to remote device">
+<!ENTITY host "Host:">
+<!ENTITY port "Port:">
+<!ENTITY connect "Connect">
+<!ENTITY connecting "Connecting…">
+<!ENTITY availableAddons "Available remote add-ons:">
+<!ENTITY availableTabs "Available remote tabs:">
+<!ENTITY availableProcesses "Available remote processes:">
+<!ENTITY connectionError "Error:">
+<!ENTITY errorTimeout "Error: connection timeout.">
+<!ENTITY errorRefused "Error: connection refused.">
+<!ENTITY errorUnexpected "Unexpected error.">
+
+<!-- LOCALIZATION NOTE (remoteHelp, remoteDocumentation, remoteHelpSuffix):
+these strings will be concatenated in a single label, remoteDocumentation will
+be used as text for a link to MDN. -->
+<!ENTITY remoteHelp "Firefox Developer Tools can debug remote devices (Firefox for Android and Firefox OS, for example). Make sure that you have turned on the ‘Remote debugging’ option in the remote device. For more, see the ">
+<!ENTITY remoteDocumentation "documentation">
+<!ENTITY remoteHelpSuffix ".">
+
diff --git a/devtools/client/locales/en-US/connection-screen.properties b/devtools/client/locales/en-US/connection-screen.properties
new file mode 100644
index 000000000..69928ef08
--- /dev/null
+++ b/devtools/client/locales/en-US/connection-screen.properties
@@ -0,0 +1,9 @@
+# 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/.
+
+# LOCALIZATION NOTE : FILE This file contains the Remote Connection strings.
+# The Remote Connection window can reached from the "connect…" menuitem
+# in the Web Developer menu.
+
+mainProcess=Main Process
diff --git a/devtools/client/locales/en-US/debugger.dtd b/devtools/client/locales/en-US/debugger.dtd
new file mode 100644
index 000000000..f2cc46cda
--- /dev/null
+++ b/devtools/client/locales/en-US/debugger.dtd
@@ -0,0 +1,212 @@
+<!-- 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/. -->
+
+<!-- LOCALIZATION NOTE : FILE This file contains the Debugger strings -->
+<!-- LOCALIZATION NOTE : FILE Do not translate commandkey -->
+
+<!-- LOCALIZATION NOTE : FILE The correct localization of this file might be to
+ - keep it in English, or another language commonly spoken among web developers.
+ - You want to make that choice consistent across the developer tools.
+ - A good criteria is the language in which you'd find the best
+ - documentation on web development on the web. -->
+
+<!-- LOCALIZATION NOTE (debuggerUI.closeButton.tooltip): This is the tooltip for
+ - the button that closes the debugger UI. -->
+<!ENTITY debuggerUI.closeButton.tooltip "Close">
+
+<!-- LOCALIZATION NOTE (debuggerUI.panesButton.tooltip): This is the tooltip for
+ - the button that toggles the panes visible or hidden in the debugger UI. -->
+<!ENTITY debuggerUI.panesButton.tooltip "Toggle panes">
+
+<!-- LOCALIZATION NOTE (debuggerUI.blackBoxMessage.label): This is the message
+ - displayed to users when they select a black boxed source from the sources
+ - list in the debugger. -->
+<!ENTITY debuggerUI.blackBoxMessage.label "This source is black boxed: its breakpoints are disabled, and stepping skips through it.">
+
+<!-- LOCALIZATION NOTE (debuggerUI.blackBoxMessage.unBlackBoxButton): This is
+ - the text displayed in the button to stop black boxing the currently selected
+ - source. -->
+<!ENTITY debuggerUI.blackBoxMessage.unBlackBoxButton "Stop black boxing this source">
+
+<!-- LOCALIZATION NOTE (debuggerUI.optsButton.tooltip): This is the tooltip for
+ - the button that opens up an options context menu for the debugger UI. -->
+<!ENTITY debuggerUI.optsButton.tooltip "Debugger Options">
+
+<!-- LOCALIZATION NOTE (debuggerUI.sources.blackBoxTooltip): This is the tooltip
+ - for the button that black boxes the selected source. -->
+<!ENTITY debuggerUI.sources.blackBoxTooltip "Toggle Black Boxing">
+
+<!-- LOCALIZATION NOTE (debuggerUI.sources.prettyPrint): This is the tooltip for the
+ - button that pretty prints the selected source. -->
+<!ENTITY debuggerUI.sources.prettyPrint "Prettify Source">
+
+<!-- LOCALIZATION NOTE (debuggerUI.autoPrettyPrint): This is the label for the
+ - checkbox that toggles auto pretty print. -->
+<!ENTITY debuggerUI.autoPrettyPrint "Auto Prettify Minified Sources">
+<!ENTITY debuggerUI.autoPrettyPrint.accesskey "P">
+
+<!-- LOCALIZATION NOTE (debuggerUI.sources.toggleBreakpoints): This is the tooltip for the
+ - button that toggles all breakpoints for all sources. -->
+<!ENTITY debuggerUI.sources.toggleBreakpoints "Enable/disable all breakpoints">
+
+<!-- LOCALIZATION NOTE (debuggerUI.clearButton): This is the label for
+ - the button that clears the collected tracing data in the tracing tab. -->
+<!ENTITY debuggerUI.clearButton "Clear">
+
+<!-- LOCALIZATION NOTE (debuggerUI.clearButton.tooltip): This is the tooltip for
+ - the button that clears the collected tracing data in the tracing tab. -->
+<!ENTITY debuggerUI.clearButton.tooltip "Clear the collected traces">
+
+<!-- LOCALIZATION NOTE (debuggerUI.pauseExceptions): This is the label for the
+ - checkbox that toggles pausing on exceptions. -->
+<!ENTITY debuggerUI.pauseExceptions "Pause on Exceptions">
+<!ENTITY debuggerUI.pauseExceptions.accesskey "E">
+
+<!-- LOCALIZATION NOTE (debuggerUI.ignoreCaughtExceptions): This is the label for the
+ - checkbox that toggles ignoring caught exceptions. -->
+<!ENTITY debuggerUI.ignoreCaughtExceptions "Ignore Caught Exceptions">
+<!ENTITY debuggerUI.ignoreCaughtExceptions.accesskey "C">
+
+<!-- LOCALIZATION NOTE (debuggerUI.showPanesOnInit): This is the label for the
+ - checkbox that toggles visibility of panes when opening the debugger. -->
+<!ENTITY debuggerUI.showPanesOnInit "Show Panes on Startup">
+<!ENTITY debuggerUI.showPanesOnInit.accesskey "S">
+
+<!-- LOCALIZATION NOTE (debuggerUI.showVarsFilter): This is the label for the
+ - checkbox that toggles visibility of a designated variables filter box. -->
+<!ENTITY debuggerUI.showVarsFilter "Show Variables Filter Box">
+<!ENTITY debuggerUI.showVarsFilter.accesskey "V">
+
+<!-- LOCALIZATION NOTE (debuggerUI.showOnlyEnum): This is the label for the
+ - checkbox that toggles visibility of hidden (non-enumerable) variables and
+ - properties in stack views. The "enumerable" flag is a state of a property
+ - defined in JavaScript. When in doubt, leave untranslated. -->
+<!ENTITY debuggerUI.showOnlyEnum "Show Only Enumerable Properties">
+<!ENTITY debuggerUI.showOnlyEnum.accesskey "P">
+
+<!-- LOCALIZATION NOTE (debuggerUI.showOriginalSource): This is the label for
+ - the checkbox that toggles the display of original or sourcemap-derived
+ - sources. -->
+<!ENTITY debuggerUI.showOriginalSource "Show Original Sources">
+<!ENTITY debuggerUI.showOriginalSource.accesskey "O">
+
+<!-- LOCALIZATION NOTE (debuggerUI.autoBlackBox): This is the label for
+ - the checkbox that toggles whether sources that we suspect are minified are
+ - automatically black boxed or not. -->
+<!ENTITY debuggerUI.autoBlackBox "Automatically Black Box Minified Sources">
+<!ENTITY debuggerUI.autoBlackBox.accesskey "B">
+
+<!-- LOCALIZATION NOTE (debuggerUI.searchPanelOperators): This is the text that
+ - appears in the filter panel popup as a header for the operators part. -->
+<!ENTITY debuggerUI.searchPanelOperators "Operators:">
+
+<!-- LOCALIZATION NOTE (debuggerUI.searchFile): This is the text that appears
+ - in the source editor's context menu for the scripts search operation. -->
+<!ENTITY debuggerUI.searchFile "Filter Scripts">
+<!ENTITY debuggerUI.searchFile.key "P">
+<!ENTITY debuggerUI.searchFile.altkey "O">
+<!ENTITY debuggerUI.searchFile.accesskey "P">
+
+<!-- LOCALIZATION NOTE (debuggerUI.searchGlobal): This is the text that appears
+ - in the source editor's context menu for the global search operation. -->
+<!ENTITY debuggerUI.searchGlobal "Search in All Files">
+<!ENTITY debuggerUI.searchGlobal.key "F">
+<!ENTITY debuggerUI.searchGlobal.accesskey "F">
+
+<!-- LOCALIZATION NOTE (debuggerUI.searchFunction): This is the text that appears
+ - in the source editor's context menu for the function search operation. -->
+<!ENTITY debuggerUI.searchFunction "Search for Function Definition">
+<!ENTITY debuggerUI.searchFunction.key "D">
+<!ENTITY debuggerUI.searchFunction.accesskey "D">
+
+<!-- LOCALIZATION NOTE (debuggerUI.searchToken): This is the text that appears
+ - in the source editor's context menu for the token search operation. -->
+<!ENTITY debuggerUI.searchToken "Find">
+<!ENTITY debuggerUI.searchToken.key "F">
+<!ENTITY debuggerUI.searchToken.accesskey "F">
+
+<!-- LOCALIZATION NOTE (debuggerUI.searchLine): This is the text that appears
+ - in the source editor's context menu for the line search operation. -->
+<!ENTITY debuggerUI.searchGoToLine "Go to Line…">
+<!ENTITY debuggerUI.searchGoToLine.key "L">
+<!ENTITY debuggerUI.searchGoToLine.accesskey "L">
+
+<!-- LOCALIZATION NOTE (debuggerUI.searchVariable): This is the text that appears
+ - in the source editor's context menu for the variables search operation. -->
+<!ENTITY debuggerUI.searchVariable "Filter Variables">
+<!ENTITY debuggerUI.searchVariable.key "V">
+<!ENTITY debuggerUI.searchVariable.accesskey "V">
+
+<!-- LOCALIZATION NOTE (debuggerUI.focusVariables): This is the text that appears
+ - in the source editor's context menu for the variables focus operation. -->
+<!ENTITY debuggerUI.focusVariables "Focus Variables Tree">
+<!ENTITY debuggerUI.focusVariables.key "V">
+<!ENTITY debuggerUI.focusVariables.accesskey "V">
+
+<!-- LOCALIZATION NOTE (debuggerUI.condBreakPanelTitle): This is the text that
+ - appears in the conditional breakpoint panel popup as a description. -->
+<!ENTITY debuggerUI.condBreakPanelTitle "This breakpoint will stop execution only if the following expression is true">
+
+<!-- LOCALIZATION NOTE (debuggerUI.seMenuBreak): This is the text that
+ - appears in the source editor context menu for adding a breakpoint. -->
+<!ENTITY debuggerUI.seMenuBreak "Add Breakpoint">
+<!ENTITY debuggerUI.seMenuBreak.key "B">
+
+<!-- LOCALIZATION NOTE (debuggerUI.seMenuCondBreak): This is the text that
+ - appears in the source editor context menu for adding a conditional
+ - breakpoint. -->
+<!ENTITY debuggerUI.seMenuCondBreak "Add Conditional Breakpoint">
+<!ENTITY debuggerUI.seMenuCondBreak.key "B">
+
+<!-- LOCALIZATION NOTE (debuggerUI.seMenuBreak): This is the text that
+ - appears in the source editor context menu for editing a breakpoint. -->
+<!ENTITY debuggerUI.seEditMenuCondBreak "Edit Conditional Breakpoint">
+<!ENTITY debuggerUI.seEditMenuCondBreak.key "B">
+
+<!-- LOCALIZATION NOTE (debuggerUI.tabs.*): This is the text that
+ - appears in the debugger's side pane tabs. -->
+<!ENTITY debuggerUI.tabs.workers "Workers">
+<!ENTITY debuggerUI.tabs.sources "Sources">
+<!ENTITY debuggerUI.tabs.traces "Traces">
+<!ENTITY debuggerUI.tabs.callstack "Call Stack">
+<!ENTITY debuggerUI.tabs.variables "Variables">
+<!ENTITY debuggerUI.tabs.events "Events">
+
+<!-- LOCALIZATION NOTE (debuggerUI.seMenuAddWatch): This is the text that
+ - appears in the source editor context menu for adding an expression. -->
+<!ENTITY debuggerUI.seMenuAddWatch "Selection to Watch Expression">
+<!ENTITY debuggerUI.seMenuAddWatch.key "E">
+
+<!-- LOCALIZATION NOTE (debuggerUI.addWatch): This is the text that
+ - appears in the watch expressions context menu for adding an expression. -->
+<!ENTITY debuggerUI.addWatch "Add Watch Expression">
+<!ENTITY debuggerUI.addWatch.accesskey "E">
+
+<!-- LOCALIZATION NOTE (debuggerUI.removeWatch): This is the text that
+ - appears in the watch expressions context menu for removing all expressions. -->
+<!ENTITY debuggerUI.removeAllWatch "Remove All Watch Expressions">
+<!ENTITY debuggerUI.removeAllWatch.key "E">
+<!ENTITY debuggerUI.removeAllWatch.accesskey "E">
+
+<!-- LOCALIZATION NOTE (debuggerUI.stepping): These are the keycodes that
+ - control the stepping commands in the debugger (continue, step over,
+ - step in and step out). -->
+<!ENTITY debuggerUI.stepping.resume1 "VK_F8">
+<!ENTITY debuggerUI.stepping.stepOver1 "VK_F10">
+<!ENTITY debuggerUI.stepping.stepIn1 "VK_F11">
+<!ENTITY debuggerUI.stepping.stepOut1 "VK_F11">
+
+<!-- LOCALIZATION NOTE (debuggerUI.context.newTab): This is the label
+ - for the Open in New Tab menu item displayed in the context menu of the
+ - debugger sources side menu. This should be the same as
+ - netmonitorUI.context.newTab -->
+<!ENTITY debuggerUI.context.newTab "Open in New Tab">
+<!ENTITY debuggerUI.context.newTab.accesskey "O">
+
+<!-- LOCALIZATION NOTE (debuggerUI.context.copyUrl): This is the label displayed
+ - on the context menu that copies the selected request's url. This should be
+ - the same as netmonitorUI.context.copyUrl -->
+<!ENTITY debuggerUI.context.copyUrl "Copy URL">
+<!ENTITY debuggerUI.context.copyUrl.accesskey "C">
+<!ENTITY debuggerUI.context.copyUrl.key "C">
diff --git a/devtools/client/locales/en-US/debugger.properties b/devtools/client/locales/en-US/debugger.properties
new file mode 100644
index 000000000..1107ad4dc
--- /dev/null
+++ b/devtools/client/locales/en-US/debugger.properties
@@ -0,0 +1,383 @@
+# 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/.
+
+# LOCALIZATION NOTE These strings are used inside the Debugger
+# which is available from the Web Developer sub-menu -> 'Debugger'.
+# The correct localization of this file might be to keep it in
+# English, or another language commonly spoken among web developers.
+# You want to make that choice consistent across the developer tools.
+# A good criteria is the language in which you'd find the best
+# documentation on web development on the web.
+
+# LOCALIZATION NOTE (collapsePanes): This is the tooltip for the button
+# that collapses the left and right panes in the debugger UI.
+collapsePanes=Collapse panes
+
+# LOCALIZATION NOTE (expandPanes): This is the tooltip for the button
+# that expands the left and right panes in the debugger UI.
+expandPanes=Expand panes
+
+# LOCALIZATION NOTE (pauseButtonTooltip): The tooltip that is displayed for the pause
+# button when the debugger is in a running state.
+pauseButtonTooltip=Click to pause (%S)
+
+# LOCALIZATION NOTE (pausePendingButtonTooltip): The tooltip that is displayed for
+# the pause button after it's been clicked but before the next JavaScript to run.
+pausePendingButtonTooltip=Waiting for next execution
+
+# LOCALIZATION NOTE (resumeButtonTooltip): The label that is displayed on the pause
+# button when the debugger is in a paused state.
+resumeButtonTooltip=Click to resume (%S)
+
+# LOCALIZATION NOTE (stepOverTooltip): The label that is displayed on the
+# button that steps over a function call.
+stepOverTooltip=Step Over (%S)
+
+# LOCALIZATION NOTE (stepInTooltip): The label that is displayed on the
+# button that steps into a function call.
+stepInTooltip=Step In (%S)
+
+# LOCALIZATION NOTE (stepOutTooltip): The label that is displayed on the
+# button that steps out of a function call.
+stepOutTooltip=Step Out (%S)
+
+# LOCALIZATION NOTE (noWorkersText): The text to display in the workers list
+# when there are no workers.
+noWorkersText=This page has no workers.
+
+# LOCALIZATION NOTE (noSourcesText): The text to display in the sources list
+# when there are no sources.
+noSourcesText=This page has no sources.
+
+# LOCALIZATION NOTE (noEventListenersText): The text to display in the events tab
+# when there are no events.
+noEventListenersText=No event listeners to display
+
+# LOCALIZATION NOTE (noStackFramesText): The text to display in the call stack tab
+# when there are no stack frames.
+noStackFramesText=No stack frames to display
+
+# LOCALIZATION NOTE (eventCheckboxTooltip): The tooltip text to display when
+# the user hovers over the checkbox used to toggle an event breakpoint.
+eventCheckboxTooltip=Toggle breaking on this event
+
+# LOCALIZATION NOTE (eventOnSelector): The text to display in the events tab
+# for every event item, between the event type and event selector.
+eventOnSelector=on
+
+# LOCALIZATION NOTE (eventInSource): The text to display in the events tab
+# for every event item, between the event selector and listener's owner source.
+eventInSource=in
+
+# LOCALIZATION NOTE (eventNodes): The text to display in the events tab when
+# an event is listened on more than one target node.
+eventNodes=%S nodes
+
+# LOCALIZATION NOTE (eventNative): The text to display in the events tab when
+# a listener is added from plugins, thus getting translated to native code.
+eventNative=[native code]
+
+# LOCALIZATION NOTE (*Events): The text to display in the events tab for
+# each group of sub-level event entries.
+animationEvents=Animation
+audioEvents=Audio
+batteryEvents=Battery
+clipboardEvents=Clipboard
+compositionEvents=Composition
+deviceEvents=Device
+displayEvents=Display
+dragAndDropEvents=Drag and Drop
+gamepadEvents=Gamepad
+indexedDBEvents=IndexedDB
+interactionEvents=Interaction
+keyboardEvents=Keyboard
+mediaEvents=HTML5 Media
+mouseEvents=Mouse
+mutationEvents=Mutation
+navigationEvents=Navigation
+pointerLockEvents=Pointer Lock
+sensorEvents=Sensor
+storageEvents=Storage
+timeEvents=Time
+touchEvents=Touch
+otherEvents=Other
+
+# LOCALIZATION NOTE (blackBoxCheckboxTooltip): The tooltip text to display when
+# the user hovers over the checkbox used to toggle black boxing its associated
+# source.
+blackBoxCheckboxTooltip=Toggle black boxing
+
+# LOCALIZATION NOTE (noMatchingStringsText): The text to display in the
+# global search results when there are no matching strings after filtering.
+noMatchingStringsText=No matches found
+
+# LOCALIZATION NOTE (emptySearchText): This is the text that appears in the
+# filter text box when it is empty and the scripts container is selected.
+emptySearchText=Search scripts (%S)
+
+# LOCALIZATION NOTE (emptyVariablesFilterText): This is the text that
+# appears in the filter text box for the variables view container.
+emptyVariablesFilterText=Filter variables
+
+# LOCALIZATION NOTE (emptyPropertiesFilterText): This is the text that
+# appears in the filter text box for the editor's variables view bubble.
+emptyPropertiesFilterText=Filter properties
+
+# LOCALIZATION NOTE (searchPanelFilter): This is the text that appears in the
+# filter panel popup for the filter scripts operation.
+searchPanelFilter=Filter scripts (%S)
+
+# LOCALIZATION NOTE (searchPanelGlobal): This is the text that appears in the
+# filter panel popup for the global search operation.
+searchPanelGlobal=Search in all files (%S)
+
+# LOCALIZATION NOTE (searchPanelFunction): This is the text that appears in the
+# filter panel popup for the function search operation.
+searchPanelFunction=Search for function definition (%S)
+
+# LOCALIZATION NOTE (searchPanelToken): This is the text that appears in the
+# filter panel popup for the token search operation.
+searchPanelToken=Find in this file (%S)
+
+# LOCALIZATION NOTE (searchPanelGoToLine): This is the text that appears in the
+# filter panel popup for the line search operation.
+searchPanelGoToLine=Go to line (%S)
+
+# LOCALIZATION NOTE (searchPanelVariable): This is the text that appears in the
+# filter panel popup for the variables search operation.
+searchPanelVariable=Filter variables (%S)
+
+# LOCALIZATION NOTE (breakpointMenuItem): The text for all the elements that
+# are displayed in the breakpoints menu item popup.
+breakpointMenuItem.setConditional=Configure conditional breakpoint
+breakpointMenuItem.enableSelf=Enable breakpoint
+breakpointMenuItem.disableSelf=Disable breakpoint
+breakpointMenuItem.deleteSelf=Remove breakpoint
+breakpointMenuItem.enableOthers=Enable others
+breakpointMenuItem.disableOthers=Disable others
+breakpointMenuItem.deleteOthers=Remove others
+breakpointMenuItem.enableAll=Enable all breakpoints
+breakpointMenuItem.disableAll=Disable all breakpoints
+breakpointMenuItem.deleteAll=Remove all breakpoints
+
+# LOCALIZATION NOTE (breakpoints.header): Breakpoints right sidebar pane header.
+breakpoints.header=Breakpoints
+
+# LOCALIZATION NOTE (callStack.header): Call Stack right sidebar pane header.
+callStack.header=Call Stack
+
+# LOCALIZATION NOTE (callStack.notPaused): Call Stack right sidebar pane
+# message when not paused.
+callStack.notPaused=Not Paused
+
+# LOCALIZATION NOTE (callStack.collapse): Call Stack right sidebar pane
+# message to hide some of the frames that are shown.
+callStack.collapse=Collapse Rows
+
+# LOCALIZATION NOTE (callStack.expand): Call Stack right sidebar pane
+# message to show more of the frames.
+callStack.expand=Expand Rows
+
+# LOCALIZATION NOTE (editor.searchResults): Editor Search bar message
+# for the summarizing the selected search result. e.g. 5 of 10 results.
+editor.searchResults=%d of %d results
+
+# LOCALIZATION NOTE (editor.noResults): Editor Search bar message
+# for when no results found.
+editor.noResults=no results
+
+# LOCALIZATION NOTE(editor.addBreakpoint): Editor gutter context menu item
+# for adding a breakpoint on a line.
+editor.addBreakpoint=Add Breakpoint
+
+# LOCALIZATION NOTE(editor.removeBreakpoint): Editor gutter context menu item
+# for removing a breakpoint on a line.
+editor.removeBreakpoint=Remove Breakpoint
+
+# LOCALIZATION NOTE(editor.editBreakpoint): Editor gutter context menu item
+# for setting a breakpoint condition on a line.
+editor.editBreakpoint=Edit Breakpoint
+
+# LOCALIZATION NOTE(editor.addConditionalBreakpoint): Editor gutter context
+# menu item for adding a breakpoint condition on a line.
+editor.addConditionalBreakpoint=Add Conditional Breakpoint
+
+# LOCALIZATION NOTE (sourceTabs.closeTab): Editor source tab context menu item
+# for closing the selected tab below the mouse.
+sourceTabs.closeTab=Close tab
+
+# LOCALIZATION NOTE (sourceTabs.closeOtherTabs): Editor source tab context menu item
+# for closing the other tabs.
+sourceTabs.closeOtherTabs=Close others
+
+# LOCALIZATION NOTE (sourceTabs.closeTabsToRight): Editor source tab context menu item
+# for closing the tabs to the right of the selected tab.
+sourceTabs.closeTabsToRight=Close tabs to the right
+
+# LOCALIZATION NOTE (sourceTabs.closeAllTabs): Editor source tab context menu item
+# for closing all tabs.
+sourceTabs.closeAllTabs=Close all tabs
+
+# LOCALIZATION NOTE (scopes.header): Scopes right sidebar pane header.
+scopes.header=Scopes
+
+# LOCALIZATION NOTE (scopes.notAvailable): Scopes right sidebar pane message
+# for when the debugger is paused, but there isn't pause data.
+scopes.notAvailable=Scopes Unavailable
+
+# LOCALIZATION NOTE (scopes.notPaused): Scopes right sidebar pane message
+# for when the debugger is not paused.
+scopes.notPaused=Not Paused
+
+# LOCALIZATION NOTE (sources.header): Sources left sidebar header
+sources.header=Sources
+
+# LOCALIZATION NOTE (sources.search): Sources left sidebar prompt
+# e.g. Cmd+P to search. On a mac, we use the command unicode character.
+# On windows, it's ctrl.
+sources.search=%S to search
+
+# LOCALIZATION NOTE (watchExpressions.header): Watch Expressions right sidebar
+# pane header.
+watchExpressions.header=Watch Expressions
+
+# LOCALIZATION NOTE (welcome.search): The center pane welcome panel's
+# search prompt. e.g. cmd+p to search for files. On windows, it's ctrl, on
+# a mac we use the unicode character.
+welcome.search=%S to search for files
+
+# LOCALIZATION NOTE (sourceSearch.search): The center pane Source Search
+# prompt for searching for files.
+sourceSearch.search=Search…
+
+# LOCALIZATION NOTE (sourceSearch.noResults): The center pane Source Search
+# message when the query did not match any of the sources.
+sourceSearch.noResults=No files matching %S found
+
+# LOCALIZATION NOTE (ignoreExceptions): The pause on exceptions button tooltip
+# when the debugger will not pause on exceptions.
+ignoreExceptions=Ignore exceptions. Click to pause on uncaught exceptions
+
+# LOCALIZATION NOTE (pauseOnUncaughtExceptions): The pause on exceptions button
+# tooltip when the debugger will pause on uncaught exceptions.
+pauseOnUncaughtExceptions=Pause on uncaught exceptions. Click to pause on all exceptions
+
+# LOCALIZATION NOTE (pauseOnExceptions): The pause on exceptions button tooltip
+# when the debugger will pause on all exceptions.
+pauseOnExceptions=Pause on all exceptions. Click to ignore exceptions
+
+# LOCALIZATION NOTE (loadingText): The text that is displayed in the script
+# editor when the loading process has started but there is no file to display
+# yet.
+loadingText=Loading\u2026
+
+# LOCALIZATION NOTE (errorLoadingText2): The text that is displayed in the debugger
+# viewer when there is an error loading a file
+errorLoadingText2=Error loading this URL: %S
+
+# LOCALIZATION NOTE (addWatchExpressionText): The text that is displayed in the
+# watch expressions list to add a new item.
+addWatchExpressionText=Add watch expression
+
+# LOCALIZATION NOTE (addWatchExpressionButton): The button that is displayed in the
+# variables view popup.
+addWatchExpressionButton=Watch
+
+# LOCALIZATION NOTE (emptyVariablesText): The text that is displayed in the
+# variables pane when there are no variables to display.
+emptyVariablesText=No variables to display
+
+# LOCALIZATION NOTE (scopeLabel): The text that is displayed in the variables
+# pane as a header for each variable scope (e.g. "Global scope, "With scope",
+# etc.).
+scopeLabel=%S scope
+
+# LOCALIZATION NOTE (watchExpressionsScopeLabel): The name of the watch
+# expressions scope. This text is displayed in the variables pane as a header for
+# the watch expressions scope.
+watchExpressionsScopeLabel=Watch expressions
+
+# LOCALIZATION NOTE (globalScopeLabel): The name of the global scope. This text
+# is added to scopeLabel and displayed in the variables pane as a header for
+# the global scope.
+globalScopeLabel=Global
+
+# LOCALIZATION NOTE (variablesViewErrorStacktrace): This is the text that is
+# shown before the stack trace in an error.
+variablesViewErrorStacktrace=Stack trace:
+
+# LOCALIZATION NOTE (variablesViewMoreObjects): the text that is displayed
+# when you have an object preview that does not show all of the elements. At the end of the list
+# you see "N more..." in the web console output.
+# This is a semi-colon list of plural forms.
+# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
+# #1 number of remaining items in the object
+# example: 3 more…
+variablesViewMoreObjects=#1 more…;#1 more…
+
+# LOCALIZATION NOTE (variablesEditableNameTooltip): The text that is displayed
+# in the variables list on an item with an editable name.
+variablesEditableNameTooltip=Double click to edit
+
+# LOCALIZATION NOTE (variablesEditableValueTooltip): The text that is displayed
+# in the variables list on an item with an editable value.
+variablesEditableValueTooltip=Click to change value
+
+# LOCALIZATION NOTE (variablesCloseButtonTooltip): The text that is displayed
+# in the variables list on an item which can be removed.
+variablesCloseButtonTooltip=Click to remove
+
+# LOCALIZATION NOTE (variablesEditButtonTooltip): The text that is displayed
+# in the variables list on a getter or setter which can be edited.
+variablesEditButtonTooltip=Click to set value
+
+# LOCALIZATION NOTE (variablesEditableValueTooltip): The text that is displayed
+# in a tooltip on the "open in inspector" button in the the variables list for a
+# DOMNode item.
+variablesDomNodeValueTooltip=Click to select the node in the inspector
+
+# LOCALIZATION NOTE (configurable|...|Tooltip): The text that is displayed
+# in the variables list on certain variables or properties as tooltips.
+# Expanations of what these represent can be found at the following links:
+# https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
+# https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/isExtensible
+# https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/isFrozen
+# https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/isSealed
+# It's probably best to keep these in English.
+configurableTooltip=configurable
+enumerableTooltip=enumerable
+writableTooltip=writable
+frozenTooltip=frozen
+sealedTooltip=sealed
+extensibleTooltip=extensible
+overriddenTooltip=overridden
+WebIDLTooltip=WebIDL
+
+# LOCALIZATION NOTE (variablesSeparatorLabel): The text that is displayed
+# in the variables list as a separator between the name and value.
+variablesSeparatorLabel=:
+
+# LOCALIZATION NOTE (watchExpressionsSeparatorLabel2): The text that is displayed
+# in the watch expressions list as a separator between the code and evaluation.
+watchExpressionsSeparatorLabel2=\u0020→
+
+# LOCALIZATION NOTE (functionSearchSeparatorLabel): The text that is displayed
+# in the functions search panel as a separator between function's inferred name
+# and its real name (if available).
+functionSearchSeparatorLabel=â†
+
+# LOCALIZATION NOTE (resumptionOrderPanelTitle): This is the text that appears
+# as a description in the notification panel popup, when multiple debuggers are
+# open in separate tabs and the user tries to resume them in the wrong order.
+# The substitution parameter is the URL of the last paused window that must be
+# resumed first.
+resumptionOrderPanelTitle=There are one or more paused debuggers. Please resume the most-recently paused debugger first at: %S
+
+variablesViewOptimizedOut=(optimized away)
+variablesViewUninitialized=(uninitialized)
+variablesViewMissingArgs=(unavailable)
+
+anonymousSourcesLabel=Anonymous Sources
+
+experimental=This is an experimental feature
diff --git a/devtools/client/locales/en-US/device.properties b/devtools/client/locales/en-US/device.properties
new file mode 100644
index 000000000..2e4219709
--- /dev/null
+++ b/devtools/client/locales/en-US/device.properties
@@ -0,0 +1,20 @@
+# 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/.
+
+# LOCALIZATION NOTE These strings are used inside Device Emulation developer
+# tools. The correct localization of this file might be to keep it in English,
+# or another language commonly spoken among web developers. You want to make
+# that choice consistent across the developer tools. A good criteria is the
+# language in which you'd find the best documentation on web development on the
+# web.
+
+# LOCALIZATION NOTE:
+# These strings are category names in a list of devices that a user can choose
+# to simulate (e.g. "ZTE Open C", "VIA Vixen", "720p HD Television", etc).
+device.phones=Phones
+device.tablets=Tablets
+device.laptops=Laptops
+device.televisions=TVs
+device.consoles=Gaming consoles
+device.watches=Watches
diff --git a/devtools/client/locales/en-US/dom.properties b/devtools/client/locales/en-US/dom.properties
new file mode 100644
index 000000000..3c09d4e41
--- /dev/null
+++ b/devtools/client/locales/en-US/dom.properties
@@ -0,0 +1,19 @@
+# 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/.
+
+# LOCALIZATION NOTE These strings are used inside the DOM panel
+# which is available from the Web Developer sub-menu -> 'DOM'.
+# The correct localization of this file might be to keep it in
+# English, or another language commonly spoken among web developers.
+# You want to make that choice consistent across the developer tools.
+# A good criteria is the language in which you'd find the best
+# documentation on web development on the web.
+
+# LOCALIZATION NOTE (dom.filterDOMPanel): A placeholder text used for
+# DOM panel search box.
+dom.filterDOMPanel=Filter DOM Panel
+
+# LOCALIZATION NOTE (dom.refresh): A label for Refresh button in
+# DOM panel toolbar
+dom.refresh=Refresh \ No newline at end of file
diff --git a/devtools/client/locales/en-US/eyedropper.properties b/devtools/client/locales/en-US/eyedropper.properties
new file mode 100644
index 000000000..0f320ab37
--- /dev/null
+++ b/devtools/client/locales/en-US/eyedropper.properties
@@ -0,0 +1,14 @@
+# 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/.
+
+# LOCALIZATION NOTE These strings are used in the Eyedropper color tool.
+# LOCALIZATION NOTE The correct localization of this file might be to keep it
+# in English, or another language commonly spoken among web developers.
+# You want to make that choice consistent across the developer tools.
+# A good criteria is the language in which you'd find the best documentation
+# on web development on the web.
+
+# LOCALIZATION NOTE (colorValue.copied): This text is displayed when the user selects a
+# color with the eyedropper and it's copied to the clipboard.
+colorValue.copied=copied
diff --git a/devtools/client/locales/en-US/filterwidget.properties b/devtools/client/locales/en-US/filterwidget.properties
new file mode 100644
index 000000000..ddd2f2665
--- /dev/null
+++ b/devtools/client/locales/en-US/filterwidget.properties
@@ -0,0 +1,61 @@
+# 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/.
+
+# LOCALIZATION NOTE These strings are used in the CSS Filter Editor Widget
+# which can be found in a tooltip that appears in the Rule View when clicking
+# on a filter swatch displayed next to CSS declarations like 'filter: blur(2px)'.
+
+# LOCALIZATION NOTE (emptyFilterList):
+# This string is displayed when filter's list is empty
+# (no filter specified / all removed)
+emptyFilterList=No filter specified
+
+# LOCALIZATION NOTE (emptyPresetList):
+# This string is displayed when preset's list is empty
+emptyPresetList=You don’t have any saved presets. \
+You can store filter presets by choosing a name and saving them. \
+Presets are quickly accessible and you can re-use them with ease.
+
+# LOCALIZATION NOTE (addUsingList):
+# This string is displayed under [emptyFilterList] when filter's
+# list is empty, guiding user to add a filter using the list below it
+addUsingList=Add a filter using the list below
+
+# LOCALIZATION NOTE (dropShadowPlaceholder):
+# This string is used as a placeholder for drop-shadow's input
+# in the filter list (shown when <input> is empty)
+dropShadowPlaceholder=x y radius color
+
+# LOCALIZATION NOTE (dragHandleTooltipText):
+# This string is used as a tooltip text (shown on mouse hover) on the
+# drag handles of filters which are used to re-order filters
+dragHandleTooltipText=Drag up or down to re-order filter
+
+# LOCALIZATION NOTE (labelDragTooltipText):
+# This string is used as a tooltip text (shown on mouse hover) on the
+# filters' labels which can be dragged left/right to increase/decrease
+# the filter's value (like photoshop)
+labelDragTooltipText=Drag left or right to decrease or increase the value
+
+# LOCALIZATION NOTE (filterListSelectPlaceholder):
+# This string is used as a preview option in the list of possible filters
+# <select>
+filterListSelectPlaceholder=Select a Filter
+
+# LOCALIZATION NOTE (addNewFilterButton):
+# This string is displayed on a button used to add new filters
+addNewFilterButton=Add
+
+# LOCALIZATION NOTE (newPresetPlaceholder):
+# This string is used as a placeholder in the list of presets which is used to
+# save a new preset
+newPresetPlaceholder=Preset Name
+
+# LOCALIZATION NOTE (savePresetButton):
+# This string is displayed on a button used to save a new preset
+savePresetButton=Save
+
+# LOCALIZATION NOTE(presetsToggleButton):
+# This string is used in a button which toggles the presets list
+presetsToggleButton=Presets
diff --git a/devtools/client/locales/en-US/font-inspector.properties b/devtools/client/locales/en-US/font-inspector.properties
new file mode 100644
index 000000000..6b1f3bafa
--- /dev/null
+++ b/devtools/client/locales/en-US/font-inspector.properties
@@ -0,0 +1,29 @@
+# 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/.
+
+# LOCALIZATION NOTE This file contains the Font Inspector strings.
+# The Font Inspector is a panel accessible in the Inspector sidebar.
+
+# LOCALIZATION NOTE (fontinspector.seeAll) This is the label of a link that will show all
+# the fonts used in the page, instead of the ones related to the inspected element.
+fontinspector.seeAll=Show all fonts used
+
+# LOCALIZATION NOTE (fontinspector.seeAll.tooltip) see fontinspector.seeAll.
+fontinspector.seeAll.tooltip=See all the fonts used in the page
+
+# LOCALIZATION NOTE (fontinspector.usedAs) This label introduces the name used to refer to
+# the font in a stylesheet.
+fontinspector.usedAs=Used as:
+
+# LOCALIZATION NOTE (fontinspector.system) This label indicates that the font is a local
+# system font.
+fontinspector.system=system
+
+# LOCALIZATION NOTE (fontinspector.remote) This label indicates that the font is a remote
+# font.
+fontinspector.remote=remote
+
+# LOCALIZATION NOTE (previewHint):
+# This is the label shown as the placeholder in font inspector preview text box.
+fontinspector.previewText=Preview Text
diff --git a/devtools/client/locales/en-US/graphs.properties b/devtools/client/locales/en-US/graphs.properties
new file mode 100644
index 000000000..ddc881258
--- /dev/null
+++ b/devtools/client/locales/en-US/graphs.properties
@@ -0,0 +1,24 @@
+# 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/.
+
+# LOCALIZATION NOTE These strings are used inside the Performance Tools
+# which is available from the Web Developer sub-menu -> 'Performance'.
+# The correct localization of this file might be to keep it in
+# English, or another language commonly spoken among web developers.
+# You want to make that choice consistent across the developer tools.
+# A good criteria is the language in which you'd find the best
+# documentation on web development on the web. These strings
+# are specifically for marker names in the performance tool.
+
+# LOCALIZATION NOTE (graphs.label.average):
+# This string is displayed on graphs when showing an average.
+graphs.label.average=avg
+
+# LOCALIZATION NOTE (graphs.label.minimum):
+# This string is displayed on graphs when showing a minimum.
+graphs.label.minimum=min
+
+# LOCALIZATION NOTE (graphs.label.maximum):
+# This string is displayed on graphs when showing a maximum.
+graphs.label.maximum=max
diff --git a/devtools/client/locales/en-US/har.properties b/devtools/client/locales/en-US/har.properties
new file mode 100644
index 000000000..8fcb41871
--- /dev/null
+++ b/devtools/client/locales/en-US/har.properties
@@ -0,0 +1,22 @@
+# 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/.
+
+# LOCALIZATION NOTE These strings are used inside the Network Monitor
+# which is available from the Web Developer sub-menu -> 'Network Monitor'.
+# The correct localization of this file might be to keep it in
+# English, or another language commonly spoken among web developers.
+# You want to make that choice consistent across the developer tools.
+# A good criteria is the language in which you'd find the best
+# documentation on web development on the web.
+
+# LOCALIZATION NOTE (har.responseBodyNotIncluded): A label used within
+# HAR file explaining that HTTP response bodies are not includes
+# in exported data.
+har.responseBodyNotIncluded=Response bodies are not included.
+
+# LOCALIZATION NOTE (har.responseBodyNotIncluded): A label used within
+# HAR file explaining that HTTP request bodies are not includes
+# in exported data.
+har.requestBodyNotIncluded=Request bodies are not included.
+
diff --git a/devtools/client/locales/en-US/inspector.properties b/devtools/client/locales/en-US/inspector.properties
new file mode 100644
index 000000000..4f4829678
--- /dev/null
+++ b/devtools/client/locales/en-US/inspector.properties
@@ -0,0 +1,364 @@
+# 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/.
+
+# LOCALIZATION NOTE These strings are used inside the Inspector
+# which is available from the Web Developer sub-menu -> 'Inspect'.
+#
+# The correct localization of this file might be to keep it in
+# English, or another language commonly spoken among web developers.
+# You want to make that choice consistent across the developer tools.
+# A good criteria is the language in which you'd find the best
+# documentation on web development on the web.
+
+breadcrumbs.siblings=Siblings
+
+# LOCALIZATION NOTE (debuggerPausedWarning): Used in the Inspector tool, when
+# the user switch to the inspector when the debugger is paused.
+debuggerPausedWarning.message=Debugger is paused. Some features like mouse selection will not work.
+
+# LOCALIZATION NOTE (nodeMenu.tooltiptext)
+# This menu appears in the Infobar (on top of the highlighted node) once
+# the node is selected.
+nodeMenu.tooltiptext=Node operations
+
+inspector.panelLabel.markupView=Markup View
+
+# LOCALIZATION NOTE (markupView.more.showing)
+# When there are too many nodes to load at once, we will offer to
+# show all the nodes.
+markupView.more.showing=Some nodes were hidden.
+
+# LOCALIZATION NOTE (markupView.more.showAll2): Semi-colon list of plural forms.
+# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
+markupView.more.showAll2=Show one more node;Show all #1 nodes
+
+# LOCALIZATION NOTE (markupView.whitespaceOnly)
+# Used in a tooltip that appears when the user hovers over whitespace-only text nodes in
+# the inspector.
+markupView.whitespaceOnly=Whitespace-only text node: %S
+
+#LOCALIZATION NOTE: Used in the image preview tooltip when the image could not be loaded
+previewTooltip.image.brokenImage=Could not load the image
+
+# LOCALIZATION NOTE: Used in color picker tooltip when the eyedropper is disabled for
+# non-HTML documents
+eyedropper.disabled.title=Unavailable in non-HTML documents
+
+#LOCALIZATION NOTE: Used in the image preview tooltip when the image could not be loaded
+eventsTooltip.openInDebugger=Open in Debugger
+
+# LOCALIZATION NOTE (docsTooltip.visitMDN): Shown in the tooltip that displays
+# help from MDN. This is a link to the complete MDN documentation page.
+docsTooltip.visitMDN=Visit MDN page
+
+# LOCALIZATION NOTE (docsTooltip.visitMDN): Shown in the docs tooltip when the MDN page
+# could not be loaded (for example, because of a connectivity problem).
+docsTooltip.loadDocsError=Could not load docs page.
+
+# LOCALIZATION NOTE (inspector.collapsePane): This is the tooltip for the button
+# that collapses the right panel (rules, computed, box-model, etc...) in the
+# inspector UI.
+inspector.collapsePane=Collapse pane
+
+# LOCALIZATION NOTE (inspector.expandPane): This is the tooltip for the button
+# that expands the right panel (rules, computed, box-model, etc...) in the
+# inspector UI.
+inspector.expandPane=Expand pane
+
+# LOCALIZATION NOTE (inspector.searchResultsCount): This is the label that
+# will show up next to the inspector search box. %1$S is the current result
+# index and %2$S is the total number of search results. For example: "3 of 9".
+# This won't be visible until the search box is updated in Bug 835896.
+inspector.searchResultsCount2=%1$S of %2$S
+
+# LOCALIZATION NOTE (inspector.searchResultsNone): This is the label that
+# will show up next to the inspector search box when no matches were found
+# for the given string.
+# This won't be visible until the search box is updated in Bug 835896.
+inspector.searchResultsNone=No matches
+
+# LOCALIZATION NOTE (inspector.menu.openUrlInNewTab.label): This is the label of
+# a menu item in the inspector contextual-menu that appears when the user right-
+# clicks on the attribute of a node in the inspector that is a URL, and that
+# allows to open that URL in a new tab.
+inspector.menu.openUrlInNewTab.label=Open Link in New Tab
+
+# LOCALIZATION NOTE (inspector.menu.copyUrlToClipboard.label): This is the label
+# of a menu item in the inspector contextual-menu that appears when the user
+# right-clicks on the attribute of a node in the inspector that is a URL, and
+# that allows to copy that URL in the clipboard.
+inspector.menu.copyUrlToClipboard.label=Copy Link Address
+
+# LOCALIZATION NOTE (inspector.menu.selectElement.label): This is the label of a
+# menu item in the inspector contextual-menu that appears when the user right-
+# clicks on the attribute of a node in the inspector that is the ID of another
+# element in the DOM (like with <label for="input-id">), and that allows to
+# select that element in the inspector.
+inspector.menu.selectElement.label=Select Element #%S
+
+# LOCALIZATION NOTE (inspectorEditAttribute.label): This is the label of a
+# sub-menu "Attribute" in the inspector contextual-menu that appears
+# when the user right-clicks on the node in the inspector, and that allows
+# to edit an attribute on this node.
+inspectorEditAttribute.label=Edit Attribute %S
+inspectorEditAttribute.accesskey=E
+
+# LOCALIZATION NOTE (inspectorRemoveAttribute.label): This is the label of a
+# sub-menu "Attribute" in the inspector contextual-menu that appears
+# when the user right-clicks on the attribute of a node in the inspector,
+# and that allows to remove this attribute.
+inspectorRemoveAttribute.label=Remove Attribute %S
+inspectorRemoveAttribute.accesskey=R
+
+# LOCALIZATION NOTE (inspector.nodePreview.selectNodeLabel):
+# This string is displayed in a tooltip that is shown when hovering over a DOM
+# node preview (e.g. something like "div#foo.bar").
+# DOM node previews can be displayed in places like the animation-inspector, the
+# console or the object inspector.
+# The tooltip invites the user to click on the node in order to select it in the
+# inspector panel.
+inspector.nodePreview.selectNodeLabel=Click to select this node in the Inspector
+
+# LOCALIZATION NOTE (inspector.nodePreview.highlightNodeLabel):
+# This string is displayed in a tooltip that is shown when hovering over a the
+# inspector icon displayed next to a DOM node preview (e.g. next to something
+# like "div#foo.bar").
+# DOM node previews can be displayed in places like the animation-inspector, the
+# console or the object inspector.
+# The tooltip invites the user to click on the icon in order to highlight the
+# node in the page.
+inspector.nodePreview.highlightNodeLabel=Click to highlight this node in the page
+
+# LOCALIZATION NOTE (inspectorHTMLEdit.label): This is the label shown
+# in the inspector contextual-menu for the item that lets users edit the
+# (outer) HTML of the current node
+inspectorHTMLEdit.label=Edit As HTML
+inspectorHTMLEdit.accesskey=E
+
+# LOCALIZATION NOTE (inspectorCopyInnerHTML.label): This is the label shown
+# in the inspector contextual-menu for the item that lets users copy the
+# inner HTML of the current node
+inspectorCopyInnerHTML.label=Inner HTML
+inspectorCopyInnerHTML.accesskey=I
+
+# LOCALIZATION NOTE (inspectorCopyOuterHTML.label): This is the label shown
+# in the inspector contextual-menu for the item that lets users copy the
+# outer HTML of the current node
+inspectorCopyOuterHTML.label=Outer HTML
+inspectorCopyOuterHTML.accesskey=O
+
+# LOCALIZATION NOTE (inspectorCopyCSSSelector.label): This is the label
+# shown in the inspector contextual-menu for the item that lets users copy
+# the CSS Selector of the current node
+inspectorCopyCSSSelector.label=CSS Selector
+inspectorCopyCSSSelector.accesskey=S
+
+# LOCALIZATION NOTE (inspectorPasteOuterHTML.label): This is the label shown
+# in the inspector contextual-menu for the item that lets users paste outer
+# HTML in the current node
+inspectorPasteOuterHTML.label=Outer HTML
+inspectorPasteOuterHTML.accesskey=O
+
+# LOCALIZATION NOTE (inspectorPasteInnerHTML.label): This is the label shown
+# in the inspector contextual-menu for the item that lets users paste inner
+# HTML in the current node
+inspectorPasteInnerHTML.label=Inner HTML
+inspectorPasteInnerHTML.accesskey=I
+
+# LOCALIZATION NOTE (inspectorHTMLPasteBefore.label): This is the label shown
+# in the inspector contextual-menu for the item that lets users paste
+# the HTML before the current node
+inspectorHTMLPasteBefore.label=Before
+inspectorHTMLPasteBefore.accesskey=B
+
+# LOCALIZATION NOTE (inspectorHTMLPasteAfter.label): This is the label shown
+# in the inspector contextual-menu for the item that lets users paste
+# the HTML after the current node
+inspectorHTMLPasteAfter.label=After
+inspectorHTMLPasteAfter.accesskey=A
+
+# LOCALIZATION NOTE (inspectorHTMLPasteFirstChild.label): This is the label
+# shown in the inspector contextual-menu for the item that lets users paste
+# the HTML as the first child the current node
+inspectorHTMLPasteFirstChild.label=As First Child
+inspectorHTMLPasteFirstChild.accesskey=F
+
+# LOCALIZATION NOTE (inspectorHTMLPasteLastChild.label): This is the label
+# shown in the inspector contextual-menu for the item that lets users paste
+# the HTML as the last child the current node
+inspectorHTMLPasteLastChild.label=As Last Child
+inspectorHTMLPasteLastChild.accesskey=L
+
+# LOCALIZATION NOTE (inspectorScrollNodeIntoView.label): This is the label
+# shown in the inspector contextual-menu for the item that lets users scroll
+# the current node into view
+inspectorScrollNodeIntoView.label=Scroll Into View
+inspectorScrollNodeIntoView.accesskey=S
+
+# LOCALIZATION NOTE (inspectorHTMLDelete.label): This is the label shown in
+# the inspector contextual-menu for the item that lets users delete the
+# current node
+inspectorHTMLDelete.label=Delete Node
+inspectorHTMLDelete.accesskey=D
+
+# LOCALIZATION NOTE (inspectorAttributesSubmenu.label): This is the label
+# shown in the inspector contextual-menu for the sub-menu of the other
+# attribute items, which allow to:
+# - add new attribute
+# - edit attribute
+# - remove attribute
+inspectorAttributesSubmenu.label=Attributes
+inspectorAttributesSubmenu.accesskey=A
+
+# LOCALIZATION NOTE (inspectorAddAttribute.label): This is the label shown in
+# the inspector contextual-menu for the item that lets users add attribute
+# to current node
+inspectorAddAttribute.label=Add Attribute
+inspectorAddAttribute.accesskey=A
+
+# LOCALIZATION NOTE (inspectorSearchHTML.label3): This is the label that is
+# shown as the placeholder for the markup view search in the inspector.
+inspectorSearchHTML.label3=Search HTML
+
+# LOCALIZATION NOTE (inspectorImageDataUri.label): This is the label
+# shown in the inspector contextual-menu for the item that lets users copy
+# the URL embedding the image data encoded in Base 64 (what we name
+# here Image Data URL). For more information:
+# https://developer.mozilla.org/en-US/docs/Web/HTTP/data_URIs
+inspectorImageDataUri.label=Image Data-URL
+
+# LOCALIZATION NOTE (inspectorShowDOMProperties.label): This is the label
+# shown in the inspector contextual-menu for the item that lets users see
+# the DOM properties of the current node. When triggered, this item
+# opens the split Console and displays the properties in its side panel.
+inspectorShowDOMProperties.label=Show DOM Properties
+
+# LOCALIZATION NOTE (inspectorUseInConsole.label): This is the label
+# shown in the inspector contextual-menu for the item that outputs a
+# variable for the current node to the console. When triggered,
+# this item opens the split Console.
+inspectorUseInConsole.label=Use in Console
+
+# LOCALIZATION NOTE (inspectorExpandNode.label): This is the label
+# shown in the inspector contextual-menu for recursively expanding
+# mark-up elements
+inspectorExpandNode.label=Expand All
+
+# LOCALIZATION NOTE (inspectorCollapseNode.label): This is the label
+# shown in the inspector contextual-menu for recursively collapsing
+# mark-up elements
+inspectorCollapseNode.label=Collapse
+
+# LOCALIZATION NOTE (inspectorScreenshotNode.label): This is the label
+# shown in the inspector contextual-menu for the item that lets users take
+# a screenshot of the currently selected node.
+inspectorScreenshotNode.label=Screenshot Node
+
+# LOCALIZATION NOTE (inspectorDuplicateNode.label): This is the label
+# shown in the inspector contextual-menu for the item that lets users
+# duplicate the currently selected node.
+inspectorDuplicateNode.label=Duplicate Node
+
+# LOCALIZATION NOTE (inspectorAddNode.label): This is the label shown in
+# the inspector toolbar for the button that lets users add elements to the
+# DOM (as children of the currently selected element).
+inspectorAddNode.label=Create New Node
+inspectorAddNode.accesskey=C
+
+# LOCALIZATION NOTE (inspectorCopyHTMLSubmenu.label): This is the label
+# shown in the inspector contextual-menu for the sub-menu of the other
+# copy items, which allow to:
+# - Copy Inner HTML
+# - Copy Outer HTML
+# - Copy Unique selector
+# - Copy Image data URI
+inspectorCopyHTMLSubmenu.label=Copy
+
+# LOCALIZATION NOTE (inspectorPasteHTMLSubmenu.label): This is the label
+# shown in the inspector contextual-menu for the sub-menu of the other
+# paste items, which allow to:
+# - Paste Inner HTML
+# - Paste Outer HTML
+# - Before
+# - After
+# - As First Child
+# - As Last Child
+inspectorPasteHTMLSubmenu.label=Paste
+
+# LOCALIZATION NOTE (inspector.searchHTML.key):
+# Key shortcut used to focus the DOM element search box on top-right corner of
+# the markup view
+inspector.searchHTML.key=CmdOrCtrl+F
+
+# LOCALIZATION NOTE (markupView.hide.key):
+# Key shortcut used to hide the selected node in the markup view.
+markupView.hide.key=h
+
+# LOCALIZATION NOTE (markupView.edit.key):
+# Key shortcut used to hide the selected node in the markup view.
+markupView.edit.key=F2
+
+# LOCALIZATION NOTE (markupView.scrollInto.key):
+# Key shortcut used to scroll the webpage in order to ensure the selected node
+# is visible
+markupView.scrollInto.key=s
+
+# LOCALIZATION NOTE (inspector.sidebar.fontInspectorTitle):
+# This is the title shown in a tab in the side panel of the Inspector panel
+# that corresponds to the tool displaying the list of fonts used in the page.
+inspector.sidebar.fontInspectorTitle=Fonts
+
+# LOCALIZATION NOTE (inspector.sidebar.ruleViewTitle):
+# This is the title shown in a tab in the side panel of the Inspector panel
+# that corresponds to the tool displaying the list of CSS rules used
+# in the page.
+inspector.sidebar.ruleViewTitle=Rules
+
+# LOCALIZATION NOTE (inspector.sidebar.computedViewTitle):
+# This is the title shown in a tab in the side panel of the Inspector panel
+# that corresponds to the tool displaying the list of computed CSS values
+# used in the page.
+inspector.sidebar.computedViewTitle=Computed
+
+# LOCALIZATION NOTE (inspector.sidebar.computedViewTitle):
+# This is the title shown in a tab in the side panel of the Inspector panel
+# that corresponds to the tool displaying layout information defined in the page.
+inspector.sidebar.layoutViewTitle=Layout
+
+# LOCALIZATION NOTE (inspector.sidebar.animationInspectorTitle):
+# This is the title shown in a tab in the side panel of the Inspector panel
+# that corresponds to the tool displaying animations defined in the page.
+inspector.sidebar.animationInspectorTitle=Animations
+
+# LOCALIZATION NOTE (inspector.eyedropper.label): A string displayed as the tooltip of
+# a button in the inspector which toggles the Eyedropper tool
+inspector.eyedropper.label=Grab a color from the page
+
+# LOCALIZATION NOTE (inspector.breadcrumbs.label): A string visible only to a screen reader and
+# is used to label (using aria-label attribute) a container for inspector breadcrumbs
+inspector.breadcrumbs.label=Breadcrumbs
+
+# LOCALIZATION NOTE (inspector.browserStyles.label): This is the label for the checkbox
+# that specifies whether the styles that are not from the user's stylesheet should be
+# displayed or not.
+inspector.browserStyles.label=Browser styles
+
+# LOCALIZATION NOTE (inspector.filterStyles.placeholder): This is the placeholder that
+# goes in the search box when no search term has been entered.
+inspector.filterStyles.placeholder=Filter Styles
+
+# LOCALIZATION NOTE (inspector.addRule.tooltip): This is the tooltip shown when
+# hovering the `Add new rule` button in the rules view toolbar. This should
+# match ruleView.contextmenu.addNewRule in styleinspector.properties
+inspector.addRule.tooltip=Add new rule
+
+# LOCALIZATION NOTE (inspector.togglePseudo.tooltip): This is the tooltip
+# shown when hovering over the `Toggle Pseudo Class Panel` button in the
+# rule view toolbar.
+inspector.togglePseudo.tooltip=Toggle pseudo-classes
+
+# LOCALIZATION NOTE (inspector.noProperties): In the case where there are no CSS
+# properties to display e.g. due to search criteria this message is
+# displayed.
+inspector.noProperties=No CSS properties found.
diff --git a/devtools/client/locales/en-US/jit-optimizations.properties b/devtools/client/locales/en-US/jit-optimizations.properties
new file mode 100644
index 000000000..092d30b79
--- /dev/null
+++ b/devtools/client/locales/en-US/jit-optimizations.properties
@@ -0,0 +1,35 @@
+# 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/.
+
+# LOCALIZATION NOTE These strings are used within the JIT tools
+# in the Performance Tools which is available from the Web Developer
+# sub-menu -> 'Performance' The correct localization of this file might
+# be to keep it in English, or another language commonly spoken among
+# web developers. You want to make that choice consistent across the
+# developer tools. A good criteria is the language in which you'd find the best
+# documentation on web development on the web.
+
+# LOCALIZATION NOTE (jit.title):
+# This string is displayed in the header of the JIT Optimizations view.
+jit.title=JIT Optimizations
+
+# LOCALIZATION NOTE (jit.optimizationFailure):
+# This string is displayed in a tooltip when no JIT optimizations were detected.
+jit.optimizationFailure=Optimization failed
+
+# LOCALIZATION NOTE (jit.samples):
+# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
+# This string is displayed for the unit representing the number of times a
+# frame is sampled.
+# "#1" represents the number of samples
+# example: 30 samples
+jit.samples=#1 sample;#1 samples
+
+# LOCALIZATION NOTE (jit.types):
+# This string is displayed for the group of Ion Types in the optimizations view.
+jit.types=Types
+
+# LOCALIZATION NOTE (jit.attempts):
+# This string is displayed for the group of optimization attempts in the optimizations view.
+jit.attempts=Attempts
diff --git a/devtools/client/locales/en-US/jsonview.properties b/devtools/client/locales/en-US/jsonview.properties
new file mode 100644
index 000000000..5a876a652
--- /dev/null
+++ b/devtools/client/locales/en-US/jsonview.properties
@@ -0,0 +1,49 @@
+# 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/.
+
+# LOCALIZATION NOTE These strings are used in the JSON View tool
+# that is used to inspect application/json document types loaded
+# in the browser.
+
+# LOCALIZATION NOTE The correct localization of this file might be to keep it
+# in English, or another language commonly spoken among web developers.
+# You want to make that choice consistent across the developer tools.
+# A good criteria is the language in which you'd find the best documentation
+# on web development on the web.
+
+# LOCALIZATION NOTE (jsonViewer.tab.JSON, jsonViewer.tab.RawData,
+# jsonViewer.tab.Headers): Label for a panel tab.
+jsonViewer.tab.JSON=JSON
+jsonViewer.tab.RawData=Raw Data
+jsonViewer.tab.Headers=Headers
+
+# LOCALIZATION NOTE (jsonViewer.responseHeaders, jsonViewer.requestHeaders):
+# Label for header groups within the 'Headers' panel.
+jsonViewer.responseHeaders=Response Headers
+jsonViewer.requestHeaders=Request Headers
+
+# LOCALIZATION NOTE (jsonViewer.Save): Label for save command
+jsonViewer.Save=Save
+
+# LOCALIZATION NOTE (jsonViewer.Copy): Label for clipboard copy command
+jsonViewer.Copy=Copy
+
+# LOCALIZATION NOTE (jsonViewer.ExpandAll): Label for expanding all nodes
+jsonViewer.ExpandAll=Expand All
+
+# LOCALIZATION NOTE (jsonViewer.PrettyPrint): Label for JSON
+# pretty print action button.
+jsonViewer.PrettyPrint=Pretty Print
+
+# LOCALIZATION NOTE (jsonViewer.reps.more): Label used in arrays
+# that have more items than displayed.
+jsonViewer.reps.more=more…
+
+# LOCALIZATION NOTE (jsonViewer.filterJSON): Label used in search box
+# at the top right cornder of the JSON Viewer.
+jsonViewer.filterJSON=Filter JSON
+
+# LOCALIZATION NOTE (jsonViewer.reps.reference): Label used for cycle
+# references in an array.
+jsonViewer.reps.reference=Cycle Reference
diff --git a/devtools/client/locales/en-US/layout.properties b/devtools/client/locales/en-US/layout.properties
new file mode 100644
index 000000000..af93881c2
--- /dev/null
+++ b/devtools/client/locales/en-US/layout.properties
@@ -0,0 +1,15 @@
+# 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/.
+
+# LOCALIZATION NOTE This file contains the Layout Inspector strings.
+# The Layout Inspector is a panel accessible in the Inspector sidebar.
+# The Layout Inspector may need to be enabled in about:config by setting
+# devtools.layoutview.enabled to true.
+
+# LOCALIZATION NOTE (layout.header): The accordion header for the CSS Grid pane.
+layout.header=Grid
+
+# LOCALIZATION NOTE (layout.noGrids): In the case where there are no CSS grid
+# containers to display.
+layout.noGrids=No grids
diff --git a/devtools/client/locales/en-US/markers.properties b/devtools/client/locales/en-US/markers.properties
new file mode 100644
index 000000000..11fe9bd7f
--- /dev/null
+++ b/devtools/client/locales/en-US/markers.properties
@@ -0,0 +1,174 @@
+# 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/.
+
+# LOCALIZATION NOTE These strings are used inside the Performance Tools
+# which is available from the Web Developer sub-menu -> 'Performance'.
+# The correct localization of this file might be to keep it in
+# English, or another language commonly spoken among web developers.
+# You want to make that choice consistent across the developer tools.
+# A good criteria is the language in which you'd find the best
+# documentation on web development on the web. These strings
+# are specifically for marker names in the performance tool.
+
+# LOCALIZATION NOTE (marker.label.*):
+# These strings are displayed in the Performance Tool waterfall, identifying markers.
+# We want to use the same wording as Google Chrome when appropriate.
+marker.label.styles=Recalculate Style
+marker.label.reflow=Layout
+marker.label.paint=Paint
+marker.label.composite=Composite Layers
+marker.label.compositeForwardTransaction=Composite Request Sent
+marker.label.javascript=Function Call
+marker.label.parseHTML=Parse HTML
+marker.label.parseXML=Parse XML
+marker.label.domevent=DOM Event
+marker.label.consoleTime=Console
+marker.label.garbageCollection2=Garbage Collection
+marker.label.garbageCollection.incremental=Incremental GC
+marker.label.garbageCollection.nonIncremental=Non-incremental GC
+marker.label.minorGC=Minor GC
+marker.label.cycleCollection=Cycle Collection
+marker.label.cycleCollection.forgetSkippable=CC Graph Reduction
+marker.label.timestamp=Timestamp
+marker.label.worker=Worker
+marker.label.messagePort=MessagePort
+marker.label.unknown=Unknown
+
+# LOCALIZATION NOTE (marker.label.javascript.*):
+# These strings are displayed as JavaScript markers that have special
+# reasons that can be translated.
+marker.label.javascript.scriptElement=Script Tag
+marker.label.javascript.promiseCallback=Promise Callback
+marker.label.javascript.promiseInit=Promise Init
+marker.label.javascript.workerRunnable=Worker
+marker.label.javascript.jsURI=JavaScript URI
+marker.label.javascript.eventHandler=Event Handler
+
+# LOCALIZATION NOTE (marker.field.*):
+# Strings used in the waterfall sidebar as property names.
+
+# General marker fields
+marker.field.start=Start:
+marker.field.end=End:
+marker.field.duration=Duration:
+
+# General "reason" for a marker (JavaScript, Garbage Collection)
+marker.field.causeName=Cause:
+# General "type" for a marker (Cycle Collection, Garbage Collection)
+marker.field.type=Type:
+# General "label" for a marker (user defined)
+marker.field.label=Label:
+
+# Field names for stack values
+marker.field.stack=Stack:
+marker.field.startStack=Stack at start:
+marker.field.endStack=Stack at end:
+
+# %S is the "Async Cause" of a marker, and this signifies that the cause
+# was an asynchronous one in a displayed stack.
+marker.field.asyncStack=(Async: %S)
+
+# For console.time markers
+marker.field.consoleTimerName=Timer Name:
+
+# For DOM Event markers
+marker.field.DOMEventType=Event Type:
+marker.field.DOMEventPhase=Phase:
+
+# Non-incremental cause for a Garbage Collection marker
+marker.field.nonIncrementalCause=Non-incremental Cause:
+
+# For "Recalculate Style" markers
+marker.field.restyleHint=Restyle Hint:
+
+# The type of operation performed by a Worker.
+marker.worker.serializeDataOffMainThread=Serialize data in Worker
+marker.worker.serializeDataOnMainThread=Serialize data on the main thread
+marker.worker.deserializeDataOffMainThread=Deserialize data in Worker
+marker.worker.deserializeDataOnMainThread=Deserialize data on the main thread
+
+# The type of operation performed by a MessagePort
+marker.messagePort.serializeData=Serialize data
+marker.messagePort.deserializeData=Deserialize data
+
+# Strings used in the waterfall sidebar as values.
+marker.value.unknownFrame=<unknown location>
+marker.value.DOMEventTargetPhase=Target
+marker.value.DOMEventCapturingPhase=Capture
+marker.value.DOMEventBubblingPhase=Bubbling
+
+# LOCALIZATION NOTE (marker.gcreason.label.*):
+# These strings are used to give a concise but readable description of a GC reason.
+marker.gcreason.label.API=API Call
+marker.gcreason.label.EAGER_ALLOC_TRIGGER=Eager Allocation Trigger
+marker.gcreason.label.DESTROY_RUNTIME=Shutdown
+marker.gcreason.label.LAST_DITCH=Out of Memory
+marker.gcreason.label.TOO_MUCH_MALLOC=Too Many Bytes Allocated
+marker.gcreason.label.ALLOC_TRIGGER=Too Many Allocations
+marker.gcreason.label.DEBUG_GC=Debug GC
+marker.gcreason.label.COMPARTMENT_REVIVED=Dead Global Revived
+marker.gcreason.label.RESET=Finish Incremental Cycle
+marker.gcreason.label.OUT_OF_NURSERY=Nursery is Full
+marker.gcreason.label.EVICT_NURSERY=Nursery Eviction
+marker.gcreason.label.FULL_STORE_BUFFER=Nursery Objects Too Active
+marker.gcreason.label.SHARED_MEMORY_LIMIT=Large Allocation Failed
+marker.gcreason.label.PERIODIC_FULL_GC=Periodic Full GC
+marker.gcreason.label.INCREMENTAL_TOO_SLOW=Allocations Rate Too Fast
+marker.gcreason.label.COMPONENT_UTILS=Cu.forceGC
+marker.gcreason.label.MEM_PRESSURE=Low Memory
+marker.gcreason.label.CC_WAITING=Forced by Cycle Collection
+marker.gcreason.label.CC_FORCED=Forced by Cycle Collection
+marker.gcreason.label.LOAD_END=Page Load Finished
+marker.gcreason.label.PAGE_HIDE=Moved to Background
+marker.gcreason.label.NSJSCONTEXT_DESTROY=Destroy JS Context
+marker.gcreason.label.SET_NEW_DOCUMENT=New Document
+marker.gcreason.label.SET_DOC_SHELL=New Document
+marker.gcreason.label.DOM_UTILS=API Call
+marker.gcreason.label.DOM_IPC=IPC
+marker.gcreason.label.DOM_WORKER=Periodic Worker GC
+marker.gcreason.label.INTER_SLICE_GC=Periodic Incremental GC Slice
+marker.gcreason.label.FULL_GC_TIMER=Periodic Full GC
+marker.gcreason.label.SHUTDOWN_CC=Shutdown
+marker.gcreason.label.FINISH_LARGE_EVALUATE=Large Eval
+marker.gcreason.label.DOM_WINDOW_UTILS=User Inactive
+marker.gcreason.label.USER_INACTIVE=User Inactive
+
+# The name of a nursery collection.
+marker.nurseryCollection=Nursery Collection
+
+# LOCALIZATION NOTE (marker.gcreason.description.*):
+# These strings are used to give an expanded description of why a GC occurred.
+marker.gcreason.description.API=There was an API call to force garbage collection.
+marker.gcreason.description.EAGER_ALLOC_TRIGGER=JavaScript returned to the event loop and there were enough bytes allocated since the last GC that a new GC cycle was triggered.
+marker.gcreason.description.DESTROY_RUNTIME=Firefox destroyed a JavaScript runtime or context, and this was the final garbage collection before shutting down.
+marker.gcreason.description.LAST_DITCH=JavaScript attempted to allocate, but there was no memory available. Doing a full compacting garbage collection as an attempt to free up memory for the allocation.
+marker.gcreason.description.TOO_MUCH_MALLOC=JavaScript allocated too many bytes, and forced a garbage collection.
+marker.gcreason.description.ALLOC_TRIGGER=JavaScript allocated too many times, and forced a garbage collection.
+marker.gcreason.description.DEBUG_GC=GC due to Zeal debug settings.
+marker.gcreason.description.COMPARTMENT_REVIVED=A global object that was thought to be dead at the start of the GC cycle was revived by the end of the GC cycle.
+marker.gcreason.description.RESET=The active incremental GC cycle was forced to finish immediately.
+marker.gcreason.description.OUT_OF_NURSERY=JavaScript allocated enough new objects in the nursery that it became full and triggered a minor GC.
+marker.gcreason.description.EVICT_NURSERY=Work needed to be done on the tenured heap, requiring the nursery to be empty.
+marker.gcreason.description.FULL_STORE_BUFFER=There were too many properties on tenured objects whose value was an object in the nursery.
+marker.gcreason.description.SHARED_MEMORY_LIMIT=A large allocation was requested, but there was not enough memory.
+marker.gcreason.description.PERIODIC_FULL_GC=JavaScript returned to the event loop, and it has been a relatively long time since Firefox performed a garbage collection.
+marker.gcreason.description.INCREMENTAL_TOO_SLOW=A full, non-incremental garbage collection was triggered because there was a faster rate of allocations than the existing incremental garbage collection cycle could keep up with.
+marker.gcreason.description.COMPONENT_UTILS=Components.utils.forceGC() was called to force a garbage collection.
+marker.gcreason.description.MEM_PRESSURE=There was very low memory available.
+marker.gcreason.description.CC_WAITING=The cycle collector required a garbage collection.
+marker.gcreason.description.CC_FORCED=The cycle collector required a garbage collection.
+marker.gcreason.description.LOAD_END=The document finished loading.
+marker.gcreason.description.PAGE_HIDE=The tab or window was moved to the background.
+marker.gcreason.description.NSJSCONTEXT_DESTROY=Firefox destroyed a JavaScript runtime or context, and this was the final garbage collection before shutting down.
+marker.gcreason.description.SET_NEW_DOCUMENT=The page has been navigated to a new document.
+marker.gcreason.description.SET_DOC_SHELL=The page has been navigated to a new document.
+marker.gcreason.description.DOM_UTILS=There was an API call to force garbage collection.
+marker.gcreason.description.DOM_IPC=Received an inter-process message that requested a garbage collection.
+marker.gcreason.description.DOM_WORKER=The worker was idle for a relatively long time.
+marker.gcreason.description.INTER_SLICE_GC=There has been a relatively long time since the last incremental GC slice.
+marker.gcreason.description.FULL_GC_TIMER=JavaScript returned to the event loop, and it has been a relatively long time since we performed a garbage collection.
+marker.gcreason.description.SHUTDOWN_CC=Firefox destroyed a JavaScript runtime or context, and this was the final garbage collection before shutting down.
+marker.gcreason.description.FINISH_LARGE_EVALUATE=Firefox finished evaluating a large script, and performed a GC because the script will never be run again.
+marker.gcreason.description.DOM_WINDOW_UTILS=The user was inactive for a long time. Took the opportunity to perform GC when it was unlikely to be noticed.
+marker.gcreason.description.USER_INACTIVE=The user was inactive for a long time. Firefox took the opportunity to perform GC when it was unlikely to be noticed.
diff --git a/devtools/client/locales/en-US/memory.properties b/devtools/client/locales/en-US/memory.properties
new file mode 100644
index 000000000..e271a8a7d
--- /dev/null
+++ b/devtools/client/locales/en-US/memory.properties
@@ -0,0 +1,446 @@
+# 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/.
+
+# LOCALIZATION NOTE These strings are used inside the Memory Tools
+# which is available from the Web Developer sub-menu -> 'Memory'.
+# The correct localization of this file might be to keep it in
+# English, or another language commonly spoken among web developers.
+# You want to make that choice consistent across the developer tools.
+# A good criteria is the language in which you'd find the best
+# documentation on web development on the web.
+
+# LOCALIZATION NOTE (snapshot.io.save): The label for the link that saves a
+# snapshot to disk.
+snapshot.io.save=Save
+
+# LOCALIZATION NOTE (snapshot.io.delete): The label for the link that deletes
+# a snapshot
+snapshot.io.delete=Delete
+
+# LOCALIZATION NOTE (snapshot.io.save.window): The title for the window
+# displayed when saving a snapshot to disk.
+snapshot.io.save.window=Save Snapshot
+
+# LOCALIZATION NOTE (snapshot.io.import.window): The title for the window
+# displayed when importing a snapshot form disk.
+snapshot.io.import.window=Import Snapshot
+
+# LOCALIZATION NOTE (snapshot.io.filter): The title for the filter used to
+# filter file types (*.fxsnapshot)
+snapshot.io.filter=Firefox Snapshots
+
+# LOCALIZATION NOTE (aggregate.mb): The label annotating the number of bytes (in
+# megabytes) in a snapshot. %S represents the value, rounded to 2 decimal
+# points.
+aggregate.mb=%S MB
+
+# LOCALIZATION NOTE (snapshot-title.loading): The title for a snapshot before
+# it has a creation time to display.
+snapshot-title.loading=Processing…
+
+# LOCALIZATION NOTE (checkbox.recordAllocationStacks): The label describing the
+# boolean checkbox whether or not to record call stacks.
+checkbox.recordAllocationStacks=Record call stacks
+
+# LOCALIZATION NOTE (checkbox.recordAllocationStacks.tooltip): The tooltip for
+# the label describing the boolean checkbox whether or not to record call
+# stacks.
+checkbox.recordAllocationStacks.tooltip=Toggle the recording of the call stack of when an object was allocated. Subsequent snapshots will be able to group and label objects by call stacks, but only with those objects created after toggling this option. Recording call stacks has a performance overhead.
+
+# LOCALIZATION NOTE (toolbar.displayBy): The label describing the select menu
+# options of the display options.
+toolbar.displayBy=Group by:
+
+# LOCALIZATION NOTE (toolbar.displayBy.tooltip): The tooltip for the label
+# describing the select menu options of the display options.
+toolbar.displayBy.tooltip=Change how objects are grouped
+
+# LOCALIZATION NOTE (toolbar.pop-view): The text in the button to go back to the
+# previous view.
+toolbar.pop-view=â†
+
+# LOCALIZATION NOTE (toolbar.pop-view.label): The text for the label for the
+# button to go back to the previous view.
+toolbar.pop-view.label=Go back to aggregates
+
+# LOCALIZATION NOTE (toolbar.viewing-individuals): The text letting the user
+# know that they are viewing individual nodes from a census group.
+toolbar.viewing-individuals=â‚ Viewing individuals in group
+
+# LOCALIZATION NOTE (censusDisplays.coarseType.tooltip): The tooltip for the
+# "coarse type" display option.
+censusDisplays.coarseType.tooltip=Group items by their type
+
+# LOCALIZATION NOTE (censusDisplays.allocationStack.tooltip): The tooltip for
+# the "call stack" display option.
+censusDisplays.allocationStack.tooltip=Group items by the JavaScript stack recorded when the object was allocated
+
+# LOCALIZATION NOTE (censusDisplays.invertedAllocationStack.tooltip): The
+# tooltip for the "inverted call stack" display option.
+censusDisplays.invertedAllocationStack.tooltip=Group items by the inverted JavaScript call stack recorded when the object was created
+
+# LOCALIZATION NOTE (censusDisplays.treeMap.tooltip): The tooltip for the
+# "tree map" display option.
+censusDisplays.treeMap.tooltip=Visualize memory usage: larger blocks account for a larger percent of memory usage
+
+# LOCALIZATION NOTE (censusDisplays.objectClass.tooltip): The tooltip for the
+# "object class" display option.
+censusDisplays.objectClass.tooltip=Group items by their JavaScript Object [[class]] name
+
+# LOCALIZATION NOTE (censusDisplays.internalType.tooltip): The tooltip for the
+# "internal type" display option.
+censusDisplays.internalType.tooltip=Group items by their internal C++ type
+
+# LOCALIZATION NOTE (toolbar.labelBy): The label describing the select menu
+# options of the label options.
+toolbar.labelBy=Label by:
+
+# LOCALIZATION NOTE (toolbar.labelBy): The tooltip for the label describing the
+# select menu options of the label options.
+toolbar.labelBy.tooltip=Change how objects are labeled
+
+# LOCALIZATION NOTE (dominatorTreeDisplays.coarseType.tooltip): The tooltip for
+# the "coarse type" dominator tree display option.
+dominatorTreeDisplays.coarseType.tooltip=Label objects by the broad categories they fit in
+
+# LOCALIZATION NOTE (dominatorTreeDisplays.allocationStack.tooltip): The
+# tooltip for the "call stack" dominator tree display option.
+dominatorTreeDisplays.allocationStack.tooltip=Label objects by the JavaScript stack recorded when it was allocated
+
+# LOCALIZATION NOTE (dominatorTreeDisplays.internalType.tooltip): The
+# tooltip for the "internal type" dominator tree display option.
+dominatorTreeDisplays.internalType.tooltip=Label objects by their internal C++ type name
+
+# LOCALIZATION NOTE (treeMapDisplays.coarseType.tooltip): The tooltip for
+# the "coarse type" tree map display option.
+treeMapDisplays.coarseType.tooltip=Label objects by the broad categories they fit in
+
+# LOCALIZATION NOTE (toolbar.view): The label for the view selector in the
+# toolbar.
+toolbar.view=View:
+
+# LOCALIZATION NOTE (toolbar.view.tooltip): The tooltip for the label for the
+# view selector in the toolbar.
+toolbar.view.tooltip=Change the view of the snapshot
+
+# LOCALIZATION NOTE (toolbar.view.census): The label for the census view option
+# in the toolbar.
+toolbar.view.census=Aggregate
+
+# LOCALIZATION NOTE (toolbar.view.census.tooltip): The tooltip for the label for
+# the census view option in the toolbar.
+toolbar.view.census.tooltip=View a summary of the snapshot’s contents by aggregating objects into groups
+
+# LOCALIZATION NOTE (toolbar.view.dominators): The label for the dominators view
+# option in the toolbar.
+toolbar.view.dominators=Dominators
+
+# LOCALIZATION NOTE (toolbar.view.dominators.tooltip): The tooltip for the label
+# for the dominators view option in the toolbar.
+toolbar.view.dominators.tooltip=View the dominator tree and surface the largest structures in the snapshot
+
+# LOCALIZATION NOTE (toolbar.view.treemap): The label for the tree map option
+# in the toolbar.
+toolbar.view.treemap=Tree Map
+
+# LOCALIZATION NOTE (toolbar.view.treemap.tooltip): The tooltip for the label for
+# the tree map view option in the toolbar.
+toolbar.view.treemap.tooltip=Visualize memory usage: larger blocks account for a larger percent of memory usage
+
+# LOCALIZATION NOTE (take-snapshot): The label describing the button that
+# initiates taking a snapshot, either as the main label, or a tooltip.
+take-snapshot=Take snapshot
+
+# LOCALIZATION NOTE (import-snapshot): The label describing the button that
+# initiates importing a snapshot.
+import-snapshot=Import…
+
+# LOCALIZATION NOTE (clear-snapshots.tooltip): The tooltip for the button that
+# deletes existing snapshot.
+clear-snapshots.tooltip=Delete all snapshots
+
+# LOCALIZATION NOTE (diff-snapshots.tooltip): The tooltip for the button that
+# initiates selecting two snapshots to diff with each other.
+diff-snapshots.tooltip=Compare snapshots
+
+# LOCALIZATION NOTE (filter.placeholder): The placeholder text used for the
+# memory tool's filter search box.
+filter.placeholder=Filter
+
+# LOCALIZATION NOTE (filter.tooltip): The tooltip text used for the memory
+# tool's filter search box.
+filter.tooltip=Filter the contents of the snapshot
+
+# LOCALIZATION NOTE (tree-item.view-individuals.tooltip): The tooltip for the
+# button to view individuals in this group.
+tree-item.view-individuals.tooltip=View individual nodes in this group and their retaining paths
+
+# LOCALIZATION NOTE (tree-item.load-more): The label for the links to fetch the
+# lazily loaded sub trees in the dominator tree view.
+tree-item.load-more=Load more…
+
+# LOCALIZATION NOTE (tree-item.rootlist): The label for the root of the
+# dominator tree.
+tree-item.rootlist=GC Roots
+
+# LOCALIZATION NOTE (tree-item.nostack): The label describing the row in the heap tree
+# that represents a row broken down by call stack when no stack was available.
+tree-item.nostack=(no stack available)
+
+# LOCALIZATION NOTE (tree-item.nofilename): The label describing the row in the
+# heap tree that represents a row broken down by filename when no filename was
+# available.
+tree-item.nofilename=(no filename available)
+
+# LOCALIZATION NOTE (tree-item.root): The label describing the row in the heap tree
+# that represents the root of the tree when inverted.
+tree-item.root=(root)
+
+# LOCALIZATION NOTE (tree-item.percent2): A percent of bytes or count displayed in the tree view.
+# there are two "%" after %S to escape and display "%"
+tree-item.percent2=%S%%
+
+# LOCALIZATION NOTE (diffing.baseline): The name of the baseline snapshot in a
+# diffing comparison.
+diffing.baseline=Baseline
+
+# LOCALIZATION NOTE (diffing.comparison): The name of the snapshot being
+# compared to the baseline in a diffing comparison.
+diffing.comparison=Comparison
+
+# LOCALIZATION NOTE (diffing.prompt.selectBaseline): The prompt to select the
+# first snapshot when doing a diffing comparison.
+diffing.prompt.selectBaseline=Select the baseline snapshot
+
+# LOCALIZATION NOTE (diffing.prompt.selectComparison): The prompt to select the
+# second snapshot when doing a diffing comparison.
+diffing.prompt.selectComparison=Select the snapshot to compare to the baseline
+
+# LOCALIZATION NOTE (diffing.state.error): The label describing the diffing
+# state ERROR, used in the snapshot list when an error occurs while diffing two
+# snapshots.
+diffing.state.error=Error
+
+# LOCALIZATION NOTE (diffing.state.error.full): The text describing the diffing
+# state ERROR, used in the main view when an error occurs while diffing two
+# snapshots.
+diffing.state.error.full=There was an error while comparing snapshots.
+
+# LOCALIZATION NOTE (diffing.state.taking-diff): The label describing the diffin
+# state TAKING_DIFF, used in the snapshots list when computing the difference
+# between two snapshots.
+diffing.state.taking-diff=Computing difference…
+
+# LOCALIZATION NOTE (diffing.state.taking-diff.full): The label describing the
+# diffing state TAKING_DIFF, used in the main view when computing the difference
+# between two snapshots.
+diffing.state.taking-diff.full=Computing difference…
+
+# LOCALIZATION NOTE (diffing.state.selecting): The label describing the diffing
+# state SELECTING.
+diffing.state.selecting=Select two snapshots to compare
+
+# LOCALIZATION NOTE (diffing.state.selecting.full): The label describing the
+# diffing state SELECTING, used in the main view when selecting snapshots to
+# diff.
+diffing.state.selecting.full=Select two snapshots to compare
+
+# LOCALIZATION NOTE (dominatorTree.state.computing): The label describing the
+# dominator tree state COMPUTING.
+dominatorTree.state.computing=Generating dominators report…
+
+# LOCALIZATION NOTE (dominatorTree.state.computing): The label describing the
+# dominator tree state COMPUTING, used in the dominator tree view.
+dominatorTree.state.computing.full=Generating dominators report…
+
+# LOCALIZATION NOTE (dominatorTree.state.fetching): The label describing the
+# dominator tree state FETCHING.
+dominatorTree.state.fetching=Computing sizes…
+
+# LOCALIZATION NOTE (dominatorTree.state.fetching): The label describing the
+# dominator tree state FETCHING, used in the dominator tree view.
+dominatorTree.state.fetching.full=Computing dominator’s retained sizes…
+
+# LOCALIZATION NOTE (dominatorTree.state.incrementalFetching): The label
+# describing the dominator tree state INCREMENTAL_FETCHING.
+dominatorTree.state.incrementalFetching=Fetching…
+
+# LOCALIZATION NOTE (dominatorTree.state.incrementalFetching): The label describing the
+# dominator tree state INCREMENTAL_FETCHING, used in the dominator tree view.
+dominatorTree.state.incrementalFetching.full=Fetching more…
+
+# LOCALIZATION NOTE (dominatorTree.state.error): The label describing the
+# dominator tree state ERROR.
+dominatorTree.state.error=Error
+
+# LOCALIZATION NOTE (dominatorTree.state.error): The label describing the
+# dominator tree state ERROR, used in the dominator tree view.
+dominatorTree.state.error.full=There was an error while processing the dominator tree
+
+# LOCALIZATION NOTE (snapshot.state.saving.full): The label describing the
+# snapshot state SAVING, used in the main heap view.
+snapshot.state.saving.full=Saving snapshot…
+
+# LOCALIZATION NOTE (snapshot.state.importing.full): The label describing the
+# snapshot state IMPORTING, used in the main heap view.
+snapshot.state.importing.full=Importing…
+
+# LOCALIZATION NOTE (snapshot.state.reading.full): The label describing the
+# snapshot state READING, and SAVED, due to these states being combined
+# visually, used in the main heap view.
+snapshot.state.reading.full=Reading snapshot…
+
+# LOCALIZATION NOTE (snapshot.state.saving-census.full): The label describing
+# the snapshot state SAVING, used in the main heap view.
+snapshot.state.saving-census.full=Generating aggregate report…
+
+# LOCALIZATION NOTE (snapshot.state.saving-tree-map.full): The label describing
+# the snapshot state SAVING, used in the main heap view.
+snapshot.state.saving-tree-map.full=Saving tree map…
+
+# LOCALIZATION NOTE (snapshot.state.error.full): The label describing the
+# snapshot state ERROR, used in the main heap view.
+snapshot.state.error.full=There was an error processing this snapshot.
+
+# LOCALIZATION NOTE (individuals.state.error): The short message displayed when
+# there is an error fetching individuals from a group.
+individuals.state.error=Error
+
+# LOCALIZATION NOTE (individuals.state.error.full): The longer message displayed
+# when there is an error fetching individuals from a group.
+individuals.state.error.full=There was an error while fetching individuals in the group
+
+# LOCALIZATION NOTE (individuals.state.fetching): The short message displayed
+# while fetching individuals.
+individuals.state.fetching=Fetching…
+
+# LOCALIZATION NOTE (individuals.state.fetching.full): The longer message
+# displayed while fetching individuals.
+individuals.state.fetching.full=Fetching individuals in group…
+
+# LOCALIZATION NOTE (individuals.field.node): The header label for an individual
+# node.
+individuals.field.node=Node
+
+# LOCALIZATION NOTE (individuals.field.node.tooltip): The tooltip for the header
+# label for an individual node.
+individuals.field.node.tooltip=The individual node in the snapshot
+
+# LOCALIZATION NOTE (snapshot.state.saving): The label describing the snapshot
+# state SAVING, used in the snapshot list view
+snapshot.state.saving=Saving snapshot…
+
+# LOCALIZATION NOTE (snapshot.state.importing): The label describing the
+# snapshot state IMPORTING, used in the snapshot list view
+snapshot.state.importing=Importing snapshot…
+
+# LOCALIZATION NOTE (snapshot.state.reading): The label describing the snapshot
+# state READING, and SAVED, due to these states being combined visually, used in
+# the snapshot list view.
+snapshot.state.reading=Reading snapshot…
+
+# LOCALIZATION NOTE (snapshot.state.saving-census): The label describing the
+# snapshot state SAVING, used in snapshot list view.
+snapshot.state.saving-census=Saving report…
+
+# LOCALIZATION NOTE (snapshot.state.saving-census): The label describing the
+# snapshot state SAVING, used in snapshot list view.
+snapshot.state.saving-tree-map=Saving tree map…
+
+# LOCALIZATION NOTE (snapshot.state.error): The label describing the snapshot
+# state ERROR, used in the snapshot list view.
+snapshot.state.error=Error
+
+# LOCALIZATION NOTE (heapview.no-difference): Message displayed when there is no
+# difference between two snapshots.
+heapview.no-difference=No difference between the baseline and comparison.
+
+# LOCALIZATION NOTE (heapview.none-match): Message displayed when there are no
+# matches when filtering.
+heapview.none-match=No matches.
+
+# LOCALIZATION NOTE (heapview.none-match): Message displayed when there report
+# is empty.
+heapview.empty=Empty.
+
+# LOCALIZATION NOTE (heapview.noAllocationStacks): The message displayed to
+# users when selecting a display by "call stack" but no call stacks
+# were recorded in the heap snapshot.
+heapview.noAllocationStacks=No call stacks found. Record call stacks before taking a snapshot.
+
+# LOCALIZATION NOTE (heapview.field.retainedSize): The name of the column in the
+# dominator tree view for retained byte sizes.
+heapview.field.retainedSize=Retained Size (Bytes)
+
+# LOCALIZATION NOTE (heapview.field.retainedSize.tooltip): The tooltip for the
+# column header in the dominator tree view for retained byte sizes.
+heapview.field.retainedSize.tooltip=The sum of the size of the object itself, and the sizes of all the other objects kept alive by it
+
+# LOCALIZATION NOTE (heapview.field.shallowSize): The name of the column in the
+# dominator tree view for shallow byte sizes.
+heapview.field.shallowSize=Shallow Size (Bytes)
+
+# LOCALIZATION NOTE (heapview.field.shallowSize.tooltip): The tooltip for the
+# column header in the dominator tree view for shallow byte sizes.
+heapview.field.shallowSize.tooltip=The size of the object itself
+
+# LOCALIZATION NOTE (dominatortree.field.label): The name of the column in the
+# dominator tree for an object's label.
+dominatortree.field.label=Dominator
+
+# LOCALIZATION NOTE (dominatortree.field.label.tooltip): The tooltip for the column
+# header in the dominator tree view for an object's label.
+dominatortree.field.label.tooltip=The label for an object in memory
+
+# LOCALIZATION NOTE (heapview.field.bytes): The name of the column in the heap
+# view for bytes.
+heapview.field.bytes=Bytes
+
+# LOCALIZATION NOTE (heapview.field.bytes.tooltip): The tooltip for the column
+# header in the heap view for bytes.
+heapview.field.bytes.tooltip=The number of bytes taken up by this group, excluding subgroups
+
+# LOCALIZATION NOTE (heapview.field.count): The name of the column in the heap
+# view for count.
+heapview.field.count=Count
+
+# LOCALIZATION NOTE (heapview.field.count.tooltip): The tooltip for the column
+# header in the heap view for count.
+heapview.field.count.tooltip=The number of reachable objects in this group, excluding subgroups
+
+# LOCALIZATION NOTE (heapview.field.totalbytes): The name of the column in the
+# heap view for total bytes.
+heapview.field.totalbytes=Total Bytes
+
+# LOCALIZATION NOTE (heapview.field.totalbytes.tooltip): The tooltip for the
+# column header in the heap view for total bytes.
+heapview.field.totalbytes.tooltip=The number of bytes taken up by this group, including subgroups
+
+# LOCALIZATION NOTE (heapview.field.totalcount): The name of the column in the
+# heap view for total count.
+heapview.field.totalcount=Total Count
+
+# LOCALIZATION NOTE (heapview.field.totalcount.tooltip): The tooltip for the
+# column header in the heap view for total count.
+heapview.field.totalcount.tooltip=The number of reachable objects in this group, including subgroups
+
+# LOCALIZATION NOTE (heapview.field.name): The name of the column in the heap
+# view for name.
+heapview.field.name=Group
+
+# LOCALIZATION NOTE (heapview.field.name.tooltip): The tooltip for the column
+# header in the heap view for name.
+heapview.field.name.tooltip=The name of this group
+
+# LOCALIZATION NOTE (shortest-paths.header): The header label for the shortest
+# paths pane.
+shortest-paths.header=Retaining Paths (from Garbage Collector Roots)
+
+# LOCALIZATION NOTE (shortest-paths.select-node): The message displayed in the
+# shortest paths pane when a node is not yet selected.
+shortest-paths.select-node=Select an item to view its retaining paths
+
+# LOCALIZATION NOTE (tree-map.node-count): The label for the count value of a
+# node in the tree map
+tree-map.node-count=count
diff --git a/devtools/client/locales/en-US/menus.properties b/devtools/client/locales/en-US/menus.properties
new file mode 100644
index 000000000..66e158cbd
--- /dev/null
+++ b/devtools/client/locales/en-US/menus.properties
@@ -0,0 +1,67 @@
+# 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/.
+
+devToolsCmd.key = VK_F12
+devToolsCmd.keytext = F12
+
+devtoolsServiceWorkers.label = Service Workers
+devtoolsServiceWorkers.accesskey = k
+
+devtoolsConnect.label = Connect…
+devtoolsConnect.accesskey = C
+
+browserConsoleCmd.label = Browser Console
+browserConsoleCmd.accesskey = B
+browserConsoleCmd.key = j
+
+responsiveDesignMode.label = Responsive Design Mode
+responsiveDesignMode.accesskey = R
+responsiveDesignMode.key = M
+
+eyedropper.label = Eyedropper
+eyedropper.accesskey = Y
+
+# LOCALIZATION NOTE (scratchpad.label): This menu item label appears
+# in the Tools menu. See bug 653093.
+# The Scratchpad is intended to provide a simple text editor for creating
+# and evaluating bits of JavaScript code for the purposes of function
+# prototyping, experimentation and convenient scripting.
+#
+# It's quite possible that you won't have a good analogue for the word
+# "Scratchpad" in your locale. You should feel free to find a close
+# approximation to it or choose a word (or words) that means
+# "simple discardable text editor".
+scratchpad.label = Scratchpad
+scratchpad.accesskey = s
+scratchpad.key = VK_F4
+scratchpad.keytext = F4
+
+# LOCALIZATION NOTE (browserToolboxMenu.label): This is the label for the
+# application menu item that opens the browser toolbox UI in the Tools menu.
+browserToolboxMenu.label = Browser Toolbox
+browserToolboxMenu.accesskey = e
+browserToolboxMenu.key = i
+
+# LOCALIZATION NOTE (browserContentToolboxMenu.label): This is the label for the
+# application menu item that opens the browser content toolbox UI in the Tools menu.
+# This toolbox allows to debug the chrome of the content process in multiprocess builds.
+browserContentToolboxMenu.label = Browser Content Toolbox
+browserContentToolboxMenu.accesskey = x
+
+devToolbarMenu.label = Developer Toolbar
+devToolbarMenu.accesskey = v
+devToolbarMenu.key = VK_F2
+devToolbarMenu.keytext = F2
+
+webide.label = WebIDE
+webide.accesskey = W
+webide.key = VK_F8
+webide.keytext = F8
+
+devToolboxMenuItem.label = Toggle Tools
+devToolboxMenuItem.accesskey = T
+devToolboxMenuItem.key = I
+
+getMoreDevtoolsCmd.label = Get More Tools
+getMoreDevtoolsCmd.accesskey = M
diff --git a/devtools/client/locales/en-US/netmonitor.properties b/devtools/client/locales/en-US/netmonitor.properties
new file mode 100644
index 000000000..e6118ca9f
--- /dev/null
+++ b/devtools/client/locales/en-US/netmonitor.properties
@@ -0,0 +1,747 @@
+# 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/.
+
+# LOCALIZATION NOTE These strings are used inside the Network Monitor
+# which is available from the Web Developer sub-menu -> 'Network Monitor'.
+# The correct localization of this file might be to keep it in
+# English, or another language commonly spoken among web developers.
+# You want to make that choice consistent across the developer tools.
+# A good criteria is the language in which you'd find the best
+# documentation on web development on the web.
+
+# LOCALIZATION NOTE (netmonitor.security.state.secure)
+# This string is used as an tooltip for request that was performed over secure
+# channel i.e. the connection was encrypted.
+netmonitor.security.state.secure=The connection used to fetch this resource was secure.
+
+# LOCALIZATION NOTE (netmonitor.security.state.insecure)
+# This string is used as an tooltip for request that was performed over insecure
+# channel i.e. the connection was not https
+netmonitor.security.state.insecure=The connection used to fetch this resource was not secure.
+
+# LOCALIZATION NOTE (netmonitor.security.state.broken)
+# This string is used as an tooltip for request that failed due to security
+# issues.
+netmonitor.security.state.broken=A security error prevented the resource from being loaded.
+
+# LOCALIZATION NOTE (netmonitor.security.state.weak)
+# This string is used as an tooltip for request that had minor security issues
+netmonitor.security.state.weak=This resource was transferred over a connection that used weak encryption.
+
+# LOCALIZATION NOTE (netmonitor.security.enabled):
+# This string is used to indicate that a specific security feature is used by
+# a connection in the security details tab.
+# For example: "HTTP Strict Transport Security: Enabled"
+netmonitor.security.enabled=Enabled
+
+# LOCALIZATION NOTE (netmonitor.security.disabled):
+# This string is used to indicate that a specific security feature is not used by
+# a connection in the security details tab.
+# For example: "HTTP Strict Transport Security: Disabled"
+netmonitor.security.disabled=Disabled
+
+# LOCALIZATION NOTE (netmonitor.security.hostHeader):
+# This string is used as a header for section containing security information
+# related to the remote host. %S is replaced with the domain name of the remote
+# host. For example: Host example.com
+netmonitor.security.hostHeader=Host %S:
+
+# LOCALIZATION NOTE (netmonitor.security.notAvailable):
+# This string is used to indicate that a certain piece of information is not
+# available to be displayed. For example a certificate that has no organization
+# defined:
+# Organization: <Not Available>
+netmonitor.security.notAvailable=<Not Available>
+
+# LOCALIZATION NOTE (collapseDetailsPane): This is the tooltip for the button
+# that collapses the network details pane in the UI.
+collapseDetailsPane=Hide request details
+
+# LOCALIZATION NOTE (expandDetailsPane): This is the tooltip for the button
+# that expands the network details pane in the UI.
+expandDetailsPane=Show request details
+
+# LOCALIZATION NOTE (headersEmptyText): This is the text displayed in the
+# headers tab of the network details pane when there are no headers available.
+headersEmptyText=No headers for this request
+
+# LOCALIZATION NOTE (headersFilterText): This is the text displayed in the
+# headers tab of the network details pane for the filtering input.
+headersFilterText=Filter headers
+
+# LOCALIZATION NOTE (cookiesEmptyText): This is the text displayed in the
+# cookies tab of the network details pane when there are no cookies available.
+cookiesEmptyText=No cookies for this request
+
+# LOCALIZATION NOTE (cookiesFilterText): This is the text displayed in the
+# cookies tab of the network details pane for the filtering input.
+cookiesFilterText=Filter cookies
+
+# LOCALIZATION NOTE (paramsEmptyText): This is the text displayed in the
+# params tab of the network details pane when there are no params available.
+paramsEmptyText=No parameters for this request
+
+# LOCALIZATION NOTE (paramsFilterText): This is the text displayed in the
+# params tab of the network details pane for the filtering input.
+paramsFilterText=Filter request parameters
+
+# LOCALIZATION NOTE (paramsQueryString): This is the label displayed
+# in the network details params tab identifying the query string.
+paramsQueryString=Query string
+
+# LOCALIZATION NOTE (paramsFormData): This is the label displayed
+# in the network details params tab identifying the form data.
+paramsFormData=Form data
+
+# LOCALIZATION NOTE (paramsPostPayload): This is the label displayed
+# in the network details params tab identifying the request payload.
+paramsPostPayload=Request payload
+
+# LOCALIZATION NOTE (requestHeaders): This is the label displayed
+# in the network details headers tab identifying the request headers.
+requestHeaders=Request headers
+
+# LOCALIZATION NOTE (requestHeadersFromUpload): This is the label displayed
+# in the network details headers tab identifying the request headers from
+# the upload stream of a POST request's body.
+requestHeadersFromUpload=Request headers from upload stream
+
+# LOCALIZATION NOTE (responseHeaders): This is the label displayed
+# in the network details headers tab identifying the response headers.
+responseHeaders=Response headers
+
+# LOCALIZATION NOTE (requestCookies): This is the label displayed
+# in the network details params tab identifying the request cookies.
+requestCookies=Request cookies
+
+# LOCALIZATION NOTE (responseCookies): This is the label displayed
+# in the network details params tab identifying the response cookies.
+responseCookies=Response cookies
+
+# LOCALIZATION NOTE (jsonFilterText): This is the text displayed
+# in the response tab of the network details pane for the JSON filtering input.
+jsonFilterText=Filter properties
+
+# LOCALIZATION NOTE (jsonScopeName): This is the text displayed
+# in the response tab of the network details pane for a JSON scope.
+jsonScopeName=JSON
+
+# LOCALIZATION NOTE (jsonpScopeName): This is the text displayed
+# in the response tab of the network details pane for a JSONP scope.
+jsonpScopeName=JSONP → callback %S()
+
+# LOCALIZATION NOTE (networkMenu.sortedAsc): This is the tooltip displayed
+# in the network table toolbar, for any column that is sorted ascending.
+networkMenu.sortedAsc=Sorted ascending
+
+# LOCALIZATION NOTE (networkMenu.sortedDesc): This is the tooltip displayed
+# in the network table toolbar, for any column that is sorted descending.
+networkMenu.sortedDesc=Sorted descending
+
+# LOCALIZATION NOTE (networkMenu.empty): This is the label displayed
+# in the network table footer when there are no requests available.
+networkMenu.empty=No requests
+
+# LOCALIZATION NOTE (networkMenu.summary): Semi-colon list of plural forms.
+# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
+# This label is displayed in the network table footer providing concise
+# information about all requests. Parameters: #1 is the number of requests,
+# #2 is the size, #3 is the number of seconds.
+networkMenu.summary=One request, #2 KB, #3 s;#1 requests, #2 KB, #3 s
+
+# LOCALIZATION NOTE (networkMenu.sizeB): This is the label displayed
+# in the network menu specifying the size of a request (in bytes).
+networkMenu.sizeB=%S B
+
+# LOCALIZATION NOTE (networkMenu.sizeKB): This is the label displayed
+# in the network menu specifying the size of a request (in kilobytes).
+networkMenu.sizeKB=%S KB
+
+# LOCALIZATION NOTE (networkMenu.sizeMB): This is the label displayed
+# in the network menu specifying the size of a request (in megabytes).
+networkMenu.sizeMB=%S MB
+
+# LOCALIZATION NOTE (networkMenu.sizeGB): This is the label displayed
+# in the network menu specifying the size of a request (in gigabytes).
+networkMenu.sizeGB=%S GB
+
+# LOCALIZATION NOTE (networkMenu.sizeUnavailable): This is the label displayed
+# in the network menu specifying the transferred size of a request is
+# unavailable.
+networkMenu.sizeUnavailable=—
+
+# LOCALIZATION NOTE (networkMenu.sizeCached): This is the label displayed
+# in the network menu specifying the transferred of a request is
+# cached.
+networkMenu.sizeCached=cached
+
+# LOCALIZATION NOTE (networkMenu.sizeServiceWorker): This is the label displayed
+# in the network menu specifying the transferred of a request computed
+# by a service worker.
+networkMenu.sizeServiceWorker=service worker
+
+# LOCALIZATION NOTE (networkMenu.totalMS): This is the label displayed
+# in the network menu specifying the time for a request to finish (in milliseconds).
+networkMenu.totalMS=→ %S ms
+
+# LOCALIZATION NOTE (networkMenu.millisecond): This is the label displayed
+# in the network menu specifying timing interval divisions (in milliseconds).
+networkMenu.millisecond=%S ms
+
+# LOCALIZATION NOTE (networkMenu.second): This is the label displayed
+# in the network menu specifying timing interval divisions (in seconds).
+networkMenu.second=%S s
+
+# LOCALIZATION NOTE (networkMenu.minute): This is the label displayed
+# in the network menu specifying timing interval divisions (in minutes).
+networkMenu.minute=%S min
+
+# LOCALIZATION NOTE (pieChart.loading): This is the label displayed
+# for pie charts (e.g., in the performance analysis view) when there is
+# no data available yet.
+pieChart.loading=Loading
+
+# LOCALIZATION NOTE (pieChart.unavailable): This is the label displayed
+# for pie charts (e.g., in the performance analysis view) when there is
+# no data available, even after loading it.
+pieChart.unavailable=Empty
+
+# LOCALIZATION NOTE (tableChart.loading): This is the label displayed
+# for table charts (e.g., in the performance analysis view) when there is
+# no data available yet.
+tableChart.loading=Please wait…
+
+# LOCALIZATION NOTE (tableChart.unavailable): This is the label displayed
+# for table charts (e.g., in the performance analysis view) when there is
+# no data available, even after loading it.
+tableChart.unavailable=No data available
+
+# LOCALIZATION NOTE (charts.sizeKB): This is the label displayed
+# in pie or table charts specifying the size of a request (in kilobytes).
+charts.sizeKB=%S KB
+
+# LOCALIZATION NOTE (charts.totalS): This is the label displayed
+# in pie or table charts specifying the time for a request to finish (in seconds).
+charts.totalS=%S s
+
+# LOCALIZATION NOTE (charts.cacheEnabled): This is the label displayed
+# in the performance analysis view for "cache enabled" charts.
+charts.cacheEnabled=Primed cache
+
+# LOCALIZATION NOTE (charts.cacheDisabled): This is the label displayed
+# in the performance analysis view for "cache disabled" charts.
+charts.cacheDisabled=Empty cache
+
+# LOCALIZATION NOTE (charts.totalSize): This is the label displayed
+# in the performance analysis view for total requests size, in kilobytes.
+charts.totalSize=Size: %S KB
+
+# LOCALIZATION NOTE (charts.totalSeconds): Semi-colon list of plural forms.
+# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
+# This is the label displayed in the performance analysis view for the
+# total requests time, in seconds.
+charts.totalSeconds=Time: #1 second;Time: #1 seconds
+
+# LOCALIZATION NOTE (charts.totalCached): This is the label displayed
+# in the performance analysis view for total cached responses.
+charts.totalCached=Cached responses: %S
+
+# LOCALIZATION NOTE (charts.totalCount): This is the label displayed
+# in the performance analysis view for total requests.
+charts.totalCount=Total requests: %S
+
+# LOCALIZATION NOTE (netRequest.headers): A label used for Headers tab
+# This tab displays list of HTTP headers
+netRequest.headers=Headers
+
+# LOCALIZATION NOTE (netRequest.response): A label used for Response tab
+# This tab displays HTTP response body
+netRequest.response=Response
+
+# LOCALIZATION NOTE (netRequest.rawData): A label used for a section
+# in Response tab. This section displays raw response body as it's
+# been received from the backend (debugger server)
+netRequest.rawData=Raw Data
+
+# LOCALIZATION NOTE (netRequest.xml): A label used for a section
+# in Response tab. This section displays parsed XML response body.
+netRequest.xml=XML
+
+# LOCALIZATION NOTE (netRequest.image): A label used for a section
+# in Response tab. This section displays images returned in response body.
+netRequest.image=Image
+
+# LOCALIZATION NOTE (netRequest.sizeLimitMessage): A label used
+# in Response and Post tabs in case the body is bigger than given limit.
+# It allows the user to click and fetch more from the backend.
+# The {{link}} will be replace at run-time by an active link.
+# String with ID 'netRequest.sizeLimitMessageLink' will be used as text
+# for this link.
+netRequest.sizeLimitMessage=Size limit has been reached. Click {{link}} to load more.
+netRequest.sizeLimitMessageLink=here
+
+# LOCALIZATION NOTE (netRequest.responseBodyDiscarded): A label used
+# in Response tab if the response body is not available.
+netRequest.responseBodyDiscarded=Response body was not stored.
+
+# LOCALIZATION NOTE (netRequest.requestBodyDiscarded): A label used
+# in Post tab if the post body is not available.
+netRequest.requestBodyDiscarded=Request POST body was not stored.
+
+# LOCALIZATION NOTE (netRequest.post): A label used for Post tab
+# This tab displays HTTP post body
+netRequest.post=POST
+
+# LOCALIZATION NOTE (netRequest.cookies): A label used for Cookies tab
+# This tab displays request and response cookies.
+netRequest.cookies=Cookies
+
+# LOCALIZATION NOTE (netRequest.params): A label used for URL parameters tab
+# This tab displays data parsed from URL query string.
+netRequest.params=Params
+
+# LOCALIZATION NOTE (netRequest.callstack): A label used for request stacktrace tab
+# This tab displays the request's JavaScript stack trace. Should be identical to
+# debuggerUI.tabs.callstack
+netRequest.callstack=Call Stack
+
+# LOCALIZATION NOTE (certmgr.subjectinfo.label):
+# A label used for a certificate section in security tab.
+# This section displays Name and organization who has been assigned the fingerprints
+certmgr.subjectinfo.label=Issued To
+
+# LOCALIZATION NOTE (certmgr.certdetail.cn):
+# A label used for Issued To and Issued By sub-section in security tab
+certmgr.certdetail.cn=Common Name (CN):
+
+# LOCALIZATION NOTE (certmgr.certdetail.o):
+# A label used for Issued To and Issued By sub-section in security tab
+certmgr.certdetail.o=Organization (O):
+
+# LOCALIZATION NOTE (certmgr.certdetail.ou):
+# A label used for Issued To and Issued By sub-section in security tab
+certmgr.certdetail.ou=Organizational Unit (OU):
+
+# LOCALIZATION NOTE (certmgr.issuerinfo.label):
+# A label used for a certificate section in security tab
+# This section displays Name and organization who issued the fingerprints
+certmgr.issuerinfo.label=Issued By
+
+# LOCALIZATION NOTE (certmgr.periodofvalidity.label):
+# A label used for a certificate section in security tab
+# This section displays the valide period of this fingerprints
+certmgr.periodofvalidity.label=Period of Validity
+
+# LOCALIZATION NOTE (certmgr.certdetail.cn):
+# A label used for Period of Validity sub-section in security tab
+certmgr.begins=Begins On:
+
+# LOCALIZATION NOTE (certmgr.certdetail.cn):
+# A label used for Period of Validity sub-section in security tab
+certmgr.expires=Expires On:
+
+# LOCALIZATION NOTE (certmgr.fingerprints.label):
+# A label used for a certificate section in security tab
+# This section displays the valide period of this fingerprints
+certmgr.fingerprints.label=Fingerprints
+
+# LOCALIZATION NOTE (certmgr.certdetail.sha256fingerprint):
+# A label used for Fingerprints sub-section in security tab
+certmgr.certdetail.sha256fingerprint=SHA-256 Fingerprint:
+
+# LOCALIZATION NOTE (certmgr.certdetail.sha1fingerprint):
+# A label used for Fingerprints sub-section in security tab
+certmgr.certdetail.sha1fingerprint=SHA1 Fingerprint:
+
+# LOCALIZATION NOTE (netmonitor.perfNotice1/2/3): These are the labels displayed
+# in the network table when empty to start performance analysis.
+netmonitor.perfNotice1=• Click on the
+netmonitor.perfNotice2=button to start performance analysis.
+netmonitor.perfNotice3=Analyze
+
+# LOCALIZATION NOTE (netmonitor.reload1/2/3): These are the labels displayed
+# in the network table when empty to start logging network requests.
+netmonitor.reloadNotice1=• Perform a request or
+netmonitor.reloadNotice2=Reload
+netmonitor.reloadNotice3=the page to see detailed information about network activity.
+
+# LOCALIZATION NOTE (netmonitor.toolbar.status2): This is the label displayed
+# in the network table toolbar, above the "status" column.
+netmonitor.toolbar.status3=Status
+
+# LOCALIZATION NOTE (netmonitor.toolbar.method): This is the label displayed
+# in the network table toolbar, above the "method" column.
+netmonitor.toolbar.method=Method
+
+# LOCALIZATION NOTE (netmonitor.toolbar.file): This is the label displayed
+# in the network table toolbar, above the "file" column.
+netmonitor.toolbar.file=File
+
+# LOCALIZATION NOTE (netmonitor.toolbar.domain): This is the label displayed
+# in the network table toolbar, above the "domain" column.
+netmonitor.toolbar.domain=Domain
+
+# LOCALIZATION NOTE (netmonitor.toolbar.cause): This is the label displayed
+# in the network table toolbar, above the "cause" column.
+netmonitor.toolbar.cause=Cause
+
+# LOCALIZATION NOTE (netmonitor.toolbar.type): This is the label displayed
+# in the network table toolbar, above the "type" column.
+netmonitor.toolbar.type=Type
+
+# LOCALIZATION NOTE (netmonitor.toolbar.transferred): This is the label displayed
+# in the network table toolbar, above the "transferred" column, which is the
+# compressed / encoded size.
+netmonitor.toolbar.transferred=Transferred
+
+# LOCALIZATION NOTE (netmonitor.toolbar.size): This is the label displayed
+# in the network table toolbar, above the "size" column, which is the
+# uncompressed / decoded size.
+netmonitor.toolbar.size=Size
+
+# LOCALIZATION NOTE (netmonitor.toolbar.waterfall): This is the label displayed
+# in the network table toolbar, above the "waterfall" column.
+netmonitor.toolbar.waterfall=Timeline
+
+# LOCALIZATION NOTE (netmonitor.tab.headers): This is the label displayed
+# in the network details pane identifying the headers tab.
+netmonitor.tab.headers=Headers
+
+# LOCALIZATION NOTE (netmonitor.tab.cookies): This is the label displayed
+# in the network details pane identifying the cookies tab.
+netmonitor.tab.cookies=Cookies
+
+# LOCALIZATION NOTE (netmonitor.tab.params): This is the label displayed
+# in the network details pane identifying the params tab.
+netmonitor.tab.params=Params
+
+# LOCALIZATION NOTE (netmonitor.tab.response): This is the label displayed
+# in the network details pane identifying the response tab.
+netmonitor.tab.response=Response
+
+# LOCALIZATION NOTE (netmonitor.tab.timings): This is the label displayed
+# in the network details pane identifying the timings tab.
+netmonitor.tab.timings=Timings
+
+# LOCALIZATION NOTE (netmonitor.tab.preview): This is the label displayed
+# in the network details pane identifying the preview tab.
+netmonitor.tab.preview=Preview
+
+# LOCALIZATION NOTE (netmonitor.tab.security): This is the label displayed
+# in the network details pane identifying the security tab.
+netmonitor.tab.security=Security
+
+# LOCALIZATION NOTE (netmonitor.toolbar.filter.all): This is the label displayed
+# in the network toolbar for the "All" filtering button.
+netmonitor.toolbar.filter.all=All
+
+# LOCALIZATION NOTE (netmonitor.toolbar.filter.html): This is the label displayed
+# in the network toolbar for the "HTML" filtering button.
+netmonitor.toolbar.filter.html=HTML
+
+# LOCALIZATION NOTE (netmonitor.toolbar.filter.css): This is the label displayed
+# in the network toolbar for the "CSS" filtering button.
+netmonitor.toolbar.filter.css=CSS
+
+# LOCALIZATION NOTE (netmonitor.toolbar.filter.js): This is the label displayed
+# in the network toolbar for the "JS" filtering button.
+netmonitor.toolbar.filter.js=JS
+
+# LOCALIZATION NOTE (netmonitor.toolbar.filter.xhr): This is the label displayed
+# in the network toolbar for the "XHR" filtering button.
+netmonitor.toolbar.filter.xhr=XHR
+
+# LOCALIZATION NOTE (netmonitor.toolbar.filter.fonts): This is the label displayed
+# in the network toolbar for the "Fonts" filtering button.
+netmonitor.toolbar.filter.fonts=Fonts
+
+# LOCALIZATION NOTE (netmonitor.toolbar.filter.images): This is the label displayed
+# in the network toolbar for the "Images" filtering button.
+netmonitor.toolbar.filter.images=Images
+
+# LOCALIZATION NOTE (netmonitor.toolbar.filter.media): This is the label displayed
+# in the network toolbar for the "Media" filtering button.
+netmonitor.toolbar.filter.media=Media
+
+# LOCALIZATION NOTE (netmonitor.toolbar.filter.flash): This is the label displayed
+# in the network toolbar for the "Flash" filtering button.
+netmonitor.toolbar.filter.flash=Flash
+
+# LOCALIZATION NOTE (netmonitor.toolbar.filter.ws): This is the label displayed
+# in the network toolbar for the "WS" filtering button.
+netmonitor.toolbar.filter.ws=WS
+
+# LOCALIZATION NOTE (netmonitor.toolbar.filter.other): This is the label displayed
+# in the network toolbar for the "Other" filtering button.
+netmonitor.toolbar.filter.other=Other
+
+# LOCALIZATION NOTE (netmonitor.toolbar.filterFreetext.label): This is the label
+# displayed in the network toolbar for the url filtering textbox.
+netmonitor.toolbar.filterFreetext.label=Filter URLs
+
+# LOCALIZATION NOTE (netmonitor.toolbar.filterFreetext.key): This is the
+# shortcut key to focus on the toolbar url filtering textbox
+netmonitor.toolbar.filterFreetext.key=CmdOrCtrl+F
+
+# LOCALIZATION NOTE (netmonitor.toolbar.clear): This is the label displayed
+# in the network toolbar for the "Clear" button.
+netmonitor.toolbar.clear=Clear
+
+# LOCALIZATION NOTE (netmonitor.toolbar.perf): This is the label displayed
+# in the network toolbar for the performance analysis button.
+netmonitor.toolbar.perf=Toggle performance analysis…
+
+# LOCALIZATION NOTE (netmonitor.summary.url): This is the label displayed
+# in the network details headers tab identifying the URL.
+netmonitor.summary.url=Request URL:
+
+# LOCALIZATION NOTE (netmonitor.summary.method): This is the label displayed
+# in the network details headers tab identifying the method.
+netmonitor.summary.method=Request method:
+
+# LOCALIZATION NOTE (netmonitor.summary.address): This is the label displayed
+# in the network details headers tab identifying the remote address.
+netmonitor.summary.address=Remote address:
+
+# LOCALIZATION NOTE (netmonitor.summary.status): This is the label displayed
+# in the network details headers tab identifying the status code.
+netmonitor.summary.status=Status code:
+
+# LOCALIZATION NOTE (netmonitor.summary.version): This is the label displayed
+# in the network details headers tab identifying the http version.
+netmonitor.summary.version=Version:
+
+# LOCALIZATION NOTE (netmonitor.summary.editAndResend): This is the label displayed
+# on the button in the headers tab that opens a form to edit and resend the currently
+# displayed request
+netmonitor.summary.editAndResend=Edit and Resend
+
+# LOCALIZATION NOTE (netmonitor.summary.rawHeaders): This is the label displayed
+# on the button in the headers tab that toggle view for raw request/response headers
+# from the currently displayed request
+netmonitor.summary.rawHeaders=Raw headers
+
+# LOCALIZATION NOTE (netmonitor.summary.rawHeaders.requestHeaders): This is the label displayed
+# in the network details headers tab identifying the raw request headers textarea
+netmonitor.summary.rawHeaders.requestHeaders=Request headers:
+
+# LOCALIZATION NOTE (netmonitor.summary.rawHeaders.responseHeaders): This is the label displayed
+# in the network details headers tab identifying the raw response headers textarea
+netmonitor.summary.rawHeaders.responseHeaders=Response headers:
+
+# LOCALIZATION NOTE (netmonitor.summary.size): This is the label displayed
+# in the network details headers tab identifying the headers size.
+netmonitor.summary.size=Headers size:
+
+# LOCALIZATION NOTE (netmonitor.response.name): This is the label displayed
+# in the network details response tab identifying an image's file name.
+netmonitor.response.name=Name:
+
+# LOCALIZATION NOTE (netmonitor.response.dimensions): This is the label displayed
+# in the network details response tab identifying an image's dimensions.
+netmonitor.response.dimensions=Dimensions:
+
+# LOCALIZATION NOTE (netmonitor.response.mime): This is the label displayed
+# in the network details response tab identifying an image's mime.
+netmonitor.response.mime=MIME Type:
+
+# LOCALIZATION NOTE (netmonitor.timings.blocked): This is the label displayed
+# in the network details timings tab identifying the amount of time spent
+# in a "blocked" state.
+netmonitor.timings.blocked=Blocked:
+
+# LOCALIZATION NOTE (netmonitor.timings.dns): This is the label displayed
+# in the network details timings tab identifying the amount of time spent
+# in a "dns" state.
+netmonitor.timings.dns=DNS resolution:
+
+# LOCALIZATION NOTE (netmonitor.timings.connect): This is the label displayed
+# in the network details timings tab identifying the amount of time spent
+# in a "connect" state.
+netmonitor.timings.connect=Connecting:
+
+# LOCALIZATION NOTE (netmonitor.timings.send): This is the label displayed
+# in the network details timings tab identifying the amount of time spent
+# in a "send" state.
+netmonitor.timings.send=Sending:
+
+# LOCALIZATION NOTE (netmonitor.timings.wait): This is the label displayed
+# in the network details timings tab identifying the amount of time spent
+# in a "wait" state.
+netmonitor.timings.wait=Waiting:
+
+# LOCALIZATION NOTE (netmonitor.timings.receive): This is the label displayed
+# in the network details timings tab identifying the amount of time spent
+# in a "receive" state.
+netmonitor.timings.receive=Receiving:
+
+# LOCALIZATION NOTE (netmonitor.security.warning.cipher): A tooltip
+# for warning icon that indicates a connection uses insecure cipher suite.
+netmonitor.security.warning.cipher=The cipher used for encryption is deprecated and insecure.
+
+# LOCALIZATION NOTE (netmonitor.security.error): This is the label displayed
+# in the security tab if a security error prevented the connection.
+netmonitor.security.error=An error occured:
+
+# LOCALIZATION NOTE (netmonitor.security.protocolVersion): This is the label displayed
+# in the security tab describing TLS/SSL protocol version.
+netmonitor.security.protocolVersion=Protocol version:
+
+# LOCALIZATION NOTE (netmonitor.security.cipherSuite): This is the label displayed
+# in the security tab describing the cipher suite used to secure this connection.
+netmonitor.security.cipherSuite=Cipher suite:
+
+# LOCALIZATION NOTE (netmonitor.security.hsts): This is the label displayed
+# in the security tab describing the usage of HTTP Strict Transport Security.
+netmonitor.security.hsts=HTTP Strict Transport Security:
+
+# LOCALIZATION NOTE (netmonitor.security.hpkp): This is the label displayed
+# in the security tab describing the usage of Public Key Pinning.
+netmonitor.security.hpkp=Public Key Pinning:
+
+# LOCALIZATION NOTE (netmonitor.security.connection): This is the label displayed
+# in the security tab describing the section containing information related to
+# the secure connection.
+netmonitor.security.connection=Connection:
+
+# LOCALIZATION NOTE (netmonitor.security.certificate): This is the label displayed
+# in the security tab describing the server certificate section.
+netmonitor.security.certificate=Certificate:
+
+# LOCALIZATION NOTE (netmonitor.context.copyUrl): This is the label displayed
+# on the context menu that copies the selected request's url
+netmonitor.context.copyUrl=Copy URL
+
+# LOCALIZATION NOTE (netmonitor.context.copyUrl.accesskey): This is the access key
+# for the Copy URL menu item displayed in the context menu for a request
+netmonitor.context.copyUrl.accesskey=U
+
+# LOCALIZATION NOTE (netmonitor.context.copyUrlParams): This is the label displayed
+# on the context menu that copies the selected request's url parameters
+netmonitor.context.copyUrlParams=Copy URL Parameters
+
+# LOCALIZATION NOTE (netmonitor.context.copyUrlParams.accesskey): This is the access key
+# for the Copy URL Parameters menu item displayed in the context menu for a request
+netmonitor.context.copyUrlParams.accesskey=P
+
+# LOCALIZATION NOTE (netmonitor.context.copyPostData): This is the label displayed
+# on the context menu that copies the selected request's post data
+netmonitor.context.copyPostData=Copy POST Data
+
+# LOCALIZATION NOTE (netmonitor.context.copyPostData.accesskey): This is the access key
+# for the Copy POST Data menu item displayed in the context menu for a request
+netmonitor.context.copyPostData.accesskey=D
+
+# LOCALIZATION NOTE (netmonitor.context.copyAsCurl): This is the label displayed
+# on the context menu that copies the selected request as a cURL command.
+# The capitalization is part of the official name and should be used throughout all languages.
+# http://en.wikipedia.org/wiki/CURL
+netmonitor.context.copyAsCurl=Copy as cURL
+
+# LOCALIZATION NOTE (netmonitor.context.copyAsCUrl.accesskey): This is the access key
+# for the Copy as cURL menu item displayed in the context menu for a request
+netmonitor.context.copyAsCurl.accesskey=C
+
+# LOCALIZATION NOTE (netmonitor.context.copyRequestHeaders): This is the label displayed
+# on the context menu that copies the selected item's request headers
+netmonitor.context.copyRequestHeaders=Copy Request Headers
+
+# LOCALIZATION NOTE (netmonitor.context.copyRequestHeaders.accesskey): This is the access key
+# for the Copy Request Headers menu item displayed in the context menu for a request
+netmonitor.context.copyRequestHeaders.accesskey=Q
+
+# LOCALIZATION NOTE (netmonitor.context.copyResponseHeaders): This is the label displayed
+# on the context menu that copies the selected item's response headers
+netmonitor.context.copyResponseHeaders=Copy Response Headers
+
+# LOCALIZATION NOTE (netmonitor.context.copyResponseHeaders.accesskey): This is the access key
+# for the Copy Response Headers menu item displayed in the context menu for a response
+netmonitor.context.copyResponseHeaders.accesskey=S
+
+# LOCALIZATION NOTE (netmonitor.context.copyResponse): This is the label displayed
+# on the context menu that copies the selected response as a string
+netmonitor.context.copyResponse=Copy Response
+
+# LOCALIZATION NOTE (netmonitor.context.copyRespose.accesskey): This is the access key
+# for the Copy Response menu item displayed in the context menu for a request
+netmonitor.context.copyResponse.accesskey=R
+
+# LOCALIZATION NOTE (netmonitor.context.copyImageAsDataUri): This is the label displayed
+# on the context menu that copies the selected image as data uri
+netmonitor.context.copyImageAsDataUri=Copy Image as Data URI
+
+# LOCALIZATION NOTE (netmonitor.context.copyImageAsDataUri.accesskey): This is the access key
+# for the Copy Image As Data URI menu item displayed in the context menu for a request
+netmonitor.context.copyImageAsDataUri.accesskey=I
+
+# LOCALIZATION NOTE (netmonitor.context.copyAllAsHar): This is the label displayed
+# on the context menu that copies all as HAR format
+netmonitor.context.copyAllAsHar=Copy All As HAR
+
+# LOCALIZATION NOTE (netmonitor.context.copyAllAsHar.accesskey): This is the access key
+# for the Copy All As HAR menu item displayed in the context menu for a network panel
+netmonitor.context.copyAllAsHar.accesskey=O
+
+# LOCALIZATION NOTE (netmonitor.context.saveAllAsHar): This is the label displayed
+# on the context menu that saves all as HAR format
+netmonitor.context.saveAllAsHar=Save All As HAR
+
+# LOCALIZATION NOTE (netmonitor.context.saveAllAsHar.accesskey): This is the access key
+# for the Save All As HAR menu item displayed in the context menu for a network panel
+netmonitor.context.saveAllAsHar.accesskey=H
+
+# LOCALIZATION NOTE (netmonitor.context.editAndResend): This is the label displayed
+# on the context menu that opens a form to edit and resend the currently
+# displayed request
+netmonitor.context.editAndResend=Edit and Resend
+
+# LOCALIZATION NOTE (netmonitor.context.editAndResend.accesskey): This is the access key
+# for the "Edit and Resend" menu item displayed in the context menu for a request
+netmonitor.context.editAndResend.accesskey=E
+
+# LOCALIZATION NOTE (netmonitor.context.newTab): This is the label
+# for the Open in New Tab menu item displayed in the context menu of the
+# network container
+netmonitor.context.newTab=Open in New Tab
+
+# LOCALIZATION NOTE (netmonitor.context.newTab.accesskey): This is the access key
+# for the Open in New Tab menu item displayed in the context menu of the
+# network container
+netmonitor.context.newTab.accesskey=T
+
+# LOCALIZATION NOTE (netmonitor.context.perfTools): This is the label displayed
+# on the context menu that shows the performance analysis tools
+netmonitor.context.perfTools=Start Performance Analysis…
+
+# LOCALIZATION NOTE (netmonitor.context.perfTools.accesskey): This is the access key
+# for the performance analysis menu item displayed in the context menu for a request
+netmonitor.context.perfTools.accesskey=A
+
+# LOCALIZATION NOTE (netmonitor.custom.newRequest): This is the label displayed
+# as the title of the new custom request form
+netmonitor.custom.newRequest=New Request
+
+# LOCALIZATION NOTE (netmonitor.custom.query): This is the label displayed
+# above the query string entry in the custom request form
+netmonitor.custom.query=Query String:
+
+# LOCALIZATION NOTE (netmonitor.custom.headers): This is the label displayed
+# above the request headers entry in the custom request form
+netmonitor.custom.headers=Request Headers:
+
+# LOCALIZATION NOTE (netmonitor.custom.postData): This is the label displayed
+# above the request body entry in the custom request form
+netmonitor.custom.postData=Request Body:
+
+# LOCALIZATION NOTE (netmonitor.custom.send): This is the label displayed
+# on the button which sends the custom request
+netmonitor.custom.send=Send
+
+# LOCALIZATION NOTE (netmonitor.custom.cancel): This is the label displayed
+# on the button which cancels and closes the custom request form
+netmonitor.custom.cancel=Cancel
+
+# LOCALIZATION NOTE (netmonitor.backButton): This is the label displayed
+# on the button which exists the performance statistics view
+netmonitor.backButton=Back
diff --git a/devtools/client/locales/en-US/performance.dtd b/devtools/client/locales/en-US/performance.dtd
new file mode 100644
index 000000000..4009ae104
--- /dev/null
+++ b/devtools/client/locales/en-US/performance.dtd
@@ -0,0 +1,137 @@
+<!-- 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/. -->
+
+<!-- LOCALIZATION NOTE : FILE This file contains the Performance strings -->
+<!-- LOCALIZATION NOTE : FILE Do not translate commandkey -->
+
+<!-- LOCALIZATION NOTE : FILE The correct localization of this file might be to
+ - keep it in English, or another language commonly spoken among web developers.
+ - You want to make that choice consistent across the developer tools.
+ - A good criteria is the language in which you'd find the best
+ - documentation on web development on the web. -->
+
+<!-- LOCALIZATION NOTE (performanceUI.bufferStatusTooltip): This string
+ - is displayed as the tooltip for the buffer capacity during a recording. -->
+<!ENTITY performanceUI.bufferStatusTooltip "The profiler stores samples in a circular buffer, and once the buffer reaches the limit for a recording, newer samples begin to overwrite samples at the beginning of the recording.">
+
+<!-- LOCALIZATION NOTE (performanceUI.disabledRealTime.nonE10SBuild): This string
+ - is displayed as a message for why the real time overview graph is disabled
+ - when running on a non-multiprocess build. -->
+<!ENTITY performanceUI.disabledRealTime.nonE10SBuild "Realtime recording data disabled on non-multiprocess Firefox.">
+
+<!-- LOCALIZATION NOTE (performanceUI.disabledRealTime.disabledE10S): This string
+ - is displayed as a message for why the real time overview graph is disabled
+ - when running on a build that can run multiprocess Firefox, but just is not enabled. -->
+<!ENTITY performanceUI.disabledRealTime.disabledE10S "Enable multiprocess Firefox in preferences for rendering recording data in realtime.">
+
+<!-- LOCALIZATION NOTE (performanceUI.bufferStatusFull): This string
+ - is displayed when the profiler's circular buffer has started to overlap. -->
+<!ENTITY performanceUI.bufferStatusFull "The buffer is full. Older samples are now being overwritten.">
+
+<!-- LOCALIZATION NOTE (performanceUI.loadingNotice): This is the label shown
+ - in the details view while the profiler is unavailable, for example, while
+ - in Private Browsing mode. -->
+<!ENTITY performanceUI.unavailableNoticePB "Recording a profile is currently unavailable. Please close all private browsing windows and try again.">
+
+<!-- LOCALIZATION NOTE (performanceUI.loadingNotice): This is the label shown
+ - in the details view while loading a profile. -->
+<!ENTITY performanceUI.loadingNotice "Loading…">
+
+<!-- LOCALIZATION NOTE (performanceUI.toolbar.*): These strings are displayed
+ - in the toolbar on buttons that select which view is currently shown. -->
+<!ENTITY performanceUI.toolbar.waterfall "Waterfall">
+<!ENTITY performanceUI.toolbar.waterfall.tooltiptext "Shows the different operations the browser is performing during the recording, laid out sequentially as a waterfall.">
+<!ENTITY performanceUI.toolbar.js-calltree "Call Tree">
+<!ENTITY performanceUI.toolbar.js-calltree.tooltiptext "Highlights JavaScript functions where the browser spent most time during the recording.">
+<!ENTITY performanceUI.toolbar.memory-calltree "Allocations">
+<!ENTITY performanceUI.toolbar.allocations.tooltiptext "Shows where memory was allocated during the recording.">
+<!ENTITY performanceUI.toolbar.js-flamegraph "JS Flame Chart">
+<!ENTITY performanceUI.toolbar.js-flamegraph.tooltiptext "Shows the JavaScript call stack over the course of the recording.">
+<!ENTITY performanceUI.toolbar.memory-flamegraph "Allocations Flame Chart">
+
+<!-- LOCALIZATION NOTE (performanceUI.table.*): These strings are displayed
+ - in the call tree headers for a recording. -->
+<!ENTITY performanceUI.table.totalDuration "Total Time">
+<!ENTITY performanceUI.table.totalDuration.tooltip "The amount of time spent in this function and functions it calls.">
+<!ENTITY performanceUI.table.selfDuration "Self Time">
+<!ENTITY performanceUI.table.selfDuration.tooltip "The amount of time spent only within this function.">
+<!ENTITY performanceUI.table.totalPercentage "Total Cost">
+<!ENTITY performanceUI.table.totalPercentage.tooltip "The percentage of time spent in this function and functions it calls.">
+<!ENTITY performanceUI.table.selfPercentage "Self Cost">
+<!ENTITY performanceUI.table.selfPercentage.tooltip "The percentage of time spent only within this function.">
+<!ENTITY performanceUI.table.samples "Samples">
+<!ENTITY performanceUI.table.samples.tooltip "The number of times this function was on the stack when the profiler took a sample.">
+<!ENTITY performanceUI.table.function "Function">
+<!ENTITY performanceUI.table.function.tooltip "The name and source location of the sampled function.">
+<!ENTITY performanceUI.table.totalAlloc "Total Sampled Allocations">
+<!ENTITY performanceUI.table.totalAlloc.tooltip "The total number of Object allocations sampled at this location and in callees.">
+<!ENTITY performanceUI.table.selfAlloc "Self Sampled Allocations">
+<!ENTITY performanceUI.table.selfAlloc.tooltip "The number of Object allocations sampled at this location.">
+
+<!-- LOCALIZATION NOTE (performanceUI.options.filter.tooltiptext): This string
+ - is displayed next to the filter button-->
+<!ENTITY performanceUI.options.filter.tooltiptext "Select what data to display in the timeline">
+
+<!-- LOCALIZATION NOTE (performanceUI.options.gear.tooltiptext): This is the
+ - tooltip for the options button. -->
+<!ENTITY performanceUI.options.gear.tooltiptext "Configure performance preferences.">
+
+<!-- LOCALIZATION NOTE (performanceUI.invertTree): This is the label shown next to
+ - a checkbox that inverts and un-inverts the profiler's call tree. -->
+<!ENTITY performanceUI.invertTree "Invert Call Tree">
+<!ENTITY performanceUI.invertTree.tooltiptext "Inverting the call tree displays the profiled call paths starting from the youngest frames and expanding out to the older frames.">
+
+<!-- LOCALIZATION NOTE (performanceUI.invertFlameGraph): This is the label shown next to
+ - a checkbox that inverts and un-inverts the profiler's flame graph. -->
+<!ENTITY performanceUI.invertFlameGraph "Invert Flame Chart">
+<!ENTITY performanceUI.invertFlameGraph.tooltiptext "Inverting the flame chart displays the profiled call paths starting from the youngest frames and expanding out to the older frames.">
+
+<!-- LOCALIZATION NOTE (performanceUI.showPlatformData): This is the
+ - label for the checkbox that toggles whether or not Gecko platform data
+ - is displayed in the profiler. -->
+<!ENTITY performanceUI.showPlatformData "Show Gecko Platform Data">
+<!ENTITY performanceUI.showPlatformData.tooltiptext "Showing platform data enables the JavaScript Profiler reports to include Gecko platform symbols.">
+
+<!-- LOCALIZATION NOTE (performanceUI.showJITOptimizations): This string
+ - is displayed next to a checkbox determining whether or not JIT optimization data
+ - should be displayed. -->
+<!ENTITY performanceUI.showJITOptimizations "Show JIT Optimizations">
+<!ENTITY performanceUI.showJITOptimizations.tooltiptext "Show JIT optimization data sampled in each JavaScript frame.">
+
+<!-- LOCALIZATION NOTE (performanceUI.flattenTreeRecursion): This is the
+ - label for the checkbox that toggles the flattening of tree recursion in inspected
+ - functions in the profiler. -->
+<!ENTITY performanceUI.flattenTreeRecursion "Flatten Tree Recursion">
+<!ENTITY performanceUI.flattenTreeRecursion.tooltiptext "Flatten recursion when inspecting functions.">
+
+<!-- LOCALIZATION NOTE (performanceUI.enableMemory): This string
+ - is displayed next to a checkbox determining whether or not memory
+ - measurements are enabled. -->
+<!ENTITY performanceUI.enableMemory "Record Memory">
+<!ENTITY performanceUI.enableMemory.tooltiptext "Record memory consumption while profiling.">
+
+<!-- LOCALIZATION NOTE (performanceUI.enableAllocations): This string
+ - is displayed next to a checkbox determining whether or not allocation
+ - measurements are enabled. -->
+<!ENTITY performanceUI.enableAllocations "Record Allocations">
+<!ENTITY performanceUI.enableAllocations.tooltiptext "Record Object allocations while profiling.">
+
+<!-- LOCALIZATION NOTE (performanceUI.enableFramerate): This string
+ - is displayed next to a checkbox determining whether or not framerate
+ - is recorded. -->
+<!ENTITY performanceUI.enableFramerate "Record Framerate">
+<!ENTITY performanceUI.enableFramerate.tooltiptext "Record framerate while profiling.">
+
+<!-- LOCALIZATION NOTE (performanceUI.console.recordingNoticeStart/recordingNoticeEnd):
+ - This string is displayed when a recording is selected that started via console.profile.
+ - Wraps the command used to start, like "Currently recording via console.profile("label")" -->
+<!ENTITY performanceUI.console.recordingNoticeStart "Currently recording via">
+<!ENTITY performanceUI.console.recordingNoticeEnd "">
+
+<!-- LOCALIZATION NOTE (performanceUI.console.stopCommandStart/stopCommandEnd):
+ - This string is displayed when a recording is selected that started via console.profile.
+ - Indicates how to stop the recording, wrapping the command, like
+ - "Stop recording by entering console.profileEnd("label") into the console." -->
+<!ENTITY performanceUI.console.stopCommandStart "Stop recording by entering">
+<!ENTITY performanceUI.console.stopCommandEnd "into the console.">
diff --git a/devtools/client/locales/en-US/performance.properties b/devtools/client/locales/en-US/performance.properties
new file mode 100644
index 000000000..86ddc29b5
--- /dev/null
+++ b/devtools/client/locales/en-US/performance.properties
@@ -0,0 +1,160 @@
+# 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/.
+
+# LOCALIZATION NOTE These strings are used inside the Performance Tools
+# which is available from the Web Developer sub-menu -> 'Performance'.
+# The correct localization of this file might be to keep it in
+# English, or another language commonly spoken among web developers.
+# You want to make that choice consistent across the developer tools.
+# A good criteria is the language in which you'd find the best
+# documentation on web development on the web.
+
+# LOCALIZATION NOTE (noRecordingsText): The text to display in the
+# recordings menu when there are no recorded profiles yet.
+noRecordingsText=There are no profiles yet.
+
+# LOCALIZATION NOTE (recordingsList.itemLabel):
+# This string is displayed in the recordings list of the Performance Tools,
+# identifying a set of function calls. %S represents the number of recording,
+# iterating for every new recording, resulting in "Recording #1", "Recording #2", etc.
+recordingsList.itemLabel=Recording #%S
+
+# LOCALIZATION NOTE (recordingsList.recordingLabel):
+# This string is displayed in the recordings list of the Performance Tools,
+# for an item that has not finished recording.
+recordingsList.recordingLabel=In progress…
+
+# LOCALIZATION NOTE (recordingsList.loadingLabel):
+# This string is displayed in the recordings list of the Performance Tools,
+# for an item that is finished and is loading.
+recordingsList.loadingLabel=Loading…
+
+# LOCALIZATION NOTE (recordingsList.durationLabel):
+# This string is displayed in the recordings list of the Performance Tools,
+# for an item that has finished recording.
+recordingsList.durationLabel=%S ms
+
+# LOCALIZATION NOTE (recordingsList.saveLabel):
+# This string is displayed in the recordings list of the Performance Tools,
+# for saving an item to disk.
+recordingsList.saveLabel=Save
+
+# LOCALIZATION NOTE (graphs.fps):
+# This string is displayed in the framerate graph of the Performance Tools,
+# as the unit used to measure frames per second. This label should be kept
+# AS SHORT AS POSSIBLE so it doesn't obstruct important parts of the graph.
+graphs.fps=fps
+
+# LOCALIZATION NOTE (graphs.ms):
+# This string is displayed in the flamegraph of the Performance Tools,
+# as the unit used to measure time (in milliseconds). This label should be kept
+# AS SHORT AS POSSIBLE so it doesn't obstruct important parts of the graph.
+graphs.ms=ms
+
+# LOCALIZATION NOTE (graphs.memory):
+# This string is displayed in the memory graph of the Performance tool,
+# as the unit used to memory consumption. This label should be kept
+# AS SHORT AS POSSIBLE so it doesn't obstruct important parts of the graph.
+graphs.memory=MB
+
+# LOCALIZATION NOTE (category.*):
+# These strings are displayed in the categories graph of the Performance Tools,
+# as the legend for each block in every bar. These labels should be kept
+# AS SHORT AS POSSIBLE so they don't obstruct important parts of the graph.
+category.other=Gecko
+category.css=Styles
+category.js=JIT
+category.gc=GC
+category.network=Network
+category.graphics=Graphics
+category.storage=Storage
+category.events=Input & Events
+category.tools=Tools
+
+# LOCALIZATION NOTE (table.bytes):
+# This string is displayed in the call tree after bytesize units.
+# %S represents the value in bytes.
+table.bytes=%S B
+
+# LOCALIZATION NOTE (table.ms2):
+# This string is displayed in the call tree after units of time in milliseconds.
+# %S represents the value in milliseconds.
+table.ms2=%S ms
+
+# LOCALIZATION NOTE (table.percentage3):
+# This string is displayed in the call tree after units representing percentages.
+# %S represents the value in percentage with two decimal points, localized.
+# there are two "%" after %S to escape and display "%"
+table.percentage3=%S%%
+
+# LOCALIZATION NOTE (table.root):
+# This string is displayed in the call tree for the root node.
+table.root=(root)
+
+# LOCALIZATION NOTE (table.idle):
+# This string is displayed in the call tree for the idle blocks.
+table.idle=(idle)
+
+# LOCALIZATION NOTE (table.url.tooltiptext):
+# This string is displayed in the call tree as the tooltip text for the url
+# labels which, when clicked, jump to the debugger.
+table.url.tooltiptext=View source in Debugger
+
+# LOCALIZATION NOTE (table.view-optimizations.tooltiptext2):
+# This string is displayed in the icon displayed next to frames that
+# have optimization data
+table.view-optimizations.tooltiptext2=Frame contains JIT optimization data
+
+# LOCALIZATION NOTE (recordingsList.importDialogTitle):
+# This string is displayed as a title for importing a recoring from disk.
+recordingsList.importDialogTitle=Import recording…
+
+# LOCALIZATION NOTE (recordingsList.saveDialogTitle):
+# This string is displayed as a title for saving a recording to disk.
+recordingsList.saveDialogTitle=Save recording…
+
+# LOCALIZATION NOTE (recordingsList.saveDialogJSONFilter):
+# This string is displayed as a filter for saving a recording to disk.
+recordingsList.saveDialogJSONFilter=JSON Files
+
+# LOCALIZATION NOTE (recordingsList.saveDialogAllFilter):
+# This string is displayed as a filter for saving a recording to disk.
+recordingsList.saveDialogAllFilter=All Files
+
+# LOCALIZATION NOTE (timeline.tick):
+# This string is displayed in the timeline overview, for delimiting ticks
+# by time, in milliseconds.
+timeline.tick=%S ms
+
+# LOCALIZATION NOTE (timeline.records):
+# This string is displayed in the timeline waterfall, as a title for the menu.
+timeline.records=RECORDS
+
+# LOCALIZATION NOTE (profiler.bufferFull):
+# This string is displayed when recording, indicating how much of the
+# buffer is currently be used.
+# %S is the percentage of the buffer used -- there are two "%"s after to escape
+# the % that is actually displayed.
+# Example: "Buffer 54% full"
+profiler.bufferFull=Buffer %S%% full
+
+# LOCALIZATION NOTE (recordings.start):
+# The label shown on the main recording buttons to start recording.
+recordings.start=Start Recording Performance
+
+# LOCALIZATION NOTE (recordings.stop):
+# The label shown on the main recording buttons to stop recording.
+recordings.stop=Stop Recording Performance
+
+# LOCALIZATION NOTE (recordings.start.tooltip):
+# This string is displayed as a tooltip on a button that starts a new profile.
+recordings.start.tooltip=Toggle the recording state of a performance recording.
+
+# LOCALIZATION NOTE (recordings.import.tooltip):
+# This string is displayed on a button that opens a dialog to import a saved profile data file.
+recordings.import.tooltip=Import…
+
+# LOCALIZATION NOTE (recordings.clear.tooltip):
+# This string is displayed on a button that removes all the recordings.
+recordings.clear.tooltip=Clear
diff --git a/devtools/client/locales/en-US/projecteditor.properties b/devtools/client/locales/en-US/projecteditor.properties
new file mode 100644
index 000000000..bca7326f1
--- /dev/null
+++ b/devtools/client/locales/en-US/projecteditor.properties
@@ -0,0 +1,88 @@
+# 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/.
+
+# LOCALIZATION NOTE These strings are used inside the ProjectEditor component
+# which is used for editing files in a directory and is used inside the
+# App Manager.
+# The correct localization of this file might be to keep it in
+# English, or another language commonly spoken among web developers.
+# You want to make that choice consistent across the developer tools.
+# A good criteria is the language in which you'd find the best
+# documentation on web development on the web.
+
+# LOCALIZATION NOTE (projecteditor.confirmUnsavedTitle):
+# This string is displayed as as the title of the confirm prompt that checks
+# to make sure if the project can be closed/switched without saving changes
+projecteditor.confirmUnsavedTitle=Unsaved Changes
+
+# LOCALIZATION NOTE (projecteditor.confirmUnsavedLabel2):
+# This string is displayed as the message of the confirm prompt that checks
+# to make sure if the project can be closed/switched without saving changes
+projecteditor.confirmUnsavedLabel2=You have unsaved changes that will be lost. Are you sure you want to continue?
+
+# LOCALIZATION NOTE (projecteditor.deleteLabel):
+# This string is displayed as a context menu item for allowing the selected
+# file / folder to be deleted.
+projecteditor.deleteLabel=Delete
+
+# LOCALIZATION NOTE (projecteditor.deletePromptTitle):
+# This string is displayed as as the title of the confirm prompt that checks
+# to make sure if a file or folder should be removed.
+projecteditor.deletePromptTitle=Delete
+
+# LOCALIZATION NOTE (projecteditor.deleteFolderPromptMessage):
+# This string is displayed as as the message of the confirm prompt that checks
+# to make sure if a folder should be removed.
+projecteditor.deleteFolderPromptMessage=Are you sure you want to delete this folder?
+
+# LOCALIZATION NOTE (projecteditor.deleteFilePromptMessage):
+# This string is displayed as as the message of the confirm prompt that checks
+# to make sure if a file should be removed.
+projecteditor.deleteFilePromptMessage=Are you sure you want to delete this file?
+
+# LOCALIZATION NOTE (projecteditor.newLabel):
+# This string is displayed as a menu item for adding a new file to
+# the directory.
+projecteditor.newLabel=New…
+
+# LOCALIZATION NOTE (projecteditor.renameLabel):
+# This string is displayed as a menu item for renaming a file in
+# the directory.
+projecteditor.renameLabel=Rename
+
+# LOCALIZATION NOTE (projecteditor.saveLabel):
+# This string is displayed as a menu item for saving the current file.
+projecteditor.saveLabel=Save
+
+# LOCALIZATION NOTE (projecteditor.saveAsLabel):
+# This string is displayed as a menu item for saving the current file
+# with a new name.
+projecteditor.saveAsLabel=Save As…
+
+# LOCALIZATION NOTE (projecteditor.selectFileLabel):
+# This string is displayed as the title on the file picker when saving a file.
+projecteditor.selectFileLabel=Select a File
+
+# LOCALIZATION NOTE (projecteditor.openFolderLabel):
+# This string is displayed as the title on the file picker when opening a folder.
+projecteditor.openFolderLabel=Select a Folder
+
+# LOCALIZATION NOTE (projecteditor.openFileLabel):
+# This string is displayed as the title on the file picker when opening a file.
+projecteditor.openFileLabel=Open a File
+
+# LOCALIZATION NOTE (projecteditor.find.commandkey): This is the key to use in
+# conjunction with accel (Command on Mac or Ctrl on other platforms) to search
+# text in the files.
+projecteditor.find.commandkey=F
+
+# LOCALIZATION NOTE (projecteditor.save.commandkey): This is the key to use in
+# conjunction with accel (Command on Mac or Ctrl on other platforms) to
+# save the file. It is used with accel+shift to "save as".
+projecteditor.save.commandkey=S
+
+# LOCALIZATION NOTE (projecteditor.new.commandkey): This is the key to use in
+# conjunction with accel (Command on Mac or Ctrl on other platforms) to
+# create a new file.
+projecteditor.new.commandkey=N
diff --git a/devtools/client/locales/en-US/responsive.properties b/devtools/client/locales/en-US/responsive.properties
new file mode 100644
index 000000000..4347ea0b4
--- /dev/null
+++ b/devtools/client/locales/en-US/responsive.properties
@@ -0,0 +1,81 @@
+# 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/.
+
+# LOCALIZATION NOTE These strings are used inside the Responsive Design Mode,
+# available from the Web Developer sub-menu -> 'Responsive Design Mode'.
+#
+# The correct localization of this file might be to keep it in
+# English, or another language commonly spoken among web developers.
+# You want to make that choice consistent across the developer tools.
+# A good criteria is the language in which you'd find the best
+# documentation on web development on the web.
+
+# LOCALIZATION NOTE (responsive.editDeviceList): option displayed in the device
+# selector
+responsive.editDeviceList=Edit list…
+
+# LOCALIZATION NOTE (responsive.exit): tooltip text of the exit button.
+responsive.exit=Close Responsive Design Mode
+
+# LOCALIZATION NOTE (responsive.deviceListLoading): placeholder text for
+# device selector when it's still fetching devices
+responsive.deviceListLoading=Loading…
+
+# LOCALIZATION NOTE (responsive.deviceListError): placeholder text for
+# device selector when an error occurred
+responsive.deviceListError=No list available
+
+# LOCALIZATION NOTE (responsive.done): button text in the device list modal
+responsive.done=Done
+
+# LOCALIZATION NOTE (responsive.noDeviceSelected): placeholder text for the
+# device selector
+responsive.noDeviceSelected=no device selected
+
+# LOCALIZATION NOTE (responsive.title): the title displayed in the global
+# toolbar
+responsive.title=Responsive Design Mode
+
+# LOCALIZATION NOTE (responsive.enableTouch): tooltip text for the touch
+# simulation button when it's disabled
+responsive.enableTouch=Enable touch simulation
+
+# LOCALIZATION NOTE (responsive.disableTouch): tooltip text for the touch
+# simulation button when it's enabled
+responsive.disableTouch=Disable touch simulation
+
+# LOCALIZATION NOTE (responsive.screenshot): tooltip of the screenshot button.
+responsive.screenshot=Take a screenshot of the viewport
+
+# LOCALIZATION NOTE (responsive.screenshotGeneratedFilename): The auto generated
+# filename.
+# The first argument (%1$S) is the date string in yyyy-mm-dd format and the
+# second argument (%2$S) is the time string in HH.MM.SS format.
+responsive.screenshotGeneratedFilename=Screen Shot %1$S at %2$S
+
+# LOCALIZATION NOTE (responsive.remoteOnly): Message displayed in the tab's
+# notification box if a user tries to open Responsive Design Mode in a
+# non-remote tab.
+responsive.remoteOnly=Responsive Design Mode is only available for remote browser tabs, such as those used for web content in multi-process Firefox.
+
+# LOCALIZATION NOTE (responsive.noContainerTabs): Message displayed in the tab's
+# notification box if a user tries to open Responsive Design Mode in a
+# container tab.
+responsive.noContainerTabs=Responsive Design Mode is currently unavailable in container tabs.
+
+# LOCALIZATION NOTE (responsive.noThrottling): UI option in a menu to configure
+# network throttling. This option is the default and disables throttling so you
+# just have normal network conditions. There is not very much room in the UI
+# so a short string would be best if possible.
+responsive.noThrottling=No throttling
+
+# LOCALIZATION NOTE (responsive.devicePixelRatio): tooltip for the
+# DevicePixelRatio (DPR) dropdown when is enabled.
+responsive.devicePixelRatio=Device Pixel Ratio
+
+# LOCALIZATION NOTE (responsive.autoDPR): tooltip for the DevicePixelRatio
+# (DPR) dropdown when is disabled because a device is selected.
+# The argument (%1$S) is the selected device (e.g. iPhone 6) that set
+# automatically the DPR value.
+responsive.autoDPR=DPR automatically set by %1$S
diff --git a/devtools/client/locales/en-US/responsiveUI.properties b/devtools/client/locales/en-US/responsiveUI.properties
new file mode 100644
index 000000000..6a4930246
--- /dev/null
+++ b/devtools/client/locales/en-US/responsiveUI.properties
@@ -0,0 +1,69 @@
+# 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/.
+
+# LOCALIZATION NOTE These strings are used inside the Responsive Mode
+# which is available from the Web Developer sub-menu -> 'Responsive Mode'.
+#
+# The correct localization of this file might be to keep it in
+# English, or another language commonly spoken among web developers.
+# You want to make that choice consistent across the developer tools.
+# A good criteria is the language in which you'd find the best
+# documentation on web development on the web.
+
+
+# LOCALIZATION NOTE (responsiveUI.rotate2): tooltip of the rotate button.
+responsiveUI.rotate2=Rotate
+
+# LOCALIZATION NOTE (responsiveUI.screenshot): tooltip of the screenshot button.
+responsiveUI.screenshot=Screenshot
+
+# LOCALIZATION NOTE (responsiveUI.userAgentPlaceholder): placeholder for the user agent input.
+responsiveUI.userAgentPlaceholder=Custom User Agent
+
+# LOCALIZATION NOTE (responsiveUI.screenshotGeneratedFilename): The auto generated filename.
+# The first argument (%1$S) is the date string in yyyy-mm-dd format and the second
+# argument (%2$S) is the time string in HH.MM.SS format.
+responsiveUI.screenshotGeneratedFilename=Screen Shot %1$S at %2$S
+
+# LOCALIZATION NOTE (responsiveUI.touch): tooltip of the touch button.
+responsiveUI.touch=Simulate touch events (page reload might be needed)
+
+# LOCALIZATION NOTE (responsiveUI.addPreset): label of the add preset button.
+responsiveUI.addPreset=Add Preset
+
+# LOCALIZATION NOTE (responsiveUI.removePreset): label of the remove preset button.
+responsiveUI.removePreset=Remove Preset
+
+# LOCALIZATION NOTE (responsiveUI.customResolution): label of the first item
+# in the menulist at the beginning of the toolbar. For %S is replace with the
+# current size of the page. For example: "400x600".
+responsiveUI.customResolution=%S (custom)
+
+# LOCALIZATION NOTE (responsiveUI.namedResolution): label of custom items with a name
+# in the menulist of the toolbar.
+# For example: "320x480 (phone)".
+responsiveUI.namedResolution=%S (%S)
+
+# LOCALIZATION NOTE (responsiveUI.customNamePromptTitle1): prompt title when asking
+# the user to specify a name for a new custom preset.
+responsiveUI.customNamePromptTitle1=Responsive Design Mode
+
+# LOCALIZATION NOTE (responsiveUI.close1): tooltip text of the close button.
+responsiveUI.close1=Leave Responsive Design Mode
+
+# LOCALIZATION NOTE (responsiveUI.customNamePromptMsg): prompt message when asking
+# the user to specify a name for a new custom preset.
+responsiveUI.customNamePromptMsg=Give a name to the %Sx%S preset
+
+# LOCALIZATION NOTE (responsiveUI.resizer): tooltip showed when
+# overring the resizers.
+responsiveUI.resizerTooltip=Use the Control key for more precision. Use Shift key for rounded sizes.
+
+# LOCALIZATION NOTE (responsiveUI.needReload): notification that appears
+# when touch events are enabled
+responsiveUI.needReload=If touch event listeners have been added earlier, the page needs to be reloaded.
+responsiveUI.notificationReload=Reload
+responsiveUI.notificationReload_accesskey=R
+responsiveUI.dontShowReloadNotification=Never show again
+responsiveUI.dontShowReloadNotification_accesskey=N
diff --git a/devtools/client/locales/en-US/scratchpad.dtd b/devtools/client/locales/en-US/scratchpad.dtd
new file mode 100644
index 000000000..d9f9a19c2
--- /dev/null
+++ b/devtools/client/locales/en-US/scratchpad.dtd
@@ -0,0 +1,155 @@
+<!-- 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/. -->
+
+<!-- LOCALIZATION NOTE : FILE This file contains the Scratchpad window strings -->
+<!-- LOCALIZATION NOTE : FILE Do not translate commandkeys -->
+
+<!-- LOCALIZATION NOTE : FILE The correct localization of this file might be to
+ - keep it in English, or another language commonly spoken among web developers.
+ - You want to make that choice consistent across the developer tools.
+ - A good criteria is the language in which you'd find the best
+ - documentation on web development on the web. -->
+
+<!-- LOCALIZATION NOTE (scratchpad.title):
+ - The Scratchpad is intended to provide a simple text editor for creating
+ - and evaluating bits of JavaScript code for the purposes of function
+ - prototyping, experimentation and convenient scripting.
+ -
+ - It's quite possible that you won't have a good analogue for the word
+ - "Scratchpad" in your locale. You should feel free to find a close
+ - approximation to it or choose a word (or words) that means
+ - "simple discardable text editor". -->
+<!ENTITY window.title "Scratchpad">
+
+<!ENTITY fileMenu.label "File">
+<!ENTITY fileMenu.accesskey "F">
+
+<!ENTITY newWindowCmd.label "New Window">
+<!ENTITY newWindowCmd.accesskey "N">
+<!ENTITY newWindowCmd.commandkey "n">
+
+<!ENTITY openFileCmd.label "Open File…">
+<!ENTITY openFileCmd.accesskey "O">
+<!ENTITY openFileCmd.commandkey "o">
+
+<!ENTITY openRecentMenu.label "Open Recent">
+<!ENTITY openRecentMenu.accesskey "R">
+
+<!ENTITY revertCmd.label "Revert…">
+<!ENTITY revertCmd.accesskey "t">
+
+<!ENTITY saveFileCmd.label "Save">
+<!ENTITY saveFileCmd.accesskey "S">
+<!ENTITY saveFileCmd.commandkey "s">
+
+<!ENTITY saveFileAsCmd.label "Save As…">
+<!ENTITY saveFileAsCmd.accesskey "A">
+
+<!ENTITY closeCmd.label "Close">
+<!ENTITY closeCmd.key "W">
+<!ENTITY closeCmd.accesskey "C">
+
+<!ENTITY viewMenu.label "View">
+<!ENTITY viewMenu.accesskey "V">
+
+<!ENTITY lineNumbers.label "Show Line Numbers">
+<!ENTITY lineNumbers.accesskey "L">
+
+<!ENTITY wordWrap.label "Wrap Text">
+<!ENTITY wordWrap.accesskey "W">
+
+<!ENTITY highlightTrailingSpace.label "Highlight Trailing Space">
+<!ENTITY highlightTrailingSpace.accesskey "H">
+
+<!ENTITY largerFont.label "Larger Font">
+<!ENTITY largerFont.accesskey "a">
+<!ENTITY largerFont.commandkey "+">
+<!ENTITY largerFont.commandkey2 "="> <!-- + is above this key on many keyboards -->
+
+<!ENTITY smallerFont.label "Smaller Font">
+<!ENTITY smallerFont.accesskey "M">
+<!ENTITY smallerFont.commandkey "-">
+
+<!ENTITY normalSize.label "Normal Size">
+<!ENTITY normalSize.accesskey "N">
+<!ENTITY normalSize.commandkey "0">
+
+<!ENTITY editMenu.label "Edit">
+<!ENTITY editMenu.accesskey "E">
+
+<!ENTITY run.label "Run">
+<!ENTITY run.accesskey "R">
+<!ENTITY run.key "r">
+
+<!ENTITY inspect.label "Inspect">
+<!ENTITY inspect.accesskey "I">
+<!ENTITY inspect.key "i">
+
+<!ENTITY display.label "Display">
+<!ENTITY display.accesskey "D">
+<!ENTITY display.key "l">
+
+<!ENTITY pprint.label "Pretty Print">
+<!ENTITY pprint.key "p">
+<!ENTITY pprint.accesskey "P">
+
+<!-- LOCALIZATION NOTE (environmentMenu.label, accesskey): This menu item was
+ - renamed from "Context" to avoid confusion with the right-click context
+ - menu in the text area. It refers to the JavaScript Environment (or context)
+ - the user is evaluating against. I.e., Content (current tab) or Chrome
+ - (browser).
+ -->
+<!ENTITY environmentMenu.label "Environment">
+<!ENTITY environmentMenu.accesskey "N">
+
+
+<!ENTITY contentContext.label "Content">
+<!ENTITY contentContext.accesskey "C">
+
+<!-- LOCALIZATION NOTE (browserContext.label, accesskey): This menu item is used
+ - to select an execution environment for the browser window itself as opposed
+ - to content. This is a feature for browser and addon developers and only
+ - enabled via the devtools.chrome.enabled preference. Formerly, this label
+ - was called "Chrome".
+ -->
+<!ENTITY browserContext.label "Browser">
+<!ENTITY browserContext.accesskey "B">
+
+<!-- LOCALIZATION NOTE some localizations of Windows (ex:french, german) use "?"
+ - for the help button in the menubar but Gnome does not.
+ -->
+<!ENTITY helpMenu.label "Help">
+<!ENTITY helpMenu.accesskey "H">
+<!ENTITY helpMenuWin.label "Help">
+<!ENTITY helpMenuWin.accesskey "H">
+
+<!ENTITY documentationLink.label "Scratchpad Help on MDN">
+<!ENTITY documentationLink.accesskey "D">
+
+
+<!-- LOCALIZATION NOTE (resetContext2.label): This command allows the developer
+ - to reset/clear the global object of the environment where the code executes.
+ -->
+<!ENTITY resetContext2.label "Reset Variables">
+<!ENTITY resetContext2.accesskey "T">
+
+<!ENTITY reloadAndRun.label "Reload And Run">
+<!ENTITY reloadAndRun.accesskey "E">
+<!ENTITY reloadAndRun.key "r">
+
+<!ENTITY executeMenu.label "Execute">
+<!ENTITY executeMenu.accesskey "X">
+
+<!-- LOCALIZATION NOTE (errorConsoleCmd.commandkey): This command key launches
+ - the browser Error Console, the key should be identical to the property of
+ - the same name in browser.dtd.
+ -->
+<!ENTITY errorConsoleCmd.commandkey "j">
+
+<!-- LOCALIZATION NOTE (evalFunction.label): This command allows the developer
+ - to evaluate the top-level function that the cursor is currently at.
+ -->
+<!ENTITY evalFunction.label "Evaluate Current Function">
+<!ENTITY evalFunction.accesskey "v">
+<!ENTITY evalFunction.key "e">
diff --git a/devtools/client/locales/en-US/scratchpad.properties b/devtools/client/locales/en-US/scratchpad.properties
new file mode 100644
index 000000000..250b0cb2f
--- /dev/null
+++ b/devtools/client/locales/en-US/scratchpad.properties
@@ -0,0 +1,105 @@
+# 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/.
+
+# LOCALIZATION NOTE These strings are used inside the JavaScript scratchpad
+# which is available from the Web Developer sub-menu -> 'Scratchpad'.
+#
+# The correct localization of this file might be to keep it in
+# English, or another language commonly spoken among web developers.
+# You want to make that choice consistent across the developer tools.
+# A good criteria is the language in which you'd find the best
+# documentation on web development on the web.
+
+# LOCALIZATION NOTE (export.fileOverwriteConfirmation): This is displayed when
+# the user attempts to save to an already existing file.
+export.fileOverwriteConfirmation=File exists. Overwrite?
+
+# LOCALIZATION NOTE (browserWindow.unavailable): This error message is shown
+# when Scratchpad does not find any recently active main browser window.
+browserWindow.unavailable=Scratchpad cannot find any browser window to execute the code in.
+
+# LOCALIZATION NOTE (scratchpadContext.invalid): This error message is shown
+# when user tries to run an operation in Scratchpad in an unsupported context.
+scratchpadContext.invalid=Scratchpad cannot run this operation in the current mode.
+
+# LOCALIZATION NOTE (openFile.title): This is the file picker title, when you
+# open a file from Scratchpad.
+openFile.title=Open File
+
+# LOCALIZATION NOTE (openFile.failed): This is the message displayed when file
+# open fails.
+openFile.failed=Failed to read the file.
+
+# LOCALIZATION NOTE (importFromFile.convert.failed): This is the message
+# displayed when file conversion from some charset to Unicode fails.
+# %1 is the name of the charset from which the conversion failed.
+importFromFile.convert.failed=Failed to convert file to Unicode from %1$S.
+
+# LOCALIZATION NOTE (clearRecentMenuItems.label): This is the label for the
+# menuitem in the 'Open Recent'-menu which clears all recent files.
+clearRecentMenuItems.label=Clear Items
+
+# LOCALIZATION NOTE (saveFileAs): This is the file picker title, when you save
+# a file in Scratchpad.
+saveFileAs=Save File As
+
+# LOCALIZATION NOTE (saveFile.failed): This is the message displayed when file
+# save fails.
+saveFile.failed=The file save operation failed.
+
+# LOCALIZATION NOTE (confirmClose): This is message in the prompt dialog when
+# you try to close a scratchpad with unsaved changes.
+confirmClose=Do you want to save the changes you made to this scratchpad?
+
+# LOCALIZATION NOTE (confirmClose.title): This is title of the prompt dialog when
+# you try to close a scratchpad with unsaved changes.
+confirmClose.title=Unsaved Changes
+
+# LOCALIZATION NOTE (confirmRevert): This is message in the prompt dialog when
+# you try to revert unsaved content of scratchpad.
+confirmRevert=Do you want to revert the changes you made to this scratchpad?
+
+# LOCALIZATION NOTE (confirmRevert.title): This is title of the prompt dialog when
+# you try to revert unsaved content of scratchpad.
+confirmRevert.title=Revert Changes
+
+# LOCALIZATION NOTE (scratchpadIntro): This is a multi-line comment explaining
+# how to use the Scratchpad. Note that this should be a valid JavaScript
+# comment inside /* and */.
+scratchpadIntro1=/*\n * This is a JavaScript Scratchpad.\n *\n * Enter some JavaScript, then Right Click or choose from the Execute Menu:\n * 1. Run to evaluate the selected text (%1$S),\n * 2. Inspect to bring up an Object Inspector on the result (%2$S), or,\n * 3. Display to insert the result in a comment after the selection. (%3$S)\n */\n\n
+
+# LOCALIZATION NOTE (notification.browserContext): This is the message displayed
+# over the top of the editor when the user has switched to browser context.
+browserContext.notification=This scratchpad executes in the Browser context.
+
+# LOCALIZATION NOTE (help.openDocumentationPage): This returns a localized link with
+# documentation for Scratchpad on MDN.
+help.openDocumentationPage=https://developer.mozilla.org/en/Tools/Scratchpad
+
+# LOCALIZATION NOTE (scratchpad.statusBarLineCol): Line, Column
+# information displayed in statusbar when selection is made in
+# Scratchpad.
+scratchpad.statusBarLineCol = Line %1$S, Col %2$S
+
+# LOCALIZATION NOTE (fileExists.notification): This is the message displayed
+# over the top of the the editor when a file does not exist.
+fileNoLongerExists.notification=This file no longer exists.
+
+# LOCALIZATION NOTE (propertiesFilterPlaceholder): this is the text that
+# appears in the filter text box for the properties view container.
+propertiesFilterPlaceholder=Filter properties
+
+# LOCALIZATION NOTE (connectionTimeout): message displayed when the Remote Scratchpad
+# fails to connect to the server due to a timeout.
+connectionTimeout=Connection timeout. Check the Error Console on both ends for potential error messages. Reopen the Scratchpad to try again.
+
+# LOCALIZATION NOTE (selfxss.msg): the text that is displayed when
+# a new user of the developer tools pastes code into the console
+# %1 is the text of selfxss.okstring
+selfxss.msg=Scam Warning: Take care when pasting things you don’t understand. This could allow attackers to steal your identity or take control of your computer. Please type ‘%S’ in the scratchpad below to allow pasting.
+
+# LOCALIZATION NOTE (selfxss.msg): the string to be typed
+# in by a new user of the developer tools when they receive the sefxss.msg prompt.
+# Please avoid using non-keyboard characters here
+selfxss.okstring=allow pasting
diff --git a/devtools/client/locales/en-US/shadereditor.dtd b/devtools/client/locales/en-US/shadereditor.dtd
new file mode 100644
index 000000000..9773e6ccc
--- /dev/null
+++ b/devtools/client/locales/en-US/shadereditor.dtd
@@ -0,0 +1,32 @@
+<!-- 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/. -->
+
+<!-- LOCALIZATION NOTE : FILE This file contains the Debugger strings -->
+<!-- LOCALIZATION NOTE : FILE Do not translate commandkey -->
+
+<!-- LOCALIZATION NOTE : FILE The correct localization of this file might be to
+ - keep it in English, or another language commonly spoken among web developers.
+ - You want to make that choice consistent across the developer tools.
+ - A good criteria is the language in which you'd find the best
+ - documentation on web development on the web. -->
+
+<!-- LOCALIZATION NOTE (shaderEditorUI.vertexShader): This is the label for
+ - the pane that displays a vertex shader's source. -->
+<!ENTITY shaderEditorUI.vertexShader "Vertex Shader">
+
+<!-- LOCALIZATION NOTE (shaderEditorUI.fragmentShader): This is the label for
+ - the pane that displays a fragment shader's source. -->
+<!ENTITY shaderEditorUI.fragmentShader "Fragment Shader">
+
+<!-- LOCALIZATION NOTE (shaderEditorUI.reloadNotice1): This is the label shown
+ - on the button that triggers a page refresh. -->
+<!ENTITY shaderEditorUI.reloadNotice1 "Reload">
+
+<!-- LOCALIZATION NOTE (shaderEditorUI.reloadNotice2): This is the label shown
+ - along with the button that triggers a page refresh. -->
+<!ENTITY shaderEditorUI.reloadNotice2 "the page to be able to edit GLSL code.">
+
+<!-- LOCALIZATION NOTE (shaderEditorUI.emptyNotice): This is the label shown
+ - while the page is refreshing and the tool waits for a WebGL context. -->
+<!ENTITY shaderEditorUI.emptyNotice "Waiting for a WebGL context to be created…">
diff --git a/devtools/client/locales/en-US/shadereditor.properties b/devtools/client/locales/en-US/shadereditor.properties
new file mode 100644
index 000000000..899833c17
--- /dev/null
+++ b/devtools/client/locales/en-US/shadereditor.properties
@@ -0,0 +1,22 @@
+# 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/.
+
+# LOCALIZATION NOTE These strings are used inside the Debugger
+# which is available from the Web Developer sub-menu -> 'Debugger'.
+# The correct localization of this file might be to keep it in
+# English, or another language commonly spoken among web developers.
+# You want to make that choice consistent across the developer tools.
+# A good criteria is the language in which you'd find the best
+# documentation on web development on the web.
+
+# LOCALIZATION NOTE (shadersList.programLabel):
+# This string is displayed in the programs list of the Shader Editor,
+# identifying a set of linked GLSL shaders.
+shadersList.programLabel=Program %S
+
+# LOCALIZATION NOTE (shadersList.blackboxLabel):
+# This string is displayed in the programs list of the Shader Editor, while
+# the user hovers over the checkbox used to toggle blackboxing of a program's
+# associated fragment shader.
+shadersList.blackboxLabel=Toggle geometry visibility
diff --git a/devtools/client/locales/en-US/shared.properties b/devtools/client/locales/en-US/shared.properties
new file mode 100644
index 000000000..a4ed711fd
--- /dev/null
+++ b/devtools/client/locales/en-US/shared.properties
@@ -0,0 +1,11 @@
+# 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/.
+
+# LOCALIZATION NOTE (dimensions): This is used to display the dimensions
+# of a node or image, like 100×200.
+dimensions=%S\u00D7%S
+
+# LOCALIZATION NOTE (groupCheckbox.tooltip): This is used in the SideMenuWidget
+# as the default tooltip of a group checkbox
+sideMenu.groupCheckbox.tooltip=Toggle all checkboxes in this group \ No newline at end of file
diff --git a/devtools/client/locales/en-US/sourceeditor.dtd b/devtools/client/locales/en-US/sourceeditor.dtd
new file mode 100644
index 000000000..20252048f
--- /dev/null
+++ b/devtools/client/locales/en-US/sourceeditor.dtd
@@ -0,0 +1,19 @@
+<!-- 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/. -->
+
+<!-- LOCALIZATION NOTE : FILE This file contains the Source Editor component
+ - strings. The source editor component is used within the Scratchpad and
+ - Style Editor tools. -->
+
+<!-- LOCALIZATION NOTE : FILE Do not translate commandkeys -->
+
+<!-- LOCALIZATION NOTE : FILE The correct localization of this file might be to
+ - keep it in English, or another language commonly spoken among web developers.
+ - You want to make that choice consistent across the developer tools.
+ - A good criteria is the language in which you'd find the best
+ - documentation on web development on the web. -->
+
+<!ENTITY gotoLineCmd.label "Jump to line…">
+<!ENTITY gotoLineCmd.key "J">
+<!ENTITY gotoLineCmd.accesskey "J">
diff --git a/devtools/client/locales/en-US/sourceeditor.properties b/devtools/client/locales/en-US/sourceeditor.properties
new file mode 100644
index 000000000..842d549f0
--- /dev/null
+++ b/devtools/client/locales/en-US/sourceeditor.properties
@@ -0,0 +1,139 @@
+# 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/.
+
+# LOCALIZATION NOTE These strings are used inside the Source Editor component.
+# This component is used whenever source code is displayed for the purpose of
+# being edited, inside the Firefox developer tools - current examples are the
+# Scratchpad and the Style Editor tools.
+
+# LOCALIZATION NOTE The correct localization of this file might be to keep it
+# in English, or another language commonly spoken among web developers.
+# You want to make that choice consistent across the developer tools.
+# A good criteria is the language in which you'd find the best documentation
+# on web development on the web.
+
+# LOCALIZATION NOTE (findCmd.promptTitle): This is the dialog title used
+# when the user wants to search for a string in the code. You can
+# access this feature by pressing Ctrl-F on Windows/Linux or Cmd-F on Mac.
+findCmd.promptTitle=Find…
+
+# LOCALIZATION NOTE (findCmd.promptMessage): This is the message shown when
+# the user wants to search for a string in the code. You can
+# access this feature by pressing Ctrl-F on Windows/Linux or Cmd-F on Mac.
+findCmd.promptMessage=Search for:
+
+# LOCALIZATION NOTE (gotoLineCmd.promptTitle): This is the dialog title used
+# when the user wants to jump to a specific line number in the code. You can
+# access this feature by pressing Ctrl-J on Windows/Linux or Cmd-J on Mac.
+gotoLineCmd.promptTitle=Go to line…
+
+# LOCALIZATION NOTE (gotoLineCmd.promptMessage): This is the message shown when
+# the user wants to jump to a specific line number in the code. You can
+# access this feature by pressing Ctrl-J on Windows/Linux or Cmd-J on Mac.
+gotoLineCmd.promptMessage=Jump to line number:
+
+# LOCALIZATION NOTE (annotation.breakpoint.title): This is the text shown in
+# front of any breakpoint annotation when it is displayed as a tooltip in one of
+# the editor gutters. This feature is used in the JavaScript Debugger.
+annotation.breakpoint.title=Breakpoint: %S
+
+# LOCALIZATION NOTE (annotation.currentLine): This is the text shown in
+# a tooltip displayed in any of the editor gutters when the user hovers the
+# current line.
+annotation.currentLine=Current line
+
+# LOCALIZATION NOTE (annotation.debugLocation.title): This is the text shown in
+# a tooltip displayed in any of the editor gutters when the user hovers the
+# current debugger location. The debugger can pause the JavaScript execution at
+# user-defined lines.
+annotation.debugLocation.title=Current step: %S
+
+# LOCALIZATION NOTE (autocompletion.docsLink): This is the text shown on
+# the link inside of the documentation popup. If you type 'document' in Scratchpad
+# then press Shift+Space you can see the popup.
+autocompletion.docsLink=docs
+
+# LOCALIZATION NOTE (autocompletion.notFound): This is the text shown in
+# the documentation popup if Tern fails to find a type for the object.
+autocompletion.notFound=not found
+
+# LOCALIZATION NOTE (jumpToLine.commandkey): This is the key to use in
+# conjunction with accel (Command on Mac or Ctrl on other platforms) to jump to
+# a specific line in the editor.
+jumpToLine.commandkey=J
+
+# LOCALIZATION NOTE (toggleComment.commandkey): This is the key to use in
+# conjunction with accel (Command on Mac or Ctrl on other platforms) to either
+# comment or uncomment selected lines in the editor.
+toggleComment.commandkey=/
+
+# LOCALIZATION NOTE (indentLess.commandkey): This is the key to use in
+# conjunction with accel (Command on Mac or Ctrl on other platforms) to reduce
+# indentation level in CodeMirror. However, its default value also used by
+# the Toolbox to switch between tools so we disable it.
+#
+# DO NOT translate this key without proper synchronization with toolbox.dtd.
+indentLess.commandkey=[
+
+# LOCALIZATION NOTE (indentMore.commandkey): This is the key to use in
+# conjunction with accel (Command on Mac or Ctrl on other platforms) to increase
+# indentation level in CodeMirror. However, its default value also used by
+# the Toolbox to switch between tools
+#
+# DO NOT translate this key without proper synchronization with toolbox.dtd.
+indentMore.commandkey=]
+
+# LOCALIZATION NOTE (moveLineUp.commandkey): This is the combination of keys
+# used to move the current line up.
+# Do not localize "Alt", "Up", or change the format of the string. These are key
+# identifiers, not messages displayed to the user.
+moveLineUp.commandkey=Alt-Up
+
+# LOCALIZATION NOTE (moveLineDown.commandkey): This is the combination of keys
+# used to move the current line up.
+# Do not localize "Alt", "Down", or change the format of the string. These are
+# key identifiers, not messages displayed to the user.
+moveLineDown.commandkey=Alt-Down
+
+# LOCALIZATION NOTE (autocompletion.commandkey): This is the key, used with
+# Ctrl, for code autocompletion.
+# Do not localize "Space", it's the key identifier, not a message displayed to
+# the user.
+autocompletion.commandkey=Space
+
+# LOCALIZATION NOTE (showInformation2.commandkey): This is the combination of
+# keys used to display more information, like type inference.
+# Do not localize "Shift", "Ctrl", "Space", or change the format of the string.
+# These are key identifiers, not messages displayed to the user.
+showInformation2.commandkey=Shift-Ctrl-Space
+
+# LOCALIZATION NOTE (find.key):
+# Key shortcut used to find the typed search
+# Do not localize "CmdOrCtrl", "F", or change the format of the string. These are
+# key identifiers, not messages displayed to the user.
+find.key=CmdOrCtrl+F
+
+# LOCALIZATION NOTE (replaceAll.key):
+# Key shortcut used to replace the content of the editor
+# Do not localize "Shift", "CmdOrCtrl", "F", or change the format of the string. These are
+# key identifiers, not messages displayed to the user.
+replaceAll.key=Shift+CmdOrCtrl+F
+
+# LOCALIZATION NOTE (replaceAllMac.key):
+# Key shortcut used to replace the content of the editor on Mac
+# Do not localize "Alt", "CmdOrCtrl", "F", or change the format of the string. These are
+# key identifiers, not messages displayed to the user.
+replaceAllMac.key=Alt+CmdOrCtrl+F
+
+# LOCALIZATION NOTE (findNext.key):
+# Key shortcut used to find again the typed search
+# Do not localize "CmdOrCtrl", "G", or change the format of the string. These are
+# key identifiers, not messages displayed to the user.
+findNext.key=CmdOrCtrl+G
+
+# LOCALIZATION NOTE (findPrev.key):
+# Key shortcut used to find the previous typed search
+# Do not localize "Shift", "CmdOrCtrl", "G", or change the format of the string. These are
+# key identifiers, not messages displayed to the user.
+findPrev.key=Shift+CmdOrCtrl+G
diff --git a/devtools/client/locales/en-US/startup.properties b/devtools/client/locales/en-US/startup.properties
new file mode 100644
index 000000000..4486ac98a
--- /dev/null
+++ b/devtools/client/locales/en-US/startup.properties
@@ -0,0 +1,262 @@
+# 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/.
+
+# LOCALIZATION NOTE (optionsButton.tooltip): This is used as the tooltip
+# for the options panel tab.
+optionsButton.tooltip=Toolbox Options
+
+# LOCALIZATION NOTE (options.label): This is used as the label of the tab in
+# the devtools window.
+options.label=Options
+
+# LOCALIZATION NOTE (options.panelLabel): This is used as the label for the
+# toolbox panel.
+options.panelLabel=Toolbox Options Panel
+
+# LOCALIZATION NOTE (options.darkTheme.label2)
+# Used as a label for dark theme
+options.darkTheme.label2=Dark
+
+# LOCALIZATION NOTE (options.lightTheme.label2)
+# Used as a label for light theme
+options.lightTheme.label2=Light
+
+# LOCALIZATION NOTE (options.firebugTheme.label2)
+# Used as a label for Firebug theme
+options.firebugTheme.label2=Firebug
+
+# LOCALIZATION NOTE (performance.label):
+# This string is displayed in the title of the tab when the profiler is
+# displayed inside the developer tools window and in the Developer Tools Menu.
+performance.label=Performance
+
+# LOCALIZATION NOTE (performance.panelLabel):
+# This is used as the label for the toolbox panel.
+performance.panelLabel=Performance Panel
+
+# LOCALIZATION NOTE (performance.commandkey, performance.accesskey)
+# Used for the menuitem in the tool menu
+performance.commandkey=VK_F5
+performance.accesskey=P
+
+# LOCALIZATION NOTE (performance.tooltip):
+# This string is displayed in the tooltip of the tab when the profiler is
+# displayed inside the developer tools window.
+# Keyboard shortcut for Performance Tools will be shown inside brackets.
+performance.tooltip=Performance (%S)
+
+# LOCALIZATION NOTE (MenuWebconsole.label): the string displayed in the Tools
+# menu as a shortcut to open the devtools with the Web Console tab selected.
+MenuWebconsole.label=Web Console
+
+# LOCALIZATION NOTE (ToolboxTabWebconsole.label): the string displayed as the
+# label of the tab in the devtools window.
+ToolboxTabWebconsole.label=Console
+
+# LOCALIZATION NOTE (ToolboxWebConsole.panelLabel): the string used as the
+# label for the toolbox panel.
+ToolboxWebConsole.panelLabel=Console Panel
+
+# LOCALIZATION NOTE (ToolboxWebconsole.tooltip2): the string displayed in the
+# tooltip of the tab when the Web Console is displayed inside the developer
+# tools window.
+# Keyboard shortcut for Console will be shown inside the brackets.
+ToolboxWebconsole.tooltip2=Web Console (%S)
+
+cmd.commandkey=K
+webConsoleCmd.accesskey=W
+
+# LOCALIZATION NOTE (ToolboxDebugger.label):
+# This string is displayed in the title of the tab when the debugger is
+# displayed inside the developer tools window and in the Developer Tools Menu.
+ToolboxDebugger.label=Debugger
+
+# LOCALIZATION NOTE (ToolboxDebugger.panelLabel):
+# This is used as the label for the toolbox panel.
+ToolboxDebugger.panelLabel=Debugger Panel
+
+# LOCALIZATION NOTE (ToolboxDebugger.tooltip2):
+# This string is displayed in the tooltip of the tab when the debugger is
+# displayed inside the developer tools window..
+# A keyboard shortcut for JS Debugger will be shown inside brackets.
+ToolboxDebugger.tooltip2=JavaScript Debugger (%S)
+
+# LOCALIZATION NOTE (debuggerMenu.commandkey, debuggerMenu.accesskey)
+# Used for the menuitem in the tool menu
+debuggerMenu.commandkey=S
+debuggerMenu.accesskey=D
+
+# LOCALIZATION NOTE (ToolboxStyleEditor.label):
+# This string is displayed in the title of the tab when the style editor is
+# displayed inside the developer tools window and in the Developer Tools Menu.
+ToolboxStyleEditor.label=Style Editor
+
+# LOCALIZATION NOTE (ToolboxStyleEditor.panelLabel):
+# This is used as the label for the toolbox panel.
+ToolboxStyleEditor.panelLabel=Style Editor Panel
+
+# LOCALIZATION NOTE (ToolboxStyleEditor.tooltip3):
+# This string is displayed in the tooltip of the tab when the style editor is
+# displayed inside the developer tools window.
+# A keyboard shortcut for Stylesheet Editor will be shown inside the latter pair of brackets.
+ToolboxStyleEditor.tooltip3=Stylesheet Editor (CSS) (%S)
+
+# LOCALIZATION NOTE (open.commandkey): This the key to use in
+# conjunction with shift to open the style editor
+open.commandkey=VK_F7
+
+# LOCALIZATION NOTE (open.accesskey): The access key used to open the style
+# editor.
+open.accesskey=l
+
+# LOCALIZATION NOTE (ToolboxShaderEditor.label):
+# This string is displayed in the title of the tab when the Shader Editor is
+# displayed inside the developer tools window and in the Developer Tools Menu.
+ToolboxShaderEditor.label=Shader Editor
+
+# LOCALIZATION NOTE (ToolboxShaderEditor.panelLabel):
+# This is used as the label for the toolbox panel.
+ToolboxShaderEditor.panelLabel=Shader Editor Panel
+
+# LOCALIZATION NOTE (ToolboxShaderEditor.tooltip):
+# This string is displayed in the tooltip of the tab when the Shader Editor is
+# displayed inside the developer tools window.
+ToolboxShaderEditor.tooltip=Live GLSL shader language editor for WebGL
+
+# LOCALIZATION NOTE (ToolboxCanvasDebugger.label):
+# This string is displayed in the title of the tab when the Shader Editor is
+# displayed inside the developer tools window and in the Developer Tools Menu.
+ToolboxCanvasDebugger.label=Canvas
+
+# LOCALIZATION NOTE (ToolboxCanvasDebugger.panelLabel):
+# This is used as the label for the toolbox panel.
+ToolboxCanvasDebugger.panelLabel=Canvas Panel
+
+# LOCALIZATION NOTE (ToolboxCanvasDebugger.tooltip):
+# This string is displayed in the tooltip of the tab when the Shader Editor is
+# displayed inside the developer tools window.
+ToolboxCanvasDebugger.tooltip=Tools to inspect and debug <canvas> contexts
+
+# LOCALIZATION NOTE (ToolboxWebAudioEditor1.label):
+# This string is displayed in the title of the tab when the Web Audio Editor
+# is displayed inside the developer tools window and in the Developer Tools Menu.
+ToolboxWebAudioEditor1.label=Web Audio
+
+# LOCALIZATION NOTE (ToolboxWebAudioEditor1.panelLabel):
+# This is used as the label for the toolbox panel.
+ToolboxWebAudioEditor1.panelLabel=Web Audio Panel
+
+# LOCALIZATION NOTE (ToolboxWebAudioEditor1.tooltip):
+# This string is displayed in the tooltip of the tab when the Web Audio Editor is
+# displayed inside the developer tools window.
+ToolboxWebAudioEditor1.tooltip=Web Audio context visualizer and audio node inspector
+
+# LOCALIZATION NOTE (inspector.*)
+# Used for the menuitem in the tool menu
+inspector.label=Inspector
+inspector.commandkey=C
+inspector.accesskey=I
+
+# LOCALIZATION NOTE (inspector.panelLabel)
+# Labels applied to the panel and views within the panel in the toolbox
+inspector.panelLabel=Inspector Panel
+
+# LOCALIZATION NOTE (inspector.tooltip2)
+# Keyboard shortcut for DOM and Style Inspector will be shown inside brackets.
+inspector.tooltip2=DOM and Style Inspector (%S)
+
+# LOCALIZATION NOTE (netmonitor.label):
+# This string is displayed in the title of the tab when the Network Monitor is
+# displayed inside the developer tools window and in the Developer Tools Menu.
+netmonitor.label=Network
+
+# LOCALIZATION NOTE (netmonitor.panelLabel):
+# This is used as the label for the toolbox panel.
+netmonitor.panelLabel=Network Panel
+
+# LOCALIZATION NOTE (netmonitor.commandkey, netmonitor.accesskey)
+# Used for the menuitem in the tool menu
+netmonitor.commandkey=Q
+netmonitor.accesskey=N
+
+# LOCALIZATION NOTE (netmonitor.tooltip2):
+# This string is displayed in the tooltip of the tab when the Network Monitor is
+# displayed inside the developer tools window.
+# Keyboard shortcut for Network Monitor will be shown inside the brackets.
+netmonitor.tooltip2=Network Monitor (%S)
+
+# LOCALIZATION NOTE (storage.commandkey): This the key to use in
+# conjunction with shift to open the storage editor
+storage.commandkey=VK_F9
+
+# LOCALIZATION NOTE (storage.accesskey): The access key used to open the storage
+# editor.
+storage.accesskey=a
+
+# LOCALIZATION NOTE (storage.label):
+# This string is displayed as the label of the tab in the developer tools window
+storage.label=Storage
+
+# LOCALIZATION NOTE (storage.menuLabel):
+# This string is displayed in the Tools menu as a shortcut to open the devtools
+# with the Storage Inspector tab selected.
+storage.menuLabel=Storage Inspector
+
+# LOCALIZATION NOTE (storage.panelLabel):
+# This string is used as the aria-label for the iframe of the Storage Inspector
+# tool in developer tools toolbox.
+storage.panelLabel=Storage Panel
+
+# LOCALIZATION NOTE (storage.tooltip3):
+# This string is displayed in the tooltip of the tab when the storage editor is
+# displayed inside the developer tools window.
+# A keyboard shortcut for Storage Inspector will be shown inside the brackets.
+storage.tooltip3=Storage Inspector (Cookies, Local Storage, …) (%S)
+
+# LOCALIZATION NOTE (scratchpad.label): this string is displayed in the title of
+# the tab when the Scratchpad is displayed inside the developer tools window and
+# in the Developer Tools Menu.
+scratchpad.label=Scratchpad
+
+# LOCALIZATION NOTE (scratchpad.panelLabel): this is used as the
+# label for the toolbox panel.
+scratchpad.panelLabel=Scratchpad Panel
+
+# LOCALIZATION NOTE (scratchpad.tooltip): This string is displayed in the
+# tooltip of the tab when the Scratchpad is displayed inside the developer tools
+# window.
+scratchpad.tooltip=Scratchpad
+
+# LOCALIZATION NOTE (memory.label): This string is displayed in the title of the
+# tab when the memory tool is displayed inside the developer tools window and in
+# the Developer Tools Menu.
+memory.label=Memory
+
+# LOCALIZATION NOTE (memory.panelLabel): This is used as the label for the
+# toolbox panel.
+memory.panelLabel=Memory Panel
+
+# LOCALIZATION NOTE (memory.tooltip): This string is displayed in the tooltip of
+# the tab when the memory tool is displayed inside the developer tools window.
+memory.tooltip=Memory
+
+# LOCALIZATION NOTE (dom.label):
+# This string is displayed in the title of the tab when the DOM panel is
+# displayed inside the developer tools window and in the Developer Tools Menu.
+dom.label=DOM
+
+# LOCALIZATION NOTE (dom.panelLabel):
+# This is used as the label for the toolbox panel.
+dom.panelLabel=DOM Panel
+
+# LOCALIZATION NOTE (dom.commandkey, dom.accesskey)
+# Used for the menuitem in the tool menu
+dom.commandkey=W
+dom.accesskey=D
+
+# LOCALIZATION NOTE (dom.tooltip):
+# This string is displayed in the tooltip of the tab when the DOM is
+# displayed inside the developer tools window.
+# Keyboard shortcut for DOM panel will be shown inside the brackets.
+dom.tooltip=DOM (%S)
diff --git a/devtools/client/locales/en-US/storage.dtd b/devtools/client/locales/en-US/storage.dtd
new file mode 100644
index 000000000..211c79436
--- /dev/null
+++ b/devtools/client/locales/en-US/storage.dtd
@@ -0,0 +1,11 @@
+<!-- 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/. -->
+
+<!-- LOCALIZATION NOTE : This file contains the Storage Inspector strings. -->
+
+<!-- LOCALIZATION NOTE : Placeholder for the searchbox that allows you to filter the table items. -->
+<!ENTITY searchBox.placeholder "Filter items">
+
+<!-- LOCALIZATION NOTE : Label of popup menu action to delete all storage items. -->
+<!ENTITY storage.popupMenu.deleteAllLabel "Delete All">
diff --git a/devtools/client/locales/en-US/storage.properties b/devtools/client/locales/en-US/storage.properties
new file mode 100644
index 000000000..1eeb88ff9
--- /dev/null
+++ b/devtools/client/locales/en-US/storage.properties
@@ -0,0 +1,97 @@
+# 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/.
+
+# LOCALIZATION NOTE These strings are used inside the Storage Editor tool.
+# LOCALIZATION NOTE The correct localization of this file might be to keep it
+# in English, or another language commonly spoken among web developers.
+# You want to make that choice consistent across the developer tools.
+# A good criteria is the language in which you'd find the best documentation
+# on web development on the web.
+
+# LOCALIZATION NOTE (storage.filter.key):
+# Key shortcut used to focus the filter box on top of the data view
+storage.filter.key=CmdOrCtrl+F
+
+# LOCALIZATION NOTE (tree.emptyText):
+# This string is displayed when the Storage Tree is empty. This can happen when
+# there are no websites on the current page (about:blank)
+tree.emptyText=No hosts on the page
+
+# LOCALIZATION NOTE (table.emptyText):
+# This string is displayed when there are no rows in the Storage Table for the
+# selected host.
+table.emptyText=No data present for selected host
+
+# LOCALIZATION NOTE (tree.labels.*):
+# These strings are the labels for Storage type groups present in the Storage
+# Tree, like cookies, local storage etc.
+tree.labels.cookies=Cookies
+tree.labels.localStorage=Local Storage
+tree.labels.sessionStorage=Session Storage
+tree.labels.indexedDB=Indexed DB
+tree.labels.Cache=Cache Storage
+
+# LOCALIZATION NOTE (table.headers.*.*):
+# These strings are the header names of the columns in the Storage Table for
+# each type of storage available through the Storage Tree to the side.
+table.headers.cookies.name=Name
+table.headers.cookies.path=Path
+table.headers.cookies.host=Domain
+table.headers.cookies.expires=Expires on
+table.headers.cookies.value=Value
+table.headers.cookies.lastAccessed=Last accessed on
+table.headers.cookies.creationTime=Created on
+
+table.headers.localStorage.name=Key
+table.headers.localStorage.value=Value
+
+table.headers.sessionStorage.name=Key
+table.headers.sessionStorage.value=Value
+
+table.headers.Cache.url=URL
+table.headers.Cache.status=Status
+
+table.headers.indexedDB.name=Key
+table.headers.indexedDB.db=Database Name
+table.headers.indexedDB.objectStore=Object Store Name
+table.headers.indexedDB.value=Value
+table.headers.indexedDB.origin=Origin
+table.headers.indexedDB.version=Version
+table.headers.indexedDB.objectStores=Object Stores
+table.headers.indexedDB.keyPath=Key
+table.headers.indexedDB.autoIncrement=Auto Increment
+table.headers.indexedDB.indexes=Indexes
+
+# LOCALIZATION NOTE (label.expires.session):
+# This string is displayed in the expires column when the cookie is Session
+# Cookie
+label.expires.session=Session
+
+# LOCALIZATION NOTE (storage.search.placeholder):
+# This is the placeholder text in the sidebar search box
+storage.search.placeholder=Filter values
+
+# LOCALIZATION NOTE (storage.data.label):
+# This is the heading displayed over the item value in the sidebar
+storage.data.label=Data
+
+# LOCALIZATION NOTE (storage.parsedValue.label):
+# This is the heading displayed over the item parsed value in the sidebar
+storage.parsedValue.label=Parsed Value
+
+# LOCALIZATION NOTE (storage.popupMenu.deleteLabel):
+# Label of popup menu action to delete storage item.
+storage.popupMenu.deleteLabel=Delete “%Sâ€
+
+# LOCALIZATION NOTE (storage.popupMenu.deleteAllLabel):
+# Label of popup menu action to delete all storage items.
+storage.popupMenu.deleteAllFromLabel=Delete All From “%Sâ€
+
+# LOCALIZATION NOTE (storage.idb.deleteBlocked):
+# Warning notification when IndexedDB database could not be deleted immediately.
+storage.idb.deleteBlocked=Database “%S†will be deleted after all connections are closed.
+
+# LOCALIZATION NOTE (storage.idb.deleteError):
+# Error notification when IndexedDB database could not be deleted.
+storage.idb.deleteError=Database “%S†could not be deleted.
diff --git a/devtools/client/locales/en-US/styleeditor.dtd b/devtools/client/locales/en-US/styleeditor.dtd
new file mode 100644
index 000000000..99c512c24
--- /dev/null
+++ b/devtools/client/locales/en-US/styleeditor.dtd
@@ -0,0 +1,67 @@
+<!-- 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/. -->
+
+<!-- LOCALIZATION NOTE : FILE This file contains the Style Editor window strings -->
+<!-- LOCALIZATION NOTE : FILE Do not translate commandkeys -->
+<!-- LOCALIZATION NOTE : The correct localization of this file might be to keep
+ it in English, or another language commonly spoken among web developers.
+ You want to make that choice consistent across the developer tools.
+ A good criteria is the language in which you'd find the best documentation
+ on web development on the web. -->
+
+<!ENTITY newButton.label "New">
+<!ENTITY newButton.tooltip "Create and append a new style sheet to the document">
+<!ENTITY newButton.accesskey "N">
+
+<!ENTITY importButton.label "Import…">
+<!ENTITY importButton.tooltip "Import and append an existing style sheet to the document">
+<!ENTITY importButton.accesskey "I">
+
+<!ENTITY visibilityToggle.tooltip "Toggle style sheet visibility">
+
+<!ENTITY saveButton.label "Save">
+<!ENTITY saveButton.tooltip "Save this style sheet to a file">
+<!ENTITY saveButton.accesskey "S">
+
+<!ENTITY optionsButton.tooltip "Style Editor options">
+
+<!-- LOCALIZATION NOTE (showOriginalSources.label): This is the label on the context
+ menu item to toggle showing original sources in the editor. -->
+<!ENTITY showOriginalSources.label "Show original sources">
+
+<!-- LOCALIZATION NOTE (showOriginalSources.accesskey): This is the access key for
+ the menu item to toggle showing original sources in the editor. -->
+<!ENTITY showOriginalSources.accesskey "o">
+
+<!-- LOCALIZATION NOTE (showMediaSidebar.label): This is the label on the context
+ menu item to toggle showing @media rule shortcuts in a sidebar. -->
+<!ENTITY showMediaSidebar.label "Show @media sidebar">
+
+<!-- LOCALIZATION NOTE (showMediaSidebar.accesskey): This is the access key for
+ the menu item to toggle showing the @media sidebar. -->
+<!ENTITY showMediaSidebar.accesskey "m">
+
+<!-- LOCALICATION NOTE (mediaRules.label): This is shown above the list of @media rules
+ in each stylesheet editor sidebar. -->
+<!ENTITY mediaRules.label "@media rules">
+
+<!ENTITY editorTextbox.placeholder "Type CSS here.">
+
+<!-- LOCALICATION NOTE (noStyleSheet.label): This is shown when a page has no
+ stylesheet. -->
+<!ENTITY noStyleSheet.label "This page has no style sheet.">
+
+<!-- LOCALICATION NOTE (noStyleSheet-tip-start.label): This is the start of a
+ tip sentence shown when there is no stylesheet. It suggests to create a new
+ stylesheet and provides an action link to do so. -->
+<!ENTITY noStyleSheet-tip-start.label "Perhaps you'd like to ">
+<!-- LOCALICATION NOTE (noStyleSheet-tip-action.label): This is text for the
+ link that triggers creation of a new stylesheet. -->
+<!ENTITY noStyleSheet-tip-action.label "append a new style sheet">
+<!-- LOCALICATION NOTE (noStyleSheet-tip-end.label): End of the tip sentence -->
+<!ENTITY noStyleSheet-tip-end.label "?">
+
+<!-- LOCALIZATION NOTE (openLinkNewTab.label): This is the text for the
+ context menu item that opens a stylesheet in a new tab -->
+<!ENTITY openLinkNewTab.label "Open Link in New Tab">
diff --git a/devtools/client/locales/en-US/styleeditor.properties b/devtools/client/locales/en-US/styleeditor.properties
new file mode 100644
index 000000000..0be5b0100
--- /dev/null
+++ b/devtools/client/locales/en-US/styleeditor.properties
@@ -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/.
+
+# LOCALIZATION NOTE These strings are used inside the Style Editor.
+# LOCALIZATION NOTE The correct localization of this file might be to keep it
+# in English, or another language commonly spoken among web developers.
+# You want to make that choice consistent across the developer tools.
+# A good criteria is the language in which you'd find the best documentation
+# on web development on the web.
+
+# LOCALIZATION NOTE (inlineStyleSheet): This is the name used for an style sheet
+# that is declared inline in the <style> element. Shown in the stylesheets list.
+# the argument is the index (order) of the containing <style> element in the
+# document.
+inlineStyleSheet=<inline style sheet #%S>
+
+# LOCALIZATION NOTE (newStyleSheet): This is the default name for a new
+# user-created style sheet.
+newStyleSheet=New style sheet #%S
+
+# LOCALIZATION NOTE (ruleCount.label): Semicolon-separated list of plural forms.
+# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
+# This is shown in the style sheets list.
+# #1 rule.
+# example: 111 rules.
+ruleCount.label=#1 rule.;#1 rules.
+
+# LOCALIZATION NOTE (error-load): This is shown when loading fails.
+error-load=Style sheet could not be loaded.
+
+# LOCALIZATION NOTE (error-save): This is shown when saving fails.
+error-save=Style sheet could not be saved.
+
+# LOCALIZATION NOTE (error-compressed): This is shown when we can't show
+# coverage information because the css source is compressed.
+error-compressed=Can’t show coverage information for compressed stylesheets
+
+# LOCALIZATION NOTE (importStyleSheet.title): This is the file picker title,
+# when you import a style sheet into the Style Editor.
+importStyleSheet.title=Import style sheet
+
+# LOCALIZATION NOTE (importStyleSheet.filter): This is the *.css filter title
+importStyleSheet.filter=CSS files
+
+# LOCALIZATION NOTE (saveStyleSheet.title): This is the file picker title,
+# when you save a style sheet from the Style Editor.
+saveStyleSheet.title=Save style sheet
+
+# LOCALIZATION NOTE (saveStyleSheet.filter): This is the *.css filter title
+saveStyleSheet.filter=CSS files
+
+# LOCALIZATION NOTE (saveStyleSheet.commandkey): This the key to use in
+# conjunction with accel (Command on Mac or Ctrl on other platforms) to Save
+saveStyleSheet.commandkey=S
+
diff --git a/devtools/client/locales/en-US/toolbox.dtd b/devtools/client/locales/en-US/toolbox.dtd
new file mode 100644
index 000000000..6097fa82a
--- /dev/null
+++ b/devtools/client/locales/en-US/toolbox.dtd
@@ -0,0 +1,220 @@
+<!-- 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/. -->
+
+<!-- LOCALIZATION NOTE : FILE This file contains the Toolbox strings -->
+<!-- LOCALIZATION NOTE : FILE Do not translate key -->
+
+<!ENTITY closeCmd.key "W">
+<!ENTITY toggleToolbox.key "I">
+<!ENTITY toggleToolboxF12.keycode "VK_F12">
+<!ENTITY toggleToolboxF12.keytext "F12">
+
+<!ENTITY toolboxCloseButton.tooltip "Close Developer Tools">
+
+<!-- LOCALIZATION NOTE (toolboxFramesButton): This is the label for
+ - the iframes menu list that appears only when the document has some.
+ - It allows you to switch the context of the whole toolbox. -->
+<!ENTITY toolboxFramesTooltip "Select an iframe as the currently targeted document">
+
+<!-- LOCALIZATION NOTE (toolboxNoAutoHideButton): This is the label for
+ - the button to force the popups/panels to stay visible on blur.
+ - This is only visible in the browser toolbox as it is meant for
+ - addon developers and Firefox contributors. -->
+<!ENTITY toolboxNoAutoHideTooltip "Disable popup auto hide">
+
+<!-- LOCALIZATION NOTE (browserToolboxErrorMessage): This is the label
+ - shown next to error details when the Browser Toolbox is unable to open. -->
+<!ENTITY browserToolboxErrorMessage "Error opening Browser Toolbox:">
+
+<!-- LOCALIZATION NOTE (options.context.advancedSettings): This is the label for
+ - the heading of the advanced settings group in the options panel. -->
+<!ENTITY options.context.advancedSettings "Advanced settings">
+
+<!-- LOCALIZATION NOTE (options.context.inspector): This is the label for
+ - the heading of the Inspector group in the options panel. -->
+<!ENTITY options.context.inspector "Inspector">
+
+<!-- LOCALIZATION NOTE (options.showUserAgentStyles.label): This is the label
+ - for the checkbox option to show user agent styles in the Inspector
+ - panel. -->
+<!ENTITY options.showUserAgentStyles.label "Show Browser Styles">
+<!ENTITY options.showUserAgentStyles.tooltip "Turning this on will show default styles that are loaded by the browser.">
+
+<!-- LOCALIZATION NOTE (options.collapseAttrs.label): This is the label
+ - for the checkbox option to enable collapse attributes in the Inspector
+ - panel. -->
+<!ENTITY options.collapseAttrs.label "Truncate DOM attributes">
+<!ENTITY options.collapseAttrs.tooltip "Truncate long attributes in the inspector">
+
+<!-- LOCALIZATION NOTE (options.defaultColorUnit.label): This is the label for a
+ - dropdown list that controls the default color unit used in the inspector.
+ - This label is visible in the options panel. -->
+<!ENTITY options.defaultColorUnit.label "Default color unit">
+
+<!-- LOCALIZATION NOTE (options.defaultColorUnit.accesskey): This is the access
+ - key for a dropdown list that controls the default color unit used in the
+ - inspector. This is visible in the options panel. -->
+<!ENTITY options.defaultColorUnit.accesskey "U">
+
+<!-- LOCALIZATION NOTE (options.defaultColorUnit.authored): This is used in the
+ - 'Default color unit' dropdown list and is visible in the options panel. -->
+<!ENTITY options.defaultColorUnit.authored "As Authored">
+
+<!-- LOCALIZATION NOTE (options.defaultColorUnit.hex): This is used in the
+ - 'Default color unit' dropdown list and is visible in the options panel. -->
+<!ENTITY options.defaultColorUnit.hex "Hex">
+
+<!-- LOCALIZATION NOTE (options.defaultColorUnit.hsl): This is used in the
+ - 'Default color unit' dropdown list and is visible in the options panel. -->
+<!ENTITY options.defaultColorUnit.hsl "HSL(A)">
+
+<!-- LOCALIZATION NOTE (options.defaultColorUnit.rgb): This is used in the
+ - 'Default color unit' dropdown list and is visible in the options panel. -->
+<!ENTITY options.defaultColorUnit.rgb "RGB(A)">
+
+<!-- LOCALIZATION NOTE (options.defaultColorUnit.name): This is used in
+ - the 'Default color unit' dropdown list and is visible in the options panel.
+ - -->
+<!ENTITY options.defaultColorUnit.name "Color Names">
+
+<!-- LOCALIZATION NOTE (options.context.triggersPageRefresh): This is the
+ - triggers page refresh footnote under the advanced settings group in the
+ - options panel and is used for settings that trigger page reload. -->
+<!ENTITY options.context.triggersPageRefresh "* Current session only, reloads the page">
+
+<!-- LOCALIZATION NOTE (options.enableChrome.label4): This is the label for the
+ - checkbox that toggles chrome debugging, i.e. devtools.chrome.enabled
+ - boolean preference in about:config, in the options panel. -->
+<!ENTITY options.enableChrome.label5 "Enable browser chrome and add-on debugging toolboxes">
+<!ENTITY options.enableChrome.tooltip3 "Turning this option on will allow you to use various developer tools in browser context (via Tools > Web Developer > Browser Toolbox) and debug add-ons from the Add-ons Manager">
+
+<!-- LOCALIZATION NOTE (options.enableRemote.label3): This is the label for the
+ - checkbox that toggles remote debugging, i.e. devtools.debugger.remote-enabled
+ - boolean preference in about:config, in the options panel. -->
+<!ENTITY options.enableRemote.label3 "Enable remote debugging">
+<!ENTITY options.enableRemote.tooltip2 "Turning this option on will allow the developer tools to debug a remote instance like Firefox OS">
+
+<!-- LOCALIZATION NOTE (options.enableWorkers.label): This is the label for the
+ - checkbox that toggles worker debugging, i.e. devtools.debugger.workers
+ - boolean preference in about:config, in the options panel. -->
+<!ENTITY options.enableWorkers.label "Enable worker debugging (in development)">
+<!ENTITY options.enableWorkers.tooltip "Turning this option on will allow the developer tools to debug workers">
+
+<!-- LOCALIZATION NOTE (options.disableJavaScript.label,
+ - options.disableJavaScript.tooltip): This is the options panel label and
+ - tooltip for the checkbox that toggles JavaScript on or off. -->
+<!ENTITY options.disableJavaScript.label "Disable JavaScript *">
+<!ENTITY options.disableJavaScript.tooltip "Turning this option on will disable JavaScript for the current tab. If the tab or the toolbox is closed then this setting will be forgotten.">
+
+<!-- LOCALIZATION NOTE (options.disableHTTPCache.label,
+ - options.disableHTTPCache.tooltip): This is the options panel label and
+ - tooltip for the checkbox that toggles the HTTP cache on or off. -->
+<!ENTITY options.disableHTTPCache.label "Disable HTTP Cache (when toolbox is open)">
+<!ENTITY options.disableHTTPCache.tooltip "Turning this option on will disable the HTTP cache for all tabs that have the toolbox open. Service Workers are not affected by this option.">
+
+<!-- LOCALIZATION NOTE (options.enableServiceWorkersHTTP.label,
+ - options.enableServiceWorkersHTTP.tooltip): This is the options panel label and
+ - tooltip for the checkbox that toggles the service workers testing features on or off. This option enables service workers over HTTP. -->
+<!ENTITY options.enableServiceWorkersHTTP.label "Enable Service Workers over HTTP (when toolbox is open)">
+<!ENTITY options.enableServiceWorkersHTTP.tooltip "Turning this option on will enable the service workers over HTTP for all tabs that have the toolbox open.">
+
+<!-- LOCALIZATION NOTE (options.selectDefaultTools.label2): This is the label for
+ - the heading of group of checkboxes corresponding to the default developer
+ - tools. -->
+<!ENTITY options.selectDefaultTools.label2 "Default Developer Tools">
+
+<!-- LOCALIZATION NOTE (options.selectAdditionalTools.label): This is the label for
+ - the heading of group of checkboxes corresponding to the developer tools
+ - added by add-ons. This heading is hidden when there is no developer tool
+ - installed by add-ons. -->
+<!ENTITY options.selectAdditionalTools.label "Developer Tools installed by add-ons">
+
+<!-- LOCALIZATION NOTE (options.selectEnabledToolboxButtons.label): This is the label for
+ - the heading of group of checkboxes corresponding to the default developer
+ - tool buttons. -->
+<!ENTITY options.selectEnabledToolboxButtons.label "Available Toolbox Buttons">
+
+<!-- LOCALIZATION NOTE (options.toolNotSupported.label): This is the label for
+ - the explanation of the * marker on a tool which is currently not supported
+ - for the target of the toolbox. -->
+<!ENTITY options.toolNotSupported.label "* Not supported for current toolbox target">
+
+<!-- LOCALIZATION NOTE (options.selectDevToolsTheme.label2): This is the label for
+ - the heading of the radiobox corresponding to the theme of the developer
+ - tools. -->
+<!ENTITY options.selectDevToolsTheme.label2 "Themes">
+
+<!-- LOCALIZATION NOTE (options.usedeveditiontheme.*) Options under the
+ - toolbox for enabling and disabling the Developer Edition browser theme. -->
+<!ENTITY options.usedeveditiontheme.label "Use Developer Edition browser theme">
+<!ENTITY options.usedeveditiontheme.tooltip "Toggles the Developer Edition browser theme.">
+
+<!-- LOCALIZATION NOTE (options.webconsole.label): This is the label for the
+ - heading of the group of Web Console preferences in the options panel. -->
+<!ENTITY options.webconsole.label "Web Console">
+
+<!-- LOCALIZATION NOTE (options.timestampMessages.label): This is the
+ - label for the checkbox that toggles timestamps in the Web Console -->
+<!ENTITY options.timestampMessages.label "Enable timestamps">
+<!ENTITY options.timestampMessages.tooltip "If you enable this option commands and output in the Web Console will display a timestamp">
+
+<!-- LOCALIZATION NOTE (options.debugger.label): This is the label for the
+ - heading of the group of Debugger preferences in the options panel. -->
+<!ENTITY options.debugger.label "Debugger">
+
+<!-- LOCALIZATION NOTE (options.sourceMap.label): This is the
+ - label for the checkbox that toggles source maps in the Debugger -->
+<!ENTITY options.sourceMaps.label "Enable Source Maps">
+<!ENTITY options.sourceMaps.tooltip "If you enable this option sources will be mapped in the Debugger and Console.">
+
+<!-- LOCALIZATION NOTE (options.styleeditor.label): This is the label for the
+ - heading of the group of Style Editor preferences in the options
+ - panel. -->
+<!ENTITY options.styleeditor.label "Style Editor">
+
+<!-- LOCALIZATION NOTE (options.stylesheetSourceMaps.label): This is the
+ - label for the checkbox that toggles showing original sources in the Style Editor -->
+<!ENTITY options.stylesheetSourceMaps.label "Show original sources">
+<!ENTITY options.stylesheetSourceMaps.tooltip "Show original sources (e.g. Sass files) in the Style Editor and Inspector">
+
+<!-- LOCALIZATION NOTE (options.stylesheetAutocompletion.label): This is the
+ - label for the checkbox that toggles autocompletion of css in the Style Editor -->
+<!ENTITY options.stylesheetAutocompletion.label "Autocomplete CSS">
+<!ENTITY options.stylesheetAutocompletion.tooltip "Autocomplete CSS properties, values and selectors in Style Editor as you type">
+
+<!-- LOCALIZATION NOTE (options.commonprefs): This is the label for the heading
+ of all preferences that affect both the Web Console and the Network
+ Monitor -->
+<!ENTITY options.commonPrefs.label "Common Preferences">
+
+<!-- LOCALIZATION NOTE (options.enablePersistentLogs.label): This is the
+ - label for the checkbox that toggles persistent logs in the Web Console and
+ - network monitor, i.e. devtools.webconsole.persistlog a boolean preference in
+ - about:config, in the options panel. -->
+<!ENTITY options.enablePersistentLogs.label "Enable persistent logs">
+<!ENTITY options.enablePersistentLogs.tooltip "If you enable this option the Web Console and Network Monitor will not clear the output each time you navigate to a new page">
+
+<!-- LOCALIZATION NOTE (options.showPlatformData.label): This is the
+ - label for the checkbox that toggles the display of the platform data in the,
+ - Profiler i.e. devtools.profiler.ui.show-platform-data a boolean preference
+ - in about:config, in the options panel. -->
+<!ENTITY options.showPlatformData.label "Show Gecko platform data">
+<!ENTITY options.showPlatformData.tooltip "If you enable this option the JavaScript Profiler reports will include
+Gecko platform symbols">
+
+<!-- LOCALIZATION NOTE (options.sourceeditor.*): Options under the editor
+ - section. -->
+
+<!ENTITY options.sourceeditor.label "Editor Preferences">
+<!ENTITY options.sourceeditor.detectindentation.label "Detect indentation">
+<!ENTITY options.sourceeditor.detectindentation.tooltip "Guess indentation based on source content">
+<!ENTITY options.sourceeditor.autoclosebrackets.label "Autoclose brackets">
+<!ENTITY options.sourceeditor.autoclosebrackets.tooltip "Automatically insert closing brackets">
+<!ENTITY options.sourceeditor.expandtab.label "Indent using spaces">
+<!ENTITY options.sourceeditor.expandtab.tooltip "Use spaces instead of the tab character">
+<!ENTITY options.sourceeditor.tabsize.label "Tab size">
+<!ENTITY options.sourceeditor.tabsize.accesskey "T">
+<!ENTITY options.sourceeditor.keybinding.label "Keybindings">
+<!ENTITY options.sourceeditor.keybinding.accesskey "K">
+<!ENTITY options.sourceeditor.keybinding.default.label "Default">
diff --git a/devtools/client/locales/en-US/toolbox.properties b/devtools/client/locales/en-US/toolbox.properties
new file mode 100644
index 000000000..225bd320b
--- /dev/null
+++ b/devtools/client/locales/en-US/toolbox.properties
@@ -0,0 +1,160 @@
+# 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/.
+
+toolboxDockButtons.bottom.tooltip=Dock to bottom of browser window
+toolboxDockButtons.side.tooltip=Dock to side of browser window
+toolboxDockButtons.window.tooltip=Show in separate window
+
+# LOCALIZATION NOTE (toolboxDockButtons.bottom.minimize): This string is shown
+# as a tooltip that appears in the toolbox when it is in "bottom host" mode and
+# when hovering over the minimize button in the toolbar. When clicked, the
+# button minimizes the toolbox so that just the toolbar is visible at the
+# bottom.
+toolboxDockButtons.bottom.minimize=Minimize the toolbox
+
+# LOCALIZATION NOTE (toolboxDockButtons.bottom.maximize): This string is shown
+# as a tooltip that appears in the toolbox when it is in "bottom host" mode and
+# when hovering over the maximize button in the toolbar. When clicked, the
+# button maximizes the toolbox again (if it had been minimized before) so that
+# the whole toolbox is visible again.
+toolboxDockButtons.bottom.maximize=Maximize the toolbox
+
+# LOCALIZATION NOTE (toolboxToggleButton.errors): Semi-colon list of plural
+# forms.
+# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
+# #1 number of errors in the current web page
+toolboxToggleButton.errors=#1 error;#1 errors
+
+# LOCALIZATION NOTE (toolboxToggleButton.warnings): Semi-colon list of plural
+# forms.
+# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
+# #1 number of warnings in the current web page
+toolboxToggleButton.warnings=#1 warning;#1 warnings
+
+# LOCALIZATION NOTE (toolboxToggleButton.tooltip): This string is shown
+# as tooltip in the developer toolbar to open/close the developer tools.
+# It's using toolboxToggleButton.errors as first and
+# toolboxToggleButton.warnings as second argument to show the number of errors
+# and warnings.
+toolboxToggleButton.tooltip=%1$S, %2$S\nClick to toggle the developer tools.
+
+# LOCALIZATION NOTE (toolbar.closeButton.tooltip)
+# Used as a message in tooltip when overing the close button of the Developer
+# Toolbar.
+toolbar.closeButton.tooltip=Close Developer Toolbar
+
+# LOCALIZATION NOTE (toolbar.toolsButton.tooltip)
+# Used as a message in tooltip when overing the wrench icon of the Developer
+# Toolbar, which toggle the developer toolbox.
+toolbar.toolsButton.tooltip=Toggle developer tools
+
+# LOCALIZATION NOTE (toolbox.titleTemplate1): This is the template
+# used to format the title of the toolbox.
+# The URL of the page being targeted: %1$S.
+toolbox.titleTemplate1=Developer Tools - %1$S
+
+# LOCALIZATION NOTE (toolbox.titleTemplate2): This is the template
+# used to format the title of the toolbox.
+# The page title or other name for the thing being targeted: %1$S
+# The URL of the page being targeted: %2$S.
+toolbox.titleTemplate2=Developer Tools - %1$S - %2$S
+
+# LOCALIZATION NOTE (toolbox.defaultTitle): This is used as the tool
+# name when no tool is selected.
+toolbox.defaultTitle=Developer Tools
+
+# LOCALIZATION NOTE (toolbox.label): This is used as the label for the
+# toolbox as a whole
+toolbox.label=Developer Tools
+
+# LOCALIZATION NOTE (options.toolNotSupported): This is the template
+# used to add a * marker to the label for the Options Panel tool checkbox for the
+# tool which is not supported for the current toolbox target.
+# The name of the tool: %1$S.
+options.toolNotSupportedMarker=%1$S *
+
+# LOCALIZATION NOTE (scratchpad.keycode)
+# Used for opening scratchpad from the detached toolbox window
+# Needs to match scratchpad.keycode from browser.dtd
+scratchpad.keycode=VK_F4
+
+# LOCALIZATION NOTE (browserConsoleCmd.commandkey)
+# Used for toggling the browser console from the detached toolbox window
+# Needs to match browserConsoleCmd.commandkey from browser.dtd
+browserConsoleCmd.commandkey=j
+
+# LOCALIZATION NOTE (pickButton.tooltip)
+# This is the tooltip of the pick button in the toolbox toolbar
+pickButton.tooltip=Pick an element from the page
+
+# LOCALIZATION NOTE (sidebar.showAllTabs.tooltip)
+# This is the tooltip shown when hover over the '…' button in the tabbed side
+# bar, when there's no enough space to show all tabs at once
+sidebar.showAllTabs.tooltip=All tabs
+
+# LOCALIZATION NOTE (toolbox.noContentProcessForTab.message)
+# Used as a message in the alert displayed when trying to open a browser
+# content toolbox and there is no content process running for the current tab
+toolbox.noContentProcessForTab.message=No content process for this tab.
+
+# LOCALIZATION NOTE (toolbox.viewCssSourceInStyleEditor.label)
+# Used as a message in either tooltips or contextual menu items to open the
+# corresponding URL as a css file in the Style-Editor tool.
+# DEV NOTE: Mostly used wherever toolbox.viewSourceInStyleEditor is used.
+toolbox.viewCssSourceInStyleEditor.label=Open File in Style-Editor
+
+# LOCALIZATION NOTE (toolbox.viewJsSourceInDebugger.label)
+# Used as a message in either tooltips or contextual menu items to open the
+# corresponding URL as a js file in the Debugger tool.
+# DEV NOTE: Mostly used wherever toolbox.viewSourceInDebugger is used.
+toolbox.viewJsSourceInDebugger.label=Open File in Debugger
+
+toolbox.resumeOrderWarning=Page did not resume after the debugger was attached. To fix this, please close and re-open the toolbox.
+
+# LOCALIZATION NOTE (toolbox.options.key)
+# Key shortcut used to open the options panel
+toolbox.options.key=CmdOrCtrl+Shift+O
+
+# LOCALIZATION NOTE (toolbox.help.key)
+# Key shortcut used to open the options panel
+toolbox.help.key=F1
+
+# LOCALIZATION NOTE (toolbox.nextTool.key)
+# Key shortcut used to select the next tool
+toolbox.nextTool.key=CmdOrCtrl+]
+
+# LOCALIZATION NOTE (toolbox.previousTool.key)
+# Key shortcut used to select the previous tool
+toolbox.previousTool.key=CmdOrCtrl+[
+
+# LOCALIZATION NOTE (toolbox.zoom*.key)
+# Key shortcuts used to zomm in/out or reset the toolbox
+# Should match fullZoom*Cmd.commandkey values from browser.dtd
+toolbox.zoomIn.key=CmdOrCtrl+Plus
+toolbox.zoomIn2.key=CmdOrCtrl+=
+toolbox.zoomIn3.key=
+
+toolbox.zoomOut.key=CmdOrCtrl+-
+toolbox.zoomOut2.key=
+
+toolbox.zoomReset.key=CmdOrCtrl+0
+toolbox.zoomReset2.key=
+
+# LOCALIZATION NOTE (toolbox.reload*.key)
+# Key shortcuts used to reload the page
+toolbox.reload.key=CmdOrCtrl+R
+toolbox.reload2.key=F5
+
+# LOCALIZATION NOTE (toolbox.forceReload*.key)
+# Key shortcuts used to force reload of the page by bypassing caches
+toolbox.forceReload.key=CmdOrCtrl+Shift+R
+toolbox.forceReload2.key=CmdOrCtrl+F5
+
+# LOCALIZATION NOTE (toolbox.minimize.key)
+# Key shortcut used to minimize the toolbox
+toolbox.minimize.key=CmdOrCtrl+Shift+U
+
+# LOCALIZATION NOTE (toolbox.toggleHost.key)
+# Key shortcut used to move the toolbox in bottom or side of the browser window
+toolbox.toggleHost.key=CmdOrCtrl+Shift+D
diff --git a/devtools/client/locales/en-US/webConsole.dtd b/devtools/client/locales/en-US/webConsole.dtd
new file mode 100644
index 000000000..17ae02699
--- /dev/null
+++ b/devtools/client/locales/en-US/webConsole.dtd
@@ -0,0 +1,101 @@
+<!-- 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/. -->
+
+<!-- LOCALIZATION NOTE : FILE The correct localization of this file might be to
+ - keep it in English, or another language commonly spoken among web developers.
+ - You want to make that choice consistent across the developer tools.
+ - A good criteria is the language in which you'd find the best
+ - documentation on web development on the web. -->
+
+<!ENTITY window.title "Web Console">
+<!ENTITY browserConsole.title "Browser Console">
+
+<!-- LOCALIZATION NOTE (openURL.label): You can see this string in the Web
+ - Console context menu. -->
+<!ENTITY openURL.label "Open URL in New Tab">
+<!ENTITY openURL.accesskey "T">
+
+<!-- LOCALIZATION NOTE (btnPageNet.label): This string is used for the menu
+ - button that allows users to toggle the network logging output.
+ - This string and the following strings toggle various kinds of output
+ - filters. -->
+<!ENTITY btnPageNet.label "Net">
+<!ENTITY btnPageNet.tooltip "Log network access">
+<!ENTITY btnPageNet.accesskey "N">
+<!-- LOCALIZATION NOTE (btnPageNet.accesskeyMacOSX): This string is used as
+ - access key for the menu button that allows users to toggle the network
+ - logging output. On MacOSX accesskeys are available with Ctrl-*. Please make
+ - sure you do not use the following letters: A, E, N and P. These are used
+ - for editing commands in text inputs. -->
+<!ENTITY btnPageNet.accesskeyMacOSX "t">
+<!ENTITY btnPageCSS.label "CSS">
+<!ENTITY btnPageCSS.tooltip2 "Log CSS errors and warnings">
+<!ENTITY btnPageCSS.accesskey "C">
+<!ENTITY btnPageJS.label "JS">
+<!ENTITY btnPageJS.tooltip "Log JavaScript exceptions">
+<!ENTITY btnPageJS.accesskey "J">
+<!ENTITY btnPageSecurity.label "Security">
+<!ENTITY btnPageSecurity.tooltip "Log security errors and warnings">
+<!ENTITY btnPageSecurity.accesskey "u">
+
+<!-- LOCALIZATION NOTE (btnPageLogging): This is used as the text of the
+ - the toolbar. It shows or hides messages that the web developer inserted on
+ - the page for debugging purposes, using calls such console.log() and
+ - console.error(). -->
+<!ENTITY btnPageLogging.label "Logging">
+<!ENTITY btnPageLogging.tooltip "Log messages sent to the window.console object">
+<!ENTITY btnPageLogging.accesskey3 "L">
+<!ENTITY btnConsoleErrors "Errors">
+<!ENTITY btnConsoleInfo "Info">
+<!ENTITY btnConsoleWarnings "Warnings">
+<!ENTITY btnConsoleLog "Log">
+<!ENTITY btnConsoleXhr "XHR">
+<!ENTITY btnConsoleReflows "Reflows">
+
+<!-- LOCALIZATION NOTE (btnServerLogging): This is used as the text of the
+ - the toolbar. It shows or hides messages that the web developer inserted on
+ - the page for debugging purposes, using calls on the HTTP server. -->
+<!ENTITY btnServerLogging.label "Server">
+<!ENTITY btnServerLogging.tooltip "Log messages received from a web server">
+<!ENTITY btnServerLogging.accesskey "S">
+<!ENTITY btnServerErrors "Errors">
+<!ENTITY btnServerInfo "Info">
+<!ENTITY btnServerWarnings "Warnings">
+<!ENTITY btnServerLog "Log">
+
+<!-- LOCALIZATION NODE (btnConsoleSharedWorkers) the term "Shared Workers"
+ - should not be translated. -->
+<!ENTITY btnConsoleSharedWorkers "Shared Workers">
+<!-- LOCALIZATION NODE (btnConsoleServiceWorkers) the term "Service Workers"
+ - should not be translated. -->
+<!ENTITY btnConsoleServiceWorkers "Service Workers">
+<!-- LOCALIZATION NODE (btnConsoleWindowlessWorkers) the term "Workers"
+ - should not be translated. -->
+<!ENTITY btnConsoleWindowlessWorkers "Add-on or Chrome Workers">
+
+<!ENTITY filterOutput.placeholder "Filter output">
+<!ENTITY btnClear.label "Clear">
+<!ENTITY btnClear.tooltip "Clear the Web Console output">
+<!ENTITY btnClear.accesskey "r">
+
+<!ENTITY fullZoomEnlargeCmd.commandkey "+">
+<!ENTITY fullZoomEnlargeCmd.commandkey2 "="> <!-- + is above this key on many keyboards -->
+<!ENTITY fullZoomEnlargeCmd.commandkey3 "">
+
+<!ENTITY fullZoomReduceCmd.commandkey "-">
+<!ENTITY fullZoomReduceCmd.commandkey2 "">
+
+<!ENTITY fullZoomResetCmd.commandkey "0">
+<!ENTITY fullZoomResetCmd.commandkey2 "">
+
+<!ENTITY copyURLCmd.label "Copy Link Location">
+<!ENTITY copyURLCmd.accesskey "a">
+
+<!ENTITY closeCmd.key "W">
+<!ENTITY findCmd.key "F">
+<!ENTITY clearOutputCtrl.key "L">
+<!ENTITY openInVarViewCmd.label "Open in Variables View">
+<!ENTITY openInVarViewCmd.accesskey "V">
+<!ENTITY storeAsGlobalVar.label "Store as global variable">
+<!ENTITY storeAsGlobalVar.accesskey "S">
diff --git a/devtools/client/locales/en-US/webaudioeditor.dtd b/devtools/client/locales/en-US/webaudioeditor.dtd
new file mode 100644
index 000000000..778c24cb7
--- /dev/null
+++ b/devtools/client/locales/en-US/webaudioeditor.dtd
@@ -0,0 +1,53 @@
+<!-- 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/. -->
+
+<!-- LOCALIZATION NOTE : FILE This file contains the Debugger strings -->
+<!-- LOCALIZATION NOTE : FILE Do not translate commandkey -->
+
+<!-- LOCALIZATION NOTE : FILE The correct localization of this file might be to
+ - keep it in English, or another language commonly spoken among web developers.
+ - You want to make that choice consistent across the developer tools.
+ - A good criteria is the language in which you'd find the best
+ - documentation on web development on the web. -->
+
+<!-- LOCALIZATION NOTE (webAudioEditorUI.reloadNotice1): This is the label shown
+ - on the button that triggers a page refresh. -->
+<!ENTITY webAudioEditorUI.reloadNotice1 "Reload">
+
+<!-- LOCALIZATION NOTE (webAudioEditorUI.reloadNotice2): This is the label shown
+ - along with the button that triggers a page refresh. -->
+<!ENTITY webAudioEditorUI.reloadNotice2 "the page to view and edit the audio context.">
+
+<!-- LOCALIZATION NOTE (webAudioEditorUI.emptyNotice): This is the label shown
+ - while the page is refreshing and the tool waits for a audio context. -->
+<!ENTITY webAudioEditorUI.emptyNotice "Waiting for an audio context to be created…">
+
+<!-- LOCALIZATION NOTE (webAudioEditorUI.tab.properties2): This is the label shown
+ - for the properties tab view. -->
+<!ENTITY webAudioEditorUI.tab.properties2 "Properties">
+
+<!-- LOCALIZATION NOTE (webAudioEditorUI.tab.automation): This is the label shown
+ - for the automation tab view. -->
+<!ENTITY webAudioEditorUI.tab.automation "Automation">
+
+<!-- LOCALIZATION NOTE (webAudioEditorUI.inspectorTitle): This is the title for the
+ - AudioNode inspector view. -->
+<!ENTITY webAudioEditorUI.inspectorTitle "AudioNode Inspector">
+
+<!-- LOCALIZATION NOTE (webAudioEditorUI.inspectorEmpty): This is the title for the
+ - AudioNode inspector view empty message. -->
+<!ENTITY webAudioEditorUI.inspectorEmpty "No AudioNode selected.">
+
+<!-- LOCALIZATION NOTE (webAudioEditorUI.propertiesEmpty): This is the title for the
+ - AudioNode inspector view properties tab empty message. -->
+<!ENTITY webAudioEditorUI.propertiesEmpty "Node does not have any properties.">
+
+<!-- LOCALIZATION NOTE (webAudioEditorUI.automationEmpty): This is the title for the
+ - AudioNode inspector view automation tab empty message. -->
+<!ENTITY webAudioEditorUI.automationEmpty "Node does not have any AudioParams.">
+
+<!-- LOCALIZATION NOTE (webAudioEditorUI.automationNoEvents): This is the title for the
+ - AudioNode inspector view automation tab message when there are no automation
+ - events. -->
+<!ENTITY webAudioEditorUI.automationNoEvents "AudioParam does not have any automation events.">
diff --git a/devtools/client/locales/en-US/webaudioeditor.properties b/devtools/client/locales/en-US/webaudioeditor.properties
new file mode 100644
index 000000000..3cb9c93ef
--- /dev/null
+++ b/devtools/client/locales/en-US/webaudioeditor.properties
@@ -0,0 +1,20 @@
+# 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/.
+
+# LOCALIZATION NOTE These strings are used inside the Web Audio tool
+# which is available in the developer tools' toolbox, once
+# enabled in the developer tools' preference "Web Audio".
+# The correct localization of this file might be to keep it in
+# English, or another language commonly spoken among web developers.
+# You want to make that choice consistent across the developer tools.
+# A good criteria is the language in which you'd find the best
+# documentation on web development on the web.
+
+# LOCALIZATION NOTE (collapseInspector): This is the tooltip for the button
+# that collapses the inspector in the web audio tool UI.
+collapseInspector=Collapse inspector
+
+# LOCALIZATION NOTE (expandInspector): This is the tooltip for the button
+# that expands the inspector in the web audio tool UI.
+expandInspector=Expand inspector
diff --git a/devtools/client/locales/en-US/webconsole.properties b/devtools/client/locales/en-US/webconsole.properties
new file mode 100644
index 000000000..b16cdb0ae
--- /dev/null
+++ b/devtools/client/locales/en-US/webconsole.properties
@@ -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/.
+
+# LOCALIZATION NOTE
+# The correct localization of this file might be to keep it in
+# English, or another language commonly spoken among web developers.
+# You want to make that choice consistent across the developer tools.
+# A good criteria is the language in which you'd find the best
+# documentation on web development on the web.
+
+
+# LOCALIZATION NOTE (timestampFormat): %1$02S = hours (24-hour clock),
+# %2$02S = minutes, %3$02S = seconds, %4$03S = milliseconds.
+timestampFormat=%02S:%02S:%02S.%03S
+
+helperFuncUnsupportedTypeError=Can’t call pprint on this type of object.
+
+# LOCALIZATION NOTE (NetworkPanel.deltaDurationMS): this string is used to
+# show the duration between two network events (e.g request and response
+# header or response header and response body). Parameters: %S is the duration.
+NetworkPanel.durationMS=%Sms
+
+ConsoleAPIDisabled=The Web Console logging API (console.log, console.info, console.warn, console.error) has been disabled by a script on this page.
+
+# LOCALIZATION NOTE (webConsoleWindowTitleAndURL): the Web Console floating
+# panel title. For RTL languages you need to set the LRM in the string to give
+# the URL the correct direction. Parameters: %S is the web page URL.
+webConsoleWindowTitleAndURL=Web Console - %S
+
+# LOCALIZATION NOTE (webConsoleXhrIndicator): the indicator displayed before
+# a URL in the Web Console that was requested using an XMLHttpRequest.
+# Should probably be the same as &btnConsoleXhr; in webConsole.dtd
+webConsoleXhrIndicator=XHR
+
+# LOCALIZATION NOTE (webConsoleMixedContentWarning): the message displayed
+# after a URL in the Web Console that has been flagged for Mixed Content (i.e.
+# http content in an https page).
+webConsoleMixedContentWarning=Mixed Content
+
+# LOCALIZATION NOTE (webConsoleMoreInfoLabel): the more info tag displayed
+# after security related web console messages.
+webConsoleMoreInfoLabel=Learn More
+
+# LOCALIZATION NOTE (scratchpad.linkText): the text used in the right hand
+# side of the Web Console command line when JavaScript is being entered, to
+# indicate how to jump into scratchpad mode.
+scratchpad.linkText=Shift+RETURN - Open in Scratchpad
+
+# LOCALIZATION NOTE (reflow.*): the console displays reflow activity.
+# We can get 2 kind of lines: with JS link or without JS link. It looks like
+# that:
+# reflow: 12ms
+# reflow: 12ms function foobar, file.js line 42
+# The 2nd line, from "function" to the end of the line, is a link to the
+# JavaScript debugger.
+reflow.messageWithNoLink=reflow: %Sms
+reflow.messageWithLink=reflow: %Sms\u0020
+reflow.messageLinkText=function %1$S, %2$S line %3$S
+
+# LOCALIZATION NOTE (stacktrace.anonymousFunction): this string is used to
+# display JavaScript functions that have no given name - they are said to be
+# anonymous. Test console.trace() in the webconsole.
+stacktrace.anonymousFunction=<anonymous>
+
+# LOCALIZATION NOTE (stacktrace.asyncStack): this string is used to
+# indicate that a given stack frame has an async parent.
+# %S is the "Async Cause" of the frame.
+stacktrace.asyncStack=(Async: %S)
+
+# LOCALIZATION NOTE (timerStarted): this string is used to display the result
+# of the console.time() call. Parameters: %S is the name of the timer.
+timerStarted=%S: timer started
+
+# LOCALIZATION NOTE (timeEnd): this string is used to display the result of
+# the console.timeEnd() call. Parameters: %1$S is the name of the timer, %2$S
+# is the number of milliseconds.
+timeEnd=%1$S: %2$Sms
+
+# LOCALIZATION NOTE (consoleCleared): this string is displayed when receiving a
+# call to console.clear() to let the user know the previous messages of the
+# console have been removed programmatically.
+consoleCleared=Console was cleared.
+
+# LOCALIZATION NOTE (noCounterLabel): this string is used to display
+# count-messages with no label provided.
+noCounterLabel=<no label>
+
+# LOCALIZATION NOTE (noGroupLabel): this string is used to display
+# console.group messages with no label provided.
+noGroupLabel=<no group label>
+
+# LOCALIZATION NOTE (Autocomplete.blank): this string is used when inputnode
+# string containing anchor doesn't matches to any property in the content.
+Autocomplete.blank= <- no result
+
+maxTimersExceeded=The maximum allowed number of timers in this page was exceeded.
+
+# LOCALIZATION NOTE (maxCountersExceeded): Error message shown when the maximum
+# number of console.count()-counters was exceeded.
+maxCountersExceeded=The maximum allowed number of counters in this page was exceeded.
+
+# LOCALIZATION NOTE (longStringEllipsis): the string displayed after a long
+# string. This string is clickable such that the rest of the string is
+# retrieved from the server.
+longStringEllipsis=[…]
+
+# LOCALIZATION NOTE (longStringTooLong): the string displayed after the user
+# tries to expand a long string.
+longStringTooLong=The string you are trying to view is too long to be displayed by the Web Console.
+
+# LOCALIZATION NOTE (connectionTimeout): message displayed when the Remote Web
+# Console fails to connect to the server due to a timeout.
+connectionTimeout=Connection timeout. Check the Error Console on both ends for potential error messages. Reopen the Web Console to try again.
+
+# LOCALIZATION NOTE (propertiesFilterPlaceholder): this is the text that
+# appears in the filter text box for the properties view container.
+propertiesFilterPlaceholder=Filter properties
+
+# LOCALIZATION NOTE (emptyPropertiesList): the text that is displayed in the
+# properties pane when there are no properties to display.
+emptyPropertiesList=No properties to display
+
+# LOCALIZATION NOTE (messageRepeats.tooltip2): the tooltip text that is displayed
+# when you hover the red bubble that shows how many times a message is repeated
+# in the web console output.
+# This is a semi-colon list of plural forms.
+# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
+# #1 number of message repeats
+# example: 3 repeats
+messageRepeats.tooltip2=#1 repeat;#1 repeats
+
+# LOCALIZATION NOTE (openNodeInInspector): the text that is displayed in a
+# tooltip when hovering over the inspector icon next to a DOM Node in the console
+# output
+openNodeInInspector=Click to select the node in the inspector
+
+# LOCALIZATION NOTE (cdFunctionInvalidArgument): the text that is displayed when
+# cd() is invoked with an invalid argument.
+cdFunctionInvalidArgument=Cannot cd() to the given window. Invalid argument.
+
+# LOCALIZATION NOTE (selfxss.msg): the text that is displayed when
+# a new user of the developer tools pastes code into the console
+# %1 is the text of selfxss.okstring
+selfxss.msg=Scam Warning: Take care when pasting things you don’t understand. This could allow attackers to steal your identity or take control of your computer. Please type ‘%S’ below (no need to press enter) to allow pasting.
+
+# LOCALIZATION NOTE (selfxss.msg): the string to be typed
+# in by a new user of the developer tools when they receive the sefxss.msg prompt.
+# Please avoid using non-keyboard characters here
+selfxss.okstring=allow pasting
+
+# LOCALIZATION NOTE (messageToggleDetails): the text that is displayed when
+# you hover the arrow for expanding/collapsing the message details. For
+# console.error() and other messages we show the stacktrace.
+messageToggleDetails=Show/hide message details.
+
+# LOCALIZATION NOTE (groupToggle): the text that is displayed when
+# you hover the arrow for expanding/collapsing the messages of a group.
+groupToggle=Show/hide group.
+
+# LOCALIZATION NOTE (emptySlotLabel): the text is displayed when an Array
+# with empty slots is printed to the console.
+# This is a semi-colon list of plural forms.
+# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
+# #1 number of empty slots
+# example: 1 empty slot
+# example: 5 empty slots
+emptySlotLabel=#1 empty slot;#1 empty slots
+
+# LOCALIZATION NOTE (table.index, table.iterationIndex, table.key, table.value):
+# the column header displayed in the console table widget.
+table.index=(index)
+table.iterationIndex=(iteration index)
+table.key=Key
+table.value=Values
+
+# LOCALIZATION NOTE (severity.error, severity.warn, severity.info, severity.log):
+# tooltip for icons next to console output
+severity.error=Error
+severity.warn=Warning
+severity.info=Info
+severity.log=Log
+
+# LOCALIZATION NOTE (level.error, level.warn, level.info, level.log, level.debug):
+# tooltip for icons next to console output
+level.error=Error
+level.warn=Warning
+level.info=Info
+level.log=Log
+level.debug=Debug
+
+# LOCALIZATION NOTE (webconsole.find.key)
+# Key shortcut used to focus the search box on upper right of the console
+webconsole.find.key=CmdOrCtrl+F
+
+# LOCALIZATION NOTE (webconsole.close.key)
+# Key shortcut used to close the Browser console (doesn't work in regular web console)
+webconsole.close.key=CmdOrCtrl+W
+
+# LOCALIZATION NOTE (webconsole.clear.key*)
+# Key shortcut used to clear the console output
+webconsole.clear.key=Ctrl+Shift+L
+webconsole.clear.keyOSX=Ctrl+L
diff --git a/devtools/client/locales/en-US/webide.dtd b/devtools/client/locales/en-US/webide.dtd
new file mode 100644
index 000000000..5e1a80ccd
--- /dev/null
+++ b/devtools/client/locales/en-US/webide.dtd
@@ -0,0 +1,218 @@
+<!-- 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/. -->
+
+<!ENTITY windowTitle "Firefox WebIDE">
+
+<!ENTITY projectMenu_label "Project">
+<!ENTITY projectMenu_accesskey "P">
+<!ENTITY projectMenu_newApp_label "New App…">
+<!ENTITY projectMenu_newApp_accesskey "N">
+<!ENTITY projectMenu_importPackagedApp_label "Open Packaged App…">
+<!ENTITY projectMenu_importPackagedApp_accesskey "P">
+<!ENTITY projectMenu_importHostedApp_label "Open Hosted App…">
+<!ENTITY projectMenu_importHostedApp_accesskey "H">
+<!ENTITY projectMenu_selectApp_label "Open App…">
+<!ENTITY projectMenu_selectApp_accesskey "O">
+<!ENTITY projectMenu_play_label "Install and Run">
+<!ENTITY projectMenu_play_accesskey "I">
+<!ENTITY projectMenu_stop_label "Stop App">
+<!ENTITY projectMenu_stop_accesskey "S">
+<!ENTITY projectMenu_debug_label "Debug App">
+<!ENTITY projectMenu_debug_accesskey "D">
+<!ENTITY projectMenu_remove_label "Remove Project">
+<!ENTITY projectMenu_remove_accesskey "R">
+<!ENTITY projectMenu_showPrefs_label "Preferences">
+<!ENTITY projectMenu_showPrefs_accesskey "e">
+<!ENTITY projectMenu_manageComponents_label "Manage Extra Components">
+<!ENTITY projectMenu_manageComponents_accesskey "M">
+<!ENTITY projectMenu_refreshTabs_label "Refresh Tabs">
+
+<!ENTITY runtimeMenu_label "Runtime">
+<!ENTITY runtimeMenu_accesskey "R">
+<!ENTITY runtimeMenu_disconnect_label "Disconnect">
+<!ENTITY runtimeMenu_disconnect_accesskey "D">
+<!ENTITY runtimeMenu_showPermissionTable_label "Permissions Table">
+<!ENTITY runtimeMenu_showPermissionTable_accesskey "P">
+<!ENTITY runtimeMenu_takeScreenshot_label "Screenshot">
+<!ENTITY runtimeMenu_takeScreenshot_accesskey "S">
+<!ENTITY runtimeMenu_showDetails_label "Runtime Info">
+<!ENTITY runtimeMenu_showDetails_accesskey "E">
+<!ENTITY runtimeMenu_showMonitor_label "Monitor">
+<!ENTITY runtimeMenu_showMonitor_accesskey "M">
+<!ENTITY runtimeMenu_showDevicePrefs_label "Device Preferences">
+<!ENTITY runtimeMenu_showDevicePrefs_accesskey "D">
+<!ENTITY runtimeMenu_showSettings_label "Device Settings">
+<!ENTITY runtimeMenu_showSettings_accesskey "s">
+
+<!ENTITY viewMenu_label "View">
+<!ENTITY viewMenu_accesskey "V">
+<!ENTITY viewMenu_toggleEditor_label "Toggle Editor">
+<!ENTITY viewMenu_toggleEditor_accesskey "E">
+<!ENTITY viewMenu_zoomin_label "Zoom In">
+<!ENTITY viewMenu_zoomin_accesskey "I">
+<!ENTITY viewMenu_zoomout_label "Zoom Out">
+<!ENTITY viewMenu_zoomout_accesskey "O">
+<!ENTITY viewMenu_resetzoom_label "Reset Zoom">
+<!ENTITY viewMenu_resetzoom_accesskey "R">
+
+<!ENTITY projectButton_label "Open App">
+<!ENTITY runtimeButton_label "Select Runtime">
+
+<!-- We try to repicate Firefox' bindings: -->
+<!-- quit app -->
+<!ENTITY key_quit "W">
+<!-- open menu -->
+<!ENTITY key_showProjectPanel "O">
+<!-- reload app -->
+<!ENTITY key_play "R">
+<!-- show toolbox -->
+<!ENTITY key_toggleToolbox "VK_F12">
+<!-- toggle sidebar -->
+<!ENTITY key_toggleEditor "B">
+<!-- zoom -->
+<!ENTITY key_zoomin "+">
+<!ENTITY key_zoomin2 "=">
+<!ENTITY key_zoomout "-">
+<!ENTITY key_resetzoom "0">
+
+<!ENTITY projectPanel_myProjects "My Projects">
+<!ENTITY projectPanel_runtimeApps "Runtime Apps">
+<!ENTITY projectPanel_tabs "Tabs">
+<!ENTITY runtimePanel_usb "USB Devices">
+<!ENTITY runtimePanel_wifi "Wi-Fi Devices">
+<!ENTITY runtimePanel_simulator "Simulators">
+<!ENTITY runtimePanel_other "Other">
+<!ENTITY runtimePanel_installsimulator "Install Simulator">
+<!ENTITY runtimePanel_noadbhelper "Install ADB Helper">
+<!ENTITY runtimePanel_nousbdevice "Can’t see your device?">
+<!ENTITY runtimePanel_refreshDevices_label "Refresh Devices">
+
+<!-- Lense -->
+<!ENTITY details_valid_header "valid">
+<!ENTITY details_warning_header "warnings">
+<!ENTITY details_error_header "errors">
+<!ENTITY details_description "Description">
+<!ENTITY details_location "Location">
+<!ENTITY details_manifestURL "App ID">
+<!ENTITY details_removeProject_button "Remove Project">
+<!ENTITY details_showPrepackageLog_button "Show Pre-package Log">
+
+<!-- New App -->
+<!ENTITY newAppWindowTitle "New App">
+<!ENTITY newAppHeader "Select template">
+<!ENTITY newAppLoadingTemplate "Loading templates…">
+<!ENTITY newAppProjectName "Project Name:">
+
+
+<!-- Decks -->
+
+<!ENTITY deck_close "Close">
+
+<!-- Addons -->
+<!ENTITY addons_title "Extra Components">
+<!ENTITY addons_aboutaddons "Open Add-ons Manager">
+
+<!-- Prefs -->
+<!ENTITY prefs_title "Preferences">
+<!ENTITY prefs_editor_title "Editor">
+<!ENTITY prefs_general_title "General">
+<!ENTITY prefs_restore "Restore Defaults">
+<!ENTITY prefs_manage_components "Manage Extra Components">
+<!ENTITY prefs_options_autoconnectruntime "Reconnect to previous runtime">
+<!ENTITY prefs_options_autoconnectruntime_tooltip "Reconnect to previous runtime when WebIDE starts">
+<!ENTITY prefs_options_rememberlastproject "Remember last project">
+<!ENTITY prefs_options_rememberlastproject_tooltip "Restore previous project when WebIDE starts">
+<!ENTITY prefs_options_templatesurl "Templates URL">
+<!ENTITY prefs_options_templatesurl_tooltip "Index of available templates">
+<!ENTITY prefs_options_showeditor "Show editor">
+<!ENTITY prefs_options_showeditor_tooltip "Show internal editor">
+<!ENTITY prefs_options_tabsize "Tab size">
+<!ENTITY prefs_options_expandtab "Soft tabs">
+<!ENTITY prefs_options_expandtab_tooltip "Use spaces instead of the tab character">
+<!ENTITY prefs_options_detectindentation "Autoindent">
+<!ENTITY prefs_options_detectindentation_tooltip "Guess indentation based on source content">
+<!ENTITY prefs_options_autocomplete "Autocomplete">
+<!ENTITY prefs_options_autocomplete_tooltip "Enable code autocompletion">
+<!ENTITY prefs_options_autoclosebrackets "Autoclose brackets">
+<!ENTITY prefs_options_autoclosebrackets_tooltip "Automatically insert closing brackets">
+<!ENTITY prefs_options_keybindings "Keybindings">
+<!ENTITY prefs_options_keybindings_default "Default">
+<!ENTITY prefs_options_autosavefiles "Autosave files">
+<!ENTITY prefs_options_autosavefiles_tooltip "Automatically save edited files before running project">
+
+<!-- Permissions Table -->
+<!ENTITY permissionstable_title "Permissions Table">
+<!ENTITY permissionstable_name_header "Name">
+
+<!-- Runtime Details -->
+<!ENTITY runtimedetails_title "Runtime Info">
+<!ENTITY runtimedetails_adbIsRoot "ADB is root: ">
+<!ENTITY runtimedetails_summonADBRoot "root device">
+<!ENTITY runtimedetails_ADBRootWarning "(requires unlocked bootloader)">
+<!ENTITY runtimedetails_unrestrictedPrivileges "Unrestricted DevTools privileges: ">
+<!ENTITY runtimedetails_requestPrivileges "request higher privileges">
+<!ENTITY runtimedetails_privilegesWarning "(Will reboot device. Requires root access.)">
+
+<!-- Device Preferences and Settings -->
+<!ENTITY device_typeboolean "Boolean">
+<!ENTITY device_typenumber "Integer">
+<!ENTITY device_typestring "String">
+<!ENTITY device_typeobject "Object">
+<!ENTITY device_typenone "Select a type">
+
+<!-- Device Preferences -->
+<!ENTITY devicepreference_title "Device Preferences">
+<!ENTITY devicepreference_search "Search preferences">
+<!ENTITY devicepreference_newname "New preference name">
+<!ENTITY devicepreference_newtext "Preference value">
+<!ENTITY devicepreference_addnew "Add new preference">
+
+<!-- Device Settings -->
+<!ENTITY devicesetting_title "Device Settings">
+<!ENTITY devicesetting_search "Search settings">
+<!ENTITY devicesetting_newname "New setting name">
+<!ENTITY devicesetting_newtext "Setting value">
+<!ENTITY devicesetting_addnew "Add new setting">
+
+<!-- Monitor -->
+<!ENTITY monitor_title "Monitor">
+<!ENTITY monitor_help "Help">
+
+<!-- WiFi Authentication -->
+<!-- LOCALIZATION NOTE (wifi_auth_header): The header displayed on the dialog
+ that instructs the user to transfer an authentication token to the
+ server. -->
+<!ENTITY wifi_auth_header "Client Identification">
+<!-- LOCALIZATION NOTE (wifi_auth_scan_request): Instructions requesting the
+ user to transfer authentication info by scanning a QR code. -->
+<!ENTITY wifi_auth_scan_request "The endpoint you are connecting to needs more information to authenticate this connection. Please scan the QR code below via the prompt on your other device.">
+<!-- LOCALIZATION NOTE (wifi_auth_no_scanner): Link text to assist users with
+ devices that can't scan a QR code. -->
+<!ENTITY wifi_auth_no_scanner "No QR scanner prompt?">
+<!-- LOCALIZATION NOTE (wifi_auth_yes_scanner): Link text to assist users with
+ devices that can scan a QR code. -->
+<!ENTITY wifi_auth_yes_scanner "Have a QR scanner prompt?">
+<!-- LOCALIZATION NOTE (wifi_auth_token_request): Instructions requesting the
+ user to transfer authentication info by transferring a token. -->
+<!ENTITY wifi_auth_token_request "If your other device asks for a token instead of scanning a QR code, please copy the value below to the other device:">
+<!ENTITY wifi_auth_qr_size_note "If the QR code appears too small for the connection to be successfully established, try zooming or enlarging the window.">
+
+<!-- Logs panel -->
+<!ENTITY logs_title "Pre-packaging Command Logs">
+
+<!-- Simulator Options -->
+<!ENTITY simulator_title "Simulator Options">
+<!ENTITY simulator_remove "Delete Simulator">
+<!ENTITY simulator_reset "Restore Defaults">
+<!ENTITY simulator_name "Name">
+<!ENTITY simulator_software "Software">
+<!ENTITY simulator_version "Version">
+<!ENTITY simulator_profile "Profile">
+<!ENTITY simulator_hardware "Hardware">
+<!ENTITY simulator_device "Device">
+<!ENTITY simulator_screenSize "Screen">
+<!ENTITY simulator_pixelRatio "Pixel Ratio">
+<!ENTITY simulator_tv_data "TV Simulation">
+<!ENTITY simulator_tv_data_open "Config Data">
+<!ENTITY simulator_tv_data_open_button "Open Config Directory…">
diff --git a/devtools/client/locales/en-US/webide.properties b/devtools/client/locales/en-US/webide.properties
new file mode 100644
index 000000000..2368ad7f1
--- /dev/null
+++ b/devtools/client/locales/en-US/webide.properties
@@ -0,0 +1,92 @@
+# 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/.
+
+title_noApp=Firefox WebIDE
+title_app=Firefox WebIDE: %S
+
+runtimeButton_label=Select Runtime
+projectButton_label=Open App
+
+mainProcess_label=Main Process
+
+local_runtime=Local Runtime
+remote_runtime=Remote Runtime
+remote_runtime_promptTitle=Remote Runtime
+remote_runtime_promptMessage=hostname:port
+
+importPackagedApp_title=Select Directory
+importHostedApp_title=Open Hosted App
+importHostedApp_header=Enter Manifest URL
+
+selectCustomBinary_title=Select custom B2G binary
+selectCustomProfile_title=Select custom Gaia profile
+
+notification_showTroubleShooting_label=Troubleshooting
+notification_showTroubleShooting_accesskey=T
+
+# LOCALIZATION NOTE (project_tab_loading): This is shown as a temporary tab
+# title for browser tab projects when the tab is still loading.
+project_tab_loading=Loading…
+
+# These messages appear in a notification box when an error occur.
+
+error_cantInstallNotFullyConnected=Can’t install project. Not fully connected.
+error_cantInstallValidationErrors=Can’t install project. Validation errors.
+error_listRunningApps=Can’t get app list from device
+
+# Variable: name of the operation (in english)
+error_operationTimeout=Operation timed out: %1$S
+error_operationFail=Operation failed: %1$S
+
+# Variable: app name
+error_cantConnectToApp=Can’t connect to app: %1$S
+
+# Variable: error message (in english)
+error_cantFetchAddonsJSON=Can’t fetch the add-on list: %S
+
+error_appProjectsLoadFailed=Unable to load project list. This can occur if you’ve used this profile with a newer version of Firefox.
+error_folderCreationFailed=Unable to create project folder in the selected directory.
+
+# Variable: runtime app build ID (looks like this %Y%M%D format) and firefox build ID (same format)
+error_runtimeVersionTooRecent=The connected runtime has a more recent build date (%1$S) than your desktop Firefox (%2$S) does. This is an unsupported setup and may cause DevTools to fail. Please update Firefox.
+
+addons_stable=stable
+addons_unstable=unstable
+# LOCALIZATION NOTE (addons_simulator_label): This label is shown as the name of
+# a given simulator version in the "Manage Simulators" pane. %1$S: Firefox OS
+# version in the simulator, ex. 1.3. %2$S: Simulator stability label, ex.
+# "stable" or "unstable".
+addons_simulator_label=Firefox OS %1$S Simulator (%2$S)
+addons_install_button=install
+addons_uninstall_button=uninstall
+addons_adb_label=ADB Helper Add-on
+addons_adapters_label=Tools Adapters Add-on
+addons_adb_warning=USB devices won’t be detected without this add-on
+addons_status_unknown=?
+addons_status_installed=Installed
+addons_status_uninstalled=Not Installed
+addons_status_preparing=preparing
+addons_status_downloading=downloading
+addons_status_installing=installing
+
+runtimedetails_checkno=no
+runtimedetails_checkyes=yes
+runtimedetails_checkunknown=unknown (requires ADB Helper 0.4.0 or later)
+runtimedetails_notUSBDevice=Not a USB device
+
+# Validation status
+status_tooltip=Validation status: %1$S
+status_valid=VALID
+status_warning=WARNINGS
+status_error=ERRORS
+status_unknown=UNKNOWN
+
+# Device preferences and settings
+device_reset_default=Reset to default
+
+# Simulator options
+simulator_custom_device=Custom
+simulator_custom_binary=Custom B2G binary…
+simulator_custom_profile=Custom Gaia profile…
+simulator_default_profile=Use default
diff --git a/devtools/client/locales/jar.mn b/devtools/client/locales/jar.mn
new file mode 100644
index 000000000..43ce3aef2
--- /dev/null
+++ b/devtools/client/locales/jar.mn
@@ -0,0 +1,8 @@
+#filter substitution
+# 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/.
+
+@AB_CD@.jar:
+% locale devtools @AB_CD@ %locale/@AB_CD@/devtools/client/
+ locale/@AB_CD@/devtools/client/ (%*)
diff --git a/devtools/client/locales/l10n.ini b/devtools/client/locales/l10n.ini
new file mode 100644
index 000000000..24f711466
--- /dev/null
+++ b/devtools/client/locales/l10n.ini
@@ -0,0 +1,12 @@
+; 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/.
+
+; This file is specifically to allow SeaMonkey and Thunderbird
+; to use the devtools client localizations from comm-*
+
+[general]
+depth = ../../..
+
+[compare]
+dirs = devtools/client
diff --git a/devtools/client/locales/moz.build b/devtools/client/locales/moz.build
new file mode 100644
index 000000000..aac3a838c
--- /dev/null
+++ b/devtools/client/locales/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+JAR_MANIFESTS += ['jar.mn']
diff --git a/devtools/client/memory/actions/allocations.js b/devtools/client/memory/actions/allocations.js
new file mode 100644
index 000000000..521319925
--- /dev/null
+++ b/devtools/client/memory/actions/allocations.js
@@ -0,0 +1,20 @@
+/* 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 { actions, ALLOCATION_RECORDING_OPTIONS } = require("../constants");
+
+exports.toggleRecordingAllocationStacks = function (front) {
+ return function* (dispatch, getState) {
+ dispatch({ type: actions.TOGGLE_RECORD_ALLOCATION_STACKS_START });
+
+ if (getState().recordingAllocationStacks) {
+ yield front.stopRecordingAllocations();
+ } else {
+ yield front.startRecordingAllocations(ALLOCATION_RECORDING_OPTIONS);
+ }
+
+ dispatch({ type: actions.TOGGLE_RECORD_ALLOCATION_STACKS_END });
+ };
+};
diff --git a/devtools/client/memory/actions/census-display.js b/devtools/client/memory/actions/census-display.js
new file mode 100644
index 000000000..348a8951a
--- /dev/null
+++ b/devtools/client/memory/actions/census-display.js
@@ -0,0 +1,34 @@
+/* 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 { assert } = require("devtools/shared/DevToolsUtils");
+const { actions } = require("../constants");
+const { refresh } = require("./refresh");
+
+exports.setCensusDisplayAndRefresh = function (heapWorker, display) {
+ return function* (dispatch, getState) {
+ dispatch(setCensusDisplay(display));
+ yield dispatch(refresh(heapWorker));
+ };
+};
+
+/**
+ * Clears out all cached census data in the snapshots and sets new display data
+ * for censuses.
+ *
+ * @param {censusDisplayModel} display
+ */
+const setCensusDisplay = exports.setCensusDisplay = function (display) {
+ assert(typeof display === "object"
+ && display
+ && display.breakdown
+ && display.breakdown.by,
+ `Breakdowns must be an object with a \`by\` property, attempted to set: ${uneval(display)}`);
+
+ return {
+ type: actions.SET_CENSUS_DISPLAY,
+ display,
+ };
+};
diff --git a/devtools/client/memory/actions/diffing.js b/devtools/client/memory/actions/diffing.js
new file mode 100644
index 000000000..70af307bb
--- /dev/null
+++ b/devtools/client/memory/actions/diffing.js
@@ -0,0 +1,201 @@
+/* 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 { assert, reportException } = require("devtools/shared/DevToolsUtils");
+const { actions, diffingState, viewState } = require("../constants");
+const telemetry = require("../telemetry");
+const {
+ getSnapshot,
+ censusIsUpToDate,
+ snapshotIsDiffable,
+ findSelectedSnapshot,
+} = require("../utils");
+// This is a circular dependency, so do not destructure the needed properties.
+const snapshotActions = require("./snapshot");
+
+/**
+ * Toggle diffing mode on or off.
+ */
+const toggleDiffing = exports.toggleDiffing = function () {
+ return function (dispatch, getState) {
+ dispatch({
+ type: actions.CHANGE_VIEW,
+ newViewState: getState().diffing ? viewState.CENSUS : viewState.DIFFING,
+ oldDiffing: getState().diffing,
+ oldSelected: findSelectedSnapshot(getState()),
+ });
+ };
+};
+
+/**
+ * Select the given snapshot for diffing.
+ *
+ * @param {snapshotModel} snapshot
+ */
+const selectSnapshotForDiffing = exports.selectSnapshotForDiffing = function (snapshot) {
+ assert(snapshotIsDiffable(snapshot),
+ "To select a snapshot for diffing, it must be diffable");
+ return { type: actions.SELECT_SNAPSHOT_FOR_DIFFING, snapshot };
+};
+
+/**
+ * Compute the difference between the first and second snapshots.
+ *
+ * @param {HeapAnalysesClient} heapWorker
+ * @param {snapshotModel} first
+ * @param {snapshotModel} second
+ */
+const takeCensusDiff = exports.takeCensusDiff = function (heapWorker, first, second) {
+ return function* (dispatch, getState) {
+ assert(snapshotIsDiffable(first),
+ `First snapshot must be in a diffable state, found ${first.state}`);
+ assert(snapshotIsDiffable(second),
+ `Second snapshot must be in a diffable state, found ${second.state}`);
+
+ let report, parentMap;
+ let display = getState().censusDisplay;
+ let filter = getState().filter;
+
+ if (censusIsUpToDate(filter, display, getState().diffing.census)) {
+ return;
+ }
+
+ do {
+ if (!getState().diffing
+ || getState().diffing.firstSnapshotId !== first.id
+ || getState().diffing.secondSnapshotId !== second.id) {
+ // If we stopped diffing or stopped and then started diffing a different
+ // pair of snapshots, then just give up with diffing this pair. In the
+ // latter case, a newly spawned task will handle the diffing for the new
+ // pair.
+ return;
+ }
+
+ display = getState().censusDisplay;
+ filter = getState().filter;
+
+ dispatch({
+ type: actions.TAKE_CENSUS_DIFF_START,
+ first,
+ second,
+ filter,
+ display,
+ });
+
+ let opts = display.inverted
+ ? { asInvertedTreeNode: true }
+ : { asTreeNode: true };
+ opts.filter = filter || null;
+
+ try {
+ ({ delta: report, parentMap } = yield heapWorker.takeCensusDiff(
+ first.path,
+ second.path,
+ { breakdown: display.breakdown },
+ opts));
+ } catch (error) {
+ reportException("actions/diffing/takeCensusDiff", error);
+ dispatch({ type: actions.DIFFING_ERROR, error });
+ return;
+ }
+ }
+ while (filter !== getState().filter
+ || display !== getState().censusDisplay);
+
+ dispatch({
+ type: actions.TAKE_CENSUS_DIFF_END,
+ first,
+ second,
+ report,
+ parentMap,
+ filter,
+ display,
+ });
+
+ telemetry.countDiff({ filter, display });
+ };
+};
+
+/**
+ * Ensure that the current diffing data is up to date with the currently
+ * selected display, filter, etc. If the state is not up-to-date, then a
+ * recompute is triggered.
+ *
+ * @param {HeapAnalysesClient} heapWorker
+ */
+const refreshDiffing = exports.refreshDiffing = function (heapWorker) {
+ return function* (dispatch, getState) {
+ if (getState().diffing.secondSnapshotId === null) {
+ return;
+ }
+
+ assert(getState().diffing.firstSnapshotId,
+ "Should have first snapshot id");
+
+ if (getState().diffing.state === diffingState.TAKING_DIFF) {
+ // There is an existing task that will ensure that the diffing data is
+ // up-to-date.
+ return;
+ }
+
+ const { firstSnapshotId, secondSnapshotId } = getState().diffing;
+
+ const first = getSnapshot(getState(), firstSnapshotId);
+ const second = getSnapshot(getState(), secondSnapshotId);
+ dispatch(takeCensusDiff(heapWorker, first, second));
+ };
+};
+
+/**
+ * Select the given snapshot for diffing and refresh the diffing data if
+ * necessary (for example, if two snapshots are now selected for diffing).
+ *
+ * @param {HeapAnalysesClient} heapWorker
+ * @param {snapshotModel} snapshot
+ */
+const selectSnapshotForDiffingAndRefresh = exports.selectSnapshotForDiffingAndRefresh = function (heapWorker, snapshot) {
+ return function* (dispatch, getState) {
+ assert(getState().diffing,
+ "If we are selecting for diffing, we must be in diffing mode");
+ dispatch(selectSnapshotForDiffing(snapshot));
+ yield dispatch(refreshDiffing(heapWorker));
+ };
+};
+
+/**
+ * Expand the given node in the diffing's census's delta-report.
+ *
+ * @param {CensusTreeNode} node
+ */
+const expandDiffingCensusNode = exports.expandDiffingCensusNode = function (node) {
+ return {
+ type: actions.EXPAND_DIFFING_CENSUS_NODE,
+ node,
+ };
+};
+
+/**
+ * Collapse the given node in the diffing's census's delta-report.
+ *
+ * @param {CensusTreeNode} node
+ */
+const collapseDiffingCensusNode = exports.collapseDiffingCensusNode = function (node) {
+ return {
+ type: actions.COLLAPSE_DIFFING_CENSUS_NODE,
+ node,
+ };
+};
+
+/**
+ * Focus the given node in the snapshot's census's report.
+ *
+ * @param {DominatorTreeNode} node
+ */
+const focusDiffingCensusNode = exports.focusDiffingCensusNode = function (node) {
+ return {
+ type: actions.FOCUS_DIFFING_CENSUS_NODE,
+ node,
+ };
+};
diff --git a/devtools/client/memory/actions/filter.js b/devtools/client/memory/actions/filter.js
new file mode 100644
index 000000000..c578b037a
--- /dev/null
+++ b/devtools/client/memory/actions/filter.js
@@ -0,0 +1,35 @@
+/* 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 { actions } = require("../constants");
+const { refresh } = require("./refresh");
+
+const setFilterString = exports.setFilterString = function (filterString) {
+ return {
+ type: actions.SET_FILTER_STRING,
+ filter: filterString
+ };
+};
+
+// The number of milliseconds we should wait before kicking off a new census
+// when the filter string is updated. This helps us avoid doing any work while
+// the user is still typing.
+const FILTER_INPUT_DEBOUNCE_MS = 250;
+
+// The timer id for the debounced census refresh.
+let timerId = null;
+
+exports.setFilterStringAndRefresh = function (filterString, heapWorker) {
+ return function* (dispatch, getState) {
+ dispatch(setFilterString(filterString));
+
+ if (timerId !== null) {
+ clearTimeout(timerId);
+ }
+
+ timerId = setTimeout(() => dispatch(refresh(heapWorker)),
+ FILTER_INPUT_DEBOUNCE_MS);
+ };
+};
diff --git a/devtools/client/memory/actions/io.js b/devtools/client/memory/actions/io.js
new file mode 100644
index 000000000..10b45aee5
--- /dev/null
+++ b/devtools/client/memory/actions/io.js
@@ -0,0 +1,97 @@
+/* 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 { immutableUpdate, reportException, assert } = require("devtools/shared/DevToolsUtils");
+const { snapshotState: states, actions, viewState } = require("../constants");
+const { L10N, openFilePicker, createSnapshot } = require("../utils");
+const telemetry = require("../telemetry");
+const { OS } = require("resource://gre/modules/osfile.jsm");
+const {
+ selectSnapshot,
+ computeSnapshotData,
+ readSnapshot,
+ takeCensus,
+ takeTreeMap
+} = require("./snapshot");
+const VALID_EXPORT_STATES = [states.SAVED, states.READ];
+
+exports.pickFileAndExportSnapshot = function (snapshot) {
+ return function* (dispatch, getState) {
+ let outputFile = yield openFilePicker({
+ title: L10N.getFormatStr("snapshot.io.save.window"),
+ defaultName: OS.Path.basename(snapshot.path),
+ filters: [[L10N.getFormatStr("snapshot.io.filter"), "*.fxsnapshot"]],
+ mode: "save",
+ });
+
+ if (!outputFile) {
+ return;
+ }
+
+ yield dispatch(exportSnapshot(snapshot, outputFile.path));
+ };
+};
+
+const exportSnapshot = exports.exportSnapshot = function (snapshot, dest) {
+ return function* (dispatch, getState) {
+ telemetry.countExportSnapshot();
+
+ dispatch({ type: actions.EXPORT_SNAPSHOT_START, snapshot });
+
+ assert(VALID_EXPORT_STATES.includes(snapshot.state),
+ `Snapshot is in invalid state for exporting: ${snapshot.state}`);
+
+ try {
+ yield OS.File.copy(snapshot.path, dest);
+ } catch (error) {
+ reportException("exportSnapshot", error);
+ dispatch({ type: actions.EXPORT_SNAPSHOT_ERROR, snapshot, error });
+ }
+
+ dispatch({ type: actions.EXPORT_SNAPSHOT_END, snapshot });
+ };
+};
+
+const pickFileAndImportSnapshotAndCensus = exports.pickFileAndImportSnapshotAndCensus = function (heapWorker) {
+ return function* (dispatch, getState) {
+ let input = yield openFilePicker({
+ title: L10N.getFormatStr("snapshot.io.import.window"),
+ filters: [[L10N.getFormatStr("snapshot.io.filter"), "*.fxsnapshot"]],
+ mode: "open",
+ });
+
+ if (!input) {
+ return;
+ }
+
+ yield dispatch(importSnapshotAndCensus(heapWorker, input.path));
+ };
+};
+
+const importSnapshotAndCensus = exports.importSnapshotAndCensus = function (heapWorker, path) {
+ return function* (dispatch, getState) {
+ telemetry.countImportSnapshot();
+
+ const snapshot = immutableUpdate(createSnapshot(getState()), {
+ path,
+ state: states.IMPORTING,
+ imported: true,
+ });
+ const id = snapshot.id;
+
+ dispatch({ type: actions.IMPORT_SNAPSHOT_START, snapshot });
+ dispatch(selectSnapshot(snapshot.id));
+
+ try {
+ yield dispatch(readSnapshot(heapWorker, id));
+ yield dispatch(computeSnapshotData(heapWorker, id));
+ } catch (error) {
+ reportException("importSnapshot", error);
+ dispatch({ type: actions.IMPORT_SNAPSHOT_ERROR, error, id });
+ }
+
+ dispatch({ type: actions.IMPORT_SNAPSHOT_END, id });
+ };
+};
diff --git a/devtools/client/memory/actions/label-display.js b/devtools/client/memory/actions/label-display.js
new file mode 100644
index 000000000..6e68293ab
--- /dev/null
+++ b/devtools/client/memory/actions/label-display.js
@@ -0,0 +1,38 @@
+/* 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 { assert } = require("devtools/shared/DevToolsUtils");
+const { actions } = require("../constants");
+const { refresh } = require("./refresh");
+
+/**
+ * Change the display we use for labeling individual nodes and refresh the
+ * current data.
+ */
+exports.setLabelDisplayAndRefresh = function (heapWorker, display) {
+ return function* (dispatch, getState) {
+ // Clears out all stored census data and sets the display.
+ dispatch(setLabelDisplay(display));
+ yield dispatch(refresh(heapWorker));
+ };
+};
+
+/**
+ * Change the display we use for labeling individual nodes.
+ *
+ * @param {labelDisplayModel} display
+ */
+const setLabelDisplay = exports.setLabelDisplay = function (display) {
+ assert(typeof display === "object"
+ && display
+ && display.breakdown
+ && display.breakdown.by,
+ `Breakdowns must be an object with a \`by\` property, attempted to set: ${uneval(display)}`);
+
+ return {
+ type: actions.SET_LABEL_DISPLAY,
+ display,
+ };
+};
diff --git a/devtools/client/memory/actions/moz.build b/devtools/client/memory/actions/moz.build
new file mode 100644
index 000000000..8b26aff9f
--- /dev/null
+++ b/devtools/client/memory/actions/moz.build
@@ -0,0 +1,19 @@
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'allocations.js',
+ 'census-display.js',
+ 'diffing.js',
+ 'filter.js',
+ 'io.js',
+ 'label-display.js',
+ 'refresh.js',
+ 'sizes.js',
+ 'snapshot.js',
+ 'task-cache.js',
+ 'tree-map-display.js',
+ 'view.js',
+)
diff --git a/devtools/client/memory/actions/refresh.js b/devtools/client/memory/actions/refresh.js
new file mode 100644
index 000000000..801a4f867
--- /dev/null
+++ b/devtools/client/memory/actions/refresh.js
@@ -0,0 +1,44 @@
+/* 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 { assert } = require("devtools/shared/DevToolsUtils");
+const { viewState } = require("../constants");
+const { refreshDiffing } = require("./diffing");
+const snapshot = require("./snapshot");
+
+/**
+ * Refresh the main thread's data from the heap analyses worker, if needed.
+ *
+ * @param {HeapAnalysesWorker} heapWorker
+ */
+exports.refresh = function (heapWorker) {
+ return function* (dispatch, getState) {
+ switch (getState().view.state) {
+ case viewState.DIFFING:
+ assert(getState().diffing, "Should have diffing state if in diffing view");
+ yield dispatch(refreshDiffing(heapWorker));
+ return;
+
+ case viewState.CENSUS:
+ yield dispatch(snapshot.refreshSelectedCensus(heapWorker));
+ return;
+
+ case viewState.DOMINATOR_TREE:
+ yield dispatch(snapshot.refreshSelectedDominatorTree(heapWorker));
+ return;
+
+ case viewState.TREE_MAP:
+ yield dispatch(snapshot.refreshSelectedTreeMap(heapWorker));
+ return;
+
+ case viewState.INDIVIDUALS:
+ yield dispatch(snapshot.refreshIndividuals(heapWorker));
+ return;
+
+ default:
+ assert(false, `Unexpected view state: ${getState().view.state}`);
+ }
+ };
+};
diff --git a/devtools/client/memory/actions/sizes.js b/devtools/client/memory/actions/sizes.js
new file mode 100644
index 000000000..7239803e7
--- /dev/null
+++ b/devtools/client/memory/actions/sizes.js
@@ -0,0 +1,13 @@
+/* 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 { actions } = require("../constants");
+
+exports.resizeShortestPaths = function (newSize) {
+ return {
+ type: actions.RESIZE_SHORTEST_PATHS,
+ size: newSize,
+ };
+};
diff --git a/devtools/client/memory/actions/snapshot.js b/devtools/client/memory/actions/snapshot.js
new file mode 100644
index 000000000..adb24977e
--- /dev/null
+++ b/devtools/client/memory/actions/snapshot.js
@@ -0,0 +1,865 @@
+/* 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 { Preferences } = require("resource://gre/modules/Preferences.jsm");
+const { assert, reportException, isSet } = require("devtools/shared/DevToolsUtils");
+const {
+ censusIsUpToDate,
+ getSnapshot,
+ createSnapshot,
+ dominatorTreeIsComputed,
+} = require("../utils");
+const {
+ actions,
+ snapshotState: states,
+ viewState,
+ censusState,
+ treeMapState,
+ dominatorTreeState,
+ individualsState,
+} = require("../constants");
+const telemetry = require("../telemetry");
+const view = require("./view");
+const refresh = require("./refresh");
+const diffing = require("./diffing");
+const TaskCache = require("./task-cache");
+
+/**
+ * A series of actions are fired from this task to save, read and generate the
+ * initial census from a snapshot.
+ *
+ * @param {MemoryFront}
+ * @param {HeapAnalysesClient}
+ * @param {Object}
+ */
+const takeSnapshotAndCensus = exports.takeSnapshotAndCensus = function (front, heapWorker) {
+ return function* (dispatch, getState) {
+ const id = yield dispatch(takeSnapshot(front));
+ if (id === null) {
+ return;
+ }
+
+ yield dispatch(readSnapshot(heapWorker, id));
+ if (getSnapshot(getState(), id).state !== states.READ) {
+ return;
+ }
+
+ yield dispatch(computeSnapshotData(heapWorker, id));
+ };
+};
+
+/**
+ * Create the census for the snapshot with the provided snapshot id. If the
+ * current view is the DOMINATOR_TREE view, create the dominator tree for this
+ * snapshot as well.
+ *
+ * @param {HeapAnalysesClient} heapWorker
+ * @param {snapshotId} id
+ */
+const computeSnapshotData = exports.computeSnapshotData = function (heapWorker, id) {
+ return function* (dispatch, getState) {
+ if (getSnapshot(getState(), id).state !== states.READ) {
+ return;
+ }
+
+ // Decide which type of census to take.
+ const censusTaker = getCurrentCensusTaker(getState().view.state);
+ yield dispatch(censusTaker(heapWorker, id));
+
+ if (getState().view.state === viewState.DOMINATOR_TREE &&
+ !getSnapshot(getState(), id).dominatorTree) {
+ yield dispatch(computeAndFetchDominatorTree(heapWorker, id));
+ }
+ };
+};
+
+/**
+ * Selects a snapshot and if the snapshot's census is using a different
+ * display, take a new census.
+ *
+ * @param {HeapAnalysesClient} heapWorker
+ * @param {snapshotId} id
+ */
+const selectSnapshotAndRefresh = exports.selectSnapshotAndRefresh = function (heapWorker, id) {
+ return function* (dispatch, getState) {
+ if (getState().diffing || getState().individuals) {
+ dispatch(view.changeView(viewState.CENSUS));
+ }
+
+ dispatch(selectSnapshot(id));
+ yield dispatch(refresh.refresh(heapWorker));
+ };
+};
+
+/**
+ * Take a snapshot and return its id on success, or null on failure.
+ *
+ * @param {MemoryFront} front
+ * @returns {Number|null}
+ */
+const takeSnapshot = exports.takeSnapshot = function (front) {
+ return function* (dispatch, getState) {
+ telemetry.countTakeSnapshot();
+
+ if (getState().diffing || getState().individuals) {
+ dispatch(view.changeView(viewState.CENSUS));
+ }
+
+ const snapshot = createSnapshot(getState());
+ const id = snapshot.id;
+ dispatch({ type: actions.TAKE_SNAPSHOT_START, snapshot });
+ dispatch(selectSnapshot(id));
+
+ let path;
+ try {
+ path = yield front.saveHeapSnapshot();
+ } catch (error) {
+ reportException("takeSnapshot", error);
+ dispatch({ type: actions.SNAPSHOT_ERROR, id, error });
+ return null;
+ }
+
+ dispatch({ type: actions.TAKE_SNAPSHOT_END, id, path });
+ return snapshot.id;
+ };
+};
+
+/**
+ * Reads a snapshot into memory; necessary to do before taking
+ * a census on the snapshot. May only be called once per snapshot.
+ *
+ * @param {HeapAnalysesClient} heapWorker
+ * @param {snapshotId} id
+ */
+const readSnapshot = exports.readSnapshot =
+TaskCache.declareCacheableTask({
+ getCacheKey(_, id) {
+ return id;
+ },
+
+ task: function* (heapWorker, id, removeFromCache, dispatch, getState) {
+ const snapshot = getSnapshot(getState(), id);
+ assert([states.SAVED, states.IMPORTING].includes(snapshot.state),
+ `Should only read a snapshot once. Found snapshot in state ${snapshot.state}`);
+
+ let creationTime;
+
+ dispatch({ type: actions.READ_SNAPSHOT_START, id });
+ try {
+ yield heapWorker.readHeapSnapshot(snapshot.path);
+ creationTime = yield heapWorker.getCreationTime(snapshot.path);
+ } catch (error) {
+ removeFromCache();
+ reportException("readSnapshot", error);
+ dispatch({ type: actions.SNAPSHOT_ERROR, id, error });
+ return;
+ }
+
+ removeFromCache();
+ dispatch({ type: actions.READ_SNAPSHOT_END, id, creationTime });
+ }
+});
+
+let takeCensusTaskCounter = 0;
+
+/**
+ * Census and tree maps both require snapshots. This function shares the logic
+ * of creating snapshots, but is configurable with specific actions for the
+ * individual census types.
+ *
+ * @param {getDisplay} Get the display object from the state.
+ * @param {getCensus} Get the census from the snapshot.
+ * @param {beginAction} Action to send at the beginning of a heap snapshot.
+ * @param {endAction} Action to send at the end of a heap snapshot.
+ * @param {errorAction} Action to send if a snapshot has an error.
+ */
+function makeTakeCensusTask({ getDisplay, getFilter, getCensus, beginAction,
+ endAction, errorAction, canTakeCensus }) {
+ /**
+ * @param {HeapAnalysesClient} heapWorker
+ * @param {snapshotId} id
+ *
+ * @see {Snapshot} model defined in devtools/client/memory/models.js
+ * @see `devtools/shared/heapsnapshot/HeapAnalysesClient.js`
+ * @see `js/src/doc/Debugger/Debugger.Memory.md` for breakdown details
+ */
+ let thisTakeCensusTaskId = ++takeCensusTaskCounter;
+ return TaskCache.declareCacheableTask({
+ getCacheKey(_, id) {
+ return `take-census-task-${thisTakeCensusTaskId}-${id}`;
+ },
+
+ task: function* (heapWorker, id, removeFromCache, dispatch, getState) {
+ const snapshot = getSnapshot(getState(), id);
+ if (!snapshot) {
+ removeFromCache();
+ return;
+ }
+
+ // Assert that snapshot is in a valid state
+ assert(canTakeCensus(snapshot),
+ `Attempting to take a census when the snapshot is not in a ready state. snapshot.state = ${snapshot.state}, census.state = ${(getCensus(snapshot) || { state: null }).state}`);
+
+ let report, parentMap;
+ let display = getDisplay(getState());
+ let filter = getFilter(getState());
+
+ // If display, filter and inversion haven't changed, don't do anything.
+ if (censusIsUpToDate(filter, display, getCensus(snapshot))) {
+ removeFromCache();
+ return;
+ }
+
+ // Keep taking a census if the display changes while our request is in
+ // flight. Recheck that the display used for the census is the same as the
+ // state's display.
+ do {
+ display = getDisplay(getState());
+ filter = getState().filter;
+
+ dispatch({
+ type: beginAction,
+ id,
+ filter,
+ display
+ });
+
+ let opts = display.inverted
+ ? { asInvertedTreeNode: true }
+ : { asTreeNode: true };
+
+ opts.filter = filter || null;
+
+ try {
+ ({ report, parentMap } = yield heapWorker.takeCensus(
+ snapshot.path,
+ { breakdown: display.breakdown },
+ opts));
+ } catch (error) {
+ removeFromCache();
+ reportException("takeCensus", error);
+ dispatch({ type: errorAction, id, error });
+ return;
+ }
+ }
+ while (filter !== getState().filter ||
+ display !== getDisplay(getState()));
+
+ removeFromCache();
+ dispatch({
+ type: endAction,
+ id,
+ display,
+ filter,
+ report,
+ parentMap
+ });
+
+ telemetry.countCensus({ filter, display });
+ }
+ });
+}
+
+/**
+ * Take a census.
+ */
+const takeCensus = exports.takeCensus = makeTakeCensusTask({
+ getDisplay: (state) => state.censusDisplay,
+ getFilter: (state) => state.filter,
+ getCensus: (snapshot) => snapshot.census,
+ beginAction: actions.TAKE_CENSUS_START,
+ endAction: actions.TAKE_CENSUS_END,
+ errorAction: actions.TAKE_CENSUS_ERROR,
+ canTakeCensus: snapshot =>
+ snapshot.state === states.READ &&
+ (!snapshot.census || snapshot.census.state === censusState.SAVED),
+});
+
+/**
+ * Take a census for the treemap.
+ */
+const takeTreeMap = exports.takeTreeMap = makeTakeCensusTask({
+ getDisplay: (state) => state.treeMapDisplay,
+ getFilter: () => null,
+ getCensus: (snapshot) => snapshot.treeMap,
+ beginAction: actions.TAKE_TREE_MAP_START,
+ endAction: actions.TAKE_TREE_MAP_END,
+ errorAction: actions.TAKE_TREE_MAP_ERROR,
+ canTakeCensus: snapshot =>
+ snapshot.state === states.READ &&
+ (!snapshot.treeMap || snapshot.treeMap.state === treeMapState.SAVED),
+});
+
+/**
+ * Define what should be the default mode for taking a census based on the
+ * default view of the tool.
+ */
+const defaultCensusTaker = takeTreeMap;
+
+/**
+ * Pick the default census taker when taking a snapshot. This should be
+ * determined by the current view. If the view doesn't include a census, then
+ * use the default one defined above. Some census information is always needed
+ * to display some basic information about a snapshot.
+ *
+ * @param {string} value from viewState
+ */
+const getCurrentCensusTaker = exports.getCurrentCensusTaker = function (currentView) {
+ switch (currentView) {
+ case viewState.TREE_MAP:
+ return takeTreeMap;
+ case viewState.CENSUS:
+ return takeCensus;
+ default:
+ return defaultCensusTaker;
+ }
+};
+
+/**
+ * Focus the given node in the individuals view.
+ *
+ * @param {DominatorTreeNode} node.
+ */
+const focusIndividual = exports.focusIndividual = function (node) {
+ return {
+ type: actions.FOCUS_INDIVIDUAL,
+ node,
+ };
+};
+
+/**
+ * Fetch the individual `DominatorTreeNodes` for the census group specified by
+ * `censusBreakdown` and `reportLeafIndex`.
+ *
+ * @param {HeapAnalysesClient} heapWorker
+ * @param {SnapshotId} id
+ * @param {Object} censusBreakdown
+ * @param {Set<Number> | Number} reportLeafIndex
+ */
+const fetchIndividuals = exports.fetchIndividuals =
+function (heapWorker, id, censusBreakdown, reportLeafIndex) {
+ return function* (dispatch, getState) {
+ if (getState().view.state !== viewState.INDIVIDUALS) {
+ dispatch(view.changeView(viewState.INDIVIDUALS));
+ }
+
+ const snapshot = getSnapshot(getState(), id);
+ assert(snapshot && snapshot.state === states.READ,
+ "The snapshot should already be read into memory");
+
+ if (!dominatorTreeIsComputed(snapshot)) {
+ yield dispatch(computeAndFetchDominatorTree(heapWorker, id));
+ }
+
+ const snapshot_ = getSnapshot(getState(), id);
+ assert(snapshot_.dominatorTree && snapshot_.dominatorTree.root,
+ "Should have a dominator tree with a root.");
+
+ const dominatorTreeId = snapshot_.dominatorTree.dominatorTreeId;
+
+ const indices = isSet(reportLeafIndex)
+ ? reportLeafIndex
+ : new Set([reportLeafIndex]);
+
+ let labelDisplay;
+ let nodes;
+ do {
+ labelDisplay = getState().labelDisplay;
+ assert(labelDisplay && labelDisplay.breakdown && labelDisplay.breakdown.by,
+ `Should have a breakdown to label nodes with, got: ${uneval(labelDisplay)}`);
+
+ if (getState().view.state !== viewState.INDIVIDUALS) {
+ // We switched views while in the process of fetching individuals -- any
+ // further work is useless.
+ return;
+ }
+
+ dispatch({ type: actions.FETCH_INDIVIDUALS_START });
+
+ try {
+ ({ nodes } = yield heapWorker.getCensusIndividuals({
+ dominatorTreeId,
+ indices,
+ censusBreakdown,
+ labelBreakdown: labelDisplay.breakdown,
+ maxRetainingPaths: Preferences.get("devtools.memory.max-retaining-paths"),
+ maxIndividuals: Preferences.get("devtools.memory.max-individuals"),
+ }));
+ } catch (error) {
+ reportException("actions/snapshot/fetchIndividuals", error);
+ dispatch({ type: actions.INDIVIDUALS_ERROR, error });
+ return;
+ }
+ }
+ while (labelDisplay !== getState().labelDisplay);
+
+ dispatch({
+ type: actions.FETCH_INDIVIDUALS_END,
+ id,
+ censusBreakdown,
+ indices,
+ labelDisplay,
+ nodes,
+ dominatorTree: snapshot_.dominatorTree,
+ });
+ };
+};
+
+/**
+ * Refresh the current individuals view.
+ *
+ * @param {HeapAnalysesClient} heapWorker
+ */
+const refreshIndividuals = exports.refreshIndividuals = function (heapWorker) {
+ return function* (dispatch, getState) {
+ assert(getState().view.state === viewState.INDIVIDUALS,
+ "Should be in INDIVIDUALS view.");
+
+ const { individuals } = getState();
+
+ switch (individuals.state) {
+ case individualsState.COMPUTING_DOMINATOR_TREE:
+ case individualsState.FETCHING:
+ // Nothing to do here.
+ return;
+
+ case individualsState.FETCHED:
+ if (getState().individuals.labelDisplay === getState().labelDisplay) {
+ return;
+ }
+ break;
+
+ case individualsState.ERROR:
+ // Doesn't hurt to retry: maybe we won't get an error this time around?
+ break;
+
+ default:
+ assert(false, `Unexpected individuals state: ${individuals.state}`);
+ return;
+ }
+
+ yield dispatch(fetchIndividuals(heapWorker,
+ individuals.id,
+ individuals.censusBreakdown,
+ individuals.indices));
+ };
+};
+
+/**
+ * Refresh the selected snapshot's census data, if need be (for example,
+ * display configuration changed).
+ *
+ * @param {HeapAnalysesClient} heapWorker
+ */
+const refreshSelectedCensus = exports.refreshSelectedCensus = function (heapWorker) {
+ return function* (dispatch, getState) {
+ let snapshot = getState().snapshots.find(s => s.selected);
+ if (!snapshot || snapshot.state !== states.READ) {
+ return;
+ }
+
+ // Intermediate snapshot states will get handled by the task action that is
+ // orchestrating them. For example, if the snapshot census's state is
+ // SAVING, then the takeCensus action will keep taking a census until
+ // the inverted property matches the inverted state. If the snapshot is
+ // still in the process of being saved or read, the takeSnapshotAndCensus
+ // task action will follow through and ensure that a census is taken.
+ if ((snapshot.census && snapshot.census.state === censusState.SAVED) ||
+ !snapshot.census) {
+ yield dispatch(takeCensus(heapWorker, snapshot.id));
+ }
+ };
+};
+
+/**
+ * Refresh the selected snapshot's tree map data, if need be (for example,
+ * display configuration changed).
+ *
+ * @param {HeapAnalysesClient} heapWorker
+ */
+const refreshSelectedTreeMap = exports.refreshSelectedTreeMap = function (heapWorker) {
+ return function* (dispatch, getState) {
+ let snapshot = getState().snapshots.find(s => s.selected);
+ if (!snapshot || snapshot.state !== states.READ) {
+ return;
+ }
+
+ // Intermediate snapshot states will get handled by the task action that is
+ // orchestrating them. For example, if the snapshot census's state is
+ // SAVING, then the takeCensus action will keep taking a census until
+ // the inverted property matches the inverted state. If the snapshot is
+ // still in the process of being saved or read, the takeSnapshotAndCensus
+ // task action will follow through and ensure that a census is taken.
+ if ((snapshot.treeMap && snapshot.treeMap.state === treeMapState.SAVED) ||
+ !snapshot.treeMap) {
+ yield dispatch(takeTreeMap(heapWorker, snapshot.id));
+ }
+ };
+};
+
+/**
+ * Request that the `HeapAnalysesWorker` compute the dominator tree for the
+ * snapshot with the given `id`.
+ *
+ * @param {HeapAnalysesClient} heapWorker
+ * @param {SnapshotId} id
+ *
+ * @returns {Promise<DominatorTreeId>}
+ */
+const computeDominatorTree = exports.computeDominatorTree =
+TaskCache.declareCacheableTask({
+ getCacheKey(_, id) {
+ return id;
+ },
+
+ task: function* (heapWorker, id, removeFromCache, dispatch, getState) {
+ const snapshot = getSnapshot(getState(), id);
+ assert(!(snapshot.dominatorTree && snapshot.dominatorTree.dominatorTreeId),
+ "Should not re-compute dominator trees");
+
+ dispatch({ type: actions.COMPUTE_DOMINATOR_TREE_START, id });
+
+ let dominatorTreeId;
+ try {
+ dominatorTreeId = yield heapWorker.computeDominatorTree(snapshot.path);
+ } catch (error) {
+ removeFromCache();
+ reportException("actions/snapshot/computeDominatorTree", error);
+ dispatch({ type: actions.DOMINATOR_TREE_ERROR, id, error });
+ return null;
+ }
+
+ removeFromCache();
+ dispatch({ type: actions.COMPUTE_DOMINATOR_TREE_END, id, dominatorTreeId });
+ return dominatorTreeId;
+ }
+});
+
+/**
+ * Get the partial subtree, starting from the root, of the
+ * snapshot-with-the-given-id's dominator tree.
+ *
+ * @param {HeapAnalysesClient} heapWorker
+ * @param {SnapshotId} id
+ *
+ * @returns {Promise<DominatorTreeNode>}
+ */
+const fetchDominatorTree = exports.fetchDominatorTree =
+TaskCache.declareCacheableTask({
+ getCacheKey(_, id) {
+ return id;
+ },
+
+ task: function* (heapWorker, id, removeFromCache, dispatch, getState) {
+ const snapshot = getSnapshot(getState(), id);
+ assert(dominatorTreeIsComputed(snapshot),
+ "Should have dominator tree model and it should be computed");
+
+ let display;
+ let root;
+ do {
+ display = getState().labelDisplay;
+ assert(display && display.breakdown,
+ `Should have a breakdown to describe nodes with, got: ${uneval(display)}`);
+
+ dispatch({ type: actions.FETCH_DOMINATOR_TREE_START, id, display });
+
+ try {
+ root = yield heapWorker.getDominatorTree({
+ dominatorTreeId: snapshot.dominatorTree.dominatorTreeId,
+ breakdown: display.breakdown,
+ maxRetainingPaths: Preferences.get("devtools.memory.max-retaining-paths"),
+ });
+ } catch (error) {
+ removeFromCache();
+ reportException("actions/snapshot/fetchDominatorTree", error);
+ dispatch({ type: actions.DOMINATOR_TREE_ERROR, id, error });
+ return null;
+ }
+ }
+ while (display !== getState().labelDisplay);
+
+ removeFromCache();
+ dispatch({ type: actions.FETCH_DOMINATOR_TREE_END, id, root });
+ telemetry.countDominatorTree({ display });
+ return root;
+ }
+});
+
+/**
+ * Fetch the immediately dominated children represented by the placeholder
+ * `lazyChildren` from snapshot-with-the-given-id's dominator tree.
+ *
+ * @param {HeapAnalysesClient} heapWorker
+ * @param {SnapshotId} id
+ * @param {DominatorTreeLazyChildren} lazyChildren
+ */
+const fetchImmediatelyDominated = exports.fetchImmediatelyDominated =
+TaskCache.declareCacheableTask({
+ getCacheKey(_, id, lazyChildren) {
+ return `${id}-${lazyChildren.key()}`;
+ },
+
+ task: function* (heapWorker, id, lazyChildren, removeFromCache, dispatch, getState) {
+ const snapshot = getSnapshot(getState(), id);
+ assert(snapshot.dominatorTree, "Should have dominator tree model");
+ assert(snapshot.dominatorTree.state === dominatorTreeState.LOADED ||
+ snapshot.dominatorTree.state === dominatorTreeState.INCREMENTAL_FETCHING,
+ "Cannot fetch immediately dominated nodes in a dominator tree unless " +
+ " the dominator tree has already been computed");
+
+ let display;
+ let response;
+ do {
+ display = getState().labelDisplay;
+ assert(display, "Should have a display to describe nodes with.");
+
+ dispatch({ type: actions.FETCH_IMMEDIATELY_DOMINATED_START, id });
+
+ try {
+ response = yield heapWorker.getImmediatelyDominated({
+ dominatorTreeId: snapshot.dominatorTree.dominatorTreeId,
+ breakdown: display.breakdown,
+ nodeId: lazyChildren.parentNodeId(),
+ startIndex: lazyChildren.siblingIndex(),
+ maxRetainingPaths: Preferences.get("devtools.memory.max-retaining-paths"),
+ });
+ } catch (error) {
+ removeFromCache();
+ reportException("actions/snapshot/fetchImmediatelyDominated", error);
+ dispatch({ type: actions.DOMINATOR_TREE_ERROR, id, error });
+ return null;
+ }
+ }
+ while (display !== getState().labelDisplay);
+
+ removeFromCache();
+ dispatch({
+ type: actions.FETCH_IMMEDIATELY_DOMINATED_END,
+ id,
+ path: response.path,
+ nodes: response.nodes,
+ moreChildrenAvailable: response.moreChildrenAvailable,
+ });
+ }
+});
+
+/**
+ * Compute and then fetch the dominator tree of the snapshot with the given
+ * `id`.
+ *
+ * @param {HeapAnalysesClient} heapWorker
+ * @param {SnapshotId} id
+ *
+ * @returns {Promise<DominatorTreeNode>}
+ */
+const computeAndFetchDominatorTree = exports.computeAndFetchDominatorTree =
+TaskCache.declareCacheableTask({
+ getCacheKey(_, id) {
+ return id;
+ },
+
+ task: function* (heapWorker, id, removeFromCache, dispatch, getState) {
+ const dominatorTreeId = yield dispatch(computeDominatorTree(heapWorker, id));
+ if (dominatorTreeId === null) {
+ removeFromCache();
+ return null;
+ }
+
+ const root = yield dispatch(fetchDominatorTree(heapWorker, id));
+ removeFromCache();
+
+ if (!root) {
+ return null;
+ }
+
+ return root;
+ }
+});
+
+/**
+ * Update the currently selected snapshot's dominator tree.
+ *
+ * @param {HeapAnalysesClient} heapWorker
+ */
+const refreshSelectedDominatorTree = exports.refreshSelectedDominatorTree = function (heapWorker) {
+ return function* (dispatch, getState) {
+ let snapshot = getState().snapshots.find(s => s.selected);
+ if (!snapshot) {
+ return;
+ }
+
+ if (snapshot.dominatorTree &&
+ !(snapshot.dominatorTree.state === dominatorTreeState.COMPUTED ||
+ snapshot.dominatorTree.state === dominatorTreeState.LOADED ||
+ snapshot.dominatorTree.state === dominatorTreeState.INCREMENTAL_FETCHING)) {
+ return;
+ }
+
+ if (snapshot.state === states.READ) {
+ if (snapshot.dominatorTree) {
+ yield dispatch(fetchDominatorTree(heapWorker, snapshot.id));
+ } else {
+ yield dispatch(computeAndFetchDominatorTree(heapWorker, snapshot.id));
+ }
+ } else {
+ // If there was an error, we can't continue. If we are still saving or
+ // reading the snapshot, then takeSnapshotAndCensus will finish the job
+ // for us.
+ return;
+ }
+ };
+};
+
+/**
+ * Select the snapshot with the given id.
+ *
+ * @param {snapshotId} id
+ * @see {Snapshot} model defined in devtools/client/memory/models.js
+ */
+const selectSnapshot = exports.selectSnapshot = function (id) {
+ return {
+ type: actions.SELECT_SNAPSHOT,
+ id
+ };
+};
+
+/**
+ * Delete all snapshots that are in the READ or ERROR state
+ *
+ * @param {HeapAnalysesClient} heapWorker
+ */
+const clearSnapshots = exports.clearSnapshots = function (heapWorker) {
+ return function* (dispatch, getState) {
+ let snapshots = getState().snapshots.filter(s => {
+ let snapshotReady = s.state === states.READ || s.state === states.ERROR;
+ let censusReady = (s.treeMap && s.treeMap.state === treeMapState.SAVED) ||
+ (s.census && s.census.state === censusState.SAVED);
+
+ return snapshotReady && censusReady;
+ });
+
+ let ids = snapshots.map(s => s.id);
+
+ dispatch({ type: actions.DELETE_SNAPSHOTS_START, ids });
+
+ if (getState().diffing) {
+ dispatch(diffing.toggleDiffing());
+ }
+ if (getState().individuals) {
+ dispatch(view.popView());
+ }
+
+ yield Promise.all(snapshots.map(snapshot => {
+ return heapWorker.deleteHeapSnapshot(snapshot.path).catch(error => {
+ reportException("clearSnapshots", error);
+ dispatch({ type: actions.SNAPSHOT_ERROR, id: snapshot.id, error });
+ });
+ }));
+
+ dispatch({ type: actions.DELETE_SNAPSHOTS_END, ids });
+ };
+};
+
+/**
+ * Delete a snapshot
+ *
+ * @param {HeapAnalysesClient} heapWorker
+ * @param {snapshotModel} snapshot
+ */
+const deleteSnapshot = exports.deleteSnapshot = function (heapWorker, snapshot) {
+ return function* (dispatch, getState) {
+ dispatch({ type: actions.DELETE_SNAPSHOTS_START, ids: [snapshot.id] });
+
+ try {
+ yield heapWorker.deleteHeapSnapshot(snapshot.path);
+ } catch (error) {
+ reportException("deleteSnapshot", error);
+ dispatch({ type: actions.SNAPSHOT_ERROR, id: snapshot.id, error });
+ }
+
+ dispatch({ type: actions.DELETE_SNAPSHOTS_END, ids: [snapshot.id] });
+ };
+};
+
+/**
+ * Expand the given node in the snapshot's census report.
+ *
+ * @param {CensusTreeNode} node
+ */
+const expandCensusNode = exports.expandCensusNode = function (id, node) {
+ return {
+ type: actions.EXPAND_CENSUS_NODE,
+ id,
+ node,
+ };
+};
+
+/**
+ * Collapse the given node in the snapshot's census report.
+ *
+ * @param {CensusTreeNode} node
+ */
+const collapseCensusNode = exports.collapseCensusNode = function (id, node) {
+ return {
+ type: actions.COLLAPSE_CENSUS_NODE,
+ id,
+ node,
+ };
+};
+
+/**
+ * Focus the given node in the snapshot's census's report.
+ *
+ * @param {SnapshotId} id
+ * @param {DominatorTreeNode} node
+ */
+const focusCensusNode = exports.focusCensusNode = function (id, node) {
+ return {
+ type: actions.FOCUS_CENSUS_NODE,
+ id,
+ node,
+ };
+};
+
+/**
+ * Expand the given node in the snapshot's dominator tree.
+ *
+ * @param {DominatorTreeTreeNode} node
+ */
+const expandDominatorTreeNode = exports.expandDominatorTreeNode = function (id, node) {
+ return {
+ type: actions.EXPAND_DOMINATOR_TREE_NODE,
+ id,
+ node,
+ };
+};
+
+/**
+ * Collapse the given node in the snapshot's dominator tree.
+ *
+ * @param {DominatorTreeTreeNode} node
+ */
+const collapseDominatorTreeNode = exports.collapseDominatorTreeNode = function (id, node) {
+ return {
+ type: actions.COLLAPSE_DOMINATOR_TREE_NODE,
+ id,
+ node,
+ };
+};
+
+/**
+ * Focus the given node in the snapshot's dominator tree.
+ *
+ * @param {SnapshotId} id
+ * @param {DominatorTreeNode} node
+ */
+const focusDominatorTreeNode = exports.focusDominatorTreeNode = function (id, node) {
+ return {
+ type: actions.FOCUS_DOMINATOR_TREE_NODE,
+ id,
+ node,
+ };
+};
diff --git a/devtools/client/memory/actions/task-cache.js b/devtools/client/memory/actions/task-cache.js
new file mode 100644
index 000000000..14e44d0c3
--- /dev/null
+++ b/devtools/client/memory/actions/task-cache.js
@@ -0,0 +1,99 @@
+/* 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 { assert } = require("devtools/shared/DevToolsUtils");
+
+/**
+ * The `TaskCache` allows for re-using active tasks when spawning a second task
+ * would simply duplicate work and is unnecessary. It maps from a task's unique
+ * key to the promise of its result.
+ */
+const TaskCache = module.exports = class TaskCache {
+ constructor() {
+ this._cache = new Map();
+ }
+
+ /**
+ * Get the promise keyed by the given unique `key`, if one exists.
+ *
+ * @param {Any} key
+ * @returns {Promise<Any> | undefined}
+ */
+ get(key) {
+ return this._cache.get(key);
+ }
+
+ /**
+ * Put the task result promise in the cache and associate it with the given
+ * `key` which must not already have an entry in the cache.
+ *
+ * @param {Any} key
+ * @param {Promise<Any>} promise
+ */
+ put(key, promise) {
+ assert(!this._cache.has(key),
+ "We should not override extant entries");
+
+ this._cache.set(key, promise);
+ }
+
+ /**
+ * Remove the cache entry with the given key.
+ *
+ * @param {Any} key
+ */
+ remove(key) {
+ assert(this._cache.has(key),
+ `Should have an extant entry for key = ${key}`);
+
+ this._cache.delete(key);
+ }
+};
+
+/**
+ * Create a new action-orchestrating task that is automatically cached. The
+ * tasks themselves are responsible from removing themselves from the cache.
+ *
+ * @param {Function(...args) -> Any} getCacheKey
+ * @param {Generator(...args) -> Any} task
+ *
+ * @returns Cacheable, Action-Creating Task
+ */
+TaskCache.declareCacheableTask = function ({ getCacheKey, task }) {
+ const cache = new TaskCache();
+
+ return function (...args) {
+ return function* (dispatch, getState) {
+ const key = getCacheKey(...args);
+
+ const extantResult = cache.get(key);
+ if (extantResult) {
+ return extantResult;
+ }
+
+ // Ensure that we have our new entry in the cache *before* dispatching the
+ // task!
+ let resolve;
+ cache.put(key, new Promise(r => {
+ resolve = r;
+ }));
+
+ resolve(dispatch(function* () {
+ try {
+ args.push(() => cache.remove(key), dispatch, getState);
+ return yield* task(...args);
+ } catch (error) {
+ // Don't perma-cache errors.
+ if (cache.get(key)) {
+ cache.remove(key);
+ }
+ throw error;
+ }
+ }));
+
+ return yield cache.get(key);
+ };
+ };
+};
diff --git a/devtools/client/memory/actions/tree-map-display.js b/devtools/client/memory/actions/tree-map-display.js
new file mode 100644
index 000000000..e4ab9988c
--- /dev/null
+++ b/devtools/client/memory/actions/tree-map-display.js
@@ -0,0 +1,37 @@
+/* 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 { assert } = require("devtools/shared/DevToolsUtils");
+const { actions } = require("../constants");
+const { refresh } = require("./refresh");
+/**
+ * Sets the tree map display as the current display and refreshes the tree map
+ * census.
+ */
+exports.setTreeMapAndRefresh = function (heapWorker, display) {
+ return function* (dispatch, getState) {
+ dispatch(setTreeMap(display));
+ yield dispatch(refresh(heapWorker));
+ };
+};
+
+/**
+ * Clears out all cached census data in the snapshots and sets new display data
+ * for tree maps.
+ *
+ * @param {treeMapModel} display
+ */
+const setTreeMap = exports.setTreeMap = function (display) {
+ assert(typeof display === "object"
+ && display
+ && display.breakdown
+ && display.breakdown.by,
+ `Breakdowns must be an object with a \`by\` property, attempted to set: ${uneval(display)}`);
+
+ return {
+ type: actions.SET_TREE_MAP_DISPLAY,
+ display,
+ };
+};
diff --git a/devtools/client/memory/actions/view.js b/devtools/client/memory/actions/view.js
new file mode 100644
index 000000000..5ff508840
--- /dev/null
+++ b/devtools/client/memory/actions/view.js
@@ -0,0 +1,67 @@
+/* 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 { assert } = require("devtools/shared/DevToolsUtils");
+const { actions } = require("../constants");
+const { findSelectedSnapshot } = require("../utils");
+const refresh = require("./refresh");
+
+/**
+ * Change the currently selected view.
+ *
+ * @param {viewState} view
+ */
+const changeView = exports.changeView = function (view) {
+ return function (dispatch, getState) {
+ dispatch({
+ type: actions.CHANGE_VIEW,
+ newViewState: view,
+ oldDiffing: getState().diffing,
+ oldSelected: findSelectedSnapshot(getState()),
+ });
+ };
+};
+
+/**
+ * Given that we are in the INDIVIDUALS view state, go back to the state we were
+ * in before.
+ */
+const popView = exports.popView = function () {
+ return function (dispatch, getState) {
+ const { previous } = getState().view;
+ assert(previous);
+ dispatch({
+ type: actions.POP_VIEW,
+ previousView: previous,
+ });
+ };
+};
+
+/**
+ * Change the currently selected view and ensure all our data is up to date from
+ * the heap worker.
+ *
+ * @param {viewState} view
+ * @param {HeapAnalysesClient} heapWorker
+ */
+exports.changeViewAndRefresh = function (view, heapWorker) {
+ return function* (dispatch, getState) {
+ dispatch(changeView(view));
+ yield dispatch(refresh.refresh(heapWorker));
+ };
+};
+
+/**
+ * Given that we are in the INDIVIDUALS view state, go back to the state we were
+ * previously in and refresh our data.
+ *
+ * @param {HeapAnalysesClient} heapWorker
+ */
+exports.popViewAndRefresh = function (heapWorker) {
+ return function* (dispatch, getState) {
+ dispatch(popView());
+ yield dispatch(refresh.refresh(heapWorker));
+ };
+};
diff --git a/devtools/client/memory/app.js b/devtools/client/memory/app.js
new file mode 100644
index 000000000..b705e0a99
--- /dev/null
+++ b/devtools/client/memory/app.js
@@ -0,0 +1,322 @@
+/* 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/. */
+
+const { assert } = require("devtools/shared/DevToolsUtils");
+const { appinfo } = require("Services");
+const { DOM: dom, createClass, createFactory, PropTypes } = require("devtools/client/shared/vendor/react");
+const { connect } = require("devtools/client/shared/vendor/react-redux");
+const { censusDisplays, labelDisplays, treeMapDisplays, diffingState, viewState } = require("./constants");
+const { toggleRecordingAllocationStacks } = require("./actions/allocations");
+const { setCensusDisplayAndRefresh } = require("./actions/census-display");
+const { setLabelDisplayAndRefresh } = require("./actions/label-display");
+const { setTreeMapDisplayAndRefresh } = require("./actions/tree-map-display");
+
+const {
+ getCustomCensusDisplays,
+ getCustomLabelDisplays,
+ getCustomTreeMapDisplays,
+} = require("devtools/client/memory/utils");
+const {
+ selectSnapshotForDiffingAndRefresh,
+ toggleDiffing,
+ expandDiffingCensusNode,
+ collapseDiffingCensusNode,
+ focusDiffingCensusNode,
+} = require("./actions/diffing");
+const { setFilterStringAndRefresh } = require("./actions/filter");
+const { pickFileAndExportSnapshot, pickFileAndImportSnapshotAndCensus } = require("./actions/io");
+const {
+ selectSnapshotAndRefresh,
+ takeSnapshotAndCensus,
+ clearSnapshots,
+ deleteSnapshot,
+ fetchImmediatelyDominated,
+ expandCensusNode,
+ collapseCensusNode,
+ focusCensusNode,
+ expandDominatorTreeNode,
+ collapseDominatorTreeNode,
+ focusDominatorTreeNode,
+ fetchIndividuals,
+ focusIndividual,
+} = require("./actions/snapshot");
+const { changeViewAndRefresh, popViewAndRefresh } = require("./actions/view");
+const { resizeShortestPaths } = require("./actions/sizes");
+const Toolbar = createFactory(require("./components/toolbar"));
+const List = createFactory(require("./components/list"));
+const SnapshotListItem = createFactory(require("./components/snapshot-list-item"));
+const Heap = createFactory(require("./components/heap"));
+const { app: appModel } = require("./models");
+
+const MemoryApp = createClass({
+ displayName: "MemoryApp",
+
+ propTypes: appModel,
+
+ getDefaultProps() {
+ return {};
+ },
+
+ componentDidMount() {
+ // Attach the keydown listener directly to the window. When an element that
+ // has the focus (such as a tree node) is removed from the DOM, the focus
+ // falls back to the body.
+ window.addEventListener("keydown", this.onKeyDown);
+ },
+
+ componentWillUnmount() {
+ window.removeEventListener("keydown", this.onKeyDown);
+ },
+
+ childContextTypes: {
+ front: PropTypes.any,
+ heapWorker: PropTypes.any,
+ toolbox: PropTypes.any,
+ },
+
+ getChildContext() {
+ return {
+ front: this.props.front,
+ heapWorker: this.props.heapWorker,
+ toolbox: this.props.toolbox,
+ };
+ },
+
+ onKeyDown(e) {
+ let { snapshots, dispatch, heapWorker } = this.props;
+ const selectedSnapshot = snapshots.find(s => s.selected);
+ const selectedIndex = snapshots.indexOf(selectedSnapshot);
+
+ let isOSX = appinfo.OS == "Darwin";
+ let isAccelKey = (isOSX && e.metaKey) || (!isOSX && e.ctrlKey);
+
+ // On ACCEL+UP, select previous snapshot.
+ if (isAccelKey && e.key === "ArrowUp") {
+ let previousIndex = Math.max(0, selectedIndex - 1);
+ let previousSnapshotId = snapshots[previousIndex].id;
+ dispatch(selectSnapshotAndRefresh(heapWorker, previousSnapshotId));
+ }
+
+ // On ACCEL+DOWN, select next snapshot.
+ if (isAccelKey && e.key === "ArrowDown") {
+ let nextIndex = Math.min(snapshots.length - 1, selectedIndex + 1);
+ let nextSnapshotId = snapshots[nextIndex].id;
+ dispatch(selectSnapshotAndRefresh(heapWorker, nextSnapshotId));
+ }
+ },
+
+ _getCensusDisplays() {
+ const customDisplays = getCustomCensusDisplays();
+ const custom = Object.keys(customDisplays).reduce((arr, key) => {
+ arr.push(customDisplays[key]);
+ return arr;
+ }, []);
+
+ return [
+ censusDisplays.coarseType,
+ censusDisplays.allocationStack,
+ censusDisplays.invertedAllocationStack,
+ ].concat(custom);
+ },
+
+ _getLabelDisplays() {
+ const customDisplays = getCustomLabelDisplays();
+ const custom = Object.keys(customDisplays).reduce((arr, key) => {
+ arr.push(customDisplays[key]);
+ return arr;
+ }, []);
+
+ return [
+ labelDisplays.coarseType,
+ labelDisplays.allocationStack,
+ ].concat(custom);
+ },
+
+ _getTreeMapDisplays() {
+ const customDisplays = getCustomTreeMapDisplays();
+ const custom = Object.keys(customDisplays).reduce((arr, key) => {
+ arr.push(customDisplays[key]);
+ return arr;
+ }, []);
+
+ return [
+ treeMapDisplays.coarseType
+ ].concat(custom);
+ },
+
+ render() {
+ let {
+ dispatch,
+ snapshots,
+ front,
+ heapWorker,
+ allocations,
+ toolbox,
+ filter,
+ diffing,
+ view,
+ sizes,
+ censusDisplay,
+ labelDisplay,
+ individuals,
+ } = this.props;
+
+ const selectedSnapshot = snapshots.find(s => s.selected);
+
+ const onClickSnapshotListItem = diffing && diffing.state === diffingState.SELECTING
+ ? snapshot => dispatch(selectSnapshotForDiffingAndRefresh(heapWorker, snapshot))
+ : snapshot => dispatch(selectSnapshotAndRefresh(heapWorker, snapshot.id));
+
+ return (
+ dom.div(
+ {
+ id: "memory-tool"
+ },
+
+ Toolbar({
+ snapshots,
+ censusDisplays: this._getCensusDisplays(),
+ censusDisplay,
+ onCensusDisplayChange: newDisplay =>
+ dispatch(setCensusDisplayAndRefresh(heapWorker, newDisplay)),
+ onImportClick: () => dispatch(pickFileAndImportSnapshotAndCensus(heapWorker)),
+ onClearSnapshotsClick: () => dispatch(clearSnapshots(heapWorker)),
+ onTakeSnapshotClick: () => dispatch(takeSnapshotAndCensus(front, heapWorker)),
+ onToggleRecordAllocationStacks: () =>
+ dispatch(toggleRecordingAllocationStacks(front)),
+ allocations,
+ filterString: filter,
+ setFilterString: filterString =>
+ dispatch(setFilterStringAndRefresh(filterString, heapWorker)),
+ diffing,
+ onToggleDiffing: () => dispatch(toggleDiffing()),
+ view,
+ labelDisplays: this._getLabelDisplays(),
+ labelDisplay,
+ onLabelDisplayChange: newDisplay =>
+ dispatch(setLabelDisplayAndRefresh(heapWorker, newDisplay)),
+ treeMapDisplays: this._getTreeMapDisplays(),
+ onTreeMapDisplayChange: newDisplay =>
+ dispatch(setTreeMapDisplayAndRefresh(heapWorker, newDisplay)),
+ onViewChange: v => dispatch(changeViewAndRefresh(v, heapWorker)),
+ }),
+
+ dom.div(
+ {
+ id: "memory-tool-container"
+ },
+
+ List({
+ itemComponent: SnapshotListItem,
+ items: snapshots,
+ onSave: snapshot => dispatch(pickFileAndExportSnapshot(snapshot)),
+ onDelete: snapshot => dispatch(deleteSnapshot(heapWorker, snapshot)),
+ onClick: onClickSnapshotListItem,
+ diffing,
+ }),
+
+ Heap({
+ snapshot: selectedSnapshot,
+ diffing,
+ onViewSourceInDebugger: frame => toolbox.viewSourceInDebugger(frame.source, frame.line),
+ onSnapshotClick: () =>
+ dispatch(takeSnapshotAndCensus(front, heapWorker)),
+ onLoadMoreSiblings: lazyChildren =>
+ dispatch(fetchImmediatelyDominated(heapWorker,
+ selectedSnapshot.id,
+ lazyChildren)),
+ onPopView: () => dispatch(popViewAndRefresh(heapWorker)),
+ individuals,
+ onViewIndividuals: node => {
+ const snapshotId = diffing
+ ? diffing.secondSnapshotId
+ : selectedSnapshot.id;
+ dispatch(fetchIndividuals(heapWorker,
+ snapshotId,
+ censusDisplay.breakdown,
+ node.reportLeafIndex));
+ },
+ onFocusIndividual: node => {
+ assert(view.state === viewState.INDIVIDUALS,
+ "Should be in the individuals view");
+ dispatch(focusIndividual(node));
+ },
+ onCensusExpand: (census, node) => {
+ if (diffing) {
+ assert(diffing.census === census,
+ "Should only expand active census");
+ dispatch(expandDiffingCensusNode(node));
+ } else {
+ assert(selectedSnapshot && selectedSnapshot.census === census,
+ "If not diffing, should be expanding on selected snapshot's census");
+ dispatch(expandCensusNode(selectedSnapshot.id, node));
+ }
+ },
+ onCensusCollapse: (census, node) => {
+ if (diffing) {
+ assert(diffing.census === census,
+ "Should only collapse active census");
+ dispatch(collapseDiffingCensusNode(node));
+ } else {
+ assert(selectedSnapshot && selectedSnapshot.census === census,
+ "If not diffing, should be collapsing on selected snapshot's census");
+ dispatch(collapseCensusNode(selectedSnapshot.id, node));
+ }
+ },
+ onCensusFocus: (census, node) => {
+ if (diffing) {
+ assert(diffing.census === census,
+ "Should only focus nodes in active census");
+ dispatch(focusDiffingCensusNode(node));
+ } else {
+ assert(selectedSnapshot && selectedSnapshot.census === census,
+ "If not diffing, should be focusing on nodes in selected snapshot's census");
+ dispatch(focusCensusNode(selectedSnapshot.id, node));
+ }
+ },
+ onDominatorTreeExpand: node => {
+ assert(view.state === viewState.DOMINATOR_TREE,
+ "If expanding dominator tree nodes, should be in dominator tree view");
+ assert(selectedSnapshot, "...and we should have a selected snapshot");
+ assert(selectedSnapshot.dominatorTree,
+ "...and that snapshot should have a dominator tree");
+ dispatch(expandDominatorTreeNode(selectedSnapshot.id, node));
+ },
+ onDominatorTreeCollapse: node => {
+ assert(view.state === viewState.DOMINATOR_TREE,
+ "If collapsing dominator tree nodes, should be in dominator tree view");
+ assert(selectedSnapshot, "...and we should have a selected snapshot");
+ assert(selectedSnapshot.dominatorTree,
+ "...and that snapshot should have a dominator tree");
+ dispatch(collapseDominatorTreeNode(selectedSnapshot.id, node));
+ },
+ onDominatorTreeFocus: node => {
+ assert(view.state === viewState.DOMINATOR_TREE,
+ "If focusing dominator tree nodes, should be in dominator tree view");
+ assert(selectedSnapshot, "...and we should have a selected snapshot");
+ assert(selectedSnapshot.dominatorTree,
+ "...and that snapshot should have a dominator tree");
+ dispatch(focusDominatorTreeNode(selectedSnapshot.id, node));
+ },
+ onShortestPathsResize: newSize => {
+ dispatch(resizeShortestPaths(newSize));
+ },
+ sizes,
+ view,
+ })
+ )
+ )
+ );
+ },
+});
+
+/**
+ * Passed into react-redux's `connect` method that is called on store change
+ * and passed to components.
+ */
+function mapStateToProps(state) {
+ return state;
+}
+
+module.exports = connect(mapStateToProps)(MemoryApp);
diff --git a/devtools/client/memory/components/census-header.js b/devtools/client/memory/components/census-header.js
new file mode 100644
index 000000000..d897ed132
--- /dev/null
+++ b/devtools/client/memory/components/census-header.js
@@ -0,0 +1,72 @@
+/* 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/. */
+
+const { DOM: dom, createClass } = require("devtools/client/shared/vendor/react");
+const { L10N } = require("../utils");
+const models = require("../models");
+
+const CensusHeader = module.exports = createClass({
+ displayName: "CensusHeader",
+
+ propTypes: {
+ diffing: models.diffingModel,
+ },
+
+ render() {
+ let individualsCell;
+ if (!this.props.diffing) {
+ individualsCell = dom.span({
+ className: "heap-tree-item-field heap-tree-item-individuals"
+ });
+ }
+
+ return dom.div(
+ {
+ className: "header"
+ },
+
+ dom.span(
+ {
+ className: "heap-tree-item-bytes",
+ title: L10N.getStr("heapview.field.bytes.tooltip"),
+ },
+ L10N.getStr("heapview.field.bytes")
+ ),
+
+ dom.span(
+ {
+ className: "heap-tree-item-count",
+ title: L10N.getStr("heapview.field.count.tooltip"),
+ },
+ L10N.getStr("heapview.field.count")
+ ),
+
+ dom.span(
+ {
+ className: "heap-tree-item-total-bytes",
+ title: L10N.getStr("heapview.field.totalbytes.tooltip"),
+ },
+ L10N.getStr("heapview.field.totalbytes")
+ ),
+
+ dom.span(
+ {
+ className: "heap-tree-item-total-count",
+ title: L10N.getStr("heapview.field.totalcount.tooltip"),
+ },
+ L10N.getStr("heapview.field.totalcount")
+ ),
+
+ individualsCell,
+
+ dom.span(
+ {
+ className: "heap-tree-item-name",
+ title: L10N.getStr("heapview.field.name.tooltip"),
+ },
+ L10N.getStr("heapview.field.name")
+ )
+ );
+ }
+});
diff --git a/devtools/client/memory/components/census-tree-item.js b/devtools/client/memory/components/census-tree-item.js
new file mode 100644
index 000000000..c5d08cefc
--- /dev/null
+++ b/devtools/client/memory/components/census-tree-item.js
@@ -0,0 +1,134 @@
+/* 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 { isSavedFrame } = require("devtools/shared/DevToolsUtils");
+const { DOM: dom, createClass, createFactory } = require("devtools/client/shared/vendor/react");
+const { L10N, formatNumber, formatPercent } = require("../utils");
+const Frame = createFactory(require("devtools/client/shared/components/frame"));
+const { TREE_ROW_HEIGHT } = require("../constants");
+
+const CensusTreeItem = module.exports = createClass({
+ displayName: "CensusTreeItem",
+
+ shouldComponentUpdate(nextProps, nextState) {
+ return this.props.item != nextProps.item
+ || this.props.depth != nextProps.depth
+ || this.props.expanded != nextProps.expanded
+ || this.props.focused != nextProps.focused
+ || this.props.diffing != nextProps.diffing;
+ },
+
+ render() {
+ let {
+ item,
+ depth,
+ arrow,
+ focused,
+ getPercentBytes,
+ getPercentCount,
+ diffing,
+ onViewSourceInDebugger,
+ onViewIndividuals,
+ inverted,
+ } = this.props;
+
+ const bytes = formatNumber(item.bytes, !!diffing);
+ const percentBytes = formatPercent(getPercentBytes(item.bytes), !!diffing);
+
+ const count = formatNumber(item.count, !!diffing);
+ const percentCount = formatPercent(getPercentCount(item.count), !!diffing);
+
+ const totalBytes = formatNumber(item.totalBytes, !!diffing);
+ const percentTotalBytes = formatPercent(getPercentBytes(item.totalBytes), !!diffing);
+
+ const totalCount = formatNumber(item.totalCount, !!diffing);
+ const percentTotalCount = formatPercent(getPercentCount(item.totalCount), !!diffing);
+
+ let pointer;
+ if (inverted && depth > 0) {
+ pointer = dom.span({ className: "children-pointer" }, "↖");
+ } else if (!inverted && item.children && item.children.length) {
+ pointer = dom.span({ className: "children-pointer" }, "↘");
+ }
+
+ let individualsCell;
+ if (!diffing) {
+ let individualsButton;
+ if (item.reportLeafIndex !== undefined) {
+ individualsButton = dom.button(
+ {
+ key: `individuals-button-${item.id}`,
+ title: L10N.getStr("tree-item.view-individuals.tooltip"),
+ className: "devtools-button individuals-button",
+ onClick: e => {
+ // Don't let the event bubble up to cause this item to focus after
+ // we have switched views, which would lead to assertion failures.
+ e.preventDefault();
+ e.stopPropagation();
+
+ onViewIndividuals(item);
+ },
+ },
+ "â‚"
+ );
+ }
+ individualsCell = dom.span(
+ { className: "heap-tree-item-field heap-tree-item-individuals" },
+ individualsButton
+ );
+ }
+
+ return dom.div(
+ { className: `heap-tree-item ${focused ? "focused" : ""}` },
+ dom.span({ className: "heap-tree-item-field heap-tree-item-bytes" },
+ dom.span({ className: "heap-tree-number" }, bytes),
+ dom.span({ className: "heap-tree-percent" }, percentBytes)),
+ dom.span({ className: "heap-tree-item-field heap-tree-item-count" },
+ dom.span({ className: "heap-tree-number" }, count),
+ dom.span({ className: "heap-tree-percent" }, percentCount)),
+ dom.span({ className: "heap-tree-item-field heap-tree-item-total-bytes" },
+ dom.span({ className: "heap-tree-number" }, totalBytes),
+ dom.span({ className: "heap-tree-percent" }, percentTotalBytes)),
+ dom.span({ className: "heap-tree-item-field heap-tree-item-total-count" },
+ dom.span({ className: "heap-tree-number" }, totalCount),
+ dom.span({ className: "heap-tree-percent" }, percentTotalCount)),
+ individualsCell,
+ dom.span(
+ {
+ className: "heap-tree-item-field heap-tree-item-name",
+ style: { marginInlineStart: depth * TREE_ROW_HEIGHT }
+ },
+ arrow,
+ pointer,
+ this.toLabel(item.name, onViewSourceInDebugger)
+ )
+ );
+ },
+
+ toLabel(name, linkToDebugger) {
+ if (isSavedFrame(name)) {
+ return Frame({
+ frame: name,
+ onClick: () => linkToDebugger(name),
+ showFunctionName: true,
+ showHost: true,
+ });
+ }
+
+ if (name === null) {
+ return L10N.getStr("tree-item.root");
+ }
+
+ if (name === "noStack") {
+ return L10N.getStr("tree-item.nostack");
+ }
+
+ if (name === "noFilename") {
+ return L10N.getStr("tree-item.nofilename");
+ }
+
+ return String(name);
+ },
+});
diff --git a/devtools/client/memory/components/census.js b/devtools/client/memory/components/census.js
new file mode 100644
index 000000000..3274b26bb
--- /dev/null
+++ b/devtools/client/memory/components/census.js
@@ -0,0 +1,79 @@
+/* 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/. */
+
+const { DOM: dom, createClass, PropTypes, createFactory } = require("devtools/client/shared/vendor/react");
+const Tree = createFactory(require("devtools/client/shared/components/tree"));
+const CensusTreeItem = createFactory(require("./census-tree-item"));
+const { createParentMap } = require("../utils");
+const { TREE_ROW_HEIGHT } = require("../constants");
+const { censusModel, diffingModel } = require("../models");
+
+const Census = module.exports = createClass({
+ displayName: "Census",
+
+ propTypes: {
+ census: censusModel,
+ onExpand: PropTypes.func.isRequired,
+ onCollapse: PropTypes.func.isRequired,
+ onFocus: PropTypes.func.isRequired,
+ onViewSourceInDebugger: PropTypes.func.isRequired,
+ onViewIndividuals: PropTypes.func.isRequired,
+ diffing: diffingModel,
+ },
+
+ render() {
+ let {
+ census,
+ onExpand,
+ onCollapse,
+ onFocus,
+ diffing,
+ onViewSourceInDebugger,
+ onViewIndividuals,
+ } = this.props;
+
+ const report = census.report;
+ let parentMap = census.parentMap;
+ const { totalBytes, totalCount } = report;
+
+ const getPercentBytes = totalBytes === 0
+ ? _ => 0
+ : bytes => (bytes / totalBytes) * 100;
+
+ const getPercentCount = totalCount === 0
+ ? _ => 0
+ : count => (count / totalCount) * 100;
+
+ return Tree({
+ autoExpandDepth: 0,
+ focused: census.focused,
+ getParent: node => {
+ const parent = parentMap[node.id];
+ return parent === report ? null : parent;
+ },
+ getChildren: node => node.children || [],
+ isExpanded: node => census.expanded.has(node.id),
+ onExpand,
+ onCollapse,
+ onFocus,
+ renderItem: (item, depth, focused, arrow, expanded) =>
+ new CensusTreeItem({
+ onViewSourceInDebugger,
+ item,
+ depth,
+ focused,
+ arrow,
+ expanded,
+ getPercentBytes,
+ getPercentCount,
+ diffing,
+ inverted: census.display.inverted,
+ onViewIndividuals,
+ }),
+ getRoots: () => report.children || [],
+ getKey: node => node.id,
+ itemHeight: TREE_ROW_HEIGHT,
+ });
+ }
+});
diff --git a/devtools/client/memory/components/dominator-tree-header.js b/devtools/client/memory/components/dominator-tree-header.js
new file mode 100644
index 000000000..ae1c7520e
--- /dev/null
+++ b/devtools/client/memory/components/dominator-tree-header.js
@@ -0,0 +1,44 @@
+/* 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/. */
+
+const { DOM: dom, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
+const { L10N } = require("../utils");
+
+const DominatorTreeHeader = module.exports = createClass({
+ displayName: "DominatorTreeHeader",
+
+ propTypes: { },
+
+ render() {
+ return dom.div(
+ {
+ className: "header"
+ },
+
+ dom.span(
+ {
+ className: "heap-tree-item-bytes",
+ title: L10N.getStr("heapview.field.retainedSize.tooltip"),
+ },
+ L10N.getStr("heapview.field.retainedSize")
+ ),
+
+ dom.span(
+ {
+ className: "heap-tree-item-bytes",
+ title: L10N.getStr("heapview.field.shallowSize.tooltip"),
+ },
+ L10N.getStr("heapview.field.shallowSize")
+ ),
+
+ dom.span(
+ {
+ className: "heap-tree-item-name",
+ title: L10N.getStr("dominatortree.field.label.tooltip"),
+ },
+ L10N.getStr("dominatortree.field.label")
+ )
+ );
+ }
+});
diff --git a/devtools/client/memory/components/dominator-tree-item.js b/devtools/client/memory/components/dominator-tree-item.js
new file mode 100644
index 000000000..bf76ee9b4
--- /dev/null
+++ b/devtools/client/memory/components/dominator-tree-item.js
@@ -0,0 +1,142 @@
+/* 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/. */
+
+const { assert, isSavedFrame } = require("devtools/shared/DevToolsUtils");
+const { DOM: dom, createClass, createFactory, PropTypes } = require("devtools/client/shared/vendor/react");
+const { L10N, formatNumber, formatPercent } = require("../utils");
+const Frame = createFactory(require("devtools/client/shared/components/frame"));
+const { TREE_ROW_HEIGHT } = require("../constants");
+
+const Separator = createFactory(createClass({
+ displayName: "Separator",
+
+ render() {
+ return dom.span({ className: "separator" }, "›");
+ }
+}));
+
+const DominatorTreeItem = module.exports = createClass({
+ displayName: "DominatorTreeItem",
+
+ propTypes: {
+ item: PropTypes.object.isRequired,
+ depth: PropTypes.number.isRequired,
+ arrow: PropTypes.object,
+ focused: PropTypes.bool.isRequired,
+ getPercentSize: PropTypes.func.isRequired,
+ onViewSourceInDebugger: PropTypes.func.isRequired,
+ },
+
+ shouldComponentUpdate(nextProps, nextState) {
+ return this.props.item != nextProps.item
+ || this.props.depth != nextProps.depth
+ || this.props.expanded != nextProps.expanded
+ || this.props.focused != nextProps.focused;
+ },
+
+ render() {
+ let {
+ item,
+ depth,
+ arrow,
+ focused,
+ getPercentSize,
+ onViewSourceInDebugger,
+ } = this.props;
+
+ const retainedSize = formatNumber(item.retainedSize);
+ const percentRetainedSize = formatPercent(getPercentSize(item.retainedSize));
+
+ const shallowSize = formatNumber(item.shallowSize);
+ const percentShallowSize = formatPercent(getPercentSize(item.shallowSize));
+
+ // Build up our label UI as an array of each label piece, which is either a
+ // string or a frame, and separators in between them.
+
+ assert(item.label.length > 0,
+ "Our label should not be empty");
+ const label = Array(item.label.length * 2 - 1);
+ label.fill(undefined);
+
+ for (let i = 0, length = item.label.length; i < length; i++) {
+ const piece = item.label[i];
+ const key = `${item.nodeId}-label-${i}`;
+
+ // `i` is the index of the label piece we are rendering, `label[i*2]` is
+ // where the rendered label piece belngs, and `label[i*2+1]` (if it isn't
+ // out of bounds) is where the separator belongs.
+
+ if (isSavedFrame(piece)) {
+ label[i * 2] = Frame({
+ key,
+ onClick: () => onViewSourceInDebugger(piece),
+ frame: piece,
+ showFunctionName: true
+ });
+ } else if (piece === "noStack") {
+ label[i * 2] = dom.span({ key, className: "not-available" },
+ L10N.getStr("tree-item.nostack"));
+ } else if (piece === "noFilename") {
+ label[i * 2] = dom.span({ key, className: "not-available" },
+ L10N.getStr("tree-item.nofilename"));
+ } else if (piece === "JS::ubi::RootList") {
+ // Don't use the usual labeling machinery for root lists: replace it
+ // with the "GC Roots" string.
+ label.splice(0, label.length);
+ label.push(L10N.getStr("tree-item.rootlist"));
+ break;
+ } else {
+ label[i * 2] = piece;
+ }
+
+ // If this is not the last piece of the label, add a separator.
+ if (i < length - 1) {
+ label[i * 2 + 1] = Separator({ key: `${item.nodeId}-separator-${i}` });
+ }
+ }
+
+ return dom.div(
+ {
+ className: `heap-tree-item ${focused ? "focused" : ""} node-${item.nodeId}`
+ },
+
+ dom.span(
+ {
+ className: "heap-tree-item-field heap-tree-item-bytes"
+ },
+ dom.span(
+ {
+ className: "heap-tree-number"
+ },
+ retainedSize
+ ),
+ dom.span({ className: "heap-tree-percent" }, percentRetainedSize)
+ ),
+
+ dom.span(
+ {
+ className: "heap-tree-item-field heap-tree-item-bytes"
+ },
+ dom.span(
+ {
+ className: "heap-tree-number"
+ },
+ shallowSize
+ ),
+ dom.span({ className: "heap-tree-percent" }, percentShallowSize)
+ ),
+
+ dom.span(
+ {
+ className: "heap-tree-item-field heap-tree-item-name",
+ style: { marginInlineStart: depth * TREE_ROW_HEIGHT }
+ },
+ arrow,
+ label,
+ dom.span({ className: "heap-tree-item-address" },
+ `@ 0x${item.nodeId.toString(16)}`)
+ )
+ );
+ },
+});
diff --git a/devtools/client/memory/components/dominator-tree.js b/devtools/client/memory/components/dominator-tree.js
new file mode 100644
index 000000000..a1105a4fc
--- /dev/null
+++ b/devtools/client/memory/components/dominator-tree.js
@@ -0,0 +1,216 @@
+/* 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/. */
+
+const { DOM: dom, createClass, PropTypes, createFactory } = require("devtools/client/shared/vendor/react");
+const { assert, safeErrorString } = require("devtools/shared/DevToolsUtils");
+const { createParentMap } = require("devtools/shared/heapsnapshot/CensusUtils");
+const Tree = createFactory(require("devtools/client/shared/components/tree"));
+const DominatorTreeItem = createFactory(require("./dominator-tree-item"));
+const { L10N } = require("../utils");
+const { TREE_ROW_HEIGHT, dominatorTreeState } = require("../constants");
+const { dominatorTreeModel } = require("../models");
+const DominatorTreeLazyChildren = require("../dominator-tree-lazy-children");
+
+const DOMINATOR_TREE_AUTO_EXPAND_DEPTH = 3;
+
+/**
+ * A throbber that represents a subtree in the dominator tree that is actively
+ * being incrementally loaded and fetched from the `HeapAnalysesWorker`.
+ */
+const DominatorTreeSubtreeFetching = createFactory(createClass({
+ displayName: "DominatorTreeSubtreeFetching",
+
+ shouldComponentUpdate(nextProps, nextState) {
+ return this.props.depth !== nextProps.depth
+ || this.props.focused !== nextProps.focused;
+ },
+
+ propTypes: {
+ depth: PropTypes.number.isRequired,
+ focused: PropTypes.bool.isRequired,
+ },
+
+ render() {
+ let {
+ depth,
+ focused,
+ } = this.props;
+
+ return dom.div(
+ {
+ className: `heap-tree-item subtree-fetching ${focused ? "focused" : ""}`
+ },
+ dom.span({ className: "heap-tree-item-field heap-tree-item-bytes" }),
+ dom.span({ className: "heap-tree-item-field heap-tree-item-bytes" }),
+ dom.span({
+ className: "heap-tree-item-field heap-tree-item-name devtools-throbber",
+ style: { marginInlineStart: depth * TREE_ROW_HEIGHT }
+ })
+ );
+ }
+}));
+
+/**
+ * A link to fetch and load more siblings in the dominator tree, when there are
+ * already many loaded above.
+ */
+const DominatorTreeSiblingLink = createFactory(createClass({
+ displayName: "DominatorTreeSiblingLink",
+
+ propTypes: {
+ depth: PropTypes.number.isRequired,
+ focused: PropTypes.bool.isRequired,
+ item: PropTypes.instanceOf(DominatorTreeLazyChildren).isRequired,
+ onLoadMoreSiblings: PropTypes.func.isRequired,
+ },
+
+ shouldComponentUpdate(nextProps, nextState) {
+ return this.props.depth !== nextProps.depth
+ || this.props.focused !== nextProps.focused;
+ },
+
+ render() {
+ let {
+ depth,
+ focused,
+ item,
+ onLoadMoreSiblings,
+ } = this.props;
+
+ return dom.div(
+ {
+ className: `heap-tree-item more-children ${focused ? "focused" : ""}`
+ },
+ dom.span({ className: "heap-tree-item-field heap-tree-item-bytes" }),
+ dom.span({ className: "heap-tree-item-field heap-tree-item-bytes" }),
+ dom.span(
+ {
+ className: "heap-tree-item-field heap-tree-item-name",
+ style: { marginInlineStart: depth * TREE_ROW_HEIGHT }
+ },
+ dom.a(
+ {
+ onClick: () => onLoadMoreSiblings(item)
+ },
+ L10N.getStr("tree-item.load-more")
+ )
+ )
+ );
+ }
+}));
+
+/**
+ * The actual dominator tree rendered as an expandable and collapsible tree.
+ */
+const DominatorTree = module.exports = createClass({
+ displayName: "DominatorTree",
+
+ propTypes: {
+ dominatorTree: dominatorTreeModel.isRequired,
+ onLoadMoreSiblings: PropTypes.func.isRequired,
+ onViewSourceInDebugger: PropTypes.func.isRequired,
+ onExpand: PropTypes.func.isRequired,
+ onCollapse: PropTypes.func.isRequired,
+ },
+
+ shouldComponentUpdate(nextProps, nextState) {
+ // Safe to use referential equality here because all of our mutations on
+ // dominator tree models use immutableUpdate in a persistent manner. The
+ // exception to the rule are mutations of the expanded set, however we take
+ // care that the dominatorTree model itself is still re-allocated when
+ // mutations to the expanded set occur. Because of the re-allocations, we
+ // can continue using referential equality here.
+ return this.props.dominatorTree !== nextProps.dominatorTree;
+ },
+
+ render() {
+ const { dominatorTree, onViewSourceInDebugger, onLoadMoreSiblings } = this.props;
+
+ const parentMap = createParentMap(dominatorTree.root, node => node.nodeId);
+
+ return Tree({
+ key: "dominator-tree-tree",
+ autoExpandDepth: DOMINATOR_TREE_AUTO_EXPAND_DEPTH,
+ focused: dominatorTree.focused,
+ getParent: node =>
+ node instanceof DominatorTreeLazyChildren
+ ? parentMap[node.parentNodeId()]
+ : parentMap[node.nodeId],
+ getChildren: node => {
+ const children = node.children ? node.children.slice() : [];
+ if (node.moreChildrenAvailable) {
+ children.push(new DominatorTreeLazyChildren(node.nodeId, children.length));
+ }
+ return children;
+ },
+ isExpanded: node => {
+ return node instanceof DominatorTreeLazyChildren
+ ? false
+ : dominatorTree.expanded.has(node.nodeId);
+ },
+ onExpand: item => {
+ if (item instanceof DominatorTreeLazyChildren) {
+ return;
+ }
+
+ if (item.moreChildrenAvailable && (!item.children || !item.children.length)) {
+ const startIndex = item.children ? item.children.length : 0;
+ onLoadMoreSiblings(new DominatorTreeLazyChildren(item.nodeId, startIndex));
+ }
+
+ this.props.onExpand(item);
+ },
+ onCollapse: item => {
+ if (item instanceof DominatorTreeLazyChildren) {
+ return;
+ }
+
+ this.props.onCollapse(item);
+ },
+ onFocus: item => {
+ if (item instanceof DominatorTreeLazyChildren) {
+ return;
+ }
+
+ this.props.onFocus(item);
+ },
+ renderItem: (item, depth, focused, arrow, expanded) => {
+ if (item instanceof DominatorTreeLazyChildren) {
+ if (item.isFirstChild()) {
+ assert(dominatorTree.state === dominatorTreeState.INCREMENTAL_FETCHING,
+ "If we are displaying a throbber for loading a subtree, " +
+ "then we should be INCREMENTAL_FETCHING those children right now");
+ return DominatorTreeSubtreeFetching({
+ key: item.key(),
+ depth,
+ focused,
+ });
+ }
+
+ return DominatorTreeSiblingLink({
+ key: item.key(),
+ item,
+ depth,
+ focused,
+ onLoadMoreSiblings,
+ });
+ }
+
+ return DominatorTreeItem({
+ item,
+ depth,
+ focused,
+ arrow,
+ expanded,
+ getPercentSize: size => (size / dominatorTree.root.retainedSize) * 100,
+ onViewSourceInDebugger,
+ });
+ },
+ getRoots: () => [dominatorTree.root],
+ getKey: node =>
+ node instanceof DominatorTreeLazyChildren ? node.key() : node.nodeId,
+ itemHeight: TREE_ROW_HEIGHT,
+ });
+ }
+});
diff --git a/devtools/client/memory/components/heap.js b/devtools/client/memory/components/heap.js
new file mode 100644
index 000000000..786f37ae1
--- /dev/null
+++ b/devtools/client/memory/components/heap.js
@@ -0,0 +1,455 @@
+/* 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/. */
+
+const { DOM: dom, createClass, PropTypes, createFactory } = require("devtools/client/shared/vendor/react");
+const { assert, safeErrorString } = require("devtools/shared/DevToolsUtils");
+const Census = createFactory(require("./census"));
+const CensusHeader = createFactory(require("./census-header"));
+const DominatorTree = createFactory(require("./dominator-tree"));
+const DominatorTreeHeader = createFactory(require("./dominator-tree-header"));
+const TreeMap = createFactory(require("./tree-map"));
+const HSplitBox = createFactory(require("devtools/client/shared/components/h-split-box"));
+const Individuals = createFactory(require("./individuals"));
+const IndividualsHeader = createFactory(require("./individuals-header"));
+const ShortestPaths = createFactory(require("./shortest-paths"));
+const { getStatusTextFull, L10N } = require("../utils");
+const {
+ snapshotState: states,
+ diffingState,
+ viewState,
+ censusState,
+ treeMapState,
+ dominatorTreeState,
+ individualsState,
+} = require("../constants");
+const models = require("../models");
+const { snapshot: snapshotModel, diffingModel } = models;
+
+/**
+ * Get the app state's current state atom.
+ *
+ * @see the relevant state string constants in `../constants.js`.
+ *
+ * @param {models.view} view
+ * @param {snapshotModel} snapshot
+ * @param {diffingModel} diffing
+ * @param {individualsModel} individuals
+ *
+ * @return {snapshotState|diffingState|dominatorTreeState}
+ */
+function getState(view, snapshot, diffing, individuals) {
+ switch (view.state) {
+ case viewState.CENSUS:
+ return snapshot.census
+ ? snapshot.census.state
+ : snapshot.state;
+
+ case viewState.DIFFING:
+ return diffing.state;
+
+ case viewState.TREE_MAP:
+ return snapshot.treeMap
+ ? snapshot.treeMap.state
+ : snapshot.state;
+
+ case viewState.DOMINATOR_TREE:
+ return snapshot.dominatorTree
+ ? snapshot.dominatorTree.state
+ : snapshot.state;
+
+ case viewState.INDIVIDUALS:
+ return individuals.state;
+ }
+
+ assert(false, `Unexpected view state: ${view.state}`);
+ return null;
+}
+
+/**
+ * Return true if we should display a status message when we are in the given
+ * state. Return false otherwise.
+ *
+ * @param {snapshotState|diffingState|dominatorTreeState} state
+ * @param {models.view} view
+ * @param {snapshotModel} snapshot
+ *
+ * @returns {Boolean}
+ */
+function shouldDisplayStatus(state, view, snapshot) {
+ switch (state) {
+ case states.IMPORTING:
+ case states.SAVING:
+ case states.SAVED:
+ case states.READING:
+ case censusState.SAVING:
+ case treeMapState.SAVING:
+ case diffingState.SELECTING:
+ case diffingState.TAKING_DIFF:
+ case dominatorTreeState.COMPUTING:
+ case dominatorTreeState.COMPUTED:
+ case dominatorTreeState.FETCHING:
+ case individualsState.COMPUTING_DOMINATOR_TREE:
+ case individualsState.FETCHING:
+ return true;
+ }
+ return view.state === viewState.DOMINATOR_TREE && !snapshot.dominatorTree;
+}
+
+/**
+ * Get the status text to display for the given state.
+ *
+ * @param {snapshotState|diffingState|dominatorTreeState} state
+ * @param {diffingModel} diffing
+ *
+ * @returns {String}
+ */
+function getStateStatusText(state, diffing) {
+ if (state === diffingState.SELECTING) {
+ return L10N.getStr(diffing.firstSnapshotId === null
+ ? "diffing.prompt.selectBaseline"
+ : "diffing.prompt.selectComparison");
+ }
+
+ return getStatusTextFull(state);
+}
+
+/**
+ * Given that we should display a status message, return true if we should also
+ * display a throbber along with the status message. Return false otherwise.
+ *
+ * @param {diffingModel} diffing
+ *
+ * @returns {Boolean}
+ */
+function shouldDisplayThrobber(diffing) {
+ return !diffing || diffing.state !== diffingState.SELECTING;
+}
+
+/**
+ * Get the current state's error, or return null if there is none.
+ *
+ * @param {snapshotModel} snapshot
+ * @param {diffingModel} diffing
+ * @param {individualsModel} individuals
+ *
+ * @returns {Error|null}
+ */
+function getError(snapshot, diffing, individuals) {
+ if (diffing) {
+ if (diffing.state === diffingState.ERROR) {
+ return diffing.error;
+ }
+ if (diffing.census === censusState.ERROR) {
+ return diffing.census.error;
+ }
+ }
+
+ if (snapshot) {
+ if (snapshot.state === states.ERROR) {
+ return snapshot.error;
+ }
+
+ if (snapshot.census === censusState.ERROR) {
+ return snapshot.census.error;
+ }
+
+ if (snapshot.treeMap === treeMapState.ERROR) {
+ return snapshot.treeMap.error;
+ }
+
+ if (snapshot.dominatorTree &&
+ snapshot.dominatorTree.state === dominatorTreeState.ERROR) {
+ return snapshot.dominatorTree.error;
+ }
+ }
+
+ if (individuals && individuals.state === individualsState.ERROR) {
+ return individuals.error;
+ }
+
+ return null;
+}
+
+/**
+ * Main view for the memory tool.
+ *
+ * The Heap component contains several panels for different states; an initial
+ * state of only a button to take a snapshot, loading states, the census view
+ * tree, the dominator tree, etc.
+ */
+const Heap = module.exports = createClass({
+ displayName: "Heap",
+
+ propTypes: {
+ onSnapshotClick: PropTypes.func.isRequired,
+ onLoadMoreSiblings: PropTypes.func.isRequired,
+ onCensusExpand: PropTypes.func.isRequired,
+ onCensusCollapse: PropTypes.func.isRequired,
+ onDominatorTreeExpand: PropTypes.func.isRequired,
+ onDominatorTreeCollapse: PropTypes.func.isRequired,
+ onCensusFocus: PropTypes.func.isRequired,
+ onDominatorTreeFocus: PropTypes.func.isRequired,
+ onShortestPathsResize: PropTypes.func.isRequired,
+ snapshot: snapshotModel,
+ onViewSourceInDebugger: PropTypes.func.isRequired,
+ onPopView: PropTypes.func.isRequired,
+ individuals: models.individuals,
+ onViewIndividuals: PropTypes.func.isRequired,
+ onFocusIndividual: PropTypes.func.isRequired,
+ diffing: diffingModel,
+ view: models.view.isRequired,
+ sizes: PropTypes.object.isRequired,
+ },
+
+ render() {
+ let {
+ snapshot,
+ diffing,
+ onSnapshotClick,
+ onLoadMoreSiblings,
+ onViewSourceInDebugger,
+ onViewIndividuals,
+ individuals,
+ view,
+ } = this.props;
+
+
+ if (!diffing && !snapshot && !individuals) {
+ return this._renderInitial(onSnapshotClick);
+ }
+
+ const state = getState(view, snapshot, diffing, individuals);
+ const statusText = getStateStatusText(state, diffing);
+
+ if (shouldDisplayStatus(state, view, snapshot)) {
+ return this._renderStatus(state, statusText, diffing);
+ }
+
+ const error = getError(snapshot, diffing, individuals);
+ if (error) {
+ return this._renderError(state, statusText, error);
+ }
+
+ if (view.state === viewState.CENSUS || view.state === viewState.DIFFING) {
+ const census = view.state === viewState.CENSUS
+ ? snapshot.census
+ : diffing.census;
+ if (!census) {
+ return this._renderStatus(state, statusText, diffing);
+ }
+ return this._renderCensus(state, census, diffing, onViewSourceInDebugger,
+ onViewIndividuals);
+ }
+
+ if (view.state === viewState.TREE_MAP) {
+ return this._renderTreeMap(state, snapshot.treeMap);
+ }
+
+ if (view.state === viewState.INDIVIDUALS) {
+ assert(individuals.state === individualsState.FETCHED,
+ "Should have fetched the individuals -- other states are rendered as statuses");
+ return this._renderIndividuals(state, individuals,
+ individuals.dominatorTree,
+ onViewSourceInDebugger);
+ }
+
+ assert(view.state === viewState.DOMINATOR_TREE,
+ "If we aren't in progress, looking at a census, or diffing, then we " +
+ "must be looking at a dominator tree");
+ assert(!diffing, "Should not have diffing");
+ assert(snapshot.dominatorTree, "Should have a dominator tree");
+
+ return this._renderDominatorTree(state, onViewSourceInDebugger, snapshot.dominatorTree,
+ onLoadMoreSiblings);
+ },
+
+ /**
+ * Render the heap view's container panel with the given contents inside of
+ * it.
+ *
+ * @param {snapshotState|diffingState|dominatorTreeState} state
+ * @param {...Any} contents
+ */
+ _renderHeapView(state, ...contents) {
+ return dom.div(
+ {
+ id: "heap-view",
+ "data-state": state
+ },
+ dom.div(
+ {
+ className: "heap-view-panel",
+ "data-state": state,
+ },
+ ...contents
+ )
+ );
+ },
+
+ _renderInitial(onSnapshotClick) {
+ return this._renderHeapView("initial", dom.button(
+ {
+ className: "devtools-toolbarbutton take-snapshot",
+ onClick: onSnapshotClick,
+ "data-standalone": true,
+ "data-text-only": true,
+ },
+ L10N.getStr("take-snapshot")
+ ));
+ },
+
+ _renderStatus(state, statusText, diffing) {
+ let throbber = "";
+ if (shouldDisplayThrobber(diffing)) {
+ throbber = "devtools-throbber";
+ }
+
+ return this._renderHeapView(state, dom.span(
+ {
+ className: `snapshot-status ${throbber}`
+ },
+ statusText
+ ));
+ },
+
+ _renderError(state, statusText, error) {
+ return this._renderHeapView(
+ state,
+ dom.span({ className: "snapshot-status error" }, statusText),
+ dom.pre({}, safeErrorString(error))
+ );
+ },
+
+ _renderCensus(state, census, diffing, onViewSourceInDebugger, onViewIndividuals) {
+ assert(census.report, "Should not render census that does not have a report");
+
+ if (!census.report.children) {
+ const msg = diffing ? L10N.getStr("heapview.no-difference")
+ : census.filter ? L10N.getStr("heapview.none-match")
+ : L10N.getStr("heapview.empty");
+ return this._renderHeapView(state, dom.div({ className: "empty" }, msg));
+ }
+
+ const contents = [];
+
+ if (census.display.breakdown.by === "allocationStack"
+ && census.report.children
+ && census.report.children.length === 1
+ && census.report.children[0].name === "noStack") {
+ contents.push(dom.div({ className: "error no-allocation-stacks" },
+ L10N.getStr("heapview.noAllocationStacks")));
+ }
+
+ contents.push(CensusHeader({ diffing }));
+ contents.push(Census({
+ onViewSourceInDebugger,
+ onViewIndividuals,
+ diffing,
+ census,
+ onExpand: node => this.props.onCensusExpand(census, node),
+ onCollapse: node => this.props.onCensusCollapse(census, node),
+ onFocus: node => this.props.onCensusFocus(census, node),
+ }));
+
+ return this._renderHeapView(state, ...contents);
+ },
+
+ _renderTreeMap(state, treeMap) {
+ return this._renderHeapView(
+ state,
+ TreeMap({ treeMap })
+ );
+ },
+
+ _renderIndividuals(state, individuals, dominatorTree, onViewSourceInDebugger) {
+ assert(individuals.state === individualsState.FETCHED,
+ "Should have fetched individuals");
+ assert(dominatorTree && dominatorTree.root,
+ "Should have a dominator tree and its root");
+
+ const tree = dom.div(
+ {
+ className: "vbox",
+ style: {
+ overflowY: "auto"
+ }
+ },
+ IndividualsHeader(),
+ Individuals({
+ individuals,
+ dominatorTree,
+ onViewSourceInDebugger,
+ onFocus: this.props.onFocusIndividual
+ })
+ );
+
+ const shortestPaths = ShortestPaths({
+ graph: individuals.focused
+ ? individuals.focused.shortestPaths
+ : null
+ });
+
+ return this._renderHeapView(
+ state,
+ dom.div(
+ { className: "hbox devtools-toolbar" },
+ dom.label(
+ { id: "pop-view-button-label" },
+ dom.button(
+ {
+ id: "pop-view-button",
+ className: "devtools-button",
+ onClick: this.props.onPopView,
+ },
+ L10N.getStr("toolbar.pop-view")
+ ),
+ L10N.getStr("toolbar.pop-view.label")
+ ),
+ L10N.getStr("toolbar.viewing-individuals")
+ ),
+ HSplitBox({
+ start: tree,
+ end: shortestPaths,
+ startWidth: this.props.sizes.shortestPathsSize,
+ onResize: this.props.onShortestPathsResize,
+ })
+ );
+ },
+
+ _renderDominatorTree(state, onViewSourceInDebugger, dominatorTree, onLoadMoreSiblings) {
+ const tree = dom.div(
+ {
+ className: "vbox",
+ style: {
+ overflowY: "auto"
+ }
+ },
+ DominatorTreeHeader(),
+ DominatorTree({
+ onViewSourceInDebugger,
+ dominatorTree,
+ onLoadMoreSiblings,
+ onExpand: this.props.onDominatorTreeExpand,
+ onCollapse: this.props.onDominatorTreeCollapse,
+ onFocus: this.props.onDominatorTreeFocus,
+ })
+ );
+
+ const shortestPaths = ShortestPaths({
+ graph: dominatorTree.focused
+ ? dominatorTree.focused.shortestPaths
+ : null
+ });
+
+ return this._renderHeapView(
+ state,
+ HSplitBox({
+ start: tree,
+ end: shortestPaths,
+ startWidth: this.props.sizes.shortestPathsSize,
+ onResize: this.props.onShortestPathsResize,
+ })
+ );
+ },
+});
diff --git a/devtools/client/memory/components/individuals-header.js b/devtools/client/memory/components/individuals-header.js
new file mode 100644
index 000000000..7cacd163e
--- /dev/null
+++ b/devtools/client/memory/components/individuals-header.js
@@ -0,0 +1,44 @@
+/* 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/. */
+
+const { DOM: dom, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
+const { L10N } = require("../utils");
+
+const IndividualsHeader = module.exports = createClass({
+ displayName: "IndividualsHeader",
+
+ propTypes: { },
+
+ render() {
+ return dom.div(
+ {
+ className: "header"
+ },
+
+ dom.span(
+ {
+ className: "heap-tree-item-bytes",
+ title: L10N.getStr("heapview.field.retainedSize.tooltip"),
+ },
+ L10N.getStr("heapview.field.retainedSize")
+ ),
+
+ dom.span(
+ {
+ className: "heap-tree-item-bytes",
+ title: L10N.getStr("heapview.field.shallowSize.tooltip"),
+ },
+ L10N.getStr("heapview.field.shallowSize")
+ ),
+
+ dom.span(
+ {
+ className: "heap-tree-item-name",
+ title: L10N.getStr("individuals.field.node.tooltip"),
+ },
+ L10N.getStr("individuals.field.node")
+ )
+ );
+ }
+});
diff --git a/devtools/client/memory/components/individuals.js b/devtools/client/memory/components/individuals.js
new file mode 100644
index 000000000..56c784820
--- /dev/null
+++ b/devtools/client/memory/components/individuals.js
@@ -0,0 +1,61 @@
+/* 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/. */
+
+const { DOM: dom, createClass, PropTypes, createFactory } = require("devtools/client/shared/vendor/react");
+const { assert } = require("devtools/shared/DevToolsUtils");
+const { createParentMap } = require("devtools/shared/heapsnapshot/CensusUtils");
+const Tree = createFactory(require("devtools/client/shared/components/tree"));
+const DominatorTreeItem = createFactory(require("./dominator-tree-item"));
+const { L10N } = require("../utils");
+const { TREE_ROW_HEIGHT } = require("../constants");
+const models = require("../models");
+
+/**
+ * The list of individuals in a census group.
+ */
+const Individuals = module.exports = createClass({
+ displayName: "Individuals",
+
+ propTypes: {
+ onViewSourceInDebugger: PropTypes.func.isRequired,
+ onFocus: PropTypes.func.isRequired,
+ individuals: models.individuals,
+ dominatorTree: models.dominatorTreeModel,
+ },
+
+ render() {
+ const {
+ individuals,
+ dominatorTree,
+ onViewSourceInDebugger,
+ onFocus,
+ } = this.props;
+
+ return Tree({
+ key: "individuals-tree",
+ autoExpandDepth: 0,
+ focused: individuals.focused,
+ getParent: node => null,
+ getChildren: node => [],
+ isExpanded: node => false,
+ onExpand: () => {},
+ onCollapse: () => {},
+ onFocus,
+ renderItem: (item, depth, focused, _, expanded) => {
+ return DominatorTreeItem({
+ item,
+ depth,
+ focused,
+ arrow: undefined,
+ expanded,
+ getPercentSize: size => (size / dominatorTree.root.retainedSize) * 100,
+ onViewSourceInDebugger,
+ });
+ },
+ getRoots: () => individuals.nodes,
+ getKey: node => node.nodeId,
+ itemHeight: TREE_ROW_HEIGHT,
+ });
+ }
+});
diff --git a/devtools/client/memory/components/list.js b/devtools/client/memory/components/list.js
new file mode 100644
index 000000000..3aa27da69
--- /dev/null
+++ b/devtools/client/memory/components/list.js
@@ -0,0 +1,35 @@
+/* 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/. */
+
+const { DOM: dom, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
+
+/**
+ * Generic list component that takes another react component to represent
+ * the children nodes as `itemComponent`, and a list of items to render
+ * as that component with a click handler.
+ */
+const List = module.exports = createClass({
+ displayName: "List",
+
+ propTypes: {
+ itemComponent: PropTypes.any.isRequired,
+ onClick: PropTypes.func,
+ items: PropTypes.array.isRequired,
+ },
+
+ render() {
+ let { items, onClick, itemComponent: Item } = this.props;
+
+ return (
+ dom.ul({ className: "list" }, ...items.map((item, index) => {
+ return Item(Object.assign({}, this.props, {
+ key: index,
+ item,
+ index,
+ onClick: () => onClick(item),
+ }));
+ }))
+ );
+ }
+});
diff --git a/devtools/client/memory/components/moz.build b/devtools/client/memory/components/moz.build
new file mode 100644
index 000000000..529454dd9
--- /dev/null
+++ b/devtools/client/memory/components/moz.build
@@ -0,0 +1,25 @@
+# vim: set filetype=python:
+# 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/.
+
+DIRS += [
+ 'tree-map',
+]
+
+DevToolsModules(
+ 'census-header.js',
+ 'census-tree-item.js',
+ 'census.js',
+ 'dominator-tree-header.js',
+ 'dominator-tree-item.js',
+ 'dominator-tree.js',
+ 'heap.js',
+ 'individuals-header.js',
+ 'individuals.js',
+ 'list.js',
+ 'shortest-paths.js',
+ 'snapshot-list-item.js',
+ 'toolbar.js',
+ 'tree-map.js',
+)
diff --git a/devtools/client/memory/components/shortest-paths.js b/devtools/client/memory/components/shortest-paths.js
new file mode 100644
index 000000000..23cb8134b
--- /dev/null
+++ b/devtools/client/memory/components/shortest-paths.js
@@ -0,0 +1,184 @@
+/* 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 {
+ DOM: dom,
+ createClass,
+ PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const { isSavedFrame } = require("devtools/shared/DevToolsUtils");
+const { getSourceNames } = require("devtools/client/shared/source-utils");
+const { L10N } = require("../utils");
+
+const GRAPH_DEFAULTS = {
+ translate: [20, 20],
+ scale: 1
+};
+
+const NO_STACK = "noStack";
+const NO_FILENAME = "noFilename";
+const ROOT_LIST = "JS::ubi::RootList";
+
+function stringifyLabel(label, id) {
+ const sanitized = [];
+
+ for (let i = 0, length = label.length; i < length; i++) {
+ const piece = label[i];
+
+ if (isSavedFrame(piece)) {
+ const { short } = getSourceNames(piece.source);
+ sanitized[i] = `${piece.functionDisplayName} @ ${short}:${piece.line}:${piece.column}`;
+ } else if (piece === NO_STACK) {
+ sanitized[i] = L10N.getStr("tree-item.nostack");
+ } else if (piece === NO_FILENAME) {
+ sanitized[i] = L10N.getStr("tree-item.nofilename");
+ } else if (piece === ROOT_LIST) {
+ // Don't use the usual labeling machinery for root lists: replace it
+ // with the "GC Roots" string.
+ sanitized.splice(0, label.length);
+ sanitized.push(L10N.getStr("tree-item.rootlist"));
+ break;
+ } else {
+ sanitized[i] = "" + piece;
+ }
+ }
+
+ return `${sanitized.join(" › ")} @ 0x${id.toString(16)}`;
+}
+
+module.exports = createClass({
+ displayName: "ShortestPaths",
+
+ propTypes: {
+ graph: PropTypes.shape({
+ nodes: PropTypes.arrayOf(PropTypes.object),
+ edges: PropTypes.arrayOf(PropTypes.object),
+ }),
+ },
+
+ getInitialState() {
+ return { zoom: null };
+ },
+
+ shouldComponentUpdate(nextProps) {
+ return this.props.graph != nextProps.graph;
+ },
+
+ componentDidMount() {
+ if (this.props.graph) {
+ this._renderGraph(this.refs.container, this.props.graph);
+ }
+ },
+
+ componentDidUpdate() {
+ if (this.props.graph) {
+ this._renderGraph(this.refs.container, this.props.graph);
+ }
+ },
+
+ componentWillUnmount() {
+ if (this.state.zoom) {
+ this.state.zoom.on("zoom", null);
+ }
+ },
+
+ render() {
+ let contents;
+ if (this.props.graph) {
+ // Let the componentDidMount or componentDidUpdate method draw the graph
+ // with DagreD3. We just provide the container for the graph here.
+ contents = dom.div({
+ ref: "container",
+ style: {
+ flex: 1,
+ height: "100%",
+ width: "100%",
+ }
+ });
+ } else {
+ contents = dom.div(
+ {
+ id: "shortest-paths-select-node-msg"
+ },
+ L10N.getStr("shortest-paths.select-node")
+ );
+ }
+
+ return dom.div(
+ {
+ id: "shortest-paths",
+ className: "vbox",
+ },
+ dom.label(
+ {
+ id: "shortest-paths-header",
+ className: "header",
+ },
+ L10N.getStr("shortest-paths.header")
+ ),
+ contents
+ );
+ },
+
+ _renderGraph(container, { nodes, edges }) {
+ if (!container.firstChild) {
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ svg.setAttribute("id", "graph-svg");
+ svg.setAttribute("xlink", "http://www.w3.org/1999/xlink");
+ svg.style.width = "100%";
+ svg.style.height = "100%";
+
+ const target = document.createElementNS("http://www.w3.org/2000/svg", "g");
+ target.setAttribute("id", "graph-target");
+ target.style.width = "100%";
+ target.style.height = "100%";
+
+ svg.appendChild(target);
+ container.appendChild(svg);
+ }
+
+ const graph = new dagreD3.Digraph();
+
+ for (let i = 0; i < nodes.length; i++) {
+ graph.addNode(nodes[i].id, {
+ id: nodes[i].id,
+ label: stringifyLabel(nodes[i].label, nodes[i].id),
+ });
+ }
+
+ for (let i = 0; i < edges.length; i++) {
+ graph.addEdge(null, edges[i].from, edges[i].to, {
+ label: edges[i].name
+ });
+ }
+
+ const renderer = new dagreD3.Renderer();
+ renderer.drawNodes();
+ renderer.drawEdgePaths();
+
+ const svg = d3.select("#graph-svg");
+ const target = d3.select("#graph-target");
+
+ let zoom = this.state.zoom;
+ if (!zoom) {
+ zoom = d3.behavior.zoom().on("zoom", function () {
+ target.attr(
+ "transform",
+ `translate(${d3.event.translate}) scale(${d3.event.scale})`
+ );
+ });
+ svg.call(zoom);
+ this.setState({ zoom });
+ }
+
+ const { translate, scale } = GRAPH_DEFAULTS;
+ zoom.scale(scale);
+ zoom.translate(translate);
+ target.attr("transform", `translate(${translate}) scale(${scale})`);
+
+ const layout = dagreD3.layout();
+ renderer.layout(layout).run(graph, target);
+ },
+});
diff --git a/devtools/client/memory/components/snapshot-list-item.js b/devtools/client/memory/components/snapshot-list-item.js
new file mode 100644
index 000000000..37db81d13
--- /dev/null
+++ b/devtools/client/memory/components/snapshot-list-item.js
@@ -0,0 +1,114 @@
+/* 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/. */
+
+const { assert } = require("devtools/shared/DevToolsUtils");
+const { DOM: dom, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
+const {
+ L10N,
+ getSnapshotTitle,
+ getSnapshotTotals,
+ getStatusText,
+ snapshotIsDiffable,
+ getSavedCensus
+} = require("../utils");
+const {
+ snapshotState: states,
+ diffingState,
+ censusState,
+ treeMapState
+} = require("../constants");
+const { snapshot: snapshotModel } = require("../models");
+
+const SnapshotListItem = module.exports = createClass({
+ displayName: "SnapshotListItem",
+
+ propTypes: {
+ onClick: PropTypes.func.isRequired,
+ onSave: PropTypes.func.isRequired,
+ onDelete: PropTypes.func.isRequired,
+ item: snapshotModel.isRequired,
+ index: PropTypes.number.isRequired,
+ },
+
+ render() {
+ let { index, item: snapshot, onClick, onSave, onDelete, diffing } = this.props;
+ let className = `snapshot-list-item ${snapshot.selected ? " selected" : ""}`;
+ let statusText = getStatusText(snapshot.state);
+ let wantThrobber = !!statusText;
+ let title = getSnapshotTitle(snapshot);
+
+ const selectedForDiffing = diffing
+ && (diffing.firstSnapshotId === snapshot.id
+ || diffing.secondSnapshotId === snapshot.id);
+
+ let checkbox;
+ if (diffing && snapshotIsDiffable(snapshot)) {
+ if (diffing.state === diffingState.SELECTING) {
+ wantThrobber = false;
+ }
+
+ const checkboxAttrs = {
+ type: "checkbox",
+ checked: false,
+ };
+
+ if (selectedForDiffing) {
+ checkboxAttrs.checked = true;
+ checkboxAttrs.disabled = true;
+ className += " selected";
+ statusText = L10N.getStr(diffing.firstSnapshotId === snapshot.id
+ ? "diffing.baseline"
+ : "diffing.comparison");
+ }
+
+ if (selectedForDiffing || diffing.state == diffingState.SELECTING) {
+ checkbox = dom.input(checkboxAttrs);
+ }
+ }
+
+ let details;
+ if (!selectedForDiffing) {
+ // See if a tree map or census is in the read state.
+ let census = getSavedCensus(snapshot);
+
+ // If there is census data, fill in the total bytes.
+ if (census) {
+ let { bytes } = getSnapshotTotals(census);
+ let formatBytes = L10N.getFormatStr("aggregate.mb", L10N.numberWithDecimals(bytes / 1000000, 2));
+
+ details = dom.span({ className: "snapshot-totals" },
+ dom.span({ className: "total-bytes" }, formatBytes)
+ );
+ }
+ }
+ if (!details) {
+ details = dom.span({ className: "snapshot-state" }, statusText);
+ }
+
+ let saveLink = !snapshot.path ? void 0 : dom.a({
+ onClick: () => onSave(snapshot),
+ className: "save",
+ }, L10N.getStr("snapshot.io.save"));
+
+ let deleteButton = !snapshot.path ? void 0 : dom.button({
+ onClick: () => onDelete(snapshot),
+ className: "devtools-button delete",
+ title: L10N.getStr("snapshot.io.delete")
+ });
+
+ return (
+ dom.li({ className, onClick },
+ dom.span({ className: `snapshot-title ${wantThrobber ? " devtools-throbber" : ""}` },
+ checkbox,
+ title,
+ deleteButton
+ ),
+ dom.span({ className: "snapshot-info" },
+ details,
+ saveLink
+ )
+ )
+ );
+ }
+});
diff --git a/devtools/client/memory/components/toolbar.js b/devtools/client/memory/components/toolbar.js
new file mode 100644
index 000000000..de60b2af9
--- /dev/null
+++ b/devtools/client/memory/components/toolbar.js
@@ -0,0 +1,300 @@
+/* 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 { assert } = require("devtools/shared/DevToolsUtils");
+const { DOM: dom, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
+const { L10N } = require("../utils");
+const models = require("../models");
+const { viewState } = require("../constants");
+
+module.exports = createClass({
+ displayName: "Toolbar",
+
+ propTypes: {
+ censusDisplays: PropTypes.arrayOf(PropTypes.shape({
+ displayName: PropTypes.string.isRequired,
+ })).isRequired,
+ censusDisplay: PropTypes.shape({
+ displayName: PropTypes.string.isRequired,
+ }).isRequired,
+ onTakeSnapshotClick: PropTypes.func.isRequired,
+ onImportClick: PropTypes.func.isRequired,
+ onClearSnapshotsClick: PropTypes.func.isRequired,
+ onCensusDisplayChange: PropTypes.func.isRequired,
+ onToggleRecordAllocationStacks: PropTypes.func.isRequired,
+ allocations: models.allocations,
+ filterString: PropTypes.string,
+ setFilterString: PropTypes.func.isRequired,
+ diffing: models.diffingModel,
+ onToggleDiffing: PropTypes.func.isRequired,
+ view: models.view.isRequired,
+ onViewChange: PropTypes.func.isRequired,
+ labelDisplays: PropTypes.arrayOf(PropTypes.shape({
+ displayName: PropTypes.string.isRequired,
+ })).isRequired,
+ labelDisplay: PropTypes.shape({
+ displayName: PropTypes.string.isRequired,
+ }).isRequired,
+ onLabelDisplayChange: PropTypes.func.isRequired,
+ treeMapDisplays: PropTypes.arrayOf(PropTypes.shape({
+ displayName: PropTypes.string.isRequired,
+ })).isRequired,
+ onTreeMapDisplayChange: PropTypes.func.isRequired,
+ snapshots: PropTypes.arrayOf(models.snapshot).isRequired,
+ },
+
+ render() {
+ let {
+ onTakeSnapshotClick,
+ onImportClick,
+ onClearSnapshotsClick,
+ onCensusDisplayChange,
+ censusDisplays,
+ censusDisplay,
+ labelDisplays,
+ labelDisplay,
+ onLabelDisplayChange,
+ treeMapDisplays,
+ onTreeMapDisplayChange,
+ onToggleRecordAllocationStacks,
+ allocations,
+ filterString,
+ setFilterString,
+ snapshots,
+ diffing,
+ onToggleDiffing,
+ view,
+ onViewChange,
+ } = this.props;
+
+ let viewToolbarOptions;
+ if (view.state == viewState.CENSUS || view.state === viewState.DIFFING) {
+ viewToolbarOptions = dom.div(
+ {
+ className: "toolbar-group"
+ },
+
+ dom.label(
+ {
+ className: "display-by",
+ title: L10N.getStr("toolbar.displayBy.tooltip"),
+ },
+ L10N.getStr("toolbar.displayBy"),
+ dom.select(
+ {
+ id: "select-display",
+ className: "select-display",
+ onChange: e => {
+ const newDisplay =
+ censusDisplays.find(b => b.displayName === e.target.value);
+ onCensusDisplayChange(newDisplay);
+ },
+ value: censusDisplay.displayName,
+ },
+ censusDisplays.map(({ tooltip, displayName }) => dom.option(
+ {
+ key: `display-${displayName}`,
+ value: displayName,
+ title: tooltip,
+ },
+ displayName
+ ))
+ )
+ ),
+
+ dom.div({ id: "toolbar-spacer", className: "spacer" }),
+
+ dom.input({
+ id: "filter",
+ type: "search",
+ className: "devtools-filterinput",
+ placeholder: L10N.getStr("filter.placeholder"),
+ title: L10N.getStr("filter.tooltip"),
+ onChange: event => setFilterString(event.target.value),
+ value: filterString || undefined,
+ })
+ );
+ } else if (view.state == viewState.TREE_MAP) {
+ assert(treeMapDisplays.length >= 1,
+ "Should always have at least one tree map display");
+
+ // Only show the dropdown if there are multiple display options
+ viewToolbarOptions = treeMapDisplays.length > 1
+ ? dom.div(
+ {
+ className: "toolbar-group"
+ },
+
+ dom.label(
+ {
+ className: "display-by",
+ title: L10N.getStr("toolbar.displayBy.tooltip"),
+ },
+ L10N.getStr("toolbar.displayBy"),
+ dom.select(
+ {
+ id: "select-tree-map-display",
+ onChange: e => {
+ const newDisplay =
+ treeMapDisplays.find(b => b.displayName === e.target.value);
+ onTreeMapDisplayChange(newDisplay);
+ },
+ },
+ treeMapDisplays.map(({ tooltip, displayName }) => dom.option(
+ {
+ key: `tree-map-display-${displayName}`,
+ value: displayName,
+ title: tooltip,
+ },
+ displayName
+ ))
+ )
+ )
+ )
+ : null;
+ } else {
+ assert(view.state === viewState.DOMINATOR_TREE ||
+ view.state === viewState.INDIVIDUALS);
+
+ viewToolbarOptions = dom.div(
+ {
+ className: "toolbar-group"
+ },
+
+ dom.label(
+ {
+ className: "label-by",
+ title: L10N.getStr("toolbar.labelBy.tooltip"),
+ },
+ L10N.getStr("toolbar.labelBy"),
+ dom.select(
+ {
+ id: "select-label-display",
+ onChange: e => {
+ const newDisplay =
+ labelDisplays.find(b => b.displayName === e.target.value);
+ onLabelDisplayChange(newDisplay);
+ },
+ value: labelDisplay.displayName,
+ },
+ labelDisplays.map(({ tooltip, displayName }) => dom.option(
+ {
+ key: `label-display-${displayName}`,
+ value: displayName,
+ title: tooltip,
+ },
+ displayName
+ ))
+ )
+ )
+ );
+ }
+
+ let viewSelect;
+ if (view.state !== viewState.DIFFING && view.state !== viewState.INDIVIDUALS) {
+ viewSelect = dom.label(
+ {
+ title: L10N.getStr("toolbar.view.tooltip"),
+ },
+ L10N.getStr("toolbar.view"),
+ dom.select(
+ {
+ id: "select-view",
+ onChange: e => onViewChange(e.target.value),
+ defaultValue: view,
+ value: view.state,
+ },
+ dom.option(
+ {
+ value: viewState.TREE_MAP,
+ title: L10N.getStr("toolbar.view.treemap.tooltip"),
+ },
+ L10N.getStr("toolbar.view.treemap")
+ ),
+ dom.option(
+ {
+ value: viewState.CENSUS,
+ title: L10N.getStr("toolbar.view.census.tooltip"),
+ },
+ L10N.getStr("toolbar.view.census")
+ ),
+ dom.option(
+ {
+ value: viewState.DOMINATOR_TREE,
+ title: L10N.getStr("toolbar.view.dominators.tooltip"),
+ },
+ L10N.getStr("toolbar.view.dominators")
+ )
+ )
+ );
+ }
+
+ return (
+ dom.div(
+ {
+ className: "devtools-toolbar"
+ },
+
+ dom.div(
+ {
+ className: "toolbar-group"
+ },
+
+ dom.button({
+ id: "clear-snapshots",
+ className: "clear-snapshots devtools-button",
+ disabled: !snapshots.length,
+ onClick: onClearSnapshotsClick,
+ title: L10N.getStr("clear-snapshots.tooltip")
+ }),
+
+ dom.button({
+ id: "take-snapshot",
+ className: "take-snapshot devtools-button",
+ onClick: onTakeSnapshotClick,
+ title: L10N.getStr("take-snapshot")
+ }),
+
+ dom.button(
+ {
+ id: "diff-snapshots",
+ className: "devtools-button devtools-monospace" + (!!diffing ? " checked" : ""),
+ disabled: snapshots.length < 2,
+ onClick: onToggleDiffing,
+ title: L10N.getStr("diff-snapshots.tooltip"),
+ }
+ ),
+
+ dom.button(
+ {
+ id: "import-snapshot",
+ className: "devtools-toolbarbutton import-snapshot devtools-button",
+ onClick: onImportClick,
+ title: L10N.getStr("import-snapshot"),
+ }
+ )
+ ),
+
+ dom.label(
+ {
+ id: "record-allocation-stacks-label",
+ title: L10N.getStr("checkbox.recordAllocationStacks.tooltip"),
+ },
+ dom.input({
+ id: "record-allocation-stacks-checkbox",
+ type: "checkbox",
+ checked: allocations.recording,
+ disabled: allocations.togglingInProgress,
+ onChange: onToggleRecordAllocationStacks,
+ }),
+ L10N.getStr("checkbox.recordAllocationStacks")
+ ),
+
+ viewSelect,
+ viewToolbarOptions
+ )
+ );
+ }
+});
diff --git a/devtools/client/memory/components/tree-map.js b/devtools/client/memory/components/tree-map.js
new file mode 100644
index 000000000..b6764605e
--- /dev/null
+++ b/devtools/client/memory/components/tree-map.js
@@ -0,0 +1,71 @@
+/* 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 { DOM: dom, createClass } = require("devtools/client/shared/vendor/react");
+const { treeMapModel } = require("../models");
+const startVisualization = require("./tree-map/start");
+
+module.exports = createClass({
+ propTypes: {
+ treeMap: treeMapModel
+ },
+
+ displayName: "TreeMap",
+
+ getInitialState() {
+ return {};
+ },
+
+ componentDidMount() {
+ const { treeMap } = this.props;
+ if (treeMap && treeMap.report) {
+ this._startVisualization();
+ }
+ },
+
+ shouldComponentUpdate(nextProps) {
+ const oldTreeMap = this.props.treeMap;
+ const newTreeMap = nextProps.treeMap;
+ return oldTreeMap !== newTreeMap;
+ },
+
+ componentDidUpdate(prevProps) {
+ this._stopVisualization();
+
+ if (this.props.treeMap && this.props.treeMap.report) {
+ this._startVisualization();
+ }
+ },
+
+ componentWillUnmount() {
+ if (this.state.stopVisualization) {
+ this.state.stopVisualization();
+ }
+ },
+
+ _stopVisualization() {
+ if (this.state.stopVisualization) {
+ this.state.stopVisualization();
+ this.setState({ stopVisualization: null });
+ }
+ },
+
+ _startVisualization() {
+ const { container } = this.refs;
+ const { report } = this.props.treeMap;
+ const stopVisualization = startVisualization(container, report);
+ this.setState({ stopVisualization });
+ },
+
+ render() {
+ return dom.div(
+ {
+ ref: "container",
+ className: "tree-map-container"
+ }
+ );
+ }
+});
diff --git a/devtools/client/memory/components/tree-map/canvas-utils.js b/devtools/client/memory/components/tree-map/canvas-utils.js
new file mode 100644
index 000000000..c7d67a0bf
--- /dev/null
+++ b/devtools/client/memory/components/tree-map/canvas-utils.js
@@ -0,0 +1,134 @@
+/* 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/. */
+
+/* eslint-env browser */
+
+"use strict";
+
+/**
+ * Create 2 canvases and contexts for drawing onto, 1 main canvas, and 1 zoom
+ * canvas. The main canvas dimensions match the parent div, but the CSS can be
+ * transformed to be zoomed and dragged around (potentially creating a blurry
+ * canvas once zoomed in). The zoom canvas is a zoomed in section that matches
+ * the parent div's dimensions and is kept in place through CSS. A zoomed in
+ * view of the visualization is drawn onto this canvas, providing a crisp zoomed
+ * in view of the tree map.
+ */
+const { debounce } = require("sdk/lang/functional");
+const EventEmitter = require("devtools/shared/event-emitter");
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const FULLSCREEN_STYLE = {
+ width: "100%",
+ height: "100%",
+ position: "absolute",
+};
+
+/**
+ * Create the canvases, resize handlers, and return references to them all
+ *
+ * @param {HTMLDivElement} parentEl
+ * @param {Number} debounceRate
+ * @return {Object}
+ */
+function Canvases(parentEl, debounceRate) {
+ EventEmitter.decorate(this);
+ this.container = createContainingDiv(parentEl);
+
+ // This canvas contains all of the treemap
+ this.main = createCanvas(this.container, "main");
+ // This canvas contains only the zoomed in portion, overlaying the main canvas
+ this.zoom = createCanvas(this.container, "zoom");
+
+ this.removeHandlers = handleResizes(this, debounceRate);
+}
+
+Canvases.prototype = {
+
+ /**
+ * Remove the handlers and elements
+ *
+ * @return {type} description
+ */
+ destroy : function () {
+ this.removeHandlers();
+ this.container.removeChild(this.main.canvas);
+ this.container.removeChild(this.zoom.canvas);
+ }
+};
+
+module.exports = Canvases;
+
+/**
+ * Create the containing div
+ *
+ * @param {HTMLDivElement} parentEl
+ * @return {HTMLDivElement}
+ */
+function createContainingDiv(parentEl) {
+ let div = parentEl.ownerDocument.createElementNS(HTML_NS, "div");
+ Object.assign(div.style, FULLSCREEN_STYLE);
+ parentEl.appendChild(div);
+ return div;
+}
+
+/**
+ * Create a canvas and context
+ *
+ * @param {HTMLDivElement} container
+ * @param {String} className
+ * @return {Object} { canvas, ctx }
+ */
+function createCanvas(container, className) {
+ let window = container.ownerDocument.defaultView;
+ let canvas = container.ownerDocument.createElementNS(HTML_NS, "canvas");
+ container.appendChild(canvas);
+ canvas.width = container.offsetWidth * window.devicePixelRatio;
+ canvas.height = container.offsetHeight * window.devicePixelRatio;
+ canvas.className = className;
+
+ Object.assign(canvas.style, FULLSCREEN_STYLE, {
+ pointerEvents: "none"
+ });
+
+ let ctx = canvas.getContext("2d");
+
+ return { canvas, ctx };
+}
+
+/**
+ * Resize the canvases' resolutions, and fires out the onResize callback
+ *
+ * @param {HTMLDivElement} container
+ * @param {Object} canvases
+ * @param {Number} debounceRate
+ */
+function handleResizes(canvases, debounceRate) {
+ let { container, main, zoom } = canvases;
+ let window = container.ownerDocument.defaultView;
+
+ function resize() {
+ let width = container.offsetWidth * window.devicePixelRatio;
+ let height = container.offsetHeight * window.devicePixelRatio;
+
+ main.canvas.width = width;
+ main.canvas.height = height;
+ zoom.canvas.width = width;
+ zoom.canvas.height = height;
+
+ canvases.emit("resize");
+ }
+
+ // Tests may not need debouncing
+ let debouncedResize = debounceRate > 0
+ ? debounce(resize, debounceRate)
+ : resize;
+
+ window.addEventListener("resize", debouncedResize, false);
+ resize();
+
+ return function removeResizeHandlers() {
+ window.removeEventListener("resize", debouncedResize, false);
+ };
+}
diff --git a/devtools/client/memory/components/tree-map/color-coarse-type.js b/devtools/client/memory/components/tree-map/color-coarse-type.js
new file mode 100644
index 000000000..5f033ea26
--- /dev/null
+++ b/devtools/client/memory/components/tree-map/color-coarse-type.js
@@ -0,0 +1,70 @@
+/* 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";
+
+/**
+ * Color the boxes in the treemap
+ */
+
+const TYPES = [ "objects", "other", "strings", "scripts" ];
+
+// The factors determine how much the hue shifts
+const TYPE_FACTOR = TYPES.length * 3;
+const DEPTH_FACTOR = -10;
+const H = 0.5;
+const S = 0.6;
+const L = 0.9;
+
+/**
+ * Recursively find the index of the coarse type of a node
+ *
+ * @param {Object} node
+ * d3 treemap
+ * @return {Integer}
+ * index
+ */
+function findCoarseTypeIndex(node) {
+ let index = TYPES.indexOf(node.name);
+
+ if (node.parent) {
+ return index === -1 ? findCoarseTypeIndex(node.parent) : index;
+ }
+
+ return TYPES.indexOf("other");
+}
+
+/**
+ * Decide a color value for depth to be used in the HSL computation
+ *
+ * @param {Object} node
+ * @return {Number}
+ */
+function depthColorFactor(node) {
+ return Math.min(1, node.depth / DEPTH_FACTOR);
+}
+
+/**
+ * Decide a color value for type to be used in the HSL computation
+ *
+ * @param {Object} node
+ * @return {Number}
+ */
+function typeColorFactor(node) {
+ return findCoarseTypeIndex(node) / TYPE_FACTOR;
+}
+
+/**
+ * Color a node
+ *
+ * @param {Object} node
+ * @return {Array} HSL values ranged 0-1
+ */
+module.exports = function colorCoarseType(node) {
+ let h = Math.min(1, H + typeColorFactor(node));
+ let s = Math.min(1, S);
+ let l = Math.min(1, L + depthColorFactor(node));
+
+ return [h, s, l];
+};
diff --git a/devtools/client/memory/components/tree-map/drag-zoom.js b/devtools/client/memory/components/tree-map/drag-zoom.js
new file mode 100644
index 000000000..3de970725
--- /dev/null
+++ b/devtools/client/memory/components/tree-map/drag-zoom.js
@@ -0,0 +1,316 @@
+/* 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 { debounce } = require("sdk/lang/functional");
+const { lerp } = require("devtools/client/memory/utils");
+const EventEmitter = require("devtools/shared/event-emitter");
+
+const LERP_SPEED = 0.5;
+const ZOOM_SPEED = 0.01;
+const TRANSLATE_EPSILON = 1;
+const ZOOM_EPSILON = 0.001;
+const LINE_SCROLL_MODE = 1;
+const SCROLL_LINE_SIZE = 15;
+
+/**
+ * DragZoom is a constructor that contains the state of the current dragging and
+ * zooming behavior. It sets the scrolling and zooming behaviors.
+ *
+ * @param {HTMLElement} container description
+ * The container for the canvases
+ */
+function DragZoom(container, debounceRate, requestAnimationFrame) {
+ EventEmitter.decorate(this);
+
+ this.isDragging = false;
+
+ // The current mouse position
+ this.mouseX = container.offsetWidth / 2;
+ this.mouseY = container.offsetHeight / 2;
+
+ // The total size of the visualization after being zoomed, in pixels
+ this.zoomedWidth = container.offsetWidth;
+ this.zoomedHeight = container.offsetHeight;
+
+ // How much the visualization has been zoomed in
+ this.zoom = 0;
+
+ // The offset of visualization from the container. This is applied after
+ // the zoom, and the visualization by default is centered
+ this.translateX = 0;
+ this.translateY = 0;
+
+ // The size of the offset between the top/left of the container, and the
+ // top/left of the containing element. This value takes into account
+ // the devicePixelRatio for canvas draws.
+ this.offsetX = 0;
+ this.offsetY = 0;
+
+ // The smoothed values that are animated and eventually match the target
+ // values. The values are updated by the update loop
+ this.smoothZoom = 0;
+ this.smoothTranslateX = 0;
+ this.smoothTranslateY = 0;
+
+ // Add the constant values for testing purposes
+ this.ZOOM_SPEED = ZOOM_SPEED;
+ this.ZOOM_EPSILON = ZOOM_EPSILON;
+
+ let update = createUpdateLoop(container, this, requestAnimationFrame);
+
+ this.destroy = setHandlers(this, container, update, debounceRate);
+}
+
+module.exports = DragZoom;
+
+/**
+ * Returns an update loop. This loop smoothly updates the visualization when
+ * actions are performed. Once the animations have reached their target values
+ * the animation loop is stopped.
+ *
+ * Any value in the `dragZoom` object that starts with "smooth" is the
+ * smoothed version of a value that is interpolating toward the target value.
+ * For instance `dragZoom.smoothZoom` approaches `dragZoom.zoom` on each
+ * iteration of the update loop until it's sufficiently close as defined by
+ * the epsilon values.
+ *
+ * Only these smoothed values and the container CSS are updated by the loop.
+ *
+ * @param {HTMLDivElement} container
+ * @param {Object} dragZoom
+ * The values that represent the current dragZoom state
+ * @param {Function} requestAnimationFrame
+ */
+function createUpdateLoop(container, dragZoom, requestAnimationFrame) {
+ let isLooping = false;
+
+ function update() {
+ let isScrollChanging = (
+ Math.abs(dragZoom.smoothZoom - dragZoom.zoom) > ZOOM_EPSILON
+ );
+ let isTranslateChanging = (
+ Math.abs(dragZoom.smoothTranslateX - dragZoom.translateX)
+ > TRANSLATE_EPSILON ||
+ Math.abs(dragZoom.smoothTranslateY - dragZoom.translateY)
+ > TRANSLATE_EPSILON
+ );
+
+ isLooping = isScrollChanging || isTranslateChanging;
+
+ if (isScrollChanging) {
+ dragZoom.smoothZoom = lerp(dragZoom.smoothZoom, dragZoom.zoom,
+ LERP_SPEED);
+ } else {
+ dragZoom.smoothZoom = dragZoom.zoom;
+ }
+
+ if (isTranslateChanging) {
+ dragZoom.smoothTranslateX = lerp(dragZoom.smoothTranslateX,
+ dragZoom.translateX, LERP_SPEED);
+ dragZoom.smoothTranslateY = lerp(dragZoom.smoothTranslateY,
+ dragZoom.translateY, LERP_SPEED);
+ } else {
+ dragZoom.smoothTranslateX = dragZoom.translateX;
+ dragZoom.smoothTranslateY = dragZoom.translateY;
+ }
+
+ let zoom = 1 + dragZoom.smoothZoom;
+ let x = dragZoom.smoothTranslateX;
+ let y = dragZoom.smoothTranslateY;
+ container.style.transform = `translate(${x}px, ${y}px) scale(${zoom})`;
+
+ if (isLooping) {
+ requestAnimationFrame(update);
+ }
+ }
+
+ // Go ahead and start the update loop
+ update();
+
+ return function restartLoopingIfStopped() {
+ if (!isLooping) {
+ update();
+ }
+ };
+}
+
+/**
+ * Set the various event listeners and return a function to remove them
+ *
+ * @param {Object} dragZoom
+ * @param {HTMLElement} container
+ * @param {Function} update
+ * @return {Function} The function to remove the handlers
+ */
+function setHandlers(dragZoom, container, update, debounceRate) {
+ let emitChanged = debounce(() => dragZoom.emit("change"), debounceRate);
+
+ let removeDragHandlers =
+ setDragHandlers(container, dragZoom, emitChanged, update);
+ let removeScrollHandlers =
+ setScrollHandlers(container, dragZoom, emitChanged, update);
+
+ return function removeHandlers() {
+ removeDragHandlers();
+ removeScrollHandlers();
+ };
+}
+
+/**
+ * Sets handlers for when the user drags on the canvas. It will update dragZoom
+ * object with new translate and offset values.
+ *
+ * @param {HTMLElement} container
+ * @param {Object} dragZoom
+ * @param {Function} changed
+ * @param {Function} update
+ */
+function setDragHandlers(container, dragZoom, emitChanged, update) {
+ let parentEl = container.parentElement;
+
+ function startDrag() {
+ dragZoom.isDragging = true;
+ container.style.cursor = "grabbing";
+ }
+
+ function stopDrag() {
+ dragZoom.isDragging = false;
+ container.style.cursor = "grab";
+ }
+
+ function drag(event) {
+ let prevMouseX = dragZoom.mouseX;
+ let prevMouseY = dragZoom.mouseY;
+
+ dragZoom.mouseX = event.clientX - parentEl.offsetLeft;
+ dragZoom.mouseY = event.clientY - parentEl.offsetTop;
+
+ if (!dragZoom.isDragging) {
+ return;
+ }
+
+ dragZoom.translateX += dragZoom.mouseX - prevMouseX;
+ dragZoom.translateY += dragZoom.mouseY - prevMouseY;
+
+ keepInView(container, dragZoom);
+
+ emitChanged();
+ update();
+ }
+
+ parentEl.addEventListener("mousedown", startDrag, false);
+ parentEl.addEventListener("mouseup", stopDrag, false);
+ parentEl.addEventListener("mouseout", stopDrag, false);
+ parentEl.addEventListener("mousemove", drag, false);
+
+ return function removeListeners() {
+ parentEl.removeEventListener("mousedown", startDrag, false);
+ parentEl.removeEventListener("mouseup", stopDrag, false);
+ parentEl.removeEventListener("mouseout", stopDrag, false);
+ parentEl.removeEventListener("mousemove", drag, false);
+ };
+}
+
+/**
+ * Sets the handlers for when the user scrolls. It updates the dragZoom object
+ * and keeps the canvases all within the view. After changing values update
+ * loop is called, and the changed event is emitted.
+ *
+ * @param {HTMLDivElement} container
+ * @param {Object} dragZoom
+ * @param {Function} changed
+ * @param {Function} update
+ */
+function setScrollHandlers(container, dragZoom, emitChanged, update) {
+ let window = container.ownerDocument.defaultView;
+
+ function handleWheel(event) {
+ event.preventDefault();
+
+ if (dragZoom.isDragging) {
+ return;
+ }
+
+ // Update the zoom level
+ let scrollDelta = getScrollDelta(event, window);
+ let prevZoom = dragZoom.zoom;
+ dragZoom.zoom = Math.max(0, dragZoom.zoom - scrollDelta * ZOOM_SPEED);
+ let deltaZoom = dragZoom.zoom - prevZoom;
+
+ // Calculate the updated width and height
+ let prevZoomedWidth = container.offsetWidth * (1 + prevZoom);
+ let prevZoomedHeight = container.offsetHeight * (1 + prevZoom);
+ dragZoom.zoomedWidth = container.offsetWidth * (1 + dragZoom.zoom);
+ dragZoom.zoomedHeight = container.offsetHeight * (1 + dragZoom.zoom);
+ let deltaWidth = dragZoom.zoomedWidth - prevZoomedWidth;
+ let deltaHeight = dragZoom.zoomedHeight - prevZoomedHeight;
+
+ let mouseOffsetX = dragZoom.mouseX - container.offsetWidth / 2;
+ let mouseOffsetY = dragZoom.mouseY - container.offsetHeight / 2;
+
+ // The ratio of where the center of the mouse is in regards to the total
+ // zoomed width/height
+ let ratioZoomX = (prevZoomedWidth / 2 + mouseOffsetX - dragZoom.translateX)
+ / prevZoomedWidth;
+ let ratioZoomY = (prevZoomedHeight / 2 + mouseOffsetY - dragZoom.translateY)
+ / prevZoomedHeight;
+
+ // Distribute the change in width and height based on the above ratio
+ dragZoom.translateX -= lerp(-deltaWidth / 2, deltaWidth / 2, ratioZoomX);
+ dragZoom.translateY -= lerp(-deltaHeight / 2, deltaHeight / 2, ratioZoomY);
+
+ // Keep the canvas in range of the container
+ keepInView(container, dragZoom);
+ emitChanged();
+ update();
+ }
+
+ container.addEventListener("wheel", handleWheel, false);
+
+ return function removeListener() {
+ container.removeEventListener("wheel", handleWheel, false);
+ };
+}
+
+/**
+ * Account for the various mouse wheel event types, per pixel or per line
+ *
+ * @param {WheelEvent} event
+ * @param {Window} window
+ * @return {Number} The scroll size in pixels
+ */
+function getScrollDelta(event, window) {
+ if (event.deltaMode === LINE_SCROLL_MODE) {
+ // Update by a fixed arbitrary value to normalize scroll types
+ return event.deltaY * SCROLL_LINE_SIZE;
+ }
+ return event.deltaY;
+}
+
+/**
+ * Keep the dragging and zooming within the view by updating the values in the
+ * `dragZoom` object.
+ *
+ * @param {HTMLDivElement} container
+ * @param {Object} dragZoom
+ */
+function keepInView(container, dragZoom) {
+ let { devicePixelRatio } = container.ownerDocument.defaultView;
+ let overdrawX = (dragZoom.zoomedWidth - container.offsetWidth) / 2;
+ let overdrawY = (dragZoom.zoomedHeight - container.offsetHeight) / 2;
+
+ dragZoom.translateX = Math.max(-overdrawX,
+ Math.min(overdrawX, dragZoom.translateX));
+ dragZoom.translateY = Math.max(-overdrawY,
+ Math.min(overdrawY, dragZoom.translateY));
+
+ dragZoom.offsetX = devicePixelRatio * (
+ (dragZoom.zoomedWidth - container.offsetWidth) / 2 - dragZoom.translateX
+ );
+ dragZoom.offsetY = devicePixelRatio * (
+ (dragZoom.zoomedHeight - container.offsetHeight) / 2 - dragZoom.translateY
+ );
+}
diff --git a/devtools/client/memory/components/tree-map/draw.js b/devtools/client/memory/components/tree-map/draw.js
new file mode 100644
index 000000000..a55c1eae6
--- /dev/null
+++ b/devtools/client/memory/components/tree-map/draw.js
@@ -0,0 +1,295 @@
+/* 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";
+/**
+ * Draw the treemap into the provided canvases using the 2d context. The treemap
+ * layout is computed with d3. There are 2 canvases provided, each matching
+ * the resolution of the window. The main canvas is a fully drawn version of
+ * the treemap that is positioned and zoomed using css. It gets blurry the more
+ * you zoom in as it doesn't get redrawn when zooming. The zoom canvas is
+ * repositioned absolutely after every change in the dragZoom object, and then
+ * redrawn to provide a full-resolution (non-blurry) view of zoomed in segment
+ * of the treemap.
+ */
+
+const colorCoarseType = require("./color-coarse-type");
+const {
+ hslToStyle,
+ formatAbbreviatedBytes,
+ L10N
+} = require("devtools/client/memory/utils");
+
+// A constant fully zoomed out dragZoom object for the main canvas
+const NO_SCROLL = {
+ translateX: 0,
+ translateY: 0,
+ zoom: 0,
+ offsetX: 0,
+ offsetY: 0
+};
+
+// Drawing constants
+const ELLIPSIS = "...";
+const TEXT_MARGIN = 2;
+const TEXT_COLOR = "#000000";
+const TEXT_LIGHT_COLOR = "rgba(0,0,0,0.5)";
+const LINE_WIDTH = 1;
+const FONT_SIZE = 10;
+const FONT_LINE_HEIGHT = 2;
+const PADDING = [5 + FONT_SIZE, 5, 5, 5];
+const COUNT_LABEL = L10N.getStr("tree-map.node-count");
+
+/**
+ * Setup and start drawing the treemap visualization
+ *
+ * @param {Object} report
+ * @param {Object} canvases
+ * A CanvasUtils object that contains references to the main and zoom
+ * canvases and contexts
+ * @param {Object} dragZoom
+ * A DragZoom object representing the current state of the dragging
+ * and zooming behavior
+ */
+exports.setupDraw = function (report, canvases, dragZoom) {
+ let getTreemap = configureD3Treemap.bind(null, canvases.main.canvas);
+
+ let treemap, nodes;
+
+ function drawFullTreemap() {
+ treemap = getTreemap();
+ nodes = treemap(report);
+ drawTreemap(canvases.main, nodes, NO_SCROLL);
+ drawTreemap(canvases.zoom, nodes, dragZoom);
+ }
+
+ function drawZoomedTreemap() {
+ drawTreemap(canvases.zoom, nodes, dragZoom);
+ positionZoomedCanvas(canvases.zoom.canvas, dragZoom);
+ }
+
+ drawFullTreemap();
+ canvases.on("resize", drawFullTreemap);
+ dragZoom.on("change", drawZoomedTreemap);
+};
+
+/**
+ * Returns a configured d3 treemap function
+ *
+ * @param {HTMLCanvasElement} canvas
+ * @return {Function}
+ */
+const configureD3Treemap = exports.configureD3Treemap = function (canvas) {
+ let window = canvas.ownerDocument.defaultView;
+ let ratio = window.devicePixelRatio;
+ let treemap = window.d3.layout.treemap()
+ .size([
+ // The d3 layout includes the padding around everything, add some
+ // extra padding to the size to compensate for thi
+ canvas.width + (PADDING[1] + PADDING[3]) * ratio,
+ canvas.height + (PADDING[0] + PADDING[2]) * ratio
+ ])
+ .sticky(true)
+ .padding([
+ PADDING[0] * ratio,
+ PADDING[1] * ratio,
+ PADDING[2] * ratio,
+ PADDING[3] * ratio,
+ ])
+ .value(d => d.bytes);
+
+ /**
+ * Create treemap nodes from a census report that are sorted by depth
+ *
+ * @param {Object} report
+ * @return {Array} An array of d3 treemap nodes
+ * // https://github.com/mbostock/d3/wiki/Treemap-Layout
+ * parent - the parent node, or null for the root.
+ * children - the array of child nodes, or null for leaf nodes.
+ * value - the node value, as returned by the value accessor.
+ * depth - the depth of the node, starting at 0 for the root.
+ * area - the computed pixel area of this node.
+ * x - the minimum x-coordinate of the node position.
+ * y - the minimum y-coordinate of the node position.
+ * z - the orientation of this cell’s subdivision, if any.
+ * dx - the x-extent of the node position.
+ * dy - the y-extent of the node position.
+ */
+ return function depthSortedNodes(report) {
+ let nodes = treemap(report);
+ nodes.sort((a, b) => a.depth - b.depth);
+ return nodes;
+ };
+};
+
+/**
+ * Draw the text, cut it in half every time it doesn't fit until it fits or
+ * it's smaller than the "..." text.
+ *
+ * @param {CanvasRenderingContext2D} ctx
+ * @param {Number} x
+ * the position of the text
+ * @param {Number} y
+ * the position of the text
+ * @param {Number} innerWidth
+ * the inner width of the containing treemap cell
+ * @param {Text} name
+ */
+const drawTruncatedName = exports.drawTruncatedName = function (ctx, x, y,
+ innerWidth,
+ name) {
+ let truncated = name.substr(0, Math.floor(name.length / 2));
+ let formatted = truncated + ELLIPSIS;
+
+ if (ctx.measureText(formatted).width > innerWidth) {
+ drawTruncatedName(ctx, x, y, innerWidth, truncated);
+ } else {
+ ctx.fillText(formatted, x, y);
+ }
+};
+
+/**
+ * Fit and draw the text in a node with the following strategies to shrink
+ * down the text size:
+ *
+ * Function 608KB 9083 count
+ * Function
+ * Func...
+ * Fu...
+ * ...
+ *
+ * @param {CanvasRenderingContext2D} ctx
+ * @param {Object} node
+ * @param {Number} borderWidth
+ * @param {Object} dragZoom
+ * @param {Array} padding
+ */
+const drawText = exports.drawText = function (ctx, node, borderWidth, ratio,
+ dragZoom, padding) {
+ let { dx, dy, name, totalBytes, totalCount } = node;
+ let scale = dragZoom.zoom + 1;
+ dx *= scale;
+ dy *= scale;
+
+ // Start checking to see how much text we can fit in, optimizing for the
+ // common case of lots of small leaf nodes
+ if (FONT_SIZE * FONT_LINE_HEIGHT < dy) {
+ let margin = borderWidth(node) * 1.5 + ratio * TEXT_MARGIN;
+ let x = margin + (node.x - padding[0]) * scale - dragZoom.offsetX;
+ let y = margin + (node.y - padding[1]) * scale - dragZoom.offsetY;
+ let innerWidth = dx - margin * 2;
+ let nameSize = ctx.measureText(name).width;
+
+ if (ctx.measureText(ELLIPSIS).width > innerWidth) {
+ return;
+ }
+
+ ctx.fillStyle = TEXT_COLOR;
+
+ if (nameSize > innerWidth) {
+ // The name is too long - halve the name as an expediant way to shorten it
+ drawTruncatedName(ctx, x, y, innerWidth, name);
+ } else {
+ let bytesFormatted = formatAbbreviatedBytes(totalBytes);
+ let countFormatted = `${totalCount} ${COUNT_LABEL}`;
+ let byteSize = ctx.measureText(bytesFormatted).width;
+ let countSize = ctx.measureText(countFormatted).width;
+ let spaceSize = ctx.measureText(" ").width;
+
+ if (nameSize + byteSize + countSize + spaceSize * 3 > innerWidth) {
+ // The full name will fit
+ ctx.fillText(`${name}`, x, y);
+ } else {
+ // The full name plus the byte information will fit
+ ctx.fillText(name, x, y);
+ ctx.fillStyle = TEXT_LIGHT_COLOR;
+ ctx.fillText(`${bytesFormatted} ${countFormatted}`,
+ x + nameSize + spaceSize, y);
+ }
+ }
+ }
+};
+
+/**
+ * Draw a box given a node
+ *
+ * @param {CanvasRenderingContext2D} ctx
+ * @param {Object} node
+ * @param {Number} borderWidth
+ * @param {Number} ratio
+ * @param {Object} dragZoom
+ * @param {Array} padding
+ */
+const drawBox = exports.drawBox = function (ctx, node, borderWidth, dragZoom,
+ padding) {
+ let border = borderWidth(node);
+ let fillHSL = colorCoarseType(node);
+ let strokeHSL = [fillHSL[0], fillHSL[1], fillHSL[2] * 0.5];
+ let scale = 1 + dragZoom.zoom;
+
+ // Offset the draw so that box strokes don't overlap
+ let x = scale * (node.x - padding[0]) - dragZoom.offsetX + border / 2;
+ let y = scale * (node.y - padding[1]) - dragZoom.offsetY + border / 2;
+ let dx = scale * node.dx - border;
+ let dy = scale * node.dy - border;
+
+ ctx.fillStyle = hslToStyle(...fillHSL);
+ ctx.fillRect(x, y, dx, dy);
+
+ ctx.strokeStyle = hslToStyle(...strokeHSL);
+ ctx.lineWidth = border;
+ ctx.strokeRect(x, y, dx, dy);
+};
+
+/**
+ * Draw the overall treemap
+ *
+ * @param {HTMLCanvasElement} canvas
+ * @param {CanvasRenderingContext2D} ctx
+ * @param {Array} nodes
+ * @param {Objbect} dragZoom
+ */
+const drawTreemap = exports.drawTreemap = function ({canvas, ctx}, nodes,
+ dragZoom) {
+ let window = canvas.ownerDocument.defaultView;
+ let ratio = window.devicePixelRatio;
+ let canvasArea = canvas.width * canvas.height;
+ // Subtract the outer padding from the tree map layout.
+ let padding = [PADDING[3] * ratio, PADDING[0] * ratio];
+
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ ctx.font = `${FONT_SIZE * ratio}px sans-serif`;
+ ctx.textBaseline = "top";
+
+ function borderWidth(node) {
+ let areaRatio = Math.sqrt(node.area / canvasArea);
+ return ratio * Math.max(1, LINE_WIDTH * areaRatio);
+ }
+
+ for (let i = 0; i < nodes.length; i++) {
+ let node = nodes[i];
+ if (node.parent === undefined) {
+ continue;
+ }
+
+ drawBox(ctx, node, borderWidth, dragZoom, padding);
+ drawText(ctx, node, borderWidth, ratio, dragZoom, padding);
+ }
+};
+
+/**
+ * Set the position of the zoomed in canvas. It always take up 100% of the view
+ * window, but is transformed relative to the zoomed in containing element,
+ * essentially reversing the transform of the containing element.
+ *
+ * @param {HTMLCanvasElement} canvas
+ * @param {Object} dragZoom
+ */
+const positionZoomedCanvas = function (canvas, dragZoom) {
+ let scale = 1 / (1 + dragZoom.zoom);
+ let x = -dragZoom.translateX;
+ let y = -dragZoom.translateY;
+ canvas.style.transform = `scale(${scale}) translate(${x}px, ${y}px)`;
+};
+
+exports.positionZoomedCanvas = positionZoomedCanvas;
diff --git a/devtools/client/memory/components/tree-map/moz.build b/devtools/client/memory/components/tree-map/moz.build
new file mode 100644
index 000000000..aab193191
--- /dev/null
+++ b/devtools/client/memory/components/tree-map/moz.build
@@ -0,0 +1,12 @@
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'canvas-utils.js',
+ 'color-coarse-type.js',
+ 'drag-zoom.js',
+ 'draw.js',
+ 'start.js',
+)
diff --git a/devtools/client/memory/components/tree-map/start.js b/devtools/client/memory/components/tree-map/start.js
new file mode 100644
index 000000000..9cebe2a9d
--- /dev/null
+++ b/devtools/client/memory/components/tree-map/start.js
@@ -0,0 +1,32 @@
+/* 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 { setupDraw } = require("./draw");
+const DragZoom = require("./drag-zoom");
+const CanvasUtils = require("./canvas-utils");
+
+/**
+ * Start the tree map visualization
+ *
+ * @param {HTMLDivElement} container
+ * @param {Object} report
+ * the report from a census
+ * @param {Number} debounceRate
+ */
+module.exports = function startVisualization(parentEl, report,
+ debounceRate = 60) {
+ let window = parentEl.ownerDocument.defaultView;
+ let canvases = new CanvasUtils(parentEl, debounceRate);
+ let dragZoom = new DragZoom(canvases.container, debounceRate,
+ window.requestAnimationFrame);
+
+ setupDraw(report, canvases, dragZoom);
+
+ return function stopVisualization() {
+ canvases.destroy();
+ dragZoom.destroy();
+ };
+};
diff --git a/devtools/client/memory/constants.js b/devtools/client/memory/constants.js
new file mode 100644
index 000000000..b8e382c73
--- /dev/null
+++ b/devtools/client/memory/constants.js
@@ -0,0 +1,342 @@
+/* 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";
+
+// Options passed to MemoryFront's startRecordingAllocations never change.
+exports.ALLOCATION_RECORDING_OPTIONS = {
+ probability: 1,
+ maxLogLength: 1
+};
+
+// If TREE_ROW_HEIGHT changes, be sure to change `var(--heap-tree-row-height)`
+// in `devtools/client/themes/memory.css`
+exports.TREE_ROW_HEIGHT = 18;
+
+/** * Actions ******************************************************************/
+
+const actions = exports.actions = {};
+
+// Fired by UI to request a snapshot from the actor.
+actions.TAKE_SNAPSHOT_START = "take-snapshot-start";
+actions.TAKE_SNAPSHOT_END = "take-snapshot-end";
+
+// When a heap snapshot is read into memory -- only fired
+// once per snapshot.
+actions.READ_SNAPSHOT_START = "read-snapshot-start";
+actions.READ_SNAPSHOT_END = "read-snapshot-end";
+
+// When a census is being performed on a heap snapshot
+actions.TAKE_CENSUS_START = "take-census-start";
+actions.TAKE_CENSUS_END = "take-census-end";
+actions.TAKE_CENSUS_ERROR = "take-census-error";
+
+// When a tree map is being calculated on a heap snapshot
+actions.TAKE_TREE_MAP_START = "take-tree-map-start";
+actions.TAKE_TREE_MAP_END = "take-tree-map-end";
+actions.TAKE_TREE_MAP_ERROR = "take-tree-map-error";
+
+// When requesting that the server start/stop recording allocation stacks.
+actions.TOGGLE_RECORD_ALLOCATION_STACKS_START = "toggle-record-allocation-stacks-start";
+actions.TOGGLE_RECORD_ALLOCATION_STACKS_END = "toggle-record-allocation-stacks-end";
+
+// When a heap snapshot is being saved to a user-specified
+// location on disk.
+actions.EXPORT_SNAPSHOT_START = "export-snapshot-start";
+actions.EXPORT_SNAPSHOT_END = "export-snapshot-end";
+actions.EXPORT_SNAPSHOT_ERROR = "export-snapshot-error";
+
+// When a heap snapshot is being read from a user selected file,
+// and represents the entire state until the census is available.
+actions.IMPORT_SNAPSHOT_START = "import-snapshot-start";
+actions.IMPORT_SNAPSHOT_END = "import-snapshot-end";
+actions.IMPORT_SNAPSHOT_ERROR = "import-snapshot-error";
+
+// Fired by UI to select a snapshot to view.
+actions.SELECT_SNAPSHOT = "select-snapshot";
+
+// Fired to delete a provided list of snapshots
+actions.DELETE_SNAPSHOTS_START = "delete-snapshots-start";
+actions.DELETE_SNAPSHOTS_END = "delete-snapshots-end";
+
+// Fired to toggle tree inversion on or off.
+actions.TOGGLE_INVERTED = "toggle-inverted";
+
+// Fired when a snapshot is selected for diffing.
+actions.SELECT_SNAPSHOT_FOR_DIFFING = "select-snapshot-for-diffing";
+
+// Fired when taking a census diff.
+actions.TAKE_CENSUS_DIFF_START = "take-census-diff-start";
+actions.TAKE_CENSUS_DIFF_END = "take-census-diff-end";
+actions.DIFFING_ERROR = "diffing-error";
+
+// Fired to set a new census display.
+actions.SET_CENSUS_DISPLAY = "set-census-display";
+
+// Fired to change the display that controls the dominator tree labels.
+actions.SET_LABEL_DISPLAY = "set-label-display";
+
+// Fired to set a tree map display
+actions.SET_TREE_MAP_DISPLAY = "set-tree-map-display";
+
+// Fired when changing between census or dominators view.
+actions.CHANGE_VIEW = "change-view";
+actions.POP_VIEW = "pop-view";
+
+// Fired when there is an error processing a snapshot or taking a census.
+actions.SNAPSHOT_ERROR = "snapshot-error";
+
+// Fired when there is a new filter string set.
+actions.SET_FILTER_STRING = "set-filter-string";
+
+// Fired to expand or collapse nodes in census reports.
+actions.EXPAND_CENSUS_NODE = "expand-census-node";
+actions.EXPAND_DIFFING_CENSUS_NODE = "expand-diffing-census-node";
+actions.COLLAPSE_CENSUS_NODE = "collapse-census-node";
+actions.COLLAPSE_DIFFING_CENSUS_NODE = "collapse-diffing-census-node";
+
+// Fired when nodes in various trees are focused.
+actions.FOCUS_CENSUS_NODE = "focus-census-node";
+actions.FOCUS_DIFFING_CENSUS_NODE = "focus-diffing-census-node";
+actions.FOCUS_DOMINATOR_TREE_NODE = "focus-dominator-tree-node";
+
+actions.FOCUS_INDIVIDUAL = "focus-individual";
+actions.FETCH_INDIVIDUALS_START = "fetch-individuals-start";
+actions.FETCH_INDIVIDUALS_END = "fetch-individuals-end";
+actions.INDIVIDUALS_ERROR = "individuals-error";
+
+actions.COMPUTE_DOMINATOR_TREE_START = "compute-dominator-tree-start";
+actions.COMPUTE_DOMINATOR_TREE_END = "compute-dominator-tree-end";
+actions.FETCH_DOMINATOR_TREE_START = "fetch-dominator-tree-start";
+actions.FETCH_DOMINATOR_TREE_END = "fetch-dominator-tree-end";
+actions.DOMINATOR_TREE_ERROR = "dominator-tree-error";
+actions.FETCH_IMMEDIATELY_DOMINATED_START = "fetch-immediately-dominated-start";
+actions.FETCH_IMMEDIATELY_DOMINATED_END = "fetch-immediately-dominated-end";
+actions.EXPAND_DOMINATOR_TREE_NODE = "expand-dominator-tree-node";
+actions.COLLAPSE_DOMINATOR_TREE_NODE = "collapse-dominator-tree-node";
+
+actions.RESIZE_SHORTEST_PATHS = "resize-shortest-paths";
+
+/** * Census Displays ***************************************************************/
+
+const COUNT = Object.freeze({ by: "count", count: true, bytes: true });
+const INTERNAL_TYPE = Object.freeze({ by: "internalType", then: COUNT });
+const ALLOCATION_STACK = Object.freeze({ by: "allocationStack", then: COUNT, noStack: COUNT });
+const OBJECT_CLASS = Object.freeze({ by: "objectClass", then: COUNT, other: COUNT });
+const COARSE_TYPE = Object.freeze({
+ by: "coarseType",
+ objects: OBJECT_CLASS,
+ strings: COUNT,
+ scripts: {
+ by: "filename",
+ then: INTERNAL_TYPE,
+ noFilename: INTERNAL_TYPE
+ },
+ other: INTERNAL_TYPE,
+});
+
+exports.censusDisplays = Object.freeze({
+ coarseType: Object.freeze({
+ displayName: "Type",
+ get tooltip() {
+ // Importing down here is necessary because of the circular dependency
+ // this introduces with `./utils.js`.
+ const { L10N } = require("./utils");
+ return L10N.getStr("censusDisplays.coarseType.tooltip");
+ },
+ inverted: true,
+ breakdown: COARSE_TYPE
+ }),
+
+ allocationStack: Object.freeze({
+ displayName: "Call Stack",
+ get tooltip() {
+ const { L10N } = require("./utils");
+ return L10N.getStr("censusDisplays.allocationStack.tooltip");
+ },
+ inverted: false,
+ breakdown: ALLOCATION_STACK,
+ }),
+
+ invertedAllocationStack: Object.freeze({
+ displayName: "Inverted Call Stack",
+ get tooltip() {
+ const { L10N } = require("./utils");
+ return L10N.getStr("censusDisplays.invertedAllocationStack.tooltip");
+ },
+ inverted: true,
+ breakdown: ALLOCATION_STACK,
+ }),
+});
+
+const DOMINATOR_TREE_LABEL_COARSE_TYPE = Object.freeze({
+ by: "coarseType",
+ objects: OBJECT_CLASS,
+ scripts: Object.freeze({
+ by: "internalType",
+ then: Object.freeze({
+ by: "filename",
+ then: COUNT,
+ noFilename: COUNT,
+ }),
+ }),
+ strings: INTERNAL_TYPE,
+ other: INTERNAL_TYPE,
+});
+
+exports.labelDisplays = Object.freeze({
+ coarseType: Object.freeze({
+ displayName: "Type",
+ get tooltip() {
+ const { L10N } = require("./utils");
+ return L10N.getStr("dominatorTreeDisplays.coarseType.tooltip");
+ },
+ breakdown: DOMINATOR_TREE_LABEL_COARSE_TYPE
+ }),
+
+ allocationStack: Object.freeze({
+ displayName: "Call Stack",
+ get tooltip() {
+ const { L10N } = require("./utils");
+ return L10N.getStr("dominatorTreeDisplays.allocationStack.tooltip");
+ },
+ breakdown: Object.freeze({
+ by: "allocationStack",
+ then: DOMINATOR_TREE_LABEL_COARSE_TYPE,
+ noStack: DOMINATOR_TREE_LABEL_COARSE_TYPE,
+ }),
+ }),
+});
+
+exports.treeMapDisplays = Object.freeze({
+ coarseType: Object.freeze({
+ displayName: "Type",
+ get tooltip() {
+ const { L10N } = require("./utils");
+ return L10N.getStr("treeMapDisplays.coarseType.tooltip");
+ },
+ breakdown: COARSE_TYPE,
+ inverted: false,
+ })
+});
+
+/** * View States **************************************************************/
+
+/**
+ * The various main views that the tool can be in.
+ */
+const viewState = exports.viewState = Object.create(null);
+viewState.CENSUS = "view-state-census";
+viewState.DIFFING = "view-state-diffing";
+viewState.DOMINATOR_TREE = "view-state-dominator-tree";
+viewState.TREE_MAP = "view-state-tree-map";
+viewState.INDIVIDUALS = "view-state-individuals";
+
+/** * Snapshot States **********************************************************/
+
+const snapshotState = exports.snapshotState = Object.create(null);
+
+/**
+ * Various states a snapshot can be in.
+ * An FSM describing snapshot states:
+ *
+ * SAVING -> SAVED -> READING -> READ
+ * ↗
+ * IMPORTING
+ *
+ * Any of these states may go to the ERROR state, from which they can never
+ * leave (mwah ha ha ha!)
+ */
+snapshotState.ERROR = "snapshot-state-error";
+snapshotState.IMPORTING = "snapshot-state-importing";
+snapshotState.SAVING = "snapshot-state-saving";
+snapshotState.SAVED = "snapshot-state-saved";
+snapshotState.READING = "snapshot-state-reading";
+snapshotState.READ = "snapshot-state-read";
+
+/*
+ * Various states the census model can be in.
+ *
+ * SAVING <-> SAVED
+ * |
+ * V
+ * ERROR
+ */
+
+const censusState = exports.censusState = Object.create(null);
+
+censusState.SAVING = "census-state-saving";
+censusState.SAVED = "census-state-saved";
+censusState.ERROR = "census-state-error";
+
+/*
+ * Various states the tree map model can be in.
+ *
+ * SAVING <-> SAVED
+ * |
+ * V
+ * ERROR
+ */
+
+const treeMapState = exports.treeMapState = Object.create(null);
+
+treeMapState.SAVING = "tree-map-state-saving";
+treeMapState.SAVED = "tree-map-state-saved";
+treeMapState.ERROR = "tree-map-state-error";
+
+/** * Diffing States ***********************************************************/
+
+/*
+ * Various states the diffing model can be in.
+ *
+ * SELECTING --> TAKING_DIFF <---> TOOK_DIFF
+ * |
+ * V
+ * ERROR
+ */
+const diffingState = exports.diffingState = Object.create(null);
+
+// Selecting the two snapshots to diff.
+diffingState.SELECTING = "diffing-state-selecting";
+
+// Currently computing the diff between the two selected snapshots.
+diffingState.TAKING_DIFF = "diffing-state-taking-diff";
+
+// Have the diff between the two selected snapshots.
+diffingState.TOOK_DIFF = "diffing-state-took-diff";
+
+// An error occurred while computing the diff.
+diffingState.ERROR = "diffing-state-error";
+
+/** * Dominator Tree States ****************************************************/
+
+/*
+ * Various states the dominator tree model can be in.
+ *
+ * COMPUTING -> COMPUTED -> FETCHING -> LOADED <--> INCREMENTAL_FETCHING
+ *
+ * Any state may lead to the ERROR state, from which it can never leave.
+ */
+const dominatorTreeState = exports.dominatorTreeState = Object.create(null);
+dominatorTreeState.COMPUTING = "dominator-tree-state-computing";
+dominatorTreeState.COMPUTED = "dominator-tree-state-computed";
+dominatorTreeState.FETCHING = "dominator-tree-state-fetching";
+dominatorTreeState.LOADED = "dominator-tree-state-loaded";
+dominatorTreeState.INCREMENTAL_FETCHING = "dominator-tree-state-incremental-fetching";
+dominatorTreeState.ERROR = "dominator-tree-state-error";
+
+/** * States for Individuals Model *********************************************/
+
+/*
+ * Various states the individuals model can be in.
+ *
+ * COMPUTING_DOMINATOR_TREE -> FETCHING -> FETCHED
+ *
+ * Any state may lead to the ERROR state, from which it can never leave.
+ */
+const individualsState = exports.individualsState = Object.create(null);
+individualsState.COMPUTING_DOMINATOR_TREE = "individuals-state-computing-dominator-tree";
+individualsState.FETCHING = "individuals-state-fetching";
+individualsState.FETCHED = "individuals-state-fetched";
+individualsState.ERROR = "individuals-state-error";
diff --git a/devtools/client/memory/dominator-tree-lazy-children.js b/devtools/client/memory/dominator-tree-lazy-children.js
new file mode 100644
index 000000000..85ae29a56
--- /dev/null
+++ b/devtools/client/memory/dominator-tree-lazy-children.js
@@ -0,0 +1,58 @@
+/* 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/. */
+
+/**
+ * The `DominatorTreeLazyChildren` is a placeholder that represents a future
+ * subtree in an existing `DominatorTreeNode` tree that is currently being
+ * incrementally fetched from the `HeapAnalysesWorker`.
+ *
+ * @param {NodeId} parentNodeId
+ * @param {Number} siblingIndex
+ */
+function DominatorTreeLazyChildren(parentNodeId, siblingIndex) {
+ this._parentNodeId = parentNodeId;
+ this._siblingIndex = siblingIndex;
+}
+
+/**
+ * Generate a unique key for this `DominatorTreeLazyChildren` instance. This can
+ * be used as the key in a hash table or as the `key` property for a React
+ * component, for example.
+ *
+ * @returns {String}
+ */
+DominatorTreeLazyChildren.prototype.key = function () {
+ return `dominator-tree-lazy-children-${this._parentNodeId}-${this._siblingIndex}`;
+};
+
+/**
+ * Return true if this is a placeholder for the first child of its
+ * parent. Return false if it is a placeholder for loading more of its parent's
+ * children.
+ *
+ * @returns {Boolean}
+ */
+DominatorTreeLazyChildren.prototype.isFirstChild = function () {
+ return this._siblingIndex === 0;
+};
+
+/**
+ * Get this subtree's parent node's identifier.
+ *
+ * @returns {NodeId}
+ */
+DominatorTreeLazyChildren.prototype.parentNodeId = function () {
+ return this._parentNodeId;
+};
+
+/**
+ * Get this subtree's index in its parent's children array.
+ *
+ * @returns {Number}
+ */
+DominatorTreeLazyChildren.prototype.siblingIndex = function () {
+ return this._siblingIndex;
+};
+
+module.exports = DominatorTreeLazyChildren;
diff --git a/devtools/client/memory/initializer.js b/devtools/client/memory/initializer.js
new file mode 100644
index 000000000..adb6e40c1
--- /dev/null
+++ b/devtools/client/memory/initializer.js
@@ -0,0 +1,67 @@
+/* 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 { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+const BrowserLoaderModule = {};
+Cu.import("resource://devtools/client/shared/browser-loader.js", BrowserLoaderModule);
+const { require } = BrowserLoaderModule.BrowserLoader({
+ baseURI: "resource://devtools/client/memory/",
+ window
+});
+const { Task } = require("devtools/shared/task");
+const { createFactory, createElement } = require("devtools/client/shared/vendor/react");
+const ReactDOM = require("devtools/client/shared/vendor/react-dom");
+const { Provider } = require("devtools/client/shared/vendor/react-redux");
+const App = createFactory(require("devtools/client/memory/app"));
+const Store = require("devtools/client/memory/store");
+const { assert } = require("devtools/shared/DevToolsUtils");
+
+/**
+ * The current target, toolbox, MemoryFront, and HeapAnalysesClient, set by this tool's host.
+ */
+var gToolbox, gTarget, gFront, gHeapAnalysesClient;
+
+/**
+ * Variables set by `initialize()`
+ */
+var gStore, gRoot, gApp, gProvider, unsubscribe, isHighlighted, telemetry;
+
+var initialize = Task.async(function* () {
+ gRoot = document.querySelector("#app");
+ gStore = Store();
+ gApp = createElement(App, { toolbox: gToolbox, front: gFront, heapWorker: gHeapAnalysesClient });
+ gProvider = createElement(Provider, { store: gStore }, gApp);
+ ReactDOM.render(gProvider, gRoot);
+ unsubscribe = gStore.subscribe(onStateChange);
+});
+
+var destroy = Task.async(function* () {
+ const ok = ReactDOM.unmountComponentAtNode(gRoot);
+ assert(ok, "Should successfully unmount the memory tool's top level React component");
+
+ unsubscribe();
+
+ gStore, gRoot, gApp, gProvider, unsubscribe, isHighlighted;
+});
+
+/**
+ * Fired on any state change, currently only handles toggling
+ * the highlighting of the tool when recording allocations.
+ */
+function onStateChange() {
+ let isRecording = gStore.getState().allocations.recording;
+ if (isRecording === isHighlighted) {
+ return;
+ }
+
+ if (isRecording) {
+ gToolbox.highlightTool("memory");
+ } else {
+ gToolbox.unhighlightTool("memory");
+ }
+
+ isHighlighted = isRecording;
+}
diff --git a/devtools/client/memory/memory.xhtml b/devtools/client/memory/memory.xhtml
new file mode 100644
index 000000000..c28c9e95a
--- /dev/null
+++ b/devtools/client/memory/memory.xhtml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html [
+ <!ENTITY % htmlDTD
+ PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "DTD/xhtml1-strict.dtd">
+ %htmlDTD;
+]>
+
+<!-- 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/. -->
+<html xmlns="http://www.w3.org/1999/xhtml" dir="">
+ <head>
+ <link rel="stylesheet" href="chrome://devtools/skin/widgets.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://devtools/skin/memory.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://devtools/skin/components-frame.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://devtools/skin/components-h-split-box.css" type="text/css"/>
+ </head>
+ <body class="theme-body">
+ <div id="app"></div>
+
+ <script type="application/javascript;version=1.8"
+ src="chrome://devtools/content/shared/theme-switching.js"
+ defer="true">
+ </script>
+
+ <script type="application/javascript;version=1.8"
+ src="initializer.js"
+ defer="true">
+ </script>
+
+ <script type="application/javascript"
+ src="chrome://devtools/content/shared/vendor/d3.js"
+ defer="true">
+ </script>
+
+ <script type="application/javascript"
+ src="chrome://devtools/content/shared/vendor/dagre-d3.js"
+ defer="true">
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/memory/models.js b/devtools/client/memory/models.js
new file mode 100644
index 000000000..7624f7acc
--- /dev/null
+++ b/devtools/client/memory/models.js
@@ -0,0 +1,519 @@
+/* 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/. */
+
+const { assert } = require("devtools/shared/DevToolsUtils");
+const { MemoryFront } = require("devtools/shared/fronts/memory");
+const HeapAnalysesClient = require("devtools/shared/heapsnapshot/HeapAnalysesClient");
+const { PropTypes } = require("devtools/client/shared/vendor/react");
+const {
+ snapshotState: states,
+ diffingState,
+ dominatorTreeState,
+ viewState,
+ individualsState,
+} = require("./constants");
+
+/**
+ * ONLY USE THIS FOR MODEL VALIDATORS IN CONJUCTION WITH assert()!
+ *
+ * React checks that the returned values from validator functions are instances
+ * of Error, but because React is loaded in its own global, that check is always
+ * false and always results in a warning.
+ *
+ * To work around this and still get model validation, just call assert() inside
+ * a function passed to catchAndIgnore. The assert() function will still report
+ * assertion failures, but this funciton will swallow the errors so that React
+ * doesn't go crazy and drown out the real error in irrelevant and incorrect
+ * warnings.
+ *
+ * Example usage:
+ *
+ * const MyModel = PropTypes.shape({
+ * someProperty: catchAndIgnore(function (model) {
+ * assert(someInvariant(model.someProperty), "Should blah blah");
+ * })
+ * });
+ */
+function catchAndIgnore(fn) {
+ return function (...args) {
+ try {
+ fn(...args);
+ } catch (err) { }
+
+ return null;
+ };
+}
+
+/**
+ * The data describing the census report's shape, and its associated metadata.
+ *
+ * @see `js/src/doc/Debugger/Debugger.Memory.md`
+ */
+const censusDisplayModel = exports.censusDisplay = PropTypes.shape({
+ displayName: PropTypes.string.isRequired,
+ tooltip: PropTypes.string.isRequired,
+ inverted: PropTypes.bool.isRequired,
+ breakdown: PropTypes.shape({
+ by: PropTypes.string.isRequired,
+ })
+});
+
+/**
+ * How we want to label nodes in the dominator tree, and associated
+ * metadata. The notable difference from `censusDisplayModel` is the lack of
+ * an `inverted` property.
+ *
+ * @see `js/src/doc/Debugger/Debugger.Memory.md`
+ */
+const labelDisplayModel = exports.labelDisplay = PropTypes.shape({
+ displayName: PropTypes.string.isRequired,
+ tooltip: PropTypes.string.isRequired,
+ breakdown: PropTypes.shape({
+ by: PropTypes.string.isRequired,
+ })
+});
+
+/**
+ * The data describing the tree map's shape, and its associated metadata.
+ *
+ * @see `js/src/doc/Debugger/Debugger.Memory.md`
+ */
+const treeMapDisplayModel = exports.treeMapDisplay = PropTypes.shape({
+ displayName: PropTypes.string.isRequired,
+ tooltip: PropTypes.string.isRequired,
+ inverted: PropTypes.bool.isRequired,
+ breakdown: PropTypes.shape({
+ by: PropTypes.string.isRequired,
+ })
+});
+
+/**
+ * Tree map model.
+ */
+const treeMapModel = exports.treeMapModel = PropTypes.shape({
+ // The current census report data.
+ report: PropTypes.object,
+ // The display data used to generate the current census.
+ display: treeMapDisplayModel,
+ // The current treeMapState this is in
+ state: catchAndIgnore(function (treeMap) {
+ switch (treeMap.state) {
+ case treeMapState.SAVING:
+ assert(!treeMap.report, "Should not have a report");
+ assert(!treeMap.error, "Should not have an error");
+ break;
+ case treeMapState.SAVED:
+ assert(treeMap.report, "Should have a report");
+ assert(!treeMap.error, "Should not have an error");
+ break;
+
+ case treeMapState.ERROR:
+ assert(treeMap.error, "Should have an error");
+ break;
+
+ default:
+ assert(false, `Unexpected treeMap state: ${treeMap.state}`);
+ }
+ })
+});
+
+let censusModel = exports.censusModel = PropTypes.shape({
+ // The current census report data.
+ report: PropTypes.object,
+ // The parent map for the report.
+ parentMap: PropTypes.object,
+ // The display data used to generate the current census.
+ display: censusDisplayModel,
+ // If present, the currently cached report's filter string used for pruning
+ // the tree items.
+ filter: PropTypes.string,
+ // The Immutable.Set<CensusTreeNode.id> of expanded node ids in the report
+ // tree.
+ expanded: catchAndIgnore(function (census) {
+ if (census.report) {
+ assert(census.expanded,
+ "If we have a report, we should also have the set of expanded nodes");
+ }
+ }),
+ // If a node is currently focused in the report tree, then this is it.
+ focused: PropTypes.object,
+ // The censusModelState that this census is currently in.
+ state: catchAndIgnore(function (census) {
+ switch (census.state) {
+ case censusState.SAVING:
+ assert(!census.report, "Should not have a report");
+ assert(!census.parentMap, "Should not have a parent map");
+ assert(census.expanded, "Should not have an expanded set");
+ assert(!census.error, "Should not have an error");
+ break;
+
+ case censusState.SAVED:
+ assert(census.report, "Should have a report");
+ assert(census.parentMap, "Should have a parent map");
+ assert(census.expanded, "Should have an expanded set");
+ assert(!census.error, "Should not have an error");
+ break;
+
+ case censusState.ERROR:
+ assert(!census.report, "Should not have a report");
+ assert(census.error, "Should have an error");
+ break;
+
+ default:
+ assert(false, `Unexpected census state: ${census.state}`);
+ }
+ })
+});
+
+/**
+ * Dominator tree model.
+ */
+let dominatorTreeModel = exports.dominatorTreeModel = PropTypes.shape({
+ // The id of this dominator tree.
+ dominatorTreeId: PropTypes.number,
+
+ // The root DominatorTreeNode of this dominator tree.
+ root: PropTypes.object,
+
+ // The Set<NodeId> of expanded nodes in this dominator tree.
+ expanded: PropTypes.object,
+
+ // If a node is currently focused in the dominator tree, then this is it.
+ focused: PropTypes.object,
+
+ // If an error was thrown while getting this dominator tree, the `Error`
+ // instance (or an error string message) is attached here.
+ error: PropTypes.oneOfType([
+ PropTypes.string,
+ PropTypes.object,
+ ]),
+
+ // The display used to generate descriptive labels of nodes in this dominator
+ // tree.
+ display: labelDisplayModel,
+
+ // The number of active requests to incrementally fetch subtrees. This should
+ // only be non-zero when the state is INCREMENTAL_FETCHING.
+ activeFetchRequestCount: PropTypes.number,
+
+ // The dominatorTreeState that this domintor tree is currently in.
+ state: catchAndIgnore(function (dominatorTree) {
+ switch (dominatorTree.state) {
+ case dominatorTreeState.COMPUTING:
+ assert(dominatorTree.dominatorTreeId == null,
+ "Should not have a dominator tree id yet");
+ assert(!dominatorTree.root,
+ "Should not have the root of the tree yet");
+ assert(!dominatorTree.error,
+ "Should not have an error");
+ break;
+
+ case dominatorTreeState.COMPUTED:
+ case dominatorTreeState.FETCHING:
+ assert(dominatorTree.dominatorTreeId != null,
+ "Should have a dominator tree id");
+ assert(!dominatorTree.root,
+ "Should not have the root of the tree yet");
+ assert(!dominatorTree.error,
+ "Should not have an error");
+ break;
+
+ case dominatorTreeState.INCREMENTAL_FETCHING:
+ assert(typeof dominatorTree.activeFetchRequestCount === "number",
+ "The active fetch request count is a number when we are in the " +
+ "INCREMENTAL_FETCHING state");
+ assert(dominatorTree.activeFetchRequestCount > 0,
+ "We are keeping track of how many active requests are in flight.");
+ // Fall through...
+ case dominatorTreeState.LOADED:
+ assert(dominatorTree.dominatorTreeId != null,
+ "Should have a dominator tree id");
+ assert(dominatorTree.root,
+ "Should have the root of the tree");
+ assert(dominatorTree.expanded,
+ "Should have an expanded set");
+ assert(!dominatorTree.error,
+ "Should not have an error");
+ break;
+
+ case dominatorTreeState.ERROR:
+ assert(dominatorTree.error, "Should have an error");
+ break;
+
+ default:
+ assert(false,
+ `Unexpected dominator tree state: ${dominatorTree.state}`);
+ }
+ }),
+});
+
+/**
+ * Snapshot model.
+ */
+let stateKeys = Object.keys(states).map(state => states[state]);
+const snapshotId = PropTypes.number;
+let snapshotModel = exports.snapshot = PropTypes.shape({
+ // Unique ID for a snapshot
+ id: snapshotId.isRequired,
+ // Whether or not this snapshot is currently selected.
+ selected: PropTypes.bool.isRequired,
+ // Filesystem path to where the snapshot is stored; used to identify the
+ // snapshot for HeapAnalysesClient.
+ path: PropTypes.string,
+ // Current census data for this snapshot.
+ census: censusModel,
+ // Current dominator tree data for this snapshot.
+ dominatorTree: dominatorTreeModel,
+ // Current tree map data for this snapshot.
+ treeMap: treeMapModel,
+ // If an error was thrown while processing this snapshot, the `Error` instance
+ // is attached here.
+ error: PropTypes.object,
+ // Boolean indicating whether or not this snapshot was imported.
+ imported: PropTypes.bool.isRequired,
+ // The creation time of the snapshot; required after the snapshot has been
+ // read.
+ creationTime: PropTypes.number,
+ // The current state the snapshot is in.
+ // @see ./constants.js
+ state: catchAndIgnore(function (snapshot, propName) {
+ let current = snapshot.state;
+ let shouldHavePath = [states.IMPORTING, states.SAVED, states.READ];
+ let shouldHaveCreationTime = [states.READ];
+
+ if (!stateKeys.includes(current)) {
+ throw new Error(`Snapshot state must be one of ${stateKeys}.`);
+ }
+ if (shouldHavePath.includes(current) && !snapshot.path) {
+ throw new Error(`Snapshots in state ${current} must have a snapshot path.`);
+ }
+ if (shouldHaveCreationTime.includes(current) && !snapshot.creationTime) {
+ throw new Error(`Snapshots in state ${current} must have a creation time.`);
+ }
+ }),
+});
+
+let allocationsModel = exports.allocations = PropTypes.shape({
+ // True iff we are recording allocation stacks right now.
+ recording: PropTypes.bool.isRequired,
+ // True iff we are in the process of toggling the recording of allocation
+ // stacks on or off right now.
+ togglingInProgress: PropTypes.bool.isRequired,
+});
+
+let diffingModel = exports.diffingModel = PropTypes.shape({
+ // The id of the first snapshot to diff.
+ firstSnapshotId: snapshotId,
+
+ // The id of the second snapshot to diff.
+ secondSnapshotId: catchAndIgnore(function (diffing, propName) {
+ if (diffing.secondSnapshotId && !diffing.firstSnapshotId) {
+ throw new Error("Cannot have second snapshot without already having " +
+ "first snapshot");
+ }
+ return snapshotId(diffing, propName);
+ }),
+
+ // The current census data for the diffing.
+ census: censusModel,
+
+ // If an error was thrown while diffing, the `Error` instance is attached
+ // here.
+ error: PropTypes.object,
+
+ // The current state the diffing is in.
+ // @see ./constants.js
+ state: catchAndIgnore(function (diffing) {
+ switch (diffing.state) {
+ case diffingState.TOOK_DIFF:
+ assert(diffing.census, "If we took a diff, we should have a census");
+ // Fall through...
+ case diffingState.TAKING_DIFF:
+ assert(diffing.firstSnapshotId, "Should have first snapshot");
+ assert(diffing.secondSnapshotId, "Should have second snapshot");
+ break;
+
+ case diffingState.SELECTING:
+ break;
+
+ case diffingState.ERROR:
+ assert(diffing.error, "Should have error");
+ break;
+
+ default:
+ assert(false, `Bad diffing state: ${diffing.state}`);
+ }
+ }),
+});
+
+let previousViewModel = exports.previousView = PropTypes.shape({
+ state: catchAndIgnore(function (previous) {
+ switch (previous.state) {
+ case viewState.DIFFING:
+ assert(previous.diffing, "Should have previous diffing state.");
+ assert(!previous.selected, "Should not have a previously selected snapshot.");
+ break;
+
+ case viewState.CENSUS:
+ case viewState.DOMINATOR_TREE:
+ case viewState.TREE_MAP:
+ assert(previous.selected, "Should have a previously selected snapshot.");
+ break;
+
+ case viewState.INDIVIDUALS:
+ default:
+ assert(false, `Unexpected previous view state: ${previous.state}.`);
+ }
+ }),
+
+ // The previous diffing state, if any.
+ diffing: diffingModel,
+
+ // The previously selected snapshot, if any.
+ selected: snapshotId,
+});
+
+let viewModel = exports.view = PropTypes.shape({
+ // The current view state.
+ state: catchAndIgnore(function (view) {
+ switch (view.state) {
+ case viewState.DIFFING:
+ case viewState.CENSUS:
+ case viewState.DOMINATOR_TREE:
+ case viewState.INDIVIDUALS:
+ case viewState.TREE_MAP:
+ break;
+
+ default:
+ assert(false, `Unexpected type of view: ${view.state}`);
+ }
+ }),
+
+ // The previous view state.
+ previous: previousViewModel,
+});
+
+const individualsModel = exports.individuals = PropTypes.shape({
+ error: PropTypes.object,
+
+ nodes: PropTypes.arrayOf(PropTypes.object),
+
+ dominatorTree: dominatorTreeModel,
+
+ id: snapshotId,
+
+ censusBreakdown: PropTypes.object,
+
+ indices: PropTypes.object,
+
+ labelDisplay: labelDisplayModel,
+
+ focused: PropTypes.object,
+
+ state: catchAndIgnore(function (individuals) {
+ switch (individuals.state) {
+ case individualsState.COMPUTING_DOMINATOR_TREE:
+ case individualsState.FETCHING:
+ assert(!individuals.nodes, "Should not have individual nodes");
+ assert(!individuals.dominatorTree, "Should not have dominator tree");
+ assert(!individuals.id, "Should not have an id");
+ assert(!individuals.censusBreakdown, "Should not have a censusBreakdown");
+ assert(!individuals.indices, "Should not have indices");
+ assert(!individuals.labelDisplay, "Should not have a labelDisplay");
+ break;
+
+ case individualsState.FETCHED:
+ assert(individuals.nodes, "Should have individual nodes");
+ assert(individuals.dominatorTree, "Should have dominator tree");
+ assert(individuals.id, "Should have an id");
+ assert(individuals.censusBreakdown, "Should have a censusBreakdown");
+ assert(individuals.indices, "Should have indices");
+ assert(individuals.labelDisplay, "Should have a labelDisplay");
+ break;
+
+ case individualsState.ERROR:
+ assert(individuals.error, "Should have an error object");
+ break;
+
+ default:
+ assert(false, `Unexpected individuals state: ${individuals.state}`);
+ break;
+ }
+ }),
+});
+
+let appModel = exports.app = {
+ // {MemoryFront} Used to communicate with platform
+ front: PropTypes.instanceOf(MemoryFront),
+
+ // Allocations recording related data.
+ allocations: allocationsModel.isRequired,
+
+ // {HeapAnalysesClient} Used to interface with snapshots
+ heapWorker: PropTypes.instanceOf(HeapAnalysesClient),
+
+ // The display data describing how we want the census data to be.
+ censusDisplay: censusDisplayModel.isRequired,
+
+ // The display data describing how we want the dominator tree labels to be
+ // computed.
+ labelDisplay: labelDisplayModel.isRequired,
+
+ // The display data describing how we want the dominator tree labels to be
+ // computed.
+ treeMapDisplay: treeMapDisplayModel.isRequired,
+
+ // List of reference to all snapshots taken
+ snapshots: PropTypes.arrayOf(snapshotModel).isRequired,
+
+ // If present, a filter string for pruning the tree items.
+ filter: PropTypes.string,
+
+ // If present, the current diffing state.
+ diffing: diffingModel,
+
+ // If present, the current individuals state.
+ individuals: individualsModel,
+
+ // The current type of view.
+ view: function (app) {
+ viewModel.isRequired(app, "view");
+
+ catchAndIgnore(function (app) {
+ switch (app.view.state) {
+ case viewState.DIFFING:
+ assert(app.diffing, "Should be diffing");
+ break;
+
+ case viewState.INDIVIDUALS:
+ case viewState.CENSUS:
+ case viewState.DOMINATOR_TREE:
+ case viewState.TREE_MAP:
+ assert(!app.diffing, "Should not be diffing");
+ break;
+
+ default:
+ assert(false, `Unexpected type of view: ${view.state}`);
+ }
+ })(app);
+
+ catchAndIgnore(function (app) {
+ switch (app.view.state) {
+ case viewState.INDIVIDUALS:
+ assert(app.individuals, "Should have individuals state");
+ break;
+
+ case viewState.DIFFING:
+ case viewState.CENSUS:
+ case viewState.DOMINATOR_TREE:
+ case viewState.TREE_MAP:
+ assert(!app.individuals, "Should not have individuals state");
+ break;
+
+ default:
+ assert(false, `Unexpected type of view: ${view.state}`);
+ }
+ })(app);
+ },
+};
diff --git a/devtools/client/memory/moz.build b/devtools/client/memory/moz.build
new file mode 100644
index 000000000..dccb57938
--- /dev/null
+++ b/devtools/client/memory/moz.build
@@ -0,0 +1,29 @@
+# vim: set filetype=python:
+# 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/.
+
+with Files('**'):
+ BUG_COMPONENT = ('Firefox', 'Developer Tools: Memory')
+
+DIRS += [
+ 'actions',
+ 'components',
+ 'reducers',
+]
+
+DevToolsModules(
+ 'app.js',
+ 'constants.js',
+ 'dominator-tree-lazy-children.js',
+ 'models.js',
+ 'panel.js',
+ 'reducers.js',
+ 'store.js',
+ 'telemetry.js',
+ 'utils.js',
+)
+
+BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
+XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
+MOCHITEST_CHROME_MANIFESTS += ['test/chrome/chrome.ini']
diff --git a/devtools/client/memory/panel.js b/devtools/client/memory/panel.js
new file mode 100644
index 000000000..cf867faff
--- /dev/null
+++ b/devtools/client/memory/panel.js
@@ -0,0 +1,75 @@
+/* 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 { Task } = require("devtools/shared/task");
+const EventEmitter = require("devtools/shared/event-emitter");
+const { MemoryFront } = require("devtools/shared/fronts/memory");
+const HeapAnalysesClient = require("devtools/shared/heapsnapshot/HeapAnalysesClient");
+const promise = require("promise");
+
+function MemoryPanel(iframeWindow, toolbox) {
+ this.panelWin = iframeWindow;
+ this._toolbox = toolbox;
+
+ EventEmitter.decorate(this);
+}
+
+MemoryPanel.prototype = {
+ open: Task.async(function* () {
+ if (this._opening) {
+ return this._opening;
+ }
+
+ this.panelWin.gToolbox = this._toolbox;
+ this.panelWin.gTarget = this.target;
+
+ const rootForm = yield this.target.root;
+ this.panelWin.gFront = new MemoryFront(this.target.client,
+ this.target.form,
+ rootForm);
+ this.panelWin.gHeapAnalysesClient = new HeapAnalysesClient();
+
+ yield this.panelWin.gFront.attach();
+
+ this._opening = this.panelWin.initialize().then(() => {
+ this.isReady = true;
+ this.emit("ready");
+ return this;
+ });
+
+ return this._opening;
+ }),
+
+ // DevToolPanel API
+
+ get target() {
+ return this._toolbox.target;
+ },
+
+ destroy: Task.async(function* () {
+ // Make sure this panel is not already destroyed.
+ if (this._destroyer) {
+ return this._destroyer;
+ }
+
+ yield this.panelWin.gFront.detach();
+
+ this._destroyer = this.panelWin.destroy().then(() => {
+ // Destroy front to ensure packet handler is removed from client
+ this.panelWin.gFront.destroy();
+ this.panelWin.gHeapAnalysesClient.destroy();
+ this.panelWin = null;
+ this._opening = null;
+ this.isReady = false;
+ this.emit("destroyed");
+ });
+
+ return this._destroyer;
+ })
+};
+
+exports.MemoryPanel = MemoryPanel;
diff --git a/devtools/client/memory/reducers.js b/devtools/client/memory/reducers.js
new file mode 100644
index 000000000..5f82c3d3b
--- /dev/null
+++ b/devtools/client/memory/reducers.js
@@ -0,0 +1,16 @@
+/* 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";
+
+exports.allocations = require("./reducers/allocations");
+exports.censusDisplay = require("./reducers/census-display");
+exports.diffing = require("./reducers/diffing");
+exports.individuals = require("./reducers/individuals");
+exports.labelDisplay = require("./reducers/label-display");
+exports.treeMapDisplay = require("./reducers/tree-map-display");
+exports.errors = require("./reducers/errors");
+exports.filter = require("./reducers/filter");
+exports.sizes = require("./reducers/sizes");
+exports.snapshots = require("./reducers/snapshots");
+exports.view = require("./reducers/view");
diff --git a/devtools/client/memory/reducers/allocations.js b/devtools/client/memory/reducers/allocations.js
new file mode 100644
index 000000000..ccb92f825
--- /dev/null
+++ b/devtools/client/memory/reducers/allocations.js
@@ -0,0 +1,42 @@
+/* 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/. */
+
+const { assert } = require("devtools/shared/DevToolsUtils");
+const { actions } = require("../constants");
+
+let handlers = Object.create(null);
+
+handlers[actions.TOGGLE_RECORD_ALLOCATION_STACKS_START] = function (state, action) {
+ assert(!state.togglingInProgress,
+ "Changing recording state must not be reentrant.");
+
+ return {
+ recording: !state.recording,
+ togglingInProgress: true,
+ };
+};
+
+handlers[actions.TOGGLE_RECORD_ALLOCATION_STACKS_END] = function (state, action) {
+ assert(state.togglingInProgress,
+ "Should not complete changing recording state if we weren't changing "
+ + "recording state already.");
+
+ return {
+ recording: state.recording,
+ togglingInProgress: false,
+ };
+};
+
+const DEFAULT_ALLOCATIONS_STATE = {
+ recording: false,
+ togglingInProgress: false
+};
+
+module.exports = function (state = DEFAULT_ALLOCATIONS_STATE, action) {
+ let handle = handlers[action.type];
+ if (handle) {
+ return handle(state, action);
+ }
+ return state;
+};
diff --git a/devtools/client/memory/reducers/census-display.js b/devtools/client/memory/reducers/census-display.js
new file mode 100644
index 000000000..83535a201
--- /dev/null
+++ b/devtools/client/memory/reducers/census-display.js
@@ -0,0 +1,21 @@
+/* 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 { actions, censusDisplays } = require("../constants");
+const DEFAULT_CENSUS_DISPLAY = censusDisplays.coarseType;
+
+let handlers = Object.create(null);
+
+handlers[actions.SET_CENSUS_DISPLAY] = function (_, { display }) {
+ return display;
+};
+
+module.exports = function (state = DEFAULT_CENSUS_DISPLAY, action) {
+ let handle = handlers[action.type];
+ if (handle) {
+ return handle(state, action);
+ }
+ return state;
+};
diff --git a/devtools/client/memory/reducers/diffing.js b/devtools/client/memory/reducers/diffing.js
new file mode 100644
index 000000000..6d4973c8e
--- /dev/null
+++ b/devtools/client/memory/reducers/diffing.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 Immutable = require("devtools/client/shared/vendor/immutable");
+const { immutableUpdate, assert } = require("devtools/shared/DevToolsUtils");
+const { actions, diffingState, viewState } = require("../constants");
+const { snapshotIsDiffable } = require("../utils");
+
+const handlers = Object.create(null);
+
+handlers[actions.POP_VIEW] = function (diffing, { previousView }) {
+ if (previousView.state === viewState.DIFFING) {
+ assert(previousView.diffing, "Should have previousView.diffing");
+ return previousView.diffing;
+ }
+
+ return null;
+};
+
+handlers[actions.CHANGE_VIEW] = function (diffing, { newViewState }) {
+ if (newViewState === viewState.DIFFING) {
+ assert(!diffing, "Should not switch to diffing view when already diffing");
+ return Object.freeze({
+ firstSnapshotId: null,
+ secondSnapshotId: null,
+ census: null,
+ state: diffingState.SELECTING,
+ });
+ }
+
+ return null;
+};
+
+handlers[actions.SELECT_SNAPSHOT_FOR_DIFFING] = function (diffing, { snapshot }) {
+ assert(diffing,
+ "Should never select a snapshot for diffing when we aren't diffing " +
+ "anything");
+ assert(diffing.state === diffingState.SELECTING,
+ "Can't select when not in SELECTING state");
+ assert(snapshotIsDiffable(snapshot),
+ "snapshot must be in a diffable state");
+
+ if (!diffing.firstSnapshotId) {
+ return immutableUpdate(diffing, {
+ firstSnapshotId: snapshot.id
+ });
+ }
+
+ assert(!diffing.secondSnapshotId,
+ "If we aren't selecting the first, then we must be selecting the " +
+ "second");
+
+ if (snapshot.id === diffing.firstSnapshotId) {
+ // Ignore requests to select the same snapshot.
+ return diffing;
+ }
+
+ return immutableUpdate(diffing, {
+ secondSnapshotId: snapshot.id
+ });
+};
+
+handlers[actions.TAKE_CENSUS_DIFF_START] = function (diffing, action) {
+ assert(diffing, "Should be diffing when starting a census diff");
+ assert(action.first.id === diffing.firstSnapshotId,
+ "First snapshot's id should match");
+ assert(action.second.id === diffing.secondSnapshotId,
+ "Second snapshot's id should match");
+
+ return immutableUpdate(diffing, {
+ state: diffingState.TAKING_DIFF,
+ census: {
+ report: null,
+ inverted: action.inverted,
+ filter: action.filter,
+ display: action.display,
+ }
+ });
+};
+
+handlers[actions.TAKE_CENSUS_DIFF_END] = function (diffing, action) {
+ assert(diffing, "Should be diffing when ending a census diff");
+ assert(action.first.id === diffing.firstSnapshotId,
+ "First snapshot's id should match");
+ assert(action.second.id === diffing.secondSnapshotId,
+ "Second snapshot's id should match");
+
+ return immutableUpdate(diffing, {
+ state: diffingState.TOOK_DIFF,
+ census: {
+ report: action.report,
+ parentMap: action.parentMap,
+ expanded: Immutable.Set(),
+ inverted: action.inverted,
+ filter: action.filter,
+ display: action.display,
+ }
+ });
+};
+
+handlers[actions.DIFFING_ERROR] = function (diffing, action) {
+ return {
+ state: diffingState.ERROR,
+ error: action.error
+ };
+};
+
+handlers[actions.EXPAND_DIFFING_CENSUS_NODE] = function (diffing, { node }) {
+ assert(diffing, "Should be diffing if expanding diffing's census nodes");
+ assert(diffing.state === diffingState.TOOK_DIFF,
+ "Should have taken the census diff if expanding nodes");
+ assert(diffing.census, "Should have a census");
+ assert(diffing.census.report, "Should have a census report");
+ assert(diffing.census.expanded, "Should have a census's expanded set");
+
+ const expanded = diffing.census.expanded.add(node.id);
+ const census = immutableUpdate(diffing.census, { expanded });
+ return immutableUpdate(diffing, { census });
+};
+
+handlers[actions.COLLAPSE_DIFFING_CENSUS_NODE] = function (diffing, { node }) {
+ assert(diffing, "Should be diffing if expanding diffing's census nodes");
+ assert(diffing.state === diffingState.TOOK_DIFF,
+ "Should have taken the census diff if expanding nodes");
+ assert(diffing.census, "Should have a census");
+ assert(diffing.census.report, "Should have a census report");
+ assert(diffing.census.expanded, "Should have a census's expanded set");
+
+ const expanded = diffing.census.expanded.delete(node.id);
+ const census = immutableUpdate(diffing.census, { expanded });
+ return immutableUpdate(diffing, { census });
+};
+
+handlers[actions.FOCUS_DIFFING_CENSUS_NODE] = function (diffing, { node }) {
+ assert(diffing, "Should be diffing.");
+ assert(diffing.census, "Should have a census");
+ const census = immutableUpdate(diffing.census, { focused: node });
+ return immutableUpdate(diffing, { census });
+};
+
+module.exports = function (diffing = null, action) {
+ const handler = handlers[action.type];
+ return handler ? handler(diffing, action) : diffing;
+};
diff --git a/devtools/client/memory/reducers/errors.js b/devtools/client/memory/reducers/errors.js
new file mode 100644
index 000000000..5fc282df2
--- /dev/null
+++ b/devtools/client/memory/reducers/errors.js
@@ -0,0 +1,17 @@
+/* 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/. */
+
+const { ERROR_TYPE: TASK_ERROR_TYPE } = require("devtools/client/shared/redux/middleware/task");
+
+/**
+ * Handle errors dispatched from task middleware and
+ * store them so we can check in tests or dump them out.
+ */
+module.exports = function (state = [], action) {
+ switch (action.type) {
+ case TASK_ERROR_TYPE:
+ return [...state, action.error];
+ }
+ return state;
+};
diff --git a/devtools/client/memory/reducers/filter.js b/devtools/client/memory/reducers/filter.js
new file mode 100644
index 000000000..99188142c
--- /dev/null
+++ b/devtools/client/memory/reducers/filter.js
@@ -0,0 +1,14 @@
+/* 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 { actions } = require("../constants");
+
+module.exports = function (filterString = null, action) {
+ if (action.type === actions.SET_FILTER_STRING) {
+ return action.filter || null;
+ } else {
+ return filterString;
+ }
+};
diff --git a/devtools/client/memory/reducers/individuals.js b/devtools/client/memory/reducers/individuals.js
new file mode 100644
index 000000000..10f016fe5
--- /dev/null
+++ b/devtools/client/memory/reducers/individuals.js
@@ -0,0 +1,73 @@
+/* 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 { assert, immutableUpdate } = require("devtools/shared/DevToolsUtils");
+const { actions, individualsState, viewState } = require("../constants");
+
+const handlers = Object.create(null);
+
+handlers[actions.POP_VIEW] = function (_state, _action) {
+ return null;
+};
+
+handlers[actions.CHANGE_VIEW] = function (individuals, { newViewState }) {
+ if (newViewState === viewState.INDIVIDUALS) {
+ assert(!individuals,
+ "Should not switch to individuals view when already in individuals view");
+ return Object.freeze({
+ state: individualsState.COMPUTING_DOMINATOR_TREE,
+ });
+ }
+
+ return null;
+};
+
+handlers[actions.FOCUS_INDIVIDUAL] = function (individuals, { node }) {
+ assert(individuals, "Should have individuals");
+ return immutableUpdate(individuals, { focused: node });
+};
+
+handlers[actions.FETCH_INDIVIDUALS_START] = function (individuals, action) {
+ assert(individuals, "Should have individuals");
+ return Object.freeze({
+ state: individualsState.FETCHING,
+ focused: individuals.focused,
+ });
+};
+
+handlers[actions.FETCH_INDIVIDUALS_END] = function (individuals, action) {
+ assert(individuals, "Should have individuals");
+ assert(!individuals.nodes, "Should not have nodes");
+ assert(individuals.state === individualsState.FETCHING,
+ "Should only end fetching individuals after starting.");
+
+ const focused = individuals.focused
+ ? action.nodes.find(n => n.nodeId === individuals.focused.nodeId)
+ : null;
+
+ return Object.freeze({
+ state: individualsState.FETCHED,
+ nodes: action.nodes,
+ id: action.id,
+ censusBreakdown: action.censusBreakdown,
+ indices: action.indices,
+ labelDisplay: action.labelDisplay,
+ focused,
+ dominatorTree: action.dominatorTree,
+ });
+};
+
+handlers[actions.INDIVIDUALS_ERROR] = function (_, { error }) {
+ return Object.freeze({
+ error,
+ nodes: null,
+ state: individualsState.ERROR,
+ });
+};
+
+module.exports = function (individuals = null, action) {
+ const handler = handlers[action.type];
+ return handler ? handler(individuals, action) : individuals;
+};
diff --git a/devtools/client/memory/reducers/label-display.js b/devtools/client/memory/reducers/label-display.js
new file mode 100644
index 000000000..e7d142682
--- /dev/null
+++ b/devtools/client/memory/reducers/label-display.js
@@ -0,0 +1,19 @@
+/* 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 { actions, labelDisplays } = require("../constants");
+const DEFAULT_LABEL_DISPLAY = labelDisplays.coarseType;
+
+const handlers = Object.create(null);
+
+handlers[actions.SET_LABEL_DISPLAY] = function (_, { display }) {
+ return display;
+};
+
+module.exports = function (state = DEFAULT_LABEL_DISPLAY, action) {
+ const handler = handlers[action.type];
+ return handler ? handler(state, action) : state;
+};
diff --git a/devtools/client/memory/reducers/moz.build b/devtools/client/memory/reducers/moz.build
new file mode 100644
index 000000000..664c4496c
--- /dev/null
+++ b/devtools/client/memory/reducers/moz.build
@@ -0,0 +1,18 @@
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'allocations.js',
+ 'census-display.js',
+ 'diffing.js',
+ 'errors.js',
+ 'filter.js',
+ 'individuals.js',
+ 'label-display.js',
+ 'sizes.js',
+ 'snapshots.js',
+ 'tree-map-display.js',
+ 'view.js',
+)
diff --git a/devtools/client/memory/reducers/sizes.js b/devtools/client/memory/reducers/sizes.js
new file mode 100644
index 000000000..f04530cfc
--- /dev/null
+++ b/devtools/client/memory/reducers/sizes.js
@@ -0,0 +1,18 @@
+/* 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 { actions } = require("../constants");
+const { immutableUpdate } = require("devtools/shared/DevToolsUtils");
+
+const handlers = Object.create(null);
+
+handlers[actions.RESIZE_SHORTEST_PATHS] = function (sizes, { size }) {
+ return immutableUpdate(sizes, { shortestPathsSize: size });
+};
+
+module.exports = function (sizes = { shortestPathsSize: .5 }, action) {
+ const handler = handlers[action.type];
+ return handler ? handler(sizes, action) : sizes;
+};
diff --git a/devtools/client/memory/reducers/snapshots.js b/devtools/client/memory/reducers/snapshots.js
new file mode 100644
index 000000000..6293bdded
--- /dev/null
+++ b/devtools/client/memory/reducers/snapshots.js
@@ -0,0 +1,459 @@
+/* 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 Immutable = require("devtools/client/shared/vendor/immutable");
+const { immutableUpdate, assert } = require("devtools/shared/DevToolsUtils");
+const {
+ actions,
+ snapshotState: states,
+ censusState,
+ treeMapState,
+ dominatorTreeState,
+ viewState,
+} = require("../constants");
+const DominatorTreeNode = require("devtools/shared/heapsnapshot/DominatorTreeNode");
+
+const handlers = Object.create(null);
+
+handlers[actions.SNAPSHOT_ERROR] = function (snapshots, { id, error }) {
+ return snapshots.map(snapshot => {
+ return snapshot.id === id
+ ? immutableUpdate(snapshot, { state: states.ERROR, error })
+ : snapshot;
+ });
+};
+
+handlers[actions.TAKE_SNAPSHOT_START] = function (snapshots, { snapshot }) {
+ return [...snapshots, snapshot];
+};
+
+handlers[actions.TAKE_SNAPSHOT_END] = function (snapshots, { id, path }) {
+ return snapshots.map(snapshot => {
+ return snapshot.id === id
+ ? immutableUpdate(snapshot, { state: states.SAVED, path })
+ : snapshot;
+ });
+};
+
+handlers[actions.IMPORT_SNAPSHOT_START] = handlers[actions.TAKE_SNAPSHOT_START];
+
+handlers[actions.READ_SNAPSHOT_START] = function (snapshots, { id }) {
+ return snapshots.map(snapshot => {
+ return snapshot.id === id
+ ? immutableUpdate(snapshot, { state: states.READING })
+ : snapshot;
+ });
+};
+
+handlers[actions.READ_SNAPSHOT_END] = function (snapshots, { id, creationTime }) {
+ return snapshots.map(snapshot => {
+ return snapshot.id === id
+ ? immutableUpdate(snapshot, { state: states.READ, creationTime })
+ : snapshot;
+ });
+};
+
+handlers[actions.TAKE_CENSUS_START] = function (snapshots, { id, display, filter }) {
+ const census = {
+ report: null,
+ display,
+ filter,
+ state: censusState.SAVING
+ };
+
+ return snapshots.map(snapshot => {
+ return snapshot.id === id
+ ? immutableUpdate(snapshot, { census })
+ : snapshot;
+ });
+};
+
+handlers[actions.TAKE_CENSUS_END] = function (snapshots, { id,
+ report,
+ parentMap,
+ display,
+ filter }) {
+ const census = {
+ report,
+ parentMap,
+ expanded: Immutable.Set(),
+ display,
+ filter,
+ state: censusState.SAVED
+ };
+
+ return snapshots.map(snapshot => {
+ return snapshot.id === id
+ ? immutableUpdate(snapshot, { census })
+ : snapshot;
+ });
+};
+
+handlers[actions.TAKE_CENSUS_ERROR] = function (snapshots, { id, error }) {
+ assert(error, "actions with TAKE_CENSUS_ERROR should have an error");
+
+ return snapshots.map(snapshot => {
+ if (snapshot.id !== id) {
+ return snapshot;
+ }
+
+ const census = Object.freeze({
+ state: censusState.ERROR,
+ error,
+ });
+
+ return immutableUpdate(snapshot, { census });
+ });
+};
+
+handlers[actions.TAKE_TREE_MAP_START] = function (snapshots, { id, display }) {
+ const treeMap = {
+ report: null,
+ display,
+ state: treeMapState.SAVING
+ };
+
+ return snapshots.map(snapshot => {
+ return snapshot.id === id
+ ? immutableUpdate(snapshot, { treeMap })
+ : snapshot;
+ });
+};
+
+handlers[actions.TAKE_TREE_MAP_END] = function (snapshots, action) {
+ const { id, report, display } = action;
+ const treeMap = {
+ report,
+ display,
+ state: treeMapState.SAVED
+ };
+
+ return snapshots.map(snapshot => {
+ return snapshot.id === id
+ ? immutableUpdate(snapshot, { treeMap })
+ : snapshot;
+ });
+};
+
+handlers[actions.TAKE_TREE_MAP_ERROR] = function (snapshots, { id, error }) {
+ assert(error, "actions with TAKE_TREE_MAP_ERROR should have an error");
+
+ return snapshots.map(snapshot => {
+ if (snapshot.id !== id) {
+ return snapshot;
+ }
+
+ const treeMap = Object.freeze({
+ state: treeMapState.ERROR,
+ error,
+ });
+
+ return immutableUpdate(snapshot, { treeMap });
+ });
+};
+
+handlers[actions.EXPAND_CENSUS_NODE] = function (snapshots, { id, node }) {
+ return snapshots.map(snapshot => {
+ if (snapshot.id !== id) {
+ return snapshot;
+ }
+
+ assert(snapshot.census, "Should have a census");
+ assert(snapshot.census.report, "Should have a census report");
+ assert(snapshot.census.expanded, "Should have a census's expanded set");
+
+ const expanded = snapshot.census.expanded.add(node.id);
+ const census = immutableUpdate(snapshot.census, { expanded });
+ return immutableUpdate(snapshot, { census });
+ });
+};
+
+handlers[actions.COLLAPSE_CENSUS_NODE] = function (snapshots, { id, node }) {
+ return snapshots.map(snapshot => {
+ if (snapshot.id !== id) {
+ return snapshot;
+ }
+
+ assert(snapshot.census, "Should have a census");
+ assert(snapshot.census.report, "Should have a census report");
+ assert(snapshot.census.expanded, "Should have a census's expanded set");
+
+ const expanded = snapshot.census.expanded.delete(node.id);
+ const census = immutableUpdate(snapshot.census, { expanded });
+ return immutableUpdate(snapshot, { census });
+ });
+};
+
+handlers[actions.FOCUS_CENSUS_NODE] = function (snapshots, { id, node }) {
+ return snapshots.map(snapshot => {
+ if (snapshot.id !== id) {
+ return snapshot;
+ }
+
+ assert(snapshot.census, "Should have a census");
+ const census = immutableUpdate(snapshot.census, { focused: node });
+ return immutableUpdate(snapshot, { census });
+ });
+};
+
+handlers[actions.SELECT_SNAPSHOT] = function (snapshots, { id }) {
+ return snapshots.map(s => immutableUpdate(s, { selected: s.id === id }));
+};
+
+handlers[actions.DELETE_SNAPSHOTS_START] = function (snapshots, { ids }) {
+ return snapshots.filter(s => ids.indexOf(s.id) === -1);
+};
+
+handlers[actions.DELETE_SNAPSHOTS_END] = function (snapshots) {
+ return snapshots;
+};
+
+handlers[actions.CHANGE_VIEW] = function (snapshots, { newViewState }) {
+ return newViewState === viewState.DIFFING
+ ? snapshots.map(s => immutableUpdate(s, { selected: false }))
+ : snapshots;
+};
+
+handlers[actions.POP_VIEW] = function (snapshots, { previousView }) {
+ return snapshots.map(s => immutableUpdate(s, {
+ selected: s.id === previousView.selected
+ }));
+};
+
+handlers[actions.COMPUTE_DOMINATOR_TREE_START] = function (snapshots, { id }) {
+ const dominatorTree = Object.freeze({
+ state: dominatorTreeState.COMPUTING,
+ dominatorTreeId: undefined,
+ root: undefined,
+ });
+
+ return snapshots.map(snapshot => {
+ if (snapshot.id !== id) {
+ return snapshot;
+ }
+
+ assert(!snapshot.dominatorTree,
+ "Should not have a dominator tree model");
+ return immutableUpdate(snapshot, { dominatorTree });
+ });
+};
+
+handlers[actions.COMPUTE_DOMINATOR_TREE_END] = function (snapshots, { id, dominatorTreeId }) {
+ return snapshots.map(snapshot => {
+ if (snapshot.id !== id) {
+ return snapshot;
+ }
+
+ assert(snapshot.dominatorTree, "Should have a dominator tree model");
+ assert(snapshot.dominatorTree.state == dominatorTreeState.COMPUTING,
+ "Should be in the COMPUTING state");
+
+ const dominatorTree = immutableUpdate(snapshot.dominatorTree, {
+ state: dominatorTreeState.COMPUTED,
+ dominatorTreeId,
+ });
+ return immutableUpdate(snapshot, { dominatorTree });
+ });
+};
+
+handlers[actions.FETCH_DOMINATOR_TREE_START] = function (snapshots, { id, display }) {
+ return snapshots.map(snapshot => {
+ if (snapshot.id !== id) {
+ return snapshot;
+ }
+
+ assert(snapshot.dominatorTree, "Should have a dominator tree model");
+ assert(snapshot.dominatorTree.state !== dominatorTreeState.COMPUTING &&
+ snapshot.dominatorTree.state !== dominatorTreeState.ERROR,
+ `Should have already computed the dominator tree, found state = ${snapshot.dominatorTree.state}`);
+
+ const dominatorTree = immutableUpdate(snapshot.dominatorTree, {
+ state: dominatorTreeState.FETCHING,
+ root: undefined,
+ display,
+ });
+ return immutableUpdate(snapshot, { dominatorTree });
+ });
+};
+
+handlers[actions.FETCH_DOMINATOR_TREE_END] = function (snapshots, { id, root }) {
+ return snapshots.map(snapshot => {
+ if (snapshot.id !== id) {
+ return snapshot;
+ }
+
+ assert(snapshot.dominatorTree, "Should have a dominator tree model");
+ assert(snapshot.dominatorTree.state == dominatorTreeState.FETCHING,
+ "Should be in the FETCHING state");
+
+ let focused;
+ if (snapshot.dominatorTree.focused) {
+ focused = (function findFocused(node) {
+ if (node.nodeId === snapshot.dominatorTree.focused.nodeId) {
+ return node;
+ }
+
+ if (node.children) {
+ const length = node.children.length;
+ for (let i = 0; i < length; i++) {
+ const result = findFocused(node.children[i]);
+ if (result) {
+ return result;
+ }
+ }
+ }
+
+ return undefined;
+ }(root));
+ }
+
+ const dominatorTree = immutableUpdate(snapshot.dominatorTree, {
+ state: dominatorTreeState.LOADED,
+ root,
+ expanded: Immutable.Set(),
+ focused,
+ });
+
+ return immutableUpdate(snapshot, { dominatorTree });
+ });
+};
+
+handlers[actions.EXPAND_DOMINATOR_TREE_NODE] = function (snapshots, { id, node }) {
+ return snapshots.map(snapshot => {
+ if (snapshot.id !== id) {
+ return snapshot;
+ }
+
+ assert(snapshot.dominatorTree, "Should have a dominator tree");
+ assert(snapshot.dominatorTree.expanded,
+ "Should have the dominator tree's expanded set");
+
+ const expanded = snapshot.dominatorTree.expanded.add(node.nodeId);
+ const dominatorTree = immutableUpdate(snapshot.dominatorTree, { expanded });
+ return immutableUpdate(snapshot, { dominatorTree });
+ });
+};
+
+handlers[actions.COLLAPSE_DOMINATOR_TREE_NODE] = function (snapshots, { id, node }) {
+ return snapshots.map(snapshot => {
+ if (snapshot.id !== id) {
+ return snapshot;
+ }
+
+ assert(snapshot.dominatorTree, "Should have a dominator tree");
+ assert(snapshot.dominatorTree.expanded,
+ "Should have the dominator tree's expanded set");
+
+ const expanded = snapshot.dominatorTree.expanded.delete(node.nodeId);
+ const dominatorTree = immutableUpdate(snapshot.dominatorTree, { expanded });
+ return immutableUpdate(snapshot, { dominatorTree });
+ });
+};
+
+handlers[actions.FOCUS_DOMINATOR_TREE_NODE] = function (snapshots, { id, node }) {
+ return snapshots.map(snapshot => {
+ if (snapshot.id !== id) {
+ return snapshot;
+ }
+
+ assert(snapshot.dominatorTree, "Should have a dominator tree");
+ const dominatorTree = immutableUpdate(snapshot.dominatorTree, { focused: node });
+ return immutableUpdate(snapshot, { dominatorTree });
+ });
+};
+
+handlers[actions.FETCH_IMMEDIATELY_DOMINATED_START] = function (snapshots, { id }) {
+ return snapshots.map(snapshot => {
+ if (snapshot.id !== id) {
+ return snapshot;
+ }
+
+ assert(snapshot.dominatorTree, "Should have a dominator tree model");
+ assert(snapshot.dominatorTree.state == dominatorTreeState.INCREMENTAL_FETCHING ||
+ snapshot.dominatorTree.state == dominatorTreeState.LOADED,
+ "The dominator tree should be loaded if we are going to " +
+ "incrementally fetch children.");
+
+ const activeFetchRequestCount = snapshot.dominatorTree.activeFetchRequestCount
+ ? snapshot.dominatorTree.activeFetchRequestCount + 1
+ : 1;
+
+ const dominatorTree = immutableUpdate(snapshot.dominatorTree, {
+ state: dominatorTreeState.INCREMENTAL_FETCHING,
+ activeFetchRequestCount,
+ });
+
+ return immutableUpdate(snapshot, { dominatorTree });
+ });
+};
+
+handlers[actions.FETCH_IMMEDIATELY_DOMINATED_END] =
+ function (snapshots, { id, path, nodes, moreChildrenAvailable}) {
+ return snapshots.map(snapshot => {
+ if (snapshot.id !== id) {
+ return snapshot;
+ }
+
+ assert(snapshot.dominatorTree, "Should have a dominator tree model");
+ assert(snapshot.dominatorTree.root, "Should have a dominator tree model root");
+ assert(snapshot.dominatorTree.state === dominatorTreeState.INCREMENTAL_FETCHING,
+ "The dominator tree state should be INCREMENTAL_FETCHING");
+
+ const root = DominatorTreeNode.insert(snapshot.dominatorTree.root,
+ path,
+ nodes,
+ moreChildrenAvailable);
+
+ const focused = snapshot.dominatorTree.focused
+ ? DominatorTreeNode.getNodeByIdAlongPath(snapshot.dominatorTree.focused.nodeId,
+ root,
+ path)
+ : undefined;
+
+ const activeFetchRequestCount = snapshot.dominatorTree.activeFetchRequestCount === 1
+ ? undefined
+ : snapshot.dominatorTree.activeFetchRequestCount - 1;
+
+ // If there are still outstanding requests, we need to stay in the
+ // INCREMENTAL_FETCHING state until they complete.
+ const state = activeFetchRequestCount
+ ? dominatorTreeState.INCREMENTAL_FETCHING
+ : dominatorTreeState.LOADED;
+
+ const dominatorTree = immutableUpdate(snapshot.dominatorTree, {
+ state,
+ root,
+ focused,
+ activeFetchRequestCount,
+ });
+
+ return immutableUpdate(snapshot, { dominatorTree });
+ });
+ };
+
+handlers[actions.DOMINATOR_TREE_ERROR] = function (snapshots, { id, error }) {
+ assert(error, "actions with DOMINATOR_TREE_ERROR should have an error");
+
+ return snapshots.map(snapshot => {
+ if (snapshot.id !== id) {
+ return snapshot;
+ }
+
+ const dominatorTree = Object.freeze({
+ state: dominatorTreeState.ERROR,
+ error,
+ });
+
+ return immutableUpdate(snapshot, { dominatorTree });
+ });
+};
+
+module.exports = function (snapshots = [], action) {
+ const handler = handlers[action.type];
+ if (handler) {
+ return handler(snapshots, action);
+ }
+ return snapshots;
+};
diff --git a/devtools/client/memory/reducers/tree-map-display.js b/devtools/client/memory/reducers/tree-map-display.js
new file mode 100644
index 000000000..a0d2faadc
--- /dev/null
+++ b/devtools/client/memory/reducers/tree-map-display.js
@@ -0,0 +1,19 @@
+/* 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 { actions, treeMapDisplays } = require("../constants");
+const DEFAULT_TREE_MAP_DISPLAY = treeMapDisplays.coarseType;
+
+const handlers = Object.create(null);
+
+handlers[actions.SET_TREE_MAP_DISPLAY] = function (_, { display }) {
+ return display;
+};
+
+module.exports = function (state = DEFAULT_TREE_MAP_DISPLAY, action) {
+ const handler = handlers[action.type];
+ return handler ? handler(state, action) : state;
+};
diff --git a/devtools/client/memory/reducers/view.js b/devtools/client/memory/reducers/view.js
new file mode 100644
index 000000000..5d31e0c9b
--- /dev/null
+++ b/devtools/client/memory/reducers/view.js
@@ -0,0 +1,49 @@
+/* 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 { assert } = require("devtools/shared/DevToolsUtils");
+const { actions, viewState } = require("../constants");
+
+const handlers = Object.create(null);
+
+handlers[actions.POP_VIEW] = function (view, _) {
+ assert(view.previous, "Had better have a previous view state when POP_VIEW");
+ return Object.freeze({
+ state: view.previous.state,
+ previous: null,
+ });
+};
+
+handlers[actions.CHANGE_VIEW] = function (view, action) {
+ const { newViewState, oldDiffing, oldSelected } = action;
+ assert(newViewState);
+
+ if (newViewState === viewState.INDIVIDUALS) {
+ assert(oldDiffing || oldSelected);
+ return Object.freeze({
+ state: newViewState,
+ previous: Object.freeze({
+ state: view.state,
+ selected: oldSelected,
+ diffing: oldDiffing,
+ }),
+ });
+ }
+
+ return Object.freeze({
+ state: newViewState,
+ previous: null,
+ });
+};
+
+const DEFAULT_VIEW = {
+ state: viewState.TREE_MAP,
+ previous: null,
+};
+
+module.exports = function (view = DEFAULT_VIEW, action) {
+ const handler = handlers[action.type];
+ return handler ? handler(view, action) : view;
+};
diff --git a/devtools/client/memory/store.js b/devtools/client/memory/store.js
new file mode 100644
index 000000000..3fd2e8a68
--- /dev/null
+++ b/devtools/client/memory/store.js
@@ -0,0 +1,33 @@
+/* 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/. */
+
+const { combineReducers } = require("../shared/vendor/redux");
+const createStore = require("../shared/redux/create-store");
+const reducers = require("./reducers");
+const { viewState } = require("./constants");
+const flags = require("devtools/shared/flags");
+
+module.exports = function () {
+ let shouldLog = false;
+ let history;
+
+ // If testing, store the action history in an array
+ // we'll later attach to the store
+ if (flags.testing) {
+ history = [];
+ // Uncomment this for TONS of logging in tests.
+ // shouldLog = true;
+ }
+
+ let store = createStore({
+ log: shouldLog,
+ history
+ })(combineReducers(reducers), {});
+
+ if (history) {
+ store.history = history;
+ }
+
+ return store;
+};
diff --git a/devtools/client/memory/telemetry.js b/devtools/client/memory/telemetry.js
new file mode 100644
index 000000000..1e55805d3
--- /dev/null
+++ b/devtools/client/memory/telemetry.js
@@ -0,0 +1,91 @@
+/* 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/. */
+
+// This module exports methods to record telemetry data for memory tool usage.
+//
+// NB: Ensure that *every* exported function is wrapped in `makeInfallible` so
+// that our probes don't accidentally break code that actually does productive
+// work for the user!
+
+const { telemetry } = require("Services");
+const { makeInfallible, immutableUpdate } = require("devtools/shared/DevToolsUtils");
+const { labelDisplays, treeMapDisplays, censusDisplays } = require("./constants");
+
+exports.countTakeSnapshot = makeInfallible(function () {
+ const histogram = telemetry.getHistogramById("DEVTOOLS_MEMORY_TAKE_SNAPSHOT_COUNT");
+ histogram.add(1);
+}, "devtools/client/memory/telemetry#countTakeSnapshot");
+
+exports.countImportSnapshot = makeInfallible(function () {
+ const histogram = telemetry.getHistogramById("DEVTOOLS_MEMORY_IMPORT_SNAPSHOT_COUNT");
+ histogram.add(1);
+}, "devtools/client/memory/telemetry#countImportSnapshot");
+
+exports.countExportSnapshot = makeInfallible(function () {
+ const histogram = telemetry.getHistogramById("DEVTOOLS_MEMORY_EXPORT_SNAPSHOT_COUNT");
+ histogram.add(1);
+}, "devtools/client/memory/telemetry#countExportSnapshot");
+
+const COARSE_TYPE = "Coarse Type";
+const ALLOCATION_STACK = "Allocation Stack";
+const INVERTED_ALLOCATION_STACK = "Inverted Allocation Stack";
+const CUSTOM = "Custom";
+
+/**
+ * @param {String|null} filter
+ * The filter string used, if any.
+ *
+ * @param {Boolean} diffing
+ * True if the census was a diffing census, false otherwise.
+ *
+ * @param {censusDisplayModel} display
+ * The display used with the census.
+ */
+exports.countCensus = makeInfallible(function ({ filter, diffing, display }) {
+ let histogram = telemetry.getHistogramById("DEVTOOLS_MEMORY_INVERTED_CENSUS");
+ histogram.add(!!display.inverted);
+
+ histogram = telemetry.getHistogramById("DEVTOOLS_MEMORY_FILTER_CENSUS");
+ histogram.add(!!filter);
+
+ histogram = telemetry.getHistogramById("DEVTOOLS_MEMORY_DIFF_CENSUS");
+ histogram.add(!!diffing);
+
+ histogram = telemetry.getKeyedHistogramById("DEVTOOLS_MEMORY_BREAKDOWN_CENSUS_COUNT");
+ if (display === censusDisplays.coarseType) {
+ histogram.add(COARSE_TYPE);
+ } else if (display === censusDisplays.allocationStack) {
+ histogram.add(ALLOCATION_STACK);
+ } else if (display === censusDisplays.invertedAllocationStack) {
+ histogram.add(INVERTED_ALLOCATION_STACK);
+ } else {
+ histogram.add(CUSTOM);
+ }
+}, "devtools/client/memory/telemetry#countCensus");
+
+/**
+ * @param {Object} opts
+ * The same parameters specified for countCensus.
+ */
+exports.countDiff = makeInfallible(function (opts) {
+ exports.countCensus(immutableUpdate(opts, { diffing: true }));
+}, "devtools/client/memory/telemetry#countDiff");
+
+/**
+ * @param {Object} display
+ * The display used to label nodes in the dominator tree.
+ */
+exports.countDominatorTree = makeInfallible(function ({ display }) {
+ let histogram = telemetry.getHistogramById("DEVTOOLS_MEMORY_DOMINATOR_TREE_COUNT");
+ histogram.add(1);
+
+ histogram = telemetry.getKeyedHistogramById("DEVTOOLS_MEMORY_BREAKDOWN_DOMINATOR_TREE_COUNT");
+ if (display === labelDisplays.coarseType) {
+ histogram.add(COARSE_TYPE);
+ } else if (display === labelDisplays.allocationStack) {
+ histogram.add(ALLOCATION_STACK);
+ } else {
+ histogram.add(CUSTOM);
+ }
+}, "devtools/client/memory/telemetry#countDominatorTree");
diff --git a/devtools/client/memory/test/browser/.eslintrc.js b/devtools/client/memory/test/browser/.eslintrc.js
new file mode 100644
index 000000000..698ae9181
--- /dev/null
+++ b/devtools/client/memory/test/browser/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../../../.eslintrc.mochitests.js"
+};
diff --git a/devtools/client/memory/test/browser/browser.ini b/devtools/client/memory/test/browser/browser.ini
new file mode 100644
index 000000000..dc803a335
--- /dev/null
+++ b/devtools/client/memory/test/browser/browser.ini
@@ -0,0 +1,31 @@
+[DEFAULT]
+tags = devtools devtools-memory
+subsuite = devtools
+support-files =
+ head.js
+ doc_big_tree.html
+ doc_empty.html
+ doc_steady_allocation.html
+ !/devtools/client/framework/test/shared-head.js
+ !/devtools/client/framework/test/shared-redux-head.js
+
+[browser_memory_allocationStackDisplay_01.js]
+ skip-if = debug # bug 1219554
+[browser_memory_displays_01.js]
+[browser_memory_clear_snapshots.js]
+[browser_memory_diff_01.js]
+[browser_memory_dominator_trees_01.js]
+[browser_memory_dominator_trees_02.js]
+[browser_memory_filter_01.js]
+[browser_memory_individuals_01.js]
+[browser_memory_keyboard.js]
+[browser_memory_keyboard-snapshot-list.js]
+[browser_memory_no_allocation_stacks.js]
+[browser_memory_no_auto_expand.js]
+ skip-if = debug # bug 1219554
+[browser_memory_percents_01.js]
+[browser_memory_refresh_does_not_leak.js]
+[browser_memory_simple_01.js]
+[browser_memory_transferHeapSnapshot_e10s_01.js]
+[browser_memory_tree_map-01.js]
+[browser_memory_tree_map-02.js]
diff --git a/devtools/client/memory/test/browser/browser_memory_allocationStackDisplay_01.js b/devtools/client/memory/test/browser/browser_memory_allocationStackDisplay_01.js
new file mode 100644
index 000000000..60cd5c456
--- /dev/null
+++ b/devtools/client/memory/test/browser/browser_memory_allocationStackDisplay_01.js
@@ -0,0 +1,39 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Sanity test that we can show allocation stack displays in the tree.
+
+"use strict";
+
+const { toggleRecordingAllocationStacks } = require("devtools/client/memory/actions/allocations");
+const { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot");
+const censusDisplayActions = require("devtools/client/memory/actions/census-display");
+const { viewState } = require("devtools/client/memory/constants");
+const { changeView } = require("devtools/client/memory/actions/view");
+
+const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
+
+this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
+ const heapWorker = panel.panelWin.gHeapAnalysesClient;
+ const front = panel.panelWin.gFront;
+ const { getState, dispatch } = panel.panelWin.gStore;
+ const doc = panel.panelWin.document;
+
+ dispatch(changeView(viewState.CENSUS));
+
+ dispatch(censusDisplayActions.setCensusDisplay(censusDisplays.invertedAllocationStack));
+ is(getState().censusDisplay.breakdown.by, "allocationStack");
+
+ yield dispatch(toggleRecordingAllocationStacks(front));
+ ok(getState().allocations.recording);
+
+ // Let some allocations build up.
+ yield waitForTime(500);
+
+ yield dispatch(takeSnapshotAndCensus(front, heapWorker));
+
+ const names = [...doc.querySelectorAll(".frame-link-function-display-name")];
+ ok(names.length, "Should have rendered some allocation stack tree items");
+ ok(names.some(e => !!e.textContent.trim()),
+ "And at least some of them should have functionDisplayNames");
+});
diff --git a/devtools/client/memory/test/browser/browser_memory_clear_snapshots.js b/devtools/client/memory/test/browser/browser_memory_clear_snapshots.js
new file mode 100644
index 000000000..ca577bcd2
--- /dev/null
+++ b/devtools/client/memory/test/browser/browser_memory_clear_snapshots.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests taking and then clearing snapshots.
+ */
+
+ const { treeMapState } = require("devtools/client/memory/constants");
+ const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
+
+ this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
+ const { gStore, document } = panel.panelWin;
+ const { getState, dispatch } = gStore;
+
+ let snapshotEls = document.querySelectorAll("#memory-tool-container .list li");
+ is(getState().snapshots.length, 0, "Starts with no snapshots in store");
+ is(snapshotEls.length, 0, "No snapshots visible");
+
+ info("Take two snapshots");
+ takeSnapshot(panel.panelWin);
+ takeSnapshot(panel.panelWin);
+ yield waitUntilState(gStore, state =>
+ state.snapshots.length === 2 &&
+ state.snapshots[0].treeMap && state.snapshots[1].treeMap &&
+ state.snapshots[0].treeMap.state === treeMapState.SAVED &&
+ state.snapshots[1].treeMap.state === treeMapState.SAVED);
+
+ snapshotEls = document.querySelectorAll("#memory-tool-container .list li");
+ is(snapshotEls.length, 2, "Two snapshots visible");
+
+ info("Click on Clear Snapshots");
+ yield clearSnapshots(panel.panelWin);
+ is(getState().snapshots.length, 0, "No snapshots in store");
+ snapshotEls = document.querySelectorAll("#memory-tool-container .list li");
+ is(snapshotEls.length, 0, "No snapshot visible");
+ });
diff --git a/devtools/client/memory/test/browser/browser_memory_diff_01.js b/devtools/client/memory/test/browser/browser_memory_diff_01.js
new file mode 100644
index 000000000..0deb2a078
--- /dev/null
+++ b/devtools/client/memory/test/browser/browser_memory_diff_01.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test diffing.
+
+"use strict";
+
+const {
+ snapshotState,
+ diffingState,
+ treeMapState
+} = require("devtools/client/memory/constants");
+
+const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
+
+this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
+ const heapWorker = panel.panelWin.gHeapAnalysesClient;
+ const front = panel.panelWin.gFront;
+ const store = panel.panelWin.gStore;
+ const { getState, dispatch } = store;
+ const doc = panel.panelWin.document;
+
+ ok(!getState().diffing, "Not diffing by default.");
+
+ // Take two snapshots.
+ const takeSnapshotButton = doc.getElementById("take-snapshot");
+ EventUtils.synthesizeMouseAtCenter(takeSnapshotButton, {}, panel.panelWin);
+ yield waitForTime(1000);
+ EventUtils.synthesizeMouseAtCenter(takeSnapshotButton, {}, panel.panelWin);
+
+ // Enable diffing mode.
+ const diffButton = doc.getElementById("diff-snapshots");
+ EventUtils.synthesizeMouseAtCenter(diffButton, {}, panel.panelWin);
+ yield waitUntilState(store,
+ state =>
+ !!state.diffing &&
+ state.diffing.state === diffingState.SELECTING);
+ ok(true, "Clicking the diffing button put us into the diffing state.");
+ is(getDisplayedSnapshotStatus(doc), "Select the baseline snapshot");
+
+ yield waitUntilState(store, state =>
+ state.snapshots.length === 2 &&
+ state.snapshots[0].treeMap && state.snapshots[1].treeMap &&
+ state.snapshots[0].treeMap.state === treeMapState.SAVED &&
+ state.snapshots[1].treeMap.state === treeMapState.SAVED);
+
+ const listItems = [...doc.querySelectorAll(".snapshot-list-item")];
+ is(listItems.length, 2, "Should have two snapshot list items");
+
+ // Select the first snapshot.
+ EventUtils.synthesizeMouseAtCenter(listItems[0], {}, panel.panelWin);
+ yield waitUntilState(store,
+ state =>
+ state.diffing.state === diffingState.SELECTING &&
+ state.diffing.firstSnapshotId);
+ is(getDisplayedSnapshotStatus(doc),
+ "Select the snapshot to compare to the baseline");
+
+ // Select the second snapshot.
+ EventUtils.synthesizeMouseAtCenter(listItems[1], {}, panel.panelWin);
+ yield waitUntilState(store,
+ state =>
+ state.diffing.state === diffingState.TAKING_DIFF);
+ ok(true, "Selecting two snapshots for diffing triggers computing the diff");
+
+ // .startsWith because the ellipsis is lost in translation.
+ ok(getDisplayedSnapshotStatus(doc).startsWith("Computing difference"));
+
+ yield waitUntilState(store,
+ state => state.diffing.state === diffingState.TOOK_DIFF);
+ ok(true, "And that diff is computed successfully");
+ is(getDisplayedSnapshotStatus(doc), null, "No status text anymore");
+ ok(doc.querySelector(".heap-tree-item"), "And instead we should be showing the tree");
+});
diff --git a/devtools/client/memory/test/browser/browser_memory_displays_01.js b/devtools/client/memory/test/browser/browser_memory_displays_01.js
new file mode 100644
index 000000000..b5f9e34d9
--- /dev/null
+++ b/devtools/client/memory/test/browser/browser_memory_displays_01.js
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the heap tree renders rows based on the display
+ */
+
+const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
+const { viewState, censusState } = require("devtools/client/memory/constants");
+const { changeView } = require("devtools/client/memory/actions/view");
+
+this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
+ const { gStore, document } = panel.panelWin;
+
+ const { dispatch } = panel.panelWin.gStore;
+
+ function $$(selector) {
+ return [...document.querySelectorAll(selector)];
+ }
+ dispatch(changeView(viewState.CENSUS));
+
+ yield takeSnapshot(panel.panelWin);
+
+ yield waitUntilState(gStore, state =>
+ state.snapshots[0].census &&
+ state.snapshots[0].census.state === censusState.SAVED);
+
+ info("Check coarse type heap view");
+ ["Function", "js::Shape", "Object", "strings"].forEach(findNameCell);
+
+ yield setCensusDisplay(panel.panelWin, censusDisplays.allocationStack);
+ info("Check allocation stack heap view");
+ [L10N.getStr("tree-item.nostack")].forEach(findNameCell);
+
+ function findNameCell(name) {
+ const el = $$(".tree .heap-tree-item-name span")
+ .find(e => e.textContent === name);
+ ok(el, `Found heap tree item cell for ${name}.`);
+ }
+});
diff --git a/devtools/client/memory/test/browser/browser_memory_dominator_trees_01.js b/devtools/client/memory/test/browser/browser_memory_dominator_trees_01.js
new file mode 100644
index 000000000..3380d6e21
--- /dev/null
+++ b/devtools/client/memory/test/browser/browser_memory_dominator_trees_01.js
@@ -0,0 +1,147 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Sanity test for dominator trees, their focused nodes, and keyboard navigating
+// through nodes across incrementally fetching subtrees.
+
+"use strict";
+
+const {
+ dominatorTreeState,
+ viewState,
+} = require("devtools/client/memory/constants");
+const {
+ expandDominatorTreeNode,
+} = require("devtools/client/memory/actions/snapshot");
+const { changeView } = require("devtools/client/memory/actions/view");
+
+const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_big_tree.html";
+
+this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
+ // Taking snapshots and computing dominator trees is slow :-/
+ requestLongerTimeout(4);
+
+ const store = panel.panelWin.gStore;
+ const { getState, dispatch } = store;
+ const doc = panel.panelWin.document;
+
+ dispatch(changeView(viewState.DOMINATOR_TREE));
+
+ // Take a snapshot.
+
+ const takeSnapshotButton = doc.getElementById("take-snapshot");
+ EventUtils.synthesizeMouseAtCenter(takeSnapshotButton, {}, panel.panelWin);
+
+ // Wait for the dominator tree to be computed and fetched.
+
+ yield waitUntilDominatorTreeState(store, [dominatorTreeState.LOADED]);
+ ok(true, "Computed and fetched the dominator tree.");
+
+ // Expand all the dominator tree nodes that are eagerly fetched, except for
+ // the leaves which will trigger fetching their lazily loaded subtrees.
+
+ const id = getState().snapshots[0].id;
+ const root = getState().snapshots[0].dominatorTree.root;
+ (function expandAllEagerlyFetched(node = root) {
+ if (!node.moreChildrenAvailable || node.children) {
+ dispatch(expandDominatorTreeNode(id, node));
+ }
+
+ if (node.children) {
+ for (let child of node.children) {
+ expandAllEagerlyFetched(child);
+ }
+ }
+ }());
+
+ // Find the deepest eagerly loaded node: one which has more children but none
+ // of them are loaded.
+
+ const deepest = (function findDeepest(node = root) {
+ if (node.moreChildrenAvailable && !node.children) {
+ return node;
+ }
+
+ if (node.children) {
+ for (let child of node.children) {
+ const found = findDeepest(child);
+ if (found) {
+ return found;
+ }
+ }
+ }
+
+ return null;
+ }());
+
+ ok(deepest, "Found the deepest node");
+ ok(!getState().snapshots[0].dominatorTree.expanded.has(deepest.nodeId),
+ "The deepest node should not be expanded");
+
+ // Select the deepest node.
+
+ EventUtils.synthesizeMouseAtCenter(doc.querySelector(`.node-${deepest.nodeId}`),
+ {},
+ panel.panelWin);
+ yield waitUntilState(store, state =>
+ state.snapshots[0].dominatorTree.focused.nodeId === deepest.nodeId);
+ ok(doc.querySelector(`.node-${deepest.nodeId}`).classList.contains("focused"),
+ "The deepest node should be focused now");
+
+ // Expand the deepest node, which triggers an incremental fetch of its lazily
+ // loaded subtree.
+
+ EventUtils.synthesizeKey("VK_RIGHT", {}, panel.panelWin);
+ yield waitUntilState(store, state =>
+ state.snapshots[0].dominatorTree.expanded.has(deepest.nodeId));
+ is(getState().snapshots[0].dominatorTree.state,
+ dominatorTreeState.INCREMENTAL_FETCHING,
+ "Expanding the deepest node should start an incremental fetch of its subtree");
+ ok(doc.querySelector(`.node-${deepest.nodeId}`).classList.contains("focused"),
+ "The deepest node should still be focused after expansion");
+
+ // Wait for the incremental fetch to complete.
+
+ yield waitUntilState(store, state =>
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED);
+ ok(true, "And the incremental fetch completes.");
+ ok(doc.querySelector(`.node-${deepest.nodeId}`).classList.contains("focused"),
+ "The deepest node should still be focused after we have loaded its children");
+
+ // Find the most up-to-date version of the node whose children we just
+ // incrementally fetched.
+
+ const newDeepest = (function findNewDeepest(node = getState().snapshots[0].dominatorTree.root) {
+ if (node.nodeId === deepest.nodeId) {
+ return node;
+ }
+
+ if (node.children) {
+ for (let child of node.children) {
+ const found = findNewDeepest(child);
+ if (found) {
+ return found;
+ }
+ }
+ }
+
+ return null;
+ }());
+
+ ok(newDeepest, "We found the up-to-date version of deepest");
+ ok(newDeepest.children, "And its children are loaded");
+ ok(newDeepest.children.length, "And there are more than 0 children");
+
+ const firstChild = newDeepest.children[0];
+ ok(firstChild, "deepest should have a first child");
+ ok(doc.querySelector(`.node-${firstChild.nodeId}`),
+ "and the first child should exist in the dom");
+
+ // Select the newly loaded first child by pressing the right arrow once more.
+
+ EventUtils.synthesizeKey("VK_RIGHT", {}, panel.panelWin);
+ yield waitUntilState(store, state =>
+ state.snapshots[0].dominatorTree.focused === firstChild);
+ ok(doc.querySelector(`.node-${firstChild.nodeId}`).classList.contains("focused"),
+ "The first child should now be focused");
+});
diff --git a/devtools/client/memory/test/browser/browser_memory_dominator_trees_02.js b/devtools/client/memory/test/browser/browser_memory_dominator_trees_02.js
new file mode 100644
index 000000000..72bc52567
--- /dev/null
+++ b/devtools/client/memory/test/browser/browser_memory_dominator_trees_02.js
@@ -0,0 +1,64 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Integration test for mouse interaction in the dominator tree
+
+"use strict";
+
+const {
+ dominatorTreeState,
+ viewState,
+} = require("devtools/client/memory/constants");
+const { changeView } = require("devtools/client/memory/actions/view");
+
+const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
+
+function clickOnNodeArrow(node, panel) {
+ EventUtils.synthesizeMouseAtCenter(node.querySelector(".arrow"),
+ {}, panel.panelWin);
+}
+
+this.test = makeMemoryTest(TEST_URL, function* ({ panel }) {
+ // Taking snapshots and computing dominator trees is slow :-/
+ requestLongerTimeout(4);
+
+ const store = panel.panelWin.gStore;
+ const { getState, dispatch } = store;
+ const doc = panel.panelWin.document;
+
+ dispatch(changeView(viewState.DOMINATOR_TREE));
+
+ // Take a snapshot.
+ const takeSnapshotButton = doc.getElementById("take-snapshot");
+ EventUtils.synthesizeMouseAtCenter(takeSnapshotButton, {}, panel.panelWin);
+
+ // Wait for the dominator tree to be computed and fetched.
+ yield waitUntilState(store, state =>
+ state.snapshots[0] &&
+ state.snapshots[0].dominatorTree &&
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED);
+ ok(true, "Computed and fetched the dominator tree.");
+
+ const root = getState().snapshots[0].dominatorTree.root;
+ ok(getState().snapshots[0].dominatorTree.expanded.has(root.nodeId),
+ "Root node is expanded by default");
+
+ // Click on root arrow to collapse the root element
+ const rootNode = doc.querySelector(`.node-${root.nodeId}`);
+ clickOnNodeArrow(rootNode, panel);
+
+ yield waitUntilState(store, state =>
+ state.snapshots[0] &&
+ state.snapshots[0].dominatorTree &&
+ !state.snapshots[0].dominatorTree.expanded.has(root.nodeId));
+ ok(true, "Root node collapsed");
+
+ // Click on root arrow to expand it again
+ clickOnNodeArrow(rootNode, panel);
+
+ yield waitUntilState(store, state =>
+ state.snapshots[0] &&
+ state.snapshots[0].dominatorTree &&
+ state.snapshots[0].dominatorTree.expanded.has(root.nodeId));
+ ok(true, "Root node is expanded again");
+});
diff --git a/devtools/client/memory/test/browser/browser_memory_filter_01.js b/devtools/client/memory/test/browser/browser_memory_filter_01.js
new file mode 100644
index 000000000..448e2aaa9
--- /dev/null
+++ b/devtools/client/memory/test/browser/browser_memory_filter_01.js
@@ -0,0 +1,81 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Sanity test that we can show allocation stack displays in the tree.
+
+"use strict";
+
+const {
+ dominatorTreeState,
+ snapshotState,
+ viewState,
+ censusState,
+} = require("devtools/client/memory/constants");
+const { changeViewAndRefresh, changeView } = require("devtools/client/memory/actions/view");
+
+const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
+
+this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
+ const heapWorker = panel.panelWin.gHeapAnalysesClient;
+ const front = panel.panelWin.gFront;
+ const store = panel.panelWin.gStore;
+ const { getState, dispatch } = store;
+ const doc = panel.panelWin.document;
+
+ dispatch(changeView(viewState.CENSUS));
+
+ const takeSnapshotButton = doc.getElementById("take-snapshot");
+ EventUtils.synthesizeMouseAtCenter(takeSnapshotButton, {}, panel.panelWin);
+
+ yield waitUntilState(store, state =>
+ state.snapshots.length === 1 &&
+ state.snapshots[0].census &&
+ state.snapshots[0].census.state === censusState.SAVING);
+
+ let filterInput = doc.getElementById("filter");
+ EventUtils.synthesizeMouseAtCenter(filterInput, {}, panel.panelWin);
+ EventUtils.sendString("js::Shape", panel.panelWin);
+
+ yield waitUntilState(store, state =>
+ state.snapshots.length === 1 &&
+ state.snapshots[0].census &&
+ state.snapshots[0].census.state === censusState.SAVING);
+ ok(true, "adding a filter string should trigger census recompute");
+
+ yield waitUntilState(store, state =>
+ state.snapshots.length === 1 &&
+ state.snapshots[0].census &&
+ state.snapshots[0].census.state === censusState.SAVED);
+
+ let nameElem = doc.querySelector(".heap-tree-item-field.heap-tree-item-name");
+ ok(nameElem, "Should get a tree item row with a name");
+ is(nameElem.textContent.trim(), "js::Shape", "the tree item should be the one we filtered for");
+ is(filterInput.value, "js::Shape",
+ "and filter input contains the user value");
+
+ // Now switch the dominator view, then switch back to census view
+ // and check that the filter word is still correctly applied
+ dispatch(changeViewAndRefresh(viewState.DOMINATOR_TREE, heapWorker));
+ ok(true, "change view to dominator tree");
+
+ // Wait for the dominator tree to be computed and fetched.
+ yield waitUntilDominatorTreeState(store, [dominatorTreeState.LOADED]);
+ ok(true, "computed and fetched the dominator tree.");
+
+ dispatch(changeViewAndRefresh(viewState.CENSUS, heapWorker));
+ ok(true, "change view back to census");
+
+ yield waitUntilState(store, state =>
+ state.snapshots.length === 1 &&
+ state.snapshots[0].census &&
+ state.snapshots[0].census.state === censusState.SAVED);
+
+ nameElem = doc.querySelector(".heap-tree-item-field.heap-tree-item-name");
+ filterInput = doc.getElementById("filter");
+
+ ok(nameElem, "Should still get a tree item row with a name");
+ is(nameElem.textContent.trim(), "js::Shape",
+ "the tree item should still be the one we filtered for");
+ is(filterInput.value, "js::Shape",
+ "and filter input still contains the user value");
+});
diff --git a/devtools/client/memory/test/browser/browser_memory_individuals_01.js b/devtools/client/memory/test/browser/browser_memory_individuals_01.js
new file mode 100644
index 000000000..eae8248c3
--- /dev/null
+++ b/devtools/client/memory/test/browser/browser_memory_individuals_01.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Sanity test that we can show census group individuals, and then go back to
+// the previous view.
+
+"use strict";
+
+const {
+ individualsState,
+ viewState,
+ censusState,
+} = require("devtools/client/memory/constants");
+const { changeViewAndRefresh, changeView } = require("devtools/client/memory/actions/view");
+
+const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
+
+this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
+ const heapWorker = panel.panelWin.gHeapAnalysesClient;
+ const front = panel.panelWin.gFront;
+ const store = panel.panelWin.gStore;
+ const { getState, dispatch } = store;
+ const doc = panel.panelWin.document;
+
+ dispatch(changeView(viewState.CENSUS));
+
+ // Take a snapshot and wait for the census to finish.
+
+ const takeSnapshotButton = doc.getElementById("take-snapshot");
+ EventUtils.synthesizeMouseAtCenter(takeSnapshotButton, {}, panel.panelWin);
+
+ yield waitUntilState(store, state => {
+ return state.snapshots.length === 1 &&
+ state.snapshots[0].census &&
+ state.snapshots[0].census.state === censusState.SAVED;
+ });
+
+ // Click on the first individuals button found, and wait for the individuals
+ // to be fetched.
+
+ const individualsButton = doc.querySelector(".individuals-button");
+ EventUtils.synthesizeMouseAtCenter(individualsButton, {}, panel.panelWin);
+
+ yield waitUntilState(store, state => {
+ return state.view.state === viewState.INDIVIDUALS &&
+ state.individuals &&
+ state.individuals.state === individualsState.FETCHED;
+ });
+
+ ok(doc.getElementById("shortest-paths"),
+ "Should be showing the shortest paths component");
+ ok(doc.querySelector(".heap-tree-item"),
+ "Should be showing the individuals");
+
+ // Go back to the previous view.
+
+ const popViewButton = doc.getElementById("pop-view-button");
+ ok(popViewButton, "Should be showing the #pop-view-button");
+ EventUtils.synthesizeMouseAtCenter(popViewButton, {}, panel.panelWin);
+
+ yield waitUntilState(store, state => {
+ return state.view.state === viewState.CENSUS;
+ });
+
+ ok(!doc.getElementById("shortest-paths"),
+ "Should not be showing the shortest paths component anymore");
+});
diff --git a/devtools/client/memory/test/browser/browser_memory_keyboard-snapshot-list.js b/devtools/client/memory/test/browser/browser_memory_keyboard-snapshot-list.js
new file mode 100644
index 000000000..eb7cb8aca
--- /dev/null
+++ b/devtools/client/memory/test/browser/browser_memory_keyboard-snapshot-list.js
@@ -0,0 +1,99 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that using ACCEL+UP/DOWN, the user can navigate between snapshots.
+
+"use strict";
+
+const {
+ snapshotState,
+ viewState,
+ censusState
+} = require("devtools/client/memory/constants");
+const {
+ takeSnapshotAndCensus
+} = require("devtools/client/memory/actions/snapshot");
+const { changeView } = require("devtools/client/memory/actions/view");
+
+const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
+
+this.test = makeMemoryTest(TEST_URL, function* ({ panel }) {
+ // Creating snapshots already takes ~25 seconds on linux 32 debug machines
+ // which makes the test very likely to go over the allowed timeout
+ requestLongerTimeout(2);
+
+ const heapWorker = panel.panelWin.gHeapAnalysesClient;
+ const front = panel.panelWin.gFront;
+ const store = panel.panelWin.gStore;
+ const { dispatch } = store;
+ const doc = panel.panelWin.document;
+
+ dispatch(changeView(viewState.CENSUS));
+
+ info("Take 3 snapshots");
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+
+ yield waitUntilState(store, state =>
+ state.snapshots.length == 3 &&
+ state.snapshots.every(s => s.census && s.census.state === censusState.SAVED));
+ ok(true, "All snapshots censuses are in SAVED state");
+
+ yield waitUntilSnapshotSelected(store, 2);
+ ok(true, "Third snapshot selected after creating all snapshots.");
+
+ info("Press ACCEL+UP key, expect second snapshot selected.");
+ EventUtils.synthesizeKey("VK_UP", { accelKey: true }, panel.panelWin);
+ yield waitUntilSnapshotSelected(store, 1);
+ ok(true, "Second snapshot selected after alt+UP.");
+
+ info("Press ACCEL+UP key, expect first snapshot selected.");
+ EventUtils.synthesizeKey("VK_UP", { accelKey: true }, panel.panelWin);
+ yield waitUntilSnapshotSelected(store, 0);
+ ok(true, "First snapshot is selected after ACCEL+UP");
+
+ info("Check ACCEL+UP is a noop when the first snapshot is selected.");
+ EventUtils.synthesizeKey("VK_UP", { accelKey: true }, panel.panelWin);
+ // We assume the snapshot selection should be synchronous here.
+ is(getSelectedSnapshotIndex(store), 0, "First snapshot is still selected");
+
+ info("Press ACCEL+DOWN key, expect second snapshot selected.");
+ EventUtils.synthesizeKey("VK_DOWN", { accelKey: true }, panel.panelWin);
+ yield waitUntilSnapshotSelected(store, 1);
+ ok(true, "Second snapshot is selected after ACCEL+DOWN");
+
+ info("Click on first node.");
+ let firstNode = doc.querySelector(".tree .heap-tree-item-name");
+ EventUtils.synthesizeMouseAtCenter(firstNode, {}, panel.panelWin);
+ yield waitUntilState(store, state => state.snapshots[1].census.focused ===
+ state.snapshots[1].census.report.children[0]
+ );
+ ok(true, "First root is selected after click.");
+
+ info("Press DOWN key, expect second root focused.");
+ EventUtils.synthesizeKey("VK_DOWN", {}, panel.panelWin);
+ yield waitUntilState(store, state => state.snapshots[1].census.focused ===
+ state.snapshots[1].census.report.children[1]
+ );
+ ok(true, "Second root is selected after pressing DOWN.");
+ is(getSelectedSnapshotIndex(store), 1, "Second snapshot is still selected");
+
+ info("Press UP key, expect second root focused.");
+ EventUtils.synthesizeKey("VK_UP", {}, panel.panelWin);
+ yield waitUntilState(store, state => state.snapshots[1].census.focused ===
+ state.snapshots[1].census.report.children[0]
+ );
+ ok(true, "First root is selected after pressing UP.");
+ is(getSelectedSnapshotIndex(store), 1, "Second snapshot is still selected");
+
+ info("Press ACCEL+DOWN key, expect third snapshot selected.");
+ EventUtils.synthesizeKey("VK_DOWN", { accelKey: true }, panel.panelWin);
+ yield waitUntilSnapshotSelected(store, 2);
+ ok(true, "Thirdˆ snapshot is selected after ACCEL+DOWN");
+
+ info("Check ACCEL+DOWN is a noop when the last snapshot is selected.");
+ EventUtils.synthesizeKey("VK_DOWN", { accelKey: true }, panel.panelWin);
+ // We assume the snapshot selection should be synchronous here.
+ is(getSelectedSnapshotIndex(store), 2, "Third snapshot is still selected");
+});
diff --git a/devtools/client/memory/test/browser/browser_memory_keyboard.js b/devtools/client/memory/test/browser/browser_memory_keyboard.js
new file mode 100644
index 000000000..243570f83
--- /dev/null
+++ b/devtools/client/memory/test/browser/browser_memory_keyboard.js
@@ -0,0 +1,107 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Bug 1246570 - Check that when pressing on LEFT arrow, the parent tree node
+// gets focused.
+
+"use strict";
+
+const {
+ snapshotState,
+ censusState,
+ viewState
+} = require("devtools/client/memory/constants");
+const {
+ takeSnapshotAndCensus
+} = require("devtools/client/memory/actions/snapshot");
+const { changeView } = require("devtools/client/memory/actions/view");
+
+const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
+
+function waitUntilFocused(store, node) {
+ return waitUntilState(store, state =>
+ state.snapshots.length === 1 &&
+ state.snapshots[0].census &&
+ state.snapshots[0].census.state === censusState.SAVED &&
+ state.snapshots[0].census.focused &&
+ state.snapshots[0].census.focused === node
+ );
+}
+
+function waitUntilExpanded(store, node) {
+ return waitUntilState(store, state =>
+ state.snapshots[0] &&
+ state.snapshots[0].census &&
+ state.snapshots[0].census.expanded.has(node.id));
+}
+
+this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
+ const heapWorker = panel.panelWin.gHeapAnalysesClient;
+ const front = panel.panelWin.gFront;
+ const store = panel.panelWin.gStore;
+ const { getState, dispatch } = store;
+ const doc = panel.panelWin.document;
+
+ dispatch(changeView(viewState.CENSUS));
+
+ is(getState().censusDisplay.breakdown.by, "coarseType");
+
+ yield dispatch(takeSnapshotAndCensus(front, heapWorker));
+ let census = getState().snapshots[0].census;
+ let root1 = census.report.children[0];
+ let root2 = census.report.children[0];
+ let root3 = census.report.children[0];
+ let root4 = census.report.children[0];
+ let child1 = root1.children[0];
+
+ info("Click on first node.");
+ let firstNode = doc.querySelector(".tree .heap-tree-item-name");
+ EventUtils.synthesizeMouseAtCenter(firstNode, {}, panel.panelWin);
+ yield waitUntilFocused(store, root1);
+ ok(true, "First root is selected after click.");
+
+ info("Press DOWN key, expect second root focused.");
+ EventUtils.synthesizeKey("VK_DOWN", {}, panel.panelWin);
+ yield waitUntilFocused(store, root2);
+ ok(true, "Second root is selected after pressing DOWN arrow.");
+
+ info("Press DOWN key, expect third root focused.");
+ EventUtils.synthesizeKey("VK_DOWN", {}, panel.panelWin);
+ yield waitUntilFocused(store, root3);
+ ok(true, "Third root is selected after pressing DOWN arrow.");
+
+ info("Press DOWN key, expect fourth root focused.");
+ EventUtils.synthesizeKey("VK_DOWN", {}, panel.panelWin);
+ yield waitUntilFocused(store, root4);
+ ok(true, "Fourth root is selected after pressing DOWN arrow.");
+
+ info("Press UP key, expect third root focused.");
+ EventUtils.synthesizeKey("VK_UP", {}, panel.panelWin);
+ yield waitUntilFocused(store, root3);
+ ok(true, "Third root is selected after pressing UP arrow.");
+
+ info("Press UP key, expect second root focused.");
+ EventUtils.synthesizeKey("VK_UP", {}, panel.panelWin);
+ yield waitUntilFocused(store, root2);
+ ok(true, "Second root is selected after pressing UP arrow.");
+
+ info("Press UP key, expect first root focused.");
+ EventUtils.synthesizeKey("VK_UP", {}, panel.panelWin);
+ yield waitUntilFocused(store, root1);
+ ok(true, "First root is selected after pressing UP arrow.");
+
+ info("Press RIGHT key");
+ EventUtils.synthesizeKey("VK_RIGHT", {}, panel.panelWin);
+ yield waitUntilExpanded(store, root1);
+ ok(true, "Root node is expanded.");
+
+ info("Press RIGHT key, expect first child focused.");
+ EventUtils.synthesizeKey("VK_RIGHT", {}, panel.panelWin);
+ yield waitUntilFocused(store, child1);
+ ok(true, "First child is selected after pressing RIGHT arrow.");
+
+ info("Press LEFT key, expect first root focused.");
+ EventUtils.synthesizeKey("VK_LEFT", {}, panel.panelWin);
+ yield waitUntilFocused(store, root1);
+ ok(true, "First root is selected after pressing LEFT arrow.");
+});
diff --git a/devtools/client/memory/test/browser/browser_memory_no_allocation_stacks.js b/devtools/client/memory/test/browser/browser_memory_no_allocation_stacks.js
new file mode 100644
index 000000000..cd8770285
--- /dev/null
+++ b/devtools/client/memory/test/browser/browser_memory_no_allocation_stacks.js
@@ -0,0 +1,39 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Sanity test that we can show allocation stack displays in the tree.
+
+"use strict";
+
+const { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot");
+const censusDisplayActions = require("devtools/client/memory/actions/census-display");
+const { viewState } = require("devtools/client/memory/constants");
+const { changeView } = require("devtools/client/memory/actions/view");
+
+const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
+
+this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
+ const heapWorker = panel.panelWin.gHeapAnalysesClient;
+ const front = panel.panelWin.gFront;
+ const { getState, dispatch } = panel.panelWin.gStore;
+ const doc = panel.panelWin.document;
+
+ dispatch(changeView(viewState.CENSUS));
+
+ ok(!getState().allocations.recording,
+ "Should not be recording allocagtions");
+
+ yield dispatch(takeSnapshotAndCensus(front, heapWorker));
+ yield dispatch(censusDisplayActions.setCensusDisplayAndRefresh(
+ heapWorker,
+ censusDisplays.allocationStack));
+
+ is(getState().censusDisplay.breakdown.by, "allocationStack",
+ "Should be using allocation stack breakdown");
+
+ ok(!getState().allocations.recording,
+ "Should still not be recording allocagtions");
+
+ ok(doc.querySelector(".no-allocation-stacks"),
+ "Because we did not record allocations, the no-allocation-stack warning should be visible");
+});
diff --git a/devtools/client/memory/test/browser/browser_memory_no_auto_expand.js b/devtools/client/memory/test/browser/browser_memory_no_auto_expand.js
new file mode 100644
index 000000000..ffd481c74
--- /dev/null
+++ b/devtools/client/memory/test/browser/browser_memory_no_auto_expand.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Bug 1221150 - Ensure that census trees do not accidentally auto expand
+// when clicking on the allocation stacks checkbox.
+
+"use strict";
+
+const { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot");
+const { viewState } = require("devtools/client/memory/constants");
+const { changeView } = require("devtools/client/memory/actions/view");
+
+const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
+
+this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
+ const heapWorker = panel.panelWin.gHeapAnalysesClient;
+ const front = panel.panelWin.gFront;
+ const { getState, dispatch } = panel.panelWin.gStore;
+ const doc = panel.panelWin.document;
+
+ dispatch(changeView(viewState.CENSUS));
+
+ yield dispatch(takeSnapshotAndCensus(front, heapWorker));
+
+ is(getState().allocations.recording, false);
+ const recordingCheckbox = doc.getElementById("record-allocation-stacks-checkbox");
+ EventUtils.synthesizeMouseAtCenter(recordingCheckbox, {}, panel.panelWin);
+ is(getState().allocations.recording, true);
+
+ const nameElems = [...doc.querySelectorAll(".heap-tree-item-field.heap-tree-item-name")];
+
+ for (let el of nameElems) {
+ dumpn(`Found ${el.textContent.trim()}`);
+ is(el.style.marginInlineStart, "0px",
+ "None of the elements should be an indented/expanded child");
+ }
+});
diff --git a/devtools/client/memory/test/browser/browser_memory_percents_01.js b/devtools/client/memory/test/browser/browser_memory_percents_01.js
new file mode 100644
index 000000000..c3ed37530
--- /dev/null
+++ b/devtools/client/memory/test/browser/browser_memory_percents_01.js
@@ -0,0 +1,47 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Sanity test that we calculate percentages in the tree.
+
+"use strict";
+
+const { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot");
+const { viewState } = require("devtools/client/memory/constants");
+const { changeView } = require("devtools/client/memory/actions/view");
+
+const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
+
+function checkCells(cells) {
+ ok(cells.length > 1, "Should have found some");
+ // Ignore the first header cell.
+ for (let cell of cells.slice(1)) {
+ const percent = cell.querySelector(".heap-tree-percent");
+ ok(percent, "should have a percent cell");
+ ok(percent.textContent.match(/^\d?\d%$/), "should be of the form nn% or n%");
+ }
+}
+
+this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
+ const heapWorker = panel.panelWin.gHeapAnalysesClient;
+ const front = panel.panelWin.gFront;
+ const { getState, dispatch } = panel.panelWin.gStore;
+ const doc = panel.panelWin.document;
+
+ dispatch(changeView(viewState.CENSUS));
+
+ yield dispatch(takeSnapshotAndCensus(front, heapWorker));
+ is(getState().censusDisplay.breakdown.by, "coarseType",
+ "Should be using coarse type breakdown");
+
+ const bytesCells = [...doc.querySelectorAll(".heap-tree-item-bytes")];
+ checkCells(bytesCells);
+
+ const totalBytesCells = [...doc.querySelectorAll(".heap-tree-item-total-bytes")];
+ checkCells(totalBytesCells);
+
+ const countCells = [...doc.querySelectorAll(".heap-tree-item-count")];
+ checkCells(countCells);
+
+ const totalCountCells = [...doc.querySelectorAll(".heap-tree-item-total-count")];
+ checkCells(totalCountCells);
+});
diff --git a/devtools/client/memory/test/browser/browser_memory_refresh_does_not_leak.js b/devtools/client/memory/test/browser/browser_memory_refresh_does_not_leak.js
new file mode 100644
index 000000000..7ab768b01
--- /dev/null
+++ b/devtools/client/memory/test/browser/browser_memory_refresh_does_not_leak.js
@@ -0,0 +1,108 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that refreshing the page with devtools open does not leak the old
+// windows from previous navigations.
+//
+// IF THIS TEST STARTS FAILING, YOU ARE LEAKING EVERY WINDOW EVER NAVIGATED TO
+// WHILE DEVTOOLS ARE OPEN! THIS IS NOT SPECIFIC TO THE MEMORY TOOL ONLY!
+
+"use strict";
+
+const HeapSnapshotFileUtils = require("devtools/shared/heapsnapshot/HeapSnapshotFileUtils");
+const { getLabelAndShallowSize } = require("devtools/shared/heapsnapshot/DominatorTreeNode");
+
+const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_empty.html";
+
+function* getWindowsInSnapshot(front) {
+ dumpn("Taking snapshot.");
+ const path = yield front.saveHeapSnapshot();
+ dumpn("Took snapshot with path = " + path);
+ const snapshot = ChromeUtils.readHeapSnapshot(path);
+ dumpn("Read snapshot into memory, taking census.");
+ const report = snapshot.takeCensus({
+ breakdown: {
+ by: "objectClass",
+ then: { by: "bucket" },
+ other: { by: "count", count: true, bytes: false },
+ }
+ });
+ dumpn("Took census, window count = " + report.Window.count);
+ return report.Window;
+}
+
+const DESCRIPTION = {
+ by: "coarseType",
+ objects: {
+ by: "objectClass",
+ then: { by: "count", count: true, bytes: false },
+ other: { by: "count", count: true, bytes: false },
+ },
+ strings: { by: "count", count: true, bytes: false },
+ scripts: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: false },
+ },
+ other: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: false },
+ }
+};
+
+this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
+ const heapWorker = panel.panelWin.gHeapAnalysesClient;
+ const front = panel.panelWin.gFront;
+ const store = panel.panelWin.gStore;
+ const { getState, dispatch } = store;
+ const doc = panel.panelWin.document;
+
+ const startWindows = yield getWindowsInSnapshot(front);
+ dumpn("Initial windows found = " + startWindows.map(w => "0x" + w.toString(16)).join(", "));
+ is(startWindows.length, 1);
+
+ yield refreshTab(tab);
+
+ const endWindows = yield getWindowsInSnapshot(front);
+ is(endWindows.length, 1);
+
+ if (endWindows.length === 1) {
+ return;
+ }
+
+ dumpn("Test failed, diagnosing leaking windows.");
+ dumpn("(This may fail if a moving GC has relocated the initial Window objects.)");
+
+ dumpn("Taking full runtime snapshot.");
+ const path = yield front.saveHeapSnapshot({ boundaries: { runtime: true } });
+ dumpn("Full runtime's snapshot path = " + path);
+
+ dumpn("Reading full runtime heap snapshot.");
+ const snapshot = ChromeUtils.readHeapSnapshot(path);
+ dumpn("Done reading full runtime heap snapshot.");
+
+ const dominatorTree = snapshot.computeDominatorTree();
+ const paths = snapshot.computeShortestPaths(dominatorTree.root, startWindows, 50);
+
+ for (let i = 0; i < startWindows.length; i++) {
+ dumpn("Shortest retaining paths for leaking Window 0x" + startWindows[i].toString(16) + " =========================");
+ let j = 0;
+ for (let retainingPath of paths.get(startWindows[i])) {
+ if (retainingPath.find(part => part.predecessor === startWindows[i])) {
+ // Skip paths that loop out from the target window and back to it again.
+ continue;
+ }
+
+ dumpn(" Path #" + (++j) + ": --------------------------------------------------------------------");
+ for (let part of retainingPath) {
+ const { label } = getLabelAndShallowSize(part.predecessor, snapshot, DESCRIPTION);
+ dumpn(" 0x" + part.predecessor.toString(16) +
+ " (" + label.join(" > ") + ")");
+ dumpn(" |");
+ dumpn(" " + part.edge);
+ dumpn(" |");
+ dumpn(" V");
+ }
+ dumpn(" 0x" + startWindows[i].toString(16) + " (objects > Window)");
+ }
+ }
+});
diff --git a/devtools/client/memory/test/browser/browser_memory_simple_01.js b/devtools/client/memory/test/browser/browser_memory_simple_01.js
new file mode 100644
index 000000000..9eaea2ad2
--- /dev/null
+++ b/devtools/client/memory/test/browser/browser_memory_simple_01.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests taking snapshots and default states.
+ */
+
+const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
+const { viewState, censusState } = require("devtools/client/memory/constants");
+const { changeView } = require("devtools/client/memory/actions/view");
+
+this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
+ const { gStore, document } = panel.panelWin;
+ const { getState, dispatch } = gStore;
+
+ dispatch(changeView(viewState.CENSUS));
+
+ let snapshotEls = document.querySelectorAll("#memory-tool-container .list li");
+ is(getState().snapshots.length, 0, "Starts with no snapshots in store");
+ is(snapshotEls.length, 0, "No snapshots rendered");
+
+ yield takeSnapshot(panel.panelWin);
+ snapshotEls = document.querySelectorAll("#memory-tool-container .list li");
+ is(getState().snapshots.length, 1, "One snapshot was created in store");
+ is(snapshotEls.length, 1, "One snapshot was rendered");
+ ok(snapshotEls[0].classList.contains("selected"), "Only snapshot has `selected` class");
+
+ yield takeSnapshot(panel.panelWin);
+ snapshotEls = document.querySelectorAll("#memory-tool-container .list li");
+ is(getState().snapshots.length, 2, "Two snapshots created in store");
+ is(snapshotEls.length, 2, "Two snapshots rendered");
+ ok(!snapshotEls[0].classList.contains("selected"), "First snapshot no longer has `selected` class");
+ ok(snapshotEls[1].classList.contains("selected"), "Second snapshot has `selected` class");
+
+ yield waitUntilCensusState(gStore, s => s.census, [censusState.SAVED,
+ censusState.SAVED]);
+
+ ok(document.querySelector(".heap-tree-item-name"),
+ "Should have rendered some tree items");
+});
diff --git a/devtools/client/memory/test/browser/browser_memory_transferHeapSnapshot_e10s_01.js b/devtools/client/memory/test/browser/browser_memory_transferHeapSnapshot_e10s_01.js
new file mode 100644
index 000000000..8ebb7622a
--- /dev/null
+++ b/devtools/client/memory/test/browser/browser_memory_transferHeapSnapshot_e10s_01.js
@@ -0,0 +1,28 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that we can save a heap snapshot and transfer it over the RDP in e10s
+// where the child process is sandboxed and so we have to use
+// HeapSnapshotFileActor to get the heap snapshot file.
+
+"use strict";
+
+const TEST_URL = "data:text/html,<html><body></body></html>";
+
+this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
+ const memoryFront = panel.panelWin.gFront;
+ ok(memoryFront, "Should get the MemoryFront");
+
+ const snapshotFilePath = yield memoryFront.saveHeapSnapshot({
+ // Force a copy so that we go through the HeapSnapshotFileActor's
+ // transferHeapSnapshot request and exercise this code path on e10s.
+ forceCopy: true
+ });
+
+ ok(!!(yield OS.File.stat(snapshotFilePath)),
+ "Should have the heap snapshot file");
+
+ const snapshot = ChromeUtils.readHeapSnapshot(snapshotFilePath);
+ ok(snapshot instanceof HeapSnapshot,
+ "And we should be able to read a HeapSnapshot instance from the file");
+});
diff --git a/devtools/client/memory/test/browser/browser_memory_tree_map-01.js b/devtools/client/memory/test/browser/browser_memory_tree_map-01.js
new file mode 100644
index 000000000..ca564a658
--- /dev/null
+++ b/devtools/client/memory/test/browser/browser_memory_tree_map-01.js
@@ -0,0 +1,102 @@
+/* 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/. */
+
+// Make sure the canvases are created correctly
+
+"use strict";
+
+const CanvasUtils = require("devtools/client/memory/components/tree-map/canvas-utils");
+const D3_SCRIPT = '<script type="application/javascript" ' +
+ 'src="chrome://devtools/content/shared/vendor/d3.js>';
+const TEST_URL = `data:text/html,<html><body>${D3_SCRIPT}</body></html>`;
+
+this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
+ const document = panel.panelWin.document;
+ const window = panel.panelWin;
+ const div = document.createElement("div");
+
+ Object.assign(div.style, {
+ width: "100px",
+ height: "200px",
+ position: "absolute"
+ });
+
+ document.body.appendChild(div);
+
+ info("Create the canvases");
+
+ let canvases = new CanvasUtils(div, 0);
+
+ info("Test the shape of the returned object");
+
+ is(typeof canvases, "object", "Canvases create an object");
+ is(typeof canvases.emit, "function", "Decorated with an EventEmitter");
+ is(typeof canvases.on, "function", "Decorated with an EventEmitter");
+ is(div.children[0], canvases.container, "Div has the container");
+ ok(canvases.main.canvas instanceof window.HTMLCanvasElement,
+ "Creates the main canvas");
+ ok(canvases.zoom.canvas instanceof window.HTMLCanvasElement,
+ "Creates the zoom canvas");
+ ok(canvases.main.ctx instanceof window.CanvasRenderingContext2D,
+ "Creates the main canvas context");
+ ok(canvases.zoom.ctx instanceof window.CanvasRenderingContext2D,
+ "Creates the zoom canvas context");
+
+ info("Test resizing");
+
+ let timesResizeCalled = 0;
+ canvases.on("resize", function () {
+ timesResizeCalled++;
+ });
+
+ let main = canvases.main.canvas;
+ let zoom = canvases.zoom.canvas;
+ let ratio = window.devicePixelRatio;
+
+ is(main.width, 100 * ratio,
+ "Main canvas width is the same as the parent div");
+ is(main.height, 200 * ratio,
+ "Main canvas height is the same as the parent div");
+ is(zoom.width, 100 * ratio,
+ "Zoom canvas width is the same as the parent div");
+ is(zoom.height, 200 * ratio,
+ "Zoom canvas height is the same as the parent div");
+ is(timesResizeCalled, 0,
+ "Resize was not emitted");
+
+ div.style.width = "500px";
+ div.style.height = "700px";
+
+ window.dispatchEvent(new Event("resize"));
+
+ is(main.width, 500 * ratio,
+ "Main canvas width is resized to be the same as the parent div");
+ is(main.height, 700 * ratio,
+ "Main canvas height is resized to be the same as the parent div");
+ is(zoom.width, 500 * ratio,
+ "Zoom canvas width is resized to be the same as the parent div");
+ is(zoom.height, 700 * ratio,
+ "Zoom canvas height is resized to be the same as the parent div");
+ is(timesResizeCalled, 1,
+ "'resize' was emitted was emitted");
+
+ div.style.width = "1100px";
+ div.style.height = "1300px";
+
+ canvases.destroy();
+ window.dispatchEvent(new Event("resize"));
+
+ is(main.width, 500 * ratio,
+ "Main canvas width is not resized after destroy");
+ is(main.height, 700 * ratio,
+ "Main canvas height is not resized after destroy");
+ is(zoom.width, 500 * ratio,
+ "Zoom canvas width is not resized after destroy");
+ is(zoom.height, 700 * ratio,
+ "Zoom canvas height is not resized after destroy");
+ is(timesResizeCalled, 1,
+ "onResize was not called again");
+
+ document.body.removeChild(div);
+});
diff --git a/devtools/client/memory/test/browser/browser_memory_tree_map-02.js b/devtools/client/memory/test/browser/browser_memory_tree_map-02.js
new file mode 100644
index 000000000..15f8b0457
--- /dev/null
+++ b/devtools/client/memory/test/browser/browser_memory_tree_map-02.js
@@ -0,0 +1,161 @@
+/* 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 the drag and zooming behavior
+
+"use strict";
+
+const CanvasUtils = require("devtools/client/memory/components/tree-map/canvas-utils");
+const DragZoom = require("devtools/client/memory/components/tree-map/drag-zoom");
+
+const TEST_URL = "data:text/html,<html><body></body></html>";
+const PIXEL_SCROLL_MODE = 0;
+const PIXEL_DELTA = 10;
+const MAX_RAF_LOOP = 1000;
+
+this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
+ const panelWin = panel.panelWin;
+ const panelDoc = panelWin.document;
+ const div = panelDoc.createElement("div");
+
+ Object.assign(div.style, {
+ width: "100px",
+ height: "200px",
+ position: "absolute",
+ left:0,
+ top:0
+ });
+
+ let rafMock = createRAFMock();
+
+ panelDoc.body.appendChild(div);
+
+ let canvases = new CanvasUtils(div, 0);
+ let dragZoom = new DragZoom(canvases.container, 0, rafMock.raf);
+ let style = canvases.container.style;
+
+ info("Check initial state of dragZoom");
+ {
+ is(dragZoom.zoom, 0, "Zooming starts at 0");
+ is(dragZoom.smoothZoom, 0, "Smoothed zooming starts at 0");
+ is(rafMock.timesCalled, 0, "No RAFs have been queued");
+ is(style.transform, "translate(0px, 0px) scale(1)",
+ "No transforms have been done.");
+
+ canvases.container.dispatchEvent(new WheelEvent("wheel", {
+ deltaY: -PIXEL_DELTA,
+ deltaMode: PIXEL_SCROLL_MODE
+ }));
+
+ is(style.transform, "translate(0px, 0px) scale(1.05)",
+ "The div has been slightly scaled.");
+ is(dragZoom.zoom, PIXEL_DELTA * dragZoom.ZOOM_SPEED,
+ "The zoom was increased");
+ ok(floatEquality(dragZoom.smoothZoom, 0.05),
+ "The smooth zoom is between the initial value and the target");
+ is(rafMock.timesCalled, 1, "A RAF has been queued");
+ }
+
+ info("RAF will eventually stop once the smooth values approach the target");
+ {
+ let i;
+ let lastCallCount;
+ for (i = 0; i < MAX_RAF_LOOP; i++) {
+ if (lastCallCount === rafMock.timesCalled) {
+ break;
+ }
+ lastCallCount = rafMock.timesCalled;
+ rafMock.nextFrame();
+ }
+ is(style.transform, "translate(0px, 0px) scale(1.1)",
+ "The scale has been fully applied");
+ is(dragZoom.zoom, dragZoom.smoothZoom,
+ "The smooth and target zoom values match");
+ isnot(MAX_RAF_LOOP, i,
+ "The RAF loop correctly stopped");
+ }
+
+ info("Dragging correctly translates the div");
+ {
+ let initialX = dragZoom.translateX;
+ let initialY = dragZoom.translateY;
+ div.dispatchEvent(new MouseEvent("mousemove", {
+ clientX: 10,
+ clientY: 10,
+ }));
+ div.dispatchEvent(new MouseEvent("mousedown"));
+ div.dispatchEvent(new MouseEvent("mousemove", {
+ clientX: 20,
+ clientY: 20,
+ }));
+ div.dispatchEvent(new MouseEvent("mouseup"));
+
+ is(style.transform, "translate(2.5px, 5px) scale(1.1)",
+ "The style is correctly translated");
+ ok(floatEquality(dragZoom.translateX, 5),
+ "Translate X moved by some pixel amount");
+ ok(floatEquality(dragZoom.translateY, 10),
+ "Translate Y moved by some pixel amount");
+ }
+
+ info("Zooming centers around the mouse");
+ {
+ canvases.container.dispatchEvent(new WheelEvent("wheel", {
+ deltaY: -PIXEL_DELTA,
+ deltaMode: PIXEL_SCROLL_MODE
+ }));
+ // Run through the RAF loop to zoom in towards that value.
+ let lastCallCount;
+ for (let i = 0; i < MAX_RAF_LOOP; i++) {
+ if (lastCallCount === rafMock.timesCalled) {
+ break;
+ }
+ lastCallCount = rafMock.timesCalled;
+ rafMock.nextFrame();
+ }
+ is(style.transform, "translate(8.18182px, 18.1818px) scale(1.2)",
+ "Zooming affects the translation to keep the mouse centered");
+ ok(floatEquality(dragZoom.translateX, 8.181818181818185),
+ "Translate X was affected by the mouse position");
+ ok(floatEquality(dragZoom.translateY, 18.18181818181817),
+ "Translate Y was affected by the mouse position");
+ is(dragZoom.zoom, 0.2, "Zooming starts at 0");
+ }
+
+ dragZoom.destroy();
+
+ info("Scroll isn't tracked after destruction");
+ {
+ let previousZoom = dragZoom.zoom;
+ let previousSmoothZoom = dragZoom.smoothZoom;
+
+ canvases.container.dispatchEvent(new WheelEvent("wheel", {
+ deltaY: -PIXEL_DELTA,
+ deltaMode: PIXEL_SCROLL_MODE
+ }));
+
+ is(dragZoom.zoom, previousZoom,
+ "The zoom stayed the same");
+ is(dragZoom.smoothZoom, previousSmoothZoom,
+ "The smooth zoom stayed the same");
+ }
+
+ info("Translation isn't tracked after destruction");
+ {
+ let initialX = dragZoom.translateX;
+ let initialY = dragZoom.translateY;
+
+ div.dispatchEvent(new MouseEvent("mousedown"));
+ div.dispatchEvent(new MouseEvent("mousemove"), {
+ clientX: 40,
+ clientY: 40,
+ });
+ div.dispatchEvent(new MouseEvent("mouseup"));
+ is(dragZoom.translateX, initialX,
+ "The translationX didn't change");
+ is(dragZoom.translateY, initialY,
+ "The translationY didn't change");
+ }
+ panelDoc.body.removeChild(div);
+});
diff --git a/devtools/client/memory/test/browser/doc_big_tree.html b/devtools/client/memory/test/browser/doc_big_tree.html
new file mode 100644
index 000000000..4ad60402a
--- /dev/null
+++ b/devtools/client/memory/test/browser/doc_big_tree.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <script>
+ window.big = (function makeBig(depth = 0) {
+ var big = Array(5);
+ big.fill(undefined);
+ if (depth < 5) {
+ big = big.map(_ => makeBig(depth + 1));
+ }
+ return big;
+ }());
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/memory/test/browser/doc_empty.html b/devtools/client/memory/test/browser/doc_empty.html
new file mode 100644
index 000000000..ef123d8d2
--- /dev/null
+++ b/devtools/client/memory/test/browser/doc_empty.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ </head>
+ <body>
+ This is an empty window.
+ </body>
+</html>
diff --git a/devtools/client/memory/test/browser/doc_steady_allocation.html b/devtools/client/memory/test/browser/doc_steady_allocation.html
new file mode 100644
index 000000000..65703c878
--- /dev/null
+++ b/devtools/client/memory/test/browser/doc_steady_allocation.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <script>
+ var objects = window.objects = [];
+
+ var allocate = this.allocate = function allocate() {
+ for (var i = 0; i < 100; i++)
+ objects.push({});
+ setTimeout(allocate, 10);
+ }
+
+ allocate();
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/memory/test/browser/head.js b/devtools/client/memory/test/browser/head.js
new file mode 100644
index 000000000..cb9b470ff
--- /dev/null
+++ b/devtools/client/memory/test/browser/head.js
@@ -0,0 +1,248 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Load the shared test helpers into this compartment.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js",
+ this);
+
+// Load the shared Redux helpers into this compartment.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/framework/test/shared-redux-head.js",
+ this);
+
+var { censusDisplays, snapshotState: states } = require("devtools/client/memory/constants");
+var { L10N } = require("devtools/client/memory/utils");
+
+Services.prefs.setBoolPref("devtools.memory.enabled", true);
+
+/**
+ * Open the memory panel for the given tab.
+ */
+this.openMemoryPanel = Task.async(function* (tab) {
+ info("Opening memory panel.");
+ const target = TargetFactory.forTab(tab);
+ const toolbox = yield gDevTools.showToolbox(target, "memory");
+ info("Memory panel shown successfully.");
+ let panel = toolbox.getCurrentPanel();
+ return { tab, panel };
+});
+
+/**
+ * Close the memory panel for the given tab.
+ */
+this.closeMemoryPanel = Task.async(function* (tab) {
+ info("Closing memory panel.");
+ const target = TargetFactory.forTab(tab);
+ const toolbox = gDevTools.getToolbox(target);
+ yield toolbox.destroy();
+ info("Closed memory panel successfully.");
+});
+
+/**
+ * Return a test function that adds a tab with the given url, opens the memory
+ * panel, runs the given generator, closes the memory panel, removes the tab,
+ * and finishes.
+ *
+ * Example usage:
+ *
+ * this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
+ * // Your tests go here...
+ * });
+ */
+function makeMemoryTest(url, generator) {
+ return Task.async(function* () {
+ waitForExplicitFinish();
+
+ // It can take a long time to save a snapshot to disk, read the snapshots
+ // back from disk, and finally perform analyses on them.
+ requestLongerTimeout(2);
+
+ const tab = yield addTab(url);
+ const results = yield openMemoryPanel(tab);
+
+ try {
+ yield* generator(results);
+ } catch (err) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(err));
+ }
+
+ yield closeMemoryPanel(tab);
+ yield removeTab(tab);
+
+ finish();
+ });
+}
+
+function dumpn(msg) {
+ dump(`MEMORY-TEST: ${msg}\n`);
+}
+
+/**
+ * Returns a promise that will resolve when the provided store matches
+ * the expected array. expectedStates is an array of dominatorTree states.
+ * Expectations :
+ * - store.getState().snapshots.length == expected.length
+ * - snapshots[i].dominatorTree.state == expected[i]
+ *
+ * @param {Store} store
+ * @param {Array<string>} expectedStates [description]
+ * @return {Promise}
+ */
+function waitUntilDominatorTreeState(store, expected) {
+ let predicate = () => {
+ let snapshots = store.getState().snapshots;
+ return snapshots.length === expected.length &&
+ expected.every((state, i) => {
+ return snapshots[i].dominatorTree &&
+ snapshots[i].dominatorTree.state === state;
+ });
+ };
+ info(`Waiting for dominator trees to be of state: ${expected}`);
+ return waitUntilState(store, predicate);
+}
+
+function takeSnapshot(window) {
+ let { gStore, document } = window;
+ let snapshotCount = gStore.getState().snapshots.length;
+ info("Taking snapshot...");
+ document.querySelector(".devtools-toolbar .take-snapshot").click();
+ return waitUntilState(gStore, () => gStore.getState().snapshots.length === snapshotCount + 1);
+}
+
+function clearSnapshots(window) {
+ let { gStore, document } = window;
+ document.querySelector(".devtools-toolbar .clear-snapshots").click();
+ return waitUntilState(gStore, () => gStore.getState().snapshots.every(
+ (snapshot) => snapshot.state !== states.READ)
+ );
+}
+
+/**
+ * Sets the current requested display and waits for the selected snapshot to use
+ * it and complete the new census that entails.
+ */
+function setCensusDisplay(window, display) {
+ info(`Setting census display to ${display}...`);
+ let { gStore, gHeapAnalysesClient } = window;
+ // XXX: Should handle this via clicking the DOM, but React doesn't
+ // fire the onChange event, so just change it in the store.
+ // window.document.querySelector(`.select-display`).value = type;
+ gStore.dispatch(require("devtools/client/memory/actions/census-display")
+ .setCensusDisplayAndRefresh(gHeapAnalysesClient, display));
+
+ return waitUntilState(window.gStore, () => {
+ let selected = window.gStore.getState().snapshots.find(s => s.selected);
+ return selected.state === states.READ &&
+ selected.census &&
+ selected.census.state === censusState.SAVED &&
+ selected.census.display === display;
+ });
+}
+
+/**
+ * Get the snapshot tatus text currently displayed, or null if none is
+ * displayed.
+ *
+ * @param {Document} document
+ */
+function getDisplayedSnapshotStatus(document) {
+ const status = document.querySelector(".snapshot-status");
+ return status ? status.textContent.trim() : null;
+}
+
+/**
+ * Get the index of the currently selected snapshot.
+ *
+ * @return {Number}
+ */
+function getSelectedSnapshotIndex(store) {
+ let snapshots = store.getState().snapshots;
+ let selectedSnapshot = snapshots.find(s => s.selected);
+ return snapshots.indexOf(selectedSnapshot);
+}
+
+/**
+ * Returns a promise that will resolve when the snapshot with provided index
+ * becomes selected.
+ *
+ * @return {Promise}
+ */
+function waitUntilSnapshotSelected(store, snapshotIndex) {
+ return waitUntilState(store, state =>
+ state.snapshots[snapshotIndex] &&
+ state.snapshots[snapshotIndex].selected === true);
+}
+
+
+/**
+ * Wait until the state has censuses in a certain state.
+ *
+ * @return {Promise}
+ */
+function waitUntilCensusState(store, getCensus, expected) {
+ let predicate = () => {
+ let snapshots = store.getState().snapshots;
+
+ info("Current census state:" +
+ snapshots.map(x => getCensus(x) ? getCensus(x).state : null));
+
+ return snapshots.length === expected.length &&
+ expected.every((state, i) => {
+ let census = getCensus(snapshots[i]);
+ return (state === "*") ||
+ (!census && !state) ||
+ (census && census.state === state);
+ });
+ };
+ info(`Waiting for snapshot censuses to be of state: ${expected}`);
+ return waitUntilState(store, predicate);
+}
+
+/**
+ * Mock out the requestAnimationFrame.
+ *
+ * @return {Object}
+ * @function nextFrame
+ * Call the last queued function
+ * @function raf
+ * The mocked raf function
+ * @function timesCalled
+ * How many times the RAF has been called
+ */
+function createRAFMock() {
+ let queuedFns = [];
+ let mock = { timesCalled: 0 };
+
+ mock.nextFrame = function () {
+ let thisQueue = queuedFns;
+ queuedFns = [];
+ for (var i = 0; i < thisQueue.length; i++) {
+ thisQueue[i]();
+ }
+ };
+
+ mock.raf = function (fn) {
+ mock.timesCalled++;
+ queuedFns.push(fn);
+ };
+ return mock;
+}
+
+/**
+ * Test to see if two floats are equivalent.
+ *
+ * @param {Float} a
+ * @param {Float} b
+ * @return {Boolean}
+ */
+function floatEquality(a, b) {
+ const EPSILON = 0.00000000001;
+ const equals = Math.abs(a - b) < EPSILON;
+ if (!equals) {
+ info(`${a} not equal to ${b}`);
+ }
+ return equals;
+}
diff --git a/devtools/client/memory/test/chrome/chrome.ini b/devtools/client/memory/test/chrome/chrome.ini
new file mode 100644
index 000000000..7803bcda3
--- /dev/null
+++ b/devtools/client/memory/test/chrome/chrome.ini
@@ -0,0 +1,20 @@
+[DEFAULT]
+support-files =
+ head.js
+
+[test_CensusTreeItem_01.html]
+[test_DominatorTree_01.html]
+[test_DominatorTree_02.html]
+[test_DominatorTree_03.html]
+[test_DominatorTreeItem_01.html]
+[test_Heap_01.html]
+[test_Heap_02.html]
+[test_Heap_03.html]
+[test_Heap_04.html]
+[test_Heap_05.html]
+[test_List_01.html]
+[test_ShortestPaths_01.html]
+[test_ShortestPaths_02.html]
+[test_SnapshotListItem_01.html]
+[test_Toolbar_01.html]
+[test_TreeMap_01.html]
diff --git a/devtools/client/memory/test/chrome/head.js b/devtools/client/memory/test/chrome/head.js
new file mode 100644
index 000000000..4ca5a7a7e
--- /dev/null
+++ b/devtools/client/memory/test/chrome/head.js
@@ -0,0 +1,335 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+var { BrowserLoader } = Cu.import("resource://devtools/client/shared/browser-loader.js", {});
+var { require } = BrowserLoader({
+ baseURI: "resource://devtools/client/memory/",
+ window
+});
+var { Assert } = require("resource://testing-common/Assert.jsm");
+var Services = require("Services");
+var { Task } = require("devtools/shared/task");
+
+var EXPECTED_DTU_ASSERT_FAILURE_COUNT = 0;
+
+SimpleTest.registerCleanupFunction(function () {
+ if (DevToolsUtils.assertionFailureCount !== EXPECTED_DTU_ASSERT_FAILURE_COUNT) {
+ ok(false, "Should have had the expected number of DevToolsUtils.assert() failures. Expected " +
+ EXPECTED_DTU_ASSERT_FAILURE_COUNT + ", got " + DevToolsUtils.assertionFailureCount);
+ }
+});
+
+var DevToolsUtils = require("devtools/shared/DevToolsUtils");
+var { immutableUpdate } = DevToolsUtils;
+var flags = require("devtools/shared/flags");
+flags.testing = true;
+
+var constants = require("devtools/client/memory/constants");
+var {
+ censusDisplays,
+ diffingState,
+ labelDisplays,
+ dominatorTreeState,
+ snapshotState,
+ viewState,
+ censusState
+} = constants;
+
+const {
+ L10N,
+} = require("devtools/client/memory/utils");
+
+var models = require("devtools/client/memory/models");
+
+var Immutable = require("devtools/client/shared/vendor/immutable");
+var React = require("devtools/client/shared/vendor/react");
+var ReactDOM = require("devtools/client/shared/vendor/react-dom");
+var Heap = React.createFactory(require("devtools/client/memory/components/heap"));
+var CensusTreeItem = React.createFactory(require("devtools/client/memory/components/census-tree-item"));
+var DominatorTreeComponent = React.createFactory(require("devtools/client/memory/components/dominator-tree"));
+var DominatorTreeItem = React.createFactory(require("devtools/client/memory/components/dominator-tree-item"));
+var ShortestPaths = React.createFactory(require("devtools/client/memory/components/shortest-paths"));
+var TreeMap = React.createFactory(require("devtools/client/memory/components/tree-map"));
+var SnapshotListItem = React.createFactory(require("devtools/client/memory/components/snapshot-list-item"));
+var List = React.createFactory(require("devtools/client/memory/components/list"));
+var Toolbar = React.createFactory(require("devtools/client/memory/components/toolbar"));
+
+// All tests are asynchronous.
+SimpleTest.waitForExplicitFinish();
+
+var noop = () => {};
+
+var TEST_CENSUS_TREE_ITEM_PROPS = Object.freeze({
+ item: Object.freeze({
+ bytes: 10,
+ count: 1,
+ totalBytes: 10,
+ totalCount: 1,
+ name: "foo",
+ children: [
+ Object.freeze({
+ bytes: 10,
+ count: 1,
+ totalBytes: 10,
+ totalCount: 1,
+ name: "bar",
+ })
+ ]
+ }),
+ depth: 0,
+ arrow: ">",
+ focused: true,
+ getPercentBytes: () => 50,
+ getPercentCount: () => 50,
+ showSign: false,
+ onViewSourceInDebugger: noop,
+ inverted: false,
+});
+
+// Counter for mock DominatorTreeNode ids.
+var TEST_NODE_ID_COUNTER = 0;
+
+/**
+ * Create a mock DominatorTreeNode for testing, with sane defaults. Override any
+ * property by providing it on `opts`. Optionally pass child nodes as well.
+ *
+ * @param {Object} opts
+ * @param {Array<DominatorTreeNode>?} children
+ *
+ * @returns {DominatorTreeNode}
+ */
+function makeTestDominatorTreeNode(opts, children) {
+ const nodeId = TEST_NODE_ID_COUNTER++;
+
+ const node = Object.assign({
+ nodeId,
+ label: ["other", "SomeType"],
+ shallowSize: 1,
+ retainedSize: (children || []).reduce((size, c) => size + c.retainedSize, 1),
+ parentId: undefined,
+ children,
+ moreChildrenAvailable: true,
+ }, opts);
+
+ if (children && children.length) {
+ children.map(c => c.parentId = node.nodeId);
+ }
+
+ return node;
+}
+
+var TEST_DOMINATOR_TREE = Object.freeze({
+ dominatorTreeId: 666,
+ root: (function makeTree(depth = 0) {
+ let children;
+ if (depth <= 3) {
+ children = [
+ makeTree(depth + 1),
+ makeTree(depth + 1),
+ makeTree(depth + 1),
+ ];
+ }
+ return makeTestDominatorTreeNode({}, children);
+ }()),
+ expanded: new Set(),
+ focused: null,
+ error: null,
+ display: labelDisplays.coarseType,
+ activeFetchRequestCount: null,
+ state: dominatorTreeState.LOADED,
+});
+
+var TEST_DOMINATOR_TREE_PROPS = Object.freeze({
+ dominatorTree: TEST_DOMINATOR_TREE,
+ onLoadMoreSiblings: noop,
+ onViewSourceInDebugger: noop,
+ onExpand: noop,
+ onCollapse: noop,
+});
+
+var TEST_SHORTEST_PATHS_PROPS = Object.freeze({
+ graph: Object.freeze({
+ nodes: [
+ { id: 1, label: ["other", "SomeType"] },
+ { id: 2, label: ["other", "SomeType"] },
+ { id: 3, label: ["other", "SomeType"] },
+ ],
+ edges: [
+ { from: 1, to: 2, name: "1->2" },
+ { from: 1, to: 3, name: "1->3" },
+ { from: 2, to: 3, name: "2->3" },
+ ],
+ }),
+});
+
+var TEST_SNAPSHOT = Object.freeze({
+ id: 1337,
+ selected: true,
+ path: "/fake/path/to/snapshot",
+ census: Object.freeze({
+ report: Object.freeze({
+ objects: Object.freeze({ count: 4, bytes: 400 }),
+ scripts: Object.freeze({ count: 3, bytes: 300 }),
+ strings: Object.freeze({ count: 2, bytes: 200 }),
+ other: Object.freeze({ count: 1, bytes: 100 }),
+ }),
+ display: Object.freeze({
+ displayName: "Test Display",
+ tooltip: "Test display tooltup",
+ inverted: false,
+ breakdown: Object.freeze({
+ by: "coarseType",
+ objects: Object.freeze({ by: "count", count: true, bytes: true }),
+ scripts: Object.freeze({ by: "count", count: true, bytes: true }),
+ strings: Object.freeze({ by: "count", count: true, bytes: true }),
+ other: Object.freeze({ by: "count", count: true, bytes: true }),
+ }),
+ }),
+ state: censusState.SAVED,
+ inverted: false,
+ filter: null,
+ expanded: new Set(),
+ focused: null,
+ parentMap: Object.freeze(Object.create(null))
+ }),
+ dominatorTree: TEST_DOMINATOR_TREE,
+ error: null,
+ imported: false,
+ creationTime: 0,
+ state: snapshotState.READ,
+});
+
+var TEST_HEAP_PROPS = Object.freeze({
+ onSnapshotClick: noop,
+ onLoadMoreSiblings: noop,
+ onCensusExpand: noop,
+ onCensusCollapse: noop,
+ onDominatorTreeExpand: noop,
+ onDominatorTreeCollapse: noop,
+ onCensusFocus: noop,
+ onDominatorTreeFocus: noop,
+ onViewSourceInDebugger: noop,
+ diffing: null,
+ view: { state: viewState.CENSUS, },
+ snapshot: TEST_SNAPSHOT,
+ sizes: Object.freeze({ shortestPathsSize: .5 }),
+ onShortestPathsResize: noop,
+});
+
+var TEST_TOOLBAR_PROPS = Object.freeze({
+ censusDisplays: [
+ censusDisplays.coarseType,
+ censusDisplays.allocationStack,
+ censusDisplays.invertedAllocationStack,
+ ],
+ censusDisplay: censusDisplays.coarseType,
+ onTakeSnapshotClick: noop,
+ onImportClick: noop,
+ onCensusDisplayChange: noop,
+ onToggleRecordAllocationStacks: noop,
+ allocations: models.allocations,
+ onToggleInverted: noop,
+ inverted: false,
+ filterString: null,
+ setFilterString: noop,
+ diffing: null,
+ onToggleDiffing: noop,
+ view: { state: viewState.CENSUS, },
+ onViewChange: noop,
+ labelDisplays: [
+ labelDisplays.coarseType,
+ labelDisplays.allocationStack,
+ ],
+ labelDisplay: labelDisplays.coarseType,
+ onLabelDisplayChange: noop,
+ snapshots: [],
+});
+
+function makeTestCensusNode() {
+ return {
+ name: "Function",
+ bytes: 100,
+ totalBytes: 100,
+ count: 100,
+ totalCount: 100,
+ children: []
+ };
+}
+
+var TEST_TREE_MAP_PROPS = Object.freeze({
+ treeMap: Object.freeze({
+ report: {
+ name: null,
+ bytes: 0,
+ totalBytes: 400,
+ count: 0,
+ totalCount: 400,
+ children: [
+ {
+ name: "objects",
+ bytes: 0,
+ totalBytes: 200,
+ count: 0,
+ totalCount: 200,
+ children: [ makeTestCensusNode(), makeTestCensusNode() ]
+ },
+ {
+ name: "other",
+ bytes: 0,
+ totalBytes: 200,
+ count: 0,
+ totalCount: 200,
+ children: [ makeTestCensusNode(), makeTestCensusNode() ],
+ }
+ ]
+ }
+ })
+});
+
+var TEST_SNAPSHOT_LIST_ITEM_PROPS = Object.freeze({
+ onClick: noop,
+ onSave: noop,
+ onDelete: noop,
+ item: TEST_SNAPSHOT,
+ index: 1234,
+});
+
+function onNextAnimationFrame(fn) {
+ return () =>
+ requestAnimationFrame(() =>
+ requestAnimationFrame(fn));
+}
+
+/**
+ * Render the provided ReactElement in the provided HTML container.
+ * Returns a Promise that will resolve the rendered element as a React
+ * component.
+ */
+function renderComponent(element, container) {
+ return new Promise(resolve => {
+ let component = ReactDOM.render(element, container,
+ onNextAnimationFrame(() => {
+ dumpn("Rendered = " + container.innerHTML);
+ resolve(component);
+ }));
+ });
+}
+
+function setState(component, newState) {
+ return new Promise(resolve => {
+ component.setState(newState, onNextAnimationFrame(resolve));
+ });
+}
+
+function setProps(component, newProps) {
+ return new Promise(resolve => {
+ component.setProps(newProps, onNextAnimationFrame(resolve));
+ });
+}
+
+function dumpn(msg) {
+ dump(`MEMORY-TEST: ${msg}\n`);
+}
diff --git a/devtools/client/memory/test/chrome/test_CensusTreeItem_01.html b/devtools/client/memory/test/chrome/test_CensusTreeItem_01.html
new file mode 100644
index 000000000..fef996330
--- /dev/null
+++ b/devtools/client/memory/test/chrome/test_CensusTreeItem_01.html
@@ -0,0 +1,65 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that children pointers show up at the correct times.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+ <!-- Give the container height so that the whole tree is rendered. -->
+ <div id="container" style="height: 900px;"></div>
+
+ <pre id="test">
+ <script src="head.js" type="application/javascript;version=1.8"></script>
+ <script type="application/javascript;version=1.8">
+ window.onload = Task.async(function* () {
+ try {
+ const container = document.getElementById("container");
+
+ yield renderComponent(CensusTreeItem(immutableUpdate(TEST_CENSUS_TREE_ITEM_PROPS, {
+ inverted: true,
+ depth: 0,
+ })), container);
+
+ ok(!container.querySelector(".children-pointer"),
+ "Don't show children pointer for roots when we are inverted");
+
+ yield renderComponent(CensusTreeItem(immutableUpdate(TEST_CENSUS_TREE_ITEM_PROPS, {
+ inverted: true,
+ depth: 1,
+ })), container);
+
+ ok(container.querySelector(".children-pointer"),
+ "Do show children pointer for non-roots when we are inverted");
+
+ yield renderComponent(CensusTreeItem(immutableUpdate(TEST_CENSUS_TREE_ITEM_PROPS, {
+ inverted: false,
+ item: immutableUpdate(TEST_CENSUS_TREE_ITEM_PROPS.item, { children: undefined }),
+ })), container);
+
+ ok(!container.querySelector(".children-pointer"),
+ "Don't show children pointer when non-inverted and no children");
+
+ yield renderComponent(CensusTreeItem(immutableUpdate(TEST_CENSUS_TREE_ITEM_PROPS, {
+ inverted: false,
+ depth: 0,
+ item: immutableUpdate(TEST_CENSUS_TREE_ITEM_PROPS.item, { children: [{}] }),
+ })), container);
+
+ ok(container.querySelector(".children-pointer"),
+ "Do show children pointer when non-inverted and have children");
+
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+ });
+ </script>
+ </pre>
+</body>
+</html>
diff --git a/devtools/client/memory/test/chrome/test_DominatorTreeItem_01.html b/devtools/client/memory/test/chrome/test_DominatorTreeItem_01.html
new file mode 100644
index 000000000..56cba7391
--- /dev/null
+++ b/devtools/client/memory/test/chrome/test_DominatorTreeItem_01.html
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that we don't display `JS::ubi::RootList` for the root, and instead show "GC Roots".
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+ <!-- Give the container height so that the whole tree is rendered. -->
+ <div id="container" style="height: 900px;"></div>
+
+ <pre id="test">
+ <script src="head.js" type="application/javascript;version=1.8"></script>
+ <script type="application/javascript;version=1.8">
+ window.onload = Task.async(function* () {
+ try {
+ const container = document.getElementById("container");
+
+ yield renderComponent(DominatorTreeItem({
+ item: makeTestDominatorTreeNode({ label: ["other", "JS::ubi::RootList"] }),
+ depth: 0,
+ arrow: React.DOM.div(),
+ focused: true,
+ getPercentSize: _ => 50,
+ onViewSourceInDebugger: _ => { },
+ }), container);
+
+ ok(container.textContent.indexOf("JS::ubi::RootList") == -1,
+ "Should not display `JS::ubi::RootList`");
+ ok(container.textContent.indexOf("GC Roots") >= 0,
+ "Should display `GC Roots` instead");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+ });
+ </script>
+ </pre>
+</body>
+</html>
diff --git a/devtools/client/memory/test/chrome/test_DominatorTree_01.html b/devtools/client/memory/test/chrome/test_DominatorTree_01.html
new file mode 100644
index 000000000..582576e49
--- /dev/null
+++ b/devtools/client/memory/test/chrome/test_DominatorTree_01.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that we show a place holder for a subtree we are lazily fetching.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+ <!-- Give the container height so that the whole tree is rendered. -->
+ <div id="container" style="height: 900px;"></div>
+
+ <pre id="test">
+ <script src="head.js" type="application/javascript;version=1.8"></script>
+ <script type="application/javascript;version=1.8">
+ window.onload = Task.async(function* () {
+ try {
+ const container = document.getElementById("container");
+
+ const root = makeTestDominatorTreeNode({ moreChildrenAvailable: true});
+ ok(!root.children);
+
+ const expanded = new Set();
+ expanded.add(root.nodeId);
+
+ yield renderComponent(DominatorTreeComponent(immutableUpdate(TEST_DOMINATOR_TREE_PROPS, {
+ dominatorTree: immutableUpdate(TEST_DOMINATOR_TREE_PROPS.dominatorTree, {
+ expanded,
+ root,
+ state: dominatorTreeState.INCREMENTAL_FETCHING,
+ activeFetchRequestCount: 1,
+ }),
+ })), container);
+
+ ok(container.querySelector(".subtree-fetching"),
+ "Expanded nodes with more children available, but no children " +
+ "loaded, should get a placeholder");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+ });
+ </script>
+ </pre>
+</body>
+</html>
diff --git a/devtools/client/memory/test/chrome/test_DominatorTree_02.html b/devtools/client/memory/test/chrome/test_DominatorTree_02.html
new file mode 100644
index 000000000..ffdac3263
--- /dev/null
+++ b/devtools/client/memory/test/chrome/test_DominatorTree_02.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that we show a link to load more children when some (but not all) are loaded.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+ <!-- Give the container height so that the whole tree is rendered. -->
+ <div id="container" style="height: 900px;"></div>
+
+ <pre id="test">
+ <script src="head.js" type="application/javascript;version=1.8"></script>
+ <script type="application/javascript;version=1.8">
+ window.onload = Task.async(function* () {
+ try {
+ const container = document.getElementById("container");
+
+ const root = makeTestDominatorTreeNode({ moreChildrenAvailable: true }, [
+ makeTestDominatorTreeNode({}),
+ ]);
+ ok(root.children);
+ ok(root.moreChildrenAvailable);
+
+ const expanded = new Set();
+ expanded.add(root.nodeId);
+
+ yield renderComponent(DominatorTreeComponent(immutableUpdate(TEST_DOMINATOR_TREE_PROPS, {
+ dominatorTree: immutableUpdate(TEST_DOMINATOR_TREE_PROPS.dominatorTree, {
+ expanded,
+ root,
+ }),
+ })), container);
+
+ ok(container.querySelector(".more-children"),
+ "Should get a link to load more children");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+ });
+ </script>
+ </pre>
+</body>
+</html>
diff --git a/devtools/client/memory/test/chrome/test_DominatorTree_03.html b/devtools/client/memory/test/chrome/test_DominatorTree_03.html
new file mode 100644
index 000000000..e9656dad8
--- /dev/null
+++ b/devtools/client/memory/test/chrome/test_DominatorTree_03.html
@@ -0,0 +1,75 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that expanded DominatorTreeItems are correctly rendered and updated
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+ <!-- Give the container height so that the whole tree is rendered. -->
+ <div id="container" style="height: 900px;"></div>
+
+ <pre id="test">
+ <script src="head.js" type="application/javascript;version=1.8"></script>
+ <script type="application/javascript;version=1.8">
+ window.onload = Task.async(function* () {
+ try {
+ const container = document.getElementById("container");
+
+ // simple tree with one root and one child
+ const root = makeTestDominatorTreeNode(
+ { moreChildrenAvailable: false },
+ [
+ makeTestDominatorTreeNode({ moreChildrenAvailable: false }),
+ ]);
+ ok(root.children);
+
+ // root node is expanded
+ const expanded = new Set();
+ expanded.add(root.nodeId);
+
+ let component = yield renderComponent(
+ DominatorTreeComponent(immutableUpdate(
+ TEST_DOMINATOR_TREE_PROPS,
+ {
+ dominatorTree: immutableUpdate(
+ TEST_DOMINATOR_TREE_PROPS.dominatorTree,
+ { expanded, root }
+ ),
+ })), container);
+ ok(true, "Dominator tree rendered");
+
+ is(container.querySelectorAll(".tree-node").length, 2,
+ "Should display two rows");
+ is(container.querySelectorAll(".arrow.open").length, 1,
+ "Should display one expanded arrow");
+
+ yield setProps(component, immutableUpdate(
+ TEST_DOMINATOR_TREE_PROPS,
+ {
+ dominatorTree: immutableUpdate(
+ TEST_DOMINATOR_TREE_PROPS.dominatorTree,
+ { expanded: new Set(), root }
+ )
+ }));
+ ok(true, "Dominator tree props updated to collapse all nodes");
+
+ is(container.querySelectorAll(".tree-node").length, 1,
+ "Should display only one row");
+ is(container.querySelectorAll(".arrow.open").length, 0,
+ "Should display no expanded arrow");
+
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+ });
+ </script>
+ </pre>
+</body>
+</html>
diff --git a/devtools/client/memory/test/chrome/test_Heap_01.html b/devtools/client/memory/test/chrome/test_Heap_01.html
new file mode 100644
index 000000000..5d5e72389
--- /dev/null
+++ b/devtools/client/memory/test/chrome/test_Heap_01.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that rendering a dominator tree error is handled correctly.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+ <div id="container"></div>
+ <pre id="test">
+ <script src="head.js" type="application/javascript;version=1.8"></script>
+
+ <script type="application/javascript;version=1.8">
+ window.onload = Task.async(function* () {
+ try {
+ ok(React, "Should get React");
+ ok(Heap, "Should get Heap");
+
+ const errorMessage = "Something went wrong!";
+ const container = document.getElementById("container");
+
+ const props = immutableUpdate(TEST_HEAP_PROPS, {
+ view: { state: viewState.DOMINATOR_TREE, },
+ snapshot: immutableUpdate(TEST_HEAP_PROPS.snapshot, {
+ dominatorTree: {
+ error: new Error(errorMessage),
+ state: dominatorTreeState.ERROR,
+ }
+ })
+ });
+
+ yield renderComponent(Heap(props), container);
+
+ ok(container.querySelector(".error"), "Should render an error view");
+ ok(container.textContent.indexOf(errorMessage) !== -1,
+ "Should see our error message");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+ });
+ </script>
+ </pre>
+</body>
+</html>
diff --git a/devtools/client/memory/test/chrome/test_Heap_02.html b/devtools/client/memory/test/chrome/test_Heap_02.html
new file mode 100644
index 000000000..800f1044c
--- /dev/null
+++ b/devtools/client/memory/test/chrome/test_Heap_02.html
@@ -0,0 +1,78 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that the currently selected view is rendered.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+ <div id="container"></div>
+ <pre id="test">
+ <script src="head.js" type="application/javascript;version=1.8"></script>
+ <script type="application/javascript;version=1.8">
+ window.onload = Task.async(function* () {
+ try {
+ ok(React, "Should get React");
+ ok(Heap, "Should get Heap");
+
+ const errorMessage = "Something went wrong!";
+ const container = document.getElementById("container");
+
+ // Dominator tree view.
+
+ yield renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, {
+ view: { state: viewState.DOMINATOR_TREE, },
+ })), container);
+
+ ok(container.querySelector(`[data-state=${dominatorTreeState.LOADED}]`),
+ "Should render the dominator tree.");
+
+ // Census view.
+
+ yield renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, {
+ view: { state: viewState.CENSUS, },
+ })), container);
+
+ ok(container.querySelector(`[data-state=${censusState.SAVED}]`),
+ "Should render the census.");
+
+ // Diffing view.
+
+ yield renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, {
+ view: { state: viewState.DIFFING, },
+ snapshot: null,
+ diffing: {
+ firstSnapshotId: null,
+ secondSnapshotId: null,
+ census: null,
+ error: null,
+ state: diffingState.SELECTING,
+ },
+ })), container);
+
+ ok(container.querySelector(`[data-state=${diffingState.SELECTING}]`),
+ "Should render the diffing.");
+
+ // Initial view.
+
+ yield renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, {
+ snapshot: null,
+ diffing: null,
+ })), container);
+
+ ok(container.querySelector("[data-state=initial]"),
+ "With no snapshot, nor a diffing, should render initial prompt.");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+ });
+ </script>
+ </pre>
+</body>
+</html>
diff --git a/devtools/client/memory/test/chrome/test_Heap_03.html b/devtools/client/memory/test/chrome/test_Heap_03.html
new file mode 100644
index 000000000..7f0f52255
--- /dev/null
+++ b/devtools/client/memory/test/chrome/test_Heap_03.html
@@ -0,0 +1,74 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that we show a throbber while computing and fetching dominator trees,
+but not in other dominator tree states.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+ <div id="container"></div>
+ <pre id="test">
+ <script src="head.js" type="application/javascript;version=1.8"></script>
+ <script type="application/javascript;version=1.8">
+ window.onload = Task.async(function* () {
+ try {
+ const container = document.getElementById("container");
+
+ for (let state of [dominatorTreeState.COMPUTING, dominatorTreeState.FETCHING]) {
+ yield renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, {
+ view: { state: viewState.DOMINATOR_TREE, },
+ snapshot: immutableUpdate(TEST_HEAP_PROPS.snapshot, {
+ dominatorTree: immutableUpdate(TEST_HEAP_PROPS.snapshot.dominatorTree, {
+ state,
+ root: null,
+ dominatorTreeId: state === dominatorTreeState.FETCHING ? 1 : null,
+ }),
+ }),
+ })), container);
+
+ ok(container.querySelector(".devtools-throbber"),
+ `Should show a throbber for state = ${state}`);
+ }
+
+ for (let state of [dominatorTreeState.LOADED, dominatorTreeState.INCREMENTAL_FETCHING]) {
+ yield renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, {
+ view: { state: viewState.DOMINATOR_TREE, },
+ snapshot: immutableUpdate(TEST_HEAP_PROPS.snapshot, {
+ dominatorTree: immutableUpdate(TEST_HEAP_PROPS.snapshot.dominatorTree, {
+ state,
+ activeFetchRequestCount: state === dominatorTreeState.INCREMENTAL_FETCHING ? 1 : undefined,
+ }),
+ }),
+ })), container);
+
+ ok(!container.querySelector(".devtools-throbber"),
+ `Should not show a throbber for state = ${state}`);
+ }
+
+ yield renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, {
+ view: { state: viewState.DOMINATOR_TREE, },
+ snapshot: immutableUpdate(TEST_HEAP_PROPS.snapshot, {
+ dominatorTree: {
+ state: dominatorTreeState.ERROR,
+ error: new Error("example error for testing"),
+ },
+ }),
+ })), container);
+
+ ok(!container.querySelector(".devtools-throbber"),
+ `Should not show a throbber for ERROR state`);
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+ });
+ </script>
+ </pre>
+</body>
+</html>
diff --git a/devtools/client/memory/test/chrome/test_Heap_04.html b/devtools/client/memory/test/chrome/test_Heap_04.html
new file mode 100644
index 000000000..ccf4c9c6d
--- /dev/null
+++ b/devtools/client/memory/test/chrome/test_Heap_04.html
@@ -0,0 +1,121 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that we show the "hey you're not recording allocation stacks" message at the appropriate times.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+ <div id="container"></div>
+ <pre id="test">
+ <script src="head.js" type="application/javascript;version=1.8"></script>
+ <script type="application/javascript;version=1.8">
+ window.onload = Task.async(function* () {
+ try {
+ const container = document.getElementById("container");
+
+ yield renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, {
+ snapshot: immutableUpdate(TEST_HEAP_PROPS.snapshot, {
+ census: immutableUpdate(TEST_HEAP_PROPS.snapshot.census, {
+ report: {
+ bytes: 1,
+ totalBytes: 1,
+ count: 1,
+ totalCount: 1,
+ id: 1,
+ parent: undefined,
+ children: [
+ {
+ name: "noStack",
+ bytes: 1,
+ totalBytes: 1,
+ count: 1,
+ totalCount: 1,
+ children: undefined,
+ id: 3,
+ parent: 1,
+ }
+ ]
+ },
+ display: censusDisplays.allocationStack,
+ }),
+ }),
+ })), container);
+
+ ok(container.querySelector(".no-allocation-stacks"),
+ "When there are no allocation stacks, we should show the message");
+
+ yield renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, {
+ snapshot: immutableUpdate(TEST_HEAP_PROPS.snapshot, {
+ census: immutableUpdate(TEST_HEAP_PROPS.snapshot.census, {
+ report: {
+ bytes: 1,
+ totalBytes: 1,
+ count: 1,
+ totalCount: 1,
+ id: 1,
+ parent: undefined,
+ children: [
+ {
+ name: Cu.getJSTestingFunctions().saveStack(),
+ bytes: 1,
+ totalBytes: 1,
+ count: 1,
+ totalCount: 1,
+ children: undefined,
+ id: 2,
+ parent: 1,
+ },
+ {
+ name: "noStack",
+ bytes: 1,
+ totalBytes: 1,
+ count: 1,
+ totalCount: 1,
+ children: undefined,
+ id: 3,
+ parent: 1,
+ }
+ ]
+ },
+ display: censusDisplays.allocationStack,
+ }),
+ }),
+ })), container);
+
+ ok(!container.querySelector(".no-allocation-stacks"),
+ "When there are allocation stacks, we should not show the message");
+
+ yield renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, {
+ snapshot: immutableUpdate(TEST_HEAP_PROPS.snapshot, {
+ census: immutableUpdate(TEST_HEAP_PROPS.snapshot.census, {
+ report: {
+ bytes: 1,
+ totalBytes: 1,
+ count: 1,
+ totalCount: 1,
+ id: 1,
+ parent: undefined,
+ children: undefined
+ },
+ display: censusDisplays.allocationStack,
+ }),
+ }),
+ })), container);
+
+ ok(!container.querySelector(".no-allocation-stacks"),
+ "When there isn't census data, we should not show the message");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+ });
+ </script>
+ </pre>
+</body>
+</html>
diff --git a/devtools/client/memory/test/chrome/test_Heap_05.html b/devtools/client/memory/test/chrome/test_Heap_05.html
new file mode 100644
index 000000000..14365e3ab
--- /dev/null
+++ b/devtools/client/memory/test/chrome/test_Heap_05.html
@@ -0,0 +1,132 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that we show a message when the census results are empty.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+ <div id="container"></div>
+ <pre id="test">
+ <script src="head.js" type="application/javascript;version=1.8"></script>
+ <script type="application/javascript;version=1.8">
+ window.onload = Task.async(function* () {
+ try {
+ const container = document.getElementById("container");
+
+ yield renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, {
+ snapshot: immutableUpdate(TEST_HEAP_PROPS.snapshot, {
+ census: immutableUpdate(TEST_HEAP_PROPS.snapshot.census, {
+ report: {
+ bytes: 1,
+ totalBytes: 1,
+ count: 1,
+ totalCount: 1,
+ id: 1,
+ parent: undefined,
+ children: [
+ {
+ name: Cu.getJSTestingFunctions().saveStack(),
+ bytes: 1,
+ totalBytes: 1,
+ count: 1,
+ totalCount: 1,
+ children: undefined,
+ id: 2,
+ parent: 1,
+ },
+ {
+ name: "noStack",
+ bytes: 1,
+ totalBytes: 1,
+ count: 1,
+ totalCount: 1,
+ children: undefined,
+ id: 3,
+ parent: 1,
+ }
+ ]
+ },
+ display: censusDisplays.allocationStack,
+ }),
+ }),
+ })), container);
+
+ ok(!container.querySelector(".empty"),
+ "When the report is not empty, we should not show the empty message");
+
+ // Empty Census Report
+
+ const emptyCensus = {
+ report: {
+ bytes: 0,
+ totalBytes: 0,
+ count: 0,
+ totalCount: 0,
+ id: 1,
+ parent: undefined,
+ children: undefined,
+ },
+ parentMap: Object.create(null),
+ display: censusDisplays.allocationStack,
+ filter: null,
+ expanded: new Immutable.Set(),
+ focused: null,
+ state: censusState.SAVED,
+ };
+
+ yield renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, {
+ snapshot: immutableUpdate(TEST_HEAP_PROPS.snapshot, {
+ census: immutableUpdate(TEST_HEAP_PROPS.snapshot.census, emptyCensus),
+ }),
+ })), container);
+
+ ok(container.querySelector(".empty"),
+ "When the report is empty in census view, we show the empty message");
+ ok(container.textContent.indexOf(L10N.getStr("heapview.empty")) >= 0);
+
+ // Empty Diffing Report
+
+ yield renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, {
+ view: { state: viewState.DIFFING, },
+ diffing: {
+ firstSnapshotId: 1,
+ secondSnapshotId: 2,
+ census: emptyCensus,
+ state: diffingState.TOOK_DIFF,
+ },
+ snapshot: null,
+ })), container);
+
+ ok(container.querySelector(".empty"),
+ "When the report is empty in diffing view, the empty message is shown");
+ ok(container.textContent.indexOf(L10N.getStr("heapview.no-difference")) >= 0);
+
+ // Empty Filtered Census
+
+ yield renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, {
+ snapshot: immutableUpdate(TEST_HEAP_PROPS.snapshot, {
+ census: immutableUpdate(TEST_HEAP_PROPS.snapshot.census, immutableUpdate(emptyCensus, {
+ filter: "zzzz"
+ })),
+ }),
+ })), container);
+
+ ok(container.querySelector(".empty"),
+ "When the report is empty in census view w/ filter, we show the empty message");
+ ok(container.textContent.indexOf(L10N.getStr("heapview.none-match")) >= 0);
+
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+ });
+ </script>
+ </pre>
+</body>
+</html>
diff --git a/devtools/client/memory/test/chrome/test_List_01.html b/devtools/client/memory/test/chrome/test_List_01.html
new file mode 100644
index 000000000..911a7bc77
--- /dev/null
+++ b/devtools/client/memory/test/chrome/test_List_01.html
@@ -0,0 +1,74 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test to verify the delete button calls the onDelete handler for an item
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+ <div id="container"></div>
+
+ <pre id="test">
+ <script src="head.js" type="application/javascript;version=1.8"></script>
+ <script type="application/javascript;version=1.8">
+ window.onload = Task.async(function* () {
+ try {
+ const container = document.getElementById("container");
+
+ let deletedSnapshots = [];
+
+ let snapshots = [ TEST_SNAPSHOT, TEST_SNAPSHOT, TEST_SNAPSHOT ]
+ .map((snapshot, index) => immutableUpdate(snapshot, {
+ index: snapshot.index + index
+ }));
+
+ yield renderComponent(
+ List({
+ itemComponent: SnapshotListItem,
+ onClick: noop,
+ onDelete: (item) => deletedSnapshots.push(item),
+ items: snapshots
+ }),
+ container
+ );
+
+ let deleteButtons = container.querySelectorAll('.delete');
+
+ is(container.querySelectorAll('.snapshot-list-item').length, 3,
+ "There are 3 list items\n");
+ is(deletedSnapshots.length, 0,
+ "Not snapshots have been deleted\n");
+
+ deleteButtons[1].click();
+
+ is(deletedSnapshots.length, 1, "One snapshot was deleted\n");
+ is(deletedSnapshots[0], snapshots[1],
+ "Deleted snapshot was added to the deleted list\n");
+
+ deleteButtons[0].click();
+
+ is(deletedSnapshots.length, 2, "Two snapshots were deleted\n");
+ is(deletedSnapshots[1], snapshots[0],
+ "Deleted snapshot was added to the deleted list\n");
+
+ deleteButtons[2].click();
+
+ is(deletedSnapshots.length, 3, "Three snapshots were deleted\n");
+ is(deletedSnapshots[2], snapshots[2],
+ "Deleted snapshot was added to the deleted list\n");
+
+
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+ });
+ </script>
+ </pre>
+</body>
+</html>
diff --git a/devtools/client/memory/test/chrome/test_ShortestPaths_01.html b/devtools/client/memory/test/chrome/test_ShortestPaths_01.html
new file mode 100644
index 000000000..e2ad1867a
--- /dev/null
+++ b/devtools/client/memory/test/chrome/test_ShortestPaths_01.html
@@ -0,0 +1,112 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that the ShortestPaths component properly renders a graph of the merged shortest paths.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+
+ <script type="application/javascript"
+ src="chrome://devtools/content/shared/vendor/d3.js">
+ </script>
+ <script type="application/javascript"
+ src="chrome://devtools/content/shared/vendor/dagre-d3.js">
+ </script>
+</head>
+<body>
+ <!-- Give the container height so that the whole tree is rendered. -->
+ <div id="container" style="height: 900px;"></div>
+
+ <pre id="test">
+ <script src="head.js" type="application/javascript;version=1.8"></script>
+ <script type="application/javascript;version=1.8">
+ window.onload = Task.async(function* () {
+ try {
+ const container = document.getElementById("container");
+
+ yield renderComponent(ShortestPaths(TEST_SHORTEST_PATHS_PROPS), container);
+
+ let found1 = false;
+ let found2 = false;
+ let found3 = false;
+
+ let found1to2 = false;
+ let found1to3 = false;
+ let found2to3 = false;
+
+ const tspans = [...container.querySelectorAll("tspan")];
+ for (let el of tspans) {
+ const text = el.textContent.trim();
+ dumpn("tspan's text = " + text);
+
+ switch (text) {
+ // Nodes
+
+ case "other › SomeType @ 0x1": {
+ ok(!found1, "Should only find node 1 once");
+ found1 = true;
+ break;
+ }
+
+ case "other › SomeType @ 0x2": {
+ ok(!found2, "Should only find node 2 once");
+ found2 = true;
+ break;
+ }
+
+ case "other › SomeType @ 0x3": {
+ ok(!found3, "Should only find node 3 once");
+ found3 = true;
+ break;
+ }
+
+ // Edges
+
+ case "1->2": {
+ ok(!found1to2, "Should only find edge 1->2 once");
+ found1to2 = true;
+ break;
+ }
+
+ case "1->3": {
+ ok(!found1to3, "Should only find edge 1->3 once");
+ found1to3 = true;
+ break;
+ }
+
+ case "2->3": {
+ ok(!found2to3, "Should only find edge 2->3 once");
+ found2to3 = true;
+ break;
+ }
+
+ // Unexpected
+
+ default: {
+ ok(false, `Unexpected tspan: ${text}`);
+ break;
+ }
+ }
+ }
+
+ ok(found1, "Should have rendered node 1");
+ ok(found2, "Should have rendered node 2");
+ ok(found3, "Should have rendered node 3");
+
+ ok(found1to2, "Should have rendered edge 1->2");
+ ok(found1to3, "Should have rendered edge 1->3");
+ ok(found2to3, "Should have rendered edge 2->3");
+
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+ });
+ </script>
+ </pre>
+</body>
+</html>
diff --git a/devtools/client/memory/test/chrome/test_ShortestPaths_02.html b/devtools/client/memory/test/chrome/test_ShortestPaths_02.html
new file mode 100644
index 000000000..cb6d48faa
--- /dev/null
+++ b/devtools/client/memory/test/chrome/test_ShortestPaths_02.html
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that the ShortestPaths component renders a suggestion to select a node when there is no graph.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+
+ <script type="application/javascript"
+ src="chrome://devtools/content/shared/vendor/d3.js">
+ </script>
+ <script type="application/javascript"
+ src="chrome://devtools/content/shared/vendor/dagre-d3.js">
+ </script>
+</head>
+<body>
+ <!-- Give the container height so that the whole tree is rendered. -->
+ <div id="container" style="height: 900px;"></div>
+
+ <pre id="test">
+ <script src="head.js" type="application/javascript;version=1.8"></script>
+ <script type="application/javascript;version=1.8">
+ window.onload = Task.async(function* () {
+ try {
+ const container = document.getElementById("container");
+
+ yield renderComponent(ShortestPaths(immutableUpdate(TEST_SHORTEST_PATHS_PROPS,
+ { graph: null })),
+ container);
+
+ ok(container.textContent.indexOf(L10N.getStr("shortest-paths.select-node")) !== -1,
+ "The node selection prompt is displayed");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+ });
+ </script>
+ </pre>
+</body>
+</html>
diff --git a/devtools/client/memory/test/chrome/test_SnapshotListItem_01.html b/devtools/client/memory/test/chrome/test_SnapshotListItem_01.html
new file mode 100644
index 000000000..0081496ce
--- /dev/null
+++ b/devtools/client/memory/test/chrome/test_SnapshotListItem_01.html
@@ -0,0 +1,53 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test to verify that the delete button only shows up for a snapshot when it has a
+path.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+ <div id="container"></div>
+
+ <pre id="test">
+ <script src="head.js" type="application/javascript;version=1.8"></script>
+ <script type="application/javascript;version=1.8">
+ window.onload = Task.async(function* () {
+ try {
+ const container = document.getElementById("container");
+
+ yield renderComponent(
+ SnapshotListItem(TEST_SNAPSHOT_LIST_ITEM_PROPS),
+ container
+ );
+
+ ok(container.querySelector('.delete'),
+ "Should have delete button when there is a path");
+
+ const pathlessProps = immutableUpdate(
+ TEST_SNAPSHOT_LIST_ITEM_PROPS,
+ {item: immutableUpdate(TEST_SNAPSHOT, {path: null})}
+ );
+
+ yield renderComponent(
+ SnapshotListItem(pathlessProps),
+ container
+ );
+
+ ok(!container.querySelector('.delete'),
+ "No delete button should be found if there is no path\n");
+
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+ });
+ </script>
+ </pre>
+</body>
+</html>
diff --git a/devtools/client/memory/test/chrome/test_Toolbar_01.html b/devtools/client/memory/test/chrome/test_Toolbar_01.html
new file mode 100644
index 000000000..57546df83
--- /dev/null
+++ b/devtools/client/memory/test/chrome/test_Toolbar_01.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that the Toolbar component shows the view switcher only at the appropriate times.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+ <div id="container"></div>
+ <pre id="test">
+ <script src="head.js" type="application/javascript;version=1.8"></script>
+ <script type="application/javascript;version=1.8">
+ window.onload = Task.async(function* () {
+ try {
+ const container = document.getElementById("container");
+
+ // Census and dominator tree views.
+
+ for (let view of [viewState.CENSUS, viewState.DOMINATOR_TREE]) {
+ yield renderComponent(Toolbar(immutableUpdate(TEST_TOOLBAR_PROPS, {
+ view: { state: view },
+ })), container);
+
+ ok(container.querySelector("#select-view"),
+ `The view selector is shown in view = ${view}`);
+ }
+
+ yield renderComponent(Toolbar(immutableUpdate(TEST_TOOLBAR_PROPS, {
+ view: { state: viewState.DIFFING, },
+ })), container);
+
+ ok(!container.querySelector("#select-view"),
+ "The view selector is NOT shown in the DIFFING view");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+ });
+ </script>
+ </pre>
+</body>
+</html>
diff --git a/devtools/client/memory/test/chrome/test_TreeMap_01.html b/devtools/client/memory/test/chrome/test_TreeMap_01.html
new file mode 100644
index 000000000..cdc293854
--- /dev/null
+++ b/devtools/client/memory/test/chrome/test_TreeMap_01.html
@@ -0,0 +1,44 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that the Tree Map correctly renders onto 2 managed canvases.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+
+ <script type="application/javascript"
+ src="chrome://devtools/content/shared/vendor/d3.js">
+ </script>
+</head>
+<body>
+ <!-- Give the container height so that the whole tree is rendered. -->
+ <div id="container" style="height: 900px;"></div>
+
+ <pre id="test">
+ <script src="head.js" type="application/javascript;version=1.8"></script>
+ <script type="application/javascript;version=1.8">
+ window.onload = Task.async(function*() {
+ try {
+ const container = document.getElementById("container");
+
+ yield renderComponent(TreeMap(TEST_TREE_MAP_PROPS), container);
+
+ let treeMapContainer = container.querySelector(".tree-map-container");
+ ok(treeMapContainer, "Component creates a container");
+
+ let canvases = treeMapContainer.querySelectorAll("canvas");
+ is(canvases.length, 2, "Creates 2 canvases");
+
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+ });
+ </script>
+ </pre>
+</body>
+</html>
diff --git a/devtools/client/memory/test/unit/.eslintrc.js b/devtools/client/memory/test/unit/.eslintrc.js
new file mode 100644
index 000000000..aec096a0f
--- /dev/null
+++ b/devtools/client/memory/test/unit/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../../../.eslintrc.xpcshell.js"
+};
diff --git a/devtools/client/memory/test/unit/head.js b/devtools/client/memory/test/unit/head.js
new file mode 100644
index 000000000..f1335dc3d
--- /dev/null
+++ b/devtools/client/memory/test/unit/head.js
@@ -0,0 +1,128 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+var { console } = Cu.import("resource://gre/modules/Console.jsm", {});
+var { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+
+var Services = require("Services");
+var DevToolsUtils = require("devtools/shared/DevToolsUtils");
+var flags = require("devtools/shared/flags");
+flags.testing = true;
+flags.wantLogging = true;
+flags.wantVerbose = false;
+
+var { OS } = require("resource://gre/modules/osfile.jsm");
+var { FileUtils } = require("resource://gre/modules/FileUtils.jsm");
+var { TargetFactory } = require("devtools/client/framework/target");
+var promise = require("promise");
+var defer = require("devtools/shared/defer");
+var { Task } = require("devtools/shared/task");
+var { expectState } = require("devtools/server/actors/common");
+var HeapSnapshotFileUtils = require("devtools/shared/heapsnapshot/HeapSnapshotFileUtils");
+var HeapAnalysesClient = require("devtools/shared/heapsnapshot/HeapAnalysesClient");
+var { addDebuggerToGlobal } = require("resource://gre/modules/jsdebugger.jsm");
+var Store = require("devtools/client/memory/store");
+var { L10N } = require("devtools/client/memory/utils");
+var SYSTEM_PRINCIPAL = Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal);
+
+var EXPECTED_DTU_ASSERT_FAILURE_COUNT = 0;
+
+do_register_cleanup(function () {
+ equal(DevToolsUtils.assertionFailureCount, EXPECTED_DTU_ASSERT_FAILURE_COUNT,
+ "Should have had the expected number of DevToolsUtils.assert() failures.");
+});
+
+function dumpn(msg) {
+ dump(`MEMORY-TEST: ${msg}\n`);
+}
+
+function initDebugger() {
+ let global = new Cu.Sandbox(SYSTEM_PRINCIPAL, { freshZone: true });
+ addDebuggerToGlobal(global);
+ return new global.Debugger();
+}
+
+function StubbedMemoryFront() {
+ this.state = "detached";
+ this.recordingAllocations = false;
+ this.dbg = initDebugger();
+}
+
+StubbedMemoryFront.prototype.attach = Task.async(function* () {
+ this.state = "attached";
+});
+
+StubbedMemoryFront.prototype.detach = Task.async(function* () {
+ this.state = "detached";
+});
+
+StubbedMemoryFront.prototype.saveHeapSnapshot = expectState("attached", Task.async(function* () {
+ return ThreadSafeChromeUtils.saveHeapSnapshot({ runtime: true });
+}), "saveHeapSnapshot");
+
+StubbedMemoryFront.prototype.startRecordingAllocations = expectState("attached", Task.async(function* () {
+ this.recordingAllocations = true;
+}));
+
+StubbedMemoryFront.prototype.stopRecordingAllocations = expectState("attached", Task.async(function* () {
+ this.recordingAllocations = false;
+}));
+
+function waitUntilSnapshotState(store, expected) {
+ let predicate = () => {
+ let snapshots = store.getState().snapshots;
+ do_print(snapshots.map(x => x.state));
+ return snapshots.length === expected.length &&
+ expected.every((state, i) => state === "*" || snapshots[i].state === state);
+ };
+ do_print(`Waiting for snapshots to be of state: ${expected}`);
+ return waitUntilState(store, predicate);
+}
+
+function findReportLeafIndex(node, name = null) {
+ if (node.reportLeafIndex && (!name || node.name === name)) {
+ return node.reportLeafIndex;
+ }
+
+ if (node.children) {
+ for (let child of node.children) {
+ const found = findReportLeafIndex(child);
+ if (found) {
+ return found;
+ }
+ }
+ }
+
+ return null;
+}
+
+function waitUntilCensusState(store, getCensus, expected) {
+ let predicate = () => {
+ let snapshots = store.getState().snapshots;
+
+ do_print("Current census state:" +
+ snapshots.map(x => getCensus(x) ? getCensus(x).state : null));
+
+ return snapshots.length === expected.length &&
+ expected.every((state, i) => {
+ let census = getCensus(snapshots[i]);
+ return (state === "*") ||
+ (!census && !state) ||
+ (census && census.state === state);
+ });
+ };
+ do_print(`Waiting for snapshots' censuses to be of state: ${expected}`);
+ return waitUntilState(store, predicate);
+}
+
+function* createTempFile() {
+ let file = FileUtils.getFile("TmpD", ["tmp.fxsnapshot"]);
+ file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
+ let destPath = file.path;
+ let stat = yield OS.File.stat(destPath);
+ ok(stat.size === 0, "new file is 0 bytes at start");
+ return destPath;
+}
diff --git a/devtools/client/memory/test/unit/test_action-clear-snapshots_01.js b/devtools/client/memory/test/unit/test_action-clear-snapshots_01.js
new file mode 100644
index 000000000..a4e611e84
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_action-clear-snapshots_01.js
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test clearSnapshots deletes snapshots with READ censuses
+
+let { takeSnapshotAndCensus, clearSnapshots } = require("devtools/client/memory/actions/snapshot");
+let { snapshotState: states, actions } = require("devtools/client/memory/constants");
+const { treeMapState } = require("devtools/client/memory/constants");
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ let front = new StubbedMemoryFront();
+ let heapWorker = new HeapAnalysesClient();
+ yield front.attach();
+ let store = Store();
+ const { getState, dispatch } = store;
+
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ yield waitUntilCensusState(store, s => s.treeMap, [treeMapState.SAVED]);
+ ok(true, "snapshot created");
+
+ ok(true, "dispatch clearSnapshots action");
+ let deleteEvents = Promise.all([
+ waitUntilAction(store, actions.DELETE_SNAPSHOTS_START),
+ waitUntilAction(store, actions.DELETE_SNAPSHOTS_END)
+ ]);
+ dispatch(clearSnapshots(heapWorker));
+ yield deleteEvents;
+ ok(true, "received delete snapshots events");
+
+ equal(getState().snapshots.length, 0, "no snapshot remaining");
+
+ heapWorker.destroy();
+ yield front.detach();
+});
diff --git a/devtools/client/memory/test/unit/test_action-clear-snapshots_02.js b/devtools/client/memory/test/unit/test_action-clear-snapshots_02.js
new file mode 100644
index 000000000..bb8c118cd
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_action-clear-snapshots_02.js
@@ -0,0 +1,47 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test clearSnapshots preserves snapshots with state != READ or ERROR
+
+let { takeSnapshotAndCensus, clearSnapshots, takeSnapshot } = require("devtools/client/memory/actions/snapshot");
+let { snapshotState: states, treeMapState, actions } = require("devtools/client/memory/constants");
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ let front = new StubbedMemoryFront();
+ let heapWorker = new HeapAnalysesClient();
+ yield front.attach();
+ let store = Store();
+ const { getState, dispatch } = store;
+
+ ok(true, "create a snapshot with a census in SAVED state");
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ ok(true, "create a snapshot in SAVED state");
+ dispatch(takeSnapshot(front));
+ yield waitUntilSnapshotState(store, [states.SAVED, states.SAVED]);
+ yield waitUntilCensusState(store, snapshot => snapshot.treeMap,
+ [treeMapState.SAVED, null]);
+ ok(true, "snapshots created with expected states");
+
+ ok(true, "dispatch clearSnapshots action");
+ let deleteEvents = Promise.all([
+ waitUntilAction(store, actions.DELETE_SNAPSHOTS_START),
+ waitUntilAction(store, actions.DELETE_SNAPSHOTS_END)
+ ]);
+ dispatch(clearSnapshots(heapWorker));
+ yield deleteEvents;
+ ok(true, "received delete snapshots events");
+
+ equal(getState().snapshots.length, 1, "one snapshot remaining");
+ let remainingSnapshot = getState().snapshots[0];
+ equal(remainingSnapshot.treeMap, undefined,
+ "remaining snapshot doesn't have a treeMap property");
+ equal(remainingSnapshot.census, undefined,
+ "remaining snapshot doesn't have a census property");
+
+ heapWorker.destroy();
+ yield front.detach();
+});
diff --git a/devtools/client/memory/test/unit/test_action-clear-snapshots_03.js b/devtools/client/memory/test/unit/test_action-clear-snapshots_03.js
new file mode 100644
index 000000000..ae888f858
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_action-clear-snapshots_03.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test clearSnapshots deletes snapshots with state ERROR
+
+let { takeSnapshotAndCensus, clearSnapshots } = require("devtools/client/memory/actions/snapshot");
+let { snapshotState: states, treeMapState, actions } = require("devtools/client/memory/constants");
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ let front = new StubbedMemoryFront();
+ let heapWorker = new HeapAnalysesClient();
+ yield front.attach();
+ let store = Store();
+ const { getState, dispatch } = store;
+
+ ok(true, "create a snapshot with a treeMap");
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ yield waitUntilSnapshotState(store, [states.SAVED]);
+ ok(true, "snapshot created with a SAVED state");
+ yield waitUntilCensusState(store, snapshot => snapshot.treeMap,
+ [treeMapState.SAVED]);
+ ok(true, "treeMap created with a SAVED state");
+
+ ok(true, "set snapshot state to error");
+ let id = getState().snapshots[0].id;
+ dispatch({ type: actions.SNAPSHOT_ERROR, id, error: new Error("_") });
+ yield waitUntilSnapshotState(store, [states.ERROR]);
+ ok(true, "snapshot set to error state");
+
+ ok(true, "dispatch clearSnapshots action");
+ let deleteEvents = Promise.all([
+ waitUntilAction(store, actions.DELETE_SNAPSHOTS_START),
+ waitUntilAction(store, actions.DELETE_SNAPSHOTS_END)
+ ]);
+ dispatch(clearSnapshots(heapWorker));
+ yield deleteEvents;
+ ok(true, "received delete snapshots events");
+ equal(getState().snapshots.length, 0, "error snapshot deleted");
+
+ heapWorker.destroy();
+ yield front.detach();
+});
diff --git a/devtools/client/memory/test/unit/test_action-clear-snapshots_04.js b/devtools/client/memory/test/unit/test_action-clear-snapshots_04.js
new file mode 100644
index 000000000..f5b316b1a
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_action-clear-snapshots_04.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test clearSnapshots deletes several snapshots
+
+let { takeSnapshotAndCensus, clearSnapshots } = require("devtools/client/memory/actions/snapshot");
+let { snapshotState: states, actions, treeMapState } = require("devtools/client/memory/constants");
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ let front = new StubbedMemoryFront();
+ let heapWorker = new HeapAnalysesClient();
+ yield front.attach();
+ let store = Store();
+ const { getState, dispatch } = store;
+
+ ok(true, "create 3 snapshots with a saved census");
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ yield waitUntilCensusState(store, snapshot => snapshot.treeMap,
+ [treeMapState.SAVED, treeMapState.SAVED,
+ treeMapState.SAVED]);
+ ok(true, "snapshots created with a saved census");
+
+ ok(true, "set first snapshot state to error");
+ let id = getState().snapshots[0].id;
+ dispatch({ type: actions.SNAPSHOT_ERROR, id, error: new Error("_") });
+ yield waitUntilSnapshotState(store,
+ [states.ERROR, states.READ, states.READ]);
+ ok(true, "first snapshot set to error state");
+
+ ok(true, "dispatch clearSnapshots action");
+ let deleteEvents = Promise.all([
+ waitUntilAction(store, actions.DELETE_SNAPSHOTS_START),
+ waitUntilAction(store, actions.DELETE_SNAPSHOTS_END)
+ ]);
+ dispatch(clearSnapshots(heapWorker));
+ yield deleteEvents;
+ ok(true, "received delete snapshots events");
+
+ equal(getState().snapshots.length, 0, "no snapshot remaining");
+
+ heapWorker.destroy();
+ yield front.detach();
+});
diff --git a/devtools/client/memory/test/unit/test_action-clear-snapshots_05.js b/devtools/client/memory/test/unit/test_action-clear-snapshots_05.js
new file mode 100644
index 000000000..9b889b3a4
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_action-clear-snapshots_05.js
@@ -0,0 +1,47 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test clearSnapshots deletes several snapshots
+
+let { takeSnapshotAndCensus, clearSnapshots } = require("devtools/client/memory/actions/snapshot");
+let { snapshotState: states, actions, treeMapState } = require("devtools/client/memory/constants");
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ let front = new StubbedMemoryFront();
+ let heapWorker = new HeapAnalysesClient();
+ yield front.attach();
+ let store = Store();
+ const { getState, dispatch } = store;
+
+ ok(true, "create 2 snapshots with a saved census");
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ ok(true, "snapshots created with a saved census");
+ yield waitUntilCensusState(store, snapshot => snapshot.treeMap,
+ [treeMapState.SAVED, treeMapState.SAVED]);
+
+ let errorHeapWorker = {
+ deleteHeapSnapshot: function () {
+ return Promise.reject("_");
+ }
+ };
+
+ ok(true, "dispatch clearSnapshots action");
+ let deleteEvents = Promise.all([
+ waitUntilAction(store, actions.DELETE_SNAPSHOTS_START),
+ waitUntilAction(store, actions.DELETE_SNAPSHOTS_END),
+ waitUntilAction(store, actions.SNAPSHOT_ERROR),
+ waitUntilAction(store, actions.SNAPSHOT_ERROR),
+ ]);
+ dispatch(clearSnapshots(errorHeapWorker));
+ yield deleteEvents;
+ ok(true, "received delete snapshots and snapshot error events");
+ equal(getState().snapshots.length, 0, "no snapshot remaining");
+
+ heapWorker.destroy();
+ yield front.detach();
+});
diff --git a/devtools/client/memory/test/unit/test_action-clear-snapshots_06.js b/devtools/client/memory/test/unit/test_action-clear-snapshots_06.js
new file mode 100644
index 000000000..1ee32bb12
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_action-clear-snapshots_06.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that clearSnapshots disables diffing when deleting snapshots
+
+const {
+ takeSnapshotAndCensus,
+ clearSnapshots
+} = require("devtools/client/memory/actions/snapshot");
+const {
+ snapshotState: states,
+ actions,
+ treeMapState
+} = require("devtools/client/memory/constants");
+const {
+ toggleDiffing,
+ selectSnapshotForDiffingAndRefresh
+} = require("devtools/client/memory/actions/diffing");
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ let front = new StubbedMemoryFront();
+ let heapWorker = new HeapAnalysesClient();
+ yield front.attach();
+ let store = Store();
+ const { getState, dispatch } = store;
+
+ ok(true, "create 2 snapshots with a saved census");
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ yield waitUntilCensusState(store, snapshot => snapshot.treeMap,
+ [treeMapState.SAVED, treeMapState.SAVED]);
+ ok(true, "snapshots created with a saved census");
+
+ dispatch(toggleDiffing());
+ dispatch(selectSnapshotForDiffingAndRefresh(heapWorker,
+ getState().snapshots[0]));
+ dispatch(selectSnapshotForDiffingAndRefresh(heapWorker,
+ getState().snapshots[1]));
+
+ ok(getState().diffing, "We should be in diffing view");
+
+ yield waitUntilAction(store, actions.TAKE_CENSUS_DIFF_END);
+ ok(true, "Received TAKE_CENSUS_DIFF_END action");
+
+ ok(true, "Dispatch clearSnapshots action");
+ let deleteEvents = Promise.all([
+ waitUntilAction(store, actions.DELETE_SNAPSHOTS_START),
+ waitUntilAction(store, actions.DELETE_SNAPSHOTS_END)
+ ]);
+ dispatch(clearSnapshots(heapWorker));
+ yield deleteEvents;
+ ok(true, "received delete snapshots events");
+
+ ok(getState().snapshots.length === 0, "Snapshots array should be empty");
+ ok(!getState().diffing, "We should no longer be diffing");
+
+ heapWorker.destroy();
+ yield front.detach();
+});
diff --git a/devtools/client/memory/test/unit/test_action-export-snapshot.js b/devtools/client/memory/test/unit/test_action-export-snapshot.js
new file mode 100644
index 000000000..0582abbf0
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_action-export-snapshot.js
@@ -0,0 +1,39 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test exporting a snapshot to a user specified location on disk.
+
+let { exportSnapshot } = require("devtools/client/memory/actions/io");
+let { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot");
+let { snapshotState: states, actions, treeMapState } = require("devtools/client/memory/constants");
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ let front = new StubbedMemoryFront();
+ let heapWorker = new HeapAnalysesClient();
+ yield front.attach();
+ let store = Store();
+ const { getState, dispatch } = store;
+
+ let destPath = yield createTempFile();
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ yield waitUntilCensusState(store, snapshot => snapshot.treeMap,
+ [treeMapState.SAVED]);
+
+ let exportEvents = Promise.all([
+ waitUntilAction(store, actions.EXPORT_SNAPSHOT_START),
+ waitUntilAction(store, actions.EXPORT_SNAPSHOT_END)
+ ]);
+ dispatch(exportSnapshot(getState().snapshots[0], destPath));
+ yield exportEvents;
+
+ stat = yield OS.File.stat(destPath);
+ do_print(stat.size);
+ ok(stat.size > 0, "destination file is more than 0 bytes");
+
+ heapWorker.destroy();
+ yield front.detach();
+});
diff --git a/devtools/client/memory/test/unit/test_action-filter-01.js b/devtools/client/memory/test/unit/test_action-filter-01.js
new file mode 100644
index 000000000..e0894606d
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_action-filter-01.js
@@ -0,0 +1,23 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test setting the filter string.
+
+let { setFilterString } = require("devtools/client/memory/actions/filter");
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ let store = Store();
+ const { getState, dispatch } = store;
+
+ equal(getState().filter, null, "no filter by default");
+
+ dispatch(setFilterString("my filter"));
+ equal(getState().filter, "my filter", "now we have the expected filter");
+
+ dispatch(setFilterString(""));
+ equal(getState().filter, null, "no filter again");
+});
diff --git a/devtools/client/memory/test/unit/test_action-filter-02.js b/devtools/client/memory/test/unit/test_action-filter-02.js
new file mode 100644
index 000000000..31d777704
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_action-filter-02.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that changing filter state properly refreshes the selected census.
+
+let { snapshotState: states, viewState, censusState } = require("devtools/client/memory/constants");
+let { setFilterStringAndRefresh } = require("devtools/client/memory/actions/filter");
+let { takeSnapshotAndCensus, selectSnapshotAndRefresh } = require("devtools/client/memory/actions/snapshot");
+let { setCensusDisplay } = require("devtools/client/memory/actions/census-display");
+let { changeView } = require("devtools/client/memory/actions/view");
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ let front = new StubbedMemoryFront();
+ let heapWorker = new HeapAnalysesClient();
+ yield front.attach();
+ let store = Store();
+ let { getState, dispatch } = store;
+
+ dispatch(changeView(viewState.CENSUS));
+
+ equal(getState().filter, null, "no filter by default");
+
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+
+ yield waitUntilCensusState(store, snapshot => snapshot.census,
+ [censusState.SAVED,
+ censusState.SAVED,
+ censusState.SAVED]);
+ ok(true, "saved 3 snapshots and took a census of each of them");
+
+ dispatch(setFilterStringAndRefresh("str", heapWorker));
+ yield waitUntilCensusState(store, snapshot => snapshot.census,
+ [censusState.SAVED,
+ censusState.SAVED,
+ censusState.SAVING]);
+ ok(true, "setting filter string should recompute the selected snapshot's census");
+
+ equal(getState().filter, "str", "now inverted");
+
+ yield waitUntilCensusState(store, snapshot => snapshot.census,
+ [censusState.SAVED,
+ censusState.SAVED,
+ censusState.SAVED]);
+
+ equal(getState().snapshots[0].census.filter, null);
+ equal(getState().snapshots[1].census.filter, null);
+ equal(getState().snapshots[2].census.filter, "str");
+
+ dispatch(selectSnapshotAndRefresh(heapWorker, getState().snapshots[1].id));
+ yield waitUntilCensusState(store, snapshot => snapshot.census,
+ [censusState.SAVED,
+ censusState.SAVING,
+ censusState.SAVED]);
+ ok(true, "selecting non-inverted census should trigger a recompute");
+
+ yield waitUntilCensusState(store, snapshot => snapshot.census,
+ [censusState.SAVED,
+ censusState.SAVED,
+ censusState.SAVED]);
+
+ equal(getState().snapshots[0].census.filter, null);
+ equal(getState().snapshots[1].census.filter, "str");
+ equal(getState().snapshots[2].census.filter, "str");
+
+ heapWorker.destroy();
+ yield front.detach();
+});
diff --git a/devtools/client/memory/test/unit/test_action-filter-03.js b/devtools/client/memory/test/unit/test_action-filter-03.js
new file mode 100644
index 000000000..4d46d5361
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_action-filter-03.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that changing filter state in the middle of taking a snapshot results in
+// the properly fitered census.
+
+let { snapshotState: states, censusState, viewState } = require("devtools/client/memory/constants");
+let { setFilterString, setFilterStringAndRefresh } = require("devtools/client/memory/actions/filter");
+let { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot");
+let { changeView } = require("devtools/client/memory/actions/view");
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ let front = new StubbedMemoryFront();
+ let heapWorker = new HeapAnalysesClient();
+ yield front.attach();
+ let store = Store();
+ let { getState, dispatch } = store;
+
+ dispatch(changeView(viewState.CENSUS));
+
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ yield waitUntilSnapshotState(store, [states.SAVING]);
+
+ dispatch(setFilterString("str"));
+
+ yield waitUntilCensusState(store, snapshot => snapshot.census,
+ [censusState.SAVED]);
+ equal(getState().filter, "str",
+ "should want filtered trees");
+ equal(getState().snapshots[0].census.filter, "str",
+ "snapshot-we-were-in-the-middle-of-saving's census should be filtered");
+
+ dispatch(setFilterStringAndRefresh("", heapWorker));
+ yield waitUntilCensusState(store, snapshot => snapshot.census,
+ [censusState.SAVING]);
+ ok(true, "changing filter string retriggers census");
+ ok(!getState().filter, "no longer filtering");
+
+ dispatch(setFilterString("obj"));
+ yield waitUntilCensusState(store, snapshot => snapshot.census,
+ [censusState.SAVED]);
+ equal(getState().filter, "obj", "filtering for obj now");
+ equal(getState().snapshots[0].census.filter, "obj",
+ "census-we-were-in-the-middle-of-recomputing should be filtered again");
+
+ heapWorker.destroy();
+ yield front.detach();
+});
diff --git a/devtools/client/memory/test/unit/test_action-import-snapshot-and-census.js b/devtools/client/memory/test/unit/test_action-import-snapshot-and-census.js
new file mode 100644
index 000000000..a7e1d366a
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_action-import-snapshot-and-census.js
@@ -0,0 +1,98 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests the task creator `importSnapshotAndCensus()` for the whole flow of
+ * importing a snapshot, and its sub-actions.
+ */
+
+let { actions, snapshotState: states, treeMapState } = require("devtools/client/memory/constants");
+let { exportSnapshot, importSnapshotAndCensus } = require("devtools/client/memory/actions/io");
+let { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot");
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ let front = new StubbedMemoryFront();
+ let heapWorker = new HeapAnalysesClient();
+ yield front.attach();
+ let store = Store();
+ let { subscribe, dispatch, getState } = store;
+
+ let destPath = yield createTempFile();
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ yield waitUntilCensusState(store, s => s.treeMap, [treeMapState.SAVED]);
+
+ let exportEvents = Promise.all([
+ waitUntilAction(store, actions.EXPORT_SNAPSHOT_START),
+ waitUntilAction(store, actions.EXPORT_SNAPSHOT_END)
+ ]);
+ dispatch(exportSnapshot(getState().snapshots[0], destPath));
+ yield exportEvents;
+
+ // Now import our freshly exported snapshot
+ let snapshotI = 0;
+ let censusI = 0;
+ let snapshotStates = ["IMPORTING", "READING", "READ"];
+ let censusStates = ["SAVING", "SAVED"];
+ let expectStates = () => {
+ let snapshot = getState().snapshots[1];
+ if (!snapshot) {
+ return;
+ }
+ if (snapshotI < snapshotStates.length) {
+ let isCorrectState = snapshot.state === states[snapshotStates[snapshotI]];
+ if (isCorrectState) {
+ ok(true, `Found expected snapshot state ${snapshotStates[snapshotI]}`);
+ snapshotI++;
+ }
+ }
+ if (snapshot.treeMap && censusI < censusStates.length) {
+ if (snapshot.treeMap.state === treeMapState[censusStates[censusI]]) {
+ ok(true, `Found expected census state ${censusStates[censusI]}`);
+ censusI++;
+ }
+ }
+ };
+
+ let unsubscribe = subscribe(expectStates);
+ dispatch(importSnapshotAndCensus(heapWorker, destPath));
+
+ yield waitUntilState(store, () => { return snapshotI === snapshotStates.length &&
+ censusI === censusStates.length; });
+ unsubscribe();
+ equal(snapshotI, snapshotStates.length, "importSnapshotAndCensus() produces the correct sequence of states in a snapshot");
+ equal(getState().snapshots[1].state, states.READ, "imported snapshot is in READ state");
+ equal(censusI, censusStates.length, "importSnapshotAndCensus() produces the correct sequence of states in a census");
+ equal(getState().snapshots[1].treeMap.state, treeMapState.SAVED, "imported snapshot is in READ state");
+ ok(getState().snapshots[1].selected, "imported snapshot is selected");
+
+ // Check snapshot data
+ let snapshot1 = getState().snapshots[0];
+ let snapshot2 = getState().snapshots[1];
+
+ equal(snapshot1.treeMap.display, snapshot2.treeMap.display,
+ "imported snapshot has correct display");
+
+ // Clone the census data so we can destructively remove the ID/parents to compare
+ // equal census data
+ let census1 = stripUnique(JSON.parse(JSON.stringify(snapshot1.treeMap.report)));
+ let census2 = stripUnique(JSON.parse(JSON.stringify(snapshot2.treeMap.report)));
+
+ equal(JSON.stringify(census1), JSON.stringify(census2), "Imported snapshot has correct census");
+
+ function stripUnique(obj) {
+ let children = obj.children || [];
+ for (let child of children) {
+ delete child.id;
+ delete child.parent;
+ stripUnique(child);
+ }
+ delete obj.id;
+ delete obj.parent;
+ return obj;
+ }
+});
diff --git a/devtools/client/memory/test/unit/test_action-import-snapshot-dominator-tree.js b/devtools/client/memory/test/unit/test_action-import-snapshot-dominator-tree.js
new file mode 100644
index 000000000..868f22ccf
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_action-import-snapshot-dominator-tree.js
@@ -0,0 +1,84 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests `importSnapshotAndCensus()` when importing snapshots from the dominator
+ * tree view. The snapshot is expected to be loaded and its dominator tree
+ * should be computed.
+ */
+
+let { snapshotState, dominatorTreeState, viewState, treeMapState } =
+ require("devtools/client/memory/constants");
+let { importSnapshotAndCensus } = require("devtools/client/memory/actions/io");
+let { changeViewAndRefresh } = require("devtools/client/memory/actions/view");
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ let front = new StubbedMemoryFront();
+ let heapWorker = new HeapAnalysesClient();
+ yield front.attach();
+ let store = Store();
+ let { subscribe, dispatch, getState } = store;
+
+ dispatch(changeViewAndRefresh(viewState.DOMINATOR_TREE, heapWorker));
+ equal(getState().view.state, viewState.DOMINATOR_TREE,
+ "We should now be in the DOMINATOR_TREE view");
+
+ let i = 0;
+ let expected = [
+ "IMPORTING",
+ "READING",
+ "READ",
+ "treeMap:SAVING",
+ "treeMap:SAVED",
+ "dominatorTree:COMPUTING",
+ "dominatorTree:FETCHING",
+ "dominatorTree:LOADED",
+ ];
+ let expectStates = () => {
+ let snapshot = getState().snapshots[0];
+ if (snapshot && hasExpectedState(snapshot, expected[i])) {
+ ok(true, `Found expected state ${expected[i]}`);
+ i++;
+ }
+ };
+
+ let unsubscribe = subscribe(expectStates);
+ const snapshotPath = yield front.saveHeapSnapshot();
+ dispatch(importSnapshotAndCensus(heapWorker, snapshotPath));
+
+ yield waitUntilState(store, () => i === expected.length);
+ unsubscribe();
+ equal(i, expected.length, "importSnapshotAndCensus() produces the correct " +
+ "sequence of states in a snapshot");
+ equal(getState().snapshots[0].dominatorTree.state, dominatorTreeState.LOADED,
+ "imported snapshot's dominator tree is in LOADED state");
+ ok(getState().snapshots[0].selected, "imported snapshot is selected");
+});
+
+/**
+ * Check that the provided snapshot is in the expected state. The expected state
+ * is a snapshotState by default. If the expected state is prefixed by
+ * dominatorTree, a dominatorTree is expected on the provided snapshot, in the
+ * corresponding state from dominatorTreeState.
+ */
+function hasExpectedState(snapshot, expectedState) {
+ let isDominatorState = expectedState.indexOf("dominatorTree:") === 0;
+ if (isDominatorState) {
+ let state = dominatorTreeState[expectedState.replace("dominatorTree:", "")];
+ return snapshot.dominatorTree && snapshot.dominatorTree.state === state;
+ }
+
+ let isTreeMapState = expectedState.indexOf("treeMap:") === 0;
+ if (isTreeMapState) {
+ let state = treeMapState[expectedState.replace("treeMap:", "")];
+ return snapshot.treeMap && snapshot.treeMap.state === state;
+ }
+
+ let state = snapshotState[expectedState];
+ return snapshot.state === state;
+}
diff --git a/devtools/client/memory/test/unit/test_action-select-snapshot.js b/devtools/client/memory/test/unit/test_action-select-snapshot.js
new file mode 100644
index 000000000..2329ab2fa
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_action-select-snapshot.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests the reducer responding to the action `selectSnapshot(snapshot)`
+ */
+
+let actions = require("devtools/client/memory/actions/snapshot");
+let { snapshotState: states } = require("devtools/client/memory/constants");
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ let front = new StubbedMemoryFront();
+ yield front.attach();
+ let store = Store();
+
+ for (let i = 0; i < 5; i++) {
+ store.dispatch(actions.takeSnapshot(front));
+ }
+
+ yield waitUntilState(store, ({ snapshots }) => snapshots.length === 5 && snapshots.every(isDone));
+
+ for (let i = 0; i < 5; i++) {
+ do_print(`Selecting snapshot[${i}]`);
+ store.dispatch(actions.selectSnapshot(store.getState().snapshots[i].id));
+ yield waitUntilState(store, ({ snapshots }) => snapshots[i].selected);
+
+ let { snapshots } = store.getState();
+ ok(snapshots[i].selected, `snapshot[${i}] selected`);
+ equal(snapshots.filter(s => !s.selected).length, 4, "All other snapshots are unselected");
+ }
+});
+
+function isDone(s) { return s.state === states.SAVED; }
diff --git a/devtools/client/memory/test/unit/test_action-set-display-and-refresh-01.js b/devtools/client/memory/test/unit/test_action-set-display-and-refresh-01.js
new file mode 100644
index 000000000..570ffdf05
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_action-set-display-and-refresh-01.js
@@ -0,0 +1,118 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests the task creator `setCensusDisplayAndRefreshAndRefresh()` for display
+ * changing. We test this rather than `setCensusDisplayAndRefresh` directly, as
+ * we use the refresh action in the app itself composed from
+ * `setCensusDisplayAndRefresh`.
+ */
+
+let { censusDisplays, snapshotState: states, censusState, viewState } = require("devtools/client/memory/constants");
+let { setCensusDisplayAndRefresh } = require("devtools/client/memory/actions/census-display");
+let { takeSnapshotAndCensus, selectSnapshotAndRefresh } = require("devtools/client/memory/actions/snapshot");
+const { changeView } = require("devtools/client/memory/actions/view");
+
+function run_test() {
+ run_next_test();
+}
+
+// We test setting an invalid display, which triggers an assertion failure.
+EXPECTED_DTU_ASSERT_FAILURE_COUNT = 1;
+
+add_task(function* () {
+ let front = new StubbedMemoryFront();
+ let heapWorker = new HeapAnalysesClient();
+ yield front.attach();
+ let store = Store();
+ let { getState, dispatch } = store;
+
+ dispatch(changeView(viewState.CENSUS));
+
+ // Test default display with no snapshots
+ equal(getState().censusDisplay.breakdown.by, "coarseType",
+ "default coarseType display selected at start.");
+ dispatch(setCensusDisplayAndRefresh(heapWorker,
+ censusDisplays.allocationStack));
+ equal(getState().censusDisplay.breakdown.by, "allocationStack",
+ "display changed with no snapshots");
+
+ // Test invalid displays
+ ok(getState().errors.length === 0, "No error actions in the queue.");
+ dispatch(setCensusDisplayAndRefresh(heapWorker, {}));
+ yield waitUntilState(store, () => getState().errors.length === 1);
+ ok(true, "Emits an error action when passing in an invalid display object");
+
+ equal(getState().censusDisplay.breakdown.by, "allocationStack",
+ "current display unchanged when passing invalid display");
+
+ // Test new snapshots
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ yield waitUntilCensusState(store, snapshot => snapshot.census,
+ [censusState.SAVED]);
+
+ equal(getState().snapshots[0].census.display, censusDisplays.allocationStack,
+ "New snapshot's census uses correct display");
+
+ // Updates when changing display during `SAVING`
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ yield waitUntilCensusState(store, snapshot => snapshot.census,
+ [censusState.SAVED, censusState.SAVING]);
+ dispatch(setCensusDisplayAndRefresh(heapWorker, censusDisplays.coarseType));
+ yield waitUntilCensusState(store, snapshot => snapshot.census,
+ [censusState.SAVED, censusState.SAVED]);
+ equal(getState().snapshots[1].census.display, censusDisplays.coarseType,
+ "Changing display while saving a snapshot results in a census using the new display");
+
+
+ // Updates when changing display during `SAVING_CENSUS`
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ yield waitUntilCensusState(store, snapshot => snapshot.census,
+ [censusState.SAVED,
+ censusState.SAVED,
+ censusState.SAVING]);
+ dispatch(setCensusDisplayAndRefresh(heapWorker, censusDisplays.allocationStack));
+ yield waitUntilCensusState(store, snapshot => snapshot.census,
+ [censusState.SAVED,
+ censusState.SAVED,
+ censusState.SAVED]);
+ equal(getState().snapshots[2].census.display, censusDisplays.allocationStack,
+ "Display can be changed while saving census, stores updated display in snapshot");
+
+ // Updates census on currently selected snapshot when changing display
+ ok(getState().snapshots[2].selected, "Third snapshot currently selected");
+ dispatch(setCensusDisplayAndRefresh(heapWorker, censusDisplays.coarseType));
+ yield waitUntilState(store, state => state.snapshots[2].census.state === censusState.SAVING);
+ yield waitUntilState(store, state => state.snapshots[2].census.state === censusState.SAVED);
+ equal(getState().snapshots[2].census.display, censusDisplays.coarseType,
+ "Snapshot census updated when changing displays after already generating one census");
+
+ dispatch(setCensusDisplayAndRefresh(heapWorker, censusDisplays.allocationStack));
+ yield waitUntilState(store, state => state.snapshots[2].census.state === censusState.SAVED);
+ equal(getState().snapshots[2].census.display, censusDisplays.allocationStack,
+ "Snapshot census updated when changing displays after already generating one census");
+
+ // Does not update unselected censuses.
+ ok(!getState().snapshots[1].selected, "Second snapshot selected currently");
+ equal(getState().snapshots[1].census.display, censusDisplays.coarseType,
+ "Second snapshot using `coarseType` display still and not yet updated to correct display");
+
+ // Updates to current display when switching to stale snapshot.
+ dispatch(selectSnapshotAndRefresh(heapWorker, getState().snapshots[1].id));
+ yield waitUntilCensusState(store, snapshot => snapshot.census,
+ [censusState.SAVED,
+ censusState.SAVING,
+ censusState.SAVED]);
+ yield waitUntilCensusState(store, snapshot => snapshot.census,
+ [censusState.SAVED,
+ censusState.SAVED,
+ censusState.SAVED]);
+
+ ok(getState().snapshots[1].selected, "Second snapshot selected currently");
+ equal(getState().snapshots[1].census.display, censusDisplays.allocationStack,
+ "Second snapshot using `allocationStack` display and updated to correct display");
+
+ heapWorker.destroy();
+ yield front.detach();
+});
diff --git a/devtools/client/memory/test/unit/test_action-set-display-and-refresh-02.js b/devtools/client/memory/test/unit/test_action-set-display-and-refresh-02.js
new file mode 100644
index 000000000..5be5444d4
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_action-set-display-and-refresh-02.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests the task creator `setCensusDisplayAndRefreshAndRefresh()` for custom
+ * displays.
+ */
+
+let { snapshotState: states, censusState, viewState } = require("devtools/client/memory/constants");
+let { setCensusDisplayAndRefresh } = require("devtools/client/memory/actions/census-display");
+let { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot");
+let { changeView } = require("devtools/client/memory/actions/view");
+
+let CUSTOM = {
+ displayName: "Custom",
+ tooltip: "Custom tooltip",
+ inverted: false,
+ breakdown: {
+ by: "internalType",
+ then: { by: "count", bytes: true, count: false }
+ }
+};
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ let front = new StubbedMemoryFront();
+ let heapWorker = new HeapAnalysesClient();
+ yield front.attach();
+ let store = Store();
+ let { getState, dispatch } = store;
+
+ dispatch(changeView(viewState.CENSUS));
+ dispatch(setCensusDisplayAndRefresh(heapWorker, CUSTOM));
+ equal(getState().censusDisplay, CUSTOM,
+ "CUSTOM display stored in display state.");
+
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ yield waitUntilCensusState(store, s => s.census, [censusState.SAVED]);
+
+ equal(getState().snapshots[0].census.display, CUSTOM,
+ "New snapshot stored CUSTOM display when done taking census");
+ ok(getState().snapshots[0].census.report.children.length, "Census has some children");
+ // Ensure we don't have `count` in any results
+ ok(getState().snapshots[0].census.report.children.every(c => !c.count),
+ "Census used CUSTOM display without counts");
+ // Ensure we do have `bytes` in the results
+ ok(getState().snapshots[0].census.report.children.every(c => typeof c.bytes === "number"),
+ "Census used CUSTOM display with bytes");
+});
diff --git a/devtools/client/memory/test/unit/test_action-set-display.js b/devtools/client/memory/test/unit/test_action-set-display.js
new file mode 100644
index 000000000..43ea975da
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_action-set-display.js
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests the action creator `setCensusDisplay()` for display changing. Does not
+ * test refreshing the census information, check `setCensusDisplayAndRefresh`
+ * action for that.
+ */
+
+let { censusDisplays, snapshotState: states, censusState, viewState } = require("devtools/client/memory/constants");
+let { setCensusDisplay } = require("devtools/client/memory/actions/census-display");
+let { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot");
+const { changeView } = require("devtools/client/memory/actions/view");
+
+function run_test() {
+ run_next_test();
+}
+
+// We test setting an invalid display, which triggers an assertion failure.
+EXPECTED_DTU_ASSERT_FAILURE_COUNT = 1;
+
+add_task(function* () {
+ let front = new StubbedMemoryFront();
+ let heapWorker = new HeapAnalysesClient();
+ yield front.attach();
+ let store = Store();
+ let { getState, dispatch } = store;
+
+ dispatch(changeView(viewState.CENSUS));
+
+ // Test default display with no snapshots
+ equal(getState().censusDisplay.breakdown.by, "coarseType",
+ "default coarseType display selected at start.");
+
+ dispatch(setCensusDisplay(censusDisplays.allocationStack));
+ equal(getState().censusDisplay.breakdown.by, "allocationStack",
+ "display changed with no snapshots");
+
+ // Test invalid displays
+ try {
+ dispatch(setCensusDisplay({}));
+ ok(false, "Throws when passing in an invalid display object");
+ } catch (e) {
+ ok(true, "Throws when passing in an invalid display object");
+ }
+ equal(getState().censusDisplay.breakdown.by, "allocationStack",
+ "current display unchanged when passing invalid display");
+
+ // Test new snapshots
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ yield waitUntilCensusState(store, s => s.census, [censusState.SAVED]);
+ equal(getState().snapshots[0].census.display, censusDisplays.allocationStack,
+ "New snapshots use the current, non-default display");
+});
diff --git a/devtools/client/memory/test/unit/test_action-take-census.js b/devtools/client/memory/test/unit/test_action-take-census.js
new file mode 100644
index 000000000..3e3d30e8e
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_action-take-census.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests the async reducer responding to the action `takeCensus(heapWorker, snapshot)`
+ */
+
+var { snapshotState: states, censusDisplays, censusState, censusState, viewState } = require("devtools/client/memory/constants");
+var actions = require("devtools/client/memory/actions/snapshot");
+var { changeView } = require("devtools/client/memory/actions/view");
+
+
+function run_test() {
+ run_next_test();
+}
+
+// This tests taking a census on a snapshot that is still being read, which
+// triggers an assertion failure.
+EXPECTED_DTU_ASSERT_FAILURE_COUNT = 1;
+
+add_task(function* () {
+ let front = new StubbedMemoryFront();
+ let heapWorker = new HeapAnalysesClient();
+ yield front.attach();
+ let store = Store();
+
+ store.dispatch(changeView(viewState.CENSUS));
+
+ store.dispatch(actions.takeSnapshot(front));
+ yield waitUntilState(store, () => {
+ let snapshots = store.getState().snapshots;
+ return snapshots.length === 1 && snapshots[0].state === states.SAVED;
+ });
+
+ let snapshot = store.getState().snapshots[0];
+ equal(snapshot.census, null, "No census data exists yet on the snapshot.");
+
+ // Test error case of wrong state.
+ store.dispatch(actions.takeCensus(heapWorker, snapshot.id));
+ yield waitUntilState(store, () => store.getState().errors.length === 1);
+
+ dumpn("Found error: " + store.getState().errors[0]);
+ ok(/Assertion failure/.test(store.getState().errors[0]),
+ "Error thrown when taking a census of a snapshot that has not been read.");
+
+ store.dispatch(actions.readSnapshot(heapWorker, snapshot.id));
+ yield waitUntilState(store, () => store.getState().snapshots[0].state === states.READ);
+
+ store.dispatch(actions.takeCensus(heapWorker, snapshot.id));
+ yield waitUntilCensusState(store, s => s.census, [censusState.SAVING]);
+ yield waitUntilCensusState(store, s => s.census, [censusState.SAVED]);
+
+ snapshot = store.getState().snapshots[0];
+ ok(snapshot.census, "Snapshot has census after saved census");
+ ok(snapshot.census.report.children.length, "Census is in tree node form");
+ equal(snapshot.census.display, censusDisplays.coarseType,
+ "Snapshot stored correct display used for the census");
+
+});
diff --git a/devtools/client/memory/test/unit/test_action-take-snapshot-and-census.js b/devtools/client/memory/test/unit/test_action-take-snapshot-and-census.js
new file mode 100644
index 000000000..77c3b8e38
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_action-take-snapshot-and-census.js
@@ -0,0 +1,58 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests the task creator `takeSnapshotAndCensus()` for the whole flow of
+ * taking a snapshot, and its sub-actions.
+ */
+
+let { snapshotState: states, treeMapState } = require("devtools/client/memory/constants");
+let actions = require("devtools/client/memory/actions/snapshot");
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ let front = new StubbedMemoryFront();
+ let heapWorker = new HeapAnalysesClient();
+ yield front.attach();
+ let store = Store();
+
+ let snapshotI = 0;
+ let censusI = 0;
+ let snapshotStates = ["SAVING", "SAVED", "READING", "READ"];
+ let censusStates = ["SAVING", "SAVED"];
+ let expectStates = () => {
+ let snapshot = store.getState().snapshots[0];
+ if (!snapshot) {
+ return;
+ }
+ if (snapshotI < snapshotStates.length) {
+ let isCorrectState = snapshot.state === states[snapshotStates[snapshotI]];
+ if (isCorrectState) {
+ ok(true, `Found expected snapshot state ${snapshotStates[snapshotI]}`);
+ snapshotI++;
+ }
+ }
+ if (snapshot.treeMap && censusI < censusStates.length) {
+ if (snapshot.treeMap.state === treeMapState[censusStates[censusI]]) {
+ ok(true, `Found expected census state ${censusStates[censusI]}`);
+ censusI++;
+ }
+ }
+ };
+
+
+ let unsubscribe = store.subscribe(expectStates);
+ store.dispatch(actions.takeSnapshotAndCensus(front, heapWorker));
+
+ yield waitUntilState(store, () => { return snapshotI === snapshotStates.length &&
+ censusI === censusStates.length; });
+ unsubscribe();
+
+ ok(true, "takeSnapshotAndCensus() produces the correct sequence of states in a snapshot");
+ let snapshot = store.getState().snapshots[0];
+ ok(snapshot.treeMap, "snapshot has tree map census data");
+ ok(snapshot.selected, "snapshot is selected");
+});
diff --git a/devtools/client/memory/test/unit/test_action-take-snapshot.js b/devtools/client/memory/test/unit/test_action-take-snapshot.js
new file mode 100644
index 000000000..c05583b22
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_action-take-snapshot.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests the async reducer responding to the action `takeSnapshot(front)`
+ */
+
+let actions = require("devtools/client/memory/actions/snapshot");
+let { snapshotState: states } = require("devtools/client/memory/constants");
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ let front = new StubbedMemoryFront();
+ yield front.attach();
+ let store = Store();
+
+ let unsubscribe = store.subscribe(checkState);
+
+ let foundPendingState = false;
+ let foundDoneState = false;
+
+ function checkState() {
+ let { snapshots } = store.getState();
+ let lastSnapshot = snapshots[snapshots.length - 1];
+
+ if (lastSnapshot.state === states.SAVING) {
+ foundPendingState = true;
+ ok(foundPendingState, "Got state change for pending heap snapshot request");
+ ok(!lastSnapshot.path, "Snapshot does not yet have a path");
+ ok(!lastSnapshot.census, "Has no census data when loading");
+ }
+ else if (lastSnapshot.state === states.SAVED) {
+ foundDoneState = true;
+ ok(foundDoneState, "Got state change for completed heap snapshot request");
+ ok(foundPendingState, "SAVED state occurs after SAVING state");
+ ok(lastSnapshot.path, "Snapshot fetched with a path");
+ ok(snapshots.every(s => s.selected === (s.id === lastSnapshot.id)),
+ "Only recent snapshot is selected");
+ }
+ }
+
+ for (let i = 0; i < 4; i++) {
+ store.dispatch(actions.takeSnapshot(front));
+ yield waitUntilState(store, () => foundPendingState && foundDoneState);
+
+ // reset state trackers
+ foundDoneState = foundPendingState = false;
+ }
+
+ unsubscribe();
+});
diff --git a/devtools/client/memory/test/unit/test_action-toggle-inverted-and-refresh-01.js b/devtools/client/memory/test/unit/test_action-toggle-inverted-and-refresh-01.js
new file mode 100644
index 000000000..cd015557d
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_action-toggle-inverted-and-refresh-01.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that changing displays with different inverted state properly
+// refreshes the selected census.
+
+const {
+ censusDisplays,
+ snapshotState: states,
+ censusState,
+ viewState
+} = require("devtools/client/memory/constants");
+const {
+ setCensusDisplayAndRefresh
+} = require("devtools/client/memory/actions/census-display");
+const {
+ takeSnapshotAndCensus,
+ selectSnapshotAndRefresh,
+} = require("devtools/client/memory/actions/snapshot");
+const { changeView } = require("devtools/client/memory/actions/view");
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ let front = new StubbedMemoryFront();
+ let heapWorker = new HeapAnalysesClient();
+ yield front.attach();
+ let store = Store();
+ let { getState, dispatch } = store;
+
+ dispatch(changeView(viewState.CENSUS));
+
+ // Select a non-inverted display.
+ dispatch(setCensusDisplayAndRefresh(heapWorker, censusDisplays.allocationStack));
+ equal(getState().censusDisplay.inverted, false, "not inverted by default");
+
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+
+ yield waitUntilCensusState(store, s => s.census, [censusState.SAVED,
+ censusState.SAVED,
+ censusState.SAVED]);
+ ok(true, "saved 3 snapshots and took a census of each of them");
+
+ // Select an inverted display.
+ dispatch(setCensusDisplayAndRefresh(heapWorker, censusDisplays.invertedAllocationStack));
+
+ yield waitUntilCensusState(store, s => s.census, [censusState.SAVED,
+ censusState.SAVED,
+ censusState.SAVING]);
+ ok(true, "toggling inverted should recompute the selected snapshot's census");
+
+ equal(getState().censusDisplay.inverted, true, "now inverted");
+
+ yield waitUntilCensusState(store, s => s.census, [censusState.SAVED,
+ censusState.SAVED,
+ censusState.SAVED]);
+
+ equal(getState().snapshots[0].census.display.inverted, false);
+ equal(getState().snapshots[1].census.display.inverted, false);
+ equal(getState().snapshots[2].census.display.inverted, true);
+
+ dispatch(selectSnapshotAndRefresh(heapWorker, getState().snapshots[1].id));
+ yield waitUntilCensusState(store, s => s.census, [censusState.SAVED,
+ censusState.SAVING,
+ censusState.SAVED]);
+ ok(true, "selecting non-inverted census should trigger a recompute");
+
+ yield waitUntilCensusState(store, s => s.census, [censusState.SAVED,
+ censusState.SAVED,
+ censusState.SAVED]);
+
+ equal(getState().snapshots[0].census.display.inverted, false);
+ equal(getState().snapshots[1].census.display.inverted, true);
+ equal(getState().snapshots[2].census.display.inverted, true);
+
+ heapWorker.destroy();
+ yield front.detach();
+});
diff --git a/devtools/client/memory/test/unit/test_action-toggle-inverted-and-refresh-02.js b/devtools/client/memory/test/unit/test_action-toggle-inverted-and-refresh-02.js
new file mode 100644
index 000000000..f0cdba264
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_action-toggle-inverted-and-refresh-02.js
@@ -0,0 +1,58 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that changing inverted state in the middle of taking a snapshot results
+// in an inverted census.
+
+const { censusDisplays, snapshotState: states, censusState, viewState } = require("devtools/client/memory/constants");
+const { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot");
+const {
+ setCensusDisplay,
+ setCensusDisplayAndRefresh,
+} = require("devtools/client/memory/actions/census-display");
+const { changeView } = require("devtools/client/memory/actions/view");
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ let front = new StubbedMemoryFront();
+ let heapWorker = new HeapAnalysesClient();
+ yield front.attach();
+ let store = Store();
+ let { getState, dispatch } = store;
+
+ dispatch(changeView(viewState.CENSUS));
+
+ dispatch(setCensusDisplay(censusDisplays.allocationStack));
+ equal(getState().censusDisplay.inverted, false,
+ "Should not have an inverted census display");
+
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ yield waitUntilSnapshotState(store, [states.SAVING]);
+
+ dispatch(setCensusDisplayAndRefresh(heapWorker, censusDisplays.invertedAllocationStack));
+
+ yield waitUntilCensusState(store, s => s.census, [censusState.SAVED]);
+
+ ok(getState().censusDisplay.inverted,
+ "should want inverted trees");
+ ok(getState().snapshots[0].census.display.inverted,
+ "snapshot-we-were-in-the-middle-of-saving's census should be inverted");
+
+ dispatch(setCensusDisplayAndRefresh(heapWorker, censusDisplays.allocationStack));
+ yield waitUntilCensusState(store, s => s.census, [censusState.SAVING]);
+ ok(true, "toggling inverted retriggers census");
+ ok(!getState().censusDisplay.inverted, "no longer inverted");
+
+ dispatch(setCensusDisplayAndRefresh(heapWorker, censusDisplays.invertedAllocationStack));
+ yield waitUntilCensusState(store, s => s.census, [censusState.SAVED]);
+ ok(getState().censusDisplay.inverted, "inverted again");
+ ok(getState().snapshots[0].census.display.inverted,
+ "census-we-were-in-the-middle-of-recomputing should be inverted again");
+
+ heapWorker.destroy();
+ yield front.detach();
+});
diff --git a/devtools/client/memory/test/unit/test_action-toggle-inverted.js b/devtools/client/memory/test/unit/test_action-toggle-inverted.js
new file mode 100644
index 000000000..665e5d822
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_action-toggle-inverted.js
@@ -0,0 +1,28 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test toggling the top level inversion state of the tree.
+
+const { censusDisplays } = require("devtools/client/memory/constants");
+const { setCensusDisplay } = require("devtools/client/memory/actions/census-display");
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ let store = Store();
+ const { getState, dispatch } = store;
+
+ dispatch(setCensusDisplay(censusDisplays.allocationStack));
+ equal(getState().censusDisplay.inverted, false,
+ "not inverted initially");
+
+ dispatch(setCensusDisplay(censusDisplays.invertedAllocationStack));
+ equal(getState().censusDisplay.inverted, true, "now inverted after toggling");
+
+ dispatch(setCensusDisplay(censusDisplays.allocationStack));
+ equal(getState().censusDisplay.inverted, false,
+ "not inverted again after toggling again");
+});
diff --git a/devtools/client/memory/test/unit/test_action-toggle-recording-allocations.js b/devtools/client/memory/test/unit/test_action-toggle-recording-allocations.js
new file mode 100644
index 000000000..ebbc17173
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_action-toggle-recording-allocations.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Test toggling the recording of allocation stacks.
+ */
+
+let { toggleRecordingAllocationStacks } = require("devtools/client/memory/actions/allocations");
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ let front = new StubbedMemoryFront();
+ yield front.attach();
+ let store = Store();
+ const { getState, dispatch } = store;
+
+ equal(getState().allocations.recording, false, "not recording by default");
+ equal(getState().allocations.togglingInProgress, false,
+ "not in the process of toggling by default");
+
+ dispatch(toggleRecordingAllocationStacks(front));
+ yield waitUntilState(store, () => getState().allocations.togglingInProgress);
+ ok(true, "`togglingInProgress` set to true when toggling on");
+ yield waitUntilState(store, () => !getState().allocations.togglingInProgress);
+
+ equal(getState().allocations.recording, true, "now we are recording");
+ ok(front.recordingAllocations, "front is recording too");
+
+ dispatch(toggleRecordingAllocationStacks(front));
+ yield waitUntilState(store, () => getState().allocations.togglingInProgress);
+ ok(true, "`togglingInProgress` set to true when toggling off");
+ yield waitUntilState(store, () => !getState().allocations.togglingInProgress);
+
+ equal(getState().allocations.recording, false, "now we are not recording");
+ ok(front.recordingAllocations, "front is not recording anymore");
+
+ yield front.detach();
+});
diff --git a/devtools/client/memory/test/unit/test_action_diffing_01.js b/devtools/client/memory/test/unit/test_action_diffing_01.js
new file mode 100644
index 000000000..24893581e
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_action_diffing_01.js
@@ -0,0 +1,29 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test toggling of diffing.
+
+const { toggleDiffing } = require("devtools/client/memory/actions/diffing");
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ let front = new StubbedMemoryFront();
+ let heapWorker = new HeapAnalysesClient();
+ yield front.attach();
+ let store = Store();
+ const { getState, dispatch } = store;
+
+ equal(getState().diffing, null, "not diffing by default");
+
+ dispatch(toggleDiffing());
+ ok(getState().diffing, "now diffing after toggling");
+
+ dispatch(toggleDiffing());
+ equal(getState().diffing, null, "not diffing again after toggling again");
+
+ heapWorker.destroy();
+ yield front.detach();
+});
diff --git a/devtools/client/memory/test/unit/test_action_diffing_02.js b/devtools/client/memory/test/unit/test_action_diffing_02.js
new file mode 100644
index 000000000..fc68cee65
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_action_diffing_02.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that toggling diffing unselects all snapshots.
+
+const { snapshotState, censusState, viewState } = require("devtools/client/memory/constants");
+const { toggleDiffing } = require("devtools/client/memory/actions/diffing");
+const { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot");
+const { changeView } = require("devtools/client/memory/actions/view");
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ let front = new StubbedMemoryFront();
+ let heapWorker = new HeapAnalysesClient();
+ yield front.attach();
+ let store = Store();
+ const { getState, dispatch } = store;
+
+ dispatch(changeView(viewState.CENSUS));
+
+ equal(getState().diffing, null, "not diffing by default");
+
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ yield waitUntilCensusState(store, s => s.census, [censusState.SAVED,
+ censusState.SAVED,
+ censusState.SAVED]);
+
+ ok(getState().snapshots.some(s => s.selected),
+ "One of the new snapshots is selected");
+
+ dispatch(toggleDiffing());
+ ok(getState().diffing, "now diffing after toggling");
+
+ for (let s of getState().snapshots) {
+ ok(!s.selected,
+ "No snapshot should be selected after entering diffing mode");
+ }
+
+ heapWorker.destroy();
+ yield front.detach();
+});
diff --git a/devtools/client/memory/test/unit/test_action_diffing_03.js b/devtools/client/memory/test/unit/test_action_diffing_03.js
new file mode 100644
index 000000000..3dd901724
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_action_diffing_03.js
@@ -0,0 +1,104 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test selecting snapshots for diffing.
+
+const { diffingState, snapshotState, viewState } = require("devtools/client/memory/constants");
+const {
+ toggleDiffing,
+ selectSnapshotForDiffing
+} = require("devtools/client/memory/actions/diffing");
+const { takeSnapshot } = require("devtools/client/memory/actions/snapshot");
+const { changeView } = require("devtools/client/memory/actions/view");
+
+function run_test() {
+ run_next_test();
+}
+
+// We test that you (1) cannot select a snapshot that is not in a diffable
+// state, and (2) cannot select more than 2 snapshots for diffing. Both attempts
+// trigger assertion failures.
+EXPECTED_DTU_ASSERT_FAILURE_COUNT = 2;
+
+add_task(function* () {
+ let front = new StubbedMemoryFront();
+ let heapWorker = new HeapAnalysesClient();
+ yield front.attach();
+ let store = Store();
+ const { getState, dispatch } = store;
+
+ dispatch(changeView(viewState.CENSUS));
+ equal(getState().diffing, null, "not diffing by default");
+
+ dispatch(takeSnapshot(front, heapWorker));
+ dispatch(takeSnapshot(front, heapWorker));
+ dispatch(takeSnapshot(front, heapWorker));
+
+ yield waitUntilSnapshotState(store, [snapshotState.SAVED,
+ snapshotState.SAVED,
+ snapshotState.SAVED]);
+ dispatch(takeSnapshot(front));
+
+ // Start diffing.
+ dispatch(toggleDiffing());
+ ok(getState().diffing, "now diffing after toggling");
+ equal(getState().diffing.firstSnapshotId, null,
+ "no first snapshot selected");
+ equal(getState().diffing.secondSnapshotId, null,
+ "no second snapshot selected");
+ equal(getState().diffing.state, diffingState.SELECTING,
+ "should be in diffing state SELECTING");
+
+ // Can't select a snapshot that is not in a diffable state.
+ equal(getState().snapshots[3].state, snapshotState.SAVING,
+ "the last snapshot is still in the process of being saved");
+ dumpn("Expecting exception:");
+ let threw = false;
+ try {
+ dispatch(selectSnapshotForDiffing(getState().snapshots[3]));
+ } catch (error) {
+ threw = true;
+ }
+ ok(threw, "Should not be able to select snapshots that aren't ready for diffing");
+
+ // Select first snapshot for diffing.
+ dispatch(selectSnapshotForDiffing(getState().snapshots[0]));
+ ok(getState().diffing, "now diffing after toggling");
+ equal(getState().diffing.firstSnapshotId, getState().snapshots[0].id,
+ "first snapshot selected");
+ equal(getState().diffing.secondSnapshotId, null,
+ "no second snapshot selected");
+ equal(getState().diffing.state, diffingState.SELECTING,
+ "should still be in diffing state SELECTING");
+
+ // Can't diff first snapshot with itself; this is a noop.
+ dispatch(selectSnapshotForDiffing(getState().snapshots[0]));
+ ok(getState().diffing, "still diffing");
+ equal(getState().diffing.firstSnapshotId, getState().snapshots[0].id,
+ "first snapshot still selected");
+ equal(getState().diffing.secondSnapshotId, null,
+ "still no second snapshot selected");
+ equal(getState().diffing.state, diffingState.SELECTING,
+ "should still be in diffing state SELECTING");
+
+ // Select second snapshot for diffing.
+ dispatch(selectSnapshotForDiffing(getState().snapshots[1]));
+ ok(getState().diffing, "still diffing");
+ equal(getState().diffing.firstSnapshotId, getState().snapshots[0].id,
+ "first snapshot still selected");
+ equal(getState().diffing.secondSnapshotId, getState().snapshots[1].id,
+ "second snapshot selected");
+
+ // Can't select more than two snapshots for diffing.
+ dumpn("Expecting exception:");
+ threw = false;
+ try {
+ dispatch(selectSnapshotForDiffing(getState().snapshots[2]));
+ } catch (error) {
+ threw = true;
+ }
+ ok(threw, "Can't select more than two snapshots for diffing");
+
+ heapWorker.destroy();
+ yield front.detach();
+});
diff --git a/devtools/client/memory/test/unit/test_action_diffing_04.js b/devtools/client/memory/test/unit/test_action_diffing_04.js
new file mode 100644
index 000000000..2c3cd098b
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_action_diffing_04.js
@@ -0,0 +1,78 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that we compute census diffs.
+
+const {
+ diffingState,
+ snapshotState,
+ viewState
+} = require("devtools/client/memory/constants");
+const {
+ toggleDiffing,
+ selectSnapshotForDiffingAndRefresh
+} = require("devtools/client/memory/actions/diffing");
+const {
+ takeSnapshot,
+ readSnapshot
+} = require("devtools/client/memory/actions/snapshot");
+const { changeView } = require("devtools/client/memory/actions/view");
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ let front = new StubbedMemoryFront();
+ let heapWorker = new HeapAnalysesClient();
+ yield front.attach();
+ let store = Store();
+ const { getState, dispatch } = store;
+ dispatch(changeView(viewState.CENSUS));
+
+ equal(getState().diffing, null, "not diffing by default");
+
+ const s1 = yield dispatch(takeSnapshot(front, heapWorker));
+ const s2 = yield dispatch(takeSnapshot(front, heapWorker));
+ const s3 = yield dispatch(takeSnapshot(front, heapWorker));
+ dispatch(readSnapshot(heapWorker, s1));
+ dispatch(readSnapshot(heapWorker, s2));
+ dispatch(readSnapshot(heapWorker, s3));
+ yield waitUntilSnapshotState(store, [snapshotState.READ,
+ snapshotState.READ,
+ snapshotState.READ]);
+
+ dispatch(toggleDiffing());
+ dispatch(selectSnapshotForDiffingAndRefresh(heapWorker,
+ getState().snapshots[0]));
+ dispatch(selectSnapshotForDiffingAndRefresh(heapWorker,
+ getState().snapshots[1]));
+
+ ok(getState().diffing, "We should be diffing.");
+ equal(getState().diffing.firstSnapshotId, getState().snapshots[0].id,
+ "First snapshot selected.");
+ equal(getState().diffing.secondSnapshotId, getState().snapshots[1].id,
+ "Second snapshot selected.");
+
+ yield waitUntilState(store,
+ state =>
+ state.diffing.state === diffingState.TAKING_DIFF);
+ ok(true,
+ "Selecting two snapshots for diffing should trigger computing a diff.");
+
+ yield waitUntilState(store,
+ state => state.diffing.state === diffingState.TOOK_DIFF);
+ ok(true, "And then the diff should complete.");
+ ok(getState().diffing.census, "And we should have a census.");
+ ok(getState().diffing.census.report, "And that census should have a report.");
+ equal(getState().diffing.census.display, getState().censusDisplay,
+ "And that census should have the correct display");
+ equal(getState().diffing.census.filter, getState().filter,
+ "And that census should have the correct filter");
+ equal(getState().diffing.census.display.inverted,
+ getState().censusDisplay.inverted,
+ "And that census should have the correct inversion");
+
+ heapWorker.destroy();
+ yield front.detach();
+});
diff --git a/devtools/client/memory/test/unit/test_action_diffing_05.js b/devtools/client/memory/test/unit/test_action_diffing_05.js
new file mode 100644
index 000000000..e4a1491ac
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_action_diffing_05.js
@@ -0,0 +1,112 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that we recompute census diffs at the appropriate times.
+
+const {
+ diffingState,
+ snapshotState,
+ censusDisplays,
+ viewState,
+} = require("devtools/client/memory/constants");
+const {
+ setCensusDisplayAndRefresh,
+} = require("devtools/client/memory/actions/census-display");
+const {
+ toggleDiffing,
+ selectSnapshotForDiffingAndRefresh,
+} = require("devtools/client/memory/actions/diffing");
+const {
+ setFilterStringAndRefresh,
+} = require("devtools/client/memory/actions/filter");
+const {
+ takeSnapshot,
+ readSnapshot,
+} = require("devtools/client/memory/actions/snapshot");
+const { changeView } = require("devtools/client/memory/actions/view");
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ let front = new StubbedMemoryFront();
+ let heapWorker = new HeapAnalysesClient();
+ yield front.attach();
+ let store = Store();
+ const { getState, dispatch } = store;
+ dispatch(changeView(viewState.CENSUS));
+
+ yield dispatch(setCensusDisplayAndRefresh(heapWorker,
+ censusDisplays.allocationStack));
+ equal(getState().censusDisplay.inverted, false,
+ "not inverted at start");
+
+ equal(getState().diffing, null, "not diffing by default");
+
+ const s1 = yield dispatch(takeSnapshot(front, heapWorker));
+ const s2 = yield dispatch(takeSnapshot(front, heapWorker));
+ const s3 = yield dispatch(takeSnapshot(front, heapWorker));
+ dispatch(readSnapshot(heapWorker, s1));
+ dispatch(readSnapshot(heapWorker, s2));
+ dispatch(readSnapshot(heapWorker, s3));
+ yield waitUntilSnapshotState(store, [snapshotState.READ,
+ snapshotState.READ,
+ snapshotState.READ]);
+
+ yield dispatch(toggleDiffing());
+ dispatch(selectSnapshotForDiffingAndRefresh(heapWorker,
+ getState().snapshots[0]));
+ dispatch(selectSnapshotForDiffingAndRefresh(heapWorker,
+ getState().snapshots[1]));
+ yield waitUntilState(store,
+ state => state.diffing.state === diffingState.TOOK_DIFF);
+
+ const shouldTriggerRecompute = [
+ {
+ name: "toggling inversion",
+ func: () => dispatch(setCensusDisplayAndRefresh(
+ heapWorker,
+ censusDisplays.invertedAllocationStack))
+ },
+ {
+ name: "filtering",
+ func: () => dispatch(setFilterStringAndRefresh("scr", heapWorker))
+ },
+ {
+ name: "changing displays",
+ func: () =>
+ dispatch(setCensusDisplayAndRefresh(heapWorker,
+ censusDisplays.coarseType))
+ }
+ ];
+
+ for (let { name, func } of shouldTriggerRecompute) {
+ dumpn(`Testing that "${name}" triggers a diff recompute`);
+ func();
+
+ yield waitUntilState(store,
+ state =>
+ state.diffing.state === diffingState.TAKING_DIFF);
+ ok(true, "triggered diff recompute.");
+
+ yield waitUntilState(store,
+ state =>
+ state.diffing.state === diffingState.TOOK_DIFF);
+ ok(true, "And then the diff should complete.");
+ ok(getState().diffing.census, "And we should have a census.");
+ ok(getState().diffing.census.report,
+ "And that census should have a report.");
+ equal(getState().diffing.census.display,
+ getState().censusDisplay,
+ "And that census should have the correct display");
+ equal(getState().diffing.census.filter, getState().filter,
+ "And that census should have the correct filter");
+ equal(getState().diffing.census.display.inverted,
+ getState().censusDisplay.inverted,
+ "And that census should have the correct inversion");
+ }
+
+ heapWorker.destroy();
+ yield front.detach();
+});
diff --git a/devtools/client/memory/test/unit/test_dominator_trees_01.js b/devtools/client/memory/test/unit/test_dominator_trees_01.js
new file mode 100644
index 000000000..5fcf441d4
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_dominator_trees_01.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that we can compute and fetch the dominator tree for a snapshot.
+
+let {
+ snapshotState: states,
+ dominatorTreeState,
+ treeMapState,
+} = require("devtools/client/memory/constants");
+let {
+ takeSnapshotAndCensus,
+ computeAndFetchDominatorTree,
+} = require("devtools/client/memory/actions/snapshot");
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ let front = new StubbedMemoryFront();
+ let heapWorker = new HeapAnalysesClient();
+ yield front.attach();
+ let store = Store();
+ let { getState, dispatch } = store;
+
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ yield waitUntilCensusState(store, s => s.treeMap, [treeMapState.SAVED]);
+ ok(!getState().snapshots[0].dominatorTree,
+ "There shouldn't be a dominator tree model yet since it is not computed " +
+ "until we switch to the dominators view.");
+
+ // Change to the dominator tree view.
+ dispatch(computeAndFetchDominatorTree(heapWorker, getState().snapshots[0].id));
+ ok(getState().snapshots[0].dominatorTree,
+ "Should now have a dominator tree model for the selected snapshot");
+
+ // Wait for the dominator tree to start being computed.
+ yield waitUntilState(store, state =>
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.COMPUTING);
+ ok(true, "The dominator tree started computing");
+ ok(!getState().snapshots[0].dominatorTree.root,
+ "When the dominator tree is computing, we should not have its root");
+
+ // Wait for the dominator tree to finish computing and start being fetched.
+ yield waitUntilState(store, state =>
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.FETCHING);
+ ok(true, "The dominator tree started fetching");
+ ok(!getState().snapshots[0].dominatorTree.root,
+ "When the dominator tree is fetching, we should not have its root");
+
+ // Wait for the dominator tree to finish being fetched.
+ yield waitUntilState(store, state =>
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED);
+ ok(true, "The dominator tree was fetched");
+ ok(getState().snapshots[0].dominatorTree.root,
+ "When the dominator tree is loaded, we should have its root");
+
+ heapWorker.destroy();
+ yield front.detach();
+});
diff --git a/devtools/client/memory/test/unit/test_dominator_trees_02.js b/devtools/client/memory/test/unit/test_dominator_trees_02.js
new file mode 100644
index 000000000..b35961004
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_dominator_trees_02.js
@@ -0,0 +1,64 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that selecting the dominator tree view automatically kicks off fetching
+// and computing dominator trees.
+
+const {
+ snapshotState: states,
+ dominatorTreeState,
+ viewState,
+ treeMapState,
+} = require("devtools/client/memory/constants");
+const {
+ takeSnapshotAndCensus,
+} = require("devtools/client/memory/actions/snapshot");
+const {
+ changeViewAndRefresh
+} = require("devtools/client/memory/actions/view");
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ let front = new StubbedMemoryFront();
+ let heapWorker = new HeapAnalysesClient();
+ yield front.attach();
+ let store = Store();
+ let { getState, dispatch } = store;
+
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ yield waitUntilCensusState(store, s => s.treeMap, [treeMapState.SAVED]);
+ ok(!getState().snapshots[0].dominatorTree,
+ "There shouldn't be a dominator tree model yet since it is not computed " +
+ "until we switch to the dominators view.");
+
+ dispatch(changeViewAndRefresh(viewState.DOMINATOR_TREE, heapWorker));
+ ok(getState().snapshots[0].dominatorTree,
+ "Should now have a dominator tree model for the selected snapshot");
+
+ // Wait for the dominator tree to start being computed.
+ yield waitUntilState(store, state =>
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.COMPUTING);
+ ok(true, "The dominator tree started computing");
+ ok(!getState().snapshots[0].dominatorTree.root,
+ "When the dominator tree is computing, we should not have its root");
+
+ // Wait for the dominator tree to finish computing and start being fetched.
+ yield waitUntilState(store, state =>
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.FETCHING);
+ ok(true, "The dominator tree started fetching");
+ ok(!getState().snapshots[0].dominatorTree.root,
+ "When the dominator tree is fetching, we should not have its root");
+
+ // Wait for the dominator tree to finish being fetched.
+ yield waitUntilState(store, state =>
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED);
+ ok(true, "The dominator tree was fetched");
+ ok(getState().snapshots[0].dominatorTree.root,
+ "When the dominator tree is loaded, we should have its root");
+
+ heapWorker.destroy();
+ yield front.detach();
+});
diff --git a/devtools/client/memory/test/unit/test_dominator_trees_03.js b/devtools/client/memory/test/unit/test_dominator_trees_03.js
new file mode 100644
index 000000000..09e6477f4
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_dominator_trees_03.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that selecting the dominator tree view and then taking a snapshot
+// properly kicks off fetching and computing dominator trees.
+
+const {
+ snapshotState: states,
+ dominatorTreeState,
+ viewState,
+} = require("devtools/client/memory/constants");
+const {
+ takeSnapshotAndCensus,
+} = require("devtools/client/memory/actions/snapshot");
+const {
+ changeView
+} = require("devtools/client/memory/actions/view");
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ let front = new StubbedMemoryFront();
+ let heapWorker = new HeapAnalysesClient();
+ yield front.attach();
+ let store = Store();
+ let { getState, dispatch } = store;
+
+ dispatch(changeView(viewState.DOMINATOR_TREE));
+ equal(getState().view.state, viewState.DOMINATOR_TREE,
+ "We should now be in the DOMINATOR_TREE view");
+
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+
+ // Wait for the dominator tree to start being computed.
+ yield waitUntilState(store, state =>
+ state.snapshots[0] && state.snapshots[0].dominatorTree);
+ equal(getState().snapshots[0].dominatorTree.state,
+ dominatorTreeState.COMPUTING,
+ "The dominator tree started computing");
+ ok(!getState().snapshots[0].dominatorTree.root,
+ "When the dominator tree is computing, we should not have its root");
+
+ // Wait for the dominator tree to finish computing and start being fetched.
+ yield waitUntilState(store, state =>
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.FETCHING);
+ ok(true, "The dominator tree started fetching");
+ ok(!getState().snapshots[0].dominatorTree.root,
+ "When the dominator tree is fetching, we should not have its root");
+
+ // Wait for the dominator tree to finish being fetched.
+ yield waitUntilState(store, state =>
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED);
+ ok(true, "The dominator tree was fetched");
+ ok(getState().snapshots[0].dominatorTree.root,
+ "When the dominator tree is loaded, we should have its root");
+
+ heapWorker.destroy();
+ yield front.detach();
+});
diff --git a/devtools/client/memory/test/unit/test_dominator_trees_04.js b/devtools/client/memory/test/unit/test_dominator_trees_04.js
new file mode 100644
index 000000000..ba375c354
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_dominator_trees_04.js
@@ -0,0 +1,69 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that selecting the dominator tree view while in the middle of taking a
+// snapshot properly kicks off fetching and computing dominator trees.
+
+const {
+ snapshotState: states,
+ dominatorTreeState,
+ viewState,
+} = require("devtools/client/memory/constants");
+const {
+ takeSnapshotAndCensus,
+} = require("devtools/client/memory/actions/snapshot");
+const {
+ changeView
+} = require("devtools/client/memory/actions/view");
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ let front = new StubbedMemoryFront();
+ let heapWorker = new HeapAnalysesClient();
+ yield front.attach();
+
+ for (let intermediateSnapshotState of [states.SAVING,
+ states.READING,
+ states.READ]) {
+ dumpn(`Testing switching to the DOMINATOR_TREE view in the middle of the ${intermediateSnapshotState} snapshot state`);
+
+ let store = Store();
+ let { getState, dispatch } = store;
+
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ yield waitUntilSnapshotState(store, [intermediateSnapshotState]);
+
+ dispatch(changeView(viewState.DOMINATOR_TREE));
+ equal(getState().view.state, viewState.DOMINATOR_TREE,
+ "We should now be in the DOMINATOR_TREE view");
+
+ // Wait for the dominator tree to start being computed.
+ yield waitUntilState(store, state =>
+ state.snapshots[0] && state.snapshots[0].dominatorTree);
+ equal(getState().snapshots[0].dominatorTree.state,
+ dominatorTreeState.COMPUTING,
+ "The dominator tree started computing");
+ ok(!getState().snapshots[0].dominatorTree.root,
+ "When the dominator tree is computing, we should not have its root");
+
+ // Wait for the dominator tree to finish computing and start being fetched.
+ yield waitUntilState(store, state =>
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.FETCHING);
+ ok(true, "The dominator tree started fetching");
+ ok(!getState().snapshots[0].dominatorTree.root,
+ "When the dominator tree is fetching, we should not have its root");
+
+ // Wait for the dominator tree to finish being fetched.
+ yield waitUntilState(store, state =>
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED);
+ ok(true, "The dominator tree was fetched");
+ ok(getState().snapshots[0].dominatorTree.root,
+ "When the dominator tree is loaded, we should have its root");
+ }
+
+ heapWorker.destroy();
+ yield front.detach();
+});
diff --git a/devtools/client/memory/test/unit/test_dominator_trees_05.js b/devtools/client/memory/test/unit/test_dominator_trees_05.js
new file mode 100644
index 000000000..42ea13e18
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_dominator_trees_05.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that changing the currently selected snapshot to a snapshot that does
+// not have a dominator tree will automatically compute and fetch one for it.
+
+let {
+ snapshotState: states,
+ dominatorTreeState,
+ viewState,
+ treeMapState,
+} = require("devtools/client/memory/constants");
+let {
+ takeSnapshotAndCensus,
+ selectSnapshotAndRefresh,
+} = require("devtools/client/memory/actions/snapshot");
+
+let { changeView } = require("devtools/client/memory/actions/view");
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ let front = new StubbedMemoryFront();
+ let heapWorker = new HeapAnalysesClient();
+ yield front.attach();
+ let store = Store();
+ let { getState, dispatch } = store;
+
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ yield waitUntilCensusState(store, s => s.treeMap, [treeMapState.SAVED,
+ treeMapState.SAVED]);
+
+ ok(getState().snapshots[1].selected, "The second snapshot is selected");
+
+ // Change to the dominator tree view.
+ dispatch(changeView(viewState.DOMINATOR_TREE));
+
+ // Wait for the dominator tree to finish being fetched.
+ yield waitUntilState(store, state =>
+ state.snapshots[1].dominatorTree &&
+ state.snapshots[1].dominatorTree.state === dominatorTreeState.LOADED);
+ ok(true, "The second snapshot's dominator tree was fetched");
+
+ // Select the first snapshot.
+ dispatch(selectSnapshotAndRefresh(heapWorker, getState().snapshots[0].id));
+
+ // And now the first snapshot should have its dominator tree fetched and
+ // computed because of the new selection.
+ yield waitUntilState(store, state =>
+ state.snapshots[0].dominatorTree &&
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED);
+ ok(true, "The first snapshot's dominator tree was fetched");
+
+ heapWorker.destroy();
+ yield front.detach();
+});
diff --git a/devtools/client/memory/test/unit/test_dominator_trees_06.js b/devtools/client/memory/test/unit/test_dominator_trees_06.js
new file mode 100644
index 000000000..78f869102
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_dominator_trees_06.js
@@ -0,0 +1,127 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that we can incrementally fetch a subtree of a dominator tree.
+
+const {
+ snapshotState: states,
+ dominatorTreeState,
+ viewState,
+} = require("devtools/client/memory/constants");
+const {
+ takeSnapshotAndCensus,
+ selectSnapshotAndRefresh,
+ fetchImmediatelyDominated,
+} = require("devtools/client/memory/actions/snapshot");
+const DominatorTreeLazyChildren
+ = require("devtools/client/memory/dominator-tree-lazy-children");
+
+const { changeView } = require("devtools/client/memory/actions/view");
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ let front = new StubbedMemoryFront();
+ let heapWorker = new HeapAnalysesClient();
+ yield front.attach();
+ let store = Store();
+ let { getState, dispatch } = store;
+
+ dispatch(changeView(viewState.DOMINATOR_TREE));
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+
+ // Wait for the dominator tree to finish being fetched.
+ yield waitUntilState(store, state =>
+ state.snapshots[0] &&
+ state.snapshots[0].dominatorTree &&
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED);
+ ok(getState().snapshots[0].dominatorTree.root,
+ "The dominator tree was fetched");
+
+ // Find a node that has children, but none of them are loaded.
+
+ function findNode(node) {
+ if (node.moreChildrenAvailable && !node.children) {
+ return node;
+ }
+
+ if (node.children) {
+ for (let child of node.children) {
+ const found = findNode(child);
+ if (found) {
+ return found;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ const oldRoot = getState().snapshots[0].dominatorTree.root;
+ const oldNode = findNode(oldRoot);
+ ok(oldNode,
+ "Should have found a node with children that are not loaded since we " +
+ "only send partial dominator trees across initially and load the rest " +
+ "on demand");
+ ok(oldNode !== oldRoot, "But the node should not be the root");
+
+ const lazyChildren = new DominatorTreeLazyChildren(oldNode.nodeId, 0);
+ dispatch(fetchImmediatelyDominated(heapWorker, getState().snapshots[0].id, lazyChildren));
+
+ equal(getState().snapshots[0].dominatorTree.state,
+ dominatorTreeState.INCREMENTAL_FETCHING,
+ "Fetching immediately dominated children should put us in the " +
+ "INCREMENTAL_FETCHING state");
+
+ yield waitUntilState(store, state =>
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED);
+ ok(true,
+ "The dominator tree should go back to LOADED after the incremental " +
+ "fetching is done.");
+
+ const newRoot = getState().snapshots[0].dominatorTree.root;
+ ok(oldRoot !== newRoot,
+ "When we insert new nodes, we get a new tree");
+ equal(oldRoot.children.length, newRoot.children.length,
+ "The new tree's root should have the same number of children as the " +
+ "old root's");
+
+ let differentChildrenCount = 0;
+ for (let i = 0; i < oldRoot.children.length; i++) {
+ if (oldRoot.children[i] !== newRoot.children[i]) {
+ differentChildrenCount++;
+ }
+ }
+ equal(differentChildrenCount, 1,
+ "All subtrees except the subtree we inserted incrementally fetched " +
+ "children into should be the same because we use persistent updates");
+
+ // Find the new node which has the children inserted.
+
+ function findNewNode(node) {
+ if (node.nodeId === oldNode.nodeId) {
+ return node;
+ }
+
+ if (node.children) {
+ for (let child of node.children) {
+ const found = findNewNode(child);
+ if (found) {
+ return found;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ const newNode = findNewNode(newRoot);
+ ok(newNode, "Should find the node in the new tree again");
+ ok(newNode !== oldNode, "We did not mutate the old node in place, instead created a new node");
+ ok(newNode.children, "And the new node should have the children attached");
+
+ heapWorker.destroy();
+ yield front.detach();
+});
diff --git a/devtools/client/memory/test/unit/test_dominator_trees_07.js b/devtools/client/memory/test/unit/test_dominator_trees_07.js
new file mode 100644
index 000000000..9121fc878
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_dominator_trees_07.js
@@ -0,0 +1,146 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that we can incrementally fetch two subtrees in the same dominator tree
+// concurrently. This exercises the activeFetchRequestCount machinery.
+
+const {
+ snapshotState: states,
+ dominatorTreeState,
+ viewState,
+} = require("devtools/client/memory/constants");
+const {
+ takeSnapshotAndCensus,
+ selectSnapshotAndRefresh,
+ fetchImmediatelyDominated,
+} = require("devtools/client/memory/actions/snapshot");
+const DominatorTreeLazyChildren
+ = require("devtools/client/memory/dominator-tree-lazy-children");
+
+const { changeView } = require("devtools/client/memory/actions/view");
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ let front = new StubbedMemoryFront();
+ let heapWorker = new HeapAnalysesClient();
+ yield front.attach();
+ let store = Store();
+ let { getState, dispatch } = store;
+
+ dispatch(changeView(viewState.DOMINATOR_TREE));
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+
+ // Wait for the dominator tree to finish being fetched.
+ yield waitUntilState(store, state =>
+ state.snapshots[0] &&
+ state.snapshots[0].dominatorTree &&
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED);
+ ok(getState().snapshots[0].dominatorTree.root,
+ "The dominator tree was fetched");
+
+ // Find a node that has more children.
+
+ function findNode(node) {
+ if (node.moreChildrenAvailable && !node.children) {
+ return node;
+ }
+
+ if (node.children) {
+ for (let child of node.children) {
+ const found = findNode(child);
+ if (found) {
+ return found;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ const oldRoot = getState().snapshots[0].dominatorTree.root;
+ const oldNode = findNode(oldRoot);
+ ok(oldNode, "Should have found a node with more children.");
+
+ // Find another node that has more children.
+ function findNodeRev(node) {
+ if (node.moreChildrenAvailable && !node.children) {
+ return node;
+ }
+
+ if (node.children) {
+ for (let child of node.children.slice().reverse()) {
+ const found = findNodeRev(child);
+ if (found) {
+ return found;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ const oldNode2 = findNodeRev(oldRoot);
+ ok(oldNode2, "Should have found another node with more children.");
+ ok(oldNode !== oldNode2,
+ "The second node should not be the same as the first one");
+
+ // Fetch both subtrees concurrently.
+ dispatch(fetchImmediatelyDominated(heapWorker, getState().snapshots[0].id,
+ new DominatorTreeLazyChildren(oldNode.nodeId, 0)));
+ dispatch(fetchImmediatelyDominated(heapWorker, getState().snapshots[0].id,
+ new DominatorTreeLazyChildren(oldNode2.nodeId, 0)));
+
+ equal(getState().snapshots[0].dominatorTree.state,
+ dominatorTreeState.INCREMENTAL_FETCHING,
+ "Fetching immediately dominated children should put us in the " +
+ "INCREMENTAL_FETCHING state");
+
+ yield waitUntilState(store, state =>
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED);
+ ok(true,
+ "The dominator tree should go back to LOADED after the incremental " +
+ "fetching is done.");
+
+ const newRoot = getState().snapshots[0].dominatorTree.root;
+ ok(oldRoot !== newRoot,
+ "When we insert new nodes, we get a new tree");
+
+ // Find the new node which has the children inserted.
+
+ function findNodeWithId(id, node) {
+ if (node.nodeId === id) {
+ return node;
+ }
+
+ if (node.children) {
+ for (let child of node.children) {
+ const found = findNodeWithId(id, child);
+ if (found) {
+ return found;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ const newNode = findNodeWithId(oldNode.nodeId, newRoot);
+ ok(newNode, "Should find the node in the new tree again");
+ ok(newNode !== oldNode,
+ "We did not mutate the old node in place, instead created a new node");
+ ok(newNode.children.length,
+ "And the new node should have the new children attached");
+
+ const newNode2 = findNodeWithId(oldNode2.nodeId, newRoot);
+ ok(newNode2, "Should find the second node in the new tree again");
+ ok(newNode2 !== oldNode2,
+ "We did not mutate the second old node in place, instead created a new node");
+ ok(newNode2.children,
+ "And the new node should have the new children attached");
+
+ heapWorker.destroy();
+ yield front.detach();
+});
diff --git a/devtools/client/memory/test/unit/test_dominator_trees_08.js b/devtools/client/memory/test/unit/test_dominator_trees_08.js
new file mode 100644
index 000000000..27f35180c
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_dominator_trees_08.js
@@ -0,0 +1,81 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that we can change the display with which we describe a dominator tree
+// and that the dominator tree is re-fetched.
+
+const {
+ snapshotState: states,
+ dominatorTreeState,
+ viewState,
+ labelDisplays,
+ treeMapState
+} = require("devtools/client/memory/constants");
+const {
+ setLabelDisplayAndRefresh
+} = require("devtools/client/memory/actions/label-display");
+const {
+ changeView,
+} = require("devtools/client/memory/actions/view");
+const {
+ takeSnapshotAndCensus,
+ computeAndFetchDominatorTree,
+} = require("devtools/client/memory/actions/snapshot");
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ let front = new StubbedMemoryFront();
+ let heapWorker = new HeapAnalysesClient();
+ yield front.attach();
+ let store = Store();
+ let { getState, dispatch } = store;
+
+ dispatch(changeView(viewState.DOMINATOR_TREE));
+
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ yield waitUntilCensusState(store, s => s.treeMap, [treeMapState.SAVED]);
+ ok(!getState().snapshots[0].dominatorTree,
+ "There shouldn't be a dominator tree model yet since it is not computed " +
+ "until we switch to the dominators view.");
+
+ // Wait for the dominator tree to finish being fetched.
+ yield waitUntilState(store, state =>
+ state.snapshots[0] &&
+ state.snapshots[0].dominatorTree &&
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED);
+
+ ok(getState().labelDisplay,
+ "We have a default display for describing nodes in a dominator tree");
+ equal(getState().labelDisplay,
+ labelDisplays.coarseType,
+ "and the default is coarse type");
+ equal(getState().labelDisplay,
+ getState().snapshots[0].dominatorTree.display,
+ "and the newly computed dominator tree has that display");
+
+ // Switch to the allocationStack display.
+ dispatch(setLabelDisplayAndRefresh(
+ heapWorker,
+ labelDisplays.allocationStack));
+
+ yield waitUntilState(store, state =>
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.FETCHING);
+ ok(true,
+ "switching display types caused the dominator tree to be fetched " +
+ "again.");
+
+ yield waitUntilState(store, state =>
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED);
+ equal(getState().snapshots[0].dominatorTree.display,
+ labelDisplays.allocationStack,
+ "The new dominator tree's display is allocationStack");
+ equal(getState().labelDisplay,
+ labelDisplays.allocationStack,
+ "as is our requested dominator tree display");
+
+ heapWorker.destroy();
+ yield front.detach();
+});
diff --git a/devtools/client/memory/test/unit/test_dominator_trees_09.js b/devtools/client/memory/test/unit/test_dominator_trees_09.js
new file mode 100644
index 000000000..c1d1f7495
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_dominator_trees_09.js
@@ -0,0 +1,78 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that we can change the display with which we describe a dominator tree
+// while the dominator tree is in the middle of being fetched.
+
+const {
+ snapshotState: states,
+ dominatorTreeState,
+ viewState,
+ labelDisplays,
+ treeMapState,
+} = require("devtools/client/memory/constants");
+const {
+ setLabelDisplayAndRefresh
+} = require("devtools/client/memory/actions/label-display");
+const {
+ changeView,
+} = require("devtools/client/memory/actions/view");
+const {
+ takeSnapshotAndCensus,
+ computeAndFetchDominatorTree,
+} = require("devtools/client/memory/actions/snapshot");
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ let front = new StubbedMemoryFront();
+ let heapWorker = new HeapAnalysesClient();
+ yield front.attach();
+ let store = Store();
+ let { getState, dispatch } = store;
+
+ dispatch(changeView(viewState.DOMINATOR_TREE));
+
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ yield waitUntilCensusState(store, s => s.treeMap, [treeMapState.SAVED]);
+ ok(!getState().snapshots[0].dominatorTree,
+ "There shouldn't be a dominator tree model yet since it is not computed " +
+ "until we switch to the dominators view.");
+
+ // Wait for the dominator tree to start fetching.
+ yield waitUntilState(store, state =>
+ state.snapshots[0] &&
+ state.snapshots[0].dominatorTree &&
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.FETCHING);
+
+ ok(getState().labelDisplay,
+ "We have a default display for describing nodes in a dominator tree");
+ equal(getState().labelDisplay,
+ labelDisplays.coarseType,
+ "and the default is coarse type");
+ equal(getState().labelDisplay,
+ getState().snapshots[0].dominatorTree.display,
+ "and the newly computed dominator tree has that display");
+
+ // Switch to the allocationStack display while we are still fetching the
+ // dominator tree.
+ dispatch(setLabelDisplayAndRefresh(
+ heapWorker,
+ labelDisplays.allocationStack));
+
+ // Wait for the dominator tree to finish being fetched.
+ yield waitUntilState(store, state =>
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED);
+
+ equal(getState().snapshots[0].dominatorTree.display,
+ labelDisplays.allocationStack,
+ "The new dominator tree's display is allocationStack");
+ equal(getState().labelDisplay,
+ labelDisplays.allocationStack,
+ "as is our requested dominator tree display");
+
+ heapWorker.destroy();
+ yield front.detach();
+});
diff --git a/devtools/client/memory/test/unit/test_dominator_trees_10.js b/devtools/client/memory/test/unit/test_dominator_trees_10.js
new file mode 100644
index 000000000..f7cf86a10
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_dominator_trees_10.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that we maintain focus of the selected dominator tree node across
+// changing breakdowns for labeling them.
+
+let {
+ snapshotState: states,
+ dominatorTreeState,
+ labelDisplays,
+ viewState,
+} = require("devtools/client/memory/constants");
+let {
+ takeSnapshotAndCensus,
+ focusDominatorTreeNode,
+} = require("devtools/client/memory/actions/snapshot");
+const {
+ changeView,
+} = require("devtools/client/memory/actions/view");
+const {
+ setLabelDisplayAndRefresh,
+} = require("devtools/client/memory/actions/label-display");
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ let front = new StubbedMemoryFront();
+ let heapWorker = new HeapAnalysesClient();
+ yield front.attach();
+ let store = Store();
+ let { getState, dispatch } = store;
+
+ dispatch(changeView(viewState.DOMINATOR_TREE));
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+
+ // Wait for the dominator tree to finish being fetched.
+ yield waitUntilState(store, state =>
+ state.snapshots[0] &&
+ state.snapshots[0].dominatorTree &&
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED);
+ ok(true, "The dominator tree was fetched");
+
+ const root = getState().snapshots[0].dominatorTree.root;
+ ok(root, "When the dominator tree is loaded, we should have its root");
+
+ dispatch(focusDominatorTreeNode(getState().snapshots[0].id, root));
+ equal(root, getState().snapshots[0].dominatorTree.focused,
+ "The root should be focused.");
+
+ equal(getState().labelDisplay, labelDisplays.coarseType,
+ "Using labelDisplays.coarseType by default");
+ dispatch(setLabelDisplayAndRefresh(heapWorker,
+ labelDisplays.allocationStack));
+ equal(getState().labelDisplay, labelDisplays.allocationStack,
+ "Using labelDisplays.allocationStack now");
+
+ yield waitUntilState(store, state =>
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.FETCHING);
+ ok(true, "We started re-fetching the dominator tree");
+
+ yield waitUntilState(store, state =>
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED);
+ ok(true, "The dominator tree was loaded again");
+
+ ok(getState().snapshots[0].dominatorTree.focused,
+ "Still have a focused node");
+ equal(getState().snapshots[0].dominatorTree.focused.nodeId, root.nodeId,
+ "Focused node is the same as before");
+
+ heapWorker.destroy();
+ yield front.detach();
+});
diff --git a/devtools/client/memory/test/unit/test_individuals_01.js b/devtools/client/memory/test/unit/test_individuals_01.js
new file mode 100644
index 000000000..36971a7dc
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_individuals_01.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Basic test for switching to the individuals view.
+
+const {
+ censusState,
+ viewState,
+ individualsState,
+} = require("devtools/client/memory/constants");
+const {
+ fetchIndividuals,
+ takeSnapshotAndCensus,
+} = require("devtools/client/memory/actions/snapshot");
+const {
+ changeView,
+} = require("devtools/client/memory/actions/view");
+
+function run_test() {
+ run_next_test();
+}
+
+const EXPECTED_INDIVIDUAL_STATES = [
+ individualsState.COMPUTING_DOMINATOR_TREE,
+ individualsState.FETCHING,
+ individualsState.FETCHED,
+];
+
+add_task(function* () {
+ let front = new StubbedMemoryFront();
+ let heapWorker = new HeapAnalysesClient();
+ yield front.attach();
+ let store = Store();
+ const { getState, dispatch } = store;
+
+ equal(getState().individuals, null,
+ "no individuals state by default");
+
+ dispatch(changeView(viewState.CENSUS));
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ yield waitUntilCensusState(store, s => s.census, [censusState.SAVED]);
+
+ const root = getState().snapshots[0].census.report;
+ ok(root, "Should have a census");
+
+ const reportLeafIndex = findReportLeafIndex(root);
+ ok(reportLeafIndex, "Should get a reportLeafIndex");
+
+ const snapshotId = getState().snapshots[0].id;
+ ok(snapshotId, "Should have a snapshot id");
+
+ const breakdown = getState().snapshots[0].census.display.breakdown;
+ ok(breakdown, "Should have a breakdown");
+
+ dispatch(fetchIndividuals(heapWorker, snapshotId, breakdown,
+ reportLeafIndex));
+
+ // Wait for each expected state.
+ for (let state of EXPECTED_INDIVIDUAL_STATES) {
+ yield waitUntilState(store, s => {
+ return s.view.state === viewState.INDIVIDUALS &&
+ s.individuals &&
+ s.individuals.state === state;
+ });
+ ok(true, `Reached state = ${state}`);
+ }
+
+ ok(getState().individuals, "Should have individuals state");
+ ok(getState().individuals.nodes, "Should have individuals nodes");
+ ok(getState().individuals.nodes.length > 0,
+ "Should have a positive number of nodes");
+
+ heapWorker.destroy();
+ yield front.detach();
+});
diff --git a/devtools/client/memory/test/unit/test_individuals_02.js b/devtools/client/memory/test/unit/test_individuals_02.js
new file mode 100644
index 000000000..9e23e2c4f
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_individuals_02.js
@@ -0,0 +1,88 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test switching to the individuals view when we are in the middle of computing
+// a dominator tree.
+
+const {
+ censusState,
+ dominatorTreeState,
+ viewState,
+ individualsState,
+} = require("devtools/client/memory/constants");
+const {
+ fetchIndividuals,
+ takeSnapshotAndCensus,
+ computeDominatorTree,
+} = require("devtools/client/memory/actions/snapshot");
+const {
+ changeView,
+} = require("devtools/client/memory/actions/view");
+
+function run_test() {
+ run_next_test();
+}
+
+const EXPECTED_INDIVIDUAL_STATES = [
+ individualsState.COMPUTING_DOMINATOR_TREE,
+ individualsState.FETCHING,
+ individualsState.FETCHED,
+];
+
+add_task(function* () {
+ let front = new StubbedMemoryFront();
+ let heapWorker = new HeapAnalysesClient();
+ yield front.attach();
+ let store = Store();
+ const { getState, dispatch } = store;
+
+ equal(getState().individuals, null,
+ "no individuals state by default");
+
+ dispatch(changeView(viewState.CENSUS));
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ yield waitUntilCensusState(store, s => s.census, [censusState.SAVED]);
+
+ const root = getState().snapshots[0].census.report;
+ ok(root, "Should have a census");
+
+ const reportLeafIndex = findReportLeafIndex(root);
+ ok(reportLeafIndex, "Should get a reportLeafIndex");
+
+ const snapshotId = getState().snapshots[0].id;
+ ok(snapshotId, "Should have a snapshot id");
+
+ const breakdown = getState().snapshots[0].census.display.breakdown;
+ ok(breakdown, "Should have a breakdown");
+
+ // Start computing a dominator tree.
+
+ dispatch(computeDominatorTree(heapWorker, snapshotId));
+ equal(getState().snapshots[0].dominatorTree.state,
+ dominatorTreeState.COMPUTING,
+ "Should be computing dominator tree");
+
+ // Fetch individuals in the middle of computing the dominator tree.
+
+ dispatch(fetchIndividuals(heapWorker, snapshotId, breakdown,
+ reportLeafIndex));
+
+ // Wait for each expected state.
+ for (let state of EXPECTED_INDIVIDUAL_STATES) {
+ yield waitUntilState(store, s => {
+ return s.view.state === viewState.INDIVIDUALS &&
+ s.individuals &&
+ s.individuals.state === state;
+ });
+ ok(true, `Reached state = ${state}`);
+ }
+
+ ok(getState().individuals, "Should have individuals state");
+ ok(getState().individuals.nodes, "Should have individuals nodes");
+ ok(getState().individuals.nodes.length > 0,
+ "Should have a positive number of nodes");
+
+ heapWorker.destroy();
+ yield front.detach();
+});
diff --git a/devtools/client/memory/test/unit/test_individuals_03.js b/devtools/client/memory/test/unit/test_individuals_03.js
new file mode 100644
index 000000000..54460b737
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_individuals_03.js
@@ -0,0 +1,106 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test switching to the individuals view when we are in the diffing view.
+
+const {
+ censusState,
+ diffingState,
+ viewState,
+ individualsState,
+} = require("devtools/client/memory/constants");
+const {
+ fetchIndividuals,
+ takeSnapshotAndCensus,
+} = require("devtools/client/memory/actions/snapshot");
+const {
+ changeView,
+ popViewAndRefresh,
+} = require("devtools/client/memory/actions/view");
+const {
+ selectSnapshotForDiffingAndRefresh,
+} = require("devtools/client/memory/actions/diffing");
+
+function run_test() {
+ run_next_test();
+}
+
+const EXPECTED_INDIVIDUAL_STATES = [
+ individualsState.COMPUTING_DOMINATOR_TREE,
+ individualsState.FETCHING,
+ individualsState.FETCHED,
+];
+
+add_task(function* () {
+ let front = new StubbedMemoryFront();
+ let heapWorker = new HeapAnalysesClient();
+ yield front.attach();
+ let store = Store();
+ const { getState, dispatch } = store;
+
+ dispatch(changeView(viewState.CENSUS));
+
+ // Take two snapshots and diff them from each other.
+
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ yield waitUntilCensusState(store, s => s.census, [censusState.SAVED,
+ censusState.SAVED]);
+
+ dispatch(changeView(viewState.DIFFING));
+ dispatch(selectSnapshotForDiffingAndRefresh(heapWorker, getState().snapshots[0]));
+ dispatch(selectSnapshotForDiffingAndRefresh(heapWorker, getState().snapshots[1]));
+
+ yield waitUntilState(store, state => {
+ return state.diffing &&
+ state.diffing.state === diffingState.TOOK_DIFF;
+ });
+ ok(getState().diffing.census);
+
+ // Fetch individuals.
+
+ const root = getState().diffing.census.report;
+ ok(root, "Should have a census");
+
+ const reportLeafIndex = findReportLeafIndex(root);
+ ok(reportLeafIndex, "Should get a reportLeafIndex");
+
+ const snapshotId = getState().diffing.secondSnapshotId;
+ ok(snapshotId, "Should have a snapshot id");
+
+ const breakdown = getState().censusDisplay.breakdown;
+ ok(breakdown, "Should have a breakdown");
+
+ dispatch(fetchIndividuals(heapWorker, snapshotId, breakdown,
+ reportLeafIndex));
+
+ for (let state of EXPECTED_INDIVIDUAL_STATES) {
+ yield waitUntilState(store, s => {
+ return s.view.state === viewState.INDIVIDUALS &&
+ s.individuals &&
+ s.individuals.state === state;
+ });
+ ok(true, `Reached state = ${state}`);
+ }
+
+ ok(getState().individuals, "Should have individuals state");
+ ok(getState().individuals.nodes, "Should have individuals nodes");
+ ok(getState().individuals.nodes.length > 0,
+ "Should have a positive number of nodes");
+
+ // Pop the view back to the diffing.
+
+ dispatch(popViewAndRefresh(heapWorker));
+
+ yield waitUntilState(store, state => {
+ return state.diffing &&
+ state.diffing.state === diffingState.TOOK_DIFF;
+ });
+
+ ok(getState().diffing.census.report,
+ "We have our census diff again after popping back to the last view");
+
+ heapWorker.destroy();
+ yield front.detach();
+});
diff --git a/devtools/client/memory/test/unit/test_individuals_04.js b/devtools/client/memory/test/unit/test_individuals_04.js
new file mode 100644
index 000000000..c160d555f
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_individuals_04.js
@@ -0,0 +1,89 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test showing individual Array objects.
+
+const {
+ censusState,
+ viewState,
+ individualsState,
+} = require("devtools/client/memory/constants");
+const {
+ fetchIndividuals,
+ takeSnapshotAndCensus,
+} = require("devtools/client/memory/actions/snapshot");
+const {
+ changeView,
+} = require("devtools/client/memory/actions/view");
+const {
+ setFilterString,
+} = require("devtools/client/memory/actions/filter");
+
+function run_test() {
+ run_next_test();
+}
+
+const EXPECTED_INDIVIDUAL_STATES = [
+ individualsState.COMPUTING_DOMINATOR_TREE,
+ individualsState.FETCHING,
+ individualsState.FETCHED,
+];
+
+add_task(function* () {
+ let front = new StubbedMemoryFront();
+ let heapWorker = new HeapAnalysesClient();
+ yield front.attach();
+ let store = Store();
+ const { getState, dispatch } = store;
+
+ dispatch(changeView(viewState.CENSUS));
+ dispatch(setFilterString("Array"));
+
+ // Take a snapshot and wait for the census to finish.
+
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ yield waitUntilCensusState(store, s => s.census, [censusState.SAVED]);
+
+ // Fetch individuals.
+
+ const root = getState().snapshots[0].census.report;
+ ok(root, "Should have a census");
+
+ const reportLeafIndex = findReportLeafIndex(root, "Array");
+ ok(reportLeafIndex, "Should get a reportLeafIndex for Array");
+
+ const snapshotId = getState().snapshots[0].id;
+ ok(snapshotId, "Should have a snapshot id");
+
+ const breakdown = getState().censusDisplay.breakdown;
+ ok(breakdown, "Should have a breakdown");
+
+ dispatch(fetchIndividuals(heapWorker, snapshotId, breakdown,
+ reportLeafIndex));
+
+ for (let state of EXPECTED_INDIVIDUAL_STATES) {
+ yield waitUntilState(store, s => {
+ return s.view.state === viewState.INDIVIDUALS &&
+ s.individuals &&
+ s.individuals.state === state;
+ });
+ ok(true, `Reached state = ${state}`);
+ }
+
+ ok(getState().individuals, "Should have individuals state");
+ ok(getState().individuals.nodes, "Should have individuals nodes");
+ ok(getState().individuals.nodes.length > 0,
+ "Should have a positive number of nodes");
+
+ // Assert that all the individuals are `Array`s.
+
+ for (let node of getState().individuals.nodes) {
+ dumpn("Checking node: " + node.label.join(" > "));
+ ok(node.label.find(part => part === "Array"),
+ "The node should be an Array node");
+ }
+
+ heapWorker.destroy();
+ yield front.detach();
+});
diff --git a/devtools/client/memory/test/unit/test_individuals_05.js b/devtools/client/memory/test/unit/test_individuals_05.js
new file mode 100644
index 000000000..1f8873826
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_individuals_05.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test showing individual objects that do not have allocation stacks.
+
+const {
+ censusState,
+ viewState,
+ individualsState,
+ censusDisplays,
+} = require("devtools/client/memory/constants");
+const {
+ fetchIndividuals,
+ takeSnapshotAndCensus,
+} = require("devtools/client/memory/actions/snapshot");
+const {
+ changeView,
+} = require("devtools/client/memory/actions/view");
+const {
+ setCensusDisplay,
+} = require("devtools/client/memory/actions/census-display");
+
+function run_test() {
+ run_next_test();
+}
+
+const EXPECTED_INDIVIDUAL_STATES = [
+ individualsState.COMPUTING_DOMINATOR_TREE,
+ individualsState.FETCHING,
+ individualsState.FETCHED,
+];
+
+add_task(function* () {
+ let front = new StubbedMemoryFront();
+ let heapWorker = new HeapAnalysesClient();
+ yield front.attach();
+ let store = Store();
+ const { getState, dispatch } = store;
+
+ dispatch(changeView(viewState.CENSUS));
+ dispatch(setCensusDisplay(censusDisplays.invertedAllocationStack));
+
+ // Take a snapshot and wait for the census to finish.
+
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ yield waitUntilCensusState(store, s => s.census, [censusState.SAVED]);
+
+ // Fetch individuals.
+
+ const root = getState().snapshots[0].census.report;
+ ok(root, "Should have a census");
+
+ const reportLeafIndex = findReportLeafIndex(root, "noStack");
+ ok(reportLeafIndex, "Should get a reportLeafIndex for noStack");
+
+ const snapshotId = getState().snapshots[0].id;
+ ok(snapshotId, "Should have a snapshot id");
+
+ const breakdown = getState().censusDisplay.breakdown;
+ ok(breakdown, "Should have a breakdown");
+
+ dispatch(fetchIndividuals(heapWorker, snapshotId, breakdown,
+ reportLeafIndex));
+
+ for (let state of EXPECTED_INDIVIDUAL_STATES) {
+ yield waitUntilState(store, s => {
+ return s.view.state === viewState.INDIVIDUALS &&
+ s.individuals &&
+ s.individuals.state === state;
+ });
+ ok(true, `Reached state = ${state}`);
+ }
+
+ ok(getState().individuals, "Should have individuals state");
+ ok(getState().individuals.nodes, "Should have individuals nodes");
+ ok(getState().individuals.nodes.length > 0,
+ "Should have a positive number of nodes");
+
+ heapWorker.destroy();
+ yield front.detach();
+});
diff --git a/devtools/client/memory/test/unit/test_individuals_06.js b/devtools/client/memory/test/unit/test_individuals_06.js
new file mode 100644
index 000000000..6147b8181
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_individuals_06.js
@@ -0,0 +1,84 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that clearing the current individuals' snapshot leaves the individuals
+// view.
+
+const {
+ censusState,
+ viewState,
+ individualsState,
+} = require("devtools/client/memory/constants");
+const {
+ fetchIndividuals,
+ takeSnapshotAndCensus,
+ clearSnapshots,
+} = require("devtools/client/memory/actions/snapshot");
+const {
+ changeView,
+} = require("devtools/client/memory/actions/view");
+
+function run_test() {
+ run_next_test();
+}
+
+const EXPECTED_INDIVIDUAL_STATES = [
+ individualsState.COMPUTING_DOMINATOR_TREE,
+ individualsState.FETCHING,
+ individualsState.FETCHED,
+];
+
+add_task(function* () {
+ let front = new StubbedMemoryFront();
+ let heapWorker = new HeapAnalysesClient();
+ yield front.attach();
+ let store = Store();
+ const { getState, dispatch } = store;
+
+ dispatch(changeView(viewState.CENSUS));
+
+ // Take a snapshot and wait for the census to finish.
+
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ yield waitUntilCensusState(store, s => s.census, [censusState.SAVED]);
+
+ // Fetch individuals.
+
+ const root = getState().snapshots[0].census.report;
+ ok(root, "Should have a census");
+
+ const reportLeafIndex = findReportLeafIndex(root);
+ ok(reportLeafIndex, "Should get a reportLeafIndex");
+
+ const snapshotId = getState().snapshots[0].id;
+ ok(snapshotId, "Should have a snapshot id");
+
+ const breakdown = getState().censusDisplay.breakdown;
+ ok(breakdown, "Should have a breakdown");
+
+ dispatch(fetchIndividuals(heapWorker, snapshotId, breakdown,
+ reportLeafIndex));
+
+ for (let state of EXPECTED_INDIVIDUAL_STATES) {
+ yield waitUntilState(store, s => {
+ return s.view.state === viewState.INDIVIDUALS &&
+ s.individuals &&
+ s.individuals.state === state;
+ });
+ ok(true, `Reached state = ${state}`);
+ }
+
+ ok(getState().individuals, "Should have individuals state");
+ ok(getState().individuals.nodes, "Should have individuals nodes");
+ ok(getState().individuals.nodes.length > 0,
+ "Should have a positive number of nodes");
+
+ dispatch(clearSnapshots(heapWorker));
+
+ equal(getState().view.state, viewState.CENSUS,
+ "Went back to census view");
+
+ heapWorker.destroy();
+ yield front.detach();
+});
diff --git a/devtools/client/memory/test/unit/test_pop_view_01.js b/devtools/client/memory/test/unit/test_pop_view_01.js
new file mode 100644
index 000000000..9e6a17095
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_pop_view_01.js
@@ -0,0 +1,81 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test popping views from each intermediate individuals model state.
+
+const {
+ censusState,
+ viewState,
+ individualsState,
+} = require("devtools/client/memory/constants");
+const {
+ fetchIndividuals,
+ takeSnapshotAndCensus,
+} = require("devtools/client/memory/actions/snapshot");
+const {
+ changeView,
+ popViewAndRefresh,
+} = require("devtools/client/memory/actions/view");
+
+function run_test() {
+ run_next_test();
+}
+
+const TEST_STATES = [
+ individualsState.COMPUTING_DOMINATOR_TREE,
+ individualsState.FETCHING,
+ individualsState.FETCHED,
+];
+
+add_task(function* () {
+ let front = new StubbedMemoryFront();
+ let heapWorker = new HeapAnalysesClient();
+ yield front.attach();
+ let store = Store();
+ const { getState, dispatch } = store;
+
+ equal(getState().individuals, null,
+ "no individuals state by default");
+
+ dispatch(changeView(viewState.CENSUS));
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ yield waitUntilCensusState(store, s => s.census, [censusState.SAVED]);
+
+ const root = getState().snapshots[0].census.report;
+ ok(root, "Should have a census");
+
+ const reportLeafIndex = findReportLeafIndex(root);
+ ok(reportLeafIndex, "Should get a reportLeafIndex");
+
+ const snapshotId = getState().snapshots[0].id;
+ ok(snapshotId, "Should have a snapshot id");
+
+ const breakdown = getState().snapshots[0].census.display.breakdown;
+ ok(breakdown, "Should have a breakdown");
+
+ for (let state of TEST_STATES) {
+ dumpn(`Testing popping back to the old view from state = ${state}`);
+
+ dispatch(fetchIndividuals(heapWorker, snapshotId, breakdown,
+ reportLeafIndex));
+
+ // Wait for the expected test state.
+ yield waitUntilState(store, s => {
+ return s.view.state === viewState.INDIVIDUALS &&
+ s.individuals &&
+ s.individuals.state === state;
+ });
+ ok(true, `Reached state = ${state}`);
+
+ // Pop back to the CENSUS state.
+ dispatch(popViewAndRefresh(heapWorker));
+ yield waitUntilState(store, s => {
+ return s.view.state === viewState.CENSUS;
+ });
+ ok(!getState().individuals, "Should no longer have individuals");
+ }
+
+ heapWorker.destroy();
+ yield front.detach();
+});
diff --git a/devtools/client/memory/test/unit/test_tree-map-01.js b/devtools/client/memory/test/unit/test_tree-map-01.js
new file mode 100644
index 000000000..15d9a765a
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_tree-map-01.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { drawBox } = require("devtools/client/memory/components/tree-map/draw");
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ let fillRectValues, strokeRectValues;
+ let ctx = {
+ fillRect: (...args) => fillRectValues = args,
+ strokeRect: (...args) => strokeRectValues = args
+ };
+ let node = {
+ x: 20,
+ y: 30,
+ dx: 50,
+ dy: 70,
+ type: "other",
+ depth: 2
+ };
+ let padding = [10, 10];
+ let borderWidth = () => 1;
+ let dragZoom = {
+ offsetX: 0,
+ offsetY: 0,
+ zoom: 0
+ };
+ drawBox(ctx, node, borderWidth, dragZoom, padding);
+ ok(true, JSON.stringify([ctx, fillRectValues, strokeRectValues]));
+ equal(ctx.fillStyle, "hsl(210,60%,70%)", "The fillStyle is set");
+ equal(ctx.strokeStyle, "hsl(210,60%,35%)", "The strokeStyle is set");
+ equal(ctx.lineWidth, 1, "The lineWidth is set");
+ deepEqual(fillRectValues, [10.5, 20.5, 49, 69], "Draws a filled rectangle");
+ deepEqual(strokeRectValues, [10.5, 20.5, 49, 69], "Draws a stroked rectangle");
+
+
+ dragZoom.zoom = 0.5;
+
+ drawBox(ctx, node, borderWidth, dragZoom, padding);
+ ok(true, JSON.stringify([ctx, fillRectValues, strokeRectValues]));
+ deepEqual(fillRectValues, [15.5, 30.5, 74, 104],
+ "Draws a zoomed filled rectangle");
+ deepEqual(strokeRectValues, [15.5, 30.5, 74, 104],
+ "Draws a zoomed stroked rectangle");
+
+ dragZoom.offsetX = 110;
+ dragZoom.offsetY = 130;
+
+ drawBox(ctx, node, borderWidth, dragZoom, padding);
+ deepEqual(fillRectValues, [-94.5, -99.5, 74, 104],
+ "Draws a zoomed and offset filled rectangle");
+ deepEqual(strokeRectValues, [-94.5, -99.5, 74, 104],
+ "Draws a zoomed and offset stroked rectangle");
+});
diff --git a/devtools/client/memory/test/unit/test_tree-map-02.js b/devtools/client/memory/test/unit/test_tree-map-02.js
new file mode 100644
index 000000000..4f7200f0a
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_tree-map-02.js
@@ -0,0 +1,81 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { drawText } = require("devtools/client/memory/components/tree-map/draw");
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ // Mock out the Canvas2dContext
+ let ctx = {
+ fillText: (...args) => fillTextValues.push(args),
+ measureText: (text) => {
+ let width = text ? text.length * 10 : 0;
+ return { width };
+ }
+ };
+ let node = {
+ x: 20,
+ y: 30,
+ dx: 500,
+ dy: 70,
+ name: "Example Node",
+ totalBytes: 1200,
+ totalCount: 100
+ };
+ let ratio = 0;
+ let borderWidth = () => 1;
+ let dragZoom = {
+ offsetX: 0,
+ offsetY: 0,
+ zoom: 0
+ };
+ let fillTextValues = [];
+ let padding = [10, 10];
+
+ drawText(ctx, node, borderWidth, ratio, dragZoom, padding);
+ deepEqual(fillTextValues[0], ["Example Node", 11.5, 21.5],
+ "Fills in the full node name");
+ deepEqual(fillTextValues[1], ["1KiB 100 count", 141.5, 21.5],
+ "Includes the full byte and count information");
+
+ fillTextValues = [];
+ node.dx = 250;
+ drawText(ctx, node, borderWidth, ratio, dragZoom, padding);
+
+ deepEqual(fillTextValues[0], ["Example Node", 11.5, 21.5],
+ "Fills in the full node name");
+ deepEqual(fillTextValues[1], undefined,
+ "Drops off the byte and count information if not enough room");
+
+ fillTextValues = [];
+ node.dx = 100;
+ drawText(ctx, node, borderWidth, ratio, dragZoom, padding);
+
+ deepEqual(fillTextValues[0], ["Exampl...", 11.5, 21.5],
+ "Cuts the name with ellipsis");
+ deepEqual(fillTextValues[1], undefined,
+ "Drops off the byte and count information if not enough room");
+
+ fillTextValues = [];
+ node.dx = 40;
+ drawText(ctx, node, borderWidth, ratio, dragZoom, padding);
+
+ deepEqual(fillTextValues[0], ["...", 11.5, 21.5],
+ "Shows only ellipsis when smaller");
+ deepEqual(fillTextValues[1], undefined,
+ "Drops off the byte and count information if not enough room");
+
+ fillTextValues = [];
+ node.dx = 20;
+ drawText(ctx, node, borderWidth, ratio, dragZoom, padding);
+
+ deepEqual(fillTextValues[0], undefined,
+ "Draw nothing when not enough room");
+ deepEqual(fillTextValues[1], undefined,
+ "Drops off the byte and count information if not enough room");
+});
diff --git a/devtools/client/memory/test/unit/test_utils-get-snapshot-totals.js b/devtools/client/memory/test/unit/test_utils-get-snapshot-totals.js
new file mode 100644
index 000000000..c4560fb07
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_utils-get-snapshot-totals.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that we use the correct snapshot aggregate value
+ * in `utils.getSnapshotTotals(snapshot)`
+ */
+
+const { censusDisplays, snapshotState: states, viewState, censusState } = require("devtools/client/memory/constants");
+const { getSnapshotTotals } = require("devtools/client/memory/utils");
+const { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot");
+const { setCensusDisplayAndRefresh } = require("devtools/client/memory/actions/census-display");
+const { changeView } = require("devtools/client/memory/actions/view");
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ let front = new StubbedMemoryFront();
+ let heapWorker = new HeapAnalysesClient();
+ yield front.attach();
+ let store = Store();
+ let { getState, dispatch } = store;
+
+ dispatch(changeView(viewState.CENSUS));
+
+ yield dispatch(setCensusDisplayAndRefresh(heapWorker,
+ censusDisplays.allocationStack));
+
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ yield waitUntilCensusState(store, s => s.census, [censusState.SAVED]);
+
+ ok(!getState().snapshots[0].census.display.inverted, "Snapshot is not inverted");
+
+ let census = getState().snapshots[0].census;
+ let result = aggregate(census.report);
+ let totalBytes = result.bytes;
+ let totalCount = result.count;
+
+ ok(totalBytes > 0, "counted up bytes in the census");
+ ok(totalCount > 0, "counted up count in the census");
+
+ result = getSnapshotTotals(getState().snapshots[0].census);
+ equal(totalBytes, result.bytes, "getSnapshotTotals reuslted in correct bytes");
+ equal(totalCount, result.count, "getSnapshotTotals reuslted in correct count");
+
+ dispatch(setCensusDisplayAndRefresh(heapWorker,
+ censusDisplays.invertedAllocationStack));
+
+ yield waitUntilCensusState(store, s => s.census, [censusState.SAVING]);
+ yield waitUntilCensusState(store, s => s.census, [censusState.SAVED]);
+ ok(getState().snapshots[0].census.display.inverted, "Snapshot is inverted");
+
+ result = getSnapshotTotals(getState().snapshots[0].census);
+ equal(totalBytes, result.bytes,
+ "getSnapshotTotals reuslted in correct bytes when inverted");
+ equal(totalCount, result.count,
+ "getSnapshotTotals reuslted in correct count when inverted");
+});
+
+function aggregate(report) {
+ let totalBytes = report.bytes;
+ let totalCount = report.count;
+ for (let child of (report.children || [])) {
+ let { bytes, count } = aggregate(child);
+ totalBytes += bytes;
+ totalCount += count;
+ }
+ return { bytes: totalBytes, count: totalCount };
+}
diff --git a/devtools/client/memory/test/unit/test_utils.js b/devtools/client/memory/test/unit/test_utils.js
new file mode 100644
index 000000000..f6deddfc9
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_utils.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests the task creator `takeSnapshotAndCensus()` for the whole flow of
+ * taking a snapshot, and its sub-actions. Tests the formatNumber and
+ * formatPercent methods.
+ */
+
+let utils = require("devtools/client/memory/utils");
+let { snapshotState: states, viewState } = require("devtools/client/memory/constants");
+let { Preferences } = require("resource://gre/modules/Preferences.jsm");
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ let s1 = utils.createSnapshot({ view: { state: viewState.CENSUS } });
+ let s2 = utils.createSnapshot({ view: { state: viewState.CENSUS } });
+ equal(s1.state, states.SAVING, "utils.createSnapshot() creates snapshot in saving state");
+ ok(s1.id !== s2.id, "utils.createSnapshot() creates snapshot with unique ids");
+
+ let custom = { by: "internalType", then: { by: "count", bytes: true }};
+ Preferences.set("devtools.memory.custom-census-displays", JSON.stringify({ "My Display": custom }));
+
+ equal(utils.getCustomCensusDisplays()["My Display"].by, custom.by,
+ "utils.getCustomCensusDisplays() returns custom displays");
+
+ ok(true, "test formatNumber util functions");
+ equal(utils.formatNumber(12), "12", "formatNumber returns 12 for 12");
+
+ equal(utils.formatNumber(0), "0", "formatNumber returns 0 for 0");
+ equal(utils.formatNumber(-0), "0", "formatNumber returns 0 for -0");
+ equal(utils.formatNumber(+0), "0", "formatNumber returns 0 for +0");
+
+ equal(utils.formatNumber(1234567), "1 234 567",
+ "formatNumber adds a space every 3rd digit");
+ equal(utils.formatNumber(12345678), "12 345 678",
+ "formatNumber adds a space every 3rd digit");
+ equal(utils.formatNumber(123456789), "123 456 789",
+ "formatNumber adds a space every 3rd digit");
+
+ equal(utils.formatNumber(12, true), "+12",
+ "formatNumber can display number sign");
+ equal(utils.formatNumber(-12, true), "-12",
+ "formatNumber can display number sign (negative)");
+
+ ok(true, "test formatPercent util functions");
+ equal(utils.formatPercent(12), "12%", "formatPercent returns 12% for 12");
+ equal(utils.formatPercent(12345), "12 345%",
+ "formatPercent returns 12 345% for 12345");
+
+ equal(utils.formatAbbreviatedBytes(12), "12B", "Formats bytes");
+ equal(utils.formatAbbreviatedBytes(12345), "12KiB", "Formats kilobytes");
+ equal(utils.formatAbbreviatedBytes(12345678), "11MiB", "Formats megabytes");
+ equal(utils.formatAbbreviatedBytes(12345678912), "11GiB", "Formats gigabytes");
+
+ equal(utils.hslToStyle(0.5, 0.6, 0.7),
+ "hsl(180,60%,70%)", "hslToStyle converts an array to a style string");
+ equal(utils.hslToStyle(0, 0, 0),
+ "hsl(0,0%,0%)", "hslToStyle converts an array to a style string");
+ equal(utils.hslToStyle(1, 1, 1),
+ "hsl(360,100%,100%)", "hslToStyle converts an array to a style string");
+
+ equal(utils.lerp(5, 7, 0), 5, "lerp return first number for 0");
+ equal(utils.lerp(5, 7, 1), 7, "lerp return second number for 1");
+ equal(utils.lerp(5, 7, 0.5), 6, "lerp interpolates the numbers for 0.5");
+});
diff --git a/devtools/client/memory/test/unit/xpcshell.ini b/devtools/client/memory/test/unit/xpcshell.ini
new file mode 100644
index 000000000..dade269c3
--- /dev/null
+++ b/devtools/client/memory/test/unit/xpcshell.ini
@@ -0,0 +1,56 @@
+[DEFAULT]
+tags = devtools devtools-memory
+head = head.js ../../../framework/test/shared-redux-head.js
+tail =
+firefox-appdir = browser
+skip-if = toolkit == 'android'
+
+[test_action_diffing_01.js]
+[test_action_diffing_02.js]
+[test_action_diffing_03.js]
+[test_action_diffing_04.js]
+[test_action_diffing_05.js]
+[test_action-clear-snapshots_01.js]
+[test_action-clear-snapshots_02.js]
+[test_action-clear-snapshots_03.js]
+[test_action-clear-snapshots_04.js]
+[test_action-clear-snapshots_05.js]
+[test_action-clear-snapshots_06.js]
+[test_action-export-snapshot.js]
+[test_action-filter-01.js]
+[test_action-filter-02.js]
+[test_action-filter-03.js]
+[test_action-import-snapshot-and-census.js]
+[test_action-import-snapshot-dominator-tree.js]
+[test_action-select-snapshot.js]
+[test_action-set-display.js]
+[test_action-set-display-and-refresh-01.js]
+[test_action-set-display-and-refresh-02.js]
+[test_action-take-census.js]
+[test_action-take-snapshot.js]
+[test_action-take-snapshot-and-census.js]
+[test_action-toggle-inverted.js]
+[test_action-toggle-inverted-and-refresh-01.js]
+[test_action-toggle-inverted-and-refresh-02.js]
+[test_action-toggle-recording-allocations.js]
+[test_dominator_trees_01.js]
+[test_dominator_trees_02.js]
+[test_dominator_trees_03.js]
+[test_dominator_trees_04.js]
+[test_dominator_trees_05.js]
+[test_dominator_trees_06.js]
+[test_dominator_trees_07.js]
+[test_dominator_trees_08.js]
+[test_dominator_trees_09.js]
+[test_dominator_trees_10.js]
+[test_individuals_01.js]
+[test_individuals_02.js]
+[test_individuals_03.js]
+[test_individuals_04.js]
+[test_individuals_05.js]
+[test_individuals_06.js]
+[test_pop_view_01.js]
+[test_tree-map-01.js]
+[test_tree-map-02.js]
+[test_utils.js]
+[test_utils-get-snapshot-totals.js]
diff --git a/devtools/client/memory/utils.js b/devtools/client/memory/utils.js
new file mode 100644
index 000000000..d36c9251c
--- /dev/null
+++ b/devtools/client/memory/utils.js
@@ -0,0 +1,529 @@
+/* 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/. */
+
+const { Cu, Cc, Ci } = require("chrome");
+
+const { LocalizationHelper } = require("devtools/shared/l10n");
+const STRINGS_URI = "devtools/client/locales/memory.properties";
+const L10N = exports.L10N = new LocalizationHelper(STRINGS_URI);
+
+const { OS } = require("resource://gre/modules/osfile.jsm");
+const { assert } = require("devtools/shared/DevToolsUtils");
+const { Preferences } = require("resource://gre/modules/Preferences.jsm");
+const CUSTOM_CENSUS_DISPLAY_PREF = "devtools.memory.custom-census-displays";
+const CUSTOM_LABEL_DISPLAY_PREF = "devtools.memory.custom-label-displays";
+const CUSTOM_TREE_MAP_DISPLAY_PREF = "devtools.memory.custom-tree-map-displays";
+const BYTES = 1024;
+const KILOBYTES = Math.pow(BYTES, 2);
+const MEGABYTES = Math.pow(BYTES, 3);
+const DevToolsUtils = require("devtools/shared/DevToolsUtils");
+const {
+ snapshotState: states,
+ diffingState,
+ censusState,
+ treeMapState,
+ dominatorTreeState,
+ individualsState,
+} = require("./constants");
+
+/**
+ * Takes a snapshot object and returns the localized form of its timestamp to be
+ * used as a title.
+ *
+ * @param {Snapshot} snapshot
+ * @return {String}
+ */
+exports.getSnapshotTitle = function (snapshot) {
+ if (!snapshot.creationTime) {
+ return L10N.getStr("snapshot-title.loading");
+ }
+
+ if (snapshot.imported) {
+ // Strip out the extension if it's the expected ".fxsnapshot"
+ return OS.Path.basename(snapshot.path.replace(/\.fxsnapshot$/, ""));
+ }
+
+ let date = new Date(snapshot.creationTime / 1000);
+ return date.toLocaleTimeString(void 0, {
+ year: "2-digit",
+ month: "2-digit",
+ day: "2-digit",
+ hour12: false
+ });
+};
+
+function getCustomDisplaysHelper(pref) {
+ let customDisplays = Object.create(null);
+ try {
+ customDisplays = JSON.parse(Preferences.get(pref)) || Object.create(null);
+ } catch (e) {
+ DevToolsUtils.reportException(
+ `String stored in "${pref}" pref cannot be parsed by \`JSON.parse()\`.`);
+ }
+ return Object.freeze(customDisplays);
+}
+
+/**
+ * Returns custom displays defined in `devtools.memory.custom-census-displays`
+ * pref.
+ *
+ * @return {Object}
+ */
+exports.getCustomCensusDisplays = function () {
+ return getCustomDisplaysHelper(CUSTOM_CENSUS_DISPLAY_PREF);
+};
+
+/**
+ * Returns custom displays defined in
+ * `devtools.memory.custom-label-displays` pref.
+ *
+ * @return {Object}
+ */
+exports.getCustomLabelDisplays = function () {
+ return getCustomDisplaysHelper(CUSTOM_LABEL_DISPLAY_PREF);
+};
+
+/**
+ * Returns custom displays defined in
+ * `devtools.memory.custom-tree-map-displays` pref.
+ *
+ * @return {Object}
+ */
+exports.getCustomTreeMapDisplays = function () {
+ return getCustomDisplaysHelper(CUSTOM_TREE_MAP_DISPLAY_PREF);
+};
+
+/**
+ * Returns a string representing a readable form of the snapshot's state. More
+ * concise than `getStatusTextFull`.
+ *
+ * @param {snapshotState | diffingState} state
+ * @return {String}
+ */
+exports.getStatusText = function (state) {
+ assert(state, "Must have a state");
+
+ switch (state) {
+ case diffingState.ERROR:
+ return L10N.getStr("diffing.state.error");
+
+ case states.ERROR:
+ return L10N.getStr("snapshot.state.error");
+
+ case states.SAVING:
+ return L10N.getStr("snapshot.state.saving");
+
+ case states.IMPORTING:
+ return L10N.getStr("snapshot.state.importing");
+
+ case states.SAVED:
+ case states.READING:
+ return L10N.getStr("snapshot.state.reading");
+
+ case censusState.SAVING:
+ return L10N.getStr("snapshot.state.saving-census");
+
+ case treeMapState.SAVING:
+ return L10N.getStr("snapshot.state.saving-tree-map");
+
+ case diffingState.TAKING_DIFF:
+ return L10N.getStr("diffing.state.taking-diff");
+
+ case diffingState.SELECTING:
+ return L10N.getStr("diffing.state.selecting");
+
+ case dominatorTreeState.COMPUTING:
+ case individualsState.COMPUTING_DOMINATOR_TREE:
+ return L10N.getStr("dominatorTree.state.computing");
+
+ case dominatorTreeState.COMPUTED:
+ case dominatorTreeState.FETCHING:
+ return L10N.getStr("dominatorTree.state.fetching");
+
+ case dominatorTreeState.INCREMENTAL_FETCHING:
+ return L10N.getStr("dominatorTree.state.incrementalFetching");
+
+ case dominatorTreeState.ERROR:
+ return L10N.getStr("dominatorTree.state.error");
+
+ case individualsState.ERROR:
+ return L10N.getStr("individuals.state.error");
+
+ case individualsState.FETCHING:
+ return L10N.getStr("individuals.state.fetching");
+
+ // These states do not have any message to show as other content will be
+ // displayed.
+ case dominatorTreeState.LOADED:
+ case diffingState.TOOK_DIFF:
+ case states.READ:
+ case censusState.SAVED:
+ case treeMapState.SAVED:
+ case individualsState.FETCHED:
+ return "";
+
+ default:
+ assert(false, `Unexpected state: ${state}`);
+ return "";
+ }
+};
+
+/**
+ * Returns a string representing a readable form of the snapshot's state;
+ * more verbose than `getStatusText`.
+ *
+ * @param {snapshotState | diffingState} state
+ * @return {String}
+ */
+exports.getStatusTextFull = function (state) {
+ assert(!!state, "Must have a state");
+
+ switch (state) {
+ case diffingState.ERROR:
+ return L10N.getStr("diffing.state.error.full");
+
+ case states.ERROR:
+ return L10N.getStr("snapshot.state.error.full");
+
+ case states.SAVING:
+ return L10N.getStr("snapshot.state.saving.full");
+
+ case states.IMPORTING:
+ return L10N.getStr("snapshot.state.importing");
+
+ case states.SAVED:
+ case states.READING:
+ return L10N.getStr("snapshot.state.reading.full");
+
+ case censusState.SAVING:
+ return L10N.getStr("snapshot.state.saving-census.full");
+
+ case treeMapState.SAVING:
+ return L10N.getStr("snapshot.state.saving-tree-map.full");
+
+ case diffingState.TAKING_DIFF:
+ return L10N.getStr("diffing.state.taking-diff.full");
+
+ case diffingState.SELECTING:
+ return L10N.getStr("diffing.state.selecting.full");
+
+ case dominatorTreeState.COMPUTING:
+ case individualsState.COMPUTING_DOMINATOR_TREE:
+ return L10N.getStr("dominatorTree.state.computing.full");
+
+ case dominatorTreeState.COMPUTED:
+ case dominatorTreeState.FETCHING:
+ return L10N.getStr("dominatorTree.state.fetching.full");
+
+ case dominatorTreeState.INCREMENTAL_FETCHING:
+ return L10N.getStr("dominatorTree.state.incrementalFetching.full");
+
+ case dominatorTreeState.ERROR:
+ return L10N.getStr("dominatorTree.state.error.full");
+
+ case individualsState.ERROR:
+ return L10N.getStr("individuals.state.error.full");
+
+ case individualsState.FETCHING:
+ return L10N.getStr("individuals.state.fetching.full");
+
+ // These states do not have any full message to show as other content will
+ // be displayed.
+ case dominatorTreeState.LOADED:
+ case diffingState.TOOK_DIFF:
+ case states.READ:
+ case censusState.SAVED:
+ case treeMapState.SAVED:
+ case individualsState.FETCHED:
+ return "";
+
+ default:
+ assert(false, `Unexpected state: ${state}`);
+ return "";
+ }
+};
+
+/**
+ * Return true if the snapshot is in a diffable state, false otherwise.
+ *
+ * @param {snapshotModel} snapshot
+ * @returns {Boolean}
+ */
+exports.snapshotIsDiffable = function snapshotIsDiffable(snapshot) {
+ return (snapshot.census && snapshot.census.state === censusState.SAVED)
+ || (snapshot.census && snapshot.census.state === censusState.SAVING)
+ || snapshot.state === states.SAVED
+ || snapshot.state === states.READ;
+};
+
+/**
+ * Takes an array of snapshots and a snapshot and returns
+ * the snapshot instance in `snapshots` that matches
+ * the snapshot passed in.
+ *
+ * @param {appModel} state
+ * @param {snapshotId} id
+ * @return {snapshotModel|null}
+ */
+exports.getSnapshot = function getSnapshot(state, id) {
+ const found = state.snapshots.find(s => s.id === id);
+ assert(found, `No matching snapshot found with id = ${id}`);
+ return found;
+};
+
+/**
+ * Get the ID of the selected snapshot, if one is selected, null otherwise.
+ *
+ * @returns {SnapshotId|null}
+ */
+exports.findSelectedSnapshot = function (state) {
+ const found = state.snapshots.find(s => s.selected);
+ return found ? found.id : null;
+};
+
+/**
+ * Creates a new snapshot object.
+ *
+ * @param {appModel} state
+ * @return {Snapshot}
+ */
+let ID_COUNTER = 0;
+exports.createSnapshot = function createSnapshot(state) {
+ let dominatorTree = null;
+ if (state.view.state === dominatorTreeState.DOMINATOR_TREE) {
+ dominatorTree = Object.freeze({
+ dominatorTreeId: null,
+ root: null,
+ error: null,
+ state: dominatorTreeState.COMPUTING,
+ });
+ }
+
+ return Object.freeze({
+ id: ++ID_COUNTER,
+ state: states.SAVING,
+ dominatorTree,
+ census: null,
+ treeMap: null,
+ path: null,
+ imported: false,
+ selected: false,
+ error: null,
+ });
+};
+
+/**
+ * Return true if the census is up to date with regards to the current filtering
+ * and requested display, false otherwise.
+ *
+ * @param {String} filter
+ * @param {censusDisplayModel} display
+ * @param {censusModel} census
+ *
+ * @returns {Boolean}
+ */
+exports.censusIsUpToDate = function (filter, display, census) {
+ return census
+ // Filter could be null == undefined so use loose equality.
+ && filter == census.filter
+ && display === census.display;
+};
+
+
+/**
+ * Check to see if the snapshot is in a state that it can take a census.
+ *
+ * @param {SnapshotModel} A snapshot to check.
+ * @param {Boolean} Assert that the snapshot must be in a ready state.
+ * @returns {Boolean}
+ */
+exports.canTakeCensus = function (snapshot) {
+ return snapshot.state === states.READ &&
+ ((!snapshot.census || snapshot.census.state === censusState.SAVED) ||
+ (!snapshot.treeMap || snapshot.treeMap.state === treeMapState.SAVED));
+};
+
+/**
+ * Returns true if the given snapshot's dominator tree has been computed, false
+ * otherwise.
+ *
+ * @param {SnapshotModel} snapshot
+ * @returns {Boolean}
+ */
+exports.dominatorTreeIsComputed = function (snapshot) {
+ return snapshot.dominatorTree &&
+ (snapshot.dominatorTree.state === dominatorTreeState.COMPUTED ||
+ snapshot.dominatorTree.state === dominatorTreeState.LOADED ||
+ snapshot.dominatorTree.state === dominatorTreeState.INCREMENTAL_FETCHING);
+};
+
+/**
+ * Find the first SAVED census, either from the tree map or the normal
+ * census.
+ *
+ * @param {SnapshotModel} snapshot
+ * @returns {Object|null} Either the census, or null if one hasn't completed
+ */
+exports.getSavedCensus = function (snapshot) {
+ if (snapshot.treeMap && snapshot.treeMap.state === treeMapState.SAVED) {
+ return snapshot.treeMap;
+ }
+ if (snapshot.census && snapshot.census.state === censusState.SAVED) {
+ return snapshot.census;
+ }
+ return null;
+};
+
+/**
+ * Takes a snapshot and returns the total bytes and total count that this
+ * snapshot represents.
+ *
+ * @param {CensusModel} census
+ * @return {Object}
+ */
+exports.getSnapshotTotals = function (census) {
+ let bytes = 0;
+ let count = 0;
+
+ let report = census.report;
+ if (report) {
+ bytes = report.totalBytes;
+ count = report.totalCount;
+ }
+
+ return { bytes, count };
+};
+
+/**
+ * Takes some configurations and opens up a file picker and returns
+ * a promise to the chosen file if successful.
+ *
+ * @param {String} .title
+ * The title displayed in the file picker window.
+ * @param {Array<Array<String>>} .filters
+ * An array of filters to display in the file picker. Each filter in the array
+ * is a duple of two strings, one a name for the filter, and one the filter itself
+ * (like "*.json").
+ * @param {String} .defaultName
+ * The default name chosen by the file picker window.
+ * @param {String} .mode
+ * The mode that this filepicker should open in. Can be "open" or "save".
+ * @return {Promise<?nsILocalFile>}
+ * The file selected by the user, or null, if cancelled.
+ */
+exports.openFilePicker = function ({ title, filters, defaultName, mode }) {
+ mode = mode === "save" ? Ci.nsIFilePicker.modeSave :
+ mode === "open" ? Ci.nsIFilePicker.modeOpen : null;
+
+ if (mode == void 0) {
+ throw new Error("No valid mode specified for nsIFilePicker.");
+ }
+
+ let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ fp.init(window, title, mode);
+
+ for (let filter of (filters || [])) {
+ fp.appendFilter(filter[0], filter[1]);
+ }
+ fp.defaultString = defaultName;
+
+ return new Promise(resolve => {
+ fp.open({
+ done: result => {
+ if (result === Ci.nsIFilePicker.returnCancel) {
+ resolve(null);
+ return;
+ }
+ resolve(fp.file);
+ }
+ });
+ });
+};
+
+/**
+ * Format the provided number with a space every 3 digits, and optionally
+ * prefixed by its sign.
+ *
+ * @param {Number} number
+ * @param {Boolean} showSign (defaults to false)
+ */
+exports.formatNumber = function (number, showSign = false) {
+ const rounded = Math.round(number);
+ if (rounded === 0 || rounded === -0) {
+ return "0";
+ }
+
+ const abs = String(Math.abs(rounded));
+ // replace every digit followed by (sets of 3 digits) by (itself and a space)
+ const formatted = abs.replace(/(\d)(?=(\d{3})+$)/g, "$1 ");
+
+ if (showSign) {
+ const sign = rounded < 0 ? "-" : "+";
+ return sign + formatted;
+ }
+ return formatted;
+};
+
+/**
+ * Format the provided percentage following the same logic as formatNumber and
+ * an additional % suffix.
+ *
+ * @param {Number} percent
+ * @param {Boolean} showSign (defaults to false)
+ */
+exports.formatPercent = function (percent, showSign = false) {
+ return exports.L10N.getFormatStr("tree-item.percent2",
+ exports.formatNumber(percent, showSign));
+};
+
+/**
+ * Change an HSL color array with values ranged 0-1 to a properly formatted
+ * ctx.fillStyle string.
+ *
+ * @param {Number} h
+ * hue values ranged between [0 - 1]
+ * @param {Number} s
+ * hue values ranged between [0 - 1]
+ * @param {Number} l
+ * hue values ranged between [0 - 1]
+ * @return {type}
+ */
+exports.hslToStyle = function (h, s, l) {
+ h = parseInt(h * 360, 10);
+ s = parseInt(s * 100, 10);
+ l = parseInt(l * 100, 10);
+
+ return `hsl(${h},${s}%,${l}%)`;
+};
+
+/**
+ * Linearly interpolate between 2 numbers.
+ *
+ * @param {Number} a
+ * @param {Number} b
+ * @param {Number} t
+ * A value of 0 returns a, and 1 returns b
+ * @return {Number}
+ */
+exports.lerp = function (a, b, t) {
+ return a * (1 - t) + b * t;
+};
+
+/**
+ * Format a number of bytes as human readable, e.g. 13434 => '13KiB'.
+ *
+ * @param {Number} n
+ * Number of bytes
+ * @return {String}
+ */
+exports.formatAbbreviatedBytes = function (n) {
+ if (n < BYTES) {
+ return n + "B";
+ } else if (n < KILOBYTES) {
+ return Math.floor(n / BYTES) + "KiB";
+ } else if (n < MEGABYTES) {
+ return Math.floor(n / KILOBYTES) + "MiB";
+ }
+ return Math.floor(n / MEGABYTES) + "GiB";
+};
diff --git a/devtools/client/menus.js b/devtools/client/menus.js
new file mode 100644
index 000000000..1aee85095
--- /dev/null
+++ b/devtools/client/menus.js
@@ -0,0 +1,195 @@
+/* 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";
+
+/**
+ * This module defines the sorted list of menuitems inserted into the
+ * "Web Developer" menu.
+ * It also defines the key shortcuts that relates to them.
+ *
+ * Various fields are necessary for historical compatiblity with XUL/addons:
+ * - id:
+ * used as <xul:menuitem> id attribute
+ * - l10nKey:
+ * prefix used to locale localization strings from menus.properties
+ * - oncommand:
+ * function called when the menu item or key shortcut are fired
+ * - key:
+ * - id:
+ * prefixed by 'key_' to compute <xul:key> id attribute
+ * - modifiers:
+ * optional modifiers for the key shortcut
+ * - keytext:
+ * boolean, to set to true for key shortcut using regular character
+ * - additionalKeys:
+ * Array of additional keys, see `key` definition.
+ * - disabled:
+ * If true, the menuitem and key shortcut are going to be hidden and disabled
+ * on startup, until some runtime code eventually enable them.
+ * - checkbox:
+ * If true, the menuitem is prefixed by a checkbox and runtime code can
+ * toggle it.
+ */
+
+const Services = require("Services");
+const isMac = Services.appinfo.OS === "Darwin";
+
+loader.lazyRequireGetter(this, "gDevToolsBrowser", "devtools/client/framework/devtools-browser", true);
+loader.lazyRequireGetter(this, "CommandUtils", "devtools/client/shared/developer-toolbar", true);
+loader.lazyRequireGetter(this, "TargetFactory", "devtools/client/framework/target", true);
+
+loader.lazyImporter(this, "BrowserToolboxProcess", "resource://devtools/client/framework/ToolboxProcess.jsm");
+loader.lazyImporter(this, "ResponsiveUIManager", "resource://devtools/client/responsivedesign/responsivedesign.jsm");
+loader.lazyImporter(this, "ScratchpadManager", "resource://devtools/client/scratchpad/scratchpad-manager.jsm");
+
+exports.menuitems = [
+ { id: "menu_devToolbox",
+ l10nKey: "devToolboxMenuItem",
+ oncommand(event) {
+ let window = event.target.ownerDocument.defaultView;
+ gDevToolsBrowser.toggleToolboxCommand(window.gBrowser);
+ },
+ key: {
+ id: "devToolboxMenuItem",
+ modifiers: isMac ? "accel,alt" : "accel,shift",
+ // This is the only one with a letter key
+ // and needs to be translated differently
+ keytext: true,
+ },
+ additionalKeys: [{
+ id: "devToolboxMenuItemF12",
+ l10nKey: "devToolsCmd",
+ }],
+ checkbox: true
+ },
+ { id: "menu_devtools_separator",
+ separator: true },
+ { id: "menu_devToolbar",
+ l10nKey: "devToolbarMenu",
+ disabled: true,
+ oncommand(event) {
+ let window = event.target.ownerDocument.defaultView;
+ // Distinguish events when selecting a menuitem, where we either open
+ // or close the toolbar and when hitting the key shortcut where we just
+ // focus the toolbar if it doesn't already has it.
+ if (event.target.tagName.toLowerCase() == "menuitem") {
+ window.DeveloperToolbar.toggle();
+ } else {
+ window.DeveloperToolbar.focusToggle();
+ }
+ },
+ key: {
+ id: "devToolbar",
+ modifiers: "shift"
+ },
+ checkbox: true
+ },
+ { id: "menu_webide",
+ l10nKey: "webide",
+ disabled: true,
+ oncommand() {
+ gDevToolsBrowser.openWebIDE();
+ },
+ key: {
+ id: "webide",
+ modifiers: "shift"
+ }
+ },
+ { id: "menu_browserToolbox",
+ l10nKey: "browserToolboxMenu",
+ disabled: true,
+ oncommand() {
+ BrowserToolboxProcess.init();
+ },
+ key: {
+ id: "browserToolbox",
+ modifiers: "accel,alt,shift",
+ keytext: true
+ }
+ },
+ { id: "menu_browserContentToolbox",
+ l10nKey: "browserContentToolboxMenu",
+ disabled: true,
+ oncommand(event) {
+ let window = event.target.ownerDocument.defaultView;
+ gDevToolsBrowser.openContentProcessToolbox(window.gBrowser);
+ }
+ },
+ { id: "menu_browserConsole",
+ l10nKey: "browserConsoleCmd",
+ oncommand() {
+ let HUDService = require("devtools/client/webconsole/hudservice");
+ HUDService.openBrowserConsoleOrFocus();
+ },
+ key: {
+ id: "browserConsole",
+ modifiers: "accel,shift",
+ keytext: true
+ }
+ },
+ { id: "menu_responsiveUI",
+ l10nKey: "responsiveDesignMode",
+ oncommand(event) {
+ let window = event.target.ownerDocument.defaultView;
+ ResponsiveUIManager.toggle(window, window.gBrowser.selectedTab);
+ },
+ key: {
+ id: "responsiveUI",
+ modifiers: isMac ? "accel,alt" : "accel,shift",
+ keytext: true
+ },
+ checkbox: true
+ },
+ { id: "menu_eyedropper",
+ l10nKey: "eyedropper",
+ oncommand(event) {
+ let window = event.target.ownerDocument.defaultView;
+ let target = TargetFactory.forTab(window.gBrowser.selectedTab);
+
+ CommandUtils.createRequisition(target, {
+ environment: CommandUtils.createEnvironment({target})
+ }).then(requisition => {
+ requisition.updateExec("eyedropper --frommenu");
+ }, e => console.error(e));
+ },
+ checkbox: true
+ },
+ { id: "menu_scratchpad",
+ l10nKey: "scratchpad",
+ oncommand() {
+ ScratchpadManager.openScratchpad();
+ },
+ key: {
+ id: "scratchpad",
+ modifiers: "shift"
+ }
+ },
+ { id: "menu_devtools_serviceworkers",
+ l10nKey: "devtoolsServiceWorkers",
+ disabled: true,
+ oncommand(event) {
+ let window = event.target.ownerDocument.defaultView;
+ gDevToolsBrowser.openAboutDebugging(window.gBrowser, "workers");
+ }
+ },
+ { id: "menu_devtools_connect",
+ l10nKey: "devtoolsConnect",
+ disabled: true,
+ oncommand(event) {
+ let window = event.target.ownerDocument.defaultView;
+ gDevToolsBrowser.openConnectScreen(window.gBrowser);
+ }
+ },
+ { separator: true,
+ id: "devToolsEndSeparator"
+ },
+ { id: "getMoreDevtools",
+ l10nKey: "getMoreDevtoolsCmd",
+ oncommand(event) {
+ let window = event.target.ownerDocument.defaultView;
+ window.openUILinkIn("https://addons.mozilla.org/firefox/collections/mozilla/webdeveloper/", "tab");
+ }
+ },
+];
diff --git a/devtools/client/moz.build b/devtools/client/moz.build
new file mode 100644
index 000000000..b63de757c
--- /dev/null
+++ b/devtools/client/moz.build
@@ -0,0 +1,54 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+include('../templates.mozbuild')
+
+DIRS += [
+ 'aboutdebugging',
+ 'animationinspector',
+ 'canvasdebugger',
+ 'commandline',
+ 'debugger',
+ 'dom',
+ 'framework',
+ 'inspector',
+ 'jsonview',
+ 'locales',
+ 'memory',
+ 'netmonitor',
+ 'performance',
+ 'preferences',
+ 'projecteditor',
+ 'responsive.html',
+ 'responsivedesign',
+ 'scratchpad',
+ 'shadereditor',
+ 'shared',
+ 'shims',
+ 'sourceeditor',
+ 'storage',
+ 'styleeditor',
+ 'themes',
+ 'webaudioeditor',
+ 'webconsole',
+ 'webide',
+]
+
+# Shim old theme paths used by DevTools add-ons
+if CONFIG['MOZ_BUILD_APP'] == 'browser':
+ DIRS += ['themes/shims']
+
+EXTRA_COMPONENTS += [
+ 'devtools-startup.js',
+ 'devtools-startup.manifest',
+]
+
+JAR_MANIFESTS += ['jar.mn']
+
+DevToolsModules(
+ 'definitions.js',
+ 'menus.js',
+)
diff --git a/devtools/client/netmonitor/.eslintrc.js b/devtools/client/netmonitor/.eslintrc.js
new file mode 100644
index 000000000..6e8808a3c
--- /dev/null
+++ b/devtools/client/netmonitor/.eslintrc.js
@@ -0,0 +1,15 @@
+"use strict";
+
+module.exports = {
+ // Extend from the devtools eslintrc.
+ "extends": "../../.eslintrc.js",
+
+ "rules": {
+ // The netmonitor is being migrated to HTML and cleaned of
+ // chrome-privileged code, so this rule disallows requiring chrome
+ // code. Some files in the netmonitor disable this rule still. The
+ // goal is to enable the rule globally on all files.
+ /* eslint-disable max-len */
+ "mozilla/reject-some-requires": ["error", "^(chrome|chrome:.*|resource:.*|devtools/server/.*|.*\\.jsm|devtools/shared/platform/(chome|content)/.*)$"],
+ },
+};
diff --git a/devtools/client/netmonitor/actions/filters.js b/devtools/client/netmonitor/actions/filters.js
new file mode 100644
index 000000000..71582546a
--- /dev/null
+++ b/devtools/client/netmonitor/actions/filters.js
@@ -0,0 +1,57 @@
+/* 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 {
+ TOGGLE_FILTER_TYPE,
+ ENABLE_FILTER_TYPE_ONLY,
+ SET_FILTER_TEXT,
+} = require("../constants");
+
+/**
+ * Toggle an existing filter type state.
+ * If type 'all' is specified, all the other filter types are set to false.
+ * Available filter types are defined in filters reducer.
+ *
+ * @param {string} filter - A filter type is going to be updated
+ */
+function toggleFilterType(filter) {
+ return {
+ type: TOGGLE_FILTER_TYPE,
+ filter,
+ };
+}
+
+/**
+ * Enable filter type exclusively.
+ * Except filter type is set to true, all the other filter types are set
+ * to false.
+ * Available filter types are defined in filters reducer.
+ *
+ * @param {string} filter - A filter type is going to be updated
+ */
+function enableFilterTypeOnly(filter) {
+ return {
+ type: ENABLE_FILTER_TYPE_ONLY,
+ filter,
+ };
+}
+
+/**
+ * Set filter text.
+ *
+ * @param {string} url - A filter text is going to be set
+ */
+function setFilterText(url) {
+ return {
+ type: SET_FILTER_TEXT,
+ url,
+ };
+}
+
+module.exports = {
+ toggleFilterType,
+ enableFilterTypeOnly,
+ setFilterText,
+};
diff --git a/devtools/client/netmonitor/actions/index.js b/devtools/client/netmonitor/actions/index.js
new file mode 100644
index 000000000..3f7b0bd2f
--- /dev/null
+++ b/devtools/client/netmonitor/actions/index.js
@@ -0,0 +1,9 @@
+/* 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 filters = require("./filters");
+const sidebar = require("./sidebar");
+
+module.exports = Object.assign({}, filters, sidebar);
diff --git a/devtools/client/netmonitor/actions/moz.build b/devtools/client/netmonitor/actions/moz.build
new file mode 100644
index 000000000..477cafb41
--- /dev/null
+++ b/devtools/client/netmonitor/actions/moz.build
@@ -0,0 +1,10 @@
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'filters.js',
+ 'index.js',
+ 'sidebar.js',
+)
diff --git a/devtools/client/netmonitor/actions/sidebar.js b/devtools/client/netmonitor/actions/sidebar.js
new file mode 100644
index 000000000..7e8dca5c1
--- /dev/null
+++ b/devtools/client/netmonitor/actions/sidebar.js
@@ -0,0 +1,49 @@
+/* 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 {
+ DISABLE_TOGGLE_BUTTON,
+ SHOW_SIDEBAR,
+ TOGGLE_SIDEBAR,
+} = require("../constants");
+
+/**
+ * Change ToggleButton disabled state.
+ *
+ * @param {boolean} disabled - expected button disabled state
+ */
+function disableToggleButton(disabled) {
+ return {
+ type: DISABLE_TOGGLE_BUTTON,
+ disabled: disabled,
+ };
+}
+
+/**
+ * Change sidebar visible state.
+ *
+ * @param {boolean} visible - expected sidebar visible state
+ */
+function showSidebar(visible) {
+ return {
+ type: SHOW_SIDEBAR,
+ visible: visible,
+ };
+}
+
+/**
+ * Toggle to show/hide sidebar.
+ */
+function toggleSidebar() {
+ return {
+ type: TOGGLE_SIDEBAR,
+ };
+}
+
+module.exports = {
+ disableToggleButton,
+ showSidebar,
+ toggleSidebar,
+};
diff --git a/devtools/client/netmonitor/components/filter-buttons.js b/devtools/client/netmonitor/components/filter-buttons.js
new file mode 100644
index 000000000..f24db8c53
--- /dev/null
+++ b/devtools/client/netmonitor/components/filter-buttons.js
@@ -0,0 +1,49 @@
+/* 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 { DOM, PropTypes } = require("devtools/client/shared/vendor/react");
+const { connect } = require("devtools/client/shared/vendor/react-redux");
+const { L10N } = require("../l10n");
+const Actions = require("../actions/index");
+
+const { button, div } = DOM;
+
+function FilterButtons({
+ filterTypes,
+ triggerFilterType,
+}) {
+ const buttons = filterTypes.entrySeq().map(([type, checked]) => {
+ let classList = ["menu-filter-button"];
+ checked && classList.push("checked");
+
+ return button({
+ id: `requests-menu-filter-${type}-button`,
+ className: classList.join(" "),
+ "data-key": type,
+ onClick: triggerFilterType,
+ onKeyDown: triggerFilterType,
+ "aria-pressed": checked,
+ }, L10N.getStr(`netmonitor.toolbar.filter.${type}`));
+ }).toArray();
+
+ return div({ id: "requests-menu-filter-buttons" }, buttons);
+}
+
+FilterButtons.PropTypes = {
+ state: PropTypes.object.isRequired,
+ triggerFilterType: PropTypes.func.iRequired,
+};
+
+module.exports = connect(
+ (state) => ({ filterTypes: state.filters.types }),
+ (dispatch) => ({
+ triggerFilterType: (evt) => {
+ if (evt.type === "keydown" && (evt.key !== "" || evt.key !== "Enter")) {
+ return;
+ }
+ dispatch(Actions.toggleFilterType(evt.target.dataset.key));
+ },
+ })
+)(FilterButtons);
diff --git a/devtools/client/netmonitor/components/moz.build b/devtools/client/netmonitor/components/moz.build
new file mode 100644
index 000000000..47ef7f026
--- /dev/null
+++ b/devtools/client/netmonitor/components/moz.build
@@ -0,0 +1,10 @@
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'filter-buttons.js',
+ 'search-box.js',
+ 'toggle-button.js',
+)
diff --git a/devtools/client/netmonitor/components/search-box.js b/devtools/client/netmonitor/components/search-box.js
new file mode 100644
index 000000000..42400e232
--- /dev/null
+++ b/devtools/client/netmonitor/components/search-box.js
@@ -0,0 +1,24 @@
+/* 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 { connect } = require("devtools/client/shared/vendor/react-redux");
+const SearchBox = require("devtools/client/shared/components/search-box");
+const { L10N } = require("../l10n");
+const Actions = require("../actions/index");
+const { FREETEXT_FILTER_SEARCH_DELAY } = require("../constants");
+
+module.exports = connect(
+ (state) => ({
+ delay: FREETEXT_FILTER_SEARCH_DELAY,
+ keyShortcut: L10N.getStr("netmonitor.toolbar.filterFreetext.key"),
+ placeholder: L10N.getStr("netmonitor.toolbar.filterFreetext.label"),
+ type: "filter",
+ }),
+ (dispatch) => ({
+ onChange: (url) => {
+ dispatch(Actions.setFilterText(url));
+ },
+ })
+)(SearchBox);
diff --git a/devtools/client/netmonitor/components/toggle-button.js b/devtools/client/netmonitor/components/toggle-button.js
new file mode 100644
index 000000000..db546c55d
--- /dev/null
+++ b/devtools/client/netmonitor/components/toggle-button.js
@@ -0,0 +1,69 @@
+/* -*- 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/. */
+/* globals NetMonitorView */
+"use strict";
+
+const { DOM, PropTypes } = require("devtools/client/shared/vendor/react");
+const { connect } = require("devtools/client/shared/vendor/react-redux");
+const { L10N } = require("../l10n");
+const Actions = require("../actions/index");
+
+// Shortcuts
+const { button } = DOM;
+
+/**
+ * Button used to toggle sidebar
+ */
+function ToggleButton({
+ disabled,
+ onToggle,
+ visible,
+}) {
+ let className = ["devtools-button"];
+ if (!visible) {
+ className.push("pane-collapsed");
+ }
+ let titleMsg = visible ? L10N.getStr("collapseDetailsPane") :
+ L10N.getStr("expandDetailsPane");
+
+ return button({
+ id: "details-pane-toggle",
+ className: className.join(" "),
+ title: titleMsg,
+ disabled: disabled,
+ tabIndex: "0",
+ onMouseDown: onToggle,
+ });
+}
+
+ToggleButton.propTypes = {
+ disabled: PropTypes.bool.isRequired,
+ onToggle: PropTypes.func.isRequired,
+ visible: PropTypes.bool.isRequired,
+};
+
+module.exports = connect(
+ (state) => ({
+ disabled: state.sidebar.toggleButtonDisabled,
+ visible: state.sidebar.visible,
+ }),
+ (dispatch) => ({
+ onToggle: () => {
+ dispatch(Actions.toggleSidebar());
+
+ let requestsMenu = NetMonitorView.RequestsMenu;
+ let selectedIndex = requestsMenu.selectedIndex;
+
+ // Make sure there's a selection if the button is pressed, to avoid
+ // showing an empty network details pane.
+ if (selectedIndex == -1 && requestsMenu.itemCount) {
+ requestsMenu.selectedIndex = 0;
+ } else {
+ requestsMenu.selectedIndex = -1;
+ }
+ },
+ })
+)(ToggleButton);
diff --git a/devtools/client/netmonitor/constants.js b/devtools/client/netmonitor/constants.js
new file mode 100644
index 000000000..a540d74b2
--- /dev/null
+++ b/devtools/client/netmonitor/constants.js
@@ -0,0 +1,19 @@
+/* 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 general = {
+ FREETEXT_FILTER_SEARCH_DELAY: 200,
+};
+
+const actionTypes = {
+ TOGGLE_FILTER_TYPE: "TOGGLE_FILTER_TYPE",
+ ENABLE_FILTER_TYPE_ONLY: "ENABLE_FILTER_TYPE_ONLY",
+ TOGGLE_SIDEBAR: "TOGGLE_SIDEBAR",
+ SHOW_SIDEBAR: "SHOW_SIDEBAR",
+ DISABLE_TOGGLE_BUTTON: "DISABLE_TOGGLE_BUTTON",
+ SET_FILTER_TEXT: "SET_FILTER_TEXT",
+};
+
+module.exports = Object.assign({}, general, actionTypes);
diff --git a/devtools/client/netmonitor/custom-request-view.js b/devtools/client/netmonitor/custom-request-view.js
new file mode 100644
index 000000000..3159ffcc7
--- /dev/null
+++ b/devtools/client/netmonitor/custom-request-view.js
@@ -0,0 +1,216 @@
+/* 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/. */
+/* globals window, dumpn, gNetwork, $, EVENTS, NetMonitorView */
+"use strict";
+
+const {Task} = require("devtools/shared/task");
+const {writeHeaderText, getKeyWithEvent} = require("./request-utils");
+
+loader.lazyRequireGetter(this, "NetworkHelper",
+ "devtools/shared/webconsole/network-helper");
+
+/**
+ * Functions handling the custom request view.
+ */
+function CustomRequestView() {
+ dumpn("CustomRequestView was instantiated");
+}
+
+CustomRequestView.prototype = {
+ /**
+ * Initialization function, called when the network monitor is started.
+ */
+ initialize: function () {
+ dumpn("Initializing the CustomRequestView");
+
+ this.updateCustomRequestEvent = getKeyWithEvent(this.onUpdate.bind(this));
+ $("#custom-pane").addEventListener("input",
+ this.updateCustomRequestEvent, false);
+ },
+
+ /**
+ * Destruction function, called when the network monitor is closed.
+ */
+ destroy: function () {
+ dumpn("Destroying the CustomRequestView");
+
+ $("#custom-pane").removeEventListener("input",
+ this.updateCustomRequestEvent, false);
+ },
+
+ /**
+ * Populates this view with the specified data.
+ *
+ * @param object data
+ * The data source (this should be the attachment of a request item).
+ * @return object
+ * Returns a promise that resolves upon population the view.
+ */
+ populate: Task.async(function* (data) {
+ $("#custom-url-value").value = data.url;
+ $("#custom-method-value").value = data.method;
+ this.updateCustomQuery(data.url);
+
+ if (data.requestHeaders) {
+ let headers = data.requestHeaders.headers;
+ $("#custom-headers-value").value = writeHeaderText(headers);
+ }
+ if (data.requestPostData) {
+ let postData = data.requestPostData.postData.text;
+ $("#custom-postdata-value").value = yield gNetwork.getString(postData);
+ }
+
+ window.emit(EVENTS.CUSTOMREQUESTVIEW_POPULATED);
+ }),
+
+ /**
+ * Handle user input in the custom request form.
+ *
+ * @param object field
+ * the field that the user updated.
+ */
+ onUpdate: function (field) {
+ let selectedItem = NetMonitorView.RequestsMenu.selectedItem;
+ let value;
+
+ switch (field) {
+ case "method":
+ value = $("#custom-method-value").value.trim();
+ selectedItem.attachment.method = value;
+ break;
+ case "url":
+ value = $("#custom-url-value").value;
+ this.updateCustomQuery(value);
+ selectedItem.attachment.url = value;
+ break;
+ case "query":
+ let query = $("#custom-query-value").value;
+ this.updateCustomUrl(query);
+ field = "url";
+ value = $("#custom-url-value").value;
+ selectedItem.attachment.url = value;
+ break;
+ case "body":
+ value = $("#custom-postdata-value").value;
+ selectedItem.attachment.requestPostData = { postData: { text: value } };
+ break;
+ case "headers":
+ let headersText = $("#custom-headers-value").value;
+ value = parseHeadersText(headersText);
+ selectedItem.attachment.requestHeaders = { headers: value };
+ break;
+ }
+
+ NetMonitorView.RequestsMenu.updateMenuView(selectedItem, field, value);
+ },
+
+ /**
+ * Update the query string field based on the url.
+ *
+ * @param object url
+ * The URL to extract query string from.
+ */
+ updateCustomQuery: function (url) {
+ let paramsArray = NetworkHelper.parseQueryString(
+ NetworkHelper.nsIURL(url).query);
+
+ if (!paramsArray) {
+ $("#custom-query").hidden = true;
+ return;
+ }
+
+ $("#custom-query").hidden = false;
+ $("#custom-query-value").value = writeQueryText(paramsArray);
+ },
+
+ /**
+ * Update the url based on the query string field.
+ *
+ * @param object queryText
+ * The contents of the query string field.
+ */
+ updateCustomUrl: function (queryText) {
+ let params = parseQueryText(queryText);
+ let queryString = writeQueryString(params);
+
+ let url = $("#custom-url-value").value;
+ let oldQuery = NetworkHelper.nsIURL(url).query;
+ let path = url.replace(oldQuery, queryString);
+
+ $("#custom-url-value").value = path;
+ }
+};
+
+/**
+ * Parse text representation of multiple HTTP headers.
+ *
+ * @param string text
+ * Text of headers
+ * @return array
+ * Array of headers info {name, value}
+ */
+function parseHeadersText(text) {
+ return parseRequestText(text, "\\S+?", ":");
+}
+
+/**
+ * Parse readable text list of a query string.
+ *
+ * @param string text
+ * Text of query string represetation
+ * @return array
+ * Array of query params {name, value}
+ */
+function parseQueryText(text) {
+ return parseRequestText(text, ".+?", "=");
+}
+
+/**
+ * Parse a text representation of a name[divider]value list with
+ * the given name regex and divider character.
+ *
+ * @param string text
+ * Text of list
+ * @return array
+ * Array of headers info {name, value}
+ */
+function parseRequestText(text, namereg, divider) {
+ let regex = new RegExp("(" + namereg + ")\\" + divider + "\\s*(.+)");
+ let pairs = [];
+
+ for (let line of text.split("\n")) {
+ let matches;
+ if (matches = regex.exec(line)) { // eslint-disable-line
+ let [, name, value] = matches;
+ pairs.push({name: name, value: value});
+ }
+ }
+ return pairs;
+}
+
+/**
+ * Write out a list of query params into a chunk of text
+ *
+ * @param array params
+ * Array of query params {name, value}
+ * @return string
+ * List of query params in text format
+ */
+function writeQueryText(params) {
+ return params.map(({name, value}) => name + "=" + value).join("\n");
+}
+
+/**
+ * Write out a list of query params into a query string
+ *
+ * @param array params
+ * Array of query params {name, value}
+ * @return string
+ * Query string that can be appended to a url.
+ */
+function writeQueryString(params) {
+ return params.map(({name, value}) => name + "=" + value).join("&");
+}
+
+exports.CustomRequestView = CustomRequestView;
diff --git a/devtools/client/netmonitor/events.js b/devtools/client/netmonitor/events.js
new file mode 100644
index 000000000..8062a2f8c
--- /dev/null
+++ b/devtools/client/netmonitor/events.js
@@ -0,0 +1,86 @@
+"use strict";
+
+// The panel's window global is an EventEmitter firing the following events:
+const EVENTS = {
+ // When the monitored target begins and finishes navigating.
+ TARGET_WILL_NAVIGATE: "NetMonitor:TargetWillNavigate",
+ TARGET_DID_NAVIGATE: "NetMonitor:TargetNavigate",
+
+ // When a network or timeline event is received.
+ // See https://developer.mozilla.org/docs/Tools/Web_Console/remoting for
+ // more information about what each packet is supposed to deliver.
+ NETWORK_EVENT: "NetMonitor:NetworkEvent",
+ TIMELINE_EVENT: "NetMonitor:TimelineEvent",
+
+ // When a network event is added to the view
+ REQUEST_ADDED: "NetMonitor:RequestAdded",
+
+ // When request headers begin and finish receiving.
+ UPDATING_REQUEST_HEADERS: "NetMonitor:NetworkEventUpdating:RequestHeaders",
+ RECEIVED_REQUEST_HEADERS: "NetMonitor:NetworkEventUpdated:RequestHeaders",
+
+ // When request cookies begin and finish receiving.
+ UPDATING_REQUEST_COOKIES: "NetMonitor:NetworkEventUpdating:RequestCookies",
+ RECEIVED_REQUEST_COOKIES: "NetMonitor:NetworkEventUpdated:RequestCookies",
+
+ // When request post data begins and finishes receiving.
+ UPDATING_REQUEST_POST_DATA: "NetMonitor:NetworkEventUpdating:RequestPostData",
+ RECEIVED_REQUEST_POST_DATA: "NetMonitor:NetworkEventUpdated:RequestPostData",
+
+ // When security information begins and finishes receiving.
+ UPDATING_SECURITY_INFO: "NetMonitor::NetworkEventUpdating:SecurityInfo",
+ RECEIVED_SECURITY_INFO: "NetMonitor::NetworkEventUpdated:SecurityInfo",
+
+ // When response headers begin and finish receiving.
+ UPDATING_RESPONSE_HEADERS: "NetMonitor:NetworkEventUpdating:ResponseHeaders",
+ RECEIVED_RESPONSE_HEADERS: "NetMonitor:NetworkEventUpdated:ResponseHeaders",
+
+ // When response cookies begin and finish receiving.
+ UPDATING_RESPONSE_COOKIES: "NetMonitor:NetworkEventUpdating:ResponseCookies",
+ RECEIVED_RESPONSE_COOKIES: "NetMonitor:NetworkEventUpdated:ResponseCookies",
+
+ // When event timings begin and finish receiving.
+ UPDATING_EVENT_TIMINGS: "NetMonitor:NetworkEventUpdating:EventTimings",
+ RECEIVED_EVENT_TIMINGS: "NetMonitor:NetworkEventUpdated:EventTimings",
+
+ // When response content begins, updates and finishes receiving.
+ STARTED_RECEIVING_RESPONSE: "NetMonitor:NetworkEventUpdating:ResponseStart",
+ UPDATING_RESPONSE_CONTENT: "NetMonitor:NetworkEventUpdating:ResponseContent",
+ RECEIVED_RESPONSE_CONTENT: "NetMonitor:NetworkEventUpdated:ResponseContent",
+
+ // When the request post params are displayed in the UI.
+ REQUEST_POST_PARAMS_DISPLAYED: "NetMonitor:RequestPostParamsAvailable",
+
+ // When the response body is displayed in the UI.
+ RESPONSE_BODY_DISPLAYED: "NetMonitor:ResponseBodyAvailable",
+
+ // When the html response preview is displayed in the UI.
+ RESPONSE_HTML_PREVIEW_DISPLAYED: "NetMonitor:ResponseHtmlPreviewAvailable",
+
+ // When the image response thumbnail is displayed in the UI.
+ RESPONSE_IMAGE_THUMBNAIL_DISPLAYED:
+ "NetMonitor:ResponseImageThumbnailAvailable",
+
+ // When a tab is selected in the NetworkDetailsView and subsequently rendered.
+ TAB_UPDATED: "NetMonitor:TabUpdated",
+
+ // Fired when Sidebar has finished being populated.
+ SIDEBAR_POPULATED: "NetMonitor:SidebarPopulated",
+
+ // Fired when NetworkDetailsView has finished being populated.
+ NETWORKDETAILSVIEW_POPULATED: "NetMonitor:NetworkDetailsViewPopulated",
+
+ // Fired when CustomRequestView has finished being populated.
+ CUSTOMREQUESTVIEW_POPULATED: "NetMonitor:CustomRequestViewPopulated",
+
+ // Fired when charts have been displayed in the PerformanceStatisticsView.
+ PLACEHOLDER_CHARTS_DISPLAYED: "NetMonitor:PlaceholderChartsDisplayed",
+ PRIMED_CACHE_CHART_DISPLAYED: "NetMonitor:PrimedChartsDisplayed",
+ EMPTY_CACHE_CHART_DISPLAYED: "NetMonitor:EmptyChartsDisplayed",
+
+ // Fired once the NetMonitorController establishes a connection to the debug
+ // target.
+ CONNECTED: "connected",
+};
+
+exports.EVENTS = EVENTS;
diff --git a/devtools/client/netmonitor/filter-predicates.js b/devtools/client/netmonitor/filter-predicates.js
new file mode 100644
index 000000000..9c8e49c62
--- /dev/null
+++ b/devtools/client/netmonitor/filter-predicates.js
@@ -0,0 +1,129 @@
+"use strict";
+
+/**
+ * Predicates used when filtering items.
+ *
+ * @param object item
+ * The filtered item.
+ * @return boolean
+ * True if the item should be visible, false otherwise.
+ */
+function all() {
+ return true;
+}
+
+function isHtml({ mimeType }) {
+ return mimeType && mimeType.includes("/html");
+}
+
+function isCss({ mimeType }) {
+ return mimeType && mimeType.includes("/css");
+}
+
+function isJs({ mimeType }) {
+ return mimeType && (
+ mimeType.includes("/ecmascript") ||
+ mimeType.includes("/javascript") ||
+ mimeType.includes("/x-javascript"));
+}
+
+function isXHR(item) {
+ // Show the request it is XHR, except if the request is a WS upgrade
+ return item.isXHR && !isWS(item);
+}
+
+function isFont({ url, mimeType }) {
+ // Fonts are a mess.
+ return (mimeType && (
+ mimeType.includes("font/") ||
+ mimeType.includes("/font"))) ||
+ url.includes(".eot") ||
+ url.includes(".ttf") ||
+ url.includes(".otf") ||
+ url.includes(".woff");
+}
+
+function isImage({ mimeType }) {
+ return mimeType && mimeType.includes("image/");
+}
+
+function isMedia({ mimeType }) {
+ // Not including images.
+ return mimeType && (
+ mimeType.includes("audio/") ||
+ mimeType.includes("video/") ||
+ mimeType.includes("model/"));
+}
+
+function isFlash({ url, mimeType }) {
+ // Flash is a mess.
+ return (mimeType && (
+ mimeType.includes("/x-flv") ||
+ mimeType.includes("/x-shockwave-flash"))) ||
+ url.includes(".swf") ||
+ url.includes(".flv");
+}
+
+function isWS({ requestHeaders, responseHeaders }) {
+ // Detect a websocket upgrade if request has an Upgrade header with value 'websocket'
+ if (!requestHeaders || !Array.isArray(requestHeaders.headers)) {
+ return false;
+ }
+
+ // Find the 'upgrade' header.
+ let upgradeHeader = requestHeaders.headers.find(header => {
+ return (header.name == "Upgrade");
+ });
+
+ // If no header found on request, check response - mainly to get
+ // something we can unit test, as it is impossible to set
+ // the Upgrade header on outgoing XHR as per the spec.
+ if (!upgradeHeader && responseHeaders &&
+ Array.isArray(responseHeaders.headers)) {
+ upgradeHeader = responseHeaders.headers.find(header => {
+ return (header.name == "Upgrade");
+ });
+ }
+
+ // Return false if there is no such header or if its value isn't 'websocket'.
+ if (!upgradeHeader || upgradeHeader.value != "websocket") {
+ return false;
+ }
+
+ return true;
+}
+
+function isOther(item) {
+ let tests = [isHtml, isCss, isJs, isXHR, isFont, isImage, isMedia, isFlash, isWS];
+ return tests.every(is => !is(item));
+}
+
+function isFreetextMatch({ url }, text) {
+ let lowerCaseUrl = url.toLowerCase();
+ let lowerCaseText = text.toLowerCase();
+ let textLength = text.length;
+ // Support negative filtering
+ if (text.startsWith("-") && textLength > 1) {
+ lowerCaseText = lowerCaseText.substring(1, textLength);
+ return !lowerCaseUrl.includes(lowerCaseText);
+ }
+
+ // no text is a positive match
+ return !text || lowerCaseUrl.includes(lowerCaseText);
+}
+
+exports.Filters = {
+ all: all,
+ html: isHtml,
+ css: isCss,
+ js: isJs,
+ xhr: isXHR,
+ fonts: isFont,
+ images: isImage,
+ media: isMedia,
+ flash: isFlash,
+ ws: isWS,
+ other: isOther,
+};
+
+exports.isFreetextMatch = isFreetextMatch;
diff --git a/devtools/client/netmonitor/har/har-automation.js b/devtools/client/netmonitor/har/har-automation.js
new file mode 100644
index 000000000..0885c4f96
--- /dev/null
+++ b/devtools/client/netmonitor/har/har-automation.js
@@ -0,0 +1,273 @@
+/* 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";
+/* eslint-disable mozilla/reject-some-requires */
+const { Ci } = require("chrome");
+const { Class } = require("sdk/core/heritage");
+const { resolve } = require("promise");
+const Services = require("Services");
+
+loader.lazyRequireGetter(this, "HarCollector", "devtools/client/netmonitor/har/har-collector", true);
+loader.lazyRequireGetter(this, "HarExporter", "devtools/client/netmonitor/har/har-exporter", true);
+loader.lazyRequireGetter(this, "HarUtils", "devtools/client/netmonitor/har/har-utils", true);
+
+const prefDomain = "devtools.netmonitor.har.";
+
+// Helper tracer. Should be generic sharable by other modules (bug 1171927)
+const trace = {
+ log: function (...args) {
+ }
+};
+
+/**
+ * This object is responsible for automated HAR export. It listens
+ * for Network activity, collects all HTTP data and triggers HAR
+ * export when the page is loaded.
+ *
+ * The user needs to enable the following preference to make the
+ * auto-export work: devtools.netmonitor.har.enableAutoExportToFile
+ *
+ * HAR files are stored within directory that is specified in this
+ * preference: devtools.netmonitor.har.defaultLogDir
+ *
+ * If the default log directory preference isn't set the following
+ * directory is used by default: <profile>/har/logs
+ */
+var HarAutomation = Class({
+ // Initialization
+
+ initialize: function (toolbox) {
+ this.toolbox = toolbox;
+
+ let target = toolbox.target;
+ target.makeRemote().then(() => {
+ this.startMonitoring(target.client, target.form);
+ });
+ },
+
+ destroy: function () {
+ if (this.collector) {
+ this.collector.stop();
+ }
+
+ if (this.tabWatcher) {
+ this.tabWatcher.disconnect();
+ }
+ },
+
+ // Automation
+
+ startMonitoring: function (client, tabGrip, callback) {
+ if (!client) {
+ return;
+ }
+
+ if (!tabGrip) {
+ return;
+ }
+
+ this.debuggerClient = client;
+ this.tabClient = this.toolbox.target.activeTab;
+ this.webConsoleClient = this.toolbox.target.activeConsole;
+
+ this.tabWatcher = new TabWatcher(this.toolbox, this);
+ this.tabWatcher.connect();
+ },
+
+ pageLoadBegin: function (response) {
+ this.resetCollector();
+ },
+
+ resetCollector: function () {
+ if (this.collector) {
+ this.collector.stop();
+ }
+
+ // A page is about to be loaded, start collecting HTTP
+ // data from events sent from the backend.
+ this.collector = new HarCollector({
+ webConsoleClient: this.webConsoleClient,
+ debuggerClient: this.debuggerClient
+ });
+
+ this.collector.start();
+ },
+
+ /**
+ * A page is done loading, export collected data. Note that
+ * some requests for additional page resources might be pending,
+ * so export all after all has been properly received from the backend.
+ *
+ * This collector still works and collects any consequent HTTP
+ * traffic (e.g. XHRs) happening after the page is loaded and
+ * The additional traffic can be exported by executing
+ * triggerExport on this object.
+ */
+ pageLoadDone: function (response) {
+ trace.log("HarAutomation.pageLoadDone; ", response);
+
+ if (this.collector) {
+ this.collector.waitForHarLoad().then(collector => {
+ return this.autoExport();
+ });
+ }
+ },
+
+ autoExport: function () {
+ let autoExport = Services.prefs.getBoolPref(prefDomain +
+ "enableAutoExportToFile");
+
+ if (!autoExport) {
+ return resolve();
+ }
+
+ // Auto export to file is enabled, so save collected data
+ // into a file and use all the default options.
+ let data = {
+ fileName: Services.prefs.getCharPref(prefDomain + "defaultFileName"),
+ };
+
+ return this.executeExport(data);
+ },
+
+ // Public API
+
+ /**
+ * Export all what is currently collected.
+ */
+ triggerExport: function (data) {
+ if (!data.fileName) {
+ data.fileName = Services.prefs.getCharPref(prefDomain +
+ "defaultFileName");
+ }
+
+ return this.executeExport(data);
+ },
+
+ /**
+ * Clear currently collected data.
+ */
+ clear: function () {
+ this.resetCollector();
+ },
+
+ // HAR Export
+
+ /**
+ * Execute HAR export. This method fetches all data from the
+ * Network panel (asynchronously) and saves it into a file.
+ */
+ executeExport: function (data) {
+ let items = this.collector.getItems();
+ let form = this.toolbox.target.form;
+ let title = form.title || form.url;
+
+ let options = {
+ getString: this.getString.bind(this),
+ view: this,
+ items: items,
+ };
+
+ options.defaultFileName = data.fileName;
+ options.compress = data.compress;
+ options.title = data.title || title;
+ options.id = data.id;
+ options.jsonp = data.jsonp;
+ options.includeResponseBodies = data.includeResponseBodies;
+ options.jsonpCallback = data.jsonpCallback;
+ options.forceExport = data.forceExport;
+
+ trace.log("HarAutomation.executeExport; " + data.fileName, options);
+
+ return HarExporter.fetchHarData(options).then(jsonString => {
+ // Save the HAR file if the file name is provided.
+ if (jsonString && options.defaultFileName) {
+ let file = getDefaultTargetFile(options);
+ if (file) {
+ HarUtils.saveToFile(file, jsonString, options.compress);
+ }
+ }
+
+ return jsonString;
+ });
+ },
+
+ /**
+ * Fetches the full text of a string.
+ */
+ getString: function (stringGrip) {
+ return this.webConsoleClient.getString(stringGrip);
+ },
+});
+
+// Helpers
+
+function TabWatcher(toolbox, listener) {
+ this.target = toolbox.target;
+ this.listener = listener;
+
+ this.onTabNavigated = this.onTabNavigated.bind(this);
+}
+
+TabWatcher.prototype = {
+ // Connection
+
+ connect: function () {
+ this.target.on("navigate", this.onTabNavigated);
+ this.target.on("will-navigate", this.onTabNavigated);
+ },
+
+ disconnect: function () {
+ if (!this.target) {
+ return;
+ }
+
+ this.target.off("navigate", this.onTabNavigated);
+ this.target.off("will-navigate", this.onTabNavigated);
+ },
+
+ // Event Handlers
+
+ /**
+ * Called for each location change in the monitored tab.
+ *
+ * @param string aType
+ * Packet type.
+ * @param object aPacket
+ * Packet received from the server.
+ */
+ onTabNavigated: function (type, packet) {
+ switch (type) {
+ case "will-navigate": {
+ this.listener.pageLoadBegin(packet);
+ break;
+ }
+ case "navigate": {
+ this.listener.pageLoadDone(packet);
+ break;
+ }
+ }
+ },
+};
+
+// Protocol Helpers
+
+/**
+ * Returns target file for exported HAR data.
+ */
+function getDefaultTargetFile(options) {
+ let path = options.defaultLogDir ||
+ Services.prefs.getCharPref("devtools.netmonitor.har.defaultLogDir");
+ let folder = HarUtils.getLocalDirectory(path);
+ let fileName = HarUtils.getHarFileName(options.defaultFileName,
+ options.jsonp, options.compress);
+
+ folder.append(fileName);
+ folder.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("0666", 8));
+
+ return folder;
+}
+
+// Exports from this module
+exports.HarAutomation = HarAutomation;
diff --git a/devtools/client/netmonitor/har/har-builder.js b/devtools/client/netmonitor/har/har-builder.js
new file mode 100644
index 000000000..f28e43016
--- /dev/null
+++ b/devtools/client/netmonitor/har/har-builder.js
@@ -0,0 +1,491 @@
+/* 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 { defer, all } = require("promise");
+const { LocalizationHelper } = require("devtools/shared/l10n");
+const Services = require("Services");
+const appInfo = Services.appinfo;
+const { CurlUtils } = require("devtools/client/shared/curl");
+const { getFormDataSections } = require("devtools/client/netmonitor/request-utils");
+
+loader.lazyRequireGetter(this, "NetworkHelper", "devtools/shared/webconsole/network-helper");
+
+loader.lazyGetter(this, "L10N", () => {
+ return new LocalizationHelper("devtools/client/locales/har.properties");
+});
+
+const HAR_VERSION = "1.1";
+
+/**
+ * This object is responsible for building HAR file. See HAR spec:
+ * https://dvcs.w3.org/hg/webperf/raw-file/tip/specs/HAR/Overview.html
+ * http://www.softwareishard.com/blog/har-12-spec/
+ *
+ * @param {Object} options configuration object
+ *
+ * The following options are supported:
+ *
+ * - items {Array}: List of Network requests to be exported. It is possible
+ * to use directly: NetMonitorView.RequestsMenu.items
+ *
+ * - id {String}: ID of the exported page.
+ *
+ * - title {String}: Title of the exported page.
+ *
+ * - includeResponseBodies {Boolean}: Set to true to include HTTP response
+ * bodies in the result data structure.
+ */
+var HarBuilder = function (options) {
+ this._options = options;
+ this._pageMap = [];
+};
+
+HarBuilder.prototype = {
+ // Public API
+
+ /**
+ * This is the main method used to build the entire result HAR data.
+ * The process is asynchronous since it can involve additional RDP
+ * communication (e.g. resolving long strings).
+ *
+ * @returns {Promise} A promise that resolves to the HAR object when
+ * the entire build process is done.
+ */
+ build: function () {
+ this.promises = [];
+
+ // Build basic structure for data.
+ let log = this.buildLog();
+
+ // Build entries.
+ let items = this._options.items;
+ for (let i = 0; i < items.length; i++) {
+ let file = items[i].attachment;
+ log.entries.push(this.buildEntry(log, file));
+ }
+
+ // Some data needs to be fetched from the backend during the
+ // build process, so wait till all is done.
+ let { resolve, promise } = defer();
+ all(this.promises).then(results => resolve({ log: log }));
+
+ return promise;
+ },
+
+ // Helpers
+
+ buildLog: function () {
+ return {
+ version: HAR_VERSION,
+ creator: {
+ name: appInfo.name,
+ version: appInfo.version
+ },
+ browser: {
+ name: appInfo.name,
+ version: appInfo.version
+ },
+ pages: [],
+ entries: [],
+ };
+ },
+
+ buildPage: function (file) {
+ let page = {};
+
+ // Page start time is set when the first request is processed
+ // (see buildEntry)
+ page.startedDateTime = 0;
+ page.id = "page_" + this._options.id;
+ page.title = this._options.title;
+
+ return page;
+ },
+
+ getPage: function (log, file) {
+ let id = this._options.id;
+ let page = this._pageMap[id];
+ if (page) {
+ return page;
+ }
+
+ this._pageMap[id] = page = this.buildPage(file);
+ log.pages.push(page);
+
+ return page;
+ },
+
+ buildEntry: function (log, file) {
+ let page = this.getPage(log, file);
+
+ let entry = {};
+ entry.pageref = page.id;
+ entry.startedDateTime = dateToJSON(new Date(file.startedMillis));
+ entry.time = file.endedMillis - file.startedMillis;
+
+ entry.request = this.buildRequest(file);
+ entry.response = this.buildResponse(file);
+ entry.cache = this.buildCache(file);
+ entry.timings = file.eventTimings ? file.eventTimings.timings : {};
+
+ if (file.remoteAddress) {
+ entry.serverIPAddress = file.remoteAddress;
+ }
+
+ if (file.remotePort) {
+ entry.connection = file.remotePort + "";
+ }
+
+ // Compute page load start time according to the first request start time.
+ if (!page.startedDateTime) {
+ page.startedDateTime = entry.startedDateTime;
+ page.pageTimings = this.buildPageTimings(page, file);
+ }
+
+ return entry;
+ },
+
+ buildPageTimings: function (page, file) {
+ // Event timing info isn't available
+ let timings = {
+ onContentLoad: -1,
+ onLoad: -1
+ };
+
+ return timings;
+ },
+
+ buildRequest: function (file) {
+ let request = {
+ bodySize: 0
+ };
+
+ request.method = file.method;
+ request.url = file.url;
+ request.httpVersion = file.httpVersion || "";
+
+ request.headers = this.buildHeaders(file.requestHeaders);
+ request.headers = this.appendHeadersPostData(request.headers, file);
+ request.cookies = this.buildCookies(file.requestCookies);
+
+ request.queryString = NetworkHelper.parseQueryString(
+ NetworkHelper.nsIURL(file.url).query) || [];
+
+ request.postData = this.buildPostData(file);
+
+ request.headersSize = file.requestHeaders.headersSize;
+
+ // Set request body size, but make sure the body is fetched
+ // from the backend.
+ if (file.requestPostData) {
+ this.fetchData(file.requestPostData.postData.text).then(value => {
+ request.bodySize = value.length;
+ });
+ }
+
+ return request;
+ },
+
+ /**
+ * Fetch all header values from the backend (if necessary) and
+ * build the result HAR structure.
+ *
+ * @param {Object} input Request or response header object.
+ */
+ buildHeaders: function (input) {
+ if (!input) {
+ return [];
+ }
+
+ return this.buildNameValuePairs(input.headers);
+ },
+
+ appendHeadersPostData: function (input = [], file) {
+ if (!file.requestPostData) {
+ return input;
+ }
+
+ this.fetchData(file.requestPostData.postData.text).then(value => {
+ let multipartHeaders = CurlUtils.getHeadersFromMultipartText(value);
+ for (let header of multipartHeaders) {
+ input.push(header);
+ }
+ });
+
+ return input;
+ },
+
+ buildCookies: function (input) {
+ if (!input) {
+ return [];
+ }
+
+ return this.buildNameValuePairs(input.cookies);
+ },
+
+ buildNameValuePairs: function (entries) {
+ let result = [];
+
+ // HAR requires headers array to be presented, so always
+ // return at least an empty array.
+ if (!entries) {
+ return result;
+ }
+
+ // Make sure header values are fully fetched from the server.
+ entries.forEach(entry => {
+ this.fetchData(entry.value).then(value => {
+ result.push({
+ name: entry.name,
+ value: value
+ });
+ });
+ });
+
+ return result;
+ },
+
+ buildPostData: function (file) {
+ let postData = {
+ mimeType: findValue(file.requestHeaders.headers, "content-type"),
+ params: [],
+ text: ""
+ };
+
+ if (!file.requestPostData) {
+ return postData;
+ }
+
+ if (file.requestPostData.postDataDiscarded) {
+ postData.comment = L10N.getStr("har.requestBodyNotIncluded");
+ return postData;
+ }
+
+ // Load request body from the backend.
+ this.fetchData(file.requestPostData.postData.text).then(postDataText => {
+ postData.text = postDataText;
+
+ // If we are dealing with URL encoded body, parse parameters.
+ let { headers } = file.requestHeaders;
+ if (CurlUtils.isUrlEncodedRequest({ headers, postDataText })) {
+ postData.mimeType = "application/x-www-form-urlencoded";
+
+ // Extract form parameters and produce nice HAR array.
+ getFormDataSections(
+ file.requestHeaders,
+ file.requestHeadersFromUploadStream,
+ file.requestPostData,
+ this._options.getString
+ ).then(formDataSections => {
+ formDataSections.forEach(section => {
+ let paramsArray = NetworkHelper.parseQueryString(section);
+ if (paramsArray) {
+ postData.params = [...postData.params, ...paramsArray];
+ }
+ });
+ });
+ }
+ });
+
+ return postData;
+ },
+
+ buildResponse: function (file) {
+ let response = {
+ status: 0
+ };
+
+ // Arbitrary value if it's aborted to make sure status has a number
+ if (file.status) {
+ response.status = parseInt(file.status, 10);
+ }
+
+ let responseHeaders = file.responseHeaders;
+
+ response.statusText = file.statusText || "";
+ response.httpVersion = file.httpVersion || "";
+
+ response.headers = this.buildHeaders(responseHeaders);
+ response.cookies = this.buildCookies(file.responseCookies);
+ response.content = this.buildContent(file);
+
+ let headers = responseHeaders ? responseHeaders.headers : null;
+ let headersSize = responseHeaders ? responseHeaders.headersSize : -1;
+
+ response.redirectURL = findValue(headers, "Location");
+ response.headersSize = headersSize;
+
+ // 'bodySize' is size of the received response body in bytes.
+ // Set to zero in case of responses coming from the cache (304).
+ // Set to -1 if the info is not available.
+ if (typeof file.transferredSize != "number") {
+ response.bodySize = (response.status == 304) ? 0 : -1;
+ } else {
+ response.bodySize = file.transferredSize;
+ }
+
+ return response;
+ },
+
+ buildContent: function (file) {
+ let content = {
+ mimeType: file.mimeType,
+ size: -1
+ };
+
+ let responseContent = file.responseContent;
+ if (responseContent && responseContent.content) {
+ content.size = responseContent.content.size;
+ content.encoding = responseContent.content.encoding;
+ }
+
+ let includeBodies = this._options.includeResponseBodies;
+ let contentDiscarded = responseContent ?
+ responseContent.contentDiscarded : false;
+
+ // The comment is appended only if the response content
+ // is explicitly discarded.
+ if (!includeBodies || contentDiscarded) {
+ content.comment = L10N.getStr("har.responseBodyNotIncluded");
+ return content;
+ }
+
+ if (responseContent) {
+ let text = responseContent.content.text;
+ this.fetchData(text).then(value => {
+ content.text = value;
+ });
+ }
+
+ return content;
+ },
+
+ buildCache: function (file) {
+ let cache = {};
+
+ if (!file.fromCache) {
+ return cache;
+ }
+
+ // There is no such info yet in the Net panel.
+ // cache.beforeRequest = {};
+
+ if (file.cacheEntry) {
+ cache.afterRequest = this.buildCacheEntry(file.cacheEntry);
+ } else {
+ cache.afterRequest = null;
+ }
+
+ return cache;
+ },
+
+ buildCacheEntry: function (cacheEntry) {
+ let cache = {};
+
+ cache.expires = findValue(cacheEntry, "Expires");
+ cache.lastAccess = findValue(cacheEntry, "Last Fetched");
+ cache.eTag = "";
+ cache.hitCount = findValue(cacheEntry, "Fetch Count");
+
+ return cache;
+ },
+
+ getBlockingEndTime: function (file) {
+ if (file.resolveStarted && file.connectStarted) {
+ return file.resolvingTime;
+ }
+
+ if (file.connectStarted) {
+ return file.connectingTime;
+ }
+
+ if (file.sendStarted) {
+ return file.sendingTime;
+ }
+
+ return (file.sendingTime > file.startTime) ?
+ file.sendingTime : file.waitingForTime;
+ },
+
+ // RDP Helpers
+
+ fetchData: function (string) {
+ let promise = this._options.getString(string).then(value => {
+ return value;
+ });
+
+ // Building HAR is asynchronous and not done till all
+ // collected promises are resolved.
+ this.promises.push(promise);
+
+ return promise;
+ }
+};
+
+// Helpers
+
+/**
+ * Find specified value within an array of name-value pairs
+ * (used for headers, cookies and cache entries)
+ */
+function findValue(arr, name) {
+ if (!arr) {
+ return "";
+ }
+
+ name = name.toLowerCase();
+ let result = arr.find(entry => entry.name.toLowerCase() == name);
+ return result ? result.value : "";
+}
+
+/**
+ * Generate HAR representation of a date.
+ * (YYYY-MM-DDThh:mm:ss.sTZD, e.g. 2009-07-24T19:20:30.45+01:00)
+ * See also HAR Schema: http://janodvarko.cz/har/viewer/
+ *
+ * Note: it would be great if we could utilize Date.toJSON(), but
+ * it doesn't return proper time zone offset.
+ *
+ * An example:
+ * This helper returns: 2015-05-29T16:10:30.424+02:00
+ * Date.toJSON() returns: 2015-05-29T14:10:30.424Z
+ *
+ * @param date {Date} The date object we want to convert.
+ */
+function dateToJSON(date) {
+ function f(n, c) {
+ if (!c) {
+ c = 2;
+ }
+ let s = new String(n);
+ while (s.length < c) {
+ s = "0" + s;
+ }
+ return s;
+ }
+
+ let result = date.getFullYear() + "-" +
+ f(date.getMonth() + 1) + "-" +
+ f(date.getDate()) + "T" +
+ f(date.getHours()) + ":" +
+ f(date.getMinutes()) + ":" +
+ f(date.getSeconds()) + "." +
+ f(date.getMilliseconds(), 3);
+
+ let offset = date.getTimezoneOffset();
+ let positive = offset > 0;
+
+ // Convert to positive number before using Math.floor (see issue 5512)
+ offset = Math.abs(offset);
+ let offsetHours = Math.floor(offset / 60);
+ let offsetMinutes = Math.floor(offset % 60);
+ let prettyOffset = (positive > 0 ? "-" : "+") + f(offsetHours) +
+ ":" + f(offsetMinutes);
+
+ return result + prettyOffset;
+}
+
+// Exports from this module
+exports.HarBuilder = HarBuilder;
diff --git a/devtools/client/netmonitor/har/har-collector.js b/devtools/client/netmonitor/har/har-collector.js
new file mode 100644
index 000000000..e3c510756
--- /dev/null
+++ b/devtools/client/netmonitor/har/har-collector.js
@@ -0,0 +1,462 @@
+/* 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 { defer, all } = require("promise");
+const { makeInfallible } = require("devtools/shared/DevToolsUtils");
+const Services = require("Services");
+
+// Helper tracer. Should be generic sharable by other modules (bug 1171927)
+const trace = {
+ log: function (...args) {
+ }
+};
+
+/**
+ * This object is responsible for collecting data related to all
+ * HTTP requests executed by the page (including inner iframes).
+ */
+function HarCollector(options) {
+ this.webConsoleClient = options.webConsoleClient;
+ this.debuggerClient = options.debuggerClient;
+
+ this.onNetworkEvent = this.onNetworkEvent.bind(this);
+ this.onNetworkEventUpdate = this.onNetworkEventUpdate.bind(this);
+ this.onRequestHeaders = this.onRequestHeaders.bind(this);
+ this.onRequestCookies = this.onRequestCookies.bind(this);
+ this.onRequestPostData = this.onRequestPostData.bind(this);
+ this.onResponseHeaders = this.onResponseHeaders.bind(this);
+ this.onResponseCookies = this.onResponseCookies.bind(this);
+ this.onResponseContent = this.onResponseContent.bind(this);
+ this.onEventTimings = this.onEventTimings.bind(this);
+
+ this.onPageLoadTimeout = this.onPageLoadTimeout.bind(this);
+
+ this.clear();
+}
+
+HarCollector.prototype = {
+ // Connection
+
+ start: function () {
+ this.debuggerClient.addListener("networkEvent", this.onNetworkEvent);
+ this.debuggerClient.addListener("networkEventUpdate",
+ this.onNetworkEventUpdate);
+ },
+
+ stop: function () {
+ this.debuggerClient.removeListener("networkEvent", this.onNetworkEvent);
+ this.debuggerClient.removeListener("networkEventUpdate",
+ this.onNetworkEventUpdate);
+ },
+
+ clear: function () {
+ // Any pending requests events will be ignored (they turn
+ // into zombies, since not present in the files array).
+ this.files = new Map();
+ this.items = [];
+ this.firstRequestStart = -1;
+ this.lastRequestStart = -1;
+ this.requests = [];
+ },
+
+ waitForHarLoad: function () {
+ // There should be yet another timeout e.g.:
+ // 'devtools.netmonitor.har.pageLoadTimeout'
+ // that should force export even if page isn't fully loaded.
+ let deferred = defer();
+ this.waitForResponses().then(() => {
+ trace.log("HarCollector.waitForHarLoad; DONE HAR loaded!");
+ deferred.resolve(this);
+ });
+
+ return deferred.promise;
+ },
+
+ waitForResponses: function () {
+ trace.log("HarCollector.waitForResponses; " + this.requests.length);
+
+ // All requests for additional data must be received to have complete
+ // HTTP info to generate the result HAR file. So, wait for all current
+ // promises. Note that new promises (requests) can be generated during the
+ // process of HTTP data collection.
+ return waitForAll(this.requests).then(() => {
+ // All responses are received from the backend now. We yet need to
+ // wait for a little while to see if a new request appears. If yes,
+ // lets's start gathering HTTP data again. If no, we can declare
+ // the page loaded.
+ // If some new requests appears in the meantime the promise will
+ // be rejected and we need to wait for responses all over again.
+ return this.waitForTimeout().then(() => {
+ // Page loaded!
+ }, () => {
+ trace.log("HarCollector.waitForResponses; NEW requests " +
+ "appeared during page timeout!");
+
+ // New requests executed, let's wait again.
+ return this.waitForResponses();
+ });
+ });
+ },
+
+ // Page Loaded Timeout
+
+ /**
+ * The page is loaded when there are no new requests within given period
+ * of time. The time is set in preferences:
+ * 'devtools.netmonitor.har.pageLoadedTimeout'
+ */
+ waitForTimeout: function () {
+ // The auto-export is not done if the timeout is set to zero (or less).
+ // This is useful in cases where the export is done manually through
+ // API exposed to the content.
+ let timeout = Services.prefs.getIntPref(
+ "devtools.netmonitor.har.pageLoadedTimeout");
+
+ trace.log("HarCollector.waitForTimeout; " + timeout);
+
+ this.pageLoadDeferred = defer();
+
+ if (timeout <= 0) {
+ this.pageLoadDeferred.resolve();
+ return this.pageLoadDeferred.promise;
+ }
+
+ this.pageLoadTimeout = setTimeout(this.onPageLoadTimeout, timeout);
+
+ return this.pageLoadDeferred.promise;
+ },
+
+ onPageLoadTimeout: function () {
+ trace.log("HarCollector.onPageLoadTimeout;");
+
+ // Ha, page has been loaded. Resolve the final timeout promise.
+ this.pageLoadDeferred.resolve();
+ },
+
+ resetPageLoadTimeout: function () {
+ // Remove the current timeout.
+ if (this.pageLoadTimeout) {
+ trace.log("HarCollector.resetPageLoadTimeout;");
+
+ clearTimeout(this.pageLoadTimeout);
+ this.pageLoadTimeout = null;
+ }
+
+ // Reject the current page load promise
+ if (this.pageLoadDeferred) {
+ this.pageLoadDeferred.reject();
+ this.pageLoadDeferred = null;
+ }
+ },
+
+ // Collected Data
+
+ getFile: function (actorId) {
+ return this.files.get(actorId);
+ },
+
+ getItems: function () {
+ return this.items;
+ },
+
+ // Event Handlers
+
+ onNetworkEvent: function (type, packet) {
+ // Skip events from different console actors.
+ if (packet.from != this.webConsoleClient.actor) {
+ return;
+ }
+
+ trace.log("HarCollector.onNetworkEvent; " + type, packet);
+
+ let { actor, startedDateTime, method, url, isXHR } = packet.eventActor;
+ let startTime = Date.parse(startedDateTime);
+
+ if (this.firstRequestStart == -1) {
+ this.firstRequestStart = startTime;
+ }
+
+ if (this.lastRequestEnd < startTime) {
+ this.lastRequestEnd = startTime;
+ }
+
+ let file = this.getFile(actor);
+ if (file) {
+ console.error("HarCollector.onNetworkEvent; ERROR " +
+ "existing file conflict!");
+ return;
+ }
+
+ file = {
+ startedDeltaMillis: startTime - this.firstRequestStart,
+ startedMillis: startTime,
+ method: method,
+ url: url,
+ isXHR: isXHR
+ };
+
+ this.files.set(actor, file);
+
+ // Mimic the Net panel data structure
+ this.items.push({
+ attachment: file
+ });
+ },
+
+ onNetworkEventUpdate: function (type, packet) {
+ let actor = packet.from;
+
+ // Skip events from unknown actors (not in the list).
+ // It can happen when there are zombie requests received after
+ // the target is closed or multiple tabs are attached through
+ // one connection (one DebuggerClient object).
+ let file = this.getFile(packet.from);
+ if (!file) {
+ return;
+ }
+
+ trace.log("HarCollector.onNetworkEventUpdate; " +
+ packet.updateType, packet);
+
+ let includeResponseBodies = Services.prefs.getBoolPref(
+ "devtools.netmonitor.har.includeResponseBodies");
+
+ let request;
+ switch (packet.updateType) {
+ case "requestHeaders":
+ request = this.getData(actor, "getRequestHeaders",
+ this.onRequestHeaders);
+ break;
+ case "requestCookies":
+ request = this.getData(actor, "getRequestCookies",
+ this.onRequestCookies);
+ break;
+ case "requestPostData":
+ request = this.getData(actor, "getRequestPostData",
+ this.onRequestPostData);
+ break;
+ case "responseHeaders":
+ request = this.getData(actor, "getResponseHeaders",
+ this.onResponseHeaders);
+ break;
+ case "responseCookies":
+ request = this.getData(actor, "getResponseCookies",
+ this.onResponseCookies);
+ break;
+ case "responseStart":
+ file.httpVersion = packet.response.httpVersion;
+ file.status = packet.response.status;
+ file.statusText = packet.response.statusText;
+ break;
+ case "responseContent":
+ file.contentSize = packet.contentSize;
+ file.mimeType = packet.mimeType;
+ file.transferredSize = packet.transferredSize;
+
+ if (includeResponseBodies) {
+ request = this.getData(actor, "getResponseContent",
+ this.onResponseContent);
+ }
+ break;
+ case "eventTimings":
+ request = this.getData(actor, "getEventTimings",
+ this.onEventTimings);
+ break;
+ }
+
+ if (request) {
+ this.requests.push(request);
+ }
+
+ this.resetPageLoadTimeout();
+ },
+
+ getData: function (actor, method, callback) {
+ let deferred = defer();
+
+ if (!this.webConsoleClient[method]) {
+ console.error("HarCollector.getData; ERROR " +
+ "Unknown method!");
+ return deferred.resolve();
+ }
+
+ let file = this.getFile(actor);
+
+ trace.log("HarCollector.getData; REQUEST " + method +
+ ", " + file.url, file);
+
+ this.webConsoleClient[method](actor, response => {
+ trace.log("HarCollector.getData; RESPONSE " + method +
+ ", " + file.url, response);
+
+ callback(response);
+ deferred.resolve(response);
+ });
+
+ return deferred.promise;
+ },
+
+ /**
+ * Handles additional information received for a "requestHeaders" packet.
+ *
+ * @param object response
+ * The message received from the server.
+ */
+ onRequestHeaders: function (response) {
+ let file = this.getFile(response.from);
+ file.requestHeaders = response;
+
+ this.getLongHeaders(response.headers);
+ },
+
+ /**
+ * Handles additional information received for a "requestCookies" packet.
+ *
+ * @param object response
+ * The message received from the server.
+ */
+ onRequestCookies: function (response) {
+ let file = this.getFile(response.from);
+ file.requestCookies = response;
+
+ this.getLongHeaders(response.cookies);
+ },
+
+ /**
+ * Handles additional information received for a "requestPostData" packet.
+ *
+ * @param object response
+ * The message received from the server.
+ */
+ onRequestPostData: function (response) {
+ trace.log("HarCollector.onRequestPostData;", response);
+
+ let file = this.getFile(response.from);
+ file.requestPostData = response;
+
+ // Resolve long string
+ let text = response.postData.text;
+ if (typeof text == "object") {
+ this.getString(text).then(value => {
+ response.postData.text = value;
+ });
+ }
+ },
+
+ /**
+ * Handles additional information received for a "responseHeaders" packet.
+ *
+ * @param object response
+ * The message received from the server.
+ */
+ onResponseHeaders: function (response) {
+ let file = this.getFile(response.from);
+ file.responseHeaders = response;
+
+ this.getLongHeaders(response.headers);
+ },
+
+ /**
+ * Handles additional information received for a "responseCookies" packet.
+ *
+ * @param object response
+ * The message received from the server.
+ */
+ onResponseCookies: function (response) {
+ let file = this.getFile(response.from);
+ file.responseCookies = response;
+
+ this.getLongHeaders(response.cookies);
+ },
+
+ /**
+ * Handles additional information received for a "responseContent" packet.
+ *
+ * @param object response
+ * The message received from the server.
+ */
+ onResponseContent: function (response) {
+ let file = this.getFile(response.from);
+ file.responseContent = response;
+
+ // Resolve long string
+ let text = response.content.text;
+ if (typeof text == "object") {
+ this.getString(text).then(value => {
+ response.content.text = value;
+ });
+ }
+ },
+
+ /**
+ * Handles additional information received for a "eventTimings" packet.
+ *
+ * @param object response
+ * The message received from the server.
+ */
+ onEventTimings: function (response) {
+ let file = this.getFile(response.from);
+ file.eventTimings = response;
+
+ let totalTime = response.totalTime;
+ file.totalTime = totalTime;
+ file.endedMillis = file.startedMillis + totalTime;
+ },
+
+ // Helpers
+
+ getLongHeaders: makeInfallible(function (headers) {
+ for (let header of headers) {
+ if (typeof header.value == "object") {
+ this.getString(header.value).then(value => {
+ header.value = value;
+ });
+ }
+ }
+ }),
+
+ /**
+ * Fetches the full text of a string.
+ *
+ * @param object | string stringGrip
+ * The long string grip containing the corresponding actor.
+ * If you pass in a plain string (by accident or because you're lazy),
+ * then a promise of the same string is simply returned.
+ * @return object Promise
+ * A promise that is resolved when the full string contents
+ * are available, or rejected if something goes wrong.
+ */
+ getString: function (stringGrip) {
+ let promise = this.webConsoleClient.getString(stringGrip);
+ this.requests.push(promise);
+ return promise;
+ }
+};
+
+// Helpers
+
+/**
+ * Helper function that allows to wait for array of promises. It is
+ * possible to dynamically add new promises in the provided array.
+ * The function will wait even for the newly added promises.
+ * (this isn't possible with the default Promise.all);
+ */
+function waitForAll(promises) {
+ // Remove all from the original array and get clone of it.
+ let clone = promises.splice(0, promises.length);
+
+ // Wait for all promises in the given array.
+ return all(clone).then(() => {
+ // If there are new promises (in the original array)
+ // to wait for - chain them!
+ if (promises.length) {
+ return waitForAll(promises);
+ }
+ return undefined;
+ });
+}
+
+// Exports from this module
+exports.HarCollector = HarCollector;
diff --git a/devtools/client/netmonitor/har/har-exporter.js b/devtools/client/netmonitor/har/har-exporter.js
new file mode 100644
index 000000000..972cf87dc
--- /dev/null
+++ b/devtools/client/netmonitor/har/har-exporter.js
@@ -0,0 +1,187 @@
+/* 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";
+/* eslint-disable mozilla/reject-some-requires */
+const { Cc, Ci } = require("chrome");
+const Services = require("Services");
+/* eslint-disable mozilla/reject-some-requires */
+const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
+const { resolve } = require("promise");
+const { HarUtils } = require("./har-utils.js");
+const { HarBuilder } = require("./har-builder.js");
+
+XPCOMUtils.defineLazyGetter(this, "clipboardHelper", function () {
+ return Cc["@mozilla.org/widget/clipboardhelper;1"]
+ .getService(Ci.nsIClipboardHelper);
+});
+
+var uid = 1;
+
+// Helper tracer. Should be generic sharable by other modules (bug 1171927)
+const trace = {
+ log: function (...args) {
+ }
+};
+
+/**
+ * This object represents the main public API designed to access
+ * Network export logic. Clients, such as the Network panel itself,
+ * should use this API to export collected HTTP data from the panel.
+ */
+const HarExporter = {
+ // Public API
+
+ /**
+ * Save collected HTTP data from the Network panel into HAR file.
+ *
+ * @param Object options
+ * Configuration object
+ *
+ * The following options are supported:
+ *
+ * - includeResponseBodies {Boolean}: If set to true, HTTP response bodies
+ * are also included in the HAR file (can produce significantly bigger
+ * amount of data).
+ *
+ * - items {Array}: List of Network requests to be exported. It is possible
+ * to use directly: NetMonitorView.RequestsMenu.items
+ *
+ * - jsonp {Boolean}: If set to true the export format is HARP (support
+ * for JSONP syntax).
+ *
+ * - jsonpCallback {String}: Default name of JSONP callback (used for
+ * HARP format).
+ *
+ * - compress {Boolean}: If set to true the final HAR file is zipped.
+ * This represents great disk-space optimization.
+ *
+ * - defaultFileName {String}: Default name of the target HAR file.
+ * The default file name supports formatters, see:
+ * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleFormat
+ *
+ * - defaultLogDir {String}: Default log directory for automated logs.
+ *
+ * - id {String}: ID of the page (used in the HAR file).
+ *
+ * - title {String}: Title of the page (used in the HAR file).
+ *
+ * - forceExport {Boolean}: The result HAR file is created even if
+ * there are no HTTP entries.
+ */
+ save: function (options) {
+ // Set default options related to save operation.
+ options.defaultFileName = Services.prefs.getCharPref(
+ "devtools.netmonitor.har.defaultFileName");
+ options.compress = Services.prefs.getBoolPref(
+ "devtools.netmonitor.har.compress");
+
+ // Get target file for exported data. Bail out, if the user
+ // presses cancel.
+ let file = HarUtils.getTargetFile(options.defaultFileName,
+ options.jsonp, options.compress);
+
+ if (!file) {
+ return resolve();
+ }
+
+ trace.log("HarExporter.save; " + options.defaultFileName, options);
+
+ return this.fetchHarData(options).then(jsonString => {
+ if (!HarUtils.saveToFile(file, jsonString, options.compress)) {
+ let msg = "Failed to save HAR file at: " + options.defaultFileName;
+ console.error(msg);
+ }
+ return jsonString;
+ });
+ },
+
+ /**
+ * Copy HAR string into the clipboard.
+ *
+ * @param Object options
+ * Configuration object, see save() for detailed description.
+ */
+ copy: function (options) {
+ return this.fetchHarData(options).then(jsonString => {
+ clipboardHelper.copyString(jsonString);
+ return jsonString;
+ });
+ },
+
+ // Helpers
+
+ fetchHarData: function (options) {
+ // Generate page ID
+ options.id = options.id || uid++;
+
+ // Set default generic HAR export options.
+ options.jsonp = options.jsonp ||
+ Services.prefs.getBoolPref("devtools.netmonitor.har.jsonp");
+ options.includeResponseBodies = options.includeResponseBodies ||
+ Services.prefs.getBoolPref(
+ "devtools.netmonitor.har.includeResponseBodies");
+ options.jsonpCallback = options.jsonpCallback ||
+ Services.prefs.getCharPref("devtools.netmonitor.har.jsonpCallback");
+ options.forceExport = options.forceExport ||
+ Services.prefs.getBoolPref("devtools.netmonitor.har.forceExport");
+
+ // Build HAR object.
+ return this.buildHarData(options).then(har => {
+ // Do not export an empty HAR file, unless the user
+ // explicitly says so (using the forceExport option).
+ if (!har.log.entries.length && !options.forceExport) {
+ return resolve();
+ }
+
+ let jsonString = this.stringify(har);
+ if (!jsonString) {
+ return resolve();
+ }
+
+ // If JSONP is wanted, wrap the string in a function call
+ if (options.jsonp) {
+ // This callback name is also used in HAR Viewer by default.
+ // http://www.softwareishard.com/har/viewer/
+ let callbackName = options.jsonpCallback || "onInputData";
+ jsonString = callbackName + "(" + jsonString + ");";
+ }
+
+ return jsonString;
+ }).then(null, function onError(err) {
+ console.error(err);
+ });
+ },
+
+ /**
+ * Build HAR data object. This object contains all HTTP data
+ * collected by the Network panel. The process is asynchronous
+ * since it can involve additional RDP communication (e.g. resolving
+ * long strings).
+ */
+ buildHarData: function (options) {
+ // Build HAR object from collected data.
+ let builder = new HarBuilder(options);
+ return builder.build();
+ },
+
+ /**
+ * Build JSON string from the HAR data object.
+ */
+ stringify: function (har) {
+ if (!har) {
+ return null;
+ }
+
+ try {
+ return JSON.stringify(har, null, " ");
+ } catch (err) {
+ console.error(err);
+ return undefined;
+ }
+ },
+};
+
+// Exports from this module
+exports.HarExporter = HarExporter;
diff --git a/devtools/client/netmonitor/har/har-utils.js b/devtools/client/netmonitor/har/har-utils.js
new file mode 100644
index 000000000..aa9bd3811
--- /dev/null
+++ b/devtools/client/netmonitor/har/har-utils.js
@@ -0,0 +1,189 @@
+/* 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";
+/* eslint-disable mozilla/reject-some-requires */
+const { Ci, Cc, CC } = require("chrome");
+/* eslint-disable mozilla/reject-some-requires */
+const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "dirService", function () {
+ return Cc["@mozilla.org/file/directory_service;1"]
+ .getService(Ci.nsIProperties);
+});
+
+XPCOMUtils.defineLazyGetter(this, "ZipWriter", function () {
+ return CC("@mozilla.org/zipwriter;1", "nsIZipWriter");
+});
+
+XPCOMUtils.defineLazyGetter(this, "LocalFile", function () {
+ return new CC("@mozilla.org/file/local;1", "nsILocalFile", "initWithPath");
+});
+
+XPCOMUtils.defineLazyGetter(this, "getMostRecentBrowserWindow", function () {
+ return require("sdk/window/utils").getMostRecentBrowserWindow;
+});
+
+const nsIFilePicker = Ci.nsIFilePicker;
+
+const OPEN_FLAGS = {
+ RDONLY: parseInt("0x01", 16),
+ WRONLY: parseInt("0x02", 16),
+ CREATE_FILE: parseInt("0x08", 16),
+ APPEND: parseInt("0x10", 16),
+ TRUNCATE: parseInt("0x20", 16),
+ EXCL: parseInt("0x80", 16)
+};
+
+/**
+ * Helper API for HAR export features.
+ */
+var HarUtils = {
+ /**
+ * Open File Save As dialog and let the user pick the proper file
+ * location for generated HAR log.
+ */
+ getTargetFile: function (fileName, jsonp, compress) {
+ let browser = getMostRecentBrowserWindow();
+
+ let fp = Cc["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker);
+ fp.init(browser, null, nsIFilePicker.modeSave);
+ fp.appendFilter(
+ "HTTP Archive Files", "*.har; *.harp; *.json; *.jsonp; *.zip");
+ fp.appendFilters(nsIFilePicker.filterAll | nsIFilePicker.filterText);
+ fp.filterIndex = 1;
+
+ fp.defaultString = this.getHarFileName(fileName, jsonp, compress);
+
+ let rv = fp.show();
+ if (rv == nsIFilePicker.returnOK || rv == nsIFilePicker.returnReplace) {
+ return fp.file;
+ }
+
+ return null;
+ },
+
+ getHarFileName: function (defaultFileName, jsonp, compress) {
+ let extension = jsonp ? ".harp" : ".har";
+
+ // Read more about toLocaleFormat & format string.
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleFormat
+ let now = new Date();
+ let name = now.toLocaleFormat(defaultFileName);
+ name = name.replace(/\:/gm, "-", "");
+ name = name.replace(/\//gm, "_", "");
+
+ let fileName = name + extension;
+
+ // Default file extension is zip if compressing is on.
+ if (compress) {
+ fileName += ".zip";
+ }
+
+ return fileName;
+ },
+
+ /**
+ * Save HAR string into a given file. The file might be compressed
+ * if specified in the options.
+ *
+ * @param {File} file Target file where the HAR string (JSON)
+ * should be stored.
+ * @param {String} jsonString HAR data (JSON or JSONP)
+ * @param {Boolean} compress The result file is zipped if set to true.
+ */
+ saveToFile: function (file, jsonString, compress) {
+ let openFlags = OPEN_FLAGS.WRONLY | OPEN_FLAGS.CREATE_FILE |
+ OPEN_FLAGS.TRUNCATE;
+
+ try {
+ let foStream = Cc["@mozilla.org/network/file-output-stream;1"]
+ .createInstance(Ci.nsIFileOutputStream);
+
+ let permFlags = parseInt("0666", 8);
+ foStream.init(file, openFlags, permFlags, 0);
+
+ let convertor = Cc["@mozilla.org/intl/converter-output-stream;1"]
+ .createInstance(Ci.nsIConverterOutputStream);
+ convertor.init(foStream, "UTF-8", 0, 0);
+
+ // The entire jsonString can be huge so, write the data in chunks.
+ let chunkLength = 1024 * 1024;
+ for (let i = 0; i <= jsonString.length; i++) {
+ let data = jsonString.substr(i, chunkLength + 1);
+ if (data) {
+ convertor.writeString(data);
+ }
+
+ i = i + chunkLength;
+ }
+
+ // this closes foStream
+ convertor.close();
+ } catch (err) {
+ console.error(err);
+ return false;
+ }
+
+ // If no compressing then bail out.
+ if (!compress) {
+ return true;
+ }
+
+ // Remember name of the original file, it'll be replaced by a zip file.
+ let originalFilePath = file.path;
+ let originalFileName = file.leafName;
+
+ try {
+ // Rename using unique name (the file is going to be removed).
+ file.moveTo(null, "temp" + (new Date()).getTime() + "temphar");
+
+ // Create compressed file with the original file path name.
+ let zipFile = Cc["@mozilla.org/file/local;1"]
+ .createInstance(Ci.nsILocalFile);
+ zipFile.initWithPath(originalFilePath);
+
+ // The file within the zipped file doesn't use .zip extension.
+ let fileName = originalFileName;
+ if (fileName.indexOf(".zip") == fileName.length - 4) {
+ fileName = fileName.substr(0, fileName.indexOf(".zip"));
+ }
+
+ let zip = new ZipWriter();
+ zip.open(zipFile, openFlags);
+ zip.addEntryFile(fileName, Ci.nsIZipWriter.COMPRESSION_DEFAULT,
+ file, false);
+ zip.close();
+
+ // Remove the original file (now zipped).
+ file.remove(true);
+ return true;
+ } catch (err) {
+ console.error(err);
+
+ // Something went wrong (disk space?) rename the original file back.
+ file.moveTo(null, originalFileName);
+ }
+
+ return false;
+ },
+
+ getLocalDirectory: function (path) {
+ let dir;
+
+ if (!path) {
+ dir = dirService.get("ProfD", Ci.nsILocalFile);
+ dir.append("har");
+ dir.append("logs");
+ } else {
+ dir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
+ dir.initWithPath(path);
+ }
+
+ return dir;
+ },
+};
+
+// Exports from this module
+exports.HarUtils = HarUtils;
diff --git a/devtools/client/netmonitor/har/moz.build b/devtools/client/netmonitor/har/moz.build
new file mode 100644
index 000000000..f6dd4aff8
--- /dev/null
+++ b/devtools/client/netmonitor/har/moz.build
@@ -0,0 +1,15 @@
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'har-automation.js',
+ 'har-builder.js',
+ 'har-collector.js',
+ 'har-exporter.js',
+ 'har-utils.js',
+ 'toolbox-overlay.js',
+)
+
+BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
diff --git a/devtools/client/netmonitor/har/test/.eslintrc.js b/devtools/client/netmonitor/har/test/.eslintrc.js
new file mode 100644
index 000000000..698ae9181
--- /dev/null
+++ b/devtools/client/netmonitor/har/test/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../../../.eslintrc.mochitests.js"
+};
diff --git a/devtools/client/netmonitor/har/test/browser.ini b/devtools/client/netmonitor/har/test/browser.ini
new file mode 100644
index 000000000..14d4f846f
--- /dev/null
+++ b/devtools/client/netmonitor/har/test/browser.ini
@@ -0,0 +1,12 @@
+[DEFAULT]
+tags = devtools
+subsuite = clipboard
+support-files =
+ head.js
+ html_har_post-data-test-page.html
+ !/devtools/client/netmonitor/test/head.js
+ !/devtools/client/netmonitor/test/html_simple-test-page.html
+
+[browser_net_har_copy_all_as_har.js]
+[browser_net_har_post_data.js]
+[browser_net_har_throttle_upload.js]
diff --git a/devtools/client/netmonitor/har/test/browser_net_har_copy_all_as_har.js b/devtools/client/netmonitor/har/test/browser_net_har_copy_all_as_har.js
new file mode 100644
index 000000000..10df7aba6
--- /dev/null
+++ b/devtools/client/netmonitor/har/test/browser_net_har_copy_all_as_har.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Basic tests for exporting Network panel content into HAR format.
+ */
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(SIMPLE_URL);
+
+ info("Starting test... ");
+
+ let { NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, 1);
+ tab.linkedBrowser.reload();
+ yield wait;
+
+ yield RequestsMenu.contextMenu.copyAllAsHar();
+
+ let jsonString = SpecialPowers.getClipboardData("text/unicode");
+ let har = JSON.parse(jsonString);
+
+ // Check out HAR log
+ isnot(har.log, null, "The HAR log must exist");
+ is(har.log.creator.name, "Firefox", "The creator field must be set");
+ is(har.log.browser.name, "Firefox", "The browser field must be set");
+ is(har.log.pages.length, 1, "There must be one page");
+ is(har.log.entries.length, 1, "There must be one request");
+
+ let entry = har.log.entries[0];
+ is(entry.request.method, "GET", "Check the method");
+ is(entry.request.url, SIMPLE_URL, "Check the URL");
+ is(entry.request.headers.length, 9, "Check number of request headers");
+ is(entry.response.status, 200, "Check response status");
+ is(entry.response.statusText, "OK", "Check response status text");
+ is(entry.response.headers.length, 6, "Check number of response headers");
+ is(entry.response.content.mimeType, // eslint-disable-line
+ "text/html", "Check response content type"); // eslint-disable-line
+ isnot(entry.response.content.text, undefined, // eslint-disable-line
+ "Check response body");
+ isnot(entry.timings, undefined, "Check timings");
+
+ return teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/har/test/browser_net_har_post_data.js b/devtools/client/netmonitor/har/test/browser_net_har_post_data.js
new file mode 100644
index 000000000..b3d611ca7
--- /dev/null
+++ b/devtools/client/netmonitor/har/test/browser_net_har_post_data.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests for exporting POST data into HAR format.
+ */
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(
+ HAR_EXAMPLE_URL + "html_har_post-data-test-page.html");
+
+ info("Starting test... ");
+
+ let { NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ // Execute one POST request on the page and wait till its done.
+ let wait = waitForNetworkEvents(monitor, 0, 1);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.executeTest();
+ });
+ yield wait;
+
+ // Copy HAR into the clipboard (asynchronous).
+ let jsonString = yield RequestsMenu.contextMenu.copyAllAsHar();
+ let har = JSON.parse(jsonString);
+
+ // Check out the HAR log.
+ isnot(har.log, null, "The HAR log must exist");
+ is(har.log.pages.length, 1, "There must be one page");
+ is(har.log.entries.length, 1, "There must be one request");
+
+ let entry = har.log.entries[0];
+ is(entry.request.postData.mimeType, "application/json",
+ "Check post data content type");
+ is(entry.request.postData.text, "{'first': 'John', 'last': 'Doe'}",
+ "Check post data payload");
+
+ // Clean up
+ return teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/har/test/browser_net_har_throttle_upload.js b/devtools/client/netmonitor/har/test/browser_net_har_throttle_upload.js
new file mode 100644
index 000000000..c0e424172
--- /dev/null
+++ b/devtools/client/netmonitor/har/test/browser_net_har_throttle_upload.js
@@ -0,0 +1,75 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test timing of upload when throttling.
+
+"use strict";
+
+add_task(function* () {
+ yield throttleUploadTest(true);
+ yield throttleUploadTest(false);
+});
+
+function* throttleUploadTest(actuallyThrottle) {
+ let { tab, monitor } = yield initNetMonitor(
+ HAR_EXAMPLE_URL + "html_har_post-data-test-page.html");
+
+ info("Starting test... (actuallyThrottle = " + actuallyThrottle + ")");
+
+ let { NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ const size = 4096;
+ const uploadSize = actuallyThrottle ? size / 3 : 0;
+
+ const request = {
+ "NetworkMonitor.throttleData": {
+ roundTripTimeMean: 0,
+ roundTripTimeMax: 0,
+ downloadBPSMean: 200000,
+ downloadBPSMax: 200000,
+ uploadBPSMean: uploadSize,
+ uploadBPSMax: uploadSize,
+ },
+ };
+ let client = monitor._controller.webConsoleClient;
+
+ info("sending throttle request");
+ let deferred = promise.defer();
+ client.setPreferences(request, response => {
+ deferred.resolve(response);
+ });
+ yield deferred.promise;
+
+ RequestsMenu.lazyUpdate = false;
+
+ // Execute one POST request on the page and wait till its done.
+ let wait = waitForNetworkEvents(monitor, 0, 1);
+ yield ContentTask.spawn(tab.linkedBrowser, { size }, function* (args) {
+ content.wrappedJSObject.executeTest2(args.size);
+ });
+ yield wait;
+
+ // Copy HAR into the clipboard (asynchronous).
+ let jsonString = yield RequestsMenu.contextMenu.copyAllAsHar();
+ let har = JSON.parse(jsonString);
+
+ // Check out the HAR log.
+ isnot(har.log, null, "The HAR log must exist");
+ is(har.log.pages.length, 1, "There must be one page");
+ is(har.log.entries.length, 1, "There must be one request");
+
+ let entry = har.log.entries[0];
+ is(entry.request.postData.text, "x".repeat(size),
+ "Check post data payload");
+
+ const wasTwoSeconds = entry.timings.send >= 2000;
+ if (actuallyThrottle) {
+ ok(wasTwoSeconds, "upload should have taken more than 2 seconds");
+ } else {
+ ok(!wasTwoSeconds, "upload should not have taken more than 2 seconds");
+ }
+
+ // Clean up
+ yield teardown(monitor);
+}
diff --git a/devtools/client/netmonitor/har/test/head.js b/devtools/client/netmonitor/har/test/head.js
new file mode 100644
index 000000000..22eb87fe6
--- /dev/null
+++ b/devtools/client/netmonitor/har/test/head.js
@@ -0,0 +1,14 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* eslint no-unused-vars: [2, {"vars": "local", "args": "none"}] */
+/* import-globals-from ../../test/head.js */
+
+// Load the NetMonitor head.js file to share its API.
+var netMonitorHead = "chrome://mochitests/content/browser/devtools/client/netmonitor/test/head.js";
+Services.scriptloader.loadSubScript(netMonitorHead, this);
+
+// Directory with HAR related test files.
+const HAR_EXAMPLE_URL = "http://example.com/browser/devtools/client/netmonitor/har/test/";
diff --git a/devtools/client/netmonitor/har/test/html_har_post-data-test-page.html b/devtools/client/netmonitor/har/test/html_har_post-data-test-page.html
new file mode 100644
index 000000000..816dad08e
--- /dev/null
+++ b/devtools/client/netmonitor/har/test/html_har_post-data-test-page.html
@@ -0,0 +1,39 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor Test Page</title>
+ </head>
+
+ <body>
+ <p>HAR POST data test</p>
+
+ <script type="text/javascript">
+ function post(aAddress, aData) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("POST", aAddress, true);
+ xhr.setRequestHeader("Content-Type", "application/json");
+ xhr.send(aData);
+ }
+
+ function executeTest() {
+ var url = "html_har_post-data-test-page.html";
+ var data = "{'first': 'John', 'last': 'Doe'}";
+ post(url, data);
+ }
+
+ function executeTest2(size) {
+ var url = "html_har_post-data-test-page.html";
+ var data = "x".repeat(size);
+ post(url, data);
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/netmonitor/har/toolbox-overlay.js b/devtools/client/netmonitor/har/toolbox-overlay.js
new file mode 100644
index 000000000..4ba5d08a9
--- /dev/null
+++ b/devtools/client/netmonitor/har/toolbox-overlay.js
@@ -0,0 +1,85 @@
+/* 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 Services = require("Services");
+
+loader.lazyRequireGetter(this, "HarAutomation", "devtools/client/netmonitor/har/har-automation", true);
+
+// Map of all created overlays. There is always one instance of
+// an overlay per Toolbox instance (i.e. one per browser tab).
+const overlays = new WeakMap();
+
+/**
+ * This object is responsible for initialization and cleanup for HAR
+ * export feature. It represents an overlay for the Toolbox
+ * following the same life time by listening to its events.
+ *
+ * HAR APIs are designed for integration with tools (such as Selenium)
+ * that automates the browser. Primarily, it is for automating web apps
+ * and getting HAR file for every loaded page.
+ */
+function ToolboxOverlay(toolbox) {
+ this.toolbox = toolbox;
+
+ this.onInit = this.onInit.bind(this);
+ this.onDestroy = this.onDestroy.bind(this);
+
+ this.toolbox.on("ready", this.onInit);
+ this.toolbox.on("destroy", this.onDestroy);
+}
+
+ToolboxOverlay.prototype = {
+ /**
+ * Executed when the toolbox is ready.
+ */
+ onInit: function () {
+ let autoExport = Services.prefs.getBoolPref(
+ "devtools.netmonitor.har.enableAutoExportToFile");
+
+ if (!autoExport) {
+ return;
+ }
+
+ this.initAutomation();
+ },
+
+ /**
+ * Executed when the toolbox is destroyed.
+ */
+ onDestroy: function (eventId, toolbox) {
+ this.destroyAutomation();
+ },
+
+ // Automation
+
+ initAutomation: function () {
+ this.automation = new HarAutomation(this.toolbox);
+ },
+
+ destroyAutomation: function () {
+ if (this.automation) {
+ this.automation.destroy();
+ }
+ },
+};
+
+// Registration
+function register(toolbox) {
+ if (overlays.has(toolbox)) {
+ throw Error("There is an existing overlay for the toolbox");
+ }
+
+ // Instantiate an overlay for the toolbox.
+ let overlay = new ToolboxOverlay(toolbox);
+ overlays.set(toolbox, overlay);
+}
+
+function get(toolbox) {
+ return overlays.get(toolbox);
+}
+
+// Exports from this module
+exports.register = register;
+exports.get = get;
diff --git a/devtools/client/netmonitor/l10n.js b/devtools/client/netmonitor/l10n.js
new file mode 100644
index 000000000..3375483f0
--- /dev/null
+++ b/devtools/client/netmonitor/l10n.js
@@ -0,0 +1,9 @@
+"use strict";
+
+const {LocalizationHelper} = require("devtools/shared/l10n");
+
+const NET_STRINGS_URI = "devtools/client/locales/netmonitor.properties";
+const WEBCONSOLE_STRINGS_URI = "devtools/client/locales/webconsole.properties";
+
+exports.L10N = new LocalizationHelper(NET_STRINGS_URI);
+exports.WEBCONSOLE_L10N = new LocalizationHelper(WEBCONSOLE_STRINGS_URI);
diff --git a/devtools/client/netmonitor/moz.build b/devtools/client/netmonitor/moz.build
new file mode 100644
index 000000000..4b34b093b
--- /dev/null
+++ b/devtools/client/netmonitor/moz.build
@@ -0,0 +1,31 @@
+# vim: set filetype=python:
+# 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/.
+
+DIRS += [
+ 'actions',
+ 'components',
+ 'har',
+ 'reducers',
+ 'selectors'
+]
+
+DevToolsModules(
+ 'constants.js',
+ 'custom-request-view.js',
+ 'events.js',
+ 'filter-predicates.js',
+ 'l10n.js',
+ 'panel.js',
+ 'performance-statistics-view.js',
+ 'prefs.js',
+ 'request-list-context-menu.js',
+ 'request-utils.js',
+ 'requests-menu-view.js',
+ 'sort-predicates.js',
+ 'store.js',
+ 'toolbar-view.js',
+)
+
+BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
diff --git a/devtools/client/netmonitor/netmonitor-controller.js b/devtools/client/netmonitor/netmonitor-controller.js
new file mode 100644
index 000000000..739e174fb
--- /dev/null
+++ b/devtools/client/netmonitor/netmonitor-controller.js
@@ -0,0 +1,816 @@
+/* -*- 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/. */
+/* globals window, document, NetMonitorView, gStore, Actions */
+/* exported loader */
+"use strict";
+
+var { utils: Cu } = Components;
+
+// Descriptions for what this frontend is currently doing.
+const ACTIVITY_TYPE = {
+ // Standing by and handling requests normally.
+ NONE: 0,
+
+ // Forcing the target to reload with cache enabled or disabled.
+ RELOAD: {
+ WITH_CACHE_ENABLED: 1,
+ WITH_CACHE_DISABLED: 2,
+ WITH_CACHE_DEFAULT: 3
+ },
+
+ // Enabling or disabling the cache without triggering a reload.
+ ENABLE_CACHE: 3,
+ DISABLE_CACHE: 4
+};
+
+var BrowserLoaderModule = {};
+Cu.import("resource://devtools/client/shared/browser-loader.js", BrowserLoaderModule);
+var { loader, require } = BrowserLoaderModule.BrowserLoader({
+ baseURI: "resource://devtools/client/netmonitor/",
+ window
+});
+
+const promise = require("promise");
+const Services = require("Services");
+/* eslint-disable mozilla/reject-some-requires */
+const {XPCOMUtils} = require("resource://gre/modules/XPCOMUtils.jsm");
+const EventEmitter = require("devtools/shared/event-emitter");
+const Editor = require("devtools/client/sourceeditor/editor");
+const {TimelineFront} = require("devtools/shared/fronts/timeline");
+const {Task} = require("devtools/shared/task");
+const {Prefs} = require("./prefs");
+const {EVENTS} = require("./events");
+const Actions = require("./actions/index");
+
+XPCOMUtils.defineConstant(this, "EVENTS", EVENTS);
+XPCOMUtils.defineConstant(this, "ACTIVITY_TYPE", ACTIVITY_TYPE);
+XPCOMUtils.defineConstant(this, "Editor", Editor);
+XPCOMUtils.defineConstant(this, "Prefs", Prefs);
+
+XPCOMUtils.defineLazyModuleGetter(this, "Chart",
+ "resource://devtools/client/shared/widgets/Chart.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "clipboardHelper",
+ "@mozilla.org/widget/clipboardhelper;1", "nsIClipboardHelper");
+
+Object.defineProperty(this, "NetworkHelper", {
+ get: function () {
+ return require("devtools/shared/webconsole/network-helper");
+ },
+ configurable: true,
+ enumerable: true
+});
+
+/**
+ * Object defining the network monitor controller components.
+ */
+var NetMonitorController = {
+ /**
+ * Initializes the view and connects the monitor client.
+ *
+ * @return object
+ * A promise that is resolved when the monitor finishes startup.
+ */
+ startupNetMonitor: Task.async(function* () {
+ if (this._startup) {
+ return this._startup.promise;
+ }
+ this._startup = promise.defer();
+ {
+ NetMonitorView.initialize();
+ yield this.connect();
+ }
+ this._startup.resolve();
+ return undefined;
+ }),
+
+ /**
+ * Destroys the view and disconnects the monitor client from the server.
+ *
+ * @return object
+ * A promise that is resolved when the monitor finishes shutdown.
+ */
+ shutdownNetMonitor: Task.async(function* () {
+ if (this._shutdown) {
+ return this._shutdown.promise;
+ }
+ this._shutdown = promise.defer();
+ {
+ NetMonitorView.destroy();
+ this.TargetEventsHandler.disconnect();
+ this.NetworkEventsHandler.disconnect();
+ yield this.disconnect();
+ }
+ this._shutdown.resolve();
+ return undefined;
+ }),
+
+ /**
+ * Initiates remote or chrome network monitoring based on the current target,
+ * wiring event handlers as necessary. Since the TabTarget will have already
+ * started listening to network requests by now, this is largely
+ * netmonitor-specific initialization.
+ *
+ * @return object
+ * A promise that is resolved when the monitor finishes connecting.
+ */
+ connect: Task.async(function* () {
+ if (this._connection) {
+ return this._connection.promise;
+ }
+ this._connection = promise.defer();
+
+ // Some actors like AddonActor or RootActor for chrome debugging
+ // aren't actual tabs.
+ if (this._target.isTabActor) {
+ this.tabClient = this._target.activeTab;
+ }
+
+ let connectTimeline = () => {
+ // Don't start up waiting for timeline markers if the server isn't
+ // recent enough to emit the markers we're interested in.
+ if (this._target.getTrait("documentLoadingMarkers")) {
+ this.timelineFront = new TimelineFront(this._target.client,
+ this._target.form);
+ return this.timelineFront.start({ withDocLoadingEvents: true });
+ }
+ return undefined;
+ };
+
+ this.webConsoleClient = this._target.activeConsole;
+ yield connectTimeline();
+
+ this.TargetEventsHandler.connect();
+ this.NetworkEventsHandler.connect();
+
+ window.emit(EVENTS.CONNECTED);
+
+ this._connection.resolve();
+ this._connected = true;
+ return undefined;
+ }),
+
+ /**
+ * Disconnects the debugger client and removes event handlers as necessary.
+ */
+ disconnect: Task.async(function* () {
+ if (this._disconnection) {
+ return this._disconnection.promise;
+ }
+ this._disconnection = promise.defer();
+
+ // Wait for the connection to finish first.
+ if (!this.isConnected()) {
+ yield this._connection.promise;
+ }
+
+ // When debugging local or a remote instance, the connection is closed by
+ // the RemoteTarget. The webconsole actor is stopped on disconnect.
+ this.tabClient = null;
+ this.webConsoleClient = null;
+
+ // The timeline front wasn't initialized and started if the server wasn't
+ // recent enough to emit the markers we were interested in.
+ if (this._target.getTrait("documentLoadingMarkers")) {
+ yield this.timelineFront.destroy();
+ this.timelineFront = null;
+ }
+
+ this._disconnection.resolve();
+ this._connected = false;
+ return undefined;
+ }),
+
+ /**
+ * Checks whether the netmonitor connection is active.
+ * @return boolean
+ */
+ isConnected: function () {
+ return !!this._connected;
+ },
+
+ /**
+ * Gets the activity currently performed by the frontend.
+ * @return number
+ */
+ getCurrentActivity: function () {
+ return this._currentActivity || ACTIVITY_TYPE.NONE;
+ },
+
+ /**
+ * Triggers a specific "activity" to be performed by the frontend.
+ * This can be, for example, triggering reloads or enabling/disabling cache.
+ *
+ * @param number type
+ * The activity type. See the ACTIVITY_TYPE const.
+ * @return object
+ * A promise resolved once the activity finishes and the frontend
+ * is back into "standby" mode.
+ */
+ triggerActivity: function (type) {
+ // Puts the frontend into "standby" (when there's no particular activity).
+ let standBy = () => {
+ this._currentActivity = ACTIVITY_TYPE.NONE;
+ };
+
+ // Waits for a series of "navigation start" and "navigation stop" events.
+ let waitForNavigation = () => {
+ let deferred = promise.defer();
+ this._target.once("will-navigate", () => {
+ this._target.once("navigate", () => {
+ deferred.resolve();
+ });
+ });
+ return deferred.promise;
+ };
+
+ // Reconfigures the tab, optionally triggering a reload.
+ let reconfigureTab = options => {
+ let deferred = promise.defer();
+ this._target.activeTab.reconfigure(options, deferred.resolve);
+ return deferred.promise;
+ };
+
+ // Reconfigures the tab and waits for the target to finish navigating.
+ let reconfigureTabAndWaitForNavigation = options => {
+ options.performReload = true;
+ let navigationFinished = waitForNavigation();
+ return reconfigureTab(options).then(() => navigationFinished);
+ };
+ if (type == ACTIVITY_TYPE.RELOAD.WITH_CACHE_DEFAULT) {
+ return reconfigureTabAndWaitForNavigation({}).then(standBy);
+ }
+ if (type == ACTIVITY_TYPE.RELOAD.WITH_CACHE_ENABLED) {
+ this._currentActivity = ACTIVITY_TYPE.ENABLE_CACHE;
+ this._target.once("will-navigate", () => {
+ this._currentActivity = type;
+ });
+ return reconfigureTabAndWaitForNavigation({
+ cacheDisabled: false,
+ performReload: true
+ }).then(standBy);
+ }
+ if (type == ACTIVITY_TYPE.RELOAD.WITH_CACHE_DISABLED) {
+ this._currentActivity = ACTIVITY_TYPE.DISABLE_CACHE;
+ this._target.once("will-navigate", () => {
+ this._currentActivity = type;
+ });
+ return reconfigureTabAndWaitForNavigation({
+ cacheDisabled: true,
+ performReload: true
+ }).then(standBy);
+ }
+ if (type == ACTIVITY_TYPE.ENABLE_CACHE) {
+ this._currentActivity = type;
+ return reconfigureTab({
+ cacheDisabled: false,
+ performReload: false
+ }).then(standBy);
+ }
+ if (type == ACTIVITY_TYPE.DISABLE_CACHE) {
+ this._currentActivity = type;
+ return reconfigureTab({
+ cacheDisabled: true,
+ performReload: false
+ }).then(standBy);
+ }
+ this._currentActivity = ACTIVITY_TYPE.NONE;
+ return promise.reject(new Error("Invalid activity type"));
+ },
+
+ /**
+ * Selects the specified request in the waterfall and opens the details view.
+ *
+ * @param string requestId
+ * The actor ID of the request to inspect.
+ * @return object
+ * A promise resolved once the task finishes.
+ */
+ inspectRequest: function (requestId) {
+ // Look for the request in the existing ones or wait for it to appear, if
+ // the network monitor is still loading.
+ let deferred = promise.defer();
+ let request = null;
+ let inspector = function () {
+ let predicate = i => i.value === requestId;
+ request = NetMonitorView.RequestsMenu.getItemForPredicate(predicate);
+ if (!request) {
+ // Reset filters so that the request is visible.
+ gStore.dispatch(Actions.toggleFilterType("all"));
+ request = NetMonitorView.RequestsMenu.getItemForPredicate(predicate);
+ }
+
+ // If the request was found, select it. Otherwise this function will be
+ // called again once new requests arrive.
+ if (request) {
+ window.off(EVENTS.REQUEST_ADDED, inspector);
+ NetMonitorView.RequestsMenu.selectedItem = request;
+ deferred.resolve();
+ }
+ };
+
+ inspector();
+ if (!request) {
+ window.on(EVENTS.REQUEST_ADDED, inspector);
+ }
+ return deferred.promise;
+ },
+
+ /**
+ * Getter that tells if the server supports sending custom network requests.
+ * @type boolean
+ */
+ get supportsCustomRequest() {
+ return this.webConsoleClient &&
+ (this.webConsoleClient.traits.customNetworkRequest ||
+ !this._target.isApp);
+ },
+
+ /**
+ * Getter that tells if the server includes the transferred (compressed /
+ * encoded) response size.
+ * @type boolean
+ */
+ get supportsTransferredResponseSize() {
+ return this.webConsoleClient &&
+ this.webConsoleClient.traits.transferredResponseSize;
+ },
+
+ /**
+ * Getter that tells if the server can do network performance statistics.
+ * @type boolean
+ */
+ get supportsPerfStats() {
+ return this.tabClient &&
+ (this.tabClient.traits.reconfigure || !this._target.isApp);
+ },
+
+ /**
+ * Open a given source in Debugger
+ */
+ viewSourceInDebugger(sourceURL, sourceLine) {
+ return this._toolbox.viewSourceInDebugger(sourceURL, sourceLine);
+ }
+};
+
+/**
+ * Functions handling target-related lifetime events.
+ */
+function TargetEventsHandler() {
+ this._onTabNavigated = this._onTabNavigated.bind(this);
+ this._onTabDetached = this._onTabDetached.bind(this);
+}
+
+TargetEventsHandler.prototype = {
+ get target() {
+ return NetMonitorController._target;
+ },
+
+ /**
+ * Listen for events emitted by the current tab target.
+ */
+ connect: function () {
+ dumpn("TargetEventsHandler is connecting...");
+ this.target.on("close", this._onTabDetached);
+ this.target.on("navigate", this._onTabNavigated);
+ this.target.on("will-navigate", this._onTabNavigated);
+ },
+
+ /**
+ * Remove events emitted by the current tab target.
+ */
+ disconnect: function () {
+ if (!this.target) {
+ return;
+ }
+ dumpn("TargetEventsHandler is disconnecting...");
+ this.target.off("close", this._onTabDetached);
+ this.target.off("navigate", this._onTabNavigated);
+ this.target.off("will-navigate", this._onTabNavigated);
+ },
+
+ /**
+ * Called for each location change in the monitored tab.
+ *
+ * @param string type
+ * Packet type.
+ * @param object packet
+ * Packet received from the server.
+ */
+ _onTabNavigated: function (type, packet) {
+ switch (type) {
+ case "will-navigate": {
+ // Reset UI.
+ if (!Services.prefs.getBoolPref("devtools.webconsole.persistlog")) {
+ NetMonitorView.RequestsMenu.reset();
+ NetMonitorView.Sidebar.toggle(false);
+ }
+ // Switch to the default network traffic inspector view.
+ if (NetMonitorController.getCurrentActivity() == ACTIVITY_TYPE.NONE) {
+ NetMonitorView.showNetworkInspectorView();
+ }
+ // Clear any accumulated markers.
+ NetMonitorController.NetworkEventsHandler.clearMarkers();
+
+ window.emit(EVENTS.TARGET_WILL_NAVIGATE);
+ break;
+ }
+ case "navigate": {
+ window.emit(EVENTS.TARGET_DID_NAVIGATE);
+ break;
+ }
+ }
+ },
+
+ /**
+ * Called when the monitored tab is closed.
+ */
+ _onTabDetached: function () {
+ NetMonitorController.shutdownNetMonitor();
+ }
+};
+
+/**
+ * Functions handling target network events.
+ */
+function NetworkEventsHandler() {
+ this._markers = [];
+
+ this._onNetworkEvent = this._onNetworkEvent.bind(this);
+ this._onNetworkEventUpdate = this._onNetworkEventUpdate.bind(this);
+ this._onDocLoadingMarker = this._onDocLoadingMarker.bind(this);
+ this._onRequestHeaders = this._onRequestHeaders.bind(this);
+ this._onRequestCookies = this._onRequestCookies.bind(this);
+ this._onRequestPostData = this._onRequestPostData.bind(this);
+ this._onResponseHeaders = this._onResponseHeaders.bind(this);
+ this._onResponseCookies = this._onResponseCookies.bind(this);
+ this._onResponseContent = this._onResponseContent.bind(this);
+ this._onEventTimings = this._onEventTimings.bind(this);
+}
+
+NetworkEventsHandler.prototype = {
+ get client() {
+ return NetMonitorController._target.client;
+ },
+
+ get webConsoleClient() {
+ return NetMonitorController.webConsoleClient;
+ },
+
+ get timelineFront() {
+ return NetMonitorController.timelineFront;
+ },
+
+ get firstDocumentDOMContentLoadedTimestamp() {
+ let marker = this._markers.filter(e => {
+ return e.name == "document::DOMContentLoaded";
+ })[0];
+
+ return marker ? marker.unixTime / 1000 : -1;
+ },
+
+ get firstDocumentLoadTimestamp() {
+ let marker = this._markers.filter(e => e.name == "document::Load")[0];
+ return marker ? marker.unixTime / 1000 : -1;
+ },
+
+ /**
+ * Connect to the current target client.
+ */
+ connect: function () {
+ dumpn("NetworkEventsHandler is connecting...");
+ this.webConsoleClient.on("networkEvent", this._onNetworkEvent);
+ this.webConsoleClient.on("networkEventUpdate", this._onNetworkEventUpdate);
+
+ if (this.timelineFront) {
+ this.timelineFront.on("doc-loading", this._onDocLoadingMarker);
+ }
+
+ this._displayCachedEvents();
+ },
+
+ /**
+ * Disconnect from the client.
+ */
+ disconnect: function () {
+ if (!this.client) {
+ return;
+ }
+ dumpn("NetworkEventsHandler is disconnecting...");
+ this.webConsoleClient.off("networkEvent", this._onNetworkEvent);
+ this.webConsoleClient.off("networkEventUpdate", this._onNetworkEventUpdate);
+
+ if (this.timelineFront) {
+ this.timelineFront.off("doc-loading", this._onDocLoadingMarker);
+ }
+ },
+
+ /**
+ * Display any network events already in the cache.
+ */
+ _displayCachedEvents: function () {
+ for (let cachedEvent of this.webConsoleClient.getNetworkEvents()) {
+ // First add the request to the timeline.
+ this._onNetworkEvent("networkEvent", cachedEvent);
+ // Then replay any updates already received.
+ for (let update of cachedEvent.updates) {
+ this._onNetworkEventUpdate("networkEventUpdate", {
+ packet: {
+ updateType: update
+ },
+ networkInfo: cachedEvent
+ });
+ }
+ }
+ },
+
+ /**
+ * The "DOMContentLoaded" and "Load" events sent by the timeline actor.
+ * @param object marker
+ */
+ _onDocLoadingMarker: function (marker) {
+ window.emit(EVENTS.TIMELINE_EVENT, marker);
+ this._markers.push(marker);
+ },
+
+ /**
+ * The "networkEvent" message type handler.
+ *
+ * @param string type
+ * Message type.
+ * @param object networkInfo
+ * The network request information.
+ */
+ _onNetworkEvent: function (type, networkInfo) {
+ let { actor,
+ startedDateTime,
+ request: { method, url },
+ isXHR,
+ cause,
+ fromCache,
+ fromServiceWorker
+ } = networkInfo;
+
+ NetMonitorView.RequestsMenu.addRequest(
+ actor, startedDateTime, method, url, isXHR, cause, fromCache,
+ fromServiceWorker
+ );
+ window.emit(EVENTS.NETWORK_EVENT, actor);
+ },
+
+ /**
+ * The "networkEventUpdate" message type handler.
+ *
+ * @param string type
+ * Message type.
+ * @param object packet
+ * The message received from the server.
+ * @param object networkInfo
+ * The network request information.
+ */
+ _onNetworkEventUpdate: function (type, { packet, networkInfo }) {
+ let { actor } = networkInfo;
+
+ switch (packet.updateType) {
+ case "requestHeaders":
+ this.webConsoleClient.getRequestHeaders(actor, this._onRequestHeaders);
+ window.emit(EVENTS.UPDATING_REQUEST_HEADERS, actor);
+ break;
+ case "requestCookies":
+ this.webConsoleClient.getRequestCookies(actor, this._onRequestCookies);
+ window.emit(EVENTS.UPDATING_REQUEST_COOKIES, actor);
+ break;
+ case "requestPostData":
+ this.webConsoleClient.getRequestPostData(actor,
+ this._onRequestPostData);
+ window.emit(EVENTS.UPDATING_REQUEST_POST_DATA, actor);
+ break;
+ case "securityInfo":
+ NetMonitorView.RequestsMenu.updateRequest(actor, {
+ securityState: networkInfo.securityInfo,
+ });
+ this.webConsoleClient.getSecurityInfo(actor, this._onSecurityInfo);
+ window.emit(EVENTS.UPDATING_SECURITY_INFO, actor);
+ break;
+ case "responseHeaders":
+ this.webConsoleClient.getResponseHeaders(actor,
+ this._onResponseHeaders);
+ window.emit(EVENTS.UPDATING_RESPONSE_HEADERS, actor);
+ break;
+ case "responseCookies":
+ this.webConsoleClient.getResponseCookies(actor,
+ this._onResponseCookies);
+ window.emit(EVENTS.UPDATING_RESPONSE_COOKIES, actor);
+ break;
+ case "responseStart":
+ NetMonitorView.RequestsMenu.updateRequest(actor, {
+ httpVersion: networkInfo.response.httpVersion,
+ remoteAddress: networkInfo.response.remoteAddress,
+ remotePort: networkInfo.response.remotePort,
+ status: networkInfo.response.status,
+ statusText: networkInfo.response.statusText,
+ headersSize: networkInfo.response.headersSize
+ });
+ window.emit(EVENTS.STARTED_RECEIVING_RESPONSE, actor);
+ break;
+ case "responseContent":
+ NetMonitorView.RequestsMenu.updateRequest(actor, {
+ contentSize: networkInfo.response.bodySize,
+ transferredSize: networkInfo.response.transferredSize,
+ mimeType: networkInfo.response.content.mimeType
+ });
+ this.webConsoleClient.getResponseContent(actor,
+ this._onResponseContent);
+ window.emit(EVENTS.UPDATING_RESPONSE_CONTENT, actor);
+ break;
+ case "eventTimings":
+ NetMonitorView.RequestsMenu.updateRequest(actor, {
+ totalTime: networkInfo.totalTime
+ });
+ this.webConsoleClient.getEventTimings(actor, this._onEventTimings);
+ window.emit(EVENTS.UPDATING_EVENT_TIMINGS, actor);
+ break;
+ }
+ },
+
+ /**
+ * Handles additional information received for a "requestHeaders" packet.
+ *
+ * @param object response
+ * The message received from the server.
+ */
+ _onRequestHeaders: function (response) {
+ NetMonitorView.RequestsMenu.updateRequest(response.from, {
+ requestHeaders: response
+ }, () => {
+ window.emit(EVENTS.RECEIVED_REQUEST_HEADERS, response.from);
+ });
+ },
+
+ /**
+ * Handles additional information received for a "requestCookies" packet.
+ *
+ * @param object response
+ * The message received from the server.
+ */
+ _onRequestCookies: function (response) {
+ NetMonitorView.RequestsMenu.updateRequest(response.from, {
+ requestCookies: response
+ }, () => {
+ window.emit(EVENTS.RECEIVED_REQUEST_COOKIES, response.from);
+ });
+ },
+
+ /**
+ * Handles additional information received for a "requestPostData" packet.
+ *
+ * @param object response
+ * The message received from the server.
+ */
+ _onRequestPostData: function (response) {
+ NetMonitorView.RequestsMenu.updateRequest(response.from, {
+ requestPostData: response
+ }, () => {
+ window.emit(EVENTS.RECEIVED_REQUEST_POST_DATA, response.from);
+ });
+ },
+
+ /**
+ * Handles additional information received for a "securityInfo" packet.
+ *
+ * @param object response
+ * The message received from the server.
+ */
+ _onSecurityInfo: function (response) {
+ NetMonitorView.RequestsMenu.updateRequest(response.from, {
+ securityInfo: response.securityInfo
+ }, () => {
+ window.emit(EVENTS.RECEIVED_SECURITY_INFO, response.from);
+ });
+ },
+
+ /**
+ * Handles additional information received for a "responseHeaders" packet.
+ *
+ * @param object response
+ * The message received from the server.
+ */
+ _onResponseHeaders: function (response) {
+ NetMonitorView.RequestsMenu.updateRequest(response.from, {
+ responseHeaders: response
+ }, () => {
+ window.emit(EVENTS.RECEIVED_RESPONSE_HEADERS, response.from);
+ });
+ },
+
+ /**
+ * Handles additional information received for a "responseCookies" packet.
+ *
+ * @param object response
+ * The message received from the server.
+ */
+ _onResponseCookies: function (response) {
+ NetMonitorView.RequestsMenu.updateRequest(response.from, {
+ responseCookies: response
+ }, () => {
+ window.emit(EVENTS.RECEIVED_RESPONSE_COOKIES, response.from);
+ });
+ },
+
+ /**
+ * Handles additional information received for a "responseContent" packet.
+ *
+ * @param object response
+ * The message received from the server.
+ */
+ _onResponseContent: function (response) {
+ NetMonitorView.RequestsMenu.updateRequest(response.from, {
+ responseContent: response
+ }, () => {
+ window.emit(EVENTS.RECEIVED_RESPONSE_CONTENT, response.from);
+ });
+ },
+
+ /**
+ * Handles additional information received for a "eventTimings" packet.
+ *
+ * @param object response
+ * The message received from the server.
+ */
+ _onEventTimings: function (response) {
+ NetMonitorView.RequestsMenu.updateRequest(response.from, {
+ eventTimings: response
+ }, () => {
+ window.emit(EVENTS.RECEIVED_EVENT_TIMINGS, response.from);
+ });
+ },
+
+ /**
+ * Clears all accumulated markers.
+ */
+ clearMarkers: function () {
+ this._markers.length = 0;
+ },
+
+ /**
+ * Fetches the full text of a LongString.
+ *
+ * @param object | string stringGrip
+ * The long string grip containing the corresponding actor.
+ * If you pass in a plain string (by accident or because you're lazy),
+ * then a promise of the same string is simply returned.
+ * @return object Promise
+ * A promise that is resolved when the full string contents
+ * are available, or rejected if something goes wrong.
+ */
+ getString: function (stringGrip) {
+ return this.webConsoleClient.getString(stringGrip);
+ }
+};
+
+/**
+ * Returns true if this is document is in RTL mode.
+ * @return boolean
+ */
+XPCOMUtils.defineLazyGetter(window, "isRTL", function () {
+ return window.getComputedStyle(document.documentElement, null)
+ .direction == "rtl";
+});
+
+/**
+ * Convenient way of emitting events from the panel window.
+ */
+EventEmitter.decorate(this);
+
+/**
+ * Preliminary setup for the NetMonitorController object.
+ */
+NetMonitorController.TargetEventsHandler = new TargetEventsHandler();
+NetMonitorController.NetworkEventsHandler = new NetworkEventsHandler();
+
+/**
+ * Export some properties to the global scope for easier access.
+ */
+Object.defineProperties(window, {
+ "gNetwork": {
+ get: function () {
+ return NetMonitorController.NetworkEventsHandler;
+ },
+ configurable: true
+ }
+});
+
+/**
+ * Helper method for debugging.
+ * @param string
+ */
+function dumpn(str) {
+ if (wantLogging) {
+ dump("NET-FRONTEND: " + str + "\n");
+ }
+}
+
+var wantLogging = Services.prefs.getBoolPref("devtools.debugger.log");
diff --git a/devtools/client/netmonitor/netmonitor-view.js b/devtools/client/netmonitor/netmonitor-view.js
new file mode 100644
index 000000000..68470f7a9
--- /dev/null
+++ b/devtools/client/netmonitor/netmonitor-view.js
@@ -0,0 +1,1230 @@
+/* -*- 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/. */
+/* import-globals-from ./netmonitor-controller.js */
+/* globals Prefs, gNetwork, setInterval, setTimeout, clearInterval, clearTimeout, btoa */
+/* exported $, $all */
+"use strict";
+
+XPCOMUtils.defineLazyGetter(this, "NetworkHelper", function () {
+ return require("devtools/shared/webconsole/network-helper");
+});
+
+/* eslint-disable mozilla/reject-some-requires */
+const {VariablesView} = require("resource://devtools/client/shared/widgets/VariablesView.jsm");
+/* eslint-disable mozilla/reject-some-requires */
+const {VariablesViewController} = require("resource://devtools/client/shared/widgets/VariablesViewController.jsm");
+const {ToolSidebar} = require("devtools/client/framework/sidebar");
+const {testing: isTesting} = require("devtools/shared/flags");
+const {ViewHelpers, Heritage} = require("devtools/client/shared/widgets/view-helpers");
+const {Filters} = require("./filter-predicates");
+const {getFormDataSections,
+ formDataURI,
+ getUriHostPort} = require("./request-utils");
+const {L10N} = require("./l10n");
+const {RequestsMenuView} = require("./requests-menu-view");
+const {CustomRequestView} = require("./custom-request-view");
+const {ToolbarView} = require("./toolbar-view");
+const {configureStore} = require("./store");
+const {PerformanceStatisticsView} = require("./performance-statistics-view");
+
+// Initialize the global redux variables
+var gStore = configureStore();
+
+// ms
+const WDA_DEFAULT_VERIFY_INTERVAL = 50;
+
+// Use longer timeout during testing as the tests need this process to succeed
+// and two seconds is quite short on slow debug builds. The timeout here should
+// be at least equal to the general mochitest timeout of 45 seconds so that this
+// never gets hit during testing.
+// ms
+const WDA_DEFAULT_GIVE_UP_TIMEOUT = isTesting ? 45000 : 2000;
+
+// 100 KB in bytes
+const SOURCE_SYNTAX_HIGHLIGHT_MAX_FILE_SIZE = 102400;
+const HEADERS_SIZE_DECIMALS = 3;
+const CONTENT_MIME_TYPE_MAPPINGS = {
+ "/ecmascript": Editor.modes.js,
+ "/javascript": Editor.modes.js,
+ "/x-javascript": Editor.modes.js,
+ "/html": Editor.modes.html,
+ "/xhtml": Editor.modes.html,
+ "/xml": Editor.modes.html,
+ "/atom": Editor.modes.html,
+ "/soap": Editor.modes.html,
+ "/vnd.mpeg.dash.mpd": Editor.modes.html,
+ "/rdf": Editor.modes.css,
+ "/rss": Editor.modes.css,
+ "/css": Editor.modes.css
+};
+
+const DEFAULT_EDITOR_CONFIG = {
+ mode: Editor.modes.text,
+ readOnly: true,
+ lineNumbers: true
+};
+const GENERIC_VARIABLES_VIEW_SETTINGS = {
+ lazyEmpty: true,
+ // ms
+ lazyEmptyDelay: 10,
+ searchEnabled: true,
+ editableValueTooltip: "",
+ editableNameTooltip: "",
+ preventDisableOnChange: true,
+ preventDescriptorModifiers: true,
+ eval: () => {}
+};
+
+/**
+ * Object defining the network monitor view components.
+ */
+var NetMonitorView = {
+ /**
+ * Initializes the network monitor view.
+ */
+ initialize: function () {
+ this._initializePanes();
+
+ this.Toolbar.initialize(gStore);
+ this.RequestsMenu.initialize(gStore);
+ this.NetworkDetails.initialize();
+ this.CustomRequest.initialize();
+ this.PerformanceStatistics.initialize(gStore);
+ },
+
+ /**
+ * Destroys the network monitor view.
+ */
+ destroy: function () {
+ this._isDestroyed = true;
+ this.Toolbar.destroy();
+ this.RequestsMenu.destroy();
+ this.NetworkDetails.destroy();
+ this.CustomRequest.destroy();
+
+ this._destroyPanes();
+ },
+
+ /**
+ * Initializes the UI for all the displayed panes.
+ */
+ _initializePanes: function () {
+ dumpn("Initializing the NetMonitorView panes");
+
+ this._body = $("#body");
+ this._detailsPane = $("#details-pane");
+
+ this._detailsPane.setAttribute("width", Prefs.networkDetailsWidth);
+ this._detailsPane.setAttribute("height", Prefs.networkDetailsHeight);
+ this.toggleDetailsPane({ visible: false });
+
+ // Disable the performance statistics mode.
+ if (!Prefs.statistics) {
+ $("#request-menu-context-perf").hidden = true;
+ $("#notice-perf-message").hidden = true;
+ $("#requests-menu-network-summary-button").hidden = true;
+ }
+ },
+
+ /**
+ * Destroys the UI for all the displayed panes.
+ */
+ _destroyPanes: Task.async(function* () {
+ dumpn("Destroying the NetMonitorView panes");
+
+ Prefs.networkDetailsWidth = this._detailsPane.getAttribute("width");
+ Prefs.networkDetailsHeight = this._detailsPane.getAttribute("height");
+
+ this._detailsPane = null;
+
+ for (let p of this._editorPromises.values()) {
+ let editor = yield p;
+ editor.destroy();
+ }
+ }),
+
+ /**
+ * Gets the visibility state of the network details pane.
+ * @return boolean
+ */
+ get detailsPaneHidden() {
+ return this._detailsPane.classList.contains("pane-collapsed");
+ },
+
+ /**
+ * Sets the network details pane hidden or visible.
+ *
+ * @param object flags
+ * An object containing some of the following properties:
+ * - visible: true if the pane should be shown, false to hide
+ * - animated: true to display an animation on toggle
+ * - delayed: true to wait a few cycles before toggle
+ * - callback: a function to invoke when the toggle finishes
+ * @param number tabIndex [optional]
+ * The index of the intended selected tab in the details pane.
+ */
+ toggleDetailsPane: function (flags, tabIndex) {
+ ViewHelpers.togglePane(flags, this._detailsPane);
+
+ if (flags.visible) {
+ this._body.classList.remove("pane-collapsed");
+ gStore.dispatch(Actions.showSidebar(true));
+ } else {
+ this._body.classList.add("pane-collapsed");
+ gStore.dispatch(Actions.showSidebar(false));
+ }
+
+ if (tabIndex !== undefined) {
+ $("#event-details-pane").selectedIndex = tabIndex;
+ }
+ },
+
+ /**
+ * Gets the current mode for this tool.
+ * @return string (e.g, "network-inspector-view" or "network-statistics-view")
+ */
+ get currentFrontendMode() {
+ // The getter may be called from a timeout after the panel is destroyed.
+ if (!this._body.selectedPanel) {
+ return null;
+ }
+ return this._body.selectedPanel.id;
+ },
+
+ /**
+ * Toggles between the frontend view modes ("Inspector" vs. "Statistics").
+ */
+ toggleFrontendMode: function () {
+ if (this.currentFrontendMode != "network-inspector-view") {
+ this.showNetworkInspectorView();
+ } else {
+ this.showNetworkStatisticsView();
+ }
+ },
+
+ /**
+ * Switches to the "Inspector" frontend view mode.
+ */
+ showNetworkInspectorView: function () {
+ this._body.selectedPanel = $("#network-inspector-view");
+ this.RequestsMenu._flushWaterfallViews(true);
+ },
+
+ /**
+ * Switches to the "Statistics" frontend view mode.
+ */
+ showNetworkStatisticsView: function () {
+ this._body.selectedPanel = $("#network-statistics-view");
+
+ let controller = NetMonitorController;
+ let requestsView = this.RequestsMenu;
+ let statisticsView = this.PerformanceStatistics;
+
+ Task.spawn(function* () {
+ statisticsView.displayPlaceholderCharts();
+ yield controller.triggerActivity(ACTIVITY_TYPE.RELOAD.WITH_CACHE_ENABLED);
+
+ try {
+ // • The response headers and status code are required for determining
+ // whether a response is "fresh" (cacheable).
+ // • The response content size and request total time are necessary for
+ // populating the statistics view.
+ // • The response mime type is used for categorization.
+ yield whenDataAvailable(requestsView, [
+ "responseHeaders", "status", "contentSize", "mimeType", "totalTime"
+ ]);
+ } catch (ex) {
+ // Timed out while waiting for data. Continue with what we have.
+ console.error(ex);
+ }
+
+ statisticsView.createPrimedCacheChart(requestsView.items);
+ statisticsView.createEmptyCacheChart(requestsView.items);
+ });
+ },
+
+ reloadPage: function () {
+ NetMonitorController.triggerActivity(
+ ACTIVITY_TYPE.RELOAD.WITH_CACHE_DEFAULT);
+ },
+
+ /**
+ * Lazily initializes and returns a promise for a Editor instance.
+ *
+ * @param string id
+ * The id of the editor placeholder node.
+ * @return object
+ * A promise that is resolved when the editor is available.
+ */
+ editor: function (id) {
+ dumpn("Getting a NetMonitorView editor: " + id);
+
+ if (this._editorPromises.has(id)) {
+ return this._editorPromises.get(id);
+ }
+
+ let deferred = promise.defer();
+ this._editorPromises.set(id, deferred.promise);
+
+ // Initialize the source editor and store the newly created instance
+ // in the ether of a resolved promise's value.
+ let editor = new Editor(DEFAULT_EDITOR_CONFIG);
+ editor.appendTo($(id)).then(() => deferred.resolve(editor));
+
+ return deferred.promise;
+ },
+
+ _body: null,
+ _detailsPane: null,
+ _editorPromises: new Map()
+};
+
+/**
+ * Functions handling the sidebar details view.
+ */
+function SidebarView() {
+ dumpn("SidebarView was instantiated");
+}
+
+SidebarView.prototype = {
+ /**
+ * Sets this view hidden or visible. It's visible by default.
+ *
+ * @param boolean visibleFlag
+ * Specifies the intended visibility.
+ */
+ toggle: function (visibleFlag) {
+ NetMonitorView.toggleDetailsPane({ visible: visibleFlag });
+ NetMonitorView.RequestsMenu._flushWaterfallViews(true);
+ },
+
+ /**
+ * Populates this view with the specified data.
+ *
+ * @param object data
+ * The data source (this should be the attachment of a request item).
+ * @return object
+ * Returns a promise that resolves upon population of the subview.
+ */
+ populate: Task.async(function* (data) {
+ let isCustom = data.isCustom;
+ let view = isCustom ?
+ NetMonitorView.CustomRequest :
+ NetMonitorView.NetworkDetails;
+
+ yield view.populate(data);
+ $("#details-pane").selectedIndex = isCustom ? 0 : 1;
+
+ window.emit(EVENTS.SIDEBAR_POPULATED);
+ })
+};
+
+/**
+ * Functions handling the requests details view.
+ */
+function NetworkDetailsView() {
+ dumpn("NetworkDetailsView was instantiated");
+
+ // The ToolSidebar requires the panel object to be able to emit events.
+ EventEmitter.decorate(this);
+
+ this._onTabSelect = this._onTabSelect.bind(this);
+}
+
+NetworkDetailsView.prototype = {
+ /**
+ * An object containing the state of tabs.
+ */
+ _viewState: {
+ // if updating[tab] is true a task is currently updating the given tab.
+ updating: [],
+ // if dirty[tab] is true, the tab needs to be repopulated once current
+ // update task finishes
+ dirty: [],
+ // the most recently received attachment data for the request
+ latestData: null,
+ },
+
+ /**
+ * Initialization function, called when the network monitor is started.
+ */
+ initialize: function () {
+ dumpn("Initializing the NetworkDetailsView");
+
+ this.widget = $("#event-details-pane");
+ this.sidebar = new ToolSidebar(this.widget, this, "netmonitor", {
+ disableTelemetry: true,
+ showAllTabsMenu: true
+ });
+
+ this._headers = new VariablesView($("#all-headers"),
+ Heritage.extend(GENERIC_VARIABLES_VIEW_SETTINGS, {
+ emptyText: L10N.getStr("headersEmptyText"),
+ searchPlaceholder: L10N.getStr("headersFilterText")
+ }));
+ this._cookies = new VariablesView($("#all-cookies"),
+ Heritage.extend(GENERIC_VARIABLES_VIEW_SETTINGS, {
+ emptyText: L10N.getStr("cookiesEmptyText"),
+ searchPlaceholder: L10N.getStr("cookiesFilterText")
+ }));
+ this._params = new VariablesView($("#request-params"),
+ Heritage.extend(GENERIC_VARIABLES_VIEW_SETTINGS, {
+ emptyText: L10N.getStr("paramsEmptyText"),
+ searchPlaceholder: L10N.getStr("paramsFilterText")
+ }));
+ this._json = new VariablesView($("#response-content-json"),
+ Heritage.extend(GENERIC_VARIABLES_VIEW_SETTINGS, {
+ onlyEnumVisible: true,
+ searchPlaceholder: L10N.getStr("jsonFilterText")
+ }));
+ VariablesViewController.attach(this._json);
+
+ this._paramsQueryString = L10N.getStr("paramsQueryString");
+ this._paramsFormData = L10N.getStr("paramsFormData");
+ this._paramsPostPayload = L10N.getStr("paramsPostPayload");
+ this._requestHeaders = L10N.getStr("requestHeaders");
+ this._requestHeadersFromUpload = L10N.getStr("requestHeadersFromUpload");
+ this._responseHeaders = L10N.getStr("responseHeaders");
+ this._requestCookies = L10N.getStr("requestCookies");
+ this._responseCookies = L10N.getStr("responseCookies");
+
+ $("tabpanels", this.widget).addEventListener("select", this._onTabSelect);
+ },
+
+ /**
+ * Destruction function, called when the network monitor is closed.
+ */
+ destroy: function () {
+ dumpn("Destroying the NetworkDetailsView");
+ this.sidebar.destroy();
+ $("tabpanels", this.widget).removeEventListener("select",
+ this._onTabSelect);
+ },
+
+ /**
+ * Populates this view with the specified data.
+ *
+ * @param object data
+ * The data source (this should be the attachment of a request item).
+ * @return object
+ * Returns a promise that resolves upon population the view.
+ */
+ populate: function (data) {
+ $("#request-params-box").setAttribute("flex", "1");
+ $("#request-params-box").hidden = false;
+ $("#request-post-data-textarea-box").hidden = true;
+ $("#response-content-info-header").hidden = true;
+ $("#response-content-json-box").hidden = true;
+ $("#response-content-textarea-box").hidden = true;
+ $("#raw-headers").hidden = true;
+ $("#response-content-image-box").hidden = true;
+
+ let isHtml = Filters.html(data);
+
+ // Show the "Preview" tabpanel only for plain HTML responses.
+ this.sidebar.toggleTab(isHtml, "preview-tab");
+
+ // Show the "Security" tab only for requests that
+ // 1) are https (state != insecure)
+ // 2) come from a target that provides security information.
+ let hasSecurityInfo = data.securityState &&
+ data.securityState !== "insecure";
+ this.sidebar.toggleTab(hasSecurityInfo, "security-tab");
+
+ // Switch to the "Headers" tabpanel if the "Preview" previously selected
+ // and this is not an HTML response or "Security" was selected but this
+ // request has no security information.
+
+ if (!isHtml && this.widget.selectedPanel === $("#preview-tabpanel") ||
+ !hasSecurityInfo && this.widget.selectedPanel ===
+ $("#security-tabpanel")) {
+ this.widget.selectedIndex = 0;
+ }
+
+ this._headers.empty();
+ this._cookies.empty();
+ this._params.empty();
+ this._json.empty();
+
+ this._dataSrc = { src: data, populated: [] };
+ this._onTabSelect();
+ window.emit(EVENTS.NETWORKDETAILSVIEW_POPULATED);
+
+ return promise.resolve();
+ },
+
+ /**
+ * Listener handling the tab selection event.
+ */
+ _onTabSelect: function () {
+ let { src, populated } = this._dataSrc || {};
+ let tab = this.widget.selectedIndex;
+ let view = this;
+
+ // Make sure the data source is valid and don't populate the same tab twice.
+ if (!src || populated[tab]) {
+ return;
+ }
+
+ let viewState = this._viewState;
+ if (viewState.updating[tab]) {
+ // A task is currently updating this tab. If we started another update
+ // task now it would result in a duplicated content as described in bugs
+ // 997065 and 984687. As there's no way to stop the current task mark the
+ // tab dirty and refresh the panel once the current task finishes.
+ viewState.dirty[tab] = true;
+ viewState.latestData = src;
+ return;
+ }
+
+ Task.spawn(function* () {
+ viewState.updating[tab] = true;
+ switch (tab) {
+ // "Headers"
+ case 0:
+ yield view._setSummary(src);
+ yield view._setResponseHeaders(src.responseHeaders);
+ yield view._setRequestHeaders(
+ src.requestHeaders,
+ src.requestHeadersFromUploadStream);
+ break;
+ // "Cookies"
+ case 1:
+ yield view._setResponseCookies(src.responseCookies);
+ yield view._setRequestCookies(src.requestCookies);
+ break;
+ // "Params"
+ case 2:
+ yield view._setRequestGetParams(src.url);
+ yield view._setRequestPostParams(
+ src.requestHeaders,
+ src.requestHeadersFromUploadStream,
+ src.requestPostData);
+ break;
+ // "Response"
+ case 3:
+ yield view._setResponseBody(src.url, src.responseContent);
+ break;
+ // "Timings"
+ case 4:
+ yield view._setTimingsInformation(src.eventTimings);
+ break;
+ // "Security"
+ case 5:
+ yield view._setSecurityInfo(src.securityInfo, src.url);
+ break;
+ // "Preview"
+ case 6:
+ yield view._setHtmlPreview(src.responseContent);
+ break;
+ }
+ viewState.updating[tab] = false;
+ }).then(() => {
+ if (tab == this.widget.selectedIndex) {
+ if (viewState.dirty[tab]) {
+ // The request information was updated while the task was running.
+ viewState.dirty[tab] = false;
+ view.populate(viewState.latestData);
+ } else {
+ // Tab is selected but not dirty. We're done here.
+ populated[tab] = true;
+ window.emit(EVENTS.TAB_UPDATED);
+
+ if (NetMonitorController.isConnected()) {
+ NetMonitorView.RequestsMenu.ensureSelectedItemIsVisible();
+ }
+ }
+ } else if (viewState.dirty[tab]) {
+ // Tab is dirty but no longer selected. Don't refresh it now, it'll be
+ // done if the tab is shown again.
+ viewState.dirty[tab] = false;
+ }
+ }, e => console.error(e));
+ },
+
+ /**
+ * Sets the network request summary shown in this view.
+ *
+ * @param object data
+ * The data source (this should be the attachment of a request item).
+ */
+ _setSummary: function (data) {
+ if (data.url) {
+ let unicodeUrl = NetworkHelper.convertToUnicode(unescape(data.url));
+ $("#headers-summary-url-value").setAttribute("value", unicodeUrl);
+ $("#headers-summary-url-value").setAttribute("tooltiptext", unicodeUrl);
+ $("#headers-summary-url").removeAttribute("hidden");
+ } else {
+ $("#headers-summary-url").setAttribute("hidden", "true");
+ }
+
+ if (data.method) {
+ $("#headers-summary-method-value").setAttribute("value", data.method);
+ $("#headers-summary-method").removeAttribute("hidden");
+ } else {
+ $("#headers-summary-method").setAttribute("hidden", "true");
+ }
+
+ if (data.remoteAddress) {
+ let address = data.remoteAddress;
+ if (address.indexOf(":") != -1) {
+ address = `[${address}]`;
+ }
+ if (data.remotePort) {
+ address += `:${data.remotePort}`;
+ }
+ $("#headers-summary-address-value").setAttribute("value", address);
+ $("#headers-summary-address-value").setAttribute("tooltiptext", address);
+ $("#headers-summary-address").removeAttribute("hidden");
+ } else {
+ $("#headers-summary-address").setAttribute("hidden", "true");
+ }
+
+ if (data.status) {
+ // "code" attribute is only used by css to determine the icon color
+ let code;
+ if (data.fromCache) {
+ code = "cached";
+ } else if (data.fromServiceWorker) {
+ code = "service worker";
+ } else {
+ code = data.status;
+ }
+ $("#headers-summary-status-circle").setAttribute("code", code);
+ $("#headers-summary-status-value").setAttribute("value",
+ data.status + " " + data.statusText);
+ $("#headers-summary-status").removeAttribute("hidden");
+ } else {
+ $("#headers-summary-status").setAttribute("hidden", "true");
+ }
+
+ if (data.httpVersion) {
+ $("#headers-summary-version-value").setAttribute("value",
+ data.httpVersion);
+ $("#headers-summary-version").removeAttribute("hidden");
+ } else {
+ $("#headers-summary-version").setAttribute("hidden", "true");
+ }
+ },
+
+ /**
+ * Sets the network request headers shown in this view.
+ *
+ * @param object headers
+ * The "requestHeaders" message received from the server.
+ * @param object uploadHeaders
+ * The "requestHeadersFromUploadStream" inferred from the POST payload.
+ * @return object
+ * A promise that resolves when request headers are set.
+ */
+ _setRequestHeaders: Task.async(function* (headers, uploadHeaders) {
+ if (headers && headers.headers.length) {
+ yield this._addHeaders(this._requestHeaders, headers);
+ }
+ if (uploadHeaders && uploadHeaders.headers.length) {
+ yield this._addHeaders(this._requestHeadersFromUpload, uploadHeaders);
+ }
+ }),
+
+ /**
+ * Sets the network response headers shown in this view.
+ *
+ * @param object response
+ * The message received from the server.
+ * @return object
+ * A promise that resolves when response headers are set.
+ */
+ _setResponseHeaders: Task.async(function* (response) {
+ if (response && response.headers.length) {
+ response.headers.sort((a, b) => a.name > b.name);
+ yield this._addHeaders(this._responseHeaders, response);
+ }
+ }),
+
+ /**
+ * Populates the headers container in this view with the specified data.
+ *
+ * @param string name
+ * The type of headers to populate (request or response).
+ * @param object response
+ * The message received from the server.
+ * @return object
+ * A promise that resolves when headers are added.
+ */
+ _addHeaders: Task.async(function* (name, response) {
+ let kb = response.headersSize / 1024;
+ let size = L10N.numberWithDecimals(kb, HEADERS_SIZE_DECIMALS);
+ let text = L10N.getFormatStr("networkMenu.sizeKB", size);
+
+ let headersScope = this._headers.addScope(name + " (" + text + ")");
+ headersScope.expanded = true;
+
+ for (let header of response.headers) {
+ let headerVar = headersScope.addItem(header.name, {}, {relaxed: true});
+ let headerValue = yield gNetwork.getString(header.value);
+ headerVar.setGrip(headerValue);
+ }
+ }),
+
+ /**
+ * Sets the network request cookies shown in this view.
+ *
+ * @param object response
+ * The message received from the server.
+ * @return object
+ * A promise that is resolved when the request cookies are set.
+ */
+ _setRequestCookies: Task.async(function* (response) {
+ if (response && response.cookies.length) {
+ response.cookies.sort((a, b) => a.name > b.name);
+ yield this._addCookies(this._requestCookies, response);
+ }
+ }),
+
+ /**
+ * Sets the network response cookies shown in this view.
+ *
+ * @param object response
+ * The message received from the server.
+ * @return object
+ * A promise that is resolved when the response cookies are set.
+ */
+ _setResponseCookies: Task.async(function* (response) {
+ if (response && response.cookies.length) {
+ yield this._addCookies(this._responseCookies, response);
+ }
+ }),
+
+ /**
+ * Populates the cookies container in this view with the specified data.
+ *
+ * @param string name
+ * The type of cookies to populate (request or response).
+ * @param object response
+ * The message received from the server.
+ * @return object
+ * Returns a promise that resolves upon the adding of cookies.
+ */
+ _addCookies: Task.async(function* (name, response) {
+ let cookiesScope = this._cookies.addScope(name);
+ cookiesScope.expanded = true;
+
+ for (let cookie of response.cookies) {
+ let cookieVar = cookiesScope.addItem(cookie.name, {}, {relaxed: true});
+ let cookieValue = yield gNetwork.getString(cookie.value);
+ cookieVar.setGrip(cookieValue);
+
+ // By default the cookie name and value are shown. If this is the only
+ // information available, then nothing else is to be displayed.
+ let cookieProps = Object.keys(cookie);
+ if (cookieProps.length == 2) {
+ continue;
+ }
+
+ // Display any other information other than the cookie name and value
+ // which may be available.
+ let rawObject = Object.create(null);
+ let otherProps = cookieProps.filter(e => e != "name" && e != "value");
+ for (let prop of otherProps) {
+ rawObject[prop] = cookie[prop];
+ }
+ cookieVar.populate(rawObject);
+ cookieVar.twisty = true;
+ cookieVar.expanded = true;
+ }
+ }),
+
+ /**
+ * Sets the network request get params shown in this view.
+ *
+ * @param string url
+ * The request's url.
+ */
+ _setRequestGetParams: function (url) {
+ let query = NetworkHelper.nsIURL(url).query;
+ if (query) {
+ this._addParams(this._paramsQueryString, query);
+ }
+ },
+
+ /**
+ * Sets the network request post params shown in this view.
+ *
+ * @param object headers
+ * The "requestHeaders" message received from the server.
+ * @param object uploadHeaders
+ * The "requestHeadersFromUploadStream" inferred from the POST payload.
+ * @param object postData
+ * The "requestPostData" message received from the server.
+ * @return object
+ * A promise that is resolved when the request post params are set.
+ */
+ _setRequestPostParams: Task.async(function* (headers, uploadHeaders,
+ postData) {
+ if (!headers || !uploadHeaders || !postData) {
+ return;
+ }
+
+ let formDataSections = yield getFormDataSections(
+ headers,
+ uploadHeaders,
+ postData,
+ gNetwork.getString.bind(gNetwork));
+
+ this._params.onlyEnumVisible = false;
+
+ // Handle urlencoded form data sections (e.g. "?foo=bar&baz=42").
+ if (formDataSections.length > 0) {
+ formDataSections.forEach(section => {
+ this._addParams(this._paramsFormData, section);
+ });
+ } else {
+ // Handle JSON and actual forms ("multipart/form-data" content type).
+ let postDataLongString = postData.postData.text;
+ let text = yield gNetwork.getString(postDataLongString);
+ let jsonVal = null;
+ try {
+ jsonVal = JSON.parse(text);
+ } catch (ex) { // eslint-disable-line
+ }
+
+ if (jsonVal) {
+ this._params.onlyEnumVisible = true;
+ let jsonScopeName = L10N.getStr("jsonScopeName");
+ let jsonScope = this._params.addScope(jsonScopeName);
+ jsonScope.expanded = true;
+ let jsonItem = jsonScope.addItem(undefined, { enumerable: true });
+ jsonItem.populate(jsonVal, { sorted: true });
+ } else {
+ // This is really awkward, but hey, it works. Let's show an empty
+ // scope in the params view and place the source editor containing
+ // the raw post data directly underneath.
+ $("#request-params-box").removeAttribute("flex");
+ let paramsScope = this._params.addScope(this._paramsPostPayload);
+ paramsScope.expanded = true;
+ paramsScope.locked = true;
+
+ $("#request-post-data-textarea-box").hidden = false;
+ let editor = yield NetMonitorView.editor("#request-post-data-textarea");
+ editor.setMode(Editor.modes.text);
+ editor.setText(text);
+ }
+ }
+
+ window.emit(EVENTS.REQUEST_POST_PARAMS_DISPLAYED);
+ }),
+
+ /**
+ * Populates the params container in this view with the specified data.
+ *
+ * @param string name
+ * The type of params to populate (get or post).
+ * @param string queryString
+ * A query string of params (e.g. "?foo=bar&baz=42").
+ */
+ _addParams: function (name, queryString) {
+ let paramsArray = NetworkHelper.parseQueryString(queryString);
+ if (!paramsArray) {
+ return;
+ }
+ let paramsScope = this._params.addScope(name);
+ paramsScope.expanded = true;
+
+ for (let param of paramsArray) {
+ let paramVar = paramsScope.addItem(param.name, {}, {relaxed: true});
+ paramVar.setGrip(param.value);
+ }
+ },
+
+ /**
+ * Sets the network response body shown in this view.
+ *
+ * @param string url
+ * The request's url.
+ * @param object response
+ * The message received from the server.
+ * @return object
+ * A promise that is resolved when the response body is set.
+ */
+ _setResponseBody: Task.async(function* (url, response) {
+ if (!response) {
+ return;
+ }
+ let { mimeType, text, encoding } = response.content;
+ let responseBody = yield gNetwork.getString(text);
+
+ // Handle json, which we tentatively identify by checking the MIME type
+ // for "json" after any word boundary. This works for the standard
+ // "application/json", and also for custom types like "x-bigcorp-json".
+ // Additionally, we also directly parse the response text content to
+ // verify whether it's json or not, to handle responses incorrectly
+ // labeled as text/plain instead.
+ let jsonMimeType, jsonObject, jsonObjectParseError;
+ try {
+ jsonMimeType = /\bjson/.test(mimeType);
+ jsonObject = JSON.parse(responseBody);
+ } catch (e) {
+ jsonObjectParseError = e;
+ }
+ if (jsonMimeType || jsonObject) {
+ // Extract the actual json substring in case this might be a "JSONP".
+ // This regex basically parses a function call and captures the
+ // function name and arguments in two separate groups.
+ let jsonpRegex = /^\s*([\w$]+)\s*\(\s*([^]*)\s*\)\s*;?\s*$/;
+ let [_, callbackPadding, jsonpString] = // eslint-disable-line
+ responseBody.match(jsonpRegex) || [];
+
+ // Make sure this is a valid JSON object first. If so, nicely display
+ // the parsing results in a variables view. Otherwise, simply show
+ // the contents as plain text.
+ if (callbackPadding && jsonpString) {
+ try {
+ jsonObject = JSON.parse(jsonpString);
+ } catch (e) {
+ jsonObjectParseError = e;
+ }
+ }
+
+ // Valid JSON or JSONP.
+ if (jsonObject) {
+ $("#response-content-json-box").hidden = false;
+ let jsonScopeName = callbackPadding
+ ? L10N.getFormatStr("jsonpScopeName", callbackPadding)
+ : L10N.getStr("jsonScopeName");
+
+ let jsonVar = { label: jsonScopeName, rawObject: jsonObject };
+ yield this._json.controller.setSingleVariable(jsonVar).expanded;
+ } else {
+ // Malformed JSON.
+ $("#response-content-textarea-box").hidden = false;
+ let infoHeader = $("#response-content-info-header");
+ infoHeader.setAttribute("value", jsonObjectParseError);
+ infoHeader.setAttribute("tooltiptext", jsonObjectParseError);
+ infoHeader.hidden = false;
+
+ let editor = yield NetMonitorView.editor("#response-content-textarea");
+ editor.setMode(Editor.modes.js);
+ editor.setText(responseBody);
+ }
+ } else if (mimeType.includes("image/")) {
+ // Handle images.
+ $("#response-content-image-box").setAttribute("align", "center");
+ $("#response-content-image-box").setAttribute("pack", "center");
+ $("#response-content-image-box").hidden = false;
+ $("#response-content-image").src = formDataURI(mimeType, encoding, responseBody);
+
+ // Immediately display additional information about the image:
+ // file name, mime type and encoding.
+ $("#response-content-image-name-value").setAttribute("value",
+ NetworkHelper.nsIURL(url).fileName);
+ $("#response-content-image-mime-value").setAttribute("value", mimeType);
+
+ // Wait for the image to load in order to display the width and height.
+ $("#response-content-image").onload = e => {
+ // XUL images are majestic so they don't bother storing their dimensions
+ // in width and height attributes like the rest of the folk. Hack around
+ // this by getting the bounding client rect and subtracting the margins.
+ let { width, height } = e.target.getBoundingClientRect();
+ let dimensions = (width - 2) + " \u00D7 " + (height - 2);
+ $("#response-content-image-dimensions-value").setAttribute("value",
+ dimensions);
+ };
+ } else {
+ $("#response-content-textarea-box").hidden = false;
+ let editor = yield NetMonitorView.editor("#response-content-textarea");
+ editor.setMode(Editor.modes.text);
+ editor.setText(responseBody);
+
+ // Maybe set a more appropriate mode in the Source Editor if possible,
+ // but avoid doing this for very large files.
+ if (responseBody.length < SOURCE_SYNTAX_HIGHLIGHT_MAX_FILE_SIZE) {
+ let mapping = Object.keys(CONTENT_MIME_TYPE_MAPPINGS).find(key => {
+ return mimeType.includes(key);
+ });
+
+ if (mapping) {
+ editor.setMode(CONTENT_MIME_TYPE_MAPPINGS[mapping]);
+ }
+ }
+ }
+
+ window.emit(EVENTS.RESPONSE_BODY_DISPLAYED);
+ }),
+
+ /**
+ * Sets the timings information shown in this view.
+ *
+ * @param object response
+ * The message received from the server.
+ */
+ _setTimingsInformation: function (response) {
+ if (!response) {
+ return;
+ }
+ let { blocked, dns, connect, send, wait, receive } = response.timings;
+
+ let tabboxWidth = $("#details-pane").getAttribute("width");
+
+ // Other nodes also take some space.
+ let availableWidth = tabboxWidth / 2;
+ let scale = (response.totalTime > 0 ?
+ Math.max(availableWidth / response.totalTime, 0) :
+ 0);
+
+ $("#timings-summary-blocked .requests-menu-timings-box")
+ .setAttribute("width", blocked * scale);
+ $("#timings-summary-blocked .requests-menu-timings-total")
+ .setAttribute("value", L10N.getFormatStr("networkMenu.totalMS", blocked));
+
+ $("#timings-summary-dns .requests-menu-timings-box")
+ .setAttribute("width", dns * scale);
+ $("#timings-summary-dns .requests-menu-timings-total")
+ .setAttribute("value", L10N.getFormatStr("networkMenu.totalMS", dns));
+
+ $("#timings-summary-connect .requests-menu-timings-box")
+ .setAttribute("width", connect * scale);
+ $("#timings-summary-connect .requests-menu-timings-total")
+ .setAttribute("value", L10N.getFormatStr("networkMenu.totalMS", connect));
+
+ $("#timings-summary-send .requests-menu-timings-box")
+ .setAttribute("width", send * scale);
+ $("#timings-summary-send .requests-menu-timings-total")
+ .setAttribute("value", L10N.getFormatStr("networkMenu.totalMS", send));
+
+ $("#timings-summary-wait .requests-menu-timings-box")
+ .setAttribute("width", wait * scale);
+ $("#timings-summary-wait .requests-menu-timings-total")
+ .setAttribute("value", L10N.getFormatStr("networkMenu.totalMS", wait));
+
+ $("#timings-summary-receive .requests-menu-timings-box")
+ .setAttribute("width", receive * scale);
+ $("#timings-summary-receive .requests-menu-timings-total")
+ .setAttribute("value", L10N.getFormatStr("networkMenu.totalMS", receive));
+
+ $("#timings-summary-dns .requests-menu-timings-box")
+ .style.transform = "translateX(" + (scale * blocked) + "px)";
+ $("#timings-summary-connect .requests-menu-timings-box")
+ .style.transform = "translateX(" + (scale * (blocked + dns)) + "px)";
+ $("#timings-summary-send .requests-menu-timings-box")
+ .style.transform =
+ "translateX(" + (scale * (blocked + dns + connect)) + "px)";
+ $("#timings-summary-wait .requests-menu-timings-box")
+ .style.transform =
+ "translateX(" + (scale * (blocked + dns + connect + send)) + "px)";
+ $("#timings-summary-receive .requests-menu-timings-box")
+ .style.transform =
+ "translateX(" + (scale * (blocked + dns + connect + send + wait)) +
+ "px)";
+
+ $("#timings-summary-dns .requests-menu-timings-total")
+ .style.transform = "translateX(" + (scale * blocked) + "px)";
+ $("#timings-summary-connect .requests-menu-timings-total")
+ .style.transform = "translateX(" + (scale * (blocked + dns)) + "px)";
+ $("#timings-summary-send .requests-menu-timings-total")
+ .style.transform =
+ "translateX(" + (scale * (blocked + dns + connect)) + "px)";
+ $("#timings-summary-wait .requests-menu-timings-total")
+ .style.transform =
+ "translateX(" + (scale * (blocked + dns + connect + send)) + "px)";
+ $("#timings-summary-receive .requests-menu-timings-total")
+ .style.transform =
+ "translateX(" + (scale * (blocked + dns + connect + send + wait)) +
+ "px)";
+ },
+
+ /**
+ * Sets the preview for HTML responses shown in this view.
+ *
+ * @param object response
+ * The message received from the server.
+ * @return object
+ * A promise that is resolved when the html preview is rendered.
+ */
+ _setHtmlPreview: Task.async(function* (response) {
+ if (!response) {
+ return promise.resolve();
+ }
+ let { text } = response.content;
+ let responseBody = yield gNetwork.getString(text);
+
+ // Always disable JS when previewing HTML responses.
+ let iframe = $("#response-preview");
+ iframe.contentDocument.docShell.allowJavascript = false;
+ iframe.contentDocument.documentElement.innerHTML = responseBody;
+
+ window.emit(EVENTS.RESPONSE_HTML_PREVIEW_DISPLAYED);
+ return undefined;
+ }),
+
+ /**
+ * Sets the security information shown in this view.
+ *
+ * @param object securityInfo
+ * The data received from server
+ * @param string url
+ * The URL of this request
+ * @return object
+ * A promise that is resolved when the security info is rendered.
+ */
+ _setSecurityInfo: Task.async(function* (securityInfo, url) {
+ if (!securityInfo) {
+ // We don't have security info. This could mean one of two things:
+ // 1) This connection is not secure and this tab is not visible and thus
+ // we shouldn't be here.
+ // 2) We have already received securityState and the tab is visible BUT
+ // the rest of the information is still on its way. Once it arrives
+ // this method is called again.
+ return;
+ }
+
+ /**
+ * A helper that sets value and tooltiptext attributes of an element to
+ * specified value.
+ *
+ * @param string selector
+ * A selector for the element.
+ * @param string value
+ * The value to set. If this evaluates to false a placeholder string
+ * <Not Available> is used instead.
+ */
+ function setValue(selector, value) {
+ let label = $(selector);
+ if (!value) {
+ label.setAttribute("value", L10N.getStr(
+ "netmonitor.security.notAvailable"));
+ label.setAttribute("tooltiptext", label.getAttribute("value"));
+ } else {
+ label.setAttribute("value", value);
+ label.setAttribute("tooltiptext", value);
+ }
+ }
+
+ let errorbox = $("#security-error");
+ let infobox = $("#security-information");
+
+ if (securityInfo.state === "secure" || securityInfo.state === "weak") {
+ infobox.hidden = false;
+ errorbox.hidden = true;
+
+ // Warning icons
+ let cipher = $("#security-warning-cipher");
+
+ if (securityInfo.state === "weak") {
+ cipher.hidden = securityInfo.weaknessReasons.indexOf("cipher") === -1;
+ } else {
+ cipher.hidden = true;
+ }
+
+ let enabledLabel = L10N.getStr("netmonitor.security.enabled");
+ let disabledLabel = L10N.getStr("netmonitor.security.disabled");
+
+ // Connection parameters
+ setValue("#security-protocol-version-value",
+ securityInfo.protocolVersion);
+ setValue("#security-ciphersuite-value", securityInfo.cipherSuite);
+
+ // Host header
+ let domain = getUriHostPort(url);
+ let hostHeader = L10N.getFormatStr("netmonitor.security.hostHeader",
+ domain);
+ setValue("#security-info-host-header", hostHeader);
+
+ // Parameters related to the domain
+ setValue("#security-http-strict-transport-security-value",
+ securityInfo.hsts ? enabledLabel : disabledLabel);
+
+ setValue("#security-public-key-pinning-value",
+ securityInfo.hpkp ? enabledLabel : disabledLabel);
+
+ // Certificate parameters
+ let cert = securityInfo.cert;
+ setValue("#security-cert-subject-cn", cert.subject.commonName);
+ setValue("#security-cert-subject-o", cert.subject.organization);
+ setValue("#security-cert-subject-ou", cert.subject.organizationalUnit);
+
+ setValue("#security-cert-issuer-cn", cert.issuer.commonName);
+ setValue("#security-cert-issuer-o", cert.issuer.organization);
+ setValue("#security-cert-issuer-ou", cert.issuer.organizationalUnit);
+
+ setValue("#security-cert-validity-begins", cert.validity.start);
+ setValue("#security-cert-validity-expires", cert.validity.end);
+
+ setValue("#security-cert-sha1-fingerprint", cert.fingerprint.sha1);
+ setValue("#security-cert-sha256-fingerprint", cert.fingerprint.sha256);
+ } else {
+ infobox.hidden = true;
+ errorbox.hidden = false;
+
+ // Strip any HTML from the message.
+ let plain = new DOMParser().parseFromString(securityInfo.errorMessage,
+ "text/html");
+ setValue("#security-error-message", plain.body.textContent);
+ }
+ }),
+
+ _dataSrc: null,
+ _headers: null,
+ _cookies: null,
+ _params: null,
+ _json: null,
+ _paramsQueryString: "",
+ _paramsFormData: "",
+ _paramsPostPayload: "",
+ _requestHeaders: "",
+ _responseHeaders: "",
+ _requestCookies: "",
+ _responseCookies: ""
+};
+
+/**
+ * DOM query helper.
+ * TODO: Move it into "dom-utils.js" module and "require" it when needed.
+ */
+var $ = (selector, target = document) => target.querySelector(selector);
+var $all = (selector, target = document) => target.querySelectorAll(selector);
+
+/**
+ * Makes sure certain properties are available on all objects in a data store.
+ *
+ * @param array dataStore
+ * The request view object from which to fetch the item list.
+ * @param array mandatoryFields
+ * A list of strings representing properties of objects in dataStore.
+ * @return object
+ * A promise resolved when all objects in dataStore contain the
+ * properties defined in mandatoryFields.
+ */
+function whenDataAvailable(requestsView, mandatoryFields) {
+ let deferred = promise.defer();
+
+ let interval = setInterval(() => {
+ const { attachments } = requestsView;
+ if (attachments.length > 0 && attachments.every(item => {
+ return mandatoryFields.every(field => field in item);
+ })) {
+ clearInterval(interval);
+ clearTimeout(timer);
+ deferred.resolve();
+ }
+ }, WDA_DEFAULT_VERIFY_INTERVAL);
+
+ let timer = setTimeout(() => {
+ clearInterval(interval);
+ deferred.reject(new Error("Timed out while waiting for data"));
+ }, WDA_DEFAULT_GIVE_UP_TIMEOUT);
+
+ return deferred.promise;
+}
+
+/**
+ * Preliminary setup for the NetMonitorView object.
+ */
+NetMonitorView.Toolbar = new ToolbarView();
+NetMonitorView.RequestsMenu = new RequestsMenuView();
+NetMonitorView.Sidebar = new SidebarView();
+NetMonitorView.CustomRequest = new CustomRequestView();
+NetMonitorView.NetworkDetails = new NetworkDetailsView();
+NetMonitorView.PerformanceStatistics = new PerformanceStatisticsView();
diff --git a/devtools/client/netmonitor/netmonitor.xul b/devtools/client/netmonitor/netmonitor.xul
new file mode 100644
index 000000000..bb580f7ad
--- /dev/null
+++ b/devtools/client/netmonitor/netmonitor.xul
@@ -0,0 +1,741 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/content/shared/widgets/widgets.css" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/skin/widgets.css" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/skin/netmonitor.css" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml">
+
+ <script type="application/javascript;version=1.8"
+ src="chrome://devtools/content/shared/theme-switching.js"/>
+ <script type="text/javascript" src="netmonitor-controller.js"/>
+ <script type="text/javascript" src="netmonitor-view.js"/>
+
+ <deck id="body"
+ class="theme-sidebar"
+ flex="1"
+ data-localization-bundle="devtools/client/locales/netmonitor.properties">
+
+ <vbox id="network-inspector-view" flex="1">
+ <hbox id="netmonitor-toolbar" class="devtools-toolbar">
+ <html:div xmlns="http://www.w3.org/1999/xhtml"
+ id="react-clear-button-hook"/>
+ <html:div xmlns="http://www.w3.org/1999/xhtml"
+ id="react-filter-buttons-hook"/>
+ <spacer id="requests-menu-spacer"
+ flex="1"/>
+ <toolbarbutton id="requests-menu-network-summary-button"
+ class="devtools-toolbarbutton icon-and-text"
+ data-localization="tooltiptext=netmonitor.toolbar.perf"/>
+ <html:div xmlns="http://www.w3.org/1999/xhtml"
+ id="react-search-box-hook"/>
+ <html:div xmlns="http://www.w3.org/1999/xhtml"
+ id="react-details-pane-toggle-hook"/>
+ </hbox>
+ <hbox id="network-table-and-sidebar"
+ class="devtools-responsive-container"
+ flex="1">
+ <vbox id="network-table" flex="1" class="devtools-main-content">
+ <toolbar id="requests-menu-toolbar"
+ class="devtools-toolbar"
+ align="center">
+ <hbox id="toolbar-labels" flex="1">
+ <hbox id="requests-menu-status-header-box"
+ class="requests-menu-header requests-menu-status"
+ align="center">
+ <button id="requests-menu-status-button"
+ class="requests-menu-header-button requests-menu-status"
+ data-key="status"
+ data-localization="label=netmonitor.toolbar.status3"
+ flex="1">
+ </button>
+ </hbox>
+ <hbox id="requests-menu-method-header-box"
+ class="requests-menu-header requests-menu-method"
+ align="center">
+ <button id="requests-menu-method-button"
+ class="requests-menu-header-button requests-menu-method"
+ data-key="method"
+ data-localization="label=netmonitor.toolbar.method"
+ crop="end"
+ flex="1">
+ </button>
+ </hbox>
+ <hbox id="requests-menu-icon-and-file-header-box"
+ class="requests-menu-header requests-menu-icon-and-file"
+ align="center">
+ <button id="requests-menu-file-button"
+ class="requests-menu-header-button requests-menu-file"
+ data-key="file"
+ data-localization="label=netmonitor.toolbar.file"
+ crop="end"
+ flex="1">
+ </button>
+ </hbox>
+ <hbox id="requests-menu-domain-header-box"
+ class="requests-menu-header requests-menu-security-and-domain"
+ align="center">
+ <button id="requests-menu-domain-button"
+ class="requests-menu-header-button requests-menu-security-and-domain"
+ data-key="domain"
+ data-localization="label=netmonitor.toolbar.domain"
+ crop="end"
+ flex="1">
+ </button>
+ </hbox>
+ <hbox id="requests-menu-cause-header-box"
+ class="requests-menu-header requests-menu-cause"
+ align="center">
+ <button id="requests-menu-cause-button"
+ class="requests-menu-header-button requests-menu-cause"
+ data-key="cause"
+ data-localization="label=netmonitor.toolbar.cause"
+ crop="end"
+ flex="1">
+ </button>
+ </hbox>
+ <hbox id="requests-menu-type-header-box"
+ class="requests-menu-header requests-menu-type"
+ align="center">
+ <button id="requests-menu-type-button"
+ class="requests-menu-header-button requests-menu-type"
+ data-key="type"
+ data-localization="label=netmonitor.toolbar.type"
+ crop="end"
+ flex="1">
+ </button>
+ </hbox>
+ <hbox id="requests-menu-transferred-header-box"
+ class="requests-menu-header requests-menu-transferred"
+ align="center">
+ <button id="requests-menu-transferred-button"
+ class="requests-menu-header-button requests-menu-transferred"
+ data-key="transferred"
+ data-localization="label=netmonitor.toolbar.transferred"
+ crop="end"
+ flex="1">
+ </button>
+ </hbox>
+ <hbox id="requests-menu-size-header-box"
+ class="requests-menu-header requests-menu-size"
+ align="center">
+ <button id="requests-menu-size-button"
+ class="requests-menu-header-button requests-menu-size"
+ data-key="size"
+ data-localization="label=netmonitor.toolbar.size"
+ crop="end"
+ flex="1">
+ </button>
+ </hbox>
+ <hbox id="requests-menu-waterfall-header-box"
+ class="requests-menu-header requests-menu-waterfall"
+ align="center"
+ flex="1">
+ <button id="requests-menu-waterfall-button"
+ class="requests-menu-header-button requests-menu-waterfall"
+ data-key="waterfall"
+ pack="start"
+ data-localization="label=netmonitor.toolbar.waterfall"
+ flex="1">
+ <image id="requests-menu-waterfall-image"/>
+ <box id="requests-menu-waterfall-label-wrapper">
+ <label id="requests-menu-waterfall-label"
+ class="plain requests-menu-waterfall"
+ data-localization="value=netmonitor.toolbar.waterfall"/>
+ </box>
+ </button>
+ </hbox>
+ </hbox>
+ </toolbar>
+
+ <vbox id="requests-menu-empty-notice"
+ class="side-menu-widget-empty-text">
+ <hbox id="notice-reload-message" align="center">
+ <label data-localization="content=netmonitor.reloadNotice1"/>
+ <button id="requests-menu-reload-notice-button"
+ class="devtools-toolbarbutton"
+ standalone="true"
+ data-localization="label=netmonitor.reloadNotice2"/>
+ <label data-localization="content=netmonitor.reloadNotice3"/>
+ </hbox>
+ <hbox id="notice-perf-message" align="center">
+ <label data-localization="content=netmonitor.perfNotice1"/>
+ <button id="requests-menu-perf-notice-button"
+ class="devtools-toolbarbutton"
+ standalone="true"
+ data-localization="tooltiptext=netmonitor.perfNotice3"/>
+ <label data-localization="content=netmonitor.perfNotice2"/>
+ </hbox>
+ </vbox>
+
+ <vbox id="requests-menu-contents" flex="1">
+ <hbox id="requests-menu-item-template" hidden="true">
+ <hbox class="requests-menu-subitem requests-menu-status"
+ align="center">
+ <box class="requests-menu-status-icon"/>
+ <label class="plain requests-menu-status-code"
+ crop="end"/>
+ </hbox>
+ <hbox class="requests-menu-subitem requests-menu-method-box"
+ align="center">
+ <label class="plain requests-menu-method"
+ crop="end"
+ flex="1"/>
+ </hbox>
+ <hbox class="requests-menu-subitem requests-menu-icon-and-file"
+ align="center">
+ <image class="requests-menu-icon" hidden="true"/>
+ <label class="plain requests-menu-file"
+ crop="end"
+ flex="1"/>
+ </hbox>
+ <hbox class="requests-menu-subitem requests-menu-security-and-domain"
+ align="center">
+ <image class="requests-security-state-icon" />
+ <label class="plain requests-menu-domain"
+ crop="end"
+ flex="1"/>
+ </hbox>
+ <hbox class="requests-menu-subitem requests-menu-cause" align="center">
+ <label class="requests-menu-cause-stack" value="JS" hidden="true"/>
+ <label class="plain requests-menu-cause-label" flex="1" crop="end"/>
+ </hbox>
+ <label class="plain requests-menu-subitem requests-menu-type"
+ crop="end"/>
+ <label class="plain requests-menu-subitem requests-menu-transferred"
+ crop="end"/>
+ <label class="plain requests-menu-subitem requests-menu-size"
+ crop="end"/>
+ <hbox class="requests-menu-subitem requests-menu-waterfall"
+ align="center"
+ flex="1">
+ <hbox class="requests-menu-timings"
+ align="center">
+ <label class="plain requests-menu-timings-total"/>
+ </hbox>
+ </hbox>
+ </hbox>
+ </vbox>
+ </vbox>
+
+ <splitter id="network-inspector-view-splitter"
+ class="devtools-side-splitter"/>
+
+ <deck id="details-pane"
+ hidden="true">
+ <vbox id="custom-pane"
+ class="tabpanel-content">
+ <hbox align="baseline">
+ <label data-localization="content=netmonitor.custom.newRequest"
+ class="plain tabpanel-summary-label
+ custom-header"/>
+ <hbox flex="1" pack="end"
+ class="devtools-toolbarbutton-group">
+ <button id="custom-request-send-button"
+ class="devtools-toolbarbutton"
+ data-localization="label=netmonitor.custom.send"/>
+ <button id="custom-request-close-button"
+ class="devtools-toolbarbutton"
+ data-localization="label=netmonitor.custom.cancel"/>
+ </hbox>
+ </hbox>
+ <hbox id="custom-method-and-url"
+ class="tabpanel-summary-container"
+ align="center">
+ <textbox id="custom-method-value"
+ data-key="method"/>
+ <textbox id="custom-url-value"
+ flex="1"
+ data-key="url"/>
+ </hbox>
+ <vbox id="custom-query"
+ class="tabpanel-summary-container custom-section">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=netmonitor.custom.query"/>
+ <textbox id="custom-query-value"
+ class="tabpanel-summary-input"
+ multiline="true"
+ rows="4"
+ wrap="off"
+ data-key="query"/>
+ </vbox>
+ <vbox id="custom-headers"
+ class="tabpanel-summary-container custom-section">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=netmonitor.custom.headers"/>
+ <textbox id="custom-headers-value"
+ class="tabpanel-summary-input"
+ multiline="true"
+ rows="8"
+ wrap="off"
+ data-key="headers"/>
+ </vbox>
+ <vbox id="custom-postdata"
+ class="tabpanel-summary-container custom-section">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=netmonitor.custom.postData"/>
+ <textbox id="custom-postdata-value"
+ class="tabpanel-summary-input"
+ multiline="true"
+ rows="6"
+ wrap="off"
+ data-key="body"/>
+ </vbox>
+ </vbox>
+ <tabbox id="event-details-pane"
+ class="devtools-sidebar-tabs"
+ handleCtrlTab="false">
+ <tabs>
+ <tab id="headers-tab"
+ crop="end"
+ data-localization="label=netmonitor.tab.headers"/>
+ <tab id="cookies-tab"
+ crop="end"
+ data-localization="label=netmonitor.tab.cookies"/>
+ <tab id="params-tab"
+ crop="end"
+ data-localization="label=netmonitor.tab.params"/>
+ <tab id="response-tab"
+ crop="end"
+ data-localization="label=netmonitor.tab.response"/>
+ <tab id="timings-tab"
+ crop="end"
+ data-localization="label=netmonitor.tab.timings"/>
+ <tab id="security-tab"
+ crop="end"
+ data-localization="label=netmonitor.tab.security"/>
+ <tab id="preview-tab"
+ crop="end"
+ data-localization="label=netmonitor.tab.preview"/>
+ </tabs>
+ <tabpanels flex="1">
+ <tabpanel id="headers-tabpanel"
+ class="tabpanel-content">
+ <vbox flex="1">
+ <hbox id="headers-summary-url"
+ class="tabpanel-summary-container"
+ align="center">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=netmonitor.summary.url"/>
+ <textbox id="headers-summary-url-value"
+ class="plain tabpanel-summary-value devtools-monospace cropped-textbox"
+ flex="1"
+ readonly="true"/>
+ </hbox>
+ <hbox id="headers-summary-method"
+ class="tabpanel-summary-container"
+ align="center">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=netmonitor.summary.method"/>
+ <label id="headers-summary-method-value"
+ class="plain tabpanel-summary-value devtools-monospace"
+ crop="end"
+ flex="1"/>
+ </hbox>
+ <hbox id="headers-summary-address"
+ class="tabpanel-summary-container"
+ align="center">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=netmonitor.summary.address"/>
+ <textbox id="headers-summary-address-value"
+ class="plain tabpanel-summary-value devtools-monospace cropped-textbox"
+ flex="1"
+ readonly="true"/>
+ </hbox>
+ <hbox id="headers-summary-status"
+ class="tabpanel-summary-container"
+ align="center">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=netmonitor.summary.status"/>
+ <box id="headers-summary-status-circle"
+ class="requests-menu-status-icon"/>
+ <label id="headers-summary-status-value"
+ class="plain tabpanel-summary-value devtools-monospace"
+ crop="end"
+ flex="1"/>
+ <button id="headers-summary-resend"
+ class="devtools-toolbarbutton"
+ data-localization="label=netmonitor.summary.editAndResend"/>
+ <button id="toggle-raw-headers"
+ class="devtools-toolbarbutton"
+ data-localization="label=netmonitor.summary.rawHeaders"/>
+ </hbox>
+ <hbox id="headers-summary-version"
+ class="tabpanel-summary-container"
+ align="center">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=netmonitor.summary.version"/>
+ <label id="headers-summary-version-value"
+ class="plain tabpanel-summary-value devtools-monospace"
+ crop="end"
+ flex="1"/>
+ </hbox>
+ <hbox id="raw-headers"
+ class="tabpanel-summary-container"
+ align="center"
+ hidden="true">
+ <vbox id="raw-request-headers-textarea-box" flex="1" hidden="false">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=netmonitor.summary.rawHeaders.requestHeaders"/>
+ <textbox id="raw-request-headers-textarea"
+ class="raw-response-textarea"
+ flex="1" multiline="true" readonly="true"/>
+ </vbox>
+ <vbox id="raw-response-headers-textarea-box" flex="1" hidden="false">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=netmonitor.summary.rawHeaders.responseHeaders"/>
+ <textbox id="raw-response-headers-textarea"
+ class="raw-response-textarea"
+ flex="1" multiline="true" readonly="true"/>
+ </vbox>
+ </hbox>
+ <vbox id="all-headers" flex="1"/>
+ </vbox>
+ </tabpanel>
+ <tabpanel id="cookies-tabpanel"
+ class="tabpanel-content">
+ <vbox flex="1">
+ <vbox id="all-cookies" flex="1"/>
+ </vbox>
+ </tabpanel>
+ <tabpanel id="params-tabpanel"
+ class="tabpanel-content">
+ <vbox flex="1">
+ <vbox id="request-params-box" flex="1" hidden="true">
+ <vbox id="request-params" flex="1"/>
+ </vbox>
+ <vbox id="request-post-data-textarea-box" flex="1" hidden="true">
+ <vbox id="request-post-data-textarea" flex="1"/>
+ </vbox>
+ </vbox>
+ </tabpanel>
+ <tabpanel id="response-tabpanel"
+ class="tabpanel-content">
+ <vbox flex="1">
+ <label id="response-content-info-header"/>
+ <vbox id="response-content-json-box" flex="1" hidden="true">
+ <vbox id="response-content-json" flex="1" context="network-response-popup" />
+ </vbox>
+ <vbox id="response-content-textarea-box" flex="1" hidden="true">
+ <vbox id="response-content-textarea" flex="1"/>
+ </vbox>
+ <vbox id="response-content-image-box" flex="1" hidden="true">
+ <image id="response-content-image"/>
+ <hbox>
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=netmonitor.response.name"/>
+ <label id="response-content-image-name-value"
+ class="plain tabpanel-summary-value devtools-monospace"
+ crop="end"
+ flex="1"/>
+ </hbox>
+ <hbox>
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=netmonitor.response.dimensions"/>
+ <label id="response-content-image-dimensions-value"
+ class="plain tabpanel-summary-value devtools-monospace"
+ crop="end"
+ flex="1"/>
+ </hbox>
+ <hbox>
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=netmonitor.response.mime"/>
+ <label id="response-content-image-mime-value"
+ class="plain tabpanel-summary-value devtools-monospace"
+ crop="end"
+ flex="1"/>
+ </hbox>
+ </vbox>
+ </vbox>
+ </tabpanel>
+ <tabpanel id="timings-tabpanel"
+ class="tabpanel-content">
+ <vbox flex="1">
+ <hbox id="timings-summary-blocked"
+ class="tabpanel-summary-container"
+ align="center">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=netmonitor.timings.blocked"/>
+ <hbox class="requests-menu-timings-box blocked"/>
+ <label class="plain requests-menu-timings-total"/>
+ </hbox>
+ <hbox id="timings-summary-dns"
+ class="tabpanel-summary-container"
+ align="center">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=netmonitor.timings.dns"/>
+ <hbox class="requests-menu-timings-box dns"/>
+ <label class="plain requests-menu-timings-total"/>
+ </hbox>
+ <hbox id="timings-summary-connect"
+ class="tabpanel-summary-container"
+ align="center">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=netmonitor.timings.connect"/>
+ <hbox class="requests-menu-timings-box connect"/>
+ <label class="plain requests-menu-timings-total"/>
+ </hbox>
+ <hbox id="timings-summary-send"
+ class="tabpanel-summary-container"
+ align="center">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=netmonitor.timings.send"/>
+ <hbox class="requests-menu-timings-box send"/>
+ <label class="plain requests-menu-timings-total"/>
+ </hbox>
+ <hbox id="timings-summary-wait"
+ class="tabpanel-summary-container"
+ align="center">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=netmonitor.timings.wait"/>
+ <hbox class="requests-menu-timings-box wait"/>
+ <label class="plain requests-menu-timings-total"/>
+ </hbox>
+ <hbox id="timings-summary-receive"
+ class="tabpanel-summary-container"
+ align="center">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=netmonitor.timings.receive"/>
+ <hbox class="requests-menu-timings-box receive"/>
+ <label class="plain requests-menu-timings-total"/>
+ </hbox>
+ </vbox>
+ </tabpanel>
+ <tabpanel id="security-tabpanel"
+ class="tabpanel-content">
+ <vbox id="security-error"
+ class="tabpanel-summary-container"
+ flex="1">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=netmonitor.security.error"/>
+ <hbox class="security-info-section"
+ flex="1">
+ <textbox id="security-error-message"
+ class="plain"
+ flex="1"
+ multiline="true"
+ readonly="true"/>
+ </hbox>
+ </vbox>
+ <vbox id="security-information"
+ flex="1">
+ <vbox id="security-info-connection"
+ class="tabpanel-summary-container">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=netmonitor.security.connection"/>
+ <vbox class="security-info-section">
+ <hbox id="security-protocol-version"
+ class="tabpanel-summary-container"
+ align="baseline">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=netmonitor.security.protocolVersion"/>
+ <textbox id="security-protocol-version-value"
+ class="plain tabpanel-summary-value devtools-monospace cropped-textbox"
+ flex="1"
+ readonly="true"/>
+ </hbox>
+ <hbox id="security-ciphersuite"
+ class="tabpanel-summary-container"
+ align="baseline">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=netmonitor.security.cipherSuite"/>
+ <textbox id="security-ciphersuite-value"
+ class="plain tabpanel-summary-value devtools-monospace cropped-textbox"
+ flex="1"
+ readonly="true"/>
+ <image class="security-warning-icon"
+ id="security-warning-cipher"
+ data-localization="tooltiptext=netmonitor.security.warning.cipher" />
+ </hbox>
+ </vbox>
+ </vbox>
+ <vbox id="security-info-domain"
+ class="tabpanel-summary-container">
+ <label class="plain tabpanel-summary-label"
+ id="security-info-host-header"/>
+ <vbox class="security-info-section">
+ <hbox id="security-http-strict-transport-security"
+ class="tabpanel-summary-container"
+ align="baseline">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=netmonitor.security.hsts"/>
+ <textbox id="security-http-strict-transport-security-value"
+ class="plain tabpanel-summary-value devtools-monospace cropped-textbox"
+ flex="1"
+ readonly="true"/>
+ </hbox>
+ <hbox id="security-public-key-pinning"
+ class="tabpanel-summary-container"
+ align="baseline">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=netmonitor.security.hpkp"/>
+ <textbox id="security-public-key-pinning-value"
+ class="plain tabpanel-summary-value devtools-monospace cropped-textbox"
+ flex="1"
+ readonly="true"/>
+ </hbox>
+ </vbox>
+ </vbox>
+ <vbox id="security-info-certificate"
+ class="tabpanel-summary-container">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=netmonitor.security.certificate"/>
+ <vbox class="security-info-section">
+ <vbox class="tabpanel-summary-container">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=certmgr.subjectinfo.label" flex="1"/>
+ </vbox>
+ <vbox class="security-info-section">
+ <hbox class="tabpanel-summary-container"
+ align="baseline">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=certmgr.certdetail.cn"/>
+ <textbox id="security-cert-subject-cn"
+ class="plain tabpanel-summary-value devtools-monospace cropped-textbox"
+ flex="1"
+ readonly="true"/>
+ </hbox>
+ <hbox class="tabpanel-summary-container"
+ align="baseline">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=certmgr.certdetail.o"/>
+ <textbox id="security-cert-subject-o"
+ class="plain tabpanel-summary-value devtools-monospace cropped-textbox"
+ flex="1"
+ readonly="true"/>
+ </hbox>
+ <hbox class="tabpanel-summary-container"
+ align="baseline">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=certmgr.certdetail.ou"/>
+ <textbox id="security-cert-subject-ou"
+ class="plain tabpanel-summary-value devtools-monospace cropped-textbox"
+ flex="1"
+ readonly="true"/>
+ </hbox>
+ </vbox>
+ <vbox class="tabpanel-summary-container">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=certmgr.issuerinfo.label"
+ flex="1"/>
+ </vbox>
+ <vbox class="security-info-section">
+ <hbox class="tabpanel-summary-container"
+ align="baseline">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=certmgr.certdetail.cn"/>
+ <textbox id="security-cert-issuer-cn"
+ class="plain tabpanel-summary-value devtools-monospace cropped-textbox"
+ flex="1"
+ readonly="true"/>
+ </hbox>
+ <hbox class="tabpanel-summary-container"
+ align="baseline">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=certmgr.certdetail.o"/>
+ <textbox id="security-cert-issuer-o"
+ class="plain tabpanel-summary-value devtools-monospace cropped-textbox"
+ flex="1"
+ readonly="true"/>
+ </hbox>
+ <hbox class="tabpanel-summary-container"
+ align="baseline">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=certmgr.certdetail.ou"/>
+ <textbox id="security-cert-issuer-ou"
+ class="plain tabpanel-summary-value devtools-monospace cropped-textbox"
+ flex="1"
+ readonly="true"/>
+ </hbox>
+ </vbox>
+ <vbox class="tabpanel-summary-container">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=certmgr.periodofvalidity.label"
+ flex="1"/>
+ </vbox>
+ <vbox class="security-info-section">
+ <hbox class="tabpanel-summary-container"
+ align="baseline">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=certmgr.begins"/>
+ <textbox id="security-cert-validity-begins"
+ class="plain tabpanel-summary-value devtools-monospace cropped-textbox"
+ flex="1"
+ readonly="true"/>
+ </hbox>
+ <hbox class="tabpanel-summary-container"
+ align="baseline">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=certmgr.expires"/>
+ <textbox id="security-cert-validity-expires"
+ class="plain tabpanel-summary-value devtools-monospace cropped-textbox"
+ flex="1"
+ readonly="true"/>
+ </hbox>
+ </vbox>
+ <vbox class="tabpanel-summary-container">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=certmgr.fingerprints.label"
+ flex="1"/>
+ </vbox>
+ <vbox class="security-info-section">
+ <hbox class="tabpanel-summary-container"
+ align="baseline">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=certmgr.certdetail.sha256fingerprint"/>
+ <textbox id="security-cert-sha256-fingerprint"
+ class="plain tabpanel-summary-value devtools-monospace cropped-textbox"
+ flex="1"
+ readonly="true"/>
+ </hbox>
+ <hbox class="tabpanel-summary-container"
+ align="baseline">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=certmgr.certdetail.sha1fingerprint"/>
+ <textbox id="security-cert-sha1-fingerprint"
+ class="plain tabpanel-summary-value devtools-monospace cropped-textbox"
+ flex="1"
+ readonly="true"/>
+ </hbox>
+ </vbox>
+ </vbox>
+ </vbox>
+ </vbox>
+ </tabpanel>
+ <tabpanel id="preview-tabpanel"
+ class="tabpanel-content">
+ <html:iframe id="response-preview"
+ frameborder="0"
+ sandbox=""/>
+ </tabpanel>
+ </tabpanels>
+ </tabbox>
+ </deck>
+ </hbox>
+
+ </vbox>
+
+ <box id="network-statistics-view">
+ <toolbar id="network-statistics-toolbar"
+ class="devtools-toolbar">
+ <button id="network-statistics-back-button"
+ class="devtools-toolbarbutton"
+ data-localization="label=netmonitor.backButton"/>
+ </toolbar>
+ <box id="network-statistics-charts"
+ class="devtools-responsive-container"
+ flex="1">
+ <vbox id="primed-cache-chart" pack="center" flex="1"/>
+ <splitter id="network-statistics-view-splitter"
+ class="devtools-side-splitter"/>
+ <vbox id="empty-cache-chart" pack="center" flex="1"/>
+ </box>
+ </box>
+
+ </deck>
+
+</window>
diff --git a/devtools/client/netmonitor/panel.js b/devtools/client/netmonitor/panel.js
new file mode 100644
index 000000000..5195e4178
--- /dev/null
+++ b/devtools/client/netmonitor/panel.js
@@ -0,0 +1,77 @@
+/* -*- 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 EventEmitter = require("devtools/shared/event-emitter");
+const { Task } = require("devtools/shared/task");
+const { localizeMarkup } = require("devtools/shared/l10n");
+
+function NetMonitorPanel(iframeWindow, toolbox) {
+ this.panelWin = iframeWindow;
+ this.panelDoc = iframeWindow.document;
+ this._toolbox = toolbox;
+
+ this._view = this.panelWin.NetMonitorView;
+ this._controller = this.panelWin.NetMonitorController;
+ this._controller._target = this.target;
+ this._controller._toolbox = this._toolbox;
+
+ EventEmitter.decorate(this);
+}
+
+exports.NetMonitorPanel = NetMonitorPanel;
+
+NetMonitorPanel.prototype = {
+ /**
+ * Open is effectively an asynchronous constructor.
+ *
+ * @return object
+ * A promise that is resolved when the NetMonitor completes opening.
+ */
+ open: Task.async(function* () {
+ if (this._opening) {
+ return this._opening;
+ }
+ // Localize all the nodes containing a data-localization attribute.
+ localizeMarkup(this.panelDoc);
+
+ let deferred = promise.defer();
+ this._opening = deferred.promise;
+
+ // Local monitoring needs to make the target remote.
+ if (!this.target.isRemote) {
+ yield this.target.makeRemote();
+ }
+
+ yield this._controller.startupNetMonitor();
+ this.isReady = true;
+ this.emit("ready");
+
+ deferred.resolve(this);
+ return this._opening;
+ }),
+
+ // DevToolPanel API
+
+ get target() {
+ return this._toolbox.target;
+ },
+
+ destroy: Task.async(function* () {
+ if (this._destroying) {
+ return this._destroying;
+ }
+ let deferred = promise.defer();
+ this._destroying = deferred.promise;
+
+ yield this._controller.shutdownNetMonitor();
+ this.emit("destroyed");
+
+ deferred.resolve();
+ return this._destroying;
+ })
+};
diff --git a/devtools/client/netmonitor/performance-statistics-view.js b/devtools/client/netmonitor/performance-statistics-view.js
new file mode 100644
index 000000000..c712c083d
--- /dev/null
+++ b/devtools/client/netmonitor/performance-statistics-view.js
@@ -0,0 +1,265 @@
+/* -*- 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/. */
+/* import-globals-from ./netmonitor-controller.js */
+/* globals $ */
+"use strict";
+
+const {PluralForm} = require("devtools/shared/plural-form");
+const {Filters} = require("./filter-predicates");
+const {L10N} = require("./l10n");
+const Actions = require("./actions/index");
+
+const REQUEST_TIME_DECIMALS = 2;
+const CONTENT_SIZE_DECIMALS = 2;
+
+// px
+const NETWORK_ANALYSIS_PIE_CHART_DIAMETER = 200;
+
+/**
+ * Functions handling the performance statistics view.
+ */
+function PerformanceStatisticsView() {
+}
+
+PerformanceStatisticsView.prototype = {
+ /**
+ * Initialization function, called when the debugger is started.
+ */
+ initialize: function (store) {
+ this.store = store;
+ },
+
+ /**
+ * Initializes and displays empty charts in this container.
+ */
+ displayPlaceholderCharts: function () {
+ this._createChart({
+ id: "#primed-cache-chart",
+ title: "charts.cacheEnabled"
+ });
+ this._createChart({
+ id: "#empty-cache-chart",
+ title: "charts.cacheDisabled"
+ });
+ window.emit(EVENTS.PLACEHOLDER_CHARTS_DISPLAYED);
+ },
+
+ /**
+ * Populates and displays the primed cache chart in this container.
+ *
+ * @param array items
+ * @see this._sanitizeChartDataSource
+ */
+ createPrimedCacheChart: function (items) {
+ this._createChart({
+ id: "#primed-cache-chart",
+ title: "charts.cacheEnabled",
+ data: this._sanitizeChartDataSource(items),
+ strings: this._commonChartStrings,
+ totals: this._commonChartTotals,
+ sorted: true
+ });
+ window.emit(EVENTS.PRIMED_CACHE_CHART_DISPLAYED);
+ },
+
+ /**
+ * Populates and displays the empty cache chart in this container.
+ *
+ * @param array items
+ * @see this._sanitizeChartDataSource
+ */
+ createEmptyCacheChart: function (items) {
+ this._createChart({
+ id: "#empty-cache-chart",
+ title: "charts.cacheDisabled",
+ data: this._sanitizeChartDataSource(items, true),
+ strings: this._commonChartStrings,
+ totals: this._commonChartTotals,
+ sorted: true
+ });
+ window.emit(EVENTS.EMPTY_CACHE_CHART_DISPLAYED);
+ },
+
+ /**
+ * Common stringifier predicates used for items and totals in both the
+ * "primed" and "empty" cache charts.
+ */
+ _commonChartStrings: {
+ size: value => {
+ let string = L10N.numberWithDecimals(value / 1024, CONTENT_SIZE_DECIMALS);
+ return L10N.getFormatStr("charts.sizeKB", string);
+ },
+ time: value => {
+ let string = L10N.numberWithDecimals(value / 1000, REQUEST_TIME_DECIMALS);
+ return L10N.getFormatStr("charts.totalS", string);
+ }
+ },
+ _commonChartTotals: {
+ size: total => {
+ let string = L10N.numberWithDecimals(total / 1024, CONTENT_SIZE_DECIMALS);
+ return L10N.getFormatStr("charts.totalSize", string);
+ },
+ time: total => {
+ let seconds = total / 1000;
+ let string = L10N.numberWithDecimals(seconds, REQUEST_TIME_DECIMALS);
+ return PluralForm.get(seconds,
+ L10N.getStr("charts.totalSeconds")).replace("#1", string);
+ },
+ cached: total => {
+ return L10N.getFormatStr("charts.totalCached", total);
+ },
+ count: total => {
+ return L10N.getFormatStr("charts.totalCount", total);
+ }
+ },
+
+ /**
+ * Adds a specific chart to this container.
+ *
+ * @param object
+ * An object containing all or some the following properties:
+ * - id: either "#primed-cache-chart" or "#empty-cache-chart"
+ * - title/data/strings/totals/sorted: @see Chart.jsm for details
+ */
+ _createChart: function ({ id, title, data, strings, totals, sorted }) {
+ let container = $(id);
+
+ // Nuke all existing charts of the specified type.
+ while (container.hasChildNodes()) {
+ container.firstChild.remove();
+ }
+
+ // Create a new chart.
+ let chart = Chart.PieTable(document, {
+ diameter: NETWORK_ANALYSIS_PIE_CHART_DIAMETER,
+ title: L10N.getStr(title),
+ data: data,
+ strings: strings,
+ totals: totals,
+ sorted: sorted
+ });
+
+ chart.on("click", (_, item) => {
+ // Reset FilterButtons and enable one filter exclusively
+ this.store.dispatch(Actions.enableFilterTypeOnly(item.label));
+ NetMonitorView.showNetworkInspectorView();
+ });
+
+ container.appendChild(chart.node);
+ },
+
+ /**
+ * Sanitizes the data source used for creating charts, to follow the
+ * data format spec defined in Chart.jsm.
+ *
+ * @param array items
+ * A collection of request items used as the data source for the chart.
+ * @param boolean emptyCache
+ * True if the cache is considered enabled, false for disabled.
+ */
+ _sanitizeChartDataSource: function (items, emptyCache) {
+ let data = [
+ "html", "css", "js", "xhr", "fonts", "images", "media", "flash", "ws", "other"
+ ].map(e => ({
+ cached: 0,
+ count: 0,
+ label: e,
+ size: 0,
+ time: 0
+ }));
+
+ for (let requestItem of items) {
+ let details = requestItem.attachment;
+ let type;
+
+ if (Filters.html(details)) {
+ // "html"
+ type = 0;
+ } else if (Filters.css(details)) {
+ // "css"
+ type = 1;
+ } else if (Filters.js(details)) {
+ // "js"
+ type = 2;
+ } else if (Filters.fonts(details)) {
+ // "fonts"
+ type = 4;
+ } else if (Filters.images(details)) {
+ // "images"
+ type = 5;
+ } else if (Filters.media(details)) {
+ // "media"
+ type = 6;
+ } else if (Filters.flash(details)) {
+ // "flash"
+ type = 7;
+ } else if (Filters.ws(details)) {
+ // "ws"
+ type = 8;
+ } else if (Filters.xhr(details)) {
+ // Verify XHR last, to categorize other mime types in their own blobs.
+ // "xhr"
+ type = 3;
+ } else {
+ // "other"
+ type = 9;
+ }
+
+ if (emptyCache || !responseIsFresh(details)) {
+ data[type].time += details.totalTime || 0;
+ data[type].size += details.contentSize || 0;
+ } else {
+ data[type].cached++;
+ }
+ data[type].count++;
+ }
+
+ return data.filter(e => e.count > 0);
+ },
+};
+
+/**
+ * Checks if the "Expiration Calculations" defined in section 13.2.4 of the
+ * "HTTP/1.1: Caching in HTTP" spec holds true for a collection of headers.
+ *
+ * @param object
+ * An object containing the { responseHeaders, status } properties.
+ * @return boolean
+ * True if the response is fresh and loaded from cache.
+ */
+function responseIsFresh({ responseHeaders, status }) {
+ // Check for a "304 Not Modified" status and response headers availability.
+ if (status != 304 || !responseHeaders) {
+ return false;
+ }
+
+ let list = responseHeaders.headers;
+ let cacheControl = list.filter(e => {
+ return e.name.toLowerCase() == "cache-control";
+ })[0];
+
+ let expires = list.filter(e => e.name.toLowerCase() == "expires")[0];
+
+ // Check the "Cache-Control" header for a maximum age value.
+ if (cacheControl) {
+ let maxAgeMatch =
+ cacheControl.value.match(/s-maxage\s*=\s*(\d+)/) ||
+ cacheControl.value.match(/max-age\s*=\s*(\d+)/);
+
+ if (maxAgeMatch && maxAgeMatch.pop() > 0) {
+ return true;
+ }
+ }
+
+ // Check the "Expires" header for a valid date.
+ if (expires && Date.parse(expires.value)) {
+ return true;
+ }
+
+ return false;
+}
+
+exports.PerformanceStatisticsView = PerformanceStatisticsView;
diff --git a/devtools/client/netmonitor/prefs.js b/devtools/client/netmonitor/prefs.js
new file mode 100644
index 000000000..6d4909d7c
--- /dev/null
+++ b/devtools/client/netmonitor/prefs.js
@@ -0,0 +1,14 @@
+"use strict";
+
+const {PrefsHelper} = require("devtools/client/shared/prefs");
+
+/**
+ * Shortcuts for accessing various network monitor preferences.
+ */
+
+exports.Prefs = new PrefsHelper("devtools.netmonitor", {
+ networkDetailsWidth: ["Int", "panes-network-details-width"],
+ networkDetailsHeight: ["Int", "panes-network-details-height"],
+ statistics: ["Bool", "statistics"],
+ filters: ["Json", "filters"]
+});
diff --git a/devtools/client/netmonitor/reducers/filters.js b/devtools/client/netmonitor/reducers/filters.js
new file mode 100644
index 000000000..cc81370d8
--- /dev/null
+++ b/devtools/client/netmonitor/reducers/filters.js
@@ -0,0 +1,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 I = require("devtools/client/shared/vendor/immutable");
+const {
+ TOGGLE_FILTER_TYPE,
+ ENABLE_FILTER_TYPE_ONLY,
+ SET_FILTER_TEXT,
+} = require("../constants");
+
+const FilterTypes = I.Record({
+ all: false,
+ html: false,
+ css: false,
+ js: false,
+ xhr: false,
+ fonts: false,
+ images: false,
+ media: false,
+ flash: false,
+ ws: false,
+ other: false,
+});
+
+const Filters = I.Record({
+ types: new FilterTypes({ all: true }),
+ url: "",
+});
+
+function toggleFilterType(state, action) {
+ let { filter } = action;
+ let newState;
+
+ // Ignore unknown filter type
+ if (!state.has(filter)) {
+ return state;
+ }
+ if (filter === "all") {
+ return new FilterTypes({ all: true });
+ }
+
+ newState = state.withMutations(types => {
+ types.set("all", false);
+ types.set(filter, !state.get(filter));
+ });
+
+ if (!newState.includes(true)) {
+ newState = new FilterTypes({ all: true });
+ }
+
+ return newState;
+}
+
+function enableFilterTypeOnly(state, action) {
+ let { filter } = action;
+
+ // Ignore unknown filter type
+ if (!state.has(filter)) {
+ return state;
+ }
+
+ return new FilterTypes({ [filter]: true });
+}
+
+function filters(state = new Filters(), action) {
+ switch (action.type) {
+ case TOGGLE_FILTER_TYPE:
+ return state.set("types", toggleFilterType(state.types, action));
+ case ENABLE_FILTER_TYPE_ONLY:
+ return state.set("types", enableFilterTypeOnly(state.types, action));
+ case SET_FILTER_TEXT:
+ return state.set("url", action.url);
+ default:
+ return state;
+ }
+}
+
+module.exports = filters;
diff --git a/devtools/client/netmonitor/reducers/index.js b/devtools/client/netmonitor/reducers/index.js
new file mode 100644
index 000000000..58638a030
--- /dev/null
+++ b/devtools/client/netmonitor/reducers/index.js
@@ -0,0 +1,13 @@
+/* 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 { combineReducers } = require("devtools/client/shared/vendor/redux");
+const filters = require("./filters");
+const sidebar = require("./sidebar");
+
+module.exports = combineReducers({
+ filters,
+ sidebar,
+});
diff --git a/devtools/client/netmonitor/reducers/moz.build b/devtools/client/netmonitor/reducers/moz.build
new file mode 100644
index 000000000..477cafb41
--- /dev/null
+++ b/devtools/client/netmonitor/reducers/moz.build
@@ -0,0 +1,10 @@
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'filters.js',
+ 'index.js',
+ 'sidebar.js',
+)
diff --git a/devtools/client/netmonitor/reducers/sidebar.js b/devtools/client/netmonitor/reducers/sidebar.js
new file mode 100644
index 000000000..eaa8b63df
--- /dev/null
+++ b/devtools/client/netmonitor/reducers/sidebar.js
@@ -0,0 +1,43 @@
+/* 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 I = require("devtools/client/shared/vendor/immutable");
+const {
+ DISABLE_TOGGLE_BUTTON,
+ SHOW_SIDEBAR,
+ TOGGLE_SIDEBAR,
+} = require("../constants");
+
+const SidebarState = I.Record({
+ toggleButtonDisabled: true,
+ visible: false,
+});
+
+function disableToggleButton(state, action) {
+ return state.set("toggleButtonDisabled", action.disabled);
+}
+
+function showSidebar(state, action) {
+ return state.set("visible", action.visible);
+}
+
+function toggleSidebar(state, action) {
+ return state.set("visible", !state.visible);
+}
+
+function sidebar(state = new SidebarState(), action) {
+ switch (action.type) {
+ case DISABLE_TOGGLE_BUTTON:
+ return disableToggleButton(state, action);
+ case SHOW_SIDEBAR:
+ return showSidebar(state, action);
+ case TOGGLE_SIDEBAR:
+ return toggleSidebar(state, action);
+ default:
+ return state;
+ }
+}
+
+module.exports = sidebar;
diff --git a/devtools/client/netmonitor/request-list-context-menu.js b/devtools/client/netmonitor/request-list-context-menu.js
new file mode 100644
index 000000000..215296265
--- /dev/null
+++ b/devtools/client/netmonitor/request-list-context-menu.js
@@ -0,0 +1,357 @@
+/* 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/. */
+
+/* globals NetMonitorController, NetMonitorView, gNetwork */
+
+"use strict";
+
+const Services = require("Services");
+const { Task } = require("devtools/shared/task");
+const { Curl } = require("devtools/client/shared/curl");
+const { gDevTools } = require("devtools/client/framework/devtools");
+const Menu = require("devtools/client/framework/menu");
+const MenuItem = require("devtools/client/framework/menu-item");
+const { L10N } = require("./l10n");
+const { formDataURI, getFormDataSections } = require("./request-utils");
+
+loader.lazyRequireGetter(this, "HarExporter",
+ "devtools/client/netmonitor/har/har-exporter", true);
+
+loader.lazyServiceGetter(this, "clipboardHelper",
+ "@mozilla.org/widget/clipboardhelper;1", "nsIClipboardHelper");
+
+loader.lazyRequireGetter(this, "NetworkHelper",
+ "devtools/shared/webconsole/network-helper");
+
+function RequestListContextMenu() {}
+
+RequestListContextMenu.prototype = {
+ get selectedItem() {
+ return NetMonitorView.RequestsMenu.selectedItem;
+ },
+
+ get items() {
+ return NetMonitorView.RequestsMenu.items;
+ },
+
+ /**
+ * Handle the context menu opening. Hide items if no request is selected.
+ * Since visible attribute only accept boolean value but the method call may
+ * return undefined, we use !! to force convert any object to boolean
+ */
+ open({ screenX = 0, screenY = 0 } = {}) {
+ let selectedItem = this.selectedItem;
+
+ let menu = new Menu();
+ menu.append(new MenuItem({
+ id: "request-menu-context-copy-url",
+ label: L10N.getStr("netmonitor.context.copyUrl"),
+ accesskey: L10N.getStr("netmonitor.context.copyUrl.accesskey"),
+ visible: !!selectedItem,
+ click: () => this.copyUrl(),
+ }));
+
+ menu.append(new MenuItem({
+ id: "request-menu-context-copy-url-params",
+ label: L10N.getStr("netmonitor.context.copyUrlParams"),
+ accesskey: L10N.getStr("netmonitor.context.copyUrlParams.accesskey"),
+ visible: !!(selectedItem &&
+ NetworkHelper.nsIURL(selectedItem.attachment.url).query),
+ click: () => this.copyUrlParams(),
+ }));
+
+ menu.append(new MenuItem({
+ id: "request-menu-context-copy-post-data",
+ label: L10N.getStr("netmonitor.context.copyPostData"),
+ accesskey: L10N.getStr("netmonitor.context.copyPostData.accesskey"),
+ visible: !!(selectedItem && selectedItem.attachment.requestPostData),
+ click: () => this.copyPostData(),
+ }));
+
+ menu.append(new MenuItem({
+ id: "request-menu-context-copy-as-curl",
+ label: L10N.getStr("netmonitor.context.copyAsCurl"),
+ accesskey: L10N.getStr("netmonitor.context.copyAsCurl.accesskey"),
+ visible: !!(selectedItem && selectedItem.attachment),
+ click: () => this.copyAsCurl(),
+ }));
+
+ menu.append(new MenuItem({
+ type: "separator",
+ visible: !!selectedItem,
+ }));
+
+ menu.append(new MenuItem({
+ id: "request-menu-context-copy-request-headers",
+ label: L10N.getStr("netmonitor.context.copyRequestHeaders"),
+ accesskey: L10N.getStr("netmonitor.context.copyRequestHeaders.accesskey"),
+ visible: !!(selectedItem && selectedItem.attachment.requestHeaders),
+ click: () => this.copyRequestHeaders(),
+ }));
+
+ menu.append(new MenuItem({
+ id: "response-menu-context-copy-response-headers",
+ label: L10N.getStr("netmonitor.context.copyResponseHeaders"),
+ accesskey: L10N.getStr("netmonitor.context.copyResponseHeaders.accesskey"),
+ visible: !!(selectedItem && selectedItem.attachment.responseHeaders),
+ click: () => this.copyResponseHeaders(),
+ }));
+
+ menu.append(new MenuItem({
+ id: "request-menu-context-copy-response",
+ label: L10N.getStr("netmonitor.context.copyResponse"),
+ accesskey: L10N.getStr("netmonitor.context.copyResponse.accesskey"),
+ visible: !!(selectedItem &&
+ selectedItem.attachment.responseContent &&
+ selectedItem.attachment.responseContent.content.text &&
+ selectedItem.attachment.responseContent.content.text.length !== 0),
+ click: () => this.copyResponse(),
+ }));
+
+ menu.append(new MenuItem({
+ id: "request-menu-context-copy-image-as-data-uri",
+ label: L10N.getStr("netmonitor.context.copyImageAsDataUri"),
+ accesskey: L10N.getStr("netmonitor.context.copyImageAsDataUri.accesskey"),
+ visible: !!(selectedItem &&
+ selectedItem.attachment.responseContent &&
+ selectedItem.attachment.responseContent.content
+ .mimeType.includes("image/")),
+ click: () => this.copyImageAsDataUri(),
+ }));
+
+ menu.append(new MenuItem({
+ type: "separator",
+ visible: !!selectedItem,
+ }));
+
+ menu.append(new MenuItem({
+ id: "request-menu-context-copy-all-as-har",
+ label: L10N.getStr("netmonitor.context.copyAllAsHar"),
+ accesskey: L10N.getStr("netmonitor.context.copyAllAsHar.accesskey"),
+ visible: !!this.items.length,
+ click: () => this.copyAllAsHar(),
+ }));
+
+ menu.append(new MenuItem({
+ id: "request-menu-context-save-all-as-har",
+ label: L10N.getStr("netmonitor.context.saveAllAsHar"),
+ accesskey: L10N.getStr("netmonitor.context.saveAllAsHar.accesskey"),
+ visible: !!this.items.length,
+ click: () => this.saveAllAsHar(),
+ }));
+
+ menu.append(new MenuItem({
+ type: "separator",
+ visible: !!selectedItem,
+ }));
+
+ menu.append(new MenuItem({
+ id: "request-menu-context-resend",
+ label: L10N.getStr("netmonitor.context.editAndResend"),
+ accesskey: L10N.getStr("netmonitor.context.editAndResend.accesskey"),
+ visible: !!(NetMonitorController.supportsCustomRequest &&
+ selectedItem &&
+ !selectedItem.attachment.isCustom),
+ click: () => NetMonitorView.RequestsMenu.cloneSelectedRequest(),
+ }));
+
+ menu.append(new MenuItem({
+ type: "separator",
+ visible: !!selectedItem,
+ }));
+
+ menu.append(new MenuItem({
+ id: "request-menu-context-newtab",
+ label: L10N.getStr("netmonitor.context.newTab"),
+ accesskey: L10N.getStr("netmonitor.context.newTab.accesskey"),
+ visible: !!selectedItem,
+ click: () => this.openRequestInTab()
+ }));
+
+ menu.append(new MenuItem({
+ id: "request-menu-context-perf",
+ label: L10N.getStr("netmonitor.context.perfTools"),
+ accesskey: L10N.getStr("netmonitor.context.perfTools.accesskey"),
+ visible: !!NetMonitorController.supportsPerfStats,
+ click: () => NetMonitorView.toggleFrontendMode()
+ }));
+
+ menu.popup(screenX, screenY, NetMonitorController._toolbox);
+ return menu;
+ },
+
+ /**
+ * Opens selected item in a new tab.
+ */
+ openRequestInTab() {
+ let win = Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
+ let { url } = this.selectedItem.attachment;
+ win.openUILinkIn(url, "tab", { relatedToCurrent: true });
+ },
+
+ /**
+ * Copy the request url from the currently selected item.
+ */
+ copyUrl() {
+ clipboardHelper.copyString(this.selectedItem.attachment.url);
+ },
+
+ /**
+ * Copy the request url query string parameters from the currently
+ * selected item.
+ */
+ copyUrlParams() {
+ let { url } = this.selectedItem.attachment;
+ let params = NetworkHelper.nsIURL(url).query.split("&");
+ let string = params.join(Services.appinfo.OS === "WINNT" ? "\r\n" : "\n");
+ clipboardHelper.copyString(string);
+ },
+
+ /**
+ * Copy the request form data parameters (or raw payload) from
+ * the currently selected item.
+ */
+ copyPostData: Task.async(function* () {
+ let selected = this.selectedItem.attachment;
+
+ // Try to extract any form data parameters.
+ let formDataSections = yield getFormDataSections(
+ selected.requestHeaders,
+ selected.requestHeadersFromUploadStream,
+ selected.requestPostData,
+ gNetwork.getString.bind(gNetwork));
+
+ let params = [];
+ formDataSections.forEach(section => {
+ let paramsArray = NetworkHelper.parseQueryString(section);
+ if (paramsArray) {
+ params = [...params, ...paramsArray];
+ }
+ });
+
+ let string = params
+ .map(param => param.name + (param.value ? "=" + param.value : ""))
+ .join(Services.appinfo.OS === "WINNT" ? "\r\n" : "\n");
+
+ // Fall back to raw payload.
+ if (!string) {
+ let postData = selected.requestPostData.postData.text;
+ string = yield gNetwork.getString(postData);
+ if (Services.appinfo.OS !== "WINNT") {
+ string = string.replace(/\r/g, "");
+ }
+ }
+
+ clipboardHelper.copyString(string);
+ }),
+
+ /**
+ * Copy a cURL command from the currently selected item.
+ */
+ copyAsCurl: Task.async(function* () {
+ let selected = this.selectedItem.attachment;
+
+ // Create a sanitized object for the Curl command generator.
+ let data = {
+ url: selected.url,
+ method: selected.method,
+ headers: [],
+ httpVersion: selected.httpVersion,
+ postDataText: null
+ };
+
+ // Fetch header values.
+ for (let { name, value } of selected.requestHeaders.headers) {
+ let text = yield gNetwork.getString(value);
+ data.headers.push({ name: name, value: text });
+ }
+
+ // Fetch the request payload.
+ if (selected.requestPostData) {
+ let postData = selected.requestPostData.postData.text;
+ data.postDataText = yield gNetwork.getString(postData);
+ }
+
+ clipboardHelper.copyString(Curl.generateCommand(data));
+ }),
+
+ /**
+ * Copy the raw request headers from the currently selected item.
+ */
+ copyRequestHeaders() {
+ let selected = this.selectedItem.attachment;
+ let rawHeaders = selected.requestHeaders.rawHeaders.trim();
+ if (Services.appinfo.OS !== "WINNT") {
+ rawHeaders = rawHeaders.replace(/\r/g, "");
+ }
+ clipboardHelper.copyString(rawHeaders);
+ },
+
+ /**
+ * Copy the raw response headers from the currently selected item.
+ */
+ copyResponseHeaders() {
+ let selected = this.selectedItem.attachment;
+ let rawHeaders = selected.responseHeaders.rawHeaders.trim();
+ if (Services.appinfo.OS !== "WINNT") {
+ rawHeaders = rawHeaders.replace(/\r/g, "");
+ }
+ clipboardHelper.copyString(rawHeaders);
+ },
+
+ /**
+ * Copy image as data uri.
+ */
+ copyImageAsDataUri() {
+ let selected = this.selectedItem.attachment;
+ let { mimeType, text, encoding } = selected.responseContent.content;
+
+ gNetwork.getString(text).then(string => {
+ let data = formDataURI(mimeType, encoding, string);
+ clipboardHelper.copyString(data);
+ });
+ },
+
+ /**
+ * Copy response data as a string.
+ */
+ copyResponse() {
+ let selected = this.selectedItem.attachment;
+ let text = selected.responseContent.content.text;
+
+ gNetwork.getString(text).then(string => {
+ clipboardHelper.copyString(string);
+ });
+ },
+
+ /**
+ * Copy HAR from the network panel content to the clipboard.
+ */
+ copyAllAsHar() {
+ let options = this.getDefaultHarOptions();
+ return HarExporter.copy(options);
+ },
+
+ /**
+ * Save HAR from the network panel content to a file.
+ */
+ saveAllAsHar() {
+ let options = this.getDefaultHarOptions();
+ return HarExporter.save(options);
+ },
+
+ getDefaultHarOptions() {
+ let form = NetMonitorController._target.form;
+ let title = form.title || form.url;
+
+ return {
+ getString: gNetwork.getString.bind(gNetwork),
+ view: NetMonitorView.RequestsMenu,
+ items: NetMonitorView.RequestsMenu.items,
+ title: title
+ };
+ }
+};
+
+module.exports = RequestListContextMenu;
diff --git a/devtools/client/netmonitor/request-utils.js b/devtools/client/netmonitor/request-utils.js
new file mode 100644
index 000000000..ba54efb4f
--- /dev/null
+++ b/devtools/client/netmonitor/request-utils.js
@@ -0,0 +1,185 @@
+"use strict";
+/* eslint-disable mozilla/reject-some-requires */
+const { Ci } = require("chrome");
+const { KeyCodes } = require("devtools/client/shared/keycodes");
+const { Task } = require("devtools/shared/task");
+const NetworkHelper = require("devtools/shared/webconsole/network-helper");
+
+/**
+ * Helper method to get a wrapped function which can be bound to as
+ * an event listener directly and is executed only when data-key is
+ * present in event.target.
+ *
+ * @param function callback
+ * Function to execute execute when data-key
+ * is present in event.target.
+ * @param bool onlySpaceOrReturn
+ * Flag to indicate if callback should only be called
+ when the space or return button is pressed
+ * @return function
+ * Wrapped function with the target data-key as the first argument
+ * and the event as the second argument.
+ */
+exports.getKeyWithEvent = function (callback, onlySpaceOrReturn) {
+ return function (event) {
+ let key = event.target.getAttribute("data-key");
+ let filterKeyboardEvent = !onlySpaceOrReturn ||
+ event.keyCode === KeyCodes.DOM_VK_SPACE ||
+ event.keyCode === KeyCodes.DOM_VK_RETURN;
+
+ if (key && filterKeyboardEvent) {
+ callback.call(null, key);
+ }
+ };
+};
+
+/**
+ * Extracts any urlencoded form data sections (e.g. "?foo=bar&baz=42") from a
+ * POST request.
+ *
+ * @param object headers
+ * The "requestHeaders".
+ * @param object uploadHeaders
+ * The "requestHeadersFromUploadStream".
+ * @param object postData
+ * The "requestPostData".
+ * @param object getString
+ Callback to retrieve a string from a LongStringGrip.
+ * @return array
+ * A promise that is resolved with the extracted form data.
+ */
+exports.getFormDataSections = Task.async(function* (headers, uploadHeaders, postData,
+ getString) {
+ let formDataSections = [];
+
+ let { headers: requestHeaders } = headers;
+ let { headers: payloadHeaders } = uploadHeaders;
+ let allHeaders = [...payloadHeaders, ...requestHeaders];
+
+ let contentTypeHeader = allHeaders.find(e => {
+ return e.name.toLowerCase() == "content-type";
+ });
+
+ let contentTypeLongString = contentTypeHeader ? contentTypeHeader.value : "";
+
+ let contentType = yield getString(contentTypeLongString);
+
+ if (contentType.includes("x-www-form-urlencoded")) {
+ let postDataLongString = postData.postData.text;
+ let text = yield getString(postDataLongString);
+
+ for (let section of text.split(/\r\n|\r|\n/)) {
+ // Before displaying it, make sure this section of the POST data
+ // isn't a line containing upload stream headers.
+ if (payloadHeaders.every(header => !section.startsWith(header.name))) {
+ formDataSections.push(section);
+ }
+ }
+ }
+
+ return formDataSections;
+});
+
+/**
+ * Form a data: URI given a mime type, encoding, and some text.
+ *
+ * @param {String} mimeType the mime type
+ * @param {String} encoding the encoding to use; if not set, the
+ * text will be base64-encoded.
+ * @param {String} text the text of the URI.
+ * @return {String} a data: URI
+ */
+exports.formDataURI = function (mimeType, encoding, text) {
+ if (!encoding) {
+ encoding = "base64";
+ text = btoa(text);
+ }
+ return "data:" + mimeType + ";" + encoding + "," + text;
+};
+
+/**
+ * Write out a list of headers into a chunk of text
+ *
+ * @param array headers
+ * Array of headers info {name, value}
+ * @return string text
+ * List of headers in text format
+ */
+exports.writeHeaderText = function (headers) {
+ return headers.map(({name, value}) => name + ": " + value).join("\n");
+};
+
+/**
+ * Helper for getting an abbreviated string for a mime type.
+ *
+ * @param string mimeType
+ * @return string
+ */
+exports.getAbbreviatedMimeType = function (mimeType) {
+ if (!mimeType) {
+ return "";
+ }
+ return (mimeType.split(";")[0].split("/")[1] || "").split("+")[0];
+};
+
+/**
+ * Helpers for getting details about an nsIURL.
+ *
+ * @param nsIURL | string url
+ * @return string
+ */
+exports.getUriNameWithQuery = function (url) {
+ if (!(url instanceof Ci.nsIURL)) {
+ url = NetworkHelper.nsIURL(url);
+ }
+
+ let name = NetworkHelper.convertToUnicode(
+ unescape(url.fileName || url.filePath || "/"));
+ let query = NetworkHelper.convertToUnicode(unescape(url.query));
+
+ return name + (query ? "?" + query : "");
+};
+
+exports.getUriHostPort = function (url) {
+ if (!(url instanceof Ci.nsIURL)) {
+ url = NetworkHelper.nsIURL(url);
+ }
+ return NetworkHelper.convertToUnicode(unescape(url.hostPort));
+};
+
+exports.getUriHost = function (url) {
+ return exports.getUriHostPort(url).replace(/:\d+$/, "");
+};
+
+/**
+ * Convert a nsIContentPolicy constant to a display string
+ */
+const LOAD_CAUSE_STRINGS = {
+ [Ci.nsIContentPolicy.TYPE_INVALID]: "invalid",
+ [Ci.nsIContentPolicy.TYPE_OTHER]: "other",
+ [Ci.nsIContentPolicy.TYPE_SCRIPT]: "script",
+ [Ci.nsIContentPolicy.TYPE_IMAGE]: "img",
+ [Ci.nsIContentPolicy.TYPE_STYLESHEET]: "stylesheet",
+ [Ci.nsIContentPolicy.TYPE_OBJECT]: "object",
+ [Ci.nsIContentPolicy.TYPE_DOCUMENT]: "document",
+ [Ci.nsIContentPolicy.TYPE_SUBDOCUMENT]: "subdocument",
+ [Ci.nsIContentPolicy.TYPE_REFRESH]: "refresh",
+ [Ci.nsIContentPolicy.TYPE_XBL]: "xbl",
+ [Ci.nsIContentPolicy.TYPE_PING]: "ping",
+ [Ci.nsIContentPolicy.TYPE_XMLHTTPREQUEST]: "xhr",
+ [Ci.nsIContentPolicy.TYPE_OBJECT_SUBREQUEST]: "objectSubdoc",
+ [Ci.nsIContentPolicy.TYPE_DTD]: "dtd",
+ [Ci.nsIContentPolicy.TYPE_FONT]: "font",
+ [Ci.nsIContentPolicy.TYPE_MEDIA]: "media",
+ [Ci.nsIContentPolicy.TYPE_WEBSOCKET]: "websocket",
+ [Ci.nsIContentPolicy.TYPE_CSP_REPORT]: "csp",
+ [Ci.nsIContentPolicy.TYPE_XSLT]: "xslt",
+ [Ci.nsIContentPolicy.TYPE_BEACON]: "beacon",
+ [Ci.nsIContentPolicy.TYPE_FETCH]: "fetch",
+ [Ci.nsIContentPolicy.TYPE_IMAGESET]: "imageset",
+ [Ci.nsIContentPolicy.TYPE_WEB_MANIFEST]: "webManifest"
+};
+
+exports.loadCauseString = function (causeType) {
+ return LOAD_CAUSE_STRINGS[causeType] || "unknown";
+};
diff --git a/devtools/client/netmonitor/requests-menu-view.js b/devtools/client/netmonitor/requests-menu-view.js
new file mode 100644
index 000000000..6ea6381ec
--- /dev/null
+++ b/devtools/client/netmonitor/requests-menu-view.js
@@ -0,0 +1,1649 @@
+/* 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/. */
+
+/* globals document, window, dumpn, $, gNetwork, EVENTS, Prefs,
+ NetMonitorController, NetMonitorView */
+
+"use strict";
+
+/* eslint-disable mozilla/reject-some-requires */
+const { Cu } = require("chrome");
+const {Task} = require("devtools/shared/task");
+const {DeferredTask} = Cu.import("resource://gre/modules/DeferredTask.jsm", {});
+/* eslint-disable mozilla/reject-some-requires */
+const {SideMenuWidget} = require("resource://devtools/client/shared/widgets/SideMenuWidget.jsm");
+const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+const {setImageTooltip, getImageDimensions} =
+ require("devtools/client/shared/widgets/tooltip/ImageTooltipHelper");
+const {Heritage, WidgetMethods, setNamedTimeout} =
+ require("devtools/client/shared/widgets/view-helpers");
+const {CurlUtils} = require("devtools/client/shared/curl");
+const {PluralForm} = require("devtools/shared/plural-form");
+const {Filters, isFreetextMatch} = require("./filter-predicates");
+const {Sorters} = require("./sort-predicates");
+const {L10N, WEBCONSOLE_L10N} = require("./l10n");
+const {formDataURI,
+ writeHeaderText,
+ getKeyWithEvent,
+ getAbbreviatedMimeType,
+ getUriNameWithQuery,
+ getUriHostPort,
+ getUriHost,
+ loadCauseString} = require("./request-utils");
+const Actions = require("./actions/index");
+const RequestListContextMenu = require("./request-list-context-menu");
+
+loader.lazyRequireGetter(this, "NetworkHelper",
+ "devtools/shared/webconsole/network-helper");
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const EPSILON = 0.001;
+// ms
+const RESIZE_REFRESH_RATE = 50;
+// ms
+const REQUESTS_REFRESH_RATE = 50;
+// tooltip show/hide delay in ms
+const REQUESTS_TOOLTIP_TOGGLE_DELAY = 500;
+// px
+const REQUESTS_TOOLTIP_IMAGE_MAX_DIM = 400;
+// px
+const REQUESTS_TOOLTIP_STACK_TRACE_WIDTH = 600;
+// px
+const REQUESTS_WATERFALL_SAFE_BOUNDS = 90;
+// ms
+const REQUESTS_WATERFALL_HEADER_TICKS_MULTIPLE = 5;
+// px
+const REQUESTS_WATERFALL_HEADER_TICKS_SPACING_MIN = 60;
+// ms
+const REQUESTS_WATERFALL_BACKGROUND_TICKS_MULTIPLE = 5;
+const REQUESTS_WATERFALL_BACKGROUND_TICKS_SCALES = 3;
+// px
+const REQUESTS_WATERFALL_BACKGROUND_TICKS_SPACING_MIN = 10;
+const REQUESTS_WATERFALL_BACKGROUND_TICKS_COLOR_RGB = [128, 136, 144];
+const REQUESTS_WATERFALL_BACKGROUND_TICKS_OPACITY_MIN = 32;
+// byte
+const REQUESTS_WATERFALL_BACKGROUND_TICKS_OPACITY_ADD = 32;
+const REQUESTS_WATERFALL_DOMCONTENTLOADED_TICKS_COLOR_RGBA = [255, 0, 0, 128];
+const REQUESTS_WATERFALL_LOAD_TICKS_COLOR_RGBA = [0, 0, 255, 128];
+
+// Constants for formatting bytes.
+const BYTES_IN_KB = 1024;
+const BYTES_IN_MB = Math.pow(BYTES_IN_KB, 2);
+const BYTES_IN_GB = Math.pow(BYTES_IN_KB, 3);
+const MAX_BYTES_SIZE = 1000;
+const MAX_KB_SIZE = 1000 * BYTES_IN_KB;
+const MAX_MB_SIZE = 1000 * BYTES_IN_MB;
+
+// TODO: duplicated from netmonitor-view.js. Move to a format-utils.js module.
+const REQUEST_TIME_DECIMALS = 2;
+const CONTENT_SIZE_DECIMALS = 2;
+
+const CONTENT_MIME_TYPE_ABBREVIATIONS = {
+ "ecmascript": "js",
+ "javascript": "js",
+ "x-javascript": "js"
+};
+
+// A smart store watcher to notify store changes as necessary
+function storeWatcher(initialValue, reduceValue, onChange) {
+ let currentValue = initialValue;
+
+ return () => {
+ const newValue = reduceValue(currentValue);
+ if (newValue !== currentValue) {
+ onChange(newValue, currentValue);
+ currentValue = newValue;
+ }
+ };
+}
+
+/**
+ * Functions handling the requests menu (containing details about each request,
+ * like status, method, file, domain, as well as a waterfall representing
+ * timing imformation).
+ */
+function RequestsMenuView() {
+ dumpn("RequestsMenuView was instantiated");
+
+ this._flushRequests = this._flushRequests.bind(this);
+ this._onHover = this._onHover.bind(this);
+ this._onSelect = this._onSelect.bind(this);
+ this._onSwap = this._onSwap.bind(this);
+ this._onResize = this._onResize.bind(this);
+ this._onScroll = this._onScroll.bind(this);
+ this._onSecurityIconClick = this._onSecurityIconClick.bind(this);
+}
+
+RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
+ /**
+ * Initialization function, called when the network monitor is started.
+ */
+ initialize: function (store) {
+ dumpn("Initializing the RequestsMenuView");
+
+ this.store = store;
+
+ this.contextMenu = new RequestListContextMenu();
+
+ let widgetParentEl = $("#requests-menu-contents");
+ this.widget = new SideMenuWidget(widgetParentEl);
+ this._splitter = $("#network-inspector-view-splitter");
+ this._summary = $("#requests-menu-network-summary-button");
+ this._summary.setAttribute("label", L10N.getStr("networkMenu.empty"));
+
+ // Create a tooltip for the newly appended network request item.
+ this.tooltip = new HTMLTooltip(NetMonitorController._toolbox.doc, { type: "arrow" });
+ this.tooltip.startTogglingOnHover(widgetParentEl, this._onHover, {
+ toggleDelay: REQUESTS_TOOLTIP_TOGGLE_DELAY,
+ interactive: true
+ });
+
+ this.sortContents((a, b) => Sorters.waterfall(a.attachment, b.attachment));
+
+ this.allowFocusOnRightClick = true;
+ this.maintainSelectionVisible = true;
+
+ this.widget.addEventListener("select", this._onSelect, false);
+ this.widget.addEventListener("swap", this._onSwap, false);
+ this._splitter.addEventListener("mousemove", this._onResize, false);
+ window.addEventListener("resize", this._onResize, false);
+
+ this.requestsMenuSortEvent = getKeyWithEvent(this.sortBy.bind(this));
+ this.requestsMenuSortKeyboardEvent = getKeyWithEvent(this.sortBy.bind(this), true);
+ this._onContextMenu = this._onContextMenu.bind(this);
+ this._onContextPerfCommand = () => NetMonitorView.toggleFrontendMode();
+ this._onReloadCommand = () => NetMonitorView.reloadPage();
+ this._flushRequestsTask = new DeferredTask(this._flushRequests,
+ REQUESTS_REFRESH_RATE);
+
+ this.sendCustomRequestEvent = this.sendCustomRequest.bind(this);
+ this.closeCustomRequestEvent = this.closeCustomRequest.bind(this);
+ this.cloneSelectedRequestEvent = this.cloneSelectedRequest.bind(this);
+ this.toggleRawHeadersEvent = this.toggleRawHeaders.bind(this);
+
+ this.reFilterRequests = this.reFilterRequests.bind(this);
+
+ $("#toolbar-labels").addEventListener("click",
+ this.requestsMenuSortEvent, false);
+ $("#toolbar-labels").addEventListener("keydown",
+ this.requestsMenuSortKeyboardEvent, false);
+ $("#toggle-raw-headers").addEventListener("click",
+ this.toggleRawHeadersEvent, false);
+ $("#requests-menu-contents").addEventListener("scroll", this._onScroll, true);
+ $("#requests-menu-contents").addEventListener("contextmenu", this._onContextMenu);
+
+ this.unsubscribeStore = store.subscribe(storeWatcher(
+ null,
+ () => store.getState().filters,
+ (newFilters) => {
+ this._activeFilters = newFilters.types
+ .toSeq()
+ .filter((checked, key) => checked)
+ .keySeq()
+ .toArray();
+ this._currentFreetextFilter = newFilters.url;
+ this.reFilterRequests();
+ }
+ ));
+
+ Prefs.filters.forEach(type =>
+ store.dispatch(Actions.toggleFilterType(type)));
+
+ window.once("connected", this._onConnect.bind(this));
+ },
+
+ _onConnect: function () {
+ $("#requests-menu-reload-notice-button").addEventListener("command",
+ this._onReloadCommand, false);
+
+ if (NetMonitorController.supportsCustomRequest) {
+ $("#custom-request-send-button").addEventListener("click",
+ this.sendCustomRequestEvent, false);
+ $("#custom-request-close-button").addEventListener("click",
+ this.closeCustomRequestEvent, false);
+ $("#headers-summary-resend").addEventListener("click",
+ this.cloneSelectedRequestEvent, false);
+ } else {
+ $("#headers-summary-resend").hidden = true;
+ }
+
+ if (NetMonitorController.supportsPerfStats) {
+ $("#requests-menu-perf-notice-button").addEventListener("command",
+ this._onContextPerfCommand, false);
+ $("#requests-menu-network-summary-button").addEventListener("command",
+ this._onContextPerfCommand, false);
+ $("#network-statistics-back-button").addEventListener("command",
+ this._onContextPerfCommand, false);
+ } else {
+ $("#notice-perf-message").hidden = true;
+ $("#requests-menu-network-summary-button").hidden = true;
+ }
+
+ if (!NetMonitorController.supportsTransferredResponseSize) {
+ $("#requests-menu-transferred-header-box").hidden = true;
+ $("#requests-menu-item-template .requests-menu-transferred")
+ .hidden = true;
+ }
+ },
+
+ /**
+ * Destruction function, called when the network monitor is closed.
+ */
+ destroy: function () {
+ dumpn("Destroying the RequestsMenuView");
+
+ Prefs.filters = this._activeFilters;
+
+ /* Destroy the tooltip */
+ this.tooltip.stopTogglingOnHover();
+ this.tooltip.destroy();
+ $("#requests-menu-contents").removeEventListener("scroll", this._onScroll, true);
+ $("#requests-menu-contents").removeEventListener("contextmenu", this._onContextMenu);
+
+ this.widget.removeEventListener("select", this._onSelect, false);
+ this.widget.removeEventListener("swap", this._onSwap, false);
+ this._splitter.removeEventListener("mousemove", this._onResize, false);
+ window.removeEventListener("resize", this._onResize, false);
+
+ $("#toolbar-labels").removeEventListener("click",
+ this.requestsMenuSortEvent, false);
+ $("#toolbar-labels").removeEventListener("keydown",
+ this.requestsMenuSortKeyboardEvent, false);
+
+ this._flushRequestsTask.disarm();
+
+ $("#requests-menu-reload-notice-button").removeEventListener("command",
+ this._onReloadCommand, false);
+ $("#requests-menu-perf-notice-button").removeEventListener("command",
+ this._onContextPerfCommand, false);
+ $("#requests-menu-network-summary-button").removeEventListener("command",
+ this._onContextPerfCommand, false);
+ $("#network-statistics-back-button").removeEventListener("command",
+ this._onContextPerfCommand, false);
+
+ $("#custom-request-send-button").removeEventListener("click",
+ this.sendCustomRequestEvent, false);
+ $("#custom-request-close-button").removeEventListener("click",
+ this.closeCustomRequestEvent, false);
+ $("#headers-summary-resend").removeEventListener("click",
+ this.cloneSelectedRequestEvent, false);
+ $("#toggle-raw-headers").removeEventListener("click",
+ this.toggleRawHeadersEvent, false);
+
+ this.unsubscribeStore();
+ },
+
+ /**
+ * Resets this container (removes all the networking information).
+ */
+ reset: function () {
+ this.empty();
+ this._addQueue = [];
+ this._updateQueue = [];
+ this._firstRequestStartedMillis = -1;
+ this._lastRequestEndedMillis = -1;
+ },
+
+ /**
+ * Specifies if this view may be updated lazily.
+ */
+ _lazyUpdate: true,
+
+ get lazyUpdate() {
+ return this._lazyUpdate;
+ },
+
+ set lazyUpdate(value) {
+ this._lazyUpdate = value;
+ if (!value) {
+ this._flushRequests();
+ }
+ },
+
+ /**
+ * Adds a network request to this container.
+ *
+ * @param string id
+ * An identifier coming from the network monitor controller.
+ * @param string startedDateTime
+ * A string representation of when the request was started, which
+ * can be parsed by Date (for example "2012-09-17T19:50:03.699Z").
+ * @param string method
+ * Specifies the request method (e.g. "GET", "POST", etc.)
+ * @param string url
+ * Specifies the request's url.
+ * @param boolean isXHR
+ * True if this request was initiated via XHR.
+ * @param object cause
+ * Specifies the request's cause. Has the following properties:
+ * - type: nsContentPolicyType constant
+ * - loadingDocumentUri: URI of the request origin
+ * - stacktrace: JS stacktrace of the request
+ * @param boolean fromCache
+ * Indicates if the result came from the browser cache
+ * @param boolean fromServiceWorker
+ * Indicates if the request has been intercepted by a Service Worker
+ */
+ addRequest: function (id, startedDateTime, method, url, isXHR, cause,
+ fromCache, fromServiceWorker) {
+ this._addQueue.push([id, startedDateTime, method, url, isXHR, cause,
+ fromCache, fromServiceWorker]);
+
+ // Lazy updating is disabled in some tests.
+ if (!this.lazyUpdate) {
+ return void this._flushRequests();
+ }
+
+ this._flushRequestsTask.arm();
+ return undefined;
+ },
+
+ /**
+ * Create a new custom request form populated with the data from
+ * the currently selected request.
+ */
+ cloneSelectedRequest: function () {
+ let selected = this.selectedItem.attachment;
+
+ // Create the element node for the network request item.
+ let menuView = this._createMenuView(selected.method, selected.url,
+ selected.cause);
+
+ // Append a network request item to this container.
+ let newItem = this.push([menuView], {
+ attachment: Object.create(selected, {
+ isCustom: { value: true }
+ })
+ });
+
+ // Immediately switch to new request pane.
+ this.selectedItem = newItem;
+ },
+
+ /**
+ * Send a new HTTP request using the data in the custom request form.
+ */
+ sendCustomRequest: function () {
+ let selected = this.selectedItem.attachment;
+
+ let data = {
+ url: selected.url,
+ method: selected.method,
+ httpVersion: selected.httpVersion,
+ };
+ if (selected.requestHeaders) {
+ data.headers = selected.requestHeaders.headers;
+ }
+ if (selected.requestPostData) {
+ data.body = selected.requestPostData.postData.text;
+ }
+
+ NetMonitorController.webConsoleClient.sendHTTPRequest(data, response => {
+ let id = response.eventActor.actor;
+ this._preferredItemId = id;
+ });
+
+ this.closeCustomRequest();
+ },
+
+ /**
+ * Remove the currently selected custom request.
+ */
+ closeCustomRequest: function () {
+ this.remove(this.selectedItem);
+ NetMonitorView.Sidebar.toggle(false);
+ },
+
+ /**
+ * Shows raw request/response headers in textboxes.
+ */
+ toggleRawHeaders: function () {
+ let requestTextarea = $("#raw-request-headers-textarea");
+ let responseTextare = $("#raw-response-headers-textarea");
+ let rawHeadersHidden = $("#raw-headers").getAttribute("hidden");
+
+ if (rawHeadersHidden) {
+ let selected = this.selectedItem.attachment;
+ let selectedRequestHeaders = selected.requestHeaders.headers;
+ let selectedResponseHeaders = selected.responseHeaders.headers;
+ requestTextarea.value = writeHeaderText(selectedRequestHeaders);
+ responseTextare.value = writeHeaderText(selectedResponseHeaders);
+ $("#raw-headers").hidden = false;
+ } else {
+ requestTextarea.value = null;
+ responseTextare.value = null;
+ $("#raw-headers").hidden = true;
+ }
+ },
+
+ /**
+ * Refreshes the view contents with the newly selected filters
+ */
+ reFilterRequests: function () {
+ this.filterContents(this._filterPredicate);
+ this.refreshSummary();
+ this.refreshZebra();
+ },
+
+ /**
+ * Returns a predicate that can be used to test if a request matches any of
+ * the active filters.
+ */
+ get _filterPredicate() {
+ let currentFreetextFilter = this._currentFreetextFilter;
+
+ return requestItem => {
+ const { attachment } = requestItem;
+ return this._activeFilters.some(filterName => Filters[filterName](attachment)) &&
+ isFreetextMatch(attachment, currentFreetextFilter);
+ };
+ },
+
+ /**
+ * Sorts all network requests in this container by a specified detail.
+ *
+ * @param string type
+ * Either "status", "method", "file", "domain", "type", "transferred",
+ * "size" or "waterfall".
+ */
+ sortBy: function (type = "waterfall") {
+ let target = $("#requests-menu-" + type + "-button");
+ let headers = document.querySelectorAll(".requests-menu-header-button");
+
+ for (let header of headers) {
+ if (header != target) {
+ header.removeAttribute("sorted");
+ header.removeAttribute("tooltiptext");
+ header.parentNode.removeAttribute("active");
+ }
+ }
+
+ let direction = "";
+ if (target) {
+ if (target.getAttribute("sorted") == "ascending") {
+ target.setAttribute("sorted", direction = "descending");
+ target.setAttribute("tooltiptext",
+ L10N.getStr("networkMenu.sortedDesc"));
+ } else {
+ target.setAttribute("sorted", direction = "ascending");
+ target.setAttribute("tooltiptext",
+ L10N.getStr("networkMenu.sortedAsc"));
+ }
+ // Used to style the next column.
+ target.parentNode.setAttribute("active", "true");
+ }
+
+ // Sort by whatever was requested.
+ switch (type) {
+ case "status":
+ if (direction == "ascending") {
+ this.sortContents((a, b) => Sorters.status(a.attachment, b.attachment));
+ } else {
+ this.sortContents((a, b) => -Sorters.status(a.attachment, b.attachment));
+ }
+ break;
+ case "method":
+ if (direction == "ascending") {
+ this.sortContents((a, b) => Sorters.method(a.attachment, b.attachment));
+ } else {
+ this.sortContents((a, b) => -Sorters.method(a.attachment, b.attachment));
+ }
+ break;
+ case "file":
+ if (direction == "ascending") {
+ this.sortContents((a, b) => Sorters.file(a.attachment, b.attachment));
+ } else {
+ this.sortContents((a, b) => -Sorters.file(a.attachment, b.attachment));
+ }
+ break;
+ case "domain":
+ if (direction == "ascending") {
+ this.sortContents((a, b) => Sorters.domain(a.attachment, b.attachment));
+ } else {
+ this.sortContents((a, b) => -Sorters.domain(a.attachment, b.attachment));
+ }
+ break;
+ case "cause":
+ if (direction == "ascending") {
+ this.sortContents((a, b) => Sorters.cause(a.attachment, b.attachment));
+ } else {
+ this.sortContents((a, b) => -Sorters.cause(a.attachment, b.attachment));
+ }
+ break;
+ case "type":
+ if (direction == "ascending") {
+ this.sortContents((a, b) => Sorters.type(a.attachment, b.attachment));
+ } else {
+ this.sortContents((a, b) => -Sorters.type(a.attachment, b.attachment));
+ }
+ break;
+ case "transferred":
+ if (direction == "ascending") {
+ this.sortContents((a, b) => Sorters.transferred(a.attachment, b.attachment));
+ } else {
+ this.sortContents((a, b) => -Sorters.transferred(a.attachment, b.attachment));
+ }
+ break;
+ case "size":
+ if (direction == "ascending") {
+ this.sortContents((a, b) => Sorters.size(a.attachment, b.attachment));
+ } else {
+ this.sortContents((a, b) => -Sorters.size(a.attachment, b.attachment));
+ }
+ break;
+ case "waterfall":
+ if (direction == "ascending") {
+ this.sortContents((a, b) => Sorters.waterfall(a.attachment, b.attachment));
+ } else {
+ this.sortContents((a, b) => -Sorters.waterfall(a.attachment, b.attachment));
+ }
+ break;
+ }
+
+ this.refreshSummary();
+ this.refreshZebra();
+ },
+
+ /**
+ * Removes all network requests and closes the sidebar if open.
+ */
+ clear: function () {
+ NetMonitorController.NetworkEventsHandler.clearMarkers();
+ NetMonitorView.Sidebar.toggle(false);
+
+ this.store.dispatch(Actions.disableToggleButton(true));
+ $("#requests-menu-empty-notice").hidden = false;
+
+ this.empty();
+ this.refreshSummary();
+ },
+
+ /**
+ * Refreshes the status displayed in this container's footer, providing
+ * concise information about all requests.
+ */
+ refreshSummary: function () {
+ let visibleItems = this.visibleItems;
+ let visibleRequestsCount = visibleItems.length;
+ if (!visibleRequestsCount) {
+ this._summary.setAttribute("label", L10N.getStr("networkMenu.empty"));
+ return;
+ }
+
+ let totalBytes = this._getTotalBytesOfRequests(visibleItems);
+ let totalMillis =
+ this._getNewestRequest(visibleItems).attachment.endedMillis -
+ this._getOldestRequest(visibleItems).attachment.startedMillis;
+
+ // https://developer.mozilla.org/en-US/docs/Localization_and_Plurals
+ let str = PluralForm.get(visibleRequestsCount,
+ L10N.getStr("networkMenu.summary"));
+
+ this._summary.setAttribute("label", str
+ .replace("#1", visibleRequestsCount)
+ .replace("#2", L10N.numberWithDecimals((totalBytes || 0) / 1024,
+ CONTENT_SIZE_DECIMALS))
+ .replace("#3", L10N.numberWithDecimals((totalMillis || 0) / 1000,
+ REQUEST_TIME_DECIMALS))
+ );
+ },
+
+ /**
+ * Adds odd/even attributes to all the visible items in this container.
+ */
+ refreshZebra: function () {
+ let visibleItems = this.visibleItems;
+
+ for (let i = 0, len = visibleItems.length; i < len; i++) {
+ let requestItem = visibleItems[i];
+ let requestTarget = requestItem.target;
+
+ if (i % 2 == 0) {
+ requestTarget.setAttribute("even", "");
+ requestTarget.removeAttribute("odd");
+ } else {
+ requestTarget.setAttribute("odd", "");
+ requestTarget.removeAttribute("even");
+ }
+ }
+ },
+
+ /**
+ * Attaches security icon click listener for the given request menu item.
+ *
+ * @param object item
+ * The network request item to attach the listener to.
+ */
+ attachSecurityIconClickListener: function ({ target }) {
+ let icon = $(".requests-security-state-icon", target);
+ icon.addEventListener("click", this._onSecurityIconClick);
+ },
+
+ /**
+ * Schedules adding additional information to a network request.
+ *
+ * @param string id
+ * An identifier coming from the network monitor controller.
+ * @param object data
+ * An object containing several { key: value } tuples of network info.
+ * Supported keys are "httpVersion", "status", "statusText" etc.
+ * @param function callback
+ * A function to call once the request has been updated in the view.
+ */
+ updateRequest: function (id, data, callback) {
+ this._updateQueue.push([id, data, callback]);
+
+ // Lazy updating is disabled in some tests.
+ if (!this.lazyUpdate) {
+ return void this._flushRequests();
+ }
+
+ this._flushRequestsTask.arm();
+ return undefined;
+ },
+
+ /**
+ * Starts adding all queued additional information about network requests.
+ */
+ _flushRequests: function () {
+ // Prevent displaying any updates received after the target closed.
+ if (NetMonitorView._isDestroyed) {
+ return;
+ }
+
+ let widget = NetMonitorView.RequestsMenu.widget;
+ let isScrolledToBottom = widget.isScrolledToBottom();
+
+ for (let [id, startedDateTime, method, url, isXHR, cause, fromCache,
+ fromServiceWorker] of this._addQueue) {
+ // Convert the received date/time string to a unix timestamp.
+ let unixTime = Date.parse(startedDateTime);
+
+ // Create the element node for the network request item.
+ let menuView = this._createMenuView(method, url, cause);
+
+ // Remember the first and last event boundaries.
+ this._registerFirstRequestStart(unixTime);
+ this._registerLastRequestEnd(unixTime);
+
+ // Append a network request item to this container.
+ let requestItem = this.push([menuView, id], {
+ attachment: {
+ startedDeltaMillis: unixTime - this._firstRequestStartedMillis,
+ startedMillis: unixTime,
+ method: method,
+ url: url,
+ isXHR: isXHR,
+ cause: cause,
+ fromCache: fromCache,
+ fromServiceWorker: fromServiceWorker
+ }
+ });
+
+ if (id == this._preferredItemId) {
+ this.selectedItem = requestItem;
+ }
+
+ window.emit(EVENTS.REQUEST_ADDED, id);
+ }
+
+ if (isScrolledToBottom && this._addQueue.length) {
+ widget.scrollToBottom();
+ }
+
+ // For each queued additional information packet, get the corresponding
+ // request item in the view and update it based on the specified data.
+ for (let [id, data, callback] of this._updateQueue) {
+ let requestItem = this.getItemByValue(id);
+ if (!requestItem) {
+ // Packet corresponds to a dead request item, target navigated.
+ continue;
+ }
+
+ // Each information packet may contain several { key: value } tuples of
+ // network info, so update the view based on each one.
+ for (let key in data) {
+ let val = data[key];
+ if (val === undefined) {
+ // The information in the packet is empty, it can be safely ignored.
+ continue;
+ }
+
+ switch (key) {
+ case "requestHeaders":
+ requestItem.attachment.requestHeaders = val;
+ break;
+ case "requestCookies":
+ requestItem.attachment.requestCookies = val;
+ break;
+ case "requestPostData":
+ // Search the POST data upload stream for request headers and add
+ // them to a separate store, different from the classic headers.
+ // XXX: Be really careful here! We're creating a function inside
+ // a loop, so remember the actual request item we want to modify.
+ let currentItem = requestItem;
+ let currentStore = { headers: [], headersSize: 0 };
+
+ Task.spawn(function* () {
+ let postData = yield gNetwork.getString(val.postData.text);
+ let payloadHeaders = CurlUtils.getHeadersFromMultipartText(
+ postData);
+
+ currentStore.headers = payloadHeaders;
+ currentStore.headersSize = payloadHeaders.reduce(
+ (acc, { name, value }) =>
+ acc + name.length + value.length + 2, 0);
+
+ // The `getString` promise is async, so we need to refresh the
+ // information displayed in the network details pane again here.
+ refreshNetworkDetailsPaneIfNecessary(currentItem);
+ });
+
+ requestItem.attachment.requestPostData = val;
+ requestItem.attachment.requestHeadersFromUploadStream =
+ currentStore;
+ break;
+ case "securityState":
+ requestItem.attachment.securityState = val;
+ this.updateMenuView(requestItem, key, val);
+ break;
+ case "securityInfo":
+ requestItem.attachment.securityInfo = val;
+ break;
+ case "responseHeaders":
+ requestItem.attachment.responseHeaders = val;
+ break;
+ case "responseCookies":
+ requestItem.attachment.responseCookies = val;
+ break;
+ case "httpVersion":
+ requestItem.attachment.httpVersion = val;
+ break;
+ case "remoteAddress":
+ requestItem.attachment.remoteAddress = val;
+ this.updateMenuView(requestItem, key, val);
+ break;
+ case "remotePort":
+ requestItem.attachment.remotePort = val;
+ break;
+ case "status":
+ requestItem.attachment.status = val;
+ this.updateMenuView(requestItem, key, {
+ status: val,
+ cached: requestItem.attachment.fromCache,
+ serviceWorker: requestItem.attachment.fromServiceWorker
+ });
+ break;
+ case "statusText":
+ requestItem.attachment.statusText = val;
+ let text = (requestItem.attachment.status + " " +
+ requestItem.attachment.statusText);
+ if (requestItem.attachment.fromCache) {
+ text += " (cached)";
+ } else if (requestItem.attachment.fromServiceWorker) {
+ text += " (service worker)";
+ }
+
+ this.updateMenuView(requestItem, key, text);
+ break;
+ case "headersSize":
+ requestItem.attachment.headersSize = val;
+ break;
+ case "contentSize":
+ requestItem.attachment.contentSize = val;
+ this.updateMenuView(requestItem, key, val);
+ break;
+ case "transferredSize":
+ if (requestItem.attachment.fromCache) {
+ requestItem.attachment.transferredSize = 0;
+ this.updateMenuView(requestItem, key, "cached");
+ } else if (requestItem.attachment.fromServiceWorker) {
+ requestItem.attachment.transferredSize = 0;
+ this.updateMenuView(requestItem, key, "service worker");
+ } else {
+ requestItem.attachment.transferredSize = val;
+ this.updateMenuView(requestItem, key, val);
+ }
+ break;
+ case "mimeType":
+ requestItem.attachment.mimeType = val;
+ this.updateMenuView(requestItem, key, val);
+ break;
+ case "responseContent":
+ // If there's no mime type available when the response content
+ // is received, assume text/plain as a fallback.
+ if (!requestItem.attachment.mimeType) {
+ requestItem.attachment.mimeType = "text/plain";
+ this.updateMenuView(requestItem, "mimeType", "text/plain");
+ }
+ requestItem.attachment.responseContent = val;
+ this.updateMenuView(requestItem, key, val);
+ break;
+ case "totalTime":
+ requestItem.attachment.totalTime = val;
+ requestItem.attachment.endedMillis =
+ requestItem.attachment.startedMillis + val;
+
+ this.updateMenuView(requestItem, key, val);
+ this._registerLastRequestEnd(requestItem.attachment.endedMillis);
+ break;
+ case "eventTimings":
+ requestItem.attachment.eventTimings = val;
+ this._createWaterfallView(
+ requestItem, val.timings,
+ requestItem.attachment.fromCache ||
+ requestItem.attachment.fromServiceWorker
+ );
+ break;
+ }
+ }
+ refreshNetworkDetailsPaneIfNecessary(requestItem);
+
+ if (callback) {
+ callback();
+ }
+ }
+
+ /**
+ * Refreshes the information displayed in the sidebar, in case this update
+ * may have additional information about a request which isn't shown yet
+ * in the network details pane.
+ *
+ * @param object requestItem
+ * The item to repopulate the sidebar with in case it's selected in
+ * this requests menu.
+ */
+ function refreshNetworkDetailsPaneIfNecessary(requestItem) {
+ let selectedItem = NetMonitorView.RequestsMenu.selectedItem;
+ if (selectedItem == requestItem) {
+ NetMonitorView.NetworkDetails.populate(selectedItem.attachment);
+ }
+ }
+
+ // We're done flushing all the requests, clear the update queue.
+ this._updateQueue = [];
+ this._addQueue = [];
+
+ this.store.dispatch(Actions.disableToggleButton(!this.itemCount));
+ $("#requests-menu-empty-notice").hidden = !!this.itemCount;
+
+ // Make sure all the requests are sorted and filtered.
+ // Freshly added requests may not yet contain all the information required
+ // for sorting and filtering predicates, so this is done each time the
+ // network requests table is flushed (don't worry, events are drained first
+ // so this doesn't happen once per network event update).
+ this.sortContents();
+ this.filterContents();
+ this.refreshSummary();
+ this.refreshZebra();
+
+ // Rescale all the waterfalls so that everything is visible at once.
+ this._flushWaterfallViews();
+ },
+
+ /**
+ * Customization function for creating an item's UI.
+ *
+ * @param string method
+ * Specifies the request method (e.g. "GET", "POST", etc.)
+ * @param string url
+ * Specifies the request's url.
+ * @param object cause
+ * Specifies the request's cause. Has two properties:
+ * - type: nsContentPolicyType constant
+ * - uri: URI of the request origin
+ * @return nsIDOMNode
+ * The network request view.
+ */
+ _createMenuView: function (method, url, cause) {
+ let template = $("#requests-menu-item-template");
+ let fragment = document.createDocumentFragment();
+
+ // Flatten the DOM by removing one redundant box (the template container).
+ for (let node of template.childNodes) {
+ fragment.appendChild(node.cloneNode(true));
+ }
+
+ this.updateMenuView(fragment, "method", method);
+ this.updateMenuView(fragment, "url", url);
+ this.updateMenuView(fragment, "cause", cause);
+
+ return fragment;
+ },
+
+ /**
+ * Get a human-readable string from a number of bytes, with the B, KB, MB, or
+ * GB value. Note that the transition between abbreviations is by 1000 rather
+ * than 1024 in order to keep the displayed digits smaller as "1016 KB" is
+ * more awkward than 0.99 MB"
+ */
+ getFormattedSize(bytes) {
+ if (bytes < MAX_BYTES_SIZE) {
+ return L10N.getFormatStr("networkMenu.sizeB", bytes);
+ } else if (bytes < MAX_KB_SIZE) {
+ let kb = bytes / BYTES_IN_KB;
+ let size = L10N.numberWithDecimals(kb, CONTENT_SIZE_DECIMALS);
+ return L10N.getFormatStr("networkMenu.sizeKB", size);
+ } else if (bytes < MAX_MB_SIZE) {
+ let mb = bytes / BYTES_IN_MB;
+ let size = L10N.numberWithDecimals(mb, CONTENT_SIZE_DECIMALS);
+ return L10N.getFormatStr("networkMenu.sizeMB", size);
+ }
+ let gb = bytes / BYTES_IN_GB;
+ let size = L10N.numberWithDecimals(gb, CONTENT_SIZE_DECIMALS);
+ return L10N.getFormatStr("networkMenu.sizeGB", size);
+ },
+
+ /**
+ * Updates the information displayed in a network request item view.
+ *
+ * @param object item
+ * The network request item in this container.
+ * @param string key
+ * The type of information that is to be updated.
+ * @param any value
+ * The new value to be shown.
+ * @return object
+ * A promise that is resolved once the information is displayed.
+ */
+ updateMenuView: Task.async(function* (item, key, value) {
+ let target = item.target || item;
+
+ switch (key) {
+ case "method": {
+ let node = $(".requests-menu-method", target);
+ node.setAttribute("value", value);
+ break;
+ }
+ case "url": {
+ let uri;
+ try {
+ uri = NetworkHelper.nsIURL(value);
+ } catch (e) {
+ // User input may not make a well-formed url yet.
+ break;
+ }
+ let nameWithQuery = getUriNameWithQuery(uri);
+ let hostPort = getUriHostPort(uri);
+ let host = getUriHost(uri);
+ let unicodeUrl = NetworkHelper.convertToUnicode(unescape(uri.spec));
+
+ let file = $(".requests-menu-file", target);
+ file.setAttribute("value", nameWithQuery);
+ file.setAttribute("tooltiptext", unicodeUrl);
+
+ let domain = $(".requests-menu-domain", target);
+ domain.setAttribute("value", hostPort);
+ domain.setAttribute("tooltiptext", hostPort);
+
+ // Mark local hosts specially, where "local" is as defined in the W3C
+ // spec for secure contexts.
+ // http://www.w3.org/TR/powerful-features/
+ //
+ // * If the name falls under 'localhost'
+ // * If the name is an IPv4 address within 127.0.0.0/8
+ // * If the name is an IPv6 address within ::1/128
+ //
+ // IPv6 parsing is a little sloppy; it assumes that the address has
+ // been validated before it gets here.
+ let icon = $(".requests-security-state-icon", target);
+ icon.classList.remove("security-state-local");
+ if (host.match(/(.+\.)?localhost$/) ||
+ host.match(/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}/) ||
+ host.match(/\[[0:]+1\]/)) {
+ let tooltip = L10N.getStr("netmonitor.security.state.secure");
+ icon.classList.add("security-state-local");
+ icon.setAttribute("tooltiptext", tooltip);
+ }
+
+ break;
+ }
+ case "remoteAddress":
+ let domain = $(".requests-menu-domain", target);
+ let tooltip = (domain.getAttribute("value") +
+ (value ? " (" + value + ")" : ""));
+ domain.setAttribute("tooltiptext", tooltip);
+ break;
+ case "securityState": {
+ let icon = $(".requests-security-state-icon", target);
+ this.attachSecurityIconClickListener(item);
+
+ // Security icon for local hosts is set in the "url" branch
+ if (icon.classList.contains("security-state-local")) {
+ break;
+ }
+
+ let tooltip2 = L10N.getStr("netmonitor.security.state." + value);
+ icon.classList.add("security-state-" + value);
+ icon.setAttribute("tooltiptext", tooltip2);
+ break;
+ }
+ case "status": {
+ let node = $(".requests-menu-status-icon", target);
+ // "code" attribute is only used by css to determine the icon color
+ let code;
+ if (value.cached) {
+ code = "cached";
+ } else if (value.serviceWorker) {
+ code = "service worker";
+ } else {
+ code = value.status;
+ }
+ node.setAttribute("code", code);
+ let codeNode = $(".requests-menu-status-code", target);
+ codeNode.setAttribute("value", value.status);
+ break;
+ }
+ case "statusText": {
+ let node = $(".requests-menu-status", target);
+ node.setAttribute("tooltiptext", value);
+ break;
+ }
+ case "cause": {
+ let labelNode = $(".requests-menu-cause-label", target);
+ labelNode.setAttribute("value", loadCauseString(value.type));
+ if (value.loadingDocumentUri) {
+ labelNode.setAttribute("tooltiptext", value.loadingDocumentUri);
+ }
+
+ let stackNode = $(".requests-menu-cause-stack", target);
+ if (value.stacktrace && value.stacktrace.length > 0) {
+ stackNode.removeAttribute("hidden");
+ }
+ break;
+ }
+ case "contentSize": {
+ let node = $(".requests-menu-size", target);
+
+ let text = this.getFormattedSize(value);
+
+ node.setAttribute("value", text);
+ node.setAttribute("tooltiptext", text);
+ break;
+ }
+ case "transferredSize": {
+ let node = $(".requests-menu-transferred", target);
+
+ let text;
+ if (value === null) {
+ text = L10N.getStr("networkMenu.sizeUnavailable");
+ } else if (value === "cached") {
+ text = L10N.getStr("networkMenu.sizeCached");
+ node.classList.add("theme-comment");
+ } else if (value === "service worker") {
+ text = L10N.getStr("networkMenu.sizeServiceWorker");
+ node.classList.add("theme-comment");
+ } else {
+ text = this.getFormattedSize(value);
+ }
+
+ node.setAttribute("value", text);
+ node.setAttribute("tooltiptext", text);
+ break;
+ }
+ case "mimeType": {
+ let type = getAbbreviatedMimeType(value);
+ let node = $(".requests-menu-type", target);
+ let text = CONTENT_MIME_TYPE_ABBREVIATIONS[type] || type;
+ node.setAttribute("value", text);
+ node.setAttribute("tooltiptext", value);
+ break;
+ }
+ case "responseContent": {
+ let { mimeType } = item.attachment;
+
+ if (mimeType.includes("image/")) {
+ let { text, encoding } = value.content;
+ let responseBody = yield gNetwork.getString(text);
+ let node = $(".requests-menu-icon", item.target);
+ node.src = formDataURI(mimeType, encoding, responseBody);
+ node.setAttribute("type", "thumbnail");
+ node.removeAttribute("hidden");
+
+ window.emit(EVENTS.RESPONSE_IMAGE_THUMBNAIL_DISPLAYED);
+ }
+ break;
+ }
+ case "totalTime": {
+ let node = $(".requests-menu-timings-total", target);
+
+ // integer
+ let text = L10N.getFormatStr("networkMenu.totalMS", value);
+ node.setAttribute("value", text);
+ node.setAttribute("tooltiptext", text);
+ break;
+ }
+ }
+ }),
+
+ /**
+ * Creates a waterfall representing timing information in a network
+ * request item view.
+ *
+ * @param object item
+ * The network request item in this container.
+ * @param object timings
+ * An object containing timing information.
+ * @param boolean fromCache
+ * Indicates if the result came from the browser cache or
+ * a service worker
+ */
+ _createWaterfallView: function (item, timings, fromCache) {
+ let { target } = item;
+ let sections = ["blocked", "dns", "connect", "send", "wait", "receive"];
+ // Skipping "blocked" because it doesn't work yet.
+
+ let timingsNode = $(".requests-menu-timings", target);
+ let timingsTotal = $(".requests-menu-timings-total", timingsNode);
+
+ if (fromCache) {
+ timingsTotal.style.display = "none";
+ return;
+ }
+
+ // Add a set of boxes representing timing information.
+ for (let key of sections) {
+ let width = timings[key];
+
+ // Don't render anything if it surely won't be visible.
+ // One millisecond == one unscaled pixel.
+ if (width > 0) {
+ let timingBox = document.createElement("hbox");
+ timingBox.className = "requests-menu-timings-box " + key;
+ timingBox.setAttribute("width", width);
+ timingsNode.insertBefore(timingBox, timingsTotal);
+ }
+ }
+ },
+
+ /**
+ * Rescales and redraws all the waterfall views in this container.
+ *
+ * @param boolean reset
+ * True if this container's width was changed.
+ */
+ _flushWaterfallViews: function (reset) {
+ // Don't paint things while the waterfall view isn't even visible,
+ // or there are no items added to this container.
+ if (NetMonitorView.currentFrontendMode !=
+ "network-inspector-view" || !this.itemCount) {
+ return;
+ }
+
+ // To avoid expensive operations like getBoundingClientRect() and
+ // rebuilding the waterfall background each time a new request comes in,
+ // stuff is cached. However, in certain scenarios like when the window
+ // is resized, this needs to be invalidated.
+ if (reset) {
+ this._cachedWaterfallWidth = 0;
+ }
+
+ // Determine the scaling to be applied to all the waterfalls so that
+ // everything is visible at once. One millisecond == one unscaled pixel.
+ let availableWidth = this._waterfallWidth - REQUESTS_WATERFALL_SAFE_BOUNDS;
+ let longestWidth = this._lastRequestEndedMillis -
+ this._firstRequestStartedMillis;
+ let scale = Math.min(Math.max(availableWidth / longestWidth, EPSILON), 1);
+
+ // Redraw and set the canvas background for each waterfall view.
+ this._showWaterfallDivisionLabels(scale);
+ this._drawWaterfallBackground(scale);
+
+ // Apply CSS transforms to each waterfall in this container totalTime
+ // accurately translate and resize as needed.
+ for (let { target, attachment } of this) {
+ let timingsNode = $(".requests-menu-timings", target);
+ let totalNode = $(".requests-menu-timings-total", target);
+ let direction = window.isRTL ? -1 : 1;
+
+ // Render the timing information at a specific horizontal translation
+ // based on the delta to the first monitored event network.
+ let translateX = "translateX(" + (direction *
+ attachment.startedDeltaMillis) + "px)";
+
+ // Based on the total time passed until the last request, rescale
+ // all the waterfalls to a reasonable size.
+ let scaleX = "scaleX(" + scale + ")";
+
+ // Certain nodes should not be scaled, even if they're children of
+ // another scaled node. In this case, apply a reversed transformation.
+ let revScaleX = "scaleX(" + (1 / scale) + ")";
+
+ timingsNode.style.transform = scaleX + " " + translateX;
+ totalNode.style.transform = revScaleX;
+ }
+ },
+
+ /**
+ * Creates the labels displayed on the waterfall header in this container.
+ *
+ * @param number scale
+ * The current waterfall scale.
+ */
+ _showWaterfallDivisionLabels: function (scale) {
+ let container = $("#requests-menu-waterfall-label-wrapper");
+ let availableWidth = this._waterfallWidth - REQUESTS_WATERFALL_SAFE_BOUNDS;
+
+ // Nuke all existing labels.
+ while (container.hasChildNodes()) {
+ container.firstChild.remove();
+ }
+
+ // Build new millisecond tick labels...
+ let timingStep = REQUESTS_WATERFALL_HEADER_TICKS_MULTIPLE;
+ let optimalTickIntervalFound = false;
+
+ while (!optimalTickIntervalFound) {
+ // Ignore any divisions that would end up being too close to each other.
+ let scaledStep = scale * timingStep;
+ if (scaledStep < REQUESTS_WATERFALL_HEADER_TICKS_SPACING_MIN) {
+ timingStep <<= 1;
+ continue;
+ }
+ optimalTickIntervalFound = true;
+
+ // Insert one label for each division on the current scale.
+ let fragment = document.createDocumentFragment();
+ let direction = window.isRTL ? -1 : 1;
+
+ for (let x = 0; x < availableWidth; x += scaledStep) {
+ let translateX = "translateX(" + ((direction * x) | 0) + "px)";
+ let millisecondTime = x / scale;
+
+ let normalizedTime = millisecondTime;
+ let divisionScale = "millisecond";
+
+ // If the division is greater than 1 minute.
+ if (normalizedTime > 60000) {
+ normalizedTime /= 60000;
+ divisionScale = "minute";
+ } else if (normalizedTime > 1000) {
+ // If the division is greater than 1 second.
+ normalizedTime /= 1000;
+ divisionScale = "second";
+ }
+
+ // Showing too many decimals is bad UX.
+ if (divisionScale == "millisecond") {
+ normalizedTime |= 0;
+ } else {
+ normalizedTime = L10N.numberWithDecimals(normalizedTime,
+ REQUEST_TIME_DECIMALS);
+ }
+
+ let node = document.createElement("label");
+ let text = L10N.getFormatStr("networkMenu." +
+ divisionScale, normalizedTime);
+ node.className = "plain requests-menu-timings-division";
+ node.setAttribute("division-scale", divisionScale);
+ node.style.transform = translateX;
+
+ node.setAttribute("value", text);
+ fragment.appendChild(node);
+ }
+ container.appendChild(fragment);
+
+ container.className = "requests-menu-waterfall-visible";
+ }
+ },
+
+ /**
+ * Creates the background displayed on each waterfall view in this container.
+ *
+ * @param number scale
+ * The current waterfall scale.
+ */
+ _drawWaterfallBackground: function (scale) {
+ if (!this._canvas || !this._ctx) {
+ this._canvas = document.createElementNS(HTML_NS, "canvas");
+ this._ctx = this._canvas.getContext("2d");
+ }
+ let canvas = this._canvas;
+ let ctx = this._ctx;
+
+ // Nuke the context.
+ let canvasWidth = canvas.width = this._waterfallWidth;
+ // Awww yeah, 1px, repeats on Y axis.
+ let canvasHeight = canvas.height = 1;
+
+ // Start over.
+ let imageData = ctx.createImageData(canvasWidth, canvasHeight);
+ let pixelArray = imageData.data;
+
+ let buf = new ArrayBuffer(pixelArray.length);
+ let view8bit = new Uint8ClampedArray(buf);
+ let view32bit = new Uint32Array(buf);
+
+ // Build new millisecond tick lines...
+ let timingStep = REQUESTS_WATERFALL_BACKGROUND_TICKS_MULTIPLE;
+ let [r, g, b] = REQUESTS_WATERFALL_BACKGROUND_TICKS_COLOR_RGB;
+ let alphaComponent = REQUESTS_WATERFALL_BACKGROUND_TICKS_OPACITY_MIN;
+ let optimalTickIntervalFound = false;
+
+ while (!optimalTickIntervalFound) {
+ // Ignore any divisions that would end up being too close to each other.
+ let scaledStep = scale * timingStep;
+ if (scaledStep < REQUESTS_WATERFALL_BACKGROUND_TICKS_SPACING_MIN) {
+ timingStep <<= 1;
+ continue;
+ }
+ optimalTickIntervalFound = true;
+
+ // Insert one pixel for each division on each scale.
+ for (let i = 1; i <= REQUESTS_WATERFALL_BACKGROUND_TICKS_SCALES; i++) {
+ let increment = scaledStep * Math.pow(2, i);
+ for (let x = 0; x < canvasWidth; x += increment) {
+ let position = (window.isRTL ? canvasWidth - x : x) | 0;
+ view32bit[position] =
+ (alphaComponent << 24) | (b << 16) | (g << 8) | r;
+ }
+ alphaComponent += REQUESTS_WATERFALL_BACKGROUND_TICKS_OPACITY_ADD;
+ }
+ }
+
+ {
+ let t = NetMonitorController.NetworkEventsHandler
+ .firstDocumentDOMContentLoadedTimestamp;
+
+ let delta = Math.floor((t - this._firstRequestStartedMillis) * scale);
+ let [r1, g1, b1, a1] =
+ REQUESTS_WATERFALL_DOMCONTENTLOADED_TICKS_COLOR_RGBA;
+ view32bit[delta] = (a1 << 24) | (r1 << 16) | (g1 << 8) | b1;
+ }
+ {
+ let t = NetMonitorController.NetworkEventsHandler
+ .firstDocumentLoadTimestamp;
+
+ let delta = Math.floor((t - this._firstRequestStartedMillis) * scale);
+ let [r2, g2, b2, a2] = REQUESTS_WATERFALL_LOAD_TICKS_COLOR_RGBA;
+ view32bit[delta] = (a2 << 24) | (r2 << 16) | (g2 << 8) | b2;
+ }
+
+ // Flush the image data and cache the waterfall background.
+ pixelArray.set(view8bit);
+ ctx.putImageData(imageData, 0, 0);
+ document.mozSetImageElement("waterfall-background", canvas);
+ },
+
+ /**
+ * The selection listener for this container.
+ */
+ _onSelect: function ({ detail: item }) {
+ if (item) {
+ NetMonitorView.Sidebar.populate(item.attachment);
+ NetMonitorView.Sidebar.toggle(true);
+ } else {
+ NetMonitorView.Sidebar.toggle(false);
+ }
+ },
+
+ /**
+ * The swap listener for this container.
+ * Called when two items switch places, when the contents are sorted.
+ */
+ _onSwap: function ({ detail: [firstItem, secondItem] }) {
+ // Reattach click listener to the security icons
+ this.attachSecurityIconClickListener(firstItem);
+ this.attachSecurityIconClickListener(secondItem);
+ },
+
+ /**
+ * The predicate used when deciding whether a popup should be shown
+ * over a request item or not.
+ *
+ * @param nsIDOMNode target
+ * The element node currently being hovered.
+ * @param object tooltip
+ * The current tooltip instance.
+ * @return {Promise}
+ */
+ _onHover: Task.async(function* (target, tooltip) {
+ let requestItem = this.getItemForElement(target);
+ if (!requestItem) {
+ return false;
+ }
+
+ let hovered = requestItem.attachment;
+ if (hovered.responseContent && target.closest(".requests-menu-icon-and-file")) {
+ return this._setTooltipImageContent(tooltip, requestItem);
+ } else if (hovered.cause && target.closest(".requests-menu-cause-stack")) {
+ return this._setTooltipStackTraceContent(tooltip, requestItem);
+ }
+
+ return false;
+ }),
+
+ _setTooltipImageContent: Task.async(function* (tooltip, requestItem) {
+ let { mimeType, text, encoding } = requestItem.attachment.responseContent.content;
+
+ if (!mimeType || !mimeType.includes("image/")) {
+ return false;
+ }
+
+ let string = yield gNetwork.getString(text);
+ let src = formDataURI(mimeType, encoding, string);
+ let maxDim = REQUESTS_TOOLTIP_IMAGE_MAX_DIM;
+ let { naturalWidth, naturalHeight } = yield getImageDimensions(tooltip.doc, src);
+ let options = { maxDim, naturalWidth, naturalHeight };
+ setImageTooltip(tooltip, tooltip.doc, src, options);
+
+ return $(".requests-menu-icon", requestItem.target);
+ }),
+
+ _setTooltipStackTraceContent: Task.async(function* (tooltip, requestItem) {
+ let {stacktrace} = requestItem.attachment.cause;
+
+ if (!stacktrace || stacktrace.length == 0) {
+ return false;
+ }
+
+ let doc = tooltip.doc;
+ let el = doc.createElementNS(HTML_NS, "div");
+ el.className = "stack-trace-tooltip devtools-monospace";
+
+ for (let f of stacktrace) {
+ let { functionName, filename, lineNumber, columnNumber, asyncCause } = f;
+
+ if (asyncCause) {
+ // if there is asyncCause, append a "divider" row into the trace
+ let asyncFrameEl = doc.createElementNS(HTML_NS, "div");
+ asyncFrameEl.className = "stack-frame stack-frame-async";
+ asyncFrameEl.textContent =
+ WEBCONSOLE_L10N.getFormatStr("stacktrace.asyncStack", asyncCause);
+ el.appendChild(asyncFrameEl);
+ }
+
+ // Parse a source name in format "url -> url"
+ let sourceUrl = filename.split(" -> ").pop();
+
+ let frameEl = doc.createElementNS(HTML_NS, "div");
+ frameEl.className = "stack-frame stack-frame-call";
+
+ let funcEl = doc.createElementNS(HTML_NS, "span");
+ funcEl.className = "stack-frame-function-name";
+ funcEl.textContent =
+ functionName || WEBCONSOLE_L10N.getStr("stacktrace.anonymousFunction");
+ frameEl.appendChild(funcEl);
+
+ let sourceEl = doc.createElementNS(HTML_NS, "span");
+ sourceEl.className = "stack-frame-source-name";
+ frameEl.appendChild(sourceEl);
+
+ let sourceInnerEl = doc.createElementNS(HTML_NS, "span");
+ sourceInnerEl.className = "stack-frame-source-name-inner";
+ sourceEl.appendChild(sourceInnerEl);
+
+ sourceInnerEl.textContent = sourceUrl;
+ sourceInnerEl.title = sourceUrl;
+
+ let lineEl = doc.createElementNS(HTML_NS, "span");
+ lineEl.className = "stack-frame-line";
+ lineEl.textContent = `:${lineNumber}:${columnNumber}`;
+ sourceInnerEl.appendChild(lineEl);
+
+ frameEl.addEventListener("click", () => {
+ // hide the tooltip immediately, not after delay
+ tooltip.hide();
+ NetMonitorController.viewSourceInDebugger(filename, lineNumber);
+ }, false);
+
+ el.appendChild(frameEl);
+ }
+
+ tooltip.setContent(el, {width: REQUESTS_TOOLTIP_STACK_TRACE_WIDTH});
+
+ return true;
+ }),
+
+ /**
+ * A handler that opens the security tab in the details view if secure or
+ * broken security indicator is clicked.
+ */
+ _onSecurityIconClick: function (e) {
+ let state = this.selectedItem.attachment.securityState;
+ if (state !== "insecure") {
+ // Choose the security tab.
+ NetMonitorView.NetworkDetails.widget.selectedIndex = 5;
+ }
+ },
+
+ /**
+ * The resize listener for this container's window.
+ */
+ _onResize: function (e) {
+ // Allow requests to settle down first.
+ setNamedTimeout("resize-events",
+ RESIZE_REFRESH_RATE, () => this._flushWaterfallViews(true));
+ },
+
+ /**
+ * Scroll listener for the requests menu view.
+ */
+ _onScroll: function () {
+ this.tooltip.hide();
+ },
+
+ /**
+ * Open context menu
+ */
+ _onContextMenu: function (e) {
+ e.preventDefault();
+ this.contextMenu.open(e);
+ },
+
+ /**
+ * Checks if the specified unix time is the first one to be known of,
+ * and saves it if so.
+ *
+ * @param number unixTime
+ * The milliseconds to check and save.
+ */
+ _registerFirstRequestStart: function (unixTime) {
+ if (this._firstRequestStartedMillis == -1) {
+ this._firstRequestStartedMillis = unixTime;
+ }
+ },
+
+ /**
+ * Checks if the specified unix time is the last one to be known of,
+ * and saves it if so.
+ *
+ * @param number unixTime
+ * The milliseconds to check and save.
+ */
+ _registerLastRequestEnd: function (unixTime) {
+ if (this._lastRequestEndedMillis < unixTime) {
+ this._lastRequestEndedMillis = unixTime;
+ }
+ },
+
+ /**
+ * Gets the total number of bytes representing the cumulated content size of
+ * a set of requests. Returns 0 for an empty set.
+ *
+ * @param array itemsArray
+ * @return number
+ */
+ _getTotalBytesOfRequests: function (itemsArray) {
+ if (!itemsArray.length) {
+ return 0;
+ }
+
+ let result = 0;
+ itemsArray.forEach(item => {
+ let size = item.attachment.contentSize;
+ result += (typeof size == "number") ? size : 0;
+ });
+
+ return result;
+ },
+
+ /**
+ * Gets the oldest (first performed) request in a set. Returns null for an
+ * empty set.
+ *
+ * @param array itemsArray
+ * @return object
+ */
+ _getOldestRequest: function (itemsArray) {
+ if (!itemsArray.length) {
+ return null;
+ }
+ return itemsArray.reduce((prev, curr) =>
+ prev.attachment.startedMillis < curr.attachment.startedMillis ?
+ prev : curr);
+ },
+
+ /**
+ * Gets the newest (latest performed) request in a set. Returns null for an
+ * empty set.
+ *
+ * @param array itemsArray
+ * @return object
+ */
+ _getNewestRequest: function (itemsArray) {
+ if (!itemsArray.length) {
+ return null;
+ }
+ return itemsArray.reduce((prev, curr) =>
+ prev.attachment.startedMillis > curr.attachment.startedMillis ?
+ prev : curr);
+ },
+
+ /**
+ * Gets the available waterfall width in this container.
+ * @return number
+ */
+ get _waterfallWidth() {
+ if (this._cachedWaterfallWidth == 0) {
+ let container = $("#requests-menu-toolbar");
+ let waterfall = $("#requests-menu-waterfall-header-box");
+ let containerBounds = container.getBoundingClientRect();
+ let waterfallBounds = waterfall.getBoundingClientRect();
+ if (!window.isRTL) {
+ this._cachedWaterfallWidth = containerBounds.width -
+ waterfallBounds.left;
+ } else {
+ this._cachedWaterfallWidth = waterfallBounds.right;
+ }
+ }
+ return this._cachedWaterfallWidth;
+ },
+
+ _splitter: null,
+ _summary: null,
+ _canvas: null,
+ _ctx: null,
+ _cachedWaterfallWidth: 0,
+ _firstRequestStartedMillis: -1,
+ _lastRequestEndedMillis: -1,
+ _updateQueue: [],
+ _addQueue: [],
+ _updateTimeout: null,
+ _resizeTimeout: null,
+ _activeFilters: ["all"],
+ _currentFreetextFilter: ""
+});
+
+exports.RequestsMenuView = RequestsMenuView;
diff --git a/devtools/client/netmonitor/selectors/index.js b/devtools/client/netmonitor/selectors/index.js
new file mode 100644
index 000000000..f473149b5
--- /dev/null
+++ b/devtools/client/netmonitor/selectors/index.js
@@ -0,0 +1,8 @@
+/* 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";
+
+module.exports = {
+ // selectors...
+};
diff --git a/devtools/client/netmonitor/selectors/moz.build b/devtools/client/netmonitor/selectors/moz.build
new file mode 100644
index 000000000..b3975906e
--- /dev/null
+++ b/devtools/client/netmonitor/selectors/moz.build
@@ -0,0 +1,8 @@
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'index.js'
+)
diff --git a/devtools/client/netmonitor/sort-predicates.js b/devtools/client/netmonitor/sort-predicates.js
new file mode 100644
index 000000000..1ead67c22
--- /dev/null
+++ b/devtools/client/netmonitor/sort-predicates.js
@@ -0,0 +1,92 @@
+"use strict";
+
+const { getAbbreviatedMimeType,
+ getUriNameWithQuery,
+ getUriHostPort,
+ loadCauseString } = require("./request-utils");
+
+/**
+ * Predicates used when sorting items.
+ *
+ * @param object first
+ * The first item used in the comparison.
+ * @param object second
+ * The second item used in the comparison.
+ * @return number
+ * <0 to sort first to a lower index than second
+ * =0 to leave first and second unchanged with respect to each other
+ * >0 to sort second to a lower index than first
+ */
+
+function waterfall(first, second) {
+ return first.startedMillis - second.startedMillis;
+}
+
+function status(first, second) {
+ return first.status == second.status
+ ? first.startedMillis - second.startedMillis
+ : first.status - second.status;
+}
+
+function method(first, second) {
+ if (first.method == second.method) {
+ return first.startedMillis - second.startedMillis;
+ }
+ return first.method > second.method ? 1 : -1;
+}
+
+function file(first, second) {
+ let firstUrl = getUriNameWithQuery(first.url).toLowerCase();
+ let secondUrl = getUriNameWithQuery(second.url).toLowerCase();
+ if (firstUrl == secondUrl) {
+ return first.startedMillis - second.startedMillis;
+ }
+ return firstUrl > secondUrl ? 1 : -1;
+}
+
+function domain(first, second) {
+ let firstDomain = getUriHostPort(first.url).toLowerCase();
+ let secondDomain = getUriHostPort(second.url).toLowerCase();
+ if (firstDomain == secondDomain) {
+ return first.startedMillis - second.startedMillis;
+ }
+ return firstDomain > secondDomain ? 1 : -1;
+}
+
+function cause(first, second) {
+ let firstCause = loadCauseString(first.cause.type);
+ let secondCause = loadCauseString(second.cause.type);
+ if (firstCause == secondCause) {
+ return first.startedMillis - second.startedMillis;
+ }
+ return firstCause > secondCause ? 1 : -1;
+}
+
+function type(first, second) {
+ let firstType = getAbbreviatedMimeType(first.mimeType).toLowerCase();
+ let secondType = getAbbreviatedMimeType(second.mimeType).toLowerCase();
+ if (firstType == secondType) {
+ return first.startedMillis - second.startedMillis;
+ }
+ return firstType > secondType ? 1 : -1;
+}
+
+function transferred(first, second) {
+ return first.transferredSize - second.transferredSize;
+}
+
+function size(first, second) {
+ return first.contentSize - second.contentSize;
+}
+
+exports.Sorters = {
+ status,
+ method,
+ file,
+ domain,
+ cause,
+ type,
+ transferred,
+ size,
+ waterfall,
+};
diff --git a/devtools/client/netmonitor/store.js b/devtools/client/netmonitor/store.js
new file mode 100644
index 000000000..454b94711
--- /dev/null
+++ b/devtools/client/netmonitor/store.js
@@ -0,0 +1,13 @@
+/* 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 createStore = require("devtools/client/shared/redux/create-store");
+const reducers = require("./reducers/index");
+
+function configureStore() {
+ return createStore()(reducers);
+}
+
+exports.configureStore = configureStore;
diff --git a/devtools/client/netmonitor/test/.eslintrc.js b/devtools/client/netmonitor/test/.eslintrc.js
new file mode 100644
index 000000000..8d15a76d9
--- /dev/null
+++ b/devtools/client/netmonitor/test/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../../.eslintrc.mochitests.js"
+};
diff --git a/devtools/client/netmonitor/test/browser.ini b/devtools/client/netmonitor/test/browser.ini
new file mode 100644
index 000000000..5dfe9012d
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser.ini
@@ -0,0 +1,156 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ dropmarker.svg
+ head.js
+ html_cause-test-page.html
+ html_content-type-test-page.html
+ html_content-type-without-cache-test-page.html
+ html_brotli-test-page.html
+ html_image-tooltip-test-page.html
+ html_cors-test-page.html
+ html_custom-get-page.html
+ html_cyrillic-test-page.html
+ html_frame-test-page.html
+ html_frame-subdocument.html
+ html_filter-test-page.html
+ html_infinite-get-page.html
+ html_json-custom-mime-test-page.html
+ html_json-long-test-page.html
+ html_json-malformed-test-page.html
+ html_json-text-mime-test-page.html
+ html_jsonp-test-page.html
+ html_navigate-test-page.html
+ html_params-test-page.html
+ html_post-data-test-page.html
+ html_post-json-test-page.html
+ html_post-raw-test-page.html
+ html_post-raw-with-headers-test-page.html
+ html_simple-test-page.html
+ html_single-get-page.html
+ html_send-beacon.html
+ html_sorting-test-page.html
+ html_statistics-test-page.html
+ html_status-codes-test-page.html
+ html_api-calls-test-page.html
+ html_copy-as-curl.html
+ html_curl-utils.html
+ sjs_content-type-test-server.sjs
+ sjs_cors-test-server.sjs
+ sjs_https-redirect-test-server.sjs
+ sjs_hsts-test-server.sjs
+ sjs_simple-test-server.sjs
+ sjs_sorting-test-server.sjs
+ sjs_status-codes-test-server.sjs
+ sjs_truncate-test-server.sjs
+ test-image.png
+ service-workers/status-codes.html
+ service-workers/status-codes-service-worker.js
+ !/devtools/client/framework/test/shared-head.js
+
+[browser_net_aaa_leaktest.js]
+[browser_net_accessibility-01.js]
+[browser_net_accessibility-02.js]
+skip-if = (toolkit == "cocoa" && e10s) # bug 1252254
+[browser_net_api-calls.js]
+[browser_net_autoscroll.js]
+skip-if = true # Bug 1309191 - replace with rewritten version in React
+[browser_net_cached-status.js]
+[browser_net_cause.js]
+[browser_net_cause_redirect.js]
+[browser_net_service-worker-status.js]
+[browser_net_charts-01.js]
+[browser_net_charts-02.js]
+[browser_net_charts-03.js]
+[browser_net_charts-04.js]
+[browser_net_charts-05.js]
+[browser_net_charts-06.js]
+[browser_net_charts-07.js]
+[browser_net_clear.js]
+[browser_net_complex-params.js]
+[browser_net_content-type.js]
+[browser_net_brotli.js]
+[browser_net_curl-utils.js]
+[browser_net_copy_image_as_data_uri.js]
+subsuite = clipboard
+[browser_net_copy_svg_image_as_data_uri.js]
+subsuite = clipboard
+[browser_net_copy_url.js]
+subsuite = clipboard
+[browser_net_copy_params.js]
+subsuite = clipboard
+[browser_net_copy_response.js]
+subsuite = clipboard
+[browser_net_copy_headers.js]
+subsuite = clipboard
+[browser_net_copy_as_curl.js]
+subsuite = clipboard
+[browser_net_cors_requests.js]
+[browser_net_cyrillic-01.js]
+[browser_net_cyrillic-02.js]
+[browser_net_details-no-duplicated-content.js]
+skip-if = (os == 'linux' && e10s && debug) # Bug 1242204
+[browser_net_frame.js]
+[browser_net_filter-01.js]
+[browser_net_filter-02.js]
+[browser_net_filter-03.js]
+[browser_net_filter-04.js]
+[browser_net_footer-summary.js]
+[browser_net_html-preview.js]
+[browser_net_icon-preview.js]
+[browser_net_image-tooltip.js]
+[browser_net_json-long.js]
+[browser_net_json-malformed.js]
+[browser_net_json_custom_mime.js]
+[browser_net_json_text_mime.js]
+[browser_net_jsonp.js]
+[browser_net_large-response.js]
+[browser_net_leak_on_tab_close.js]
+[browser_net_open_request_in_tab.js]
+[browser_net_page-nav.js]
+[browser_net_pane-collapse.js]
+[browser_net_pane-toggle.js]
+[browser_net_post-data-01.js]
+[browser_net_post-data-02.js]
+[browser_net_post-data-03.js]
+[browser_net_post-data-04.js]
+[browser_net_prefs-and-l10n.js]
+[browser_net_prefs-reload.js]
+[browser_net_raw_headers.js]
+[browser_net_reload-button.js]
+[browser_net_reload-markers.js]
+[browser_net_req-resp-bodies.js]
+[browser_net_resend_cors.js]
+[browser_net_resend_headers.js]
+[browser_net_resend.js]
+[browser_net_security-details.js]
+[browser_net_security-error.js]
+[browser_net_security-icon-click.js]
+[browser_net_security-redirect.js]
+[browser_net_security-state.js]
+[browser_net_security-tab-deselect.js]
+[browser_net_security-tab-visibility.js]
+[browser_net_security-warnings.js]
+[browser_net_send-beacon.js]
+[browser_net_send-beacon-other-tab.js]
+[browser_net_simple-init.js]
+[browser_net_simple-request-data.js]
+skip-if = true # Bug 1258809
+[browser_net_simple-request-details.js]
+skip-if = true # Bug 1258809
+[browser_net_simple-request.js]
+[browser_net_sort-01.js]
+skip-if = (e10s && debug && os == 'mac') # Bug 1253037
+[browser_net_sort-02.js]
+[browser_net_sort-03.js]
+[browser_net_statistics-01.js]
+[browser_net_statistics-02.js]
+[browser_net_statistics-03.js]
+[browser_net_status-codes.js]
+[browser_net_streaming-response.js]
+[browser_net_throttle.js]
+[browser_net_timeline_ticks.js]
+[browser_net_timing-division.js]
+[browser_net_truncate.js]
+[browser_net_persistent_logs.js]
diff --git a/devtools/client/netmonitor/test/browser_net_aaa_leaktest.js b/devtools/client/netmonitor/test/browser_net_aaa_leaktest.js
new file mode 100644
index 000000000..31c1e03ad
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_aaa_leaktest.js
@@ -0,0 +1,28 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if the network monitor leaks on initialization and sudden destruction.
+ * You can also use this initialization format as a template for other tests.
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(SIMPLE_URL);
+ info("Starting test... ");
+
+ let { document, NetMonitorView, NetMonitorController } = monitor.panelWin;
+ let { RequestsMenu, NetworkDetails } = NetMonitorView;
+
+ ok(tab, "Should have a tab available.");
+ ok(monitor, "Should have a network monitor pane available.");
+
+ ok(document, "Should have a document available.");
+ ok(NetMonitorView, "Should have a NetMonitorView object available.");
+ ok(NetMonitorController, "Should have a NetMonitorController object available.");
+ ok(RequestsMenu, "Should have a RequestsMenu object available.");
+ ok(NetworkDetails, "Should have a NetworkDetails object available.");
+
+ yield teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_accessibility-01.js b/devtools/client/netmonitor/test/browser_net_accessibility-01.js
new file mode 100644
index 000000000..c0832064f
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_accessibility-01.js
@@ -0,0 +1,87 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if focus modifiers work for the SideMenuWidget.
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(CUSTOM_GET_URL);
+ info("Starting test... ");
+
+ // It seems that this test may be slow on Ubuntu builds running on ec2.
+ requestLongerTimeout(2);
+
+ let { NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ let count = 0;
+ function check(selectedIndex, paneVisibility) {
+ info("Performing check " + (count++) + ".");
+
+ is(RequestsMenu.selectedIndex, selectedIndex,
+ "The selected item in the requests menu was incorrect.");
+ is(NetMonitorView.detailsPaneHidden, !paneVisibility,
+ "The network requests details pane visibility state was incorrect.");
+ }
+
+ let wait = waitForNetworkEvents(monitor, 2);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests(2);
+ });
+ yield wait;
+
+ check(-1, false);
+
+ RequestsMenu.focusLastVisibleItem();
+ check(1, true);
+ RequestsMenu.focusFirstVisibleItem();
+ check(0, true);
+
+ RequestsMenu.focusNextItem();
+ check(1, true);
+ RequestsMenu.focusPrevItem();
+ check(0, true);
+
+ RequestsMenu.focusItemAtDelta(+1);
+ check(1, true);
+ RequestsMenu.focusItemAtDelta(-1);
+ check(0, true);
+
+ RequestsMenu.focusItemAtDelta(+10);
+ check(1, true);
+ RequestsMenu.focusItemAtDelta(-10);
+ check(0, true);
+
+ wait = waitForNetworkEvents(monitor, 18);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests(18);
+ });
+ yield wait;
+
+ RequestsMenu.focusLastVisibleItem();
+ check(19, true);
+ RequestsMenu.focusFirstVisibleItem();
+ check(0, true);
+
+ RequestsMenu.focusNextItem();
+ check(1, true);
+ RequestsMenu.focusPrevItem();
+ check(0, true);
+
+ RequestsMenu.focusItemAtDelta(+10);
+ check(10, true);
+ RequestsMenu.focusItemAtDelta(-10);
+ check(0, true);
+
+ RequestsMenu.focusItemAtDelta(+100);
+ check(19, true);
+ RequestsMenu.focusItemAtDelta(-100);
+ check(0, true);
+
+ yield teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_accessibility-02.js b/devtools/client/netmonitor/test/browser_net_accessibility-02.js
new file mode 100644
index 000000000..33420a440
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_accessibility-02.js
@@ -0,0 +1,130 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if keyboard and mouse navigation works in the network requests menu.
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(CUSTOM_GET_URL);
+ info("Starting test... ");
+
+ // It seems that this test may be slow on Ubuntu builds running on ec2.
+ requestLongerTimeout(2);
+
+ let { window, $, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ let count = 0;
+ function check(selectedIndex, paneVisibility) {
+ info("Performing check " + (count++) + ".");
+
+ is(RequestsMenu.selectedIndex, selectedIndex,
+ "The selected item in the requests menu was incorrect.");
+ is(NetMonitorView.detailsPaneHidden, !paneVisibility,
+ "The network requests details pane visibility state was incorrect.");
+ }
+
+ let wait = waitForNetworkEvents(monitor, 2);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests(2);
+ });
+ yield wait;
+
+ check(-1, false);
+
+ EventUtils.sendKey("DOWN", window);
+ check(0, true);
+ EventUtils.sendKey("UP", window);
+ check(0, true);
+
+ EventUtils.sendKey("PAGE_DOWN", window);
+ check(1, true);
+ EventUtils.sendKey("PAGE_UP", window);
+ check(0, true);
+
+ EventUtils.sendKey("END", window);
+ check(1, true);
+ EventUtils.sendKey("HOME", window);
+ check(0, true);
+
+ wait = waitForNetworkEvents(monitor, 18);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests(18);
+ });
+ yield wait;
+
+ EventUtils.sendKey("DOWN", window);
+ check(1, true);
+ EventUtils.sendKey("DOWN", window);
+ check(2, true);
+ EventUtils.sendKey("UP", window);
+ check(1, true);
+ EventUtils.sendKey("UP", window);
+ check(0, true);
+
+ EventUtils.sendKey("PAGE_DOWN", window);
+ check(4, true);
+ EventUtils.sendKey("PAGE_DOWN", window);
+ check(8, true);
+ EventUtils.sendKey("PAGE_UP", window);
+ check(4, true);
+ EventUtils.sendKey("PAGE_UP", window);
+ check(0, true);
+
+ EventUtils.sendKey("HOME", window);
+ check(0, true);
+ EventUtils.sendKey("HOME", window);
+ check(0, true);
+ EventUtils.sendKey("PAGE_UP", window);
+ check(0, true);
+ EventUtils.sendKey("HOME", window);
+ check(0, true);
+
+ EventUtils.sendKey("END", window);
+ check(19, true);
+ EventUtils.sendKey("END", window);
+ check(19, true);
+ EventUtils.sendKey("PAGE_DOWN", window);
+ check(19, true);
+ EventUtils.sendKey("END", window);
+ check(19, true);
+
+ EventUtils.sendKey("PAGE_UP", window);
+ check(15, true);
+ EventUtils.sendKey("PAGE_UP", window);
+ check(11, true);
+ EventUtils.sendKey("UP", window);
+ check(10, true);
+ EventUtils.sendKey("UP", window);
+ check(9, true);
+ EventUtils.sendKey("PAGE_DOWN", window);
+ check(13, true);
+ EventUtils.sendKey("PAGE_DOWN", window);
+ check(17, true);
+ EventUtils.sendKey("PAGE_DOWN", window);
+ check(19, true);
+ EventUtils.sendKey("PAGE_DOWN", window);
+ check(19, true);
+
+ EventUtils.sendKey("HOME", window);
+ check(0, true);
+ EventUtils.sendKey("DOWN", window);
+ check(1, true);
+ EventUtils.sendKey("END", window);
+ check(19, true);
+ EventUtils.sendKey("DOWN", window);
+ check(19, true);
+
+ EventUtils.sendMouseEvent({ type: "mousedown" }, $("#details-pane-toggle"));
+ check(-1, false);
+
+ EventUtils.sendMouseEvent({ type: "mousedown" }, $(".side-menu-widget-item"));
+ check(0, true);
+
+ yield teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_api-calls.js b/devtools/client/netmonitor/test/browser_net_api-calls.js
new file mode 100644
index 000000000..994dc0354
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_api-calls.js
@@ -0,0 +1,39 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests whether API call URLs (without a filename) are correctly displayed
+ * (including Unicode)
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(API_CALLS_URL);
+ info("Starting test... ");
+
+ let { NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ const REQUEST_URIS = [
+ "http://example.com/api/fileName.xml",
+ "http://example.com/api/file%E2%98%A2.xml",
+ "http://example.com/api/ascii/get/",
+ "http://example.com/api/unicode/%E2%98%A2/",
+ "http://example.com/api/search/?q=search%E2%98%A2"
+ ];
+
+ let wait = waitForNetworkEvents(monitor, 5);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests();
+ });
+ yield wait;
+
+ REQUEST_URIS.forEach(function (uri, index) {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(index), "GET", uri);
+ });
+
+ yield teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_autoscroll.js b/devtools/client/netmonitor/test/browser_net_autoscroll.js
new file mode 100644
index 000000000..9abb3fd17
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_autoscroll.js
@@ -0,0 +1,75 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Bug 863102 - Automatically scroll down upon new network requests.
+ */
+add_task(function* () {
+ requestLongerTimeout(2);
+
+ let { monitor } = yield initNetMonitor(INFINITE_GET_URL);
+ let win = monitor.panelWin;
+ let topNode = win.document.getElementById("requests-menu-contents");
+ let requestsContainer = topNode.getElementsByTagName("scrollbox")[0];
+ ok(!!requestsContainer, "Container element exists as expected.");
+
+ // (1) Check that the scroll position is maintained at the bottom
+ // when the requests overflow the vertical size of the container.
+ yield waitForRequestsToOverflowContainer();
+ yield waitForScroll();
+ ok(scrolledToBottom(requestsContainer), "Scrolled to bottom on overflow.");
+
+ // (2) Now set the scroll position somewhere in the middle and check
+ // that additional requests do not change the scroll position.
+ let children = requestsContainer.childNodes;
+ let middleNode = children.item(children.length / 2);
+ middleNode.scrollIntoView();
+ ok(!scrolledToBottom(requestsContainer), "Not scrolled to bottom.");
+ // save for comparison later
+ let scrollTop = requestsContainer.scrollTop;
+ yield waitForNetworkEvents(monitor, 8);
+ yield waitSomeTime();
+ is(requestsContainer.scrollTop, scrollTop, "Did not scroll.");
+
+ // (3) Now set the scroll position back at the bottom and check that
+ // additional requests *do* cause the container to scroll down.
+ requestsContainer.scrollTop = requestsContainer.scrollHeight;
+ ok(scrolledToBottom(requestsContainer), "Set scroll position to bottom.");
+ yield waitForNetworkEvents(monitor, 8);
+ yield waitForScroll();
+ ok(scrolledToBottom(requestsContainer), "Still scrolled to bottom.");
+
+ // (4) Now select an item in the list and check that additional requests
+ // do not change the scroll position.
+ monitor.panelWin.NetMonitorView.RequestsMenu.selectedIndex = 0;
+ yield waitForNetworkEvents(monitor, 8);
+ yield waitSomeTime();
+ is(requestsContainer.scrollTop, 0, "Did not scroll.");
+
+ // Done: clean up.
+ yield teardown(monitor);
+
+ function* waitForRequestsToOverflowContainer() {
+ while (true) {
+ yield waitForNetworkEvents(monitor, 1);
+ if (requestsContainer.scrollHeight > requestsContainer.clientHeight) {
+ return;
+ }
+ }
+ }
+
+ function scrolledToBottom(element) {
+ return element.scrollTop + element.clientHeight >= element.scrollHeight;
+ }
+
+ function waitSomeTime() {
+ // Wait to make sure no scrolls happen
+ return wait(50);
+ }
+
+ function waitForScroll() {
+ return monitor._view.RequestsMenu.widget.once("scroll-to-bottom");
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_brotli.js b/devtools/client/netmonitor/test/browser_net_brotli.js
new file mode 100644
index 000000000..cc6908d68
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_brotli.js
@@ -0,0 +1,91 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const BROTLI_URL = HTTPS_EXAMPLE_URL + "html_brotli-test-page.html";
+const BROTLI_REQUESTS = 1;
+
+/**
+ * Test brotli encoded response is handled correctly on HTTPS.
+ */
+
+add_task(function* () {
+ let { L10N } = require("devtools/client/netmonitor/l10n");
+
+ let { tab, monitor } = yield initNetMonitor(BROTLI_URL);
+ info("Starting test... ");
+
+ let { document, Editor, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, BROTLI_REQUESTS);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests();
+ });
+ yield wait;
+
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(0),
+ "GET", HTTPS_CONTENT_TYPE_SJS + "?fmt=br", {
+ status: 200,
+ statusText: "Connected",
+ type: "plain",
+ fullMimeType: "text/plain",
+ transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 10),
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 64),
+ time: true
+ });
+
+ let onEvent = waitForResponseBodyDisplayed();
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.getElementById("details-pane-toggle"));
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.querySelectorAll("#details-pane tab")[3]);
+ yield onEvent;
+ yield testResponseTab("br");
+
+ yield teardown(monitor);
+
+ function* testResponseTab(type) {
+ let tabEl = document.querySelectorAll("#details-pane tab")[3];
+ let tabpanel = document.querySelectorAll("#details-pane tabpanel")[3];
+
+ is(tabEl.getAttribute("selected"), "true",
+ "The response tab in the network details pane should be selected.");
+
+ function checkVisibility(box) {
+ is(tabpanel.querySelector("#response-content-info-header")
+ .hasAttribute("hidden"), true,
+ "The response info header doesn't have the intended visibility.");
+ is(tabpanel.querySelector("#response-content-json-box")
+ .hasAttribute("hidden"), box != "json",
+ "The response content json box doesn't have the intended visibility.");
+ is(tabpanel.querySelector("#response-content-textarea-box")
+ .hasAttribute("hidden"), box != "textarea",
+ "The response content textarea box doesn't have the intended visibility.");
+ is(tabpanel.querySelector("#response-content-image-box")
+ .hasAttribute("hidden"), box != "image",
+ "The response content image box doesn't have the intended visibility.");
+ }
+
+ switch (type) {
+ case "br": {
+ checkVisibility("textarea");
+
+ let expected = "X".repeat(64);
+ let editor = yield NetMonitorView.editor("#response-content-textarea");
+ is(editor.getText(), expected,
+ "The text shown in the source editor is incorrect for the brotli request.");
+ is(editor.getMode(), Editor.modes.text,
+ "The mode active in the source editor is incorrect for the brotli request.");
+ break;
+ }
+ }
+ }
+
+ function waitForResponseBodyDisplayed() {
+ return monitor.panelWin.once(monitor.panelWin.EVENTS.RESPONSE_BODY_DISPLAYED);
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_cached-status.js b/devtools/client/netmonitor/test/browser_net_cached-status.js
new file mode 100644
index 000000000..66b926bea
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_cached-status.js
@@ -0,0 +1,111 @@
+/* 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";
+
+/**
+ * Tests if cached requests have the correct status code
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(STATUS_CODES_URL, null, true);
+ info("Starting test... ");
+
+ let { NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu, NetworkDetails } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+ NetworkDetails._params.lazyEmpty = false;
+
+ const REQUEST_DATA = [
+ {
+ method: "GET",
+ uri: STATUS_CODES_SJS + "?sts=ok&cached",
+ details: {
+ status: 200,
+ statusText: "OK",
+ type: "plain",
+ fullMimeType: "text/plain; charset=utf-8"
+ }
+ },
+ {
+ method: "GET",
+ uri: STATUS_CODES_SJS + "?sts=redirect&cached",
+ details: {
+ status: 301,
+ statusText: "Moved Permanently",
+ type: "html",
+ fullMimeType: "text/html; charset=utf-8"
+ }
+ },
+ {
+ method: "GET",
+ uri: "http://example.com/redirected",
+ details: {
+ status: 404,
+ statusText: "Not Found",
+ type: "html",
+ fullMimeType: "text/html; charset=utf-8"
+ }
+ },
+ {
+ method: "GET",
+ uri: STATUS_CODES_SJS + "?sts=ok&cached",
+ details: {
+ status: 200,
+ statusText: "OK (cached)",
+ displayedStatus: "cached",
+ type: "plain",
+ fullMimeType: "text/plain; charset=utf-8"
+ }
+ },
+ {
+ method: "GET",
+ uri: STATUS_CODES_SJS + "?sts=redirect&cached",
+ details: {
+ status: 301,
+ statusText: "Moved Permanently (cached)",
+ displayedStatus: "cached",
+ type: "html",
+ fullMimeType: "text/html; charset=utf-8"
+ }
+ },
+ {
+ method: "GET",
+ uri: "http://example.com/redirected",
+ details: {
+ status: 404,
+ statusText: "Not Found",
+ type: "html",
+ fullMimeType: "text/html; charset=utf-8"
+ }
+ }
+ ];
+
+ info("Performing requests #1...");
+ yield performRequestsAndWait();
+
+ info("Performing requests #2...");
+ yield performRequestsAndWait();
+
+ let index = 0;
+ for (let request of REQUEST_DATA) {
+ let item = RequestsMenu.getItemAtIndex(index);
+
+ info("Verifying request #" + index);
+ yield verifyRequestItemTarget(item, request.method, request.uri, request.details);
+
+ index++;
+ }
+
+ yield teardown(monitor);
+
+ function* performRequestsAndWait() {
+ let wait = waitForNetworkEvents(monitor, 3);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performCachedRequests();
+ });
+ yield wait;
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_cause.js b/devtools/client/netmonitor/test/browser_net_cause.js
new file mode 100644
index 000000000..2e73965d0
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_cause.js
@@ -0,0 +1,147 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if request cause is reported correctly.
+ */
+
+const CAUSE_FILE_NAME = "html_cause-test-page.html";
+const CAUSE_URL = EXAMPLE_URL + CAUSE_FILE_NAME;
+
+const EXPECTED_REQUESTS = [
+ {
+ method: "GET",
+ url: CAUSE_URL,
+ causeType: "document",
+ causeUri: "",
+ // The document load has internal privileged JS code on the stack
+ stack: true
+ },
+ {
+ method: "GET",
+ url: EXAMPLE_URL + "stylesheet_request",
+ causeType: "stylesheet",
+ causeUri: CAUSE_URL,
+ stack: false
+ },
+ {
+ method: "GET",
+ url: EXAMPLE_URL + "img_request",
+ causeType: "img",
+ causeUri: CAUSE_URL,
+ stack: false
+ },
+ {
+ method: "GET",
+ url: EXAMPLE_URL + "xhr_request",
+ causeType: "xhr",
+ causeUri: CAUSE_URL,
+ stack: [{ fn: "performXhrRequest", file: CAUSE_FILE_NAME, line: 22 }]
+ },
+ {
+ method: "GET",
+ url: EXAMPLE_URL + "fetch_request",
+ causeType: "fetch",
+ causeUri: CAUSE_URL,
+ stack: [{ fn: "performFetchRequest", file: CAUSE_FILE_NAME, line: 26 }]
+ },
+ {
+ method: "GET",
+ url: EXAMPLE_URL + "promise_fetch_request",
+ causeType: "fetch",
+ causeUri: CAUSE_URL,
+ stack: [
+ { fn: "performPromiseFetchRequest", file: CAUSE_FILE_NAME, line: 38 },
+ { fn: null, file: CAUSE_FILE_NAME, line: 37, asyncCause: "promise callback" },
+ ]
+ },
+ {
+ method: "GET",
+ url: EXAMPLE_URL + "timeout_fetch_request",
+ causeType: "fetch",
+ causeUri: CAUSE_URL,
+ stack: [
+ { fn: "performTimeoutFetchRequest", file: CAUSE_FILE_NAME, line: 40 },
+ { fn: "performPromiseFetchRequest", file: CAUSE_FILE_NAME, line: 39,
+ asyncCause: "setTimeout handler" },
+ ]
+ },
+ {
+ method: "POST",
+ url: EXAMPLE_URL + "beacon_request",
+ causeType: "beacon",
+ causeUri: CAUSE_URL,
+ stack: [{ fn: "performBeaconRequest", file: CAUSE_FILE_NAME, line: 30 }]
+ },
+];
+
+add_task(function* () {
+ // Async stacks aren't on by default in all builds
+ yield SpecialPowers.pushPrefEnv({ set: [["javascript.options.asyncstack", true]] });
+
+ // the initNetMonitor function clears the network request list after the
+ // page is loaded. That's why we first load a bogus page from SIMPLE_URL,
+ // and only then load the real thing from CAUSE_URL - we want to catch
+ // all the requests the page is making, not only the XHRs.
+ // We can't use about:blank here, because initNetMonitor checks that the
+ // page has actually made at least one request.
+ let { tab, monitor } = yield initNetMonitor(SIMPLE_URL);
+ let { $, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, EXPECTED_REQUESTS.length);
+ tab.linkedBrowser.loadURI(CAUSE_URL);
+ yield wait;
+
+ is(RequestsMenu.itemCount, EXPECTED_REQUESTS.length,
+ "All the page events should be recorded.");
+
+ EXPECTED_REQUESTS.forEach((spec, i) => {
+ let { method, url, causeType, causeUri, stack } = spec;
+
+ let requestItem = RequestsMenu.getItemAtIndex(i);
+ verifyRequestItemTarget(requestItem,
+ method, url, { cause: { type: causeType, loadingDocumentUri: causeUri } }
+ );
+
+ let { stacktrace } = requestItem.attachment.cause;
+ let stackLen = stacktrace ? stacktrace.length : 0;
+
+ if (stack) {
+ ok(stacktrace, `Request #${i} has a stacktrace`);
+ ok(stackLen > 0,
+ `Request #${i} (${causeType}) has a stacktrace with ${stackLen} items`);
+
+ // if "stack" is array, check the details about the top stack frames
+ if (Array.isArray(stack)) {
+ stack.forEach((frame, j) => {
+ is(stacktrace[j].functionName, frame.fn,
+ `Request #${i} has the correct function on JS stack frame #${j}`);
+ is(stacktrace[j].filename.split("/").pop(), frame.file,
+ `Request #${i} has the correct file on JS stack frame #${j}`);
+ is(stacktrace[j].lineNumber, frame.line,
+ `Request #${i} has the correct line number on JS stack frame #${j}`);
+ is(stacktrace[j].asyncCause, frame.asyncCause,
+ `Request #${i} has the correct async cause on JS stack frame #${j}`);
+ });
+ }
+ } else {
+ is(stackLen, 0, `Request #${i} (${causeType}) has an empty stacktrace`);
+ }
+ });
+
+ // Sort the requests by cause and check the order
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-cause-button"));
+ let expectedOrder = EXPECTED_REQUESTS.map(r => r.causeType).sort();
+ expectedOrder.forEach((expectedCause, i) => {
+ let { target } = RequestsMenu.getItemAtIndex(i);
+ let causeLabel = target.querySelector(".requests-menu-cause-label");
+ let cause = causeLabel.getAttribute("value");
+ is(cause, expectedCause, `The request #${i} has the expected cause after sorting`);
+ });
+
+ yield teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_cause_redirect.js b/devtools/client/netmonitor/test/browser_net_cause_redirect.js
new file mode 100644
index 000000000..ace6390ab
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_cause_redirect.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if request JS stack is property reported if the request is internally
+ * redirected without hitting the network (HSTS is one of such cases)
+ */
+
+add_task(function* () {
+ const EXPECTED_REQUESTS = [
+ // Request to HTTP URL, redirects to HTTPS, has callstack
+ { status: 302, hasStack: true },
+ // Serves HTTPS, sets the Strict-Transport-Security header, no stack
+ { status: 200, hasStack: false },
+ // Second request to HTTP redirects to HTTPS internally
+ { status: 200, hasStack: true },
+ ];
+
+ let { tab, monitor } = yield initNetMonitor(CUSTOM_GET_URL);
+ let { RequestsMenu } = monitor.panelWin.NetMonitorView;
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, EXPECTED_REQUESTS.length);
+ yield performRequests(2, HSTS_SJS);
+ yield wait;
+
+ EXPECTED_REQUESTS.forEach(({status, hasStack}, i) => {
+ let { attachment } = RequestsMenu.getItemAtIndex(i);
+
+ is(attachment.status, status, `Request #${i} has the expected status`);
+
+ let { stacktrace } = attachment.cause;
+ let stackLen = stacktrace ? stacktrace.length : 0;
+
+ if (hasStack) {
+ ok(stacktrace, `Request #${i} has a stacktrace`);
+ ok(stackLen > 0, `Request #${i} has a stacktrace with ${stackLen} items`);
+ } else {
+ is(stackLen, 0, `Request #${i} has an empty stacktrace`);
+ }
+ });
+
+ // Send a request to reset the HSTS policy to state before the test
+ wait = waitForNetworkEvents(monitor, 1);
+ yield performRequests(1, HSTS_SJS + "?reset");
+ yield wait;
+
+ yield teardown(monitor);
+
+ function performRequests(count, url) {
+ return ContentTask.spawn(tab.linkedBrowser, { count, url }, function* (args) {
+ content.wrappedJSObject.performRequests(args.count, args.url);
+ });
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_charts-01.js b/devtools/client/netmonitor/test/browser_net_charts-01.js
new file mode 100644
index 000000000..987881836
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_charts-01.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Makes sure Pie Charts have the right internal structure.
+ */
+
+add_task(function* () {
+ let { monitor } = yield initNetMonitor(SIMPLE_URL);
+ info("Starting test... ");
+
+ let { document, Chart } = monitor.panelWin;
+
+ let pie = Chart.Pie(document, {
+ width: 100,
+ height: 100,
+ data: [{
+ size: 1,
+ label: "foo"
+ }, {
+ size: 2,
+ label: "bar"
+ }, {
+ size: 3,
+ label: "baz"
+ }]
+ });
+
+ let node = pie.node;
+ let slices = node.querySelectorAll(".pie-chart-slice.chart-colored-blob");
+ let labels = node.querySelectorAll(".pie-chart-label");
+
+ ok(node.classList.contains("pie-chart-container") &&
+ node.classList.contains("generic-chart-container"),
+ "A pie chart container was created successfully.");
+
+ is(slices.length, 3,
+ "There should be 3 pie chart slices created.");
+ ok(slices[0].getAttribute("d").match(
+ /\s*M 50,50 L 49\.\d+,97\.\d+ A 47\.5,47\.5 0 0 1 49\.\d+,2\.5\d* Z/),
+ "The first slice has the correct data.");
+ ok(slices[1].getAttribute("d").match(
+ /\s*M 50,50 L 91\.\d+,26\.\d+ A 47\.5,47\.5 0 0 1 49\.\d+,97\.\d+ Z/),
+ "The second slice has the correct data.");
+ ok(slices[2].getAttribute("d").match(
+ /\s*M 50,50 L 50\.\d+,2\.5\d* A 47\.5,47\.5 0 0 1 91\.\d+,26\.\d+ Z/),
+ "The third slice has the correct data.");
+
+ ok(slices[0].hasAttribute("largest"),
+ "The first slice should be the largest one.");
+ ok(slices[2].hasAttribute("smallest"),
+ "The third slice should be the smallest one.");
+
+ ok(slices[0].getAttribute("name"), "baz",
+ "The first slice's name is correct.");
+ ok(slices[1].getAttribute("name"), "bar",
+ "The first slice's name is correct.");
+ ok(slices[2].getAttribute("name"), "foo",
+ "The first slice's name is correct.");
+
+ is(labels.length, 3,
+ "There should be 3 pie chart labels created.");
+ is(labels[0].textContent, "baz",
+ "The first label's text is correct.");
+ is(labels[1].textContent, "bar",
+ "The first label's text is correct.");
+ is(labels[2].textContent, "foo",
+ "The first label's text is correct.");
+
+ yield teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_charts-02.js b/devtools/client/netmonitor/test/browser_net_charts-02.js
new file mode 100644
index 000000000..ae53147f0
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_charts-02.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Makes sure Pie Charts have the right internal structure when
+ * initialized with empty data.
+ */
+
+add_task(function* () {
+ let { L10N } = require("devtools/client/netmonitor/l10n");
+
+ let { monitor } = yield initNetMonitor(SIMPLE_URL);
+ info("Starting test... ");
+
+ let { document, Chart } = monitor.panelWin;
+
+ let pie = Chart.Pie(document, {
+ data: null,
+ width: 100,
+ height: 100
+ });
+
+ let node = pie.node;
+ let slices = node.querySelectorAll(".pie-chart-slice.chart-colored-blob");
+ let labels = node.querySelectorAll(".pie-chart-label");
+
+ ok(node.classList.contains("pie-chart-container") &&
+ node.classList.contains("generic-chart-container"),
+ "A pie chart container was created successfully.");
+
+ is(slices.length, 1, "There should be 1 pie chart slice created.");
+ ok(slices[0].getAttribute("d").match(
+ /\s*M 50,50 L 50\.\d+,2\.5\d* A 47\.5,47\.5 0 1 1 49\.\d+,2\.5\d* Z/),
+ "The first slice has the correct data.");
+
+ ok(slices[0].hasAttribute("largest"),
+ "The first slice should be the largest one.");
+ ok(slices[0].hasAttribute("smallest"),
+ "The first slice should also be the smallest one.");
+ ok(slices[0].getAttribute("name"), L10N.getStr("pieChart.loading"),
+ "The first slice's name is correct.");
+
+ is(labels.length, 1, "There should be 1 pie chart label created.");
+ is(labels[0].textContent, "Loading", "The first label's text is correct.");
+
+ yield teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_charts-03.js b/devtools/client/netmonitor/test/browser_net_charts-03.js
new file mode 100644
index 000000000..c7d9b0c1a
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_charts-03.js
@@ -0,0 +1,106 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Makes sure Table Charts have the right internal structure.
+ */
+
+add_task(function* () {
+ let { L10N } = require("devtools/client/netmonitor/l10n");
+
+ let { monitor } = yield initNetMonitor(SIMPLE_URL);
+ info("Starting test... ");
+
+ let { document, Chart } = monitor.panelWin;
+
+ let table = Chart.Table(document, {
+ title: "Table title",
+ data: [{
+ label1: 1,
+ label2: 11.1
+ }, {
+ label1: 2,
+ label2: 12.2
+ }, {
+ label1: 3,
+ label2: 13.3
+ }],
+ strings: {
+ label2: (value, index) => value + ["foo", "bar", "baz"][index]
+ },
+ totals: {
+ label1: value => "Hello " + L10N.numberWithDecimals(value, 2),
+ label2: value => "World " + L10N.numberWithDecimals(value, 2)
+ }
+ });
+
+ let node = table.node;
+ let title = node.querySelector(".table-chart-title");
+ let grid = node.querySelector(".table-chart-grid");
+ let totals = node.querySelector(".table-chart-totals");
+ let rows = grid.querySelectorAll(".table-chart-row");
+ let sums = node.querySelectorAll(".table-chart-summary-label");
+
+ ok(node.classList.contains("table-chart-container") &&
+ node.classList.contains("generic-chart-container"),
+ "A table chart container was created successfully.");
+
+ ok(title, "A title node was created successfully.");
+ is(title.getAttribute("value"), "Table title",
+ "The title node displays the correct text.");
+
+ is(rows.length, 3, "There should be 3 table chart rows created.");
+
+ ok(rows[0].querySelector(".table-chart-row-box.chart-colored-blob"),
+ "A colored blob exists for the firt row.");
+ is(rows[0].querySelectorAll("label")[0].getAttribute("name"), "label1",
+ "The first column of the first row exists.");
+ is(rows[0].querySelectorAll("label")[1].getAttribute("name"), "label2",
+ "The second column of the first row exists.");
+ is(rows[0].querySelectorAll("label")[0].getAttribute("value"), "1",
+ "The first column of the first row displays the correct text.");
+ is(rows[0].querySelectorAll("label")[1].getAttribute("value"), "11.1foo",
+ "The second column of the first row displays the correct text.");
+
+ ok(rows[1].querySelector(".table-chart-row-box.chart-colored-blob"),
+ "A colored blob exists for the second row.");
+ is(rows[1].querySelectorAll("label")[0].getAttribute("name"), "label1",
+ "The first column of the second row exists.");
+ is(rows[1].querySelectorAll("label")[1].getAttribute("name"), "label2",
+ "The second column of the second row exists.");
+ is(rows[1].querySelectorAll("label")[0].getAttribute("value"), "2",
+ "The first column of the second row displays the correct text.");
+ is(rows[1].querySelectorAll("label")[1].getAttribute("value"), "12.2bar",
+ "The second column of the first row displays the correct text.");
+
+ ok(rows[2].querySelector(".table-chart-row-box.chart-colored-blob"),
+ "A colored blob exists for the third row.");
+ is(rows[2].querySelectorAll("label")[0].getAttribute("name"), "label1",
+ "The first column of the third row exists.");
+ is(rows[2].querySelectorAll("label")[1].getAttribute("name"), "label2",
+ "The second column of the third row exists.");
+ is(rows[2].querySelectorAll("label")[0].getAttribute("value"), "3",
+ "The first column of the third row displays the correct text.");
+ is(rows[2].querySelectorAll("label")[1].getAttribute("value"), "13.3baz",
+ "The second column of the third row displays the correct text.");
+
+ is(sums.length, 2, "There should be 2 total summaries created.");
+
+ is(totals.querySelectorAll(".table-chart-summary-label")[0].getAttribute("name"),
+ "label1",
+ "The first sum's type is correct.");
+ is(totals.querySelectorAll(".table-chart-summary-label")[0].getAttribute("value"),
+ "Hello 6",
+ "The first sum's value is correct.");
+
+ is(totals.querySelectorAll(".table-chart-summary-label")[1].getAttribute("name"),
+ "label2",
+ "The second sum's type is correct.");
+ is(totals.querySelectorAll(".table-chart-summary-label")[1].getAttribute("value"),
+ "World 36.60",
+ "The second sum's value is correct.");
+
+ yield teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_charts-04.js b/devtools/client/netmonitor/test/browser_net_charts-04.js
new file mode 100644
index 000000000..0d150c409
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_charts-04.js
@@ -0,0 +1,75 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Makes sure Pie Charts have the right internal structure when
+ * initialized with empty data.
+ */
+
+add_task(function* () {
+ let { L10N } = require("devtools/client/netmonitor/l10n");
+
+ let { monitor } = yield initNetMonitor(SIMPLE_URL);
+ info("Starting test... ");
+
+ let { document, Chart } = monitor.panelWin;
+
+ let table = Chart.Table(document, {
+ title: "Table title",
+ data: null,
+ totals: {
+ label1: value => "Hello " + L10N.numberWithDecimals(value, 2),
+ label2: value => "World " + L10N.numberWithDecimals(value, 2)
+ }
+ });
+
+ let node = table.node;
+ let title = node.querySelector(".table-chart-title");
+ let grid = node.querySelector(".table-chart-grid");
+ let totals = node.querySelector(".table-chart-totals");
+ let rows = grid.querySelectorAll(".table-chart-row");
+ let sums = node.querySelectorAll(".table-chart-summary-label");
+
+ ok(node.classList.contains("table-chart-container") &&
+ node.classList.contains("generic-chart-container"),
+ "A table chart container was created successfully.");
+
+ ok(title, "A title node was created successfully.");
+ is(title.getAttribute("value"), "Table title",
+ "The title node displays the correct text.");
+
+ is(rows.length, 1, "There should be 1 table chart row created.");
+
+ ok(rows[0].querySelector(".table-chart-row-box.chart-colored-blob"),
+ "A colored blob exists for the firt row.");
+ is(rows[0].querySelectorAll("label")[0].getAttribute("name"), "size",
+ "The first column of the first row exists.");
+ is(rows[0].querySelectorAll("label")[1].getAttribute("name"), "label",
+ "The second column of the first row exists.");
+ is(rows[0].querySelectorAll("label")[0].getAttribute("value"), "",
+ "The first column of the first row displays the correct text.");
+ is(rows[0].querySelectorAll("label")[1].getAttribute("value"),
+ L10N.getStr("tableChart.loading"),
+ "The second column of the first row displays the correct text.");
+
+ is(sums.length, 2,
+ "There should be 2 total summaries created.");
+
+ is(totals.querySelectorAll(".table-chart-summary-label")[0].getAttribute("name"),
+ "label1",
+ "The first sum's type is correct.");
+ is(totals.querySelectorAll(".table-chart-summary-label")[0].getAttribute("value"),
+ "Hello 0",
+ "The first sum's value is correct.");
+
+ is(totals.querySelectorAll(".table-chart-summary-label")[1].getAttribute("name"),
+ "label2",
+ "The second sum's type is correct.");
+ is(totals.querySelectorAll(".table-chart-summary-label")[1].getAttribute("value"),
+ "World 0",
+ "The second sum's value is correct.");
+
+ yield teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_charts-05.js b/devtools/client/netmonitor/test/browser_net_charts-05.js
new file mode 100644
index 000000000..00445b132
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_charts-05.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Makes sure Pie+Table Charts have the right internal structure.
+ */
+
+add_task(function* () {
+ let { L10N } = require("devtools/client/netmonitor/l10n");
+
+ let { monitor } = yield initNetMonitor(SIMPLE_URL);
+ info("Starting test... ");
+
+ let { document, Chart } = monitor.panelWin;
+
+ let chart = Chart.PieTable(document, {
+ title: "Table title",
+ data: [{
+ size: 1,
+ label: 11.1
+ }, {
+ size: 2,
+ label: 12.2
+ }, {
+ size: 3,
+ label: 13.3
+ }],
+ strings: {
+ label2: (value, index) => value + ["foo", "bar", "baz"][index]
+ },
+ totals: {
+ size: value => "Hello " + L10N.numberWithDecimals(value, 2),
+ label: value => "World " + L10N.numberWithDecimals(value, 2)
+ }
+ });
+
+ ok(chart.pie, "The pie chart proxy is accessible.");
+ ok(chart.table, "The table chart proxy is accessible.");
+
+ let node = chart.node;
+ let rows = node.querySelectorAll(".table-chart-row");
+ let sums = node.querySelectorAll(".table-chart-summary-label");
+
+ ok(node.classList.contains("pie-table-chart-container"),
+ "A pie+table chart container was created successfully.");
+
+ ok(node.querySelector(".table-chart-title"),
+ "A title node was created successfully.");
+ ok(node.querySelector(".pie-chart-container"),
+ "A pie chart was created successfully.");
+ ok(node.querySelector(".table-chart-container"),
+ "A table chart was created successfully.");
+
+ is(rows.length, 3, "There should be 3 pie chart slices created.");
+ is(rows.length, 3, "There should be 3 table chart rows created.");
+ is(sums.length, 2, "There should be 2 total summaries created.");
+
+ yield teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_charts-06.js b/devtools/client/netmonitor/test/browser_net_charts-06.js
new file mode 100644
index 000000000..4bb70e53e
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_charts-06.js
@@ -0,0 +1,47 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Makes sure Pie Charts correctly handle empty source data.
+ */
+
+add_task(function* () {
+ let { L10N } = require("devtools/client/netmonitor/l10n");
+
+ let { monitor } = yield initNetMonitor(SIMPLE_URL);
+ info("Starting test... ");
+
+ let { document, Chart } = monitor.panelWin;
+
+ let pie = Chart.Pie(document, {
+ data: [],
+ width: 100,
+ height: 100
+ });
+
+ let node = pie.node;
+ let slices = node.querySelectorAll(".pie-chart-slice.chart-colored-blob");
+ let labels = node.querySelectorAll(".pie-chart-label");
+
+ is(slices.length, 1,
+ "There should be 1 pie chart slice created.");
+ ok(slices[0].getAttribute("d").match(
+ /\s*M 50,50 L 50\.\d+,2\.5\d* A 47\.5,47\.5 0 1 1 49\.\d+,2\.5\d* Z/),
+ "The slice has the correct data.");
+
+ ok(slices[0].hasAttribute("largest"),
+ "The slice should be the largest one.");
+ ok(slices[0].hasAttribute("smallest"),
+ "The slice should also be the smallest one.");
+ ok(slices[0].getAttribute("name"), L10N.getStr("pieChart.unavailable"),
+ "The slice's name is correct.");
+
+ is(labels.length, 1,
+ "There should be 1 pie chart label created.");
+ is(labels[0].textContent, "Empty",
+ "The label's text is correct.");
+
+ yield teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_charts-07.js b/devtools/client/netmonitor/test/browser_net_charts-07.js
new file mode 100644
index 000000000..bb992e4eb
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_charts-07.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Makes sure Table Charts correctly handle empty source data.
+ */
+
+add_task(function* () {
+ let { L10N } = require("devtools/client/netmonitor/l10n");
+
+ let { monitor } = yield initNetMonitor(SIMPLE_URL);
+ info("Starting test... ");
+
+ let { document, Chart } = monitor.panelWin;
+
+ let table = Chart.Table(document, {
+ data: [],
+ totals: {
+ label1: value => "Hello " + L10N.numberWithDecimals(value, 2),
+ label2: value => "World " + L10N.numberWithDecimals(value, 2)
+ }
+ });
+
+ let node = table.node;
+ let grid = node.querySelector(".table-chart-grid");
+ let totals = node.querySelector(".table-chart-totals");
+ let rows = grid.querySelectorAll(".table-chart-row");
+ let sums = node.querySelectorAll(".table-chart-summary-label");
+
+ is(rows.length, 1, "There should be 1 table chart row created.");
+
+ ok(rows[0].querySelector(".table-chart-row-box.chart-colored-blob"),
+ "A colored blob exists for the firt row.");
+ is(rows[0].querySelectorAll("label")[0].getAttribute("name"), "size",
+ "The first column of the first row exists.");
+ is(rows[0].querySelectorAll("label")[1].getAttribute("name"), "label",
+ "The second column of the first row exists.");
+ is(rows[0].querySelectorAll("label")[0].getAttribute("value"), "",
+ "The first column of the first row displays the correct text.");
+ is(rows[0].querySelectorAll("label")[1].getAttribute("value"),
+ L10N.getStr("tableChart.unavailable"),
+ "The second column of the first row displays the correct text.");
+
+ is(sums.length, 2, "There should be 2 total summaries created.");
+
+ is(totals.querySelectorAll(".table-chart-summary-label")[0].getAttribute("name"),
+ "label1",
+ "The first sum's type is correct.");
+ is(totals.querySelectorAll(".table-chart-summary-label")[0].getAttribute("value"),
+ "Hello 0",
+ "The first sum's value is correct.");
+
+ is(totals.querySelectorAll(".table-chart-summary-label")[1].getAttribute("name"),
+ "label2",
+ "The second sum's type is correct.");
+ is(totals.querySelectorAll(".table-chart-summary-label")[1].getAttribute("value"),
+ "World 0",
+ "The second sum's value is correct.");
+
+ yield teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_clear.js b/devtools/client/netmonitor/test/browser_net_clear.js
new file mode 100644
index 000000000..94a60cd39
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_clear.js
@@ -0,0 +1,77 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if the clear button empties the request menu.
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(SIMPLE_URL);
+ info("Starting test... ");
+
+ let { $, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+ let detailsPane = $("#details-pane");
+ let detailsPaneToggleButton = $("#details-pane-toggle");
+ let clearButton = $("#requests-menu-clear-button");
+
+ RequestsMenu.lazyUpdate = false;
+
+ // Make sure we start in a sane state
+ assertNoRequestState(RequestsMenu, detailsPaneToggleButton);
+
+ // Load one request and assert it shows up in the list
+ let networkEvent = monitor.panelWin.once(monitor.panelWin.EVENTS.NETWORK_EVENT);
+ tab.linkedBrowser.reload();
+ yield networkEvent;
+
+ assertSingleRequestState();
+
+ // Click clear and make sure the requests are gone
+ EventUtils.sendMouseEvent({ type: "click" }, clearButton);
+ assertNoRequestState();
+
+ // Load a second request and make sure they still show up
+ networkEvent = monitor.panelWin.once(monitor.panelWin.EVENTS.NETWORK_EVENT);
+ tab.linkedBrowser.reload();
+ yield networkEvent;
+
+ assertSingleRequestState();
+
+ // Make sure we can now open the details pane
+ NetMonitorView.toggleDetailsPane({ visible: true, animated: false });
+ ok(!detailsPane.classList.contains("pane-collapsed") &&
+ !detailsPaneToggleButton.classList.contains("pane-collapsed"),
+ "The details pane should be visible after clicking the toggle button.");
+
+ // Click clear and make sure the details pane closes
+ EventUtils.sendMouseEvent({ type: "click" }, clearButton);
+ assertNoRequestState();
+ ok(detailsPane.classList.contains("pane-collapsed") &&
+ detailsPaneToggleButton.classList.contains("pane-collapsed"),
+ "The details pane should not be visible clicking 'clear'.");
+
+ return teardown(monitor);
+
+ /**
+ * Asserts the state of the network monitor when one request has loaded
+ */
+ function assertSingleRequestState() {
+ is(RequestsMenu.itemCount, 1,
+ "The request menu should have one item at this point.");
+ is(detailsPaneToggleButton.hasAttribute("disabled"), false,
+ "The pane toggle button should be enabled after a request is made.");
+ }
+
+ /**
+ * Asserts the state of the network monitor when no requests have loaded
+ */
+ function assertNoRequestState() {
+ is(RequestsMenu.itemCount, 0,
+ "The request menu should be empty at this point.");
+ is(detailsPaneToggleButton.hasAttribute("disabled"), true,
+ "The pane toggle button should be disabled when the request menu is cleared.");
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_complex-params.js b/devtools/client/netmonitor/test/browser_net_complex-params.js
new file mode 100644
index 000000000..103c644bb
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_complex-params.js
@@ -0,0 +1,195 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests whether complex request params and payload sent via POST are
+ * displayed correctly.
+ */
+
+add_task(function* () {
+ let { L10N } = require("devtools/client/netmonitor/l10n");
+
+ let { tab, monitor } = yield initNetMonitor(PARAMS_URL);
+ info("Starting test... ");
+
+ let { document, EVENTS, Editor, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu, NetworkDetails } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+ NetworkDetails._params.lazyEmpty = false;
+
+ let wait = waitForNetworkEvents(monitor, 1, 6);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests();
+ });
+ yield wait;
+
+ let onEvent = monitor.panelWin.once(EVENTS.REQUEST_POST_PARAMS_DISPLAYED);
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.getElementById("details-pane-toggle"));
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.querySelectorAll("#details-pane tab")[2]);
+ yield onEvent;
+ yield testParamsTab1("a", '""', '{ "foo": "bar" }', '""');
+
+ onEvent = monitor.panelWin.once(EVENTS.REQUEST_POST_PARAMS_DISPLAYED);
+ RequestsMenu.selectedIndex = 1;
+ yield onEvent;
+ yield testParamsTab1("a", '"b"', '{ "foo": "bar" }', '""');
+
+ onEvent = monitor.panelWin.once(EVENTS.REQUEST_POST_PARAMS_DISPLAYED);
+ RequestsMenu.selectedIndex = 2;
+ yield onEvent;
+ yield testParamsTab1("a", '"b"', "foo", '"bar"');
+
+ onEvent = monitor.panelWin.once(EVENTS.REQUEST_POST_PARAMS_DISPLAYED);
+ RequestsMenu.selectedIndex = 3;
+ yield onEvent;
+ yield testParamsTab2("a", '""', '{ "foo": "bar" }', "js");
+
+ onEvent = monitor.panelWin.once(EVENTS.REQUEST_POST_PARAMS_DISPLAYED);
+ RequestsMenu.selectedIndex = 4;
+ yield onEvent;
+ yield testParamsTab2("a", '"b"', '{ "foo": "bar" }', "js");
+
+ onEvent = monitor.panelWin.once(EVENTS.REQUEST_POST_PARAMS_DISPLAYED);
+ RequestsMenu.selectedIndex = 5;
+ yield onEvent;
+ yield testParamsTab2("a", '"b"', "?foo=bar", "text");
+
+ onEvent = monitor.panelWin.once(EVENTS.SIDEBAR_POPULATED);
+ RequestsMenu.selectedIndex = 6;
+ yield onEvent;
+ yield testParamsTab3("a", '"b"');
+
+ yield teardown(monitor);
+
+ function testParamsTab1(queryStringParamName, queryStringParamValue,
+ formDataParamName, formDataParamValue) {
+ let tabpanel = document.querySelectorAll("#details-pane tabpanel")[2];
+
+ is(tabpanel.querySelectorAll(".variables-view-scope").length, 2,
+ "The number of param scopes displayed in this tabpanel is incorrect.");
+ is(tabpanel.querySelectorAll(".variable-or-property").length, 2,
+ "The number of param values displayed in this tabpanel is incorrect.");
+ is(tabpanel.querySelectorAll(".variables-view-empty-notice").length, 0,
+ "The empty notice should not be displayed in this tabpanel.");
+
+ is(tabpanel.querySelector("#request-params-box")
+ .hasAttribute("hidden"), false,
+ "The request params box should not be hidden.");
+ is(tabpanel.querySelector("#request-post-data-textarea-box")
+ .hasAttribute("hidden"), true,
+ "The request post data textarea box should be hidden.");
+
+ let paramsScope = tabpanel.querySelectorAll(".variables-view-scope")[0];
+ let formDataScope = tabpanel.querySelectorAll(".variables-view-scope")[1];
+
+ is(paramsScope.querySelector(".name").getAttribute("value"),
+ L10N.getStr("paramsQueryString"),
+ "The params scope doesn't have the correct title.");
+ is(formDataScope.querySelector(".name").getAttribute("value"),
+ L10N.getStr("paramsFormData"),
+ "The form data scope doesn't have the correct title.");
+
+ is(paramsScope.querySelectorAll(".variables-view-variable .name")[0]
+ .getAttribute("value"),
+ queryStringParamName,
+ "The first query string param name was incorrect.");
+ is(paramsScope.querySelectorAll(".variables-view-variable .value")[0]
+ .getAttribute("value"),
+ queryStringParamValue,
+ "The first query string param value was incorrect.");
+
+ is(formDataScope.querySelectorAll(".variables-view-variable .name")[0]
+ .getAttribute("value"),
+ formDataParamName,
+ "The first form data param name was incorrect.");
+ is(formDataScope.querySelectorAll(".variables-view-variable .value")[0]
+ .getAttribute("value"),
+ formDataParamValue,
+ "The first form data param value was incorrect.");
+ }
+
+ function* testParamsTab2(queryStringParamName, queryStringParamValue,
+ requestPayload, editorMode) {
+ let isJSON = editorMode == "js";
+ let tabpanel = document.querySelectorAll("#details-pane tabpanel")[2];
+
+ is(tabpanel.querySelectorAll(".variables-view-scope").length, 2,
+ "The number of param scopes displayed in this tabpanel is incorrect.");
+ is(tabpanel.querySelectorAll(".variable-or-property").length, isJSON ? 4 : 1,
+ "The number of param values displayed in this tabpanel is incorrect.");
+ is(tabpanel.querySelectorAll(".variables-view-empty-notice").length, 0,
+ "The empty notice should not be displayed in this tabpanel.");
+
+ is(tabpanel.querySelector("#request-params-box")
+ .hasAttribute("hidden"), false,
+ "The request params box should not be hidden.");
+ is(tabpanel.querySelector("#request-post-data-textarea-box")
+ .hasAttribute("hidden"), isJSON,
+ "The request post data textarea box should be hidden.");
+
+ let paramsScope = tabpanel.querySelectorAll(".variables-view-scope")[0];
+ let payloadScope = tabpanel.querySelectorAll(".variables-view-scope")[1];
+
+ is(paramsScope.querySelector(".name").getAttribute("value"),
+ L10N.getStr("paramsQueryString"),
+ "The params scope doesn't have the correct title.");
+ is(payloadScope.querySelector(".name").getAttribute("value"),
+ isJSON ? L10N.getStr("jsonScopeName") : L10N.getStr("paramsPostPayload"),
+ "The request payload scope doesn't have the correct title.");
+
+ is(paramsScope.querySelectorAll(".variables-view-variable .name")[0]
+ .getAttribute("value"),
+ queryStringParamName,
+ "The first query string param name was incorrect.");
+ is(paramsScope.querySelectorAll(".variables-view-variable .value")[0]
+ .getAttribute("value"),
+ queryStringParamValue,
+ "The first query string param value was incorrect.");
+
+ if (isJSON) {
+ let requestPayloadObject = JSON.parse(requestPayload);
+ let requestPairs = Object.keys(requestPayloadObject)
+ .map(k => [k, requestPayloadObject[k]]);
+ let displayedNames = payloadScope.querySelectorAll(
+ ".variables-view-property.variable-or-property .name");
+ let displayedValues = payloadScope.querySelectorAll(
+ ".variables-view-property.variable-or-property .value");
+ for (let i = 0; i < requestPairs.length; i++) {
+ let [requestPayloadName, requestPayloadValue] = requestPairs[i];
+ is(requestPayloadName, displayedNames[i].getAttribute("value"),
+ "JSON property name " + i + " should be displayed correctly");
+ is('"' + requestPayloadValue + '"', displayedValues[i].getAttribute("value"),
+ "JSON property value " + i + " should be displayed correctly");
+ }
+ } else {
+ let editor = yield NetMonitorView.editor("#request-post-data-textarea");
+ is(editor.getText(), requestPayload,
+ "The text shown in the source editor is incorrect.");
+ is(editor.getMode(), Editor.modes[editorMode],
+ "The mode active in the source editor is incorrect.");
+ }
+ }
+
+ function testParamsTab3(queryStringParamName, queryStringParamValue) {
+ let tabpanel = document.querySelectorAll("#details-pane tabpanel")[2];
+
+ is(tabpanel.querySelectorAll(".variables-view-scope").length, 0,
+ "The number of param scopes displayed in this tabpanel is incorrect.");
+ is(tabpanel.querySelectorAll(".variable-or-property").length, 0,
+ "The number of param values displayed in this tabpanel is incorrect.");
+ is(tabpanel.querySelectorAll(".variables-view-empty-notice").length, 1,
+ "The empty notice should be displayed in this tabpanel.");
+
+ is(tabpanel.querySelector("#request-params-box")
+ .hasAttribute("hidden"), false,
+ "The request params box should not be hidden.");
+ is(tabpanel.querySelector("#request-post-data-textarea-box")
+ .hasAttribute("hidden"), true,
+ "The request post data textarea box should be hidden.");
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_content-type.js b/devtools/client/netmonitor/test/browser_net_content-type.js
new file mode 100644
index 000000000..1951bc69d
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_content-type.js
@@ -0,0 +1,255 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if different response content types are handled correctly.
+ */
+
+add_task(function* () {
+ let { L10N } = require("devtools/client/netmonitor/l10n");
+
+ let { tab, monitor } = yield initNetMonitor(CONTENT_TYPE_WITHOUT_CACHE_URL);
+ info("Starting test... ");
+
+ let { document, Editor, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, CONTENT_TYPE_WITHOUT_CACHE_REQUESTS);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests();
+ });
+ yield wait;
+
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(0),
+ "GET", CONTENT_TYPE_SJS + "?fmt=xml", {
+ status: 200,
+ statusText: "OK",
+ type: "xml",
+ fullMimeType: "text/xml; charset=utf-8",
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 42),
+ time: true
+ });
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(1),
+ "GET", CONTENT_TYPE_SJS + "?fmt=css", {
+ status: 200,
+ statusText: "OK",
+ type: "css",
+ fullMimeType: "text/css; charset=utf-8",
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 34),
+ time: true
+ });
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(2),
+ "GET", CONTENT_TYPE_SJS + "?fmt=js", {
+ status: 200,
+ statusText: "OK",
+ type: "js",
+ fullMimeType: "application/javascript; charset=utf-8",
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 34),
+ time: true
+ });
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(3),
+ "GET", CONTENT_TYPE_SJS + "?fmt=json", {
+ status: 200,
+ statusText: "OK",
+ type: "json",
+ fullMimeType: "application/json; charset=utf-8",
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 29),
+ time: true
+ });
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(4),
+ "GET", CONTENT_TYPE_SJS + "?fmt=bogus", {
+ status: 404,
+ statusText: "Not Found",
+ type: "html",
+ fullMimeType: "text/html; charset=utf-8",
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 24),
+ time: true
+ });
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(5),
+ "GET", TEST_IMAGE, {
+ fuzzyUrl: true,
+ status: 200,
+ statusText: "OK",
+ type: "png",
+ fullMimeType: "image/png",
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 580),
+ time: true
+ });
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(6),
+ "GET", CONTENT_TYPE_SJS + "?fmt=gzip", {
+ status: 200,
+ statusText: "OK",
+ type: "plain",
+ fullMimeType: "text/plain",
+ transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 73),
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 10.73),
+ time: true
+ });
+
+ let onEvent = waitForResponseBodyDisplayed();
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.getElementById("details-pane-toggle"));
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.querySelectorAll("#details-pane tab")[3]);
+ yield onEvent;
+ yield testResponseTab("xml");
+
+ yield selectIndexAndWaitForTabUpdated(1);
+ yield testResponseTab("css");
+
+ yield selectIndexAndWaitForTabUpdated(2);
+ yield testResponseTab("js");
+
+ yield selectIndexAndWaitForTabUpdated(3);
+ yield testResponseTab("json");
+
+ yield selectIndexAndWaitForTabUpdated(4);
+ yield testResponseTab("html");
+
+ yield selectIndexAndWaitForTabUpdated(5);
+ yield testResponseTab("png");
+
+ yield selectIndexAndWaitForTabUpdated(6);
+ yield testResponseTab("gzip");
+
+ yield teardown(monitor);
+
+ function* testResponseTab(type) {
+ let tabEl = document.querySelectorAll("#details-pane tab")[3];
+ let tabpanel = document.querySelectorAll("#details-pane tabpanel")[3];
+
+ is(tabEl.getAttribute("selected"), "true",
+ "The response tab in the network details pane should be selected.");
+
+ function checkVisibility(box) {
+ is(tabpanel.querySelector("#response-content-info-header")
+ .hasAttribute("hidden"), true,
+ "The response info header doesn't have the intended visibility.");
+ is(tabpanel.querySelector("#response-content-json-box")
+ .hasAttribute("hidden"), box != "json",
+ "The response content json box doesn't have the intended visibility.");
+ is(tabpanel.querySelector("#response-content-textarea-box")
+ .hasAttribute("hidden"), box != "textarea",
+ "The response content textarea box doesn't have the intended visibility.");
+ is(tabpanel.querySelector("#response-content-image-box")
+ .hasAttribute("hidden"), box != "image",
+ "The response content image box doesn't have the intended visibility.");
+ }
+
+ switch (type) {
+ case "xml": {
+ checkVisibility("textarea");
+
+ let editor = yield NetMonitorView.editor("#response-content-textarea");
+ is(editor.getText(), "<label value='greeting'>Hello XML!</label>",
+ "The text shown in the source editor is incorrect for the xml request.");
+ is(editor.getMode(), Editor.modes.html,
+ "The mode active in the source editor is incorrect for the xml request.");
+ break;
+ }
+ case "css": {
+ checkVisibility("textarea");
+
+ let editor = yield NetMonitorView.editor("#response-content-textarea");
+ is(editor.getText(), "body:pre { content: 'Hello CSS!' }",
+ "The text shown in the source editor is incorrect for the xml request.");
+ is(editor.getMode(), Editor.modes.css,
+ "The mode active in the source editor is incorrect for the xml request.");
+ break;
+ }
+ case "js": {
+ checkVisibility("textarea");
+
+ let editor = yield NetMonitorView.editor("#response-content-textarea");
+ is(editor.getText(), "function() { return 'Hello JS!'; }",
+ "The text shown in the source editor is incorrect for the xml request.");
+ is(editor.getMode(), Editor.modes.js,
+ "The mode active in the source editor is incorrect for the xml request.");
+ break;
+ }
+ case "json": {
+ checkVisibility("json");
+
+ is(tabpanel.querySelectorAll(".variables-view-scope").length, 1,
+ "There should be 1 json scope displayed in this tabpanel.");
+ is(tabpanel.querySelectorAll(".variables-view-property").length, 2,
+ "There should be 2 json properties displayed in this tabpanel.");
+ is(tabpanel.querySelectorAll(".variables-view-empty-notice").length, 0,
+ "The empty notice should not be displayed in this tabpanel.");
+
+ let jsonScope = tabpanel.querySelectorAll(".variables-view-scope")[0];
+
+ is(jsonScope.querySelector(".name").getAttribute("value"),
+ L10N.getStr("jsonScopeName"),
+ "The json scope doesn't have the correct title.");
+
+ is(jsonScope.querySelectorAll(".variables-view-property .name")[0]
+ .getAttribute("value"),
+ "greeting", "The first json property name was incorrect.");
+ is(jsonScope.querySelectorAll(".variables-view-property .value")[0]
+ .getAttribute("value"),
+ "\"Hello JSON!\"", "The first json property value was incorrect.");
+
+ is(jsonScope.querySelectorAll(".variables-view-property .name")[1]
+ .getAttribute("value"),
+ "__proto__", "The second json property name was incorrect.");
+ is(jsonScope.querySelectorAll(".variables-view-property .value")[1]
+ .getAttribute("value"),
+ "Object", "The second json property value was incorrect.");
+ break;
+ }
+ case "html": {
+ checkVisibility("textarea");
+
+ let editor = yield NetMonitorView.editor("#response-content-textarea");
+ is(editor.getText(), "<blink>Not Found</blink>",
+ "The text shown in the source editor is incorrect for the xml request.");
+ is(editor.getMode(), Editor.modes.html,
+ "The mode active in the source editor is incorrect for the xml request.");
+ break;
+ }
+ case "png": {
+ checkVisibility("image");
+
+ let imageNode = tabpanel.querySelector("#response-content-image");
+ yield once(imageNode, "load");
+
+ is(tabpanel.querySelector("#response-content-image-name-value")
+ .getAttribute("value"), "test-image.png",
+ "The image name info isn't correct.");
+ is(tabpanel.querySelector("#response-content-image-mime-value")
+ .getAttribute("value"), "image/png",
+ "The image mime info isn't correct.");
+ is(tabpanel.querySelector("#response-content-image-dimensions-value")
+ .getAttribute("value"), "16" + " \u00D7 " + "16",
+ "The image dimensions info isn't correct.");
+ break;
+ }
+ case "gzip": {
+ checkVisibility("textarea");
+
+ let expected = new Array(1000).join("Hello gzip!");
+ let editor = yield NetMonitorView.editor("#response-content-textarea");
+ is(editor.getText(), expected,
+ "The text shown in the source editor is incorrect for the gzip request.");
+ is(editor.getMode(), Editor.modes.text,
+ "The mode active in the source editor is incorrect for the gzip request.");
+ break;
+ }
+ }
+ }
+
+ function selectIndexAndWaitForTabUpdated(index) {
+ let onTabUpdated = monitor.panelWin.once(monitor.panelWin.EVENTS.TAB_UPDATED);
+ RequestsMenu.selectedIndex = index;
+ return onTabUpdated;
+ }
+
+ function waitForResponseBodyDisplayed() {
+ return monitor.panelWin.once(monitor.panelWin.EVENTS.RESPONSE_BODY_DISPLAYED);
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_copy_as_curl.js b/devtools/client/netmonitor/test/browser_net_copy_as_curl.js
new file mode 100644
index 000000000..9cf66aa4f
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_copy_as_curl.js
@@ -0,0 +1,88 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if Copy as cURL works.
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(CURL_URL);
+ info("Starting test... ");
+
+ // Different quote chars are used for Windows and POSIX
+ const QUOTE = Services.appinfo.OS == "WINNT" ? "\"" : "'";
+
+ // Quote a string, escape the quotes inside the string
+ function quote(str) {
+ return QUOTE + str.replace(new RegExp(QUOTE, "g"), `\\${QUOTE}`) + QUOTE;
+ }
+
+ // Header param is formatted as -H "Header: value" or -H 'Header: value'
+ function header(h) {
+ return "-H " + quote(h);
+ }
+
+ // Construct the expected command
+ const EXPECTED_RESULT = [
+ "curl " + quote(SIMPLE_SJS),
+ "--compressed",
+ header("Host: example.com"),
+ header("User-Agent: " + navigator.userAgent),
+ header("Accept: */*"),
+ header("Accept-Language: " + navigator.language),
+ header("X-Custom-Header-1: Custom value"),
+ header("X-Custom-Header-2: 8.8.8.8"),
+ header("X-Custom-Header-3: Mon, 3 Mar 2014 11:11:11 GMT"),
+ header("Referer: " + CURL_URL),
+ header("Connection: keep-alive"),
+ header("Pragma: no-cache"),
+ header("Cache-Control: no-cache")
+ ];
+
+ let { NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, 1);
+ yield ContentTask.spawn(tab.linkedBrowser, SIMPLE_SJS, function* (url) {
+ content.wrappedJSObject.performRequest(url);
+ });
+ yield wait;
+
+ let requestItem = RequestsMenu.getItemAtIndex(0);
+ RequestsMenu.selectedItem = requestItem;
+
+ yield waitForClipboardPromise(function setup() {
+ RequestsMenu.contextMenu.copyAsCurl();
+ }, function validate(result) {
+ if (typeof result !== "string") {
+ return false;
+ }
+
+ // Different setups may produce the same command, but with the
+ // parameters in a different order in the commandline (which is fine).
+ // Here we confirm that the commands are the same even in that case.
+
+ // This monster regexp parses the command line into an array of arguments,
+ // recognizing quoted args with matching quotes and escaped quotes inside:
+ // [ "curl 'url'", "--standalone-arg", "-arg-with-quoted-string 'value\'s'" ]
+ let matchRe = /[-A-Za-z1-9]+(?: ([\"'])(?:\\\1|.)*?\1)?/g;
+
+ let actual = result.match(matchRe);
+
+ // Must begin with the same "curl 'URL'" segment
+ if (!actual || EXPECTED_RESULT[0] != actual[0]) {
+ return false;
+ }
+
+ // Must match each of the params in the middle (headers and --compressed)
+ return EXPECTED_RESULT.length === actual.length &&
+ EXPECTED_RESULT.every(param => actual.includes(param));
+ });
+
+ info("Clipboard contains a cURL command for the currently selected item's url.");
+
+ yield teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_copy_headers.js b/devtools/client/netmonitor/test/browser_net_copy_headers.js
new file mode 100644
index 000000000..36ce2fb34
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_copy_headers.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if copying a request's request/response headers works.
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(SIMPLE_URL);
+ info("Starting test... ");
+
+ let { NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, 1);
+ tab.linkedBrowser.reload();
+ yield wait;
+
+ let requestItem = RequestsMenu.getItemAtIndex(0);
+ RequestsMenu.selectedItem = requestItem;
+
+ let { method, httpVersion, status, statusText } = requestItem.attachment;
+
+ const EXPECTED_REQUEST_HEADERS = [
+ `${method} ${SIMPLE_URL} ${httpVersion}`,
+ "Host: example.com",
+ "User-Agent: " + navigator.userAgent + "",
+ "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
+ "Accept-Language: " + navigator.languages.join(",") + ";q=0.5",
+ "Accept-Encoding: gzip, deflate",
+ "Connection: keep-alive",
+ "Upgrade-Insecure-Requests: 1",
+ "Pragma: no-cache",
+ "Cache-Control: no-cache"
+ ].join("\n");
+
+ yield waitForClipboardPromise(function setup() {
+ RequestsMenu.contextMenu.copyRequestHeaders();
+ }, function validate(result) {
+ // Sometimes, a "Cookie" header is left over from other tests. Remove it:
+ result = String(result).replace(/Cookie: [^\n]+\n/, "");
+ return result === EXPECTED_REQUEST_HEADERS;
+ });
+ info("Clipboard contains the currently selected item's request headers.");
+
+ const EXPECTED_RESPONSE_HEADERS = [
+ `${httpVersion} ${status} ${statusText}`,
+ "Last-Modified: Sun, 3 May 2015 11:11:11 GMT",
+ "Content-Type: text/html",
+ "Content-Length: 465",
+ "Connection: close",
+ "Server: httpd.js",
+ "Date: Sun, 3 May 2015 11:11:11 GMT"
+ ].join("\n");
+
+ yield waitForClipboardPromise(function setup() {
+ RequestsMenu.contextMenu.copyResponseHeaders();
+ }, function validate(result) {
+ // Fake the "Last-Modified" and "Date" headers because they will vary:
+ result = String(result)
+ .replace(/Last-Modified: [^\n]+ GMT/, "Last-Modified: Sun, 3 May 2015 11:11:11 GMT")
+ .replace(/Date: [^\n]+ GMT/, "Date: Sun, 3 May 2015 11:11:11 GMT");
+ return result === EXPECTED_RESPONSE_HEADERS;
+ });
+ info("Clipboard contains the currently selected item's response headers.");
+
+ yield teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_copy_image_as_data_uri.js b/devtools/client/netmonitor/test/browser_net_copy_image_as_data_uri.js
new file mode 100644
index 000000000..144ced80d
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_copy_image_as_data_uri.js
@@ -0,0 +1,35 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if copying an image as data uri works.
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(CONTENT_TYPE_WITHOUT_CACHE_URL);
+ info("Starting test... ");
+
+ let { NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, CONTENT_TYPE_WITHOUT_CACHE_REQUESTS);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests();
+ });
+ yield wait;
+
+ let requestItem = RequestsMenu.getItemAtIndex(5);
+ RequestsMenu.selectedItem = requestItem;
+
+ yield waitForClipboardPromise(function setup() {
+ RequestsMenu.contextMenu.copyImageAsDataUri();
+ }, TEST_IMAGE_DATA_URI);
+
+ ok(true, "Clipboard contains the currently selected image as data uri.");
+
+ yield teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_copy_params.js b/devtools/client/netmonitor/test/browser_net_copy_params.js
new file mode 100644
index 000000000..1cb6f6620
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_copy_params.js
@@ -0,0 +1,98 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests whether copying a request item's parameters works.
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(PARAMS_URL);
+ info("Starting test... ");
+
+ let { NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, 1, 6);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests();
+ });
+ yield wait;
+
+ RequestsMenu.selectedItem = RequestsMenu.getItemAtIndex(0);
+ yield testCopyUrlParamsHidden(false);
+ yield testCopyUrlParams("a");
+ yield testCopyPostDataHidden(false);
+ yield testCopyPostData("{ \"foo\": \"bar\" }");
+
+ RequestsMenu.selectedItem = RequestsMenu.getItemAtIndex(1);
+ yield testCopyUrlParamsHidden(false);
+ yield testCopyUrlParams("a=b");
+ yield testCopyPostDataHidden(false);
+ yield testCopyPostData("{ \"foo\": \"bar\" }");
+
+ RequestsMenu.selectedItem = RequestsMenu.getItemAtIndex(2);
+ yield testCopyUrlParamsHidden(false);
+ yield testCopyUrlParams("a=b");
+ yield testCopyPostDataHidden(false);
+ yield testCopyPostData("foo=bar");
+
+ RequestsMenu.selectedItem = RequestsMenu.getItemAtIndex(3);
+ yield testCopyUrlParamsHidden(false);
+ yield testCopyUrlParams("a");
+ yield testCopyPostDataHidden(false);
+ yield testCopyPostData("{ \"foo\": \"bar\" }");
+
+ RequestsMenu.selectedItem = RequestsMenu.getItemAtIndex(4);
+ yield testCopyUrlParamsHidden(false);
+ yield testCopyUrlParams("a=b");
+ yield testCopyPostDataHidden(false);
+ yield testCopyPostData("{ \"foo\": \"bar\" }");
+
+ RequestsMenu.selectedItem = RequestsMenu.getItemAtIndex(5);
+ yield testCopyUrlParamsHidden(false);
+ yield testCopyUrlParams("a=b");
+ yield testCopyPostDataHidden(false);
+ yield testCopyPostData("?foo=bar");
+
+ RequestsMenu.selectedItem = RequestsMenu.getItemAtIndex(6);
+ yield testCopyUrlParamsHidden(true);
+ yield testCopyPostDataHidden(true);
+
+ return teardown(monitor);
+
+ function testCopyUrlParamsHidden(hidden) {
+ let allMenuItems = openContextMenuAndGetAllItems(NetMonitorView);
+ let copyUrlParamsNode = allMenuItems.find(item =>
+ item.id === "request-menu-context-copy-url-params");
+ is(copyUrlParamsNode.visible, !hidden,
+ "The \"Copy URL Parameters\" context menu item should" + (hidden ? " " : " not ") +
+ "be hidden.");
+ }
+
+ function* testCopyUrlParams(queryString) {
+ yield waitForClipboardPromise(function setup() {
+ RequestsMenu.contextMenu.copyUrlParams();
+ }, queryString);
+ ok(true, "The url query string copied from the selected item is correct.");
+ }
+
+ function testCopyPostDataHidden(hidden) {
+ let allMenuItems = openContextMenuAndGetAllItems(NetMonitorView);
+ let copyPostDataNode = allMenuItems.find(item =>
+ item.id === "request-menu-context-copy-post-data");
+ is(copyPostDataNode.visible, !hidden,
+ "The \"Copy POST Data\" context menu item should" + (hidden ? " " : " not ") +
+ "be hidden.");
+ }
+
+ function* testCopyPostData(postData) {
+ yield waitForClipboardPromise(function setup() {
+ RequestsMenu.contextMenu.copyPostData();
+ }, postData);
+ ok(true, "The post data string copied from the selected item is correct.");
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_copy_response.js b/devtools/client/netmonitor/test/browser_net_copy_response.js
new file mode 100644
index 000000000..411fe5cf0
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_copy_response.js
@@ -0,0 +1,35 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if copying a request's response works.
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(CONTENT_TYPE_WITHOUT_CACHE_URL);
+ info("Starting test... ");
+
+ const EXPECTED_RESULT = '{ "greeting": "Hello JSON!" }';
+
+ let { NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, CONTENT_TYPE_WITHOUT_CACHE_REQUESTS);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests();
+ });
+ yield wait;
+
+ let requestItem = RequestsMenu.getItemAtIndex(3);
+ RequestsMenu.selectedItem = requestItem;
+
+ yield waitForClipboardPromise(function setup() {
+ RequestsMenu.contextMenu.copyResponse();
+ }, EXPECTED_RESULT);
+
+ yield teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_copy_svg_image_as_data_uri.js b/devtools/client/netmonitor/test/browser_net_copy_svg_image_as_data_uri.js
new file mode 100644
index 000000000..252ce92bd
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_copy_svg_image_as_data_uri.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if copying an image as data uri works.
+ */
+
+const SVG_URL = EXAMPLE_URL + "dropmarker.svg";
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(CURL_URL);
+ info("Starting test... ");
+
+ let { NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, 1);
+ yield ContentTask.spawn(tab.linkedBrowser, SVG_URL, function* (url) {
+ content.wrappedJSObject.performRequest(url);
+ });
+ yield wait;
+
+ let requestItem = RequestsMenu.getItemAtIndex(0);
+ RequestsMenu.selectedItem = requestItem;
+
+ yield waitForClipboardPromise(function setup() {
+ RequestsMenu.contextMenu.copyImageAsDataUri();
+ }, function check(text) {
+ return text.startsWith("data:") && !/undefined/.test(text);
+ });
+
+ yield teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_copy_url.js b/devtools/client/netmonitor/test/browser_net_copy_url.js
new file mode 100644
index 000000000..660f5fe79
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_copy_url.js
@@ -0,0 +1,31 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if copying a request's url works.
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(CUSTOM_GET_URL);
+ info("Starting test... ");
+
+ let { NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ let wait = waitForNetworkEvents(monitor, 1);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests(1);
+ });
+ yield wait;
+
+ let requestItem = RequestsMenu.getItemAtIndex(0);
+ RequestsMenu.selectedItem = requestItem;
+
+ yield waitForClipboardPromise(function setup() {
+ RequestsMenu.contextMenu.copyUrl();
+ }, requestItem.attachment.url);
+
+ yield teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_cors_requests.js b/devtools/client/netmonitor/test/browser_net_cors_requests.js
new file mode 100644
index 000000000..d61b8e2f0
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_cors_requests.js
@@ -0,0 +1,33 @@
+/* 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 CORS preflight requests are displayed by network monitor
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(CORS_URL);
+ let { RequestsMenu } = monitor.panelWin.NetMonitorView;
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, 1, 1);
+
+ info("Performing a CORS request");
+ let requestUrl = "http://test1.example.com" + CORS_SJS_PATH;
+ yield ContentTask.spawn(tab.linkedBrowser, requestUrl, function* (url) {
+ content.wrappedJSObject.performRequests(url, "triggering/preflight", "post-data");
+ });
+
+ info("Waiting until the requests appear in netmonitor");
+ yield wait;
+
+ info("Checking the preflight and flight methods");
+ ["OPTIONS", "POST"].forEach((method, i) => {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(i), method, requestUrl);
+ });
+
+ yield teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_curl-utils.js b/devtools/client/netmonitor/test/browser_net_curl-utils.js
new file mode 100644
index 000000000..7a5fc7926
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_curl-utils.js
@@ -0,0 +1,228 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests Curl Utils functionality.
+ */
+
+const { CurlUtils } = require("devtools/client/shared/curl");
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(CURL_UTILS_URL);
+ info("Starting test... ");
+
+ let { NetMonitorView, gNetwork } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, 1, 3);
+ yield ContentTask.spawn(tab.linkedBrowser, SIMPLE_SJS, function* (url) {
+ content.wrappedJSObject.performRequests(url);
+ });
+ yield wait;
+
+ let requests = {
+ get: RequestsMenu.getItemAtIndex(0),
+ post: RequestsMenu.getItemAtIndex(1),
+ multipart: RequestsMenu.getItemAtIndex(2),
+ multipartForm: RequestsMenu.getItemAtIndex(3)
+ };
+
+ let data = yield createCurlData(requests.get.attachment, gNetwork);
+ testFindHeader(data);
+
+ data = yield createCurlData(requests.post.attachment, gNetwork);
+ testIsUrlEncodedRequest(data);
+ testWritePostDataTextParams(data);
+
+ data = yield createCurlData(requests.multipart.attachment, gNetwork);
+ testIsMultipartRequest(data);
+ testGetMultipartBoundary(data);
+ testRemoveBinaryDataFromMultipartText(data);
+
+ data = yield createCurlData(requests.multipartForm.attachment, gNetwork);
+ testGetHeadersFromMultipartText(data);
+
+ if (Services.appinfo.OS != "WINNT") {
+ testEscapeStringPosix();
+ } else {
+ testEscapeStringWin();
+ }
+
+ yield teardown(monitor);
+});
+
+function testIsUrlEncodedRequest(data) {
+ let isUrlEncoded = CurlUtils.isUrlEncodedRequest(data);
+ ok(isUrlEncoded, "Should return true for url encoded requests.");
+}
+
+function testIsMultipartRequest(data) {
+ let isMultipart = CurlUtils.isMultipartRequest(data);
+ ok(isMultipart, "Should return true for multipart/form-data requests.");
+}
+
+function testFindHeader(data) {
+ let headers = data.headers;
+ let hostName = CurlUtils.findHeader(headers, "Host");
+ let requestedWithLowerCased = CurlUtils.findHeader(headers, "x-requested-with");
+ let doesNotExist = CurlUtils.findHeader(headers, "X-Does-Not-Exist");
+
+ is(hostName, "example.com",
+ "Header with name 'Host' should be found in the request array.");
+ is(requestedWithLowerCased, "XMLHttpRequest",
+ "The search should be case insensitive.");
+ is(doesNotExist, null,
+ "Should return null when a header is not found.");
+}
+
+function testWritePostDataTextParams(data) {
+ let params = CurlUtils.writePostDataTextParams(data.postDataText);
+ is(params, "param1=value1&param2=value2&param3=value3",
+ "Should return a serialized representation of the request parameters");
+}
+
+function testGetMultipartBoundary(data) {
+ let boundary = CurlUtils.getMultipartBoundary(data);
+ ok(/-{3,}\w+/.test(boundary),
+ "A boundary string should be found in a multipart request.");
+}
+
+function testRemoveBinaryDataFromMultipartText(data) {
+ let generatedBoundary = CurlUtils.getMultipartBoundary(data);
+ let text = data.postDataText;
+ let binaryRemoved =
+ CurlUtils.removeBinaryDataFromMultipartText(text, generatedBoundary);
+ let boundary = "--" + generatedBoundary;
+
+ const EXPECTED_POSIX_RESULT = [
+ "$'",
+ boundary,
+ "\\r\\n\\r\\n",
+ "Content-Disposition: form-data; name=\"param1\"",
+ "\\r\\n\\r\\n",
+ "value1",
+ "\\r\\n",
+ boundary,
+ "\\r\\n\\r\\n",
+ "Content-Disposition: form-data; name=\"file\"; filename=\"filename.png\"",
+ "\\r\\n",
+ "Content-Type: image/png",
+ "\\r\\n\\r\\n",
+ boundary + "--",
+ "\\r\\n",
+ "'"
+ ].join("");
+
+ const EXPECTED_WIN_RESULT = [
+ '"' + boundary + '"^',
+ "\u000d\u000A\u000d\u000A",
+ '"Content-Disposition: form-data; name=""param1"""^',
+ "\u000d\u000A\u000d\u000A",
+ '"value1"^',
+ "\u000d\u000A",
+ '"' + boundary + '"^',
+ "\u000d\u000A\u000d\u000A",
+ '"Content-Disposition: form-data; name=""file""; filename=""filename.png"""^',
+ "\u000d\u000A",
+ '"Content-Type: image/png"^',
+ "\u000d\u000A\u000d\u000A",
+ '"' + boundary + '--"^',
+ "\u000d\u000A",
+ '""'
+ ].join("");
+
+ if (Services.appinfo.OS != "WINNT") {
+ is(CurlUtils.escapeStringPosix(binaryRemoved), EXPECTED_POSIX_RESULT,
+ "The mulitpart request payload should not contain binary data.");
+ } else {
+ is(CurlUtils.escapeStringWin(binaryRemoved), EXPECTED_WIN_RESULT,
+ "WinNT: The mulitpart request payload should not contain binary data.");
+ }
+}
+
+function testGetHeadersFromMultipartText(data) {
+ let headers = CurlUtils.getHeadersFromMultipartText(data.postDataText);
+
+ ok(Array.isArray(headers), "Should return an array.");
+ ok(headers.length > 0, "There should exist at least one request header.");
+ is(headers[0].name, "Content-Type", "The first header name should be 'Content-Type'.");
+}
+
+function testEscapeStringPosix() {
+ let surroundedWithQuotes = "A simple string";
+ is(CurlUtils.escapeStringPosix(surroundedWithQuotes), "'A simple string'",
+ "The string should be surrounded with single quotes.");
+
+ let singleQuotes = "It's unusual to put crickets in your coffee.";
+ is(CurlUtils.escapeStringPosix(singleQuotes),
+ "$'It\\'s unusual to put crickets in your coffee.'",
+ "Single quotes should be escaped.");
+
+ let newLines = "Line 1\r\nLine 2\u000d\u000ALine3";
+ is(CurlUtils.escapeStringPosix(newLines), "$'Line 1\\r\\nLine 2\\r\\nLine3'",
+ "Newlines should be escaped.");
+
+ let controlChars = "\u0007 \u0009 \u000C \u001B";
+ is(CurlUtils.escapeStringPosix(controlChars), "$'\\x07 \\x09 \\x0c \\x1b'",
+ "Control characters should be escaped.");
+
+ let extendedAsciiChars = "æ ø ü ß ö é";
+ is(CurlUtils.escapeStringPosix(extendedAsciiChars),
+ "$'\\xc3\\xa6 \\xc3\\xb8 \\xc3\\xbc \\xc3\\x9f \\xc3\\xb6 \\xc3\\xa9'",
+ "Character codes outside of the decimal range 32 - 126 should be escaped.");
+}
+
+function testEscapeStringWin() {
+ let surroundedWithDoubleQuotes = "A simple string";
+ is(CurlUtils.escapeStringWin(surroundedWithDoubleQuotes), '"A simple string"',
+ "The string should be surrounded with double quotes.");
+
+ let doubleQuotes = "Quote: \"Time is an illusion. Lunchtime doubly so.\"";
+ is(CurlUtils.escapeStringWin(doubleQuotes),
+ '"Quote: ""Time is an illusion. Lunchtime doubly so."""',
+ "Double quotes should be escaped.");
+
+ let percentSigns = "%AppData%";
+ is(CurlUtils.escapeStringWin(percentSigns), '""%"AppData"%""',
+ "Percent signs should be escaped.");
+
+ let backslashes = "\\A simple string\\";
+ is(CurlUtils.escapeStringWin(backslashes), '"\\\\A simple string\\\\"',
+ "Backslashes should be escaped.");
+
+ let newLines = "line1\r\nline2\r\nline3";
+ is(CurlUtils.escapeStringWin(newLines),
+ '"line1"^\u000d\u000A"line2"^\u000d\u000A"line3"',
+ "Newlines should be escaped.");
+}
+
+function* createCurlData(selected, network, controller) {
+ let { url, method, httpVersion } = selected;
+
+ // Create a sanitized object for the Curl command generator.
+ let data = {
+ url,
+ method,
+ headers: [],
+ httpVersion,
+ postDataText: null
+ };
+
+ // Fetch header values.
+ for (let { name, value } of selected.requestHeaders.headers) {
+ let text = yield network.getString(value);
+ data.headers.push({ name: name, value: text });
+ }
+
+ // Fetch the request payload.
+ if (selected.requestPostData) {
+ let postData = selected.requestPostData.postData.text;
+ data.postDataText = yield network.getString(postData);
+ }
+
+ return data;
+}
diff --git a/devtools/client/netmonitor/test/browser_net_cyrillic-01.js b/devtools/client/netmonitor/test/browser_net_cyrillic-01.js
new file mode 100644
index 000000000..43d6f522e
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_cyrillic-01.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if cyrillic text is rendered correctly in the source editor.
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(CYRILLIC_URL);
+ info("Starting test... ");
+
+ let { document, EVENTS, Editor, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, 1);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests();
+ });
+ yield wait;
+
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(0),
+ "GET", CONTENT_TYPE_SJS + "?fmt=txt", {
+ status: 200,
+ statusText: "DA DA DA"
+ });
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.getElementById("details-pane-toggle"));
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.querySelectorAll("#details-pane tab")[3]);
+
+ yield monitor.panelWin.once(EVENTS.RESPONSE_BODY_DISPLAYED);
+ let editor = yield NetMonitorView.editor("#response-content-textarea");
+ // u044F = Ñ
+ is(editor.getText().indexOf("\u044F"), 26,
+ "The text shown in the source editor is correct.");
+ is(editor.getMode(), Editor.modes.text,
+ "The mode active in the source editor is correct.");
+
+ yield teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_cyrillic-02.js b/devtools/client/netmonitor/test/browser_net_cyrillic-02.js
new file mode 100644
index 000000000..cd6b2000e
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_cyrillic-02.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if cyrillic text is rendered correctly in the source editor
+ * when loaded directly from an HTML page.
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(CYRILLIC_URL);
+ info("Starting test... ");
+
+ let { document, EVENTS, Editor, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, 1);
+ tab.linkedBrowser.reload();
+ yield wait;
+
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(0),
+ "GET", CYRILLIC_URL, {
+ status: 200,
+ statusText: "OK"
+ });
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.getElementById("details-pane-toggle"));
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.querySelectorAll("#details-pane tab")[3]);
+
+ yield monitor.panelWin.once(EVENTS.RESPONSE_BODY_DISPLAYED);
+ let editor = yield NetMonitorView.editor("#response-content-textarea");
+ // u044F = Ñ
+ is(editor.getText().indexOf("\u044F"), 486,
+ "The text shown in the source editor is correct.");
+ is(editor.getMode(), Editor.modes.html,
+ "The mode active in the source editor is correct.");
+
+ return teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_details-no-duplicated-content.js b/devtools/client/netmonitor/test/browser_net_details-no-duplicated-content.js
new file mode 100644
index 000000000..c3df51ced
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_details-no-duplicated-content.js
@@ -0,0 +1,172 @@
+/* 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";
+
+// A test to ensure that the content in details pane is not duplicated.
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(CUSTOM_GET_URL);
+ let panel = monitor.panelWin;
+ let { NetMonitorView, EVENTS } = panel;
+ let { RequestsMenu, NetworkDetails } = NetMonitorView;
+
+ const COOKIE_UNIQUE_PATH = "/do-not-use-in-other-tests-using-cookies";
+
+ let TEST_CASES = [
+ {
+ desc: "Test headers tab",
+ pageURI: CUSTOM_GET_URL,
+ requestURI: null,
+ isPost: false,
+ tabIndex: 0,
+ variablesView: NetworkDetails._headers,
+ expectedScopeLength: 2,
+ },
+ {
+ desc: "Test cookies tab",
+ pageURI: CUSTOM_GET_URL,
+ requestURI: COOKIE_UNIQUE_PATH,
+ isPost: false,
+ tabIndex: 1,
+ variablesView: NetworkDetails._cookies,
+ expectedScopeLength: 1,
+ },
+ {
+ desc: "Test params tab",
+ pageURI: POST_RAW_URL,
+ requestURI: null,
+ isPost: true,
+ tabIndex: 2,
+ variablesView: NetworkDetails._params,
+ expectedScopeLength: 1,
+ },
+ ];
+
+ info("Adding a cookie for the \"Cookie\" tab test");
+ yield setDocCookie("a=b; path=" + COOKIE_UNIQUE_PATH);
+
+ info("Running tests");
+ for (let spec of TEST_CASES) {
+ yield runTestCase(spec);
+ }
+
+ // Remove the cookie. If an error occurs the path of the cookie ensures it
+ // doesn't mess with the other tests.
+ info("Removing the added cookie.");
+ yield setDocCookie(
+ "a=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=" + COOKIE_UNIQUE_PATH);
+
+ yield teardown(monitor);
+
+ /**
+ * Set a content document cookie
+ */
+ function setDocCookie(cookie) {
+ return ContentTask.spawn(tab.linkedBrowser, cookie, function* (cookieArg) {
+ content.document.cookie = cookieArg;
+ });
+ }
+
+ /**
+ * A helper that handles the execution of each case.
+ */
+ function* runTestCase(spec) {
+ info("Running case: " + spec.desc);
+ let wait = waitForNetworkEvents(monitor, 1);
+ tab.linkedBrowser.loadURI(spec.pageURI);
+ yield wait;
+
+ RequestsMenu.clear();
+ yield waitForFinalDetailTabUpdate(spec.tabIndex, spec.isPost, spec.requestURI);
+
+ is(spec.variablesView._store.length, spec.expectedScopeLength,
+ "View contains " + spec.expectedScopeLength + " scope headers");
+ }
+
+ /**
+ * A helper that prepares the variables view for the actual testing. It
+ * - selects the correct tab
+ * - performs the specified request to specified URI
+ * - opens the details view
+ * - waits for the final update to happen
+ */
+ function* waitForFinalDetailTabUpdate(tabIndex, isPost, uri) {
+ let onNetworkEvent = panel.once(EVENTS.NETWORK_EVENT);
+ let onDetailsPopulated = panel.once(EVENTS.NETWORKDETAILSVIEW_POPULATED);
+ let onRequestFinished = isPost ?
+ waitForNetworkEvents(monitor, 0, 1) :
+ waitForNetworkEvents(monitor, 1);
+
+ info("Performing a request");
+ yield ContentTask.spawn(tab.linkedBrowser, uri, function* (url) {
+ content.wrappedJSObject.performRequests(1, url);
+ });
+
+ info("Waiting for NETWORK_EVENT");
+ yield onNetworkEvent;
+
+ if (!RequestsMenu.getItemAtIndex(0)) {
+ info("Waiting for the request to be added to the view");
+ yield monitor.panelWin.once(EVENTS.REQUEST_ADDED);
+ }
+
+ ok(true, "Received NETWORK_EVENT. Selecting the item.");
+ let item = RequestsMenu.getItemAtIndex(0);
+ RequestsMenu.selectedItem = item;
+
+ info("Item selected. Waiting for NETWORKDETAILSVIEW_POPULATED");
+ yield onDetailsPopulated;
+
+ info("Received populated event. Selecting tab at index " + tabIndex);
+ NetworkDetails.widget.selectedIndex = tabIndex;
+
+ info("Waiting for request to finish.");
+ yield onRequestFinished;
+
+ ok(true, "Request finished.");
+
+ /**
+ * Because this test uses lazy updates there's four scenarios to consider:
+ * #1: Everything is updated and test is ready to continue.
+ * #2: There's updates that are waiting to be flushed.
+ * #3: Updates are flushed but the tab update is still running.
+ * #4: There's pending updates and a tab update is still running.
+ *
+ * For case #1 there's not going to be a TAB_UPDATED event so don't wait for
+ * it (bug 1106181).
+ *
+ * For cases #2 and #3 it's enough to wait for one TAB_UPDATED event as for
+ * - case #2 the next flush will perform the final update and single
+ * TAB_UPDATED event is emitted.
+ * - case #3 the running update is the final update that'll emit one
+ * TAB_UPDATED event.
+ *
+ * For case #4 we must wait for the updates to be flushed before we can
+ * start waiting for TAB_UPDATED event or we'll continue the test right
+ * after the pending update finishes.
+ */
+ let hasQueuedUpdates = RequestsMenu._updateQueue.length !== 0;
+ let hasRunningTabUpdate = NetworkDetails._viewState.updating[tabIndex];
+
+ if (hasQueuedUpdates || hasRunningTabUpdate) {
+ info("There's pending updates - waiting for them to finish.");
+ info(" hasQueuedUpdates: " + hasQueuedUpdates);
+ info(" hasRunningTabUpdate: " + hasRunningTabUpdate);
+
+ if (hasQueuedUpdates && hasRunningTabUpdate) {
+ info("Waiting for updates to be flushed.");
+ // _flushRequests calls .populate which emits the following event
+ yield panel.once(EVENTS.NETWORKDETAILSVIEW_POPULATED);
+
+ info("Requests flushed.");
+ }
+
+ info("Waiting for final tab update.");
+ yield waitFor(panel, EVENTS.TAB_UPDATED);
+ }
+
+ info("All updates completed.");
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_filter-01.js b/devtools/client/netmonitor/test/browser_net_filter-01.js
new file mode 100644
index 000000000..b0d76c629
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_filter-01.js
@@ -0,0 +1,264 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test if filtering items in the network table works correctly.
+ */
+const BASIC_REQUESTS = [
+ { url: "sjs_content-type-test-server.sjs?fmt=html&res=undefined&text=Sample" },
+ { url: "sjs_content-type-test-server.sjs?fmt=css&text=sample" },
+ { url: "sjs_content-type-test-server.sjs?fmt=js&text=sample" },
+];
+
+const REQUESTS_WITH_MEDIA = BASIC_REQUESTS.concat([
+ { url: "sjs_content-type-test-server.sjs?fmt=font" },
+ { url: "sjs_content-type-test-server.sjs?fmt=image" },
+ { url: "sjs_content-type-test-server.sjs?fmt=audio" },
+ { url: "sjs_content-type-test-server.sjs?fmt=video" },
+]);
+
+const REQUESTS_WITH_MEDIA_AND_FLASH = REQUESTS_WITH_MEDIA.concat([
+ { url: "sjs_content-type-test-server.sjs?fmt=flash" },
+]);
+
+const REQUESTS_WITH_MEDIA_AND_FLASH_AND_WS = REQUESTS_WITH_MEDIA_AND_FLASH.concat([
+ /* "Upgrade" is a reserved header and can not be set on XMLHttpRequest */
+ { url: "sjs_content-type-test-server.sjs?fmt=ws" },
+]);
+
+add_task(function* () {
+ let Actions = require("devtools/client/netmonitor/actions/index");
+ let { monitor } = yield initNetMonitor(FILTERING_URL);
+ let { gStore } = monitor.panelWin;
+
+ function setFreetextFilter(value) {
+ gStore.dispatch(Actions.setFilterText(value));
+ }
+
+ info("Starting test... ");
+
+ let { $, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, 9);
+ loadCommonFrameScript();
+ yield performRequestsInContent(REQUESTS_WITH_MEDIA_AND_FLASH_AND_WS);
+ yield wait;
+
+ EventUtils.sendMouseEvent({ type: "mousedown" }, $("#details-pane-toggle"));
+
+ isnot(RequestsMenu.selectedItem, null,
+ "There should be a selected item in the requests menu.");
+ is(RequestsMenu.selectedIndex, 0,
+ "The first item should be selected in the requests menu.");
+ is(NetMonitorView.detailsPaneHidden, false,
+ "The details pane should not be hidden after toggle button was pressed.");
+
+ // First test with single filters...
+ testFilterButtons(monitor, "all");
+ testContents([1, 1, 1, 1, 1, 1, 1, 1, 1]);
+
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-html-button"));
+ testFilterButtons(monitor, "html");
+ testContents([1, 0, 0, 0, 0, 0, 0, 0, 0]);
+
+ // Reset filters
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-all-button"));
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-css-button"));
+ testFilterButtons(monitor, "css");
+ testContents([0, 1, 0, 0, 0, 0, 0, 0, 0]);
+
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-all-button"));
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-js-button"));
+ testFilterButtons(monitor, "js");
+ testContents([0, 0, 1, 0, 0, 0, 0, 0, 0]);
+
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-all-button"));
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-xhr-button"));
+ testFilterButtons(monitor, "xhr");
+ testContents([1, 1, 1, 1, 1, 1, 1, 1, 0]);
+
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-all-button"));
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-fonts-button"));
+ testFilterButtons(monitor, "fonts");
+ testContents([0, 0, 0, 1, 0, 0, 0, 0, 0]);
+
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-all-button"));
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-images-button"));
+ testFilterButtons(monitor, "images");
+ testContents([0, 0, 0, 0, 1, 0, 0, 0, 0]);
+
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-all-button"));
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-media-button"));
+ testFilterButtons(monitor, "media");
+ testContents([0, 0, 0, 0, 0, 1, 1, 0, 0]);
+
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-all-button"));
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-flash-button"));
+ testFilterButtons(monitor, "flash");
+ testContents([0, 0, 0, 0, 0, 0, 0, 1, 0]);
+
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-all-button"));
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-ws-button"));
+ testFilterButtons(monitor, "ws");
+ testContents([0, 0, 0, 0, 0, 0, 0, 0, 1]);
+
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-all-button"));
+ testFilterButtons(monitor, "all");
+ testContents([1, 1, 1, 1, 1, 1, 1, 1, 1]);
+
+ // Text in filter box that matches nothing should hide all.
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-all-button"));
+ setFreetextFilter("foobar");
+ testContents([0, 0, 0, 0, 0, 0, 0, 0, 0]);
+
+ // Text in filter box that matches should filter out everything else.
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-all-button"));
+ setFreetextFilter("sample");
+ testContents([1, 1, 1, 0, 0, 0, 0, 0, 0]);
+
+ // Text in filter box that matches should filter out everything else.
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-all-button"));
+ setFreetextFilter("SAMPLE");
+ testContents([1, 1, 1, 0, 0, 0, 0, 0, 0]);
+
+ // Test negative filtering (only show unmatched items)
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-all-button"));
+ setFreetextFilter("-sample");
+ testContents([0, 0, 0, 1, 1, 1, 1, 1, 1]);
+
+ // ...then combine multiple filters together.
+
+ // Enable filtering for html and css; should show request of both type.
+ setFreetextFilter("");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-html-button"));
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-css-button"));
+ testFilterButtonsCustom(monitor, [0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0]);
+ testContents([1, 1, 0, 0, 0, 0, 0, 0, 0]);
+
+ // Html and css filter enabled and text filter should show just the html and css match.
+ // Should not show both the items matching the button plus the items matching the text.
+ setFreetextFilter("sample");
+ testContents([1, 1, 0, 0, 0, 0, 0, 0, 0]);
+
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-flash-button"));
+ setFreetextFilter("");
+ testFilterButtonsCustom(monitor, [0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0]);
+ testContents([1, 1, 0, 0, 0, 0, 0, 1, 0]);
+
+ // Disable some filters. Only one left active.
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-css-button"));
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-flash-button"));
+ testFilterButtons(monitor, "html");
+ testContents([1, 0, 0, 0, 0, 0, 0, 0, 0]);
+
+ // Disable last active filter. Should toggle to all.
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-html-button"));
+ testFilterButtons(monitor, "all");
+ testContents([1, 1, 1, 1, 1, 1, 1, 1, 1]);
+
+ // Enable few filters and click on all. Only "all" should be checked.
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-html-button"));
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-css-button"));
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-ws-button"));
+ testFilterButtonsCustom(monitor, [0, 1, 1, 0, 0, 0, 0, 0, 0, 1]);
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-all-button"));
+ testFilterButtons(monitor, "all");
+ testContents([1, 1, 1, 1, 1, 1, 1, 1, 1]);
+
+ yield teardown(monitor);
+
+ function testContents(visibility) {
+ isnot(RequestsMenu.selectedItem, null,
+ "There should still be a selected item after filtering.");
+ is(RequestsMenu.selectedIndex, 0,
+ "The first item should be still selected after filtering.");
+ is(NetMonitorView.detailsPaneHidden, false,
+ "The details pane should still be visible after filtering.");
+
+ is(RequestsMenu.items.length, visibility.length,
+ "There should be a specific amount of items in the requests menu.");
+ is(RequestsMenu.visibleItems.length, visibility.filter(e => e).length,
+ "There should be a specific amount of visbile items in the requests menu.");
+
+ for (let i = 0; i < visibility.length; i++) {
+ is(RequestsMenu.getItemAtIndex(i).target.hidden, !visibility[i],
+ "The item at index " + i + " doesn't have the correct hidden state.");
+ }
+
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(0),
+ "GET", CONTENT_TYPE_SJS + "?fmt=html", {
+ fuzzyUrl: true,
+ status: 200,
+ statusText: "OK",
+ type: "html",
+ fullMimeType: "text/html; charset=utf-8"
+ });
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(1),
+ "GET", CONTENT_TYPE_SJS + "?fmt=css", {
+ fuzzyUrl: true,
+ status: 200,
+ statusText: "OK",
+ type: "css",
+ fullMimeType: "text/css; charset=utf-8"
+ });
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(2),
+ "GET", CONTENT_TYPE_SJS + "?fmt=js", {
+ fuzzyUrl: true,
+ status: 200,
+ statusText: "OK",
+ type: "js",
+ fullMimeType: "application/javascript; charset=utf-8"
+ });
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(3),
+ "GET", CONTENT_TYPE_SJS + "?fmt=font", {
+ fuzzyUrl: true,
+ status: 200,
+ statusText: "OK",
+ type: "woff",
+ fullMimeType: "font/woff"
+ });
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(4),
+ "GET", CONTENT_TYPE_SJS + "?fmt=image", {
+ fuzzyUrl: true,
+ status: 200,
+ statusText: "OK",
+ type: "png",
+ fullMimeType: "image/png"
+ });
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(5),
+ "GET", CONTENT_TYPE_SJS + "?fmt=audio", {
+ fuzzyUrl: true,
+ status: 200,
+ statusText: "OK",
+ type: "ogg",
+ fullMimeType: "audio/ogg"
+ });
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(6),
+ "GET", CONTENT_TYPE_SJS + "?fmt=video", {
+ fuzzyUrl: true,
+ status: 200,
+ statusText: "OK",
+ type: "webm",
+ fullMimeType: "video/webm"
+ });
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(7),
+ "GET", CONTENT_TYPE_SJS + "?fmt=flash", {
+ fuzzyUrl: true,
+ status: 200,
+ statusText: "OK",
+ type: "x-shockwave-flash",
+ fullMimeType: "application/x-shockwave-flash"
+ });
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(8),
+ "GET", CONTENT_TYPE_SJS + "?fmt=ws", {
+ fuzzyUrl: true,
+ status: 101,
+ statusText: "Switching Protocols",
+ });
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_filter-02.js b/devtools/client/netmonitor/test/browser_net_filter-02.js
new file mode 100644
index 000000000..70a051b6d
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_filter-02.js
@@ -0,0 +1,200 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test if filtering items in the network table works correctly with new requests.
+ */
+
+const BASIC_REQUESTS = [
+ { url: "sjs_content-type-test-server.sjs?fmt=html&res=undefined" },
+ { url: "sjs_content-type-test-server.sjs?fmt=css" },
+ { url: "sjs_content-type-test-server.sjs?fmt=js" },
+];
+
+const REQUESTS_WITH_MEDIA = BASIC_REQUESTS.concat([
+ { url: "sjs_content-type-test-server.sjs?fmt=font" },
+ { url: "sjs_content-type-test-server.sjs?fmt=image" },
+ { url: "sjs_content-type-test-server.sjs?fmt=audio" },
+ { url: "sjs_content-type-test-server.sjs?fmt=video" },
+]);
+
+const REQUESTS_WITH_MEDIA_AND_FLASH = REQUESTS_WITH_MEDIA.concat([
+ { url: "sjs_content-type-test-server.sjs?fmt=flash" },
+]);
+
+const REQUESTS_WITH_MEDIA_AND_FLASH_AND_WS = REQUESTS_WITH_MEDIA_AND_FLASH.concat([
+ /* "Upgrade" is a reserved header and can not be set on XMLHttpRequest */
+ { url: "sjs_content-type-test-server.sjs?fmt=ws" },
+]);
+
+add_task(function* () {
+ let { monitor } = yield initNetMonitor(FILTERING_URL);
+ info("Starting test... ");
+
+ // It seems that this test may be slow on Ubuntu builds running on ec2.
+ requestLongerTimeout(2);
+
+ let { $, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, 9);
+ loadCommonFrameScript();
+ yield performRequestsInContent(REQUESTS_WITH_MEDIA_AND_FLASH_AND_WS);
+ yield wait;
+
+ EventUtils.sendMouseEvent({ type: "mousedown" }, $("#details-pane-toggle"));
+
+ isnot(RequestsMenu.selectedItem, null,
+ "There should be a selected item in the requests menu.");
+ is(RequestsMenu.selectedIndex, 0,
+ "The first item should be selected in the requests menu.");
+ is(NetMonitorView.detailsPaneHidden, false,
+ "The details pane should not be hidden after toggle button was pressed.");
+
+ testFilterButtons(monitor, "all");
+ testContents([1, 1, 1, 1, 1, 1, 1, 1, 1]);
+
+ info("Testing html filtering.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-html-button"));
+ testFilterButtons(monitor, "html");
+ testContents([1, 0, 0, 0, 0, 0, 0, 0, 0]);
+
+ info("Performing more requests.");
+ wait = waitForNetworkEvents(monitor, 9);
+ yield performRequestsInContent(REQUESTS_WITH_MEDIA_AND_FLASH_AND_WS);
+ yield wait;
+
+ info("Testing html filtering again.");
+ testFilterButtons(monitor, "html");
+ testContents([1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0]);
+
+ info("Performing more requests.");
+ wait = waitForNetworkEvents(monitor, 9);
+ yield performRequestsInContent(REQUESTS_WITH_MEDIA_AND_FLASH_AND_WS);
+ yield wait;
+
+ info("Testing html filtering again.");
+ testFilterButtons(monitor, "html");
+ testContents([1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0,
+ 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0]);
+
+ info("Resetting filters.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-all-button"));
+ testFilterButtons(monitor, "all");
+ testContents([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]);
+
+ yield teardown(monitor);
+
+ function testContents(visibility) {
+ isnot(RequestsMenu.selectedItem, null,
+ "There should still be a selected item after filtering.");
+ is(RequestsMenu.selectedIndex, 0,
+ "The first item should be still selected after filtering.");
+ is(NetMonitorView.detailsPaneHidden, false,
+ "The details pane should still be visible after filtering.");
+
+ is(RequestsMenu.items.length, visibility.length,
+ "There should be a specific amount of items in the requests menu.");
+ is(RequestsMenu.visibleItems.length, visibility.filter(e => e).length,
+ "There should be a specific amount of visbile items in the requests menu.");
+
+ for (let i = 0; i < visibility.length; i++) {
+ is(RequestsMenu.getItemAtIndex(i).target.hidden, !visibility[i],
+ "The item at index " + i + " doesn't have the correct hidden state.");
+ }
+
+ for (let i = 0; i < visibility.length; i += 9) {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(i),
+ "GET", CONTENT_TYPE_SJS + "?fmt=html", {
+ fuzzyUrl: true,
+ status: 200,
+ statusText: "OK",
+ type: "html",
+ fullMimeType: "text/html; charset=utf-8"
+ });
+ }
+ for (let i = 1; i < visibility.length; i += 9) {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(i),
+ "GET", CONTENT_TYPE_SJS + "?fmt=css", {
+ fuzzyUrl: true,
+ status: 200,
+ statusText: "OK",
+ type: "css",
+ fullMimeType: "text/css; charset=utf-8"
+ });
+ }
+ for (let i = 2; i < visibility.length; i += 9) {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(i),
+ "GET", CONTENT_TYPE_SJS + "?fmt=js", {
+ fuzzyUrl: true,
+ status: 200,
+ statusText: "OK",
+ type: "js",
+ fullMimeType: "application/javascript; charset=utf-8"
+ });
+ }
+ for (let i = 3; i < visibility.length; i += 9) {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(i),
+ "GET", CONTENT_TYPE_SJS + "?fmt=font", {
+ fuzzyUrl: true,
+ status: 200,
+ statusText: "OK",
+ type: "woff",
+ fullMimeType: "font/woff"
+ });
+ }
+ for (let i = 4; i < visibility.length; i += 9) {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(i),
+ "GET", CONTENT_TYPE_SJS + "?fmt=image", {
+ fuzzyUrl: true,
+ status: 200,
+ statusText: "OK",
+ type: "png",
+ fullMimeType: "image/png"
+ });
+ }
+ for (let i = 5; i < visibility.length; i += 9) {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(i),
+ "GET", CONTENT_TYPE_SJS + "?fmt=audio", {
+ fuzzyUrl: true,
+ status: 200,
+ statusText: "OK",
+ type: "ogg",
+ fullMimeType: "audio/ogg"
+ });
+ }
+ for (let i = 6; i < visibility.length; i += 9) {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(i),
+ "GET", CONTENT_TYPE_SJS + "?fmt=video", {
+ fuzzyUrl: true,
+ status: 200,
+ statusText: "OK",
+ type: "webm",
+ fullMimeType: "video/webm"
+ });
+ }
+ for (let i = 7; i < visibility.length; i += 9) {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(i),
+ "GET", CONTENT_TYPE_SJS + "?fmt=flash", {
+ fuzzyUrl: true,
+ status: 200,
+ statusText: "OK",
+ type: "x-shockwave-flash",
+ fullMimeType: "application/x-shockwave-flash"
+ });
+ }
+ for (let i = 8; i < visibility.length; i += 9) {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(i),
+ "GET", CONTENT_TYPE_SJS + "?fmt=ws", {
+ fuzzyUrl: true,
+ status: 101,
+ statusText: "Switching Protocols"
+ });
+ }
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_filter-03.js b/devtools/client/netmonitor/test/browser_net_filter-03.js
new file mode 100644
index 000000000..2babdaab3
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_filter-03.js
@@ -0,0 +1,185 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test if filtering items in the network table works correctly with new requests
+ * and while sorting is enabled.
+ */
+const BASIC_REQUESTS = [
+ { url: "sjs_content-type-test-server.sjs?fmt=html&res=undefined" },
+ { url: "sjs_content-type-test-server.sjs?fmt=css" },
+ { url: "sjs_content-type-test-server.sjs?fmt=js" },
+];
+
+const REQUESTS_WITH_MEDIA = BASIC_REQUESTS.concat([
+ { url: "sjs_content-type-test-server.sjs?fmt=font" },
+ { url: "sjs_content-type-test-server.sjs?fmt=image" },
+ { url: "sjs_content-type-test-server.sjs?fmt=audio" },
+ { url: "sjs_content-type-test-server.sjs?fmt=video" },
+]);
+
+add_task(function* () {
+ let { monitor } = yield initNetMonitor(FILTERING_URL);
+ info("Starting test... ");
+
+ // It seems that this test may be slow on Ubuntu builds running on ec2.
+ requestLongerTimeout(2);
+
+ let { $, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ // The test assumes that the first HTML request here has a longer response
+ // body than the other HTML requests performed later during the test.
+ let requests = Cu.cloneInto(REQUESTS_WITH_MEDIA, {});
+ let newres = "res=<p>" + new Array(10).join(Math.random(10)) + "</p>";
+ requests[0].url = requests[0].url.replace("res=undefined", newres);
+
+ loadCommonFrameScript();
+
+ let wait = waitForNetworkEvents(monitor, 7);
+ yield performRequestsInContent(requests);
+ yield wait;
+
+ EventUtils.sendMouseEvent({ type: "mousedown" }, $("#details-pane-toggle"));
+
+ isnot(RequestsMenu.selectedItem, null,
+ "There should be a selected item in the requests menu.");
+ is(RequestsMenu.selectedIndex, 0,
+ "The first item should be selected in the requests menu.");
+ is(NetMonitorView.detailsPaneHidden, false,
+ "The details pane should not be hidden after toggle button was pressed.");
+
+ testFilterButtons(monitor, "all");
+ testContents([0, 1, 2, 3, 4, 5, 6], 7, 0);
+
+ info("Sorting by size, ascending.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-size-button"));
+ testFilterButtons(monitor, "all");
+ testContents([6, 4, 5, 0, 1, 2, 3], 7, 6);
+
+ info("Testing html filtering.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-html-button"));
+ testFilterButtons(monitor, "html");
+ testContents([6, 4, 5, 0, 1, 2, 3], 1, 6);
+
+ info("Performing more requests.");
+ wait = waitForNetworkEvents(monitor, 7);
+ performRequestsInContent(REQUESTS_WITH_MEDIA);
+ yield wait;
+
+ info("Testing html filtering again.");
+ resetSorting();
+ testFilterButtons(monitor, "html");
+ testContents([8, 13, 9, 11, 10, 12, 0, 4, 1, 5, 2, 6, 3, 7], 2, 13);
+
+ info("Performing more requests.");
+ performRequestsInContent(REQUESTS_WITH_MEDIA);
+ yield waitForNetworkEvents(monitor, 7);
+
+ info("Testing html filtering again.");
+ resetSorting();
+ testFilterButtons(monitor, "html");
+ testContents([12, 13, 20, 14, 16, 18, 15, 17, 19, 0, 4, 8, 1, 5, 9, 2, 6, 10, 3, 7, 11],
+ 3, 20);
+
+ yield teardown(monitor);
+
+ function resetSorting() {
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-waterfall-button"));
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-size-button"));
+ }
+
+ function testContents(order, visible, selection) {
+ isnot(RequestsMenu.selectedItem, null,
+ "There should still be a selected item after filtering.");
+ is(RequestsMenu.selectedIndex, selection,
+ "The first item should be still selected after filtering.");
+ is(NetMonitorView.detailsPaneHidden, false,
+ "The details pane should still be visible after filtering.");
+
+ is(RequestsMenu.items.length, order.length,
+ "There should be a specific amount of items in the requests menu.");
+ is(RequestsMenu.visibleItems.length, visible,
+ "There should be a specific amount of visbile items in the requests menu.");
+
+ for (let i = 0; i < order.length; i++) {
+ is(RequestsMenu.getItemAtIndex(i), RequestsMenu.items[i],
+ "The requests menu items aren't ordered correctly. Misplaced item " + i + ".");
+ }
+
+ for (let i = 0, len = order.length / 7; i < len; i++) {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(order[i]),
+ "GET", CONTENT_TYPE_SJS + "?fmt=html", {
+ fuzzyUrl: true,
+ status: 200,
+ statusText: "OK",
+ type: "html",
+ fullMimeType: "text/html; charset=utf-8"
+ });
+ }
+ for (let i = 0, len = order.length / 7; i < len; i++) {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(order[i + len]),
+ "GET", CONTENT_TYPE_SJS + "?fmt=css", {
+ fuzzyUrl: true,
+ status: 200,
+ statusText: "OK",
+ type: "css",
+ fullMimeType: "text/css; charset=utf-8"
+ });
+ }
+ for (let i = 0, len = order.length / 7; i < len; i++) {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(order[i + len * 2]),
+ "GET", CONTENT_TYPE_SJS + "?fmt=js", {
+ fuzzyUrl: true,
+ status: 200,
+ statusText: "OK",
+ type: "js",
+ fullMimeType: "application/javascript; charset=utf-8"
+ });
+ }
+ for (let i = 0, len = order.length / 7; i < len; i++) {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(order[i + len * 3]),
+ "GET", CONTENT_TYPE_SJS + "?fmt=font", {
+ fuzzyUrl: true,
+ status: 200,
+ statusText: "OK",
+ type: "woff",
+ fullMimeType: "font/woff"
+ });
+ }
+ for (let i = 0, len = order.length / 7; i < len; i++) {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(order[i + len * 4]),
+ "GET", CONTENT_TYPE_SJS + "?fmt=image", {
+ fuzzyUrl: true,
+ status: 200,
+ statusText: "OK",
+ type: "png",
+ fullMimeType: "image/png"
+ });
+ }
+ for (let i = 0, len = order.length / 7; i < len; i++) {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(order[i + len * 5]),
+ "GET", CONTENT_TYPE_SJS + "?fmt=audio", {
+ fuzzyUrl: true,
+ status: 200,
+ statusText: "OK",
+ type: "ogg",
+ fullMimeType: "audio/ogg"
+ });
+ }
+ for (let i = 0, len = order.length / 7; i < len; i++) {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(order[i + len * 6]),
+ "GET", CONTENT_TYPE_SJS + "?fmt=video", {
+ fuzzyUrl: true,
+ status: 200,
+ statusText: "OK",
+ type: "webm",
+ fullMimeType: "video/webm"
+ });
+ }
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_filter-04.js b/devtools/client/netmonitor/test/browser_net_filter-04.js
new file mode 100644
index 000000000..e617dbaa9
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_filter-04.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if invalid filter types are sanitized when loaded from the preferences.
+ */
+
+const BASIC_REQUESTS = [
+ { url: "sjs_content-type-test-server.sjs?fmt=html&res=undefined" },
+ { url: "sjs_content-type-test-server.sjs?fmt=css" },
+ { url: "sjs_content-type-test-server.sjs?fmt=js" },
+];
+
+const REQUESTS_WITH_MEDIA = BASIC_REQUESTS.concat([
+ { url: "sjs_content-type-test-server.sjs?fmt=font" },
+ { url: "sjs_content-type-test-server.sjs?fmt=image" },
+ { url: "sjs_content-type-test-server.sjs?fmt=audio" },
+ { url: "sjs_content-type-test-server.sjs?fmt=video" },
+]);
+
+const REQUESTS_WITH_MEDIA_AND_FLASH = REQUESTS_WITH_MEDIA.concat([
+ { url: "sjs_content-type-test-server.sjs?fmt=flash" },
+]);
+
+const REQUESTS_WITH_MEDIA_AND_FLASH_AND_WS = REQUESTS_WITH_MEDIA_AND_FLASH.concat([
+ /* "Upgrade" is a reserved header and can not be set on XMLHttpRequest */
+ { url: "sjs_content-type-test-server.sjs?fmt=ws" },
+]);
+
+add_task(function* () {
+ Services.prefs.setCharPref("devtools.netmonitor.filters", '["js", "bogus"]');
+
+ let { monitor } = yield initNetMonitor(FILTERING_URL);
+ info("Starting test... ");
+
+ let { Prefs, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ is(Prefs.filters.length, 2,
+ "All filter types were loaded as an array from the preferences.");
+ is(Prefs.filters[0], "js",
+ "The first filter type is correct.");
+ is(Prefs.filters[1], "bogus",
+ "The second filter type is invalid, but loaded anyway.");
+
+ let wait = waitForNetworkEvents(monitor, 9);
+ loadCommonFrameScript();
+ yield performRequestsInContent(REQUESTS_WITH_MEDIA_AND_FLASH_AND_WS);
+ yield wait;
+
+ testFilterButtons(monitor, "js");
+ ok(true, "Only the correct filter type was taken into consideration.");
+
+ yield teardown(monitor);
+
+ let filters = Services.prefs.getCharPref("devtools.netmonitor.filters");
+ is(filters, '["js"]',
+ "The bogus filter type was ignored and removed from the preferences.");
+});
diff --git a/devtools/client/netmonitor/test/browser_net_footer-summary.js b/devtools/client/netmonitor/test/browser_net_footer-summary.js
new file mode 100644
index 000000000..e484b2097
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_footer-summary.js
@@ -0,0 +1,75 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test if the summary text displayed in the network requests menu footer
+ * is correct.
+ */
+
+add_task(function* () {
+ requestLongerTimeout(2);
+ let { L10N } = require("devtools/client/netmonitor/l10n");
+ let { PluralForm } = require("devtools/shared/plural-form");
+
+ let { tab, monitor } = yield initNetMonitor(FILTERING_URL);
+ info("Starting test... ");
+
+ let { $, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+ testStatus();
+
+ for (let i = 0; i < 2; i++) {
+ info(`Performing requests in batch #${i}`);
+ let wait = waitForNetworkEvents(monitor, 8);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests('{ "getMedia": true, "getFlash": true }');
+ });
+ yield wait;
+
+ testStatus();
+
+ let buttons = ["html", "css", "js", "xhr", "fonts", "images", "media", "flash"];
+ for (let button of buttons) {
+ let buttonEl = $(`#requests-menu-filter-${button}-button`);
+ EventUtils.sendMouseEvent({ type: "click" }, buttonEl);
+ testStatus();
+ }
+ }
+
+ yield teardown(monitor);
+
+ function testStatus() {
+ let summary = $("#requests-menu-network-summary-button");
+ let value = summary.getAttribute("label");
+ info("Current summary: " + value);
+
+ let visibleItems = RequestsMenu.visibleItems;
+ let visibleRequestsCount = visibleItems.length;
+ let totalRequestsCount = RequestsMenu.itemCount;
+ info("Current requests: " + visibleRequestsCount + " of " + totalRequestsCount + ".");
+
+ if (!totalRequestsCount || !visibleRequestsCount) {
+ is(value, L10N.getStr("networkMenu.empty"),
+ "The current summary text is incorrect, expected an 'empty' label.");
+ return;
+ }
+
+ let totalBytes = RequestsMenu._getTotalBytesOfRequests(visibleItems);
+ let totalMillis =
+ RequestsMenu._getNewestRequest(visibleItems).attachment.endedMillis -
+ RequestsMenu._getOldestRequest(visibleItems).attachment.startedMillis;
+
+ info("Computed total bytes: " + totalBytes);
+ info("Computed total millis: " + totalMillis);
+
+ is(value, PluralForm.get(visibleRequestsCount, L10N.getStr("networkMenu.summary"))
+ .replace("#1", visibleRequestsCount)
+ .replace("#2", L10N.numberWithDecimals((totalBytes || 0) / 1024, 2))
+ .replace("#3", L10N.numberWithDecimals((totalMillis || 0) / 1000, 2))
+ , "The current summary text is incorrect.");
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_frame.js b/devtools/client/netmonitor/test/browser_net_frame.js
new file mode 100644
index 000000000..eeded652b
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_frame.js
@@ -0,0 +1,221 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests for all expected requests when an iframe is loading a subdocument.
+ */
+
+const TOP_FILE_NAME = "html_frame-test-page.html";
+const SUB_FILE_NAME = "html_frame-subdocument.html";
+const TOP_URL = EXAMPLE_URL + TOP_FILE_NAME;
+const SUB_URL = EXAMPLE_URL + SUB_FILE_NAME;
+
+const EXPECTED_REQUESTS_TOP = [
+ {
+ method: "GET",
+ url: TOP_URL,
+ causeType: "document",
+ causeUri: "",
+ stack: true
+ },
+ {
+ method: "GET",
+ url: EXAMPLE_URL + "stylesheet_request",
+ causeType: "stylesheet",
+ causeUri: TOP_URL,
+ stack: false
+ },
+ {
+ method: "GET",
+ url: EXAMPLE_URL + "img_request",
+ causeType: "img",
+ causeUri: TOP_URL,
+ stack: false
+ },
+ {
+ method: "GET",
+ url: EXAMPLE_URL + "xhr_request",
+ causeType: "xhr",
+ causeUri: TOP_URL,
+ stack: [{ fn: "performXhrRequest", file: TOP_FILE_NAME, line: 23 }]
+ },
+ {
+ method: "GET",
+ url: EXAMPLE_URL + "fetch_request",
+ causeType: "fetch",
+ causeUri: TOP_URL,
+ stack: [{ fn: "performFetchRequest", file: TOP_FILE_NAME, line: 27 }]
+ },
+ {
+ method: "GET",
+ url: EXAMPLE_URL + "promise_fetch_request",
+ causeType: "fetch",
+ causeUri: TOP_URL,
+ stack: [
+ { fn: "performPromiseFetchRequest", file: TOP_FILE_NAME, line: 39 },
+ { fn: null, file: TOP_FILE_NAME, line: 38, asyncCause: "promise callback" },
+ ]
+ },
+ {
+ method: "GET",
+ url: EXAMPLE_URL + "timeout_fetch_request",
+ causeType: "fetch",
+ causeUri: TOP_URL,
+ stack: [
+ { fn: "performTimeoutFetchRequest", file: TOP_FILE_NAME, line: 41 },
+ { fn: "performPromiseFetchRequest", file: TOP_FILE_NAME, line: 40,
+ asyncCause: "setTimeout handler" },
+ ]
+ },
+ {
+ method: "POST",
+ url: EXAMPLE_URL + "beacon_request",
+ causeType: "beacon",
+ causeUri: TOP_URL,
+ stack: [{ fn: "performBeaconRequest", file: TOP_FILE_NAME, line: 31 }]
+ },
+];
+
+const EXPECTED_REQUESTS_SUB = [
+ {
+ method: "GET",
+ url: SUB_URL,
+ causeType: "subdocument",
+ causeUri: TOP_URL,
+ stack: false
+ },
+ {
+ method: "GET",
+ url: EXAMPLE_URL + "stylesheet_request",
+ causeType: "stylesheet",
+ causeUri: SUB_URL,
+ stack: false
+ },
+ {
+ method: "GET",
+ url: EXAMPLE_URL + "img_request",
+ causeType: "img",
+ causeUri: SUB_URL,
+ stack: false
+ },
+ {
+ method: "GET",
+ url: EXAMPLE_URL + "xhr_request",
+ causeType: "xhr",
+ causeUri: SUB_URL,
+ stack: [{ fn: "performXhrRequest", file: SUB_FILE_NAME, line: 22 }]
+ },
+ {
+ method: "GET",
+ url: EXAMPLE_URL + "fetch_request",
+ causeType: "fetch",
+ causeUri: SUB_URL,
+ stack: [{ fn: "performFetchRequest", file: SUB_FILE_NAME, line: 26 }]
+ },
+ {
+ method: "GET",
+ url: EXAMPLE_URL + "promise_fetch_request",
+ causeType: "fetch",
+ causeUri: SUB_URL,
+ stack: [
+ { fn: "performPromiseFetchRequest", file: SUB_FILE_NAME, line: 38 },
+ { fn: null, file: SUB_FILE_NAME, line: 37, asyncCause: "promise callback" },
+ ]
+ },
+ {
+ method: "GET",
+ url: EXAMPLE_URL + "timeout_fetch_request",
+ causeType: "fetch",
+ causeUri: SUB_URL,
+ stack: [
+ { fn: "performTimeoutFetchRequest", file: SUB_FILE_NAME, line: 40 },
+ { fn: "performPromiseFetchRequest", file: SUB_FILE_NAME, line: 39,
+ asyncCause: "setTimeout handler" },
+ ]
+ },
+ {
+ method: "POST",
+ url: EXAMPLE_URL + "beacon_request",
+ causeType: "beacon",
+ causeUri: SUB_URL,
+ stack: [{ fn: "performBeaconRequest", file: SUB_FILE_NAME, line: 30 }]
+ },
+];
+
+const REQUEST_COUNT = EXPECTED_REQUESTS_TOP.length + EXPECTED_REQUESTS_SUB.length;
+
+add_task(function* () {
+ // Async stacks aren't on by default in all builds
+ yield SpecialPowers.pushPrefEnv({ set: [["javascript.options.asyncstack", true]] });
+
+ // the initNetMonitor function clears the network request list after the
+ // page is loaded. That's why we first load a bogus page from SIMPLE_URL,
+ // and only then load the real thing from TOP_URL - we want to catch
+ // all the requests the page is making, not only the XHRs.
+ // We can't use about:blank here, because initNetMonitor checks that the
+ // page has actually made at least one request.
+ let { tab, monitor } = yield initNetMonitor(SIMPLE_URL);
+ let { NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+ RequestsMenu.lazyUpdate = false;
+
+ tab.linkedBrowser.loadURI(TOP_URL, null, null);
+
+ yield waitForNetworkEvents(monitor, REQUEST_COUNT);
+
+ is(RequestsMenu.itemCount, REQUEST_COUNT,
+ "All the page events should be recorded.");
+
+ // While there is a defined order for requests in each document separately, the requests
+ // from different documents may interleave in various ways that change per test run, so
+ // there is not a single order when considering all the requests together.
+ let currentTop = 0;
+ let currentSub = 0;
+ for (let i = 0; i < REQUEST_COUNT; i++) {
+ let requestItem = RequestsMenu.getItemAtIndex(i);
+
+ let itemUrl = requestItem.attachment.url;
+ let itemCauseUri = requestItem.target.querySelector(".requests-menu-cause-label")
+ .getAttribute("tooltiptext");
+ let spec;
+ if (itemUrl == SUB_URL || itemCauseUri == SUB_URL) {
+ spec = EXPECTED_REQUESTS_SUB[currentSub++];
+ } else {
+ spec = EXPECTED_REQUESTS_TOP[currentTop++];
+ }
+ let { method, url, causeType, causeUri, stack } = spec;
+
+ verifyRequestItemTarget(requestItem,
+ method, url, { cause: { type: causeType, loadingDocumentUri: causeUri } }
+ );
+
+ let { stacktrace } = requestItem.attachment.cause;
+ let stackLen = stacktrace ? stacktrace.length : 0;
+
+ if (stack) {
+ ok(stacktrace, `Request #${i} has a stacktrace`);
+ ok(stackLen > 0,
+ `Request #${i} (${causeType}) has a stacktrace with ${stackLen} items`);
+
+ // if "stack" is array, check the details about the top stack frames
+ if (Array.isArray(stack)) {
+ stack.forEach((frame, j) => {
+ is(stacktrace[j].functionName, frame.fn,
+ `Request #${i} has the correct function on JS stack frame #${j}`);
+ is(stacktrace[j].filename.split("/").pop(), frame.file,
+ `Request #${i} has the correct file on JS stack frame #${j}`);
+ is(stacktrace[j].lineNumber, frame.line,
+ `Request #${i} has the correct line number on JS stack frame #${j}`);
+ is(stacktrace[j].asyncCause, frame.asyncCause,
+ `Request #${i} has the correct async cause on JS stack frame #${j}`);
+ });
+ }
+ } else {
+ is(stackLen, 0, `Request #${i} (${causeType}) has an empty stacktrace`);
+ }
+ }
+
+ yield teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_html-preview.js b/devtools/client/netmonitor/test/browser_net_html-preview.js
new file mode 100644
index 000000000..351009de5
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_html-preview.js
@@ -0,0 +1,62 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if html responses show and properly populate a "Preview" tab.
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(CONTENT_TYPE_URL);
+ info("Starting test... ");
+
+ let { $, document, EVENTS, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, 6);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests();
+ });
+ yield wait;
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.getElementById("details-pane-toggle"));
+
+ is($("#event-details-pane").selectedIndex, 0,
+ "The first tab in the details pane should be selected.");
+ is($("#preview-tab").hidden, true,
+ "The preview tab should be hidden for non html responses.");
+ is($("#preview-tabpanel").hidden, false,
+ "The preview tabpanel is not hidden for non html responses.");
+
+ RequestsMenu.selectedIndex = 4;
+ NetMonitorView.toggleDetailsPane({ visible: true, animated: false }, 6);
+
+ is($("#event-details-pane").selectedIndex, 6,
+ "The sixth tab in the details pane should be selected.");
+ is($("#preview-tab").hidden, false,
+ "The preview tab should be visible now.");
+
+ yield monitor.panelWin.once(EVENTS.RESPONSE_HTML_PREVIEW_DISPLAYED);
+ let iframe = $("#response-preview");
+ ok(iframe,
+ "There should be a response preview iframe available.");
+ ok(iframe.contentDocument,
+ "The iframe's content document should be available.");
+ is(iframe.contentDocument.querySelector("blink").textContent, "Not Found",
+ "The iframe's content document should be loaded and correct.");
+
+ RequestsMenu.selectedIndex = 5;
+
+ is($("#event-details-pane").selectedIndex, 0,
+ "The first tab in the details pane should be selected again.");
+ is($("#preview-tab").hidden, true,
+ "The preview tab should be hidden again for non html responses.");
+ is($("#preview-tabpanel").hidden, false,
+ "The preview tabpanel is not hidden again for non html responses.");
+
+ yield teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_icon-preview.js b/devtools/client/netmonitor/test/browser_net_icon-preview.js
new file mode 100644
index 000000000..e3c5bde4e
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_icon-preview.js
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if image responses show a thumbnail in the requests menu.
+ */
+
+add_task(function* () {
+ let Actions = require("devtools/client/netmonitor/actions/index");
+
+ let { tab, monitor } = yield initNetMonitor(CONTENT_TYPE_WITHOUT_CACHE_URL);
+ info("Starting test... ");
+
+ let { $, $all, EVENTS, ACTIVITY_TYPE, NetMonitorView, NetMonitorController,
+ gStore } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ let wait = waitForEvents();
+ yield performRequests();
+ yield wait;
+
+ info("Checking the image thumbnail when all items are shown.");
+ checkImageThumbnail();
+
+ RequestsMenu.sortBy("size");
+ info("Checking the image thumbnail when all items are sorted.");
+ checkImageThumbnail();
+
+ gStore.dispatch(Actions.toggleFilterType("images"));
+ info("Checking the image thumbnail when only images are shown.");
+ checkImageThumbnail();
+
+ info("Reloading the debuggee and performing all requests again...");
+ wait = waitForEvents();
+ yield reloadAndPerformRequests();
+ yield wait;
+
+ info("Checking the image thumbnail after a reload.");
+ checkImageThumbnail();
+
+ yield teardown(monitor);
+
+ function waitForEvents() {
+ return promise.all([
+ waitForNetworkEvents(monitor, CONTENT_TYPE_WITHOUT_CACHE_REQUESTS),
+ monitor.panelWin.once(EVENTS.RESPONSE_IMAGE_THUMBNAIL_DISPLAYED)
+ ]);
+ }
+
+ function performRequests() {
+ return ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests();
+ });
+ }
+
+ function* reloadAndPerformRequests() {
+ yield NetMonitorController.triggerActivity(ACTIVITY_TYPE.RELOAD.WITH_CACHE_ENABLED);
+ yield performRequests();
+ }
+
+ function checkImageThumbnail() {
+ is($all(".requests-menu-icon[type=thumbnail]").length, 1,
+ "There should be only one image request with a thumbnail displayed.");
+ is($(".requests-menu-icon[type=thumbnail]").src, TEST_IMAGE_DATA_URI,
+ "The image requests-menu-icon thumbnail is displayed correctly.");
+ is($(".requests-menu-icon[type=thumbnail]").hidden, false,
+ "The image requests-menu-icon thumbnail should not be hidden.");
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_image-tooltip.js b/devtools/client/netmonitor/test/browser_net_image-tooltip.js
new file mode 100644
index 000000000..04cd26959
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_image-tooltip.js
@@ -0,0 +1,101 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const IMAGE_TOOLTIP_URL = EXAMPLE_URL + "html_image-tooltip-test-page.html";
+const IMAGE_TOOLTIP_REQUESTS = 1;
+
+/**
+ * Tests if image responses show a popup in the requests menu when hovered.
+ */
+add_task(function* test() {
+ let { tab, monitor } = yield initNetMonitor(IMAGE_TOOLTIP_URL);
+ info("Starting test... ");
+
+ let { $, EVENTS, ACTIVITY_TYPE, NetMonitorView, NetMonitorController } =
+ monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+ RequestsMenu.lazyUpdate = true;
+
+ let onEvents = waitForNetworkEvents(monitor, IMAGE_TOOLTIP_REQUESTS);
+ let onThumbnail = monitor.panelWin.once(EVENTS.RESPONSE_IMAGE_THUMBNAIL_DISPLAYED);
+
+ yield performRequests();
+ yield onEvents;
+ yield onThumbnail;
+
+ info("Checking the image thumbnail after a few requests were made...");
+ yield showTooltipAndVerify(RequestsMenu.tooltip, RequestsMenu.items[0]);
+
+ // Hide tooltip before next test, to avoid the situation that tooltip covers
+ // the icon for the request of the next test.
+ info("Checking the image thumbnail gets hidden...");
+ yield hideTooltipAndVerify(RequestsMenu.tooltip, RequestsMenu.items[0]);
+
+ // +1 extra document reload
+ onEvents = waitForNetworkEvents(monitor, IMAGE_TOOLTIP_REQUESTS + 1);
+ onThumbnail = monitor.panelWin.once(EVENTS.RESPONSE_IMAGE_THUMBNAIL_DISPLAYED);
+
+ info("Reloading the debuggee and performing all requests again...");
+ yield NetMonitorController.triggerActivity(ACTIVITY_TYPE.RELOAD.WITH_CACHE_ENABLED);
+ yield performRequests();
+ yield onEvents;
+ yield onThumbnail;
+
+ info("Checking the image thumbnail after a reload.");
+ yield showTooltipAndVerify(RequestsMenu.tooltip, RequestsMenu.items[1]);
+
+ info("Checking if the image thumbnail is hidden when mouse leaves the menu widget");
+ let requestsMenuEl = $("#requests-menu-contents");
+ let onHidden = RequestsMenu.tooltip.once("hidden");
+ EventUtils.synthesizeMouse(requestsMenuEl, 0, 0, {type: "mouseout"}, monitor.panelWin);
+ yield onHidden;
+
+ yield teardown(monitor);
+
+ function performRequests() {
+ return ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests();
+ });
+ }
+
+ /**
+ * Show a tooltip on the {requestItem} and verify that it was displayed
+ * with the expected content.
+ */
+ function* showTooltipAndVerify(tooltip, requestItem) {
+ let anchor = $(".requests-menu-file", requestItem.target);
+ yield showTooltipOn(tooltip, anchor);
+
+ info("Tooltip was successfully opened for the image request.");
+ is(tooltip.panel.querySelector("img").src, TEST_IMAGE_DATA_URI,
+ "The tooltip's image content is displayed correctly.");
+ }
+
+ /**
+ * Trigger a tooltip over an element by sending mousemove event.
+ * @return a promise that resolves when the tooltip is shown
+ */
+ function showTooltipOn(tooltip, element) {
+ let onShown = tooltip.once("shown");
+ let win = element.ownerDocument.defaultView;
+ EventUtils.synthesizeMouseAtCenter(element, {type: "mousemove"}, win);
+ return onShown;
+ }
+
+ /**
+ * Hide a tooltip on the {requestItem} and verify that it was closed.
+ */
+ function* hideTooltipAndVerify(tooltip, requestItem) {
+ // Hovering method hides tooltip.
+ let anchor = $(".requests-menu-method", requestItem.target);
+
+ let onHidden = tooltip.once("hidden");
+ let win = anchor.ownerDocument.defaultView;
+ EventUtils.synthesizeMouseAtCenter(anchor, {type: "mousemove"}, win);
+ yield onHidden;
+
+ info("Tooltip was successfully closed.");
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_json-long.js b/devtools/client/netmonitor/test/browser_net_json-long.js
new file mode 100644
index 000000000..2347d26c4
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_json-long.js
@@ -0,0 +1,98 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if very long JSON responses are handled correctly.
+ */
+
+add_task(function* () {
+ let { L10N } = require("devtools/client/netmonitor/l10n");
+
+ let { tab, monitor } = yield initNetMonitor(JSON_LONG_URL);
+ info("Starting test... ");
+
+ // This is receiving over 80 KB of json and will populate over 6000 items
+ // in a variables view instance. Debug builds are slow.
+ requestLongerTimeout(4);
+
+ let { document, EVENTS, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, 1);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests();
+ });
+ yield wait;
+
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(0),
+ "GET", CONTENT_TYPE_SJS + "?fmt=json-long", {
+ status: 200,
+ statusText: "OK",
+ type: "json",
+ fullMimeType: "text/json; charset=utf-8",
+ size: L10N.getFormatStr("networkMenu.sizeKB",
+ L10N.numberWithDecimals(85975 / 1024, 2)),
+ time: true
+ });
+
+ let onEvent = monitor.panelWin.once(EVENTS.RESPONSE_BODY_DISPLAYED);
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.getElementById("details-pane-toggle"));
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.querySelectorAll("#details-pane tab")[3]);
+ yield onEvent;
+
+ testResponseTab();
+
+ yield teardown(monitor);
+
+ function testResponseTab() {
+ let tabEl = document.querySelectorAll("#details-pane tab")[3];
+ let tabpanel = document.querySelectorAll("#details-pane tabpanel")[3];
+
+ is(tabEl.getAttribute("selected"), "true",
+ "The response tab in the network details pane should be selected.");
+
+ is(tabpanel.querySelector("#response-content-info-header")
+ .hasAttribute("hidden"), true,
+ "The response info header doesn't have the intended visibility.");
+ is(tabpanel.querySelector("#response-content-json-box")
+ .hasAttribute("hidden"), false,
+ "The response content json box doesn't have the intended visibility.");
+ is(tabpanel.querySelector("#response-content-textarea-box")
+ .hasAttribute("hidden"), true,
+ "The response content textarea box doesn't have the intended visibility.");
+ is(tabpanel.querySelector("#response-content-image-box")
+ .hasAttribute("hidden"), true,
+ "The response content image box doesn't have the intended visibility.");
+
+ is(tabpanel.querySelectorAll(".variables-view-scope").length, 1,
+ "There should be 1 json scope displayed in this tabpanel.");
+ is(tabpanel.querySelectorAll(".variables-view-property").length, 6143,
+ "There should be 6143 json properties displayed in this tabpanel.");
+ is(tabpanel.querySelectorAll(".variables-view-empty-notice").length, 0,
+ "The empty notice should not be displayed in this tabpanel.");
+
+ let jsonScope = tabpanel.querySelectorAll(".variables-view-scope")[0];
+ let names = ".variables-view-property > .title > .name";
+ let values = ".variables-view-property > .title > .value";
+
+ is(jsonScope.querySelector(".name").getAttribute("value"),
+ L10N.getStr("jsonScopeName"),
+ "The json scope doesn't have the correct title.");
+
+ is(jsonScope.querySelectorAll(names)[0].getAttribute("value"),
+ "0", "The first json property name was incorrect.");
+ is(jsonScope.querySelectorAll(values)[0].getAttribute("value"),
+ "Object", "The first json property value was incorrect.");
+
+ is(jsonScope.querySelectorAll(names)[1].getAttribute("value"),
+ "greeting", "The second json property name was incorrect.");
+ is(jsonScope.querySelectorAll(values)[1].getAttribute("value"),
+ "\"Hello long string JSON!\"", "The second json property value was incorrect.");
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_json-malformed.js b/devtools/client/netmonitor/test/browser_net_json-malformed.js
new file mode 100644
index 000000000..6bed60480
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_json-malformed.js
@@ -0,0 +1,77 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if malformed JSON responses are handled correctly.
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(JSON_MALFORMED_URL);
+ info("Starting test... ");
+
+ let { document, EVENTS, Editor, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, 1);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests();
+ });
+ yield wait;
+
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(0),
+ "GET", CONTENT_TYPE_SJS + "?fmt=json-malformed", {
+ status: 200,
+ statusText: "OK",
+ type: "json",
+ fullMimeType: "text/json; charset=utf-8"
+ });
+
+ let onEvent = monitor.panelWin.once(EVENTS.RESPONSE_BODY_DISPLAYED);
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.getElementById("details-pane-toggle"));
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.querySelectorAll("#details-pane tab")[3]);
+ yield onEvent;
+
+ let tabEl = document.querySelectorAll("#details-pane tab")[3];
+ let tabpanel = document.querySelectorAll("#details-pane tabpanel")[3];
+
+ is(tabEl.getAttribute("selected"), "true",
+ "The response tab in the network details pane should be selected.");
+
+ is(tabpanel.querySelector("#response-content-info-header")
+ .hasAttribute("hidden"), false,
+ "The response info header doesn't have the intended visibility.");
+ is(tabpanel.querySelector("#response-content-info-header")
+ .getAttribute("value"),
+ "SyntaxError: JSON.parse: unexpected non-whitespace character after JSON data" +
+ " at line 1 column 40 of the JSON data",
+ "The response info header doesn't have the intended value attribute.");
+ is(tabpanel.querySelector("#response-content-info-header")
+ .getAttribute("tooltiptext"),
+ "SyntaxError: JSON.parse: unexpected non-whitespace character after JSON data" +
+ " at line 1 column 40 of the JSON data",
+ "The response info header doesn't have the intended tooltiptext attribute.");
+
+ is(tabpanel.querySelector("#response-content-json-box")
+ .hasAttribute("hidden"), true,
+ "The response content json box doesn't have the intended visibility.");
+ is(tabpanel.querySelector("#response-content-textarea-box")
+ .hasAttribute("hidden"), false,
+ "The response content textarea box doesn't have the intended visibility.");
+ is(tabpanel.querySelector("#response-content-image-box")
+ .hasAttribute("hidden"), true,
+ "The response content image box doesn't have the intended visibility.");
+
+ let editor = yield NetMonitorView.editor("#response-content-textarea");
+ is(editor.getText(), "{ \"greeting\": \"Hello malformed JSON!\" },",
+ "The text shown in the source editor is incorrect.");
+ is(editor.getMode(), Editor.modes.js,
+ "The mode active in the source editor is incorrect.");
+
+ yield teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_json_custom_mime.js b/devtools/client/netmonitor/test/browser_net_json_custom_mime.js
new file mode 100644
index 000000000..210ffbbe8
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_json_custom_mime.js
@@ -0,0 +1,90 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if JSON responses with unusal/custom MIME types are handled correctly.
+ */
+
+add_task(function* () {
+ let { L10N } = require("devtools/client/netmonitor/l10n");
+
+ let { tab, monitor } = yield initNetMonitor(JSON_CUSTOM_MIME_URL);
+ info("Starting test... ");
+
+ let { document, EVENTS, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, 1);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests();
+ });
+ yield wait;
+
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(0),
+ "GET", CONTENT_TYPE_SJS + "?fmt=json-custom-mime", {
+ status: 200,
+ statusText: "OK",
+ type: "x-bigcorp-json",
+ fullMimeType: "text/x-bigcorp-json; charset=utf-8",
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 41),
+ time: true
+ });
+
+ let onEvent = monitor.panelWin.once(EVENTS.RESPONSE_BODY_DISPLAYED);
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.getElementById("details-pane-toggle"));
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.querySelectorAll("#details-pane tab")[3]);
+ yield onEvent;
+
+ testResponseTab();
+
+ yield teardown(monitor);
+
+ function testResponseTab() {
+ let tabEl = document.querySelectorAll("#details-pane tab")[3];
+ let tabpanel = document.querySelectorAll("#details-pane tabpanel")[3];
+
+ is(tabEl.getAttribute("selected"), "true",
+ "The response tab in the network details pane should be selected.");
+
+ is(tabpanel.querySelector("#response-content-info-header")
+ .hasAttribute("hidden"), true,
+ "The response info header doesn't have the intended visibility.");
+ is(tabpanel.querySelector("#response-content-json-box")
+ .hasAttribute("hidden"), false,
+ "The response content json box doesn't have the intended visibility.");
+ is(tabpanel.querySelector("#response-content-textarea-box")
+ .hasAttribute("hidden"), true,
+ "The response content textarea box doesn't have the intended visibility.");
+ is(tabpanel.querySelector("#response-content-image-box")
+ .hasAttribute("hidden"), true,
+ "The response content image box doesn't have the intended visibility.");
+
+ is(tabpanel.querySelectorAll(".variables-view-scope").length, 1,
+ "There should be 1 json scope displayed in this tabpanel.");
+ is(tabpanel.querySelectorAll(".variables-view-property").length, 2,
+ "There should be 2 json properties displayed in this tabpanel.");
+ is(tabpanel.querySelectorAll(".variables-view-empty-notice").length, 0,
+ "The empty notice should not be displayed in this tabpanel.");
+
+ let jsonScope = tabpanel.querySelectorAll(".variables-view-scope")[0];
+ is(jsonScope.querySelectorAll(".variables-view-property .name")[0]
+ .getAttribute("value"),
+ "greeting", "The first json property name was incorrect.");
+ is(jsonScope.querySelectorAll(".variables-view-property .value")[0]
+ .getAttribute("value"),
+ "\"Hello oddly-named JSON!\"", "The first json property value was incorrect.");
+
+ is(jsonScope.querySelectorAll(".variables-view-property .name")[1]
+ .getAttribute("value"),
+ "__proto__", "The second json property name was incorrect.");
+ is(jsonScope.querySelectorAll(".variables-view-property .value")[1]
+ .getAttribute("value"),
+ "Object", "The second json property value was incorrect.");
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_json_text_mime.js b/devtools/client/netmonitor/test/browser_net_json_text_mime.js
new file mode 100644
index 000000000..edc98a5c9
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_json_text_mime.js
@@ -0,0 +1,90 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if JSON responses with unusal/custom MIME types are handled correctly.
+ */
+
+add_task(function* () {
+ let { L10N } = require("devtools/client/netmonitor/l10n");
+
+ let { tab, monitor } = yield initNetMonitor(JSON_TEXT_MIME_URL);
+ info("Starting test... ");
+
+ let { document, EVENTS, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, 1);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests();
+ });
+ yield wait;
+
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(0),
+ "GET", CONTENT_TYPE_SJS + "?fmt=json-text-mime", {
+ status: 200,
+ statusText: "OK",
+ type: "plain",
+ fullMimeType: "text/plain; charset=utf-8",
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 41),
+ time: true
+ });
+
+ let onEvent = monitor.panelWin.once(EVENTS.RESPONSE_BODY_DISPLAYED);
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.getElementById("details-pane-toggle"));
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.querySelectorAll("#details-pane tab")[3]);
+ yield onEvent;
+
+ testResponseTab();
+
+ yield teardown(monitor);
+
+ function testResponseTab() {
+ let tabEl = document.querySelectorAll("#details-pane tab")[3];
+ let tabpanel = document.querySelectorAll("#details-pane tabpanel")[3];
+
+ is(tabEl.getAttribute("selected"), "true",
+ "The response tab in the network details pane should be selected.");
+
+ is(tabpanel.querySelector("#response-content-info-header")
+ .hasAttribute("hidden"), true,
+ "The response info header doesn't have the intended visibility.");
+ is(tabpanel.querySelector("#response-content-json-box")
+ .hasAttribute("hidden"), false,
+ "The response content json box doesn't have the intended visibility.");
+ is(tabpanel.querySelector("#response-content-textarea-box")
+ .hasAttribute("hidden"), true,
+ "The response content textarea box doesn't have the intended visibility.");
+ is(tabpanel.querySelector("#response-content-image-box")
+ .hasAttribute("hidden"), true,
+ "The response content image box doesn't have the intended visibility.");
+
+ is(tabpanel.querySelectorAll(".variables-view-scope").length, 1,
+ "There should be 1 json scope displayed in this tabpanel.");
+ is(tabpanel.querySelectorAll(".variables-view-property").length, 2,
+ "There should be 2 json properties displayed in this tabpanel.");
+ is(tabpanel.querySelectorAll(".variables-view-empty-notice").length, 0,
+ "The empty notice should not be displayed in this tabpanel.");
+
+ let jsonScope = tabpanel.querySelectorAll(".variables-view-scope")[0];
+ is(jsonScope.querySelectorAll(".variables-view-property .name")[0]
+ .getAttribute("value"),
+ "greeting", "The first json property name was incorrect.");
+ is(jsonScope.querySelectorAll(".variables-view-property .value")[0]
+ .getAttribute("value"),
+ "\"Hello third-party JSON!\"", "The first json property value was incorrect.");
+
+ is(jsonScope.querySelectorAll(".variables-view-property .name")[1]
+ .getAttribute("value"),
+ "__proto__", "The second json property name was incorrect.");
+ is(jsonScope.querySelectorAll(".variables-view-property .value")[1]
+ .getAttribute("value"),
+ "Object", "The second json property value was incorrect.");
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_jsonp.js b/devtools/client/netmonitor/test/browser_net_jsonp.js
new file mode 100644
index 000000000..3007d8c4d
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_jsonp.js
@@ -0,0 +1,111 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if JSONP responses are handled correctly.
+ */
+
+add_task(function* () {
+ let { L10N } = require("devtools/client/netmonitor/l10n");
+
+ let { tab, monitor } = yield initNetMonitor(JSONP_URL);
+ info("Starting test... ");
+
+ let { document, EVENTS, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu, NetworkDetails } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+ NetworkDetails._json.lazyEmpty = false;
+
+ let wait = waitForNetworkEvents(monitor, 2);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests();
+ });
+ yield wait;
+
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(0),
+ "GET", CONTENT_TYPE_SJS + "?fmt=jsonp&jsonp=$_0123Fun", {
+ status: 200,
+ statusText: "OK",
+ type: "json",
+ fullMimeType: "text/json; charset=utf-8",
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 41),
+ time: true
+ });
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(1),
+ "GET", CONTENT_TYPE_SJS + "?fmt=jsonp2&jsonp=$_4567Sad", {
+ status: 200,
+ statusText: "OK",
+ type: "json",
+ fullMimeType: "text/json; charset=utf-8",
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 54),
+ time: true
+ });
+
+ let onEvent = monitor.panelWin.once(EVENTS.RESPONSE_BODY_DISPLAYED);
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.getElementById("details-pane-toggle"));
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.querySelectorAll("#details-pane tab")[3]);
+ yield onEvent;
+
+ testResponseTab("$_0123Fun", "\"Hello JSONP!\"");
+
+ onEvent = monitor.panelWin.once(EVENTS.RESPONSE_BODY_DISPLAYED);
+ RequestsMenu.selectedIndex = 1;
+ yield onEvent;
+
+ testResponseTab("$_4567Sad", "\"Hello weird JSONP!\"");
+
+ yield teardown(monitor);
+
+ function testResponseTab(func, greeting) {
+ let tabEl = document.querySelectorAll("#details-pane tab")[3];
+ let tabpanel = document.querySelectorAll("#details-pane tabpanel")[3];
+
+ is(tabEl.getAttribute("selected"), "true",
+ "The response tab in the network details pane should be selected.");
+
+ is(tabpanel.querySelector("#response-content-info-header")
+ .hasAttribute("hidden"), true,
+ "The response info header doesn't have the intended visibility.");
+ is(tabpanel.querySelector("#response-content-json-box")
+ .hasAttribute("hidden"), false,
+ "The response content json box doesn't have the intended visibility.");
+ is(tabpanel.querySelector("#response-content-textarea-box")
+ .hasAttribute("hidden"), true,
+ "The response content textarea box doesn't have the intended visibility.");
+ is(tabpanel.querySelector("#response-content-image-box")
+ .hasAttribute("hidden"), true,
+ "The response content image box doesn't have the intended visibility.");
+
+ is(tabpanel.querySelectorAll(".variables-view-scope").length, 1,
+ "There should be 1 json scope displayed in this tabpanel.");
+ is(tabpanel.querySelectorAll(".variables-view-property").length, 2,
+ "There should be 2 json properties displayed in this tabpanel.");
+ is(tabpanel.querySelectorAll(".variables-view-empty-notice").length, 0,
+ "The empty notice should not be displayed in this tabpanel.");
+
+ let jsonScope = tabpanel.querySelectorAll(".variables-view-scope")[0];
+
+ is(jsonScope.querySelector(".name").getAttribute("value"),
+ L10N.getFormatStr("jsonpScopeName", func),
+ "The json scope doesn't have the correct title.");
+
+ is(jsonScope.querySelectorAll(".variables-view-property .name")[0]
+ .getAttribute("value"),
+ "greeting", "The first json property name was incorrect.");
+ is(jsonScope.querySelectorAll(".variables-view-property .value")[0]
+ .getAttribute("value"),
+ greeting, "The first json property value was incorrect.");
+
+ is(jsonScope.querySelectorAll(".variables-view-property .name")[1]
+ .getAttribute("value"),
+ "__proto__", "The second json property name was incorrect.");
+ is(jsonScope.querySelectorAll(".variables-view-property .value")[1]
+ .getAttribute("value"),
+ "Object", "The second json property value was incorrect.");
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_large-response.js b/devtools/client/netmonitor/test/browser_net_large-response.js
new file mode 100644
index 000000000..98d67b46d
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_large-response.js
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if very large response contents are just displayed as plain text.
+ */
+
+const HTML_LONG_URL = CONTENT_TYPE_SJS + "?fmt=html-long";
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(CUSTOM_GET_URL);
+ info("Starting test... ");
+
+ // This test could potentially be slow because over 100 KB of stuff
+ // is going to be requested and displayed in the source editor.
+ requestLongerTimeout(2);
+
+ let { document, EVENTS, Editor, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, 1);
+ yield ContentTask.spawn(tab.linkedBrowser, HTML_LONG_URL, function* (url) {
+ content.wrappedJSObject.performRequests(1, url);
+ });
+ yield wait;
+
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(0),
+ "GET", CONTENT_TYPE_SJS + "?fmt=html-long", {
+ status: 200,
+ statusText: "OK"
+ });
+
+ let onEvent = monitor.panelWin.once(EVENTS.RESPONSE_BODY_DISPLAYED);
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.getElementById("details-pane-toggle"));
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.querySelectorAll("#details-pane tab")[3]);
+ yield onEvent;
+
+ let editor = yield NetMonitorView.editor("#response-content-textarea");
+ ok(editor.getText().match(/^<p>/),
+ "The text shown in the source editor is incorrect.");
+ is(editor.getMode(), Editor.modes.text,
+ "The mode active in the source editor is incorrect.");
+
+ yield teardown(monitor);
+
+ // This test uses a lot of memory, so force a GC to help fragmentation.
+ info("Forcing GC after netmonitor test.");
+ Cu.forceGC();
+});
diff --git a/devtools/client/netmonitor/test/browser_net_leak_on_tab_close.js b/devtools/client/netmonitor/test/browser_net_leak_on_tab_close.js
new file mode 100644
index 000000000..9e788f36c
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_leak_on_tab_close.js
@@ -0,0 +1,17 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests that netmonitor doesn't leak windows on parent-side pages (bug 1285638)
+ */
+
+add_task(function* () {
+ // Tell initNetMonitor to enable cache. Otherwise it will assert that there were more
+ // than zero network requests during the page load. But when loading about:config,
+ // there are none.
+ let { monitor } = yield initNetMonitor("about:config", null, true);
+ ok(monitor, "The network monitor was opened");
+ yield teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_open_request_in_tab.js b/devtools/client/netmonitor/test/browser_net_open_request_in_tab.js
new file mode 100644
index 000000000..8e7ffcc84
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_open_request_in_tab.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if Open in new tab works.
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(CUSTOM_GET_URL);
+ info("Starting test...");
+
+ let { NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, 1);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests(1);
+ });
+ yield wait;
+
+ let requestItem = RequestsMenu.getItemAtIndex(0);
+ RequestsMenu.selectedItem = requestItem;
+
+ let onTabOpen = once(gBrowser.tabContainer, "TabOpen", false);
+ RequestsMenu.contextMenu.openRequestInTab();
+ yield onTabOpen;
+
+ ok(true, "A new tab has been opened");
+
+ yield teardown(monitor);
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/client/netmonitor/test/browser_net_page-nav.js b/devtools/client/netmonitor/test/browser_net_page-nav.js
new file mode 100644
index 000000000..6ac18297c
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_page-nav.js
@@ -0,0 +1,69 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if page navigation ("close", "navigate", etc.) triggers an appropriate
+ * action in the network monitor.
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(SIMPLE_URL);
+ info("Starting test... ");
+
+ let { EVENTS } = monitor.panelWin;
+
+ yield testNavigate();
+ yield testNavigateBack();
+ yield testClose();
+
+ function* testNavigate() {
+ info("Navigating forward...");
+
+ let onWillNav = monitor.panelWin.once(EVENTS.TARGET_WILL_NAVIGATE);
+ let onDidNav = monitor.panelWin.once(EVENTS.TARGET_DID_NAVIGATE);
+
+ tab.linkedBrowser.loadURI(NAVIGATE_URL);
+ yield onWillNav;
+
+ is(tab.linkedBrowser.currentURI.spec, SIMPLE_URL,
+ "Target started navigating to the correct location.");
+
+ yield onDidNav;
+ is(tab.linkedBrowser.currentURI.spec, NAVIGATE_URL,
+ "Target finished navigating to the correct location.");
+ }
+
+ function* testNavigateBack() {
+ info("Navigating backward...");
+
+ let onWillNav = monitor.panelWin.once(EVENTS.TARGET_WILL_NAVIGATE);
+ let onDidNav = monitor.panelWin.once(EVENTS.TARGET_DID_NAVIGATE);
+
+ tab.linkedBrowser.loadURI(SIMPLE_URL);
+ yield onWillNav;
+
+ is(tab.linkedBrowser.currentURI.spec, NAVIGATE_URL,
+ "Target started navigating back to the previous location.");
+
+ yield onDidNav;
+ is(tab.linkedBrowser.currentURI.spec, SIMPLE_URL,
+ "Target finished navigating back to the previous location.");
+ }
+
+ function* testClose() {
+ info("Closing...");
+
+ let onDestroyed = monitor.once("destroyed");
+ removeTab(tab);
+ yield onDestroyed;
+
+ ok(!monitor._controller.client,
+ "There shouldn't be a client available after destruction.");
+ ok(!monitor._controller.tabClient,
+ "There shouldn't be a tabClient available after destruction.");
+ ok(!monitor._controller.webConsoleClient,
+ "There shouldn't be a webConsoleClient available after destruction.");
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_pane-collapse.js b/devtools/client/netmonitor/test/browser_net_pane-collapse.js
new file mode 100644
index 000000000..2760ec000
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_pane-collapse.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if the network monitor panes collapse properly.
+ */
+
+add_task(function* () {
+ let { monitor } = yield initNetMonitor(SIMPLE_URL);
+ info("Starting test... ");
+
+ let { document, Prefs, NetMonitorView } = monitor.panelWin;
+ let detailsPane = document.getElementById("details-pane");
+ let detailsPaneToggleButton = document.getElementById("details-pane-toggle");
+
+ ok(detailsPane.classList.contains("pane-collapsed") &&
+ detailsPaneToggleButton.classList.contains("pane-collapsed"),
+ "The details pane should initially be hidden.");
+
+ NetMonitorView.toggleDetailsPane({ visible: true, animated: false });
+
+ let width = ~~(detailsPane.getAttribute("width"));
+ is(width, Prefs.networkDetailsWidth,
+ "The details pane has an incorrect width.");
+ is(detailsPane.style.marginLeft, "0px",
+ "The details pane has an incorrect left margin.");
+ is(detailsPane.style.marginRight, "0px",
+ "The details pane has an incorrect right margin.");
+ ok(!detailsPane.hasAttribute("animated"),
+ "The details pane has an incorrect animated attribute.");
+ ok(!detailsPane.classList.contains("pane-collapsed") &&
+ !detailsPaneToggleButton.classList.contains("pane-collapsed"),
+ "The details pane should at this point be visible.");
+
+ // Trigger reflow to make sure the UI is in required state.
+ document.documentElement.getBoundingClientRect();
+
+ NetMonitorView.toggleDetailsPane({ visible: false, animated: true });
+
+ yield once(detailsPane, "transitionend");
+
+ let margin = -(width + 1) + "px";
+ is(width, Prefs.networkDetailsWidth,
+ "The details pane has an incorrect width after collapsing.");
+ is(detailsPane.style.marginLeft, margin,
+ "The details pane has an incorrect left margin after collapsing.");
+ is(detailsPane.style.marginRight, margin,
+ "The details pane has an incorrect right margin after collapsing.");
+ ok(!detailsPane.hasAttribute("animated"),
+ "The details pane has an incorrect attribute after an animated collapsing.");
+ ok(detailsPane.classList.contains("pane-collapsed") &&
+ detailsPaneToggleButton.classList.contains("pane-collapsed"),
+ "The details pane should not be visible after collapsing.");
+
+ NetMonitorView.toggleDetailsPane({ visible: true, animated: false });
+
+ is(width, Prefs.networkDetailsWidth,
+ "The details pane has an incorrect width after uncollapsing.");
+ is(detailsPane.style.marginLeft, "0px",
+ "The details pane has an incorrect left margin after uncollapsing.");
+ is(detailsPane.style.marginRight, "0px",
+ "The details pane has an incorrect right margin after uncollapsing.");
+ ok(!detailsPane.hasAttribute("animated"),
+ "The details pane has an incorrect attribute after an unanimated uncollapsing.");
+ ok(!detailsPane.classList.contains("pane-collapsed") &&
+ !detailsPaneToggleButton.classList.contains("pane-collapsed"),
+ "The details pane should be visible again after uncollapsing.");
+
+ yield teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_pane-toggle.js b/devtools/client/netmonitor/test/browser_net_pane-toggle.js
new file mode 100644
index 000000000..87b71019c
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_pane-toggle.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if toggling the details pane works as expected.
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(SIMPLE_URL);
+ info("Starting test... ");
+
+ let { $, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+ let { NETWORK_EVENT, TAB_UPDATED } = monitor.panelWin.EVENTS;
+ RequestsMenu.lazyUpdate = false;
+
+ let toggleButton = $("#details-pane-toggle");
+
+ is(toggleButton.hasAttribute("disabled"), true,
+ "The pane toggle button should be disabled when the frontend is opened.");
+ is(toggleButton.classList.contains("pane-collapsed"), true,
+ "The pane toggle button should indicate that the details pane is " +
+ "collapsed when the frontend is opened.");
+ is(NetMonitorView.detailsPaneHidden, true,
+ "The details pane should be hidden when the frontend is opened.");
+ is(RequestsMenu.selectedItem, null,
+ "There should be no selected item in the requests menu.");
+
+ let networkEvent = monitor.panelWin.once(NETWORK_EVENT);
+ tab.linkedBrowser.reload();
+ yield networkEvent;
+
+ is(toggleButton.hasAttribute("disabled"), false,
+ "The pane toggle button should be enabled after the first request.");
+ is(toggleButton.classList.contains("pane-collapsed"), true,
+ "The pane toggle button should still indicate that the details pane is " +
+ "collapsed after the first request.");
+ is(NetMonitorView.detailsPaneHidden, true,
+ "The details pane should still be hidden after the first request.");
+ is(RequestsMenu.selectedItem, null,
+ "There should still be no selected item in the requests menu.");
+
+ EventUtils.sendMouseEvent({ type: "mousedown" }, toggleButton);
+
+ yield monitor.panelWin.once(TAB_UPDATED);
+
+ is(toggleButton.hasAttribute("disabled"), false,
+ "The pane toggle button should still be enabled after being pressed.");
+ is(toggleButton.classList.contains("pane-collapsed"), false,
+ "The pane toggle button should now indicate that the details pane is " +
+ "not collapsed anymore after being pressed.");
+ is(NetMonitorView.detailsPaneHidden, false,
+ "The details pane should not be hidden after toggle button was pressed.");
+ isnot(RequestsMenu.selectedItem, null,
+ "There should be a selected item in the requests menu.");
+ is(RequestsMenu.selectedIndex, 0,
+ "The first item should be selected in the requests menu.");
+
+ EventUtils.sendMouseEvent({ type: "mousedown" }, toggleButton);
+
+ is(toggleButton.hasAttribute("disabled"), false,
+ "The pane toggle button should still be enabled after being pressed again.");
+ is(toggleButton.classList.contains("pane-collapsed"), true,
+ "The pane toggle button should now indicate that the details pane is " +
+ "collapsed after being pressed again.");
+ is(NetMonitorView.detailsPaneHidden, true,
+ "The details pane should now be hidden after the toggle button was pressed again.");
+ is(RequestsMenu.selectedItem, null,
+ "There should now be no selected item in the requests menu.");
+
+ yield teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_persistent_logs.js b/devtools/client/netmonitor/test/browser_net_persistent_logs.js
new file mode 100644
index 000000000..ac2371e1f
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_persistent_logs.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if the network monitor leaks on initialization and sudden destruction.
+ * You can also use this initialization format as a template for other tests.
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(SINGLE_GET_URL);
+ info("Starting test... ");
+
+ let { NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ Services.prefs.setBoolPref("devtools.webconsole.persistlog", false);
+
+ yield reloadAndWait();
+
+ is(RequestsMenu.itemCount, 2,
+ "The request menu should have two items at this point.");
+
+ yield reloadAndWait();
+
+ // Since the reload clears the log, we still expect two requests in the log
+ is(RequestsMenu.itemCount, 2,
+ "The request menu should still have two items at this point.");
+
+ // Now we toggle the persistence logs on
+ Services.prefs.setBoolPref("devtools.webconsole.persistlog", true);
+
+ yield reloadAndWait();
+
+ // Since we togged the persistence logs, we expect four items after the reload
+ is(RequestsMenu.itemCount, 4,
+ "The request menu should now have four items at this point.");
+
+ Services.prefs.setBoolPref("devtools.webconsole.persistlog", false);
+ return teardown(monitor);
+
+ /**
+ * Reload the page and wait for 2 GET requests. Race-free.
+ */
+ function reloadAndWait() {
+ let wait = waitForNetworkEvents(monitor, 2);
+ tab.linkedBrowser.reload();
+ return wait;
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_post-data-01.js b/devtools/client/netmonitor/test/browser_net_post-data-01.js
new file mode 100644
index 000000000..6d5f8dc1b
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_post-data-01.js
@@ -0,0 +1,166 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if the POST requests display the correct information in the UI.
+ */
+
+add_task(function* () {
+ let { L10N } = require("devtools/client/netmonitor/l10n");
+
+ let { tab, monitor } = yield initNetMonitor(POST_DATA_URL);
+ info("Starting test... ");
+
+ let { document, EVENTS, Editor, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu, NetworkDetails } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+ NetworkDetails._params.lazyEmpty = false;
+
+ let wait = waitForNetworkEvents(monitor, 0, 2);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests();
+ });
+ yield wait;
+
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(0),
+ "POST", SIMPLE_SJS + "?foo=bar&baz=42&type=urlencoded", {
+ status: 200,
+ statusText: "Och Aye",
+ type: "plain",
+ fullMimeType: "text/plain; charset=utf-8",
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 12),
+ time: true
+ });
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(1),
+ "POST", SIMPLE_SJS + "?foo=bar&baz=42&type=multipart", {
+ status: 200,
+ statusText: "Och Aye",
+ type: "plain",
+ fullMimeType: "text/plain; charset=utf-8",
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 12),
+ time: true
+ });
+
+ let onEvent = monitor.panelWin.once(EVENTS.TAB_UPDATED);
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.getElementById("details-pane-toggle"));
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.querySelectorAll("#details-pane tab")[2]);
+ yield onEvent;
+ yield testParamsTab("urlencoded");
+
+ onEvent = monitor.panelWin.once(EVENTS.TAB_UPDATED);
+ RequestsMenu.selectedIndex = 1;
+ yield onEvent;
+ yield testParamsTab("multipart");
+
+ return teardown(monitor);
+
+ function* testParamsTab(type) {
+ let tabEl = document.querySelectorAll("#details-pane tab")[2];
+ let tabpanel = document.querySelectorAll("#details-pane tabpanel")[2];
+
+ is(tabEl.getAttribute("selected"), "true",
+ "The params tab in the network details pane should be selected.");
+
+ function checkVisibility(box) {
+ is(tabpanel.querySelector("#request-params-box")
+ .hasAttribute("hidden"), !box.includes("params"),
+ "The request params box doesn't have the indended visibility.");
+ is(tabpanel.querySelector("#request-post-data-textarea-box")
+ .hasAttribute("hidden"), !box.includes("textarea"),
+ "The request post data textarea box doesn't have the indended visibility.");
+ }
+
+ is(tabpanel.querySelectorAll(".variables-view-scope").length, 2,
+ "There should be 2 param scopes displayed in this tabpanel.");
+ is(tabpanel.querySelectorAll(".variables-view-empty-notice").length, 0,
+ "The empty notice should not be displayed in this tabpanel.");
+
+ let queryScope = tabpanel.querySelectorAll(".variables-view-scope")[0];
+ let postScope = tabpanel.querySelectorAll(".variables-view-scope")[1];
+
+ is(queryScope.querySelector(".name").getAttribute("value"),
+ L10N.getStr("paramsQueryString"),
+ "The query scope doesn't have the correct title.");
+
+ is(postScope.querySelector(".name").getAttribute("value"),
+ L10N.getStr(type == "urlencoded" ? "paramsFormData" : "paramsPostPayload"),
+ "The post scope doesn't have the correct title.");
+
+ is(queryScope.querySelectorAll(".variables-view-variable .name")[0]
+ .getAttribute("value"),
+ "foo", "The first query param name was incorrect.");
+ is(queryScope.querySelectorAll(".variables-view-variable .value")[0]
+ .getAttribute("value"),
+ "\"bar\"", "The first query param value was incorrect.");
+ is(queryScope.querySelectorAll(".variables-view-variable .name")[1]
+ .getAttribute("value"),
+ "baz", "The second query param name was incorrect.");
+ is(queryScope.querySelectorAll(".variables-view-variable .value")[1]
+ .getAttribute("value"),
+ "\"42\"", "The second query param value was incorrect.");
+ is(queryScope.querySelectorAll(".variables-view-variable .name")[2]
+ .getAttribute("value"),
+ "type", "The third query param name was incorrect.");
+ is(queryScope.querySelectorAll(".variables-view-variable .value")[2]
+ .getAttribute("value"),
+ "\"" + type + "\"", "The third query param value was incorrect.");
+
+ if (type == "urlencoded") {
+ checkVisibility("params");
+
+ is(tabpanel.querySelectorAll(".variables-view-variable").length, 5,
+ "There should be 5 param values displayed in this tabpanel.");
+ is(queryScope.querySelectorAll(".variables-view-variable").length, 3,
+ "There should be 3 param values displayed in the query scope.");
+ is(postScope.querySelectorAll(".variables-view-variable").length, 2,
+ "There should be 2 param values displayed in the post scope.");
+
+ is(postScope.querySelectorAll(".variables-view-variable .name")[0]
+ .getAttribute("value"),
+ "foo", "The first post param name was incorrect.");
+ is(postScope.querySelectorAll(".variables-view-variable .value")[0]
+ .getAttribute("value"),
+ "\"bar\"", "The first post param value was incorrect.");
+ is(postScope.querySelectorAll(".variables-view-variable .name")[1]
+ .getAttribute("value"),
+ "baz", "The second post param name was incorrect.");
+ is(postScope.querySelectorAll(".variables-view-variable .value")[1]
+ .getAttribute("value"),
+ "\"123\"", "The second post param value was incorrect.");
+ } else {
+ checkVisibility("params textarea");
+
+ is(tabpanel.querySelectorAll(".variables-view-variable").length, 3,
+ "There should be 3 param values displayed in this tabpanel.");
+ is(queryScope.querySelectorAll(".variables-view-variable").length, 3,
+ "There should be 3 param values displayed in the query scope.");
+ is(postScope.querySelectorAll(".variables-view-variable").length, 0,
+ "There should be 0 param values displayed in the post scope.");
+
+ let editor = yield NetMonitorView.editor("#request-post-data-textarea");
+ let text = editor.getText();
+
+ ok(text.includes("Content-Disposition: form-data; name=\"text\""),
+ "The text shown in the source editor is incorrect (1.1).");
+ ok(text.includes("Content-Disposition: form-data; name=\"email\""),
+ "The text shown in the source editor is incorrect (2.1).");
+ ok(text.includes("Content-Disposition: form-data; name=\"range\""),
+ "The text shown in the source editor is incorrect (3.1).");
+ ok(text.includes("Content-Disposition: form-data; name=\"Custom field\""),
+ "The text shown in the source editor is incorrect (4.1).");
+ ok(text.includes("Some text..."),
+ "The text shown in the source editor is incorrect (2.2).");
+ ok(text.includes("42"),
+ "The text shown in the source editor is incorrect (3.2).");
+ ok(text.includes("Extra data"),
+ "The text shown in the source editor is incorrect (4.2).");
+ is(editor.getMode(), Editor.modes.text,
+ "The mode active in the source editor is incorrect.");
+ }
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_post-data-02.js b/devtools/client/netmonitor/test/browser_net_post-data-02.js
new file mode 100644
index 000000000..3cdd2f14a
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_post-data-02.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if the POST requests display the correct information in the UI,
+ * for raw payloads with attached content-type headers.
+ */
+
+add_task(function* () {
+ let { L10N } = require("devtools/client/netmonitor/l10n");
+
+ let { tab, monitor } = yield initNetMonitor(POST_RAW_URL);
+ info("Starting test... ");
+
+ let { document, EVENTS, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu, NetworkDetails } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+ NetworkDetails._params.lazyEmpty = false;
+
+ let wait = waitForNetworkEvents(monitor, 0, 1);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests();
+ });
+ yield wait;
+
+ let onEvent = monitor.panelWin.once(EVENTS.TAB_UPDATED);
+ NetMonitorView.toggleDetailsPane({ visible: true }, 2);
+ RequestsMenu.selectedIndex = 0;
+ yield onEvent;
+
+ let tabEl = document.querySelectorAll("#event-details-pane tab")[2];
+ let tabpanel = document.querySelectorAll("#event-details-pane tabpanel")[2];
+
+ is(tabEl.getAttribute("selected"), "true",
+ "The params tab in the network details pane should be selected.");
+
+ is(tabpanel.querySelector("#request-params-box")
+ .hasAttribute("hidden"), false,
+ "The request params box doesn't have the indended visibility.");
+ is(tabpanel.querySelector("#request-post-data-textarea-box")
+ .hasAttribute("hidden"), true,
+ "The request post data textarea box doesn't have the indended visibility.");
+
+ is(tabpanel.querySelectorAll(".variables-view-scope").length, 1,
+ "There should be 1 param scopes displayed in this tabpanel.");
+ is(tabpanel.querySelectorAll(".variables-view-empty-notice").length, 0,
+ "The empty notice should not be displayed in this tabpanel.");
+
+ let postScope = tabpanel.querySelectorAll(".variables-view-scope")[0];
+ is(postScope.querySelector(".name").getAttribute("value"),
+ L10N.getStr("paramsFormData"),
+ "The post scope doesn't have the correct title.");
+
+ is(postScope.querySelectorAll(".variables-view-variable").length, 2,
+ "There should be 2 param values displayed in the post scope.");
+ is(postScope.querySelectorAll(".variables-view-variable .name")[0]
+ .getAttribute("value"),
+ "foo", "The first query param name was incorrect.");
+ is(postScope.querySelectorAll(".variables-view-variable .value")[0]
+ .getAttribute("value"),
+ "\"bar\"", "The first query param value was incorrect.");
+ is(postScope.querySelectorAll(".variables-view-variable .name")[1]
+ .getAttribute("value"),
+ "baz", "The second query param name was incorrect.");
+ is(postScope.querySelectorAll(".variables-view-variable .value")[1]
+ .getAttribute("value"),
+ "\"123\"", "The second query param value was incorrect.");
+
+ return teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_post-data-03.js b/devtools/client/netmonitor/test/browser_net_post-data-03.js
new file mode 100644
index 000000000..3433f89ce
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_post-data-03.js
@@ -0,0 +1,98 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if the POST requests display the correct information in the UI,
+ * for raw payloads with content-type headers attached to the upload stream.
+ */
+
+add_task(function* () {
+ let { L10N } = require("devtools/client/netmonitor/l10n");
+
+ let { tab, monitor } = yield initNetMonitor(POST_RAW_WITH_HEADERS_URL);
+ info("Starting test... ");
+
+ let { document, EVENTS, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, 0, 1);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests();
+ });
+ yield wait;
+
+ let onEvent = monitor.panelWin.once(EVENTS.TAB_UPDATED);
+ NetMonitorView.toggleDetailsPane({ visible: true });
+ RequestsMenu.selectedIndex = 0;
+ yield onEvent;
+
+ let tabEl = document.querySelectorAll("#details-pane tab")[0];
+ let tabpanel = document.querySelectorAll("#details-pane tabpanel")[0];
+ let requestFromUploadScope = tabpanel.querySelectorAll(".variables-view-scope")[2];
+
+ is(tabEl.getAttribute("selected"), "true",
+ "The headers tab in the network details pane should be selected.");
+ is(tabpanel.querySelectorAll(".variables-view-scope").length, 3,
+ "There should be 3 header scopes displayed in this tabpanel.");
+
+ is(requestFromUploadScope.querySelector(".name").getAttribute("value"),
+ L10N.getStr("requestHeadersFromUpload") + " (" +
+ L10N.getFormatStr("networkMenu.sizeKB", L10N.numberWithDecimals(74 / 1024, 3)) + ")",
+ "The request headers from upload scope doesn't have the correct title.");
+
+ is(requestFromUploadScope.querySelectorAll(".variables-view-variable").length, 2,
+ "There should be 2 headers displayed in the request headers from upload scope.");
+
+ is(requestFromUploadScope.querySelectorAll(".variables-view-variable .name")[0]
+ .getAttribute("value"),
+ "content-type", "The first request header name was incorrect.");
+ is(requestFromUploadScope.querySelectorAll(".variables-view-variable .value")[0]
+ .getAttribute("value"), "\"application/x-www-form-urlencoded\"",
+ "The first request header value was incorrect.");
+ is(requestFromUploadScope.querySelectorAll(".variables-view-variable .name")[1]
+ .getAttribute("value"),
+ "custom-header", "The second request header name was incorrect.");
+ is(requestFromUploadScope.querySelectorAll(".variables-view-variable .value")[1]
+ .getAttribute("value"),
+ "\"hello world!\"", "The second request header value was incorrect.");
+
+ onEvent = monitor.panelWin.once(EVENTS.TAB_UPDATED);
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.querySelectorAll("#details-pane tab")[2]);
+ yield onEvent;
+
+ tabEl = document.querySelectorAll("#details-pane tab")[2];
+ tabpanel = document.querySelectorAll("#details-pane tabpanel")[2];
+ let formDataScope = tabpanel.querySelectorAll(".variables-view-scope")[0];
+
+ is(tab.getAttribute("selected"), "true",
+ "The response tab in the network details pane should be selected.");
+ is(tabpanel.querySelectorAll(".variables-view-scope").length, 1,
+ "There should be 1 header scope displayed in this tabpanel.");
+
+ is(formDataScope.querySelector(".name").getAttribute("value"),
+ L10N.getStr("paramsFormData"),
+ "The form data scope doesn't have the correct title.");
+
+ is(formDataScope.querySelectorAll(".variables-view-variable").length, 2,
+ "There should be 2 payload values displayed in the form data scope.");
+
+ is(formDataScope.querySelectorAll(".variables-view-variable .name")[0]
+ .getAttribute("value"),
+ "foo", "The first payload param name was incorrect.");
+ is(formDataScope.querySelectorAll(".variables-view-variable .value")[0]
+ .getAttribute("value"),
+ "\"bar\"", "The first payload param value was incorrect.");
+ is(formDataScope.querySelectorAll(".variables-view-variable .name")[1]
+ .getAttribute("value"),
+ "baz", "The second payload param name was incorrect.");
+ is(formDataScope.querySelectorAll(".variables-view-variable .value")[1]
+ .getAttribute("value"),
+ "\"123\"", "The second payload param value was incorrect.");
+
+ return teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_post-data-04.js b/devtools/client/netmonitor/test/browser_net_post-data-04.js
new file mode 100644
index 000000000..565792287
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_post-data-04.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if the POST requests display the correct information in the UI,
+ * for JSON payloads.
+ */
+
+add_task(function* () {
+ let { L10N } = require("devtools/client/netmonitor/l10n");
+
+ let { tab, monitor } = yield initNetMonitor(POST_JSON_URL);
+ info("Starting test... ");
+
+ let { document, EVENTS, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu, NetworkDetails } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+ NetworkDetails._params.lazyEmpty = false;
+
+ let wait = waitForNetworkEvents(monitor, 0, 1);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests();
+ });
+ yield wait;
+
+ let onEvent = monitor.panelWin.once(EVENTS.TAB_UPDATED);
+ NetMonitorView.toggleDetailsPane({ visible: true }, 2);
+ RequestsMenu.selectedIndex = 0;
+ yield onEvent;
+
+ let tabEl = document.querySelectorAll("#event-details-pane tab")[2];
+ let tabpanel = document.querySelectorAll("#event-details-pane tabpanel")[2];
+
+ is(tabEl.getAttribute("selected"), "true",
+ "The params tab in the network details pane should be selected.");
+
+ is(tabpanel.querySelector("#request-params-box")
+ .hasAttribute("hidden"), false,
+ "The request params box doesn't have the intended visibility.");
+ is(tabpanel.querySelector("#request-post-data-textarea-box")
+ .hasAttribute("hidden"), true,
+ "The request post data textarea box doesn't have the intended visibility.");
+
+ is(tabpanel.querySelectorAll(".variables-view-scope").length, 1,
+ "There should be 1 param scopes displayed in this tabpanel.");
+ is(tabpanel.querySelectorAll(".variables-view-empty-notice").length, 0,
+ "The empty notice should not be displayed in this tabpanel.");
+
+ let jsonScope = tabpanel.querySelectorAll(".variables-view-scope")[0];
+ is(jsonScope.querySelector(".name").getAttribute("value"),
+ L10N.getStr("jsonScopeName"),
+ "The JSON scope doesn't have the correct title.");
+
+ let valueScope = tabpanel.querySelector(
+ ".variables-view-scope > .variables-view-element-details");
+
+ is(valueScope.querySelectorAll(".variables-view-variable").length, 1,
+ "There should be 1 value displayed in the JSON scope.");
+ is(valueScope.querySelector(".variables-view-property .name")
+ .getAttribute("value"),
+ "a", "The JSON var name was incorrect.");
+ is(valueScope.querySelector(".variables-view-property .value")
+ .getAttribute("value"),
+ "1", "The JSON var value was incorrect.");
+
+ let detailsParent = valueScope.querySelector(".variables-view-property .name")
+ .closest(".variables-view-element-details");
+ is(detailsParent.hasAttribute("open"), true, "The JSON value must be visible");
+
+ return teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_prefs-and-l10n.js b/devtools/client/netmonitor/test/browser_net_prefs-and-l10n.js
new file mode 100644
index 000000000..e73f94d6d
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_prefs-and-l10n.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if the preferences and localization objects work correctly.
+ */
+
+add_task(function* () {
+ let { L10N } = require("devtools/client/netmonitor/l10n");
+
+ let { monitor } = yield initNetMonitor(SIMPLE_URL);
+ info("Starting test... ");
+
+ ok(monitor.panelWin.Prefs,
+ "Should have a preferences object available on the panel window.");
+
+ testL10N();
+ testPrefs();
+
+ return teardown(monitor);
+
+ function testL10N() {
+ is(typeof L10N.getStr("netmonitor.security.enabled"), "string",
+ "The getStr() method didn't return a valid string.");
+ is(typeof L10N.getFormatStr("networkMenu.totalMS", "foo"), "string",
+ "The getFormatStr() method didn't return a valid string.");
+ }
+
+ function testPrefs() {
+ let { Prefs } = monitor.panelWin;
+
+ is(Prefs.networkDetailsWidth,
+ Services.prefs.getIntPref("devtools.netmonitor.panes-network-details-width"),
+ "Getting a pref should work correctly.");
+
+ let previousValue = Prefs.networkDetailsWidth;
+ let bogusValue = ~~(Math.random() * 100);
+ Prefs.networkDetailsWidth = bogusValue;
+ is(Prefs.networkDetailsWidth,
+ Services.prefs.getIntPref("devtools.netmonitor.panes-network-details-width"),
+ "Getting a pref after it has been modified should work correctly.");
+ is(Prefs.networkDetailsWidth, bogusValue,
+ "The pref wasn't updated correctly in the preferences object.");
+
+ Prefs.networkDetailsWidth = previousValue;
+ is(Prefs.networkDetailsWidth,
+ Services.prefs.getIntPref("devtools.netmonitor.panes-network-details-width"),
+ "Getting a pref after it has been modified again should work correctly.");
+ is(Prefs.networkDetailsWidth, previousValue,
+ "The pref wasn't updated correctly again in the preferences object.");
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_prefs-reload.js b/devtools/client/netmonitor/test/browser_net_prefs-reload.js
new file mode 100644
index 000000000..ee56ee446
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_prefs-reload.js
@@ -0,0 +1,215 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if the prefs that should survive across tool reloads work.
+ */
+
+add_task(function* () {
+ let Actions = require("devtools/client/netmonitor/actions/index");
+ let { monitor } = yield initNetMonitor(SIMPLE_URL);
+ info("Starting test... ");
+
+ // This test reopens the network monitor a bunch of times, for different
+ // hosts (bottom, side, window). This seems to be slow on debug builds.
+ requestLongerTimeout(3);
+
+ // Use these getters instead of caching instances inside the panel win,
+ // since the tool is reopened a bunch of times during this test
+ // and the instances will differ.
+ let getView = () => monitor.panelWin.NetMonitorView;
+ let getStore = () => monitor.panelWin.gStore;
+
+ let prefsToCheck = {
+ filters: {
+ // A custom new value to be used for the verified preference.
+ newValue: ["html", "css"],
+ // Getter used to retrieve the current value from the frontend, in order
+ // to verify that the pref was applied properly.
+ validateValue: ($) => getView().RequestsMenu._activeFilters,
+ // Predicate used to modify the frontend when setting the new pref value,
+ // before trying to validate the changes.
+ modifyFrontend: ($, value) => value.forEach(e =>
+ getStore().dispatch(Actions.toggleFilterType(e)))
+ },
+ networkDetailsWidth: {
+ newValue: ~~(Math.random() * 200 + 100),
+ validateValue: ($) => ~~$("#details-pane").getAttribute("width"),
+ modifyFrontend: ($, value) => $("#details-pane").setAttribute("width", value)
+ },
+ networkDetailsHeight: {
+ newValue: ~~(Math.random() * 300 + 100),
+ validateValue: ($) => ~~$("#details-pane").getAttribute("height"),
+ modifyFrontend: ($, value) => $("#details-pane").setAttribute("height", value)
+ }
+ /* add more prefs here... */
+ };
+
+ yield testBottom();
+ yield testSide();
+ yield testWindow();
+
+ info("Moving toolbox back to the bottom...");
+ yield monitor._toolbox.switchHost(Toolbox.HostType.BOTTOM);
+ return teardown(monitor);
+
+ function storeFirstPrefValues() {
+ info("Caching initial pref values.");
+
+ for (let name in prefsToCheck) {
+ let currentValue = monitor.panelWin.Prefs[name];
+ prefsToCheck[name].firstValue = currentValue;
+ }
+ }
+
+ function validateFirstPrefValues() {
+ info("Validating current pref values to the UI elements.");
+
+ for (let name in prefsToCheck) {
+ let currentValue = monitor.panelWin.Prefs[name];
+ let firstValue = prefsToCheck[name].firstValue;
+ let validateValue = prefsToCheck[name].validateValue;
+
+ is(currentValue.toSource(), firstValue.toSource(),
+ "Pref " + name + " should be equal to first value: " + firstValue);
+ is(currentValue.toSource(), validateValue(monitor.panelWin.$).toSource(),
+ "Pref " + name + " should validate: " + currentValue);
+ }
+ }
+
+ function modifyFrontend() {
+ info("Modifying UI elements to the specified new values.");
+
+ for (let name in prefsToCheck) {
+ let currentValue = monitor.panelWin.Prefs[name];
+ let firstValue = prefsToCheck[name].firstValue;
+ let newValue = prefsToCheck[name].newValue;
+ let validateValue = prefsToCheck[name].validateValue;
+ let modFrontend = prefsToCheck[name].modifyFrontend;
+
+ modFrontend(monitor.panelWin.$, newValue);
+ info("Modified UI element affecting " + name + " to: " + newValue);
+
+ is(currentValue.toSource(), firstValue.toSource(),
+ "Pref " + name + " should still be equal to first value: " + firstValue);
+ isnot(currentValue.toSource(), newValue.toSource(),
+ "Pref " + name + " should't yet be equal to second value: " + newValue);
+ is(newValue.toSource(), validateValue(monitor.panelWin.$).toSource(),
+ "The UI element affecting " + name + " should validate: " + newValue);
+ }
+ }
+
+ function validateNewPrefValues() {
+ info("Invalidating old pref values to the modified UI elements.");
+
+ for (let name in prefsToCheck) {
+ let currentValue = monitor.panelWin.Prefs[name];
+ let firstValue = prefsToCheck[name].firstValue;
+ let newValue = prefsToCheck[name].newValue;
+ let validateValue = prefsToCheck[name].validateValue;
+
+ isnot(currentValue.toSource(), firstValue.toSource(),
+ "Pref " + name + " should't be equal to first value: " + firstValue);
+ is(currentValue.toSource(), newValue.toSource(),
+ "Pref " + name + " should now be equal to second value: " + newValue);
+ is(newValue.toSource(), validateValue(monitor.panelWin.$).toSource(),
+ "The UI element affecting " + name + " should validate: " + newValue);
+ }
+ }
+
+ function resetFrontend() {
+ info("Resetting UI elements to the cached initial pref values.");
+
+ for (let name in prefsToCheck) {
+ let currentValue = monitor.panelWin.Prefs[name];
+ let firstValue = prefsToCheck[name].firstValue;
+ let newValue = prefsToCheck[name].newValue;
+ let validateValue = prefsToCheck[name].validateValue;
+ let modFrontend = prefsToCheck[name].modifyFrontend;
+
+ modFrontend(monitor.panelWin.$, firstValue);
+ info("Modified UI element affecting " + name + " to: " + firstValue);
+
+ isnot(currentValue.toSource(), firstValue.toSource(),
+ "Pref " + name + " should't yet be equal to first value: " + firstValue);
+ is(currentValue.toSource(), newValue.toSource(),
+ "Pref " + name + " should still be equal to second value: " + newValue);
+ is(firstValue.toSource(), validateValue(monitor.panelWin.$).toSource(),
+ "The UI element affecting " + name + " should validate: " + firstValue);
+ }
+ }
+
+ function* testBottom() {
+ info("Testing prefs reload for a bottom host.");
+ storeFirstPrefValues();
+
+ // Validate and modify while toolbox is on the bottom.
+ validateFirstPrefValues();
+ modifyFrontend();
+
+ let newMonitor = yield restartNetMonitor(monitor);
+ monitor = newMonitor.monitor;
+
+ // Revalidate and reset frontend while toolbox is on the bottom.
+ validateNewPrefValues();
+ resetFrontend();
+
+ newMonitor = yield restartNetMonitor(monitor);
+ monitor = newMonitor.monitor;
+
+ // Revalidate.
+ validateFirstPrefValues();
+ }
+
+ function* testSide() {
+ info("Moving toolbox to the side...");
+
+ yield monitor._toolbox.switchHost(Toolbox.HostType.SIDE);
+ info("Testing prefs reload for a side host.");
+ storeFirstPrefValues();
+
+ // Validate and modify frontend while toolbox is on the side.
+ validateFirstPrefValues();
+ modifyFrontend();
+
+ let newMonitor = yield restartNetMonitor(monitor);
+ monitor = newMonitor.monitor;
+
+ // Revalidate and reset frontend while toolbox is on the side.
+ validateNewPrefValues();
+ resetFrontend();
+
+ newMonitor = yield restartNetMonitor(monitor);
+ monitor = newMonitor.monitor;
+
+ // Revalidate.
+ validateFirstPrefValues();
+ }
+
+ function* testWindow() {
+ info("Moving toolbox into a window...");
+
+ yield monitor._toolbox.switchHost(Toolbox.HostType.WINDOW);
+ info("Testing prefs reload for a window host.");
+ storeFirstPrefValues();
+
+ // Validate and modify frontend while toolbox is in a window.
+ validateFirstPrefValues();
+ modifyFrontend();
+
+ let newMonitor = yield restartNetMonitor(monitor);
+ monitor = newMonitor.monitor;
+
+ // Revalidate and reset frontend while toolbox is in a window.
+ validateNewPrefValues();
+ resetFrontend();
+
+ newMonitor = yield restartNetMonitor(monitor);
+ monitor = newMonitor.monitor;
+
+ // Revalidate.
+ validateFirstPrefValues();
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_raw_headers.js b/devtools/client/netmonitor/test/browser_net_raw_headers.js
new file mode 100644
index 000000000..2cb734745
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_raw_headers.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if showing raw headers works.
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(POST_DATA_URL);
+ info("Starting test... ");
+
+ let { document, EVENTS, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, 0, 2);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests();
+ });
+ yield wait;
+
+ let origItem = RequestsMenu.getItemAtIndex(0);
+
+ let onTabEvent = monitor.panelWin.once(EVENTS.TAB_UPDATED);
+ RequestsMenu.selectedItem = origItem;
+ yield onTabEvent;
+
+ EventUtils.sendMouseEvent({ type: "click" },
+ document.getElementById("toggle-raw-headers"));
+
+ testShowRawHeaders(origItem.attachment);
+
+ EventUtils.sendMouseEvent({ type: "click" },
+ document.getElementById("toggle-raw-headers"));
+
+ testHideRawHeaders(document);
+
+ return teardown(monitor);
+
+ /*
+ * Tests that raw headers were displayed correctly
+ */
+ function testShowRawHeaders(data) {
+ let requestHeaders = document.getElementById("raw-request-headers-textarea").value;
+ for (let header of data.requestHeaders.headers) {
+ ok(requestHeaders.indexOf(header.name + ": " + header.value) >= 0,
+ "textarea contains request headers");
+ }
+ let responseHeaders = document.getElementById("raw-response-headers-textarea").value;
+ for (let header of data.responseHeaders.headers) {
+ ok(responseHeaders.indexOf(header.name + ": " + header.value) >= 0,
+ "textarea contains response headers");
+ }
+ }
+
+ /*
+ * Tests that raw headers textareas are hidden and empty
+ */
+ function testHideRawHeaders() {
+ let rawHeadersHidden = document.getElementById("raw-headers").getAttribute("hidden");
+ let requestTextarea = document.getElementById("raw-request-headers-textarea");
+ let responseTextarea = document.getElementById("raw-response-headers-textarea");
+ ok(rawHeadersHidden, "raw headers textareas are hidden");
+ ok(requestTextarea.value == "", "raw request headers textarea is empty");
+ ok(responseTextarea.value == "", "raw response headers textarea is empty");
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_reload-button.js b/devtools/client/netmonitor/test/browser_net_reload-button.js
new file mode 100644
index 000000000..e91de8302
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_reload-button.js
@@ -0,0 +1,25 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if the empty-requests reload button works.
+ */
+
+add_task(function* () {
+ let { monitor } = yield initNetMonitor(SINGLE_GET_URL);
+ info("Starting test... ");
+
+ let { document, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ let wait = waitForNetworkEvents(monitor, 2);
+ let button = document.querySelector("#requests-menu-reload-notice-button");
+ button.click();
+ yield wait;
+
+ is(RequestsMenu.itemCount, 2, "The request menu should have two items after reloading");
+
+ return teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_reload-markers.js b/devtools/client/netmonitor/test/browser_net_reload-markers.js
new file mode 100644
index 000000000..26866830f
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_reload-markers.js
@@ -0,0 +1,35 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if the empty-requests reload button works.
+ */
+
+add_task(function* () {
+ let { monitor } = yield initNetMonitor(SINGLE_GET_URL);
+ info("Starting test... ");
+
+ let { document, EVENTS } = monitor.panelWin;
+ let button = document.querySelector("#requests-menu-reload-notice-button");
+ button.click();
+
+ let markers = [];
+
+ monitor.panelWin.on(EVENTS.TIMELINE_EVENT, (_, marker) => {
+ markers.push(marker);
+ });
+
+ yield waitForNetworkEvents(monitor, 2);
+ yield waitUntil(() => markers.length == 2);
+
+ ok(true, "Reloading finished");
+
+ is(markers[0].name, "document::DOMContentLoaded",
+ "The first received marker is correct.");
+ is(markers[1].name, "document::Load",
+ "The second received marker is correct.");
+
+ return teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_req-resp-bodies.js b/devtools/client/netmonitor/test/browser_net_req-resp-bodies.js
new file mode 100644
index 000000000..71a913501
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_req-resp-bodies.js
@@ -0,0 +1,68 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test if request and response body logging stays on after opening the console.
+ */
+
+add_task(function* () {
+ let { L10N } = require("devtools/client/netmonitor/l10n");
+
+ let { tab, monitor } = yield initNetMonitor(JSON_LONG_URL);
+ info("Starting test... ");
+
+ let { NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ // Perform first batch of requests.
+ let wait = waitForNetworkEvents(monitor, 1);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests();
+ });
+ yield wait;
+
+ verifyRequest(0);
+
+ // Switch to the webconsole.
+ let onWebConsole = monitor._toolbox.once("webconsole-selected");
+ monitor._toolbox.selectTool("webconsole");
+ yield onWebConsole;
+
+ // Switch back to the netmonitor.
+ let onNetMonitor = monitor._toolbox.once("netmonitor-selected");
+ monitor._toolbox.selectTool("netmonitor");
+ yield onNetMonitor;
+
+ // Reload debugee.
+ wait = waitForNetworkEvents(monitor, 1);
+ tab.linkedBrowser.reload();
+ yield wait;
+
+ // Perform another batch of requests.
+ wait = waitForNetworkEvents(monitor, 1);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests();
+ });
+ yield wait;
+
+ verifyRequest(1);
+
+ return teardown(monitor);
+
+ function verifyRequest(offset) {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(offset),
+ "GET", CONTENT_TYPE_SJS + "?fmt=json-long", {
+ status: 200,
+ statusText: "OK",
+ type: "json",
+ fullMimeType: "text/json; charset=utf-8",
+ size: L10N.getFormatStr("networkMenu.sizeKB",
+ L10N.numberWithDecimals(85975 / 1024, 2)),
+ time: true
+ });
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_resend.js b/devtools/client/netmonitor/test/browser_net_resend.js
new file mode 100644
index 000000000..7b540ec50
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_resend.js
@@ -0,0 +1,174 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if resending a request works.
+ */
+
+const ADD_QUERY = "t1=t2";
+const ADD_HEADER = "Test-header: true";
+const ADD_UA_HEADER = "User-Agent: Custom-Agent";
+const ADD_POSTDATA = "&t3=t4";
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(POST_DATA_URL);
+ info("Starting test... ");
+
+ let { panelWin } = monitor;
+ let { document, EVENTS, NetMonitorView } = panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, 0, 2);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests();
+ });
+ yield wait;
+
+ let origItem = RequestsMenu.getItemAtIndex(0);
+
+ let onTabUpdated = panelWin.once(EVENTS.TAB_UPDATED);
+ RequestsMenu.selectedItem = origItem;
+ yield onTabUpdated;
+
+ // add a new custom request cloned from selected request
+ let onPopulated = panelWin.once(EVENTS.CUSTOMREQUESTVIEW_POPULATED);
+ RequestsMenu.cloneSelectedRequest();
+ yield onPopulated;
+
+ testCustomForm(origItem.attachment);
+
+ let customItem = RequestsMenu.selectedItem;
+ testCustomItem(customItem, origItem);
+
+ // edit the custom request
+ yield editCustomForm();
+ testCustomItemChanged(customItem, origItem);
+
+ // send the new request
+ wait = waitForNetworkEvents(monitor, 0, 1);
+ RequestsMenu.sendCustomRequest();
+ yield wait;
+
+ let sentItem = RequestsMenu.selectedItem;
+ testSentRequest(sentItem.attachment, origItem.attachment);
+
+ return teardown(monitor);
+
+ function testCustomItem(item, orig) {
+ let method = item.target.querySelector(".requests-menu-method").value;
+ let origMethod = orig.target.querySelector(".requests-menu-method").value;
+ is(method, origMethod, "menu item is showing the same method as original request");
+
+ let file = item.target.querySelector(".requests-menu-file").value;
+ let origFile = orig.target.querySelector(".requests-menu-file").value;
+ is(file, origFile, "menu item is showing the same file name as original request");
+
+ let domain = item.target.querySelector(".requests-menu-domain").value;
+ let origDomain = orig.target.querySelector(".requests-menu-domain").value;
+ is(domain, origDomain, "menu item is showing the same domain as original request");
+ }
+
+ function testCustomItemChanged(item, orig) {
+ let file = item.target.querySelector(".requests-menu-file").value;
+ let expectedFile = orig.target.querySelector(".requests-menu-file").value +
+ "&" + ADD_QUERY;
+
+ is(file, expectedFile, "menu item is updated to reflect url entered in form");
+ }
+
+ /*
+ * Test that the New Request form was populated correctly
+ */
+ function testCustomForm(data) {
+ is(document.getElementById("custom-method-value").value, data.method,
+ "new request form showing correct method");
+
+ is(document.getElementById("custom-url-value").value, data.url,
+ "new request form showing correct url");
+
+ let query = document.getElementById("custom-query-value");
+ is(query.value, "foo=bar\nbaz=42\ntype=urlencoded",
+ "new request form showing correct query string");
+
+ let headers = document.getElementById("custom-headers-value").value.split("\n");
+ for (let {name, value} of data.requestHeaders.headers) {
+ ok(headers.indexOf(name + ": " + value) >= 0, "form contains header from request");
+ }
+
+ let postData = document.getElementById("custom-postdata-value");
+ is(postData.value, data.requestPostData.postData.text,
+ "new request form showing correct post data");
+ }
+
+ /*
+ * Add some params and headers to the request form
+ */
+ function* editCustomForm() {
+ panelWin.focus();
+
+ let query = document.getElementById("custom-query-value");
+ let queryFocus = once(query, "focus", false);
+ // Bug 1195825: Due to some unexplained dark-matter with promise,
+ // focus only works if delayed by one tick.
+ query.setSelectionRange(query.value.length, query.value.length);
+ executeSoon(() => query.focus());
+ yield queryFocus;
+
+ // add params to url query string field
+ type(["VK_RETURN"]);
+ type(ADD_QUERY);
+
+ let headers = document.getElementById("custom-headers-value");
+ let headersFocus = once(headers, "focus", false);
+ headers.setSelectionRange(headers.value.length, headers.value.length);
+ headers.focus();
+ yield headersFocus;
+
+ // add a header
+ type(["VK_RETURN"]);
+ type(ADD_HEADER);
+
+ // add a User-Agent header, to check if default headers can be modified
+ // (there will be two of them, first gets overwritten by the second)
+ type(["VK_RETURN"]);
+ type(ADD_UA_HEADER);
+
+ let postData = document.getElementById("custom-postdata-value");
+ let postFocus = once(postData, "focus", false);
+ postData.setSelectionRange(postData.value.length, postData.value.length);
+ postData.focus();
+ yield postFocus;
+
+ // add to POST data
+ type(ADD_POSTDATA);
+ }
+
+ /*
+ * Make sure newly created event matches expected request
+ */
+ function testSentRequest(data, origData) {
+ is(data.method, origData.method, "correct method in sent request");
+ is(data.url, origData.url + "&" + ADD_QUERY, "correct url in sent request");
+
+ let { headers } = data.requestHeaders;
+ let hasHeader = headers.some(h => `${h.name}: ${h.value}` == ADD_HEADER);
+ ok(hasHeader, "new header added to sent request");
+
+ let hasUAHeader = headers.some(h => `${h.name}: ${h.value}` == ADD_UA_HEADER);
+ ok(hasUAHeader, "User-Agent header added to sent request");
+
+ is(data.requestPostData.postData.text,
+ origData.requestPostData.postData.text + ADD_POSTDATA,
+ "post data added to sent request");
+ }
+
+ function type(string) {
+ for (let ch of string) {
+ EventUtils.synthesizeKey(ch, {}, panelWin);
+ }
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_resend_cors.js b/devtools/client/netmonitor/test/browser_net_resend_cors.js
new file mode 100644
index 000000000..d63c3b54e
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_resend_cors.js
@@ -0,0 +1,80 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if resending a CORS request avoids the security checks and doesn't send
+ * a preflight OPTIONS request (bug 1270096 and friends)
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(CORS_URL);
+ info("Starting test... ");
+
+ let { EVENTS, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ let requestUrl = "http://test1.example.com" + CORS_SJS_PATH;
+
+ info("Waiting for OPTIONS, then POST");
+ let wait = waitForNetworkEvents(monitor, 1, 1);
+ yield ContentTask.spawn(tab.linkedBrowser, requestUrl, function* (url) {
+ content.wrappedJSObject.performRequests(url, "triggering/preflight", "post-data");
+ });
+ yield wait;
+
+ const METHODS = ["OPTIONS", "POST"];
+
+ // Check the requests that were sent
+ for (let [i, method] of METHODS.entries()) {
+ let { attachment } = RequestsMenu.getItemAtIndex(i);
+ is(attachment.method, method, `The ${method} request has the right method`);
+ is(attachment.url, requestUrl, `The ${method} request has the right URL`);
+ }
+
+ // Resend both requests without modification. Wait for resent OPTIONS, then POST.
+ // POST is supposed to have no preflight OPTIONS request this time (CORS is disabled)
+ let onRequests = waitForNetworkEvents(monitor, 1, 1);
+ for (let [i, method] of METHODS.entries()) {
+ let item = RequestsMenu.getItemAtIndex(i);
+
+ info(`Selecting the ${method} request (at index ${i})`);
+ let onUpdate = monitor.panelWin.once(EVENTS.TAB_UPDATED);
+ RequestsMenu.selectedItem = item;
+ yield onUpdate;
+
+ info("Cloning the selected request into a custom clone");
+ let onPopulate = monitor.panelWin.once(EVENTS.CUSTOMREQUESTVIEW_POPULATED);
+ RequestsMenu.cloneSelectedRequest();
+ yield onPopulate;
+
+ info("Sending the cloned request (without change)");
+ RequestsMenu.sendCustomRequest();
+ }
+
+ info("Waiting for both resent requests");
+ yield onRequests;
+
+ // Check the resent requests
+ for (let [i, method] of METHODS.entries()) {
+ let index = i + 2;
+ let item = RequestsMenu.getItemAtIndex(index).attachment;
+ is(item.method, method, `The ${method} request has the right method`);
+ is(item.url, requestUrl, `The ${method} request has the right URL`);
+ is(item.status, 200, `The ${method} response has the right status`);
+
+ if (method === "POST") {
+ is(item.requestPostData.postData.text, "post-data",
+ "The POST request has the right POST data");
+ // eslint-disable-next-line mozilla/no-cpows-in-tests
+ is(item.responseContent.content.text, "Access-Control-Allow-Origin: *",
+ "The POST response has the right content");
+ }
+ }
+
+ info("Finishing the test");
+ return teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_resend_headers.js b/devtools/client/netmonitor/test/browser_net_resend_headers.js
new file mode 100644
index 000000000..0503817e3
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_resend_headers.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test if custom request headers are not ignored (bug 1270096 and friends)
+ */
+
+add_task(function* () {
+ let { monitor } = yield initNetMonitor(SIMPLE_SJS);
+ info("Starting test... ");
+
+ let { NetMonitorView, NetMonitorController } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ let requestUrl = SIMPLE_SJS;
+ let requestHeaders = [
+ { name: "Host", value: "fakehost.example.com" },
+ { name: "User-Agent", value: "Testzilla" },
+ { name: "Referer", value: "http://example.com/referrer" },
+ { name: "Accept", value: "application/jarda"},
+ { name: "Accept-Encoding", value: "compress, identity, funcoding" },
+ { name: "Accept-Language", value: "cs-CZ" }
+ ];
+
+ let wait = waitForNetworkEvents(monitor, 0, 1);
+ NetMonitorController.webConsoleClient.sendHTTPRequest({
+ url: requestUrl,
+ method: "POST",
+ headers: requestHeaders,
+ body: "Hello"
+ });
+ yield wait;
+
+ let { attachment } = RequestsMenu.getItemAtIndex(0);
+ is(attachment.method, "POST", "The request has the right method");
+ is(attachment.url, requestUrl, "The request has the right URL");
+
+ for (let { name, value } of attachment.requestHeaders.headers) {
+ info(`Request header: ${name}: ${value}`);
+ }
+
+ function hasRequestHeader(name, value) {
+ let { headers } = attachment.requestHeaders;
+ return headers.some(h => h.name === name && h.value === value);
+ }
+
+ function hasNotRequestHeader(name) {
+ let { headers } = attachment.requestHeaders;
+ return headers.every(h => h.name !== name);
+ }
+
+ for (let { name, value } of requestHeaders) {
+ ok(hasRequestHeader(name, value), `The ${name} header has the right value`);
+ }
+
+ // Check that the Cookie header was not added silently (i.e., that the request is
+ // anonymous.
+ for (let name of ["Cookie"]) {
+ ok(hasNotRequestHeader(name), `The ${name} header is not present`);
+ }
+
+ return teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_security-details.js b/devtools/client/netmonitor/test/browser_net_security-details.js
new file mode 100644
index 000000000..0a83b3ed9
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_security-details.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 that Security details tab contains the expected data.
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(CUSTOM_GET_URL);
+ let { $, EVENTS, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu, NetworkDetails } = NetMonitorView;
+ RequestsMenu.lazyUpdate = false;
+
+ info("Performing a secure request.");
+ const REQUESTS_URL = "https://example.com" + CORS_SJS_PATH;
+ let wait = waitForNetworkEvents(monitor, 1);
+ yield ContentTask.spawn(tab.linkedBrowser, REQUESTS_URL, function* (url) {
+ content.wrappedJSObject.performRequests(1, url);
+ });
+ yield wait;
+
+ info("Selecting the request.");
+ RequestsMenu.selectedIndex = 0;
+
+ info("Waiting for details pane to be updated.");
+ yield monitor.panelWin.once(EVENTS.TAB_UPDATED);
+
+ info("Selecting security tab.");
+ NetworkDetails.widget.selectedIndex = 5;
+
+ info("Waiting for security tab to be updated.");
+ yield monitor.panelWin.once(EVENTS.TAB_UPDATED);
+
+ let errorbox = $("#security-error");
+ let infobox = $("#security-information");
+
+ is(errorbox.hidden, true, "Error box is hidden.");
+ is(infobox.hidden, false, "Information box visible.");
+
+ // Connection
+
+ // The protocol will be TLS but the exact version depends on which protocol
+ // the test server example.com supports.
+ let protocol = $("#security-protocol-version-value").value;
+ ok(protocol.startsWith("TLS"), "The protocol " + protocol + " seems valid.");
+
+ // The cipher suite used by the test server example.com might change at any
+ // moment but all of them should start with "TLS_".
+ // http://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml
+ let suite = $("#security-ciphersuite-value").value;
+ ok(suite.startsWith("TLS_"), "The suite " + suite + " seems valid.");
+
+ // Host
+ checkLabel("#security-info-host-header", "Host example.com:");
+ checkLabel("#security-http-strict-transport-security-value", "Disabled");
+ checkLabel("#security-public-key-pinning-value", "Disabled");
+
+ // Cert
+ checkLabel("#security-cert-subject-cn", "example.com");
+ checkLabel("#security-cert-subject-o", "<Not Available>");
+ checkLabel("#security-cert-subject-ou", "<Not Available>");
+
+ checkLabel("#security-cert-issuer-cn", "Temporary Certificate Authority");
+ checkLabel("#security-cert-issuer-o", "Mozilla Testing");
+ checkLabel("#security-cert-issuer-ou", "<Not Available>");
+
+ // Locale sensitive and varies between timezones. Cant't compare equality or
+ // the test fails depending on which part of the world the test is executed.
+ checkLabelNotEmpty("#security-cert-validity-begins");
+ checkLabelNotEmpty("#security-cert-validity-expires");
+
+ checkLabelNotEmpty("#security-cert-sha1-fingerprint");
+ checkLabelNotEmpty("#security-cert-sha256-fingerprint");
+ yield teardown(monitor);
+
+ /**
+ * A helper that compares value attribute of a label with given selector to the
+ * expected value.
+ */
+ function checkLabel(selector, expected) {
+ info("Checking label " + selector);
+
+ let element = $(selector);
+
+ ok(element, "Selector matched an element.");
+ is(element.value, expected, "Label has the expected value.");
+ }
+
+ /**
+ * A helper that checks the label with given selector is not an empty string.
+ */
+ function checkLabelNotEmpty(selector) {
+ info("Checking that label " + selector + " is non-empty.");
+
+ let element = $(selector);
+
+ ok(element, "Selector matched an element.");
+ isnot(element.value, "", "Label was not empty.");
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_security-error.js b/devtools/client/netmonitor/test/browser_net_security-error.js
new file mode 100644
index 000000000..f6b8b34f3
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_security-error.js
@@ -0,0 +1,70 @@
+/* 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 Security details tab shows an error message with broken connections.
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(CUSTOM_GET_URL);
+ let { $, EVENTS, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu, NetworkDetails } = NetMonitorView;
+ RequestsMenu.lazyUpdate = false;
+
+ info("Requesting a resource that has a certificate problem.");
+
+ let wait = waitForSecurityBrokenNetworkEvent();
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests(1, "https://nocert.example.com");
+ });
+ yield wait;
+
+ info("Selecting the request.");
+ RequestsMenu.selectedIndex = 0;
+
+ info("Waiting for details pane to be updated.");
+ yield monitor.panelWin.once(EVENTS.TAB_UPDATED);
+
+ info("Selecting security tab.");
+ NetworkDetails.widget.selectedIndex = 5;
+
+ info("Waiting for security tab to be updated.");
+ yield monitor.panelWin.once(EVENTS.TAB_UPDATED);
+
+ let errorbox = $("#security-error");
+ let errormsg = $("#security-error-message");
+ let infobox = $("#security-information");
+
+ is(errorbox.hidden, false, "Error box is visble.");
+ is(infobox.hidden, true, "Information box is hidden.");
+
+ isnot(errormsg.value, "", "Error message is not empty.");
+
+ return teardown(monitor);
+
+ /**
+ * Returns a promise that's resolved once a request with security issues is
+ * completed.
+ */
+ function waitForSecurityBrokenNetworkEvent() {
+ let awaitedEvents = [
+ "UPDATING_REQUEST_HEADERS",
+ "RECEIVED_REQUEST_HEADERS",
+ "UPDATING_REQUEST_COOKIES",
+ "RECEIVED_REQUEST_COOKIES",
+ "STARTED_RECEIVING_RESPONSE",
+ "UPDATING_RESPONSE_CONTENT",
+ "RECEIVED_RESPONSE_CONTENT",
+ "UPDATING_EVENT_TIMINGS",
+ "RECEIVED_EVENT_TIMINGS",
+ ];
+
+ let promises = awaitedEvents.map((event) => {
+ return monitor.panelWin.once(EVENTS[event]);
+ });
+
+ return Promise.all(promises);
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_security-icon-click.js b/devtools/client/netmonitor/test/browser_net_security-icon-click.js
new file mode 100644
index 000000000..2385b11aa
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_security-icon-click.js
@@ -0,0 +1,57 @@
+/* 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 clicking on the security indicator opens the security details tab.
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(CUSTOM_GET_URL);
+ let { $, EVENTS, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu, NetworkDetails } = NetMonitorView;
+ RequestsMenu.lazyUpdate = false;
+
+ info("Requesting a resource over HTTPS.");
+ yield performRequestAndWait("https://example.com" + CORS_SJS_PATH + "?request_2");
+ yield performRequestAndWait("https://example.com" + CORS_SJS_PATH + "?request_1");
+
+ is(RequestsMenu.itemCount, 2, "Two events event logged.");
+
+ yield clickAndTestSecurityIcon();
+
+ info("Selecting headers panel again.");
+ NetworkDetails.widget.selectedIndex = 0;
+ yield monitor.panelWin.once(EVENTS.TAB_UPDATED);
+
+ info("Sorting the items by filename.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-file-button"));
+
+ info("Testing that security icon can be clicked after the items were sorted.");
+ yield clickAndTestSecurityIcon();
+
+ return teardown(monitor);
+
+ function* performRequestAndWait(url) {
+ let wait = waitForNetworkEvents(monitor, 1);
+ yield ContentTask.spawn(tab.linkedBrowser, { url }, function* (args) {
+ content.wrappedJSObject.performRequests(1, args.url);
+ });
+ return wait;
+ }
+
+ function* clickAndTestSecurityIcon() {
+ let item = RequestsMenu.items[0];
+ let icon = $(".requests-security-state-icon", item.target);
+
+ info("Clicking security icon of the first request and waiting for the " +
+ "panel to update.");
+
+ icon.click();
+ yield monitor.panelWin.once(EVENTS.TAB_UPDATED);
+
+ is(NetworkDetails.widget.selectedPanel, $("#security-tabpanel"),
+ "Security tab is selected.");
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_security-redirect.js b/devtools/client/netmonitor/test/browser_net_security-redirect.js
new file mode 100644
index 000000000..5f2956dbb
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_security-redirect.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 a http -> https redirect shows secure icon only for redirected https
+ * request.
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(CUSTOM_GET_URL);
+ let { $, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, 2);
+ yield ContentTask.spawn(tab.linkedBrowser, HTTPS_REDIRECT_SJS, function* (url) {
+ content.wrappedJSObject.performRequests(1, url);
+ });
+ yield wait;
+
+ is(RequestsMenu.itemCount, 2, "There were two requests due to redirect.");
+
+ let initial = RequestsMenu.items[0];
+ let redirect = RequestsMenu.items[1];
+
+ let initialSecurityIcon = $(".requests-security-state-icon", initial.target);
+ let redirectSecurityIcon = $(".requests-security-state-icon", redirect.target);
+
+ ok(initialSecurityIcon.classList.contains("security-state-insecure"),
+ "Initial request was marked insecure.");
+
+ ok(redirectSecurityIcon.classList.contains("security-state-secure"),
+ "Redirected request was marked secure.");
+
+ yield teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_security-state.js b/devtools/client/netmonitor/test/browser_net_security-state.js
new file mode 100644
index 000000000..054e7c969
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_security-state.js
@@ -0,0 +1,119 @@
+/* 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 correct security state indicator appears depending on the security
+ * state.
+ */
+
+add_task(function* () {
+ const EXPECTED_SECURITY_STATES = {
+ "test1.example.com": "security-state-insecure",
+ "example.com": "security-state-secure",
+ "nocert.example.com": "security-state-broken",
+ "localhost": "security-state-local",
+ };
+
+ let { tab, monitor } = yield initNetMonitor(CUSTOM_GET_URL);
+ let { $, EVENTS, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+ RequestsMenu.lazyUpdate = false;
+
+ yield performRequests();
+
+ for (let item of RequestsMenu.items) {
+ let domain = $(".requests-menu-domain", item.target).value;
+
+ info("Found a request to " + domain);
+ ok(domain in EXPECTED_SECURITY_STATES, "Domain " + domain + " was expected.");
+
+ let classes = $(".requests-security-state-icon", item.target).classList;
+ let expectedClass = EXPECTED_SECURITY_STATES[domain];
+
+ info("Classes of security state icon are: " + classes);
+ info("Security state icon is expected to contain class: " + expectedClass);
+ ok(classes.contains(expectedClass), "Icon contained the correct class name.");
+ }
+
+ return teardown(monitor);
+
+ /**
+ * A helper that performs requests to
+ * - https://nocert.example.com (broken)
+ * - https://example.com (secure)
+ * - http://test1.example.com (insecure)
+ * - http://localhost (local)
+ * and waits until NetworkMonitor has handled all packets sent by the server.
+ */
+ function* performRequests() {
+ function executeRequests(count, url) {
+ return ContentTask.spawn(tab.linkedBrowser, {count, url}, function* (args) {
+ content.wrappedJSObject.performRequests(args.count, args.url);
+ });
+ }
+
+ // waitForNetworkEvents does not work for requests with security errors as
+ // those only emit 9/13 events of a successful request.
+ let done = waitForSecurityBrokenNetworkEvent();
+
+ info("Requesting a resource that has a certificate problem.");
+ yield executeRequests(1, "https://nocert.example.com");
+
+ // Wait for the request to complete before firing another request. Otherwise
+ // the request with security issues interfere with waitForNetworkEvents.
+ info("Waiting for request to complete.");
+ yield done;
+
+ // Next perform a request over HTTP. If done the other way around the latter
+ // occasionally hangs waiting for event timings that don't seem to appear...
+ done = waitForNetworkEvents(monitor, 1);
+ info("Requesting a resource over HTTP.");
+ yield executeRequests(1, "http://test1.example.com" + CORS_SJS_PATH);
+ yield done;
+
+ done = waitForNetworkEvents(monitor, 1);
+ info("Requesting a resource over HTTPS.");
+ yield executeRequests(1, "https://example.com" + CORS_SJS_PATH);
+ yield done;
+
+ done = waitForSecurityBrokenNetworkEvent(true);
+ info("Requesting a resource over HTTP to localhost.");
+ yield executeRequests(1, "http://localhost" + CORS_SJS_PATH);
+ yield done;
+
+ const expectedCount = Object.keys(EXPECTED_SECURITY_STATES).length;
+ is(RequestsMenu.itemCount, expectedCount, expectedCount + " events logged.");
+ }
+
+ /**
+ * Returns a promise that's resolved once a request with security issues is
+ * completed.
+ */
+ function waitForSecurityBrokenNetworkEvent(networkError) {
+ let awaitedEvents = [
+ "UPDATING_REQUEST_HEADERS",
+ "RECEIVED_REQUEST_HEADERS",
+ "UPDATING_REQUEST_COOKIES",
+ "RECEIVED_REQUEST_COOKIES",
+ "STARTED_RECEIVING_RESPONSE",
+ "UPDATING_RESPONSE_CONTENT",
+ "RECEIVED_RESPONSE_CONTENT",
+ "UPDATING_EVENT_TIMINGS",
+ "RECEIVED_EVENT_TIMINGS",
+ ];
+
+ // If the reason for breakage is a network error, then the
+ // STARTED_RECEIVING_RESPONSE event does not fire.
+ if (networkError) {
+ awaitedEvents = awaitedEvents.filter(e => e !== "STARTED_RECEIVING_RESPONSE");
+ }
+
+ let promises = awaitedEvents.map((event) => {
+ return monitor.panelWin.once(EVENTS[event]);
+ });
+
+ return Promise.all(promises);
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_security-tab-deselect.js b/devtools/client/netmonitor/test/browser_net_security-tab-deselect.js
new file mode 100644
index 000000000..4a2dd0885
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_security-tab-deselect.js
@@ -0,0 +1,46 @@
+/* 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 security details tab is no longer selected if an insecure request
+ * is selected.
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(CUSTOM_GET_URL);
+ let { EVENTS, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu, NetworkDetails } = NetMonitorView;
+ RequestsMenu.lazyUpdate = false;
+
+ info("Performing requests.");
+ let wait = waitForNetworkEvents(monitor, 2);
+ const REQUEST_URLS = [
+ "https://example.com" + CORS_SJS_PATH,
+ "http://example.com" + CORS_SJS_PATH,
+ ];
+ yield ContentTask.spawn(tab.linkedBrowser, REQUEST_URLS, function* (urls) {
+ for (let url of urls) {
+ content.wrappedJSObject.performRequests(1, url);
+ }
+ });
+ yield wait;
+
+ info("Selecting secure request.");
+ RequestsMenu.selectedIndex = 0;
+
+ info("Selecting security tab.");
+ NetworkDetails.widget.selectedIndex = 5;
+
+ info("Selecting insecure request.");
+ RequestsMenu.selectedIndex = 1;
+
+ info("Waiting for security tab to be updated.");
+ yield monitor.panelWin.once(EVENTS.NETWORKDETAILSVIEW_POPULATED);
+
+ is(NetworkDetails.widget.selectedIndex, 0,
+ "Selected tab was reset when selected security tab was hidden.");
+
+ return teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_security-tab-visibility.js b/devtools/client/netmonitor/test/browser_net_security-tab-visibility.js
new file mode 100644
index 000000000..b6685d7fe
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_security-tab-visibility.js
@@ -0,0 +1,121 @@
+/* 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 security details tab is visible only when it should.
+ */
+
+add_task(function* () {
+ const TEST_DATA = [
+ {
+ desc: "http request",
+ uri: "http://example.com" + CORS_SJS_PATH,
+ visibleOnNewEvent: false,
+ visibleOnSecurityInfo: false,
+ visibleOnceComplete: false,
+ }, {
+ desc: "working https request",
+ uri: "https://example.com" + CORS_SJS_PATH,
+ visibleOnNewEvent: false,
+ visibleOnSecurityInfo: true,
+ visibleOnceComplete: true,
+ }, {
+ desc: "broken https request",
+ uri: "https://nocert.example.com",
+ isBroken: true,
+ visibleOnNewEvent: false,
+ visibleOnSecurityInfo: true,
+ visibleOnceComplete: true,
+ }
+ ];
+
+ let { tab, monitor } = yield initNetMonitor(CUSTOM_GET_URL);
+ let { $, EVENTS, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+ RequestsMenu.lazyUpdate = false;
+
+ for (let testcase of TEST_DATA) {
+ info("Testing Security tab visibility for " + testcase.desc);
+ let onNewItem = monitor.panelWin.once(EVENTS.NETWORK_EVENT);
+ let onSecurityInfo = monitor.panelWin.once(EVENTS.RECEIVED_SECURITY_INFO);
+ let onComplete = testcase.isBroken ?
+ waitForSecurityBrokenNetworkEvent() :
+ waitForNetworkEvents(monitor, 1);
+
+ let tabEl = $("#security-tab");
+ let tabpanel = $("#security-tabpanel");
+
+ info("Performing a request to " + testcase.uri);
+ yield ContentTask.spawn(tab.linkedBrowser, testcase.uri, function* (url) {
+ content.wrappedJSObject.performRequests(1, url);
+ });
+
+ info("Waiting for new network event.");
+ yield onNewItem;
+
+ info("Selecting the request.");
+ RequestsMenu.selectedIndex = 0;
+
+ is(RequestsMenu.selectedItem.attachment.securityState, undefined,
+ "Security state has not yet arrived.");
+ is(tabEl.hidden, !testcase.visibleOnNewEvent,
+ "Security tab is " +
+ (testcase.visibleOnNewEvent ? "visible" : "hidden") +
+ " after new request was added to the menu.");
+ is(tabpanel.hidden, false,
+ "#security-tabpanel is visible after new request was added to the menu.");
+
+ info("Waiting for security information to arrive.");
+ yield onSecurityInfo;
+
+ ok(RequestsMenu.selectedItem.attachment.securityState,
+ "Security state arrived.");
+ is(tabEl.hidden, !testcase.visibleOnSecurityInfo,
+ "Security tab is " +
+ (testcase.visibleOnSecurityInfo ? "visible" : "hidden") +
+ " after security information arrived.");
+ is(tabpanel.hidden, false,
+ "#security-tabpanel is visible after security information arrived.");
+
+ info("Waiting for request to complete.");
+ yield onComplete;
+
+ is(tabEl.hidden, !testcase.visibleOnceComplete,
+ "Security tab is " +
+ (testcase.visibleOnceComplete ? "visible" : "hidden") +
+ " after request has been completed.");
+ is(tabpanel.hidden, false,
+ "#security-tabpanel is visible after request is complete.");
+
+ info("Clearing requests.");
+ RequestsMenu.clear();
+ }
+
+ return teardown(monitor);
+
+ /**
+ * Returns a promise that's resolved once a request with security issues is
+ * completed.
+ */
+ function waitForSecurityBrokenNetworkEvent() {
+ let awaitedEvents = [
+ "UPDATING_REQUEST_HEADERS",
+ "RECEIVED_REQUEST_HEADERS",
+ "UPDATING_REQUEST_COOKIES",
+ "RECEIVED_REQUEST_COOKIES",
+ "STARTED_RECEIVING_RESPONSE",
+ "UPDATING_RESPONSE_CONTENT",
+ "RECEIVED_RESPONSE_CONTENT",
+ "UPDATING_EVENT_TIMINGS",
+ "RECEIVED_EVENT_TIMINGS",
+ ];
+
+ let promises = awaitedEvents.map((event) => {
+ return monitor.panelWin.once(EVENTS[event]);
+ });
+
+ return Promise.all(promises);
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_security-warnings.js b/devtools/client/netmonitor/test/browser_net_security-warnings.js
new file mode 100644
index 000000000..cdfee70a1
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_security-warnings.js
@@ -0,0 +1,56 @@
+/* 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 warning indicators are shown when appropriate.
+ */
+
+const TEST_CASES = [
+ {
+ desc: "no warnings",
+ uri: "https://example.com" + CORS_SJS_PATH,
+ warnCipher: false,
+ },
+];
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(CUSTOM_GET_URL);
+ let { $, EVENTS, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu, NetworkDetails } = NetMonitorView;
+ RequestsMenu.lazyUpdate = false;
+
+ let cipher = $("#security-warning-cipher");
+
+ for (let test of TEST_CASES) {
+ info("Testing site with " + test.desc);
+
+ info("Performing request to " + test.uri);
+ let wait = waitForNetworkEvents(monitor, 1);
+ yield ContentTask.spawn(tab.linkedBrowser, test.uri, function* (url) {
+ content.wrappedJSObject.performRequests(1, url);
+ });
+ yield wait;
+
+ info("Selecting the request.");
+ RequestsMenu.selectedIndex = 0;
+
+ info("Waiting for details pane to be updated.");
+ yield monitor.panelWin.once(EVENTS.TAB_UPDATED);
+
+ if (NetworkDetails.widget.selectedIndex !== 5) {
+ info("Selecting security tab.");
+ NetworkDetails.widget.selectedIndex = 5;
+
+ info("Waiting for details pane to be updated.");
+ yield monitor.panelWin.once(EVENTS.TAB_UPDATED);
+ }
+
+ is(cipher.hidden, !test.warnCipher, "Cipher suite warning is hidden.");
+
+ RequestsMenu.clear();
+ }
+
+ return teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_send-beacon-other-tab.js b/devtools/client/netmonitor/test/browser_net_send-beacon-other-tab.js
new file mode 100644
index 000000000..b425ad5ca
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_send-beacon-other-tab.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if beacons from other tabs are properly ignored.
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(SIMPLE_URL);
+ let { RequestsMenu } = monitor.panelWin.NetMonitorView;
+ RequestsMenu.lazyUpdate = false;
+
+ let beaconTab = yield addTab(SEND_BEACON_URL);
+ info("Beacon tab added successfully.");
+
+ is(RequestsMenu.itemCount, 0, "The requests menu should be empty.");
+
+ let wait = waitForNetworkEvents(monitor, 1);
+ yield ContentTask.spawn(beaconTab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequest();
+ });
+ tab.linkedBrowser.reload();
+ yield wait;
+
+ is(RequestsMenu.itemCount, 1, "Only the reload should be recorded.");
+ let request = RequestsMenu.getItemAtIndex(0);
+ is(request.attachment.method, "GET", "The method is correct.");
+ is(request.attachment.status, "200", "The status is correct.");
+
+ yield removeTab(beaconTab);
+ return teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_send-beacon.js b/devtools/client/netmonitor/test/browser_net_send-beacon.js
new file mode 100644
index 000000000..bdc30a960
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_send-beacon.js
@@ -0,0 +1,31 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if beacons are handled correctly.
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(SEND_BEACON_URL);
+ let { RequestsMenu } = monitor.panelWin.NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ is(RequestsMenu.itemCount, 0, "The requests menu should be empty.");
+
+ let wait = waitForNetworkEvents(monitor, 1);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequest();
+ });
+ yield wait;
+
+ is(RequestsMenu.itemCount, 1, "The beacon should be recorded.");
+ let request = RequestsMenu.getItemAtIndex(0);
+ is(request.attachment.method, "POST", "The method is correct.");
+ ok(request.attachment.url.endsWith("beacon_request"), "The URL is correct.");
+ is(request.attachment.status, "404", "The status is correct.");
+
+ return teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_service-worker-status.js b/devtools/client/netmonitor/test/browser_net_service-worker-status.js
new file mode 100644
index 000000000..d7ada1645
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_service-worker-status.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";
+
+/**
+ * Tests if requests intercepted by service workers have the correct status code
+ */
+
+// Service workers only work on https
+const URL = EXAMPLE_URL.replace("http:", "https:");
+
+const TEST_URL = URL + "service-workers/status-codes.html";
+
+add_task(function* () {
+ yield new Promise(done => {
+ let options = { "set": [
+ // Accept workers from mochitest's http.
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.openWindow.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ]};
+ SpecialPowers.pushPrefEnv(options, done);
+ });
+
+ let { tab, monitor } = yield initNetMonitor(TEST_URL, null, true);
+ info("Starting test... ");
+
+ let { NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ const REQUEST_DATA = [
+ {
+ method: "GET",
+ uri: URL + "service-workers/test/200",
+ details: {
+ status: 200,
+ statusText: "OK (service worker)",
+ displayedStatus: "service worker",
+ type: "plain",
+ fullMimeType: "text/plain; charset=UTF-8"
+ },
+ stackFunctions: ["doXHR", "performRequests"]
+ },
+ ];
+
+ info("Registering the service worker...");
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ yield content.wrappedJSObject.registerServiceWorker();
+ });
+
+ info("Performing requests...");
+ let wait = waitForNetworkEvents(monitor, REQUEST_DATA.length);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests();
+ });
+ yield wait;
+
+ let index = 0;
+ for (let request of REQUEST_DATA) {
+ let item = RequestsMenu.getItemAtIndex(index);
+
+ info(`Verifying request #${index}`);
+ yield verifyRequestItemTarget(item, request.method, request.uri, request.details);
+
+ let { stacktrace } = item.attachment.cause;
+ let stackLen = stacktrace ? stacktrace.length : 0;
+
+ ok(stacktrace, `Request #${index} has a stacktrace`);
+ ok(stackLen >= request.stackFunctions.length,
+ `Request #${index} has a stacktrace with enough (${stackLen}) items`);
+
+ request.stackFunctions.forEach((functionName, j) => {
+ is(stacktrace[j].functionName, functionName,
+ `Request #${index} has the correct function at position #${j} on the stack`);
+ });
+
+ index++;
+ }
+
+ info("Unregistering the service worker...");
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ yield content.wrappedJSObject.unregisterServiceWorker();
+ });
+
+ yield teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_simple-init.js b/devtools/client/netmonitor/test/browser_net_simple-init.js
new file mode 100644
index 000000000..19d05811c
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_simple-init.js
@@ -0,0 +1,93 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Simple check if the network monitor starts up and shuts down properly.
+ */
+
+function test() {
+ // These test suite functions are removed from the global scope inside a
+ // cleanup function. However, we still need them.
+ let gInfo = info;
+ let gOk = ok;
+
+ initNetMonitor(SIMPLE_URL).then(({ tab, monitor }) => {
+ info("Starting test... ");
+
+ is(tab.linkedBrowser.currentURI.spec, SIMPLE_URL,
+ "The current tab's location is the correct one.");
+
+ function checkIfInitialized(tag) {
+ info(`Checking if initialization is ok (${tag}).`);
+
+ ok(monitor._view,
+ `The network monitor view object exists (${tag}).`);
+ ok(monitor._controller,
+ `The network monitor controller object exists (${tag}).`);
+ ok(monitor._controller._startup,
+ `The network monitor controller object exists and is initialized (${tag}).`);
+
+ ok(monitor.isReady,
+ `The network monitor panel appears to be ready (${tag}).`);
+
+ ok(monitor._controller.tabClient,
+ `There should be a tabClient available at this point (${tag}).`);
+ ok(monitor._controller.webConsoleClient,
+ `There should be a webConsoleClient available at this point (${tag}).`);
+ ok(monitor._controller.timelineFront,
+ `There should be a timelineFront available at this point (${tag}).`);
+ }
+
+ function checkIfDestroyed(tag) {
+ gInfo("Checking if destruction is ok.");
+
+ gOk(monitor._view,
+ `The network monitor view object still exists (${tag}).`);
+ gOk(monitor._controller,
+ `The network monitor controller object still exists (${tag}).`);
+ gOk(monitor._controller._shutdown,
+ `The network monitor controller object still exists and is destroyed (${tag}).`);
+
+ gOk(!monitor._controller.tabClient,
+ `There shouldn't be a tabClient available after destruction (${tag}).`);
+ gOk(!monitor._controller.webConsoleClient,
+ `There shouldn't be a webConsoleClient available after destruction (${tag}).`);
+ gOk(!monitor._controller.timelineFront,
+ `There shouldn't be a timelineFront available after destruction (${tag}).`);
+ }
+
+ executeSoon(() => {
+ checkIfInitialized(1);
+
+ monitor._controller.startupNetMonitor()
+ .then(() => {
+ info("Starting up again shouldn't do anything special.");
+ checkIfInitialized(2);
+ return monitor._controller.connect();
+ })
+ .then(() => {
+ info("Connecting again shouldn't do anything special.");
+ checkIfInitialized(3);
+ return teardown(monitor);
+ })
+ .then(finish);
+ });
+
+ registerCleanupFunction(() => {
+ checkIfDestroyed(1);
+
+ monitor._controller.shutdownNetMonitor()
+ .then(() => {
+ gInfo("Shutting down again shouldn't do anything special.");
+ checkIfDestroyed(2);
+ return monitor._controller.disconnect();
+ })
+ .then(() => {
+ gInfo("Disconnecting again shouldn't do anything special.");
+ checkIfDestroyed(3);
+ });
+ });
+ });
+}
diff --git a/devtools/client/netmonitor/test/browser_net_simple-request-data.js b/devtools/client/netmonitor/test/browser_net_simple-request-data.js
new file mode 100644
index 000000000..1b952bd71
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_simple-request-data.js
@@ -0,0 +1,247 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if requests render correct information in the menu UI.
+ */
+
+function test() {
+ let { L10N } = require("devtools/client/netmonitor/l10n");
+
+ initNetMonitor(SIMPLE_SJS).then(({ tab, monitor }) => {
+ info("Starting test... ");
+
+ let { NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ waitForNetworkEvents(monitor, 1)
+ .then(() => teardown(monitor))
+ .then(finish);
+
+ monitor.panelWin.once(monitor.panelWin.EVENTS.NETWORK_EVENT, () => {
+ is(RequestsMenu.selectedItem, null,
+ "There shouldn't be any selected item in the requests menu.");
+ is(RequestsMenu.itemCount, 1,
+ "The requests menu should not be empty after the first request.");
+ is(NetMonitorView.detailsPaneHidden, true,
+ "The details pane should still be hidden after the first request.");
+
+ let requestItem = RequestsMenu.getItemAtIndex(0);
+
+ is(typeof requestItem.value, "string",
+ "The attached request id is incorrect.");
+ isnot(requestItem.value, "",
+ "The attached request id should not be empty.");
+
+ is(typeof requestItem.attachment.startedDeltaMillis, "number",
+ "The attached startedDeltaMillis is incorrect.");
+ is(requestItem.attachment.startedDeltaMillis, 0,
+ "The attached startedDeltaMillis should be zero.");
+
+ is(typeof requestItem.attachment.startedMillis, "number",
+ "The attached startedMillis is incorrect.");
+ isnot(requestItem.attachment.startedMillis, 0,
+ "The attached startedMillis should not be zero.");
+
+ is(requestItem.attachment.requestHeaders, undefined,
+ "The requestHeaders should not yet be set.");
+ is(requestItem.attachment.requestCookies, undefined,
+ "The requestCookies should not yet be set.");
+ is(requestItem.attachment.requestPostData, undefined,
+ "The requestPostData should not yet be set.");
+
+ is(requestItem.attachment.responseHeaders, undefined,
+ "The responseHeaders should not yet be set.");
+ is(requestItem.attachment.responseCookies, undefined,
+ "The responseCookies should not yet be set.");
+
+ is(requestItem.attachment.httpVersion, undefined,
+ "The httpVersion should not yet be set.");
+ is(requestItem.attachment.status, undefined,
+ "The status should not yet be set.");
+ is(requestItem.attachment.statusText, undefined,
+ "The statusText should not yet be set.");
+
+ is(requestItem.attachment.headersSize, undefined,
+ "The headersSize should not yet be set.");
+ is(requestItem.attachment.transferredSize, undefined,
+ "The transferredSize should not yet be set.");
+ is(requestItem.attachment.contentSize, undefined,
+ "The contentSize should not yet be set.");
+
+ is(requestItem.attachment.mimeType, undefined,
+ "The mimeType should not yet be set.");
+ is(requestItem.attachment.responseContent, undefined,
+ "The responseContent should not yet be set.");
+
+ is(requestItem.attachment.totalTime, undefined,
+ "The totalTime should not yet be set.");
+ is(requestItem.attachment.eventTimings, undefined,
+ "The eventTimings should not yet be set.");
+
+ verifyRequestItemTarget(requestItem, "GET", SIMPLE_SJS);
+ });
+
+ monitor.panelWin.once(monitor.panelWin.EVENTS.RECEIVED_REQUEST_HEADERS, () => {
+ let requestItem = RequestsMenu.getItemAtIndex(0);
+
+ ok(requestItem.attachment.requestHeaders,
+ "There should be a requestHeaders attachment available.");
+ is(requestItem.attachment.requestHeaders.headers.length, 9,
+ "The requestHeaders attachment has an incorrect |headers| property.");
+ isnot(requestItem.attachment.requestHeaders.headersSize, 0,
+ "The requestHeaders attachment has an incorrect |headersSize| property.");
+ // Can't test for the exact request headers size because the value may
+ // vary across platforms ("User-Agent" header differs).
+
+ verifyRequestItemTarget(requestItem, "GET", SIMPLE_SJS);
+ });
+
+ monitor.panelWin.once(monitor.panelWin.EVENTS.RECEIVED_REQUEST_COOKIES, () => {
+ let requestItem = RequestsMenu.getItemAtIndex(0);
+
+ ok(requestItem.attachment.requestCookies,
+ "There should be a requestCookies attachment available.");
+ is(requestItem.attachment.requestCookies.cookies.length, 2,
+ "The requestCookies attachment has an incorrect |cookies| property.");
+
+ verifyRequestItemTarget(requestItem, "GET", SIMPLE_SJS);
+ });
+
+ monitor.panelWin.once(monitor.panelWin.EVENTS.RECEIVED_REQUEST_POST_DATA, () => {
+ ok(false, "Trap listener: this request doesn't have any post data.");
+ });
+
+ monitor.panelWin.once(monitor.panelWin.EVENTS.RECEIVED_RESPONSE_HEADERS, () => {
+ let requestItem = RequestsMenu.getItemAtIndex(0);
+
+ ok(requestItem.attachment.responseHeaders,
+ "There should be a responseHeaders attachment available.");
+ is(requestItem.attachment.responseHeaders.headers.length, 10,
+ "The responseHeaders attachment has an incorrect |headers| property.");
+ is(requestItem.attachment.responseHeaders.headersSize, 330,
+ "The responseHeaders attachment has an incorrect |headersSize| property.");
+
+ verifyRequestItemTarget(requestItem, "GET", SIMPLE_SJS);
+ });
+
+ monitor.panelWin.once(monitor.panelWin.EVENTS.RECEIVED_RESPONSE_COOKIES, () => {
+ let requestItem = RequestsMenu.getItemAtIndex(0);
+
+ ok(requestItem.attachment.responseCookies,
+ "There should be a responseCookies attachment available.");
+ is(requestItem.attachment.responseCookies.cookies.length, 2,
+ "The responseCookies attachment has an incorrect |cookies| property.");
+
+ verifyRequestItemTarget(requestItem, "GET", SIMPLE_SJS);
+ });
+
+ monitor.panelWin.once(monitor.panelWin.EVENTS.STARTED_RECEIVING_RESPONSE, () => {
+ let requestItem = RequestsMenu.getItemAtIndex(0);
+
+ is(requestItem.attachment.httpVersion, "HTTP/1.1",
+ "The httpVersion attachment has an incorrect value.");
+ is(requestItem.attachment.status, "200",
+ "The status attachment has an incorrect value.");
+ is(requestItem.attachment.statusText, "Och Aye",
+ "The statusText attachment has an incorrect value.");
+ is(requestItem.attachment.headersSize, 330,
+ "The headersSize attachment has an incorrect value.");
+
+ verifyRequestItemTarget(requestItem, "GET", SIMPLE_SJS, {
+ status: "200",
+ statusText: "Och Aye"
+ });
+ });
+
+ monitor.panelWin.once(monitor.panelWin.EVENTS.UPDATING_RESPONSE_CONTENT, () => {
+ let requestItem = RequestsMenu.getItemAtIndex(0);
+
+ is(requestItem.attachment.transferredSize, "12",
+ "The transferredSize attachment has an incorrect value.");
+ is(requestItem.attachment.contentSize, "12",
+ "The contentSize attachment has an incorrect value.");
+ is(requestItem.attachment.mimeType, "text/plain; charset=utf-8",
+ "The mimeType attachment has an incorrect value.");
+
+ verifyRequestItemTarget(requestItem, "GET", SIMPLE_SJS, {
+ type: "plain",
+ fullMimeType: "text/plain; charset=utf-8",
+ transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.01),
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.01),
+ });
+ });
+
+ monitor.panelWin.once(monitor.panelWin.EVENTS.RECEIVED_RESPONSE_CONTENT, () => {
+ let requestItem = RequestsMenu.getItemAtIndex(0);
+
+ ok(requestItem.attachment.responseContent,
+ "There should be a responseContent attachment available.");
+ is(requestItem.attachment.responseContent.content.mimeType,
+ "text/plain; charset=utf-8",
+ "The responseContent attachment has an incorrect |content.mimeType| property.");
+ is(requestItem.attachment.responseContent.content.text,
+ "Hello world!",
+ "The responseContent attachment has an incorrect |content.text| property.");
+ is(requestItem.attachment.responseContent.content.size,
+ 12,
+ "The responseContent attachment has an incorrect |content.size| property.");
+
+ verifyRequestItemTarget(requestItem, "GET", SIMPLE_SJS, {
+ type: "plain",
+ fullMimeType: "text/plain; charset=utf-8",
+ transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.01),
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.01),
+ });
+ });
+
+ monitor.panelWin.once(monitor.panelWin.EVENTS.UPDATING_EVENT_TIMINGS, () => {
+ let requestItem = RequestsMenu.getItemAtIndex(0);
+
+ is(typeof requestItem.attachment.totalTime, "number",
+ "The attached totalTime is incorrect.");
+ ok(requestItem.attachment.totalTime >= 0,
+ "The attached totalTime should be positive.");
+
+ is(typeof requestItem.attachment.endedMillis, "number",
+ "The attached endedMillis is incorrect.");
+ ok(requestItem.attachment.endedMillis >= 0,
+ "The attached endedMillis should be positive.");
+
+ verifyRequestItemTarget(requestItem, "GET", SIMPLE_SJS, {
+ time: true
+ });
+ });
+
+ monitor.panelWin.once(monitor.panelWin.EVENTS.RECEIVED_EVENT_TIMINGS, () => {
+ let requestItem = RequestsMenu.getItemAtIndex(0);
+
+ ok(requestItem.attachment.eventTimings,
+ "There should be a eventTimings attachment available.");
+ is(typeof requestItem.attachment.eventTimings.timings.blocked, "number",
+ "The eventTimings attachment has an incorrect |timings.blocked| property.");
+ is(typeof requestItem.attachment.eventTimings.timings.dns, "number",
+ "The eventTimings attachment has an incorrect |timings.dns| property.");
+ is(typeof requestItem.attachment.eventTimings.timings.connect, "number",
+ "The eventTimings attachment has an incorrect |timings.connect| property.");
+ is(typeof requestItem.attachment.eventTimings.timings.send, "number",
+ "The eventTimings attachment has an incorrect |timings.send| property.");
+ is(typeof requestItem.attachment.eventTimings.timings.wait, "number",
+ "The eventTimings attachment has an incorrect |timings.wait| property.");
+ is(typeof requestItem.attachment.eventTimings.timings.receive, "number",
+ "The eventTimings attachment has an incorrect |timings.receive| property.");
+ is(typeof requestItem.attachment.eventTimings.totalTime, "number",
+ "The eventTimings attachment has an incorrect |totalTime| property.");
+
+ verifyRequestItemTarget(requestItem, "GET", SIMPLE_SJS, {
+ time: true
+ });
+ });
+
+ tab.linkedBrowser.reload();
+ });
+}
diff --git a/devtools/client/netmonitor/test/browser_net_simple-request-details.js b/devtools/client/netmonitor/test/browser_net_simple-request-details.js
new file mode 100644
index 000000000..6be634e68
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_simple-request-details.js
@@ -0,0 +1,261 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if requests render correct information in the details UI.
+ */
+
+add_task(function* () {
+ let { L10N } = require("devtools/client/netmonitor/l10n");
+
+ let { tab, monitor } = yield initNetMonitor(SIMPLE_SJS);
+ info("Starting test... ");
+
+ let { document, EVENTS, Editor, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, 1);
+ tab.linkedBrowser.reload();
+ yield wait;
+
+ is(RequestsMenu.selectedItem, null,
+ "There shouldn't be any selected item in the requests menu.");
+ is(RequestsMenu.itemCount, 1,
+ "The requests menu should not be empty after the first request.");
+ is(NetMonitorView.detailsPaneHidden, true,
+ "The details pane should still be hidden after the first request.");
+
+ let onTabUpdated = monitor.panelWin.once(EVENTS.TAB_UPDATED);
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.getElementById("details-pane-toggle"));
+ yield onTabUpdated;
+
+ isnot(RequestsMenu.selectedItem, null,
+ "There should be a selected item in the requests menu.");
+ is(RequestsMenu.selectedIndex, 0,
+ "The first item should be selected in the requests menu.");
+ is(NetMonitorView.detailsPaneHidden, false,
+ "The details pane should not be hidden after toggle button was pressed.");
+
+ testHeadersTab();
+ yield testCookiesTab();
+ testParamsTab();
+ yield testResponseTab();
+ testTimingsTab();
+ return teardown(monitor);
+
+ function testHeadersTab() {
+ let tabEl = document.querySelectorAll("#details-pane tab")[0];
+ let tabpanel = document.querySelectorAll("#details-pane tabpanel")[0];
+
+ is(tabEl.getAttribute("selected"), "true",
+ "The headers tab in the network details pane should be selected.");
+
+ is(tabpanel.querySelector("#headers-summary-url-value").getAttribute("value"),
+ SIMPLE_SJS, "The url summary value is incorrect.");
+ is(tabpanel.querySelector("#headers-summary-url-value").getAttribute("tooltiptext"),
+ SIMPLE_SJS, "The url summary tooltiptext is incorrect.");
+ is(tabpanel.querySelector("#headers-summary-method-value").getAttribute("value"),
+ "GET", "The method summary value is incorrect.");
+ is(tabpanel.querySelector("#headers-summary-address-value").getAttribute("value"),
+ "127.0.0.1:8888", "The remote address summary value is incorrect.");
+ is(tabpanel.querySelector("#headers-summary-status-circle").getAttribute("code"),
+ "200", "The status summary code is incorrect.");
+ is(tabpanel.querySelector("#headers-summary-status-value").getAttribute("value"),
+ "200 Och Aye", "The status summary value is incorrect.");
+
+ is(tabpanel.querySelectorAll(".variables-view-scope").length, 2,
+ "There should be 2 header scopes displayed in this tabpanel.");
+ is(tabpanel.querySelectorAll(".variable-or-property").length, 19,
+ "There should be 19 header values displayed in this tabpanel.");
+
+ is(tabpanel.querySelectorAll(".variables-view-empty-notice").length, 0,
+ "The empty notice should not be displayed in this tabpanel.");
+
+ let responseScope = tabpanel.querySelectorAll(".variables-view-scope")[0];
+ let requestScope = tabpanel.querySelectorAll(".variables-view-scope")[1];
+
+ is(responseScope.querySelector(".name").getAttribute("value"),
+ L10N.getStr("responseHeaders") + " (" +
+ L10N.getFormatStr("networkMenu.sizeKB",
+ L10N.numberWithDecimals(330 / 1024, 3)) + ")",
+ "The response headers scope doesn't have the correct title.");
+
+ ok(requestScope.querySelector(".name").getAttribute("value").includes(
+ L10N.getStr("requestHeaders") + " (0"),
+ "The request headers scope doesn't have the correct title.");
+ // Can't test for full request headers title because the size may
+ // vary across platforms ("User-Agent" header differs). We're pretty
+ // sure it's smaller than 1 MB though, so it starts with a 0.
+
+ is(responseScope.querySelectorAll(".variables-view-variable .name")[0]
+ .getAttribute("value"),
+ "Cache-Control", "The first response header name was incorrect.");
+ is(responseScope.querySelectorAll(".variables-view-variable .value")[0]
+ .getAttribute("value"),
+ "\"no-cache, no-store, must-revalidate\"",
+ "The first response header value was incorrect.");
+ is(responseScope.querySelectorAll(".variables-view-variable .name")[1]
+ .getAttribute("value"),
+ "Connection", "The second response header name was incorrect.");
+ is(responseScope.querySelectorAll(".variables-view-variable .value")[1]
+ .getAttribute("value"),
+ "\"close\"", "The second response header value was incorrect.");
+ is(responseScope.querySelectorAll(".variables-view-variable .name")[2]
+ .getAttribute("value"),
+ "Content-Length", "The third response header name was incorrect.");
+ is(responseScope.querySelectorAll(".variables-view-variable .value")[2]
+ .getAttribute("value"),
+ "\"12\"", "The third response header value was incorrect.");
+ is(responseScope.querySelectorAll(".variables-view-variable .name")[3]
+ .getAttribute("value"),
+ "Content-Type", "The fourth response header name was incorrect.");
+ is(responseScope.querySelectorAll(".variables-view-variable .value")[3]
+ .getAttribute("value"),
+ "\"text/plain; charset=utf-8\"", "The fourth response header value was incorrect.");
+ is(responseScope.querySelectorAll(".variables-view-variable .name")[9]
+ .getAttribute("value"),
+ "foo-bar", "The last response header name was incorrect.");
+ is(responseScope.querySelectorAll(".variables-view-variable .value")[9]
+ .getAttribute("value"),
+ "\"baz\"", "The last response header value was incorrect.");
+
+ is(requestScope.querySelectorAll(".variables-view-variable .name")[0]
+ .getAttribute("value"),
+ "Host", "The first request header name was incorrect.");
+ is(requestScope.querySelectorAll(".variables-view-variable .value")[0]
+ .getAttribute("value"),
+ "\"example.com\"", "The first request header value was incorrect.");
+ is(requestScope.querySelectorAll(".variables-view-variable .name")[6]
+ .getAttribute("value"),
+ "Connection", "The ante-penultimate request header name was incorrect.");
+ is(requestScope.querySelectorAll(".variables-view-variable .value")[6]
+ .getAttribute("value"),
+ "\"keep-alive\"", "The ante-penultimate request header value was incorrect.");
+ is(requestScope.querySelectorAll(".variables-view-variable .name")[7]
+ .getAttribute("value"),
+ "Pragma", "The penultimate request header name was incorrect.");
+ is(requestScope.querySelectorAll(".variables-view-variable .value")[7]
+ .getAttribute("value"),
+ "\"no-cache\"", "The penultimate request header value was incorrect.");
+ is(requestScope.querySelectorAll(".variables-view-variable .name")[8]
+ .getAttribute("value"),
+ "Cache-Control", "The last request header name was incorrect.");
+ is(requestScope.querySelectorAll(".variables-view-variable .value")[8]
+ .getAttribute("value"),
+ "\"no-cache\"", "The last request header value was incorrect.");
+ }
+
+ function* testCookiesTab() {
+ let onEvent = monitor.panelWin.once(EVENTS.TAB_UPDATED);
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.querySelectorAll("#details-pane tab")[1]);
+ yield onEvent;
+
+ let tabEl = document.querySelectorAll("#details-pane tab")[1];
+ let tabpanel = document.querySelectorAll("#details-pane tabpanel")[1];
+
+ is(tabEl.getAttribute("selected"), "true",
+ "The cookies tab in the network details pane should be selected.");
+
+ is(tabpanel.querySelectorAll(".variables-view-scope").length, 2,
+ "There should be 2 cookie scopes displayed in this tabpanel.");
+ is(tabpanel.querySelectorAll(".variable-or-property").length, 6,
+ "There should be 6 cookie values displayed in this tabpanel.");
+ }
+
+ function testParamsTab() {
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.querySelectorAll("#details-pane tab")[2]);
+
+ let tabEl = document.querySelectorAll("#details-pane tab")[2];
+ let tabpanel = document.querySelectorAll("#details-pane tabpanel")[2];
+
+ is(tabEl.getAttribute("selected"), "true",
+ "The params tab in the network details pane should be selected.");
+
+ is(tabpanel.querySelectorAll(".variables-view-scope").length, 0,
+ "There should be no param scopes displayed in this tabpanel.");
+ is(tabpanel.querySelectorAll(".variable-or-property").length, 0,
+ "There should be no param values displayed in this tabpanel.");
+ is(tabpanel.querySelectorAll(".variables-view-empty-notice").length, 1,
+ "The empty notice should be displayed in this tabpanel.");
+
+ is(tabpanel.querySelector("#request-params-box")
+ .hasAttribute("hidden"), false,
+ "The request params box should not be hidden.");
+ is(tabpanel.querySelector("#request-post-data-textarea-box")
+ .hasAttribute("hidden"), true,
+ "The request post data textarea box should be hidden.");
+ }
+
+ function* testResponseTab() {
+ let onEvent = monitor.panelWin.once(EVENTS.TAB_UPDATED);
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.querySelectorAll("#details-pane tab")[3]);
+ yield onEvent;
+
+ let tabEl = document.querySelectorAll("#details-pane tab")[3];
+ let tabpanel = document.querySelectorAll("#details-pane tabpanel")[3];
+
+ is(tabEl.getAttribute("selected"), "true",
+ "The response tab in the network details pane should be selected.");
+
+ is(tabpanel.querySelector("#response-content-info-header")
+ .hasAttribute("hidden"), true,
+ "The response info header should be hidden.");
+ is(tabpanel.querySelector("#response-content-json-box")
+ .hasAttribute("hidden"), true,
+ "The response content json box should be hidden.");
+ is(tabpanel.querySelector("#response-content-textarea-box")
+ .hasAttribute("hidden"), false,
+ "The response content textarea box should not be hidden.");
+ is(tabpanel.querySelector("#response-content-image-box")
+ .hasAttribute("hidden"), true,
+ "The response content image box should be hidden.");
+
+ let editor = yield NetMonitorView.editor("#response-content-textarea");
+ is(editor.getText(), "Hello world!",
+ "The text shown in the source editor is incorrect.");
+ is(editor.getMode(), Editor.modes.text,
+ "The mode active in the source editor is incorrect.");
+ }
+
+ function testTimingsTab() {
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.querySelectorAll("#details-pane tab")[4]);
+
+ let tabEl = document.querySelectorAll("#details-pane tab")[4];
+ let tabpanel = document.querySelectorAll("#details-pane tabpanel")[4];
+
+ is(tabEl.getAttribute("selected"), "true",
+ "The timings tab in the network details pane should be selected.");
+
+ ok(tabpanel.querySelector("#timings-summary-blocked .requests-menu-timings-total")
+ .getAttribute("value").match(/[0-9]+/),
+ "The blocked timing info does not appear to be correct.");
+
+ ok(tabpanel.querySelector("#timings-summary-dns .requests-menu-timings-total")
+ .getAttribute("value").match(/[0-9]+/),
+ "The dns timing info does not appear to be correct.");
+
+ ok(tabpanel.querySelector("#timings-summary-connect .requests-menu-timings-total")
+ .getAttribute("value").match(/[0-9]+/),
+ "The connect timing info does not appear to be correct.");
+
+ ok(tabpanel.querySelector("#timings-summary-send .requests-menu-timings-total")
+ .getAttribute("value").match(/[0-9]+/),
+ "The send timing info does not appear to be correct.");
+
+ ok(tabpanel.querySelector("#timings-summary-wait .requests-menu-timings-total")
+ .getAttribute("value").match(/[0-9]+/),
+ "The wait timing info does not appear to be correct.");
+
+ ok(tabpanel.querySelector("#timings-summary-receive .requests-menu-timings-total")
+ .getAttribute("value").match(/[0-9]+/),
+ "The receive timing info does not appear to be correct.");
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_simple-request.js b/devtools/client/netmonitor/test/browser_net_simple-request.js
new file mode 100644
index 000000000..898cb3710
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_simple-request.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Test whether the UI state properly reflects existence of requests
+ * displayed in the Net panel. The following parts of the UI are
+ * tested:
+ * 1) Side panel visibility
+ * 2) Side panel toggle button
+ * 3) Empty user message visibility
+ * 4) Number of requests displayed
+ */
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(SIMPLE_URL);
+ info("Starting test... ");
+
+ let { document, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ is(document.querySelector("#details-pane-toggle").hasAttribute("disabled"), true,
+ "The pane toggle button should be disabled when the frontend is opened.");
+ is(document.querySelector("#requests-menu-empty-notice").hasAttribute("hidden"), false,
+ "An empty notice should be displayed when the frontend is opened.");
+ is(RequestsMenu.itemCount, 0,
+ "The requests menu should be empty when the frontend is opened.");
+ is(NetMonitorView.detailsPaneHidden, true,
+ "The details pane should be hidden when the frontend is opened.");
+
+ yield reloadAndWait();
+
+ is(document.querySelector("#details-pane-toggle").hasAttribute("disabled"), false,
+ "The pane toggle button should be enabled after the first request.");
+ is(document.querySelector("#requests-menu-empty-notice").hasAttribute("hidden"), true,
+ "The empty notice should be hidden after the first request.");
+ is(RequestsMenu.itemCount, 1,
+ "The requests menu should not be empty after the first request.");
+ is(NetMonitorView.detailsPaneHidden, true,
+ "The details pane should still be hidden after the first request.");
+
+ yield reloadAndWait();
+
+ is(document.querySelector("#details-pane-toggle").hasAttribute("disabled"), false,
+ "The pane toggle button should be still be enabled after a reload.");
+ is(document.querySelector("#requests-menu-empty-notice").hasAttribute("hidden"), true,
+ "The empty notice should be still hidden after a reload.");
+ is(RequestsMenu.itemCount, 1,
+ "The requests menu should not be empty after a reload.");
+ is(NetMonitorView.detailsPaneHidden, true,
+ "The details pane should still be hidden after a reload.");
+
+ RequestsMenu.clear();
+
+ is(document.querySelector("#details-pane-toggle").hasAttribute("disabled"), true,
+ "The pane toggle button should be disabled when after clear.");
+ is(document.querySelector("#requests-menu-empty-notice").hasAttribute("hidden"), false,
+ "An empty notice should be displayed again after clear.");
+ is(RequestsMenu.itemCount, 0,
+ "The requests menu should be empty after clear.");
+ is(NetMonitorView.detailsPaneHidden, true,
+ "The details pane should be hidden after clear.");
+
+ return teardown(monitor);
+
+ function* reloadAndWait() {
+ let wait = waitForNetworkEvents(monitor, 1);
+ tab.linkedBrowser.reload();
+ return wait;
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_sort-01.js b/devtools/client/netmonitor/test/browser_net_sort-01.js
new file mode 100644
index 000000000..2c4e718dc
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_sort-01.js
@@ -0,0 +1,230 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test if the sorting mechanism works correctly.
+ */
+
+add_task(function* () {
+ let { L10N } = require("devtools/client/netmonitor/l10n");
+
+ let { tab, monitor } = yield initNetMonitor(STATUS_CODES_URL);
+ info("Starting test... ");
+
+ let { $all, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, 5);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests();
+ });
+ yield wait;
+
+ testContents([0, 1, 2, 3, 4]);
+
+ info("Testing swap(0, 0)");
+ RequestsMenu.swapItemsAtIndices(0, 0);
+ RequestsMenu.refreshZebra();
+ testContents([0, 1, 2, 3, 4]);
+
+ info("Testing swap(0, 1)");
+ RequestsMenu.swapItemsAtIndices(0, 1);
+ RequestsMenu.refreshZebra();
+ testContents([1, 0, 2, 3, 4]);
+
+ info("Testing swap(0, 2)");
+ RequestsMenu.swapItemsAtIndices(0, 2);
+ RequestsMenu.refreshZebra();
+ testContents([1, 2, 0, 3, 4]);
+
+ info("Testing swap(0, 3)");
+ RequestsMenu.swapItemsAtIndices(0, 3);
+ RequestsMenu.refreshZebra();
+ testContents([1, 2, 3, 0, 4]);
+
+ info("Testing swap(0, 4)");
+ RequestsMenu.swapItemsAtIndices(0, 4);
+ RequestsMenu.refreshZebra();
+ testContents([1, 2, 3, 4, 0]);
+
+ info("Testing swap(1, 0)");
+ RequestsMenu.swapItemsAtIndices(1, 0);
+ RequestsMenu.refreshZebra();
+ testContents([0, 2, 3, 4, 1]);
+
+ info("Testing swap(1, 1)");
+ RequestsMenu.swapItemsAtIndices(1, 1);
+ RequestsMenu.refreshZebra();
+ testContents([0, 2, 3, 4, 1]);
+
+ info("Testing swap(1, 2)");
+ RequestsMenu.swapItemsAtIndices(1, 2);
+ RequestsMenu.refreshZebra();
+ testContents([0, 1, 3, 4, 2]);
+
+ info("Testing swap(1, 3)");
+ RequestsMenu.swapItemsAtIndices(1, 3);
+ RequestsMenu.refreshZebra();
+ testContents([0, 3, 1, 4, 2]);
+
+ info("Testing swap(1, 4)");
+ RequestsMenu.swapItemsAtIndices(1, 4);
+ RequestsMenu.refreshZebra();
+ testContents([0, 3, 4, 1, 2]);
+
+ info("Testing swap(2, 0)");
+ RequestsMenu.swapItemsAtIndices(2, 0);
+ RequestsMenu.refreshZebra();
+ testContents([2, 3, 4, 1, 0]);
+
+ info("Testing swap(2, 1)");
+ RequestsMenu.swapItemsAtIndices(2, 1);
+ RequestsMenu.refreshZebra();
+ testContents([1, 3, 4, 2, 0]);
+
+ info("Testing swap(2, 2)");
+ RequestsMenu.swapItemsAtIndices(2, 2);
+ RequestsMenu.refreshZebra();
+ testContents([1, 3, 4, 2, 0]);
+
+ info("Testing swap(2, 3)");
+ RequestsMenu.swapItemsAtIndices(2, 3);
+ RequestsMenu.refreshZebra();
+ testContents([1, 2, 4, 3, 0]);
+
+ info("Testing swap(2, 4)");
+ RequestsMenu.swapItemsAtIndices(2, 4);
+ RequestsMenu.refreshZebra();
+ testContents([1, 4, 2, 3, 0]);
+
+ info("Testing swap(3, 0)");
+ RequestsMenu.swapItemsAtIndices(3, 0);
+ RequestsMenu.refreshZebra();
+ testContents([1, 4, 2, 0, 3]);
+
+ info("Testing swap(3, 1)");
+ RequestsMenu.swapItemsAtIndices(3, 1);
+ RequestsMenu.refreshZebra();
+ testContents([3, 4, 2, 0, 1]);
+
+ info("Testing swap(3, 2)");
+ RequestsMenu.swapItemsAtIndices(3, 2);
+ RequestsMenu.refreshZebra();
+ testContents([2, 4, 3, 0, 1]);
+
+ info("Testing swap(3, 3)");
+ RequestsMenu.swapItemsAtIndices(3, 3);
+ RequestsMenu.refreshZebra();
+ testContents([2, 4, 3, 0, 1]);
+
+ info("Testing swap(3, 4)");
+ RequestsMenu.swapItemsAtIndices(3, 4);
+ RequestsMenu.refreshZebra();
+ testContents([2, 3, 4, 0, 1]);
+
+ info("Testing swap(4, 0)");
+ RequestsMenu.swapItemsAtIndices(4, 0);
+ RequestsMenu.refreshZebra();
+ testContents([2, 3, 0, 4, 1]);
+
+ info("Testing swap(4, 1)");
+ RequestsMenu.swapItemsAtIndices(4, 1);
+ RequestsMenu.refreshZebra();
+ testContents([2, 3, 0, 1, 4]);
+
+ info("Testing swap(4, 2)");
+ RequestsMenu.swapItemsAtIndices(4, 2);
+ RequestsMenu.refreshZebra();
+ testContents([4, 3, 0, 1, 2]);
+
+ info("Testing swap(4, 3)");
+ RequestsMenu.swapItemsAtIndices(4, 3);
+ RequestsMenu.refreshZebra();
+ testContents([3, 4, 0, 1, 2]);
+
+ info("Testing swap(4, 4)");
+ RequestsMenu.swapItemsAtIndices(4, 4);
+ RequestsMenu.refreshZebra();
+ testContents([3, 4, 0, 1, 2]);
+
+ info("Clearing sort.");
+ RequestsMenu.sortBy();
+ testContents([0, 1, 2, 3, 4]);
+
+ return teardown(monitor);
+
+ function testContents([a, b, c, d, e]) {
+ is(RequestsMenu.items.length, 5,
+ "There should be a total of 5 items in the requests menu.");
+ is(RequestsMenu.visibleItems.length, 5,
+ "There should be a total of 5 visbile items in the requests menu.");
+ is($all(".side-menu-widget-item").length, 5,
+ "The visible items in the requests menu are, in fact, visible!");
+
+ is(RequestsMenu.getItemAtIndex(0), RequestsMenu.items[0],
+ "The requests menu items aren't ordered correctly. First item is misplaced.");
+ is(RequestsMenu.getItemAtIndex(1), RequestsMenu.items[1],
+ "The requests menu items aren't ordered correctly. Second item is misplaced.");
+ is(RequestsMenu.getItemAtIndex(2), RequestsMenu.items[2],
+ "The requests menu items aren't ordered correctly. Third item is misplaced.");
+ is(RequestsMenu.getItemAtIndex(3), RequestsMenu.items[3],
+ "The requests menu items aren't ordered correctly. Fourth item is misplaced.");
+ is(RequestsMenu.getItemAtIndex(4), RequestsMenu.items[4],
+ "The requests menu items aren't ordered correctly. Fifth item is misplaced.");
+
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(a),
+ "GET", STATUS_CODES_SJS + "?sts=100", {
+ status: 101,
+ statusText: "Switching Protocols",
+ type: "plain",
+ fullMimeType: "text/plain; charset=utf-8",
+ transferred: L10N.getStr("networkMenu.sizeUnavailable"),
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 0),
+ time: true
+ });
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(b),
+ "GET", STATUS_CODES_SJS + "?sts=200", {
+ status: 202,
+ statusText: "Created",
+ type: "plain",
+ fullMimeType: "text/plain; charset=utf-8",
+ transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 22),
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 22),
+ time: true
+ });
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(c),
+ "GET", STATUS_CODES_SJS + "?sts=300", {
+ status: 303,
+ statusText: "See Other",
+ type: "plain",
+ fullMimeType: "text/plain; charset=utf-8",
+ transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 22),
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 22),
+ time: true
+ });
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(d),
+ "GET", STATUS_CODES_SJS + "?sts=400", {
+ status: 404,
+ statusText: "Not Found",
+ type: "plain",
+ fullMimeType: "text/plain; charset=utf-8",
+ transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 22),
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 22),
+ time: true
+ });
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(e),
+ "GET", STATUS_CODES_SJS + "?sts=500", {
+ status: 501,
+ statusText: "Not Implemented",
+ type: "plain",
+ fullMimeType: "text/plain; charset=utf-8",
+ transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 22),
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 22),
+ time: true
+ });
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_sort-02.js b/devtools/client/netmonitor/test/browser_net_sort-02.js
new file mode 100644
index 000000000..ce8c69e45
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_sort-02.js
@@ -0,0 +1,272 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test if sorting columns in the network table works correctly.
+ */
+
+add_task(function* () {
+ let { L10N } = require("devtools/client/netmonitor/l10n");
+
+ let { monitor } = yield initNetMonitor(SORTING_URL);
+ info("Starting test... ");
+
+ // It seems that this test may be slow on debug builds. This could be because
+ // of the heavy dom manipulation associated with sorting.
+ requestLongerTimeout(2);
+
+ let { $, $all, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ // Loading the frame script and preparing the xhr request URLs so we can
+ // generate some requests later.
+ loadCommonFrameScript();
+ let requests = [{
+ url: "sjs_sorting-test-server.sjs?index=1&" + Math.random(),
+ method: "GET1"
+ }, {
+ url: "sjs_sorting-test-server.sjs?index=5&" + Math.random(),
+ method: "GET5"
+ }, {
+ url: "sjs_sorting-test-server.sjs?index=2&" + Math.random(),
+ method: "GET2"
+ }, {
+ url: "sjs_sorting-test-server.sjs?index=4&" + Math.random(),
+ method: "GET4"
+ }, {
+ url: "sjs_sorting-test-server.sjs?index=3&" + Math.random(),
+ method: "GET3"
+ }];
+
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, 5);
+ yield performRequestsInContent(requests);
+ yield wait;
+
+ EventUtils.sendMouseEvent({ type: "mousedown" }, $("#details-pane-toggle"));
+
+ isnot(RequestsMenu.selectedItem, null,
+ "There should be a selected item in the requests menu.");
+ is(RequestsMenu.selectedIndex, 0,
+ "The first item should be selected in the requests menu.");
+ is(NetMonitorView.detailsPaneHidden, false,
+ "The details pane should not be hidden after toggle button was pressed.");
+
+ testHeaders();
+ testContents([0, 2, 4, 3, 1]);
+
+ info("Testing status sort, ascending.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-status-button"));
+ testHeaders("status", "ascending");
+ testContents([0, 1, 2, 3, 4]);
+
+ info("Testing status sort, descending.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-status-button"));
+ testHeaders("status", "descending");
+ testContents([4, 3, 2, 1, 0]);
+
+ info("Testing status sort, ascending. Checking sort loops correctly.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-status-button"));
+ testHeaders("status", "ascending");
+ testContents([0, 1, 2, 3, 4]);
+
+ info("Testing method sort, ascending.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-method-button"));
+ testHeaders("method", "ascending");
+ testContents([0, 1, 2, 3, 4]);
+
+ info("Testing method sort, descending.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-method-button"));
+ testHeaders("method", "descending");
+ testContents([4, 3, 2, 1, 0]);
+
+ info("Testing method sort, ascending. Checking sort loops correctly.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-method-button"));
+ testHeaders("method", "ascending");
+ testContents([0, 1, 2, 3, 4]);
+
+ info("Testing file sort, ascending.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-file-button"));
+ testHeaders("file", "ascending");
+ testContents([0, 1, 2, 3, 4]);
+
+ info("Testing file sort, descending.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-file-button"));
+ testHeaders("file", "descending");
+ testContents([4, 3, 2, 1, 0]);
+
+ info("Testing file sort, ascending. Checking sort loops correctly.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-file-button"));
+ testHeaders("file", "ascending");
+ testContents([0, 1, 2, 3, 4]);
+
+ info("Testing type sort, ascending.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-type-button"));
+ testHeaders("type", "ascending");
+ testContents([0, 1, 2, 3, 4]);
+
+ info("Testing type sort, descending.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-type-button"));
+ testHeaders("type", "descending");
+ testContents([4, 3, 2, 1, 0]);
+
+ info("Testing type sort, ascending. Checking sort loops correctly.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-type-button"));
+ testHeaders("type", "ascending");
+ testContents([0, 1, 2, 3, 4]);
+
+ info("Testing transferred sort, ascending.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-transferred-button"));
+ testHeaders("transferred", "ascending");
+ testContents([0, 1, 2, 3, 4]);
+
+ info("Testing transferred sort, descending.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-transferred-button"));
+ testHeaders("transferred", "descending");
+ testContents([4, 3, 2, 1, 0]);
+
+ info("Testing transferred sort, ascending. Checking sort loops correctly.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-transferred-button"));
+ testHeaders("transferred", "ascending");
+ testContents([0, 1, 2, 3, 4]);
+
+ info("Testing size sort, ascending.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-size-button"));
+ testHeaders("size", "ascending");
+ testContents([0, 1, 2, 3, 4]);
+
+ info("Testing size sort, descending.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-size-button"));
+ testHeaders("size", "descending");
+ testContents([4, 3, 2, 1, 0]);
+
+ info("Testing size sort, ascending. Checking sort loops correctly.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-size-button"));
+ testHeaders("size", "ascending");
+ testContents([0, 1, 2, 3, 4]);
+
+ info("Testing waterfall sort, ascending.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-waterfall-button"));
+ testHeaders("waterfall", "ascending");
+ testContents([0, 2, 4, 3, 1]);
+
+ info("Testing waterfall sort, descending.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-waterfall-button"));
+ testHeaders("waterfall", "descending");
+ testContents([4, 2, 0, 1, 3]);
+
+ info("Testing waterfall sort, ascending. Checking sort loops correctly.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-waterfall-button"));
+ testHeaders("waterfall", "ascending");
+ testContents([0, 2, 4, 3, 1]);
+
+ return teardown(monitor);
+
+ function testHeaders(sortType, direction) {
+ let doc = monitor.panelWin.document;
+ let target = doc.querySelector("#requests-menu-" + sortType + "-button");
+ let headers = doc.querySelectorAll(".requests-menu-header-button");
+
+ for (let header of headers) {
+ if (header != target) {
+ is(header.hasAttribute("sorted"), false,
+ "The " + header.id + " header should not have a 'sorted' attribute.");
+ is(header.hasAttribute("tooltiptext"), false,
+ "The " + header.id + " header should not have a 'tooltiptext' attribute.");
+ } else {
+ is(header.getAttribute("sorted"), direction,
+ "The " + header.id + " header has an incorrect 'sorted' attribute.");
+ is(header.getAttribute("tooltiptext"), direction == "ascending"
+ ? L10N.getStr("networkMenu.sortedAsc")
+ : L10N.getStr("networkMenu.sortedDesc"),
+ "The " + header.id + " has an incorrect 'tooltiptext' attribute.");
+ }
+ }
+ }
+
+ function testContents([a, b, c, d, e]) {
+ isnot(RequestsMenu.selectedItem, null,
+ "There should still be a selected item after sorting.");
+ is(RequestsMenu.selectedIndex, a,
+ "The first item should be still selected after sorting.");
+ is(NetMonitorView.detailsPaneHidden, false,
+ "The details pane should still be visible after sorting.");
+
+ is(RequestsMenu.items.length, 5,
+ "There should be a total of 5 items in the requests menu.");
+ is(RequestsMenu.visibleItems.length, 5,
+ "There should be a total of 5 visbile items in the requests menu.");
+ is($all(".side-menu-widget-item").length, 5,
+ "The visible items in the requests menu are, in fact, visible!");
+
+ is(RequestsMenu.getItemAtIndex(0), RequestsMenu.items[0],
+ "The requests menu items aren't ordered correctly. First item is misplaced.");
+ is(RequestsMenu.getItemAtIndex(1), RequestsMenu.items[1],
+ "The requests menu items aren't ordered correctly. Second item is misplaced.");
+ is(RequestsMenu.getItemAtIndex(2), RequestsMenu.items[2],
+ "The requests menu items aren't ordered correctly. Third item is misplaced.");
+ is(RequestsMenu.getItemAtIndex(3), RequestsMenu.items[3],
+ "The requests menu items aren't ordered correctly. Fourth item is misplaced.");
+ is(RequestsMenu.getItemAtIndex(4), RequestsMenu.items[4],
+ "The requests menu items aren't ordered correctly. Fifth item is misplaced.");
+
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(a),
+ "GET1", SORTING_SJS + "?index=1", {
+ fuzzyUrl: true,
+ status: 101,
+ statusText: "Meh",
+ type: "1",
+ fullMimeType: "text/1",
+ transferred: L10N.getStr("networkMenu.sizeUnavailable"),
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 0),
+ time: true
+ });
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(b),
+ "GET2", SORTING_SJS + "?index=2", {
+ fuzzyUrl: true,
+ status: 200,
+ statusText: "Meh",
+ type: "2",
+ fullMimeType: "text/2",
+ transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 19),
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 19),
+ time: true
+ });
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(c),
+ "GET3", SORTING_SJS + "?index=3", {
+ fuzzyUrl: true,
+ status: 300,
+ statusText: "Meh",
+ type: "3",
+ fullMimeType: "text/3",
+ transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 29),
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 29),
+ time: true
+ });
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(d),
+ "GET4", SORTING_SJS + "?index=4", {
+ fuzzyUrl: true,
+ status: 400,
+ statusText: "Meh",
+ type: "4",
+ fullMimeType: "text/4",
+ transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 39),
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 39),
+ time: true
+ });
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(e),
+ "GET5", SORTING_SJS + "?index=5", {
+ fuzzyUrl: true,
+ status: 500,
+ statusText: "Meh",
+ type: "5",
+ fullMimeType: "text/5",
+ transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 49),
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 49),
+ time: true
+ });
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_sort-03.js b/devtools/client/netmonitor/test/browser_net_sort-03.js
new file mode 100644
index 000000000..ada0872a8
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_sort-03.js
@@ -0,0 +1,209 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test if sorting columns in the network table works correctly with new requests.
+ */
+
+add_task(function* () {
+ let { L10N } = require("devtools/client/netmonitor/l10n");
+
+ let { monitor } = yield initNetMonitor(SORTING_URL);
+ info("Starting test... ");
+
+ // It seems that this test may be slow on debug builds. This could be because
+ // of the heavy dom manipulation associated with sorting.
+ requestLongerTimeout(2);
+
+ let { $, $all, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ // Loading the frame script and preparing the xhr request URLs so we can
+ // generate some requests later.
+ loadCommonFrameScript();
+ let requests = [{
+ url: "sjs_sorting-test-server.sjs?index=1&" + Math.random(),
+ method: "GET1"
+ }, {
+ url: "sjs_sorting-test-server.sjs?index=5&" + Math.random(),
+ method: "GET5"
+ }, {
+ url: "sjs_sorting-test-server.sjs?index=2&" + Math.random(),
+ method: "GET2"
+ }, {
+ url: "sjs_sorting-test-server.sjs?index=4&" + Math.random(),
+ method: "GET4"
+ }, {
+ url: "sjs_sorting-test-server.sjs?index=3&" + Math.random(),
+ method: "GET3"
+ }];
+
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, 5);
+ yield performRequestsInContent(requests);
+ yield wait;
+
+ EventUtils.sendMouseEvent({ type: "mousedown" }, $("#details-pane-toggle"));
+
+ isnot(RequestsMenu.selectedItem, null,
+ "There should be a selected item in the requests menu.");
+ is(RequestsMenu.selectedIndex, 0,
+ "The first item should be selected in the requests menu.");
+ is(NetMonitorView.detailsPaneHidden, false,
+ "The details pane should not be hidden after toggle button was pressed.");
+
+ testHeaders();
+ testContents([0, 2, 4, 3, 1], 0);
+
+ info("Testing status sort, ascending.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-status-button"));
+ testHeaders("status", "ascending");
+ testContents([0, 1, 2, 3, 4], 0);
+
+ info("Performing more requests.");
+ wait = waitForNetworkEvents(monitor, 5);
+ yield performRequestsInContent(requests);
+ yield wait;
+
+ info("Testing status sort again, ascending.");
+ testHeaders("status", "ascending");
+ testContents([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 0);
+
+ info("Testing status sort, descending.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-status-button"));
+ testHeaders("status", "descending");
+ testContents([9, 8, 7, 6, 5, 4, 3, 2, 1, 0], 9);
+
+ info("Performing more requests.");
+ wait = waitForNetworkEvents(monitor, 5);
+ yield performRequestsInContent(requests);
+ yield wait;
+
+ info("Testing status sort again, descending.");
+ testHeaders("status", "descending");
+ testContents([14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0], 14);
+
+ info("Testing status sort yet again, ascending.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-status-button"));
+ testHeaders("status", "ascending");
+ testContents([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], 0);
+
+ info("Testing status sort yet again, descending.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-status-button"));
+ testHeaders("status", "descending");
+ testContents([14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0], 14);
+
+ return teardown(monitor);
+
+ function testHeaders(sortType, direction) {
+ let doc = monitor.panelWin.document;
+ let target = doc.querySelector("#requests-menu-" + sortType + "-button");
+ let headers = doc.querySelectorAll(".requests-menu-header-button");
+
+ for (let header of headers) {
+ if (header != target) {
+ is(header.hasAttribute("sorted"), false,
+ "The " + header.id + " header should not have a 'sorted' attribute.");
+ is(header.hasAttribute("tooltiptext"), false,
+ "The " + header.id + " header should not have a 'tooltiptext' attribute.");
+ } else {
+ is(header.getAttribute("sorted"), direction,
+ "The " + header.id + " header has an incorrect 'sorted' attribute.");
+ is(header.getAttribute("tooltiptext"), direction == "ascending"
+ ? L10N.getStr("networkMenu.sortedAsc")
+ : L10N.getStr("networkMenu.sortedDesc"),
+ "The " + header.id + " has an incorrect 'tooltiptext' attribute.");
+ }
+ }
+ }
+
+ function testContents(order, selection) {
+ isnot(RequestsMenu.selectedItem, null,
+ "There should still be a selected item after sorting.");
+ is(RequestsMenu.selectedIndex, selection,
+ "The first item should be still selected after sorting.");
+ is(NetMonitorView.detailsPaneHidden, false,
+ "The details pane should still be visible after sorting.");
+
+ is(RequestsMenu.items.length, order.length,
+ "There should be a specific number of items in the requests menu.");
+ is(RequestsMenu.visibleItems.length, order.length,
+ "There should be a specific number of visbile items in the requests menu.");
+ is($all(".side-menu-widget-item").length, order.length,
+ "The visible items in the requests menu are, in fact, visible!");
+
+ for (let i = 0; i < order.length; i++) {
+ is(RequestsMenu.getItemAtIndex(i), RequestsMenu.items[i],
+ "The requests menu items aren't ordered correctly. Misplaced item " + i + ".");
+ }
+
+ for (let i = 0, len = order.length / 5; i < len; i++) {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(order[i]),
+ "GET1", SORTING_SJS + "?index=1", {
+ fuzzyUrl: true,
+ status: 101,
+ statusText: "Meh",
+ type: "1",
+ fullMimeType: "text/1",
+ transferred: L10N.getStr("networkMenu.sizeUnavailable"),
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 0),
+ time: true
+ });
+ }
+ for (let i = 0, len = order.length / 5; i < len; i++) {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(order[i + len]),
+ "GET2", SORTING_SJS + "?index=2", {
+ fuzzyUrl: true,
+ status: 200,
+ statusText: "Meh",
+ type: "2",
+ fullMimeType: "text/2",
+ transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 19),
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 19),
+ time: true
+ });
+ }
+ for (let i = 0, len = order.length / 5; i < len; i++) {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(order[i + len * 2]),
+ "GET3", SORTING_SJS + "?index=3", {
+ fuzzyUrl: true,
+ status: 300,
+ statusText: "Meh",
+ type: "3",
+ fullMimeType: "text/3",
+ transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 29),
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 29),
+ time: true
+ });
+ }
+ for (let i = 0, len = order.length / 5; i < len; i++) {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(order[i + len * 3]),
+ "GET4", SORTING_SJS + "?index=4", {
+ fuzzyUrl: true,
+ status: 400,
+ statusText: "Meh",
+ type: "4",
+ fullMimeType: "text/4",
+ transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 39),
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 39),
+ time: true
+ });
+ }
+ for (let i = 0, len = order.length / 5; i < len; i++) {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(order[i + len * 4]),
+ "GET5", SORTING_SJS + "?index=5", {
+ fuzzyUrl: true,
+ status: 500,
+ statusText: "Meh",
+ type: "5",
+ fullMimeType: "text/5",
+ transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 49),
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 49),
+ time: true
+ });
+ }
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_statistics-01.js b/devtools/client/netmonitor/test/browser_net_statistics-01.js
new file mode 100644
index 000000000..d7e75b997
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_statistics-01.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if the statistics view is populated correctly.
+ */
+
+add_task(function* () {
+ let { monitor } = yield initNetMonitor(STATISTICS_URL);
+ info("Starting test... ");
+
+ let panel = monitor.panelWin;
+ let { $, $all, EVENTS, NetMonitorView } = panel;
+ is(NetMonitorView.currentFrontendMode, "network-inspector-view",
+ "The initial frontend mode is correct.");
+
+ is($("#primed-cache-chart").childNodes.length, 0,
+ "There should be no primed cache chart created yet.");
+ is($("#empty-cache-chart").childNodes.length, 0,
+ "There should be no empty cache chart created yet.");
+
+ let onChartDisplayed = Promise.all([
+ panel.once(EVENTS.PRIMED_CACHE_CHART_DISPLAYED),
+ panel.once(EVENTS.EMPTY_CACHE_CHART_DISPLAYED)
+ ]);
+ let onPlaceholderDisplayed = panel.once(EVENTS.PLACEHOLDER_CHARTS_DISPLAYED);
+
+ info("Displaying statistics view");
+ NetMonitorView.toggleFrontendMode();
+ is(NetMonitorView.currentFrontendMode, "network-statistics-view",
+ "The current frontend mode is correct.");
+
+ info("Waiting for placeholder to display");
+ yield onPlaceholderDisplayed;
+ is($("#primed-cache-chart").childNodes.length, 1,
+ "There should be a placeholder primed cache chart created now.");
+ is($("#empty-cache-chart").childNodes.length, 1,
+ "There should be a placeholder empty cache chart created now.");
+
+ is($all(".pie-chart-container[placeholder=true]").length, 2,
+ "Two placeholder pie chart appear to be rendered correctly.");
+ is($all(".table-chart-container[placeholder=true]").length, 2,
+ "Two placeholder table chart appear to be rendered correctly.");
+
+ info("Waiting for chart to display");
+ yield onChartDisplayed;
+ is($("#primed-cache-chart").childNodes.length, 1,
+ "There should be a real primed cache chart created now.");
+ is($("#empty-cache-chart").childNodes.length, 1,
+ "There should be a real empty cache chart created now.");
+
+ yield waitUntil(
+ () => $all(".pie-chart-container:not([placeholder=true])").length == 2);
+ ok(true, "Two real pie charts appear to be rendered correctly.");
+
+ yield waitUntil(
+ () => $all(".table-chart-container:not([placeholder=true])").length == 2);
+ ok(true, "Two real table charts appear to be rendered correctly.");
+
+ yield teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_statistics-02.js b/devtools/client/netmonitor/test/browser_net_statistics-02.js
new file mode 100644
index 000000000..361247e16
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_statistics-02.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if the network inspector view is shown when the target navigates
+ * away while in the statistics view.
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(STATISTICS_URL);
+ info("Starting test... ");
+
+ let panel = monitor.panelWin;
+ let { EVENTS, NetMonitorView } = panel;
+ is(NetMonitorView.currentFrontendMode, "network-inspector-view",
+ "The initial frontend mode is correct.");
+
+ let onChartDisplayed = Promise.all([
+ panel.once(EVENTS.PRIMED_CACHE_CHART_DISPLAYED),
+ panel.once(EVENTS.EMPTY_CACHE_CHART_DISPLAYED)
+ ]);
+
+ info("Displaying statistics view");
+ NetMonitorView.toggleFrontendMode();
+ yield onChartDisplayed;
+ is(NetMonitorView.currentFrontendMode, "network-statistics-view",
+ "The frontend mode is currently in the statistics view.");
+
+ info("Reloading page");
+ let onWillNavigate = panel.once(EVENTS.TARGET_WILL_NAVIGATE);
+ let onDidNavigate = panel.once(EVENTS.TARGET_DID_NAVIGATE);
+ tab.linkedBrowser.reload();
+ yield onWillNavigate;
+ is(NetMonitorView.currentFrontendMode, "network-inspector-view",
+ "The frontend mode switched back to the inspector view.");
+ yield onDidNavigate;
+ is(NetMonitorView.currentFrontendMode, "network-inspector-view",
+ "The frontend mode is still in the inspector view.");
+ yield teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_statistics-03.js b/devtools/client/netmonitor/test/browser_net_statistics-03.js
new file mode 100644
index 000000000..f3c6bf691
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_statistics-03.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test if the correct filtering predicates are used when filtering from
+ * the performance analysis view.
+ */
+
+add_task(function* () {
+ let { monitor } = yield initNetMonitor(FILTERING_URL);
+ info("Starting test... ");
+
+ let panel = monitor.panelWin;
+ let { $, EVENTS, NetMonitorView } = panel;
+
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-html-button"));
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-css-button"));
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-js-button"));
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-ws-button"));
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-other-button"));
+ testFilterButtonsCustom(monitor, [0, 1, 1, 1, 0, 0, 0, 0, 0, 1]);
+ info("The correct filtering predicates are used before entering perf. analysis mode.");
+
+ let onEvents = promise.all([
+ panel.once(EVENTS.PRIMED_CACHE_CHART_DISPLAYED),
+ panel.once(EVENTS.EMPTY_CACHE_CHART_DISPLAYED)
+ ]);
+ NetMonitorView.toggleFrontendMode();
+ yield onEvents;
+
+ is(NetMonitorView.currentFrontendMode, "network-statistics-view",
+ "The frontend mode is switched to the statistics view.");
+
+ EventUtils.sendMouseEvent({ type: "click" }, $(".pie-chart-slice"));
+
+ is(NetMonitorView.currentFrontendMode, "network-inspector-view",
+ "The frontend mode is switched back to the inspector view.");
+
+ testFilterButtons(monitor, "html");
+ info("The correct filtering predicate is used when exiting perf. analysis mode.");
+
+ yield teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_status-codes.js b/devtools/client/netmonitor/test/browser_net_status-codes.js
new file mode 100644
index 000000000..f38ee71e4
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_status-codes.js
@@ -0,0 +1,213 @@
+/* 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";
+
+/**
+ * Tests if requests display the correct status code and text in the UI.
+ */
+
+add_task(function* () {
+ let { L10N } = require("devtools/client/netmonitor/l10n");
+
+ let { tab, monitor } = yield initNetMonitor(STATUS_CODES_URL);
+
+ info("Starting test... ");
+
+ let { document, EVENTS, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu, NetworkDetails } = NetMonitorView;
+ let requestItems = [];
+
+ RequestsMenu.lazyUpdate = false;
+ NetworkDetails._params.lazyEmpty = false;
+
+ const REQUEST_DATA = [
+ {
+ // request #0
+ method: "GET",
+ uri: STATUS_CODES_SJS + "?sts=100",
+ details: {
+ status: 101,
+ statusText: "Switching Protocols",
+ type: "plain",
+ fullMimeType: "text/plain; charset=utf-8",
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 0),
+ time: true
+ }
+ },
+ {
+ // request #1
+ method: "GET",
+ uri: STATUS_CODES_SJS + "?sts=200",
+ details: {
+ status: 202,
+ statusText: "Created",
+ type: "plain",
+ fullMimeType: "text/plain; charset=utf-8",
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 22),
+ time: true
+ }
+ },
+ {
+ // request #2
+ method: "GET",
+ uri: STATUS_CODES_SJS + "?sts=300",
+ details: {
+ status: 303,
+ statusText: "See Other",
+ type: "plain",
+ fullMimeType: "text/plain; charset=utf-8",
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 22),
+ time: true
+ }
+ },
+ {
+ // request #3
+ method: "GET",
+ uri: STATUS_CODES_SJS + "?sts=400",
+ details: {
+ status: 404,
+ statusText: "Not Found",
+ type: "plain",
+ fullMimeType: "text/plain; charset=utf-8",
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 22),
+ time: true
+ }
+ },
+ {
+ // request #4
+ method: "GET",
+ uri: STATUS_CODES_SJS + "?sts=500",
+ details: {
+ status: 501,
+ statusText: "Not Implemented",
+ type: "plain",
+ fullMimeType: "text/plain; charset=utf-8",
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 22),
+ time: true
+ }
+ }
+ ];
+
+ let wait = waitForNetworkEvents(monitor, 5);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests();
+ });
+ yield wait;
+
+ info("Performing tests");
+ yield verifyRequests();
+ yield testTab(0, testSummary);
+ yield testTab(2, testParams);
+
+ return teardown(monitor);
+
+ /**
+ * A helper that verifies all requests show the correct information and caches
+ * RequestsMenu items to requestItems array.
+ */
+ function* verifyRequests() {
+ info("Verifying requests contain correct information.");
+ let index = 0;
+ for (let request of REQUEST_DATA) {
+ let item = RequestsMenu.getItemAtIndex(index);
+ requestItems[index] = item;
+
+ info("Verifying request #" + index);
+ yield verifyRequestItemTarget(item, request.method, request.uri, request.details);
+
+ index++;
+ }
+ }
+
+ /**
+ * A helper that opens a given tab of request details pane, selects and passes
+ * all requests to the given test function.
+ *
+ * @param Number tabIdx
+ * The index of NetworkDetails tab to activate.
+ * @param Function testFn(requestItem)
+ * A function that should perform all necessary tests. It's called once
+ * for every item of REQUEST_DATA with that item being selected in the
+ * NetworkMonitor.
+ */
+ function* testTab(tabIdx, testFn) {
+ info("Testing tab #" + tabIdx);
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.querySelectorAll("#details-pane tab")[tabIdx]);
+
+ let counter = 0;
+ for (let item of REQUEST_DATA) {
+ info("Waiting tab #" + tabIdx + " to update with request #" + counter);
+ yield chooseRequest(counter);
+
+ info("Tab updated. Performing checks");
+ yield testFn(item);
+
+ counter++;
+ }
+ }
+
+ /**
+ * A function that tests "Summary" contains correct information.
+ */
+ function* testSummary(data) {
+ let tabpanel = document.querySelectorAll("#details-pane tabpanel")[0];
+
+ let { method, uri, details: { status, statusText } } = data;
+ is(tabpanel.querySelector("#headers-summary-url-value").getAttribute("value"),
+ uri, "The url summary value is incorrect.");
+ is(tabpanel.querySelector("#headers-summary-method-value").getAttribute("value"),
+ method, "The method summary value is incorrect.");
+ is(tabpanel.querySelector("#headers-summary-status-circle").getAttribute("code"),
+ status, "The status summary code is incorrect.");
+ is(tabpanel.querySelector("#headers-summary-status-value").getAttribute("value"),
+ status + " " + statusText, "The status summary value is incorrect.");
+ }
+
+ /**
+ * A function that tests "Params" tab contains correct information.
+ */
+ function* testParams(data) {
+ let tabpanel = document.querySelectorAll("#details-pane tabpanel")[2];
+ let statusParamValue = data.uri.split("=").pop();
+ let statusParamShownValue = "\"" + statusParamValue + "\"";
+
+ is(tabpanel.querySelectorAll(".variables-view-scope").length, 1,
+ "There should be 1 param scope displayed in this tabpanel.");
+ is(tabpanel.querySelectorAll(".variable-or-property").length, 1,
+ "There should be 1 param value displayed in this tabpanel.");
+ is(tabpanel.querySelectorAll(".variables-view-empty-notice").length, 0,
+ "The empty notice should not be displayed in this tabpanel.");
+
+ let paramsScope = tabpanel.querySelectorAll(".variables-view-scope")[0];
+
+ is(paramsScope.querySelector(".name").getAttribute("value"),
+ L10N.getStr("paramsQueryString"),
+ "The params scope doesn't have the correct title.");
+
+ is(paramsScope.querySelectorAll(".variables-view-variable .name")[0]
+ .getAttribute("value"),
+ "sts", "The param name was incorrect.");
+ is(paramsScope.querySelectorAll(".variables-view-variable .value")[0]
+ .getAttribute("value"),
+ statusParamShownValue, "The param value was incorrect.");
+
+ is(tabpanel.querySelector("#request-params-box")
+ .hasAttribute("hidden"), false,
+ "The request params box should not be hidden.");
+ is(tabpanel.querySelector("#request-post-data-textarea-box")
+ .hasAttribute("hidden"), true,
+ "The request post data textarea box should be hidden.");
+ }
+
+ /**
+ * A helper that clicks on a specified request and returns a promise resolved
+ * when NetworkDetails has been populated with the data of the given request.
+ */
+ function chooseRequest(index) {
+ let onTabUpdated = monitor.panelWin.once(EVENTS.TAB_UPDATED);
+ EventUtils.sendMouseEvent({ type: "mousedown" }, requestItems[index].target);
+ return onTabUpdated;
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_streaming-response.js b/devtools/client/netmonitor/test/browser_net_streaming-response.js
new file mode 100644
index 000000000..49a75ec32
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_streaming-response.js
@@ -0,0 +1,69 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if reponses from streaming content types (MPEG-DASH, HLS) are
+ * displayed as XML or plain text
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(CUSTOM_GET_URL);
+
+ info("Starting test... ");
+ let { panelWin } = monitor;
+ let { document, Editor, NetMonitorView } = panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ const REQUESTS = [
+ [ "hls-m3u8", /^#EXTM3U/, Editor.modes.text ],
+ [ "mpeg-dash", /^<\?xml/, Editor.modes.html ]
+ ];
+
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, REQUESTS.length);
+ for (let [fmt] of REQUESTS) {
+ let url = CONTENT_TYPE_SJS + "?fmt=" + fmt;
+ yield ContentTask.spawn(tab.linkedBrowser, { url }, function* (args) {
+ content.wrappedJSObject.performRequests(1, args.url);
+ });
+ }
+ yield wait;
+
+ REQUESTS.forEach(([ fmt ], i) => {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(i),
+ "GET", CONTENT_TYPE_SJS + "?fmt=" + fmt, {
+ status: 200,
+ statusText: "OK"
+ });
+ });
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.getElementById("details-pane-toggle"));
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.querySelectorAll("#details-pane tab")[3]);
+
+ yield panelWin.once(panelWin.EVENTS.RESPONSE_BODY_DISPLAYED);
+ let editor = yield NetMonitorView.editor("#response-content-textarea");
+
+ // the hls-m3u8 part
+ testEditorContent(editor, REQUESTS[0]);
+
+ RequestsMenu.selectedIndex = 1;
+ yield panelWin.once(panelWin.EVENTS.TAB_UPDATED);
+ yield panelWin.once(panelWin.EVENTS.RESPONSE_BODY_DISPLAYED);
+
+ // the mpeg-dash part
+ testEditorContent(editor, REQUESTS[1]);
+
+ return teardown(monitor);
+
+ function testEditorContent(e, [ fmt, textRe, mode ]) {
+ ok(e.getText().match(textRe),
+ "The text shown in the source editor for " + fmt + " is correct.");
+ is(e.getMode(), mode,
+ "The mode active in the source editor for " + fmt + " is correct.");
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_throttle.js b/devtools/client/netmonitor/test/browser_net_throttle.js
new file mode 100644
index 000000000..c1e7723b8
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_throttle.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Network throttling integration test.
+
+"use strict";
+
+add_task(function* () {
+ yield throttleTest(true);
+ yield throttleTest(false);
+});
+
+function* throttleTest(actuallyThrottle) {
+ requestLongerTimeout(2);
+
+ let { monitor } = yield initNetMonitor(SIMPLE_URL);
+ const {ACTIVITY_TYPE, EVENTS, NetMonitorController, NetMonitorView} = monitor.panelWin;
+
+ info("Starting test... (actuallyThrottle = " + actuallyThrottle + ")");
+
+ // When throttling, must be smaller than the length of the content
+ // of SIMPLE_URL in bytes.
+ const size = actuallyThrottle ? 200 : 0;
+
+ const request = {
+ "NetworkMonitor.throttleData": {
+ roundTripTimeMean: 0,
+ roundTripTimeMax: 0,
+ downloadBPSMean: size,
+ downloadBPSMax: size,
+ uploadBPSMean: 10000,
+ uploadBPSMax: 10000,
+ },
+ };
+ let client = monitor._controller.webConsoleClient;
+
+ info("sending throttle request");
+ let deferred = promise.defer();
+ client.setPreferences(request, response => {
+ deferred.resolve(response);
+ });
+ yield deferred.promise;
+
+ let eventPromise = monitor.panelWin.once(EVENTS.RECEIVED_EVENT_TIMINGS);
+ yield NetMonitorController.triggerActivity(ACTIVITY_TYPE.RELOAD.WITH_CACHE_DISABLED);
+ yield eventPromise;
+
+ let requestItem = NetMonitorView.RequestsMenu.getItemAtIndex(0);
+ const reportedOneSecond = requestItem.attachment.eventTimings.timings.receive > 1000;
+ if (actuallyThrottle) {
+ ok(reportedOneSecond, "download reported as taking more than one second");
+ } else {
+ ok(!reportedOneSecond, "download reported as taking less than one second");
+ }
+
+ yield teardown(monitor);
+}
diff --git a/devtools/client/netmonitor/test/browser_net_timeline_ticks.js b/devtools/client/netmonitor/test/browser_net_timeline_ticks.js
new file mode 100644
index 000000000..2aafcb98d
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_timeline_ticks.js
@@ -0,0 +1,142 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if timeline correctly displays interval divisions.
+ */
+
+add_task(function* () {
+ let { L10N } = require("devtools/client/netmonitor/l10n");
+
+ let { tab, monitor } = yield initNetMonitor(SIMPLE_URL);
+ info("Starting test... ");
+
+ let { $, $all, NetMonitorView, NetMonitorController } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ // Disable transferred size column support for this test.
+ // Without this, the waterfall only has enough room for one division, which
+ // would remove most of the value of this test.
+ $("#requests-menu-transferred-header-box").hidden = true;
+ $("#requests-menu-item-template .requests-menu-transferred").hidden = true;
+
+ RequestsMenu.lazyUpdate = false;
+
+ ok($("#requests-menu-waterfall-label"),
+ "An timeline label should be displayed when the frontend is opened.");
+ ok($all(".requests-menu-timings-division").length == 0,
+ "No tick labels should be displayed when the frontend is opened.");
+
+ ok(!RequestsMenu._canvas, "No canvas should be created when the frontend is opened.");
+ ok(!RequestsMenu._ctx, "No 2d context should be created when the frontend is opened.");
+
+ let wait = waitForNetworkEvents(monitor, 1);
+ tab.linkedBrowser.reload();
+ yield wait;
+
+ // Make sure the DOMContentLoaded and load markers don't interfere with
+ // this test by removing them and redrawing the waterfall (bug 1224088).
+ NetMonitorController.NetworkEventsHandler.clearMarkers();
+ RequestsMenu._flushWaterfallViews(true);
+
+ ok(!$("#requests-menu-waterfall-label"),
+ "The timeline label should be hidden after the first request.");
+ ok($all(".requests-menu-timings-division").length >= 3,
+ "There should be at least 3 tick labels in the network requests header.");
+
+ is($all(".requests-menu-timings-division")[0].getAttribute("value"),
+ L10N.getFormatStr("networkMenu.millisecond", 0),
+ "The first tick label has an incorrect value");
+ is($all(".requests-menu-timings-division")[1].getAttribute("value"),
+ L10N.getFormatStr("networkMenu.millisecond", 80),
+ "The second tick label has an incorrect value");
+ is($all(".requests-menu-timings-division")[2].getAttribute("value"),
+ L10N.getFormatStr("networkMenu.millisecond", 160),
+ "The third tick label has an incorrect value");
+
+ is($all(".requests-menu-timings-division")[0].style.transform, "translateX(0px)",
+ "The first tick label has an incorrect translation");
+ is($all(".requests-menu-timings-division")[1].style.transform, "translateX(80px)",
+ "The second tick label has an incorrect translation");
+ is($all(".requests-menu-timings-division")[2].style.transform, "translateX(160px)",
+ "The third tick label has an incorrect translation");
+
+ ok(RequestsMenu._canvas, "A canvas should be created after the first request.");
+ ok(RequestsMenu._ctx, "A 2d context should be created after the first request.");
+
+ let imageData = RequestsMenu._ctx.getImageData(0, 0, 161, 1);
+ ok(imageData, "The image data should have been created.");
+
+ let data = imageData.data;
+ ok(data, "The image data should contain a pixel array.");
+
+ ok(hasPixelAt(0), "The tick at 0 is should not be empty.");
+ ok(!hasPixelAt(1), "The tick at 1 is should be empty.");
+ ok(!hasPixelAt(19), "The tick at 19 is should be empty.");
+ ok(hasPixelAt(20), "The tick at 20 is should not be empty.");
+ ok(!hasPixelAt(21), "The tick at 21 is should be empty.");
+ ok(!hasPixelAt(39), "The tick at 39 is should be empty.");
+ ok(hasPixelAt(40), "The tick at 40 is should not be empty.");
+ ok(!hasPixelAt(41), "The tick at 41 is should be empty.");
+ ok(!hasPixelAt(59), "The tick at 59 is should be empty.");
+ ok(hasPixelAt(60), "The tick at 60 is should not be empty.");
+ ok(!hasPixelAt(61), "The tick at 61 is should be empty.");
+ ok(!hasPixelAt(79), "The tick at 79 is should be empty.");
+ ok(hasPixelAt(80), "The tick at 80 is should not be empty.");
+ ok(!hasPixelAt(81), "The tick at 81 is should be empty.");
+ ok(!hasPixelAt(159), "The tick at 159 is should be empty.");
+ ok(hasPixelAt(160), "The tick at 160 is should not be empty.");
+ ok(!hasPixelAt(161), "The tick at 161 is should be empty.");
+
+ ok(isPixelBrighterAtThan(0, 20),
+ "The tick at 0 should be brighter than the one at 20");
+ ok(isPixelBrighterAtThan(40, 20),
+ "The tick at 40 should be brighter than the one at 20");
+ ok(isPixelBrighterAtThan(40, 60),
+ "The tick at 40 should be brighter than the one at 60");
+ ok(isPixelBrighterAtThan(80, 60),
+ "The tick at 80 should be brighter than the one at 60");
+
+ ok(isPixelBrighterAtThan(80, 100),
+ "The tick at 80 should be brighter than the one at 100");
+ ok(isPixelBrighterAtThan(120, 100),
+ "The tick at 120 should be brighter than the one at 100");
+ ok(isPixelBrighterAtThan(120, 140),
+ "The tick at 120 should be brighter than the one at 140");
+ ok(isPixelBrighterAtThan(160, 140),
+ "The tick at 160 should be brighter than the one at 140");
+
+ ok(isPixelEquallyBright(20, 60),
+ "The tick at 20 should be equally bright to the one at 60");
+ ok(isPixelEquallyBright(100, 140),
+ "The tick at 100 should be equally bright to the one at 140");
+
+ ok(isPixelEquallyBright(40, 120),
+ "The tick at 40 should be equally bright to the one at 120");
+
+ ok(isPixelEquallyBright(0, 80),
+ "The tick at 80 should be equally bright to the one at 160");
+ ok(isPixelEquallyBright(80, 160),
+ "The tick at 80 should be equally bright to the one at 160");
+
+ function hasPixelAt(x) {
+ let i = (x | 0) * 4;
+ return data[i] && data[i + 1] && data[i + 2] && data[i + 3];
+ }
+
+ function isPixelBrighterAtThan(x1, x2) {
+ let i = (x1 | 0) * 4;
+ let j = (x2 | 0) * 4;
+ return data[i + 3] > data [j + 3];
+ }
+
+ function isPixelEquallyBright(x1, x2) {
+ let i = (x1 | 0) * 4;
+ let j = (x2 | 0) * 4;
+ return data[i + 3] == data [j + 3];
+ }
+
+ return teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_timing-division.js b/devtools/client/netmonitor/test/browser_net_timing-division.js
new file mode 100644
index 000000000..0114ba235
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_timing-division.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if timing intervals are divided againts seconds when appropriate.
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(CUSTOM_GET_URL);
+ info("Starting test... ");
+
+ let { $all, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, 2);
+ // Timeout needed for having enough divisions on the time scale.
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests(2, null, 3000);
+ });
+ yield wait;
+
+ let milDivs = $all(".requests-menu-timings-division[division-scale=millisecond]");
+ let secDivs = $all(".requests-menu-timings-division[division-scale=second]");
+ let minDivs = $all(".requests-menu-timings-division[division-scale=minute]");
+
+ info("Number of millisecond divisions: " + milDivs.length);
+ info("Number of second divisions: " + secDivs.length);
+ info("Number of minute divisions: " + minDivs.length);
+
+ for (let div of milDivs) {
+ info("Millisecond division: " + div.getAttribute("value"));
+ }
+ for (let div of secDivs) {
+ info("Second division: " + div.getAttribute("value"));
+ }
+ for (let div of minDivs) {
+ info("Minute division: " + div.getAttribute("value"));
+ }
+
+ is(RequestsMenu.itemCount, 2,
+ "There should be only two requests made.");
+
+ let firstRequest = RequestsMenu.getItemAtIndex(0);
+ let lastRequest = RequestsMenu.getItemAtIndex(1);
+
+ info("First request happened at: " +
+ firstRequest.attachment.responseHeaders.headers.find(e => e.name == "Date").value);
+ info("Last request happened at: " +
+ lastRequest.attachment.responseHeaders.headers.find(e => e.name == "Date").value);
+
+ ok(secDivs.length,
+ "There should be at least one division on the seconds time scale.");
+ ok(secDivs[0].getAttribute("value").match(/\d+\.\d{2}\s\w+/),
+ "The division on the seconds time scale looks legit.");
+
+ return teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_truncate.js b/devtools/client/netmonitor/test/browser_net_truncate.js
new file mode 100644
index 000000000..bfb5c896d
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_truncate.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Verifies that truncated response bodies still have the correct reported size.
+ */
+
+function test() {
+ let { L10N } = require("devtools/client/netmonitor/l10n");
+ const { RESPONSE_BODY_LIMIT } = require("devtools/shared/webconsole/network-monitor");
+
+ const URL = EXAMPLE_URL + "sjs_truncate-test-server.sjs?limit=" + RESPONSE_BODY_LIMIT;
+
+ // Another slow test on Linux debug.
+ requestLongerTimeout(2);
+
+ initNetMonitor(URL).then(({ tab, monitor }) => {
+ info("Starting test... ");
+
+ let { NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ waitForNetworkEvents(monitor, 1)
+ .then(() => teardown(monitor))
+ .then(finish);
+
+ monitor.panelWin.once(monitor.panelWin.EVENTS.RECEIVED_RESPONSE_CONTENT, () => {
+ let requestItem = RequestsMenu.getItemAtIndex(0);
+
+ verifyRequestItemTarget(RequestsMenu, requestItem, "GET", URL, {
+ type: "plain",
+ fullMimeType: "text/plain; charset=utf-8",
+ transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeMB", 2),
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeMB", 2),
+ });
+ });
+
+ tab.linkedBrowser.reload();
+ });
+}
diff --git a/devtools/client/netmonitor/test/dropmarker.svg b/devtools/client/netmonitor/test/dropmarker.svg
new file mode 100644
index 000000000..3e2987682
--- /dev/null
+++ b/devtools/client/netmonitor/test/dropmarker.svg
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="8" height="4" viewBox="0 0 8 4">
+ <polygon points="0,0 4,4 8,0" fill="#b6babf"/>
+</svg>
diff --git a/devtools/client/netmonitor/test/head.js b/devtools/client/netmonitor/test/head.js
new file mode 100644
index 000000000..d733cc1d4
--- /dev/null
+++ b/devtools/client/netmonitor/test/head.js
@@ -0,0 +1,518 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/* import-globals-from ../../framework/test/shared-head.js */
+
+// shared-head.js handles imports, constants, and utility functions
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js",
+ this);
+
+var NetworkHelper = require("devtools/shared/webconsole/network-helper");
+var { Toolbox } = require("devtools/client/framework/toolbox");
+
+const EXAMPLE_URL = "http://example.com/browser/devtools/client/netmonitor/test/";
+const HTTPS_EXAMPLE_URL = "https://example.com/browser/devtools/client/netmonitor/test/";
+
+const API_CALLS_URL = EXAMPLE_URL + "html_api-calls-test-page.html";
+const SIMPLE_URL = EXAMPLE_URL + "html_simple-test-page.html";
+const NAVIGATE_URL = EXAMPLE_URL + "html_navigate-test-page.html";
+const CONTENT_TYPE_URL = EXAMPLE_URL + "html_content-type-test-page.html";
+const CONTENT_TYPE_WITHOUT_CACHE_URL = EXAMPLE_URL + "html_content-type-without-cache-test-page.html";
+const CONTENT_TYPE_WITHOUT_CACHE_REQUESTS = 8;
+const CYRILLIC_URL = EXAMPLE_URL + "html_cyrillic-test-page.html";
+const STATUS_CODES_URL = EXAMPLE_URL + "html_status-codes-test-page.html";
+const POST_DATA_URL = EXAMPLE_URL + "html_post-data-test-page.html";
+const POST_JSON_URL = EXAMPLE_URL + "html_post-json-test-page.html";
+const POST_RAW_URL = EXAMPLE_URL + "html_post-raw-test-page.html";
+const POST_RAW_WITH_HEADERS_URL = EXAMPLE_URL + "html_post-raw-with-headers-test-page.html";
+const PARAMS_URL = EXAMPLE_URL + "html_params-test-page.html";
+const JSONP_URL = EXAMPLE_URL + "html_jsonp-test-page.html";
+const JSON_LONG_URL = EXAMPLE_URL + "html_json-long-test-page.html";
+const JSON_MALFORMED_URL = EXAMPLE_URL + "html_json-malformed-test-page.html";
+const JSON_CUSTOM_MIME_URL = EXAMPLE_URL + "html_json-custom-mime-test-page.html";
+const JSON_TEXT_MIME_URL = EXAMPLE_URL + "html_json-text-mime-test-page.html";
+const SORTING_URL = EXAMPLE_URL + "html_sorting-test-page.html";
+const FILTERING_URL = EXAMPLE_URL + "html_filter-test-page.html";
+const INFINITE_GET_URL = EXAMPLE_URL + "html_infinite-get-page.html";
+const CUSTOM_GET_URL = EXAMPLE_URL + "html_custom-get-page.html";
+const SINGLE_GET_URL = EXAMPLE_URL + "html_single-get-page.html";
+const STATISTICS_URL = EXAMPLE_URL + "html_statistics-test-page.html";
+const CURL_URL = EXAMPLE_URL + "html_copy-as-curl.html";
+const CURL_UTILS_URL = EXAMPLE_URL + "html_curl-utils.html";
+const SEND_BEACON_URL = EXAMPLE_URL + "html_send-beacon.html";
+const CORS_URL = EXAMPLE_URL + "html_cors-test-page.html";
+
+const SIMPLE_SJS = EXAMPLE_URL + "sjs_simple-test-server.sjs";
+const CONTENT_TYPE_SJS = EXAMPLE_URL + "sjs_content-type-test-server.sjs";
+const HTTPS_CONTENT_TYPE_SJS = HTTPS_EXAMPLE_URL + "sjs_content-type-test-server.sjs";
+const STATUS_CODES_SJS = EXAMPLE_URL + "sjs_status-codes-test-server.sjs";
+const SORTING_SJS = EXAMPLE_URL + "sjs_sorting-test-server.sjs";
+const HTTPS_REDIRECT_SJS = EXAMPLE_URL + "sjs_https-redirect-test-server.sjs";
+const CORS_SJS_PATH = "/browser/devtools/client/netmonitor/test/sjs_cors-test-server.sjs";
+const HSTS_SJS = EXAMPLE_URL + "sjs_hsts-test-server.sjs";
+
+const HSTS_BASE_URL = EXAMPLE_URL;
+const HSTS_PAGE_URL = CUSTOM_GET_URL;
+
+const TEST_IMAGE = EXAMPLE_URL + "test-image.png";
+const TEST_IMAGE_DATA_URI = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==";
+
+const FRAME_SCRIPT_UTILS_URL = "chrome://devtools/content/shared/frame-script-utils.js";
+
+// All tests are asynchronous.
+waitForExplicitFinish();
+
+const gEnableLogging = Services.prefs.getBoolPref("devtools.debugger.log");
+// To enable logging for try runs, just set the pref to true.
+Services.prefs.setBoolPref("devtools.debugger.log", false);
+
+// Uncomment this pref to dump all devtools emitted events to the console.
+// Services.prefs.setBoolPref("devtools.dump.emit", true);
+
+// Always reset some prefs to their original values after the test finishes.
+const gDefaultFilters = Services.prefs.getCharPref("devtools.netmonitor.filters");
+
+registerCleanupFunction(() => {
+ info("finish() was called, cleaning up...");
+
+ Services.prefs.setBoolPref("devtools.debugger.log", gEnableLogging);
+ Services.prefs.setCharPref("devtools.netmonitor.filters", gDefaultFilters);
+ Services.prefs.clearUserPref("devtools.cache.disabled");
+});
+
+function waitForNavigation(aTarget) {
+ let deferred = promise.defer();
+ aTarget.once("will-navigate", () => {
+ aTarget.once("navigate", () => {
+ deferred.resolve();
+ });
+ });
+ return deferred.promise;
+}
+
+function reconfigureTab(aTarget, aOptions) {
+ let deferred = promise.defer();
+ aTarget.activeTab.reconfigure(aOptions, deferred.resolve);
+ return deferred.promise;
+}
+
+function toggleCache(aTarget, aDisabled) {
+ let options = { cacheDisabled: aDisabled, performReload: true };
+ let navigationFinished = waitForNavigation(aTarget);
+
+ // Disable the cache for any toolbox that it is opened from this point on.
+ Services.prefs.setBoolPref("devtools.cache.disabled", aDisabled);
+
+ return reconfigureTab(aTarget, options).then(() => navigationFinished);
+}
+
+function initNetMonitor(aUrl, aWindow, aEnableCache) {
+ info("Initializing a network monitor pane.");
+
+ return Task.spawn(function* () {
+ let tab = yield addTab(aUrl);
+ info("Net tab added successfully: " + aUrl);
+
+ let target = TargetFactory.forTab(tab);
+
+ yield target.makeRemote();
+ info("Target remoted.");
+
+ if (!aEnableCache) {
+ info("Disabling cache and reloading page.");
+ yield toggleCache(target, true);
+ info("Cache disabled when the current and all future toolboxes are open.");
+ // Remove any requests generated by the reload while toggling the cache to
+ // avoid interfering with the test.
+ isnot([...target.activeConsole.getNetworkEvents()].length, 0,
+ "Request to reconfigure the tab was recorded.");
+ target.activeConsole.clearNetworkRequests();
+ }
+
+ let toolbox = yield gDevTools.showToolbox(target, "netmonitor");
+ info("Network monitor pane shown successfully.");
+
+ let monitor = toolbox.getCurrentPanel();
+ return {tab, monitor};
+ });
+}
+
+function restartNetMonitor(monitor, newUrl) {
+ info("Restarting the specified network monitor.");
+
+ return Task.spawn(function* () {
+ let tab = monitor.target.tab;
+ let url = newUrl || tab.linkedBrowser.currentURI.spec;
+
+ let onDestroyed = monitor.once("destroyed");
+ yield removeTab(tab);
+ yield onDestroyed;
+
+ return initNetMonitor(url);
+ });
+}
+
+function teardown(monitor) {
+ info("Destroying the specified network monitor.");
+
+ return Task.spawn(function* () {
+ let tab = monitor.target.tab;
+
+ let onDestroyed = monitor.once("destroyed");
+ yield removeTab(tab);
+ yield onDestroyed;
+ });
+}
+
+function waitForNetworkEvents(aMonitor, aGetRequests, aPostRequests = 0) {
+ let deferred = promise.defer();
+
+ let panel = aMonitor.panelWin;
+ let events = panel.EVENTS;
+
+ let progress = {};
+ let genericEvents = 0;
+ let postEvents = 0;
+
+ let awaitedEventsToListeners = [
+ ["UPDATING_REQUEST_HEADERS", onGenericEvent],
+ ["RECEIVED_REQUEST_HEADERS", onGenericEvent],
+ ["UPDATING_REQUEST_COOKIES", onGenericEvent],
+ ["RECEIVED_REQUEST_COOKIES", onGenericEvent],
+ ["UPDATING_REQUEST_POST_DATA", onPostEvent],
+ ["RECEIVED_REQUEST_POST_DATA", onPostEvent],
+ ["UPDATING_RESPONSE_HEADERS", onGenericEvent],
+ ["RECEIVED_RESPONSE_HEADERS", onGenericEvent],
+ ["UPDATING_RESPONSE_COOKIES", onGenericEvent],
+ ["RECEIVED_RESPONSE_COOKIES", onGenericEvent],
+ ["STARTED_RECEIVING_RESPONSE", onGenericEvent],
+ ["UPDATING_RESPONSE_CONTENT", onGenericEvent],
+ ["RECEIVED_RESPONSE_CONTENT", onGenericEvent],
+ ["UPDATING_EVENT_TIMINGS", onGenericEvent],
+ ["RECEIVED_EVENT_TIMINGS", onGenericEvent]
+ ];
+
+ function initProgressForURL(url) {
+ if (progress[url]) return;
+ progress[url] = {};
+ awaitedEventsToListeners.forEach(([e]) => progress[url][e] = 0);
+ }
+
+ function updateProgressForURL(url, event) {
+ initProgressForURL(url);
+ progress[url][Object.keys(events).find(e => events[e] == event)] = 1;
+ }
+
+ function onGenericEvent(event, actor) {
+ genericEvents++;
+ maybeResolve(event, actor);
+ }
+
+ function onPostEvent(event, actor) {
+ postEvents++;
+ maybeResolve(event, actor);
+ }
+
+ function maybeResolve(event, actor) {
+ info("> Network events progress: " +
+ genericEvents + "/" + ((aGetRequests + aPostRequests) * 13) + ", " +
+ postEvents + "/" + (aPostRequests * 2) + ", " +
+ "got " + event + " for " + actor);
+
+ let networkInfo =
+ panel.NetMonitorController.webConsoleClient.getNetworkRequest(actor);
+ let url = networkInfo.request.url;
+ updateProgressForURL(url, event);
+
+ // Uncomment this to get a detailed progress logging (when debugging a test)
+ // info("> Current state: " + JSON.stringify(progress, null, 2));
+
+ // There are 15 updates which need to be fired for a request to be
+ // considered finished. The "requestPostData" packet isn't fired for
+ // non-POST requests.
+ if (genericEvents >= (aGetRequests + aPostRequests) * 13 &&
+ postEvents >= aPostRequests * 2) {
+
+ awaitedEventsToListeners.forEach(([e, l]) => panel.off(events[e], l));
+ executeSoon(deferred.resolve);
+ }
+ }
+
+ awaitedEventsToListeners.forEach(([e, l]) => panel.on(events[e], l));
+ return deferred.promise;
+}
+
+function verifyRequestItemTarget(aRequestItem, aMethod, aUrl, aData = {}) {
+ info("> Verifying: " + aMethod + " " + aUrl + " " + aData.toSource());
+ // This bloats log sizes significantly in automation (bug 992485)
+ // info("> Request: " + aRequestItem.attachment.toSource());
+
+ let requestsMenu = aRequestItem.ownerView;
+ let widgetIndex = requestsMenu.indexOfItem(aRequestItem);
+ let visibleIndex = requestsMenu.visibleItems.indexOf(aRequestItem);
+
+ info("Widget index of item: " + widgetIndex);
+ info("Visible index of item: " + visibleIndex);
+
+ let { fuzzyUrl, status, statusText, cause, type, fullMimeType,
+ transferred, size, time, displayedStatus } = aData;
+ let { attachment, target } = aRequestItem;
+
+ let uri = Services.io.newURI(aUrl, null, null).QueryInterface(Ci.nsIURL);
+ let unicodeUrl = NetworkHelper.convertToUnicode(unescape(aUrl));
+ let name = NetworkHelper.convertToUnicode(unescape(uri.fileName || uri.filePath || "/"));
+ let query = NetworkHelper.convertToUnicode(unescape(uri.query));
+ let hostPort = uri.hostPort;
+ let remoteAddress = attachment.remoteAddress;
+
+ if (fuzzyUrl) {
+ ok(attachment.method.startsWith(aMethod), "The attached method is correct.");
+ ok(attachment.url.startsWith(aUrl), "The attached url is correct.");
+ } else {
+ is(attachment.method, aMethod, "The attached method is correct.");
+ is(attachment.url, aUrl, "The attached url is correct.");
+ }
+
+ is(target.querySelector(".requests-menu-method").getAttribute("value"),
+ aMethod, "The displayed method is correct.");
+
+ if (fuzzyUrl) {
+ ok(target.querySelector(".requests-menu-file").getAttribute("value").startsWith(
+ name + (query ? "?" + query : "")), "The displayed file is correct.");
+ ok(target.querySelector(".requests-menu-file").getAttribute("tooltiptext").startsWith(unicodeUrl),
+ "The tooltip file is correct.");
+ } else {
+ is(target.querySelector(".requests-menu-file").getAttribute("value"),
+ name + (query ? "?" + query : ""), "The displayed file is correct.");
+ is(target.querySelector(".requests-menu-file").getAttribute("tooltiptext"),
+ unicodeUrl, "The tooltip file is correct.");
+ }
+
+ is(target.querySelector(".requests-menu-domain").getAttribute("value"),
+ hostPort, "The displayed domain is correct.");
+
+ let domainTooltip = hostPort + (remoteAddress ? " (" + remoteAddress + ")" : "");
+ is(target.querySelector(".requests-menu-domain").getAttribute("tooltiptext"),
+ domainTooltip, "The tooltip domain is correct.");
+
+ if (status !== undefined) {
+ let value = target.querySelector(".requests-menu-status-icon").getAttribute("code");
+ let codeValue = target.querySelector(".requests-menu-status-code").getAttribute("value");
+ let tooltip = target.querySelector(".requests-menu-status").getAttribute("tooltiptext");
+ info("Displayed status: " + value);
+ info("Displayed code: " + codeValue);
+ info("Tooltip status: " + tooltip);
+ is(value, displayedStatus ? displayedStatus : status, "The displayed status is correct.");
+ is(codeValue, status, "The displayed status code is correct.");
+ is(tooltip, status + " " + statusText, "The tooltip status is correct.");
+ }
+ if (cause !== undefined) {
+ let causeLabel = target.querySelector(".requests-menu-cause-label");
+ let value = causeLabel.getAttribute("value");
+ let tooltip = causeLabel.getAttribute("tooltiptext");
+ info("Displayed cause: " + value);
+ info("Tooltip cause: " + tooltip);
+ is(value, cause.type, "The displayed cause is correct.");
+ is(tooltip, cause.loadingDocumentUri, "The tooltip cause is correct.")
+ }
+ if (type !== undefined) {
+ let value = target.querySelector(".requests-menu-type").getAttribute("value");
+ let tooltip = target.querySelector(".requests-menu-type").getAttribute("tooltiptext");
+ info("Displayed type: " + value);
+ info("Tooltip type: " + tooltip);
+ is(value, type, "The displayed type is correct.");
+ is(tooltip, fullMimeType, "The tooltip type is correct.");
+ }
+ if (transferred !== undefined) {
+ let value = target.querySelector(".requests-menu-transferred").getAttribute("value");
+ let tooltip = target.querySelector(".requests-menu-transferred").getAttribute("tooltiptext");
+ info("Displayed transferred size: " + value);
+ info("Tooltip transferred size: " + tooltip);
+ is(value, transferred, "The displayed transferred size is correct.");
+ is(tooltip, transferred, "The tooltip transferred size is correct.");
+ }
+ if (size !== undefined) {
+ let value = target.querySelector(".requests-menu-size").getAttribute("value");
+ let tooltip = target.querySelector(".requests-menu-size").getAttribute("tooltiptext");
+ info("Displayed size: " + value);
+ info("Tooltip size: " + tooltip);
+ is(value, size, "The displayed size is correct.");
+ is(tooltip, size, "The tooltip size is correct.");
+ }
+ if (time !== undefined) {
+ let value = target.querySelector(".requests-menu-timings-total").getAttribute("value");
+ let tooltip = target.querySelector(".requests-menu-timings-total").getAttribute("tooltiptext");
+ info("Displayed time: " + value);
+ info("Tooltip time: " + tooltip);
+ ok(~~(value.match(/[0-9]+/)) >= 0, "The displayed time is correct.");
+ ok(~~(tooltip.match(/[0-9]+/)) >= 0, "The tooltip time is correct.");
+ }
+
+ if (visibleIndex != -1) {
+ if (visibleIndex % 2 == 0) {
+ ok(aRequestItem.target.hasAttribute("even"),
+ aRequestItem.value + " should have 'even' attribute.");
+ ok(!aRequestItem.target.hasAttribute("odd"),
+ aRequestItem.value + " shouldn't have 'odd' attribute.");
+ } else {
+ ok(!aRequestItem.target.hasAttribute("even"),
+ aRequestItem.value + " shouldn't have 'even' attribute.");
+ ok(aRequestItem.target.hasAttribute("odd"),
+ aRequestItem.value + " should have 'odd' attribute.");
+ }
+ }
+}
+
+/**
+ * Helper function for waiting for an event to fire before resolving a promise.
+ * Example: waitFor(aMonitor.panelWin, aMonitor.panelWin.EVENTS.TAB_UPDATED);
+ *
+ * @param object subject
+ * The event emitter object that is being listened to.
+ * @param string eventName
+ * The name of the event to listen to.
+ * @return object
+ * Returns a promise that resolves upon firing of the event.
+ */
+function waitFor(subject, eventName) {
+ let deferred = promise.defer();
+ subject.once(eventName, deferred.resolve);
+ return deferred.promise;
+}
+
+/**
+ * Tests if a button for a filter of given type is the only one checked.
+ *
+ * @param string filterType
+ * The type of the filter that should be the only one checked.
+ */
+function testFilterButtons(monitor, filterType) {
+ let doc = monitor.panelWin.document;
+ let target = doc.querySelector("#requests-menu-filter-" + filterType + "-button");
+ ok(target, `Filter button '${filterType}' was found`);
+ let buttons = [...doc.querySelectorAll(".menu-filter-button")];
+ ok(buttons.length > 0, "More than zero filter buttons were found");
+
+ // Only target should be checked.
+ let checkStatus = buttons.map(button => button == target ? 1 : 0);
+ testFilterButtonsCustom(monitor, checkStatus);
+}
+
+/**
+ * Tests if filter buttons have 'checked' attributes set correctly.
+ *
+ * @param array aIsChecked
+ * An array specifying if a button at given index should have a
+ * 'checked' attribute. For example, if the third item of the array
+ * evaluates to true, the third button should be checked.
+ */
+function testFilterButtonsCustom(aMonitor, aIsChecked) {
+ let doc = aMonitor.panelWin.document;
+ let buttons = doc.querySelectorAll(".menu-filter-button");
+ for (let i = 0; i < aIsChecked.length; i++) {
+ let button = buttons[i];
+ if (aIsChecked[i]) {
+ is(button.classList.contains("checked"), true,
+ "The " + button.id + " button should have a 'checked' class.");
+ } else {
+ is(button.classList.contains("checked"), false,
+ "The " + button.id + " button should not have a 'checked' class.");
+ }
+ }
+}
+
+/**
+ * Loads shared/frame-script-utils.js in the specified tab.
+ *
+ * @param tab
+ * Optional tab to load the frame script in. Defaults to the current tab.
+ */
+function loadCommonFrameScript(tab) {
+ let browser = tab ? tab.linkedBrowser : gBrowser.selectedBrowser;
+
+ browser.messageManager.loadFrameScript(FRAME_SCRIPT_UTILS_URL, false);
+}
+
+/**
+ * Perform the specified requests in the context of the page content.
+ *
+ * @param Array requests
+ * An array of objects specifying the requests to perform. See
+ * shared/frame-script-utils.js for more information.
+ *
+ * @return A promise that resolves once the requests complete.
+ */
+function performRequestsInContent(requests) {
+ info("Performing requests in the context of the content.");
+ return executeInContent("devtools:test:xhr", requests);
+}
+
+/**
+ * Send an async message to the frame script (chrome -> content) and wait for a
+ * response message with the same name (content -> chrome).
+ *
+ * @param String name
+ * The message name. Should be one of the messages defined
+ * shared/frame-script-utils.js
+ * @param Object data
+ * Optional data to send along
+ * @param Object objects
+ * Optional CPOW objects to send along
+ * @param Boolean expectResponse
+ * If set to false, don't wait for a response with the same name from the
+ * content script. Defaults to true.
+ *
+ * @return Promise
+ * Resolves to the response data if a response is expected, immediately
+ * resolves otherwise
+ */
+function executeInContent(name, data = {}, objects = {}, expectResponse = true) {
+ let mm = gBrowser.selectedBrowser.messageManager;
+
+ mm.sendAsyncMessage(name, data, objects);
+ if (expectResponse) {
+ return waitForContentMessage(name);
+ } else {
+ return promise.resolve();
+ }
+}
+
+/**
+ * Wait for a content -> chrome message on the message manager (the window
+ * messagemanager is used).
+ * @param {String} name The message name
+ * @return {Promise} A promise that resolves to the response data when the
+ * message has been received
+ */
+function waitForContentMessage(name) {
+ let mm = gBrowser.selectedBrowser.messageManager;
+
+ let def = promise.defer();
+ mm.addMessageListener(name, function onMessage(msg) {
+ mm.removeMessageListener(name, onMessage);
+ def.resolve(msg);
+ });
+ return def.promise;
+}
+
+/**
+ * Open the requestMenu menu and return all of it's items in a flat array
+ * @param {netmonitorPanel} netmonitor
+ * @param {Event} event mouse event with screenX and screenX coordinates
+ * @return An array of MenuItems
+ */
+function openContextMenuAndGetAllItems(netmonitor, event) {
+ let menu = netmonitor.RequestsMenu.contextMenu.open(event);
+
+ // Flatten all menu items into a single array to make searching through it easier
+ let allItems = [].concat.apply([], menu.items.map(function addItem(item) {
+ if (item.submenu) {
+ return addItem(item.submenu.items);
+ }
+ return item;
+ }));
+
+ return allItems;
+}
diff --git a/devtools/client/netmonitor/test/html_api-calls-test-page.html b/devtools/client/netmonitor/test/html_api-calls-test-page.html
new file mode 100644
index 000000000..e31872319
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_api-calls-test-page.html
@@ -0,0 +1,46 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>API calls request test</p>
+
+ <script type="text/javascript">
+ function get(aAddress, aCallback) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", aAddress, true);
+
+ xhr.onreadystatechange = function() {
+ if (this.readyState == this.DONE) {
+ aCallback();
+ }
+ };
+ xhr.send();
+ }
+
+ function performRequests() {
+ get("/api/fileName.xml", function() {
+ get("/api/file%E2%98%A2.xml", function() {
+ get("/api/ascii/get/", function() {
+ get("/api/unicode/%E2%98%A2/", function() {
+ get("/api/search/?q=search%E2%98%A2", function() {
+ // Done.
+ });
+ });
+ });
+ });
+ });
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/netmonitor/test/html_brotli-test-page.html b/devtools/client/netmonitor/test/html_brotli-test-page.html
new file mode 100644
index 000000000..d5afae4b3
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_brotli-test-page.html
@@ -0,0 +1,38 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>Brotli test</p>
+
+ <script type="text/javascript">
+ function get(aAddress, aCallback) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", aAddress, true);
+
+ xhr.onreadystatechange = function() {
+ if (this.readyState == this.DONE) {
+ aCallback();
+ }
+ };
+ xhr.send(null);
+ }
+
+ function performRequests() {
+ get("sjs_content-type-test-server.sjs?fmt=br", function() {
+ // Done.
+ });
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/netmonitor/test/html_cause-test-page.html b/devtools/client/netmonitor/test/html_cause-test-page.html
new file mode 100644
index 000000000..d2b86682b
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_cause-test-page.html
@@ -0,0 +1,48 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ <link rel="stylesheet" type="text/css" href="stylesheet_request" />
+ </head>
+
+ <body>
+ <p>Request cause test</p>
+ <img src="img_request" />
+ <script type="text/javascript">
+ function performXhrRequest() {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", "xhr_request", true);
+ xhr.send();
+ }
+
+ function performFetchRequest() {
+ fetch("fetch_request");
+ }
+
+ function performBeaconRequest() {
+ navigator.sendBeacon("beacon_request");
+ }
+
+ performXhrRequest();
+ performFetchRequest();
+
+ // Perform some requests with async stacks
+ Promise.resolve().then(function performPromiseFetchRequest() {
+ fetch("promise_fetch_request");
+ setTimeout(function performTimeoutFetchRequest() {
+ fetch("timeout_fetch_request");
+
+ // Finally, send a beacon request
+ performBeaconRequest();
+ }, 0);
+ });
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/netmonitor/test/html_content-type-test-page.html b/devtools/client/netmonitor/test/html_content-type-test-page.html
new file mode 100644
index 000000000..23ecf1f44
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_content-type-test-page.html
@@ -0,0 +1,48 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>Content type test</p>
+
+ <script type="text/javascript">
+ function get(aAddress, aCallback) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", aAddress, true);
+
+ xhr.onreadystatechange = function() {
+ if (this.readyState == this.DONE) {
+ aCallback();
+ }
+ };
+ xhr.send(null);
+ }
+
+ function performRequests() {
+ get("sjs_content-type-test-server.sjs?fmt=xml", function() {
+ get("sjs_content-type-test-server.sjs?fmt=css", function() {
+ get("sjs_content-type-test-server.sjs?fmt=js", function() {
+ get("sjs_content-type-test-server.sjs?fmt=json", function() {
+ get("sjs_content-type-test-server.sjs?fmt=bogus", function() {
+ get("test-image.png", function() {
+ // Done.
+ });
+ });
+ });
+ });
+ });
+ });
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/netmonitor/test/html_content-type-without-cache-test-page.html b/devtools/client/netmonitor/test/html_content-type-without-cache-test-page.html
new file mode 100644
index 000000000..f27e6e105
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_content-type-without-cache-test-page.html
@@ -0,0 +1,52 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>Content type test</p>
+
+ <script type="text/javascript">
+ function get(aAddress, aCallback) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", aAddress, true);
+
+ xhr.onreadystatechange = function() {
+ if (this.readyState == this.DONE) {
+ aCallback();
+ }
+ };
+ xhr.send(null);
+ }
+
+ function performRequests() {
+ get("sjs_content-type-test-server.sjs?fmt=xml", function() {
+ get("sjs_content-type-test-server.sjs?fmt=css", function() {
+ get("sjs_content-type-test-server.sjs?fmt=js", function() {
+ get("sjs_content-type-test-server.sjs?fmt=json", function() {
+ get("sjs_content-type-test-server.sjs?fmt=bogus", function() {
+ get("test-image.png?v=" + Math.random(), function() {
+ get("sjs_content-type-test-server.sjs?fmt=gzip", function() {
+ get("sjs_content-type-test-server.sjs?fmt=br", function() {
+ // Done.
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/netmonitor/test/html_copy-as-curl.html b/devtools/client/netmonitor/test/html_copy-as-curl.html
new file mode 100644
index 000000000..3ddcfbced
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_copy-as-curl.html
@@ -0,0 +1,30 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>Performing a GET request</p>
+
+ <script type="text/javascript">
+ function performRequest(aUrl) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", aUrl, true);
+ xhr.setRequestHeader("Accept-Language", window.navigator.language);
+ xhr.setRequestHeader("X-Custom-Header-1", "Custom value");
+ xhr.setRequestHeader("X-Custom-Header-2", "8.8.8.8");
+ xhr.setRequestHeader("X-Custom-Header-3", "Mon, 3 Mar 2014 11:11:11 GMT");
+ xhr.send(null);
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/netmonitor/test/html_cors-test-page.html b/devtools/client/netmonitor/test/html_cors-test-page.html
new file mode 100644
index 000000000..179b2ed00
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_cors-test-page.html
@@ -0,0 +1,31 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>POST with CORS test page</p>
+
+ <script type="text/javascript">
+ function post(url, contentType, postData) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("POST", url, true);
+ xhr.setRequestHeader("Content-Type", contentType);
+ xhr.send(postData);
+ }
+
+ function performRequests(url, contentType, postData) {
+ post(url, contentType, postData);
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/netmonitor/test/html_curl-utils.html b/devtools/client/netmonitor/test/html_curl-utils.html
new file mode 100644
index 000000000..8ff7ecdf0
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_curl-utils.html
@@ -0,0 +1,102 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>Performing requests</p>
+
+ <p>
+ <canvas width="100" height="100"></canvas>
+ </p>
+
+ <hr/>
+
+ <form method="post" action="#" enctype="multipart/form-data" target="target" id="post-form">
+ <input type="text" name="param1" value="value1"/>
+ <input type="text" name="param2" value="value2"/>
+ <input type="text" name="param3" value="value3"/>
+ <input type="submit"/>
+ </form>
+ <iframe name="target"></iframe>
+
+ <script type="text/javascript">
+
+ function ajaxGet(aUrl, aCallback) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", aUrl + "?param1=value1&param2=value2&param3=value3", true);
+ xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
+ xhr.onload = function() {
+ aCallback();
+ };
+ xhr.send();
+ }
+
+ function ajaxPost(aUrl, aCallback) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("POST", aUrl, true);
+ xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
+ xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
+ xhr.onload = function() {
+ aCallback();
+ };
+ var params = "param1=value1&param2=value2&param3=value3";
+ xhr.send(params);
+ }
+
+ function ajaxMultipart(aUrl, aCallback) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("POST", aUrl, true);
+ xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
+ xhr.onload = function() {
+ aCallback();
+ };
+
+ getCanvasElem().toBlob((blob) => {
+ var formData = new FormData();
+ formData.append("param1", "value1");
+ formData.append("file", blob, "filename.png");
+ xhr.send(formData);
+ });
+ }
+
+ function submitForm() {
+ var form = document.querySelector("#post-form");
+ form.submit();
+ }
+
+ function getCanvasElem() {
+ return document.querySelector("canvas");
+ }
+
+ function initCanvas() {
+ var canvas = getCanvasElem();
+ var ctx = canvas.getContext("2d");
+ ctx.fillRect(0,0,100,100);
+ ctx.clearRect(20,20,60,60);
+ ctx.strokeRect(25,25,50,50);
+ }
+
+ function performRequests(aUrl) {
+ ajaxGet(aUrl, () => {
+ ajaxPost(aUrl, () => {
+ ajaxMultipart(aUrl, () => {
+ submitForm();
+ });
+ });
+ });
+ }
+
+ initCanvas();
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/netmonitor/test/html_custom-get-page.html b/devtools/client/netmonitor/test/html_custom-get-page.html
new file mode 100644
index 000000000..19e40f93a
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_custom-get-page.html
@@ -0,0 +1,44 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>Performing a custom number of GETs</p>
+
+ <script type="text/javascript">
+ function get(aAddress, aCallback) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", aAddress, true);
+
+ xhr.onreadystatechange = function() {
+ if (this.readyState == this.DONE) {
+ aCallback();
+ }
+ };
+ xhr.send(null);
+ }
+
+ // Use a count parameter to defeat caching.
+ var count = 0;
+
+ function performRequests(aTotal, aUrl, aTimeout = 0) {
+ if (!aTotal) {
+ return;
+ }
+ get(aUrl || "request_" + (count++), function() {
+ setTimeout(performRequests.bind(this, --aTotal, aUrl, aTimeout), aTimeout);
+ });
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/netmonitor/test/html_cyrillic-test-page.html b/devtools/client/netmonitor/test/html_cyrillic-test-page.html
new file mode 100644
index 000000000..8735ac674
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_cyrillic-test-page.html
@@ -0,0 +1,39 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>Cyrillic type test</p>
+ <p>Братан, Ñ‚Ñ‹ вообще качаешьÑÑ?</p>
+
+ <script type="text/javascript">
+ function get(aAddress, aCallback) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", aAddress, true);
+
+ xhr.onreadystatechange = function() {
+ if (this.readyState == this.DONE) {
+ aCallback();
+ }
+ };
+ xhr.send(null);
+ }
+
+ function performRequests() {
+ get("sjs_content-type-test-server.sjs?fmt=txt", function() {
+ // Done.
+ });
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/netmonitor/test/html_filter-test-page.html b/devtools/client/netmonitor/test/html_filter-test-page.html
new file mode 100644
index 000000000..eb5d02ed9
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_filter-test-page.html
@@ -0,0 +1,60 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>Filtering test</p>
+
+ <script type="text/javascript">
+ function get(aAddress, aCallback) {
+ var xhr = new XMLHttpRequest();
+ // Use a random parameter to defeat caching.
+ xhr.open("GET", aAddress + "&" + Math.random(), true);
+
+ xhr.onreadystatechange = function() {
+ if (this.readyState == this.DONE) {
+ aCallback();
+ }
+ };
+ xhr.send(null);
+ }
+
+ function performRequests(aOptions) {
+ var options = JSON.parse(aOptions);
+ get("sjs_content-type-test-server.sjs?fmt=html&res=" + options.htmlContent, function() {
+ get("sjs_content-type-test-server.sjs?fmt=css", function() {
+ get("sjs_content-type-test-server.sjs?fmt=js", function() {
+ if (!options.getMedia) {
+ return;
+ }
+ get("sjs_content-type-test-server.sjs?fmt=font", function() {
+ get("sjs_content-type-test-server.sjs?fmt=image", function() {
+ get("sjs_content-type-test-server.sjs?fmt=audio", function() {
+ get("sjs_content-type-test-server.sjs?fmt=video", function() {
+ if (!options.getFlash) {
+ return;
+ }
+ get("sjs_content-type-test-server.sjs?fmt=flash", function() {
+ // Done.
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/netmonitor/test/html_frame-subdocument.html b/devtools/client/netmonitor/test/html_frame-subdocument.html
new file mode 100644
index 000000000..9e800582c
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_frame-subdocument.html
@@ -0,0 +1,48 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ <link rel="stylesheet" type="text/css" href="stylesheet_request" />
+ </head>
+
+ <body>
+ <p>Request frame test</p>
+ <img src="img_request" />
+ <script type="text/javascript">
+ function performXhrRequest() {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", "xhr_request", true);
+ xhr.send();
+ }
+
+ function performFetchRequest() {
+ fetch("fetch_request");
+ }
+
+ function performBeaconRequest() {
+ navigator.sendBeacon("beacon_request");
+ }
+
+ performXhrRequest();
+ performFetchRequest();
+
+ // Perform some requests with async stacks
+ Promise.resolve().then(function performPromiseFetchRequest() {
+ fetch("promise_fetch_request");
+ setTimeout(function performTimeoutFetchRequest() {
+ fetch("timeout_fetch_request");
+
+ // Finally, send a beacon request
+ performBeaconRequest();
+ }, 0);
+ });
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/netmonitor/test/html_frame-test-page.html b/devtools/client/netmonitor/test/html_frame-test-page.html
new file mode 100644
index 000000000..66f6620af
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_frame-test-page.html
@@ -0,0 +1,49 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ <link rel="stylesheet" type="text/css" href="stylesheet_request" />
+ </head>
+
+ <body>
+ <p>Request frame test</p>
+ <img src="img_request" />
+ <iframe src="html_frame-subdocument.html"></iframe>
+ <script type="text/javascript">
+ function performXhrRequest() {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", "xhr_request", true);
+ xhr.send();
+ }
+
+ function performFetchRequest() {
+ fetch("fetch_request");
+ }
+
+ function performBeaconRequest() {
+ navigator.sendBeacon("beacon_request");
+ }
+
+ performXhrRequest();
+ performFetchRequest();
+
+ // Perform some requests with async stacks
+ Promise.resolve().then(function performPromiseFetchRequest() {
+ fetch("promise_fetch_request");
+ setTimeout(function performTimeoutFetchRequest() {
+ fetch("timeout_fetch_request");
+
+ // Finally, send a beacon request
+ performBeaconRequest();
+ }, 0);
+ });
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/netmonitor/test/html_image-tooltip-test-page.html b/devtools/client/netmonitor/test/html_image-tooltip-test-page.html
new file mode 100644
index 000000000..c39db909e
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_image-tooltip-test-page.html
@@ -0,0 +1,26 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>tooltip test</p>
+
+ <script type="text/javascript">
+ function performRequests() {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", "test-image.png?v=" + Math.random(), true);
+ xhr.send(null);
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/netmonitor/test/html_infinite-get-page.html b/devtools/client/netmonitor/test/html_infinite-get-page.html
new file mode 100644
index 000000000..f51b718ad
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_infinite-get-page.html
@@ -0,0 +1,41 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>Infinite GETs</p>
+
+ <script type="text/javascript">
+ function get(aAddress, aCallback) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", aAddress, true);
+
+ xhr.onreadystatechange = function() {
+ if (this.readyState == this.DONE) {
+ aCallback();
+ }
+ };
+ xhr.send(null);
+ }
+
+ // Use a count parameter to defeat caching.
+ var count = 0;
+
+ (function performRequests() {
+ get("request_" + (count++), function() {
+ setTimeout(performRequests, 50);
+ });
+ })();
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/netmonitor/test/html_json-custom-mime-test-page.html b/devtools/client/netmonitor/test/html_json-custom-mime-test-page.html
new file mode 100644
index 000000000..646fc60ea
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_json-custom-mime-test-page.html
@@ -0,0 +1,38 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>JSONP test</p>
+
+ <script type="text/javascript">
+ function get(aAddress, aCallback) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", aAddress, true);
+
+ xhr.onreadystatechange = function() {
+ if (this.readyState == this.DONE) {
+ aCallback();
+ }
+ };
+ xhr.send(null);
+ }
+
+ function performRequests() {
+ get("sjs_content-type-test-server.sjs?fmt=json-custom-mime", function() {
+ // Done.
+ });
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/netmonitor/test/html_json-long-test-page.html b/devtools/client/netmonitor/test/html_json-long-test-page.html
new file mode 100644
index 000000000..b538b4c27
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_json-long-test-page.html
@@ -0,0 +1,38 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>JSON long string test</p>
+
+ <script type="text/javascript">
+ function get(aAddress, aCallback) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", aAddress, true);
+
+ xhr.onreadystatechange = function() {
+ if (this.readyState == this.DONE) {
+ aCallback();
+ }
+ };
+ xhr.send(null);
+ }
+
+ function performRequests() {
+ get("sjs_content-type-test-server.sjs?fmt=json-long", function() {
+ // Done.
+ });
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/netmonitor/test/html_json-malformed-test-page.html b/devtools/client/netmonitor/test/html_json-malformed-test-page.html
new file mode 100644
index 000000000..0c8627ab5
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_json-malformed-test-page.html
@@ -0,0 +1,38 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>JSON malformed test</p>
+
+ <script type="text/javascript">
+ function get(aAddress, aCallback) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", aAddress, true);
+
+ xhr.onreadystatechange = function() {
+ if (this.readyState == this.DONE) {
+ aCallback();
+ }
+ };
+ xhr.send(null);
+ }
+
+ function performRequests() {
+ get("sjs_content-type-test-server.sjs?fmt=json-malformed", function() {
+ // Done.
+ });
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/netmonitor/test/html_json-text-mime-test-page.html b/devtools/client/netmonitor/test/html_json-text-mime-test-page.html
new file mode 100644
index 000000000..2c64e2531
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_json-text-mime-test-page.html
@@ -0,0 +1,38 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>JSON text test</p>
+
+ <script type="text/javascript">
+ function get(aAddress, aCallback) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", aAddress, true);
+
+ xhr.onreadystatechange = function() {
+ if (this.readyState == this.DONE) {
+ aCallback();
+ }
+ };
+ xhr.send(null);
+ }
+
+ function performRequests() {
+ get("sjs_content-type-test-server.sjs?fmt=json-text-mime", function() {
+ // Done.
+ });
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/netmonitor/test/html_jsonp-test-page.html b/devtools/client/netmonitor/test/html_jsonp-test-page.html
new file mode 100644
index 000000000..78c0da08b
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_jsonp-test-page.html
@@ -0,0 +1,40 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>JSONP test</p>
+
+ <script type="text/javascript">
+ function get(aAddress, aCallback) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", aAddress, true);
+
+ xhr.onreadystatechange = function() {
+ if (this.readyState == this.DONE) {
+ aCallback();
+ }
+ };
+ xhr.send(null);
+ }
+
+ function performRequests() {
+ get("sjs_content-type-test-server.sjs?fmt=jsonp&jsonp=$_0123Fun", function() {
+ get("sjs_content-type-test-server.sjs?fmt=jsonp2&jsonp=$_4567Sad", function() {
+ // Done.
+ });
+ });
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/netmonitor/test/html_navigate-test-page.html b/devtools/client/netmonitor/test/html_navigate-test-page.html
new file mode 100644
index 000000000..23f00f3df
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_navigate-test-page.html
@@ -0,0 +1,18 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>Navigation test</p>
+ </body>
+
+</html>
diff --git a/devtools/client/netmonitor/test/html_params-test-page.html b/devtools/client/netmonitor/test/html_params-test-page.html
new file mode 100644
index 000000000..3f30e3d76
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_params-test-page.html
@@ -0,0 +1,67 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>Request params type test</p>
+
+ <script type="text/javascript">
+ function get(aAddress, aQuery) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", aAddress + aQuery, true);
+ xhr.send();
+ }
+
+ function post(aAddress, aQuery, aContentType, aPostBody) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("POST", aAddress + aQuery, true);
+ xhr.setRequestHeader("content-type", aContentType);
+ xhr.send(aPostBody);
+ }
+
+ function performRequests() {
+ var urlencoded = "application/x-www-form-urlencoded";
+
+ setTimeout(function() {
+ post("baz", "?a", urlencoded, '{ "foo": "bar" }');
+
+ setTimeout(function() {
+ post("baz", "?a=b", urlencoded, '{ "foo": "bar" }');
+
+ setTimeout(function() {
+ post("baz", "?a=b", urlencoded, '?foo=bar');
+
+ setTimeout(function() {
+ post("baz", "?a", undefined, '{ "foo": "bar" }');
+
+ setTimeout(function() {
+ post("baz", "?a=b", undefined, '{ "foo": "bar" }');
+
+ setTimeout(function() {
+ post("baz", "?a=b", undefined, '?foo=bar');
+
+ setTimeout(function() {
+ get("baz", "");
+
+ // Done.
+ }, 10);
+ }, 10);
+ }, 10);
+ }, 10);
+ }, 10);
+ }, 10);
+ }, 10);
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/netmonitor/test/html_post-data-test-page.html b/devtools/client/netmonitor/test/html_post-data-test-page.html
new file mode 100644
index 000000000..8dedc7b60
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_post-data-test-page.html
@@ -0,0 +1,77 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ <style>
+ input {
+ display: block;
+ margin: 12px;
+ }
+ </style>
+ </head>
+
+ <body>
+ <p>POST data test</p>
+ <form enctype="multipart/form-data" method="post" name="form-name">
+ <input type="text" name="text" placeholder="text" value="Some text..."/>
+ <input type="email" name="email" placeholder="email"/>
+ <input type="range" name="range" value="42"/>
+ <input type="button" value="Post me!" onclick="window.form()">
+ </form>
+
+ <script type="text/javascript">
+ function post(aAddress, aMessage, aCallback) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("POST", aAddress, true);
+ xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
+
+ var data = "";
+ for (var i in aMessage) {
+ data += "&" + i + "=" + aMessage[i];
+ }
+
+ xhr.onreadystatechange = function() {
+ if (this.readyState == this.DONE) {
+ aCallback();
+ }
+ };
+ xhr.send(data);
+ }
+
+ function form(aAddress, aForm, aCallback) {
+ var formData = new FormData(document.forms.namedItem(aForm));
+ formData.append("Custom field", "Extra data");
+
+ var xhr = new XMLHttpRequest();
+ xhr.open("POST", aAddress, true);
+
+ xhr.onreadystatechange = function() {
+ if (this.readyState == this.DONE) {
+ aCallback();
+ }
+ };
+ xhr.send(formData);
+ }
+
+ function performRequests() {
+ var url = "sjs_simple-test-server.sjs";
+ var url1 = url + "?foo=bar&baz=42&type=urlencoded";
+ var url2 = url + "?foo=bar&baz=42&type=multipart";
+
+ post(url1, { foo: "bar", baz: 123 }, function() {
+ form(url2, "form-name", function() {
+ // Done.
+ });
+ });
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/netmonitor/test/html_post-json-test-page.html b/devtools/client/netmonitor/test/html_post-json-test-page.html
new file mode 100644
index 000000000..129feaf08
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_post-json-test-page.html
@@ -0,0 +1,39 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>POST raw test</p>
+
+ <script type="text/javascript">
+ function post(address, message, callback) {
+ let xhr = new XMLHttpRequest();
+ xhr.open("POST", address, true);
+ xhr.setRequestHeader("Content-Type", "application/json");
+
+ xhr.onreadystatechange = function () {
+ if (this.readyState == this.DONE) {
+ callback();
+ }
+ };
+ xhr.send(message);
+ }
+
+ function performRequests() {
+ post("sjs_simple-test-server.sjs", JSON.stringify({a: 1}), function () {
+ // Done.
+ });
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/netmonitor/test/html_post-raw-test-page.html b/devtools/client/netmonitor/test/html_post-raw-test-page.html
new file mode 100644
index 000000000..b4456348c
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_post-raw-test-page.html
@@ -0,0 +1,40 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>POST raw test</p>
+
+ <script type="text/javascript">
+ function post(aAddress, aMessage, aCallback) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("POST", aAddress, true);
+ xhr.setRequestHeader("content-type", "application/x-www-form-urlencoded");
+
+ xhr.onreadystatechange = function() {
+ if (this.readyState == this.DONE) {
+ aCallback();
+ }
+ };
+ xhr.send(aMessage);
+ }
+
+ function performRequests() {
+ var rawData = "foo=bar&baz=123";
+ post("sjs_simple-test-server.sjs", rawData, function() {
+ // Done.
+ });
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/netmonitor/test/html_post-raw-with-headers-test-page.html b/devtools/client/netmonitor/test/html_post-raw-with-headers-test-page.html
new file mode 100644
index 000000000..3bb8f9071
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_post-raw-with-headers-test-page.html
@@ -0,0 +1,45 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>POST raw with headers test</p>
+
+ <script type="text/javascript">
+ function post(aAddress, aMessage, aCallback) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("POST", aAddress, true);
+
+ xhr.onreadystatechange = function() {
+ if (this.readyState == this.DONE) {
+ aCallback();
+ }
+ };
+ xhr.send(aMessage);
+ }
+
+ function performRequests() {
+ var rawData = [
+ "content-type: application/x-www-form-urlencoded\r",
+ "custom-header: hello world!\r",
+ "\r",
+ "\r",
+ "foo=bar&baz=123"
+ ];
+ post("sjs_simple-test-server.sjs", rawData.join("\n"), function() {
+ // Done.
+ });
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/netmonitor/test/html_send-beacon.html b/devtools/client/netmonitor/test/html_send-beacon.html
new file mode 100644
index 000000000..95cc005bd
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_send-beacon.html
@@ -0,0 +1,23 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>Send beacon test</p>
+
+ <script type="text/javascript">
+ function performRequest() {
+ navigator.sendBeacon("beacon_request");
+ }
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/netmonitor/test/html_simple-test-page.html b/devtools/client/netmonitor/test/html_simple-test-page.html
new file mode 100644
index 000000000..846681dbd
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_simple-test-page.html
@@ -0,0 +1,18 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>Simple test</p>
+ </body>
+
+</html>
diff --git a/devtools/client/netmonitor/test/html_single-get-page.html b/devtools/client/netmonitor/test/html_single-get-page.html
new file mode 100644
index 000000000..0055d4ee0
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_single-get-page.html
@@ -0,0 +1,36 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>Performing a custom number of GETs</p>
+
+ <script type="text/javascript">
+ function get(aAddress, aCallback) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", aAddress, true);
+
+ xhr.onreadystatechange = function() {
+ if (this.readyState == this.DONE) {
+ aCallback();
+ }
+ };
+ xhr.send(null);
+ }
+
+ (function performRequests() {
+ get("request_0", function() {});
+ })();
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/netmonitor/test/html_sorting-test-page.html b/devtools/client/netmonitor/test/html_sorting-test-page.html
new file mode 100644
index 000000000..640c58b8e
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_sorting-test-page.html
@@ -0,0 +1,18 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>Sorting test</p>
+ </body>
+
+</html>
diff --git a/devtools/client/netmonitor/test/html_statistics-test-page.html b/devtools/client/netmonitor/test/html_statistics-test-page.html
new file mode 100644
index 000000000..b4b15b82b
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_statistics-test-page.html
@@ -0,0 +1,40 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>Statistics test</p>
+
+ <script type="text/javascript">
+ function get(aAddress) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", aAddress, true);
+ xhr.send(null);
+ }
+
+ get("sjs_content-type-test-server.sjs?sts=304&fmt=txt");
+ get("sjs_content-type-test-server.sjs?sts=304&fmt=xml");
+ get("sjs_content-type-test-server.sjs?sts=304&fmt=html");
+ get("sjs_content-type-test-server.sjs?sts=304&fmt=css");
+ get("sjs_content-type-test-server.sjs?sts=304&fmt=js");
+ get("sjs_content-type-test-server.sjs?sts=304&fmt=json");
+ get("sjs_content-type-test-server.sjs?sts=304&fmt=jsonp");
+ get("sjs_content-type-test-server.sjs?sts=304&fmt=font");
+ get("sjs_content-type-test-server.sjs?sts=304&fmt=image");
+ get("sjs_content-type-test-server.sjs?sts=304&fmt=audio");
+ get("sjs_content-type-test-server.sjs?sts=304&fmt=video");
+ get("sjs_content-type-test-server.sjs?sts=304&fmt=flash");
+ get("test-image.png?v=" + Math.random());
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/netmonitor/test/html_status-codes-test-page.html b/devtools/client/netmonitor/test/html_status-codes-test-page.html
new file mode 100644
index 000000000..4be779bd4
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_status-codes-test-page.html
@@ -0,0 +1,55 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>Status codes test</p>
+
+ <script type="text/javascript">
+ function get(aAddress, aCallback) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", aAddress, true);
+
+ xhr.onreadystatechange = function() {
+ if (this.readyState == this.DONE) {
+ aCallback();
+ }
+ };
+ xhr.send(null);
+ }
+
+ function performRequests() {
+ get("sjs_status-codes-test-server.sjs?sts=100", function() {
+ get("sjs_status-codes-test-server.sjs?sts=200", function() {
+ get("sjs_status-codes-test-server.sjs?sts=300", function() {
+ get("sjs_status-codes-test-server.sjs?sts=400", function() {
+ get("sjs_status-codes-test-server.sjs?sts=500", function() {
+ // Done.
+ });
+ });
+ });
+ });
+ });
+ }
+
+ function performCachedRequests() {
+ get("sjs_status-codes-test-server.sjs?sts=ok&cached", function() {
+ get("sjs_status-codes-test-server.sjs?sts=redirect&cached", function() {
+ // Done.
+ });
+ });
+ }
+
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/netmonitor/test/service-workers/status-codes-service-worker.js b/devtools/client/netmonitor/test/service-workers/status-codes-service-worker.js
new file mode 100644
index 000000000..3c70c7dcb
--- /dev/null
+++ b/devtools/client/netmonitor/test/service-workers/status-codes-service-worker.js
@@ -0,0 +1,15 @@
+/* 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";
+
+self.addEventListener("activate", event => {
+ // start controlling the already loaded page
+ event.waitUntil(self.clients.claim());
+});
+
+self.addEventListener("fetch", event => {
+ let response = new Response("Service worker response");
+ event.respondWith(response);
+});
diff --git a/devtools/client/netmonitor/test/service-workers/status-codes.html b/devtools/client/netmonitor/test/service-workers/status-codes.html
new file mode 100644
index 000000000..65c79ee00
--- /dev/null
+++ b/devtools/client/netmonitor/test/service-workers/status-codes.html
@@ -0,0 +1,59 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>Status codes test</p>
+
+ <script type="text/javascript">
+ let swRegistration;
+
+ function registerServiceWorker() {
+ let sw = navigator.serviceWorker;
+ return sw.register("status-codes-service-worker.js")
+ .then(registration => {
+ swRegistration = registration;
+ console.log("Registered, scope is:", registration.scope);
+ return sw.ready;
+ }).then(() => {
+ // wait until the page is controlled
+ return new Promise(resolve => {
+ if (sw.controller) {
+ resolve();
+ } else {
+ sw.addEventListener('controllerchange', function onControllerChange() {
+ sw.removeEventListener('controllerchange', onControllerChange);
+ resolve();
+ });
+ }
+ });
+ }).catch(err => {
+ console.error("Registration failed");
+ });
+ }
+
+ function unregisterServiceWorker() {
+ return swRegistration.unregister();
+ }
+
+ function performRequests() {
+ return new Promise(function doXHR(done) {
+ let xhr = new XMLHttpRequest();
+ xhr.open("GET", "test/200", true);
+ xhr.onreadystatechange = done;
+ xhr.send(null);
+ });
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/netmonitor/test/sjs_content-type-test-server.sjs b/devtools/client/netmonitor/test/sjs_content-type-test-server.sjs
new file mode 100644
index 000000000..ee9a82e27
--- /dev/null
+++ b/devtools/client/netmonitor/test/sjs_content-type-test-server.sjs
@@ -0,0 +1,273 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { classes: Cc, interfaces: Ci } = Components;
+
+function gzipCompressString(string, obs) {
+
+ let scs = Cc["@mozilla.org/streamConverters;1"]
+ .getService(Ci.nsIStreamConverterService);
+ let listener = Cc["@mozilla.org/network/stream-loader;1"]
+ .createInstance(Ci.nsIStreamLoader);
+ listener.init(obs);
+ let converter = scs.asyncConvertData("uncompressed", "gzip",
+ listener, null);
+ let stringStream = Cc["@mozilla.org/io/string-input-stream;1"]
+ .createInstance(Ci.nsIStringInputStream);
+ stringStream.data = string;
+ converter.onStartRequest(null, null);
+ converter.onDataAvailable(null, null, stringStream, 0, string.length);
+ converter.onStopRequest(null, null, null);
+}
+
+function doubleGzipCompressString(string, observer) {
+ let observer2 = {
+ onStreamComplete: function(loader, context, status, length, result) {
+ let buffer = String.fromCharCode.apply(this, result);
+ gzipCompressString(buffer, observer);
+ }
+ };
+ gzipCompressString(string, observer2);
+}
+
+function handleRequest(request, response) {
+ response.processAsync();
+
+ let params = request.queryString.split("&");
+ let format = (params.filter((s) => s.includes("fmt="))[0] || "").split("=")[1];
+ let status = (params.filter((s) => s.includes("sts="))[0] || "").split("=")[1] || 200;
+
+ let cachedCount = 0;
+ let cacheExpire = 60; // seconds
+
+ function setCacheHeaders() {
+ if (status != 304) {
+ response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
+ response.setHeader("Pragma", "no-cache");
+ response.setHeader("Expires", "0");
+ return;
+ }
+ // Spice things up a little!
+ if (cachedCount % 2) {
+ response.setHeader("Cache-Control", "max-age=" + cacheExpire, false);
+ } else {
+ response.setHeader("Expires", Date(Date.now() + cacheExpire * 1000), false);
+ }
+ cachedCount++;
+ }
+
+ let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ timer.initWithCallback(() => {
+ // to avoid garbage collection
+ timer = null;
+ switch (format) {
+ case "txt": {
+ response.setStatusLine(request.httpVersion, status, "DA DA DA");
+ response.setHeader("Content-Type", "text/plain", false);
+ setCacheHeaders();
+ response.write("Братан, Ñ‚Ñ‹ вообще качаешьÑÑ?");
+ response.finish();
+ break;
+ }
+ case "xml": {
+ response.setStatusLine(request.httpVersion, status, "OK");
+ response.setHeader("Content-Type", "text/xml; charset=utf-8", false);
+ setCacheHeaders();
+ response.write("<label value='greeting'>Hello XML!</label>");
+ response.finish();
+ break;
+ }
+ case "html": {
+ let content = (params.filter((s) => s.includes("res="))[0] || "").split("=")[1];
+ response.setStatusLine(request.httpVersion, status, "OK");
+ response.setHeader("Content-Type", "text/html; charset=utf-8", false);
+ setCacheHeaders();
+ response.write(content || "<p>Hello HTML!</p>");
+ response.finish();
+ break;
+ }
+ case "html-long": {
+ let str = new Array(102400 /* 100 KB in bytes */).join(".");
+ response.setStatusLine(request.httpVersion, status, "OK");
+ response.setHeader("Content-Type", "text/html; charset=utf-8", false);
+ setCacheHeaders();
+ response.write("<p>" + str + "</p>");
+ response.finish();
+ break;
+ }
+ case "css": {
+ response.setStatusLine(request.httpVersion, status, "OK");
+ response.setHeader("Content-Type", "text/css; charset=utf-8", false);
+ setCacheHeaders();
+ response.write("body:pre { content: 'Hello CSS!' }");
+ response.finish();
+ break;
+ }
+ case "js": {
+ response.setStatusLine(request.httpVersion, status, "OK");
+ response.setHeader("Content-Type", "application/javascript; charset=utf-8", false);
+ setCacheHeaders();
+ response.write("function() { return 'Hello JS!'; }");
+ response.finish();
+ break;
+ }
+ case "json": {
+ response.setStatusLine(request.httpVersion, status, "OK");
+ response.setHeader("Content-Type", "application/json; charset=utf-8", false);
+ setCacheHeaders();
+ response.write("{ \"greeting\": \"Hello JSON!\" }");
+ response.finish();
+ break;
+ }
+ case "jsonp": {
+ let fun = (params.filter((s) => s.includes("jsonp="))[0] || "").split("=")[1];
+ response.setStatusLine(request.httpVersion, status, "OK");
+ response.setHeader("Content-Type", "text/json; charset=utf-8", false);
+ setCacheHeaders();
+ response.write(fun + "({ \"greeting\": \"Hello JSONP!\" })");
+ response.finish();
+ break;
+ }
+ case "jsonp2": {
+ let fun = (params.filter((s) => s.includes("jsonp="))[0] || "").split("=")[1];
+ response.setStatusLine(request.httpVersion, status, "OK");
+ response.setHeader("Content-Type", "text/json; charset=utf-8", false);
+ setCacheHeaders();
+ response.write(" " + fun + " ( { \"greeting\": \"Hello weird JSONP!\" } ) ; ");
+ response.finish();
+ break;
+ }
+ case "json-long": {
+ let str = "{ \"greeting\": \"Hello long string JSON!\" },";
+ response.setStatusLine(request.httpVersion, status, "OK");
+ response.setHeader("Content-Type", "text/json; charset=utf-8", false);
+ setCacheHeaders();
+ response.write("[" + new Array(2048).join(str).slice(0, -1) + "]");
+ response.finish();
+ break;
+ }
+ case "json-malformed": {
+ response.setStatusLine(request.httpVersion, status, "OK");
+ response.setHeader("Content-Type", "text/json; charset=utf-8", false);
+ setCacheHeaders();
+ response.write("{ \"greeting\": \"Hello malformed JSON!\" },");
+ response.finish();
+ break;
+ }
+ case "json-text-mime": {
+ response.setStatusLine(request.httpVersion, status, "OK");
+ response.setHeader("Content-Type", "text/plain; charset=utf-8", false);
+ setCacheHeaders();
+ response.write("{ \"greeting\": \"Hello third-party JSON!\" }");
+ response.finish();
+ break;
+ }
+ case "json-custom-mime": {
+ response.setStatusLine(request.httpVersion, status, "OK");
+ response.setHeader("Content-Type", "text/x-bigcorp-json; charset=utf-8", false);
+ setCacheHeaders();
+ response.write("{ \"greeting\": \"Hello oddly-named JSON!\" }");
+ response.finish();
+ break;
+ }
+ case "font": {
+ response.setStatusLine(request.httpVersion, status, "OK");
+ response.setHeader("Content-Type", "font/woff", false);
+ setCacheHeaders();
+ response.finish();
+ break;
+ }
+ case "image": {
+ response.setStatusLine(request.httpVersion, status, "OK");
+ response.setHeader("Content-Type", "image/png", false);
+ setCacheHeaders();
+ response.finish();
+ break;
+ }
+ case "audio": {
+ response.setStatusLine(request.httpVersion, status, "OK");
+ response.setHeader("Content-Type", "audio/ogg", false);
+ setCacheHeaders();
+ response.finish();
+ break;
+ }
+ case "video": {
+ response.setStatusLine(request.httpVersion, status, "OK");
+ response.setHeader("Content-Type", "video/webm", false);
+ setCacheHeaders();
+ response.finish();
+ break;
+ }
+ case "flash": {
+ response.setStatusLine(request.httpVersion, status, "OK");
+ response.setHeader("Content-Type", "application/x-shockwave-flash", false);
+ setCacheHeaders();
+ response.finish();
+ break;
+ }
+ case "ws": {
+ response.setStatusLine(request.httpVersion, 101, "Switching Protocols");
+ response.setHeader("Connection", "upgrade", false);
+ response.setHeader("Upgrade", "websocket", false);
+ setCacheHeaders();
+ response.finish();
+ break;
+ }
+ case "gzip": {
+ // Note: we're doing a double gzip encoding to test multiple
+ // converters in network monitor.
+ response.setStatusLine(request.httpVersion, status, "OK");
+ response.setHeader("Content-Type", "text/plain", false);
+ response.setHeader("Content-Encoding", "gzip\t ,gzip", false);
+ setCacheHeaders();
+ let observer = {
+ onStreamComplete: function(loader, context, status, length, result) {
+ let buffer = String.fromCharCode.apply(this, result);
+ response.setHeader("Content-Length", "" + buffer.length, false);
+ response.write(buffer);
+ response.finish();
+ }
+ };
+ let data = new Array(1000).join("Hello gzip!");
+ doubleGzipCompressString(data, observer);
+ break;
+ }
+ case "br": {
+ response.setStatusLine(request.httpVersion, status, "Connected");
+ response.setHeader("Content-Type", "text/plain", false);
+ response.setHeader("Content-Encoding", "br", false);
+ setCacheHeaders();
+ response.setHeader("Content-Length", "10", false);
+ // Use static data since we cannot encode brotli.
+ response.write("\x1b\x3f\x00\x00\x24\xb0\xe2\x99\x80\x12");
+ response.finish();
+ break;
+ }
+ case "hls-m3u8": {
+ response.setStatusLine(request.httpVersion, status, "OK");
+ response.setHeader("Content-Type", "application/x-mpegurl", false);
+ setCacheHeaders();
+ response.write("#EXTM3U\n");
+ response.finish();
+ break;
+ }
+ case "mpeg-dash": {
+ response.setStatusLine(request.httpVersion, status, "OK");
+ response.setHeader("Content-Type", "video/vnd.mpeg.dash.mpd", false);
+ setCacheHeaders();
+ response.write('<?xml version="1.0" encoding="UTF-8"?>\n');
+ response.write('<MPD xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"></MPD>\n');
+ response.finish();
+ break;
+ }
+ default: {
+ response.setStatusLine(request.httpVersion, 404, "Not Found");
+ response.setHeader("Content-Type", "text/html; charset=utf-8", false);
+ setCacheHeaders();
+ response.write("<blink>Not Found</blink>");
+ response.finish();
+ break;
+ }
+ }
+ }, 10, Ci.nsITimer.TYPE_ONE_SHOT); // Make sure this request takes a few ms.
+}
diff --git a/devtools/client/netmonitor/test/sjs_cors-test-server.sjs b/devtools/client/netmonitor/test/sjs_cors-test-server.sjs
new file mode 100644
index 000000000..0bab80901
--- /dev/null
+++ b/devtools/client/netmonitor/test/sjs_cors-test-server.sjs
@@ -0,0 +1,17 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function handleRequest(request, response) {
+ response.setStatusLine(request.httpVersion, 200, "Och Aye");
+
+ response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
+ response.setHeader("Pragma", "no-cache");
+ response.setHeader("Expires", "0");
+
+ response.setHeader("Access-Control-Allow-Origin", "*", false);
+ response.setHeader("Access-Control-Allow-Headers", "content-type", false);
+
+ response.setHeader("Content-Type", "text/plain; charset=utf-8", false);
+
+ response.write("Access-Control-Allow-Origin: *");
+}
diff --git a/devtools/client/netmonitor/test/sjs_hsts-test-server.sjs b/devtools/client/netmonitor/test/sjs_hsts-test-server.sjs
new file mode 100644
index 000000000..c5715886e
--- /dev/null
+++ b/devtools/client/netmonitor/test/sjs_hsts-test-server.sjs
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function handleRequest(request, response) {
+ response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
+ response.setHeader("Pragma", "no-cache");
+ response.setHeader("Expires", "0");
+
+ if (request.queryString === "reset") {
+ // Reset the HSTS policy, prevent influencing other tests
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Strict-Transport-Security", "max-age=0");
+ response.write("Resetting HSTS");
+ } else if (request.scheme === "http") {
+ response.setStatusLine(request.httpVersion, 302, "Found");
+ response.setHeader("Location", "https://" + request.host + request.path);
+ } else {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Strict-Transport-Security", "max-age=100");
+ response.write("Page was accessed over HTTPS!");
+ }
+}
diff --git a/devtools/client/netmonitor/test/sjs_https-redirect-test-server.sjs b/devtools/client/netmonitor/test/sjs_https-redirect-test-server.sjs
new file mode 100644
index 000000000..14ea34559
--- /dev/null
+++ b/devtools/client/netmonitor/test/sjs_https-redirect-test-server.sjs
@@ -0,0 +1,19 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function handleRequest(request, response) {
+ response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
+ response.setHeader("Pragma", "no-cache");
+ response.setHeader("Expires", "0");
+
+ response.setHeader("Access-Control-Allow-Origin", "*", false);
+
+ if (request.scheme === "http") {
+ response.setStatusLine(request.httpVersion, 302, "Found");
+ response.setHeader("Location", "https://" + request.host + request.path);
+ } else {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.write("Page was accessed over HTTPS!");
+ }
+
+}
diff --git a/devtools/client/netmonitor/test/sjs_simple-test-server.sjs b/devtools/client/netmonitor/test/sjs_simple-test-server.sjs
new file mode 100644
index 000000000..9a3d44b6d
--- /dev/null
+++ b/devtools/client/netmonitor/test/sjs_simple-test-server.sjs
@@ -0,0 +1,17 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function handleRequest(request, response) {
+ response.setStatusLine(request.httpVersion, 200, "Och Aye");
+
+ response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
+ response.setHeader("Pragma", "no-cache");
+ response.setHeader("Expires", "0");
+
+ response.setHeader("Set-Cookie", "bob=true; Max-Age=10; HttpOnly", true);
+ response.setHeader("Set-Cookie", "tom=cool; Max-Age=10; HttpOnly", true);
+
+ response.setHeader("Content-Type", "text/plain; charset=utf-8", false);
+ response.setHeader("Foo-Bar", "baz", false);
+ response.write("Hello world!");
+}
diff --git a/devtools/client/netmonitor/test/sjs_sorting-test-server.sjs b/devtools/client/netmonitor/test/sjs_sorting-test-server.sjs
new file mode 100644
index 000000000..54c62866b
--- /dev/null
+++ b/devtools/client/netmonitor/test/sjs_sorting-test-server.sjs
@@ -0,0 +1,26 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { classes: Cc, interfaces: Ci } = Components;
+
+function handleRequest(request, response) {
+ response.processAsync();
+
+ let params = request.queryString.split("&");
+ let index = params.filter((s) => s.includes("index="))[0].split("=")[1];
+
+ let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ timer.initWithCallback(() => {
+ // to avoid garbage collection
+ timer = null;
+ response.setStatusLine(request.httpVersion, index == 1 ? 101 : index * 100, "Meh");
+
+ response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
+ response.setHeader("Pragma", "no-cache");
+ response.setHeader("Expires", "0");
+
+ response.setHeader("Content-Type", "text/" + index, false);
+ response.write(new Array(index * 10).join(index)); // + 0.01 KB
+ response.finish();
+ }, 10, Ci.nsITimer.TYPE_ONE_SHOT); // Make sure this request takes a few ms.
+}
diff --git a/devtools/client/netmonitor/test/sjs_status-codes-test-server.sjs b/devtools/client/netmonitor/test/sjs_status-codes-test-server.sjs
new file mode 100644
index 000000000..4f17d1235
--- /dev/null
+++ b/devtools/client/netmonitor/test/sjs_status-codes-test-server.sjs
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { classes: Cc, interfaces: Ci } = Components;
+
+function handleRequest(request, response) {
+ response.processAsync();
+
+ let params = request.queryString.split("&");
+ let status = params.filter(s => s.includes("sts="))[0].split("=")[1];
+ let cached = params.filter(s => s === 'cached').length !== 0;
+
+ let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ timer.initWithCallback(() => {
+ // to avoid garbage collection
+ timer = null;
+ switch (status) {
+ case "100":
+ response.setStatusLine(request.httpVersion, 101, "Switching Protocols");
+ break;
+ case "200":
+ response.setStatusLine(request.httpVersion, 202, "Created");
+ break;
+ case "300":
+ response.setStatusLine(request.httpVersion, 303, "See Other");
+ break;
+ case "400":
+ response.setStatusLine(request.httpVersion, 404, "Not Found");
+ break;
+ case "500":
+ response.setStatusLine(request.httpVersion, 501, "Not Implemented");
+ break;
+ case "ok":
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ break;
+ case "redirect":
+ response.setStatusLine(request.httpVersion, 301, "Moved Permanently");
+ response.setHeader("Location", "http://example.com/redirected");
+ break;
+ }
+
+ if(!cached) {
+ response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
+ response.setHeader("Pragma", "no-cache");
+ response.setHeader("Expires", "0");
+ }
+ else {
+ response.setHeader("Cache-Control", "no-transform,public,max-age=300,s-maxage=900");
+ response.setHeader("Expires", "Thu, 01 Dec 2100 20:00:00 GMT");
+ }
+
+ response.setHeader("Content-Type", "text/plain; charset=utf-8", false);
+ response.write("Hello status code " + status + "!");
+ response.finish();
+ }, 10, Ci.nsITimer.TYPE_ONE_SHOT); // Make sure this request takes a few ms.
+}
diff --git a/devtools/client/netmonitor/test/sjs_truncate-test-server.sjs b/devtools/client/netmonitor/test/sjs_truncate-test-server.sjs
new file mode 100644
index 000000000..54db23d9a
--- /dev/null
+++ b/devtools/client/netmonitor/test/sjs_truncate-test-server.sjs
@@ -0,0 +1,18 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function handleRequest(request, response) {
+ let params = request.queryString.split("&");
+ let limit = (params.filter((s) => s.includes("limit="))[0] || "").split("=")[1];
+
+ response.setStatusLine(request.httpVersion, 200, "Och Aye");
+
+ response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
+ response.setHeader("Pragma", "no-cache");
+ response.setHeader("Expires", "0");
+ response.setHeader("Content-Type", "text/plain; charset=utf-8", false);
+
+ response.write("x".repeat(2 * parseInt(limit, 10)));
+
+ response.write("Hello world!");
+}
diff --git a/devtools/client/netmonitor/test/test-image.png b/devtools/client/netmonitor/test/test-image.png
new file mode 100644
index 000000000..769c63634
--- /dev/null
+++ b/devtools/client/netmonitor/test/test-image.png
Binary files differ
diff --git a/devtools/client/netmonitor/toolbar-view.js b/devtools/client/netmonitor/toolbar-view.js
new file mode 100644
index 000000000..28c3cf99b
--- /dev/null
+++ b/devtools/client/netmonitor/toolbar-view.js
@@ -0,0 +1,77 @@
+/* globals dumpn, $, NetMonitorView */
+"use strict";
+
+const { createFactory, DOM } = require("devtools/client/shared/vendor/react");
+const ReactDOM = require("devtools/client/shared/vendor/react-dom");
+const Provider = createFactory(require("devtools/client/shared/vendor/react-redux").Provider);
+const FilterButtons = createFactory(require("./components/filter-buttons"));
+const ToggleButton = createFactory(require("./components/toggle-button"));
+const SearchBox = createFactory(require("./components/search-box"));
+const { L10N } = require("./l10n");
+
+// Shortcuts
+const { button } = DOM;
+
+/**
+ * Functions handling the toolbar view: expand/collapse button etc.
+ */
+function ToolbarView() {
+ dumpn("ToolbarView was instantiated");
+}
+
+ToolbarView.prototype = {
+ /**
+ * Initialization function, called when the debugger is started.
+ */
+ initialize: function (store) {
+ dumpn("Initializing the ToolbarView");
+
+ this._clearContainerNode = $("#react-clear-button-hook");
+ this._filterContainerNode = $("#react-filter-buttons-hook");
+ this._toggleContainerNode = $("#react-details-pane-toggle-hook");
+ this._searchContainerNode = $("#react-search-box-hook");
+
+ // clear button
+ ReactDOM.render(button({
+ id: "requests-menu-clear-button",
+ className: "devtools-button devtools-clear-icon",
+ title: L10N.getStr("netmonitor.toolbar.clear"),
+ onClick: () => {
+ NetMonitorView.RequestsMenu.clear();
+ }
+ }), this._clearContainerNode);
+
+ // filter button
+ ReactDOM.render(Provider(
+ { store },
+ FilterButtons()
+ ), this._filterContainerNode);
+
+ // search box
+ ReactDOM.render(Provider(
+ { store },
+ SearchBox()
+ ), this._searchContainerNode);
+
+ // details pane toggle button
+ ReactDOM.render(Provider(
+ { store },
+ ToggleButton()
+ ), this._toggleContainerNode);
+ },
+
+ /**
+ * Destruction function, called when the debugger is closed.
+ */
+ destroy: function () {
+ dumpn("Destroying the ToolbarView");
+
+ ReactDOM.unmountComponentAtNode(this._clearContainerNode);
+ ReactDOM.unmountComponentAtNode(this._filterContainerNode);
+ ReactDOM.unmountComponentAtNode(this._toggleContainerNode);
+ ReactDOM.unmountComponentAtNode(this._searchContainerNode);
+ }
+
+};
+
+exports.ToolbarView = ToolbarView;
diff --git a/devtools/client/package.json b/devtools/client/package.json
new file mode 100644
index 000000000..3b89783ba
--- /dev/null
+++ b/devtools/client/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "devtools",
+ "version": "0.0.1",
+ "description": "",
+ "main": "",
+ "scripts": {
+ "build": "webpack"
+ },
+ "babel": {
+ "presets": [ "es2015" ]
+ },
+ "author": "",
+ "license": "",
+ "devDependencies": {
+ "babel-core": "^6.11.4",
+ "babel-loader": "^6.2.4",
+ "babel-preset-es2015": "^6.9.0",
+ "raw-loader": "^0.5.1",
+ "webpack": "^1.13.1"
+ }
+}
diff --git a/devtools/client/performance/components/jit-optimizations-item.js b/devtools/client/performance/components/jit-optimizations-item.js
new file mode 100644
index 000000000..e5c77ef02
--- /dev/null
+++ b/devtools/client/performance/components/jit-optimizations-item.js
@@ -0,0 +1,175 @@
+/* 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 { LocalizationHelper } = require("devtools/shared/l10n");
+const STRINGS_URI = "devtools/client/locales/jit-optimizations.properties";
+const L10N = new LocalizationHelper(STRINGS_URI);
+
+const {PluralForm} = require("devtools/shared/plural-form");
+const { DOM: dom, PropTypes, createClass, createFactory } = require("devtools/client/shared/vendor/react");
+const Frame = createFactory(require("devtools/client/shared/components/frame"));
+const PROPNAME_MAX_LENGTH = 4;
+// If TREE_ROW_HEIGHT changes, be sure to change `var(--jit-tree-row-height)`
+// in `devtools/client/themes/jit-optimizations.css`
+const TREE_ROW_HEIGHT = 14;
+
+const OPTIMIZATION_ITEM_TYPES = ["site", "attempts", "types", "attempt", "type",
+ "observedtype"];
+
+/* eslint-disable no-unused-vars */
+/**
+ * TODO - Re-enable this eslint rule. The JIT tool is a work in progress, and isn't fully
+ * integrated as of yet.
+ */
+const {
+ JITOptimizations, hasSuccessfulOutcome, isSuccessfulOutcome
+} = require("devtools/client/performance/modules/logic/jit");
+const OPTIMIZATION_FAILURE = L10N.getStr("jit.optimizationFailure");
+const JIT_SAMPLES = L10N.getStr("jit.samples");
+const JIT_TYPES = L10N.getStr("jit.types");
+const JIT_ATTEMPTS = L10N.getStr("jit.attempts");
+/* eslint-enable no-unused-vars */
+
+const JITOptimizationsItem = createClass({
+ displayName: "JITOptimizationsItem",
+
+ propTypes: {
+ onViewSourceInDebugger: PropTypes.func.isRequired,
+ frameData: PropTypes.object.isRequired,
+ type: PropTypes.oneOf(OPTIMIZATION_ITEM_TYPES).isRequired,
+ },
+
+ _renderSite({ item: site, onViewSourceInDebugger, frameData }) {
+ let attempts = site.data.attempts;
+ let lastStrategy = attempts[attempts.length - 1].strategy;
+ let propString = "";
+ let propertyName = site.data.propertyName;
+
+ // Display property name if it exists
+ if (propertyName) {
+ if (propertyName.length > PROPNAME_MAX_LENGTH) {
+ propString = ` (.${propertyName.substr(0, PROPNAME_MAX_LENGTH)}…)`;
+ } else {
+ propString = ` (.${propertyName})`;
+ }
+ }
+
+ let sampleString = PluralForm.get(site.samples, JIT_SAMPLES)
+ .replace("#1", site.samples);
+ let text = dom.span(
+ { className: "optimization-site-title" },
+ `${lastStrategy}${propString} – (${sampleString})`
+ );
+ let frame = Frame({
+ onClick: () => onViewSourceInDebugger(frameData.url, site.data.line),
+ frame: {
+ source: frameData.url,
+ line: +site.data.line,
+ column: site.data.column,
+ }
+ });
+ let children = [text, frame];
+
+ if (!hasSuccessfulOutcome(site)) {
+ children.unshift(dom.span({ className: "opt-icon warning" }));
+ }
+
+ return dom.span({ className: "optimization-site" }, ...children);
+ },
+
+ _renderAttempts({ item: attempts }) {
+ return dom.span({ className: "optimization-attempts" },
+ `${JIT_ATTEMPTS} (${attempts.length})`
+ );
+ },
+
+ _renderTypes({ item: types }) {
+ return dom.span({ className: "optimization-types" },
+ `${JIT_TYPES} (${types.length})`
+ );
+ },
+
+ _renderAttempt({ item: attempt }) {
+ let success = isSuccessfulOutcome(attempt.outcome);
+ let { strategy, outcome } = attempt;
+ return dom.span({ className: "optimization-attempt" },
+ dom.span({ className: "optimization-strategy" }, strategy),
+ " → ",
+ dom.span({ className: `optimization-outcome ${success ? "success" : "failure"}` },
+ outcome)
+ );
+ },
+
+ _renderType({ item: type }) {
+ return dom.span({ className: "optimization-ion-type" },
+ `${type.site}:${type.mirType}`);
+ },
+
+ _renderObservedType({ onViewSourceInDebugger, item: type }) {
+ let children = [
+ dom.span({ className: "optimization-observed-type-keyed" },
+ `${type.keyedBy}${type.name ? ` → ${type.name}` : ""}`)
+ ];
+
+ // If we have a line and location, make a link to the debugger
+ if (type.location && type.line) {
+ children.push(
+ Frame({
+ onClick: () => onViewSourceInDebugger(type.location, type.line),
+ frame: {
+ source: type.location,
+ line: type.line,
+ column: type.column,
+ }
+ })
+ );
+ // Otherwise if we just have a location, it's probably just a memory location.
+ } else if (type.location) {
+ children.push(`@${type.location}`);
+ }
+
+ return dom.span({ className: "optimization-observed-type" }, ...children);
+ },
+
+ render() {
+ /* eslint-disable no-unused-vars */
+ /**
+ * TODO - Re-enable this eslint rule. The JIT tool is a work in progress, and these
+ * undefined variables may represent intended functionality.
+ */
+ let {
+ depth,
+ arrow,
+ type,
+ // TODO - The following are currently unused.
+ item,
+ focused,
+ frameData,
+ onViewSourceInDebugger,
+ } = this.props;
+ /* eslint-enable no-unused-vars */
+
+ let content;
+ switch (type) {
+ case "site": content = this._renderSite(this.props); break;
+ case "attempts": content = this._renderAttempts(this.props); break;
+ case "types": content = this._renderTypes(this.props); break;
+ case "attempt": content = this._renderAttempt(this.props); break;
+ case "type": content = this._renderType(this.props); break;
+ case "observedtype": content = this._renderObservedType(this.props); break;
+ }
+
+ return dom.div(
+ {
+ className: `optimization-tree-item optimization-tree-item-${type}`,
+ style: { marginInlineStart: depth * TREE_ROW_HEIGHT }
+ },
+ arrow,
+ content
+ );
+ },
+});
+
+module.exports = JITOptimizationsItem;
diff --git a/devtools/client/performance/components/jit-optimizations.js b/devtools/client/performance/components/jit-optimizations.js
new file mode 100644
index 000000000..c189aa1ce
--- /dev/null
+++ b/devtools/client/performance/components/jit-optimizations.js
@@ -0,0 +1,248 @@
+/* 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 { LocalizationHelper } = require("devtools/shared/l10n");
+const STRINGS_URI = "devtools/client/locales/jit-optimizations.properties";
+const L10N = new LocalizationHelper(STRINGS_URI);
+
+const { assert } = require("devtools/shared/DevToolsUtils");
+const { DOM: dom, createClass, createFactory, PropTypes } = require("devtools/client/shared/vendor/react");
+const Tree = createFactory(require("../../shared/components/tree"));
+const OptimizationsItem = createFactory(require("./jit-optimizations-item"));
+const FrameView = createFactory(require("../../shared/components/frame"));
+const JIT_TITLE = L10N.getStr("jit.title");
+// If TREE_ROW_HEIGHT changes, be sure to change `var(--jit-tree-row-height)`
+// in `devtools/client/themes/jit-optimizations.css`
+const TREE_ROW_HEIGHT = 14;
+
+/* eslint-disable no-unused-vars */
+/**
+ * TODO - Re-enable this eslint rule. The JIT tool is a work in progress, and isn't fully
+ * integrated as of yet, and this may represent intended functionality.
+ */
+const onClickTooltipString = frame =>
+ L10N.getFormatStr("viewsourceindebugger",
+ `${frame.source}:${frame.line}:${frame.column}`);
+/* eslint-enable no-unused-vars */
+
+const optimizationAttemptModel = {
+ id: PropTypes.number.isRequired,
+ strategy: PropTypes.string.isRequired,
+ outcome: PropTypes.string.isRequired,
+};
+
+const optimizationObservedTypeModel = {
+ keyedBy: PropTypes.string.isRequired,
+ name: PropTypes.string,
+ location: PropTypes.string,
+ line: PropTypes.string,
+};
+
+const optimizationIonTypeModel = {
+ id: PropTypes.number.isRequired,
+ typeset: PropTypes.arrayOf(optimizationObservedTypeModel),
+ site: PropTypes.number.isRequired,
+ mirType: PropTypes.number.isRequired,
+};
+
+const optimizationSiteModel = {
+ id: PropTypes.number.isRequired,
+ propertyName: PropTypes.string,
+ line: PropTypes.number.isRequired,
+ column: PropTypes.number.isRequired,
+ data: PropTypes.shape({
+ attempts: PropTypes.arrayOf(optimizationAttemptModel).isRequired,
+ types: PropTypes.arrayOf(optimizationIonTypeModel).isRequired,
+ }).isRequired,
+};
+
+const JITOptimizations = createClass({
+ displayName: "JITOptimizations",
+
+ propTypes: {
+ onViewSourceInDebugger: PropTypes.func.isRequired,
+ frameData: PropTypes.object.isRequired,
+ optimizationSites: PropTypes.arrayOf(optimizationSiteModel).isRequired,
+ autoExpandDepth: PropTypes.number,
+ },
+
+ getDefaultProps() {
+ return {
+ autoExpandDepth: 0
+ };
+ },
+
+ getInitialState() {
+ return {
+ expanded: new Set()
+ };
+ },
+
+ /**
+ * Frame data generated from `frameNode.getInfo()`, or an empty
+ * object, as well as a handler for clicking on the frame component.
+ *
+ * @param {?Object} .frameData
+ * @param {Function} .onViewSourceInDebugger
+ * @return {ReactElement}
+ */
+ _createHeader: function ({ frameData, onViewSourceInDebugger }) {
+ let { isMetaCategory, url, line } = frameData;
+ let name = isMetaCategory ? frameData.categoryData.label :
+ frameData.functionName || "";
+
+ // Simulate `SavedFrame`s interface
+ let frame = { source: url, line: +line, functionDisplayName: name };
+
+ // Neither Meta Category nodes, or the lack of a selected frame node,
+ // renders out a frame source, like "file.js:123"; so just use
+ // an empty span.
+ let frameComponent;
+ if (isMetaCategory || !name) {
+ frameComponent = dom.span();
+ } else {
+ frameComponent = FrameView({
+ frame,
+ onClick: () => onViewSourceInDebugger(frame),
+ });
+ }
+
+ return dom.div({ className: "optimization-header" },
+ dom.span({ className: "header-title" }, JIT_TITLE),
+ dom.span({ className: "header-function-name" }, name),
+ frameComponent
+ );
+ },
+
+ _createTree(props) {
+ let {
+ autoExpandDepth,
+ frameData,
+ onViewSourceInDebugger,
+ optimizationSites: sites
+ } = this.props;
+
+ let getSite = id => sites.find(site => site.id === id);
+ let getIonTypeForObserved = type => {
+ return getSite(type.id).data.types
+ .find(iontype => (iontype.typeset || [])
+ .indexOf(type) !== -1);
+ };
+ let isSite = site => getSite(site.id) === site;
+ let isAttempts = attempts => getSite(attempts.id).data.attempts === attempts;
+ let isAttempt = attempt => getSite(attempt.id).data.attempts.indexOf(attempt) !== -1;
+ let isTypes = types => getSite(types.id).data.types === types;
+ let isType = type => getSite(type.id).data.types.indexOf(type) !== -1;
+ let isObservedType = type => getIonTypeForObserved(type);
+
+ let getRowType = node => {
+ if (isSite(node)) {
+ return "site";
+ }
+ if (isAttempts(node)) {
+ return "attempts";
+ }
+ if (isTypes(node)) {
+ return "types";
+ }
+ if (isAttempt(node)) {
+ return "attempt";
+ }
+ if (isType(node)) {
+ return "type";
+ }
+ if (isObservedType(node)) {
+ return "observedtype";
+ }
+ return null;
+ };
+
+ // Creates a unique key for each node in the
+ // optimizations data
+ let getKey = node => {
+ let site = getSite(node.id);
+ if (isSite(node)) {
+ return node.id;
+ } else if (isAttempts(node)) {
+ return `${node.id}-A`;
+ } else if (isTypes(node)) {
+ return `${node.id}-T`;
+ } else if (isType(node)) {
+ return `${node.id}-T-${site.data.types.indexOf(node)}`;
+ } else if (isAttempt(node)) {
+ return `${node.id}-A-${site.data.attempts.indexOf(node)}`;
+ } else if (isObservedType(node)) {
+ let iontype = getIonTypeForObserved(node);
+ return `${getKey(iontype)}-O-${iontype.typeset.indexOf(node)}`;
+ }
+ return "";
+ };
+
+ return Tree({
+ autoExpandDepth,
+ getParent: node => {
+ let site = getSite(node.id);
+ let parent;
+ if (isAttempts(node) || isTypes(node)) {
+ parent = site;
+ } else if (isType(node)) {
+ parent = site.data.types;
+ } else if (isAttempt(node)) {
+ parent = site.data.attempts;
+ } else if (isObservedType(node)) {
+ parent = getIonTypeForObserved(node);
+ }
+ assert(parent, "Could not find a parent for optimization data node");
+
+ return parent;
+ },
+ getChildren: node => {
+ if (isSite(node)) {
+ return [node.data.types, node.data.attempts];
+ } else if (isAttempts(node) || isTypes(node)) {
+ return node;
+ } else if (isType(node)) {
+ return node.typeset || [];
+ }
+ return [];
+ },
+ isExpanded: node => this.state.expanded.has(node),
+ onExpand: node => this.setState(state => {
+ let expanded = new Set(state.expanded);
+ expanded.add(node);
+ return { expanded };
+ }),
+ onCollapse: node => this.setState(state => {
+ let expanded = new Set(state.expanded);
+ expanded.delete(node);
+ return { expanded };
+ }),
+ onFocus: function () {},
+ getKey,
+ getRoots: () => sites || [],
+ itemHeight: TREE_ROW_HEIGHT,
+ renderItem: (item, depth, focused, arrow, expanded) =>
+ new OptimizationsItem({
+ onViewSourceInDebugger,
+ item,
+ depth,
+ focused,
+ arrow,
+ expanded,
+ type: getRowType(item),
+ frameData,
+ }),
+ });
+ },
+
+ render() {
+ let header = this._createHeader(this.props);
+ let tree = this._createTree(this.props);
+
+ return dom.div({}, header, tree);
+ }
+});
+
+module.exports = JITOptimizations;
diff --git a/devtools/client/performance/components/moz.build b/devtools/client/performance/components/moz.build
new file mode 100644
index 000000000..55de59215
--- /dev/null
+++ b/devtools/client/performance/components/moz.build
@@ -0,0 +1,19 @@
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'jit-optimizations-item.js',
+ 'jit-optimizations.js',
+ 'recording-button.js',
+ 'recording-controls.js',
+ 'recording-list-item.js',
+ 'recording-list.js',
+ 'waterfall-header.js',
+ 'waterfall-tree-row.js',
+ 'waterfall-tree.js',
+ 'waterfall.js',
+)
+
+MOCHITEST_CHROME_MANIFESTS += ['test/chrome.ini']
diff --git a/devtools/client/performance/components/recording-button.js b/devtools/client/performance/components/recording-button.js
new file mode 100644
index 000000000..877fd0e2b
--- /dev/null
+++ b/devtools/client/performance/components/recording-button.js
@@ -0,0 +1,37 @@
+/* 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 {L10N} = require("devtools/client/performance/modules/global");
+const {DOM, createClass} = require("devtools/client/shared/vendor/react");
+const {button} = DOM;
+
+module.exports = createClass({
+ displayName: "Recording Button",
+
+ render() {
+ let {
+ onRecordButtonClick,
+ isRecording,
+ isLocked
+ } = this.props;
+
+ let classList = ["devtools-button", "record-button"];
+
+ if (isRecording) {
+ classList.push("checked");
+ }
+
+ return button(
+ {
+ className: classList.join(" "),
+ onClick: onRecordButtonClick,
+ "data-standalone": "true",
+ "data-text-only": "true",
+ disabled: isLocked
+ },
+ isRecording ? L10N.getStr("recordings.stop") : L10N.getStr("recordings.start")
+ );
+ }
+});
diff --git a/devtools/client/performance/components/recording-controls.js b/devtools/client/performance/components/recording-controls.js
new file mode 100644
index 000000000..88f788ef3
--- /dev/null
+++ b/devtools/client/performance/components/recording-controls.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 {L10N} = require("devtools/client/performance/modules/global");
+const {DOM, createClass} = require("devtools/client/shared/vendor/react");
+const {div, button} = DOM;
+
+module.exports = createClass({
+ displayName: "Recording Controls",
+
+ render() {
+ let {
+ onClearButtonClick,
+ onRecordButtonClick,
+ onImportButtonClick,
+ isRecording,
+ isLocked
+ } = this.props;
+
+ let recordButtonClassList = ["devtools-button", "record-button"];
+
+ if (isRecording) {
+ recordButtonClassList.push("checked");
+ }
+
+ return (
+ div({ className: "devtools-toolbar" },
+ div({ className: "toolbar-group" },
+ button({
+ id: "clear-button",
+ className: "devtools-button",
+ title: L10N.getStr("recordings.clear.tooltip"),
+ onClick: onClearButtonClick
+ }),
+ button({
+ id: "main-record-button",
+ className: recordButtonClassList.join(" "),
+ disabled: isLocked,
+ title: L10N.getStr("recordings.start.tooltip"),
+ onClick: onRecordButtonClick
+ }),
+ button({
+ id: "import-button",
+ className: "devtools-button",
+ title: L10N.getStr("recordings.import.tooltip"),
+ onClick: onImportButtonClick
+ })
+ )
+ )
+ );
+ }
+});
diff --git a/devtools/client/performance/components/recording-list-item.js b/devtools/client/performance/components/recording-list-item.js
new file mode 100644
index 000000000..37efec90d
--- /dev/null
+++ b/devtools/client/performance/components/recording-list-item.js
@@ -0,0 +1,49 @@
+/* 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 {DOM, createClass} = require("devtools/client/shared/vendor/react");
+const {div, li, span, button} = DOM;
+const {L10N} = require("devtools/client/performance/modules/global");
+
+module.exports = createClass({
+ displayName: "Recording List Item",
+
+ render() {
+ const {
+ label,
+ duration,
+ onSelect,
+ onSave,
+ isLoading,
+ isSelected,
+ isRecording
+ } = this.props;
+
+ const className = `recording-list-item ${isSelected ? "selected" : ""}`;
+
+ let durationText;
+ if (isLoading) {
+ durationText = L10N.getStr("recordingsList.loadingLabel");
+ } else if (isRecording) {
+ durationText = L10N.getStr("recordingsList.recordingLabel");
+ } else {
+ durationText = L10N.getFormatStr("recordingsList.durationLabel", duration);
+ }
+
+ return (
+ li({ className, onClick: onSelect },
+ div({ className: "recording-list-item-label" },
+ label
+ ),
+ div({ className: "recording-list-item-footer" },
+ span({ className: "recording-list-item-duration" }, durationText),
+ button({ className: "recording-list-item-save", onClick: onSave },
+ L10N.getStr("recordingsList.saveLabel")
+ )
+ )
+ )
+ );
+ }
+});
diff --git a/devtools/client/performance/components/recording-list.js b/devtools/client/performance/components/recording-list.js
new file mode 100644
index 000000000..1df7f2b71
--- /dev/null
+++ b/devtools/client/performance/components/recording-list.js
@@ -0,0 +1,23 @@
+/* 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 {DOM, createClass} = require("devtools/client/shared/vendor/react");
+const {L10N} = require("devtools/client/performance/modules/global");
+const {ul, div} = DOM;
+
+module.exports = createClass({
+ displayName: "Recording List",
+
+ render() {
+ const {
+ items,
+ itemComponent: Item,
+ } = this.props;
+
+ return items.length > 0
+ ? ul({ className: "recording-list" }, ...items.map(Item))
+ : div({ className: "recording-list-empty" }, L10N.getStr("noRecordingsText"));
+ }
+});
diff --git a/devtools/client/performance/components/test/chrome.ini b/devtools/client/performance/components/test/chrome.ini
new file mode 100644
index 000000000..5ba24a9af
--- /dev/null
+++ b/devtools/client/performance/components/test/chrome.ini
@@ -0,0 +1,5 @@
+[DEFAULT]
+support-files =
+ head.js
+
+[test_jit_optimizations_01.html]
diff --git a/devtools/client/performance/components/test/head.js b/devtools/client/performance/components/test/head.js
new file mode 100644
index 000000000..be8184160
--- /dev/null
+++ b/devtools/client/performance/components/test/head.js
@@ -0,0 +1,187 @@
+/* Any copyright is dedicated to the Public Domain.
+ yield new Promise(function(){});
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/* global window, document, SimpleTest, requestAnimationFrame, is, ok */
+/* exported Cc, Ci, Cu, Cr, Assert, Task, TargetFactory, Toolbox, browserRequire,
+ forceRender, setProps, dumpn, checkOptimizationHeader, checkOptimizationTree */
+let { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+let { require } = Cu.import("resource://gre/modules/devtools/shared/Loader.jsm", {});
+let { Assert } = require("resource://testing-common/Assert.jsm");
+let { BrowserLoader } = Cu.import("resource://devtools/client/shared/browser-loader.js", {});
+let defer = require("devtools/shared/defer");
+let flags = require("devtools/shared/flags");
+let { Task } = require("devtools/shared/task");
+let { TargetFactory } = require("devtools/client/framework/target");
+let { Toolbox } = require("devtools/client/framework/toolbox");
+
+flags.testing = true;
+let { require: browserRequire } = BrowserLoader({
+ baseURI: "resource://devtools/client/performance/",
+ window
+});
+
+let $ = (selector, scope = document) => scope.querySelector(selector);
+let $$ = (selector, scope = document) => scope.querySelectorAll(selector);
+
+function forceRender(comp) {
+ return setState(comp, {})
+ .then(() => setState(comp, {}));
+}
+
+// All tests are asynchronous.
+SimpleTest.waitForExplicitFinish();
+
+function onNextAnimationFrame(fn) {
+ return () =>
+ requestAnimationFrame(() =>
+ requestAnimationFrame(fn));
+}
+
+function setState(component, newState) {
+ let deferred = defer();
+ component.setState(newState, onNextAnimationFrame(deferred.resolve));
+ return deferred.promise;
+}
+
+function setProps(component, newState) {
+ let deferred = defer();
+ component.setProps(newState, onNextAnimationFrame(deferred.resolve));
+ return deferred.promise;
+}
+
+function dumpn(msg) {
+ dump(`PERFORMANCE-COMPONENT-TEST: ${msg}\n`);
+}
+
+/**
+ * Default opts data for testing. First site has a simple IonType,
+ * and an IonType with an ObservedType, and a successful outcome.
+ * Second site does not have a successful outcome.
+ */
+let OPTS_DATA_GENERAL = [{
+ id: 1,
+ propertyName: "my property name",
+ line: 100,
+ column: 200,
+ samples: 90,
+ data: {
+ attempts: [
+ { id: 1, strategy: "GetElem_TypedObject", outcome: "AccessNotTypedObject" },
+ { id: 1, strategy: "GetElem_Dense", outcome: "AccessNotDense" },
+ { id: 1, strategy: "GetElem_TypedStatic", outcome: "Disabled" },
+ { id: 1, strategy: "GetElem_TypedArray", outcome: "GenericSuccess" },
+ ],
+ types: [{
+ id: 1,
+ site: "Receiver",
+ mirType: "Object",
+ typeset: [{
+ id: 1,
+ keyedBy: "constructor",
+ name: "MyView",
+ location: "http://internet.com/file.js",
+ line: "123",
+ }]
+ }, {
+ id: 1,
+ typeset: void 0,
+ site: "Index",
+ mirType: "Int32",
+ }]
+ }
+}, {
+ id: 2,
+ propertyName: void 0,
+ line: 50,
+ column: 51,
+ samples: 100,
+ data: {
+ attempts: [
+ { id: 2, strategy: "Call_Inline", outcome: "CantInlineBigData" }
+ ],
+ types: [{
+ id: 2,
+ site: "Call_Target",
+ mirType: "Object",
+ typeset: [
+ { id: 2, keyedBy: "primitive" },
+ { id: 2, keyedBy: "constructor", name: "B", location: "http://mypage.com/file.js", line: "2" },
+ { id: 2, keyedBy: "constructor", name: "C", location: "http://mypage.com/file.js", line: "3" },
+ { id: 2, keyedBy: "constructor", name: "D", location: "http://mypage.com/file.js", line: "4" },
+ ],
+ }]
+ }
+}];
+
+OPTS_DATA_GENERAL.forEach(site => {
+ site.data.types.forEach(type => {
+ if (type.typeset) {
+ type.typeset.id = site.id;
+ }
+ });
+ site.data.attempts.id = site.id;
+ site.data.types.id = site.id;
+});
+
+function checkOptimizationHeader(name, file, line) {
+ is($(".optimization-header .header-function-name").textContent, name,
+ "correct optimization header function name");
+ is($(".optimization-header .frame-link-filename").textContent, file,
+ "correct optimization header file name");
+ is($(".optimization-header .frame-link-line").textContent, `:${line}`,
+ "correct optimization header line");
+}
+
+function checkOptimizationTree(rowData) {
+ let rows = $$(".tree .tree-node");
+
+ for (let i = 0; i < rowData.length; i++) {
+ let row = rows[i];
+ let expected = rowData[i];
+
+ switch (expected.type) {
+ case "site":
+ is($(".optimization-site-title", row).textContent,
+ `${expected.strategy} – (${expected.samples} samples)`,
+ `row ${i}th: correct optimization site row`);
+
+ is(!!$(".opt-icon.warning", row), !!expected.failureIcon,
+ `row ${i}th: expected visibility of failure icon for unsuccessful outcomes`);
+ break;
+ case "types":
+ is($(".optimization-types", row).textContent,
+ `Types (${expected.count})`,
+ `row ${i}th: correct types row`);
+ break;
+ case "attempts":
+ is($(".optimization-attempts", row).textContent,
+ `Attempts (${expected.count})`,
+ `row ${i}th: correct attempts row`);
+ break;
+ case "type":
+ is($(".optimization-ion-type", row).textContent,
+ `${expected.site}:${expected.mirType}`,
+ `row ${i}th: correct ion type row`);
+ break;
+ case "observedtype":
+ is($(".optimization-observed-type-keyed", row).textContent,
+ expected.name ?
+ `${expected.keyedBy} → ${expected.name}` :
+ expected.keyedBy,
+ `row ${i}th: correct observed type row`);
+ break;
+ case "attempt":
+ is($(".optimization-strategy", row).textContent, expected.strategy,
+ `row ${i}th: correct attempt row, attempt item`);
+ is($(".optimization-outcome", row).textContent, expected.outcome,
+ `row ${i}th: correct attempt row, outcome item`);
+ ok($(".optimization-outcome", row)
+ .classList.contains(expected.success ? "success" : "failure"),
+ `row ${i}th: correct attempt row, failure/success status`);
+ break;
+ }
+ }
+}
diff --git a/devtools/client/performance/components/test/test_jit_optimizations_01.html b/devtools/client/performance/components/test/test_jit_optimizations_01.html
new file mode 100644
index 000000000..edc9c34cd
--- /dev/null
+++ b/devtools/client/performance/components/test/test_jit_optimizations_01.html
@@ -0,0 +1,70 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test the rendering of the JIT Optimizations tree. Tests when jit data has observed types, multiple observed types, multiple sites, a site with a successful strategy, site with no successful strategy.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>JITOptimizations component test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body style="height: 10000px;">
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ let ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ let React = browserRequire("devtools/client/shared/vendor/react");
+ let JITOptimizations = React.createFactory(browserRequire("devtools/client/performance/components/jit-optimizations"));
+ ok(JITOptimizations, "Should get JITOptimizations");
+ let opts;
+
+ opts = ReactDOM.render(JITOptimizations({
+ onViewSourceInDebugger: function(){},
+ frameData: {
+ isMetaCategory: false,
+ url: "http://internet.com/file.js",
+ line: 1,
+ functionName: "myfunc",
+ },
+ optimizationSites: OPTS_DATA_GENERAL,
+ autoExpandDepth: 1000,
+ }), window.document.body);
+ yield forceRender(opts);
+
+ checkOptimizationHeader("myfunc", "file.js", "1");
+
+ checkOptimizationTree([
+ { type: "site", strategy: "GetElem_TypedArray", samples: "90" },
+ { type: "types", count: "2" },
+ { type: "type", site: "Receiver", mirType: "Object" },
+ { type: "observedtype", keyedBy: "constructor", name: "MyView" },
+ { type: "type", site: "Index", mirType: "Int32" },
+ { type: "attempts", count: "4" },
+ { type: "attempt", strategy: "GetElem_TypedObject", outcome: "AccessNotTypedObject" },
+ { type: "attempt", strategy: "GetElem_Dense", outcome: "AccessNotDense" },
+ { type: "attempt", strategy: "GetElem_TypedStatic", outcome: "Disabled" },
+ { type: "attempt", strategy: "GetElem_TypedArray", outcome: "GenericSuccess", success: true },
+ { type: "site", strategy: "Call_Inline", samples: "100", failureIcon: true },
+ { type: "types", count: "1" },
+ { type: "type", site: "Call_Target", mirType: "Object" },
+ { type: "observedtype", keyedBy: "primitive" },
+ { type: "observedtype", keyedBy: "constructor", name: "B" },
+ { type: "observedtype", keyedBy: "constructor", name: "C" },
+ { type: "observedtype", keyedBy: "constructor", name: "D" },
+ { type: "attempts", count: "1" },
+ { type: "attempt", strategy: "Call_Inline", outcome: "CantInlineBigData" },
+ ]);
+
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/performance/components/waterfall-header.js b/devtools/client/performance/components/waterfall-header.js
new file mode 100644
index 000000000..f3030091b
--- /dev/null
+++ b/devtools/client/performance/components/waterfall-header.js
@@ -0,0 +1,69 @@
+/* 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";
+
+/**
+ * The "waterfall ticks" view, a header for the markers displayed in the waterfall.
+ */
+
+const { DOM: dom, PropTypes } = require("devtools/client/shared/vendor/react");
+const { L10N } = require("../modules/global");
+const { TickUtils } = require("../modules/waterfall-ticks");
+
+// ms
+const WATERFALL_HEADER_TICKS_MULTIPLE = 5;
+// px
+const WATERFALL_HEADER_TICKS_SPACING_MIN = 50;
+// px
+const WATERFALL_HEADER_TEXT_PADDING = 3;
+
+function WaterfallHeader(props) {
+ let { startTime, dataScale, sidebarWidth, waterfallWidth } = props;
+
+ let tickInterval = TickUtils.findOptimalTickInterval({
+ ticksMultiple: WATERFALL_HEADER_TICKS_MULTIPLE,
+ ticksSpacingMin: WATERFALL_HEADER_TICKS_SPACING_MIN,
+ dataScale: dataScale
+ });
+
+ let ticks = [];
+ for (let x = 0; x < waterfallWidth; x += tickInterval) {
+ let left = x + WATERFALL_HEADER_TEXT_PADDING;
+ let time = Math.round(x / dataScale + startTime);
+ let label = L10N.getFormatStr("timeline.tick", time);
+
+ let node = dom.div({
+ className: "plain waterfall-header-tick",
+ style: { transform: `translateX(${left}px)` }
+ }, label);
+ ticks.push(node);
+ }
+
+ return dom.div(
+ { className: "waterfall-header" },
+ dom.div(
+ {
+ className: "waterfall-sidebar theme-sidebar waterfall-header-name",
+ style: { width: sidebarWidth + "px" }
+ },
+ L10N.getStr("timeline.records")
+ ),
+ dom.div(
+ { className: "waterfall-header-ticks waterfall-background-ticks" },
+ ticks
+ )
+ );
+}
+
+WaterfallHeader.displayName = "WaterfallHeader";
+
+WaterfallHeader.propTypes = {
+ startTime: PropTypes.number.isRequired,
+ dataScale: PropTypes.number.isRequired,
+ sidebarWidth: PropTypes.number.isRequired,
+ waterfallWidth: PropTypes.number.isRequired,
+};
+
+module.exports = WaterfallHeader;
diff --git a/devtools/client/performance/components/waterfall-tree-row.js b/devtools/client/performance/components/waterfall-tree-row.js
new file mode 100644
index 000000000..b87750db1
--- /dev/null
+++ b/devtools/client/performance/components/waterfall-tree-row.js
@@ -0,0 +1,107 @@
+/* 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";
+
+/**
+ * A single row (node) in the waterfall tree
+ */
+
+const { DOM: dom, PropTypes } = require("devtools/client/shared/vendor/react");
+const { MarkerBlueprintUtils } = require("../modules/marker-blueprint-utils");
+
+// px
+const LEVEL_INDENT = 10;
+// px
+const ARROW_NODE_OFFSET = -14;
+// px
+const WATERFALL_MARKER_TIMEBAR_WIDTH_MIN = 5;
+
+function buildMarkerSidebar(blueprint, props) {
+ const { marker, level, sidebarWidth } = props;
+
+ let bullet = dom.div({
+ className: `waterfall-marker-bullet marker-color-${blueprint.colorName}`,
+ style: { transform: `translateX(${level * LEVEL_INDENT}px)` },
+ "data-type": marker.name
+ });
+
+ let label = MarkerBlueprintUtils.getMarkerLabel(marker);
+
+ let name = dom.div({
+ className: "plain waterfall-marker-name",
+ style: { transform: `translateX(${level * LEVEL_INDENT}px)` },
+ title: label
+ }, label);
+
+ return dom.div({
+ className: "waterfall-sidebar theme-sidebar",
+ style: { width: sidebarWidth + "px" }
+ }, bullet, name);
+}
+
+function buildMarkerTimebar(blueprint, props) {
+ const { marker, startTime, dataScale, arrow } = props;
+ const offset = (marker.start - startTime) * dataScale + ARROW_NODE_OFFSET;
+ const width = Math.max((marker.end - marker.start) * dataScale,
+ WATERFALL_MARKER_TIMEBAR_WIDTH_MIN);
+
+ let bar = dom.div(
+ {
+ className: "waterfall-marker-wrap",
+ style: { transform: `translateX(${offset}px)` }
+ },
+ arrow,
+ dom.div({
+ className: `waterfall-marker-bar marker-color-${blueprint.colorName}`,
+ style: { width: `${width}px` },
+ "data-type": marker.name
+ })
+ );
+
+ return dom.div(
+ { className: "waterfall-marker waterfall-background-ticks" },
+ bar
+ );
+}
+
+function WaterfallTreeRow(props) {
+ const { marker, focused } = props;
+ const blueprint = MarkerBlueprintUtils.getBlueprintFor(marker);
+
+ let attrs = {
+ className: "waterfall-tree-item" + (focused ? " focused" : ""),
+ "data-otmt": marker.isOffMainThread
+ };
+
+ // Don't render an expando-arrow for leaf nodes.
+ let submarkers = marker.submarkers;
+ let hasDescendants = submarkers && submarkers.length > 0;
+ if (hasDescendants) {
+ attrs["data-expandable"] = "";
+ } else {
+ attrs["data-invisible"] = "";
+ }
+
+ return dom.div(
+ attrs,
+ buildMarkerSidebar(blueprint, props),
+ buildMarkerTimebar(blueprint, props)
+ );
+}
+
+WaterfallTreeRow.displayName = "WaterfallTreeRow";
+
+WaterfallTreeRow.propTypes = {
+ marker: PropTypes.object.isRequired,
+ level: PropTypes.number.isRequired,
+ arrow: PropTypes.element.isRequired,
+ expanded: PropTypes.bool.isRequired,
+ focused: PropTypes.bool.isRequired,
+ startTime: PropTypes.number.isRequired,
+ dataScale: PropTypes.number.isRequired,
+ sidebarWidth: PropTypes.number.isRequired,
+};
+
+module.exports = WaterfallTreeRow;
diff --git a/devtools/client/performance/components/waterfall-tree.js b/devtools/client/performance/components/waterfall-tree.js
new file mode 100644
index 000000000..031c4facf
--- /dev/null
+++ b/devtools/client/performance/components/waterfall-tree.js
@@ -0,0 +1,167 @@
+/* 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 { createClass, createFactory, PropTypes } = require("devtools/client/shared/vendor/react");
+const Tree = createFactory(require("devtools/client/shared/components/tree"));
+const WaterfallTreeRow = createFactory(require("./waterfall-tree-row"));
+
+// px - keep in sync with var(--waterfall-tree-row-height) in performance.css
+const WATERFALL_TREE_ROW_HEIGHT = 15;
+
+/**
+ * Checks if a given marker is in the specified time range.
+ *
+ * @param object e
+ * The marker containing the { start, end } timestamps.
+ * @param number start
+ * The earliest allowed time.
+ * @param number end
+ * The latest allowed time.
+ * @return boolean
+ * True if the marker fits inside the specified time range.
+ */
+function isMarkerInRange(e, start, end) {
+ let mStart = e.start | 0;
+ let mEnd = e.end | 0;
+
+ return (
+ // bounds inside
+ (mStart >= start && mEnd <= end) ||
+ // bounds outside
+ (mStart < start && mEnd > end) ||
+ // overlap start
+ (mStart < start && mEnd >= start && mEnd <= end) ||
+ // overlap end
+ (mEnd > end && mStart >= start && mStart <= end)
+ );
+}
+
+const WaterfallTree = createClass({
+ displayName: "WaterfallTree",
+
+ propTypes: {
+ marker: PropTypes.object.isRequired,
+ startTime: PropTypes.number.isRequired,
+ endTime: PropTypes.number.isRequired,
+ dataScale: PropTypes.number.isRequired,
+ sidebarWidth: PropTypes.number.isRequired,
+ waterfallWidth: PropTypes.number.isRequired,
+ onFocus: PropTypes.func,
+ },
+
+ getInitialState() {
+ return {
+ focused: null,
+ expanded: new Set()
+ };
+ },
+
+ _getRoots(node) {
+ let roots = this.props.marker.submarkers || [];
+ return roots.filter(this._filter);
+ },
+
+ /**
+ * Find the parent node of 'node' with a depth-first search of the marker tree
+ */
+ _getParent(node) {
+ function findParent(marker) {
+ if (marker.submarkers) {
+ for (let submarker of marker.submarkers) {
+ if (submarker === node) {
+ return marker;
+ }
+
+ let parent = findParent(submarker);
+ if (parent) {
+ return parent;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ let rootMarker = this.props.marker;
+ let parent = findParent(rootMarker);
+
+ // We are interested only in parent markers that are rendered,
+ // which rootMarker is not. Return null if the parent is rootMarker.
+ return parent !== rootMarker ? parent : null;
+ },
+
+ _getChildren(node) {
+ let submarkers = node.submarkers || [];
+ return submarkers.filter(this._filter);
+ },
+
+ _getKey(node) {
+ return `marker-${node.index}`;
+ },
+
+ _isExpanded(node) {
+ return this.state.expanded.has(node);
+ },
+
+ _onExpand(node) {
+ this.setState(state => {
+ let expanded = new Set(state.expanded);
+ expanded.add(node);
+ return { expanded };
+ });
+ },
+
+ _onCollapse(node) {
+ this.setState(state => {
+ let expanded = new Set(state.expanded);
+ expanded.delete(node);
+ return { expanded };
+ });
+ },
+
+ _onFocus(node) {
+ this.setState({ focused: node });
+ if (this.props.onFocus) {
+ this.props.onFocus(node);
+ }
+ },
+
+ _filter(node) {
+ let { startTime, endTime } = this.props;
+ return isMarkerInRange(node, startTime, endTime);
+ },
+
+ _renderItem(marker, level, focused, arrow, expanded) {
+ let { startTime, dataScale, sidebarWidth } = this.props;
+ return WaterfallTreeRow({
+ marker,
+ level,
+ arrow,
+ expanded,
+ focused,
+ startTime,
+ dataScale,
+ sidebarWidth
+ });
+ },
+
+ render() {
+ return Tree({
+ getRoots: this._getRoots,
+ getParent: this._getParent,
+ getChildren: this._getChildren,
+ getKey: this._getKey,
+ isExpanded: this._isExpanded,
+ onExpand: this._onExpand,
+ onCollapse: this._onCollapse,
+ onFocus: this._onFocus,
+ renderItem: this._renderItem,
+ focused: this.state.focused,
+ itemHeight: WATERFALL_TREE_ROW_HEIGHT
+ });
+ }
+});
+
+module.exports = WaterfallTree;
diff --git a/devtools/client/performance/components/waterfall.js b/devtools/client/performance/components/waterfall.js
new file mode 100644
index 000000000..067033874
--- /dev/null
+++ b/devtools/client/performance/components/waterfall.js
@@ -0,0 +1,36 @@
+/* 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";
+
+/**
+ * This file contains the "waterfall" view, essentially a detailed list
+ * of all the markers in the timeline data.
+ */
+
+const { DOM: dom, createFactory, PropTypes } = require("devtools/client/shared/vendor/react");
+const WaterfallHeader = createFactory(require("./waterfall-header"));
+const WaterfallTree = createFactory(require("./waterfall-tree"));
+
+function Waterfall(props) {
+ return dom.div(
+ { className: "waterfall-markers" },
+ WaterfallHeader(props),
+ WaterfallTree(props)
+ );
+}
+
+Waterfall.displayName = "Waterfall";
+
+Waterfall.propTypes = {
+ marker: PropTypes.object.isRequired,
+ startTime: PropTypes.number.isRequired,
+ endTime: PropTypes.number.isRequired,
+ dataScale: PropTypes.number.isRequired,
+ sidebarWidth: PropTypes.number.isRequired,
+ waterfallWidth: PropTypes.number.isRequired,
+ onFocus: PropTypes.func,
+ onBlur: PropTypes.func,
+};
+
+module.exports = Waterfall;
diff --git a/devtools/client/performance/docs/markers.md b/devtools/client/performance/docs/markers.md
new file mode 100644
index 000000000..e743f7fcd
--- /dev/null
+++ b/devtools/client/performance/docs/markers.md
@@ -0,0 +1,189 @@
+# Timeline Markers
+
+## Common
+
+* DOMHighResTimeStamp start
+* DOMHighResTimeStamp end
+* DOMString name
+* object? stack
+* object? endStack
+* unsigned short processType;
+* boolean isOffMainThread;
+
+The `processType` a GeckoProcessType enum listed in xpcom/build/nsXULAppAPI.h,
+specifying if this marker originates in a content, chrome, plugin etc. process.
+Additionally, markers may be created from any thread on those processes, and
+`isOffMainThread` highights whether or not they're from the main thread. The most
+common type of marker is probably going to be from a GeckoProcessType_Content's
+main thread when debugging content.
+
+## DOMEvent
+
+Triggered when a DOM event occurs, like a click or a keypress.
+
+* unsigned short eventPhase - a number indicating what phase this event is
+ in (target, bubbling, capturing, maps to Ci.nsIDOMEvent constants)
+* DOMString type - the type of event, like "keypress" or "click"
+
+## Reflow
+
+Reflow markers (labeled as "Layout") indicate when a change has occurred to
+a DOM element's positioning that requires the frame tree (rendering
+representation of the DOM) to figure out the new position of a handful of
+elements. Fired via `PresShell::DoReflow`
+
+## Paint
+
+* sequence<{ long height, long width, long x, long y }> rectangles - An array
+ of rectangle objects indicating where painting has occurred.
+
+## Styles
+
+Style markers (labeled as "Recalculating Styles") are triggered when Gecko
+needs to figure out the computational style of an element. Fired via
+`RestyleTracker::DoProcessRestyles` when there are elements to restyle.
+
+* DOMString restyleHint - A string indicating what kind of restyling will need
+ to be processed; for example "eRestyle_StyleAttribute" is relatively cheap,
+ whereas "eRestyle_Subtree" is more expensive. The hint can be a string of
+ any amount of the following, separated via " | ". All future restyleHints
+ are from `RestyleManager::RestyleHintToString`.
+
+ * "eRestyle_Self"
+ * "eRestyle_Subtree"
+ * "eRestyle_LaterSiblings"
+ * "eRestyle_CSSTransitions"
+ * "eRestyle_CSSAnimations"
+ * "eRestyle_SVGAttrAnimations"
+ * "eRestyle_StyleAttribute"
+ * "eRestyle_StyleAttribute_Animations"
+ * "eRestyle_Force"
+ * "eRestyle_ForceDescendants"
+
+
+## Javascript
+
+`Javascript` markers are emitted indicating when JS execution begins and ends,
+with a reason that triggered it (causeName), like a requestAnimationFrame or
+a setTimeout.
+
+* string causeName - The reason that JS was entered. There are many possible
+ reasons, and the interesting ones to show web developers (triggered by content) are:
+
+ * "\<script\> element"
+ * "EventListener.handleEvent"
+ * "setInterval handler"
+ * "setTimeout handler"
+ * "FrameRequestCallback"
+ * "EventHandlerNonNull"
+ * "promise callback"
+ * "promise initializer"
+ * "Worker runnable"
+
+ There are also many more potential JS causes, some which are just internally
+ used and won't emit a marker, but the below ones are only of interest to
+ Gecko hackers, most likely
+
+ * "promise thenable"
+ * "worker runnable"
+ * "nsHTTPIndex set HTTPIndex property"
+ * "XPCWrappedJS method call"
+ * "nsHTTPIndex OnFTPControlLog"
+ * "XPCWrappedJS QueryInterface"
+ * "xpcshell argument processingâ€
+ * "XPConnect sandbox evaluation "
+ * "component loader report global"
+ * "component loader load module"
+ * "Cross-Process Object Wrapper call/construct"
+ * "Cross-Process Object Wrapper ’set'"
+ * "Cross-Process Object Wrapper 'get'"
+ * "nsXULTemplateBuilder creation"
+ * "TestShellCommand"
+ * "precompiled XUL \<script\> element"
+ * "XBL \<field\> initialization "
+ * "NPAPI NPN_evaluate"
+ * "NPAPI get"
+ * "NPAPI set"
+ * "NPAPI doInvoke"
+ * "javascript: URI"
+ * "geolocation.always_precise indexing"
+ * "geolocation.app_settings enumeration"
+ * "WebIDL dictionary creation"
+ * "XBL \<constructor\>/\<destructor\> invocation"
+ * "message manager script load"
+ * "message handler script load"
+ * "nsGlobalWindow report new global"
+
+## GarbageCollection
+
+Emitted after a full GC cycle has completed (which is after any number of
+incremental slices).
+
+* DOMString causeName - The reason for a GC event to occur. A full list of
+ GC reasons can be found [on MDN](https://developer.mozilla.org/en-US/docs/Tools/Debugger-API/Debugger.Memory#Debugger.Memory_Handler_Functions).
+* DOMString nonincremenetalReason - If the GC could not do an incremental
+ GC (smaller, quick GC events), and we have to walk the entire heap and
+ GC everything marked, then the reason listed here is why.
+
+## nsCycleCollector::Collect
+
+An `nsCycleCollector::Collect` marker is emitted for each incremental cycle
+collection slice and each non-incremental cycle collection.
+
+# nsCycleCollector::ForgetSkippable
+
+`nsCycleCollector::ForgetSkippable` is presented as "Cycle Collection", but in
+reality it is preparation/pre-optimization for cycle collection, and not the
+actual tracing of edges and collecting of cycles.
+
+## ConsoleTime
+
+A marker generated via `console.time()` and `console.timeEnd()`.
+
+* DOMString causeName - the label passed into `console.time(label)` and
+ `console.timeEnd(label)` if passed in.
+
+## TimeStamp
+
+A marker generated via `console.timeStamp(label)`.
+
+* DOMString causeName - the label passed into `console.timeStamp(label)`
+ if passed in.
+
+## document::DOMContentLoaded
+
+A marker generated when the DOMContentLoaded event is fired.
+
+## document::Load
+
+A marker generated when the document's "load" event is fired.
+
+## Parse HTML
+
+## Parse XML
+
+## Worker
+
+Emitted whenever there's an operation dealing with Workers (any kind of worker,
+Web Workers, Service Workers etc.). Currently there are 4 types of operations
+being tracked: serializing/deserializing data on the main thread, and also
+serializing/deserializing data off the main thread.
+
+* ProfileTimelineWorkerOperationType operationType - the type of operation
+ being done by the Worker or the main thread when dealing with workers.
+ Can be one of the following enums defined in ProfileTimelineMarker.webidl
+ * "serializeDataOffMainThread"
+ * "serializeDataOnMainThread"
+ * "deserializeDataOffMainThread"
+ * "deserializeDataOnMainThread"
+
+## Composite
+
+Composite markers trace the actual time an inner composite operation
+took on the compositor thread. Currently, these markers are only especially
+interesting for Gecko platform developers, and thus disabled by default.
+
+## CompositeForwardTransaction
+
+Markers generated when the IPC request was made to the compositor from
+the child process's main thread.
diff --git a/devtools/client/performance/events.js b/devtools/client/performance/events.js
new file mode 100644
index 000000000..27514ed18
--- /dev/null
+++ b/devtools/client/performance/events.js
@@ -0,0 +1,108 @@
+/* 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 ControllerEvents = {
+ // Fired when a performance pref changes (either because the user changed it
+ // via the tool's UI, by changing something about:config or programatically).
+ PREF_CHANGED: "Performance:PrefChanged",
+
+ // Fired when the devtools theme changes.
+ THEME_CHANGED: "Performance:ThemeChanged",
+
+ // When a new recording model is received by the controller.
+ RECORDING_ADDED: "Performance:RecordingAdded",
+
+ // When a recording model gets removed from the controller.
+ RECORDING_DELETED: "Performance:RecordingDeleted",
+
+ // When a recording model becomes "started", "stopping" or "stopped".
+ RECORDING_STATE_CHANGE: "Performance:RecordingStateChange",
+
+ // When a recording is offering information on the profiler's circular buffer.
+ RECORDING_PROFILER_STATUS_UPDATE: "Performance:RecordingProfilerStatusUpdate",
+
+ // When a recording model becomes marked as selected.
+ RECORDING_SELECTED: "Performance:RecordingSelected",
+
+ // When starting a recording is attempted and fails because the backend
+ // does not permit it at this time.
+ BACKEND_FAILED_AFTER_RECORDING_START: "Performance:BackendFailedRecordingStart",
+
+ // When a recording is started and the backend has started working.
+ BACKEND_READY_AFTER_RECORDING_START: "Performance:BackendReadyRecordingStart",
+
+ // When a recording is stopped and the backend has finished cleaning up.
+ BACKEND_READY_AFTER_RECORDING_STOP: "Performance:BackendReadyRecordingStop",
+
+ // When a recording is exported.
+ RECORDING_EXPORTED: "Performance:RecordingExported",
+
+ // When a recording is imported.
+ RECORDING_IMPORTED: "Performance:RecordingImported",
+
+ // When a source is shown in the JavaScript Debugger at a specific location.
+ SOURCE_SHOWN_IN_JS_DEBUGGER: "Performance:UI:SourceShownInJsDebugger",
+ SOURCE_NOT_FOUND_IN_JS_DEBUGGER: "Performance:UI:SourceNotFoundInJsDebugger",
+
+ // Fired by the PerformanceController when `populateWithRecordings` is finished.
+ RECORDINGS_SEEDED: "Performance:RecordingsSeeded",
+};
+
+const ViewEvents = {
+ // Emitted by the `ToolbarView` when a preference changes.
+ UI_PREF_CHANGED: "Performance:UI:PrefChanged",
+
+ // When the state (display mode) changes, for example when switching between
+ // "empty", "recording" or "recorded". This causes certain parts of the UI
+ // to be hidden or visible.
+ UI_STATE_CHANGED: "Performance:UI:StateChanged",
+
+ // Emitted by the `PerformanceView` on clear button click.
+ UI_CLEAR_RECORDINGS: "Performance:UI:ClearRecordings",
+
+ // Emitted by the `PerformanceView` on record button click.
+ UI_START_RECORDING: "Performance:UI:StartRecording",
+ UI_STOP_RECORDING: "Performance:UI:StopRecording",
+
+ // Emitted by the `PerformanceView` on import/export button click.
+ UI_IMPORT_RECORDING: "Performance:UI:ImportRecording",
+ UI_EXPORT_RECORDING: "Performance:UI:ExportRecording",
+
+ // Emitted by the `PerformanceView` when the profiler's circular buffer
+ // status has been rendered.
+ UI_RECORDING_PROFILER_STATUS_RENDERED: "Performance:UI:RecordingProfilerStatusRendered",
+
+ // When a recording is selected in the UI.
+ UI_RECORDING_SELECTED: "Performance:UI:RecordingSelected",
+
+ // Emitted by the `DetailsView` when a subview is selected
+ UI_DETAILS_VIEW_SELECTED: "Performance:UI:DetailsViewSelected",
+
+ // Emitted by the `OverviewView` after something has been rendered.
+ UI_OVERVIEW_RENDERED: "Performance:UI:OverviewRendered",
+ UI_MARKERS_GRAPH_RENDERED: "Performance:UI:OverviewMarkersRendered",
+ UI_MEMORY_GRAPH_RENDERED: "Performance:UI:OverviewMemoryRendered",
+ UI_FRAMERATE_GRAPH_RENDERED: "Performance:UI:OverviewFramerateRendered",
+
+ // Emitted by the `OverviewView` when a range has been selected in the graphs.
+ UI_OVERVIEW_RANGE_SELECTED: "Performance:UI:OverviewRangeSelected",
+
+ // Emitted by the `WaterfallView` when it has been rendered.
+ UI_WATERFALL_RENDERED: "Performance:UI:WaterfallRendered",
+
+ // Emitted by the `JsCallTreeView` when it has been rendered.
+ UI_JS_CALL_TREE_RENDERED: "Performance:UI:JsCallTreeRendered",
+
+ // Emitted by the `JsFlameGraphView` when it has been rendered.
+ UI_JS_FLAMEGRAPH_RENDERED: "Performance:UI:JsFlameGraphRendered",
+
+ // Emitted by the `MemoryCallTreeView` when it has been rendered.
+ UI_MEMORY_CALL_TREE_RENDERED: "Performance:UI:MemoryCallTreeRendered",
+
+ // Emitted by the `MemoryFlameGraphView` when it has been rendered.
+ UI_MEMORY_FLAMEGRAPH_RENDERED: "Performance:UI:MemoryFlameGraphRendered",
+};
+
+module.exports = Object.assign({}, ControllerEvents, ViewEvents);
diff --git a/devtools/client/performance/legacy/actors.js b/devtools/client/performance/legacy/actors.js
new file mode 100644
index 000000000..22b4f85b1
--- /dev/null
+++ b/devtools/client/performance/legacy/actors.js
@@ -0,0 +1,263 @@
+/* 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 { Task } = require("devtools/shared/task");
+
+const EventEmitter = require("devtools/shared/event-emitter");
+const { Poller } = require("devtools/client/shared/poller");
+
+const CompatUtils = require("devtools/client/performance/legacy/compatibility");
+const RecordingUtils = require("devtools/shared/performance/recording-utils");
+const { TimelineFront } = require("devtools/shared/fronts/timeline");
+const { ProfilerFront } = require("devtools/shared/fronts/profiler");
+
+// How often do we check the status of the profiler's circular buffer in milliseconds.
+const PROFILER_CHECK_TIMER = 5000;
+
+const TIMELINE_ACTOR_METHODS = [
+ "start", "stop",
+];
+
+const PROFILER_ACTOR_METHODS = [
+ "startProfiler", "getStartOptions", "stopProfiler",
+ "registerEventNotifications", "unregisterEventNotifications"
+];
+
+/**
+ * Constructor for a facade around an underlying ProfilerFront.
+ */
+function LegacyProfilerFront(target) {
+ this._target = target;
+ this._onProfilerEvent = this._onProfilerEvent.bind(this);
+ this._checkProfilerStatus = this._checkProfilerStatus.bind(this);
+ this._PROFILER_CHECK_TIMER = this._target.TEST_MOCK_PROFILER_CHECK_TIMER ||
+ PROFILER_CHECK_TIMER;
+
+ EventEmitter.decorate(this);
+}
+
+LegacyProfilerFront.prototype = {
+ EVENTS: ["console-api-profiler", "profiler-stopped"],
+
+ // Connects to the targets underlying real ProfilerFront.
+ connect: Task.async(function* () {
+ let target = this._target;
+ this._front = new ProfilerFront(target.client, target.form);
+
+ // Fetch and store information about the SPS profiler and
+ // server profiler.
+ this.traits = {};
+ this.traits.filterable = target.getTrait("profilerDataFilterable");
+
+ // Directly register to event notifications when connected
+ // to hook into `console.profile|profileEnd` calls.
+ yield this.registerEventNotifications({ events: this.EVENTS });
+ target.client.addListener("eventNotification", this._onProfilerEvent);
+ }),
+
+ /**
+ * Unregisters events for the underlying profiler actor.
+ */
+ destroy: Task.async(function* () {
+ if (this._poller) {
+ yield this._poller.destroy();
+ }
+ yield this.unregisterEventNotifications({ events: this.EVENTS });
+ this._target.client.removeListener("eventNotification", this._onProfilerEvent);
+ yield this._front.destroy();
+ }),
+
+ /**
+ * Starts the profiler actor, if necessary.
+ *
+ * @option {number?} bufferSize
+ * @option {number?} sampleFrequency
+ */
+ start: Task.async(function* (options = {}) {
+ // Check for poller status even if the profiler is already active --
+ // profiler can be activated via `console.profile` or another source, like
+ // the Gecko Profiler.
+ if (!this._poller) {
+ this._poller = new Poller(this._checkProfilerStatus, this._PROFILER_CHECK_TIMER,
+ false);
+ }
+ if (!this._poller.isPolling()) {
+ this._poller.on();
+ }
+
+ // Start the profiler only if it wasn't already active. The built-in
+ // nsIPerformance module will be kept recording, because it's the same instance
+ // for all targets and interacts with the whole platform, so we don't want
+ // to affect other clients by stopping (or restarting) it.
+ let {
+ isActive,
+ currentTime,
+ position,
+ generation,
+ totalSize
+ } = yield this.getStatus();
+
+ if (isActive) {
+ return { startTime: currentTime, position, generation, totalSize };
+ }
+
+ // Translate options from the recording model into profiler-specific
+ // options for the nsIProfiler
+ let profilerOptions = {
+ entries: options.bufferSize,
+ interval: options.sampleFrequency
+ ? (1000 / (options.sampleFrequency * 1000))
+ : void 0
+ };
+
+ let startInfo = yield this.startProfiler(profilerOptions);
+ let startTime = 0;
+ if ("currentTime" in startInfo) {
+ startTime = startInfo.currentTime;
+ }
+
+ return { startTime, position, generation, totalSize };
+ }),
+
+ /**
+ * Indicates the end of a recording -- does not actually stop the profiler
+ * (stopProfiler does that), but notes that we no longer need to poll
+ * for buffer status.
+ */
+ stop: Task.async(function* () {
+ yield this._poller.off();
+ }),
+
+ /**
+ * Wrapper around `profiler.isActive()` to take profiler status data and emit.
+ */
+ getStatus: Task.async(function* () {
+ let data = yield (CompatUtils.callFrontMethod("isActive").call(this));
+ // If no data, the last poll for `isActive()` was wrapping up, and the target.client
+ // is now null, so we no longer have data, so just abort here.
+ if (!data) {
+ return undefined;
+ }
+
+ // If TEST_PROFILER_FILTER_STATUS defined (via array of fields), filter
+ // out any field from isActive, used only in tests. Used to filter out
+ // buffer status fields to simulate older geckos.
+ if (this._target.TEST_PROFILER_FILTER_STATUS) {
+ data = Object.keys(data).reduce((acc, prop) => {
+ if (this._target.TEST_PROFILER_FILTER_STATUS.indexOf(prop) === -1) {
+ acc[prop] = data[prop];
+ }
+ return acc;
+ }, {});
+ }
+
+ this.emit("profiler-status", data);
+ return data;
+ }),
+
+ /**
+ * Returns profile data from now since `startTime`.
+ */
+ getProfile: Task.async(function* (options) {
+ let profilerData = yield (CompatUtils.callFrontMethod("getProfile")
+ .call(this, options));
+ // If the backend is not deduped, dedupe it ourselves, as rest of the code
+ // expects a deduped profile.
+ if (profilerData.profile.meta.version === 2) {
+ RecordingUtils.deflateProfile(profilerData.profile);
+ }
+
+ // If the backend does not support filtering by start and endtime on
+ // platform (< Fx40), do it on the client (much slower).
+ if (!this.traits.filterable) {
+ RecordingUtils.filterSamples(profilerData.profile, options.startTime || 0);
+ }
+
+ return profilerData;
+ }),
+
+ /**
+ * Invoked whenever a registered event was emitted by the profiler actor.
+ *
+ * @param object response
+ * The data received from the backend.
+ */
+ _onProfilerEvent: function (_, { topic, subject, details }) {
+ if (topic === "console-api-profiler") {
+ if (subject.action === "profile") {
+ this.emit("console-profile-start", details);
+ } else if (subject.action === "profileEnd") {
+ this.emit("console-profile-stop", details);
+ }
+ } else if (topic === "profiler-stopped") {
+ this.emit("profiler-stopped");
+ }
+ },
+
+ _checkProfilerStatus: Task.async(function* () {
+ // Calling `getStatus()` will emit the "profiler-status" on its own
+ yield this.getStatus();
+ }),
+
+ toString: () => "[object LegacyProfilerFront]"
+};
+
+/**
+ * Constructor for a facade around an underlying TimelineFront.
+ */
+function LegacyTimelineFront(target) {
+ this._target = target;
+ EventEmitter.decorate(this);
+}
+
+LegacyTimelineFront.prototype = {
+ EVENTS: ["markers", "frames", "ticks"],
+
+ connect: Task.async(function* () {
+ let supported = yield CompatUtils.timelineActorSupported(this._target);
+ this._front = supported ?
+ new TimelineFront(this._target.client, this._target.form) :
+ new CompatUtils.MockTimelineFront();
+
+ this.IS_MOCK = !supported;
+
+ // Binds underlying actor events and consolidates them to a `timeline-data`
+ // exposed event.
+ this.EVENTS.forEach(type => {
+ let handler = this[`_on${type}`] = this._onTimelineData.bind(this, type);
+ this._front.on(type, handler);
+ });
+ }),
+
+ /**
+ * Override actor's destroy, so we can unregister listeners before
+ * destroying the underlying actor.
+ */
+ destroy: Task.async(function* () {
+ this.EVENTS.forEach(type => this._front.off(type, this[`_on${type}`]));
+ yield this._front.destroy();
+ }),
+
+ /**
+ * An aggregate of all events (markers, frames, ticks) and exposes
+ * to PerformanceActorsConnection as a single event.
+ */
+ _onTimelineData: function (type, ...data) {
+ this.emit("timeline-data", type, ...data);
+ },
+
+ toString: () => "[object LegacyTimelineFront]"
+};
+
+// Bind all the methods that directly proxy to the actor
+PROFILER_ACTOR_METHODS.forEach(m => {
+ LegacyProfilerFront.prototype[m] = CompatUtils.callFrontMethod(m);
+});
+TIMELINE_ACTOR_METHODS.forEach(m => {
+ LegacyTimelineFront.prototype[m] = CompatUtils.callFrontMethod(m);
+});
+
+exports.LegacyProfilerFront = LegacyProfilerFront;
+exports.LegacyTimelineFront = LegacyTimelineFront;
diff --git a/devtools/client/performance/legacy/compatibility.js b/devtools/client/performance/legacy/compatibility.js
new file mode 100644
index 000000000..0c67800d0
--- /dev/null
+++ b/devtools/client/performance/legacy/compatibility.js
@@ -0,0 +1,66 @@
+/* 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 EventEmitter = require("devtools/shared/event-emitter");
+
+/**
+ * A dummy front decorated with the provided methods.
+ *
+ * @param array blueprint
+ * A list of [funcName, retVal] describing the class.
+ */
+function MockFront(blueprint) {
+ EventEmitter.decorate(this);
+
+ for (let [funcName, retVal] of blueprint) {
+ this[funcName] = (x => typeof x === "function" ? x() : x).bind(this, retVal);
+ }
+}
+
+function MockTimelineFront() {
+ MockFront.call(this, [
+ ["destroy"],
+ ["start", 0],
+ ["stop", 0],
+ ]);
+}
+
+/**
+ * Takes a TabTarget, and checks existence of a TimelineActor on
+ * the server, or if TEST_MOCK_TIMELINE_ACTOR is to be used.
+ *
+ * @param {TabTarget} target
+ * @return {Boolean}
+ */
+function timelineActorSupported(target) {
+ // This `target` property is used only in tests to test
+ // instances where the timeline actor is not available.
+ if (target.TEST_MOCK_TIMELINE_ACTOR) {
+ return false;
+ }
+
+ return target.hasActor("timeline");
+}
+
+/**
+ * Returns a function to be used as a method on an "Front" in ./actors.
+ * Calls the underlying actor's method.
+ */
+function callFrontMethod(method) {
+ return function () {
+ // If there's no target or client on this actor facade,
+ // abort silently -- this occurs in tests when polling occurs
+ // after the test ends, when tests do not wait for toolbox destruction
+ // (which will destroy the actor facade, turning off the polling).
+ if (!this._target || !this._target.client) {
+ return undefined;
+ }
+ return this._front[method].apply(this._front, arguments);
+ };
+}
+
+exports.MockTimelineFront = MockTimelineFront;
+exports.timelineActorSupported = timelineActorSupported;
+exports.callFrontMethod = callFrontMethod;
diff --git a/devtools/client/performance/legacy/front.js b/devtools/client/performance/legacy/front.js
new file mode 100644
index 000000000..34fb16665
--- /dev/null
+++ b/devtools/client/performance/legacy/front.js
@@ -0,0 +1,484 @@
+/* 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 { Task } = require("devtools/shared/task");
+
+const Services = require("Services");
+const promise = require("promise");
+const { extend } = require("sdk/util/object");
+
+const Actors = require("devtools/client/performance/legacy/actors");
+const { LegacyPerformanceRecording } = require("devtools/client/performance/legacy/recording");
+const { importRecording } = require("devtools/client/performance/legacy/recording");
+const { normalizePerformanceFeatures } = require("devtools/shared/performance/recording-utils");
+const flags = require("devtools/shared/flags");
+const { getDeviceFront } = require("devtools/shared/device/device");
+const { getSystemInfo } = require("devtools/shared/system");
+const events = require("sdk/event/core");
+const { EventTarget } = require("sdk/event/target");
+const { Class } = require("sdk/core/heritage");
+
+/**
+ * A connection to underlying actors (profiler, framerate, etc.)
+ * shared by all tools in a target.
+ */
+const LegacyPerformanceFront = Class({
+ extends: EventTarget,
+
+ LEGACY_FRONT: true,
+
+ traits: {
+ features: {
+ withMarkers: true,
+ withTicks: true,
+ withMemory: false,
+ withFrames: false,
+ withGCEvents: false,
+ withDocLoadingEvents: false,
+ withAllocations: false,
+ },
+ },
+
+ initialize: function (target) {
+ let { form, client } = target;
+ this._target = target;
+ this._form = form;
+ this._client = client;
+ this._pendingConsoleRecordings = [];
+ this._sitesPullTimeout = 0;
+ this._recordings = [];
+
+ this._pipeToFront = this._pipeToFront.bind(this);
+ this._onTimelineData = this._onTimelineData.bind(this);
+ this._onConsoleProfileStart = this._onConsoleProfileStart.bind(this);
+ this._onConsoleProfileStop = this._onConsoleProfileStop.bind(this);
+ this._onProfilerStatus = this._onProfilerStatus.bind(this);
+ this._onProfilerUnexpectedlyStopped = this._onProfilerUnexpectedlyStopped.bind(this);
+ },
+
+ /**
+ * Initializes a connection to the profiler and other miscellaneous actors.
+ * If in the process of opening, or already open, nothing happens.
+ *
+ * @return object
+ * A promise that is resolved once the connection is established.
+ */
+ connect: Task.async(function* () {
+ if (this._connecting) {
+ return this._connecting.promise;
+ }
+
+ // Create a promise that gets resolved upon connecting, so that
+ // other attempts to open the connection use the same resolution promise
+ this._connecting = promise.defer();
+
+ // Sets `this._profiler`, `this._timeline`.
+ // Only initialize the timeline fronts if the respective actors
+ // are available. Older Gecko versions don't have existing implementations,
+ // in which case all the methods we need can be easily mocked.
+ yield this._connectActors();
+ yield this._registerListeners();
+
+ this._connecting.resolve();
+ return this._connecting.promise;
+ }),
+
+ /**
+ * Destroys this connection.
+ */
+ destroy: Task.async(function* () {
+ if (this._connecting) {
+ yield this._connecting.promise;
+ } else {
+ return;
+ }
+
+ yield this._unregisterListeners();
+ yield this._disconnectActors();
+
+ this._connecting = null;
+ this._profiler = null;
+ this._timeline = null;
+ this._client = null;
+ this._form = null;
+ this._target = this._target;
+ }),
+
+ /**
+ * Initializes fronts and connects to the underlying actors using the facades
+ * found in ./actors.js.
+ */
+ _connectActors: Task.async(function* () {
+ this._profiler = new Actors.LegacyProfilerFront(this._target);
+ this._timeline = new Actors.LegacyTimelineFront(this._target);
+
+ yield promise.all([
+ this._profiler.connect(),
+ this._timeline.connect()
+ ]);
+
+ // If mocked timeline, update the traits
+ this.traits.features.withMarkers = !this._timeline.IS_MOCK;
+ this.traits.features.withTicks = !this._timeline.IS_MOCK;
+ }),
+
+ /**
+ * Registers listeners on events from the underlying
+ * actors, so the connection can handle them.
+ */
+ _registerListeners: function () {
+ this._timeline.on("timeline-data", this._onTimelineData);
+ this._profiler.on("console-profile-start", this._onConsoleProfileStart);
+ this._profiler.on("console-profile-stop", this._onConsoleProfileStop);
+ this._profiler.on("profiler-stopped", this._onProfilerUnexpectedlyStopped);
+ this._profiler.on("profiler-status", this._onProfilerStatus);
+ },
+
+ /**
+ * Unregisters listeners on events on the underlying actors.
+ */
+ _unregisterListeners: function () {
+ this._timeline.off("timeline-data", this._onTimelineData);
+ this._profiler.off("console-profile-start", this._onConsoleProfileStart);
+ this._profiler.off("console-profile-stop", this._onConsoleProfileStop);
+ this._profiler.off("profiler-stopped", this._onProfilerUnexpectedlyStopped);
+ this._profiler.off("profiler-status", this._onProfilerStatus);
+ },
+
+ /**
+ * Closes the connections to non-profiler actors.
+ */
+ _disconnectActors: Task.async(function* () {
+ yield promise.all([
+ this._profiler.destroy(),
+ this._timeline.destroy(),
+ ]);
+ }),
+
+ /**
+ * Invoked whenever `console.profile` is called.
+ *
+ * @param string profileLabel
+ * The provided string argument if available; undefined otherwise.
+ * @param number currentTime
+ * The time (in milliseconds) when the call was made, relative to when
+ * the nsIProfiler module was started.
+ */
+ _onConsoleProfileStart: Task.async(function* (_, { profileLabel,
+ currentTime: startTime }) {
+ let recordings = this._recordings;
+
+ // Abort if a profile with this label already exists.
+ if (recordings.find(e => e.getLabel() === profileLabel)) {
+ return;
+ }
+
+ events.emit(this, "console-profile-start");
+
+ yield this.startRecording(extend({}, getLegacyPerformanceRecordingPrefs(), {
+ console: true,
+ label: profileLabel
+ }));
+ }),
+
+ /**
+ * Invoked whenever `console.profileEnd` is called.
+ *
+ * @param string profileLabel
+ * The provided string argument if available; undefined otherwise.
+ * @param number currentTime
+ * The time (in milliseconds) when the call was made, relative to when
+ * the nsIProfiler module was started.
+ */
+ _onConsoleProfileStop: Task.async(function* (_, data) {
+ // If no data, abort; can occur if profiler isn't running and we get a surprise
+ // call to console.profileEnd()
+ if (!data) {
+ return;
+ }
+ let { profileLabel } = data;
+
+ let pending = this._recordings.filter(r => r.isConsole() && r.isRecording());
+ if (pending.length === 0) {
+ return;
+ }
+
+ let model;
+ // Try to find the corresponding `console.profile` call if
+ // a label was used in profileEnd(). If no matches, abort.
+ if (profileLabel) {
+ model = pending.find(e => e.getLabel() === profileLabel);
+ } else {
+ // If no label supplied, pop off the most recent pending console recording
+ model = pending[pending.length - 1];
+ }
+
+ // If `profileEnd()` was called with a label, and there are no matching
+ // sessions, abort.
+ if (!model) {
+ console.error(
+ "console.profileEnd() called with label that does not match a recording.");
+ return;
+ }
+
+ yield this.stopRecording(model);
+ }),
+
+ /**
+ * TODO handle bug 1144438
+ */
+ _onProfilerUnexpectedlyStopped: function () {
+ console.error("Profiler unexpectedly stopped.", arguments);
+ },
+
+ /**
+ * Called whenever there is timeline data of any of the following types:
+ * - markers
+ * - frames
+ * - ticks
+ *
+ * Populate our internal store of recordings for all currently recording sessions.
+ */
+ _onTimelineData: function (_, ...data) {
+ this._recordings.forEach(e => e._addTimelineData.apply(e, data));
+ events.emit(this, "timeline-data", ...data);
+ },
+
+ /**
+ * Called whenever the underlying profiler polls its current status.
+ */
+ _onProfilerStatus: function (_, data) {
+ // If no data emitted (whether from an older actor being destroyed
+ // from a previous test, or the server does not support it), just ignore.
+ if (!data || data.position === void 0) {
+ return;
+ }
+
+ this._currentBufferStatus = data;
+ events.emit(this, "profiler-status", data);
+ },
+
+ /**
+ * Begins a recording session
+ *
+ * @param object options
+ * An options object to pass to the actors. Supported properties are
+ * `withTicks`, `withMemory` and `withAllocations`, `probability`, and
+ * `maxLogLength`.
+ * @return object
+ * A promise that is resolved once recording has started.
+ */
+ startRecording: Task.async(function* (options = {}) {
+ let model = new LegacyPerformanceRecording(
+ normalizePerformanceFeatures(options, this.traits.features));
+
+ // All actors are started asynchronously over the remote debugging protocol.
+ // Get the corresponding start times from each one of them.
+ // The timeline actors are target-dependent, so start those as well,
+ // even though these are mocked in older Geckos (FF < 35)
+ let profilerStart = this._profiler.start(options);
+ let timelineStart = this._timeline.start(options);
+
+ let { startTime, position, generation, totalSize } = yield profilerStart;
+ let timelineStartTime = yield timelineStart;
+
+ let data = {
+ profilerStartTime: startTime, timelineStartTime,
+ generation, position, totalSize
+ };
+
+ // Signify to the model that the recording has started,
+ // populate with data and store the recording model here.
+ model._populate(data);
+ this._recordings.push(model);
+
+ events.emit(this, "recording-started", model);
+ return model;
+ }),
+
+ /**
+ * Manually ends the recording session for the corresponding LegacyPerformanceRecording.
+ *
+ * @param LegacyPerformanceRecording model
+ * The corresponding LegacyPerformanceRecording that belongs to the recording
+ * session wished to stop.
+ * @return LegacyPerformanceRecording
+ * Returns the same model, populated with the profiling data.
+ */
+ stopRecording: Task.async(function* (model) {
+ // If model isn't in the LegacyPerformanceFront internal store,
+ // then do nothing.
+ if (this._recordings.indexOf(model) === -1) {
+ return undefined;
+ }
+
+ // Flag the recording as no longer recording, so that `model.isRecording()`
+ // is false. Do this before we fetch all the data, and then subsequently
+ // the recording can be considered "completed".
+ let endTime = Date.now();
+ model._onStoppingRecording(endTime);
+ events.emit(this, "recording-stopping", model);
+
+ // Currently there are two ways profiles stop recording. Either manually in the
+ // performance tool, or via console.profileEnd. Once a recording is done,
+ // we want to deliver the model to the performance tool (either as a return
+ // from the LegacyPerformanceFront or via `console-profile-stop` event) and then
+ // remove it from the internal store.
+ //
+ // In the case where a console.profile is generated via the console (so the tools are
+ // open), we initialize the Performance tool so it can listen to those events.
+ this._recordings.splice(this._recordings.indexOf(model), 1);
+
+ let config = model.getConfiguration();
+ let startTime = model._getProfilerStartTime();
+ let profilerData = yield this._profiler.getProfile({ startTime });
+ let timelineEndTime = Date.now();
+
+ // Only if there are no more sessions recording do we stop
+ // the underlying timeline actors. If we're still recording,
+ // juse use Date.now() for the timeline end times, as those
+ // are only used in tests.
+ if (!this.isRecording()) {
+ // This doesn't stop the profiler, just turns off polling for
+ // events, and also turns off events on timeline actors.
+ yield this._profiler.stop();
+ timelineEndTime = yield this._timeline.stop(config);
+ }
+
+ let form = yield this._client.listTabs();
+ let systemHost = yield getDeviceFront(this._client, form).getDescription();
+ let systemClient = yield getSystemInfo();
+
+ // Set the results on the LegacyPerformanceRecording itself.
+ model._onStopRecording({
+ // Data available only at the end of a recording.
+ profile: profilerData.profile,
+
+ // End times for all the actors.
+ profilerEndTime: profilerData.currentTime,
+ timelineEndTime: timelineEndTime,
+ systemHost,
+ systemClient,
+ });
+
+ events.emit(this, "recording-stopped", model);
+ return model;
+ }),
+
+ /**
+ * Creates a recording object when given a nsILocalFile.
+ *
+ * @param {nsILocalFile} file
+ * The file to import the data from.
+ * @return {Promise<LegacyPerformanceRecording>}
+ */
+ importRecording: function (file) {
+ return importRecording(file);
+ },
+
+ /**
+ * Checks all currently stored recording models and returns a boolean
+ * if there is a session currently being recorded.
+ *
+ * @return Boolean
+ */
+ isRecording: function () {
+ return this._recordings.some(recording => recording.isRecording());
+ },
+
+ /**
+ * Pass in a PerformanceRecording and get a normalized value from 0 to 1 of how much
+ * of this recording's lifetime remains without being overwritten.
+ *
+ * @param {PerformanceRecording} recording
+ * @return {number?}
+ */
+ getBufferUsageForRecording: function (recording) {
+ if (!recording.isRecording() || !this._currentBufferStatus) {
+ return null;
+ }
+ let {
+ position: currentPosition,
+ totalSize,
+ generation: currentGeneration
+ } = this._currentBufferStatus;
+ let {
+ position: origPosition,
+ generation: origGeneration
+ } = recording.getStartingBufferStatus();
+
+ let normalizedCurrent = (totalSize * (currentGeneration - origGeneration))
+ + currentPosition;
+ let percent = (normalizedCurrent - origPosition) / totalSize;
+ return percent > 1 ? 1 : percent;
+ },
+
+ /**
+ * Returns the configurations set on underlying components, used in tests.
+ * Returns an object with `probability`, `maxLogLength` for allocations, and
+ * `entries` and `interval` for profiler.
+ *
+ * @return {object}
+ */
+ getConfiguration: Task.async(function* () {
+ let profilerConfig = yield this._request("profiler", "getStartOptions");
+ return profilerConfig;
+ }),
+
+ /**
+ * An event from an underlying actor that we just want
+ * to pipe to the front itself.
+ */
+ _pipeToFront: function (eventName, ...args) {
+ events.emit(this, eventName, ...args);
+ },
+
+ /**
+ * Helper method to interface with the underlying actors directly.
+ * Used only in tests.
+ */
+ _request: function (actorName, method, ...args) {
+ if (!flags.testing) {
+ throw new Error("LegacyPerformanceFront._request may only be used in tests.");
+ }
+ let actor = this[`_${actorName}`];
+ return actor[method].apply(actor, args);
+ },
+
+ /**
+ * Sets how often the "profiler-status" event should be emitted.
+ * Used in tests.
+ */
+ setProfilerStatusInterval: function (n) {
+ if (this._profiler._poller) {
+ this._profiler._poller._wait = n;
+ }
+ this._profiler._PROFILER_CHECK_TIMER = n;
+ },
+
+ toString: () => "[object LegacyPerformanceFront]"
+});
+
+/**
+ * Creates an object of configurations based off of preferences for a
+ * LegacyPerformanceRecording.
+ */
+function getLegacyPerformanceRecordingPrefs() {
+ return {
+ withMarkers: true,
+ withMemory: Services.prefs.getBoolPref(
+ "devtools.performance.ui.enable-memory"),
+ withTicks: Services.prefs.getBoolPref(
+ "devtools.performance.ui.enable-framerate"),
+ withAllocations: Services.prefs.getBoolPref(
+ "devtools.performance.ui.enable-allocations"),
+ allocationsSampleProbability: +Services.prefs.getCharPref(
+ "devtools.performance.memory.sample-probability"),
+ allocationsMaxLogLength: Services.prefs.getIntPref(
+ "devtools.performance.memory.max-log-length")
+ };
+}
+
+exports.LegacyPerformanceFront = LegacyPerformanceFront;
diff --git a/devtools/client/performance/legacy/moz.build b/devtools/client/performance/legacy/moz.build
new file mode 100644
index 000000000..00eab217b
--- /dev/null
+++ b/devtools/client/performance/legacy/moz.build
@@ -0,0 +1,12 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'actors.js',
+ 'compatibility.js',
+ 'front.js',
+ 'recording.js',
+)
diff --git a/devtools/client/performance/legacy/recording.js b/devtools/client/performance/legacy/recording.js
new file mode 100644
index 000000000..2ba141471
--- /dev/null
+++ b/devtools/client/performance/legacy/recording.js
@@ -0,0 +1,174 @@
+/* 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 { Task } = require("devtools/shared/task");
+
+const PerformanceIO = require("devtools/client/performance/modules/io");
+const RecordingUtils = require("devtools/shared/performance/recording-utils");
+const { PerformanceRecordingCommon } = require("devtools/shared/performance/recording-common");
+const { merge } = require("sdk/util/object");
+
+/**
+ * Model for a wholistic profile, containing the duration, profiling data,
+ * frames data, timeline (marker, tick, memory) data, and methods to mark
+ * a recording as 'in progress' or 'finished'.
+ */
+const LegacyPerformanceRecording = function (options = {}) {
+ this._label = options.label || "";
+ this._console = options.console || false;
+
+ this._configuration = {
+ withMarkers: options.withMarkers || false,
+ withTicks: options.withTicks || false,
+ withMemory: options.withMemory || false,
+ withAllocations: options.withAllocations || false,
+ allocationsSampleProbability: options.allocationsSampleProbability || 0,
+ allocationsMaxLogLength: options.allocationsMaxLogLength || 0,
+ bufferSize: options.bufferSize || 0,
+ sampleFrequency: options.sampleFrequency || 1
+ };
+};
+
+LegacyPerformanceRecording.prototype = merge({
+ _profilerStartTime: 0,
+ _timelineStartTime: 0,
+ _memoryStartTime: 0,
+
+ /**
+ * Saves the current recording to a file.
+ *
+ * @param nsILocalFile file
+ * The file to stream the data into.
+ */
+ exportRecording: Task.async(function* (file) {
+ let recordingData = this.getAllData();
+ yield PerformanceIO.saveRecordingToFile(recordingData, file);
+ }),
+
+ /**
+ * Sets up the instance with data from the PerformanceFront when
+ * starting a recording. Should only be called by PerformanceFront.
+ */
+ _populate: function (info) {
+ // Times must come from the actor in order to be self-consistent.
+ // However, we also want to update the view with the elapsed time
+ // even when the actor is not generating data. To do this we get
+ // the local time and use it to compute a reasonable elapsed time.
+ this._localStartTime = Date.now();
+
+ this._profilerStartTime = info.profilerStartTime;
+ this._timelineStartTime = info.timelineStartTime;
+ this._memoryStartTime = info.memoryStartTime;
+ this._startingBufferStatus = {
+ position: info.position,
+ totalSize: info.totalSize,
+ generation: info.generation
+ };
+
+ this._recording = true;
+
+ this._systemHost = {};
+ this._systemClient = {};
+ this._markers = [];
+ this._frames = [];
+ this._memory = [];
+ this._ticks = [];
+ this._allocations = { sites: [], timestamps: [], frames: [], sizes: [] };
+ },
+
+ /**
+ * Called when the signal was sent to the front to no longer record more
+ * data, and begin fetching the data. There's some delay during fetching,
+ * even though the recording is stopped, the model is not yet completed until
+ * all the data is fetched.
+ */
+ _onStoppingRecording: function (endTime) {
+ this._duration = endTime - this._localStartTime;
+ this._recording = false;
+ },
+
+ /**
+ * Sets results available from stopping a recording from PerformanceFront.
+ * Should only be called by PerformanceFront.
+ */
+ _onStopRecording: Task.async(function* ({ profilerEndTime, profile, systemClient,
+ systemHost }) {
+ // Update the duration with the accurate profilerEndTime, so we don't have
+ // samples outside of the approximate duration set in `_onStoppingRecording`.
+ this._duration = profilerEndTime - this._profilerStartTime;
+ this._profile = profile;
+ this._completed = true;
+
+ // We filter out all samples that fall out of current profile's range
+ // since the profiler is continuously running. Because of this, sample
+ // times are not guaranteed to have a zero epoch, so offset the
+ // timestamps.
+ RecordingUtils.offsetSampleTimes(this._profile, this._profilerStartTime);
+
+ // Markers need to be sorted ascending by time, to be properly displayed
+ // in a waterfall view.
+ this._markers = this._markers.sort((a, b) => (a.start > b.start));
+
+ this._systemHost = systemHost;
+ this._systemClient = systemClient;
+ }),
+
+ /**
+ * Gets the profile's start time.
+ * @return number
+ */
+ _getProfilerStartTime: function () {
+ return this._profilerStartTime;
+ },
+
+ /**
+ * Fired whenever the PerformanceFront emits markers, memory or ticks.
+ */
+ _addTimelineData: function (eventName, ...data) {
+ // If this model isn't currently recording,
+ // ignore the timeline data.
+ if (!this.isRecording()) {
+ return;
+ }
+
+ let config = this.getConfiguration();
+
+ switch (eventName) {
+ // Accumulate timeline markers into an array. Furthermore, the timestamps
+ // do not have a zero epoch, so offset all of them by the start time.
+ case "markers": {
+ if (!config.withMarkers) {
+ break;
+ }
+ let [markers] = data;
+ RecordingUtils.offsetMarkerTimes(markers, this._timelineStartTime);
+ RecordingUtils.pushAll(this._markers, markers);
+ break;
+ }
+ // Accumulate stack frames into an array.
+ case "frames": {
+ if (!config.withMarkers) {
+ break;
+ }
+ let [, frames] = data;
+ RecordingUtils.pushAll(this._frames, frames);
+ break;
+ }
+ // Save the accumulated refresh driver ticks.
+ case "ticks": {
+ if (!config.withTicks) {
+ break;
+ }
+ let [, timestamps] = data;
+ this._ticks = timestamps;
+ break;
+ }
+ }
+ },
+
+ toString: () => "[object LegacyPerformanceRecording]"
+}, PerformanceRecordingCommon);
+
+exports.LegacyPerformanceRecording = LegacyPerformanceRecording;
diff --git a/devtools/client/performance/modules/categories.js b/devtools/client/performance/modules/categories.js
new file mode 100644
index 000000000..f3f05d567
--- /dev/null
+++ b/devtools/client/performance/modules/categories.js
@@ -0,0 +1,128 @@
+/* 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 { L10N } = require("devtools/client/performance/modules/global");
+
+/**
+ * Details about each profile pseudo-stack entry cateogry.
+ * @see CATEGORY_MAPPINGS.
+ */
+const CATEGORIES = [{
+ color: "#5e88b0",
+ abbrev: "other",
+ label: L10N.getStr("category.other")
+}, {
+ color: "#46afe3",
+ abbrev: "css",
+ label: L10N.getStr("category.css")
+}, {
+ color: "#d96629",
+ abbrev: "js",
+ label: L10N.getStr("category.js")
+}, {
+ color: "#eb5368",
+ abbrev: "gc",
+ label: L10N.getStr("category.gc")
+}, {
+ color: "#df80ff",
+ abbrev: "network",
+ label: L10N.getStr("category.network")
+}, {
+ color: "#70bf53",
+ abbrev: "graphics",
+ label: L10N.getStr("category.graphics")
+}, {
+ color: "#8fa1b2",
+ abbrev: "storage",
+ label: L10N.getStr("category.storage")
+}, {
+ color: "#d99b28",
+ abbrev: "events",
+ label: L10N.getStr("category.events")
+}, {
+ color: "#8fa1b2",
+ abbrev: "tools",
+ label: L10N.getStr("category.tools")
+}];
+
+/**
+ * Mapping from category bitmasks in the profiler data to additional details.
+ * To be kept in sync with the js::ProfileEntry::Category in ProfilingStack.h
+ */
+const CATEGORY_MAPPINGS = {
+ // js::ProfileEntry::Category::OTHER
+ "16": CATEGORIES[0],
+ // js::ProfileEntry::Category::CSS
+ "32": CATEGORIES[1],
+ // js::ProfileEntry::Category::JS
+ "64": CATEGORIES[2],
+ // js::ProfileEntry::Category::GC
+ "128": CATEGORIES[3],
+ // js::ProfileEntry::Category::CC
+ "256": CATEGORIES[3],
+ // js::ProfileEntry::Category::NETWORK
+ "512": CATEGORIES[4],
+ // js::ProfileEntry::Category::GRAPHICS
+ "1024": CATEGORIES[5],
+ // js::ProfileEntry::Category::STORAGE
+ "2048": CATEGORIES[6],
+ // js::ProfileEntry::Category::EVENTS
+ "4096": CATEGORIES[7],
+ // non-bitmasks for specially-assigned categories
+ "9000": CATEGORIES[8],
+};
+
+/**
+ * Get the numeric bitmask (or set of masks) for the given category
+ * abbreviation. See `CATEGORIES` and `CATEGORY_MAPPINGS` above.
+ *
+ * CATEGORY_MASK can be called with just a name if it is expected that the
+ * category is mapped to by exactly one bitmask. If the category is mapped
+ * to by multiple masks, CATEGORY_MASK for that name must be called with
+ * an additional argument specifying the desired id (in ascending order).
+ */
+const [CATEGORY_MASK, CATEGORY_MASK_LIST] = (() => {
+ let bitmasksForCategory = {};
+ let all = Object.keys(CATEGORY_MAPPINGS);
+
+ for (let category of CATEGORIES) {
+ bitmasksForCategory[category.abbrev] = all
+ .filter(mask => CATEGORY_MAPPINGS[mask] == category)
+ .map(mask => +mask)
+ .sort();
+ }
+
+ return [
+ function (name, index) {
+ if (!(name in bitmasksForCategory)) {
+ throw new Error(`Category abbreviation "${name}" does not exist.`);
+ }
+ if (arguments.length == 1) {
+ if (bitmasksForCategory[name].length != 1) {
+ throw new Error(`Expected exactly one category number for "${name}".`);
+ } else {
+ return bitmasksForCategory[name][0];
+ }
+ } else {
+ if (index > bitmasksForCategory[name].length) {
+ throw new Error(`Index "${index}" too high for category "${name}".`);
+ }
+ return bitmasksForCategory[name][index - 1];
+ }
+ },
+
+ function (name) {
+ if (!(name in bitmasksForCategory)) {
+ throw new Error(`Category abbreviation "${name}" does not exist.`);
+ }
+ return bitmasksForCategory[name];
+ }
+ ];
+})();
+
+exports.CATEGORIES = CATEGORIES;
+exports.CATEGORY_MAPPINGS = CATEGORY_MAPPINGS;
+exports.CATEGORY_MASK = CATEGORY_MASK;
+exports.CATEGORY_MASK_LIST = CATEGORY_MASK_LIST;
diff --git a/devtools/client/performance/modules/constants.js b/devtools/client/performance/modules/constants.js
new file mode 100644
index 000000000..a0adaf596
--- /dev/null
+++ b/devtools/client/performance/modules/constants.js
@@ -0,0 +1,11 @@
+/* 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";
+
+exports.Constants = {
+ // ms
+ FRAMERATE_GRAPH_LOW_RES_INTERVAL: 100,
+ // ms
+ FRAMERATE_GRAPH_HIGH_RES_INTERVAL: 16,
+};
diff --git a/devtools/client/performance/modules/global.js b/devtools/client/performance/modules/global.js
new file mode 100644
index 000000000..0c6c86f10
--- /dev/null
+++ b/devtools/client/performance/modules/global.js
@@ -0,0 +1,36 @@
+/* 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 { MultiLocalizationHelper } = require("devtools/shared/l10n");
+const { PrefsHelper } = require("devtools/client/shared/prefs");
+
+/**
+ * Localization convenience methods.
+ */
+exports.L10N = new MultiLocalizationHelper(
+ "devtools/client/locales/markers.properties",
+ "devtools/client/locales/performance.properties"
+);
+
+/**
+ * A list of preferences for this tool. The values automatically update
+ * if somebody edits edits about:config or the prefs change somewhere else.
+ *
+ * This needs to be registered and unregistered when used for the auto-update
+ * functionality to work. The PerformanceController handles this, but if you
+ * just use this module in a test independently, ensure you call
+ * `registerObserver()` and `unregisterUnobserver()`.
+ */
+exports.PREFS = new PrefsHelper("devtools.performance", {
+ "show-triggers-for-gc-types": ["Char", "ui.show-triggers-for-gc-types"],
+ "show-platform-data": ["Bool", "ui.show-platform-data"],
+ "hidden-markers": ["Json", "timeline.hidden-markers"],
+ "memory-sample-probability": ["Float", "memory.sample-probability"],
+ "memory-max-log-length": ["Int", "memory.max-log-length"],
+ "profiler-buffer-size": ["Int", "profiler.buffer-size"],
+ "profiler-sample-frequency": ["Int", "profiler.sample-frequency-khz"],
+ // TODO: re-enable once we flame charts via bug 1148663.
+ "enable-memory-flame": ["Bool", "ui.enable-memory-flame"],
+});
diff --git a/devtools/client/performance/modules/io.js b/devtools/client/performance/modules/io.js
new file mode 100644
index 000000000..08bfd034c
--- /dev/null
+++ b/devtools/client/performance/modules/io.js
@@ -0,0 +1,171 @@
+/* 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 RecordingUtils = require("devtools/shared/performance/recording-utils");
+const { FileUtils } = require("resource://gre/modules/FileUtils.jsm");
+const { NetUtil } = require("resource://gre/modules/NetUtil.jsm");
+
+// This identifier string is used to tentatively ascertain whether or not
+// a JSON loaded from disk is actually something generated by this tool.
+// It isn't, of course, a definitive verification, but a Good Enoughâ„¢
+// approximation before continuing the import. Don't localize this.
+const PERF_TOOL_SERIALIZER_IDENTIFIER = "Recorded Performance Data";
+const PERF_TOOL_SERIALIZER_LEGACY_VERSION = 1;
+const PERF_TOOL_SERIALIZER_CURRENT_VERSION = 2;
+
+/**
+ * Helpers for importing/exporting JSON.
+ */
+
+/**
+ * Gets a nsIScriptableUnicodeConverter instance with a default UTF-8 charset.
+ * @return object
+ */
+function getUnicodeConverter() {
+ let cname = "@mozilla.org/intl/scriptableunicodeconverter";
+ let converter = Cc[cname].createInstance(Ci.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+ return converter;
+}
+
+/**
+ * Saves a recording as JSON to a file. The provided data is assumed to be
+ * acyclical, so that it can be properly serialized.
+ *
+ * @param object recordingData
+ * The recording data to stream as JSON.
+ * @param nsILocalFile file
+ * The file to stream the data into.
+ * @return object
+ * A promise that is resolved once streaming finishes, or rejected
+ * if there was an error.
+ */
+function saveRecordingToFile(recordingData, file) {
+ recordingData.fileType = PERF_TOOL_SERIALIZER_IDENTIFIER;
+ recordingData.version = PERF_TOOL_SERIALIZER_CURRENT_VERSION;
+
+ let string = JSON.stringify(recordingData);
+ let inputStream = getUnicodeConverter().convertToInputStream(string);
+ let outputStream = FileUtils.openSafeFileOutputStream(file);
+
+ return new Promise(resolve => {
+ NetUtil.asyncCopy(inputStream, outputStream, resolve);
+ });
+}
+
+/**
+ * Loads a recording stored as JSON from a file.
+ *
+ * @param nsILocalFile file
+ * The file to import the data from.
+ * @return object
+ * A promise that is resolved once importing finishes, or rejected
+ * if there was an error.
+ */
+function loadRecordingFromFile(file) {
+ let channel = NetUtil.newChannel({
+ uri: NetUtil.newURI(file),
+ loadUsingSystemPrincipal: true
+ });
+
+ channel.contentType = "text/plain";
+
+ return new Promise((resolve, reject) => {
+ NetUtil.asyncFetch(channel, (inputStream) => {
+ let recordingData;
+
+ try {
+ let string = NetUtil.readInputStreamToString(inputStream,
+ inputStream.available());
+ recordingData = JSON.parse(string);
+ } catch (e) {
+ reject(new Error("Could not read recording data file."));
+ return;
+ }
+
+ if (recordingData.fileType != PERF_TOOL_SERIALIZER_IDENTIFIER) {
+ reject(new Error("Unrecognized recording data file."));
+ return;
+ }
+
+ if (!isValidSerializerVersion(recordingData.version)) {
+ reject(new Error("Unsupported recording data file version."));
+ return;
+ }
+
+ if (recordingData.version === PERF_TOOL_SERIALIZER_LEGACY_VERSION) {
+ recordingData = convertLegacyData(recordingData);
+ }
+
+ if (recordingData.profile.meta.version === 2) {
+ RecordingUtils.deflateProfile(recordingData.profile);
+ }
+
+ // If the recording has no label, set it to be the
+ // filename without its extension.
+ if (!recordingData.label) {
+ recordingData.label = file.leafName.replace(/\.[^.]+$/, "");
+ }
+
+ resolve(recordingData);
+ });
+ });
+}
+
+/**
+ * Returns a boolean indicating whether or not the passed in `version`
+ * is supported by this serializer.
+ *
+ * @param number version
+ * @return boolean
+ */
+function isValidSerializerVersion(version) {
+ return !!~[
+ PERF_TOOL_SERIALIZER_LEGACY_VERSION,
+ PERF_TOOL_SERIALIZER_CURRENT_VERSION
+ ].indexOf(version);
+}
+
+/**
+ * Takes recording data (with version `1`, from the original profiler tool),
+ * and massages the data to be line with the current performance tool's
+ * property names and values.
+ *
+ * @param object legacyData
+ * @return object
+ */
+function convertLegacyData(legacyData) {
+ let { profilerData, ticksData, recordingDuration } = legacyData;
+
+ // The `profilerData` and `ticksData` stay, but the previously unrecorded
+ // fields just are empty arrays or objects.
+ let data = {
+ label: profilerData.profilerLabel,
+ duration: recordingDuration,
+ markers: [],
+ frames: [],
+ memory: [],
+ ticks: ticksData,
+ allocations: { sites: [], timestamps: [], frames: [], sizes: [] },
+ profile: profilerData.profile,
+ // Fake a configuration object here if there's tick data,
+ // so that it can be rendered.
+ configuration: {
+ withTicks: !!ticksData.length,
+ withMarkers: false,
+ withMemory: false,
+ withAllocations: false
+ },
+ systemHost: {},
+ systemClient: {},
+ };
+
+ return data;
+}
+
+exports.saveRecordingToFile = saveRecordingToFile;
+exports.loadRecordingFromFile = loadRecordingFromFile;
diff --git a/devtools/client/performance/modules/logic/frame-utils.js b/devtools/client/performance/modules/logic/frame-utils.js
new file mode 100644
index 000000000..f82996be2
--- /dev/null
+++ b/devtools/client/performance/modules/logic/frame-utils.js
@@ -0,0 +1,478 @@
+/* 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 global = require("devtools/client/performance/modules/global");
+const demangle = require("devtools/client/shared/demangle");
+const { assert } = require("devtools/shared/DevToolsUtils");
+const { isChromeScheme, isContentScheme, parseURL } =
+ require("devtools/client/shared/source-utils");
+
+const { CATEGORY_MASK, CATEGORY_MAPPINGS } = require("devtools/client/performance/modules/categories");
+
+// Character codes used in various parsing helper functions.
+const CHAR_CODE_R = "r".charCodeAt(0);
+const CHAR_CODE_0 = "0".charCodeAt(0);
+const CHAR_CODE_9 = "9".charCodeAt(0);
+const CHAR_CODE_CAP_Z = "Z".charCodeAt(0);
+
+const CHAR_CODE_LPAREN = "(".charCodeAt(0);
+const CHAR_CODE_RPAREN = ")".charCodeAt(0);
+const CHAR_CODE_COLON = ":".charCodeAt(0);
+const CHAR_CODE_SPACE = " ".charCodeAt(0);
+const CHAR_CODE_UNDERSCORE = "_".charCodeAt(0);
+
+const EVAL_TOKEN = "%20%3E%20eval";
+
+// The cache used to store inflated frames.
+const gInflatedFrameStore = new WeakMap();
+
+// The cache used to store frame data from `getInfo`.
+const gFrameData = new WeakMap();
+
+/**
+ * Parses the raw location of this function call to retrieve the actual
+ * function name, source url, host name, line and column.
+ */
+function parseLocation(location, fallbackLine, fallbackColumn) {
+ // Parse the `location` for the function name, source url, line, column etc.
+
+ let line, column, url;
+
+ // These two indices are used to extract the resource substring, which is
+ // location[parenIndex + 1 .. lineAndColumnIndex].
+ //
+ // There are 3 variants of location strings in the profiler (with optional
+ // column numbers):
+ // 1) "name (resource:line)"
+ // 2) "resource:line"
+ // 3) "resource"
+ //
+ // For example for (1), take "foo (bar.js:1)".
+ // ^ ^
+ // | |
+ // | |
+ // | |
+ // parenIndex will point to ------+ |
+ // |
+ // lineAndColumnIndex will point to -----+
+ //
+ // For an example without parentheses, take "bar.js:2".
+ // ^ ^
+ // | |
+ // parenIndex will point to ----------------+ |
+ // |
+ // lineAndColumIndex will point to ----------------+
+ //
+ // To parse, we look for the last occurrence of the string ' ('.
+ //
+ // For 1), all occurrences of space ' ' characters in the resource string
+ // are urlencoded, so the last occurrence of ' (' is the separator between
+ // the function name and the resource.
+ //
+ // For 2) and 3), there can be no occurences of ' (' since ' ' characters
+ // are urlencoded in the resource string.
+ //
+ // XXX: Note that 3) is ambiguous with SPS marker locations like
+ // "EnterJIT". We can't distinguish the two, so we treat 3) like a function
+ // name.
+ let parenIndex = -1;
+ let lineAndColumnIndex = -1;
+
+ let lastCharCode = location.charCodeAt(location.length - 1);
+ let i;
+ if (lastCharCode === CHAR_CODE_RPAREN) {
+ // Case 1)
+ i = location.length - 2;
+ } else if (isNumeric(lastCharCode)) {
+ // Case 2)
+ i = location.length - 1;
+ } else {
+ // Case 3)
+ i = 0;
+ }
+
+ if (i !== 0) {
+ // Look for a :number.
+ let end = i;
+ while (isNumeric(location.charCodeAt(i))) {
+ i--;
+ }
+ if (location.charCodeAt(i) === CHAR_CODE_COLON) {
+ column = location.substr(i + 1, end - i);
+ i--;
+ }
+
+ // Look for a preceding :number.
+ end = i;
+ while (isNumeric(location.charCodeAt(i))) {
+ i--;
+ }
+
+ // If two were found, the first is the line and the second is the
+ // column. If only a single :number was found, then it is the line number.
+ if (location.charCodeAt(i) === CHAR_CODE_COLON) {
+ line = location.substr(i + 1, end - i);
+ lineAndColumnIndex = i;
+ i--;
+ } else {
+ lineAndColumnIndex = i + 1;
+ line = column;
+ column = undefined;
+ }
+ }
+
+ // Look for the last occurrence of ' (' in case 1).
+ if (lastCharCode === CHAR_CODE_RPAREN) {
+ for (; i >= 0; i--) {
+ if (location.charCodeAt(i) === CHAR_CODE_LPAREN &&
+ i > 0 &&
+ location.charCodeAt(i - 1) === CHAR_CODE_SPACE) {
+ parenIndex = i;
+ break;
+ }
+ }
+ }
+
+ let parsedUrl;
+ if (lineAndColumnIndex > 0) {
+ let resource = location.substring(parenIndex + 1, lineAndColumnIndex);
+ url = resource.split(" -> ").pop();
+ if (url) {
+ parsedUrl = parseURL(url);
+ }
+ }
+
+ let functionName, fileName, port, host;
+ line = line || fallbackLine;
+ column = column || fallbackColumn;
+
+ // If the URL digged out from the `location` is valid, this is a JS frame.
+ if (parsedUrl) {
+ functionName = location.substring(0, parenIndex - 1);
+ fileName = parsedUrl.fileName;
+ port = parsedUrl.port;
+ host = parsedUrl.host;
+
+ // Check for the case of the filename containing eval
+ // e.g. "file.js%20line%2065%20%3E%20eval"
+ let evalIndex = fileName.indexOf(EVAL_TOKEN);
+ if (evalIndex !== -1 && evalIndex === (fileName.length - EVAL_TOKEN.length)) {
+ // Match the filename
+ let evalLine = line;
+ let [, _fileName, , _line] = fileName.match(/(.+)(%20line%20(\d+)%20%3E%20eval)/)
+ || [];
+ fileName = `${_fileName} (eval:${evalLine})`;
+ line = _line;
+ assert(_fileName !== undefined,
+ "Filename could not be found from an eval location site");
+ assert(_line !== undefined,
+ "Line could not be found from an eval location site");
+
+ // Match the url as well
+ [, url] = url.match(/(.+)( line (\d+) > eval)/) || [];
+ assert(url !== undefined,
+ "The URL could not be parsed correctly from an eval location site");
+ }
+ } else {
+ functionName = location;
+ url = null;
+ }
+
+ return { functionName, fileName, host, port, url, line, column };
+}
+
+/**
+ * Sets the properties of `isContent` and `category` on a frame.
+ *
+ * @param {InflatedFrame} frame
+ */
+function computeIsContentAndCategory(frame) {
+ // Only C++ stack frames have associated category information.
+ if (frame.category) {
+ return;
+ }
+
+ let location = frame.location;
+
+ // There are 3 variants of location strings in the profiler (with optional
+ // column numbers):
+ // 1) "name (resource:line)"
+ // 2) "resource:line"
+ // 3) "resource"
+ let lastCharCode = location.charCodeAt(location.length - 1);
+ let schemeStartIndex = -1;
+ if (lastCharCode === CHAR_CODE_RPAREN) {
+ // Case 1)
+ //
+ // Need to search for the last occurrence of ' (' to find the start of the
+ // resource string.
+ for (let i = location.length - 2; i >= 0; i--) {
+ if (location.charCodeAt(i) === CHAR_CODE_LPAREN &&
+ i > 0 &&
+ location.charCodeAt(i - 1) === CHAR_CODE_SPACE) {
+ schemeStartIndex = i + 1;
+ break;
+ }
+ }
+ } else {
+ // Cases 2) and 3)
+ schemeStartIndex = 0;
+ }
+
+ if (isContentScheme(location, schemeStartIndex)) {
+ frame.isContent = true;
+ return;
+ }
+
+ if (schemeStartIndex !== 0) {
+ for (let j = schemeStartIndex; j < location.length; j++) {
+ if (location.charCodeAt(j) === CHAR_CODE_R &&
+ isChromeScheme(location, j) &&
+ (location.indexOf("resource://devtools") !== -1 ||
+ location.indexOf("resource://devtools") !== -1)) {
+ frame.category = CATEGORY_MASK("tools");
+ return;
+ }
+ }
+ }
+
+ if (location === "EnterJIT") {
+ frame.category = CATEGORY_MASK("js");
+ return;
+ }
+
+ frame.category = CATEGORY_MASK("other");
+}
+
+/**
+ * Get caches to cache inflated frames and computed frame keys of a frame
+ * table.
+ *
+ * @param object framesTable
+ * @return object
+ */
+function getInflatedFrameCache(frameTable) {
+ let inflatedCache = gInflatedFrameStore.get(frameTable);
+ if (inflatedCache !== undefined) {
+ return inflatedCache;
+ }
+
+ // Fill with nulls to ensure no holes.
+ inflatedCache = Array.from({ length: frameTable.data.length }, () => null);
+ gInflatedFrameStore.set(frameTable, inflatedCache);
+ return inflatedCache;
+}
+
+/**
+ * Get or add an inflated frame to a cache.
+ *
+ * @param object cache
+ * @param number index
+ * @param object frameTable
+ * @param object stringTable
+ */
+function getOrAddInflatedFrame(cache, index, frameTable, stringTable) {
+ let inflatedFrame = cache[index];
+ if (inflatedFrame === null) {
+ inflatedFrame = cache[index] = new InflatedFrame(index, frameTable, stringTable);
+ }
+ return inflatedFrame;
+}
+
+/**
+ * An intermediate data structured used to hold inflated frames.
+ *
+ * @param number index
+ * @param object frameTable
+ * @param object stringTable
+ */
+function InflatedFrame(index, frameTable, stringTable) {
+ const LOCATION_SLOT = frameTable.schema.location;
+ const IMPLEMENTATION_SLOT = frameTable.schema.implementation;
+ const OPTIMIZATIONS_SLOT = frameTable.schema.optimizations;
+ const LINE_SLOT = frameTable.schema.line;
+ const CATEGORY_SLOT = frameTable.schema.category;
+
+ let frame = frameTable.data[index];
+ let category = frame[CATEGORY_SLOT];
+ this.location = stringTable[frame[LOCATION_SLOT]];
+ this.implementation = frame[IMPLEMENTATION_SLOT];
+ this.optimizations = frame[OPTIMIZATIONS_SLOT];
+ this.line = frame[LINE_SLOT];
+ this.column = undefined;
+ this.category = category;
+ this.isContent = false;
+
+ // Attempt to compute if this frame is a content frame, and if not,
+ // its category.
+ //
+ // Since only C++ stack frames have associated category information,
+ // attempt to generate a useful category, fallback to the one provided
+ // by the profiling data, or fallback to an unknown category.
+ computeIsContentAndCategory(this);
+}
+
+/**
+ * Gets the frame key (i.e., equivalence group) according to options. Content
+ * frames are always identified by location. Chrome frames are identified by
+ * location if content-only filtering is off. If content-filtering is on, they
+ * are identified by their category.
+ *
+ * @param object options
+ * @return string
+ */
+InflatedFrame.prototype.getFrameKey = function getFrameKey(options) {
+ if (this.isContent || !options.contentOnly || options.isRoot) {
+ options.isMetaCategoryOut = false;
+ return this.location;
+ }
+
+ if (options.isLeaf) {
+ // We only care about leaf platform frames if we are displaying content
+ // only. If no category is present, give the default category of "other".
+ //
+ // 1. The leaf is where time is _actually_ being spent, so we _need_ to
+ // show it to developers in some way to give them accurate profiling
+ // data. We decide to split the platform into various category buckets
+ // and just show time spent in each bucket.
+ //
+ // 2. The calls leading to the leaf _aren't_ where we are spending time,
+ // but _do_ give the developer context for how they got to the leaf
+ // where they _are_ spending time. For non-platform hackers, the
+ // non-leaf platform frames don't give any meaningful context, and so we
+ // can safely filter them out.
+ options.isMetaCategoryOut = true;
+ return this.category;
+ }
+
+ // Return an empty string denoting that this frame should be skipped.
+ return "";
+};
+
+function isNumeric(c) {
+ return c >= CHAR_CODE_0 && c <= CHAR_CODE_9;
+}
+
+function shouldDemangle(name) {
+ return name && name.charCodeAt &&
+ name.charCodeAt(0) === CHAR_CODE_UNDERSCORE &&
+ name.charCodeAt(1) === CHAR_CODE_UNDERSCORE &&
+ name.charCodeAt(2) === CHAR_CODE_CAP_Z;
+}
+
+/**
+ * Calculates the relative costs of this frame compared to a root,
+ * and generates allocations information if specified. Uses caching
+ * if possible.
+ *
+ * @param {ThreadNode|FrameNode} node
+ * The node we are calculating.
+ * @param {ThreadNode} options.root
+ * The root thread node to calculate relative costs.
+ * Generates [self|total] [duration|percentage] values.
+ * @param {boolean} options.allocations
+ * Generates `totalAllocations` and `selfAllocations`.
+ *
+ * @return {object}
+ */
+function getFrameInfo(node, options) {
+ let data = gFrameData.get(node);
+
+ if (!data) {
+ if (node.nodeType === "Thread") {
+ data = Object.create(null);
+ data.functionName = global.L10N.getStr("table.root");
+ } else {
+ data = parseLocation(node.location, node.line, node.column);
+ data.hasOptimizations = node.hasOptimizations();
+ data.isContent = node.isContent;
+ data.isMetaCategory = node.isMetaCategory;
+ }
+ data.samples = node.youngestFrameSamples;
+ data.categoryData = CATEGORY_MAPPINGS[node.category] || {};
+ data.nodeType = node.nodeType;
+
+ // Frame name (function location or some meta information)
+ if (data.isMetaCategory) {
+ data.name = data.categoryData.label;
+ } else if (shouldDemangle(data.functionName)) {
+ data.name = demangle(data.functionName);
+ } else {
+ data.name = data.functionName;
+ }
+
+ data.tooltiptext = data.isMetaCategory ?
+ data.categoryData.label :
+ node.location || "";
+
+ gFrameData.set(node, data);
+ }
+
+ // If no options specified, we can't calculate relative values, abort here
+ if (!options) {
+ return data;
+ }
+
+ // If a root specified, calculate the relative costs in the context of
+ // this call tree. The cached store may already have this, but generate
+ // if it does not.
+ let totalSamples = options.root.samples;
+ let totalDuration = options.root.duration;
+ if (options && options.root && !data.COSTS_CALCULATED) {
+ data.selfDuration = node.youngestFrameSamples / totalSamples * totalDuration;
+ data.selfPercentage = node.youngestFrameSamples / totalSamples * 100;
+ data.totalDuration = node.samples / totalSamples * totalDuration;
+ data.totalPercentage = node.samples / totalSamples * 100;
+ data.COSTS_CALCULATED = true;
+ }
+
+ if (options && options.allocations && !data.ALLOCATION_DATA_CALCULATED) {
+ let totalBytes = options.root.byteSize;
+ data.selfCount = node.youngestFrameSamples;
+ data.totalCount = node.samples;
+ data.selfCountPercentage = node.youngestFrameSamples / totalSamples * 100;
+ data.totalCountPercentage = node.samples / totalSamples * 100;
+ data.selfSize = node.youngestFrameByteSize;
+ data.totalSize = node.byteSize;
+ data.selfSizePercentage = node.youngestFrameByteSize / totalBytes * 100;
+ data.totalSizePercentage = node.byteSize / totalBytes * 100;
+ data.ALLOCATION_DATA_CALCULATED = true;
+ }
+
+ return data;
+}
+
+exports.getFrameInfo = getFrameInfo;
+
+/**
+ * Takes an inverted ThreadNode and searches its youngest frames for
+ * a FrameNode with matching location.
+ *
+ * @param {ThreadNode} threadNode
+ * @param {string} location
+ * @return {?FrameNode}
+ */
+function findFrameByLocation(threadNode, location) {
+ if (!threadNode.inverted) {
+ throw new Error(
+ "FrameUtils.findFrameByLocation only supports leaf nodes in an inverted tree.");
+ }
+
+ let calls = threadNode.calls;
+ for (let i = 0; i < calls.length; i++) {
+ if (calls[i].location === location) {
+ return calls[i];
+ }
+ }
+ return null;
+}
+
+exports.findFrameByLocation = findFrameByLocation;
+exports.computeIsContentAndCategory = computeIsContentAndCategory;
+exports.parseLocation = parseLocation;
+exports.getInflatedFrameCache = getInflatedFrameCache;
+exports.getOrAddInflatedFrame = getOrAddInflatedFrame;
+exports.InflatedFrame = InflatedFrame;
+exports.shouldDemangle = shouldDemangle;
diff --git a/devtools/client/performance/modules/logic/jit.js b/devtools/client/performance/modules/logic/jit.js
new file mode 100644
index 000000000..a958c3c4a
--- /dev/null
+++ b/devtools/client/performance/modules/logic/jit.js
@@ -0,0 +1,342 @@
+/* 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";
+
+// An outcome of an OptimizationAttempt that is considered successful.
+const SUCCESSFUL_OUTCOMES = [
+ "GenericSuccess", "Inlined", "DOM", "Monomorphic", "Polymorphic"
+];
+
+/**
+ * Model representing JIT optimization sites from the profiler
+ * for a frame (represented by a FrameNode). Requires optimization data from
+ * a profile, which is an array of RawOptimizationSites.
+ *
+ * When the ThreadNode for the profile iterates over the samples' frames, each
+ * frame's optimizations are accumulated in their respective FrameNodes. Each
+ * FrameNode may contain many different optimization sites. One sample may
+ * pick up optimization X on line Y in the frame, with the next sample
+ * containing optimization Z on line W in the same frame, as each frame is
+ * only function.
+ *
+ * An OptimizationSite contains a record of how many times the
+ * RawOptimizationSite was sampled, as well as the unique id based off of the
+ * original profiler array, and the RawOptimizationSite itself as a reference.
+ * @see devtools/client/performance/modules/logic/tree-model.js
+ *
+ * @struct RawOptimizationSite
+ * A structure describing a location in a script that was attempted to be optimized.
+ * Contains all the IonTypes observed, and the sequence of OptimizationAttempts that
+ * were attempted, and the line and column in the script. This is retrieved from the
+ * profiler after a recording, and our base data structure. Should always be referenced,
+ * and unmodified.
+ *
+ * Note that propertyName is an index into a string table, which needs to be
+ * provided in order for the raw optimization site to be inflated.
+ *
+ * @type {Array<IonType>} types
+ * @type {Array<OptimizationAttempt>} attempts
+ * @type {?number} propertyName
+ * @type {number} line
+ * @type {number} column
+ *
+ *
+ * @struct IonType
+ * IonMonkey attempts to classify each value in an optimization site by some type.
+ * Based off of the observed types for a value (like a variable that could be a
+ * string or an instance of an object), it determines what kind of type it should be
+ * classified as. Each IonType here contains an array of all ObservedTypes under `types`,
+ * the Ion type that IonMonkey decided this value should be (Int32, Object, etc.) as
+ * `mirType`, and the component of this optimization type that this value refers to --
+ * like a "getter" optimization, `a[b]`, has site `a` (the "Receiver") and `b`
+ * (the "Index").
+ *
+ * Generally the more ObservedTypes, the more deoptimized this OptimizationSite is.
+ * There could be no ObservedTypes, in which case `typeset` is undefined.
+ *
+ * @type {?Array<ObservedType>} typeset
+ * @type {string} site
+ * @type {string} mirType
+ *
+ *
+ * @struct ObservedType
+ * When IonMonkey attempts to determine what type a value is, it checks on each sample.
+ * The ObservedType can be thought of in more of JavaScripty-terms, rather than C++.
+ * The `keyedBy` property is a high level description of the type, like "primitive",
+ * "constructor", "function", "singleton", "alloc-site" (that one is a bit more weird).
+ * If the `keyedBy` type is a function or constructor, the ObservedType should have a
+ * `name` property, referring to the function or constructor name from the JS source.
+ * If IonMonkey can determine the origin of this type (like where the constructor is
+ * defined), the ObservedType will also have `location` and `line` properties, but
+ * `location` can sometimes be non-URL strings like "self-hosted" or a memory location
+ * like "102ca7880", or no location at all, and maybe `line` is 0 or undefined.
+ *
+ * @type {string} keyedBy
+ * @type {?string} name
+ * @type {?string} location
+ * @type {?string} line
+ *
+ *
+ * @struct OptimizationAttempt
+ * Each RawOptimizationSite contains an array of OptimizationAttempts. Generally,
+ * IonMonkey goes through a series of strategies for each kind of optimization, starting
+ * from most-niche and optimized, to the less-optimized, but more general strategies --
+ * for example, a getter opt may first try to optimize for the scenario of a getter on an
+ * `arguments` object -- that will fail most of the time, as most objects are not
+ * arguments objects, but it will attempt several strategies in order until it finds a
+ * strategy that works, or fails. Even in the best scenarios, some attempts will fail
+ * (like the arguments getter example), which is OK, as long as some attempt succeeds
+ * (with the earlier attempts preferred, as those are more optimized). In an
+ * OptimizationAttempt structure, we store just the `strategy` name and `outcome` name,
+ * both from enums in js/public/TrackedOptimizationInfo.h as TRACKED_STRATEGY_LIST and
+ * TRACKED_OUTCOME_LIST, respectively. An array of successful outcome strings are above
+ * in SUCCESSFUL_OUTCOMES.
+ *
+ * @see js/public/TrackedOptimizationInfo.h
+ *
+ * @type {string} strategy
+ * @type {string} outcome
+ */
+
+/*
+ * A wrapper around RawOptimizationSite to record sample count and ID (referring to the
+ * index of where this is in the initially seeded optimizations data), so we don't mutate
+ * the original data from the profiler. Provides methods to access the underlying
+ * optimization data easily, so understanding the semantics of JIT data isn't necessary.
+ *
+ * @constructor
+ *
+ * @param {Array<RawOptimizationSite>} optimizations
+ * @param {number} optsIndex
+ *
+ * @type {RawOptimizationSite} data
+ * @type {number} samples
+ * @type {number} id
+ */
+
+const OptimizationSite = function (id, opts) {
+ this.id = id;
+ this.data = opts;
+ this.samples = 1;
+};
+
+/**
+ * Constructor for JITOptimizations. A collection of OptimizationSites for a frame.
+ *
+ * @constructor
+ * @param {Array<RawOptimizationSite>} rawSites
+ * Array of raw optimization sites.
+ * @param {Array<string>} stringTable
+ * Array of strings from the profiler used to inflate
+ * JIT optimizations. Do not modify this!
+ */
+
+const JITOptimizations = function (rawSites, stringTable) {
+ // Build a histogram of optimization sites.
+ let sites = [];
+
+ for (let rawSite of rawSites) {
+ let existingSite = sites.find((site) => site.data === rawSite);
+ if (existingSite) {
+ existingSite.samples++;
+ } else {
+ sites.push(new OptimizationSite(sites.length, rawSite));
+ }
+ }
+
+ // Inflate the optimization information.
+ for (let site of sites) {
+ let data = site.data;
+ let STRATEGY_SLOT = data.attempts.schema.strategy;
+ let OUTCOME_SLOT = data.attempts.schema.outcome;
+ let attempts = data.attempts.data.map((a) => {
+ return {
+ id: site.id,
+ strategy: stringTable[a[STRATEGY_SLOT]],
+ outcome: stringTable[a[OUTCOME_SLOT]]
+ };
+ });
+ let types = data.types.map((t) => {
+ let typeset = maybeTypeset(t.typeset, stringTable);
+ if (typeset) {
+ typeset.forEach(ts => {
+ ts.id = site.id;
+ });
+ }
+
+ return {
+ id: site.id,
+ typeset,
+ site: stringTable[t.site],
+ mirType: stringTable[t.mirType]
+ };
+ });
+ // Add IDs to to all children objects, so we can correllate sites when
+ // just looking at a specific type, attempt, etc..
+ attempts.id = types.id = site.id;
+
+ site.data = {
+ attempts,
+ types,
+ propertyName: maybeString(stringTable, data.propertyName),
+ line: data.line,
+ column: data.column
+ };
+ }
+
+ this.optimizationSites = sites.sort((a, b) => b.samples - a.samples);
+};
+
+/**
+ * Make JITOptimizations iterable.
+ */
+JITOptimizations.prototype = {
+ [Symbol.iterator]: function* () {
+ yield* this.optimizationSites;
+ },
+
+ get length() {
+ return this.optimizationSites.length;
+ }
+};
+
+/**
+ * Takes an "outcome" string from an OptimizationAttempt and returns
+ * a boolean indicating whether or not its a successful outcome.
+ *
+ * @return {boolean}
+ */
+
+function isSuccessfulOutcome(outcome) {
+ return !!~SUCCESSFUL_OUTCOMES.indexOf(outcome);
+}
+
+/**
+ * Takes an OptimizationSite. Returns a boolean indicating if the passed
+ * in OptimizationSite has a "good" outcome at the end of its attempted strategies.
+ *
+ * @param {OptimizationSite} optimizationSite
+ * @return {boolean}
+ */
+
+function hasSuccessfulOutcome(optimizationSite) {
+ let attempts = optimizationSite.data.attempts;
+ let lastOutcome = attempts[attempts.length - 1].outcome;
+ return isSuccessfulOutcome(lastOutcome);
+}
+
+function maybeString(stringTable, index) {
+ return index ? stringTable[index] : undefined;
+}
+
+function maybeTypeset(typeset, stringTable) {
+ if (!typeset) {
+ return undefined;
+ }
+ return typeset.map((ty) => {
+ return {
+ keyedBy: maybeString(stringTable, ty.keyedBy),
+ name: maybeString(stringTable, ty.name),
+ location: maybeString(stringTable, ty.location),
+ line: ty.line
+ };
+ });
+}
+
+// Map of optimization implementation names to an enum.
+const IMPLEMENTATION_MAP = {
+ "interpreter": 0,
+ "baseline": 1,
+ "ion": 2
+};
+const IMPLEMENTATION_NAMES = Object.keys(IMPLEMENTATION_MAP);
+
+/**
+ * Takes data from a FrameNode and computes rendering positions for
+ * a stacked mountain graph, to visualize JIT optimization tiers over time.
+ *
+ * @param {FrameNode} frameNode
+ * The FrameNode who's optimizations we're iterating.
+ * @param {Array<number>} sampleTimes
+ * An array of every sample time within the range we're counting.
+ * From a ThreadNode's `sampleTimes` property.
+ * @param {number} bucketSize
+ * Size of each bucket in milliseconds.
+ * `duration / resolution = bucketSize` in OptimizationsGraph.
+ * @return {?Array<object>}
+ */
+function createTierGraphDataFromFrameNode(frameNode, sampleTimes, bucketSize) {
+ let tierData = frameNode.getTierData();
+ let stringTable = frameNode._stringTable;
+ let output = [];
+ let implEnum;
+
+ let tierDataIndex = 0;
+ let nextOptSample = tierData[tierDataIndex];
+
+ // Bucket data
+ let samplesInCurrentBucket = 0;
+ let currentBucketStartTime = sampleTimes[0];
+ let bucket = [];
+
+ // Store previous data point so we can have straight vertical lines
+ let previousValues;
+
+ // Iterate one after the samples, so we can finalize the last bucket
+ for (let i = 0; i <= sampleTimes.length; i++) {
+ let sampleTime = sampleTimes[i];
+
+ // If this sample is in the next bucket, or we're done
+ // checking sampleTimes and on the last iteration, finalize previous bucket
+ if (sampleTime >= (currentBucketStartTime + bucketSize) ||
+ i >= sampleTimes.length) {
+ let dataPoint = {};
+ dataPoint.values = [];
+ dataPoint.delta = currentBucketStartTime;
+
+ // Map the opt site counts as a normalized percentage (0-1)
+ // of its count in context of total samples this bucket
+ for (let j = 0; j < IMPLEMENTATION_NAMES.length; j++) {
+ dataPoint.values[j] = (bucket[j] || 0) / (samplesInCurrentBucket || 1);
+ }
+
+ // Push the values from the previous bucket to the same time
+ // as the current bucket so we get a straight vertical line.
+ if (previousValues) {
+ let data = Object.create(null);
+ data.values = previousValues;
+ data.delta = currentBucketStartTime;
+ output.push(data);
+ }
+
+ output.push(dataPoint);
+
+ // Set the new start time of this bucket and reset its count
+ currentBucketStartTime += bucketSize;
+ samplesInCurrentBucket = 0;
+ previousValues = dataPoint.values;
+ bucket = [];
+ }
+
+ // If this sample observed an optimization in this frame, record it
+ if (nextOptSample && nextOptSample.time === sampleTime) {
+ // If no implementation defined, it was the "interpreter".
+ implEnum = IMPLEMENTATION_MAP[stringTable[nextOptSample.implementation] ||
+ "interpreter"];
+ bucket[implEnum] = (bucket[implEnum] || 0) + 1;
+ nextOptSample = tierData[++tierDataIndex];
+ }
+
+ samplesInCurrentBucket++;
+ }
+
+ return output;
+}
+
+exports.createTierGraphDataFromFrameNode = createTierGraphDataFromFrameNode;
+exports.OptimizationSite = OptimizationSite;
+exports.JITOptimizations = JITOptimizations;
+exports.hasSuccessfulOutcome = hasSuccessfulOutcome;
+exports.isSuccessfulOutcome = isSuccessfulOutcome;
+exports.SUCCESSFUL_OUTCOMES = SUCCESSFUL_OUTCOMES;
diff --git a/devtools/client/performance/modules/logic/moz.build b/devtools/client/performance/modules/logic/moz.build
new file mode 100644
index 000000000..179cd71b3
--- /dev/null
+++ b/devtools/client/performance/modules/logic/moz.build
@@ -0,0 +1,12 @@
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'frame-utils.js',
+ 'jit.js',
+ 'telemetry.js',
+ 'tree-model.js',
+ 'waterfall-utils.js',
+)
diff --git a/devtools/client/performance/modules/logic/telemetry.js b/devtools/client/performance/modules/logic/telemetry.js
new file mode 100644
index 000000000..b8e322170
--- /dev/null
+++ b/devtools/client/performance/modules/logic/telemetry.js
@@ -0,0 +1,122 @@
+/* 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 Telemetry = require("devtools/client/shared/telemetry");
+const flags = require("devtools/shared/flags");
+const EVENTS = require("devtools/client/performance/events");
+
+const EVENT_MAP_FLAGS = new Map([
+ [EVENTS.RECORDING_IMPORTED, "DEVTOOLS_PERFTOOLS_RECORDING_IMPORT_FLAG"],
+ [EVENTS.RECORDING_EXPORTED, "DEVTOOLS_PERFTOOLS_RECORDING_EXPORT_FLAG"],
+]);
+
+const RECORDING_FEATURES = [
+ "withMarkers", "withTicks", "withMemory", "withAllocations"
+];
+
+const SELECTED_VIEW_HISTOGRAM_NAME = "DEVTOOLS_PERFTOOLS_SELECTED_VIEW_MS";
+
+function PerformanceTelemetry(emitter) {
+ this._emitter = emitter;
+ this._telemetry = new Telemetry();
+ this.onFlagEvent = this.onFlagEvent.bind(this);
+ this.onRecordingStateChange = this.onRecordingStateChange.bind(this);
+ this.onViewSelected = this.onViewSelected.bind(this);
+
+ for (let [event] of EVENT_MAP_FLAGS) {
+ this._emitter.on(event, this.onFlagEvent);
+ }
+
+ this._emitter.on(EVENTS.RECORDING_STATE_CHANGE, this.onRecordingStateChange);
+ this._emitter.on(EVENTS.UI_DETAILS_VIEW_SELECTED, this.onViewSelected);
+
+ if (flags.testing) {
+ this.recordLogs();
+ }
+}
+
+PerformanceTelemetry.prototype.destroy = function () {
+ if (this._previousView) {
+ this._telemetry.stopTimer(SELECTED_VIEW_HISTOGRAM_NAME, this._previousView);
+ }
+
+ this._telemetry.destroy();
+ for (let [event] of EVENT_MAP_FLAGS) {
+ this._emitter.off(event, this.onFlagEvent);
+ }
+ this._emitter.off(EVENTS.RECORDING_STATE_CHANGE, this.onRecordingStateChange);
+ this._emitter.off(EVENTS.UI_DETAILS_VIEW_SELECTED, this.onViewSelected);
+ this._emitter = null;
+};
+
+PerformanceTelemetry.prototype.onFlagEvent = function (eventName, ...data) {
+ this._telemetry.log(EVENT_MAP_FLAGS.get(eventName), true);
+};
+
+PerformanceTelemetry.prototype.onRecordingStateChange = function (_, status, model) {
+ if (status != "recording-stopped") {
+ return;
+ }
+
+ if (model.isConsole()) {
+ this._telemetry.log("DEVTOOLS_PERFTOOLS_CONSOLE_RECORDING_COUNT", true);
+ } else {
+ this._telemetry.log("DEVTOOLS_PERFTOOLS_RECORDING_COUNT", true);
+ }
+
+ this._telemetry.log("DEVTOOLS_PERFTOOLS_RECORDING_DURATION_MS", model.getDuration());
+
+ let config = model.getConfiguration();
+ for (let k in config) {
+ if (RECORDING_FEATURES.indexOf(k) !== -1) {
+ this._telemetry.logKeyed("DEVTOOLS_PERFTOOLS_RECORDING_FEATURES_USED", k,
+ config[k]);
+ }
+ }
+};
+
+PerformanceTelemetry.prototype.onViewSelected = function (_, viewName) {
+ if (this._previousView) {
+ this._telemetry.stopTimer(SELECTED_VIEW_HISTOGRAM_NAME, this._previousView);
+ }
+ this._previousView = viewName;
+ this._telemetry.startTimer(SELECTED_VIEW_HISTOGRAM_NAME);
+};
+
+/**
+ * Utility to record histogram calls to this instance.
+ * Should only be used in testing mode; throws otherwise.
+ */
+PerformanceTelemetry.prototype.recordLogs = function () {
+ if (!flags.testing) {
+ throw new Error("Can only record telemetry logs in tests.");
+ }
+
+ let originalLog = this._telemetry.log;
+ let originalLogKeyed = this._telemetry.logKeyed;
+ this._log = {};
+
+ this._telemetry.log = (function (histo, data) {
+ let results = this._log[histo] = this._log[histo] || [];
+ results.push(data);
+ originalLog(histo, data);
+ }).bind(this);
+
+ this._telemetry.logKeyed = (function (histo, key, data) {
+ let results = this._log[histo] = this._log[histo] || [];
+ results.push([key, data]);
+ originalLogKeyed(histo, key, data);
+ }).bind(this);
+};
+
+PerformanceTelemetry.prototype.getLogs = function () {
+ if (!flags.testing) {
+ throw new Error("Can only get telemetry logs in tests.");
+ }
+
+ return this._log;
+};
+
+exports.PerformanceTelemetry = PerformanceTelemetry;
diff --git a/devtools/client/performance/modules/logic/tree-model.js b/devtools/client/performance/modules/logic/tree-model.js
new file mode 100644
index 000000000..b6376ee8a
--- /dev/null
+++ b/devtools/client/performance/modules/logic/tree-model.js
@@ -0,0 +1,556 @@
+/* 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 { JITOptimizations } = require("devtools/client/performance/modules/logic/jit");
+const FrameUtils = require("devtools/client/performance/modules/logic/frame-utils");
+
+/**
+ * A call tree for a thread. This is essentially a linkage between all frames
+ * of all samples into a single tree structure, with additional information
+ * on each node, like the time spent (in milliseconds) and samples count.
+ *
+ * @param object thread
+ * The raw thread object received from the backend. Contains samples,
+ * stackTable, frameTable, and stringTable.
+ * @param object options
+ * Additional supported options
+ * - number startTime
+ * - number endTime
+ * - boolean contentOnly [optional]
+ * - boolean invertTree [optional]
+ * - boolean flattenRecursion [optional]
+ */
+function ThreadNode(thread, options = {}) {
+ if (options.endTime == void 0 || options.startTime == void 0) {
+ throw new Error("ThreadNode requires both `startTime` and `endTime`.");
+ }
+ this.samples = 0;
+ this.sampleTimes = [];
+ this.youngestFrameSamples = 0;
+ this.calls = [];
+ this.duration = options.endTime - options.startTime;
+ this.nodeType = "Thread";
+ this.inverted = options.invertTree;
+
+ // Total bytesize of all allocations if enabled
+ this.byteSize = 0;
+ this.youngestFrameByteSize = 0;
+
+ let { samples, stackTable, frameTable, stringTable } = thread;
+
+ // Nothing to do if there are no samples.
+ if (samples.data.length === 0) {
+ return;
+ }
+
+ this._buildInverted(samples, stackTable, frameTable, stringTable, options);
+ if (!options.invertTree) {
+ this._uninvert();
+ }
+}
+
+ThreadNode.prototype = {
+ /**
+ * Build an inverted call tree from profile samples. The format of the
+ * samples is described in tools/profiler/ProfileEntry.h, under the heading
+ * "ThreadProfile JSON Format".
+ *
+ * The profile data is naturally presented inverted. Inverting the call tree
+ * is also the default in the Performance tool.
+ *
+ * @param object samples
+ * The raw samples array received from the backend.
+ * @param object stackTable
+ * The table of deduplicated stacks from the backend.
+ * @param object frameTable
+ * The table of deduplicated frames from the backend.
+ * @param object stringTable
+ * The table of deduplicated strings from the backend.
+ * @param object options
+ * Additional supported options
+ * - number startTime
+ * - number endTime
+ * - boolean contentOnly [optional]
+ * - boolean invertTree [optional]
+ */
+ _buildInverted: function buildInverted(samples, stackTable, frameTable, stringTable,
+ options) {
+ function getOrAddFrameNode(calls, isLeaf, frameKey, inflatedFrame, isMetaCategory,
+ leafTable) {
+ // Insert the inflated frame into the call tree at the current level.
+ let frameNode;
+
+ // Leaf nodes have fan out much greater than non-leaf nodes, thus the
+ // use of a hash table. Otherwise, do linear search.
+ //
+ // Note that this method is very hot, thus the manual looping over
+ // Array.prototype.find.
+ if (isLeaf) {
+ frameNode = leafTable[frameKey];
+ } else {
+ for (let i = 0; i < calls.length; i++) {
+ if (calls[i].key === frameKey) {
+ frameNode = calls[i];
+ break;
+ }
+ }
+ }
+
+ if (!frameNode) {
+ frameNode = new FrameNode(frameKey, inflatedFrame, isMetaCategory);
+ if (isLeaf) {
+ leafTable[frameKey] = frameNode;
+ }
+ calls.push(frameNode);
+ }
+
+ return frameNode;
+ }
+
+ const SAMPLE_STACK_SLOT = samples.schema.stack;
+ const SAMPLE_TIME_SLOT = samples.schema.time;
+ const SAMPLE_BYTESIZE_SLOT = samples.schema.size;
+
+ const STACK_PREFIX_SLOT = stackTable.schema.prefix;
+ const STACK_FRAME_SLOT = stackTable.schema.frame;
+
+ const getOrAddInflatedFrame = FrameUtils.getOrAddInflatedFrame;
+
+ let samplesData = samples.data;
+ let stacksData = stackTable.data;
+
+ // Caches.
+ let inflatedFrameCache = FrameUtils.getInflatedFrameCache(frameTable);
+ let leafTable = Object.create(null);
+
+ let startTime = options.startTime;
+ let endTime = options.endTime;
+ let flattenRecursion = options.flattenRecursion;
+
+ // Reused options object passed to InflatedFrame.prototype.getFrameKey.
+ let mutableFrameKeyOptions = {
+ contentOnly: options.contentOnly,
+ isRoot: false,
+ isLeaf: false,
+ isMetaCategoryOut: false
+ };
+
+ let byteSize = 0;
+ for (let i = 0; i < samplesData.length; i++) {
+ let sample = samplesData[i];
+ let sampleTime = sample[SAMPLE_TIME_SLOT];
+
+ if (SAMPLE_BYTESIZE_SLOT !== void 0) {
+ byteSize = sample[SAMPLE_BYTESIZE_SLOT];
+ }
+
+ // A sample's end time is considered to be its time of sampling. Its
+ // start time is the sampling time of the previous sample.
+ //
+ // Thus, we compare sampleTime <= start instead of < to filter out
+ // samples that end exactly at the start time.
+ if (!sampleTime || sampleTime <= startTime || sampleTime > endTime) {
+ continue;
+ }
+
+ let stackIndex = sample[SAMPLE_STACK_SLOT];
+ let calls = this.calls;
+ let prevCalls = this.calls;
+ let prevFrameKey;
+ let isLeaf = mutableFrameKeyOptions.isLeaf = true;
+ let skipRoot = options.invertTree;
+
+ // Inflate the stack and build the FrameNode call tree directly.
+ //
+ // In the profiler data, each frame's stack is referenced by an index
+ // into stackTable.
+ //
+ // Each entry in stackTable is a pair [ prefixIndex, frameIndex ]. The
+ // prefixIndex is itself an index into stackTable, referencing the
+ // prefix of the current stack (that is, the younger frames). In other
+ // words, the stackTable is encoded as a trie of the inverted
+ // callstack. The frameIndex is an index into frameTable, describing the
+ // frame at the current depth.
+ //
+ // This algorithm inflates each frame in the frame table while walking
+ // the stack trie as described above.
+ //
+ // The frame key is then computed from the inflated frame /and/ the
+ // current depth in the FrameNode call tree. That is, the frame key is
+ // not wholly determinable from just the inflated frame.
+ //
+ // For content frames, the frame key is just its location. For chrome
+ // frames, the key may be a metacategory or its location, depending on
+ // rendering options and its position in the FrameNode call tree.
+ //
+ // The frame key is then used to build up the inverted FrameNode call
+ // tree.
+ //
+ // Note that various filtering functions, such as filtering for content
+ // frames or flattening recursion, are inlined into the stack inflation
+ // loop. This is important for performance as it avoids intermediate
+ // structures and multiple passes.
+ while (stackIndex !== null) {
+ let stackEntry = stacksData[stackIndex];
+ let frameIndex = stackEntry[STACK_FRAME_SLOT];
+
+ // Fetch the stack prefix (i.e. older frames) index.
+ stackIndex = stackEntry[STACK_PREFIX_SLOT];
+
+ // Do not include the (root) node in this sample, as the costs of each frame
+ // will make it clear to differentiate (root)->B vs (root)->A->B
+ // when a tree is inverted, a revert of bug 1147604
+ if (stackIndex === null && skipRoot) {
+ break;
+ }
+
+ // Inflate the frame.
+ let inflatedFrame = getOrAddInflatedFrame(inflatedFrameCache, frameIndex,
+ frameTable, stringTable);
+
+ // Compute the frame key.
+ mutableFrameKeyOptions.isRoot = stackIndex === null;
+ let frameKey = inflatedFrame.getFrameKey(mutableFrameKeyOptions);
+
+ // An empty frame key means this frame should be skipped.
+ if (frameKey === "") {
+ continue;
+ }
+
+ // If we shouldn't flatten the current frame into the previous one, advance a
+ // level in the call tree.
+ let shouldFlatten = flattenRecursion && frameKey === prevFrameKey;
+ if (!shouldFlatten) {
+ calls = prevCalls;
+ }
+
+ let frameNode = getOrAddFrameNode(calls, isLeaf, frameKey, inflatedFrame,
+ mutableFrameKeyOptions.isMetaCategoryOut,
+ leafTable);
+ if (isLeaf) {
+ frameNode.youngestFrameSamples++;
+ frameNode._addOptimizations(inflatedFrame.optimizations,
+ inflatedFrame.implementation, sampleTime,
+ stringTable);
+
+ if (byteSize) {
+ frameNode.youngestFrameByteSize += byteSize;
+ }
+ }
+
+ // Don't overcount flattened recursive frames.
+ if (!shouldFlatten) {
+ frameNode.samples++;
+ if (byteSize) {
+ frameNode.byteSize += byteSize;
+ }
+ }
+
+ prevFrameKey = frameKey;
+ prevCalls = frameNode.calls;
+ isLeaf = mutableFrameKeyOptions.isLeaf = false;
+ }
+
+ this.samples++;
+ this.sampleTimes.push(sampleTime);
+ if (byteSize) {
+ this.byteSize += byteSize;
+ }
+ }
+ },
+
+ /**
+ * Uninverts the call tree after its having been built.
+ */
+ _uninvert: function uninvert() {
+ function mergeOrAddFrameNode(calls, node, samples, size) {
+ // Unlike the inverted call tree, we don't use a root table for the top
+ // level, as in general, there are many fewer entry points than
+ // leaves. Instead, linear search is used regardless of level.
+ for (let i = 0; i < calls.length; i++) {
+ if (calls[i].key === node.key) {
+ let foundNode = calls[i];
+ foundNode._merge(node, samples, size);
+ return foundNode.calls;
+ }
+ }
+ let copy = node._clone(samples, size);
+ calls.push(copy);
+ return copy.calls;
+ }
+
+ let workstack = [{ node: this, level: 0 }];
+ let spine = [];
+ let entry;
+
+ // The new root.
+ let rootCalls = [];
+
+ // Walk depth-first and keep the current spine (e.g., callstack).
+ do {
+ entry = workstack.pop();
+ if (entry) {
+ spine[entry.level] = entry;
+
+ let node = entry.node;
+ let calls = node.calls;
+ let callSamples = 0;
+ let callByteSize = 0;
+
+ // Continue the depth-first walk.
+ for (let i = 0; i < calls.length; i++) {
+ workstack.push({ node: calls[i], level: entry.level + 1 });
+ callSamples += calls[i].samples;
+ callByteSize += calls[i].byteSize;
+ }
+
+ // The sample delta is used to distinguish stacks.
+ //
+ // Suppose we have the following stack samples:
+ //
+ // A -> B
+ // A -> C
+ // A
+ //
+ // The inverted tree is:
+ //
+ // A
+ // / \
+ // B C
+ //
+ // with A.samples = 3, B.samples = 1, C.samples = 1.
+ //
+ // A is distinguished as being its own stack because
+ // A.samples - (B.samples + C.samples) > 0.
+ //
+ // Note that bottoming out is a degenerate where callSamples = 0.
+
+ let samplesDelta = node.samples - callSamples;
+ let byteSizeDelta = node.byteSize - callByteSize;
+ if (samplesDelta > 0) {
+ // Reverse the spine and add them to the uninverted call tree.
+ let uninvertedCalls = rootCalls;
+ for (let level = entry.level; level > 0; level--) {
+ let callee = spine[level];
+ uninvertedCalls = mergeOrAddFrameNode(uninvertedCalls, callee.node,
+ samplesDelta, byteSizeDelta);
+ }
+ }
+ }
+ } while (entry);
+
+ // Replace the toplevel calls with rootCalls, which now contains the
+ // uninverted roots.
+ this.calls = rootCalls;
+ },
+
+ /**
+ * Gets additional details about this node.
+ * @see FrameNode.prototype.getInfo for more information.
+ *
+ * @return object
+ */
+ getInfo: function (options) {
+ return FrameUtils.getFrameInfo(this, options);
+ },
+
+ /**
+ * Mimicks the interface of FrameNode, and a ThreadNode can never have
+ * optimization data (at the moment, anyway), so provide a function
+ * to return null so we don't need to check if a frame node is a thread
+ * or not everytime we fetch optimization data.
+ *
+ * @return {null}
+ */
+
+ hasOptimizations: function () {
+ return null;
+ }
+};
+
+/**
+ * A function call node in a tree. Represents a function call with a unique context,
+ * resulting in each FrameNode having its own row in the corresponding tree view.
+ * Take samples:
+ * A()->B()->C()
+ * A()->B()
+ * Q()->B()
+ *
+ * In inverted tree, A()->B()->C() would have one frame node, and A()->B() and
+ * Q()->B() would share a frame node.
+ * In an uninverted tree, A()->B()->C() and A()->B() would share a frame node,
+ * with Q()->B() having its own.
+ *
+ * In all cases, all the frame nodes originated from the same InflatedFrame.
+ *
+ * @param string frameKey
+ * The key associated with this frame. The key determines identity of
+ * the node.
+ * @param string location
+ * The location of this function call. Note that this isn't sanitized,
+ * so it may very well (not?) include the function name, url, etc.
+ * @param number line
+ * The line number inside the source containing this function call.
+ * @param number category
+ * The category type of this function call ("js", "graphics" etc.).
+ * @param number allocations
+ * The number of memory allocations performed in this frame.
+ * @param number isContent
+ * Whether this frame is content.
+ * @param boolean isMetaCategory
+ * Whether or not this is a platform node that should appear as a
+ * generalized meta category or not.
+ */
+function FrameNode(frameKey, { location, line, category, isContent }, isMetaCategory) {
+ this.key = frameKey;
+ this.location = location;
+ this.line = line;
+ this.youngestFrameSamples = 0;
+ this.samples = 0;
+ this.calls = [];
+ this.isContent = !!isContent;
+ this._optimizations = null;
+ this._tierData = [];
+ this._stringTable = null;
+ this.isMetaCategory = !!isMetaCategory;
+ this.category = category;
+ this.nodeType = "Frame";
+ this.byteSize = 0;
+ this.youngestFrameByteSize = 0;
+}
+
+FrameNode.prototype = {
+ /**
+ * Take optimization data observed for this frame.
+ *
+ * @param object optimizationSite
+ * Any JIT optimization information attached to the current
+ * sample. Lazily inflated via stringTable.
+ * @param number implementation
+ * JIT implementation used for this observed frame (baseline, ion);
+ * can be null indicating "interpreter"
+ * @param number time
+ * The time this optimization occurred.
+ * @param object stringTable
+ * The string table used to inflate the optimizationSite.
+ */
+ _addOptimizations: function (site, implementation, time, stringTable) {
+ // Simply accumulate optimization sites for now. Processing is done lazily
+ // by JITOptimizations, if optimization information is actually displayed.
+ if (site) {
+ let opts = this._optimizations;
+ if (opts === null) {
+ opts = this._optimizations = [];
+ }
+ opts.push(site);
+ }
+
+ if (!this._stringTable) {
+ this._stringTable = stringTable;
+ }
+
+ // Record type of implementation used and the sample time
+ this._tierData.push({ implementation, time });
+ },
+
+ _clone: function (samples, size) {
+ let newNode = new FrameNode(this.key, this, this.isMetaCategory);
+ newNode._merge(this, samples, size);
+ return newNode;
+ },
+
+ _merge: function (otherNode, samples, size) {
+ if (this === otherNode) {
+ return;
+ }
+
+ this.samples += samples;
+ this.byteSize += size;
+ if (otherNode.youngestFrameSamples > 0) {
+ this.youngestFrameSamples += samples;
+ }
+
+ if (otherNode.youngestFrameByteSize > 0) {
+ this.youngestFrameByteSize += otherNode.youngestFrameByteSize;
+ }
+
+ if (this._stringTable === null) {
+ this._stringTable = otherNode._stringTable;
+ }
+
+ if (otherNode._optimizations) {
+ if (!this._optimizations) {
+ this._optimizations = [];
+ }
+ let opts = this._optimizations;
+ let otherOpts = otherNode._optimizations;
+ for (let i = 0; i < otherOpts.length; i++) {
+ opts.push(otherOpts[i]);
+ }
+ }
+
+ if (otherNode._tierData.length) {
+ let tierData = this._tierData;
+ let otherTierData = otherNode._tierData;
+ for (let i = 0; i < otherTierData.length; i++) {
+ tierData.push(otherTierData[i]);
+ }
+ tierData.sort((a, b) => a.time - b.time);
+ }
+ },
+
+ /**
+ * Returns the parsed location and additional data describing
+ * this frame. Uses cached data if possible. Takes the following
+ * options:
+ *
+ * @param {ThreadNode} options.root
+ * The root thread node to calculate relative costs.
+ * Generates [self|total] [duration|percentage] values.
+ * @param {boolean} options.allocations
+ * Generates `totalAllocations` and `selfAllocations`.
+ *
+ * @return object
+ * The computed { name, file, url, line } properties for this
+ * function call, as well as additional params if options specified.
+ */
+ getInfo: function (options) {
+ return FrameUtils.getFrameInfo(this, options);
+ },
+
+ /**
+ * Returns whether or not the frame node has an JITOptimizations model.
+ *
+ * @return {Boolean}
+ */
+ hasOptimizations: function () {
+ return !this.isMetaCategory && !!this._optimizations;
+ },
+
+ /**
+ * Returns the underlying JITOptimizations model representing
+ * the optimization attempts occuring in this frame.
+ *
+ * @return {JITOptimizations|null}
+ */
+ getOptimizations: function () {
+ if (!this._optimizations) {
+ return null;
+ }
+ return new JITOptimizations(this._optimizations, this._stringTable);
+ },
+
+ /**
+ * Returns the tiers used overtime.
+ *
+ * @return {Array<object>}
+ */
+ getTierData: function () {
+ return this._tierData;
+ }
+};
+
+exports.ThreadNode = ThreadNode;
+exports.FrameNode = FrameNode;
diff --git a/devtools/client/performance/modules/logic/waterfall-utils.js b/devtools/client/performance/modules/logic/waterfall-utils.js
new file mode 100644
index 000000000..04c05a544
--- /dev/null
+++ b/devtools/client/performance/modules/logic/waterfall-utils.js
@@ -0,0 +1,167 @@
+/* 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";
+
+/**
+ * Utility functions for collapsing markers into a waterfall.
+ */
+
+const { extend } = require("sdk/util/object");
+const { MarkerBlueprintUtils } = require("devtools/client/performance/modules/marker-blueprint-utils");
+
+/**
+ * Creates a parent marker, which functions like a regular marker,
+ * but is able to hold additional child markers.
+ *
+ * The marker is seeded with values from `marker`.
+ * @param object marker
+ * @return object
+ */
+function createParentNode(marker) {
+ return extend(marker, { submarkers: [] });
+}
+
+/**
+ * Collapses markers into a tree-like structure.
+ * @param object rootNode
+ * @param array markersList
+ * @param array filter
+ */
+function collapseMarkersIntoNode({ rootNode, markersList, filter }) {
+ let {
+ getCurrentParentNode,
+ pushNode,
+ popParentNode
+ } = createParentNodeFactory(rootNode);
+
+ for (let i = 0, len = markersList.length; i < len; i++) {
+ let curr = markersList[i];
+
+ // If this marker type should not be displayed, just skip
+ if (!MarkerBlueprintUtils.shouldDisplayMarker(curr, filter)) {
+ continue;
+ }
+
+ let parentNode = getCurrentParentNode();
+ let blueprint = MarkerBlueprintUtils.getBlueprintFor(curr);
+
+ let nestable = "nestable" in blueprint ? blueprint.nestable : true;
+ let collapsible = "collapsible" in blueprint ? blueprint.collapsible : true;
+
+ let finalized = false;
+
+ // Extend the marker with extra properties needed in the marker tree
+ let extendedProps = { index: i };
+ if (collapsible) {
+ extendedProps.submarkers = [];
+ }
+ curr = extend(curr, extendedProps);
+
+ // If not nestible, just push it inside the root node. Additionally,
+ // markers originating outside the main thread are considered to be
+ // "never collapsible", to avoid confusion.
+ // A beter solution would be to collapse every marker with its siblings
+ // from the same thread, but that would require a thread id attached
+ // to all markers, which is potentially expensive and rather useless at
+ // the moment, since we don't really have that many OTMT markers.
+ if (!nestable || curr.isOffMainThread) {
+ pushNode(rootNode, curr);
+ continue;
+ }
+
+ // First off, if any parent nodes exist, finish them off
+ // recursively upwards if this marker is outside their ranges and nestable.
+ while (!finalized && parentNode) {
+ // If this marker is eclipsed by the current parent marker,
+ // make it a child of the current parent and stop going upwards.
+ // If the markers aren't from the same process, attach them to the root
+ // node as well. Every process has its own main thread.
+ if (nestable &&
+ curr.start >= parentNode.start &&
+ curr.end <= parentNode.end &&
+ curr.processType == parentNode.processType) {
+ pushNode(parentNode, curr);
+ finalized = true;
+ break;
+ }
+
+ // If this marker is still nestable, but outside of the range
+ // of the current parent, iterate upwards on the next parent
+ // and finalize the current parent.
+ if (nestable) {
+ popParentNode();
+ parentNode = getCurrentParentNode();
+ continue;
+ }
+ }
+
+ if (!finalized) {
+ pushNode(rootNode, curr);
+ }
+ }
+}
+
+/**
+ * Takes a root marker node and creates a hash of functions used
+ * to manage the creation and nesting of additional parent markers.
+ *
+ * @param {object} root
+ * @return {object}
+ */
+function createParentNodeFactory(root) {
+ let parentMarkers = [];
+ let factory = {
+ /**
+ * Pops the most recent parent node off the stack, finalizing it.
+ * Sets the `end` time based on the most recent child if not defined.
+ */
+ popParentNode: () => {
+ if (parentMarkers.length === 0) {
+ throw new Error("Cannot pop parent markers when none exist.");
+ }
+
+ let lastParent = parentMarkers.pop();
+
+ // If this finished parent marker doesn't have an end time,
+ // so probably a synthesized marker, use the last marker's end time.
+ if (lastParent.end == void 0) {
+ lastParent.end = lastParent.submarkers[lastParent.submarkers.length - 1].end;
+ }
+
+ // If no children were ever pushed into this parent node,
+ // remove its submarkers so it behaves like a non collapsible
+ // node.
+ if (!lastParent.submarkers.length) {
+ delete lastParent.submarkers;
+ }
+
+ return lastParent;
+ },
+
+ /**
+ * Returns the most recent parent node.
+ */
+ getCurrentParentNode: () => parentMarkers.length
+ ? parentMarkers[parentMarkers.length - 1]
+ : null,
+
+ /**
+ * Push this marker into the most recent parent node.
+ */
+ pushNode: (parent, marker) => {
+ parent.submarkers.push(marker);
+
+ // If pushing a parent marker, track it as the top of
+ // the parent stack.
+ if (marker.submarkers) {
+ parentMarkers.push(marker);
+ }
+ }
+ };
+
+ return factory;
+}
+
+exports.createParentNode = createParentNode;
+exports.collapseMarkersIntoNode = collapseMarkersIntoNode;
diff --git a/devtools/client/performance/modules/marker-blueprint-utils.js b/devtools/client/performance/modules/marker-blueprint-utils.js
new file mode 100644
index 000000000..e60ea0eaa
--- /dev/null
+++ b/devtools/client/performance/modules/marker-blueprint-utils.js
@@ -0,0 +1,104 @@
+/* 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 { TIMELINE_BLUEPRINT } = require("devtools/client/performance/modules/markers");
+
+/**
+ * This file contains utilities for parsing out the markers blueprint
+ * to generate strings to be displayed in the UI.
+ */
+
+exports.MarkerBlueprintUtils = {
+ /**
+ * Takes a marker and a list of marker names that should be hidden, and
+ * determines if this marker should be filtered or not.
+ *
+ * @param object marker
+ * @return boolean
+ */
+ shouldDisplayMarker: function (marker, hiddenMarkerNames) {
+ if (!hiddenMarkerNames || hiddenMarkerNames.length == 0) {
+ return true;
+ }
+
+ // If this marker isn't yet defined in the blueprint, simply check if the
+ // entire category of "UNKNOWN" markers are supposed to be visible or not.
+ let isUnknown = !(marker.name in TIMELINE_BLUEPRINT);
+ if (isUnknown) {
+ return hiddenMarkerNames.indexOf("UNKNOWN") == -1;
+ }
+
+ return hiddenMarkerNames.indexOf(marker.name) == -1;
+ },
+
+ /**
+ * Takes a marker and returns the blueprint definition for that marker type,
+ * falling back to the UNKNOWN blueprint definition if undefined.
+ *
+ * @param object marker
+ * @return object
+ */
+ getBlueprintFor: function (marker) {
+ return TIMELINE_BLUEPRINT[marker.name] || TIMELINE_BLUEPRINT.UNKNOWN;
+ },
+
+ /**
+ * Returns the label to display for a marker, based off the blueprints.
+ *
+ * @param object marker
+ * @return string
+ */
+ getMarkerLabel: function (marker) {
+ let blueprint = this.getBlueprintFor(marker);
+ let dynamic = typeof blueprint.label === "function";
+ let label = dynamic ? blueprint.label(marker) : blueprint.label;
+ return label;
+ },
+
+ /**
+ * Returns the generic label to display for a marker name.
+ * (e.g. "Function Call" for JS markers, rather than "setTimeout", etc.)
+ *
+ * @param string type
+ * @return string
+ */
+ getMarkerGenericName: function (markerName) {
+ let blueprint = this.getBlueprintFor({ name: markerName });
+ let dynamic = typeof blueprint.label === "function";
+ let generic = dynamic ? blueprint.label() : blueprint.label;
+
+ // If no class name found, attempt to throw a descriptive error as to
+ // how the marker implementor can fix this.
+ if (!generic) {
+ let message = `Could not find marker generic name for "${markerName}".`;
+ if (typeof blueprint.label === "function") {
+ message += ` The following function must return a generic name string when no` +
+ ` marker passed: ${blueprint.label}`;
+ } else {
+ message += ` ${markerName}.label must be defined in the marker blueprint.`;
+ }
+ throw new Error(message);
+ }
+
+ return generic;
+ },
+
+ /**
+ * Returns an array of objects with key/value pairs of what should be rendered
+ * in the marker details view.
+ *
+ * @param object marker
+ * @return array<object>
+ */
+ getMarkerFields: function (marker) {
+ let blueprint = this.getBlueprintFor(marker);
+ let dynamic = typeof blueprint.fields === "function";
+ let fields = dynamic ? blueprint.fields(marker) : blueprint.fields;
+
+ return Object.entries(fields || {})
+ .filter(([_, value]) => dynamic ? true : value in marker)
+ .map(([label, value]) => ({ label, value: dynamic ? value : marker[value] }));
+ },
+};
diff --git a/devtools/client/performance/modules/marker-dom-utils.js b/devtools/client/performance/modules/marker-dom-utils.js
new file mode 100644
index 000000000..006b13171
--- /dev/null
+++ b/devtools/client/performance/modules/marker-dom-utils.js
@@ -0,0 +1,257 @@
+/* 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";
+
+/**
+ * This file contains utilities for creating DOM nodes for markers
+ * to be displayed in the UI.
+ */
+
+const { L10N, PREFS } = require("devtools/client/performance/modules/global");
+const { MarkerBlueprintUtils } = require("devtools/client/performance/modules/marker-blueprint-utils");
+const { getSourceNames } = require("devtools/client/shared/source-utils");
+
+/**
+ * Utilites for creating elements for markers.
+ */
+exports.MarkerDOMUtils = {
+ /**
+ * Builds all the fields possible for the given marker. Returns an
+ * array of elements to be appended to a parent element.
+ *
+ * @param document doc
+ * @param object marker
+ * @return array<nsIDOMNode>
+ */
+ buildFields: function (doc, marker) {
+ let fields = MarkerBlueprintUtils.getMarkerFields(marker);
+ return fields.map(({ label, value }) => this.buildNameValueLabel(doc, label, value));
+ },
+
+ /**
+ * Builds the label representing the marker's type.
+ *
+ * @param document doc
+ * @param object marker
+ * @return nsIDOMNode
+ */
+ buildTitle: function (doc, marker) {
+ let blueprint = MarkerBlueprintUtils.getBlueprintFor(marker);
+
+ let hbox = doc.createElement("hbox");
+ hbox.setAttribute("align", "center");
+
+ let bullet = doc.createElement("hbox");
+ bullet.className = `marker-details-bullet marker-color-${blueprint.colorName}`;
+
+ let title = MarkerBlueprintUtils.getMarkerLabel(marker);
+ let label = doc.createElement("label");
+ label.className = "marker-details-type";
+ label.setAttribute("value", title);
+
+ hbox.appendChild(bullet);
+ hbox.appendChild(label);
+
+ return hbox;
+ },
+
+ /**
+ * Builds the label representing the marker's duration.
+ *
+ * @param document doc
+ * @param object marker
+ * @return nsIDOMNode
+ */
+ buildDuration: function (doc, marker) {
+ let label = L10N.getStr("marker.field.duration");
+ let start = L10N.getFormatStrWithNumbers("timeline.tick", marker.start);
+ let end = L10N.getFormatStrWithNumbers("timeline.tick", marker.end);
+ let duration = L10N.getFormatStrWithNumbers("timeline.tick",
+ marker.end - marker.start);
+
+ let el = this.buildNameValueLabel(doc, label, duration);
+ el.classList.add("marker-details-duration");
+ el.setAttribute("tooltiptext", `${start} → ${end}`);
+
+ return el;
+ },
+
+ /**
+ * Builds labels for name:value pairs.
+ * E.g. "Start: 100ms", "Duration: 200ms", ...
+ *
+ * @param document doc
+ * @param string field
+ * @param string value
+ * @return nsIDOMNode
+ */
+ buildNameValueLabel: function (doc, field, value) {
+ let hbox = doc.createElement("hbox");
+ hbox.className = "marker-details-labelcontainer";
+
+ let nameLabel = doc.createElement("label");
+ nameLabel.className = "plain marker-details-name-label";
+ nameLabel.setAttribute("value", field);
+ hbox.appendChild(nameLabel);
+
+ let valueLabel = doc.createElement("label");
+ valueLabel.className = "plain marker-details-value-label";
+ valueLabel.setAttribute("value", value);
+ hbox.appendChild(valueLabel);
+
+ return hbox;
+ },
+
+ /**
+ * Builds a stack trace in an element.
+ *
+ * @param document doc
+ * @param object params
+ * An options object with the following members:
+ * - string type: string identifier for type of stack ("stack", "startStack"
+ or "endStack"
+ * - number frameIndex: the index of the topmost stack frame
+ * - array frames: array of stack frames
+ */
+ buildStackTrace: function (doc, { type, frameIndex, frames }) {
+ let container = doc.createElement("vbox");
+ container.className = "marker-details-stack";
+ container.setAttribute("type", type);
+
+ let nameLabel = doc.createElement("label");
+ nameLabel.className = "plain marker-details-name-label";
+ nameLabel.setAttribute("value", L10N.getStr(`marker.field.${type}`));
+ container.appendChild(nameLabel);
+
+ // Workaround for profiles that have looping stack traces. See
+ // bug 1246555.
+ let wasAsyncParent = false;
+ let seen = new Set();
+
+ while (frameIndex > 0) {
+ if (seen.has(frameIndex)) {
+ break;
+ }
+ seen.add(frameIndex);
+
+ let frame = frames[frameIndex];
+ let url = frame.source;
+ let displayName = frame.functionDisplayName;
+ let line = frame.line;
+
+ // If the previous frame had an async parent, then the async
+ // cause is in this frame and should be displayed.
+ if (wasAsyncParent) {
+ let asyncStr = L10N.getFormatStr("marker.field.asyncStack", frame.asyncCause);
+ let asyncBox = doc.createElement("hbox");
+ let asyncLabel = doc.createElement("label");
+ asyncLabel.className = "devtools-monospace";
+ asyncLabel.setAttribute("value", asyncStr);
+ asyncBox.appendChild(asyncLabel);
+ container.appendChild(asyncBox);
+ wasAsyncParent = false;
+ }
+
+ let hbox = doc.createElement("hbox");
+
+ if (displayName) {
+ let functionLabel = doc.createElement("label");
+ functionLabel.className = "devtools-monospace";
+ functionLabel.setAttribute("value", displayName);
+ hbox.appendChild(functionLabel);
+ }
+
+ if (url) {
+ let linkNode = doc.createElement("a");
+ linkNode.className = "waterfall-marker-location devtools-source-link";
+ linkNode.href = url;
+ linkNode.draggable = false;
+ linkNode.setAttribute("title", url);
+
+ let urlLabel = doc.createElement("label");
+ urlLabel.className = "filename";
+ urlLabel.setAttribute("value", getSourceNames(url).short);
+ linkNode.appendChild(urlLabel);
+
+ let lineLabel = doc.createElement("label");
+ lineLabel.className = "line-number";
+ lineLabel.setAttribute("value", `:${line}`);
+ linkNode.appendChild(lineLabel);
+
+ hbox.appendChild(linkNode);
+
+ // Clicking here will bubble up to the parent,
+ // which handles the view source.
+ linkNode.setAttribute("data-action", JSON.stringify({
+ url: url,
+ line: line,
+ action: "view-source"
+ }));
+ }
+
+ if (!displayName && !url) {
+ let unknownLabel = doc.createElement("label");
+ unknownLabel.setAttribute("value", L10N.getStr("marker.value.unknownFrame"));
+ hbox.appendChild(unknownLabel);
+ }
+
+ container.appendChild(hbox);
+
+ if (frame.asyncParent) {
+ frameIndex = frame.asyncParent;
+ wasAsyncParent = true;
+ } else {
+ frameIndex = frame.parent;
+ }
+ }
+
+ return container;
+ },
+
+ /**
+ * Builds any custom fields specific to the marker.
+ *
+ * @param document doc
+ * @param object marker
+ * @param object options
+ * @return array<nsIDOMNode>
+ */
+ buildCustom: function (doc, marker, options) {
+ let elements = [];
+
+ if (options.allocations && shouldShowAllocationsTrigger(marker)) {
+ let hbox = doc.createElement("hbox");
+ hbox.className = "marker-details-customcontainer";
+
+ let label = doc.createElement("label");
+ label.className = "custom-button devtools-button";
+ label.setAttribute("value", "Show allocation triggers");
+ label.setAttribute("type", "show-allocations");
+ label.setAttribute("data-action", JSON.stringify({
+ endTime: marker.start,
+ action: "show-allocations"
+ }));
+
+ hbox.appendChild(label);
+ elements.push(hbox);
+ }
+
+ return elements;
+ },
+};
+
+/**
+ * Takes a marker and determines if this marker should display
+ * the allocations trigger button.
+ *
+ * @param object marker
+ * @return boolean
+ */
+function shouldShowAllocationsTrigger(marker) {
+ if (marker.name == "GarbageCollection") {
+ let showTriggers = PREFS["show-triggers-for-gc-types"];
+ return showTriggers.split(" ").indexOf(marker.causeName) !== -1;
+ }
+ return false;
+}
diff --git a/devtools/client/performance/modules/marker-formatters.js b/devtools/client/performance/modules/marker-formatters.js
new file mode 100644
index 000000000..0d74913cc
--- /dev/null
+++ b/devtools/client/performance/modules/marker-formatters.js
@@ -0,0 +1,199 @@
+/* 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";
+
+/**
+ * This file contains utilities for creating elements for markers to be displayed,
+ * and parsing out the blueprint to generate correct values for markers.
+ */
+const { Ci } = require("chrome");
+const { L10N, PREFS } = require("devtools/client/performance/modules/global");
+
+// String used to fill in platform data when it should be hidden.
+const GECKO_SYMBOL = "(Gecko)";
+
+/**
+ * Mapping of JS marker causes to a friendlier form. Only
+ * markers that are considered "from content" should be labeled here.
+ */
+const JS_MARKER_MAP = {
+ "<script> element": L10N.getStr("marker.label.javascript.scriptElement"),
+ "promise callback": L10N.getStr("marker.label.javascript.promiseCallback"),
+ "promise initializer": L10N.getStr("marker.label.javascript.promiseInit"),
+ "Worker runnable": L10N.getStr("marker.label.javascript.workerRunnable"),
+ "javascript: URI": L10N.getStr("marker.label.javascript.jsURI"),
+ // The difference between these two event handler markers are differences
+ // in their WebIDL implementation, so distinguishing them is not necessary.
+ "EventHandlerNonNull": L10N.getStr("marker.label.javascript.eventHandler"),
+ "EventListener.handleEvent": L10N.getStr("marker.label.javascript.eventHandler"),
+ // These markers do not get L10N'd because they're JS names.
+ "setInterval handler": "setInterval",
+ "setTimeout handler": "setTimeout",
+ "FrameRequestCallback": "requestAnimationFrame",
+};
+
+/**
+ * A series of formatters used by the blueprint.
+ */
+exports.Formatters = {
+ /**
+ * Uses the marker name as the label for markers that do not have
+ * a blueprint entry. Uses "Other" in the marker filter menu.
+ */
+ UnknownLabel: function (marker = {}) {
+ return marker.name || L10N.getStr("marker.label.unknown");
+ },
+
+ /* Group 0 - Reflow and Rendering pipeline */
+
+ StylesFields: function (marker) {
+ if ("restyleHint" in marker) {
+ let label = marker.restyleHint.replace(/eRestyle_/g, "");
+ return {
+ [L10N.getStr("marker.field.restyleHint")]: label
+ };
+ }
+ return null;
+ },
+
+ /* Group 1 - JS */
+
+ DOMEventFields: function (marker) {
+ let fields = Object.create(null);
+
+ if ("type" in marker) {
+ fields[L10N.getStr("marker.field.DOMEventType")] = marker.type;
+ }
+
+ if ("eventPhase" in marker) {
+ let label;
+ switch (marker.eventPhase) {
+ case Ci.nsIDOMEvent.AT_TARGET:
+ label = L10N.getStr("marker.value.DOMEventTargetPhase");
+ break;
+ case Ci.nsIDOMEvent.CAPTURING_PHASE:
+ label = L10N.getStr("marker.value.DOMEventCapturingPhase");
+ break;
+ case Ci.nsIDOMEvent.BUBBLING_PHASE:
+ label = L10N.getStr("marker.value.DOMEventBubblingPhase");
+ break;
+ }
+ fields[L10N.getStr("marker.field.DOMEventPhase")] = label;
+ }
+
+ return fields;
+ },
+
+ JSLabel: function (marker = {}) {
+ let generic = L10N.getStr("marker.label.javascript");
+ if ("causeName" in marker) {
+ return JS_MARKER_MAP[marker.causeName] || generic;
+ }
+ return generic;
+ },
+
+ JSFields: function (marker) {
+ if ("causeName" in marker && !JS_MARKER_MAP[marker.causeName]) {
+ let label = PREFS["show-platform-data"] ? marker.causeName : GECKO_SYMBOL;
+ return {
+ [L10N.getStr("marker.field.causeName")]: label
+ };
+ }
+ return null;
+ },
+
+ GCLabel: function (marker) {
+ if (!marker) {
+ return L10N.getStr("marker.label.garbageCollection2");
+ }
+ // Only if a `nonincrementalReason` exists, do we want to label
+ // this as a non incremental GC event.
+ if ("nonincrementalReason" in marker) {
+ return L10N.getStr("marker.label.garbageCollection.nonIncremental");
+ }
+ return L10N.getStr("marker.label.garbageCollection.incremental");
+ },
+
+ GCFields: function (marker) {
+ let fields = Object.create(null);
+
+ if ("causeName" in marker) {
+ let cause = marker.causeName;
+ let label = L10N.getStr(`marker.gcreason.label.${cause}`) || cause;
+ fields[L10N.getStr("marker.field.causeName")] = label;
+ }
+
+ if ("nonincrementalReason" in marker) {
+ let label = marker.nonincrementalReason;
+ fields[L10N.getStr("marker.field.nonIncrementalCause")] = label;
+ }
+
+ return fields;
+ },
+
+ MinorGCFields: function (marker) {
+ let fields = Object.create(null);
+
+ if ("causeName" in marker) {
+ let cause = marker.causeName;
+ let label = L10N.getStr(`marker.gcreason.label.${cause}`) || cause;
+ fields[L10N.getStr("marker.field.causeName")] = label;
+ }
+
+ fields[L10N.getStr("marker.field.type")] = L10N.getStr("marker.nurseryCollection");
+
+ return fields;
+ },
+
+ CycleCollectionFields: function (marker) {
+ let label = marker.name.replace(/nsCycleCollector::/g, "");
+ return {
+ [L10N.getStr("marker.field.type")]: label
+ };
+ },
+
+ WorkerFields: function (marker) {
+ if ("workerOperation" in marker) {
+ let label = L10N.getStr(`marker.worker.${marker.workerOperation}`);
+ return {
+ [L10N.getStr("marker.field.type")]: label
+ };
+ }
+ return null;
+ },
+
+ MessagePortFields: function (marker) {
+ if ("messagePortOperation" in marker) {
+ let label = L10N.getStr(`marker.messagePort.${marker.messagePortOperation}`);
+ return {
+ [L10N.getStr("marker.field.type")]: label
+ };
+ }
+ return null;
+ },
+
+ /* Group 2 - User Controlled */
+
+ ConsoleTimeFields: {
+ [L10N.getStr("marker.field.consoleTimerName")]: "causeName"
+ },
+
+ TimeStampFields: {
+ [L10N.getStr("marker.field.label")]: "causeName"
+ }
+};
+
+/**
+ * Takes a main label (e.g. "Timestamp") and a property name (e.g. "causeName"),
+ * and returns a string that represents that property value for a marker if it
+ * exists (e.g. "Timestamp (rendering)"), or just the main label if it does not.
+ *
+ * @param string mainLabel
+ * @param string propName
+ */
+exports.Formatters.labelForProperty = function (mainLabel, propName) {
+ return (marker = {}) => marker[propName]
+ ? `${mainLabel} (${marker[propName]})`
+ : mainLabel;
+};
diff --git a/devtools/client/performance/modules/markers.js b/devtools/client/performance/modules/markers.js
new file mode 100644
index 000000000..da9d3aad3
--- /dev/null
+++ b/devtools/client/performance/modules/markers.js
@@ -0,0 +1,170 @@
+/* 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 { L10N } = require("devtools/client/performance/modules/global");
+const { Formatters } = require("devtools/client/performance/modules/marker-formatters");
+
+/**
+ * A simple schema for mapping markers to the timeline UI. The keys correspond
+ * to marker names, while the values are objects with the following format:
+ *
+ * - group: The row index in the overview graph; multiple markers
+ * can be added on the same row. @see <overview.js/buildGraphImage>
+ * - label: The label used in the waterfall to identify the marker. Can be a
+ * string or just a function that accepts the marker and returns a
+ * string (if you want to use a dynamic property for the main label).
+ * If you use a function for a label, it *must* handle the case where
+ * no marker is provided, to get a generic label used to describe
+ * all markers of this type.
+ * - fields: The fields used in the marker details view to display more
+ * information about a currently selected marker. Can either be an
+ * object of fields, or simply a function that accepts the marker and
+ * returns such an object (if you want to use properties dynamically).
+ * For example, a field in the object such as { "Cause": "causeName" }
+ * would render something like `Cause: ${marker.causeName}` in the UI.
+ * - colorName: The label of the DevTools color used for this marker. If
+ * adding a new color, be sure to check that there's an entry
+ * for `.marker-color-graphs-{COLORNAME}` for the equivilent
+ * entry in "./devtools/client/themes/performance.css"
+ * https://developer.mozilla.org/en-US/docs/Tools/DevToolsColors
+ * - collapsible: Whether or not this marker can contain other markers it
+ * eclipses, and becomes collapsible to reveal its nestable
+ * children. Defaults to true.
+ * - nestable: Whether or not this marker can be nested inside an eclipsing
+ * collapsible marker. Defaults to true.
+ */
+const TIMELINE_BLUEPRINT = {
+ /* Default definition used for markers that occur but are not defined here.
+ * Should ultimately be defined, but this gives us room to work on the
+ * front end separately from the platform. */
+ "UNKNOWN": {
+ group: 2,
+ colorName: "graphs-grey",
+ label: Formatters.UnknownLabel,
+ },
+
+ /* Group 0 - Reflow and Rendering pipeline */
+
+ "Styles": {
+ group: 0,
+ colorName: "graphs-purple",
+ label: L10N.getStr("marker.label.styles"),
+ fields: Formatters.StylesFields,
+ },
+ "Reflow": {
+ group: 0,
+ colorName: "graphs-purple",
+ label: L10N.getStr("marker.label.reflow"),
+ },
+ "Paint": {
+ group: 0,
+ colorName: "graphs-green",
+ label: L10N.getStr("marker.label.paint"),
+ },
+ "Composite": {
+ group: 0,
+ colorName: "graphs-green",
+ label: L10N.getStr("marker.label.composite"),
+ },
+ "CompositeForwardTransaction": {
+ group: 0,
+ colorName: "graphs-bluegrey",
+ label: L10N.getStr("marker.label.compositeForwardTransaction"),
+ },
+
+ /* Group 1 - JS */
+
+ "DOMEvent": {
+ group: 1,
+ colorName: "graphs-yellow",
+ label: L10N.getStr("marker.label.domevent"),
+ fields: Formatters.DOMEventFields,
+ },
+ "document::DOMContentLoaded": {
+ group: 1,
+ colorName: "graphs-full-red",
+ label: "DOMContentLoaded"
+ },
+ "document::Load": {
+ group: 1,
+ colorName: "graphs-full-blue",
+ label: "Load"
+ },
+ "Javascript": {
+ group: 1,
+ colorName: "graphs-yellow",
+ label: Formatters.JSLabel,
+ fields: Formatters.JSFields
+ },
+ "Parse HTML": {
+ group: 1,
+ colorName: "graphs-yellow",
+ label: L10N.getStr("marker.label.parseHTML"),
+ },
+ "Parse XML": {
+ group: 1,
+ colorName: "graphs-yellow",
+ label: L10N.getStr("marker.label.parseXML"),
+ },
+ "GarbageCollection": {
+ group: 1,
+ colorName: "graphs-red",
+ label: Formatters.GCLabel,
+ fields: Formatters.GCFields,
+ },
+ "MinorGC": {
+ group: 1,
+ colorName: "graphs-red",
+ label: L10N.getStr("marker.label.minorGC"),
+ fields: Formatters.MinorGCFields,
+ },
+ "nsCycleCollector::Collect": {
+ group: 1,
+ colorName: "graphs-red",
+ label: L10N.getStr("marker.label.cycleCollection"),
+ fields: Formatters.CycleCollectionFields,
+ },
+ "nsCycleCollector::ForgetSkippable": {
+ group: 1,
+ colorName: "graphs-red",
+ label: L10N.getStr("marker.label.cycleCollection.forgetSkippable"),
+ fields: Formatters.CycleCollectionFields,
+ },
+ "Worker": {
+ group: 1,
+ colorName: "graphs-orange",
+ label: L10N.getStr("marker.label.worker"),
+ fields: Formatters.WorkerFields
+ },
+ "MessagePort": {
+ group: 1,
+ colorName: "graphs-orange",
+ label: L10N.getStr("marker.label.messagePort"),
+ fields: Formatters.MessagePortFields
+ },
+
+ /* Group 2 - User Controlled */
+
+ "ConsoleTime": {
+ group: 2,
+ colorName: "graphs-blue",
+ label: Formatters.labelForProperty(L10N.getStr("marker.label.consoleTime"),
+ "causeName"),
+ fields: Formatters.ConsoleTimeFields,
+ nestable: false,
+ collapsible: false,
+ },
+ "TimeStamp": {
+ group: 2,
+ colorName: "graphs-blue",
+ label: Formatters.labelForProperty(L10N.getStr("marker.label.timestamp"),
+ "causeName"),
+ fields: Formatters.TimeStampFields,
+ collapsible: false,
+ },
+};
+
+// Exported symbols.
+exports.TIMELINE_BLUEPRINT = TIMELINE_BLUEPRINT;
diff --git a/devtools/client/performance/modules/moz.build b/devtools/client/performance/modules/moz.build
new file mode 100644
index 000000000..45d2ae0d2
--- /dev/null
+++ b/devtools/client/performance/modules/moz.build
@@ -0,0 +1,22 @@
+# vim: set filetype=python:
+# 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/.
+
+DIRS += [
+ 'logic',
+ 'widgets',
+]
+
+DevToolsModules(
+ 'categories.js',
+ 'constants.js',
+ 'global.js',
+ 'io.js',
+ 'marker-blueprint-utils.js',
+ 'marker-dom-utils.js',
+ 'marker-formatters.js',
+ 'markers.js',
+ 'utils.js',
+ 'waterfall-ticks.js',
+)
diff --git a/devtools/client/performance/modules/utils.js b/devtools/client/performance/modules/utils.js
new file mode 100644
index 000000000..a376edc6a
--- /dev/null
+++ b/devtools/client/performance/modules/utils.js
@@ -0,0 +1,21 @@
+/* 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";
+
+/* globals document */
+
+/**
+ * React components grab the namespace of the element they are mounting to. This function
+ * takes a XUL element, and makes sure to create a properly namespaced HTML element to
+ * avoid React creating XUL elements.
+ *
+ * {XULElement} xulElement
+ * return {HTMLElement} div
+ */
+
+exports.createHtmlMount = function (xulElement) {
+ let htmlElement = document.createElementNS("http://www.w3.org/1999/xhtml", "div");
+ xulElement.appendChild(htmlElement);
+ return htmlElement;
+};
diff --git a/devtools/client/performance/modules/waterfall-ticks.js b/devtools/client/performance/modules/waterfall-ticks.js
new file mode 100644
index 000000000..76eb8a6c9
--- /dev/null
+++ b/devtools/client/performance/modules/waterfall-ticks.js
@@ -0,0 +1,98 @@
+/* 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 HTML_NS = "http://www.w3.org/1999/xhtml";
+
+// ms
+const WATERFALL_BACKGROUND_TICKS_MULTIPLE = 5;
+const WATERFALL_BACKGROUND_TICKS_SCALES = 3;
+// px
+const WATERFALL_BACKGROUND_TICKS_SPACING_MIN = 10;
+const WATERFALL_BACKGROUND_TICKS_COLOR_RGB = [128, 136, 144];
+// byte
+const WATERFALL_BACKGROUND_TICKS_OPACITY_MIN = 32;
+// byte
+const WATERFALL_BACKGROUND_TICKS_OPACITY_ADD = 32;
+
+const FIND_OPTIMAL_TICK_INTERVAL_MAX_ITERS = 100;
+
+/**
+ * Creates the background displayed on the marker's waterfall.
+ */
+function drawWaterfallBackground(doc, dataScale, waterfallWidth) {
+ let canvas = doc.createElementNS(HTML_NS, "canvas");
+ let ctx = canvas.getContext("2d");
+
+ // Nuke the context.
+ let canvasWidth = canvas.width = waterfallWidth;
+ // Awww yeah, 1px, repeats on Y axis.
+ let canvasHeight = canvas.height = 1;
+
+ // Start over.
+ let imageData = ctx.createImageData(canvasWidth, canvasHeight);
+ let pixelArray = imageData.data;
+
+ let buf = new ArrayBuffer(pixelArray.length);
+ let view8bit = new Uint8ClampedArray(buf);
+ let view32bit = new Uint32Array(buf);
+
+ // Build new millisecond tick lines...
+ let [r, g, b] = WATERFALL_BACKGROUND_TICKS_COLOR_RGB;
+ let alphaComponent = WATERFALL_BACKGROUND_TICKS_OPACITY_MIN;
+ let tickInterval = findOptimalTickInterval({
+ ticksMultiple: WATERFALL_BACKGROUND_TICKS_MULTIPLE,
+ ticksSpacingMin: WATERFALL_BACKGROUND_TICKS_SPACING_MIN,
+ dataScale: dataScale
+ });
+
+ // Insert one pixel for each division on each scale.
+ for (let i = 1; i <= WATERFALL_BACKGROUND_TICKS_SCALES; i++) {
+ let increment = tickInterval * Math.pow(2, i);
+ for (let x = 0; x < canvasWidth; x += increment) {
+ let position = x | 0;
+ view32bit[position] = (alphaComponent << 24) | (b << 16) | (g << 8) | r;
+ }
+ alphaComponent += WATERFALL_BACKGROUND_TICKS_OPACITY_ADD;
+ }
+
+ // Flush the image data and cache the waterfall background.
+ pixelArray.set(view8bit);
+ ctx.putImageData(imageData, 0, 0);
+ doc.mozSetImageElement("waterfall-background", canvas);
+
+ return canvas;
+}
+
+/**
+ * Finds the optimal tick interval between time markers in this timeline.
+ *
+ * @param number ticksMultiple
+ * @param number ticksSpacingMin
+ * @param number dataScale
+ * @return number
+ */
+function findOptimalTickInterval({ ticksMultiple, ticksSpacingMin, dataScale }) {
+ let timingStep = ticksMultiple;
+ let maxIters = FIND_OPTIMAL_TICK_INTERVAL_MAX_ITERS;
+ let numIters = 0;
+
+ if (dataScale > ticksSpacingMin) {
+ return dataScale;
+ }
+
+ while (true) {
+ let scaledStep = dataScale * timingStep;
+ if (++numIters > maxIters) {
+ return scaledStep;
+ }
+ if (scaledStep < ticksSpacingMin) {
+ timingStep <<= 1;
+ continue;
+ }
+ return scaledStep;
+ }
+}
+
+exports.TickUtils = { findOptimalTickInterval, drawWaterfallBackground };
diff --git a/devtools/client/performance/modules/widgets/graphs.js b/devtools/client/performance/modules/widgets/graphs.js
new file mode 100644
index 000000000..9d9262027
--- /dev/null
+++ b/devtools/client/performance/modules/widgets/graphs.js
@@ -0,0 +1,514 @@
+/* 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";
+
+/**
+ * This file contains the base line graph that all Performance line graphs use.
+ */
+
+const { Task } = require("devtools/shared/task");
+const { Heritage } = require("devtools/client/shared/widgets/view-helpers");
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+const MountainGraphWidget = require("devtools/client/shared/widgets/MountainGraphWidget");
+const { CanvasGraphUtils } = require("devtools/client/shared/widgets/Graphs");
+
+const promise = require("promise");
+const EventEmitter = require("devtools/shared/event-emitter");
+
+const { colorUtils } = require("devtools/shared/css/color");
+const { getColor } = require("devtools/client/shared/theme");
+const ProfilerGlobal = require("devtools/client/performance/modules/global");
+const { MarkersOverview } = require("devtools/client/performance/modules/widgets/markers-overview");
+const { createTierGraphDataFromFrameNode } = require("devtools/client/performance/modules/logic/jit");
+
+/**
+ * For line graphs
+ */
+// px
+const HEIGHT = 35;
+// px
+const STROKE_WIDTH = 1;
+const DAMPEN_VALUES = 0.95;
+const CLIPHEAD_LINE_COLOR = "#666";
+const SELECTION_LINE_COLOR = "#555";
+const SELECTION_BACKGROUND_COLOR_NAME = "graphs-blue";
+const FRAMERATE_GRAPH_COLOR_NAME = "graphs-green";
+const MEMORY_GRAPH_COLOR_NAME = "graphs-blue";
+
+/**
+ * For timeline overview
+ */
+// px
+const MARKERS_GRAPH_HEADER_HEIGHT = 14;
+// px
+const MARKERS_GRAPH_ROW_HEIGHT = 10;
+// px
+const MARKERS_GROUP_VERTICAL_PADDING = 4;
+
+/**
+ * For optimization graph
+ */
+const OPTIMIZATIONS_GRAPH_RESOLUTION = 100;
+
+/**
+ * A base class for performance graphs to inherit from.
+ *
+ * @param nsIDOMNode parent
+ * The parent node holding the overview.
+ * @param string metric
+ * The unit of measurement for this graph.
+ */
+function PerformanceGraph(parent, metric) {
+ LineGraphWidget.call(this, parent, { metric });
+ this.setTheme();
+}
+
+PerformanceGraph.prototype = Heritage.extend(LineGraphWidget.prototype, {
+ strokeWidth: STROKE_WIDTH,
+ dampenValuesFactor: DAMPEN_VALUES,
+ fixedHeight: HEIGHT,
+ clipheadLineColor: CLIPHEAD_LINE_COLOR,
+ selectionLineColor: SELECTION_LINE_COLOR,
+ withTooltipArrows: false,
+ withFixedTooltipPositions: true,
+
+ /**
+ * Disables selection and empties this graph.
+ */
+ clearView: function () {
+ this.selectionEnabled = false;
+ this.dropSelection();
+ this.setData([]);
+ },
+
+ /**
+ * Sets the theme via `theme` to either "light" or "dark",
+ * and updates the internal styling to match. Requires a redraw
+ * to see the effects.
+ */
+ setTheme: function (theme) {
+ theme = theme || "light";
+ let mainColor = getColor(this.mainColor || "graphs-blue", theme);
+ this.backgroundColor = getColor("body-background", theme);
+ this.strokeColor = mainColor;
+ this.backgroundGradientStart = colorUtils.setAlpha(mainColor, 0.2);
+ this.backgroundGradientEnd = colorUtils.setAlpha(mainColor, 0.2);
+ this.selectionBackgroundColor = colorUtils.setAlpha(
+ getColor(SELECTION_BACKGROUND_COLOR_NAME, theme), 0.25);
+ this.selectionStripesColor = "rgba(255, 255, 255, 0.1)";
+ this.maximumLineColor = colorUtils.setAlpha(mainColor, 0.4);
+ this.averageLineColor = colorUtils.setAlpha(mainColor, 0.7);
+ this.minimumLineColor = colorUtils.setAlpha(mainColor, 0.9);
+ }
+});
+
+/**
+ * Constructor for the framerate graph. Inherits from PerformanceGraph.
+ *
+ * @param nsIDOMNode parent
+ * The parent node holding the overview.
+ */
+function FramerateGraph(parent) {
+ PerformanceGraph.call(this, parent, ProfilerGlobal.L10N.getStr("graphs.fps"));
+}
+
+FramerateGraph.prototype = Heritage.extend(PerformanceGraph.prototype, {
+ mainColor: FRAMERATE_GRAPH_COLOR_NAME,
+ setPerformanceData: function ({ duration, ticks }, resolution) {
+ this.dataDuration = duration;
+ return this.setDataFromTimestamps(ticks, resolution, duration);
+ }
+});
+
+/**
+ * Constructor for the memory graph. Inherits from PerformanceGraph.
+ *
+ * @param nsIDOMNode parent
+ * The parent node holding the overview.
+ */
+function MemoryGraph(parent) {
+ PerformanceGraph.call(this, parent, ProfilerGlobal.L10N.getStr("graphs.memory"));
+}
+
+MemoryGraph.prototype = Heritage.extend(PerformanceGraph.prototype, {
+ mainColor: MEMORY_GRAPH_COLOR_NAME,
+ setPerformanceData: function ({ duration, memory }) {
+ this.dataDuration = duration;
+ return this.setData(memory);
+ }
+});
+
+function TimelineGraph(parent, filter) {
+ MarkersOverview.call(this, parent, filter);
+}
+
+TimelineGraph.prototype = Heritage.extend(MarkersOverview.prototype, {
+ headerHeight: MARKERS_GRAPH_HEADER_HEIGHT,
+ rowHeight: MARKERS_GRAPH_ROW_HEIGHT,
+ groupPadding: MARKERS_GROUP_VERTICAL_PADDING,
+ setPerformanceData: MarkersOverview.prototype.setData
+});
+
+/**
+ * Definitions file for GraphsController, indicating the constructor,
+ * selector and other meta for each of the graphs controller by
+ * GraphsController.
+ */
+const GRAPH_DEFINITIONS = {
+ memory: {
+ constructor: MemoryGraph,
+ selector: "#memory-overview",
+ },
+ framerate: {
+ constructor: FramerateGraph,
+ selector: "#time-framerate",
+ },
+ timeline: {
+ constructor: TimelineGraph,
+ selector: "#markers-overview",
+ primaryLink: true
+ }
+};
+
+/**
+ * A controller for orchestrating the performance's tool overview graphs. Constructs,
+ * syncs, toggles displays and defines the memory, framerate and timeline view.
+ *
+ * @param {object} definition
+ * @param {DOMElement} root
+ * @param {function} getFilter
+ * @param {function} getTheme
+ */
+function GraphsController({ definition, root, getFilter, getTheme }) {
+ this._graphs = {};
+ this._enabled = new Set();
+ this._definition = definition || GRAPH_DEFINITIONS;
+ this._root = root;
+ this._getFilter = getFilter;
+ this._getTheme = getTheme;
+ this._primaryLink = Object.keys(this._definition)
+ .filter(name => this._definition[name].primaryLink)[0];
+ this.$ = root.ownerDocument.querySelector.bind(root.ownerDocument);
+
+ EventEmitter.decorate(this);
+ this._onSelecting = this._onSelecting.bind(this);
+}
+
+GraphsController.prototype = {
+
+ /**
+ * Returns the corresponding graph by `graphName`.
+ */
+ get: function (graphName) {
+ return this._graphs[graphName];
+ },
+
+ /**
+ * Iterates through all graphs and renders the data
+ * from a RecordingModel. Takes a resolution value used in
+ * some graphs.
+ * Saves rendering progress as a promise to be consumed by `destroy`,
+ * to wait for cleaning up rendering during destruction.
+ */
+ render: Task.async(function* (recordingData, resolution) {
+ // Get the previous render promise so we don't start rendering
+ // until the previous render cycle completes, which can occur
+ // especially when a recording is finished, and triggers a
+ // fresh rendering at a higher rate
+ yield (this._rendering && this._rendering.promise);
+
+ // Check after yielding to ensure we're not tearing down,
+ // as this can create a race condition in tests
+ if (this._destroyed) {
+ return;
+ }
+
+ this._rendering = promise.defer();
+ for (let graph of (yield this._getEnabled())) {
+ yield graph.setPerformanceData(recordingData, resolution);
+ this.emit("rendered", graph.graphName);
+ }
+ this._rendering.resolve();
+ }),
+
+ /**
+ * Destroys the underlying graphs.
+ */
+ destroy: Task.async(function* () {
+ let primary = this._getPrimaryLink();
+
+ this._destroyed = true;
+
+ if (primary) {
+ primary.off("selecting", this._onSelecting);
+ }
+
+ // If there was rendering, wait until the most recent render cycle
+ // has finished
+ if (this._rendering) {
+ yield this._rendering.promise;
+ }
+
+ for (let graph of this.getWidgets()) {
+ yield graph.destroy();
+ }
+ }),
+
+ /**
+ * Applies the theme to the underlying graphs. Optionally takes
+ * a `redraw` boolean in the options to force redraw.
+ */
+ setTheme: function (options = {}) {
+ let theme = options.theme || this._getTheme();
+ for (let graph of this.getWidgets()) {
+ graph.setTheme(theme);
+ graph.refresh({ force: options.redraw });
+ }
+ },
+
+ /**
+ * Sets up the graph, if needed. Returns a promise resolving
+ * to the graph if it is enabled once it's ready, or otherwise returns
+ * null if disabled.
+ */
+ isAvailable: Task.async(function* (graphName) {
+ if (!this._enabled.has(graphName)) {
+ return null;
+ }
+
+ let graph = this.get(graphName);
+
+ if (!graph) {
+ graph = yield this._construct(graphName);
+ }
+
+ yield graph.ready();
+ return graph;
+ }),
+
+ /**
+ * Enable or disable a subgraph controlled by GraphsController.
+ * This determines what graphs are visible and get rendered.
+ */
+ enable: function (graphName, isEnabled) {
+ let el = this.$(this._definition[graphName].selector);
+ el.classList[isEnabled ? "remove" : "add"]("hidden");
+
+ // If no status change, just return
+ if (this._enabled.has(graphName) === isEnabled) {
+ return;
+ }
+ if (isEnabled) {
+ this._enabled.add(graphName);
+ } else {
+ this._enabled.delete(graphName);
+ }
+
+ // Invalidate our cache of ready-to-go graphs
+ this._enabledGraphs = null;
+ },
+
+ /**
+ * Disables all graphs controller by the GraphsController, and
+ * also hides the root element. This is a one way switch, and used
+ * when older platforms do not have any timeline data.
+ */
+ disableAll: function () {
+ this._root.classList.add("hidden");
+ // Hide all the subelements
+ Object.keys(this._definition).forEach(graphName => this.enable(graphName, false));
+ },
+
+ /**
+ * Sets a mapped selection on the graph that is the main controller
+ * for keeping the graphs' selections in sync.
+ */
+ setMappedSelection: function (selection, { mapStart, mapEnd }) {
+ return this._getPrimaryLink().setMappedSelection(selection, { mapStart, mapEnd });
+ },
+
+ /**
+ * Fetches the currently mapped selection. If graphs are not yet rendered,
+ * (which throws in Graphs.js), return null.
+ */
+ getMappedSelection: function ({ mapStart, mapEnd }) {
+ let primary = this._getPrimaryLink();
+ if (primary && primary.hasData()) {
+ return primary.getMappedSelection({ mapStart, mapEnd });
+ }
+ return null;
+ },
+
+ /**
+ * Returns an array of graphs that have been created, not necessarily
+ * enabled currently.
+ */
+ getWidgets: function () {
+ return Object.keys(this._graphs).map(name => this._graphs[name]);
+ },
+
+ /**
+ * Drops the selection.
+ */
+ dropSelection: function () {
+ if (this._getPrimaryLink()) {
+ return this._getPrimaryLink().dropSelection();
+ }
+ return null;
+ },
+
+ /**
+ * Makes sure the selection is enabled or disabled in all the graphs.
+ */
+ selectionEnabled: Task.async(function* (enabled) {
+ for (let graph of (yield this._getEnabled())) {
+ graph.selectionEnabled = enabled;
+ }
+ }),
+
+ /**
+ * Creates the graph `graphName` and initializes it.
+ */
+ _construct: Task.async(function* (graphName) {
+ let def = this._definition[graphName];
+ let el = this.$(def.selector);
+ let filter = this._getFilter();
+ let graph = this._graphs[graphName] = new def.constructor(el, filter);
+ graph.graphName = graphName;
+
+ yield graph.ready();
+
+ // Sync the graphs' animations and selections together
+ if (def.primaryLink) {
+ graph.on("selecting", this._onSelecting);
+ } else {
+ CanvasGraphUtils.linkAnimation(this._getPrimaryLink(), graph);
+ CanvasGraphUtils.linkSelection(this._getPrimaryLink(), graph);
+ }
+
+ // Sets the container element's visibility based off of enabled status
+ el.classList[this._enabled.has(graphName) ? "remove" : "add"]("hidden");
+
+ this.setTheme();
+ return graph;
+ }),
+
+ /**
+ * Returns the main graph for this collection, that all graphs
+ * are bound to for syncing and selection.
+ */
+ _getPrimaryLink: function () {
+ return this.get(this._primaryLink);
+ },
+
+ /**
+ * Emitted when a selection occurs.
+ */
+ _onSelecting: function () {
+ this.emit("selecting");
+ },
+
+ /**
+ * Resolves to an array with all graphs that are enabled, and
+ * creates them if needed. Different than just iterating over `this._graphs`,
+ * as those could be enabled. Uses caching, as rendering happens many times per second,
+ * compared to how often which graphs/features are changed (rarely).
+ */
+ _getEnabled: Task.async(function* () {
+ if (this._enabledGraphs) {
+ return this._enabledGraphs;
+ }
+ let enabled = [];
+ for (let graphName of this._enabled) {
+ let graph = yield this.isAvailable(graphName);
+ if (graph) {
+ enabled.push(graph);
+ }
+ }
+ this._enabledGraphs = enabled;
+ return this._enabledGraphs;
+ }),
+};
+
+/**
+ * A base class for performance graphs to inherit from.
+ *
+ * @param nsIDOMNode parent
+ * The parent node holding the overview.
+ * @param string metric
+ * The unit of measurement for this graph.
+ */
+function OptimizationsGraph(parent) {
+ MountainGraphWidget.call(this, parent);
+ this.setTheme();
+}
+
+OptimizationsGraph.prototype = Heritage.extend(MountainGraphWidget.prototype, {
+
+ render: Task.async(function* (threadNode, frameNode) {
+ // Regardless if we draw or clear the graph, wait
+ // until it's ready.
+ yield this.ready();
+
+ if (!threadNode || !frameNode) {
+ this.setData([]);
+ return;
+ }
+
+ let { sampleTimes } = threadNode;
+
+ if (!sampleTimes.length) {
+ this.setData([]);
+ return;
+ }
+
+ // Take startTime/endTime from samples recorded, rather than
+ // using duration directly from threadNode, as the first sample that
+ // equals the startTime does not get recorded.
+ let startTime = sampleTimes[0];
+ let endTime = sampleTimes[sampleTimes.length - 1];
+
+ let bucketSize = (endTime - startTime) / OPTIMIZATIONS_GRAPH_RESOLUTION;
+ let data = createTierGraphDataFromFrameNode(frameNode, sampleTimes, bucketSize);
+
+ // If for some reason we don't have data (like the frameNode doesn't
+ // have optimizations, but it shouldn't be at this point if it doesn't),
+ // log an error.
+ if (!data) {
+ console.error(
+ `FrameNode#${frameNode.location} does not have optimizations data to render.`);
+ return;
+ }
+
+ this.dataOffsetX = startTime;
+ yield this.setData(data);
+ }),
+
+ /**
+ * Sets the theme via `theme` to either "light" or "dark",
+ * and updates the internal styling to match. Requires a redraw
+ * to see the effects.
+ */
+ setTheme: function (theme) {
+ theme = theme || "light";
+
+ let interpreterColor = getColor("graphs-red", theme);
+ let baselineColor = getColor("graphs-blue", theme);
+ let ionColor = getColor("graphs-green", theme);
+
+ this.format = [
+ { color: interpreterColor },
+ { color: baselineColor },
+ { color: ionColor },
+ ];
+
+ this.backgroundColor = getColor("sidebar-background", theme);
+ }
+});
+
+exports.OptimizationsGraph = OptimizationsGraph;
+exports.FramerateGraph = FramerateGraph;
+exports.MemoryGraph = MemoryGraph;
+exports.TimelineGraph = TimelineGraph;
+exports.GraphsController = GraphsController;
diff --git a/devtools/client/performance/modules/widgets/marker-details.js b/devtools/client/performance/modules/widgets/marker-details.js
new file mode 100644
index 000000000..56494e7c2
--- /dev/null
+++ b/devtools/client/performance/modules/widgets/marker-details.js
@@ -0,0 +1,164 @@
+/* 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";
+
+/**
+ * This file contains the rendering code for the marker sidebar.
+ */
+
+const EventEmitter = require("devtools/shared/event-emitter");
+const { MarkerDOMUtils } = require("devtools/client/performance/modules/marker-dom-utils");
+
+/**
+ * A detailed view for one single marker.
+ *
+ * @param nsIDOMNode parent
+ * The parent node holding the view.
+ * @param nsIDOMNode splitter
+ * The splitter node that the resize event is bound to.
+ */
+function MarkerDetails(parent, splitter) {
+ EventEmitter.decorate(this);
+
+ this._document = parent.ownerDocument;
+ this._parent = parent;
+ this._splitter = splitter;
+
+ this._onClick = this._onClick.bind(this);
+ this._onSplitterMouseUp = this._onSplitterMouseUp.bind(this);
+
+ this._parent.addEventListener("click", this._onClick);
+ this._splitter.addEventListener("mouseup", this._onSplitterMouseUp);
+
+ this.hidden = true;
+}
+
+MarkerDetails.prototype = {
+ /**
+ * Sets this view's width.
+ * @param number
+ */
+ set width(value) {
+ this._parent.setAttribute("width", value);
+ },
+
+ /**
+ * Sets this view's width.
+ * @return number
+ */
+ get width() {
+ return +this._parent.getAttribute("width");
+ },
+
+ /**
+ * Sets this view's visibility.
+ * @param boolean
+ */
+ set hidden(value) {
+ if (this._parent.hidden != value) {
+ this._parent.hidden = value;
+ this.emit("resize");
+ }
+ },
+
+ /**
+ * Gets this view's visibility.
+ * @param boolean
+ */
+ get hidden() {
+ return this._parent.hidden;
+ },
+
+ /**
+ * Clears the marker details from this view.
+ */
+ empty: function () {
+ this._parent.innerHTML = "";
+ },
+
+ /**
+ * Populates view with marker's details.
+ *
+ * @param object params
+ * An options object holding:
+ * - marker: The marker to display.
+ * - frames: Array of stack frame information; see stack.js.
+ * - allocations: Whether or not allocations were enabled for this
+ * recording. [optional]
+ */
+ render: function (options) {
+ let { marker, frames } = options;
+ this.empty();
+
+ let elements = [];
+ elements.push(MarkerDOMUtils.buildTitle(this._document, marker));
+ elements.push(MarkerDOMUtils.buildDuration(this._document, marker));
+ MarkerDOMUtils.buildFields(this._document, marker).forEach(f => elements.push(f));
+ MarkerDOMUtils.buildCustom(this._document, marker, options)
+ .forEach(f => elements.push(f));
+
+ // Build a stack element -- and use the "startStack" label if
+ // we have both a startStack and endStack.
+ if (marker.stack) {
+ let type = marker.endStack ? "startStack" : "stack";
+ elements.push(MarkerDOMUtils.buildStackTrace(this._document, {
+ frameIndex: marker.stack, frames, type
+ }));
+ }
+ if (marker.endStack) {
+ let type = "endStack";
+ elements.push(MarkerDOMUtils.buildStackTrace(this._document, {
+ frameIndex: marker.endStack, frames, type
+ }));
+ }
+
+ elements.forEach(el => this._parent.appendChild(el));
+ },
+
+ /**
+ * Handles click in the marker details view. Based on the target,
+ * can handle different actions -- only supporting view source links
+ * for the moment.
+ */
+ _onClick: function (e) {
+ let data = findActionFromEvent(e.target, this._parent);
+ if (!data) {
+ return;
+ }
+
+ this.emit(data.action, data);
+ },
+
+ /**
+ * Handles the "mouseup" event on the marker details view splitter.
+ */
+ _onSplitterMouseUp: function () {
+ this.emit("resize");
+ }
+};
+
+/**
+ * Take an element from an event `target`, and ascend through
+ * the DOM, looking for an element with a `data-action` attribute. Return
+ * the parsed `data-action` value found, or null if none found before
+ * reaching the parent `container`.
+ *
+ * @param {Element} target
+ * @param {Element} container
+ * @return {?object}
+ */
+function findActionFromEvent(target, container) {
+ let el = target;
+ let action;
+ while (el !== container) {
+ action = el.getAttribute("data-action");
+ if (action) {
+ return JSON.parse(action);
+ }
+ el = el.parentNode;
+ }
+ return null;
+}
+
+exports.MarkerDetails = MarkerDetails;
diff --git a/devtools/client/performance/modules/widgets/markers-overview.js b/devtools/client/performance/modules/widgets/markers-overview.js
new file mode 100644
index 000000000..89bc79a8d
--- /dev/null
+++ b/devtools/client/performance/modules/widgets/markers-overview.js
@@ -0,0 +1,243 @@
+/* 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";
+
+/**
+ * This file contains the "markers overview" graph, which is a minimap of all
+ * the timeline data. Regions inside it may be selected, determining which
+ * markers are visible in the "waterfall".
+ */
+
+const { Heritage } = require("devtools/client/shared/widgets/view-helpers");
+const { AbstractCanvasGraph } = require("devtools/client/shared/widgets/Graphs");
+
+const { colorUtils } = require("devtools/shared/css/color");
+const { getColor } = require("devtools/client/shared/theme");
+const ProfilerGlobal = require("devtools/client/performance/modules/global");
+const { MarkerBlueprintUtils } = require("devtools/client/performance/modules/marker-blueprint-utils");
+const { TickUtils } = require("devtools/client/performance/modules/waterfall-ticks");
+const { TIMELINE_BLUEPRINT } = require("devtools/client/performance/modules/markers");
+
+// px
+const OVERVIEW_HEADER_HEIGHT = 14;
+// px
+const OVERVIEW_ROW_HEIGHT = 11;
+
+const OVERVIEW_SELECTION_LINE_COLOR = "#666";
+const OVERVIEW_CLIPHEAD_LINE_COLOR = "#555";
+
+// ms
+const OVERVIEW_HEADER_TICKS_MULTIPLE = 100;
+// px
+const OVERVIEW_HEADER_TICKS_SPACING_MIN = 75;
+// px
+const OVERVIEW_HEADER_TEXT_FONT_SIZE = 9;
+const OVERVIEW_HEADER_TEXT_FONT_FAMILY = "sans-serif";
+// px
+const OVERVIEW_HEADER_TEXT_PADDING_LEFT = 6;
+// px
+const OVERVIEW_HEADER_TEXT_PADDING_TOP = 1;
+// px
+const OVERVIEW_MARKER_WIDTH_MIN = 4;
+// px
+const OVERVIEW_GROUP_VERTICAL_PADDING = 5;
+
+/**
+ * An overview for the markers data.
+ *
+ * @param nsIDOMNode parent
+ * The parent node holding the overview.
+ * @param Array<String> filter
+ * List of names of marker types that should not be shown.
+ */
+function MarkersOverview(parent, filter = [], ...args) {
+ AbstractCanvasGraph.apply(this, [parent, "markers-overview", ...args]);
+ this.setTheme();
+ this.setFilter(filter);
+}
+
+MarkersOverview.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
+ clipheadLineColor: OVERVIEW_CLIPHEAD_LINE_COLOR,
+ selectionLineColor: OVERVIEW_SELECTION_LINE_COLOR,
+ headerHeight: OVERVIEW_HEADER_HEIGHT,
+ rowHeight: OVERVIEW_ROW_HEIGHT,
+ groupPadding: OVERVIEW_GROUP_VERTICAL_PADDING,
+
+ /**
+ * Compute the height of the overview.
+ */
+ get fixedHeight() {
+ return this.headerHeight + this.rowHeight * this._numberOfGroups;
+ },
+
+ /**
+ * List of marker types that should not be shown in the graph.
+ */
+ setFilter: function (filter) {
+ this._paintBatches = new Map();
+ this._filter = filter;
+ this._groupMap = Object.create(null);
+
+ let observedGroups = new Set();
+
+ for (let type in TIMELINE_BLUEPRINT) {
+ if (filter.indexOf(type) !== -1) {
+ continue;
+ }
+ this._paintBatches.set(type, { definition: TIMELINE_BLUEPRINT[type], batch: [] });
+ observedGroups.add(TIMELINE_BLUEPRINT[type].group);
+ }
+
+ // Take our set of observed groups and order them and map
+ // the group numbers to fill in the holes via `_groupMap`.
+ // This normalizes our rows by removing rows that aren't used
+ // if filters are enabled.
+ let actualPosition = 0;
+ for (let groupNumber of Array.from(observedGroups).sort()) {
+ this._groupMap[groupNumber] = actualPosition++;
+ }
+ this._numberOfGroups = Object.keys(this._groupMap).length;
+ },
+
+ /**
+ * Disables selection and empties this graph.
+ */
+ clearView: function () {
+ this.selectionEnabled = false;
+ this.dropSelection();
+ this.setData({ duration: 0, markers: [] });
+ },
+
+ /**
+ * Renders the graph's data source.
+ * @see AbstractCanvasGraph.prototype.buildGraphImage
+ */
+ buildGraphImage: function () {
+ let { markers, duration } = this._data;
+
+ let { canvas, ctx } = this._getNamedCanvas("markers-overview-data");
+ let canvasWidth = this._width;
+ let canvasHeight = this._height;
+
+ // Group markers into separate paint batches. This is necessary to
+ // draw all markers sharing the same style at once.
+ for (let marker of markers) {
+ // Again skip over markers that we're filtering -- we don't want them
+ // to be labeled as "Unknown"
+ if (!MarkerBlueprintUtils.shouldDisplayMarker(marker, this._filter)) {
+ continue;
+ }
+
+ let markerType = this._paintBatches.get(marker.name) ||
+ this._paintBatches.get("UNKNOWN");
+ markerType.batch.push(marker);
+ }
+
+ // Calculate each row's height, and the time-based scaling.
+
+ let groupHeight = this.rowHeight * this._pixelRatio;
+ let groupPadding = this.groupPadding * this._pixelRatio;
+ let headerHeight = this.headerHeight * this._pixelRatio;
+ let dataScale = this.dataScaleX = canvasWidth / duration;
+
+ // Draw the header and overview background.
+
+ ctx.fillStyle = this.headerBackgroundColor;
+ ctx.fillRect(0, 0, canvasWidth, headerHeight);
+
+ ctx.fillStyle = this.backgroundColor;
+ ctx.fillRect(0, headerHeight, canvasWidth, canvasHeight);
+
+ // Draw the alternating odd/even group backgrounds.
+
+ ctx.fillStyle = this.alternatingBackgroundColor;
+ ctx.beginPath();
+
+ for (let i = 0; i < this._numberOfGroups; i += 2) {
+ let top = headerHeight + i * groupHeight;
+ ctx.rect(0, top, canvasWidth, groupHeight);
+ }
+
+ ctx.fill();
+
+ // Draw the timeline header ticks.
+
+ let fontSize = OVERVIEW_HEADER_TEXT_FONT_SIZE * this._pixelRatio;
+ let fontFamily = OVERVIEW_HEADER_TEXT_FONT_FAMILY;
+ let textPaddingLeft = OVERVIEW_HEADER_TEXT_PADDING_LEFT * this._pixelRatio;
+ let textPaddingTop = OVERVIEW_HEADER_TEXT_PADDING_TOP * this._pixelRatio;
+
+ let tickInterval = TickUtils.findOptimalTickInterval({
+ ticksMultiple: OVERVIEW_HEADER_TICKS_MULTIPLE,
+ ticksSpacingMin: OVERVIEW_HEADER_TICKS_SPACING_MIN,
+ dataScale: dataScale
+ });
+
+ ctx.textBaseline = "middle";
+ ctx.font = fontSize + "px " + fontFamily;
+ ctx.fillStyle = this.headerTextColor;
+ ctx.strokeStyle = this.headerTimelineStrokeColor;
+ ctx.beginPath();
+
+ for (let x = 0; x < canvasWidth; x += tickInterval) {
+ let lineLeft = x;
+ let textLeft = lineLeft + textPaddingLeft;
+ let time = Math.round(x / dataScale);
+ let label = ProfilerGlobal.L10N.getFormatStr("timeline.tick", time);
+ ctx.fillText(label, textLeft, headerHeight / 2 + textPaddingTop);
+ ctx.moveTo(lineLeft, 0);
+ ctx.lineTo(lineLeft, canvasHeight);
+ }
+
+ ctx.stroke();
+
+ // Draw the timeline markers.
+
+ for (let [, { definition, batch }] of this._paintBatches) {
+ let group = this._groupMap[definition.group];
+ let top = headerHeight + group * groupHeight + groupPadding / 2;
+ let height = groupHeight - groupPadding;
+
+ let color = getColor(definition.colorName, this.theme);
+ ctx.fillStyle = color;
+ ctx.beginPath();
+
+ for (let { start, end } of batch) {
+ let left = start * dataScale;
+ let width = Math.max((end - start) * dataScale, OVERVIEW_MARKER_WIDTH_MIN);
+ ctx.rect(left, top, width, height);
+ }
+
+ ctx.fill();
+
+ // Since all the markers in this batch (thus sharing the same style) have
+ // been drawn, empty it. The next time new markers will be available,
+ // they will be sorted and drawn again.
+ batch.length = 0;
+ }
+
+ return canvas;
+ },
+
+ /**
+ * Sets the theme via `theme` to either "light" or "dark",
+ * and updates the internal styling to match. Requires a redraw
+ * to see the effects.
+ */
+ setTheme: function (theme) {
+ this.theme = theme = theme || "light";
+ this.backgroundColor = getColor("body-background", theme);
+ this.selectionBackgroundColor = colorUtils.setAlpha(
+ getColor("selection-background", theme), 0.25);
+ this.selectionStripesColor = colorUtils.setAlpha("#fff", 0.1);
+ this.headerBackgroundColor = getColor("body-background", theme);
+ this.headerTextColor = getColor("body-color", theme);
+ this.headerTimelineStrokeColor = colorUtils.setAlpha(
+ getColor("body-color-alt", theme), 0.25);
+ this.alternatingBackgroundColor = colorUtils.setAlpha(
+ getColor("body-color", theme), 0.05);
+ }
+});
+
+exports.MarkersOverview = MarkersOverview;
diff --git a/devtools/client/performance/modules/widgets/moz.build b/devtools/client/performance/modules/widgets/moz.build
new file mode 100644
index 000000000..9f733838a
--- /dev/null
+++ b/devtools/client/performance/modules/widgets/moz.build
@@ -0,0 +1,11 @@
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'graphs.js',
+ 'marker-details.js',
+ 'markers-overview.js',
+ 'tree-view.js',
+)
diff --git a/devtools/client/performance/modules/widgets/tree-view.js b/devtools/client/performance/modules/widgets/tree-view.js
new file mode 100644
index 000000000..d3d81fe3b
--- /dev/null
+++ b/devtools/client/performance/modules/widgets/tree-view.js
@@ -0,0 +1,406 @@
+/* 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";
+
+/**
+ * This file contains the tree view, displaying all the samples and frames
+ * received from the proviler in a tree-like structure.
+ */
+
+const { L10N } = require("devtools/client/performance/modules/global");
+const { Heritage } = require("devtools/client/shared/widgets/view-helpers");
+const { AbstractTreeItem } = require("resource://devtools/client/shared/widgets/AbstractTreeItem.jsm");
+
+const URL_LABEL_TOOLTIP = L10N.getStr("table.url.tooltiptext");
+const VIEW_OPTIMIZATIONS_TOOLTIP = L10N.getStr("table.view-optimizations.tooltiptext2");
+
+// px
+const CALL_TREE_INDENTATION = 16;
+
+// Used for rendering values in cells
+const FORMATTERS = {
+ TIME: (value) => L10N.getFormatStr("table.ms2", L10N.numberWithDecimals(value, 2)),
+ PERCENT: (value) => L10N.getFormatStr("table.percentage3",
+ L10N.numberWithDecimals(value, 2)),
+ NUMBER: (value) => value || 0,
+ BYTESIZE: (value) => L10N.getFormatStr("table.bytes", (value || 0))
+};
+
+/**
+ * Definitions for rendering cells. Triads of class name, property name from
+ * `frame.getInfo()`, and a formatter function.
+ */
+const CELLS = {
+ duration: ["duration", "totalDuration", FORMATTERS.TIME],
+ percentage: ["percentage", "totalPercentage", FORMATTERS.PERCENT],
+ selfDuration: ["self-duration", "selfDuration", FORMATTERS.TIME],
+ selfPercentage: ["self-percentage", "selfPercentage", FORMATTERS.PERCENT],
+ samples: ["samples", "samples", FORMATTERS.NUMBER],
+
+ selfSize: ["self-size", "selfSize", FORMATTERS.BYTESIZE],
+ selfSizePercentage: ["self-size-percentage", "selfSizePercentage", FORMATTERS.PERCENT],
+ selfCount: ["self-count", "selfCount", FORMATTERS.NUMBER],
+ selfCountPercentage: ["self-count-percentage", "selfCountPercentage",
+ FORMATTERS.PERCENT],
+ size: ["size", "totalSize", FORMATTERS.BYTESIZE],
+ sizePercentage: ["size-percentage", "totalSizePercentage", FORMATTERS.PERCENT],
+ count: ["count", "totalCount", FORMATTERS.NUMBER],
+ countPercentage: ["count-percentage", "totalCountPercentage", FORMATTERS.PERCENT],
+};
+const CELL_TYPES = Object.keys(CELLS);
+
+const DEFAULT_SORTING_PREDICATE = (frameA, frameB) => {
+ let dataA = frameA.getDisplayedData();
+ let dataB = frameB.getDisplayedData();
+ let isAllocations = "totalSize" in dataA;
+
+ if (isAllocations) {
+ if (this.inverted && dataA.selfSize !== dataB.selfSize) {
+ return dataA.selfSize < dataB.selfSize ? 1 : -1;
+ }
+ return dataA.totalSize < dataB.totalSize ? 1 : -1;
+ }
+
+ if (this.inverted && dataA.selfPercentage !== dataB.selfPercentage) {
+ return dataA.selfPercentage < dataB.selfPercentage ? 1 : -1;
+ }
+ return dataA.totalPercentage < dataB.totalPercentage ? 1 : -1;
+};
+
+// depth
+const DEFAULT_AUTO_EXPAND_DEPTH = 3;
+const DEFAULT_VISIBLE_CELLS = {
+ duration: true,
+ percentage: true,
+ selfDuration: true,
+ selfPercentage: true,
+ samples: true,
+ function: true,
+
+ // allocation columns
+ count: false,
+ selfCount: false,
+ size: false,
+ selfSize: false,
+ countPercentage: false,
+ selfCountPercentage: false,
+ sizePercentage: false,
+ selfSizePercentage: false,
+};
+
+/**
+ * An item in a call tree view, which looks like this:
+ *
+ * Time (ms) | Cost | Calls | Function
+ * ============================================================================
+ * 1,000.00 | 100.00% | | â–¼ (root)
+ * 500.12 | 50.01% | 300 | â–¼ foo Categ. 1
+ * 300.34 | 30.03% | 1500 | â–¼ bar Categ. 2
+ * 10.56 | 0.01% | 42 | â–¶ call_with_children Categ. 3
+ * 90.78 | 0.09% | 25 | call_without_children Categ. 4
+ *
+ * Every instance of a `CallView` represents a row in the call tree. The same
+ * parent node is used for all rows.
+ *
+ * @param CallView caller
+ * The CallView considered the "caller" frame. This newly created
+ * instance will be represent the "callee". Should be null for root nodes.
+ * @param ThreadNode | FrameNode frame
+ * Details about this function, like { samples, duration, calls } etc.
+ * @param number level [optional]
+ * The indentation level in the call tree. The root node is at level 0.
+ * @param boolean hidden [optional]
+ * Whether this node should be hidden and not contribute to depth/level
+ * calculations. Defaults to false.
+ * @param boolean inverted [optional]
+ * Whether the call tree has been inverted (bottom up, rather than
+ * top-down). Defaults to false.
+ * @param function sortingPredicate [optional]
+ * The predicate used to sort the tree items when created. Defaults to
+ * the caller's `sortingPredicate` if a caller exists, otherwise defaults
+ * to DEFAULT_SORTING_PREDICATE. The two passed arguments are FrameNodes.
+ * @param number autoExpandDepth [optional]
+ * The depth to which the tree should automatically expand. Defualts to
+ * the caller's `autoExpandDepth` if a caller exists, otherwise defaults
+ * to DEFAULT_AUTO_EXPAND_DEPTH.
+ * @param object visibleCells
+ * An object specifying which cells are visible in the tree. Defaults to
+ * the caller's `visibleCells` if a caller exists, otherwise defaults
+ * to DEFAULT_VISIBLE_CELLS.
+ * @param boolean showOptimizationHint [optional]
+ * Whether or not to show an icon indicating if the frame has optimization
+ * data.
+ */
+function CallView({
+ caller, frame, level, hidden, inverted,
+ sortingPredicate, autoExpandDepth, visibleCells,
+ showOptimizationHint
+}) {
+ AbstractTreeItem.call(this, {
+ parent: caller,
+ level: level | 0 - (hidden ? 1 : 0)
+ });
+
+ if (sortingPredicate != null) {
+ this.sortingPredicate = sortingPredicate;
+ } else if (caller) {
+ this.sortingPredicate = caller.sortingPredicate;
+ } else {
+ this.sortingPredicate = DEFAULT_SORTING_PREDICATE;
+ }
+
+ if (autoExpandDepth != null) {
+ this.autoExpandDepth = autoExpandDepth;
+ } else if (caller) {
+ this.autoExpandDepth = caller.autoExpandDepth;
+ } else {
+ this.autoExpandDepth = DEFAULT_AUTO_EXPAND_DEPTH;
+ }
+
+ if (visibleCells != null) {
+ this.visibleCells = visibleCells;
+ } else if (caller) {
+ this.visibleCells = caller.visibleCells;
+ } else {
+ this.visibleCells = Object.create(DEFAULT_VISIBLE_CELLS);
+ }
+
+ this.caller = caller;
+ this.frame = frame;
+ this.hidden = hidden;
+ this.inverted = inverted;
+ this.showOptimizationHint = showOptimizationHint;
+
+ this._onUrlClick = this._onUrlClick.bind(this);
+}
+
+CallView.prototype = Heritage.extend(AbstractTreeItem.prototype, {
+ /**
+ * Creates the view for this tree node.
+ * @param nsIDOMNode document
+ * @param nsIDOMNode arrowNode
+ * @return nsIDOMNode
+ */
+ _displaySelf: function (document, arrowNode) {
+ let frameInfo = this.getDisplayedData();
+ let cells = [];
+
+ for (let type of CELL_TYPES) {
+ if (this.visibleCells[type]) {
+ // Inline for speed, but pass in the formatted value via
+ // cell definition, as well as the element type.
+ cells.push(this._createCell(document, CELLS[type][2](frameInfo[CELLS[type][1]]),
+ CELLS[type][0]));
+ }
+ }
+
+ if (this.visibleCells.function) {
+ cells.push(this._createFunctionCell(document, arrowNode, frameInfo.name, frameInfo,
+ this.level));
+ }
+
+ let targetNode = document.createElement("hbox");
+ targetNode.className = "call-tree-item";
+ targetNode.setAttribute("origin", frameInfo.isContent ? "content" : "chrome");
+ targetNode.setAttribute("category", frameInfo.categoryData.abbrev || "");
+ targetNode.setAttribute("tooltiptext", frameInfo.tooltiptext);
+
+ if (this.hidden) {
+ targetNode.style.display = "none";
+ }
+
+ for (let i = 0; i < cells.length; i++) {
+ targetNode.appendChild(cells[i]);
+ }
+
+ return targetNode;
+ },
+
+ /**
+ * Populates this node in the call tree with the corresponding "callees".
+ * These are defined in the `frame` data source for this call view.
+ * @param array:AbstractTreeItem children
+ */
+ _populateSelf: function (children) {
+ let newLevel = this.level + 1;
+
+ for (let newFrame of this.frame.calls) {
+ children.push(new CallView({
+ caller: this,
+ frame: newFrame,
+ level: newLevel,
+ inverted: this.inverted
+ }));
+ }
+
+ // Sort the "callees" asc. by samples, before inserting them in the tree,
+ // if no other sorting predicate was specified on this on the root item.
+ children.sort(this.sortingPredicate.bind(this));
+ },
+
+ /**
+ * Functions creating each cell in this call view.
+ * Invoked by `_displaySelf`.
+ */
+ _createCell: function (doc, value, type) {
+ let cell = doc.createElement("description");
+ cell.className = "plain call-tree-cell";
+ cell.setAttribute("type", type);
+ cell.setAttribute("crop", "end");
+ // Add a tabulation to the cell text in case it's is selected and copied.
+ cell.textContent = value + "\t";
+ return cell;
+ },
+
+ _createFunctionCell: function (doc, arrowNode, frameName, frameInfo, frameLevel) {
+ let cell = doc.createElement("hbox");
+ cell.className = "call-tree-cell";
+ cell.style.marginInlineStart = (frameLevel * CALL_TREE_INDENTATION) + "px";
+ cell.setAttribute("type", "function");
+ cell.appendChild(arrowNode);
+
+ // Render optimization hint if this frame has opt data.
+ if (this.root.showOptimizationHint && frameInfo.hasOptimizations &&
+ !frameInfo.isMetaCategory) {
+ let icon = doc.createElement("description");
+ icon.setAttribute("tooltiptext", VIEW_OPTIMIZATIONS_TOOLTIP);
+ icon.className = "opt-icon";
+ cell.appendChild(icon);
+ }
+
+ // Don't render a name label node if there's no function name. A different
+ // location label node will be rendered instead.
+ if (frameName) {
+ let nameNode = doc.createElement("description");
+ nameNode.className = "plain call-tree-name";
+ nameNode.textContent = frameName;
+ cell.appendChild(nameNode);
+ }
+
+ // Don't render detailed labels for meta category frames
+ if (!frameInfo.isMetaCategory) {
+ this._appendFunctionDetailsCells(doc, cell, frameInfo);
+ }
+
+ // Don't render an expando-arrow for leaf nodes.
+ let hasDescendants = Object.keys(this.frame.calls).length > 0;
+ if (!hasDescendants) {
+ arrowNode.setAttribute("invisible", "");
+ }
+
+ // Add a line break to the last description of the row in case it's selected
+ // and copied.
+ let lastDescription = cell.querySelector("description:last-of-type");
+ lastDescription.textContent = lastDescription.textContent + "\n";
+
+ // Add spaces as frameLevel indicators in case the row is selected and
+ // copied. These spaces won't be displayed in the cell content.
+ let firstDescription = cell.querySelector("description:first-of-type");
+ let levelIndicator = frameLevel > 0 ? " ".repeat(frameLevel) : "";
+ firstDescription.textContent = levelIndicator + firstDescription.textContent;
+
+ return cell;
+ },
+
+ _appendFunctionDetailsCells: function (doc, cell, frameInfo) {
+ if (frameInfo.fileName) {
+ let urlNode = doc.createElement("description");
+ urlNode.className = "plain call-tree-url";
+ urlNode.textContent = frameInfo.fileName;
+ urlNode.setAttribute("tooltiptext", URL_LABEL_TOOLTIP + " → " + frameInfo.url);
+ urlNode.addEventListener("mousedown", this._onUrlClick);
+ cell.appendChild(urlNode);
+ }
+
+ if (frameInfo.line) {
+ let lineNode = doc.createElement("description");
+ lineNode.className = "plain call-tree-line";
+ lineNode.textContent = ":" + frameInfo.line;
+ cell.appendChild(lineNode);
+ }
+
+ if (frameInfo.column) {
+ let columnNode = doc.createElement("description");
+ columnNode.className = "plain call-tree-column";
+ columnNode.textContent = ":" + frameInfo.column;
+ cell.appendChild(columnNode);
+ }
+
+ if (frameInfo.host) {
+ let hostNode = doc.createElement("description");
+ hostNode.className = "plain call-tree-host";
+ hostNode.textContent = frameInfo.host;
+ cell.appendChild(hostNode);
+ }
+
+ if (frameInfo.categoryData.label) {
+ let categoryNode = doc.createElement("description");
+ categoryNode.className = "plain call-tree-category";
+ categoryNode.style.color = frameInfo.categoryData.color;
+ categoryNode.textContent = frameInfo.categoryData.label;
+ cell.appendChild(categoryNode);
+ }
+ },
+
+ /**
+ * Gets the data displayed about this tree item, based on the FrameNode
+ * model associated with this view.
+ *
+ * @return object
+ */
+ getDisplayedData: function () {
+ if (this._cachedDisplayedData) {
+ return this._cachedDisplayedData;
+ }
+
+ this._cachedDisplayedData = this.frame.getInfo({
+ root: this.root.frame,
+ allocations: (this.visibleCells.count || this.visibleCells.selfCount)
+ });
+
+ return this._cachedDisplayedData;
+
+ /**
+ * When inverting call tree, the costs and times are dependent on position
+ * in the tree. We must only count leaf nodes with self cost, and total costs
+ * dependent on how many times the leaf node was found with a full stack path.
+ *
+ * Total | Self | Calls | Function
+ * ============================================================================
+ * 100% | 100% | 100 | â–¼ C
+ * 50% | 0% | 50 | â–¼ B
+ * 50% | 0% | 50 | â–¼ A
+ * 50% | 0% | 50 | â–¼ B
+ *
+ * Every instance of a `CallView` represents a row in the call tree. The same
+ * container node is used for all rows.
+ */
+ },
+
+ /**
+ * Toggles the category information hidden or visible.
+ * @param boolean visible
+ */
+ toggleCategories: function (visible) {
+ if (!visible) {
+ this.container.setAttribute("categories-hidden", "");
+ } else {
+ this.container.removeAttribute("categories-hidden");
+ }
+ },
+
+ /**
+ * Handler for the "click" event on the url node of this call view.
+ */
+ _onUrlClick: function (e) {
+ e.preventDefault();
+ e.stopPropagation();
+ // Only emit for left click events
+ if (e.button === 0) {
+ this.root.emit("link", this);
+ }
+ },
+});
+
+exports.CallView = CallView;
diff --git a/devtools/client/performance/moz.build b/devtools/client/performance/moz.build
new file mode 100644
index 000000000..909d69d80
--- /dev/null
+++ b/devtools/client/performance/moz.build
@@ -0,0 +1,19 @@
+# vim: set filetype=python:
+# 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/.
+
+DIRS += [
+ 'components',
+ 'legacy',
+ 'modules',
+ 'test',
+]
+
+DevToolsModules(
+ 'events.js',
+ 'panel.js'
+)
+
+BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
+XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
diff --git a/devtools/client/performance/panel.js b/devtools/client/performance/panel.js
new file mode 100644
index 000000000..6fa9f37cf
--- /dev/null
+++ b/devtools/client/performance/panel.js
@@ -0,0 +1,100 @@
+/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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 { Task } = require("devtools/shared/task");
+
+loader.lazyRequireGetter(this, "promise");
+loader.lazyRequireGetter(this, "EventEmitter",
+ "devtools/shared/event-emitter");
+
+function PerformancePanel(iframeWindow, toolbox) {
+ this.panelWin = iframeWindow;
+ this.toolbox = toolbox;
+
+ EventEmitter.decorate(this);
+}
+
+exports.PerformancePanel = PerformancePanel;
+
+PerformancePanel.prototype = {
+ /**
+ * Open is effectively an asynchronous constructor.
+ *
+ * @return object
+ * A promise that is resolved when the Performance tool
+ * completes opening.
+ */
+ open: Task.async(function* () {
+ if (this._opening) {
+ return this._opening;
+ }
+ let deferred = promise.defer();
+ this._opening = deferred.promise;
+
+ this.panelWin.gToolbox = this.toolbox;
+ this.panelWin.gTarget = this.target;
+ this._checkRecordingStatus = this._checkRecordingStatus.bind(this);
+
+ // Actor is already created in the toolbox; reuse
+ // the same front, and the toolbox will also initialize the front,
+ // but redo it here so we can hook into the same event to prevent race conditions
+ // in the case of the front still being in the process of opening.
+ let front = yield this.panelWin.gToolbox.initPerformance();
+
+ // This should only happen if this is completely unsupported (when profiler
+ // does not exist), and in that case, the tool shouldn't be available,
+ // so let's ensure this assertion.
+ if (!front) {
+ console.error("No PerformanceFront found in toolbox.");
+ }
+
+ this.panelWin.gFront = front;
+ let { PerformanceController, EVENTS } = this.panelWin;
+ PerformanceController.on(EVENTS.RECORDING_ADDED, this._checkRecordingStatus);
+ PerformanceController.on(EVENTS.RECORDING_STATE_CHANGE, this._checkRecordingStatus);
+ yield this.panelWin.startupPerformance();
+
+ // Fire this once incase we have an in-progress recording (console profile)
+ // that caused this start up, and no state change yet, so we can highlight the
+ // tab if we need.
+ this._checkRecordingStatus();
+
+ this.isReady = true;
+ this.emit("ready");
+
+ deferred.resolve(this);
+ return this._opening;
+ }),
+
+ // DevToolPanel API
+
+ get target() {
+ return this.toolbox.target;
+ },
+
+ destroy: Task.async(function* () {
+ // Make sure this panel is not already destroyed.
+ if (this._destroyed) {
+ return;
+ }
+
+ let { PerformanceController, EVENTS } = this.panelWin;
+ PerformanceController.off(EVENTS.RECORDING_ADDED, this._checkRecordingStatus);
+ PerformanceController.off(EVENTS.RECORDING_STATE_CHANGE, this._checkRecordingStatus);
+ yield this.panelWin.shutdownPerformance();
+ this.emit("destroyed");
+ this._destroyed = true;
+ }),
+
+ _checkRecordingStatus: function () {
+ if (this.panelWin.PerformanceController.isRecording()) {
+ this.toolbox.highlightTool("performance");
+ } else {
+ this.toolbox.unhighlightTool("performance");
+ }
+ }
+};
diff --git a/devtools/client/performance/performance-controller.js b/devtools/client/performance/performance-controller.js
new file mode 100644
index 000000000..e47a0c401
--- /dev/null
+++ b/devtools/client/performance/performance-controller.js
@@ -0,0 +1,595 @@
+/* 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";
+
+/* globals window, document, PerformanceView, ToolbarView, RecordingsView, DetailsView */
+
+/* exported Cc, Ci, Cu, Cr, loader */
+var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+var BrowserLoaderModule = {};
+Cu.import("resource://devtools/client/shared/browser-loader.js", BrowserLoaderModule);
+var { loader, require } = BrowserLoaderModule.BrowserLoader({
+ baseURI: "resource://devtools/client/performance/",
+ window
+});
+var { Task } = require("devtools/shared/task");
+/* exported Heritage, ViewHelpers, WidgetMethods, setNamedTimeout, clearNamedTimeout */
+var { Heritage, ViewHelpers, WidgetMethods, setNamedTimeout, clearNamedTimeout } = require("devtools/client/shared/widgets/view-helpers");
+var { gDevTools } = require("devtools/client/framework/devtools");
+
+// Events emitted by various objects in the panel.
+var EVENTS = require("devtools/client/performance/events");
+Object.defineProperty(this, "EVENTS", {
+ value: EVENTS,
+ enumerable: true,
+ writable: false
+});
+
+/* exported React, ReactDOM, JITOptimizationsView, RecordingControls, RecordingButton,
+ RecordingList, RecordingListItem, Services, Waterfall, promise, EventEmitter,
+ DevToolsUtils, system */
+var React = require("devtools/client/shared/vendor/react");
+var ReactDOM = require("devtools/client/shared/vendor/react-dom");
+var Waterfall = React.createFactory(require("devtools/client/performance/components/waterfall"));
+var JITOptimizationsView = React.createFactory(require("devtools/client/performance/components/jit-optimizations"));
+var RecordingControls = React.createFactory(require("devtools/client/performance/components/recording-controls"));
+var RecordingButton = React.createFactory(require("devtools/client/performance/components/recording-button"));
+var RecordingList = React.createFactory(require("devtools/client/performance/components/recording-list"));
+var RecordingListItem = React.createFactory(require("devtools/client/performance/components/recording-list-item"));
+
+var Services = require("Services");
+var promise = require("promise");
+var EventEmitter = require("devtools/shared/event-emitter");
+var DevToolsUtils = require("devtools/shared/DevToolsUtils");
+var flags = require("devtools/shared/flags");
+var system = require("devtools/shared/system");
+
+// Logic modules
+/* exported L10N, PerformanceTelemetry, TIMELINE_BLUEPRINT, RecordingUtils,
+ PerformanceUtils, OptimizationsGraph, GraphsController,
+ MarkerDetails, MarkerBlueprintUtils, WaterfallUtils, FrameUtils, CallView, ThreadNode,
+ FrameNode */
+var { L10N } = require("devtools/client/performance/modules/global");
+var { PerformanceTelemetry } = require("devtools/client/performance/modules/logic/telemetry");
+var { TIMELINE_BLUEPRINT } = require("devtools/client/performance/modules/markers");
+var RecordingUtils = require("devtools/shared/performance/recording-utils");
+var PerformanceUtils = require("devtools/client/performance/modules/utils");
+var { OptimizationsGraph, GraphsController } = require("devtools/client/performance/modules/widgets/graphs");
+var { MarkerDetails } = require("devtools/client/performance/modules/widgets/marker-details");
+var { MarkerBlueprintUtils } = require("devtools/client/performance/modules/marker-blueprint-utils");
+var WaterfallUtils = require("devtools/client/performance/modules/logic/waterfall-utils");
+var FrameUtils = require("devtools/client/performance/modules/logic/frame-utils");
+var { CallView } = require("devtools/client/performance/modules/widgets/tree-view");
+var { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
+var { FrameNode } = require("devtools/client/performance/modules/logic/tree-model");
+
+// Widgets modules
+
+/* exported OptionsView, FlameGraph, FlameGraphUtils, TreeWidget, SideMenuWidget */
+var { OptionsView } = require("devtools/client/shared/options-view");
+var { FlameGraph, FlameGraphUtils } = require("devtools/client/shared/widgets/FlameGraph");
+var { TreeWidget } = require("devtools/client/shared/widgets/TreeWidget");
+var { SideMenuWidget } = require("resource://devtools/client/shared/widgets/SideMenuWidget.jsm");
+
+/* exported BRANCH_NAME */
+var BRANCH_NAME = "devtools.performance.ui.";
+
+/**
+ * The current target, toolbox and PerformanceFront, set by this tool's host.
+ */
+/* exported gToolbox, gTarget, gFront */
+var gToolbox, gTarget, gFront;
+
+/* exported startupPerformance, shutdownPerformance, PerformanceController */
+
+/**
+ * Initializes the profiler controller and views.
+ */
+var startupPerformance = Task.async(function* () {
+ yield PerformanceController.initialize();
+ yield PerformanceView.initialize();
+ PerformanceController.enableFrontEventListeners();
+});
+
+/**
+ * Destroys the profiler controller and views.
+ */
+var shutdownPerformance = Task.async(function* () {
+ yield PerformanceController.destroy();
+ yield PerformanceView.destroy();
+ PerformanceController.disableFrontEventListeners();
+});
+
+/**
+ * Functions handling target-related lifetime events and
+ * UI interaction.
+ */
+var PerformanceController = {
+ _recordings: [],
+ _currentRecording: null,
+
+ /**
+ * Listen for events emitted by the current tab target and
+ * main UI events.
+ */
+ initialize: Task.async(function* () {
+ this._telemetry = new PerformanceTelemetry(this);
+ this.startRecording = this.startRecording.bind(this);
+ this.stopRecording = this.stopRecording.bind(this);
+ this.importRecording = this.importRecording.bind(this);
+ this.exportRecording = this.exportRecording.bind(this);
+ this.clearRecordings = this.clearRecordings.bind(this);
+ this._onRecordingSelectFromView = this._onRecordingSelectFromView.bind(this);
+ this._onPrefChanged = this._onPrefChanged.bind(this);
+ this._onThemeChanged = this._onThemeChanged.bind(this);
+ this._onFrontEvent = this._onFrontEvent.bind(this);
+ this._pipe = this._pipe.bind(this);
+
+ // Store data regarding if e10s is enabled.
+ this._e10s = Services.appinfo.browserTabsRemoteAutostart;
+ this._setMultiprocessAttributes();
+
+ this._prefs = require("devtools/client/performance/modules/global").PREFS;
+ this._prefs.registerObserver();
+ this._prefs.on("pref-changed", this._onPrefChanged);
+
+ ToolbarView.on(EVENTS.UI_PREF_CHANGED, this._onPrefChanged);
+ PerformanceView.on(EVENTS.UI_START_RECORDING, this.startRecording);
+ PerformanceView.on(EVENTS.UI_STOP_RECORDING, this.stopRecording);
+ PerformanceView.on(EVENTS.UI_IMPORT_RECORDING, this.importRecording);
+ PerformanceView.on(EVENTS.UI_CLEAR_RECORDINGS, this.clearRecordings);
+ RecordingsView.on(EVENTS.UI_EXPORT_RECORDING, this.exportRecording);
+ RecordingsView.on(EVENTS.UI_RECORDING_SELECTED, this._onRecordingSelectFromView);
+ DetailsView.on(EVENTS.UI_DETAILS_VIEW_SELECTED, this._pipe);
+
+ gDevTools.on("pref-changed", this._onThemeChanged);
+ }),
+
+ /**
+ * Remove events handled by the PerformanceController
+ */
+ destroy: function () {
+ this._telemetry.destroy();
+ this._prefs.off("pref-changed", this._onPrefChanged);
+ this._prefs.unregisterObserver();
+
+ ToolbarView.off(EVENTS.UI_PREF_CHANGED, this._onPrefChanged);
+ PerformanceView.off(EVENTS.UI_START_RECORDING, this.startRecording);
+ PerformanceView.off(EVENTS.UI_STOP_RECORDING, this.stopRecording);
+ PerformanceView.off(EVENTS.UI_IMPORT_RECORDING, this.importRecording);
+ PerformanceView.off(EVENTS.UI_CLEAR_RECORDINGS, this.clearRecordings);
+ RecordingsView.off(EVENTS.UI_EXPORT_RECORDING, this.exportRecording);
+ RecordingsView.off(EVENTS.UI_RECORDING_SELECTED, this._onRecordingSelectFromView);
+ DetailsView.off(EVENTS.UI_DETAILS_VIEW_SELECTED, this._pipe);
+
+ gDevTools.off("pref-changed", this._onThemeChanged);
+ },
+
+ /**
+ * Enables front event listeners.
+ *
+ * The rationale behind this is given by the async intialization of all the
+ * frontend components. Even though the panel is considered "open" only after
+ * both the controller and the view are created, and even though their
+ * initialization is sequential (controller, then view), the controller might
+ * start handling backend events before the view finishes if the event
+ * listeners are added too soon.
+ */
+ enableFrontEventListeners: function () {
+ gFront.on("*", this._onFrontEvent);
+ },
+
+ /**
+ * Disables front event listeners.
+ */
+ disableFrontEventListeners: function () {
+ gFront.off("*", this._onFrontEvent);
+ },
+
+ /**
+ * Returns the current devtools theme.
+ */
+ getTheme: function () {
+ return Services.prefs.getCharPref("devtools.theme");
+ },
+
+ /**
+ * Get a boolean preference setting from `prefName` via the underlying
+ * OptionsView in the ToolbarView. This preference is guaranteed to be
+ * displayed in the UI.
+ *
+ * @param string prefName
+ * @return boolean
+ */
+ getOption: function (prefName) {
+ return ToolbarView.optionsView.getPref(prefName);
+ },
+
+ /**
+ * Get a preference setting from `prefName`. This preference can be of
+ * any type and might not be displayed in the UI.
+ *
+ * @param string prefName
+ * @return any
+ */
+ getPref: function (prefName) {
+ return this._prefs[prefName];
+ },
+
+ /**
+ * Set a preference setting from `prefName`. This preference can be of
+ * any type and might not be displayed in the UI.
+ *
+ * @param string prefName
+ * @param any prefValue
+ */
+ setPref: function (prefName, prefValue) {
+ this._prefs[prefName] = prefValue;
+ },
+
+ /**
+ * Checks whether or not a new recording is supported by the PerformanceFront.
+ * @return Promise:boolean
+ */
+ canCurrentlyRecord: Task.async(function* () {
+ // If we're testing the legacy front, the performance actor will exist,
+ // with `canCurrentlyRecord` method; this ensures we test the legacy path.
+ if (gFront.LEGACY_FRONT) {
+ return true;
+ }
+ let hasActor = yield gTarget.hasActor("performance");
+ if (!hasActor) {
+ return true;
+ }
+ let actorCanCheck = yield gTarget.actorHasMethod("performance", "canCurrentlyRecord");
+ if (!actorCanCheck) {
+ return true;
+ }
+ return (yield gFront.canCurrentlyRecord()).success;
+ }),
+
+ /**
+ * Starts recording with the PerformanceFront.
+ */
+ startRecording: Task.async(function* () {
+ let options = {
+ withMarkers: true,
+ withTicks: this.getOption("enable-framerate"),
+ withMemory: this.getOption("enable-memory"),
+ withFrames: true,
+ withGCEvents: true,
+ withAllocations: this.getOption("enable-allocations"),
+ allocationsSampleProbability: this.getPref("memory-sample-probability"),
+ allocationsMaxLogLength: this.getPref("memory-max-log-length"),
+ bufferSize: this.getPref("profiler-buffer-size"),
+ sampleFrequency: this.getPref("profiler-sample-frequency")
+ };
+
+ let recordingStarted = yield gFront.startRecording(options);
+
+ // In some cases, like when the target has a private browsing tab,
+ // recording is not currently supported because of the profiler module.
+ // Present a notification in this case alerting the user of this issue.
+ if (!recordingStarted) {
+ this.emit(EVENTS.BACKEND_FAILED_AFTER_RECORDING_START);
+ PerformanceView.setState("unavailable");
+ } else {
+ this.emit(EVENTS.BACKEND_READY_AFTER_RECORDING_START);
+ }
+ }),
+
+ /**
+ * Stops recording with the PerformanceFront.
+ */
+ stopRecording: Task.async(function* () {
+ let recording = this.getLatestManualRecording();
+ yield gFront.stopRecording(recording);
+ this.emit(EVENTS.BACKEND_READY_AFTER_RECORDING_STOP);
+ }),
+
+ /**
+ * Saves the given recording to a file. Emits `EVENTS.RECORDING_EXPORTED`
+ * when the file was saved.
+ *
+ * @param PerformanceRecording recording
+ * The model that holds the recording data.
+ * @param nsILocalFile file
+ * The file to stream the data into.
+ */
+ exportRecording: Task.async(function* (_, recording, file) {
+ yield recording.exportRecording(file);
+ this.emit(EVENTS.RECORDING_EXPORTED, recording, file);
+ }),
+
+ /**
+ * Clears all completed recordings from the list as well as the current non-console
+ * recording. Emits `EVENTS.RECORDING_DELETED` when complete so other components can
+ * clean up.
+ */
+ clearRecordings: Task.async(function* () {
+ for (let i = this._recordings.length - 1; i >= 0; i--) {
+ let model = this._recordings[i];
+ if (!model.isConsole() && model.isRecording()) {
+ yield this.stopRecording();
+ }
+ // If last recording is not recording, but finalizing itself,
+ // wait for that to finish
+ if (!model.isRecording() && !model.isCompleted()) {
+ yield this.waitForStateChangeOnRecording(model, "recording-stopped");
+ }
+ // If recording is completed,
+ // clean it up from UI and remove it from the _recordings array.
+ if (model.isCompleted()) {
+ this.emit(EVENTS.RECORDING_DELETED, model);
+ this._recordings.splice(i, 1);
+ }
+ }
+ if (this._recordings.length > 0) {
+ if (!this._recordings.includes(this.getCurrentRecording())) {
+ this.setCurrentRecording(this._recordings[0]);
+ }
+ } else {
+ this.setCurrentRecording(null);
+ }
+ }),
+
+ /**
+ * Loads a recording from a file, adding it to the recordings list. Emits
+ * `EVENTS.RECORDING_IMPORTED` when the file was loaded.
+ *
+ * @param nsILocalFile file
+ * The file to import the data from.
+ */
+ importRecording: Task.async(function* (_, file) {
+ let recording = yield gFront.importRecording(file);
+ this._addRecordingIfUnknown(recording);
+
+ this.emit(EVENTS.RECORDING_IMPORTED, recording);
+ }),
+
+ /**
+ * Sets the currently active PerformanceRecording. Should rarely be called directly,
+ * as RecordingsView handles this when manually selected a recording item. Exceptions
+ * are when clearing the view.
+ * @param PerformanceRecording recording
+ */
+ setCurrentRecording: function (recording) {
+ if (this._currentRecording !== recording) {
+ this._currentRecording = recording;
+ this.emit(EVENTS.RECORDING_SELECTED, recording);
+ }
+ },
+
+ /**
+ * Gets the currently active PerformanceRecording.
+ * @return PerformanceRecording
+ */
+ getCurrentRecording: function () {
+ return this._currentRecording;
+ },
+
+ /**
+ * Get most recently added recording that was triggered manually (via UI).
+ * @return PerformanceRecording
+ */
+ getLatestManualRecording: function () {
+ for (let i = this._recordings.length - 1; i >= 0; i--) {
+ let model = this._recordings[i];
+ if (!model.isConsole() && !model.isImported()) {
+ return this._recordings[i];
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Fired from RecordingsView, we listen on the PerformanceController so we can
+ * set it here and re-emit on the controller, where all views can listen.
+ */
+ _onRecordingSelectFromView: function (_, recording) {
+ this.setCurrentRecording(recording);
+ },
+
+ /**
+ * Fired when the ToolbarView fires a PREF_CHANGED event.
+ * with the value.
+ */
+ _onPrefChanged: function (_, prefName, prefValue) {
+ this.emit(EVENTS.PREF_CHANGED, prefName, prefValue);
+ },
+
+ /*
+ * Called when the developer tools theme changes.
+ */
+ _onThemeChanged: function (_, data) {
+ // Right now, gDevTools only emits `pref-changed` for the theme,
+ // but this could change in the future.
+ if (data.pref !== "devtools.theme") {
+ return;
+ }
+
+ this.emit(EVENTS.THEME_CHANGED, data.newValue);
+ },
+
+ /**
+ * Fired from the front on any event. Propagates to other handlers from here.
+ */
+ _onFrontEvent: function (eventName, ...data) {
+ switch (eventName) {
+ case "profiler-status":
+ let [profilerStatus] = data;
+ this.emit(EVENTS.RECORDING_PROFILER_STATUS_UPDATE, profilerStatus);
+ break;
+ case "recording-started":
+ case "recording-stopping":
+ case "recording-stopped":
+ let [recordingModel] = data;
+ this._addRecordingIfUnknown(recordingModel);
+ this.emit(EVENTS.RECORDING_STATE_CHANGE, eventName, recordingModel);
+ break;
+ }
+ },
+
+ /**
+ * Stores a recording internally.
+ *
+ * @param {PerformanceRecordingFront} recording
+ */
+ _addRecordingIfUnknown: function (recording) {
+ if (this._recordings.indexOf(recording) === -1) {
+ this._recordings.push(recording);
+ this.emit(EVENTS.RECORDING_ADDED, recording);
+ }
+ },
+
+ /**
+ * Takes a recording and returns a value between 0 and 1 indicating how much
+ * of the buffer is used.
+ */
+ getBufferUsageForRecording: function (recording) {
+ return gFront.getBufferUsageForRecording(recording);
+ },
+
+ /**
+ * Returns a boolean indicating if any recordings are currently in progress or not.
+ */
+ isRecording: function () {
+ return this._recordings.some(r => r.isRecording());
+ },
+
+ /**
+ * Returns the internal store of recording models.
+ */
+ getRecordings: function () {
+ return this._recordings;
+ },
+
+ /**
+ * Returns traits from the front.
+ */
+ getTraits: function () {
+ return gFront.traits;
+ },
+
+ /**
+ * Utility method taking a string or an array of strings of feature names (like
+ * "withAllocations" or "withMarkers"), and returns whether or not the current
+ * recording supports that feature, based off of UI preferences and server support.
+ *
+ * @option {Array<string>|string} features
+ * A string or array of strings indicating what configuration is needed on the
+ * recording model, like `withTicks`, or `withMemory`.
+ *
+ * @return boolean
+ */
+ isFeatureSupported: function (features) {
+ if (!features) {
+ return true;
+ }
+
+ let recording = this.getCurrentRecording();
+ if (!recording) {
+ return false;
+ }
+
+ let config = recording.getConfiguration();
+ return [].concat(features).every(f => config[f]);
+ },
+
+ /**
+ * Takes an array of PerformanceRecordingFronts and adds them to the internal
+ * store of the UI. Used by the toolbox to lazily seed recordings that
+ * were observed before the panel was loaded in the scenario where `console.profile()`
+ * is used before the tool is loaded.
+ *
+ * @param {Array<PerformanceRecordingFront>} recordings
+ */
+ populateWithRecordings: function (recordings = []) {
+ for (let recording of recordings) {
+ PerformanceController._addRecordingIfUnknown(recording);
+ }
+ this.emit(EVENTS.RECORDINGS_SEEDED);
+ },
+
+ /**
+ * Returns an object with `supported` and `enabled` properties indicating
+ * whether or not the platform is capable of turning on e10s and whether or not
+ * it's already enabled, respectively.
+ *
+ * @return {object}
+ */
+ getMultiprocessStatus: function () {
+ // If testing, set both supported and enabled to true so we
+ // have realtime rendering tests in non-e10s. This function is
+ // overridden wholesale in tests when we want to test multiprocess support
+ // specifically.
+ if (flags.testing) {
+ return { supported: true, enabled: true };
+ }
+ let supported = system.constants.E10S_TESTING_ONLY;
+ // This is only checked on tool startup -- requires a restart if
+ // e10s subsequently enabled.
+ let enabled = this._e10s;
+ return { supported, enabled };
+ },
+
+ /**
+ * Takes a PerformanceRecording and a state, and waits for
+ * the event to be emitted from the front for that recording.
+ *
+ * @param {PerformanceRecordingFront} recording
+ * @param {string} expectedState
+ * @return {Promise}
+ */
+ waitForStateChangeOnRecording: Task.async(function* (recording, expectedState) {
+ let deferred = promise.defer();
+ this.on(EVENTS.RECORDING_STATE_CHANGE, function handler(state, model) {
+ if (state === expectedState && model === recording) {
+ this.off(EVENTS.RECORDING_STATE_CHANGE, handler);
+ deferred.resolve();
+ }
+ });
+ yield deferred.promise;
+ }),
+
+ /**
+ * Called on init, sets an `e10s` attribute on the main view container with
+ * "disabled" if e10s is possible on the platform and just not on, or "unsupported"
+ * if e10s is not possible on the platform. If e10s is on, no attribute is set.
+ */
+ _setMultiprocessAttributes: function () {
+ let { enabled, supported } = this.getMultiprocessStatus();
+ if (!enabled && supported) {
+ $("#performance-view").setAttribute("e10s", "disabled");
+ } else if (!enabled && !supported) {
+ // Could be a chance where the directive goes away yet e10s is still on
+ $("#performance-view").setAttribute("e10s", "unsupported");
+ }
+ },
+
+ /**
+ * Pipes an event from some source to the PerformanceController.
+ */
+ _pipe: function (eventName, ...data) {
+ this.emit(eventName, ...data);
+ },
+
+ toString: () => "[object PerformanceController]"
+};
+
+/**
+ * Convenient way of emitting events from the controller.
+ */
+EventEmitter.decorate(PerformanceController);
+
+/**
+ * DOM query helpers.
+ */
+/* exported $, $$ */
+function $(selector, target = document) {
+ return target.querySelector(selector);
+}
+function $$(selector, target = document) {
+ return target.querySelectorAll(selector);
+}
diff --git a/devtools/client/performance/performance-view.js b/devtools/client/performance/performance-view.js
new file mode 100644
index 000000000..f490dda5f
--- /dev/null
+++ b/devtools/client/performance/performance-view.js
@@ -0,0 +1,411 @@
+/* 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/. */
+/* import-globals-from performance-controller.js */
+/* globals OverviewView, window */
+"use strict";
+/**
+ * Master view handler for the performance tool.
+ */
+var PerformanceView = {
+
+ _state: null,
+
+ // Set to true if the front emits a "buffer-status" event, indicating
+ // that the server has support for determining buffer status.
+ _bufferStatusSupported: false,
+
+ // Mapping of state to selectors for different properties and their values,
+ // from the main profiler view. Used in `PerformanceView.setState()`
+ states: {
+ "unavailable": [
+ {
+ sel: "#performance-view",
+ opt: "selectedPanel",
+ val: () => $("#unavailable-notice")
+ },
+ {
+ sel: "#performance-view-content",
+ opt: "hidden",
+ val: () => true
+ },
+ ],
+ "empty": [
+ {
+ sel: "#performance-view",
+ opt: "selectedPanel",
+ val: () => $("#empty-notice")
+ },
+ {
+ sel: "#performance-view-content",
+ opt: "hidden",
+ val: () => true
+ },
+ ],
+ "recording": [
+ {
+ sel: "#performance-view",
+ opt: "selectedPanel",
+ val: () => $("#performance-view-content")
+ },
+ {
+ sel: "#performance-view-content",
+ opt: "hidden",
+ val: () => false
+ },
+ {
+ sel: "#details-pane-container",
+ opt: "selectedPanel",
+ val: () => $("#recording-notice")
+ },
+ ],
+ "console-recording": [
+ {
+ sel: "#performance-view",
+ opt: "selectedPanel",
+ val: () => $("#performance-view-content")
+ },
+ {
+ sel: "#performance-view-content",
+ opt: "hidden",
+ val: () => false
+ },
+ {
+ sel: "#details-pane-container",
+ opt: "selectedPanel",
+ val: () => $("#console-recording-notice")
+ },
+ ],
+ "recorded": [
+ {
+ sel: "#performance-view",
+ opt: "selectedPanel",
+ val: () => $("#performance-view-content")
+ },
+ {
+ sel: "#performance-view-content",
+ opt: "hidden",
+ val: () => false
+ },
+ {
+ sel: "#details-pane-container",
+ opt: "selectedPanel",
+ val: () => $("#details-pane")
+ },
+ ],
+ "loading": [
+ {
+ sel: "#performance-view",
+ opt: "selectedPanel",
+ val: () => $("#performance-view-content")
+ },
+ {
+ sel: "#performance-view-content",
+ opt: "hidden",
+ val: () => false
+ },
+ {
+ sel: "#details-pane-container",
+ opt: "selectedPanel",
+ val: () => $("#loading-notice")
+ },
+ ]
+ },
+
+ /**
+ * Sets up the view with event binding and main subviews.
+ */
+ initialize: Task.async(function* () {
+ this._onRecordButtonClick = this._onRecordButtonClick.bind(this);
+ this._onImportButtonClick = this._onImportButtonClick.bind(this);
+ this._onClearButtonClick = this._onClearButtonClick.bind(this);
+ this._onRecordingSelected = this._onRecordingSelected.bind(this);
+ this._onProfilerStatusUpdated = this._onProfilerStatusUpdated.bind(this);
+ this._onRecordingStateChange = this._onRecordingStateChange.bind(this);
+ this._onNewRecordingFailed = this._onNewRecordingFailed.bind(this);
+
+ // Bind to controller events to unlock the record button
+ PerformanceController.on(EVENTS.RECORDING_SELECTED, this._onRecordingSelected);
+ PerformanceController.on(EVENTS.RECORDING_PROFILER_STATUS_UPDATE,
+ this._onProfilerStatusUpdated);
+ PerformanceController.on(EVENTS.RECORDING_STATE_CHANGE, this._onRecordingStateChange);
+ PerformanceController.on(EVENTS.RECORDING_ADDED, this._onRecordingStateChange);
+ PerformanceController.on(EVENTS.BACKEND_FAILED_AFTER_RECORDING_START,
+ this._onNewRecordingFailed);
+
+ if (yield PerformanceController.canCurrentlyRecord()) {
+ this.setState("empty");
+ } else {
+ this.setState("unavailable");
+ }
+
+ // Initialize the ToolbarView first, because other views may need access
+ // to the OptionsView via the controller, to read prefs.
+ yield ToolbarView.initialize();
+ yield RecordingsView.initialize();
+ yield OverviewView.initialize();
+ yield DetailsView.initialize();
+
+ // DE-XUL: Begin migrating the toolbar to React. Temporarily hold state here.
+ this._recordingControlsState = {
+ onRecordButtonClick: this._onRecordButtonClick,
+ onImportButtonClick: this._onImportButtonClick,
+ onClearButtonClick: this._onClearButtonClick,
+ isRecording: false,
+ isDisabled: false
+ };
+ // Mount to an HTML element.
+ const {createHtmlMount} = PerformanceUtils;
+ this._recordingControlsMount = createHtmlMount($("#recording-controls-mount"));
+ this._recordingButtonsMounts = Array.from($$(".recording-button-mount"))
+ .map(createHtmlMount);
+
+ this._renderRecordingControls();
+ }),
+
+ /**
+ * DE-XUL: Render the recording controls and buttons using React.
+ */
+ _renderRecordingControls: function () {
+ ReactDOM.render(RecordingControls(this._recordingControlsState),
+ this._recordingControlsMount);
+ for (let button of this._recordingButtonsMounts) {
+ ReactDOM.render(RecordingButton(this._recordingControlsState), button);
+ }
+ },
+
+ /**
+ * Unbinds events and destroys subviews.
+ */
+ destroy: Task.async(function* () {
+ PerformanceController.off(EVENTS.RECORDING_SELECTED, this._onRecordingSelected);
+ PerformanceController.off(EVENTS.RECORDING_PROFILER_STATUS_UPDATE,
+ this._onProfilerStatusUpdated);
+ PerformanceController.off(EVENTS.RECORDING_STATE_CHANGE,
+ this._onRecordingStateChange);
+ PerformanceController.off(EVENTS.RECORDING_ADDED, this._onRecordingStateChange);
+ PerformanceController.off(EVENTS.BACKEND_FAILED_AFTER_RECORDING_START,
+ this._onNewRecordingFailed);
+
+ yield ToolbarView.destroy();
+ yield RecordingsView.destroy();
+ yield OverviewView.destroy();
+ yield DetailsView.destroy();
+ }),
+
+ /**
+ * Sets the state of the profiler view. Possible options are "unavailable",
+ * "empty", "recording", "console-recording", "recorded".
+ */
+ setState: function (state) {
+ // Make sure that the focus isn't captured on a hidden iframe. This fixes a
+ // XUL bug where shortcuts stop working.
+ const iframes = window.document.querySelectorAll("iframe");
+ for (let iframe of iframes) {
+ iframe.blur();
+ }
+ window.focus();
+
+ let viewConfig = this.states[state];
+ if (!viewConfig) {
+ throw new Error(`Invalid state for PerformanceView: ${state}`);
+ }
+ for (let { sel, opt, val } of viewConfig) {
+ for (let el of $$(sel)) {
+ el[opt] = val();
+ }
+ }
+
+ this._state = state;
+
+ if (state === "console-recording") {
+ let recording = PerformanceController.getCurrentRecording();
+ let label = recording.getLabel() || "";
+
+ // Wrap the label in quotes if it exists for the commands.
+ label = label ? `"${label}"` : "";
+
+ let startCommand = $(".console-profile-recording-notice .console-profile-command");
+ let stopCommand = $(".console-profile-stop-notice .console-profile-command");
+
+ startCommand.value = `console.profile(${label})`;
+ stopCommand.value = `console.profileEnd(${label})`;
+ }
+
+ this.updateBufferStatus();
+ this.emit(EVENTS.UI_STATE_CHANGED, state);
+ },
+
+ /**
+ * Returns the state of the PerformanceView.
+ */
+ getState: function () {
+ return this._state;
+ },
+
+ /**
+ * Updates the displayed buffer status.
+ */
+ updateBufferStatus: function () {
+ // If we've never seen a "buffer-status" event from the front, ignore
+ // and keep the buffer elements hidden.
+ if (!this._bufferStatusSupported) {
+ return;
+ }
+
+ let recording = PerformanceController.getCurrentRecording();
+ if (!recording || !recording.isRecording()) {
+ return;
+ }
+
+ let bufferUsage = PerformanceController.getBufferUsageForRecording(recording) || 0;
+
+ // Normalize to a percentage value
+ let percent = Math.floor(bufferUsage * 100);
+
+ let $container = $("#details-pane-container");
+ let $bufferLabel = $(".buffer-status-message", $container.selectedPanel);
+
+ // Be a little flexible on the buffer status, although not sure how
+ // this could happen, as RecordingModel clamps.
+ if (percent >= 99) {
+ $container.setAttribute("buffer-status", "full");
+ } else {
+ $container.setAttribute("buffer-status", "in-progress");
+ }
+
+ $bufferLabel.value = L10N.getFormatStr("profiler.bufferFull", percent);
+ this.emit(EVENTS.UI_RECORDING_PROFILER_STATUS_RENDERED, percent);
+ },
+
+ /**
+ * Toggles the `locked` attribute on the record buttons based
+ * on `lock`.
+ *
+ * @param {boolean} lock
+ */
+ _lockRecordButtons: function (lock) {
+ this._recordingControlsState.isLocked = lock;
+ this._renderRecordingControls();
+ },
+
+ /*
+ * Toggles the `checked` attribute on the record buttons based
+ * on `activate`.
+ *
+ * @param {boolean} activate
+ */
+ _toggleRecordButtons: function (activate) {
+ this._recordingControlsState.isRecording = !!activate;
+ this._renderRecordingControls();
+ },
+
+ /**
+ * When a recording has started.
+ */
+ _onRecordingStateChange: function () {
+ let currentRecording = PerformanceController.getCurrentRecording();
+ let recordings = PerformanceController.getRecordings();
+
+ this._toggleRecordButtons(recordings.find(r => !r.isConsole() && r.isRecording()));
+ this._lockRecordButtons(recordings.find(r => !r.isConsole() && r.isFinalizing()));
+
+ if (currentRecording && currentRecording.isFinalizing()) {
+ this.setState("loading");
+ }
+ if (currentRecording && currentRecording.isCompleted()) {
+ this.setState("recorded");
+ }
+ if (currentRecording && currentRecording.isRecording()) {
+ this.updateBufferStatus();
+ }
+ },
+
+ /**
+ * When starting a recording has failed.
+ */
+ _onNewRecordingFailed: function (e) {
+ this._lockRecordButtons(false);
+ this._toggleRecordButtons(false);
+ },
+
+ /**
+ * Handler for clicking the clear button.
+ */
+ _onClearButtonClick: function (e) {
+ this.emit(EVENTS.UI_CLEAR_RECORDINGS);
+ },
+
+ /**
+ * Handler for clicking the record button.
+ */
+ _onRecordButtonClick: function (e) {
+ if (this._recordingControlsState.isRecording) {
+ this.emit(EVENTS.UI_STOP_RECORDING);
+ } else {
+ this._lockRecordButtons(true);
+ this._toggleRecordButtons(true);
+ this.emit(EVENTS.UI_START_RECORDING);
+ }
+ },
+
+ /**
+ * Handler for clicking the import button.
+ */
+ _onImportButtonClick: function (e) {
+ let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ fp.init(window, L10N.getStr("recordingsList.importDialogTitle"),
+ Ci.nsIFilePicker.modeOpen);
+ fp.appendFilter(L10N.getStr("recordingsList.saveDialogJSONFilter"), "*.json");
+ fp.appendFilter(L10N.getStr("recordingsList.saveDialogAllFilter"), "*.*");
+
+ if (fp.show() == Ci.nsIFilePicker.returnOK) {
+ this.emit(EVENTS.UI_IMPORT_RECORDING, fp.file);
+ }
+ },
+
+ /**
+ * Fired when a recording is selected. Used to toggle the profiler view state.
+ */
+ _onRecordingSelected: function (_, recording) {
+ if (!recording) {
+ this.setState("empty");
+ } else if (recording.isRecording() && recording.isConsole()) {
+ this.setState("console-recording");
+ } else if (recording.isRecording()) {
+ this.setState("recording");
+ } else {
+ this.setState("recorded");
+ }
+ },
+
+ /**
+ * Fired when the controller has updated information on the buffer's status.
+ * Update the buffer status display if shown.
+ */
+ _onProfilerStatusUpdated: function (_, profilerStatus) {
+ // We only care about buffer status here, so check to see
+ // if it has position.
+ if (!profilerStatus || profilerStatus.position === void 0) {
+ return;
+ }
+ // If this is our first buffer event, set the status and add a class
+ if (!this._bufferStatusSupported) {
+ this._bufferStatusSupported = true;
+ $("#details-pane-container").setAttribute("buffer-status", "in-progress");
+ }
+
+ if (!this.getState("recording") && !this.getState("console-recording")) {
+ return;
+ }
+
+ this.updateBufferStatus();
+ },
+
+ toString: () => "[object PerformanceView]"
+};
+
+/**
+ * Convenient way of emitting events from the view.
+ */
+EventEmitter.decorate(PerformanceView);
diff --git a/devtools/client/performance/performance.xul b/devtools/client/performance/performance.xul
new file mode 100644
index 000000000..c30104b74
--- /dev/null
+++ b/devtools/client/performance/performance.xul
@@ -0,0 +1,368 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/content/shared/widgets/widgets.css" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/skin/widgets.css" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/skin/performance.css" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/skin/jit-optimizations.css" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/skin/components-frame.css" type="text/css"?>
+<!DOCTYPE window [
+ <!ENTITY % performanceDTD SYSTEM "chrome://devtools/locale/performance.dtd">
+ %performanceDTD;
+]>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml">
+ <script src="chrome://devtools/content/shared/theme-switching.js"/>
+ <script type="application/javascript" src="performance-controller.js"/>
+ <script type="application/javascript" src="performance-view.js"/>
+ <script type="application/javascript" src="views/overview.js"/>
+ <script type="application/javascript" src="views/toolbar.js"/>
+ <script type="application/javascript" src="views/details-abstract-subview.js"/>
+ <script type="application/javascript" src="views/details-waterfall.js"/>
+ <script type="application/javascript" src="views/details-js-call-tree.js"/>
+ <script type="application/javascript" src="views/details-js-flamegraph.js"/>
+ <script type="application/javascript" src="views/details-memory-call-tree.js"/>
+ <script type="application/javascript" src="views/details-memory-flamegraph.js"/>
+ <script type="application/javascript" src="views/details.js"/>
+ <script type="application/javascript" src="views/recordings.js"/>
+
+ <popupset id="performance-options-popupset">
+ <menupopup id="performance-filter-menupopup" position="before_start"/>
+ <menupopup id="performance-options-menupopup" position="before_end">
+ <menuitem id="option-show-platform-data"
+ type="checkbox"
+ data-pref="show-platform-data"
+ label="&performanceUI.showPlatformData;"
+ tooltiptext="&performanceUI.showPlatformData.tooltiptext;"/>
+ <menuitem id="option-show-jit-optimizations"
+ class="experimental-option"
+ type="checkbox"
+ data-pref="show-jit-optimizations"
+ label="&performanceUI.showJITOptimizations;"
+ tooltiptext="&performanceUI.showJITOptimizations.tooltiptext;"/>
+ <menuitem id="option-enable-memory"
+ class="experimental-option"
+ type="checkbox"
+ data-pref="enable-memory"
+ label="&performanceUI.enableMemory;"
+ tooltiptext="&performanceUI.enableMemory.tooltiptext;"/>
+ <menuitem id="option-enable-allocations"
+ type="checkbox"
+ data-pref="enable-allocations"
+ label="&performanceUI.enableAllocations;"
+ tooltiptext="&performanceUI.enableAllocations.tooltiptext;"/>
+ <menuitem id="option-enable-framerate"
+ type="checkbox"
+ data-pref="enable-framerate"
+ label="&performanceUI.enableFramerate;"
+ tooltiptext="&performanceUI.enableFramerate.tooltiptext;"/>
+ <menuitem id="option-invert-call-tree"
+ type="checkbox"
+ data-pref="invert-call-tree"
+ label="&performanceUI.invertTree;"
+ tooltiptext="&performanceUI.invertTree.tooltiptext;"/>
+ <menuitem id="option-invert-flame-graph"
+ type="checkbox"
+ data-pref="invert-flame-graph"
+ label="&performanceUI.invertFlameGraph;"
+ tooltiptext="&performanceUI.invertFlameGraph.tooltiptext;"/>
+ <menuitem id="option-flatten-tree-recursion"
+ type="checkbox"
+ data-pref="flatten-tree-recursion"
+ label="&performanceUI.flattenTreeRecursion;"
+ tooltiptext="&performanceUI.flattenTreeRecursion.tooltiptext;"/>
+ </menupopup>
+ </popupset>
+
+ <hbox id="body" class="theme-body performance-tool" flex="1">
+
+ <!-- Sidebar: controls and recording list -->
+ <vbox id="recordings-pane">
+ <hbox id="recordings-controls">
+ <html:div id='recording-controls-mount'/>
+ </hbox>
+ <vbox id="recordings-list" class="theme-sidebar" flex="1">
+ <html:div id="recording-list-mount"/>
+ </vbox>
+ </vbox>
+
+ <!-- Main panel content -->
+ <vbox id="performance-pane" flex="1">
+
+ <!-- Top toolbar controls -->
+ <toolbar id="performance-toolbar"
+ class="devtools-toolbar">
+ <hbox id="performance-toolbar-controls-other"
+ class="devtools-toolbarbutton-group">
+ <toolbarbutton id="filter-button"
+ class="devtools-toolbarbutton"
+ popup="performance-filter-menupopup"
+ tooltiptext="&performanceUI.options.filter.tooltiptext;"/>
+ </hbox>
+ <hbox id="performance-toolbar-controls-detail-views"
+ class="devtools-toolbarbutton-group">
+ <toolbarbutton id="select-waterfall-view"
+ class="devtools-toolbarbutton devtools-button"
+ label="&performanceUI.toolbar.waterfall;"
+ hidden="true"
+ data-view="waterfall"
+ tooltiptext="&performanceUI.toolbar.waterfall.tooltiptext;" />
+ <toolbarbutton id="select-js-calltree-view"
+ class="devtools-toolbarbutton devtools-button"
+ label="&performanceUI.toolbar.js-calltree;"
+ hidden="true"
+ data-view="js-calltree"
+ tooltiptext="&performanceUI.toolbar.js-calltree.tooltiptext;" />
+ <toolbarbutton id="select-js-flamegraph-view"
+ class="devtools-toolbarbutton devtools-button"
+ label="&performanceUI.toolbar.js-flamegraph;"
+ hidden="true"
+ data-view="js-flamegraph"
+ tooltiptext="&performanceUI.toolbar.js-flamegraph.tooltiptext;" />
+ <toolbarbutton id="select-memory-calltree-view"
+ class="devtools-toolbarbutton devtools-button"
+ label="&performanceUI.toolbar.memory-calltree;"
+ hidden="true"
+ data-view="memory-calltree"
+ tooltiptext="&performanceUI.toolbar.allocations.tooltiptext;" />
+ <toolbarbutton id="select-memory-flamegraph-view"
+ class="devtools-toolbarbutton devtools-button"
+ label="&performanceUI.toolbar.memory-flamegraph;"
+ hidden="true"
+ data-view="memory-flamegraph" />
+ </hbox>
+ <spacer flex="1"></spacer>
+ <hbox id="performance-toolbar-controls-options"
+ class="devtools-toolbarbutton-group">
+ <toolbarbutton id="performance-options-button"
+ class="devtools-toolbarbutton devtools-option-toolbarbutton"
+ popup="performance-options-menupopup"
+ tooltiptext="&performanceUI.options.gear.tooltiptext;"/>
+ </hbox>
+ </toolbar>
+
+ <!-- Recording contents and general notice messages -->
+ <deck id="performance-view" flex="1">
+
+ <!-- A default notice, shown while initially opening the tool.
+ Keep this element the first child of #performance-view. -->
+ <hbox id="tool-loading-notice"
+ class="notice-container"
+ flex="1">
+ </hbox>
+
+ <!-- "Unavailable" notice, shown when the entire tool is disabled,
+ for example, when in private browsing mode. -->
+ <vbox id="unavailable-notice"
+ class="notice-container"
+ align="center"
+ pack="center"
+ flex="1">
+ <hbox pack="center">
+ <html:div class='recording-button-mount'/>
+ </hbox>
+ <description class="tool-disabled-message">
+ &performanceUI.unavailableNoticePB;
+ </description>
+ </vbox>
+
+ <!-- "Empty" notice, shown when there's no recordings available -->
+ <hbox id="empty-notice"
+ class="notice-container"
+ align="center"
+ pack="center"
+ flex="1">
+ <hbox pack="center">
+ <html:div class='recording-button-mount'/>
+ </hbox>
+ </hbox>
+
+ <!-- Recording contents -->
+ <vbox id="performance-view-content" flex="1">
+
+ <!-- Overview graphs -->
+ <vbox id="overview-pane">
+ <hbox id="markers-overview"/>
+ <hbox id="memory-overview"/>
+ <hbox id="time-framerate"/>
+ </vbox>
+
+ <!-- Detail views and specific notice messages -->
+ <deck id="details-pane-container" flex="1">
+
+ <!-- "Loading" notice, shown when a recording is being loaded -->
+ <hbox id="loading-notice"
+ class="notice-container devtools-throbber"
+ align="center"
+ pack="center"
+ flex="1">
+ <label value="&performanceUI.loadingNotice;"/>
+ </hbox>
+
+ <!-- "Recording" notice, shown when a recording is in progress -->
+ <vbox id="recording-notice"
+ class="notice-container"
+ align="center"
+ pack="center"
+ flex="1">
+ <hbox pack="center">
+ <html:div class='recording-button-mount'/>
+ </hbox>
+ <label class="realtime-disabled-message"
+ value="&performanceUI.disabledRealTime.nonE10SBuild;"/>
+ <label class="realtime-disabled-on-e10s-message"
+ value="&performanceUI.disabledRealTime.disabledE10S;"/>
+ <label class="buffer-status-message"
+ tooltiptext="&performanceUI.bufferStatusTooltip;"/>
+ <label class="buffer-status-message-full"
+ value="&performanceUI.bufferStatusFull;"/>
+ </vbox>
+
+ <!-- "Console" notice, shown when a console recording is in progress -->
+ <vbox id="console-recording-notice"
+ class="notice-container"
+ align="center"
+ pack="center"
+ flex="1">
+ <hbox class="console-profile-recording-notice">
+ <label value="&performanceUI.console.recordingNoticeStart;" />
+ <label class="console-profile-command" />
+ <label value="&performanceUI.console.recordingNoticeEnd;" />
+ </hbox>
+ <hbox class="console-profile-stop-notice">
+ <label value="&performanceUI.console.stopCommandStart;" />
+ <label class="console-profile-command" />
+ <label value="&performanceUI.console.stopCommandEnd;" />
+ </hbox>
+ <label class="realtime-disabled-message"
+ value="&performanceUI.disabledRealTime.nonE10SBuild;"/>
+ <label class="realtime-disabled-on-e10s-message"
+ value="&performanceUI.disabledRealTime.disabledE10S;"/>
+ <label class="buffer-status-message"
+ tooltiptext="&performanceUI.bufferStatusTooltip;"/>
+ <label class="buffer-status-message-full"
+ value="&performanceUI.bufferStatusFull;"/>
+ </vbox>
+
+ <!-- Detail views -->
+ <deck id="details-pane" flex="1">
+
+ <!-- Waterfall -->
+ <hbox id="waterfall-view" flex="1">
+ <html:div xmlns="http://www.w3.org/1999/xhtml" id="waterfall-tree" />
+ <splitter class="devtools-side-splitter"/>
+ <vbox id="waterfall-details"
+ class="theme-sidebar"/>
+ </hbox>
+
+ <!-- JS Tree and JIT view -->
+ <hbox id="js-profile-view" flex="1">
+ <vbox id="js-calltree-view" flex="1">
+ <hbox class="call-tree-headers-container">
+ <label class="plain call-tree-header"
+ type="duration"
+ crop="end"
+ value="&performanceUI.table.totalDuration;"
+ tooltiptext="&performanceUI.table.totalDuration.tooltip;"/>
+ <label class="plain call-tree-header"
+ type="percentage"
+ crop="end"
+ value="&performanceUI.table.totalPercentage;"
+ tooltiptext="&performanceUI.table.totalPercentage.tooltip;"/>
+ <label class="plain call-tree-header"
+ type="self-duration"
+ crop="end"
+ value="&performanceUI.table.selfDuration;"
+ tooltiptext="&performanceUI.table.selfDuration.tooltip;"/>
+ <label class="plain call-tree-header"
+ type="self-percentage"
+ crop="end"
+ value="&performanceUI.table.selfPercentage;"
+ tooltiptext="&performanceUI.table.selfPercentage.tooltip;"/>
+ <label class="plain call-tree-header"
+ type="samples"
+ crop="end"
+ value="&performanceUI.table.samples;"
+ tooltiptext="&performanceUI.table.samples.tooltip;"/>
+ <label class="plain call-tree-header"
+ type="function"
+ crop="end"
+ value="&performanceUI.table.function;"
+ tooltiptext="&performanceUI.table.function.tooltip;"/>
+ </hbox>
+ <vbox class="call-tree-cells-container" flex="1"/>
+ </vbox>
+ <splitter class="devtools-side-splitter"/>
+ <!-- Optimizations Panel -->
+ <vbox id="jit-optimizations-view"
+ class="hidden">
+ </vbox>
+ </hbox>
+
+ <!-- JS FlameChart -->
+ <hbox id="js-flamegraph-view" flex="1">
+ </hbox>
+
+ <!-- Memory Tree -->
+ <vbox id="memory-calltree-view" flex="1">
+ <hbox class="call-tree-headers-container">
+ <label class="plain call-tree-header"
+ type="self-size"
+ crop="end"
+ value="Self Bytes"
+ tooltiptext="&performanceUI.table.totalAlloc.tooltip;"/>
+ <label class="plain call-tree-header"
+ type="self-size-percentage"
+ crop="end"
+ value="Self Bytes %"
+ tooltiptext="&performanceUI.table.totalAlloc.tooltip;"/>
+ <label class="plain call-tree-header"
+ type="self-count"
+ crop="end"
+ value="Self Count"
+ tooltiptext="&performanceUI.table.totalAlloc.tooltip;"/>
+ <label class="plain call-tree-header"
+ type="self-count-percentage"
+ crop="end"
+ value="Self Count %"
+ tooltiptext="&performanceUI.table.totalAlloc.tooltip;"/>
+ <label class="plain call-tree-header"
+ type="size"
+ crop="end"
+ value="Total Size"
+ tooltiptext="&performanceUI.table.totalAlloc.tooltip;"/>
+ <label class="plain call-tree-header"
+ type="size-percentage"
+ crop="end"
+ value="Total Size %"
+ tooltiptext="&performanceUI.table.totalAlloc.tooltip;"/>
+ <label class="plain call-tree-header"
+ type="count"
+ crop="end"
+ value="Total Count"
+ tooltiptext="&performanceUI.table.totalAlloc.tooltip;"/>
+ <label class="plain call-tree-header"
+ type="count-percentage"
+ crop="end"
+ value="Total Count %"
+ tooltiptext="&performanceUI.table.totalAlloc.tooltip;"/>
+ <label class="plain call-tree-header"
+ type="function"
+ crop="end"
+ value="&performanceUI.table.function;"/>
+ </hbox>
+ <vbox class="call-tree-cells-container" flex="1"/>
+ </vbox>
+
+ <!-- Memory FlameChart -->
+ <hbox id="memory-flamegraph-view" flex="1"></hbox>
+ </deck>
+ </deck>
+ </vbox>
+ </deck>
+ </vbox>
+ </hbox>
+</window>
diff --git a/devtools/client/performance/test/.eslintrc.js b/devtools/client/performance/test/.eslintrc.js
new file mode 100644
index 000000000..8d15a76d9
--- /dev/null
+++ b/devtools/client/performance/test/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../../.eslintrc.mochitests.js"
+};
diff --git a/devtools/client/performance/test/browser.ini b/devtools/client/performance/test/browser.ini
new file mode 100644
index 000000000..1d1954177
--- /dev/null
+++ b/devtools/client/performance/test/browser.ini
@@ -0,0 +1,124 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+skip-if = os == 'linux' && e10s && (asan || debug) # Bug 1254821
+support-files =
+ doc_allocs.html
+ doc_innerHTML.html
+ doc_markers.html
+ doc_simple-test.html
+ doc_worker.html
+ js_simpleWorker.js
+ head.js
+
+[browser_aaa-run-first-leaktest.js]
+[browser_perf-button-states.js]
+[browser_perf-calltree-js-categories.js]
+[browser_perf-calltree-js-columns.js]
+[browser_perf-calltree-js-events.js]
+[browser_perf-calltree-memory-columns.js]
+[browser_perf-console-record-01.js]
+[browser_perf-console-record-02.js]
+[browser_perf-console-record-03.js]
+[browser_perf-console-record-04.js]
+[browser_perf-console-record-05.js]
+[browser_perf-console-record-06.js]
+[browser_perf-console-record-07.js]
+[browser_perf-console-record-08.js]
+[browser_perf-console-record-09.js]
+[browser_perf-details-01-toggle.js]
+[browser_perf-details-02-utility-fun.js]
+[browser_perf-details-03-without-allocations.js]
+[browser_perf-details-04-toolbar-buttons.js]
+[browser_perf-details-05-preserve-view.js]
+[browser_perf-details-06-rerender-on-selection.js]
+[browser_perf-details-07-bleed-events.js]
+# [browser_perf-details-gc-snap.js] TODO bug 1256350
+[browser_perf-details-render-00-waterfall.js]
+[browser_perf-details-render-01-js-calltree.js]
+[browser_perf-details-render-02-js-flamegraph.js]
+[browser_perf-details-render-03-memory-calltree.js]
+[browser_perf-details-render-04-memory-flamegraph.js]
+[browser_perf-docload.js]
+[browser_perf-highlighted.js]
+[browser_perf-loading-01.js]
+[browser_perf-loading-02.js]
+# [browser_perf-marker-details.js] TODO bug 1256350
+[browser_perf-options-01-toggle-throw.js]
+[browser_perf-options-02-toggle-throw-alt.js]
+[browser_perf-options-03-toggle-meta.js]
+[browser_perf-options-enable-framerate-01.js]
+[browser_perf-options-enable-framerate-02.js]
+[browser_perf-options-enable-memory-01.js]
+[browser_perf-options-enable-memory-02.js]
+[browser_perf-options-flatten-tree-recursion-01.js]
+[browser_perf-options-flatten-tree-recursion-02.js]
+[browser_perf-options-invert-call-tree-01.js]
+[browser_perf-options-invert-call-tree-02.js]
+[browser_perf-options-invert-flame-graph-01.js]
+[browser_perf-options-invert-flame-graph-02.js]
+[browser_perf-options-propagate-allocations.js]
+[browser_perf-options-propagate-profiler.js]
+[browser_perf-options-show-idle-blocks-01.js]
+[browser_perf-options-show-idle-blocks-02.js]
+# [browser_perf-options-show-jit-optimizations.js] TODO bug 1256350
+[browser_perf-options-show-platform-data-01.js]
+[browser_perf-options-show-platform-data-02.js]
+[browser_perf-overview-render-01.js]
+[browser_perf-overview-render-02.js]
+[browser_perf-overview-render-03.js]
+[browser_perf-overview-render-04.js]
+[browser_perf-overview-selection-01.js]
+[browser_perf-overview-selection-02.js]
+[browser_perf-overview-selection-03.js]
+[browser_perf-overview-time-interval.js]
+# [browser_perf-private-browsing.js] TODO bug 1256350
+[browser_perf-range-changed-render.js]
+[browser_perf-recording-notices-01.js]
+[browser_perf-recording-notices-02.js]
+[browser_perf-recording-notices-03.js]
+[browser_perf-recording-notices-04.js]
+[browser_perf-recording-notices-05.js]
+[browser_perf-recording-selected-01.js]
+[browser_perf-recording-selected-02.js]
+[browser_perf-recording-selected-03.js]
+[browser_perf-recording-selected-04.js]
+[browser_perf-recordings-clear-01.js]
+[browser_perf-recordings-clear-02.js]
+# [browser_perf-recordings-io-01.js] TODO bug 1256350
+# [browser_perf-recordings-io-02.js] TODO bug 1256350
+# [browser_perf-recordings-io-03.js] TODO bug 1256350
+# [browser_perf-recordings-io-04.js] TODO bug 1256350
+# [browser_perf-recordings-io-05.js] TODO bug 1256350
+# [browser_perf-recordings-io-06.js] TODO bug 1256350
+[browser_perf-refresh.js]
+[browser_perf-states.js]
+[browser_perf-telemetry-01.js]
+[browser_perf-telemetry-02.js]
+[browser_perf-telemetry-03.js]
+[browser_perf-telemetry-04.js]
+# [browser_perf-theme-toggle.js] TODO bug 1256350
+[browser_perf-tree-abstract-01.js]
+[browser_perf-tree-abstract-02.js]
+[browser_perf-tree-abstract-03.js]
+[browser_perf-tree-abstract-04.js]
+[browser_perf-tree-abstract-05.js]
+[browser_perf-tree-view-01.js]
+[browser_perf-tree-view-02.js]
+[browser_perf-tree-view-03.js]
+[browser_perf-tree-view-04.js]
+[browser_perf-tree-view-05.js]
+[browser_perf-tree-view-06.js]
+[browser_perf-tree-view-07.js]
+[browser_perf-tree-view-08.js]
+[browser_perf-tree-view-09.js]
+[browser_perf-tree-view-10.js]
+# [browser_perf-tree-view-11.js] TODO bug 1256350
+[browser_perf-ui-recording.js]
+# [browser_timeline-filters-01.js] TODO bug 1256350
+# [browser_timeline-filters-02.js] TODO bug 1256350
+[browser_timeline-waterfall-background.js]
+[browser_timeline-waterfall-generic.js]
+# [browser_timeline-waterfall-rerender.js] TODO bug 1256350
+# [browser_timeline-waterfall-sidebar.js] TODO bug 1256350
+# [browser_timeline-waterfall-workers.js] TODO bug 1256350
diff --git a/devtools/client/performance/test/browser_aaa-run-first-leaktest.js b/devtools/client/performance/test/browser_aaa-run-first-leaktest.js
new file mode 100644
index 000000000..d3ecef42e
--- /dev/null
+++ b/devtools/client/performance/test/browser_aaa-run-first-leaktest.js
@@ -0,0 +1,28 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if the performance tool leaks on initialization and sudden destruction.
+ * You can also use this initialization format as a template for other tests.
+ */
+
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+
+add_task(function* () {
+ let { target, toolbox, panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ ok(target, "Should have a target available.");
+ ok(toolbox, "Should have a toolbox available.");
+ ok(panel, "Should have a panel available.");
+
+ ok(panel.panelWin.gTarget, "Should have a target reference on the panel window.");
+ ok(panel.panelWin.gToolbox, "Should have a toolbox reference on the panel window.");
+ ok(panel.panelWin.gFront, "Should have a front reference on the panel window.");
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-button-states.js b/devtools/client/performance/test/browser_perf-button-states.js
new file mode 100644
index 000000000..7f7ca1b2a
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-button-states.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the recording button states are set as expected.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { once } = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { $, $$, EVENTS, PerformanceController, PerformanceView } = panel.panelWin;
+
+ let recordButton = $("#main-record-button");
+
+ checkRecordButtonsStates(false, false);
+
+ let uiStartClick = once(PerformanceView, EVENTS.UI_START_RECORDING);
+ let recordingStarted = once(PerformanceController, EVENTS.RECORDING_STATE_CHANGE, {
+ expectedArgs: { "1": "recording-started" }
+ });
+ let backendStartReady = once(PerformanceController,
+ EVENTS.BACKEND_READY_AFTER_RECORDING_START);
+ let uiStateRecording = once(PerformanceView, EVENTS.UI_STATE_CHANGED, {
+ expectedArgs: { "1": "recording" }
+ });
+
+ click(recordButton);
+ yield uiStartClick;
+
+ checkRecordButtonsStates(true, true);
+
+ yield recordingStarted;
+
+ checkRecordButtonsStates(true, false);
+
+ yield backendStartReady;
+ yield uiStateRecording;
+
+ let uiStopClick = once(PerformanceView, EVENTS.UI_STOP_RECORDING);
+ let recordingStopped = once(PerformanceController, EVENTS.RECORDING_STATE_CHANGE, {
+ expectedArgs: { "1": "recording-stopped" }
+ });
+ let backendStopReady = once(PerformanceController,
+ EVENTS.BACKEND_READY_AFTER_RECORDING_STOP);
+ let uiStateRecorded = once(PerformanceView, EVENTS.UI_STATE_CHANGED, {
+ expectedArgs: { "1": "recorded" }
+ });
+
+ click(recordButton);
+ yield uiStopClick;
+ yield recordingStopped;
+
+ checkRecordButtonsStates(false, false);
+
+ yield backendStopReady;
+ yield uiStateRecorded;
+
+ yield teardownToolboxAndRemoveTab(panel);
+
+ function checkRecordButtonsStates(checked, locked) {
+ for (let button of $$(".record-button")) {
+ is(button.classList.contains("checked"), checked,
+ "The record button checked state should be " + checked);
+ is(button.disabled, locked,
+ "The record button locked state should be " + locked);
+ }
+ }
+});
diff --git a/devtools/client/performance/test/browser_perf-calltree-js-categories.js b/devtools/client/performance/test/browser_perf-calltree-js-categories.js
new file mode 100644
index 000000000..c0710932f
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-calltree-js-categories.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the categories are shown in the js call tree when
+ * platform data is enabled.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { UI_SHOW_PLATFORM_DATA_PREF } = require("devtools/client/performance/test/helpers/prefs");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+const { busyWait } = require("devtools/client/performance/test/helpers/wait-utils");
+const { once } = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { EVENTS, $, $$, DetailsView, JsCallTreeView } = panel.panelWin;
+
+ // Enable platform data to show the categories in the tree.
+ Services.prefs.setBoolPref(UI_SHOW_PLATFORM_DATA_PREF, true);
+
+ yield startRecording(panel);
+ // To show the `Gecko` category in the tree.
+ yield busyWait(100);
+ yield stopRecording(panel);
+
+ let rendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
+ yield DetailsView.selectView("js-calltree");
+ yield rendered;
+
+ is($(".call-tree-cells-container").hasAttribute("categories-hidden"), false,
+ "The call tree cells container should show the categories now.");
+ ok(geckoCategoryPresent($$),
+ "A category node with the text `Gecko` is displayed in the tree.");
+
+ // Disable platform data to hide the categories.
+ Services.prefs.setBoolPref(UI_SHOW_PLATFORM_DATA_PREF, false);
+
+ is($(".call-tree-cells-container").getAttribute("categories-hidden"), "",
+ "The call tree cells container should hide the categories now.");
+ ok(!geckoCategoryPresent($$),
+ "A category node with the text `Gecko` doesn't exist in the tree anymore.");
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
+
+function geckoCategoryPresent($$) {
+ for (let elem of $$(".call-tree-category")) {
+ if (elem.textContent.trim() == "Gecko") {
+ return true;
+ }
+ }
+ return false;
+}
diff --git a/devtools/client/performance/test/browser_perf-calltree-js-columns.js b/devtools/client/performance/test/browser_perf-calltree-js-columns.js
new file mode 100644
index 000000000..5c8b6e2f3
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-calltree-js-columns.js
@@ -0,0 +1,66 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the js call tree view renders the correct columns.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { UI_SHOW_PLATFORM_DATA_PREF } = require("devtools/client/performance/test/helpers/prefs");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+const { busyWait } = require("devtools/client/performance/test/helpers/wait-utils");
+const { once } = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { EVENTS, $, $$, DetailsView, JsCallTreeView } = panel.panelWin;
+
+ // Enable platform data to show the platform functions in the tree.
+ Services.prefs.setBoolPref(UI_SHOW_PLATFORM_DATA_PREF, true);
+
+ yield startRecording(panel);
+ // To show the `busyWait` function in the tree.
+ yield busyWait(100);
+ yield stopRecording(panel);
+
+ let rendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
+ yield DetailsView.selectView("js-calltree");
+ yield rendered;
+
+ ok(DetailsView.isViewSelected(JsCallTreeView), "The call tree is now selected.");
+
+ testCells($, $$, {
+ "duration": true,
+ "percentage": true,
+ "allocations": false,
+ "self-duration": true,
+ "self-percentage": true,
+ "self-allocations": false,
+ "samples": true,
+ "function": true
+ });
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
+
+function testCells($, $$, visibleCells) {
+ for (let cell in visibleCells) {
+ if (visibleCells[cell]) {
+ ok($(`.call-tree-cell[type=${cell}]`),
+ `At least one ${cell} column was visible in the tree.`);
+ } else {
+ ok(!$(`.call-tree-cell[type=${cell}]`),
+ `No ${cell} columns were visible in the tree.`);
+ }
+ }
+
+ is($$(".call-tree-cell", $(".call-tree-item")).length,
+ Object.keys(visibleCells).filter(e => visibleCells[e]).length,
+ "The correct number of cells were found in the tree.");
+}
diff --git a/devtools/client/performance/test/browser_perf-calltree-js-events.js b/devtools/client/performance/test/browser_perf-calltree-js-events.js
new file mode 100644
index 000000000..c93c7f069
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-calltree-js-events.js
@@ -0,0 +1,58 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the call tree up/down events work for js calltrees.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+const { synthesizeProfile } = require("devtools/client/performance/test/helpers/synth-utils");
+const { once } = require("devtools/client/performance/test/helpers/event-utils");
+const { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
+
+add_task(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { EVENTS, $, DetailsView, OverviewView, JsCallTreeView } = panel.panelWin;
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ let rendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
+ yield DetailsView.selectView("js-calltree");
+ yield rendered;
+
+ // Mock the profile used so we can get a deterministic tree created.
+ let profile = synthesizeProfile();
+ let threadNode = new ThreadNode(profile.threads[0], OverviewView.getTimeInterval());
+ JsCallTreeView._populateCallTree(threadNode);
+ JsCallTreeView.emit(EVENTS.UI_JS_CALL_TREE_RENDERED);
+
+ let firstTreeItem = $("#js-calltree-view .call-tree-item");
+
+ // DE-XUL: There are focus issues with XUL. Focus first, then synthesize the clicks
+ // so that keyboard events work correctly.
+ firstTreeItem.focus();
+
+ let count = 0;
+ let onFocus = () => count++;
+ JsCallTreeView.on("focus", onFocus);
+
+ click(firstTreeItem);
+
+ key("VK_DOWN");
+ key("VK_DOWN");
+ key("VK_DOWN");
+ key("VK_DOWN");
+
+ JsCallTreeView.off("focus", onFocus);
+ is(count, 4, "Several focus events are fired for the calltree.");
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-calltree-memory-columns.js b/devtools/client/performance/test/browser_perf-calltree-memory-columns.js
new file mode 100644
index 000000000..9eb8a8de9
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-calltree-memory-columns.js
@@ -0,0 +1,69 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the memory call tree view renders the correct columns.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { UI_ENABLE_ALLOCATIONS_PREF } = require("devtools/client/performance/test/helpers/prefs");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+const { once } = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { EVENTS, $, $$, DetailsView, MemoryCallTreeView } = panel.panelWin;
+
+ // Enable allocations to test.
+ Services.prefs.setBoolPref(UI_ENABLE_ALLOCATIONS_PREF, true);
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ let rendered = once(MemoryCallTreeView, EVENTS.UI_MEMORY_CALL_TREE_RENDERED);
+ yield DetailsView.selectView("memory-calltree");
+ yield rendered;
+
+ ok(DetailsView.isViewSelected(MemoryCallTreeView), "The call tree is now selected.");
+
+ testCells($, $$, {
+ "duration": false,
+ "percentage": false,
+ "count": true,
+ "count-percentage": true,
+ "size": true,
+ "size-percentage": true,
+ "self-duration": false,
+ "self-percentage": false,
+ "self-count": true,
+ "self-count-percentage": true,
+ "self-size": true,
+ "self-size-percentage": true,
+ "samples": false,
+ "function": true
+ });
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
+
+function testCells($, $$, visibleCells) {
+ for (let cell in visibleCells) {
+ if (visibleCells[cell]) {
+ ok($(`.call-tree-cell[type=${cell}]`),
+ `At least one ${cell} column was visible in the tree.`);
+ } else {
+ ok(!$(`.call-tree-cell[type=${cell}]`),
+ `No ${cell} columns were visible in the tree.`);
+ }
+ }
+
+ is($$(".call-tree-cell", $(".call-tree-item")).length,
+ Object.keys(visibleCells).filter(e => visibleCells[e]).length,
+ "The correct number of cells were found in the tree.");
+}
diff --git a/devtools/client/performance/test/browser_perf-console-record-01.js b/devtools/client/performance/test/browser_perf-console-record-01.js
new file mode 100644
index 000000000..9353c2f9a
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-console-record-01.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if the profiler is populated by console recordings that have finished
+ * before it was opened.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { initPerformanceInTab, initConsoleInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { waitUntil } = require("devtools/client/performance/test/helpers/wait-utils");
+const { getSelectedRecording } = require("devtools/client/performance/test/helpers/recording-utils");
+
+add_task(function* () {
+ let { target, console } = yield initConsoleInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ yield console.profile("rust");
+ yield console.profileEnd("rust");
+
+ let { panel } = yield initPerformanceInTab({ tab: target.tab });
+ let { PerformanceController, WaterfallView } = panel.panelWin;
+
+ yield waitUntil(() => PerformanceController.getRecordings().length == 1);
+ yield waitUntil(() => WaterfallView.wasRenderedAtLeastOnce);
+
+ let recordings = PerformanceController.getRecordings();
+ is(recordings.length, 1, "One recording found in the performance panel.");
+ is(recordings[0].isConsole(), true, "Recording came from console.profile.");
+ is(recordings[0].getLabel(), "rust", "Correct label in the recording model.");
+
+ const selected = getSelectedRecording(panel);
+
+ is(selected, recordings[0],
+ "The profile from console should be selected as it's the only one.");
+ is(selected.getLabel(), "rust",
+ "The profile label for the first recording is correct.");
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-console-record-02.js b/devtools/client/performance/test/browser_perf-console-record-02.js
new file mode 100644
index 000000000..36d0a54d1
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-console-record-02.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if the profiler is populated by in-progress console recordings
+ * when it is opened.
+ */
+
+const { Constants } = require("devtools/client/performance/modules/constants");
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { initPerformanceInTab, initConsoleInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { waitForRecordingStoppedEvents } = require("devtools/client/performance/test/helpers/actions");
+const { waitUntil } = require("devtools/client/performance/test/helpers/wait-utils");
+const { times } = require("devtools/client/performance/test/helpers/event-utils");
+const { getSelectedRecording } = require("devtools/client/performance/test/helpers/recording-utils");
+
+add_task(function* () {
+ let { target, console } = yield initConsoleInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ yield console.profile("rust");
+ yield console.profile("rust2");
+
+ let { panel } = yield initPerformanceInTab({ tab: target.tab });
+ let { EVENTS, PerformanceController, OverviewView } = panel.panelWin;
+
+ yield waitUntil(() => PerformanceController.getRecordings().length == 2);
+
+ let recordings = PerformanceController.getRecordings();
+ is(recordings.length, 2, "Two recordings found in the performance panel.");
+ is(recordings[0].isConsole(), true, "Recording came from console.profile (1).");
+ is(recordings[0].getLabel(), "rust", "Correct label in the recording model (1).");
+ is(recordings[0].isRecording(), true, "Recording is still recording (1).");
+ is(recordings[1].isConsole(), true, "Recording came from console.profile (2).");
+ is(recordings[1].getLabel(), "rust2", "Correct label in the recording model (2).");
+ is(recordings[1].isRecording(), true, "Recording is still recording (2).");
+
+ const selected = getSelectedRecording(panel);
+ is(selected, recordings[0],
+ "The first console recording should be selected.");
+ is(selected.getLabel(), "rust",
+ "The profile label for the first recording is correct.");
+
+ // Ensure overview is still rendering.
+ yield times(OverviewView, EVENTS.UI_OVERVIEW_RENDERED, 3, {
+ expectedArgs: { "1": Constants.FRAMERATE_GRAPH_LOW_RES_INTERVAL }
+ });
+
+ let stopped = waitForRecordingStoppedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true
+ });
+ yield console.profileEnd("rust");
+ yield stopped;
+
+ stopped = waitForRecordingStoppedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true,
+ // only emitted when a finished recording is selected
+ skipWaitingForOverview: true,
+ skipWaitingForSubview: true,
+ });
+ yield console.profileEnd("rust2");
+ yield stopped;
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-console-record-03.js b/devtools/client/performance/test/browser_perf-console-record-03.js
new file mode 100644
index 000000000..a12aab5f2
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-console-record-03.js
@@ -0,0 +1,58 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if the profiler is populated by in-progress console recordings, and
+ * also console recordings that have finished before it was opened.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { initPerformanceInTab, initConsoleInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { waitForRecordingStoppedEvents } = require("devtools/client/performance/test/helpers/actions");
+const { waitUntil } = require("devtools/client/performance/test/helpers/wait-utils");
+const { getSelectedRecording } = require("devtools/client/performance/test/helpers/recording-utils");
+
+add_task(function* () {
+ let { target, console } = yield initConsoleInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ yield console.profile("rust");
+ yield console.profileEnd("rust");
+ yield console.profile("rust2");
+
+ let { panel } = yield initPerformanceInTab({ tab: target.tab });
+ let { PerformanceController, WaterfallView } = panel.panelWin;
+
+ yield waitUntil(() => PerformanceController.getRecordings().length == 2);
+ yield waitUntil(() => WaterfallView.wasRenderedAtLeastOnce);
+
+ let recordings = PerformanceController.getRecordings();
+ is(recordings.length, 2, "Two recordings found in the performance panel.");
+ is(recordings[0].isConsole(), true, "Recording came from console.profile (1).");
+ is(recordings[0].getLabel(), "rust", "Correct label in the recording model (1).");
+ is(recordings[0].isRecording(), false, "Recording is still recording (1).");
+ is(recordings[1].isConsole(), true, "Recording came from console.profile (2).");
+ is(recordings[1].getLabel(), "rust2", "Correct label in the recording model (2).");
+ is(recordings[1].isRecording(), true, "Recording is still recording (2).");
+
+ const selected = getSelectedRecording(panel);
+ is(selected, recordings[0],
+ "The first console recording should be selected.");
+ is(selected.getLabel(), "rust",
+ "The profile label for the first recording is correct.");
+
+ let stopped = waitForRecordingStoppedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true,
+ // only emitted when a finished recording is selected
+ skipWaitingForOverview: true,
+ skipWaitingForSubview: true,
+ });
+ yield console.profileEnd("rust2");
+ yield stopped;
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-console-record-04.js b/devtools/client/performance/test/browser_perf-console-record-04.js
new file mode 100644
index 000000000..6465bc746
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-console-record-04.js
@@ -0,0 +1,58 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the profiler can handle creation and stopping of console profiles
+ * after being opened.
+ */
+
+const { Constants } = require("devtools/client/performance/modules/constants");
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { initPerformanceInTab, initConsoleInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { waitForRecordingStartedEvents, waitForRecordingStoppedEvents } = require("devtools/client/performance/test/helpers/actions");
+const { times } = require("devtools/client/performance/test/helpers/event-utils");
+const { getSelectedRecording } = require("devtools/client/performance/test/helpers/recording-utils");
+
+add_task(function* () {
+ let { target, console } = yield initConsoleInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { panel } = yield initPerformanceInTab({ tab: target.tab });
+ let { EVENTS, PerformanceController, OverviewView } = panel.panelWin;
+
+ let started = waitForRecordingStartedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true
+ });
+ yield console.profile("rust");
+ yield started;
+
+ let recordings = PerformanceController.getRecordings();
+ is(recordings.length, 1, "One recording found in the performance panel.");
+ is(recordings[0].isConsole(), true, "Recording came from console.profile.");
+ is(recordings[0].getLabel(), "rust", "Correct label in the recording model.");
+ is(recordings[0].isRecording(), true, "Recording is still recording.");
+
+ const selected = getSelectedRecording(panel);
+ is(selected, recordings[0],
+ "The profile from console should be selected as it's the only one.");
+ is(selected.getLabel(), "rust",
+ "The profile label for the first recording is correct.");
+
+ // Ensure overview is still rendering.
+ yield times(OverviewView, EVENTS.UI_OVERVIEW_RENDERED, 3, {
+ expectedArgs: { "1": Constants.FRAMERATE_GRAPH_LOW_RES_INTERVAL }
+ });
+
+ let stopped = waitForRecordingStoppedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true
+ });
+ yield console.profileEnd("rust");
+ yield stopped;
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-console-record-05.js b/devtools/client/performance/test/browser_perf-console-record-05.js
new file mode 100644
index 000000000..373fd5b0f
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-console-record-05.js
@@ -0,0 +1,92 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that multiple recordings with the same label (non-overlapping) appear
+ * in the recording list.
+ */
+
+const { Constants } = require("devtools/client/performance/modules/constants");
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { initPerformanceInTab, initConsoleInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { waitForRecordingStartedEvents, waitForRecordingStoppedEvents } = require("devtools/client/performance/test/helpers/actions");
+const { times } = require("devtools/client/performance/test/helpers/event-utils");
+const { getSelectedRecording } = require("devtools/client/performance/test/helpers/recording-utils");
+
+add_task(function* () {
+ let { target, console } = yield initConsoleInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { panel } = yield initPerformanceInTab({ tab: target.tab });
+ let { EVENTS, PerformanceController, OverviewView } = panel.panelWin;
+
+ let started = waitForRecordingStartedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true
+ });
+ yield console.profile("rust");
+ yield started;
+
+ let recordings = PerformanceController.getRecordings();
+ is(recordings.length, 1, "One recording found in the performance panel.");
+ is(recordings[0].isConsole(), true, "Recording came from console.profile (1).");
+ is(recordings[0].getLabel(), "rust", "Correct label in the recording model (1).");
+ is(recordings[0].isRecording(), true, "Recording is still recording (1).");
+
+ let selected = getSelectedRecording(panel);
+ is(selected, recordings[0],
+ "The profile from console should be selected as it's the only one.");
+ is(selected.getLabel(), "rust",
+ "The profile label for the first recording is correct.");
+
+ // Ensure overview is still rendering.
+ yield times(OverviewView, EVENTS.UI_OVERVIEW_RENDERED, 3, {
+ expectedArgs: { "1": Constants.FRAMERATE_GRAPH_LOW_RES_INTERVAL }
+ });
+
+ let stopped = waitForRecordingStoppedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true
+ });
+ yield console.profileEnd("rust");
+ yield stopped;
+
+ started = waitForRecordingStartedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true,
+ // only emitted when an in-progress recording is selected
+ skipWaitingForOverview: true,
+ // the view state won't switch to "console-recording" unless the new
+ // in-progress recording is selected, which won't happen
+ skipWaitingForViewState: true,
+ });
+ yield console.profile("rust");
+ yield started;
+
+ recordings = PerformanceController.getRecordings();
+ is(recordings.length, 2, "Two recordings found in the performance panel.");
+ is(recordings[1].isConsole(), true, "Recording came from console.profile (2).");
+ is(recordings[1].getLabel(), "rust", "Correct label in the recording model (2).");
+ is(recordings[1].isRecording(), true, "Recording is still recording (2).");
+
+ selected = getSelectedRecording(panel);
+ is(selected, recordings[0],
+ "The profile from console should still be selected");
+ is(selected.getLabel(), "rust",
+ "The profile label for the first recording is correct.");
+
+ stopped = waitForRecordingStoppedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true,
+ // only emitted when a finished recording is selected
+ skipWaitingForOverview: true,
+ skipWaitingForSubview: true,
+ });
+ yield console.profileEnd("rust");
+ yield stopped;
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-console-record-06.js b/devtools/client/performance/test/browser_perf-console-record-06.js
new file mode 100644
index 000000000..f1057c261
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-console-record-06.js
@@ -0,0 +1,96 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that console recordings can overlap (not completely nested).
+ */
+
+const { Constants } = require("devtools/client/performance/modules/constants");
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { initPerformanceInTab, initConsoleInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { waitForRecordingStartedEvents, waitForRecordingStoppedEvents } = require("devtools/client/performance/test/helpers/actions");
+const { times } = require("devtools/client/performance/test/helpers/event-utils");
+const { getSelectedRecording } = require("devtools/client/performance/test/helpers/recording-utils");
+
+add_task(function* () {
+ let { target, console } = yield initConsoleInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { panel } = yield initPerformanceInTab({ tab: target.tab });
+ let { EVENTS, PerformanceController, OverviewView } = panel.panelWin;
+
+ let started = waitForRecordingStartedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true
+ });
+ yield console.profile("rust");
+ yield started;
+
+ let recordings = PerformanceController.getRecordings();
+ is(recordings.length, 1, "A recording found in the performance panel.");
+ is(getSelectedRecording(panel), recordings[0],
+ "The first console recording should be selected.");
+
+ // Ensure overview is still rendering.
+ yield times(OverviewView, EVENTS.UI_OVERVIEW_RENDERED, 3, {
+ expectedArgs: { "1": Constants.FRAMERATE_GRAPH_LOW_RES_INTERVAL }
+ });
+
+ started = waitForRecordingStartedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true,
+ // only emitted when an in-progress recording is selected
+ skipWaitingForOverview: true,
+ // the view state won't switch to "console-recording" unless the new
+ // in-progress recording is selected, which won't happen
+ skipWaitingForViewState: true,
+ });
+ yield console.profile("golang");
+ yield started;
+
+ recordings = PerformanceController.getRecordings();
+ is(recordings.length, 2, "Two recordings found in the performance panel.");
+ is(getSelectedRecording(panel), recordings[0],
+ "The first console recording should still be selected.");
+
+ // Ensure overview is still rendering.
+ yield times(OverviewView, EVENTS.UI_OVERVIEW_RENDERED, 3, {
+ expectedArgs: { "1": Constants.FRAMERATE_GRAPH_LOW_RES_INTERVAL }
+ });
+
+ let stopped = waitForRecordingStoppedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true
+ });
+ yield console.profileEnd("rust");
+ yield stopped;
+
+ recordings = PerformanceController.getRecordings();
+ is(recordings.length, 2, "Two recordings found in the performance panel.");
+ is(getSelectedRecording(panel), recordings[0],
+ "The first console recording should still be selected.");
+ is(recordings[0].isRecording(), false,
+ "The first console recording should no longer be recording.");
+
+ stopped = waitForRecordingStoppedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true,
+ // only emitted when a finished recording is selected
+ skipWaitingForOverview: true,
+ skipWaitingForSubview: true,
+ });
+ yield console.profileEnd("golang");
+ yield stopped;
+
+ recordings = PerformanceController.getRecordings();
+ is(recordings.length, 2, "Two recordings found in the performance panel.");
+ is(getSelectedRecording(panel), recordings[0],
+ "The first console recording should still be selected.");
+ is(recordings[1].isRecording(), false,
+ "The second console recording should no longer be recording.");
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-console-record-07.js b/devtools/client/performance/test/browser_perf-console-record-07.js
new file mode 100644
index 000000000..af8dc5144
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-console-record-07.js
@@ -0,0 +1,170 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that a call to console.profileEnd() with no label ends the
+ * most recent console recording, and console.profileEnd() with a label that
+ * does not match any pending recordings does nothing.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { initPerformanceInTab, initConsoleInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { waitForRecordingStartedEvents, waitForRecordingStoppedEvents } = require("devtools/client/performance/test/helpers/actions");
+const { idleWait } = require("devtools/client/performance/test/helpers/wait-utils");
+const { getSelectedRecording } = require("devtools/client/performance/test/helpers/recording-utils");
+
+add_task(function* () {
+ let { target, console } = yield initConsoleInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { panel } = yield initPerformanceInTab({ tab: target.tab });
+ let { PerformanceController } = panel.panelWin;
+
+ let started = waitForRecordingStartedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true
+ });
+ yield console.profile();
+ yield started;
+
+ started = waitForRecordingStartedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true,
+ // only emitted when an in-progress recording is selected
+ skipWaitingForOverview: true,
+ // the view state won't switch to "console-recording" unless the new
+ // in-progress recording is selected, which won't happen
+ skipWaitingForViewState: true,
+ });
+ yield console.profile("1");
+ yield started;
+
+ started = waitForRecordingStartedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true,
+ // only emitted when an in-progress recording is selected
+ skipWaitingForOverview: true,
+ // the view state won't switch to "console-recording" unless the new
+ // in-progress recording is selected, which won't happen
+ skipWaitingForViewState: true,
+ });
+ yield console.profile("2");
+ yield started;
+
+ let recordings = PerformanceController.getRecordings();
+ let selected = getSelectedRecording(panel);
+ is(recordings.length, 3, "Three recordings found in the performance panel.");
+ is(recordings[0].getLabel(), "", "Checking label of recording 1");
+ is(recordings[1].getLabel(), "1", "Checking label of recording 2");
+ is(recordings[2].getLabel(), "2", "Checking label of recording 3");
+ is(selected, recordings[0],
+ "The first console recording should be selected.");
+
+ is(recordings[0].isRecording(), true,
+ "All recordings should now be started. (1)");
+ is(recordings[1].isRecording(), true,
+ "All recordings should now be started. (2)");
+ is(recordings[2].isRecording(), true,
+ "All recordings should now be started. (3)");
+
+ let stopped = waitForRecordingStoppedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true,
+ // only emitted when a finished recording is selected
+ skipWaitingForOverview: true,
+ skipWaitingForSubview: true,
+ // the view state won't switch to "recorded" unless the new
+ // finished recording is selected, which won't happen
+ skipWaitingForViewState: true,
+ });
+ yield console.profileEnd();
+ yield stopped;
+
+ selected = getSelectedRecording(panel);
+ recordings = PerformanceController.getRecordings();
+ is(recordings.length, 3, "Three recordings found in the performance panel.");
+ is(selected, recordings[0],
+ "The first console recording should still be selected.");
+
+ is(recordings[0].isRecording(), true, "The not most recent recording should not stop " +
+ "when calling console.profileEnd with no args.");
+ is(recordings[1].isRecording(), true, "The not most recent recording should not stop " +
+ "when calling console.profileEnd with no args.");
+ is(recordings[2].isRecording(), false, "Only the most recent recording should stop " +
+ "when calling console.profileEnd with no args.");
+
+ info("Trying to `profileEnd` a non-existent console recording.");
+ console.profileEnd("fxos");
+ yield idleWait(1000);
+
+ selected = getSelectedRecording(panel);
+ recordings = PerformanceController.getRecordings();
+ is(recordings.length, 3, "Three recordings found in the performance panel.");
+ is(selected, recordings[0],
+ "The first console recording should still be selected.");
+
+ is(recordings[0].isRecording(), true,
+ "The first recording should not be ended yet.");
+ is(recordings[1].isRecording(), true,
+ "The second recording should not be ended yet.");
+ is(recordings[2].isRecording(), false,
+ "The third recording should still be ended.");
+
+ stopped = waitForRecordingStoppedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true,
+ // only emitted when a finished recording is selected
+ skipWaitingForOverview: true,
+ skipWaitingForSubview: true,
+ // the view state won't switch to "recorded" unless the new
+ // finished recording is selected, which won't happen
+ skipWaitingForViewState: true,
+ });
+ yield console.profileEnd();
+ yield stopped;
+
+ selected = getSelectedRecording(panel);
+ recordings = PerformanceController.getRecordings();
+ is(recordings.length, 3, "Three recordings found in the performance panel.");
+ is(selected, recordings[0],
+ "The first console recording should still be selected.");
+
+ is(recordings[0].isRecording(), true,
+ "The first recording should not be ended yet.");
+ is(recordings[1].isRecording(), false,
+ "The second recording should not be ended yet.");
+ is(recordings[2].isRecording(), false,
+ "The third recording should still be ended.");
+
+ stopped = waitForRecordingStoppedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true
+ });
+ yield console.profileEnd();
+ yield stopped;
+
+ selected = getSelectedRecording(panel);
+ recordings = PerformanceController.getRecordings();
+ is(recordings.length, 3, "Three recordings found in the performance panel.");
+ is(selected, recordings[0],
+ "The first console recording should be selected.");
+
+ is(recordings[0].isRecording(), false,
+ "All recordings should now be ended. (1)");
+ is(recordings[1].isRecording(), false,
+ "All recordings should now be ended. (2)");
+ is(recordings[2].isRecording(), false,
+ "All recordings should now be ended. (3)");
+
+ info("Trying to `profileEnd` with no pending recordings.");
+ console.profileEnd();
+ yield idleWait(1000);
+
+ ok(true, "Calling console.profileEnd() with no argument and no pending recordings " +
+ "does not throw.");
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-console-record-08.js b/devtools/client/performance/test/browser_perf-console-record-08.js
new file mode 100644
index 000000000..2ad81c413
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-console-record-08.js
@@ -0,0 +1,268 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if the profiler can correctly handle simultaneous console and manual
+ * recordings (via `console.profile` and clicking the record button).
+ */
+
+const { Constants } = require("devtools/client/performance/modules/constants");
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { initPerformanceInTab, initConsoleInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+const { waitForRecordingStartedEvents, waitForRecordingStoppedEvents } = require("devtools/client/performance/test/helpers/actions");
+const { once, times } = require("devtools/client/performance/test/helpers/event-utils");
+const { setSelectedRecording } = require("devtools/client/performance/test/helpers/recording-utils");
+
+/**
+ * The following are bit flag constants that are used to represent the state of a
+ * recording.
+ */
+
+// Represents a manually recorded profile, if a user hit the record button.
+const MANUAL = 0;
+// Represents a recorded profile from console.profile().
+const CONSOLE = 1;
+// Represents a profile that is currently recording.
+const RECORDING = 2;
+// Represents a profile that is currently selected.
+const SELECTED = 4;
+
+/**
+ * Utility function to provide a meaningful inteface for testing that the bits
+ * match for the recording state.
+ * @param {integer} expected - The expected bit values packed in an integer.
+ * @param {integer} actual - The actual bit values packed in an integer.
+ */
+function hasBitFlag(expected, actual) {
+ return !!(expected & actual);
+}
+
+add_task(function* () {
+ // This test seems to take a very long time to finish on Linux VMs.
+ requestLongerTimeout(4);
+
+ let { target, console } = yield initConsoleInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { panel } = yield initPerformanceInTab({ tab: target.tab });
+ let { EVENTS, PerformanceController, OverviewView } = panel.panelWin;
+
+ info("Recording 1 - Starting console.profile()...");
+ let started = waitForRecordingStartedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true
+ });
+ yield console.profile("rust");
+ yield started;
+ testRecordings(PerformanceController, [
+ CONSOLE + SELECTED + RECORDING
+ ]);
+
+ info("Recording 2 - Starting manual recording...");
+ yield startRecording(panel);
+ testRecordings(PerformanceController, [
+ CONSOLE + RECORDING,
+ MANUAL + RECORDING + SELECTED
+ ]);
+
+ info("Recording 3 - Starting console.profile(\"3\")...");
+ started = waitForRecordingStartedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true,
+ // only emitted when an in-progress recording is selected
+ skipWaitingForOverview: true,
+ // the view state won't switch to "console-recording" unless the new
+ // in-progress recording is selected, which won't happen
+ skipWaitingForViewState: true,
+ });
+ yield console.profile("3");
+ yield started;
+ testRecordings(PerformanceController, [
+ CONSOLE + RECORDING,
+ MANUAL + RECORDING + SELECTED,
+ CONSOLE + RECORDING
+ ]);
+
+ info("Recording 4 - Starting console.profile(\"4\")...");
+ started = waitForRecordingStartedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true,
+ // only emitted when an in-progress recording is selected
+ skipWaitingForOverview: true,
+ // the view state won't switch to "console-recording" unless the new
+ // in-progress recording is selected, which won't happen
+ skipWaitingForViewState: true,
+ });
+ yield console.profile("4");
+ yield started;
+ testRecordings(PerformanceController, [
+ CONSOLE + RECORDING,
+ MANUAL + RECORDING + SELECTED,
+ CONSOLE + RECORDING,
+ CONSOLE + RECORDING
+ ]);
+
+ info("Recording 4 - Ending console.profileEnd()...");
+ let stopped = waitForRecordingStoppedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true,
+ // only emitted when a finished recording is selected
+ skipWaitingForOverview: true,
+ skipWaitingForSubview: true,
+ // the view state won't switch to "recorded" unless the new
+ // finished recording is selected, which won't happen
+ skipWaitingForViewState: true,
+ });
+ yield console.profileEnd();
+ yield stopped;
+ testRecordings(PerformanceController, [
+ CONSOLE + RECORDING,
+ MANUAL + RECORDING + SELECTED,
+ CONSOLE + RECORDING,
+ CONSOLE
+ ]);
+
+ info("Recording 4 - Select last recording...");
+ let recordingSelected = once(PerformanceController, EVENTS.RECORDING_SELECTED);
+ setSelectedRecording(panel, 3);
+ yield recordingSelected;
+ testRecordings(PerformanceController, [
+ CONSOLE + RECORDING,
+ MANUAL + RECORDING,
+ CONSOLE + RECORDING,
+ CONSOLE + SELECTED
+ ]);
+ ok(!OverviewView.isRendering(),
+ "Stop rendering overview when a completed recording is selected.");
+
+ info("Recording 2 - Stop manual recording.");
+
+ yield stopRecording(panel);
+ testRecordings(PerformanceController, [
+ CONSOLE + RECORDING,
+ MANUAL + SELECTED,
+ CONSOLE + RECORDING,
+ CONSOLE
+ ]);
+ ok(!OverviewView.isRendering(),
+ "Stop rendering overview when a completed recording is selected.");
+
+ info("Recording 1 - Select first recording.");
+ recordingSelected = once(PerformanceController, EVENTS.RECORDING_SELECTED);
+ setSelectedRecording(panel, 0);
+ yield recordingSelected;
+ testRecordings(PerformanceController, [
+ CONSOLE + RECORDING + SELECTED,
+ MANUAL,
+ CONSOLE + RECORDING,
+ CONSOLE
+ ]);
+ ok(OverviewView.isRendering(),
+ "Should be rendering overview a recording in progress is selected.");
+
+ // Ensure overview is still rendering.
+ yield times(OverviewView, EVENTS.UI_OVERVIEW_RENDERED, 3, {
+ expectedArgs: { "1": Constants.FRAMERATE_GRAPH_LOW_RES_INTERVAL }
+ });
+
+ info("Ending console.profileEnd()...");
+ stopped = waitForRecordingStoppedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true,
+ // only emitted when a finished recording is selected
+ skipWaitingForOverview: true,
+ skipWaitingForSubview: true,
+ // the view state won't switch to "recorded" unless the new
+ // finished recording is selected, which won't happen
+ skipWaitingForViewState: true,
+ });
+ yield console.profileEnd();
+ yield stopped;
+ testRecordings(PerformanceController, [
+ CONSOLE + RECORDING + SELECTED,
+ MANUAL,
+ CONSOLE,
+ CONSOLE
+ ]);
+ ok(OverviewView.isRendering(),
+ "Should be rendering overview a recording in progress is selected.");
+
+ // Ensure overview is still rendering.
+ yield times(OverviewView, EVENTS.UI_OVERVIEW_RENDERED, 3, {
+ expectedArgs: { "1": Constants.FRAMERATE_GRAPH_LOW_RES_INTERVAL }
+ });
+
+ info("Recording 5 - Start one more manual recording.");
+ yield startRecording(panel);
+ testRecordings(PerformanceController, [
+ CONSOLE + RECORDING,
+ MANUAL,
+ CONSOLE,
+ CONSOLE,
+ MANUAL + RECORDING + SELECTED
+ ]);
+ ok(OverviewView.isRendering(),
+ "Should be rendering overview a recording in progress is selected.");
+
+ // Ensure overview is still rendering.
+ yield times(OverviewView, EVENTS.UI_OVERVIEW_RENDERED, 3, {
+ expectedArgs: { "1": Constants.FRAMERATE_GRAPH_LOW_RES_INTERVAL }
+ });
+
+ info("Recording 5 - Stop manual recording.");
+ yield stopRecording(panel);
+ testRecordings(PerformanceController, [
+ CONSOLE + RECORDING,
+ MANUAL,
+ CONSOLE,
+ CONSOLE,
+ MANUAL + SELECTED
+ ]);
+ ok(!OverviewView.isRendering(),
+ "Stop rendering overview when a completed recording is selected.");
+
+ info("Recording 1 - Ending console.profileEnd()...");
+ stopped = waitForRecordingStoppedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true,
+ // only emitted when a finished recording is selected
+ skipWaitingForOverview: true,
+ skipWaitingForSubview: true,
+ // the view state won't switch to "recorded" unless the new
+ // in-progress recording is selected, which won't happen
+ skipWaitingForViewState: true,
+ });
+ yield console.profileEnd();
+ yield stopped;
+ testRecordings(PerformanceController, [
+ CONSOLE,
+ MANUAL,
+ CONSOLE,
+ CONSOLE,
+ MANUAL + SELECTED
+ ]);
+ ok(!OverviewView.isRendering(),
+ "Stop rendering overview when a completed recording is selected.");
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
+
+function testRecordings(controller, expectedBitFlags) {
+ let recordings = controller.getRecordings();
+ let current = controller.getCurrentRecording();
+ is(recordings.length, expectedBitFlags.length, "Expected number of recordings.");
+
+ recordings.forEach((recording, i) => {
+ const expected = expectedBitFlags[i];
+ is(recording.isConsole(), hasBitFlag(expected, CONSOLE),
+ `Recording ${i + 1} has expected console state.`);
+ is(recording.isRecording(), hasBitFlag(expected, RECORDING),
+ `Recording ${i + 1} has expected console state.`);
+ is((recording == current), hasBitFlag(expected, SELECTED),
+ `Recording ${i + 1} has expected selected state.`);
+ });
+}
diff --git a/devtools/client/performance/test/browser_perf-console-record-09.js b/devtools/client/performance/test/browser_perf-console-record-09.js
new file mode 100644
index 000000000..06c14faa5
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-console-record-09.js
@@ -0,0 +1,64 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that an error is not thrown when clearing out the recordings if there's
+ * an in-progress console profile and that console profiles are not cleared
+ * if in progress.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { initPerformanceInTab, initConsoleInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+const { waitForRecordingStartedEvents } = require("devtools/client/performance/test/helpers/actions");
+const { idleWait } = require("devtools/client/performance/test/helpers/wait-utils");
+
+add_task(function* () {
+ let { target, console } = yield initConsoleInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { panel } = yield initPerformanceInTab({ tab: target.tab });
+ let { PerformanceController } = panel.panelWin;
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ info("Starting console.profile()...");
+ let started = waitForRecordingStartedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true,
+ // only emitted when an in-progress recording is selected
+ skipWaitingForOverview: true,
+ // the view state won't switch to "console-recording" unless the new
+ // in-progress recording is selected, which won't happen
+ skipWaitingForViewState: true,
+ });
+ yield console.profile();
+ yield started;
+
+ yield PerformanceController.clearRecordings();
+ let recordings = PerformanceController.getRecordings();
+ is(recordings.length, 1, "One recording found in the performance panel.");
+ is(recordings[0].isConsole(), true, "Recording came from console.profile.");
+ is(recordings[0].getLabel(), "", "Correct label in the recording model.");
+ is(PerformanceController.getCurrentRecording(), recordings[0],
+ "There current recording should be the first one.");
+
+ info("Attempting to end console.profileEnd()...");
+ yield console.profileEnd();
+ yield idleWait(1000);
+
+ ok(true,
+ "Stopping an in-progress console profile after clearing recordings does not throw.");
+
+ yield PerformanceController.clearRecordings();
+ recordings = PerformanceController.getRecordings();
+ is(recordings.length, 0, "No recordings found");
+ is(PerformanceController.getCurrentRecording(), null,
+ "There should be no current recording.");
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-details-01-toggle.js b/devtools/client/performance/test/browser_perf-details-01-toggle.js
new file mode 100644
index 000000000..8cc7f9e0c
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-details-01-toggle.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the details view toggles subviews.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+const { once } = require("devtools/client/performance/test/helpers/event-utils");
+const { command } = require("devtools/client/performance/test/helpers/input-utils");
+
+add_task(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { EVENTS, $, DetailsView } = panel.panelWin;
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ info("Checking views on startup.");
+ checkViews(DetailsView, $, "waterfall");
+
+ // Select calltree view.
+ let viewChanged = once(DetailsView, EVENTS.UI_DETAILS_VIEW_SELECTED,
+ { spreadArgs: true });
+ command($("toolbarbutton[data-view='js-calltree']"));
+ let [, viewName] = yield viewChanged;
+ is(viewName, "js-calltree", "UI_DETAILS_VIEW_SELECTED fired with view name");
+ checkViews(DetailsView, $, "js-calltree");
+
+ // Select js flamegraph view.
+ viewChanged = once(DetailsView, EVENTS.UI_DETAILS_VIEW_SELECTED, { spreadArgs: true });
+ command($("toolbarbutton[data-view='js-flamegraph']"));
+ [, viewName] = yield viewChanged;
+ is(viewName, "js-flamegraph", "UI_DETAILS_VIEW_SELECTED fired with view name");
+ checkViews(DetailsView, $, "js-flamegraph");
+
+ // Select waterfall view.
+ viewChanged = once(DetailsView, EVENTS.UI_DETAILS_VIEW_SELECTED, { spreadArgs: true });
+ command($("toolbarbutton[data-view='waterfall']"));
+ [, viewName] = yield viewChanged;
+ is(viewName, "waterfall", "UI_DETAILS_VIEW_SELECTED fired with view name");
+ checkViews(DetailsView, $, "waterfall");
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
+
+function checkViews(DetailsView, $, currentView) {
+ for (let viewName in DetailsView.components) {
+ let button = $(`toolbarbutton[data-view="${viewName}"]`);
+
+ is(DetailsView.el.selectedPanel.id, DetailsView.components[currentView].id,
+ `DetailsView correctly has ${currentView} selected.`);
+
+ if (viewName == currentView) {
+ ok(button.getAttribute("checked"), `${viewName} button checked.`);
+ } else {
+ ok(!button.getAttribute("checked"), `${viewName} button not checked.`);
+ }
+ }
+}
diff --git a/devtools/client/performance/test/browser_perf-details-02-utility-fun.js b/devtools/client/performance/test/browser_perf-details-02-utility-fun.js
new file mode 100644
index 000000000..5914742dd
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-details-02-utility-fun.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the details view utility functions work as advertised.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+const { once } = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let {
+ EVENTS,
+ DetailsView,
+ WaterfallView,
+ JsCallTreeView,
+ JsFlameGraphView
+ } = panel.panelWin;
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ ok(DetailsView.isViewSelected(WaterfallView),
+ "The waterfall view is selected by default in the details view.");
+
+ // Select js calltree view.
+ let selected = once(DetailsView, EVENTS.UI_DETAILS_VIEW_SELECTED);
+ yield DetailsView.selectView("js-calltree");
+ yield selected;
+
+ ok(DetailsView.isViewSelected(JsCallTreeView),
+ "The js calltree view is now selected in the details view.");
+
+ // Select js flamegraph view.
+ selected = once(DetailsView, EVENTS.UI_DETAILS_VIEW_SELECTED);
+ yield DetailsView.selectView("js-flamegraph");
+ yield selected;
+
+ ok(DetailsView.isViewSelected(JsFlameGraphView),
+ "The js flamegraph view is now selected in the details view.");
+
+ // Select waterfall view.
+ selected = once(DetailsView, EVENTS.UI_DETAILS_VIEW_SELECTED);
+ yield DetailsView.selectView("waterfall");
+ yield selected;
+
+ ok(DetailsView.isViewSelected(WaterfallView),
+ "The waterfall view is now selected in the details view.");
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-details-03-without-allocations.js b/devtools/client/performance/test/browser_perf-details-03-without-allocations.js
new file mode 100644
index 000000000..c69c1de9f
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-details-03-without-allocations.js
@@ -0,0 +1,127 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the details view hides the allocations buttons when a recording
+ * does not have allocations data ("withAllocations": false), and that when an
+ * allocations panel is selected to a panel that does not have allocations goes
+ * to a default panel instead.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { UI_ENABLE_ALLOCATIONS_PREF } = require("devtools/client/performance/test/helpers/prefs");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+const { once } = require("devtools/client/performance/test/helpers/event-utils");
+const { setSelectedRecording } = require("devtools/client/performance/test/helpers/recording-utils");
+
+add_task(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let {
+ EVENTS,
+ $,
+ DetailsView,
+ WaterfallView,
+ MemoryCallTreeView,
+ MemoryFlameGraphView
+ } = panel.panelWin;
+
+ let flameBtn = $("toolbarbutton[data-view='memory-flamegraph']");
+ let callBtn = $("toolbarbutton[data-view='memory-calltree']");
+
+ // Disable allocations to prevent recording them.
+ Services.prefs.setBoolPref(UI_ENABLE_ALLOCATIONS_PREF, false);
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ ok(DetailsView.isViewSelected(WaterfallView),
+ "The waterfall view is selected by default in the details view.");
+
+ // Re-enable allocations to test.
+ Services.prefs.setBoolPref(UI_ENABLE_ALLOCATIONS_PREF, true);
+
+ // The toolbar buttons will always be hidden when a recording isn't available,
+ // so make sure we have one that's finished.
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ ok(DetailsView.isViewSelected(WaterfallView),
+ "The waterfall view is still selected in the details view.");
+
+ is(callBtn.hidden, false,
+ "The `memory-calltree` button is shown when recording has memory data.");
+ is(flameBtn.hidden, false,
+ "The `memory-flamegraph` button is shown when recording has memory data.");
+
+ let selected = once(DetailsView, EVENTS.UI_DETAILS_VIEW_SELECTED);
+ let rendered = once(MemoryCallTreeView, EVENTS.UI_MEMORY_CALL_TREE_RENDERED);
+ DetailsView.selectView("memory-calltree");
+ yield selected;
+ yield rendered;
+
+ ok(DetailsView.isViewSelected(MemoryCallTreeView),
+ "The memory call tree view can now be selected.");
+
+ selected = once(DetailsView, EVENTS.UI_DETAILS_VIEW_SELECTED);
+ rendered = once(MemoryFlameGraphView, EVENTS.UI_MEMORY_FLAMEGRAPH_RENDERED);
+ DetailsView.selectView("memory-flamegraph");
+ yield selected;
+ yield rendered;
+
+ ok(DetailsView.isViewSelected(MemoryFlameGraphView),
+ "The memory flamegraph view can now be selected.");
+
+ // Select the first recording with no memory data.
+ selected = once(DetailsView, EVENTS.UI_DETAILS_VIEW_SELECTED);
+ rendered = once(WaterfallView, EVENTS.UI_WATERFALL_RENDERED);
+ setSelectedRecording(panel, 0);
+ yield selected;
+ yield rendered;
+
+ ok(DetailsView.isViewSelected(WaterfallView), "The waterfall view is now selected " +
+ "when switching back to a recording that does not have memory data.");
+
+ is(callBtn.hidden, true,
+ "The `memory-calltree` button is hidden when recording has no memory data.");
+ is(flameBtn.hidden, true,
+ "The `memory-flamegraph` button is hidden when recording has no memory data.");
+
+ // Go back to the recording with memory data.
+ rendered = once(WaterfallView, EVENTS.UI_WATERFALL_RENDERED);
+ setSelectedRecording(panel, 1);
+ yield rendered;
+
+ ok(DetailsView.isViewSelected(WaterfallView),
+ "The waterfall view is still selected in the details view.");
+
+ is(callBtn.hidden, false,
+ "The `memory-calltree` button is shown when recording has memory data.");
+ is(flameBtn.hidden, false,
+ "The `memory-flamegraph` button is shown when recording has memory data.");
+
+ selected = once(DetailsView, EVENTS.UI_DETAILS_VIEW_SELECTED);
+ rendered = once(MemoryCallTreeView, EVENTS.UI_MEMORY_CALL_TREE_RENDERED);
+ DetailsView.selectView("memory-calltree");
+ yield selected;
+ yield rendered;
+
+ ok(DetailsView.isViewSelected(MemoryCallTreeView), "The memory call tree view can be " +
+ "selected again after going back to the view with memory data.");
+
+ selected = once(DetailsView, EVENTS.UI_DETAILS_VIEW_SELECTED);
+ rendered = once(MemoryFlameGraphView, EVENTS.UI_MEMORY_FLAMEGRAPH_RENDERED);
+ DetailsView.selectView("memory-flamegraph");
+ yield selected;
+ yield rendered;
+
+ ok(DetailsView.isViewSelected(MemoryFlameGraphView), "The memory flamegraph view can " +
+ "be selected again after going back to the view with memory data.");
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-details-04-toolbar-buttons.js b/devtools/client/performance/test/browser_perf-details-04-toolbar-buttons.js
new file mode 100644
index 000000000..9dec9fe7c
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-details-04-toolbar-buttons.js
@@ -0,0 +1,145 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the details view hides the toolbar buttons when a recording
+ * doesn't exist or is in progress.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+const { once } = require("devtools/client/performance/test/helpers/event-utils");
+const { setSelectedRecording, getSelectedRecordingIndex } = require("devtools/client/performance/test/helpers/recording-utils");
+
+add_task(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let {
+ EVENTS,
+ $,
+ PerformanceController,
+ WaterfallView
+ } = panel.panelWin;
+
+ let waterfallBtn = $("toolbarbutton[data-view='waterfall']");
+ let jsFlameBtn = $("toolbarbutton[data-view='js-flamegraph']");
+ let jsCallBtn = $("toolbarbutton[data-view='js-calltree']");
+ let memFlameBtn = $("toolbarbutton[data-view='memory-flamegraph']");
+ let memCallBtn = $("toolbarbutton[data-view='memory-calltree']");
+
+ is(waterfallBtn.hidden, true,
+ "The `waterfall` button is hidden when tool starts.");
+ is(jsFlameBtn.hidden, true,
+ "The `js-flamegraph` button is hidden when tool starts.");
+ is(jsCallBtn.hidden, true,
+ "The `js-calltree` button is hidden when tool starts.");
+ is(memFlameBtn.hidden, true,
+ "The `memory-flamegraph` button is hidden when tool starts.");
+ is(memCallBtn.hidden, true,
+ "The `memory-calltree` button is hidden when tool starts.");
+
+ yield startRecording(panel);
+
+ is(waterfallBtn.hidden, true,
+ "The `waterfall` button is hidden when recording starts.");
+ is(jsFlameBtn.hidden, true,
+ "The `js-flamegraph` button is hidden when recording starts.");
+ is(jsCallBtn.hidden, true,
+ "The `js-calltree` button is hidden when recording starts.");
+ is(memFlameBtn.hidden, true,
+ "The `memory-flamegraph` button is hidden when recording starts.");
+ is(memCallBtn.hidden, true,
+ "The `memory-calltree` button is hidden when recording starts.");
+
+ yield stopRecording(panel);
+
+ is(waterfallBtn.hidden, false,
+ "The `waterfall` button is visible when recording ends.");
+ is(jsFlameBtn.hidden, false,
+ "The `js-flamegraph` button is visible when recording ends.");
+ is(jsCallBtn.hidden, false,
+ "The `js-calltree` button is visible when recording ends.");
+ is(memFlameBtn.hidden, true,
+ "The `memory-flamegraph` button is hidden when recording ends.");
+ is(memCallBtn.hidden, true,
+ "The `memory-calltree` button is hidden when recording ends.");
+
+ yield startRecording(panel);
+
+ is(waterfallBtn.hidden, true,
+ "The `waterfall` button is hidden when another recording starts.");
+ is(jsFlameBtn.hidden, true,
+ "The `js-flamegraph` button is hidden when another recording starts.");
+ is(jsCallBtn.hidden, true,
+ "The `js-calltree` button is hidden when another recording starts.");
+ is(memFlameBtn.hidden, true,
+ "The `memory-flamegraph` button is hidden when another recording starts.");
+ is(memCallBtn.hidden, true,
+ "The `memory-calltree` button is hidden when another recording starts.");
+
+ let selected = once(PerformanceController, EVENTS.RECORDING_SELECTED);
+ let rendered = once(WaterfallView, EVENTS.UI_WATERFALL_RENDERED);
+ setSelectedRecording(panel, 0);
+ yield selected;
+ yield rendered;
+
+ let selectedIndex = getSelectedRecordingIndex(panel);
+ is(selectedIndex, 0,
+ "The first recording was selected again.");
+
+ is(waterfallBtn.hidden, false,
+ "The `waterfall` button is visible when first recording selected.");
+ is(jsFlameBtn.hidden, false,
+ "The `js-flamegraph` button is visible when first recording selected.");
+ is(jsCallBtn.hidden, false,
+ "The `js-calltree` button is visible when first recording selected.");
+ is(memFlameBtn.hidden, true,
+ "The `memory-flamegraph` button is hidden when first recording selected.");
+ is(memCallBtn.hidden, true,
+ "The `memory-calltree` button is hidden when first recording selected.");
+
+ selected = once(PerformanceController, EVENTS.RECORDING_SELECTED);
+ setSelectedRecording(panel, 1);
+ yield selected;
+
+ selectedIndex = getSelectedRecordingIndex(panel);
+ is(selectedIndex, 1,
+ "The second recording was selected again.");
+
+ is(waterfallBtn.hidden, true,
+ "The `waterfall button` still is hidden when second recording selected.");
+ is(jsFlameBtn.hidden, true,
+ "The `js-flamegraph button` still is hidden when second recording selected.");
+ is(jsCallBtn.hidden, true,
+ "The `js-calltree button` still is hidden when second recording selected.");
+ is(memFlameBtn.hidden, true,
+ "The `memory-flamegraph button` still is hidden when second recording selected.");
+ is(memCallBtn.hidden, true,
+ "The `memory-calltree button` still is hidden when second recording selected.");
+
+ rendered = once(WaterfallView, EVENTS.UI_WATERFALL_RENDERED);
+ yield stopRecording(panel);
+ yield rendered;
+
+ selectedIndex = getSelectedRecordingIndex(panel);
+ is(selectedIndex, 1,
+ "The second recording is still selected.");
+
+ is(waterfallBtn.hidden, false,
+ "The `waterfall` button is visible when second recording finished.");
+ is(jsFlameBtn.hidden, false,
+ "The `js-flamegraph` button is visible when second recording finished.");
+ is(jsCallBtn.hidden, false,
+ "The `js-calltree` button is visible when second recording finished.");
+ is(memFlameBtn.hidden, true,
+ "The `memory-flamegraph` button is hidden when second recording finished.");
+ is(memCallBtn.hidden, true,
+ "The `memory-calltree` button is hidden when second recording finished.");
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-details-05-preserve-view.js b/devtools/client/performance/test/browser_perf-details-05-preserve-view.js
new file mode 100644
index 000000000..00c71db7e
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-details-05-preserve-view.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the same details view is selected after recordings are cleared
+ * and a new recording starts.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+const { once } = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { EVENTS, PerformanceController, DetailsView, JsCallTreeView } = panel.panelWin;
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ let selected = once(DetailsView, EVENTS.UI_DETAILS_VIEW_SELECTED);
+ let rendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
+ yield DetailsView.selectView("js-calltree");
+ yield selected;
+ yield rendered;
+
+ ok(DetailsView.isViewSelected(JsCallTreeView),
+ "The js calltree view is now selected in the details view.");
+
+ let cleared = once(PerformanceController, EVENTS.RECORDING_SELECTED,
+ { expectedArgs: { "1": null } });
+ yield PerformanceController.clearRecordings();
+ yield cleared;
+
+ yield startRecording(panel);
+ yield stopRecording(panel, {
+ expectedViewClass: "JsCallTreeView",
+ expectedViewEvent: "UI_JS_CALL_TREE_RENDERED"
+ });
+
+ ok(DetailsView.isViewSelected(JsCallTreeView),
+ "The js calltree view is still selected in the details view.");
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-details-06-rerender-on-selection.js b/devtools/client/performance/test/browser_perf-details-06-rerender-on-selection.js
new file mode 100644
index 000000000..abe2dfc75
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-details-06-rerender-on-selection.js
@@ -0,0 +1,79 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that when flame chart views scroll to change selection,
+ * other detail views are rerendered.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+const { once } = require("devtools/client/performance/test/helpers/event-utils");
+const { scrollCanvasGraph, HORIZONTAL_AXIS } = require("devtools/client/performance/test/helpers/input-utils");
+
+add_task(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let {
+ EVENTS,
+ OverviewView,
+ DetailsView,
+ WaterfallView,
+ JsCallTreeView,
+ JsFlameGraphView
+ } = panel.panelWin;
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ let waterfallRendered = once(WaterfallView, EVENTS.UI_WATERFALL_RENDERED);
+ OverviewView.setTimeInterval({ startTime: 10, endTime: 20 });
+ yield waterfallRendered;
+
+ // Select the call tree to make sure it's initialized and ready to receive
+ // redrawing requests once reselected.
+ let callTreeRendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
+ yield DetailsView.selectView("js-calltree");
+ yield callTreeRendered;
+
+ // Switch to the flamegraph and perform a scroll over the visualization.
+ // The waterfall and call tree should get rerendered when reselected.
+ let flamegraphRendered = once(JsFlameGraphView, EVENTS.UI_JS_FLAMEGRAPH_RENDERED);
+ yield DetailsView.selectView("js-flamegraph");
+ yield flamegraphRendered;
+
+ let overviewRangeSelected = once(OverviewView, EVENTS.UI_OVERVIEW_RANGE_SELECTED);
+ let waterfallRerendered = once(WaterfallView, EVENTS.UI_WATERFALL_RENDERED);
+ let callTreeRerendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
+
+ once(JsFlameGraphView, EVENTS.UI_JS_FLAMEGRAPH_RENDERED).then(() => {
+ ok(false, "FlameGraphView should not publicly rerender, the internal state " +
+ "and drawing should be handled by the underlying widget.");
+ });
+
+ // Reset the range to full view, trigger a "selection" event as if
+ // our mouse has done this
+ scrollCanvasGraph(JsFlameGraphView.graph, {
+ axis: HORIZONTAL_AXIS,
+ wheel: 200,
+ x: 10
+ });
+
+ yield overviewRangeSelected;
+ ok(true, "Overview range was changed.");
+
+ yield DetailsView.selectView("waterfall");
+ yield waterfallRerendered;
+ ok(true, "Waterfall rerendered by flame graph changing interval.");
+
+ yield DetailsView.selectView("js-calltree");
+ yield callTreeRerendered;
+ ok(true, "CallTree rerendered by flame graph changing interval.");
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-details-07-bleed-events.js b/devtools/client/performance/test/browser_perf-details-07-bleed-events.js
new file mode 100644
index 000000000..f299aadad
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-details-07-bleed-events.js
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that events don't bleed between detail views.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+const { once } = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { EVENTS, DetailsView, JsCallTreeView } = panel.panelWin;
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ // The waterfall should render by default, and we want to make
+ // sure that the render events don't bleed between detail views
+ // so test that's the case after both views have been created.
+ let callTreeRendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
+ yield DetailsView.selectView("js-calltree");
+ yield callTreeRendered;
+
+ let waterfallSelected = once(DetailsView, EVENTS.UI_DETAILS_VIEW_SELECTED);
+ yield DetailsView.selectView("waterfall");
+ yield waterfallSelected;
+
+ once(JsCallTreeView, EVENTS.UI_WATERFALL_RENDERED).then(() =>
+ ok(false, "JsCallTreeView should not receive UI_WATERFALL_RENDERED event."));
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ let callTreeRerendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
+ yield DetailsView.selectView("js-calltree");
+ yield callTreeRerendered;
+
+ ok(true, "Test passed.");
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-details-render-00-waterfall.js b/devtools/client/performance/test/browser_perf-details-render-00-waterfall.js
new file mode 100644
index 000000000..5f65fa00d
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-details-render-00-waterfall.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the waterfall view renders content after recording.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+
+add_task(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { DetailsView, WaterfallView } = panel.panelWin;
+
+ yield startRecording(panel);
+ // Already waits for EVENTS.UI_WATERFALL_RENDERED.
+ yield stopRecording(panel);
+
+ ok(DetailsView.isViewSelected(WaterfallView),
+ "The waterfall view is selected by default in the details view.");
+
+ ok(true, "WaterfallView rendered after recording is stopped.");
+
+ yield startRecording(panel);
+ // Already waits for EVENTS.UI_WATERFALL_RENDERED.
+ yield stopRecording(panel);
+
+ ok(DetailsView.isViewSelected(WaterfallView),
+ "The waterfall view is still selected in the details view.");
+
+ ok(true, "WaterfallView rendered again after recording completed a second time.");
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-details-render-01-js-calltree.js b/devtools/client/performance/test/browser_perf-details-render-01-js-calltree.js
new file mode 100644
index 000000000..bc191f2fc
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-details-render-01-js-calltree.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the js call tree view renders content after recording.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+const { once } = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { EVENTS, DetailsView, JsCallTreeView } = panel.panelWin;
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ let rendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
+ yield DetailsView.selectView("js-calltree");
+ yield rendered;
+
+ ok(true, "JsCallTreeView rendered after recording is stopped.");
+
+ yield startRecording(panel);
+ yield stopRecording(panel, {
+ expectedViewClass: "JsCallTreeView",
+ expectedViewEvent: "UI_JS_CALL_TREE_RENDERED"
+ });
+
+ ok(true, "JsCallTreeView rendered again after recording completed a second time.");
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-details-render-02-js-flamegraph.js b/devtools/client/performance/test/browser_perf-details-render-02-js-flamegraph.js
new file mode 100644
index 000000000..e5e74fc7c
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-details-render-02-js-flamegraph.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the js flamegraph view renders content after recording.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+const { once } = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { EVENTS, DetailsView, JsFlameGraphView } = panel.panelWin;
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ let rendered = once(JsFlameGraphView, EVENTS.UI_JS_FLAMEGRAPH_RENDERED);
+ yield DetailsView.selectView("js-flamegraph");
+ yield rendered;
+
+ ok(true, "JsFlameGraphView rendered after recording is stopped.");
+
+ yield startRecording(panel);
+ yield stopRecording(panel, {
+ expectedViewClass: "JsFlameGraphView",
+ expectedViewEvent: "UI_JS_FLAMEGRAPH_RENDERED"
+ });
+
+ ok(true, "JsFlameGraphView rendered again after recording completed a second time.");
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-details-render-03-memory-calltree.js b/devtools/client/performance/test/browser_perf-details-render-03-memory-calltree.js
new file mode 100644
index 000000000..758dea8c6
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-details-render-03-memory-calltree.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the memory call tree view renders content after recording.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { UI_ENABLE_ALLOCATIONS_PREF } = require("devtools/client/performance/test/helpers/prefs");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+const { once } = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { EVENTS, DetailsView, MemoryCallTreeView } = panel.panelWin;
+
+ // Enable allocations to test.
+ Services.prefs.setBoolPref(UI_ENABLE_ALLOCATIONS_PREF, true);
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ let rendered = once(MemoryCallTreeView, EVENTS.UI_MEMORY_CALL_TREE_RENDERED);
+ yield DetailsView.selectView("memory-calltree");
+ yield rendered;
+
+ ok(true, "MemoryCallTreeView rendered after recording is stopped.");
+
+ yield startRecording(panel);
+ yield stopRecording(panel, {
+ expectedViewClass: "MemoryCallTreeView",
+ expectedViewEvent: "UI_MEMORY_CALL_TREE_RENDERED"
+ });
+
+ ok(true, "MemoryCallTreeView rendered again after recording completed a second time.");
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-details-render-04-memory-flamegraph.js b/devtools/client/performance/test/browser_perf-details-render-04-memory-flamegraph.js
new file mode 100644
index 000000000..119f090e5
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-details-render-04-memory-flamegraph.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the memory call tree view renders content after recording.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { UI_ENABLE_ALLOCATIONS_PREF } = require("devtools/client/performance/test/helpers/prefs");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+const { once } = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { EVENTS, DetailsView, MemoryFlameGraphView } = panel.panelWin;
+
+ // Enable allocations to test.
+ Services.prefs.setBoolPref(UI_ENABLE_ALLOCATIONS_PREF, true);
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ let rendered = once(MemoryFlameGraphView, EVENTS.UI_MEMORY_FLAMEGRAPH_RENDERED);
+ yield DetailsView.selectView("memory-flamegraph");
+ yield rendered;
+
+ ok(true, "MemoryFlameGraphView rendered after recording is stopped.");
+
+ yield startRecording(panel);
+ yield stopRecording(panel, {
+ expectedViewClass: "MemoryFlameGraphView",
+ expectedViewEvent: "UI_MEMORY_FLAMEGRAPH_RENDERED"
+ });
+
+ ok(true,
+ "MemoryFlameGraphView rendered again after recording completed a second time.");
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-docload.js b/devtools/client/performance/test/browser_perf-docload.js
new file mode 100644
index 000000000..b92a8cfbd
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-docload.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if the sidebar is updated with "DOMContentLoaded" and "load" markers.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording, reload } = require("devtools/client/performance/test/helpers/actions");
+const { waitUntil } = require("devtools/client/performance/test/helpers/wait-utils");
+
+add_task(function* () {
+ let { panel, target } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { PerformanceController } = panel.panelWin;
+
+ yield startRecording(panel);
+ yield reload(target);
+
+ yield waitUntil(() => {
+ // Wait until we get the necessary markers.
+ let markers = PerformanceController.getCurrentRecording().getMarkers();
+ if (!markers.some(m => m.name == "document::DOMContentLoaded") ||
+ !markers.some(m => m.name == "document::Load")) {
+ return false;
+ }
+
+ ok(markers.filter(m => m.name == "document::DOMContentLoaded").length == 1,
+ "There should only be one `DOMContentLoaded` marker.");
+ ok(markers.filter(m => m.name == "document::Load").length == 1,
+ "There should only be one `load` marker.");
+
+ return true;
+ });
+
+ yield stopRecording(panel);
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-gc-snap.js b/devtools/client/performance/test/browser_perf-gc-snap.js
new file mode 100644
index 000000000..57589825e
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-gc-snap.js
@@ -0,0 +1,146 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+/* eslint-disable */
+/**
+ * Tests that the marker details on GC markers displays allocation
+ * buttons and snaps to the correct range
+ */
+function* spawnTest() {
+ let { panel } = yield initPerformance(ALLOCS_URL);
+ let { $, $$, EVENTS, PerformanceController, OverviewView, DetailsView, WaterfallView, MemoryCallTreeView } = panel.panelWin;
+ let EPSILON = 0.00001;
+
+ Services.prefs.setBoolPref(ALLOCATIONS_PREF, true);
+
+ yield startRecording(panel);
+ yield idleWait(1000);
+ yield stopRecording(panel);
+
+ injectGCMarkers(PerformanceController, WaterfallView);
+
+ // Select everything
+ let rendered = WaterfallView.once(EVENTS.UI_WATERFALL_RENDERED);
+ OverviewView.setTimeInterval({ startTime: 0, endTime: Number.MAX_VALUE });
+ yield rendered;
+
+ let bars = $$(".waterfall-marker-bar");
+ let gcMarkers = PerformanceController.getCurrentRecording().getMarkers();
+ ok(gcMarkers.length === 9, "should have 9 GC markers");
+ ok(bars.length === 9, "should have 9 GC markers rendered");
+
+ /**
+ * Check when it's the second marker of the first GC cycle.
+ */
+
+ let targetMarker = gcMarkers[1];
+ let targetBar = bars[1];
+ info(`Clicking GC Marker of type ${targetMarker.causeName} ${targetMarker.start}:${targetMarker.end}`);
+ EventUtils.sendMouseEvent({ type: "mousedown" }, targetBar);
+ let showAllocsButton;
+ // On slower machines this can not be found immediately?
+ yield waitUntil(() => showAllocsButton = $("#waterfall-details .custom-button[type='show-allocations']"));
+ ok(showAllocsButton, "GC buttons when allocations are enabled");
+
+ rendered = once(MemoryCallTreeView, EVENTS.UI_MEMORY_CALL_TREE_RENDERED);
+ EventUtils.sendMouseEvent({ type: "click" }, showAllocsButton);
+ yield rendered;
+
+ is(OverviewView.getTimeInterval().startTime, 0, "When clicking first GC, should use 0 as start time");
+ within(OverviewView.getTimeInterval().endTime, targetMarker.start, EPSILON, "Correct end time range");
+
+ let duration = PerformanceController.getCurrentRecording().getDuration();
+ rendered = once(WaterfallView, EVENTS.UI_WATERFALL_RENDERED);
+ OverviewView.setTimeInterval({ startTime: 0, endTime: duration });
+ yield DetailsView.selectView("waterfall");
+ yield rendered;
+
+ /**
+ * Check when there is a previous GC cycle
+ */
+
+ bars = $$(".waterfall-marker-bar");
+ targetMarker = gcMarkers[4];
+ targetBar = bars[4];
+
+ info(`Clicking GC Marker of type ${targetMarker.causeName} ${targetMarker.start}:${targetMarker.end}`);
+ EventUtils.sendMouseEvent({ type: "mousedown" }, targetBar);
+ // On slower machines this can not be found immediately?
+ yield waitUntil(() => showAllocsButton = $("#waterfall-details .custom-button[type='show-allocations']"));
+ ok(showAllocsButton, "GC buttons when allocations are enabled");
+
+ rendered = once(MemoryCallTreeView, EVENTS.UI_MEMORY_CALL_TREE_RENDERED);
+ EventUtils.sendMouseEvent({ type: "click" }, showAllocsButton);
+ yield rendered;
+
+ within(OverviewView.getTimeInterval().startTime, gcMarkers[2].end, EPSILON,
+ "selection start range is last marker from previous GC cycle.");
+ within(OverviewView.getTimeInterval().endTime, targetMarker.start, EPSILON,
+ "selection end range is current GC marker's start time");
+
+ /**
+ * Now with allocations disabled
+ */
+
+ // Reselect the entire recording -- due to bug 1196945, the new recording
+ // won't reset the selection
+ duration = PerformanceController.getCurrentRecording().getDuration();
+ rendered = once(WaterfallView, EVENTS.UI_WATERFALL_RENDERED);
+ OverviewView.setTimeInterval({ startTime: 0, endTime: duration });
+ yield rendered;
+
+ Services.prefs.setBoolPref(ALLOCATIONS_PREF, false);
+ yield startRecording(panel);
+ rendered = once(WaterfallView, EVENTS.UI_WATERFALL_RENDERED);
+ yield stopRecording(panel);
+ yield rendered;
+
+ injectGCMarkers(PerformanceController, WaterfallView);
+
+ // Select everything
+ rendered = WaterfallView.once(EVENTS.UI_WATERFALL_RENDERED);
+ OverviewView.setTimeInterval({ startTime: 0, endTime: Number.MAX_VALUE });
+ yield rendered;
+
+ ok(true, "WaterfallView rendered after recording is stopped.");
+
+ bars = $$(".waterfall-marker-bar");
+ gcMarkers = PerformanceController.getCurrentRecording().getMarkers();
+
+ EventUtils.sendMouseEvent({ type: "mousedown" }, bars[0]);
+ showAllocsButton = $("#waterfall-details .custom-button[type='show-allocations']");
+ ok(!showAllocsButton, "No GC buttons when allocations are disabled");
+
+
+ yield teardown(panel);
+ finish();
+}
+
+function injectGCMarkers(controller, waterfall) {
+ // Push some fake GC markers into the recording
+ let realMarkers = controller.getCurrentRecording().getMarkers();
+ // Invalidate marker cache
+ waterfall._cache.delete(realMarkers);
+ realMarkers.length = 0;
+ for (let gcMarker of GC_MARKERS) {
+ realMarkers.push(gcMarker);
+ }
+}
+
+var GC_MARKERS = [
+ { causeName: "TOO_MUCH_MALLOC", cycle: 1 },
+ { causeName: "TOO_MUCH_MALLOC", cycle: 1 },
+ { causeName: "TOO_MUCH_MALLOC", cycle: 1 },
+ { causeName: "ALLOC_TRIGGER", cycle: 2 },
+ { causeName: "ALLOC_TRIGGER", cycle: 2 },
+ { causeName: "ALLOC_TRIGGER", cycle: 2 },
+ { causeName: "SET_NEW_DOCUMENT", cycle: 3 },
+ { causeName: "SET_NEW_DOCUMENT", cycle: 3 },
+ { causeName: "SET_NEW_DOCUMENT", cycle: 3 },
+].map((marker, i) => {
+ marker.name = "GarbageCollection";
+ marker.start = 50 + (i * 10);
+ marker.end = marker.start + 9;
+ return marker;
+});
+/* eslint-enable */
diff --git a/devtools/client/performance/test/browser_perf-highlighted.js b/devtools/client/performance/test/browser_perf-highlighted.js
new file mode 100644
index 000000000..72ad90547
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-highlighted.js
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the toolbox tab for performance is highlighted when recording,
+ * whether already loaded, or via console.profile with an unloaded performance tools.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { initPerformanceInTab, initConsoleInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+const { waitUntil } = require("devtools/client/performance/test/helpers/wait-utils");
+
+add_task(function* () {
+ let { target, toolbox, console } = yield initConsoleInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let tab = toolbox.doc.getElementById("toolbox-tab-performance");
+
+ yield console.profile("rust");
+ yield waitUntil(() => tab.hasAttribute("highlighted"));
+
+ ok(tab.hasAttribute("highlighted"), "Performance tab is highlighted during recording " +
+ "from console.profile when unloaded.");
+
+ yield console.profileEnd("rust");
+ yield waitUntil(() => !tab.hasAttribute("highlighted"));
+
+ ok(!tab.hasAttribute("highlighted"),
+ "Performance tab is no longer highlighted when console.profile recording finishes.");
+
+ let { panel } = yield initPerformanceInTab({ tab: target.tab });
+
+ yield startRecording(panel);
+
+ ok(tab.hasAttribute("highlighted"),
+ "Performance tab is highlighted during recording while in performance tool.");
+
+ yield stopRecording(panel);
+
+ ok(!tab.hasAttribute("highlighted"),
+ "Performance tab is no longer highlighted when recording finishes.");
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-loading-01.js b/devtools/client/performance/test/browser_perf-loading-01.js
new file mode 100644
index 000000000..d732efcae
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-loading-01.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the recordings view shows the right label while recording, after
+ * recording, and once the record has loaded.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+const { once } = require("devtools/client/performance/test/helpers/event-utils");
+const { getSelectedRecording, getDurationLabelText } = require("devtools/client/performance/test/helpers/recording-utils");
+
+add_task(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { EVENTS, L10N, PerformanceController } = panel.panelWin;
+
+ yield startRecording(panel);
+
+ is(getDurationLabelText(panel, 0),
+ L10N.getStr("recordingsList.recordingLabel"),
+ "The duration node should show the 'recording' message while recording");
+
+ let recordingStopping = once(PerformanceController, EVENTS.RECORDING_STATE_CHANGE, {
+ expectedArgs: { "1": "recording-stopping" }
+ });
+ let recordingStopped = once(PerformanceController, EVENTS.RECORDING_STATE_CHANGE, {
+ expectedArgs: { "1": "recording-stopped" }
+ });
+ let everythingStopped = stopRecording(panel);
+
+ yield recordingStopping;
+ is(getDurationLabelText(panel, 0),
+ L10N.getStr("recordingsList.loadingLabel"),
+ "The duration node should show the 'loading' message while stopping");
+
+ yield recordingStopped;
+ const selected = getSelectedRecording(panel);
+ is(getDurationLabelText(panel, 0),
+ L10N.getFormatStr("recordingsList.durationLabel",
+ selected.getDuration().toFixed(0)),
+ "The duration node should show the duration after the record has stopped");
+
+ yield everythingStopped;
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-loading-02.js b/devtools/client/performance/test/browser_perf-loading-02.js
new file mode 100644
index 000000000..c860cb7a9
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-loading-02.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the details view is locked after recording has stopped and before
+ * the recording has finished loading.
+ * Also test that the details view isn't locked if the recording that is being
+ * stopped isn't the active one.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+const { once } = require("devtools/client/performance/test/helpers/event-utils");
+const { getSelectedRecordingIndex, setSelectedRecording } = require("devtools/client/performance/test/helpers/recording-utils");
+
+add_task(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { EVENTS, $, PerformanceController } = panel.panelWin;
+ let detailsContainer = $("#details-pane-container");
+ let recordingNotice = $("#recording-notice");
+ let loadingNotice = $("#loading-notice");
+ let detailsPane = $("#details-pane");
+
+ yield startRecording(panel);
+
+ is(detailsContainer.selectedPanel, recordingNotice,
+ "The recording-notice is shown while recording.");
+
+ let recordingStopping = once(PerformanceController, EVENTS.RECORDING_STATE_CHANGE, {
+ expectedArgs: { "1": "recording-stopping" }
+ });
+ let recordingStopped = once(PerformanceController, EVENTS.RECORDING_STATE_CHANGE, {
+ expectedArgs: { "1": "recording-stopped" }
+ });
+ let everythingStopped = stopRecording(panel);
+
+ yield recordingStopping;
+ is(detailsContainer.selectedPanel, loadingNotice,
+ "The loading-notice is shown while the record is stopping.");
+
+ yield recordingStopped;
+ is(detailsContainer.selectedPanel, detailsPane,
+ "The details panel is shown after the record has stopped.");
+
+ yield everythingStopped;
+ yield startRecording(panel);
+
+ info("While the 2nd record is still going, switch to the first one.");
+ let recordingSelected = once(PerformanceController, EVENTS.RECORDING_SELECTED);
+ setSelectedRecording(panel, 0);
+ yield recordingSelected;
+
+ recordingStopping = once(PerformanceController, EVENTS.RECORDING_STATE_CHANGE, {
+ expectedArgs: { "1": "recording-stopping" }
+ });
+ recordingStopped = once(PerformanceController, EVENTS.RECORDING_STATE_CHANGE, {
+ expectedArgs: { "1": "recording-stopped" }
+ });
+ everythingStopped = stopRecording(panel);
+
+ yield recordingStopping;
+ is(detailsContainer.selectedPanel, detailsPane,
+ "The details panel is still shown while the 2nd record is being stopped.");
+ is(getSelectedRecordingIndex(panel), 0,
+ "The first record is still selected.");
+
+ yield recordingStopped;
+
+ is(detailsContainer.selectedPanel, detailsPane,
+ "The details panel is still shown after the 2nd record has stopped.");
+ is(getSelectedRecordingIndex(panel), 1,
+ "The second record is now selected.");
+
+ yield everythingStopped;
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-marker-details.js b/devtools/client/performance/test/browser_perf-marker-details.js
new file mode 100644
index 000000000..8607f269d
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-marker-details.js
@@ -0,0 +1,146 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+/* eslint-disable */
+/**
+ * Tests if the Marker Details view renders all properties expected
+ * for each marker.
+ */
+
+function* spawnTest() {
+ let { target, panel } = yield initPerformance(MARKERS_URL);
+ let { $, $$, EVENTS, PerformanceController, OverviewView, WaterfallView } = panel.panelWin;
+
+ // Hijack the markers massaging part of creating the waterfall view,
+ // to prevent collapsing markers and allowing this test to verify
+ // everything individually. A better solution would be to just expand
+ // all markers first and then skip the meta nodes, but I'm lazy.
+ WaterfallView._prepareWaterfallTree = markers => {
+ return { submarkers: markers };
+ };
+
+ const MARKER_TYPES = [
+ "Styles", "Reflow", "ConsoleTime", "TimeStamp"
+ ];
+
+ yield startRecording(panel);
+ ok(true, "Recording has started.");
+
+ yield waitUntil(() => {
+ // Wait until we get all the different markers.
+ let markers = PerformanceController.getCurrentRecording().getMarkers();
+ return MARKER_TYPES.every(type => markers.some(m => m.name === type));
+ });
+
+ yield stopRecording(panel);
+ ok(true, "Recording has ended.");
+
+ info("No need to select everything in the timeline.");
+ info("All the markers should be displayed by default.");
+
+ let bars = Array.prototype.filter.call($$(".waterfall-marker-bar"),
+ (bar) => MARKER_TYPES.indexOf(bar.getAttribute("type")) !== -1);
+ let markers = PerformanceController.getCurrentRecording().getMarkers()
+ .filter(m => MARKER_TYPES.indexOf(m.name) !== -1);
+
+ info(`Got ${bars.length} bars and ${markers.length} markers.`);
+ info("Markers types from datasrc: " + Array.map(markers, e => e.name));
+ info("Markers names from sidebar: " + Array.map(bars, e => e.parentNode.parentNode.querySelector(".waterfall-marker-name").getAttribute("value")));
+
+ ok(bars.length >= MARKER_TYPES.length, `Got at least ${MARKER_TYPES.length} markers (1)`);
+ ok(markers.length >= MARKER_TYPES.length, `Got at least ${MARKER_TYPES.length} markers (2)`);
+
+ // Sanity check that markers are in chronologically ascending order
+ markers.reduce((previous, m) => {
+ if (m.start <= previous) {
+ ok(false, "Markers are not in order");
+ info(markers);
+ }
+ return m.start;
+ }, 0);
+
+ // Override the timestamp marker's stack with our own recursive stack, which
+ // can happen for unknown reasons (bug 1246555); we should not cause a crash
+ // when attempting to render a recursive stack trace
+ let timestampMarker = markers.find(m => m.name === "ConsoleTime");
+ ok(typeof timestampMarker.stack === "number", "ConsoleTime marker has a stack before overwriting.");
+ let frames = PerformanceController.getCurrentRecording().getFrames();
+ let frameIndex = timestampMarker.stack = frames.length;
+ frames.push({ line: 1, column: 1, source: "file.js", functionDisplayName: "test", parent: frameIndex + 1});
+ frames.push({ line: 1, column: 1, source: "file.js", functionDisplayName: "test", parent: frameIndex + 2 });
+ frames.push({ line: 1, column: 1, source: "file.js", functionDisplayName: "test", parent: frameIndex });
+
+ const tests = {
+ ConsoleTime: function (marker) {
+ info("Got `ConsoleTime` marker with data: " + JSON.stringify(marker));
+ ok(marker.stack === frameIndex, "Should have the ConsoleTime marker with recursive stack");
+ shouldHaveStack($, "startStack", marker);
+ shouldHaveStack($, "endStack", marker);
+ shouldHaveLabel($, "Timer Name:", "!!!", marker);
+ return true;
+ },
+ TimeStamp: function (marker) {
+ info("Got `TimeStamp` marker with data: " + JSON.stringify(marker));
+ shouldHaveLabel($, "Label:", "go", marker);
+ shouldHaveStack($, "stack", marker);
+ return true;
+ },
+ Styles: function (marker) {
+ info("Got `Styles` marker with data: " + JSON.stringify(marker));
+ if (marker.restyleHint) {
+ shouldHaveLabel($, "Restyle Hint:", marker.restyleHint.replace(/eRestyle_/g, ""), marker);
+ }
+ if (marker.stack) {
+ shouldHaveStack($, "stack", marker);
+ return true;
+ }
+ },
+ Reflow: function (marker) {
+ info("Got `Reflow` marker with data: " + JSON.stringify(marker));
+ if (marker.stack) {
+ shouldHaveStack($, "stack", marker);
+ return true;
+ }
+ }
+ };
+
+ // Keep track of all marker tests that are finished so we only
+ // run through each marker test once, so we don't spam 500 redundant
+ // tests.
+ let testsDone = [];
+
+ for (let i = 0; i < bars.length; i++) {
+ let bar = bars[i];
+ let m = markers[i];
+ EventUtils.sendMouseEvent({ type: "mousedown" }, bar);
+
+ if (tests[m.name]) {
+ if (testsDone.indexOf(m.name) === -1) {
+ let fullTestComplete = tests[m.name](m);
+ if (fullTestComplete) {
+ testsDone.push(m.name);
+ }
+ }
+ } else {
+ throw new Error(`No tests for ${m.name} -- should be filtered out.`);
+ }
+
+ if (testsDone.length === Object.keys(tests).length) {
+ break;
+ }
+ }
+
+ yield teardown(panel);
+ finish();
+}
+
+function shouldHaveStack($, type, marker) {
+ ok($(`#waterfall-details .marker-details-stack[type=${type}]`), `${marker.name} has a stack: ${type}`);
+}
+
+function shouldHaveLabel($, name, value, marker) {
+ let $name = $(`#waterfall-details .marker-details-labelcontainer .marker-details-labelname[value="${name}"]`);
+ let $value = $name.parentNode.querySelector(".marker-details-labelvalue");
+ is($value.getAttribute("value"), value, `${marker.name} has correct label for ${name}:${value}`);
+}
+/* eslint-enable */
diff --git a/devtools/client/performance/test/browser_perf-options-01-toggle-throw.js b/devtools/client/performance/test/browser_perf-options-01-toggle-throw.js
new file mode 100644
index 000000000..4ecfb4152
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-options-01-toggle-throw.js
@@ -0,0 +1,31 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that toggling preferences before there are any recordings does not throw.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+
+add_task(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { DetailsView, JsCallTreeView } = panel.panelWin;
+
+ yield DetailsView.selectView("js-calltree");
+
+ // Manually call the _onPrefChanged function so we can catch an error.
+ try {
+ JsCallTreeView._onPrefChanged(null, "invert-call-tree", true);
+ ok(true, "Toggling preferences before there are any recordings should not fail.");
+ } catch (e) {
+ ok(false, "Toggling preferences before there are any recordings should not fail.");
+ }
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-options-02-toggle-throw-alt.js b/devtools/client/performance/test/browser_perf-options-02-toggle-throw-alt.js
new file mode 100644
index 000000000..cdea1556a
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-options-02-toggle-throw-alt.js
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that toggling preferences during a recording does not throw.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+
+add_task(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { DetailsView, JsCallTreeView } = panel.panelWin;
+
+ yield DetailsView.selectView("js-calltree");
+ yield startRecording(panel);
+
+ // Manually call the _onPrefChanged function so we can catch an error.
+ try {
+ JsCallTreeView._onPrefChanged(null, "invert-call-tree", true);
+ ok(true, "Toggling preferences during a recording should not fail.");
+ } catch (e) {
+ ok(false, "Toggling preferences during a recording should not fail.");
+ }
+
+ yield stopRecording(panel, {
+ expectedViewClass: "JsCallTreeView",
+ expectedViewEvent: "UI_JS_CALL_TREE_RENDERED"
+ });
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-options-03-toggle-meta.js b/devtools/client/performance/test/browser_perf-options-03-toggle-meta.js
new file mode 100644
index 000000000..384133fff
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-options-03-toggle-meta.js
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that toggling meta option prefs change visibility of other options.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { UI_EXPERIMENTAL_PREF } = require("devtools/client/performance/test/helpers/prefs");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+
+add_task(function* () {
+ Services.prefs.setBoolPref(UI_EXPERIMENTAL_PREF, false);
+
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { $ } = panel.panelWin;
+ let $body = $(".theme-body");
+ let $menu = $("#performance-options-menupopup");
+
+ ok(!$body.classList.contains("experimental-enabled"),
+ "The body node does not have `experimental-enabled` on start.");
+ ok(!$menu.classList.contains("experimental-enabled"),
+ "The menu popup does not have `experimental-enabled` on start.");
+
+ Services.prefs.setBoolPref(UI_EXPERIMENTAL_PREF, true);
+
+ ok($body.classList.contains("experimental-enabled"),
+ "The body node has `experimental-enabled` after toggle.");
+ ok($menu.classList.contains("experimental-enabled"),
+ "The menu popup has `experimental-enabled` after toggle.");
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-options-enable-framerate-01.js b/devtools/client/performance/test/browser_perf-options-enable-framerate-01.js
new file mode 100644
index 000000000..ad75db6cf
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-options-enable-framerate-01.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that `enable-framerate` toggles the visibility of the fps graph,
+ * as well as enabling ticks data on the PerformanceFront.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { UI_ENABLE_FRAMERATE_PREF } = require("devtools/client/performance/test/helpers/prefs");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+const { isVisible } = require("devtools/client/performance/test/helpers/dom-utils");
+
+add_task(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { $, PerformanceController } = panel.panelWin;
+
+ // Disable framerate to test.
+ Services.prefs.setBoolPref(UI_ENABLE_FRAMERATE_PREF, false);
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ is(PerformanceController.getCurrentRecording().getConfiguration().withTicks, false,
+ "PerformanceFront started without ticks recording.");
+ ok(!isVisible($("#time-framerate")),
+ "The fps graph is hidden when ticks disabled.");
+
+ // Re-enable framerate.
+ Services.prefs.setBoolPref(UI_ENABLE_FRAMERATE_PREF, true);
+
+ is(PerformanceController.getCurrentRecording().getConfiguration().withTicks, false,
+ "PerformanceFront still marked without ticks recording.");
+ ok(!isVisible($("#time-framerate")),
+ "The fps graph is still hidden if recording does not contain ticks.");
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ is(PerformanceController.getCurrentRecording().getConfiguration().withTicks, true,
+ "PerformanceFront started with ticks recording.");
+ ok(isVisible($("#time-framerate")),
+ "The fps graph is not hidden when ticks enabled before recording.");
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-options-enable-framerate-02.js b/devtools/client/performance/test/browser_perf-options-enable-framerate-02.js
new file mode 100644
index 000000000..b7f870bba
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-options-enable-framerate-02.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that toggling `enable-memory` during a recording doesn't change that
+ * recording's state and does not break.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { UI_ENABLE_FRAMERATE_PREF } = require("devtools/client/performance/test/helpers/prefs");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+
+add_task(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { PerformanceController } = panel.panelWin;
+
+ // Test starting without framerate, and stopping with it.
+ Services.prefs.setBoolPref(UI_ENABLE_FRAMERATE_PREF, false);
+ yield startRecording(panel);
+
+ Services.prefs.setBoolPref(UI_ENABLE_FRAMERATE_PREF, true);
+ yield stopRecording(panel);
+
+ is(PerformanceController.getCurrentRecording().getConfiguration().withTicks, false,
+ "The recording finished without tracking framerate.");
+
+ // Test starting with framerate, and stopping without it.
+ yield startRecording(panel);
+
+ Services.prefs.setBoolPref(UI_ENABLE_FRAMERATE_PREF, false);
+ yield stopRecording(panel);
+
+ is(PerformanceController.getCurrentRecording().getConfiguration().withTicks, true,
+ "The recording finished with tracking framerate.");
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-options-enable-memory-01.js b/devtools/client/performance/test/browser_perf-options-enable-memory-01.js
new file mode 100644
index 000000000..9785d54d6
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-options-enable-memory-01.js
@@ -0,0 +1,58 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that `enable-memory` toggles the visibility of the memory graph,
+ * as well as enabling memory data on the PerformanceFront.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { UI_ENABLE_MEMORY_PREF } = require("devtools/client/performance/test/helpers/prefs");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+const { isVisible } = require("devtools/client/performance/test/helpers/dom-utils");
+
+add_task(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { $, PerformanceController } = panel.panelWin;
+
+ // Disable memory to test.
+ Services.prefs.setBoolPref(UI_ENABLE_MEMORY_PREF, false);
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ is(PerformanceController.getCurrentRecording().getConfiguration().withMemory, false,
+ "PerformanceFront started without memory recording.");
+ is(PerformanceController.getCurrentRecording().getConfiguration().withAllocations,
+ false, "PerformanceFront started without allocations recording.");
+ ok(!isVisible($("#memory-overview")),
+ "The memory graph is hidden when memory disabled.");
+
+ // Re-enable memory.
+ Services.prefs.setBoolPref(UI_ENABLE_MEMORY_PREF, true);
+
+ is(PerformanceController.getCurrentRecording().getConfiguration().withMemory, false,
+ "PerformanceFront still marked without memory recording.");
+ is(PerformanceController.getCurrentRecording().getConfiguration().withAllocations,
+ false, "PerformanceFront still marked without allocations recording.");
+ ok(!isVisible($("#memory-overview")), "memory graph is still hidden after enabling " +
+ "if recording did not start recording memory");
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ is(PerformanceController.getCurrentRecording().getConfiguration().withMemory, true,
+ "PerformanceFront started with memory recording.");
+ is(PerformanceController.getCurrentRecording().getConfiguration().withAllocations,
+ false, "PerformanceFront did not record with allocations.");
+ ok(isVisible($("#memory-overview")),
+ "The memory graph is not hidden when memory enabled before recording.");
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-options-enable-memory-02.js b/devtools/client/performance/test/browser_perf-options-enable-memory-02.js
new file mode 100644
index 000000000..b9c577687
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-options-enable-memory-02.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that toggling `enable-memory` during a recording doesn't change that
+ * recording's state and does not break.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { UI_ENABLE_MEMORY_PREF } = require("devtools/client/performance/test/helpers/prefs");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+
+add_task(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { PerformanceController } = panel.panelWin;
+
+ // Test starting without memory, and stopping with it.
+ Services.prefs.setBoolPref(UI_ENABLE_MEMORY_PREF, false);
+ yield startRecording(panel);
+
+ Services.prefs.setBoolPref(UI_ENABLE_MEMORY_PREF, true);
+ yield stopRecording(panel);
+
+ is(PerformanceController.getCurrentRecording().getConfiguration().withMemory, false,
+ "The recording finished without tracking memory.");
+ is(PerformanceController.getCurrentRecording().getConfiguration().withAllocations,
+ false,
+ "The recording finished without tracking allocations.");
+
+ // Test starting with memory, and stopping without it.
+ yield startRecording(panel);
+
+ Services.prefs.setBoolPref(UI_ENABLE_MEMORY_PREF, false);
+ yield stopRecording(panel);
+
+ is(PerformanceController.getCurrentRecording().getConfiguration().withMemory, true,
+ "The recording finished with tracking memory.");
+ is(PerformanceController.getCurrentRecording().getConfiguration().withAllocations,
+ false,
+ "The recording still is not recording allocations.");
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-options-flatten-tree-recursion-01.js b/devtools/client/performance/test/browser_perf-options-flatten-tree-recursion-01.js
new file mode 100644
index 000000000..9fccd6199
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-options-flatten-tree-recursion-01.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the js flamegraphs get rerendered when toggling `flatten-tree-recursion`.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { UI_FLATTEN_RECURSION_PREF } = require("devtools/client/performance/test/helpers/prefs");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+const { once } = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let {
+ EVENTS,
+ PerformanceController,
+ DetailsView,
+ JsFlameGraphView,
+ FlameGraphUtils
+ } = panel.panelWin;
+
+ Services.prefs.setBoolPref(UI_FLATTEN_RECURSION_PREF, true);
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ let rendered = once(JsFlameGraphView, EVENTS.UI_JS_FLAMEGRAPH_RENDERED);
+ yield DetailsView.selectView("js-flamegraph");
+ yield rendered;
+
+ let thread1 = PerformanceController.getCurrentRecording().getProfile().threads[0];
+ let rendering1 = FlameGraphUtils._cache.get(thread1);
+
+ ok(thread1,
+ "The samples were retrieved from the controller.");
+ ok(rendering1,
+ "The rendering data was cached.");
+
+ rendered = once(JsFlameGraphView, EVENTS.UI_JS_FLAMEGRAPH_RENDERED);
+ Services.prefs.setBoolPref(UI_FLATTEN_RECURSION_PREF, false);
+ yield rendered;
+ ok(true, "JsFlameGraphView rerendered when toggling flatten-tree-recursion.");
+
+ let thread2 = PerformanceController.getCurrentRecording().getProfile().threads[0];
+ let rendering2 = FlameGraphUtils._cache.get(thread2);
+
+ is(thread1, thread2,
+ "The same samples data should be retrieved from the controller (1).");
+ isnot(rendering1, rendering2,
+ "The rendering data should be different because other options were used (1).");
+
+ rendered = once(JsFlameGraphView, EVENTS.UI_JS_FLAMEGRAPH_RENDERED);
+ Services.prefs.setBoolPref(UI_FLATTEN_RECURSION_PREF, true);
+ yield rendered;
+ ok(true, "JsFlameGraphView rerendered when toggling back flatten-tree-recursion.");
+
+ let thread3 = PerformanceController.getCurrentRecording().getProfile().threads[0];
+ let rendering3 = FlameGraphUtils._cache.get(thread3);
+
+ is(thread2, thread3,
+ "The same samples data should be retrieved from the controller (2).");
+ isnot(rendering2, rendering3,
+ "The rendering data should be different because other options were used (2).");
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-options-flatten-tree-recursion-02.js b/devtools/client/performance/test/browser_perf-options-flatten-tree-recursion-02.js
new file mode 100644
index 000000000..509dd0f66
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-options-flatten-tree-recursion-02.js
@@ -0,0 +1,86 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the memory flamegraphs get rerendered when toggling
+ * `flatten-tree-recursion`.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { UI_FLATTEN_RECURSION_PREF, UI_ENABLE_ALLOCATIONS_PREF } = require("devtools/client/performance/test/helpers/prefs");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+const { once } = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let {
+ EVENTS,
+ PerformanceController,
+ DetailsView,
+ MemoryFlameGraphView,
+ RecordingUtils,
+ FlameGraphUtils
+ } = panel.panelWin;
+
+ // Enable memory to test
+ Services.prefs.setBoolPref(UI_ENABLE_ALLOCATIONS_PREF, true);
+ Services.prefs.setBoolPref(UI_FLATTEN_RECURSION_PREF, true);
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ let rendered = once(MemoryFlameGraphView, EVENTS.UI_MEMORY_FLAMEGRAPH_RENDERED);
+ yield DetailsView.selectView("memory-flamegraph");
+ yield rendered;
+
+ let allocations1 = PerformanceController.getCurrentRecording().getAllocations();
+ let thread1 = RecordingUtils.getProfileThreadFromAllocations(allocations1);
+ let rendering1 = FlameGraphUtils._cache.get(thread1);
+
+ ok(allocations1,
+ "The allocations were retrieved from the controller.");
+ ok(thread1,
+ "The allocations profile was synthesized by the utility funcs.");
+ ok(rendering1,
+ "The rendering data was cached.");
+
+ rendered = once(MemoryFlameGraphView, EVENTS.UI_MEMORY_FLAMEGRAPH_RENDERED);
+ Services.prefs.setBoolPref(UI_FLATTEN_RECURSION_PREF, false);
+ yield rendered;
+ ok(true, "MemoryFlameGraphView rerendered when toggling flatten-tree-recursion.");
+
+ let allocations2 = PerformanceController.getCurrentRecording().getAllocations();
+ let thread2 = RecordingUtils.getProfileThreadFromAllocations(allocations2);
+ let rendering2 = FlameGraphUtils._cache.get(thread2);
+
+ is(allocations1, allocations2,
+ "The same allocations data should be retrieved from the controller (1).");
+ is(thread1, thread2,
+ "The same allocations profile should be retrieved from the utility funcs. (1).");
+ isnot(rendering1, rendering2,
+ "The rendering data should be different because other options were used (1).");
+
+ rendered = once(MemoryFlameGraphView, EVENTS.UI_MEMORY_FLAMEGRAPH_RENDERED);
+ Services.prefs.setBoolPref(UI_FLATTEN_RECURSION_PREF, true);
+ yield rendered;
+ ok(true, "MemoryFlameGraphView rerendered when toggling back flatten-tree-recursion.");
+
+ let allocations3 = PerformanceController.getCurrentRecording().getAllocations();
+ let thread3 = RecordingUtils.getProfileThreadFromAllocations(allocations3);
+ let rendering3 = FlameGraphUtils._cache.get(thread3);
+
+ is(allocations2, allocations3,
+ "The same allocations data should be retrieved from the controller (2).");
+ is(thread2, thread3,
+ "The same allocations profile should be retrieved from the utility funcs. (2).");
+ isnot(rendering2, rendering3,
+ "The rendering data should be different because other options were used (2).");
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-options-invert-call-tree-01.js b/devtools/client/performance/test/browser_perf-options-invert-call-tree-01.js
new file mode 100644
index 000000000..cd84e8754
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-options-invert-call-tree-01.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the js call tree views get rerendered when toggling `invert-call-tree`.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { UI_INVERT_CALL_TREE_PREF } = require("devtools/client/performance/test/helpers/prefs");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+const { once } = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { EVENTS, DetailsView, JsCallTreeView } = panel.panelWin;
+
+ Services.prefs.setBoolPref(UI_INVERT_CALL_TREE_PREF, true);
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ let rendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
+ yield DetailsView.selectView("js-calltree");
+ yield rendered;
+
+ rendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
+ Services.prefs.setBoolPref(UI_INVERT_CALL_TREE_PREF, false);
+ yield rendered;
+ ok(true, "JsCallTreeView rerendered when toggling invert-call-tree.");
+
+ rendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
+ Services.prefs.setBoolPref(UI_INVERT_CALL_TREE_PREF, true);
+ yield rendered;
+ ok(true, "JsCallTreeView rerendered when toggling back invert-call-tree.");
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-options-invert-call-tree-02.js b/devtools/client/performance/test/browser_perf-options-invert-call-tree-02.js
new file mode 100644
index 000000000..ae0c8ede8
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-options-invert-call-tree-02.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the memory call tree views get rerendered when toggling `invert-call-tree`.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { UI_ENABLE_ALLOCATIONS_PREF, UI_INVERT_CALL_TREE_PREF } = require("devtools/client/performance/test/helpers/prefs");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+const { once } = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { EVENTS, DetailsView, MemoryCallTreeView } = panel.panelWin;
+
+ // Enable allocations to test.
+ Services.prefs.setBoolPref(UI_ENABLE_ALLOCATIONS_PREF, true);
+ Services.prefs.setBoolPref(UI_INVERT_CALL_TREE_PREF, true);
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ let rendered = once(MemoryCallTreeView, EVENTS.UI_MEMORY_CALL_TREE_RENDERED);
+ yield DetailsView.selectView("memory-calltree");
+ yield rendered;
+
+ rendered = once(MemoryCallTreeView, EVENTS.UI_MEMORY_CALL_TREE_RENDERED);
+ Services.prefs.setBoolPref(UI_INVERT_CALL_TREE_PREF, false);
+ yield rendered;
+ ok(true, "MemoryCallTreeView rerendered when toggling invert-call-tree.");
+
+ rendered = once(MemoryCallTreeView, EVENTS.UI_MEMORY_CALL_TREE_RENDERED);
+ Services.prefs.setBoolPref(UI_INVERT_CALL_TREE_PREF, true);
+ yield rendered;
+ ok(true, "MemoryCallTreeView rerendered when toggling back invert-call-tree.");
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-options-invert-flame-graph-01.js b/devtools/client/performance/test/browser_perf-options-invert-flame-graph-01.js
new file mode 100644
index 000000000..ee009bacf
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-options-invert-flame-graph-01.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the js flamegraphs views get rerendered when toggling `invert-flame-graph`.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { UI_INVERT_FLAME_PREF } = require("devtools/client/performance/test/helpers/prefs");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+const { once } = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { EVENTS, DetailsView, JsFlameGraphView } = panel.panelWin;
+
+ Services.prefs.setBoolPref(UI_INVERT_FLAME_PREF, true);
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ let rendered = once(JsFlameGraphView, EVENTS.UI_JS_FLAMEGRAPH_RENDERED);
+ yield DetailsView.selectView("js-flamegraph");
+ yield rendered;
+
+ rendered = once(JsFlameGraphView, EVENTS.UI_JS_FLAMEGRAPH_RENDERED);
+ Services.prefs.setBoolPref(UI_INVERT_FLAME_PREF, false);
+ yield rendered;
+ ok(true, "JsFlameGraphView rerendered when toggling invert-call-tree.");
+
+ rendered = once(JsFlameGraphView, EVENTS.UI_JS_FLAMEGRAPH_RENDERED);
+ Services.prefs.setBoolPref(UI_INVERT_FLAME_PREF, true);
+ yield rendered;
+ ok(true, "JsFlameGraphView rerendered when toggling back invert-call-tree.");
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-options-invert-flame-graph-02.js b/devtools/client/performance/test/browser_perf-options-invert-flame-graph-02.js
new file mode 100644
index 000000000..0a9322547
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-options-invert-flame-graph-02.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the memory flamegraphs views get rerendered when toggling
+ * `invert-flame-graph`.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { UI_ENABLE_ALLOCATIONS_PREF, UI_INVERT_FLAME_PREF } = require("devtools/client/performance/test/helpers/prefs");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+const { once } = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { EVENTS, DetailsView, MemoryFlameGraphView } = panel.panelWin;
+
+ // Enable allocations to test.
+ Services.prefs.setBoolPref(UI_ENABLE_ALLOCATIONS_PREF, true);
+ Services.prefs.setBoolPref(UI_INVERT_FLAME_PREF, true);
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ let rendered = once(MemoryFlameGraphView, EVENTS.UI_MEMORY_FLAMEGRAPH_RENDERED);
+ yield DetailsView.selectView("memory-flamegraph");
+ yield rendered;
+
+ rendered = once(MemoryFlameGraphView, EVENTS.UI_MEMORY_FLAMEGRAPH_RENDERED);
+ Services.prefs.setBoolPref(UI_INVERT_FLAME_PREF, false);
+ yield rendered;
+ ok(true, "MemoryFlameGraphView rerendered when toggling invert-call-tree.");
+
+ rendered = once(MemoryFlameGraphView, EVENTS.UI_MEMORY_FLAMEGRAPH_RENDERED);
+ Services.prefs.setBoolPref(UI_INVERT_FLAME_PREF, true);
+ yield rendered;
+ ok(true, "MemoryFlameGraphView rerendered when toggling back invert-call-tree.");
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-options-propagate-allocations.js b/devtools/client/performance/test/browser_perf-options-propagate-allocations.js
new file mode 100644
index 000000000..509452be4
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-options-propagate-allocations.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that setting the `devtools.performance.memory.` prefs propagate to
+ * the memory actor.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { MEMORY_SAMPLE_PROB_PREF, MEMORY_MAX_LOG_LEN_PREF, UI_ENABLE_ALLOCATIONS_PREF } = require("devtools/client/performance/test/helpers/prefs");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+
+add_task(function* () {
+ let { panel, toolbox } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ // Enable allocations to test.
+ Services.prefs.setBoolPref(UI_ENABLE_ALLOCATIONS_PREF, true);
+ Services.prefs.setCharPref(MEMORY_SAMPLE_PROB_PREF, "0.213");
+ Services.prefs.setIntPref(MEMORY_MAX_LOG_LEN_PREF, 777777);
+
+ yield startRecording(panel);
+ let { probability, maxLogLength } = yield toolbox.performance.getConfiguration();
+ yield stopRecording(panel);
+
+ is(probability, 0.213,
+ "The allocations probability option is set on memory actor.");
+ is(maxLogLength, 777777,
+ "The allocations max log length option is set on memory actor.");
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-options-propagate-profiler.js b/devtools/client/performance/test/browser_perf-options-propagate-profiler.js
new file mode 100644
index 000000000..d59233051
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-options-propagate-profiler.js
@@ -0,0 +1,32 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that setting the `devtools.performance.profiler.` prefs propagate
+ * to the profiler actor.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { PROFILER_BUFFER_SIZE_PREF, PROFILER_SAMPLE_RATE_PREF } = require("devtools/client/performance/test/helpers/prefs");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+
+add_task(function* () {
+ let { panel, toolbox } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ Services.prefs.setIntPref(PROFILER_BUFFER_SIZE_PREF, 1000);
+ Services.prefs.setIntPref(PROFILER_SAMPLE_RATE_PREF, 2);
+
+ yield startRecording(panel);
+ let { entries, interval } = yield toolbox.performance.getConfiguration();
+ yield stopRecording(panel);
+
+ is(entries, 1000, "profiler entries option is set on profiler");
+ is(interval, 0.5, "profiler interval option is set on profiler");
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-options-show-idle-blocks-01.js b/devtools/client/performance/test/browser_perf-options-show-idle-blocks-01.js
new file mode 100644
index 000000000..8c59ede42
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-options-show-idle-blocks-01.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the js flamegraphs get rerendered when toggling `show-idle-blocks`.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { UI_SHOW_IDLE_BLOCKS_PREF } = require("devtools/client/performance/test/helpers/prefs");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+const { once } = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { EVENTS, DetailsView, JsFlameGraphView } = panel.panelWin;
+
+ Services.prefs.setBoolPref(UI_SHOW_IDLE_BLOCKS_PREF, true);
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ let rendered = once(JsFlameGraphView, EVENTS.UI_JS_FLAMEGRAPH_RENDERED);
+ yield DetailsView.selectView("js-flamegraph");
+ yield rendered;
+
+ rendered = once(JsFlameGraphView, EVENTS.UI_JS_FLAMEGRAPH_RENDERED);
+ Services.prefs.setBoolPref(UI_SHOW_IDLE_BLOCKS_PREF, false);
+ yield rendered;
+ ok(true, "JsFlameGraphView rerendered when toggling show-idle-blocks.");
+
+ rendered = once(JsFlameGraphView, EVENTS.UI_JS_FLAMEGRAPH_RENDERED);
+ Services.prefs.setBoolPref(UI_SHOW_IDLE_BLOCKS_PREF, true);
+ yield rendered;
+ ok(true, "JsFlameGraphView rerendered when toggling back show-idle-blocks.");
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-options-show-idle-blocks-02.js b/devtools/client/performance/test/browser_perf-options-show-idle-blocks-02.js
new file mode 100644
index 000000000..3e0146ac7
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-options-show-idle-blocks-02.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the memory flamegraphs get rerendered when toggling `show-idle-blocks`.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { UI_ENABLE_ALLOCATIONS_PREF, UI_SHOW_IDLE_BLOCKS_PREF } = require("devtools/client/performance/test/helpers/prefs");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+const { once } = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { EVENTS, DetailsView, MemoryFlameGraphView } = panel.panelWin;
+
+ // Enable allocations to test.
+ Services.prefs.setBoolPref(UI_ENABLE_ALLOCATIONS_PREF, true);
+ Services.prefs.setBoolPref(UI_SHOW_IDLE_BLOCKS_PREF, true);
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ let rendered = once(MemoryFlameGraphView, EVENTS.UI_MEMORY_FLAMEGRAPH_RENDERED);
+ yield DetailsView.selectView("memory-flamegraph");
+ yield rendered;
+
+ rendered = once(MemoryFlameGraphView, EVENTS.UI_MEMORY_FLAMEGRAPH_RENDERED);
+ Services.prefs.setBoolPref(UI_SHOW_IDLE_BLOCKS_PREF, false);
+ yield rendered;
+ ok(true, "MemoryFlameGraphView rerendered when toggling show-idle-blocks.");
+
+ rendered = once(MemoryFlameGraphView, EVENTS.UI_MEMORY_FLAMEGRAPH_RENDERED);
+ Services.prefs.setBoolPref(UI_SHOW_IDLE_BLOCKS_PREF, true);
+ yield rendered;
+ ok(true, "MemoryFlameGraphView rerendered when toggling back show-idle-blocks.");
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-options-show-jit-optimizations.js b/devtools/client/performance/test/browser_perf-options-show-jit-optimizations.js
new file mode 100644
index 000000000..fd0bbc663
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-options-show-jit-optimizations.js
@@ -0,0 +1,260 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+/* eslint-disable */
+// Bug 1235788, increase time out of this test
+requestLongerTimeout(2);
+
+/**
+ * Tests that the JIT Optimizations view renders optimization data
+ * if on, and displays selected frames on focus.
+ */
+ const { setSelectedRecording } = require("devtools/client/performance/test/helpers/recording-utils");
+Services.prefs.setBoolPref(INVERT_PREF, false);
+
+function* spawnTest() {
+ let { panel } = yield initPerformance(SIMPLE_URL);
+ let { EVENTS, $, $$, window, PerformanceController } = panel.panelWin;
+ let { OverviewView, DetailsView, OptimizationsListView, JsCallTreeView } = panel.panelWin;
+
+ let profilerData = { threads: [gThread] };
+
+ is(Services.prefs.getBoolPref(JIT_PREF), false, "record JIT Optimizations pref off by default");
+ Services.prefs.setBoolPref(JIT_PREF, true);
+ is(Services.prefs.getBoolPref(JIT_PREF), true, "toggle on record JIT Optimizations");
+
+ // Make two recordings, so we have one to switch to later, as the
+ // second one will have fake sample data
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ yield DetailsView.selectView("js-calltree");
+
+ yield injectAndRenderProfilerData();
+
+ is($("#jit-optimizations-view").classList.contains("hidden"), true,
+ "JIT Optimizations should be hidden when pref is on, but no frame selected");
+
+ // A is never a leaf, so it's optimizations should not be shown.
+ yield checkFrame(1);
+
+ // gRawSite2 and gRawSite3 are both optimizations on B, so they'll have
+ // indices in descending order of # of samples.
+ yield checkFrame(2, true);
+
+ // Leaf node (C) with no optimizations should not display any opts.
+ yield checkFrame(3);
+
+ // Select the node with optimizations and change to a new recording
+ // to ensure the opts view is cleared
+ let rendered = once(JsCallTreeView, "focus");
+ mousedown(window, $$(".call-tree-item")[2]);
+ yield rendered;
+ let isHidden = $("#jit-optimizations-view").classList.contains("hidden");
+ ok(!isHidden, "opts view should be visible when selecting a frame with opts");
+
+ let select = once(PerformanceController, EVENTS.RECORDING_SELECTED);
+ rendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
+ setSelectedRecording(panel, 0);
+ yield Promise.all([select, rendered]);
+
+ isHidden = $("#jit-optimizations-view").classList.contains("hidden");
+ ok(isHidden, "opts view is hidden when switching recordings");
+
+ rendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
+ setSelectedRecording(panel, 1);
+ yield rendered;
+
+ rendered = once(JsCallTreeView, "focus");
+ mousedown(window, $$(".call-tree-item")[2]);
+ yield rendered;
+ isHidden = $("#jit-optimizations-view").classList.contains("hidden");
+ ok(!isHidden, "opts view should be visible when selecting a frame with opts");
+
+ rendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
+ Services.prefs.setBoolPref(JIT_PREF, false);
+ yield rendered;
+ ok(true, "call tree rerendered when JIT pref changes");
+ isHidden = $("#jit-optimizations-view").classList.contains("hidden");
+ ok(isHidden, "opts view hidden when toggling off jit pref");
+
+ rendered = once(JsCallTreeView, "focus");
+ mousedown(window, $$(".call-tree-item")[2]);
+ yield rendered;
+ isHidden = $("#jit-optimizations-view").classList.contains("hidden");
+ ok(isHidden, "opts view hidden when jit pref off and selecting a frame with opts");
+
+ yield teardown(panel);
+ finish();
+
+ function* injectAndRenderProfilerData() {
+ // Get current recording and inject our mock data
+ info("Injecting mock profile data");
+ let recording = PerformanceController.getCurrentRecording();
+ recording._profile = profilerData;
+
+ // Force a rerender
+ let rendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
+ JsCallTreeView.render(OverviewView.getTimeInterval());
+ yield rendered;
+ }
+
+ function* checkFrame(frameIndex, hasOpts) {
+ info(`Checking frame ${frameIndex}`);
+ // Click the frame
+ let rendered = once(JsCallTreeView, "focus");
+ mousedown(window, $$(".call-tree-item")[frameIndex]);
+ yield rendered;
+
+ let isHidden = $("#jit-optimizations-view").classList.contains("hidden");
+ if (hasOpts) {
+ ok(!isHidden, "JIT Optimizations view is not hidden if current frame has opts.");
+ } else {
+ ok(isHidden, "JIT Optimizations view is hidden if current frame does not have opts");
+ }
+ }
+}
+
+var gUniqueStacks = new RecordingUtils.UniqueStacks();
+
+function uniqStr(s) {
+ return gUniqueStacks.getOrAddStringIndex(s);
+}
+
+// Since deflateThread doesn't handle deflating optimization info, use
+// placeholder names A_O1, B_O2, and B_O3, which will be used to manually
+// splice deduped opts into the profile.
+var gThread = RecordingUtils.deflateThread({
+ samples: [{
+ time: 0,
+ frames: [
+ { location: "(root)" }
+ ]
+ }, {
+ time: 5,
+ frames: [
+ { location: "(root)" },
+ { location: "A_O1" },
+ { location: "B_O2" },
+ { location: "C (http://foo/bar/baz:56)" }
+ ]
+ }, {
+ time: 5 + 1,
+ frames: [
+ { location: "(root)" },
+ { location: "A (http://foo/bar/baz:12)" },
+ { location: "B_O2" },
+ ]
+ }, {
+ time: 5 + 1 + 2,
+ frames: [
+ { location: "(root)" },
+ { location: "A_O1" },
+ { location: "B_O3" },
+ ]
+ }, {
+ time: 5 + 1 + 2 + 7,
+ frames: [
+ { location: "(root)" },
+ { location: "A_O1" },
+ { location: "E (http://foo/bar/baz:90)" },
+ { location: "F (http://foo/bar/baz:99)" }
+ ]
+ }],
+ markers: []
+}, gUniqueStacks);
+
+// 3 RawOptimizationSites
+var gRawSite1 = {
+ _testFrameInfo: { name: "A", line: "12", file: "@baz" },
+ line: 12,
+ column: 2,
+ types: [{
+ mirType: uniqStr("Object"),
+ site: uniqStr("A (http://foo/bar/bar:12)"),
+ typeset: [{
+ keyedBy: uniqStr("constructor"),
+ name: uniqStr("Foo"),
+ location: uniqStr("A (http://foo/bar/baz:12)")
+ }, {
+ keyedBy: uniqStr("primitive"),
+ location: uniqStr("self-hosted")
+ }]
+ }],
+ attempts: {
+ schema: {
+ outcome: 0,
+ strategy: 1
+ },
+ data: [
+ [uniqStr("Failure1"), uniqStr("SomeGetter1")],
+ [uniqStr("Failure2"), uniqStr("SomeGetter2")],
+ [uniqStr("Failure3"), uniqStr("SomeGetter3")]
+ ]
+ }
+};
+
+var gRawSite2 = {
+ _testFrameInfo: { name: "B", line: "10", file: "@boo" },
+ line: 40,
+ types: [{
+ mirType: uniqStr("Int32"),
+ site: uniqStr("Receiver")
+ }],
+ attempts: {
+ schema: {
+ outcome: 0,
+ strategy: 1
+ },
+ data: [
+ [uniqStr("Failure1"), uniqStr("SomeGetter1")],
+ [uniqStr("Failure2"), uniqStr("SomeGetter2")],
+ [uniqStr("Inlined"), uniqStr("SomeGetter3")]
+ ]
+ }
+};
+
+var gRawSite3 = {
+ _testFrameInfo: { name: "B", line: "10", file: "@boo" },
+ line: 34,
+ types: [{
+ mirType: uniqStr("Int32"),
+ site: uniqStr("Receiver")
+ }],
+ attempts: {
+ schema: {
+ outcome: 0,
+ strategy: 1
+ },
+ data: [
+ [uniqStr("Failure1"), uniqStr("SomeGetter1")],
+ [uniqStr("Failure2"), uniqStr("SomeGetter2")],
+ [uniqStr("Failure3"), uniqStr("SomeGetter3")]
+ ]
+ }
+};
+
+gThread.frameTable.data.forEach((frame) => {
+ const LOCATION_SLOT = gThread.frameTable.schema.location;
+ const OPTIMIZATIONS_SLOT = gThread.frameTable.schema.optimizations;
+
+ let l = gThread.stringTable[frame[LOCATION_SLOT]];
+ switch (l) {
+ case "A_O1":
+ frame[LOCATION_SLOT] = uniqStr("A (http://foo/bar/baz:12)");
+ frame[OPTIMIZATIONS_SLOT] = gRawSite1;
+ break;
+ case "B_O2":
+ frame[LOCATION_SLOT] = uniqStr("B (http://foo/bar/boo:10)");
+ frame[OPTIMIZATIONS_SLOT] = gRawSite2;
+ break;
+ case "B_O3":
+ frame[LOCATION_SLOT] = uniqStr("B (http://foo/bar/boo:10)");
+ frame[OPTIMIZATIONS_SLOT] = gRawSite3;
+ break;
+ }
+});
+/* eslint-enable */
diff --git a/devtools/client/performance/test/browser_perf-options-show-platform-data-01.js b/devtools/client/performance/test/browser_perf-options-show-platform-data-01.js
new file mode 100644
index 000000000..20e69bd9a
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-options-show-platform-data-01.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the js call tree views get rerendered when toggling `show-platform-data`.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { UI_SHOW_PLATFORM_DATA_PREF } = require("devtools/client/performance/test/helpers/prefs");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+const { once } = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { EVENTS, DetailsView, JsCallTreeView } = panel.panelWin;
+
+ Services.prefs.setBoolPref(UI_SHOW_PLATFORM_DATA_PREF, true);
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ let rendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
+ yield DetailsView.selectView("js-calltree");
+ yield rendered;
+
+ rendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
+ Services.prefs.setBoolPref(UI_SHOW_PLATFORM_DATA_PREF, false);
+ yield rendered;
+ ok(true, "JsCallTreeView rerendered when toggling show-idle-blocks.");
+
+ rendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
+ Services.prefs.setBoolPref(UI_SHOW_PLATFORM_DATA_PREF, true);
+ yield rendered;
+ ok(true, "JsCallTreeView rerendered when toggling back show-idle-blocks.");
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-options-show-platform-data-02.js b/devtools/client/performance/test/browser_perf-options-show-platform-data-02.js
new file mode 100644
index 000000000..df199e797
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-options-show-platform-data-02.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the js flamegraphs views get rerendered when toggling `show-platform-data`.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { UI_SHOW_PLATFORM_DATA_PREF } = require("devtools/client/performance/test/helpers/prefs");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+const { once } = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { EVENTS, DetailsView, JsFlameGraphView } = panel.panelWin;
+
+ Services.prefs.setBoolPref(UI_SHOW_PLATFORM_DATA_PREF, true);
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ let rendered = once(JsFlameGraphView, EVENTS.UI_JS_FLAMEGRAPH_RENDERED);
+ yield DetailsView.selectView("js-flamegraph");
+ yield rendered;
+
+ rendered = once(JsFlameGraphView, EVENTS.UI_JS_FLAMEGRAPH_RENDERED);
+ Services.prefs.setBoolPref(UI_SHOW_PLATFORM_DATA_PREF, false);
+ yield rendered;
+ ok(true, "JsFlameGraphView rerendered when toggling show-idle-blocks.");
+
+ rendered = once(JsFlameGraphView, EVENTS.UI_JS_FLAMEGRAPH_RENDERED);
+ Services.prefs.setBoolPref(UI_SHOW_PLATFORM_DATA_PREF, true);
+ yield rendered;
+ ok(true, "JsFlameGraphView rerendered when toggling back show-idle-blocks.");
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-overview-render-01.js b/devtools/client/performance/test/browser_perf-overview-render-01.js
new file mode 100644
index 000000000..a34ba21ea
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-overview-render-01.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the overview continuously renders content when recording.
+ */
+
+const { Constants } = require("devtools/client/performance/modules/constants");
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+const { times } = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { EVENTS, OverviewView } = panel.panelWin;
+
+ yield startRecording(panel);
+
+ // Ensure overview keeps rendering.
+ yield times(OverviewView, EVENTS.UI_OVERVIEW_RENDERED, 3, {
+ expectedArgs: { "1": Constants.FRAMERATE_GRAPH_LOW_RES_INTERVAL }
+ });
+
+ ok(true, "Overview was rendered while recording.");
+
+ yield stopRecording(panel);
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-overview-render-02.js b/devtools/client/performance/test/browser_perf-overview-render-02.js
new file mode 100644
index 000000000..a7cb7026e
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-overview-render-02.js
@@ -0,0 +1,91 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the overview graphs cannot be selected during recording
+ * and that they're cleared upon rerecording.
+ */
+
+const { Constants } = require("devtools/client/performance/modules/constants");
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { UI_ENABLE_MEMORY_PREF } = require("devtools/client/performance/test/helpers/prefs");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+const { times } = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { EVENTS, OverviewView } = panel.panelWin;
+
+ // Enable memory to test.
+ Services.prefs.setBoolPref(UI_ENABLE_MEMORY_PREF, true);
+
+ yield startRecording(panel);
+
+ let framerate = OverviewView.graphs.get("framerate");
+ let markers = OverviewView.graphs.get("timeline");
+ let memory = OverviewView.graphs.get("memory");
+
+ ok("selectionEnabled" in framerate,
+ "The selection should not be enabled for the framerate overview (1).");
+ is(framerate.selectionEnabled, false,
+ "The selection should not be enabled for the framerate overview (2).");
+ is(framerate.hasSelection(), false,
+ "The framerate overview shouldn't have a selection before recording.");
+
+ ok("selectionEnabled" in markers,
+ "The selection should not be enabled for the markers overview (1).");
+ is(markers.selectionEnabled, false,
+ "The selection should not be enabled for the markers overview (2).");
+ is(markers.hasSelection(), false,
+ "The markers overview shouldn't have a selection before recording.");
+
+ ok("selectionEnabled" in memory,
+ "The selection should not be enabled for the memory overview (1).");
+ is(memory.selectionEnabled, false,
+ "The selection should not be enabled for the memory overview (2).");
+ is(memory.hasSelection(), false,
+ "The memory overview shouldn't have a selection before recording.");
+
+ // Ensure overview keeps rendering.
+ yield times(OverviewView, EVENTS.UI_OVERVIEW_RENDERED, 3, {
+ expectedArgs: { "1": Constants.FRAMERATE_GRAPH_LOW_RES_INTERVAL }
+ });
+
+ ok("selectionEnabled" in framerate,
+ "The selection should still not be enabled for the framerate overview (1).");
+ is(framerate.selectionEnabled, false,
+ "The selection should still not be enabled for the framerate overview (2).");
+ is(framerate.hasSelection(), false,
+ "The framerate overview still shouldn't have a selection before recording.");
+
+ ok("selectionEnabled" in markers,
+ "The selection should still not be enabled for the markers overview (1).");
+ is(markers.selectionEnabled, false,
+ "The selection should still not be enabled for the markers overview (2).");
+ is(markers.hasSelection(), false,
+ "The markers overview still shouldn't have a selection before recording.");
+
+ ok("selectionEnabled" in memory,
+ "The selection should still not be enabled for the memory overview (1).");
+ is(memory.selectionEnabled, false,
+ "The selection should still not be enabled for the memory overview (2).");
+ is(memory.hasSelection(), false,
+ "The memory overview still shouldn't have a selection before recording.");
+
+ yield stopRecording(panel);
+
+ is(framerate.selectionEnabled, true,
+ "The selection should now be enabled for the framerate overview.");
+ is(markers.selectionEnabled, true,
+ "The selection should now be enabled for the markers overview.");
+ is(memory.selectionEnabled, true,
+ "The selection should now be enabled for the memory overview.");
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-overview-render-03.js b/devtools/client/performance/test/browser_perf-overview-render-03.js
new file mode 100644
index 000000000..e46ce2f91
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-overview-render-03.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the overview graphs share the exact same width and scaling.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { UI_ENABLE_MEMORY_PREF } = require("devtools/client/performance/test/helpers/prefs");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+const { waitUntil } = require("devtools/client/performance/test/helpers/wait-utils");
+
+add_task(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { PerformanceController, OverviewView } = panel.panelWin;
+
+ // Enable memory to test.
+ Services.prefs.setBoolPref(UI_ENABLE_MEMORY_PREF, true);
+
+ let doChecks = () => {
+ let markers = OverviewView.graphs.get("timeline");
+ let framerate = OverviewView.graphs.get("framerate");
+ let memory = OverviewView.graphs.get("memory");
+
+ ok(markers.width > 0,
+ "The overview's markers graph has a width.");
+ ok(markers.dataScaleX > 0,
+ "The overview's markers graph has a data scale factor.");
+
+ ok(memory.width > 0,
+ "The overview's memory graph has a width.");
+ ok(memory.dataDuration > 0,
+ "The overview's memory graph has a data duration.");
+ ok(memory.dataScaleX > 0,
+ "The overview's memory graph has a data scale factor.");
+
+ ok(framerate.width > 0,
+ "The overview's framerate graph has a width.");
+ ok(framerate.dataDuration > 0,
+ "The overview's framerate graph has a data duration.");
+ ok(framerate.dataScaleX > 0,
+ "The overview's framerate graph has a data scale factor.");
+
+ is(markers.width, memory.width,
+ "The markers and memory graphs widths are the same.");
+ is(markers.width, framerate.width,
+ "The markers and framerate graphs widths are the same.");
+
+ is(memory.dataDuration, framerate.dataDuration,
+ "The memory and framerate graphs data duration are the same.");
+
+ is(markers.dataScaleX, memory.dataScaleX,
+ "The markers and memory graphs data scale are the same.");
+ is(markers.dataScaleX, framerate.dataScaleX,
+ "The markers and framerate graphs data scale are the same.");
+ };
+
+ yield startRecording(panel);
+ doChecks();
+
+ yield waitUntil(() => PerformanceController.getCurrentRecording().getMarkers().length);
+ yield waitUntil(() => PerformanceController.getCurrentRecording().getMemory().length);
+ yield waitUntil(() => PerformanceController.getCurrentRecording().getTicks().length);
+ doChecks();
+
+ yield stopRecording(panel);
+ doChecks();
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-overview-render-04.js b/devtools/client/performance/test/browser_perf-overview-render-04.js
new file mode 100644
index 000000000..22c856851
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-overview-render-04.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the overview graphs do not render when realtime rendering is off
+ * due to lack of e10s.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { UI_ENABLE_MEMORY_PREF } = require("devtools/client/performance/test/helpers/prefs");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+const { waitUntil } = require("devtools/client/performance/test/helpers/wait-utils");
+const { isVisible } = require("devtools/client/performance/test/helpers/dom-utils");
+const { setSelectedRecording } = require("devtools/client/performance/test/helpers/recording-utils");
+
+add_task(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { $, EVENTS, PerformanceController, OverviewView } = panel.panelWin;
+
+ // Enable memory to test.
+ Services.prefs.setBoolPref(UI_ENABLE_MEMORY_PREF, true);
+
+ // Set realtime rendering off.
+ OverviewView.isRealtimeRenderingEnabled = () => false;
+
+ let updated = 0;
+ OverviewView.on(EVENTS.UI_OVERVIEW_RENDERED, () => updated++);
+
+ yield startRecording(panel, { skipWaitingForOverview: true });
+
+ is(isVisible($("#overview-pane")), false, "Overview graphs hidden.");
+ is(updated, 0, "Overview graphs have not been updated");
+
+ yield waitUntil(() => PerformanceController.getCurrentRecording().getMarkers().length);
+ yield waitUntil(() => PerformanceController.getCurrentRecording().getMemory().length);
+ yield waitUntil(() => PerformanceController.getCurrentRecording().getTicks().length);
+ is(isVisible($("#overview-pane")), false, "Overview graphs still hidden.");
+ is(updated, 0, "Overview graphs have still not been updated");
+
+ yield stopRecording(panel);
+
+ is(isVisible($("#overview-pane")), true, "Overview graphs no longer hidden.");
+ is(updated, 1, "Overview graphs rendered upon completion.");
+
+ yield startRecording(panel, { skipWaitingForOverview: true });
+
+ is(isVisible($("#overview-pane")), false,
+ "Overview graphs hidden again when starting new recording.");
+ is(updated, 1, "Overview graphs have not been updated again.");
+
+ setSelectedRecording(panel, 0);
+ is(isVisible($("#overview-pane")), true,
+ "Overview graphs no longer hidden when switching back to complete recording.");
+ is(updated, 1, "Overview graphs have not been updated again.");
+
+ setSelectedRecording(panel, 1);
+ is(isVisible($("#overview-pane")), false,
+ "Overview graphs hidden again when going back to inprogress recording.");
+ is(updated, 1, "Overview graphs have not been updated again.");
+
+ yield stopRecording(panel);
+
+ is(isVisible($("#overview-pane")), true,
+ "overview graphs no longer hidden when recording finishes");
+ is(updated, 2, "Overview graphs rendered again upon completion.");
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-overview-selection-01.js b/devtools/client/performance/test/browser_perf-overview-selection-01.js
new file mode 100644
index 000000000..b8a8d730b
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-overview-selection-01.js
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that events are fired from selection manipulation.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+const { once } = require("devtools/client/performance/test/helpers/event-utils");
+const { dragStartCanvasGraph, dragStopCanvasGraph, clickCanvasGraph } = require("devtools/client/performance/test/helpers/input-utils");
+
+add_task(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { EVENTS, PerformanceController, OverviewView } = panel.panelWin;
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ let duration = PerformanceController.getCurrentRecording().getDuration();
+ let graph = OverviewView.graphs.get("timeline");
+
+ // Select the first half of the graph.
+
+ let rangeSelected = once(OverviewView, EVENTS.UI_OVERVIEW_RANGE_SELECTED,
+ { spreadArgs: true });
+ dragStartCanvasGraph(graph, { x: 0 });
+ let [, { startTime, endTime }] = yield rangeSelected;
+ is(endTime, duration, "The selected range is the entire graph, for now.");
+
+ rangeSelected = once(OverviewView, EVENTS.UI_OVERVIEW_RANGE_SELECTED,
+ { spreadArgs: true });
+ dragStopCanvasGraph(graph, { x: graph.width / 2 });
+ [, { startTime, endTime }] = yield rangeSelected;
+ is(endTime, duration / 2, "The selected range is half of the graph.");
+
+ is(graph.hasSelection(), true,
+ "A selection exists on the graph.");
+ is(startTime, 0,
+ "The UI_OVERVIEW_RANGE_SELECTED event fired with 0 as a `startTime`.");
+ is(endTime, duration / 2,
+ `The UI_OVERVIEW_RANGE_SELECTED event fired with ${duration / 2} as \`endTime\`.`);
+
+ let mapStart = () => 0;
+ let mapEnd = () => duration;
+ let actual = graph.getMappedSelection({ mapStart, mapEnd });
+ is(actual.min, 0, "Graph selection starts at 0.");
+ is(actual.max, duration / 2, `Graph selection ends at ${duration / 2}.`);
+
+ // Listen to deselection.
+
+ rangeSelected = once(OverviewView, EVENTS.UI_OVERVIEW_RANGE_SELECTED,
+ { spreadArgs: true });
+ clickCanvasGraph(graph, { x: 3 * graph.width / 4 });
+ [, { startTime, endTime }] = yield rangeSelected;
+
+ is(graph.hasSelection(), false,
+ "A selection no longer on the graph.");
+ is(startTime, 0,
+ "The UI_OVERVIEW_RANGE_SELECTED event fired with 0 as a `startTime`.");
+ is(endTime, duration,
+ "The UI_OVERVIEW_RANGE_SELECTED event fired with duration as `endTime`.");
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-overview-selection-02.js b/devtools/client/performance/test/browser_perf-overview-selection-02.js
new file mode 100644
index 000000000..71b410094
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-overview-selection-02.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the graphs' selection is correctly disabled or enabled.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { UI_ENABLE_MEMORY_PREF } = require("devtools/client/performance/test/helpers/prefs");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+
+add_task(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { OverviewView } = panel.panelWin;
+
+ // Enable memory to test.
+ Services.prefs.setBoolPref(UI_ENABLE_MEMORY_PREF, true);
+
+ yield startRecording(panel);
+
+ let markersOverview = OverviewView.graphs.get("timeline");
+ let memoryGraph = OverviewView.graphs.get("memory");
+ let framerateGraph = OverviewView.graphs.get("framerate");
+
+ ok(markersOverview,
+ "The markers graph should have been created now.");
+ ok(memoryGraph,
+ "The memory graph should have been created now.");
+ ok(framerateGraph,
+ "The framerate graph should have been created now.");
+
+ ok(!markersOverview.selectionEnabled,
+ "Selection shouldn't be enabled when the first recording started (2).");
+ ok(!memoryGraph.selectionEnabled,
+ "Selection shouldn't be enabled when the first recording started (3).");
+ ok(!framerateGraph.selectionEnabled,
+ "Selection shouldn't be enabled when the first recording started (1).");
+
+ yield stopRecording(panel);
+
+ ok(markersOverview.selectionEnabled,
+ "Selection should be enabled when the first recording finishes (2).");
+ ok(memoryGraph.selectionEnabled,
+ "Selection should be enabled when the first recording finishes (3).");
+ ok(framerateGraph.selectionEnabled,
+ "Selection should be enabled when the first recording finishes (1).");
+
+ yield startRecording(panel);
+
+ ok(!markersOverview.selectionEnabled,
+ "Selection shouldn't be enabled when the second recording started (2).");
+ ok(!memoryGraph.selectionEnabled,
+ "Selection shouldn't be enabled when the second recording started (3).");
+ ok(!framerateGraph.selectionEnabled,
+ "Selection shouldn't be enabled when the second recording started (1).");
+
+ yield stopRecording(panel);
+
+ ok(markersOverview.selectionEnabled,
+ "Selection should be enabled when the first second finishes (2).");
+ ok(memoryGraph.selectionEnabled,
+ "Selection should be enabled when the first second finishes (3).");
+ ok(framerateGraph.selectionEnabled,
+ "Selection should be enabled when the first second finishes (1).");
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-overview-selection-03.js b/devtools/client/performance/test/browser_perf-overview-selection-03.js
new file mode 100644
index 000000000..8f06901e8
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-overview-selection-03.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the graphs' selections are linked.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { UI_ENABLE_MEMORY_PREF } = require("devtools/client/performance/test/helpers/prefs");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+const { times } = require("devtools/client/performance/test/helpers/event-utils");
+const { dragStartCanvasGraph, dragStopCanvasGraph } = require("devtools/client/performance/test/helpers/input-utils");
+
+add_task(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { EVENTS, OverviewView } = panel.panelWin;
+
+ // Enable memory to test.
+ Services.prefs.setBoolPref(UI_ENABLE_MEMORY_PREF, true);
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ let markersOverview = OverviewView.graphs.get("timeline");
+ let memoryGraph = OverviewView.graphs.get("memory");
+ let framerateGraph = OverviewView.graphs.get("framerate");
+ let width = framerateGraph.width;
+
+ // Perform a selection inside the framerate graph.
+
+ let rangeSelected = times(OverviewView, EVENTS.UI_OVERVIEW_RANGE_SELECTED, 2);
+ dragStartCanvasGraph(framerateGraph, { x: 0 });
+ dragStopCanvasGraph(framerateGraph, { x: width / 2 });
+ yield rangeSelected;
+
+ is(markersOverview.getSelection().toSource(), framerateGraph.getSelection().toSource(),
+ "The markers overview has a correct selection.");
+ is(memoryGraph.getSelection().toSource(), framerateGraph.getSelection().toSource(),
+ "The memory overview has a correct selection.");
+ is(framerateGraph.getSelection().toSource(), "({start:0, end:" + (width / 2) + "})",
+ "The framerate graph has a correct selection.");
+
+ // Perform a selection inside the markers overview.
+
+ markersOverview.dropSelection();
+
+ rangeSelected = times(OverviewView, EVENTS.UI_OVERVIEW_RANGE_SELECTED, 2);
+ dragStartCanvasGraph(markersOverview, { x: 0 });
+ dragStopCanvasGraph(markersOverview, { x: width / 4 });
+ yield rangeSelected;
+
+ is(markersOverview.getSelection().toSource(), framerateGraph.getSelection().toSource(),
+ "The markers overview has a correct selection.");
+ is(memoryGraph.getSelection().toSource(), framerateGraph.getSelection().toSource(),
+ "The memory overview has a correct selection.");
+ is(framerateGraph.getSelection().toSource(), "({start:0, end:" + (width / 4) + "})",
+ "The framerate graph has a correct selection.");
+
+ // Perform a selection inside the memory overview.
+
+ markersOverview.dropSelection();
+
+ rangeSelected = times(OverviewView, EVENTS.UI_OVERVIEW_RANGE_SELECTED, 2);
+ dragStartCanvasGraph(memoryGraph, { x: 0 });
+ dragStopCanvasGraph(memoryGraph, { x: width / 10 });
+ yield rangeSelected;
+
+ is(markersOverview.getSelection().toSource(), framerateGraph.getSelection().toSource(),
+ "The markers overview has a correct selection.");
+ is(memoryGraph.getSelection().toSource(), framerateGraph.getSelection().toSource(),
+ "The memory overview has a correct selection.");
+ is(framerateGraph.getSelection().toSource(), "({start:0, end:" + (width / 10) + "})",
+ "The framerate graph has a correct selection.");
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-overview-time-interval.js b/devtools/client/performance/test/browser_perf-overview-time-interval.js
new file mode 100644
index 000000000..b66e3ef86
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-overview-time-interval.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the `setTimeInterval` and `getTimeInterval` functions
+ * work properly.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+const { once } = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { EVENTS, OverviewView } = panel.panelWin;
+
+ try {
+ OverviewView.setTimeInterval({ starTime: 0, endTime: 1 });
+ ok(false, "Setting a time interval shouldn't have worked.");
+ } catch (e) {
+ ok(true, "Setting a time interval didn't work, as expected.");
+ }
+
+ try {
+ OverviewView.getTimeInterval();
+ ok(false, "Getting the time interval shouldn't have worked.");
+ } catch (e) {
+ ok(true, "Getting the time interval didn't work, as expected.");
+ }
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ // Get/set the time interval and wait for the event propagation.
+
+ let rangeSelected = once(OverviewView, EVENTS.UI_OVERVIEW_RANGE_SELECTED);
+ OverviewView.setTimeInterval({ startTime: 10, endTime: 20 });
+ yield rangeSelected;
+
+ let firstInterval = OverviewView.getTimeInterval();
+ info("First interval start time: " + firstInterval.startTime);
+ info("First interval end time: " + firstInterval.endTime);
+ is(Math.round(firstInterval.startTime), 10,
+ "The interval's start time was properly set.");
+ is(Math.round(firstInterval.endTime), 20,
+ "The interval's end time was properly set.");
+
+ // Get/set another time interval and make sure there's no event propagation.
+
+ function fail() {
+ ok(false, "The selection event should not have propagated.");
+ }
+
+ OverviewView.on(EVENTS.UI_OVERVIEW_RANGE_SELECTED, fail);
+ OverviewView.setTimeInterval({ startTime: 30, endTime: 40 }, { stopPropagation: true });
+ OverviewView.off(EVENTS.UI_OVERVIEW_RANGE_SELECTED, fail);
+
+ let secondInterval = OverviewView.getTimeInterval();
+ info("Second interval start time: " + secondInterval.startTime);
+ info("Second interval end time: " + secondInterval.endTime);
+ is(Math.round(secondInterval.startTime), 30,
+ "The interval's start time was properly set again.");
+ is(Math.round(secondInterval.endTime), 40,
+ "The interval's end time was properly set again.");
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-private-browsing.js b/devtools/client/performance/test/browser_perf-private-browsing.js
new file mode 100644
index 000000000..dd2383fd4
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-private-browsing.js
@@ -0,0 +1,114 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the frontend is disabled when in private browsing mode.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { addWindow } = require("devtools/client/performance/test/helpers/tab-utils");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+const { once } = require("devtools/client/performance/test/helpers/event-utils");
+
+let gPanelWinTuples = [];
+
+add_task(function* () {
+ yield testNormalWindow();
+ yield testPrivateWindow();
+ yield testRecordingFailingInWindow(0);
+ yield testRecordingFailingInWindow(1);
+ yield teardownPerfInWindow(1, { shouldCloseWindow: true, dontWaitForTabClose: true });
+ yield testRecordingSucceedingInWindow(0);
+ yield teardownPerfInWindow(0, { shouldCloseWindow: false });
+
+ gPanelWinTuples = null;
+});
+
+function* createPanelInNewWindow(options) {
+ let win = yield addWindow(options);
+ return yield createPanelInWindow(options, win);
+}
+
+function* createPanelInWindow(options, win = window) {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: win
+ }, options);
+
+ gPanelWinTuples.push({ panel, win });
+ return { panel, win };
+}
+
+function* testNormalWindow() {
+ let { panel } = yield createPanelInWindow({
+ private: false
+ });
+
+ let { PerformanceView } = panel.panelWin;
+
+ is(PerformanceView.getState(), "empty",
+ "The initial state of the performance panel view is correct (1).");
+}
+
+function* testPrivateWindow() {
+ let { panel } = yield createPanelInNewWindow({
+ private: true,
+ // The add-on SDK can't seem to be able to listen to "ready" or "close"
+ // events for private tabs. Don't really absolutely need to though.
+ dontWaitForTabReady: true
+ });
+
+ let { PerformanceView } = panel.panelWin;
+
+ is(PerformanceView.getState(), "unavailable",
+ "The initial state of the performance panel view is correct (2).");
+}
+
+function* testRecordingFailingInWindow(index) {
+ let { panel } = gPanelWinTuples[index];
+ let { EVENTS, PerformanceController } = panel.panelWin;
+
+ let onRecordingStarted = () => {
+ ok(false, "Recording should not start while a private window is present.");
+ };
+
+ PerformanceController.on(EVENTS.RECORDING_STATE_CHANGE, onRecordingStarted);
+
+ let whenFailed = once(PerformanceController,
+ EVENTS.BACKEND_FAILED_AFTER_RECORDING_START);
+ PerformanceController.startRecording();
+ yield whenFailed;
+ ok(true, "Recording has failed.");
+
+ PerformanceController.off(EVENTS.RECORDING_STATE_CHANGE, onRecordingStarted);
+}
+
+function* testRecordingSucceedingInWindow(index) {
+ let { panel } = gPanelWinTuples[index];
+ let { EVENTS, PerformanceController } = panel.panelWin;
+
+ let onRecordingFailed = () => {
+ ok(false, "Recording should start while now private windows are present.");
+ };
+
+ PerformanceController.on(EVENTS.BACKEND_FAILED_AFTER_RECORDING_START,
+ onRecordingFailed);
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+ ok(true, "Recording has succeeded.");
+
+ PerformanceController.off(EVENTS.BACKEND_FAILED_AFTER_RECORDING_START,
+ onRecordingFailed);
+}
+
+function* teardownPerfInWindow(index, options) {
+ let { panel, win } = gPanelWinTuples[index];
+ yield teardownToolboxAndRemoveTab(panel, options);
+
+ if (options.shouldCloseWindow) {
+ win.close();
+ }
+}
diff --git a/devtools/client/performance/test/browser_perf-range-changed-render.js b/devtools/client/performance/test/browser_perf-range-changed-render.js
new file mode 100644
index 000000000..b3b9c6a92
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-range-changed-render.js
@@ -0,0 +1,81 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the detail views are rerendered after the range changes.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+const { once } = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let {
+ EVENTS,
+ OverviewView,
+ DetailsView,
+ WaterfallView,
+ JsCallTreeView,
+ JsFlameGraphView
+ } = panel.panelWin;
+
+ let updatedWaterfall = 0;
+ let updatedCallTree = 0;
+ let updatedFlameGraph = 0;
+ let updateWaterfall = () => updatedWaterfall++;
+ let updateCallTree = () => updatedCallTree++;
+ let updateFlameGraph = () => updatedFlameGraph++;
+ WaterfallView.on(EVENTS.UI_WATERFALL_RENDERED, updateWaterfall);
+ JsCallTreeView.on(EVENTS.UI_JS_CALL_TREE_RENDERED, updateCallTree);
+ JsFlameGraphView.on(EVENTS.UI_JS_FLAMEGRAPH_RENDERED, updateFlameGraph);
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ let rendered = once(WaterfallView, EVENTS.UI_WATERFALL_RENDERED);
+ OverviewView.emit(EVENTS.UI_OVERVIEW_RANGE_SELECTED, { startTime: 0, endTime: 10 });
+ yield rendered;
+ ok(true, "Waterfall rerenders when a range in the overview graph is selected.");
+
+ rendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
+ yield DetailsView.selectView("js-calltree");
+ yield rendered;
+ ok(true, "Call tree rerenders after its corresponding pane is shown.");
+
+ rendered = once(JsFlameGraphView, EVENTS.UI_JS_FLAMEGRAPH_RENDERED);
+ yield DetailsView.selectView("js-flamegraph");
+ yield rendered;
+ ok(true, "Flamegraph rerenders after its corresponding pane is shown.");
+
+ rendered = once(JsFlameGraphView, EVENTS.UI_JS_FLAMEGRAPH_RENDERED);
+ OverviewView.emit(EVENTS.UI_OVERVIEW_RANGE_SELECTED);
+ yield rendered;
+ ok(true, "Flamegraph rerenders when a range in the overview graph is removed.");
+
+ rendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
+ yield DetailsView.selectView("js-calltree");
+ yield rendered;
+ ok(true, "Call tree rerenders after its corresponding pane is shown.");
+
+ rendered = once(WaterfallView, EVENTS.UI_WATERFALL_RENDERED);
+ yield DetailsView.selectView("waterfall");
+ yield rendered;
+ ok(true, "Waterfall rerenders after its corresponding pane is shown.");
+
+ is(updatedWaterfall, 3, "WaterfallView rerendered 3 times.");
+ is(updatedCallTree, 2, "JsCallTreeView rerendered 2 times.");
+ is(updatedFlameGraph, 2, "JsFlameGraphView rerendered 2 times.");
+
+ WaterfallView.off(EVENTS.UI_WATERFALL_RENDERED, updateWaterfall);
+ JsCallTreeView.off(EVENTS.UI_JS_CALL_TREE_RENDERED, updateCallTree);
+ JsFlameGraphView.off(EVENTS.UI_JS_FLAMEGRAPH_RENDERED, updateFlameGraph);
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-recording-notices-01.js b/devtools/client/performance/test/browser_perf-recording-notices-01.js
new file mode 100644
index 000000000..697691a55
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-recording-notices-01.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the recording notice panes are toggled in correct scenarios
+ * for initialization and a single recording.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+
+add_task(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { $, PerformanceView } = panel.panelWin;
+
+ let MAIN_CONTAINER = $("#performance-view");
+ let EMPTY = $("#empty-notice");
+ let CONTENT = $("#performance-view-content");
+ let DETAILS_CONTAINER = $("#details-pane-container");
+ let RECORDING = $("#recording-notice");
+ let DETAILS = $("#details-pane");
+
+ is(PerformanceView.getState(), "empty", "Correct default state.");
+ is(MAIN_CONTAINER.selectedPanel, EMPTY, "Showing empty panel on load.");
+
+ yield startRecording(panel);
+
+ is(PerformanceView.getState(), "recording", "Correct state during recording.");
+ is(MAIN_CONTAINER.selectedPanel, CONTENT, "Showing main view with timeline.");
+ is(DETAILS_CONTAINER.selectedPanel, RECORDING, "Showing recording panel.");
+
+ yield stopRecording(panel);
+
+ is(PerformanceView.getState(), "recorded", "Correct state after recording.");
+ is(MAIN_CONTAINER.selectedPanel, CONTENT, "Showing main view with timeline.");
+ is(DETAILS_CONTAINER.selectedPanel, DETAILS, "Showing rendered graphs.");
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-recording-notices-02.js b/devtools/client/performance/test/browser_perf-recording-notices-02.js
new file mode 100644
index 000000000..b7905b3d7
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-recording-notices-02.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the recording notice panes are toggled when going between
+ * a completed recording and an in-progress recording.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+const { once } = require("devtools/client/performance/test/helpers/event-utils");
+const { setSelectedRecording } = require("devtools/client/performance/test/helpers/recording-utils");
+
+add_task(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let {
+ EVENTS,
+ $,
+ PerformanceController,
+ PerformanceView,
+ } = panel.panelWin;
+
+ let MAIN_CONTAINER = $("#performance-view");
+ let CONTENT = $("#performance-view-content");
+ let DETAILS_CONTAINER = $("#details-pane-container");
+ let RECORDING = $("#recording-notice");
+ let DETAILS = $("#details-pane");
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ yield startRecording(panel);
+
+ is(PerformanceView.getState(), "recording", "Correct state during recording.");
+ is(MAIN_CONTAINER.selectedPanel, CONTENT, "Showing main view with timeline.");
+ is(DETAILS_CONTAINER.selectedPanel, RECORDING, "Showing recording panel.");
+
+ let selected = once(PerformanceController, EVENTS.RECORDING_SELECTED);
+ setSelectedRecording(panel, 0);
+ yield selected;
+
+ is(PerformanceView.getState(), "recorded",
+ "Correct state during recording but selecting a completed recording.");
+ is(MAIN_CONTAINER.selectedPanel, CONTENT, "Showing main view with timeline.");
+ is(DETAILS_CONTAINER.selectedPanel, DETAILS, "Showing recorded panel.");
+
+ selected = once(PerformanceController, EVENTS.RECORDING_SELECTED);
+ setSelectedRecording(panel, 1);
+ yield selected;
+
+ is(PerformanceView.getState(), "recording",
+ "Correct state when switching back to recording in progress.");
+ is(MAIN_CONTAINER.selectedPanel, CONTENT, "Showing main view with timeline.");
+ is(DETAILS_CONTAINER.selectedPanel, RECORDING, "Showing recording panel.");
+
+ yield stopRecording(panel);
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-recording-notices-03.js b/devtools/client/performance/test/browser_perf-recording-notices-03.js
new file mode 100644
index 000000000..eeb439677
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-recording-notices-03.js
@@ -0,0 +1,135 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that recording notices display buffer status when available,
+ * and can switch between different recordings with the correct buffer
+ * information displayed.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { PROFILER_BUFFER_SIZE_PREF } = require("devtools/client/performance/test/helpers/prefs");
+const { pmmLoadFrameScripts, pmmStopProfiler, pmmClearFrameScripts } = require("devtools/client/performance/test/helpers/profiler-mm-utils");
+const { initPerformanceInTab, initConsoleInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+const { waitUntil } = require("devtools/client/performance/test/helpers/wait-utils");
+const { once } = require("devtools/client/performance/test/helpers/event-utils");
+const { setSelectedRecording } = require("devtools/client/performance/test/helpers/recording-utils");
+
+add_task(function* () {
+ // Make sure the profiler module is stopped so we can set a new buffer limit.
+ pmmLoadFrameScripts(gBrowser);
+ yield pmmStopProfiler();
+
+ // Keep the profiler's buffer large, but still get to 1% relatively quick.
+ Services.prefs.setIntPref(PROFILER_BUFFER_SIZE_PREF, 1000000);
+
+ let { target, console } = yield initConsoleInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { panel } = yield initPerformanceInTab({ tab: target.tab });
+ let {
+ gFront,
+ EVENTS,
+ $,
+ PerformanceController,
+ PerformanceView,
+ } = panel.panelWin;
+
+ // Set a fast profiler-status update interval.
+ yield gFront.setProfilerStatusInterval(10);
+
+ let DETAILS_CONTAINER = $("#details-pane-container");
+ let NORMAL_BUFFER_STATUS_MESSAGE = $("#recording-notice .buffer-status-message");
+ let CONSOLE_BUFFER_STATUS_MESSAGE =
+ $("#console-recording-notice .buffer-status-message");
+ let gPercent;
+
+ // Start a manual recording.
+ yield startRecording(panel);
+
+ yield waitUntil(function* () {
+ [, gPercent] = yield once(PerformanceView,
+ EVENTS.UI_RECORDING_PROFILER_STATUS_RENDERED,
+ { spreadArgs: true });
+ return gPercent > 0;
+ });
+
+ ok(true, "Buffer percentage increased in display (1).");
+
+ let bufferUsage = PerformanceController.getBufferUsageForRecording(
+ PerformanceController.getCurrentRecording());
+ either(DETAILS_CONTAINER.getAttribute("buffer-status"), "in-progress", "full",
+ "Container has [buffer-status=in-progress] or [buffer-status=full].");
+ ok(NORMAL_BUFFER_STATUS_MESSAGE.value.indexOf(gPercent + "%") !== -1,
+ "Buffer status text has correct percentage.");
+
+ // Start a console profile.
+ yield console.profile("rust");
+
+ yield waitUntil(function* () {
+ [, gPercent] = yield once(PerformanceView,
+ EVENTS.UI_RECORDING_PROFILER_STATUS_RENDERED,
+ { spreadArgs: true });
+ return gPercent > Math.floor(bufferUsage * 100);
+ });
+
+ ok(true, "Buffer percentage increased in display (2).");
+
+ bufferUsage = PerformanceController.getBufferUsageForRecording(
+ PerformanceController.getCurrentRecording());
+ either(DETAILS_CONTAINER.getAttribute("buffer-status"), "in-progress", "full",
+ "Container has [buffer-status=in-progress] or [buffer-status=full].");
+ ok(NORMAL_BUFFER_STATUS_MESSAGE.value.indexOf(gPercent + "%") !== -1,
+ "Buffer status text has correct percentage.");
+
+ // Select the console recording.
+ let selected = once(PerformanceController, EVENTS.RECORDING_SELECTED);
+ setSelectedRecording(panel, 1);
+ yield selected;
+
+ yield waitUntil(function* () {
+ [, gPercent] = yield once(PerformanceView,
+ EVENTS.UI_RECORDING_PROFILER_STATUS_RENDERED,
+ { spreadArgs: true });
+ return gPercent > 0;
+ });
+
+ ok(true, "Percentage updated for newly selected recording.");
+
+ either(DETAILS_CONTAINER.getAttribute("buffer-status"), "in-progress", "full",
+ "Container has [buffer-status=in-progress] or [buffer-status=full].");
+ ok(CONSOLE_BUFFER_STATUS_MESSAGE.value.indexOf(gPercent + "%") !== -1,
+ "Buffer status text has correct percentage for console recording.");
+
+ // Stop the console profile, then select the original manual recording.
+ yield console.profileEnd("rust");
+
+ selected = once(PerformanceController, EVENTS.RECORDING_SELECTED);
+ setSelectedRecording(panel, 0);
+ yield selected;
+
+ yield waitUntil(function* () {
+ [, gPercent] = yield once(PerformanceView,
+ EVENTS.UI_RECORDING_PROFILER_STATUS_RENDERED,
+ { spreadArgs: true });
+ return gPercent > Math.floor(bufferUsage * 100);
+ });
+
+ ok(true, "Buffer percentage increased in display (3).");
+
+ either(DETAILS_CONTAINER.getAttribute("buffer-status"), "in-progress", "full",
+ "Container has [buffer-status=in-progress] or [buffer-status=full].");
+ ok(NORMAL_BUFFER_STATUS_MESSAGE.value.indexOf(gPercent + "%") !== -1,
+ "Buffer status text has correct percentage.");
+
+ // Stop the manual recording.
+ yield stopRecording(panel);
+
+ yield teardownToolboxAndRemoveTab(panel);
+
+ pmmClearFrameScripts();
+});
diff --git a/devtools/client/performance/test/browser_perf-recording-notices-04.js b/devtools/client/performance/test/browser_perf-recording-notices-04.js
new file mode 100644
index 000000000..067cda9dc
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-recording-notices-04.js
@@ -0,0 +1,66 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that when a recording overlaps the circular buffer, that
+ * a class is assigned to the recording notices.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { PROFILER_BUFFER_SIZE_PREF } = require("devtools/client/performance/test/helpers/prefs");
+const { pmmLoadFrameScripts, pmmStopProfiler, pmmClearFrameScripts } = require("devtools/client/performance/test/helpers/profiler-mm-utils");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+const { waitUntil } = require("devtools/client/performance/test/helpers/wait-utils");
+const { once } = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(function* () {
+ // Make sure the profiler module is stopped so we can set a new buffer limit.
+ pmmLoadFrameScripts(gBrowser);
+ yield pmmStopProfiler();
+
+ // Keep the profiler's buffer small, to get to 100% really quickly.
+ Services.prefs.setIntPref(PROFILER_BUFFER_SIZE_PREF, 10000);
+
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { gFront, EVENTS, $, PerformanceController, PerformanceView } = panel.panelWin;
+
+ // Set a fast profiler-status update interval
+ yield gFront.setProfilerStatusInterval(10);
+
+ let DETAILS_CONTAINER = $("#details-pane-container");
+ let NORMAL_BUFFER_STATUS_MESSAGE = $("#recording-notice .buffer-status-message");
+ let gPercent;
+
+ // Start a manual recording.
+ yield startRecording(panel);
+
+ yield waitUntil(function* () {
+ [, gPercent] = yield once(PerformanceView,
+ EVENTS.UI_RECORDING_PROFILER_STATUS_RENDERED,
+ { spreadArgs: true });
+ return gPercent == 100;
+ });
+
+ ok(true, "Buffer percentage increased in display.");
+
+ let bufferUsage = PerformanceController.getBufferUsageForRecording(
+ PerformanceController.getCurrentRecording());
+ ok(bufferUsage, 1, "Buffer is full for this recording.");
+ ok(DETAILS_CONTAINER.getAttribute("buffer-status"), "full",
+ "Container has [buffer-status=full].");
+ ok(NORMAL_BUFFER_STATUS_MESSAGE.value.indexOf(gPercent + "%") !== -1,
+ "Buffer status text has correct percentage.");
+
+ // Stop the manual recording.
+ yield stopRecording(panel);
+
+ yield teardownToolboxAndRemoveTab(panel);
+
+ pmmClearFrameScripts();
+});
diff --git a/devtools/client/performance/test/browser_perf-recording-notices-05.js b/devtools/client/performance/test/browser_perf-recording-notices-05.js
new file mode 100644
index 000000000..b6267470d
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-recording-notices-05.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the circular buffer notices work when e10s is on/off.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+
+add_task(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { gFront, $, PerformanceController } = panel.panelWin;
+
+ // Set a fast profiler-status update interval
+ yield gFront.setProfilerStatusInterval(10);
+
+ let supported = false;
+ let enabled = false;
+
+ PerformanceController.getMultiprocessStatus = () => {
+ return { supported, enabled };
+ };
+
+ PerformanceController._setMultiprocessAttributes();
+ ok($("#performance-view").getAttribute("e10s"), "unsupported",
+ "When e10s is disabled and no option to turn on, container has [e10s=unsupported].");
+
+ supported = true;
+ enabled = false;
+ PerformanceController._setMultiprocessAttributes();
+ ok($("#performance-view").getAttribute("e10s"), "disabled",
+ "When e10s is disabled and but is supported, container has [e10s=disabled].");
+
+ supported = false;
+ enabled = true;
+ PerformanceController._setMultiprocessAttributes();
+ ok($("#performance-view").getAttribute("e10s"), "",
+ "When e10s is enabled, but not supported, this probably means we no longer have " +
+ "E10S_TESTING_ONLY, and we have no e10s attribute.");
+
+ supported = true;
+ enabled = true;
+ PerformanceController._setMultiprocessAttributes();
+ ok($("#performance-view").getAttribute("e10s"), "",
+ "When e10s is enabled and supported, there should be no e10s attribute.");
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-recording-selected-01.js b/devtools/client/performance/test/browser_perf-recording-selected-01.js
new file mode 100644
index 000000000..15cb66ec9
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-recording-selected-01.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if the profiler correctly handles multiple recordings and can
+ * successfully switch between them.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+const { once } = require("devtools/client/performance/test/helpers/event-utils");
+const { setSelectedRecording, getRecordingsCount, getSelectedRecordingIndex } = require("devtools/client/performance/test/helpers/recording-utils");
+
+add_task(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { EVENTS, PerformanceController } = panel.panelWin;
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ is(getRecordingsCount(panel), 2,
+ "There should be two recordings visible.");
+ is(getSelectedRecordingIndex(panel), 1,
+ "The second recording item should be selected.");
+
+ let selected = once(PerformanceController, EVENTS.RECORDING_SELECTED);
+ setSelectedRecording(panel, 0);
+ yield selected;
+
+ is(getRecordingsCount(panel), 2,
+ "There should still be two recordings visible.");
+ is(getSelectedRecordingIndex(panel), 0,
+ "The first recording item should be selected.");
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-recording-selected-02.js b/devtools/client/performance/test/browser_perf-recording-selected-02.js
new file mode 100644
index 000000000..0bffa3a73
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-recording-selected-02.js
@@ -0,0 +1,58 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if the profiler correctly handles multiple recordings and can
+ * successfully switch between them, even when one of them is in progress.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+const { once } = require("devtools/client/performance/test/helpers/event-utils");
+const { getSelectedRecordingIndex, setSelectedRecording, getRecordingsCount } = require("devtools/client/performance/test/helpers/recording-utils");
+
+add_task(function* () {
+ // This test seems to take a very long time to finish on Linux VMs.
+ requestLongerTimeout(4);
+
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { EVENTS, PerformanceController } = panel.panelWin;
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ yield startRecording(panel);
+
+ is(getRecordingsCount(panel), 2,
+ "There should be two recordings visible.");
+ is(getSelectedRecordingIndex(panel), 1,
+ "The new recording item should be selected.");
+
+ let selected = once(PerformanceController, EVENTS.RECORDING_SELECTED);
+ setSelectedRecording(panel, 0);
+ yield selected;
+
+ is(getRecordingsCount(panel), 2,
+ "There should still be two recordings visible.");
+ is(getSelectedRecordingIndex(panel), 0,
+ "The first recording item should be selected now.");
+
+ selected = once(PerformanceController, EVENTS.RECORDING_SELECTED);
+ setSelectedRecording(panel, 1);
+ yield selected;
+
+ is(getRecordingsCount(panel), 2,
+ "There should still be two recordings visible.");
+ is(getSelectedRecordingIndex(panel), 1,
+ "The second recording item should be selected again.");
+
+ yield stopRecording(panel);
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-recording-selected-03.js b/devtools/client/performance/test/browser_perf-recording-selected-03.js
new file mode 100644
index 000000000..7febfbb2b
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-recording-selected-03.js
@@ -0,0 +1,44 @@
+/* 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";
+
+/**
+ * Tests if the profiler UI does not forget that recording is active when
+ * selected recording changes.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+const { once } = require("devtools/client/performance/test/helpers/event-utils");
+const { setSelectedRecording } = require("devtools/client/performance/test/helpers/recording-utils");
+
+add_task(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { $, EVENTS, PerformanceController } = panel.panelWin;
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ yield startRecording(panel);
+
+ info("Selecting recording #0 and waiting for it to be displayed.");
+
+ let selected = once(PerformanceController, EVENTS.RECORDING_SELECTED);
+ setSelectedRecording(panel, 0);
+ yield selected;
+
+ ok($("#main-record-button").classList.contains("checked"),
+ "Button is still checked after selecting another item.");
+ ok(!$("#main-record-button").hasAttribute("disabled"),
+ "Button is not locked after selecting another item.");
+
+ yield stopRecording(panel);
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-recording-selected-04.js b/devtools/client/performance/test/browser_perf-recording-selected-04.js
new file mode 100644
index 000000000..014ef5bdd
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-recording-selected-04.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that all components can get rerendered for a profile when switching.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { UI_ENABLE_MEMORY_PREF, UI_ENABLE_ALLOCATIONS_PREF } = require("devtools/client/performance/test/helpers/prefs");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording, waitForAllWidgetsRendered } = require("devtools/client/performance/test/helpers/actions");
+const { setSelectedRecording } = require("devtools/client/performance/test/helpers/recording-utils");
+
+add_task(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { DetailsView, DetailsSubview } = panel.panelWin;
+
+ // Enable memory to test the memory overview.
+ Services.prefs.setBoolPref(UI_ENABLE_MEMORY_PREF, true);
+
+ // Enable allocations to test the memory-calltree and memory-flamegraph.
+ Services.prefs.setBoolPref(UI_ENABLE_ALLOCATIONS_PREF, true);
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ // Ã…llow widgets to be updated while hidden, to make testing easier.
+ DetailsSubview.canUpdateWhileHidden = true;
+
+ // Cycle through all the views to initialize them. The waterfall is shown
+ // by default, but all the other views are created lazily, so won't emit
+ // any events.
+ yield DetailsView.selectView("js-calltree");
+ yield DetailsView.selectView("js-flamegraph");
+ yield DetailsView.selectView("memory-calltree");
+ yield DetailsView.selectView("memory-flamegraph");
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ let rerender = waitForAllWidgetsRendered(panel);
+ setSelectedRecording(panel, 0);
+ yield rerender;
+
+ ok(true, "All widgets were rendered when selecting the first recording.");
+
+ rerender = waitForAllWidgetsRendered(panel);
+ setSelectedRecording(panel, 1);
+ yield rerender;
+
+ ok(true, "All widgets were rendered when selecting the second recording.");
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-recordings-clear-01.js b/devtools/client/performance/test/browser_perf-recordings-clear-01.js
new file mode 100644
index 000000000..87579896a
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-recordings-clear-01.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that clearing recordings empties out the recordings list and toggles
+ * the empty notice state.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { initPanelInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+const { getRecordingsCount } = require("devtools/client/performance/test/helpers/recording-utils");
+
+add_task(function* () {
+ let { panel } = yield initPanelInNewTab({
+ tool: "performance",
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { PerformanceController, PerformanceView } = panel.panelWin;
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ is(getRecordingsCount(panel), 1,
+ "The recordings list should have one recording.");
+ isnot(PerformanceView.getState(), "empty",
+ "PerformanceView should not be in an empty state.");
+ isnot(PerformanceController.getCurrentRecording(), null,
+ "There should be a current recording.");
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ is(getRecordingsCount(panel), 2,
+ "The recordings list should have two recordings.");
+ isnot(PerformanceView.getState(), "empty",
+ "PerformanceView should not be in an empty state.");
+ isnot(PerformanceController.getCurrentRecording(), null,
+ "There should be a current recording.");
+
+ yield PerformanceController.clearRecordings();
+
+ is(getRecordingsCount(panel), 0,
+ "The recordings list should be empty.");
+ is(PerformanceView.getState(), "empty",
+ "PerformanceView should be in an empty state.");
+ is(PerformanceController.getCurrentRecording(), null,
+ "There should be no current recording.");
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-recordings-clear-02.js b/devtools/client/performance/test/browser_perf-recordings-clear-02.js
new file mode 100644
index 000000000..d8196dbd1
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-recordings-clear-02.js
@@ -0,0 +1,69 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that clearing recordings empties out the recordings list and stops
+ * a current recording if recording and can continue recording after.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { initPanelInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+const { times, once } = require("devtools/client/performance/test/helpers/event-utils");
+const { getRecordingsCount } = require("devtools/client/performance/test/helpers/recording-utils");
+
+add_task(function* () {
+ let { panel } = yield initPanelInNewTab({
+ tool: "performance",
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { EVENTS, PerformanceController, PerformanceView } = panel.panelWin;
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ is(getRecordingsCount(panel), 1,
+ "The recordings list should have one recording.");
+ isnot(PerformanceView.getState(), "empty",
+ "PerformanceView should not be in an empty state.");
+ isnot(PerformanceController.getCurrentRecording(), null,
+ "There should be a current recording.");
+
+ yield startRecording(panel);
+
+ is(getRecordingsCount(panel), 2,
+ "The recordings list should have two recordings.");
+ isnot(PerformanceView.getState(), "empty",
+ "PerformanceView should not be in an empty state.");
+ isnot(PerformanceController.getCurrentRecording(), null,
+ "There should be a current recording.");
+
+ let recordingDeleted = times(PerformanceController, EVENTS.RECORDING_DELETED, 2);
+ let recordingStopped = once(PerformanceController, EVENTS.RECORDING_STATE_CHANGE, {
+ expectedArgs: { "1": "recording-stopped" }
+ });
+
+ PerformanceController.clearRecordings();
+
+ yield recordingDeleted;
+ yield recordingStopped;
+
+ is(getRecordingsCount(panel), 0,
+ "The recordings list should be empty.");
+ is(PerformanceView.getState(), "empty",
+ "PerformanceView should be in an empty state.");
+ is(PerformanceController.getCurrentRecording(), null,
+ "There should be no current recording.");
+
+ // Bug 1169146: Try another recording after clearing mid-recording.
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ is(getRecordingsCount(panel), 1,
+ "The recordings list should have one recording.");
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-recordings-io-01.js b/devtools/client/performance/test/browser_perf-recordings-io-01.js
new file mode 100644
index 000000000..90a014421
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-recordings-io-01.js
@@ -0,0 +1,94 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+/* eslint-disable */
+/**
+ * Tests if the performance tool is able to save and load recordings.
+ */
+
+var test = Task.async(function* () {
+ var { target, panel, toolbox } = yield initPerformance(SIMPLE_URL);
+ var { $, EVENTS, PerformanceController, PerformanceView, DetailsView, DetailsSubview } = panel.panelWin;
+
+ // Enable allocations to test the memory-calltree and memory-flamegraph.
+ Services.prefs.setBoolPref(ALLOCATIONS_PREF, true);
+ Services.prefs.setBoolPref(MEMORY_PREF, true);
+ Services.prefs.setBoolPref(FRAMERATE_PREF, true);
+
+ // Need to allow widgets to be updated while hidden, otherwise we can't use
+ // `waitForWidgetsRendered`.
+ DetailsSubview.canUpdateWhileHidden = true;
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ // Cycle through all the views to initialize them, otherwise we can't use
+ // `waitForWidgetsRendered`. The waterfall is shown by default, but all the
+ // other views are created lazily, so won't emit any events.
+ yield DetailsView.selectView("js-calltree");
+ yield DetailsView.selectView("js-flamegraph");
+ yield DetailsView.selectView("memory-calltree");
+ yield DetailsView.selectView("memory-flamegraph");
+
+ // Verify original recording.
+
+ let originalData = PerformanceController.getCurrentRecording().getAllData();
+ ok(originalData, "The original recording is not empty.");
+
+ // Save recording.
+
+ let file = FileUtils.getFile("TmpD", ["tmpprofile.json"]);
+ file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("666", 8));
+
+ let exported = once(PerformanceController, EVENTS.RECORDING_EXPORTED);
+ yield PerformanceController.exportRecording("", PerformanceController.getCurrentRecording(), file);
+
+ yield exported;
+ ok(true, "The recording data appears to have been successfully saved.");
+
+ // Check if the imported file name has tmpprofile in it as the file
+ // names also has different suffix to avoid conflict
+
+ let displayedName = $(".recording-item-title").getAttribute("value");
+ ok(/^tmpprofile/.test(displayedName), "File has expected display name after import");
+ ok(!/\.json$/.test(displayedName), "Display name does not have .json in it");
+
+ // Import recording.
+
+ let rerendered = waitForWidgetsRendered(panel);
+ let imported = once(PerformanceController, EVENTS.RECORDING_IMPORTED);
+ PerformanceView.emit(EVENTS.UI_IMPORT_RECORDING, file);
+
+ yield imported;
+ ok(true, "The recording data appears to have been successfully imported.");
+
+ yield rerendered;
+ ok(true, "The imported data was re-rendered.");
+
+ // Verify imported recording.
+
+ let importedData = PerformanceController.getCurrentRecording().getAllData();
+
+ ok(/^tmpprofile/.test(importedData.label),
+ "The imported data label is identical to the filename without its extension.");
+ is(importedData.duration, originalData.duration,
+ "The imported data is identical to the original data (1).");
+ is(importedData.markers.toSource(), originalData.markers.toSource(),
+ "The imported data is identical to the original data (2).");
+ is(importedData.memory.toSource(), originalData.memory.toSource(),
+ "The imported data is identical to the original data (3).");
+ is(importedData.ticks.toSource(), originalData.ticks.toSource(),
+ "The imported data is identical to the original data (4).");
+ is(importedData.allocations.toSource(), originalData.allocations.toSource(),
+ "The imported data is identical to the original data (5).");
+ is(importedData.profile.toSource(), originalData.profile.toSource(),
+ "The imported data is identical to the original data (6).");
+ is(importedData.configuration.withTicks, originalData.configuration.withTicks,
+ "The imported data is identical to the original data (7).");
+ is(importedData.configuration.withMemory, originalData.configuration.withMemory,
+ "The imported data is identical to the original data (8).");
+
+ yield teardown(panel);
+ finish();
+});
+/* eslint-enable */
diff --git a/devtools/client/performance/test/browser_perf-recordings-io-02.js b/devtools/client/performance/test/browser_perf-recordings-io-02.js
new file mode 100644
index 000000000..48e7fb63c
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-recordings-io-02.js
@@ -0,0 +1,26 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+/* eslint-disable */
+/**
+ * Tests if the performance tool gracefully handles loading bogus files.
+ */
+
+var test = Task.async(function* () {
+ let { target, panel, toolbox } = yield initPerformance(SIMPLE_URL);
+ let { EVENTS, PerformanceController } = panel.panelWin;
+
+ let file = FileUtils.getFile("TmpD", ["tmpprofile.json"]);
+ file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("666", 8));
+
+ try {
+ yield PerformanceController.importRecording("", file);
+ ok(false, "The recording succeeded unexpectedly.");
+ } catch (e) {
+ is(e.message, "Could not read recording data file.", "Message is correct.");
+ ok(true, "The recording was cancelled.");
+ }
+
+ yield teardown(panel);
+ finish();
+});
diff --git a/devtools/client/performance/test/browser_perf-recordings-io-03.js b/devtools/client/performance/test/browser_perf-recordings-io-03.js
new file mode 100644
index 000000000..e9fafd392
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-recordings-io-03.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+/* eslint-disable */
+/**
+ * Tests if the performance tool gracefully handles loading files that are JSON,
+ * but don't contain the appropriate recording data.
+ */
+
+var { FileUtils } = Cu.import("resource://gre/modules/FileUtils.jsm", {});
+var { NetUtil } = Cu.import("resource://gre/modules/NetUtil.jsm", {});
+
+var test = Task.async(function* () {
+ let { target, panel, toolbox } = yield initPerformance(SIMPLE_URL);
+ let { EVENTS, PerformanceController } = panel.panelWin;
+
+ let file = FileUtils.getFile("TmpD", ["tmpprofile.json"]);
+ file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("666", 8));
+ yield asyncCopy({ bogus: "data" }, file);
+
+ try {
+ yield PerformanceController.importRecording("", file);
+ ok(false, "The recording succeeded unexpectedly.");
+ } catch (e) {
+ is(e.message, "Unrecognized recording data file.", "Message is correct.");
+ ok(true, "The recording was cancelled.");
+ }
+
+ yield teardown(panel);
+ finish();
+});
+
+function getUnicodeConverter() {
+ let className = "@mozilla.org/intl/scriptableunicodeconverter";
+ let converter = Cc[className].createInstance(Ci.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+ return converter;
+}
+
+function asyncCopy(data, file) {
+ let deferred = Promise.defer();
+
+ let string = JSON.stringify(data);
+ let inputStream = getUnicodeConverter().convertToInputStream(string);
+ let outputStream = FileUtils.openSafeFileOutputStream(file);
+
+ NetUtil.asyncCopy(inputStream, outputStream, status => {
+ if (!Components.isSuccessCode(status)) {
+ deferred.reject(new Error("Could not save data to file."));
+ }
+ deferred.resolve();
+ });
+
+ return deferred.promise;
+}
+/* eslint-enable */
diff --git a/devtools/client/performance/test/browser_perf-recordings-io-04.js b/devtools/client/performance/test/browser_perf-recordings-io-04.js
new file mode 100644
index 000000000..2da84f438
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-recordings-io-04.js
@@ -0,0 +1,178 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+/* eslint-disable */
+/**
+ * Tests if the performance tool can import profiler data from the
+ * original profiler tool (Performance Recording v1, and Profiler data v2) and the correct views and graphs are loaded.
+ */
+var TICKS_DATA = (function () {
+ let ticks = [];
+ for (let i = 0; i < 100; i++) {
+ ticks.push(i * 10);
+ }
+ return ticks;
+})();
+
+var PROFILER_DATA = (function () {
+ let data = {};
+ let threads = data.threads = [];
+ let thread = {};
+ threads.push(thread);
+ thread.name = "Content";
+ thread.samples = [{
+ time: 5,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "C" }
+ ]
+ }, {
+ time: 5 + 6,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "D" }
+ ]
+ }, {
+ time: 5 + 6 + 7,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "E" },
+ { location: "F" }
+ ]
+ }, {
+ time: 20,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "C" },
+ { location: "D" },
+ { location: "E" },
+ { location: "F" },
+ { location: "G" }
+ ]
+ }];
+
+ // Handled in other deflating tests
+ thread.markers = [];
+
+ let meta = data.meta = {};
+ meta.version = 2;
+ meta.interval = 1;
+ meta.stackwalk = 0;
+ meta.product = "Firefox";
+ return data;
+})();
+
+var test = Task.async(function* () {
+ let { target, panel, toolbox } = yield initPerformance(SIMPLE_URL);
+ let { $, EVENTS, PerformanceController, DetailsView, OverviewView, JsCallTreeView } = panel.panelWin;
+
+ // Enable memory to test the memory-calltree and memory-flamegraph.
+ Services.prefs.setBoolPref(ALLOCATIONS_PREF, true);
+
+ // Create a structure from the data that mimics the old profiler's data.
+ // Different name for `ticks`, different way of storing time,
+ // and no memory, markers data.
+ let oldProfilerData = {
+ profilerData: { profile: PROFILER_DATA },
+ ticksData: TICKS_DATA,
+ recordingDuration: 10000,
+ fileType: "Recorded Performance Data",
+ version: 1
+ };
+
+ // Save recording as an old profiler data.
+ let file = FileUtils.getFile("TmpD", ["tmpprofile.json"]);
+ file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("666", 8));
+ yield asyncCopy(oldProfilerData, file);
+
+ // Import recording.
+
+ let calltreeRendered = once(OverviewView, EVENTS.UI_FRAMERATE_GRAPH_RENDERED);
+ let fpsRendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
+ let imported = once(PerformanceController, EVENTS.RECORDING_IMPORTED);
+ yield PerformanceController.importRecording("", file);
+
+ yield imported;
+ ok(true, "The original profiler data appears to have been successfully imported.");
+
+ yield calltreeRendered;
+ yield fpsRendered;
+ ok(true, "The imported data was re-rendered.");
+
+ // Ensure that only framerate and js calltree/flamegraph view are available
+ is(isVisible($("#overview-pane")), true, "overview graph container still shown");
+ is(isVisible($("#memory-overview")), false, "memory graph hidden");
+ is(isVisible($("#markers-overview")), false, "markers overview graph hidden");
+ is(isVisible($("#time-framerate")), true, "fps graph shown");
+ is($("#select-waterfall-view").hidden, true, "waterfall button hidden");
+ is($("#select-js-calltree-view").hidden, false, "jscalltree button shown");
+ is($("#select-js-flamegraph-view").hidden, false, "jsflamegraph button shown");
+ is($("#select-memory-calltree-view").hidden, true, "memorycalltree button hidden");
+ is($("#select-memory-flamegraph-view").hidden, true, "memoryflamegraph button hidden");
+ ok(DetailsView.isViewSelected(JsCallTreeView), "jscalltree view selected as its the only option");
+
+ // Verify imported recording.
+
+ let importedData = PerformanceController.getCurrentRecording().getAllData();
+ let expected = Object.create({
+ duration: 10000,
+ markers: [].toSource(),
+ frames: [].toSource(),
+ memory: [].toSource(),
+ ticks: TICKS_DATA.toSource(),
+ profile: RecordingUtils.deflateProfile(JSON.parse(JSON.stringify(PROFILER_DATA))).toSource(),
+ allocations: ({sites:[], timestamps:[], frames:[], sizes: []}).toSource(),
+ withTicks: true,
+ withMemory: false,
+ sampleFrequency: void 0
+ });
+
+ for (let field in expected) {
+ if (!!~["withTicks", "withMemory", "sampleFrequency"].indexOf(field)) {
+ is(importedData.configuration[field], expected[field], `${field} successfully converted in legacy import.`);
+ } else if (field === "profile") {
+ is(importedData.profile.toSource(), expected.profile,
+ "profiler data's samples successfully converted in legacy import.");
+ is(importedData.profile.meta.version, 3, "Updated meta version to 3.");
+ } else {
+ let data = importedData[field];
+ is(typeof data === "object" ? data.toSource() : data, expected[field],
+ `${field} successfully converted in legacy import.`);
+ }
+ }
+
+ yield teardown(panel);
+ finish();
+});
+
+function getUnicodeConverter() {
+ let className = "@mozilla.org/intl/scriptableunicodeconverter";
+ let converter = Cc[className].createInstance(Ci.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+ return converter;
+}
+
+function asyncCopy(data, file) {
+ let deferred = Promise.defer();
+
+ let string = JSON.stringify(data);
+ let inputStream = getUnicodeConverter().convertToInputStream(string);
+ let outputStream = FileUtils.openSafeFileOutputStream(file);
+
+ NetUtil.asyncCopy(inputStream, outputStream, status => {
+ if (!Components.isSuccessCode(status)) {
+ deferred.reject(new Error("Could not save data to file."));
+ }
+ deferred.resolve();
+ });
+
+ return deferred.promise;
+}
+/* eslint-enable */
diff --git a/devtools/client/performance/test/browser_perf-recordings-io-05.js b/devtools/client/performance/test/browser_perf-recordings-io-05.js
new file mode 100644
index 000000000..e836da917
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-recordings-io-05.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+/* eslint-disable */
+/**
+ * Test that when importing and no graphs rendered yet, we do not get a
+ * `getMappedSelection` error.
+ */
+
+var test = Task.async(function* () {
+ var { target, panel, toolbox } = yield initPerformance(SIMPLE_URL);
+ var { EVENTS, PerformanceController, WaterfallView } = panel.panelWin;
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ // Save recording.
+
+ let file = FileUtils.getFile("TmpD", ["tmpprofile.json"]);
+ file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("666", 8));
+
+ let exported = once(PerformanceController, EVENTS.RECORDING_EXPORTED);
+ yield PerformanceController.exportRecording("", PerformanceController.getCurrentRecording(), file);
+
+ yield exported;
+ ok(true, "The recording data appears to have been successfully saved.");
+
+ // Clear and re-import.
+
+ yield PerformanceController.clearRecordings();
+
+ let rendered = once(WaterfallView, EVENTS.UI_WATERFALL_RENDERED);
+ let imported = once(PerformanceController, EVENTS.RECORDING_IMPORTED);
+ yield PerformanceController.importRecording("", file);
+ yield imported;
+ yield rendered;
+
+ ok(true, "No error was thrown.");
+
+ yield teardown(panel);
+ finish();
+});
+/* eslint-enable */
diff --git a/devtools/client/performance/test/browser_perf-recordings-io-06.js b/devtools/client/performance/test/browser_perf-recordings-io-06.js
new file mode 100644
index 000000000..18734ce52
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-recordings-io-06.js
@@ -0,0 +1,142 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+/* eslint-disable */
+/**
+ * Tests if the performance tool can import profiler data when Profiler is v2
+ * and requires deflating, and has an extra thread that's a string. Not sure
+ * what causes this.
+ */
+var STRINGED_THREAD = (function () {
+ let thread = {};
+
+ thread.libs = [{
+ start: 123,
+ end: 456,
+ offset: 0,
+ name: "",
+ breakpadId: ""
+ }];
+ thread.meta = { version: 2, interval: 1, stackwalk: 0, processType: 1, startTime: 0 };
+ thread.threads = [{
+ name: "Plugin",
+ tid: 4197,
+ samples: [],
+ markers: [],
+ }];
+
+ return JSON.stringify(thread);
+})();
+
+var PROFILER_DATA = (function () {
+ let data = {};
+ let threads = data.threads = [];
+ let thread = {};
+ threads.push(thread);
+ threads.push(STRINGED_THREAD);
+ thread.name = "Content";
+ thread.samples = [{
+ time: 5,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "C" }
+ ]
+ }, {
+ time: 5 + 6,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "D" }
+ ]
+ }, {
+ time: 5 + 6 + 7,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "E" },
+ { location: "F" }
+ ]
+ }, {
+ time: 20,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "C" },
+ { location: "D" },
+ { location: "E" },
+ { location: "F" },
+ { location: "G" }
+ ]
+ }];
+
+ // Handled in other deflating tests
+ thread.markers = [];
+
+ let meta = data.meta = {};
+ meta.version = 2;
+ meta.interval = 1;
+ meta.stackwalk = 0;
+ meta.product = "Firefox";
+ return data;
+})();
+
+var test = Task.async(function* () {
+ let { target, panel, toolbox } = yield initPerformance(SIMPLE_URL);
+ let { $, EVENTS, PerformanceController, DetailsView, JsCallTreeView } = panel.panelWin;
+
+ let profilerData = {
+ profile: PROFILER_DATA,
+ duration: 10000,
+ configuration: {},
+ fileType: "Recorded Performance Data",
+ version: 2
+ };
+
+ let file = FileUtils.getFile("TmpD", ["tmpprofile.json"]);
+ file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("666", 8));
+ yield asyncCopy(profilerData, file);
+
+ // Import recording.
+
+ let calltreeRendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
+ let imported = once(PerformanceController, EVENTS.RECORDING_IMPORTED);
+ yield PerformanceController.importRecording("", file);
+
+ yield imported;
+ ok(true, "The profiler data appears to have been successfully imported.");
+
+ yield calltreeRendered;
+ ok(true, "The imported data was re-rendered.");
+
+ yield teardown(panel);
+ finish();
+});
+
+function getUnicodeConverter() {
+ let className = "@mozilla.org/intl/scriptableunicodeconverter";
+ let converter = Cc[className].createInstance(Ci.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+ return converter;
+}
+
+function asyncCopy(data, file) {
+ let deferred = Promise.defer();
+
+ let string = JSON.stringify(data);
+ let inputStream = getUnicodeConverter().convertToInputStream(string);
+ let outputStream = FileUtils.openSafeFileOutputStream(file);
+
+ NetUtil.asyncCopy(inputStream, outputStream, status => {
+ if (!Components.isSuccessCode(status)) {
+ deferred.reject(new Error("Could not save data to file."));
+ }
+ deferred.resolve();
+ });
+
+ return deferred.promise;
+}
+/* eslint-enable */
diff --git a/devtools/client/performance/test/browser_perf-refresh.js b/devtools/client/performance/test/browser_perf-refresh.js
new file mode 100644
index 000000000..825e2153f
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-refresh.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Rough test that the recording still continues after a refresh.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording, reload } = require("devtools/client/performance/test/helpers/actions");
+const { waitUntil } = require("devtools/client/performance/test/helpers/wait-utils");
+
+add_task(function* () {
+ let { panel, target } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { PerformanceController } = panel.panelWin;
+
+ yield startRecording(panel);
+ yield reload(target);
+
+ let recording = PerformanceController.getCurrentRecording();
+ let markersLength = recording.getAllData().markers.length;
+
+ ok(recording.isRecording(),
+ "RecordingModel should still be recording after reload");
+
+ yield waitUntil(() => recording.getMarkers().length > markersLength);
+ ok("Markers continue after reload.");
+
+ yield stopRecording(panel);
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-states.js b/devtools/client/performance/test/browser_perf-states.js
new file mode 100644
index 000000000..c01fb3121
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-states.js
@@ -0,0 +1,102 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that view states and lazy component intialization works.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { UI_ENABLE_MEMORY_PREF, UI_ENABLE_ALLOCATIONS_PREF } = require("devtools/client/performance/test/helpers/prefs");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+
+add_task(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { PerformanceView, OverviewView, DetailsView } = panel.panelWin;
+
+ is(PerformanceView.getState(), "empty",
+ "The intial state of the performance panel view is correct.");
+
+ ok(!(OverviewView.graphs.get("timeline")),
+ "The markers graph should not have been created yet.");
+ ok(!(OverviewView.graphs.get("memory")),
+ "The memory graph should not have been created yet.");
+ ok(!(OverviewView.graphs.get("framerate")),
+ "The framerate graph should not have been created yet.");
+
+ ok(!DetailsView.components.waterfall.initialized,
+ "The waterfall detail view should not have been created yet.");
+ ok(!DetailsView.components["js-calltree"].initialized,
+ "The js-calltree detail view should not have been created yet.");
+ ok(!DetailsView.components["js-flamegraph"].initialized,
+ "The js-flamegraph detail view should not have been created yet.");
+ ok(!DetailsView.components["memory-calltree"].initialized,
+ "The memory-calltree detail view should not have been created yet.");
+ ok(!DetailsView.components["memory-flamegraph"].initialized,
+ "The memory-flamegraph detail view should not have been created yet.");
+
+ Services.prefs.setBoolPref(UI_ENABLE_MEMORY_PREF, true);
+ Services.prefs.setBoolPref(UI_ENABLE_ALLOCATIONS_PREF, true);
+
+ ok(!(OverviewView.graphs.get("timeline")),
+ "The markers graph should still not have been created yet.");
+ ok(!(OverviewView.graphs.get("memory")),
+ "The memory graph should still not have been created yet.");
+ ok(!(OverviewView.graphs.get("framerate")),
+ "The framerate graph should still not have been created yet.");
+
+ yield startRecording(panel);
+
+ is(PerformanceView.getState(), "recording",
+ "The current state of the performance panel view is 'recording'.");
+ ok(OverviewView.graphs.get("memory"),
+ "The memory graph should have been created now.");
+ ok(OverviewView.graphs.get("framerate"),
+ "The framerate graph should have been created now.");
+
+ yield stopRecording(panel);
+
+ is(PerformanceView.getState(), "recorded",
+ "The current state of the performance panel view is 'recorded'.");
+ ok(!DetailsView.components["js-calltree"].initialized,
+ "The js-calltree detail view should still not have been created yet.");
+ ok(!DetailsView.components["js-flamegraph"].initialized,
+ "The js-flamegraph detail view should still not have been created yet.");
+ ok(!DetailsView.components["memory-calltree"].initialized,
+ "The memory-calltree detail view should still not have been created yet.");
+ ok(!DetailsView.components["memory-flamegraph"].initialized,
+ "The memory-flamegraph detail view should still not have been created yet.");
+
+ yield DetailsView.selectView("js-calltree");
+
+ is(PerformanceView.getState(), "recorded",
+ "The current state of the performance panel view is still 'recorded'.");
+ ok(DetailsView.components["js-calltree"].initialized,
+ "The js-calltree detail view should still have been created now.");
+ ok(!DetailsView.components["js-flamegraph"].initialized,
+ "The js-flamegraph detail view should still not have been created yet.");
+ ok(!DetailsView.components["memory-calltree"].initialized,
+ "The memory-calltree detail view should still not have been created yet.");
+ ok(!DetailsView.components["memory-flamegraph"].initialized,
+ "The memory-flamegraph detail view should still not have been created yet.");
+
+ yield DetailsView.selectView("memory-calltree");
+
+ is(PerformanceView.getState(), "recorded",
+ "The current state of the performance panel view is still 'recorded'.");
+ ok(DetailsView.components["js-calltree"].initialized,
+ "The js-calltree detail view should still register as being created.");
+ ok(!DetailsView.components["js-flamegraph"].initialized,
+ "The js-flamegraph detail view should still not have been created yet.");
+ ok(DetailsView.components["memory-calltree"].initialized,
+ "The memory-calltree detail view should still have been created now.");
+ ok(!DetailsView.components["memory-flamegraph"].initialized,
+ "The memory-flamegraph detail view should still not have been created yet.");
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-telemetry-01.js b/devtools/client/performance/test/browser_perf-telemetry-01.js
new file mode 100644
index 000000000..2c37e6c5a
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-telemetry-01.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the performance telemetry module records events at appropriate times.
+ * Specificaly the state about a recording process.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { UI_ENABLE_MEMORY_PREF } = require("devtools/client/performance/test/helpers/prefs");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+
+add_task(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { PerformanceController } = panel.panelWin;
+
+ let telemetry = PerformanceController._telemetry;
+ let logs = telemetry.getLogs();
+ let DURATION = "DEVTOOLS_PERFTOOLS_RECORDING_DURATION_MS";
+ let COUNT = "DEVTOOLS_PERFTOOLS_RECORDING_COUNT";
+ let CONSOLE_COUNT = "DEVTOOLS_PERFTOOLS_CONSOLE_RECORDING_COUNT";
+ let FEATURES = "DEVTOOLS_PERFTOOLS_RECORDING_FEATURES_USED";
+
+ Services.prefs.setBoolPref(UI_ENABLE_MEMORY_PREF, false);
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ Services.prefs.setBoolPref(UI_ENABLE_MEMORY_PREF, true);
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ is(logs[DURATION].length, 2, `There are two entries for ${DURATION}.`);
+ ok(logs[DURATION].every(d => typeof d === "number"),
+ `Every ${DURATION} entry is a number.`);
+ is(logs[COUNT].length, 2, `There are two entries for ${COUNT}.`);
+ is(logs[CONSOLE_COUNT], void 0, `There are no entries for ${CONSOLE_COUNT}.`);
+ is(logs[FEATURES].length, 8,
+ `There are two recordings worth of entries for ${FEATURES}.`);
+ ok(logs[FEATURES].find(r => r[0] === "withMemory" && r[1] === true),
+ "One feature entry for memory enabled.");
+ ok(logs[FEATURES].find(r => r[0] === "withMemory" && r[1] === false),
+ "One feature entry for memory disabled.");
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-telemetry-02.js b/devtools/client/performance/test/browser_perf-telemetry-02.js
new file mode 100644
index 000000000..6fe268e3a
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-telemetry-02.js
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the performance telemetry module records events at appropriate times.
+ * Specifically export/import.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+const { once } = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { EVENTS, PerformanceController } = panel.panelWin;
+
+ let telemetry = PerformanceController._telemetry;
+ let logs = telemetry.getLogs();
+ let EXPORTED = "DEVTOOLS_PERFTOOLS_RECORDING_EXPORT_FLAG";
+ let IMPORTED = "DEVTOOLS_PERFTOOLS_RECORDING_IMPORT_FLAG";
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ let file = FileUtils.getFile("TmpD", ["tmpprofile.json"]);
+ file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("666", 8));
+
+ let exported = once(PerformanceController, EVENTS.RECORDING_EXPORTED);
+ yield PerformanceController.exportRecording("",
+ PerformanceController.getCurrentRecording(), file);
+ yield exported;
+
+ ok(logs[EXPORTED], `A telemetry entry for ${EXPORTED} exists after exporting.`);
+
+ let imported = once(PerformanceController, EVENTS.RECORDING_IMPORTED);
+ yield PerformanceController.importRecording(null, file);
+ yield imported;
+
+ ok(logs[IMPORTED], `A telemetry entry for ${IMPORTED} exists after importing.`);
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-telemetry-03.js b/devtools/client/performance/test/browser_perf-telemetry-03.js
new file mode 100644
index 000000000..a10f314d2
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-telemetry-03.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the performance telemetry module records events at appropriate times.
+ * Specifically the destruction of certain views.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+const { once } = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let {
+ EVENTS,
+ PerformanceController,
+ DetailsView,
+ JsCallTreeView,
+ JsFlameGraphView
+ } = panel.panelWin;
+
+ let telemetry = PerformanceController._telemetry;
+ let logs = telemetry.getLogs();
+ let VIEWS = "DEVTOOLS_PERFTOOLS_SELECTED_VIEW_MS";
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ let calltreeRendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
+ let flamegraphRendered = once(JsFlameGraphView, EVENTS.UI_JS_FLAMEGRAPH_RENDERED);
+
+ // Go through some views to check later.
+ yield DetailsView.selectView("js-calltree");
+ yield calltreeRendered;
+
+ yield DetailsView.selectView("js-flamegraph");
+ yield flamegraphRendered;
+
+ yield teardownToolboxAndRemoveTab(panel);
+
+ // Check views after destruction to ensure `js-flamegraph` gets called
+ // with a time during destruction.
+ ok(logs[VIEWS].find(r => r[0] === "waterfall" && typeof r[1] === "number"),
+ `${VIEWS} for waterfall view and time.`);
+ ok(logs[VIEWS].find(r => r[0] === "js-calltree" && typeof r[1] === "number"),
+ `${VIEWS} for js-calltree view and time.`);
+ ok(logs[VIEWS].find(r => r[0] === "js-flamegraph" && typeof r[1] === "number"),
+ `${VIEWS} for js-flamegraph view and time.`);
+});
diff --git a/devtools/client/performance/test/browser_perf-telemetry-04.js b/devtools/client/performance/test/browser_perf-telemetry-04.js
new file mode 100644
index 000000000..362b54714
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-telemetry-04.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the performance telemetry module records events at appropriate times.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { initPerformanceInTab, initConsoleInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { waitForRecordingStartedEvents, waitForRecordingStoppedEvents } = require("devtools/client/performance/test/helpers/actions");
+
+add_task(function* () {
+ let { target, console } = yield initConsoleInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { panel } = yield initPerformanceInTab({ tab: target.tab });
+ let { PerformanceController } = panel.panelWin;
+
+ let telemetry = PerformanceController._telemetry;
+ let logs = telemetry.getLogs();
+ let DURATION = "DEVTOOLS_PERFTOOLS_RECORDING_DURATION_MS";
+ let CONSOLE_COUNT = "DEVTOOLS_PERFTOOLS_CONSOLE_RECORDING_COUNT";
+ let FEATURES = "DEVTOOLS_PERFTOOLS_RECORDING_FEATURES_USED";
+
+ let started = waitForRecordingStartedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true
+ });
+ yield console.profile("rust");
+ yield started;
+
+ let stopped = waitForRecordingStoppedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true
+ });
+ yield console.profileEnd("rust");
+ yield stopped;
+
+ is(logs[DURATION].length, 1, `There is one entry for ${DURATION}.`);
+ ok(logs[DURATION].every(d => typeof d === "number"),
+ `Every ${DURATION} entry is a number.`);
+ is(logs[CONSOLE_COUNT].length, 1, `There is one entry for ${CONSOLE_COUNT}.`);
+ is(logs[FEATURES].length, 4,
+ `There is one recording worth of entries for ${FEATURES}.`);
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-theme-toggle.js b/devtools/client/performance/test/browser_perf-theme-toggle.js
new file mode 100644
index 000000000..f8dbe9767
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-theme-toggle.js
@@ -0,0 +1,78 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+/* eslint-disable */
+/**
+ * Tests if the markers and memory overviews render with the correct
+ * theme on load, and rerenders when changed.
+ */
+
+const { setTheme } = require("devtools/client/shared/theme");
+
+const LIGHT_BG = "white";
+const DARK_BG = "#14171a";
+
+setTheme("dark");
+Services.prefs.setBoolPref(MEMORY_PREF, false);
+
+requestLongerTimeout(2);
+
+function* spawnTest() {
+ let { panel } = yield initPerformance(SIMPLE_URL);
+ let { EVENTS, $, OverviewView, document: doc } = panel.panelWin;
+
+ yield startRecording(panel);
+ let markers = OverviewView.graphs.get("timeline");
+ is(markers.backgroundColor, DARK_BG,
+ "correct theme on load for markers.");
+ yield stopRecording(panel);
+
+ let refreshed = once(markers, "refresh");
+ setTheme("light");
+ yield refreshed;
+
+ ok(true, "markers were rerendered after theme change.");
+ is(markers.backgroundColor, LIGHT_BG,
+ "correct theme on after toggle for markers.");
+
+ // reset back to dark
+ refreshed = once(markers, "refresh");
+ setTheme("dark");
+ yield refreshed;
+
+ info("Testing with memory overview");
+
+ Services.prefs.setBoolPref(MEMORY_PREF, true);
+
+ yield startRecording(panel);
+ let memory = OverviewView.graphs.get("memory");
+ is(memory.backgroundColor, DARK_BG,
+ "correct theme on load for memory.");
+ yield stopRecording(panel);
+
+ refreshed = Promise.all([
+ once(markers, "refresh"),
+ once(memory, "refresh"),
+ ]);
+ setTheme("light");
+ yield refreshed;
+
+ ok(true, "Both memory and markers were rerendered after theme change.");
+ is(markers.backgroundColor, LIGHT_BG,
+ "correct theme on after toggle for markers.");
+ is(memory.backgroundColor, LIGHT_BG,
+ "correct theme on after toggle for memory.");
+
+ refreshed = Promise.all([
+ once(markers, "refresh"),
+ once(memory, "refresh"),
+ ]);
+
+ // Set theme back to light
+ setTheme("light");
+ yield refreshed;
+
+ yield teardown(panel);
+ finish();
+}
+/* eslint-enable */
diff --git a/devtools/client/performance/test/browser_perf-tree-abstract-01.js b/devtools/client/performance/test/browser_perf-tree-abstract-01.js
new file mode 100644
index 000000000..9b56a1b8c
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-tree-abstract-01.js
@@ -0,0 +1,154 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if the abstract tree base class for the profiler's tree view
+ * works as advertised.
+ */
+
+const { appendAndWaitForPaint } = require("devtools/client/performance/test/helpers/dom-utils");
+const { synthesizeCustomTreeClass } = require("devtools/client/performance/test/helpers/synth-utils");
+const { once } = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(function* () {
+ let { MyCustomTreeItem, myDataSrc } = synthesizeCustomTreeClass();
+
+ let container = document.createElement("vbox");
+ yield appendAndWaitForPaint(gBrowser.selectedBrowser.parentNode, container);
+
+ // Populate the tree and test the root item...
+
+ let treeRoot = new MyCustomTreeItem(myDataSrc, { parent: null });
+ treeRoot.attachTo(container);
+
+ ok(!treeRoot.expanded,
+ "The root node should not be expanded yet.");
+ ok(!treeRoot.populated,
+ "The root node should not be populated yet.");
+
+ is(container.childNodes.length, 1,
+ "The container node should have one child available.");
+ is(container.childNodes[0], treeRoot.target,
+ "The root node's target is a child of the container node.");
+
+ is(treeRoot.root, treeRoot,
+ "The root node has the correct root.");
+ is(treeRoot.parent, null,
+ "The root node has the correct parent.");
+ is(treeRoot.level, 0,
+ "The root node has the correct level.");
+ is(treeRoot.target.style.marginInlineStart, "0px",
+ "The root node's indentation is correct.");
+ is(treeRoot.target.textContent, "root",
+ "The root node's text contents are correct.");
+ is(treeRoot.container, container,
+ "The root node's container is correct.");
+
+ // Expand the root and test the child items...
+
+ let receivedExpandEvent = once(treeRoot, "expand", { spreadArgs: true });
+ let receivedFocusEvent = once(treeRoot, "focus");
+ mousedown(treeRoot.target.querySelector(".arrow"));
+
+ let [, eventItem] = yield receivedExpandEvent;
+ is(eventItem, treeRoot,
+ "The 'expand' event target is correct (1).");
+
+ yield receivedFocusEvent;
+ is(document.commandDispatcher.focusedElement, treeRoot.target,
+ "The root node is now focused.");
+
+ let fooItem = treeRoot.getChild(0);
+ let barItem = treeRoot.getChild(1);
+
+ is(container.childNodes.length, 3,
+ "The container node should now have three children available.");
+ is(container.childNodes[0], treeRoot.target,
+ "The root node's target is a child of the container node.");
+ is(container.childNodes[1], fooItem.target,
+ "The 'foo' node's target is a child of the container node.");
+ is(container.childNodes[2], barItem.target,
+ "The 'bar' node's target is a child of the container node.");
+
+ is(fooItem.root, treeRoot,
+ "The 'foo' node has the correct root.");
+ is(fooItem.parent, treeRoot,
+ "The 'foo' node has the correct parent.");
+ is(fooItem.level, 1,
+ "The 'foo' node has the correct level.");
+ is(fooItem.target.style.marginInlineStart, "10px",
+ "The 'foo' node's indentation is correct.");
+ is(fooItem.target.textContent, "foo",
+ "The 'foo' node's text contents are correct.");
+ is(fooItem.container, container,
+ "The 'foo' node's container is correct.");
+
+ is(barItem.root, treeRoot,
+ "The 'bar' node has the correct root.");
+ is(barItem.parent, treeRoot,
+ "The 'bar' node has the correct parent.");
+ is(barItem.level, 1,
+ "The 'bar' node has the correct level.");
+ is(barItem.target.style.marginInlineStart, "10px",
+ "The 'bar' node's indentation is correct.");
+ is(barItem.target.textContent, "bar",
+ "The 'bar' node's text contents are correct.");
+ is(barItem.container, container,
+ "The 'bar' node's container is correct.");
+
+ // Test clicking on the `foo` node...
+
+ receivedFocusEvent = once(treeRoot, "focus", { spreadArgs: true });
+ mousedown(fooItem.target);
+
+ [, eventItem] = yield receivedFocusEvent;
+ is(eventItem, fooItem,
+ "The 'focus' event target is correct (2).");
+ is(document.commandDispatcher.focusedElement, fooItem.target,
+ "The 'foo' node is now focused.");
+
+ // Test double clicking on the `bar` node...
+
+ receivedExpandEvent = once(treeRoot, "expand", { spreadArgs: true });
+ receivedFocusEvent = once(treeRoot, "focus");
+ dblclick(barItem.target);
+
+ [, eventItem] = yield receivedExpandEvent;
+ is(eventItem, barItem,
+ "The 'expand' event target is correct (3).");
+
+ yield receivedFocusEvent;
+ is(document.commandDispatcher.focusedElement, barItem.target,
+ "The 'foo' node is now focused.");
+
+ // A child item got expanded, test the descendants...
+
+ let bazItem = barItem.getChild(0);
+
+ is(container.childNodes.length, 4,
+ "The container node should now have four children available.");
+ is(container.childNodes[0], treeRoot.target,
+ "The root node's target is a child of the container node.");
+ is(container.childNodes[1], fooItem.target,
+ "The 'foo' node's target is a child of the container node.");
+ is(container.childNodes[2], barItem.target,
+ "The 'bar' node's target is a child of the container node.");
+ is(container.childNodes[3], bazItem.target,
+ "The 'baz' node's target is a child of the container node.");
+
+ is(bazItem.root, treeRoot,
+ "The 'baz' node has the correct root.");
+ is(bazItem.parent, barItem,
+ "The 'baz' node has the correct parent.");
+ is(bazItem.level, 2,
+ "The 'baz' node has the correct level.");
+ is(bazItem.target.style.marginInlineStart, "20px",
+ "The 'baz' node's indentation is correct.");
+ is(bazItem.target.textContent, "baz",
+ "The 'baz' node's text contents are correct.");
+ is(bazItem.container, container,
+ "The 'baz' node's container is correct.");
+
+ container.remove();
+});
diff --git a/devtools/client/performance/test/browser_perf-tree-abstract-02.js b/devtools/client/performance/test/browser_perf-tree-abstract-02.js
new file mode 100644
index 000000000..62db4cfd6
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-tree-abstract-02.js
@@ -0,0 +1,138 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if the abstract tree base class for the profiler's tree view
+ * has a functional public API.
+ */
+
+const { appendAndWaitForPaint } = require("devtools/client/performance/test/helpers/dom-utils");
+const { synthesizeCustomTreeClass } = require("devtools/client/performance/test/helpers/synth-utils");
+
+add_task(function* () {
+ let { MyCustomTreeItem, myDataSrc } = synthesizeCustomTreeClass();
+
+ let container = document.createElement("vbox");
+ yield appendAndWaitForPaint(gBrowser.selectedBrowser.parentNode, container);
+
+ // Populate the tree and test the root item...
+
+ let treeRoot = new MyCustomTreeItem(myDataSrc, { parent: null });
+ treeRoot.autoExpandDepth = 1;
+ treeRoot.attachTo(container);
+
+ ok(treeRoot.expanded,
+ "The root node should now be expanded.");
+ ok(treeRoot.populated,
+ "The root node should now be populated.");
+
+ let fooItem = treeRoot.getChild(0);
+ let barItem = treeRoot.getChild(1);
+ ok(!fooItem.expanded && !barItem.expanded,
+ "The 'foo' and 'bar' nodes should not be expanded yet.");
+ ok(!fooItem.populated && !barItem.populated,
+ "The 'foo' and 'bar' nodes should not be populated yet.");
+
+ fooItem.expand();
+ barItem.expand();
+ ok(fooItem.expanded && barItem.expanded,
+ "The 'foo' and 'bar' nodes should now be expanded.");
+ ok(!fooItem.populated,
+ "The 'foo' node should not be populated because it's empty.");
+ ok(barItem.populated,
+ "The 'bar' node should now be populated.");
+
+ let bazItem = barItem.getChild(0);
+ ok(!bazItem.expanded,
+ "The 'bar' node should not be expanded yet.");
+ ok(!bazItem.populated,
+ "The 'bar' node should not be populated yet.");
+
+ bazItem.expand();
+ ok(bazItem.expanded,
+ "The 'baz' node should now be expanded.");
+ ok(!bazItem.populated,
+ "The 'baz' node should not be populated because it's empty.");
+
+ ok(!treeRoot.getChild(-1) && !treeRoot.getChild(2),
+ "Calling `getChild` with out of bounds indices will return null (1).");
+ ok(!fooItem.getChild(-1) && !fooItem.getChild(0),
+ "Calling `getChild` with out of bounds indices will return null (2).");
+ ok(!barItem.getChild(-1) && !barItem.getChild(1),
+ "Calling `getChild` with out of bounds indices will return null (3).");
+ ok(!bazItem.getChild(-1) && !bazItem.getChild(0),
+ "Calling `getChild` with out of bounds indices will return null (4).");
+
+ // Finished expanding all nodes in the tree...
+ // Continue checking.
+
+ is(container.childNodes.length, 4,
+ "The container node should now have four children available.");
+ is(container.childNodes[0], treeRoot.target,
+ "The root node's target is a child of the container node.");
+ is(container.childNodes[1], fooItem.target,
+ "The 'foo' node's target is a child of the container node.");
+ is(container.childNodes[2], barItem.target,
+ "The 'bar' node's target is a child of the container node.");
+ is(container.childNodes[3], bazItem.target,
+ "The 'baz' node's target is a child of the container node.");
+
+ treeRoot.collapse();
+ is(container.childNodes.length, 1,
+ "The container node should now have one children available.");
+
+ ok(!treeRoot.expanded,
+ "The root node should not be expanded anymore.");
+ ok(fooItem.expanded && barItem.expanded && bazItem.expanded,
+ "The 'foo', 'bar' and 'baz' nodes should still be expanded.");
+ ok(treeRoot.populated && barItem.populated,
+ "The root and 'bar' nodes should still be populated.");
+ ok(!fooItem.populated && !bazItem.populated,
+ "The 'foo' and 'baz' nodes should still not be populated because they're empty.");
+
+ treeRoot.expand();
+ is(container.childNodes.length, 4,
+ "The container node should now have four children available again.");
+
+ ok(treeRoot.expanded && fooItem.expanded && barItem.expanded && bazItem.expanded,
+ "The root, 'foo', 'bar' and 'baz' nodes should now be reexpanded.");
+ ok(treeRoot.populated && barItem.populated,
+ "The root and 'bar' nodes should still be populated.");
+ ok(!fooItem.populated && !bazItem.populated,
+ "The 'foo' and 'baz' nodes should still not be populated because they're empty.");
+
+ // Test `focus` on the root node...
+
+ treeRoot.focus();
+ is(document.commandDispatcher.focusedElement, treeRoot.target,
+ "The root node is now focused.");
+
+ // Test `focus` on a leaf node...
+
+ bazItem.focus();
+ is(document.commandDispatcher.focusedElement, bazItem.target,
+ "The 'baz' node is now focused.");
+
+ // Test `remove`...
+
+ barItem.remove();
+ is(container.childNodes.length, 2,
+ "The container node should now have two children available.");
+ is(container.childNodes[0], treeRoot.target,
+ "The root node should be the first in the container node.");
+ is(container.childNodes[1], fooItem.target,
+ "The 'foo' node should be the second in the container node.");
+
+ fooItem.remove();
+ is(container.childNodes.length, 1,
+ "The container node should now have one children available.");
+ is(container.childNodes[0], treeRoot.target,
+ "The root node should be the only in the container node.");
+
+ treeRoot.remove();
+ is(container.childNodes.length, 0,
+ "The container node should now have no children available.");
+
+ container.remove();
+});
diff --git a/devtools/client/performance/test/browser_perf-tree-abstract-03.js b/devtools/client/performance/test/browser_perf-tree-abstract-03.js
new file mode 100644
index 000000000..4e427fcd3
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-tree-abstract-03.js
@@ -0,0 +1,151 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if the abstract tree base class for the profiler's tree view
+ * is keyboard accessible.
+ */
+
+const { appendAndWaitForPaint } = require("devtools/client/performance/test/helpers/dom-utils");
+const { synthesizeCustomTreeClass } = require("devtools/client/performance/test/helpers/synth-utils");
+
+add_task(function* () {
+ let { MyCustomTreeItem, myDataSrc } = synthesizeCustomTreeClass();
+
+ let container = document.createElement("vbox");
+ yield appendAndWaitForPaint(gBrowser.selectedBrowser.parentNode, container);
+
+ // Populate the tree by pressing RIGHT...
+
+ let treeRoot = new MyCustomTreeItem(myDataSrc, { parent: null });
+ treeRoot.attachTo(container);
+ treeRoot.focus();
+
+ key("VK_RIGHT");
+ ok(treeRoot.expanded,
+ "The root node is now expanded.");
+ is(document.commandDispatcher.focusedElement, treeRoot.target,
+ "The root node is still focused.");
+
+ let fooItem = treeRoot.getChild(0);
+ let barItem = treeRoot.getChild(1);
+
+ key("VK_RIGHT");
+ ok(!fooItem.expanded,
+ "The 'foo' node is not expanded yet.");
+ is(document.commandDispatcher.focusedElement, fooItem.target,
+ "The 'foo' node is now focused.");
+
+ key("VK_RIGHT");
+ ok(fooItem.expanded,
+ "The 'foo' node is now expanded.");
+ is(document.commandDispatcher.focusedElement, fooItem.target,
+ "The 'foo' node is still focused.");
+
+ key("VK_RIGHT");
+ ok(!barItem.expanded,
+ "The 'bar' node is not expanded yet.");
+ is(document.commandDispatcher.focusedElement, barItem.target,
+ "The 'bar' node is now focused.");
+
+ key("VK_RIGHT");
+ ok(barItem.expanded,
+ "The 'bar' node is now expanded.");
+ is(document.commandDispatcher.focusedElement, barItem.target,
+ "The 'bar' node is still focused.");
+
+ let bazItem = barItem.getChild(0);
+
+ key("VK_RIGHT");
+ ok(!bazItem.expanded,
+ "The 'baz' node is not expanded yet.");
+ is(document.commandDispatcher.focusedElement, bazItem.target,
+ "The 'baz' node is now focused.");
+
+ key("VK_RIGHT");
+ ok(bazItem.expanded,
+ "The 'baz' node is now expanded.");
+ is(document.commandDispatcher.focusedElement, bazItem.target,
+ "The 'baz' node is still focused.");
+
+ // Test RIGHT on a leaf node.
+
+ key("VK_RIGHT");
+ is(document.commandDispatcher.focusedElement, bazItem.target,
+ "The 'baz' node is still focused.");
+
+ // Test DOWN on a leaf node.
+
+ key("VK_DOWN");
+ is(document.commandDispatcher.focusedElement, bazItem.target,
+ "The 'baz' node is now refocused.");
+
+ // Test UP.
+
+ key("VK_UP");
+ is(document.commandDispatcher.focusedElement, barItem.target,
+ "The 'bar' node is now refocused.");
+
+ key("VK_UP");
+ is(document.commandDispatcher.focusedElement, fooItem.target,
+ "The 'foo' node is now refocused.");
+
+ key("VK_UP");
+ is(document.commandDispatcher.focusedElement, treeRoot.target,
+ "The root node is now refocused.");
+
+ // Test DOWN.
+
+ key("VK_DOWN");
+ is(document.commandDispatcher.focusedElement, fooItem.target,
+ "The 'foo' node is now refocused.");
+
+ key("VK_DOWN");
+ is(document.commandDispatcher.focusedElement, barItem.target,
+ "The 'bar' node is now refocused.");
+
+ key("VK_DOWN");
+ is(document.commandDispatcher.focusedElement, bazItem.target,
+ "The 'baz' node is now refocused.");
+
+ // Test LEFT.
+
+ key("VK_LEFT");
+ ok(barItem.expanded,
+ "The 'bar' node is still expanded.");
+ is(document.commandDispatcher.focusedElement, barItem.target,
+ "The 'bar' node is now refocused.");
+
+ key("VK_LEFT");
+ ok(!barItem.expanded,
+ "The 'bar' node is not expanded anymore.");
+ is(document.commandDispatcher.focusedElement, barItem.target,
+ "The 'bar' node is still focused.");
+
+ key("VK_LEFT");
+ ok(treeRoot.expanded,
+ "The root node is still expanded.");
+ is(document.commandDispatcher.focusedElement, treeRoot.target,
+ "The root node is now refocused.");
+
+ key("VK_LEFT");
+ ok(!treeRoot.expanded,
+ "The root node is not expanded anymore.");
+ is(document.commandDispatcher.focusedElement, treeRoot.target,
+ "The root node is still focused.");
+
+ // Test LEFT on the root node.
+
+ key("VK_LEFT");
+ is(document.commandDispatcher.focusedElement, treeRoot.target,
+ "The root node is still focused.");
+
+ // Test UP on the root node.
+
+ key("VK_UP");
+ is(document.commandDispatcher.focusedElement, treeRoot.target,
+ "The root node is still focused.");
+
+ container.remove();
+});
diff --git a/devtools/client/performance/test/browser_perf-tree-abstract-04.js b/devtools/client/performance/test/browser_perf-tree-abstract-04.js
new file mode 100644
index 000000000..614235ab8
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-tree-abstract-04.js
@@ -0,0 +1,35 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the treeview expander arrow doesn't react to dblclick events.
+ */
+
+const { appendAndWaitForPaint } = require("devtools/client/performance/test/helpers/dom-utils");
+const { synthesizeCustomTreeClass } = require("devtools/client/performance/test/helpers/synth-utils");
+const { once } = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(function* () {
+ let { MyCustomTreeItem, myDataSrc } = synthesizeCustomTreeClass();
+
+ let container = document.createElement("vbox");
+ yield appendAndWaitForPaint(gBrowser.selectedBrowser.parentNode, container);
+
+ // Populate the tree and test the root item...
+
+ let treeRoot = new MyCustomTreeItem(myDataSrc, { parent: null });
+ treeRoot.attachTo(container);
+
+ let originalTreeRootExpandedState = treeRoot.expanded;
+ info("Double clicking on the root item arrow and waiting for focus event.");
+
+ let receivedFocusEvent = once(treeRoot, "focus");
+ dblclick(treeRoot.target.querySelector(".arrow"));
+ yield receivedFocusEvent;
+
+ is(treeRoot.expanded, originalTreeRootExpandedState,
+ "A double click on the arrow was ignored.");
+
+ container.remove();
+});
diff --git a/devtools/client/performance/test/browser_perf-tree-abstract-05.js b/devtools/client/performance/test/browser_perf-tree-abstract-05.js
new file mode 100644
index 000000000..88138b6f3
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-tree-abstract-05.js
@@ -0,0 +1,103 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if the abstract tree base class for the profiler's tree view
+ * supports PageUp/PageDown/Home/End keys.
+ */
+
+const { appendAndWaitForPaint } = require("devtools/client/performance/test/helpers/dom-utils");
+const { synthesizeCustomTreeClass } = require("devtools/client/performance/test/helpers/synth-utils");
+
+add_task(function* () {
+ let { MyCustomTreeItem } = synthesizeCustomTreeClass();
+
+ let container = document.createElement("vbox");
+ container.style.height = "100%";
+ container.style.overflow = "scroll";
+ yield appendAndWaitForPaint(gBrowser.selectedBrowser.parentNode, container);
+
+ let myDataSrc = {
+ label: "root",
+ children: []
+ };
+
+ for (let i = 0; i < 1000; i++) {
+ myDataSrc.children.push({
+ label: "child-" + i,
+ children: []
+ });
+ }
+
+ let treeRoot = new MyCustomTreeItem(myDataSrc, { parent: null });
+ treeRoot.attachTo(container);
+ treeRoot.focus();
+ treeRoot.expand();
+
+ is(document.commandDispatcher.focusedElement, treeRoot.target,
+ "The root node is focused.");
+
+ // Test HOME and END
+
+ key("VK_END");
+ is(document.commandDispatcher.focusedElement,
+ treeRoot.getChild(myDataSrc.children.length - 1).target,
+ "The last node is focused.");
+
+ key("VK_HOME");
+ is(document.commandDispatcher.focusedElement, treeRoot.target,
+ "The first (root) node is focused.");
+
+ // Test PageUp and PageDown
+
+ let nodesPerPageSize = treeRoot._getNodesPerPageSize();
+
+ key("VK_PAGE_DOWN");
+ is(document.commandDispatcher.focusedElement,
+ treeRoot.getChild(nodesPerPageSize - 1).target,
+ "The first node in the second page is focused.");
+
+ key("VK_PAGE_DOWN");
+ is(document.commandDispatcher.focusedElement,
+ treeRoot.getChild(nodesPerPageSize * 2 - 1).target,
+ "The first node in the third page is focused.");
+
+ key("VK_PAGE_UP");
+ is(document.commandDispatcher.focusedElement,
+ treeRoot.getChild(nodesPerPageSize - 1).target,
+ "The first node in the second page is focused.");
+
+ key("VK_PAGE_UP");
+ is(document.commandDispatcher.focusedElement, treeRoot.target,
+ "The first (root) node is focused.");
+
+ // Test PageUp in the middle of the first page
+
+ let middleIndex = Math.floor(nodesPerPageSize / 2);
+
+ treeRoot.getChild(middleIndex).target.focus();
+ is(document.commandDispatcher.focusedElement,
+ treeRoot.getChild(middleIndex).target,
+ "The middle node in the first page is focused.");
+
+ key("VK_PAGE_UP");
+ is(document.commandDispatcher.focusedElement, treeRoot.target,
+ "The first (root) node is focused.");
+
+ // Test PageDown in the middle of the last page
+
+ middleIndex = Math.ceil(myDataSrc.children.length - middleIndex);
+
+ treeRoot.getChild(middleIndex).target.focus();
+ is(document.commandDispatcher.focusedElement,
+ treeRoot.getChild(middleIndex).target,
+ "The middle node in the last page is focused.");
+
+ key("VK_PAGE_DOWN");
+ is(document.commandDispatcher.focusedElement,
+ treeRoot.getChild(myDataSrc.children.length - 1).target,
+ "The last node is focused.");
+
+ container.remove();
+});
diff --git a/devtools/client/performance/test/browser_perf-tree-view-01.js b/devtools/client/performance/test/browser_perf-tree-view-01.js
new file mode 100644
index 000000000..a2699d3d0
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-tree-view-01.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://foo/bar/creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if the profiler's tree view implementation works properly and
+ * creates the correct column structure.
+ */
+
+const { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
+const { CallView } = require("devtools/client/performance/modules/widgets/tree-view");
+const { synthesizeProfile } = require("devtools/client/performance/test/helpers/synth-utils");
+
+add_task(function () {
+ let profile = synthesizeProfile();
+ let threadNode = new ThreadNode(profile.threads[0], { startTime: 0, endTime: 20 });
+
+ // Don't display the synthesized (root) and the real (root) node twice.
+ threadNode.calls = threadNode.calls[0].calls;
+
+ let treeRoot = new CallView({ frame: threadNode });
+ let container = document.createElement("vbox");
+ treeRoot.autoExpandDepth = 0;
+ treeRoot.attachTo(container);
+
+ is(container.childNodes.length, 1,
+ "The container node should have one child available.");
+ is(container.childNodes[0].className, "call-tree-item",
+ "The root node in the tree has the correct class name.");
+
+ is(container.childNodes[0].childNodes.length, 6,
+ "The root node in the tree has the correct number of children.");
+ is(container.childNodes[0].querySelectorAll(".call-tree-cell").length, 6,
+ "The root node in the tree has only 6 'call-tree-cell' children.");
+
+ is(container.childNodes[0].childNodes[0].getAttribute("type"), "duration",
+ "The root node in the tree has a duration cell.");
+ is(container.childNodes[0].childNodes[0].textContent.trim(), "20 ms",
+ "The root node in the tree has the correct duration cell value.");
+
+ is(container.childNodes[0].childNodes[1].getAttribute("type"), "percentage",
+ "The root node in the tree has a percentage cell.");
+ is(container.childNodes[0].childNodes[1].textContent.trim(), "100%",
+ "The root node in the tree has the correct percentage cell value.");
+
+ is(container.childNodes[0].childNodes[2].getAttribute("type"), "self-duration",
+ "The root node in the tree has a self-duration cell.");
+ is(container.childNodes[0].childNodes[2].textContent.trim(), "0 ms",
+ "The root node in the tree has the correct self-duration cell value.");
+
+ is(container.childNodes[0].childNodes[3].getAttribute("type"), "self-percentage",
+ "The root node in the tree has a self-percentage cell.");
+ is(container.childNodes[0].childNodes[3].textContent.trim(), "0%",
+ "The root node in the tree has the correct self-percentage cell value.");
+
+ is(container.childNodes[0].childNodes[4].getAttribute("type"), "samples",
+ "The root node in the tree has an samples cell.");
+ is(container.childNodes[0].childNodes[4].textContent.trim(), "0",
+ "The root node in the tree has the correct samples cell value.");
+
+ is(container.childNodes[0].childNodes[5].getAttribute("type"), "function",
+ "The root node in the tree has a function cell.");
+ is(container.childNodes[0].childNodes[5].style.marginInlineStart, "0px",
+ "The root node in the tree has the correct indentation.");
+});
diff --git a/devtools/client/performance/test/browser_perf-tree-view-02.js b/devtools/client/performance/test/browser_perf-tree-view-02.js
new file mode 100644
index 000000000..bb325ba90
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-tree-view-02.js
@@ -0,0 +1,148 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if the profiler's tree view implementation works properly and
+ * creates the correct column structure after expanding some of the nodes.
+ * Also tests that demangling works.
+ */
+
+const { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
+const { CallView } = require("devtools/client/performance/modules/widgets/tree-view");
+const { synthesizeProfile } = require("devtools/client/performance/test/helpers/synth-utils");
+
+const MANGLED_FN = "__Z3FooIiEvv";
+const UNMANGLED_FN = "void Foo<int>()";
+
+add_task(function () {
+ // Create a profile and mangle a function inside the string table.
+ let profile = synthesizeProfile();
+
+ profile.threads[0].stringTable[1] =
+ profile.threads[0].stringTable[1].replace("A (", `${MANGLED_FN} (`);
+
+ let threadNode = new ThreadNode(profile.threads[0], { startTime: 0, endTime: 20 });
+
+ // Don't display the synthesized (root) and the real (root) node twice.
+ threadNode.calls = threadNode.calls[0].calls;
+
+ let treeRoot = new CallView({ frame: threadNode });
+ let container = document.createElement("vbox");
+ treeRoot.autoExpandDepth = 0;
+ treeRoot.attachTo(container);
+
+ let $$ = node => container.querySelectorAll(node);
+ let $fun = (node, ancestor) => (ancestor || container).querySelector(
+ ".call-tree-cell[type=function] > " + node);
+ let $$fun = (node, ancestor) => (ancestor || container).querySelectorAll(
+ ".call-tree-cell[type=function] > " + node);
+ let $$dur = i => container.querySelectorAll(".call-tree-cell[type=duration]")[i];
+ let $$per = i => container.querySelectorAll(".call-tree-cell[type=percentage]")[i];
+ let $$sam = i => container.querySelectorAll(".call-tree-cell[type=samples]")[i];
+
+ is(container.childNodes.length, 1,
+ "The container node should have one child available.");
+ is(container.childNodes[0].className, "call-tree-item",
+ "The root node in the tree has the correct class name.");
+
+ is($$dur(0).textContent.trim(), "20 ms",
+ "The root's duration cell displays the correct value.");
+ is($$per(0).textContent.trim(), "100%",
+ "The root's percentage cell displays the correct value.");
+ is($$sam(0).textContent.trim(), "0",
+ "The root's samples cell displays the correct value.");
+ is($$fun(".call-tree-name")[0].textContent.trim(), "(root)",
+ "The root's function cell displays the correct name.");
+ is($$fun(".call-tree-url")[0], null,
+ "The root's function cell displays no url.");
+ is($$fun(".call-tree-line")[0], null,
+ "The root's function cell displays no line.");
+ is($$fun(".call-tree-host")[0], null,
+ "The root's function cell displays no host.");
+ is($$fun(".call-tree-category")[0], null,
+ "The root's function cell displays no category.");
+
+ treeRoot.expand();
+
+ is(container.childNodes.length, 2,
+ "The container node should have two children available.");
+ is(container.childNodes[0].className, "call-tree-item",
+ "The root node in the tree has the correct class name.");
+ is(container.childNodes[1].className, "call-tree-item",
+ "The .A node in the tree has the correct class name.");
+
+ // Test demangling in the profiler tree.
+ is($$dur(1).textContent.trim(), "20 ms",
+ "The .A node's duration cell displays the correct value.");
+ is($$per(1).textContent.trim(), "100%",
+ "The .A node's percentage cell displays the correct value.");
+ is($$sam(1).textContent.trim(), "0",
+ "The .A node's samples cell displays the correct value.");
+
+ is($fun(".call-tree-name", $$(".call-tree-item")[1]).textContent.trim(), UNMANGLED_FN,
+ "The .A node's function cell displays the correct name.");
+ is($fun(".call-tree-url", $$(".call-tree-item")[1]).textContent.trim(), "baz",
+ "The .A node's function cell displays the correct url.");
+ is($fun(".call-tree-line", $$(".call-tree-item")[1]).textContent.trim(), ":12",
+ "The .A node's function cell displays the correct line.");
+ is($fun(".call-tree-host", $$(".call-tree-item")[1]).textContent.trim(), "foo",
+ "The .A node's function cell displays the correct host.");
+ is($fun(".call-tree-category", $$(".call-tree-item")[1]).textContent.trim(), "Gecko",
+ "The .A node's function cell displays the correct category.");
+
+ ok($$(".call-tree-item")[1].getAttribute("tooltiptext").includes(MANGLED_FN),
+ "The .A node's row's tooltip contains the original mangled name.");
+
+ let A = treeRoot.getChild();
+ A.expand();
+
+ is(container.childNodes.length, 4,
+ "The container node should have four children available.");
+ is(container.childNodes[0].className, "call-tree-item",
+ "The root node in the tree has the correct class name.");
+ is(container.childNodes[1].className, "call-tree-item",
+ "The .A node in the tree has the correct class name.");
+ is(container.childNodes[2].className, "call-tree-item",
+ "The .B node in the tree has the correct class name.");
+ is(container.childNodes[3].className, "call-tree-item",
+ "The .E node in the tree has the correct class name.");
+
+ is($$dur(2).textContent.trim(), "15 ms",
+ "The .A.B node's duration cell displays the correct value.");
+ is($$per(2).textContent.trim(), "75%",
+ "The .A.B node's percentage cell displays the correct value.");
+ is($$sam(2).textContent.trim(), "0",
+ "The .A.B node's samples cell displays the correct value.");
+ is($fun(".call-tree-name", $$(".call-tree-item")[2]).textContent.trim(), "B",
+ "The .A.B node's function cell displays the correct name.");
+ is($fun(".call-tree-url", $$(".call-tree-item")[2]).textContent.trim(), "baz",
+ "The .A.B node's function cell displays the correct url.");
+ ok($fun(".call-tree-url", $$(".call-tree-item")[2]).getAttribute("tooltiptext").includes("http://foo/bar/baz"),
+ "The .A.B node's function cell displays the correct url tooltiptext.");
+ is($fun(".call-tree-line", $$(".call-tree-item")[2]).textContent.trim(), ":34",
+ "The .A.B node's function cell displays the correct line.");
+ is($fun(".call-tree-host", $$(".call-tree-item")[2]).textContent.trim(), "foo",
+ "The .A.B node's function cell displays the correct host.");
+ is($fun(".call-tree-category", $$(".call-tree-item")[2]).textContent.trim(), "Styles",
+ "The .A.B node's function cell displays the correct category.");
+
+ is($$dur(3).textContent.trim(), "5 ms",
+ "The .A.E node's duration cell displays the correct value.");
+ is($$per(3).textContent.trim(), "25%",
+ "The .A.E node's percentage cell displays the correct value.");
+ is($$sam(3).textContent.trim(), "0",
+ "The .A.E node's samples cell displays the correct value.");
+ is($fun(".call-tree-name", $$(".call-tree-item")[3]).textContent.trim(), "E",
+ "The .A.E node's function cell displays the correct name.");
+ is($fun(".call-tree-url", $$(".call-tree-item")[3]).textContent.trim(), "baz",
+ "The .A.E node's function cell displays the correct url.");
+ ok($fun(".call-tree-url", $$(".call-tree-item")[3]).getAttribute("tooltiptext").includes("http://foo/bar/baz"),
+ "The .A.E node's function cell displays the correct url tooltiptext.");
+ is($fun(".call-tree-line", $$(".call-tree-item")[3]).textContent.trim(), ":90",
+ "The .A.E node's function cell displays the correct line.");
+ is($fun(".call-tree-host", $$(".call-tree-item")[3]).textContent.trim(), "foo",
+ "The .A.E node's function cell displays the correct host.");
+ is($fun(".call-tree-category", $$(".call-tree-item")[3]).textContent.trim(), "GC",
+ "The .A.E node's function cell displays the correct category.");
+});
diff --git a/devtools/client/performance/test/browser_perf-tree-view-03.js b/devtools/client/performance/test/browser_perf-tree-view-03.js
new file mode 100644
index 000000000..44ab50f32
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-tree-view-03.js
@@ -0,0 +1,79 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if the profiler's tree view implementation works properly and
+ * creates the correct column structure and can auto-expand all nodes.
+ */
+
+const { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
+const { CallView } = require("devtools/client/performance/modules/widgets/tree-view");
+const { synthesizeProfile } = require("devtools/client/performance/test/helpers/synth-utils");
+
+add_task(function () {
+ let profile = synthesizeProfile();
+ let threadNode = new ThreadNode(profile.threads[0], { startTime: 0, endTime: 20 });
+
+ // Don't display the synthesized (root) and the real (root) node twice.
+ threadNode.calls = threadNode.calls[0].calls;
+
+ let treeRoot = new CallView({ frame: threadNode });
+ let container = document.createElement("vbox");
+ treeRoot.attachTo(container);
+
+ let $$fun = i => container.querySelectorAll(".call-tree-cell[type=function]")[i];
+ let $$nam = i => container.querySelectorAll(
+ ".call-tree-cell[type=function] > .call-tree-name")[i];
+ let $$dur = i => container.querySelectorAll(".call-tree-cell[type=duration]")[i];
+
+ is(container.childNodes.length, 7,
+ "The container node should have all children available.");
+ is(Array.filter(container.childNodes, e => e.className != "call-tree-item").length, 0,
+ "All item nodes in the tree have the correct class name.");
+
+ is($$fun(0).style.marginInlineStart, "0px",
+ "The root node's function cell has the correct indentation.");
+ is($$fun(1).style.marginInlineStart, "16px",
+ "The .A node's function cell has the correct indentation.");
+ is($$fun(2).style.marginInlineStart, "32px",
+ "The .A.B node's function cell has the correct indentation.");
+ is($$fun(3).style.marginInlineStart, "48px",
+ "The .A.B.D node's function cell has the correct indentation.");
+ is($$fun(4).style.marginInlineStart, "48px",
+ "The .A.B.C node's function cell has the correct indentation.");
+ is($$fun(5).style.marginInlineStart, "32px",
+ "The .A.E node's function cell has the correct indentation.");
+ is($$fun(6).style.marginInlineStart, "48px",
+ "The .A.E.F node's function cell has the correct indentation.");
+
+ is($$nam(0).textContent.trim(), "(root)",
+ "The root node's function cell displays the correct name.");
+ is($$nam(1).textContent.trim(), "A",
+ "The .A node's function cell displays the correct name.");
+ is($$nam(2).textContent.trim(), "B",
+ "The .A.B node's function cell displays the correct name.");
+ is($$nam(3).textContent.trim(), "D",
+ "The .A.B.D node's function cell displays the correct name.");
+ is($$nam(4).textContent.trim(), "C",
+ "The .A.B.C node's function cell displays the correct name.");
+ is($$nam(5).textContent.trim(), "E",
+ "The .A.E node's function cell displays the correct name.");
+ is($$nam(6).textContent.trim(), "F",
+ "The .A.E.F node's function cell displays the correct name.");
+
+ is($$dur(0).textContent.trim(), "20 ms",
+ "The root node's function cell displays the correct duration.");
+ is($$dur(1).textContent.trim(), "20 ms",
+ "The .A node's function cell displays the correct duration.");
+ is($$dur(2).textContent.trim(), "15 ms",
+ "The .A.B node's function cell displays the correct duration.");
+ is($$dur(3).textContent.trim(), "10 ms",
+ "The .A.B.D node's function cell displays the correct duration.");
+ is($$dur(4).textContent.trim(), "5 ms",
+ "The .A.B.C node's function cell displays the correct duration.");
+ is($$dur(5).textContent.trim(), "5 ms",
+ "The .A.E node's function cell displays the correct duration.");
+ is($$dur(6).textContent.trim(), "5 ms",
+ "The .A.E.F node's function cell displays the correct duration.");
+});
diff --git a/devtools/client/performance/test/browser_perf-tree-view-04.js b/devtools/client/performance/test/browser_perf-tree-view-04.js
new file mode 100644
index 000000000..b2bc3dae5
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-tree-view-04.js
@@ -0,0 +1,78 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if the profiler's tree view implementation works properly and
+ * creates the correct DOM nodes in the correct order.
+ */
+
+const { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
+const { CallView } = require("devtools/client/performance/modules/widgets/tree-view");
+const { synthesizeProfile } = require("devtools/client/performance/test/helpers/synth-utils");
+
+add_task(function () {
+ let profile = synthesizeProfile();
+ let threadNode = new ThreadNode(profile.threads[0], { startTime: 0, endTime: 20 });
+
+ // Don't display the synthesized (root) and the real (root) node twice.
+ threadNode.calls = threadNode.calls[0].calls;
+
+ let treeRoot = new CallView({ frame: threadNode });
+ let container = document.createElement("vbox");
+ treeRoot.attachTo(container);
+
+ is(treeRoot.target.getAttribute("origin"), "chrome",
+ "The root node's 'origin' attribute is correct.");
+ is(treeRoot.target.getAttribute("category"), "",
+ "The root node's 'category' attribute is correct.");
+ is(treeRoot.target.getAttribute("tooltiptext"), "",
+ "The root node's 'tooltiptext' attribute is correct.");
+ is(treeRoot.target.querySelector(".call-tree-category"), null,
+ "The root node's category label cell should be hidden.");
+
+ let A = treeRoot.getChild();
+ let B = A.getChild();
+ let D = B.getChild();
+
+ is(D.target.getAttribute("origin"), "chrome",
+ "The .A.B.D node's 'origin' attribute is correct.");
+ is(D.target.getAttribute("category"), "gc",
+ "The .A.B.D node's 'category' attribute is correct.");
+ is(D.target.getAttribute("tooltiptext"), "D (http://foo/bar/baz:78:9)",
+ "The .A.B.D node's 'tooltiptext' attribute is correct.");
+
+ is(D.target.childNodes.length, 6,
+ "The number of columns displayed for tree items is correct.");
+ is(D.target.childNodes[0].getAttribute("type"), "duration",
+ "The first column displayed for tree items is correct.");
+ is(D.target.childNodes[1].getAttribute("type"), "percentage",
+ "The third column displayed for tree items is correct.");
+ is(D.target.childNodes[2].getAttribute("type"), "self-duration",
+ "The second column displayed for tree items is correct.");
+ is(D.target.childNodes[3].getAttribute("type"), "self-percentage",
+ "The fourth column displayed for tree items is correct.");
+ is(D.target.childNodes[4].getAttribute("type"), "samples",
+ "The fifth column displayed for tree items is correct.");
+ is(D.target.childNodes[5].getAttribute("type"), "function",
+ "The sixth column displayed for tree items is correct.");
+
+ let functionCell = D.target.childNodes[5];
+
+ is(functionCell.childNodes.length, 7,
+ "The number of columns displayed for function cells is correct.");
+ is(functionCell.childNodes[0].className, "arrow theme-twisty",
+ "The first node displayed for function cells is correct.");
+ is(functionCell.childNodes[1].className, "plain call-tree-name",
+ "The second node displayed for function cells is correct.");
+ is(functionCell.childNodes[2].className, "plain call-tree-url",
+ "The third node displayed for function cells is correct.");
+ is(functionCell.childNodes[3].className, "plain call-tree-line",
+ "The fourth node displayed for function cells is correct.");
+ is(functionCell.childNodes[4].className, "plain call-tree-column",
+ "The fifth node displayed for function cells is correct.");
+ is(functionCell.childNodes[5].className, "plain call-tree-host",
+ "The sixth node displayed for function cells is correct.");
+ is(functionCell.childNodes[6].className, "plain call-tree-category",
+ "The seventh node displayed for function cells is correct.");
+});
diff --git a/devtools/client/performance/test/browser_perf-tree-view-05.js b/devtools/client/performance/test/browser_perf-tree-view-05.js
new file mode 100644
index 000000000..045ed4ce2
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-tree-view-05.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if the profiler's tree view implementation works properly and
+ * can toggle categories hidden or visible.
+ */
+
+const { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
+const { CallView } = require("devtools/client/performance/modules/widgets/tree-view");
+const { synthesizeProfile } = require("devtools/client/performance/test/helpers/synth-utils");
+
+add_task(function () {
+ let profile = synthesizeProfile();
+ let threadNode = new ThreadNode(profile.threads[0], { startTime: 0, endTime: 20 });
+
+ // Don't display the synthesized (root) and the real (root) node twice.
+ threadNode.calls = threadNode.calls[0].calls;
+
+ let treeRoot = new CallView({ frame: threadNode });
+ let container = document.createElement("vbox");
+ treeRoot.attachTo(container);
+
+ let categories = container.querySelectorAll(".call-tree-category");
+ is(categories.length, 6,
+ "The call tree displays a correct number of categories.");
+ ok(!container.hasAttribute("categories-hidden"),
+ "All categories should be visible in the tree.");
+
+ treeRoot.toggleCategories(false);
+ is(categories.length, 6,
+ "The call tree displays the same number of categories.");
+ ok(container.hasAttribute("categories-hidden"),
+ "All categories should now be hidden in the tree.");
+});
diff --git a/devtools/client/performance/test/browser_perf-tree-view-06.js b/devtools/client/performance/test/browser_perf-tree-view-06.js
new file mode 100644
index 000000000..305195ddc
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-tree-view-06.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if the profiler's tree view implementation works properly and
+ * correctly emits events when certain DOM nodes are clicked.
+ */
+
+const { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
+const { CallView } = require("devtools/client/performance/modules/widgets/tree-view");
+const { synthesizeProfile } = require("devtools/client/performance/test/helpers/synth-utils");
+const { idleWait, waitUntil } = require("devtools/client/performance/test/helpers/wait-utils");
+
+add_task(function* () {
+ let profile = synthesizeProfile();
+ let threadNode = new ThreadNode(profile.threads[0], { startTime: 0, endTime: 20 });
+
+ // Don't display the synthesized (root) and the real (root) node twice.
+ threadNode.calls = threadNode.calls[0].calls;
+
+ let treeRoot = new CallView({ frame: threadNode });
+ let container = document.createElement("vbox");
+ treeRoot.attachTo(container);
+
+ let A = treeRoot.getChild();
+ let B = A.getChild();
+ let D = B.getChild();
+
+ let linkEvent = null;
+ let handler = (_, e) => {
+ linkEvent = e;
+ };
+
+ treeRoot.on("link", handler);
+
+ // Fire right click.
+ rightMousedown(D.target.querySelector(".call-tree-url"));
+
+ // Ensure link was not called for right click.
+ yield idleWait(100);
+ ok(!linkEvent, "The `link` event not fired for right click.");
+
+ // Fire left click.
+ mousedown(D.target.querySelector(".call-tree-url"));
+
+ // Ensure link was called for left click.
+ yield waitUntil(() => linkEvent);
+ is(linkEvent, D, "The `link` event target is correct.");
+
+ treeRoot.off("link", handler);
+});
diff --git a/devtools/client/performance/test/browser_perf-tree-view-07.js b/devtools/client/performance/test/browser_perf-tree-view-07.js
new file mode 100644
index 000000000..cc2cdd612
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-tree-view-07.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if the profiler's tree view implementation works properly and
+ * has the correct 'root', 'parent', 'level' etc. accessors on child nodes.
+ */
+
+const { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
+const { CallView } = require("devtools/client/performance/modules/widgets/tree-view");
+const { synthesizeProfile } = require("devtools/client/performance/test/helpers/synth-utils");
+
+add_task(function () {
+ let profile = synthesizeProfile();
+ let threadNode = new ThreadNode(profile.threads[0], { startTime: 0, endTime: 20 });
+
+ // Don't display the synthesized (root) and the real (root) node twice.
+ threadNode.calls = threadNode.calls[0].calls;
+
+ let treeRoot = new CallView({ frame: threadNode });
+ let container = document.createElement("vbox");
+ container.id = "call-tree-container";
+ treeRoot.attachTo(container);
+
+ let A = treeRoot.getChild();
+ let B = A.getChild();
+ let D = B.getChild();
+
+ is(D.root, treeRoot,
+ "The .A.B.D node has the correct root.");
+ is(D.parent, B,
+ "The .A.B.D node has the correct parent.");
+ is(D.level, 3,
+ "The .A.B.D node has the correct level.");
+ is(D.target.className, "call-tree-item",
+ "The .A.B.D node has the correct target node.");
+ is(D.container.id, "call-tree-container",
+ "The .A.B.D node has the correct container node.");
+});
diff --git a/devtools/client/performance/test/browser_perf-tree-view-08.js b/devtools/client/performance/test/browser_perf-tree-view-08.js
new file mode 100644
index 000000000..7b65ea45c
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-tree-view-08.js
@@ -0,0 +1,109 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the profiler's tree view renders generalized platform data
+ * when `contentOnly` is on correctly.
+ */
+
+const { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
+const { CallView } = require("devtools/client/performance/modules/widgets/tree-view");
+const { CATEGORY_MASK } = require("devtools/client/performance/modules/categories");
+const RecordingUtils = require("devtools/shared/performance/recording-utils");
+
+add_task(function () {
+ let threadNode = new ThreadNode(gProfile.threads[0], { startTime: 0, endTime: 20,
+ contentOnly: true });
+
+ // Don't display the synthesized (root) and the real (root) node twice.
+ threadNode.calls = threadNode.calls[0].calls;
+
+ let treeRoot = new CallView({ frame: threadNode, autoExpandDepth: 10 });
+ let container = document.createElement("vbox");
+ treeRoot.attachTo(container);
+
+ /*
+ * (root)
+ * - A
+ * - B
+ * - C
+ * - D
+ * - (GC)
+ * - E
+ * - F
+ * - (JS)
+ * - (JS)
+ */
+
+ let A = treeRoot.getChild(0);
+ let JS = treeRoot.getChild(1);
+ let GC = A.getChild(1);
+ let JS2 = A.getChild(2).getChild().getChild();
+
+ is(JS.target.getAttribute("category"), "js",
+ "Generalized JS node has correct category");
+ is(JS.target.getAttribute("tooltiptext"), "JIT",
+ "Generalized JS node has correct category");
+ is(JS.target.querySelector(".call-tree-name").textContent.trim(), "JIT",
+ "Generalized JS node has correct display value as just the category name.");
+
+ is(JS2.target.getAttribute("category"), "js",
+ "Generalized second JS node has correct category");
+ is(JS2.target.getAttribute("tooltiptext"), "JIT",
+ "Generalized second JS node has correct category");
+ is(JS2.target.querySelector(".call-tree-name").textContent.trim(), "JIT",
+ "Generalized second JS node has correct display value as just the category name.");
+
+ is(GC.target.getAttribute("category"), "gc",
+ "Generalized GC node has correct category");
+ is(GC.target.getAttribute("tooltiptext"), "GC",
+ "Generalized GC node has correct category");
+ is(GC.target.querySelector(".call-tree-name").textContent.trim(), "GC",
+ "Generalized GC node has correct display value as just the category name.");
+});
+
+const gProfile = RecordingUtils.deflateProfile({
+ meta: { version: 2 },
+ threads: [{
+ samples: [{
+ time: 1,
+ frames: [
+ { location: "(root)" },
+ { location: "http://content/A" },
+ { location: "http://content/B" },
+ { location: "http://content/C" }
+ ]
+ }, {
+ time: 1 + 1,
+ frames: [
+ { location: "(root)" },
+ { location: "http://content/A" },
+ { location: "http://content/B" },
+ { location: "http://content/D" }
+ ]
+ }, {
+ time: 1 + 1 + 2,
+ frames: [
+ { location: "(root)" },
+ { location: "http://content/A" },
+ { location: "http://content/E" },
+ { location: "http://content/F" },
+ { location: "platform_JS", category: CATEGORY_MASK("js") },
+ ]
+ }, {
+ time: 1 + 1 + 2 + 3,
+ frames: [
+ { location: "(root)" },
+ { location: "platform_JS2", category: CATEGORY_MASK("js") },
+ ]
+ }, {
+ time: 1 + 1 + 2 + 3 + 5,
+ frames: [
+ { location: "(root)" },
+ { location: "http://content/A" },
+ { location: "platform_GC", category: CATEGORY_MASK("gc", 1) },
+ ]
+ }]
+ }]
+});
diff --git a/devtools/client/performance/test/browser_perf-tree-view-09.js b/devtools/client/performance/test/browser_perf-tree-view-09.js
new file mode 100644
index 000000000..c7f11549e
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-tree-view-09.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the profiler's tree view sorts inverted call trees by
+ * "self cost" and not "total cost".
+ */
+
+const { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
+const { CallView } = require("devtools/client/performance/modules/widgets/tree-view");
+const RecordingUtils = require("devtools/shared/performance/recording-utils");
+
+add_task(function () {
+ let threadNode = new ThreadNode(gProfile.threads[0], { startTime: 0, endTime: 20,
+ invertTree: true });
+ let treeRoot = new CallView({ frame: threadNode, inverted: true });
+ let container = document.createElement("vbox");
+ treeRoot.attachTo(container);
+
+ is(treeRoot.getChild(0).frame.location, "B",
+ "The tree root's first child is the `B` function.");
+ is(treeRoot.getChild(1).frame.location, "A",
+ "The tree root's second child is the `A` function.");
+});
+
+const gProfile = RecordingUtils.deflateProfile({
+ meta: { version: 2 },
+ threads: [{
+ samples: [{
+ time: 1,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ ]
+ }, {
+ time: 2,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" }
+ ]
+ }, {
+ time: 3,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ ]
+ }, {
+ time: 4,
+ frames: [
+ { location: "(root)" },
+ { location: "A" }
+ ]
+ }]
+ }]
+});
diff --git a/devtools/client/performance/test/browser_perf-tree-view-10.js b/devtools/client/performance/test/browser_perf-tree-view-10.js
new file mode 100644
index 000000000..342b47b92
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-tree-view-10.js
@@ -0,0 +1,160 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if the profiler's tree view, when inverted, displays the self and
+ * total costs correctly.
+ */
+
+const { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
+const { CallView } = require("devtools/client/performance/modules/widgets/tree-view");
+const RecordingUtils = require("devtools/shared/performance/recording-utils");
+
+add_task(function () {
+ let threadNode = new ThreadNode(gProfile.threads[0], { startTime: 0, endTime: 50,
+ invertTree: true });
+ let treeRoot = new CallView({ frame: threadNode, inverted: true });
+ let container = document.createElement("vbox");
+ treeRoot.attachTo(container);
+
+ // Add 1 to each index to skip the hidden root node
+ let $$nam = i => container.querySelectorAll(
+ ".call-tree-cell[type=function] > .call-tree-name")[i + 1];
+ let $$per = i => container.querySelectorAll(
+ ".call-tree-cell[type=percentage]")[i + 1];
+ let $$selfper = i => container.querySelectorAll(
+ ".call-tree-cell[type='self-percentage']")[i + 1];
+
+ /**
+ * Samples:
+ *
+ * A->C
+ * A->B
+ * A->B->C x4
+ * A->B->D x4
+ *
+ * Expected:
+ *
+ * +--total--+--self--+--tree----+
+ * | 50% | 50% | C |
+ * | 40% | 0 | -> B |
+ * | 30% | 0 | -> A |
+ * | 10% | 0 | -> A |
+ * | 40% | 40% | D |
+ * | 40% | 0 | -> B |
+ * | 40% | 0 | -> A |
+ * | 10% | 10% | B |
+ * | 10% | 0 | -> A |
+ * +---------+--------+----------+
+ */
+
+ is(container.childNodes.length, 10,
+ "The container node should have all children available.");
+
+ // total, self, indent + name
+ [
+ [ 50, 50, "C"],
+ [ 40, 0, " B"],
+ [ 30, 0, " A"],
+ [ 10, 0, " A"],
+ [ 40, 40, "D"],
+ [ 40, 0, " B"],
+ [ 40, 0, " A"],
+ [ 10, 10, "B"],
+ [ 10, 0, " A"],
+ ].forEach(function (def, i) {
+ info(`Checking ${i}th tree item.`);
+
+ let [total, self, name] = def;
+ name = name.trim();
+
+ is($$nam(i).textContent.trim(), name, `${name} has correct name.`);
+ is($$per(i).textContent.trim(), `${total}%`, `${name} has correct total percent.`);
+ is($$selfper(i).textContent.trim(), `${self}%`, `${name} has correct self percent.`);
+ });
+});
+
+const gProfile = RecordingUtils.deflateProfile({
+ meta: { version: 2 },
+ threads: [{
+ samples: [{
+ time: 5,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "C" }
+ ]
+ }, {
+ time: 10,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "D" }
+ ]
+ }, {
+ time: 15,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "C" },
+ ]
+ }, {
+ time: 20,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ ]
+ }, {
+ time: 25,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "C" }
+ ]
+ }, {
+ time: 30,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "C" }
+ ]
+ }, {
+ time: 35,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "D" }
+ ]
+ }, {
+ time: 40,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "D" }
+ ]
+ }, {
+ time: 45,
+ frames: [
+ { location: "(root)" },
+ { location: "B" },
+ { location: "C" }
+ ]
+ }, {
+ time: 50,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "D" }
+ ]
+ }]
+ }]
+});
diff --git a/devtools/client/performance/test/browser_perf-tree-view-11.js b/devtools/client/performance/test/browser_perf-tree-view-11.js
new file mode 100644
index 000000000..a316098e3
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-tree-view-11.js
@@ -0,0 +1,154 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+/* eslint-disable */
+/**
+ * Tests that if `show-jit-optimizations` is true, then an
+ * icon is next to the frame with optimizations
+ */
+
+var { CATEGORY_MASK } = require("devtools/client/performance/modules/categories");
+
+function* spawnTest() {
+ let { panel } = yield initPerformance(SIMPLE_URL);
+ let { EVENTS, $, $$, window, PerformanceController } = panel.panelWin;
+ let { OverviewView, DetailsView, JsCallTreeView } = panel.panelWin;
+
+ let profilerData = { threads: [gThread] };
+
+ Services.prefs.setBoolPref(JIT_PREF, true);
+ Services.prefs.setBoolPref(PLATFORM_DATA_PREF, false);
+ Services.prefs.setBoolPref(INVERT_PREF, false);
+
+ // Make two recordings, so we have one to switch to later, as the
+ // second one will have fake sample data
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ yield DetailsView.selectView("js-calltree");
+
+ yield injectAndRenderProfilerData();
+
+ let rows = $$("#js-calltree-view .call-tree-item");
+ is(rows.length, 4, "4 call tree rows exist");
+ for (let row of rows) {
+ let name = $(".call-tree-name", row).textContent.trim();
+ switch (name) {
+ case "A":
+ ok($(".opt-icon", row), "found an opt icon on a leaf node with opt data");
+ break;
+ case "C":
+ ok(!$(".opt-icon", row), "frames without opt data do not have an icon");
+ break;
+ case "Gecko":
+ ok(!$(".opt-icon", row), "meta category frames with opt data do not have an icon");
+ break;
+ case "(root)":
+ ok(!$(".opt-icon", row), "root frame certainly does not have opt data");
+ break;
+ default:
+ ok(false, `Unidentified frame: ${name}`);
+ break;
+ }
+ }
+
+ yield teardown(panel);
+ finish();
+
+ function* injectAndRenderProfilerData() {
+ // Get current recording and inject our mock data
+ info("Injecting mock profile data");
+ let recording = PerformanceController.getCurrentRecording();
+ recording._profile = profilerData;
+
+ // Force a rerender
+ let rendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
+ JsCallTreeView.render(OverviewView.getTimeInterval());
+ yield rendered;
+ }
+}
+
+var gUniqueStacks = new RecordingUtils.UniqueStacks();
+
+function uniqStr(s) {
+ return gUniqueStacks.getOrAddStringIndex(s);
+}
+
+// Since deflateThread doesn't handle deflating optimization info, use
+// placeholder names A_O1, B_O2, and B_O3, which will be used to manually
+// splice deduped opts into the profile.
+var gThread = RecordingUtils.deflateThread({
+ samples: [{
+ time: 0,
+ frames: [
+ { location: "(root)" }
+ ]
+ }, {
+ time: 5,
+ frames: [
+ { location: "(root)" },
+ { location: "A (http://foo:1)" },
+ ]
+ }, {
+ time: 5 + 1,
+ frames: [
+ { location: "(root)" },
+ { location: "C (http://foo/bar/baz:56)" }
+ ]
+ }, {
+ time: 5 + 1 + 2,
+ frames: [
+ { location: "(root)" },
+ { category: CATEGORY_MASK("other"), location: "PlatformCode" }
+ ]
+ }],
+ markers: []
+}, gUniqueStacks);
+
+// 3 RawOptimizationSites
+var gRawSite1 = {
+ _testFrameInfo: { name: "A", line: "12", file: "@baz" },
+ line: 12,
+ column: 2,
+ types: [{
+ mirType: uniqStr("Object"),
+ site: uniqStr("A (http://foo/bar/bar:12)"),
+ typeset: [{
+ keyedBy: uniqStr("constructor"),
+ name: uniqStr("Foo"),
+ location: uniqStr("A (http://foo/bar/baz:12)")
+ }, {
+ keyedBy: uniqStr("primitive"),
+ location: uniqStr("self-hosted")
+ }]
+ }],
+ attempts: {
+ schema: {
+ outcome: 0,
+ strategy: 1
+ },
+ data: [
+ [uniqStr("Failure1"), uniqStr("SomeGetter1")],
+ [uniqStr("Failure2"), uniqStr("SomeGetter2")],
+ [uniqStr("Failure3"), uniqStr("SomeGetter3")]
+ ]
+ }
+};
+
+gThread.frameTable.data.forEach((frame) => {
+ const LOCATION_SLOT = gThread.frameTable.schema.location;
+ const OPTIMIZATIONS_SLOT = gThread.frameTable.schema.optimizations;
+
+ let l = gThread.stringTable[frame[LOCATION_SLOT]];
+ switch (l) {
+ case "A (http://foo:1)":
+ frame[LOCATION_SLOT] = uniqStr("A (http://foo:1)");
+ frame[OPTIMIZATIONS_SLOT] = gRawSite1;
+ break;
+ case "PlatformCode":
+ frame[LOCATION_SLOT] = uniqStr("PlatformCode");
+ frame[OPTIMIZATIONS_SLOT] = gRawSite1;
+ break;
+ }
+});
+/* eslint-enable */
diff --git a/devtools/client/performance/test/browser_perf-ui-recording.js b/devtools/client/performance/test/browser_perf-ui-recording.js
new file mode 100644
index 000000000..b585f763b
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-ui-recording.js
@@ -0,0 +1,39 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the controller handles recording via the `stopwatch` button
+ * in the UI.
+ */
+
+const { pmmLoadFrameScripts, pmmIsProfilerActive, pmmClearFrameScripts } = require("devtools/client/performance/test/helpers/profiler-mm-utils");
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+
+add_task(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ pmmLoadFrameScripts(gBrowser);
+
+ ok(!(yield pmmIsProfilerActive()),
+ "The built-in profiler module should not have been automatically started.");
+
+ yield startRecording(panel);
+
+ ok((yield pmmIsProfilerActive()),
+ "The built-in profiler module should now be active.");
+
+ yield stopRecording(panel);
+
+ ok((yield pmmIsProfilerActive()),
+ "The built-in profiler module should still be active.");
+
+ yield teardownToolboxAndRemoveTab(panel);
+
+ pmmClearFrameScripts();
+});
diff --git a/devtools/client/performance/test/browser_timeline-filters-01.js b/devtools/client/performance/test/browser_timeline-filters-01.js
new file mode 100644
index 000000000..4a8d48585
--- /dev/null
+++ b/devtools/client/performance/test/browser_timeline-filters-01.js
@@ -0,0 +1,119 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable */
+
+/**
+ * Tests markers filtering mechanism.
+ */
+
+const EPSILON = 0.00000001;
+
+function* spawnTest() {
+ let { panel } = yield initPerformance(SIMPLE_URL);
+ let { $, $$, EVENTS, PerformanceController, OverviewView, WaterfallView } = panel.panelWin;
+ let { TimelineGraph } = require("devtools/client/performance/modules/widgets/graphs");
+ let { rowHeight: MARKERS_GRAPH_ROW_HEIGHT } = TimelineGraph.prototype;
+
+ yield startRecording(panel);
+ ok(true, "Recording has started.");
+
+ yield waitUntil(() => {
+ // Wait until we get 3 different markers.
+ let markers = PerformanceController.getCurrentRecording().getMarkers();
+ return markers.some(m => m.name == "Styles") &&
+ markers.some(m => m.name == "Reflow") &&
+ markers.some(m => m.name == "Paint");
+ });
+
+ yield stopRecording(panel);
+ ok(true, "Recording has ended.");
+
+ // Push some fake markers of a type we do not have a blueprint for
+ let markers = PerformanceController.getCurrentRecording().getMarkers();
+ let endTime = markers[markers.length - 1].end;
+ markers.push({ name: "CustomMarker", start: endTime + EPSILON, end: endTime + (EPSILON * 2) });
+ markers.push({ name: "CustomMarker", start: endTime + (EPSILON * 3), end: endTime + (EPSILON * 4) });
+
+ // Invalidate marker cache
+ WaterfallView._cache.delete(markers);
+
+ // Select everything
+ let waterfallRendered = WaterfallView.once(EVENTS.UI_WATERFALL_RENDERED);
+ OverviewView.setTimeInterval({ startTime: 0, endTime: Number.MAX_VALUE });
+
+ $("#filter-button").click();
+ let menuItem1 = $("menuitem[marker-type=Styles]");
+ let menuItem2 = $("menuitem[marker-type=Reflow]");
+ let menuItem3 = $("menuitem[marker-type=Paint]");
+ let menuItem4 = $("menuitem[marker-type=UNKNOWN]");
+
+ let overview = OverviewView.graphs.get("timeline");
+ let originalHeight = overview.fixedHeight;
+
+ yield waterfallRendered;
+
+ ok($(".waterfall-marker-bar[type=Styles]"), "Found at least one 'Styles' marker (1)");
+ ok($(".waterfall-marker-bar[type=Reflow]"), "Found at least one 'Reflow' marker (1)");
+ ok($(".waterfall-marker-bar[type=Paint]"), "Found at least one 'Paint' marker (1)");
+ ok($(".waterfall-marker-bar[type=CustomMarker]"), "Found at least one 'Unknown' marker (1)");
+
+ let heightBefore = overview.fixedHeight;
+ EventUtils.synthesizeMouseAtCenter(menuItem1, {type: "mouseup"}, panel.panelWin);
+ yield waitForOverviewAndCommand(overview, menuItem1);
+
+ is(overview.fixedHeight, heightBefore, "Overview height hasn't changed");
+ ok(!$(".waterfall-marker-bar[type=Styles]"), "No 'Styles' marker (2)");
+ ok($(".waterfall-marker-bar[type=Reflow]"), "Found at least one 'Reflow' marker (2)");
+ ok($(".waterfall-marker-bar[type=Paint]"), "Found at least one 'Paint' marker (2)");
+ ok($(".waterfall-marker-bar[type=CustomMarker]"), "Found at least one 'Unknown' marker (2)");
+
+ heightBefore = overview.fixedHeight;
+ EventUtils.synthesizeMouseAtCenter(menuItem2, {type: "mouseup"}, panel.panelWin);
+ yield waitForOverviewAndCommand(overview, menuItem2);
+
+ is(overview.fixedHeight, heightBefore, "Overview height hasn't changed");
+ ok(!$(".waterfall-marker-bar[type=Styles]"), "No 'Styles' marker (3)");
+ ok(!$(".waterfall-marker-bar[type=Reflow]"), "No 'Reflow' marker (3)");
+ ok($(".waterfall-marker-bar[type=Paint]"), "Found at least one 'Paint' marker (3)");
+ ok($(".waterfall-marker-bar[type=CustomMarker]"), "Found at least one 'Unknown' marker (3)");
+
+ heightBefore = overview.fixedHeight;
+ EventUtils.synthesizeMouseAtCenter(menuItem3, {type: "mouseup"}, panel.panelWin);
+ yield waitForOverviewAndCommand(overview, menuItem3);
+
+ is(overview.fixedHeight, heightBefore - MARKERS_GRAPH_ROW_HEIGHT, "Overview is smaller");
+ ok(!$(".waterfall-marker-bar[type=Styles]"), "No 'Styles' marker (4)");
+ ok(!$(".waterfall-marker-bar[type=Reflow]"), "No 'Reflow' marker (4)");
+ ok(!$(".waterfall-marker-bar[type=Paint]"), "No 'Paint' marker (4)");
+ ok($(".waterfall-marker-bar[type=CustomMarker]"), "Found at least one 'Unknown' marker (4)");
+
+ EventUtils.synthesizeMouseAtCenter(menuItem4, {type: "mouseup"}, panel.panelWin);
+ yield waitForOverviewAndCommand(overview, menuItem4);
+
+ ok(!$(".waterfall-marker-bar[type=Styles]"), "No 'Styles' marker (5)");
+ ok(!$(".waterfall-marker-bar[type=Reflow]"), "No 'Reflow' marker (5)");
+ ok(!$(".waterfall-marker-bar[type=Paint]"), "No 'Paint' marker (5)");
+ ok(!$(".waterfall-marker-bar[type=CustomMarker]"), "No 'Unknown' marker (5)");
+
+ for (let item of [menuItem1, menuItem2, menuItem3]) {
+ EventUtils.synthesizeMouseAtCenter(item, {type: "mouseup"}, panel.panelWin);
+ yield waitForOverviewAndCommand(overview, item);
+ }
+
+ ok($(".waterfall-marker-bar[type=Styles]"), "Found at least one 'Styles' marker (6)");
+ ok($(".waterfall-marker-bar[type=Reflow]"), "Found at least one 'Reflow' marker (6)");
+ ok($(".waterfall-marker-bar[type=Paint]"), "Found at least one 'Paint' marker (6)");
+ ok(!$(".waterfall-marker-bar[type=CustomMarker]"), "No 'Unknown' marker (6)");
+
+ is(overview.fixedHeight, originalHeight, "Overview restored");
+
+ yield teardown(panel);
+ finish();
+}
+
+function waitForOverviewAndCommand(overview, item) {
+ let overviewRendered = overview.once("refresh");
+ let menuitemCommandDispatched = once(item, "command");
+ return Promise.all([overviewRendered, menuitemCommandDispatched]);
+}
+/* eslint-enable */
diff --git a/devtools/client/performance/test/browser_timeline-filters-02.js b/devtools/client/performance/test/browser_timeline-filters-02.js
new file mode 100644
index 000000000..f9ab00711
--- /dev/null
+++ b/devtools/client/performance/test/browser_timeline-filters-02.js
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+/* eslint-disable */
+/**
+ * Tests markers filtering mechanism.
+ */
+
+const URL = EXAMPLE_URL + "doc_innerHTML.html";
+
+function* spawnTest() {
+ let { panel } = yield initPerformance(URL);
+ let { $, $$, EVENTS, PerformanceController, OverviewView, WaterfallView } = panel.panelWin;
+
+ yield startRecording(panel);
+ ok(true, "Recording has started.");
+
+ yield waitUntil(() => {
+ let markers = PerformanceController.getCurrentRecording().getMarkers();
+ return markers.some(m => m.name == "Parse HTML") &&
+ markers.some(m => m.name == "Javascript");
+ });
+
+ let waterfallRendered = WaterfallView.once(EVENTS.UI_WATERFALL_RENDERED);
+ yield stopRecording(panel);
+
+ $("#filter-button").click();
+ let filterJS = $("menuitem[marker-type=Javascript]");
+
+ yield waterfallRendered;
+
+ ok($(".waterfall-marker-bar[type=Javascript]"), "Found at least one 'Javascript' marker");
+ ok(!$(".waterfall-marker-bar[type='Parse HTML']"), "Found no Parse HTML markers as they are nested still");
+
+ EventUtils.synthesizeMouseAtCenter(filterJS, {type: "mouseup"}, panel.panelWin);
+ yield Promise.all([
+ WaterfallView.once(EVENTS.UI_WATERFALL_RENDERED),
+ once(filterJS, "command")
+ ]);
+
+ ok(!$(".waterfall-marker-bar[type=Javascript]"), "Javascript markers are all hidden.");
+ ok($(".waterfall-marker-bar[type='Parse HTML']"),
+ "Found at least one 'Parse HTML' marker still visible after hiding JS markers");
+
+ yield teardown(panel);
+ finish();
+}
+/* eslint-enable */
diff --git a/devtools/client/performance/test/browser_timeline-waterfall-background.js b/devtools/client/performance/test/browser_timeline-waterfall-background.js
new file mode 100644
index 000000000..85d5bd28c
--- /dev/null
+++ b/devtools/client/performance/test/browser_timeline-waterfall-background.js
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if the waterfall background is a 1px high canvas stretching across
+ * the container bounds.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording, waitForOverviewRenderedWithMarkers } = require("devtools/client/performance/test/helpers/actions");
+
+add_task(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { WaterfallView } = panel.panelWin;
+
+ yield startRecording(panel);
+ ok(true, "Recording has started.");
+
+ // Ensure overview is rendering and some markers were received.
+ yield waitForOverviewRenderedWithMarkers(panel);
+
+ yield stopRecording(panel);
+ ok(true, "Recording has ended.");
+
+ // Test the waterfall background.
+
+ ok(WaterfallView.canvas, "A canvas should be created after the recording ended.");
+
+ is(WaterfallView.canvas.width, WaterfallView.waterfallWidth,
+ "The canvas width is correct.");
+ is(WaterfallView.canvas.height, 1,
+ "The canvas height is correct.");
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_timeline-waterfall-generic.js b/devtools/client/performance/test/browser_timeline-waterfall-generic.js
new file mode 100644
index 000000000..bcb87d80c
--- /dev/null
+++ b/devtools/client/performance/test/browser_timeline-waterfall-generic.js
@@ -0,0 +1,105 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if the waterfall is properly built after finishing a recording.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
+const { startRecording, stopRecording, waitForOverviewRenderedWithMarkers } = require("devtools/client/performance/test/helpers/actions");
+const { once } = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { $, $$, EVENTS, WaterfallView } = panel.panelWin;
+
+ yield startRecording(panel);
+ ok(true, "Recording has started.");
+
+ // Ensure overview is rendering and some markers were received.
+ yield waitForOverviewRenderedWithMarkers(panel);
+
+ yield stopRecording(panel);
+ ok(true, "Recording has ended.");
+
+ // Test the header container.
+
+ ok($(".waterfall-header"),
+ "A header container should have been created.");
+
+ // Test the header sidebar (left).
+
+ ok($(".waterfall-header > .waterfall-sidebar"),
+ "A header sidebar node should have been created.");
+
+ // Test the header ticks (right).
+
+ ok($(".waterfall-header-ticks"),
+ "A header ticks node should have been created.");
+ ok($$(".waterfall-header-ticks > .waterfall-header-tick").length > 0,
+ "Some header tick labels should have been created inside the tick node.");
+
+ // Test the markers sidebar (left).
+
+ ok($$(".waterfall-tree-item > .waterfall-sidebar").length,
+ "Some marker sidebar nodes should have been created.");
+ ok($$(".waterfall-tree-item > .waterfall-sidebar > .waterfall-marker-bullet").length,
+ "Some marker color bullets should have been created inside the sidebar.");
+ ok($$(".waterfall-tree-item > .waterfall-sidebar > .waterfall-marker-name").length,
+ "Some marker name labels should have been created inside the sidebar.");
+
+ // Test the markers waterfall (right).
+
+ ok($$(".waterfall-tree-item > .waterfall-marker").length,
+ "Some marker waterfall nodes should have been created.");
+ ok($$(".waterfall-tree-item > .waterfall-marker .waterfall-marker-bar").length,
+ "Some marker color bars should have been created inside the waterfall.");
+
+ // Test the sidebar.
+
+ let detailsView = WaterfallView.details;
+ // Make sure the bounds are up to date.
+ WaterfallView._recalculateBounds();
+
+ let parentWidthBefore = $("#waterfall-view").getBoundingClientRect().width;
+ let sidebarWidthBefore = $(".waterfall-sidebar").getBoundingClientRect().width;
+ let detailsWidthBefore = $("#waterfall-details").getBoundingClientRect().width;
+
+ ok(detailsView.hidden,
+ "The details view in the waterfall view is hidden by default.");
+ is(detailsWidthBefore, 0,
+ "The details view width should be 0 when hidden.");
+ is(WaterfallView.waterfallWidth,
+ parentWidthBefore - sidebarWidthBefore
+ - WaterfallView.WATERFALL_MARKER_SIDEBAR_SAFE_BOUNDS,
+ "The waterfall width is correct (1).");
+
+ let waterfallRerendered = once(WaterfallView, EVENTS.UI_WATERFALL_RENDERED);
+ $$(".waterfall-tree-item")[0].click();
+ yield waterfallRerendered;
+
+ let parentWidthAfter = $("#waterfall-view").getBoundingClientRect().width;
+ let sidebarWidthAfter = $(".waterfall-sidebar").getBoundingClientRect().width;
+ let detailsWidthAfter = $("#waterfall-details").getBoundingClientRect().width;
+
+ ok(!detailsView.hidden,
+ "The details view in the waterfall view is now visible.");
+ is(parentWidthBefore, parentWidthAfter,
+ "The parent view's width should not have changed.");
+ is(sidebarWidthBefore, sidebarWidthAfter,
+ "The sidebar view's width should not have changed.");
+ isnot(detailsWidthAfter, 0,
+ "The details view width should not be 0 when visible.");
+ is(WaterfallView.waterfallWidth,
+ parentWidthAfter - sidebarWidthAfter - detailsWidthAfter
+ - WaterfallView.WATERFALL_MARKER_SIDEBAR_SAFE_BOUNDS,
+ "The waterfall width is correct (2).");
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_timeline-waterfall-rerender.js b/devtools/client/performance/test/browser_timeline-waterfall-rerender.js
new file mode 100644
index 000000000..8bf842560
--- /dev/null
+++ b/devtools/client/performance/test/browser_timeline-waterfall-rerender.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable */
+/**
+ * Tests if the waterfall remembers the selection when rerendering.
+ */
+
+function* spawnTest() {
+ let { target, panel } = yield initPerformance(SIMPLE_URL);
+ let { $, $$, EVENTS, PerformanceController, OverviewView, WaterfallView } = panel.panelWin;
+
+ const MIN_MARKERS_COUNT = 50;
+ const MAX_MARKERS_SELECT = 20;
+
+ yield startRecording(panel);
+ ok(true, "Recording has started.");
+
+ let updated = 0;
+ OverviewView.on(EVENTS.UI_OVERVIEW_RENDERED, () => updated++);
+
+ ok((yield waitUntil(() => updated > 0)),
+ "The overview graphs were updated a bunch of times.");
+ ok((yield waitUntil(() => PerformanceController.getCurrentRecording().getMarkers().length > MIN_MARKERS_COUNT)),
+ "There are some markers available.");
+
+ yield stopRecording(panel);
+ ok(true, "Recording has ended.");
+
+ let currentMarkers = PerformanceController.getCurrentRecording().getMarkers();
+ info("Gathered markers: " + JSON.stringify(currentMarkers, null, 2));
+
+ let initialBarsCount = $$(".waterfall-marker-bar").length;
+ info("Initial bars count: " + initialBarsCount);
+
+ // Select a portion of the overview.
+ let rerendered = WaterfallView.once(EVENTS.UI_WATERFALL_RENDERED);
+ OverviewView.setTimeInterval({ startTime: 0, endTime: currentMarkers[MAX_MARKERS_SELECT].end });
+ yield rerendered;
+
+ ok(!$(".waterfall-tree-item:focus"),
+ "There is no item focused in the waterfall yet.");
+ ok($("#waterfall-details").hidden,
+ "The waterfall sidebar is initially hidden.");
+
+ // Focus the second item in the tree.
+ WaterfallView._markersRoot.getChild(1).focus();
+
+ let beforeResizeBarsCount = $$(".waterfall-marker-bar").length;
+ info("Before resize bars count: " + beforeResizeBarsCount);
+ ok(beforeResizeBarsCount < initialBarsCount,
+ "A subset of the total markers was selected.");
+
+ is(Array.indexOf($$(".waterfall-tree-item"), $(".waterfall-tree-item:focus")), 2,
+ "The correct item was focused in the tree.");
+ ok(!$("#waterfall-details").hidden,
+ "The waterfall sidebar is now visible.");
+
+ // Simulate a resize on the marker details.
+ rerendered = WaterfallView.once(EVENTS.UI_WATERFALL_RENDERED);
+ EventUtils.sendMouseEvent({ type: "mouseup" }, WaterfallView.detailsSplitter);
+ yield rerendered;
+
+ let afterResizeBarsCount = $$(".waterfall-marker-bar").length;
+ info("After resize bars count: " + afterResizeBarsCount);
+ is(afterResizeBarsCount, beforeResizeBarsCount,
+ "The same subset of the total markers remained visible.");
+
+ is(Array.indexOf($$(".waterfall-tree-item"), $(".waterfall-tree-item:focus")), 2,
+ "The correct item is still focused in the tree.");
+ ok(!$("#waterfall-details").hidden,
+ "The waterfall sidebar is still visible.");
+
+ yield teardown(panel);
+ finish();
+}
+/* eslint-enable */
diff --git a/devtools/client/performance/test/browser_timeline-waterfall-sidebar.js b/devtools/client/performance/test/browser_timeline-waterfall-sidebar.js
new file mode 100644
index 000000000..1c2c1ccae
--- /dev/null
+++ b/devtools/client/performance/test/browser_timeline-waterfall-sidebar.js
@@ -0,0 +1,77 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable */
+/**
+ * Tests if the sidebar is properly updated when a marker is selected.
+ */
+
+function* spawnTest() {
+ let { target, panel } = yield initPerformance(SIMPLE_URL);
+ let { $, $$, PerformanceController, WaterfallView } = panel.panelWin;
+ let { L10N } = require("devtools/client/performance/modules/global");
+ let { MarkerBlueprintUtils } = require("devtools/client/performance/modules/marker-blueprint-utils");
+
+ // Hijack the markers massaging part of creating the waterfall view,
+ // to prevent collapsing markers and allowing this test to verify
+ // everything individually. A better solution would be to just expand
+ // all markers first and then skip the meta nodes, but I'm lazy.
+ WaterfallView._prepareWaterfallTree = markers => {
+ return { submarkers: markers };
+ };
+
+ yield startRecording(panel);
+ ok(true, "Recording has started.");
+
+ yield waitUntil(() => {
+ // Wait until we get 3 different markers.
+ let markers = PerformanceController.getCurrentRecording().getMarkers();
+ return markers.some(m => m.name == "Styles") &&
+ markers.some(m => m.name == "Reflow") &&
+ markers.some(m => m.name == "Paint");
+ });
+
+ yield stopRecording(panel);
+ ok(true, "Recording has ended.");
+
+ info("No need to select everything in the timeline.");
+ info("All the markers should be displayed by default.");
+
+ let bars = $$(".waterfall-marker-bar");
+ let markers = PerformanceController.getCurrentRecording().getMarkers();
+
+ info(`Got ${bars.length} bars and ${markers.length} markers.`);
+ info("Markers types from datasrc: " + Array.map(markers, e => e.name));
+ info("Markers names from sidebar: " + Array.map(bars, e => e.parentNode.parentNode.querySelector(".waterfall-marker-name").getAttribute("value")));
+
+ ok(bars.length > 2, "Got at least 3 markers (1)");
+ ok(markers.length > 2, "Got at least 3 markers (2)");
+
+ let toMs = ms => L10N.getFormatStrWithNumbers("timeline.tick", ms);
+
+ for (let i = 0; i < bars.length; i++) {
+ let bar = bars[i];
+ let mkr = markers[i];
+ EventUtils.sendMouseEvent({ type: "mousedown" }, bar);
+
+ let type = $(".marker-details-type").getAttribute("value");
+ let tooltip = $(".marker-details-duration").getAttribute("tooltiptext");
+ let duration = $(".marker-details-duration .marker-details-labelvalue").getAttribute("value");
+
+ info("Current marker data: " + mkr.toSource());
+ info("Current marker output: " + $("#waterfall-details").innerHTML);
+
+ is(type, MarkerBlueprintUtils.getMarkerLabel(mkr), "Sidebar title matches markers name.");
+
+ // Values are rounded. We don't use a strict equality.
+ is(toMs(mkr.end - mkr.start), duration, "Sidebar duration is valid.");
+
+ // For some reason, anything that creates "→" here turns it into a "â" for some reason.
+ // So just check that start and end time are in there somewhere.
+ ok(tooltip.indexOf(toMs(mkr.start)) !== -1, "Tooltip has start time.");
+ ok(tooltip.indexOf(toMs(mkr.end)) !== -1, "Tooltip has end time.");
+ }
+
+ yield teardown(panel);
+ finish();
+}
+/* eslint-enable */
diff --git a/devtools/client/performance/test/browser_timeline-waterfall-workers.js b/devtools/client/performance/test/browser_timeline-waterfall-workers.js
new file mode 100644
index 000000000..5430b8fdc
--- /dev/null
+++ b/devtools/client/performance/test/browser_timeline-waterfall-workers.js
@@ -0,0 +1,97 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+/* eslint-disable */
+/**
+ * Tests if the sidebar is properly updated with worker markers.
+ */
+
+function* spawnTest() {
+ let { panel } = yield initPerformance(WORKER_URL);
+ let { $$, $, PerformanceController } = panel.panelWin;
+
+ loadFrameScripts();
+
+ yield startRecording(panel);
+ ok(true, "Recording has started.");
+
+ evalInDebuggee("performWork()");
+
+ yield waitUntil(() => {
+ // Wait until we get the worker markers.
+ let markers = PerformanceController.getCurrentRecording().getMarkers();
+ if (!markers.some(m => m.name == "Worker") ||
+ !markers.some(m => m.workerOperation == "serializeDataOffMainThread") ||
+ !markers.some(m => m.workerOperation == "serializeDataOnMainThread") ||
+ !markers.some(m => m.workerOperation == "deserializeDataOffMainThread") ||
+ !markers.some(m => m.workerOperation == "deserializeDataOnMainThread")) {
+ return false;
+ }
+
+ testWorkerMarkerData(markers.find(m => m.name == "Worker"));
+ return true;
+ });
+
+ yield stopRecording(panel);
+ ok(true, "Recording has ended.");
+
+ for (let node of $$(".waterfall-marker-name[value=Worker")) {
+ testWorkerMarkerUI(node.parentNode.parentNode);
+ }
+
+ yield teardown(panel);
+ finish();
+}
+
+function testWorkerMarkerData(marker) {
+ ok(true, "Found a worker marker.");
+
+ ok("start" in marker,
+ "The start time is specified in the worker marker.");
+ ok("end" in marker,
+ "The end time is specified in the worker marker.");
+
+ ok("workerOperation" in marker,
+ "The worker operation is specified in the worker marker.");
+
+ ok("processType" in marker,
+ "The process type is specified in the worker marker.");
+ ok("isOffMainThread" in marker,
+ "The thread origin is specified in the worker marker.");
+}
+
+function testWorkerMarkerUI(node) {
+ is(node.className, "waterfall-tree-item",
+ "The marker node has the correct class name.");
+ ok(node.hasAttribute("otmt"),
+ "The marker node specifies if it is off the main thread or not.");
+}
+
+/**
+ * Takes a string `script` and evaluates it directly in the content
+ * in potentially a different process.
+ */
+function evalInDebuggee(script) {
+ let { generateUUID } = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
+ let deferred = Promise.defer();
+
+ if (!mm) {
+ throw new Error("`loadFrameScripts()` must be called when using MessageManager.");
+ }
+
+ let id = generateUUID().toString();
+ mm.sendAsyncMessage("devtools:test:eval", { script: script, id: id });
+ mm.addMessageListener("devtools:test:eval:response", handler);
+
+ function handler({ data }) {
+ if (id !== data.id) {
+ return;
+ }
+
+ mm.removeMessageListener("devtools:test:eval:response", handler);
+ deferred.resolve(data.value);
+ }
+
+ return deferred.promise;
+}
+/* eslint-enable */
diff --git a/devtools/client/performance/test/doc_allocs.html b/devtools/client/performance/test/doc_allocs.html
new file mode 100644
index 000000000..83f927e43
--- /dev/null
+++ b/devtools/client/performance/test/doc_allocs.html
@@ -0,0 +1,26 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Performance test page</title>
+ </head>
+
+ <body>
+ <script type="text/javascript">
+ "use strict";
+ const allocs = [];
+ function test() {
+ for (let i = 0; i < 10; i++) {
+ allocs.push({});
+ }
+ }
+
+ // Prevent this script from being garbage collected.
+ window.setInterval(test, 1);
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/performance/test/doc_innerHTML.html b/devtools/client/performance/test/doc_innerHTML.html
new file mode 100644
index 000000000..f5ce72de2
--- /dev/null
+++ b/devtools/client/performance/test/doc_innerHTML.html
@@ -0,0 +1,21 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Performance tool + innerHTML test page</title>
+ </head>
+
+ <body>
+ <script type="text/javascript">
+ "use strict";
+ window.test = function () {
+ document.body.innerHTML = "<h1>LOL</h1>";
+ };
+ setInterval(window.test, 100);
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/performance/test/doc_markers.html b/devtools/client/performance/test/doc_markers.html
new file mode 100644
index 000000000..93ae5c8e1
--- /dev/null
+++ b/devtools/client/performance/test/doc_markers.html
@@ -0,0 +1,38 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Performance tool marker generation</title>
+ </head>
+
+ <body>
+ <script type="text/javascript">
+ "use strict";
+ function test() {
+ let i = 10;
+ // generate sync styles and reflows
+ while (--i) {
+ /* eslint-disable no-unused-vars */
+ let h = document.body.clientHeight;
+ /* eslint-enable no-unused-vars */
+ document.body.style.height = (200 + i) + "px";
+ // paint
+ document.body.style.borderTop = i + "px solid red";
+ }
+ console.time("!!!");
+ test2();
+ }
+ function test2() {
+ console.timeStamp("go");
+ console.timeEnd("!!!");
+ }
+
+ // Prevent this script from being garbage collected.
+ window.setInterval(test, 1);
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/performance/test/doc_simple-test.html b/devtools/client/performance/test/doc_simple-test.html
new file mode 100644
index 000000000..5cda6eaa6
--- /dev/null
+++ b/devtools/client/performance/test/doc_simple-test.html
@@ -0,0 +1,27 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Performance test page</title>
+ </head>
+
+ <body>
+ <script type="text/javascript">
+ "use strict";
+ let x = 1;
+ function test() {
+ document.body.style.borderTop = x + "px solid red";
+ x = 1 ^ x;
+ // flush pending reflows
+ document.body.innerHeight;
+ }
+
+ // Prevent this script from being garbage collected.
+ window.setInterval(test, 1);
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/performance/test/doc_worker.html b/devtools/client/performance/test/doc_worker.html
new file mode 100644
index 000000000..fd1962157
--- /dev/null
+++ b/devtools/client/performance/test/doc_worker.html
@@ -0,0 +1,29 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Performance test page</title>
+ </head>
+
+ <body>
+ <script type="text/javascript">
+ "use strict";
+
+ /* exported performWork */
+ function performWork() {
+ const worker = new Worker("js_simpleWorker.js");
+
+ worker.addEventListener("message", function (e) {
+ console.log(e.data);
+ console.timeStamp("Done");
+ }, false);
+
+ worker.postMessage("Hello World");
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/performance/test/head.js b/devtools/client/performance/test/head.js
new file mode 100644
index 000000000..0aa48d5a1
--- /dev/null
+++ b/devtools/client/performance/test/head.js
@@ -0,0 +1,93 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const { require, loader } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+
+/* exported loader, either, click, dblclick, mousedown, rightMousedown, key */
+// All tests are asynchronous.
+waitForExplicitFinish();
+
+// Performance tests are much heavier because of their reliance on the
+// profiler module, memory measurements, frequent canvas operations etc. Many of
+// of them take longer than 30 seconds to finish on try server VMs, even though
+// they superficially do very little.
+requestLongerTimeout(3);
+
+// Same as `is`, but takes in two possible values.
+const either = (value, a, b, message) => {
+ if (value == a) {
+ is(value, a, message);
+ } else if (value == b) {
+ is(value, b, message);
+ } else {
+ ok(false, message);
+ }
+};
+
+// Shortcut for simulating a click on an element.
+const click = (node, win = window) => {
+ EventUtils.sendMouseEvent({ type: "click" }, node, win);
+};
+
+// Shortcut for simulating a double click on an element.
+const dblclick = (node, win = window) => {
+ EventUtils.sendMouseEvent({ type: "dblclick" }, node, win);
+};
+
+// Shortcut for simulating a mousedown on an element.
+const mousedown = (node, win = window) => {
+ EventUtils.sendMouseEvent({ type: "mousedown" }, node, win);
+};
+
+// Shortcut for simulating a mousedown using the right mouse button on an element.
+const rightMousedown = (node, win = window) => {
+ EventUtils.sendMouseEvent({ type: "mousedown", button: 2 }, node, win);
+};
+
+// Shortcut for firing a key event, like "VK_UP", "VK_DOWN", etc.
+const key = (id, win = window) => {
+ EventUtils.synthesizeKey(id, {}, win);
+};
+
+// Don't pollute global scope.
+(() => {
+ const flags = require("devtools/shared/flags");
+ const PrefUtils = require("devtools/client/performance/test/helpers/prefs");
+
+ flags.testing = true;
+
+ // Make sure all the prefs are reverted to their defaults once tests finish.
+ let stopObservingPrefs = PrefUtils.whenUnknownPrefChanged("devtools.performance",
+ pref => {
+ ok(false, `Unknown pref changed: ${pref}. Please add it to test/helpers/prefs.js ` +
+ "to make sure it's reverted to its default value when the tests finishes, " +
+ "and avoid interfering with future tests.\n");
+ });
+
+ // By default, enable memory flame graphs for tests for now.
+ // TODO: remove when we have flame charts via bug 1148663.
+ Services.prefs.setBoolPref(PrefUtils.UI_ENABLE_MEMORY_FLAME_CHART, true);
+
+ registerCleanupFunction(() => {
+ info("finish() was called, cleaning up...");
+ flags.testing = false;
+
+ PrefUtils.rollbackPrefsToDefault();
+ stopObservingPrefs();
+
+ // Manually stop the profiler module at the end of all tests, to hopefully
+ // avoid at least some leaks on OSX. Theoretically the module should never
+ // be active at this point. We shouldn't have to do this, but rather
+ // find and fix the leak in the module itself. Bug 1257439.
+ let nsIProfilerModule = Cc["@mozilla.org/tools/profiler;1"]
+ .getService(Ci.nsIProfiler);
+ nsIProfilerModule.StopProfiler();
+
+ // Forces GC, CC and shrinking GC to get rid of disconnected docshells
+ // and windows.
+ Cu.forceGC();
+ Cu.forceCC();
+ Cu.forceShrinkingGC();
+ });
+})();
diff --git a/devtools/client/performance/test/helpers/actions.js b/devtools/client/performance/test/helpers/actions.js
new file mode 100644
index 000000000..e6c70e565
--- /dev/null
+++ b/devtools/client/performance/test/helpers/actions.js
@@ -0,0 +1,155 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const { Constants } = require("devtools/client/performance/modules/constants");
+const { once, times } = require("devtools/client/performance/test/helpers/event-utils");
+const { waitUntil } = require("devtools/client/performance/test/helpers/wait-utils");
+
+/**
+ * Starts a manual recording in the given performance tool panel and
+ * waits for it to finish starting.
+ */
+exports.startRecording = function (panel, options = {}) {
+ let controller = panel.panelWin.PerformanceController;
+
+ return Promise.all([
+ controller.startRecording(),
+ exports.waitForRecordingStartedEvents(panel, options)
+ ]);
+};
+
+/**
+ * Stops the latest recording in the given performance tool panel and
+ * waits for it to finish stopping.
+ */
+exports.stopRecording = function (panel, options = {}) {
+ let controller = panel.panelWin.PerformanceController;
+
+ return Promise.all([
+ controller.stopRecording(),
+ exports.waitForRecordingStoppedEvents(panel, options)
+ ]);
+};
+
+/**
+ * Waits for all the necessary events to be emitted after a recording starts.
+ */
+exports.waitForRecordingStartedEvents = function (panel, options = {}) {
+ options.expectedViewState = options.expectedViewState || /^(console-)?recording$/;
+
+ let EVENTS = panel.panelWin.EVENTS;
+ let controller = panel.panelWin.PerformanceController;
+ let view = panel.panelWin.PerformanceView;
+ let overview = panel.panelWin.OverviewView;
+
+ return Promise.all([
+ options.skipWaitingForBackendReady
+ ? null
+ : once(controller, EVENTS.BACKEND_READY_AFTER_RECORDING_START),
+ options.skipWaitingForRecordingStarted
+ ? null
+ : once(controller, EVENTS.RECORDING_STATE_CHANGE, {
+ expectedArgs: { "1": "recording-started" }
+ }),
+ options.skipWaitingForViewState
+ ? null
+ : once(view, EVENTS.UI_STATE_CHANGED, {
+ expectedArgs: { "1": options.expectedViewState }
+ }),
+ options.skipWaitingForOverview
+ ? null
+ : once(overview, EVENTS.UI_OVERVIEW_RENDERED, {
+ expectedArgs: { "1": Constants.FRAMERATE_GRAPH_LOW_RES_INTERVAL }
+ }),
+ ]);
+};
+
+/**
+ * Waits for all the necessary events to be emitted after a recording finishes.
+ */
+exports.waitForRecordingStoppedEvents = function (panel, options = {}) {
+ options.expectedViewClass = options.expectedViewClass || "WaterfallView";
+ options.expectedViewEvent = options.expectedViewEvent || "UI_WATERFALL_RENDERED";
+ options.expectedViewState = options.expectedViewState || "recorded";
+
+ let EVENTS = panel.panelWin.EVENTS;
+ let controller = panel.panelWin.PerformanceController;
+ let view = panel.panelWin.PerformanceView;
+ let overview = panel.panelWin.OverviewView;
+ let subview = panel.panelWin[options.expectedViewClass];
+
+ return Promise.all([
+ options.skipWaitingForBackendReady
+ ? null
+ : once(controller, EVENTS.BACKEND_READY_AFTER_RECORDING_STOP),
+ options.skipWaitingForRecordingStop
+ ? null
+ : once(controller, EVENTS.RECORDING_STATE_CHANGE, {
+ expectedArgs: { "1": "recording-stopping" }
+ }),
+ options.skipWaitingForRecordingStop
+ ? null
+ : once(controller, EVENTS.RECORDING_STATE_CHANGE, {
+ expectedArgs: { "1": "recording-stopped" }
+ }),
+ options.skipWaitingForViewState
+ ? null
+ : once(view, EVENTS.UI_STATE_CHANGED, {
+ expectedArgs: { "1": options.expectedViewState }
+ }),
+ options.skipWaitingForOverview
+ ? null
+ : once(overview, EVENTS.UI_OVERVIEW_RENDERED, {
+ expectedArgs: { "1": Constants.FRAMERATE_GRAPH_HIGH_RES_INTERVAL }
+ }),
+ options.skipWaitingForSubview
+ ? null
+ : once(subview, EVENTS[options.expectedViewEvent]),
+ ]);
+};
+
+/**
+ * Waits for rendering to happen once on all the performance tool's widgets.
+ */
+exports.waitForAllWidgetsRendered = (panel) => {
+ let { panelWin } = panel;
+ let { EVENTS } = panelWin;
+
+ return Promise.all([
+ once(panelWin.OverviewView, EVENTS.UI_MARKERS_GRAPH_RENDERED),
+ once(panelWin.OverviewView, EVENTS.UI_MEMORY_GRAPH_RENDERED),
+ once(panelWin.OverviewView, EVENTS.UI_FRAMERATE_GRAPH_RENDERED),
+ once(panelWin.OverviewView, EVENTS.UI_OVERVIEW_RENDERED),
+ once(panelWin.WaterfallView, EVENTS.UI_WATERFALL_RENDERED),
+ once(panelWin.JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED),
+ once(panelWin.JsFlameGraphView, EVENTS.UI_JS_FLAMEGRAPH_RENDERED),
+ once(panelWin.MemoryCallTreeView, EVENTS.UI_MEMORY_CALL_TREE_RENDERED),
+ once(panelWin.MemoryFlameGraphView, EVENTS.UI_MEMORY_FLAMEGRAPH_RENDERED)
+ ]);
+};
+
+/**
+ * Waits for rendering to happen on the performance tool's overview graph,
+ * making sure some markers were also rendered.
+ */
+exports.waitForOverviewRenderedWithMarkers = (panel, minTimes = 3, minMarkers = 1) => {
+ let { EVENTS, OverviewView, PerformanceController } = panel.panelWin;
+
+ return Promise.all([
+ times(OverviewView, EVENTS.UI_OVERVIEW_RENDERED, minTimes, {
+ expectedArgs: { "1": Constants.FRAMERATE_GRAPH_LOW_RES_INTERVAL }
+ }),
+ waitUntil(() =>
+ PerformanceController.getCurrentRecording().getMarkers().length >= minMarkers
+ ),
+ ]);
+};
+
+/**
+ * Reloads the given tab target.
+ */
+exports.reload = (target) => {
+ target.activeTab.reload();
+ return once(target, "navigate");
+};
diff --git a/devtools/client/performance/test/helpers/dom-utils.js b/devtools/client/performance/test/helpers/dom-utils.js
new file mode 100644
index 000000000..559b2b8d8
--- /dev/null
+++ b/devtools/client/performance/test/helpers/dom-utils.js
@@ -0,0 +1,30 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const Services = require("Services");
+const { waitForMozAfterPaint } = require("devtools/client/performance/test/helpers/wait-utils");
+
+/**
+ * Checks if a DOM node is considered visible.
+ */
+exports.isVisible = (element) => {
+ return !element.classList.contains("hidden") && !element.hidden;
+};
+
+/**
+ * Appends the provided element to the provided parent node. If run in e10s
+ * mode, will also wait for MozAfterPaint to make sure the tab is rendered.
+ * Should be reviewed if Bug 1240509 lands.
+ */
+exports.appendAndWaitForPaint = function (parent, element) {
+ let isE10s = Services.appinfo.browserTabsRemoteAutostart;
+ if (isE10s) {
+ let win = parent.ownerDocument.defaultView;
+ let onMozAfterPaint = waitForMozAfterPaint(win);
+ parent.appendChild(element);
+ return onMozAfterPaint;
+ }
+ parent.appendChild(element);
+ return null;
+};
diff --git a/devtools/client/performance/test/helpers/event-utils.js b/devtools/client/performance/test/helpers/event-utils.js
new file mode 100644
index 000000000..aa184accc
--- /dev/null
+++ b/devtools/client/performance/test/helpers/event-utils.js
@@ -0,0 +1,114 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/* globals dump */
+
+const Services = require("Services");
+
+const KNOWN_EE_APIS = [
+ ["on", "off"],
+ ["addEventListener", "removeEventListener"],
+ ["addListener", "removeListener"]
+];
+
+/**
+ * Listens for any event for a single time on a target, no matter what kind of
+ * event emitter it is, returning a promise resolved with the passed arguments
+ * once the event is fired.
+ */
+exports.once = function (target, eventName, options = {}) {
+ return exports.times(target, eventName, 1, options);
+};
+
+/**
+ * Waits for any event to be fired a specified amount of times on a target, no
+ * matter what kind of event emitter.
+ * Possible options: `useCapture`, `spreadArgs`, `expectedArgs`
+ */
+exports.times = function (target, eventName, receiveCount, options = {}) {
+ let msg = `Waiting for event: '${eventName}' on ${target} for ${receiveCount} time(s)`;
+ if ("expectedArgs" in options) {
+ dump(`${msg} with arguments: ${JSON.stringify(options.expectedArgs)}.\n`);
+ } else {
+ dump(`${msg}.\n`);
+ }
+
+ return new Promise((resolve, reject) => {
+ if (typeof eventName != "string") {
+ reject(new Error(`Unexpected event name: ${eventName}.`));
+ }
+
+ let API = KNOWN_EE_APIS.find(([a, r]) => (a in target) && (r in target));
+ if (!API) {
+ reject(new Error("Target is not a supported event listener."));
+ return;
+ }
+
+ let [add, remove] = API;
+
+ target[add](eventName, function onEvent(...args) {
+ if ("expectedArgs" in options) {
+ for (let index of Object.keys(options.expectedArgs)) {
+ if (
+ // Expected argument matches this regexp.
+ (options.expectedArgs[index] instanceof RegExp &&
+ !options.expectedArgs[index].exec(args[index])) ||
+ // Expected argument is not a regexp and equal to the received arg.
+ (!(options.expectedArgs[index] instanceof RegExp) &&
+ options.expectedArgs[index] != args[index])
+ ) {
+ dump(`Ignoring event '${eventName}' with unexpected argument at index ` +
+ `${index}: ${args[index]}\n`);
+ return;
+ }
+ }
+ }
+ if (--receiveCount > 0) {
+ dump(`Event: '${eventName}' on ${target} needs to be fired ${receiveCount} ` +
+ `more time(s).\n`);
+ } else if (!receiveCount) {
+ dump(`Event: '${eventName}' on ${target} received.\n`);
+ target[remove](eventName, onEvent, options.useCapture);
+ resolve(options.spreadArgs ? args : args[0]);
+ }
+ }, options.useCapture);
+ });
+};
+
+/**
+ * Like `once`, but for observer notifications.
+ */
+exports.observeOnce = function (notificationName, options = {}) {
+ return exports.observeTimes(notificationName, 1, options);
+};
+
+/**
+ * Like `times`, but for observer notifications.
+ * Possible options: `expectedSubject`
+ */
+exports.observeTimes = function (notificationName, receiveCount, options = {}) {
+ dump(`Waiting for notification: '${notificationName}' for ${receiveCount} time(s).\n`);
+
+ return new Promise((resolve, reject) => {
+ if (typeof notificationName != "string") {
+ reject(new Error(`Unexpected notification name: ${notificationName}.`));
+ }
+
+ Services.obs.addObserver(function onObserve(subject, topic, data) {
+ if ("expectedSubject" in options && options.expectedSubject != subject) {
+ dump(`Ignoring notification '${notificationName}' with unexpected subject: ` +
+ `${subject}\n`);
+ return;
+ }
+ if (--receiveCount > 0) {
+ dump(`Notification: '${notificationName}' needs to be fired ${receiveCount} ` +
+ `more time(s).\n`);
+ } else if (!receiveCount) {
+ dump(`Notification: '${notificationName}' received.\n`);
+ Services.obs.removeObserver(onObserve, topic);
+ resolve(data);
+ }
+ }, notificationName, false);
+ });
+};
diff --git a/devtools/client/performance/test/helpers/input-utils.js b/devtools/client/performance/test/helpers/input-utils.js
new file mode 100644
index 000000000..180091d07
--- /dev/null
+++ b/devtools/client/performance/test/helpers/input-utils.js
@@ -0,0 +1,75 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+exports.HORIZONTAL_AXIS = 1;
+exports.VERTICAL_AXIS = 2;
+
+/**
+ * Simulates a command event on an element.
+ */
+exports.command = (node) => {
+ let ev = node.ownerDocument.createEvent("XULCommandEvent");
+ ev.initCommandEvent("command", true, true, node.ownerDocument.defaultView, 0, false,
+ false, false, false, null);
+ node.dispatchEvent(ev);
+};
+
+/**
+ * Simulates a click event on a devtools canvas graph.
+ */
+exports.clickCanvasGraph = (graph, { x, y }) => {
+ x = x || 0;
+ y = y || 0;
+ x /= graph._window.devicePixelRatio;
+ y /= graph._window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseDown({ testX: x, testY: y });
+ graph._onMouseUp({ testX: x, testY: y });
+};
+
+/**
+ * Simulates a drag start event on a devtools canvas graph.
+ */
+exports.dragStartCanvasGraph = (graph, { x, y }) => {
+ x = x || 0;
+ y = y || 0;
+ x /= graph._window.devicePixelRatio;
+ y /= graph._window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseDown({ testX: x, testY: y });
+};
+
+/**
+ * Simulates a drag stop event on a devtools canvas graph.
+ */
+exports.dragStopCanvasGraph = (graph, { x, y }) => {
+ x = x || 0;
+ y = y || 0;
+ x /= graph._window.devicePixelRatio;
+ y /= graph._window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseUp({ testX: x, testY: y });
+};
+
+/**
+ * Simulates a scroll event on a devtools canvas graph.
+ */
+exports.scrollCanvasGraph = (graph, { axis, wheel, x, y }) => {
+ x = x || 1;
+ y = y || 1;
+ x /= graph._window.devicePixelRatio;
+ y /= graph._window.devicePixelRatio;
+ graph._onMouseMove({
+ testX: x,
+ testY: y
+ });
+ graph._onMouseWheel({
+ testX: x,
+ testY: y,
+ axis: axis,
+ detail: wheel,
+ HORIZONTAL_AXIS: exports.HORIZONTAL_AXIS,
+ VERTICAL_AXIS: exports.VERTICAL_AXIS
+ });
+};
diff --git a/devtools/client/performance/test/helpers/moz.build b/devtools/client/performance/test/helpers/moz.build
new file mode 100644
index 000000000..b858530d6
--- /dev/null
+++ b/devtools/client/performance/test/helpers/moz.build
@@ -0,0 +1,20 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'actions.js',
+ 'dom-utils.js',
+ 'event-utils.js',
+ 'input-utils.js',
+ 'panel-utils.js',
+ 'prefs.js',
+ 'profiler-mm-utils.js',
+ 'recording-utils.js',
+ 'synth-utils.js',
+ 'tab-utils.js',
+ 'urls.js',
+ 'wait-utils.js',
+)
diff --git a/devtools/client/performance/test/helpers/panel-utils.js b/devtools/client/performance/test/helpers/panel-utils.js
new file mode 100644
index 000000000..468a86607
--- /dev/null
+++ b/devtools/client/performance/test/helpers/panel-utils.js
@@ -0,0 +1,106 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/* globals dump */
+
+const { gDevTools } = require("devtools/client/framework/devtools");
+const { TargetFactory } = require("devtools/client/framework/target");
+const { addTab, removeTab } = require("devtools/client/performance/test/helpers/tab-utils");
+const { once } = require("devtools/client/performance/test/helpers/event-utils");
+
+/**
+ * Initializes a toolbox panel in a new tab.
+ */
+exports.initPanelInNewTab = function* ({ tool, url, win }, options = {}) {
+ let tab = yield addTab({ url, win }, options);
+ return (yield exports.initPanelInTab({ tool, tab }));
+};
+
+/**
+ * Initializes a toolbox panel in the specified tab.
+ */
+exports.initPanelInTab = function* ({ tool, tab }) {
+ dump(`Initializing a ${tool} panel.\n`);
+
+ let target = TargetFactory.forTab(tab);
+ yield target.makeRemote();
+
+ // Open a toolbox and wait for the connection to the performance actors
+ // to be opened. This is necessary because of the WebConsole's
+ // `profile` and `profileEnd` methods.
+ let toolbox = yield gDevTools.showToolbox(target, tool);
+ yield toolbox.initPerformance();
+
+ let panel = toolbox.getCurrentPanel();
+ return { target, toolbox, panel };
+};
+
+/**
+ * Initializes a performance panel in a new tab.
+ */
+exports.initPerformanceInNewTab = function* ({ url, win }, options = {}) {
+ let tab = yield addTab({ url, win }, options);
+ return (yield exports.initPerformanceInTab({ tab }));
+};
+
+/**
+ * Initializes a performance panel in the specified tab.
+ */
+exports.initPerformanceInTab = function* ({ tab }) {
+ return (yield exports.initPanelInTab({
+ tool: "performance",
+ tab: tab
+ }));
+};
+
+/**
+ * Initializes a webconsole panel in a new tab.
+ * Returns a console property that allows calls to `profile` and `profileEnd`.
+ */
+exports.initConsoleInNewTab = function* ({ url, win }, options = {}) {
+ let tab = yield addTab({ url, win }, options);
+ return (yield exports.initConsoleInTab({ tab }));
+};
+
+/**
+ * Initializes a webconsole panel in the specified tab.
+ * Returns a console property that allows calls to `profile` and `profileEnd`.
+ */
+exports.initConsoleInTab = function* ({ tab }) {
+ let { target, toolbox, panel } = yield exports.initPanelInTab({
+ tool: "webconsole",
+ tab: tab
+ });
+
+ let consoleMethod = function* (method, label, event) {
+ let recordingEventReceived = once(toolbox.performance, event);
+ if (label === undefined) {
+ yield panel.hud.jsterm.execute(`console.${method}()`);
+ } else {
+ yield panel.hud.jsterm.execute(`console.${method}("${label}")`);
+ }
+ yield recordingEventReceived;
+ };
+
+ let profile = function* (label) {
+ return yield consoleMethod("profile", label, "recording-started");
+ };
+
+ let profileEnd = function* (label) {
+ return yield consoleMethod("profileEnd", label, "recording-stopped");
+ };
+
+ return { target, toolbox, panel, console: { profile, profileEnd } };
+};
+
+/**
+ * Tears down a toolbox panel and removes an associated tab.
+ */
+exports.teardownToolboxAndRemoveTab = function* (panel, options) {
+ dump("Destroying panel.\n");
+
+ let tab = panel.target.tab;
+ yield panel.toolbox.destroy();
+ yield removeTab(tab, options);
+};
diff --git a/devtools/client/performance/test/helpers/prefs.js b/devtools/client/performance/test/helpers/prefs.js
new file mode 100644
index 000000000..4d17afe12
--- /dev/null
+++ b/devtools/client/performance/test/helpers/prefs.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const Services = require("Services");
+const { Preferences } = require("resource://gre/modules/Preferences.jsm");
+
+// Prefs to revert to default once tests finish. Keep these in sync with
+// all the preferences defined in devtools/client/preferences/devtools.js.
+exports.MEMORY_SAMPLE_PROB_PREF = "devtools.performance.memory.sample-probability";
+exports.MEMORY_MAX_LOG_LEN_PREF = "devtools.performance.memory.max-log-length";
+exports.PROFILER_BUFFER_SIZE_PREF = "devtools.performance.profiler.buffer-size";
+exports.PROFILER_SAMPLE_RATE_PREF = "devtools.performance.profiler.sample-frequency-khz";
+
+exports.UI_EXPERIMENTAL_PREF = "devtools.performance.ui.experimental";
+exports.UI_INVERT_CALL_TREE_PREF = "devtools.performance.ui.invert-call-tree";
+exports.UI_INVERT_FLAME_PREF = "devtools.performance.ui.invert-flame-graph";
+exports.UI_FLATTEN_RECURSION_PREF = "devtools.performance.ui.flatten-tree-recursion";
+exports.UI_SHOW_PLATFORM_DATA_PREF = "devtools.performance.ui.show-platform-data";
+exports.UI_SHOW_IDLE_BLOCKS_PREF = "devtools.performance.ui.show-idle-blocks";
+exports.UI_ENABLE_FRAMERATE_PREF = "devtools.performance.ui.enable-framerate";
+exports.UI_ENABLE_MEMORY_PREF = "devtools.performance.ui.enable-memory";
+exports.UI_ENABLE_ALLOCATIONS_PREF = "devtools.performance.ui.enable-allocations";
+exports.UI_ENABLE_MEMORY_FLAME_CHART = "devtools.performance.ui.enable-memory-flame";
+
+exports.DEFAULT_PREF_VALUES = [
+ "devtools.debugger.log",
+ "devtools.performance.enabled",
+ "devtools.performance.timeline.hidden-markers",
+ exports.MEMORY_SAMPLE_PROB_PREF,
+ exports.MEMORY_MAX_LOG_LEN_PREF,
+ exports.PROFILER_BUFFER_SIZE_PREF,
+ exports.PROFILER_SAMPLE_RATE_PREF,
+ exports.UI_EXPERIMENTAL_PREF,
+ exports.UI_INVERT_CALL_TREE_PREF,
+ exports.UI_INVERT_FLAME_PREF,
+ exports.UI_FLATTEN_RECURSION_PREF,
+ exports.UI_SHOW_PLATFORM_DATA_PREF,
+ exports.UI_SHOW_IDLE_BLOCKS_PREF,
+ exports.UI_ENABLE_FRAMERATE_PREF,
+ exports.UI_ENABLE_MEMORY_PREF,
+ exports.UI_ENABLE_ALLOCATIONS_PREF,
+ exports.UI_ENABLE_MEMORY_FLAME_CHART,
+ "devtools.performance.ui.show-jit-optimizations",
+ "devtools.performance.ui.show-triggers-for-gc-types",
+].reduce((prefValues, prefName) => {
+ prefValues[prefName] = Preferences.get(prefName);
+ return prefValues;
+}, {});
+
+/**
+ * Invokes callback when a pref which is not in the `DEFAULT_PREF_VALUES` store
+ * is changed. Returns a cleanup function.
+ */
+exports.whenUnknownPrefChanged = function (branch, callback) {
+ function onObserve(subject, topic, data) {
+ if (!(data in exports.DEFAULT_PREF_VALUES)) {
+ callback(data);
+ }
+ }
+ Services.prefs.addObserver(branch, onObserve, false);
+ return () => Services.prefs.removeObserver(branch, onObserve);
+};
+
+/**
+ * Reverts all known preferences to their default values.
+ */
+exports.rollbackPrefsToDefault = function () {
+ for (let prefName of Object.keys(exports.DEFAULT_PREF_VALUES)) {
+ Preferences.set(prefName, exports.DEFAULT_PREF_VALUES[prefName]);
+ }
+};
diff --git a/devtools/client/performance/test/helpers/profiler-mm-utils.js b/devtools/client/performance/test/helpers/profiler-mm-utils.js
new file mode 100644
index 000000000..bffebf818
--- /dev/null
+++ b/devtools/client/performance/test/helpers/profiler-mm-utils.js
@@ -0,0 +1,117 @@
+/* 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";
+
+/**
+ * The following functions are used in testing to control and inspect
+ * the nsIProfiler in child process content. These should be called from
+ * the parent process.
+ */
+
+const { Cc, Ci } = require("chrome");
+const { Task } = require("devtools/shared/task");
+
+const FRAME_SCRIPT_UTILS_URL = "chrome://devtools/content/shared/frame-script-utils.js";
+
+let gMM = null;
+
+/**
+ * Loads the relevant frame scripts into the provided browser's message manager.
+ */
+exports.pmmLoadFrameScripts = (gBrowser) => {
+ gMM = gBrowser.selectedBrowser.messageManager;
+ gMM.loadFrameScript(FRAME_SCRIPT_UTILS_URL, false);
+};
+
+/**
+ * Clears the cached message manager.
+ */
+exports.pmmClearFrameScripts = () => {
+ gMM = null;
+};
+
+/**
+ * Sends a message to the message listener, attaching an id to the payload data.
+ * Resolves a returned promise when the response is received from the message
+ * listener, with the same id as part of the response payload data.
+ */
+exports.pmmUniqueMessage = function (message, payload) {
+ if (!gMM) {
+ throw new Error("`pmmLoadFrameScripts()` must be called when using MessageManager.");
+ }
+
+ let { generateUUID } = Cc["@mozilla.org/uuid-generator;1"]
+ .getService(Ci.nsIUUIDGenerator);
+ payload.id = generateUUID().toString();
+
+ return new Promise(resolve => {
+ gMM.addMessageListener(message + ":response", function onHandler({ data }) {
+ if (payload.id == data.id) {
+ gMM.removeMessageListener(message + ":response", onHandler);
+ resolve(data.data);
+ }
+ });
+ gMM.sendAsyncMessage(message, payload);
+ });
+};
+
+/**
+ * Checks if the nsProfiler module is active.
+ */
+exports.pmmIsProfilerActive = () => {
+ return exports.pmmSendProfilerCommand("IsActive");
+};
+
+/**
+ * Starts the nsProfiler module.
+ */
+exports.pmmStartProfiler = Task.async(function* ({ entries, interval, features }) {
+ let isActive = (yield exports.pmmSendProfilerCommand("IsActive")).isActive;
+ if (!isActive) {
+ return exports.pmmSendProfilerCommand("StartProfiler", [entries, interval, features,
+ features.length]);
+ }
+ return null;
+});
+/**
+ * Stops the nsProfiler module.
+ */
+exports.pmmStopProfiler = Task.async(function* () {
+ let isActive = (yield exports.pmmSendProfilerCommand("IsActive")).isActive;
+ if (isActive) {
+ return exports.pmmSendProfilerCommand("StopProfiler");
+ }
+ return null;
+});
+
+/**
+ * Calls a method on the nsProfiler module.
+ */
+exports.pmmSendProfilerCommand = (method, args = []) => {
+ return exports.pmmUniqueMessage("devtools:test:profiler", { method, args });
+};
+
+/**
+ * Evaluates a script in content, returning a promise resolved with the
+ * returned result.
+ */
+exports.pmmEvalInDebuggee = (script) => {
+ return exports.pmmUniqueMessage("devtools:test:eval", { script });
+};
+
+/**
+ * Evaluates a console method in content.
+ */
+exports.pmmConsoleMethod = function (method, ...args) {
+ // Terrible ugly hack -- this gets stringified when it uses the
+ // message manager, so an undefined arg in `console.profileEnd()`
+ // turns into a stringified "null", which is terrible. This method
+ // is only used for test helpers, so swap out the argument if its undefined
+ // with an empty string. Differences between empty string and undefined are
+ // tested on the front itself.
+ if (args[0] == null) {
+ args[0] = "";
+ }
+ return exports.pmmUniqueMessage("devtools:test:console", { method, args });
+};
diff --git a/devtools/client/performance/test/helpers/recording-utils.js b/devtools/client/performance/test/helpers/recording-utils.js
new file mode 100644
index 000000000..e51e2d5dd
--- /dev/null
+++ b/devtools/client/performance/test/helpers/recording-utils.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";
+
+/**
+ * These utilities provide a functional interface for accessing the particulars
+ * about the recording's details.
+ */
+
+/**
+ * Access the selected view from the panel's recording list.
+ *
+ * @param {object} panel - The current panel.
+ * @return {object} The recording model.
+ */
+exports.getSelectedRecording = function (panel) {
+ const view = panel.panelWin.RecordingsView;
+ return view.selected;
+};
+
+/**
+ * Set the selected index of the recording via the panel.
+ *
+ * @param {object} panel - The current panel.
+ * @return {number} index
+ */
+exports.setSelectedRecording = function (panel, index) {
+ const view = panel.panelWin.RecordingsView;
+ view.setSelectedByIndex(index);
+ return index;
+};
+
+/**
+ * Access the selected view from the panel's recording list.
+ *
+ * @param {object} panel - The current panel.
+ * @return {number} index
+ */
+exports.getSelectedRecordingIndex = function (panel) {
+ const view = panel.panelWin.RecordingsView;
+ return view.getSelectedIndex();
+};
+
+exports.getDurationLabelText = function (panel, elementIndex) {
+ const { $$ } = panel.panelWin;
+ const elements = $$(".recording-list-item-duration", panel.panelWin.document);
+ return elements[elementIndex].innerHTML;
+};
+
+exports.getRecordingsCount = function (panel) {
+ const { $$ } = panel.panelWin;
+ return $$(".recording-list-item", panel.panelWin.document).length;
+};
diff --git a/devtools/client/performance/test/helpers/synth-utils.js b/devtools/client/performance/test/helpers/synth-utils.js
new file mode 100644
index 000000000..d4631a3f1
--- /dev/null
+++ b/devtools/client/performance/test/helpers/synth-utils.js
@@ -0,0 +1,99 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Generates a generalized profile with some samples.
+ */
+exports.synthesizeProfile = () => {
+ const { CATEGORY_MASK } = require("devtools/client/performance/modules/categories");
+ const RecordingUtils = require("devtools/shared/performance/recording-utils");
+
+ return RecordingUtils.deflateProfile({
+ meta: { version: 2 },
+ threads: [{
+ samples: [{
+ time: 1,
+ frames: [
+ { category: CATEGORY_MASK("other"), location: "(root)" },
+ { category: CATEGORY_MASK("other"), location: "A (http://foo/bar/baz:12)" },
+ { category: CATEGORY_MASK("css"), location: "B (http://foo/bar/baz:34)" },
+ { category: CATEGORY_MASK("js"), location: "C (http://foo/bar/baz:56)" }
+ ]
+ }, {
+ time: 1 + 1,
+ frames: [
+ { category: CATEGORY_MASK("other"), location: "(root)" },
+ { category: CATEGORY_MASK("other"), location: "A (http://foo/bar/baz:12)" },
+ { category: CATEGORY_MASK("css"), location: "B (http://foo/bar/baz:34)" },
+ { category: CATEGORY_MASK("gc", 1), location: "D (http://foo/bar/baz:78:9)" }
+ ]
+ }, {
+ time: 1 + 1 + 2,
+ frames: [
+ { category: CATEGORY_MASK("other"), location: "(root)" },
+ { category: CATEGORY_MASK("other"), location: "A (http://foo/bar/baz:12)" },
+ { category: CATEGORY_MASK("css"), location: "B (http://foo/bar/baz:34)" },
+ { category: CATEGORY_MASK("gc", 1), location: "D (http://foo/bar/baz:78:9)" }
+ ]
+ }, {
+ time: 1 + 1 + 2 + 3,
+ frames: [
+ { category: CATEGORY_MASK("other"), location: "(root)" },
+ { category: CATEGORY_MASK("other"), location: "A (http://foo/bar/baz:12)" },
+ { category: CATEGORY_MASK("gc", 2), location: "E (http://foo/bar/baz:90)" },
+ { category: CATEGORY_MASK("network"), location: "F (http://foo/bar/baz:99)" }
+ ]
+ }]
+ }]
+ });
+};
+
+/**
+ * Generates a simple implementation for a tree class.
+ */
+exports.synthesizeCustomTreeClass = () => {
+ const { Cu } = require("chrome");
+ const { AbstractTreeItem } = Cu.import("resource://devtools/client/shared/widgets/AbstractTreeItem.jsm", {});
+ const { Heritage } = require("devtools/client/shared/widgets/view-helpers");
+
+ function MyCustomTreeItem(dataSrc, properties) {
+ AbstractTreeItem.call(this, properties);
+ this.itemDataSrc = dataSrc;
+ }
+
+ MyCustomTreeItem.prototype = Heritage.extend(AbstractTreeItem.prototype, {
+ _displaySelf: function (document, arrowNode) {
+ let node = document.createElement("hbox");
+ node.style.marginInlineStart = (this.level * 10) + "px";
+ node.appendChild(arrowNode);
+ node.appendChild(document.createTextNode(this.itemDataSrc.label));
+ return node;
+ },
+
+ _populateSelf: function (children) {
+ for (let childDataSrc of this.itemDataSrc.children) {
+ children.push(new MyCustomTreeItem(childDataSrc, {
+ parent: this,
+ level: this.level + 1
+ }));
+ }
+ }
+ });
+
+ const myDataSrc = {
+ label: "root",
+ children: [{
+ label: "foo",
+ children: []
+ }, {
+ label: "bar",
+ children: [{
+ label: "baz",
+ children: []
+ }]
+ }]
+ };
+
+ return { MyCustomTreeItem, myDataSrc };
+};
diff --git a/devtools/client/performance/test/helpers/tab-utils.js b/devtools/client/performance/test/helpers/tab-utils.js
new file mode 100644
index 000000000..3247faabf
--- /dev/null
+++ b/devtools/client/performance/test/helpers/tab-utils.js
@@ -0,0 +1,85 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/* globals dump */
+
+const Services = require("Services");
+const tabs = require("sdk/tabs");
+const tabUtils = require("sdk/tabs/utils");
+const { viewFor } = require("sdk/view/core");
+const { waitForDelayedStartupFinished } = require("devtools/client/performance/test/helpers/wait-utils");
+const { gDevTools } = require("devtools/client/framework/devtools");
+
+/**
+ * Gets a random integer in between an interval. Used to uniquely identify
+ * added tabs by augmenting the URL.
+ */
+function getRandomInt(min, max) {
+ return Math.floor(Math.random() * (max - min + 1)) + min;
+}
+
+/**
+ * Adds a browser tab with the given url in the specified window and waits
+ * for it to load.
+ */
+exports.addTab = function ({ url, win }, options = {}) {
+ let id = getRandomInt(0, Number.MAX_SAFE_INTEGER - 1);
+ url += `#${id}`;
+
+ dump(`Adding tab with url: ${url}.\n`);
+
+ return new Promise(resolve => {
+ let tab;
+
+ tabs.on("ready", function onOpen(model) {
+ if (tab != viewFor(model)) {
+ return;
+ }
+ dump(`Tab added and finished loading: ${model.url}.\n`);
+ tabs.off("ready", onOpen);
+ resolve(tab);
+ });
+
+ win.focus();
+ tab = tabUtils.openTab(win, url);
+
+ if (options.dontWaitForTabReady) {
+ resolve(tab);
+ }
+ });
+};
+
+/**
+ * Removes a browser tab from the specified window and waits for it to close.
+ */
+exports.removeTab = function (tab, options = {}) {
+ dump(`Removing tab: ${tabUtils.getURI(tab)}.\n`);
+
+ return new Promise(resolve => {
+ tabs.on("close", function onClose(model) {
+ if (tab != viewFor(model)) {
+ return;
+ }
+ dump(`Tab removed and finished closing: ${model.url}.\n`);
+ tabs.off("close", onClose);
+ resolve(tab);
+ });
+
+ tabUtils.closeTab(tab);
+
+ if (options.dontWaitForTabClose) {
+ resolve(tab);
+ }
+ });
+};
+
+/**
+ * Adds a browser window with the provided options.
+ */
+exports.addWindow = function* (options) {
+ let { OpenBrowserWindow } = Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
+ let win = OpenBrowserWindow(options);
+ yield waitForDelayedStartupFinished(win);
+ return win;
+};
diff --git a/devtools/client/performance/test/helpers/urls.js b/devtools/client/performance/test/helpers/urls.js
new file mode 100644
index 000000000..3bf1180b3
--- /dev/null
+++ b/devtools/client/performance/test/helpers/urls.js
@@ -0,0 +1,6 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+exports.EXAMPLE_URL = "http://example.com/browser/devtools/client/performance/test";
+exports.SIMPLE_URL = `${exports.EXAMPLE_URL}/doc_simple-test.html`;
diff --git a/devtools/client/performance/test/helpers/wait-utils.js b/devtools/client/performance/test/helpers/wait-utils.js
new file mode 100644
index 000000000..be654b7d8
--- /dev/null
+++ b/devtools/client/performance/test/helpers/wait-utils.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/* globals dump */
+
+const { CC } = require("chrome");
+const { Task } = require("devtools/shared/task");
+const DevToolsUtils = require("devtools/shared/DevToolsUtils");
+const { once, observeOnce } = require("devtools/client/performance/test/helpers/event-utils");
+
+/**
+ * Blocks the main thread for the specified amount of time.
+ */
+exports.busyWait = function (time) {
+ dump(`Busy waiting for: ${time} milliseconds.\n`);
+ let start = Date.now();
+ /* eslint-disable no-unused-vars */
+ let stack;
+ while (Date.now() - start < time) {
+ stack = CC.stack;
+ }
+ /* eslint-enable no-unused-vars */
+};
+
+/**
+ * Idly waits for the specified amount of time.
+ */
+exports.idleWait = function (time) {
+ dump(`Idly waiting for: ${time} milliseconds.\n`);
+ return DevToolsUtils.waitForTime(time);
+};
+
+/**
+ * Waits until a predicate returns true.
+ */
+exports.waitUntil = function* (predicate, interval = 100, tries = 100) {
+ for (let i = 1; i <= tries; i++) {
+ if (yield Task.spawn(predicate)) {
+ dump(`Predicate returned true after ${i} tries.\n`);
+ return;
+ }
+ yield exports.idleWait(interval);
+ }
+ throw new Error(`Predicate returned false after ${tries} tries, aborting.\n`);
+};
+
+/**
+ * Waits for a `MozAfterPaint` event to be fired on the specified window.
+ */
+exports.waitForMozAfterPaint = function (window) {
+ return once(window, "MozAfterPaint");
+};
+
+/**
+ * Waits for the `browser-delayed-startup-finished` observer notification
+ * to be fired on the specified window.
+ */
+exports.waitForDelayedStartupFinished = function (window) {
+ return observeOnce("browser-delayed-startup-finished", { expectedSubject: window });
+};
diff --git a/devtools/client/performance/test/js_simpleWorker.js b/devtools/client/performance/test/js_simpleWorker.js
new file mode 100644
index 000000000..5d254dfb3
--- /dev/null
+++ b/devtools/client/performance/test/js_simpleWorker.js
@@ -0,0 +1,6 @@
+"use strict";
+
+self.addEventListener("message", function (e) {
+ self.postMessage(e.data);
+ self.close();
+}, false);
diff --git a/devtools/client/performance/test/moz.build b/devtools/client/performance/test/moz.build
new file mode 100644
index 000000000..6bdf1a018
--- /dev/null
+++ b/devtools/client/performance/test/moz.build
@@ -0,0 +1,8 @@
+# vim: set filetype=python:
+# 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/.
+
+DIRS += [
+ 'helpers',
+]
diff --git a/devtools/client/performance/test/unit/.eslintrc.js b/devtools/client/performance/test/unit/.eslintrc.js
new file mode 100644
index 000000000..aec096a0f
--- /dev/null
+++ b/devtools/client/performance/test/unit/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../../../.eslintrc.xpcshell.js"
+};
diff --git a/devtools/client/performance/test/unit/head.js b/devtools/client/performance/test/unit/head.js
new file mode 100644
index 000000000..84128a7e8
--- /dev/null
+++ b/devtools/client/performance/test/unit/head.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+/* exported Cc, Ci, Cu, Cr, Services, console, PLATFORM_DATA_PREF, getFrameNodePath,
+ synthesizeProfileForTest */
+var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+var { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+var Services = require("Services");
+var { console } = require("resource://gre/modules/Console.jsm");
+const RecordingUtils = require("devtools/shared/performance/recording-utils");
+const PLATFORM_DATA_PREF = "devtools.performance.ui.show-platform-data";
+
+/**
+ * Get a path in a FrameNode call tree.
+ */
+function getFrameNodePath(root, path) {
+ let calls = root.calls;
+ let foundNode;
+ for (let key of path.split(" > ")) {
+ foundNode = calls.find((node) => node.key == key);
+ if (!foundNode) {
+ break;
+ }
+ calls = foundNode.calls;
+ }
+ return foundNode;
+}
+
+/**
+ * Synthesize a profile for testing.
+ */
+function synthesizeProfileForTest(samples) {
+ samples.unshift({
+ time: 0,
+ frames: [
+ { location: "(root)" }
+ ]
+ });
+
+ let uniqueStacks = new RecordingUtils.UniqueStacks();
+ return RecordingUtils.deflateThread({
+ samples: samples,
+ markers: []
+ }, uniqueStacks);
+}
diff --git a/devtools/client/performance/test/unit/test_frame-utils-01.js b/devtools/client/performance/test/unit/test_frame-utils-01.js
new file mode 100644
index 000000000..a85ec9282
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_frame-utils-01.js
@@ -0,0 +1,133 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that frame-utils isContent and parseLocation work as intended
+ * when parsing over frames from the profiler.
+ */
+
+const CONTENT_LOCATIONS = [
+ "hello/<.world (https://foo/bar.js:123:987)",
+ "hello/<.world (http://foo/bar.js:123:987)",
+ "hello/<.world (http://foo/bar.js:123)",
+ "hello/<.world (http://foo/bar.js#baz:123:987)",
+ "hello/<.world (http://foo/bar.js?myquery=params&search=1:123:987)",
+ "hello/<.world (http://foo/#bar:123:987)",
+ "hello/<.world (http://foo/:123:987)",
+
+ // Test scripts with port numbers (bug 1164131)
+ "hello/<.world (http://localhost:8888/file.js:100:1)",
+ "hello/<.world (http://localhost:8888/file.js:100)",
+
+ // Eval
+ "hello/<.world (http://localhost:8888/file.js line 65 > eval:1)",
+
+ // Occurs when executing an inline script on a root html page with port
+ // (I've never seen it with a column number but check anyway) bug 1164131
+ "hello/<.world (http://localhost:8888/:1)",
+ "hello/<.world (http://localhost:8888/:100:50)",
+
+ // bug 1197636
+ "Native[\"arraycopy(blah)\"] (http://localhost:8888/profiler.html:4)",
+ "Native[\"arraycopy(blah)\"] (http://localhost:8888/profiler.html:4:5)",
+].map(argify);
+
+const CHROME_LOCATIONS = [
+ { location: "Startup::XRE_InitChildProcess", line: 456, column: 123 },
+ { location: "chrome://browser/content/content.js", line: 456, column: 123 },
+ "setTimeout_timer (resource://gre/foo.js:123:434)",
+ "hello/<.world (jar:file://Users/mcurie/Dev/jetpacks.js)",
+ "hello/<.world (resource://foo.js -> http://bar/baz.js:123:987)",
+ "EnterJIT",
+].map(argify);
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function () {
+ const { computeIsContentAndCategory, parseLocation } = require("devtools/client/performance/modules/logic/frame-utils");
+ let isContent = (frame) => {
+ computeIsContentAndCategory(frame);
+ return frame.isContent;
+ };
+
+ for (let frame of CONTENT_LOCATIONS) {
+ ok(isContent.apply(null, frameify(frame)),
+ `${frame[0]} should be considered a content frame.`);
+ }
+
+ for (let frame of CHROME_LOCATIONS) {
+ ok(!isContent.apply(null, frameify(frame)),
+ `${frame[0]} should not be considered a content frame.`);
+ }
+
+ // functionName, fileName, host, url, line, column
+ const FIELDS = ["functionName", "fileName", "host", "url", "line", "column", "host",
+ "port"];
+
+ /* eslint-disable max-len */
+ const PARSED_CONTENT = [
+ ["hello/<.world", "bar.js", "foo", "https://foo/bar.js", 123, 987, "foo", null],
+ ["hello/<.world", "bar.js", "foo", "http://foo/bar.js", 123, 987, "foo", null],
+ ["hello/<.world", "bar.js", "foo", "http://foo/bar.js", 123, null, "foo", null],
+ ["hello/<.world", "bar.js", "foo", "http://foo/bar.js#baz", 123, 987, "foo", null],
+ ["hello/<.world", "bar.js", "foo", "http://foo/bar.js?myquery=params&search=1", 123, 987, "foo", null],
+ ["hello/<.world", "/", "foo", "http://foo/#bar", 123, 987, "foo", null],
+ ["hello/<.world", "/", "foo", "http://foo/", 123, 987, "foo", null],
+ ["hello/<.world", "file.js", "localhost:8888", "http://localhost:8888/file.js", 100, 1, "localhost:8888", 8888],
+ ["hello/<.world", "file.js", "localhost:8888", "http://localhost:8888/file.js", 100, null, "localhost:8888", 8888],
+ ["hello/<.world", "file.js (eval:1)", "localhost:8888", "http://localhost:8888/file.js", 65, null, "localhost:8888", 8888],
+ ["hello/<.world", "/", "localhost:8888", "http://localhost:8888/", 1, null, "localhost:8888", 8888],
+ ["hello/<.world", "/", "localhost:8888", "http://localhost:8888/", 100, 50, "localhost:8888", 8888],
+ ["Native[\"arraycopy(blah)\"]", "profiler.html", "localhost:8888", "http://localhost:8888/profiler.html", 4, null, "localhost:8888", 8888],
+ ["Native[\"arraycopy(blah)\"]", "profiler.html", "localhost:8888", "http://localhost:8888/profiler.html", 4, 5, "localhost:8888", 8888],
+ ];
+ /* eslint-enable max-len */
+
+ for (let i = 0; i < PARSED_CONTENT.length; i++) {
+ let parsed = parseLocation.apply(null, CONTENT_LOCATIONS[i]);
+ for (let j = 0; j < FIELDS.length; j++) {
+ equal(parsed[FIELDS[j]], PARSED_CONTENT[i][j],
+ `${CONTENT_LOCATIONS[i]} was parsed to correct ${FIELDS[j]}`);
+ }
+ }
+
+ const PARSED_CHROME = [
+ ["Startup::XRE_InitChildProcess", null, null, null, 456, 123, null, null],
+ ["chrome://browser/content/content.js", null, null, null, 456, 123, null, null],
+ ["setTimeout_timer", "foo.js", null, "resource://gre/foo.js", 123, 434, null, null],
+ ["hello/<.world (jar:file://Users/mcurie/Dev/jetpacks.js)", null, null, null,
+ null, null, null, null],
+ ["hello/<.world", "baz.js", "bar", "http://bar/baz.js", 123, 987, "bar", null],
+ ["EnterJIT", null, null, null, null, null, null, null],
+ ];
+
+ for (let i = 0; i < PARSED_CHROME.length; i++) {
+ let parsed = parseLocation.apply(null, CHROME_LOCATIONS[i]);
+ for (let j = 0; j < FIELDS.length; j++) {
+ equal(parsed[FIELDS[j]], PARSED_CHROME[i][j],
+ `${CHROME_LOCATIONS[i]} was parsed to correct ${FIELDS[j]}`);
+ }
+ }
+});
+
+/**
+ * Takes either a string or an object and turns it into an array that
+ * parseLocation.apply expects.
+ */
+function argify(val) {
+ if (typeof val === "string") {
+ return [val];
+ }
+ return [val.location, val.line, val.column];
+}
+
+/**
+ * Takes the result of argify and turns it into an array that can be passed to
+ * isContent.apply.
+ */
+function frameify(val) {
+ return [{ location: val[0] }];
+}
diff --git a/devtools/client/performance/test/unit/test_frame-utils-02.js b/devtools/client/performance/test/unit/test_frame-utils-02.js
new file mode 100644
index 000000000..ef0d275bd
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_frame-utils-02.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests the function testing whether or not a frame is content or chrome
+ * works properly.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function () {
+ let FrameUtils = require("devtools/client/performance/modules/logic/frame-utils");
+
+ let isContent = (frame) => {
+ FrameUtils.computeIsContentAndCategory(frame);
+ return frame.isContent;
+ };
+
+ ok(isContent({ location: "http://foo" }),
+ "Verifying content/chrome frames is working properly.");
+ ok(isContent({ location: "https://foo" }),
+ "Verifying content/chrome frames is working properly.");
+ ok(isContent({ location: "file://foo" }),
+ "Verifying content/chrome frames is working properly.");
+
+ ok(!isContent({ location: "chrome://foo" }),
+ "Verifying content/chrome frames is working properly.");
+ ok(!isContent({ location: "resource://foo" }),
+ "Verifying content/chrome frames is working properly.");
+
+ ok(!isContent({ location: "chrome://foo -> http://bar" }),
+ "Verifying content/chrome frames is working properly.");
+ ok(!isContent({ location: "chrome://foo -> https://bar" }),
+ "Verifying content/chrome frames is working properly.");
+ ok(!isContent({ location: "chrome://foo -> file://bar" }),
+ "Verifying content/chrome frames is working properly.");
+
+ ok(!isContent({ location: "resource://foo -> http://bar" }),
+ "Verifying content/chrome frames is working properly.");
+ ok(!isContent({ location: "resource://foo -> https://bar" }),
+ "Verifying content/chrome frames is working properly.");
+ ok(!isContent({ location: "resource://foo -> file://bar" }),
+ "Verifying content/chrome frames is working properly.");
+
+ ok(!isContent({ category: 1, location: "chrome://foo" }),
+ "Verifying content/chrome frames is working properly.");
+ ok(!isContent({ category: 1, location: "resource://foo" }),
+ "Verifying content/chrome frames is working properly.");
+
+ ok(!isContent({ category: 1, location: "file://foo -> http://bar" }),
+ "Verifying content/chrome frames is working properly.");
+ ok(!isContent({ category: 1, location: "file://foo -> https://bar" }),
+ "Verifying content/chrome frames is working properly.");
+ ok(!isContent({ category: 1, location: "file://foo -> file://bar" }),
+ "Verifying content/chrome frames is working properly.");
+});
diff --git a/devtools/client/performance/test/unit/test_jit-graph-data.js b/devtools/client/performance/test/unit/test_jit-graph-data.js
new file mode 100644
index 000000000..b298f4bcc
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_jit-graph-data.js
@@ -0,0 +1,209 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Unit test for `createTierGraphDataFromFrameNode` function.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+const SAMPLE_COUNT = 1000;
+const RESOLUTION = 50;
+const TIME_PER_SAMPLE = 5;
+
+// Offset needed since ThreadNode requires the first sample to be strictly
+// greater than its start time. This lets us still have pretty numbers
+// in this test to keep it (more) simple, which it sorely needs.
+const TIME_OFFSET = 5;
+
+add_task(function test() {
+ let { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
+ let { createTierGraphDataFromFrameNode } = require("devtools/client/performance/modules/logic/jit");
+
+ // Select the second half of the set of samples
+ let startTime = (SAMPLE_COUNT / 2 * TIME_PER_SAMPLE) - TIME_OFFSET;
+ let endTime = (SAMPLE_COUNT * TIME_PER_SAMPLE) - TIME_OFFSET;
+ let invertTree = true;
+
+ let root = new ThreadNode(gThread, { invertTree, startTime, endTime });
+
+ equal(root.samples, SAMPLE_COUNT / 2,
+ "root has correct amount of samples");
+ equal(root.sampleTimes.length, SAMPLE_COUNT / 2,
+ "root has correct amount of sample times");
+ // Add time offset since the first sample begins TIME_OFFSET after startTime
+ equal(root.sampleTimes[0], startTime + TIME_OFFSET,
+ "root recorded first sample time in scope");
+ equal(root.sampleTimes[root.sampleTimes.length - 1], endTime,
+ "root recorded last sample time in scope");
+
+ let frame = getFrameNodePath(root, "X");
+ let data = createTierGraphDataFromFrameNode(frame, root.sampleTimes,
+ (endTime - startTime) / RESOLUTION);
+
+ let TIME_PER_WINDOW = SAMPLE_COUNT / 2 / RESOLUTION * TIME_PER_SAMPLE;
+
+ // Filter out the dupes created with the same delta so the graph
+ // can render correctly.
+ let filteredData = [];
+ for (let i = 0; i < data.length; i++) {
+ if (!i || data[i].delta !== data[i - 1].delta) {
+ filteredData.push(data[i]);
+ }
+ }
+ data = filteredData;
+
+ for (let i = 0; i < 11; i++) {
+ equal(data[i].delta, startTime + TIME_OFFSET + (TIME_PER_WINDOW * i),
+ "first window has correct x");
+ equal(data[i].values[0], 0.2, "first window has 2 frames in interpreter");
+ equal(data[i].values[1], 0.2, "first window has 2 frames in baseline");
+ equal(data[i].values[2], 0.2, "first window has 2 frames in ion");
+ }
+ // Start on 11, since i===10 is where the values change, and the new value (0,0,0)
+ // is removed in `filteredData`
+ for (let i = 11; i < 20; i++) {
+ equal(data[i].delta, startTime + TIME_OFFSET + (TIME_PER_WINDOW * i),
+ "second window has correct x");
+ equal(data[i].values[0], 0, "second window observed no optimizations");
+ equal(data[i].values[1], 0, "second window observed no optimizations");
+ equal(data[i].values[2], 0, "second window observed no optimizations");
+ }
+ // Start on 21, since i===20 is where the values change, and the new value (0.3,0,0)
+ // is removed in `filteredData`
+ for (let i = 21; i < 30; i++) {
+ equal(data[i].delta, startTime + TIME_OFFSET + (TIME_PER_WINDOW * i),
+ "third window has correct x");
+ equal(data[i].values[0], 0.3, "third window has 3 frames in interpreter");
+ equal(data[i].values[1], 0, "third window has 0 frames in baseline");
+ equal(data[i].values[2], 0, "third window has 0 frames in ion");
+ }
+});
+
+var gUniqueStacks = new RecordingUtils.UniqueStacks();
+
+function uniqStr(s) {
+ return gUniqueStacks.getOrAddStringIndex(s);
+}
+
+const TIER_PATTERNS = [
+ // 0-99
+ ["X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0"],
+ // 100-199
+ ["X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0"],
+ // 200-299
+ ["X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0"],
+ // 300-399
+ ["X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0"],
+ // 400-499
+ ["X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0"],
+
+ // 500-599
+ // Test current frames in all opts
+ ["A", "A", "A", "A", "X_1", "X_2", "X_1", "X_2", "X_0", "X_0"],
+
+ // 600-699
+ // Nothing for current frame
+ ["A", "B", "A", "B", "A", "B", "A", "B", "A", "B"],
+
+ // 700-799
+ // A few frames where the frame is not the leaf node
+ ["X_2 -> Y", "X_2 -> Y", "X_2 -> Y", "X_0", "X_0", "X_0", "A", "A", "A", "A"],
+
+ // 800-899
+ ["X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0"],
+ // 900-999
+ ["X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0"],
+];
+
+function createSample(i, frames) {
+ let sample = {};
+ sample.time = i * TIME_PER_SAMPLE;
+ sample.frames = [{ location: "(root)" }];
+ if (i === 0) {
+ return sample;
+ }
+ if (frames) {
+ frames.split(" -> ").forEach(frame => sample.frames.push({ location: frame }));
+ }
+ return sample;
+}
+
+var SAMPLES = (function () {
+ let samples = [];
+
+ for (let i = 0; i < SAMPLE_COUNT;) {
+ let pattern = TIER_PATTERNS[Math.floor(i / 100)];
+ for (let j = 0; j < pattern.length; j++) {
+ samples.push(createSample(i + j, pattern[j]));
+ }
+ i += 10;
+ }
+
+ return samples;
+})();
+
+var gThread = RecordingUtils.deflateThread({ samples: SAMPLES, markers: [] },
+ gUniqueStacks);
+
+var gRawSite1 = {
+ line: 12,
+ column: 2,
+ types: [{
+ mirType: uniqStr("Object"),
+ site: uniqStr("B (http://foo/bar:10)"),
+ typeset: [{
+ keyedBy: uniqStr("constructor"),
+ name: uniqStr("Foo"),
+ location: uniqStr("B (http://foo/bar:10)")
+ }, {
+ keyedBy: uniqStr("primitive"),
+ location: uniqStr("self-hosted")
+ }]
+ }],
+ attempts: {
+ schema: {
+ outcome: 0,
+ strategy: 1
+ },
+ data: [
+ [uniqStr("Failure1"), uniqStr("SomeGetter1")],
+ [uniqStr("Failure2"), uniqStr("SomeGetter2")],
+ [uniqStr("Inlined"), uniqStr("SomeGetter3")]
+ ]
+ }
+};
+
+function serialize(x) {
+ return JSON.parse(JSON.stringify(x));
+}
+
+gThread.frameTable.data.forEach((frame) => {
+ const LOCATION_SLOT = gThread.frameTable.schema.location;
+ const OPTIMIZATIONS_SLOT = gThread.frameTable.schema.optimizations;
+ const IMPLEMENTATION_SLOT = gThread.frameTable.schema.implementation;
+
+ let l = gThread.stringTable[frame[LOCATION_SLOT]];
+ switch (l) {
+ // Rename some of the location sites so we can register different
+ // frames with different opt sites
+ case "X_0":
+ frame[LOCATION_SLOT] = uniqStr("X");
+ frame[OPTIMIZATIONS_SLOT] = serialize(gRawSite1);
+ frame[IMPLEMENTATION_SLOT] = null;
+ break;
+ case "X_1":
+ frame[LOCATION_SLOT] = uniqStr("X");
+ frame[OPTIMIZATIONS_SLOT] = serialize(gRawSite1);
+ frame[IMPLEMENTATION_SLOT] = uniqStr("baseline");
+ break;
+ case "X_2":
+ frame[LOCATION_SLOT] = uniqStr("X");
+ frame[OPTIMIZATIONS_SLOT] = serialize(gRawSite1);
+ frame[IMPLEMENTATION_SLOT] = uniqStr("ion");
+ break;
+ }
+});
diff --git a/devtools/client/performance/test/unit/test_jit-model-01.js b/devtools/client/performance/test/unit/test_jit-model-01.js
new file mode 100644
index 000000000..da50f293c
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_jit-model-01.js
@@ -0,0 +1,120 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that JITOptimizations track optimization sites and create
+ * an OptimizationSiteProfile when adding optimization sites, like from the
+ * FrameNode, and the returning of that data is as expected.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function test() {
+ let { JITOptimizations } = require("devtools/client/performance/modules/logic/jit");
+
+ let rawSites = [];
+ rawSites.push(gRawSite2);
+ rawSites.push(gRawSite2);
+ rawSites.push(gRawSite1);
+ rawSites.push(gRawSite1);
+ rawSites.push(gRawSite2);
+ rawSites.push(gRawSite3);
+
+ let jit = new JITOptimizations(rawSites, gStringTable.stringTable);
+ let sites = jit.optimizationSites;
+
+ let [first, second, third] = sites;
+
+ equal(first.id, 0, "site id is array index");
+ equal(first.samples, 3, "first OptimizationSiteProfile has correct sample count");
+ equal(first.data.line, 34, "includes OptimizationSite as reference under `data`");
+ equal(second.id, 1, "site id is array index");
+ equal(second.samples, 2, "second OptimizationSiteProfile has correct sample count");
+ equal(second.data.line, 12, "includes OptimizationSite as reference under `data`");
+ equal(third.id, 2, "site id is array index");
+ equal(third.samples, 1, "third OptimizationSiteProfile has correct sample count");
+ equal(third.data.line, 78, "includes OptimizationSite as reference under `data`");
+});
+
+var gStringTable = new RecordingUtils.UniqueStrings();
+
+function uniqStr(s) {
+ return gStringTable.getOrAddStringIndex(s);
+}
+
+var gRawSite1 = {
+ line: 12,
+ column: 2,
+ types: [{
+ mirType: uniqStr("Object"),
+ site: uniqStr("A (http://foo/bar/bar:12)"),
+ typeset: [{
+ keyedBy: uniqStr("constructor"),
+ name: uniqStr("Foo"),
+ location: uniqStr("A (http://foo/bar/baz:12)")
+ }, {
+ keyedBy: uniqStr("primitive"),
+ location: uniqStr("self-hosted")
+ }]
+ }],
+ attempts: {
+ schema: {
+ outcome: 0,
+ strategy: 1
+ },
+ data: [
+ [uniqStr("Failure1"), uniqStr("SomeGetter1")],
+ [uniqStr("Failure2"), uniqStr("SomeGetter2")],
+ [uniqStr("Inlined"), uniqStr("SomeGetter3")]
+ ]
+ }
+};
+
+var gRawSite2 = {
+ line: 34,
+ types: [{
+ mirType: uniqStr("Int32"),
+ site: uniqStr("Receiver")
+ }],
+ attempts: {
+ schema: {
+ outcome: 0,
+ strategy: 1
+ },
+ data: [
+ [uniqStr("Failure1"), uniqStr("SomeGetter1")],
+ [uniqStr("Failure2"), uniqStr("SomeGetter2")],
+ [uniqStr("Failure3"), uniqStr("SomeGetter3")]
+ ]
+ }
+};
+
+var gRawSite3 = {
+ line: 78,
+ types: [{
+ mirType: uniqStr("Object"),
+ site: uniqStr("A (http://foo/bar/bar:12)"),
+ typeset: [{
+ keyedBy: uniqStr("constructor"),
+ name: uniqStr("Foo"),
+ location: uniqStr("A (http://foo/bar/baz:12)")
+ }, {
+ keyedBy: uniqStr("primitive"),
+ location: uniqStr("self-hosted")
+ }]
+ }],
+ attempts: {
+ schema: {
+ outcome: 0,
+ strategy: 1
+ },
+ data: [
+ [uniqStr("Failure1"), uniqStr("SomeGetter1")],
+ [uniqStr("Failure2"), uniqStr("SomeGetter2")],
+ [uniqStr("GenericSuccess"), uniqStr("SomeGetter3")]
+ ]
+ }
+};
diff --git a/devtools/client/performance/test/unit/test_jit-model-02.js b/devtools/client/performance/test/unit/test_jit-model-02.js
new file mode 100644
index 000000000..19373e399
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_jit-model-02.js
@@ -0,0 +1,149 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that JITOptimizations create OptimizationSites, and the underlying
+ * hasSuccessfulOutcome/isSuccessfulOutcome work as intended.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function test() {
+ let {
+ JITOptimizations, hasSuccessfulOutcome, isSuccessfulOutcome, SUCCESSFUL_OUTCOMES
+ } = require("devtools/client/performance/modules/logic/jit");
+
+ let rawSites = [];
+ rawSites.push(gRawSite2);
+ rawSites.push(gRawSite2);
+ rawSites.push(gRawSite1);
+ rawSites.push(gRawSite1);
+ rawSites.push(gRawSite2);
+ rawSites.push(gRawSite3);
+
+ let jit = new JITOptimizations(rawSites, gStringTable.stringTable);
+ let sites = jit.optimizationSites;
+
+ let [first, second, third] = sites;
+
+ /* hasSuccessfulOutcome */
+ equal(hasSuccessfulOutcome(first), false,
+ "hasSuccessfulOutcome() returns expected (1)");
+ equal(hasSuccessfulOutcome(second), true,
+ "hasSuccessfulOutcome() returns expected (2)");
+ equal(hasSuccessfulOutcome(third), true,
+ "hasSuccessfulOutcome() returns expected (3)");
+
+ /* .data.attempts */
+ equal(first.data.attempts.length, 2,
+ "optSite.data.attempts has the correct amount of attempts (1)");
+ equal(second.data.attempts.length, 5,
+ "optSite.data.attempts has the correct amount of attempts (2)");
+ equal(third.data.attempts.length, 3,
+ "optSite.data.attempts has the correct amount of attempts (3)");
+
+ /* .data.types */
+ equal(first.data.types.length, 1,
+ "optSite.data.types has the correct amount of IonTypes (1)");
+ equal(second.data.types.length, 2,
+ "optSite.data.types has the correct amount of IonTypes (2)");
+ equal(third.data.types.length, 1,
+ "optSite.data.types has the correct amount of IonTypes (3)");
+
+ /* isSuccessfulOutcome */
+ ok(SUCCESSFUL_OUTCOMES.length, "Have some successful outcomes in SUCCESSFUL_OUTCOMES");
+ SUCCESSFUL_OUTCOMES.forEach(outcome =>
+ ok(isSuccessfulOutcome(outcome),
+ `${outcome} considered a successful outcome via isSuccessfulOutcome()`));
+});
+
+var gStringTable = new RecordingUtils.UniqueStrings();
+
+function uniqStr(s) {
+ return gStringTable.getOrAddStringIndex(s);
+}
+
+var gRawSite1 = {
+ line: 12,
+ column: 2,
+ types: [{
+ mirType: uniqStr("Object"),
+ site: uniqStr("A (http://foo/bar/bar:12)"),
+ typeset: [{
+ keyedBy: uniqStr("constructor"),
+ name: uniqStr("Foo"),
+ location: uniqStr("A (http://foo/bar/baz:12)")
+ }, {
+ keyedBy: uniqStr("constructor"),
+ location: uniqStr("A (http://foo/bar/baz:12)")
+ }]
+ }, {
+ mirType: uniqStr("Int32"),
+ site: uniqStr("A (http://foo/bar/bar:12)"),
+ typeset: [{
+ keyedBy: uniqStr("primitive"),
+ location: uniqStr("self-hosted")
+ }]
+ }],
+ attempts: {
+ schema: {
+ outcome: 0,
+ strategy: 1
+ },
+ data: [
+ [uniqStr("Failure1"), uniqStr("SomeGetter1")],
+ [uniqStr("Failure1"), uniqStr("SomeGetter1")],
+ [uniqStr("Failure1"), uniqStr("SomeGetter1")],
+ [uniqStr("Failure2"), uniqStr("SomeGetter2")],
+ [uniqStr("Inlined"), uniqStr("SomeGetter3")]
+ ]
+ }
+};
+
+var gRawSite2 = {
+ line: 34,
+ types: [{
+ mirType: uniqStr("Int32"),
+ site: uniqStr("Receiver")
+ }],
+ attempts: {
+ schema: {
+ outcome: 0,
+ strategy: 1
+ },
+ data: [
+ [uniqStr("Failure1"), uniqStr("SomeGetter1")],
+ [uniqStr("Failure2"), uniqStr("SomeGetter2")]
+ ]
+ }
+};
+
+var gRawSite3 = {
+ line: 78,
+ types: [{
+ mirType: uniqStr("Object"),
+ site: uniqStr("A (http://foo/bar/bar:12)"),
+ typeset: [{
+ keyedBy: uniqStr("constructor"),
+ name: uniqStr("Foo"),
+ location: uniqStr("A (http://foo/bar/baz:12)")
+ }, {
+ keyedBy: uniqStr("primitive"),
+ location: uniqStr("self-hosted")
+ }]
+ }],
+ attempts: {
+ schema: {
+ outcome: 0,
+ strategy: 1
+ },
+ data: [
+ [uniqStr("Failure1"), uniqStr("SomeGetter1")],
+ [uniqStr("Failure2"), uniqStr("SomeGetter2")],
+ [uniqStr("GenericSuccess"), uniqStr("SomeGetter3")]
+ ]
+ }
+};
diff --git a/devtools/client/performance/test/unit/test_marker-blueprint.js b/devtools/client/performance/test/unit/test_marker-blueprint.js
new file mode 100644
index 000000000..b3db47c0f
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_marker-blueprint.js
@@ -0,0 +1,29 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+/**
+ * Tests if the timeline blueprint has a correct structure.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function () {
+ let { TIMELINE_BLUEPRINT } = require("devtools/client/performance/modules/markers");
+
+ ok(TIMELINE_BLUEPRINT,
+ "A timeline blueprint should be available.");
+
+ ok(Object.keys(TIMELINE_BLUEPRINT).length,
+ "The timeline blueprint has at least one entry.");
+
+ for (let value of Object.values(TIMELINE_BLUEPRINT)) {
+ ok("group" in value,
+ "Each entry in the timeline blueprint contains a `group` key.");
+ ok("colorName" in value,
+ "Each entry in the timeline blueprint contains a `colorName` key.");
+ ok("label" in value,
+ "Each entry in the timeline blueprint contains a `label` key.");
+ }
+});
diff --git a/devtools/client/performance/test/unit/test_marker-utils.js b/devtools/client/performance/test/unit/test_marker-utils.js
new file mode 100644
index 000000000..6fc06efbe
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_marker-utils.js
@@ -0,0 +1,115 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests the marker utils methods.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function () {
+ let { TIMELINE_BLUEPRINT } = require("devtools/client/performance/modules/markers");
+ let { PREFS } = require("devtools/client/performance/modules/global");
+ let { MarkerBlueprintUtils } = require("devtools/client/performance/modules/marker-blueprint-utils");
+
+ PREFS.registerObserver();
+
+ Services.prefs.setBoolPref(PLATFORM_DATA_PREF, false);
+
+ equal(MarkerBlueprintUtils.getMarkerLabel(
+ { name: "DOMEvent" }), "DOM Event",
+ "getMarkerLabel() returns a simple label");
+ equal(MarkerBlueprintUtils.getMarkerLabel(
+ { name: "Javascript", causeName: "setTimeout handler" }), "setTimeout",
+ "getMarkerLabel() returns a label defined via function");
+ equal(MarkerBlueprintUtils.getMarkerLabel(
+ { name: "GarbageCollection", causeName: "ALLOC_TRIGGER" }), "Incremental GC",
+ "getMarkerLabel() returns a label for a function that is generalizable");
+
+ ok(MarkerBlueprintUtils.getMarkerFields({ name: "Paint" }).length === 0,
+ "getMarkerFields() returns an empty array when no fields defined");
+
+ let fields = MarkerBlueprintUtils.getMarkerFields(
+ { name: "ConsoleTime", causeName: "snowstorm" });
+ equal(fields[0].label, "Timer Name:",
+ "getMarkerFields() returns an array with proper label");
+ equal(fields[0].value, "snowstorm",
+ "getMarkerFields() returns an array with proper value");
+
+ fields = MarkerBlueprintUtils.getMarkerFields({ name: "DOMEvent", type: "mouseclick" });
+ equal(fields.length, 1,
+ "getMarkerFields() ignores fields that are not found on marker");
+ equal(fields[0].label, "Event Type:",
+ "getMarkerFields() returns an array with proper label");
+ equal(fields[0].value, "mouseclick",
+ "getMarkerFields() returns an array with proper value");
+
+ fields = MarkerBlueprintUtils.getMarkerFields(
+ { name: "DOMEvent", eventPhase: Ci.nsIDOMEvent.AT_TARGET, type: "mouseclick" });
+ equal(fields.length, 2,
+ "getMarkerFields() returns multiple fields when using a fields function");
+ equal(fields[0].label, "Event Type:",
+ "getMarkerFields() correctly returns fields via function (1)");
+ equal(fields[0].value, "mouseclick",
+ "getMarkerFields() correctly returns fields via function (2)");
+ equal(fields[1].label, "Phase:",
+ "getMarkerFields() correctly returns fields via function (3)");
+ equal(fields[1].value, "Target",
+ "getMarkerFields() correctly returns fields via function (4)");
+
+ fields = MarkerBlueprintUtils.getMarkerFields(
+ { name: "GarbageCollection", causeName: "ALLOC_TRIGGER" });
+ equal(fields[0].value, "Too Many Allocations", "Uses L10N for GC reasons");
+
+ fields = MarkerBlueprintUtils.getMarkerFields(
+ { name: "GarbageCollection", causeName: "NOT_A_GC_REASON" });
+ equal(fields[0].value, "NOT_A_GC_REASON",
+ "Defaults to enum for GC reasons when not L10N'd");
+
+ equal(MarkerBlueprintUtils.getMarkerFields(
+ { name: "Javascript", causeName: "Some Platform Field" })[0].value, "(Gecko)",
+ "Correctly obfuscates JS markers when platform data is off.");
+ Services.prefs.setBoolPref(PLATFORM_DATA_PREF, true);
+ equal(MarkerBlueprintUtils.getMarkerFields(
+ { name: "Javascript", causeName: "Some Platform Field" })[0].value,
+ "Some Platform Field",
+ "Correctly deobfuscates JS markers when platform data is on.");
+
+ equal(MarkerBlueprintUtils.getMarkerGenericName("Javascript"), "Function Call",
+ "getMarkerGenericName() returns correct string when defined via function");
+ equal(MarkerBlueprintUtils.getMarkerGenericName("GarbageCollection"),
+ "Garbage Collection",
+ "getMarkerGenericName() returns correct string when defined via function");
+ equal(MarkerBlueprintUtils.getMarkerGenericName("Reflow"), "Layout",
+ "getMarkerGenericName() returns correct string when defined via string");
+
+ TIMELINE_BLUEPRINT.fakemarker = { group: 0 };
+ try {
+ MarkerBlueprintUtils.getMarkerGenericName("fakemarker");
+ ok(false, "getMarkerGenericName() should throw when no label on blueprint.");
+ } catch (e) {
+ ok(true, "getMarkerGenericName() should throw when no label on blueprint.");
+ }
+
+ TIMELINE_BLUEPRINT.fakemarker = { group: 0, label: () => void 0 };
+ try {
+ MarkerBlueprintUtils.getMarkerGenericName("fakemarker");
+ ok(false,
+ "getMarkerGenericName() should throw when label function returnd undefined.");
+ } catch (e) {
+ ok(true,
+ "getMarkerGenericName() should throw when label function returnd undefined.");
+ }
+
+ delete TIMELINE_BLUEPRINT.fakemarker;
+
+ equal(MarkerBlueprintUtils.getBlueprintFor({ name: "Reflow" }).label, "Layout",
+ "getBlueprintFor() should return marker def for passed in marker.");
+ equal(MarkerBlueprintUtils.getBlueprintFor({ name: "Not sure!" }).label(), "Unknown",
+ "getBlueprintFor() should return a default marker def if the marker is undefined.");
+
+ PREFS.unregisterObserver();
+});
diff --git a/devtools/client/performance/test/unit/test_perf-utils-allocations-to-samples.js b/devtools/client/performance/test/unit/test_perf-utils-allocations-to-samples.js
new file mode 100644
index 000000000..2b114ab82
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_perf-utils-allocations-to-samples.js
@@ -0,0 +1,96 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if allocations data received from the performance actor is properly
+ * converted to something that follows the same structure as the samples data
+ * received from the profiler.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function () {
+ const { getProfileThreadFromAllocations } = require("devtools/shared/performance/recording-utils");
+ let output = getProfileThreadFromAllocations(TEST_DATA);
+ equal(output.toSource(), EXPECTED_OUTPUT.toSource(), "The output is correct.");
+});
+
+var TEST_DATA = {
+ sites: [0, 0, 1, 2, 3],
+ timestamps: [50, 100, 150, 200, 250],
+ sizes: [0, 0, 100, 200, 300],
+ frames: [
+ null, {
+ source: "A",
+ line: 1,
+ column: 2,
+ functionDisplayName: "x",
+ parent: 0
+ }, {
+ source: "B",
+ line: 3,
+ column: 4,
+ functionDisplayName: "y",
+ parent: 1
+ }, {
+ source: "C",
+ line: 5,
+ column: 6,
+ functionDisplayName: null,
+ parent: 2
+ }
+ ]
+};
+
+/* eslint-disable no-inline-comments */
+var EXPECTED_OUTPUT = {
+ name: "allocations",
+ samples: {
+ "schema": {
+ "stack": 0,
+ "time": 1,
+ "size": 2,
+ },
+ data: [
+ [ 1, 150, 100 ],
+ [ 2, 200, 200 ],
+ [ 3, 250, 300 ]
+ ]
+ },
+ stackTable: {
+ "schema": {
+ "prefix": 0,
+ "frame": 1
+ },
+ "data": [
+ null,
+ [ null, 1 ], // x (A:1:2)
+ [ 1, 2 ], // x (A:1:2) > y (B:3:4)
+ [ 2, 3 ] // x (A:1:2) > y (B:3:4) > C:5:6
+ ]
+ },
+ frameTable: {
+ "schema": {
+ "location": 0,
+ "implementation": 1,
+ "optimizations": 2,
+ "line": 3,
+ "category": 4
+ },
+ data: [
+ null,
+ [ 0 ],
+ [ 1 ],
+ [ 2 ]
+ ]
+ },
+ "stringTable": [
+ "x (A:1:2)",
+ "y (B:3:4)",
+ "C:5:6"
+ ],
+};
+/* eslint-enable no-inline-comments */
diff --git a/devtools/client/performance/test/unit/test_profiler-categories.js b/devtools/client/performance/test/unit/test_profiler-categories.js
new file mode 100644
index 000000000..7ba288167
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_profiler-categories.js
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if the profiler categories are mapped correctly.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function () {
+ let { CATEGORIES, CATEGORY_MAPPINGS } = require("devtools/client/performance/modules/categories");
+ let { L10N } = require("devtools/client/performance/modules/global");
+ let count = CATEGORIES.length;
+
+ ok(count,
+ "Should have a non-empty list of categories available.");
+
+ ok(CATEGORIES.some(e => e.color),
+ "All categories have an associated color.");
+
+ ok(CATEGORIES.every(e => e.label),
+ "All categories have an associated label.");
+
+ ok(CATEGORIES.every(e => e.label === L10N.getStr("category." + e.abbrev)),
+ "All categories have a correctly localized label.");
+
+ ok(Object.keys(CATEGORY_MAPPINGS).every(e => (Number(e) >= 9000 && Number(e) <= 9999) ||
+ Number.isInteger(Math.log2(e))),
+ "All bitmask mappings keys are powers of 2, or between 9000-9999 for special " +
+ "categories.");
+
+ ok(Object.keys(CATEGORY_MAPPINGS).every(e => CATEGORIES.indexOf(CATEGORY_MAPPINGS[e])
+ !== -1),
+ "All bitmask mappings point to a category.");
+});
diff --git a/devtools/client/performance/test/unit/test_tree-model-01.js b/devtools/client/performance/test/unit/test_tree-model-01.js
new file mode 100644
index 000000000..cac397795
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_tree-model-01.js
@@ -0,0 +1,160 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if a call tree model can be correctly computed from a samples array.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function test() {
+ const { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
+
+ // Create a root node from a given samples array.
+
+ let threadNode = new ThreadNode(gThread, { startTime: 0, endTime: 20 });
+ let root = getFrameNodePath(threadNode, "(root)");
+
+ // Test the root node.
+
+ equal(threadNode.getInfo().nodeType, "Thread",
+ "The correct node type was retrieved for the root node.");
+
+ equal(threadNode.duration, 20,
+ "The correct duration was calculated for the ThreadNode.");
+ equal(root.getInfo().functionName, "(root)",
+ "The correct function name was retrieved for the root node.");
+ equal(root.getInfo().categoryData.abbrev, "other",
+ "The correct empty category data was retrieved for the root node.");
+
+ equal(root.calls.length, 1,
+ "The correct number of child calls were calculated for the root node.");
+ ok(getFrameNodePath(root, "A"),
+ "The root node's only child call is correct.");
+
+ // Test all the descendant nodes.
+
+ equal(getFrameNodePath(root, "A").calls.length, 2,
+ "The correct number of child calls were calculated for the 'A' node.");
+ ok(getFrameNodePath(root, "A > B"),
+ "The 'A' node has a 'B' child call.");
+ ok(getFrameNodePath(root, "A > E"),
+ "The 'A' node has a 'E' child call.");
+
+ equal(getFrameNodePath(root, "A > B").calls.length, 2,
+ "The correct number of child calls were calculated for the 'A > B' node.");
+ ok(getFrameNodePath(root, "A > B > C"),
+ "The 'A > B' node has a 'C' child call.");
+ ok(getFrameNodePath(root, "A > B > D"),
+ "The 'A > B' node has a 'D' child call.");
+
+ equal(getFrameNodePath(root, "A > E").calls.length, 1,
+ "The correct number of child calls were calculated for the 'A > E' node.");
+ ok(getFrameNodePath(root, "A > E > F"),
+ "The 'A > E' node has a 'F' child call.");
+
+ equal(getFrameNodePath(root, "A > B > C").calls.length, 1,
+ "The correct number of child calls were calculated for the 'A > B > C' node.");
+ ok(getFrameNodePath(root, "A > B > C > D"),
+ "The 'A > B > C' node has a 'D' child call.");
+
+ equal(getFrameNodePath(root, "A > B > C > D").calls.length, 1,
+ "The correct number of child calls were calculated for the 'A > B > C > D' node.");
+ ok(getFrameNodePath(root, "A > B > C > D > E"),
+ "The 'A > B > C > D' node has a 'E' child call.");
+
+ equal(getFrameNodePath(root, "A > B > C > D > E").calls.length, 1,
+ "The correct number of child calls were calculated for the 'A > B > C > D > E' " +
+ "node.");
+ ok(getFrameNodePath(root, "A > B > C > D > E > F"),
+ "The 'A > B > C > D > E' node has a 'F' child call.");
+
+ equal(getFrameNodePath(root, "A > B > C > D > E > F").calls.length, 1,
+ "The correct number of child calls were calculated for the 'A > B > C > D > E > F' " +
+ "node.");
+ ok(getFrameNodePath(root, "A > B > C > D > E > F > G"),
+ "The 'A > B > C > D > E > F' node has a 'G' child call.");
+
+ equal(getFrameNodePath(root, "A > B > C > D > E > F > G").calls.length, 0,
+ "The correct number of child calls were calculated for the " +
+ "'A > B > C > D > E > F > G' node.");
+ equal(getFrameNodePath(root, "A > B > D").calls.length, 0,
+ "The correct number of child calls were calculated for the 'A > B > D' node.");
+ equal(getFrameNodePath(root, "A > E > F").calls.length, 0,
+ "The correct number of child calls were calculated for the 'A > E > F' node.");
+
+ // Check the location, sample times, and samples of the root.
+
+ equal(getFrameNodePath(root, "A").location, "A",
+ "The 'A' node has the correct location.");
+ equal(getFrameNodePath(root, "A").youngestFrameSamples, 0,
+ "The 'A' has correct `youngestFrameSamples`");
+ equal(getFrameNodePath(root, "A").samples, 4,
+ "The 'A' has correct `samples`");
+
+ // A frame that is both a leaf and caught in another stack
+ equal(getFrameNodePath(root, "A > B > C").youngestFrameSamples, 1,
+ "The 'A > B > C' has correct `youngestFrameSamples`");
+ equal(getFrameNodePath(root, "A > B > C").samples, 2,
+ "The 'A > B > C' has correct `samples`");
+
+ // ...and the rightmost leaf.
+
+ equal(getFrameNodePath(root, "A > E > F").location, "F",
+ "The 'A > E > F' node has the correct location.");
+ equal(getFrameNodePath(root, "A > E > F").samples, 1,
+ "The 'A > E > F' node has the correct number of samples.");
+ equal(getFrameNodePath(root, "A > E > F").youngestFrameSamples, 1,
+ "The 'A > E > F' node has the correct number of youngestFrameSamples.");
+
+ // ...and the leftmost leaf.
+
+ equal(getFrameNodePath(root, "A > B > C > D > E > F > G").location, "G",
+ "The 'A > B > C > D > E > F > G' node has the correct location.");
+ equal(getFrameNodePath(root, "A > B > C > D > E > F > G").samples, 1,
+ "The 'A > B > C > D > E > F > G' node has the correct number of samples.");
+ equal(getFrameNodePath(root, "A > B > C > D > E > F > G").youngestFrameSamples, 1,
+ "The 'A > B > C > D > E > F > G' node has the correct number of " +
+ "youngestFrameSamples.");
+});
+
+var gThread = synthesizeProfileForTest([{
+ time: 5,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "C" }
+ ]
+}, {
+ time: 5 + 6,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "D" }
+ ]
+}, {
+ time: 5 + 6 + 7,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "E" },
+ { location: "F" }
+ ]
+}, {
+ time: 20,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "C" },
+ { location: "D" },
+ { location: "E" },
+ { location: "F" },
+ { location: "G" }
+ ]
+}]);
diff --git a/devtools/client/performance/test/unit/test_tree-model-02.js b/devtools/client/performance/test/unit/test_tree-model-02.js
new file mode 100644
index 000000000..2cbff11be
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_tree-model-02.js
@@ -0,0 +1,62 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if a call tree model ignores samples with no timing information.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function test() {
+ let { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
+
+ // Create a root node from a given samples array.
+
+ let thread = new ThreadNode(gThread, { startTime: 0, endTime: 10 });
+ let root = getFrameNodePath(thread, "(root)");
+
+ // Test the ThreadNode, only node with a duration.
+ equal(thread.duration, 10,
+ "The correct duration was calculated for the ThreadNode.");
+
+ equal(root.calls.length, 1,
+ "The correct number of child calls were calculated for the root node.");
+ ok(getFrameNodePath(root, "A"),
+ "The root node's only child call is correct.");
+
+ // Test all the descendant nodes.
+
+ equal(getFrameNodePath(root, "A").calls.length, 1,
+ "The correct number of child calls were calculated for the 'A' node.");
+ ok(getFrameNodePath(root, "A > B"),
+ "The 'A' node's only child call is correct.");
+
+ equal(getFrameNodePath(root, "A > B").calls.length, 1,
+ "The correct number of child calls were calculated for the 'A > B' node.");
+ ok(getFrameNodePath(root, "A > B > C"),
+ "The 'A > B' node's only child call is correct.");
+
+ equal(getFrameNodePath(root, "A > B > C").calls.length, 0,
+ "The correct number of child calls were calculated for the 'A > B > C' node.");
+});
+
+var gThread = synthesizeProfileForTest([{
+ time: 5,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "C" }
+ ]
+}, {
+ time: null,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "D" }
+ ]
+}]);
diff --git a/devtools/client/performance/test/unit/test_tree-model-03.js b/devtools/client/performance/test/unit/test_tree-model-03.js
new file mode 100644
index 000000000..dad90710a
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_tree-model-03.js
@@ -0,0 +1,95 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if a call tree model can be correctly computed from a samples array,
+ * while at the same time filtering by duration.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function test() {
+ let { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
+
+ // Create a root node from a given samples array, filtering by time.
+ //
+ // Filtering from 5 to 18 includes the 2nd and 3rd samples. The 2nd sample
+ // starts exactly on 5 and ends at 11. The 3rd sample starts at 11 and ends
+ // exactly at 18.
+ let startTime = 5;
+ let endTime = 18;
+ let thread = new ThreadNode(gThread, { startTime, endTime });
+ let root = getFrameNodePath(thread, "(root)");
+
+ // Test the root node.
+
+ equal(thread.duration, endTime - startTime,
+ "The correct duration was calculated for the ThreadNode.");
+
+ equal(root.calls.length, 1,
+ "The correct number of child calls were calculated for the root node.");
+ ok(getFrameNodePath(root, "A"),
+ "The root node's only child call is correct.");
+
+ // Test all the descendant nodes.
+
+ equal(getFrameNodePath(root, "A").calls.length, 2,
+ "The correct number of child calls were calculated for the 'A' node.");
+ ok(getFrameNodePath(root, "A > B"),
+ "The 'A' node has a 'B' child call.");
+ ok(getFrameNodePath(root, "A > E"),
+ "The 'A' node has a 'E' child call.");
+
+ equal(getFrameNodePath(root, "A > B").calls.length, 1,
+ "The correct number of child calls were calculated for the 'A > B' node.");
+ ok(getFrameNodePath(root, "A > B > D"),
+ "The 'A > B' node's only child call is correct.");
+
+ equal(getFrameNodePath(root, "A > E").calls.length, 1,
+ "The correct number of child calls were calculated for the 'A > E' node.");
+ ok(getFrameNodePath(root, "A > E > F"),
+ "The 'A > E' node's only child call is correct.");
+
+ equal(getFrameNodePath(root, "A > B > D").calls.length, 0,
+ "The correct number of child calls were calculated for the 'A > B > D' node.");
+ equal(getFrameNodePath(root, "A > E > F").calls.length, 0,
+ "The correct number of child calls were calculated for the 'A > E > F' node.");
+});
+
+var gThread = synthesizeProfileForTest([{
+ time: 5,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "C" }
+ ]
+}, {
+ time: 5 + 6,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "D" }
+ ]
+}, {
+ time: 5 + 6 + 7,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "E" },
+ { location: "F" }
+ ]
+}, {
+ time: 5 + 6 + 7 + 8,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "C" },
+ { location: "D" }
+ ]
+}]);
diff --git a/devtools/client/performance/test/unit/test_tree-model-04.js b/devtools/client/performance/test/unit/test_tree-model-04.js
new file mode 100644
index 000000000..6bf69111e
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_tree-model-04.js
@@ -0,0 +1,91 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if a call tree model can be correctly computed from a samples array,
+ * while at the same time filtering by duration and content-only frames.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function test() {
+ let { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
+
+ // Create a root node from a given samples array, filtering by time.
+
+ let startTime = 5;
+ let endTime = 18;
+ let thread = new ThreadNode(gThread, { startTime, endTime, contentOnly: true });
+ let root = getFrameNodePath(thread, "(root)");
+
+ // Test the ThreadNode, only node which should have duration
+ equal(thread.duration, endTime - startTime,
+ "The correct duration was calculated for the root ThreadNode.");
+
+ equal(root.calls.length, 2,
+ "The correct number of child calls were calculated for the root node.");
+ ok(getFrameNodePath(root, "http://D"),
+ "The root has a 'http://D' child call.");
+ ok(getFrameNodePath(root, "http://A"),
+ "The root has a 'http://A' child call.");
+
+ // Test all the descendant nodes.
+
+ equal(getFrameNodePath(root, "http://A").calls.length, 1,
+ "The correct number of child calls were calculated for the 'http://A' node.");
+ ok(getFrameNodePath(root, "http://A > https://E"),
+ "The 'http://A' node's only child call is correct.");
+
+ equal(getFrameNodePath(root, "http://A > https://E").calls.length, 1,
+ "The correct number of child calls were calculated for the 'http://A > http://E' node.");
+ ok(getFrameNodePath(root, "http://A > https://E > file://F"),
+ "The 'http://A > https://E' node's only child call is correct.");
+
+ equal(getFrameNodePath(root, "http://A > https://E > file://F").calls.length, 1,
+ "The correct number of child calls were calculated for the 'http://A > https://E >> file://F' node.");
+ ok(getFrameNodePath(root, "http://A > https://E > file://F > app://H"),
+ "The 'http://A > https://E >> file://F' node's only child call is correct.");
+
+ equal(getFrameNodePath(root, "http://D").calls.length, 0,
+ "The correct number of child calls were calculated for the 'http://D' node.");
+});
+
+var gThread = synthesizeProfileForTest([{
+ time: 5,
+ frames: [
+ { location: "(root)" },
+ { location: "http://A" },
+ { location: "http://B" },
+ { location: "http://C" }
+ ]
+}, {
+ time: 5 + 6,
+ frames: [
+ { location: "(root)" },
+ { location: "chrome://A" },
+ { location: "resource://B" },
+ { location: "jar:file://G" },
+ { location: "http://D" }
+ ]
+}, {
+ time: 5 + 6 + 7,
+ frames: [
+ { location: "(root)" },
+ { location: "http://A" },
+ { location: "https://E" },
+ { location: "file://F" },
+ { location: "app://H" },
+ ]
+}, {
+ time: 5 + 6 + 7 + 8,
+ frames: [
+ { location: "(root)" },
+ { location: "http://A" },
+ { location: "http://B" },
+ { location: "http://C" },
+ { location: "http://D" }
+ ]
+}]);
diff --git a/devtools/client/performance/test/unit/test_tree-model-05.js b/devtools/client/performance/test/unit/test_tree-model-05.js
new file mode 100644
index 000000000..3b9470798
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_tree-model-05.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if an inverted call tree model can be correctly computed from a samples
+ * array.
+ */
+
+var time = 1;
+
+var gThread = synthesizeProfileForTest([{
+ time: time++,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "C" }
+ ]
+}, {
+ time: time++,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "D" },
+ { location: "C" }
+ ]
+}, {
+ time: time++,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "E" },
+ { location: "C" }
+ ],
+}, {
+ time: time++,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "F" }
+ ]
+}]);
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function test() {
+ let { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
+
+ let root = new ThreadNode(gThread, { invertTree: true, startTime: 0, endTime: 4 });
+
+ equal(root.calls.length, 2,
+ "Should get the 2 youngest frames, not the 1 oldest frame");
+
+ let C = getFrameNodePath(root, "C");
+ ok(C, "Should have C as a child of the root.");
+
+ equal(C.calls.length, 3,
+ "Should have 3 frames that called C.");
+ ok(getFrameNodePath(C, "B"), "B called C.");
+ ok(getFrameNodePath(C, "D"), "D called C.");
+ ok(getFrameNodePath(C, "E"), "E called C.");
+
+ equal(getFrameNodePath(C, "B").calls.length, 1);
+ ok(getFrameNodePath(C, "B > A"), "A called B called C");
+ equal(getFrameNodePath(C, "D").calls.length, 1);
+ ok(getFrameNodePath(C, "D > A"), "A called D called C");
+ equal(getFrameNodePath(C, "E").calls.length, 1);
+ ok(getFrameNodePath(C, "E > A"), "A called E called C");
+
+ let F = getFrameNodePath(root, "F");
+ ok(F, "Should have F as a child of the root.");
+
+ equal(F.calls.length, 1);
+ ok(getFrameNodePath(F, "B"), "B called F");
+
+ equal(getFrameNodePath(F, "B").calls.length, 1);
+ ok(getFrameNodePath(F, "B > A"), "A called B called F");
+});
diff --git a/devtools/client/performance/test/unit/test_tree-model-06.js b/devtools/client/performance/test/unit/test_tree-model-06.js
new file mode 100644
index 000000000..7a678852c
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_tree-model-06.js
@@ -0,0 +1,176 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that when constructing FrameNodes, if optimization data is available,
+ * the FrameNodes have the correct optimization data after iterating over samples,
+ * and only youngest frames capture optimization data.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function test() {
+ let { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
+ let root = getFrameNodePath(new ThreadNode(gThread, { startTime: 0,
+ endTime: 30 }), "(root)");
+
+ let A = getFrameNodePath(root, "A");
+ let B = getFrameNodePath(A, "B");
+ let C = getFrameNodePath(B, "C");
+ let Aopts = A.getOptimizations();
+ let Bopts = B.getOptimizations();
+ let Copts = C.getOptimizations();
+
+ ok(!Aopts, "A() was never youngest frame, so should not have optimization data");
+
+ equal(Bopts.length, 2, "B() only has optimization data when it was a youngest frame");
+
+ // Check a few properties on the OptimizationSites.
+ let optSitesObserved = new Set();
+ for (let opt of Bopts) {
+ if (opt.data.line === 12) {
+ equal(opt.samples, 2, "Correct amount of samples for B()'s first opt site");
+ equal(opt.data.attempts.length, 3, "First opt site has 3 attempts");
+ equal(opt.data.attempts[0].strategy, "SomeGetter1", "inflated strategy name");
+ equal(opt.data.attempts[0].outcome, "Failure1", "inflated outcome name");
+ equal(opt.data.types[0].typeset[0].keyedBy, "constructor", "inflates type info");
+ optSitesObserved.add("first");
+ } else {
+ equal(opt.samples, 1, "Correct amount of samples for B()'s second opt site");
+ optSitesObserved.add("second");
+ }
+ }
+
+ ok(optSitesObserved.has("first"), "first opt site for B() was checked");
+ ok(optSitesObserved.has("second"), "second opt site for B() was checked");
+
+ equal(Copts.length, 1, "C() always youngest frame, so has optimization data");
+});
+
+var gUniqueStacks = new RecordingUtils.UniqueStacks();
+
+function uniqStr(s) {
+ return gUniqueStacks.getOrAddStringIndex(s);
+}
+
+var gThread = RecordingUtils.deflateThread({
+ samples: [{
+ time: 0,
+ frames: [
+ { location: "(root)" }
+ ]
+ }, {
+ time: 10,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B_LEAF_1" }
+ ]
+ }, {
+ time: 15,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B_NOTLEAF" },
+ { location: "C" },
+ ]
+ }, {
+ time: 20,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B_LEAF_2" }
+ ]
+ }, {
+ time: 25,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B_LEAF_2" }
+ ]
+ }],
+ markers: []
+}, gUniqueStacks);
+
+var gRawSite1 = {
+ line: 12,
+ column: 2,
+ types: [{
+ mirType: uniqStr("Object"),
+ site: uniqStr("B (http://foo/bar:10)"),
+ typeset: [{
+ keyedBy: uniqStr("constructor"),
+ name: uniqStr("Foo"),
+ location: uniqStr("B (http://foo/bar:10)")
+ }, {
+ keyedBy: uniqStr("primitive"),
+ location: uniqStr("self-hosted")
+ }]
+ }],
+ attempts: {
+ schema: {
+ outcome: 0,
+ strategy: 1
+ },
+ data: [
+ [uniqStr("Failure1"), uniqStr("SomeGetter1")],
+ [uniqStr("Failure2"), uniqStr("SomeGetter2")],
+ [uniqStr("Inlined"), uniqStr("SomeGetter3")]
+ ]
+ }
+};
+
+var gRawSite2 = {
+ line: 22,
+ types: [{
+ mirType: uniqStr("Int32"),
+ site: uniqStr("Receiver")
+ }],
+ attempts: {
+ schema: {
+ outcome: 0,
+ strategy: 1
+ },
+ data: [
+ [uniqStr("Failure1"), uniqStr("SomeGetter1")],
+ [uniqStr("Failure2"), uniqStr("SomeGetter2")],
+ [uniqStr("Failure3"), uniqStr("SomeGetter3")]
+ ]
+ }
+};
+
+function serialize(x) {
+ return JSON.parse(JSON.stringify(x));
+}
+
+gThread.frameTable.data.forEach((frame) => {
+ const LOCATION_SLOT = gThread.frameTable.schema.location;
+ const OPTIMIZATIONS_SLOT = gThread.frameTable.schema.optimizations;
+
+ let l = gThread.stringTable[frame[LOCATION_SLOT]];
+ switch (l) {
+ case "A":
+ frame[OPTIMIZATIONS_SLOT] = serialize(gRawSite1);
+ break;
+ // Rename some of the location sites so we can register different
+ // frames with different opt sites
+ case "B_LEAF_1":
+ frame[OPTIMIZATIONS_SLOT] = serialize(gRawSite2);
+ frame[LOCATION_SLOT] = uniqStr("B");
+ break;
+ case "B_LEAF_2":
+ frame[OPTIMIZATIONS_SLOT] = serialize(gRawSite1);
+ frame[LOCATION_SLOT] = uniqStr("B");
+ break;
+ case "B_NOTLEAF":
+ frame[OPTIMIZATIONS_SLOT] = serialize(gRawSite1);
+ frame[LOCATION_SLOT] = uniqStr("B");
+ break;
+ case "C":
+ frame[OPTIMIZATIONS_SLOT] = serialize(gRawSite1);
+ break;
+ }
+});
diff --git a/devtools/client/performance/test/unit/test_tree-model-07.js b/devtools/client/performance/test/unit/test_tree-model-07.js
new file mode 100644
index 000000000..2ea08c5ca
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_tree-model-07.js
@@ -0,0 +1,101 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that when displaying only content nodes, platform nodes are generalized.
+ */
+
+var { CATEGORY_MASK } = require("devtools/client/performance/modules/categories");
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function test() {
+ let { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
+ let url = (n) => `http://content/${n}`;
+
+ // Create a root node from a given samples array.
+
+ let root = getFrameNodePath(new ThreadNode(gThread, { startTime: 5, endTime: 30,
+ contentOnly: true }), "(root)");
+
+ /*
+ * should have a tree like:
+ * root
+ * - (JS)
+ * - A
+ * - (GC)
+ * - B
+ * - C
+ * - D
+ * - E
+ * - F
+ * - (JS)
+ */
+
+ // Test the root node.
+
+ equal(root.calls.length, 2, "root has 2 children");
+ ok(getFrameNodePath(root, url("A")), "root has content child");
+ ok(getFrameNodePath(root, "64"), "root has platform generalized child");
+ equal(getFrameNodePath(root, "64").calls.length, 0,
+ "platform generalized child is a leaf.");
+
+ ok(getFrameNodePath(root, `${url("A")} > 128`),
+ "A has platform generalized child of another type");
+ equal(getFrameNodePath(root, `${url("A")} > 128`).calls.length, 0,
+ "second generalized type is a leaf.");
+
+ ok(getFrameNodePath(root, `${url("A")} > ${url("E")} > ${url("F")} > 64`),
+ "a second leaf of the first generalized type exists deep in the tree.");
+ ok(getFrameNodePath(root, `${url("A")} > 128`),
+ "A has platform generalized child of another type");
+
+ equal(getFrameNodePath(root, "64").category,
+ getFrameNodePath(root, `${url("A")} > ${url("E")} > ${url("F")} > 64`).category,
+ "generalized frames of same type are duplicated in top-down view");
+});
+
+var gThread = synthesizeProfileForTest([{
+ time: 5,
+ frames: [
+ { location: "(root)" },
+ { location: "http://content/A" },
+ { location: "http://content/B" },
+ { location: "http://content/C" }
+ ]
+}, {
+ time: 5 + 6,
+ frames: [
+ { location: "(root)" },
+ { location: "http://content/A" },
+ { location: "http://content/B" },
+ { location: "contentY", category: CATEGORY_MASK("css") },
+ { location: "http://content/D" }
+ ]
+}, {
+ time: 5 + 6 + 7,
+ frames: [
+ { location: "(root)" },
+ { location: "http://content/A" },
+ { location: "contentY", category: CATEGORY_MASK("css") },
+ { location: "http://content/E" },
+ { location: "http://content/F" },
+ { location: "contentY", category: CATEGORY_MASK("js") },
+ ]
+}, {
+ time: 5 + 20,
+ frames: [
+ { location: "(root)" },
+ { location: "contentX", category: CATEGORY_MASK("js") },
+ ]
+}, {
+ time: 5 + 25,
+ frames: [
+ { location: "(root)" },
+ { location: "http://content/A" },
+ { location: "contentZ", category: CATEGORY_MASK("gc", 1) },
+ ]
+}]);
diff --git a/devtools/client/performance/test/unit/test_tree-model-08.js b/devtools/client/performance/test/unit/test_tree-model-08.js
new file mode 100644
index 000000000..59f7e0d34
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_tree-model-08.js
@@ -0,0 +1,99 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Verifies if FrameNodes retain and parse their data appropriately.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function test() {
+ let FrameUtils = require("devtools/client/performance/modules/logic/frame-utils");
+ let { FrameNode } = require("devtools/client/performance/modules/logic/tree-model");
+ let { CATEGORY_MASK } = require("devtools/client/performance/modules/categories");
+ let compute = frame => {
+ FrameUtils.computeIsContentAndCategory(frame);
+ return frame;
+ };
+
+ let frames = [
+ new FrameNode("hello/<.world (http://foo/bar.js:123:987)", compute({
+ location: "hello/<.world (http://foo/bar.js:123:987)",
+ line: 456,
+ }), false),
+ new FrameNode("hello/<.world (http://foo/bar.js#baz:123:987)", compute({
+ location: "hello/<.world (http://foo/bar.js#baz:123:987)",
+ line: 456,
+ }), false),
+ new FrameNode("hello/<.world (http://foo/#bar:123:987)", compute({
+ location: "hello/<.world (http://foo/#bar:123:987)",
+ line: 456,
+ }), false),
+ new FrameNode("hello/<.world (http://foo/:123:987)", compute({
+ location: "hello/<.world (http://foo/:123:987)",
+ line: 456,
+ }), false),
+ new FrameNode("hello/<.world (resource://foo.js -> http://bar/baz.js:123:987)", compute({
+ location: "hello/<.world (resource://foo.js -> http://bar/baz.js:123:987)",
+ line: 456,
+ }), false),
+ new FrameNode("Foo::Bar::Baz", compute({
+ location: "Foo::Bar::Baz",
+ line: 456,
+ category: CATEGORY_MASK("other"),
+ }), false),
+ new FrameNode("EnterJIT", compute({
+ location: "EnterJIT",
+ }), false),
+ new FrameNode("chrome://browser/content/content.js", compute({
+ location: "chrome://browser/content/content.js",
+ line: 456,
+ column: 123
+ }), false),
+ new FrameNode("hello/<.world (resource://gre/foo.js:123:434)", compute({
+ location: "hello/<.world (resource://gre/foo.js:123:434)",
+ line: 456
+ }), false),
+ new FrameNode("main (http://localhost:8888/file.js:123:987)", compute({
+ location: "main (http://localhost:8888/file.js:123:987)",
+ line: 123,
+ }), false),
+ new FrameNode("main (resource://devtools/timeline.js:123)", compute({
+ location: "main (resource://devtools/timeline.js:123)",
+ }), false),
+ ];
+
+ let fields = ["nodeType", "functionName", "fileName", "host", "url", "line", "column",
+ "categoryData.abbrev", "isContent", "port"];
+ let expected = [
+ // nodeType, functionName, fileName, host, url, line, column, categoryData.abbrev,
+ // isContent, port
+ ["Frame", "hello/<.world", "bar.js", "foo", "http://foo/bar.js", 123, 987, void 0, true],
+ ["Frame", "hello/<.world", "bar.js", "foo", "http://foo/bar.js#baz", 123, 987, void 0, true],
+ ["Frame", "hello/<.world", "/", "foo", "http://foo/#bar", 123, 987, void 0, true],
+ ["Frame", "hello/<.world", "/", "foo", "http://foo/", 123, 987, void 0, true],
+ ["Frame", "hello/<.world", "baz.js", "bar", "http://bar/baz.js", 123, 987, "other", false],
+ ["Frame", "Foo::Bar::Baz", null, null, null, 456, void 0, "other", false],
+ ["Frame", "EnterJIT", null, null, null, null, null, "js", false],
+ ["Frame", "chrome://browser/content/content.js", null, null, null, 456, null, "other", false],
+ ["Frame", "hello/<.world", "foo.js", null, "resource://gre/foo.js", 123, 434, "other", false],
+ ["Frame", "main", "file.js", "localhost:8888", "http://localhost:8888/file.js", 123, 987, null, true, 8888],
+ ["Frame", "main", "timeline.js", null, "resource://devtools/timeline.js", 123, null, "tools", false]
+ ];
+
+ for (let i = 0; i < frames.length; i++) {
+ let info = frames[i].getInfo();
+ let expect = expected[i];
+
+ for (let j = 0; j < fields.length; j++) {
+ let field = fields[j];
+ let value = field === "categoryData.abbrev"
+ ? info.categoryData.abbrev
+ : info[field];
+ equal(value, expect[j], `${field} for frame #${i} is correct: ${expect[j]}`);
+ }
+ }
+});
diff --git a/devtools/client/performance/test/unit/test_tree-model-09.js b/devtools/client/performance/test/unit/test_tree-model-09.js
new file mode 100644
index 000000000..1bf267227
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_tree-model-09.js
@@ -0,0 +1,84 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that when displaying only content nodes, platform nodes are generalized.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function test() {
+ let { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
+ let url = (n) => `http://content/${n}`;
+
+ // Create a root node from a given samples array.
+
+ let root = getFrameNodePath(new ThreadNode(gThread, { startTime: 5, endTime: 25,
+ contentOnly: true }), "(root)");
+
+ /*
+ * should have a tree like:
+ * root
+ * - (Tools)
+ * - A
+ * - B
+ * - C
+ * - D
+ * - E
+ * - F
+ * - (Tools)
+ */
+
+ // Test the root node.
+
+ equal(root.calls.length, 2, "root has 2 children");
+ ok(getFrameNodePath(root, url("A")), "root has content child");
+ ok(getFrameNodePath(root, "9000"),
+ "root has platform generalized child from Chrome JS");
+ equal(getFrameNodePath(root, "9000").calls.length, 0,
+ "platform generalized child is a leaf.");
+
+ ok(getFrameNodePath(root, `${url("A")} > ${url("E")} > ${url("F")} > 9000`),
+ "a second leaf of the generalized Chrome JS exists.");
+
+ equal(getFrameNodePath(root, "9000").category,
+ getFrameNodePath(root, `${url("A")} > ${url("E")} > ${url("F")} > 9000`).category,
+ "generalized frames of same type are duplicated in top-down view");
+});
+
+var gThread = synthesizeProfileForTest([{
+ time: 5,
+ frames: [
+ { location: "(root)" },
+ { location: "http://content/A" },
+ { location: "http://content/B" },
+ { location: "http://content/C" }
+ ]
+}, {
+ time: 5 + 6,
+ frames: [
+ { location: "(root)" },
+ { location: "http://content/A" },
+ { location: "http://content/B" },
+ { location: "fn (resource://loader.js -> resource://devtools/timeline.js)" },
+ { location: "http://content/D" }
+ ]
+}, {
+ time: 5 + 6 + 7,
+ frames: [
+ { location: "(root)" },
+ { location: "http://content/A" },
+ { location: "http://content/E" },
+ { location: "http://content/F" },
+ { location: "fn (resource://loader.js -> resource://devtools/promise.js)" }
+ ]
+}, {
+ time: 5 + 20,
+ frames: [
+ { location: "(root)" },
+ { location: "somefn (resource://loader.js -> resource://devtools/framerate.js)" }
+ ]
+}]);
diff --git a/devtools/client/performance/test/unit/test_tree-model-10.js b/devtools/client/performance/test/unit/test_tree-model-10.js
new file mode 100644
index 000000000..9553c7052
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_tree-model-10.js
@@ -0,0 +1,153 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the tree model calculates correct costs/percentages for
+ * frame nodes. The model-only version of browser_profiler-tree-view-10.js
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function () {
+ let { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
+ let thread = new ThreadNode(gThread, { invertTree: true, startTime: 0, endTime: 50 });
+
+ /**
+ * Samples
+ *
+ * A->C
+ * A->B
+ * A->B->C x4
+ * A->B->D x4
+ *
+ * Expected Tree
+ * +--total--+--self--+--tree-------------+
+ * | 50% | 50% | C
+ * | 40% | 0 | -> B
+ * | 30% | 0 | -> A
+ * | 10% | 0 | -> A
+ *
+ * | 40% | 40% | D
+ * | 40% | 0 | -> B
+ * | 40% | 0 | -> A
+ *
+ * | 10% | 10% | B
+ * | 10% | 0 | -> A
+ */
+
+ [
+ // total, self, name
+ [ 50, 50, "C", [
+ [ 40, 0, "B", [
+ [ 30, 0, "A"]
+ ]],
+ [ 10, 0, "A"]
+ ]],
+ [ 40, 40, "D", [
+ [ 40, 0, "B", [
+ [ 40, 0, "A"],
+ ]]
+ ]],
+ [ 10, 10, "B", [
+ [ 10, 0, "A"],
+ ]]
+ ].forEach(compareFrameInfo(thread));
+});
+
+function compareFrameInfo(root, parent) {
+ parent = parent || root;
+ return function (def) {
+ let [total, self, name, children] = def;
+ let node = getFrameNodePath(parent, name);
+ let data = node.getInfo({ root });
+ equal(total, data.totalPercentage,
+ `${name} has correct total percentage: ${data.totalPercentage}`);
+ equal(self, data.selfPercentage,
+ `${name} has correct self percentage: ${data.selfPercentage}`);
+ if (children) {
+ children.forEach(compareFrameInfo(root, node));
+ }
+ };
+}
+
+var gThread = synthesizeProfileForTest([{
+ time: 5,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "C" }
+ ]
+}, {
+ time: 10,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "D" }
+ ]
+}, {
+ time: 15,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "C" },
+ ]
+}, {
+ time: 20,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ ]
+}, {
+ time: 25,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "C" }
+ ]
+}, {
+ time: 30,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "C" }
+ ]
+}, {
+ time: 35,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "D" }
+ ]
+}, {
+ time: 40,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "D" }
+ ]
+}, {
+ time: 45,
+ frames: [
+ { location: "(root)" },
+ { location: "B" },
+ { location: "C" }
+ ]
+}, {
+ time: 50,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "D" }
+ ]
+}]);
diff --git a/devtools/client/performance/test/unit/test_tree-model-11.js b/devtools/client/performance/test/unit/test_tree-model-11.js
new file mode 100644
index 000000000..c665dfe32
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_tree-model-11.js
@@ -0,0 +1,90 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the costs for recursive frames does not overcount the collapsed
+ * samples.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function () {
+ let { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
+ let thread = new ThreadNode(gThread, { startTime: 0, endTime: 50,
+ flattenRecursion: true });
+
+ /**
+ * Samples
+ *
+ * A->B->C
+ * A->B->B->B->C
+ * A->B
+ * A->B->B->B
+ */
+
+ [
+ // total, self, name
+ [ 100, 0, "(root)", [
+ [ 100, 0, "A", [
+ [ 100, 50, "B", [
+ [ 50, 50, "C"]
+ ]]
+ ]],
+ ]],
+ ].forEach(compareFrameInfo(thread));
+});
+
+function compareFrameInfo(root, parent) {
+ parent = parent || root;
+ return function (def) {
+ let [total, self, name, children] = def;
+ let node = getFrameNodePath(parent, name);
+ let data = node.getInfo({ root });
+ equal(total, data.totalPercentage,
+ `${name} has correct total percentage: ${data.totalPercentage}`);
+ equal(self, data.selfPercentage,
+ `${name} has correct self percentage: ${data.selfPercentage}`);
+ if (children) {
+ children.forEach(compareFrameInfo(root, node));
+ }
+ };
+}
+
+var gThread = synthesizeProfileForTest([{
+ time: 5,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "B" },
+ { location: "B" },
+ { location: "C" }
+ ]
+}, {
+ time: 10,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "C" }
+ ]
+}, {
+ time: 15,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "B" },
+ { location: "B" },
+ ]
+}, {
+ time: 20,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ ]
+}]);
diff --git a/devtools/client/performance/test/unit/test_tree-model-12.js b/devtools/client/performance/test/unit/test_tree-model-12.js
new file mode 100644
index 000000000..fde96e349
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_tree-model-12.js
@@ -0,0 +1,94 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that uninverting the call tree works correctly when there are stacks
+// in the profile that prefixes of other stacks.
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function () {
+ let { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
+ let thread = new ThreadNode(gThread, { startTime: 0, endTime: 50 });
+ let root = getFrameNodePath(thread, "(root)");
+
+ /**
+ * Samples
+ *
+ * A->B
+ * C->B
+ * B
+ * A
+ * Z->Y->X
+ * W->Y->X
+ * Y->X
+ */
+
+ equal(getFrameNodePath(root, "A > B").youngestFrameSamples, 1,
+ "A > B has the correct self count");
+ equal(getFrameNodePath(root, "C > B").youngestFrameSamples, 1,
+ "C > B has the correct self count");
+ equal(getFrameNodePath(root, "B").youngestFrameSamples, 1,
+ "B has the correct self count");
+ equal(getFrameNodePath(root, "A").youngestFrameSamples, 1,
+ "A has the correct self count");
+ equal(getFrameNodePath(root, "Z > Y > X").youngestFrameSamples, 1,
+ "Z > Y > X has the correct self count");
+ equal(getFrameNodePath(root, "W > Y > X").youngestFrameSamples, 1,
+ "W > Y > X has the correct self count");
+ equal(getFrameNodePath(root, "Y > X").youngestFrameSamples, 1,
+ "Y > X has the correct self count");
+});
+
+var gThread = synthesizeProfileForTest([{
+ time: 5,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ ]
+}, {
+ time: 10,
+ frames: [
+ { location: "(root)" },
+ { location: "C" },
+ { location: "B" },
+ ]
+}, {
+ time: 15,
+ frames: [
+ { location: "(root)" },
+ { location: "B" },
+ ]
+}, {
+ time: 20,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ ]
+}, {
+ time: 21,
+ frames: [
+ { location: "(root)" },
+ { location: "Z" },
+ { location: "Y" },
+ { location: "X" },
+ ]
+}, {
+ time: 22,
+ frames: [
+ { location: "(root)" },
+ { location: "W" },
+ { location: "Y" },
+ { location: "X" },
+ ]
+}, {
+ time: 23,
+ frames: [
+ { location: "(root)" },
+ { location: "Y" },
+ { location: "X" },
+ ]
+}]);
diff --git a/devtools/client/performance/test/unit/test_tree-model-13.js b/devtools/client/performance/test/unit/test_tree-model-13.js
new file mode 100644
index 000000000..a1aa666f1
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_tree-model-13.js
@@ -0,0 +1,86 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Like test_tree-model-12, but inverted.
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function () {
+ let { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
+ let root = new ThreadNode(gThread, { invertTree: true, startTime: 0, endTime: 50 });
+
+ /**
+ * Samples
+ *
+ * A->B
+ * C->B
+ * B
+ * A
+ * Z->Y->X
+ * W->Y->X
+ * Y->X
+ */
+
+ equal(getFrameNodePath(root, "B").youngestFrameSamples, 3,
+ "B has the correct self count");
+ equal(getFrameNodePath(root, "A").youngestFrameSamples, 1,
+ "A has the correct self count");
+ equal(getFrameNodePath(root, "X").youngestFrameSamples, 3,
+ "X has the correct self count");
+ equal(getFrameNodePath(root, "X > Y").samples, 3,
+ "X > Y has the correct total count");
+});
+
+var gThread = synthesizeProfileForTest([{
+ time: 5,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ ]
+}, {
+ time: 10,
+ frames: [
+ { location: "(root)" },
+ { location: "C" },
+ { location: "B" },
+ ]
+}, {
+ time: 15,
+ frames: [
+ { location: "(root)" },
+ { location: "B" },
+ ]
+}, {
+ time: 20,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ ]
+}, {
+ time: 21,
+ frames: [
+ { location: "(root)" },
+ { location: "Z" },
+ { location: "Y" },
+ { location: "X" },
+ ]
+}, {
+ time: 22,
+ frames: [
+ { location: "(root)" },
+ { location: "W" },
+ { location: "Y" },
+ { location: "X" },
+ ]
+}, {
+ time: 23,
+ frames: [
+ { location: "(root)" },
+ { location: "Y" },
+ { location: "X" },
+ ]
+}]);
diff --git a/devtools/client/performance/test/unit/test_tree-model-allocations-01.js b/devtools/client/performance/test/unit/test_tree-model-allocations-01.js
new file mode 100644
index 000000000..331a625f9
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_tree-model-allocations-01.js
@@ -0,0 +1,95 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+/**
+ * Tests that the tree model calculates correct costs/percentages for
+ * allocation frame nodes.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function () {
+ let { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
+ const { getProfileThreadFromAllocations } = require("devtools/shared/performance/recording-utils");
+ let allocationData = getProfileThreadFromAllocations(TEST_DATA);
+ let thread = new ThreadNode(allocationData, { startTime: 0, endTime: 1000 });
+
+ /* eslint-disable max-len */
+ /**
+ * Values are in order according to:
+ * +-------------+------------+-------------+-------------+------------------------------+
+ * | Self Bytes | Self Count | Total Bytes | Total Count | Function |
+ * +-------------+------------+-------------+-------------+------------------------------+
+ * | 1790272 41% | 8307 17% | 1790372 42% | 8317 18% | V someFunc @ a.j:345:6 |
+ * | 100 1% | 10 1% | 100 1% | 10 1% | > callerFunc @ b.j:765:34 |
+ * +-------------+------------+-------------+-------------+------------------------------+
+ */
+ /* eslint-enable max-len */
+ [
+ [100, 10, 1, 33, 1000, 100, 3, 100, "x (A:1:2)", [
+ [200, 20, 1, 33, 900, 90, 2, 66, "y (B:3:4)", [
+ [700, 70, 1, 33, 700, 70, 1, 33, "z (C:5:6)"]
+ ]]
+ ]]
+ ].forEach(compareFrameInfo(thread));
+});
+
+function compareFrameInfo(root, parent) {
+ parent = parent || root;
+ let fields = [
+ "selfSize", "selfSizePercentage", "selfCount", "selfCountPercentage",
+ "totalSize", "totalSizePercentage", "totalCount", "totalCountPercentage"
+ ];
+ return function (def) {
+ let children;
+ if (Array.isArray(def[def.length - 1])) {
+ children = def.pop();
+ }
+ let name = def.pop();
+ let expected = def;
+
+ let node = getFrameNodePath(parent, name);
+ let data = node.getInfo({ root, allocations: true });
+
+ fields.forEach((field, i) => {
+ let actual = data[field];
+ if (/percentage/i.test(field)) {
+ actual = Number.parseInt(actual, 10);
+ }
+ equal(actual, expected[i], `${name} has correct ${field}: ${expected[i]}`);
+ });
+
+ if (children) {
+ children.forEach(compareFrameInfo(root, node));
+ }
+ };
+}
+
+var TEST_DATA = {
+ sites: [1, 2, 3],
+ timestamps: [150, 200, 250],
+ sizes: [100, 200, 700],
+ frames: [
+ null, {
+ source: "A",
+ line: 1,
+ column: 2,
+ functionDisplayName: "x",
+ parent: 0
+ }, {
+ source: "B",
+ line: 3,
+ column: 4,
+ functionDisplayName: "y",
+ parent: 1
+ }, {
+ source: "C",
+ line: 5,
+ column: 6,
+ functionDisplayName: "z",
+ parent: 2
+ }
+ ]
+};
diff --git a/devtools/client/performance/test/unit/test_tree-model-allocations-02.js b/devtools/client/performance/test/unit/test_tree-model-allocations-02.js
new file mode 100644
index 000000000..cfc5c4048
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_tree-model-allocations-02.js
@@ -0,0 +1,105 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the tree model calculates correct costs/percentages for
+ * allocation frame nodes. Inverted version of test_tree-model-allocations-01.js
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function () {
+ let { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
+ const { getProfileThreadFromAllocations } = require("devtools/shared/performance/recording-utils");
+ let allocationData = getProfileThreadFromAllocations(TEST_DATA);
+ let thread = new ThreadNode(allocationData, { invertTree: true, startTime: 0,
+ endTime: 1000 });
+
+ /* eslint-disable max-len */
+ /**
+ * Values are in order according to:
+ * +-------------+------------+-------------+-------------+------------------------------+
+ * | Self Bytes | Self Count | Total Bytes | Total Count | Function |
+ * +-------------+------------+-------------+-------------+------------------------------+
+ * | 1790272 41% | 8307 17% | 1790372 42% | 8317 18% | V someFunc @ a.j:345:6 |
+ * | 100 1% | 10 1% | 100 1% | 10 1% | > callerFunc @ b.j:765:34 |
+ * +-------------+------------+-------------+-------------+------------------------------+
+ */
+ /* eslint-enable max-len */
+ [
+ [700, 70, 1, 33, 700, 70, 1, 33, "z (C:5:6)", [
+ [0, 0, 0, 0, 700, 70, 1, 33, "y (B:3:4)", [
+ [0, 0, 0, 0, 700, 70, 1, 33, "x (A:1:2)"]
+ ]]
+ ]],
+ [200, 20, 1, 33, 200, 20, 1, 33, "y (B:3:4)", [
+ [0, 0, 0, 0, 200, 20, 1, 33, "x (A:1:2)"]
+ ]],
+ [100, 10, 1, 33, 100, 10, 1, 33, "x (A:1:2)"]
+ ].forEach(compareFrameInfo(thread));
+});
+
+function compareFrameInfo(root, parent) {
+ parent = parent || root;
+ let fields = [
+ "selfSize", "selfSizePercentage", "selfCount", "selfCountPercentage",
+ "totalSize", "totalSizePercentage", "totalCount", "totalCountPercentage"
+ ];
+
+ return function (def) {
+ let children;
+
+ if (Array.isArray(def[def.length - 1])) {
+ children = def.pop();
+ }
+
+ let name = def.pop();
+ let expected = def;
+
+ let node = getFrameNodePath(parent, name);
+ let data = node.getInfo({ root, allocations: true });
+
+ fields.forEach((field, i) => {
+ let actual = data[field];
+ if (/percentage/i.test(field)) {
+ actual = Number.parseInt(actual, 10);
+ }
+ equal(actual, expected[i], `${name} has correct ${field}: ${expected[i]}`);
+ });
+
+ if (children) {
+ children.forEach(compareFrameInfo(root, node));
+ }
+ };
+}
+
+var TEST_DATA = {
+ sites: [0, 1, 2, 3],
+ timestamps: [0, 150, 200, 250],
+ sizes: [0, 100, 200, 700],
+ frames: [{
+ source: "(root)"
+ }, {
+ source: "A",
+ line: 1,
+ column: 2,
+ functionDisplayName: "x",
+ parent: 0
+ }, {
+ source: "B",
+ line: 3,
+ column: 4,
+ functionDisplayName: "y",
+ parent: 1
+ }, {
+ source: "C",
+ line: 5,
+ column: 6,
+ functionDisplayName: "z",
+ parent: 2
+ }
+ ]
+};
diff --git a/devtools/client/performance/test/unit/test_waterfall-utils-collapse-01.js b/devtools/client/performance/test/unit/test_waterfall-utils-collapse-01.js
new file mode 100644
index 000000000..e329622db
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_waterfall-utils-collapse-01.js
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if the waterfall collapsing logic works properly.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function test() {
+ const WaterfallUtils = require("devtools/client/performance/modules/logic/waterfall-utils");
+
+ let rootMarkerNode = WaterfallUtils.createParentNode({ name: "(root)" });
+
+ WaterfallUtils.collapseMarkersIntoNode({
+ rootNode: rootMarkerNode,
+ markersList: gTestMarkers
+ });
+
+ function compare(marker, expected) {
+ for (let prop in expected) {
+ if (prop === "submarkers") {
+ for (let i = 0; i < expected.submarkers.length; i++) {
+ compare(marker.submarkers[i], expected.submarkers[i]);
+ }
+ } else if (prop !== "uid") {
+ equal(marker[prop], expected[prop], `${expected.name} matches ${prop}`);
+ }
+ }
+ }
+
+ compare(rootMarkerNode, gExpectedOutput);
+});
+
+const gTestMarkers = [
+ { start: 1, end: 18, name: "DOMEvent" },
+ // Test that JS markers can fold in DOM events and have marker children
+ { start: 2, end: 16, name: "Javascript" },
+ // Test all these markers can be children
+ { start: 3, end: 4, name: "Paint" },
+ { start: 5, end: 6, name: "Reflow" },
+ { start: 7, end: 8, name: "Styles" },
+ { start: 9, end: 9, name: "TimeStamp" },
+ { start: 10, end: 11, name: "Parse HTML" },
+ { start: 12, end: 13, name: "Parse XML" },
+ { start: 14, end: 15, name: "GarbageCollection" },
+ // Test that JS markers can be parents without being a child of DOM events
+ { start: 25, end: 30, name: "Javascript" },
+ { start: 26, end: 27, name: "Paint" },
+];
+
+const gExpectedOutput = {
+ name: "(root)", submarkers: [
+ { start: 1, end: 18, name: "DOMEvent", submarkers: [
+ { start: 2, end: 16, name: "Javascript", submarkers: [
+ { start: 3, end: 4, name: "Paint" },
+ { start: 5, end: 6, name: "Reflow" },
+ { start: 7, end: 8, name: "Styles" },
+ { start: 9, end: 9, name: "TimeStamp" },
+ { start: 10, end: 11, name: "Parse HTML" },
+ { start: 12, end: 13, name: "Parse XML" },
+ { start: 14, end: 15, name: "GarbageCollection" },
+ ]}
+ ]},
+ { start: 25, end: 30, name: "Javascript", submarkers: [
+ { start: 26, end: 27, name: "Paint" },
+ ]}
+ ]};
diff --git a/devtools/client/performance/test/unit/test_waterfall-utils-collapse-02.js b/devtools/client/performance/test/unit/test_waterfall-utils-collapse-02.js
new file mode 100644
index 000000000..1cc33f45a
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_waterfall-utils-collapse-02.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if the waterfall collapsing logic works properly for console.time/console.timeEnd
+ * markers, as they should ignore any sort of collapsing.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function test() {
+ const WaterfallUtils = require("devtools/client/performance/modules/logic/waterfall-utils");
+
+ let rootMarkerNode = WaterfallUtils.createParentNode({ name: "(root)" });
+
+ WaterfallUtils.collapseMarkersIntoNode({
+ rootNode: rootMarkerNode,
+ markersList: gTestMarkers
+ });
+
+ function compare(marker, expected) {
+ for (let prop in expected) {
+ if (prop === "submarkers") {
+ for (let i = 0; i < expected.submarkers.length; i++) {
+ compare(marker.submarkers[i], expected.submarkers[i]);
+ }
+ } else if (prop !== "uid") {
+ equal(marker[prop], expected[prop], `${expected.name} matches ${prop}`);
+ }
+ }
+ }
+
+ compare(rootMarkerNode, gExpectedOutput);
+});
+
+const gTestMarkers = [
+ { start: 2, end: 9, name: "Javascript" },
+ { start: 3, end: 4, name: "Paint" },
+ // Time range starting in nest, ending outside
+ { start: 5, end: 12, name: "ConsoleTime", causeName: "1" },
+
+ // Time range starting outside of nest, ending inside
+ { start: 15, end: 21, name: "ConsoleTime", causeName: "2" },
+ { start: 18, end: 22, name: "Javascript" },
+ { start: 19, end: 20, name: "Paint" },
+
+ // Time range completely eclipsing nest
+ { start: 30, end: 40, name: "ConsoleTime", causeName: "3" },
+ { start: 34, end: 39, name: "Javascript" },
+ { start: 35, end: 36, name: "Paint" },
+
+ // Time range completely eclipsed by nest
+ { start: 50, end: 60, name: "Javascript" },
+ { start: 54, end: 59, name: "ConsoleTime", causeName: "4" },
+ { start: 56, end: 57, name: "Paint" },
+];
+
+const gExpectedOutput = {
+ name: "(root)", submarkers: [
+ { start: 2, end: 9, name: "Javascript", submarkers: [
+ { start: 3, end: 4, name: "Paint" }
+ ]},
+ { start: 5, end: 12, name: "ConsoleTime", causeName: "1" },
+
+ { start: 15, end: 21, name: "ConsoleTime", causeName: "2" },
+ { start: 18, end: 22, name: "Javascript", submarkers: [
+ { start: 19, end: 20, name: "Paint" }
+ ]},
+
+ { start: 30, end: 40, name: "ConsoleTime", causeName: "3" },
+ { start: 34, end: 39, name: "Javascript", submarkers: [
+ { start: 35, end: 36, name: "Paint" },
+ ]},
+
+ { start: 50, end: 60, name: "Javascript", submarkers: [
+ { start: 56, end: 57, name: "Paint" },
+ ]},
+ { start: 54, end: 59, name: "ConsoleTime", causeName: "4" },
+ ]};
diff --git a/devtools/client/performance/test/unit/test_waterfall-utils-collapse-03.js b/devtools/client/performance/test/unit/test_waterfall-utils-collapse-03.js
new file mode 100644
index 000000000..00b6d2db0
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_waterfall-utils-collapse-03.js
@@ -0,0 +1,64 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the waterfall collapsing works when atleast two
+ * collapsible markers downward, and the following marker is outside of both ranges.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function test() {
+ const WaterfallUtils = require("devtools/client/performance/modules/logic/waterfall-utils");
+
+ let rootMarkerNode = WaterfallUtils.createParentNode({ name: "(root)" });
+
+ WaterfallUtils.collapseMarkersIntoNode({
+ rootNode: rootMarkerNode,
+ markersList: gTestMarkers
+ });
+
+ function compare(marker, expected) {
+ for (let prop in expected) {
+ if (prop === "submarkers") {
+ for (let i = 0; i < expected.submarkers.length; i++) {
+ compare(marker.submarkers[i], expected.submarkers[i]);
+ }
+ } else if (prop !== "uid") {
+ equal(marker[prop], expected[prop], `${expected.name} matches ${prop}`);
+ }
+ }
+ }
+
+ compare(rootMarkerNode, gExpectedOutput);
+});
+
+const gTestMarkers = [
+ { start: 2, end: 10, name: "DOMEvent" },
+ { start: 3, end: 9, name: "Javascript" },
+ { start: 4, end: 8, name: "GarbageCollection" },
+ { start: 11, end: 12, name: "Styles" },
+ { start: 13, end: 14, name: "Styles" },
+ { start: 15, end: 25, name: "DOMEvent" },
+ { start: 17, end: 24, name: "Javascript" },
+ { start: 18, end: 19, name: "GarbageCollection" },
+];
+
+const gExpectedOutput = {
+ name: "(root)", submarkers: [
+ { start: 2, end: 10, name: "DOMEvent", submarkers: [
+ { start: 3, end: 9, name: "Javascript", submarkers: [
+ { start: 4, end: 8, name: "GarbageCollection" }
+ ]}
+ ]},
+ { start: 11, end: 12, name: "Styles" },
+ { start: 13, end: 14, name: "Styles" },
+ { start: 15, end: 25, name: "DOMEvent", submarkers: [
+ { start: 17, end: 24, name: "Javascript", submarkers: [
+ { start: 18, end: 19, name: "GarbageCollection" }
+ ]}
+ ]},
+ ]};
diff --git a/devtools/client/performance/test/unit/test_waterfall-utils-collapse-04.js b/devtools/client/performance/test/unit/test_waterfall-utils-collapse-04.js
new file mode 100644
index 000000000..916a3b1d4
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_waterfall-utils-collapse-04.js
@@ -0,0 +1,103 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if the waterfall collapsing logic works properly
+ * when filtering parents and children.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function test() {
+ const WaterfallUtils = require("devtools/client/performance/modules/logic/waterfall-utils");
+
+ [
+ [["DOMEvent"], gExpectedOutputNoDOMEvent],
+ [["Javascript"], gExpectedOutputNoJS],
+ [["DOMEvent", "Javascript"], gExpectedOutputNoDOMEventOrJS],
+ ].forEach(([filter, expected]) => {
+ let rootMarkerNode = WaterfallUtils.createParentNode({ name: "(root)" });
+
+ WaterfallUtils.collapseMarkersIntoNode({
+ rootNode: rootMarkerNode,
+ markersList: gTestMarkers,
+ filter
+ });
+
+ compare(rootMarkerNode, expected);
+ });
+
+ function compare(marker, expected) {
+ for (let prop in expected) {
+ if (prop === "submarkers") {
+ for (let i = 0; i < expected.submarkers.length; i++) {
+ compare(marker.submarkers[i], expected.submarkers[i]);
+ }
+ } else if (prop !== "uid") {
+ equal(marker[prop], expected[prop], `${expected.name} matches ${prop}`);
+ }
+ }
+ }
+});
+
+const gTestMarkers = [
+ { start: 1, end: 18, name: "DOMEvent" },
+ // Test that JS markers can fold in DOM events and have marker children
+ { start: 2, end: 16, name: "Javascript" },
+ // Test all these markers can be children
+ { start: 3, end: 4, name: "Paint" },
+ { start: 5, end: 6, name: "Reflow" },
+ { start: 7, end: 8, name: "Styles" },
+ { start: 9, end: 9, name: "TimeStamp" },
+ { start: 10, end: 11, name: "Parse HTML" },
+ { start: 12, end: 13, name: "Parse XML" },
+ { start: 14, end: 15, name: "GarbageCollection" },
+ // Test that JS markers can be parents without being a child of DOM events
+ { start: 25, end: 30, name: "Javascript" },
+ { start: 26, end: 27, name: "Paint" },
+];
+
+const gExpectedOutputNoJS = {
+ name: "(root)", submarkers: [
+ { start: 1, end: 18, name: "DOMEvent", submarkers: [
+ { start: 3, end: 4, name: "Paint" },
+ { start: 5, end: 6, name: "Reflow" },
+ { start: 7, end: 8, name: "Styles" },
+ { start: 9, end: 9, name: "TimeStamp" },
+ { start: 10, end: 11, name: "Parse HTML" },
+ { start: 12, end: 13, name: "Parse XML" },
+ { start: 14, end: 15, name: "GarbageCollection" },
+ ]},
+ { start: 26, end: 27, name: "Paint" },
+ ]};
+
+const gExpectedOutputNoDOMEvent = {
+ name: "(root)", submarkers: [
+ { start: 2, end: 16, name: "Javascript", submarkers: [
+ { start: 3, end: 4, name: "Paint" },
+ { start: 5, end: 6, name: "Reflow" },
+ { start: 7, end: 8, name: "Styles" },
+ { start: 9, end: 9, name: "TimeStamp" },
+ { start: 10, end: 11, name: "Parse HTML" },
+ { start: 12, end: 13, name: "Parse XML" },
+ { start: 14, end: 15, name: "GarbageCollection" },
+ ]},
+ { start: 25, end: 30, name: "Javascript", submarkers: [
+ { start: 26, end: 27, name: "Paint" },
+ ]}
+ ]};
+
+const gExpectedOutputNoDOMEventOrJS = {
+ name: "(root)", submarkers: [
+ { start: 3, end: 4, name: "Paint" },
+ { start: 5, end: 6, name: "Reflow" },
+ { start: 7, end: 8, name: "Styles" },
+ { start: 9, end: 9, name: "TimeStamp" },
+ { start: 10, end: 11, name: "Parse HTML" },
+ { start: 12, end: 13, name: "Parse XML" },
+ { start: 14, end: 15, name: "GarbageCollection" },
+ { start: 26, end: 27, name: "Paint" },
+ ]};
diff --git a/devtools/client/performance/test/unit/test_waterfall-utils-collapse-05.js b/devtools/client/performance/test/unit/test_waterfall-utils-collapse-05.js
new file mode 100644
index 000000000..ba85c2adc
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_waterfall-utils-collapse-05.js
@@ -0,0 +1,164 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if the waterfall collapsing logic works properly
+ * when dealing with OTMT markers.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function test() {
+ const WaterfallUtils = require("devtools/client/performance/modules/logic/waterfall-utils");
+
+ let rootMarkerNode = WaterfallUtils.createParentNode({ name: "(root)" });
+
+ WaterfallUtils.collapseMarkersIntoNode({
+ rootNode: rootMarkerNode,
+ markersList: gTestMarkers
+ });
+
+ compare(rootMarkerNode, gExpectedOutput);
+
+ function compare(marker, expected) {
+ for (let prop in expected) {
+ if (prop === "submarkers") {
+ for (let i = 0; i < expected.submarkers.length; i++) {
+ compare(marker.submarkers[i], expected.submarkers[i]);
+ }
+ } else if (prop !== "uid") {
+ equal(marker[prop], expected[prop], `${expected.name} matches ${prop}`);
+ }
+ }
+ }
+});
+
+const gTestMarkers = [
+ { start: 1, end: 4, name: "A1-mt", processType: 1, isOffMainThread: false },
+ // This should collapse only under A1-mt
+ { start: 2, end: 3, name: "B1", processType: 1, isOffMainThread: false },
+ // This should never collapse.
+ { start: 2, end: 3, name: "C1", processType: 1, isOffMainThread: true },
+
+ { start: 5, end: 8, name: "A1-otmt", processType: 1, isOffMainThread: true },
+ // This should collapse only under A1-mt
+ { start: 6, end: 7, name: "B2", processType: 1, isOffMainThread: false },
+ // This should never collapse.
+ { start: 6, end: 7, name: "C2", processType: 1, isOffMainThread: true },
+
+ { start: 9, end: 12, name: "A2-mt", processType: 2, isOffMainThread: false },
+ // This should collapse only under A2-mt
+ { start: 10, end: 11, name: "D1", processType: 2, isOffMainThread: false },
+ // This should never collapse.
+ { start: 10, end: 11, name: "E1", processType: 2, isOffMainThread: true },
+
+ { start: 13, end: 16, name: "A2-otmt", processType: 2, isOffMainThread: true },
+ // This should collapse only under A2-mt
+ { start: 14, end: 15, name: "D2", processType: 2, isOffMainThread: false },
+ // This should never collapse.
+ { start: 14, end: 15, name: "E2", processType: 2, isOffMainThread: true },
+
+ // This should not collapse, because there's no parent in this process.
+ { start: 14, end: 15, name: "F", processType: 3, isOffMainThread: false },
+
+ // This should never collapse.
+ { start: 14, end: 15, name: "G", processType: 3, isOffMainThread: true },
+];
+
+const gExpectedOutput = {
+ name: "(root)",
+ submarkers: [{
+ start: 1,
+ end: 4,
+ name: "A1-mt",
+ processType: 1,
+ isOffMainThread: false,
+ submarkers: [{
+ start: 2,
+ end: 3,
+ name: "B1",
+ processType: 1,
+ isOffMainThread: false
+ }]
+ }, {
+ start: 2,
+ end: 3,
+ name: "C1",
+ processType: 1,
+ isOffMainThread: true
+ }, {
+ start: 5,
+ end: 8,
+ name: "A1-otmt",
+ processType: 1,
+ isOffMainThread: true,
+ submarkers: [{
+ start: 6,
+ end: 7,
+ name: "B2",
+ processType: 1,
+ isOffMainThread: false
+ }]
+ }, {
+ start: 6,
+ end: 7,
+ name: "C2",
+ processType: 1,
+ isOffMainThread: true
+ }, {
+ start: 9,
+ end: 12,
+ name: "A2-mt",
+ processType: 2,
+ isOffMainThread: false,
+ submarkers: [{
+ start: 10,
+ end: 11,
+ name: "D1",
+ processType: 2,
+ isOffMainThread: false
+ }]
+ }, {
+ start: 10,
+ end: 11,
+ name: "E1",
+ processType: 2,
+ isOffMainThread: true
+ }, {
+ start: 13,
+ end: 16,
+ name: "A2-otmt",
+ processType: 2,
+ isOffMainThread: true,
+ submarkers: [{
+ start: 14,
+ end: 15,
+ name: "D2",
+ processType: 2,
+ isOffMainThread: false
+ }]
+ }, {
+ start: 14,
+ end: 15,
+ name: "E2",
+ processType: 2,
+ isOffMainThread: true
+ }, {
+ start: 14,
+ end: 15,
+ name: "F",
+ processType: 3,
+ isOffMainThread: false,
+ submarkers: []
+ }, {
+ start: 14,
+ end: 15,
+ name: "G",
+ processType: 3,
+ isOffMainThread: true,
+ submarkers: []
+ }]
+};
diff --git a/devtools/client/performance/test/unit/xpcshell.ini b/devtools/client/performance/test/unit/xpcshell.ini
new file mode 100644
index 000000000..b9d0c1403
--- /dev/null
+++ b/devtools/client/performance/test/unit/xpcshell.ini
@@ -0,0 +1,36 @@
+[DEFAULT]
+tags = devtools
+head = head.js
+tail =
+firefox-appdir = browser
+skip-if = toolkit == 'android'
+
+[test_frame-utils-01.js]
+[test_frame-utils-02.js]
+[test_marker-blueprint.js]
+[test_marker-utils.js]
+[test_profiler-categories.js]
+[test_jit-graph-data.js]
+[test_jit-model-01.js]
+[test_jit-model-02.js]
+[test_perf-utils-allocations-to-samples.js]
+[test_tree-model-01.js]
+[test_tree-model-02.js]
+[test_tree-model-03.js]
+[test_tree-model-04.js]
+[test_tree-model-05.js]
+[test_tree-model-06.js]
+[test_tree-model-07.js]
+[test_tree-model-08.js]
+[test_tree-model-09.js]
+[test_tree-model-10.js]
+[test_tree-model-11.js]
+[test_tree-model-12.js]
+[test_tree-model-13.js]
+[test_tree-model-allocations-01.js]
+[test_tree-model-allocations-02.js]
+[test_waterfall-utils-collapse-01.js]
+[test_waterfall-utils-collapse-02.js]
+[test_waterfall-utils-collapse-03.js]
+[test_waterfall-utils-collapse-04.js]
+[test_waterfall-utils-collapse-05.js]
diff --git a/devtools/client/performance/views/details-abstract-subview.js b/devtools/client/performance/views/details-abstract-subview.js
new file mode 100644
index 000000000..86ea45366
--- /dev/null
+++ b/devtools/client/performance/views/details-abstract-subview.js
@@ -0,0 +1,194 @@
+/* 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/. */
+/* import-globals-from ../performance-controller.js */
+/* import-globals-from ../performance-view.js */
+/* exported DetailsSubview */
+"use strict";
+
+/**
+ * A base class from which all detail views inherit.
+ */
+var DetailsSubview = {
+ /**
+ * Sets up the view with event binding.
+ */
+ initialize: function () {
+ this._onRecordingStoppedOrSelected = this._onRecordingStoppedOrSelected.bind(this);
+ this._onOverviewRangeChange = this._onOverviewRangeChange.bind(this);
+ this._onDetailsViewSelected = this._onDetailsViewSelected.bind(this);
+ this._onPrefChanged = this._onPrefChanged.bind(this);
+
+ PerformanceController.on(EVENTS.RECORDING_STATE_CHANGE,
+ this._onRecordingStoppedOrSelected);
+ PerformanceController.on(EVENTS.RECORDING_SELECTED,
+ this._onRecordingStoppedOrSelected);
+ PerformanceController.on(EVENTS.PREF_CHANGED, this._onPrefChanged);
+ OverviewView.on(EVENTS.UI_OVERVIEW_RANGE_SELECTED, this._onOverviewRangeChange);
+ DetailsView.on(EVENTS.UI_DETAILS_VIEW_SELECTED, this._onDetailsViewSelected);
+
+ let self = this;
+ let originalRenderFn = this.render;
+ let afterRenderFn = () => {
+ this._wasRendered = true;
+ };
+
+ this.render = Task.async(function* (...args) {
+ let maybeRetval = yield originalRenderFn.apply(self, args);
+ afterRenderFn();
+ return maybeRetval;
+ });
+ },
+
+ /**
+ * Unbinds events.
+ */
+ destroy: function () {
+ clearNamedTimeout("range-change-debounce");
+
+ PerformanceController.off(EVENTS.RECORDING_STATE_CHANGE,
+ this._onRecordingStoppedOrSelected);
+ PerformanceController.off(EVENTS.RECORDING_SELECTED,
+ this._onRecordingStoppedOrSelected);
+ PerformanceController.off(EVENTS.PREF_CHANGED, this._onPrefChanged);
+ OverviewView.off(EVENTS.UI_OVERVIEW_RANGE_SELECTED, this._onOverviewRangeChange);
+ DetailsView.off(EVENTS.UI_DETAILS_VIEW_SELECTED, this._onDetailsViewSelected);
+ },
+
+ /**
+ * Returns true if this view was rendered at least once.
+ */
+ get wasRenderedAtLeastOnce() {
+ return !!this._wasRendered;
+ },
+
+ /**
+ * Amount of time (in milliseconds) to wait until this view gets updated,
+ * when the range is changed in the overview.
+ */
+ rangeChangeDebounceTime: 0,
+
+ /**
+ * When the overview range changes, all details views will require a
+ * rerendering at a later point, determined by `shouldUpdateWhenShown` and
+ * `canUpdateWhileHidden` and whether or not its the current view.
+ * Set `requiresUpdateOnRangeChange` to false to not invalidate the view
+ * when the range changes.
+ */
+ requiresUpdateOnRangeChange: true,
+
+ /**
+ * Flag specifying if this view should be updated when selected. This will
+ * be set to true, for example, when the range changes in the overview and
+ * this view is not currently visible.
+ */
+ shouldUpdateWhenShown: false,
+
+ /**
+ * Flag specifying if this view may get updated even when it's not selected.
+ * Should only be used in tests.
+ */
+ canUpdateWhileHidden: false,
+
+ /**
+ * An array of preferences under `devtools.performance.ui.` that the view should
+ * rerender and callback `this._onRerenderPrefChanged` upon change.
+ */
+ rerenderPrefs: [],
+
+ /**
+ * An array of preferences under `devtools.performance.` that the view should
+ * observe and callback `this._onObservedPrefChange` upon change.
+ */
+ observedPrefs: [],
+
+ /**
+ * Flag specifying if this view should update while the overview selection
+ * area is actively being dragged by the mouse.
+ */
+ shouldUpdateWhileMouseIsActive: false,
+
+ /**
+ * Called when recording stops or is selected.
+ */
+ _onRecordingStoppedOrSelected: function (_, state, recording) {
+ if (typeof state !== "string") {
+ recording = state;
+ }
+ if (arguments.length === 3 && state !== "recording-stopped") {
+ return;
+ }
+
+ if (!recording || !recording.isCompleted()) {
+ return;
+ }
+ if (DetailsView.isViewSelected(this) || this.canUpdateWhileHidden) {
+ this.render(OverviewView.getTimeInterval());
+ } else {
+ this.shouldUpdateWhenShown = true;
+ }
+ },
+
+ /**
+ * Fired when a range is selected or cleared in the OverviewView.
+ */
+ _onOverviewRangeChange: function (_, interval) {
+ if (!this.requiresUpdateOnRangeChange) {
+ return;
+ }
+ if (DetailsView.isViewSelected(this)) {
+ let debounced = () => {
+ if (!this.shouldUpdateWhileMouseIsActive && OverviewView.isMouseActive) {
+ // Don't render yet, while the selection is still being dragged.
+ setNamedTimeout("range-change-debounce", this.rangeChangeDebounceTime,
+ debounced);
+ } else {
+ this.render(interval);
+ }
+ };
+ setNamedTimeout("range-change-debounce", this.rangeChangeDebounceTime, debounced);
+ } else {
+ this.shouldUpdateWhenShown = true;
+ }
+ },
+
+ /**
+ * Fired when a view is selected in the DetailsView.
+ */
+ _onDetailsViewSelected: function () {
+ if (DetailsView.isViewSelected(this) && this.shouldUpdateWhenShown) {
+ this.render(OverviewView.getTimeInterval());
+ this.shouldUpdateWhenShown = false;
+ }
+ },
+
+ /**
+ * Fired when a preference in `devtools.performance.ui.` is changed.
+ */
+ _onPrefChanged: function (_, prefName) {
+ if (~this.observedPrefs.indexOf(prefName) && this._onObservedPrefChange) {
+ this._onObservedPrefChange(_, prefName);
+ }
+
+ // All detail views require a recording to be complete, so do not
+ // attempt to render if recording is in progress or does not exist.
+ let recording = PerformanceController.getCurrentRecording();
+ if (!recording || !recording.isCompleted()) {
+ return;
+ }
+
+ if (!~this.rerenderPrefs.indexOf(prefName)) {
+ return;
+ }
+
+ if (this._onRerenderPrefChanged) {
+ this._onRerenderPrefChanged(_, prefName);
+ }
+
+ if (DetailsView.isViewSelected(this) || this.canUpdateWhileHidden) {
+ this.render(OverviewView.getTimeInterval());
+ } else {
+ this.shouldUpdateWhenShown = true;
+ }
+ }
+};
diff --git a/devtools/client/performance/views/details-js-call-tree.js b/devtools/client/performance/views/details-js-call-tree.js
new file mode 100644
index 000000000..6c4e808af
--- /dev/null
+++ b/devtools/client/performance/views/details-js-call-tree.js
@@ -0,0 +1,193 @@
+/* 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/. */
+/* import-globals-from ../performance-controller.js */
+/* import-globals-from ../performance-view.js */
+/* globals DetailsSubview */
+"use strict";
+
+/**
+ * CallTree view containing profiler call tree, controlled by DetailsView.
+ */
+var JsCallTreeView = Heritage.extend(DetailsSubview, {
+
+ rerenderPrefs: [
+ "invert-call-tree",
+ "show-platform-data",
+ "flatten-tree-recursion",
+ "show-jit-optimizations",
+ ],
+
+ // Units are in milliseconds.
+ rangeChangeDebounceTime: 75,
+
+ /**
+ * Sets up the view with event binding.
+ */
+ initialize: function () {
+ DetailsSubview.initialize.call(this);
+
+ this._onLink = this._onLink.bind(this);
+ this._onFocus = this._onFocus.bind(this);
+
+ this.container = $("#js-calltree-view .call-tree-cells-container");
+
+ this.optimizationsElement = $("#jit-optimizations-view");
+ },
+
+ /**
+ * Unbinds events.
+ */
+ destroy: function () {
+ ReactDOM.unmountComponentAtNode(this.optimizationsElement);
+ this.optimizationsElement = null;
+ this.container = null;
+ this.threadNode = null;
+ DetailsSubview.destroy.call(this);
+ },
+
+ /**
+ * Method for handling all the set up for rendering a new call tree.
+ *
+ * @param object interval [optional]
+ * The { startTime, endTime }, in milliseconds.
+ */
+ render: function (interval = {}) {
+ let recording = PerformanceController.getCurrentRecording();
+ let profile = recording.getProfile();
+ let showOptimizations = PerformanceController.getOption("show-jit-optimizations");
+
+ let options = {
+ contentOnly: !PerformanceController.getOption("show-platform-data"),
+ invertTree: PerformanceController.getOption("invert-call-tree"),
+ flattenRecursion: PerformanceController.getOption("flatten-tree-recursion"),
+ showOptimizationHint: showOptimizations
+ };
+ let threadNode = this.threadNode = this._prepareCallTree(profile, interval, options);
+ this._populateCallTree(threadNode, options);
+
+ // For better or worse, re-rendering loses frame selection,
+ // so we should always hide opts on rerender
+ this.hideOptimizations();
+
+ this.emit(EVENTS.UI_JS_CALL_TREE_RENDERED);
+ },
+
+ showOptimizations: function () {
+ this.optimizationsElement.classList.remove("hidden");
+ },
+
+ hideOptimizations: function () {
+ this.optimizationsElement.classList.add("hidden");
+ },
+
+ _onFocus: function (_, treeItem) {
+ let showOptimizations = PerformanceController.getOption("show-jit-optimizations");
+ let frameNode = treeItem.frame;
+ let optimizationSites = frameNode && frameNode.hasOptimizations()
+ ? frameNode.getOptimizations().optimizationSites
+ : [];
+
+ if (!showOptimizations || !frameNode || optimizationSites.length === 0) {
+ this.hideOptimizations();
+ this.emit("focus", treeItem);
+ return;
+ }
+
+ this.showOptimizations();
+
+ let frameData = frameNode.getInfo();
+ let optimizations = JITOptimizationsView({
+ frameData,
+ optimizationSites,
+ onViewSourceInDebugger: (url, line) => {
+ gToolbox.viewSourceInDebugger(url, line).then(success => {
+ if (success) {
+ this.emit(EVENTS.SOURCE_SHOWN_IN_JS_DEBUGGER);
+ } else {
+ this.emit(EVENTS.SOURCE_NOT_FOUND_IN_JS_DEBUGGER);
+ }
+ });
+ }
+ });
+
+ ReactDOM.render(optimizations, this.optimizationsElement);
+
+ this.emit("focus", treeItem);
+ },
+
+ /**
+ * Fired on the "link" event for the call tree in this container.
+ */
+ _onLink: function (_, treeItem) {
+ let { url, line } = treeItem.frame.getInfo();
+ gToolbox.viewSourceInDebugger(url, line).then(success => {
+ if (success) {
+ this.emit(EVENTS.SOURCE_SHOWN_IN_JS_DEBUGGER);
+ } else {
+ this.emit(EVENTS.SOURCE_NOT_FOUND_IN_JS_DEBUGGER);
+ }
+ });
+ },
+
+ /**
+ * Called when the recording is stopped and prepares data to
+ * populate the call tree.
+ */
+ _prepareCallTree: function (profile, { startTime, endTime }, options) {
+ let thread = profile.threads[0];
+ let { contentOnly, invertTree, flattenRecursion } = options;
+ let threadNode = new ThreadNode(thread, { startTime, endTime, contentOnly, invertTree,
+ flattenRecursion });
+
+ // Real profiles from nsProfiler (i.e. not synthesized from allocation
+ // logs) always have a (root) node. Go down one level in the uninverted
+ // view to avoid displaying both the synthesized root node and the (root)
+ // node from the profiler.
+ if (!invertTree) {
+ threadNode.calls = threadNode.calls[0].calls;
+ }
+
+ return threadNode;
+ },
+
+ /**
+ * Renders the call tree.
+ */
+ _populateCallTree: function (frameNode, options = {}) {
+ // If we have an empty profile (no samples), then don't invert the tree, as
+ // it would hide the root node and a completely blank call tree space can be
+ // mis-interpreted as an error.
+ let inverted = options.invertTree && frameNode.samples > 0;
+
+ let root = new CallView({
+ frame: frameNode,
+ inverted: inverted,
+ // The synthesized root node is hidden in inverted call trees.
+ hidden: inverted,
+ // Call trees should only auto-expand when not inverted. Passing undefined
+ // will default to the CALL_TREE_AUTO_EXPAND depth.
+ autoExpandDepth: inverted ? 0 : undefined,
+ showOptimizationHint: options.showOptimizationHint
+ });
+
+ // Bind events.
+ root.on("link", this._onLink);
+ root.on("focus", this._onFocus);
+
+ // Clear out other call trees.
+ this.container.innerHTML = "";
+ root.attachTo(this.container);
+
+ // When platform data isn't shown, hide the cateogry labels, since they're
+ // only available for C++ frames. Pass *false* to make them invisible.
+ root.toggleCategories(!options.contentOnly);
+
+ // Return the CallView for tests
+ return root;
+ },
+
+ toString: () => "[object JsCallTreeView]"
+});
+
+EventEmitter.decorate(JsCallTreeView);
diff --git a/devtools/client/performance/views/details-js-flamegraph.js b/devtools/client/performance/views/details-js-flamegraph.js
new file mode 100644
index 000000000..0aca21252
--- /dev/null
+++ b/devtools/client/performance/views/details-js-flamegraph.js
@@ -0,0 +1,125 @@
+/* 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/. */
+/* import-globals-from ../performance-controller.js */
+/* import-globals-from ../performance-view.js */
+/* globals DetailsSubview */
+"use strict";
+
+/**
+ * FlameGraph view containing a pyramid-like visualization of a profile,
+ * controlled by DetailsView.
+ */
+var JsFlameGraphView = Heritage.extend(DetailsSubview, {
+
+ shouldUpdateWhileMouseIsActive: true,
+
+ rerenderPrefs: [
+ "invert-flame-graph",
+ "flatten-tree-recursion",
+ "show-platform-data",
+ "show-idle-blocks"
+ ],
+
+ /**
+ * Sets up the view with event binding.
+ */
+ initialize: Task.async(function* () {
+ DetailsSubview.initialize.call(this);
+
+ this.graph = new FlameGraph($("#js-flamegraph-view"));
+ this.graph.timelineTickUnits = L10N.getStr("graphs.ms");
+ this.graph.setTheme(PerformanceController.getTheme());
+ yield this.graph.ready();
+
+ this._onRangeChangeInGraph = this._onRangeChangeInGraph.bind(this);
+ this._onThemeChanged = this._onThemeChanged.bind(this);
+
+ PerformanceController.on(EVENTS.THEME_CHANGED, this._onThemeChanged);
+ this.graph.on("selecting", this._onRangeChangeInGraph);
+ }),
+
+ /**
+ * Unbinds events.
+ */
+ destroy: Task.async(function* () {
+ DetailsSubview.destroy.call(this);
+
+ PerformanceController.off(EVENTS.THEME_CHANGED, this._onThemeChanged);
+ this.graph.off("selecting", this._onRangeChangeInGraph);
+
+ yield this.graph.destroy();
+ }),
+
+ /**
+ * Method for handling all the set up for rendering a new flamegraph.
+ *
+ * @param object interval [optional]
+ * The { startTime, endTime }, in milliseconds.
+ */
+ render: function (interval = {}) {
+ let recording = PerformanceController.getCurrentRecording();
+ let duration = recording.getDuration();
+ let profile = recording.getProfile();
+ let thread = profile.threads[0];
+
+ let data = FlameGraphUtils.createFlameGraphDataFromThread(thread, {
+ invertTree: PerformanceController.getOption("invert-flame-graph"),
+ flattenRecursion: PerformanceController.getOption("flatten-tree-recursion"),
+ contentOnly: !PerformanceController.getOption("show-platform-data"),
+ showIdleBlocks: PerformanceController.getOption("show-idle-blocks")
+ && L10N.getStr("table.idle")
+ });
+
+ this.graph.setData({ data,
+ bounds: {
+ startTime: 0,
+ endTime: duration
+ },
+ visible: {
+ startTime: interval.startTime || 0,
+ endTime: interval.endTime || duration
+ }
+ });
+
+ this.graph.focus();
+
+ this.emit(EVENTS.UI_JS_FLAMEGRAPH_RENDERED);
+ },
+
+ /**
+ * Fired when a range is selected or cleared in the FlameGraph.
+ */
+ _onRangeChangeInGraph: function () {
+ let interval = this.graph.getViewRange();
+
+ // Squelch rerendering this view when we update the range here
+ // to avoid recursion, as our FlameGraph handles rerendering itself
+ // when originating from within the graph.
+ this.requiresUpdateOnRangeChange = false;
+ OverviewView.setTimeInterval(interval);
+ this.requiresUpdateOnRangeChange = true;
+ },
+
+ /**
+ * Called whenever a pref is changed and this view needs to be rerendered.
+ */
+ _onRerenderPrefChanged: function () {
+ let recording = PerformanceController.getCurrentRecording();
+ let profile = recording.getProfile();
+ let thread = profile.threads[0];
+ FlameGraphUtils.removeFromCache(thread);
+ },
+
+ /**
+ * Called when `devtools.theme` changes.
+ */
+ _onThemeChanged: function (_, theme) {
+ this.graph.setTheme(theme);
+ this.graph.refresh({ force: true });
+ },
+
+ toString: () => "[object JsFlameGraphView]"
+});
+
+EventEmitter.decorate(JsFlameGraphView);
diff --git a/devtools/client/performance/views/details-memory-call-tree.js b/devtools/client/performance/views/details-memory-call-tree.js
new file mode 100644
index 000000000..883d92e63
--- /dev/null
+++ b/devtools/client/performance/views/details-memory-call-tree.js
@@ -0,0 +1,130 @@
+/* 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/. */
+/* import-globals-from ../performance-controller.js */
+/* import-globals-from ../performance-view.js */
+/* globals DetailsSubview */
+"use strict";
+
+/**
+ * CallTree view containing memory allocation sites, controlled by DetailsView.
+ */
+var MemoryCallTreeView = Heritage.extend(DetailsSubview, {
+
+ rerenderPrefs: [
+ "invert-call-tree"
+ ],
+
+ // Units are in milliseconds.
+ rangeChangeDebounceTime: 100,
+
+ /**
+ * Sets up the view with event binding.
+ */
+ initialize: function () {
+ DetailsSubview.initialize.call(this);
+
+ this._onLink = this._onLink.bind(this);
+
+ this.container = $("#memory-calltree-view > .call-tree-cells-container");
+ },
+
+ /**
+ * Unbinds events.
+ */
+ destroy: function () {
+ DetailsSubview.destroy.call(this);
+ },
+
+ /**
+ * Method for handling all the set up for rendering a new call tree.
+ *
+ * @param object interval [optional]
+ * The { startTime, endTime }, in milliseconds.
+ */
+ render: function (interval = {}) {
+ let options = {
+ invertTree: PerformanceController.getOption("invert-call-tree")
+ };
+ let recording = PerformanceController.getCurrentRecording();
+ let allocations = recording.getAllocations();
+ let threadNode = this._prepareCallTree(allocations, interval, options);
+ this._populateCallTree(threadNode, options);
+ this.emit(EVENTS.UI_MEMORY_CALL_TREE_RENDERED);
+ },
+
+ /**
+ * Fired on the "link" event for the call tree in this container.
+ */
+ _onLink: function (_, treeItem) {
+ let { url, line } = treeItem.frame.getInfo();
+ gToolbox.viewSourceInDebugger(url, line).then(success => {
+ if (success) {
+ this.emit(EVENTS.SOURCE_SHOWN_IN_JS_DEBUGGER);
+ } else {
+ this.emit(EVENTS.SOURCE_NOT_FOUND_IN_JS_DEBUGGER);
+ }
+ });
+ },
+
+ /**
+ * Called when the recording is stopped and prepares data to
+ * populate the call tree.
+ */
+ _prepareCallTree: function (allocations, { startTime, endTime }, options) {
+ let thread = RecordingUtils.getProfileThreadFromAllocations(allocations);
+ let { invertTree } = options;
+
+ return new ThreadNode(thread, { startTime, endTime, invertTree });
+ },
+
+ /**
+ * Renders the call tree.
+ */
+ _populateCallTree: function (frameNode, options = {}) {
+ // If we have an empty profile (no samples), then don't invert the tree, as
+ // it would hide the root node and a completely blank call tree space can be
+ // mis-interpreted as an error.
+ let inverted = options.invertTree && frameNode.samples > 0;
+
+ let root = new CallView({
+ frame: frameNode,
+ inverted: inverted,
+ // Root nodes are hidden in inverted call trees.
+ hidden: inverted,
+ // Call trees should only auto-expand when not inverted. Passing undefined
+ // will default to the CALL_TREE_AUTO_EXPAND depth.
+ autoExpandDepth: inverted ? 0 : undefined,
+ // Some cells like the time duration and cost percentage don't make sense
+ // for a memory allocations call tree.
+ visibleCells: {
+ selfCount: true,
+ count: true,
+ selfSize: true,
+ size: true,
+ selfCountPercentage: true,
+ countPercentage: true,
+ selfSizePercentage: true,
+ sizePercentage: true,
+ function: true
+ }
+ });
+
+ // Bind events.
+ root.on("link", this._onLink);
+
+ // Pipe "focus" events to the view, mostly for tests
+ root.on("focus", () => this.emit("focus"));
+
+ // Clear out other call trees.
+ this.container.innerHTML = "";
+ root.attachTo(this.container);
+
+ // Memory allocation samples don't contain cateogry labels.
+ root.toggleCategories(false);
+ },
+
+ toString: () => "[object MemoryCallTreeView]"
+});
+
+EventEmitter.decorate(MemoryCallTreeView);
diff --git a/devtools/client/performance/views/details-memory-flamegraph.js b/devtools/client/performance/views/details-memory-flamegraph.js
new file mode 100644
index 000000000..70eaa3c7a
--- /dev/null
+++ b/devtools/client/performance/views/details-memory-flamegraph.js
@@ -0,0 +1,121 @@
+/* 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/. */
+/* import-globals-from ../performance-controller.js */
+/* import-globals-from ../performance-view.js */
+/* globals DetailsSubview */
+"use strict";
+
+/**
+ * FlameGraph view containing a pyramid-like visualization of memory allocation
+ * sites, controlled by DetailsView.
+ */
+var MemoryFlameGraphView = Heritage.extend(DetailsSubview, {
+
+ shouldUpdateWhileMouseIsActive: true,
+
+ rerenderPrefs: [
+ "invert-flame-graph",
+ "flatten-tree-recursion",
+ "show-idle-blocks"
+ ],
+
+ /**
+ * Sets up the view with event binding.
+ */
+ initialize: Task.async(function* () {
+ DetailsSubview.initialize.call(this);
+
+ this.graph = new FlameGraph($("#memory-flamegraph-view"));
+ this.graph.timelineTickUnits = L10N.getStr("graphs.ms");
+ this.graph.setTheme(PerformanceController.getTheme());
+ yield this.graph.ready();
+
+ this._onRangeChangeInGraph = this._onRangeChangeInGraph.bind(this);
+ this._onThemeChanged = this._onThemeChanged.bind(this);
+
+ PerformanceController.on(EVENTS.THEME_CHANGED, this._onThemeChanged);
+ this.graph.on("selecting", this._onRangeChangeInGraph);
+ }),
+
+ /**
+ * Unbinds events.
+ */
+ destroy: Task.async(function* () {
+ DetailsSubview.destroy.call(this);
+
+ PerformanceController.off(EVENTS.THEME_CHANGED, this._onThemeChanged);
+ this.graph.off("selecting", this._onRangeChangeInGraph);
+
+ yield this.graph.destroy();
+ }),
+
+ /**
+ * Method for handling all the set up for rendering a new flamegraph.
+ *
+ * @param object interval [optional]
+ * The { startTime, endTime }, in milliseconds.
+ */
+ render: function (interval = {}) {
+ let recording = PerformanceController.getCurrentRecording();
+ let duration = recording.getDuration();
+ let allocations = recording.getAllocations();
+
+ let thread = RecordingUtils.getProfileThreadFromAllocations(allocations);
+ let data = FlameGraphUtils.createFlameGraphDataFromThread(thread, {
+ invertStack: PerformanceController.getOption("invert-flame-graph"),
+ flattenRecursion: PerformanceController.getOption("flatten-tree-recursion"),
+ showIdleBlocks: PerformanceController.getOption("show-idle-blocks")
+ && L10N.getStr("table.idle")
+ });
+
+ this.graph.setData({ data,
+ bounds: {
+ startTime: 0,
+ endTime: duration
+ },
+ visible: {
+ startTime: interval.startTime || 0,
+ endTime: interval.endTime || duration
+ }
+ });
+
+ this.emit(EVENTS.UI_MEMORY_FLAMEGRAPH_RENDERED);
+ },
+
+ /**
+ * Fired when a range is selected or cleared in the FlameGraph.
+ */
+ _onRangeChangeInGraph: function () {
+ let interval = this.graph.getViewRange();
+
+ // Squelch rerendering this view when we update the range here
+ // to avoid recursion, as our FlameGraph handles rerendering itself
+ // when originating from within the graph.
+ this.requiresUpdateOnRangeChange = false;
+ OverviewView.setTimeInterval(interval);
+ this.requiresUpdateOnRangeChange = true;
+ },
+
+ /**
+ * Called whenever a pref is changed and this view needs to be rerendered.
+ */
+ _onRerenderPrefChanged: function () {
+ let recording = PerformanceController.getCurrentRecording();
+ let allocations = recording.getAllocations();
+ let thread = RecordingUtils.getProfileThreadFromAllocations(allocations);
+ FlameGraphUtils.removeFromCache(thread);
+ },
+
+ /**
+ * Called when `devtools.theme` changes.
+ */
+ _onThemeChanged: function (_, theme) {
+ this.graph.setTheme(theme);
+ this.graph.refresh({ force: true });
+ },
+
+ toString: () => "[object MemoryFlameGraphView]"
+});
+
+EventEmitter.decorate(MemoryFlameGraphView);
diff --git a/devtools/client/performance/views/details-waterfall.js b/devtools/client/performance/views/details-waterfall.js
new file mode 100644
index 000000000..db8def053
--- /dev/null
+++ b/devtools/client/performance/views/details-waterfall.js
@@ -0,0 +1,252 @@
+/* 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/. */
+/* import-globals-from ../performance-controller.js */
+/* import-globals-from ../performance-view.js */
+/* globals window, DetailsSubview */
+"use strict";
+
+const MARKER_DETAILS_WIDTH = 200;
+// Units are in milliseconds.
+const WATERFALL_RESIZE_EVENTS_DRAIN = 100;
+
+const { TickUtils } = require("devtools/client/performance/modules/waterfall-ticks");
+
+/**
+ * Waterfall view containing the timeline markers, controlled by DetailsView.
+ */
+var WaterfallView = Heritage.extend(DetailsSubview, {
+
+ // Smallest unit of time between two markers. Larger by 10x^3 than Number.EPSILON.
+ MARKER_EPSILON: 0.000000000001,
+ // px
+ WATERFALL_MARKER_SIDEBAR_WIDTH: 175,
+ // px
+ WATERFALL_MARKER_SIDEBAR_SAFE_BOUNDS: 20,
+
+ observedPrefs: [
+ "hidden-markers"
+ ],
+
+ rerenderPrefs: [
+ "hidden-markers"
+ ],
+
+ // Units are in milliseconds.
+ rangeChangeDebounceTime: 75,
+
+ /**
+ * Sets up the view with event binding.
+ */
+ initialize: function () {
+ DetailsSubview.initialize.call(this);
+
+ this._cache = new WeakMap();
+
+ this._onMarkerSelected = this._onMarkerSelected.bind(this);
+ this._onResize = this._onResize.bind(this);
+ this._onViewSource = this._onViewSource.bind(this);
+ this._onShowAllocations = this._onShowAllocations.bind(this);
+ this._hiddenMarkers = PerformanceController.getPref("hidden-markers");
+
+ this.treeContainer = $("#waterfall-tree");
+ this.detailsContainer = $("#waterfall-details");
+ this.detailsSplitter = $("#waterfall-view > splitter");
+
+ this.details = new MarkerDetails($("#waterfall-details"),
+ $("#waterfall-view > splitter"));
+ this.details.hidden = true;
+
+ this.details.on("resize", this._onResize);
+ this.details.on("view-source", this._onViewSource);
+ this.details.on("show-allocations", this._onShowAllocations);
+ window.addEventListener("resize", this._onResize);
+
+ // TODO bug 1167093 save the previously set width, and ensure minimum width
+ this.details.width = MARKER_DETAILS_WIDTH;
+ },
+
+ /**
+ * Unbinds events.
+ */
+ destroy: function () {
+ DetailsSubview.destroy.call(this);
+
+ clearNamedTimeout("waterfall-resize");
+
+ this._cache = null;
+
+ this.details.off("resize", this._onResize);
+ this.details.off("view-source", this._onViewSource);
+ this.details.off("show-allocations", this._onShowAllocations);
+ window.removeEventListener("resize", this._onResize);
+
+ ReactDOM.unmountComponentAtNode(this.treeContainer);
+ },
+
+ /**
+ * Method for handling all the set up for rendering a new waterfall.
+ *
+ * @param object interval [optional]
+ * The { startTime, endTime }, in milliseconds.
+ */
+ render: function (interval = {}) {
+ let recording = PerformanceController.getCurrentRecording();
+ if (recording.isRecording()) {
+ return;
+ }
+ let startTime = interval.startTime || 0;
+ let endTime = interval.endTime || recording.getDuration();
+ let markers = recording.getMarkers();
+ let rootMarkerNode = this._prepareWaterfallTree(markers);
+
+ this._populateWaterfallTree(rootMarkerNode, { startTime, endTime });
+ this.emit(EVENTS.UI_WATERFALL_RENDERED);
+ },
+
+ /**
+ * Called when a marker is selected in the waterfall view,
+ * updating the markers detail view.
+ */
+ _onMarkerSelected: function (event, marker) {
+ let recording = PerformanceController.getCurrentRecording();
+ let frames = recording.getFrames();
+ let allocations = recording.getConfiguration().withAllocations;
+
+ if (event === "selected") {
+ this.details.render({ marker, frames, allocations });
+ this.details.hidden = false;
+ }
+ if (event === "unselected") {
+ this.details.empty();
+ }
+ },
+
+ /**
+ * Called when the marker details view is resized.
+ */
+ _onResize: function () {
+ setNamedTimeout("waterfall-resize", WATERFALL_RESIZE_EVENTS_DRAIN, () => {
+ this.render(OverviewView.getTimeInterval());
+ });
+ },
+
+ /**
+ * Called whenever an observed pref is changed.
+ */
+ _onObservedPrefChange: function (_, prefName) {
+ this._hiddenMarkers = PerformanceController.getPref("hidden-markers");
+
+ // Clear the cache as we'll need to recompute the collapsed
+ // marker model
+ this._cache = new WeakMap();
+ },
+
+ /**
+ * Called when MarkerDetails view emits an event to view source.
+ */
+ _onViewSource: function (_, data) {
+ gToolbox.viewSourceInDebugger(data.url, data.line);
+ },
+
+ /**
+ * Called when MarkerDetails view emits an event to snap to allocations.
+ */
+ _onShowAllocations: function (_, data) {
+ let { endTime } = data;
+ let startTime = 0;
+ let recording = PerformanceController.getCurrentRecording();
+ let markers = recording.getMarkers();
+
+ let lastGCMarkerFromPreviousCycle = null;
+ let lastGCMarker = null;
+ // Iterate over markers looking for the most recent GC marker
+ // from the cycle before the marker's whose allocations we're interested in.
+ for (let marker of markers) {
+ // We found the marker whose allocations we're tracking; abort
+ if (marker.start === endTime) {
+ break;
+ }
+
+ if (marker.name === "GarbageCollection") {
+ if (lastGCMarker && lastGCMarker.cycle !== marker.cycle) {
+ lastGCMarkerFromPreviousCycle = lastGCMarker;
+ }
+ lastGCMarker = marker;
+ }
+ }
+
+ if (lastGCMarkerFromPreviousCycle) {
+ startTime = lastGCMarkerFromPreviousCycle.end;
+ }
+
+ // Adjust times so we don't include the range of these markers themselves.
+ endTime -= this.MARKER_EPSILON;
+ startTime += startTime !== 0 ? this.MARKER_EPSILON : 0;
+
+ OverviewView.setTimeInterval({ startTime, endTime });
+ DetailsView.selectView("memory-calltree");
+ },
+
+ /**
+ * Called when the recording is stopped and prepares data to
+ * populate the waterfall tree.
+ */
+ _prepareWaterfallTree: function (markers) {
+ let cached = this._cache.get(markers);
+ if (cached) {
+ return cached;
+ }
+
+ let rootMarkerNode = WaterfallUtils.createParentNode({ name: "(root)" });
+
+ WaterfallUtils.collapseMarkersIntoNode({
+ rootNode: rootMarkerNode,
+ markersList: markers,
+ filter: this._hiddenMarkers
+ });
+
+ this._cache.set(markers, rootMarkerNode);
+ return rootMarkerNode;
+ },
+
+ /**
+ * Calculates the available width for the waterfall.
+ * This should be invoked every time the container node is resized.
+ */
+ _recalculateBounds: function () {
+ this.waterfallWidth = this.treeContainer.clientWidth
+ - this.WATERFALL_MARKER_SIDEBAR_WIDTH
+ - this.WATERFALL_MARKER_SIDEBAR_SAFE_BOUNDS;
+ },
+
+ /**
+ * Renders the waterfall tree.
+ */
+ _populateWaterfallTree: function (rootMarkerNode, interval) {
+ this._recalculateBounds();
+
+ let doc = this.treeContainer.ownerDocument;
+ let startTime = interval.startTime | 0;
+ let endTime = interval.endTime | 0;
+ let dataScale = this.waterfallWidth / (endTime - startTime);
+
+ this.canvas = TickUtils.drawWaterfallBackground(doc, dataScale, this.waterfallWidth);
+
+ let treeView = Waterfall({
+ marker: rootMarkerNode,
+ startTime,
+ endTime,
+ dataScale,
+ sidebarWidth: this.WATERFALL_MARKER_SIDEBAR_WIDTH,
+ waterfallWidth: this.waterfallWidth,
+ onFocus: node => this._onMarkerSelected("selected", node)
+ });
+
+ ReactDOM.render(treeView, this.treeContainer);
+ },
+
+ toString: () => "[object WaterfallView]"
+});
+
+EventEmitter.decorate(WaterfallView);
diff --git a/devtools/client/performance/views/details.js b/devtools/client/performance/views/details.js
new file mode 100644
index 000000000..95557bc36
--- /dev/null
+++ b/devtools/client/performance/views/details.js
@@ -0,0 +1,263 @@
+/* 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/. */
+/* import-globals-from ../performance-controller.js */
+/* import-globals-from ../performance-view.js */
+/* globals WaterfallView, JsCallTreeView, JsFlameGraphView, MemoryCallTreeView,
+ MemoryFlameGraphView */
+"use strict";
+
+/**
+ * Details view containing call trees, flamegraphs and markers waterfall.
+ * Manages subviews and toggles visibility between them.
+ */
+var DetailsView = {
+ /**
+ * Name to (node id, view object, actor requirements, pref killswitch)
+ * mapping of subviews.
+ */
+ components: {
+ "waterfall": {
+ id: "waterfall-view",
+ view: WaterfallView,
+ features: ["withMarkers"]
+ },
+ "js-calltree": {
+ id: "js-profile-view",
+ view: JsCallTreeView
+ },
+ "js-flamegraph": {
+ id: "js-flamegraph-view",
+ view: JsFlameGraphView,
+ },
+ "memory-calltree": {
+ id: "memory-calltree-view",
+ view: MemoryCallTreeView,
+ features: ["withAllocations"]
+ },
+ "memory-flamegraph": {
+ id: "memory-flamegraph-view",
+ view: MemoryFlameGraphView,
+ features: ["withAllocations"],
+ prefs: ["enable-memory-flame"],
+ },
+ },
+
+ /**
+ * Sets up the view with event binding, initializes subviews.
+ */
+ initialize: Task.async(function* () {
+ this.el = $("#details-pane");
+ this.toolbar = $("#performance-toolbar-controls-detail-views");
+
+ this._onViewToggle = this._onViewToggle.bind(this);
+ this._onRecordingStoppedOrSelected = this._onRecordingStoppedOrSelected.bind(this);
+ this.setAvailableViews = this.setAvailableViews.bind(this);
+
+ for (let button of $$("toolbarbutton[data-view]", this.toolbar)) {
+ button.addEventListener("command", this._onViewToggle);
+ }
+
+ yield this.setAvailableViews();
+
+ PerformanceController.on(EVENTS.RECORDING_STATE_CHANGE,
+ this._onRecordingStoppedOrSelected);
+ PerformanceController.on(EVENTS.RECORDING_SELECTED,
+ this._onRecordingStoppedOrSelected);
+ PerformanceController.on(EVENTS.PREF_CHANGED, this.setAvailableViews);
+ }),
+
+ /**
+ * Unbinds events, destroys subviews.
+ */
+ destroy: Task.async(function* () {
+ for (let button of $$("toolbarbutton[data-view]", this.toolbar)) {
+ button.removeEventListener("command", this._onViewToggle);
+ }
+
+ for (let component of Object.values(this.components)) {
+ component.initialized && (yield component.view.destroy());
+ }
+
+ PerformanceController.off(EVENTS.RECORDING_STATE_CHANGE,
+ this._onRecordingStoppedOrSelected);
+ PerformanceController.off(EVENTS.RECORDING_SELECTED,
+ this._onRecordingStoppedOrSelected);
+ PerformanceController.off(EVENTS.PREF_CHANGED, this.setAvailableViews);
+ }),
+
+ /**
+ * Sets the possible views based off of recording features and server actor support
+ * by hiding/showing the buttons that select them and going to default view
+ * if currently selected. Called when a preference changes in
+ * `devtools.performance.ui.`.
+ */
+ setAvailableViews: Task.async(function* () {
+ let recording = PerformanceController.getCurrentRecording();
+ let isCompleted = recording && recording.isCompleted();
+ let invalidCurrentView = false;
+
+ for (let [name, { view }] of Object.entries(this.components)) {
+ let isSupported = this._isViewSupported(name);
+
+ $(`toolbarbutton[data-view=${name}]`).hidden = !isSupported;
+
+ // If the view is currently selected and not supported, go back to the
+ // default view.
+ if (!isSupported && this.isViewSelected(view)) {
+ invalidCurrentView = true;
+ }
+ }
+
+ // Two scenarios in which we select the default view.
+ //
+ // 1: If we currently have selected a view that is no longer valid due
+ // to feature support, and this isn't the first view, and the current recording
+ // is completed.
+ //
+ // 2. If we have a finished recording and no panel was selected yet,
+ // use a default now that we have the recording configurations
+ if ((this._initialized && isCompleted && invalidCurrentView) ||
+ (!this._initialized && isCompleted && recording)) {
+ yield this.selectDefaultView();
+ }
+ }),
+
+ /**
+ * Takes a view name and determines if the current recording
+ * can support the view.
+ *
+ * @param {string} viewName
+ * @return {boolean}
+ */
+ _isViewSupported: function (viewName) {
+ let { features, prefs } = this.components[viewName];
+ let recording = PerformanceController.getCurrentRecording();
+
+ if (!recording || !recording.isCompleted()) {
+ return false;
+ }
+
+ let prefSupported = (prefs && prefs.length) ?
+ prefs.every(p => PerformanceController.getPref(p)) :
+ true;
+ return PerformanceController.isFeatureSupported(features) && prefSupported;
+ },
+
+ /**
+ * Select one of the DetailView's subviews to be rendered,
+ * hiding the others.
+ *
+ * @param String viewName
+ * Name of the view to be shown.
+ */
+ selectView: Task.async(function* (viewName) {
+ let component = this.components[viewName];
+ this.el.selectedPanel = $("#" + component.id);
+
+ yield this._whenViewInitialized(component);
+
+ for (let button of $$("toolbarbutton[data-view]", this.toolbar)) {
+ if (button.getAttribute("data-view") === viewName) {
+ button.setAttribute("checked", true);
+ } else {
+ button.removeAttribute("checked");
+ }
+ }
+
+ // Set a flag indicating that a view was explicitly set based on a
+ // recording's features.
+ this._initialized = true;
+
+ this.emit(EVENTS.UI_DETAILS_VIEW_SELECTED, viewName);
+ }),
+
+ /**
+ * Selects a default view based off of protocol support
+ * and preferences enabled.
+ */
+ selectDefaultView: function () {
+ // We want the waterfall to be default view in almost all cases, except when
+ // timeline actor isn't supported, or we have markers disabled (which should only
+ // occur temporarily via bug 1156499
+ if (this._isViewSupported("waterfall")) {
+ return this.selectView("waterfall");
+ }
+ // The JS CallTree should always be supported since the profiler
+ // actor is as old as the world.
+ return this.selectView("js-calltree");
+ },
+
+ /**
+ * Checks if the provided view is currently selected.
+ *
+ * @param object viewObject
+ * @return boolean
+ */
+ isViewSelected: function (viewObject) {
+ // If not initialized, and we have no recordings,
+ // no views are selected (even though there's a selected panel)
+ if (!this._initialized) {
+ return false;
+ }
+
+ let selectedPanel = this.el.selectedPanel;
+ let selectedId = selectedPanel.id;
+
+ for (let { id, view } of Object.values(this.components)) {
+ if (id == selectedId && view == viewObject) {
+ return true;
+ }
+ }
+
+ return false;
+ },
+
+ /**
+ * Initializes a subview if it wasn't already set up, and makes sure
+ * it's populated with recording data if there is some available.
+ *
+ * @param object component
+ * A component descriptor from DetailsView.components
+ */
+ _whenViewInitialized: Task.async(function* (component) {
+ if (component.initialized) {
+ return;
+ }
+ component.initialized = true;
+ yield component.view.initialize();
+
+ // If this view is initialized *after* a recording is shown, it won't display
+ // any data. Make sure it's populated by setting `shouldUpdateWhenShown`.
+ // All detail views require a recording to be complete, so do not
+ // attempt to render if recording is in progress or does not exist.
+ let recording = PerformanceController.getCurrentRecording();
+ if (recording && recording.isCompleted()) {
+ component.view.shouldUpdateWhenShown = true;
+ }
+ }),
+
+ /**
+ * Called when recording stops or is selected.
+ */
+ _onRecordingStoppedOrSelected: function (_, state, recording) {
+ if (typeof state === "string" && state !== "recording-stopped") {
+ return;
+ }
+ this.setAvailableViews();
+ },
+
+ /**
+ * Called when a view button is clicked.
+ */
+ _onViewToggle: function (e) {
+ this.selectView(e.target.getAttribute("data-view"));
+ },
+
+ toString: () => "[object DetailsView]"
+};
+
+/**
+ * Convenient way of emitting events from the view.
+ */
+EventEmitter.decorate(DetailsView);
diff --git a/devtools/client/performance/views/overview.js b/devtools/client/performance/views/overview.js
new file mode 100644
index 000000000..f45a6d844
--- /dev/null
+++ b/devtools/client/performance/views/overview.js
@@ -0,0 +1,423 @@
+/* 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/. */
+/* import-globals-from ../performance-controller.js */
+/* import-globals-from ../performance-view.js */
+"use strict";
+
+// No sense updating the overview more often than receiving data from the
+// backend. Make sure this isn't lower than DEFAULT_TIMELINE_DATA_PULL_TIMEOUT
+// in devtools/server/actors/timeline.js
+
+// The following units are in milliseconds.
+const OVERVIEW_UPDATE_INTERVAL = 200;
+const FRAMERATE_GRAPH_LOW_RES_INTERVAL = 100;
+const FRAMERATE_GRAPH_HIGH_RES_INTERVAL = 16;
+const GRAPH_REQUIREMENTS = {
+ timeline: {
+ features: ["withMarkers"]
+ },
+ framerate: {
+ features: ["withTicks"]
+ },
+ memory: {
+ features: ["withMemory"]
+ },
+};
+
+/**
+ * View handler for the overview panel's time view, displaying
+ * framerate, timeline and memory over time.
+ */
+var OverviewView = {
+
+ /**
+ * How frequently we attempt to render the graphs. Overridden
+ * in tests.
+ */
+ OVERVIEW_UPDATE_INTERVAL: OVERVIEW_UPDATE_INTERVAL,
+
+ /**
+ * Sets up the view with event binding.
+ */
+ initialize: function () {
+ this.graphs = new GraphsController({
+ root: $("#overview-pane"),
+ getFilter: () => PerformanceController.getPref("hidden-markers"),
+ getTheme: () => PerformanceController.getTheme(),
+ });
+
+ // If no timeline support, shut it all down.
+ if (!PerformanceController.getTraits().features.withMarkers) {
+ this.disable();
+ return;
+ }
+
+ // Store info on multiprocess support.
+ this._multiprocessData = PerformanceController.getMultiprocessStatus();
+
+ this._onRecordingStateChange = this._onRecordingStateChange.bind(this);
+ this._onRecordingSelected = this._onRecordingSelected.bind(this);
+ this._onRecordingTick = this._onRecordingTick.bind(this);
+ this._onGraphSelecting = this._onGraphSelecting.bind(this);
+ this._onGraphRendered = this._onGraphRendered.bind(this);
+ this._onPrefChanged = this._onPrefChanged.bind(this);
+ this._onThemeChanged = this._onThemeChanged.bind(this);
+
+ // Toggle the initial visibility of memory and framerate graph containers
+ // based off of prefs.
+ PerformanceController.on(EVENTS.PREF_CHANGED, this._onPrefChanged);
+ PerformanceController.on(EVENTS.THEME_CHANGED, this._onThemeChanged);
+ PerformanceController.on(EVENTS.RECORDING_STATE_CHANGE, this._onRecordingStateChange);
+ PerformanceController.on(EVENTS.RECORDING_SELECTED, this._onRecordingSelected);
+ this.graphs.on("selecting", this._onGraphSelecting);
+ this.graphs.on("rendered", this._onGraphRendered);
+ },
+
+ /**
+ * Unbinds events.
+ */
+ destroy: Task.async(function* () {
+ PerformanceController.off(EVENTS.PREF_CHANGED, this._onPrefChanged);
+ PerformanceController.off(EVENTS.THEME_CHANGED, this._onThemeChanged);
+ PerformanceController.off(EVENTS.RECORDING_STATE_CHANGE,
+ this._onRecordingStateChange);
+ PerformanceController.off(EVENTS.RECORDING_SELECTED, this._onRecordingSelected);
+ this.graphs.off("selecting", this._onGraphSelecting);
+ this.graphs.off("rendered", this._onGraphRendered);
+ yield this.graphs.destroy();
+ }),
+
+ /**
+ * Returns true if any of the overview graphs have mouse dragging active,
+ * false otherwise.
+ */
+ get isMouseActive() {
+ // Fetch all graphs currently stored in the GraphsController.
+ // These graphs are not necessarily active, but will not have
+ // an active mouse, in that case.
+ return !!this.graphs.getWidgets().some(e => e.isMouseActive);
+ },
+
+ /**
+ * Disabled in the event we're using a Timeline mock, so we'll have no
+ * timeline, ticks or memory data to show, so just block rendering and hide
+ * the panel.
+ */
+ disable: function () {
+ this._disabled = true;
+ this.graphs.disableAll();
+ },
+
+ /**
+ * Returns the disabled status.
+ *
+ * @return boolean
+ */
+ isDisabled: function () {
+ return this._disabled;
+ },
+
+ /**
+ * Sets the time interval selection for all graphs in this overview.
+ *
+ * @param object interval
+ * The { startTime, endTime }, in milliseconds.
+ */
+ setTimeInterval: function (interval, options = {}) {
+ let recording = PerformanceController.getCurrentRecording();
+ if (recording == null) {
+ throw new Error("A recording should be available in order to set the selection.");
+ }
+ if (this.isDisabled()) {
+ return;
+ }
+ let mapStart = () => 0;
+ let mapEnd = () => recording.getDuration();
+ let selection = { start: interval.startTime, end: interval.endTime };
+ this._stopSelectionChangeEventPropagation = options.stopPropagation;
+ this.graphs.setMappedSelection(selection, { mapStart, mapEnd });
+ this._stopSelectionChangeEventPropagation = false;
+ },
+
+ /**
+ * Gets the time interval selection for all graphs in this overview.
+ *
+ * @return object
+ * The { startTime, endTime }, in milliseconds.
+ */
+ getTimeInterval: function () {
+ let recording = PerformanceController.getCurrentRecording();
+ if (recording == null) {
+ throw new Error("A recording should be available in order to get the selection.");
+ }
+ if (this.isDisabled()) {
+ return { startTime: 0, endTime: recording.getDuration() };
+ }
+ let mapStart = () => 0;
+ let mapEnd = () => recording.getDuration();
+ let selection = this.graphs.getMappedSelection({ mapStart, mapEnd });
+ // If no selection returned, this means the overview graphs have not been rendered
+ // yet, so act as if we have no selection (the full recording). Also
+ // if the selection range distance is tiny, assume the range was cleared or just
+ // clicked, and we do not have a range.
+ if (!selection || (selection.max - selection.min) < 1) {
+ return { startTime: 0, endTime: recording.getDuration() };
+ }
+ return { startTime: selection.min, endTime: selection.max };
+ },
+
+ /**
+ * Method for handling all the set up for rendering the overview graphs.
+ *
+ * @param number resolution
+ * The fps graph resolution. @see Graphs.js
+ */
+ render: Task.async(function* (resolution) {
+ if (this.isDisabled()) {
+ return;
+ }
+
+ let recording = PerformanceController.getCurrentRecording();
+ yield this.graphs.render(recording.getAllData(), resolution);
+
+ // Finished rendering all graphs in this overview.
+ this.emit(EVENTS.UI_OVERVIEW_RENDERED, resolution);
+ }),
+
+ /**
+ * Called at most every OVERVIEW_UPDATE_INTERVAL milliseconds
+ * and uses data fetched from the controller to render
+ * data into all the corresponding overview graphs.
+ */
+ _onRecordingTick: Task.async(function* () {
+ yield this.render(FRAMERATE_GRAPH_LOW_RES_INTERVAL);
+ this._prepareNextTick();
+ }),
+
+ /**
+ * Called to refresh the timer to keep firing _onRecordingTick.
+ */
+ _prepareNextTick: function () {
+ // Check here to see if there's still a _timeoutId, incase
+ // `stop` was called before the _prepareNextTick call was executed.
+ if (this.isRendering()) {
+ this._timeoutId = setTimeout(this._onRecordingTick, this.OVERVIEW_UPDATE_INTERVAL);
+ }
+ },
+
+ /**
+ * Called when recording state changes.
+ */
+ _onRecordingStateChange: OverviewViewOnStateChange(Task.async(
+ function* (_, state, recording) {
+ if (state !== "recording-stopped") {
+ return;
+ }
+ // Check to see if the recording that just stopped is the current recording.
+ // If it is, render the high-res graphs. For manual recordings, it will also
+ // be the current recording, but profiles generated by `console.profile` can stop
+ // while having another profile selected -- in this case, OverviewView should keep
+ // rendering the current recording.
+ if (recording !== PerformanceController.getCurrentRecording()) {
+ return;
+ }
+ this.render(FRAMERATE_GRAPH_HIGH_RES_INTERVAL);
+ yield this._checkSelection(recording);
+ })),
+
+ /**
+ * Called when a new recording is selected.
+ */
+ _onRecordingSelected: OverviewViewOnStateChange(Task.async(function* (_, recording) {
+ this._setGraphVisibilityFromRecordingFeatures(recording);
+
+ // If this recording is complete, render the high res graph
+ if (recording.isCompleted()) {
+ yield this.render(FRAMERATE_GRAPH_HIGH_RES_INTERVAL);
+ }
+ yield this._checkSelection(recording);
+ this.graphs.dropSelection();
+ })),
+
+ /**
+ * Start the polling for rendering the overview graph.
+ */
+ _startPolling: function () {
+ this._timeoutId = setTimeout(this._onRecordingTick, this.OVERVIEW_UPDATE_INTERVAL);
+ },
+
+ /**
+ * Stop the polling for rendering the overview graph.
+ */
+ _stopPolling: function () {
+ clearTimeout(this._timeoutId);
+ this._timeoutId = null;
+ },
+
+ /**
+ * Whether or not the overview view is in a state of polling rendering.
+ */
+ isRendering: function () {
+ return !!this._timeoutId;
+ },
+
+ /**
+ * Makes sure the selection is enabled or disabled in all the graphs,
+ * based on whether a recording currently exists and is not in progress.
+ */
+ _checkSelection: Task.async(function* (recording) {
+ let isEnabled = recording ? recording.isCompleted() : false;
+ yield this.graphs.selectionEnabled(isEnabled);
+ }),
+
+ /**
+ * Fired when the graph selection has changed. Called by
+ * mouseup and scroll events.
+ */
+ _onGraphSelecting: function () {
+ if (this._stopSelectionChangeEventPropagation) {
+ return;
+ }
+
+ this.emit(EVENTS.UI_OVERVIEW_RANGE_SELECTED, this.getTimeInterval());
+ },
+
+ _onGraphRendered: function (_, graphName) {
+ switch (graphName) {
+ case "timeline":
+ this.emit(EVENTS.UI_MARKERS_GRAPH_RENDERED);
+ break;
+ case "memory":
+ this.emit(EVENTS.UI_MEMORY_GRAPH_RENDERED);
+ break;
+ case "framerate":
+ this.emit(EVENTS.UI_FRAMERATE_GRAPH_RENDERED);
+ break;
+ }
+ },
+
+ /**
+ * Called whenever a preference in `devtools.performance.ui.` changes.
+ * Does not care about the enabling of memory/framerate graphs,
+ * because those will set values on a recording model, and
+ * the graphs will render based on the existence.
+ */
+ _onPrefChanged: Task.async(function* (_, prefName, prefValue) {
+ switch (prefName) {
+ case "hidden-markers": {
+ let graph = yield this.graphs.isAvailable("timeline");
+ if (graph) {
+ let filter = PerformanceController.getPref("hidden-markers");
+ graph.setFilter(filter);
+ graph.refresh({ force: true });
+ }
+ break;
+ }
+ }
+ }),
+
+ _setGraphVisibilityFromRecordingFeatures: function (recording) {
+ for (let [graphName, requirements] of Object.entries(GRAPH_REQUIREMENTS)) {
+ this.graphs.enable(graphName,
+ PerformanceController.isFeatureSupported(requirements.features));
+ }
+ },
+
+ /**
+ * Fetch the multiprocess status and if e10s is not currently on, disable
+ * realtime rendering.
+ *
+ * @return {boolean}
+ */
+ isRealtimeRenderingEnabled: function () {
+ return this._multiprocessData.enabled;
+ },
+
+ /**
+ * Show the graphs overview panel when a recording is finished
+ * when non-realtime graphs are enabled. Also set the graph visibility
+ * so the performance graphs know which graphs to render.
+ *
+ * @param {RecordingModel} recording
+ */
+ _showGraphsPanel: function (recording) {
+ this._setGraphVisibilityFromRecordingFeatures(recording);
+ $("#overview-pane").classList.remove("hidden");
+ },
+
+ /**
+ * Hide the graphs container completely.
+ */
+ _hideGraphsPanel: function () {
+ $("#overview-pane").classList.add("hidden");
+ },
+
+ /**
+ * Called when `devtools.theme` changes.
+ */
+ _onThemeChanged: function (_, theme) {
+ this.graphs.setTheme({ theme, redraw: true });
+ },
+
+ toString: () => "[object OverviewView]"
+};
+
+/**
+ * Utility that can wrap a method of OverviewView that
+ * handles a recording state change like when a recording is starting,
+ * stopping, or about to start/stop, and determines whether or not
+ * the polling for rendering the overview graphs needs to start or stop.
+ * Must be called with the OverviewView context.
+ *
+ * @param {function?} fn
+ * @return {function}
+ */
+function OverviewViewOnStateChange(fn) {
+ return function _onRecordingStateChange(eventName, recording) {
+ // Normalize arguments for the RECORDING_STATE_CHANGE event,
+ // as it also has a `state` argument.
+ if (typeof recording === "string") {
+ recording = arguments[2];
+ }
+
+ let currentRecording = PerformanceController.getCurrentRecording();
+
+ // All these methods require a recording to exist selected and
+ // from the event name, since there is a delay between starting
+ // a recording and changing the selection.
+ if (!currentRecording || !recording) {
+ // If no recording (this can occur when having a console.profile recording, and
+ // we do not stop it from the backend), and we are still rendering updates,
+ // stop that.
+ if (this.isRendering()) {
+ this._stopPolling();
+ }
+ return;
+ }
+
+ // If realtime rendering is not enabed (e10s not on), then
+ // show the disabled message, or the full graphs if the recording is completed
+ if (!this.isRealtimeRenderingEnabled()) {
+ if (recording.isRecording()) {
+ this._hideGraphsPanel();
+ // Abort, as we do not want to change polling status.
+ return;
+ }
+ this._showGraphsPanel(recording);
+ }
+
+ if (this.isRendering() && !currentRecording.isRecording()) {
+ this._stopPolling();
+ } else if (currentRecording.isRecording() && !this.isRendering()) {
+ this._startPolling();
+ }
+
+ if (fn) {
+ fn.apply(this, arguments);
+ }
+ };
+}
+
+// Decorates the OverviewView as an EventEmitter
+EventEmitter.decorate(OverviewView);
diff --git a/devtools/client/performance/views/recordings.js b/devtools/client/performance/views/recordings.js
new file mode 100644
index 000000000..487ea4f03
--- /dev/null
+++ b/devtools/client/performance/views/recordings.js
@@ -0,0 +1,202 @@
+/* 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/. */
+/* import-globals-from ../performance-controller.js */
+/* import-globals-from ../performance-view.js */
+/* globals document, window */
+"use strict";
+
+/**
+ * Functions handling the recordings UI.
+ */
+var RecordingsView = {
+ /**
+ * Initialization function, called when the tool is started.
+ */
+ initialize: function () {
+ this._onSelect = this._onSelect.bind(this);
+ this._onRecordingStateChange = this._onRecordingStateChange.bind(this);
+ this._onNewRecording = this._onNewRecording.bind(this);
+ this._onSaveButtonClick = this._onSaveButtonClick.bind(this);
+ this._onRecordingDeleted = this._onRecordingDeleted.bind(this);
+ this._onRecordingExported = this._onRecordingExported.bind(this);
+
+ PerformanceController.on(EVENTS.RECORDING_STATE_CHANGE, this._onRecordingStateChange);
+ PerformanceController.on(EVENTS.RECORDING_ADDED, this._onNewRecording);
+ PerformanceController.on(EVENTS.RECORDING_DELETED, this._onRecordingDeleted);
+ PerformanceController.on(EVENTS.RECORDING_EXPORTED, this._onRecordingExported);
+
+ // DE-XUL: Begin migrating the recording sidebar to React. Temporarily hold state
+ // here.
+ this._listState = {
+ recordings: [],
+ labels: new WeakMap(),
+ selected: null,
+ };
+ this._listMount = PerformanceUtils.createHtmlMount($("#recording-list-mount"));
+ this._renderList();
+ },
+
+ /**
+ * Get the index of the currently selected recording. Only used by tests.
+ * @return {integer} index
+ */
+ getSelectedIndex() {
+ const { recordings, selected } = this._listState;
+ return recordings.indexOf(selected);
+ },
+
+ /**
+ * Set the currently selected recording via its index. Only used by tests.
+ * @param {integer} index
+ */
+ setSelectedByIndex(index) {
+ this._onSelect(this._listState.recordings[index]);
+ this._renderList();
+ },
+
+ /**
+ * DE-XUL: During the migration, this getter will access the selected recording from
+ * the private _listState object so that tests will continue to pass.
+ */
+ get selected() {
+ return this._listState.selected;
+ },
+
+ /**
+ * DE-XUL: During the migration, this getter will access the number of recordings.
+ */
+ get itemCount() {
+ return this._listState.recordings.length;
+ },
+
+ /**
+ * DE-XUL: Render the recording list using React.
+ */
+ _renderList: function () {
+ const {recordings, labels, selected} = this._listState;
+
+ const recordingList = RecordingList({
+ itemComponent: RecordingListItem,
+ items: recordings.map(recording => ({
+ onSelect: () => this._onSelect(recording),
+ onSave: () => this._onSaveButtonClick(recording),
+ isLoading: !recording.isRecording() && !recording.isCompleted(),
+ isRecording: recording.isRecording(),
+ isSelected: recording === selected,
+ duration: recording.getDuration().toFixed(0),
+ label: labels.get(recording),
+ }))
+ });
+
+ ReactDOM.render(recordingList, this._listMount);
+ },
+
+ /**
+ * Destruction function, called when the tool is closed.
+ */
+ destroy: function () {
+ PerformanceController.off(EVENTS.RECORDING_STATE_CHANGE,
+ this._onRecordingStateChange);
+ PerformanceController.off(EVENTS.RECORDING_ADDED, this._onNewRecording);
+ PerformanceController.off(EVENTS.RECORDING_DELETED, this._onRecordingDeleted);
+ PerformanceController.off(EVENTS.RECORDING_EXPORTED, this._onRecordingExported);
+ },
+
+ /**
+ * Called when a new recording is stored in the UI. This handles
+ * when recordings are lazily loaded (like a console.profile occurring
+ * before the tool is loaded) or imported. In normal manual recording cases,
+ * this will also be fired.
+ */
+ _onNewRecording: function (_, recording) {
+ this._onRecordingStateChange(_, null, recording);
+ },
+
+ /**
+ * Signals that a recording has changed state.
+ *
+ * @param string state
+ * Can be "recording-started", "recording-stopped", "recording-stopping"
+ * @param RecordingModel recording
+ * Model of the recording that was started.
+ */
+ _onRecordingStateChange: function (_, state, recording) {
+ const { recordings, labels } = this._listState;
+
+ if (!recordings.includes(recording)) {
+ recordings.push(recording);
+ labels.set(recording, recording.getLabel() ||
+ L10N.getFormatStr("recordingsList.itemLabel", recordings.length));
+
+ // If this is a manual recording, immediately select it, or
+ // select a console profile if its the only one
+ if (!recording.isConsole() || !this._listState.selected) {
+ this._onSelect(recording);
+ }
+ }
+
+ // Determine if the recording needs to be selected.
+ const isCompletedManualRecording = !recording.isConsole() && recording.isCompleted();
+ if (recording.isImported() || isCompletedManualRecording) {
+ this._onSelect(recording);
+ }
+
+ this._renderList();
+ },
+
+ /**
+ * Clears out all non-console recordings.
+ */
+ _onRecordingDeleted: function (_, recording) {
+ const { recordings } = this._listState;
+ const index = recordings.indexOf(recording);
+ if (index === -1) {
+ throw new Error("Attempting to remove a recording that doesn't exist.");
+ }
+ recordings.splice(index, 1);
+ this._renderList();
+ },
+
+ /**
+ * The select listener for this container.
+ */
+ _onSelect: Task.async(function* (recording) {
+ this._listState.selected = recording;
+ this.emit(EVENTS.UI_RECORDING_SELECTED, recording);
+ this._renderList();
+ }),
+
+ /**
+ * The click listener for the "save" button of each item in this container.
+ */
+ _onSaveButtonClick: function (recording) {
+ let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ fp.init(window, L10N.getStr("recordingsList.saveDialogTitle"),
+ Ci.nsIFilePicker.modeSave);
+ fp.appendFilter(L10N.getStr("recordingsList.saveDialogJSONFilter"), "*.json");
+ fp.appendFilter(L10N.getStr("recordingsList.saveDialogAllFilter"), "*.*");
+ fp.defaultString = "profile.json";
+
+ fp.open({ done: result => {
+ if (result == Ci.nsIFilePicker.returnCancel) {
+ return;
+ }
+ this.emit(EVENTS.UI_EXPORT_RECORDING, recording, fp.file);
+ }});
+ },
+
+ _onRecordingExported: function (_, recording, file) {
+ if (recording.isConsole()) {
+ return;
+ }
+ const name = file.leafName.replace(/\..+$/, "");
+ this._listState.labels.set(recording, name);
+ this._renderList();
+ }
+};
+
+/**
+ * Convenient way of emitting events from the RecordingsView.
+ */
+EventEmitter.decorate(RecordingsView);
diff --git a/devtools/client/performance/views/toolbar.js b/devtools/client/performance/views/toolbar.js
new file mode 100644
index 000000000..bcab09a86
--- /dev/null
+++ b/devtools/client/performance/views/toolbar.js
@@ -0,0 +1,160 @@
+/* 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/. */
+/* import-globals-from ../performance-controller.js */
+/* import-globals-from ../performance-view.js */
+/* globals document */
+"use strict";
+
+/**
+ * View handler for toolbar events (mostly option toggling and triggering)
+ */
+var ToolbarView = {
+ /**
+ * Sets up the view with event binding.
+ */
+ initialize: Task.async(function* () {
+ this._onFilterPopupShowing = this._onFilterPopupShowing.bind(this);
+ this._onFilterPopupHiding = this._onFilterPopupHiding.bind(this);
+ this._onHiddenMarkersChanged = this._onHiddenMarkersChanged.bind(this);
+ this._onPrefChanged = this._onPrefChanged.bind(this);
+ this._popup = $("#performance-options-menupopup");
+
+ this.optionsView = new OptionsView({
+ branchName: BRANCH_NAME,
+ menupopup: this._popup
+ });
+
+ // Set the visibility of experimental UI options on load
+ // based off of `devtools.performance.ui.experimental` preference
+ let experimentalEnabled = PerformanceController.getOption("experimental");
+ this._toggleExperimentalUI(experimentalEnabled);
+
+ yield this.optionsView.initialize();
+ this.optionsView.on("pref-changed", this._onPrefChanged);
+
+ this._buildMarkersFilterPopup();
+ this._updateHiddenMarkersPopup();
+ $("#performance-filter-menupopup").addEventListener("popupshowing",
+ this._onFilterPopupShowing);
+ $("#performance-filter-menupopup").addEventListener("popuphiding",
+ this._onFilterPopupHiding);
+ }),
+
+ /**
+ * Unbinds events and cleans up view.
+ */
+ destroy: function () {
+ $("#performance-filter-menupopup").removeEventListener("popupshowing",
+ this._onFilterPopupShowing);
+ $("#performance-filter-menupopup").removeEventListener("popuphiding",
+ this._onFilterPopupHiding);
+ this._popup = null;
+
+ this.optionsView.off("pref-changed", this._onPrefChanged);
+ this.optionsView.destroy();
+ },
+
+ /**
+ * Creates the timeline markers filter popup.
+ */
+ _buildMarkersFilterPopup: function () {
+ for (let [markerName, markerDetails] of Object.entries(TIMELINE_BLUEPRINT)) {
+ let menuitem = document.createElement("menuitem");
+ menuitem.setAttribute("closemenu", "none");
+ menuitem.setAttribute("type", "checkbox");
+ menuitem.setAttribute("align", "center");
+ menuitem.setAttribute("flex", "1");
+ menuitem.setAttribute("label",
+ MarkerBlueprintUtils.getMarkerGenericName(markerName));
+ menuitem.setAttribute("marker-type", markerName);
+ menuitem.className = `marker-color-${markerDetails.colorName}`;
+
+ menuitem.addEventListener("command", this._onHiddenMarkersChanged);
+
+ $("#performance-filter-menupopup").appendChild(menuitem);
+ }
+ },
+
+ /**
+ * Updates the menu items checked state in the timeline markers filter popup.
+ */
+ _updateHiddenMarkersPopup: function () {
+ let menuItems = $$("#performance-filter-menupopup menuitem[marker-type]");
+ let hiddenMarkers = PerformanceController.getPref("hidden-markers");
+
+ for (let menuitem of menuItems) {
+ if (~hiddenMarkers.indexOf(menuitem.getAttribute("marker-type"))) {
+ menuitem.removeAttribute("checked");
+ } else {
+ menuitem.setAttribute("checked", "true");
+ }
+ }
+ },
+
+ /**
+ * Fired when `devtools.performance.ui.experimental` is changed, or
+ * during init. Toggles the visibility of experimental performance tool options
+ * in the UI options.
+ *
+ * Sets or removes "experimental-enabled" on the menu and main elements,
+ * hiding or showing all elements with class "experimental-option".
+ *
+ * TODO re-enable "#option-enable-memory" permanently once stable in bug 1163350
+ * TODO re-enable "#option-show-jit-optimizations" permanently once stable in
+ * bug 1163351
+ *
+ * @param {boolean} isEnabled
+ */
+ _toggleExperimentalUI: function (isEnabled) {
+ if (isEnabled) {
+ $(".theme-body").classList.add("experimental-enabled");
+ this._popup.classList.add("experimental-enabled");
+ } else {
+ $(".theme-body").classList.remove("experimental-enabled");
+ this._popup.classList.remove("experimental-enabled");
+ }
+ },
+
+ /**
+ * Fired when the markers filter popup starts to show.
+ */
+ _onFilterPopupShowing: function () {
+ $("#filter-button").setAttribute("open", "true");
+ },
+
+ /**
+ * Fired when the markers filter popup starts to hide.
+ */
+ _onFilterPopupHiding: function () {
+ $("#filter-button").removeAttribute("open");
+ },
+
+ /**
+ * Fired when a menu item in the markers filter popup is checked or unchecked.
+ */
+ _onHiddenMarkersChanged: function () {
+ let checkedMenuItems =
+ $$("#performance-filter-menupopup menuitem[marker-type]:not([checked])");
+ let hiddenMarkers = Array.map(checkedMenuItems, e => e.getAttribute("marker-type"));
+ PerformanceController.setPref("hidden-markers", hiddenMarkers);
+ },
+
+ /**
+ * Fired when a preference changes in the underlying OptionsView.
+ * Propogated by the PerformanceController.
+ */
+ _onPrefChanged: function (_, prefName) {
+ let value = PerformanceController.getOption(prefName);
+
+ if (prefName === "experimental") {
+ this._toggleExperimentalUI(value);
+ }
+
+ this.emit(EVENTS.UI_PREF_CHANGED, prefName, value);
+ },
+
+ toString: () => "[object ToolbarView]"
+};
+
+EventEmitter.decorate(ToolbarView);
diff --git a/devtools/client/preferences/devtools.js b/devtools/client/preferences/devtools.js
new file mode 100644
index 000000000..9de9cd34a
--- /dev/null
+++ b/devtools/client/preferences/devtools.js
@@ -0,0 +1,366 @@
+# -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+# 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/.
+
+// Developer edition promo preferences
+pref("devtools.devedition.promo.shown", false);
+pref("devtools.devedition.promo.url", "https://www.mozilla.org/firefox/developer/?utm_source=firefox-dev-tools&utm_medium=firefox-browser&utm_content=betadoorhanger");
+
+// Only potentially show in beta release
+#if MOZ_UPDATE_CHANNEL == beta
+ pref("devtools.devedition.promo.enabled", true);
+#else
+ pref("devtools.devedition.promo.enabled", false);
+#endif
+
+// DevTools development workflow
+pref("devtools.loader.hotreload", false);
+
+// Developer toolbar preferences
+pref("devtools.toolbar.enabled", true);
+pref("devtools.toolbar.visible", false);
+
+// Enable DevTools WebIDE by default
+pref("devtools.webide.enabled", true);
+
+// Toolbox preferences
+pref("devtools.toolbox.footer.height", 250);
+pref("devtools.toolbox.sidebar.width", 500);
+pref("devtools.toolbox.host", "bottom");
+pref("devtools.toolbox.previousHost", "side");
+pref("devtools.toolbox.selectedTool", "webconsole");
+pref("devtools.toolbox.toolbarSpec", '["splitconsole", "paintflashing toggle","scratchpad","resize toggle","screenshot --fullpage", "rulers", "measure"]');
+pref("devtools.toolbox.sideEnabled", true);
+pref("devtools.toolbox.zoomValue", "1");
+pref("devtools.toolbox.splitconsoleEnabled", false);
+pref("devtools.toolbox.splitconsoleHeight", 100);
+
+// Toolbox Button preferences
+pref("devtools.command-button-frames.enabled", true);
+pref("devtools.command-button-splitconsole.enabled", true);
+pref("devtools.command-button-paintflashing.enabled", false);
+pref("devtools.command-button-scratchpad.enabled", false);
+pref("devtools.command-button-responsive.enabled", true);
+pref("devtools.command-button-screenshot.enabled", false);
+pref("devtools.command-button-rulers.enabled", false);
+pref("devtools.command-button-measure.enabled", false);
+pref("devtools.command-button-noautohide.enabled", false);
+
+// Inspector preferences
+// Enable the Inspector
+pref("devtools.inspector.enabled", true);
+// What was the last active sidebar in the inspector
+pref("devtools.inspector.activeSidebar", "ruleview");
+pref("devtools.inspector.remote", false);
+// Collapse pseudo-elements by default in the rule-view
+pref("devtools.inspector.show_pseudo_elements", false);
+// The default size for image preview tooltips in the rule-view/computed-view/markup-view
+pref("devtools.inspector.imagePreviewTooltipSize", 300);
+// Enable user agent style inspection in rule-view
+pref("devtools.inspector.showUserAgentStyles", false);
+// Show all native anonymous content (like controls in <video> tags)
+pref("devtools.inspector.showAllAnonymousContent", false);
+// Enable the MDN docs tooltip
+pref("devtools.inspector.mdnDocsTooltip.enabled", true);
+
+// Enable the Font Inspector
+pref("devtools.fontinspector.enabled", true);
+
+// Enable the Layout View
+pref("devtools.layoutview.enabled", false);
+
+// By how many times eyedropper will magnify pixels
+pref("devtools.eyedropper.zoom", 6);
+
+// Enable to collapse attributes that are too long.
+pref("devtools.markup.collapseAttributes", true);
+
+// Length to collapse attributes
+pref("devtools.markup.collapseAttributeLength", 120);
+
+// DevTools default color unit
+pref("devtools.defaultColorUnit", "authored");
+
+// Enable the Responsive UI tool
+pref("devtools.responsiveUI.no-reload-notification", false);
+
+// Enable the Debugger
+pref("devtools.debugger.enabled", true);
+pref("devtools.debugger.chrome-debugging-host", "localhost");
+pref("devtools.debugger.chrome-debugging-port", 6080);
+pref("devtools.debugger.chrome-debugging-websocket", false);
+pref("devtools.debugger.remote-host", "localhost");
+pref("devtools.debugger.remote-timeout", 20000);
+pref("devtools.debugger.pause-on-exceptions", false);
+pref("devtools.debugger.ignore-caught-exceptions", true);
+pref("devtools.debugger.source-maps-enabled", true);
+pref("devtools.debugger.client-source-maps-enabled", true);
+pref("devtools.debugger.pretty-print-enabled", true);
+pref("devtools.debugger.auto-pretty-print", false);
+pref("devtools.debugger.auto-black-box", true);
+pref("devtools.debugger.workers", false);
+
+#ifdef RELEASE_OR_BETA
+pref("devtools.debugger.new-debugger-frontend", false);
+#else
+pref("devtools.debugger.new-debugger-frontend", true);
+#endif
+
+// The default Debugger UI settings
+pref("devtools.debugger.ui.panes-workers-and-sources-width", 200);
+pref("devtools.debugger.ui.panes-instruments-width", 300);
+pref("devtools.debugger.ui.panes-visible-on-startup", false);
+pref("devtools.debugger.ui.variables-sorting-enabled", true);
+pref("devtools.debugger.ui.variables-only-enum-visible", false);
+pref("devtools.debugger.ui.variables-searchbox-visible", false);
+
+// Enable the Memory tools
+pref("devtools.memory.enabled", true);
+
+pref("devtools.memory.custom-census-displays", "{}");
+pref("devtools.memory.custom-label-displays", "{}");
+pref("devtools.memory.custom-tree-map-displays", "{}");
+
+pref("devtools.memory.max-individuals", 1000);
+pref("devtools.memory.max-retaining-paths", 10);
+
+// Enable the Performance tools
+pref("devtools.performance.enabled", true);
+
+// The default Performance UI settings
+pref("devtools.performance.memory.sample-probability", "0.05");
+// Can't go higher than this without causing internal allocation overflows while
+// serializing the allocations data over the RDP.
+pref("devtools.performance.memory.max-log-length", 125000);
+pref("devtools.performance.timeline.hidden-markers",
+ "[\"Composite\",\"CompositeForwardTransaction\"]");
+pref("devtools.performance.profiler.buffer-size", 10000000);
+pref("devtools.performance.profiler.sample-frequency-khz", 1);
+pref("devtools.performance.ui.invert-call-tree", true);
+pref("devtools.performance.ui.invert-flame-graph", false);
+pref("devtools.performance.ui.flatten-tree-recursion", true);
+pref("devtools.performance.ui.show-platform-data", false);
+pref("devtools.performance.ui.show-idle-blocks", true);
+pref("devtools.performance.ui.enable-memory", false);
+pref("devtools.performance.ui.enable-allocations", false);
+pref("devtools.performance.ui.enable-framerate", true);
+pref("devtools.performance.ui.show-jit-optimizations", false);
+pref("devtools.performance.ui.show-triggers-for-gc-types",
+ "TOO_MUCH_MALLOC ALLOC_TRIGGER LAST_DITCH EAGER_ALLOC_TRIGGER");
+
+// Temporary pref disabling memory flame views
+// TODO remove once we have flame charts via bug 1148663
+pref("devtools.performance.ui.enable-memory-flame", false);
+
+// Enable experimental options in the UI only in Nightly
+#if defined(NIGHTLY_BUILD)
+pref("devtools.performance.ui.experimental", true);
+#else
+pref("devtools.performance.ui.experimental", false);
+#endif
+
+// The default cache UI setting
+pref("devtools.cache.disabled", false);
+
+// The default service workers UI setting
+pref("devtools.serviceWorkers.testing.enabled", false);
+
+// Enable the Network Monitor
+pref("devtools.netmonitor.enabled", true);
+
+// The default Network Monitor UI settings
+pref("devtools.netmonitor.panes-network-details-width", 550);
+pref("devtools.netmonitor.panes-network-details-height", 450);
+pref("devtools.netmonitor.statistics", true);
+pref("devtools.netmonitor.filters", "[\"all\"]");
+
+// The default Network monitor HAR export setting
+pref("devtools.netmonitor.har.defaultLogDir", "");
+pref("devtools.netmonitor.har.defaultFileName", "Archive %y-%m-%d %H-%M-%S");
+pref("devtools.netmonitor.har.jsonp", false);
+pref("devtools.netmonitor.har.jsonpCallback", "");
+pref("devtools.netmonitor.har.includeResponseBodies", true);
+pref("devtools.netmonitor.har.compress", false);
+pref("devtools.netmonitor.har.forceExport", false);
+pref("devtools.netmonitor.har.pageLoadedTimeout", 1500);
+pref("devtools.netmonitor.har.enableAutoExportToFile", false);
+
+// Scratchpad settings
+// - recentFileMax: The maximum number of recently-opened files
+// stored. Setting this preference to 0 will not
+// clear any recent files, but rather hide the
+// 'Open Recent'-menu.
+// - lineNumbers: Whether to show line numbers or not.
+// - wrapText: Whether to wrap text or not.
+// - showTrailingSpace: Whether to highlight trailing space or not.
+// - editorFontSize: Editor font size configuration.
+// - enableAutocompletion: Whether to enable JavaScript autocompletion.
+pref("devtools.scratchpad.recentFilesMax", 10);
+pref("devtools.scratchpad.lineNumbers", true);
+pref("devtools.scratchpad.wrapText", false);
+pref("devtools.scratchpad.showTrailingSpace", false);
+pref("devtools.scratchpad.editorFontSize", 12);
+pref("devtools.scratchpad.enableAutocompletion", true);
+
+// Enable the Storage Inspector
+pref("devtools.storage.enabled", false);
+
+// Enable the Style Editor.
+pref("devtools.styleeditor.enabled", true);
+pref("devtools.styleeditor.source-maps-enabled", true);
+pref("devtools.styleeditor.autocompletion-enabled", true);
+pref("devtools.styleeditor.showMediaSidebar", true);
+pref("devtools.styleeditor.mediaSidebarWidth", 238);
+pref("devtools.styleeditor.navSidebarWidth", 245);
+pref("devtools.styleeditor.transitions", true);
+
+// Enable the Shader Editor.
+pref("devtools.shadereditor.enabled", false);
+
+// Enable the Canvas Debugger.
+pref("devtools.canvasdebugger.enabled", false);
+
+// Enable the Web Audio Editor
+pref("devtools.webaudioeditor.enabled", false);
+
+// Enable Scratchpad
+pref("devtools.scratchpad.enabled", false);
+
+// Make sure the DOM panel is hidden by default
+pref("devtools.dom.enabled", false);
+
+// Web Audio Editor Inspector Width should be a preference
+pref("devtools.webaudioeditor.inspectorWidth", 300);
+
+// Default theme ("dark" or "light")
+#ifdef MOZ_DEV_EDITION
+sticky_pref("devtools.theme", "dark");
+#else
+sticky_pref("devtools.theme", "light");
+#endif
+
+// Web console filters
+pref("devtools.webconsole.filter.error", true);
+pref("devtools.webconsole.filter.warn", true);
+pref("devtools.webconsole.filter.info", true);
+pref("devtools.webconsole.filter.log", true);
+pref("devtools.webconsole.filter.debug", true);
+pref("devtools.webconsole.filter.net", false);
+pref("devtools.webconsole.filter.netxhr", false);
+// Deprecated - old console frontend
+pref("devtools.webconsole.filter.network", true);
+pref("devtools.webconsole.filter.networkinfo", false);
+pref("devtools.webconsole.filter.netwarn", true);
+pref("devtools.webconsole.filter.csserror", true);
+pref("devtools.webconsole.filter.cssparser", false);
+pref("devtools.webconsole.filter.csslog", false);
+pref("devtools.webconsole.filter.exception", true);
+pref("devtools.webconsole.filter.jswarn", true);
+pref("devtools.webconsole.filter.jslog", false);
+pref("devtools.webconsole.filter.secerror", true);
+pref("devtools.webconsole.filter.secwarn", true);
+pref("devtools.webconsole.filter.serviceworkers", true);
+pref("devtools.webconsole.filter.sharedworkers", false);
+pref("devtools.webconsole.filter.windowlessworkers", false);
+pref("devtools.webconsole.filter.servererror", false);
+pref("devtools.webconsole.filter.serverwarn", false);
+pref("devtools.webconsole.filter.serverinfo", false);
+pref("devtools.webconsole.filter.serverlog", false);
+
+// Remember the Browser Console filters
+pref("devtools.browserconsole.filter.network", true);
+pref("devtools.browserconsole.filter.networkinfo", false);
+pref("devtools.browserconsole.filter.netwarn", true);
+pref("devtools.browserconsole.filter.netxhr", false);
+pref("devtools.browserconsole.filter.csserror", true);
+pref("devtools.browserconsole.filter.cssparser", false);
+pref("devtools.browserconsole.filter.csslog", false);
+pref("devtools.browserconsole.filter.exception", true);
+pref("devtools.browserconsole.filter.jswarn", true);
+pref("devtools.browserconsole.filter.jslog", true);
+pref("devtools.browserconsole.filter.error", true);
+pref("devtools.browserconsole.filter.warn", true);
+pref("devtools.browserconsole.filter.info", true);
+pref("devtools.browserconsole.filter.log", true);
+pref("devtools.browserconsole.filter.secerror", true);
+pref("devtools.browserconsole.filter.secwarn", true);
+pref("devtools.browserconsole.filter.serviceworkers", true);
+pref("devtools.browserconsole.filter.sharedworkers", true);
+pref("devtools.browserconsole.filter.windowlessworkers", true);
+pref("devtools.browserconsole.filter.servererror", false);
+pref("devtools.browserconsole.filter.serverwarn", false);
+pref("devtools.browserconsole.filter.serverinfo", false);
+pref("devtools.browserconsole.filter.serverlog", false);
+
+// Web console filter settings bar
+pref("devtools.webconsole.ui.filterbar", false);
+
+// Max number of inputs to store in web console history.
+pref("devtools.webconsole.inputHistoryCount", 50);
+
+// Persistent logging: |true| if you want the Web Console to keep all of the
+// logged messages after reloading the page, |false| if you want the output to
+// be cleared each time page navigation happens.
+pref("devtools.webconsole.persistlog", false);
+
+// Web Console timestamp: |true| if you want the logs and instructions
+// in the Web Console to display a timestamp, or |false| to not display
+// any timestamps.
+pref("devtools.webconsole.timestampMessages", false);
+
+// Web Console automatic multiline mode: |true| if you want incomplete statements
+// to automatically trigger multiline editing (equivalent to shift + enter).
+pref("devtools.webconsole.autoMultiline", true);
+
+// Enable the experimental webconsole frontend
+#if defined(NIGHTLY_BUILD)
+pref("devtools.webconsole.new-frontend-enabled", true);
+#else
+pref("devtools.webconsole.new-frontend-enabled", false);
+#endif
+
+// Enable the experimental support for source maps in console (work in progress)
+pref("devtools.sourcemap.locations.enabled", false);
+
+// The number of lines that are displayed in the web console.
+pref("devtools.hud.loglimit", 1000);
+
+// The number of lines that are displayed in the web console for the Net,
+// CSS, JS and Web Developer categories. These defaults should be kept in sync
+// with DEFAULT_LOG_LIMIT in the webconsole frontend.
+pref("devtools.hud.loglimit.network", 1000);
+pref("devtools.hud.loglimit.cssparser", 1000);
+pref("devtools.hud.loglimit.exception", 1000);
+pref("devtools.hud.loglimit.console", 1000);
+
+// The developer tools editor configuration:
+// - tabsize: how many spaces to use when a Tab character is displayed.
+// - expandtab: expand Tab characters to spaces.
+// - keymap: which keymap to use (can be 'default', 'emacs' or 'vim')
+// - autoclosebrackets: whether to permit automatic bracket/quote closing.
+// - detectindentation: whether to detect the indentation from the file
+// - enableCodeFolding: Whether to enable code folding or not.
+pref("devtools.editor.tabsize", 2);
+pref("devtools.editor.expandtab", true);
+pref("devtools.editor.keymap", "default");
+pref("devtools.editor.autoclosebrackets", true);
+pref("devtools.editor.detectindentation", true);
+pref("devtools.editor.enableCodeFolding", true);
+pref("devtools.editor.autocomplete", true);
+
+// Pref to store the browser version at the time of a telemetry ping for an
+// opened developer tool. This allows us to ping telemetry just once per browser
+// version for each user.
+pref("devtools.telemetry.tools.opened.version", "{}");
+
+// Enable the JSON View tool (an inspector for application/json documents) on
+// Nightly and Dev. Edition.
+#ifdef RELEASE_OR_BETA
+pref("devtools.jsonview.enabled", false);
+#else
+pref("devtools.jsonview.enabled", true);
+#endif
+
+// Enable the HTML responsive design mode for all channels.
+pref("devtools.responsive.html.enabled", true);
diff --git a/devtools/client/preferences/moz.build b/devtools/client/preferences/moz.build
new file mode 100644
index 000000000..93676fab8
--- /dev/null
+++ b/devtools/client/preferences/moz.build
@@ -0,0 +1,9 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+JS_PREFERENCE_PP_FILES += [
+ 'devtools.js',
+]
diff --git a/devtools/client/projecteditor/chrome/content/projecteditor-loader.js b/devtools/client/projecteditor/chrome/content/projecteditor-loader.js
new file mode 100644
index 000000000..adee8f143
--- /dev/null
+++ b/devtools/client/projecteditor/chrome/content/projecteditor-loader.js
@@ -0,0 +1,176 @@
+var Cu = Components.utils;
+const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const {FileUtils} = Cu.import("resource://gre/modules/FileUtils.jsm", {});
+const {NetUtil} = Cu.import("resource://gre/modules/NetUtil.jsm", {});
+const promise = require("promise");
+const ProjectEditor = require("devtools/client/projecteditor/lib/projecteditor");
+
+const SAMPLE_PATH = buildTempDirectoryStructure();
+const SAMPLE_NAME = "DevTools Content Application Name";
+const SAMPLE_PROJECT_URL = "data:text/html;charset=utf-8,<body><h1>Project Overview</h1></body>";
+const SAMPLE_ICON = "chrome://devtools/skin/images/tool-debugger.svg";
+
+/**
+ * Create a workspace for working on projecteditor, available at
+ * chrome://devtools/content/projecteditor/chrome/content/projecteditor-loader.xul.
+ * This emulates the integration points that the app manager uses.
+ */
+var appManagerEditor;
+
+// Log a message to the project overview URL to make development easier
+function log(msg) {
+ if (!appManagerEditor) {
+ return;
+ }
+
+ let doc = appManagerEditor.iframe.contentDocument;
+ let el = doc.createElement("p");
+ el.textContent = msg;
+ doc.body.appendChild(el);
+}
+
+document.addEventListener("DOMContentLoaded", function onDOMReady(e) {
+ document.removeEventListener("DOMContentLoaded", onDOMReady, false);
+ let iframe = document.getElementById("projecteditor-iframe");
+ window.projecteditor = ProjectEditor.ProjectEditor(iframe);
+
+ projecteditor.on("onEditorCreated", (editor, a) => {
+ log("editor created: " + editor);
+ if (editor.label === "app-manager") {
+ appManagerEditor = editor;
+ appManagerEditor.on("load", function foo() {
+ appManagerEditor.off("load", foo);
+ log("Working on: " + SAMPLE_PATH);
+ });
+ }
+ });
+ projecteditor.on("onEditorDestroyed", (editor) => {
+ log("editor destroyed: " + editor);
+ });
+ projecteditor.on("onEditorSave", (editor, resource) => {
+ log("editor saved: " + editor, resource.path);
+ });
+ projecteditor.on("onTreeSelected", (resource) => {
+ log("tree selected: " + resource.path);
+ });
+ projecteditor.on("onEditorLoad", (editor) => {
+ log("editor loaded: " + editor);
+ });
+ projecteditor.on("onEditorActivated", (editor) => {
+ log("editor focused: " + editor);
+ });
+ projecteditor.on("onEditorDeactivated", (editor) => {
+ log("editor blur: " + editor);
+ });
+ projecteditor.on("onEditorChange", (editor) => {
+ log("editor changed: " + editor);
+ });
+ projecteditor.on("onCommand", (cmd) => {
+ log("Command: " + cmd);
+ });
+
+ projecteditor.loaded.then(() => {
+ projecteditor.setProjectToAppPath(SAMPLE_PATH, {
+ name: SAMPLE_NAME,
+ iconUrl: SAMPLE_ICON,
+ projectOverviewURL: SAMPLE_PROJECT_URL,
+ validationStatus: "valid"
+ }).then(() => {
+ let allResources = projecteditor.project.allResources();
+ console.log("All resources have been loaded", allResources, allResources.map(r=>r.basename).join("|"));
+ });
+
+ });
+
+}, false);
+
+/**
+ * Build a temporary directory as a workspace for this loader
+ * https://developer.mozilla.org/en-US/Add-ons/Code_snippets/File_I_O
+ */
+function buildTempDirectoryStructure() {
+
+ // First create (and remove) the temp dir to discard any changes
+ let TEMP_DIR = FileUtils.getDir("TmpD", ["ProjectEditor"], true);
+ TEMP_DIR.remove(true);
+
+ // Now rebuild our fake project.
+ TEMP_DIR = FileUtils.getDir("TmpD", ["ProjectEditor"], true);
+
+ FileUtils.getDir("TmpD", ["ProjectEditor", "css"], true);
+ FileUtils.getDir("TmpD", ["ProjectEditor", "data"], true);
+ FileUtils.getDir("TmpD", ["ProjectEditor", "img", "icons"], true);
+ FileUtils.getDir("TmpD", ["ProjectEditor", "js"], true);
+
+ let htmlFile = FileUtils.getFile("TmpD", ["ProjectEditor", "index.html"]);
+ htmlFile.createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
+ writeToFile(htmlFile, [
+ "<!DOCTYPE html>",
+ '<html lang="en">',
+ " <head>",
+ ' <meta charset="utf-8" />',
+ " <title>ProjectEditor Temp File</title>",
+ ' <link rel="stylesheet" href="style.css" />',
+ " </head>",
+ ' <body id="home">',
+ " <p>ProjectEditor Temp File</p>",
+ " </body>",
+ "</html>"].join("\n")
+ );
+
+ let readmeFile = FileUtils.getFile("TmpD", ["ProjectEditor", "README.md"]);
+ readmeFile.createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
+ writeToFile(readmeFile, [
+ "## Readme"
+ ].join("\n")
+ );
+
+ let licenseFile = FileUtils.getFile("TmpD", ["ProjectEditor", "LICENSE"]);
+ licenseFile.createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
+ writeToFile(licenseFile, [
+ "/* 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/. */"
+ ].join("\n")
+ );
+
+ let cssFile = FileUtils.getFile("TmpD", ["ProjectEditor", "css", "styles.css"]);
+ cssFile.createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
+ writeToFile(cssFile, [
+ "body {",
+ " background: red;",
+ "}"
+ ].join("\n")
+ );
+
+ FileUtils.getFile("TmpD", ["ProjectEditor", "js", "script.js"]).createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
+
+ FileUtils.getFile("TmpD", ["ProjectEditor", "img", "fake.png"]).createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
+ FileUtils.getFile("TmpD", ["ProjectEditor", "img", "icons", "16x16.png"]).createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
+ FileUtils.getFile("TmpD", ["ProjectEditor", "img", "icons", "32x32.png"]).createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
+ FileUtils.getFile("TmpD", ["ProjectEditor", "img", "icons", "128x128.png"]).createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
+ FileUtils.getFile("TmpD", ["ProjectEditor", "img", "icons", "vector.svg"]).createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
+
+ return TEMP_DIR.path;
+}
+
+
+// https://developer.mozilla.org/en-US/Add-ons/Code_snippets/File_I_O#Writing_to_a_file
+function writeToFile(file, data) {
+
+ let defer = promise.defer();
+ var ostream = FileUtils.openSafeFileOutputStream(file);
+
+ var converter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"].
+ createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+ var istream = converter.convertToInputStream(data);
+
+ // The last argument (the callback) is optional.
+ NetUtil.asyncCopy(istream, ostream, function (status) {
+ if (!Components.isSuccessCode(status)) {
+ // Handle error!
+ console.log("ERROR WRITING TEMP FILE", status);
+ }
+ });
+}
diff --git a/devtools/client/projecteditor/chrome/content/projecteditor-loader.xul b/devtools/client/projecteditor/chrome/content/projecteditor-loader.xul
new file mode 100644
index 000000000..84db8ea48
--- /dev/null
+++ b/devtools/client/projecteditor/chrome/content/projecteditor-loader.xul
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+<!DOCTYPE window [
+<!ENTITY % toolboxDTD SYSTEM "chrome://devtools/locale/toolbox.dtd" >
+ %toolboxDTD;
+]>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+<script type="application/javascript;version=1.8" src="projecteditor-loader.js"></script>
+
+ <commandset id="toolbox-commandset">
+ <command id="projecteditor-cmd-close" oncommand="window.close();"/>
+ </commandset>
+
+ <keyset id="projecteditor-keyset">
+ <key id="projecteditor-key-close"
+ key="&closeCmd.key;"
+ command="projecteditor-cmd-close"
+ modifiers="accel"/>
+ </keyset>
+
+ <iframe id="projecteditor-iframe" flex="1" forceOwnRefreshDriver=""></iframe>
+</window>
diff --git a/devtools/client/projecteditor/chrome/content/projecteditor-test.xul b/devtools/client/projecteditor/chrome/content/projecteditor-test.xul
new file mode 100644
index 000000000..ee2be12f0
--- /dev/null
+++ b/devtools/client/projecteditor/chrome/content/projecteditor-test.xul
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+<?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?>
+<?xml-stylesheet href="chrome://global/skin/global.css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml">
+
+ <script type="application/javascript" src="chrome://global/content/globalOverlay.js"></script>
+
+ <commandset id="mainCommandSet">
+ <commandset id="editMenuCommands"/>
+ </commandset>
+ <menubar></menubar>
+ <iframe id='projecteditor-iframe' flex="1"></iframe>
+</window>
diff --git a/devtools/client/projecteditor/chrome/content/projecteditor.xul b/devtools/client/projecteditor/chrome/content/projecteditor.xul
new file mode 100644
index 000000000..795fe9fab
--- /dev/null
+++ b/devtools/client/projecteditor/chrome/content/projecteditor.xul
@@ -0,0 +1,87 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+<?xml-stylesheet href="chrome://devtools/skin/light-theme.css" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/skin/projecteditor/projecteditor.css" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/content/debugger/debugger.css" type="text/css"?>
+<?xml-stylesheet href="resource://devtools/client/themes/common.css" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/skin/markup.css" type="text/css"?>
+
+<?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?>
+
+<!DOCTYPE window [
+<!ENTITY % scratchpadDTD SYSTEM "chrome://devtools/locale/scratchpad.dtd" >
+ %scratchpadDTD;
+<!ENTITY % editMenuStrings SYSTEM "chrome://global/locale/editMenuOverlay.dtd">
+%editMenuStrings;
+<!ENTITY % sourceEditorStrings SYSTEM "chrome://devtools/locale/sourceeditor.dtd">
+%sourceEditorStrings;
+]>
+
+<page xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" class="theme-body theme-light">
+
+ <script type="application/javascript" src="chrome://global/content/globalOverlay.js"/>
+
+ <commandset id="projecteditor-commandset" />
+ <commandset id="editMenuCommands"/>
+ <keyset id="projecteditor-keyset" />
+ <keyset id="editMenuKeys"/>
+
+ <!-- Eventually we want to let plugins declare their own menu items.
+ Wait unti app manager lands to deal with this integration point.
+ -->
+ <menubar id="projecteditor-menubar">
+ <menu id="file-menu" label="&fileMenu.label;" accesskey="&fileMenu.accesskey;">
+ <menupopup id="file-menu-popup" />
+ </menu>
+
+ <menu id="edit-menu" label="&editMenu.label;"
+ accesskey="&editMenu.accesskey;">
+ <menupopup id="edit-menu-popup">
+ <menuitem id="menu_undo"/>
+ <menuitem id="menu_redo"/>
+ <menuseparator/>
+ <menuitem id="menu_cut"/>
+ <menuitem id="menu_copy"/>
+ <menuitem id="menu_paste"/>
+ </menupopup>
+ </menu>
+ </menubar>
+
+ <popupset>
+ <menupopup id="context-menu-popup">
+ </menupopup>
+ <menupopup id="texteditor-context-popup">
+ <menuitem id="cMenu_cut"/>
+ <menuitem id="cMenu_copy"/>
+ <menuitem id="cMenu_paste"/>
+ <menuitem id="cMenu_delete"/>
+ <menuseparator/>
+ <menuitem id="cMenu_selectAll"/>
+ </menupopup>
+ </popupset>
+
+ <deck id="main-deck" flex="1">
+ <vbox flex="1" id="source-deckitem">
+ <hbox id="sources-body" flex="1">
+ <vbox width="250" id="sources">
+ <vbox flex="1">
+ </vbox>
+ <toolbar id="project-toolbar" class="devtools-toolbar" hidden="true"></toolbar>
+ </vbox>
+ <splitter id="source-editor-splitter" class="devtools-side-splitter"/>
+ <vbox id="shells" flex="4">
+ <toolbar id="projecteditor-toolbar" class="devtools-toolbar">
+ <hbox id="plugin-toolbar-left"/>
+ <spacer flex="1"/>
+ <hbox id="plugin-toolbar-right"/>
+ </toolbar>
+ <box id="shells-deck-container" flex="4"></box>
+ <toolbar id="projecteditor-toolbar-bottom" class="devtools-toolbar">
+ </toolbar>
+ </vbox>
+ </hbox>
+ </vbox>
+ </deck>
+</page>
diff --git a/devtools/client/projecteditor/lib/editors.js b/devtools/client/projecteditor/lib/editors.js
new file mode 100644
index 000000000..7d0150cf7
--- /dev/null
+++ b/devtools/client/projecteditor/lib/editors.js
@@ -0,0 +1,303 @@
+/* -*- 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/. */
+
+const { Cu } = require("chrome");
+const { Class } = require("sdk/core/heritage");
+const { EventTarget } = require("sdk/event/target");
+const { emit } = require("sdk/event/core");
+const promise = require("promise");
+const Editor = require("devtools/client/sourceeditor/editor");
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+/**
+ * ItchEditor is extended to implement an editor, which is the main view
+ * that shows up when a file is selected. This object should not be used
+ * directly - use TextEditor for a basic code editor.
+ */
+var ItchEditor = Class({
+ extends: EventTarget,
+
+ /**
+ * A boolean specifying if the toolbar above the editor should be hidden.
+ */
+ hidesToolbar: false,
+
+ /**
+ * A boolean specifying whether the editor can be edited / saved.
+ * For instance, a 'save' doesn't make sense on an image.
+ */
+ isEditable: false,
+
+ toString: function () {
+ return this.label || "";
+ },
+
+ emit: function (name, ...args) {
+ emit(this, name, ...args);
+ },
+
+ /* Does the editor not have any unsaved changes? */
+ isClean: function () {
+ return true;
+ },
+
+ /**
+ * Initialize the editor with a single host. This should be called
+ * by objects extending this object with:
+ * ItchEditor.prototype.initialize.apply(this, arguments)
+ */
+ initialize: function (host) {
+ this.host = host;
+ this.doc = host.document;
+ this.label = "";
+ this.elt = this.doc.createElement("vbox");
+ this.elt.setAttribute("flex", "1");
+ this.elt.editor = this;
+ this.toolbar = this.doc.querySelector("#projecteditor-toolbar");
+ this.projectEditorKeyset = host.projectEditorKeyset;
+ this.projectEditorCommandset = host.projectEditorCommandset;
+ },
+
+ /**
+ * Sets the visibility of the element that shows up above the editor
+ * based on the this.hidesToolbar property.
+ */
+ setToolbarVisibility: function () {
+ if (this.hidesToolbar) {
+ this.toolbar.setAttribute("hidden", "true");
+ } else {
+ this.toolbar.removeAttribute("hidden");
+ }
+ },
+
+
+ /**
+ * Load a single resource into the editor.
+ *
+ * @param Resource resource
+ * The single file / item that is being dealt with (see stores/base)
+ * @returns Promise
+ * A promise that is resolved once the editor has loaded the contents
+ * of the resource.
+ */
+ load: function (resource) {
+ return promise.resolve();
+ },
+
+ /**
+ * Clean up the editor. This can have different meanings
+ * depending on the type of editor.
+ */
+ destroy: function () {
+
+ },
+
+ /**
+ * Give focus to the editor. This can have different meanings
+ * depending on the type of editor.
+ *
+ * @returns Promise
+ * A promise that is resolved once the editor has been focused.
+ */
+ focus: function () {
+ return promise.resolve();
+ }
+});
+exports.ItchEditor = ItchEditor;
+
+/**
+ * The main implementation of the ItchEditor class. The TextEditor is used
+ * when editing any sort of plain text file, and can be created with different
+ * modes for syntax highlighting depending on the language.
+ */
+var TextEditor = Class({
+ extends: ItchEditor,
+
+ isEditable: true,
+
+ /**
+ * Extra keyboard shortcuts to use with the editor. Shortcuts defined
+ * within projecteditor should be triggered when they happen in the editor, and
+ * they would usually be swallowed without registering them.
+ * See "devtools/sourceeditor/editor" for more information.
+ */
+ get extraKeys() {
+ let extraKeys = {};
+
+ // Copy all of the registered keys into extraKeys object, to notify CodeMirror
+ // that it should be ignoring these keys
+ [...this.projectEditorKeyset.querySelectorAll("key")].forEach((key) => {
+ let keyUpper = key.getAttribute("key").toUpperCase();
+ let toolModifiers = key.getAttribute("modifiers");
+ let modifiers = {
+ alt: toolModifiers.includes("alt"),
+ shift: toolModifiers.includes("shift")
+ };
+
+ // On the key press, we will dispatch the event within projecteditor.
+ extraKeys[Editor.accel(keyUpper, modifiers)] = () => {
+ let doc = this.projectEditorCommandset.ownerDocument;
+ let event = doc.createEvent("Event");
+ event.initEvent("command", true, true);
+ let command = this.projectEditorCommandset.querySelector("#" + key.getAttribute("command"));
+ command.dispatchEvent(event);
+ };
+ });
+
+ return extraKeys;
+ },
+
+ isClean: function () {
+ if (!this.editor.isAppended()) {
+ return true;
+ }
+ return this.editor.getText() === this._savedResourceContents;
+ },
+
+ initialize: function (document, mode = Editor.modes.text) {
+ ItchEditor.prototype.initialize.apply(this, arguments);
+ this.label = mode.name;
+ this.editor = new Editor({
+ mode: mode,
+ lineNumbers: true,
+ extraKeys: this.extraKeys,
+ themeSwitching: false,
+ autocomplete: true,
+ contextMenu: this.host.textEditorContextMenuPopup
+ });
+
+ // Trigger a few editor specific events on `this`.
+ this.editor.on("change", (...args) => {
+ this.emit("change", ...args);
+ });
+ this.editor.on("cursorActivity", (...args) => {
+ this.emit("cursorActivity", ...args);
+ });
+ this.editor.on("focus", (...args) => {
+ this.emit("focus", ...args);
+ });
+ this.editor.on("saveRequested", (...args) => {
+ this.emit("saveRequested", ...args);
+ });
+
+ this.appended = this.editor.appendTo(this.elt);
+ },
+
+ /**
+ * Clean up the editor. This can have different meanings
+ * depending on the type of editor.
+ */
+ destroy: function () {
+ this.editor.destroy();
+ this.editor = null;
+ },
+
+ /**
+ * Load a single resource into the text editor.
+ *
+ * @param Resource resource
+ * The single file / item that is being dealt with (see stores/base)
+ * @returns Promise
+ * A promise that is resolved once the text editor has loaded the
+ * contents of the resource.
+ */
+ load: function (resource) {
+ // Wait for the editor.appendTo and resource.load before proceeding.
+ // They can run in parallel.
+ return promise.all([
+ resource.load(),
+ this.appended
+ ]).then(([resourceContents])=> {
+ if (!this.editor) {
+ return;
+ }
+ this._savedResourceContents = resourceContents;
+ this.editor.setText(resourceContents);
+ this.editor.clearHistory();
+ this.editor.setClean();
+ this.emit("load");
+ }, console.error);
+ },
+
+ /**
+ * Save the resource based on the current state of the editor
+ *
+ * @param Resource resource
+ * The single file / item to be saved
+ * @returns Promise
+ * A promise that is resolved once the resource has been
+ * saved.
+ */
+ save: function (resource) {
+ let newText = this.editor.getText();
+ return resource.save(newText).then(() => {
+ this._savedResourceContents = newText;
+ this.emit("save", resource);
+ });
+ },
+
+ /**
+ * Give focus to the code editor.
+ *
+ * @returns Promise
+ * A promise that is resolved once the editor has been focused.
+ */
+ focus: function () {
+ return this.appended.then(() => {
+ if (this.editor) {
+ this.editor.focus();
+ }
+ });
+ }
+});
+
+/**
+ * Wrapper for TextEditor using JavaScript syntax highlighting.
+ */
+function JSEditor(host) {
+ return TextEditor(host, Editor.modes.js);
+}
+
+/**
+ * Wrapper for TextEditor using CSS syntax highlighting.
+ */
+function CSSEditor(host) {
+ return TextEditor(host, Editor.modes.css);
+}
+
+/**
+ * Wrapper for TextEditor using HTML syntax highlighting.
+ */
+function HTMLEditor(host) {
+ return TextEditor(host, Editor.modes.html);
+}
+
+/**
+ * Get the type of editor that can handle a particular resource.
+ * @param Resource resource
+ * The single file that is going to be opened.
+ * @returns Type:Editor
+ * The type of editor that can handle this resource. The
+ * return value is a constructor function.
+ */
+function EditorTypeForResource(resource) {
+ const categoryMap = {
+ "txt": TextEditor,
+ "html": HTMLEditor,
+ "xml": HTMLEditor,
+ "css": CSSEditor,
+ "js": JSEditor,
+ "json": JSEditor
+ };
+ return categoryMap[resource.contentCategory] || TextEditor;
+}
+
+exports.TextEditor = TextEditor;
+exports.JSEditor = JSEditor;
+exports.CSSEditor = CSSEditor;
+exports.HTMLEditor = HTMLEditor;
+exports.EditorTypeForResource = EditorTypeForResource;
diff --git a/devtools/client/projecteditor/lib/helpers/event.js b/devtools/client/projecteditor/lib/helpers/event.js
new file mode 100644
index 000000000..74b4adb04
--- /dev/null
+++ b/devtools/client/projecteditor/lib/helpers/event.js
@@ -0,0 +1,86 @@
+/* -*- 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/. */
+
+/**
+ * This file wraps EventEmitter objects to provide functions to forget
+ * all events bound on a certain object.
+ */
+
+const { Class } = require("sdk/core/heritage");
+
+/**
+ * The Scope object is used to keep track of listeners.
+ * This object is not exported.
+ */
+var Scope = Class({
+ on: function (target, event, handler) {
+ this.listeners = this.listeners || [];
+ this.listeners.push({
+ target: target,
+ event: event,
+ handler: handler
+ });
+ target.on(event, handler);
+ },
+
+ off: function (t, e, h) {
+ if (!this.listeners) return;
+ this.listeners = this.listeners.filter(({ target, event, handler }) => {
+ return !(target === t && event === e && handler === h);
+ });
+ target.off(event, handler);
+ },
+
+ clear: function (clearTarget) {
+ if (!this.listeners) return;
+ this.listeners = this.listeners.filter(({ target, event, handler }) => {
+ if (target === clearTarget) {
+ target.off(event, handler);
+ return false;
+ }
+ return true;
+ });
+ },
+
+ destroy: function () {
+ if (!this.listeners) return;
+ this.listeners.forEach(({ target, event, handler }) => {
+ target.off(event, handler);
+ });
+ this.listeners = undefined;
+ }
+});
+
+var scopes = new WeakMap();
+function scope(owner) {
+ if (!scopes.has(owner)) {
+ let scope = new Scope(owner);
+ scopes.set(owner, scope);
+ return scope;
+ }
+ return scopes.get(owner);
+}
+exports.scope = scope;
+
+exports.on = function on(owner, target, event, handler) {
+ if (!target) return;
+ scope(owner).on(target, event, handler);
+};
+
+exports.off = function off(owner, target, event, handler) {
+ if (!target) return;
+ scope(owner).off(target, event, handler);
+};
+
+exports.forget = function forget(owner, target) {
+ scope(owner).clear(target);
+};
+
+exports.done = function done(owner) {
+ scope(owner).destroy();
+ scopes.delete(owner);
+};
+
diff --git a/devtools/client/projecteditor/lib/helpers/file-picker.js b/devtools/client/projecteditor/lib/helpers/file-picker.js
new file mode 100644
index 000000000..1dab0f001
--- /dev/null
+++ b/devtools/client/projecteditor/lib/helpers/file-picker.js
@@ -0,0 +1,116 @@
+/* -*- 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/. */
+
+/**
+ * This file contains helper functions for showing OS-specific
+ * file and folder pickers.
+ */
+
+const { Cu, Cc, Ci } = require("chrome");
+const { FileUtils } = Cu.import("resource://gre/modules/FileUtils.jsm", {});
+const promise = require("promise");
+const { merge } = require("sdk/util/object");
+const { getLocalizedString } = require("devtools/client/projecteditor/lib/helpers/l10n");
+
+/**
+ * Show a file / folder picker.
+ * https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIFilePicker
+ *
+ * @param object options
+ * Additional options for setting the source. Supported options:
+ * - directory: string, The path to default opening
+ * - defaultName: string, The filename including extension that
+ * should be suggested to the user as a default
+ * - window: DOMWindow, The filename including extension that
+ * should be suggested to the user as a default
+ * - title: string, The filename including extension that
+ * should be suggested to the user as a default
+ * - mode: int, The type of picker to open.
+ *
+ * @return promise
+ * A promise that is resolved with the full path
+ * after the file has been picked.
+ */
+function showPicker(options) {
+ let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ if (options.directory) {
+ try {
+ fp.displayDirectory = FileUtils.File(options.directory);
+ } catch (ex) {
+ console.warn(ex);
+ }
+ }
+
+ if (options.defaultName) {
+ fp.defaultString = options.defaultName;
+ }
+
+ fp.init(options.window, options.title, options.mode);
+ let deferred = promise.defer();
+ fp.open({
+ done: function (res) {
+ if (res === Ci.nsIFilePicker.returnOK || res === Ci.nsIFilePicker.returnReplace) {
+ deferred.resolve(fp.file.path);
+ } else {
+ deferred.reject();
+ }
+ }
+ });
+ return deferred.promise;
+}
+exports.showPicker = showPicker;
+
+/**
+ * Show a save dialog
+ * https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIFilePicker
+ *
+ * @param object options
+ * Additional options as specified in showPicker
+ *
+ * @return promise
+ * A promise that is resolved when the save dialog has closed
+ */
+function showSave(options) {
+ return showPicker(merge({
+ title: getLocalizedString("projecteditor.selectFileLabel"),
+ mode: Ci.nsIFilePicker.modeSave
+ }, options));
+}
+exports.showSave = showSave;
+
+/**
+ * Show a file open dialog
+ *
+ * @param object options
+ * Additional options as specified in showPicker
+ *
+ * @return promise
+ * A promise that is resolved when the file has been opened
+ */
+function showOpen(options) {
+ return showPicker(merge({
+ title: getLocalizedString("projecteditor.openFileLabel"),
+ mode: Ci.nsIFilePicker.modeOpen
+ }, options));
+}
+exports.showOpen = showOpen;
+
+/**
+ * Show a folder open dialog
+ *
+ * @param object options
+ * Additional options as specified in showPicker
+ *
+ * @return promise
+ * A promise that is resolved when the folder has been opened
+ */
+function showOpenFolder(options) {
+ return showPicker(merge({
+ title: getLocalizedString("projecteditor.openFolderLabel"),
+ mode: Ci.nsIFilePicker.modeGetFolder
+ }, options));
+}
+exports.showOpenFolder = showOpenFolder;
diff --git a/devtools/client/projecteditor/lib/helpers/l10n.js b/devtools/client/projecteditor/lib/helpers/l10n.js
new file mode 100644
index 000000000..b2b315ff8
--- /dev/null
+++ b/devtools/client/projecteditor/lib/helpers/l10n.js
@@ -0,0 +1,26 @@
+/* -*- 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";
+
+/**
+ * This file contains helper functions for internationalizing projecteditor strings
+ */
+
+const { LocalizationHelper } = require("devtools/shared/l10n");
+const ITCHPAD_STRINGS_URI = "devtools/client/locales/projecteditor.properties";
+const L10N = new LocalizationHelper(ITCHPAD_STRINGS_URI);
+
+function getLocalizedString(name) {
+ try {
+ return L10N.getStr(name);
+ } catch (ex) {
+ console.log("Error reading '" + name + "'");
+ throw new Error("l10n error with " + name);
+ }
+}
+
+exports.getLocalizedString = getLocalizedString;
diff --git a/devtools/client/projecteditor/lib/helpers/moz.build b/devtools/client/projecteditor/lib/helpers/moz.build
new file mode 100644
index 000000000..c2e14fce6
--- /dev/null
+++ b/devtools/client/projecteditor/lib/helpers/moz.build
@@ -0,0 +1,12 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'event.js',
+ 'file-picker.js',
+ 'l10n.js',
+ 'prompts.js',
+)
diff --git a/devtools/client/projecteditor/lib/helpers/prompts.js b/devtools/client/projecteditor/lib/helpers/prompts.js
new file mode 100644
index 000000000..0df6af304
--- /dev/null
+++ b/devtools/client/projecteditor/lib/helpers/prompts.js
@@ -0,0 +1,33 @@
+/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */
+
+/**
+ * This file contains helper functions for showing user prompts.
+ * See https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIPromptService
+ */
+
+const { Cu, Cc, Ci } = require("chrome");
+const { getLocalizedString } = require("devtools/client/projecteditor/lib/helpers/l10n");
+const prompts = Cc["@mozilla.org/embedcomp/prompt-service;1"]
+ .getService(Ci.nsIPromptService);
+
+/**
+ * Show a prompt.
+ *
+ * @param string title
+ * The title to the dialog
+ * @param string message
+ * The message to display
+ *
+ * @return bool
+ * Whether the user has confirmed the action
+ */
+function confirm(title, message) {
+ var result = prompts.confirm(null, title || "Title of this Dialog", message || "Are you sure?");
+ return result;
+}
+exports.confirm = confirm;
+
diff --git a/devtools/client/projecteditor/lib/helpers/readdir.js b/devtools/client/projecteditor/lib/helpers/readdir.js
new file mode 100644
index 000000000..054730faf
--- /dev/null
+++ b/devtools/client/projecteditor/lib/helpers/readdir.js
@@ -0,0 +1,89 @@
+/* -*- 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/. */
+
+importScripts("resource://gre/modules/osfile.jsm");
+
+/**
+ * This file is meant to be loaded in a worker using:
+ * new ChromeWorker("chrome://devtools/content/projecteditor/lib/helpers/readdir.js");
+ *
+ * Read a local directory inside of a web woker
+ *
+ * @param {string} path
+ * window to inspect
+ * @param {RegExp|string} ignore
+ * A pattern to ignore certain files. This is
+ * called with file.name.match(ignore).
+ * @param {Number} maxDepth
+ * How many directories to recurse before stopping.
+ * Directories with depth > maxDepth will be ignored.
+ */
+function readDir(path, ignore, maxDepth = Infinity) {
+ let ret = {};
+
+ let set = new Set();
+
+ let info = OS.File.stat(path);
+ set.add({
+ path: path,
+ name: info.name,
+ isDir: info.isDir,
+ isSymLink: info.isSymLink,
+ depth: 0
+ });
+
+ for (let info of set) {
+ let children = [];
+
+ if (info.isDir && !info.isSymLink) {
+ if (info.depth > maxDepth) {
+ continue;
+ }
+
+ let iterator = new OS.File.DirectoryIterator(info.path);
+ try {
+ for (let child in iterator) {
+ if (ignore && child.name.match(ignore)) {
+ continue;
+ }
+
+ children.push(child.path);
+ set.add({
+ path: child.path,
+ name: child.name,
+ isDir: child.isDir,
+ isSymLink: child.isSymLink,
+ depth: info.depth + 1
+ });
+ }
+ } finally {
+ iterator.close();
+ }
+ }
+
+ ret[info.path] = {
+ name: info.name,
+ isDir: info.isDir,
+ isSymLink: info.isSymLink,
+ depth: info.depth,
+ children: children,
+ };
+ }
+
+ return ret;
+}
+
+onmessage = function (event) {
+ try {
+ let {path, ignore, depth} = event.data;
+ let message = readDir(path, ignore, depth);
+ postMessage(message);
+ } catch (ex) {
+ console.log(ex);
+ }
+};
+
+
diff --git a/devtools/client/projecteditor/lib/moz.build b/devtools/client/projecteditor/lib/moz.build
new file mode 100644
index 000000000..91b88ed91
--- /dev/null
+++ b/devtools/client/projecteditor/lib/moz.build
@@ -0,0 +1,19 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DIRS += [
+ 'helpers',
+ 'plugins',
+ 'stores',
+]
+
+DevToolsModules(
+ 'editors.js',
+ 'project.js',
+ 'projecteditor.js',
+ 'shells.js',
+ 'tree.js',
+)
diff --git a/devtools/client/projecteditor/lib/plugins/app-manager/app-project-editor.js b/devtools/client/projecteditor/lib/plugins/app-manager/app-project-editor.js
new file mode 100644
index 000000000..9a66770b0
--- /dev/null
+++ b/devtools/client/projecteditor/lib/plugins/app-manager/app-project-editor.js
@@ -0,0 +1,56 @@
+/* -*- 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/. */
+
+const { Cu } = require("chrome");
+const { Class } = require("sdk/core/heritage");
+const promise = require("promise");
+const { ItchEditor } = require("devtools/client/projecteditor/lib/editors");
+
+var AppProjectEditor = Class({
+ extends: ItchEditor,
+
+ hidesToolbar: true,
+
+ initialize: function (host) {
+ ItchEditor.prototype.initialize.apply(this, arguments);
+ this.appended = promise.resolve();
+ this.host = host;
+ this.label = "app-manager";
+ },
+
+ destroy: function () {
+ this.elt.remove();
+ this.elt = null;
+ },
+
+ load: function (resource) {
+ let {appManagerOpts} = this.host.project;
+
+ // Only load the frame the first time it is selected
+ if (!this.iframe || this.iframe.getAttribute("src") !== appManagerOpts.projectOverviewURL) {
+
+ this.elt.textContent = "";
+ let iframe = this.iframe = this.elt.ownerDocument.createElement("iframe");
+ let iframeLoaded = this.iframeLoaded = promise.defer();
+
+ iframe.addEventListener("load", function onLoad() {
+ iframe.removeEventListener("load", onLoad);
+ iframeLoaded.resolve();
+ });
+
+ iframe.setAttribute("flex", "1");
+ iframe.setAttribute("src", appManagerOpts.projectOverviewURL);
+ this.elt.appendChild(iframe);
+
+ }
+
+ promise.all([this.iframeLoaded.promise, this.appended]).then(() => {
+ this.emit("load");
+ });
+ }
+});
+
+exports.AppProjectEditor = AppProjectEditor;
diff --git a/devtools/client/projecteditor/lib/plugins/app-manager/moz.build b/devtools/client/projecteditor/lib/plugins/app-manager/moz.build
new file mode 100644
index 000000000..8aae52725
--- /dev/null
+++ b/devtools/client/projecteditor/lib/plugins/app-manager/moz.build
@@ -0,0 +1,10 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'app-project-editor.js',
+ 'plugin.js',
+)
diff --git a/devtools/client/projecteditor/lib/plugins/app-manager/plugin.js b/devtools/client/projecteditor/lib/plugins/app-manager/plugin.js
new file mode 100644
index 000000000..82bbab34b
--- /dev/null
+++ b/devtools/client/projecteditor/lib/plugins/app-manager/plugin.js
@@ -0,0 +1,77 @@
+const { Cu } = require("chrome");
+const { Class } = require("sdk/core/heritage");
+const { EventTarget } = require("sdk/event/target");
+const { emit } = require("sdk/event/core");
+const promise = require("promise");
+var { registerPlugin, Plugin } = require("devtools/client/projecteditor/lib/plugins/core");
+const { AppProjectEditor } = require("./app-project-editor");
+const OPTION_URL = "chrome://devtools/skin/images/tool-options.svg";
+const Services = require("Services");
+const Strings = Services.strings.createBundle("chrome://devtools/locale/webide.properties");
+
+var AppManagerRenderer = Class({
+ extends: Plugin,
+
+ isAppManagerProject: function () {
+ return !!this.host.project.appManagerOpts;
+ },
+ editorForResource: function (resource) {
+ if (!resource.parent && this.isAppManagerProject()) {
+ return AppProjectEditor;
+ }
+ },
+ getUI: function (parent) {
+ let doc = parent.ownerDocument;
+ if (parent.childElementCount == 0) {
+ let image = doc.createElement("image");
+ let optionImage = doc.createElement("image");
+ let flexElement = doc.createElement("div");
+ let nameLabel = doc.createElement("span");
+ let statusElement = doc.createElement("div");
+
+ image.className = "project-image";
+ optionImage.className = "project-options";
+ optionImage.setAttribute("src", OPTION_URL);
+ nameLabel.className = "project-name-label";
+ statusElement.className = "project-status";
+ flexElement.className = "project-flex";
+
+ parent.appendChild(image);
+ parent.appendChild(nameLabel);
+ parent.appendChild(flexElement);
+ parent.appendChild(statusElement);
+ parent.appendChild(optionImage);
+ }
+
+ return {
+ image: parent.querySelector(".project-image"),
+ nameLabel: parent.querySelector(".project-name-label"),
+ statusElement: parent.querySelector(".project-status")
+ };
+ },
+ onAnnotate: function (resource, editor, elt) {
+ if (resource.parent || !this.isAppManagerProject()) {
+ return;
+ }
+
+ let {appManagerOpts} = this.host.project;
+ let doc = elt.ownerDocument;
+
+ let {image, nameLabel, statusElement} = this.getUI(elt);
+ let name = appManagerOpts.name || resource.basename;
+ let url = appManagerOpts.iconUrl || "icon-sample.png";
+ let status = appManagerOpts.validationStatus || "unknown";
+ let tooltip = Strings.formatStringFromName("status_tooltip",
+ [Strings.GetStringFromName("status_" + status)], 1);
+
+ nameLabel.textContent = name;
+ image.setAttribute("src", url);
+ statusElement.setAttribute("status", status);
+ statusElement.setAttribute("tooltiptext", tooltip);
+
+ return true;
+ }
+});
+
+exports.AppManagerRenderer = AppManagerRenderer;
+registerPlugin(AppManagerRenderer);
diff --git a/devtools/client/projecteditor/lib/plugins/core.js b/devtools/client/projecteditor/lib/plugins/core.js
new file mode 100644
index 000000000..933eda043
--- /dev/null
+++ b/devtools/client/projecteditor/lib/plugins/core.js
@@ -0,0 +1,83 @@
+/* -*- 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/. */
+
+// This is the core plugin API.
+
+const { Class } = require("sdk/core/heritage");
+
+var Plugin = Class({
+ initialize: function (host) {
+ this.host = host;
+ this.init(host);
+ },
+
+ destroy: function (host) { },
+
+ init: function (host) {},
+
+ showForCategories: function (elt, categories) {
+ this._showFor = this._showFor || [];
+ let set = new Set(categories);
+ this._showFor.push({
+ elt: elt,
+ categories: new Set(categories)
+ });
+ if (this.host.currentEditor) {
+ this.onEditorActivated(this.host.currentEditor);
+ } else {
+ elt.classList.add("plugin-hidden");
+ }
+ },
+
+ priv: function (item) {
+ if (!this._privData) {
+ this._privData = new WeakMap();
+ }
+ if (!this._privData.has(item)) {
+ this._privData.set(item, {});
+ }
+ return this._privData.get(item);
+ },
+ onTreeSelected: function (resource) {},
+
+
+ // Editor state lifetime...
+ onEditorCreated: function (editor) {},
+ onEditorDestroyed: function (editor) {},
+
+ onEditorActivated: function (editor) {
+ if (this._showFor) {
+ let category = editor.category;
+ for (let item of this._showFor) {
+ if (item.categories.has(category)) {
+ item.elt.classList.remove("plugin-hidden");
+ } else {
+ item.elt.classList.add("plugin-hidden");
+ }
+ }
+ }
+ },
+ onEditorDeactivated: function (editor) {
+ if (this._showFor) {
+ for (let item of this._showFor) {
+ item.elt.classList.add("plugin-hidden");
+ }
+ }
+ },
+
+ onEditorLoad: function (editor) {},
+ onEditorSave: function (editor) {},
+ onEditorChange: function (editor) {},
+ onEditorCursorActivity: function (editor) {},
+});
+exports.Plugin = Plugin;
+
+function registerPlugin(constr) {
+ exports.registeredPlugins.push(constr);
+}
+exports.registerPlugin = registerPlugin;
+
+exports.registeredPlugins = [];
diff --git a/devtools/client/projecteditor/lib/plugins/delete/delete.js b/devtools/client/projecteditor/lib/plugins/delete/delete.js
new file mode 100644
index 000000000..b28d6a0ef
--- /dev/null
+++ b/devtools/client/projecteditor/lib/plugins/delete/delete.js
@@ -0,0 +1,67 @@
+/* -*- 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/. */
+
+const { Class } = require("sdk/core/heritage");
+const { registerPlugin, Plugin } = require("devtools/client/projecteditor/lib/plugins/core");
+const { confirm } = require("devtools/client/projecteditor/lib/helpers/prompts");
+const { getLocalizedString } = require("devtools/client/projecteditor/lib/helpers/l10n");
+
+var DeletePlugin = Class({
+ extends: Plugin,
+ shouldConfirm: true,
+
+ init: function (host) {
+ this.host.addCommand(this, {
+ id: "cmd-delete"
+ });
+ this.contextMenuItem = this.host.createMenuItem({
+ parent: this.host.contextMenuPopup,
+ label: getLocalizedString("projecteditor.deleteLabel"),
+ command: "cmd-delete"
+ });
+ },
+
+ confirmDelete: function (resource) {
+ let deletePromptMessage = resource.isDir ?
+ getLocalizedString("projecteditor.deleteFolderPromptMessage") :
+ getLocalizedString("projecteditor.deleteFilePromptMessage");
+ return !this.shouldConfirm || confirm(
+ getLocalizedString("projecteditor.deletePromptTitle"),
+ deletePromptMessage
+ );
+ },
+
+ onContextMenuOpen: function (resource) {
+ // Do not allow deletion of the top level items in the tree. In the
+ // case of the Web IDE in particular this can leave the UI in a weird
+ // state. If we'd like to add ability to delete the project folder from
+ // the tree in the future, then the UI could be cleaned up by listening
+ // to the ProjectTree's "resource-removed" event.
+ if (!resource.parent) {
+ this.contextMenuItem.setAttribute("hidden", "true");
+ } else {
+ this.contextMenuItem.removeAttribute("hidden");
+ }
+ },
+
+ onCommand: function (cmd) {
+ if (cmd === "cmd-delete") {
+ let tree = this.host.projectTree;
+ let resource = tree.getSelectedResource();
+
+ if (!this.confirmDelete(resource)) {
+ return;
+ }
+
+ resource.delete().then(() => {
+ this.host.project.refresh();
+ });
+ }
+ }
+});
+
+exports.DeletePlugin = DeletePlugin;
+registerPlugin(DeletePlugin);
diff --git a/devtools/client/projecteditor/lib/plugins/delete/moz.build b/devtools/client/projecteditor/lib/plugins/delete/moz.build
new file mode 100644
index 000000000..4b1d00466
--- /dev/null
+++ b/devtools/client/projecteditor/lib/plugins/delete/moz.build
@@ -0,0 +1,9 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'delete.js',
+)
diff --git a/devtools/client/projecteditor/lib/plugins/dirty/dirty.js b/devtools/client/projecteditor/lib/plugins/dirty/dirty.js
new file mode 100644
index 000000000..f976c626f
--- /dev/null
+++ b/devtools/client/projecteditor/lib/plugins/dirty/dirty.js
@@ -0,0 +1,47 @@
+/* -*- 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/. */
+
+const { Class } = require("sdk/core/heritage");
+const { registerPlugin, Plugin } = require("devtools/client/projecteditor/lib/plugins/core");
+const { emit } = require("sdk/event/core");
+
+var DirtyPlugin = Class({
+ extends: Plugin,
+
+ onEditorSave: function (editor) { this.onEditorChange(editor); },
+ onEditorLoad: function (editor) { this.onEditorChange(editor); },
+
+ onEditorChange: function (editor) {
+ // Only run on a TextEditor
+ if (!editor || !editor.editor) {
+ return;
+ }
+
+ // Dont' force a refresh unless the dirty state has changed...
+ let priv = this.priv(editor);
+ let clean = editor.isClean();
+ if (priv.isClean !== clean) {
+ let resource = editor.shell.resource;
+ emit(resource, "label-change", resource);
+ priv.isClean = clean;
+ }
+ },
+
+ onAnnotate: function (resource, editor, elt) {
+ // Only run on a TextEditor
+ if (!editor || !editor.editor) {
+ return;
+ }
+
+ if (!editor.isClean()) {
+ elt.textContent = "*" + resource.displayName;
+ return true;
+ }
+ }
+});
+exports.DirtyPlugin = DirtyPlugin;
+
+registerPlugin(DirtyPlugin);
diff --git a/devtools/client/projecteditor/lib/plugins/dirty/moz.build b/devtools/client/projecteditor/lib/plugins/dirty/moz.build
new file mode 100644
index 000000000..b86c5a9af
--- /dev/null
+++ b/devtools/client/projecteditor/lib/plugins/dirty/moz.build
@@ -0,0 +1,9 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'dirty.js',
+)
diff --git a/devtools/client/projecteditor/lib/plugins/image-view/image-editor.js b/devtools/client/projecteditor/lib/plugins/image-view/image-editor.js
new file mode 100644
index 000000000..668fcbeb2
--- /dev/null
+++ b/devtools/client/projecteditor/lib/plugins/image-view/image-editor.js
@@ -0,0 +1,50 @@
+/* -*- 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/. */
+
+const { Cu } = require("chrome");
+const { Class } = require("sdk/core/heritage");
+const promise = require("promise");
+const { ItchEditor } = require("devtools/client/projecteditor/lib/editors");
+
+var ImageEditor = Class({
+ extends: ItchEditor,
+
+ initialize: function () {
+ ItchEditor.prototype.initialize.apply(this, arguments);
+ this.label = "image";
+ this.appended = promise.resolve();
+ },
+
+ load: function (resource) {
+ this.elt.innerHTML = "";
+ let image = this.image = this.doc.createElement("image");
+ image.className = "editor-image";
+ image.setAttribute("src", resource.uri);
+
+ let box1 = this.doc.createElement("box");
+ box1.appendChild(image);
+
+ let box2 = this.doc.createElement("box");
+ box2.setAttribute("flex", 1);
+
+ this.elt.appendChild(box1);
+ this.elt.appendChild(box2);
+
+ this.appended.then(() => {
+ this.emit("load");
+ });
+ },
+
+ destroy: function () {
+ if (this.image) {
+ this.image.remove();
+ this.image = null;
+ }
+ }
+
+});
+
+exports.ImageEditor = ImageEditor;
diff --git a/devtools/client/projecteditor/lib/plugins/image-view/moz.build b/devtools/client/projecteditor/lib/plugins/image-view/moz.build
new file mode 100644
index 000000000..d67370e5b
--- /dev/null
+++ b/devtools/client/projecteditor/lib/plugins/image-view/moz.build
@@ -0,0 +1,10 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'image-editor.js',
+ 'plugin.js',
+)
diff --git a/devtools/client/projecteditor/lib/plugins/image-view/plugin.js b/devtools/client/projecteditor/lib/plugins/image-view/plugin.js
new file mode 100644
index 000000000..626ea3c9a
--- /dev/null
+++ b/devtools/client/projecteditor/lib/plugins/image-view/plugin.js
@@ -0,0 +1,28 @@
+/* -*- 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/. */
+
+const { Cu } = require("chrome");
+const { Class } = require("sdk/core/heritage");
+const promise = require("promise");
+const { ImageEditor } = require("./image-editor");
+const { registerPlugin, Plugin } = require("devtools/client/projecteditor/lib/plugins/core");
+
+var ImageEditorPlugin = Class({
+ extends: Plugin,
+
+ editorForResource: function (node) {
+ if (node.contentCategory === "image") {
+ return ImageEditor;
+ }
+ },
+
+ init: function (host) {
+
+ }
+});
+
+exports.ImageEditorPlugin = ImageEditorPlugin;
+registerPlugin(ImageEditorPlugin);
diff --git a/devtools/client/projecteditor/lib/plugins/logging/logging.js b/devtools/client/projecteditor/lib/plugins/logging/logging.js
new file mode 100644
index 000000000..cd5757b72
--- /dev/null
+++ b/devtools/client/projecteditor/lib/plugins/logging/logging.js
@@ -0,0 +1,29 @@
+/* -*- 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/. */
+
+var { Class } = require("sdk/core/heritage");
+var { registerPlugin, Plugin } = require("devtools/client/projecteditor/lib/plugins/core");
+
+var LoggingPlugin = Class({
+ extends: Plugin,
+
+ // Editor state lifetime...
+ onEditorCreated: function (editor) { console.log("editor created: " + editor); },
+ onEditorDestroyed: function (editor) { console.log("editor destroyed: " + editor);},
+
+ onEditorSave: function (editor) { console.log("editor saved: " + editor); },
+ onEditorLoad: function (editor) { console.log("editor loaded: " + editor); },
+
+ onEditorActivated: function (editor) { console.log("editor activated: " + editor);},
+ onEditorDeactivated: function (editor) { console.log("editor deactivated: " + editor);},
+
+ onEditorChange: function (editor) { console.log("editor changed: " + editor);},
+
+ onCommand: function (cmd) { console.log("Command: " + cmd); }
+});
+exports.LoggingPlugin = LoggingPlugin;
+
+registerPlugin(LoggingPlugin);
diff --git a/devtools/client/projecteditor/lib/plugins/logging/moz.build b/devtools/client/projecteditor/lib/plugins/logging/moz.build
new file mode 100644
index 000000000..5d8d98fbe
--- /dev/null
+++ b/devtools/client/projecteditor/lib/plugins/logging/moz.build
@@ -0,0 +1,9 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'logging.js',
+)
diff --git a/devtools/client/projecteditor/lib/plugins/moz.build b/devtools/client/projecteditor/lib/plugins/moz.build
new file mode 100644
index 000000000..17bff7ce0
--- /dev/null
+++ b/devtools/client/projecteditor/lib/plugins/moz.build
@@ -0,0 +1,21 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DIRS += [
+ 'app-manager',
+ 'delete',
+ 'dirty',
+ 'image-view',
+ 'logging',
+ 'new',
+ 'rename',
+ 'save',
+ 'status-bar',
+]
+
+DevToolsModules(
+ 'core.js',
+)
diff --git a/devtools/client/projecteditor/lib/plugins/new/moz.build b/devtools/client/projecteditor/lib/plugins/new/moz.build
new file mode 100644
index 000000000..3caacefb1
--- /dev/null
+++ b/devtools/client/projecteditor/lib/plugins/new/moz.build
@@ -0,0 +1,9 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'new.js',
+)
diff --git a/devtools/client/projecteditor/lib/plugins/new/new.js b/devtools/client/projecteditor/lib/plugins/new/new.js
new file mode 100644
index 000000000..220cb4977
--- /dev/null
+++ b/devtools/client/projecteditor/lib/plugins/new/new.js
@@ -0,0 +1,80 @@
+/* -*- 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/. */
+
+const { Class } = require("sdk/core/heritage");
+const { registerPlugin, Plugin } = require("devtools/client/projecteditor/lib/plugins/core");
+const { getLocalizedString } = require("devtools/client/projecteditor/lib/helpers/l10n");
+
+// Handles the new command.
+var NewFile = Class({
+ extends: Plugin,
+
+ init: function () {
+ this.command = this.host.addCommand(this, {
+ id: "cmd-new",
+ key: getLocalizedString("projecteditor.new.commandkey"),
+ modifiers: "accel"
+ });
+ this.host.createMenuItem({
+ parent: this.host.fileMenuPopup,
+ label: getLocalizedString("projecteditor.newLabel"),
+ command: "cmd-new",
+ key: "key_cmd-new"
+ });
+ this.host.createMenuItem({
+ parent: this.host.contextMenuPopup,
+ label: getLocalizedString("projecteditor.newLabel"),
+ command: "cmd-new"
+ });
+ },
+
+ onCommand: function (cmd) {
+ if (cmd === "cmd-new") {
+ let tree = this.host.projectTree;
+ let resource = tree.getSelectedResource();
+ parent = resource.isDir ? resource : resource.parent;
+ sibling = resource.isDir ? null : resource;
+
+ if (!("createChild" in parent)) {
+ return;
+ }
+
+ let extension = sibling ? sibling.contentCategory : parent.store.defaultCategory;
+ let template = "untitled{1}." + extension;
+ let name = this.suggestName(parent, template);
+
+ tree.promptNew(name, parent, sibling).then(name => {
+
+ // XXX: sanitize bad file names.
+
+ // If the name is already taken, just add/increment a number.
+ if (parent.hasChild(name)) {
+ let matches = name.match(/([^\d.]*)(\d*)([^.]*)(.*)/);
+ template = matches[1] + "{1}" + matches[3] + matches[4];
+ name = this.suggestName(parent, template, parseInt(matches[2]) || 2);
+ }
+
+ return parent.createChild(name);
+ }).then(resource => {
+ tree.selectResource(resource);
+ this.host.currentEditor.focus();
+ }).then(null, console.error);
+ }
+ },
+
+ suggestName: function (parent, template, start = 1) {
+ let i = start;
+ let name;
+ do {
+ name = template.replace("\{1\}", i === 1 ? "" : i);
+ i++;
+ } while (parent.hasChild(name));
+
+ return name;
+ }
+});
+exports.NewFile = NewFile;
+registerPlugin(NewFile);
diff --git a/devtools/client/projecteditor/lib/plugins/rename/moz.build b/devtools/client/projecteditor/lib/plugins/rename/moz.build
new file mode 100644
index 000000000..2b1612452
--- /dev/null
+++ b/devtools/client/projecteditor/lib/plugins/rename/moz.build
@@ -0,0 +1,9 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'rename.js',
+)
diff --git a/devtools/client/projecteditor/lib/plugins/rename/rename.js b/devtools/client/projecteditor/lib/plugins/rename/rename.js
new file mode 100644
index 000000000..850401869
--- /dev/null
+++ b/devtools/client/projecteditor/lib/plugins/rename/rename.js
@@ -0,0 +1,74 @@
+/* -*- 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/. */
+
+const { Class } = require("sdk/core/heritage");
+const { registerPlugin, Plugin } = require("devtools/client/projecteditor/lib/plugins/core");
+const { getLocalizedString } = require("devtools/client/projecteditor/lib/helpers/l10n");
+
+var RenamePlugin = Class({
+ extends: Plugin,
+
+ init: function (host) {
+ this.host.addCommand(this, {
+ id: "cmd-rename"
+ });
+ this.contextMenuItem = this.host.createMenuItem({
+ parent: this.host.contextMenuPopup,
+ label: getLocalizedString("projecteditor.renameLabel"),
+ command: "cmd-rename"
+ });
+ },
+
+ onContextMenuOpen: function (resource) {
+ if (resource.isRoot) {
+ this.contextMenuItem.setAttribute("hidden", "true");
+ } else {
+ this.contextMenuItem.removeAttribute("hidden");
+ }
+ },
+
+ onCommand: function (cmd) {
+ if (cmd === "cmd-rename") {
+ let tree = this.host.projectTree;
+ let resource = tree.getSelectedResource();
+ let parent = resource.parent;
+ let oldName = resource.basename;
+
+ tree.promptEdit(oldName, resource).then(name => {
+ if (name === oldName) {
+ return resource;
+ }
+ if (parent.hasChild(name)) {
+ let matches = name.match(/([^\d.]*)(\d*)([^.]*)(.*)/);
+ let template = matches[1] + "{1}" + matches[3] + matches[4];
+ name = this.suggestName(resource, template, parseInt(matches[2]) || 2);
+ }
+ return parent.rename(oldName, name);
+ }).then(resource => {
+ this.host.project.refresh();
+ tree.selectResource(resource);
+ if (!resource.isDir) {
+ this.host.currentEditor.focus();
+ }
+ }).then(null, console.error);
+ }
+ },
+
+ suggestName: function (resource, template, start = 1) {
+ let i = start;
+ let name;
+ let parent = resource.parent;
+ do {
+ name = template.replace("\{1\}", i === 1 ? "" : i);
+ i++;
+ } while (parent.hasChild(name));
+
+ return name;
+ }
+});
+
+exports.RenamePlugin = RenamePlugin;
+registerPlugin(RenamePlugin);
diff --git a/devtools/client/projecteditor/lib/plugins/save/moz.build b/devtools/client/projecteditor/lib/plugins/save/moz.build
new file mode 100644
index 000000000..66df054eb
--- /dev/null
+++ b/devtools/client/projecteditor/lib/plugins/save/moz.build
@@ -0,0 +1,9 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'save.js',
+)
diff --git a/devtools/client/projecteditor/lib/plugins/save/save.js b/devtools/client/projecteditor/lib/plugins/save/save.js
new file mode 100644
index 000000000..43b2185d2
--- /dev/null
+++ b/devtools/client/projecteditor/lib/plugins/save/save.js
@@ -0,0 +1,93 @@
+/* -*- 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/. */
+
+const { Class } = require("sdk/core/heritage");
+const { registerPlugin, Plugin } = require("devtools/client/projecteditor/lib/plugins/core");
+const picker = require("devtools/client/projecteditor/lib/helpers/file-picker");
+const { getLocalizedString } = require("devtools/client/projecteditor/lib/helpers/l10n");
+
+// Handles the save command.
+var SavePlugin = Class({
+ extends: Plugin,
+
+ init: function (host) {
+
+ this.host.addCommand(this, {
+ id: "cmd-save",
+ key: getLocalizedString("projecteditor.save.commandkey"),
+ modifiers: "accel"
+ });
+ this.host.addCommand(this, {
+ id: "cmd-saveas",
+ key: getLocalizedString("projecteditor.save.commandkey"),
+ modifiers: "accel shift"
+ });
+ this.host.createMenuItem({
+ parent: this.host.fileMenuPopup,
+ label: getLocalizedString("projecteditor.saveLabel"),
+ command: "cmd-save",
+ key: "key_cmd-save"
+ });
+ this.host.createMenuItem({
+ parent: this.host.fileMenuPopup,
+ label: getLocalizedString("projecteditor.saveAsLabel"),
+ command: "cmd-saveas",
+ key: "key_cmd-saveas"
+ });
+ },
+
+ isCommandEnabled: function (cmd) {
+ let currentEditor = this.host.currentEditor;
+ return currentEditor.isEditable;
+ },
+
+ onCommand: function (cmd) {
+ if (cmd === "cmd-save") {
+ this.onEditorSaveRequested();
+ } else if (cmd === "cmd-saveas") {
+ this.saveAs();
+ }
+ },
+
+ saveAs: function () {
+ let editor = this.host.currentEditor;
+ let project = this.host.resourceFor(editor);
+
+ let resource;
+ picker.showSave({
+ window: this.host.window,
+ directory: project && project.parent ? project.parent.path : null,
+ defaultName: project ? project.basename : null,
+ }).then(path => {
+ return this.createResource(path);
+ }).then(res => {
+ resource = res;
+ return this.saveResource(editor, resource);
+ }).then(() => {
+ this.host.openResource(resource);
+ }).then(null, console.error);
+ },
+
+ onEditorSaveRequested: function () {
+ let editor = this.host.currentEditor;
+ let resource = this.host.resourceFor(editor);
+ if (!resource) {
+ return this.saveAs();
+ }
+
+ return this.saveResource(editor, resource);
+ },
+
+ createResource: function (path) {
+ return this.host.project.resourceFor(path, { create: true });
+ },
+
+ saveResource: function (editor, resource) {
+ return editor.save(resource);
+ }
+});
+exports.SavePlugin = SavePlugin;
+registerPlugin(SavePlugin);
diff --git a/devtools/client/projecteditor/lib/plugins/status-bar/moz.build b/devtools/client/projecteditor/lib/plugins/status-bar/moz.build
new file mode 100644
index 000000000..87ce21584
--- /dev/null
+++ b/devtools/client/projecteditor/lib/plugins/status-bar/moz.build
@@ -0,0 +1,9 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'plugin.js',
+)
diff --git a/devtools/client/projecteditor/lib/plugins/status-bar/plugin.js b/devtools/client/projecteditor/lib/plugins/status-bar/plugin.js
new file mode 100644
index 000000000..9450baef3
--- /dev/null
+++ b/devtools/client/projecteditor/lib/plugins/status-bar/plugin.js
@@ -0,0 +1,105 @@
+/* -*- 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/. */
+
+const { Cu } = require("chrome");
+const { Class } = require("sdk/core/heritage");
+const promise = require("promise");
+const { registerPlugin, Plugin } = require("devtools/client/projecteditor/lib/plugins/core");
+
+/**
+ * Print information about the currently opened file
+ * and the state of the current editor
+ */
+var StatusBarPlugin = Class({
+ extends: Plugin,
+
+ init: function () {
+ this.box = this.host.createElement("hbox", {
+ parent: "#projecteditor-toolbar-bottom"
+ });
+
+ this.activeMode = this.host.createElement("label", {
+ parent: this.box,
+ class: "projecteditor-basic-display"
+ });
+
+ this.cursorPosition = this.host.createElement("label", {
+ parent: this.box,
+ class: "projecteditor-basic-display"
+ });
+
+ this.fileLabel = this.host.createElement("label", {
+ parent: "#plugin-toolbar-left",
+ class: "projecteditor-file-label"
+ });
+ },
+
+ destroy: function () {
+ },
+
+ /**
+ * Print information about the current state of the editor
+ *
+ * @param Editor editor
+ */
+ render: function (editor, resource) {
+ if (!resource || resource.isDir) {
+ this.fileLabel.textContent = "";
+ this.cursorPosition.value = "";
+ return;
+ }
+
+ this.fileLabel.textContent = resource.basename;
+ this.activeMode.value = editor.toString();
+ if (editor.editor) {
+ let cursorStart = editor.editor.getCursor("start");
+ let cursorEnd = editor.editor.getCursor("end");
+ if (cursorStart.line === cursorEnd.line && cursorStart.ch === cursorEnd.ch) {
+ this.cursorPosition.value = cursorStart.line + " " + cursorStart.ch;
+ } else {
+ this.cursorPosition.value = cursorStart.line + " " + cursorStart.ch + " | " +
+ cursorEnd.line + " " + cursorEnd.ch;
+ }
+ } else {
+ this.cursorPosition.value = "";
+ }
+ },
+
+
+ /**
+ * Print the current file name
+ *
+ * @param Resource resource
+ */
+ onTreeSelected: function (resource) {
+ if (!resource || resource.isDir) {
+ this.fileLabel.textContent = "";
+ return;
+ }
+ this.fileLabel.textContent = resource.basename;
+ },
+
+ onEditorDeactivated: function (editor) {
+ this.fileLabel.textContent = "";
+ this.cursorPosition.value = "";
+ },
+
+ onEditorChange: function (editor, resource) {
+ this.render(editor, resource);
+ },
+
+ onEditorCursorActivity: function (editor, resource) {
+ this.render(editor, resource);
+ },
+
+ onEditorActivated: function (editor, resource) {
+ this.render(editor, resource);
+ },
+
+});
+
+exports.StatusBarPlugin = StatusBarPlugin;
+registerPlugin(StatusBarPlugin);
diff --git a/devtools/client/projecteditor/lib/project.js b/devtools/client/projecteditor/lib/project.js
new file mode 100644
index 000000000..8e0a8802d
--- /dev/null
+++ b/devtools/client/projecteditor/lib/project.js
@@ -0,0 +1,246 @@
+/* -*- 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/. */
+
+const { Cu } = require("chrome");
+const { Class } = require("sdk/core/heritage");
+const { EventTarget } = require("sdk/event/target");
+const { emit } = require("sdk/event/core");
+const { scope, on, forget } = require("devtools/client/projecteditor/lib/helpers/event");
+const prefs = require("sdk/preferences/service");
+const { LocalStore } = require("devtools/client/projecteditor/lib/stores/local");
+const { OS } = Cu.import("resource://gre/modules/osfile.jsm", {});
+const { Task } = require("devtools/shared/task");
+const promise = require("promise");
+const { TextEncoder, TextDecoder } = require("sdk/io/buffer");
+const url = require("sdk/url");
+
+const gDecoder = new TextDecoder();
+const gEncoder = new TextEncoder();
+
+/**
+ * A Project keeps track of the opened folders using LocalStore
+ * objects. Resources are generally requested from the project,
+ * even though the Store is actually keeping track of them.
+ *
+ *
+ * This object emits the following events:
+ * - "refresh-complete": After all stores have been refreshed from disk.
+ * - "store-added": When a store has been added to the project.
+ * - "store-removed": When a store has been removed from the project.
+ * - "resource-added": When a resource has been added to one of the stores.
+ * - "resource-removed": When a resource has been removed from one of the stores.
+ */
+var Project = Class({
+ extends: EventTarget,
+
+ /**
+ * Intialize the Project.
+ *
+ * @param Object options
+ * Options to be passed into Project.load function
+ */
+ initialize: function (options) {
+ this.localStores = new Map();
+
+ this.load(options);
+ },
+
+ destroy: function () {
+ // We are removing the store because the project never gets persisted.
+ // There may need to be separate destroy functionality that doesn't remove
+ // from project if this is saved to DB.
+ this.removeAllStores();
+ },
+
+ toString: function () {
+ return "[Project] " + this.name;
+ },
+
+ /**
+ * Load a project given metadata about it.
+ *
+ * @param Object options
+ * Information about the project, containing:
+ * id: An ID (currently unused, but could be used for saving)
+ * name: The display name of the project
+ * directories: An array of path strings to load
+ */
+ load: function (options) {
+ this.id = options.id;
+ this.name = options.name || "Untitled";
+
+ let paths = new Set(options.directories.map(name => OS.Path.normalize(name)));
+
+ for (let [path, store] of this.localStores) {
+ if (!paths.has(path)) {
+ this.removePath(path);
+ }
+ }
+
+ for (let path of paths) {
+ this.addPath(path);
+ }
+ },
+
+ /**
+ * Refresh all project stores from disk
+ *
+ * @returns Promise
+ * A promise that resolves when everything has been refreshed.
+ */
+ refresh: function () {
+ return Task.spawn(function* () {
+ for (let [path, store] of this.localStores) {
+ yield store.refresh();
+ }
+ emit(this, "refresh-complete");
+ }.bind(this));
+ },
+
+
+ /**
+ * Fetch a resource from the backing storage system for the store.
+ *
+ * @param string path
+ * The path to fetch
+ * @param Object options
+ * "create": bool indicating whether to create a file if it does not exist.
+ * @returns Promise
+ * A promise that resolves with the Resource.
+ */
+ resourceFor: function (path, options) {
+ let store = this.storeContaining(path);
+ return store.resourceFor(path, options);
+ },
+
+ /**
+ * Get every resource used inside of the project.
+ *
+ * @returns Array<Resource>
+ * A list of all Resources in all Stores.
+ */
+ allResources: function () {
+ let resources = [];
+ for (let store of this.allStores()) {
+ resources = resources.concat(store.allResources());
+ }
+ return resources;
+ },
+
+ /**
+ * Get every Path used inside of the project.
+ *
+ * @returns generator-iterator<Store>
+ * A list of all Stores
+ */
+ allStores: function* () {
+ for (let [path, store] of this.localStores) {
+ yield store;
+ }
+ },
+
+ /**
+ * Get every file path used inside of the project.
+ *
+ * @returns Array<string>
+ * A list of all file paths
+ */
+ allPaths: function () {
+ return [...this.localStores.keys()];
+ },
+
+ /**
+ * Get the store that contains a path.
+ *
+ * @returns Store
+ * The store, if any. Will return null if no store
+ * contains the given path.
+ */
+ storeContaining: function (path) {
+ let containingStore = null;
+ for (let store of this.allStores()) {
+ if (store.contains(path)) {
+ // With nested projects, the final containing store will be returned.
+ containingStore = store;
+ }
+ }
+ return containingStore;
+ },
+
+ /**
+ * Add a store at the current path. If a store already exists
+ * for this path, then return it.
+ *
+ * @param string path
+ * @returns LocalStore
+ */
+ addPath: function (path) {
+ if (!this.localStores.has(path)) {
+ this.addLocalStore(new LocalStore(path));
+ }
+ return this.localStores.get(path);
+ },
+
+ /**
+ * Remove a store for a given path.
+ *
+ * @param string path
+ */
+ removePath: function (path) {
+ this.removeLocalStore(this.localStores.get(path));
+ },
+
+
+ /**
+ * Add the given Store to the project.
+ * Fires a 'store-added' event on the project.
+ *
+ * @param Store store
+ */
+ addLocalStore: function (store) {
+ store.canPair = true;
+ this.localStores.set(store.path, store);
+
+ // Originally StoreCollection.addStore
+ on(this, store, "resource-added", (resource) => {
+ emit(this, "resource-added", resource);
+ });
+ on(this, store, "resource-removed", (resource) => {
+ emit(this, "resource-removed", resource);
+ });
+
+ emit(this, "store-added", store);
+ },
+
+
+ /**
+ * Remove all of the Stores belonging to the project.
+ */
+ removeAllStores: function () {
+ for (let store of this.allStores()) {
+ this.removeLocalStore(store);
+ }
+ },
+
+ /**
+ * Remove the given Store from the project.
+ * Fires a 'store-removed' event on the project.
+ *
+ * @param Store store
+ */
+ removeLocalStore: function (store) {
+ // XXX: tree selection should be reset if active element is affected by
+ // the store being removed
+ if (store) {
+ this.localStores.delete(store.path);
+ forget(this, store);
+ emit(this, "store-removed", store);
+ store.destroy();
+ }
+ }
+});
+
+exports.Project = Project;
diff --git a/devtools/client/projecteditor/lib/projecteditor.js b/devtools/client/projecteditor/lib/projecteditor.js
new file mode 100644
index 000000000..a3ef06249
--- /dev/null
+++ b/devtools/client/projecteditor/lib/projecteditor.js
@@ -0,0 +1,816 @@
+/* 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 { Class } = require("sdk/core/heritage");
+const { Project } = require("devtools/client/projecteditor/lib/project");
+const { ProjectTreeView } = require("devtools/client/projecteditor/lib/tree");
+const { ShellDeck } = require("devtools/client/projecteditor/lib/shells");
+const { Resource } = require("devtools/client/projecteditor/lib/stores/resource");
+const { registeredPlugins } = require("devtools/client/projecteditor/lib/plugins/core");
+const { EventTarget } = require("sdk/event/target");
+const { on, forget } = require("devtools/client/projecteditor/lib/helpers/event");
+const { emit } = require("sdk/event/core");
+const { merge } = require("sdk/util/object");
+const promise = require("promise");
+const { ViewHelpers } = require("devtools/client/shared/widgets/view-helpers");
+const { DOMHelpers } = require("resource://devtools/client/shared/DOMHelpers.jsm");
+const Services = require("Services");
+const { Task } = require("devtools/shared/task");
+const ITCHPAD_URL = "chrome://devtools/content/projecteditor/chrome/content/projecteditor.xul";
+const { confirm } = require("devtools/client/projecteditor/lib/helpers/prompts");
+const { getLocalizedString } = require("devtools/client/projecteditor/lib/helpers/l10n");
+
+// Enabled Plugins
+require("devtools/client/projecteditor/lib/plugins/dirty/dirty");
+require("devtools/client/projecteditor/lib/plugins/delete/delete");
+require("devtools/client/projecteditor/lib/plugins/new/new");
+require("devtools/client/projecteditor/lib/plugins/rename/rename");
+require("devtools/client/projecteditor/lib/plugins/save/save");
+require("devtools/client/projecteditor/lib/plugins/image-view/plugin");
+require("devtools/client/projecteditor/lib/plugins/app-manager/plugin");
+require("devtools/client/projecteditor/lib/plugins/status-bar/plugin");
+
+// Uncomment to enable logging.
+// require("devtools/client/projecteditor/lib/plugins/logging/logging");
+
+/**
+ * This is the main class tying together an instance of the ProjectEditor.
+ * The frontend is contained inside of this.iframe, which loads projecteditor.xul.
+ *
+ * Usage:
+ * let projecteditor = new ProjectEditor(frame);
+ * projecteditor.loaded.then((projecteditor) => {
+ * // Ready to use.
+ * });
+ *
+ * Responsible for maintaining:
+ * - The list of Plugins for this instance.
+ * - The ShellDeck, which includes all Shells for opened Resources
+ * -- Shells take in a Resource, and construct the appropriate Editor
+ * - The Project, which includes all Stores for this instance
+ * -- Stores manage all Resources starting from a root directory
+ * --- Resources are a representation of a file on disk
+ * - The ProjectTreeView that builds the UI for interacting with the
+ * project.
+ *
+ * This object emits the following events:
+ * - "onEditorDestroyed": When editor is destroyed
+ * - "onEditorSave": When editor is saved
+ * - "onEditorLoad": When editor is loaded
+ * - "onEditorActivated": When editor is activated
+ * - "onEditorChange": When editor is changed
+ * - "onEditorCursorActivity": When there is cursor activity in a text editor
+ * - "onCommand": When a command happens
+ * - "onEditorDestroyed": When editor is destroyed
+ * - "onContextMenuOpen": When the context menu is opened on the project tree
+ *
+ * The events can be bound like so:
+ * projecteditor.on("onEditorCreated", (editor) => { });
+ */
+var ProjectEditor = Class({
+ extends: EventTarget,
+
+ /**
+ * Initialize ProjectEditor, and load into an iframe if specified.
+ *
+ * @param Iframe iframe
+ * The iframe to inject the DOM into. If this is not
+ * specified, then this.load(frame) will need to be called
+ * before accessing ProjectEditor.
+ * @param Object options
+ * - menubar: a <menubar> element to inject menus into
+ * - menuindex: Integer child index to insert menus
+ */
+ initialize: function (iframe, options = {}) {
+ this._onTreeSelected = this._onTreeSelected.bind(this);
+ this._onTreeResourceRemoved = this._onTreeResourceRemoved.bind(this);
+ this._onEditorCreated = this._onEditorCreated.bind(this);
+ this._onEditorActivated = this._onEditorActivated.bind(this);
+ this._onEditorDeactivated = this._onEditorDeactivated.bind(this);
+ this._updateMenuItems = this._updateMenuItems.bind(this);
+ this._updateContextMenuItems = this._updateContextMenuItems.bind(this);
+ this.destroy = this.destroy.bind(this);
+ this.menubar = options.menubar || null;
+ this.menuindex = options.menuindex || null;
+ this._menuEnabled = true;
+ this._destroyed = false;
+ this._loaded = false;
+ this._pluginCommands = new Map();
+ if (iframe) {
+ this.load(iframe);
+ }
+ },
+
+ /**
+ * Load the instance inside of a specified iframe.
+ * This can be called more than once, and it will return the promise
+ * from the first call.
+ *
+ * @param Iframe iframe
+ * The iframe to inject the projecteditor DOM into
+ * @returns Promise
+ * A promise that is resolved once the iframe has been
+ * loaded.
+ */
+ load: function (iframe) {
+ if (this.loaded) {
+ return this.loaded;
+ }
+
+ let deferred = promise.defer();
+ this.loaded = deferred.promise;
+ this.iframe = iframe;
+
+ let domReady = () => {
+ if (this._destroyed) {
+ deferred.reject("Error: ProjectEditor has been destroyed before loading");
+ return;
+ }
+ this._onLoad();
+ this._loaded = true;
+ deferred.resolve(this);
+ };
+
+ let domHelper = new DOMHelpers(this.iframe.contentWindow);
+ domHelper.onceDOMReady(domReady);
+
+ this.iframe.setAttribute("src", ITCHPAD_URL);
+
+ return this.loaded;
+ },
+
+ /**
+ * Build the projecteditor DOM inside of this.iframe.
+ */
+ _onLoad: function () {
+ this.document = this.iframe.contentDocument;
+ this.window = this.iframe.contentWindow;
+
+ this._initCommands();
+ this._buildMenubar();
+ this._buildSidebar();
+
+ this.window.addEventListener("unload", this.destroy, false);
+
+ // Editor management
+ this.shells = new ShellDeck(this, this.document);
+ this.shells.on("editor-created", this._onEditorCreated);
+ this.shells.on("editor-activated", this._onEditorActivated);
+ this.shells.on("editor-deactivated", this._onEditorDeactivated);
+
+ let shellContainer = this.document.querySelector("#shells-deck-container");
+ shellContainer.appendChild(this.shells.elt);
+
+ // We are not allowing preset projects for now - rebuild a fresh one
+ // each time.
+ this.setProject(new Project({
+ id: "",
+ name: "",
+ directories: [],
+ openFiles: []
+ }));
+
+ this._initPlugins();
+ },
+
+ _buildMenubar: function () {
+
+ this.contextMenuPopup = this.document.getElementById("context-menu-popup");
+ this.contextMenuPopup.addEventListener("popupshowing", this._updateContextMenuItems);
+
+ this.textEditorContextMenuPopup = this.document.getElementById("texteditor-context-popup");
+ this.textEditorContextMenuPopup.addEventListener("popupshowing", this._updateMenuItems);
+
+ this.editMenu = this.document.getElementById("edit-menu");
+ this.fileMenu = this.document.getElementById("file-menu");
+
+ this.editMenuPopup = this.document.getElementById("edit-menu-popup");
+ this.fileMenuPopup = this.document.getElementById("file-menu-popup");
+ this.editMenu.addEventListener("popupshowing", this._updateMenuItems);
+ this.fileMenu.addEventListener("popupshowing", this._updateMenuItems);
+
+ if (this.menubar) {
+ let body = this.menubar.ownerDocument.body ||
+ this.menubar.ownerDocument.querySelector("window");
+ body.appendChild(this.projectEditorCommandset);
+ body.appendChild(this.projectEditorKeyset);
+ body.appendChild(this.editorCommandset);
+ body.appendChild(this.editorKeyset);
+ body.appendChild(this.contextMenuPopup);
+ body.appendChild(this.textEditorContextMenuPopup);
+
+ let index = this.menuindex || 0;
+ this.menubar.insertBefore(this.editMenu, this.menubar.children[index]);
+ this.menubar.insertBefore(this.fileMenu, this.menubar.children[index]);
+ } else {
+ this.document.getElementById("projecteditor-menubar").style.display = "block";
+ }
+
+ // Insert a controller to allow enabling and disabling of menu items.
+ this._commandWindow = this.editorCommandset.ownerDocument.defaultView;
+ this._commandController = getCommandController(this);
+ this._commandWindow.controllers.insertControllerAt(0, this._commandController);
+ },
+
+ /**
+ * Create the project tree sidebar that lists files.
+ */
+ _buildSidebar: function () {
+ this.projectTree = new ProjectTreeView(this.document, {
+ resourceVisible: this.resourceVisible.bind(this),
+ resourceFormatter: this.resourceFormatter.bind(this),
+ contextMenuPopup: this.contextMenuPopup
+ });
+ on(this, this.projectTree, "selection", this._onTreeSelected);
+ on(this, this.projectTree, "resource-removed", this._onTreeResourceRemoved);
+
+ let sourcesBox = this.document.querySelector("#sources > vbox");
+ sourcesBox.appendChild(this.projectTree.elt);
+ },
+
+ /**
+ * Set up listeners for commands to dispatch to all of the plugins
+ */
+ _initCommands: function () {
+
+ this.projectEditorCommandset = this.document.getElementById("projecteditor-commandset");
+ this.projectEditorKeyset = this.document.getElementById("projecteditor-keyset");
+
+ this.editorCommandset = this.document.getElementById("editMenuCommands");
+ this.editorKeyset = this.document.getElementById("editMenuKeys");
+
+ this.projectEditorCommandset.addEventListener("command", (evt) => {
+ evt.stopPropagation();
+ evt.preventDefault();
+ this.pluginDispatch("onCommand", evt.target.id, evt.target);
+ });
+ },
+
+ /**
+ * Initialize each plugin in registeredPlugins
+ */
+ _initPlugins: function () {
+ this._plugins = [];
+
+ for (let plugin of registeredPlugins) {
+ try {
+ this._plugins.push(plugin(this));
+ } catch (ex) {
+ console.exception(ex);
+ }
+ }
+
+ this.pluginDispatch("lateInit");
+ },
+
+ /**
+ * Enable / disable necessary menu items using globalOverlay.js.
+ */
+ _updateMenuItems: function () {
+ let window = this.editMenu.ownerDocument.defaultView;
+ let commands = ["cmd_undo", "cmd_redo", "cmd_delete", "cmd_cut", "cmd_copy", "cmd_paste"];
+ commands.forEach(window.goUpdateCommand);
+
+ for (let c of this._pluginCommands.keys()) {
+ window.goUpdateCommand(c);
+ }
+ },
+
+ /**
+ * Enable / disable necessary context menu items by passing an event
+ * onto plugins.
+ */
+ _updateContextMenuItems: function () {
+ let resource = this.projectTree.getSelectedResource();
+ this.pluginDispatch("onContextMenuOpen", resource);
+ },
+
+ /**
+ * Destroy all objects on the iframe unload event.
+ */
+ destroy: function () {
+ this._destroyed = true;
+
+
+ // If been destroyed before the iframe finished loading, then
+ // the properties below will not exist.
+ if (!this._loaded) {
+ this.iframe.setAttribute("src", "about:blank");
+ return;
+ }
+
+ // Reset the src for the iframe so if it reused for a new ProjectEditor
+ // instance, the load will fire properly.
+ this.window.removeEventListener("unload", this.destroy, false);
+ this.iframe.setAttribute("src", "about:blank");
+
+ this._plugins.forEach(plugin => { plugin.destroy(); });
+
+ forget(this, this.projectTree);
+ this.projectTree.destroy();
+ this.projectTree = null;
+
+ this.shells.destroy();
+
+ this.projectEditorCommandset.remove();
+ this.projectEditorKeyset.remove();
+ this.editorCommandset.remove();
+ this.editorKeyset.remove();
+ this.contextMenuPopup.remove();
+ this.textEditorContextMenuPopup.remove();
+ this.editMenu.remove();
+ this.fileMenu.remove();
+
+ this._commandWindow.controllers.removeController(this._commandController);
+ this._commandController = null;
+
+ forget(this, this.project);
+ this.project.destroy();
+ this.project = null;
+ },
+
+ /**
+ * Set the current project viewed by the projecteditor.
+ *
+ * @param Project project
+ * The project to set.
+ */
+ setProject: function (project) {
+ if (this.project) {
+ forget(this, this.project);
+ }
+ this.project = project;
+ this.projectTree.setProject(project);
+
+ // Whenever a store gets removed, clean up any editors that
+ // exist for resources within it.
+ on(this, project, "store-removed", (store) => {
+ store.allResources().forEach((resource) => {
+ this.shells.removeResource(resource);
+ });
+ });
+ },
+
+ /**
+ * Set the current project viewed by the projecteditor to a single path,
+ * used by the app manager.
+ *
+ * @param string path
+ * The file path to set
+ * @param Object opts
+ * Custom options used by the project.
+ * - name: display name for project
+ * - iconUrl: path to icon for project
+ * - validationStatus: one of 'unknown|error|warning|valid'
+ * - projectOverviewURL: path to load for iframe when project
+ * is selected in the tree.
+ * @param Promise
+ * Promise that is resolved once the project is ready to be used.
+ */
+ setProjectToAppPath: function (path, opts = {}) {
+ this.project.appManagerOpts = opts;
+
+ let existingPaths = this.project.allPaths();
+ if (existingPaths.length !== 1 || existingPaths[0] !== path) {
+ // Only fully reset if this is a new path.
+ this.project.removeAllStores();
+ this.project.addPath(path);
+ } else {
+ // Otherwise, just ask for the root to be redrawn
+ let rootResource = this.project.localStores.get(path).root;
+ emit(rootResource, "label-change", rootResource);
+ }
+
+ return this.project.refresh();
+ },
+
+ /**
+ * Open a resource in a particular shell.
+ *
+ * @param Resource resource
+ * The file to be opened.
+ */
+ openResource: function (resource) {
+ let shell = this.shells.open(resource);
+ this.projectTree.selectResource(resource);
+ shell.editor.focus();
+ },
+
+ /**
+ * When a node is selected in the tree, open its associated editor.
+ *
+ * @param Resource resource
+ * The file that has been selected
+ */
+ _onTreeSelected: function (resource) {
+ // Don't attempt to open a directory that is not the root element.
+ if (resource.isDir && resource.parent) {
+ return;
+ }
+ this.pluginDispatch("onTreeSelected", resource);
+ this.openResource(resource);
+ },
+
+ /**
+ * When a node is removed, destroy it and its associated editor.
+ *
+ * @param Resource resource
+ * The resource being removed
+ */
+ _onTreeResourceRemoved: function (resource) {
+ this.shells.removeResource(resource);
+ },
+
+ /**
+ * Create an xul element with options
+ *
+ * @param string type
+ * The tag name of the element to create.
+ * @param Object options
+ * "command": DOMNode or string ID of a command element.
+ * "parent": DOMNode or selector of parent to append child to.
+ * anything other keys are set as an attribute as the element.
+ * @returns DOMElement
+ * The element that has been created.
+ */
+ createElement: function (type, options) {
+ let elt = this.document.createElement(type);
+
+ let parent;
+
+ for (let opt in options) {
+ if (opt === "command") {
+ let command = typeof (options.command) === "string" ? options.command : options.command.id;
+ elt.setAttribute("command", command);
+ } else if (opt === "parent") {
+ continue;
+ } else {
+ elt.setAttribute(opt, options[opt]);
+ }
+ }
+
+ if (options.parent) {
+ let parent = options.parent;
+ if (typeof (parent) === "string") {
+ parent = this.document.querySelector(parent);
+ }
+ parent.appendChild(elt);
+ }
+
+ return elt;
+ },
+
+ /**
+ * Create a "menuitem" xul element with options
+ *
+ * @param Object options
+ * See createElement for available options.
+ * @returns DOMElement
+ * The menuitem that has been created.
+ */
+ createMenuItem: function (options) {
+ return this.createElement("menuitem", options);
+ },
+
+ /**
+ * Add a command to the projecteditor document.
+ * This method is meant to be used with plugins.
+ *
+ * @param Object definition
+ * key: a key/keycode string. Example: "f".
+ * id: Unique ID. Example: "find".
+ * modifiers: Key modifiers. Example: "accel".
+ * @returns DOMElement
+ * The command element that has been created.
+ */
+ addCommand: function (plugin, definition) {
+ this._pluginCommands.set(definition.id, plugin);
+ let document = this.projectEditorKeyset.ownerDocument;
+ let command = document.createElement("command");
+ command.setAttribute("id", definition.id);
+ if (definition.key) {
+ let key = document.createElement("key");
+ key.id = "key_" + definition.id;
+
+ let keyName = definition.key;
+ if (keyName.startsWith("VK_")) {
+ key.setAttribute("keycode", keyName);
+ } else {
+ key.setAttribute("key", keyName);
+ }
+ key.setAttribute("modifiers", definition.modifiers);
+ key.setAttribute("command", definition.id);
+ this.projectEditorKeyset.appendChild(key);
+ }
+ command.setAttribute("oncommand", "void(0);"); // needed. See bug 371900
+ this.projectEditorCommandset.appendChild(command);
+ return command;
+ },
+
+ /**
+ * Get the instance of a plugin registered with a certain type.
+ *
+ * @param Type pluginType
+ * The type, such as SavePlugin
+ * @returns Plugin
+ * The plugin instance matching the specified type.
+ */
+ getPlugin: function (pluginType) {
+ for (let plugin of this.plugins) {
+ if (plugin.constructor === pluginType) {
+ return plugin;
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Get all plugin instances active for the current project
+ *
+ * @returns [Plugin]
+ */
+ get plugins() {
+ if (!this._plugins) {
+ console.log("plugins requested before _plugins was set");
+ return [];
+ }
+ // Could filter further based on the type of project selected,
+ // but no need right now.
+ return this._plugins;
+ },
+
+ /**
+ * Dispatch an onEditorCreated event, and listen for other events specific
+ * to this editor instance.
+ *
+ * @param Editor editor
+ * The new editor instance.
+ */
+ _onEditorCreated: function (editor) {
+ this.pluginDispatch("onEditorCreated", editor);
+ this._editorListenAndDispatch(editor, "change", "onEditorChange");
+ this._editorListenAndDispatch(editor, "cursorActivity", "onEditorCursorActivity");
+ this._editorListenAndDispatch(editor, "load", "onEditorLoad");
+ this._editorListenAndDispatch(editor, "saveRequested", "onEditorSaveRequested");
+ this._editorListenAndDispatch(editor, "save", "onEditorSave");
+
+ editor.on("focus", () => {
+ this.projectTree.selectResource(this.resourceFor(editor));
+ });
+ },
+
+ /**
+ * Dispatch an onEditorActivated event and finish setting up once the
+ * editor is ready to use.
+ *
+ * @param Editor editor
+ * The editor instance, which is now appended in the document.
+ * @param Resource resource
+ * The resource used by the editor
+ */
+ _onEditorActivated: function (editor, resource) {
+ editor.setToolbarVisibility();
+ this.pluginDispatch("onEditorActivated", editor, resource);
+ },
+
+ /**
+ * Dispatch an onEditorDactivated event once an editor loses focus
+ *
+ * @param Editor editor
+ * The editor instance, which is no longer active.
+ * @param Resource resource
+ * The resource used by the editor
+ */
+ _onEditorDeactivated: function (editor, resource) {
+ this.pluginDispatch("onEditorDeactivated", editor, resource);
+ },
+
+ /**
+ * Call a method on all plugins that implement the method.
+ * Also emits the same handler name on `this`.
+ *
+ * @param string handler
+ * Which function name to call on plugins.
+ * @param ...args args
+ * All remaining parameters are passed into the handler.
+ */
+ pluginDispatch: function (handler, ...args) {
+ emit(this, handler, ...args);
+ this.plugins.forEach(plugin => {
+ try {
+ if (handler in plugin) plugin[handler](...args);
+ } catch (ex) {
+ console.error(ex);
+ }
+ });
+ },
+
+ /**
+ * Listen to an event on the editor object and dispatch it
+ * to all plugins that implement the associated method
+ *
+ * @param Editor editor
+ * Which editor to listen to
+ * @param string event
+ * Which editor event to listen for
+ * @param string handler
+ * Which plugin method to call
+ */
+ _editorListenAndDispatch: function (editor, event, handler) {
+ editor.on(event, (...args) => {
+ this.pluginDispatch(handler, editor, this.resourceFor(editor), ...args);
+ });
+ },
+
+ /**
+ * Find a shell for a resource.
+ *
+ * @param Resource resource
+ * The file to be opened.
+ * @returns Shell
+ */
+ shellFor: function (resource) {
+ return this.shells.shellFor(resource);
+ },
+
+ /**
+ * Returns the Editor for a given resource.
+ *
+ * @param Resource resource
+ * The file to check.
+ * @returns Editor
+ * Instance of the editor for this file.
+ */
+ editorFor: function (resource) {
+ let shell = this.shellFor(resource);
+ return shell ? shell.editor : shell;
+ },
+
+ /**
+ * Returns a resource for the given editor
+ *
+ * @param Editor editor
+ * The editor to check
+ * @returns Resource
+ * The resource associated with this editor
+ */
+ resourceFor: function (editor) {
+ if (editor && editor.shell && editor.shell.resource) {
+ return editor.shell.resource;
+ }
+ return null;
+ },
+
+ /**
+ * Decide whether a given resource should be hidden in the tree.
+ *
+ * @param Resource resource
+ * The resource in the tree
+ * @returns Boolean
+ * True if the node should be visible, false if hidden.
+ */
+ resourceVisible: function (resource) {
+ return true;
+ },
+
+ /**
+ * Format the given node for display in the resource tree view.
+ *
+ * @param Resource resource
+ * The file to be opened.
+ * @param DOMNode elt
+ * The element in the tree to render into.
+ */
+ resourceFormatter: function (resource, elt) {
+ let editor = this.editorFor(resource);
+ let renderedByPlugin = false;
+
+ // Allow plugins to override default templating of resource in tree.
+ this.plugins.forEach(plugin => {
+ if (!plugin.onAnnotate) {
+ return;
+ }
+ if (plugin.onAnnotate(resource, editor, elt)) {
+ renderedByPlugin = true;
+ }
+ });
+
+ // If no plugin wants to handle it, just use a string from the resource.
+ if (!renderedByPlugin) {
+ elt.textContent = resource.displayName;
+ }
+ },
+
+ get sourcesVisible() {
+ return this.sourceToggle.classList.contains("pane-collapsed");
+ },
+
+ get currentShell() {
+ return this.shells.currentShell;
+ },
+
+ get currentEditor() {
+ return this.shells.currentEditor;
+ },
+
+ /**
+ * Whether or not menu items should be able to be enabled.
+ * Note that even if this is true, certain menu items will not be
+ * enabled until the correct state is achieved (for instance, the
+ * 'copy' menu item is only enabled when there is a selection).
+ * But if this is false, then nothing will be enabled.
+ */
+ set menuEnabled(val) {
+ this._menuEnabled = val;
+ if (this._loaded) {
+ this._updateMenuItems();
+ }
+ },
+
+ get menuEnabled() {
+ return this._menuEnabled;
+ },
+
+ /**
+ * Are there any unsaved resources in the Project?
+ */
+ get hasUnsavedResources() {
+ return this.project.allResources().some(resource=> {
+ let editor = this.editorFor(resource);
+ return editor && !editor.isClean();
+ });
+ },
+
+ /**
+ * Check with the user about navigating away with unsaved changes.
+ *
+ * @returns Boolean
+ * True if there are no unsaved changes
+ * Otherwise, ask the user to confirm and return the outcome.
+ */
+ confirmUnsaved: function () {
+ if (this.hasUnsavedResources) {
+ return confirm(
+ getLocalizedString("projecteditor.confirmUnsavedTitle"),
+ getLocalizedString("projecteditor.confirmUnsavedLabel2")
+ );
+ }
+
+ return true;
+ },
+
+ /**
+ * Save all the changes in source files.
+ *
+ * @returns Boolean
+ * True if there were resources to save.
+ */
+ saveAllFiles: Task.async(function* () {
+ if (this.hasUnsavedResources) {
+ for (let resource of this.project.allResources()) {
+ let editor = this.editorFor(resource);
+ if (editor && !editor.isClean()) {
+ yield editor.save(resource);
+ }
+ }
+
+ return true;
+ }
+
+ return false;
+ })
+
+});
+
+
+/**
+ * Returns a controller object that can be used for
+ * editor-specific commands such as find, jump to line,
+ * copy/paste, etc.
+ */
+function getCommandController(host) {
+ return {
+ supportsCommand: function (cmd) {
+ return host._pluginCommands.get(cmd);
+ },
+
+ isCommandEnabled: function (cmd) {
+ if (!host.menuEnabled) {
+ return false;
+ }
+ let plugin = host._pluginCommands.get(cmd);
+ if (plugin && plugin.isCommandEnabled) {
+ return plugin.isCommandEnabled(cmd);
+ }
+ return true;
+ },
+ doCommand: function (cmd) {
+ }
+ };
+}
+
+exports.ProjectEditor = ProjectEditor;
diff --git a/devtools/client/projecteditor/lib/shells.js b/devtools/client/projecteditor/lib/shells.js
new file mode 100644
index 000000000..8004f24a2
--- /dev/null
+++ b/devtools/client/projecteditor/lib/shells.js
@@ -0,0 +1,243 @@
+/* -*- 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/. */
+
+const { Cu } = require("chrome");
+const { Class } = require("sdk/core/heritage");
+const { EventTarget } = require("sdk/event/target");
+const { emit } = require("sdk/event/core");
+const { EditorTypeForResource } = require("devtools/client/projecteditor/lib/editors");
+const NetworkHelper = require("devtools/shared/webconsole/network-helper");
+const promise = require("promise");
+
+/**
+ * The Shell is the object that manages the editor for a single resource.
+ * It is in charge of selecting the proper Editor (text/image/plugin-defined)
+ * and instantiating / appending the editor.
+ * This object is not exported, it is just used internally by the ShellDeck.
+ *
+ * This object has a promise `editorAppended`, that will resolve once the editor
+ * is ready to be used.
+ */
+var Shell = Class({
+ extends: EventTarget,
+
+ /**
+ * @param ProjectEditor host
+ * @param Resource resource
+ */
+ initialize: function (host, resource) {
+ this.host = host;
+ this.doc = host.document;
+ this.resource = resource;
+ this.elt = this.doc.createElement("vbox");
+ this.elt.classList.add("view-project-detail");
+ this.elt.shell = this;
+
+ let constructor = this._editorTypeForResource();
+
+ this.editor = constructor(this.host);
+ this.editor.shell = this;
+ this.editorAppended = this.editor.appended;
+
+ this.editor.on("load", () => {
+ this.editorDeferred.resolve();
+ });
+ this.elt.appendChild(this.editor.elt);
+ },
+
+ /**
+ * Start loading the resource. The 'load' event happens as
+ * a result of this function, so any listeners to 'editorAppended'
+ * need to be added before calling this.
+ */
+ load: function () {
+ this.editorDeferred = promise.defer();
+ this.editorLoaded = this.editorDeferred.promise;
+ this.editor.load(this.resource);
+ },
+
+ /**
+ * Destroy the shell and its associated editor
+ */
+ destroy: function () {
+ this.editor.destroy();
+ this.resource.destroy();
+ },
+
+ /**
+ * Make sure the correct editor is selected for the resource.
+ * @returns Type:Editor
+ */
+ _editorTypeForResource: function () {
+ let resource = this.resource;
+ let constructor = EditorTypeForResource(resource);
+
+ if (this.host.plugins) {
+ this.host.plugins.forEach(plugin => {
+ if (plugin.editorForResource) {
+ let pluginEditor = plugin.editorForResource(resource);
+ if (pluginEditor) {
+ constructor = pluginEditor;
+ }
+ }
+ });
+ }
+
+ return constructor;
+ }
+});
+
+/**
+ * The ShellDeck is in charge of managing the list of active Shells for
+ * the current ProjectEditor instance (aka host).
+ *
+ * This object emits the following events:
+ * - "editor-created": When an editor is initially created
+ * - "editor-activated": When an editor is ready to use
+ * - "editor-deactivated": When an editor is ready to use
+ */
+var ShellDeck = Class({
+ extends: EventTarget,
+
+ /**
+ * @param ProjectEditor host
+ * @param Document document
+ */
+ initialize: function (host, document) {
+ this.doc = document;
+ this.host = host;
+ this.deck = this.doc.createElement("deck");
+ this.deck.setAttribute("flex", "1");
+ this.elt = this.deck;
+
+ this.shells = new Map();
+
+ this._activeShell = null;
+ },
+
+ /**
+ * Open a resource in a Shell. Will create the Shell
+ * if it doesn't exist yet.
+ *
+ * @param Resource resource
+ * The file to be opened
+ * @returns Shell
+ */
+ open: function (defaultResource) {
+ let shell = this.shellFor(defaultResource);
+ if (!shell) {
+ shell = this._createShell(defaultResource);
+ this.shells.set(defaultResource, shell);
+ }
+ this.selectShell(shell);
+ return shell;
+ },
+
+ /**
+ * Create a new Shell for a resource. Called by `open`.
+ *
+ * @returns Shell
+ */
+ _createShell: function (defaultResource) {
+ let shell = Shell(this.host, defaultResource);
+
+ shell.editorAppended.then(() => {
+ this.shells.set(shell.resource, shell);
+ emit(this, "editor-created", shell.editor);
+ if (this.currentShell === shell) {
+ this.selectShell(shell);
+ }
+
+ });
+
+ shell.load();
+ this.deck.appendChild(shell.elt);
+ return shell;
+ },
+
+ /**
+ * Remove the shell for a given resource.
+ *
+ * @param Resource resource
+ */
+ removeResource: function (resource) {
+ let shell = this.shellFor(resource);
+ if (shell) {
+ this.shells.delete(resource);
+ shell.destroy();
+ }
+ },
+
+ destroy: function () {
+ for (let [resource, shell] of this.shells.entries()) {
+ this.shells.delete(resource);
+ shell.destroy();
+ }
+ },
+
+ /**
+ * Select a given shell and open its editor.
+ * Will fire editor-deactivated on the old selected Shell (if any),
+ * and editor-activated on the new one once it is ready
+ *
+ * @param Shell shell
+ */
+ selectShell: function (shell) {
+ // Don't fire another activate if this is already the active shell
+ if (this._activeShell != shell) {
+ if (this._activeShell) {
+ emit(this, "editor-deactivated", this._activeShell.editor, this._activeShell.resource);
+ }
+ this.deck.selectedPanel = shell.elt;
+ this._activeShell = shell;
+
+ // Only reload the shell if the editor doesn't have local changes.
+ if (shell.editor.isClean()) {
+ shell.load();
+ }
+ shell.editorLoaded.then(() => {
+ // Handle case where another shell has been requested before this
+ // one is finished loading.
+ if (this._activeShell === shell) {
+ emit(this, "editor-activated", shell.editor, shell.resource);
+ }
+ });
+ }
+ },
+
+ /**
+ * Find a Shell for a Resource.
+ *
+ * @param Resource resource
+ * @returns Shell
+ */
+ shellFor: function (resource) {
+ return this.shells.get(resource);
+ },
+
+ /**
+ * The currently active Shell. Note: the editor may not yet be available
+ * on the current shell. Best to wait for the 'editor-activated' event
+ * instead.
+ *
+ * @returns Shell
+ */
+ get currentShell() {
+ return this._activeShell;
+ },
+
+ /**
+ * The currently active Editor, or null if it is not ready.
+ *
+ * @returns Editor
+ */
+ get currentEditor() {
+ let shell = this.currentShell;
+ return shell ? shell.editor : null;
+ },
+
+});
+exports.ShellDeck = ShellDeck;
diff --git a/devtools/client/projecteditor/lib/stores/base.js b/devtools/client/projecteditor/lib/stores/base.js
new file mode 100644
index 000000000..ef9495c77
--- /dev/null
+++ b/devtools/client/projecteditor/lib/stores/base.js
@@ -0,0 +1,58 @@
+/* -*- 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/. */
+
+const { Cc, Ci, Cu } = require("chrome");
+const { Class } = require("sdk/core/heritage");
+const { EventTarget } = require("sdk/event/target");
+const { emit } = require("sdk/event/core");
+const promise = require("promise");
+
+/**
+ * A Store object maintains a collection of Resource objects stored in a tree.
+ *
+ * The Store class should not be instantiated directly. Instead, you should
+ * use a class extending it - right now this is only a LocalStore.
+ *
+ * Events:
+ * This object emits the 'resource-added' and 'resource-removed' events.
+ */
+var Store = Class({
+ extends: EventTarget,
+
+ /**
+ * Should be called during initialize() of a subclass.
+ */
+ initStore: function () {
+ this.resources = new Map();
+ },
+
+ refresh: function () {
+ return promise.resolve();
+ },
+
+ /**
+ * Return a sorted Array of all Resources in the Store
+ */
+ allResources: function () {
+ var resources = [];
+ function addResource(resource) {
+ resources.push(resource);
+ resource.childrenSorted.forEach(addResource);
+ }
+ addResource(this.root);
+ return resources;
+ },
+
+ notifyAdd: function (resource) {
+ emit(this, "resource-added", resource);
+ },
+
+ notifyRemove: function (resource) {
+ emit(this, "resource-removed", resource);
+ }
+});
+
+exports.Store = Store;
diff --git a/devtools/client/projecteditor/lib/stores/local.js b/devtools/client/projecteditor/lib/stores/local.js
new file mode 100644
index 000000000..1f782dadf
--- /dev/null
+++ b/devtools/client/projecteditor/lib/stores/local.js
@@ -0,0 +1,215 @@
+/* -*- 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/. */
+
+const { Cc, Ci, Cu, ChromeWorker } = require("chrome");
+const { Class } = require("sdk/core/heritage");
+const { OS } = require("resource://gre/modules/osfile.jsm");
+const { emit } = require("sdk/event/core");
+const { Store } = require("devtools/client/projecteditor/lib/stores/base");
+const { Task } = require("devtools/shared/task");
+const promise = require("promise");
+const Services = require("Services");
+const { on, forget } = require("devtools/client/projecteditor/lib/helpers/event");
+const { FileResource } = require("devtools/client/projecteditor/lib/stores/resource");
+
+const CHECK_LINKED_DIRECTORY_DELAY = 5000;
+const SHOULD_LIVE_REFRESH = true;
+// XXX: Ignores should be customizable
+const IGNORE_REGEX = /(^\.)|(\~$)|(^node_modules$)/;
+
+/**
+ * A LocalStore object maintains a collection of Resource objects
+ * from the file system.
+ *
+ * This object emits the following events:
+ * - "resource-added": When a resource is added
+ * - "resource-removed": When a resource is removed
+ */
+var LocalStore = Class({
+ extends: Store,
+
+ defaultCategory: "js",
+
+ initialize: function(path) {
+ this.initStore();
+ this.path = OS.Path.normalize(path);
+ this.rootPath = this.path;
+ this.displayName = this.path;
+ this.root = this._forPath(this.path);
+ this.notifyAdd(this.root);
+ this.refreshLoop = this.refreshLoop.bind(this);
+ this.refreshLoop();
+ },
+
+ destroy: function() {
+ clearTimeout(this._refreshTimeout);
+
+ if (this._refreshDeferred) {
+ this._refreshDeferred.reject("destroy");
+ }
+ if (this.worker) {
+ this.worker.terminate();
+ }
+
+ this._refreshTimeout = null;
+ this._refreshDeferred = null;
+ this.worker = null;
+
+ if (this.root) {
+ forget(this, this.root);
+ this.root.destroy();
+ }
+ },
+
+ toString: function() { return "[LocalStore:" + this.path + "]" },
+
+ /**
+ * Return a FileResource object for the given path. If a FileInfo
+ * is provided the resource will use it, otherwise the FileResource
+ * might not have full information until the next refresh.
+ *
+ * The following parameters are passed into the FileResource constructor
+ * See resource.js for information about them
+ *
+ * @param String path
+ * @param FileInfo info
+ * @returns Resource
+ */
+ _forPath: function(path, info=null) {
+ if (this.resources.has(path)) {
+ return this.resources.get(path);
+ }
+
+ let resource = FileResource(this, path, info);
+ this.resources.set(path, resource);
+ return resource;
+ },
+
+ /**
+ * Return a promise that resolves to a fully-functional FileResource
+ * within this project. This will hit the disk for stat info.
+ * options:
+ *
+ * create: If true, a resource will be created even if the underlying
+ * file doesn't exist.
+ */
+ resourceFor: function(path, options) {
+ path = OS.Path.normalize(path);
+
+ if (this.resources.has(path)) {
+ return promise.resolve(this.resources.get(path));
+ }
+
+ if (!this.contains(path)) {
+ return promise.reject(new Error(path + " does not belong to " + this.path));
+ }
+
+ return Task.spawn(function*() {
+ let parent = yield this.resourceFor(OS.Path.dirname(path));
+
+ let info;
+ try {
+ info = yield OS.File.stat(path);
+ } catch (ex if ex instanceof OS.File.Error && ex.becauseNoSuchFile) {
+ if (!options.create) {
+ throw ex;
+ }
+ }
+
+ let resource = this._forPath(path, info);
+ parent.addChild(resource);
+ return resource;
+ }.bind(this));
+ },
+
+ refreshLoop: function() {
+ // XXX: Once Bug 958280 adds a watch function, will not need to forever loop here.
+ this.refresh().then(() => {
+ if (SHOULD_LIVE_REFRESH) {
+ this._refreshTimeout = setTimeout(this.refreshLoop,
+ CHECK_LINKED_DIRECTORY_DELAY);
+ }
+ });
+ },
+
+ _refreshTimeout: null,
+ _refreshDeferred: null,
+
+ /**
+ * Refresh the directory structure.
+ */
+ refresh: function(path=this.rootPath) {
+ if (this._refreshDeferred) {
+ return this._refreshDeferred.promise;
+ }
+ this._refreshDeferred = promise.defer();
+
+ let worker = this.worker = new ChromeWorker("chrome://devtools/content/projecteditor/lib/helpers/readdir.js");
+ let start = Date.now();
+
+ worker.onmessage = evt => {
+ // console.log("Directory read finished in " + ( Date.now() - start ) +"ms", evt);
+ for (path in evt.data) {
+ let info = evt.data[path];
+ info.path = path;
+
+ let resource = this._forPath(path, info);
+ resource.info = info;
+ if (info.isDir) {
+ let newChildren = new Set();
+ for (let childPath of info.children) {
+ childInfo = evt.data[childPath];
+ newChildren.add(this._forPath(childPath, childInfo));
+ }
+ resource.setChildren(newChildren);
+ }
+ resource.info.children = null;
+ }
+
+ worker = null;
+ this._refreshDeferred.resolve();
+ this._refreshDeferred = null;
+ };
+ worker.onerror = ex => {
+ console.error(ex);
+ worker = null;
+ this._refreshDeferred.reject(ex);
+ this._refreshDeferred = null;
+ }
+ worker.postMessage({ path: this.rootPath, ignore: IGNORE_REGEX });
+ return this._refreshDeferred.promise;
+ },
+
+ /**
+ * Returns true if the given path would be a child of the store's
+ * root directory.
+ */
+ contains: function(path) {
+ path = OS.Path.normalize(path);
+ let thisPath = OS.Path.split(this.rootPath);
+ let thatPath = OS.Path.split(path)
+
+ if (!(thisPath.absolute && thatPath.absolute)) {
+ throw new Error("Contains only works with absolute paths.");
+ }
+
+ if (thisPath.winDrive && (thisPath.winDrive != thatPath.winDrive)) {
+ return false;
+ }
+
+ if (thatPath.components.length <= thisPath.components.length) {
+ return false;
+ }
+
+ for (let i = 0; i < thisPath.components.length; i++) {
+ if (thisPath.components[i] != thatPath.components[i]) {
+ return false;
+ }
+ }
+ return true;
+ }
+});
+exports.LocalStore = LocalStore;
diff --git a/devtools/client/projecteditor/lib/stores/moz.build b/devtools/client/projecteditor/lib/stores/moz.build
new file mode 100644
index 000000000..5a6becd92
--- /dev/null
+++ b/devtools/client/projecteditor/lib/stores/moz.build
@@ -0,0 +1,11 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'base.js',
+ 'local.js',
+ 'resource.js',
+)
diff --git a/devtools/client/projecteditor/lib/stores/resource.js b/devtools/client/projecteditor/lib/stores/resource.js
new file mode 100644
index 000000000..53e3e7348
--- /dev/null
+++ b/devtools/client/projecteditor/lib/stores/resource.js
@@ -0,0 +1,398 @@
+/* -*- 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 { Cc, Ci, Cu } = require("chrome");
+const { TextEncoder, TextDecoder } = require("sdk/io/buffer");
+const { Class } = require("sdk/core/heritage");
+const { EventTarget } = require("sdk/event/target");
+const { emit } = require("sdk/event/core");
+const URL = require("sdk/url");
+const promise = require("promise");
+const { OS } = Cu.import("resource://gre/modules/osfile.jsm", {});
+const { FileUtils } = Cu.import("resource://gre/modules/FileUtils.jsm", {});
+const mimeService = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
+const { Task } = require("devtools/shared/task");
+
+const gDecoder = new TextDecoder();
+const gEncoder = new TextEncoder();
+
+/**
+ * A Resource is a single file-like object that can be respresented
+ * as a file for ProjectEditor.
+ *
+ * The Resource class is not exported, and should not be instantiated
+ * Instead, you should use the FileResource class that extends it.
+ *
+ * This object emits the following events:
+ * - "children-changed": When a child has been added or removed.
+ * See setChildren.
+ * - "deleted": When the resource has been deleted.
+ */
+var Resource = Class({
+ extends: EventTarget,
+
+ refresh: function () { return promise.resolve(this); },
+ destroy: function () { },
+ delete: function () { },
+
+ setURI: function (uri) {
+ if (typeof (uri) === "string") {
+ uri = URL.URL(uri);
+ }
+ this.uri = uri;
+ },
+
+ /**
+ * Is there more than 1 child Resource?
+ */
+ get hasChildren() { return this.children && this.children.size > 0; },
+
+ /**
+ * Is this Resource the root (top level for the store)?
+ */
+ get isRoot() {
+ return !this.parent;
+ },
+
+ /**
+ * Sorted array of children for display
+ */
+ get childrenSorted() {
+ if (!this.hasChildren) {
+ return [];
+ }
+
+ return [...this.children].sort((a, b)=> {
+ // Put directories above files.
+ if (a.isDir !== b.isDir) {
+ return b.isDir;
+ }
+ return a.basename.toLowerCase() > b.basename.toLowerCase();
+ });
+ },
+
+ /**
+ * Set the children set of this Resource, and notify of any
+ * additions / removals that happened in the change.
+ */
+ setChildren: function (newChildren) {
+ let oldChildren = this.children || new Set();
+ let change = false;
+
+ for (let child of oldChildren) {
+ if (!newChildren.has(child)) {
+ change = true;
+ child.parent = null;
+ this.store.notifyRemove(child);
+ }
+ }
+
+ for (let child of newChildren) {
+ if (!oldChildren.has(child)) {
+ change = true;
+ child.parent = this;
+ this.store.notifyAdd(child);
+ }
+ }
+
+ this.children = newChildren;
+ if (change) {
+ emit(this, "children-changed", this);
+ }
+ },
+
+ /**
+ * Add a resource to children set and notify of the change.
+ *
+ * @param Resource resource
+ */
+ addChild: function (resource) {
+ this.children = this.children || new Set();
+
+ resource.parent = this;
+ this.children.add(resource);
+ this.store.notifyAdd(resource);
+ emit(this, "children-changed", this);
+ return resource;
+ },
+
+ /**
+ * Checks if current object has child with specific name.
+ *
+ * @param string name
+ */
+ hasChild: function (name) {
+ for (let child of this.children) {
+ if (child.basename === name) {
+ return true;
+ }
+ }
+ return false;
+ },
+
+ /**
+ * Remove a resource to children set and notify of the change.
+ *
+ * @param Resource resource
+ */
+ removeChild: function (resource) {
+ resource.parent = null;
+ this.children.remove(resource);
+ this.store.notifyRemove(resource);
+ emit(this, "children-changed", this);
+ return resource;
+ },
+
+ /**
+ * Return a set with children, children of children, etc -
+ * gathered recursively.
+ *
+ * @returns Set<Resource>
+ */
+ allDescendants: function () {
+ let set = new Set();
+
+ function addChildren(item) {
+ if (!item.children) {
+ return;
+ }
+
+ for (let child of item.children) {
+ set.add(child);
+ }
+ }
+
+ addChildren(this);
+ for (let item of set) {
+ addChildren(item);
+ }
+
+ return set;
+ },
+});
+
+/**
+ * A FileResource is an implementation of Resource for a File System
+ * backing. This is exported, and should be used instead of Resource.
+ */
+var FileResource = Class({
+ extends: Resource,
+
+ /**
+ * @param Store store
+ * @param String path
+ * @param FileInfo info
+ * https://developer.mozilla.org/en-US/docs/JavaScript_OS.File/OS.File.Info
+ */
+ initialize: function (store, path, info) {
+ this.store = store;
+ this.path = path;
+
+ this.setURI(URL.URL(URL.fromFilename(path)));
+ this._lastReadModification = undefined;
+
+ this.info = info;
+ this.parent = null;
+ },
+
+ toString: function () {
+ return "[FileResource:" + this.path + "]";
+ },
+
+ destroy: function () {
+ if (this._refreshDeferred) {
+ this._refreshDeferred.reject();
+ }
+ this._refreshDeferred = null;
+ },
+
+ /**
+ * Fetch and cache information about this particular file.
+ * https://developer.mozilla.org/en-US/docs/JavaScript_OS.File/OS.File_for_the_main_thread#OS.File.stat
+ *
+ * @returns Promise
+ * Resolves once the File.stat has finished.
+ */
+ refresh: function () {
+ if (this._refreshDeferred) {
+ return this._refreshDeferred.promise;
+ }
+ this._refreshDeferred = promise.defer();
+ OS.File.stat(this.path).then(info => {
+ this.info = info;
+ if (this._refreshDeferred) {
+ this._refreshDeferred.resolve(this);
+ this._refreshDeferred = null;
+ }
+ });
+ return this._refreshDeferred.promise;
+ },
+
+ /**
+ * Return the trailing name component of this Resource
+ */
+ get basename() {
+ return this.path.replace(/\/+$/, "").replace(/\\/g, "/").replace(/.*\//, "");
+ },
+
+ /**
+ * A string to be used when displaying this Resource in views
+ */
+ get displayName() {
+ return this.basename + (this.isDir ? "/" : "");
+ },
+
+ /**
+ * Is this FileResource a directory? Rather than checking children
+ * here, we use this.info. So this could return a false negative
+ * if there was no info passed in on constructor and the first
+ * refresh hasn't yet finished.
+ */
+ get isDir() {
+ if (!this.info) { return false; }
+ return this.info.isDir && !this.info.isSymLink;
+ },
+
+ /**
+ * Read the file as a string asynchronously.
+ *
+ * @returns Promise
+ * Resolves with the text of the file.
+ */
+ load: function () {
+ return OS.File.read(this.path).then(bytes => {
+ return gDecoder.decode(bytes);
+ });
+ },
+
+ /**
+ * Delete the file from the filesystem
+ *
+ * @returns Promise
+ * Resolves when the file is deleted
+ */
+ delete: function () {
+ emit(this, "deleted", this);
+ if (this.isDir) {
+ return OS.File.removeDir(this.path);
+ } else {
+ return OS.File.remove(this.path);
+ }
+ },
+
+ /**
+ * Add a text file as a child of this FileResource.
+ * This instance must be a directory.
+ *
+ * @param string name
+ * The filename (path will be generated based on this.path).
+ * string initial
+ * The content to write to the new file.
+ * @returns Promise
+ * Resolves with the new FileResource once it has
+ * been written to disk.
+ * Rejected if this is not a directory.
+ */
+ createChild: function (name, initial = "") {
+ if (!this.isDir) {
+ return promise.reject(new Error("Cannot add child to a regular file"));
+ }
+
+ let newPath = OS.Path.join(this.path, name);
+
+ let buffer = initial ? gEncoder.encode(initial) : "";
+ return OS.File.writeAtomic(newPath, buffer, {
+ noOverwrite: true
+ }).then(() => {
+ return this.store.refresh();
+ }).then(() => {
+ let resource = this.store.resources.get(newPath);
+ if (!resource) {
+ throw new Error("Error creating " + newPath);
+ }
+ return resource;
+ });
+ },
+
+ /**
+ * Rename the file from the filesystem
+ *
+ * @returns Promise
+ * Resolves with the renamed FileResource.
+ */
+ rename: function (oldName, newName) {
+ let oldPath = OS.Path.join(this.path, oldName);
+ let newPath = OS.Path.join(this.path, newName);
+
+ return OS.File.move(oldPath, newPath).then(() => {
+ return this.store.refresh();
+ }).then(() => {
+ let resource = this.store.resources.get(newPath);
+ if (!resource) {
+ throw new Error("Error creating " + newPath);
+ }
+ return resource;
+ });
+ },
+
+ /**
+ * Write a string to this file.
+ *
+ * @param string content
+ * @returns Promise
+ * Resolves once it has been written to disk.
+ * Rejected if there is an error
+ */
+ save: Task.async(function* (content) {
+ // XXX: writeAtomic was losing permissions after saving on OSX
+ // return OS.File.writeAtomic(this.path, buffer, { tmpPath: this.path + ".tmp" });
+ let buffer = gEncoder.encode(content);
+ let path = this.path;
+ let file = yield OS.File.open(path, {truncate: true});
+ yield file.write(buffer);
+ yield file.close();
+ }),
+
+ /**
+ * Attempts to get the content type from the file.
+ */
+ get contentType() {
+ if (this._contentType) {
+ return this._contentType;
+ }
+ if (this.isDir) {
+ return "x-directory/normal";
+ }
+ try {
+ this._contentType = mimeService.getTypeFromFile(new FileUtils.File(this.path));
+ } catch (ex) {
+ if (ex.name !== "NS_ERROR_NOT_AVAILABLE" &&
+ ex.name !== "NS_ERROR_FAILURE") {
+ console.error(ex, this.path);
+ }
+ this._contentType = null;
+ }
+ return this._contentType;
+ },
+
+ /**
+ * A string used when determining the type of Editor to open for this.
+ * See editors.js -> EditorTypeForResource.
+ */
+ get contentCategory() {
+ const NetworkHelper = require("devtools/shared/webconsole/network-helper");
+ let category = NetworkHelper.mimeCategoryMap[this.contentType];
+ // Special treatment for manifest.webapp.
+ if (!category && this.basename === "manifest.webapp") {
+ return "json";
+ }
+ return category || "txt";
+ }
+});
+
+exports.FileResource = FileResource;
diff --git a/devtools/client/projecteditor/lib/tree.js b/devtools/client/projecteditor/lib/tree.js
new file mode 100644
index 000000000..50597804d
--- /dev/null
+++ b/devtools/client/projecteditor/lib/tree.js
@@ -0,0 +1,593 @@
+/* -*- 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/. */
+
+const { Cu } = require("chrome");
+const { Class } = require("sdk/core/heritage");
+const { emit } = require("sdk/event/core");
+const { EventTarget } = require("sdk/event/target");
+const { merge } = require("sdk/util/object");
+const promise = require("promise");
+const { InplaceEditor } = require("devtools/client/shared/inplace-editor");
+const { on, forget } = require("devtools/client/projecteditor/lib/helpers/event");
+const { OS } = Cu.import("resource://gre/modules/osfile.jsm", {});
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+
+/**
+ * ResourceContainer is used as the view of a single Resource in
+ * the tree. It is not exported.
+ */
+var ResourceContainer = Class({
+ /**
+ * @param ProjectTreeView tree
+ * @param Resource resource
+ */
+ initialize: function (tree, resource) {
+ this.tree = tree;
+ this.resource = resource;
+ this.elt = null;
+ this.expander = null;
+ this.children = null;
+
+ let doc = tree.doc;
+
+ this.elt = doc.createElementNS(HTML_NS, "li");
+ this.elt.classList.add("child");
+
+ this.line = doc.createElementNS(HTML_NS, "div");
+ this.line.classList.add("child");
+ this.line.classList.add("entry");
+ this.line.setAttribute("theme", "dark");
+ this.line.setAttribute("tabindex", "0");
+
+ this.elt.appendChild(this.line);
+
+ this.highlighter = doc.createElementNS(HTML_NS, "span");
+ this.highlighter.classList.add("highlighter");
+ this.line.appendChild(this.highlighter);
+
+ this.expander = doc.createElementNS(HTML_NS, "span");
+ this.expander.className = "arrow expander";
+ this.expander.setAttribute("open", "");
+ this.line.appendChild(this.expander);
+
+ this.label = doc.createElementNS(HTML_NS, "span");
+ this.label.className = "file-label";
+ this.line.appendChild(this.label);
+
+ this.line.addEventListener("contextmenu", (ev) => {
+ this.select();
+ this.openContextMenu(ev);
+ }, false);
+
+ this.children = doc.createElementNS(HTML_NS, "ul");
+ this.children.classList.add("children");
+
+ this.elt.appendChild(this.children);
+
+ this.line.addEventListener("click", (evt) => {
+ this.select();
+ this.toggleExpansion();
+ evt.stopPropagation();
+ }, false);
+ this.expander.addEventListener("click", (evt) => {
+ this.toggleExpansion();
+ this.select();
+ evt.stopPropagation();
+ }, true);
+
+ if (!this.resource.isRoot) {
+ this.expanded = false;
+ }
+ this.update();
+ },
+
+ toggleExpansion: function () {
+ if (!this.resource.isRoot) {
+ this.expanded = !this.expanded;
+ } else {
+ this.expanded = true;
+ }
+ },
+
+ destroy: function () {
+ this.elt.remove();
+ this.expander.remove();
+ this.highlighter.remove();
+ this.children.remove();
+ this.label.remove();
+ this.elt = this.expander = this.highlighter = this.children = this.label = null;
+ },
+
+ /**
+ * Open the context menu when right clicking on the view.
+ * XXX: We could pass this to plugins to allow themselves
+ * to be register/remove items from the context menu if needed.
+ *
+ * @param Event e
+ */
+ openContextMenu: function (ev) {
+ ev.preventDefault();
+ let popup = this.tree.options.contextMenuPopup;
+ popup.openPopupAtScreen(ev.screenX, ev.screenY, true);
+ },
+
+ /**
+ * Update the view based on the current state of the Resource.
+ */
+ update: function () {
+ let visible = this.tree.options.resourceVisible ?
+ this.tree.options.resourceVisible(this.resource) :
+ true;
+
+ this.elt.hidden = !visible;
+
+ this.tree.options.resourceFormatter(this.resource, this.label);
+
+ this.expander.style.visibility = this.resource.hasChildren ? "visible" : "hidden";
+
+ },
+
+ /**
+ * Select this view in the ProjectTreeView.
+ */
+ select: function () {
+ this.tree.selectContainer(this);
+ },
+
+ /**
+ * @returns Boolean
+ * Is this view currently selected
+ */
+ get selected() {
+ return this.line.classList.contains("selected");
+ },
+
+ /**
+ * Set the selected state in the UI.
+ */
+ set selected(v) {
+ if (v) {
+ this.line.classList.add("selected");
+ } else {
+ this.line.classList.remove("selected");
+ }
+ },
+
+ /**
+ * @returns Boolean
+ * Are any children visible.
+ */
+ get expanded() {
+ return !this.elt.classList.contains("tree-collapsed");
+ },
+
+ /**
+ * Set the visiblity state of children.
+ */
+ set expanded(v) {
+ if (v) {
+ this.elt.classList.remove("tree-collapsed");
+ this.expander.setAttribute("open", "");
+ } else {
+ this.expander.removeAttribute("open");
+ this.elt.classList.add("tree-collapsed");
+ }
+ }
+});
+
+/**
+ * TreeView is a view managing a list of children.
+ * It is not to be instantiated directly - only extended.
+ * Use ProjectTreeView instead.
+ */
+var TreeView = Class({
+ extends: EventTarget,
+
+ /**
+ * @param Document document
+ * @param Object options
+ * - contextMenuPopup: a <menupopup> element
+ * - resourceFormatter: a function(Resource, DOMNode)
+ * that renders the resource into the view
+ * - resourceVisible: a function(Resource) -> Boolean
+ * that determines if the resource should show up.
+ */
+ initialize: function (doc, options) {
+ this.doc = doc;
+ this.options = merge({
+ resourceFormatter: function (resource, elt) {
+ elt.textContent = resource.toString();
+ }
+ }, options);
+ this.models = new Set();
+ this.roots = new Set();
+ this._containers = new Map();
+ this.elt = this.doc.createElementNS(HTML_NS, "div");
+ this.elt.tree = this;
+ this.elt.className = "sources-tree";
+ this.elt.setAttribute("with-arrows", "true");
+ this.elt.setAttribute("theme", "dark");
+ this.elt.setAttribute("flex", "1");
+
+ this.children = this.doc.createElementNS(HTML_NS, "ul");
+ this.elt.appendChild(this.children);
+
+ this.resourceChildrenChanged = this.resourceChildrenChanged.bind(this);
+ this.removeResource = this.removeResource.bind(this);
+ this.updateResource = this.updateResource.bind(this);
+ },
+
+ destroy: function () {
+ this._destroyed = true;
+ this.elt.remove();
+ },
+
+ /**
+ * Helper function to create DOM elements for promptNew and promptEdit
+ */
+ createInputContainer: function () {
+ let inputholder = this.doc.createElementNS(HTML_NS, "div");
+ inputholder.className = "child entry";
+
+ let expander = this.doc.createElementNS(HTML_NS, "span");
+ expander.className = "arrow expander";
+ expander.setAttribute("invisible", "");
+ inputholder.appendChild(expander);
+
+ let placeholder = this.doc.createElementNS(HTML_NS, "div");
+ placeholder.className = "child";
+ inputholder.appendChild(placeholder);
+
+ return {inputholder, placeholder};
+ },
+
+ /**
+ * Prompt the user to create a new file in the tree.
+ *
+ * @param string initial
+ * The suggested starting file name
+ * @param Resource parent
+ * @param Resource sibling
+ * Which resource to put this next to. If not set,
+ * it will be put in front of all other children.
+ *
+ * @returns Promise
+ * Resolves once the prompt has been successful,
+ * Rejected if it is cancelled
+ */
+ promptNew: function (initial, parent, sibling = null) {
+ let deferred = promise.defer();
+
+ let parentContainer = this._containers.get(parent);
+ let item = this.doc.createElement("li");
+ item.className = "child";
+
+ let {inputholder, placeholder} = this.createInputContainer();
+ item.appendChild(inputholder);
+
+ let children = parentContainer.children;
+ sibling = sibling ? this._containers.get(sibling).elt : null;
+ parentContainer.children.insertBefore(item, sibling ? sibling.nextSibling : children.firstChild);
+
+ new InplaceEditor({
+ element: placeholder,
+ initial: initial,
+ preserveTextStyles: true,
+ start: editor => {
+ editor.input.select();
+ },
+ done: function (val, commit) {
+ if (commit) {
+ deferred.resolve(val);
+ } else {
+ deferred.reject(val);
+ }
+ parentContainer.line.focus();
+ },
+ destroy: () => {
+ item.parentNode.removeChild(item);
+ },
+ });
+
+ return deferred.promise;
+ },
+
+ /**
+ * Prompt the user to rename file in the tree.
+ *
+ * @param string initial
+ * The suggested starting file name
+ * @param resource
+ *
+ * @returns Promise
+ * Resolves once the prompt has been successful,
+ * Rejected if it is cancelled
+ */
+ promptEdit: function (initial, resource) {
+ let deferred = promise.defer();
+ let item = this._containers.get(resource).elt;
+ let originalText = item.childNodes[0];
+
+ let {inputholder, placeholder} = this.createInputContainer();
+ item.insertBefore(inputholder, originalText);
+
+ item.removeChild(originalText);
+
+ new InplaceEditor({
+ element: placeholder,
+ initial: initial,
+ preserveTextStyles: true,
+ start: editor => {
+ editor.input.select();
+ },
+ done: function (val, commit) {
+ if (val === initial) {
+ item.insertBefore(originalText, inputholder);
+ }
+
+ item.removeChild(inputholder);
+
+ if (commit) {
+ deferred.resolve(val);
+ } else {
+ deferred.reject(val);
+ }
+ },
+ });
+
+ return deferred.promise;
+ },
+
+ /**
+ * Add a new Store into the TreeView
+ *
+ * @param Store model
+ */
+ addModel: function (model) {
+ if (this.models.has(model)) {
+ // Requesting to add a model that already exists
+ return;
+ }
+ this.models.add(model);
+ let placeholder = this.doc.createElementNS(HTML_NS, "li");
+ placeholder.style.display = "none";
+ this.children.appendChild(placeholder);
+ this.roots.add(model.root);
+ model.root.refresh().then(root => {
+ if (this._destroyed || !this.models.has(model)) {
+ // model may have been removed during the initial refresh.
+ // In this case, do not import the resource or add to DOM, just leave it be.
+ return;
+ }
+ let container = this.importResource(root);
+ container.line.classList.add("entry-group-title");
+ container.line.setAttribute("theme", "dark");
+ this.selectContainer(container);
+
+ this.children.insertBefore(container.elt, placeholder);
+ this.children.removeChild(placeholder);
+ });
+ },
+
+ /**
+ * Remove a Store from the TreeView
+ *
+ * @param Store model
+ */
+ removeModel: function (model) {
+ this.models.delete(model);
+ this.removeResource(model.root);
+ },
+
+
+ /**
+ * Get the ResourceContainer. Used for testing the view.
+ *
+ * @param Resource resource
+ * @returns ResourceContainer
+ */
+ getViewContainer: function (resource) {
+ return this._containers.get(resource);
+ },
+
+ /**
+ * Select a ResourceContainer in the tree.
+ *
+ * @param ResourceContainer container
+ */
+ selectContainer: function (container) {
+ if (this.selectedContainer === container) {
+ return;
+ }
+ if (this.selectedContainer) {
+ this.selectedContainer.selected = false;
+ }
+ this.selectedContainer = container;
+ container.selected = true;
+ emit(this, "selection", container.resource);
+ },
+
+ /**
+ * Select a Resource in the tree.
+ *
+ * @param Resource resource
+ */
+ selectResource: function (resource) {
+ this.selectContainer(this._containers.get(resource));
+ },
+
+ /**
+ * Get the currently selected Resource
+ *
+ * @param Resource resource
+ */
+ getSelectedResource: function () {
+ return this.selectedContainer.resource;
+ },
+
+ /**
+ * Insert a Resource into the view.
+ * Makes a new ResourceContainer if needed
+ *
+ * @param Resource resource
+ */
+ importResource: function (resource) {
+ if (!resource) {
+ return null;
+ }
+
+ if (this._containers.has(resource)) {
+ return this._containers.get(resource);
+ }
+ var container = ResourceContainer(this, resource);
+ this._containers.set(resource, container);
+ this._updateChildren(container);
+
+ on(this, resource, "children-changed", this.resourceChildrenChanged);
+ on(this, resource, "label-change", this.updateResource);
+ on(this, resource, "deleted", this.removeResource);
+
+ return container;
+ },
+
+ /**
+ * Remove a Resource (including children) from the view.
+ *
+ * @param Resource resource
+ */
+ removeResource: function (resource) {
+ let toRemove = resource.allDescendants();
+ toRemove.add(resource);
+ for (let remove of toRemove) {
+ this._removeResource(remove);
+ }
+ },
+
+ /**
+ * Remove an individual Resource (but not children) from the view.
+ *
+ * @param Resource resource
+ */
+ _removeResource: function (resource) {
+ forget(this, resource);
+ if (this._containers.get(resource)) {
+ this._containers.get(resource).destroy();
+ this._containers.delete(resource);
+ }
+ emit(this, "resource-removed", resource);
+ },
+
+ /**
+ * Listener for when a resource has new children.
+ * This can happen as files are being loaded in from FileSystem, for example.
+ *
+ * @param Resource resource
+ */
+ resourceChildrenChanged: function (resource) {
+ this.updateResource(resource);
+ this._updateChildren(this._containers.get(resource));
+ },
+
+ /**
+ * Listener for when a label in the view has been updated.
+ * For example, the 'dirty' plugin marks changed files with an '*'
+ * next to the filename, and notifies with this event.
+ *
+ * @param Resource resource
+ */
+ updateResource: function (resource) {
+ let container = this._containers.get(resource);
+ container.update();
+ },
+
+ /**
+ * Build necessary ResourceContainers for a Resource and its
+ * children, then append them into the view.
+ *
+ * @param ResourceContainer container
+ */
+ _updateChildren: function (container) {
+ let resource = container.resource;
+ let fragment = this.doc.createDocumentFragment();
+ if (resource.children) {
+ for (let child of resource.childrenSorted) {
+ let childContainer = this.importResource(child);
+ fragment.appendChild(childContainer.elt);
+ }
+ }
+
+ while (container.children.firstChild) {
+ container.children.firstChild.remove();
+ }
+
+ container.children.appendChild(fragment);
+ },
+});
+
+/**
+ * ProjectTreeView is the implementation of TreeView
+ * that is exported. This is the class that is to be used
+ * directly.
+ */
+var ProjectTreeView = Class({
+ extends: TreeView,
+
+ /**
+ * See TreeView.initialize
+ *
+ * @param Document document
+ * @param Object options
+ */
+ initialize: function (document, options) {
+ TreeView.prototype.initialize.apply(this, arguments);
+ },
+
+ destroy: function () {
+ this.forgetProject();
+ TreeView.prototype.destroy.apply(this, arguments);
+ },
+
+ /**
+ * Remove current project and empty the tree
+ */
+ forgetProject: function () {
+ if (this.project) {
+ forget(this, this.project);
+ for (let store of this.project.allStores()) {
+ this.removeModel(store);
+ }
+ }
+ },
+
+ /**
+ * Show a project in the tree
+ *
+ * @param Project project
+ * The project to render into a tree
+ */
+ setProject: function (project) {
+ this.forgetProject();
+ this.project = project;
+ if (this.project) {
+ on(this, project, "store-added", this.addModel.bind(this));
+ on(this, project, "store-removed", this.removeModel.bind(this));
+ on(this, project, "project-saved", this.refresh.bind(this));
+ this.refresh();
+ }
+ },
+
+ /**
+ * Refresh the tree with all of the current project stores
+ */
+ refresh: function () {
+ for (let store of this.project.allStores()) {
+ this.addModel(store);
+ }
+ }
+});
+
+exports.ProjectTreeView = ProjectTreeView;
diff --git a/devtools/client/projecteditor/moz.build b/devtools/client/projecteditor/moz.build
new file mode 100644
index 000000000..049493833
--- /dev/null
+++ b/devtools/client/projecteditor/moz.build
@@ -0,0 +1,9 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DIRS += ['lib']
+
+BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
diff --git a/devtools/client/projecteditor/test/.eslintrc.js b/devtools/client/projecteditor/test/.eslintrc.js
new file mode 100644
index 000000000..8d15a76d9
--- /dev/null
+++ b/devtools/client/projecteditor/test/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../../.eslintrc.mochitests.js"
+};
diff --git a/devtools/client/projecteditor/test/browser.ini b/devtools/client/projecteditor/test/browser.ini
new file mode 100644
index 000000000..e7fdc7ae5
--- /dev/null
+++ b/devtools/client/projecteditor/test/browser.ini
@@ -0,0 +1,31 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ head.js
+ helper_homepage.html
+ helper_edits.js
+
+[browser_projecteditor_app_options.js]
+[browser_projecteditor_confirm_unsaved.js]
+[browser_projecteditor_contextmenu_01.js]
+skip-if = asan # Bug 1083140
+[browser_projecteditor_contextmenu_02.js]
+skip-if = true # Bug 1173950
+[browser_projecteditor_delete_file.js]
+skip-if = e10s # Frequent failures in e10s - Bug 1020027
+[browser_projecteditor_rename_file_01.js]
+[browser_projecteditor_rename_file_02.js]
+[browser_projecteditor_editing_01.js]
+[browser_projecteditor_editors_image.js]
+[browser_projecteditor_external_change.js]
+[browser_projecteditor_immediate_destroy.js]
+[browser_projecteditor_init.js]
+[browser_projecteditor_menubar_01.js]
+[browser_projecteditor_menubar_02.js]
+skip-if = true # Bug 1173950
+[browser_projecteditor_new_file.js]
+[browser_projecteditor_saveall.js]
+[browser_projecteditor_stores.js]
+[browser_projecteditor_tree_selection_01.js]
+[browser_projecteditor_tree_selection_02.js]
diff --git a/devtools/client/projecteditor/test/browser_projecteditor_app_options.js b/devtools/client/projecteditor/test/browser_projecteditor_app_options.js
new file mode 100644
index 000000000..aa608e205
--- /dev/null
+++ b/devtools/client/projecteditor/test/browser_projecteditor_app_options.js
@@ -0,0 +1,87 @@
+/* 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";
+
+// Test that options can be changed without resetting the whole
+// editor.
+add_task(function* () {
+
+ let TEMP_PATH = buildTempDirectoryStructure();
+ let projecteditor = yield addProjectEditorTab();
+
+ let resourceBeenAdded = promise.defer();
+ projecteditor.project.once("resource-added", () => {
+ info("A resource has been added");
+ resourceBeenAdded.resolve();
+ });
+
+ info("About to set project to: " + TEMP_PATH);
+ yield projecteditor.setProjectToAppPath(TEMP_PATH, {
+ name: "Test",
+ iconUrl: "chrome://devtools/skin/images/tool-options.svg",
+ projectOverviewURL: SAMPLE_WEBAPP_URL
+ });
+
+ info("Making sure a resource has been added before continuing");
+ yield resourceBeenAdded.promise;
+
+ info("From now on, if a resource is added it should fail");
+ projecteditor.project.on("resource-added", failIfResourceAdded);
+
+ info("Getting ahold and validating the project header DOM");
+ let header = projecteditor.document.querySelector(".entry-group-title");
+ let image = header.querySelector(".project-image");
+ let nameLabel = header.querySelector(".project-name-label");
+ let statusElement = header.querySelector(".project-status");
+ is(statusElement.getAttribute("status"), "unknown", "The status starts out as unknown.");
+ is(nameLabel.textContent, "Test", "The name label has been set correctly");
+ is(image.getAttribute("src"), "chrome://devtools/skin/images/tool-options.svg", "The icon has been set correctly");
+
+ info("About to set project with new options.");
+ yield projecteditor.setProjectToAppPath(TEMP_PATH, {
+ name: "Test2",
+ iconUrl: "chrome://devtools/skin/images/tool-inspector.svg",
+ projectOverviewURL: SAMPLE_WEBAPP_URL,
+ validationStatus: "error"
+ });
+
+ info("Getting ahold of and validating the project header DOM");
+ is(statusElement.getAttribute("status"), "error", "The status has been set correctly.");
+ is(nameLabel.textContent, "Test2", "The name label has been set correctly");
+ is(image.getAttribute("src"), "chrome://devtools/skin/images/tool-inspector.svg", "The icon has been set correctly");
+
+ info("About to set project with new options.");
+ yield projecteditor.setProjectToAppPath(TEMP_PATH, {
+ name: "Test3",
+ iconUrl: "chrome://devtools/skin/images/tool-webconsole.svg",
+ projectOverviewURL: SAMPLE_WEBAPP_URL,
+ validationStatus: "warning"
+ });
+
+ info("Getting ahold of and validating the project header DOM");
+ is(statusElement.getAttribute("status"), "warning", "The status has been set correctly.");
+ is(nameLabel.textContent, "Test3", "The name label has been set correctly");
+ is(image.getAttribute("src"), "chrome://devtools/skin/images/tool-webconsole.svg", "The icon has been set correctly");
+
+ info("About to set project with new options.");
+ yield projecteditor.setProjectToAppPath(TEMP_PATH, {
+ name: "Test4",
+ iconUrl: "chrome://devtools/skin/images/tool-debugger.svg",
+ projectOverviewURL: SAMPLE_WEBAPP_URL,
+ validationStatus: "valid"
+ });
+
+ info("Getting ahold of and validating the project header DOM");
+ is(statusElement.getAttribute("status"), "valid", "The status has been set correctly.");
+ is(nameLabel.textContent, "Test4", "The name label has been set correctly");
+ is(image.getAttribute("src"), "chrome://devtools/skin/images/tool-debugger.svg", "The icon has been set correctly");
+
+ info("Test finished, cleaning up");
+ projecteditor.project.off("resource-added", failIfResourceAdded);
+});
+
+function failIfResourceAdded() {
+ ok(false, "A resource has been added, but it shouldn't have been");
+}
diff --git a/devtools/client/projecteditor/test/browser_projecteditor_confirm_unsaved.js b/devtools/client/projecteditor/test/browser_projecteditor_confirm_unsaved.js
new file mode 100644
index 000000000..72640d243
--- /dev/null
+++ b/devtools/client/projecteditor/test/browser_projecteditor_confirm_unsaved.js
@@ -0,0 +1,60 @@
+/* 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";
+
+loadHelperScript("helper_edits.js");
+
+// Test that a prompt shows up when requested if a file is unsaved.
+add_task(function* () {
+ let projecteditor = yield addProjectEditorTabForTempDirectory();
+ ok(true, "ProjectEditor has loaded");
+
+ let resources = projecteditor.project.allResources();
+ yield selectFile(projecteditor, resources[2]);
+ let editor = projecteditor.currentEditor;
+ let originalText = editor.editor.getText();
+
+ ok(!projecteditor.hasUnsavedResources, "There are no unsaved resources");
+ ok(projecteditor.confirmUnsaved(), "When there are no unsaved changes, confirmUnsaved() is true");
+ editor.editor.setText("bar");
+ editor.editor.setText(originalText);
+ ok(!projecteditor.hasUnsavedResources, "There are no unsaved resources");
+ ok(projecteditor.confirmUnsaved(), "When an editor has changed but is still the original text, confirmUnsaved() is true");
+
+ editor.editor.setText("bar");
+
+ checkConfirmYes(projecteditor);
+ checkConfirmNo(projecteditor);
+});
+
+function checkConfirmYes(projecteditor, container) {
+ function confirmYes(aSubject) {
+ info("confirm dialog observed as expected, going to click OK");
+ Services.obs.removeObserver(confirmYes, "common-dialog-loaded");
+ Services.obs.removeObserver(confirmYes, "tabmodal-dialog-loaded");
+ aSubject.Dialog.ui.button0.click();
+ }
+
+ Services.obs.addObserver(confirmYes, "common-dialog-loaded", false);
+ Services.obs.addObserver(confirmYes, "tabmodal-dialog-loaded", false);
+
+ ok(projecteditor.hasUnsavedResources, "There are unsaved resources");
+ ok(projecteditor.confirmUnsaved(), "When there are unsaved changes, clicking OK makes confirmUnsaved() true");
+}
+
+function checkConfirmNo(projecteditor, container) {
+ function confirmNo(aSubject) {
+ info("confirm dialog observed as expected, going to click cancel");
+ Services.obs.removeObserver(confirmNo, "common-dialog-loaded");
+ Services.obs.removeObserver(confirmNo, "tabmodal-dialog-loaded");
+ aSubject.Dialog.ui.button1.click();
+ }
+
+ Services.obs.addObserver(confirmNo, "common-dialog-loaded", false);
+ Services.obs.addObserver(confirmNo, "tabmodal-dialog-loaded", false);
+
+ ok(projecteditor.hasUnsavedResources, "There are unsaved resources");
+ ok(!projecteditor.confirmUnsaved(), "When there are unsaved changes, clicking cancel makes confirmUnsaved() false");
+}
diff --git a/devtools/client/projecteditor/test/browser_projecteditor_contextmenu_01.js b/devtools/client/projecteditor/test/browser_projecteditor_contextmenu_01.js
new file mode 100644
index 000000000..44ffe1722
--- /dev/null
+++ b/devtools/client/projecteditor/test/browser_projecteditor_contextmenu_01.js
@@ -0,0 +1,27 @@
+/* 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";
+
+// Test that context menus append to the correct document.
+
+add_task(function* () {
+ let projecteditor = yield addProjectEditorTabForTempDirectory({
+ menubar: false
+ });
+ ok(projecteditor, "ProjectEditor has loaded");
+
+ let contextMenuPopup = projecteditor.document.querySelector("#context-menu-popup");
+ let textEditorContextMenuPopup = projecteditor.document.querySelector("#texteditor-context-popup");
+ ok(contextMenuPopup, "The menu has loaded in the projecteditor document");
+ ok(textEditorContextMenuPopup, "The menu has loaded in the projecteditor document");
+
+ let projecteditor2 = yield addProjectEditorTabForTempDirectory();
+ contextMenuPopup = projecteditor2.document.getElementById("context-menu-popup");
+ textEditorContextMenuPopup = projecteditor2.document.getElementById("texteditor-context-popup");
+ ok(!contextMenuPopup, "The menu has NOT loaded in the projecteditor document");
+ ok(!textEditorContextMenuPopup, "The menu has NOT loaded in the projecteditor document");
+ ok(content.document.querySelector("#context-menu-popup"), "The menu has loaded in the specified element");
+ ok(content.document.querySelector("#texteditor-context-popup"), "The menu has loaded in the specified element");
+});
diff --git a/devtools/client/projecteditor/test/browser_projecteditor_contextmenu_02.js b/devtools/client/projecteditor/test/browser_projecteditor_contextmenu_02.js
new file mode 100644
index 000000000..cf43b3e21
--- /dev/null
+++ b/devtools/client/projecteditor/test/browser_projecteditor_contextmenu_02.js
@@ -0,0 +1,66 @@
+/* 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";
+
+loadHelperScript("helper_edits.js");
+
+// Test context menu enabled / disabled state in editor
+
+add_task(function* () {
+ let projecteditor = yield addProjectEditorTabForTempDirectory();
+ ok(projecteditor, "ProjectEditor has loaded");
+
+ let {textEditorContextMenuPopup} = projecteditor;
+
+ // Update menu items for a clean slate, so previous tests cannot
+ // affect paste, and possibly other side effects
+ projecteditor._updateMenuItems();
+
+ let cmdDelete = textEditorContextMenuPopup.querySelector("[command=cmd_delete]");
+ let cmdSelectAll = textEditorContextMenuPopup.querySelector("[command=cmd_selectAll]");
+ let cmdCut = textEditorContextMenuPopup.querySelector("[command=cmd_cut]");
+ let cmdCopy = textEditorContextMenuPopup.querySelector("[command=cmd_copy]");
+ let cmdPaste = textEditorContextMenuPopup.querySelector("[command=cmd_paste]");
+
+ info("Opening resource");
+ let resource = projecteditor.project.allResources()[2];
+ yield selectFile(projecteditor, resource);
+ let editor = projecteditor.currentEditor;
+ editor.editor.focus();
+
+ info("Opening context menu on resource");
+ yield openContextMenuForEditor(editor, textEditorContextMenuPopup);
+
+ is(cmdDelete.getAttribute("disabled"), "true", "cmdDelete is disabled");
+ is(cmdSelectAll.getAttribute("disabled"), "", "cmdSelectAll is enabled");
+ is(cmdCut.getAttribute("disabled"), "true", "cmdCut is disabled");
+ is(cmdCopy.getAttribute("disabled"), "true", "cmdCopy is disabled");
+ is(cmdPaste.getAttribute("disabled"), "", "cmdPaste is enabled");
+
+ info("Setting a selection and repening context menu on resource");
+ yield closeContextMenuForEditor(editor, textEditorContextMenuPopup);
+ editor.editor.setSelection({line: 0, ch: 0}, {line: 0, ch: 2});
+ yield openContextMenuForEditor(editor, textEditorContextMenuPopup);
+
+ is(cmdDelete.getAttribute("disabled"), "", "cmdDelete is enabled");
+ is(cmdSelectAll.getAttribute("disabled"), "", "cmdSelectAll is enabled");
+ is(cmdCut.getAttribute("disabled"), "", "cmdCut is enabled");
+ is(cmdCopy.getAttribute("disabled"), "", "cmdCopy is enabled");
+ is(cmdPaste.getAttribute("disabled"), "", "cmdPaste is enabled");
+});
+
+function* openContextMenuForEditor(editor, contextMenu) {
+ let editorDoc = editor.editor.container.contentDocument;
+ let shown = onPopupShow(contextMenu);
+ EventUtils.synthesizeMouse(editorDoc.body, 2, 2,
+ {type: "contextmenu", button: 2}, editorDoc.defaultView);
+ yield shown;
+}
+function* closeContextMenuForEditor(editor, contextMenu) {
+ let editorDoc = editor.editor.container.contentDocument;
+ let hidden = onPopupHidden(contextMenu);
+ contextMenu.hidePopup();
+ yield hidden;
+}
diff --git a/devtools/client/projecteditor/test/browser_projecteditor_delete_file.js b/devtools/client/projecteditor/test/browser_projecteditor_delete_file.js
new file mode 100644
index 000000000..446c1dbcb
--- /dev/null
+++ b/devtools/client/projecteditor/test/browser_projecteditor_delete_file.js
@@ -0,0 +1,85 @@
+/* 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";
+
+// Test tree selection functionality
+
+add_task(function* () {
+ let projecteditor = yield addProjectEditorTabForTempDirectory();
+ ok(true, "ProjectEditor has loaded");
+
+ let root = [...projecteditor.project.allStores()][0].root;
+ is(root.path, TEMP_PATH, "The root store is set to the correct temp path.");
+ for (let child of root.children) {
+ yield deleteWithContextMenu(projecteditor, projecteditor.projectTree.getViewContainer(child));
+ }
+
+ yield testDeleteOnRoot(projecteditor, projecteditor.projectTree.getViewContainer(root));
+});
+
+
+function openContextMenuOn(node) {
+ EventUtils.synthesizeMouseAtCenter(
+ node,
+ {button: 2, type: "contextmenu"},
+ node.ownerDocument.defaultView
+ );
+}
+
+function* testDeleteOnRoot(projecteditor, container) {
+ let popup = projecteditor.contextMenuPopup;
+ let oncePopupShown = onPopupShow(popup);
+ openContextMenuOn(container.label);
+ yield oncePopupShown;
+
+ let deleteCommand = popup.querySelector("[command=cmd-delete]");
+ ok(deleteCommand, "Delete command exists in popup");
+ is(deleteCommand.getAttribute("hidden"), "true", "Delete command is hidden");
+}
+
+function deleteWithContextMenu(projecteditor, container) {
+ let defer = promise.defer();
+
+ let popup = projecteditor.contextMenuPopup;
+ let resource = container.resource;
+ info("Going to attempt deletion for: " + resource.path);
+
+ onPopupShow(popup).then(function () {
+ let deleteCommand = popup.querySelector("[command=cmd-delete]");
+ ok(deleteCommand, "Delete command exists in popup");
+ is(deleteCommand.getAttribute("hidden"), "", "Delete command is visible");
+ is(deleteCommand.getAttribute("disabled"), "", "Delete command is enabled");
+
+ function onConfirmShown(aSubject) {
+ info("confirm dialog observed as expected");
+ Services.obs.removeObserver(onConfirmShown, "common-dialog-loaded");
+ Services.obs.removeObserver(onConfirmShown, "tabmodal-dialog-loaded");
+
+ projecteditor.project.on("refresh-complete", function refreshComplete() {
+ projecteditor.project.off("refresh-complete", refreshComplete);
+ OS.File.stat(resource.path).then(() => {
+ ok(false, "The file was not deleted");
+ defer.resolve();
+ }, (ex) => {
+ ok(ex instanceof OS.File.Error && ex.becauseNoSuchFile, "OS.File.stat promise was rejected because the file is gone");
+ defer.resolve();
+ });
+ });
+
+ // Click the 'OK' button
+ aSubject.Dialog.ui.button0.click();
+ }
+
+ Services.obs.addObserver(onConfirmShown, "common-dialog-loaded", false);
+ Services.obs.addObserver(onConfirmShown, "tabmodal-dialog-loaded", false);
+
+ deleteCommand.click();
+ popup.hidePopup();
+ });
+
+ openContextMenuOn(container.label);
+
+ return defer.promise;
+}
diff --git a/devtools/client/projecteditor/test/browser_projecteditor_editing_01.js b/devtools/client/projecteditor/test/browser_projecteditor_editing_01.js
new file mode 100644
index 000000000..c7ff1c0be
--- /dev/null
+++ b/devtools/client/projecteditor/test/browser_projecteditor_editing_01.js
@@ -0,0 +1,70 @@
+/* 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";
+
+//
+// Whitelisting this test.
+// As part of bug 1077403, the leaking uncaught rejection should be fixed.
+//
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed("destroy");
+
+loadHelperScript("helper_edits.js");
+
+// Test ProjectEditor basic functionality
+add_task(function* () {
+ let projecteditor = yield addProjectEditorTabForTempDirectory();
+ let TEMP_PATH = projecteditor.project.allPaths()[0];
+
+ is(getTempFile("").path, TEMP_PATH, "Temp path is set correctly.");
+
+ ok(projecteditor.currentEditor, "There is an editor for projecteditor");
+ let resources = projecteditor.project.allResources();
+
+ for (let data of helperEditData) {
+ info("Processing " + data.path);
+ let resource = resources.filter(r=>r.basename === data.basename)[0];
+ yield selectFile(projecteditor, resource);
+ yield testEditFile(projecteditor, getTempFile(data.path).path, data.newContent);
+ }
+});
+
+function* testEditFile(projecteditor, filePath, newData) {
+ info("Testing file editing for: " + filePath);
+
+ let initialData = yield getFileData(filePath);
+ let editor = projecteditor.currentEditor;
+ let resource = projecteditor.resourceFor(editor);
+ let viewContainer = projecteditor.projectTree.getViewContainer(resource);
+ let originalTreeLabel = viewContainer.label.textContent;
+
+ is(resource.path, filePath, "Resource path is set correctly");
+ is(editor.editor.getText(), initialData, "Editor is loaded with correct file contents");
+
+ info("Setting text in the editor and doing checks before saving");
+
+ editor.editor.undo();
+ editor.editor.undo();
+ is(editor.editor.getText(), initialData, "Editor is still loaded with correct contents after undo");
+
+ editor.editor.setText(newData);
+ is(editor.editor.getText(), newData, "Editor has been filled with new data");
+ is(viewContainer.label.textContent, "*" + originalTreeLabel, "Label is marked as changed");
+
+ info("Saving the editor and checking to make sure the file gets saved on disk");
+
+ editor.save(resource);
+
+ let savedResource = yield onceEditorSave(projecteditor);
+
+ is(viewContainer.label.textContent, originalTreeLabel, "Label is unmarked as changed");
+ is(savedResource.path, filePath, "The saved resouce path matches the original file path");
+ is(savedResource, resource, "The saved resource is the same as the original resource");
+
+ let savedData = yield getFileData(filePath);
+ is(savedData, newData, "Data has been correctly saved to disk");
+
+ info("Finished checking saving for " + filePath);
+
+}
diff --git a/devtools/client/projecteditor/test/browser_projecteditor_editors_image.js b/devtools/client/projecteditor/test/browser_projecteditor_editors_image.js
new file mode 100644
index 000000000..0b19cb5d1
--- /dev/null
+++ b/devtools/client/projecteditor/test/browser_projecteditor_editors_image.js
@@ -0,0 +1,74 @@
+/* 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";
+
+//
+// Whitelisting this test.
+// As part of bug 1077403, the leaking uncaught rejection should be fixed.
+//
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed("destroy");
+
+loadHelperScript("helper_edits.js");
+
+// Test ProjectEditor image editor functionality
+add_task(function* () {
+ let projecteditor = yield addProjectEditorTabForTempDirectory();
+ let TEMP_PATH = projecteditor.project.allPaths()[0];
+
+ is(getTempFile("").path, TEMP_PATH, "Temp path is set correctly.");
+
+ ok(projecteditor.currentEditor, "There is an editor for projecteditor");
+ let resources = projecteditor.project.allResources();
+
+ let helperImageData = [
+ {
+ basename: "16x16.png",
+ path: "img/icons/16x16.png"
+ },
+ {
+ basename: "32x32.png",
+ path: "img/icons/32x32.png"
+ },
+ {
+ basename: "128x128.png",
+ path: "img/icons/128x128.png"
+ },
+ ];
+
+ for (let data of helperImageData) {
+ info("Processing " + data.path);
+ let resource = resources.filter(r=>r.basename === data.basename)[0];
+ yield selectFile(projecteditor, resource);
+ yield testEditor(projecteditor, getTempFile(data.path).path);
+ }
+});
+
+function* testEditor(projecteditor, filePath) {
+ info("Testing file editing for: " + filePath);
+
+ let editor = projecteditor.currentEditor;
+ let resource = projecteditor.resourceFor(editor);
+
+ is(resource.path, filePath, "Resource path is set correctly");
+
+ let images = editor.elt.querySelectorAll("image");
+ is(images.length, 1, "There is one image inside the editor");
+ is(images[0], editor.image, "The image property is set correctly with the DOM");
+ is(editor.image.getAttribute("src"), resource.uri, "The image has the resource URL");
+
+ info("Selecting another resource, then reselecting this one");
+ projecteditor.projectTree.selectResource(resource.store.root);
+ yield onceEditorActivated(projecteditor);
+ projecteditor.projectTree.selectResource(resource);
+ yield onceEditorActivated(projecteditor);
+
+ editor = projecteditor.currentEditor;
+ images = editor.elt.querySelectorAll("image");
+ ok(images.length, 1, "There is one image inside the editor");
+ is(images[0], editor.image, "The image property is set correctly with the DOM");
+ is(editor.image.getAttribute("src"), resource.uri, "The image has the resource URL");
+
+ info("Finished checking saving for " + filePath);
+}
diff --git a/devtools/client/projecteditor/test/browser_projecteditor_external_change.js b/devtools/client/projecteditor/test/browser_projecteditor_external_change.js
new file mode 100644
index 000000000..12d90a869
--- /dev/null
+++ b/devtools/client/projecteditor/test/browser_projecteditor_external_change.js
@@ -0,0 +1,84 @@
+/* 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";
+
+loadHelperScript("helper_edits.js");
+
+// Test ProjectEditor reaction to external changes (made outside of the)
+// editor.
+add_task(function* () {
+ let projecteditor = yield addProjectEditorTabForTempDirectory();
+ let TEMP_PATH = projecteditor.project.allPaths()[0];
+
+ is(getTempFile("").path, TEMP_PATH, "Temp path is set correctly.");
+
+ ok(projecteditor.currentEditor, "There is an editor for projecteditor");
+ let resources = projecteditor.project.allResources();
+
+ for (let data of helperEditData) {
+ info("Processing " + data.path);
+ let resource = resources.filter(r=>r.basename === data.basename)[0];
+ yield selectFile(projecteditor, resource);
+ yield testChangeFileExternally(projecteditor, getTempFile(data.path).path, data.newContent);
+ yield testChangeUnsavedFileExternally(projecteditor, getTempFile(data.path).path, data.newContent + "[changed]");
+ }
+});
+
+function* testChangeUnsavedFileExternally(projecteditor, filePath, newData) {
+ info("Testing file external changes for: " + filePath);
+
+ let editor = projecteditor.currentEditor;
+ let resource = projecteditor.resourceFor(editor);
+ let initialData = yield getFileData(filePath);
+
+ is(resource.path, filePath, "Resource path is set correctly");
+ is(editor.editor.getText(), initialData, "Editor is loaded with correct file contents");
+
+ info("Editing but not saving file in project editor");
+ ok(editor.isClean(), "Editor is clean");
+ editor.editor.setText("foobar");
+ ok(!editor.isClean(), "Editor is dirty");
+
+ info("Editor has been selected, writing to file externally");
+ yield writeToFile(resource.path, newData);
+
+ info("Selecting another resource, then reselecting this one");
+ projecteditor.projectTree.selectResource(resource.store.root);
+ yield onceEditorActivated(projecteditor);
+ projecteditor.projectTree.selectResource(resource);
+ yield onceEditorActivated(projecteditor);
+
+ editor = projecteditor.currentEditor;
+ info("Checking to make sure the editor is now populated correctly");
+ is(editor.editor.getText(), "foobar", "Editor has not been updated with new file contents");
+
+ info("Finished checking saving for " + filePath);
+}
+
+function* testChangeFileExternally(projecteditor, filePath, newData) {
+ info("Testing file external changes for: " + filePath);
+
+ let editor = projecteditor.currentEditor;
+ let resource = projecteditor.resourceFor(editor);
+ let initialData = yield getFileData(filePath);
+
+ is(resource.path, filePath, "Resource path is set correctly");
+ is(editor.editor.getText(), initialData, "Editor is loaded with correct file contents");
+
+ info("Editor has been selected, writing to file externally");
+ yield writeToFile(resource.path, newData);
+
+ info("Selecting another resource, then reselecting this one");
+ projecteditor.projectTree.selectResource(resource.store.root);
+ yield onceEditorActivated(projecteditor);
+ projecteditor.projectTree.selectResource(resource);
+ yield onceEditorActivated(projecteditor);
+
+ editor = projecteditor.currentEditor;
+ info("Checking to make sure the editor is now populated correctly");
+ is(editor.editor.getText(), newData, "Editor has been updated with correct file contents");
+
+ info("Finished checking saving for " + filePath);
+}
diff --git a/devtools/client/projecteditor/test/browser_projecteditor_immediate_destroy.js b/devtools/client/projecteditor/test/browser_projecteditor_immediate_destroy.js
new file mode 100644
index 000000000..0773be55c
--- /dev/null
+++ b/devtools/client/projecteditor/test/browser_projecteditor_immediate_destroy.js
@@ -0,0 +1,93 @@
+/* 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";
+
+//
+// Whitelisting this test.
+// As part of bug 1077403, the leaking uncaught rejection should be fixed.
+//
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed("destroy");
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed("TypeError: this.window is null");
+
+// Test that projecteditor can be destroyed in various states of loading
+// without causing any leaks or exceptions.
+
+add_task(function* () {
+
+ info("Testing tab closure when projecteditor is in various states");
+ let loaderUrl = "chrome://devtools/content/projecteditor/chrome/content/projecteditor-test.xul";
+
+ yield addTab(loaderUrl).then(() => {
+ let iframe = content.document.getElementById("projecteditor-iframe");
+ ok(iframe, "Tab has placeholder iframe for projecteditor");
+
+ info("Closing the tab without doing anything");
+ gBrowser.removeCurrentTab();
+ });
+
+ yield addTab(loaderUrl).then(() => {
+ let iframe = content.document.getElementById("projecteditor-iframe");
+ ok(iframe, "Tab has placeholder iframe for projecteditor");
+
+ let projecteditor = ProjectEditor.ProjectEditor();
+ ok(projecteditor, "ProjectEditor has been initialized");
+
+ info("Closing the tab before attempting to load");
+ gBrowser.removeCurrentTab();
+ });
+
+ yield addTab(loaderUrl).then(() => {
+ let iframe = content.document.getElementById("projecteditor-iframe");
+ ok(iframe, "Tab has placeholder iframe for projecteditor");
+
+ let projecteditor = ProjectEditor.ProjectEditor();
+ ok(projecteditor, "ProjectEditor has been initialized");
+
+ projecteditor.load(iframe);
+
+ info("Closing the tab after a load is requested, but before load is finished");
+ gBrowser.removeCurrentTab();
+ });
+
+ yield addTab(loaderUrl).then(() => {
+ let iframe = content.document.getElementById("projecteditor-iframe");
+ ok(iframe, "Tab has placeholder iframe for projecteditor");
+
+ let projecteditor = ProjectEditor.ProjectEditor();
+ ok(projecteditor, "ProjectEditor has been initialized");
+
+ return projecteditor.load(iframe).then(() => {
+ info("Closing the tab after a load has been requested and finished");
+ gBrowser.removeCurrentTab();
+ });
+ });
+
+ yield addTab(loaderUrl).then(() => {
+ let iframe = content.document.getElementById("projecteditor-iframe");
+ ok(iframe, "Tab has placeholder iframe for projecteditor");
+
+ let projecteditor = ProjectEditor.ProjectEditor(iframe);
+ ok(projecteditor, "ProjectEditor has been initialized");
+
+ let loadedDone = promise.defer();
+ projecteditor.loaded.then(() => {
+ ok(false, "Loaded has finished after destroy() has been called");
+ loadedDone.resolve();
+ }, () => {
+ ok(true, "Loaded has been rejected after destroy() has been called");
+ loadedDone.resolve();
+ });
+
+ projecteditor.destroy();
+
+ return loadedDone.promise.then(() => {
+ gBrowser.removeCurrentTab();
+ });
+ });
+
+ finish();
+});
+
+
diff --git a/devtools/client/projecteditor/test/browser_projecteditor_init.js b/devtools/client/projecteditor/test/browser_projecteditor_init.js
new file mode 100644
index 000000000..3ee947e0d
--- /dev/null
+++ b/devtools/client/projecteditor/test/browser_projecteditor_init.js
@@ -0,0 +1,18 @@
+/* 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";
+
+// Test that projecteditor can be initialized.
+
+function test() {
+ info("Initializing projecteditor");
+ addProjectEditorTab().then((projecteditor) => {
+ ok(projecteditor, "Load callback has been called");
+ ok(projecteditor.shells, "ProjectEditor has shells");
+ ok(projecteditor.project, "ProjectEditor has a project");
+ finish();
+ });
+}
+
diff --git a/devtools/client/projecteditor/test/browser_projecteditor_menubar_01.js b/devtools/client/projecteditor/test/browser_projecteditor_menubar_01.js
new file mode 100644
index 000000000..1641169e7
--- /dev/null
+++ b/devtools/client/projecteditor/test/browser_projecteditor_menubar_01.js
@@ -0,0 +1,28 @@
+/* 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";
+
+// Test that menu bar appends to the correct document.
+
+add_task(function* () {
+ let projecteditor = yield addProjectEditorTabForTempDirectory({
+ menubar: false
+ });
+ ok(projecteditor, "ProjectEditor has loaded");
+
+ let fileMenu = projecteditor.document.getElementById("file-menu");
+ let editMenu = projecteditor.document.getElementById("edit-menu");
+ ok(fileMenu, "The menu has loaded in the projecteditor document");
+ ok(editMenu, "The menu has loaded in the projecteditor document");
+
+ let projecteditor2 = yield addProjectEditorTabForTempDirectory();
+ let menubar = projecteditor2.menubar;
+ fileMenu = projecteditor2.document.getElementById("file-menu");
+ editMenu = projecteditor2.document.getElementById("edit-menu");
+ ok(!fileMenu, "The menu has NOT loaded in the projecteditor document");
+ ok(!editMenu, "The menu has NOT loaded in the projecteditor document");
+ ok(content.document.querySelector("#file-menu"), "The menu has loaded in the specified element");
+ ok(content.document.querySelector("#edit-menu"), "The menu has loaded in the specified element");
+});
diff --git a/devtools/client/projecteditor/test/browser_projecteditor_menubar_02.js b/devtools/client/projecteditor/test/browser_projecteditor_menubar_02.js
new file mode 100644
index 000000000..d0d41f743
--- /dev/null
+++ b/devtools/client/projecteditor/test/browser_projecteditor_menubar_02.js
@@ -0,0 +1,123 @@
+/* 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";
+
+loadHelperScript("helper_edits.js");
+
+// Test menu bar enabled / disabled state.
+
+add_task(function* () {
+ let projecteditor = yield addProjectEditorTabForTempDirectory();
+ let menubar = projecteditor.menubar;
+
+ // Update menu items for a clean slate, so previous tests cannot
+ // affect paste, and possibly other side effects
+ projecteditor._updateMenuItems();
+
+ // let projecteditor = yield addProjectEditorTabForTempDirectory();
+ ok(projecteditor, "ProjectEditor has loaded");
+
+ let fileMenu = menubar.querySelector("#file-menu");
+ let editMenu = menubar.querySelector("#edit-menu");
+ ok(fileMenu, "The menu has loaded in the projecteditor document");
+ ok(editMenu, "The menu has loaded in the projecteditor document");
+
+ let cmdNew = fileMenu.querySelector("[command=cmd-new]");
+ let cmdSave = fileMenu.querySelector("[command=cmd-save]");
+ let cmdSaveas = fileMenu.querySelector("[command=cmd-saveas]");
+
+ let cmdUndo = editMenu.querySelector("[command=cmd_undo]");
+ let cmdRedo = editMenu.querySelector("[command=cmd_redo]");
+ let cmdCut = editMenu.querySelector("[command=cmd_cut]");
+ let cmdCopy = editMenu.querySelector("[command=cmd_copy]");
+ let cmdPaste = editMenu.querySelector("[command=cmd_paste]");
+
+ info("Checking initial state of menus");
+ yield openAndCloseMenu(fileMenu);
+ yield openAndCloseMenu(editMenu);
+
+ is(cmdNew.getAttribute("disabled"), "", "File menu item is enabled");
+ is(cmdSave.getAttribute("disabled"), "true", "File menu item is disabled");
+ is(cmdSaveas.getAttribute("disabled"), "true", "File menu item is disabled");
+
+ is(cmdUndo.getAttribute("disabled"), "true", "Edit menu item is disabled");
+ is(cmdRedo.getAttribute("disabled"), "true", "Edit menu item is disabled");
+ is(cmdCut.getAttribute("disabled"), "true", "Edit menu item is disabled");
+ is(cmdCopy.getAttribute("disabled"), "true", "Edit menu item is disabled");
+ is(cmdPaste.getAttribute("disabled"), "true", "Edit menu item is disabled");
+
+ projecteditor.menuEnabled = false;
+
+ info("Checking with menuEnabled = false");
+ yield openAndCloseMenu(fileMenu);
+ yield openAndCloseMenu(editMenu);
+
+ is(cmdNew.getAttribute("disabled"), "true", "File menu item is disabled");
+ is(cmdSave.getAttribute("disabled"), "true", "File menu item is disabled");
+ is(cmdSaveas.getAttribute("disabled"), "true", "File menu item is disabled");
+
+ is(cmdUndo.getAttribute("disabled"), "true", "Edit menu item is disabled");
+ is(cmdRedo.getAttribute("disabled"), "true", "Edit menu item is disabled");
+ is(cmdCut.getAttribute("disabled"), "true", "Edit menu item is disabled");
+ is(cmdCopy.getAttribute("disabled"), "true", "Edit menu item is disabled");
+ is(cmdPaste.getAttribute("disabled"), "true", "Edit menu item is disabled");
+
+ info("Checking with menuEnabled=true");
+ projecteditor.menuEnabled = true;
+
+ yield openAndCloseMenu(fileMenu);
+ yield openAndCloseMenu(editMenu);
+
+ is(cmdNew.getAttribute("disabled"), "", "File menu item is enabled");
+ is(cmdSave.getAttribute("disabled"), "true", "File menu item is disabled");
+ is(cmdSaveas.getAttribute("disabled"), "true", "File menu item is disabled");
+
+ is(cmdUndo.getAttribute("disabled"), "true", "Edit menu item is disabled");
+ is(cmdRedo.getAttribute("disabled"), "true", "Edit menu item is disabled");
+ is(cmdCut.getAttribute("disabled"), "true", "Edit menu item is disabled");
+ is(cmdCopy.getAttribute("disabled"), "true", "Edit menu item is disabled");
+ is(cmdPaste.getAttribute("disabled"), "true", "Edit menu item is disabled");
+
+ info("Checking with resource selected");
+ let resource = projecteditor.project.allResources()[2];
+ yield selectFile(projecteditor, resource);
+ let editor = projecteditor.currentEditor;
+
+ let onChange = promise.defer();
+
+ projecteditor.on("onEditorChange", () => {
+ info("onEditorChange has been detected");
+ onChange.resolve();
+ });
+ editor.editor.focus();
+ EventUtils.synthesizeKey("f", { }, projecteditor.window);
+
+ yield onChange;
+ yield openAndCloseMenu(fileMenu);
+ yield openAndCloseMenu(editMenu);
+
+ is(cmdNew.getAttribute("disabled"), "", "File menu item is enabled");
+ is(cmdSave.getAttribute("disabled"), "", "File menu item is enabled");
+ is(cmdSaveas.getAttribute("disabled"), "", "File menu item is enabled");
+
+ // Use editor.canUndo() to see if this is failing - the menu disabled property
+ // should be in sync with this because of isCommandEnabled in editor.js.
+ info('cmdUndo.getAttribute("disabled") is: "' + cmdUndo.getAttribute("disabled") + '"');
+ ok(editor.editor.canUndo(), "Edit menu item is enabled");
+
+ is(cmdRedo.getAttribute("disabled"), "true", "Edit menu item is disabled");
+ is(cmdCut.getAttribute("disabled"), "true", "Edit menu item is disabled");
+ is(cmdCopy.getAttribute("disabled"), "true", "Edit menu item is disabled");
+ is(cmdPaste.getAttribute("disabled"), "", "Edit menu item is enabled");
+});
+
+function* openAndCloseMenu(menu) {
+ let shown = onPopupShow(menu);
+ EventUtils.synthesizeMouseAtCenter(menu, {}, menu.ownerDocument.defaultView);
+ yield shown;
+ let hidden = onPopupHidden(menu);
+ EventUtils.synthesizeMouseAtCenter(menu, {}, menu.ownerDocument.defaultView);
+ yield hidden;
+}
diff --git a/devtools/client/projecteditor/test/browser_projecteditor_new_file.js b/devtools/client/projecteditor/test/browser_projecteditor_new_file.js
new file mode 100644
index 000000000..aaaee0369
--- /dev/null
+++ b/devtools/client/projecteditor/test/browser_projecteditor_new_file.js
@@ -0,0 +1,13 @@
+/* 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";
+
+// Test tree selection functionality
+
+add_task(function* () {
+ let projecteditor = yield addProjectEditorTabForTempDirectory();
+ ok(projecteditor, "ProjectEditor has loaded");
+
+});
diff --git a/devtools/client/projecteditor/test/browser_projecteditor_rename_file_01.js b/devtools/client/projecteditor/test/browser_projecteditor_rename_file_01.js
new file mode 100644
index 000000000..914fa73cc
--- /dev/null
+++ b/devtools/client/projecteditor/test/browser_projecteditor_rename_file_01.js
@@ -0,0 +1,19 @@
+/* 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";
+
+// Test file rename functionality
+
+add_task(function* () {
+ let projecteditor = yield addProjectEditorTabForTempDirectory();
+ ok(true, "ProjectEditor has loaded");
+
+ let root = [...projecteditor.project.allStores()][0].root;
+ is(root.path, TEMP_PATH, "The root store is set to the correct temp path.");
+ for (let child of root.children) {
+ yield renameWithContextMenu(projecteditor,
+ projecteditor.projectTree.getViewContainer(child), ".renamed");
+ }
+});
diff --git a/devtools/client/projecteditor/test/browser_projecteditor_rename_file_02.js b/devtools/client/projecteditor/test/browser_projecteditor_rename_file_02.js
new file mode 100644
index 000000000..a2964da2a
--- /dev/null
+++ b/devtools/client/projecteditor/test/browser_projecteditor_rename_file_02.js
@@ -0,0 +1,26 @@
+/* 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";
+
+// Test file rename functionality with non ascii characters
+
+add_task(function* () {
+ let projecteditor = yield addProjectEditorTabForTempDirectory();
+ ok(true, "ProjectEditor has loaded");
+
+ let root = [...projecteditor.project.allStores()][0].root;
+ is(root.path, TEMP_PATH, "The root store is set to the correct temp path.");
+
+ let childrenList = [];
+ for (let child of root.children) {
+ yield renameWithContextMenu(projecteditor,
+ projecteditor.projectTree.getViewContainer(child), ".ren\u0061\u0308med");
+ childrenList.push(child.basename + ".ren\u0061\u0308med");
+ }
+ for (let child of root.children) {
+ is(childrenList.indexOf(child.basename) == -1, false,
+ "Failed to update tree with non-ascii character");
+ }
+});
diff --git a/devtools/client/projecteditor/test/browser_projecteditor_saveall.js b/devtools/client/projecteditor/test/browser_projecteditor_saveall.js
new file mode 100644
index 000000000..2468ea4fc
--- /dev/null
+++ b/devtools/client/projecteditor/test/browser_projecteditor_saveall.js
@@ -0,0 +1,64 @@
+/* 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";
+
+//
+// Whitelisting this test.
+// As part of bug 1077403, the leaking uncaught rejection should be fixed.
+//
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed("destroy");
+
+loadHelperScript("helper_edits.js");
+
+// Test ProjectEditor basic functionality
+add_task(function* () {
+ let projecteditor = yield addProjectEditorTabForTempDirectory();
+ let TEMP_PATH = projecteditor.project.allPaths()[0];
+
+ is(getTempFile("").path, TEMP_PATH, "Temp path is set correctly.");
+
+ ok(projecteditor.currentEditor, "There is an editor for projecteditor");
+ let resources = projecteditor.project.allResources();
+
+ for (let data of helperEditData) {
+ info("Processing " + data.path);
+ let resource = resources.filter(r=>r.basename === data.basename)[0];
+ yield selectFile(projecteditor, resource);
+ yield editFile(projecteditor, getTempFile(data.path).path, data.newContent);
+ }
+
+ info("Saving all resources");
+ ok(projecteditor.hasUnsavedResources, "hasUnsavedResources");
+ yield projecteditor.saveAllFiles();
+ ok(!projecteditor.hasUnsavedResources, "!hasUnsavedResources");
+ for (let data of helperEditData) {
+ let filePath = getTempFile(data.path).path;
+ info("Asserting that data at " + filePath + " has been saved");
+ let resource = resources.filter(r=>r.basename === data.basename)[0];
+ yield selectFile(projecteditor, resource);
+ let editor = projecteditor.currentEditor;
+ let savedData = yield getFileData(filePath);
+ is(savedData, data.newContent, "Data has been correctly saved to disk");
+ }
+});
+
+function* editFile(projecteditor, filePath, newData) {
+ info("Testing file editing for: " + filePath);
+
+ let initialData = yield getFileData(filePath);
+ let editor = projecteditor.currentEditor;
+ let resource = projecteditor.resourceFor(editor);
+ let viewContainer = projecteditor.projectTree.getViewContainer(resource);
+ let originalTreeLabel = viewContainer.label.textContent;
+
+ is(resource.path, filePath, "Resource path is set correctly");
+ is(editor.editor.getText(), initialData, "Editor is loaded with correct file contents");
+
+ info("Setting text in the editor");
+
+ editor.editor.setText(newData);
+ is(editor.editor.getText(), newData, "Editor has been filled with new data");
+ is(viewContainer.label.textContent, "*" + originalTreeLabel, "Label is marked as changed");
+}
diff --git a/devtools/client/projecteditor/test/browser_projecteditor_stores.js b/devtools/client/projecteditor/test/browser_projecteditor_stores.js
new file mode 100644
index 000000000..c85a7526b
--- /dev/null
+++ b/devtools/client/projecteditor/test/browser_projecteditor_stores.js
@@ -0,0 +1,16 @@
+/* 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";
+
+// Test ProjectEditor basic functionality
+add_task(function* () {
+ let projecteditor = yield addProjectEditorTabForTempDirectory();
+ let TEMP_PATH = projecteditor.project.allPaths()[0];
+ is(getTempFile("").path, TEMP_PATH, "Temp path is set correctly.");
+
+ is(projecteditor.project.allPaths().length, 1, "1 path is set");
+ projecteditor.project.removeAllStores();
+ is(projecteditor.project.allPaths().length, 0, "No paths are remaining");
+});
diff --git a/devtools/client/projecteditor/test/browser_projecteditor_tree_selection_01.js b/devtools/client/projecteditor/test/browser_projecteditor_tree_selection_01.js
new file mode 100644
index 000000000..0a98f7122
--- /dev/null
+++ b/devtools/client/projecteditor/test/browser_projecteditor_tree_selection_01.js
@@ -0,0 +1,98 @@
+/* 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";
+
+// Test tree selection functionality
+
+add_task(function* () {
+ let projecteditor = yield addProjectEditorTabForTempDirectory();
+ let TEMP_PATH = projecteditor.project.allPaths()[0];
+
+ is(getTempFile("").path, TEMP_PATH, "Temp path is set correctly.");
+
+ ok(projecteditor.currentEditor, "There is an editor for projecteditor");
+ let resources = projecteditor.project.allResources();
+
+ is(
+ resources.map(r=>r.basename).join("|"),
+ TEMP_FOLDER_NAME + "|css|styles.css|data|img|icons|128x128.png|16x16.png|32x32.png|vector.svg|fake.png|js|script.js|index.html|LICENSE|README.md",
+ "Resources came through in proper order"
+ );
+
+ for (let i = 0; i < resources.length; i++) {
+ yield selectFileFirstLoad(projecteditor, resources[i]);
+ }
+ for (let i = 0; i < resources.length; i++) {
+ yield selectFileSubsequentLoad(projecteditor, resources[i]);
+ }
+ for (let i = 0; i < resources.length; i++) {
+ yield selectFileSubsequentLoad(projecteditor, resources[i]);
+ }
+});
+
+function* selectFileFirstLoad(projecteditor, resource) {
+ ok(resource && resource.path, "A valid resource has been passed in for selection " + (resource && resource.path));
+ projecteditor.projectTree.selectResource(resource);
+ let container = projecteditor.projectTree.getViewContainer(resource);
+
+ if (resource.isRoot) {
+ ok(container.expanded, "The root directory is expanded by default.");
+ container.line.click();
+ ok(container.expanded, "Clicking on the line does not toggles expansion.");
+ return;
+ }
+ if (resource.isDir) {
+ ok(!container.expanded, "A directory is not expanded by default.");
+ container.line.click();
+ ok(container.expanded, "Clicking on the line toggles expansion.");
+ container.line.click();
+ ok(!container.expanded, "Clicking on the line toggles expansion.");
+ return;
+ }
+
+ let [editorCreated, editorLoaded, editorActivated] = yield promise.all([
+ onceEditorCreated(projecteditor),
+ onceEditorLoad(projecteditor),
+ onceEditorActivated(projecteditor)
+ ]);
+
+ is(editorCreated, projecteditor.currentEditor, "Editor has been created for " + resource.path);
+ is(editorActivated, projecteditor.currentEditor, "Editor has been activated for " + resource.path);
+ is(editorLoaded, projecteditor.currentEditor, "Editor has been loaded for " + resource.path);
+}
+
+function* selectFileSubsequentLoad(projecteditor, resource) {
+ ok(resource && resource.path, "A valid resource has been passed in for selection " + (resource && resource.path));
+ projecteditor.projectTree.selectResource(resource);
+
+ if (resource.isDir) {
+ return;
+ }
+
+ // Make sure text editors are focused immediately when selected.
+ let focusPromise = promise.resolve();
+ if (projecteditor.currentEditor.editor) {
+ focusPromise = onEditorFocus(projecteditor.currentEditor);
+ }
+
+ // Only activated should fire the next time
+ // (may add load() if we begin checking for changes from disk)
+ let [editorActivated] = yield promise.all([
+ onceEditorActivated(projecteditor)
+ ]);
+
+ is(editorActivated, projecteditor.currentEditor, "Editor has been activated for " + resource.path);
+
+ yield focusPromise;
+}
+
+function onEditorFocus(editor) {
+ let def = promise.defer();
+ editor.on("focus", function focus() {
+ editor.off("focus", focus);
+ def.resolve();
+ });
+ return def.promise;
+}
diff --git a/devtools/client/projecteditor/test/browser_projecteditor_tree_selection_02.js b/devtools/client/projecteditor/test/browser_projecteditor_tree_selection_02.js
new file mode 100644
index 000000000..51826e4dc
--- /dev/null
+++ b/devtools/client/projecteditor/test/browser_projecteditor_tree_selection_02.js
@@ -0,0 +1,76 @@
+/* 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";
+
+//
+// Whitelisting this test.
+// As part of bug 1077403, the leaking uncaught rejection should be fixed.
+//
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed("destroy");
+
+// Test that files get reselected in the tree when their editor
+// is focused. https://bugzilla.mozilla.org/show_bug.cgi?id=1011116.
+
+add_task(function* () {
+ let projecteditor = yield addProjectEditorTabForTempDirectory();
+ let TEMP_PATH = projecteditor.project.allPaths()[0];
+
+ is(getTempFile("").path, TEMP_PATH, "Temp path is set correctly.");
+
+ ok(projecteditor.currentEditor, "There is an editor for projecteditor");
+ let resources = projecteditor.project.allResources();
+
+ is(
+ resources.map(r=>r.basename).join("|"),
+ TEMP_FOLDER_NAME + "|css|styles.css|data|img|icons|128x128.png|16x16.png|32x32.png|vector.svg|fake.png|js|script.js|index.html|LICENSE|README.md",
+ "Resources came through in proper order"
+ );
+
+ for (let i = 0; i < resources.length; i++) {
+ yield selectAndRefocusFile(projecteditor, resources[i]);
+ }
+});
+
+function* selectAndRefocusFile(projecteditor, resource) {
+ ok(resource && resource.path, "A valid resource has been passed in for selection " + (resource && resource.path));
+ projecteditor.projectTree.selectResource(resource);
+
+ if (resource.isDir) {
+ return;
+ }
+
+ let [editorCreated, editorLoaded, editorActivated] = yield promise.all([
+ onceEditorCreated(projecteditor),
+ onceEditorLoad(projecteditor),
+ onceEditorActivated(projecteditor)
+ ]);
+
+ if (projecteditor.currentEditor.editor) {
+ // This is a text editor. Go ahead and select a directory then refocus
+ // the editor to make sure it is reselected in tree.
+ let treeContainer = projecteditor.projectTree.getViewContainer(getDirectoryInStore(resource));
+ treeContainer.line.click();
+ EventUtils.synthesizeMouseAtCenter(treeContainer.elt, {}, treeContainer.elt.ownerDocument.defaultView);
+ let waitForTreeSelect = onTreeSelection(projecteditor);
+ projecteditor.currentEditor.focus();
+ yield waitForTreeSelect;
+
+ is(projecteditor.projectTree.getSelectedResource(), resource, "The resource gets reselected in the tree");
+ }
+}
+
+// Return a directory to select in the tree.
+function getDirectoryInStore(resource) {
+ return resource.store.root.childrenSorted.filter(r=>r.isDir)[0];
+}
+
+function onTreeSelection(projecteditor) {
+ let def = promise.defer();
+ projecteditor.projectTree.on("selection", function selection() {
+ projecteditor.projectTree.off("focus", selection);
+ def.resolve();
+ });
+ return def.promise;
+}
diff --git a/devtools/client/projecteditor/test/head.js b/devtools/client/projecteditor/test/head.js
new file mode 100644
index 000000000..d5d9ce849
--- /dev/null
+++ b/devtools/client/projecteditor/test/head.js
@@ -0,0 +1,391 @@
+/* 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 Cu = Components.utils;
+const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const {TargetFactory} = require("devtools/client/framework/target");
+const {console} = Cu.import("resource://gre/modules/Console.jsm", {});
+const promise = require("promise");
+const {FileUtils} = Cu.import("resource://gre/modules/FileUtils.jsm", {});
+const {NetUtil} = Cu.import("resource://gre/modules/NetUtil.jsm", {});
+const ProjectEditor = require("devtools/client/projecteditor/lib/projecteditor");
+const DevToolsUtils = require("devtools/shared/DevToolsUtils");
+const flags = require("devtools/shared/flags");
+
+const TEST_URL_ROOT = "http://mochi.test:8888/browser/devtools/client/projecteditor/test/";
+const SAMPLE_WEBAPP_URL = TEST_URL_ROOT + "/helper_homepage.html";
+var TEMP_PATH;
+var TEMP_FOLDER_NAME = "ProjectEditor" + (new Date().getTime());
+
+// All test are asynchronous
+waitForExplicitFinish();
+
+// Uncomment this pref to dump all devtools emitted events to the console.
+// Services.prefs.setBoolPref("devtools.dump.emit", true);
+
+// Set the testing flag and reset it when the test ends
+flags.testing = true;
+registerCleanupFunction(() => flags.testing = false);
+
+// Clear preferences that may be set during the course of tests.
+registerCleanupFunction(() => {
+ // Services.prefs.clearUserPref("devtools.dump.emit");
+ TEMP_PATH = null;
+ TEMP_FOLDER_NAME = null;
+});
+
+// Auto close the toolbox and close the test tabs when the test ends
+registerCleanupFunction(() => {
+ try {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ gDevTools.closeToolbox(target);
+ } catch (ex) {
+ dump(ex);
+ }
+ while (gBrowser.tabs.length > 1) {
+ gBrowser.removeCurrentTab();
+ }
+});
+
+/**
+ * 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 tab object when the url is loaded
+ */
+function addTab(url) {
+ info("Adding a new tab with URL: '" + url + "'");
+ let def = promise.defer();
+
+ let tab = gBrowser.selectedTab = gBrowser.addTab(url);
+ BrowserTestUtils.browserLoaded(tab.linkedBrowser).then(function () {
+ info("URL '" + url + "' loading complete");
+ waitForFocus(() => {
+ def.resolve(tab);
+ }, content);
+ });
+
+ return def.promise;
+}
+
+/**
+ * Some tests may need to import one or more of the test helper scripts.
+ * A test helper script is simply a js file that contains common test code that
+ * is either not common-enough to be in head.js, or that is located in a separate
+ * directory.
+ * The script will be loaded synchronously and in the test's scope.
+ * @param {String} filePath The file path, relative to the current directory.
+ * Examples:
+ * - "helper_attributes_test_runner.js"
+ * - "../../../commandline/test/helpers.js"
+ */
+function loadHelperScript(filePath) {
+ let testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/"));
+ Services.scriptloader.loadSubScript(testDir + "/" + filePath, this);
+}
+
+function addProjectEditorTabForTempDirectory(opts = {}) {
+ try {
+ TEMP_PATH = buildTempDirectoryStructure();
+ } catch (e) {
+ // Bug 1037292 - The test servers sometimes are unable to
+ // write to the temporary directory due to locked files
+ // or access denied errors. Try again if this failed.
+ info("Project Editor temp directory creation failed. Trying again.");
+ TEMP_PATH = buildTempDirectoryStructure();
+ }
+ let customOpts = {
+ name: "Test",
+ iconUrl: "chrome://devtools/skin/images/tool-options.svg",
+ projectOverviewURL: SAMPLE_WEBAPP_URL
+ };
+
+ info("Adding a project editor tab for editing at: " + TEMP_PATH);
+ return addProjectEditorTab(opts).then((projecteditor) => {
+ return projecteditor.setProjectToAppPath(TEMP_PATH, customOpts).then(() => {
+ return projecteditor;
+ });
+ });
+}
+
+function addProjectEditorTab(opts = {}) {
+ return addTab("chrome://devtools/content/projecteditor/chrome/content/projecteditor-test.xul").then(() => {
+ let iframe = content.document.getElementById("projecteditor-iframe");
+ if (opts.menubar !== false) {
+ opts.menubar = content.document.querySelector("menubar");
+ }
+ let projecteditor = ProjectEditor.ProjectEditor(iframe, opts);
+
+
+ ok(iframe, "Tab has placeholder iframe for projecteditor");
+ ok(projecteditor, "ProjectEditor has been initialized");
+
+ return projecteditor.loaded.then((projecteditor) => {
+ return projecteditor;
+ });
+ });
+}
+
+/**
+ * Build a temporary directory as a workspace for this loader
+ * https://developer.mozilla.org/en-US/Add-ons/Code_snippets/File_I_O
+ */
+function buildTempDirectoryStructure() {
+
+ let dirName = TEMP_FOLDER_NAME;
+ info("Building a temporary directory at " + dirName);
+
+ // First create (and remove) the temp dir to discard any changes
+ let TEMP_DIR = FileUtils.getDir("TmpD", [dirName], true);
+ TEMP_DIR.remove(true);
+
+ // Now rebuild our fake project.
+ TEMP_DIR = FileUtils.getDir("TmpD", [dirName], true);
+
+ FileUtils.getDir("TmpD", [dirName, "css"], true);
+ FileUtils.getDir("TmpD", [dirName, "data"], true);
+ FileUtils.getDir("TmpD", [dirName, "img", "icons"], true);
+ FileUtils.getDir("TmpD", [dirName, "js"], true);
+
+ let htmlFile = FileUtils.getFile("TmpD", [dirName, "index.html"]);
+ htmlFile.createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
+ writeToFileSync(htmlFile, [
+ "<!DOCTYPE html>",
+ '<html lang="en">',
+ " <head>",
+ ' <meta charset="utf-8" />',
+ " <title>ProjectEditor Temp File</title>",
+ ' <link rel="stylesheet" href="style.css" />',
+ " </head>",
+ ' <body id="home">',
+ " <p>ProjectEditor Temp File</p>",
+ " </body>",
+ "</html>"].join("\n")
+ );
+
+ let readmeFile = FileUtils.getFile("TmpD", [dirName, "README.md"]);
+ readmeFile.createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
+ writeToFileSync(readmeFile, [
+ "## Readme"
+ ].join("\n")
+ );
+
+ let licenseFile = FileUtils.getFile("TmpD", [dirName, "LICENSE"]);
+ licenseFile.createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
+ writeToFileSync(licenseFile, [
+ "/* 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/. */"
+ ].join("\n")
+ );
+
+ let cssFile = FileUtils.getFile("TmpD", [dirName, "css", "styles.css"]);
+ cssFile.createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
+ writeToFileSync(cssFile, [
+ "body {",
+ " background: red;",
+ "}"
+ ].join("\n")
+ );
+
+ FileUtils.getFile("TmpD", [dirName, "js", "script.js"]).createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
+
+ FileUtils.getFile("TmpD", [dirName, "img", "fake.png"]).createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
+ FileUtils.getFile("TmpD", [dirName, "img", "icons", "16x16.png"]).createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
+ FileUtils.getFile("TmpD", [dirName, "img", "icons", "32x32.png"]).createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
+ FileUtils.getFile("TmpD", [dirName, "img", "icons", "128x128.png"]).createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
+ FileUtils.getFile("TmpD", [dirName, "img", "icons", "vector.svg"]).createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
+
+ return TEMP_DIR.path;
+}
+
+// https://developer.mozilla.org/en-US/Add-ons/Code_snippets/File_I_O#Writing_to_a_file
+function writeToFile(file, data) {
+ if (typeof file === "string") {
+ file = new FileUtils.File(file);
+ }
+ info("Writing to file: " + file.path + " (exists? " + file.exists() + ")");
+ let defer = promise.defer();
+ var ostream = FileUtils.openSafeFileOutputStream(file);
+
+ var converter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"].
+ createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+ var istream = converter.convertToInputStream(data);
+
+ // The last argument (the callback) is optional.
+ NetUtil.asyncCopy(istream, ostream, function (status) {
+ if (!Components.isSuccessCode(status)) {
+ // Handle error!
+ info("ERROR WRITING TEMP FILE", status);
+ }
+ defer.resolve();
+ });
+ return defer.promise;
+}
+
+// This is used when setting up the test.
+// You should typically use the async version of this, writeToFile.
+// https://developer.mozilla.org/en-US/Add-ons/Code_snippets/File_I_O#More
+function writeToFileSync(file, data) {
+ // file is nsIFile, data is a string
+ var foStream = Components.classes["@mozilla.org/network/file-output-stream;1"].
+ createInstance(Components.interfaces.nsIFileOutputStream);
+
+ // use 0x02 | 0x10 to open file for appending.
+ foStream.init(file, 0x02 | 0x08 | 0x20, 0o666, 0);
+ // write, create, truncate
+ // In a c file operation, we have no need to set file mode with or operation,
+ // directly using "r" or "w" usually.
+
+ // if you are sure there will never ever be any non-ascii text in data you can
+ // also call foStream.write(data, data.length) directly
+ var converter = Components.classes["@mozilla.org/intl/converter-output-stream;1"].
+ createInstance(Components.interfaces.nsIConverterOutputStream);
+ converter.init(foStream, "UTF-8", 0, 0);
+ converter.writeString(data);
+ converter.close(); // this closes foStream
+}
+
+function getTempFile(path) {
+ let parts = [TEMP_FOLDER_NAME];
+ parts = parts.concat(path.split("/"));
+ return FileUtils.getFile("TmpD", parts);
+}
+
+// https://developer.mozilla.org/en-US/Add-ons/Code_snippets/File_I_O#Writing_to_a_file
+function* getFileData(file) {
+ if (typeof file === "string") {
+ file = new FileUtils.File(file);
+ }
+ let def = promise.defer();
+
+ NetUtil.asyncFetch({
+ uri: NetUtil.newURI(file),
+ loadUsingSystemPrincipal: true
+ }, function (inputStream, status) {
+ if (!Components.isSuccessCode(status)) {
+ info("ERROR READING TEMP FILE", status);
+ }
+
+ // Detect if an empty file is loaded
+ try {
+ inputStream.available();
+ } catch (e) {
+ def.resolve("");
+ return;
+ }
+
+ var data = NetUtil.readInputStreamToString(inputStream, inputStream.available());
+ def.resolve(data);
+ });
+
+ return def.promise;
+}
+
+/**
+ * Rename the resource of the provided container using the context menu.
+ *
+ * @param {ProjectEditor} projecteditor the current project editor instance
+ * @param {Shell} container for the resource to rename
+ * @param {String} newName the name to use for renaming the resource
+ * @return {Promise} a promise that resolves when the resource has been renamed
+ */
+var renameWithContextMenu = Task.async(function* (projecteditor,
+ container, newName) {
+ let popup = projecteditor.contextMenuPopup;
+ let resource = container.resource;
+ info("Going to attempt renaming for: " + resource.path);
+
+ let waitForPopupShow = onPopupShow(popup);
+ openContextMenu(container.label);
+ yield waitForPopupShow;
+
+ let renameCommand = popup.querySelector("[command=cmd-rename]");
+ ok(renameCommand, "Rename command exists in popup");
+ is(renameCommand.getAttribute("hidden"), "", "Rename command is visible");
+ is(renameCommand.getAttribute("disabled"), "", "Rename command is enabled");
+
+ renameCommand.click();
+ popup.hidePopup();
+ let input = container.elt.childNodes[0].childNodes[1];
+ input.value = resource.basename + newName;
+
+ let waitForProjectRefresh = onceProjectRefreshed(projecteditor);
+ EventUtils.synthesizeKey("VK_RETURN", {}, projecteditor.window);
+ yield waitForProjectRefresh;
+
+ try {
+ yield OS.File.stat(resource.path + newName);
+ ok(true, "File is renamed");
+ } catch (e) {
+ ok(false, "Failed to rename file");
+ }
+});
+
+function onceEditorCreated(projecteditor) {
+ let def = promise.defer();
+ projecteditor.once("onEditorCreated", (editor) => {
+ def.resolve(editor);
+ });
+ return def.promise;
+}
+
+function onceEditorLoad(projecteditor) {
+ let def = promise.defer();
+ projecteditor.once("onEditorLoad", (editor) => {
+ def.resolve(editor);
+ });
+ return def.promise;
+}
+
+function onceEditorActivated(projecteditor) {
+ let def = promise.defer();
+ projecteditor.once("onEditorActivated", (editor) => {
+ def.resolve(editor);
+ });
+ return def.promise;
+}
+
+function onceEditorSave(projecteditor) {
+ let def = promise.defer();
+ projecteditor.once("onEditorSave", (editor, resource) => {
+ def.resolve(resource);
+ });
+ return def.promise;
+}
+
+function onceProjectRefreshed(projecteditor) {
+ return new Promise(resolve => {
+ projecteditor.project.on("refresh-complete", function refreshComplete() {
+ projecteditor.project.off("refresh-complete", refreshComplete);
+ resolve();
+ });
+ });
+}
+
+function onPopupShow(menu) {
+ let defer = promise.defer();
+ menu.addEventListener("popupshown", function onpopupshown() {
+ menu.removeEventListener("popupshown", onpopupshown);
+ defer.resolve();
+ });
+ return defer.promise;
+}
+
+function onPopupHidden(menu) {
+ let defer = promise.defer();
+ menu.addEventListener("popuphidden", function onpopuphidden() {
+ menu.removeEventListener("popuphidden", onpopuphidden);
+ defer.resolve();
+ });
+ return defer.promise;
+}
+
+function openContextMenu(node) {
+ EventUtils.synthesizeMouseAtCenter(
+ node,
+ {button: 2, type: "contextmenu"},
+ node.ownerDocument.defaultView
+ );
+}
diff --git a/devtools/client/projecteditor/test/helper_edits.js b/devtools/client/projecteditor/test/helper_edits.js
new file mode 100644
index 000000000..d8e83672b
--- /dev/null
+++ b/devtools/client/projecteditor/test/helper_edits.js
@@ -0,0 +1,53 @@
+/* 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";
+
+var helperEditData = [
+ {
+ basename: "styles.css",
+ path: "css/styles.css",
+ newContent: "body,html { color: orange; }"
+ },
+ {
+ basename: "index.html",
+ path: "index.html",
+ newContent: "<h1>Changed Content Again</h1>"
+ },
+ {
+ basename: "LICENSE",
+ path: "LICENSE",
+ newContent: "My new license"
+ },
+ {
+ basename: "README.md",
+ path: "README.md",
+ newContent: "My awesome readme"
+ },
+ {
+ basename: "script.js",
+ path: "js/script.js",
+ newContent: "alert('hi')"
+ },
+ {
+ basename: "vector.svg",
+ path: "img/icons/vector.svg",
+ newContent: "<svg></svg>"
+ },
+];
+
+function* selectFile(projecteditor, resource) {
+ ok(resource && resource.path, "A valid resource has been passed in for selection " + (resource && resource.path));
+ projecteditor.projectTree.selectResource(resource);
+
+ if (resource.isDir) {
+ return;
+ }
+
+ let [editorActivated] = yield promise.all([
+ onceEditorActivated(projecteditor)
+ ]);
+
+ is(editorActivated, projecteditor.currentEditor, "Editor has been activated for " + resource.path);
+}
diff --git a/devtools/client/projecteditor/test/helper_homepage.html b/devtools/client/projecteditor/test/helper_homepage.html
new file mode 100644
index 000000000..a4402a9bd
--- /dev/null
+++ b/devtools/client/projecteditor/test/helper_homepage.html
@@ -0,0 +1 @@
+<h1>ProjectEditor tests</h1> \ No newline at end of file
diff --git a/devtools/client/responsive.html/actions/devices.js b/devtools/client/responsive.html/actions/devices.js
new file mode 100644
index 000000000..b06134450
--- /dev/null
+++ b/devtools/client/responsive.html/actions/devices.js
@@ -0,0 +1,138 @@
+/* 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 {
+ ADD_DEVICE,
+ ADD_DEVICE_TYPE,
+ LOAD_DEVICE_LIST_START,
+ LOAD_DEVICE_LIST_ERROR,
+ LOAD_DEVICE_LIST_END,
+ UPDATE_DEVICE_DISPLAYED,
+ UPDATE_DEVICE_MODAL_OPEN,
+} = require("./index");
+
+const { getDevices } = require("devtools/client/shared/devices");
+
+const Services = require("Services");
+const DISPLAYED_DEVICES_PREF = "devtools.responsive.html.displayedDeviceList";
+
+/**
+ * Returns an object containing the user preference of displayed devices.
+ *
+ * @return {Object} containing two Sets:
+ * - added: Names of the devices that were explicitly enabled by the user
+ * - removed: Names of the devices that were explicitly removed by the user
+ */
+function loadPreferredDevices() {
+ let preferredDevices = {
+ "added": new Set(),
+ "removed": new Set(),
+ };
+
+ if (Services.prefs.prefHasUserValue(DISPLAYED_DEVICES_PREF)) {
+ try {
+ let savedData = Services.prefs.getCharPref(DISPLAYED_DEVICES_PREF);
+ savedData = JSON.parse(savedData);
+ if (savedData.added && savedData.removed) {
+ preferredDevices.added = new Set(savedData.added);
+ preferredDevices.removed = new Set(savedData.removed);
+ }
+ } catch (e) {
+ console.error(e);
+ }
+ }
+
+ return preferredDevices;
+}
+
+/**
+ * Update the displayed device list preference with the given device list.
+ *
+ * @param {Object} containing two Sets:
+ * - added: Names of the devices that were explicitly enabled by the user
+ * - removed: Names of the devices that were explicitly removed by the user
+ */
+function updatePreferredDevices(devices) {
+ let devicesToSave = {
+ added: Array.from(devices.added),
+ removed: Array.from(devices.removed),
+ };
+ devicesToSave = JSON.stringify(devicesToSave);
+ Services.prefs.setCharPref(DISPLAYED_DEVICES_PREF, devicesToSave);
+}
+
+module.exports = {
+
+ // This function is only exported for testing purposes
+ _loadPreferredDevices: loadPreferredDevices,
+
+ updatePreferredDevices: updatePreferredDevices,
+
+ addDevice(device, deviceType) {
+ return {
+ type: ADD_DEVICE,
+ device,
+ deviceType,
+ };
+ },
+
+ addDeviceType(deviceType) {
+ return {
+ type: ADD_DEVICE_TYPE,
+ deviceType,
+ };
+ },
+
+ updateDeviceDisplayed(device, deviceType, displayed) {
+ return {
+ type: UPDATE_DEVICE_DISPLAYED,
+ device,
+ deviceType,
+ displayed,
+ };
+ },
+
+ loadDevices() {
+ return function* (dispatch, getState) {
+ yield dispatch({ type: LOAD_DEVICE_LIST_START });
+ let preferredDevices = loadPreferredDevices();
+ let devices;
+
+ try {
+ devices = yield getDevices();
+ } catch (e) {
+ console.error("Could not load device list: " + e);
+ dispatch({ type: LOAD_DEVICE_LIST_ERROR });
+ return;
+ }
+
+ for (let type of devices.TYPES) {
+ dispatch(module.exports.addDeviceType(type));
+ for (let device of devices[type]) {
+ if (device.os == "fxos") {
+ continue;
+ }
+
+ let newDevice = Object.assign({}, device, {
+ displayed: preferredDevices.added.has(device.name) ||
+ (device.featured && !(preferredDevices.removed.has(device.name))),
+ });
+
+ dispatch(module.exports.addDevice(newDevice, type));
+ }
+ }
+ dispatch({ type: LOAD_DEVICE_LIST_END });
+ };
+ },
+
+ updateDeviceModalOpen(isOpen) {
+ return {
+ type: UPDATE_DEVICE_MODAL_OPEN,
+ isOpen,
+ };
+ },
+
+};
diff --git a/devtools/client/responsive.html/actions/display-pixel-ratio.js b/devtools/client/responsive.html/actions/display-pixel-ratio.js
new file mode 100644
index 000000000..ff3343bb5
--- /dev/null
+++ b/devtools/client/responsive.html/actions/display-pixel-ratio.js
@@ -0,0 +1,23 @@
+/* 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 { CHANGE_DISPLAY_PIXEL_RATIO } = require("./index");
+
+module.exports = {
+
+ /**
+ * The pixel ratio of the display has changed. This may be triggered by the user
+ * when changing the monitor resolution, or when the window is dragged to a different
+ * display with a different pixel ratio.
+ */
+ changeDisplayPixelRatio(displayPixelRatio) {
+ return {
+ type: CHANGE_DISPLAY_PIXEL_RATIO,
+ displayPixelRatio,
+ };
+ },
+
+};
diff --git a/devtools/client/responsive.html/actions/index.js b/devtools/client/responsive.html/actions/index.js
new file mode 100644
index 000000000..06cc8d1a5
--- /dev/null
+++ b/devtools/client/responsive.html/actions/index.js
@@ -0,0 +1,77 @@
+/* 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";
+
+// This file lists all of the actions available in responsive design. This
+// central list of constants makes it easy to see all possible action names at
+// a glance. Please add a comment with each new action type.
+
+const { createEnum } = require("../utils/enum");
+
+createEnum([
+
+ // Add a new device.
+ "ADD_DEVICE",
+
+ // Add a new device type.
+ "ADD_DEVICE_TYPE",
+
+ // Add an additional viewport to display the document.
+ "ADD_VIEWPORT",
+
+ // Change the device displayed in the viewport.
+ "CHANGE_DEVICE",
+
+ // Change the location of the page. This may be triggered by the user
+ // directly entering a new URL, navigating with links, etc.
+ "CHANGE_LOCATION",
+
+ // The pixel ratio of the display has changed. This may be triggered by the user
+ // when changing the monitor resolution, or when the window is dragged to a different
+ // display with a different pixel ratio.
+ "CHANGE_DISPLAY_PIXEL_RATIO",
+
+ // Change the network throttling profile.
+ "CHANGE_NETWORK_THROTTLING",
+
+ // The pixel ratio of the viewport has changed. This may be triggered by the user
+ // when changing the device displayed in the viewport, or when a pixel ratio is
+ // selected from the DPR dropdown.
+ "CHANGE_PIXEL_RATIO",
+
+ // Change the touch simulation state.
+ "CHANGE_TOUCH_SIMULATION",
+
+ // Indicates that the device list is being loaded
+ "LOAD_DEVICE_LIST_START",
+
+ // Indicates that the device list loading action threw an error
+ "LOAD_DEVICE_LIST_ERROR",
+
+ // Indicates that the device list has been loaded successfully
+ "LOAD_DEVICE_LIST_END",
+
+ // Remove the viewport's device assocation.
+ "REMOVE_DEVICE",
+
+ // Resize the viewport.
+ "RESIZE_VIEWPORT",
+
+ // Rotate the viewport.
+ "ROTATE_VIEWPORT",
+
+ // Take a screenshot of the viewport.
+ "TAKE_SCREENSHOT_START",
+
+ // Indicates when the screenshot action ends.
+ "TAKE_SCREENSHOT_END",
+
+ // Update the device display state in the device selector.
+ "UPDATE_DEVICE_DISPLAYED",
+
+ // Update the device modal open state.
+ "UPDATE_DEVICE_MODAL_OPEN",
+
+], module.exports);
diff --git a/devtools/client/responsive.html/actions/location.js b/devtools/client/responsive.html/actions/location.js
new file mode 100644
index 000000000..565825e5e
--- /dev/null
+++ b/devtools/client/responsive.html/actions/location.js
@@ -0,0 +1,22 @@
+/* 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 { CHANGE_LOCATION } = require("./index");
+
+module.exports = {
+
+ /**
+ * The location of the page has changed. This may be triggered by the user
+ * directly entering a new URL, navigating with links, etc.
+ */
+ changeLocation(location) {
+ return {
+ type: CHANGE_LOCATION,
+ location,
+ };
+ },
+
+};
diff --git a/devtools/client/responsive.html/actions/moz.build b/devtools/client/responsive.html/actions/moz.build
new file mode 100644
index 000000000..8f44c7118
--- /dev/null
+++ b/devtools/client/responsive.html/actions/moz.build
@@ -0,0 +1,16 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'devices.js',
+ 'display-pixel-ratio.js',
+ 'index.js',
+ 'location.js',
+ 'network-throttling.js',
+ 'screenshot.js',
+ 'touch-simulation.js',
+ 'viewports.js',
+)
diff --git a/devtools/client/responsive.html/actions/network-throttling.js b/devtools/client/responsive.html/actions/network-throttling.js
new file mode 100644
index 000000000..e92fb995c
--- /dev/null
+++ b/devtools/client/responsive.html/actions/network-throttling.js
@@ -0,0 +1,21 @@
+/* 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 {
+ CHANGE_NETWORK_THROTTLING,
+} = require("./index");
+
+module.exports = {
+
+ changeNetworkThrottling(enabled, profile) {
+ return {
+ type: CHANGE_NETWORK_THROTTLING,
+ enabled,
+ profile,
+ };
+ },
+
+};
diff --git a/devtools/client/responsive.html/actions/screenshot.js b/devtools/client/responsive.html/actions/screenshot.js
new file mode 100644
index 000000000..8d660d74f
--- /dev/null
+++ b/devtools/client/responsive.html/actions/screenshot.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/. */
+
+/* eslint-env browser */
+
+"use strict";
+
+const {
+ TAKE_SCREENSHOT_START,
+ TAKE_SCREENSHOT_END,
+} = require("./index");
+
+const { getFormatStr } = require("../utils/l10n");
+const { getToplevelWindow } = require("sdk/window/utils");
+const { Task: { spawn } } = require("devtools/shared/task");
+const e10s = require("../utils/e10s");
+
+const CAMERA_AUDIO_URL = "resource://devtools/client/themes/audio/shutter.wav";
+
+const animationFrame = () => new Promise(resolve => {
+ window.requestAnimationFrame(resolve);
+});
+
+function getFileName() {
+ let date = new Date();
+ let month = ("0" + (date.getMonth() + 1)).substr(-2);
+ let day = ("0" + date.getDate()).substr(-2);
+ let dateString = [date.getFullYear(), month, day].join("-");
+ let timeString = date.toTimeString().replace(/:/g, ".").split(" ")[0];
+
+ return getFormatStr("responsive.screenshotGeneratedFilename", dateString,
+ timeString);
+}
+
+function createScreenshotFor(node) {
+ let mm = node.frameLoader.messageManager;
+
+ return e10s.request(mm, "RequestScreenshot");
+}
+
+function saveToFile(data, filename) {
+ return spawn(function* () {
+ const chromeWindow = getToplevelWindow(window);
+ const chromeDocument = chromeWindow.document;
+
+ // append .png extension to filename if it doesn't exist
+ filename = filename.replace(/\.png$|$/i, ".png");
+
+ chromeWindow.saveURL(data, filename, null,
+ true, true,
+ chromeDocument.documentURIObject, chromeDocument);
+ });
+}
+
+function simulateCameraEffects(node) {
+ let cameraAudio = new window.Audio(CAMERA_AUDIO_URL);
+ cameraAudio.play();
+ node.animate({ opacity: [ 0, 1 ] }, 500);
+}
+
+module.exports = {
+
+ takeScreenshot() {
+ return function* (dispatch, getState) {
+ yield dispatch({ type: TAKE_SCREENSHOT_START });
+
+ // Waiting the next repaint, to ensure the react components
+ // can be properly render after the action dispatched above
+ yield animationFrame();
+
+ let iframe = document.querySelector("iframe");
+ let data = yield createScreenshotFor(iframe);
+
+ simulateCameraEffects(iframe);
+
+ yield saveToFile(data, getFileName());
+
+ dispatch({ type: TAKE_SCREENSHOT_END });
+ };
+ }
+};
diff --git a/devtools/client/responsive.html/actions/touch-simulation.js b/devtools/client/responsive.html/actions/touch-simulation.js
new file mode 100644
index 000000000..8f98101e7
--- /dev/null
+++ b/devtools/client/responsive.html/actions/touch-simulation.js
@@ -0,0 +1,22 @@
+/* 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/. */
+
+/* eslint-env browser */
+
+"use strict";
+
+const {
+ CHANGE_TOUCH_SIMULATION
+} = require("./index");
+
+module.exports = {
+
+ changeTouchSimulation(enabled) {
+ return {
+ type: CHANGE_TOUCH_SIMULATION,
+ enabled,
+ };
+ },
+
+};
diff --git a/devtools/client/responsive.html/actions/viewports.js b/devtools/client/responsive.html/actions/viewports.js
new file mode 100644
index 000000000..7e51ada4a
--- /dev/null
+++ b/devtools/client/responsive.html/actions/viewports.js
@@ -0,0 +1,81 @@
+/* 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 {
+ ADD_VIEWPORT,
+ CHANGE_DEVICE,
+ CHANGE_PIXEL_RATIO,
+ REMOVE_DEVICE,
+ RESIZE_VIEWPORT,
+ ROTATE_VIEWPORT
+} = require("./index");
+
+module.exports = {
+
+ /**
+ * Add an additional viewport to display the document.
+ */
+ addViewport() {
+ return {
+ type: ADD_VIEWPORT,
+ };
+ },
+
+ /**
+ * Change the viewport device.
+ */
+ changeDevice(id, device) {
+ return {
+ type: CHANGE_DEVICE,
+ id,
+ device,
+ };
+ },
+
+ /**
+ * Change the viewport pixel ratio.
+ */
+ changePixelRatio(id, pixelRatio = 0) {
+ return {
+ type: CHANGE_PIXEL_RATIO,
+ id,
+ pixelRatio,
+ };
+ },
+
+ /**
+ * Remove the viewport's device assocation.
+ */
+ removeDevice(id) {
+ return {
+ type: REMOVE_DEVICE,
+ id,
+ };
+ },
+
+ /**
+ * Resize the viewport.
+ */
+ resizeViewport(id, width, height) {
+ return {
+ type: RESIZE_VIEWPORT,
+ id,
+ width,
+ height,
+ };
+ },
+
+ /**
+ * Rotate the viewport.
+ */
+ rotateViewport(id) {
+ return {
+ type: ROTATE_VIEWPORT,
+ id,
+ };
+ },
+
+};
diff --git a/devtools/client/responsive.html/app.js b/devtools/client/responsive.html/app.js
new file mode 100644
index 000000000..739d32b0e
--- /dev/null
+++ b/devtools/client/responsive.html/app.js
@@ -0,0 +1,209 @@
+/* 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/. */
+
+ /* eslint-env browser */
+
+"use strict";
+
+const { createClass, createFactory, PropTypes, DOM: dom } =
+ require("devtools/client/shared/vendor/react");
+const { connect } = require("devtools/client/shared/vendor/react-redux");
+
+const {
+ updateDeviceDisplayed,
+ updateDeviceModalOpen,
+ updatePreferredDevices,
+} = require("./actions/devices");
+const { changeNetworkThrottling } = require("./actions/network-throttling");
+const { takeScreenshot } = require("./actions/screenshot");
+const { changeTouchSimulation } = require("./actions/touch-simulation");
+const {
+ changeDevice,
+ changePixelRatio,
+ removeDevice,
+ resizeViewport,
+ rotateViewport,
+} = require("./actions/viewports");
+const DeviceModal = createFactory(require("./components/device-modal"));
+const GlobalToolbar = createFactory(require("./components/global-toolbar"));
+const Viewports = createFactory(require("./components/viewports"));
+const Types = require("./types");
+
+let App = createClass({
+ displayName: "App",
+
+ propTypes: {
+ devices: PropTypes.shape(Types.devices).isRequired,
+ displayPixelRatio: Types.pixelRatio.value.isRequired,
+ location: Types.location.isRequired,
+ networkThrottling: PropTypes.shape(Types.networkThrottling).isRequired,
+ screenshot: PropTypes.shape(Types.screenshot).isRequired,
+ touchSimulation: PropTypes.shape(Types.touchSimulation).isRequired,
+ viewports: PropTypes.arrayOf(PropTypes.shape(Types.viewport)).isRequired,
+ },
+
+ onBrowserMounted() {
+ window.postMessage({ type: "browser-mounted" }, "*");
+ },
+
+ onChangeDevice(id, device) {
+ window.postMessage({
+ type: "change-device",
+ device,
+ }, "*");
+ this.props.dispatch(changeDevice(id, device.name));
+ this.props.dispatch(changeTouchSimulation(device.touch));
+ this.props.dispatch(changePixelRatio(id, device.pixelRatio));
+ },
+
+ onChangeNetworkThrottling(enabled, profile) {
+ window.postMessage({
+ type: "change-network-throtting",
+ enabled,
+ profile,
+ }, "*");
+ this.props.dispatch(changeNetworkThrottling(enabled, profile));
+ },
+
+ onChangePixelRatio(pixelRatio) {
+ window.postMessage({
+ type: "change-pixel-ratio",
+ pixelRatio,
+ }, "*");
+ this.props.dispatch(changePixelRatio(0, pixelRatio));
+ },
+
+ onChangeTouchSimulation(enabled) {
+ window.postMessage({
+ type: "change-touch-simulation",
+ enabled,
+ }, "*");
+ this.props.dispatch(changeTouchSimulation(enabled));
+ },
+
+ onContentResize({ width, height }) {
+ window.postMessage({
+ type: "content-resize",
+ width,
+ height,
+ }, "*");
+ },
+
+ onDeviceListUpdate(devices) {
+ updatePreferredDevices(devices);
+ },
+
+ onExit() {
+ window.postMessage({ type: "exit" }, "*");
+ },
+
+ onRemoveDevice(id) {
+ // TODO: Bug 1332754: Move messaging and logic into the action creator.
+ window.postMessage({
+ type: "remove-device",
+ }, "*");
+ this.props.dispatch(removeDevice(id));
+ this.props.dispatch(changeTouchSimulation(false));
+ this.props.dispatch(changePixelRatio(id, 0));
+ },
+
+ onResizeViewport(id, width, height) {
+ this.props.dispatch(resizeViewport(id, width, height));
+ },
+
+ onRotateViewport(id) {
+ this.props.dispatch(rotateViewport(id));
+ },
+
+ onScreenshot() {
+ this.props.dispatch(takeScreenshot());
+ },
+
+ onUpdateDeviceDisplayed(device, deviceType, displayed) {
+ this.props.dispatch(updateDeviceDisplayed(device, deviceType, displayed));
+ },
+
+ onUpdateDeviceModalOpen(isOpen) {
+ this.props.dispatch(updateDeviceModalOpen(isOpen));
+ },
+
+ render() {
+ let {
+ devices,
+ displayPixelRatio,
+ location,
+ networkThrottling,
+ screenshot,
+ touchSimulation,
+ viewports,
+ } = this.props;
+
+ let {
+ onBrowserMounted,
+ onChangeDevice,
+ onChangeNetworkThrottling,
+ onChangePixelRatio,
+ onChangeTouchSimulation,
+ onContentResize,
+ onDeviceListUpdate,
+ onExit,
+ onRemoveDevice,
+ onResizeViewport,
+ onRotateViewport,
+ onScreenshot,
+ onUpdateDeviceDisplayed,
+ onUpdateDeviceModalOpen,
+ } = this;
+
+ let selectedDevice = "";
+ let selectedPixelRatio = { value: 0 };
+
+ if (viewports.length) {
+ selectedDevice = viewports[0].device;
+ selectedPixelRatio = viewports[0].pixelRatio;
+ }
+
+ return dom.div(
+ {
+ id: "app",
+ },
+ GlobalToolbar({
+ devices,
+ displayPixelRatio,
+ networkThrottling,
+ screenshot,
+ selectedDevice,
+ selectedPixelRatio,
+ touchSimulation,
+ onChangeNetworkThrottling,
+ onChangePixelRatio,
+ onChangeTouchSimulation,
+ onExit,
+ onScreenshot,
+ }),
+ Viewports({
+ devices,
+ location,
+ screenshot,
+ viewports,
+ onBrowserMounted,
+ onChangeDevice,
+ onContentResize,
+ onRemoveDevice,
+ onRotateViewport,
+ onResizeViewport,
+ onUpdateDeviceModalOpen,
+ }),
+ DeviceModal({
+ devices,
+ onDeviceListUpdate,
+ onUpdateDeviceDisplayed,
+ onUpdateDeviceModalOpen,
+ })
+ );
+ },
+
+});
+
+module.exports = connect(state => state)(App);
diff --git a/devtools/client/responsive.html/browser/moz.build b/devtools/client/responsive.html/browser/moz.build
new file mode 100644
index 000000000..f99bbc443
--- /dev/null
+++ b/devtools/client/responsive.html/browser/moz.build
@@ -0,0 +1,11 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'swap.js',
+ 'tunnel.js',
+ 'web-navigation.js',
+)
diff --git a/devtools/client/responsive.html/browser/swap.js b/devtools/client/responsive.html/browser/swap.js
new file mode 100644
index 000000000..7ab028065
--- /dev/null
+++ b/devtools/client/responsive.html/browser/swap.js
@@ -0,0 +1,309 @@
+/* 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 promise = require("promise");
+const { Task } = require("devtools/shared/task");
+const { tunnelToInnerBrowser } = require("./tunnel");
+
+/**
+ * Swap page content from an existing tab into a new browser within a container
+ * page. Page state is preserved by using `swapFrameLoaders`, just like when
+ * you move a tab to a new window. This provides a seamless transition for the
+ * user since the page is not reloaded.
+ *
+ * See /devtools/docs/responsive-design-mode.md for a high level overview of how
+ * this is used in RDM. The steps described there are copied into the code
+ * below.
+ *
+ * For additional low level details about swapping browser content,
+ * see /devtools/client/responsive.html/docs/browser-swap.md.
+ *
+ * @param tab
+ * A browser tab with content to be swapped.
+ * @param containerURL
+ * URL to a page that holds an inner browser.
+ * @param getInnerBrowser
+ * Function that returns a Promise to the inner browser within the
+ * container page. It is called with the outer browser that loaded the
+ * container page.
+ */
+function swapToInnerBrowser({ tab, containerURL, getInnerBrowser }) {
+ let gBrowser = tab.ownerDocument.defaultView.gBrowser;
+ let innerBrowser;
+ let tunnel;
+
+ // Dispatch a custom event each time the _viewport content_ is swapped from one browser
+ // to another. DevTools server code uses this to follow the content if there is an
+ // active DevTools connection. While browser.xml does dispatch it's own SwapDocShells
+ // event, this one is easier for DevTools to follow because it's only emitted once per
+ // transition, instead of twice like SwapDocShells.
+ let dispatchDevToolsBrowserSwap = (from, to) => {
+ let CustomEvent = tab.ownerDocument.defaultView.CustomEvent;
+ let event = new CustomEvent("DevTools:BrowserSwap", {
+ detail: to,
+ bubbles: true,
+ });
+ from.dispatchEvent(event);
+ };
+
+ return {
+
+ start: Task.async(function* () {
+ tab.isResponsiveDesignMode = true;
+
+ // Freeze navigation temporarily to avoid "blinking" in the location bar.
+ freezeNavigationState(tab);
+
+ // 1. Create a temporary, hidden tab to load the tool UI.
+ let containerTab = gBrowser.addTab("about:blank", {
+ skipAnimation: true,
+ forceNotRemote: true,
+ });
+ gBrowser.hideTab(containerTab);
+ let containerBrowser = containerTab.linkedBrowser;
+ // Prevent the `containerURL` from ending up in the tab's history.
+ containerBrowser.loadURIWithFlags(containerURL, {
+ flags: Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_HISTORY,
+ });
+
+ // Copy tab listener state flags to container tab. Each tab gets its own tab
+ // listener and state flags which cache document loading progress. The state flags
+ // are checked when switching tabs to update the browser UI. The later step of
+ // `swapBrowsersAndCloseOther` will fold the state back into the main tab.
+ let stateFlags = gBrowser._tabListeners.get(tab).mStateFlags;
+ gBrowser._tabListeners.get(containerTab).mStateFlags = stateFlags;
+
+ // 2. Mark the tool tab browser's docshell as active so the viewport frame
+ // is created eagerly and will be ready to swap.
+ // This line is crucial when the tool UI is loaded into a background tab.
+ // Without it, the viewport browser's frame is created lazily, leading to
+ // a multi-second delay before it would be possible to `swapFrameLoaders`.
+ // Even worse than the delay, there appears to be no obvious event fired
+ // after the frame is set lazily, so it's unclear how to know that work
+ // has finished.
+ containerBrowser.docShellIsActive = true;
+
+ // 3. Create the initial viewport inside the tool UI.
+ // The calling application will use container page loaded into the tab to
+ // do whatever it needs to create the inner browser.
+ yield tabLoaded(containerTab);
+ innerBrowser = yield getInnerBrowser(containerBrowser);
+ addXULBrowserDecorations(innerBrowser);
+ if (innerBrowser.isRemoteBrowser != tab.linkedBrowser.isRemoteBrowser) {
+ throw new Error("The inner browser's remoteness must match the " +
+ "original tab.");
+ }
+
+ // 4. Swap tab content from the regular browser tab to the browser within
+ // the viewport in the tool UI, preserving all state via
+ // `gBrowser._swapBrowserDocShells`.
+ dispatchDevToolsBrowserSwap(tab.linkedBrowser, innerBrowser);
+ gBrowser._swapBrowserDocShells(tab, innerBrowser);
+
+ // 5. Force the original browser tab to be non-remote since the tool UI
+ // must be loaded in the parent process, and we're about to swap the
+ // tool UI into this tab.
+ gBrowser.updateBrowserRemoteness(tab.linkedBrowser, false);
+
+ // 6. Swap the tool UI (with viewport showing the content) into the
+ // original browser tab and close the temporary tab used to load the
+ // tool via `swapBrowsersAndCloseOther`.
+ gBrowser.swapBrowsersAndCloseOther(tab, containerTab);
+
+ // 7. Start a tunnel from the tool tab's browser to the viewport browser
+ // so that some browser UI functions, like navigation, are connected to
+ // the content in the viewport, instead of the tool page.
+ tunnel = tunnelToInnerBrowser(tab.linkedBrowser, innerBrowser);
+ yield tunnel.start();
+
+ // Swapping browsers disconnects the find bar UI from the browser.
+ // If the find bar has been initialized, reconnect it.
+ if (gBrowser.isFindBarInitialized(tab)) {
+ let findBar = gBrowser.getFindBar(tab);
+ findBar.browser = tab.linkedBrowser;
+ if (!findBar.hidden) {
+ // Force the find bar to activate again, restoring the search string.
+ findBar.onFindCommand();
+ }
+ }
+
+ // Force the browser UI to match the new state of the tab and browser.
+ thawNavigationState(tab);
+ gBrowser.setTabTitle(tab);
+ gBrowser.updateCurrentBrowser(true);
+ }),
+
+ stop() {
+ // 1. Stop the tunnel between outer and inner browsers.
+ tunnel.stop();
+ tunnel = null;
+
+ // 2. Create a temporary, hidden tab to hold the content.
+ let contentTab = gBrowser.addTab("about:blank", {
+ skipAnimation: true,
+ });
+ gBrowser.hideTab(contentTab);
+ let contentBrowser = contentTab.linkedBrowser;
+
+ // 3. Mark the content tab browser's docshell as active so the frame
+ // is created eagerly and will be ready to swap.
+ contentBrowser.docShellIsActive = true;
+
+ // 4. Swap tab content from the browser within the viewport in the tool UI
+ // to the regular browser tab, preserving all state via
+ // `gBrowser._swapBrowserDocShells`.
+ dispatchDevToolsBrowserSwap(innerBrowser, contentBrowser);
+ gBrowser._swapBrowserDocShells(contentTab, innerBrowser);
+ innerBrowser = null;
+
+ // Copy tab listener state flags to content tab. See similar comment in `start`
+ // above for more details.
+ let stateFlags = gBrowser._tabListeners.get(tab).mStateFlags;
+ gBrowser._tabListeners.get(contentTab).mStateFlags = stateFlags;
+
+ // 5. Force the original browser tab to be remote since web content is
+ // loaded in the child process, and we're about to swap the content
+ // into this tab.
+ gBrowser.updateBrowserRemoteness(tab.linkedBrowser, true);
+
+ // 6. Swap the content into the original browser tab and close the
+ // temporary tab used to hold the content via
+ // `swapBrowsersAndCloseOther`.
+ dispatchDevToolsBrowserSwap(contentBrowser, tab.linkedBrowser);
+ gBrowser.swapBrowsersAndCloseOther(tab, contentTab);
+
+ // Swapping browsers disconnects the find bar UI from the browser.
+ // If the find bar has been initialized, reconnect it.
+ if (gBrowser.isFindBarInitialized(tab)) {
+ let findBar = gBrowser.getFindBar(tab);
+ findBar.browser = tab.linkedBrowser;
+ if (!findBar.hidden) {
+ // Force the find bar to activate again, restoring the search string.
+ findBar.onFindCommand();
+ }
+ }
+
+ gBrowser = null;
+
+ // The focus manager seems to get a little dizzy after all this swapping. If a
+ // content element had been focused inside the viewport before stopping, it will
+ // have lost focus. Activate the frame to restore expected focus.
+ tab.linkedBrowser.frameLoader.activateRemoteFrame();
+
+ delete tab.isResponsiveDesignMode;
+ },
+
+ };
+}
+
+/**
+ * Browser navigation properties we'll freeze temporarily to avoid "blinking" in the
+ * location bar, etc. caused by the containerURL peeking through before the swap is
+ * complete.
+ */
+const NAVIGATION_PROPERTIES = [
+ "currentURI",
+ "contentTitle",
+ "securityUI",
+];
+
+function freezeNavigationState(tab) {
+ // Browser navigation properties we'll freeze temporarily to avoid "blinking" in the
+ // location bar, etc. caused by the containerURL peeking through before the swap is
+ // complete.
+ for (let property of NAVIGATION_PROPERTIES) {
+ let value = tab.linkedBrowser[property];
+ Object.defineProperty(tab.linkedBrowser, property, {
+ get() {
+ return value;
+ },
+ configurable: true,
+ enumerable: true,
+ });
+ }
+}
+
+function thawNavigationState(tab) {
+ // Thaw out the properties we froze at the beginning now that the swap is complete.
+ for (let property of NAVIGATION_PROPERTIES) {
+ delete tab.linkedBrowser[property];
+ }
+}
+
+/**
+ * Browser elements that are passed to `gBrowser._swapBrowserDocShells` are
+ * expected to have certain properties that currently exist only on
+ * <xul:browser> elements. In particular, <iframe mozbrowser> elements don't
+ * have them.
+ *
+ * Rather than duplicate the swapping code used by the browser to work around
+ * this, we stub out the missing properties needed for the swap to complete.
+ */
+function addXULBrowserDecorations(browser) {
+ if (browser.isRemoteBrowser == undefined) {
+ Object.defineProperty(browser, "isRemoteBrowser", {
+ get() {
+ return this.getAttribute("remote") == "true";
+ },
+ configurable: true,
+ enumerable: true,
+ });
+ }
+ if (browser.messageManager == undefined) {
+ Object.defineProperty(browser, "messageManager", {
+ get() {
+ return this.frameLoader.messageManager;
+ },
+ configurable: true,
+ enumerable: true,
+ });
+ }
+ if (browser.outerWindowID == undefined) {
+ Object.defineProperty(browser, "outerWindowID", {
+ get() {
+ return browser._outerWindowID;
+ },
+ configurable: true,
+ enumerable: true,
+ });
+ }
+
+ // It's not necessary for these to actually do anything. These properties are
+ // swapped between browsers in browser.xml's `swapDocShells`, and then their
+ // `swapBrowser` methods are called, so we define them here for that to work
+ // without errors. During the swap process above, these will move from the
+ // the new inner browser to the original tab's browser (step 4) and then to
+ // the temporary container tab's browser (step 7), which is then closed.
+ if (browser._remoteWebNavigationImpl == undefined) {
+ browser._remoteWebNavigationImpl = {
+ swapBrowser() {},
+ };
+ }
+ if (browser._remoteWebProgressManager == undefined) {
+ browser._remoteWebProgressManager = {
+ swapBrowser() {},
+ };
+ }
+}
+
+function tabLoaded(tab) {
+ let deferred = promise.defer();
+
+ function handle(event) {
+ if (event.originalTarget != tab.linkedBrowser.contentDocument ||
+ event.target.location.href == "about:blank") {
+ return;
+ }
+ tab.linkedBrowser.removeEventListener("load", handle, true);
+ deferred.resolve(event);
+ }
+
+ tab.linkedBrowser.addEventListener("load", handle, true);
+ return deferred.promise;
+}
+
+exports.swapToInnerBrowser = swapToInnerBrowser;
diff --git a/devtools/client/responsive.html/browser/tunnel.js b/devtools/client/responsive.html/browser/tunnel.js
new file mode 100644
index 000000000..fdbfe8918
--- /dev/null
+++ b/devtools/client/responsive.html/browser/tunnel.js
@@ -0,0 +1,619 @@
+/* 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 { Task } = require("devtools/shared/task");
+const { BrowserElementWebNavigation } = require("./web-navigation");
+const { getStack } = require("devtools/shared/platform/stack");
+
+// A symbol used to hold onto the frame loader from the outer browser while tunneling.
+const FRAME_LOADER = Symbol("devtools/responsive/frame-loader");
+
+function debug(msg) {
+ // console.log(msg);
+}
+
+/**
+ * Properties swapped between browsers by browser.xml's `swapDocShells`. See also the
+ * list at /devtools/client/responsive.html/docs/browser-swap.md.
+ */
+const SWAPPED_BROWSER_STATE = [
+ "_remoteFinder",
+ "_securityUI",
+ "_documentURI",
+ "_documentContentType",
+ "_contentTitle",
+ "_characterSet",
+ "_contentPrincipal",
+ "_imageDocument",
+ "_fullZoom",
+ "_textZoom",
+ "_isSyntheticDocument",
+ "_innerWindowID",
+ "_manifestURI",
+];
+
+/**
+ * This module takes an "outer" <xul:browser> from a browser tab as described by
+ * Firefox's tabbrowser.xml and wires it up to an "inner" <iframe mozbrowser>
+ * browser element containing arbitrary page content of interest.
+ *
+ * The inner <iframe mozbrowser> element is _just_ the page content. It is not
+ * enough to to replace <xul:browser> on its own. <xul:browser> comes along
+ * with lots of associated functionality via XBL bindings defined for such
+ * elements in browser.xml and remote-browser.xml, and the Firefox UI depends on
+ * these various things to make the UI function.
+ *
+ * By mapping various methods, properties, and messages from the outer browser
+ * to the inner browser, we can control the content inside the inner browser
+ * using the standard Firefox UI elements for navigation, reloading, and more.
+ *
+ * The approaches used in this module were chosen to avoid needing changes to
+ * the core browser for this specialized use case. If we start to increase
+ * usage of <iframe mozbrowser> in the core browser, we should avoid this module
+ * and instead refactor things to work with mozbrowser directly.
+ *
+ * For the moment though, this serves as a sufficient path to connect the
+ * Firefox UI to a mozbrowser.
+ *
+ * @param outer
+ * A <xul:browser> from a regular browser tab.
+ * @param inner
+ * A <iframe mozbrowser> containing page content to be wired up to the
+ * primary browser UI via the outer browser.
+ */
+function tunnelToInnerBrowser(outer, inner) {
+ let browserWindow = outer.ownerDocument.defaultView;
+ let gBrowser = browserWindow.gBrowser;
+ let mmTunnel;
+
+ return {
+
+ start: Task.async(function* () {
+ if (outer.isRemoteBrowser) {
+ throw new Error("The outer browser must be non-remote.");
+ }
+ if (!inner.isRemoteBrowser) {
+ throw new Error("The inner browser must be remote.");
+ }
+
+ // Various browser methods access the `frameLoader` property, including:
+ // * `saveBrowser` from contentAreaUtils.js
+ // * `docShellIsActive` from remote-browser.xml
+ // * `hasContentOpener` from remote-browser.xml
+ // * `preserveLayers` from remote-browser.xml
+ // * `receiveMessage` from SessionStore.jsm
+ // In general, these methods are interested in the `frameLoader` for the content,
+ // so we redirect them to the inner browser's `frameLoader`.
+ outer[FRAME_LOADER] = outer.frameLoader;
+ Object.defineProperty(outer, "frameLoader", {
+ get() {
+ let stack = getStack();
+ // One exception is `receiveMessage` from SessionStore.jsm. SessionStore
+ // expects data updates to come in as messages targeted to a <xul:browser>.
+ // In addition, it verifies[1] correctness by checking that the received
+ // message's `targetFrameLoader` property matches the `frameLoader` of the
+ // <xul:browser>. To keep SessionStore functioning as expected, we give it the
+ // outer `frameLoader` as if nothing has changed.
+ // [1]: https://dxr.mozilla.org/mozilla-central/rev/b1b18f25c0ea69d9ee57c4198d577dfcd0129ce1/browser/components/sessionstore/SessionStore.jsm#716
+ if (stack.caller.filename.endsWith("SessionStore.jsm")) {
+ return outer[FRAME_LOADER];
+ }
+ return inner.frameLoader;
+ },
+ configurable: true,
+ enumerable: true,
+ });
+
+ // The `outerWindowID` of the content is used by browser actions like view source
+ // and print. They send the ID down to the client to find the right content frame
+ // to act on.
+ Object.defineProperty(outer, "outerWindowID", {
+ get() {
+ return inner.outerWindowID;
+ },
+ configurable: true,
+ enumerable: true,
+ });
+
+ // The `permanentKey` property on a <xul:browser> is used to index into various maps
+ // held by the session store. When you swap content around with
+ // `_swapBrowserDocShells`, these keys are also swapped so they follow the content.
+ // This means the key that matches the content is on the inner browser. Since we
+ // want the browser UI to believe the page content is part of the outer browser, we
+ // copy the content's `permanentKey` up to the outer browser.
+ debug("Copy inner permanentKey to outer browser");
+ outer.permanentKey = inner.permanentKey;
+
+ // Replace the outer browser's native messageManager with a message manager tunnel
+ // which we can use to route messages of interest to the inner browser instead.
+ // Note: The _actual_ messageManager accessible from
+ // `browser.frameLoader.messageManager` is not overridable and is left unchanged.
+ // Only the XBL getter `browser.messageManager` is overridden. Browser UI code
+ // always uses this getter instead of `browser.frameLoader.messageManager` directly,
+ // so this has the effect of overriding the message manager for browser UI code.
+ mmTunnel = new MessageManagerTunnel(outer, inner);
+
+ // We are tunneling to an inner browser with a specific remoteness, so it is simpler
+ // for the logic of the browser UI to assume this tab has taken on that remoteness,
+ // even though it's not true. Since the actions the browser UI performs are sent
+ // down to the inner browser by this tunnel, the tab's remoteness effectively is the
+ // remoteness of the inner browser.
+ outer.setAttribute("remote", "true");
+
+ // Clear out any cached state that references the current non-remote XBL binding,
+ // such as form fill controllers. Otherwise they will remain in place and leak the
+ // outer docshell.
+ outer.destroy();
+ // The XBL binding for remote browsers uses the message manager for many actions in
+ // the UI and that works well here, since it gives us one main thing we need to
+ // route to the inner browser (the messages), instead of having to tweak many
+ // different browser properties. It is safe to alter a XBL binding dynamically.
+ // The content within is not reloaded.
+ outer.style.MozBinding = "url(chrome://browser/content/tabbrowser.xml" +
+ "#tabbrowser-remote-browser)";
+
+ // The constructor of the new XBL binding is run asynchronously and there is no
+ // event to signal its completion. Spin an event loop to watch for properties that
+ // are set by the contructor.
+ while (!outer._remoteWebNavigation) {
+ Services.tm.currentThread.processNextEvent(true);
+ }
+
+ // Replace the `webNavigation` object with our own version which tries to use
+ // mozbrowser APIs where possible. This replaces the webNavigation object that the
+ // remote-browser.xml binding creates. We do not care about it's original value
+ // because stop() will remove the remote-browser.xml binding and these will no
+ // longer be used.
+ let webNavigation = new BrowserElementWebNavigation(inner);
+ webNavigation.copyStateFrom(inner._remoteWebNavigationImpl);
+ outer._remoteWebNavigation = webNavigation;
+ outer._remoteWebNavigationImpl = webNavigation;
+
+ // Now that we've flipped to the remote browser XBL binding, add `progressListener`
+ // onto the remote version of `webProgress`. Normally tabbrowser.xml does this step
+ // when it creates a new browser, etc. Since we manually changed the XBL binding
+ // above, it caused a fresh webProgress object to be created which does not have any
+ // listeners added. So, we get the listener that gBrowser is using for the tab and
+ // reattach it here.
+ let tab = gBrowser.getTabForBrowser(outer);
+ let filteredProgressListener = gBrowser._tabFilters.get(tab);
+ outer.webProgress.addProgressListener(filteredProgressListener);
+
+ // Add the inner browser to tabbrowser's WeakMap from browser to tab. This assists
+ // with tabbrowser's processing of some events such as MozLayerTreeReady which
+ // bubble up from the remote content frame and trigger tabbrowser to lookup the tab
+ // associated with the browser that triggered the event.
+ gBrowser._tabForBrowser.set(inner, tab);
+
+ // All of the browser state from content was swapped onto the inner browser. Pull
+ // this state up to the outer browser.
+ for (let property of SWAPPED_BROWSER_STATE) {
+ outer[property] = inner[property];
+ }
+
+ // Expose `PopupNotifications` on the content's owner global.
+ // This is used by PermissionUI.jsm for permission doorhangers.
+ // Note: This pollutes the responsive.html tool UI's global.
+ Object.defineProperty(inner.ownerGlobal, "PopupNotifications", {
+ get() {
+ return outer.ownerGlobal.PopupNotifications;
+ },
+ configurable: true,
+ enumerable: true,
+ });
+
+ // Expose `whereToOpenLink` on the content's owner global.
+ // This is used by ContentClick.jsm when opening links in ways other than just
+ // navigating the viewport.
+ // Note: This pollutes the responsive.html tool UI's global.
+ Object.defineProperty(inner.ownerGlobal, "whereToOpenLink", {
+ get() {
+ return outer.ownerGlobal.whereToOpenLink;
+ },
+ configurable: true,
+ enumerable: true,
+ });
+
+ // Add mozbrowser event handlers
+ inner.addEventListener("mozbrowseropenwindow", this);
+ }),
+
+ handleEvent(event) {
+ if (event.type != "mozbrowseropenwindow") {
+ return;
+ }
+
+ // Minimal support for <a target/> and window.open() which just ensures we at
+ // least open them somewhere (in a new tab). The following things are ignored:
+ // * Specific target names (everything treated as _blank)
+ // * Window features
+ // * window.opener
+ // These things are deferred for now, since content which does depend on them seems
+ // outside the main focus of RDM.
+ let { detail } = event;
+ event.preventDefault();
+ let uri = Services.io.newURI(detail.url, null, null);
+ // This API is used mainly because it's near the path used for <a target/> with
+ // regular browser tabs (which calls `openURIInFrame`). The more elaborate APIs
+ // that support openers, window features, etc. didn't seem callable from JS and / or
+ // this event doesn't give enough info to use them.
+ browserWindow.browserDOMWindow
+ .openURI(uri, null, Ci.nsIBrowserDOMWindow.OPEN_NEWTAB,
+ Ci.nsIBrowserDOMWindow.OPEN_NEW);
+ },
+
+ stop() {
+ let tab = gBrowser.getTabForBrowser(outer);
+ let filteredProgressListener = gBrowser._tabFilters.get(tab);
+
+ // The browser's state has changed over time while the tunnel was active. Push the
+ // the current state down to the inner browser, so that it follows the content in
+ // case that browser will be swapped elsewhere.
+ for (let property of SWAPPED_BROWSER_STATE) {
+ inner[property] = outer[property];
+ }
+
+ // Remove the inner browser from the WeakMap from browser to tab.
+ gBrowser._tabForBrowser.delete(inner);
+
+ // Remove the progress listener we added manually.
+ outer.webProgress.removeProgressListener(filteredProgressListener);
+
+ // Reset the XBL binding back to the default.
+ outer.destroy();
+ outer.style.MozBinding = "";
+
+ // Reset @remote since this is now back to a regular, non-remote browser
+ outer.setAttribute("remote", "false");
+
+ // Delete browser window properties exposed on content's owner global
+ delete inner.ownerGlobal.PopupNotifications;
+ delete inner.ownerGlobal.whereToOpenLink;
+
+ // Remove mozbrowser event handlers
+ inner.removeEventListener("mozbrowseropenwindow", this);
+
+ mmTunnel.destroy();
+ mmTunnel = null;
+
+ // Reset overridden XBL properties and methods. Deleting the override
+ // means it will fallback to the original XBL binding definitions which
+ // are on the prototype.
+ delete outer.frameLoader;
+ delete outer[FRAME_LOADER];
+ delete outer.outerWindowID;
+
+ // Invalidate outer's permanentKey so that SessionStore stops associating
+ // things that happen to the outer browser with the content inside in the
+ // inner browser.
+ outer.permanentKey = { id: "zombie" };
+
+ browserWindow = null;
+ gBrowser = null;
+ },
+
+ };
+}
+
+exports.tunnelToInnerBrowser = tunnelToInnerBrowser;
+
+/**
+ * This module allows specific messages of interest to be directed from the
+ * outer browser to the inner browser (and vice versa) in a targetted fashion
+ * without having to touch the original code paths that use them.
+ */
+function MessageManagerTunnel(outer, inner) {
+ if (outer.isRemoteBrowser) {
+ throw new Error("The outer browser must be non-remote.");
+ }
+ this.outer = outer;
+ this.inner = inner;
+ this.tunneledMessageNames = new Set();
+ this.init();
+}
+
+MessageManagerTunnel.prototype = {
+
+ /**
+ * Most message manager methods are left alone and are just passed along to
+ * the outer browser's real message manager.
+ */
+ PASS_THROUGH_METHODS: [
+ "killChild",
+ "assertPermission",
+ "assertContainApp",
+ "assertAppHasPermission",
+ "assertAppHasStatus",
+ "removeDelayedFrameScript",
+ "getDelayedFrameScripts",
+ "loadProcessScript",
+ "removeDelayedProcessScript",
+ "getDelayedProcessScripts",
+ "addWeakMessageListener",
+ "removeWeakMessageListener",
+ ],
+
+ /**
+ * The following methods are overridden with special behavior while tunneling.
+ */
+ OVERRIDDEN_METHODS: [
+ "loadFrameScript",
+ "addMessageListener",
+ "removeMessageListener",
+ "sendAsyncMessage",
+ ],
+
+ OUTER_TO_INNER_MESSAGES: [
+ // Messages sent from remote-browser.xml
+ "Browser:PurgeSessionHistory",
+ "InPermitUnload",
+ "PermitUnload",
+ // Messages sent from browser.js
+ "Browser:Reload",
+ // Messages sent from SelectParentHelper.jsm
+ "Forms:DismissedDropDown",
+ "Forms:MouseOut",
+ "Forms:MouseOver",
+ "Forms:SelectDropDownItem",
+ // Messages sent from SessionStore.jsm
+ "SessionStore:flush",
+ ],
+
+ INNER_TO_OUTER_MESSAGES: [
+ // Messages sent to RemoteWebProgress.jsm
+ "Content:LoadURIResult",
+ "Content:LocationChange",
+ "Content:ProgressChange",
+ "Content:SecurityChange",
+ "Content:StateChange",
+ "Content:StatusChange",
+ // Messages sent to remote-browser.xml
+ "DOMTitleChanged",
+ "ImageDocumentLoaded",
+ "Forms:ShowDropDown",
+ "Forms:HideDropDown",
+ "InPermitUnload",
+ "PermitUnload",
+ // Messages sent to tabbrowser.xml
+ "contextmenu",
+ // Messages sent to SelectParentHelper.jsm
+ "Forms:UpdateDropDown",
+ // Messages sent to browser.js
+ "PageVisibility:Hide",
+ "PageVisibility:Show",
+ // Messages sent to SessionStore.jsm
+ "SessionStore:update",
+ // Messages sent to BrowserTestUtils.jsm
+ "browser-test-utils:loadEvent",
+ ],
+
+ OUTER_TO_INNER_MESSAGE_PREFIXES: [
+ // Messages sent from nsContextMenu.js
+ "ContextMenu:",
+ // Messages sent from DevTools
+ "debug:",
+ // Messages sent from findbar.xml
+ "Findbar:",
+ // Messages sent from RemoteFinder.jsm
+ "Finder:",
+ // Messages sent from InlineSpellChecker.jsm
+ "InlineSpellChecker:",
+ // Messages sent from pageinfo.js
+ "PageInfo:",
+ // Messages sent from printUtils.js
+ "Printing:",
+ // Messages sent from browser-social.js
+ "Social:",
+ "PageMetadata:",
+ // Messages sent from viewSourceUtils.js
+ "ViewSource:",
+ ],
+
+ INNER_TO_OUTER_MESSAGE_PREFIXES: [
+ // Messages sent to nsContextMenu.js
+ "ContextMenu:",
+ // Messages sent to DevTools
+ "debug:",
+ // Messages sent to findbar.xml
+ "Findbar:",
+ // Messages sent to RemoteFinder.jsm
+ "Finder:",
+ // Messages sent to pageinfo.js
+ "PageInfo:",
+ // Messages sent to printUtils.js
+ "Printing:",
+ // Messages sent to browser-social.js
+ "Social:",
+ "PageMetadata:",
+ // Messages sent to viewSourceUtils.js
+ "ViewSource:",
+ ],
+
+ OUTER_TO_INNER_FRAME_SCRIPTS: [
+ // DevTools server for OOP frames
+ "resource://devtools/server/child.js"
+ ],
+
+ get outerParentMM() {
+ if (!this.outer[FRAME_LOADER]) {
+ return null;
+ }
+ return this.outer[FRAME_LOADER].messageManager;
+ },
+
+ get outerChildMM() {
+ // This is only possible because we require the outer browser to be
+ // non-remote, so we're able to reach into its window and use the child
+ // side message manager there.
+ let docShell = this.outer[FRAME_LOADER].docShell;
+ return docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIContentFrameMessageManager);
+ },
+
+ get innerParentMM() {
+ if (!this.inner.frameLoader) {
+ return null;
+ }
+ return this.inner.frameLoader.messageManager;
+ },
+
+ init() {
+ for (let method of this.PASS_THROUGH_METHODS) {
+ // Workaround bug 449811 to ensure a fresh binding each time through the loop
+ let _method = method;
+ this[_method] = (...args) => {
+ if (!this.outerParentMM) {
+ return null;
+ }
+ return this.outerParentMM[_method](...args);
+ };
+ }
+
+ for (let name of this.INNER_TO_OUTER_MESSAGES) {
+ this.innerParentMM.addMessageListener(name, this);
+ this.tunneledMessageNames.add(name);
+ }
+
+ Services.obs.addObserver(this, "message-manager-close", false);
+
+ // Replace the outer browser's messageManager with this tunnel
+ Object.defineProperty(this.outer, "messageManager", {
+ value: this,
+ writable: false,
+ configurable: true,
+ enumerable: true,
+ });
+ },
+
+ destroy() {
+ if (this.destroyed) {
+ return;
+ }
+ this.destroyed = true;
+ debug("Destroy tunnel");
+
+ // Watch for the messageManager to close. In most cases, the caller will stop the
+ // tunnel gracefully before this, but when the browser window closes or application
+ // exits, we may not see the high-level close events.
+ Services.obs.removeObserver(this, "message-manager-close");
+
+ // Reset the messageManager. Deleting the override means it will fallback to the
+ // original XBL binding definitions which are on the prototype.
+ delete this.outer.messageManager;
+
+ for (let name of this.tunneledMessageNames) {
+ this.innerParentMM.removeMessageListener(name, this);
+ }
+
+ // Some objects may have cached this tunnel as the messageManager for a frame. To
+ // ensure it keeps working after tunnel close, rewrite the overidden methods as pass
+ // through methods.
+ for (let method of this.OVERRIDDEN_METHODS) {
+ // Workaround bug 449811 to ensure a fresh binding each time through the loop
+ let _method = method;
+ this[_method] = (...args) => {
+ if (!this.outerParentMM) {
+ return null;
+ }
+ return this.outerParentMM[_method](...args);
+ };
+ }
+ },
+
+ observe(subject, topic, data) {
+ if (topic != "message-manager-close") {
+ return;
+ }
+ if (subject == this.innerParentMM) {
+ debug("Inner messageManager has closed");
+ this.destroy();
+ }
+ if (subject == this.outerParentMM) {
+ debug("Outer messageManager has closed");
+ this.destroy();
+ }
+ },
+
+ loadFrameScript(url, ...args) {
+ debug(`Calling loadFrameScript for ${url}`);
+
+ if (!this.OUTER_TO_INNER_FRAME_SCRIPTS.includes(url)) {
+ debug(`Should load ${url} into inner?`);
+ this.outerParentMM.loadFrameScript(url, ...args);
+ return;
+ }
+
+ debug(`Load ${url} into inner`);
+ this.innerParentMM.loadFrameScript(url, ...args);
+ },
+
+ addMessageListener(name, ...args) {
+ debug(`Calling addMessageListener for ${name}`);
+
+ debug(`Add outer listener for ${name}`);
+ // Add an outer listener, just like a simple pass through
+ this.outerParentMM.addMessageListener(name, ...args);
+
+ // If the message name is part of a prefix we're tunneling, we also need to add the
+ // tunnel as an inner listener.
+ if (this.INNER_TO_OUTER_MESSAGE_PREFIXES.some(prefix => name.startsWith(prefix))) {
+ debug(`Add inner listener for ${name}`);
+ this.innerParentMM.addMessageListener(name, this);
+ this.tunneledMessageNames.add(name);
+ }
+ },
+
+ removeMessageListener(name, ...args) {
+ debug(`Calling removeMessageListener for ${name}`);
+
+ debug(`Remove outer listener for ${name}`);
+ // Remove an outer listener, just like a simple pass through
+ this.outerParentMM.removeMessageListener(name, ...args);
+
+ // Leave the tunnel as an inner listener for the case of prefix messages to avoid
+ // tracking counts of add calls. The inner listener will get removed on destroy.
+ },
+
+ sendAsyncMessage(name, ...args) {
+ debug(`Calling sendAsyncMessage for ${name}`);
+
+ if (!this._shouldTunnelOuterToInner(name)) {
+ debug(`Should ${name} go to inner?`);
+ this.outerParentMM.sendAsyncMessage(name, ...args);
+ return;
+ }
+
+ debug(`${name} outer -> inner`);
+ this.innerParentMM.sendAsyncMessage(name, ...args);
+ },
+
+ receiveMessage({ name, data, objects, principal, sync }) {
+ if (!this._shouldTunnelInnerToOuter(name)) {
+ debug(`Received unexpected message ${name}`);
+ return undefined;
+ }
+
+ debug(`${name} inner -> outer, sync: ${sync}`);
+ if (sync) {
+ return this.outerChildMM.sendSyncMessage(name, data, objects, principal);
+ }
+ this.outerChildMM.sendAsyncMessage(name, data, objects, principal);
+ return undefined;
+ },
+
+ _shouldTunnelOuterToInner(name) {
+ return this.OUTER_TO_INNER_MESSAGES.includes(name) ||
+ this.OUTER_TO_INNER_MESSAGE_PREFIXES.some(prefix => name.startsWith(prefix));
+ },
+
+ _shouldTunnelInnerToOuter(name) {
+ return this.INNER_TO_OUTER_MESSAGES.includes(name) ||
+ this.INNER_TO_OUTER_MESSAGE_PREFIXES.some(prefix => name.startsWith(prefix));
+ },
+
+};
diff --git a/devtools/client/responsive.html/browser/web-navigation.js b/devtools/client/responsive.html/browser/web-navigation.js
new file mode 100644
index 000000000..4519df0bd
--- /dev/null
+++ b/devtools/client/responsive.html/browser/web-navigation.js
@@ -0,0 +1,179 @@
+/* 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, Cu, Cr } = require("chrome");
+const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
+const Services = require("Services");
+const { NetUtil } = require("resource://gre/modules/NetUtil.jsm");
+
+function readInputStreamToString(stream) {
+ return NetUtil.readInputStreamToString(stream, stream.available());
+}
+
+/**
+ * This object aims to provide the nsIWebNavigation interface for mozbrowser elements.
+ * nsIWebNavigation is one of the interfaces expected on <xul:browser>s, so this wrapper
+ * helps mozbrowser elements support this.
+ *
+ * It attempts to use the mozbrowser API wherever possible, however some methods don't
+ * exist yet, so we fallback to the WebNavigation frame script messages in those cases.
+ * Ideally the mozbrowser API would eventually be extended to cover all properties and
+ * methods used here.
+ *
+ * This is largely copied from RemoteWebNavigation.js, which uses the message manager to
+ * perform all actions.
+ */
+function BrowserElementWebNavigation(browser) {
+ this._browser = browser;
+}
+
+BrowserElementWebNavigation.prototype = {
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIWebNavigation,
+ Ci.nsISupports
+ ]),
+
+ get _mm() {
+ return this._browser.frameLoader.messageManager;
+ },
+
+ canGoBack: false,
+ canGoForward: false,
+
+ goBack() {
+ this._browser.goBack();
+ },
+
+ goForward() {
+ this._browser.goForward();
+ },
+
+ gotoIndex(index) {
+ // No equivalent in the current BrowserElement API
+ this._sendMessage("WebNavigation:GotoIndex", { index });
+ },
+
+ loadURI(uri, flags, referrer, postData, headers) {
+ // No equivalent in the current BrowserElement API
+ this.loadURIWithOptions(uri, flags, referrer,
+ Ci.nsIHttpChannel.REFERRER_POLICY_DEFAULT,
+ postData, headers, null);
+ },
+
+ loadURIWithOptions(uri, flags, referrer, referrerPolicy, postData, headers,
+ baseURI) {
+ // No equivalent in the current BrowserElement API
+ this._sendMessage("WebNavigation:LoadURI", {
+ uri,
+ flags,
+ referrer: referrer ? referrer.spec : null,
+ referrerPolicy: referrerPolicy,
+ postData: postData ? readInputStreamToString(postData) : null,
+ headers: headers ? readInputStreamToString(headers) : null,
+ baseURI: baseURI ? baseURI.spec : null,
+ });
+ },
+
+ setOriginAttributesBeforeLoading(originAttributes) {
+ // No equivalent in the current BrowserElement API
+ this._sendMessage("WebNavigation:SetOriginAttributes", {
+ originAttributes,
+ });
+ },
+
+ reload(flags) {
+ let hardReload = false;
+ if (flags & this.LOAD_FLAGS_BYPASS_PROXY ||
+ flags & this.LOAD_FLAGS_BYPASS_CACHE) {
+ hardReload = true;
+ }
+ this._browser.reload(hardReload);
+ },
+
+ stop(flags) {
+ // No equivalent in the current BrowserElement API
+ this._sendMessage("WebNavigation:Stop", { flags });
+ },
+
+ get document() {
+ return this._browser.contentDocument;
+ },
+
+ _currentURI: null,
+ get currentURI() {
+ if (!this._currentURI) {
+ this._currentURI = Services.io.newURI("about:blank", null, null);
+ }
+ return this._currentURI;
+ },
+ set currentURI(uri) {
+ this._browser.src = uri.spec;
+ },
+
+ referringURI: null,
+
+ // Bug 1233803 - accessing the sessionHistory of remote browsers should be
+ // done in content scripts.
+ get sessionHistory() {
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+ set sessionHistory(value) {
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ _sendMessage(message, data) {
+ try {
+ this._mm.sendAsyncMessage(message, data);
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ },
+
+ swapBrowser(browser) {
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ copyStateFrom(otherWebNavigation) {
+ const state = [
+ "canGoBack",
+ "canGoForward",
+ "_currentURI",
+ ];
+ for (let property of state) {
+ this[property] = otherWebNavigation[property];
+ }
+ },
+
+};
+
+const FLAGS = [
+ "LOAD_FLAGS_MASK",
+ "LOAD_FLAGS_NONE",
+ "LOAD_FLAGS_IS_REFRESH",
+ "LOAD_FLAGS_IS_LINK",
+ "LOAD_FLAGS_BYPASS_HISTORY",
+ "LOAD_FLAGS_REPLACE_HISTORY",
+ "LOAD_FLAGS_BYPASS_CACHE",
+ "LOAD_FLAGS_BYPASS_PROXY",
+ "LOAD_FLAGS_CHARSET_CHANGE",
+ "LOAD_FLAGS_STOP_CONTENT",
+ "LOAD_FLAGS_FROM_EXTERNAL",
+ "LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP",
+ "LOAD_FLAGS_FIRST_LOAD",
+ "LOAD_FLAGS_ALLOW_POPUPS",
+ "LOAD_FLAGS_BYPASS_CLASSIFIER",
+ "LOAD_FLAGS_FORCE_ALLOW_COOKIES",
+ "STOP_NETWORK",
+ "STOP_CONTENT",
+ "STOP_ALL",
+];
+
+for (let flag of FLAGS) {
+ BrowserElementWebNavigation.prototype[flag] = Ci.nsIWebNavigation[flag];
+}
+
+exports.BrowserElementWebNavigation = BrowserElementWebNavigation;
diff --git a/devtools/client/responsive.html/components/browser.js b/devtools/client/responsive.html/components/browser.js
new file mode 100644
index 000000000..f2902905b
--- /dev/null
+++ b/devtools/client/responsive.html/components/browser.js
@@ -0,0 +1,149 @@
+/* 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/. */
+
+/* eslint-env browser */
+
+"use strict";
+
+const { Task } = require("devtools/shared/task");
+const flags = require("devtools/shared/flags");
+const { getToplevelWindow } = require("sdk/window/utils");
+const { DOM: dom, createClass, addons, PropTypes } =
+ require("devtools/client/shared/vendor/react");
+
+const Types = require("../types");
+const e10s = require("../utils/e10s");
+const message = require("../utils/message");
+
+module.exports = createClass({
+
+ /**
+ * This component is not allowed to depend directly on frequently changing
+ * data (width, height) due to the use of `dangerouslySetInnerHTML` below.
+ * Any changes in props will cause the <iframe> to be removed and added again,
+ * throwing away the current state of the page.
+ */
+ displayName: "Browser",
+
+ propTypes: {
+ location: Types.location.isRequired,
+ swapAfterMount: PropTypes.bool.isRequired,
+ onBrowserMounted: PropTypes.func.isRequired,
+ onContentResize: PropTypes.func.isRequired,
+ },
+
+ mixins: [ addons.PureRenderMixin ],
+
+ /**
+ * Once the browser element has mounted, load the frame script and enable
+ * various features, like floating scrollbars.
+ */
+ componentDidMount: Task.async(function* () {
+ // If we are not swapping browsers after mount, it's safe to start the frame
+ // script now.
+ if (!this.props.swapAfterMount) {
+ yield this.startFrameScript();
+ }
+
+ // Notify manager.js that this browser has mounted, so that it can trigger
+ // a swap if needed and continue with the rest of its startup.
+ this.props.onBrowserMounted();
+
+ // If we are swapping browsers after mount, wait for the swap to complete
+ // and start the frame script after that.
+ if (this.props.swapAfterMount) {
+ yield message.wait(window, "start-frame-script");
+ yield this.startFrameScript();
+ message.post(window, "start-frame-script:done");
+ }
+
+ // Stop the frame script when requested in the future.
+ message.wait(window, "stop-frame-script").then(() => {
+ this.stopFrameScript();
+ });
+ }),
+
+ onContentResize(msg) {
+ let { onContentResize } = this.props;
+ let { width, height } = msg.data;
+ onContentResize({
+ width,
+ height,
+ });
+ },
+
+ startFrameScript: Task.async(function* () {
+ let { onContentResize } = this;
+ let browser = this.refs.browserContainer.querySelector("iframe.browser");
+ let mm = browser.frameLoader.messageManager;
+
+ // Notify tests when the content has received a resize event. This is not
+ // quite the same timing as when we _set_ a new size around the browser,
+ // since it still needs to do async work before the content is actually
+ // resized to match.
+ e10s.on(mm, "OnContentResize", onContentResize);
+
+ let ready = e10s.once(mm, "ChildScriptReady");
+ mm.loadFrameScript("resource://devtools/client/responsivedesign/" +
+ "responsivedesign-child.js", true);
+ yield ready;
+
+ let browserWindow = getToplevelWindow(window);
+ let requiresFloatingScrollbars =
+ !browserWindow.matchMedia("(-moz-overlay-scrollbars)").matches;
+
+ yield e10s.request(mm, "Start", {
+ requiresFloatingScrollbars,
+ // Tests expect events on resize to yield on various size changes
+ notifyOnResize: flags.testing,
+ });
+ }),
+
+ stopFrameScript: Task.async(function* () {
+ let { onContentResize } = this;
+
+ let browser = this.refs.browserContainer.querySelector("iframe.browser");
+ let mm = browser.frameLoader.messageManager;
+ e10s.off(mm, "OnContentResize", onContentResize);
+ yield e10s.request(mm, "Stop");
+ message.post(window, "stop-frame-script:done");
+ }),
+
+ render() {
+ let {
+ location,
+ } = this.props;
+
+ // innerHTML expects & to be an HTML entity
+ location = location.replace(/&/g, "&amp;");
+
+ return dom.div(
+ {
+ ref: "browserContainer",
+ className: "browser-container",
+
+ /**
+ * React uses a whitelist for attributes, so we need some way to set
+ * attributes it does not know about, such as @mozbrowser. If this were
+ * the only issue, we could use componentDidMount or ref: node => {} to
+ * set the atttibutes. In the case of @remote, the attribute must be set
+ * before the element is added to the DOM to have any effect, which we
+ * are able to do with this approach.
+ *
+ * @noisolation and @allowfullscreen are needed so that these frames
+ * have the same access to browser features as regular browser tabs.
+ * The `swapFrameLoaders` platform API we use compares such features
+ * before allowing the swap to proceed.
+ */
+ dangerouslySetInnerHTML: {
+ __html: `<iframe class="browser" mozbrowser="true" remote="true"
+ noisolation="true" allowfullscreen="true"
+ src="${location}" width="100%" height="100%">
+ </iframe>`
+ }
+ }
+ );
+ },
+
+});
diff --git a/devtools/client/responsive.html/components/device-modal.js b/devtools/client/responsive.html/components/device-modal.js
new file mode 100644
index 000000000..d28b97472
--- /dev/null
+++ b/devtools/client/responsive.html/components/device-modal.js
@@ -0,0 +1,181 @@
+/* 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/. */
+
+/* eslint-env browser */
+
+"use strict";
+
+const { DOM: dom, createClass, PropTypes, addons } =
+ require("devtools/client/shared/vendor/react");
+const { getStr } = require("../utils/l10n");
+const Types = require("../types");
+
+module.exports = createClass({
+ displayName: "DeviceModal",
+
+ propTypes: {
+ devices: PropTypes.shape(Types.devices).isRequired,
+ onDeviceListUpdate: PropTypes.func.isRequired,
+ onUpdateDeviceDisplayed: PropTypes.func.isRequired,
+ onUpdateDeviceModalOpen: PropTypes.func.isRequired,
+ },
+
+ mixins: [ addons.PureRenderMixin ],
+
+ getInitialState() {
+ return {};
+ },
+
+ componentDidMount() {
+ window.addEventListener("keydown", this.onKeyDown, true);
+ },
+
+ componentWillReceiveProps(nextProps) {
+ let {
+ devices,
+ } = nextProps;
+
+ for (let type of devices.types) {
+ for (let device of devices[type]) {
+ this.setState({
+ [device.name]: device.displayed,
+ });
+ }
+ }
+ },
+
+ componentWillUnmount() {
+ window.removeEventListener("keydown", this.onKeyDown, true);
+ },
+
+ onDeviceCheckboxClick({ target }) {
+ this.setState({
+ [target.value]: !this.state[target.value]
+ });
+ },
+
+ onDeviceModalSubmit() {
+ let {
+ devices,
+ onDeviceListUpdate,
+ onUpdateDeviceDisplayed,
+ onUpdateDeviceModalOpen,
+ } = this.props;
+
+ let preferredDevices = {
+ "added": new Set(),
+ "removed": new Set(),
+ };
+
+ for (let type of devices.types) {
+ for (let device of devices[type]) {
+ let newState = this.state[device.name];
+
+ if (device.featured && !newState) {
+ preferredDevices.removed.add(device.name);
+ } else if (!device.featured && newState) {
+ preferredDevices.added.add(device.name);
+ }
+
+ if (this.state[device.name] != device.displayed) {
+ onUpdateDeviceDisplayed(device, type, this.state[device.name]);
+ }
+ }
+ }
+
+ onDeviceListUpdate(preferredDevices);
+ onUpdateDeviceModalOpen(false);
+ },
+
+ onKeyDown(event) {
+ if (!this.props.devices.isModalOpen) {
+ return;
+ }
+ // Escape keycode
+ if (event.keyCode === 27) {
+ let {
+ onUpdateDeviceModalOpen
+ } = this.props;
+ onUpdateDeviceModalOpen(false);
+ }
+ },
+
+ render() {
+ let {
+ devices,
+ onUpdateDeviceModalOpen,
+ } = this.props;
+
+ const sortedDevices = {};
+ for (let type of devices.types) {
+ sortedDevices[type] = Object.assign([], devices[type])
+ .sort((a, b) => a.name.localeCompare(b.name));
+ }
+
+ return dom.div(
+ {
+ id: "device-modal-wrapper",
+ className: this.props.devices.isModalOpen ? "opened" : "closed",
+ },
+ dom.div(
+ {
+ className: "device-modal container",
+ },
+ dom.button({
+ id: "device-close-button",
+ className: "toolbar-button devtools-button",
+ onClick: () => onUpdateDeviceModalOpen(false),
+ }),
+ dom.div(
+ {
+ className: "device-modal-content",
+ },
+ devices.types.map(type => {
+ return dom.div(
+ {
+ className: "device-type",
+ key: type,
+ },
+ dom.header(
+ {
+ className: "device-header",
+ },
+ type
+ ),
+ sortedDevices[type].map(device => {
+ return dom.label(
+ {
+ className: "device-label",
+ key: device.name,
+ },
+ dom.input({
+ className: "device-input-checkbox",
+ type: "checkbox",
+ value: device.name,
+ checked: this.state[device.name],
+ onChange: this.onDeviceCheckboxClick,
+ }),
+ device.name
+ );
+ })
+ );
+ })
+ ),
+ dom.button(
+ {
+ id: "device-submit-button",
+ onClick: this.onDeviceModalSubmit,
+ },
+ getStr("responsive.done")
+ )
+ ),
+ dom.div(
+ {
+ className: "modal-overlay",
+ onClick: () => onUpdateDeviceModalOpen(false),
+ }
+ )
+ );
+ },
+});
diff --git a/devtools/client/responsive.html/components/device-selector.js b/devtools/client/responsive.html/components/device-selector.js
new file mode 100644
index 000000000..3215ce5fb
--- /dev/null
+++ b/devtools/client/responsive.html/components/device-selector.js
@@ -0,0 +1,122 @@
+/* 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 { getStr } = require("../utils/l10n");
+const { DOM: dom, createClass, PropTypes, addons } =
+ require("devtools/client/shared/vendor/react");
+
+const Types = require("../types");
+const OPEN_DEVICE_MODAL_VALUE = "OPEN_DEVICE_MODAL";
+
+module.exports = createClass({
+ displayName: "DeviceSelector",
+
+ propTypes: {
+ devices: PropTypes.shape(Types.devices).isRequired,
+ selectedDevice: PropTypes.string.isRequired,
+ onChangeDevice: PropTypes.func.isRequired,
+ onResizeViewport: PropTypes.func.isRequired,
+ onUpdateDeviceModalOpen: PropTypes.func.isRequired,
+ },
+
+ mixins: [ addons.PureRenderMixin ],
+
+ onSelectChange({ target }) {
+ let {
+ devices,
+ onChangeDevice,
+ onResizeViewport,
+ onUpdateDeviceModalOpen,
+ } = this.props;
+
+ if (target.value === OPEN_DEVICE_MODAL_VALUE) {
+ onUpdateDeviceModalOpen(true);
+ return;
+ }
+ for (let type of devices.types) {
+ for (let device of devices[type]) {
+ if (device.name === target.value) {
+ onResizeViewport(device.width, device.height);
+ onChangeDevice(device);
+ return;
+ }
+ }
+ }
+ },
+
+ render() {
+ let {
+ devices,
+ selectedDevice,
+ } = this.props;
+
+ let options = [];
+ for (let type of devices.types) {
+ for (let device of devices[type]) {
+ if (device.displayed) {
+ options.push(device);
+ }
+ }
+ }
+
+ options.sort(function (a, b) {
+ return a.name.localeCompare(b.name);
+ });
+
+ let selectClass = "viewport-device-selector";
+ if (selectedDevice) {
+ selectClass += " selected";
+ }
+
+ let state = devices.listState;
+ let listContent;
+
+ if (state == Types.deviceListState.LOADED) {
+ listContent = [dom.option({
+ value: "",
+ title: "",
+ disabled: true,
+ hidden: true,
+ }, getStr("responsive.noDeviceSelected")),
+ options.map(device => {
+ return dom.option({
+ key: device.name,
+ value: device.name,
+ title: "",
+ }, device.name);
+ }),
+ dom.option({
+ value: OPEN_DEVICE_MODAL_VALUE,
+ title: "",
+ }, getStr("responsive.editDeviceList"))];
+ } else if (state == Types.deviceListState.LOADING
+ || state == Types.deviceListState.INITIALIZED) {
+ listContent = [dom.option({
+ value: "",
+ title: "",
+ disabled: true,
+ }, getStr("responsive.deviceListLoading"))];
+ } else if (state == Types.deviceListState.ERROR) {
+ listContent = [dom.option({
+ value: "",
+ title: "",
+ disabled: true,
+ }, getStr("responsive.deviceListError"))];
+ }
+
+ return dom.select(
+ {
+ className: selectClass,
+ value: selectedDevice,
+ title: selectedDevice,
+ onChange: this.onSelectChange,
+ disabled: (state !== Types.deviceListState.LOADED),
+ },
+ ...listContent
+ );
+ },
+
+});
diff --git a/devtools/client/responsive.html/components/dpr-selector.js b/devtools/client/responsive.html/components/dpr-selector.js
new file mode 100644
index 000000000..31b8db1c2
--- /dev/null
+++ b/devtools/client/responsive.html/components/dpr-selector.js
@@ -0,0 +1,131 @@
+/* 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/. */
+
+/* eslint-env browser */
+
+"use strict";
+
+const { DOM: dom, createClass, PropTypes, addons } =
+ require("devtools/client/shared/vendor/react");
+
+const Types = require("../types");
+const { getStr, getFormatStr } = require("../utils/l10n");
+
+const PIXEL_RATIO_PRESET = [1, 2, 3];
+
+const createVisibleOption = value =>
+ dom.option({
+ value,
+ title: value,
+ key: value,
+ }, value);
+
+const createHiddenOption = value =>
+ dom.option({
+ value,
+ title: value,
+ hidden: true,
+ disabled: true,
+ }, value);
+
+module.exports = createClass({
+ displayName: "DPRSelector",
+
+ propTypes: {
+ devices: PropTypes.shape(Types.devices).isRequired,
+ displayPixelRatio: Types.pixelRatio.value.isRequired,
+ selectedDevice: PropTypes.string.isRequired,
+ selectedPixelRatio: PropTypes.shape(Types.pixelRatio).isRequired,
+ onChangePixelRatio: PropTypes.func.isRequired,
+ },
+
+ mixins: [ addons.PureRenderMixin ],
+
+ getInitialState() {
+ return {
+ isFocused: false
+ };
+ },
+
+ onFocusChange({type}) {
+ this.setState({
+ isFocused: type === "focus"
+ });
+ },
+
+ onSelectChange({ target }) {
+ this.props.onChangePixelRatio(+target.value);
+ },
+
+ render() {
+ let {
+ devices,
+ displayPixelRatio,
+ selectedDevice,
+ selectedPixelRatio,
+ } = this.props;
+
+ let hiddenOptions = [];
+
+ for (let type of devices.types) {
+ for (let device of devices[type]) {
+ if (device.displayed &&
+ !hiddenOptions.includes(device.pixelRatio) &&
+ !PIXEL_RATIO_PRESET.includes(device.pixelRatio)) {
+ hiddenOptions.push(device.pixelRatio);
+ }
+ }
+ }
+
+ if (!PIXEL_RATIO_PRESET.includes(displayPixelRatio)) {
+ hiddenOptions.push(displayPixelRatio);
+ }
+
+ let state = devices.listState;
+ let isDisabled = (state !== Types.deviceListState.LOADED) || (selectedDevice !== "");
+ let selectorClass = "";
+ let title;
+
+ if (isDisabled) {
+ selectorClass += " disabled";
+ title = getFormatStr("responsive.autoDPR", selectedDevice);
+ } else {
+ title = getStr("responsive.devicePixelRatio");
+
+ if (selectedPixelRatio.value) {
+ selectorClass += " selected";
+ }
+ }
+
+ if (this.state.isFocused) {
+ selectorClass += " focused";
+ }
+
+ let listContent = PIXEL_RATIO_PRESET.map(createVisibleOption);
+
+ if (state == Types.deviceListState.LOADED) {
+ listContent = listContent.concat(hiddenOptions.map(createHiddenOption));
+ }
+
+ return dom.label(
+ {
+ id: "global-dpr-selector",
+ className: selectorClass,
+ title,
+ },
+ "DPR",
+ dom.select(
+ {
+ value: selectedPixelRatio.value || displayPixelRatio,
+ disabled: isDisabled,
+ onChange: this.onSelectChange,
+ onFocus: this.onFocusChange,
+ onBlur: this.onFocusChange,
+ },
+ ...listContent
+ )
+ );
+ },
+
+});
diff --git a/devtools/client/responsive.html/components/global-toolbar.js b/devtools/client/responsive.html/components/global-toolbar.js
new file mode 100644
index 000000000..6c31fa338
--- /dev/null
+++ b/devtools/client/responsive.html/components/global-toolbar.js
@@ -0,0 +1,101 @@
+/* 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 { DOM: dom, createClass, createFactory, PropTypes, addons } =
+ require("devtools/client/shared/vendor/react");
+
+const { getStr } = require("../utils/l10n");
+const Types = require("../types");
+const DPRSelector = createFactory(require("./dpr-selector"));
+const NetworkThrottlingSelector = createFactory(require("./network-throttling-selector"));
+
+module.exports = createClass({
+ displayName: "GlobalToolbar",
+
+ propTypes: {
+ devices: PropTypes.shape(Types.devices).isRequired,
+ displayPixelRatio: Types.pixelRatio.value.isRequired,
+ networkThrottling: PropTypes.shape(Types.networkThrottling).isRequired,
+ screenshot: PropTypes.shape(Types.screenshot).isRequired,
+ selectedDevice: PropTypes.string.isRequired,
+ selectedPixelRatio: PropTypes.shape(Types.pixelRatio).isRequired,
+ touchSimulation: PropTypes.shape(Types.touchSimulation).isRequired,
+ onChangeNetworkThrottling: PropTypes.func.isRequired,
+ onChangePixelRatio: PropTypes.func.isRequired,
+ onChangeTouchSimulation: PropTypes.func.isRequired,
+ onExit: PropTypes.func.isRequired,
+ onScreenshot: PropTypes.func.isRequired,
+ },
+
+ mixins: [ addons.PureRenderMixin ],
+
+ render() {
+ let {
+ devices,
+ displayPixelRatio,
+ networkThrottling,
+ screenshot,
+ selectedDevice,
+ selectedPixelRatio,
+ touchSimulation,
+ onChangeNetworkThrottling,
+ onChangePixelRatio,
+ onChangeTouchSimulation,
+ onExit,
+ onScreenshot,
+ } = this.props;
+
+ let touchButtonClass = "toolbar-button devtools-button";
+ if (touchSimulation.enabled) {
+ touchButtonClass += " active";
+ }
+
+ return dom.header(
+ {
+ id: "global-toolbar",
+ className: "container",
+ },
+ dom.span(
+ {
+ className: "title",
+ },
+ getStr("responsive.title")
+ ),
+ NetworkThrottlingSelector({
+ networkThrottling,
+ onChangeNetworkThrottling,
+ }),
+ DPRSelector({
+ devices,
+ displayPixelRatio,
+ selectedDevice,
+ selectedPixelRatio,
+ onChangePixelRatio,
+ }),
+ dom.button({
+ id: "global-touch-simulation-button",
+ className: touchButtonClass,
+ title: (touchSimulation.enabled ?
+ getStr("responsive.disableTouch") : getStr("responsive.enableTouch")),
+ onClick: () => onChangeTouchSimulation(!touchSimulation.enabled),
+ }),
+ dom.button({
+ id: "global-screenshot-button",
+ className: "toolbar-button devtools-button",
+ title: getStr("responsive.screenshot"),
+ onClick: onScreenshot,
+ disabled: screenshot.isCapturing,
+ }),
+ dom.button({
+ id: "global-exit-button",
+ className: "toolbar-button devtools-button",
+ title: getStr("responsive.exit"),
+ onClick: onExit,
+ })
+ );
+ },
+
+});
diff --git a/devtools/client/responsive.html/components/moz.build b/devtools/client/responsive.html/components/moz.build
new file mode 100644
index 000000000..4ad36f992
--- /dev/null
+++ b/devtools/client/responsive.html/components/moz.build
@@ -0,0 +1,19 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'browser.js',
+ 'device-modal.js',
+ 'device-selector.js',
+ 'dpr-selector.js',
+ 'global-toolbar.js',
+ 'network-throttling-selector.js',
+ 'resizable-viewport.js',
+ 'viewport-dimension.js',
+ 'viewport-toolbar.js',
+ 'viewport.js',
+ 'viewports.js',
+)
diff --git a/devtools/client/responsive.html/components/network-throttling-selector.js b/devtools/client/responsive.html/components/network-throttling-selector.js
new file mode 100644
index 000000000..fa9f5c6a0
--- /dev/null
+++ b/devtools/client/responsive.html/components/network-throttling-selector.js
@@ -0,0 +1,92 @@
+/* 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 { DOM: dom, createClass, PropTypes, addons } =
+ require("devtools/client/shared/vendor/react");
+
+const Types = require("../types");
+const { getStr } = require("../utils/l10n");
+const throttlingProfiles = require("devtools/client/shared/network-throttling-profiles");
+
+module.exports = createClass({
+
+ displayName: "NetworkThrottlingSelector",
+
+ propTypes: {
+ networkThrottling: PropTypes.shape(Types.networkThrottling).isRequired,
+ onChangeNetworkThrottling: PropTypes.func.isRequired,
+ },
+
+ mixins: [ addons.PureRenderMixin ],
+
+ onSelectChange({ target }) {
+ let {
+ onChangeNetworkThrottling,
+ } = this.props;
+
+ if (target.value == getStr("responsive.noThrottling")) {
+ onChangeNetworkThrottling(false, "");
+ return;
+ }
+
+ for (let profile of throttlingProfiles) {
+ if (profile.id === target.value) {
+ onChangeNetworkThrottling(true, profile.id);
+ return;
+ }
+ }
+ },
+
+ render() {
+ let {
+ networkThrottling,
+ } = this.props;
+
+ let selectClass = "";
+ let selectedProfile;
+ if (networkThrottling.enabled) {
+ selectClass += " selected";
+ selectedProfile = networkThrottling.profile;
+ } else {
+ selectedProfile = getStr("responsive.noThrottling");
+ }
+
+ let listContent = [
+ dom.option(
+ {
+ key: "disabled",
+ },
+ getStr("responsive.noThrottling")
+ ),
+ dom.option(
+ {
+ key: "divider",
+ className: "divider",
+ disabled: true,
+ }
+ ),
+ throttlingProfiles.map(profile => {
+ return dom.option(
+ {
+ key: profile.id,
+ },
+ profile.id
+ );
+ }),
+ ];
+
+ return dom.select(
+ {
+ id: "global-network-throttling-selector",
+ className: selectClass,
+ value: selectedProfile,
+ onChange: this.onSelectChange,
+ },
+ ...listContent
+ );
+ },
+
+});
diff --git a/devtools/client/responsive.html/components/resizable-viewport.js b/devtools/client/responsive.html/components/resizable-viewport.js
new file mode 100644
index 000000000..1d94cd052
--- /dev/null
+++ b/devtools/client/responsive.html/components/resizable-viewport.js
@@ -0,0 +1,195 @@
+/* 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/. */
+
+/* global window */
+
+"use strict";
+
+const { DOM: dom, createClass, createFactory, PropTypes } =
+ require("devtools/client/shared/vendor/react");
+
+const Constants = require("../constants");
+const Types = require("../types");
+const Browser = createFactory(require("./browser"));
+const ViewportToolbar = createFactory(require("./viewport-toolbar"));
+
+const VIEWPORT_MIN_WIDTH = Constants.MIN_VIEWPORT_DIMENSION;
+const VIEWPORT_MIN_HEIGHT = Constants.MIN_VIEWPORT_DIMENSION;
+
+module.exports = createClass({
+
+ displayName: "ResizableViewport",
+
+ propTypes: {
+ devices: PropTypes.shape(Types.devices).isRequired,
+ location: Types.location.isRequired,
+ screenshot: PropTypes.shape(Types.screenshot).isRequired,
+ swapAfterMount: PropTypes.bool.isRequired,
+ viewport: PropTypes.shape(Types.viewport).isRequired,
+ onBrowserMounted: PropTypes.func.isRequired,
+ onChangeDevice: PropTypes.func.isRequired,
+ onContentResize: PropTypes.func.isRequired,
+ onRemoveDevice: PropTypes.func.isRequired,
+ onResizeViewport: PropTypes.func.isRequired,
+ onRotateViewport: PropTypes.func.isRequired,
+ onUpdateDeviceModalOpen: PropTypes.func.isRequired,
+ },
+
+ getInitialState() {
+ return {
+ isResizing: false,
+ lastClientX: 0,
+ lastClientY: 0,
+ ignoreX: false,
+ ignoreY: false,
+ };
+ },
+
+ onResizeStart({ target, clientX, clientY }) {
+ window.addEventListener("mousemove", this.onResizeDrag, true);
+ window.addEventListener("mouseup", this.onResizeStop, true);
+
+ this.setState({
+ isResizing: true,
+ lastClientX: clientX,
+ lastClientY: clientY,
+ ignoreX: target === this.refs.resizeBarY,
+ ignoreY: target === this.refs.resizeBarX,
+ });
+ },
+
+ onResizeStop() {
+ window.removeEventListener("mousemove", this.onResizeDrag, true);
+ window.removeEventListener("mouseup", this.onResizeStop, true);
+
+ this.setState({
+ isResizing: false,
+ lastClientX: 0,
+ lastClientY: 0,
+ ignoreX: false,
+ ignoreY: false,
+ });
+ },
+
+ onResizeDrag({ clientX, clientY }) {
+ if (!this.state.isResizing) {
+ return;
+ }
+
+ let { lastClientX, lastClientY, ignoreX, ignoreY } = this.state;
+ // the viewport is centered horizontally, so horizontal resize resizes
+ // by twice the distance the mouse was dragged - on left and right side.
+ let deltaX = 2 * (clientX - lastClientX);
+ let deltaY = (clientY - lastClientY);
+
+ if (ignoreX) {
+ deltaX = 0;
+ }
+ if (ignoreY) {
+ deltaY = 0;
+ }
+
+ let width = this.props.viewport.width + deltaX;
+ let height = this.props.viewport.height + deltaY;
+
+ if (width < VIEWPORT_MIN_WIDTH) {
+ width = VIEWPORT_MIN_WIDTH;
+ } else {
+ lastClientX = clientX;
+ }
+
+ if (height < VIEWPORT_MIN_HEIGHT) {
+ height = VIEWPORT_MIN_HEIGHT;
+ } else {
+ lastClientY = clientY;
+ }
+
+ // Update the viewport store with the new width and height.
+ this.props.onResizeViewport(width, height);
+ // Change the device selector back to an unselected device
+ // TODO: Bug 1332754: Logic like this probably belongs in the action creator.
+ if (this.props.viewport.device) {
+ // In bug 1329843 and others, we may eventually stop this approach of removing the
+ // the properties of the device on resize. However, at the moment, there is no
+ // way to edit dPR when a device is selected, and there is no UI at all for editing
+ // UA, so it's important to keep doing this for now.
+ this.props.onRemoveDevice();
+ }
+
+ this.setState({
+ lastClientX,
+ lastClientY
+ });
+ },
+
+ render() {
+ let {
+ devices,
+ location,
+ screenshot,
+ swapAfterMount,
+ viewport,
+ onBrowserMounted,
+ onChangeDevice,
+ onContentResize,
+ onResizeViewport,
+ onRotateViewport,
+ onUpdateDeviceModalOpen,
+ } = this.props;
+
+ let resizeHandleClass = "viewport-resize-handle";
+ if (screenshot.isCapturing) {
+ resizeHandleClass += " hidden";
+ }
+
+ let contentClass = "viewport-content";
+ if (this.state.isResizing) {
+ contentClass += " resizing";
+ }
+
+ return dom.div(
+ {
+ className: "resizable-viewport",
+ },
+ ViewportToolbar({
+ devices,
+ selectedDevice: viewport.device,
+ onChangeDevice,
+ onResizeViewport,
+ onRotateViewport,
+ onUpdateDeviceModalOpen,
+ }),
+ dom.div(
+ {
+ className: contentClass,
+ style: {
+ width: viewport.width + "px",
+ height: viewport.height + "px",
+ },
+ },
+ Browser({
+ location,
+ swapAfterMount,
+ onBrowserMounted,
+ onContentResize,
+ })
+ ),
+ dom.div({
+ className: resizeHandleClass,
+ onMouseDown: this.onResizeStart,
+ }),
+ dom.div({
+ ref: "resizeBarX",
+ className: "viewport-horizontal-resize-handle",
+ onMouseDown: this.onResizeStart,
+ }),
+ dom.div({
+ ref: "resizeBarY",
+ className: "viewport-vertical-resize-handle",
+ onMouseDown: this.onResizeStart,
+ })
+ );
+ },
+
+});
diff --git a/devtools/client/responsive.html/components/viewport-dimension.js b/devtools/client/responsive.html/components/viewport-dimension.js
new file mode 100644
index 000000000..a359cecf7
--- /dev/null
+++ b/devtools/client/responsive.html/components/viewport-dimension.js
@@ -0,0 +1,173 @@
+/* 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 { DOM: dom, createClass, PropTypes } =
+ require("devtools/client/shared/vendor/react");
+
+const Constants = require("../constants");
+const Types = require("../types");
+
+module.exports = createClass({
+ displayName: "ViewportDimension",
+
+ propTypes: {
+ viewport: PropTypes.shape(Types.viewport).isRequired,
+ onRemoveDevice: PropTypes.func.isRequired,
+ onResizeViewport: PropTypes.func.isRequired,
+ },
+
+ getInitialState() {
+ let { width, height } = this.props.viewport;
+
+ return {
+ width,
+ height,
+ isEditing: false,
+ isInvalid: false,
+ };
+ },
+
+ componentWillReceiveProps(nextProps) {
+ let { width, height } = nextProps.viewport;
+
+ this.setState({
+ width,
+ height,
+ });
+ },
+
+ validateInput(value) {
+ let isInvalid = true;
+
+ // Check the value is a number and greater than MIN_VIEWPORT_DIMENSION
+ if (/^\d{3,4}$/.test(value) &&
+ parseInt(value, 10) >= Constants.MIN_VIEWPORT_DIMENSION) {
+ isInvalid = false;
+ }
+
+ this.setState({
+ isInvalid,
+ });
+ },
+
+ onInputBlur() {
+ let { width, height } = this.props.viewport;
+
+ if (this.state.width != width || this.state.height != height) {
+ this.onInputSubmit();
+ }
+
+ this.setState({
+ isEditing: false,
+ inInvalid: false,
+ });
+ },
+
+ onInputChange({ target }) {
+ if (target.value.length > 4) {
+ return;
+ }
+
+ if (this.refs.widthInput == target) {
+ this.setState({ width: target.value });
+ this.validateInput(target.value);
+ }
+
+ if (this.refs.heightInput == target) {
+ this.setState({ height: target.value });
+ this.validateInput(target.value);
+ }
+ },
+
+ onInputFocus() {
+ this.setState({
+ isEditing: true,
+ });
+ },
+
+ onInputKeyUp({ target, keyCode }) {
+ // On Enter, submit the input
+ if (keyCode == 13) {
+ this.onInputSubmit();
+ }
+
+ // On Esc, blur the target
+ if (keyCode == 27) {
+ target.blur();
+ }
+ },
+
+ onInputSubmit() {
+ if (this.state.isInvalid) {
+ let { width, height } = this.props.viewport;
+
+ this.setState({
+ width,
+ height,
+ isInvalid: false,
+ });
+
+ return;
+ }
+
+ // Change the device selector back to an unselected device
+ // TODO: Bug 1332754: Logic like this probably belongs in the action creator.
+ if (this.props.viewport.device) {
+ this.props.onRemoveDevice();
+ }
+ this.props.onResizeViewport(parseInt(this.state.width, 10),
+ parseInt(this.state.height, 10));
+ },
+
+ render() {
+ let editableClass = "viewport-dimension-editable";
+ let inputClass = "viewport-dimension-input";
+
+ if (this.state.isEditing) {
+ editableClass += " editing";
+ inputClass += " editing";
+ }
+
+ if (this.state.isInvalid) {
+ editableClass += " invalid";
+ }
+
+ return dom.div(
+ {
+ className: "viewport-dimension",
+ },
+ dom.div(
+ {
+ className: editableClass,
+ },
+ dom.input({
+ ref: "widthInput",
+ className: inputClass,
+ size: 4,
+ value: this.state.width,
+ onBlur: this.onInputBlur,
+ onChange: this.onInputChange,
+ onFocus: this.onInputFocus,
+ onKeyUp: this.onInputKeyUp,
+ }),
+ dom.span({
+ className: "viewport-dimension-separator",
+ }, "×"),
+ dom.input({
+ ref: "heightInput",
+ className: inputClass,
+ size: 4,
+ value: this.state.height,
+ onBlur: this.onInputBlur,
+ onChange: this.onInputChange,
+ onFocus: this.onInputFocus,
+ onKeyUp: this.onInputKeyUp,
+ })
+ )
+ );
+ },
+
+});
diff --git a/devtools/client/responsive.html/components/viewport-toolbar.js b/devtools/client/responsive.html/components/viewport-toolbar.js
new file mode 100644
index 000000000..7cbc73f67
--- /dev/null
+++ b/devtools/client/responsive.html/components/viewport-toolbar.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/. */
+
+"use strict";
+
+const { DOM: dom, createClass, createFactory, PropTypes, addons } =
+ require("devtools/client/shared/vendor/react");
+
+const Types = require("../types");
+const DeviceSelector = createFactory(require("./device-selector"));
+
+module.exports = createClass({
+ displayName: "ViewportToolbar",
+
+ propTypes: {
+ devices: PropTypes.shape(Types.devices).isRequired,
+ selectedDevice: PropTypes.string.isRequired,
+ onChangeDevice: PropTypes.func.isRequired,
+ onResizeViewport: PropTypes.func.isRequired,
+ onRotateViewport: PropTypes.func.isRequired,
+ onUpdateDeviceModalOpen: PropTypes.func.isRequired,
+ },
+
+ mixins: [ addons.PureRenderMixin ],
+
+ render() {
+ let {
+ devices,
+ selectedDevice,
+ onChangeDevice,
+ onResizeViewport,
+ onRotateViewport,
+ onUpdateDeviceModalOpen,
+ } = this.props;
+
+ return dom.div(
+ {
+ className: "viewport-toolbar container",
+ },
+ DeviceSelector({
+ devices,
+ selectedDevice,
+ onChangeDevice,
+ onResizeViewport,
+ onUpdateDeviceModalOpen,
+ }),
+ dom.button({
+ className: "viewport-rotate-button toolbar-button devtools-button",
+ onClick: onRotateViewport,
+ })
+ );
+ },
+
+});
diff --git a/devtools/client/responsive.html/components/viewport.js b/devtools/client/responsive.html/components/viewport.js
new file mode 100644
index 000000000..fe41b41ee
--- /dev/null
+++ b/devtools/client/responsive.html/components/viewport.js
@@ -0,0 +1,114 @@
+/* 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 { DOM: dom, createClass, createFactory, PropTypes } =
+ require("devtools/client/shared/vendor/react");
+
+const Types = require("../types");
+const ResizableViewport = createFactory(require("./resizable-viewport"));
+const ViewportDimension = createFactory(require("./viewport-dimension"));
+
+module.exports = createClass({
+
+ displayName: "Viewport",
+
+ propTypes: {
+ devices: PropTypes.shape(Types.devices).isRequired,
+ location: Types.location.isRequired,
+ screenshot: PropTypes.shape(Types.screenshot).isRequired,
+ swapAfterMount: PropTypes.bool.isRequired,
+ viewport: PropTypes.shape(Types.viewport).isRequired,
+ onBrowserMounted: PropTypes.func.isRequired,
+ onChangeDevice: PropTypes.func.isRequired,
+ onContentResize: PropTypes.func.isRequired,
+ onRemoveDevice: PropTypes.func.isRequired,
+ onResizeViewport: PropTypes.func.isRequired,
+ onRotateViewport: PropTypes.func.isRequired,
+ onUpdateDeviceModalOpen: PropTypes.func.isRequired,
+ },
+
+ onChangeDevice(device) {
+ let {
+ viewport,
+ onChangeDevice,
+ } = this.props;
+
+ onChangeDevice(viewport.id, device);
+ },
+
+ onRemoveDevice() {
+ let {
+ viewport,
+ onRemoveDevice,
+ } = this.props;
+
+ onRemoveDevice(viewport.id);
+ },
+
+ onResizeViewport(width, height) {
+ let {
+ viewport,
+ onResizeViewport,
+ } = this.props;
+
+ onResizeViewport(viewport.id, width, height);
+ },
+
+ onRotateViewport() {
+ let {
+ viewport,
+ onRotateViewport,
+ } = this.props;
+
+ onRotateViewport(viewport.id);
+ },
+
+ render() {
+ let {
+ devices,
+ location,
+ screenshot,
+ swapAfterMount,
+ viewport,
+ onBrowserMounted,
+ onContentResize,
+ onUpdateDeviceModalOpen,
+ } = this.props;
+
+ let {
+ onChangeDevice,
+ onRemoveDevice,
+ onRotateViewport,
+ onResizeViewport,
+ } = this;
+
+ return dom.div(
+ {
+ className: "viewport",
+ },
+ ViewportDimension({
+ viewport,
+ onRemoveDevice,
+ onResizeViewport,
+ }),
+ ResizableViewport({
+ devices,
+ location,
+ screenshot,
+ swapAfterMount,
+ viewport,
+ onBrowserMounted,
+ onChangeDevice,
+ onContentResize,
+ onRemoveDevice,
+ onResizeViewport,
+ onRotateViewport,
+ onUpdateDeviceModalOpen,
+ })
+ );
+ },
+
+});
diff --git a/devtools/client/responsive.html/components/viewports.js b/devtools/client/responsive.html/components/viewports.js
new file mode 100644
index 000000000..b305d1e07
--- /dev/null
+++ b/devtools/client/responsive.html/components/viewports.js
@@ -0,0 +1,70 @@
+/* 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 { DOM: dom, createClass, createFactory, PropTypes } =
+ require("devtools/client/shared/vendor/react");
+
+const Types = require("../types");
+const Viewport = createFactory(require("./viewport"));
+
+module.exports = createClass({
+
+ displayName: "Viewports",
+
+ propTypes: {
+ devices: PropTypes.shape(Types.devices).isRequired,
+ location: Types.location.isRequired,
+ screenshot: PropTypes.shape(Types.screenshot).isRequired,
+ viewports: PropTypes.arrayOf(PropTypes.shape(Types.viewport)).isRequired,
+ onBrowserMounted: PropTypes.func.isRequired,
+ onChangeDevice: PropTypes.func.isRequired,
+ onContentResize: PropTypes.func.isRequired,
+ onRemoveDevice: PropTypes.func.isRequired,
+ onResizeViewport: PropTypes.func.isRequired,
+ onRotateViewport: PropTypes.func.isRequired,
+ onUpdateDeviceModalOpen: PropTypes.func.isRequired,
+ },
+
+ render() {
+ let {
+ devices,
+ location,
+ screenshot,
+ viewports,
+ onBrowserMounted,
+ onChangeDevice,
+ onContentResize,
+ onRemoveDevice,
+ onResizeViewport,
+ onRotateViewport,
+ onUpdateDeviceModalOpen,
+ } = this.props;
+
+ return dom.div(
+ {
+ id: "viewports",
+ },
+ viewports.map((viewport, i) => {
+ return Viewport({
+ key: viewport.id,
+ devices,
+ location,
+ screenshot,
+ swapAfterMount: i == 0,
+ viewport,
+ onBrowserMounted,
+ onChangeDevice,
+ onContentResize,
+ onRemoveDevice,
+ onResizeViewport,
+ onRotateViewport,
+ onUpdateDeviceModalOpen,
+ });
+ })
+ );
+ },
+
+});
diff --git a/devtools/client/responsive.html/constants.js b/devtools/client/responsive.html/constants.js
new file mode 100644
index 000000000..b848515ea
--- /dev/null
+++ b/devtools/client/responsive.html/constants.js
@@ -0,0 +1,8 @@
+/* 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";
+
+// The minimum viewport width and height
+exports.MIN_VIEWPORT_DIMENSION = 280;
diff --git a/devtools/client/responsive.html/docs/browser-swap.md b/devtools/client/responsive.html/docs/browser-swap.md
new file mode 100644
index 000000000..75055ad4e
--- /dev/null
+++ b/devtools/client/responsive.html/docs/browser-swap.md
@@ -0,0 +1,146 @@
+# Overview
+
+The RDM tool uses several forms of tab and browser swapping to integrate the
+tool UI cleanly into the browser UI. The high level steps of this process are
+documented at `/devtools/docs/responsive-design-mode.md`.
+
+This document contains a random assortment of low level notes about the steps
+the browser goes through when swapping browsers between tabs.
+
+# Connections between Browsers and Tabs
+
+Link between tab and browser (`gBrowser._linkBrowserToTab`):
+
+```
+aTab.linkedBrowser = browser;
+gBrowser._tabForBrowser.set(browser, aTab);
+```
+
+# Swapping Browsers between Tabs
+
+## Legend
+
+* (R): remote browsers only
+* (!R): non-remote browsers only
+
+## Functions Called
+
+When you call `gBrowser.swapBrowsersAndCloseOther` to move tab content from a
+browser in one tab to a browser in another tab, here are all the code paths
+involved:
+
+* `gBrowser.swapBrowsersAndCloseOther`
+ * `gBrowser._beginRemoveTab`
+ * `gBrowser.tabContainer.updateVisibility`
+ * Emit `TabClose`
+ * `browser.webProgress.removeProgressListener`
+ * `filter.removeProgressListener`
+ * `listener.destroy`
+ * `gBrowser._swapBrowserDocShells`
+ * `ourBrowser.webProgress.removeProgressListener`
+ * `filter.removeProgressListener`
+ * `gBrowser._swapRegisteredOpenURIs`
+ * `ourBrowser.swapDocShells(aOtherBrowser)`
+ * Emit `SwapDocShells`
+ * `PopupNotifications._swapBrowserNotifications`
+ * `browser.detachFormFill` (!R)
+ * `browser.swapFrameLoaders`
+ * `browser.attachFormFill` (!R)
+ * `browser._remoteWebNavigationImpl.swapBrowser(browser)` (R)
+ * `browser._remoteWebProgressManager.swapBrowser(browser)` (R)
+ * `browser._remoteFinder.swapBrowser(browser)` (R)
+ * Emit `EndSwapDocShells`
+ * `gBrowser.mTabProgressListener`
+ * `filter.addProgressListener`
+ * `ourBrowser.webProgress.addProgressListener`
+ * `gBrowser._endRemoveTab`
+ * `gBrowser.tabContainer._fillTrailingGap`
+ * `gBrowser._blurTab`
+ * `gBrowser._tabFilters.delete`
+ * `gBrowser._tabListeners.delete`
+ * `gBrowser._outerWindowIDBrowserMap.delete`
+ * `browser.destroy`
+ * `gBrowser.tabContainer.removeChild`
+ * `gBrowser.tabContainer.adjustTabstrip`
+ * `gBrowser.tabContainer._setPositionalAttributes`
+ * `browser.parentNode.removeChild(browser)`
+ * `gBrowser._tabForBrowser.delete`
+ * `gBrowser.mPanelContainer.removeChild`
+ * `gBrowser.setTabTitle` / `gBrowser.setTabTitleLoading`
+ * `browser.currentURI.spec`
+ * `gBrowser._tabAttrModified`
+ * `gBrowser.updateTitlebar`
+ * `gBrowser.updateCurrentBrowser`
+ * `browser.docShellIsActive` (!R)
+ * `gBrowser.showTab`
+ * `gBrowser._appendStatusPanel`
+ * `gBrowser._callProgressListeners` with `onLocationChange`
+ * `gBrowser._callProgressListeners` with `onSecurityChange`
+ * `gBrowser._callProgressListeners` with `onUpdateCurrentBrowser`
+ * `gBrowser._recordTabAccess`
+ * `gBrowser.updateTitlebar`
+ * `gBrowser._callProgressListeners` with `onStateChange`
+ * `gBrowser._setCloseKeyState`
+ * Emit `TabSelect`
+ * `gBrowser._tabAttrModified`
+ * `browser.getInPermitUnload`
+ * `gBrowser.tabContainer._setPositionalAttributes`
+ * `gBrowser._tabAttrModified`
+
+## Browser State
+
+When calling `gBrowser.swapBrowsersAndCloseOther`, the browser is not actually
+moved from one tab to the other. Instead, various properties _on_ each of the
+browsers are swapped.
+
+Browser attributes `gBrowser.swapBrowsersAndCloseOther` transfers between
+browsers:
+
+* `usercontextid`
+
+Tab attributes `gBrowser.swapBrowsersAndCloseOther` transfers between tabs:
+
+* `usercontextid`
+* `muted`
+* `soundplaying`
+* `busy`
+
+Browser properties `gBrowser.swapBrowsersAndCloseOther` transfers between
+browsers:
+
+* `mIconURL`
+* `getFindBar(aOurTab)._findField.value`
+
+Browser properties `gBrowser._swapBrowserDocShells` transfers between browsers:
+
+* `outerWindowID` in `gBrowser._outerWindowIDBrowserMap`
+* `_outerWindowID` on the browser (R)
+* `docShellIsActive`
+* `permanentKey`
+* `registeredOpenURI`
+
+Browser properties `browser.swapDocShells` transfers between browsers:
+
+* `_docShell`
+* `_webBrowserFind`
+* `_contentWindow`
+* `_webNavigation`
+* `_remoteWebNavigation` (R)
+* `_remoteWebNavigationImpl` (R)
+* `_remoteWebProgressManager` (R)
+* `_remoteWebProgress` (R)
+* `_remoteFinder` (R)
+* `_securityUI` (R)
+* `_documentURI` (R)
+* `_documentContentType` (R)
+* `_contentTitle` (R)
+* `_characterSet` (R)
+* `_contentPrincipal` (R)
+* `_imageDocument` (R)
+* `_fullZoom` (R)
+* `_textZoom` (R)
+* `_isSyntheticDocument` (R)
+* `_innerWindowID` (R)
+* `_manifestURI` (R)
+
+`browser.swapFrameLoaders` swaps the actual page content.
diff --git a/devtools/client/responsive.html/images/close.svg b/devtools/client/responsive.html/images/close.svg
new file mode 100644
index 000000000..9a491fcae
--- /dev/null
+++ b/devtools/client/responsive.html/images/close.svg
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="#0b0b0b">
+ <path d="M6.7 8l3.6-3.6c.2-.2.2-.5 0-.7-.2-.2-.5-.2-.7 0L6 7.3 2.4 3.7c-.2-.2-.5-.2-.7 0-.2.2-.2.5 0 .7L5.3 8l-3.6 3.6c-.2.2-.2.5 0 .7.2.2.5.2.7 0L6 8.7l3.6 3.6c.2.2.5.2.7 0 .2-.2.2-.5 0-.7L6.7 8z"/>
+</svg>
diff --git a/devtools/client/responsive.html/images/grippers.svg b/devtools/client/responsive.html/images/grippers.svg
new file mode 100644
index 000000000..91db83af9
--- /dev/null
+++ b/devtools/client/responsive.html/images/grippers.svg
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="#A5A5A5">
+ <path d="M16 3.2L3.1 16h1.7L16 4.9zM16 7.2L7.1 16h1.8L16 8.9zM16 11.1L11.1 16h1.8l3.1-3.1z" />
+</svg>
diff --git a/devtools/client/responsive.html/images/moz.build b/devtools/client/responsive.html/images/moz.build
new file mode 100644
index 000000000..bbce6d6c2
--- /dev/null
+++ b/devtools/client/responsive.html/images/moz.build
@@ -0,0 +1,14 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'close.svg',
+ 'grippers.svg',
+ 'rotate-viewport.svg',
+ 'screenshot.svg',
+ 'select-arrow.svg',
+ 'touch-events.svg',
+)
diff --git a/devtools/client/responsive.html/images/rotate-viewport.svg b/devtools/client/responsive.html/images/rotate-viewport.svg
new file mode 100644
index 000000000..494e47e90
--- /dev/null
+++ b/devtools/client/responsive.html/images/rotate-viewport.svg
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="#0b0b0b">
+ <path d="M3.8 13.4c-1.2 0-3.4-.6-3.7-2.8s1.3-3.3 2.1-3.5c.2-.1.4.1.5.3.1.2-.1.4-.3.5-.1 0-1.8.6-1.6 2.7.2 1.5 1.6 1.9 2.4 2l-.7-2.4c0-.2.2-.5.4-.5.2-.1.4 0 .5.2l.9 3c0 .1 0 .3-.1.4-.1.1-.2.1-.4.1zM12.3 1.7c1.2 0 3.4.6 3.7 2.8.3 2.2-1.3 3.3-2.1 3.5-.2.1-.4-.1-.5-.3s.1-.4.3-.5c.1 0 1.8-.6 1.6-2.7-.2-1.5-1.6-1.9-2.4-2l.7 2.4c.1.2-.1.4-.3.5s-.4-.1-.5-.3l-.9-3c0-.1 0-.3.1-.4h.3zM9.6 2.5L4.3 4.1c-.2.1-.4.4-.3.8l2.5 8c.1.3.4.6.8.5l5.2-1.6c.3-.1.6-.5.4-.8l-2.5-8c0-.1-.7-.6-.8-.5zm2.5 8.6l-5 1.5-.6-1.9 5-1.5.6 1.9zm-.8-2.6l-5 1.5-1.6-5.3 5-1.5 1.6 5.3z"/>
+</svg>
diff --git a/devtools/client/responsive.html/images/screenshot.svg b/devtools/client/responsive.html/images/screenshot.svg
new file mode 100644
index 000000000..306d40f93
--- /dev/null
+++ b/devtools/client/responsive.html/images/screenshot.svg
@@ -0,0 +1,7 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="#0b0b0b">
+ <path d="M14.4,4.1h-3l0-1.3c0-0.9-1.2-1.6-2.1-1.6H6.6c-0.9,0-1.9,0.7-1.9,1.6l0,1.3h-3c-0.9,0-1.6,0.7-1.6,1.6v7.4c0,0.9,0.7,1.3,1.6,1.3h12.7c0.9,0,1.6-0.3,1.6-1.3V5.7C16,4.8,15.3,4.1,14.4,4.1z M14.8,13.2H1.2v-8h4.5l0-3h4.5l0,3h4.4L14.8,13.2z"/>
+ <path d="M8,6.7c-1.3,0-2.4,1.1-2.4,2.4s1.1,2.4,2.4,2.4s2.4-1.1,2.4-2.4S9.3,6.7,8,6.7z M8,10.2c-0.7,0-1.2-0.5-1.2-1.1S7.3,8,8,8s1.2,0.5,1.2,1.1S8.7,10.2,8,10.2z"/>
+</svg>
diff --git a/devtools/client/responsive.html/images/select-arrow.svg b/devtools/client/responsive.html/images/select-arrow.svg
new file mode 100644
index 000000000..c9165a206
--- /dev/null
+++ b/devtools/client/responsive.html/images/select-arrow.svg
@@ -0,0 +1,37 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 16 16">
+ <defs>
+ <style>
+ use:not(:target) {
+ display: none;
+ }
+ #light {
+ fill: #999797;
+ }
+ #light-hovered {
+ fill: #393f4c; /* --theme-body-color */
+ }
+ #light-selected {
+ fill: #3b3b3b;
+ }
+ #dark {
+ fill: #c6ccd0;
+ }
+ #dark-hovered {
+ fill: #dde1e4;
+ }
+ #dark-selected {
+ fill: #fcfcfc;
+ }
+ </style>
+ <path id="base-path" d="M7.9 16.3c-.3 0-.6-.1-.8-.4l-4-4.8c-.2-.3-.3-.5-.1-.8.1-.3.5-.3.9-.3h8c.4 0 .7 0 .9.3.2.4.1.6-.1.9l-4 4.8c-.2.3-.5.3-.8.3zM7.8 0c.3 0 .6.1.7.4L12.4 5c.2.3.3.4.1.7-.1.4-.5.3-.8.3H3.9c-.4 0-.8.1-.9-.2-.2-.4-.1-.6.1-.9L7 .3c.2-.3.5-.3.8-.3z"/>
+ </defs>
+ <use xlink:href="#base-path" id="light"/>
+ <use xlink:href="#base-path" id="light-hovered"/>
+ <use xlink:href="#base-path" id="light-selected"/>
+ <use xlink:href="#base-path" id="dark"/>
+ <use xlink:href="#base-path" id="dark-hovered"/>
+ <use xlink:href="#base-path" id="dark-selected"/>
+</svg>
diff --git a/devtools/client/responsive.html/images/touch-events.svg b/devtools/client/responsive.html/images/touch-events.svg
new file mode 100644
index 000000000..18aa3c66d
--- /dev/null
+++ b/devtools/client/responsive.html/images/touch-events.svg
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="#0b0b0b">
+ <path d="M12.5 5.3c-.2 0-.4 0-.6.1-.2-.6-.8-1-1.4-1-.3 0-.5.1-.8.2C9.4 4.2 9 4 8.6 4h-.4V1.5C8.2.7 7.5 0 6.7 0S5.2.7 5.2 1.5v6.6l-.7-.6c-.6-.6-1.6-.6-2.2 0-.5.6-.5 1.4-.1 2.1.3.4.6 1.1 1 1.8C4.2 13.6 5.3 16 7 16h3.9s3.1-1 3.1-4V6.7c.1-.8-.7-1.4-1.5-1.4zm.6 6.7c0 2-2.1 3-2.4 3H7c-1 0-2.1-2.4-2.9-4-.3-.8-.7-1.6-1-2-.2-.3-.2-.5-.1-.7.1-.1.2-.1.3-.1.1 0 .2 0 .3.1l1.5 1.5c.3.2.6.2.7.1.1 0 .4-.2.4-.5V1.5c0-.2.2-.4.5-.4s.5.2.5.4v5.3c0 .3.2.5.5.5s.5-.2.5-.5V5.5c0-.4.2-.5.5-.5.2 0 .5.2.5.4v2c-.1.3.2.6.4.6.3 0 .5-.2.5-.5V5.8c0-.2.2-.4.5-.4s.5.2.5.4v2.3c0 .3.2.5.5.5s.5-.2.5-.5V6.7c0-.2.2-.4.5-.4s.5.2.5.4V12z"/>
+</svg>
diff --git a/devtools/client/responsive.html/index.css b/devtools/client/responsive.html/index.css
new file mode 100644
index 000000000..c88f95777
--- /dev/null
+++ b/devtools/client/responsive.html/index.css
@@ -0,0 +1,521 @@
+/* TODO: May break up into component local CSS. Pending future discussions by
+ * React component group on how to best handle CSS. */
+
+/**
+ * CSS Variables specific to the responsive design mode
+ */
+
+.theme-light {
+ --rdm-box-shadow: 0 4px 4px 0 rgba(155, 155, 155, 0.26);
+ --submit-button-active-background-color: rgba(0,0,0,0.12);
+ --submit-button-active-color: var(--theme-body-color);
+ --viewport-color: #999797;
+ --viewport-hover-color: var(--theme-body-color);
+ --viewport-active-color: #3b3b3b;
+ --viewport-selection-arrow: url("./images/select-arrow.svg#light");
+ --viewport-selection-arrow-hovered:
+ url("./images/select-arrow.svg#light-hovered");
+ --viewport-selection-arrow-selected:
+ url("./images/select-arrow.svg#light-selected");
+}
+
+.theme-dark {
+ --rdm-box-shadow: 0 4px 4px 0 rgba(105, 105, 105, 0.26);
+ --submit-button-active-background-color: var(--toolbar-tab-hover-active);
+ --submit-button-active-color: var(--theme-selection-color);
+ --viewport-color: #c6ccd0;
+ --viewport-hover-color: #dde1e4;
+ --viewport-active-color: #fcfcfc;
+ --viewport-selection-arrow: url("./images/select-arrow.svg#dark");
+ --viewport-selection-arrow-hovered:
+ url("./images/select-arrow.svg#dark-hovered");
+ --viewport-selection-arrow-selected:
+ url("./images/select-arrow.svg#dark-selected");
+}
+
+* {
+ box-sizing: border-box;
+}
+
+#root,
+html, body {
+ height: 100%;
+ margin: 0;
+}
+
+#app {
+ /* Center the viewports container */
+ display: flex;
+ align-items: center;
+ flex-direction: column;
+ padding-top: 15px;
+ padding-bottom: 1%;
+ position: relative;
+ height: 100%;
+}
+
+/**
+ * Common styles for shared components
+ */
+
+.container {
+ background-color: var(--theme-toolbar-background);
+ border: 1px solid var(--theme-splitter-color);
+}
+
+.toolbar-button {
+ margin: 1px 3px;
+ width: 16px;
+ height: 16px;
+ /* Reset styles from .devtools-button */
+ min-width: initial;
+ min-height: initial;
+ align-self: center;
+}
+
+.toolbar-button:active::before {
+ filter: url("chrome://devtools/skin/images/filters.svg#checked-icon-state");
+}
+
+select {
+ -moz-appearance: none;
+ background-color: var(--theme-toolbar-background);
+ background-image: var(--viewport-selection-arrow);
+ background-position: 100% 50%;
+ background-repeat: no-repeat;
+ background-size: 7px;
+ border: none;
+ color: var(--viewport-color);
+ height: 100%;
+ padding: 0 8px;
+ text-align: center;
+ text-overflow: ellipsis;
+ font-size: 11px;
+}
+
+select.selected {
+ background-image: var(--viewport-selection-arrow-selected);
+ color: var(--viewport-active-color);
+}
+
+select:not(:disabled):hover {
+ background-image: var(--viewport-selection-arrow-hovered);
+ color: var(--viewport-hover-color);
+}
+
+/* This is (believed to be?) separate from the identical select.selected rule
+ set so that it overrides select:hover because of file ordering once the
+ select is focused. It's unclear whether the visual effect that results here
+ is intentional and desired. */
+select:focus {
+ background-image: var(--viewport-selection-arrow-selected);
+ color: var(--viewport-active-color);
+}
+
+select > option {
+ text-align: left;
+ padding: 5px 10px;
+}
+
+select > option,
+select > option:hover {
+ color: var(--viewport-active-color);
+}
+
+select > option.divider {
+ border-top: 1px solid var(--theme-splitter-color);
+ height: 0px;
+ padding: 0;
+ font-size: 0px;
+}
+
+/**
+ * Global Toolbar
+ */
+
+#global-toolbar {
+ color: var(--theme-body-color-alt);
+ border-radius: 2px;
+ box-shadow: var(--rdm-box-shadow);
+ margin: 0 0 15px 0;
+ padding: 4px 5px;
+ display: inline-flex;
+ -moz-user-select: none;
+}
+
+#global-toolbar > .title {
+ border-right: 1px solid var(--theme-splitter-color);
+ padding: 1px 6px 0 2px;
+}
+
+#global-toolbar .toolbar-button {
+ margin: 0 0 0 5px;
+ padding: 0;
+}
+
+#global-toolbar .toolbar-button,
+#global-toolbar .toolbar-button::before {
+ width: 12px;
+ height: 12px;
+}
+
+#global-touch-simulation-button::before {
+ background-image: url("./images/touch-events.svg");
+ margin: -6px 0 0 -6px;
+}
+
+#global-touch-simulation-button.active::before {
+ filter: url("chrome://devtools/skin/images/filters.svg#checked-icon-state");
+}
+
+#global-screenshot-button::before {
+ background-image: url("./images/screenshot.svg");
+ margin: -6px 0 0 -6px;
+}
+
+#global-exit-button::before {
+ background-image: url("./images/close.svg");
+ margin: -6px 0 0 -6px;
+}
+
+#global-screenshot-button:disabled {
+ filter: url("chrome://devtools/skin/images/filters.svg#checked-icon-state");
+ opacity: 1 !important;
+}
+
+#global-network-throttling-selector {
+ height: 15px;
+ padding-left: 0;
+ width: 103px;
+}
+
+#global-dpr-selector > select {
+ padding: 0 8px 0 0;
+ margin-left: 2px;
+}
+
+#global-dpr-selector {
+ margin: 0 8px;
+ -moz-user-select: none;
+ color: var(--viewport-color);
+ font-size: 11px;
+ height: 15px;
+}
+
+#global-dpr-selector.focused,
+#global-dpr-selector:not(.disabled):hover {
+ color: var(--viewport-hover-color);
+}
+
+#global-dpr-selector:not(.disabled):hover > select {
+ background-image: var(--viewport-selection-arrow-hovered);
+ color: var(--viewport-hover-color);
+}
+
+#global-dpr-selector:focus > select {
+ background-image: var(--viewport-selection-arrow-selected);
+ color: var(--viewport-active-color);
+}
+
+#global-dpr-selector.selected,
+#global-dpr-selector.selected > select {
+ color: var(--viewport-active-color);
+}
+
+#global-dpr-selector > select > option {
+ padding: 5px;
+}
+
+#viewports {
+ /* Make sure left-most viewport is visible when there's horizontal overflow.
+ That is, when the horizontal space become smaller than the viewports and a
+ scrollbar appears, then the first viewport will still be visible */
+ position: sticky;
+ left: 0;
+ /* Individual viewports are inline elements, make sure they stay on a single
+ line */
+ white-space: nowrap;
+}
+
+/**
+ * Viewport Container
+ */
+
+.viewport {
+ display: inline-block;
+ /* Align all viewports to the top */
+ vertical-align: top;
+}
+
+.resizable-viewport {
+ border: 1px solid var(--theme-splitter-color);
+ box-shadow: var(--rdm-box-shadow);
+ position: relative;
+}
+
+/**
+ * Viewport Toolbar
+ */
+
+.viewport-toolbar {
+ border-width: 0;
+ border-bottom-width: 1px;
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ height: 18px;
+}
+
+.viewport-rotate-button {
+ position: absolute;
+ right: 0;
+}
+
+.viewport-rotate-button::before {
+ background-image: url("./images/rotate-viewport.svg");
+}
+
+/**
+ * Viewport Content
+ */
+
+.viewport-content.resizing {
+ pointer-events: none;
+}
+
+/**
+ * Viewport Browser
+ */
+
+.browser-container {
+ width: inherit;
+ height: inherit;
+}
+
+.browser {
+ display: block;
+ border: 0;
+ -moz-user-select: none;
+}
+
+.browser:-moz-focusring {
+ outline: none;
+}
+
+/**
+ * Viewport Resize Handles
+ */
+
+.viewport-resize-handle {
+ position: absolute;
+ width: 16px;
+ height: 16px;
+ bottom: 0;
+ right: 0;
+ background-image: url("./images/grippers.svg");
+ background-position: bottom right;
+ padding: 0 1px 1px 0;
+ background-repeat: no-repeat;
+ background-origin: content-box;
+ cursor: se-resize;
+}
+
+.viewport-resize-handle.hidden {
+ display: none;
+}
+
+.viewport-horizontal-resize-handle {
+ position: absolute;
+ width: 5px;
+ height: calc(100% - 16px);
+ right: -4px;
+ top: 0;
+ cursor: e-resize;
+}
+
+.viewport-vertical-resize-handle {
+ position: absolute;
+ width: calc(100% - 16px);
+ height: 5px;
+ left: 0;
+ bottom: -4px;
+ cursor: s-resize;
+}
+
+/**
+ * Viewport Dimension Label
+ */
+
+.viewport-dimension {
+ display: flex;
+ justify-content: center;
+ font: 10px sans-serif;
+ margin-bottom: 10px;
+}
+
+.viewport-dimension-editable {
+ border-bottom: 1px solid transparent;
+}
+
+.viewport-dimension-editable,
+.viewport-dimension-input {
+ color: var(--theme-body-color-inactive);
+ transition: all 0.25s ease;
+}
+
+.viewport-dimension-editable.editing,
+.viewport-dimension-input.editing {
+ color: var(--viewport-active-color);
+}
+
+.viewport-dimension-editable.editing {
+ border-bottom: 1px solid var(--theme-selection-background);
+}
+
+.viewport-dimension-editable.editing.invalid {
+ border-bottom: 1px solid #d92215;
+}
+
+.viewport-dimension-input {
+ background: transparent;
+ border: none;
+ text-align: center;
+}
+
+.viewport-dimension-separator {
+ -moz-user-select: none;
+}
+
+/**
+ * Device Modal
+ */
+
+@keyframes fade-in-and-up {
+ 0% {
+ opacity: 0;
+ transform: translateY(5px);
+ }
+ 100% {
+ opacity: 1;
+ transform: translateY(0px);
+ }
+}
+
+@keyframes fade-down-and-out {
+ 0% {
+ opacity: 1;
+ transform: translateY(0px);
+ }
+ 100% {
+ opacity: 0;
+ transform: translateY(5px);
+ visibility: hidden;
+ }
+}
+
+.device-modal {
+ border-radius: 2px;
+ box-shadow: var(--rdm-box-shadow);
+ display: none;
+ position: absolute;
+ margin: auto;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ width: 642px;
+ height: 612px;
+ z-index: 1;
+}
+
+/* Handles the opening/closing of the modal */
+#device-modal-wrapper.opened .device-modal {
+ animation: fade-in-and-up 0.3s ease;
+ animation-fill-mode: forwards;
+ display: block;
+}
+
+#device-modal-wrapper.closed .device-modal {
+ animation: fade-down-and-out 0.3s ease;
+ animation-fill-mode: forwards;
+ display: block;
+}
+
+#device-modal-wrapper.opened .modal-overlay {
+ background-color: var(--theme-splitter-color);
+ position: absolute;
+ top: 0;
+ left: 0;
+ height: 100%;
+ width: 100%;
+ z-index: 0;
+ opacity: 0.5;
+}
+
+.device-modal-content {
+ display: flex;
+ flex-direction: column;
+ flex-wrap: wrap;
+ overflow: auto;
+ height: 550px;
+ width: 600px;
+ margin: 20px;
+}
+
+#device-close-button,
+#device-close-button::before {
+ position: absolute;
+ top: 5px;
+ right: 2px;
+ width: 12px;
+ height: 12px;
+}
+
+#device-close-button::before {
+ background-image: url("./images/close.svg");
+ margin: -6px 0 0 -6px;
+}
+
+.device-type {
+ display: flex;
+ flex-direction: column;
+ padding: 10px;
+}
+
+.device-header {
+ font-size: 11px;
+ font-weight: bold;
+ text-transform: capitalize;
+ padding: 0 0 3px 23px;
+}
+
+.device-label {
+ font-size: 11px;
+ padding-bottom: 3px;
+ display: flex;
+ align-items: center;
+}
+
+.device-input-checkbox {
+ margin-right: 5px;
+}
+
+#device-submit-button {
+ background-color: var(--theme-tab-toolbar-background);
+ border-width: 1px 0 0 0;
+ border-top-width: 1px;
+ border-top-style: solid;
+ border-top-color: var(--theme-splitter-color);
+ color: var(--theme-body-color);
+ width: 100%;
+ height: 20px;
+}
+
+#device-submit-button:hover {
+ background-color: var(--toolbar-tab-hover);
+}
+
+#device-submit-button:hover:active {
+ background-color: var(--submit-button-active-background-color);
+ color: var(--submit-button-active-color);
+}
diff --git a/devtools/client/responsive.html/index.js b/devtools/client/responsive.html/index.js
new file mode 100644
index 000000000..7e8f8aeac
--- /dev/null
+++ b/devtools/client/responsive.html/index.js
@@ -0,0 +1,166 @@
+/* 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/. */
+
+/* eslint-env browser */
+
+"use strict";
+
+const { utils: Cu } = Components;
+const { BrowserLoader } =
+ Cu.import("resource://devtools/client/shared/browser-loader.js", {});
+const { require } = BrowserLoader({
+ baseURI: "resource://devtools/client/responsive.html/",
+ window
+});
+const { Task } = require("devtools/shared/task");
+const Telemetry = require("devtools/client/shared/telemetry");
+const { loadSheet } = require("sdk/stylesheet/utils");
+
+const { createFactory, createElement } =
+ require("devtools/client/shared/vendor/react");
+const ReactDOM = require("devtools/client/shared/vendor/react-dom");
+const { Provider } = require("devtools/client/shared/vendor/react-redux");
+
+const message = require("./utils/message");
+const App = createFactory(require("./app"));
+const Store = require("./store");
+const { changeLocation } = require("./actions/location");
+const { changeDisplayPixelRatio } = require("./actions/display-pixel-ratio");
+const { addViewport, resizeViewport } = require("./actions/viewports");
+const { loadDevices } = require("./actions/devices");
+
+// Exposed for use by tests
+window.require = require;
+
+let bootstrap = {
+
+ telemetry: new Telemetry(),
+
+ store: null,
+
+ init: Task.async(function* () {
+ // Load a special UA stylesheet to reset certain styles such as dropdown
+ // lists.
+ loadSheet(window,
+ "resource://devtools/client/responsive.html/responsive-ua.css",
+ "agent");
+ this.telemetry.toolOpened("responsive");
+ let store = this.store = Store();
+ let provider = createElement(Provider, { store }, App());
+ ReactDOM.render(provider, document.querySelector("#root"));
+ message.post(window, "init:done");
+ }),
+
+ destroy() {
+ this.store = null;
+ this.telemetry.toolClosed("responsive");
+ this.telemetry = null;
+ },
+
+ /**
+ * While most actions will be dispatched by React components, some external
+ * APIs that coordinate with the larger browser UI may also have actions to
+ * to dispatch. They can do so here.
+ */
+ dispatch(action) {
+ if (!this.store) {
+ // If actions are dispatched after store is destroyed, ignore them. This
+ // can happen in tests that close the tool quickly while async tasks like
+ // initDevices() below are still pending.
+ return;
+ }
+ this.store.dispatch(action);
+ },
+
+};
+
+// manager.js sends a message to signal init
+message.wait(window, "init").then(() => bootstrap.init());
+
+// manager.js sends a message to signal init is done, which can be used for delayed
+// startup work that shouldn't block initial load
+message.wait(window, "post-init").then(() => bootstrap.dispatch(loadDevices()));
+
+window.addEventListener("unload", function onUnload() {
+ window.removeEventListener("unload", onUnload);
+ bootstrap.destroy();
+});
+
+// Allows quick testing of actions from the console
+window.dispatch = action => bootstrap.dispatch(action);
+
+// Expose the store on window for testing
+Object.defineProperty(window, "store", {
+ get: () => bootstrap.store,
+ enumerable: true,
+});
+
+// Dispatch a `changeDisplayPixelRatio` action when the browser's pixel ratio is changing.
+// This is usually triggered when the user changes the monitor resolution, or when the
+// browser's window is dragged to a different display with a different pixel ratio.
+function onDPRChange() {
+ let dpr = window.devicePixelRatio;
+ let mql = window.matchMedia(`(resolution: ${dpr}dppx)`);
+
+ function listener() {
+ bootstrap.dispatch(changeDisplayPixelRatio(window.devicePixelRatio));
+ mql.removeListener(listener);
+ onDPRChange();
+ }
+
+ mql.addListener(listener);
+}
+
+/**
+ * Called by manager.js to add the initial viewport based on the original page.
+ */
+window.addInitialViewport = contentURI => {
+ try {
+ onDPRChange();
+ bootstrap.dispatch(changeLocation(contentURI));
+ bootstrap.dispatch(changeDisplayPixelRatio(window.devicePixelRatio));
+ bootstrap.dispatch(addViewport());
+ } catch (e) {
+ console.error(e);
+ }
+};
+
+/**
+ * Called by manager.js when tests want to check the viewport size.
+ */
+window.getViewportSize = () => {
+ let { width, height } = bootstrap.store.getState().viewports[0];
+ return { width, height };
+};
+
+/**
+ * Called by manager.js to set viewport size from tests, GCLI, etc.
+ */
+window.setViewportSize = ({ width, height }) => {
+ try {
+ bootstrap.dispatch(resizeViewport(0, width, height));
+ } catch (e) {
+ console.error(e);
+ }
+};
+
+/**
+ * Called by manager.js to access the viewport's browser, either for testing
+ * purposes or to reload it when touch simulation is enabled.
+ * A messageManager getter is added on the object to provide an easy access
+ * to the message manager without pulling the frame loader.
+ */
+window.getViewportBrowser = () => {
+ let browser = document.querySelector("iframe.browser");
+ if (!browser.messageManager) {
+ Object.defineProperty(browser, "messageManager", {
+ get() {
+ return this.frameLoader.messageManager;
+ },
+ configurable: true,
+ enumerable: true,
+ });
+ }
+ return browser;
+};
diff --git a/devtools/client/responsive.html/index.xhtml b/devtools/client/responsive.html/index.xhtml
new file mode 100644
index 000000000..72fe2f0f7
--- /dev/null
+++ b/devtools/client/responsive.html/index.xhtml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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/. -->
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <link rel="stylesheet" type="text/css"
+ href="resource://devtools/client/responsive.html/index.css"/>
+ <script type="application/javascript;version=1.8"
+ src="chrome://devtools/content/shared/theme-switching.js"></script>
+ <script type="application/javascript;version=1.8"
+ src="./index.js"></script>
+ </head>
+ <body class="theme-body" role="application">
+ <div id="root"/>
+ </body>
+</html>
diff --git a/devtools/client/responsive.html/manager.js b/devtools/client/responsive.html/manager.js
new file mode 100644
index 000000000..a3fbed366
--- /dev/null
+++ b/devtools/client/responsive.html/manager.js
@@ -0,0 +1,597 @@
+/* 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 promise = require("promise");
+const { Task } = require("devtools/shared/task");
+const EventEmitter = require("devtools/shared/event-emitter");
+const { getOwnerWindow } = require("sdk/tabs/utils");
+const { startup } = require("sdk/window/helpers");
+const message = require("./utils/message");
+const { swapToInnerBrowser } = require("./browser/swap");
+const { EmulationFront } = require("devtools/shared/fronts/emulation");
+const { getStr } = require("./utils/l10n");
+
+const TOOL_URL = "chrome://devtools/content/responsive.html/index.xhtml";
+
+loader.lazyRequireGetter(this, "DebuggerClient", "devtools/shared/client/main", true);
+loader.lazyRequireGetter(this, "DebuggerServer", "devtools/server/main", true);
+loader.lazyRequireGetter(this, "TargetFactory", "devtools/client/framework/target", true);
+loader.lazyRequireGetter(this, "gDevTools", "devtools/client/framework/devtools", true);
+loader.lazyRequireGetter(this, "throttlingProfiles",
+ "devtools/client/shared/network-throttling-profiles");
+
+/**
+ * ResponsiveUIManager is the external API for the browser UI, etc. to use when
+ * opening and closing the responsive UI.
+ *
+ * While the HTML UI is in an experimental stage, the older ResponsiveUIManager
+ * from devtools/client/responsivedesign/responsivedesign.jsm delegates to this
+ * object when the pref "devtools.responsive.html.enabled" is true.
+ */
+const ResponsiveUIManager = exports.ResponsiveUIManager = {
+ activeTabs: new Map(),
+
+ /**
+ * Toggle the responsive UI for a tab.
+ *
+ * @param window
+ * The main browser chrome window.
+ * @param tab
+ * The browser tab.
+ * @param options
+ * Other options associated with toggling. Currently includes:
+ * - `command`: Whether initiated via GCLI command bar or toolbox button
+ * @return Promise
+ * Resolved when the toggling has completed. If the UI has opened,
+ * it is resolved to the ResponsiveUI instance for this tab. If the
+ * the UI has closed, there is no resolution value.
+ */
+ toggle(window, tab, options) {
+ let action = this.isActiveForTab(tab) ? "close" : "open";
+ let completed = this[action + "IfNeeded"](window, tab, options);
+ completed.catch(console.error);
+ return completed;
+ },
+
+ /**
+ * Opens the responsive UI, if not already open.
+ *
+ * @param window
+ * The main browser chrome window.
+ * @param tab
+ * The browser tab.
+ * @param options
+ * Other options associated with opening. Currently includes:
+ * - `command`: Whether initiated via GCLI command bar or toolbox button
+ * @return Promise
+ * Resolved to the ResponsiveUI instance for this tab when opening is
+ * complete.
+ */
+ openIfNeeded: Task.async(function* (window, tab, options) {
+ if (!tab.linkedBrowser.isRemoteBrowser) {
+ this.showRemoteOnlyNotification(window, tab, options);
+ return promise.reject(new Error("RDM only available for remote tabs."));
+ }
+ // Remove this once we support this case in bug 1306975.
+ if (tab.linkedBrowser.hasAttribute("usercontextid")) {
+ this.showNoContainerTabsNotification(window, tab, options);
+ return promise.reject(new Error("RDM not available for container tabs."));
+ }
+ if (!this.isActiveForTab(tab)) {
+ this.initMenuCheckListenerFor(window);
+
+ let ui = new ResponsiveUI(window, tab);
+ this.activeTabs.set(tab, ui);
+ yield this.setMenuCheckFor(tab, window);
+ yield ui.inited;
+ this.emit("on", { tab });
+ }
+
+ return this.getResponsiveUIForTab(tab);
+ }),
+
+ /**
+ * Closes the responsive UI, if not already closed.
+ *
+ * @param window
+ * The main browser chrome window.
+ * @param tab
+ * The browser tab.
+ * @param options
+ * Other options associated with closing. Currently includes:
+ * - `command`: Whether initiated via GCLI command bar or toolbox button
+ * - `reason`: String detailing the specific cause for closing
+ * @return Promise
+ * Resolved (with no value) when closing is complete.
+ */
+ closeIfNeeded: Task.async(function* (window, tab, options) {
+ if (this.isActiveForTab(tab)) {
+ let ui = this.activeTabs.get(tab);
+ let destroyed = yield ui.destroy(options);
+ if (!destroyed) {
+ // Already in the process of destroying, abort.
+ return;
+ }
+ this.activeTabs.delete(tab);
+
+ if (!this.isActiveForWindow(window)) {
+ this.removeMenuCheckListenerFor(window);
+ }
+ this.emit("off", { tab });
+ yield this.setMenuCheckFor(tab, window);
+ }
+ }),
+
+ /**
+ * Returns true if responsive UI is active for a given tab.
+ *
+ * @param tab
+ * The browser tab.
+ * @return boolean
+ */
+ isActiveForTab(tab) {
+ return this.activeTabs.has(tab);
+ },
+
+ /**
+ * Returns true if responsive UI is active in any tab in the given window.
+ *
+ * @param window
+ * The main browser chrome window.
+ * @return boolean
+ */
+ isActiveForWindow(window) {
+ return [...this.activeTabs.keys()].some(t => getOwnerWindow(t) === window);
+ },
+
+ /**
+ * Return the responsive UI controller for a tab.
+ *
+ * @param tab
+ * The browser tab.
+ * @return ResponsiveUI
+ * The UI instance for this tab.
+ */
+ getResponsiveUIForTab(tab) {
+ return this.activeTabs.get(tab);
+ },
+
+ /**
+ * Handle GCLI commands.
+ *
+ * @param window
+ * The main browser chrome window.
+ * @param tab
+ * The browser tab.
+ * @param command
+ * The GCLI command name.
+ * @param args
+ * The GCLI command arguments.
+ */
+ handleGcliCommand(window, tab, command, args) {
+ let completed;
+ switch (command) {
+ case "resize to":
+ completed = this.openIfNeeded(window, tab, { command: true });
+ this.activeTabs.get(tab).setViewportSize(args);
+ break;
+ case "resize on":
+ completed = this.openIfNeeded(window, tab, { command: true });
+ break;
+ case "resize off":
+ completed = this.closeIfNeeded(window, tab, { command: true });
+ break;
+ case "resize toggle":
+ completed = this.toggle(window, tab, { command: true });
+ break;
+ default:
+ }
+ completed.catch(e => console.error(e));
+ },
+
+ handleMenuCheck({target}) {
+ ResponsiveUIManager.setMenuCheckFor(target);
+ },
+
+ initMenuCheckListenerFor(window) {
+ let { tabContainer } = window.gBrowser;
+ tabContainer.addEventListener("TabSelect", this.handleMenuCheck);
+ },
+
+ removeMenuCheckListenerFor(window) {
+ if (window && window.gBrowser && window.gBrowser.tabContainer) {
+ let { tabContainer } = window.gBrowser;
+ tabContainer.removeEventListener("TabSelect", this.handleMenuCheck);
+ }
+ },
+
+ setMenuCheckFor: Task.async(function* (tab, window = getOwnerWindow(tab)) {
+ yield startup(window);
+
+ let menu = window.document.getElementById("menu_responsiveUI");
+ if (menu) {
+ menu.setAttribute("checked", this.isActiveForTab(tab));
+ }
+ }),
+
+ showRemoteOnlyNotification(window, tab, options) {
+ this.showErrorNotification(window, tab, options, getStr("responsive.remoteOnly"));
+ },
+
+ showNoContainerTabsNotification(window, tab, options) {
+ this.showErrorNotification(window, tab, options,
+ getStr("responsive.noContainerTabs"));
+ },
+
+ showErrorNotification(window, tab, { command } = {}, msg) {
+ // Default to using the browser's per-tab notification box
+ let nbox = window.gBrowser.getNotificationBox(tab.linkedBrowser);
+
+ // If opening was initiated by GCLI command bar or toolbox button, check for an open
+ // toolbox for the tab. If one exists, use the toolbox's notification box so that the
+ // message is placed closer to the action taken by the user.
+ if (command) {
+ let target = TargetFactory.forTab(tab);
+ let toolbox = gDevTools.getToolbox(target);
+ if (toolbox) {
+ nbox = toolbox.notificationBox;
+ }
+ }
+
+ let value = "devtools-responsive-error";
+ if (nbox.getNotificationWithValue(value)) {
+ // Notification already displayed
+ return;
+ }
+
+ nbox.appendNotification(
+ msg,
+ value,
+ null,
+ nbox.PRIORITY_CRITICAL_MEDIUM,
+ []);
+ },
+};
+
+// GCLI commands in ../responsivedesign/resize-commands.js listen for events
+// from this object to know when the UI for a tab has opened or closed.
+EventEmitter.decorate(ResponsiveUIManager);
+
+/**
+ * ResponsiveUI manages the responsive design tool for a specific tab. The
+ * actual tool itself lives in a separate chrome:// document that is loaded into
+ * the tab upon opening responsive design. This object acts a helper to
+ * integrate the tool into the surrounding browser UI as needed.
+ */
+function ResponsiveUI(window, tab) {
+ this.browserWindow = window;
+ this.tab = tab;
+ this.inited = this.init();
+}
+
+ResponsiveUI.prototype = {
+
+ /**
+ * The main browser chrome window (that holds many tabs).
+ */
+ browserWindow: null,
+
+ /**
+ * The specific browser tab this responsive instance is for.
+ */
+ tab: null,
+
+ /**
+ * Promise resovled when the UI init has completed.
+ */
+ inited: null,
+
+ /**
+ * Flag set when destruction has begun.
+ */
+ destroying: false,
+
+ /**
+ * Flag set when destruction has ended.
+ */
+ destroyed: false,
+
+ /**
+ * A window reference for the chrome:// document that displays the responsive
+ * design tool. It is safe to reference this window directly even with e10s,
+ * as the tool UI is always loaded in the parent process. The web content
+ * contained *within* the tool UI on the other hand is loaded in the child
+ * process.
+ */
+ toolWindow: null,
+
+ /**
+ * Open RDM while preserving the state of the page. We use `swapFrameLoaders`
+ * to ensure all in-page state is preserved, just like when you move a tab to
+ * a new window.
+ *
+ * For more details, see /devtools/docs/responsive-design-mode.md.
+ */
+ init: Task.async(function* () {
+ let ui = this;
+
+ // Watch for tab close and window close so we can clean up RDM synchronously
+ this.tab.addEventListener("TabClose", this);
+ this.browserWindow.addEventListener("unload", this);
+
+ // Swap page content from the current tab into a viewport within RDM
+ this.swap = swapToInnerBrowser({
+ tab: this.tab,
+ containerURL: TOOL_URL,
+ getInnerBrowser: Task.async(function* (containerBrowser) {
+ let toolWindow = ui.toolWindow = containerBrowser.contentWindow;
+ toolWindow.addEventListener("message", ui);
+ yield message.request(toolWindow, "init");
+ toolWindow.addInitialViewport("about:blank");
+ yield message.wait(toolWindow, "browser-mounted");
+ return ui.getViewportBrowser();
+ })
+ });
+ yield this.swap.start();
+
+ this.tab.addEventListener("BeforeTabRemotenessChange", this);
+
+ // Notify the inner browser to start the frame script
+ yield message.request(this.toolWindow, "start-frame-script");
+
+ // Get the protocol ready to speak with emulation actor
+ yield this.connectToServer();
+
+ // Non-blocking message to tool UI to start any delayed init activities
+ message.post(this.toolWindow, "post-init");
+ }),
+
+ /**
+ * Close RDM and restore page content back into a regular tab.
+ *
+ * @param object
+ * Destroy options, which currently includes a `reason` string.
+ * @return boolean
+ * Whether this call is actually destroying. False means destruction
+ * was already in progress.
+ */
+ destroy: Task.async(function* (options) {
+ if (this.destroying) {
+ return false;
+ }
+ this.destroying = true;
+
+ // If our tab is about to be closed, there's not enough time to exit
+ // gracefully, but that shouldn't be a problem since the tab will go away.
+ // So, skip any yielding when we're about to close the tab.
+ let isWindowClosing = options && options.reason === "unload";
+ let isTabContentDestroying =
+ isWindowClosing || (options && (options.reason === "TabClose" ||
+ options.reason === "BeforeTabRemotenessChange"));
+
+ // Ensure init has finished before starting destroy
+ if (!isTabContentDestroying) {
+ yield this.inited;
+ }
+
+ this.tab.removeEventListener("TabClose", this);
+ this.tab.removeEventListener("BeforeTabRemotenessChange", this);
+ this.browserWindow.removeEventListener("unload", this);
+ this.toolWindow.removeEventListener("message", this);
+
+ if (!isTabContentDestroying) {
+ // Notify the inner browser to stop the frame script
+ yield message.request(this.toolWindow, "stop-frame-script");
+ }
+
+ // Destroy local state
+ let swap = this.swap;
+ this.browserWindow = null;
+ this.tab = null;
+ this.inited = null;
+ this.toolWindow = null;
+ this.swap = null;
+
+ // Close the debugger client used to speak with emulation actor.
+ // The actor handles clearing any overrides itself, so it's not necessary to clear
+ // anything on shutdown client side.
+ let clientClosed = this.client.close();
+ if (!isTabContentDestroying) {
+ yield clientClosed;
+ }
+ this.client = this.emulationFront = null;
+
+ if (!isWindowClosing) {
+ // Undo the swap and return the content back to a normal tab
+ swap.stop();
+ }
+
+ this.destroyed = true;
+
+ return true;
+ }),
+
+ connectToServer: Task.async(function* () {
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+ this.client = new DebuggerClient(DebuggerServer.connectPipe());
+ yield this.client.connect();
+ let { tab } = yield this.client.getTab();
+ this.emulationFront = EmulationFront(this.client, tab);
+ }),
+
+ handleEvent(event) {
+ let { browserWindow, tab } = this;
+
+ switch (event.type) {
+ case "message":
+ this.handleMessage(event);
+ break;
+ case "BeforeTabRemotenessChange":
+ case "TabClose":
+ case "unload":
+ ResponsiveUIManager.closeIfNeeded(browserWindow, tab, {
+ reason: event.type,
+ });
+ break;
+ }
+ },
+
+ handleMessage(event) {
+ if (event.origin !== "chrome://devtools") {
+ return;
+ }
+
+ switch (event.data.type) {
+ case "change-device":
+ this.onChangeDevice(event);
+ break;
+ case "change-network-throtting":
+ this.onChangeNetworkThrottling(event);
+ break;
+ case "change-pixel-ratio":
+ this.onChangePixelRatio(event);
+ break;
+ case "change-touch-simulation":
+ this.onChangeTouchSimulation(event);
+ break;
+ case "content-resize":
+ this.onContentResize(event);
+ break;
+ case "exit":
+ this.onExit();
+ break;
+ case "remove-device":
+ this.onRemoveDevice(event);
+ break;
+ }
+ },
+
+ onChangeDevice: Task.async(function* (event) {
+ let { userAgent, pixelRatio, touch } = event.data.device;
+ yield this.updateUserAgent(userAgent);
+ yield this.updateDPPX(pixelRatio);
+ yield this.updateTouchSimulation(touch);
+ // Used by tests
+ this.emit("device-changed");
+ }),
+
+ onChangeNetworkThrottling: Task.async(function* (event) {
+ let { enabled, profile } = event.data;
+ yield this.updateNetworkThrottling(enabled, profile);
+ // Used by tests
+ this.emit("network-throttling-changed");
+ }),
+
+ onChangePixelRatio(event) {
+ let { pixelRatio } = event.data;
+ this.updateDPPX(pixelRatio);
+ },
+
+ onChangeTouchSimulation(event) {
+ let { enabled } = event.data;
+ this.updateTouchSimulation(enabled);
+ },
+
+ onContentResize(event) {
+ let { width, height } = event.data;
+ this.emit("content-resize", {
+ width,
+ height,
+ });
+ },
+
+ onExit() {
+ let { browserWindow, tab } = this;
+ ResponsiveUIManager.closeIfNeeded(browserWindow, tab);
+ },
+
+ onRemoveDevice: Task.async(function* (event) {
+ yield this.updateUserAgent();
+ yield this.updateDPPX();
+ yield this.updateTouchSimulation();
+ // Used by tests
+ this.emit("device-removed");
+ }),
+
+ updateDPPX: Task.async(function* (dppx) {
+ if (!dppx) {
+ yield this.emulationFront.clearDPPXOverride();
+ return;
+ }
+ yield this.emulationFront.setDPPXOverride(dppx);
+ }),
+
+ updateNetworkThrottling: Task.async(function* (enabled, profile) {
+ if (!enabled) {
+ yield this.emulationFront.clearNetworkThrottling();
+ return;
+ }
+ let data = throttlingProfiles.find(({ id }) => id == profile);
+ let { download, upload, latency } = data;
+ yield this.emulationFront.setNetworkThrottling({
+ downloadThroughput: download,
+ uploadThroughput: upload,
+ latency,
+ });
+ }),
+
+ updateUserAgent: Task.async(function* (userAgent) {
+ if (!userAgent) {
+ yield this.emulationFront.clearUserAgentOverride();
+ return;
+ }
+ yield this.emulationFront.setUserAgentOverride(userAgent);
+ }),
+
+ updateTouchSimulation: Task.async(function* (enabled) {
+ if (!enabled) {
+ yield this.emulationFront.clearTouchEventsOverride();
+ return;
+ }
+ let reloadNeeded = yield this.emulationFront.setTouchEventsOverride(
+ Ci.nsIDocShell.TOUCHEVENTS_OVERRIDE_ENABLED
+ );
+ if (reloadNeeded) {
+ this.getViewportBrowser().reload();
+ }
+ }),
+
+ /**
+ * Helper for tests. Assumes a single viewport for now.
+ */
+ getViewportSize() {
+ return this.toolWindow.getViewportSize();
+ },
+
+ /**
+ * Helper for tests, GCLI, etc. Assumes a single viewport for now.
+ */
+ setViewportSize: Task.async(function* (size) {
+ yield this.inited;
+ this.toolWindow.setViewportSize(size);
+ }),
+
+ /**
+ * Helper for tests/reloading the viewport. Assumes a single viewport for now.
+ */
+ getViewportBrowser() {
+ return this.toolWindow.getViewportBrowser();
+ },
+
+ /**
+ * Helper for contacting the viewport content. Assumes a single viewport for now.
+ */
+ getViewportMessageManager() {
+ return this.getViewportBrowser().messageManager;
+ },
+
+};
+
+EventEmitter.decorate(ResponsiveUI.prototype);
diff --git a/devtools/client/responsive.html/moz.build b/devtools/client/responsive.html/moz.build
new file mode 100644
index 000000000..79fbf3ae4
--- /dev/null
+++ b/devtools/client/responsive.html/moz.build
@@ -0,0 +1,28 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DIRS += [
+ 'actions',
+ 'browser',
+ 'components',
+ 'images',
+ 'reducers',
+ 'utils',
+]
+
+DevToolsModules(
+ 'app.js',
+ 'constants.js',
+ 'index.css',
+ 'manager.js',
+ 'reducers.js',
+ 'responsive-ua.css',
+ 'store.js',
+ 'types.js',
+)
+
+XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
+BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
diff --git a/devtools/client/responsive.html/reducers.js b/devtools/client/responsive.html/reducers.js
new file mode 100644
index 000000000..f36cd509a
--- /dev/null
+++ b/devtools/client/responsive.html/reducers.js
@@ -0,0 +1,13 @@
+/* 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";
+
+exports.devices = require("./reducers/devices");
+exports.displayPixelRatio = require("./reducers/display-pixel-ratio");
+exports.location = require("./reducers/location");
+exports.networkThrottling = require("./reducers/network-throttling");
+exports.screenshot = require("./reducers/screenshot");
+exports.touchSimulation = require("./reducers/touch-simulation");
+exports.viewports = require("./reducers/viewports");
diff --git a/devtools/client/responsive.html/reducers/devices.js b/devtools/client/responsive.html/reducers/devices.js
new file mode 100644
index 000000000..e78632b24
--- /dev/null
+++ b/devtools/client/responsive.html/reducers/devices.js
@@ -0,0 +1,86 @@
+/* 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 {
+ ADD_DEVICE,
+ ADD_DEVICE_TYPE,
+ LOAD_DEVICE_LIST_START,
+ LOAD_DEVICE_LIST_ERROR,
+ LOAD_DEVICE_LIST_END,
+ UPDATE_DEVICE_DISPLAYED,
+ UPDATE_DEVICE_MODAL_OPEN,
+} = require("../actions/index");
+
+const Types = require("../types");
+
+const INITIAL_DEVICES = {
+ types: [],
+ isModalOpen: false,
+ listState: Types.deviceListState.INITIALIZED,
+};
+
+let reducers = {
+
+ [ADD_DEVICE](devices, { device, deviceType }) {
+ return Object.assign({}, devices, {
+ [deviceType]: [...devices[deviceType], device],
+ });
+ },
+
+ [ADD_DEVICE_TYPE](devices, { deviceType }) {
+ return Object.assign({}, devices, {
+ types: [...devices.types, deviceType],
+ [deviceType]: [],
+ });
+ },
+
+ [UPDATE_DEVICE_DISPLAYED](devices, { device, deviceType, displayed }) {
+ let newDevices = devices[deviceType].map(d => {
+ if (d == device) {
+ d.displayed = displayed;
+ }
+
+ return d;
+ });
+
+ return Object.assign({}, devices, {
+ [deviceType]: newDevices,
+ });
+ },
+
+ [LOAD_DEVICE_LIST_START](devices, action) {
+ return Object.assign({}, devices, {
+ listState: Types.deviceListState.LOADING,
+ });
+ },
+
+ [LOAD_DEVICE_LIST_ERROR](devices, action) {
+ return Object.assign({}, devices, {
+ listState: Types.deviceListState.ERROR,
+ });
+ },
+
+ [LOAD_DEVICE_LIST_END](devices, action) {
+ return Object.assign({}, devices, {
+ listState: Types.deviceListState.LOADED,
+ });
+ },
+
+ [UPDATE_DEVICE_MODAL_OPEN](devices, { isOpen }) {
+ return Object.assign({}, devices, {
+ isModalOpen: isOpen,
+ });
+ },
+
+};
+
+module.exports = function (devices = INITIAL_DEVICES, action) {
+ let reducer = reducers[action.type];
+ if (!reducer) {
+ return devices;
+ }
+ return reducer(devices, action);
+};
diff --git a/devtools/client/responsive.html/reducers/display-pixel-ratio.js b/devtools/client/responsive.html/reducers/display-pixel-ratio.js
new file mode 100644
index 000000000..3f127c206
--- /dev/null
+++ b/devtools/client/responsive.html/reducers/display-pixel-ratio.js
@@ -0,0 +1,26 @@
+/* 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/. */
+
+/* eslint-env browser */
+
+"use strict";
+
+const { CHANGE_DISPLAY_PIXEL_RATIO } = require("../actions/index");
+const INITIAL_DISPLAY_PIXEL_RATIO = 0;
+
+let reducers = {
+
+ [CHANGE_DISPLAY_PIXEL_RATIO](_, action) {
+ return action.displayPixelRatio;
+ },
+
+};
+
+module.exports = function (displayPixelRatio = INITIAL_DISPLAY_PIXEL_RATIO, action) {
+ let reducer = reducers[action.type];
+ if (!reducer) {
+ return displayPixelRatio;
+ }
+ return reducer(displayPixelRatio, action);
+};
diff --git a/devtools/client/responsive.html/reducers/location.js b/devtools/client/responsive.html/reducers/location.js
new file mode 100644
index 000000000..2063c9776
--- /dev/null
+++ b/devtools/client/responsive.html/reducers/location.js
@@ -0,0 +1,25 @@
+/* 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 { CHANGE_LOCATION } = require("../actions/index");
+
+const INITIAL_LOCATION = "about:blank";
+
+let reducers = {
+
+ [CHANGE_LOCATION](_, action) {
+ return action.location;
+ },
+
+};
+
+module.exports = function (location = INITIAL_LOCATION, action) {
+ let reducer = reducers[action.type];
+ if (!reducer) {
+ return location;
+ }
+ return reducer(location, action);
+};
diff --git a/devtools/client/responsive.html/reducers/moz.build b/devtools/client/responsive.html/reducers/moz.build
new file mode 100644
index 000000000..f1e9668f0
--- /dev/null
+++ b/devtools/client/responsive.html/reducers/moz.build
@@ -0,0 +1,15 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'devices.js',
+ 'display-pixel-ratio.js',
+ 'location.js',
+ 'network-throttling.js',
+ 'screenshot.js',
+ 'touch-simulation.js',
+ 'viewports.js',
+)
diff --git a/devtools/client/responsive.html/reducers/network-throttling.js b/devtools/client/responsive.html/reducers/network-throttling.js
new file mode 100644
index 000000000..f892553c1
--- /dev/null
+++ b/devtools/client/responsive.html/reducers/network-throttling.js
@@ -0,0 +1,33 @@
+/* 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 {
+ CHANGE_NETWORK_THROTTLING,
+} = require("../actions/index");
+
+const INITIAL_NETWORK_THROTTLING = {
+ enabled: false,
+ profile: "",
+};
+
+let reducers = {
+
+ [CHANGE_NETWORK_THROTTLING](throttling, { enabled, profile }) {
+ return {
+ enabled,
+ profile,
+ };
+ },
+
+};
+
+module.exports = function (throttling = INITIAL_NETWORK_THROTTLING, action) {
+ let reducer = reducers[action.type];
+ if (!reducer) {
+ return throttling;
+ }
+ return reducer(throttling, action);
+};
diff --git a/devtools/client/responsive.html/reducers/screenshot.js b/devtools/client/responsive.html/reducers/screenshot.js
new file mode 100644
index 000000000..9d24d8c5b
--- /dev/null
+++ b/devtools/client/responsive.html/reducers/screenshot.js
@@ -0,0 +1,31 @@
+/* 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 {
+ TAKE_SCREENSHOT_END,
+ TAKE_SCREENSHOT_START,
+} = require("../actions/index");
+
+const INITIAL_SCREENSHOT = { isCapturing: false };
+
+let reducers = {
+
+ [TAKE_SCREENSHOT_END](screenshot, action) {
+ return Object.assign({}, screenshot, { isCapturing: false });
+ },
+
+ [TAKE_SCREENSHOT_START](screenshot, action) {
+ return Object.assign({}, screenshot, { isCapturing: true });
+ },
+};
+
+module.exports = function (screenshot = INITIAL_SCREENSHOT, action) {
+ let reducer = reducers[action.type];
+ if (!reducer) {
+ return screenshot;
+ }
+ return reducer(screenshot, action);
+};
diff --git a/devtools/client/responsive.html/reducers/touch-simulation.js b/devtools/client/responsive.html/reducers/touch-simulation.js
new file mode 100644
index 000000000..b3203b644
--- /dev/null
+++ b/devtools/client/responsive.html/reducers/touch-simulation.js
@@ -0,0 +1,31 @@
+/* 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 {
+ CHANGE_TOUCH_SIMULATION,
+} = require("../actions/index");
+
+const INITIAL_TOUCH_SIMULATION = {
+ enabled: false,
+};
+
+let reducers = {
+
+ [CHANGE_TOUCH_SIMULATION](touchSimulation, { enabled }) {
+ return Object.assign({}, touchSimulation, {
+ enabled,
+ });
+ },
+
+};
+
+module.exports = function (touchSimulation = INITIAL_TOUCH_SIMULATION, action) {
+ let reducer = reducers[action.type];
+ if (!reducer) {
+ return touchSimulation;
+ }
+ return reducer(touchSimulation, action);
+};
diff --git a/devtools/client/responsive.html/reducers/viewports.js b/devtools/client/responsive.html/reducers/viewports.js
new file mode 100644
index 000000000..ee130ceaf
--- /dev/null
+++ b/devtools/client/responsive.html/reducers/viewports.js
@@ -0,0 +1,118 @@
+/* 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 {
+ ADD_VIEWPORT,
+ CHANGE_DEVICE,
+ CHANGE_PIXEL_RATIO,
+ REMOVE_DEVICE,
+ RESIZE_VIEWPORT,
+ ROTATE_VIEWPORT,
+} = require("../actions/index");
+
+let nextViewportId = 0;
+
+const INITIAL_VIEWPORTS = [];
+const INITIAL_VIEWPORT = {
+ id: nextViewportId++,
+ device: "",
+ width: 320,
+ height: 480,
+ pixelRatio: {
+ value: 0,
+ },
+};
+
+let reducers = {
+
+ [ADD_VIEWPORT](viewports) {
+ // For the moment, there can be at most one viewport.
+ if (viewports.length === 1) {
+ return viewports;
+ }
+ return [...viewports, Object.assign({}, INITIAL_VIEWPORT)];
+ },
+
+ [CHANGE_DEVICE](viewports, { id, device }) {
+ return viewports.map(viewport => {
+ if (viewport.id !== id) {
+ return viewport;
+ }
+
+ return Object.assign({}, viewport, {
+ device,
+ });
+ });
+ },
+
+ [CHANGE_PIXEL_RATIO](viewports, { id, pixelRatio }) {
+ return viewports.map(viewport => {
+ if (viewport.id !== id) {
+ return viewport;
+ }
+
+ return Object.assign({}, viewport, {
+ pixelRatio: {
+ value: pixelRatio
+ },
+ });
+ });
+ },
+
+ [REMOVE_DEVICE](viewports, { id }) {
+ return viewports.map(viewport => {
+ if (viewport.id !== id) {
+ return viewport;
+ }
+
+ return Object.assign({}, viewport, {
+ device: "",
+ });
+ });
+ },
+
+ [RESIZE_VIEWPORT](viewports, { id, width, height }) {
+ return viewports.map(viewport => {
+ if (viewport.id !== id) {
+ return viewport;
+ }
+
+ if (!width) {
+ width = viewport.width;
+ }
+ if (!height) {
+ height = viewport.height;
+ }
+
+ return Object.assign({}, viewport, {
+ width,
+ height,
+ });
+ });
+ },
+
+ [ROTATE_VIEWPORT](viewports, { id }) {
+ return viewports.map(viewport => {
+ if (viewport.id !== id) {
+ return viewport;
+ }
+
+ return Object.assign({}, viewport, {
+ width: viewport.height,
+ height: viewport.width,
+ });
+ });
+ },
+
+};
+
+module.exports = function (viewports = INITIAL_VIEWPORTS, action) {
+ let reducer = reducers[action.type];
+ if (!reducer) {
+ return viewports;
+ }
+ return reducer(viewports, action);
+};
diff --git a/devtools/client/responsive.html/responsive-ua.css b/devtools/client/responsive.html/responsive-ua.css
new file mode 100644
index 000000000..6d442b1bb
--- /dev/null
+++ b/devtools/client/responsive.html/responsive-ua.css
@@ -0,0 +1,6 @@
+@namespace url(http://www.w3.org/1999/xhtml);
+
+/* Reset default UA styles for dropdown options */
+*|*::-moz-dropdown-list {
+ border: 0 !important;
+}
diff --git a/devtools/client/responsive.html/store.js b/devtools/client/responsive.html/store.js
new file mode 100644
index 000000000..0e32819b3
--- /dev/null
+++ b/devtools/client/responsive.html/store.js
@@ -0,0 +1,33 @@
+/* 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 { combineReducers } = require("devtools/client/shared/vendor/redux");
+const createStore = require("devtools/client/shared/redux/create-store");
+const reducers = require("./reducers");
+const flags = require("devtools/shared/flags");
+
+module.exports = function () {
+ let shouldLog = false;
+ let history;
+
+ // If testing, store the action history in an array
+ // we'll later attach to the store
+ if (flags.testing) {
+ history = [];
+ shouldLog = true;
+ }
+
+ let store = createStore({
+ log: shouldLog,
+ history
+ })(combineReducers(reducers), {});
+
+ if (history) {
+ store.history = history;
+ }
+
+ return store;
+};
diff --git a/devtools/client/responsive.html/test/browser/.eslintrc.js b/devtools/client/responsive.html/test/browser/.eslintrc.js
new file mode 100644
index 000000000..698ae9181
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../../../.eslintrc.mochitests.js"
+};
diff --git a/devtools/client/responsive.html/test/browser/browser.ini b/devtools/client/responsive.html/test/browser/browser.ini
new file mode 100644
index 000000000..71cf6d9b6
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser.ini
@@ -0,0 +1,44 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+# !e10s: RDM only works for remote tabs
+skip-if = !e10s
+support-files =
+ devices.json
+ doc_page_state.html
+ geolocation.html
+ head.js
+ touch.html
+ !/devtools/client/commandline/test/helpers.js
+ !/devtools/client/framework/test/shared-head.js
+ !/devtools/client/framework/test/shared-redux-head.js
+ !/devtools/client/inspector/test/shared-head.js
+ !/devtools/client/shared/test/test-actor.js
+ !/devtools/client/shared/test/test-actor-registry.js
+
+[browser_device_change.js]
+[browser_device_modal_error.js]
+[browser_device_modal_exit.js]
+[browser_device_modal_submit.js]
+[browser_device_width.js]
+[browser_dpr_change.js]
+[browser_exit_button.js]
+[browser_frame_script_active.js]
+[browser_menu_item_01.js]
+[browser_menu_item_02.js]
+[browser_mouse_resize.js]
+[browser_navigation.js]
+[browser_network_throttling.js]
+[browser_page_state.js]
+[browser_permission_doorhanger.js]
+[browser_resize_cmd.js]
+[browser_screenshot_button.js]
+[browser_tab_close.js]
+[browser_tab_remoteness_change.js]
+[browser_toolbox_computed_view.js]
+[browser_toolbox_rule_view.js]
+[browser_toolbox_swap_browsers.js]
+[browser_touch_device.js]
+[browser_touch_simulation.js]
+[browser_viewport_basics.js]
+[browser_window_close.js]
diff --git a/devtools/client/responsive.html/test/browser/browser_device_change.js b/devtools/client/responsive.html/test/browser/browser_device_change.js
new file mode 100644
index 000000000..b88f73522
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_device_change.js
@@ -0,0 +1,95 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests changing viewport device
+const TEST_URL = "data:text/html;charset=utf-8,Device list test";
+
+const DEFAULT_DPPX = window.devicePixelRatio;
+const DEFAULT_UA = Cc["@mozilla.org/network/protocol;1?name=http"]
+ .getService(Ci.nsIHttpProtocolHandler)
+ .userAgent;
+
+const Types = require("devtools/client/responsive.html/types");
+
+const testDevice = {
+ "name": "Fake Phone RDM Test",
+ "width": 320,
+ "height": 570,
+ "pixelRatio": 5.5,
+ "userAgent": "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "custom",
+ "featured": true,
+};
+
+// Add the new device to the list
+addDeviceForTest(testDevice);
+
+addRDMTask(TEST_URL, function* ({ ui, manager }) {
+ let { store } = ui.toolWindow;
+
+ // Wait until the viewport has been added and the device list has been loaded
+ yield waitUntilState(store, state => state.viewports.length == 1
+ && state.devices.listState == Types.deviceListState.LOADED);
+
+ // Test defaults
+ testViewportDimensions(ui, 320, 480);
+ yield testUserAgent(ui, DEFAULT_UA);
+ yield testDevicePixelRatio(ui, DEFAULT_DPPX);
+ yield testTouchEventsOverride(ui, false);
+ testViewportDeviceSelectLabel(ui, "no device selected");
+
+ // Test device with custom properties
+ yield selectDevice(ui, "Fake Phone RDM Test");
+ yield waitForViewportResizeTo(ui, testDevice.width, testDevice.height);
+ yield testUserAgent(ui, testDevice.userAgent);
+ yield testDevicePixelRatio(ui, testDevice.pixelRatio);
+ yield testTouchEventsOverride(ui, true);
+
+ // Test resetting device when resizing viewport
+ let deviceRemoved = once(ui, "device-removed");
+ yield testViewportResize(ui, ".viewport-vertical-resize-handle",
+ [-10, -10], [testDevice.width, testDevice.height - 10], [0, -10], ui);
+ yield deviceRemoved;
+ yield testUserAgent(ui, DEFAULT_UA);
+ yield testDevicePixelRatio(ui, DEFAULT_DPPX);
+ yield testTouchEventsOverride(ui, false);
+ testViewportDeviceSelectLabel(ui, "no device selected");
+
+ // Test device with generic properties
+ yield selectDevice(ui, "Laptop (1366 x 768)");
+ yield waitForViewportResizeTo(ui, 1366, 768);
+ yield testUserAgent(ui, DEFAULT_UA);
+ yield testDevicePixelRatio(ui, 1);
+ yield testTouchEventsOverride(ui, false);
+});
+
+function testViewportDimensions(ui, w, h) {
+ let viewport = ui.toolWindow.document.querySelector(".viewport-content");
+
+ is(ui.toolWindow.getComputedStyle(viewport).getPropertyValue("width"),
+ `${w}px`, `Viewport should have width of ${w}px`);
+ is(ui.toolWindow.getComputedStyle(viewport).getPropertyValue("height"),
+ `${h}px`, `Viewport should have height of ${h}px`);
+}
+
+function* testUserAgent(ui, expected) {
+ let ua = yield ContentTask.spawn(ui.getViewportBrowser(), {}, function* () {
+ return content.navigator.userAgent;
+ });
+ is(ua, expected, `UA should be set to ${expected}`);
+}
+
+function* testDevicePixelRatio(ui, expected) {
+ let dppx = yield getViewportDevicePixelRatio(ui);
+ is(dppx, expected, `devicePixelRatio should be set to ${expected}`);
+}
+
+function* getViewportDevicePixelRatio(ui) {
+ return yield ContentTask.spawn(ui.getViewportBrowser(), {}, function* () {
+ return content.devicePixelRatio;
+ });
+}
diff --git a/devtools/client/responsive.html/test/browser/browser_device_modal_error.js b/devtools/client/responsive.html/test/browser/browser_device_modal_error.js
new file mode 100644
index 000000000..d9308eb6c
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_device_modal_error.js
@@ -0,0 +1,35 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test to check that RDM can handle properly an error in the device list
+
+const TEST_URL = "data:text/html;charset=utf-8,";
+const Types = require("devtools/client/responsive.html/types");
+const { getStr } = require("devtools/client/responsive.html/utils/l10n");
+
+// Set a wrong URL for the device list file
+add_task(function* () {
+ yield SpecialPowers.pushPrefEnv({
+ set: [["devtools.devices.url", TEST_URI_ROOT + "wrong_devices_file.json"]],
+ });
+});
+
+addRDMTask(TEST_URL, function* ({ ui }) {
+ let { store, document } = ui.toolWindow;
+ let select = document.querySelector(".viewport-device-selector");
+
+ // Wait until the viewport has been added and the device list state indicates
+ // an error
+ yield waitUntilState(store, state => state.viewports.length == 1
+ && state.devices.listState == Types.deviceListState.ERROR);
+
+ // The device selector placeholder should be set accordingly
+ let placeholder = select.options[select.selectedIndex].innerHTML;
+ ok(placeholder == getStr("responsive.deviceListError"),
+ "Device selector indicates an error");
+
+ // The device selector should be disabled
+ ok(select.disabled, "Device selector is disabled");
+});
diff --git a/devtools/client/responsive.html/test/browser/browser_device_modal_exit.js b/devtools/client/responsive.html/test/browser/browser_device_modal_exit.js
new file mode 100644
index 000000000..30d057ebe
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_device_modal_exit.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test submitting display device changes on the device modal
+
+const TEST_URL = "data:text/html;charset=utf-8,";
+const Types = require("devtools/client/responsive.html/types");
+
+addRDMTask(TEST_URL, function* ({ ui }) {
+ let { store, document } = ui.toolWindow;
+ let modal = document.querySelector("#device-modal-wrapper");
+ let closeButton = document.querySelector("#device-close-button");
+
+ // Wait until the viewport has been added and the device list has been loaded
+ yield waitUntilState(store, state => state.viewports.length == 1
+ && state.devices.listState == Types.deviceListState.LOADED);
+
+ openDeviceModal(ui);
+
+ let preferredDevicesBefore = _loadPreferredDevices();
+
+ info("Check the first unchecked device and exit the modal.");
+ let uncheckedCb = [...document.querySelectorAll(".device-input-checkbox")]
+ .filter(cb => !cb.checked)[0];
+ let value = uncheckedCb.value;
+ uncheckedCb.click();
+ closeButton.click();
+
+ ok(modal.classList.contains("closed") && !modal.classList.contains("opened"),
+ "The device modal is closed on exit.");
+
+ info("Check that the device list remains unchanged after exitting.");
+ let preferredDevicesAfter = _loadPreferredDevices();
+
+ is(preferredDevicesBefore.added.size, preferredDevicesAfter.added.size,
+ "Got expected number of added devices.");
+
+ is(preferredDevicesBefore.removed.size, preferredDevicesAfter.removed.size,
+ "Got expected number of removed devices.");
+
+ ok(!preferredDevicesAfter.removed.has(value),
+ value + " was not added to removed device list.");
+});
diff --git a/devtools/client/responsive.html/test/browser/browser_device_modal_submit.js b/devtools/client/responsive.html/test/browser/browser_device_modal_submit.js
new file mode 100644
index 000000000..90f364ce7
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_device_modal_submit.js
@@ -0,0 +1,146 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test submitting display device changes on the device modal
+const { getDevices } = require("devtools/client/shared/devices");
+
+const addedDevice = {
+ "name": "Fake Phone RDM Test",
+ "width": 320,
+ "height": 570,
+ "pixelRatio": 1.5,
+ "userAgent": "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "custom",
+ "featured": true,
+};
+
+const TEST_URL = "data:text/html;charset=utf-8,";
+const Types = require("devtools/client/responsive.html/types");
+
+addRDMTask(TEST_URL, function* ({ ui }) {
+ let { store, document } = ui.toolWindow;
+ let modal = document.querySelector("#device-modal-wrapper");
+ let select = document.querySelector(".viewport-device-selector");
+ let submitButton = document.querySelector("#device-submit-button");
+
+ // Wait until the viewport has been added and the device list has been loaded
+ yield waitUntilState(store, state => state.viewports.length == 1
+ && state.devices.listState == Types.deviceListState.LOADED);
+
+ openDeviceModal(ui);
+
+ info("Checking displayed device checkboxes are checked in the device modal.");
+ let checkedCbs = [...document.querySelectorAll(".device-input-checkbox")]
+ .filter(cb => cb.checked);
+
+ let remoteList = yield getDevices();
+
+ let featuredCount = remoteList.TYPES.reduce((total, type) => {
+ return total + remoteList[type].reduce((subtotal, device) => {
+ return subtotal + ((device.os != "fxos" && device.featured) ? 1 : 0);
+ }, 0);
+ }, 0);
+
+ is(featuredCount, checkedCbs.length,
+ "Got expected number of displayed devices.");
+
+ for (let cb of checkedCbs) {
+ ok(Object.keys(remoteList).filter(type => remoteList[type][cb.value]),
+ cb.value + " is correctly checked.");
+ }
+
+ // Tests where the user adds a non-featured device
+ info("Check the first unchecked device and submit new device list.");
+ let uncheckedCb = [...document.querySelectorAll(".device-input-checkbox")]
+ .filter(cb => !cb.checked)[0];
+ let value = uncheckedCb.value;
+ uncheckedCb.click();
+ submitButton.click();
+
+ ok(modal.classList.contains("closed") && !modal.classList.contains("opened"),
+ "The device modal is closed on submit.");
+
+ info("Checking that the new device is added to the user preference list.");
+ let preferredDevices = _loadPreferredDevices();
+ ok(preferredDevices.added.has(value), value + " in user added list.");
+
+ info("Checking new device is added to the device selector.");
+ let options = [...select.options];
+ is(options.length - 2, featuredCount + 1,
+ "Got expected number of devices in device selector.");
+ ok(options.filter(o => o.value === value)[0],
+ value + " added to the device selector.");
+
+ info("Reopen device modal and check new device is correctly checked");
+ openDeviceModal(ui);
+ ok([...document.querySelectorAll(".device-input-checkbox")]
+ .filter(cb => cb.checked && cb.value === value)[0],
+ value + " is checked in the device modal.");
+
+ // Tests where the user removes a featured device
+ info("Uncheck the first checked device different than the previous one");
+ let checkedCb = [...document.querySelectorAll(".device-input-checkbox")]
+ .filter(cb => cb.checked && cb.value != value)[0];
+ let checkedVal = checkedCb.value;
+ checkedCb.click();
+ submitButton.click();
+
+ info("Checking that the device is removed from the user preference list.");
+ preferredDevices = _loadPreferredDevices();
+ ok(preferredDevices.removed.has(checkedVal), checkedVal + " in removed list");
+
+ info("Checking that the device is not in the device selector.");
+ options = [...select.options];
+ is(options.length - 2, featuredCount,
+ "Got expected number of devices in device selector.");
+ ok(!options.filter(o => o.value === checkedVal)[0],
+ checkedVal + " removed from the device selector.");
+
+ info("Reopen device modal and check device is correctly unchecked");
+ openDeviceModal(ui);
+ ok([...document.querySelectorAll(".device-input-checkbox")]
+ .filter(cb => !cb.checked && cb.value === checkedVal)[0],
+ checkedVal + " is unchecked in the device modal.");
+
+ // Let's add a dummy device to simulate featured flag changes for next test
+ addDeviceForTest(addedDevice);
+});
+
+addRDMTask(TEST_URL, function* ({ ui }) {
+ let { store, document } = ui.toolWindow;
+ let select = document.querySelector(".viewport-device-selector");
+
+ // Wait until the viewport has been added and the device list has been loaded
+ yield waitUntilState(store, state => state.viewports.length == 1
+ && state.devices.listState == Types.deviceListState.LOADED);
+
+ openDeviceModal(ui);
+
+ let remoteList = yield getDevices();
+ let featuredCount = remoteList.TYPES.reduce((total, type) => {
+ return total + remoteList[type].reduce((subtotal, device) => {
+ return subtotal + ((device.os != "fxos" && device.featured) ? 1 : 0);
+ }, 0);
+ }, 0);
+ let preferredDevices = _loadPreferredDevices();
+
+ // Tests to prove that reloading the RDM didn't break our device list
+ info("Checking new featured device appears in the device selector.");
+ let options = [...select.options];
+ is(options.length - 2, featuredCount
+ - preferredDevices.removed.size + preferredDevices.added.size,
+ "Got expected number of devices in device selector.");
+
+ ok(options.filter(o => o.value === addedDevice.name)[0],
+ "dummy device added to the device selector.");
+
+ ok(options.filter(o => preferredDevices.added.has(o.value))[0],
+ "device added by user still in the device selector.");
+
+ ok(!options.filter(o => preferredDevices.removed.has(o.value))[0],
+ "device removed by user not in the device selector.");
+});
diff --git a/devtools/client/responsive.html/test/browser/browser_device_width.js b/devtools/client/responsive.html/test/browser/browser_device_width.js
new file mode 100644
index 000000000..9489d8f0b
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_device_width.js
@@ -0,0 +1,66 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URL = "data:text/html;charset=utf-8,";
+
+addRDMTask(TEST_URL, function* ({ ui, manager }) {
+ ok(ui, "An instance of the RDM should be attached to the tab.");
+ yield setViewportSize(ui, manager, 110, 500);
+
+ info("Checking initial width/height properties.");
+ yield doInitialChecks(ui);
+
+ info("Changing the RDM size");
+ yield setViewportSize(ui, manager, 90, 500);
+
+ info("Checking for screen props");
+ yield checkScreenProps(ui);
+
+ info("Setting docShell.deviceSizeIsPageSize to false");
+ yield ContentTask.spawn(ui.getViewportBrowser(), {}, function* () {
+ let docShell = content.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShell);
+ docShell.deviceSizeIsPageSize = false;
+ });
+
+ info("Checking for screen props once again.");
+ yield checkScreenProps2(ui);
+});
+
+function* doInitialChecks(ui) {
+ let { innerWidth, matchesMedia } = yield grabContentInfo(ui);
+ is(innerWidth, 110, "initial width should be 110px");
+ ok(!matchesMedia, "media query shouldn't match.");
+}
+
+function* checkScreenProps(ui) {
+ let { matchesMedia, screen } = yield grabContentInfo(ui);
+ ok(matchesMedia, "media query should match");
+ isnot(window.screen.width, screen.width,
+ "screen.width should not be the size of the screen.");
+ is(screen.width, 90, "screen.width should be the page width");
+ is(screen.height, 500, "screen.height should be the page height");
+}
+
+function* checkScreenProps2(ui) {
+ let { matchesMedia, screen } = yield grabContentInfo(ui);
+ ok(!matchesMedia, "media query should be re-evaluated.");
+ is(window.screen.width, screen.width,
+ "screen.width should be the size of the screen.");
+}
+
+function grabContentInfo(ui) {
+ return ContentTask.spawn(ui.getViewportBrowser(), {}, function* () {
+ return {
+ screen: {
+ width: content.screen.width,
+ height: content.screen.height
+ },
+ innerWidth: content.innerWidth,
+ matchesMedia: content.matchMedia("(max-device-width:100px)").matches
+ };
+ });
+}
diff --git a/devtools/client/responsive.html/test/browser/browser_dpr_change.js b/devtools/client/responsive.html/test/browser/browser_dpr_change.js
new file mode 100644
index 000000000..4c70087bf
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_dpr_change.js
@@ -0,0 +1,140 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests changing viewport DPR
+const TEST_URL = "data:text/html;charset=utf-8,DPR list test";
+const DEFAULT_DPPX = window.devicePixelRatio;
+const VIEWPORT_DPPX = DEFAULT_DPPX + 2;
+const Types = require("devtools/client/responsive.html/types");
+
+const testDevice = {
+ "name": "Fake Phone RDM Test",
+ "width": 320,
+ "height": 470,
+ "pixelRatio": 5.5,
+ "userAgent": "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "custom",
+ "featured": true,
+};
+
+// Add the new device to the list
+addDeviceForTest(testDevice);
+
+addRDMTask(TEST_URL, function* ({ ui, manager }) {
+ yield waitStartup(ui);
+
+ yield testDefaults(ui);
+ yield testChangingDevice(ui);
+ yield testResetWhenResizingViewport(ui);
+ yield testChangingDPR(ui);
+});
+
+function* waitStartup(ui) {
+ let { store } = ui.toolWindow;
+
+ // Wait until the viewport has been added and the device list has been loaded
+ yield waitUntilState(store, state => state.viewports.length == 1
+ && state.devices.listState == Types.deviceListState.LOADED);
+}
+
+function* testDefaults(ui) {
+ info("Test Defaults");
+
+ yield testDevicePixelRatio(ui, window.devicePixelRatio);
+ testViewportDPRSelect(ui, {value: window.devicePixelRatio, disabled: false});
+ testViewportDeviceSelectLabel(ui, "no device selected");
+}
+
+function* testChangingDevice(ui) {
+ info("Test Changing Device");
+
+ let waitPixelRatioChange = onceDevicePixelRatioChange(ui);
+
+ yield selectDevice(ui, testDevice.name);
+ yield waitForViewportResizeTo(ui, testDevice.width, testDevice.height);
+ yield waitPixelRatioChange;
+ yield testDevicePixelRatio(ui, testDevice.pixelRatio);
+ testViewportDPRSelect(ui, {value: testDevice.pixelRatio, disabled: true});
+ testViewportDeviceSelectLabel(ui, testDevice.name);
+}
+
+function* testResetWhenResizingViewport(ui) {
+ info("Test reset when resizing the viewport");
+
+ let waitPixelRatioChange = onceDevicePixelRatioChange(ui);
+
+ let deviceRemoved = once(ui, "device-removed");
+ yield testViewportResize(ui, ".viewport-vertical-resize-handle",
+ [-10, -10], [testDevice.width, testDevice.height - 10], [0, -10], ui);
+ yield deviceRemoved;
+
+ yield waitPixelRatioChange;
+ yield testDevicePixelRatio(ui, window.devicePixelRatio);
+
+ testViewportDPRSelect(ui, {value: window.devicePixelRatio, disabled: false});
+ testViewportDeviceSelectLabel(ui, "no device selected");
+}
+
+function* testChangingDPR(ui) {
+ info("Test changing device pixel ratio");
+
+ let waitPixelRatioChange = onceDevicePixelRatioChange(ui);
+
+ yield selectDPR(ui, VIEWPORT_DPPX);
+ yield waitPixelRatioChange;
+ yield testDevicePixelRatio(ui, VIEWPORT_DPPX);
+ testViewportDPRSelect(ui, {value: VIEWPORT_DPPX, disabled: false});
+ testViewportDeviceSelectLabel(ui, "no device selected");
+}
+
+function testViewportDPRSelect(ui, expected) {
+ info("Test viewport's DPR Select");
+
+ let select = ui.toolWindow.document.querySelector("#global-dpr-selector > select");
+ is(select.value, expected.value,
+ `DPR Select value should be: ${expected.value}`);
+ is(select.disabled, expected.disabled,
+ `DPR Select should be ${expected.disabled ? "disabled" : "enabled"}.`);
+}
+
+function* testDevicePixelRatio(ui, expected) {
+ info("Test device pixel ratio");
+
+ let dppx = yield getViewportDevicePixelRatio(ui);
+ is(dppx, expected, `devicePixelRatio should be: ${expected}`);
+}
+
+function* getViewportDevicePixelRatio(ui) {
+ return yield ContentTask.spawn(ui.getViewportBrowser(), {}, function* () {
+ return content.devicePixelRatio;
+ });
+}
+
+function onceDevicePixelRatioChange(ui) {
+ return ContentTask.spawn(ui.getViewportBrowser(), {}, function* () {
+ info(`Listening for a pixel ratio change (current: ${content.devicePixelRatio}dppx)`);
+
+ let pixelRatio = content.devicePixelRatio;
+ let mql = content.matchMedia(`(resolution: ${pixelRatio}dppx)`);
+
+ return new Promise(resolve => {
+ const onWindowCreated = () => {
+ if (pixelRatio !== content.devicePixelRatio) {
+ resolve();
+ }
+ };
+
+ addEventListener("DOMWindowCreated", onWindowCreated, {once: true});
+
+ mql.addListener(function listener() {
+ mql.removeListener(listener);
+ removeEventListener("DOMWindowCreated", onWindowCreated, {once: true});
+ resolve();
+ });
+ });
+ });
+}
diff --git a/devtools/client/responsive.html/test/browser/browser_exit_button.js b/devtools/client/responsive.html/test/browser/browser_exit_button.js
new file mode 100644
index 000000000..62e652274
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_exit_button.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URL = "data:text/html;charset=utf-8,";
+
+// Test global exit button
+addRDMTask(TEST_URL, function* (...args) {
+ yield testExitButton(...args);
+});
+
+// Test global exit button on detached tab.
+// See Bug 1262806
+add_task(function* () {
+ let tab = yield addTab(TEST_URL);
+ let { ui, manager } = yield openRDM(tab);
+
+ yield waitBootstrap(ui);
+
+ let waitTabIsDetached = Promise.all([
+ once(tab, "TabClose"),
+ once(tab.linkedBrowser, "SwapDocShells")
+ ]);
+
+ // Detach the tab with RDM open.
+ let newWindow = gBrowser.replaceTabWithWindow(tab);
+
+ // Waiting the tab is detached.
+ yield waitTabIsDetached;
+
+ // Get the new tab instance.
+ tab = newWindow.gBrowser.tabs[0];
+
+ // Detaching a tab closes RDM.
+ ok(!manager.isActiveForTab(tab),
+ "Responsive Design Mode is not active for the tab");
+
+ // Reopen the RDM and test the exit button again.
+ yield testExitButton(yield openRDM(tab));
+ yield BrowserTestUtils.closeWindow(newWindow);
+});
+
+function* waitBootstrap(ui) {
+ let { toolWindow, tab } = ui;
+ let { store } = toolWindow;
+ let url = String(tab.linkedBrowser.currentURI.spec);
+
+ // Wait until the viewport has been added.
+ yield waitUntilState(store, state => state.viewports.length == 1);
+
+ // Wait until the document has been loaded.
+ yield waitForFrameLoad(ui, url);
+}
+
+function* testExitButton({ui, manager}) {
+ yield waitBootstrap(ui);
+
+ let exitButton = ui.toolWindow.document.getElementById("global-exit-button");
+
+ ok(manager.isActiveForTab(ui.tab),
+ "Responsive Design Mode active for the tab");
+
+ exitButton.click();
+
+ yield once(manager, "off");
+
+ ok(!manager.isActiveForTab(ui.tab),
+ "Responsive Design Mode is not active for the tab");
+}
diff --git a/devtools/client/responsive.html/test/browser/browser_frame_script_active.js b/devtools/client/responsive.html/test/browser/browser_frame_script_active.js
new file mode 100644
index 000000000..81449a340
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_frame_script_active.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Verify frame script is active when expected.
+
+const e10s = require("devtools/client/responsive.html/utils/e10s");
+
+const TEST_URL = "http://example.com/";
+add_task(function* () {
+ let tab = yield addTab(TEST_URL);
+
+ let { ui } = yield openRDM(tab);
+
+ let mm = ui.getViewportBrowser().messageManager;
+ let { active } = yield e10s.request(mm, "IsActive");
+ is(active, true, "Frame script is active");
+
+ yield closeRDM(tab);
+
+ // Must re-get the messageManager on each run since it changes when RDM opens
+ // or closes due to the design of swapFrameLoaders. Also, we only have access
+ // to a valid `ui` instance while RDM is open.
+ mm = tab.linkedBrowser.messageManager;
+ ({ active } = yield e10s.request(mm, "IsActive"));
+ is(active, false, "Frame script is active");
+
+ // Try another round as well to be sure there is no state saved anywhere
+ ({ ui } = yield openRDM(tab));
+
+ mm = ui.getViewportBrowser().messageManager;
+ ({ active } = yield e10s.request(mm, "IsActive"));
+ is(active, true, "Frame script is active");
+
+ yield closeRDM(tab);
+
+ // Must re-get the messageManager on each run since it changes when RDM opens
+ // or closes due to the design of swapFrameLoaders. Also, we only have access
+ // to a valid `ui` instance while RDM is open.
+ mm = tab.linkedBrowser.messageManager;
+ ({ active } = yield e10s.request(mm, "IsActive"));
+ is(active, false, "Frame script is active");
+
+ yield removeTab(tab);
+});
diff --git a/devtools/client/responsive.html/test/browser/browser_menu_item_01.js b/devtools/client/responsive.html/test/browser/browser_menu_item_01.js
new file mode 100644
index 000000000..8e1c1c4cd
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_menu_item_01.js
@@ -0,0 +1,62 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test RDM menu item is checked when expected, on multiple tabs.
+
+const TEST_URL = "data:text/html;charset=utf-8,";
+
+const tabUtils = require("sdk/tabs/utils");
+const { startup } = require("sdk/window/helpers");
+
+const activateTab = (tab) => new Promise(resolve => {
+ let { tabContainer } = tabUtils.getOwnerWindow(tab).gBrowser;
+
+ tabContainer.addEventListener("TabSelect", function listener({type}) {
+ tabContainer.removeEventListener(type, listener);
+ resolve();
+ });
+
+ tabUtils.activateTab(tab);
+});
+
+const isMenuChecked = () => {
+ let menu = document.getElementById("menu_responsiveUI");
+ return menu.getAttribute("checked") === "true";
+};
+
+add_task(function* () {
+ yield startup(window);
+
+ ok(!isMenuChecked(),
+ "RDM menu item is unchecked by default");
+
+ const tab = yield addTab(TEST_URL);
+
+ ok(!isMenuChecked(),
+ "RDM menu item is unchecked for new tab");
+
+ yield openRDM(tab);
+
+ ok(isMenuChecked(),
+ "RDM menu item is checked with RDM open");
+
+ const tab2 = yield addTab(TEST_URL);
+
+ ok(!isMenuChecked(),
+ "RDM menu item is unchecked for new tab");
+
+ yield activateTab(tab);
+
+ ok(isMenuChecked(),
+ "RDM menu item is checked for the tab where RDM is open");
+
+ yield closeRDM(tab);
+
+ ok(!isMenuChecked(),
+ "RDM menu item is unchecked after RDM is closed");
+
+ yield removeTab(tab);
+ yield removeTab(tab2);
+});
diff --git a/devtools/client/responsive.html/test/browser/browser_menu_item_02.js b/devtools/client/responsive.html/test/browser/browser_menu_item_02.js
new file mode 100644
index 000000000..166ecb8ae
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_menu_item_02.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test RDM menu item is checked when expected, on multiple windows.
+
+const TEST_URL = "data:text/html;charset=utf-8,";
+
+const { getMostRecentBrowserWindow } = require("sdk/window/utils");
+
+const isMenuCheckedFor = ({document}) => {
+ let menu = document.getElementById("menu_responsiveUI");
+ return menu.getAttribute("checked") === "true";
+};
+
+add_task(function* () {
+ const window1 = yield BrowserTestUtils.openNewBrowserWindow();
+ let { gBrowser } = window1;
+
+ yield BrowserTestUtils.withNewTab({ gBrowser, url: TEST_URL },
+ function* (browser) {
+ let tab = gBrowser.getTabForBrowser(browser);
+
+ is(window1, getMostRecentBrowserWindow(),
+ "The new window is the active one");
+
+ ok(!isMenuCheckedFor(window1),
+ "RDM menu item is unchecked by default");
+
+ yield openRDM(tab);
+
+ ok(isMenuCheckedFor(window1),
+ "RDM menu item is checked with RDM open");
+
+ yield closeRDM(tab);
+
+ ok(!isMenuCheckedFor(window1),
+ "RDM menu item is unchecked with RDM closed");
+ });
+
+ yield BrowserTestUtils.closeWindow(window1);
+
+ is(window, getMostRecentBrowserWindow(),
+ "The original window is the active one");
+
+ ok(!isMenuCheckedFor(window),
+ "RDM menu item is unchecked");
+});
diff --git a/devtools/client/responsive.html/test/browser/browser_mouse_resize.js b/devtools/client/responsive.html/test/browser/browser_mouse_resize.js
new file mode 100644
index 000000000..98ccdab69
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_mouse_resize.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URL = "data:text/html;charset=utf-8,";
+
+addRDMTask(TEST_URL, function* ({ ui, manager }) {
+ let store = ui.toolWindow.store;
+
+ // Wait until the viewport has been added
+ yield waitUntilState(store, state => state.viewports.length == 1);
+
+ yield setViewportSize(ui, manager, 300, 300);
+
+ // Do horizontal + vertical resize
+ yield testViewportResize(ui, ".viewport-resize-handle",
+ [10, 10], [320, 310], [10, 10]);
+
+ // Do horizontal resize
+ yield testViewportResize(ui, ".viewport-horizontal-resize-handle",
+ [-10, 10], [300, 310], [-10, 0]);
+
+ // Do vertical resize
+ yield testViewportResize(ui, ".viewport-vertical-resize-handle",
+ [-10, -10], [300, 300], [0, -10], ui);
+});
diff --git a/devtools/client/responsive.html/test/browser/browser_navigation.js b/devtools/client/responsive.html/test/browser/browser_navigation.js
new file mode 100644
index 000000000..2c9f0027f
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_navigation.js
@@ -0,0 +1,98 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the primary browser navigation UI to verify it's connected to the viewport.
+
+const DUMMY_1_URL = "http://example.com/";
+const TEST_URL = `${URL_ROOT}doc_page_state.html`;
+const DUMMY_2_URL = "http://example.com/browser/";
+const DUMMY_3_URL = "http://example.com/browser/devtools/";
+
+add_task(function* () {
+ // Load up a sequence of pages:
+ // 0. DUMMY_1_URL
+ // 1. TEST_URL
+ // 2. DUMMY_2_URL
+ let tab = yield addTab(DUMMY_1_URL);
+ let browser = tab.linkedBrowser;
+ yield load(browser, TEST_URL);
+ yield load(browser, DUMMY_2_URL);
+
+ // Check session history state
+ let history = yield getSessionHistory(browser);
+ is(history.index, 2, "At page 2 in history");
+ is(history.entries.length, 3, "3 pages in history");
+ is(history.entries[0].uri, DUMMY_1_URL, "Page 0 URL matches");
+ is(history.entries[1].uri, TEST_URL, "Page 1 URL matches");
+ is(history.entries[2].uri, DUMMY_2_URL, "Page 2 URL matches");
+
+ // Go back one so we're at the test page
+ yield back(browser);
+
+ // Check session history state
+ history = yield getSessionHistory(browser);
+ is(history.index, 1, "At page 1 in history");
+ is(history.entries.length, 3, "3 pages in history");
+ is(history.entries[0].uri, DUMMY_1_URL, "Page 0 URL matches");
+ is(history.entries[1].uri, TEST_URL, "Page 1 URL matches");
+ is(history.entries[2].uri, DUMMY_2_URL, "Page 2 URL matches");
+
+ yield openRDM(tab);
+
+ ok(browser.webNavigation.canGoBack, "Going back is allowed");
+ ok(browser.webNavigation.canGoForward, "Going forward is allowed");
+ is(browser.documentURI.spec, TEST_URL, "documentURI matches page 1");
+ is(browser.contentTitle, "Page State Test", "contentTitle matches page 1");
+
+ yield forward(browser);
+
+ ok(browser.webNavigation.canGoBack, "Going back is allowed");
+ ok(!browser.webNavigation.canGoForward, "Going forward is not allowed");
+ is(browser.documentURI.spec, DUMMY_2_URL, "documentURI matches page 2");
+ is(browser.contentTitle, "mochitest index /browser/", "contentTitle matches page 2");
+
+ yield back(browser);
+ yield back(browser);
+
+ ok(!browser.webNavigation.canGoBack, "Going back is not allowed");
+ ok(browser.webNavigation.canGoForward, "Going forward is allowed");
+ is(browser.documentURI.spec, DUMMY_1_URL, "documentURI matches page 0");
+ is(browser.contentTitle, "mochitest index /", "contentTitle matches page 0");
+
+ let receivedStatusChanges = new Promise(resolve => {
+ let statusChangesSeen = 0;
+ let statusChangesExpected = 2;
+ let progressListener = {
+ onStatusChange(webProgress, request, status, message) {
+ info(message);
+ if (++statusChangesSeen == statusChangesExpected) {
+ gBrowser.removeProgressListener(progressListener);
+ ok(true, `${statusChangesExpected} status changes while loading`);
+ resolve();
+ }
+ }
+ };
+ gBrowser.addProgressListener(progressListener);
+ });
+ yield load(browser, DUMMY_3_URL);
+ yield receivedStatusChanges;
+
+ ok(browser.webNavigation.canGoBack, "Going back is allowed");
+ ok(!browser.webNavigation.canGoForward, "Going forward is not allowed");
+ is(browser.documentURI.spec, DUMMY_3_URL, "documentURI matches page 3");
+ is(browser.contentTitle, "mochitest index /browser/devtools/",
+ "contentTitle matches page 3");
+
+ yield closeRDM(tab);
+
+ // Check session history state
+ history = yield getSessionHistory(browser);
+ is(history.index, 1, "At page 1 in history");
+ is(history.entries.length, 2, "2 pages in history");
+ is(history.entries[0].uri, DUMMY_1_URL, "Page 0 URL matches");
+ is(history.entries[1].uri, DUMMY_3_URL, "Page 1 URL matches");
+
+ yield removeTab(tab);
+});
diff --git a/devtools/client/responsive.html/test/browser/browser_network_throttling.js b/devtools/client/responsive.html/test/browser/browser_network_throttling.js
new file mode 100644
index 000000000..18c4a90ed
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_network_throttling.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const throttlingProfiles = require("devtools/client/shared/network-throttling-profiles");
+
+// Tests changing network throttling
+const TEST_URL = "data:text/html;charset=utf-8,Network throttling test";
+
+addRDMTask(TEST_URL, function* ({ ui, manager }) {
+ let { store } = ui.toolWindow;
+
+ // Wait until the viewport has been added
+ yield waitUntilState(store, state => state.viewports.length == 1);
+
+ // Test defaults
+ testNetworkThrottlingSelectorLabel(ui, "No throttling");
+ yield testNetworkThrottlingState(ui, null);
+
+ // Test a fast profile
+ yield testThrottlingProfile(ui, "Wi-Fi");
+
+ // Test a slower profile
+ yield testThrottlingProfile(ui, "Regular 3G");
+
+ // Test switching back to no throttling
+ yield selectNetworkThrottling(ui, "No throttling");
+ testNetworkThrottlingSelectorLabel(ui, "No throttling");
+ yield testNetworkThrottlingState(ui, null);
+});
+
+function testNetworkThrottlingSelectorLabel(ui, expected) {
+ let selector = "#global-network-throttling-selector";
+ let select = ui.toolWindow.document.querySelector(selector);
+ is(select.selectedOptions[0].textContent, expected,
+ `Select label should be changed to ${expected}`);
+}
+
+var testNetworkThrottlingState = Task.async(function* (ui, expected) {
+ let state = yield ui.emulationFront.getNetworkThrottling();
+ Assert.deepEqual(state, expected, "Network throttling state should be " +
+ JSON.stringify(expected, null, 2));
+});
+
+var testThrottlingProfile = Task.async(function* (ui, profile) {
+ yield selectNetworkThrottling(ui, profile);
+ testNetworkThrottlingSelectorLabel(ui, profile);
+ let data = throttlingProfiles.find(({ id }) => id == profile);
+ let { download, upload, latency } = data;
+ yield testNetworkThrottlingState(ui, {
+ downloadThroughput: download,
+ uploadThroughput: upload,
+ latency,
+ });
+});
diff --git a/devtools/client/responsive.html/test/browser/browser_page_state.js b/devtools/client/responsive.html/test/browser/browser_page_state.js
new file mode 100644
index 000000000..306900535
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_page_state.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test page state to ensure page is not reloaded and session history is not
+// modified.
+
+const DUMMY_1_URL = "http://example.com/";
+const TEST_URL = `${URL_ROOT}doc_page_state.html`;
+const DUMMY_2_URL = "http://example.com/browser/";
+
+add_task(function* () {
+ // Load up a sequence of pages:
+ // 0. DUMMY_1_URL
+ // 1. TEST_URL
+ // 2. DUMMY_2_URL
+ let tab = yield addTab(DUMMY_1_URL);
+ let browser = tab.linkedBrowser;
+ yield load(browser, TEST_URL);
+ yield load(browser, DUMMY_2_URL);
+
+ // Check session history state
+ let history = yield getSessionHistory(browser);
+ is(history.index, 2, "At page 2 in history");
+ is(history.entries.length, 3, "3 pages in history");
+ is(history.entries[0].uri, DUMMY_1_URL, "Page 0 URL matches");
+ is(history.entries[1].uri, TEST_URL, "Page 1 URL matches");
+ is(history.entries[2].uri, DUMMY_2_URL, "Page 2 URL matches");
+
+ // Go back one so we're at the test page
+ yield back(browser);
+
+ // Check session history state
+ history = yield getSessionHistory(browser);
+ is(history.index, 1, "At page 1 in history");
+ is(history.entries.length, 3, "3 pages in history");
+ is(history.entries[0].uri, DUMMY_1_URL, "Page 0 URL matches");
+ is(history.entries[1].uri, TEST_URL, "Page 1 URL matches");
+ is(history.entries[2].uri, DUMMY_2_URL, "Page 2 URL matches");
+
+ // Click on content to set an altered state that would be lost on reload
+ yield BrowserTestUtils.synthesizeMouseAtCenter("body", {}, browser);
+
+ let { ui } = yield openRDM(tab);
+
+ // Check color inside the viewport
+ let color = yield spawnViewportTask(ui, {}, function* () {
+ // eslint-disable-next-line mozilla/no-cpows-in-tests
+ return content.getComputedStyle(content.document.body)
+ .getPropertyValue("background-color");
+ });
+ is(color, "rgb(0, 128, 0)",
+ "Content is still modified from click in viewport");
+
+ yield closeRDM(tab);
+
+ // Check color back in the browser tab
+ color = yield ContentTask.spawn(browser, {}, function* () {
+ // eslint-disable-next-line mozilla/no-cpows-in-tests
+ return content.getComputedStyle(content.document.body)
+ .getPropertyValue("background-color");
+ });
+ is(color, "rgb(0, 128, 0)",
+ "Content is still modified from click in browser tab");
+
+ // Check session history state
+ history = yield getSessionHistory(browser);
+ is(history.index, 1, "At page 1 in history");
+ is(history.entries.length, 3, "3 pages in history");
+ is(history.entries[0].uri, DUMMY_1_URL, "Page 0 URL matches");
+ is(history.entries[1].uri, TEST_URL, "Page 1 URL matches");
+ is(history.entries[2].uri, DUMMY_2_URL, "Page 2 URL matches");
+
+ yield removeTab(tab);
+});
diff --git a/devtools/client/responsive.html/test/browser/browser_permission_doorhanger.js b/devtools/client/responsive.html/test/browser/browser_permission_doorhanger.js
new file mode 100644
index 000000000..68b594509
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_permission_doorhanger.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that permission popups asking for user approval still appear in RDM
+const DUMMY_URL = "http://example.com/";
+const TEST_URL = `${URL_ROOT}geolocation.html`;
+
+function waitForGeolocationPrompt(win, browser) {
+ return new Promise(resolve => {
+ win.PopupNotifications.panel.addEventListener("popupshown", function popupShown() {
+ let notification = win.PopupNotifications.getNotification("geolocation", browser);
+ if (notification) {
+ win.PopupNotifications.panel.removeEventListener("popupshown", popupShown);
+ resolve();
+ }
+ });
+ });
+}
+
+add_task(function* () {
+ let tab = yield addTab(DUMMY_URL);
+ let browser = tab.linkedBrowser;
+ let win = browser.ownerGlobal;
+
+ let waitPromptPromise = waitForGeolocationPrompt(win, browser);
+
+ // Checks if a geolocation permission doorhanger appears when openning a page
+ // requesting geolocation
+ yield load(browser, TEST_URL);
+ yield waitPromptPromise;
+
+ ok(true, "Permission doorhanger appeared without RDM enabled");
+
+ // Lets switch back to the dummy website and enable RDM
+ yield load(browser, DUMMY_URL);
+ let { ui } = yield openRDM(tab);
+ let newBrowser = ui.getViewportBrowser();
+
+ waitPromptPromise = waitForGeolocationPrompt(win, newBrowser);
+
+ // Checks if the doorhanger appeared again when reloading the geolocation
+ // page inside RDM
+ yield load(browser, TEST_URL);
+ yield waitPromptPromise;
+
+ ok(true, "Permission doorhanger appeared inside RDM");
+
+ yield closeRDM(tab);
+ yield removeTab(tab);
+});
diff --git a/devtools/client/responsive.html/test/browser/browser_resize_cmd.js b/devtools/client/responsive.html/test/browser/browser_resize_cmd.js
new file mode 100644
index 000000000..7e96e866c
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_resize_cmd.js
@@ -0,0 +1,148 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* global ResponsiveUIManager */
+/* eslint key-spacing: 0 */
+
+add_task(function* () {
+ let manager = ResponsiveUIManager;
+ let done;
+
+ function isOpen() {
+ return ResponsiveUIManager.isActiveForTab(gBrowser.selectedTab);
+ }
+
+ const TEST_URL = "data:text/html;charset=utf-8,hi";
+ yield helpers.addTabWithToolbar(TEST_URL, (options) => {
+ return helpers.audit(options, [
+ {
+ setup() {
+ done = once(manager, "on");
+ return helpers.setInput(options, "resize toggle");
+ },
+ check: {
+ input: "resize toggle",
+ hints: "",
+ markup: "VVVVVVVVVVVVV",
+ status: "VALID"
+ },
+ exec: {
+ output: ""
+ },
+ post: Task.async(function* () {
+ yield done;
+ ok(isOpen(), "responsive mode is open");
+ }),
+ },
+ {
+ setup() {
+ done = once(manager, "off");
+ return helpers.setInput(options, "resize toggle");
+ },
+ check: {
+ input: "resize toggle",
+ hints: "",
+ markup: "VVVVVVVVVVVVV",
+ status: "VALID"
+ },
+ exec: {
+ output: ""
+ },
+ post: Task.async(function* () {
+ yield done;
+ ok(!isOpen(), "responsive mode is closed");
+ }),
+ },
+ ]);
+ });
+ yield helpers.addTabWithToolbar(TEST_URL, (options) => {
+ return helpers.audit(options, [
+ {
+ setup() {
+ done = once(manager, "on");
+ return helpers.setInput(options, "resize on");
+ },
+ check: {
+ input: "resize on",
+ hints: "",
+ markup: "VVVVVVVVV",
+ status: "VALID"
+ },
+ exec: {
+ output: ""
+ },
+ post: Task.async(function* () {
+ yield done;
+ ok(isOpen(), "responsive mode is open");
+ }),
+ },
+ {
+ setup() {
+ done = once(manager, "off");
+ return helpers.setInput(options, "resize off");
+ },
+ check: {
+ input: "resize off",
+ hints: "",
+ markup: "VVVVVVVVVV",
+ status: "VALID"
+ },
+ exec: {
+ output: ""
+ },
+ post: Task.async(function* () {
+ yield done;
+ ok(!isOpen(), "responsive mode is closed");
+ }),
+ },
+ ]);
+ });
+ yield helpers.addTabWithToolbar(TEST_URL, (options) => {
+ return helpers.audit(options, [
+ {
+ setup() {
+ done = once(manager, "on");
+ return helpers.setInput(options, "resize to 400 400");
+ },
+ check: {
+ input: "resize to 400 400",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVVVV",
+ status: "VALID",
+ args: {
+ width: { value: 400 },
+ height: { value: 400 },
+ }
+ },
+ exec: {
+ output: ""
+ },
+ post: Task.async(function* () {
+ yield done;
+ ok(isOpen(), "responsive mode is open");
+ }),
+ },
+ {
+ setup() {
+ done = once(manager, "off");
+ return helpers.setInput(options, "resize off");
+ },
+ check: {
+ input: "resize off",
+ hints: "",
+ markup: "VVVVVVVVVV",
+ status: "VALID"
+ },
+ exec: {
+ output: ""
+ },
+ post: Task.async(function* () {
+ yield done;
+ ok(!isOpen(), "responsive mode is closed");
+ }),
+ },
+ ]);
+ });
+});
diff --git a/devtools/client/responsive.html/test/browser/browser_screenshot_button.js b/devtools/client/responsive.html/test/browser/browser_screenshot_button.js
new file mode 100644
index 000000000..60605c33b
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_screenshot_button.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test global exit button
+
+const TEST_URL = "data:text/html;charset=utf-8,";
+
+const { OS } = require("resource://gre/modules/osfile.jsm");
+
+function* waitUntilScreenshot() {
+ return new Promise(Task.async(function* (resolve) {
+ let { Downloads } = require("resource://gre/modules/Downloads.jsm");
+ let list = yield Downloads.getList(Downloads.ALL);
+
+ let view = {
+ onDownloadAdded: download => {
+ download.whenSucceeded().then(() => {
+ resolve(download.target.path);
+ list.removeView(view);
+ });
+ }
+ };
+
+ yield list.addView(view);
+ }));
+}
+
+addRDMTask(TEST_URL, function* ({ ui: {toolWindow} }) {
+ let { store, document } = toolWindow;
+
+ // Wait until the viewport has been added
+ yield waitUntilState(store, state => state.viewports.length == 1);
+
+ info("Click the screenshot button");
+ let screenshotButton = document.getElementById("global-screenshot-button");
+ screenshotButton.click();
+
+ let whenScreenshotSucceeded = waitUntilScreenshot();
+
+ let filePath = yield whenScreenshotSucceeded;
+ let image = new Image();
+ image.src = OS.Path.toFileURI(filePath);
+
+ yield once(image, "load");
+
+ // We have only one viewport at the moment
+ let viewport = store.getState().viewports[0];
+ let ratio = window.devicePixelRatio;
+
+ is(image.width, viewport.width * ratio,
+ "screenshot width has the expected width");
+
+ is(image.height, viewport.height * ratio,
+ "screenshot width has the expected height");
+
+ yield OS.File.remove(filePath);
+});
diff --git a/devtools/client/responsive.html/test/browser/browser_tab_close.js b/devtools/client/responsive.html/test/browser/browser_tab_close.js
new file mode 100644
index 000000000..1c5ed7c91
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_tab_close.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Verify RDM closes synchronously when tabs are closed.
+
+const TEST_URL = "http://example.com/";
+
+add_task(function* () {
+ let tab = yield addTab(TEST_URL);
+
+ let { ui } = yield openRDM(tab);
+ let clientClosed = waitForClientClose(ui);
+
+ closeRDM(tab, {
+ reason: "TabClose",
+ });
+
+ // This flag is set at the end of `ResponsiveUI.destroy`. If it is true
+ // without yielding on `closeRDM` above, then we must have closed
+ // synchronously.
+ is(ui.destroyed, true, "RDM closed synchronously");
+
+ yield clientClosed;
+ yield removeTab(tab);
+});
+
+add_task(function* () {
+ let tab = yield addTab(TEST_URL);
+
+ let { ui } = yield openRDM(tab);
+ let clientClosed = waitForClientClose(ui);
+
+ yield removeTab(tab);
+
+ // This flag is set at the end of `ResponsiveUI.destroy`. If it is true without
+ // yielding on `closeRDM` itself and only removing the tab, then we must have closed
+ // synchronously in response to tab closing.
+ is(ui.destroyed, true, "RDM closed synchronously");
+
+ yield clientClosed;
+});
diff --git a/devtools/client/responsive.html/test/browser/browser_tab_remoteness_change.js b/devtools/client/responsive.html/test/browser/browser_tab_remoteness_change.js
new file mode 100644
index 000000000..7ce32ff28
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_tab_remoteness_change.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Verify RDM closes synchronously when tabs change remoteness.
+
+const TEST_URL = "http://example.com/";
+
+add_task(function* () {
+ let tab = yield addTab(TEST_URL);
+
+ let { ui } = yield openRDM(tab);
+ let clientClosed = waitForClientClose(ui);
+
+ closeRDM(tab, {
+ reason: "BeforeTabRemotenessChange",
+ });
+
+ // This flag is set at the end of `ResponsiveUI.destroy`. If it is true
+ // without yielding on `closeRDM` above, then we must have closed
+ // synchronously.
+ is(ui.destroyed, true, "RDM closed synchronously");
+
+ yield clientClosed;
+ yield removeTab(tab);
+});
+
+add_task(function* () {
+ let tab = yield addTab(TEST_URL);
+
+ let { ui } = yield openRDM(tab);
+ let clientClosed = waitForClientClose(ui);
+
+ // Load URL that requires the main process, forcing a remoteness flip
+ yield load(tab.linkedBrowser, "about:robots");
+
+ // This flag is set at the end of `ResponsiveUI.destroy`. If it is true without
+ // yielding on `closeRDM` itself and only removing the tab, then we must have closed
+ // synchronously in response to tab closing.
+ is(ui.destroyed, true, "RDM closed synchronously");
+
+ yield clientClosed;
+ yield removeTab(tab);
+});
diff --git a/devtools/client/responsive.html/test/browser/browser_toolbox_computed_view.js b/devtools/client/responsive.html/test/browser/browser_toolbox_computed_view.js
new file mode 100644
index 000000000..b0b51aa42
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_toolbox_computed_view.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Check that when the viewport is resized, the computed-view refreshes.
+
+const TEST_URI = "data:text/html;charset=utf-8,<html><style>" +
+ "div {" +
+ " width: 500px;" +
+ " height: 10px;" +
+ " background: purple;" +
+ "} " +
+ "@media screen and (max-width: 200px) {" +
+ " div { " +
+ " width: 100px;" +
+ " }" +
+ "};" +
+ "</style><div></div></html>";
+
+addRDMTask(TEST_URI, function* ({ ui, manager }) {
+ info("Open the responsive design mode and set its size to 500x500 to start");
+ yield setViewportSize(ui, manager, 500, 500);
+
+ info("Open the inspector, computed-view and select the test node");
+ let { inspector, view } = yield openComputedView();
+ yield selectNode("div", inspector);
+
+ info("Try shrinking the viewport and checking the applied styles");
+ yield testShrink(view, inspector, ui, manager);
+
+ info("Try growing the viewport and checking the applied styles");
+ yield testGrow(view, inspector, ui, manager);
+
+ yield closeToolbox();
+});
+
+function* testShrink(computedView, inspector, ui, manager) {
+ is(computedWidth(computedView), "500px", "Should show 500px initially.");
+
+ let onRefresh = inspector.once("computed-view-refreshed");
+ yield setViewportSize(ui, manager, 100, 100);
+ yield onRefresh;
+
+ is(computedWidth(computedView), "100px", "Should be 100px after shrinking.");
+}
+
+function* testGrow(computedView, inspector, ui, manager) {
+ let onRefresh = inspector.once("computed-view-refreshed");
+ yield setViewportSize(ui, manager, 500, 500);
+ yield onRefresh;
+
+ is(computedWidth(computedView), "500px", "Should be 500px after growing.");
+}
+
+function computedWidth(computedView) {
+ for (let prop of computedView.propertyViews) {
+ if (prop.name === "width") {
+ return prop.valueNode.textContent;
+ }
+ }
+ return null;
+}
diff --git a/devtools/client/responsive.html/test/browser/browser_toolbox_rule_view.js b/devtools/client/responsive.html/test/browser/browser_toolbox_rule_view.js
new file mode 100644
index 000000000..7cf012c44
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_toolbox_rule_view.js
@@ -0,0 +1,78 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Check that when the viewport is resized, the rule-view refreshes.
+
+const TEST_URI = "data:text/html;charset=utf-8,<html><style>" +
+ "div {" +
+ " width: 500px;" +
+ " height: 10px;" +
+ " background: purple;" +
+ "} " +
+ "@media screen and (max-width: 200px) {" +
+ " div { " +
+ " width: 100px;" +
+ " }" +
+ "};" +
+ "</style><div></div></html>";
+
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("devtools.toolbox.splitconsoleEnabled");
+});
+
+addRDMTask(TEST_URI, function* ({ ui, manager }) {
+ info("Open the responsive design mode and set its size to 500x500 to start");
+ yield setViewportSize(ui, manager, 500, 500);
+
+ info("Open the inspector, rule-view and select the test node");
+ let { inspector, view } = yield openRuleView();
+ yield selectNode("div", inspector);
+
+ info("Try shrinking the viewport and checking the applied styles");
+ yield testShrink(view, ui, manager);
+
+ info("Try growing the viewport and checking the applied styles");
+ yield testGrow(view, ui, manager);
+
+ info("Check that ESC still opens the split console");
+ yield testEscapeOpensSplitConsole(inspector);
+
+ yield closeToolbox();
+});
+
+function* testShrink(ruleView, ui, manager) {
+ is(numberOfRules(ruleView), 2, "Should have two rules initially.");
+
+ info("Resize to 100x100 and wait for the rule-view to update");
+ let onRefresh = ruleView.once("ruleview-refreshed");
+ yield setViewportSize(ui, manager, 100, 100);
+ yield onRefresh;
+
+ is(numberOfRules(ruleView), 3, "Should have three rules after shrinking.");
+}
+
+function* testGrow(ruleView, ui, manager) {
+ info("Resize to 500x500 and wait for the rule-view to update");
+ let onRefresh = ruleView.once("ruleview-refreshed");
+ yield setViewportSize(ui, manager, 500, 500);
+ yield onRefresh;
+
+ is(numberOfRules(ruleView), 2, "Should have two rules after growing.");
+}
+
+function* testEscapeOpensSplitConsole(inspector) {
+ ok(!inspector._toolbox._splitConsole, "Console is not split.");
+
+ info("Press escape");
+ let onSplit = inspector._toolbox.once("split-console");
+ EventUtils.synthesizeKey("VK_ESCAPE", {});
+ yield onSplit;
+
+ ok(inspector._toolbox._splitConsole, "Console is split after pressing ESC.");
+}
+
+function numberOfRules(ruleView) {
+ return ruleView.element.querySelectorAll(".ruleview-code").length;
+}
diff --git a/devtools/client/responsive.html/test/browser/browser_toolbox_swap_browsers.js b/devtools/client/responsive.html/test/browser/browser_toolbox_swap_browsers.js
new file mode 100644
index 000000000..8f7afaf01
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_toolbox_swap_browsers.js
@@ -0,0 +1,128 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Verify that toolbox remains open when opening and closing RDM.
+
+const TEST_URL = "http://example.com/";
+
+function getServerConnections(browser) {
+ ok(browser.isRemoteBrowser, "Content browser is remote");
+ return ContentTask.spawn(browser, {}, function* () {
+ const Cu = Components.utils;
+ const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+ const { DebuggerServer } = require("devtools/server/main");
+ if (!DebuggerServer._connections) {
+ return 0;
+ }
+ return Object.getOwnPropertyNames(DebuggerServer._connections);
+ });
+}
+
+let checkServerConnectionCount = Task.async(function* (browser, expected, msg) {
+ let conns = yield getServerConnections(browser);
+ is(conns.length || 0, expected, "Server connection count: " + msg);
+});
+
+let checkToolbox = Task.async(function* (tab, location) {
+ let target = TargetFactory.forTab(tab);
+ ok(!!gDevTools.getToolbox(target), `Toolbox exists ${location}`);
+});
+
+add_task(function* setup() {
+ yield SpecialPowers.pushPrefEnv({
+ set: [["dom.ipc.processCount", 1]]
+ });
+});
+
+add_task(function* () {
+ let tab = yield addTab(TEST_URL);
+
+ let tabsInDifferentProcesses = E10S_MULTI_ENABLED &&
+ (gBrowser.tabs[0].linkedBrowser.frameLoader.childID !=
+ gBrowser.tabs[1].linkedBrowser.frameLoader.childID);
+
+ info("Open toolbox outside RDM");
+ {
+ // 0: No DevTools connections yet
+ yield checkServerConnectionCount(tab.linkedBrowser, 0,
+ "0: No DevTools connections yet");
+ let { toolbox } = yield openInspector();
+ if (tabsInDifferentProcesses) {
+ // 1: Two tabs open, but only one per content process
+ yield checkServerConnectionCount(tab.linkedBrowser, 1,
+ "1: Two tabs open, but only one per content process");
+ } else {
+ // 2: One for each tab (starting tab plus the one we opened)
+ yield checkServerConnectionCount(tab.linkedBrowser, 2,
+ "2: One for each tab (starting tab plus the one we opened)");
+ }
+ yield checkToolbox(tab, "outside RDM");
+ let { ui } = yield openRDM(tab);
+ if (tabsInDifferentProcesses) {
+ // 2: RDM UI adds an extra connection, 1 + 1 = 2
+ yield checkServerConnectionCount(ui.getViewportBrowser(), 2,
+ "2: RDM UI uses an extra connection");
+ } else {
+ // 3: RDM UI adds an extra connection, 2 + 1 = 3
+ yield checkServerConnectionCount(ui.getViewportBrowser(), 3,
+ "3: RDM UI uses an extra connection");
+ }
+ yield checkToolbox(tab, "after opening RDM");
+ yield closeRDM(tab);
+ if (tabsInDifferentProcesses) {
+ // 1: RDM UI closed, return to previous connection count
+ yield checkServerConnectionCount(tab.linkedBrowser, 1,
+ "1: RDM UI closed, return to previous connection count");
+ } else {
+ // 2: RDM UI closed, return to previous connection count
+ yield checkServerConnectionCount(tab.linkedBrowser, 2,
+ "2: RDM UI closed, return to previous connection count");
+ }
+ yield checkToolbox(tab, tab.linkedBrowser, "after closing RDM");
+ yield toolbox.destroy();
+ // 0: All DevTools usage closed
+ yield checkServerConnectionCount(tab.linkedBrowser, 0,
+ "0: All DevTools usage closed");
+ }
+
+ info("Open toolbox inside RDM");
+ {
+ // 0: No DevTools connections yet
+ yield checkServerConnectionCount(tab.linkedBrowser, 0,
+ "0: No DevTools connections yet");
+ let { ui } = yield openRDM(tab);
+ // 1: RDM UI uses an extra connection
+ yield checkServerConnectionCount(ui.getViewportBrowser(), 1,
+ "1: RDM UI uses an extra connection");
+ let { toolbox } = yield openInspector();
+ if (tabsInDifferentProcesses) {
+ // 2: Two tabs open, but only one per content process
+ yield checkServerConnectionCount(ui.getViewportBrowser(), 2,
+ "2: Two tabs open, but only one per content process");
+ } else {
+ // 3: One for each tab (starting tab plus the one we opened)
+ yield checkServerConnectionCount(ui.getViewportBrowser(), 3,
+ "3: One for each tab (starting tab plus the one we opened)");
+ }
+ yield checkToolbox(tab, ui.getViewportBrowser(), "inside RDM");
+ yield closeRDM(tab);
+ if (tabsInDifferentProcesses) {
+ // 1: RDM UI closed, one less connection
+ yield checkServerConnectionCount(tab.linkedBrowser, 1,
+ "1: RDM UI closed, one less connection");
+ } else {
+ // 2: RDM UI closed, one less connection
+ yield checkServerConnectionCount(tab.linkedBrowser, 2,
+ "2: RDM UI closed, one less connection");
+ }
+ yield checkToolbox(tab, tab.linkedBrowser, "after closing RDM");
+ yield toolbox.destroy();
+ // 0: All DevTools usage closed
+ yield checkServerConnectionCount(tab.linkedBrowser, 0,
+ "0: All DevTools usage closed");
+ }
+
+ yield removeTab(tab);
+});
diff --git a/devtools/client/responsive.html/test/browser/browser_touch_device.js b/devtools/client/responsive.html/test/browser/browser_touch_device.js
new file mode 100644
index 000000000..aea6de2c4
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_touch_device.js
@@ -0,0 +1,77 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests changing viewport touch simulation
+const TEST_URL = "data:text/html;charset=utf-8,touch simulation test";
+const Types = require("devtools/client/responsive.html/types");
+
+const testDevice = {
+ "name": "Fake Phone RDM Test",
+ "width": 320,
+ "height": 470,
+ "pixelRatio": 5.5,
+ "userAgent": "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "custom",
+ "featured": true,
+};
+
+// Add the new device to the list
+addDeviceForTest(testDevice);
+
+addRDMTask(TEST_URL, function* ({ ui, manager }) {
+ yield waitStartup(ui);
+
+ yield testDefaults(ui);
+ yield testChangingDevice(ui);
+ yield testResizingViewport(ui, true, false);
+ yield testEnableTouchSimulation(ui);
+ yield testResizingViewport(ui, false, true);
+});
+
+function* waitStartup(ui) {
+ let { store } = ui.toolWindow;
+
+ // Wait until the viewport has been added and the device list has been loaded
+ yield waitUntilState(store, state => state.viewports.length == 1
+ && state.devices.listState == Types.deviceListState.LOADED);
+}
+
+function* testDefaults(ui) {
+ info("Test Defaults");
+
+ yield testTouchEventsOverride(ui, false);
+ testViewportDeviceSelectLabel(ui, "no device selected");
+}
+
+function* testChangingDevice(ui) {
+ info("Test Changing Device");
+
+ yield selectDevice(ui, testDevice.name);
+ yield waitForViewportResizeTo(ui, testDevice.width, testDevice.height);
+ yield testTouchEventsOverride(ui, true);
+ testViewportDeviceSelectLabel(ui, testDevice.name);
+}
+
+function* testResizingViewport(ui, device, expected) {
+ info(`Test resizing the viewport, device ${device}, expected ${expected}`);
+
+ let deviceRemoved = once(ui, "device-removed");
+ yield testViewportResize(ui, ".viewport-vertical-resize-handle",
+ [-10, -10], [testDevice.width, testDevice.height - 10], [0, -10], ui);
+ if (device) {
+ yield deviceRemoved;
+ }
+ yield testTouchEventsOverride(ui, expected);
+ testViewportDeviceSelectLabel(ui, "no device selected");
+}
+
+function* testEnableTouchSimulation(ui) {
+ info("Test enabling touch simulation via button");
+
+ yield enableTouchSimulation(ui);
+ yield testTouchEventsOverride(ui, true);
+}
diff --git a/devtools/client/responsive.html/test/browser/browser_touch_simulation.js b/devtools/client/responsive.html/test/browser/browser_touch_simulation.js
new file mode 100644
index 000000000..12a718306
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_touch_simulation.js
@@ -0,0 +1,228 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test global touch simulation button
+
+const TEST_URL = `${URL_ROOT}touch.html`;
+const PREF_DOM_META_VIEWPORT_ENABLED = "dom.meta-viewport.enabled";
+
+addRDMTask(TEST_URL, function* ({ ui }) {
+ yield waitBootstrap(ui);
+ yield testWithNoTouch(ui);
+ yield enableTouchSimulation(ui);
+ yield testWithTouch(ui);
+ yield testWithMetaViewportEnabled(ui);
+ yield testWithMetaViewportDisabled(ui);
+ testTouchButton(ui);
+});
+
+function* testWithNoTouch(ui) {
+ yield injectEventUtils(ui);
+ yield ContentTask.spawn(ui.getViewportBrowser(), {}, function* () {
+ let { EventUtils } = content;
+
+ let div = content.document.querySelector("div");
+ let x = 0, y = 0;
+
+ info("testWithNoTouch: Initial test parameter and mouse mouse outside div");
+ x = -1; y = -1;
+ yield EventUtils.synthesizeMouse(div, x, y,
+ { type: "mousemove", isSynthesized: false }, content);
+ div.style.transform = "none";
+ div.style.backgroundColor = "";
+
+ info("testWithNoTouch: Move mouse into the div element");
+ yield EventUtils.synthesizeMouseAtCenter(div,
+ { type: "mousemove", isSynthesized: false }, content);
+ is(div.style.backgroundColor, "red", "mouseenter or mouseover should work");
+
+ info("testWithNoTouch: Drag the div element");
+ yield EventUtils.synthesizeMouseAtCenter(div,
+ { type: "mousedown", isSynthesized: false }, content);
+ x = 100; y = 100;
+ yield EventUtils.synthesizeMouse(div, x, y,
+ { type: "mousemove", isSynthesized: false }, content);
+ is(div.style.transform, "none", "touchmove shouldn't work");
+ yield EventUtils.synthesizeMouse(div, x, y,
+ { type: "mouseup", isSynthesized: false }, content);
+
+ info("testWithNoTouch: Move mouse out of the div element");
+ x = -1; y = -1;
+ yield EventUtils.synthesizeMouse(div, x, y,
+ { type: "mousemove", isSynthesized: false }, content);
+ is(div.style.backgroundColor, "blue", "mouseout or mouseleave should work");
+
+ info("testWithNoTouch: Click the div element");
+ yield EventUtils.synthesizeClick(div);
+ is(div.dataset.isDelay, "false",
+ "300ms delay between touch events and mouse events should not work");
+ });
+}
+
+function* testWithTouch(ui) {
+ yield injectEventUtils(ui);
+
+ yield ContentTask.spawn(ui.getViewportBrowser(), {}, function* () {
+ let { EventUtils } = content;
+
+ let div = content.document.querySelector("div");
+ let x = 0, y = 0;
+
+ info("testWithTouch: Initial test parameter and mouse mouse outside div");
+ x = -1; y = -1;
+ yield EventUtils.synthesizeMouse(div, x, y,
+ { type: "mousemove", isSynthesized: false }, content);
+ div.style.transform = "none";
+ div.style.backgroundColor = "";
+
+ info("testWithTouch: Move mouse into the div element");
+ yield EventUtils.synthesizeMouseAtCenter(div,
+ { type: "mousemove", isSynthesized: false }, content);
+ isnot(div.style.backgroundColor, "red",
+ "mouseenter or mouseover should not work");
+
+ info("testWithTouch: Drag the div element");
+ yield EventUtils.synthesizeMouseAtCenter(div,
+ { type: "mousedown", isSynthesized: false }, content);
+ x = 100; y = 100;
+ yield EventUtils.synthesizeMouse(div, x, y,
+ { type: "mousemove", isSynthesized: false }, content);
+ isnot(div.style.transform, "none", "touchmove should work");
+ yield EventUtils.synthesizeMouse(div, x, y,
+ { type: "mouseup", isSynthesized: false }, content);
+
+ info("testWithTouch: Move mouse out of the div element");
+ x = -1; y = -1;
+ yield EventUtils.synthesizeMouse(div, x, y,
+ { type: "mousemove", isSynthesized: false }, content);
+ isnot(div.style.backgroundColor, "blue",
+ "mouseout or mouseleave should not work");
+ });
+}
+
+function* testWithMetaViewportEnabled(ui) {
+ yield SpecialPowers.pushPrefEnv({set: [[PREF_DOM_META_VIEWPORT_ENABLED, true]]});
+
+ yield injectEventUtils(ui);
+
+ yield ContentTask.spawn(ui.getViewportBrowser(), {}, function* () {
+ let { synthesizeClick } = content.EventUtils;
+
+ let meta = content.document.querySelector("meta[name=viewport]");
+ let div = content.document.querySelector("div");
+ div.dataset.isDelay = "false";
+
+ info("testWithMetaViewportEnabled: " +
+ "click the div element with <meta name='viewport'>");
+ meta.content = "";
+ yield synthesizeClick(div);
+ is(div.dataset.isDelay, "true",
+ "300ms delay between touch events and mouse events should work");
+
+ info("testWithMetaViewportEnabled: " +
+ "click the div element with " +
+ "<meta name='viewport' content='user-scalable=no'>");
+ meta.content = "user-scalable=no";
+ yield synthesizeClick(div);
+ is(div.dataset.isDelay, "false",
+ "300ms delay between touch events and mouse events should not work");
+
+ info("testWithMetaViewportEnabled: " +
+ "click the div element with " +
+ "<meta name='viewport' content='minimum-scale=maximum-scale'>");
+ meta.content = "minimum-scale=maximum-scale";
+ yield synthesizeClick(div);
+ is(div.dataset.isDelay, "false",
+ "300ms delay between touch events and mouse events should not work");
+
+ info("testWithMetaViewportEnabled: " +
+ "click the div element with " +
+ "<meta name='viewport' content='width=device-width'>");
+ meta.content = "width=device-width";
+ yield synthesizeClick(div);
+ is(div.dataset.isDelay, "false",
+ "300ms delay between touch events and mouse events should not work");
+ });
+}
+
+function* testWithMetaViewportDisabled(ui) {
+ yield SpecialPowers.pushPrefEnv({set: [[PREF_DOM_META_VIEWPORT_ENABLED, false]]});
+
+ yield injectEventUtils(ui);
+
+ yield ContentTask.spawn(ui.getViewportBrowser(), {}, function* () {
+ let { synthesizeClick } = content.EventUtils;
+
+ let meta = content.document.querySelector("meta[name=viewport]");
+ let div = content.document.querySelector("div");
+ div.dataset.isDelay = "false";
+
+ info("testWithMetaViewportDisabled: click the div with <meta name='viewport'>");
+ meta.content = "";
+ yield synthesizeClick(div);
+ is(div.dataset.isDelay, "true",
+ "300ms delay between touch events and mouse events should work");
+ });
+}
+
+function testTouchButton(ui) {
+ let { document } = ui.toolWindow;
+ let touchButton = document.querySelector("#global-touch-simulation-button");
+
+ ok(touchButton.classList.contains("active"),
+ "Touch simulation is active at end of test.");
+
+ touchButton.click();
+
+ ok(!touchButton.classList.contains("active"),
+ "Touch simulation is stopped on click.");
+
+ touchButton.click();
+
+ ok(touchButton.classList.contains("active"),
+ "Touch simulation is started on click.");
+}
+
+function* waitBootstrap(ui) {
+ let { store } = ui.toolWindow;
+
+ yield waitUntilState(store, state => state.viewports.length == 1);
+ yield waitForFrameLoad(ui, TEST_URL);
+}
+
+function* injectEventUtils(ui) {
+ yield ContentTask.spawn(ui.getViewportBrowser(), {}, function* () {
+ if ("EventUtils" in content) {
+ return;
+ }
+
+ let EventUtils = content.EventUtils = {};
+
+ EventUtils.window = {};
+ EventUtils.parent = EventUtils.window;
+ /* eslint-disable camelcase */
+ EventUtils._EU_Ci = Components.interfaces;
+ EventUtils._EU_Cc = Components.classes;
+ /* eslint-enable camelcase */
+ // EventUtils' `sendChar` function relies on the navigator to synthetize events.
+ EventUtils.navigator = content.navigator;
+ EventUtils.KeyboardEvent = content.KeyboardEvent;
+
+ EventUtils.synthesizeClick = element => new Promise(resolve => {
+ element.addEventListener("click", function onClick() {
+ element.removeEventListener("click", onClick);
+ resolve();
+ });
+
+ EventUtils.synthesizeMouseAtCenter(element,
+ { type: "mousedown", isSynthesized: false }, content);
+ EventUtils.synthesizeMouseAtCenter(element,
+ { type: "mouseup", isSynthesized: false }, content);
+ });
+
+ Services.scriptloader.loadSubScript(
+ "chrome://mochikit/content/tests/SimpleTest/EventUtils.js", EventUtils);
+ });
+}
diff --git a/devtools/client/responsive.html/test/browser/browser_viewport_basics.js b/devtools/client/responsive.html/test/browser/browser_viewport_basics.js
new file mode 100644
index 000000000..86fc41da9
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_viewport_basics.js
@@ -0,0 +1,30 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test viewports basics after opening, like size and location
+
+const TEST_URL = "http://example.org/";
+
+addRDMTask(TEST_URL, function* ({ ui }) {
+ let store = ui.toolWindow.store;
+
+ // Wait until the viewport has been added
+ yield waitUntilState(store, state => state.viewports.length == 1);
+
+ // A single viewport of default size appeared
+ let viewport = ui.toolWindow.document.querySelector(".viewport-content");
+
+ is(ui.toolWindow.getComputedStyle(viewport).getPropertyValue("width"),
+ "320px", "Viewport has default width");
+ is(ui.toolWindow.getComputedStyle(viewport).getPropertyValue("height"),
+ "480px", "Viewport has default height");
+
+ // Browser's location should match original tab
+ yield waitForFrameLoad(ui, TEST_URL);
+ let location = yield spawnViewportTask(ui, {}, function* () {
+ return content.location.href; // eslint-disable-line
+ });
+ is(location, TEST_URL, "Viewport location matches");
+});
diff --git a/devtools/client/responsive.html/test/browser/browser_window_close.js b/devtools/client/responsive.html/test/browser/browser_window_close.js
new file mode 100644
index 000000000..29d9d1e34
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_window_close.js
@@ -0,0 +1,25 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(function* () {
+ let newWindowPromise = BrowserTestUtils.waitForNewWindow();
+ window.open("data:text/html;charset=utf-8,", "_blank");
+ let newWindow = yield newWindowPromise;
+
+ newWindow.focus();
+ yield once(newWindow.gBrowser, "load", true);
+
+ let tab = newWindow.gBrowser.selectedTab;
+ yield openRDM(tab);
+
+ // Close the window on a tab with an active responsive design UI and
+ // wait for the UI to gracefully shutdown. This has leaked the window
+ // in the past.
+ ok(ResponsiveUIManager.isActiveForTab(tab),
+ "ResponsiveUI should be active for tab when the window is closed");
+ let offPromise = once(ResponsiveUIManager, "off");
+ yield BrowserTestUtils.closeWindow(newWindow);
+ yield offPromise;
+});
diff --git a/devtools/client/responsive.html/test/browser/devices.json b/devtools/client/responsive.html/test/browser/devices.json
new file mode 100644
index 000000000..c3f2bb363
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/devices.json
@@ -0,0 +1,651 @@
+{
+ "TYPES": [ "phones", "tablets", "laptops", "televisions", "consoles", "watches" ],
+ "phones": [
+ {
+ "name": "Firefox OS Flame",
+ "width": 320,
+ "height": 570,
+ "pixelRatio": 1.5,
+ "userAgent": "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "fxos"
+ },
+ {
+ "name": "Alcatel One Touch Fire",
+ "width": 320,
+ "height": 480,
+ "pixelRatio": 1,
+ "userAgent": "Mozilla/5.0 (Mobile; ALCATELOneTouch4012X; rv:28.0) Gecko/28.0 Firefox/28.0",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "fxos"
+ },
+ {
+ "name": "Alcatel One Touch Fire C",
+ "width": 320,
+ "height": 480,
+ "pixelRatio": 1,
+ "userAgent": "Mozilla/5.0 (Mobile; ALCATELOneTouch4019X; rv:28.0) Gecko/28.0 Firefox/28.0",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "fxos"
+ },
+ {
+ "name": "Alcatel One Touch Fire E",
+ "width": 320,
+ "height": 480,
+ "pixelRatio": 2,
+ "userAgent": "Mozilla/5.0 (Mobile; ALCATELOneTouch6015X; rv:32.0) Gecko/32.0 Firefox/32.0",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "fxos"
+ },
+ {
+ "name": "Apple iPhone 4",
+ "width": 320,
+ "height": 480,
+ "pixelRatio": 2,
+ "userAgent": "Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_2_1 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8C148 Safari/6533.18.5",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "ios"
+ },
+ {
+ "name": "Apple iPhone 5",
+ "width": 320,
+ "height": 568,
+ "pixelRatio": 2,
+ "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 7_0 like Mac OS X; en-us) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "ios"
+ },
+ {
+ "name": "Apple iPhone 5s",
+ "width": 320,
+ "height": 568,
+ "pixelRatio": 2,
+ "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 9_2_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13D15 Safari/601.1",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "ios",
+ "featured": true
+ },
+ {
+ "name": "Apple iPhone 6",
+ "width": 375,
+ "height": 667,
+ "pixelRatio": 2,
+ "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.3 (KHTML, like Gecko) Version/8.0 Mobile/12A4345d Safari/600.1.4",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "ios"
+ },
+ {
+ "name": "Apple iPhone 6 Plus",
+ "width": 414,
+ "height": 736,
+ "pixelRatio": 3,
+ "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.3 (KHTML, like Gecko) Version/8.0 Mobile/12A4345d Safari/600.1.4",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "ios",
+ "featured": true
+ },
+ {
+ "name": "Apple iPhone 6s",
+ "width": 375,
+ "height": 667,
+ "pixelRatio": 2,
+ "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.3 (KHTML, like Gecko) Version/8.0 Mobile/12A4345d Safari/600.1.4",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "ios",
+ "featured": true
+ },
+ {
+ "name": "Apple iPhone 6s Plus",
+ "width": 414,
+ "height": 736,
+ "pixelRatio": 3,
+ "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.3 (KHTML, like Gecko) Version/8.0 Mobile/12A4345d Safari/600.1.4",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "ios"
+ },
+ {
+ "name": "BlackBerry Z30",
+ "width": 360,
+ "height": 640,
+ "pixelRatio": 2,
+ "userAgent": "Mozilla/5.0 (BB10; Touch) AppleWebKit/537.10+ (KHTML, like Gecko) Version/10.0.9.2372 Mobile Safari/537.10+",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "blackberryos"
+ },
+ {
+ "name": "Geeksphone Keon",
+ "width": 320,
+ "height": 480,
+ "pixelRatio": 1,
+ "userAgent": "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "android"
+ },
+ {
+ "name": "Geeksphone Peak, Revolution",
+ "width": 360,
+ "height": 640,
+ "pixelRatio": 1.5,
+ "userAgent": "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "android"
+ },
+ {
+ "name": "Google Nexus S",
+ "width": 320,
+ "height": 533,
+ "pixelRatio": 1.5,
+ "userAgent": "Mozilla/5.0 (Linux; U; Android 2.3.4; en-us; Nexus S Build/GRJ22) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "android"
+ },
+ {
+ "name": "Google Nexus 4",
+ "width": 384,
+ "height": 640,
+ "pixelRatio": 2,
+ "userAgent": "Mozilla/5.0 (Linux; Android 4.4.4; en-us; Nexus 4 Build/JOP40D) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2307.2 Mobile Safari/537.36",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "android",
+ "featured": true
+ },
+ {
+ "name": "Google Nexus 5",
+ "width": 360,
+ "height": 640,
+ "pixelRatio": 3,
+ "userAgent": "Mozilla/5.0 (Linux; Android 4.4.4; en-us; Nexus 5 Build/JOP40D) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2307.2 Mobile Safari/537.36",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "android",
+ "featured": true
+ },
+ {
+ "name": "Google Nexus 6",
+ "width": 412,
+ "height": 732,
+ "pixelRatio": 3.5,
+ "userAgent": "Mozilla/5.0 (Linux; Android 5.1.1; Nexus 6 Build/LYZ28E) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.23 Mobile Safari/537.36",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "android",
+ "featured": true
+ },
+ {
+ "name": "Intex Cloud Fx",
+ "width": 320,
+ "height": 480,
+ "pixelRatio": 1,
+ "userAgent": "Mozilla/5.0 (Mobile; rv:32.0) Gecko/32.0 Firefox/32.0",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "fxos"
+ },
+ {
+ "name": "KDDI Fx0",
+ "width": 360,
+ "height": 640,
+ "pixelRatio": 2,
+ "userAgent": "Mozilla/5.0 (Mobile; LGL25; rv:32.0) Gecko/32.0 Firefox/32.0",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "fxos"
+ },
+ {
+ "name": "LG Fireweb",
+ "width": 320,
+ "height": 480,
+ "pixelRatio": 1,
+ "userAgent": "Mozilla/5.0 (Mobile; LG-D300; rv:18.1) Gecko/18.1 Firefox/18.1",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "fxos"
+ },
+ {
+ "name": "LG Optimus L70",
+ "width": 384,
+ "height": 640,
+ "pixelRatio": 1.25,
+ "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/30.0.1599.103 Mobile Safari/537.36",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "android"
+ },
+ {
+ "name": "Nokia Lumia 520",
+ "width": 320,
+ "height": 533,
+ "pixelRatio": 1.4,
+ "userAgent": "Mozilla/5.0 (compatible; MSIE 10.0; Windows Phone 8.0; Trident/6.0; IEMobile/10.0; ARM; Touch; NOKIA; Lumia 520)",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "android",
+ "featured": true
+ },
+ {
+ "name": "Nokia N9",
+ "width": 360,
+ "height": 640,
+ "pixelRatio": 1,
+ "userAgent": "Mozilla/5.0 (MeeGo; NokiaN9) AppleWebKit/534.13 (KHTML, like Gecko) NokiaBrowser/8.5.0 Mobile Safari/534.13",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "android"
+ },
+ {
+ "name": "OnePlus One",
+ "width": 360,
+ "height": 640,
+ "pixelRatio": 3,
+ "userAgent": "Mozilla/5.0 (Android 5.1.1; Mobile; rv:43.0) Gecko/43.0 Firefox/43.0",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "android"
+ },
+ {
+ "name": "Samsung Galaxy S3",
+ "width": 360,
+ "height": 640,
+ "pixelRatio": 2,
+ "userAgent": "Mozilla/5.0 (Linux; U; Android 4.0; en-us; GT-I9300 Build/IMM76D) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "android"
+ },
+ {
+ "name": "Samsung Galaxy S4",
+ "width": 360,
+ "height": 640,
+ "pixelRatio": 3,
+ "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; GT-I9505 Build/JDQ39) AppleWebKit/537.36 (KHTML, like Gecko) Version/1.5 Chrome/28.0.1500.94 Mobile Safari/537.36",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "android"
+ },
+ {
+ "name": "Samsung Galaxy S5",
+ "width": 360,
+ "height": 640,
+ "pixelRatio": 3,
+ "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; GT-I9505 Build/JDQ39) AppleWebKit/537.36 (KHTML, like Gecko) Version/1.5 Chrome/28.0.1500.94 Mobile Safari/537.36",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "android",
+ "featured": true
+ },
+ {
+ "name": "Samsung Galaxy S6",
+ "width": 360,
+ "height": 640,
+ "pixelRatio": 4,
+ "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; GT-I9505 Build/JDQ39) AppleWebKit/537.36 (KHTML, like Gecko) Version/1.5 Chrome/28.0.1500.94 Mobile Safari/537.36",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "android",
+ "featured": true
+ },
+ {
+ "name": "Sony Xperia Z3",
+ "width": 360,
+ "height": 640,
+ "pixelRatio": 3,
+ "userAgent": "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "android"
+ },
+ {
+ "name": "Spice Fire One Mi-FX1",
+ "width": 320,
+ "height": 480,
+ "pixelRatio": 1,
+ "userAgent": "Mozilla/5.0 (Mobile; rv:28.0) Gecko/28.0 Firefox/28.0",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "fxos"
+ },
+ {
+ "name": "Symphony GoFox F15",
+ "width": 320,
+ "height": 480,
+ "pixelRatio": 1,
+ "userAgent": "Mozilla/5.0 (Mobile; rv:30.0) Gecko/30.0 Firefox/30.0",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "fxos"
+ },
+ {
+ "name": "ZTE Open",
+ "width": 320,
+ "height": 480,
+ "pixelRatio": 1,
+ "userAgent": "Mozilla/5.0 (Mobile; ZTEOPEN; rv:18.1) Gecko/18.0 Firefox/18.1",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "fxos"
+ },
+ {
+ "name": "ZTE Open II",
+ "width": 320,
+ "height": 480,
+ "pixelRatio": 1,
+ "userAgent": "Mozilla/5.0 (Mobile; OPEN2; rv:28.0) Gecko/28.0 Firefox/28.0",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "fxos"
+ },
+ {
+ "name": "ZTE Open C",
+ "width": 320,
+ "height": 450,
+ "pixelRatio": 1.5,
+ "userAgent": "Mozilla/5.0 (Mobile; OPENC; rv:32.0) Gecko/32.0 Firefox/32.0",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "fxos"
+ },
+ {
+ "name": "Zen Fire 105",
+ "width": 320,
+ "height": 480,
+ "pixelRatio": 1,
+ "userAgent": "Mozilla/5.0 (Mobile; rv:28.0) Gecko/28.0 Firefox/28.0",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "fxos"
+ }
+ ],
+ "tablets": [
+ {
+ "name": "Amazon Kindle Fire HDX 8.9",
+ "width": 1280,
+ "height": 800,
+ "pixelRatio": 2,
+ "userAgent": "Mozilla/5.0 (Linux; U; en-us; KFAPWI Build/JDQ39) AppleWebKit/535.19 (KHTML, like Gecko) Silk/3.13 Safari/535.19 Silk-Accelerated=true",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "fireos",
+ "featured": true
+ },
+ {
+ "name": "Apple iPad",
+ "width": 1024,
+ "height": 768,
+ "pixelRatio": 2,
+ "userAgent": "Mozilla/5.0 (iPad; CPU OS 7_0 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "ios"
+ },
+ {
+ "name": "Apple iPad Air 2",
+ "width": 1024,
+ "height": 768,
+ "pixelRatio": 2,
+ "userAgent": "Mozilla/5.0 (iPad; CPU OS 4_3_5 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8L1 Safari/6533.18.5",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "ios",
+ "featured": true
+ },
+ {
+ "name": "Apple iPad Mini",
+ "width": 1024,
+ "height": 768,
+ "pixelRatio": 1,
+ "userAgent": "Mozilla/5.0 (iPad; CPU OS 4_3_5 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8L1 Safari/6533.18.5",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "ios"
+ },
+ {
+ "name": "Apple iPad Mini 2",
+ "width": 1024,
+ "height": 768,
+ "pixelRatio": 2,
+ "userAgent": "Mozilla/5.0 (iPad; CPU OS 4_3_5 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8L1 Safari/6533.18.5",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "ios",
+ "featured": true
+ },
+ {
+ "name": "BlackBerry PlayBook",
+ "width": 1024,
+ "height": 600,
+ "pixelRatio": 1,
+ "userAgent": "Mozilla/5.0 (PlayBook; U; RIM Tablet OS 2.1.0; en-US) AppleWebKit/536.2+ (KHTML like Gecko) Version/7.2.1.0 Safari/536.2+",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "blackberryos"
+ },
+ {
+ "name": "Foxconn InFocus",
+ "width": 1280,
+ "height": 800,
+ "pixelRatio": 1,
+ "userAgent": "Mozilla/5.0 (Tablet; rv:32.0) Gecko/32.0 Firefox/32.0",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "android"
+ },
+ {
+ "name": "Google Nexus 7",
+ "width": 960,
+ "height": 600,
+ "pixelRatio": 2,
+ "userAgent": "Mozilla/5.0 (Linux; Android 4.3; Nexus 7 Build/JSS15Q) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2307.2 Mobile Safari/537.36",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "android",
+ "featured": true
+ },
+ {
+ "name": "Google Nexus 10",
+ "width": 1280,
+ "height": 800,
+ "pixelRatio": 2,
+ "userAgent": "Mozilla/5.0 (Linux; Android 4.3; Nexus 10 Build/JSS15Q) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2307.2 Mobile Safari/537.36",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "android"
+ },
+ {
+ "name": "Samsung Galaxy Note 2",
+ "width": 360,
+ "height": 640,
+ "pixelRatio": 2,
+ "userAgent": "Mozilla/5.0 (Linux; U; Android 4.1; en-us; GT-N7100 Build/JRO03C) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "android"
+ },
+ {
+ "name": "Samsung Galaxy Note 3",
+ "width": 360,
+ "height": 640,
+ "pixelRatio": 3,
+ "userAgent": "Mozilla/5.0 (Linux; U; Android 4.3; en-us; SM-N900T Build/JSS15J) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "android",
+ "featured": true
+ },
+ {
+ "name": "Tesla Model S",
+ "width": 1200,
+ "height": 1920,
+ "pixelRatio": 1,
+ "userAgent": "Mozilla/5.0 (X11; Linux) AppleWebKit/534.34 (KHTML, like Gecko) QtCarBrowser Safari/534.34",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "linux"
+ },
+ {
+ "name": "VIA Vixen",
+ "width": 1024,
+ "height": 600,
+ "pixelRatio": 1,
+ "userAgent": "Mozilla/5.0 (Tablet; rv:32.0) Gecko/32.0 Firefox/32.0",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "fxos"
+ }
+ ],
+ "laptops": [
+ {
+ "name": "Laptop (1366 x 768)",
+ "width": 1366,
+ "height": 768,
+ "pixelRatio": 1,
+ "userAgent": "",
+ "touch": false,
+ "firefoxOS": false,
+ "os": "windows",
+ "featured": true
+ },
+ {
+ "name": "Laptop (1920 x 1080)",
+ "width": 1280,
+ "height": 720,
+ "pixelRatio": 1.5,
+ "userAgent": "",
+ "touch": false,
+ "firefoxOS": false,
+ "os": "windows",
+ "featured": true
+ },
+ {
+ "name": "Laptop (1920 x 1080) with touch",
+ "width": 1280,
+ "height": 720,
+ "pixelRatio": 1.5,
+ "userAgent": "",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "windows"
+ }
+ ],
+ "televisions": [
+ {
+ "name": "720p HD Television",
+ "width": 1280,
+ "height": 720,
+ "pixelRatio": 1,
+ "userAgent": "",
+ "touch": false,
+ "firefoxOS": true,
+ "os": "custom"
+ },
+ {
+ "name": "1080p Full HD Television",
+ "width": 1920,
+ "height": 1080,
+ "pixelRatio": 1,
+ "userAgent": "",
+ "touch": false,
+ "firefoxOS": true,
+ "os": "custom"
+ },
+ {
+ "name": "4K Ultra HD Television",
+ "width": 3840,
+ "height": 2160,
+ "pixelRatio": 1,
+ "userAgent": "",
+ "touch": false,
+ "firefoxOS": true,
+ "os": "custom"
+ }
+ ],
+ "consoles": [
+ {
+ "name": "Nintendo 3DS",
+ "width": 320,
+ "height": 240,
+ "pixelRatio": 1,
+ "userAgent": "Mozilla/5.0 (Nintendo 3DS; U; ; en) Version/1.7585.EU",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "nintendo"
+ },
+ {
+ "name": "Nintendo Wii U Gamepad",
+ "width": 854,
+ "height": 480,
+ "pixelRatio": 0.87,
+ "userAgent": "Mozilla/5.0 (Nintendo WiiU) AppleWebKit/536.28 (KHTML, like Gecko) NX/3.0.3.12.15 NintendoBrowser/4.1.1.9601.EU",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "nintendo"
+ },
+ {
+ "name": "Sony PlayStation Vita",
+ "width": 960,
+ "height": 544,
+ "pixelRatio": 1,
+ "userAgent": "Mozilla/5.0 (Playstation Vita 1.61) AppleWebKit/531.22.8 (KHTML, like Gecko) Silk/3.2",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "playstation"
+ }
+ ],
+ "watches": [
+ {
+ "name": "LG G Watch",
+ "width": 280,
+ "height": 280,
+ "pixelRatio": 1,
+ "userAgent": "",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "android"
+ },
+ {
+ "name": "LG G Watch R",
+ "width": 320,
+ "height": 320,
+ "pixelRatio": 1,
+ "userAgent": "",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "android"
+ },
+ {
+ "name": "Motorola Moto 360",
+ "width": 320,
+ "height": 290,
+ "pixelRatio": 1,
+ "userAgent": "Mozilla/5.0 (Linux; Android 5.0.1; Moto 360 Build/LWX48T) AppleWebkit/537.36 (KHTML, like Gecko) Chrome/19.77.34.5 Mobile Safari/537.36",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "android"
+ },
+ {
+ "name": "Samsung Gear Live",
+ "width": 320,
+ "height": 320,
+ "pixelRatio": 1,
+ "userAgent": "",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "android"
+ }
+ ]
+}
diff --git a/devtools/client/responsive.html/test/browser/doc_page_state.html b/devtools/client/responsive.html/test/browser/doc_page_state.html
new file mode 100644
index 000000000..fb4d2acf0
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/doc_page_state.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<html>
+ <head>
+ <title>Page State Test</title>
+ <style>
+ body {
+ height: 100vh;
+ background: red;
+ }
+ body.modified {
+ background: green;
+ }
+ </style>
+ </head>
+ <body onclick="this.classList.add('modified')"/>
+</html>
diff --git a/devtools/client/responsive.html/test/browser/geolocation.html b/devtools/client/responsive.html/test/browser/geolocation.html
new file mode 100644
index 000000000..03d105a19
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/geolocation.html
@@ -0,0 +1,13 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Geolocation permission test</title>
+ </head>
+ <body>
+ <script type="text/javascript">
+ "use strict";
+ navigator.geolocation.getCurrentPosition(function (pos) {});
+ </script>
+ </body>
+</html> \ No newline at end of file
diff --git a/devtools/client/responsive.html/test/browser/head.js b/devtools/client/responsive.html/test/browser/head.js
new file mode 100644
index 000000000..3be69b0af
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/head.js
@@ -0,0 +1,401 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+/* import-globals-from ../../../framework/test/shared-head.js */
+/* import-globals-from ../../../framework/test/shared-redux-head.js */
+/* import-globals-from ../../../commandline/test/helpers.js */
+/* import-globals-from ../../../inspector/test/shared-head.js */
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js",
+ this);
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/framework/test/shared-redux-head.js",
+ this);
+
+// Import the GCLI test helper
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/commandline/test/helpers.js",
+ this);
+
+// Import helpers registering the test-actor in remote targets
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/shared/test/test-actor-registry.js",
+ this);
+
+// Import helpers for the inspector that are also shared with others
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/inspector/test/shared-head.js",
+ this);
+
+const E10S_MULTI_ENABLED = Services.prefs.getIntPref("dom.ipc.processCount") > 1;
+const TEST_URI_ROOT = "http://example.com/browser/devtools/client/responsive.html/test/browser/";
+const OPEN_DEVICE_MODAL_VALUE = "OPEN_DEVICE_MODAL";
+
+const { _loadPreferredDevices } = require("devtools/client/responsive.html/actions/devices");
+const { getOwnerWindow } = require("sdk/tabs/utils");
+const asyncStorage = require("devtools/shared/async-storage");
+const { addDevice, removeDevice } = require("devtools/client/shared/devices");
+
+SimpleTest.requestCompleteLog();
+SimpleTest.waitForExplicitFinish();
+
+// Toggling the RDM UI involves several docShell swap operations, which are somewhat slow
+// on debug builds. Usually we are just barely over the limit, so a blanket factor of 2
+// should be enough.
+requestLongerTimeout(2);
+
+flags.testing = true;
+Services.prefs.clearUserPref("devtools.responsive.html.displayedDeviceList");
+Services.prefs.setCharPref("devtools.devices.url",
+ TEST_URI_ROOT + "devices.json");
+Services.prefs.setBoolPref("devtools.responsive.html.enabled", true);
+
+registerCleanupFunction(() => {
+ flags.testing = false;
+ Services.prefs.clearUserPref("devtools.devices.url");
+ Services.prefs.clearUserPref("devtools.responsive.html.enabled");
+ Services.prefs.clearUserPref("devtools.responsive.html.displayedDeviceList");
+ asyncStorage.removeItem("devtools.devices.url_cache");
+});
+
+// This depends on the "devtools.responsive.html.enabled" pref
+const { ResponsiveUIManager } = require("resource://devtools/client/responsivedesign/responsivedesign.jsm");
+
+/**
+ * Open responsive design mode for the given tab.
+ */
+var openRDM = Task.async(function* (tab) {
+ info("Opening responsive design mode");
+ let manager = ResponsiveUIManager;
+ let ui = yield manager.openIfNeeded(getOwnerWindow(tab), tab);
+ info("Responsive design mode opened");
+ return { ui, manager };
+});
+
+/**
+ * Close responsive design mode for the given tab.
+ */
+var closeRDM = Task.async(function* (tab, options) {
+ info("Closing responsive design mode");
+ let manager = ResponsiveUIManager;
+ yield manager.closeIfNeeded(getOwnerWindow(tab), tab, options);
+ info("Responsive design mode closed");
+});
+
+/**
+ * Adds a new test task that adds a tab with the given URL, opens responsive
+ * design mode, runs the given generator, closes responsive design mode, and
+ * removes the tab.
+ *
+ * Example usage:
+ *
+ * addRDMTask(TEST_URL, function*({ ui, manager }) {
+ * // Your tests go here...
+ * });
+ */
+function addRDMTask(url, generator) {
+ add_task(function* () {
+ const tab = yield addTab(url);
+ const results = yield openRDM(tab);
+
+ try {
+ yield* generator(results);
+ } catch (err) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(err));
+ }
+
+ yield closeRDM(tab);
+ yield removeTab(tab);
+ });
+}
+
+function spawnViewportTask(ui, args, task) {
+ return ContentTask.spawn(ui.getViewportBrowser(), args, task);
+}
+
+function waitForFrameLoad(ui, targetURL) {
+ return spawnViewportTask(ui, { targetURL }, function* (args) {
+ if ((content.document.readyState == "complete" ||
+ content.document.readyState == "interactive") &&
+ content.location.href == args.targetURL) {
+ return;
+ }
+ yield ContentTaskUtils.waitForEvent(this, "DOMContentLoaded");
+ });
+}
+
+function waitForViewportResizeTo(ui, width, height) {
+ return new Promise(Task.async(function* (resolve) {
+ let isSizeMatching = (data) => data.width == width && data.height == height;
+
+ // If the viewport has already the expected size, we resolve the promise immediately.
+ let size = yield getContentSize(ui);
+ if (isSizeMatching(size)) {
+ resolve();
+ return;
+ }
+
+ // Otherwise, we'll listen to both content's resize event and browser's load end;
+ // since a racing condition can happen, where the content's listener is added after
+ // the resize, because the content's document was reloaded; therefore the test would
+ // hang forever. See bug 1302879.
+ let browser = ui.getViewportBrowser();
+
+ let onResize = (_, data) => {
+ if (!isSizeMatching(data)) {
+ return;
+ }
+ ui.off("content-resize", onResize);
+ browser.removeEventListener("mozbrowserloadend", onBrowserLoadEnd);
+ info(`Got content-resize to ${width} x ${height}`);
+ resolve();
+ };
+
+ let onBrowserLoadEnd = Task.async(function* () {
+ let data = yield getContentSize(ui);
+ onResize(undefined, data);
+ });
+
+ info(`Waiting for content-resize to ${width} x ${height}`);
+ ui.on("content-resize", onResize);
+ browser.addEventListener("mozbrowserloadend",
+ onBrowserLoadEnd, { once: true });
+ }));
+}
+
+var setViewportSize = Task.async(function* (ui, manager, width, height) {
+ let size = ui.getViewportSize();
+ info(`Current size: ${size.width} x ${size.height}, ` +
+ `set to: ${width} x ${height}`);
+ if (size.width != width || size.height != height) {
+ let resized = waitForViewportResizeTo(ui, width, height);
+ ui.setViewportSize({ width, height });
+ yield resized;
+ }
+});
+
+function getElRect(selector, win) {
+ let el = win.document.querySelector(selector);
+ return el.getBoundingClientRect();
+}
+
+/**
+ * Drag an element identified by 'selector' by [x,y] amount. Returns
+ * the rect of the dragged element as it was before drag.
+ */
+function dragElementBy(selector, x, y, win) {
+ let React = win.require("devtools/client/shared/vendor/react");
+ let { Simulate } = React.addons.TestUtils;
+ let rect = getElRect(selector, win);
+ let startPoint = {
+ clientX: rect.left + Math.floor(rect.width / 2),
+ clientY: rect.top + Math.floor(rect.height / 2),
+ };
+ let endPoint = [ startPoint.clientX + x, startPoint.clientY + y ];
+
+ let elem = win.document.querySelector(selector);
+
+ // mousedown is a React listener, need to use its testing tools to avoid races
+ Simulate.mouseDown(elem, startPoint);
+
+ // mousemove and mouseup are regular DOM listeners
+ EventUtils.synthesizeMouseAtPoint(...endPoint, { type: "mousemove" }, win);
+ EventUtils.synthesizeMouseAtPoint(...endPoint, { type: "mouseup" }, win);
+
+ return rect;
+}
+
+function* testViewportResize(ui, selector, moveBy,
+ expectedViewportSize, expectedHandleMove) {
+ let win = ui.toolWindow;
+ let resized = waitForViewportResizeTo(ui, ...expectedViewportSize);
+ let startRect = dragElementBy(selector, ...moveBy, win);
+ yield resized;
+
+ let endRect = getElRect(selector, win);
+ is(endRect.left - startRect.left, expectedHandleMove[0],
+ `The x move of ${selector} is as expected`);
+ is(endRect.top - startRect.top, expectedHandleMove[1],
+ `The y move of ${selector} is as expected`);
+}
+
+function openDeviceModal({ toolWindow }) {
+ let { document } = toolWindow;
+ let React = toolWindow.require("devtools/client/shared/vendor/react");
+ let { Simulate } = React.addons.TestUtils;
+ let select = document.querySelector(".viewport-device-selector");
+ let modal = document.querySelector("#device-modal-wrapper");
+
+ info("Checking initial device modal state");
+ ok(modal.classList.contains("closed") && !modal.classList.contains("opened"),
+ "The device modal is closed by default.");
+
+ info("Opening device modal through device selector.");
+ select.value = OPEN_DEVICE_MODAL_VALUE;
+ Simulate.change(select);
+ ok(modal.classList.contains("opened") && !modal.classList.contains("closed"),
+ "The device modal is displayed.");
+}
+
+function changeSelectValue({ toolWindow }, selector, value) {
+ info(`Selecting ${value} in ${selector}.`);
+
+ return new Promise(resolve => {
+ let select = toolWindow.document.querySelector(selector);
+ isnot(select, null, `selector "${selector}" should match an existing element.`);
+
+ let option = [...select.options].find(o => o.value === String(value));
+ isnot(option, undefined, `value "${value}" should match an existing option.`);
+
+ let event = new toolWindow.UIEvent("change", {
+ view: toolWindow,
+ bubbles: true,
+ cancelable: true
+ });
+
+ select.addEventListener("change", () => {
+ is(select.value, value,
+ `Select's option with value "${value}" should be selected.`);
+ resolve();
+ }, { once: true });
+
+ select.value = value;
+ select.dispatchEvent(event);
+ });
+}
+
+const selectDevice = (ui, value) => Promise.all([
+ once(ui, "device-changed"),
+ changeSelectValue(ui, ".viewport-device-selector", value)
+]);
+
+const selectDPR = (ui, value) =>
+ changeSelectValue(ui, "#global-dpr-selector > select", value);
+
+const selectNetworkThrottling = (ui, value) => Promise.all([
+ once(ui, "network-throttling-changed"),
+ changeSelectValue(ui, "#global-network-throttling-selector", value)
+]);
+
+function getSessionHistory(browser) {
+ return ContentTask.spawn(browser, {}, function* () {
+ /* eslint-disable no-undef */
+ let { interfaces: Ci } = Components;
+ let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
+ let sessionHistory = webNav.sessionHistory;
+ let result = {
+ index: sessionHistory.index,
+ entries: []
+ };
+
+ for (let i = 0; i < sessionHistory.count; i++) {
+ let entry = sessionHistory.getEntryAtIndex(i, false);
+ result.entries.push({
+ uri: entry.URI.spec,
+ title: entry.title
+ });
+ }
+
+ return result;
+ /* eslint-enable no-undef */
+ });
+}
+
+function getContentSize(ui) {
+ return spawnViewportTask(ui, {}, () => ({
+ width: content.screen.width,
+ height: content.screen.height
+ }));
+}
+
+function waitForPageShow(browser) {
+ let mm = browser.messageManager;
+ return new Promise(resolve => {
+ let onShow = message => {
+ if (message.target != browser) {
+ return;
+ }
+ mm.removeMessageListener("PageVisibility:Show", onShow);
+ resolve();
+ };
+ mm.addMessageListener("PageVisibility:Show", onShow);
+ });
+}
+
+function waitForViewportLoad(ui) {
+ return new Promise(resolve => {
+ let browser = ui.getViewportBrowser();
+ browser.addEventListener("mozbrowserloadend", () => {
+ resolve();
+ }, { once: true });
+ });
+}
+
+function load(browser, url) {
+ let loaded = BrowserTestUtils.browserLoaded(browser, false, url);
+ browser.loadURI(url, null, null);
+ return loaded;
+}
+
+function back(browser) {
+ let shown = waitForPageShow(browser);
+ browser.goBack();
+ return shown;
+}
+
+function forward(browser) {
+ let shown = waitForPageShow(browser);
+ browser.goForward();
+ return shown;
+}
+
+function addDeviceForTest(device) {
+ info(`Adding Test Device "${device.name}" to the list.`);
+ addDevice(device);
+
+ registerCleanupFunction(() => {
+ // Note that assertions in cleanup functions are not displayed unless they failed.
+ ok(removeDevice(device), `Removed Test Device "${device.name}" from the list.`);
+ });
+}
+
+function waitForClientClose(ui) {
+ return new Promise(resolve => {
+ info("Waiting for RDM debugger client to close");
+ ui.client.addOneTimeListener("closed", () => {
+ info("RDM's debugger client is now closed");
+ resolve();
+ });
+ });
+}
+
+function* testTouchEventsOverride(ui, expected) {
+ let { document } = ui.toolWindow;
+ let touchButton = document.querySelector("#global-touch-simulation-button");
+
+ let flag = yield ui.emulationFront.getTouchEventsOverride();
+ is(flag === Ci.nsIDocShell.TOUCHEVENTS_OVERRIDE_ENABLED, expected,
+ `Touch events override should be ${expected ? "enabled" : "disabled"}`);
+ is(touchButton.classList.contains("active"), expected,
+ `Touch simulation button should be ${expected ? "" : "in"}active.`);
+}
+
+function testViewportDeviceSelectLabel(ui, expected) {
+ info("Test viewport's device select label");
+
+ let select = ui.toolWindow.document.querySelector(".viewport-device-selector");
+ is(select.selectedOptions[0].textContent, expected,
+ `Device Select value should be: ${expected}`);
+}
+
+function* enableTouchSimulation(ui) {
+ let { document } = ui.toolWindow;
+ let touchButton = document.querySelector("#global-touch-simulation-button");
+ let loaded = waitForViewportLoad(ui);
+ touchButton.click();
+ yield loaded;
+}
diff --git a/devtools/client/responsive.html/test/browser/touch.html b/devtools/client/responsive.html/test/browser/touch.html
new file mode 100644
index 000000000..98aeac68f
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/touch.html
@@ -0,0 +1,86 @@
+<!DOCTYPE html>
+
+<meta charset="utf-8" />
+<meta name="viewport" />
+<title>test</title>
+
+
+<style>
+ div {
+ border :1px solid red;
+ width: 100px; height: 100px;
+ }
+</style>
+
+<div data-is-delay="false"></div>
+
+<script type="text/javascript;version=1.8">
+ "use strict";
+ let div = document.querySelector("div");
+ let initX, initY;
+ let previousEvent = "", touchendTime = 0;
+ let updatePreviousEvent = function (e) {
+ previousEvent = e.type;
+ };
+
+ div.style.transform = "none";
+ div.style.backgroundColor = "";
+
+ div.addEventListener("touchstart", function (evt) {
+ let touch = evt.changedTouches[0];
+ initX = touch.pageX;
+ initY = touch.pageY;
+ updatePreviousEvent(evt);
+ }, true);
+
+ div.addEventListener("touchmove", function (evt) {
+ let touch = evt.changedTouches[0];
+ let deltaX = touch.pageX - initX;
+ let deltaY = touch.pageY - initY;
+ div.style.transform = "translate(" + deltaX + "px, " + deltaY + "px)";
+ updatePreviousEvent(evt);
+ }, true);
+
+ div.addEventListener("touchend", function (evt) {
+ if (!evt.touches.length) {
+ div.style.transform = "none";
+ }
+ touchendTime = performance.now();
+ updatePreviousEvent(evt);
+ }, true);
+
+ div.addEventListener("mouseenter", function (evt) {
+ div.style.backgroundColor = "red";
+ updatePreviousEvent(evt);
+ }, true);
+ div.addEventListener("mouseover", function(evt) {
+ div.style.backgroundColor = "red";
+ updatePreviousEvent(evt);
+ }, true);
+
+ div.addEventListener("mouseout", function (evt) {
+ div.style.backgroundColor = "blue";
+ updatePreviousEvent(evt);
+ }, true);
+
+ div.addEventListener("mouseleave", function (evt) {
+ div.style.backgroundColor = "blue";
+ updatePreviousEvent(evt);
+ }, true);
+
+ div.addEventListener("mousedown", function (evt) {
+ if (previousEvent === "touchend" && touchendTime !== 0) {
+ let now = performance.now();
+ div.dataset.isDelay = ((now - touchendTime) >= 300);
+ } else {
+ div.dataset.isDelay = false;
+ }
+ updatePreviousEvent(evt);
+ }, true);
+
+ div.addEventListener("mousemove", updatePreviousEvent, true);
+
+ div.addEventListener("mouseup", updatePreviousEvent, true);
+
+ div.addEventListener("click", updatePreviousEvent, true);
+</script>
diff --git a/devtools/client/responsive.html/test/unit/.eslintrc.js b/devtools/client/responsive.html/test/unit/.eslintrc.js
new file mode 100644
index 000000000..f879b967b
--- /dev/null
+++ b/devtools/client/responsive.html/test/unit/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for xpcshell.
+ "extends": "../../../../.eslintrc.xpcshell.js"
+};
diff --git a/devtools/client/responsive.html/test/unit/head.js b/devtools/client/responsive.html/test/unit/head.js
new file mode 100644
index 000000000..9c8dbffc4
--- /dev/null
+++ b/devtools/client/responsive.html/test/unit/head.js
@@ -0,0 +1,21 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+
+const { utils: Cu } = Components;
+const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+
+const promise = require("promise");
+const { Task } = require("devtools/shared/task");
+const Store = require("devtools/client/responsive.html/store");
+
+const DevToolsUtils = require("devtools/shared/DevToolsUtils");
+
+const flags = require("devtools/shared/flags");
+flags.testing = true;
+do_register_cleanup(() => {
+ flags.testing = false;
+});
diff --git a/devtools/client/responsive.html/test/unit/test_add_device.js b/devtools/client/responsive.html/test/unit/test_add_device.js
new file mode 100644
index 000000000..0a16d3cf4
--- /dev/null
+++ b/devtools/client/responsive.html/test/unit/test_add_device.js
@@ -0,0 +1,35 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test adding a new device.
+
+const {
+ addDevice,
+ addDeviceType,
+} = require("devtools/client/responsive.html/actions/devices");
+
+add_task(function* () {
+ let store = Store();
+ const { getState, dispatch } = store;
+
+ let device = {
+ "name": "Firefox OS Flame",
+ "width": 320,
+ "height": 570,
+ "pixelRatio": 1.5,
+ "userAgent": "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "fxos"
+ };
+
+ dispatch(addDeviceType("phones"));
+ dispatch(addDevice(device, "phones"));
+
+ equal(getState().devices.phones.length, 1,
+ "Correct number of phones");
+ ok(getState().devices.phones.includes(device),
+ "Device phone list contains Firefox OS Flame");
+});
diff --git a/devtools/client/responsive.html/test/unit/test_add_device_type.js b/devtools/client/responsive.html/test/unit/test_add_device_type.js
new file mode 100644
index 000000000..1c8c65be3
--- /dev/null
+++ b/devtools/client/responsive.html/test/unit/test_add_device_type.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test adding a new device type.
+
+const { addDeviceType } =
+ require("devtools/client/responsive.html/actions/devices");
+
+add_task(function* () {
+ let store = Store();
+ const { getState, dispatch } = store;
+
+ dispatch(addDeviceType("phones"));
+
+ equal(getState().devices.types.length, 1, "Correct number of device types");
+ equal(getState().devices.phones.length, 0,
+ "Defaults to an empty array of phones");
+ ok(getState().devices.types.includes("phones"),
+ "Device types contain phones");
+});
diff --git a/devtools/client/responsive.html/test/unit/test_add_viewport.js b/devtools/client/responsive.html/test/unit/test_add_viewport.js
new file mode 100644
index 000000000..b2fc3613d
--- /dev/null
+++ b/devtools/client/responsive.html/test/unit/test_add_viewport.js
@@ -0,0 +1,23 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test adding viewports to the page.
+
+const { addViewport } =
+ require("devtools/client/responsive.html/actions/viewports");
+
+add_task(function* () {
+ let store = Store();
+ const { getState, dispatch } = store;
+
+ equal(getState().viewports.length, 0, "Defaults to no viewpots at startup");
+
+ dispatch(addViewport());
+ equal(getState().viewports.length, 1, "One viewport total");
+
+ // For the moment, there can be at most one viewport.
+ dispatch(addViewport());
+ equal(getState().viewports.length, 1, "One viewport total, again");
+});
diff --git a/devtools/client/responsive.html/test/unit/test_change_device.js b/devtools/client/responsive.html/test/unit/test_change_device.js
new file mode 100644
index 000000000..0e7a6c87a
--- /dev/null
+++ b/devtools/client/responsive.html/test/unit/test_change_device.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test changing the viewport device.
+
+const {
+ addDevice,
+ addDeviceType,
+} = require("devtools/client/responsive.html/actions/devices");
+const {
+ addViewport,
+ changeDevice,
+} = require("devtools/client/responsive.html/actions/viewports");
+
+add_task(function* () {
+ let store = Store();
+ const { getState, dispatch } = store;
+
+ dispatch(addDeviceType("phones"));
+ dispatch(addDevice({
+ "name": "Firefox OS Flame",
+ "width": 320,
+ "height": 570,
+ "pixelRatio": 1.5,
+ "userAgent": "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "fxos"
+ }, "phones"));
+ dispatch(addViewport());
+
+ let viewport = getState().viewports[0];
+ equal(viewport.device, "", "Default device is unselected");
+
+ dispatch(changeDevice(0, "Firefox OS Flame"));
+
+ viewport = getState().viewports[0];
+ equal(viewport.device, "Firefox OS Flame",
+ "Changed to Firefox OS Flame device");
+});
diff --git a/devtools/client/responsive.html/test/unit/test_change_display_pixel_ratio.js b/devtools/client/responsive.html/test/unit/test_change_display_pixel_ratio.js
new file mode 100644
index 000000000..d8d968c2d
--- /dev/null
+++ b/devtools/client/responsive.html/test/unit/test_change_display_pixel_ratio.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test changing the display pixel ratio.
+
+const { changeDisplayPixelRatio } =
+ require("devtools/client/responsive.html/actions/display-pixel-ratio");
+const NEW_PIXEL_RATIO = 5.5;
+
+add_task(function* () {
+ let store = Store();
+ const { getState, dispatch } = store;
+
+ equal(getState().displayPixelRatio, 0,
+ "Defaults to 0 at startup");
+
+ dispatch(changeDisplayPixelRatio(NEW_PIXEL_RATIO));
+ equal(getState().displayPixelRatio, NEW_PIXEL_RATIO,
+ `Display Pixel Ratio changed to ${NEW_PIXEL_RATIO}`);
+});
diff --git a/devtools/client/responsive.html/test/unit/test_change_location.js b/devtools/client/responsive.html/test/unit/test_change_location.js
new file mode 100644
index 000000000..d45ce5c7a
--- /dev/null
+++ b/devtools/client/responsive.html/test/unit/test_change_location.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test changing the location of the displayed page.
+
+const { changeLocation } =
+ require("devtools/client/responsive.html/actions/location");
+
+const TEST_URL = "http://example.com";
+
+add_task(function* () {
+ let store = Store();
+ const { getState, dispatch } = store;
+
+ equal(getState().location, "about:blank",
+ "Defaults to about:blank at startup");
+
+ dispatch(changeLocation(TEST_URL));
+ equal(getState().location, TEST_URL, "Location changed to TEST_URL");
+});
diff --git a/devtools/client/responsive.html/test/unit/test_change_network_throttling.js b/devtools/client/responsive.html/test/unit/test_change_network_throttling.js
new file mode 100644
index 000000000..c20ae8133
--- /dev/null
+++ b/devtools/client/responsive.html/test/unit/test_change_network_throttling.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test changing the network throttling state
+
+const {
+ changeNetworkThrottling,
+} = require("devtools/client/responsive.html/actions/network-throttling");
+
+add_task(function* () {
+ let store = Store();
+ const { getState, dispatch } = store;
+
+ ok(!getState().networkThrottling.enabled,
+ "Network throttling is disabled by default.");
+ equal(getState().networkThrottling.profile, "",
+ "Network throttling profile is empty by default.");
+
+ dispatch(changeNetworkThrottling(true, "Bob"));
+
+ ok(getState().networkThrottling.enabled,
+ "Network throttling is enabled.");
+ equal(getState().networkThrottling.profile, "Bob",
+ "Network throttling profile is set.");
+});
diff --git a/devtools/client/responsive.html/test/unit/test_change_pixel_ratio.js b/devtools/client/responsive.html/test/unit/test_change_pixel_ratio.js
new file mode 100644
index 000000000..b594caef5
--- /dev/null
+++ b/devtools/client/responsive.html/test/unit/test_change_pixel_ratio.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test changing the viewport pixel ratio.
+
+const { addViewport, changePixelRatio } =
+ require("devtools/client/responsive.html/actions/viewports");
+const NEW_PIXEL_RATIO = 5.5;
+
+add_task(function* () {
+ let store = Store();
+ const { getState, dispatch } = store;
+
+ dispatch(addViewport());
+ dispatch(changePixelRatio(0, NEW_PIXEL_RATIO));
+
+ let viewport = getState().viewports[0];
+ equal(viewport.pixelRatio.value, NEW_PIXEL_RATIO,
+ `Viewport's pixel ratio changed to ${NEW_PIXEL_RATIO}`);
+});
diff --git a/devtools/client/responsive.html/test/unit/test_resize_viewport.js b/devtools/client/responsive.html/test/unit/test_resize_viewport.js
new file mode 100644
index 000000000..4b85554bf
--- /dev/null
+++ b/devtools/client/responsive.html/test/unit/test_resize_viewport.js
@@ -0,0 +1,21 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test resizing the viewport.
+
+const { addViewport, resizeViewport } =
+ require("devtools/client/responsive.html/actions/viewports");
+
+add_task(function* () {
+ let store = Store();
+ const { getState, dispatch } = store;
+
+ dispatch(addViewport());
+ dispatch(resizeViewport(0, 500, 500));
+
+ let viewport = getState().viewports[0];
+ equal(viewport.width, 500, "Resized width of 500");
+ equal(viewport.height, 500, "Resized height of 500");
+});
diff --git a/devtools/client/responsive.html/test/unit/test_rotate_viewport.js b/devtools/client/responsive.html/test/unit/test_rotate_viewport.js
new file mode 100644
index 000000000..541fadaa7
--- /dev/null
+++ b/devtools/client/responsive.html/test/unit/test_rotate_viewport.js
@@ -0,0 +1,25 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test rotating the viewport.
+
+const { addViewport, rotateViewport } =
+ require("devtools/client/responsive.html/actions/viewports");
+
+add_task(function* () {
+ let store = Store();
+ const { getState, dispatch } = store;
+
+ dispatch(addViewport());
+
+ let viewport = getState().viewports[0];
+ equal(viewport.width, 320, "Default width of 320");
+ equal(viewport.height, 480, "Default height of 480");
+
+ dispatch(rotateViewport(0));
+ viewport = getState().viewports[0];
+ equal(viewport.width, 480, "Rotated width of 480");
+ equal(viewport.height, 320, "Rotated height of 320");
+});
diff --git a/devtools/client/responsive.html/test/unit/test_update_device_displayed.js b/devtools/client/responsive.html/test/unit/test_update_device_displayed.js
new file mode 100644
index 000000000..34c59bb2a
--- /dev/null
+++ b/devtools/client/responsive.html/test/unit/test_update_device_displayed.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test updating the device `displayed` property
+
+const {
+ addDevice,
+ addDeviceType,
+ updateDeviceDisplayed,
+} = require("devtools/client/responsive.html/actions/devices");
+
+add_task(function* () {
+ let store = Store();
+ const { getState, dispatch } = store;
+
+ let device = {
+ "name": "Firefox OS Flame",
+ "width": 320,
+ "height": 570,
+ "pixelRatio": 1.5,
+ "userAgent": "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "fxos"
+ };
+
+ dispatch(addDeviceType("phones"));
+ dispatch(addDevice(device, "phones"));
+ dispatch(updateDeviceDisplayed(device, "phones", true));
+
+ equal(getState().devices.phones.length, 1,
+ "Correct number of phones");
+ ok(getState().devices.phones[0].displayed,
+ "Device phone list contains enabled Firefox OS Flame");
+});
diff --git a/devtools/client/responsive.html/test/unit/test_update_touch_simulation_enabled.js b/devtools/client/responsive.html/test/unit/test_update_touch_simulation_enabled.js
new file mode 100644
index 000000000..f8ba2a4b6
--- /dev/null
+++ b/devtools/client/responsive.html/test/unit/test_update_touch_simulation_enabled.js
@@ -0,0 +1,23 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test updating the touch simulation `enabled` property
+
+const {
+ changeTouchSimulation,
+} = require("devtools/client/responsive.html/actions/touch-simulation");
+
+add_task(function* () {
+ let store = Store();
+ const { getState, dispatch } = store;
+
+ ok(!getState().touchSimulation.enabled,
+ "Touch simulation is disabled by default.");
+
+ dispatch(changeTouchSimulation(true));
+
+ ok(getState().touchSimulation.enabled,
+ "Touch simulation is enabled.");
+});
diff --git a/devtools/client/responsive.html/test/unit/xpcshell.ini b/devtools/client/responsive.html/test/unit/xpcshell.ini
new file mode 100644
index 000000000..06b5e4994
--- /dev/null
+++ b/devtools/client/responsive.html/test/unit/xpcshell.ini
@@ -0,0 +1,18 @@
+[DEFAULT]
+tags = devtools
+head = head.js ../../../framework/test/shared-redux-head.js
+tail =
+firefox-appdir = browser
+
+[test_add_device.js]
+[test_add_device_type.js]
+[test_add_viewport.js]
+[test_change_device.js]
+[test_change_display_pixel_ratio.js]
+[test_change_location.js]
+[test_change_network_throttling.js]
+[test_change_pixel_ratio.js]
+[test_resize_viewport.js]
+[test_rotate_viewport.js]
+[test_update_device_displayed.js]
+[test_update_touch_simulation_enabled.js]
diff --git a/devtools/client/responsive.html/types.js b/devtools/client/responsive.html/types.js
new file mode 100644
index 000000000..2f03cdf65
--- /dev/null
+++ b/devtools/client/responsive.html/types.js
@@ -0,0 +1,164 @@
+/* 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 { PropTypes } = require("devtools/client/shared/vendor/react");
+const { createEnum } = require("./utils/enum");
+
+// React PropTypes are used to describe the expected "shape" of various common
+// objects that get passed down as props to components.
+
+/* GLOBAL */
+
+/**
+ * The location of the document displayed in the viewport(s).
+ */
+exports.location = PropTypes.string;
+
+/* DEVICE */
+
+/**
+ * A single device that can be displayed in the viewport.
+ */
+const device = {
+
+ // The name of the device
+ name: PropTypes.string,
+
+ // The width of the device
+ width: PropTypes.number,
+
+ // The height of the device
+ height: PropTypes.number,
+
+ // The pixel ratio of the device
+ pixelRatio: PropTypes.number,
+
+ // The user agent string of the device
+ userAgent: PropTypes.string,
+
+ // Whether or not it is a touch device
+ touch: PropTypes.bool,
+
+ // The operating system of the device
+ os: PropTypes.String,
+
+ // Whether or not the device is displayed in the device selector
+ displayed: PropTypes.bool,
+
+};
+
+/**
+ * An enum containing the possible values for the device list state
+ */
+exports.deviceListState = createEnum([
+ "INITIALIZED",
+ "LOADING",
+ "LOADED",
+ "ERROR",
+]);
+
+/**
+ * A list of devices and their types that can be displayed in the viewport.
+ */
+exports.devices = {
+
+ // An array of device types
+ types: PropTypes.arrayOf(PropTypes.string),
+
+ // An array of phone devices
+ phones: PropTypes.arrayOf(PropTypes.shape(device)),
+
+ // An array of tablet devices
+ tablets: PropTypes.arrayOf(PropTypes.shape(device)),
+
+ // An array of laptop devices
+ laptops: PropTypes.arrayOf(PropTypes.shape(device)),
+
+ // An array of television devices
+ televisions: PropTypes.arrayOf(PropTypes.shape(device)),
+
+ // An array of console devices
+ consoles: PropTypes.arrayOf(PropTypes.shape(device)),
+
+ // An array of watch devices
+ watches: PropTypes.arrayOf(PropTypes.shape(device)),
+
+ // Whether or not the device modal is open
+ isModalOpen: PropTypes.bool,
+
+ // Device list state, possible values are exported above in an enum
+ listState: PropTypes.oneOf(Object.keys(exports.deviceListState)),
+
+};
+
+/* VIEWPORT */
+
+/**
+ * Network throttling state for a given viewport.
+ */
+exports.networkThrottling = {
+
+ // Whether or not network throttling is enabled
+ enabled: PropTypes.bool,
+
+ // Name of the selected throttling profile
+ profile: PropTypes.string,
+
+};
+
+/**
+ * Device pixel ratio for a given viewport.
+ */
+const pixelRatio = exports.pixelRatio = {
+
+ // The device pixel ratio value
+ value: PropTypes.number,
+
+};
+
+/**
+ * Touch simulation state for a given viewport.
+ */
+exports.touchSimulation = {
+
+ // Whether or not touch simulation is enabled
+ enabled: PropTypes.bool,
+
+};
+
+/**
+ * A single viewport displaying a document.
+ */
+exports.viewport = {
+
+ // The id of the viewport
+ id: PropTypes.number,
+
+ // The currently selected device applied to the viewport
+ device: PropTypes.string,
+
+ // The width of the viewport
+ width: PropTypes.number,
+
+ // The height of the viewport
+ height: PropTypes.number,
+
+ // The devicePixelRatio of the viewport
+ pixelRatio: PropTypes.shape(pixelRatio),
+
+};
+
+/* ACTIONS IN PROGRESS */
+
+/**
+ * The progression of the screenshot.
+ */
+exports.screenshot = {
+
+ // Whether screenshot capturing is in progress
+ isCapturing: PropTypes.bool,
+
+};
diff --git a/devtools/client/responsive.html/utils/e10s.js b/devtools/client/responsive.html/utils/e10s.js
new file mode 100644
index 000000000..f45add6b0
--- /dev/null
+++ b/devtools/client/responsive.html/utils/e10s.js
@@ -0,0 +1,103 @@
+/* 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 { defer } = require("promise");
+
+// The prefix used for RDM messages in content.
+// see: devtools/client/responsivedesign/responsivedesign-child.js
+const MESSAGE_PREFIX = "ResponsiveMode:";
+const REQUEST_DONE_SUFFIX = ":Done";
+
+/**
+ * Registers a message `listener` that is called every time messages of
+ * specified `message` is emitted on the given message manager.
+ * @param {nsIMessageListenerManager} mm
+ * The Message Manager
+ * @param {String} message
+ * The message. It will be prefixed with the constant `MESSAGE_PREFIX`
+ * @param {Function} listener
+ * The listener function that processes the message.
+ */
+function on(mm, message, listener) {
+ mm.addMessageListener(MESSAGE_PREFIX + message, listener);
+}
+exports.on = on;
+
+/**
+ * Removes a message `listener` for the specified `message` on the given
+ * message manager.
+ * @param {nsIMessageListenerManager} mm
+ * The Message Manager
+ * @param {String} message
+ * The message. It will be prefixed with the constant `MESSAGE_PREFIX`
+ * @param {Function} listener
+ * The listener function that processes the message.
+ */
+function off(mm, message, listener) {
+ mm.removeMessageListener(MESSAGE_PREFIX + message, listener);
+}
+exports.off = off;
+
+/**
+ * Resolves a promise the next time the specified `message` is sent over the
+ * given message manager.
+ * @param {nsIMessageListenerManager} mm
+ * The Message Manager
+ * @param {String} message
+ * The message. It will be prefixed with the constant `MESSAGE_PREFIX`
+ * @returns {Promise}
+ * A promise that is resolved when the given message is emitted.
+ */
+function once(mm, message) {
+ let { resolve, promise } = defer();
+
+ on(mm, message, function onMessage({data}) {
+ off(mm, message, onMessage);
+ resolve(data);
+ });
+
+ return promise;
+}
+exports.once = once;
+
+/**
+ * Asynchronously emit a `message` to the listeners of the given message
+ * manager.
+ *
+ * @param {nsIMessageListenerManager} mm
+ * The Message Manager
+ * @param {String} message
+ * The message. It will be prefixed with the constant `MESSAGE_PREFIX`.
+ * @param {Object} data
+ * A JSON object containing data to be delivered to the listeners.
+ */
+function emit(mm, message, data) {
+ mm.sendAsyncMessage(MESSAGE_PREFIX + message, data);
+}
+exports.emit = emit;
+
+/**
+ * Asynchronously send a "request" over the given message manager, and returns
+ * a promise that is resolved when the request is complete.
+ *
+ * @param {nsIMessageListenerManager} mm
+ * The Message Manager
+ * @param {String} message
+ * The message. It will be prefixed with the constant `MESSAGE_PREFIX`, and
+ * also suffixed with `REQUEST_DONE_SUFFIX` for the reply.
+ * @param {Object} data
+ * A JSON object containing data to be delivered to the listeners.
+ * @returns {Promise}
+ * A promise that is resolved when the request is done.
+ */
+function request(mm, message, data) {
+ let done = once(mm, message + REQUEST_DONE_SUFFIX);
+
+ emit(mm, message, data);
+
+ return done;
+}
+exports.request = request;
diff --git a/devtools/client/responsive.html/utils/enum.js b/devtools/client/responsive.html/utils/enum.js
new file mode 100644
index 000000000..cab8ff1ce
--- /dev/null
+++ b/devtools/client/responsive.html/utils/enum.js
@@ -0,0 +1,21 @@
+/* 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";
+
+module.exports = {
+
+ /**
+ * Create a simple enum-like object with keys mirrored to values from an array.
+ * This makes comparison to a specfic value simpler without having to repeat and
+ * mis-type the value.
+ */
+ createEnum(array, target = {}) {
+ for (let key of array) {
+ target[key] = key;
+ }
+ return target;
+ }
+
+};
diff --git a/devtools/client/responsive.html/utils/l10n.js b/devtools/client/responsive.html/utils/l10n.js
new file mode 100644
index 000000000..515182462
--- /dev/null
+++ b/devtools/client/responsive.html/utils/l10n.js
@@ -0,0 +1,16 @@
+/* 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 { LocalizationHelper } = require("devtools/shared/l10n");
+const STRINGS_URI = "devtools/client/locales/responsive.properties";
+const L10N = new LocalizationHelper(STRINGS_URI);
+
+module.exports = {
+ getStr: (...args) => L10N.getStr(...args),
+ getFormatStr: (...args) => L10N.getFormatStr(...args),
+ getFormatStrWithNumbers: (...args) => L10N.getFormatStrWithNumbers(...args),
+ numberWithDecimals: (...args) => L10N.numberWithDecimals(...args),
+};
diff --git a/devtools/client/responsive.html/utils/message.js b/devtools/client/responsive.html/utils/message.js
new file mode 100644
index 000000000..d5c5b012f
--- /dev/null
+++ b/devtools/client/responsive.html/utils/message.js
@@ -0,0 +1,38 @@
+/* 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 REQUEST_DONE_SUFFIX = ":done";
+
+function wait(win, type) {
+ let deferred = promise.defer();
+
+ let onMessage = event => {
+ if (event.data.type !== type) {
+ return;
+ }
+ win.removeEventListener("message", onMessage);
+ deferred.resolve();
+ };
+ win.addEventListener("message", onMessage);
+
+ return deferred.promise;
+}
+
+function post(win, type) {
+ win.postMessage({ type }, "*");
+}
+
+function request(win, type) {
+ let done = wait(win, type + REQUEST_DONE_SUFFIX);
+ post(win, type);
+ return done;
+}
+
+exports.wait = wait;
+exports.post = post;
+exports.request = request;
diff --git a/devtools/client/responsive.html/utils/moz.build b/devtools/client/responsive.html/utils/moz.build
new file mode 100644
index 000000000..a716eae0c
--- /dev/null
+++ b/devtools/client/responsive.html/utils/moz.build
@@ -0,0 +1,12 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'e10s.js',
+ 'enum.js',
+ 'l10n.js',
+ 'message.js',
+)
diff --git a/devtools/client/responsivedesign/moz.build b/devtools/client/responsivedesign/moz.build
new file mode 100644
index 000000000..215b4e8fa
--- /dev/null
+++ b/devtools/client/responsivedesign/moz.build
@@ -0,0 +1,11 @@
+# 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/.
+
+BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
+
+DevToolsModules(
+ 'resize-commands.js',
+ 'responsivedesign-child.js',
+ 'responsivedesign.jsm',
+)
diff --git a/devtools/client/responsivedesign/resize-commands.js b/devtools/client/responsivedesign/resize-commands.js
new file mode 100644
index 000000000..b2f884df8
--- /dev/null
+++ b/devtools/client/responsivedesign/resize-commands.js
@@ -0,0 +1,96 @@
+/* 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");
+
+loader.lazyImporter(this, "ResponsiveUIManager", "resource://devtools/client/responsivedesign/responsivedesign.jsm");
+
+const BRAND_SHORT_NAME = Cc["@mozilla.org/intl/stringbundle;1"].
+ getService(Ci.nsIStringBundleService).
+ createBundle("chrome://branding/locale/brand.properties").
+ GetStringFromName("brandShortName");
+
+const l10n = require("gcli/l10n");
+
+exports.items = [
+ {
+ name: "resize",
+ description: l10n.lookup("resizeModeDesc")
+ },
+ {
+ item: "command",
+ runAt: "client",
+ name: "resize on",
+ description: l10n.lookup("resizeModeOnDesc"),
+ manual: l10n.lookupFormat("resizeModeManual2", [BRAND_SHORT_NAME]),
+ exec: gcli_cmd_resize
+ },
+ {
+ item: "command",
+ runAt: "client",
+ name: "resize off",
+ description: l10n.lookup("resizeModeOffDesc"),
+ manual: l10n.lookupFormat("resizeModeManual2", [BRAND_SHORT_NAME]),
+ exec: gcli_cmd_resize
+ },
+ {
+ item: "command",
+ runAt: "client",
+ name: "resize toggle",
+ buttonId: "command-button-responsive",
+ buttonClass: "command-button command-button-invertable",
+ tooltipText: l10n.lookup("resizeModeToggleTooltip"),
+ description: l10n.lookup("resizeModeToggleDesc"),
+ manual: l10n.lookupFormat("resizeModeManual2", [BRAND_SHORT_NAME]),
+ state: {
+ isChecked: function (aTarget) {
+ if (!aTarget.tab) {
+ return false;
+ }
+ return ResponsiveUIManager.isActiveForTab(aTarget.tab);
+ },
+ onChange: function (aTarget, aChangeHandler) {
+ if (aTarget.tab) {
+ ResponsiveUIManager.on("on", aChangeHandler);
+ ResponsiveUIManager.on("off", aChangeHandler);
+ }
+ },
+ offChange: function (aTarget, aChangeHandler) {
+ // Do not check for target.tab as it may already be null during destroy
+ ResponsiveUIManager.off("on", aChangeHandler);
+ ResponsiveUIManager.off("off", aChangeHandler);
+ },
+ },
+ exec: gcli_cmd_resize
+ },
+ {
+ item: "command",
+ runAt: "client",
+ name: "resize to",
+ description: l10n.lookup("resizeModeToDesc"),
+ params: [
+ {
+ name: "width",
+ type: "number",
+ description: l10n.lookup("resizePageArgWidthDesc"),
+ },
+ {
+ name: "height",
+ type: "number",
+ description: l10n.lookup("resizePageArgHeightDesc"),
+ },
+ ],
+ exec: gcli_cmd_resize
+ }
+];
+
+function* gcli_cmd_resize(args, context) {
+ let browserWindow = context.environment.chromeWindow;
+ yield ResponsiveUIManager.handleGcliCommand(browserWindow,
+ browserWindow.gBrowser.selectedTab,
+ this.name,
+ args);
+}
diff --git a/devtools/client/responsivedesign/responsivedesign-child.js b/devtools/client/responsivedesign/responsivedesign-child.js
new file mode 100644
index 000000000..a6ce091e2
--- /dev/null
+++ b/devtools/client/responsivedesign/responsivedesign-child.js
@@ -0,0 +1,195 @@
+/* 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";
+
+/* global content, docShell, addEventListener, addMessageListener,
+ removeEventListener, removeMessageListener, sendAsyncMessage, Services */
+
+var global = this;
+
+// Guard against loading this frame script mutiple times
+(function () {
+ if (global.responsiveFrameScriptLoaded) {
+ return;
+ }
+
+ var Ci = Components.interfaces;
+ const gDeviceSizeWasPageSize = docShell.deviceSizeIsPageSize;
+ const gFloatingScrollbarsStylesheet = Services.io.newURI("chrome://devtools/skin/floating-scrollbars-responsive-design.css", null, null);
+ var gRequiresFloatingScrollbars;
+
+ var active = false;
+ var resizeNotifications = false;
+
+ addMessageListener("ResponsiveMode:Start", startResponsiveMode);
+ addMessageListener("ResponsiveMode:Stop", stopResponsiveMode);
+ addMessageListener("ResponsiveMode:IsActive", isActive);
+
+ function debug(msg) {
+ // dump(`RDM CHILD: ${msg}\n`);
+ }
+
+ /**
+ * Used by tests to verify the state of responsive mode.
+ */
+ function isActive() {
+ sendAsyncMessage("ResponsiveMode:IsActive:Done", { active });
+ }
+
+ function startResponsiveMode({data:data}) {
+ debug("START");
+ if (active) {
+ debug("ALREADY STARTED");
+ sendAsyncMessage("ResponsiveMode:Start:Done");
+ return;
+ }
+ addMessageListener("ResponsiveMode:RequestScreenshot", screenshot);
+ let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebProgress);
+ webProgress.addProgressListener(WebProgressListener, Ci.nsIWebProgress.NOTIFY_ALL);
+ docShell.deviceSizeIsPageSize = true;
+ gRequiresFloatingScrollbars = data.requiresFloatingScrollbars;
+ if (data.notifyOnResize) {
+ startOnResize();
+ }
+
+ // At this point, a content viewer might not be loaded for this
+ // docshell. makeScrollbarsFloating will be triggered by onLocationChange.
+ if (docShell.contentViewer) {
+ makeScrollbarsFloating();
+ }
+ active = true;
+ sendAsyncMessage("ResponsiveMode:Start:Done");
+ }
+
+ function onResize() {
+ let { width, height } = content.screen;
+ debug(`EMIT RESIZE: ${width} x ${height}`);
+ sendAsyncMessage("ResponsiveMode:OnContentResize", {
+ width,
+ height,
+ });
+ }
+
+ function bindOnResize() {
+ content.addEventListener("resize", onResize, false);
+ }
+
+ function startOnResize() {
+ debug("START ON RESIZE");
+ if (resizeNotifications) {
+ return;
+ }
+ resizeNotifications = true;
+ bindOnResize();
+ addEventListener("DOMWindowCreated", bindOnResize, false);
+ }
+
+ function stopOnResize() {
+ debug("STOP ON RESIZE");
+ if (!resizeNotifications) {
+ return;
+ }
+ resizeNotifications = false;
+ content.removeEventListener("resize", onResize, false);
+ removeEventListener("DOMWindowCreated", bindOnResize, false);
+ }
+
+ function stopResponsiveMode() {
+ debug("STOP");
+ if (!active) {
+ debug("ALREADY STOPPED, ABORT");
+ return;
+ }
+ active = false;
+ removeMessageListener("ResponsiveMode:RequestScreenshot", screenshot);
+ let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebProgress);
+ webProgress.removeProgressListener(WebProgressListener);
+ docShell.deviceSizeIsPageSize = gDeviceSizeWasPageSize;
+ restoreScrollbars();
+ stopOnResize();
+ sendAsyncMessage("ResponsiveMode:Stop:Done");
+ }
+
+ function makeScrollbarsFloating() {
+ if (!gRequiresFloatingScrollbars) {
+ return;
+ }
+
+ let allDocShells = [docShell];
+
+ for (let i = 0; i < docShell.childCount; i++) {
+ let child = docShell.getChildAt(i).QueryInterface(Ci.nsIDocShell);
+ allDocShells.push(child);
+ }
+
+ for (let d of allDocShells) {
+ let win = d.contentViewer.DOMDocument.defaultView;
+ let winUtils = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+ try {
+ winUtils.loadSheet(gFloatingScrollbarsStylesheet, win.AGENT_SHEET);
+ } catch (e) { }
+ }
+
+ flushStyle();
+ }
+
+ function restoreScrollbars() {
+ let allDocShells = [docShell];
+ for (let i = 0; i < docShell.childCount; i++) {
+ allDocShells.push(docShell.getChildAt(i).QueryInterface(Ci.nsIDocShell));
+ }
+ for (let d of allDocShells) {
+ let win = d.contentViewer.DOMDocument.defaultView;
+ let winUtils = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+ try {
+ winUtils.removeSheet(gFloatingScrollbarsStylesheet, win.AGENT_SHEET);
+ } catch (e) { }
+ }
+ flushStyle();
+ }
+
+ function flushStyle() {
+ // Force presContext destruction
+ let isSticky = docShell.contentViewer.sticky;
+ docShell.contentViewer.sticky = false;
+ docShell.contentViewer.hide();
+ docShell.contentViewer.show();
+ docShell.contentViewer.sticky = isSticky;
+ }
+
+ function screenshot() {
+ let canvas = content.document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
+ let ratio = content.devicePixelRatio;
+ let width = content.innerWidth * ratio;
+ let height = content.innerHeight * ratio;
+ canvas.mozOpaque = true;
+ canvas.width = width;
+ canvas.height = height;
+ let ctx = canvas.getContext("2d");
+ ctx.scale(ratio, ratio);
+ ctx.drawWindow(content, content.scrollX, content.scrollY, width, height, "#fff");
+ sendAsyncMessage("ResponsiveMode:RequestScreenshot:Done", canvas.toDataURL());
+ }
+
+ var WebProgressListener = {
+ onLocationChange(webProgress, request, URI, flags) {
+ if (flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) {
+ return;
+ }
+ makeScrollbarsFloating();
+ },
+ QueryInterface: function QueryInterface(aIID) {
+ if (aIID.equals(Ci.nsIWebProgressListener) ||
+ aIID.equals(Ci.nsISupportsWeakReference) ||
+ aIID.equals(Ci.nsISupports)) {
+ return this;
+ }
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+ };
+})();
+
+global.responsiveFrameScriptLoaded = true;
+sendAsyncMessage("ResponsiveMode:ChildScriptReady");
diff --git a/devtools/client/responsivedesign/responsivedesign.jsm b/devtools/client/responsivedesign/responsivedesign.jsm
new file mode 100644
index 000000000..f67d1912c
--- /dev/null
+++ b/devtools/client/responsivedesign/responsivedesign.jsm
@@ -0,0 +1,1193 @@
+/* -*- 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/. */
+
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+var {loader, require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+var Telemetry = require("devtools/client/shared/telemetry");
+var {showDoorhanger} = require("devtools/client/shared/doorhanger");
+var {TouchEventSimulator} = require("devtools/shared/touch/simulator");
+var {Task} = require("devtools/shared/task");
+var promise = require("promise");
+var DevToolsUtils = require("devtools/shared/DevToolsUtils");
+var flags = require("devtools/shared/flags");
+var Services = require("Services");
+var EventEmitter = require("devtools/shared/event-emitter");
+var {ViewHelpers} = require("devtools/client/shared/widgets/view-helpers");
+var { LocalizationHelper } = require("devtools/shared/l10n");
+var { EmulationFront } = require("devtools/shared/fronts/emulation");
+
+loader.lazyImporter(this, "SystemAppProxy",
+ "resource://gre/modules/SystemAppProxy.jsm");
+loader.lazyRequireGetter(this, "DebuggerClient",
+ "devtools/shared/client/main", true);
+loader.lazyRequireGetter(this, "DebuggerServer",
+ "devtools/server/main", true);
+
+this.EXPORTED_SYMBOLS = ["ResponsiveUIManager"];
+
+const MIN_WIDTH = 50;
+const MIN_HEIGHT = 50;
+
+const MAX_WIDTH = 10000;
+const MAX_HEIGHT = 10000;
+
+const SLOW_RATIO = 6;
+const ROUND_RATIO = 10;
+
+const INPUT_PARSER = /(\d+)[^\d]+(\d+)/;
+
+const SHARED_L10N = new LocalizationHelper("devtools/client/locales/shared.properties");
+
+function debug(msg) {
+ // dump(`RDM UI: ${msg}\n`);
+}
+
+var ActiveTabs = new Map();
+
+var Manager = {
+ /**
+ * Check if the a tab is in a responsive mode.
+ * Leave the responsive mode if active,
+ * active the responsive mode if not active.
+ *
+ * @param aWindow the main window.
+ * @param aTab the tab targeted.
+ */
+ toggle: function (aWindow, aTab) {
+ if (this.isActiveForTab(aTab)) {
+ ActiveTabs.get(aTab).close();
+ } else {
+ this.openIfNeeded(aWindow, aTab);
+ }
+ },
+
+ /**
+ * Launches the responsive mode.
+ *
+ * @param aWindow the main window.
+ * @param aTab the tab targeted.
+ * @returns {ResponsiveUI} the instance of ResponsiveUI for the current tab.
+ */
+ openIfNeeded: Task.async(function* (aWindow, aTab) {
+ let ui;
+ if (!this.isActiveForTab(aTab)) {
+ ui = new ResponsiveUI(aWindow, aTab);
+ yield ui.inited;
+ } else {
+ ui = this.getResponsiveUIForTab(aTab);
+ }
+ return ui;
+ }),
+
+ /**
+ * Returns true if responsive view is active for the provided tab.
+ *
+ * @param aTab the tab targeted.
+ */
+ isActiveForTab: function (aTab) {
+ return ActiveTabs.has(aTab);
+ },
+
+ /**
+ * Return the responsive UI controller for a tab.
+ */
+ getResponsiveUIForTab: function (aTab) {
+ return ActiveTabs.get(aTab);
+ },
+
+ /**
+ * Handle gcli commands.
+ *
+ * @param aWindow the browser window.
+ * @param aTab the tab targeted.
+ * @param aCommand the command name.
+ * @param aArgs command arguments.
+ */
+ handleGcliCommand: Task.async(function* (aWindow, aTab, aCommand, aArgs) {
+ switch (aCommand) {
+ case "resize to":
+ let ui = yield this.openIfNeeded(aWindow, aTab);
+ ui.setViewportSize(aArgs);
+ break;
+ case "resize on":
+ this.openIfNeeded(aWindow, aTab);
+ break;
+ case "resize off":
+ if (this.isActiveForTab(aTab)) {
+ yield ActiveTabs.get(aTab).close();
+ }
+ break;
+ case "resize toggle":
+ this.toggle(aWindow, aTab);
+ default:
+ }
+ })
+};
+
+EventEmitter.decorate(Manager);
+
+// If the new HTML RDM UI is enabled and e10s is enabled by default (e10s is required for
+// the new HTML RDM UI to function), delegate the ResponsiveUIManager API over to that
+// tool instead. Performing this delegation here allows us to contain the pref check to a
+// single place.
+if (Services.prefs.getBoolPref("devtools.responsive.html.enabled") &&
+ Services.appinfo.browserTabsRemoteAutostart) {
+ let { ResponsiveUIManager } =
+ require("devtools/client/responsive.html/manager");
+ this.ResponsiveUIManager = ResponsiveUIManager;
+} else {
+ this.ResponsiveUIManager = Manager;
+}
+
+var defaultPresets = [
+ // Phones
+ {key: "320x480", width: 320, height: 480}, // iPhone, B2G, with <meta viewport>
+ {key: "360x640", width: 360, height: 640}, // Android 4, phones, with <meta viewport>
+
+ // Tablets
+ {key: "768x1024", width: 768, height: 1024}, // iPad, with <meta viewport>
+ {key: "800x1280", width: 800, height: 1280}, // Android 4, Tablet, with <meta viewport>
+
+ // Default width for mobile browsers, no <meta viewport>
+ {key: "980x1280", width: 980, height: 1280},
+
+ // Computer
+ {key: "1280x600", width: 1280, height: 600},
+ {key: "1920x900", width: 1920, height: 900},
+];
+
+function ResponsiveUI(aWindow, aTab)
+{
+ this.mainWindow = aWindow;
+ this.tab = aTab;
+ this.mm = this.tab.linkedBrowser.messageManager;
+ this.tabContainer = aWindow.gBrowser.tabContainer;
+ this.browser = aTab.linkedBrowser;
+ this.chromeDoc = aWindow.document;
+ this.container = aWindow.gBrowser.getBrowserContainer(this.browser);
+ this.stack = this.container.querySelector(".browserStack");
+ this._telemetry = new Telemetry();
+
+ // Let's bind some callbacks.
+ this.bound_presetSelected = this.presetSelected.bind(this);
+ this.bound_handleManualInput = this.handleManualInput.bind(this);
+ this.bound_addPreset = this.addPreset.bind(this);
+ this.bound_removePreset = this.removePreset.bind(this);
+ this.bound_rotate = this.rotate.bind(this);
+ this.bound_screenshot = () => this.screenshot();
+ this.bound_touch = this.toggleTouch.bind(this);
+ this.bound_close = this.close.bind(this);
+ this.bound_startResizing = this.startResizing.bind(this);
+ this.bound_stopResizing = this.stopResizing.bind(this);
+ this.bound_onDrag = this.onDrag.bind(this);
+ this.bound_changeUA = this.changeUA.bind(this);
+ this.bound_onContentResize = this.onContentResize.bind(this);
+
+ this.mm.addMessageListener("ResponsiveMode:OnContentResize",
+ this.bound_onContentResize);
+
+ // We must be ready to handle window or tab close now that we have saved
+ // ourselves in ActiveTabs. Otherwise we risk leaking the window.
+ this.mainWindow.addEventListener("unload", this);
+ this.tab.addEventListener("TabClose", this);
+ this.tabContainer.addEventListener("TabSelect", this);
+
+ ActiveTabs.set(this.tab, this);
+
+ this.inited = this.init();
+}
+
+ResponsiveUI.prototype = {
+ _transitionsEnabled: true,
+ get transitionsEnabled() {
+ return this._transitionsEnabled;
+ },
+ set transitionsEnabled(aValue) {
+ this._transitionsEnabled = aValue;
+ if (aValue && !this._resizing && this.stack.hasAttribute("responsivemode")) {
+ this.stack.removeAttribute("notransition");
+ } else if (!aValue) {
+ this.stack.setAttribute("notransition", "true");
+ }
+ },
+
+ init: Task.async(function* () {
+ debug("INIT BEGINS");
+ let ready = this.waitForMessage("ResponsiveMode:ChildScriptReady");
+ this.mm.loadFrameScript("resource://devtools/client/responsivedesign/responsivedesign-child.js", true);
+ yield ready;
+
+ let requiresFloatingScrollbars =
+ !this.mainWindow.matchMedia("(-moz-overlay-scrollbars)").matches;
+ let started = this.waitForMessage("ResponsiveMode:Start:Done");
+ debug("SEND START");
+ this.mm.sendAsyncMessage("ResponsiveMode:Start", {
+ requiresFloatingScrollbars,
+ // Tests expect events on resize to yield on various size changes
+ notifyOnResize: flags.testing,
+ });
+ yield started;
+
+ // Load Presets
+ this.loadPresets();
+
+ // Setup the UI
+ this.container.setAttribute("responsivemode", "true");
+ this.stack.setAttribute("responsivemode", "true");
+ this.buildUI();
+ this.checkMenus();
+
+ // Rotate the responsive mode if needed
+ try {
+ if (Services.prefs.getBoolPref("devtools.responsiveUI.rotate")) {
+ this.rotate();
+ }
+ } catch (e) {}
+
+ // Touch events support
+ this.touchEnableBefore = false;
+ this.touchEventSimulator = new TouchEventSimulator(this.browser);
+
+ yield this.connectToServer();
+ this.userAgentInput.hidden = false;
+
+ // Hook to display promotional Developer Edition doorhanger.
+ // Only displayed once.
+ showDoorhanger({
+ window: this.mainWindow,
+ type: "deveditionpromo",
+ anchor: this.chromeDoc.querySelector("#content")
+ });
+
+ // Notify that responsive mode is on.
+ this._telemetry.toolOpened("responsive");
+ ResponsiveUIManager.emit("on", { tab: this.tab });
+ }),
+
+ connectToServer: Task.async(function* () {
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+ this.client = new DebuggerClient(DebuggerServer.connectPipe());
+ yield this.client.connect();
+ let {tab} = yield this.client.getTab();
+ yield this.client.attachTab(tab.actor);
+ this.emulationFront = EmulationFront(this.client, tab);
+ }),
+
+ loadPresets: function () {
+ // Try to load presets from prefs
+ let presets = defaultPresets;
+ if (Services.prefs.prefHasUserValue("devtools.responsiveUI.presets")) {
+ try {
+ presets = JSON.parse(Services.prefs.getCharPref("devtools.responsiveUI.presets"));
+ } catch (e) {
+ // User pref is malformated.
+ console.error("Could not parse pref `devtools.responsiveUI.presets`: " + e);
+ }
+ }
+
+ this.customPreset = {key: "custom", custom: true};
+
+ if (Array.isArray(presets)) {
+ this.presets = [this.customPreset].concat(presets);
+ } else {
+ console.error("Presets value (devtools.responsiveUI.presets) is malformated.");
+ this.presets = [this.customPreset];
+ }
+
+ try {
+ let width = Services.prefs.getIntPref("devtools.responsiveUI.customWidth");
+ let height = Services.prefs.getIntPref("devtools.responsiveUI.customHeight");
+ this.customPreset.width = Math.min(MAX_WIDTH, width);
+ this.customPreset.height = Math.min(MAX_HEIGHT, height);
+
+ this.currentPresetKey = Services.prefs.getCharPref("devtools.responsiveUI.currentPreset");
+ } catch (e) {
+ // Default size. The first preset (custom) is the one that will be used.
+ let bbox = this.stack.getBoundingClientRect();
+
+ this.customPreset.width = bbox.width - 40; // horizontal padding of the container
+ this.customPreset.height = bbox.height - 80; // vertical padding + toolbar height
+
+ this.currentPresetKey = this.presets[1].key; // most common preset
+ }
+ },
+
+ /**
+ * Destroy the nodes. Remove listeners. Reset the style.
+ */
+ close: Task.async(function* () {
+ debug("CLOSE BEGINS");
+ if (this.closing) {
+ debug("ALREADY CLOSING, ABORT");
+ return;
+ }
+ this.closing = true;
+
+ // If we're closing very fast (in tests), ensure init has finished.
+ debug("CLOSE: WAIT ON INITED");
+ yield this.inited;
+ debug("CLOSE: INITED DONE");
+
+ this.unCheckMenus();
+ // Reset style of the stack.
+ debug(`CURRENT SIZE: ${this.stack.getAttribute("style")}`);
+ let style = "max-width: none;" +
+ "min-width: 0;" +
+ "max-height: none;" +
+ "min-height: 0;";
+ debug("RESET STACK SIZE");
+ this.stack.setAttribute("style", style);
+
+ // Wait for resize message before stopping in the child when testing,
+ // but only if we should expect to still get a message.
+ if (flags.testing && this.tab.linkedBrowser.messageManager) {
+ yield this.waitForMessage("ResponsiveMode:OnContentResize");
+ }
+
+ if (this.isResizing)
+ this.stopResizing();
+
+ // Remove listeners.
+ this.menulist.removeEventListener("select", this.bound_presetSelected, true);
+ this.menulist.removeEventListener("change", this.bound_handleManualInput, true);
+ this.mainWindow.removeEventListener("unload", this);
+ this.tab.removeEventListener("TabClose", this);
+ this.tabContainer.removeEventListener("TabSelect", this);
+ this.rotatebutton.removeEventListener("command", this.bound_rotate, true);
+ this.screenshotbutton.removeEventListener("command", this.bound_screenshot, true);
+ this.closebutton.removeEventListener("command", this.bound_close, true);
+ this.addbutton.removeEventListener("command", this.bound_addPreset, true);
+ this.removebutton.removeEventListener("command", this.bound_removePreset, true);
+ this.touchbutton.removeEventListener("command", this.bound_touch, true);
+ this.userAgentInput.removeEventListener("blur", this.bound_changeUA, true);
+
+ // Removed elements.
+ this.container.removeChild(this.toolbar);
+ if (this.bottomToolbar) {
+ this.bottomToolbar.remove();
+ delete this.bottomToolbar;
+ }
+ this.stack.removeChild(this.resizer);
+ this.stack.removeChild(this.resizeBarV);
+ this.stack.removeChild(this.resizeBarH);
+
+ this.stack.classList.remove("fxos-mode");
+
+ // Unset the responsive mode.
+ this.container.removeAttribute("responsivemode");
+ this.stack.removeAttribute("responsivemode");
+
+ ActiveTabs.delete(this.tab);
+ if (this.touchEventSimulator) {
+ this.touchEventSimulator.stop();
+ }
+
+ yield this.client.close();
+ this.client = this.emulationFront = null;
+
+ this._telemetry.toolClosed("responsive");
+
+ if (this.tab.linkedBrowser.messageManager) {
+ let stopped = this.waitForMessage("ResponsiveMode:Stop:Done");
+ this.tab.linkedBrowser.messageManager.sendAsyncMessage("ResponsiveMode:Stop");
+ yield stopped;
+ }
+
+ this.inited = null;
+ ResponsiveUIManager.emit("off", { tab: this.tab });
+ }),
+
+ waitForMessage(message) {
+ return new Promise(resolve => {
+ let listener = () => {
+ this.mm.removeMessageListener(message, listener);
+ resolve();
+ };
+ this.mm.addMessageListener(message, listener);
+ });
+ },
+
+ /**
+ * Emit an event when the content has been resized. Only used in tests.
+ */
+ onContentResize: function (msg) {
+ ResponsiveUIManager.emit("content-resize", {
+ tab: this.tab,
+ width: msg.data.width,
+ height: msg.data.height,
+ });
+ },
+
+ /**
+ * Handle events
+ */
+ handleEvent: function (aEvent) {
+ switch (aEvent.type) {
+ case "TabClose":
+ case "unload":
+ this.close();
+ break;
+ case "TabSelect":
+ if (this.tab.selected) {
+ this.checkMenus();
+ } else if (!this.mainWindow.gBrowser.selectedTab.responsiveUI) {
+ this.unCheckMenus();
+ }
+ break;
+ }
+ },
+
+ getViewportBrowser() {
+ return this.browser;
+ },
+
+ /**
+ * Check the menu items.
+ */
+ checkMenus: function RUI_checkMenus() {
+ this.chromeDoc.getElementById("menu_responsiveUI").setAttribute("checked", "true");
+ },
+
+ /**
+ * Uncheck the menu items.
+ */
+ unCheckMenus: function RUI_unCheckMenus() {
+ let el = this.chromeDoc.getElementById("menu_responsiveUI");
+ if (el) {
+ el.setAttribute("checked", "false");
+ }
+ },
+
+ /**
+ * Build the toolbar and the resizers.
+ *
+ * <vbox class="browserContainer"> From tabbrowser.xml
+ * <toolbar class="devtools-responsiveui-toolbar">
+ * <menulist class="devtools-responsiveui-menulist"/> // presets
+ * <toolbarbutton tabindex="0" class="devtools-responsiveui-toolbarbutton" tooltiptext="rotate"/> // rotate
+ * <toolbarbutton tabindex="0" class="devtools-responsiveui-toolbarbutton" tooltiptext="screenshot"/> // screenshot
+ * <toolbarbutton tabindex="0" class="devtools-responsiveui-toolbarbutton" tooltiptext="Leave Responsive Design Mode"/> // close
+ * </toolbar>
+ * <stack class="browserStack"> From tabbrowser.xml
+ * <browser/>
+ * <box class="devtools-responsiveui-resizehandle" bottom="0" right="0"/>
+ * <box class="devtools-responsiveui-resizebarV" top="0" right="0"/>
+ * <box class="devtools-responsiveui-resizebarH" bottom="0" left="0"/>
+ * // Additional button in FxOS mode:
+ * <button class="devtools-responsiveui-sleep-button" />
+ * <vbox class="devtools-responsiveui-volume-buttons">
+ * <button class="devtools-responsiveui-volume-up-button" />
+ * <button class="devtools-responsiveui-volume-down-button" />
+ * </vbox>
+ * </stack>
+ * <toolbar class="devtools-responsiveui-hardware-button">
+ * <toolbarbutton class="devtools-responsiveui-home-button" />
+ * </toolbar>
+ * </vbox>
+ */
+ buildUI: function RUI_buildUI() {
+ // Toolbar
+ this.toolbar = this.chromeDoc.createElement("toolbar");
+ this.toolbar.className = "devtools-responsiveui-toolbar";
+ this.toolbar.setAttribute("fullscreentoolbar", "true");
+
+ this.menulist = this.chromeDoc.createElement("menulist");
+ this.menulist.className = "devtools-responsiveui-menulist";
+ this.menulist.setAttribute("editable", "true");
+
+ this.menulist.addEventListener("select", this.bound_presetSelected, true);
+ this.menulist.addEventListener("change", this.bound_handleManualInput, true);
+
+ this.menuitems = new Map();
+
+ let menupopup = this.chromeDoc.createElement("menupopup");
+ this.registerPresets(menupopup);
+ this.menulist.appendChild(menupopup);
+
+ this.addbutton = this.chromeDoc.createElement("menuitem");
+ this.addbutton.setAttribute("label", this.strings.GetStringFromName("responsiveUI.addPreset"));
+ this.addbutton.addEventListener("command", this.bound_addPreset, true);
+
+ this.removebutton = this.chromeDoc.createElement("menuitem");
+ this.removebutton.setAttribute("label", this.strings.GetStringFromName("responsiveUI.removePreset"));
+ this.removebutton.addEventListener("command", this.bound_removePreset, true);
+
+ menupopup.appendChild(this.chromeDoc.createElement("menuseparator"));
+ menupopup.appendChild(this.addbutton);
+ menupopup.appendChild(this.removebutton);
+
+ this.rotatebutton = this.chromeDoc.createElement("toolbarbutton");
+ this.rotatebutton.setAttribute("tabindex", "0");
+ this.rotatebutton.setAttribute("tooltiptext", this.strings.GetStringFromName("responsiveUI.rotate2"));
+ this.rotatebutton.className = "devtools-responsiveui-toolbarbutton devtools-responsiveui-rotate";
+ this.rotatebutton.addEventListener("command", this.bound_rotate, true);
+
+ this.screenshotbutton = this.chromeDoc.createElement("toolbarbutton");
+ this.screenshotbutton.setAttribute("tabindex", "0");
+ this.screenshotbutton.setAttribute("tooltiptext", this.strings.GetStringFromName("responsiveUI.screenshot"));
+ this.screenshotbutton.className = "devtools-responsiveui-toolbarbutton devtools-responsiveui-screenshot";
+ this.screenshotbutton.addEventListener("command", this.bound_screenshot, true);
+
+ this.closebutton = this.chromeDoc.createElement("toolbarbutton");
+ this.closebutton.setAttribute("tabindex", "0");
+ this.closebutton.className = "devtools-responsiveui-toolbarbutton devtools-responsiveui-close";
+ this.closebutton.setAttribute("tooltiptext", this.strings.GetStringFromName("responsiveUI.close1"));
+ this.closebutton.addEventListener("command", this.bound_close, true);
+
+ this.toolbar.appendChild(this.closebutton);
+ this.toolbar.appendChild(this.menulist);
+ this.toolbar.appendChild(this.rotatebutton);
+
+ this.touchbutton = this.chromeDoc.createElement("toolbarbutton");
+ this.touchbutton.setAttribute("tabindex", "0");
+ this.touchbutton.setAttribute("tooltiptext", this.strings.GetStringFromName("responsiveUI.touch"));
+ this.touchbutton.className = "devtools-responsiveui-toolbarbutton devtools-responsiveui-touch";
+ this.touchbutton.addEventListener("command", this.bound_touch, true);
+ this.toolbar.appendChild(this.touchbutton);
+
+ this.toolbar.appendChild(this.screenshotbutton);
+
+ this.userAgentInput = this.chromeDoc.createElement("textbox");
+ this.userAgentInput.className = "devtools-responsiveui-textinput";
+ this.userAgentInput.setAttribute("placeholder",
+ this.strings.GetStringFromName("responsiveUI.userAgentPlaceholder"));
+ this.userAgentInput.addEventListener("blur", this.bound_changeUA, true);
+ this.userAgentInput.hidden = true;
+ this.toolbar.appendChild(this.userAgentInput);
+
+ // Resizers
+ let resizerTooltip = this.strings.GetStringFromName("responsiveUI.resizerTooltip");
+ this.resizer = this.chromeDoc.createElement("box");
+ this.resizer.className = "devtools-responsiveui-resizehandle";
+ this.resizer.setAttribute("right", "0");
+ this.resizer.setAttribute("bottom", "0");
+ this.resizer.setAttribute("tooltiptext", resizerTooltip);
+ this.resizer.onmousedown = this.bound_startResizing;
+
+ this.resizeBarV = this.chromeDoc.createElement("box");
+ this.resizeBarV.className = "devtools-responsiveui-resizebarV";
+ this.resizeBarV.setAttribute("top", "0");
+ this.resizeBarV.setAttribute("right", "0");
+ this.resizeBarV.setAttribute("tooltiptext", resizerTooltip);
+ this.resizeBarV.onmousedown = this.bound_startResizing;
+
+ this.resizeBarH = this.chromeDoc.createElement("box");
+ this.resizeBarH.className = "devtools-responsiveui-resizebarH";
+ this.resizeBarH.setAttribute("bottom", "0");
+ this.resizeBarH.setAttribute("left", "0");
+ this.resizeBarH.setAttribute("tooltiptext", resizerTooltip);
+ this.resizeBarH.onmousedown = this.bound_startResizing;
+
+ this.container.insertBefore(this.toolbar, this.stack);
+ this.stack.appendChild(this.resizer);
+ this.stack.appendChild(this.resizeBarV);
+ this.stack.appendChild(this.resizeBarH);
+ },
+
+ // FxOS custom controls
+ buildPhoneUI: function () {
+ this.stack.classList.add("fxos-mode");
+
+ let sleepButton = this.chromeDoc.createElement("button");
+ sleepButton.className = "devtools-responsiveui-sleep-button";
+ sleepButton.setAttribute("top", 0);
+ sleepButton.setAttribute("right", 0);
+ sleepButton.addEventListener("mousedown", () => {
+ SystemAppProxy.dispatchKeyboardEvent("keydown", {key: "Power"});
+ });
+ sleepButton.addEventListener("mouseup", () => {
+ SystemAppProxy.dispatchKeyboardEvent("keyup", {key: "Power"});
+ });
+ this.stack.appendChild(sleepButton);
+
+ let volumeButtons = this.chromeDoc.createElement("vbox");
+ volumeButtons.className = "devtools-responsiveui-volume-buttons";
+ volumeButtons.setAttribute("top", 0);
+ volumeButtons.setAttribute("left", 0);
+
+ let volumeUp = this.chromeDoc.createElement("button");
+ volumeUp.className = "devtools-responsiveui-volume-up-button";
+ volumeUp.addEventListener("mousedown", () => {
+ SystemAppProxy.dispatchKeyboardEvent("keydown", {key: "AudioVolumeUp"});
+ });
+ volumeUp.addEventListener("mouseup", () => {
+ SystemAppProxy.dispatchKeyboardEvent("keyup", {key: "AudioVolumeUp"});
+ });
+
+ let volumeDown = this.chromeDoc.createElement("button");
+ volumeDown.className = "devtools-responsiveui-volume-down-button";
+ volumeDown.addEventListener("mousedown", () => {
+ SystemAppProxy.dispatchKeyboardEvent("keydown", {key: "AudioVolumeDown"});
+ });
+ volumeDown.addEventListener("mouseup", () => {
+ SystemAppProxy.dispatchKeyboardEvent("keyup", {key: "AudioVolumeDown"});
+ });
+
+ volumeButtons.appendChild(volumeUp);
+ volumeButtons.appendChild(volumeDown);
+ this.stack.appendChild(volumeButtons);
+
+ let bottomToolbar = this.chromeDoc.createElement("toolbar");
+ bottomToolbar.className = "devtools-responsiveui-hardware-buttons";
+ bottomToolbar.setAttribute("align", "center");
+ bottomToolbar.setAttribute("pack", "center");
+
+ let homeButton = this.chromeDoc.createElement("toolbarbutton");
+ homeButton.className = "devtools-responsiveui-toolbarbutton devtools-responsiveui-home-button";
+ homeButton.addEventListener("mousedown", () => {
+ SystemAppProxy.dispatchKeyboardEvent("keydown", {key: "Home"});
+ });
+ homeButton.addEventListener("mouseup", () => {
+ SystemAppProxy.dispatchKeyboardEvent("keyup", {key: "Home"});
+ });
+ bottomToolbar.appendChild(homeButton);
+ this.bottomToolbar = bottomToolbar;
+ this.container.appendChild(bottomToolbar);
+ },
+
+ /**
+ * Validate and apply any user input on the editable menulist
+ */
+ handleManualInput: function RUI_handleManualInput() {
+ let userInput = this.menulist.inputField.value;
+ let value = INPUT_PARSER.exec(userInput);
+ let selectedPreset = this.menuitems.get(this.selectedItem);
+
+ // In case of an invalide value, we show back the last preset
+ if (!value || value.length < 3) {
+ this.setMenuLabel(this.selectedItem, selectedPreset);
+ return;
+ }
+
+ this.rotateValue = false;
+
+ if (!selectedPreset.custom) {
+ let menuitem = this.customMenuitem;
+ this.currentPresetKey = this.customPreset.key;
+ this.menulist.selectedItem = menuitem;
+ }
+
+ let w = this.customPreset.width = parseInt(value[1], 10);
+ let h = this.customPreset.height = parseInt(value[2], 10);
+
+ this.saveCustomSize();
+ this.setViewportSize({
+ width: w,
+ height: h,
+ });
+ },
+
+ /**
+ * Build the presets list and append it to the menupopup.
+ *
+ * @param aParent menupopup.
+ */
+ registerPresets: function RUI_registerPresets(aParent) {
+ let fragment = this.chromeDoc.createDocumentFragment();
+ let doc = this.chromeDoc;
+
+ for (let preset of this.presets) {
+ let menuitem = doc.createElement("menuitem");
+ menuitem.setAttribute("ispreset", true);
+ this.menuitems.set(menuitem, preset);
+
+ if (preset.key === this.currentPresetKey) {
+ menuitem.setAttribute("selected", "true");
+ this.selectedItem = menuitem;
+ }
+
+ if (preset.custom) {
+ this.customMenuitem = menuitem;
+ }
+
+ this.setMenuLabel(menuitem, preset);
+ fragment.appendChild(menuitem);
+ }
+ aParent.appendChild(fragment);
+ },
+
+ /**
+ * Set the menuitem label of a preset.
+ *
+ * @param aMenuitem menuitem to edit.
+ * @param aPreset associated preset.
+ */
+ setMenuLabel: function RUI_setMenuLabel(aMenuitem, aPreset) {
+ let size = SHARED_L10N.getFormatStr("dimensions",
+ Math.round(aPreset.width), Math.round(aPreset.height));
+
+ // .inputField might be not reachable yet (async XBL loading)
+ if (this.menulist.inputField) {
+ this.menulist.inputField.value = size;
+ }
+
+ if (aPreset.custom) {
+ size = this.strings.formatStringFromName("responsiveUI.customResolution", [size], 1);
+ } else if (aPreset.name != null && aPreset.name !== "") {
+ size = this.strings.formatStringFromName("responsiveUI.namedResolution", [size, aPreset.name], 2);
+ }
+
+ aMenuitem.setAttribute("label", size);
+ },
+
+ /**
+ * When a preset is selected, apply it.
+ */
+ presetSelected: function RUI_presetSelected() {
+ if (this.menulist.selectedItem.getAttribute("ispreset") === "true") {
+ this.selectedItem = this.menulist.selectedItem;
+
+ this.rotateValue = false;
+ let selectedPreset = this.menuitems.get(this.selectedItem);
+ this.loadPreset(selectedPreset);
+ this.currentPresetKey = selectedPreset.key;
+ this.saveCurrentPreset();
+
+ // Update the buttons hidden status according to the new selected preset
+ if (selectedPreset == this.customPreset) {
+ this.addbutton.hidden = false;
+ this.removebutton.hidden = true;
+ } else {
+ this.addbutton.hidden = true;
+ this.removebutton.hidden = false;
+ }
+ }
+ },
+
+ /**
+ * Apply a preset.
+ */
+ loadPreset(preset) {
+ this.setViewportSize(preset);
+ },
+
+ /**
+ * Add a preset to the list and the memory
+ */
+ addPreset: function RUI_addPreset() {
+ let w = this.customPreset.width;
+ let h = this.customPreset.height;
+ let newName = {};
+
+ let title = this.strings.GetStringFromName("responsiveUI.customNamePromptTitle1");
+ let message = this.strings.formatStringFromName("responsiveUI.customNamePromptMsg", [w, h], 2);
+ let promptOk = Services.prompt.prompt(null, title, message, newName, null, {});
+
+ if (!promptOk) {
+ // Prompt has been cancelled
+ this.menulist.selectedItem = this.selectedItem;
+ return;
+ }
+
+ let newPreset = {
+ key: w + "x" + h,
+ name: newName.value,
+ width: w,
+ height: h
+ };
+
+ this.presets.push(newPreset);
+
+ // Sort the presets according to width/height ascending order
+ this.presets.sort(function RUI_sortPresets(aPresetA, aPresetB) {
+ // We keep custom preset at first
+ if (aPresetA.custom && !aPresetB.custom) {
+ return 1;
+ }
+ if (!aPresetA.custom && aPresetB.custom) {
+ return -1;
+ }
+
+ if (aPresetA.width === aPresetB.width) {
+ if (aPresetA.height === aPresetB.height) {
+ return 0;
+ } else {
+ return aPresetA.height > aPresetB.height;
+ }
+ } else {
+ return aPresetA.width > aPresetB.width;
+ }
+ });
+
+ this.savePresets();
+
+ let newMenuitem = this.chromeDoc.createElement("menuitem");
+ newMenuitem.setAttribute("ispreset", true);
+ this.setMenuLabel(newMenuitem, newPreset);
+
+ this.menuitems.set(newMenuitem, newPreset);
+ let idx = this.presets.indexOf(newPreset);
+ let beforeMenuitem = this.menulist.firstChild.childNodes[idx + 1];
+ this.menulist.firstChild.insertBefore(newMenuitem, beforeMenuitem);
+
+ this.menulist.selectedItem = newMenuitem;
+ this.currentPresetKey = newPreset.key;
+ this.saveCurrentPreset();
+ },
+
+ /**
+ * remove a preset from the list and the memory
+ */
+ removePreset: function RUI_removePreset() {
+ let selectedPreset = this.menuitems.get(this.selectedItem);
+ let w = selectedPreset.width;
+ let h = selectedPreset.height;
+
+ this.presets.splice(this.presets.indexOf(selectedPreset), 1);
+ this.menulist.firstChild.removeChild(this.selectedItem);
+ this.menuitems.delete(this.selectedItem);
+
+ this.customPreset.width = w;
+ this.customPreset.height = h;
+ let menuitem = this.customMenuitem;
+ this.setMenuLabel(menuitem, this.customPreset);
+ this.menulist.selectedItem = menuitem;
+ this.currentPresetKey = this.customPreset.key;
+
+ this.setViewportSize({
+ width: w,
+ height: h,
+ });
+
+ this.savePresets();
+ },
+
+ /**
+ * Swap width and height.
+ */
+ rotate: function RUI_rotate() {
+ let selectedPreset = this.menuitems.get(this.selectedItem);
+ let width = this.rotateValue ? selectedPreset.height : selectedPreset.width;
+ let height = this.rotateValue ? selectedPreset.width : selectedPreset.height;
+
+ this.setViewportSize({
+ width: height,
+ height: width,
+ });
+
+ if (selectedPreset.custom) {
+ this.saveCustomSize();
+ } else {
+ this.rotateValue = !this.rotateValue;
+ this.saveCurrentPreset();
+ }
+ },
+
+ /**
+ * Take a screenshot of the page.
+ *
+ * @param aFileName name of the screenshot file (used for tests).
+ */
+ screenshot: function RUI_screenshot(aFileName) {
+ let filename = aFileName;
+ if (!filename) {
+ let date = new Date();
+ let month = ("0" + (date.getMonth() + 1)).substr(-2, 2);
+ let day = ("0" + date.getDate()).substr(-2, 2);
+ let dateString = [date.getFullYear(), month, day].join("-");
+ let timeString = date.toTimeString().replace(/:/g, ".").split(" ")[0];
+ filename = this.strings.formatStringFromName("responsiveUI.screenshotGeneratedFilename", [dateString, timeString], 2);
+ }
+ let mm = this.tab.linkedBrowser.messageManager;
+ let chromeWindow = this.chromeDoc.defaultView;
+ let doc = chromeWindow.document;
+ function onScreenshot(aMessage) {
+ mm.removeMessageListener("ResponsiveMode:RequestScreenshot:Done", onScreenshot);
+ chromeWindow.saveURL(aMessage.data, filename + ".png", null, true, true, doc.documentURIObject, doc);
+ }
+ mm.addMessageListener("ResponsiveMode:RequestScreenshot:Done", onScreenshot);
+ mm.sendAsyncMessage("ResponsiveMode:RequestScreenshot");
+ },
+
+ /**
+ * Enable/Disable mouse -> touch events translation.
+ */
+ enableTouch: function RUI_enableTouch() {
+ this.touchbutton.setAttribute("checked", "true");
+ return this.touchEventSimulator.start();
+ },
+
+ disableTouch: function RUI_disableTouch() {
+ this.touchbutton.removeAttribute("checked");
+ return this.touchEventSimulator.stop();
+ },
+
+ hideTouchNotification: function RUI_hideTouchNotification() {
+ let nbox = this.mainWindow.gBrowser.getNotificationBox(this.browser);
+ let n = nbox.getNotificationWithValue("responsive-ui-need-reload");
+ if (n) {
+ n.close();
+ }
+ },
+
+ toggleTouch: Task.async(function* () {
+ this.hideTouchNotification();
+ if (this.touchEventSimulator.enabled) {
+ this.disableTouch();
+ } else {
+ let isReloadNeeded = yield this.enableTouch();
+ if (isReloadNeeded) {
+ if (Services.prefs.getBoolPref("devtools.responsiveUI.no-reload-notification")) {
+ return;
+ }
+
+ let nbox = this.mainWindow.gBrowser.getNotificationBox(this.browser);
+
+ var buttons = [{
+ label: this.strings.GetStringFromName("responsiveUI.notificationReload"),
+ callback: () => {
+ this.browser.reload();
+ },
+ accessKey: this.strings.GetStringFromName("responsiveUI.notificationReload_accesskey"),
+ }, {
+ label: this.strings.GetStringFromName("responsiveUI.dontShowReloadNotification"),
+ callback: function () {
+ Services.prefs.setBoolPref("devtools.responsiveUI.no-reload-notification", true);
+ },
+ accessKey: this.strings.GetStringFromName("responsiveUI.dontShowReloadNotification_accesskey"),
+ }];
+
+ nbox.appendNotification(
+ this.strings.GetStringFromName("responsiveUI.needReload"),
+ "responsive-ui-need-reload",
+ null,
+ nbox.PRIORITY_INFO_LOW,
+ buttons);
+ }
+ }
+ }),
+
+ waitForReload() {
+ let navigatedDeferred = promise.defer();
+ let onNavigated = (_, { state }) => {
+ if (state != "stop") {
+ return;
+ }
+ this.client.removeListener("tabNavigated", onNavigated);
+ navigatedDeferred.resolve();
+ };
+ this.client.addListener("tabNavigated", onNavigated);
+ return navigatedDeferred.promise;
+ },
+
+ /**
+ * Change the user agent string
+ */
+ changeUA: Task.async(function* () {
+ let value = this.userAgentInput.value;
+ let changed;
+ if (value) {
+ changed = yield this.emulationFront.setUserAgentOverride(value);
+ this.userAgentInput.setAttribute("attention", "true");
+ } else {
+ changed = yield this.emulationFront.clearUserAgentOverride();
+ this.userAgentInput.removeAttribute("attention");
+ }
+ if (changed) {
+ let reloaded = this.waitForReload();
+ this.tab.linkedBrowser.reload();
+ yield reloaded;
+ }
+ ResponsiveUIManager.emit("userAgentChanged", { tab: this.tab });
+ }),
+
+ /**
+ * Get the current width and height.
+ */
+ getSize() {
+ let width = Number(this.stack.style.minWidth.replace("px", ""));
+ let height = Number(this.stack.style.minHeight.replace("px", ""));
+ return {
+ width,
+ height,
+ };
+ },
+
+ /**
+ * Change the size of the viewport.
+ */
+ setViewportSize({ width, height }) {
+ debug(`SET SIZE TO ${width} x ${height}`);
+ if (width) {
+ this.setWidth(width);
+ }
+ if (height) {
+ this.setHeight(height);
+ }
+ },
+
+ setWidth: function RUI_setWidth(aWidth) {
+ aWidth = Math.min(Math.max(aWidth, MIN_WIDTH), MAX_WIDTH);
+ this.stack.style.maxWidth = this.stack.style.minWidth = aWidth + "px";
+
+ if (!this.ignoreX)
+ this.resizeBarH.setAttribute("left", Math.round(aWidth / 2));
+
+ let selectedPreset = this.menuitems.get(this.selectedItem);
+
+ if (selectedPreset.custom) {
+ selectedPreset.width = aWidth;
+ this.setMenuLabel(this.selectedItem, selectedPreset);
+ }
+ },
+
+ setHeight: function RUI_setHeight(aHeight) {
+ aHeight = Math.min(Math.max(aHeight, MIN_HEIGHT), MAX_HEIGHT);
+ this.stack.style.maxHeight = this.stack.style.minHeight = aHeight + "px";
+
+ if (!this.ignoreY)
+ this.resizeBarV.setAttribute("top", Math.round(aHeight / 2));
+
+ let selectedPreset = this.menuitems.get(this.selectedItem);
+ if (selectedPreset.custom) {
+ selectedPreset.height = aHeight;
+ this.setMenuLabel(this.selectedItem, selectedPreset);
+ }
+ },
+ /**
+ * Start the process of resizing the browser.
+ *
+ * @param aEvent
+ */
+ startResizing: function RUI_startResizing(aEvent) {
+ let selectedPreset = this.menuitems.get(this.selectedItem);
+
+ if (!selectedPreset.custom) {
+ this.customPreset.width = this.rotateValue ? selectedPreset.height : selectedPreset.width;
+ this.customPreset.height = this.rotateValue ? selectedPreset.width : selectedPreset.height;
+
+ let menuitem = this.customMenuitem;
+ this.setMenuLabel(menuitem, this.customPreset);
+
+ this.currentPresetKey = this.customPreset.key;
+ this.menulist.selectedItem = menuitem;
+ }
+ this.mainWindow.addEventListener("mouseup", this.bound_stopResizing, true);
+ this.mainWindow.addEventListener("mousemove", this.bound_onDrag, true);
+ this.container.style.pointerEvents = "none";
+
+ this._resizing = true;
+ this.stack.setAttribute("notransition", "true");
+
+ this.lastScreenX = aEvent.screenX;
+ this.lastScreenY = aEvent.screenY;
+
+ this.ignoreY = (aEvent.target === this.resizeBarV);
+ this.ignoreX = (aEvent.target === this.resizeBarH);
+
+ this.isResizing = true;
+ },
+
+ /**
+ * Resizing on mouse move.
+ *
+ * @param aEvent
+ */
+ onDrag: function RUI_onDrag(aEvent) {
+ let shift = aEvent.shiftKey;
+ let ctrl = !aEvent.shiftKey && aEvent.ctrlKey;
+
+ let screenX = aEvent.screenX;
+ let screenY = aEvent.screenY;
+
+ let deltaX = screenX - this.lastScreenX;
+ let deltaY = screenY - this.lastScreenY;
+
+ if (this.ignoreY)
+ deltaY = 0;
+ if (this.ignoreX)
+ deltaX = 0;
+
+ if (ctrl) {
+ deltaX /= SLOW_RATIO;
+ deltaY /= SLOW_RATIO;
+ }
+
+ let width = this.customPreset.width + deltaX;
+ let height = this.customPreset.height + deltaY;
+
+ if (shift) {
+ let roundedWidth, roundedHeight;
+ roundedWidth = 10 * Math.floor(width / ROUND_RATIO);
+ roundedHeight = 10 * Math.floor(height / ROUND_RATIO);
+ screenX += roundedWidth - width;
+ screenY += roundedHeight - height;
+ width = roundedWidth;
+ height = roundedHeight;
+ }
+
+ if (width < MIN_WIDTH) {
+ width = MIN_WIDTH;
+ } else {
+ this.lastScreenX = screenX;
+ }
+
+ if (height < MIN_HEIGHT) {
+ height = MIN_HEIGHT;
+ } else {
+ this.lastScreenY = screenY;
+ }
+
+ this.setViewportSize({ width, height });
+ },
+
+ /**
+ * Stop End resizing
+ */
+ stopResizing: function RUI_stopResizing() {
+ this.container.style.pointerEvents = "auto";
+
+ this.mainWindow.removeEventListener("mouseup", this.bound_stopResizing, true);
+ this.mainWindow.removeEventListener("mousemove", this.bound_onDrag, true);
+
+ this.saveCustomSize();
+
+ delete this._resizing;
+ if (this.transitionsEnabled) {
+ this.stack.removeAttribute("notransition");
+ }
+ this.ignoreY = false;
+ this.ignoreX = false;
+ this.isResizing = false;
+ },
+
+ /**
+ * Store the custom size as a pref.
+ */
+ saveCustomSize: function RUI_saveCustomSize() {
+ Services.prefs.setIntPref("devtools.responsiveUI.customWidth", this.customPreset.width);
+ Services.prefs.setIntPref("devtools.responsiveUI.customHeight", this.customPreset.height);
+ },
+
+ /**
+ * Store the current preset as a pref.
+ */
+ saveCurrentPreset: function RUI_saveCurrentPreset() {
+ Services.prefs.setCharPref("devtools.responsiveUI.currentPreset", this.currentPresetKey);
+ Services.prefs.setBoolPref("devtools.responsiveUI.rotate", this.rotateValue);
+ },
+
+ /**
+ * Store the list of all registered presets as a pref.
+ */
+ savePresets: function RUI_savePresets() {
+ // We exclude the custom one
+ let registeredPresets = this.presets.filter(function (aPreset) {
+ return !aPreset.custom;
+ });
+
+ Services.prefs.setCharPref("devtools.responsiveUI.presets", JSON.stringify(registeredPresets));
+ },
+};
+
+loader.lazyGetter(ResponsiveUI.prototype, "strings", function () {
+ return Services.strings.createBundle("chrome://devtools/locale/responsiveUI.properties");
+});
diff --git a/devtools/client/responsivedesign/test/.eslintrc.js b/devtools/client/responsivedesign/test/.eslintrc.js
new file mode 100644
index 000000000..ba1263286
--- /dev/null
+++ b/devtools/client/responsivedesign/test/.eslintrc.js
@@ -0,0 +1,10 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../../.eslintrc.mochitests.js",
+ "globals": {
+ "ResponsiveUI": true,
+ "helpers": true
+ }
+};
diff --git a/devtools/client/responsivedesign/test/browser.ini b/devtools/client/responsivedesign/test/browser.ini
new file mode 100644
index 000000000..6a8f5a8d9
--- /dev/null
+++ b/devtools/client/responsivedesign/test/browser.ini
@@ -0,0 +1,21 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ head.js
+ touch.html
+ !/devtools/client/commandline/test/helpers.js
+ !/devtools/client/framework/test/shared-head.js
+
+[browser_responsive_cmd.js]
+[browser_responsivecomputedview.js]
+skip-if = e10s && debug # Bug 1252201 - Docshell leak on debug e10s
+[browser_responsiveruleview.js]
+skip-if = e10s && debug # Bug 1252201 - Docshell leak on debug e10s
+[browser_responsiveui.js]
+[browser_responsiveui_touch.js]
+[browser_responsiveuiaddcustompreset.js]
+[browser_responsive_devicewidth.js]
+[browser_responsiveui_customuseragent.js]
+[browser_responsiveui_window_close.js]
+skip-if = (os == 'linux') && e10s && debug # Bug 1277274
diff --git a/devtools/client/responsivedesign/test/browser_responsive_cmd.js b/devtools/client/responsivedesign/test/browser_responsive_cmd.js
new file mode 100644
index 000000000..8c8e798d0
--- /dev/null
+++ b/devtools/client/responsivedesign/test/browser_responsive_cmd.js
@@ -0,0 +1,143 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+//
+// Whitelisting this test.
+// As part of bug 1077403, the leaking uncaught rejection should be fixed.
+//
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed("destroy");
+
+function test() {
+ let manager = ResponsiveUIManager;
+ let done;
+
+ function isOpen() {
+ return gBrowser.getBrowserContainer(gBrowser.selectedBrowser)
+ .hasAttribute("responsivemode");
+ }
+
+ helpers.addTabWithToolbar("data:text/html;charset=utf-8,hi", (options) => {
+ return helpers.audit(options, [
+ {
+ setup() {
+ done = once(manager, "on");
+ return helpers.setInput(options, "resize toggle");
+ },
+ check: {
+ input: "resize toggle",
+ hints: "",
+ markup: "VVVVVVVVVVVVV",
+ status: "VALID"
+ },
+ exec: {
+ output: ""
+ },
+ post: Task.async(function* () {
+ yield done;
+ ok(isOpen(), "responsive mode is open");
+ }),
+ },
+ {
+ setup() {
+ done = once(manager, "off");
+ return helpers.setInput(options, "resize toggle");
+ },
+ check: {
+ input: "resize toggle",
+ hints: "",
+ markup: "VVVVVVVVVVVVV",
+ status: "VALID"
+ },
+ exec: {
+ output: ""
+ },
+ post: Task.async(function* () {
+ yield done;
+ ok(!isOpen(), "responsive mode is closed");
+ }),
+ },
+ {
+ setup() {
+ done = once(manager, "on");
+ return helpers.setInput(options, "resize on");
+ },
+ check: {
+ input: "resize on",
+ hints: "",
+ markup: "VVVVVVVVV",
+ status: "VALID"
+ },
+ exec: {
+ output: ""
+ },
+ post: Task.async(function* () {
+ yield done;
+ ok(isOpen(), "responsive mode is open");
+ }),
+ },
+ {
+ setup() {
+ done = once(manager, "off");
+ return helpers.setInput(options, "resize off");
+ },
+ check: {
+ input: "resize off",
+ hints: "",
+ markup: "VVVVVVVVVV",
+ status: "VALID"
+ },
+ exec: {
+ output: ""
+ },
+ post: Task.async(function* () {
+ yield done;
+ ok(!isOpen(), "responsive mode is closed");
+ }),
+ },
+ {
+ setup() {
+ done = once(manager, "on");
+ return helpers.setInput(options, "resize to 400 400");
+ },
+ check: {
+ input: "resize to 400 400",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVVVV",
+ status: "VALID",
+ args: {
+ width: { value: 400 },
+ height: { value: 400 },
+ }
+ },
+ exec: {
+ output: ""
+ },
+ post: Task.async(function* () {
+ yield done;
+ ok(isOpen(), "responsive mode is open");
+ }),
+ },
+ {
+ setup() {
+ done = once(manager, "off");
+ return helpers.setInput(options, "resize off");
+ },
+ check: {
+ input: "resize off",
+ hints: "",
+ markup: "VVVVVVVVVV",
+ status: "VALID"
+ },
+ exec: {
+ output: ""
+ },
+ post: Task.async(function* () {
+ yield done;
+ ok(!isOpen(), "responsive mode is closed");
+ }),
+ },
+ ]);
+ }).then(finish);
+}
diff --git a/devtools/client/responsivedesign/test/browser_responsive_devicewidth.js b/devtools/client/responsivedesign/test/browser_responsive_devicewidth.js
new file mode 100644
index 000000000..604a20783
--- /dev/null
+++ b/devtools/client/responsivedesign/test/browser_responsive_devicewidth.js
@@ -0,0 +1,68 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(function* () {
+ let tab = yield addTab("about:logo");
+ let { rdm, manager } = yield openRDM(tab);
+ ok(rdm, "An instance of the RDM should be attached to the tab.");
+ yield setSize(rdm, manager, 110, 500);
+
+ info("Checking initial width/height properties.");
+ yield doInitialChecks();
+
+ info("Changing the RDM size");
+ yield setSize(rdm, manager, 90, 500);
+
+ info("Checking for screen props");
+ yield checkScreenProps();
+
+ info("Setting docShell.deviceSizeIsPageSize to false");
+ yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ let docShell = content.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShell);
+ docShell.deviceSizeIsPageSize = false;
+ });
+
+ info("Checking for screen props once again.");
+ yield checkScreenProps2();
+
+ yield closeRDM(rdm);
+});
+
+function* doInitialChecks() {
+ let {innerWidth, matchesMedia} = yield grabContentInfo();
+ is(innerWidth, 110, "initial width should be 110px");
+ ok(!matchesMedia, "media query shouldn't match.");
+}
+
+function* checkScreenProps() {
+ let {matchesMedia, screen} = yield grabContentInfo();
+ ok(matchesMedia, "media query should match");
+ isnot(window.screen.width, screen.width,
+ "screen.width should not be the size of the screen.");
+ is(screen.width, 90, "screen.width should be the page width");
+ is(screen.height, 500, "screen.height should be the page height");
+}
+
+function* checkScreenProps2() {
+ let {matchesMedia, screen} = yield grabContentInfo();
+ ok(!matchesMedia, "media query should be re-evaluated.");
+ is(window.screen.width, screen.width,
+ "screen.width should be the size of the screen.");
+}
+
+function grabContentInfo() {
+ return ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ return {
+ screen: {
+ width: content.screen.width,
+ height: content.screen.height
+ },
+ innerWidth: content.innerWidth,
+ matchesMedia: content.matchMedia("(max-device-width:100px)").matches
+ };
+ });
+}
diff --git a/devtools/client/responsivedesign/test/browser_responsivecomputedview.js b/devtools/client/responsivedesign/test/browser_responsivecomputedview.js
new file mode 100644
index 000000000..eee2dbc03
--- /dev/null
+++ b/devtools/client/responsivedesign/test/browser_responsivecomputedview.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Check that when the viewport is resized, the computed-view refreshes.
+
+const TEST_URI = "data:text/html;charset=utf-8,<html><style>" +
+ "div {" +
+ " width: 500px;" +
+ " height: 10px;" +
+ " background: purple;" +
+ "} " +
+ "@media screen and (max-width: 200px) {" +
+ " div { " +
+ " width: 100px;" +
+ " }" +
+ "};" +
+ "</style><div></div></html>";
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+
+ info("Open the responsive design mode and set its size to 500x500 to start");
+ let { rdm, manager } = yield openRDM();
+ yield setSize(rdm, manager, 500, 500);
+
+ info("Open the inspector, computed-view and select the test node");
+ let {inspector, view} = yield openComputedView();
+ yield selectNode("div", inspector);
+
+ info("Try shrinking the viewport and checking the applied styles");
+ yield testShrink(view, inspector, rdm, manager);
+
+ info("Try growing the viewport and checking the applied styles");
+ yield testGrow(view, inspector, rdm, manager);
+
+ yield closeRDM(rdm);
+ yield closeToolbox();
+});
+
+function* testShrink(computedView, inspector, rdm, manager) {
+ is(computedWidth(computedView), "500px", "Should show 500px initially.");
+
+ let onRefresh = inspector.once("computed-view-refreshed");
+ yield setSize(rdm, manager, 100, 100);
+ yield onRefresh;
+
+ is(computedWidth(computedView), "100px", "Should be 100px after shrinking.");
+}
+
+function* testGrow(computedView, inspector, rdm, manager) {
+ let onRefresh = inspector.once("computed-view-refreshed");
+ yield setSize(rdm, manager, 500, 500);
+ yield onRefresh;
+
+ is(computedWidth(computedView), "500px", "Should be 500px after growing.");
+}
+
+function computedWidth(computedView) {
+ for (let prop of computedView.propertyViews) {
+ if (prop.name === "width") {
+ return prop.valueNode.textContent;
+ }
+ }
+ return null;
+}
diff --git a/devtools/client/responsivedesign/test/browser_responsiveruleview.js b/devtools/client/responsivedesign/test/browser_responsiveruleview.js
new file mode 100644
index 000000000..5c3698e78
--- /dev/null
+++ b/devtools/client/responsivedesign/test/browser_responsiveruleview.js
@@ -0,0 +1,95 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Check that when the viewport is resized, the rule-view refreshes.
+// Also test that ESC does open the split-console, and that the RDM menu item
+// gets updated correctly when needed.
+// TODO: split this test.
+
+const TEST_URI = "data:text/html;charset=utf-8,<html><style>" +
+ "div {" +
+ " width: 500px;" +
+ " height: 10px;" +
+ " background: purple;" +
+ "} " +
+ "@media screen and (max-width: 200px) {" +
+ " div { " +
+ " width: 100px;" +
+ " }" +
+ "};" +
+ "</style><div></div></html>";
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+
+ info("Open the responsive design mode and set its size to 500x500 to start");
+ let { rdm, manager } = yield openRDM();
+ yield setSize(rdm, manager, 500, 500);
+
+ info("Open the inspector, rule-view and select the test node");
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("div", inspector);
+
+ info("Try shrinking the viewport and checking the applied styles");
+ yield testShrink(view, rdm, manager);
+
+ info("Try growing the viewport and checking the applied styles");
+ yield testGrow(view, rdm, manager);
+
+ info("Check that ESC still opens the split console");
+ yield testEscapeOpensSplitConsole(inspector);
+
+ yield closeToolbox();
+
+ info("Test the state of the RDM menu item");
+ yield testMenuItem(rdm);
+
+ Services.prefs.clearUserPref("devtools.toolbox.splitconsoleEnabled");
+});
+
+function* testShrink(ruleView, rdm, manager) {
+ is(numberOfRules(ruleView), 2, "Should have two rules initially.");
+
+ info("Resize to 100x100 and wait for the rule-view to update");
+ let onRefresh = ruleView.once("ruleview-refreshed");
+ yield setSize(rdm, manager, 100, 100);
+ yield onRefresh;
+
+ is(numberOfRules(ruleView), 3, "Should have three rules after shrinking.");
+}
+
+function* testGrow(ruleView, rdm, manager) {
+ info("Resize to 500x500 and wait for the rule-view to update");
+ let onRefresh = ruleView.once("ruleview-refreshed");
+ yield setSize(rdm, manager, 500, 500);
+ yield onRefresh;
+
+ is(numberOfRules(ruleView), 2, "Should have two rules after growing.");
+}
+
+function* testEscapeOpensSplitConsole(inspector) {
+ ok(!inspector._toolbox._splitConsole, "Console is not split.");
+
+ info("Press escape");
+ let onSplit = inspector._toolbox.once("split-console");
+ EventUtils.synthesizeKey("VK_ESCAPE", {});
+ yield onSplit;
+
+ ok(inspector._toolbox._splitConsole, "Console is split after pressing ESC.");
+}
+
+function* testMenuItem(rdm) {
+ is(document.getElementById("menu_responsiveUI").getAttribute("checked"),
+ "true", "The menu item is checked");
+
+ yield closeRDM(rdm);
+
+ is(document.getElementById("menu_responsiveUI").getAttribute("checked"),
+ "false", "The menu item is unchecked");
+}
+
+function numberOfRules(ruleView) {
+ return ruleView.element.querySelectorAll(".ruleview-code").length;
+}
diff --git a/devtools/client/responsivedesign/test/browser_responsiveui.js b/devtools/client/responsivedesign/test/browser_responsiveui.js
new file mode 100644
index 000000000..283974d0f
--- /dev/null
+++ b/devtools/client/responsivedesign/test/browser_responsiveui.js
@@ -0,0 +1,250 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(function* () {
+ let tab = yield addTab("data:text/html,mop");
+
+ let {rdm, manager} = yield openRDM(tab, "menu");
+ let container = gBrowser.getBrowserContainer();
+ is(container.getAttribute("responsivemode"), "true",
+ "Should be in responsive mode.");
+ is(document.getElementById("menu_responsiveUI").getAttribute("checked"),
+ "true", "Menu item should be checked");
+
+ ok(rdm, "An instance of the RDM should be attached to the tab.");
+
+ let originalWidth = (yield getSizing()).width;
+
+ let documentLoaded = waitForDocLoadComplete();
+ gBrowser.loadURI("data:text/html;charset=utf-8,mop" +
+ "<div style%3D'height%3A5000px'><%2Fdiv>");
+ yield documentLoaded;
+
+ let newWidth = (yield getSizing()).width;
+ is(originalWidth, newWidth, "Floating scrollbars shouldn't change the width");
+
+ yield testPresets(rdm, manager);
+
+ info("Testing mouse resizing");
+ yield testManualMouseResize(rdm, manager);
+
+ info("Testing mouse resizing with shift key");
+ yield testManualMouseResize(rdm, manager, "shift");
+
+ info("Testing mouse resizing with ctrl key");
+ yield testManualMouseResize(rdm, manager, "ctrl");
+
+ info("Testing resizing with user custom keyboard input");
+ yield testResizeUsingCustomInput(rdm, manager);
+
+ info("Testing invalid keyboard input");
+ yield testInvalidUserInput(rdm);
+
+ info("Testing rotation");
+ yield testRotate(rdm, manager);
+
+ let {width: widthBeforeClose, height: heightBeforeClose} = yield getSizing();
+
+ info("Restarting responsive mode");
+ yield closeRDM(rdm);
+
+ let resized = waitForResizeTo(manager, widthBeforeClose, heightBeforeClose);
+ ({rdm} = yield openRDM(tab, "keyboard"));
+ yield resized;
+
+ let currentSize = yield getSizing();
+ is(currentSize.width, widthBeforeClose, "width should be restored");
+ is(currentSize.height, heightBeforeClose, "height should be restored");
+
+ container = gBrowser.getBrowserContainer();
+ is(container.getAttribute("responsivemode"), "true", "In responsive mode.");
+ is(document.getElementById("menu_responsiveUI").getAttribute("checked"),
+ "true", "menu item should be checked");
+
+ let isWinXP = navigator.userAgent.indexOf("Windows NT 5.1") != -1;
+ if (!isWinXP) {
+ yield testScreenshot(rdm);
+ }
+
+ yield closeRDM(rdm);
+ is(document.getElementById("menu_responsiveUI").getAttribute("checked"),
+ "false", "menu item should be unchecked");
+});
+
+function* testPresets(rdm, manager) {
+ // Starting from length - 4 because last 3 items are not presets :
+ // the separator, the add button and the remove button
+ for (let c = rdm.menulist.firstChild.childNodes.length - 4; c >= 0; c--) {
+ let item = rdm.menulist.firstChild.childNodes[c];
+ let [width, height] = extractSizeFromString(item.getAttribute("label"));
+ yield setPresetIndex(rdm, manager, c);
+
+ let {width: contentWidth, height: contentHeight} = yield getSizing();
+ is(contentWidth, width, "preset" + c + ": the width should be changed");
+ is(contentHeight, height, "preset" + c + ": the height should be changed");
+ }
+}
+
+function* testManualMouseResize(rdm, manager, pressedKey) {
+ yield setSize(rdm, manager, 100, 100);
+
+ let {width: initialWidth, height: initialHeight} = yield getSizing();
+ is(initialWidth, 100, "Width should be reset to 100");
+ is(initialHeight, 100, "Height should be reset to 100");
+
+ let x = 2, y = 2;
+ EventUtils.synthesizeMouse(rdm.resizer, x, y, {type: "mousedown"}, window);
+
+ let mouseMoveParams = {type: "mousemove"};
+ if (pressedKey == "shift") {
+ x += 23; y += 10;
+ mouseMoveParams.shiftKey = true;
+ } else if (pressedKey == "ctrl") {
+ x += 120; y += 60;
+ mouseMoveParams.ctrlKey = true;
+ } else {
+ x += 20; y += 10;
+ }
+
+ EventUtils.synthesizeMouse(rdm.resizer, x, y, mouseMoveParams, window);
+ EventUtils.synthesizeMouse(rdm.resizer, x, y, {type: "mouseup"}, window);
+
+ yield once(manager, "content-resize");
+
+ let expectedWidth = initialWidth + 20;
+ let expectedHeight = initialHeight + 10;
+ info("initial width: " + initialWidth);
+ info("initial height: " + initialHeight);
+
+ yield verifyResize(rdm, expectedWidth, expectedHeight);
+}
+
+function* testResizeUsingCustomInput(rdm, manager) {
+ let {width: initialWidth, height: initialHeight} = yield getSizing();
+ let expectedWidth = initialWidth - 20, expectedHeight = initialHeight - 10;
+
+ let userInput = expectedWidth + " x " + expectedHeight;
+ rdm.menulist.inputField.value = "";
+ rdm.menulist.focus();
+ processStringAsKey(userInput);
+
+ // While typing, the size should not change
+ let currentSize = yield getSizing();
+ is(currentSize.width, initialWidth, "Typing shouldn't change the width");
+ is(currentSize.height, initialHeight, "Typing shouldn't change the height");
+
+ // Only the `change` event must change the size
+ EventUtils.synthesizeKey("VK_RETURN", {});
+
+ yield once(manager, "content-resize");
+
+ yield verifyResize(rdm, expectedWidth, expectedHeight);
+}
+
+function* testInvalidUserInput(rdm) {
+ let {width: initialWidth, height: initialHeight} = yield getSizing();
+ let index = rdm.menulist.selectedIndex;
+ let expectedValue = initialWidth + "\u00D7" + initialHeight;
+ let expectedLabel = rdm.menulist.firstChild.firstChild.getAttribute("label");
+
+ let userInput = "I'm wrong";
+
+ rdm.menulist.inputField.value = "";
+ rdm.menulist.focus();
+ processStringAsKey(userInput);
+ EventUtils.synthesizeKey("VK_RETURN", {});
+
+ let currentSize = yield getSizing();
+ is(currentSize.width, initialWidth, "Width should not change");
+ is(currentSize.height, initialHeight, "Height should not change");
+ is(rdm.menulist.selectedIndex, index, "Selected item should not change.");
+ is(rdm.menulist.value, expectedValue, "Value should be reset");
+
+ let label = rdm.menulist.firstChild.firstChild.getAttribute("label");
+ is(label, expectedLabel, "Custom menuitem's label should not change");
+}
+
+function* testRotate(rdm, manager) {
+ yield setSize(rdm, manager, 100, 200);
+
+ let {width: initialWidth, height: initialHeight} = yield getSizing();
+ rdm.rotate();
+
+ yield once(manager, "content-resize");
+
+ let newSize = yield getSizing();
+ is(newSize.width, initialHeight, "The width should now be the height.");
+ is(newSize.height, initialWidth, "The height should now be the width.");
+
+ let label = rdm.menulist.firstChild.firstChild.getAttribute("label");
+ let [width, height] = extractSizeFromString(label);
+ is(width, initialHeight, "Width in label should be updated");
+ is(height, initialWidth, "Height in label should be updated");
+}
+
+function* verifyResize(rdm, expectedWidth, expectedHeight) {
+ let currentSize = yield getSizing();
+ is(currentSize.width, expectedWidth, "Width should now change");
+ is(currentSize.height, expectedHeight, "Height should now change");
+
+ is(rdm.menulist.selectedIndex, -1, "Custom menuitem cannot be selected");
+
+ let label = rdm.menulist.firstChild.firstChild.getAttribute("label");
+ let value = rdm.menulist.value;
+ isnot(label, value,
+ "The menulist item label should be different than the menulist value");
+
+ let [width, height] = extractSizeFromString(label);
+ is(width, expectedWidth, "Width in label should be updated");
+ is(height, expectedHeight, "Height in label should be updated");
+
+ [width, height] = extractSizeFromString(value);
+ is(width, expectedWidth, "Value should be updated with new width");
+ is(height, expectedHeight, "Value should be updated with new height");
+}
+
+function* testScreenshot(rdm) {
+ info("Testing screenshot");
+ rdm.screenshot("responsiveui");
+ let {FileUtils} = Cu.import("resource://gre/modules/FileUtils.jsm", {});
+
+ while (true) {
+ // while(true) until we find the file.
+ // no need for a timeout, the test will get killed anyway.
+ let file = FileUtils.getFile("DfltDwnld", [ "responsiveui.png" ]);
+ if (file.exists()) {
+ ok(true, "Screenshot file exists");
+ file.remove(false);
+ break;
+ }
+ info("checking if file exists in 200ms");
+ yield wait(200);
+ }
+}
+
+function* getSizing() {
+ let browser = gBrowser.selectedBrowser;
+ let sizing = yield ContentTask.spawn(browser, {}, function* () {
+ return {
+ width: content.innerWidth,
+ height: content.innerHeight
+ };
+ });
+ return sizing;
+}
+
+function extractSizeFromString(str) {
+ let numbers = str.match(/(\d+)[^\d]*(\d+)/);
+ if (numbers) {
+ return [numbers[1], numbers[2]];
+ }
+ return [null, null];
+}
+
+function processStringAsKey(str) {
+ for (let i = 0, l = str.length; i < l; i++) {
+ EventUtils.synthesizeKey(str.charAt(i), {});
+ }
+}
diff --git a/devtools/client/responsivedesign/test/browser_responsiveui_customuseragent.js b/devtools/client/responsivedesign/test/browser_responsiveui_customuseragent.js
new file mode 100644
index 000000000..35efc4c14
--- /dev/null
+++ b/devtools/client/responsivedesign/test/browser_responsiveui_customuseragent.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html, Custom User Agent test";
+const DEFAULT_UA = Cc["@mozilla.org/network/protocol;1?name=http"]
+ .getService(Ci.nsIHttpProtocolHandler)
+ .userAgent;
+const CHROME_UA = "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36" +
+ " (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36";
+add_task(function* () {
+ yield addTab(TEST_URI);
+
+ let {rdm, manager} = yield openRDM();
+ yield testUserAgent(DEFAULT_UA);
+
+ info("Setting UA to " + CHROME_UA);
+ yield setUserAgent(CHROME_UA, rdm, manager);
+ yield testUserAgent(CHROME_UA);
+
+ info("Resetting UA");
+ yield setUserAgent("", rdm, manager);
+ yield testUserAgent(DEFAULT_UA);
+
+ info("Setting UA to " + CHROME_UA);
+ yield setUserAgent(CHROME_UA, rdm, manager);
+ yield testUserAgent(CHROME_UA);
+
+ info("Closing responsive mode");
+
+ yield closeRDM(rdm);
+ yield testUserAgent(DEFAULT_UA);
+});
+
+function* setUserAgent(ua, rdm, manager) {
+ let input = rdm.userAgentInput;
+ input.focus();
+ input.value = ua;
+ let onUAChanged = once(manager, "userAgentChanged");
+ input.blur();
+ yield onUAChanged;
+
+ if (ua !== "") {
+ ok(input.hasAttribute("attention"), "UA input should be highlighted");
+ } else {
+ ok(!input.hasAttribute("attention"), "UA input shouldn't be highlighted");
+ }
+}
+
+function* testUserAgent(value) {
+ let ua = yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ return content.navigator.userAgent;
+ });
+ is(ua, value, `UA should be set to ${value}`);
+}
diff --git a/devtools/client/responsivedesign/test/browser_responsiveui_touch.js b/devtools/client/responsivedesign/test/browser_responsiveui_touch.js
new file mode 100644
index 000000000..c23d2dd12
--- /dev/null
+++ b/devtools/client/responsivedesign/test/browser_responsiveui_touch.js
@@ -0,0 +1,148 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = "http://mochi.test:8888/browser/devtools/client/" +
+ "responsivedesign/test/touch.html";
+const layoutReflowSynthMouseMove = "layout.reflow.synthMouseMove";
+const domViewportEnabled = "dom.meta-viewport.enabled";
+
+add_task(function* () {
+ let tab = yield addTab(TEST_URI);
+ let {rdm} = yield openRDM(tab);
+ yield pushPrefs([layoutReflowSynthMouseMove, false]);
+ yield testWithNoTouch();
+ yield rdm.enableTouch();
+ yield testWithTouch();
+ yield rdm.disableTouch();
+ yield testWithNoTouch();
+ yield closeRDM(rdm);
+});
+
+function* testWithNoTouch() {
+ let div = content.document.querySelector("div");
+ let x = 0, y = 0;
+
+ info("testWithNoTouch: Initial test parameter and mouse mouse outside div element");
+ x = -1, y = -1;
+ yield BrowserTestUtils.synthesizeMouse("div", x, y,
+ { type: "mousemove", isSynthesized: false }, gBrowser.selectedBrowser);
+ div.style.transform = "none";
+ div.style.backgroundColor = "";
+
+ info("testWithNoTouch: Move mouse into the div element");
+ yield BrowserTestUtils.synthesizeMouseAtCenter("div", { type: "mousemove", isSynthesized: false },
+ gBrowser.selectedBrowser);
+ is(div.style.backgroundColor, "red", "mouseenter or mouseover should work");
+
+ info("testWithNoTouch: Drag the div element");
+ yield BrowserTestUtils.synthesizeMouseAtCenter("div", { type: "mousedown", isSynthesized: false },
+ gBrowser.selectedBrowser);
+ x = 100; y = 100;
+ yield BrowserTestUtils.synthesizeMouse("div", x, y,
+ { type: "mousemove", isSynthesized: false }, gBrowser.selectedBrowser);
+ is(div.style.transform, "none", "touchmove shouldn't work");
+ yield BrowserTestUtils.synthesizeMouse("div", x, y,
+ { type: "mouseup", isSynthesized: false }, gBrowser.selectedBrowser);
+
+ info("testWithNoTouch: Move mouse out of the div element");
+ x = -1; y = -1;
+ yield BrowserTestUtils.synthesizeMouse("div", x, y,
+ { type: "mousemove", isSynthesized: false }, gBrowser.selectedBrowser);
+ is(div.style.backgroundColor, "blue", "mouseout or mouseleave should work");
+
+ info("testWithNoTouch: Click the div element");
+ yield synthesizeClick(div);
+ is(div.dataset.isDelay, "false", "300ms delay between touch events and mouse events should not work");
+}
+
+function* testWithTouch() {
+ let div = content.document.querySelector("div");
+ let x = 0, y = 0;
+
+ info("testWithTouch: Initial test parameter and mouse mouse outside div element");
+ x = -1, y = -1;
+ yield BrowserTestUtils.synthesizeMouse("div", x, y,
+ { type: "mousemove", isSynthesized: false }, gBrowser.selectedBrowser);
+ div.style.transform = "none";
+ div.style.backgroundColor = "";
+
+ info("testWithTouch: Move mouse into the div element");
+ yield BrowserTestUtils.synthesizeMouseAtCenter("div", { type: "mousemove", isSynthesized: false },
+ gBrowser.selectedBrowser);
+ isnot(div.style.backgroundColor, "red", "mouseenter or mouseover should not work");
+
+ info("testWithTouch: Drag the div element");
+ yield BrowserTestUtils.synthesizeMouseAtCenter("div", { type: "mousedown", isSynthesized: false },
+ gBrowser.selectedBrowser);
+ x = 100; y = 100;
+ yield BrowserTestUtils.synthesizeMouse("div", x, y,
+ { type: "mousemove", isSynthesized: false }, gBrowser.selectedBrowser);
+ isnot(div.style.transform, "none", "touchmove should work");
+ yield BrowserTestUtils.synthesizeMouse("div", x, y,
+ { type: "mouseup", isSynthesized: false }, gBrowser.selectedBrowser);
+
+ info("testWithTouch: Move mouse out of the div element");
+ x = -1; y = -1;
+ yield BrowserTestUtils.synthesizeMouse("div", x, y,
+ { type: "mousemove", isSynthesized: false }, gBrowser.selectedBrowser);
+ isnot(div.style.backgroundColor, "blue", "mouseout or mouseleave should not work");
+
+ yield testWithMetaViewportEnabled();
+ yield testWithMetaViewportDisabled();
+}
+
+function* testWithMetaViewportEnabled() {
+ yield pushPrefs([domViewportEnabled, true]);
+ let meta = content.document.querySelector("meta[name=viewport]");
+ let div = content.document.querySelector("div");
+ div.dataset.isDelay = "false";
+
+ info("testWithMetaViewportEnabled: click the div element with <meta name='viewport'>");
+ meta.content = "";
+ yield synthesizeClick(div);
+ is(div.dataset.isDelay, "true", "300ms delay between touch events and mouse events should work");
+
+ info("testWithMetaViewportEnabled: click the div element with <meta name='viewport' content='user-scalable=no'>");
+ meta.content = "user-scalable=no";
+ yield synthesizeClick(div);
+ is(div.dataset.isDelay, "false", "300ms delay between touch events and mouse events should not work");
+
+ info("testWithMetaViewportEnabled: click the div element with <meta name='viewport' content='minimum-scale=maximum-scale'>");
+ meta.content = "minimum-scale=maximum-scale";
+ yield synthesizeClick(div);
+ is(div.dataset.isDelay, "false", "300ms delay between touch events and mouse events should not work");
+
+ info("testWithMetaViewportEnabled: click the div element with <meta name='viewport' content='width=device-width'>");
+ meta.content = "width=device-width";
+ yield synthesizeClick(div);
+ is(div.dataset.isDelay, "false", "300ms delay between touch events and mouse events should not work");
+}
+
+function* testWithMetaViewportDisabled() {
+ yield pushPrefs([domViewportEnabled, false]);
+ let meta = content.document.querySelector("meta[name=viewport]");
+ let div = content.document.querySelector("div");
+ div.dataset.isDelay = "false";
+
+ info("testWithMetaViewportDisabled: click the div element with <meta name='viewport'>");
+ meta.content = "";
+ yield synthesizeClick(div);
+ is(div.dataset.isDelay, "true", "300ms delay between touch events and mouse events should work");
+}
+
+function synthesizeClick(element) {
+ let waitForClickEvent = BrowserTestUtils.waitForEvent(element, "click");
+ BrowserTestUtils.synthesizeMouseAtCenter(element, { type: "mousedown", isSynthesized: false },
+ gBrowser.selectedBrowser);
+ BrowserTestUtils.synthesizeMouseAtCenter(element, { type: "mouseup", isSynthesized: false },
+ gBrowser.selectedBrowser);
+ return waitForClickEvent;
+}
+
+function pushPrefs(...aPrefs) {
+ let deferred = promise.defer();
+ SpecialPowers.pushPrefEnv({"set": aPrefs}, deferred.resolve);
+ return deferred.promise;
+}
diff --git a/devtools/client/responsivedesign/test/browser_responsiveui_window_close.js b/devtools/client/responsivedesign/test/browser_responsiveui_window_close.js
new file mode 100644
index 000000000..a5f890a86
--- /dev/null
+++ b/devtools/client/responsivedesign/test/browser_responsiveui_window_close.js
@@ -0,0 +1,25 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(function* () {
+ let newWindowPromise = BrowserTestUtils.waitForNewWindow();
+ window.open("about:blank", "_blank");
+ let newWindow = yield newWindowPromise;
+
+ newWindow.focus();
+ yield once(newWindow.gBrowser, "load", true);
+
+ let tab = newWindow.gBrowser.selectedTab;
+ yield ResponsiveUIManager.openIfNeeded(newWindow, tab);
+
+ // Close the window on a tab with an active responsive design UI and
+ // wait for the UI to gracefully shutdown. This has leaked the window
+ // in the past.
+ ok(ResponsiveUIManager.isActiveForTab(tab),
+ "ResponsiveUI should be active for tab when the window is closed");
+ let offPromise = once(ResponsiveUIManager, "off");
+ yield BrowserTestUtils.closeWindow(newWindow);
+ yield offPromise;
+});
diff --git a/devtools/client/responsivedesign/test/browser_responsiveuiaddcustompreset.js b/devtools/client/responsivedesign/test/browser_responsiveuiaddcustompreset.js
new file mode 100644
index 000000000..3ab54b601
--- /dev/null
+++ b/devtools/client/responsivedesign/test/browser_responsiveuiaddcustompreset.js
@@ -0,0 +1,121 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(function* () {
+ let tab = yield addTab("data:text/html;charset=utf8,Test RDM custom presets");
+
+ let { rdm, manager } = yield openRDM(tab);
+
+ let oldPrompt = Services.prompt;
+ Services.prompt = {
+ value: "",
+ returnBool: true,
+ prompt: function (parent, dialogTitle, text, value, checkMsg, checkState) {
+ value.value = this.value;
+ return this.returnBool;
+ }
+ };
+
+ registerCleanupFunction(() => {
+ Services.prompt = oldPrompt;
+ });
+
+ // Is it open?
+ let container = gBrowser.getBrowserContainer();
+ is(container.getAttribute("responsivemode"), "true",
+ "Should be in responsive mode.");
+
+ ok(rdm, "RDM instance should be attached to the tab.");
+
+ // Tries to add a custom preset and cancel the prompt
+ let idx = rdm.menulist.selectedIndex;
+ let presetCount = rdm.presets.length;
+
+ Services.prompt.value = "";
+ Services.prompt.returnBool = false;
+ rdm.addbutton.doCommand();
+
+ is(idx, rdm.menulist.selectedIndex,
+ "selected item shouldn't change after add preset and cancel");
+ is(presetCount, rdm.presets.length,
+ "number of presets shouldn't change after add preset and cancel");
+
+ // Adds the custom preset with "Testing preset"
+ Services.prompt.value = "Testing preset";
+ Services.prompt.returnBool = true;
+
+ let resized = once(manager, "content-resize");
+ let customHeight = 123, customWidth = 456;
+ rdm.startResizing({});
+ rdm.setViewportSize({
+ width: customWidth,
+ height: customHeight,
+ });
+ rdm.stopResizing({});
+
+ rdm.addbutton.doCommand();
+ yield resized;
+
+ yield closeRDM(rdm);
+
+ ({rdm} = yield openRDM(tab));
+ is(container.getAttribute("responsivemode"), "true",
+ "Should be in responsive mode.");
+
+ let presetLabel = "456" + "\u00D7" + "123 (Testing preset)";
+ let customPresetIndex = yield getPresetIndex(rdm, manager, presetLabel);
+ ok(customPresetIndex >= 0, "(idx = " + customPresetIndex + ") should be the" +
+ " previously added preset in the list of items");
+
+ yield setPresetIndex(rdm, manager, customPresetIndex);
+
+ let browser = gBrowser.selectedBrowser;
+ yield ContentTask.spawn(browser, null, function* () {
+ let {innerWidth, innerHeight} = content;
+ Assert.equal(innerWidth, 456, "Selecting preset should change the width");
+ Assert.equal(innerHeight, 123, "Selecting preset should change the height");
+ });
+
+ info(`menulist count: ${rdm.menulist.itemCount}`);
+
+ rdm.removebutton.doCommand();
+
+ yield setPresetIndex(rdm, manager, 2);
+ let deletedPresetA = rdm.menulist.selectedItem.getAttribute("label");
+ rdm.removebutton.doCommand();
+
+ yield setPresetIndex(rdm, manager, 2);
+ let deletedPresetB = rdm.menulist.selectedItem.getAttribute("label");
+ rdm.removebutton.doCommand();
+
+ yield closeRDM(rdm);
+ ({rdm} = yield openRDM(tab));
+
+ customPresetIndex = yield getPresetIndex(rdm, manager, deletedPresetA);
+ is(customPresetIndex, -1,
+ "Deleted preset " + deletedPresetA + " should not be in the list anymore");
+
+ customPresetIndex = yield getPresetIndex(rdm, manager, deletedPresetB);
+ is(customPresetIndex, -1,
+ "Deleted preset " + deletedPresetB + " should not be in the list anymore");
+
+ yield closeRDM(rdm);
+});
+
+var getPresetIndex = Task.async(function* (rdm, manager, presetLabel) {
+ var testOnePreset = Task.async(function* (c) {
+ if (c == 0) {
+ return -1;
+ }
+ yield setPresetIndex(rdm, manager, c);
+
+ let item = rdm.menulist.firstChild.childNodes[c];
+ if (item.getAttribute("label") === presetLabel) {
+ return c;
+ }
+ return testOnePreset(c - 1);
+ });
+ return testOnePreset(rdm.menulist.firstChild.childNodes.length - 4);
+});
diff --git a/devtools/client/responsivedesign/test/head.js b/devtools/client/responsivedesign/test/head.js
new file mode 100644
index 000000000..3228021f6
--- /dev/null
+++ b/devtools/client/responsivedesign/test/head.js
@@ -0,0 +1,302 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+let testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/"));
+// shared-head.js handles imports, constants, and utility functions
+let sharedHeadURI = testDir + "../../../framework/test/shared-head.js";
+Services.scriptloader.loadSubScript(sharedHeadURI, this);
+
+// Import the GCLI test helper
+let gcliHelpersURI = testDir + "../../../commandline/test/helpers.js";
+Services.scriptloader.loadSubScript(gcliHelpersURI, this);
+
+flags.testing = true;
+Services.prefs.setBoolPref("devtools.responsive.html.enabled", false);
+
+registerCleanupFunction(() => {
+ flags.testing = false;
+ Services.prefs.clearUserPref("devtools.responsive.html.enabled");
+ Services.prefs.clearUserPref("devtools.responsiveUI.currentPreset");
+ Services.prefs.clearUserPref("devtools.responsiveUI.customHeight");
+ Services.prefs.clearUserPref("devtools.responsiveUI.customWidth");
+ Services.prefs.clearUserPref("devtools.responsiveUI.presets");
+ Services.prefs.clearUserPref("devtools.responsiveUI.rotate");
+});
+
+SimpleTest.requestCompleteLog();
+
+const { ResponsiveUIManager } = Cu.import("resource://devtools/client/responsivedesign/responsivedesign.jsm", {});
+
+/**
+ * Open the Responsive Design Mode
+ * @param {Tab} The browser tab to open it into (defaults to the selected tab).
+ * @param {method} The method to use to open the RDM (values: menu, keyboard)
+ * @return {rdm, manager} Returns the RUI instance and the manager
+ */
+var openRDM = Task.async(function* (tab = gBrowser.selectedTab,
+ method = "menu") {
+ let manager = ResponsiveUIManager;
+
+ let opened = once(manager, "on");
+ let resized = once(manager, "content-resize");
+ if (method == "menu") {
+ document.getElementById("menu_responsiveUI").doCommand();
+ } else {
+ synthesizeKeyFromKeyTag(document.getElementById("key_responsiveUI"));
+ }
+ yield opened;
+
+ let rdm = manager.getResponsiveUIForTab(tab);
+ rdm.transitionsEnabled = false;
+ registerCleanupFunction(() => {
+ rdm.transitionsEnabled = true;
+ });
+
+ // Wait for content to resize. This is triggered async by the preset menu
+ // auto-selecting its default entry once it's in the document.
+ yield resized;
+
+ return {rdm, manager};
+});
+
+/**
+ * Close a responsive mode instance
+ * @param {rdm} ResponsiveUI instance for the tab
+ */
+var closeRDM = Task.async(function* (rdm) {
+ let manager = ResponsiveUIManager;
+ if (!rdm) {
+ rdm = manager.getResponsiveUIForTab(gBrowser.selectedTab);
+ }
+ let closed = once(manager, "off");
+ let resized = once(manager, "content-resize");
+ rdm.close();
+ yield resized;
+ yield closed;
+});
+
+/**
+ * Open the toolbox, with the inspector tool visible.
+ * @return a promise that resolves when the inspector is ready
+ */
+var openInspector = Task.async(function* () {
+ info("Opening the inspector");
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+
+ let inspector, toolbox;
+
+ // Checking if the toolbox and the inspector are already loaded
+ // The inspector-updated event should only be waited for if the inspector
+ // isn't loaded yet
+ toolbox = gDevTools.getToolbox(target);
+ if (toolbox) {
+ inspector = toolbox.getPanel("inspector");
+ if (inspector) {
+ info("Toolbox and inspector already open");
+ return {
+ toolbox: toolbox,
+ inspector: inspector
+ };
+ }
+ }
+
+ info("Opening the toolbox");
+ toolbox = yield gDevTools.showToolbox(target, "inspector");
+ yield waitForToolboxFrameFocus(toolbox);
+ inspector = toolbox.getPanel("inspector");
+
+ info("Waiting for the inspector to update");
+ if (inspector._updateProgress) {
+ yield inspector.once("inspector-updated");
+ }
+
+ return {
+ toolbox: toolbox,
+ inspector: inspector
+ };
+});
+
+var closeToolbox = Task.async(function* () {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ yield gDevTools.closeToolbox(target);
+});
+
+/**
+ * Wait for the toolbox frame to receive focus after it loads
+ * @param {Toolbox} toolbox
+ * @return a promise that resolves when focus has been received
+ */
+function waitForToolboxFrameFocus(toolbox) {
+ info("Making sure that the toolbox's frame is focused");
+ let def = promise.defer();
+ waitForFocus(def.resolve, toolbox.win);
+ return def.promise;
+}
+
+/**
+ * Open the toolbox, with the inspector tool visible, and the sidebar that
+ * corresponds to the given id selected
+ * @return a promise that resolves when the inspector is ready and the sidebar
+ * view is visible and ready
+ */
+var openInspectorSideBar = Task.async(function* (id) {
+ let {toolbox, inspector} = yield openInspector();
+
+ info("Selecting the " + id + " sidebar");
+ inspector.sidebar.select(id);
+
+ return {
+ toolbox: toolbox,
+ inspector: inspector,
+ view: inspector[id].view || inspector[id].computedView
+ };
+});
+
+/**
+ * Checks whether the inspector's sidebar corresponding to the given id already
+ * exists
+ * @param {InspectorPanel}
+ * @param {String}
+ * @return {Boolean}
+ */
+function hasSideBarTab(inspector, id) {
+ return !!inspector.sidebar.getWindowForTab(id);
+}
+
+/**
+ * Open the toolbox, with the inspector tool visible, and the computed-view
+ * sidebar tab selected.
+ * @return a promise that resolves when the inspector is ready and the computed
+ * view is visible and ready
+ */
+function openComputedView() {
+ return openInspectorSideBar("computedview");
+}
+
+/**
+ * Open the toolbox, with the inspector tool visible, and the rule-view
+ * sidebar tab selected.
+ * @return a promise that resolves when the inspector is ready and the rule
+ * view is visible and ready
+ */
+function openRuleView() {
+ return openInspectorSideBar("ruleview");
+}
+
+/**
+ * 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 tab object when the url is loaded
+ */
+var addTab = Task.async(function* (url) {
+ info("Adding a new tab with URL: '" + url + "'");
+
+ window.focus();
+
+ let tab = gBrowser.selectedTab = gBrowser.addTab(url);
+ let browser = tab.linkedBrowser;
+
+ yield BrowserTestUtils.browserLoaded(browser);
+ info("URL '" + url + "' loading complete");
+
+ return tab;
+});
+
+/**
+ * Waits for the next load to complete in the current browser.
+ *
+ * @return promise
+ */
+function waitForDocLoadComplete(aBrowser = gBrowser) {
+ let deferred = promise.defer();
+ let progressListener = {
+ onStateChange: function (webProgress, req, flags, status) {
+ let docStop = Ci.nsIWebProgressListener.STATE_IS_NETWORK |
+ Ci.nsIWebProgressListener.STATE_STOP;
+ info(`Saw state ${flags.toString(16)} and status ${status.toString(16)}`);
+
+ // When a load needs to be retargetted to a new process it is cancelled
+ // with NS_BINDING_ABORTED so ignore that case
+ if ((flags & docStop) == docStop && status != Cr.NS_BINDING_ABORTED) {
+ aBrowser.removeProgressListener(progressListener);
+ info("Browser loaded");
+ deferred.resolve();
+ }
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
+ Ci.nsISupportsWeakReference])
+ };
+ aBrowser.addProgressListener(progressListener);
+ info("Waiting for browser load");
+ return deferred.promise;
+}
+
+/**
+ * Get the NodeFront for a node that matches a given css selector, via the
+ * protocol.
+ * @param {String|NodeFront} selector
+ * @param {InspectorPanel} inspector The instance of InspectorPanel currently
+ * loaded in the toolbox
+ * @return {Promise} Resolves to the NodeFront instance
+ */
+function getNodeFront(selector, {walker}) {
+ if (selector._form) {
+ return selector;
+ }
+ return walker.querySelector(walker.rootNode, selector);
+}
+
+/**
+ * Set the inspector's current selection to the first match of the given css
+ * selector
+ * @param {String|NodeFront} selector
+ * @param {InspectorPanel} inspector The instance of InspectorPanel currently
+ * loaded in the toolbox
+ * @param {String} reason Defaults to "test" which instructs the inspector not
+ * to highlight the node upon selection
+ * @return {Promise} Resolves when the inspector is updated with the new node
+ */
+var selectNode = Task.async(function* (selector, inspector, reason = "test") {
+ info("Selecting the node for '" + selector + "'");
+ let nodeFront = yield getNodeFront(selector, inspector);
+ let updated = inspector.once("inspector-updated");
+ inspector.selection.setNodeFront(nodeFront, reason);
+ yield updated;
+});
+
+function waitForResizeTo(manager, width, height) {
+ return new Promise(resolve => {
+ let onResize = (_, data) => {
+ if (data.width != width || data.height != height) {
+ return;
+ }
+ manager.off("content-resize", onResize);
+ info(`Got content-resize to ${width} x ${height}`);
+ resolve();
+ };
+ info(`Waiting for content-resize to ${width} x ${height}`);
+ manager.on("content-resize", onResize);
+ });
+}
+
+var setPresetIndex = Task.async(function* (rdm, manager, index) {
+ info(`Current preset: ${rdm.menulist.selectedIndex}, change to: ${index}`);
+ if (rdm.menulist.selectedIndex != index) {
+ let resized = once(manager, "content-resize");
+ rdm.menulist.selectedIndex = index;
+ yield resized;
+ }
+});
+
+var setSize = Task.async(function* (rdm, manager, width, height) {
+ let size = rdm.getSize();
+ info(`Current size: ${size.width} x ${size.height}, ` +
+ `set to: ${width} x ${height}`);
+ if (size.width != width || size.height != height) {
+ let resized = waitForResizeTo(manager, width, height);
+ rdm.setViewportSize({ width, height });
+ yield resized;
+ }
+});
diff --git a/devtools/client/responsivedesign/test/touch.html b/devtools/client/responsivedesign/test/touch.html
new file mode 100644
index 000000000..f93d36f6c
--- /dev/null
+++ b/devtools/client/responsivedesign/test/touch.html
@@ -0,0 +1,85 @@
+<!DOCTYPE html>
+
+<meta charset="utf-8" />
+<meta name="viewport" />
+<title>test</title>
+
+
+<style>
+ div {
+ border:1px solid red;
+ width: 100px; height: 100px;
+ }
+</style>
+
+<div data-is-delay="false"></div>
+
+<script>
+ var div = document.querySelector("div");
+ var initX, initY;
+ var previousEvent = "", touchendTime = 0;
+ var updatePreviousEvent = function(e){
+ previousEvent = e.type;
+ };
+
+ div.style.transform = "none";
+ div.style.backgroundColor = "";
+
+ div.addEventListener("touchstart", function(evt) {
+ var touch = evt.changedTouches[0];
+ initX = touch.pageX;
+ initY = touch.pageY;
+ updatePreviousEvent(evt);
+ }, true);
+
+ div.addEventListener("touchmove", function(evt) {
+ var touch = evt.changedTouches[0];
+ var deltaX = touch.pageX - initX;
+ var deltaY = touch.pageY - initY;
+ div.style.transform = "translate(" + deltaX + "px, " + deltaY + "px)";
+ updatePreviousEvent(evt);
+ }, true);
+
+ div.addEventListener("touchend", function(evt) {
+ if (!evt.touches.length) {
+ div.style.transform = "none";
+ }
+ touchendTime = performance.now();
+ updatePreviousEvent(evt);
+ }, true);
+
+ div.addEventListener("mouseenter", function(evt) {
+ div.style.backgroundColor = "red";
+ updatePreviousEvent(evt);
+ }, true);
+ div.addEventListener("mouseover", function(evt) {
+ div.style.backgroundColor = "red";
+ updatePreviousEvent(evt);
+ }, true);
+
+ div.addEventListener("mouseout", function(evt) {
+ div.style.backgroundColor = "blue";
+ updatePreviousEvent(evt);
+ }, true);
+
+ div.addEventListener("mouseleave", function(evt) {
+ div.style.backgroundColor = "blue";
+ updatePreviousEvent(evt);
+ }, true);
+
+ div.addEventListener("mousedown", function(evt){
+ if (previousEvent === "touchend" && touchendTime !== 0) {
+ let now = performance.now();
+ div.dataset.isDelay = ((now - touchendTime) >= 300) ? true : false;
+ } else {
+ div.dataset.isDelay = false;
+ }
+ updatePreviousEvent(evt);
+ }, true);
+
+ div.addEventListener("mousemove", updatePreviousEvent, true);
+
+ div.addEventListener("mouseup", updatePreviousEvent, true);
+
+ div.addEventListener("click", updatePreviousEvent, true);
+</script>
diff --git a/devtools/client/scratchpad/moz.build b/devtools/client/scratchpad/moz.build
new file mode 100644
index 000000000..da8257c11
--- /dev/null
+++ b/devtools/client/scratchpad/moz.build
@@ -0,0 +1,13 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'scratchpad-commands.js',
+ 'scratchpad-manager.jsm',
+ 'scratchpad-panel.js',
+)
+
+BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
diff --git a/devtools/client/scratchpad/scratchpad-commands.js b/devtools/client/scratchpad/scratchpad-commands.js
new file mode 100644
index 000000000..8ae1fc4da
--- /dev/null
+++ b/devtools/client/scratchpad/scratchpad-commands.js
@@ -0,0 +1,22 @@
+/* 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 l10n = require("gcli/l10n");
+const {Cu} = require("chrome");
+
+exports.items = [{
+ item: "command",
+ runAt: "client",
+ name: "scratchpad",
+ buttonId: "command-button-scratchpad",
+ buttonClass: "command-button command-button-invertable",
+ tooltipText: l10n.lookup("scratchpadOpenTooltip"),
+ hidden: true,
+ exec: function (args, context) {
+ const {ScratchpadManager} = Cu.import("resource://devtools/client/scratchpad/scratchpad-manager.jsm", {});
+ ScratchpadManager.openScratchpad();
+ }
+}];
diff --git a/devtools/client/scratchpad/scratchpad-manager.jsm b/devtools/client/scratchpad/scratchpad-manager.jsm
new file mode 100644
index 000000000..5b4b3bd0a
--- /dev/null
+++ b/devtools/client/scratchpad/scratchpad-manager.jsm
@@ -0,0 +1,185 @@
+/* vim:set ts=2 sw=2 sts=2 et 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";
+
+this.EXPORTED_SYMBOLS = ["ScratchpadManager"];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+const SCRATCHPAD_WINDOW_URL = "chrome://devtools/content/scratchpad/scratchpad.xul";
+const SCRATCHPAD_WINDOW_FEATURES = "chrome,titlebar,toolbar,centerscreen,resizable,dialog=no";
+
+const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const Services = require("Services");
+const Telemetry = require("devtools/client/shared/telemetry");
+
+
+/**
+ * The ScratchpadManager object opens new Scratchpad windows and manages the state
+ * of open scratchpads for session restore. There's only one ScratchpadManager in
+ * the life of the browser.
+ */
+this.ScratchpadManager = {
+
+ _nextUid: 1,
+ _scratchpads: [],
+
+ _telemetry: new Telemetry(),
+
+ /**
+ * Get the saved states of open scratchpad windows. Called by
+ * session restore.
+ *
+ * @return array
+ * The array of scratchpad states.
+ */
+ getSessionState: function SPM_getSessionState()
+ {
+ return this._scratchpads;
+ },
+
+ /**
+ * Restore scratchpad windows from the scratchpad session store file.
+ * Called by session restore.
+ *
+ * @param function aSession
+ * The session object with scratchpad states.
+ *
+ * @return array
+ * The restored scratchpad windows.
+ */
+ restoreSession: function SPM_restoreSession(aSession)
+ {
+ if (!Array.isArray(aSession)) {
+ return [];
+ }
+
+ let wins = [];
+ aSession.forEach(function (state) {
+ let win = this.openScratchpad(state);
+ wins.push(win);
+ }, this);
+
+ return wins;
+ },
+
+ /**
+ * Iterate through open scratchpad windows and save their states.
+ */
+ saveOpenWindows: function SPM_saveOpenWindows() {
+ this._scratchpads = [];
+
+ function clone(src) {
+ let dest = {};
+
+ for (let key in src) {
+ if (src.hasOwnProperty(key)) {
+ dest[key] = src[key];
+ }
+ }
+
+ return dest;
+ }
+
+ // We need to clone objects we get from Scratchpad instances
+ // because such (cross-window) objects have a property 'parent'
+ // that holds on to a ChromeWindow instance. This means that
+ // such objects are not primitive-values-only anymore so they
+ // can leak.
+
+ let enumerator = Services.wm.getEnumerator("devtools:scratchpad");
+ while (enumerator.hasMoreElements()) {
+ let win = enumerator.getNext();
+ if (!win.closed && win.Scratchpad.initialized) {
+ this._scratchpads.push(clone(win.Scratchpad.getState()));
+ }
+ }
+ },
+
+ /**
+ * Open a new scratchpad window with an optional initial state.
+ *
+ * @param object aState
+ * Optional. The initial state of the scratchpad, an object
+ * with properties filename, text, and executionContext.
+ *
+ * @return nsIDomWindow
+ * The opened scratchpad window.
+ */
+ openScratchpad: function SPM_openScratchpad(aState)
+ {
+ let params = Cc["@mozilla.org/embedcomp/dialogparam;1"]
+ .createInstance(Ci.nsIDialogParamBlock);
+
+ params.SetNumberStrings(2);
+ params.SetString(0, this.createUid());
+
+ if (aState) {
+ if (typeof aState != "object") {
+ return;
+ }
+
+ params.SetString(1, JSON.stringify(aState));
+ }
+
+ let win = Services.ww.openWindow(null, SCRATCHPAD_WINDOW_URL, "_blank",
+ SCRATCHPAD_WINDOW_FEATURES, params);
+
+ this._telemetry.toolOpened("scratchpad-window");
+ let onClose = () => {
+ this._telemetry.toolClosed("scratchpad-window");
+ };
+ win.addEventListener("unload", onClose);
+
+ // Only add the shutdown observer if we've opened a scratchpad window.
+ ShutdownObserver.init();
+
+ return win;
+ },
+
+ /**
+ * Create a unique ID for a new Scratchpad.
+ */
+ createUid: function SPM_createUid()
+ {
+ return JSON.stringify(this._nextUid++);
+ }
+};
+
+
+/**
+ * The ShutdownObserver listens for app shutdown and saves the current state
+ * of the scratchpads for session restore.
+ */
+var ShutdownObserver = {
+ _initialized: false,
+
+ init: function SDO_init()
+ {
+ if (this._initialized) {
+ return;
+ }
+
+ Services.obs.addObserver(this, "quit-application-granted", false);
+
+ this._initialized = true;
+ },
+
+ observe: function SDO_observe(aMessage, aTopic, aData)
+ {
+ if (aTopic == "quit-application-granted") {
+ ScratchpadManager.saveOpenWindows();
+ this.uninit();
+ }
+ },
+
+ uninit: function SDO_uninit()
+ {
+ Services.obs.removeObserver(this, "quit-application-granted");
+ }
+};
diff --git a/devtools/client/scratchpad/scratchpad-panel.js b/devtools/client/scratchpad/scratchpad-panel.js
new file mode 100644
index 000000000..6f92585f7
--- /dev/null
+++ b/devtools/client/scratchpad/scratchpad-panel.js
@@ -0,0 +1,56 @@
+/* -*- 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 {Cu} = require("chrome");
+const EventEmitter = require("devtools/shared/event-emitter");
+const promise = require("promise");
+
+
+function ScratchpadPanel(iframeWindow, toolbox) {
+ let { Scratchpad } = iframeWindow;
+ this._toolbox = toolbox;
+ this.panelWin = iframeWindow;
+ this.scratchpad = Scratchpad;
+
+ Scratchpad.target = this.target;
+ Scratchpad.hideMenu();
+
+ let deferred = promise.defer();
+ this._readyObserver = deferred.promise;
+ Scratchpad.addObserver({
+ onReady: function () {
+ Scratchpad.removeObserver(this);
+ deferred.resolve();
+ }
+ });
+
+ EventEmitter.decorate(this);
+}
+exports.ScratchpadPanel = ScratchpadPanel;
+
+ScratchpadPanel.prototype = {
+ /**
+ * Open is effectively an asynchronous constructor. For the ScratchpadPanel,
+ * by the time this is called, the Scratchpad will already be ready.
+ */
+ open: function () {
+ return this._readyObserver.then(() => {
+ this.isReady = true;
+ this.emit("ready");
+ return this;
+ });
+ },
+
+ get target() {
+ return this._toolbox.target;
+ },
+
+ destroy: function () {
+ this.emit("destroyed");
+ return promise.resolve();
+ }
+};
diff --git a/devtools/client/scratchpad/scratchpad.js b/devtools/client/scratchpad/scratchpad.js
new file mode 100644
index 000000000..306b635df
--- /dev/null
+++ b/devtools/client/scratchpad/scratchpad.js
@@ -0,0 +1,2480 @@
+/* vim:set ts=2 sw=2 sts=2 et:
+ * 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/. */
+
+/*
+ * Original version history can be found here:
+ * https://github.com/mozilla/workspace
+ *
+ * Copied and relicensed from the Public Domain.
+ * See bug 653934 for details.
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=653934
+ */
+
+"use strict";
+
+var Cu = Components.utils;
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+
+const SCRATCHPAD_CONTEXT_CONTENT = 1;
+const SCRATCHPAD_CONTEXT_BROWSER = 2;
+const BUTTON_POSITION_SAVE = 0;
+const BUTTON_POSITION_CANCEL = 1;
+const BUTTON_POSITION_DONT_SAVE = 2;
+const BUTTON_POSITION_REVERT = 0;
+const EVAL_FUNCTION_TIMEOUT = 1000; // milliseconds
+
+const MAXIMUM_FONT_SIZE = 96;
+const MINIMUM_FONT_SIZE = 6;
+const NORMAL_FONT_SIZE = 12;
+
+const SCRATCHPAD_L10N = "chrome://devtools/locale/scratchpad.properties";
+const DEVTOOLS_CHROME_ENABLED = "devtools.chrome.enabled";
+const PREF_RECENT_FILES_MAX = "devtools.scratchpad.recentFilesMax";
+const SHOW_LINE_NUMBERS = "devtools.scratchpad.lineNumbers";
+const WRAP_TEXT = "devtools.scratchpad.wrapText";
+const SHOW_TRAILING_SPACE = "devtools.scratchpad.showTrailingSpace";
+const EDITOR_FONT_SIZE = "devtools.scratchpad.editorFontSize";
+const ENABLE_AUTOCOMPLETION = "devtools.scratchpad.enableAutocompletion";
+const TAB_SIZE = "devtools.editor.tabsize";
+const FALLBACK_CHARSET_LIST = "intl.fallbackCharsetList.ISO-8859-1";
+
+const VARIABLES_VIEW_URL = "chrome://devtools/content/shared/widgets/VariablesView.xul";
+
+const {require, loader} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+
+const Editor = require("devtools/client/sourceeditor/editor");
+const TargetFactory = require("devtools/client/framework/target").TargetFactory;
+const EventEmitter = require("devtools/shared/event-emitter");
+const {DevToolsWorker} = require("devtools/shared/worker/worker");
+const DevToolsUtils = require("devtools/shared/DevToolsUtils");
+const flags = require("devtools/shared/flags");
+const promise = require("promise");
+const Services = require("Services");
+const {gDevTools} = require("devtools/client/framework/devtools");
+const {Heritage} = require("devtools/client/shared/widgets/view-helpers");
+
+const {XPCOMUtils} = require("resource://gre/modules/XPCOMUtils.jsm");
+const {NetUtil} = require("resource://gre/modules/NetUtil.jsm");
+const {ScratchpadManager} = require("resource://devtools/client/scratchpad/scratchpad-manager.jsm");
+const {addDebuggerToGlobal} = require("resource://gre/modules/jsdebugger.jsm");
+const {OS} = require("resource://gre/modules/osfile.jsm");
+const {Reflect} = require("resource://gre/modules/reflect.jsm");
+
+XPCOMUtils.defineConstant(this, "SCRATCHPAD_CONTEXT_CONTENT", SCRATCHPAD_CONTEXT_CONTENT);
+XPCOMUtils.defineConstant(this, "SCRATCHPAD_CONTEXT_BROWSER", SCRATCHPAD_CONTEXT_BROWSER);
+XPCOMUtils.defineConstant(this, "BUTTON_POSITION_SAVE", BUTTON_POSITION_SAVE);
+XPCOMUtils.defineConstant(this, "BUTTON_POSITION_CANCEL", BUTTON_POSITION_CANCEL);
+XPCOMUtils.defineConstant(this, "BUTTON_POSITION_DONT_SAVE", BUTTON_POSITION_DONT_SAVE);
+XPCOMUtils.defineConstant(this, "BUTTON_POSITION_REVERT", BUTTON_POSITION_REVERT);
+
+XPCOMUtils.defineLazyModuleGetter(this, "VariablesView",
+ "resource://devtools/client/shared/widgets/VariablesView.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "VariablesViewController",
+ "resource://devtools/client/shared/widgets/VariablesViewController.jsm");
+
+loader.lazyRequireGetter(this, "DebuggerServer", "devtools/server/main", true);
+
+loader.lazyRequireGetter(this, "DebuggerClient", "devtools/shared/client/main", true);
+loader.lazyRequireGetter(this, "EnvironmentClient", "devtools/shared/client/main", true);
+loader.lazyRequireGetter(this, "ObjectClient", "devtools/shared/client/main", true);
+loader.lazyRequireGetter(this, "HUDService", "devtools/client/webconsole/hudservice");
+
+XPCOMUtils.defineLazyGetter(this, "REMOTE_TIMEOUT", () =>
+ Services.prefs.getIntPref("devtools.debugger.remote-timeout"));
+
+XPCOMUtils.defineLazyModuleGetter(this, "ShortcutUtils",
+ "resource://gre/modules/ShortcutUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Reflect",
+ "resource://gre/modules/reflect.jsm");
+
+var WebConsoleUtils = require("devtools/client/webconsole/utils").Utils;
+
+/**
+ * The scratchpad object handles the Scratchpad window functionality.
+ */
+var Scratchpad = {
+ _instanceId: null,
+ _initialWindowTitle: document.title,
+ _dirty: false,
+
+ /**
+ * Check if provided string is a mode-line and, if it is, return an
+ * object with its values.
+ *
+ * @param string aLine
+ * @return string
+ */
+ _scanModeLine: function SP__scanModeLine(aLine = "")
+ {
+ aLine = aLine.trim();
+
+ let obj = {};
+ let ch1 = aLine.charAt(0);
+ let ch2 = aLine.charAt(1);
+
+ if (ch1 !== "/" || (ch2 !== "*" && ch2 !== "/")) {
+ return obj;
+ }
+
+ aLine = aLine
+ .replace(/^\/\//, "")
+ .replace(/^\/\*/, "")
+ .replace(/\*\/$/, "");
+
+ aLine.split(",").forEach(pair => {
+ let [key, val] = pair.split(":");
+
+ if (key && val) {
+ obj[key.trim()] = val.trim();
+ }
+ });
+
+ return obj;
+ },
+
+ /**
+ * Add the event listeners for popupshowing events.
+ */
+ _setupPopupShowingListeners: function SP_setupPopupShowing() {
+ let elementIDs = ["sp-menu_editpopup", "scratchpad-text-popup"];
+
+ for (let elementID of elementIDs) {
+ let elem = document.getElementById(elementID);
+ if (elem) {
+ elem.addEventListener("popupshowing", function () {
+ goUpdateGlobalEditMenuItems();
+ let commands = ["cmd_undo", "cmd_redo", "cmd_delete", "cmd_findAgain"];
+ commands.forEach(goUpdateCommand);
+ });
+ }
+ }
+ },
+
+ /**
+ * Add the event event listeners for command events.
+ */
+ _setupCommandListeners: function SP_setupCommands() {
+ let commands = {
+ "cmd_find": () => {
+ goDoCommand("cmd_find");
+ },
+ "cmd_findAgain": () => {
+ goDoCommand("cmd_findAgain");
+ },
+ "cmd_gotoLine": () => {
+ goDoCommand("cmd_gotoLine");
+ },
+ "sp-cmd-newWindow": () => {
+ Scratchpad.openScratchpad();
+ },
+ "sp-cmd-openFile": () => {
+ Scratchpad.openFile();
+ },
+ "sp-cmd-clearRecentFiles": () => {
+ Scratchpad.clearRecentFiles();
+ },
+ "sp-cmd-save": () => {
+ Scratchpad.saveFile();
+ },
+ "sp-cmd-saveas": () => {
+ Scratchpad.saveFileAs();
+ },
+ "sp-cmd-revert": () => {
+ Scratchpad.promptRevert();
+ },
+ "sp-cmd-close": () => {
+ Scratchpad.close();
+ },
+ "sp-cmd-run": () => {
+ Scratchpad.run();
+ },
+ "sp-cmd-inspect": () => {
+ Scratchpad.inspect();
+ },
+ "sp-cmd-display": () => {
+ Scratchpad.display();
+ },
+ "sp-cmd-pprint": () => {
+ Scratchpad.prettyPrint();
+ },
+ "sp-cmd-contentContext": () => {
+ Scratchpad.setContentContext();
+ },
+ "sp-cmd-browserContext": () => {
+ Scratchpad.setBrowserContext();
+ },
+ "sp-cmd-reloadAndRun": () => {
+ Scratchpad.reloadAndRun();
+ },
+ "sp-cmd-evalFunction": () => {
+ Scratchpad.evalTopLevelFunction();
+ },
+ "sp-cmd-errorConsole": () => {
+ Scratchpad.openErrorConsole();
+ },
+ "sp-cmd-webConsole": () => {
+ Scratchpad.openWebConsole();
+ },
+ "sp-cmd-documentationLink": () => {
+ Scratchpad.openDocumentationPage();
+ },
+ "sp-cmd-hideSidebar": () => {
+ Scratchpad.sidebar.hide();
+ },
+ "sp-cmd-line-numbers": () => {
+ Scratchpad.toggleEditorOption("lineNumbers", SHOW_LINE_NUMBERS);
+ },
+ "sp-cmd-wrap-text": () => {
+ Scratchpad.toggleEditorOption("lineWrapping", WRAP_TEXT);
+ },
+ "sp-cmd-highlight-trailing-space": () => {
+ Scratchpad.toggleEditorOption("showTrailingSpace", SHOW_TRAILING_SPACE);
+ },
+ "sp-cmd-larger-font": () => {
+ Scratchpad.increaseFontSize();
+ },
+ "sp-cmd-smaller-font": () => {
+ Scratchpad.decreaseFontSize();
+ },
+ "sp-cmd-normal-font": () => {
+ Scratchpad.normalFontSize();
+ },
+ };
+
+ for (let command in commands) {
+ let elem = document.getElementById(command);
+ if (elem) {
+ elem.addEventListener("command", commands[command]);
+ }
+ }
+ },
+
+ /**
+ * Check or uncheck view menu items according to stored preferences.
+ */
+ _updateViewMenuItems: function SP_updateViewMenuItems() {
+ this._updateViewMenuItem(SHOW_LINE_NUMBERS, "sp-menu-line-numbers");
+ this._updateViewMenuItem(WRAP_TEXT, "sp-menu-word-wrap");
+ this._updateViewMenuItem(SHOW_TRAILING_SPACE, "sp-menu-highlight-trailing-space");
+ this._updateViewFontMenuItem(MINIMUM_FONT_SIZE, "sp-cmd-smaller-font");
+ this._updateViewFontMenuItem(MAXIMUM_FONT_SIZE, "sp-cmd-larger-font");
+ },
+
+ /**
+ * Check or uncheck view menu item according to stored preferences.
+ */
+ _updateViewMenuItem: function SP_updateViewMenuItem(preferenceName, menuId) {
+ let checked = Services.prefs.getBoolPref(preferenceName);
+ if (checked) {
+ document.getElementById(menuId).setAttribute("checked", true);
+ } else {
+ document.getElementById(menuId).removeAttribute("checked");
+ }
+ },
+
+ /**
+ * Disable view menu item if the stored font size is equals to the given one.
+ */
+ _updateViewFontMenuItem: function SP_updateViewFontMenuItem(fontSize, commandId) {
+ let prefFontSize = Services.prefs.getIntPref(EDITOR_FONT_SIZE);
+ if (prefFontSize === fontSize) {
+ document.getElementById(commandId).setAttribute("disabled", true);
+ }
+ },
+
+ /**
+ * The script execution context. This tells Scratchpad in which context the
+ * script shall execute.
+ *
+ * Possible values:
+ * - SCRATCHPAD_CONTEXT_CONTENT to execute code in the context of the current
+ * tab content window object.
+ * - SCRATCHPAD_CONTEXT_BROWSER to execute code in the context of the
+ * currently active chrome window object.
+ */
+ executionContext: SCRATCHPAD_CONTEXT_CONTENT,
+
+ /**
+ * Tells if this Scratchpad is initialized and ready for use.
+ * @boolean
+ * @see addObserver
+ */
+ initialized: false,
+
+ /**
+ * Returns the 'dirty' state of this Scratchpad.
+ */
+ get dirty()
+ {
+ let clean = this.editor && this.editor.isClean();
+ return this._dirty || !clean;
+ },
+
+ /**
+ * Sets the 'dirty' state of this Scratchpad.
+ */
+ set dirty(aValue)
+ {
+ this._dirty = aValue;
+ if (!aValue && this.editor)
+ this.editor.setClean();
+ this._updateTitle();
+ },
+
+ /**
+ * Retrieve the xul:notificationbox DOM element. It notifies the user when
+ * the current code execution context is SCRATCHPAD_CONTEXT_BROWSER.
+ */
+ get notificationBox()
+ {
+ return document.getElementById("scratchpad-notificationbox");
+ },
+
+ /**
+ * Hide the menu bar.
+ */
+ hideMenu: function SP_hideMenu()
+ {
+ document.getElementById("sp-menubar").style.display = "none";
+ },
+
+ /**
+ * Show the menu bar.
+ */
+ showMenu: function SP_showMenu()
+ {
+ document.getElementById("sp-menubar").style.display = "";
+ },
+
+ /**
+ * Get the editor content, in the given range. If no range is given you get
+ * the entire editor content.
+ *
+ * @param number [aStart=0]
+ * Optional, start from the given offset.
+ * @param number [aEnd=content char count]
+ * Optional, end offset for the text you want. If this parameter is not
+ * given, then the text returned goes until the end of the editor
+ * content.
+ * @return string
+ * The text in the given range.
+ */
+ getText: function SP_getText(aStart, aEnd)
+ {
+ var value = this.editor.getText();
+ return value.slice(aStart || 0, aEnd || value.length);
+ },
+
+ /**
+ * Set the filename in the scratchpad UI and object
+ *
+ * @param string aFilename
+ * The new filename
+ */
+ setFilename: function SP_setFilename(aFilename)
+ {
+ this.filename = aFilename;
+ this._updateTitle();
+ },
+
+ /**
+ * Update the Scratchpad window title based on the current state.
+ * @private
+ */
+ _updateTitle: function SP__updateTitle()
+ {
+ let title = this.filename || this._initialWindowTitle;
+
+ if (this.dirty)
+ title = "*" + title;
+
+ document.title = title;
+ },
+
+ /**
+ * Get the current state of the scratchpad. Called by the
+ * Scratchpad Manager for session storing.
+ *
+ * @return object
+ * An object with 3 properties: filename, text, and
+ * executionContext.
+ */
+ getState: function SP_getState()
+ {
+ return {
+ filename: this.filename,
+ text: this.getText(),
+ executionContext: this.executionContext,
+ saved: !this.dirty
+ };
+ },
+
+ /**
+ * Set the filename and execution context using the given state. Called
+ * when scratchpad is being restored from a previous session.
+ *
+ * @param object aState
+ * An object with filename and executionContext properties.
+ */
+ setState: function SP_setState(aState)
+ {
+ if (aState.filename)
+ this.setFilename(aState.filename);
+
+ this.dirty = !aState.saved;
+
+ if (aState.executionContext == SCRATCHPAD_CONTEXT_BROWSER)
+ this.setBrowserContext();
+ else
+ this.setContentContext();
+ },
+
+ /**
+ * Get the most recent main chrome browser window
+ */
+ get browserWindow()
+ {
+ return Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
+ },
+
+ /**
+ * Get the gBrowser object of the most recent browser window.
+ */
+ get gBrowser()
+ {
+ let recentWin = this.browserWindow;
+ return recentWin ? recentWin.gBrowser : null;
+ },
+
+ /**
+ * Unique name for the current Scratchpad instance. Used to distinguish
+ * Scratchpad windows between each other. See bug 661762.
+ */
+ get uniqueName()
+ {
+ return "Scratchpad/" + this._instanceId;
+ },
+
+
+ /**
+ * Sidebar that contains the VariablesView for object inspection.
+ */
+ get sidebar()
+ {
+ if (!this._sidebar) {
+ this._sidebar = new ScratchpadSidebar(this);
+ }
+ return this._sidebar;
+ },
+
+ /**
+ * Replaces context of an editor with provided value (a string).
+ * Note: this method is simply a shortcut to editor.setText.
+ */
+ setText: function SP_setText(value)
+ {
+ return this.editor.setText(value);
+ },
+
+ /**
+ * Evaluate a string in the currently desired context, that is either the
+ * chrome window or the tab content window object.
+ *
+ * @param string aString
+ * The script you want to evaluate.
+ * @return Promise
+ * The promise for the script evaluation result.
+ */
+ evaluate: function SP_evaluate(aString)
+ {
+ let connection;
+ if (this.target) {
+ connection = ScratchpadTarget.consoleFor(this.target);
+ }
+ else if (this.executionContext == SCRATCHPAD_CONTEXT_CONTENT) {
+ connection = ScratchpadTab.consoleFor(this.gBrowser.selectedTab);
+ }
+ else {
+ connection = ScratchpadWindow.consoleFor(this.browserWindow);
+ }
+
+ let evalOptions = { url: this.uniqueName };
+
+ return connection.then(({ debuggerClient, webConsoleClient }) => {
+ let deferred = promise.defer();
+
+ webConsoleClient.evaluateJSAsync(aString, aResponse => {
+ this.debuggerClient = debuggerClient;
+ this.webConsoleClient = webConsoleClient;
+ if (aResponse.error) {
+ deferred.reject(aResponse);
+ }
+ else if (aResponse.exception !== null) {
+ deferred.resolve([aString, aResponse]);
+ }
+ else {
+ deferred.resolve([aString, undefined, aResponse.result]);
+ }
+ }, evalOptions);
+
+ return deferred.promise;
+ });
+ },
+
+ /**
+ * Execute the selected text (if any) or the entire editor content in the
+ * current context.
+ *
+ * @return Promise
+ * The promise for the script evaluation result.
+ */
+ execute: function SP_execute()
+ {
+ WebConsoleUtils.usageCount++;
+ let selection = this.editor.getSelection() || this.getText();
+ return this.evaluate(selection);
+ },
+
+ /**
+ * Execute the selected text (if any) or the entire editor content in the
+ * current context.
+ *
+ * @return Promise
+ * The promise for the script evaluation result.
+ */
+ run: function SP_run()
+ {
+ let deferred = promise.defer();
+ let reject = aReason => deferred.reject(aReason);
+
+ this.execute().then(([aString, aError, aResult]) => {
+ let resolve = () => deferred.resolve([aString, aError, aResult]);
+
+ if (aError) {
+ this.writeAsErrorComment(aError).then(resolve, reject);
+ }
+ else {
+ this.editor.dropSelection();
+ resolve();
+ }
+ }, reject);
+
+ return deferred.promise;
+ },
+
+ /**
+ * Execute the selected text (if any) or the entire editor content in the
+ * current context. The resulting object is inspected up in the sidebar.
+ *
+ * @return Promise
+ * The promise for the script evaluation result.
+ */
+ inspect: function SP_inspect()
+ {
+ let deferred = promise.defer();
+ let reject = aReason => deferred.reject(aReason);
+
+ this.execute().then(([aString, aError, aResult]) => {
+ let resolve = () => deferred.resolve([aString, aError, aResult]);
+
+ if (aError) {
+ this.writeAsErrorComment(aError).then(resolve, reject);
+ }
+ else {
+ this.editor.dropSelection();
+ this.sidebar.open(aString, aResult).then(resolve, reject);
+ }
+ }, reject);
+
+ return deferred.promise;
+ },
+
+ /**
+ * Reload the current page and execute the entire editor content when
+ * the page finishes loading. Note that this operation should be available
+ * only in the content context.
+ *
+ * @return Promise
+ * The promise for the script evaluation result.
+ */
+ reloadAndRun: function SP_reloadAndRun()
+ {
+ let deferred = promise.defer();
+
+ if (this.executionContext !== SCRATCHPAD_CONTEXT_CONTENT) {
+ console.error(this.strings.
+ GetStringFromName("scratchpadContext.invalid"));
+ return;
+ }
+
+ let target = TargetFactory.forTab(this.gBrowser.selectedTab);
+ target.once("navigate", () => {
+ this.run().then(results => deferred.resolve(results));
+ });
+ target.makeRemote().then(() => target.activeTab.reload());
+
+ return deferred.promise;
+ },
+
+ /**
+ * Execute the selected text (if any) or the entire editor content in the
+ * current context. The evaluation result is inserted into the editor after
+ * the selected text, or at the end of the editor content if there is no
+ * selected text.
+ *
+ * @return Promise
+ * The promise for the script evaluation result.
+ */
+ display: function SP_display()
+ {
+ let deferred = promise.defer();
+ let reject = aReason => deferred.reject(aReason);
+
+ this.execute().then(([aString, aError, aResult]) => {
+ let resolve = () => deferred.resolve([aString, aError, aResult]);
+
+ if (aError) {
+ this.writeAsErrorComment(aError).then(resolve, reject);
+ }
+ else if (VariablesView.isPrimitive({ value: aResult })) {
+ this._writePrimitiveAsComment(aResult).then(resolve, reject);
+ }
+ else {
+ let objectClient = new ObjectClient(this.debuggerClient, aResult);
+ objectClient.getDisplayString(aResponse => {
+ if (aResponse.error) {
+ reportError("display", aResponse);
+ reject(aResponse);
+ }
+ else {
+ this.writeAsComment(aResponse.displayString);
+ resolve();
+ }
+ });
+ }
+ }, reject);
+
+ return deferred.promise;
+ },
+
+ _prettyPrintWorker: null,
+
+ /**
+ * Get or create the worker that handles pretty printing.
+ */
+ get prettyPrintWorker() {
+ if (!this._prettyPrintWorker) {
+ this._prettyPrintWorker = new DevToolsWorker(
+ "resource://devtools/server/actors/pretty-print-worker.js",
+ { name: "pretty-print",
+ verbose: flags.wantLogging }
+ );
+ }
+ return this._prettyPrintWorker;
+ },
+
+ /**
+ * Pretty print the source text inside the scratchpad.
+ *
+ * @return Promise
+ * A promise resolved with the pretty printed code, or rejected with
+ * an error.
+ */
+ prettyPrint: function SP_prettyPrint() {
+ const uglyText = this.getText();
+ const tabsize = Services.prefs.getIntPref(TAB_SIZE);
+
+ return this.prettyPrintWorker.performTask("pretty-print", {
+ url: "(scratchpad)",
+ indent: tabsize,
+ source: uglyText
+ }).then(data => {
+ this.editor.setText(data.code);
+ }).then(null, error => {
+ this.writeAsErrorComment({ exception: error });
+ throw error;
+ });
+ },
+
+ /**
+ * Parse the text and return an AST. If we can't parse it, write an error
+ * comment and return false.
+ */
+ _parseText: function SP__parseText(aText) {
+ try {
+ return Reflect.parse(aText);
+ } catch (e) {
+ this.writeAsErrorComment({ exception: DevToolsUtils.safeErrorString(e) });
+ return false;
+ }
+ },
+
+ /**
+ * Determine if the given AST node location contains the given cursor
+ * position.
+ *
+ * @returns Boolean
+ */
+ _containsCursor: function (aLoc, aCursorPos) {
+ // Our line numbers are 1-based, while CodeMirror's are 0-based.
+ const lineNumber = aCursorPos.line + 1;
+ const columnNumber = aCursorPos.ch;
+
+ if (aLoc.start.line <= lineNumber && aLoc.end.line >= lineNumber) {
+ if (aLoc.start.line === aLoc.end.line) {
+ return aLoc.start.column <= columnNumber
+ && aLoc.end.column >= columnNumber;
+ }
+
+ if (aLoc.start.line == lineNumber) {
+ return columnNumber >= aLoc.start.column;
+ }
+
+ if (aLoc.end.line == lineNumber) {
+ return columnNumber <= aLoc.end.column;
+ }
+
+ return true;
+ }
+
+ return false;
+ },
+
+ /**
+ * Find the top level function AST node that the cursor is within.
+ *
+ * @returns Object|null
+ */
+ _findTopLevelFunction: function SP__findTopLevelFunction(aAst, aCursorPos) {
+ for (let statement of aAst.body) {
+ switch (statement.type) {
+ case "FunctionDeclaration":
+ if (this._containsCursor(statement.loc, aCursorPos)) {
+ return statement;
+ }
+ break;
+
+ case "VariableDeclaration":
+ for (let decl of statement.declarations) {
+ if (!decl.init) {
+ continue;
+ }
+ if ((decl.init.type == "FunctionExpression"
+ || decl.init.type == "ArrowFunctionExpression")
+ && this._containsCursor(decl.loc, aCursorPos)) {
+ return decl;
+ }
+ }
+ break;
+ }
+ }
+
+ return null;
+ },
+
+ /**
+ * Get the source text associated with the given function statement.
+ *
+ * @param Object aFunction
+ * @param String aFullText
+ * @returns String
+ */
+ _getFunctionText: function SP__getFunctionText(aFunction, aFullText) {
+ let functionText = "";
+ // Initially set to 0, but incremented first thing in the loop below because
+ // line numbers are 1 based, not 0 based.
+ let lineNumber = 0;
+ const { start, end } = aFunction.loc;
+ const singleLine = start.line === end.line;
+
+ for (let line of aFullText.split(/\n/g)) {
+ lineNumber++;
+
+ if (singleLine && start.line === lineNumber) {
+ functionText = line.slice(start.column, end.column);
+ break;
+ }
+
+ if (start.line === lineNumber) {
+ functionText += line.slice(start.column) + "\n";
+ continue;
+ }
+
+ if (end.line === lineNumber) {
+ functionText += line.slice(0, end.column);
+ break;
+ }
+
+ if (start.line < lineNumber && end.line > lineNumber) {
+ functionText += line + "\n";
+ }
+ }
+
+ return functionText;
+ },
+
+ /**
+ * Evaluate the top level function that the cursor is resting in.
+ *
+ * @returns Promise [text, error, result]
+ */
+ evalTopLevelFunction: function SP_evalTopLevelFunction() {
+ const text = this.getText();
+ const ast = this._parseText(text);
+ if (!ast) {
+ return promise.resolve([text, undefined, undefined]);
+ }
+
+ const cursorPos = this.editor.getCursor();
+ const funcStatement = this._findTopLevelFunction(ast, cursorPos);
+ if (!funcStatement) {
+ return promise.resolve([text, undefined, undefined]);
+ }
+
+ let functionText = this._getFunctionText(funcStatement, text);
+
+ // TODO: This is a work around for bug 940086. It should be removed when
+ // that is fixed.
+ if (funcStatement.type == "FunctionDeclaration"
+ && !functionText.startsWith("function ")) {
+ functionText = "function " + functionText;
+ funcStatement.loc.start.column -= 9;
+ }
+
+ // The decrement by one is because our line numbers are 1-based, while
+ // CodeMirror's are 0-based.
+ const from = {
+ line: funcStatement.loc.start.line - 1,
+ ch: funcStatement.loc.start.column
+ };
+ const to = {
+ line: funcStatement.loc.end.line - 1,
+ ch: funcStatement.loc.end.column
+ };
+
+ const marker = this.editor.markText(from, to, "eval-text");
+ setTimeout(() => marker.clear(), EVAL_FUNCTION_TIMEOUT);
+
+ return this.evaluate(functionText);
+ },
+
+ /**
+ * Writes out a primitive value as a comment. This handles values which are
+ * to be printed directly (number, string) as well as grips to values
+ * (null, undefined, longString).
+ *
+ * @param any aValue
+ * The value to print.
+ * @return Promise
+ * The promise that resolves after the value has been printed.
+ */
+ _writePrimitiveAsComment: function SP__writePrimitiveAsComment(aValue)
+ {
+ let deferred = promise.defer();
+
+ if (aValue.type == "longString") {
+ let client = this.webConsoleClient;
+ client.longString(aValue).substring(0, aValue.length, aResponse => {
+ if (aResponse.error) {
+ reportError("display", aResponse);
+ deferred.reject(aResponse);
+ }
+ else {
+ deferred.resolve(aResponse.substring);
+ }
+ });
+ }
+ else {
+ deferred.resolve(aValue.type || aValue);
+ }
+
+ return deferred.promise.then(aComment => {
+ this.writeAsComment(aComment);
+ });
+ },
+
+ /**
+ * Write out a value at the next line from the current insertion point.
+ * The comment block will always be preceded by a newline character.
+ * @param object aValue
+ * The Object to write out as a string
+ */
+ writeAsComment: function SP_writeAsComment(aValue)
+ {
+ let value = "\n/*\n" + aValue + "\n*/";
+
+ if (this.editor.somethingSelected()) {
+ let from = this.editor.getCursor("end");
+ this.editor.replaceSelection(this.editor.getSelection() + value);
+ let to = this.editor.getPosition(this.editor.getOffset(from) + value.length);
+ this.editor.setSelection(from, to);
+ return;
+ }
+
+ let text = this.editor.getText();
+ this.editor.setText(text + value);
+
+ let [ from, to ] = this.editor.getPosition(text.length, (text + value).length);
+ this.editor.setSelection(from, to);
+ },
+
+ /**
+ * Write out an error at the current insertion point as a block comment
+ * @param object aValue
+ * The error object to write out the message and stack trace. It must
+ * contain an |exception| property with the actual error thrown, but it
+ * will often be the entire response of an evaluateJS request.
+ * @return Promise
+ * The promise that indicates when writing the comment completes.
+ */
+ writeAsErrorComment: function SP_writeAsErrorComment(aError)
+ {
+ let deferred = promise.defer();
+
+ if (VariablesView.isPrimitive({ value: aError.exception })) {
+ let error = aError.exception;
+ let type = error.type;
+ if (type == "undefined" ||
+ type == "null" ||
+ type == "Infinity" ||
+ type == "-Infinity" ||
+ type == "NaN" ||
+ type == "-0") {
+ deferred.resolve(type);
+ }
+ else if (type == "longString") {
+ deferred.resolve(error.initial + "\u2026");
+ }
+ else {
+ deferred.resolve(error);
+ }
+ } else if ("preview" in aError.exception) {
+ let error = aError.exception;
+ let stack = this._constructErrorStack(error.preview);
+ if (typeof aError.exceptionMessage == "string") {
+ deferred.resolve(aError.exceptionMessage + stack);
+ } else {
+ deferred.resolve(stack);
+ }
+ } else {
+ // If there is no preview information, we need to ask the server for more.
+ let objectClient = new ObjectClient(this.debuggerClient, aError.exception);
+ objectClient.getPrototypeAndProperties(aResponse => {
+ if (aResponse.error) {
+ deferred.reject(aResponse);
+ return;
+ }
+
+ let { ownProperties, safeGetterValues } = aResponse;
+ let error = Object.create(null);
+
+ // Combine all the property descriptor/getter values into one object.
+ for (let key of Object.keys(safeGetterValues)) {
+ error[key] = safeGetterValues[key].getterValue;
+ }
+
+ for (let key of Object.keys(ownProperties)) {
+ error[key] = ownProperties[key].value;
+ }
+
+ let stack = this._constructErrorStack(error);
+
+ if (typeof error.message == "string") {
+ deferred.resolve(error.message + stack);
+ }
+ else {
+ objectClient.getDisplayString(aResponse => {
+ if (aResponse.error) {
+ deferred.reject(aResponse);
+ }
+ else if (typeof aResponse.displayString == "string") {
+ deferred.resolve(aResponse.displayString + stack);
+ }
+ else {
+ deferred.resolve(stack);
+ }
+ });
+ }
+ });
+ }
+
+ return deferred.promise.then(aMessage => {
+ console.error(aMessage);
+ this.writeAsComment("Exception: " + aMessage);
+ });
+ },
+
+ /**
+ * Assembles the best possible stack from the properties of the provided
+ * error.
+ */
+ _constructErrorStack(error) {
+ let stack;
+ if (typeof error.stack == "string" && error.stack) {
+ stack = error.stack;
+ } else if (typeof error.fileName == "string") {
+ stack = "@" + error.fileName;
+ if (typeof error.lineNumber == "number") {
+ stack += ":" + error.lineNumber;
+ }
+ } else if (typeof error.filename == "string") {
+ stack = "@" + error.filename;
+ if (typeof error.lineNumber == "number") {
+ stack += ":" + error.lineNumber;
+ if (typeof error.columnNumber == "number") {
+ stack += ":" + error.columnNumber;
+ }
+ }
+ } else if (typeof error.lineNumber == "number") {
+ stack = "@" + error.lineNumber;
+ if (typeof error.columnNumber == "number") {
+ stack += ":" + error.columnNumber;
+ }
+ }
+
+ return stack ? "\n" + stack.replace(/\n$/, "") : "";
+ },
+
+ // Menu Operations
+
+ /**
+ * Open a new Scratchpad window.
+ *
+ * @return nsIWindow
+ */
+ openScratchpad: function SP_openScratchpad()
+ {
+ return ScratchpadManager.openScratchpad();
+ },
+
+ /**
+ * Export the textbox content to a file.
+ *
+ * @param nsILocalFile aFile
+ * The file where you want to save the textbox content.
+ * @param boolean aNoConfirmation
+ * If the file already exists, ask for confirmation?
+ * @param boolean aSilentError
+ * True if you do not want to display an error when file save fails,
+ * false otherwise.
+ * @param function aCallback
+ * Optional function you want to call when file save completes. It will
+ * get the following arguments:
+ * 1) the nsresult status code for the export operation.
+ */
+ exportToFile: function SP_exportToFile(aFile, aNoConfirmation, aSilentError,
+ aCallback)
+ {
+ if (!aNoConfirmation && aFile.exists() &&
+ !window.confirm(this.strings.
+ GetStringFromName("export.fileOverwriteConfirmation"))) {
+ return;
+ }
+
+ let encoder = new TextEncoder();
+ let buffer = encoder.encode(this.getText());
+ let writePromise = OS.File.writeAtomic(aFile.path, buffer, {tmpPath: aFile.path + ".tmp"});
+ writePromise.then(value => {
+ if (aCallback) {
+ aCallback.call(this, Components.results.NS_OK);
+ }
+ }, reason => {
+ if (!aSilentError) {
+ window.alert(this.strings.GetStringFromName("saveFile.failed"));
+ }
+ if (aCallback) {
+ aCallback.call(this, Components.results.NS_ERROR_UNEXPECTED);
+ }
+ });
+
+ },
+
+ /**
+ * Get a list of applicable charsets.
+ * The best charset, defaulting to "UTF-8"
+ *
+ * @param string aBestCharset
+ * @return array of strings
+ */
+ _getApplicableCharsets: function SP__getApplicableCharsets(aBestCharset = "UTF-8") {
+ let charsets = Services.prefs.getCharPref(
+ FALLBACK_CHARSET_LIST).split(",").filter(function (value) {
+ return value.length;
+ });
+ charsets.unshift(aBestCharset);
+ return charsets;
+ },
+
+ /**
+ * Get content converted to unicode, using a list of input charset to try.
+ *
+ * @param string aContent
+ * @param array of string aCharsetArray
+ * @return string
+ */
+ _getUnicodeContent: function SP__getUnicodeContent(aContent, aCharsetArray) {
+ let content = null,
+ converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].createInstance(Ci.nsIScriptableUnicodeConverter),
+ success = aCharsetArray.some(charset => {
+ try {
+ converter.charset = charset;
+ content = converter.ConvertToUnicode(aContent);
+ return true;
+ } catch (e) {
+ this.notificationBox.appendNotification(
+ this.strings.formatStringFromName("importFromFile.convert.failed",
+ [ charset ], 1),
+ "file-import-convert-failed",
+ null,
+ this.notificationBox.PRIORITY_WARNING_HIGH,
+ null);
+ }
+ });
+ return content;
+ },
+
+ /**
+ * Read the content of a file and put it into the textbox.
+ *
+ * @param nsILocalFile aFile
+ * The file you want to save the textbox content into.
+ * @param boolean aSilentError
+ * True if you do not want to display an error when file load fails,
+ * false otherwise.
+ * @param function aCallback
+ * Optional function you want to call when file load completes. It will
+ * get the following arguments:
+ * 1) the nsresult status code for the import operation.
+ * 2) the data that was read from the file, if any.
+ */
+ importFromFile: function SP_importFromFile(aFile, aSilentError, aCallback)
+ {
+ // Prevent file type detection.
+ let channel = NetUtil.newChannel({
+ uri: NetUtil.newURI(aFile),
+ loadingNode: window.document,
+ securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_INHERITS,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER});
+ channel.contentType = "application/javascript";
+
+ this.notificationBox.removeAllNotifications(false);
+
+ NetUtil.asyncFetch(channel, (aInputStream, aStatus) => {
+ let content = null;
+
+ if (Components.isSuccessCode(aStatus)) {
+ let charsets = this._getApplicableCharsets();
+ content = NetUtil.readInputStreamToString(aInputStream,
+ aInputStream.available());
+ content = this._getUnicodeContent(content, charsets);
+ if (!content) {
+ let message = this.strings.formatStringFromName(
+ "importFromFile.convert.failed",
+ [ charsets.join(", ") ],
+ 1);
+ this.notificationBox.appendNotification(
+ message,
+ "file-import-convert-failed",
+ null,
+ this.notificationBox.PRIORITY_CRITICAL_MEDIUM,
+ null);
+ if (aCallback) {
+ aCallback.call(this, aStatus, content);
+ }
+ return;
+ }
+ // Check to see if the first line is a mode-line comment.
+ let line = content.split("\n")[0];
+ let modeline = this._scanModeLine(line);
+ let chrome = Services.prefs.getBoolPref(DEVTOOLS_CHROME_ENABLED);
+
+ if (chrome && modeline["-sp-context"] === "browser") {
+ this.setBrowserContext();
+ }
+
+ this.editor.setText(content);
+ this.editor.clearHistory();
+ this.dirty = false;
+ document.getElementById("sp-cmd-revert").setAttribute("disabled", true);
+ }
+ else if (!aSilentError) {
+ window.alert(this.strings.GetStringFromName("openFile.failed"));
+ }
+ this.setFilename(aFile.path);
+ this.setRecentFile(aFile);
+ if (aCallback) {
+ aCallback.call(this, aStatus, content);
+ }
+ });
+ },
+
+ /**
+ * Open a file to edit in the Scratchpad.
+ *
+ * @param integer aIndex
+ * Optional integer: clicked menuitem in the 'Open Recent'-menu.
+ */
+ openFile: function SP_openFile(aIndex)
+ {
+ let promptCallback = aFile => {
+ this.promptSave((aCloseFile, aSaved, aStatus) => {
+ let shouldOpen = aCloseFile;
+ if (aSaved && !Components.isSuccessCode(aStatus)) {
+ shouldOpen = false;
+ }
+
+ if (shouldOpen) {
+ let file;
+ if (aFile) {
+ file = aFile;
+ } else {
+ file = Components.classes["@mozilla.org/file/local;1"].
+ createInstance(Components.interfaces.nsILocalFile);
+ let filePath = this.getRecentFiles()[aIndex];
+ file.initWithPath(filePath);
+ }
+
+ if (!file.exists()) {
+ this.notificationBox.appendNotification(
+ this.strings.GetStringFromName("fileNoLongerExists.notification"),
+ "file-no-longer-exists",
+ null,
+ this.notificationBox.PRIORITY_WARNING_HIGH,
+ null);
+
+ this.clearFiles(aIndex, 1);
+ return;
+ }
+
+ this.importFromFile(file, false);
+ }
+ });
+ };
+
+ if (aIndex > -1) {
+ promptCallback();
+ } else {
+ let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ fp.init(window, this.strings.GetStringFromName("openFile.title"),
+ Ci.nsIFilePicker.modeOpen);
+ fp.defaultString = "";
+ fp.appendFilter("JavaScript Files", "*.js; *.jsm; *.json");
+ fp.appendFilter("All Files", "*.*");
+ fp.open(aResult => {
+ if (aResult != Ci.nsIFilePicker.returnCancel) {
+ promptCallback(fp.file);
+ }
+ });
+ }
+ },
+
+ /**
+ * Get recent files.
+ *
+ * @return Array
+ * File paths.
+ */
+ getRecentFiles: function SP_getRecentFiles()
+ {
+ let branch = Services.prefs.getBranch("devtools.scratchpad.");
+ let filePaths = [];
+
+ // WARNING: Do not use getCharPref here, it doesn't play nicely with
+ // Unicode strings.
+
+ if (branch.prefHasUserValue("recentFilePaths")) {
+ let data = branch.getComplexValue("recentFilePaths",
+ Ci.nsISupportsString).data;
+ filePaths = JSON.parse(data);
+ }
+
+ return filePaths;
+ },
+
+ /**
+ * Save a recent file in a JSON parsable string.
+ *
+ * @param nsILocalFile aFile
+ * The nsILocalFile we want to save as a recent file.
+ */
+ setRecentFile: function SP_setRecentFile(aFile)
+ {
+ let maxRecent = Services.prefs.getIntPref(PREF_RECENT_FILES_MAX);
+ if (maxRecent < 1) {
+ return;
+ }
+
+ let filePaths = this.getRecentFiles();
+ let filesCount = filePaths.length;
+ let pathIndex = filePaths.indexOf(aFile.path);
+
+ // We are already storing this file in the list of recent files.
+ if (pathIndex > -1) {
+ // If it's already the most recent file, we don't have to do anything.
+ if (pathIndex === (filesCount - 1)) {
+ // Updating the menu to clear the disabled state from the wrong menuitem
+ // in rare cases when two or more Scratchpad windows are open and the
+ // same file has been opened in two or more windows.
+ this.populateRecentFilesMenu();
+ return;
+ }
+
+ // It is not the most recent file. Remove it from the list, we add it as
+ // the most recent farther down.
+ filePaths.splice(pathIndex, 1);
+ }
+ // If we are not storing the file and the 'recent files'-list is full,
+ // remove the oldest file from the list.
+ else if (filesCount === maxRecent) {
+ filePaths.shift();
+ }
+
+ filePaths.push(aFile.path);
+
+ // WARNING: Do not use setCharPref here, it doesn't play nicely with
+ // Unicode strings.
+
+ let str = Cc["@mozilla.org/supports-string;1"]
+ .createInstance(Ci.nsISupportsString);
+ str.data = JSON.stringify(filePaths);
+
+ let branch = Services.prefs.getBranch("devtools.scratchpad.");
+ branch.setComplexValue("recentFilePaths",
+ Ci.nsISupportsString, str);
+ },
+
+ /**
+ * Populates the 'Open Recent'-menu.
+ */
+ populateRecentFilesMenu: function SP_populateRecentFilesMenu()
+ {
+ let maxRecent = Services.prefs.getIntPref(PREF_RECENT_FILES_MAX);
+ let recentFilesMenu = document.getElementById("sp-open_recent-menu");
+
+ if (maxRecent < 1) {
+ recentFilesMenu.setAttribute("hidden", true);
+ return;
+ }
+
+ let recentFilesPopup = recentFilesMenu.firstChild;
+ let filePaths = this.getRecentFiles();
+ let filename = this.getState().filename;
+
+ recentFilesMenu.setAttribute("disabled", true);
+ while (recentFilesPopup.hasChildNodes()) {
+ recentFilesPopup.removeChild(recentFilesPopup.firstChild);
+ }
+
+ if (filePaths.length > 0) {
+ recentFilesMenu.removeAttribute("disabled");
+
+ // Print out menuitems with the most recent file first.
+ for (let i = filePaths.length - 1; i >= 0; --i) {
+ let menuitem = document.createElement("menuitem");
+ menuitem.setAttribute("type", "radio");
+ menuitem.setAttribute("label", filePaths[i]);
+
+ if (filePaths[i] === filename) {
+ menuitem.setAttribute("checked", true);
+ menuitem.setAttribute("disabled", true);
+ }
+
+ menuitem.addEventListener("command", Scratchpad.openFile.bind(Scratchpad, i));
+ recentFilesPopup.appendChild(menuitem);
+ }
+
+ recentFilesPopup.appendChild(document.createElement("menuseparator"));
+ let clearItems = document.createElement("menuitem");
+ clearItems.setAttribute("id", "sp-menu-clear_recent");
+ clearItems.setAttribute("label",
+ this.strings.
+ GetStringFromName("clearRecentMenuItems.label"));
+ clearItems.setAttribute("command", "sp-cmd-clearRecentFiles");
+ recentFilesPopup.appendChild(clearItems);
+ }
+ },
+
+ /**
+ * Clear a range of files from the list.
+ *
+ * @param integer aIndex
+ * Index of file in menu to remove.
+ * @param integer aLength
+ * Number of files from the index 'aIndex' to remove.
+ */
+ clearFiles: function SP_clearFile(aIndex, aLength)
+ {
+ let filePaths = this.getRecentFiles();
+ filePaths.splice(aIndex, aLength);
+
+ // WARNING: Do not use setCharPref here, it doesn't play nicely with
+ // Unicode strings.
+
+ let str = Cc["@mozilla.org/supports-string;1"]
+ .createInstance(Ci.nsISupportsString);
+ str.data = JSON.stringify(filePaths);
+
+ let branch = Services.prefs.getBranch("devtools.scratchpad.");
+ branch.setComplexValue("recentFilePaths",
+ Ci.nsISupportsString, str);
+ },
+
+ /**
+ * Clear all recent files.
+ */
+ clearRecentFiles: function SP_clearRecentFiles()
+ {
+ Services.prefs.clearUserPref("devtools.scratchpad.recentFilePaths");
+ },
+
+ /**
+ * Handle changes to the 'PREF_RECENT_FILES_MAX'-preference.
+ */
+ handleRecentFileMaxChange: function SP_handleRecentFileMaxChange()
+ {
+ let maxRecent = Services.prefs.getIntPref(PREF_RECENT_FILES_MAX);
+ let menu = document.getElementById("sp-open_recent-menu");
+
+ // Hide the menu if the 'PREF_RECENT_FILES_MAX'-pref is set to zero or less.
+ if (maxRecent < 1) {
+ menu.setAttribute("hidden", true);
+ } else {
+ if (menu.hasAttribute("hidden")) {
+ if (!menu.firstChild.hasChildNodes()) {
+ this.populateRecentFilesMenu();
+ }
+
+ menu.removeAttribute("hidden");
+ }
+
+ let filePaths = this.getRecentFiles();
+ if (maxRecent < filePaths.length) {
+ let diff = filePaths.length - maxRecent;
+ this.clearFiles(0, diff);
+ }
+ }
+ },
+ /**
+ * Save the textbox content to the currently open file.
+ *
+ * @param function aCallback
+ * Optional function you want to call when file is saved
+ */
+ saveFile: function SP_saveFile(aCallback)
+ {
+ if (!this.filename) {
+ return this.saveFileAs(aCallback);
+ }
+
+ let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
+ file.initWithPath(this.filename);
+
+ this.exportToFile(file, true, false, aStatus => {
+ if (Components.isSuccessCode(aStatus)) {
+ this.dirty = false;
+ document.getElementById("sp-cmd-revert").setAttribute("disabled", true);
+ this.setRecentFile(file);
+ }
+ if (aCallback) {
+ aCallback(aStatus);
+ }
+ });
+ },
+
+ /**
+ * Save the textbox content to a new file.
+ *
+ * @param function aCallback
+ * Optional function you want to call when file is saved
+ */
+ saveFileAs: function SP_saveFileAs(aCallback)
+ {
+ let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ let fpCallback = aResult => {
+ if (aResult != Ci.nsIFilePicker.returnCancel) {
+ this.setFilename(fp.file.path);
+ this.exportToFile(fp.file, true, false, aStatus => {
+ if (Components.isSuccessCode(aStatus)) {
+ this.dirty = false;
+ this.setRecentFile(fp.file);
+ }
+ if (aCallback) {
+ aCallback(aStatus);
+ }
+ });
+ }
+ };
+
+ fp.init(window, this.strings.GetStringFromName("saveFileAs"),
+ Ci.nsIFilePicker.modeSave);
+ fp.defaultString = "scratchpad.js";
+ fp.appendFilter("JavaScript Files", "*.js; *.jsm; *.json");
+ fp.appendFilter("All Files", "*.*");
+ fp.open(fpCallback);
+ },
+
+ /**
+ * Restore content from saved version of current file.
+ *
+ * @param function aCallback
+ * Optional function you want to call when file is saved
+ */
+ revertFile: function SP_revertFile(aCallback)
+ {
+ let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
+ file.initWithPath(this.filename);
+
+ if (!file.exists()) {
+ return;
+ }
+
+ this.importFromFile(file, false, (aStatus, aContent) => {
+ if (aCallback) {
+ aCallback(aStatus);
+ }
+ });
+ },
+
+ /**
+ * Prompt to revert scratchpad if it has unsaved changes.
+ *
+ * @param function aCallback
+ * Optional function you want to call when file is saved. The callback
+ * receives three arguments:
+ * - aRevert (boolean) - tells if the file has been reverted.
+ * - status (number) - the file revert status result (if the file was
+ * saved).
+ */
+ promptRevert: function SP_promptRervert(aCallback)
+ {
+ if (this.filename) {
+ let ps = Services.prompt;
+ let flags = ps.BUTTON_POS_0 * ps.BUTTON_TITLE_REVERT +
+ ps.BUTTON_POS_1 * ps.BUTTON_TITLE_CANCEL;
+
+ let button = ps.confirmEx(window,
+ this.strings.GetStringFromName("confirmRevert.title"),
+ this.strings.GetStringFromName("confirmRevert"),
+ flags, null, null, null, null, {});
+ if (button == BUTTON_POSITION_CANCEL) {
+ if (aCallback) {
+ aCallback(false);
+ }
+
+ return;
+ }
+ if (button == BUTTON_POSITION_REVERT) {
+ this.revertFile(aStatus => {
+ if (aCallback) {
+ aCallback(true, aStatus);
+ }
+ });
+
+ return;
+ }
+ }
+ if (aCallback) {
+ aCallback(false);
+ }
+ },
+
+ /**
+ * Open the Error Console.
+ */
+ openErrorConsole: function SP_openErrorConsole()
+ {
+ HUDService.toggleBrowserConsole();
+ },
+
+ /**
+ * Open the Web Console.
+ */
+ openWebConsole: function SP_openWebConsole()
+ {
+ let target = TargetFactory.forTab(this.gBrowser.selectedTab);
+ gDevTools.showToolbox(target, "webconsole");
+ this.browserWindow.focus();
+ },
+
+ /**
+ * Set the current execution context to be the active tab content window.
+ */
+ setContentContext: function SP_setContentContext()
+ {
+ if (this.executionContext == SCRATCHPAD_CONTEXT_CONTENT) {
+ return;
+ }
+
+ let content = document.getElementById("sp-menu-content");
+ document.getElementById("sp-menu-browser").removeAttribute("checked");
+ document.getElementById("sp-cmd-reloadAndRun").removeAttribute("disabled");
+ content.setAttribute("checked", true);
+ this.executionContext = SCRATCHPAD_CONTEXT_CONTENT;
+ this.notificationBox.removeAllNotifications(false);
+ },
+
+ /**
+ * Set the current execution context to be the most recent chrome window.
+ */
+ setBrowserContext: function SP_setBrowserContext()
+ {
+ if (this.executionContext == SCRATCHPAD_CONTEXT_BROWSER) {
+ return;
+ }
+
+ let browser = document.getElementById("sp-menu-browser");
+ let reloadAndRun = document.getElementById("sp-cmd-reloadAndRun");
+
+ document.getElementById("sp-menu-content").removeAttribute("checked");
+ reloadAndRun.setAttribute("disabled", true);
+ browser.setAttribute("checked", true);
+
+ this.executionContext = SCRATCHPAD_CONTEXT_BROWSER;
+ this.notificationBox.appendNotification(
+ this.strings.GetStringFromName("browserContext.notification"),
+ SCRATCHPAD_CONTEXT_BROWSER,
+ null,
+ this.notificationBox.PRIORITY_WARNING_HIGH,
+ null);
+ },
+
+ /**
+ * Gets the ID of the inner window of the given DOM window object.
+ *
+ * @param nsIDOMWindow aWindow
+ * @return integer
+ * the inner window ID
+ */
+ getInnerWindowId: function SP_getInnerWindowId(aWindow)
+ {
+ return aWindow.QueryInterface(Ci.nsIInterfaceRequestor).
+ getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID;
+ },
+
+ updateStatusBar: function SP_updateStatusBar(aEventType)
+ {
+ var statusBarField = document.getElementById("statusbar-line-col");
+ let { line, ch } = this.editor.getCursor();
+ statusBarField.textContent = this.strings.formatStringFromName(
+ "scratchpad.statusBarLineCol", [ line + 1, ch + 1], 2);
+ },
+
+ /**
+ * The Scratchpad window load event handler. This method
+ * initializes the Scratchpad window and source editor.
+ *
+ * @param nsIDOMEvent aEvent
+ */
+ onLoad: function SP_onLoad(aEvent)
+ {
+ if (aEvent.target != document) {
+ return;
+ }
+
+ let chrome = Services.prefs.getBoolPref(DEVTOOLS_CHROME_ENABLED);
+ if (chrome) {
+ let environmentMenu = document.getElementById("sp-environment-menu");
+ let errorConsoleCommand = document.getElementById("sp-cmd-errorConsole");
+ let chromeContextCommand = document.getElementById("sp-cmd-browserContext");
+ environmentMenu.removeAttribute("hidden");
+ chromeContextCommand.removeAttribute("disabled");
+ errorConsoleCommand.removeAttribute("disabled");
+ }
+
+ let initialText = this.strings.formatStringFromName(
+ "scratchpadIntro1",
+ [ShortcutUtils.prettifyShortcut(document.getElementById("sp-key-run"), true),
+ ShortcutUtils.prettifyShortcut(document.getElementById("sp-key-inspect"), true),
+ ShortcutUtils.prettifyShortcut(document.getElementById("sp-key-display"), true)],
+ 3);
+
+ let args = window.arguments;
+ let state = null;
+
+ if (args && args[0] instanceof Ci.nsIDialogParamBlock) {
+ args = args[0];
+ this._instanceId = args.GetString(0);
+
+ state = args.GetString(1) || null;
+ if (state) {
+ state = JSON.parse(state);
+ this.setState(state);
+ if ("text" in state) {
+ initialText = state.text;
+ }
+ }
+ } else {
+ this._instanceId = ScratchpadManager.createUid();
+ }
+
+ let config = {
+ mode: Editor.modes.js,
+ value: initialText,
+ lineNumbers: Services.prefs.getBoolPref(SHOW_LINE_NUMBERS),
+ contextMenu: "scratchpad-text-popup",
+ showTrailingSpace: Services.prefs.getBoolPref(SHOW_TRAILING_SPACE),
+ autocomplete: Services.prefs.getBoolPref(ENABLE_AUTOCOMPLETION),
+ lineWrapping: Services.prefs.getBoolPref(WRAP_TEXT),
+ };
+
+ this.editor = new Editor(config);
+ let editorElement = document.querySelector("#scratchpad-editor");
+ this.editor.appendTo(editorElement).then(() => {
+ var lines = initialText.split("\n");
+
+ this.editor.setFontSize(Services.prefs.getIntPref(EDITOR_FONT_SIZE));
+
+ this.editor.on("change", this._onChanged);
+ // Keep a reference to the bound version for use in onUnload.
+ this.updateStatusBar = Scratchpad.updateStatusBar.bind(this);
+ this.editor.on("cursorActivity", this.updateStatusBar);
+ let okstring = this.strings.GetStringFromName("selfxss.okstring");
+ let msg = this.strings.formatStringFromName("selfxss.msg", [okstring], 1);
+ this._onPaste = WebConsoleUtils.pasteHandlerGen(this.editor.container.contentDocument.body,
+ document.querySelector("#scratchpad-notificationbox"),
+ msg, okstring);
+ editorElement.addEventListener("paste", this._onPaste, true);
+ editorElement.addEventListener("drop", this._onPaste);
+ this.editor.on("saveRequested", () => this.saveFile());
+ this.editor.focus();
+ this.editor.setCursor({ line: lines.length, ch: lines.pop().length });
+
+ if (state)
+ this.dirty = !state.saved;
+
+ this.initialized = true;
+ this._triggerObservers("Ready");
+ this.populateRecentFilesMenu();
+ PreferenceObserver.init();
+ CloseObserver.init();
+ }).then(null, (err) => console.error(err));
+ this._setupCommandListeners();
+ this._updateViewMenuItems();
+ this._setupPopupShowingListeners();
+ },
+
+ /**
+ * The Source Editor "change" event handler. This function updates the
+ * Scratchpad window title to show an asterisk when there are unsaved changes.
+ *
+ * @private
+ */
+ _onChanged: function SP__onChanged()
+ {
+ Scratchpad._updateTitle();
+
+ if (Scratchpad.filename) {
+ if (Scratchpad.dirty)
+ document.getElementById("sp-cmd-revert").removeAttribute("disabled");
+ else
+ document.getElementById("sp-cmd-revert").setAttribute("disabled", true);
+ }
+ },
+
+ /**
+ * Undo the last action of the user.
+ */
+ undo: function SP_undo()
+ {
+ this.editor.undo();
+ },
+
+ /**
+ * Redo the previously undone action.
+ */
+ redo: function SP_redo()
+ {
+ this.editor.redo();
+ },
+
+ /**
+ * The Scratchpad window unload event handler. This method unloads/destroys
+ * the source editor.
+ *
+ * @param nsIDOMEvent aEvent
+ */
+ onUnload: function SP_onUnload(aEvent)
+ {
+ if (aEvent.target != document) {
+ return;
+ }
+
+ // This event is created only after user uses 'reload and run' feature.
+ if (this._reloadAndRunEvent && this.gBrowser) {
+ this.gBrowser.selectedBrowser.removeEventListener("load",
+ this._reloadAndRunEvent, true);
+ }
+
+ PreferenceObserver.uninit();
+ CloseObserver.uninit();
+ if (this._onPaste) {
+ let editorElement = document.querySelector("#scratchpad-editor");
+ editorElement.removeEventListener("paste", this._onPaste, true);
+ editorElement.removeEventListener("drop", this._onPaste);
+ this._onPaste = null;
+ }
+ this.editor.off("change", this._onChanged);
+ this.editor.off("cursorActivity", this.updateStatusBar);
+ this.editor.destroy();
+ this.editor = null;
+
+ if (this._sidebar) {
+ this._sidebar.destroy();
+ this._sidebar = null;
+ }
+
+ if (this._prettyPrintWorker) {
+ this._prettyPrintWorker.destroy();
+ this._prettyPrintWorker = null;
+ }
+
+ scratchpadTargets = null;
+ this.webConsoleClient = null;
+ this.debuggerClient = null;
+ this.initialized = false;
+ },
+
+ /**
+ * Prompt to save scratchpad if it has unsaved changes.
+ *
+ * @param function aCallback
+ * Optional function you want to call when file is saved. The callback
+ * receives three arguments:
+ * - toClose (boolean) - tells if the window should be closed.
+ * - saved (boolen) - tells if the file has been saved.
+ * - status (number) - the file save status result (if the file was
+ * saved).
+ * @return boolean
+ * Whether the window should be closed
+ */
+ promptSave: function SP_promptSave(aCallback)
+ {
+ if (this.dirty) {
+ let ps = Services.prompt;
+ let flags = ps.BUTTON_POS_0 * ps.BUTTON_TITLE_SAVE +
+ ps.BUTTON_POS_1 * ps.BUTTON_TITLE_CANCEL +
+ ps.BUTTON_POS_2 * ps.BUTTON_TITLE_DONT_SAVE;
+
+ let button = ps.confirmEx(window,
+ this.strings.GetStringFromName("confirmClose.title"),
+ this.strings.GetStringFromName("confirmClose"),
+ flags, null, null, null, null, {});
+
+ if (button == BUTTON_POSITION_CANCEL) {
+ if (aCallback) {
+ aCallback(false, false);
+ }
+ return false;
+ }
+
+ if (button == BUTTON_POSITION_SAVE) {
+ this.saveFile(aStatus => {
+ if (aCallback) {
+ aCallback(true, true, aStatus);
+ }
+ });
+ return true;
+ }
+ }
+
+ if (aCallback) {
+ aCallback(true, false);
+ }
+ return true;
+ },
+
+ /**
+ * Handler for window close event. Prompts to save scratchpad if
+ * there are unsaved changes.
+ *
+ * @param nsIDOMEvent aEvent
+ * @param function aCallback
+ * Optional function you want to call when file is saved/closed.
+ * Used mainly for tests.
+ */
+ onClose: function SP_onClose(aEvent, aCallback)
+ {
+ aEvent.preventDefault();
+ this.close(aCallback);
+ },
+
+ /**
+ * Close the scratchpad window. Prompts before closing if the scratchpad
+ * has unsaved changes.
+ *
+ * @param function aCallback
+ * Optional function you want to call when file is saved
+ */
+ close: function SP_close(aCallback)
+ {
+ let shouldClose;
+
+ this.promptSave((aShouldClose, aSaved, aStatus) => {
+ shouldClose = aShouldClose;
+ if (aSaved && !Components.isSuccessCode(aStatus)) {
+ shouldClose = false;
+ }
+
+ if (shouldClose) {
+ window.close();
+ }
+
+ if (aCallback) {
+ aCallback(shouldClose);
+ }
+ });
+
+ return shouldClose;
+ },
+
+ /**
+ * Toggle a editor's boolean option.
+ */
+ toggleEditorOption: function SP_toggleEditorOption(optionName, optionPreference)
+ {
+ let newOptionValue = !this.editor.getOption(optionName);
+ this.editor.setOption(optionName, newOptionValue);
+ Services.prefs.setBoolPref(optionPreference, newOptionValue);
+ },
+
+ /**
+ * Increase the editor's font size by 1 px.
+ */
+ increaseFontSize: function SP_increaseFontSize()
+ {
+ let size = this.editor.getFontSize();
+
+ if (size < MAXIMUM_FONT_SIZE) {
+ let newFontSize = size + 1;
+ this.editor.setFontSize(newFontSize);
+ Services.prefs.setIntPref(EDITOR_FONT_SIZE, newFontSize);
+
+ if (newFontSize === MAXIMUM_FONT_SIZE) {
+ document.getElementById("sp-cmd-larger-font").setAttribute("disabled", true);
+ }
+
+ document.getElementById("sp-cmd-smaller-font").removeAttribute("disabled");
+ }
+ },
+
+ /**
+ * Decrease the editor's font size by 1 px.
+ */
+ decreaseFontSize: function SP_decreaseFontSize()
+ {
+ let size = this.editor.getFontSize();
+
+ if (size > MINIMUM_FONT_SIZE) {
+ let newFontSize = size - 1;
+ this.editor.setFontSize(newFontSize);
+ Services.prefs.setIntPref(EDITOR_FONT_SIZE, newFontSize);
+
+ if (newFontSize === MINIMUM_FONT_SIZE) {
+ document.getElementById("sp-cmd-smaller-font").setAttribute("disabled", true);
+ }
+ }
+
+ document.getElementById("sp-cmd-larger-font").removeAttribute("disabled");
+ },
+
+ /**
+ * Restore the editor's original font size.
+ */
+ normalFontSize: function SP_normalFontSize()
+ {
+ this.editor.setFontSize(NORMAL_FONT_SIZE);
+ Services.prefs.setIntPref(EDITOR_FONT_SIZE, NORMAL_FONT_SIZE);
+
+ document.getElementById("sp-cmd-larger-font").removeAttribute("disabled");
+ document.getElementById("sp-cmd-smaller-font").removeAttribute("disabled");
+ },
+
+ _observers: [],
+
+ /**
+ * Add an observer for Scratchpad events.
+ *
+ * The observer implements IScratchpadObserver := {
+ * onReady: Called when the Scratchpad and its Editor are ready.
+ * Arguments: (Scratchpad aScratchpad)
+ * }
+ *
+ * All observer handlers are optional.
+ *
+ * @param IScratchpadObserver aObserver
+ * @see removeObserver
+ */
+ addObserver: function SP_addObserver(aObserver)
+ {
+ this._observers.push(aObserver);
+ },
+
+ /**
+ * Remove an observer for Scratchpad events.
+ *
+ * @param IScratchpadObserver aObserver
+ * @see addObserver
+ */
+ removeObserver: function SP_removeObserver(aObserver)
+ {
+ let index = this._observers.indexOf(aObserver);
+ if (index != -1) {
+ this._observers.splice(index, 1);
+ }
+ },
+
+ /**
+ * Trigger named handlers in Scratchpad observers.
+ *
+ * @param string aName
+ * Name of the handler to trigger.
+ * @param Array aArgs
+ * Optional array of arguments to pass to the observer(s).
+ * @see addObserver
+ */
+ _triggerObservers: function SP_triggerObservers(aName, aArgs)
+ {
+ // insert this Scratchpad instance as the first argument
+ if (!aArgs) {
+ aArgs = [this];
+ } else {
+ aArgs.unshift(this);
+ }
+
+ // trigger all observers that implement this named handler
+ for (let i = 0; i < this._observers.length; ++i) {
+ let observer = this._observers[i];
+ let handler = observer["on" + aName];
+ if (handler) {
+ handler.apply(observer, aArgs);
+ }
+ }
+ },
+
+ /**
+ * Opens the MDN documentation page for Scratchpad.
+ */
+ openDocumentationPage: function SP_openDocumentationPage()
+ {
+ let url = this.strings.GetStringFromName("help.openDocumentationPage");
+ this.browserWindow.openUILinkIn(url,"tab");
+ this.browserWindow.focus();
+ },
+};
+
+
+/**
+ * Represents the DebuggerClient connection to a specific tab as used by the
+ * Scratchpad.
+ *
+ * @param object aTab
+ * The tab to connect to.
+ */
+function ScratchpadTab(aTab)
+{
+ this._tab = aTab;
+}
+
+var scratchpadTargets = new WeakMap();
+
+/**
+ * Returns the object containing the DebuggerClient and WebConsoleClient for a
+ * given tab or window.
+ *
+ * @param object aSubject
+ * The tab or window to obtain the connection for.
+ * @return Promise
+ * The promise for the connection information.
+ */
+ScratchpadTab.consoleFor = function consoleFor(aSubject)
+{
+ if (!scratchpadTargets.has(aSubject)) {
+ scratchpadTargets.set(aSubject, new this(aSubject));
+ }
+ return scratchpadTargets.get(aSubject).connect(aSubject);
+};
+
+
+ScratchpadTab.prototype = {
+ /**
+ * The promise for the connection.
+ */
+ _connector: null,
+
+ /**
+ * Initialize a debugger client and connect it to the debugger server.
+ *
+ * @param object aSubject
+ * The tab or window to obtain the connection for.
+ * @return Promise
+ * The promise for the result of connecting to this tab or window.
+ */
+ connect: function ST_connect(aSubject)
+ {
+ if (this._connector) {
+ return this._connector;
+ }
+
+ let deferred = promise.defer();
+ this._connector = deferred.promise;
+
+ let connectTimer = setTimeout(() => {
+ deferred.reject({
+ error: "timeout",
+ message: Scratchpad.strings.GetStringFromName("connectionTimeout"),
+ });
+ }, REMOTE_TIMEOUT);
+
+ deferred.promise.then(() => clearTimeout(connectTimer));
+
+ this._attach(aSubject).then(aTarget => {
+ let consoleActor = aTarget.form.consoleActor;
+ let client = aTarget.client;
+ client.attachConsole(consoleActor, [], (aResponse, aWebConsoleClient) => {
+ if (aResponse.error) {
+ reportError("attachConsole", aResponse);
+ deferred.reject(aResponse);
+ }
+ else {
+ deferred.resolve({
+ webConsoleClient: aWebConsoleClient,
+ debuggerClient: client
+ });
+ }
+ });
+ });
+
+ return deferred.promise;
+ },
+
+ /**
+ * Attach to this tab.
+ *
+ * @param object aSubject
+ * The tab or window to obtain the connection for.
+ * @return Promise
+ * The promise for the TabTarget for this tab.
+ */
+ _attach: function ST__attach(aSubject)
+ {
+ let target = TargetFactory.forTab(this._tab);
+ target.once("close", () => {
+ if (scratchpadTargets) {
+ scratchpadTargets.delete(aSubject);
+ }
+ });
+ return target.makeRemote().then(() => target);
+ },
+};
+
+
+/**
+ * Represents the DebuggerClient connection to a specific window as used by the
+ * Scratchpad.
+ */
+function ScratchpadWindow() {}
+
+ScratchpadWindow.consoleFor = ScratchpadTab.consoleFor;
+
+ScratchpadWindow.prototype = Heritage.extend(ScratchpadTab.prototype, {
+ /**
+ * Attach to this window.
+ *
+ * @return Promise
+ * The promise for the target for this window.
+ */
+ _attach: function SW__attach()
+ {
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+ DebuggerServer.allowChromeProcess = true;
+
+ let client = new DebuggerClient(DebuggerServer.connectPipe());
+ return client.connect()
+ .then(() => client.getProcess())
+ .then(aResponse => {
+ return { form: aResponse.form, client: client };
+ });
+ }
+});
+
+
+function ScratchpadTarget(aTarget)
+{
+ this._target = aTarget;
+}
+
+ScratchpadTarget.consoleFor = ScratchpadTab.consoleFor;
+
+ScratchpadTarget.prototype = Heritage.extend(ScratchpadTab.prototype, {
+ _attach: function ST__attach()
+ {
+ if (this._target.isRemote) {
+ return promise.resolve(this._target);
+ }
+ return this._target.makeRemote().then(() => this._target);
+ }
+});
+
+
+/**
+ * Encapsulates management of the sidebar containing the VariablesView for
+ * object inspection.
+ */
+function ScratchpadSidebar(aScratchpad)
+{
+ // Make sure to decorate this object. ToolSidebar requires the parent
+ // panel to support event (emit) API.
+ EventEmitter.decorate(this);
+
+ let ToolSidebar = require("devtools/client/framework/sidebar").ToolSidebar;
+ let tabbox = document.querySelector("#scratchpad-sidebar");
+ this._sidebar = new ToolSidebar(tabbox, this, "scratchpad");
+ this._scratchpad = aScratchpad;
+}
+
+ScratchpadSidebar.prototype = {
+ /*
+ * The ToolSidebar for this sidebar.
+ */
+ _sidebar: null,
+
+ /*
+ * The VariablesView for this sidebar.
+ */
+ variablesView: null,
+
+ /*
+ * Whether the sidebar is currently shown.
+ */
+ visible: false,
+
+ /**
+ * Open the sidebar, if not open already, and populate it with the properties
+ * of the given object.
+ *
+ * @param string aString
+ * The string that was evaluated.
+ * @param object aObject
+ * The object to inspect, which is the aEvalString evaluation result.
+ * @return Promise
+ * A promise that will resolve once the sidebar is open.
+ */
+ open: function SS_open(aEvalString, aObject)
+ {
+ this.show();
+
+ let deferred = promise.defer();
+
+ let onTabReady = () => {
+ if (this.variablesView) {
+ this.variablesView.controller.releaseActors();
+ }
+ else {
+ let window = this._sidebar.getWindowForTab("variablesview");
+ let container = window.document.querySelector("#variables");
+
+ this.variablesView = new VariablesView(container, {
+ searchEnabled: true,
+ searchPlaceholder: this._scratchpad.strings
+ .GetStringFromName("propertiesFilterPlaceholder")
+ });
+
+ VariablesViewController.attach(this.variablesView, {
+ getEnvironmentClient: aGrip => {
+ return new EnvironmentClient(this._scratchpad.debuggerClient, aGrip);
+ },
+ getObjectClient: aGrip => {
+ return new ObjectClient(this._scratchpad.debuggerClient, aGrip);
+ },
+ getLongStringClient: aActor => {
+ return this._scratchpad.webConsoleClient.longString(aActor);
+ },
+ releaseActor: aActor => {
+ this._scratchpad.debuggerClient.release(aActor);
+ }
+ });
+ }
+ this._update(aObject).then(() => deferred.resolve());
+ };
+
+ if (this._sidebar.getCurrentTabID() == "variablesview") {
+ onTabReady();
+ }
+ else {
+ this._sidebar.once("variablesview-ready", onTabReady);
+ this._sidebar.addTab("variablesview", VARIABLES_VIEW_URL, {selected: true});
+ }
+
+ return deferred.promise;
+ },
+
+ /**
+ * Show the sidebar.
+ */
+ show: function SS_show()
+ {
+ if (!this.visible) {
+ this.visible = true;
+ this._sidebar.show();
+ }
+ },
+
+ /**
+ * Hide the sidebar.
+ */
+ hide: function SS_hide()
+ {
+ if (this.visible) {
+ this.visible = false;
+ this._sidebar.hide();
+ }
+ },
+
+ /**
+ * Destroy the sidebar.
+ *
+ * @return Promise
+ * The promise that resolves when the sidebar is destroyed.
+ */
+ destroy: function SS_destroy()
+ {
+ if (this.variablesView) {
+ this.variablesView.controller.releaseActors();
+ this.variablesView = null;
+ }
+ return this._sidebar.destroy();
+ },
+
+ /**
+ * Update the object currently inspected by the sidebar.
+ *
+ * @param any aValue
+ * The JS value to inspect in the sidebar.
+ * @return Promise
+ * A promise that resolves when the update completes.
+ */
+ _update: function SS__update(aValue)
+ {
+ let options, onlyEnumVisible;
+ if (VariablesView.isPrimitive({ value: aValue })) {
+ options = { rawObject: { value: aValue } };
+ onlyEnumVisible = true;
+ } else {
+ options = { objectActor: aValue };
+ onlyEnumVisible = false;
+ }
+ let view = this.variablesView;
+ view.onlyEnumVisible = onlyEnumVisible;
+ view.empty();
+ return view.controller.setSingleVariable(options).expanded;
+ }
+};
+
+
+/**
+ * Report an error coming over the remote debugger protocol.
+ *
+ * @param string aAction
+ * The name of the action or method that failed.
+ * @param object aResponse
+ * The response packet that contains the error.
+ */
+function reportError(aAction, aResponse)
+{
+ console.error(aAction + " failed: " + aResponse.error + " " +
+ aResponse.message);
+}
+
+
+/**
+ * The PreferenceObserver listens for preference changes while Scratchpad is
+ * running.
+ */
+var PreferenceObserver = {
+ _initialized: false,
+
+ init: function PO_init()
+ {
+ if (this._initialized) {
+ return;
+ }
+
+ this.branch = Services.prefs.getBranch("devtools.scratchpad.");
+ this.branch.addObserver("", this, false);
+ this._initialized = true;
+ },
+
+ observe: function PO_observe(aMessage, aTopic, aData)
+ {
+ if (aTopic != "nsPref:changed") {
+ return;
+ }
+
+ if (aData == "recentFilesMax") {
+ Scratchpad.handleRecentFileMaxChange();
+ }
+ else if (aData == "recentFilePaths") {
+ Scratchpad.populateRecentFilesMenu();
+ }
+ },
+
+ uninit: function PO_uninit() {
+ if (!this.branch) {
+ return;
+ }
+
+ this.branch.removeObserver("", this);
+ this.branch = null;
+ }
+};
+
+
+/**
+ * The CloseObserver listens for the last browser window closing and attempts to
+ * close the Scratchpad.
+ */
+var CloseObserver = {
+ init: function CO_init()
+ {
+ Services.obs.addObserver(this, "browser-lastwindow-close-requested", false);
+ },
+
+ observe: function CO_observe(aSubject)
+ {
+ if (Scratchpad.close()) {
+ this.uninit();
+ }
+ else {
+ aSubject.QueryInterface(Ci.nsISupportsPRBool);
+ aSubject.data = true;
+ }
+ },
+
+ uninit: function CO_uninit()
+ {
+ // Will throw exception if removeObserver is called twice.
+ if (this._uninited) {
+ return;
+ }
+
+ this._uninited = true;
+ Services.obs.removeObserver(this, "browser-lastwindow-close-requested",
+ false);
+ },
+};
+
+XPCOMUtils.defineLazyGetter(Scratchpad, "strings", function () {
+ return Services.strings.createBundle(SCRATCHPAD_L10N);
+});
+
+addEventListener("load", Scratchpad.onLoad.bind(Scratchpad), false);
+addEventListener("unload", Scratchpad.onUnload.bind(Scratchpad), false);
+addEventListener("close", Scratchpad.onClose.bind(Scratchpad), false);
diff --git a/devtools/client/scratchpad/scratchpad.xul b/devtools/client/scratchpad/scratchpad.xul
new file mode 100644
index 000000000..0603fa95e
--- /dev/null
+++ b/devtools/client/scratchpad/scratchpad.xul
@@ -0,0 +1,412 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<!DOCTYPE window [
+<!ENTITY % scratchpadDTD SYSTEM "chrome://devtools/locale/scratchpad.dtd" >
+ %scratchpadDTD;
+<!ENTITY % editMenuStrings SYSTEM "chrome://global/locale/editMenuOverlay.dtd">
+%editMenuStrings;
+<!ENTITY % sourceEditorStrings SYSTEM "chrome://devtools/locale/sourceeditor.dtd">
+%sourceEditorStrings;
+]>
+
+<?xml-stylesheet href="chrome://global/skin/global.css"?>
+<?xml-stylesheet href="chrome://devtools/skin/scratchpad.css"?>
+<?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?>
+
+<window id="main-window"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="&window.title;"
+ windowtype="devtools:scratchpad"
+ macanimationtype="document"
+ fullscreenbutton="true"
+ screenX="4" screenY="4"
+ width="640" height="480"
+ persist="screenX screenY width height sizemode">
+
+<script type="application/javascript;version=1.8"
+ src="chrome://devtools/content/shared/theme-switching.js"/>
+<script type="application/javascript" src="chrome://global/content/globalOverlay.js"/>
+<script type="application/javascript" src="chrome://devtools/content/scratchpad/scratchpad.js"/>
+
+<commandset id="editMenuCommands"/>
+
+<commandset id="sourceEditorCommands">
+ <command id="cmd_find" oncommand=";"/>
+ <command id="cmd_findAgain" oncommand=";"/>
+ <command id="cmd_gotoLine" oncommand=";"/>
+</commandset>
+
+<commandset id="sp-commandset">
+ <command id="sp-cmd-newWindow" oncommand=";"/>
+ <command id="sp-cmd-openFile" oncommand=";"/>
+ <command id="sp-cmd-clearRecentFiles" oncommand=";"/>
+ <command id="sp-cmd-save" oncommand=";"/>
+ <command id="sp-cmd-saveas" oncommand=";"/>
+ <command id="sp-cmd-revert" oncommand=";" disabled="true"/>
+ <command id="sp-cmd-close" oncommand=";"/>
+ <command id="sp-cmd-line-numbers" oncommand=";"/>
+ <command id="sp-cmd-wrap-text" oncommand=";"/>
+ <command id="sp-cmd-highlight-trailing-space" oncommand=";"/>
+ <command id="sp-cmd-larger-font" oncommand=";"/>
+ <command id="sp-cmd-smaller-font" oncommand=";"/>
+ <command id="sp-cmd-normal-font" oncommand=";"/>
+ <command id="sp-cmd-run" oncommand=";"/>
+ <command id="sp-cmd-inspect" oncommand=";"/>
+ <command id="sp-cmd-display" oncommand=";"/>
+ <command id="sp-cmd-pprint" oncommand=";"/>
+ <command id="sp-cmd-contentContext" oncommand=";"/>
+ <command id="sp-cmd-browserContext" oncommand=";" disabled="true"/>
+ <command id="sp-cmd-reloadAndRun" oncommand=";"/>
+ <command id="sp-cmd-evalFunction" oncommand=";"/>
+ <command id="sp-cmd-errorConsole" oncommand=";" disabled="true"/>
+ <command id="sp-cmd-webConsole" oncommand=";"/>
+ <command id="sp-cmd-documentationLink" oncommand=";"/>
+ <command id="sp-cmd-hideSidebar" oncommand=";"/>
+</commandset>
+
+<keyset id="editMenuKeys"/>
+
+<keyset id="sp-keyset">
+ <key id="sp-key-window"
+ key="&newWindowCmd.commandkey;"
+ command="sp-cmd-newWindow"
+ modifiers="accel"/>
+ <key id="sp-key-open"
+ key="&openFileCmd.commandkey;"
+ command="sp-cmd-openFile"
+ modifiers="accel"/>
+ <key id="sp-key-save"
+ key="&saveFileCmd.commandkey;"
+ command="sp-cmd-save"
+ modifiers="accel"/>
+ <key id="sp-key-close"
+ key="&closeCmd.key;"
+ command="sp-cmd-close"
+ modifiers="accel"/>
+ <key id="sp-key-larger-font"
+ key="&largerFont.commandkey;"
+ command="sp-cmd-larger-font"
+ modifiers="accel"/>
+ <key key="&largerFont.commandkey2;"
+ command="sp-cmd-larger-font"
+ modifiers="accel"/>
+ <key id="sp-key-smaller-font"
+ key="&smallerFont.commandkey;"
+ command="sp-cmd-smaller-font"
+ modifiers="accel"/>
+ <key id="sp-key-normal-size-font"
+ key="&normalSize.commandkey;"
+ command="sp-cmd-normal-font"
+ modifiers="accel"/>
+ <key id="sp-key-run"
+ key="&run.key;"
+ command="sp-cmd-run"
+ modifiers="accel"/>
+ <key id="sp-key-inspect"
+ key="&inspect.key;"
+ command="sp-cmd-inspect"
+ modifiers="accel"/>
+ <key id="sp-key-display"
+ key="&display.key;"
+ command="sp-cmd-display"
+ modifiers="accel"/>
+ <key id="sp-key-pprint"
+ key="&pprint.key;"
+ command="sp-cmd-pprint"
+ modifiers="accel"/>
+ <key id="sp-key-reloadAndRun"
+ key="&reloadAndRun.key;"
+ command="sp-cmd-reloadAndRun"
+ modifiers="accel,shift"/>
+ <key id="sp-key-evalFunction"
+ key="&evalFunction.key;"
+ command="sp-cmd-evalFunction"
+ modifiers="accel"/>
+ <key id="sp-key-errorConsole"
+ key="&errorConsoleCmd.commandkey;"
+ command="sp-cmd-errorConsole"
+ modifiers="accel,shift"/>
+ <key id="sp-key-hideSidebar"
+ keycode="VK_ESCAPE"
+ command="sp-cmd-hideSidebar"/>
+ <key id="key_openHelp"
+ keycode="VK_F1"
+ command="sp-cmd-documentationLink"/>
+ <key id="key_gotoLine"
+ key="&gotoLineCmd.key;"
+ command="key_gotoLine"
+ modifiers="accel"/>
+
+</keyset>
+
+<menubar id="sp-menubar">
+ <menu id="sp-file-menu" label="&fileMenu.label;" accesskey="&fileMenu.accesskey;">
+ <menupopup id="sp-menu-filepopup">
+ <menuitem id="sp-menu-newscratchpad"
+ label="&newWindowCmd.label;"
+ accesskey="&newWindowCmd.accesskey;"
+ key="sp-key-window"
+ command="sp-cmd-newWindow"/>
+ <menuseparator/>
+
+ <menuitem id="sp-menu-open"
+ label="&openFileCmd.label;"
+ command="sp-cmd-openFile"
+ key="sp-key-open"
+ accesskey="&openFileCmd.accesskey;"/>
+
+ <menu id="sp-open_recent-menu" label="&openRecentMenu.label;"
+ accesskey="&openRecentMenu.accesskey;"
+ disabled="true">
+ <menupopup id="sp-menu-open_recentPopup"/>
+ </menu>
+
+ <menuitem id="sp-menu-save"
+ label="&saveFileCmd.label;"
+ accesskey="&saveFileCmd.accesskey;"
+ key="sp-key-save"
+ command="sp-cmd-save"/>
+ <menuitem id="sp-menu-saveas"
+ label="&saveFileAsCmd.label;"
+ accesskey="&saveFileAsCmd.accesskey;"
+ command="sp-cmd-saveas"/>
+ <menuitem id="sp-menu-revert"
+ label="&revertCmd.label;"
+ accesskey="&revertCmd.accesskey;"
+ command="sp-cmd-revert"/>
+ <menuseparator/>
+
+ <menuitem id="sp-menu-close"
+ label="&closeCmd.label;"
+ key="sp-key-close"
+ accesskey="&closeCmd.accesskey;"
+ command="sp-cmd-close"/>
+ </menupopup>
+ </menu>
+
+ <menu id="sp-edit-menu" label="&editMenu.label;"
+ accesskey="&editMenu.accesskey;">
+ <menupopup id="sp-menu_editpopup">
+ <menuitem id="menu_undo"/>
+ <menuitem id="menu_redo"/>
+ <menuseparator/>
+ <menuitem id="menu_cut"/>
+ <menuitem id="menu_copy"/>
+ <menuitem id="menu_paste"/>
+ <menuseparator/>
+ <menuitem id="menu_selectAll"/>
+ <menuseparator/>
+ <menuitem id="menu_find"/>
+ <menuitem id="menu_findAgain"/>
+ <menuseparator/>
+ <menuitem id="se-menu-gotoLine"
+ label="&gotoLineCmd.label;"
+ accesskey="&gotoLineCmd.accesskey;"
+ key="key_gotoLine"
+ command="cmd_gotoLine"/>
+ <menuitem id="sp-menu-pprint"
+ label="&pprint.label;"
+ accesskey="&pprint.accesskey;"
+ key="sp-key-pprint"
+ command="sp-cmd-pprint"/>
+ </menupopup>
+ </menu>
+
+ <menu id="sp-view-menu" label="&viewMenu.label;" accesskey="&viewMenu.accesskey;">
+ <menupopup id="sp-menu-viewpopup">
+ <menuitem id="sp-menu-line-numbers"
+ label="&lineNumbers.label;"
+ accesskey="&lineNumbers.accesskey;"
+ type="checkbox"
+ command="sp-cmd-line-numbers"/>
+ <menuitem id="sp-menu-word-wrap"
+ label="&wordWrap.label;"
+ accesskey="&wordWrap.accesskey;"
+ type="checkbox"
+ command="sp-cmd-wrap-text"/>
+ <menuitem id="sp-menu-highlight-trailing-space"
+ label="&highlightTrailingSpace.label;"
+ accesskey="&highlightTrailingSpace.accesskey;"
+ type="checkbox"
+ command="sp-cmd-highlight-trailing-space"/>
+ <menuseparator/>
+ <menuitem id="sp-menu-larger-font"
+ label="&largerFont.label;"
+ key="sp-key-larger-font"
+ accesskey="&largerFont.accesskey;"
+ command="sp-cmd-larger-font"/>
+ <menuitem id="sp-menu-smaller-font"
+ label="&smallerFont.label;"
+ key="sp-key-smaller-font"
+ accesskey="&smallerFont.accesskey;"
+ command="sp-cmd-smaller-font"/>
+ <menuitem id="sp-menu-normal-size-font"
+ label="&normalSize.label;"
+ key="sp-menu-normal-font"
+ accesskey="&normalSize.accesskey;"
+ command="sp-cmd-normal-font"/>
+ </menupopup>
+ </menu>
+
+ <menu id="sp-execute-menu" label="&executeMenu.label;"
+ accesskey="&executeMenu.accesskey;">
+ <menupopup id="sp-menu_executepopup">
+ <menuitem id="sp-text-run"
+ label="&run.label;"
+ accesskey="&run.accesskey;"
+ key="sp-key-run"
+ command="sp-cmd-run"/>
+ <menuitem id="sp-text-inspect"
+ label="&inspect.label;"
+ accesskey="&inspect.accesskey;"
+ key="sp-key-inspect"
+ command="sp-cmd-inspect"/>
+ <menuitem id="sp-text-display"
+ label="&display.label;"
+ accesskey="&display.accesskey;"
+ key="sp-key-display"
+ command="sp-cmd-display"/>
+ <menuseparator/>
+ <menuitem id="sp-text-reloadAndRun"
+ label="&reloadAndRun.label;"
+ key="sp-key-reloadAndRun"
+ accesskey="&reloadAndRun.accesskey;"
+ command="sp-cmd-reloadAndRun"/>
+ <menuitem id="sp-text-evalFunction"
+ label="&evalFunction.label;"
+ key="sp-key-evalFunction"
+ accesskey="&evalFunction.accesskey;"
+ command="sp-cmd-evalFunction"/>
+ </menupopup>
+ </menu>
+
+ <menu id="sp-environment-menu"
+ label="&environmentMenu.label;"
+ accesskey="&environmentMenu.accesskey;"
+ hidden="true">
+ <menupopup id="sp-menu-environment">
+ <menuitem id="sp-menu-content"
+ label="&contentContext.label;"
+ accesskey="&contentContext.accesskey;"
+ command="sp-cmd-contentContext"
+ checked="true"
+ type="radio"/>
+ <menuitem id="sp-menu-browser"
+ command="sp-cmd-browserContext"
+ label="&browserContext.label;"
+ accesskey="&browserContext.accesskey;"
+ type="radio"/>
+ </menupopup>
+ </menu>
+
+#ifdef XP_WIN
+ <menu id="sp-help-menu"
+ label="&helpMenu.label;"
+ accesskey="&helpMenuWin.accesskey;">
+#else
+ <menu id="sp-help-menu"
+ label="&helpMenu.label;"
+ accesskey="&helpMenu.accesskey;">
+#endif
+ <menupopup id="sp-menu-help">
+ <menuitem id="sp-menu-documentation"
+ label="&documentationLink.label;"
+ accesskey="&documentationLink.accesskey;"
+ command="sp-cmd-documentationLink"
+ key="key_openHelp"/>
+ </menupopup>
+ </menu>
+</menubar>
+
+<toolbar id="sp-toolbar"
+ class="devtools-toolbar">
+ <toolbarbutton id="sp-toolbar-open"
+ class="devtools-toolbarbutton"
+ label="&openFileCmd.label;"
+ command="sp-cmd-openFile"/>
+ <toolbarbutton id="sp-toolbar-save"
+ class="devtools-toolbarbutton"
+ label="&saveFileCmd.label;"
+ command="sp-cmd-save"/>
+ <toolbarbutton id="sp-toolbar-saveAs"
+ class="devtools-toolbarbutton"
+ label="&saveFileAsCmd.label;"
+ command="sp-cmd-saveas"/>
+ <toolbarspacer/>
+ <toolbarbutton id="sp-toolbar-run"
+ class="devtools-toolbarbutton"
+ label="&run.label;"
+ command="sp-cmd-run"/>
+ <toolbarbutton id="sp-toolbar-inspect"
+ class="devtools-toolbarbutton"
+ label="&inspect.label;"
+ command="sp-cmd-inspect"/>
+ <toolbarbutton id="sp-toolbar-display"
+ class="devtools-toolbarbutton"
+ label="&display.label;"
+ command="sp-cmd-display"/>
+ <toolbarspacer/>
+ <toolbarbutton id="sp-toolbar-pprint"
+ class="devtools-toolbarbutton"
+ label="&pprint.label;"
+ command="sp-cmd-pprint"/>
+</toolbar>
+
+
+<popupset id="scratchpad-popups">
+ <menupopup id="scratchpad-text-popup">
+ <menuitem id="cMenu_cut"/>
+ <menuitem id="cMenu_copy"/>
+ <menuitem id="cMenu_paste"/>
+ <menuitem id="cMenu_delete"/>
+ <menuseparator/>
+ <menuitem id="cMenu_selectAll"/>
+ <menuseparator/>
+ <menuitem id="sp-text-run"
+ label="&run.label;"
+ accesskey="&run.accesskey;"
+ key="sp-key-run"
+ command="sp-cmd-run"/>
+ <menuitem id="sp-text-inspect"
+ label="&inspect.label;"
+ accesskey="&inspect.accesskey;"
+ key="sp-key-inspect"
+ command="sp-cmd-inspect"/>
+ <menuitem id="sp-text-display"
+ label="&display.label;"
+ accesskey="&display.accesskey;"
+ key="sp-key-display"
+ command="sp-cmd-display"/>
+ <menuitem id="sp-text-evalFunction"
+ label="&evalFunction.label;"
+ key="sp-key-evalFunction"
+ accesskey="&evalFunction.accesskey;"
+ command="sp-cmd-evalFunction"/>
+ <menuseparator/>
+ <menuitem id="sp-text-reloadAndRun"
+ label="&reloadAndRun.label;"
+ key="sp-key-reloadAndRun"
+ accesskey="&reloadAndRun.accesskey;"
+ command="sp-cmd-reloadAndRun"/>
+ </menupopup>
+</popupset>
+
+<notificationbox id="scratchpad-notificationbox" flex="1">
+ <hbox flex="1">
+ <vbox id="scratchpad-editor" flex="1"/>
+ <splitter class="devtools-side-splitter"/>
+ <tabbox id="scratchpad-sidebar" class="devtools-sidebar-tabs"
+ width="300"
+ hidden="true">
+ <tabs/>
+ <tabpanels flex="1"/>
+ </tabbox>
+ </hbox>
+ <toolbar id="statusbar-line-col" class="devtools-toolbar"/>
+</notificationbox>
+
+</window>
diff --git a/devtools/client/scratchpad/test/.eslintrc.js b/devtools/client/scratchpad/test/.eslintrc.js
new file mode 100644
index 000000000..8d15a76d9
--- /dev/null
+++ b/devtools/client/scratchpad/test/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../../.eslintrc.mochitests.js"
+};
diff --git a/devtools/client/scratchpad/test/NS_ERROR_ILLEGAL_INPUT.txt b/devtools/client/scratchpad/test/NS_ERROR_ILLEGAL_INPUT.txt
new file mode 100644
index 000000000..031c0597b
--- /dev/null
+++ b/devtools/client/scratchpad/test/NS_ERROR_ILLEGAL_INPUT.txt
@@ -0,0 +1,2 @@
+Typ Datum Uhrzeit Quelle Kategorie Ereignis Benutzer Computer
+Informationen 10.08.2012 16:07:11 MSDTC Datenträger 2444 Nicht zutreffend
diff --git a/devtools/client/scratchpad/test/browser.ini b/devtools/client/scratchpad/test/browser.ini
new file mode 100644
index 000000000..cc67ce1ab
--- /dev/null
+++ b/devtools/client/scratchpad/test/browser.ini
@@ -0,0 +1,46 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files = head.js
+
+[browser_scratchpad_autocomplete.js]
+[browser_scratchpad_browser_last_window_closing.js]
+[browser_scratchpad_reset_undo.js]
+[browser_scratchpad_display_outputs_errors.js]
+[browser_scratchpad_eval_func.js]
+[browser_scratchpad_goto_line_ui.js]
+[browser_scratchpad_reload_and_run.js]
+[browser_scratchpad_display_non_error_exceptions.js]
+[browser_scratchpad_modeline.js]
+[browser_scratchpad_chrome_context_pref.js]
+[browser_scratchpad_help_key.js]
+[browser_scratchpad_recent_files.js]
+# [browser_scratchpad_confirm_close.js]
+# Disable test due to bug 807234 becoming basically permanent
+[browser_scratchpad_tab.js]
+[browser_scratchpad_wrong_window_focus.js]
+[browser_scratchpad_unsaved.js]
+[browser_scratchpad_falsy.js]
+[browser_scratchpad_edit_ui_updates.js]
+[browser_scratchpad_revert_to_saved.js]
+[browser_scratchpad_run_error_goto_line.js]
+[browser_scratchpad_contexts.js]
+[browser_scratchpad_execute_print.js]
+[browser_scratchpad_files.js]
+[browser_scratchpad_initialization.js]
+[browser_scratchpad_inspect.js]
+[browser_scratchpad_inspect_primitives.js]
+[browser_scratchpad_long_string.js]
+[browser_scratchpad_open.js]
+support-files = NS_ERROR_ILLEGAL_INPUT.txt
+[browser_scratchpad_open_error_console.js]
+[browser_scratchpad_throw_output.js]
+[browser_scratchpad_pprint-02.js]
+[browser_scratchpad_pprint.js]
+[browser_scratchpad_pprint_error_goto_line.js]
+[browser_scratchpad_restore.js]
+[browser_scratchpad_tab_switch.js]
+[browser_scratchpad_ui.js]
+[browser_scratchpad_close_toolbox.js]
+[browser_scratchpad_remember_view_options.js]
+[browser_scratchpad_disable_view_menu_items.js]
diff --git a/devtools/client/scratchpad/test/browser_scratchpad_autocomplete.js b/devtools/client/scratchpad/test/browser_scratchpad_autocomplete.js
new file mode 100644
index 000000000..3a6eef8b4
--- /dev/null
+++ b/devtools/client/scratchpad/test/browser_scratchpad_autocomplete.js
@@ -0,0 +1,66 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* Bug 968896 */
+
+// Test the completions using numbers.
+const source = "0x1.";
+const completions = ["toExponential", "toFixed", "toString"];
+const { Task } = require("devtools/shared/task");
+
+function test() {
+ const options = { tabContent: "test scratchpad autocomplete" };
+ openTabAndScratchpad(options)
+ .then(Task.async(runTests))
+ .then(finish, console.error);
+}
+
+
+function* runTests([win, sp]) {
+ const {editor} = sp;
+ const editorWin = editor.container.contentWindow;
+
+ // Show the completions popup.
+ sp.setText(source);
+ sp.editor.setCursor({ line: 0, ch: source.length });
+ yield keyOnce("suggestion-entered", " ", { ctrlKey: true });
+
+ // Get the hints popup container.
+ const hints = editorWin.document.querySelector(".CodeMirror-hints");
+
+ ok(hints,
+ "The hint container should exist.");
+ is(hints.childNodes.length, 3,
+ "The hint container should have the completions.");
+
+ let i = 0;
+ for (let completion of completions) {
+ let active = hints.querySelector(".CodeMirror-hint-active");
+ is(active.textContent, completion,
+ "Check that completion " + i++ + " is what is expected.");
+ yield keyOnce("suggestion-entered", "VK_DOWN");
+ }
+
+ // We should have looped around to the first suggestion again. Accept it.
+ yield keyOnce("after-suggest", "VK_RETURN");
+
+ is(sp.getText(), source + completions[0],
+ "Autocompletion should work and select the right element.");
+
+ // Check that the information tooltips work.
+ sp.setText("5");
+ yield keyOnce("show-information", " ", { ctrlKey: true, shiftKey: true });
+
+ // Get the information container.
+ const info = editorWin.document.querySelector(".CodeMirror-Tern-information");
+ ok(info,
+ "Info tooltip should appear.");
+ is(info.textContent.slice(0, 6), "number",
+ "Info tooltip should have expected contents.");
+
+ function keyOnce(event, key, opts = {}) {
+ const p = editor.once(event);
+ EventUtils.synthesizeKey(key, opts, editorWin);
+ return p;
+ }
+}
diff --git a/devtools/client/scratchpad/test/browser_scratchpad_browser_last_window_closing.js b/devtools/client/scratchpad/test/browser_scratchpad_browser_last_window_closing.js
new file mode 100644
index 000000000..3a8316059
--- /dev/null
+++ b/devtools/client/scratchpad/test/browser_scratchpad_browser_last_window_closing.js
@@ -0,0 +1,79 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const BUTTON_POSITION_CANCEL = 1;
+const BUTTON_POSITION_DONT_SAVE = 2;
+
+
+function test()
+{
+ waitForExplicitFinish();
+
+ // Observer must be attached *before* Scratchpad is opened.
+ CloseObserver.init();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onTabLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", onTabLoad, true);
+ openScratchpad(runTests);
+ }, true);
+
+ content.location = "data:text/html;charset=utf8,<p>test browser last window closing</p>";
+}
+
+
+
+function runTests({ Scratchpad })
+{
+ let browser = Services.wm.getEnumerator("navigator:browser").getNext();
+ let oldPrompt = Services.prompt;
+ let button;
+
+ Services.prompt = {
+ confirmEx: () => button
+ };
+
+
+ Scratchpad.dirty = true;
+
+ // Test canceling close.
+ button = BUTTON_POSITION_CANCEL;
+ CloseObserver.expectedValue = true;
+ browser.BrowserTryToCloseWindow();
+
+ // Test accepting close.
+ button = BUTTON_POSITION_DONT_SAVE;
+ CloseObserver.expectedValue = false;
+ browser.BrowserTryToCloseWindow();
+
+ // Test closing without prompt needed.
+ Scratchpad.dirty = false;
+ browser.BrowserTryToCloseWindow();
+
+ Services.prompt = oldPrompt;
+ CloseObserver.uninit();
+ finish();
+}
+
+
+var CloseObserver = {
+ expectedValue: null,
+ init: function ()
+ {
+ Services.obs.addObserver(this, "browser-lastwindow-close-requested", false);
+ },
+
+ observe: function (aSubject)
+ {
+ aSubject.QueryInterface(Ci.nsISupportsPRBool);
+ let message = this.expectedValue ? "close" : "stay open";
+ ok(this.expectedValue === aSubject.data, "Expected browser to " + message);
+ aSubject.data = true;
+ },
+
+ uninit: function ()
+ {
+ Services.obs.removeObserver(this, "browser-lastwindow-close-requested", false);
+ },
+};
diff --git a/devtools/client/scratchpad/test/browser_scratchpad_chrome_context_pref.js b/devtools/client/scratchpad/test/browser_scratchpad_chrome_context_pref.js
new file mode 100644
index 000000000..08528d2c2
--- /dev/null
+++ b/devtools/client/scratchpad/test/browser_scratchpad_chrome_context_pref.js
@@ -0,0 +1,50 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* Bug 646070 */
+
+var DEVTOOLS_CHROME_ENABLED = "devtools.chrome.enabled";
+
+function test()
+{
+ waitForExplicitFinish();
+
+ Services.prefs.setBoolPref(DEVTOOLS_CHROME_ENABLED, true);
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
+
+ openScratchpad(runTests);
+ }, true);
+
+ content.location = "data:text/html,Scratchpad test for bug 646070 - chrome context preference";
+}
+
+function runTests()
+{
+ let sp = gScratchpadWindow.Scratchpad;
+ ok(sp, "Scratchpad object exists in new window");
+
+ let environmentMenu = gScratchpadWindow.document.
+ getElementById("sp-environment-menu");
+ ok(environmentMenu, "Environment menu element exists");
+ ok(!environmentMenu.hasAttribute("hidden"),
+ "Environment menu is visible");
+
+ let errorConsoleCommand = gScratchpadWindow.document.
+ getElementById("sp-cmd-errorConsole");
+ ok(errorConsoleCommand, "Error console command element exists");
+ ok(!errorConsoleCommand.hasAttribute("disabled"),
+ "Error console command is enabled");
+
+ let chromeContextCommand = gScratchpadWindow.document.
+ getElementById("sp-cmd-browserContext");
+ ok(chromeContextCommand, "Chrome context command element exists");
+ ok(!chromeContextCommand.hasAttribute("disabled"),
+ "Chrome context command is disabled");
+
+ Services.prefs.clearUserPref(DEVTOOLS_CHROME_ENABLED);
+
+ finish();
+}
diff --git a/devtools/client/scratchpad/test/browser_scratchpad_close_toolbox.js b/devtools/client/scratchpad/test/browser_scratchpad_close_toolbox.js
new file mode 100644
index 000000000..fd1126fd4
--- /dev/null
+++ b/devtools/client/scratchpad/test/browser_scratchpad_close_toolbox.js
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that closing the toolbox after having opened a scratchpad leaves the
+// latter in a functioning state.
+
+var {Task} = require("devtools/shared/task");
+var {TargetFactory} = require("devtools/client/framework/target");
+
+function test() {
+ const options = {
+ tabContent: "test closing toolbox and then reusing scratchpad"
+ };
+ openTabAndScratchpad(options)
+ .then(Task.async(runTests))
+ .then(finish, console.error);
+}
+
+function* runTests([win, sp]) {
+ // Use the scratchpad before opening the toolbox.
+ const source = "window.foobar = 7;";
+ sp.setText(source);
+ let [,, result] = yield sp.display();
+ is(result, 7, "Display produced the expected output.");
+
+ // Now open the toolbox and close it again.
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ let toolbox = yield gDevTools.showToolbox(target, "webconsole");
+ ok(toolbox, "Toolbox was opened.");
+ let closed = yield gDevTools.closeToolbox(target);
+ is(closed, true, "Toolbox was closed.");
+
+ // Now see if using the scratcphad works as expected.
+ sp.setText(source);
+ let [,, result2] = yield sp.display();
+ is(result2, 7,
+ "Display produced the expected output after the toolbox was gone.");
+}
diff --git a/devtools/client/scratchpad/test/browser_scratchpad_confirm_close.js b/devtools/client/scratchpad/test/browser_scratchpad_confirm_close.js
new file mode 100644
index 000000000..a6318fa75
--- /dev/null
+++ b/devtools/client/scratchpad/test/browser_scratchpad_confirm_close.js
@@ -0,0 +1,230 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* Bug 653427 */
+
+var tempScope = {};
+Cu.import("resource://gre/modules/NetUtil.jsm", tempScope);
+Cu.import("resource://gre/modules/FileUtils.jsm", tempScope);
+var NetUtil = tempScope.NetUtil;
+var FileUtils = tempScope.FileUtils;
+
+// only finish() when correct number of tests are done
+const expected = 9;
+var count = 0;
+function done()
+{
+ if (++count == expected) {
+ cleanup();
+ finish();
+ }
+}
+
+var gFile;
+
+var oldPrompt = Services.prompt;
+var promptButton = -1;
+
+function test()
+{
+ waitForExplicitFinish();
+
+ gFile = createTempFile("fileForBug653427.tmp");
+ writeFile(gFile, "text", testUnsaved.call(this));
+
+ Services.prompt = {
+ confirmEx: function () {
+ return promptButton;
+ }
+ };
+
+ testNew();
+ testSavedFile();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ content.location = "data:text/html,<p>test scratchpad save file prompt on closing";
+}
+
+function testNew()
+{
+ openScratchpad(function (win) {
+ win.Scratchpad.close(function () {
+ ok(win.closed, "new scratchpad window should close without prompting");
+ done();
+ });
+ }, {noFocus: true});
+}
+
+function testSavedFile()
+{
+ openScratchpad(function (win) {
+ win.Scratchpad.filename = "test.js";
+ win.Scratchpad.editor.dirty = false;
+ win.Scratchpad.close(function () {
+ ok(win.closed, "scratchpad from file with no changes should close");
+ done();
+ });
+ }, {noFocus: true});
+}
+
+function testUnsaved()
+{
+ function setFilename(aScratchpad, aFile) {
+ aScratchpad.setFilename(aFile);
+ }
+
+ testUnsavedFileCancel(setFilename);
+ testUnsavedFileSave(setFilename);
+ testUnsavedFileDontSave(setFilename);
+ testCancelAfterLoad();
+
+ function mockSaveFile(aScratchpad) {
+ let SaveFileStub = function (aCallback) {
+ /*
+ * An argument for aCallback must pass Components.isSuccessCode
+ *
+ * A version of isSuccessCode in JavaScript:
+ * function isSuccessCode(returnCode) {
+ * return (returnCode & 0x80000000) == 0;
+ * }
+ */
+ aCallback(1);
+ };
+
+ aScratchpad.saveFile = SaveFileStub;
+ }
+
+ // Run these tests again but this time without setting a filename to
+ // test that Scratchpad always asks for confirmation on dirty editor.
+ testUnsavedFileCancel(mockSaveFile);
+ testUnsavedFileSave(mockSaveFile);
+ testUnsavedFileDontSave();
+}
+
+function testUnsavedFileCancel(aCallback = function () {})
+{
+ openScratchpad(function (win) {
+ aCallback(win.Scratchpad, "test.js");
+ win.Scratchpad.editor.dirty = true;
+
+ promptButton = win.BUTTON_POSITION_CANCEL;
+
+ win.Scratchpad.close(function () {
+ ok(!win.closed, "cancelling dialog shouldn't close scratchpad");
+ win.close();
+ done();
+ });
+ }, {noFocus: true});
+}
+
+// Test a regression where our confirmation dialog wasn't appearing
+// after openFile calls. See bug 801982.
+function testCancelAfterLoad()
+{
+ openScratchpad(function (win) {
+ win.Scratchpad.setRecentFile(gFile);
+ win.Scratchpad.openFile(0);
+ win.Scratchpad.editor.dirty = true;
+ promptButton = win.BUTTON_POSITION_CANCEL;
+
+ let EventStub = {
+ called: false,
+ preventDefault: function () {
+ EventStub.called = true;
+ }
+ };
+
+ win.Scratchpad.onClose(EventStub, function () {
+ ok(!win.closed, "cancelling dialog shouldn't close scratchpad");
+ ok(EventStub.called, "aEvent.preventDefault was called");
+
+ win.Scratchpad.editor.dirty = false;
+ win.close();
+ done();
+ });
+ }, {noFocus: true});
+}
+
+function testUnsavedFileSave(aCallback = function () {})
+{
+ openScratchpad(function (win) {
+ win.Scratchpad.importFromFile(gFile, true, function (status, content) {
+ aCallback(win.Scratchpad, gFile.path);
+
+ let text = "new text";
+ win.Scratchpad.setText(text);
+
+ promptButton = win.BUTTON_POSITION_SAVE;
+
+ win.Scratchpad.close(function () {
+ ok(win.closed, 'pressing "Save" in dialog should close scratchpad');
+ readFile(gFile, function (savedContent) {
+ is(savedContent, text, 'prompted "Save" worked when closing scratchpad');
+ done();
+ });
+ });
+ });
+ }, {noFocus: true});
+}
+
+function testUnsavedFileDontSave(aCallback = function () {})
+{
+ openScratchpad(function (win) {
+ aCallback(win.Scratchpad, gFile.path);
+ win.Scratchpad.editor.dirty = true;
+
+ promptButton = win.BUTTON_POSITION_DONT_SAVE;
+
+ win.Scratchpad.close(function () {
+ ok(win.closed, 'pressing "Don\'t Save" in dialog should close scratchpad');
+ done();
+ });
+ }, {noFocus: true});
+}
+
+function cleanup()
+{
+ Services.prompt = oldPrompt;
+ gFile.remove(false);
+ gFile = null;
+}
+
+function createTempFile(name)
+{
+ let file = FileUtils.getFile("TmpD", [name]);
+ file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o666);
+ file.QueryInterface(Ci.nsILocalFile);
+ return file;
+}
+
+function writeFile(file, content, callback)
+{
+ let fout = Cc["@mozilla.org/network/file-output-stream;1"].
+ createInstance(Ci.nsIFileOutputStream);
+ fout.init(file.QueryInterface(Ci.nsILocalFile), 0x02 | 0x08 | 0x20,
+ 0o644, fout.DEFER_OPEN);
+
+ let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
+ createInstance(Ci.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+ let fileContentStream = converter.convertToInputStream(content);
+
+ NetUtil.asyncCopy(fileContentStream, fout, callback);
+}
+
+function readFile(file, callback)
+{
+ let channel = NetUtil.newChannel({
+ uri: NetUtil.newURI(file),
+ loadUsingSystemPrincipal: true});
+ channel.contentType = "application/javascript";
+
+ NetUtil.asyncFetch(channel, function (inputStream, status) {
+ ok(Components.isSuccessCode(status),
+ "file was read successfully");
+
+ let content = NetUtil.readInputStreamToString(inputStream,
+ inputStream.available());
+ callback(content);
+ });
+}
diff --git a/devtools/client/scratchpad/test/browser_scratchpad_contexts.js b/devtools/client/scratchpad/test/browser_scratchpad_contexts.js
new file mode 100644
index 000000000..ae1933b4d
--- /dev/null
+++ b/devtools/client/scratchpad/test/browser_scratchpad_contexts.js
@@ -0,0 +1,149 @@
+/* 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";
+
+function test() {
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
+ openScratchpad(runTests);
+ }, true);
+
+ content.location = "data:text/html,test context switch in Scratchpad";
+}
+
+function runTests() {
+ let sp = gScratchpadWindow.Scratchpad;
+ let contentMenu = gScratchpadWindow.document.getElementById("sp-menu-content");
+ let chromeMenu = gScratchpadWindow.document.getElementById("sp-menu-browser");
+ let notificationBox = sp.notificationBox;
+
+ ok(contentMenu, "found #sp-menu-content");
+ ok(chromeMenu, "found #sp-menu-browser");
+ ok(notificationBox, "found Scratchpad.notificationBox");
+
+ let tests = [{
+ method: "run",
+ prepare: function* () {
+ sp.setContentContext();
+
+ is(sp.executionContext, gScratchpadWindow.SCRATCHPAD_CONTEXT_CONTENT,
+ "executionContext is content");
+
+ is(contentMenu.getAttribute("checked"), "true",
+ "content menuitem is checked");
+
+ isnot(chromeMenu.getAttribute("checked"), "true",
+ "chrome menuitem is not checked");
+
+ ok(!notificationBox.currentNotification,
+ "there is no notification in content context");
+
+ sp.editor.setText("window.foobarBug636725 = 'aloha';");
+
+ let pageResult = yield inContent(function* () {
+ return content.wrappedJSObject.foobarBug636725;
+ });
+ ok(!pageResult, "no content.foobarBug636725");
+ },
+ then: function* () {
+ is(content.wrappedJSObject.foobarBug636725, "aloha",
+ "content.foobarBug636725 has been set");
+ }
+ }, {
+ method: "run",
+ prepare: function* () {
+ sp.setBrowserContext();
+
+ is(sp.executionContext, gScratchpadWindow.SCRATCHPAD_CONTEXT_BROWSER,
+ "executionContext is chrome");
+
+ is(chromeMenu.getAttribute("checked"), "true",
+ "chrome menuitem is checked");
+
+ isnot(contentMenu.getAttribute("checked"), "true",
+ "content menuitem is not checked");
+
+ ok(notificationBox.currentNotification,
+ "there is a notification in browser context");
+
+ let [ from, to ] = sp.editor.getPosition(31, 32);
+ sp.editor.replaceText("2'", from, to);
+
+ is(sp.getText(), "window.foobarBug636725 = 'aloha2';",
+ "setText() worked");
+ },
+ then: function* () {
+ is(window.foobarBug636725, "aloha2",
+ "window.foobarBug636725 has been set");
+
+ delete window.foobarBug636725;
+ ok(!window.foobarBug636725, "no window.foobarBug636725");
+ }
+ }, {
+ method: "run",
+ prepare: function* () {
+ sp.editor.replaceText("gBrowser", sp.editor.getPosition(7));
+
+ is(sp.getText(), "window.gBrowser",
+ "setText() worked with no end for the replace range");
+ },
+ then: function* ([, , result]) {
+ is(result.class, "XULElement",
+ "chrome context has access to chrome objects");
+ }
+ }, {
+ method: "run",
+ prepare: function* () {
+ // Check that the sandbox is cached.
+ sp.editor.setText("typeof foobarBug636725cache;");
+ },
+ then: function* ([, , result]) {
+ is(result, "undefined", "global variable does not exist");
+ }
+ }, {
+ method: "run",
+ prepare: function* () {
+ sp.editor.setText("window.foobarBug636725cache = 'foo';" +
+ "typeof foobarBug636725cache;");
+ },
+ then: function* ([, , result]) {
+ is(result, "string",
+ "global variable exists across two different executions");
+ }
+ }, {
+ method: "run",
+ prepare: function* () {
+ sp.editor.setText("window.foobarBug636725cache2 = 'foo';" +
+ "typeof foobarBug636725cache2;");
+ },
+ then: function* ([, , result]) {
+ is(result, "string",
+ "global variable exists across two different executions");
+ }
+ }, {
+ method: "run",
+ prepare: function* () {
+ sp.setContentContext();
+
+ is(sp.executionContext, gScratchpadWindow.SCRATCHPAD_CONTEXT_CONTENT,
+ "executionContext is content");
+
+ sp.editor.setText("typeof foobarBug636725cache2;");
+ },
+ then: function* ([, , result]) {
+ is(result, "undefined",
+ "global variable no longer exists after changing the context");
+ }
+ }];
+
+ runAsyncCallbackTests(sp, tests).then(() => {
+ sp.setBrowserContext();
+ sp.editor.setText("delete foobarBug636725cache;" +
+ "delete foobarBug636725cache2;");
+ sp.run().then(finish);
+ });
+}
diff --git a/devtools/client/scratchpad/test/browser_scratchpad_disable_view_menu_items.js b/devtools/client/scratchpad/test/browser_scratchpad_disable_view_menu_items.js
new file mode 100644
index 000000000..ed501ce2d
--- /dev/null
+++ b/devtools/client/scratchpad/test/browser_scratchpad_disable_view_menu_items.js
@@ -0,0 +1,66 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test if the view menu items "Larger Font" and "Smaller Font" are disabled
+// when the font size reaches the maximum/minimum values.
+
+var {Task} = require("devtools/shared/task");
+
+function test() {
+ const options = {
+ tabContent: 'test if view menu items "Larger Font" and "Smaller Font" are enabled/disabled.'
+ };
+ openTabAndScratchpad(options)
+ .then(Task.async(runTests))
+ .then(finish, console.error);
+}
+
+function* runTests([win, sp]) {
+ yield testMaximumFontSize(win, sp);
+
+ yield testMinimumFontSize(win, sp);
+}
+
+const MAXIMUM_FONT_SIZE = 96;
+const MINIMUM_FONT_SIZE = 6;
+const NORMAL_FONT_SIZE = 12;
+
+var testMaximumFontSize = Task.async(function* (win, sp) {
+ let doc = win.document;
+
+ Services.prefs.clearUserPref("devtools.scratchpad.editorFontSize");
+
+ let menu = doc.getElementById("sp-menu-larger-font");
+
+ for (let i = NORMAL_FONT_SIZE; i <= MAXIMUM_FONT_SIZE; i++) {
+ menu.doCommand();
+ }
+
+ let cmd = doc.getElementById("sp-cmd-larger-font");
+ ok(cmd.getAttribute("disabled") === "true", 'Command "sp-cmd-larger-font" is disabled.');
+
+ menu = doc.getElementById("sp-menu-smaller-font");
+ menu.doCommand();
+
+ ok(cmd.hasAttribute("disabled") === false, 'Command "sp-cmd-larger-font" is enabled.');
+});
+
+var testMinimumFontSize = Task.async(function* (win, sp) {
+ let doc = win.document;
+
+ let menu = doc.getElementById("sp-menu-smaller-font");
+
+ for (let i = MAXIMUM_FONT_SIZE; i >= MINIMUM_FONT_SIZE; i--) {
+ menu.doCommand();
+ }
+
+ let cmd = doc.getElementById("sp-cmd-smaller-font");
+ ok(cmd.getAttribute("disabled") === "true", 'Command "sp-cmd-smaller-font" is disabled.');
+
+ menu = doc.getElementById("sp-menu-larger-font");
+ menu.doCommand();
+
+ ok(cmd.hasAttribute("disabled") === false, 'Command "sp-cmd-smaller-font" is enabled.');
+
+ Services.prefs.clearUserPref("devtools.scratchpad.editorFontSize");
+});
diff --git a/devtools/client/scratchpad/test/browser_scratchpad_display_non_error_exceptions.js b/devtools/client/scratchpad/test/browser_scratchpad_display_non_error_exceptions.js
new file mode 100644
index 000000000..d1f2cb513
--- /dev/null
+++ b/devtools/client/scratchpad/test/browser_scratchpad_display_non_error_exceptions.js
@@ -0,0 +1,110 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* Bug 756681 */
+
+function test()
+{
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function browserLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", browserLoad, true);
+ openScratchpad(runTests, {"state":{"text":""}});
+ }, true);
+
+ content.location = "data:text/html, test that exceptions are output as " +
+ "comments correctly in Scratchpad";
+}
+
+function runTests()
+{
+ var scratchpad = gScratchpadWindow.Scratchpad;
+
+ var message = "\"Hello World!\"";
+ var openComment = "\n/*\n";
+ var closeComment = "\n*/";
+ var error1 = "throw new Error(\"Ouch!\")";
+ var error2 = "throw \"A thrown string\"";
+ var error3 = "throw {}";
+ var error4 = "document.body.appendChild(document.body)";
+
+ let tests = [{
+ // Display message
+ method: "display",
+ code: message,
+ result: message + openComment + "Hello World!" + closeComment,
+ label: "message display output"
+ },
+ {
+ // Display error1, throw new Error("Ouch")
+ method: "display",
+ code: error1,
+ result: error1 + openComment +
+ "Exception: Error: Ouch!\n@" + scratchpad.uniqueName + ":1:7" + closeComment,
+ label: "error display output"
+ },
+ {
+ // Display error2, throw "A thrown string"
+ method: "display",
+ code: error2,
+ result: error2 + openComment + "Exception: A thrown string" + closeComment,
+ label: "thrown string display output"
+ },
+ {
+ // Display error3, throw {}
+ method: "display",
+ code: error3,
+ result: error3 + openComment + "Exception: [object Object]" + closeComment,
+ label: "thrown object display output"
+ },
+ {
+ // Display error4, document.body.appendChild(document.body)
+ method: "display",
+ code: error4,
+ result: error4 + openComment + "Exception: HierarchyRequestError: Node cannot be inserted " +
+ "at the specified point in the hierarchy\n@" +
+ scratchpad.uniqueName + ":1:0" + closeComment,
+ label: "Alternative format error display output"
+ },
+ {
+ // Run message
+ method: "run",
+ code: message,
+ result: message,
+ label: "message run output"
+ },
+ {
+ // Run error1, throw new Error("Ouch")
+ method: "run",
+ code: error1,
+ result: error1 + openComment +
+ "Exception: Error: Ouch!\n@" + scratchpad.uniqueName + ":1:7" + closeComment,
+ label: "error run output"
+ },
+ {
+ // Run error2, throw "A thrown string"
+ method: "run",
+ code: error2,
+ result: error2 + openComment + "Exception: A thrown string" + closeComment,
+ label: "thrown string run output"
+ },
+ {
+ // Run error3, throw {}
+ method: "run",
+ code: error3,
+ result: error3 + openComment + "Exception: [object Object]" + closeComment,
+ label: "thrown object run output"
+ },
+ {
+ // Run error4, document.body.appendChild(document.body)
+ method: "run",
+ code: error4,
+ result: error4 + openComment + "Exception: HierarchyRequestError: Node cannot be inserted " +
+ "at the specified point in the hierarchy\n@" +
+ scratchpad.uniqueName + ":1:0" + closeComment,
+ label: "Alternative format error run output"
+ }];
+
+ runAsyncTests(scratchpad, tests).then(finish);
+}
diff --git a/devtools/client/scratchpad/test/browser_scratchpad_display_outputs_errors.js b/devtools/client/scratchpad/test/browser_scratchpad_display_outputs_errors.js
new file mode 100644
index 000000000..3855a873d
--- /dev/null
+++ b/devtools/client/scratchpad/test/browser_scratchpad_display_outputs_errors.js
@@ -0,0 +1,72 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* Bug 690552 */
+
+function test()
+{
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function browserLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", browserLoad, true);
+ openScratchpad(runTests, {"state":{"text":""}});
+ }, true);
+
+ content.location = "data:text/html,<p>test that exceptions are output as " +
+ "comments for 'display' and not sent to the console in Scratchpad";
+}
+
+function runTests()
+{
+ let scratchpad = gScratchpadWindow.Scratchpad;
+
+ let message = "\"Hello World!\"";
+ let openComment = "\n/*\n";
+ let closeComment = "\n*/";
+ let error = "throw new Error(\"Ouch!\")";
+ let syntaxError = "(";
+
+ let tests = [{
+ method: "display",
+ code: message,
+ result: message + openComment + "Hello World!" + closeComment,
+ label: "message display output"
+ },
+ {
+ method: "display",
+ code: error,
+ result: error + openComment + "Exception: Error: Ouch!\n@" +
+ scratchpad.uniqueName + ":1:7" + closeComment,
+ label: "error display output",
+ },
+ {
+ method: "display",
+ code: syntaxError,
+ result: syntaxError + openComment + "Exception: SyntaxError: expected expression, got end of script\n@" +
+ scratchpad.uniqueName + ":1" + closeComment,
+ label: "syntaxError display output",
+ },
+ {
+ method: "run",
+ code: message,
+ result: message,
+ label: "message run output",
+ },
+ {
+ method: "run",
+ code: error,
+ result: error + openComment + "Exception: Error: Ouch!\n@" +
+ scratchpad.uniqueName + ":1:7" + closeComment,
+ label: "error run output",
+ },
+ {
+ method: "run",
+ code: syntaxError,
+ result: syntaxError + openComment + "Exception: SyntaxError: expected expression, got end of script\n@" +
+ scratchpad.uniqueName + ":1" + closeComment,
+ label: "syntaxError run output",
+ }];
+
+ runAsyncTests(scratchpad, tests).then(finish);
+}
diff --git a/devtools/client/scratchpad/test/browser_scratchpad_edit_ui_updates.js b/devtools/client/scratchpad/test/browser_scratchpad_edit_ui_updates.js
new file mode 100644
index 000000000..ade87eaac
--- /dev/null
+++ b/devtools/client/scratchpad/test/browser_scratchpad_edit_ui_updates.js
@@ -0,0 +1,206 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* Bug 699130 */
+
+"use strict";
+
+var WebConsoleUtils = require("devtools/client/webconsole/utils").Utils;
+var DEVTOOLS_CHROME_ENABLED = "devtools.chrome.enabled";
+
+function test()
+{
+ waitForExplicitFinish();
+ gBrowser.selectedTab = gBrowser.addTab();
+ Services.prefs.setBoolPref(DEVTOOLS_CHROME_ENABLED, false);
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
+ openScratchpad(runTests);
+ }, true);
+
+ content.location = "data:text/html,test Edit menu updates Scratchpad - bug 699130";
+}
+
+function runTests()
+{
+ let sp = gScratchpadWindow.Scratchpad;
+ let doc = gScratchpadWindow.document;
+ let winUtils = gScratchpadWindow.QueryInterface(Ci.nsIInterfaceRequestor).
+ getInterface(Ci.nsIDOMWindowUtils);
+ let OS = Services.appinfo.OS;
+
+ info("will test the Edit menu");
+
+ let pass = 0;
+
+ sp.setText("bug 699130: hello world! (edit menu)");
+
+ let editMenu = doc.getElementById("sp-edit-menu");
+ ok(editMenu, "the Edit menu");
+ let menubar = editMenu.parentNode;
+ ok(menubar, "menubar found");
+
+ let editMenuIndex = -1;
+ for (let i = 0; i < menubar.children.length; i++) {
+ if (menubar.children[i] === editMenu) {
+ editMenuIndex = i;
+ break;
+ }
+ }
+ isnot(editMenuIndex, -1, "Edit menu index is correct");
+
+ let menuPopup = editMenu.menupopup;
+ ok(menuPopup, "the Edit menupopup");
+ let cutItem = doc.getElementById("menu_cut");
+ ok(cutItem, "the Cut menuitem");
+ let pasteItem = doc.getElementById("menu_paste");
+ ok(pasteItem, "the Paste menuitem");
+
+ let anchor = doc.documentElement;
+ let isContextMenu = false;
+
+ let oldVal = sp.editor.getText();
+
+ let testSelfXss = function (oldVal) {
+ // Self xss prevention tests (bug 994134)
+ info("Self xss paste tests");
+ is(WebConsoleUtils.usageCount, 0, "Test for usage count getter");
+ let notificationbox = doc.getElementById("scratchpad-notificationbox");
+ let notification = notificationbox.getNotificationWithValue("selfxss-notification");
+ ok(notification, "Self-xss notification shown");
+ is(oldVal, sp.editor.getText(), "Paste blocked by self-xss prevention");
+ Services.prefs.setIntPref("devtools.selfxss.count", 10);
+ notificationbox.removeAllNotifications(true);
+ openMenu(10, 10, firstShow);
+ };
+
+ let openMenu = function (aX, aY, aCallback) {
+ if (!editMenu || OS != "Darwin") {
+ menuPopup.addEventListener("popupshown", function onPopupShown() {
+ menuPopup.removeEventListener("popupshown", onPopupShown, false);
+ executeSoon(aCallback);
+ }, false);
+ }
+
+ executeSoon(function () {
+ if (editMenu) {
+ if (OS == "Darwin") {
+ winUtils.forceUpdateNativeMenuAt(editMenuIndex);
+ executeSoon(aCallback);
+ } else {
+ editMenu.open = true;
+ }
+ } else {
+ menuPopup.openPopup(anchor, "overlap", aX, aY, isContextMenu, false);
+ }
+ });
+ };
+
+ let closeMenu = function (aCallback) {
+ if (!editMenu || OS != "Darwin") {
+ menuPopup.addEventListener("popuphidden", function onPopupHidden() {
+ menuPopup.removeEventListener("popuphidden", onPopupHidden, false);
+ executeSoon(aCallback);
+ }, false);
+ }
+
+ executeSoon(function () {
+ if (editMenu) {
+ if (OS == "Darwin") {
+ winUtils.forceUpdateNativeMenuAt(editMenuIndex);
+ executeSoon(aCallback);
+ } else {
+ editMenu.open = false;
+ }
+ } else {
+ menuPopup.hidePopup();
+ }
+ });
+ };
+
+ let firstShow = function () {
+ ok(!cutItem.hasAttribute("disabled"), "cut menuitem is enabled");
+ closeMenu(firstHide);
+ };
+
+ let firstHide = function () {
+ sp.editor.setSelection({ line: 0, ch: 0 }, { line: 0, ch: 10 });
+ openMenu(11, 11, showAfterSelect);
+ };
+
+ let showAfterSelect = function () {
+ ok(!cutItem.hasAttribute("disabled"), "cut menuitem is enabled after select");
+ closeMenu(hideAfterSelect);
+ };
+
+ let hideAfterSelect = function () {
+ sp.editor.on("change", onCut);
+ waitForFocus(function () {
+ let selectedText = sp.editor.getSelection();
+ ok(selectedText.length > 0, "non-empty selected text will be cut");
+
+ EventUtils.synthesizeKey("x", {accelKey: true}, gScratchpadWindow);
+ }, gScratchpadWindow);
+ };
+
+ let onCut = function () {
+ sp.editor.off("change", onCut);
+ openMenu(12, 12, showAfterCut);
+ };
+
+ let showAfterCut = function () {
+ ok(!cutItem.hasAttribute("disabled"), "cut menuitem is enabled after cut");
+ ok(!pasteItem.hasAttribute("disabled"), "paste menuitem is enabled after cut");
+ closeMenu(hideAfterCut);
+ };
+
+ let hideAfterCut = function () {
+ waitForFocus(function () {
+ sp.editor.on("change", onPaste);
+ EventUtils.synthesizeKey("v", {accelKey: true}, gScratchpadWindow);
+ }, gScratchpadWindow);
+ };
+
+ let onPaste = function () {
+ sp.editor.off("change", onPaste);
+ openMenu(13, 13, showAfterPaste);
+ };
+
+ let showAfterPaste = function () {
+ ok(!cutItem.hasAttribute("disabled"), "cut menuitem is enabled after paste");
+ ok(!pasteItem.hasAttribute("disabled"), "paste menuitem is enabled after paste");
+ closeMenu(hideAfterPaste);
+ };
+
+ let hideAfterPaste = function () {
+ if (pass == 0) {
+ pass++;
+ testContextMenu();
+ } else {
+ Services.prefs.clearUserPref(DEVTOOLS_CHROME_ENABLED);
+ finish();
+ }
+ };
+
+ let testContextMenu = function () {
+ info("will test the context menu");
+
+ editMenu = null;
+ isContextMenu = true;
+
+ menuPopup = doc.getElementById("scratchpad-text-popup");
+ ok(menuPopup, "the context menupopup");
+ cutItem = doc.getElementById("cMenu_cut");
+ ok(cutItem, "the Cut menuitem");
+ pasteItem = doc.getElementById("cMenu_paste");
+ ok(pasteItem, "the Paste menuitem");
+
+ sp.setText("bug 699130: hello world! (context menu)");
+ openMenu(10, 10, firstShow);
+ };
+ waitForFocus(function () {
+ WebConsoleUtils.usageCount = 0;
+ EventUtils.synthesizeKey("v", {accelKey: true}, gScratchpadWindow);
+ testSelfXss(oldVal);
+ }, gScratchpadWindow);
+}
diff --git a/devtools/client/scratchpad/test/browser_scratchpad_eval_func.js b/devtools/client/scratchpad/test/browser_scratchpad_eval_func.js
new file mode 100644
index 000000000..3753b5a35
--- /dev/null
+++ b/devtools/client/scratchpad/test/browser_scratchpad_eval_func.js
@@ -0,0 +1,86 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test()
+{
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
+ openScratchpad(runTests);
+ }, true);
+
+ content.location = "data:text/html;charset=utf8,test Scratchpad eval function.";
+}
+
+function reportErrorAndQuit(error) {
+ DevToolsUtils.reportException("browser_scratchpad_eval_func.js", error);
+ ok(false);
+ finish();
+}
+
+function runTests(sw)
+{
+ const sp = sw.Scratchpad;
+
+ let foo = "" + function main() { console.log(1); };
+ let bar = "var bar = " + (() => { console.log(2); });
+
+ const fullText =
+ foo + "\n" +
+ "\n" +
+ bar + "\n";
+
+ sp.setText(fullText);
+
+ // On the function declaration.
+ sp.editor.setCursor({ line: 0, ch: 18 });
+ sp.evalTopLevelFunction()
+ .then(([text, error, result]) => {
+ is(text, foo, "Should re-eval foo.");
+ ok(!error, "Should not have got an error.");
+ ok(result, "Should have got a result.");
+ })
+
+ // On the arrow function.
+ .then(() => {
+ sp.editor.setCursor({ line: 2, ch: 18 });
+ return sp.evalTopLevelFunction();
+ })
+ .then(([text, error, result]) => {
+ is(text, bar.replace("var ", ""), "Should re-eval bar.");
+ ok(!error, "Should not have got an error.");
+ ok(result, "Should have got a result.");
+ })
+
+ // On the empty line.
+ .then(() => {
+ sp.editor.setCursor({ line: 1, ch: 0 });
+ return sp.evalTopLevelFunction();
+ })
+ .then(([text, error, result]) => {
+ is(text, fullText,
+ "Should get full text back since we didn't find a specific function.");
+ ok(!error, "Should not have got an error.");
+ ok(!result, "Should not have got a result.");
+ })
+
+ // Syntax error.
+ .then(() => {
+ sp.setText("function {}");
+ sp.editor.setCursor({ line: 0, ch: 9 });
+ return sp.evalTopLevelFunction();
+ })
+ .then(([text, error, result]) => {
+ is(text, "function {}",
+ "Should get the full text back since there was a parse error.");
+ ok(!error, "Should not have got an error");
+ ok(!result, "Should not have got a result");
+ ok(sp.getText().includes("SyntaxError"),
+ "We should have written the syntax error to the scratchpad.");
+ })
+
+ .then(finish, reportErrorAndQuit);
+}
diff --git a/devtools/client/scratchpad/test/browser_scratchpad_execute_print.js b/devtools/client/scratchpad/test/browser_scratchpad_execute_print.js
new file mode 100644
index 000000000..029916507
--- /dev/null
+++ b/devtools/client/scratchpad/test/browser_scratchpad_execute_print.js
@@ -0,0 +1,116 @@
+/* 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";
+
+function test() {
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onTabLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", onTabLoad, true);
+ openScratchpad(runTests);
+ }, true);
+
+ content.location = "data:text/html,<p>test run() and display() in Scratchpad";
+}
+
+function runTests() {
+ let sp = gScratchpadWindow.Scratchpad;
+ let tests = [{
+ method: "run",
+ prepare: function* () {
+ yield inContent(function* () {
+ content.wrappedJSObject.foobarBug636725 = 1;
+ });
+ sp.editor.setText("++window.foobarBug636725");
+ },
+ then: function* ([code, , result]) {
+ is(code, sp.getText(), "code is correct");
+
+ let pageResult = yield inContent(function* () {
+ return content.wrappedJSObject.foobarBug636725;
+ });
+ is(result, pageResult,
+ "result is correct");
+
+ is(sp.getText(), "++window.foobarBug636725",
+ "run() does not change the editor content");
+
+ is(pageResult, 2, "run() updated window.foobarBug636725");
+ }
+ }, {
+ method: "display",
+ prepare: function* () {},
+ then: function* () {
+ let pageResult = yield inContent(function* () {
+ return content.wrappedJSObject.foobarBug636725;
+ });
+ is(pageResult, 3, "display() updated window.foobarBug636725");
+
+ is(sp.getText(), "++window.foobarBug636725\n/*\n3\n*/",
+ "display() shows evaluation result in the textbox");
+
+ is(sp.editor.getSelection(), "\n/*\n3\n*/", "getSelection is correct");
+ }
+ }, {
+ method: "run",
+ prepare: function* () {
+ sp.editor.setText("window.foobarBug636725 = 'a';\n" +
+ "window.foobarBug636725 = 'b';");
+ sp.editor.setSelection({ line: 0, ch: 0 }, { line: 0, ch: 29 });
+ },
+ then: function* ([code, , result]) {
+ is(code, "window.foobarBug636725 = 'a';", "code is correct");
+ is(result, "a", "result is correct");
+
+ is(sp.getText(), "window.foobarBug636725 = 'a';\n" +
+ "window.foobarBug636725 = 'b';",
+ "run() does not change the textbox value");
+
+ let pageResult = yield inContent(function* () {
+ return content.wrappedJSObject.foobarBug636725;
+ });
+ is(pageResult, "a", "run() worked for the selected range");
+ }
+ }, {
+ method: "display",
+ prepare: function* () {
+ sp.editor.setText("window.foobarBug636725 = 'c';\n" +
+ "window.foobarBug636725 = 'b';");
+ sp.editor.setSelection({ line: 0, ch: 0 }, { line: 0, ch: 22 });
+ },
+ then: function* () {
+ let pageResult = yield inContent(function* () {
+ return content.wrappedJSObject.foobarBug636725;
+ });
+ is(pageResult, "a", "display() worked for the selected range");
+
+ is(sp.getText(), "window.foobarBug636725" +
+ "\n/*\na\n*/" +
+ " = 'c';\n" +
+ "window.foobarBug636725 = 'b';",
+ "display() shows evaluation result in the textbox");
+
+ is(sp.editor.getSelection(), "\n/*\na\n*/", "getSelection is correct");
+ }
+ }];
+
+ runAsyncCallbackTests(sp, tests).then(function () {
+ ok(sp.editor.somethingSelected(), "something is selected");
+ sp.editor.dropSelection();
+ ok(!sp.editor.somethingSelected(), "something is no longer selected");
+ ok(!sp.editor.getSelection(), "getSelection is empty");
+
+ // Test undo/redo.
+ sp.editor.setText("foo1");
+ sp.editor.setText("foo2");
+ is(sp.getText(), "foo2", "editor content updated");
+ sp.undo();
+ is(sp.getText(), "foo1", "undo() works");
+ sp.redo();
+ is(sp.getText(), "foo2", "redo() works");
+
+ finish();
+ });
+}
diff --git a/devtools/client/scratchpad/test/browser_scratchpad_falsy.js b/devtools/client/scratchpad/test/browser_scratchpad_falsy.js
new file mode 100644
index 000000000..9eb7efb94
--- /dev/null
+++ b/devtools/client/scratchpad/test/browser_scratchpad_falsy.js
@@ -0,0 +1,69 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* Bug 679467 */
+
+function test()
+{
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
+ openScratchpad(testFalsy);
+ }, true);
+
+ content.location = "data:text/html,<p>test falsy display() values in Scratchpad";
+}
+
+function testFalsy()
+{
+ let scratchpad = gScratchpadWindow.Scratchpad;
+ verifyFalsies(scratchpad).then(function () {
+ scratchpad.setBrowserContext();
+ verifyFalsies(scratchpad).then(finish);
+ });
+}
+
+
+function verifyFalsies(scratchpad)
+{
+ let tests = [{
+ method: "display",
+ code: "undefined",
+ result: "undefined\n/*\nundefined\n*/",
+ label: "undefined is displayed"
+ },
+ {
+ method: "display",
+ code: "false",
+ result: "false\n/*\nfalse\n*/",
+ label: "false is displayed"
+ },
+ {
+ method: "display",
+ code: "0",
+ result: "0\n/*\n0\n*/",
+ label: "0 is displayed"
+ },
+ {
+ method: "display",
+ code: "null",
+ result: "null\n/*\nnull\n*/",
+ label: "null is displayed"
+ },
+ {
+ method: "display",
+ code: "NaN",
+ result: "NaN\n/*\nNaN\n*/",
+ label: "NaN is displayed"
+ },
+ {
+ method: "display",
+ code: "''",
+ result: "''\n/*\n\n*/",
+ label: "the empty string is displayed"
+ }];
+
+ return runAsyncTests(scratchpad, tests);
+}
diff --git a/devtools/client/scratchpad/test/browser_scratchpad_files.js b/devtools/client/scratchpad/test/browser_scratchpad_files.js
new file mode 100644
index 000000000..d52718a81
--- /dev/null
+++ b/devtools/client/scratchpad/test/browser_scratchpad_files.js
@@ -0,0 +1,119 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Reference to the Scratchpad object.
+var gScratchpad;
+
+// Reference to the temporary nsIFile we will work with.
+var gFile;
+
+// The temporary file content.
+var gFileContent = "hello.world('bug636725');";
+
+function test()
+{
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
+ openScratchpad(runTests);
+ }, true);
+
+ content.location = "data:text/html,<p>test file open and save in Scratchpad";
+}
+
+function runTests()
+{
+ gScratchpad = gScratchpadWindow.Scratchpad;
+
+ createTempFile("fileForBug636725.tmp", gFileContent, function (aStatus, aFile) {
+ ok(Components.isSuccessCode(aStatus),
+ "The temporary file was saved successfully");
+
+ gFile = aFile;
+ gScratchpad.importFromFile(gFile.QueryInterface(Ci.nsILocalFile), true,
+ fileImported);
+ });
+}
+
+function fileImported(aStatus, aFileContent)
+{
+ ok(Components.isSuccessCode(aStatus),
+ "the temporary file was imported successfully with Scratchpad");
+
+ is(aFileContent, gFileContent,
+ "received data is correct");
+
+ is(gScratchpad.getText(), gFileContent,
+ "the editor content is correct");
+
+ is(gScratchpad.dirty, false,
+ "the editor marks imported file as saved");
+
+ // Save the file after changes.
+ gFileContent += "// omg, saved!";
+ gScratchpad.editor.setText(gFileContent);
+
+ gScratchpad.exportToFile(gFile.QueryInterface(Ci.nsILocalFile), true, true,
+ fileExported);
+}
+
+function fileExported(aStatus)
+{
+ ok(Components.isSuccessCode(aStatus),
+ "the temporary file was exported successfully with Scratchpad");
+
+ let oldContent = gFileContent;
+
+ // Attempt another file save, with confirmation which returns false.
+ gFileContent += "// omg, saved twice!";
+ gScratchpad.editor.setText(gFileContent);
+
+ let oldConfirm = gScratchpadWindow.confirm;
+ let askedConfirmation = false;
+ gScratchpadWindow.confirm = function () {
+ askedConfirmation = true;
+ return false;
+ };
+
+ gScratchpad.exportToFile(gFile.QueryInterface(Ci.nsILocalFile), false, true,
+ fileExported2);
+
+ gScratchpadWindow.confirm = oldConfirm;
+
+ ok(askedConfirmation, "exportToFile() asked for overwrite confirmation");
+
+ gFileContent = oldContent;
+
+ let channel = NetUtil.newChannel({
+ uri: NetUtil.newURI(gFile),
+ loadUsingSystemPrincipal: true});
+ channel.contentType = "application/javascript";
+
+ // Read back the temporary file.
+ NetUtil.asyncFetch(channel, fileRead);
+}
+
+function fileExported2()
+{
+ ok(false, "exportToFile() did not cancel file overwrite");
+}
+
+function fileRead(aInputStream, aStatus)
+{
+ ok(Components.isSuccessCode(aStatus),
+ "the temporary file was read back successfully");
+
+ let updatedContent =
+ NetUtil.readInputStreamToString(aInputStream, aInputStream.available());
+
+ is(updatedContent, gFileContent, "file properly updated");
+
+ // Done!
+ gFile.remove(false);
+ gFile = null;
+ gScratchpad = null;
+ finish();
+}
diff --git a/devtools/client/scratchpad/test/browser_scratchpad_goto_line_ui.js b/devtools/client/scratchpad/test/browser_scratchpad_goto_line_ui.js
new file mode 100644
index 000000000..34c71ac0c
--- /dev/null
+++ b/devtools/client/scratchpad/test/browser_scratchpad_goto_line_ui.js
@@ -0,0 +1,43 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* Bug 714942 */
+
+function test()
+{
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function browserLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", browserLoad, true);
+ openScratchpad(runTests);
+ }, true);
+
+ content.location = "data:text/html,<p>test the 'Jump to line' feature in Scratchpad";
+}
+
+function runTests(aWindow, aScratchpad)
+{
+ let editor = aScratchpad.editor;
+ let text = "foobar bug650345\nBug650345 bazbaz\nfoobar omg\ntest";
+ editor.setText(text);
+ editor.setCursor({ line: 0, ch: 0 });
+
+ let oldPrompt = editor.openDialog;
+ let desiredValue;
+
+ editor.openDialog = function (text, cb) {
+ cb(desiredValue);
+ };
+
+ desiredValue = 3;
+ EventUtils.synthesizeKey("J", {accelKey: true}, aWindow);
+ is(editor.getCursor().line, 2, "line is correct");
+
+ desiredValue = 2;
+ EventUtils.synthesizeKey("J", {accelKey: true}, aWindow);
+ is(editor.getCursor().line, 1, "line is correct (again)");
+
+ editor.openDialog = oldPrompt;
+ finish();
+}
diff --git a/devtools/client/scratchpad/test/browser_scratchpad_help_key.js b/devtools/client/scratchpad/test/browser_scratchpad_help_key.js
new file mode 100644
index 000000000..d5383db57
--- /dev/null
+++ b/devtools/client/scratchpad/test/browser_scratchpad_help_key.js
@@ -0,0 +1,59 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* Bug 650760 */
+
+function test()
+{
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ content.location = "data:text/html,Test keybindings for opening Scratchpad MDN Documentation, bug 650760";
+ gBrowser.selectedBrowser.addEventListener("load", function onTabLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", onTabLoad, true);
+
+ openScratchpad(runTest);
+ }, true);
+}
+
+function runTest()
+{
+ let sp = gScratchpadWindow.Scratchpad;
+ ok(sp, "Scratchpad object exists in new window");
+ ok(sp.editor.hasFocus(), "the editor has focus");
+
+ let keyid = gScratchpadWindow.document.getElementById("key_openHelp");
+ let modifiers = keyid.getAttribute("modifiers");
+
+ let key = null;
+ if (keyid.getAttribute("keycode"))
+ key = keyid.getAttribute("keycode");
+
+ else if (keyid.getAttribute("key"))
+ key = keyid.getAttribute("key");
+
+ isnot(key, null, "Successfully retrieved keycode/key");
+
+ var aEvent = {
+ shiftKey: modifiers.match("shift"),
+ ctrlKey: modifiers.match("ctrl"),
+ altKey: modifiers.match("alt"),
+ metaKey: modifiers.match("meta"),
+ accelKey: modifiers.match("accel")
+ };
+
+ info("check that the MDN page is opened on \"F1\"");
+ let linkClicked = false;
+ sp.openDocumentationPage = function (event) { linkClicked = true; };
+
+ EventUtils.synthesizeKey(key, aEvent, gScratchpadWindow);
+
+ is(linkClicked, true, "MDN page will open");
+ finishTest();
+}
+
+function finishTest()
+{
+ gScratchpadWindow.close();
+ finish();
+}
diff --git a/devtools/client/scratchpad/test/browser_scratchpad_initialization.js b/devtools/client/scratchpad/test/browser_scratchpad_initialization.js
new file mode 100644
index 000000000..387bcb08c
--- /dev/null
+++ b/devtools/client/scratchpad/test/browser_scratchpad_initialization.js
@@ -0,0 +1,50 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var DEVTOOLS_CHROME_ENABLED = "devtools.chrome.enabled";
+
+function test()
+{
+ waitForExplicitFinish();
+
+ Services.prefs.setBoolPref(DEVTOOLS_CHROME_ENABLED, false);
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
+
+ openScratchpad(runTests);
+ }, true);
+
+ content.location = "data:text/html,initialization test for Scratchpad";
+}
+
+function runTests()
+{
+ let sp = gScratchpadWindow.Scratchpad;
+ ok(sp, "Scratchpad object exists in new window");
+ is(typeof sp.run, "function", "Scratchpad.run() exists");
+ is(typeof sp.inspect, "function", "Scratchpad.inspect() exists");
+ is(typeof sp.display, "function", "Scratchpad.display() exists");
+
+ let environmentMenu = gScratchpadWindow.document.
+ getElementById("sp-environment-menu");
+ ok(environmentMenu, "Environment menu element exists");
+ ok(environmentMenu.hasAttribute("hidden"),
+ "Environment menu is not visible");
+
+ let errorConsoleCommand = gScratchpadWindow.document.
+ getElementById("sp-cmd-errorConsole");
+ ok(errorConsoleCommand, "Error console command element exists");
+ is(errorConsoleCommand.getAttribute("disabled"), "true",
+ "Error console command is disabled");
+
+ let chromeContextCommand = gScratchpadWindow.document.
+ getElementById("sp-cmd-browserContext");
+ ok(chromeContextCommand, "Chrome context command element exists");
+ is(chromeContextCommand.getAttribute("disabled"), "true",
+ "Chrome context command is disabled");
+
+ Services.prefs.clearUserPref(DEVTOOLS_CHROME_ENABLED);
+ finish();
+}
diff --git a/devtools/client/scratchpad/test/browser_scratchpad_inspect.js b/devtools/client/scratchpad/test/browser_scratchpad_inspect.js
new file mode 100644
index 000000000..23194f05e
--- /dev/null
+++ b/devtools/client/scratchpad/test/browser_scratchpad_inspect.js
@@ -0,0 +1,55 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test()
+{
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
+ openScratchpad(runTests);
+ }, true);
+
+ content.location = "data:text/html;charset=utf8,<p>test inspect() in Scratchpad</p>";
+}
+
+function runTests()
+{
+ let sp = gScratchpadWindow.Scratchpad;
+
+ sp.setText("({ a: 'foobarBug636725' })");
+
+ sp.inspect().then(function () {
+ let sidebar = sp.sidebar;
+ ok(sidebar.visible, "sidebar is open");
+
+
+ let found = false;
+
+ outer: for (let scope of sidebar.variablesView) {
+ for (let [, obj] of scope) {
+ for (let [, prop] of obj) {
+ if (prop.name == "a" && prop.value == "foobarBug636725") {
+ found = true;
+ break outer;
+ }
+ }
+ }
+ }
+
+ ok(found, "found the property");
+
+ let tabbox = sidebar._sidebar._tabbox;
+ is(tabbox.width, 300, "Scratchpad sidebar width is correct");
+ ok(!tabbox.hasAttribute("hidden"), "Scratchpad sidebar visible");
+ sidebar.hide();
+ ok(tabbox.hasAttribute("hidden"), "Scratchpad sidebar hidden");
+ sp.inspect().then(function () {
+ is(tabbox.width, 300, "Scratchpad sidebar width is still correct");
+ ok(!tabbox.hasAttribute("hidden"), "Scratchpad sidebar visible again");
+ finish();
+ });
+ });
+}
diff --git a/devtools/client/scratchpad/test/browser_scratchpad_inspect_primitives.js b/devtools/client/scratchpad/test/browser_scratchpad_inspect_primitives.js
new file mode 100644
index 000000000..914f0a6d8
--- /dev/null
+++ b/devtools/client/scratchpad/test/browser_scratchpad_inspect_primitives.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that inspecting primitive values uses the object inspector, not an
+// inline comment.
+
+var {Task} = require("devtools/shared/task");
+
+function test() {
+ const options = {
+ tabContent: "test inspecting primitive values"
+ };
+ openTabAndScratchpad(options)
+ .then(Task.async(runTests))
+ .then(finish, console.error);
+}
+
+function* runTests([win, sp]) {
+ // Inspect a number.
+ yield checkResults(sp, 7);
+
+ // Inspect a string.
+ yield checkResults(sp, "foobar", true);
+
+ // Inspect a boolean.
+ yield checkResults(sp, true);
+}
+
+// Helper function that does the actual testing.
+var checkResults = Task.async(function* (sp, value, isString = false) {
+ let sourceValue = value;
+ if (isString) {
+ sourceValue = '"' + value + '"';
+ }
+ let source = "var foobar = " + sourceValue + "; foobar";
+ sp.setText(source);
+ yield sp.inspect();
+
+ let sidebar = sp.sidebar;
+ ok(sidebar.visible, "sidebar is open");
+
+ let found = false;
+
+ outer: for (let scope of sidebar.variablesView) {
+ for (let [, obj] of scope) {
+ for (let [, prop] of obj) {
+ if (prop.name == "value" && prop.value == value) {
+ found = true;
+ break outer;
+ }
+ }
+ }
+ }
+
+ ok(found, "found the value of " + value);
+
+ let tabbox = sidebar._sidebar._tabbox;
+ ok(!tabbox.hasAttribute("hidden"), "Scratchpad sidebar visible");
+ sidebar.hide();
+ ok(tabbox.hasAttribute("hidden"), "Scratchpad sidebar hidden");
+});
diff --git a/devtools/client/scratchpad/test/browser_scratchpad_long_string.js b/devtools/client/scratchpad/test/browser_scratchpad_long_string.js
new file mode 100644
index 000000000..d85a7df1c
--- /dev/null
+++ b/devtools/client/scratchpad/test/browser_scratchpad_long_string.js
@@ -0,0 +1,30 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test()
+{
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
+ openScratchpad(runTests);
+ }, true);
+
+ content.location = "data:text/html;charset=utf8,<p>test long string in Scratchpad</p>";
+}
+
+function runTests()
+{
+ let sp = gScratchpadWindow.Scratchpad;
+
+ sp.setText("'0'.repeat(10000)");
+
+ sp.display().then(() => {
+ is(sp.getText(), "'0'.repeat(10000)\n" +
+ "/*\n" + "0".repeat(10000) + "\n*/",
+ "display()ing a long string works");
+ finish();
+ });
+}
diff --git a/devtools/client/scratchpad/test/browser_scratchpad_modeline.js b/devtools/client/scratchpad/test/browser_scratchpad_modeline.js
new file mode 100644
index 000000000..20a4e8449
--- /dev/null
+++ b/devtools/client/scratchpad/test/browser_scratchpad_modeline.js
@@ -0,0 +1,87 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* Bug 644413 */
+
+var gScratchpad; // Reference to the Scratchpad object.
+var gFile; // Reference to the temporary nsIFile we will work with.
+var DEVTOOLS_CHROME_ENABLED = "devtools.chrome.enabled";
+
+// The temporary file content.
+var gFileContent = "function main() { return 0; }";
+
+function test() {
+ waitForExplicitFinish();
+
+ Services.prefs.setBoolPref(DEVTOOLS_CHROME_ENABLED, false);
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
+ openScratchpad(runTests);
+ }, true);
+
+ content.location = "data:text/html,<p>test file open and save in Scratchpad";
+}
+
+function runTests() {
+ gScratchpad = gScratchpadWindow.Scratchpad;
+ function size(obj) { return Object.keys(obj).length; }
+
+ // Test Scratchpad._scanModeLine method.
+ let obj = gScratchpad._scanModeLine();
+ is(size(obj), 0, "Mode-line object has no properties");
+
+ obj = gScratchpad._scanModeLine("/* This is not a mode-line comment */");
+ is(size(obj), 0, "Mode-line object has no properties");
+
+ obj = gScratchpad._scanModeLine("/* -sp-context:browser */");
+ is(size(obj), 1, "Mode-line object has one property");
+ is(obj["-sp-context"], "browser");
+
+ obj = gScratchpad._scanModeLine("/* -sp-context: browser */");
+ is(size(obj), 1, "Mode-line object has one property");
+ is(obj["-sp-context"], "browser");
+
+ obj = gScratchpad._scanModeLine("// -sp-context: browser");
+ is(size(obj), 1, "Mode-line object has one property");
+ is(obj["-sp-context"], "browser");
+
+ obj = gScratchpad._scanModeLine("/* -sp-context:browser, other:true */");
+ is(size(obj), 2, "Mode-line object has two properties");
+ is(obj["-sp-context"], "browser");
+ is(obj["other"], "true");
+
+ // Test importing files with a mode-line in them.
+ let content = "/* -sp-context:browser */\n" + gFileContent;
+ createTempFile("fileForBug644413.tmp", content, function (aStatus, aFile) {
+ ok(Components.isSuccessCode(aStatus), "File was saved successfully");
+
+ gFile = aFile;
+ gScratchpad.importFromFile(gFile.QueryInterface(Ci.nsILocalFile), true, fileImported);
+ });
+}
+
+function fileImported(status, content) {
+ ok(Components.isSuccessCode(status), "File was imported successfully");
+
+ // Since devtools.chrome.enabled is off, Scratchpad should still be in
+ // the content context.
+ is(gScratchpad.executionContext, gScratchpadWindow.SCRATCHPAD_CONTEXT_CONTENT);
+
+ // Set the pref and try again.
+ Services.prefs.setBoolPref(DEVTOOLS_CHROME_ENABLED, true);
+
+ gScratchpad.importFromFile(gFile.QueryInterface(Ci.nsILocalFile), true, function (status, content) {
+ ok(Components.isSuccessCode(status), "File was imported successfully");
+ is(gScratchpad.executionContext, gScratchpadWindow.SCRATCHPAD_CONTEXT_BROWSER);
+
+ gFile.remove(false);
+ gFile = null;
+ gScratchpad = null;
+ finish();
+ });
+}
+
+registerCleanupFunction(function () {
+ Services.prefs.clearUserPref(DEVTOOLS_CHROME_ENABLED);
+});
diff --git a/devtools/client/scratchpad/test/browser_scratchpad_open.js b/devtools/client/scratchpad/test/browser_scratchpad_open.js
new file mode 100644
index 000000000..ec55f0101
--- /dev/null
+++ b/devtools/client/scratchpad/test/browser_scratchpad_open.js
@@ -0,0 +1,101 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// only finish() when correct number of tests are done
+const expected = 4;
+var count = 0;
+var lastUniqueName = null;
+
+function done()
+{
+ if (++count == expected) {
+ finish();
+ }
+}
+
+function test()
+{
+ waitForExplicitFinish();
+ testOpen();
+ testOpenWithState();
+ testOpenInvalidState();
+ testOpenTestFile();
+}
+
+function testUniqueName(name)
+{
+ ok(name, "Scratchpad has a uniqueName");
+
+ if (lastUniqueName === null) {
+ lastUniqueName = name;
+ return;
+ }
+
+ ok(name !== lastUniqueName,
+ "Unique name for this instance differs from the last one.");
+}
+
+function testOpen()
+{
+ openScratchpad(function (win) {
+ is(win.Scratchpad.filename, undefined, "Default filename is undefined");
+ isnot(win.Scratchpad.getText(), null, "Default text should not be null");
+ is(win.Scratchpad.executionContext, win.SCRATCHPAD_CONTEXT_CONTENT,
+ "Default execution context is content");
+ testUniqueName(win.Scratchpad.uniqueName);
+
+ win.close();
+ done();
+ }, {noFocus: true});
+}
+
+function testOpenWithState()
+{
+ let state = {
+ filename: "testfile",
+ executionContext: 2,
+ text: "test text"
+ };
+
+ openScratchpad(function (win) {
+ is(win.Scratchpad.filename, state.filename, "Filename loaded from state");
+ is(win.Scratchpad.executionContext, state.executionContext, "Execution context loaded from state");
+ is(win.Scratchpad.getText(), state.text, "Content loaded from state");
+ testUniqueName(win.Scratchpad.uniqueName);
+
+ win.close();
+ done();
+ }, {state: state, noFocus: true});
+}
+
+function testOpenInvalidState()
+{
+ let win = openScratchpad(null, {state: 7});
+ ok(!win, "no scratchpad opened if state is not an object");
+ done();
+}
+
+function testOpenTestFile()
+{
+ let win = openScratchpad(function (win) {
+ ok(win, "scratchpad opened for file open");
+ try {
+ win.Scratchpad.importFromFile(
+ "http://example.com/browser/devtools/client/scratchpad/test/NS_ERROR_ILLEGAL_INPUT.txt",
+ "silent",
+ function (aStatus, content) {
+ let nb = win.document.querySelector("#scratchpad-notificationbox");
+ is(nb.querySelectorAll("notification").length, 1, "There is just one notification");
+ let cn = nb.currentNotification;
+ is(cn.priority, nb.PRIORITY_WARNING_HIGH, "notification priority is correct");
+ is(cn.value, "file-import-convert-failed", "notification value is corrent");
+ is(cn.type, "warning", "notification type is correct");
+ done();
+ });
+ ok(true, "importFromFile does not cause exception");
+ } catch (exception) {
+ ok(false, "importFromFile causes exception " + DevToolsUtils.safeErrorString(exception));
+ }
+ }, {noFocus: true});
+}
diff --git a/devtools/client/scratchpad/test/browser_scratchpad_open_error_console.js b/devtools/client/scratchpad/test/browser_scratchpad_open_error_console.js
new file mode 100644
index 000000000..4da2a2daf
--- /dev/null
+++ b/devtools/client/scratchpad/test/browser_scratchpad_open_error_console.js
@@ -0,0 +1,39 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const HUDService = require("devtools/client/webconsole/hudservice");
+
+function test()
+{
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
+ openScratchpad(runTests);
+ }, true);
+
+ content.location = "data:text/html;charset=utf8,test Scratchpad." +
+ "openErrorConsole()";
+}
+
+function runTests()
+{
+ Services.obs.addObserver(function observer(aSubject) {
+ Services.obs.removeObserver(observer, "web-console-created");
+ aSubject.QueryInterface(Ci.nsISupportsString);
+
+ let hud = HUDService.getBrowserConsole();
+ ok(hud, "browser console is open");
+ is(aSubject.data, hud.hudId, "notification hudId is correct");
+
+ HUDService.toggleBrowserConsole().then(finish);
+ }, "web-console-created", false);
+
+ let hud = HUDService.getBrowserConsole();
+ ok(!hud, "browser console is not open");
+ info("wait for the browser console to open from Scratchpad");
+
+ gScratchpadWindow.Scratchpad.openErrorConsole();
+}
diff --git a/devtools/client/scratchpad/test/browser_scratchpad_pprint-02.js b/devtools/client/scratchpad/test/browser_scratchpad_pprint-02.js
new file mode 100644
index 000000000..c7cd2927e
--- /dev/null
+++ b/devtools/client/scratchpad/test/browser_scratchpad_pprint-02.js
@@ -0,0 +1,40 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test()
+{
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
+ openScratchpad(runTests);
+ }, true);
+
+ content.location = "data:text/html;charset=utf8,test Scratchpad pretty print.";
+}
+
+var gTabsize;
+
+function runTests(sw)
+{
+ gTabsize = Services.prefs.getIntPref("devtools.editor.tabsize");
+ Services.prefs.setIntPref("devtools.editor.tabsize", 6);
+ const space = " ".repeat(6);
+
+ const sp = sw.Scratchpad;
+ sp.setText("function main() { console.log(5); }");
+ sp.prettyPrint().then(() => {
+ const prettyText = sp.getText();
+ ok(prettyText.includes(space));
+ finish();
+ }).then(null, error => {
+ ok(false, error);
+ });
+}
+
+registerCleanupFunction(function () {
+ Services.prefs.setIntPref("devtools.editor.tabsize", gTabsize);
+ gTabsize = null;
+});
diff --git a/devtools/client/scratchpad/test/browser_scratchpad_pprint.js b/devtools/client/scratchpad/test/browser_scratchpad_pprint.js
new file mode 100644
index 000000000..1ba9a2cda
--- /dev/null
+++ b/devtools/client/scratchpad/test/browser_scratchpad_pprint.js
@@ -0,0 +1,29 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test()
+{
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
+ openScratchpad(runTests);
+ }, true);
+
+ content.location = "data:text/html;charset=utf8,test Scratchpad pretty print.";
+}
+
+function runTests(sw)
+{
+ const sp = sw.Scratchpad;
+ sp.setText("function main() { console.log(5); }");
+ sp.prettyPrint().then(() => {
+ const prettyText = sp.getText();
+ ok(prettyText.includes("\n"));
+ finish();
+ }).then(null, error => {
+ ok(false, error);
+ });
+}
diff --git a/devtools/client/scratchpad/test/browser_scratchpad_pprint_error_goto_line.js b/devtools/client/scratchpad/test/browser_scratchpad_pprint_error_goto_line.js
new file mode 100644
index 000000000..21f266f61
--- /dev/null
+++ b/devtools/client/scratchpad/test/browser_scratchpad_pprint_error_goto_line.js
@@ -0,0 +1,78 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2; fill-column: 80 -*- */
+/* 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";
+
+function test()
+{
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
+ openScratchpad(runTests);
+ }, true);
+
+ content.location = "data:text/html;charset=utf8,"
+ + "test Scratchpad pretty print error goto line.";
+}
+
+function testJumpToPrettyPrintError(sp, error, remark) {
+ info("will test jumpToLine after prettyPrint error" + remark);
+
+ // CodeMirror lines and columns are 0-based, Scratchpad UI and error
+ // stack are 1-based.
+ is(/Invalid regular expression flag \(3:10\)/.test(error), true,
+ "prettyPrint expects error in editor text:\n" + error);
+
+ sp.editor.jumpToLine();
+
+ const editorDoc = sp.editor.container.contentDocument;
+ const lineInput = editorDoc.querySelector("input");
+ const errorLocation = lineInput.value;
+ const [ inputLine, inputColumn ] = errorLocation.split(":");
+ const errorLine = 3, errorColumn = 10;
+
+ is(inputLine, errorLine,
+ "jumpToLine input field is set from editor selection (line)");
+ is(inputColumn, errorColumn,
+ "jumpToLine input field is set from editor selection (column)");
+
+ EventUtils.synthesizeKey("VK_RETURN", { }, editorDoc.defaultView);
+
+ // CodeMirror lines and columns are 0-based, Scratchpad UI and error
+ // stack are 1-based.
+ const cursor = sp.editor.getCursor();
+ is(inputLine, cursor.line + 1, "jumpToLine goto error location (line)");
+ is(inputColumn, cursor.ch + 1, "jumpToLine goto error location (column)");
+}
+
+function runTests(sw, sp)
+{
+ sp.setText([
+ "// line 1",
+ "// line 2",
+ "var re = /a bad /regexp/; // line 3 is an obvious syntax error!",
+ "// line 4",
+ "// line 5",
+ ""
+ ].join("\n"));
+
+ sp.prettyPrint().then(aFulfill => {
+ ok(false, "Expecting Invalid regexp flag (3:10)");
+ finish();
+ }, error => {
+ testJumpToPrettyPrintError(sp, error, " (Bug 1005471, first time)");
+ });
+
+ sp.prettyPrint().then(aFulfill => {
+ ok(false, "Expecting Invalid regexp flag (3:10)");
+ finish();
+ }, error => {
+ // Second time verifies bug in earlier implementation fixed.
+ testJumpToPrettyPrintError(sp, error, " (second time)");
+ finish();
+ });
+}
diff --git a/devtools/client/scratchpad/test/browser_scratchpad_recent_files.js b/devtools/client/scratchpad/test/browser_scratchpad_recent_files.js
new file mode 100644
index 000000000..66a4e7cd1
--- /dev/null
+++ b/devtools/client/scratchpad/test/browser_scratchpad_recent_files.js
@@ -0,0 +1,350 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* Bug 651942 */
+
+// Reference to the Scratchpad object.
+var gScratchpad;
+
+// References to the temporary nsIFiles.
+var gFile01;
+var gFile02;
+var gFile03;
+var gFile04;
+
+// lists of recent files.
+var lists = {
+ recentFiles01: null,
+ recentFiles02: null,
+ recentFiles03: null,
+ recentFiles04: null,
+};
+
+// Temporary file names.
+var gFileName01 = "file01_ForBug651942.tmp";
+var gFileName02 = "☕"; // See bug 783858 for more information
+var gFileName03 = "file03_ForBug651942.tmp";
+var gFileName04 = "file04_ForBug651942.tmp";
+
+// Content for the temporary files.
+var gFileContent;
+var gFileContent01 = "hello.world.01('bug651942');";
+var gFileContent02 = "hello.world.02('bug651942');";
+var gFileContent03 = "hello.world.03('bug651942');";
+var gFileContent04 = "hello.world.04('bug651942');";
+
+function startTest()
+{
+ gScratchpad = gScratchpadWindow.Scratchpad;
+
+ gFile01 = createAndLoadTemporaryFile(gFile01, gFileName01, gFileContent01);
+ gFile02 = createAndLoadTemporaryFile(gFile02, gFileName02, gFileContent02);
+ gFile03 = createAndLoadTemporaryFile(gFile03, gFileName03, gFileContent03);
+}
+
+// Test to see if the three files we created in the 'startTest()'-method have
+// been added to the list of recent files.
+function testAddedToRecent()
+{
+ lists.recentFiles01 = gScratchpad.getRecentFiles();
+
+ is(lists.recentFiles01.length, 3,
+ "Temporary files created successfully and added to list of recent files.");
+
+ // Create a 4th file, this should clear the oldest file.
+ gFile04 = createAndLoadTemporaryFile(gFile04, gFileName04, gFileContent04);
+}
+
+// We have opened a 4th file. Test to see if the oldest recent file was removed,
+// and that the other files were reordered successfully.
+function testOverwriteRecent()
+{
+ lists.recentFiles02 = gScratchpad.getRecentFiles();
+
+ is(lists.recentFiles02[0], lists.recentFiles01[1],
+ "File02 was reordered successfully in the 'recent files'-list.");
+ is(lists.recentFiles02[1], lists.recentFiles01[2],
+ "File03 was reordered successfully in the 'recent files'-list.");
+ isnot(lists.recentFiles02[2], lists.recentFiles01[2],
+ "File04: was added successfully.");
+
+ // Open the oldest recent file.
+ gScratchpad.openFile(0);
+}
+
+// We have opened the "oldest"-recent file. Test to see if it is now the most
+// recent file, and that the other files were reordered successfully.
+function testOpenOldestRecent()
+{
+ lists.recentFiles03 = gScratchpad.getRecentFiles();
+
+ is(lists.recentFiles02[0], lists.recentFiles03[2],
+ "File04 was reordered successfully in the 'recent files'-list.");
+ is(lists.recentFiles02[1], lists.recentFiles03[0],
+ "File03 was reordered successfully in the 'recent files'-list.");
+ is(lists.recentFiles02[2], lists.recentFiles03[1],
+ "File02 was reordered successfully in the 'recent files'-list.");
+
+ Services.prefs.setIntPref("devtools.scratchpad.recentFilesMax", 0);
+}
+
+// The "devtools.scratchpad.recentFilesMax"-preference was set to zero (0).
+// This should disable the "Open Recent"-menu by hiding it (this should not
+// remove any files from the list). Test to see if it's been hidden.
+function testHideMenu()
+{
+ let menu = gScratchpadWindow.document.getElementById("sp-open_recent-menu");
+ ok(menu.hasAttribute("hidden"), "The menu was hidden successfully.");
+
+ Services.prefs.setIntPref("devtools.scratchpad.recentFilesMax", 2);
+}
+
+// We have set the recentFilesMax-pref to one (1), this enables the feature,
+// removes the two oldest files, rebuilds the menu and removes the
+// "hidden"-attribute from it. Test to see if this works.
+function testChangedMaxRecent()
+{
+ let menu = gScratchpadWindow.document.getElementById("sp-open_recent-menu");
+ ok(!menu.hasAttribute("hidden"), "The menu is visible. \\o/");
+
+ lists.recentFiles04 = gScratchpad.getRecentFiles();
+
+ is(lists.recentFiles04.length, 2,
+ "Two recent files were successfully removed from the 'recent files'-list");
+
+ let doc = gScratchpadWindow.document;
+ let popup = doc.getElementById("sp-menu-open_recentPopup");
+
+ let menuitemLabel = popup.children[0].getAttribute("label");
+ let correctMenuItem = false;
+ if (menuitemLabel === lists.recentFiles03[2] &&
+ menuitemLabel === lists.recentFiles04[1]) {
+ correctMenuItem = true;
+ }
+
+ is(correctMenuItem, true,
+ "Two recent files were successfully removed from the 'Open Recent'-menu");
+
+ // We now remove one file from the harddrive and use the recent-menuitem for
+ // it to make sure the user is notified that the file no longer exists.
+ // This is tested in testOpenDeletedFile().
+ gFile04.remove(false);
+
+ // Make sure the file has been deleted before continuing to avoid
+ // intermittent oranges.
+ waitForFileDeletion();
+}
+
+function waitForFileDeletion() {
+ if (gFile04.exists()) {
+ executeSoon(waitForFileDeletion);
+ return;
+ }
+
+ gFile04 = null;
+ gScratchpad.openFile(0);
+}
+
+// By now we should have two recent files stored in the list but one of the
+// files should be missing on the harddrive.
+function testOpenDeletedFile() {
+ let doc = gScratchpadWindow.document;
+ let popup = doc.getElementById("sp-menu-open_recentPopup");
+
+ is(gScratchpad.getRecentFiles().length, 1,
+ "The missing file was successfully removed from the list.");
+ // The number of recent files stored, plus the separator and the
+ // clearRecentMenuItems-item.
+ is(popup.children.length, 3,
+ "The missing file was successfully removed from the menu.");
+ ok(gScratchpad.notificationBox.currentNotification,
+ "The notification was successfully displayed.");
+ is(gScratchpad.notificationBox.currentNotification.label,
+ gScratchpad.strings.GetStringFromName("fileNoLongerExists.notification"),
+ "The notification label is correct.");
+
+ gScratchpad.clearRecentFiles();
+}
+
+// We have cleared the last file. Test to see if the last file was removed,
+// the menu is empty and was disabled successfully.
+function testClearedAll()
+{
+ let doc = gScratchpadWindow.document;
+ let menu = doc.getElementById("sp-open_recent-menu");
+ let popup = doc.getElementById("sp-menu-open_recentPopup");
+
+ is(gScratchpad.getRecentFiles().length, 0,
+ "All recent files removed successfully.");
+ is(popup.children.length, 0, "All menuitems removed successfully.");
+ ok(menu.hasAttribute("disabled"),
+ "No files in the menu, it was disabled successfully.");
+
+ finishTest();
+}
+
+function createAndLoadTemporaryFile(aFile, aFileName, aFileContent)
+{
+ // Create a temporary file.
+ aFile = FileUtils.getFile("TmpD", [aFileName]);
+ aFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o666);
+
+ // Write the temporary file.
+ let fout = Cc["@mozilla.org/network/file-output-stream;1"].
+ createInstance(Ci.nsIFileOutputStream);
+ fout.init(aFile.QueryInterface(Ci.nsILocalFile), 0x02 | 0x08 | 0x20,
+ 0o644, fout.DEFER_OPEN);
+
+ gScratchpad.setFilename(aFile.path);
+ gScratchpad.importFromFile(aFile.QueryInterface(Ci.nsILocalFile), true,
+ fileImported);
+ gScratchpad.saveFile(fileSaved);
+
+ return aFile;
+}
+
+function fileImported(aStatus)
+{
+ ok(Components.isSuccessCode(aStatus),
+ "the temporary file was imported successfully with Scratchpad");
+}
+
+function fileSaved(aStatus)
+{
+ ok(Components.isSuccessCode(aStatus),
+ "the temporary file was saved successfully with Scratchpad");
+
+ checkIfMenuIsPopulated();
+}
+
+function checkIfMenuIsPopulated()
+{
+ let doc = gScratchpadWindow.document;
+ let expectedMenuitemCount = doc.getElementById("sp-menu-open_recentPopup").
+ children.length;
+ // The number of recent files stored, plus the separator and the
+ // clearRecentMenuItems-item.
+ let recentFilesPlusExtra = gScratchpad.getRecentFiles().length + 2;
+
+ if (expectedMenuitemCount > 2) {
+ is(expectedMenuitemCount, recentFilesPlusExtra,
+ "the recent files menu was populated successfully.");
+ }
+}
+
+/**
+ * The PreferenceObserver listens for preference changes while Scratchpad is
+ * running.
+ */
+var PreferenceObserver = {
+ _initialized: false,
+
+ _timesFired: 0,
+ set timesFired(aNewValue) {
+ this._timesFired = aNewValue;
+ },
+ get timesFired() {
+ return this._timesFired;
+ },
+
+ init: function PO_init()
+ {
+ if (this._initialized) {
+ return;
+ }
+
+ this.branch = Services.prefs.getBranch("devtools.scratchpad.");
+ this.branch.addObserver("", this, false);
+ this._initialized = true;
+ },
+
+ observe: function PO_observe(aMessage, aTopic, aData)
+ {
+ if (aTopic != "nsPref:changed") {
+ return;
+ }
+
+ switch (this.timesFired) {
+ case 0:
+ this.timesFired = 1;
+ break;
+ case 1:
+ this.timesFired = 2;
+ break;
+ case 2:
+ this.timesFired = 3;
+ testAddedToRecent();
+ break;
+ case 3:
+ this.timesFired = 4;
+ testOverwriteRecent();
+ break;
+ case 4:
+ this.timesFired = 5;
+ testOpenOldestRecent();
+ break;
+ case 5:
+ this.timesFired = 6;
+ testHideMenu();
+ break;
+ case 6:
+ this.timesFired = 7;
+ testChangedMaxRecent();
+ break;
+ case 7:
+ this.timesFired = 8;
+ testOpenDeletedFile();
+ break;
+ case 8:
+ this.timesFired = 9;
+ testClearedAll();
+ break;
+ }
+ },
+
+ uninit: function PO_uninit() {
+ this.branch.removeObserver("", this);
+ }
+};
+
+function test()
+{
+ waitForExplicitFinish();
+
+ registerCleanupFunction(function () {
+ gFile01.remove(false);
+ gFile01 = null;
+ gFile02.remove(false);
+ gFile02 = null;
+ gFile03.remove(false);
+ gFile03 = null;
+ // gFile04 was removed earlier.
+ lists.recentFiles01 = null;
+ lists.recentFiles02 = null;
+ lists.recentFiles03 = null;
+ lists.recentFiles04 = null;
+ gScratchpad = null;
+
+ PreferenceObserver.uninit();
+ Services.prefs.clearUserPref("devtools.scratchpad.recentFilesMax");
+ });
+
+ Services.prefs.setIntPref("devtools.scratchpad.recentFilesMax", 3);
+
+ // Initiate the preference observer after we have set the temporary recent
+ // files max for this test.
+ PreferenceObserver.init();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
+ openScratchpad(startTest);
+ }, true);
+
+ content.location = "data:text/html,<p>test recent files in Scratchpad";
+}
+
+function finishTest()
+{
+ finish();
+}
diff --git a/devtools/client/scratchpad/test/browser_scratchpad_reload_and_run.js b/devtools/client/scratchpad/test/browser_scratchpad_reload_and_run.js
new file mode 100644
index 000000000..19e360b20
--- /dev/null
+++ b/devtools/client/scratchpad/test/browser_scratchpad_reload_and_run.js
@@ -0,0 +1,76 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* Bug 740948 */
+
+var DEVTOOLS_CHROME_ENABLED = "devtools.chrome.enabled";
+var EDITOR_TEXT = [
+ "var evt = new CustomEvent('foo', { bubbles: true });",
+ "document.body.innerHTML = 'Modified text';",
+ "window.dispatchEvent(evt);"
+].join("\n");
+
+function test()
+{
+ requestLongerTimeout(2);
+ waitForExplicitFinish();
+ Services.prefs.setBoolPref(DEVTOOLS_CHROME_ENABLED, true);
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
+ openScratchpad(runTests);
+ }, true);
+
+ content.location = "data:text/html,Scratchpad test for bug 740948";
+}
+
+function runTests()
+{
+ let sp = gScratchpadWindow.Scratchpad;
+ ok(sp, "Scratchpad object exists in new window");
+
+ // Test that Reload And Run command is enabled in the content
+ // context and disabled in the browser context.
+
+ let reloadAndRun = gScratchpadWindow.document.
+ getElementById("sp-cmd-reloadAndRun");
+ ok(reloadAndRun, "Reload And Run command exists");
+ ok(!reloadAndRun.hasAttribute("disabled"),
+ "Reload And Run command is enabled");
+
+ sp.setBrowserContext();
+ ok(reloadAndRun.hasAttribute("disabled"),
+ "Reload And Run command is disabled in the browser context.");
+
+ // Switch back to the content context and run our predefined
+ // code. This code modifies the body of our document and dispatches
+ // a custom event 'foo'. We listen to that event and check the
+ // body to make sure that the page has been reloaded and Scratchpad
+ // code has been executed.
+
+ sp.setContentContext();
+ sp.setText(EDITOR_TEXT);
+
+ let browser = gBrowser.selectedBrowser;
+
+ let deferred = promise.defer();
+ browser.addEventListener("DOMWindowCreated", function onWindowCreated() {
+ browser.removeEventListener("DOMWindowCreated", onWindowCreated, true);
+
+ browser.contentWindow.addEventListener("foo", function onFoo() {
+ browser.contentWindow.removeEventListener("foo", onFoo, true);
+
+ is(browser.contentWindow.document.body.innerHTML, "Modified text",
+ "After reloading, HTML is different.");
+
+ Services.prefs.clearUserPref(DEVTOOLS_CHROME_ENABLED);
+ deferred.resolve();
+ }, true);
+ }, true);
+
+ ok(browser.contentWindow.document.body.innerHTML !== "Modified text",
+ "Before reloading, HTML is intact.");
+ sp.reloadAndRun().then(deferred.promise).then(finish);
+}
+
diff --git a/devtools/client/scratchpad/test/browser_scratchpad_remember_view_options.js b/devtools/client/scratchpad/test/browser_scratchpad_remember_view_options.js
new file mode 100644
index 000000000..67073c52f
--- /dev/null
+++ b/devtools/client/scratchpad/test/browser_scratchpad_remember_view_options.js
@@ -0,0 +1,65 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* Bug 1140839 */
+
+// Test that view menu items are remembered across scratchpad invocations.
+function test()
+{
+ waitForExplicitFinish();
+
+ // To test for this bug we open a Scratchpad window and change all
+ // view menu options. After each change we compare the correspondent
+ // preference value with the expected value.
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
+ openScratchpad(runTests);
+ }, true);
+
+ content.location = "data:text/html,<title>Bug 1140839</title>" +
+ "<p>test Scratchpad should remember View options";
+}
+
+function runTests()
+{
+ let sp = gScratchpadWindow.Scratchpad;
+ let doc = gScratchpadWindow.document;
+
+ let testData = [
+ {itemMenuId: "sp-menu-line-numbers", prefId: "devtools.scratchpad.lineNumbers", expectedVal: false},
+ {itemMenuId: "sp-menu-word-wrap", prefId: "devtools.scratchpad.wrapText", expectedVal: true},
+ {itemMenuId: "sp-menu-highlight-trailing-space", prefId: "devtools.scratchpad.showTrailingSpace", expectedVal: true},
+ {itemMenuId: "sp-menu-larger-font", prefId: "devtools.scratchpad.editorFontSize", expectedVal: 13},
+ {itemMenuId: "sp-menu-normal-size-font", prefId: "devtools.scratchpad.editorFontSize", expectedVal: 12},
+ {itemMenuId: "sp-menu-smaller-font", prefId: "devtools.scratchpad.editorFontSize", expectedVal: 11},
+ ];
+
+ testData.forEach(function (data) {
+ let getPref = getPrefFunction(data.prefId);
+
+ try {
+ let menu = doc.getElementById(data.itemMenuId);
+ menu.doCommand();
+ let newPreferenceValue = getPref(data.prefId);
+ ok(newPreferenceValue === data.expectedVal, newPreferenceValue + " !== " + data.expectedVal);
+ Services.prefs.clearUserPref(data.prefId);
+ }
+ catch (exception) {
+ ok(false, "Exception thrown while executing the command of menuitem #" + data.itemMenuId);
+ }
+ });
+
+ finish();
+}
+
+function getPrefFunction(preferenceId)
+{
+ let preferenceType = Services.prefs.getPrefType(preferenceId);
+ if (preferenceType === Services.prefs.PREF_INT) {
+ return Services.prefs.getIntPref;
+ } else if (preferenceType === Services.prefs.PREF_BOOL) {
+ return Services.prefs.getBoolPref;
+ }
+}
diff --git a/devtools/client/scratchpad/test/browser_scratchpad_reset_undo.js b/devtools/client/scratchpad/test/browser_scratchpad_reset_undo.js
new file mode 100644
index 000000000..a1b60cd33
--- /dev/null
+++ b/devtools/client/scratchpad/test/browser_scratchpad_reset_undo.js
@@ -0,0 +1,155 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* Bug 684546 */
+
+// Reference to the Scratchpad chrome window object.
+var gScratchpadWindow;
+
+// Reference to the Scratchpad object.
+var gScratchpad;
+
+// Reference to the temporary nsIFile we will work with.
+var gFileA;
+var gFileB;
+
+// The temporary file content.
+var gFileAContent = "// File A ** Hello World!";
+var gFileBContent = "// File B ** Goodbye All";
+
+// Help track if one or both files are saved
+var gFirstFileSaved = false;
+
+function test()
+{
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function browserLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", browserLoad, true);
+ openScratchpad(runTests);
+ }, true);
+
+ content.location = "data:text/html,<p>test that undo get's reset after file load in Scratchpad";
+}
+
+function runTests()
+{
+ gScratchpad = gScratchpadWindow.Scratchpad;
+
+ // Create a temporary file.
+ gFileA = FileUtils.getFile("TmpD", ["fileAForBug684546.tmp"]);
+ gFileA.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o666);
+
+ gFileB = FileUtils.getFile("TmpD", ["fileBForBug684546.tmp"]);
+ gFileB.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o666);
+
+ // Write the temporary file.
+ let foutA = Cc["@mozilla.org/network/file-output-stream;1"].
+ createInstance(Ci.nsIFileOutputStream);
+ foutA.init(gFileA.QueryInterface(Ci.nsILocalFile), 0x02 | 0x08 | 0x20,
+ 0o644, foutA.DEFER_OPEN);
+
+ let foutB = Cc["@mozilla.org/network/file-output-stream;1"].
+ createInstance(Ci.nsIFileOutputStream);
+ foutB.init(gFileB.QueryInterface(Ci.nsILocalFile), 0x02 | 0x08 | 0x20,
+ 0o644, foutB.DEFER_OPEN);
+
+ let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
+ createInstance(Ci.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+ let fileContentStreamA = converter.convertToInputStream(gFileAContent);
+ let fileContentStreamB = converter.convertToInputStream(gFileBContent);
+
+ NetUtil.asyncCopy(fileContentStreamA, foutA, tempFileSaved);
+ NetUtil.asyncCopy(fileContentStreamB, foutB, tempFileSaved);
+}
+
+function tempFileSaved(aStatus)
+{
+ let success = Components.isSuccessCode(aStatus);
+
+ ok(success, "a temporary file was saved successfully");
+
+ if (!success)
+ {
+ finish();
+ return;
+ }
+
+ if (gFirstFileSaved && success)
+ {
+ ok((gFirstFileSaved && success), "Both files loaded");
+ // Import the file A into Scratchpad.
+ gScratchpad.importFromFile(gFileA.QueryInterface(Ci.nsILocalFile), true,
+ fileAImported);
+ }
+ gFirstFileSaved = success;
+}
+
+function fileAImported(aStatus, aFileContent)
+{
+ ok(Components.isSuccessCode(aStatus),
+ "the temporary file A was imported successfully with Scratchpad");
+
+ is(aFileContent, gFileAContent, "received data is correct");
+
+ is(gScratchpad.getText(), gFileAContent, "the editor content is correct");
+
+ gScratchpad.editor.replaceText("new text",
+ gScratchpad.editor.getPosition(gScratchpad.getText().length));
+
+ is(gScratchpad.getText(), gFileAContent + "new text", "text updated correctly");
+ gScratchpad.undo();
+ is(gScratchpad.getText(), gFileAContent, "undo works");
+ gScratchpad.redo();
+ is(gScratchpad.getText(), gFileAContent + "new text", "redo works");
+
+ // Import the file B into Scratchpad.
+ gScratchpad.importFromFile(gFileB.QueryInterface(Ci.nsILocalFile), true,
+ fileBImported);
+}
+
+function fileBImported(aStatus, aFileContent)
+{
+ ok(Components.isSuccessCode(aStatus),
+ "the temporary file B was imported successfully with Scratchpad");
+
+ is(aFileContent, gFileBContent, "received data is correct");
+
+ is(gScratchpad.getText(), gFileBContent, "the editor content is correct");
+
+ ok(!gScratchpad.editor.canUndo(), "editor cannot undo after load");
+
+ gScratchpad.undo();
+ is(gScratchpad.getText(), gFileBContent,
+ "the editor content is still correct after undo");
+
+ gScratchpad.editor.replaceText("new text",
+ gScratchpad.editor.getPosition(gScratchpad.getText().length));
+ is(gScratchpad.getText(), gFileBContent + "new text", "text updated correctly");
+
+ gScratchpad.undo();
+ is(gScratchpad.getText(), gFileBContent, "undo works");
+ ok(!gScratchpad.editor.canUndo(), "editor cannot undo after load (again)");
+
+ gScratchpad.redo();
+ is(gScratchpad.getText(), gFileBContent + "new text", "redo works");
+
+ // Done!
+ finish();
+}
+
+registerCleanupFunction(function () {
+ if (gFileA && gFileA.exists())
+ {
+ gFileA.remove(false);
+ gFileA = null;
+ }
+ if (gFileB && gFileB.exists())
+ {
+ gFileB.remove(false);
+ gFileB = null;
+ }
+ gScratchpad = null;
+});
diff --git a/devtools/client/scratchpad/test/browser_scratchpad_restore.js b/devtools/client/scratchpad/test/browser_scratchpad_restore.js
new file mode 100644
index 000000000..5890e954f
--- /dev/null
+++ b/devtools/client/scratchpad/test/browser_scratchpad_restore.js
@@ -0,0 +1,96 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Call the iterator for each item in the list,
+ calling the final callback with all the results
+ after every iterator call has sent its result */
+function asyncMap(items, iterator, callback)
+{
+ let expected = items.length;
+ let results = [];
+
+ items.forEach(function (item) {
+ iterator(item, function (result) {
+ results.push(result);
+ if (results.length == expected) {
+ callback(results);
+ }
+ });
+ });
+}
+
+function test()
+{
+ waitForExplicitFinish();
+ testRestore();
+}
+
+function testRestore()
+{
+ let states = [
+ {
+ filename: "testfile",
+ text: "test1",
+ executionContext: 2
+ },
+ {
+ text: "text2",
+ executionContext: 1
+ },
+ {
+ text: "text3",
+ executionContext: 1
+ }
+ ];
+
+ asyncMap(states, function (state, done) {
+ // Open some scratchpad windows
+ openScratchpad(done, {state: state, noFocus: true});
+ }, function (wins) {
+ // Then save the windows to session store
+ ScratchpadManager.saveOpenWindows();
+
+ // Then get their states
+ let session = ScratchpadManager.getSessionState();
+
+ // Then close them
+ wins.forEach(function (win) {
+ win.close();
+ });
+
+ // Clear out session state for next tests
+ ScratchpadManager.saveOpenWindows();
+
+ // Then restore them
+ let restoredWins = ScratchpadManager.restoreSession(session);
+
+ is(restoredWins.length, 3, "Three scratchad windows restored");
+
+ asyncMap(restoredWins, function (restoredWin, done) {
+ openScratchpad(function (aWin) {
+ let state = aWin.Scratchpad.getState();
+ aWin.close();
+ done(state);
+ }, {window: restoredWin, noFocus: true});
+ }, function (restoredStates) {
+ // Then make sure they were restored with the right states
+ ok(statesMatch(restoredStates, states),
+ "All scratchpad window states restored correctly");
+
+ // Yay, we're done!
+ finish();
+ });
+ });
+}
+
+function statesMatch(restoredStates, states)
+{
+ return states.every(function (state) {
+ return restoredStates.some(function (restoredState) {
+ return state.filename == restoredState.filename
+ && state.text == restoredState.text
+ && state.executionContext == restoredState.executionContext;
+ });
+ });
+}
diff --git a/devtools/client/scratchpad/test/browser_scratchpad_revert_to_saved.js b/devtools/client/scratchpad/test/browser_scratchpad_revert_to_saved.js
new file mode 100644
index 000000000..92c6c3720
--- /dev/null
+++ b/devtools/client/scratchpad/test/browser_scratchpad_revert_to_saved.js
@@ -0,0 +1,134 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* Bug 751744 */
+
+// Reference to the Scratchpad object.
+var gScratchpad;
+
+// Reference to the temporary nsIFiles.
+var gFile;
+
+// Temporary file name.
+var gFileName = "testFileForBug751744.tmp";
+
+
+// Content for the temporary file.
+var gFileContent = "/* this file is already saved */\n" +
+ "function foo() { alert('bar') }";
+var gLength = gFileContent.length;
+
+// Reference to the menu entry.
+var menu;
+
+function startTest()
+{
+ gScratchpad = gScratchpadWindow.Scratchpad;
+ menu = gScratchpadWindow.document.getElementById("sp-cmd-revert");
+ createAndLoadTemporaryFile();
+}
+
+function testAfterSaved() {
+ // Check if the revert menu is disabled as the file is at saved state.
+ ok(menu.hasAttribute("disabled"), "The revert menu entry is disabled.");
+
+ // chancging the text in the file
+ gScratchpad.setText(gScratchpad.getText() + "\nfoo();");
+ // Checking the text got changed
+ is(gScratchpad.getText(), gFileContent + "\nfoo();",
+ "The text changed the first time.");
+
+ // Revert menu now should be enabled.
+ ok(!menu.hasAttribute("disabled"),
+ "The revert menu entry is enabled after changing text first time");
+
+ // reverting back to last saved state.
+ gScratchpad.revertFile(testAfterRevert);
+}
+
+function testAfterRevert() {
+ // Check if the file's text got reverted
+ is(gScratchpad.getText(), gFileContent,
+ "The text reverted back to original text.");
+ // The revert menu should be disabled again.
+ ok(menu.hasAttribute("disabled"),
+ "The revert menu entry is disabled after reverting.");
+
+ // chancging the text in the file again
+ gScratchpad.setText(gScratchpad.getText() + "\nalert(foo.toSource());");
+ // Saving the file.
+ gScratchpad.saveFile(testAfterSecondSave);
+}
+
+function testAfterSecondSave() {
+ // revert menu entry should be disabled.
+ ok(menu.hasAttribute("disabled"),
+ "The revert menu entry is disabled after saving.");
+
+ // changing the text.
+ gScratchpad.setText(gScratchpad.getText() + "\nfoo();");
+
+ // revert menu entry should get enabled yet again.
+ ok(!menu.hasAttribute("disabled"),
+ "The revert menu entry is enabled after changing text third time");
+
+ // reverting back to last saved state.
+ gScratchpad.revertFile(testAfterSecondRevert);
+}
+
+function testAfterSecondRevert() {
+ // Check if the file's text got reverted
+ is(gScratchpad.getText(), gFileContent + "\nalert(foo.toSource());",
+ "The text reverted back to the changed saved text.");
+ // The revert menu should be disabled again.
+ ok(menu.hasAttribute("disabled"),
+ "Revert menu entry is disabled after reverting to changed saved state.");
+ gFile.remove(false);
+ gFile = gScratchpad = menu = null;
+ finish();
+}
+
+function createAndLoadTemporaryFile()
+{
+ // Create a temporary file.
+ gFile = FileUtils.getFile("TmpD", [gFileName]);
+ gFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o666);
+
+ // Write the temporary file.
+ let fout = Cc["@mozilla.org/network/file-output-stream;1"].
+ createInstance(Ci.nsIFileOutputStream);
+ fout.init(gFile.QueryInterface(Ci.nsILocalFile), 0x02 | 0x08 | 0x20,
+ 0o644, fout.DEFER_OPEN);
+
+ let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
+ createInstance(Ci.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+ let fileContentStream = converter.convertToInputStream(gFileContent);
+
+ NetUtil.asyncCopy(fileContentStream, fout, tempFileSaved);
+}
+
+function tempFileSaved(aStatus)
+{
+ ok(Components.isSuccessCode(aStatus),
+ "the temporary file was saved successfully");
+
+ // Import the file into Scratchpad.
+ gScratchpad.setFilename(gFile.path);
+ gScratchpad.importFromFile(gFile.QueryInterface(Ci.nsILocalFile), true,
+ testAfterSaved);
+}
+
+function test()
+{
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
+ openScratchpad(startTest);
+ }, true);
+
+ content.location = "data:text/html,<p>test reverting to last saved state of" +
+ " a file </p>";
+}
diff --git a/devtools/client/scratchpad/test/browser_scratchpad_run_error_goto_line.js b/devtools/client/scratchpad/test/browser_scratchpad_run_error_goto_line.js
new file mode 100644
index 000000000..a5d3d5163
--- /dev/null
+++ b/devtools/client/scratchpad/test/browser_scratchpad_run_error_goto_line.js
@@ -0,0 +1,60 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2; fill-column: 80 -*- */
+/* 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";
+
+function test()
+{
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
+ openScratchpad(runTests);
+ }, true);
+
+ content.location = "data:text/html;charset=utf8,test Scratchpad pretty print.";
+}
+
+function runTests(sw)
+{
+ const sp = sw.Scratchpad;
+ sp.setText([
+ "// line 1",
+ "// line 2",
+ "var re = /a bad /regexp/; // line 3 is an obvious syntax error!",
+ "// line 4",
+ "// line 5",
+ ""
+ ].join("\n"));
+ sp.run().then(() => {
+ // CodeMirror lines and columns are 0-based, Scratchpad UI and error
+ // stack are 1-based.
+ let errorLine = 3;
+ let editorDoc = sp.editor.container.contentDocument;
+ sp.editor.jumpToLine();
+ let lineInput = editorDoc.querySelector("input");
+ let inputLine = lineInput.value;
+ is(inputLine, errorLine, "jumpToLine input field is set from editor selection");
+ EventUtils.synthesizeKey("VK_RETURN", { }, editorDoc.defaultView);
+ // CodeMirror lines and columns are 0-based, Scratchpad UI and error
+ // stack are 1-based.
+ let cursor = sp.editor.getCursor();
+ is(cursor.line + 1, inputLine, "jumpToLine goto error location (line)");
+ is(cursor.ch + 1, 1, "jumpToLine goto error location (column)");
+ }, error => {
+ ok(false, error);
+ finish();
+ }).then(() => {
+ var statusBarField = sp.editor.container.ownerDocument.querySelector("#statusbar-line-col");
+ let { line, ch } = sp.editor.getCursor();
+ is(statusBarField.textContent, sp.strings.formatStringFromName(
+ "scratchpad.statusBarLineCol", [ line + 1, ch + 1], 2), "statusbar text is correct (" + statusBarField.textContent + ")");
+ finish();
+ }, error => {
+ ok(false, error);
+ finish();
+ });
+}
diff --git a/devtools/client/scratchpad/test/browser_scratchpad_tab.js b/devtools/client/scratchpad/test/browser_scratchpad_tab.js
new file mode 100644
index 000000000..67057f206
--- /dev/null
+++ b/devtools/client/scratchpad/test/browser_scratchpad_tab.js
@@ -0,0 +1,75 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* Bug 660560 */
+
+function test()
+{
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onTabLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", onTabLoad, true);
+
+ Services.prefs.setIntPref("devtools.editor.tabsize", 5);
+
+ openScratchpad(runTests);
+ }, true);
+
+ content.location = "data:text/html,Scratchpad test for the Tab key, bug 660560";
+}
+
+function runTests()
+{
+ let sp = gScratchpadWindow.Scratchpad;
+ ok(sp, "Scratchpad object exists in new window");
+
+ ok(sp.editor.hasFocus(), "the editor has focus");
+
+ sp.setText("window.foo;");
+ sp.editor.setCursor({ line: 0, ch: 0 });
+
+ EventUtils.synthesizeKey("VK_TAB", {}, gScratchpadWindow);
+
+ is(sp.getText(), " window.foo;", "Tab key added 5 spaces");
+
+ is(sp.editor.getCursor().line, 0, "line is correct");
+ is(sp.editor.getCursor().ch, 5, "character is correct");
+
+ sp.editor.setCursor({ line: 0, ch: 6 });
+
+ EventUtils.synthesizeKey("VK_TAB", {}, gScratchpadWindow);
+
+ is(sp.getText(), " w indow.foo;",
+ "Tab key added 4 spaces");
+
+ is(sp.editor.getCursor().line, 0, "line is correct");
+ is(sp.editor.getCursor().ch, 10, "character is correct");
+
+ gScratchpadWindow.close();
+
+ Services.prefs.setIntPref("devtools.editor.tabsize", 6);
+ Services.prefs.setBoolPref("devtools.editor.expandtab", false);
+
+ openScratchpad(runTests2);
+}
+
+function runTests2()
+{
+ let sp = gScratchpadWindow.Scratchpad;
+
+ sp.setText("window.foo;");
+ sp.editor.setCursor({ line: 0, ch: 0 });
+
+ EventUtils.synthesizeKey("VK_TAB", {}, gScratchpadWindow);
+
+ is(sp.getText(), "\twindow.foo;", "Tab key added the tab character");
+
+ is(sp.editor.getCursor().line, 0, "line is correct");
+ is(sp.editor.getCursor().ch, 1, "character is correct");
+
+ Services.prefs.clearUserPref("devtools.editor.tabsize");
+ Services.prefs.clearUserPref("devtools.editor.expandtab");
+
+ finish();
+}
diff --git a/devtools/client/scratchpad/test/browser_scratchpad_tab_switch.js b/devtools/client/scratchpad/test/browser_scratchpad_tab_switch.js
new file mode 100644
index 000000000..c2419a1e1
--- /dev/null
+++ b/devtools/client/scratchpad/test/browser_scratchpad_tab_switch.js
@@ -0,0 +1,103 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var tab1;
+var tab2;
+var sp;
+
+function test()
+{
+ waitForExplicitFinish();
+
+ tab1 = gBrowser.addTab();
+ gBrowser.selectedTab = tab1;
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad1() {
+ gBrowser.selectedBrowser.removeEventListener("load", onLoad1, true);
+
+ tab2 = gBrowser.addTab();
+ gBrowser.selectedTab = tab2;
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad2() {
+ gBrowser.selectedBrowser.removeEventListener("load", onLoad2, true);
+ openScratchpad(runTests);
+ }, true);
+ content.location = "data:text/html,test context switch in Scratchpad tab 2";
+ }, true);
+
+ content.location = "data:text/html,test context switch in Scratchpad tab 1";
+}
+
+function runTests()
+{
+ sp = gScratchpadWindow.Scratchpad;
+
+ let contentMenu = gScratchpadWindow.document.getElementById("sp-menu-content");
+ let browserMenu = gScratchpadWindow.document.getElementById("sp-menu-browser");
+ let notificationBox = sp.notificationBox;
+
+ ok(contentMenu, "found #sp-menu-content");
+ ok(browserMenu, "found #sp-menu-browser");
+ ok(notificationBox, "found Scratchpad.notificationBox");
+
+ sp.setContentContext();
+
+ is(sp.executionContext, gScratchpadWindow.SCRATCHPAD_CONTEXT_CONTENT,
+ "executionContext is content");
+
+ is(contentMenu.getAttribute("checked"), "true",
+ "content menuitem is checked");
+
+ isnot(browserMenu.getAttribute("checked"), "true",
+ "chrome menuitem is not checked");
+
+ is(notificationBox.currentNotification, null,
+ "there is no notification currently shown for content context");
+
+ sp.setText("window.foosbug653108 = 'aloha';");
+
+ ok(!content.wrappedJSObject.foosbug653108,
+ "no content.foosbug653108");
+
+ sp.run().then(function () {
+ is(content.wrappedJSObject.foosbug653108, "aloha",
+ "content.foosbug653108 has been set");
+
+ gBrowser.tabContainer.addEventListener("TabSelect", runTests2, true);
+ gBrowser.selectedTab = tab1;
+ });
+}
+
+function runTests2() {
+ gBrowser.tabContainer.removeEventListener("TabSelect", runTests2, true);
+
+ ok(!window.foosbug653108, "no window.foosbug653108");
+
+ sp.setText("window.foosbug653108");
+ sp.run().then(function ([, , result]) {
+ isnot(result, "aloha", "window.foosbug653108 is not aloha");
+
+ sp.setText("window.foosbug653108 = 'ahoyhoy';");
+ sp.run().then(function () {
+ is(content.wrappedJSObject.foosbug653108, "ahoyhoy",
+ "content.foosbug653108 has been set 2");
+
+ gBrowser.selectedBrowser.addEventListener("load", runTests3, true);
+ content.location = "data:text/html,test context switch in Scratchpad location 2";
+ });
+ });
+}
+
+function runTests3() {
+ gBrowser.selectedBrowser.removeEventListener("load", runTests3, true);
+ // Check that the sandbox is not cached.
+
+ sp.setText("typeof foosbug653108;");
+ sp.run().then(function ([, , result]) {
+ is(result, "undefined", "global variable does not exist");
+
+ tab1 = null;
+ tab2 = null;
+ sp = null;
+ finish();
+ });
+}
diff --git a/devtools/client/scratchpad/test/browser_scratchpad_throw_output.js b/devtools/client/scratchpad/test/browser_scratchpad_throw_output.js
new file mode 100644
index 000000000..cfcd5e049
--- /dev/null
+++ b/devtools/client/scratchpad/test/browser_scratchpad_throw_output.js
@@ -0,0 +1,52 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+
+function test()
+{
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
+ openScratchpad(testThrowOutput);
+ }, true);
+
+ content.location = "data:text/html;charset=utf8,<p>Test throw outputs in Scratchpad</p>";
+}
+
+function testThrowOutput()
+{
+ let scratchpad = gScratchpadWindow.Scratchpad, tests = [];
+
+ let falsyValues = ["false", "0", "-0", "null", "undefined", "Infinity",
+ "-Infinity", "NaN"];
+ falsyValues.forEach(function (value) {
+ tests.push({
+ method: "display",
+ code: "throw " + value + ";",
+ result: "throw " + value + ";\n/*\nException: " + value + "\n*/",
+ label: "Correct exception message for '" + value + "' is shown"
+ });
+ });
+
+ let { DebuggerServer } = require("devtools/server/main");
+
+ let longLength = DebuggerServer.LONG_STRING_LENGTH + 1;
+ let longString = new Array(longLength).join("a");
+ let shortedString = longString.substring(0,
+ DebuggerServer.LONG_STRING_INITIAL_LENGTH) + "\u2026";
+
+ tests.push({
+ method: "display",
+ code: "throw (new Array(" + longLength + ").join('a'));",
+ result: "throw (new Array(" + longLength + ").join('a'));\n" +
+ "/*\nException: " + shortedString + "\n*/",
+ label: "Correct exception message for a longString is shown"
+ });
+
+ runAsyncTests(scratchpad, tests).then(function () {
+ finish();
+ });
+}
diff --git a/devtools/client/scratchpad/test/browser_scratchpad_ui.js b/devtools/client/scratchpad/test/browser_scratchpad_ui.js
new file mode 100644
index 000000000..5d8af1068
--- /dev/null
+++ b/devtools/client/scratchpad/test/browser_scratchpad_ui.js
@@ -0,0 +1,74 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test()
+{
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
+ openScratchpad(runTests);
+ }, true);
+
+ content.location = "data:text/html,<title>foobarBug636725</title>" +
+ "<p>test inspect() in Scratchpad";
+}
+
+function runTests()
+{
+ let sp = gScratchpadWindow.Scratchpad;
+ let doc = gScratchpadWindow.document;
+
+ let methodsAndItems = {
+ "sp-menu-newscratchpad": "openScratchpad",
+ "sp-menu-open": "openFile",
+ "sp-menu-save": "saveFile",
+ "sp-menu-saveas": "saveFileAs",
+ "sp-text-run": "run",
+ "sp-text-inspect": "inspect",
+ "sp-text-display": "display",
+ "sp-text-reloadAndRun": "reloadAndRun",
+ "sp-menu-content": "setContentContext",
+ "sp-menu-browser": "setBrowserContext",
+ "sp-menu-pprint": "prettyPrint",
+ "sp-menu-line-numbers": "toggleEditorOption",
+ "sp-menu-word-wrap": "toggleEditorOption",
+ "sp-menu-highlight-trailing-space": "toggleEditorOption",
+ "sp-menu-larger-font": "increaseFontSize",
+ "sp-menu-smaller-font": "decreaseFontSize",
+ "sp-menu-normal-size-font": "normalFontSize",
+ };
+
+ let lastMethodCalled = null;
+
+ for (let id in methodsAndItems) {
+ lastMethodCalled = null;
+
+ let methodName = methodsAndItems[id];
+ let oldMethod = sp[methodName];
+ ok(oldMethod, "found method " + methodName + " in Scratchpad object");
+
+ sp[methodName] = () => {
+ lastMethodCalled = methodName;
+ };
+
+ let menu = doc.getElementById(id);
+ ok(menu, "found menuitem #" + id);
+
+ try {
+ menu.doCommand();
+ }
+ catch (ex) {
+ ok(false, "exception thrown while executing the command of menuitem #" + id);
+ }
+
+ ok(lastMethodCalled == methodName,
+ "method " + methodName + " invoked by the associated menuitem");
+
+ sp[methodName] = oldMethod;
+ }
+
+ finish();
+}
diff --git a/devtools/client/scratchpad/test/browser_scratchpad_unsaved.js b/devtools/client/scratchpad/test/browser_scratchpad_unsaved.js
new file mode 100644
index 000000000..54b97b75b
--- /dev/null
+++ b/devtools/client/scratchpad/test/browser_scratchpad_unsaved.js
@@ -0,0 +1,119 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* Bug 669612 */
+
+// only finish() when correct number of tests are done
+const expected = 4;
+var count = 0;
+function done()
+{
+ if (++count == expected) {
+ finish();
+ }
+}
+
+
+function test()
+{
+ waitForExplicitFinish();
+
+ testListeners();
+ testRestoreNotFromFile();
+ testRestoreFromFileSaved();
+ testRestoreFromFileUnsaved();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ content.location = "data:text/html,<p>test star* UI for unsaved file changes";
+}
+
+function testListeners()
+{
+ openScratchpad(function (aWin, aScratchpad) {
+ aScratchpad.setText("new text");
+ ok(isStar(aWin), "show star if scratchpad text changes");
+
+ aScratchpad.dirty = false;
+ ok(!isStar(aWin), "no star before changing text");
+
+ aScratchpad.setFilename("foo.js");
+ aScratchpad.setText("new text2");
+ ok(isStar(aWin), "shows star if scratchpad text changes");
+
+ aScratchpad.dirty = false;
+ ok(!isStar(aWin), "no star if scratchpad was just saved");
+
+ aScratchpad.setText("new text3");
+ ok(isStar(aWin), "shows star if scratchpad has more changes");
+
+ aScratchpad.undo();
+ ok(!isStar(aWin), "no star if scratchpad undo to save point");
+
+ aScratchpad.undo();
+ ok(isStar(aWin), "star if scratchpad undo past save point");
+
+ aWin.close();
+ done();
+ }, {noFocus: true});
+}
+
+function testRestoreNotFromFile()
+{
+ let session = [{
+ text: "test1",
+ executionContext: 1
+ }];
+
+ let [win] = ScratchpadManager.restoreSession(session);
+ openScratchpad(function (aWin, aScratchpad) {
+ aScratchpad.setText("new text");
+ ok(isStar(win), "show star if restored scratchpad isn't from a file");
+
+ win.close();
+ done();
+ }, {window: win, noFocus: true});
+}
+
+function testRestoreFromFileSaved()
+{
+ let session = [{
+ filename: "test.js",
+ text: "test1",
+ executionContext: 1,
+ saved: true
+ }];
+
+ let [win] = ScratchpadManager.restoreSession(session);
+ openScratchpad(function (aWin, aScratchpad) {
+ ok(!isStar(win), "no star before changing text in scratchpad restored from file");
+
+ aScratchpad.setText("new text");
+ ok(isStar(win), "star when text changed from scratchpad restored from file");
+
+ win.close();
+ done();
+ }, {window: win, noFocus: true});
+}
+
+function testRestoreFromFileUnsaved()
+{
+ let session = [{
+ filename: "test.js",
+ text: "test1",
+ executionContext: 1,
+ saved: false
+ }];
+
+ let [win] = ScratchpadManager.restoreSession(session);
+ openScratchpad(function () {
+ ok(isStar(win), "star with scratchpad restored with unsaved text");
+
+ win.close();
+ done();
+ }, {window: win, noFocus: true});
+}
+
+function isStar(win)
+{
+ return win.document.title.match(/^\*[^\*]/);
+}
diff --git a/devtools/client/scratchpad/test/browser_scratchpad_wrong_window_focus.js b/devtools/client/scratchpad/test/browser_scratchpad_wrong_window_focus.js
new file mode 100644
index 000000000..0d094ba98
--- /dev/null
+++ b/devtools/client/scratchpad/test/browser_scratchpad_wrong_window_focus.js
@@ -0,0 +1,93 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* Bug 661762 */
+
+// Use the old webconsole since scratchpad focus isn't working on new one (Bug 1304794)
+Services.prefs.setBoolPref("devtools.webconsole.new-frontend-enabled", false);
+registerCleanupFunction(function* () {
+ Services.prefs.clearUserPref("devtools.webconsole.new-frontend-enabled");
+});
+
+function test()
+{
+ waitForExplicitFinish();
+
+ // To test for this bug we open a Scratchpad window, save its
+ // reference and then open another one. This way the first window
+ // loses its focus.
+ //
+ // Then we open a web console and execute a console.log statement
+ // from the first Scratch window (that's why we needed to save its
+ // reference).
+ //
+ // Then we wait for our message to appear in the console and click
+ // on the location link. After that we check which Scratchpad window
+ // is currently active (it should be the older one).
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
+
+ openScratchpad(function () {
+ let sw = gScratchpadWindow;
+ let {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+ let {TargetFactory} = require("devtools/client/framework/target");
+
+ openScratchpad(function () {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ gDevTools.showToolbox(target, "webconsole").then((toolbox) => {
+ let hud = toolbox.getCurrentPanel().hud;
+ hud.jsterm.clearOutput(true);
+ testFocus(sw, hud);
+ });
+ });
+ });
+ }, true);
+
+ content.location = "data:text/html;charset=utf8,<p>test window focus for Scratchpad.";
+}
+
+function testFocus(sw, hud) {
+ let sp = sw.Scratchpad;
+
+ function onMessage(event, messages) {
+ let msg = [...messages][0];
+ let node = msg.node;
+
+ var loc = node.querySelector(".frame-link");
+ ok(loc, "location element exists");
+ is(loc.getAttribute("data-url"), sw.Scratchpad.uniqueName, "location value is correct");
+ is(loc.getAttribute("data-line"), "1", "line value is correct");
+ is(loc.getAttribute("data-column"), "1", "column value is correct");
+
+ sw.addEventListener("focus", function onFocus() {
+ sw.removeEventListener("focus", onFocus, true);
+
+ let win = Services.wm.getMostRecentWindow("devtools:scratchpad");
+
+ ok(win, "there are active Scratchpad windows");
+ is(win.Scratchpad.uniqueName, sw.Scratchpad.uniqueName,
+ "correct window is in focus");
+
+ // gScratchpadWindow will be closed automatically but we need to
+ // close the second window ourselves.
+ sw.close();
+ finish();
+ }, true);
+
+ // Simulate a click on the "Scratchpad/N:1" link.
+ EventUtils.synthesizeMouse(loc, 2, 2, {}, hud.iframeWindow);
+ }
+
+ // Sending messages to web console is an asynchronous operation. That's
+ // why we have to setup an observer here.
+ hud.ui.once("new-messages", onMessage);
+
+ sp.setText("console.log('foo');");
+ sp.run().then(function ([selection, error, result]) {
+ is(selection, "console.log('foo');", "selection is correct");
+ is(error, undefined, "error is correct");
+ is(result.type, "undefined", "result is correct");
+ });
+}
diff --git a/devtools/client/scratchpad/test/head.js b/devtools/client/scratchpad/test/head.js
new file mode 100644
index 000000000..15619a169
--- /dev/null
+++ b/devtools/client/scratchpad/test/head.js
@@ -0,0 +1,221 @@
+/* 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 {NetUtil} = Cu.import("resource://gre/modules/NetUtil.jsm", {});
+const {FileUtils} = Cu.import("resource://gre/modules/FileUtils.jsm", {});
+const {console} = Cu.import("resource://gre/modules/Console.jsm", {});
+const {ScratchpadManager} = Cu.import("resource://devtools/client/scratchpad/scratchpad-manager.jsm", {});
+const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const Services = require("Services");
+const DevToolsUtils = require("devtools/shared/DevToolsUtils");
+const flags = require("devtools/shared/flags");
+const promise = require("promise");
+
+
+var gScratchpadWindow; // Reference to the Scratchpad chrome window object
+
+flags.testing = true;
+SimpleTest.registerCleanupFunction(() => {
+ flags.testing = false;
+});
+
+/**
+ * Open a Scratchpad window.
+ *
+ * @param function aReadyCallback
+ * Optional. The function you want invoked when the Scratchpad instance
+ * is ready.
+ * @param object aOptions
+ * Optional. Options for opening the scratchpad:
+ * - window
+ * Provide this if there's already a Scratchpad window you want to wait
+ * loading for.
+ * - state
+ * Scratchpad state object. This is used when Scratchpad is open.
+ * - noFocus
+ * Boolean that tells you do not want the opened window to receive
+ * focus.
+ * @return nsIDOMWindow
+ * The new window object that holds Scratchpad. Note that the
+ * gScratchpadWindow global is also updated to reference the new window
+ * object.
+ */
+function openScratchpad(aReadyCallback, aOptions = {})
+{
+ let win = aOptions.window ||
+ ScratchpadManager.openScratchpad(aOptions.state);
+ if (!win) {
+ return;
+ }
+
+ let onLoad = function () {
+ win.removeEventListener("load", onLoad, false);
+
+ win.Scratchpad.addObserver({
+ onReady: function (aScratchpad) {
+ aScratchpad.removeObserver(this);
+
+ if (aOptions.noFocus) {
+ aReadyCallback(win, aScratchpad);
+ } else {
+ waitForFocus(aReadyCallback.bind(null, win, aScratchpad), win);
+ }
+ }
+ });
+ };
+
+ if (aReadyCallback) {
+ win.addEventListener("load", onLoad, false);
+ }
+
+ gScratchpadWindow = win;
+ return gScratchpadWindow;
+}
+
+/**
+ * Open a new tab and then open a scratchpad.
+ * @param object aOptions
+ * Optional. Options for opening the tab and scratchpad. In addition
+ * to the options supported by openScratchpad, the following options
+ * are supported:
+ * - tabContent
+ * A string providing the html content of the tab.
+ * @return Promise
+ */
+function openTabAndScratchpad(aOptions = {})
+{
+ waitForExplicitFinish();
+ return new promise(resolve => {
+ gBrowser.selectedTab = gBrowser.addTab();
+ let {selectedBrowser} = gBrowser;
+ selectedBrowser.addEventListener("load", function onLoad() {
+ selectedBrowser.removeEventListener("load", onLoad, true);
+ openScratchpad((win, sp) => resolve([win, sp]), aOptions);
+ }, true);
+ content.location = "data:text/html;charset=utf8," + (aOptions.tabContent || "");
+ });
+}
+
+/**
+ * Create a temporary file, write to it and call a callback
+ * when done.
+ *
+ * @param string aName
+ * Name of your temporary file.
+ * @param string aContent
+ * Temporary file's contents.
+ * @param function aCallback
+ * Optional callback to be called when we're done writing
+ * to the file. It will receive two parameters: status code
+ * and a file object.
+ */
+function createTempFile(aName, aContent, aCallback = function () {})
+{
+ // Create a temporary file.
+ let file = FileUtils.getFile("TmpD", [aName]);
+ file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("666", 8));
+
+ // Write the temporary file.
+ let fout = Cc["@mozilla.org/network/file-output-stream;1"].
+ createInstance(Ci.nsIFileOutputStream);
+ fout.init(file.QueryInterface(Ci.nsILocalFile), 0x02 | 0x08 | 0x20,
+ parseInt("644", 8), fout.DEFER_OPEN);
+
+ let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
+ createInstance(Ci.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+ let fileContentStream = converter.convertToInputStream(aContent);
+
+ NetUtil.asyncCopy(fileContentStream, fout, function (aStatus) {
+ aCallback(aStatus, file);
+ });
+}
+
+/**
+ * Run a set of asychronous tests sequentially defined by input and output.
+ *
+ * @param Scratchpad aScratchpad
+ * The scratchpad to use in running the tests.
+ * @param array aTests
+ * An array of test objects, each with the following properties:
+ * - method
+ * Scratchpad method to use, one of "run", "display", or "inspect".
+ * - code
+ * Code to run in the scratchpad.
+ * - result
+ * Expected code that will be in the scratchpad upon completion.
+ * - label
+ * The tests label which will be logged in the test runner output.
+ * @return Promise
+ * The promise that will be resolved when all tests are finished.
+ */
+function runAsyncTests(aScratchpad, aTests)
+{
+ let deferred = promise.defer();
+
+ (function runTest() {
+ if (aTests.length) {
+ let test = aTests.shift();
+ aScratchpad.setText(test.code);
+ aScratchpad[test.method]().then(function success() {
+ is(aScratchpad.getText(), test.result, test.label);
+ runTest();
+ }, function failure(error) {
+ ok(false, error.stack + " " + test.label);
+ runTest();
+ });
+ } else {
+ deferred.resolve();
+ }
+ })();
+
+ return deferred.promise;
+}
+
+/**
+ * Run a set of asychronous tests sequentially with callbacks to prepare each
+ * test and to be called when the test result is ready.
+ *
+ * @param Scratchpad aScratchpad
+ * The scratchpad to use in running the tests.
+ * @param array aTests
+ * An array of test objects, each with the following properties:
+ * - method
+ * Scratchpad method to use, one of "run", "display", or "inspect".
+ * - prepare
+ * The callback to run just prior to executing the scratchpad method.
+ * - then
+ * The callback to run when the scratchpad execution promise resolves.
+ * @return Promise
+ * The promise that will be resolved when all tests are finished.
+ */
+var runAsyncCallbackTests = Task.async(function* (aScratchpad, aTests) {
+ for (let {prepare, method, then} of aTests) {
+ yield prepare();
+ let res = yield aScratchpad[method]();
+ yield then(res);
+ }
+});
+
+/**
+ * A simple wrapper for ContentTask.spawn for more compact code.
+ */
+function inContent(generator) {
+ return ContentTask.spawn(gBrowser.selectedBrowser, {}, generator);
+}
+
+function cleanup()
+{
+ if (gScratchpadWindow) {
+ gScratchpadWindow.close();
+ gScratchpadWindow = null;
+ }
+ while (gBrowser.tabs.length > 1) {
+ gBrowser.removeCurrentTab();
+ }
+}
+
+registerCleanupFunction(cleanup);
diff --git a/devtools/client/shadereditor/moz.build b/devtools/client/shadereditor/moz.build
new file mode 100644
index 000000000..684fabc22
--- /dev/null
+++ b/devtools/client/shadereditor/moz.build
@@ -0,0 +1,10 @@
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'panel.js'
+)
+
+BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
diff --git a/devtools/client/shadereditor/panel.js b/devtools/client/shadereditor/panel.js
new file mode 100644
index 000000000..92fac9646
--- /dev/null
+++ b/devtools/client/shadereditor/panel.js
@@ -0,0 +1,76 @@
+/* -*- 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 { Cc, Ci, Cu, Cr } = require("chrome");
+const promise = require("promise");
+const EventEmitter = require("devtools/shared/event-emitter");
+const { WebGLFront } = require("devtools/shared/fronts/webgl");
+const DevToolsUtils = require("devtools/shared/DevToolsUtils");
+
+function ShaderEditorPanel(iframeWindow, toolbox) {
+ this.panelWin = iframeWindow;
+ this._toolbox = toolbox;
+ this._destroyer = null;
+
+ EventEmitter.decorate(this);
+}
+
+exports.ShaderEditorPanel = ShaderEditorPanel;
+
+ShaderEditorPanel.prototype = {
+ /**
+ * Open is effectively an asynchronous constructor.
+ *
+ * @return object
+ * A promise that is resolved when the Shader Editor completes opening.
+ */
+ open: function () {
+ let targetPromise;
+
+ // Local debugging needs to make the target remote.
+ if (!this.target.isRemote) {
+ targetPromise = this.target.makeRemote();
+ } else {
+ targetPromise = promise.resolve(this.target);
+ }
+
+ return targetPromise
+ .then(() => {
+ this.panelWin.gToolbox = this._toolbox;
+ this.panelWin.gTarget = this.target;
+ this.panelWin.gFront = new WebGLFront(this.target.client, this.target.form);
+ return this.panelWin.startupShaderEditor();
+ })
+ .then(() => {
+ this.isReady = true;
+ this.emit("ready");
+ return this;
+ })
+ .then(null, function onError(aReason) {
+ DevToolsUtils.reportException("ShaderEditorPanel.prototype.open", aReason);
+ });
+ },
+
+ // DevToolPanel API
+
+ get target() {
+ return this._toolbox.target;
+ },
+
+ destroy: function () {
+ // Make sure this panel is not already destroyed.
+ if (this._destroyer) {
+ return this._destroyer;
+ }
+
+ return this._destroyer = this.panelWin.shutdownShaderEditor().then(() => {
+ // Destroy front to ensure packet handler is removed from client
+ this.panelWin.gFront.destroy();
+ this.emit("destroyed");
+ });
+ }
+};
diff --git a/devtools/client/shadereditor/shadereditor.js b/devtools/client/shadereditor/shadereditor.js
new file mode 100644
index 000000000..6b53302c4
--- /dev/null
+++ b/devtools/client/shadereditor/shadereditor.js
@@ -0,0 +1,633 @@
+/* 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 { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const {XPCOMUtils} = require("resource://gre/modules/XPCOMUtils.jsm");
+const {SideMenuWidget} = require("resource://devtools/client/shared/widgets/SideMenuWidget.jsm");
+const promise = require("promise");
+const Services = require("Services");
+const EventEmitter = require("devtools/shared/event-emitter");
+const Tooltip = require("devtools/client/shared/widgets/tooltip/Tooltip");
+const Editor = require("devtools/client/sourceeditor/editor");
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const {Heritage, WidgetMethods, setNamedTimeout} =
+ require("devtools/client/shared/widgets/view-helpers");
+const {Task} = require("devtools/shared/task");
+
+// The panel's window global is an EventEmitter firing the following events:
+const EVENTS = {
+ // When new programs are received from the server.
+ NEW_PROGRAM: "ShaderEditor:NewProgram",
+ PROGRAMS_ADDED: "ShaderEditor:ProgramsAdded",
+
+ // When the vertex and fragment sources were shown in the editor.
+ SOURCES_SHOWN: "ShaderEditor:SourcesShown",
+
+ // When a shader's source was edited and compiled via the editor.
+ SHADER_COMPILED: "ShaderEditor:ShaderCompiled",
+
+ // When the UI is reset from tab navigation
+ UI_RESET: "ShaderEditor:UIReset",
+
+ // When the editor's error markers are all removed
+ EDITOR_ERROR_MARKERS_REMOVED: "ShaderEditor:EditorCleaned"
+};
+XPCOMUtils.defineConstant(this, "EVENTS", EVENTS);
+
+const STRINGS_URI = "devtools/client/locales/shadereditor.properties";
+const HIGHLIGHT_TINT = [1, 0, 0.25, 1]; // rgba
+const TYPING_MAX_DELAY = 500; // ms
+const SHADERS_AUTOGROW_ITEMS = 4;
+const GUTTER_ERROR_PANEL_OFFSET_X = 7; // px
+const GUTTER_ERROR_PANEL_DELAY = 100; // ms
+const DEFAULT_EDITOR_CONFIG = {
+ gutters: ["errors"],
+ lineNumbers: true,
+ showAnnotationRuler: true
+};
+
+/**
+ * The current target and the WebGL Editor front, set by this tool's host.
+ */
+var gToolbox, gTarget, gFront;
+
+/**
+ * Initializes the shader editor controller and views.
+ */
+function startupShaderEditor() {
+ return promise.all([
+ EventsHandler.initialize(),
+ ShadersListView.initialize(),
+ ShadersEditorsView.initialize()
+ ]);
+}
+
+/**
+ * Destroys the shader editor controller and views.
+ */
+function shutdownShaderEditor() {
+ return promise.all([
+ EventsHandler.destroy(),
+ ShadersListView.destroy(),
+ ShadersEditorsView.destroy()
+ ]);
+}
+
+/**
+ * Functions handling target-related lifetime events.
+ */
+var EventsHandler = {
+ /**
+ * Listen for events emitted by the current tab target.
+ */
+ initialize: function () {
+ this._onHostChanged = this._onHostChanged.bind(this);
+ this._onTabNavigated = this._onTabNavigated.bind(this);
+ this._onProgramLinked = this._onProgramLinked.bind(this);
+ this._onProgramsAdded = this._onProgramsAdded.bind(this);
+ gToolbox.on("host-changed", this._onHostChanged);
+ gTarget.on("will-navigate", this._onTabNavigated);
+ gTarget.on("navigate", this._onTabNavigated);
+ gFront.on("program-linked", this._onProgramLinked);
+ this.reloadButton = $("#requests-menu-reload-notice-button");
+ this.reloadButton.addEventListener("command", this._onReloadCommand);
+ },
+
+ /**
+ * Remove events emitted by the current tab target.
+ */
+ destroy: function () {
+ gToolbox.off("host-changed", this._onHostChanged);
+ gTarget.off("will-navigate", this._onTabNavigated);
+ gTarget.off("navigate", this._onTabNavigated);
+ gFront.off("program-linked", this._onProgramLinked);
+ this.reloadButton.removeEventListener("command", this._onReloadCommand);
+ },
+
+ /**
+ * Handles a command event on reload button
+ */
+ _onReloadCommand() {
+ gFront.setup({ reload: true });
+ },
+
+ /**
+ * Handles a host change event on the parent toolbox.
+ */
+ _onHostChanged: function () {
+ if (gToolbox.hostType == "side") {
+ $("#shaders-pane").removeAttribute("height");
+ }
+ },
+
+ /**
+ * Called for each location change in the debugged tab.
+ */
+ _onTabNavigated: function (event, {isFrameSwitching}) {
+ switch (event) {
+ case "will-navigate": {
+ // Make sure the backend is prepared to handle WebGL contexts.
+ if (!isFrameSwitching) {
+ gFront.setup({ reload: false });
+ }
+
+ // Reset UI.
+ ShadersListView.empty();
+ // When switching to an iframe, ensure displaying the reload button.
+ // As the document has already been loaded without being hooked.
+ if (isFrameSwitching) {
+ $("#reload-notice").hidden = false;
+ $("#waiting-notice").hidden = true;
+ } else {
+ $("#reload-notice").hidden = true;
+ $("#waiting-notice").hidden = false;
+ }
+
+ $("#content").hidden = true;
+ window.emit(EVENTS.UI_RESET);
+
+ break;
+ }
+ case "navigate": {
+ // Manually retrieve the list of program actors known to the server,
+ // because the backend won't emit "program-linked" notifications
+ // in the case of a bfcache navigation (since no new programs are
+ // actually linked).
+ gFront.getPrograms().then(this._onProgramsAdded);
+ break;
+ }
+ }
+ },
+
+ /**
+ * Called every time a program was linked in the debugged tab.
+ */
+ _onProgramLinked: function (programActor) {
+ this._addProgram(programActor);
+ window.emit(EVENTS.NEW_PROGRAM);
+ },
+
+ /**
+ * Callback for the front's getPrograms() method.
+ */
+ _onProgramsAdded: function (programActors) {
+ programActors.forEach(this._addProgram);
+ window.emit(EVENTS.PROGRAMS_ADDED);
+ },
+
+ /**
+ * Adds a program to the shaders list and unhides any modal notices.
+ */
+ _addProgram: function (programActor) {
+ $("#waiting-notice").hidden = true;
+ $("#reload-notice").hidden = true;
+ $("#content").hidden = false;
+ ShadersListView.addProgram(programActor);
+ }
+};
+
+/**
+ * Functions handling the sources UI.
+ */
+var ShadersListView = Heritage.extend(WidgetMethods, {
+ /**
+ * Initialization function, called when the tool is started.
+ */
+ initialize: function () {
+ this.widget = new SideMenuWidget(this._pane = $("#shaders-pane"), {
+ showArrows: true,
+ showItemCheckboxes: true
+ });
+
+ this._onProgramSelect = this._onProgramSelect.bind(this);
+ this._onProgramCheck = this._onProgramCheck.bind(this);
+ this._onProgramMouseOver = this._onProgramMouseOver.bind(this);
+ this._onProgramMouseOut = this._onProgramMouseOut.bind(this);
+
+ this.widget.addEventListener("select", this._onProgramSelect, false);
+ this.widget.addEventListener("check", this._onProgramCheck, false);
+ this.widget.addEventListener("mouseover", this._onProgramMouseOver, true);
+ this.widget.addEventListener("mouseout", this._onProgramMouseOut, true);
+ },
+
+ /**
+ * Destruction function, called when the tool is closed.
+ */
+ destroy: function () {
+ this.widget.removeEventListener("select", this._onProgramSelect, false);
+ this.widget.removeEventListener("check", this._onProgramCheck, false);
+ this.widget.removeEventListener("mouseover", this._onProgramMouseOver, true);
+ this.widget.removeEventListener("mouseout", this._onProgramMouseOut, true);
+ },
+
+ /**
+ * Adds a program to this programs container.
+ *
+ * @param object programActor
+ * The program actor coming from the active thread.
+ */
+ addProgram: function (programActor) {
+ if (this.hasProgram(programActor)) {
+ return;
+ }
+
+ // Currently, there's no good way of differentiating between programs
+ // in a way that helps humans. It will be a good idea to implement a
+ // standard of allowing debuggees to add some identifiable metadata to their
+ // program sources or instances.
+ let label = L10N.getFormatStr("shadersList.programLabel", this.itemCount);
+ let contents = document.createElement("label");
+ contents.className = "plain program-item";
+ contents.setAttribute("value", label);
+ contents.setAttribute("crop", "start");
+ contents.setAttribute("flex", "1");
+
+ // Append a program item to this container.
+ this.push([contents], {
+ index: -1, /* specifies on which position should the item be appended */
+ attachment: {
+ label: label,
+ programActor: programActor,
+ checkboxState: true,
+ checkboxTooltip: L10N.getStr("shadersList.blackboxLabel")
+ }
+ });
+
+ // Make sure there's always a selected item available.
+ if (!this.selectedItem) {
+ this.selectedIndex = 0;
+ }
+
+ // Prevent this container from growing indefinitely in height when the
+ // toolbox is docked to the side.
+ if (gToolbox.hostType == "side" && this.itemCount == SHADERS_AUTOGROW_ITEMS) {
+ this._pane.setAttribute("height", this._pane.getBoundingClientRect().height);
+ }
+ },
+
+ /**
+ * Returns whether a program was already added to this programs container.
+ *
+ * @param object programActor
+ * The program actor coming from the active thread.
+ * @param boolean
+ * True if the program was added, false otherwise.
+ */
+ hasProgram: function (programActor) {
+ return !!this.attachments.filter(e => e.programActor == programActor).length;
+ },
+
+ /**
+ * The select listener for the programs container.
+ */
+ _onProgramSelect: function ({ detail: sourceItem }) {
+ if (!sourceItem) {
+ return;
+ }
+ // The container is not empty and an actual item was selected.
+ let attachment = sourceItem.attachment;
+
+ function getShaders() {
+ return promise.all([
+ attachment.vs || (attachment.vs = attachment.programActor.getVertexShader()),
+ attachment.fs || (attachment.fs = attachment.programActor.getFragmentShader())
+ ]);
+ }
+ function getSources([vertexShaderActor, fragmentShaderActor]) {
+ return promise.all([
+ vertexShaderActor.getText(),
+ fragmentShaderActor.getText()
+ ]);
+ }
+ function showSources([vertexShaderText, fragmentShaderText]) {
+ return ShadersEditorsView.setText({
+ vs: vertexShaderText,
+ fs: fragmentShaderText
+ });
+ }
+
+ getShaders()
+ .then(getSources)
+ .then(showSources)
+ .then(null, e => console.error(e));
+ },
+
+ /**
+ * The check listener for the programs container.
+ */
+ _onProgramCheck: function ({ detail: { checked }, target }) {
+ let sourceItem = this.getItemForElement(target);
+ let attachment = sourceItem.attachment;
+ attachment.isBlackBoxed = !checked;
+ attachment.programActor[checked ? "unblackbox" : "blackbox"]();
+ },
+
+ /**
+ * The mouseover listener for the programs container.
+ */
+ _onProgramMouseOver: function (e) {
+ let sourceItem = this.getItemForElement(e.target, { noSiblings: true });
+ if (sourceItem && !sourceItem.attachment.isBlackBoxed) {
+ sourceItem.attachment.programActor.highlight(HIGHLIGHT_TINT);
+
+ if (e instanceof Event) {
+ e.preventDefault();
+ e.stopPropagation();
+ }
+ }
+ },
+
+ /**
+ * The mouseout listener for the programs container.
+ */
+ _onProgramMouseOut: function (e) {
+ let sourceItem = this.getItemForElement(e.target, { noSiblings: true });
+ if (sourceItem && !sourceItem.attachment.isBlackBoxed) {
+ sourceItem.attachment.programActor.unhighlight();
+
+ if (e instanceof Event) {
+ e.preventDefault();
+ e.stopPropagation();
+ }
+ }
+ }
+});
+
+/**
+ * Functions handling the editors displaying the vertex and fragment shaders.
+ */
+var ShadersEditorsView = {
+ /**
+ * Initialization function, called when the tool is started.
+ */
+ initialize: function () {
+ XPCOMUtils.defineLazyGetter(this, "_editorPromises", () => new Map());
+ this._vsFocused = this._onFocused.bind(this, "vs", "fs");
+ this._fsFocused = this._onFocused.bind(this, "fs", "vs");
+ this._vsChanged = this._onChanged.bind(this, "vs");
+ this._fsChanged = this._onChanged.bind(this, "fs");
+ },
+
+ /**
+ * Destruction function, called when the tool is closed.
+ */
+ destroy: Task.async(function* () {
+ this._destroyed = true;
+ yield this._toggleListeners("off");
+ for (let p of this._editorPromises.values()) {
+ let editor = yield p;
+ editor.destroy();
+ }
+ }),
+
+ /**
+ * Sets the text displayed in the vertex and fragment shader editors.
+ *
+ * @param object sources
+ * An object containing the following properties
+ * - vs: the vertex shader source code
+ * - fs: the fragment shader source code
+ * @return object
+ * A promise resolving upon completion of text setting.
+ */
+ setText: function (sources) {
+ let view = this;
+ function setTextAndClearHistory(editor, text) {
+ editor.setText(text);
+ editor.clearHistory();
+ }
+
+ return Task.spawn(function* () {
+ yield view._toggleListeners("off");
+ yield promise.all([
+ view._getEditor("vs").then(e => setTextAndClearHistory(e, sources.vs)),
+ view._getEditor("fs").then(e => setTextAndClearHistory(e, sources.fs))
+ ]);
+ yield view._toggleListeners("on");
+ }).then(() => window.emit(EVENTS.SOURCES_SHOWN, sources));
+ },
+
+ /**
+ * Lazily initializes and returns a promise for an Editor instance.
+ *
+ * @param string type
+ * Specifies for which shader type should an editor be retrieved,
+ * either are "vs" for a vertex, or "fs" for a fragment shader.
+ * @return object
+ * Returns a promise that resolves to an editor instance
+ */
+ _getEditor: function (type) {
+ if (this._editorPromises.has(type)) {
+ return this._editorPromises.get(type);
+ }
+
+ let deferred = promise.defer();
+ this._editorPromises.set(type, deferred.promise);
+
+ // Initialize the source editor and store the newly created instance
+ // in the ether of a resolved promise's value.
+ let parent = $("#" + type + "-editor");
+ let editor = new Editor(DEFAULT_EDITOR_CONFIG);
+ editor.config.mode = Editor.modes[type];
+
+ if (this._destroyed) {
+ deferred.resolve(editor);
+ } else {
+ editor.appendTo(parent).then(() => deferred.resolve(editor));
+ }
+
+ return deferred.promise;
+ },
+
+ /**
+ * Toggles all the event listeners for the editors either on or off.
+ *
+ * @param string flag
+ * Either "on" to enable the event listeners, "off" to disable them.
+ * @return object
+ * A promise resolving upon completion of toggling the listeners.
+ */
+ _toggleListeners: function (flag) {
+ return promise.all(["vs", "fs"].map(type => {
+ return this._getEditor(type).then(editor => {
+ editor[flag]("focus", this["_" + type + "Focused"]);
+ editor[flag]("change", this["_" + type + "Changed"]);
+ });
+ }));
+ },
+
+ /**
+ * The focus listener for a source editor.
+ *
+ * @param string focused
+ * The corresponding shader type for the focused editor (e.g. "vs").
+ * @param string focused
+ * The corresponding shader type for the other editor (e.g. "fs").
+ */
+ _onFocused: function (focused, unfocused) {
+ $("#" + focused + "-editor-label").setAttribute("selected", "");
+ $("#" + unfocused + "-editor-label").removeAttribute("selected");
+ },
+
+ /**
+ * The change listener for a source editor.
+ *
+ * @param string type
+ * The corresponding shader type for the focused editor (e.g. "vs").
+ */
+ _onChanged: function (type) {
+ setNamedTimeout("gl-typed", TYPING_MAX_DELAY, () => this._doCompile(type));
+
+ // Remove all the gutter markers and line classes from the editor.
+ this._cleanEditor(type);
+ },
+
+ /**
+ * Recompiles the source code for the shader being edited.
+ * This function is fired at a certain delay after the user stops typing.
+ *
+ * @param string type
+ * The corresponding shader type for the focused editor (e.g. "vs").
+ */
+ _doCompile: function (type) {
+ Task.spawn(function* () {
+ let editor = yield this._getEditor(type);
+ let shaderActor = yield ShadersListView.selectedAttachment[type];
+
+ try {
+ yield shaderActor.compile(editor.getText());
+ this._onSuccessfulCompilation();
+ } catch (e) {
+ this._onFailedCompilation(type, editor, e);
+ }
+ }.bind(this));
+ },
+
+ /**
+ * Called uppon a successful shader compilation.
+ */
+ _onSuccessfulCompilation: function () {
+ // Signal that the shader was compiled successfully.
+ window.emit(EVENTS.SHADER_COMPILED, null);
+ },
+
+ /**
+ * Called uppon an unsuccessful shader compilation.
+ */
+ _onFailedCompilation: function (type, editor, errors) {
+ let lineCount = editor.lineCount();
+ let currentLine = editor.getCursor().line;
+ let listeners = { mouseover: this._onMarkerMouseOver };
+
+ function matchLinesAndMessages(string) {
+ return {
+ // First number that is not equal to 0.
+ lineMatch: string.match(/\d{2,}|[1-9]/),
+ // The string after all the numbers, semicolons and spaces.
+ textMatch: string.match(/[^\s\d:][^\r\n|]*/)
+ };
+ }
+ function discardInvalidMatches(e) {
+ // Discard empty line and text matches.
+ return e.lineMatch && e.textMatch;
+ }
+ function sanitizeValidMatches(e) {
+ return {
+ // Drivers might yield confusing line numbers under some obscure
+ // circumstances. Don't throw the errors away in those cases,
+ // just display them on the currently edited line.
+ line: e.lineMatch[0] > lineCount ? currentLine : e.lineMatch[0] - 1,
+ // Trim whitespace from the beginning and the end of the message,
+ // and replace all other occurences of double spaces to a single space.
+ text: e.textMatch[0].trim().replace(/\s{2,}/g, " ")
+ };
+ }
+ function sortByLine(first, second) {
+ // Sort all the errors ascending by their corresponding line number.
+ return first.line > second.line ? 1 : -1;
+ }
+ function groupSameLineMessages(accumulator, current) {
+ // Group errors corresponding to the same line number to a single object.
+ let previous = accumulator[accumulator.length - 1];
+ if (!previous || previous.line != current.line) {
+ return [...accumulator, {
+ line: current.line,
+ messages: [current.text]
+ }];
+ } else {
+ previous.messages.push(current.text);
+ return accumulator;
+ }
+ }
+ function displayErrors({ line, messages }) {
+ // Add gutter markers and line classes for every error in the source.
+ editor.addMarker(line, "errors", "error");
+ editor.setMarkerListeners(line, "errors", "error", listeners, messages);
+ editor.addLineClass(line, "error-line");
+ }
+
+ (this._errors[type] = errors.link
+ .split("ERROR")
+ .map(matchLinesAndMessages)
+ .filter(discardInvalidMatches)
+ .map(sanitizeValidMatches)
+ .sort(sortByLine)
+ .reduce(groupSameLineMessages, []))
+ .forEach(displayErrors);
+
+ // Signal that the shader wasn't compiled successfully.
+ window.emit(EVENTS.SHADER_COMPILED, errors);
+ },
+
+ /**
+ * Event listener for the 'mouseover' event on a marker in the editor gutter.
+ */
+ _onMarkerMouseOver: function (line, node, messages) {
+ if (node._markerErrorsTooltip) {
+ return;
+ }
+
+ let tooltip = node._markerErrorsTooltip = new Tooltip(document);
+ tooltip.defaultOffsetX = GUTTER_ERROR_PANEL_OFFSET_X;
+ tooltip.setTextContent({ messages: messages });
+ tooltip.startTogglingOnHover(node, () => true, {
+ toggleDelay: GUTTER_ERROR_PANEL_DELAY
+ });
+ },
+
+ /**
+ * Removes all the gutter markers and line classes from the editor.
+ */
+ _cleanEditor: function (type) {
+ this._getEditor(type).then(editor => {
+ editor.removeAllMarkers("errors");
+ this._errors[type].forEach(e => editor.removeLineClass(e.line));
+ this._errors[type].length = 0;
+ window.emit(EVENTS.EDITOR_ERROR_MARKERS_REMOVED);
+ });
+ },
+
+ _errors: {
+ vs: [],
+ fs: []
+ }
+};
+
+/**
+ * Localization convenience methods.
+ */
+var L10N = new LocalizationHelper(STRINGS_URI);
+
+/**
+ * Convenient way of emitting events from the panel window.
+ */
+EventEmitter.decorate(this);
+
+/**
+ * DOM query helper.
+ */
+var $ = (selector, target = document) => target.querySelector(selector);
diff --git a/devtools/client/shadereditor/shadereditor.xul b/devtools/client/shadereditor/shadereditor.xul
new file mode 100644
index 000000000..dc7f764b7
--- /dev/null
+++ b/devtools/client/shadereditor/shadereditor.xul
@@ -0,0 +1,70 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/skin/widgets.css" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/skin/shadereditor.css" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/content/shared/widgets/widgets.css" type="text/css"?>
+<!DOCTYPE window [
+ <!ENTITY % debuggerDTD SYSTEM "chrome://devtools/locale/shadereditor.dtd">
+ %debuggerDTD;
+]>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script type="application/javascript;version=1.8"
+ src="chrome://devtools/content/shared/theme-switching.js"/>
+
+ <script type="application/javascript" src="shadereditor.js"/>
+
+ <vbox class="theme-body" flex="1">
+ <hbox id="reload-notice"
+ class="notice-container"
+ align="center"
+ pack="center"
+ flex="1">
+ <button id="requests-menu-reload-notice-button"
+ class="devtools-toolbarbutton"
+ standalone="true"
+ label="&shaderEditorUI.reloadNotice1;"/>
+ <label id="requests-menu-reload-notice-label"
+ class="plain"
+ value="&shaderEditorUI.reloadNotice2;"/>
+ </hbox>
+ <hbox id="waiting-notice"
+ class="notice-container devtools-throbber"
+ align="center"
+ pack="center"
+ flex="1"
+ hidden="true">
+ <label id="requests-menu-waiting-notice-label"
+ class="plain"
+ value="&shaderEditorUI.emptyNotice;"/>
+ </hbox>
+
+ <box id="content"
+ class="devtools-responsive-container"
+ flex="1"
+ hidden="true">
+ <vbox id="shaders-pane"/>
+ <splitter class="devtools-side-splitter"/>
+ <box id="shaders-editors" class="devtools-responsive-container" flex="1">
+ <vbox flex="1">
+ <vbox id="vs-editor" flex="1"/>
+ <label id="vs-editor-label"
+ class="plain editor-label"
+ value="&shaderEditorUI.vertexShader;"/>
+ </vbox>
+ <splitter id="editors-splitter" class="devtools-side-splitter"/>
+ <vbox flex="1">
+ <vbox id="fs-editor" flex="1"/>
+ <label id="fs-editor-label"
+ class="plain editor-label"
+ value="&shaderEditorUI.fragmentShader;"/>
+ </vbox>
+ </box>
+ </box>
+ </vbox>
+
+</window>
diff --git a/devtools/client/shadereditor/test/.eslintrc.js b/devtools/client/shadereditor/test/.eslintrc.js
new file mode 100644
index 000000000..8d15a76d9
--- /dev/null
+++ b/devtools/client/shadereditor/test/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../../.eslintrc.mochitests.js"
+};
diff --git a/devtools/client/shadereditor/test/browser.ini b/devtools/client/shadereditor/test/browser.ini
new file mode 100644
index 000000000..b26bc3a74
--- /dev/null
+++ b/devtools/client/shadereditor/test/browser.ini
@@ -0,0 +1,47 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ doc_blended-geometry.html
+ doc_multiple-contexts.html
+ doc_overlapping-geometry.html
+ doc_shader-order.html
+ doc_simple-canvas.html
+ head.js
+
+[browser_se_aaa_run_first_leaktest.js]
+[browser_se_bfcache.js]
+skip-if = true # Bug 942473, caused by Bug 940541
+[browser_se_editors-contents.js]
+[browser_se_editors-error-gutter.js]
+[browser_se_editors-error-tooltip.js]
+[browser_se_editors-lazy-init.js]
+[browser_se_first-run.js]
+[browser_se_navigation.js]
+[browser_se_programs-blackbox-01.js]
+[browser_se_programs-blackbox-02.js]
+[browser_se_programs-cache.js]
+[browser_se_programs-highlight-01.js]
+[browser_se_programs-highlight-02.js]
+[browser_se_programs-list.js]
+[browser_se_shaders-edit-01.js]
+[browser_se_shaders-edit-02.js]
+[browser_se_shaders-edit-03.js]
+[browser_webgl-actor-test-01.js]
+[browser_webgl-actor-test-02.js]
+[browser_webgl-actor-test-03.js]
+[browser_webgl-actor-test-04.js]
+[browser_webgl-actor-test-05.js]
+[browser_webgl-actor-test-06.js]
+[browser_webgl-actor-test-07.js]
+[browser_webgl-actor-test-08.js]
+[browser_webgl-actor-test-09.js]
+[browser_webgl-actor-test-10.js]
+[browser_webgl-actor-test-11.js]
+[browser_webgl-actor-test-12.js]
+[browser_webgl-actor-test-13.js]
+[browser_webgl-actor-test-14.js]
+[browser_webgl-actor-test-15.js]
+[browser_webgl-actor-test-16.js]
+[browser_webgl-actor-test-17.js]
+[browser_webgl-actor-test-18.js]
diff --git a/devtools/client/shadereditor/test/browser_se_aaa_run_first_leaktest.js b/devtools/client/shadereditor/test/browser_se_aaa_run_first_leaktest.js
new file mode 100644
index 000000000..6efe51091
--- /dev/null
+++ b/devtools/client/shadereditor/test/browser_se_aaa_run_first_leaktest.js
@@ -0,0 +1,17 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the shader editor leaks on initialization and sudden destruction.
+ * You can also use this initialization format as a template for other tests.
+ */
+
+function* ifWebGLSupported() {
+ let { target, panel } = yield initShaderEditor(SIMPLE_CANVAS_URL);
+
+ ok(target, "Should have a target available.");
+ ok(panel, "Should have a panel available.");
+
+ yield teardown(panel);
+ finish();
+}
diff --git a/devtools/client/shadereditor/test/browser_se_bfcache.js b/devtools/client/shadereditor/test/browser_se_bfcache.js
new file mode 100644
index 000000000..2b568e3fe
--- /dev/null
+++ b/devtools/client/shadereditor/test/browser_se_bfcache.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the shader editor works with bfcache.
+ */
+function* ifWebGLSupported() {
+ let { target, panel } = yield initShaderEditor(SIMPLE_CANVAS_URL);
+ let { gFront, $, EVENTS, ShadersListView, ShadersEditorsView } = panel.panelWin;
+
+ // Attach frame scripts if in e10s to perform
+ // history navigation via the content
+ loadFrameScripts();
+
+ let reloaded = reload(target);
+ let firstProgram = yield once(gFront, "program-linked");
+ yield reloaded;
+
+ let navigated = navigate(target, MULTIPLE_CONTEXTS_URL);
+ let [secondProgram, thirdProgram] = yield getPrograms(gFront, 2);
+ yield navigated;
+
+ let vsEditor = yield ShadersEditorsView._getEditor("vs");
+ let fsEditor = yield ShadersEditorsView._getEditor("fs");
+
+ yield navigateInHistory(target, "back", "will-navigate");
+ yield once(panel.panelWin, EVENTS.PROGRAMS_ADDED);
+ yield once(panel.panelWin, EVENTS.SOURCES_SHOWN);
+
+ is($("#content").hidden, false,
+ "The tool's content should not be hidden.");
+ is(ShadersListView.itemCount, 1,
+ "The shaders list contains one entry after navigating back.");
+ is(ShadersListView.selectedIndex, 0,
+ "The shaders list has a correct selection after navigating back.");
+
+ is(vsEditor.getText().indexOf("gl_Position"), 170,
+ "The vertex shader editor contains the correct text.");
+ is(fsEditor.getText().indexOf("gl_FragColor"), 97,
+ "The fragment shader editor contains the correct text.");
+
+ yield navigateInHistory(target, "forward", "will-navigate");
+ yield once(panel.panelWin, EVENTS.PROGRAMS_ADDED);
+ yield once(panel.panelWin, EVENTS.SOURCES_SHOWN);
+
+ is($("#content").hidden, false,
+ "The tool's content should not be hidden.");
+ is(ShadersListView.itemCount, 2,
+ "The shaders list contains two entries after navigating forward.");
+ is(ShadersListView.selectedIndex, 0,
+ "The shaders list has a correct selection after navigating forward.");
+
+ is(vsEditor.getText().indexOf("gl_Position"), 100,
+ "The vertex shader editor contains the correct text.");
+ is(fsEditor.getText().indexOf("gl_FragColor"), 89,
+ "The fragment shader editor contains the correct text.");
+
+ yield teardown(panel);
+ finish();
+}
diff --git a/devtools/client/shadereditor/test/browser_se_editors-contents.js b/devtools/client/shadereditor/test/browser_se_editors-contents.js
new file mode 100644
index 000000000..fdf2612ed
--- /dev/null
+++ b/devtools/client/shadereditor/test/browser_se_editors-contents.js
@@ -0,0 +1,30 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the editors contain the correct text when a program
+ * becomes available.
+ */
+
+function* ifWebGLSupported() {
+ let { target, panel } = yield initShaderEditor(SIMPLE_CANVAS_URL);
+ let { gFront, ShadersEditorsView, EVENTS } = panel.panelWin;
+
+ reload(target);
+ yield promise.all([
+ once(gFront, "program-linked"),
+ once(panel.panelWin, EVENTS.SOURCES_SHOWN)
+ ]);
+
+ let vsEditor = yield ShadersEditorsView._getEditor("vs");
+ let fsEditor = yield ShadersEditorsView._getEditor("fs");
+
+
+ is(vsEditor.getText().indexOf("gl_Position"), 170,
+ "The vertex shader editor contains the correct text.");
+ is(fsEditor.getText().indexOf("gl_FragColor"), 97,
+ "The fragment shader editor contains the correct text.");
+
+ yield teardown(panel);
+ finish();
+}
diff --git a/devtools/client/shadereditor/test/browser_se_editors-error-gutter.js b/devtools/client/shadereditor/test/browser_se_editors-error-gutter.js
new file mode 100644
index 000000000..439d6074f
--- /dev/null
+++ b/devtools/client/shadereditor/test/browser_se_editors-error-gutter.js
@@ -0,0 +1,156 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if error indicators are shown in the editor's gutter and text area
+ * when there's a shader compilation error.
+ */
+
+function* ifWebGLSupported() {
+ let { target, panel } = yield initShaderEditor(SIMPLE_CANVAS_URL);
+ let { gFront, EVENTS, ShadersEditorsView } = panel.panelWin;
+
+ reload(target);
+ yield promise.all([
+ once(gFront, "program-linked"),
+ once(panel.panelWin, EVENTS.SOURCES_SHOWN)
+ ]);
+
+ let vsEditor = yield ShadersEditorsView._getEditor("vs");
+ let fsEditor = yield ShadersEditorsView._getEditor("fs");
+
+ vsEditor.replaceText("vec3", { line: 7, ch: 22 }, { line: 7, ch: 26 });
+ let [, vertError] = yield onceSpread(panel.panelWin, EVENTS.SHADER_COMPILED);
+ checkHasVertFirstError(true, vertError);
+ checkHasVertSecondError(false, vertError);
+ info("Error marks added in the vertex shader editor.");
+
+ vsEditor.insertText(" ", { line: 1, ch: 0 });
+ yield once(panel.panelWin, EVENTS.EDITOR_ERROR_MARKERS_REMOVED);
+ is(vsEditor.getText(1), " precision lowp float;", "Typed space.");
+ checkHasVertFirstError(false, vertError);
+ checkHasVertSecondError(false, vertError);
+ info("Error marks removed while typing in the vertex shader editor.");
+
+ [, vertError] = yield onceSpread(panel.panelWin, EVENTS.SHADER_COMPILED);
+ checkHasVertFirstError(true, vertError);
+ checkHasVertSecondError(false, vertError);
+ info("Error marks were re-added after recompiling the vertex shader.");
+
+ fsEditor.replaceText("vec4", { line: 2, ch: 14 }, { line: 2, ch: 18 });
+ let [, fragError] = yield onceSpread(panel.panelWin, EVENTS.SHADER_COMPILED);
+ checkHasVertFirstError(true, vertError);
+ checkHasVertSecondError(false, vertError);
+ checkHasFragError(true, fragError);
+ info("Error marks added in the fragment shader editor.");
+
+ fsEditor.insertText(" ", { line: 1, ch: 0 });
+ yield once(panel.panelWin, EVENTS.EDITOR_ERROR_MARKERS_REMOVED);
+ is(fsEditor.getText(1), " precision lowp float;", "Typed space.");
+ checkHasVertFirstError(true, vertError);
+ checkHasVertSecondError(false, vertError);
+ checkHasFragError(false, fragError);
+ info("Error marks removed while typing in the fragment shader editor.");
+
+ [, fragError] = yield onceSpread(panel.panelWin, EVENTS.SHADER_COMPILED);
+ checkHasVertFirstError(true, vertError);
+ checkHasVertSecondError(false, vertError);
+ checkHasFragError(true, fragError);
+ info("Error marks were re-added after recompiling the fragment shader.");
+
+ vsEditor.replaceText("2", { line: 3, ch: 19 }, { line: 3, ch: 20 });
+ yield once(panel.panelWin, EVENTS.EDITOR_ERROR_MARKERS_REMOVED);
+ checkHasVertFirstError(false, vertError);
+ checkHasVertSecondError(false, vertError);
+ checkHasFragError(true, fragError);
+ info("Error marks removed while typing in the vertex shader editor again.");
+
+ [, vertError] = yield onceSpread(panel.panelWin, EVENTS.SHADER_COMPILED);
+ checkHasVertFirstError(true, vertError);
+ checkHasVertSecondError(true, vertError);
+ checkHasFragError(true, fragError);
+ info("Error marks were re-added after recompiling the fragment shader again.");
+
+ yield teardown(panel);
+ finish();
+
+ function checkHasVertFirstError(bool, error) {
+ ok(error, "Vertex shader compiled with errors.");
+ isnot(error.link, "", "The linkage status should not be empty.");
+
+ let line = 7;
+ info("Checking first vertex shader error on line " + line + "...");
+
+ is(vsEditor.hasMarker(line, "errors", "error"), bool,
+ "Error is " + (bool ? "" : "not ") + "shown in the editor's gutter.");
+ is(vsEditor.hasLineClass(line, "error-line"), bool,
+ "Error style is " + (bool ? "" : "not ") + "applied to the faulty line.");
+
+ let parsed = ShadersEditorsView._errors.vs;
+ is(parsed.length >= 1, bool,
+ "There's " + (bool ? ">= 1" : "< 1") + " parsed vertex shader error(s).");
+
+ if (bool) {
+ is(parsed[0].line, line,
+ "The correct line was parsed.");
+ is(parsed[0].messages.length, 2,
+ "There are 2 parsed messages.");
+ ok(parsed[0].messages[0].includes("'constructor' : too many arguments"),
+ "The correct first message was parsed.");
+ ok(parsed[0].messages[1].includes("'assign' : cannot convert from"),
+ "The correct second message was parsed.");
+ }
+ }
+
+ function checkHasVertSecondError(bool, error) {
+ ok(error, "Vertex shader compiled with errors.");
+ isnot(error.link, "", "The linkage status should not be empty.");
+
+ let line = 8;
+ info("Checking second vertex shader error on line " + line + "...");
+
+ is(vsEditor.hasMarker(line, "errors", "error"), bool,
+ "Error is " + (bool ? "" : "not ") + "shown in the editor's gutter.");
+ is(vsEditor.hasLineClass(line, "error-line"), bool,
+ "Error style is " + (bool ? "" : "not ") + "applied to the faulty line.");
+
+ let parsed = ShadersEditorsView._errors.vs;
+ is(parsed.length >= 2, bool,
+ "There's " + (bool ? ">= 2" : "< 2") + " parsed vertex shader error(s).");
+
+ if (bool) {
+ is(parsed[1].line, line,
+ "The correct line was parsed.");
+ is(parsed[1].messages.length, 1,
+ "There is 1 parsed message.");
+ ok(parsed[1].messages[0].includes("'assign' : cannot convert from"),
+ "The correct message was parsed.");
+ }
+ }
+
+ function checkHasFragError(bool, error) {
+ ok(error, "Fragment shader compiled with errors.");
+ isnot(error.link, "", "The linkage status should not be empty.");
+
+ let line = 5;
+ info("Checking first vertex shader error on line " + line + "...");
+
+ is(fsEditor.hasMarker(line, "errors", "error"), bool,
+ "Error is " + (bool ? "" : "not ") + "shown in the editor's gutter.");
+ is(fsEditor.hasLineClass(line, "error-line"), bool,
+ "Error style is " + (bool ? "" : "not ") + "applied to the faulty line.");
+
+ let parsed = ShadersEditorsView._errors.fs;
+ is(parsed.length >= 1, bool,
+ "There's " + (bool ? ">= 2" : "< 1") + " parsed fragment shader error(s).");
+
+ if (bool) {
+ is(parsed[0].line, line,
+ "The correct line was parsed.");
+ is(parsed[0].messages.length, 1,
+ "There is 1 parsed message.");
+ ok(parsed[0].messages[0].includes("'constructor' : too many arguments"),
+ "The correct message was parsed.");
+ }
+ }
+}
diff --git a/devtools/client/shadereditor/test/browser_se_editors-error-tooltip.js b/devtools/client/shadereditor/test/browser_se_editors-error-tooltip.js
new file mode 100644
index 000000000..1ce31bebf
--- /dev/null
+++ b/devtools/client/shadereditor/test/browser_se_editors-error-tooltip.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if error tooltips can be opened from the editor's gutter when there's
+ * a shader compilation error.
+ */
+
+function* ifWebGLSupported() {
+ let { target, panel } = yield initShaderEditor(SIMPLE_CANVAS_URL);
+ let { gFront, EVENTS, ShadersEditorsView } = panel.panelWin;
+
+ reload(target);
+ yield promise.all([
+ once(gFront, "program-linked"),
+ once(panel.panelWin, EVENTS.SOURCES_SHOWN)
+ ]);
+
+ let vsEditor = yield ShadersEditorsView._getEditor("vs");
+ let fsEditor = yield ShadersEditorsView._getEditor("fs");
+
+ vsEditor.replaceText("vec3", { line: 7, ch: 22 }, { line: 7, ch: 26 });
+ yield once(panel.panelWin, EVENTS.SHADER_COMPILED);
+
+ // Synthesizing 'mouseover' events doesn't work, hack around this by
+ // manually calling the event listener with the expected arguments.
+ let editorDocument = vsEditor.container.contentDocument;
+ let marker = editorDocument.querySelector(".error");
+ let parsed = ShadersEditorsView._errors.vs[0].messages;
+ ShadersEditorsView._onMarkerMouseOver(7, marker, parsed);
+
+ let tooltip = marker._markerErrorsTooltip;
+ ok(tooltip, "A tooltip was created successfully.");
+
+ let content = tooltip.content;
+ ok(tooltip.content,
+ "Some tooltip's content was set.");
+ ok(tooltip.content.className.includes("devtools-tooltip-simple-text-container"),
+ "The tooltip's content container was created correctly.");
+
+ let messages = content.childNodes;
+ is(messages.length, 2,
+ "There are two messages displayed in the tooltip.");
+ ok(messages[0].className.includes("devtools-tooltip-simple-text"),
+ "The first message was created correctly.");
+ ok(messages[1].className.includes("devtools-tooltip-simple-text"),
+ "The second message was created correctly.");
+
+ ok(messages[0].textContent.includes("'constructor' : too many arguments"),
+ "The first message contains the correct text.");
+ ok(messages[1].textContent.includes("'assign' : cannot convert"),
+ "The second message contains the correct text.");
+
+ yield teardown(panel);
+ finish();
+}
diff --git a/devtools/client/shadereditor/test/browser_se_editors-lazy-init.js b/devtools/client/shadereditor/test/browser_se_editors-lazy-init.js
new file mode 100644
index 000000000..b2d9d888e
--- /dev/null
+++ b/devtools/client/shadereditor/test/browser_se_editors-lazy-init.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if source editors are lazily initialized.
+ */
+
+function* ifWebGLSupported() {
+ let { target, panel } = yield initShaderEditor(SIMPLE_CANVAS_URL);
+ let { gFront, ShadersEditorsView } = panel.panelWin;
+
+ reload(target);
+ yield once(gFront, "program-linked");
+
+ let vsEditor = yield ShadersEditorsView._getEditor("vs");
+ let fsEditor = yield ShadersEditorsView._getEditor("fs");
+
+ ok(vsEditor, "A vertex shader editor was initialized.");
+ ok(fsEditor, "A fragment shader editor was initialized.");
+
+ isnot(vsEditor, fsEditor,
+ "The vertex shader editor is distinct from the fragment shader editor.");
+
+ let vsEditor2 = yield ShadersEditorsView._getEditor("vs");
+ let fsEditor2 = yield ShadersEditorsView._getEditor("fs");
+
+ is(vsEditor, vsEditor2,
+ "The vertex shader editor instances are cached.");
+ is(fsEditor, fsEditor2,
+ "The fragment shader editor instances are cached.");
+
+ yield teardown(panel);
+ finish();
+}
diff --git a/devtools/client/shadereditor/test/browser_se_first-run.js b/devtools/client/shadereditor/test/browser_se_first-run.js
new file mode 100644
index 000000000..33239bcbe
--- /dev/null
+++ b/devtools/client/shadereditor/test/browser_se_first-run.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the shader editor shows the appropriate UI when opened.
+ */
+
+function* ifWebGLSupported() {
+ let { target, panel } = yield initShaderEditor(SIMPLE_CANVAS_URL);
+ let { gFront, $ } = panel.panelWin;
+
+ is($("#reload-notice").hidden, false,
+ "The 'reload this page' notice should initially be visible.");
+ is($("#waiting-notice").hidden, true,
+ "The 'waiting for a WebGL context' notice should initially be hidden.");
+ is($("#content").hidden, true,
+ "The tool's content should initially be hidden.");
+
+ let navigating = once(target, "will-navigate");
+ let linked = once(gFront, "program-linked");
+ reload(target);
+
+ yield navigating;
+
+ is($("#reload-notice").hidden, true,
+ "The 'reload this page' notice should be hidden when navigating.");
+ is($("#waiting-notice").hidden, false,
+ "The 'waiting for a WebGL context' notice should be visible when navigating.");
+ is($("#content").hidden, true,
+ "The tool's content should still be hidden.");
+
+ yield linked;
+
+ is($("#reload-notice").hidden, true,
+ "The 'reload this page' notice should be hidden after linking.");
+ is($("#waiting-notice").hidden, true,
+ "The 'waiting for a WebGL context' notice should be hidden after linking.");
+ is($("#content").hidden, false,
+ "The tool's content should not be hidden anymore.");
+
+ yield teardown(panel);
+ finish();
+}
diff --git a/devtools/client/shadereditor/test/browser_se_navigation.js b/devtools/client/shadereditor/test/browser_se_navigation.js
new file mode 100644
index 000000000..f1adc3e76
--- /dev/null
+++ b/devtools/client/shadereditor/test/browser_se_navigation.js
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests target navigations are handled correctly in the UI.
+ */
+
+function* ifWebGLSupported() {
+ let { target, panel } = yield initShaderEditor(SIMPLE_CANVAS_URL);
+ let { gFront, $, EVENTS, ShadersListView, ShadersEditorsView } = panel.panelWin;
+
+ reload(target);
+ yield promise.all([
+ once(gFront, "program-linked"),
+ once(panel.panelWin, EVENTS.SOURCES_SHOWN)
+ ]);
+
+ is($("#reload-notice").hidden, true,
+ "The 'reload this page' notice should be hidden after linking.");
+ is($("#waiting-notice").hidden, true,
+ "The 'waiting for a WebGL context' notice should be visible after linking.");
+ is($("#content").hidden, false,
+ "The tool's content should not be hidden anymore.");
+
+ is(ShadersListView.itemCount, 1,
+ "The shaders list contains one entry.");
+ is(ShadersListView.selectedItem, ShadersListView.items[0],
+ "The shaders list has a correct item selected.");
+ is(ShadersListView.selectedIndex, 0,
+ "The shaders list has a correct index selected.");
+
+ let vsEditor = yield ShadersEditorsView._getEditor("vs");
+ let fsEditor = yield ShadersEditorsView._getEditor("fs");
+
+ is(vsEditor.getText().indexOf("gl_Position"), 170,
+ "The vertex shader editor contains the correct text.");
+ is(fsEditor.getText().indexOf("gl_FragColor"), 97,
+ "The fragment shader editor contains the correct text.");
+
+ let navigating = once(target, "will-navigate");
+ let navigated = once(target, "will-navigate");
+ navigate(target, "about:blank");
+
+ yield promise.all([navigating, once(panel.panelWin, EVENTS.UI_RESET) ]);
+
+ is($("#reload-notice").hidden, true,
+ "The 'reload this page' notice should be hidden while navigating.");
+ is($("#waiting-notice").hidden, false,
+ "The 'waiting for a WebGL context' notice should be visible while navigating.");
+ is($("#content").hidden, true,
+ "The tool's content should be hidden now that there's no WebGL content.");
+
+ is(ShadersListView.itemCount, 0,
+ "The shaders list should be empty.");
+ is(ShadersListView.selectedItem, null,
+ "The shaders list has no correct item.");
+ is(ShadersListView.selectedIndex, -1,
+ "The shaders list has a negative index.");
+
+ yield navigated;
+
+ is($("#reload-notice").hidden, true,
+ "The 'reload this page' notice should still be hidden after navigating.");
+ is($("#waiting-notice").hidden, false,
+ "The 'waiting for a WebGL context' notice should still be visible after navigating.");
+ is($("#content").hidden, true,
+ "The tool's content should be still hidden since there's no WebGL content.");
+
+ yield teardown(panel);
+ finish();
+}
diff --git a/devtools/client/shadereditor/test/browser_se_programs-blackbox-01.js b/devtools/client/shadereditor/test/browser_se_programs-blackbox-01.js
new file mode 100644
index 000000000..4c8199c22
--- /dev/null
+++ b/devtools/client/shadereditor/test/browser_se_programs-blackbox-01.js
@@ -0,0 +1,169 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if blackboxing a program works properly.
+ */
+
+function* ifWebGLSupported() {
+ let { target, debuggee, panel } = yield initShaderEditor(MULTIPLE_CONTEXTS_URL);
+ let { gFront, EVENTS, ShadersListView, ShadersEditorsView } = panel.panelWin;
+
+ once(panel.panelWin, EVENTS.SHADER_COMPILED).then(() => {
+ ok(false, "No shaders should be publicly compiled during this test.");
+ });
+
+ reload(target);
+ let [[firstProgramActor, secondProgramActor]] = yield promise.all([
+ getPrograms(gFront, 2),
+ once(panel.panelWin, EVENTS.SOURCES_SHOWN)
+ ]);
+
+ let vsEditor = yield ShadersEditorsView._getEditor("vs");
+ let fsEditor = yield ShadersEditorsView._getEditor("fs");
+
+ vsEditor.once("change", () => {
+ ok(false, "The vertex shader source was unexpectedly changed.");
+ });
+ fsEditor.once("change", () => {
+ ok(false, "The fragment shader source was unexpectedly changed.");
+ });
+ once(panel.panelWin, EVENTS.SOURCES_SHOWN).then(() => {
+ ok(false, "No sources should be changed form this point onward.");
+ });
+
+ yield ensurePixelIs(gFront, { x: 0, y: 0 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+ yield ensurePixelIs(gFront, { x: 0, y: 0 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+ yield ensurePixelIs(gFront, { x: 127, y: 127 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+ yield ensurePixelIs(gFront, { x: 127, y: 127 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+
+ ok(!ShadersListView.selectedAttachment.isBlackBoxed,
+ "The first program should not be blackboxed yet.");
+ is(getBlackBoxCheckbox(panel, 0).checked, true,
+ "The first blackbox checkbox should be initially checked.");
+ ok(!ShadersListView.attachments[1].isBlackBoxed,
+ "The second program should not be blackboxed yet.");
+ is(getBlackBoxCheckbox(panel, 1).checked, true,
+ "The second blackbox checkbox should be initially checked.");
+
+ getBlackBoxCheckbox(panel, 0).click();
+
+ ok(ShadersListView.selectedAttachment.isBlackBoxed,
+ "The first program should now be blackboxed.");
+ is(getBlackBoxCheckbox(panel, 0).checked, false,
+ "The first blackbox checkbox should now be unchecked.");
+ ok(!ShadersListView.attachments[1].isBlackBoxed,
+ "The second program should still not be blackboxed.");
+ is(getBlackBoxCheckbox(panel, 1).checked, true,
+ "The second blackbox checkbox should still be checked.");
+
+ yield ensurePixelIs(gFront, { x: 0, y: 0 }, { r: 0, g: 0, b: 0, a: 255 }, true, "#canvas1");
+ yield ensurePixelIs(gFront, { x: 0, y: 0 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+ yield ensurePixelIs(gFront, { x: 127, y: 127 }, { r: 0, g: 0, b: 0, a: 255 }, true, "#canvas1");
+ yield ensurePixelIs(gFront, { x: 127, y: 127 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+ ok(true, "The first program was correctly blackboxed.");
+
+ getBlackBoxCheckbox(panel, 1).click();
+
+ ok(ShadersListView.selectedAttachment.isBlackBoxed,
+ "The first program should still be blackboxed.");
+ is(getBlackBoxCheckbox(panel, 0).checked, false,
+ "The first blackbox checkbox should still be unchecked.");
+ ok(ShadersListView.attachments[1].isBlackBoxed,
+ "The second program should now be blackboxed.");
+ is(getBlackBoxCheckbox(panel, 1).checked, false,
+ "The second blackbox checkbox should now be unchecked.");
+
+ yield ensurePixelIs(gFront, { x: 0, y: 0 }, { r: 0, g: 0, b: 0, a: 255 }, true, "#canvas1");
+ yield ensurePixelIs(gFront, { x: 0, y: 0 }, { r: 0, g: 0, b: 0, a: 255 }, true, "#canvas2");
+ yield ensurePixelIs(gFront, { x: 127, y: 127 }, { r: 0, g: 0, b: 0, a: 255 }, true, "#canvas1");
+ yield ensurePixelIs(gFront, { x: 127, y: 127 }, { r: 0, g: 0, b: 0, a: 255 }, true, "#canvas2");
+ ok(true, "The second program was correctly blackboxed.");
+
+ ShadersListView._onProgramMouseOver({ target: getItemLabel(panel, 0) });
+
+ yield ensurePixelIs(gFront, { x: 0, y: 0 }, { r: 0, g: 0, b: 0, a: 255 }, true, "#canvas1");
+ yield ensurePixelIs(gFront, { x: 0, y: 0 }, { r: 0, g: 0, b: 0, a: 255 }, true, "#canvas2");
+ yield ensurePixelIs(gFront, { x: 127, y: 127 }, { r: 0, g: 0, b: 0, a: 255 }, true, "#canvas1");
+ yield ensurePixelIs(gFront, { x: 127, y: 127 }, { r: 0, g: 0, b: 0, a: 255 }, true, "#canvas2");
+ ok(true, "Highlighting shouldn't work while blackboxed (1).");
+
+ ShadersListView._onProgramMouseOut({ target: getItemLabel(panel, 0) });
+ ShadersListView._onProgramMouseOver({ target: getItemLabel(panel, 1) });
+
+ yield ensurePixelIs(gFront, { x: 0, y: 0 }, { r: 0, g: 0, b: 0, a: 255 }, true, "#canvas1");
+ yield ensurePixelIs(gFront, { x: 0, y: 0 }, { r: 0, g: 0, b: 0, a: 255 }, true, "#canvas2");
+ yield ensurePixelIs(gFront, { x: 127, y: 127 }, { r: 0, g: 0, b: 0, a: 255 }, true, "#canvas1");
+ yield ensurePixelIs(gFront, { x: 127, y: 127 }, { r: 0, g: 0, b: 0, a: 255 }, true, "#canvas2");
+ ok(true, "Highlighting shouldn't work while blackboxed (2).");
+
+ ShadersListView._onProgramMouseOut({ target: getItemLabel(panel, 1) });
+
+ yield ensurePixelIs(gFront, { x: 0, y: 0 }, { r: 0, g: 0, b: 0, a: 255 }, true, "#canvas1");
+ yield ensurePixelIs(gFront, { x: 0, y: 0 }, { r: 0, g: 0, b: 0, a: 255 }, true, "#canvas2");
+ yield ensurePixelIs(gFront, { x: 127, y: 127 }, { r: 0, g: 0, b: 0, a: 255 }, true, "#canvas1");
+ yield ensurePixelIs(gFront, { x: 127, y: 127 }, { r: 0, g: 0, b: 0, a: 255 }, true, "#canvas2");
+ ok(true, "Highlighting shouldn't work while blackboxed (3).");
+
+ getBlackBoxCheckbox(panel, 0).click();
+ getBlackBoxCheckbox(panel, 1).click();
+
+ ok(!ShadersListView.selectedAttachment.isBlackBoxed,
+ "The first program should now be unblackboxed.");
+ is(getBlackBoxCheckbox(panel, 0).checked, true,
+ "The first blackbox checkbox should now be rechecked.");
+ ok(!ShadersListView.attachments[1].isBlackBoxed,
+ "The second program should now be unblackboxed.");
+ is(getBlackBoxCheckbox(panel, 1).checked, true,
+ "The second blackbox checkbox should now be rechecked.");
+
+ yield ensurePixelIs(gFront, { x: 0, y: 0 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+ yield ensurePixelIs(gFront, { x: 0, y: 0 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+ yield ensurePixelIs(gFront, { x: 127, y: 127 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+ yield ensurePixelIs(gFront, { x: 127, y: 127 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+ ok(true, "The two programs were correctly unblackboxed.");
+
+ ShadersListView._onProgramMouseOver({ target: getItemLabel(panel, 0) });
+
+ yield ensurePixelIs(gFront, { x: 0, y: 0 }, { r: 255, g: 0, b: 0, a: 255 }, true, "#canvas1");
+ yield ensurePixelIs(gFront, { x: 0, y: 0 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+ yield ensurePixelIs(gFront, { x: 127, y: 127 }, { r: 255, g: 0, b: 0, a: 255 }, true, "#canvas1");
+ yield ensurePixelIs(gFront, { x: 127, y: 127 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+ ok(true, "The first program was correctly highlighted.");
+
+ ShadersListView._onProgramMouseOut({ target: getItemLabel(panel, 0) });
+ ShadersListView._onProgramMouseOver({ target: getItemLabel(panel, 1) });
+
+ yield ensurePixelIs(gFront, { x: 0, y: 0 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+ yield ensurePixelIs(gFront, { x: 0, y: 0 }, { r: 0, g: 0, b: 64, a: 255 }, true, "#canvas2");
+ yield ensurePixelIs(gFront, { x: 127, y: 127 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+ yield ensurePixelIs(gFront, { x: 127, y: 127 }, { r: 0, g: 0, b: 64, a: 255 }, true, "#canvas2");
+ ok(true, "The second program was correctly highlighted.");
+
+ ShadersListView._onProgramMouseOut({ target: getItemLabel(panel, 1) });
+
+ yield ensurePixelIs(gFront, { x: 0, y: 0 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+ yield ensurePixelIs(gFront, { x: 0, y: 0 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+ yield ensurePixelIs(gFront, { x: 127, y: 127 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+ yield ensurePixelIs(gFront, { x: 127, y: 127 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+ ok(true, "The two programs were correctly unhighlighted.");
+
+ yield teardown(panel);
+ finish();
+}
+
+function getItemLabel(aPanel, aIndex) {
+ return aPanel.panelWin.document.querySelectorAll(
+ ".side-menu-widget-item-contents")[aIndex];
+}
+
+function getBlackBoxCheckbox(aPanel, aIndex) {
+ return aPanel.panelWin.document.querySelectorAll(
+ ".side-menu-widget-item-checkbox")[aIndex];
+}
+
+function once(aTarget, aEvent) {
+ let deferred = promise.defer();
+ aTarget.once(aEvent, deferred.resolve);
+ return deferred.promise;
+}
diff --git a/devtools/client/shadereditor/test/browser_se_programs-blackbox-02.js b/devtools/client/shadereditor/test/browser_se_programs-blackbox-02.js
new file mode 100644
index 000000000..391a272c8
--- /dev/null
+++ b/devtools/client/shadereditor/test/browser_se_programs-blackbox-02.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if blackboxing a program works properly in tandem with blended
+ * overlapping geometry.
+ */
+
+function* ifWebGLSupported() {
+ let { target, debuggee, panel } = yield initShaderEditor(BLENDED_GEOMETRY_CANVAS_URL);
+ let { gFront, EVENTS, ShadersListView, ShadersEditorsView } = panel.panelWin;
+
+ reload(target);
+ let [[firstProgramActor, secondProgramActor]] = yield promise.all([
+ getPrograms(gFront, 2),
+ once(panel.panelWin, EVENTS.SOURCES_SHOWN)
+ ]);
+
+ yield ensurePixelIs(gFront, { x: 0, y: 0 }, { r: 127, g: 127, b: 127, a: 255 }, true);
+ yield ensurePixelIs(gFront, { x: 64, y: 64 }, { r: 0, g: 127, b: 127, a: 127 }, true);
+ ok(true, "The canvas was correctly drawn.");
+
+ getBlackBoxCheckbox(panel, 0).click();
+
+ yield ensurePixelIs(gFront, { x: 0, y: 0 }, { r: 0, g: 0, b: 0, a: 255 }, true);
+ yield ensurePixelIs(gFront, { x: 64, y: 64 }, { r: 0, g: 0, b: 0, a: 127 }, true);
+ ok(true, "The first program was correctly blackboxed.");
+
+ getBlackBoxCheckbox(panel, 0).click();
+ getBlackBoxCheckbox(panel, 1).click();
+
+ yield ensurePixelIs(gFront, { x: 0, y: 0 }, { r: 127, g: 127, b: 127, a: 255 }, true);
+ yield ensurePixelIs(gFront, { x: 64, y: 64 }, { r: 127, g: 127, b: 127, a: 255 }, true);
+ ok(true, "The second program was correctly blackboxed.");
+
+ getBlackBoxCheckbox(panel, 1).click();
+
+ yield ensurePixelIs(gFront, { x: 0, y: 0 }, { r: 127, g: 127, b: 127, a: 255 }, true);
+ yield ensurePixelIs(gFront, { x: 64, y: 64 }, { r: 0, g: 127, b: 127, a: 127 }, true);
+ ok(true, "The two programs were correctly unblackboxed.");
+
+ getBlackBoxCheckbox(panel, 0).click();
+ getBlackBoxCheckbox(panel, 1).click();
+
+ yield ensurePixelIs(gFront, { x: 0, y: 0 }, { r: 0, g: 0, b: 0, a: 255 }, true);
+ yield ensurePixelIs(gFront, { x: 64, y: 64 }, { r: 0, g: 0, b: 0, a: 255 }, true);
+ ok(true, "The two programs were correctly blackboxed again.");
+
+ getBlackBoxCheckbox(panel, 0).click();
+ getBlackBoxCheckbox(panel, 1).click();
+
+ yield ensurePixelIs(gFront, { x: 0, y: 0 }, { r: 127, g: 127, b: 127, a: 255 }, true);
+ yield ensurePixelIs(gFront, { x: 64, y: 64 }, { r: 0, g: 127, b: 127, a: 127 }, true);
+ ok(true, "The two programs were correctly unblackboxed again.");
+
+ yield teardown(panel);
+ finish();
+}
+
+function getBlackBoxCheckbox(aPanel, aIndex) {
+ return aPanel.panelWin.document.querySelectorAll(
+ ".side-menu-widget-item-checkbox")[aIndex];
+}
diff --git a/devtools/client/shadereditor/test/browser_se_programs-cache.js b/devtools/client/shadereditor/test/browser_se_programs-cache.js
new file mode 100644
index 000000000..5e5708e44
--- /dev/null
+++ b/devtools/client/shadereditor/test/browser_se_programs-cache.js
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that program and shader actors are cached in the frontend.
+ */
+
+function* ifWebGLSupported() {
+ let { target, debuggee, panel } = yield initShaderEditor(MULTIPLE_CONTEXTS_URL);
+ let { EVENTS, gFront, ShadersListView, ShadersEditorsView } = panel.panelWin;
+
+ reload(target);
+ let [[programActor]] = yield promise.all([
+ getPrograms(gFront, 1),
+ once(panel.panelWin, EVENTS.SOURCES_SHOWN)
+ ]);
+
+ let programItem = ShadersListView.selectedItem;
+
+ is(programItem.attachment.programActor, programActor,
+ "The correct program actor is cached for the selected item.");
+
+ is((yield programActor.getVertexShader()),
+ (yield programItem.attachment.vs),
+ "The cached vertex shader promise returns the correct actor.");
+
+ is((yield programActor.getFragmentShader()),
+ (yield programItem.attachment.fs),
+ "The cached fragment shader promise returns the correct actor.");
+
+ is((yield (yield programActor.getVertexShader()).getText()),
+ (yield (yield ShadersEditorsView._getEditor("vs")).getText()),
+ "The cached vertex shader promise returns the correct text.");
+
+ is((yield (yield programActor.getFragmentShader()).getText()),
+ (yield (yield ShadersEditorsView._getEditor("fs")).getText()),
+ "The cached fragment shader promise returns the correct text.");
+
+ yield teardown(panel);
+ finish();
+}
diff --git a/devtools/client/shadereditor/test/browser_se_programs-highlight-01.js b/devtools/client/shadereditor/test/browser_se_programs-highlight-01.js
new file mode 100644
index 000000000..863dc2672
--- /dev/null
+++ b/devtools/client/shadereditor/test/browser_se_programs-highlight-01.js
@@ -0,0 +1,93 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if highlighting a program works properly.
+ */
+
+function* ifWebGLSupported() {
+ let { target, panel } = yield initShaderEditor(MULTIPLE_CONTEXTS_URL);
+ let { gFront, EVENTS, ShadersListView, ShadersEditorsView } = panel.panelWin;
+
+ once(panel.panelWin, EVENTS.SHADER_COMPILED).then(() => {
+ ok(false, "No shaders should be publicly compiled during this test.");
+ });
+
+ reload(target);
+ let [[firstProgramActor, secondProgramActor]] = yield promise.all([
+ getPrograms(gFront, 2),
+ once(panel.panelWin, EVENTS.SOURCES_SHOWN)
+ ]);
+
+ let vsEditor = yield ShadersEditorsView._getEditor("vs");
+ let fsEditor = yield ShadersEditorsView._getEditor("fs");
+
+ vsEditor.once("change", () => {
+ ok(false, "The vertex shader source was unexpectedly changed.");
+ });
+ fsEditor.once("change", () => {
+ ok(false, "The fragment shader source was unexpectedly changed.");
+ });
+ once(panel.panelWin, EVENTS.SOURCES_SHOWN).then(() => {
+ ok(false, "No sources should be changed form this point onward.");
+ });
+
+ yield ensurePixelIs(gFront, { x: 0, y: 0 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+ yield ensurePixelIs(gFront, { x: 0, y: 0 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+ yield ensurePixelIs(gFront, { x: 127, y: 127 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+ yield ensurePixelIs(gFront, { x: 127, y: 127 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+
+ ShadersListView._onProgramMouseOver({ target: getItemLabel(panel, 0) });
+
+ yield ensurePixelIs(gFront, { x: 0, y: 0 }, { r: 255, g: 0, b: 0, a: 255 }, true, "#canvas1");
+ yield ensurePixelIs(gFront, { x: 0, y: 0 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+ yield ensurePixelIs(gFront, { x: 127, y: 127 }, { r: 255, g: 0, b: 0, a: 255 }, true, "#canvas1");
+ yield ensurePixelIs(gFront, { x: 127, y: 127 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+ ok(true, "The first program was correctly highlighted.");
+
+ ShadersListView._onProgramMouseOut({ target: getItemLabel(panel, 0) });
+ ShadersListView._onProgramMouseOver({ target: getItemLabel(panel, 1) });
+
+ yield ensurePixelIs(gFront, { x: 0, y: 0 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+ yield ensurePixelIs(gFront, { x: 0, y: 0 }, { r: 0, g: 0, b: 64, a: 255 }, true, "#canvas2");
+ yield ensurePixelIs(gFront, { x: 127, y: 127 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+ yield ensurePixelIs(gFront, { x: 127, y: 127 }, { r: 0, g: 0, b: 64, a: 255 }, true, "#canvas2");
+ ok(true, "The second program was correctly highlighted.");
+
+ ShadersListView._onProgramMouseOut({ target: getItemLabel(panel, 1) });
+
+ yield ensurePixelIs(gFront, { x: 0, y: 0 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+ yield ensurePixelIs(gFront, { x: 0, y: 0 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+ yield ensurePixelIs(gFront, { x: 127, y: 127 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+ yield ensurePixelIs(gFront, { x: 127, y: 127 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+ ok(true, "The two programs were correctly unhighlighted.");
+
+ ShadersListView._onProgramMouseOver({ target: getBlackBoxCheckbox(panel, 0) });
+
+ yield ensurePixelIs(gFront, { x: 0, y: 0 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+ yield ensurePixelIs(gFront, { x: 0, y: 0 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+ yield ensurePixelIs(gFront, { x: 127, y: 127 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+ yield ensurePixelIs(gFront, { x: 127, y: 127 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+ ok(true, "The two programs were left unchanged after hovering a blackbox checkbox.");
+
+ ShadersListView._onProgramMouseOut({ target: getBlackBoxCheckbox(panel, 0) });
+
+ yield ensurePixelIs(gFront, { x: 0, y: 0 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+ yield ensurePixelIs(gFront, { x: 0, y: 0 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+ yield ensurePixelIs(gFront, { x: 127, y: 127 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+ yield ensurePixelIs(gFront, { x: 127, y: 127 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+ ok(true, "The two programs were left unchanged after unhovering a blackbox checkbox.");
+
+ yield teardown(panel);
+ finish();
+}
+
+function getItemLabel(aPanel, aIndex) {
+ return aPanel.panelWin.document.querySelectorAll(
+ ".side-menu-widget-item-contents")[aIndex];
+}
+
+function getBlackBoxCheckbox(aPanel, aIndex) {
+ return aPanel.panelWin.document.querySelectorAll(
+ ".side-menu-widget-item-checkbox")[aIndex];
+}
diff --git a/devtools/client/shadereditor/test/browser_se_programs-highlight-02.js b/devtools/client/shadereditor/test/browser_se_programs-highlight-02.js
new file mode 100644
index 000000000..c6cbd796b
--- /dev/null
+++ b/devtools/client/shadereditor/test/browser_se_programs-highlight-02.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if highlighting a program works properly in tandem with blended
+ * overlapping geometry.
+ */
+
+function* ifWebGLSupported() {
+ let { target, debuggee, panel } = yield initShaderEditor(BLENDED_GEOMETRY_CANVAS_URL);
+ let { gFront, EVENTS, ShadersListView, ShadersEditorsView } = panel.panelWin;
+
+ reload(target);
+ let [[firstProgramActor, secondProgramActor]] = yield promise.all([
+ getPrograms(gFront, 2),
+ once(panel.panelWin, EVENTS.SOURCES_SHOWN)
+ ]);
+
+ yield ensurePixelIs(gFront, { x: 0, y: 0 }, { r: 127, g: 127, b: 127, a: 255 }, true);
+ yield ensurePixelIs(gFront, { x: 64, y: 64 }, { r: 0, g: 127, b: 127, a: 127 }, true);
+ ok(true, "The canvas was correctly drawn.");
+
+ ShadersListView._onProgramMouseOver({ target: getItemLabel(panel, 0) });
+
+ yield ensurePixelIs(gFront, { x: 0, y: 0 }, { r: 127, g: 0, b: 32, a: 255 }, true);
+ yield ensurePixelIs(gFront, { x: 64, y: 64 }, { r: 0, g: 0, b: 32, a: 127 }, true);
+ ok(true, "The first program was correctly highlighted.");
+
+ ShadersListView._onProgramMouseOut({ target: getItemLabel(panel, 0) });
+ ShadersListView._onProgramMouseOver({ target: getItemLabel(panel, 1) });
+
+ yield ensurePixelIs(gFront, { x: 0, y: 0 }, { r: 127, g: 127, b: 127, a: 255 }, true);
+ yield ensurePixelIs(gFront, { x: 64, y: 64 }, { r: 255, g: 0, b: 64, a: 255 }, true);
+ ok(true, "The second program was correctly highlighted.");
+
+ ShadersListView._onProgramMouseOut({ target: getItemLabel(panel, 1) });
+
+ yield ensurePixelIs(gFront, { x: 0, y: 0 }, { r: 127, g: 127, b: 127, a: 255 }, true);
+ yield ensurePixelIs(gFront, { x: 64, y: 64 }, { r: 0, g: 127, b: 127, a: 127 }, true);
+ ok(true, "The two programs were correctly unhighlighted.");
+
+ yield teardown(panel);
+ finish();
+}
+
+function getItemLabel(aPanel, aIndex) {
+ return aPanel.panelWin.document.querySelectorAll(
+ ".side-menu-widget-item-contents")[aIndex];
+}
diff --git a/devtools/client/shadereditor/test/browser_se_programs-list.js b/devtools/client/shadereditor/test/browser_se_programs-list.js
new file mode 100644
index 000000000..621880886
--- /dev/null
+++ b/devtools/client/shadereditor/test/browser_se_programs-list.js
@@ -0,0 +1,87 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the programs list contains an entry after vertex and fragment
+ * shaders are linked.
+ */
+
+function* ifWebGLSupported() {
+ let { target, panel } = yield initShaderEditor(MULTIPLE_CONTEXTS_URL);
+ let { gFront, EVENTS, L10N, ShadersListView, ShadersEditorsView } = panel.panelWin;
+
+ is(ShadersListView.itemCount, 0,
+ "The shaders list should initially be empty.");
+ is(ShadersListView.selectedItem, null,
+ "The shaders list has no selected item.");
+ is(ShadersListView.selectedIndex, -1,
+ "The shaders list has a negative index.");
+
+ reload(target);
+
+ let [firstProgramActor, secondProgramActor] = yield promise.all([
+ getPrograms(gFront, 2, (actors) => {
+ // Fired upon each actor addition, we want to check only
+ // after the first actor has been added so we can test state
+ if (actors.length === 1)
+ checkFirstProgram();
+ if (actors.length === 2)
+ checkSecondProgram();
+ }),
+ once(panel.panelWin, EVENTS.SOURCES_SHOWN)
+ ]).then(([programs, ]) => programs);
+
+ is(ShadersListView.attachments[0].label, L10N.getFormatStr("shadersList.programLabel", 0),
+ "The correct first label is shown in the shaders list.");
+ is(ShadersListView.attachments[1].label, L10N.getFormatStr("shadersList.programLabel", 1),
+ "The correct second label is shown in the shaders list.");
+
+ let vertexShader = yield firstProgramActor.getVertexShader();
+ let fragmentShader = yield firstProgramActor.getFragmentShader();
+ let vertSource = yield vertexShader.getText();
+ let fragSource = yield fragmentShader.getText();
+
+ let vsEditor = yield ShadersEditorsView._getEditor("vs");
+ let fsEditor = yield ShadersEditorsView._getEditor("fs");
+
+ is(vertSource, vsEditor.getText(),
+ "The vertex shader editor contains the correct text.");
+ is(fragSource, fsEditor.getText(),
+ "The vertex shader editor contains the correct text.");
+
+ let compiled = once(panel.panelWin, EVENTS.SHADER_COMPILED).then(() => {
+ ok(false, "Selecting a different program shouldn't recompile its shaders.");
+ });
+
+ let shown = once(panel.panelWin, EVENTS.SOURCES_SHOWN).then(() => {
+ ok(true, "The vertex and fragment sources have changed in the editors.");
+ });
+
+ EventUtils.sendMouseEvent({ type: "mousedown" }, ShadersListView.items[1].target);
+ yield shown;
+
+ is(ShadersListView.selectedItem, ShadersListView.items[1],
+ "The shaders list has a correct item selected.");
+ is(ShadersListView.selectedIndex, 1,
+ "The shaders list has a correct index selected.");
+
+ yield teardown(panel);
+ finish();
+
+ function checkFirstProgram() {
+ is(ShadersListView.itemCount, 1,
+ "The shaders list contains one entry.");
+ is(ShadersListView.selectedItem, ShadersListView.items[0],
+ "The shaders list has a correct item selected.");
+ is(ShadersListView.selectedIndex, 0,
+ "The shaders list has a correct index selected.");
+ }
+ function checkSecondProgram() {
+ is(ShadersListView.itemCount, 2,
+ "The shaders list contains two entries.");
+ is(ShadersListView.selectedItem, ShadersListView.items[0],
+ "The shaders list has a correct item selected.");
+ is(ShadersListView.selectedIndex, 0,
+ "The shaders list has a correct index selected.");
+ }
+}
diff --git a/devtools/client/shadereditor/test/browser_se_shaders-edit-01.js b/devtools/client/shadereditor/test/browser_se_shaders-edit-01.js
new file mode 100644
index 000000000..0cb52ac19
--- /dev/null
+++ b/devtools/client/shadereditor/test/browser_se_shaders-edit-01.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if editing a vertex and a fragment shader works properly.
+ */
+
+function* ifWebGLSupported() {
+ let { target, panel } = yield initShaderEditor(SIMPLE_CANVAS_URL);
+ let { gFront, $, EVENTS, ShadersEditorsView } = panel.panelWin;
+
+ reload(target);
+ yield promise.all([
+ once(gFront, "program-linked"),
+ once(panel.panelWin, EVENTS.SOURCES_SHOWN)
+ ]);
+
+ let vsEditor = yield ShadersEditorsView._getEditor("vs");
+ let fsEditor = yield ShadersEditorsView._getEditor("fs");
+
+ is(vsEditor.getText().indexOf("gl_Position"), 170,
+ "The vertex shader editor contains the correct text.");
+ is(fsEditor.getText().indexOf("gl_FragColor"), 97,
+ "The fragment shader editor contains the correct text.");
+
+ is($("#vs-editor-label").hasAttribute("selected"), false,
+ "The vertex shader editor shouldn't be initially selected.");
+ is($("#fs-editor-label").hasAttribute("selected"), false,
+ "The vertex shader editor shouldn't be initially selected.");
+
+ yield ensurePixelIs(gFront, { x: 0, y: 0 }, { r: 255, g: 0, b: 0, a: 255 }, true);
+ yield ensurePixelIs(gFront, { x: 128, y: 128 }, { r: 191, g: 64, b: 0, a: 255 }, true);
+ yield ensurePixelIs(gFront, { x: 511, y: 511 }, { r: 0, g: 255, b: 0, a: 255 }, true);
+
+ vsEditor.focus();
+
+ is($("#vs-editor-label").hasAttribute("selected"), true,
+ "The vertex shader editor should now be selected.");
+ is($("#fs-editor-label").hasAttribute("selected"), false,
+ "The vertex shader editor shouldn't still not be selected.");
+
+ vsEditor.replaceText("2.0", { line: 7, ch: 44 }, { line: 7, ch: 47 });
+ yield once(panel.panelWin, EVENTS.SHADER_COMPILED);
+
+ ok(true, "Vertex shader was changed.");
+
+ yield ensurePixelIs(gFront, { x: 0, y: 0 }, { r: 0, g: 0, b: 0, a: 255 }, true);
+ yield ensurePixelIs(gFront, { x: 128, y: 128 }, { r: 255, g: 0, b: 0, a: 255 }, true);
+ yield ensurePixelIs(gFront, { x: 511, y: 511 }, { r: 0, g: 0, b: 0, a: 255 }, true);
+
+ ok(true, "The vertex shader was recompiled successfully.");
+
+ fsEditor.focus();
+
+ is($("#vs-editor-label").hasAttribute("selected"), false,
+ "The vertex shader editor should now be deselected.");
+ is($("#fs-editor-label").hasAttribute("selected"), true,
+ "The vertex shader editor should now be selected.");
+
+ fsEditor.replaceText("0.5", { line: 5, ch: 44 }, { line: 5, ch: 47 });
+ yield once(panel.panelWin, EVENTS.SHADER_COMPILED);
+
+ ok(true, "Fragment shader was changed.");
+
+ yield ensurePixelIs(gFront, { x: 0, y: 0 }, { r: 0, g: 0, b: 0, a: 255 }, true);
+ yield ensurePixelIs(gFront, { x: 128, y: 128 }, { r: 255, g: 0, b: 0, a: 127 }, true);
+ yield ensurePixelIs(gFront, { x: 511, y: 511 }, { r: 0, g: 0, b: 0, a: 255 }, true);
+
+ ok(true, "The fragment shader was recompiled successfully.");
+
+ yield teardown(panel);
+ finish();
+}
diff --git a/devtools/client/shadereditor/test/browser_se_shaders-edit-02.js b/devtools/client/shadereditor/test/browser_se_shaders-edit-02.js
new file mode 100644
index 000000000..0bb72c461
--- /dev/null
+++ b/devtools/client/shadereditor/test/browser_se_shaders-edit-02.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if compile or linkage errors are emitted when a shader source
+ * gets malformed after being edited.
+ */
+
+function* ifWebGLSupported() {
+ let { target, panel } = yield initShaderEditor(SIMPLE_CANVAS_URL);
+ let { gFront, EVENTS, ShadersEditorsView } = panel.panelWin;
+
+ reload(target);
+ yield promise.all([
+ once(gFront, "program-linked"),
+ once(panel.panelWin, EVENTS.SOURCES_SHOWN)
+ ]);
+
+ let vsEditor = yield ShadersEditorsView._getEditor("vs");
+ let fsEditor = yield ShadersEditorsView._getEditor("fs");
+
+
+ vsEditor.replaceText("vec3", { line: 7, ch: 22 }, { line: 7, ch: 26 });
+ let [, error] = yield onceSpread(panel.panelWin, EVENTS.SHADER_COMPILED);
+
+ ok(error,
+ "The new vertex shader source was compiled with errors.");
+
+ // The implementation has the choice to defer all compile-time errors to link time.
+ let infoLog = (error.compile != "") ? error.compile : error.link;
+
+ isnot(infoLog, "",
+ "The one of the compile or link info logs should not be empty.");
+ is(infoLog.split("ERROR").length - 1, 2,
+ "The info log status contains two errors.");
+ ok(infoLog.includes("ERROR: 0:8: 'constructor'"),
+ "A constructor error is contained in the info log.");
+ ok(infoLog.includes("ERROR: 0:8: 'assign'"),
+ "An assignment error is contained in the info log.");
+
+
+ fsEditor.replaceText("vec4", { line: 2, ch: 14 }, { line: 2, ch: 18 });
+ [, error] = yield onceSpread(panel.panelWin, EVENTS.SHADER_COMPILED);
+
+ ok(error,
+ "The new fragment shader source was compiled with errors.");
+
+ infoLog = (error.compile != "") ? error.compile : error.link;
+
+ isnot(infoLog, "",
+ "The one of the compile or link info logs should not be empty.");
+ is(infoLog.split("ERROR").length - 1, 1,
+ "The info log contains one error.");
+ ok(infoLog.includes("ERROR: 0:6: 'constructor'"),
+ "A constructor error is contained in the info log.");
+
+
+ yield ensurePixelIs(gFront, { x: 0, y: 0 }, { r: 255, g: 0, b: 0, a: 255 }, true);
+ yield ensurePixelIs(gFront, { x: 511, y: 511 }, { r: 0, g: 255, b: 0, a: 255 }, true);
+
+ vsEditor.replaceText("vec4", { line: 7, ch: 22 }, { line: 7, ch: 26 });
+ [, error] = yield onceSpread(panel.panelWin, EVENTS.SHADER_COMPILED);
+ ok(!error, "The new vertex shader source was compiled successfully.");
+
+ fsEditor.replaceText("vec3", { line: 2, ch: 14 }, { line: 2, ch: 18 });
+ [, error] = yield onceSpread(panel.panelWin, EVENTS.SHADER_COMPILED);
+ ok(!error, "The new fragment shader source was compiled successfully.");
+
+ yield ensurePixelIs(gFront, { x: 0, y: 0 }, { r: 255, g: 0, b: 0, a: 255 }, true);
+ yield ensurePixelIs(gFront, { x: 511, y: 511 }, { r: 0, g: 255, b: 0, a: 255 }, true);
+
+ yield teardown(panel);
+ finish();
+}
diff --git a/devtools/client/shadereditor/test/browser_se_shaders-edit-03.js b/devtools/client/shadereditor/test/browser_se_shaders-edit-03.js
new file mode 100644
index 000000000..2c413dd72
--- /dev/null
+++ b/devtools/client/shadereditor/test/browser_se_shaders-edit-03.js
@@ -0,0 +1,85 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if editing a vertex and a fragment shader would permanently store
+ * their new source on the backend and reshow it in the frontend when required.
+ */
+
+function* ifWebGLSupported() {
+ let { target, panel } = yield initShaderEditor(MULTIPLE_CONTEXTS_URL);
+ let { gFront, EVENTS, ShadersListView, ShadersEditorsView } = panel.panelWin;
+
+ reload(target);
+
+ yield promise.all([
+ once(gFront, "program-linked"),
+ once(gFront, "program-linked")
+ ]);
+
+ yield once(panel.panelWin, EVENTS.SOURCES_SHOWN);
+
+ let vsEditor = yield ShadersEditorsView._getEditor("vs");
+ let fsEditor = yield ShadersEditorsView._getEditor("fs");
+
+ is(ShadersListView.selectedIndex, 0,
+ "The first program is currently selected.");
+ is(vsEditor.getText().indexOf("1);"), 136,
+ "The vertex shader editor contains the correct initial text (1).");
+ is(fsEditor.getText().indexOf("1);"), 117,
+ "The fragment shader editor contains the correct initial text (1).");
+ is(vsEditor.getText().indexOf("2.);"), -1,
+ "The vertex shader editor contains the correct initial text (2).");
+ is(fsEditor.getText().indexOf(".0);"), -1,
+ "The fragment shader editor contains the correct initial text (2).");
+
+ vsEditor.replaceText("2.", { line: 5, ch: 44 }, { line: 5, ch: 45 });
+ yield once(panel.panelWin, EVENTS.SHADER_COMPILED);
+
+ fsEditor.replaceText(".0", { line: 5, ch: 35 }, { line: 5, ch: 37 });
+ yield once(panel.panelWin, EVENTS.SHADER_COMPILED);
+
+ ok(true, "Vertex and fragment shaders were changed.");
+
+ yield ensurePixelIs(gFront, { x: 0, y: 0 }, { r: 0, g: 0, b: 0, a: 255 }, true, "#canvas1");
+ yield ensurePixelIs(gFront, { x: 32, y: 32 }, { r: 255, g: 255, b: 0, a: 0 }, true, "#canvas1");
+ yield ensurePixelIs(gFront, { x: 64, y: 64 }, { r: 255, g: 255, b: 0, a: 0 }, true, "#canvas1");
+ yield ensurePixelIs(gFront, { x: 127, y: 127 }, { r: 0, g: 0, b: 0, a: 255 }, true, "#canvas1");
+ yield ensurePixelIs(gFront, { x: 0, y: 0 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+ yield ensurePixelIs(gFront, { x: 32, y: 32 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+ yield ensurePixelIs(gFront, { x: 64, y: 64 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+ yield ensurePixelIs(gFront, { x: 127, y: 127 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+
+ ok(true, "The vertex and fragment shaders were recompiled successfully.");
+
+ EventUtils.sendMouseEvent({ type: "mousedown" }, ShadersListView.items[1].target);
+ yield once(panel.panelWin, EVENTS.SOURCES_SHOWN);
+
+ is(ShadersListView.selectedIndex, 1,
+ "The second program is currently selected.");
+ is(vsEditor.getText().indexOf("1);"), 136,
+ "The vertex shader editor contains the correct text (1).");
+ is(fsEditor.getText().indexOf("1);"), 117,
+ "The fragment shader editor contains the correct text (1).");
+ is(vsEditor.getText().indexOf("2.);"), -1,
+ "The vertex shader editor contains the correct text (2).");
+ is(fsEditor.getText().indexOf(".0);"), -1,
+ "The fragment shader editor contains the correct text (2).");
+
+ EventUtils.sendMouseEvent({ type: "mousedown" }, ShadersListView.items[0].target);
+ yield once(panel.panelWin, EVENTS.SOURCES_SHOWN);
+
+ is(ShadersListView.selectedIndex, 0,
+ "The first program is currently selected again.");
+ is(vsEditor.getText().indexOf("1);"), -1,
+ "The vertex shader editor contains the correct text (3).");
+ is(fsEditor.getText().indexOf("1);"), -1,
+ "The fragment shader editor contains the correct text (3).");
+ is(vsEditor.getText().indexOf("2.);"), 136,
+ "The vertex shader editor contains the correct text (4).");
+ is(fsEditor.getText().indexOf(".0);"), 116,
+ "The fragment shader editor contains the correct text (4).");
+
+ yield teardown(panel);
+ finish();
+}
diff --git a/devtools/client/shadereditor/test/browser_webgl-actor-test-01.js b/devtools/client/shadereditor/test/browser_webgl-actor-test-01.js
new file mode 100644
index 000000000..242018a76
--- /dev/null
+++ b/devtools/client/shadereditor/test/browser_webgl-actor-test-01.js
@@ -0,0 +1,16 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if a WebGL front can be created for a remote tab target.
+ */
+
+function* ifWebGLSupported() {
+ let { target, front } = yield initBackend(SIMPLE_CANVAS_URL);
+
+ ok(target, "Should have a target available.");
+ ok(front, "Should have a protocol front available.");
+
+ yield removeTab(target.tab);
+ finish();
+}
diff --git a/devtools/client/shadereditor/test/browser_webgl-actor-test-02.js b/devtools/client/shadereditor/test/browser_webgl-actor-test-02.js
new file mode 100644
index 000000000..addec87ca
--- /dev/null
+++ b/devtools/client/shadereditor/test/browser_webgl-actor-test-02.js
@@ -0,0 +1,21 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if notifications about WebGL programs being linked are not sent
+ * if the front wasn't set up first.
+ */
+
+function* ifWebGLSupported() {
+ let { target, front } = yield initBackend(SIMPLE_CANVAS_URL);
+
+ once(front, "program-linked").then(() => {
+ ok(false, "A 'program-linked' notification shouldn't have been sent!");
+ });
+
+ ok(true, "Each test requires at least one pass, fail or todo so here is a pass.");
+
+ yield reload(target);
+ yield removeTab(target.tab);
+ finish();
+}
diff --git a/devtools/client/shadereditor/test/browser_webgl-actor-test-03.js b/devtools/client/shadereditor/test/browser_webgl-actor-test-03.js
new file mode 100644
index 000000000..0381973ec
--- /dev/null
+++ b/devtools/client/shadereditor/test/browser_webgl-actor-test-03.js
@@ -0,0 +1,26 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if notifications about WebGL programs being linked are sent
+ * after a target navigation.
+ */
+
+function* ifWebGLSupported() {
+ let { target, front } = yield initBackend(SIMPLE_CANVAS_URL);
+
+ let navigated = once(target, "navigate");
+ let linked = once(front, "program-linked");
+
+ yield front.setup({ reload: true });
+ ok(true, "The front was setup up successfully.");
+
+ yield navigated;
+ ok(true, "Target automatically navigated when the front was set up.");
+
+ yield linked;
+ ok(true, "A 'program-linked' notification was sent after reloading.");
+
+ yield removeTab(target.tab);
+ finish();
+}
diff --git a/devtools/client/shadereditor/test/browser_webgl-actor-test-04.js b/devtools/client/shadereditor/test/browser_webgl-actor-test-04.js
new file mode 100644
index 000000000..4256a5329
--- /dev/null
+++ b/devtools/client/shadereditor/test/browser_webgl-actor-test-04.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if a program actor is sent when WebGL programs are linked,
+ * and that the corresponding vertex and fragment actors can be retrieved.
+ */
+
+function* ifWebGLSupported() {
+ let { target, front } = yield initBackend(SIMPLE_CANVAS_URL);
+ front.setup({ reload: true });
+
+ let programActor = yield once(front, "program-linked");
+ ok(programActor,
+ "A program actor was sent along with the 'program-linked' notification.");
+
+ let vertexShader = yield programActor.getVertexShader();
+ ok(programActor,
+ "A vertex shader actor was retrieved from the program actor.");
+
+ let fragmentShader = yield programActor.getFragmentShader();
+ ok(programActor,
+ "A fragment shader actor was retrieved from the program actor.");
+
+ yield removeTab(target.tab);
+ finish();
+}
diff --git a/devtools/client/shadereditor/test/browser_webgl-actor-test-05.js b/devtools/client/shadereditor/test/browser_webgl-actor-test-05.js
new file mode 100644
index 000000000..96e445e01
--- /dev/null
+++ b/devtools/client/shadereditor/test/browser_webgl-actor-test-05.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the source contents can be retrieved from the vertex and fragment
+ * shader actors.
+ */
+
+function* ifWebGLSupported() {
+ let { target, front } = yield initBackend(SIMPLE_CANVAS_URL);
+ front.setup({ reload: true });
+
+ let programActor = yield once(front, "program-linked");
+ let vertexShader = yield programActor.getVertexShader();
+ let fragmentShader = yield programActor.getFragmentShader();
+
+ let vertSource = yield vertexShader.getText();
+ ok(vertSource.includes("gl_Position"),
+ "The correct vertex shader source was retrieved.");
+
+ let fragSource = yield fragmentShader.getText();
+ ok(fragSource.includes("gl_FragColor"),
+ "The correct fragment shader source was retrieved.");
+
+ yield removeTab(target.tab);
+ finish();
+}
diff --git a/devtools/client/shadereditor/test/browser_webgl-actor-test-06.js b/devtools/client/shadereditor/test/browser_webgl-actor-test-06.js
new file mode 100644
index 000000000..5cbe88a77
--- /dev/null
+++ b/devtools/client/shadereditor/test/browser_webgl-actor-test-06.js
@@ -0,0 +1,64 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the highlight/unhighlight and blackbox/unblackbox operations on
+ * program actors work as expected.
+ */
+
+function* ifWebGLSupported() {
+ let { target, front } = yield initBackend(SIMPLE_CANVAS_URL);
+ front.setup({ reload: true });
+
+ let programActor = yield once(front, "program-linked");
+ let vertexShader = yield programActor.getVertexShader();
+ let fragmentShader = yield programActor.getFragmentShader();
+
+ yield ensurePixelIs(front, { x: 0, y: 0 }, { r: 255, g: 0, b: 0, a: 255 }, true);
+ yield ensurePixelIs(front, { x: 511, y: 511 }, { r: 0, g: 255, b: 0, a: 255 }, true);
+ yield checkShaderSource("The shader sources are correct before highlighting.");
+ ok(true, "The corner pixel colors are correct before highlighting.");
+
+ yield programActor.highlight([0, 1, 0, 1]);
+ yield ensurePixelIs(front, { x: 0, y: 0 }, { r: 0, g: 0, b: 0, a: 255 }, true);
+ yield ensurePixelIs(front, { x: 511, y: 511 }, { r: 0, g: 255, b: 0, a: 255 }, true);
+ yield checkShaderSource("The shader sources are preserved after highlighting.");
+ ok(true, "The corner pixel colors are correct after highlighting.");
+
+ yield programActor.unhighlight();
+ yield ensurePixelIs(front, { x: 0, y: 0 }, { r: 255, g: 0, b: 0, a: 255 }, true);
+ yield ensurePixelIs(front, { x: 511, y: 511 }, { r: 0, g: 255, b: 0, a: 255 }, true);
+ yield checkShaderSource("The shader sources are correct after unhighlighting.");
+ ok(true, "The corner pixel colors are correct after unhighlighting.");
+
+ yield programActor.blackbox();
+ yield ensurePixelIs(front, { x: 0, y: 0 }, { r: 0, g: 0, b: 0, a: 255 }, true);
+ yield ensurePixelIs(front, { x: 511, y: 511 }, { r: 0, g: 0, b: 0, a: 255 }, true);
+ yield checkShaderSource("The shader sources are preserved after blackboxing.");
+ ok(true, "The corner pixel colors are correct after blackboxing.");
+
+ yield programActor.unblackbox();
+ yield ensurePixelIs(front, { x: 0, y: 0 }, { r: 255, g: 0, b: 0, a: 255 }, true);
+ yield ensurePixelIs(front, { x: 511, y: 511 }, { r: 0, g: 255, b: 0, a: 255 }, true);
+ yield checkShaderSource("The shader sources are correct after unblackboxing.");
+ ok(true, "The corner pixel colors are correct after unblackboxing.");
+
+ function checkShaderSource(aMessage) {
+ return Task.spawn(function* () {
+ let newVertexShader = yield programActor.getVertexShader();
+ let newFragmentShader = yield programActor.getFragmentShader();
+ is(vertexShader, newVertexShader,
+ "The same vertex shader actor was retrieved.");
+ is(fragmentShader, newFragmentShader,
+ "The same fragment shader actor was retrieved.");
+
+ let vertSource = yield newVertexShader.getText();
+ let fragSource = yield newFragmentShader.getText();
+ ok(vertSource.includes("I'm special!") &&
+ fragSource.includes("I'm also special!"), aMessage);
+ });
+ }
+
+ yield removeTab(target.tab);
+ finish();
+}
diff --git a/devtools/client/shadereditor/test/browser_webgl-actor-test-07.js b/devtools/client/shadereditor/test/browser_webgl-actor-test-07.js
new file mode 100644
index 000000000..a7634de44
--- /dev/null
+++ b/devtools/client/shadereditor/test/browser_webgl-actor-test-07.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that vertex and fragment shader sources can be changed.
+ */
+
+function* ifWebGLSupported() {
+ let { target, front } = yield initBackend(SIMPLE_CANVAS_URL);
+ front.setup({ reload: true });
+
+ let programActor = yield once(front, "program-linked");
+ let vertexShader = yield programActor.getVertexShader();
+ let fragmentShader = yield programActor.getFragmentShader();
+
+ yield ensurePixelIs(front, { x: 0, y: 0 }, { r: 255, g: 0, b: 0, a: 255 }, true);
+ yield ensurePixelIs(front, { x: 128, y: 128 }, { r: 191, g: 64, b: 0, a: 255 }, true);
+ yield ensurePixelIs(front, { x: 511, y: 511 }, { r: 0, g: 255, b: 0, a: 255 }, true);
+
+ let vertSource = yield vertexShader.getText();
+ let fragSource = yield fragmentShader.getText();
+ ok(!vertSource.includes("2.0"),
+ "The vertex shader source is correct before changing it.");
+ ok(!fragSource.includes("0.5"),
+ "The fragment shader source is correct before changing it.");
+
+ let newVertSource = vertSource.replace("1.0", "2.0");
+ let status = yield vertexShader.compile(newVertSource);
+ ok(!status,
+ "The new vertex shader source was compiled without errors.");
+
+ yield ensurePixelIs(front, { x: 0, y: 0 }, { r: 0, g: 0, b: 0, a: 255 }, true);
+ yield ensurePixelIs(front, { x: 128, y: 128 }, { r: 255, g: 0, b: 0, a: 255 }, true);
+ yield ensurePixelIs(front, { x: 511, y: 511 }, { r: 0, g: 0, b: 0, a: 255 }, true);
+
+ vertSource = yield vertexShader.getText();
+ fragSource = yield fragmentShader.getText();
+ ok(vertSource.includes("2.0"),
+ "The vertex shader source is correct after changing it.");
+ ok(!fragSource.includes("0.5"),
+ "The fragment shader source is correct after changing the vertex shader.");
+
+ let newFragSource = fragSource.replace("1.0", "0.5");
+ status = yield fragmentShader.compile(newFragSource);
+ ok(!status,
+ "The new fragment shader source was compiled without errors.");
+
+ yield ensurePixelIs(front, { x: 0, y: 0 }, { r: 0, g: 0, b: 0, a: 255 }, true);
+ yield ensurePixelIs(front, { x: 128, y: 128 }, { r: 255, g: 0, b: 0, a: 127 }, true);
+ yield ensurePixelIs(front, { x: 511, y: 511 }, { r: 0, g: 0, b: 0, a: 255 }, true);
+
+ vertSource = yield vertexShader.getText();
+ fragSource = yield fragmentShader.getText();
+ ok(vertSource.includes("2.0"),
+ "The vertex shader source is correct after changing the fragment shader.");
+ ok(fragSource.includes("0.5"),
+ "The fragment shader source is correct after changing it.");
+
+ yield removeTab(target.tab);
+ finish();
+}
diff --git a/devtools/client/shadereditor/test/browser_webgl-actor-test-08.js b/devtools/client/shadereditor/test/browser_webgl-actor-test-08.js
new file mode 100644
index 000000000..8025bc703
--- /dev/null
+++ b/devtools/client/shadereditor/test/browser_webgl-actor-test-08.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the rendering is updated when a varying variable is
+ * changed in one shader.
+ */
+
+function* ifWebGLSupported() {
+ let { target, front } = yield initBackend(SIMPLE_CANVAS_URL);
+ front.setup({ reload: true });
+
+ let programActor = yield once(front, "program-linked");
+ let vertexShader = yield programActor.getVertexShader();
+ let fragmentShader = yield programActor.getFragmentShader();
+
+ let oldVertSource = yield vertexShader.getText();
+ let newVertSource = oldVertSource.replace("= aVertexColor", "= vec3(0, 0, 1)");
+ let status = yield vertexShader.compile(newVertSource);
+ ok(!status,
+ "The new vertex shader source was compiled without errors.");
+
+ yield front.waitForFrame();
+ yield ensurePixelIs(front, { x: 0, y: 0 }, { r: 0, g: 0, b: 255, a: 255 }, true);
+ yield ensurePixelIs(front, { x: 128, y: 128 }, { r: 0, g: 0, b: 255, a: 255 }, true);
+ yield ensurePixelIs(front, { x: 511, y: 511 }, { r: 0, g: 0, b: 255, a: 255 }, true);
+
+ let vertSource = yield vertexShader.getText();
+ let fragSource = yield fragmentShader.getText();
+ ok(vertSource.includes("vFragmentColor = vec3(0, 0, 1);"),
+ "The vertex shader source is correct after changing it.");
+ ok(fragSource.includes("gl_FragColor = vec4(vFragmentColor, 1.0);"),
+ "The fragment shader source is correct after changing the vertex shader.");
+
+ yield removeTab(target.tab);
+ finish();
+}
diff --git a/devtools/client/shadereditor/test/browser_webgl-actor-test-09.js b/devtools/client/shadereditor/test/browser_webgl-actor-test-09.js
new file mode 100644
index 000000000..2054140a6
--- /dev/null
+++ b/devtools/client/shadereditor/test/browser_webgl-actor-test-09.js
@@ -0,0 +1,89 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that errors are properly handled when trying to compile a
+ * defective shader source.
+ */
+
+function* ifWebGLSupported() {
+ let { target, front } = yield initBackend(SIMPLE_CANVAS_URL);
+ front.setup({ reload: true });
+
+ let programActor = yield once(front, "program-linked");
+ let vertexShader = yield programActor.getVertexShader();
+ let fragmentShader = yield programActor.getFragmentShader();
+
+ let oldVertSource = yield vertexShader.getText();
+ let newVertSource = oldVertSource.replace("vec4", "vec3");
+
+ try {
+ yield vertexShader.compile(newVertSource);
+ ok(false, "Vertex shader was compiled with a defective source!");
+ } catch (error) {
+ ok(error,
+ "The new vertex shader source was compiled with errors.");
+
+ // The implementation has the choice to defer all compile-time errors to link time.
+ let infoLog = (error.compile != "") ? error.compile : error.link;
+
+ isnot(infoLog, "",
+ "The one of the compile or link info logs should not be empty.");
+ is(infoLog.split("ERROR").length - 1, 2,
+ "The info log contains two errors.");
+ ok(infoLog.includes("ERROR: 0:8: 'constructor'"),
+ "A constructor error is contained in the info log.");
+ ok(infoLog.includes("ERROR: 0:8: 'assign'"),
+ "An assignment error is contained in the info log.");
+ }
+
+ yield ensurePixelIs(front, { x: 0, y: 0 }, { r: 255, g: 0, b: 0, a: 255 }, true);
+ yield ensurePixelIs(front, { x: 511, y: 511 }, { r: 0, g: 255, b: 0, a: 255 }, true);
+ ok(true, "The shader was reverted to the old source.");
+
+ let vertSource = yield vertexShader.getText();
+ ok(vertSource.includes("vec4(aVertexPosition, 1.0);"),
+ "The previous correct vertex shader source was preserved.");
+
+ let oldFragSource = yield fragmentShader.getText();
+ let newFragSource = oldFragSource.replace("vec3", "vec4");
+
+ try {
+ yield fragmentShader.compile(newFragSource);
+ ok(false, "Fragment shader was compiled with a defective source!");
+ } catch (error) {
+ ok(error,
+ "The new fragment shader source was compiled with errors.");
+
+ // The implementation has the choice to defer all compile-time errors to link time.
+ let infoLog = (error.compile != "") ? error.compile : error.link;
+
+ isnot(infoLog, "",
+ "The one of the compile or link info logs should not be empty.");
+ is(infoLog.split("ERROR").length - 1, 1,
+ "The info log contains one error.");
+ ok(infoLog.includes("ERROR: 0:6: 'constructor'"),
+ "A constructor error is contained in the info log.");
+ }
+
+ yield ensurePixelIs(front, { x: 0, y: 0 }, { r: 255, g: 0, b: 0, a: 255 }, true);
+ yield ensurePixelIs(front, { x: 511, y: 511 }, { r: 0, g: 255, b: 0, a: 255 }, true);
+ ok(true, "The shader was reverted to the old source.");
+
+ let fragSource = yield fragmentShader.getText();
+ ok(fragSource.includes("vec3 vFragmentColor;"),
+ "The previous correct fragment shader source was preserved.");
+
+ yield programActor.highlight([0, 1, 0, 1]);
+ yield ensurePixelIs(front, { x: 0, y: 0 }, { r: 0, g: 0, b: 0, a: 255 }, true);
+ yield ensurePixelIs(front, { x: 511, y: 511 }, { r: 0, g: 255, b: 0, a: 255 }, true);
+ ok(true, "Highlighting worked after setting a defective fragment source.");
+
+ yield programActor.unhighlight();
+ yield ensurePixelIs(front, { x: 0, y: 0 }, { r: 255, g: 0, b: 0, a: 255 }, true);
+ yield ensurePixelIs(front, { x: 511, y: 511 }, { r: 0, g: 255, b: 0, a: 255 }, true);
+ ok(true, "Unhighlighting worked after setting a defective vertex source.");
+
+ yield removeTab(target.tab);
+ finish();
+}
diff --git a/devtools/client/shadereditor/test/browser_webgl-actor-test-10.js b/devtools/client/shadereditor/test/browser_webgl-actor-test-10.js
new file mode 100644
index 000000000..87dfe35bf
--- /dev/null
+++ b/devtools/client/shadereditor/test/browser_webgl-actor-test-10.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the WebGL context is correctly instrumented every time the
+ * target navigates.
+ */
+
+function* ifWebGLSupported() {
+ let { target, front } = yield initBackend(SIMPLE_CANVAS_URL);
+
+ front.setup({ reload: true });
+ yield testHighlighting((yield once(front, "program-linked")));
+ ok(true, "Canvas was correctly instrumented on the first navigation.");
+
+ reload(target);
+ yield testHighlighting((yield once(front, "program-linked")));
+ ok(true, "Canvas was correctly instrumented on the second navigation.");
+
+ reload(target);
+ yield testHighlighting((yield once(front, "program-linked")));
+ ok(true, "Canvas was correctly instrumented on the third navigation.");
+
+ yield removeTab(target.tab);
+ finish();
+
+ function testHighlighting(programActor) {
+ return Task.spawn(function* () {
+ yield ensurePixelIs(front, { x: 0, y: 0 }, { r: 255, g: 0, b: 0, a: 255 }, true);
+ yield ensurePixelIs(front, { x: 511, y: 511 }, { r: 0, g: 255, b: 0, a: 255 }, true);
+ ok(true, "The corner pixel colors are correct before highlighting.");
+
+ yield programActor.highlight([0, 1, 0, 1]);
+ yield ensurePixelIs(front, { x: 0, y: 0 }, { r: 0, g: 0, b: 0, a: 255 }, true);
+ yield ensurePixelIs(front, { x: 511, y: 511 }, { r: 0, g: 255, b: 0, a: 255 }, true);
+ ok(true, "The corner pixel colors are correct after highlighting.");
+
+ yield programActor.unhighlight();
+ yield ensurePixelIs(front, { x: 0, y: 0 }, { r: 255, g: 0, b: 0, a: 255 }, true);
+ yield ensurePixelIs(front, { x: 511, y: 511 }, { r: 0, g: 255, b: 0, a: 255 }, true);
+ ok(true, "The corner pixel colors are correct after unhighlighting.");
+ });
+ }
+}
diff --git a/devtools/client/shadereditor/test/browser_webgl-actor-test-11.js b/devtools/client/shadereditor/test/browser_webgl-actor-test-11.js
new file mode 100644
index 000000000..28663057e
--- /dev/null
+++ b/devtools/client/shadereditor/test/browser_webgl-actor-test-11.js
@@ -0,0 +1,25 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the WebGL context is never instrumented anymore after the
+ * finalize method is called.
+ */
+
+function* ifWebGLSupported() {
+ let { target, front } = yield initBackend(SIMPLE_CANVAS_URL);
+
+ let linked = once(front, "program-linked");
+ front.setup({ reload: true });
+ yield linked;
+ ok(true, "Canvas was correctly instrumented on the first navigation.");
+
+ once(front, "program-linked").then(() => {
+ ok(false, "A 'program-linked' notification shouldn't have been sent!");
+ });
+
+ yield front.finalize();
+ yield reload(target);
+ yield removeTab(target.tab);
+ finish();
+}
diff --git a/devtools/client/shadereditor/test/browser_webgl-actor-test-12.js b/devtools/client/shadereditor/test/browser_webgl-actor-test-12.js
new file mode 100644
index 000000000..f69d5e403
--- /dev/null
+++ b/devtools/client/shadereditor/test/browser_webgl-actor-test-12.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the correct vertex and fragment shader sources are retrieved
+ * regardless of the order in which they were compiled and attached.
+ */
+
+function* ifWebGLSupported() {
+ let { target, front } = yield initBackend(SHADER_ORDER_URL);
+ front.setup({ reload: true });
+
+ let programActor = yield once(front, "program-linked");
+ let vertexShader = yield programActor.getVertexShader();
+ let fragmentShader = yield programActor.getFragmentShader();
+
+ let vertSource = yield vertexShader.getText();
+ let fragSource = yield fragmentShader.getText();
+
+ ok(vertSource.includes("I'm a vertex shader!"),
+ "The correct vertex shader text was retrieved.");
+ ok(fragSource.includes("I'm a fragment shader!"),
+ "The correct fragment shader text was retrieved.");
+
+ yield removeTab(target.tab);
+ finish();
+}
diff --git a/devtools/client/shadereditor/test/browser_webgl-actor-test-13.js b/devtools/client/shadereditor/test/browser_webgl-actor-test-13.js
new file mode 100644
index 000000000..f4730ba39
--- /dev/null
+++ b/devtools/client/shadereditor/test/browser_webgl-actor-test-13.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if multiple WebGL contexts are correctly handled.
+ */
+
+function* ifWebGLSupported() {
+ let { target, front } = yield initBackend(MULTIPLE_CONTEXTS_URL);
+ front.setup({ reload: true });
+
+ let [firstProgramActor, secondProgramActor] = yield getPrograms(front, 2);
+
+ isnot(firstProgramActor, secondProgramActor,
+ "Two distinct program actors were recevide from two separate contexts.");
+
+ let firstVertexShader = yield firstProgramActor.getVertexShader();
+ let firstFragmentShader = yield firstProgramActor.getFragmentShader();
+ let secondVertexShader = yield secondProgramActor.getVertexShader();
+ let secondFragmentShader = yield secondProgramActor.getFragmentShader();
+
+ isnot(firstVertexShader, secondVertexShader,
+ "The two programs should have distinct vertex shaders.");
+ isnot(firstFragmentShader, secondFragmentShader,
+ "The two programs should have distinct fragment shaders.");
+
+ let firstVertSource = yield firstVertexShader.getText();
+ let firstFragSource = yield firstFragmentShader.getText();
+ let secondVertSource = yield secondVertexShader.getText();
+ let secondFragSource = yield secondFragmentShader.getText();
+
+ is(firstVertSource, secondVertSource,
+ "The vertex shaders should have identical sources.");
+ is(firstFragSource, secondFragSource,
+ "The vertex shaders should have identical sources.");
+
+ yield ensurePixelIs(front, { x: 0, y: 0 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+ yield ensurePixelIs(front, { x: 0, y: 0 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+ yield ensurePixelIs(front, { x: 127, y: 127 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+ yield ensurePixelIs(front, { x: 127, y: 127 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+ ok(true, "The two canvases are correctly drawn.");
+
+ yield firstProgramActor.highlight([1, 0, 0, 1]);
+ yield ensurePixelIs(front, { x: 0, y: 0 }, { r: 255, g: 0, b: 0, a: 255 }, true, "#canvas1");
+ yield ensurePixelIs(front, { x: 0, y: 0 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+ yield ensurePixelIs(front, { x: 127, y: 127 }, { r: 255, g: 0, b: 0, a: 255 }, true, "#canvas1");
+ yield ensurePixelIs(front, { x: 127, y: 127 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+ ok(true, "The first canvas was correctly filled after highlighting.");
+
+ yield secondProgramActor.highlight([0, 1, 0, 1]);
+ yield ensurePixelIs(front, { x: 0, y: 0 }, { r: 255, g: 0, b: 0, a: 255 }, true, "#canvas1");
+ yield ensurePixelIs(front, { x: 0, y: 0 }, { r: 0, g: 255, b: 0, a: 255 }, true, "#canvas2");
+ yield ensurePixelIs(front, { x: 127, y: 127 }, { r: 255, g: 0, b: 0, a: 255 }, true, "#canvas1");
+ yield ensurePixelIs(front, { x: 127, y: 127 }, { r: 0, g: 255, b: 0, a: 255 }, true, "#canvas2");
+ ok(true, "The second canvas was correctly filled after highlighting.");
+
+ yield firstProgramActor.unhighlight();
+ yield secondProgramActor.unhighlight();
+ yield ensurePixelIs(front, { x: 0, y: 0 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+ yield ensurePixelIs(front, { x: 0, y: 0 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+ yield ensurePixelIs(front, { x: 127, y: 127 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+ yield ensurePixelIs(front, { x: 127, y: 127 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+ ok(true, "The two canvases were correctly filled after unhighlighting.");
+
+ yield removeTab(target.tab);
+ finish();
+}
diff --git a/devtools/client/shadereditor/test/browser_webgl-actor-test-14.js b/devtools/client/shadereditor/test/browser_webgl-actor-test-14.js
new file mode 100644
index 000000000..342bba382
--- /dev/null
+++ b/devtools/client/shadereditor/test/browser_webgl-actor-test-14.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the rendering is updated when a uniform variable is
+ * changed in one shader of a page with multiple WebGL contexts.
+ */
+
+function* ifWebGLSupported() {
+ let { target, front } = yield initBackend(MULTIPLE_CONTEXTS_URL);
+ front.setup({ reload: true });
+
+ let [firstProgramActor, secondProgramActor] = yield getPrograms(front, 2);
+
+ let firstFragmentShader = yield firstProgramActor.getFragmentShader();
+ let secondFragmentShader = yield secondProgramActor.getFragmentShader();
+
+ let oldFragSource = yield firstFragmentShader.getText();
+ let newFragSource = oldFragSource.replace("vec4(uColor", "vec4(0.25, 0.25, 0.25");
+ let status = yield firstFragmentShader.compile(newFragSource);
+ ok(!status,
+ "The first new fragment shader source was compiled without errors.");
+
+ yield front.waitForFrame();
+ yield ensurePixelIs(front, { x: 0, y: 0 }, { r: 64, g: 64, b: 64, a: 255 }, true, "#canvas1");
+ yield ensurePixelIs(front, { x: 127, y: 127 }, { r: 64, g: 64, b: 64, a: 255 }, true, "#canvas1");
+ yield ensurePixelIs(front, { x: 0, y: 0 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+ yield ensurePixelIs(front, { x: 127, y: 127 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+ ok(true, "The first fragment shader was changed.");
+
+ oldFragSource = yield secondFragmentShader.getText();
+ newFragSource = oldFragSource.replace("vec4(uColor", "vec4(0.75, 0.75, 0.75");
+ status = yield secondFragmentShader.compile(newFragSource);
+ ok(!status,
+ "The second new fragment shader source was compiled without errors.");
+
+ yield front.waitForFrame();
+ yield ensurePixelIs(front, { x: 0, y: 0 }, { r: 64, g: 64, b: 64, a: 255 }, true, "#canvas1");
+ yield ensurePixelIs(front, { x: 127, y: 127 }, { r: 64, g: 64, b: 64, a: 255 }, true, "#canvas1");
+ yield ensurePixelIs(front, { x: 0, y: 0 }, { r: 191, g: 191, b: 191, a: 255 }, true, "#canvas2");
+ yield ensurePixelIs(front, { x: 127, y: 127 }, { r: 191, g: 191, b: 191, a: 255 }, true, "#canvas2");
+ ok(true, "The second fragment shader was changed.");
+
+ yield removeTab(target.tab);
+ finish();
+}
diff --git a/devtools/client/shadereditor/test/browser_webgl-actor-test-15.js b/devtools/client/shadereditor/test/browser_webgl-actor-test-15.js
new file mode 100644
index 000000000..0a65dbe0a
--- /dev/null
+++ b/devtools/client/shadereditor/test/browser_webgl-actor-test-15.js
@@ -0,0 +1,133 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if program actors are cached when navigating in the bfcache.
+ */
+
+function* ifWebGLSupported() {
+ let { target, front } = yield initBackend(SIMPLE_CANVAS_URL);
+ front.setup({ reload: false });
+
+ // Attach frame scripts if in e10s to perform
+ // history navigation via the content
+ loadFrameScripts();
+
+ reload(target);
+ let firstProgram = yield once(front, "program-linked");
+ yield checkFirstCachedPrograms(firstProgram);
+ yield checkHighlightingInTheFirstPage(firstProgram);
+ ok(true, "The cached programs behave correctly before the navigation.");
+
+ navigate(target, MULTIPLE_CONTEXTS_URL);
+ let [secondProgram, thirdProgram] = yield getPrograms(front, 2);
+ yield checkSecondCachedPrograms(firstProgram, [secondProgram, thirdProgram]);
+ yield checkHighlightingInTheSecondPage(secondProgram, thirdProgram);
+ ok(true, "The cached programs behave correctly after the navigation.");
+
+ once(front, "program-linked").then(() => {
+ ok(false, "Shouldn't have received any more program-linked notifications.");
+ });
+
+ yield navigateInHistory(target, "back");
+ yield checkFirstCachedPrograms(firstProgram);
+ yield checkHighlightingInTheFirstPage(firstProgram);
+ ok(true, "The cached programs behave correctly after navigating back.");
+
+ yield navigateInHistory(target, "forward");
+ yield checkSecondCachedPrograms(firstProgram, [secondProgram, thirdProgram]);
+ yield checkHighlightingInTheSecondPage(secondProgram, thirdProgram);
+ ok(true, "The cached programs behave correctly after navigating forward.");
+
+ yield navigateInHistory(target, "back");
+ yield checkFirstCachedPrograms(firstProgram);
+ yield checkHighlightingInTheFirstPage(firstProgram);
+ ok(true, "The cached programs behave correctly after navigating back again.");
+
+ yield navigateInHistory(target, "forward");
+ yield checkSecondCachedPrograms(firstProgram, [secondProgram, thirdProgram]);
+ yield checkHighlightingInTheSecondPage(secondProgram, thirdProgram);
+ ok(true, "The cached programs behave correctly after navigating forward again.");
+
+ yield removeTab(target.tab);
+ finish();
+
+ function checkFirstCachedPrograms(programActor) {
+ return Task.spawn(function* () {
+ let programs = yield front.getPrograms();
+
+ is(programs.length, 1,
+ "There should be 1 cached program actor.");
+ is(programs[0], programActor,
+ "The cached program actor was the expected one.");
+ });
+ }
+
+ function checkSecondCachedPrograms(oldProgramActor, newProgramActors) {
+ return Task.spawn(function* () {
+ let programs = yield front.getPrograms();
+
+ is(programs.length, 2,
+ "There should be 2 cached program actors after the navigation.");
+ is(programs[0], newProgramActors[0],
+ "The first cached program actor was the expected one after the navigation.");
+ is(programs[1], newProgramActors[1],
+ "The second cached program actor was the expected one after the navigation.");
+
+ isnot(newProgramActors[0], oldProgramActor,
+ "The old program actor is not equal to the new first program actor.");
+ isnot(newProgramActors[1], oldProgramActor,
+ "The old program actor is not equal to the new second program actor.");
+ });
+ }
+
+ function checkHighlightingInTheFirstPage(programActor) {
+ return Task.spawn(function* () {
+ yield ensurePixelIs(front, { x: 0, y: 0 }, { r: 255, g: 0, b: 0, a: 255 }, true);
+ yield ensurePixelIs(front, { x: 511, y: 511 }, { r: 0, g: 255, b: 0, a: 255 }, true);
+ ok(true, "The corner pixel colors are correct before highlighting.");
+
+ yield programActor.highlight([0, 1, 0, 1]);
+ yield ensurePixelIs(front, { x: 0, y: 0 }, { r: 0, g: 0, b: 0, a: 255 }, true);
+ yield ensurePixelIs(front, { x: 511, y: 511 }, { r: 0, g: 255, b: 0, a: 255 }, true);
+ ok(true, "The corner pixel colors are correct after highlighting.");
+
+ yield programActor.unhighlight();
+ yield ensurePixelIs(front, { x: 0, y: 0 }, { r: 255, g: 0, b: 0, a: 255 }, true);
+ yield ensurePixelIs(front, { x: 511, y: 511 }, { r: 0, g: 255, b: 0, a: 255 }, true);
+ ok(true, "The corner pixel colors are correct after unhighlighting.");
+ });
+ }
+
+ function checkHighlightingInTheSecondPage(firstProgramActor, secondProgramActor) {
+ return Task.spawn(function* () {
+ yield ensurePixelIs(front, { x: 0, y: 0 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+ yield ensurePixelIs(front, { x: 0, y: 0 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+ yield ensurePixelIs(front, { x: 127, y: 127 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+ yield ensurePixelIs(front, { x: 127, y: 127 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+ ok(true, "The two canvases are correctly drawn before highlighting.");
+
+ yield firstProgramActor.highlight([1, 0, 0, 1]);
+ yield ensurePixelIs(front, { x: 0, y: 0 }, { r: 255, g: 0, b: 0, a: 255 }, true, "#canvas1");
+ yield ensurePixelIs(front, { x: 0, y: 0 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+ yield ensurePixelIs(front, { x: 127, y: 127 }, { r: 255, g: 0, b: 0, a: 255 }, true, "#canvas1");
+ yield ensurePixelIs(front, { x: 127, y: 127 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+ ok(true, "The first canvas was correctly filled after highlighting.");
+
+ yield secondProgramActor.highlight([0, 1, 0, 1]);
+ yield ensurePixelIs(front, { x: 0, y: 0 }, { r: 255, g: 0, b: 0, a: 255 }, true, "#canvas1");
+ yield ensurePixelIs(front, { x: 0, y: 0 }, { r: 0, g: 255, b: 0, a: 255 }, true, "#canvas2");
+ yield ensurePixelIs(front, { x: 127, y: 127 }, { r: 255, g: 0, b: 0, a: 255 }, true, "#canvas1");
+ yield ensurePixelIs(front, { x: 127, y: 127 }, { r: 0, g: 255, b: 0, a: 255 }, true, "#canvas2");
+ ok(true, "The second canvas was correctly filled after highlighting.");
+
+ yield firstProgramActor.unhighlight();
+ yield secondProgramActor.unhighlight();
+ yield ensurePixelIs(front, { x: 0, y: 0 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+ yield ensurePixelIs(front, { x: 0, y: 0 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+ yield ensurePixelIs(front, { x: 127, y: 127 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+ yield ensurePixelIs(front, { x: 127, y: 127 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+ ok(true, "The two canvases were correctly filled after unhighlighting.");
+ });
+ }
+}
diff --git a/devtools/client/shadereditor/test/browser_webgl-actor-test-16.js b/devtools/client/shadereditor/test/browser_webgl-actor-test-16.js
new file mode 100644
index 000000000..e61e73102
--- /dev/null
+++ b/devtools/client/shadereditor/test/browser_webgl-actor-test-16.js
@@ -0,0 +1,141 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if program actors are invalidated from the cache when a window is
+ * removed from the bfcache.
+ */
+
+function* ifWebGLSupported() {
+ let { target, front } = yield initBackend(SIMPLE_CANVAS_URL);
+ front.setup({ reload: false });
+
+ // Attach frame scripts if in e10s to perform
+ // history navigation via the content
+ loadFrameScripts();
+
+ // 0. Perform the initial reload.
+
+ reload(target);
+ let firstProgram = yield once(front, "program-linked");
+ let programs = yield front.getPrograms();
+ is(programs.length, 1,
+ "The first program should be returned by a call to getPrograms().");
+ is(programs[0], firstProgram,
+ "The first programs was correctly retrieved from the cache.");
+
+ let allPrograms = yield front._getAllPrograms();
+ is(allPrograms.length, 1,
+ "Should be only one program in cache.");
+
+ // 1. Perform a simple navigation.
+
+ navigate(target, MULTIPLE_CONTEXTS_URL);
+ let [secondProgram, thirdProgram] = yield getPrograms(front, 2);
+ programs = yield front.getPrograms();
+ is(programs.length, 2,
+ "The second and third programs should be returned by a call to getPrograms().");
+ is(programs[0], secondProgram,
+ "The second programs was correctly retrieved from the cache.");
+ is(programs[1], thirdProgram,
+ "The third programs was correctly retrieved from the cache.");
+
+ allPrograms = yield front._getAllPrograms();
+ is(allPrograms.length, 3,
+ "Should be three programs in cache.");
+
+ // 2. Perform a bfcache navigation.
+
+ yield navigateInHistory(target, "back");
+ let globalDestroyed = once(front, "global-created");
+ let globalCreated = once(front, "global-destroyed");
+ let programsLinked = once(front, "program-linked");
+ reload(target);
+
+ yield promise.all([programsLinked, globalDestroyed, globalCreated]);
+ allPrograms = yield front._getAllPrograms();
+ is(allPrograms.length, 3,
+ "Should be 3 programs total in cache.");
+
+ programs = yield front.getPrograms();
+ is(programs.length, 1,
+ "There should be 1 cached program actor now.");
+
+ yield checkHighlightingInTheFirstPage(programs[0]);
+ ok(true, "The cached programs behave correctly after navigating back and reloading.");
+
+ // 3. Perform a bfcache navigation and a page reload.
+
+ yield navigateInHistory(target, "forward");
+
+ globalDestroyed = once(front, "global-created");
+ globalCreated = once(front, "global-destroyed");
+ programsLinked = getPrograms(front, 2);
+
+ reload(target);
+
+ yield promise.all([programsLinked, globalDestroyed, globalCreated]);
+ allPrograms = yield front._getAllPrograms();
+ is(allPrograms.length, 3,
+ "Should be 3 programs total in cache.");
+
+ programs = yield front.getPrograms();
+ is(programs.length, 2,
+ "There should be 2 cached program actors now.");
+
+ yield checkHighlightingInTheSecondPage(programs[0], programs[1]);
+ ok(true, "The cached programs behave correctly after navigating forward and reloading.");
+
+ yield removeTab(target.tab);
+ finish();
+
+ function checkHighlightingInTheFirstPage(programActor) {
+ return Task.spawn(function* () {
+ yield ensurePixelIs(front, { x: 0, y: 0 }, { r: 255, g: 0, b: 0, a: 255 }, true);
+ yield ensurePixelIs(front, { x: 511, y: 511 }, { r: 0, g: 255, b: 0, a: 255 }, true);
+ ok(true, "The corner pixel colors are correct before highlighting.");
+
+ yield programActor.highlight([0, 1, 0, 1]);
+ yield ensurePixelIs(front, { x: 0, y: 0 }, { r: 0, g: 0, b: 0, a: 255 }, true);
+ yield ensurePixelIs(front, { x: 511, y: 511 }, { r: 0, g: 255, b: 0, a: 255 }, true);
+ ok(true, "The corner pixel colors are correct after highlighting.");
+
+ yield programActor.unhighlight();
+ yield ensurePixelIs(front, { x: 0, y: 0 }, { r: 255, g: 0, b: 0, a: 255 }, true);
+ yield ensurePixelIs(front, { x: 511, y: 511 }, { r: 0, g: 255, b: 0, a: 255 }, true);
+ ok(true, "The corner pixel colors are correct after unhighlighting.");
+ });
+ }
+
+ function checkHighlightingInTheSecondPage(firstProgramActor, secondProgramActor) {
+ return Task.spawn(function* () {
+ yield ensurePixelIs(front, { x: 0, y: 0 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+ yield ensurePixelIs(front, { x: 0, y: 0 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+ yield ensurePixelIs(front, { x: 127, y: 127 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+ yield ensurePixelIs(front, { x: 127, y: 127 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+ ok(true, "The two canvases are correctly drawn before highlighting.");
+
+ yield firstProgramActor.highlight([1, 0, 0, 1]);
+ yield ensurePixelIs(front, { x: 0, y: 0 }, { r: 255, g: 0, b: 0, a: 255 }, true, "#canvas1");
+ yield ensurePixelIs(front, { x: 0, y: 0 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+ yield ensurePixelIs(front, { x: 127, y: 127 }, { r: 255, g: 0, b: 0, a: 255 }, true, "#canvas1");
+ yield ensurePixelIs(front, { x: 127, y: 127 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+ ok(true, "The first canvas was correctly filled after highlighting.");
+
+ yield secondProgramActor.highlight([0, 1, 0, 1]);
+ yield ensurePixelIs(front, { x: 0, y: 0 }, { r: 255, g: 0, b: 0, a: 255 }, true, "#canvas1");
+ yield ensurePixelIs(front, { x: 0, y: 0 }, { r: 0, g: 255, b: 0, a: 255 }, true, "#canvas2");
+ yield ensurePixelIs(front, { x: 127, y: 127 }, { r: 255, g: 0, b: 0, a: 255 }, true, "#canvas1");
+ yield ensurePixelIs(front, { x: 127, y: 127 }, { r: 0, g: 255, b: 0, a: 255 }, true, "#canvas2");
+ ok(true, "The second canvas was correctly filled after highlighting.");
+
+ yield firstProgramActor.unhighlight();
+ yield secondProgramActor.unhighlight();
+ yield ensurePixelIs(front, { x: 0, y: 0 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+ yield ensurePixelIs(front, { x: 0, y: 0 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+ yield ensurePixelIs(front, { x: 127, y: 127 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+ yield ensurePixelIs(front, { x: 127, y: 127 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+ ok(true, "The two canvases were correctly filled after unhighlighting.");
+ });
+ }
+}
diff --git a/devtools/client/shadereditor/test/browser_webgl-actor-test-17.js b/devtools/client/shadereditor/test/browser_webgl-actor-test-17.js
new file mode 100644
index 000000000..92b940d4a
--- /dev/null
+++ b/devtools/client/shadereditor/test/browser_webgl-actor-test-17.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the blackbox/unblackbox operations work as expected with
+ * overlapping geometry.
+ */
+
+function* ifWebGLSupported() {
+ let { target, front } = yield initBackend(OVERLAPPING_GEOMETRY_CANVAS_URL);
+ front.setup({ reload: true });
+
+ let [firstProgramActor, secondProgramActor] = yield getPrograms(front, 2);
+
+ yield ensurePixelIs(front, { x: 0, y: 0 }, { r: 255, g: 255, b: 0, a: 255 }, true);
+ yield ensurePixelIs(front, { x: 64, y: 64 }, { r: 0, g: 255, b: 255, a: 255 }, true);
+ yield ensurePixelIs(front, { x: 127, y: 127 }, { r: 255, g: 255, b: 0, a: 255 }, true);
+ ok(true, "The corner vs. center pixel colors are correct before blackboxing.");
+
+ yield firstProgramActor.blackbox();
+ yield ensurePixelIs(front, { x: 0, y: 0 }, { r: 0, g: 0, b: 0, a: 255 }, true);
+ yield ensurePixelIs(front, { x: 64, y: 64 }, { r: 0, g: 255, b: 255, a: 255 }, true);
+ yield ensurePixelIs(front, { x: 127, y: 127 }, { r: 0, g: 0, b: 0, a: 255 }, true);
+ ok(true, "The corner vs. center pixel colors are correct after blackboxing (1).");
+
+ yield firstProgramActor.unblackbox();
+ yield ensurePixelIs(front, { x: 0, y: 0 }, { r: 255, g: 255, b: 0, a: 255 }, true);
+ yield ensurePixelIs(front, { x: 64, y: 64 }, { r: 0, g: 255, b: 255, a: 255 }, true);
+ yield ensurePixelIs(front, { x: 127, y: 127 }, { r: 255, g: 255, b: 0, a: 255 }, true);
+ ok(true, "The corner vs. center pixel colors are correct after unblackboxing (1).");
+
+ yield secondProgramActor.blackbox();
+ yield ensurePixelIs(front, { x: 0, y: 0 }, { r: 255, g: 255, b: 0, a: 255 }, true);
+ yield ensurePixelIs(front, { x: 64, y: 64 }, { r: 255, g: 255, b: 0, a: 255 }, true);
+ yield ensurePixelIs(front, { x: 127, y: 127 }, { r: 255, g: 255, b: 0, a: 255 }, true);
+ ok(true, "The corner vs. center pixel colors are correct after blackboxing (2).");
+
+ yield secondProgramActor.unblackbox();
+ yield ensurePixelIs(front, { x: 0, y: 0 }, { r: 255, g: 255, b: 0, a: 255 }, true);
+ yield ensurePixelIs(front, { x: 64, y: 64 }, { r: 0, g: 255, b: 255, a: 255 }, true);
+ yield ensurePixelIs(front, { x: 127, y: 127 }, { r: 255, g: 255, b: 0, a: 255 }, true);
+ ok(true, "The corner vs. center pixel colors are correct after unblackboxing (2).");
+
+ yield removeTab(target.tab);
+ finish();
+}
diff --git a/devtools/client/shadereditor/test/browser_webgl-actor-test-18.js b/devtools/client/shadereditor/test/browser_webgl-actor-test-18.js
new file mode 100644
index 000000000..977b07d86
--- /dev/null
+++ b/devtools/client/shadereditor/test/browser_webgl-actor-test-18.js
@@ -0,0 +1,31 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests the `getPixel` actor method works across threads.
+ */
+
+function* ifWebGLSupported() {
+ let { target, front } = yield initBackend(MULTIPLE_CONTEXTS_URL);
+ front.setup({ reload: true });
+
+ yield getPrograms(front, 2);
+
+ // Wait a frame to ensure rendering
+ yield front.waitForFrame();
+
+ let pixel = yield front.getPixel({ selector: "#canvas1", position: { x: 0, y: 0 }});
+ is(pixel.r, 255, "correct `r` value for first canvas.");
+ is(pixel.g, 255, "correct `g` value for first canvas.");
+ is(pixel.b, 0, "correct `b` value for first canvas.");
+ is(pixel.a, 255, "correct `a` value for first canvas.");
+
+ pixel = yield front.getPixel({ selector: "#canvas2", position: { x: 0, y: 0 }});
+ is(pixel.r, 0, "correct `r` value for second canvas.");
+ is(pixel.g, 255, "correct `g` value for second canvas.");
+ is(pixel.b, 255, "correct `b` value for second canvas.");
+ is(pixel.a, 255, "correct `a` value for second canvas.");
+
+ yield removeTab(target.tab);
+ finish();
+}
diff --git a/devtools/client/shadereditor/test/doc_blended-geometry.html b/devtools/client/shadereditor/test/doc_blended-geometry.html
new file mode 100644
index 000000000..75cad6dc7
--- /dev/null
+++ b/devtools/client/shadereditor/test/doc_blended-geometry.html
@@ -0,0 +1,136 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>WebGL editor test page</title>
+
+ <script id="shader-vs" type="x-shader/x-vertex">
+ precision lowp float;
+ attribute vec3 aVertexPosition;
+ uniform float uDepth;
+
+ void main(void) {
+ gl_Position = vec4(aVertexPosition, uDepth);
+ }
+ </script>
+
+ <script id="shader-fs-0" type="x-shader/x-fragment">
+ precision lowp float;
+
+ void main(void) {
+ gl_FragColor = vec4(0.5, 0.5, 0.5, 1.0);
+ }
+ </script>
+
+ <script id="shader-fs-1" type="x-shader/x-fragment">
+ precision lowp float;
+
+ void main(void) {
+ gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
+ }
+ </script>
+ </head>
+
+ <body>
+ <canvas id="canvas" width="128" height="128"></canvas>
+
+ <script type="text/javascript;version=1.8">
+ "use strict";
+
+ let canvas, gl;
+ let program = [];
+ let squareVerticesPositionBuffer;
+ let vertexPositionAttribute = [];
+ let depthUniform = [];
+
+ window.onload = function() {
+ canvas = document.querySelector("canvas");
+ gl = canvas.getContext("webgl", { preserveDrawingBuffer: true });
+ gl.clearColor(0.0, 0.0, 0.0, 1.0);
+
+ initProgram(0);
+ initProgram(1);
+ initBuffers();
+ drawScene();
+ }
+
+ function initProgram(i) {
+ let vertexShader = getShader("shader-vs");
+ let fragmentShader = getShader("shader-fs-" + i);
+
+ program[i] = gl.createProgram();
+ gl.attachShader(program[i], vertexShader);
+ gl.attachShader(program[i], fragmentShader);
+ gl.linkProgram(program[i]);
+
+ vertexPositionAttribute[i] = gl.getAttribLocation(program[i], "aVertexPosition");
+ gl.enableVertexAttribArray(vertexPositionAttribute[i]);
+
+ depthUniform[i] = gl.getUniformLocation(program[i], "uDepth");
+ }
+
+ function getShader(id) {
+ let script = document.getElementById(id);
+ let source = script.textContent;
+ let shader;
+
+ if (script.type == "x-shader/x-fragment") {
+ shader = gl.createShader(gl.FRAGMENT_SHADER);
+ } else if (script.type == "x-shader/x-vertex") {
+ shader = gl.createShader(gl.VERTEX_SHADER);
+ }
+
+ gl.shaderSource(shader, source);
+ gl.compileShader(shader);
+
+ return shader;
+ }
+
+ function initBuffers() {
+ squareVerticesPositionBuffer = gl.createBuffer();
+ gl.bindBuffer(gl.ARRAY_BUFFER, squareVerticesPositionBuffer);
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
+ 1.0, 1.0, 0.0,
+ -1.0, 1.0, 0.0,
+ 1.0, -1.0, 0.0,
+ -1.0, -1.0, 0.0
+ ]), gl.STATIC_DRAW);
+ }
+
+ function drawScene() {
+ gl.clear(gl.COLOR_BUFFER_BIT);
+
+ for (let i = 0; i < 2; i++) {
+ gl.bindBuffer(gl.ARRAY_BUFFER, squareVerticesPositionBuffer);
+ gl.vertexAttribPointer(vertexPositionAttribute[i], 3, gl.FLOAT, false, 0, 0);
+
+ gl.useProgram(program[i]);
+ gl.uniform1f(depthUniform[i], i + 1);
+ blend(i);
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
+ }
+
+ window.requestAnimationFrame(drawScene);
+ }
+
+ function blend(i) {
+ if (i == 0) {
+ gl.disable(gl.BLEND);
+ }
+ else if (i == 1) {
+ gl.enable(gl.BLEND);
+ gl.blendColor(0.5, 0, 0, 0.25);
+ gl.blendEquationSeparate(
+ gl.FUNC_REVERSE_SUBTRACT, gl.FUNC_SUBTRACT);
+ gl.blendFuncSeparate(
+ gl.CONSTANT_COLOR, gl.ONE_MINUS_CONSTANT_COLOR,
+ gl.ONE_MINUS_CONSTANT_COLOR, gl.CONSTANT_COLOR);
+ }
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/shadereditor/test/doc_multiple-contexts.html b/devtools/client/shadereditor/test/doc_multiple-contexts.html
new file mode 100644
index 000000000..039ee62d0
--- /dev/null
+++ b/devtools/client/shadereditor/test/doc_multiple-contexts.html
@@ -0,0 +1,112 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>WebGL editor test page</title>
+
+ <script id="shader-vs" type="x-shader/x-vertex">
+ precision lowp float;
+ attribute vec3 aVertexPosition;
+
+ void main(void) {
+ gl_Position = vec4(aVertexPosition, 1);
+ }
+ </script>
+
+ <script id="shader-fs" type="x-shader/x-fragment">
+ precision lowp float;
+ uniform vec3 uColor;
+
+ void main(void) {
+ gl_FragColor = vec4(uColor, 1);
+ }
+ </script>
+ </head>
+
+ <body>
+ <canvas id="canvas1" width="128" height="128"></canvas>
+ <canvas id="canvas2" width="128" height="128"></canvas>
+
+ <script type="text/javascript;version=1.8">
+ "use strict";
+
+ let canvas = [], gl = [];
+ let program = [];
+ let squareVerticesPositionBuffer = [];
+ let vertexPositionAttribute = [];
+ let colorUniform = [];
+
+ window.onload = function() {
+ for (let i = 0; i < 2; i++) {
+ canvas[i] = document.querySelector("#canvas" + (i + 1));
+ gl[i] = canvas[i].getContext("webgl", { preserveDrawingBuffer: true });
+ gl[i].clearColor(0.0, 0.0, 0.0, 1.0);
+
+ initProgram(i);
+ initBuffers(i);
+ drawScene(i);
+ }
+ }
+
+ function initProgram(i) {
+ let vertexShader = getShader(gl[i], "shader-vs");
+ let fragmentShader = getShader(gl[i], "shader-fs");
+
+ program[i] = gl[i].createProgram();
+ gl[i].attachShader(program[i], vertexShader);
+ gl[i].attachShader(program[i], fragmentShader);
+ gl[i].linkProgram(program[i]);
+
+ vertexPositionAttribute[i] = gl[i].getAttribLocation(program[i], "aVertexPosition");
+ gl[i].enableVertexAttribArray(vertexPositionAttribute[i]);
+
+ colorUniform[i] = gl[i].getUniformLocation(program[i], "uColor");
+ }
+
+ function getShader(gl, id) {
+ let script = document.getElementById(id);
+ let source = script.textContent;
+ let shader;
+
+ if (script.type == "x-shader/x-fragment") {
+ shader = gl.createShader(gl.FRAGMENT_SHADER);
+ } else if (script.type == "x-shader/x-vertex") {
+ shader = gl.createShader(gl.VERTEX_SHADER);
+ }
+
+ gl.shaderSource(shader, source);
+ gl.compileShader(shader);
+
+ return shader;
+ }
+
+ function initBuffers(i) {
+ squareVerticesPositionBuffer[i] = gl[i].createBuffer();
+ gl[i].bindBuffer(gl[i].ARRAY_BUFFER, squareVerticesPositionBuffer[i]);
+ gl[i].bufferData(gl[i].ARRAY_BUFFER, new Float32Array([
+ 1.0, 1.0, 0.0,
+ -1.0, 1.0, 0.0,
+ 1.0, -1.0, 0.0,
+ -1.0, -1.0, 0.0
+ ]), gl[i].STATIC_DRAW);
+ }
+
+ function drawScene(i) {
+ gl[i].clear(gl[i].COLOR_BUFFER_BIT);
+
+ gl[i].bindBuffer(gl[i].ARRAY_BUFFER, squareVerticesPositionBuffer[i]);
+ gl[i].vertexAttribPointer(vertexPositionAttribute[i], 3, gl[i].FLOAT, false, 0, 0);
+
+ gl[i].useProgram(program[i]);
+ gl[i].uniform3fv(colorUniform[i], i == 0 ? [1, 1, 0] : [0, 1, 1]);
+ gl[i].drawArrays(gl[i].TRIANGLE_STRIP, 0, 4);
+
+ window.requestAnimationFrame(() => drawScene(i));
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/shadereditor/test/doc_overlapping-geometry.html b/devtools/client/shadereditor/test/doc_overlapping-geometry.html
new file mode 100644
index 000000000..34be8f57a
--- /dev/null
+++ b/devtools/client/shadereditor/test/doc_overlapping-geometry.html
@@ -0,0 +1,120 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>WebGL editor test page</title>
+
+ <script id="shader-vs" type="x-shader/x-vertex">
+ precision lowp float;
+ attribute vec3 aVertexPosition;
+ uniform float uDepth;
+
+ void main(void) {
+ gl_Position = vec4(aVertexPosition, uDepth);
+ }
+ </script>
+
+ <script id="shader-fs-0" type="x-shader/x-fragment">
+ precision lowp float;
+
+ void main(void) {
+ gl_FragColor = vec4(1.0, 1.0, 0.0, 1.0);
+ }
+ </script>
+
+ <script id="shader-fs-1" type="x-shader/x-fragment">
+ precision lowp float;
+
+ void main(void) {
+ gl_FragColor = vec4(0.0, 1.0, 1.0, 1.0);
+ }
+ </script>
+ </head>
+
+ <body>
+ <canvas id="canvas" width="128" height="128"></canvas>
+
+ <script type="text/javascript;version=1.8">
+ "use strict";
+
+ let canvas, gl;
+ let program = [];
+ let squareVerticesPositionBuffer;
+ let vertexPositionAttribute = [];
+ let depthUniform = [];
+
+ window.onload = function() {
+ canvas = document.querySelector("canvas");
+ gl = canvas.getContext("webgl", { preserveDrawingBuffer: true });
+ gl.clearColor(0.0, 0.0, 0.0, 1.0);
+
+ initProgram(0);
+ initProgram(1);
+ initBuffers();
+ drawScene();
+ }
+
+ function initProgram(i) {
+ let vertexShader = getShader("shader-vs");
+ let fragmentShader = getShader("shader-fs-" + i);
+
+ program[i] = gl.createProgram();
+ gl.attachShader(program[i], vertexShader);
+ gl.attachShader(program[i], fragmentShader);
+ gl.linkProgram(program[i]);
+
+ vertexPositionAttribute[i] = gl.getAttribLocation(program[i], "aVertexPosition");
+ gl.enableVertexAttribArray(vertexPositionAttribute[i]);
+
+ depthUniform[i] = gl.getUniformLocation(program[i], "uDepth");
+ }
+
+ function getShader(id) {
+ let script = document.getElementById(id);
+ let source = script.textContent;
+ let shader;
+
+ if (script.type == "x-shader/x-fragment") {
+ shader = gl.createShader(gl.FRAGMENT_SHADER);
+ } else if (script.type == "x-shader/x-vertex") {
+ shader = gl.createShader(gl.VERTEX_SHADER);
+ }
+
+ gl.shaderSource(shader, source);
+ gl.compileShader(shader);
+
+ return shader;
+ }
+
+ function initBuffers() {
+ squareVerticesPositionBuffer = gl.createBuffer();
+ gl.bindBuffer(gl.ARRAY_BUFFER, squareVerticesPositionBuffer);
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
+ 1.0, 1.0, 0.0,
+ -1.0, 1.0, 0.0,
+ 1.0, -1.0, 0.0,
+ -1.0, -1.0, 0.0
+ ]), gl.STATIC_DRAW);
+ }
+
+ function drawScene() {
+ gl.clear(gl.COLOR_BUFFER_BIT);
+
+ for (let i = 0; i < 2; i++) {
+ gl.bindBuffer(gl.ARRAY_BUFFER, squareVerticesPositionBuffer);
+ gl.vertexAttribPointer(vertexPositionAttribute[i], 3, gl.FLOAT, false, 0, 0);
+
+ gl.useProgram(program[i]);
+ gl.uniform1f(depthUniform[i], i + 1);
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
+ }
+
+ window.requestAnimationFrame(drawScene);
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/shadereditor/test/doc_shader-order.html b/devtools/client/shadereditor/test/doc_shader-order.html
new file mode 100644
index 000000000..a7cec53aa
--- /dev/null
+++ b/devtools/client/shadereditor/test/doc_shader-order.html
@@ -0,0 +1,83 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>WebGL editor test page</title>
+
+ <script id="shader-vs" type="x-shader/x-vertex">
+ precision lowp float;
+
+ void main(void) {
+ gl_Position = vec4(0, 0, 0, 1); // I'm a vertex shader!
+ }
+ </script>
+
+ <script id="shader-fs" type="x-shader/x-fragment">
+ precision lowp float;
+ varying vec3 vFragmentColor;
+
+ void main(void) {
+ gl_FragColor = vec4(1, 0, 0, 1); // I'm a fragment shader!
+ }
+ </script>
+ </head>
+
+ <body>
+ <canvas width="512" height="512"></canvas>
+
+ <script type="text/javascript;version=1.8">
+ "use strict";
+
+ let canvas, gl;
+
+ window.onload = function() {
+ canvas = document.querySelector("canvas");
+ gl = canvas.getContext("webgl", { preserveDrawingBuffer: true });
+
+ let shaderProgram = gl.createProgram();
+ let vertexShader, fragmentShader;
+
+ // Compile and attach the shaders in a random order. The test will
+ // ensure that the correct vertex and fragment source is retrieved
+ // regardless of this crazyness.
+ if (Math.random() > 0.5) {
+ vertexShader = getShader(gl, "shader-vs");
+ fragmentShader = getShader(gl, "shader-fs");
+ } else {
+ fragmentShader = getShader(gl, "shader-fs");
+ vertexShader = getShader(gl, "shader-vs");
+ }
+ if (Math.random() > 0.5) {
+ gl.attachShader(shaderProgram, vertexShader);
+ gl.attachShader(shaderProgram, fragmentShader);
+ } else {
+ gl.attachShader(shaderProgram, fragmentShader);
+ gl.attachShader(shaderProgram, vertexShader);
+ }
+
+ gl.linkProgram(shaderProgram);
+ }
+
+ function getShader(gl, id) {
+ let script = document.getElementById(id);
+ let source = script.textContent;
+ let shader;
+
+ if (script.type == "x-shader/x-fragment") {
+ shader = gl.createShader(gl.FRAGMENT_SHADER);
+ } else if (script.type == "x-shader/x-vertex") {
+ shader = gl.createShader(gl.VERTEX_SHADER);
+ }
+
+ gl.shaderSource(shader, source);
+ gl.compileShader(shader);
+
+ return shader;
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/shadereditor/test/doc_simple-canvas.html b/devtools/client/shadereditor/test/doc_simple-canvas.html
new file mode 100644
index 000000000..2a709ad8e
--- /dev/null
+++ b/devtools/client/shadereditor/test/doc_simple-canvas.html
@@ -0,0 +1,125 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>WebGL editor test page</title>
+
+ <script id="shader-vs" type="x-shader/x-vertex">
+ precision lowp float;
+ attribute vec3 aVertexPosition;
+ attribute vec3 aVertexColor;
+ varying vec3 vFragmentColor;
+
+ void main(void) {
+ gl_Position = vec4(aVertexPosition, 1.0);
+ vFragmentColor = aVertexColor; // I'm special!
+ }
+ </script>
+
+ <script id="shader-fs" type="x-shader/x-fragment">
+ precision lowp float;
+ varying vec3 vFragmentColor;
+
+ void main(void) {
+ gl_FragColor = vec4(vFragmentColor, 1.0); // I'm also special!
+ }
+ </script>
+ </head>
+
+ <body>
+ <canvas width="512" height="512"></canvas>
+
+ <script type="text/javascript;version=1.8">
+ "use strict";
+
+ let canvas, gl;
+ let program;
+ let squareVerticesPositionBuffer;
+ let squareVerticesColorBuffer;
+ let vertexPositionAttribute;
+ let vertexColorAttribute;
+
+ window.onload = function() {
+ canvas = document.querySelector("canvas");
+ gl = canvas.getContext("webgl", { preserveDrawingBuffer: true });
+ gl.clearColor(0.0, 0.0, 0.0, 1.0);
+
+ initProgram();
+ initBuffers();
+ drawScene();
+ }
+
+ function initProgram() {
+ let vertexShader = getShader(gl, "shader-vs");
+ let fragmentShader = getShader(gl, "shader-fs");
+
+ program = gl.createProgram();
+ gl.attachShader(program, vertexShader);
+ gl.attachShader(program, fragmentShader);
+ gl.linkProgram(program);
+
+ vertexPositionAttribute = gl.getAttribLocation(program, "aVertexPosition");
+ gl.enableVertexAttribArray(vertexPositionAttribute);
+
+ vertexColorAttribute = gl.getAttribLocation(program, "aVertexColor");
+ gl.enableVertexAttribArray(vertexColorAttribute);
+ }
+
+ function getShader(gl, id) {
+ let script = document.getElementById(id);
+ let source = script.textContent;
+ let shader;
+
+ if (script.type == "x-shader/x-fragment") {
+ shader = gl.createShader(gl.FRAGMENT_SHADER);
+ } else if (script.type == "x-shader/x-vertex") {
+ shader = gl.createShader(gl.VERTEX_SHADER);
+ }
+
+ gl.shaderSource(shader, source);
+ gl.compileShader(shader);
+
+ return shader;
+ }
+
+ function initBuffers() {
+ squareVerticesPositionBuffer = gl.createBuffer();
+ gl.bindBuffer(gl.ARRAY_BUFFER, squareVerticesPositionBuffer);
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
+ 1.0, 1.0, 0.0,
+ -1.0, 1.0, 0.0,
+ 1.0, -1.0, 0.0,
+ -1.0, -1.0, 0.0
+ ]), gl.STATIC_DRAW);
+
+ squareVerticesColorBuffer = gl.createBuffer();
+ gl.bindBuffer(gl.ARRAY_BUFFER, squareVerticesColorBuffer);
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
+ 1.0, 1.0, 1.0, 1.0,
+ 1.0, 0.0, 0.0, 1.0,
+ 0.0, 1.0, 0.0, 1.0,
+ 0.0, 0.0, 1.0, 1.0
+ ]), gl.STATIC_DRAW);
+ }
+
+ function drawScene() {
+ gl.clear(gl.COLOR_BUFFER_BIT);
+
+ gl.bindBuffer(gl.ARRAY_BUFFER, squareVerticesPositionBuffer);
+ gl.vertexAttribPointer(vertexPositionAttribute, 3, gl.FLOAT, false, 0, 0);
+
+ gl.bindBuffer(gl.ARRAY_BUFFER, squareVerticesColorBuffer);
+ gl.vertexAttribPointer(vertexColorAttribute, 4, gl.FLOAT, false, 0, 0);
+
+ gl.useProgram(program);
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
+
+ window.requestAnimationFrame(drawScene);
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/shadereditor/test/head.js b/devtools/client/shadereditor/test/head.js
new file mode 100644
index 000000000..754a0605d
--- /dev/null
+++ b/devtools/client/shadereditor/test/head.js
@@ -0,0 +1,292 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+var { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+var { Task } = require("devtools/shared/task");
+
+var Services = require("Services");
+var promise = require("promise");
+var { gDevTools } = require("devtools/client/framework/devtools");
+var { DebuggerClient } = require("devtools/shared/client/main");
+var { DebuggerServer } = require("devtools/server/main");
+var { WebGLFront } = require("devtools/shared/fronts/webgl");
+var DevToolsUtils = require("devtools/shared/DevToolsUtils");
+var flags = require("devtools/shared/flags");
+var { TargetFactory } = require("devtools/client/framework/target");
+var { Toolbox } = require("devtools/client/framework/toolbox");
+var { isWebGLSupported } = require("devtools/client/shared/webgl-utils");
+var mm = null;
+
+const FRAME_SCRIPT_UTILS_URL = "chrome://devtools/content/shared/frame-script-utils.js";
+const EXAMPLE_URL = "http://example.com/browser/devtools/client/shadereditor/test/";
+const SIMPLE_CANVAS_URL = EXAMPLE_URL + "doc_simple-canvas.html";
+const SHADER_ORDER_URL = EXAMPLE_URL + "doc_shader-order.html";
+const MULTIPLE_CONTEXTS_URL = EXAMPLE_URL + "doc_multiple-contexts.html";
+const OVERLAPPING_GEOMETRY_CANVAS_URL = EXAMPLE_URL + "doc_overlapping-geometry.html";
+const BLENDED_GEOMETRY_CANVAS_URL = EXAMPLE_URL + "doc_blended-geometry.html";
+
+var gEnableLogging = Services.prefs.getBoolPref("devtools.debugger.log");
+// To enable logging for try runs, just set the pref to true.
+Services.prefs.setBoolPref("devtools.debugger.log", false);
+
+// All tests are asynchronous.
+waitForExplicitFinish();
+
+var gToolEnabled = Services.prefs.getBoolPref("devtools.shadereditor.enabled");
+
+flags.testing = true;
+
+registerCleanupFunction(() => {
+ info("finish() was called, cleaning up...");
+ flags.testing = false;
+ Services.prefs.setBoolPref("devtools.debugger.log", gEnableLogging);
+ Services.prefs.setBoolPref("devtools.shadereditor.enabled", gToolEnabled);
+
+ // These tests use a lot of memory due to GL contexts, so force a GC to help
+ // fragmentation.
+ info("Forcing GC after shadereditor test.");
+ Cu.forceGC();
+});
+
+/**
+ * Call manually in tests that use frame script utils after initializing
+ * the shader editor. Must be called after initializing so we can detect
+ * whether or not `content` is a CPOW or not. Call after init but before navigating
+ * to different pages, as bfcache and thus shader caching gets really strange if
+ * frame script attached in the middle of the test.
+ */
+function loadFrameScripts() {
+ if (Cu.isCrossProcessWrapper(content)) {
+ mm = gBrowser.selectedBrowser.messageManager;
+ mm.loadFrameScript(FRAME_SCRIPT_UTILS_URL, false);
+ }
+}
+
+function addTab(aUrl, aWindow) {
+ info("Adding tab: " + aUrl);
+
+ let deferred = promise.defer();
+ let targetWindow = aWindow || window;
+ let targetBrowser = targetWindow.gBrowser;
+
+ targetWindow.focus();
+ let tab = targetBrowser.selectedTab = targetBrowser.addTab(aUrl);
+ let linkedBrowser = tab.linkedBrowser;
+
+ BrowserTestUtils.browserLoaded(linkedBrowser).then(function () {
+ info("Tab added and finished loading: " + aUrl);
+ deferred.resolve(tab);
+ });
+
+ return deferred.promise;
+}
+
+function removeTab(aTab, aWindow) {
+ info("Removing tab.");
+
+ let deferred = promise.defer();
+ let targetWindow = aWindow || window;
+ let targetBrowser = targetWindow.gBrowser;
+ let tabContainer = targetBrowser.tabContainer;
+
+ tabContainer.addEventListener("TabClose", function onClose(aEvent) {
+ tabContainer.removeEventListener("TabClose", onClose, false);
+ info("Tab removed and finished closing.");
+ deferred.resolve();
+ }, false);
+
+ targetBrowser.removeTab(aTab);
+ return deferred.promise;
+}
+
+function handleError(aError) {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ finish();
+}
+
+function ifWebGLSupported() {
+ ok(false, "You need to define a 'ifWebGLSupported' function.");
+ finish();
+}
+
+function ifWebGLUnsupported() {
+ todo(false, "Skipping test because WebGL isn't supported.");
+ finish();
+}
+
+function test() {
+ let generator = isWebGLSupported(document) ? ifWebGLSupported : ifWebGLUnsupported;
+ Task.spawn(generator).then(null, handleError);
+}
+
+function createCanvas() {
+ return document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
+}
+
+function once(aTarget, aEventName, aUseCapture = false) {
+ info("Waiting for event: '" + aEventName + "' on " + aTarget + ".");
+
+ let deferred = promise.defer();
+
+ for (let [add, remove] of [
+ ["on", "off"], // Use event emitter before DOM events for consistency
+ ["addEventListener", "removeEventListener"],
+ ["addListener", "removeListener"]
+ ]) {
+ if ((add in aTarget) && (remove in aTarget)) {
+ aTarget[add](aEventName, function onEvent(...aArgs) {
+ aTarget[remove](aEventName, onEvent, aUseCapture);
+ deferred.resolve(...aArgs);
+ }, aUseCapture);
+ break;
+ }
+ }
+
+ return deferred.promise;
+}
+
+// Hack around `once`, as that only resolves to a single (first) argument
+// and discards the rest. `onceSpread` is similar, except resolves to an
+// array of all of the arguments in the handler. These should be consolidated
+// into the same function, but many tests will need to be changed.
+function onceSpread(aTarget, aEvent) {
+ let deferred = promise.defer();
+ aTarget.once(aEvent, (...args) => deferred.resolve(args));
+ return deferred.promise;
+}
+
+function observe(aNotificationName, aOwnsWeak = false) {
+ info("Waiting for observer notification: '" + aNotificationName + ".");
+
+ let deferred = promise.defer();
+
+ Services.obs.addObserver(function onNotification(...aArgs) {
+ Services.obs.removeObserver(onNotification, aNotificationName);
+ deferred.resolve.apply(deferred, aArgs);
+ }, aNotificationName, aOwnsWeak);
+
+ return deferred.promise;
+}
+
+function isApprox(aFirst, aSecond, aMargin = 1) {
+ return Math.abs(aFirst - aSecond) <= aMargin;
+}
+
+function isApproxColor(aFirst, aSecond, aMargin) {
+ return isApprox(aFirst.r, aSecond.r, aMargin) &&
+ isApprox(aFirst.g, aSecond.g, aMargin) &&
+ isApprox(aFirst.b, aSecond.b, aMargin) &&
+ isApprox(aFirst.a, aSecond.a, aMargin);
+}
+
+function ensurePixelIs(aFront, aPosition, aColor, aWaitFlag = false, aSelector = "canvas") {
+ return Task.spawn(function* () {
+ let pixel = yield aFront.getPixel({ selector: aSelector, position: aPosition });
+ if (isApproxColor(pixel, aColor)) {
+ ok(true, "Expected pixel is shown at: " + aPosition.toSource());
+ return;
+ }
+
+ if (aWaitFlag) {
+ yield aFront.waitForFrame();
+ return ensurePixelIs(aFront, aPosition, aColor, aWaitFlag, aSelector);
+ }
+
+ ok(false, "Expected pixel was not already shown at: " + aPosition.toSource());
+ throw new Error("Expected pixel was not already shown at: " + aPosition.toSource());
+ });
+}
+
+function navigateInHistory(aTarget, aDirection, aWaitForTargetEvent = "navigate") {
+ if (Cu.isCrossProcessWrapper(content)) {
+ if (!mm) {
+ throw new Error("`loadFrameScripts()` must be called before attempting to navigate in e10s.");
+ }
+ mm.sendAsyncMessage("devtools:test:history", { direction: aDirection });
+ }
+ else {
+ executeSoon(() => content.history[aDirection]());
+ }
+ return once(aTarget, aWaitForTargetEvent);
+}
+
+function navigate(aTarget, aUrl, aWaitForTargetEvent = "navigate") {
+ executeSoon(() => aTarget.activeTab.navigateTo(aUrl));
+ return once(aTarget, aWaitForTargetEvent);
+}
+
+function reload(aTarget, aWaitForTargetEvent = "navigate") {
+ executeSoon(() => aTarget.activeTab.reload());
+ return once(aTarget, aWaitForTargetEvent);
+}
+
+function initBackend(aUrl) {
+ info("Initializing a shader editor front.");
+
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+
+ return Task.spawn(function* () {
+ let tab = yield addTab(aUrl);
+ let target = TargetFactory.forTab(tab);
+
+ yield target.makeRemote();
+
+ let front = new WebGLFront(target.client, target.form);
+ return { target, front };
+ });
+}
+
+function initShaderEditor(aUrl) {
+ info("Initializing a shader editor pane.");
+
+ return Task.spawn(function* () {
+ let tab = yield addTab(aUrl);
+ let target = TargetFactory.forTab(tab);
+
+ yield target.makeRemote();
+
+ Services.prefs.setBoolPref("devtools.shadereditor.enabled", true);
+ let toolbox = yield gDevTools.showToolbox(target, "shadereditor");
+ let panel = toolbox.getCurrentPanel();
+ return { target, panel };
+ });
+}
+
+function teardown(aPanel) {
+ info("Destroying the specified shader editor.");
+
+ return promise.all([
+ once(aPanel, "destroyed"),
+ removeTab(aPanel.target.tab)
+ ]);
+}
+
+// Due to `program-linked` events firing synchronously, we cannot
+// just yield/chain them together, as then we miss all actors after the
+// first event since they're fired consecutively. This allows us to capture
+// all actors and returns an array containing them.
+//
+// Takes a `front` object that is an event emitter, the number of
+// programs that should be listened to and waited on, and an optional
+// `onAdd` function that calls with the entire actors array on program link
+function getPrograms(front, count, onAdd) {
+ let actors = [];
+ let deferred = promise.defer();
+ front.on("program-linked", function onLink(actor) {
+ if (actors.length !== count) {
+ actors.push(actor);
+ if (typeof onAdd === "function") onAdd(actors);
+ }
+ if (actors.length === count) {
+ front.off("program-linked", onLink);
+ deferred.resolve(actors);
+ }
+ });
+ return deferred.promise;
+}
diff --git a/devtools/client/shared/AppCacheUtils.jsm b/devtools/client/shared/AppCacheUtils.jsm
new file mode 100644
index 000000000..a2beca993
--- /dev/null
+++ b/devtools/client/shared/AppCacheUtils.jsm
@@ -0,0 +1,631 @@
+/* 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/. */
+
+/**
+ * validateManifest() warns of the following errors:
+ * - No manifest specified in page
+ * - Manifest is not utf-8
+ * - Manifest mimetype not text/cache-manifest
+ * - Manifest does not begin with "CACHE MANIFEST"
+ * - Page modified since appcache last changed
+ * - Duplicate entries
+ * - Conflicting entries e.g. in both CACHE and NETWORK sections or in cache
+ * but blocked by FALLBACK namespace
+ * - Detect referenced files that are not available
+ * - Detect referenced files that have cache-control set to no-store
+ * - Wildcards used in a section other than NETWORK
+ * - Spaces in URI not replaced with %20
+ * - Completely invalid URIs
+ * - Too many dot dot slash operators
+ * - SETTINGS section is valid
+ * - Invalid section name
+ * - etc.
+ */
+
+"use strict";
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+var { XPCOMUtils } = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {});
+var { NetUtil } = Cu.import("resource://gre/modules/NetUtil.jsm", {});
+var { LoadContextInfo } = Cu.import("resource://gre/modules/LoadContextInfo.jsm", {});
+var { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+
+var { gDevTools } = require("devtools/client/framework/devtools");
+var Services = require("Services");
+var promise = require("promise");
+var defer = require("devtools/shared/defer");
+
+this.EXPORTED_SYMBOLS = ["AppCacheUtils"];
+
+function AppCacheUtils(documentOrUri) {
+ this._parseManifest = this._parseManifest.bind(this);
+
+ if (documentOrUri) {
+ if (typeof documentOrUri == "string") {
+ this.uri = documentOrUri;
+ }
+ if (/HTMLDocument/.test(documentOrUri.toString())) {
+ this.doc = documentOrUri;
+ }
+ }
+}
+
+AppCacheUtils.prototype = {
+ get cachePath() {
+ return "";
+ },
+
+ validateManifest: function ACU_validateManifest() {
+ let deferred = defer();
+ this.errors = [];
+ // Check for missing manifest.
+ this._getManifestURI().then(manifestURI => {
+ this.manifestURI = manifestURI;
+
+ if (!this.manifestURI) {
+ this._addError(0, "noManifest");
+ deferred.resolve(this.errors);
+ }
+
+ this._getURIInfo(this.manifestURI).then(uriInfo => {
+ this._parseManifest(uriInfo).then(() => {
+ // Sort errors by line number.
+ this.errors.sort(function (a, b) {
+ return a.line - b.line;
+ });
+ deferred.resolve(this.errors);
+ });
+ });
+ });
+
+ return deferred.promise;
+ },
+
+ _parseManifest: function ACU__parseManifest(uriInfo) {
+ let deferred = defer();
+ let manifestName = uriInfo.name;
+ let manifestLastModified = new Date(uriInfo.responseHeaders["Last-Modified"]);
+
+ if (uriInfo.charset.toLowerCase() != "utf-8") {
+ this._addError(0, "notUTF8", uriInfo.charset);
+ }
+
+ if (uriInfo.mimeType != "text/cache-manifest") {
+ this._addError(0, "badMimeType", uriInfo.mimeType);
+ }
+
+ let parser = new ManifestParser(uriInfo.text, this.manifestURI);
+ let parsed = parser.parse();
+
+ if (parsed.errors.length > 0) {
+ this.errors.push.apply(this.errors, parsed.errors);
+ }
+
+ // Check for duplicate entries.
+ let dupes = {};
+ for (let parsedUri of parsed.uris) {
+ dupes[parsedUri.uri] = dupes[parsedUri.uri] || [];
+ dupes[parsedUri.uri].push({
+ line: parsedUri.line,
+ section: parsedUri.section,
+ original: parsedUri.original
+ });
+ }
+ for (let [uri, value] of Object.entries(dupes)) {
+ if (value.length > 1) {
+ this._addError(0, "duplicateURI", uri, JSON.stringify(value));
+ }
+ }
+
+ // Loop through network entries making sure that fallback and cache don't
+ // contain uris starting with the network uri.
+ for (let neturi of parsed.uris) {
+ if (neturi.section == "NETWORK") {
+ for (let parsedUri of parsed.uris) {
+ if (parsedUri.section !== "NETWORK" &&
+ parsedUri.uri.startsWith(neturi.uri)) {
+ this._addError(neturi.line, "networkBlocksURI", neturi.line,
+ neturi.original, parsedUri.line, parsedUri.original,
+ parsedUri.section);
+ }
+ }
+ }
+ }
+
+ // Loop through fallback entries making sure that fallback and cache don't
+ // contain uris starting with the network uri.
+ for (let fb of parsed.fallbacks) {
+ for (let parsedUri of parsed.uris) {
+ if (parsedUri.uri.startsWith(fb.namespace)) {
+ this._addError(fb.line, "fallbackBlocksURI", fb.line,
+ fb.original, parsedUri.line, parsedUri.original,
+ parsedUri.section);
+ }
+ }
+ }
+
+ // Check that all resources exist and that their cach-control headers are
+ // not set to no-store.
+ let current = -1;
+ for (let i = 0, len = parsed.uris.length; i < len; i++) {
+ let parsedUri = parsed.uris[i];
+ this._getURIInfo(parsedUri.uri).then(uriInfo => {
+ current++;
+
+ if (uriInfo.success) {
+ // Check that the resource was not modified after the manifest was last
+ // modified. If it was then the manifest file should be refreshed.
+ let resourceLastModified =
+ new Date(uriInfo.responseHeaders["Last-Modified"]);
+
+ if (manifestLastModified < resourceLastModified) {
+ this._addError(parsedUri.line, "fileChangedButNotManifest",
+ uriInfo.name, manifestName, parsedUri.line);
+ }
+
+ // If cache-control: no-store the file will not be added to the
+ // appCache.
+ if (uriInfo.nocache) {
+ this._addError(parsedUri.line, "cacheControlNoStore",
+ parsedUri.original, parsedUri.line);
+ }
+ } else if (parsedUri.original !== "*") {
+ this._addError(parsedUri.line, "notAvailable",
+ parsedUri.original, parsedUri.line);
+ }
+
+ if (current == len - 1) {
+ deferred.resolve();
+ }
+ });
+ }
+
+ return deferred.promise;
+ },
+
+ _getURIInfo: function ACU__getURIInfo(uri) {
+ let inputStream = Cc["@mozilla.org/scriptableinputstream;1"]
+ .createInstance(Ci.nsIScriptableInputStream);
+ let deferred = defer();
+ let buffer = "";
+ var channel = NetUtil.newChannel({
+ uri: uri,
+ loadUsingSystemPrincipal: true,
+ securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL
+ });
+
+ // Avoid the cache:
+ channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
+ channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
+
+ channel.asyncOpen2({
+ onStartRequest: function (request, context) {
+ // This empty method is needed in order for onDataAvailable to be
+ // called.
+ },
+
+ onDataAvailable: function (request, context, stream, offset, count) {
+ request.QueryInterface(Ci.nsIHttpChannel);
+ inputStream.init(stream);
+ buffer = buffer.concat(inputStream.read(count));
+ },
+
+ onStopRequest: function onStartRequest(request, context, statusCode) {
+ if (statusCode === 0) {
+ request.QueryInterface(Ci.nsIHttpChannel);
+
+ let result = {
+ name: request.name,
+ success: request.requestSucceeded,
+ status: request.responseStatus + " - " + request.responseStatusText,
+ charset: request.contentCharset || "utf-8",
+ mimeType: request.contentType,
+ contentLength: request.contentLength,
+ nocache: request.isNoCacheResponse() || request.isNoStoreResponse(),
+ prePath: request.URI.prePath + "/",
+ text: buffer
+ };
+
+ result.requestHeaders = {};
+ request.visitRequestHeaders(function (header, value) {
+ result.requestHeaders[header] = value;
+ });
+
+ result.responseHeaders = {};
+ request.visitResponseHeaders(function (header, value) {
+ result.responseHeaders[header] = value;
+ });
+
+ deferred.resolve(result);
+ } else {
+ deferred.resolve({
+ name: request.name,
+ success: false
+ });
+ }
+ }
+ });
+ return deferred.promise;
+ },
+
+ listEntries: function ACU_show(searchTerm) {
+ if (!Services.prefs.getBoolPref("browser.cache.disk.enable")) {
+ throw new Error(l10n.GetStringFromName("cacheDisabled"));
+ }
+
+ let entries = [];
+
+ let appCacheStorage = Services.cache2.appCacheStorage(LoadContextInfo.default, null);
+ appCacheStorage.asyncVisitStorage({
+ onCacheStorageInfo: function () {},
+
+ onCacheEntryInfo: function (aURI, aIdEnhance, aDataSize, aFetchCount, aLastModifiedTime, aExpirationTime) {
+ let lowerKey = aURI.asciiSpec.toLowerCase();
+
+ if (searchTerm && lowerKey.indexOf(searchTerm.toLowerCase()) == -1) {
+ return;
+ }
+
+ if (aIdEnhance) {
+ aIdEnhance += ":";
+ }
+
+ let entry = {
+ "deviceID": "offline",
+ "key": aIdEnhance + aURI.asciiSpec,
+ "fetchCount": aFetchCount,
+ "lastFetched": null,
+ "lastModified": new Date(aLastModifiedTime * 1000),
+ "expirationTime": new Date(aExpirationTime * 1000),
+ "dataSize": aDataSize
+ };
+
+ entries.push(entry);
+ return true;
+ }
+ }, true);
+
+ if (entries.length === 0) {
+ throw new Error(l10n.GetStringFromName("noResults"));
+ }
+ return entries;
+ },
+
+ viewEntry: function ACU_viewEntry(key) {
+ let wm = Cc["@mozilla.org/appshell/window-mediator;1"]
+ .getService(Ci.nsIWindowMediator);
+ let win = wm.getMostRecentWindow(gDevTools.chromeWindowType);
+ let url = "about:cache-entry?storage=appcache&context=&eid=&uri=" + key;
+ win.openUILinkIn(url, "tab");
+ },
+
+ clearAll: function ACU_clearAll() {
+ if (!Services.prefs.getBoolPref("browser.cache.disk.enable")) {
+ throw new Error(l10n.GetStringFromName("cacheDisabled"));
+ }
+
+ let appCacheStorage = Services.cache2.appCacheStorage(LoadContextInfo.default, null);
+ appCacheStorage.asyncEvictStorage({
+ onCacheEntryDoomed: function (result) {}
+ });
+ },
+
+ _getManifestURI: function ACU__getManifestURI() {
+ let deferred = defer();
+
+ let getURI = () => {
+ let htmlNode = this.doc.querySelector("html[manifest]");
+ if (htmlNode) {
+ let pageUri = this.doc.location ? this.doc.location.href : this.uri;
+ let origin = pageUri.substr(0, pageUri.lastIndexOf("/") + 1);
+ let manifestURI = htmlNode.getAttribute("manifest");
+
+ if (manifestURI.startsWith("/")) {
+ manifestURI = manifestURI.substr(1);
+ }
+
+ return origin + manifestURI;
+ }
+ };
+
+ if (this.doc) {
+ let uri = getURI();
+ return promise.resolve(uri);
+ } else {
+ this._getURIInfo(this.uri).then(uriInfo => {
+ if (uriInfo.success) {
+ let html = uriInfo.text;
+ let parser = _DOMParser;
+ this.doc = parser.parseFromString(html, "text/html");
+ let uri = getURI();
+ deferred.resolve(uri);
+ } else {
+ this.errors.push({
+ line: 0,
+ msg: l10n.GetStringFromName("invalidURI")
+ });
+ }
+ });
+ }
+ return deferred.promise;
+ },
+
+ _addError: function ACU__addError(line, l10nString, ...params) {
+ let msg;
+
+ if (params) {
+ msg = l10n.formatStringFromName(l10nString, params, params.length);
+ } else {
+ msg = l10n.GetStringFromName(l10nString);
+ }
+
+ this.errors.push({
+ line: line,
+ msg: msg
+ });
+ },
+};
+
+/**
+ * We use our own custom parser because we need far more detailed information
+ * than the system manifest parser provides.
+ *
+ * @param {String} manifestText
+ * The text content of the manifest file.
+ * @param {String} manifestURI
+ * The URI of the manifest file. This is used in calculating the path of
+ * relative URIs.
+ */
+function ManifestParser(manifestText, manifestURI) {
+ this.manifestText = manifestText;
+ this.origin = manifestURI.substr(0, manifestURI.lastIndexOf("/") + 1)
+ .replace(" ", "%20");
+}
+
+ManifestParser.prototype = {
+ parse: function OCIMP_parse() {
+ let lines = this.manifestText.split(/\r?\n/);
+ let fallbacks = this.fallbacks = [];
+ let settings = this.settings = [];
+ let errors = this.errors = [];
+ let uris = this.uris = [];
+
+ this.currSection = "CACHE";
+
+ for (let i = 0; i < lines.length; i++) {
+ let text = this.text = lines[i].trim();
+ this.currentLine = i + 1;
+
+ if (i === 0 && text !== "CACHE MANIFEST") {
+ this._addError(1, "firstLineMustBeCacheManifest", 1);
+ }
+
+ // Ignore comments
+ if (/^#/.test(text) || !text.length) {
+ continue;
+ }
+
+ if (text == "CACHE MANIFEST") {
+ if (this.currentLine != 1) {
+ this._addError(this.currentLine, "cacheManifestOnlyFirstLine2",
+ this.currentLine);
+ }
+ continue;
+ }
+
+ if (this._maybeUpdateSectionName()) {
+ continue;
+ }
+
+ switch (this.currSection) {
+ case "CACHE":
+ case "NETWORK":
+ this.parseLine();
+ break;
+ case "FALLBACK":
+ this.parseFallbackLine();
+ break;
+ case "SETTINGS":
+ this.parseSettingsLine();
+ break;
+ }
+ }
+
+ return {
+ uris: uris,
+ fallbacks: fallbacks,
+ settings: settings,
+ errors: errors
+ };
+ },
+
+ parseLine: function OCIMP_parseLine() {
+ let text = this.text;
+
+ if (text.indexOf("*") != -1) {
+ if (this.currSection != "NETWORK" || text.length != 1) {
+ this._addError(this.currentLine, "asteriskInWrongSection2",
+ this.currSection, this.currentLine);
+ return;
+ }
+ }
+
+ if (/\s/.test(text)) {
+ this._addError(this.currentLine, "escapeSpaces", this.currentLine);
+ text = text.replace(/\s/g, "%20");
+ }
+
+ if (text[0] == "/") {
+ if (text.substr(0, 4) == "/../") {
+ this._addError(this.currentLine, "slashDotDotSlashBad", this.currentLine);
+ } else {
+ this.uris.push(this._wrapURI(this.origin + text.substring(1)));
+ }
+ } else if (text.substr(0, 2) == "./") {
+ this.uris.push(this._wrapURI(this.origin + text.substring(2)));
+ } else if (text.substr(0, 4) == "http") {
+ this.uris.push(this._wrapURI(text));
+ } else {
+ let origin = this.origin;
+ let path = text;
+
+ while (path.substr(0, 3) == "../" && /^https?:\/\/.*?\/.*?\//.test(origin)) {
+ let trimIdx = origin.substr(0, origin.length - 1).lastIndexOf("/") + 1;
+ origin = origin.substr(0, trimIdx);
+ path = path.substr(3);
+ }
+
+ if (path.substr(0, 3) == "../") {
+ this._addError(this.currentLine, "tooManyDotDotSlashes", this.currentLine);
+ return;
+ }
+
+ if (/^https?:\/\//.test(path)) {
+ this.uris.push(this._wrapURI(path));
+ return;
+ }
+ this.uris.push(this._wrapURI(origin + path));
+ }
+ },
+
+ parseFallbackLine: function OCIMP_parseFallbackLine() {
+ let split = this.text.split(/\s+/);
+ let origURI = this.text;
+
+ if (split.length != 2) {
+ this._addError(this.currentLine, "fallbackUseSpaces", this.currentLine);
+ return;
+ }
+
+ let [ namespace, fallback ] = split;
+
+ if (namespace.indexOf("*") != -1) {
+ this._addError(this.currentLine, "fallbackAsterisk2", this.currentLine);
+ }
+
+ if (/\s/.test(namespace)) {
+ this._addError(this.currentLine, "escapeSpaces", this.currentLine);
+ namespace = namespace.replace(/\s/g, "%20");
+ }
+
+ if (namespace.substr(0, 4) == "/../") {
+ this._addError(this.currentLine, "slashDotDotSlashBad", this.currentLine);
+ }
+
+ if (namespace.substr(0, 2) == "./") {
+ namespace = this.origin + namespace.substring(2);
+ }
+
+ if (namespace.substr(0, 4) != "http") {
+ let origin = this.origin;
+ let path = namespace;
+
+ while (path.substr(0, 3) == "../" && /^https?:\/\/.*?\/.*?\//.test(origin)) {
+ let trimIdx = origin.substr(0, origin.length - 1).lastIndexOf("/") + 1;
+ origin = origin.substr(0, trimIdx);
+ path = path.substr(3);
+ }
+
+ if (path.substr(0, 3) == "../") {
+ this._addError(this.currentLine, "tooManyDotDotSlashes", this.currentLine);
+ }
+
+ if (/^https?:\/\//.test(path)) {
+ namespace = path;
+ } else {
+ if (path[0] == "/") {
+ path = path.substring(1);
+ }
+ namespace = origin + path;
+ }
+ }
+
+ this.text = fallback;
+ this.parseLine();
+
+ this.fallbacks.push({
+ line: this.currentLine,
+ original: origURI,
+ namespace: namespace,
+ fallback: fallback
+ });
+ },
+
+ parseSettingsLine: function OCIMP_parseSettingsLine() {
+ let text = this.text;
+
+ if (this.settings.length == 1 || !/prefer-online|fast/.test(text)) {
+ this._addError(this.currentLine, "settingsBadValue", this.currentLine);
+ return;
+ }
+
+ switch (text) {
+ case "prefer-online":
+ this.settings.push(this._wrapURI(text));
+ break;
+ case "fast":
+ this.settings.push(this._wrapURI(text));
+ break;
+ }
+ },
+
+ _wrapURI: function OCIMP__wrapURI(uri) {
+ return {
+ section: this.currSection,
+ line: this.currentLine,
+ uri: uri,
+ original: this.text
+ };
+ },
+
+ _addError: function OCIMP__addError(line, l10nString, ...params) {
+ let msg;
+
+ if (params) {
+ msg = l10n.formatStringFromName(l10nString, params, params.length);
+ } else {
+ msg = l10n.GetStringFromName(l10nString);
+ }
+
+ this.errors.push({
+ line: line,
+ msg: msg
+ });
+ },
+
+ _maybeUpdateSectionName: function OCIMP__maybeUpdateSectionName() {
+ let text = this.text;
+
+ if (text == text.toUpperCase() && text.charAt(text.length - 1) == ":") {
+ text = text.substr(0, text.length - 1);
+
+ switch (text) {
+ case "CACHE":
+ case "NETWORK":
+ case "FALLBACK":
+ case "SETTINGS":
+ this.currSection = text;
+ return true;
+ default:
+ this._addError(this.currentLine,
+ "invalidSectionName", text, this.currentLine);
+ return false;
+ }
+ }
+ },
+};
+
+XPCOMUtils.defineLazyGetter(this, "l10n", () => Services.strings
+ .createBundle("chrome://devtools/locale/appcacheutils.properties"));
+
+XPCOMUtils.defineLazyGetter(this, "appcacheservice", function () {
+ return Cc["@mozilla.org/network/application-cache-service;1"]
+ .getService(Ci.nsIApplicationCacheService);
+
+});
+
+XPCOMUtils.defineLazyGetter(this, "_DOMParser", function () {
+ return Cc["@mozilla.org/xmlextras/domparser;1"].createInstance(Ci.nsIDOMParser);
+});
diff --git a/devtools/client/shared/DOMHelpers.jsm b/devtools/client/shared/DOMHelpers.jsm
new file mode 100644
index 000000000..9c861006e
--- /dev/null
+++ b/devtools/client/shared/DOMHelpers.jsm
@@ -0,0 +1,166 @@
+/* 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 = Components.interfaces;
+const Cu = Components.utils;
+const { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
+const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const nodeFilterConstants = require("devtools/shared/dom-node-filter-constants");
+
+this.EXPORTED_SYMBOLS = ["DOMHelpers"];
+
+/**
+ * DOMHelpers
+ * Makes DOM traversal easier. Goes through iframes.
+ *
+ * @constructor
+ * @param nsIDOMWindow aWindow
+ * The content window, owning the document to traverse.
+ */
+this.DOMHelpers = function DOMHelpers(aWindow) {
+ if (!aWindow) {
+ throw new Error("window can't be null or undefined");
+ }
+ this.window = aWindow;
+};
+
+DOMHelpers.prototype = {
+ getParentObject: function Helpers_getParentObject(node)
+ {
+ let parentNode = node ? node.parentNode : null;
+
+ if (!parentNode) {
+ // Documents have no parentNode; Attr, Document, DocumentFragment, Entity,
+ // and Notation. top level windows have no parentNode
+ if (node && node == this.window.Node.DOCUMENT_NODE) {
+ // document type
+ if (node.defaultView) {
+ let embeddingFrame = node.defaultView.frameElement;
+ if (embeddingFrame)
+ return embeddingFrame.parentNode;
+ }
+ }
+ // a Document object without a parentNode or window
+ return null; // top level has no parent
+ }
+
+ if (parentNode.nodeType == this.window.Node.DOCUMENT_NODE) {
+ if (parentNode.defaultView) {
+ return parentNode.defaultView.frameElement;
+ }
+ // parent is document element, but no window at defaultView.
+ return null;
+ }
+
+ if (!parentNode.localName)
+ return null;
+
+ return parentNode;
+ },
+
+ getChildObject: function Helpers_getChildObject(node, index, previousSibling,
+ showTextNodesWithWhitespace)
+ {
+ if (!node)
+ return null;
+
+ if (node.contentDocument) {
+ // then the node is a frame
+ if (index == 0) {
+ return node.contentDocument.documentElement; // the node's HTMLElement
+ }
+ return null;
+ }
+
+ if (node.getSVGDocument) {
+ let svgDocument = node.getSVGDocument();
+ if (svgDocument) {
+ // then the node is a frame
+ if (index == 0) {
+ return svgDocument.documentElement; // the node's SVGElement
+ }
+ return null;
+ }
+ }
+
+ let child = null;
+ if (previousSibling) // then we are walking
+ child = this.getNextSibling(previousSibling);
+ else
+ child = this.getFirstChild(node);
+
+ if (showTextNodesWithWhitespace)
+ return child;
+
+ for (; child; child = this.getNextSibling(child)) {
+ if (!this.isWhitespaceText(child))
+ return child;
+ }
+
+ return null; // we have no children worth showing.
+ },
+
+ getFirstChild: function Helpers_getFirstChild(node)
+ {
+ let SHOW_ALL = nodeFilterConstants.SHOW_ALL;
+ this.treeWalker = node.ownerDocument.createTreeWalker(node,
+ SHOW_ALL, null);
+ return this.treeWalker.firstChild();
+ },
+
+ getNextSibling: function Helpers_getNextSibling(node)
+ {
+ let next = this.treeWalker.nextSibling();
+
+ if (!next)
+ delete this.treeWalker;
+
+ return next;
+ },
+
+ isWhitespaceText: function Helpers_isWhitespaceText(node)
+ {
+ return node.nodeType == this.window.Node.TEXT_NODE &&
+ !/[^\s]/.exec(node.nodeValue);
+ },
+
+ destroy: function Helpers_destroy()
+ {
+ delete this.window;
+ delete this.treeWalker;
+ },
+
+ /**
+ * A simple way to be notified (once) when a window becomes
+ * interactive (DOMContentLoaded).
+ *
+ * It is based on the chromeEventHandler. This is useful when
+ * chrome iframes are loaded in content docshells (in Firefox
+ * tabs for example).
+ */
+ onceDOMReady: function Helpers_onLocationChange(callback, targetURL) {
+ let window = this.window;
+ let docShell = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShell);
+ let onReady = function (event) {
+ if (event.target == window.document) {
+ docShell.chromeEventHandler.removeEventListener("DOMContentLoaded", onReady, false);
+ // If in `callback` the URL of the window is changed and a listener to DOMContentLoaded
+ // is attached, the event we just received will be also be caught by the new listener.
+ // We want to avoid that so we execute the callback in the next queue.
+ Services.tm.mainThread.dispatch(callback, 0);
+ }
+ };
+ if ((window.document.readyState == "complete" ||
+ window.document.readyState == "interactive") &&
+ window.location.href == targetURL) {
+ Services.tm.mainThread.dispatch(callback, 0);
+ } else {
+ docShell.chromeEventHandler.addEventListener("DOMContentLoaded", onReady, false);
+ }
+ }
+};
diff --git a/devtools/client/shared/Jsbeautify.jsm b/devtools/client/shared/Jsbeautify.jsm
new file mode 100644
index 000000000..2293afc63
--- /dev/null
+++ b/devtools/client/shared/Jsbeautify.jsm
@@ -0,0 +1,16 @@
+/* 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";
+
+/*
+ * JS Beautifier. Please use require("devtools/shared/jsbeautify/beautify") instead of
+ * this JSM.
+ */
+
+this.EXPORTED_SYMBOLS = [ "jsBeautify" ];
+
+const { require } = Components.utils.import("resource://devtools/shared/Loader.jsm", {});
+const { beautify } = require("devtools/shared/jsbeautify/beautify");
+const jsBeautify = beautify.js;
diff --git a/devtools/client/shared/SplitView.jsm b/devtools/client/shared/SplitView.jsm
new file mode 100644
index 000000000..f72aad2ac
--- /dev/null
+++ b/devtools/client/shared/SplitView.jsm
@@ -0,0 +1,312 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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 Cu = Components.utils;
+const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const {KeyCodes} = require("devtools/client/shared/keycodes");
+
+this.EXPORTED_SYMBOLS = ["SplitView"];
+
+/* this must be kept in sync with CSS (ie. splitview.css) */
+const LANDSCAPE_MEDIA_QUERY = "(min-width: 701px)";
+
+var bindings = new WeakMap();
+
+/**
+ * SplitView constructor
+ *
+ * Initialize the split view UI on an existing DOM element.
+ *
+ * A split view contains items, each of those having one summary and one details
+ * elements.
+ * It is adaptive as it behaves similarly to a richlistbox when there the aspect
+ * ratio is narrow or as a pair listbox-box otherwise.
+ *
+ * @param DOMElement aRoot
+ * @see appendItem
+ */
+this.SplitView = function SplitView(aRoot)
+{
+ this._root = aRoot;
+ this._controller = aRoot.querySelector(".splitview-controller");
+ this._nav = aRoot.querySelector(".splitview-nav");
+ this._side = aRoot.querySelector(".splitview-side-details");
+ this._activeSummary = null;
+
+ this._mql = aRoot.ownerDocument.defaultView.matchMedia(LANDSCAPE_MEDIA_QUERY);
+
+ // items list focus and search-on-type handling
+ this._nav.addEventListener("keydown", (aEvent) => {
+ function getFocusedItemWithin(nav) {
+ let node = nav.ownerDocument.activeElement;
+ while (node && node.parentNode != nav) {
+ node = node.parentNode;
+ }
+ return node;
+ }
+
+ // do not steal focus from inside iframes or textboxes
+ if (aEvent.target.ownerDocument != this._nav.ownerDocument ||
+ aEvent.target.tagName == "input" ||
+ aEvent.target.tagName == "textbox" ||
+ aEvent.target.tagName == "textarea" ||
+ aEvent.target.classList.contains("textbox")) {
+ return false;
+ }
+
+ // handle keyboard navigation within the items list
+ let newFocusOrdinal;
+ if (aEvent.keyCode == KeyCodes.DOM_VK_PAGE_UP ||
+ aEvent.keyCode == KeyCodes.DOM_VK_HOME) {
+ newFocusOrdinal = 0;
+ } else if (aEvent.keyCode == KeyCodes.DOM_VK_PAGE_DOWN ||
+ aEvent.keyCode == KeyCodes.DOM_VK_END) {
+ newFocusOrdinal = this._nav.childNodes.length - 1;
+ } else if (aEvent.keyCode == KeyCodes.DOM_VK_UP) {
+ newFocusOrdinal = getFocusedItemWithin(this._nav).getAttribute("data-ordinal");
+ newFocusOrdinal--;
+ } else if (aEvent.keyCode == KeyCodes.DOM_VK_DOWN) {
+ newFocusOrdinal = getFocusedItemWithin(this._nav).getAttribute("data-ordinal");
+ newFocusOrdinal++;
+ }
+ if (newFocusOrdinal !== undefined) {
+ aEvent.stopPropagation();
+ let el = this.getSummaryElementByOrdinal(newFocusOrdinal);
+ if (el) {
+ el.focus();
+ }
+ return false;
+ }
+ }, false);
+};
+
+SplitView.prototype = {
+ /**
+ * Retrieve whether the UI currently has a landscape orientation.
+ *
+ * @return boolean
+ */
+ get isLandscape()
+ {
+ return this._mql.matches;
+ },
+
+ /**
+ * Retrieve the root element.
+ *
+ * @return DOMElement
+ */
+ get rootElement()
+ {
+ return this._root;
+ },
+
+ /**
+ * Retrieve the active item's summary element or null if there is none.
+ *
+ * @return DOMElement
+ */
+ get activeSummary()
+ {
+ return this._activeSummary;
+ },
+
+ /**
+ * Set the active item's summary element.
+ *
+ * @param DOMElement aSummary
+ */
+ set activeSummary(aSummary)
+ {
+ if (aSummary == this._activeSummary) {
+ return;
+ }
+
+ if (this._activeSummary) {
+ let binding = bindings.get(this._activeSummary);
+
+ if (binding.onHide) {
+ binding.onHide(this._activeSummary, binding._details, binding.data);
+ }
+
+ this._activeSummary.classList.remove("splitview-active");
+ binding._details.classList.remove("splitview-active");
+ }
+
+ if (!aSummary) {
+ return;
+ }
+
+ let binding = bindings.get(aSummary);
+ aSummary.classList.add("splitview-active");
+ binding._details.classList.add("splitview-active");
+
+ this._activeSummary = aSummary;
+
+ if (binding.onShow) {
+ binding.onShow(aSummary, binding._details, binding.data);
+ }
+ },
+
+ /**
+ * Retrieve the active item's details element or null if there is none.
+ * @return DOMElement
+ */
+ get activeDetails()
+ {
+ let summary = this.activeSummary;
+ return summary ? bindings.get(summary)._details : null;
+ },
+
+ /**
+ * Retrieve the summary element for a given ordinal.
+ *
+ * @param number aOrdinal
+ * @return DOMElement
+ * Summary element with given ordinal or null if not found.
+ * @see appendItem
+ */
+ getSummaryElementByOrdinal: function SEC_getSummaryElementByOrdinal(aOrdinal)
+ {
+ return this._nav.querySelector("* > li[data-ordinal='" + aOrdinal + "']");
+ },
+
+ /**
+ * Append an item to the split view.
+ *
+ * @param DOMElement aSummary
+ * The summary element for the item.
+ * @param DOMElement aDetails
+ * The details element for the item.
+ * @param object aOptions
+ * Optional object that defines custom behavior and data for the item.
+ * All properties are optional :
+ * - function(DOMElement summary, DOMElement details, object data) onCreate
+ * Called when the item has been added.
+ * - function(summary, details, data) onShow
+ * Called when the item is shown/active.
+ * - function(summary, details, data) onHide
+ * Called when the item is hidden/inactive.
+ * - function(summary, details, data) onDestroy
+ * Called when the item has been removed.
+ * - object data
+ * Object to pass to the callbacks above.
+ * - number ordinal
+ * Items with a lower ordinal are displayed before those with a
+ * higher ordinal.
+ */
+ appendItem: function ASV_appendItem(aSummary, aDetails, aOptions)
+ {
+ let binding = aOptions || {};
+
+ binding._summary = aSummary;
+ binding._details = aDetails;
+ bindings.set(aSummary, binding);
+
+ this._nav.appendChild(aSummary);
+
+ aSummary.addEventListener("click", (aEvent) => {
+ aEvent.stopPropagation();
+ this.activeSummary = aSummary;
+ }, false);
+
+ this._side.appendChild(aDetails);
+
+ if (binding.onCreate) {
+ binding.onCreate(aSummary, aDetails, binding.data);
+ }
+ },
+
+ /**
+ * Append an item to the split view according to two template elements
+ * (one for the item's summary and the other for the item's details).
+ *
+ * @param string aName
+ * Name of the template elements to instantiate.
+ * Requires two (hidden) DOM elements with id "splitview-tpl-summary-"
+ * and "splitview-tpl-details-" suffixed with aName.
+ * @param object aOptions
+ * Optional object that defines custom behavior and data for the item.
+ * See appendItem for full description.
+ * @return object{summary:,details:}
+ * Object with the new DOM elements created for summary and details.
+ * @see appendItem
+ */
+ appendTemplatedItem: function ASV_appendTemplatedItem(aName, aOptions)
+ {
+ aOptions = aOptions || {};
+ let summary = this._root.querySelector("#splitview-tpl-summary-" + aName);
+ let details = this._root.querySelector("#splitview-tpl-details-" + aName);
+
+ summary = summary.cloneNode(true);
+ summary.id = "";
+ if (aOptions.ordinal !== undefined) { // can be zero
+ summary.style.MozBoxOrdinalGroup = aOptions.ordinal;
+ summary.setAttribute("data-ordinal", aOptions.ordinal);
+ }
+ details = details.cloneNode(true);
+ details.id = "";
+
+ this.appendItem(summary, details, aOptions);
+ return {summary: summary, details: details};
+ },
+
+ /**
+ * Remove an item from the split view.
+ *
+ * @param DOMElement aSummary
+ * Summary element of the item to remove.
+ */
+ removeItem: function ASV_removeItem(aSummary)
+ {
+ if (aSummary == this._activeSummary) {
+ this.activeSummary = null;
+ }
+
+ let binding = bindings.get(aSummary);
+ aSummary.parentNode.removeChild(aSummary);
+ binding._details.parentNode.removeChild(binding._details);
+
+ if (binding.onDestroy) {
+ binding.onDestroy(aSummary, binding._details, binding.data);
+ }
+ },
+
+ /**
+ * Remove all items from the split view.
+ */
+ removeAll: function ASV_removeAll()
+ {
+ while (this._nav.hasChildNodes()) {
+ this.removeItem(this._nav.firstChild);
+ }
+ },
+
+ /**
+ * Set the item's CSS class name.
+ * This sets the class on both the summary and details elements, retaining
+ * any SplitView-specific classes.
+ *
+ * @param DOMElement aSummary
+ * Summary element of the item to set.
+ * @param string aClassName
+ * One or more space-separated CSS classes.
+ */
+ setItemClassName: function ASV_setItemClassName(aSummary, aClassName)
+ {
+ let binding = bindings.get(aSummary);
+ let viewSpecific;
+
+ viewSpecific = aSummary.className.match(/(splitview\-[\w-]+)/g);
+ viewSpecific = viewSpecific ? viewSpecific.join(" ") : "";
+ aSummary.className = viewSpecific + " " + aClassName;
+
+ viewSpecific = binding._details.className.match(/(splitview\-[\w-]+)/g);
+ viewSpecific = viewSpecific ? viewSpecific.join(" ") : "";
+ binding._details.className = viewSpecific + " " + aClassName;
+ },
+};
diff --git a/devtools/client/shared/autocomplete-popup.js b/devtools/client/shared/autocomplete-popup.js
new file mode 100644
index 000000000..1d24c948e
--- /dev/null
+++ b/devtools/client/shared/autocomplete-popup.js
@@ -0,0 +1,599 @@
+/* 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 HTML_NS = "http://www.w3.org/1999/xhtml";
+const Services = require("Services");
+const {gDevTools} = require("devtools/client/framework/devtools");
+const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+const EventEmitter = require("devtools/shared/event-emitter");
+
+let itemIdCounter = 0;
+/**
+ * Autocomplete popup UI implementation.
+ *
+ * @constructor
+ * @param {Document} toolboxDoc
+ * The toolbox document to attach the autocomplete popup panel.
+ * @param {Object} options
+ * An object consiting any of the following options:
+ * - listId {String} The id for the list <LI> element.
+ * - position {String} The position for the tooltip ("top" or "bottom").
+ * - theme {String} String related to the theme of the popup
+ * - autoSelect {Boolean} Boolean to allow the first entry of the popup
+ * panel to be automatically selected when the popup shows.
+ * - onSelect {String} Callback called when the selected index is updated.
+ * - onClick {String} Callback called when the autocomplete popup receives a click
+ * event. The selectedIndex will already be updated if need be.
+ */
+function AutocompletePopup(toolboxDoc, options = {}) {
+ EventEmitter.decorate(this);
+
+ this._document = toolboxDoc;
+
+ this.autoSelect = options.autoSelect || false;
+ this.position = options.position || "bottom";
+ let theme = options.theme || "dark";
+
+ this.onSelectCallback = options.onSelect;
+ this.onClickCallback = options.onClick;
+
+ // If theme is auto, use the devtools.theme pref
+ if (theme === "auto") {
+ theme = Services.prefs.getCharPref("devtools.theme");
+ this.autoThemeEnabled = true;
+ // Setup theme change listener.
+ this._handleThemeChange = this._handleThemeChange.bind(this);
+ gDevTools.on("pref-changed", this._handleThemeChange);
+ }
+
+ // Create HTMLTooltip instance
+ this._tooltip = new HTMLTooltip(this._document);
+ this._tooltip.panel.classList.add(
+ "devtools-autocomplete-popup",
+ "devtools-monospace",
+ theme + "-theme");
+ // Stop this appearing as an alert to accessibility.
+ this._tooltip.panel.setAttribute("role", "presentation");
+
+ this._list = this._document.createElementNS(HTML_NS, "ul");
+ this._list.setAttribute("flex", "1");
+
+ // The list clone will be inserted in the same document as the anchor, and will receive
+ // a copy of the main list innerHTML to allow screen readers to access the list.
+ this._listClone = this._document.createElementNS(HTML_NS, "ul");
+ this._listClone.className = "devtools-autocomplete-list-aria-clone";
+
+ if (options.listId) {
+ this._list.setAttribute("id", options.listId);
+ }
+ this._list.className = "devtools-autocomplete-listbox " + theme + "-theme";
+
+ this._tooltip.setContent(this._list);
+
+ this.onClick = this.onClick.bind(this);
+ this._list.addEventListener("click", this.onClick, false);
+
+ // Array of raw autocomplete items
+ this.items = [];
+ // Map of autocompleteItem to HTMLElement
+ this.elements = new WeakMap();
+
+ this.selectedIndex = -1;
+}
+exports.AutocompletePopup = AutocompletePopup;
+
+AutocompletePopup.prototype = {
+ _document: null,
+ _tooltip: null,
+ _list: null,
+
+ onSelect: function (e) {
+ if (this.onSelectCallback) {
+ this.onSelectCallback(e);
+ }
+ },
+
+ onClick: function (e) {
+ let item = e.target.closest(".autocomplete-item");
+ if (item && typeof item.dataset.index !== "undefined") {
+ this.selectedIndex = parseInt(item.dataset.index, 10);
+ }
+
+ this.emit("popup-click");
+ if (this.onClickCallback) {
+ this.onClickCallback(e);
+ }
+ },
+
+ /**
+ * Open the autocomplete popup panel.
+ *
+ * @param {nsIDOMNode} anchor
+ * Optional node to anchor the panel to.
+ * @param {Number} xOffset
+ * Horizontal offset in pixels from the left of the node to the left
+ * of the popup.
+ * @param {Number} yOffset
+ * Vertical offset in pixels from the top of the node to the starting
+ * of the popup.
+ * @param {Number} index
+ * The position of item to select.
+ */
+ openPopup: function (anchor, xOffset = 0, yOffset = 0, index) {
+ this.__maxLabelLength = -1;
+ this._updateSize();
+
+ // Retrieve the anchor's document active element to add accessibility metadata.
+ this._activeElement = anchor.ownerDocument.activeElement;
+
+ this._tooltip.show(anchor, {
+ x: xOffset,
+ y: yOffset,
+ position: this.position,
+ });
+
+ this._tooltip.once("shown", () => {
+ if (this.autoSelect) {
+ this.selectItemAtIndex(index);
+ }
+
+ this.emit("popup-opened");
+ });
+ },
+
+ /**
+ * Select item at the provided index.
+ *
+ * @param {Number} index
+ * The position of the item to select.
+ */
+ selectItemAtIndex: function (index) {
+ if (typeof index !== "number") {
+ // If no index was provided, select the item closest to the input.
+ let isAboveInput = this.position === "top";
+ index = isAboveInput ? this.itemCount - 1 : 0;
+ }
+ this.selectedIndex = index;
+ },
+
+ /**
+ * Hide the autocomplete popup panel.
+ */
+ hidePopup: function () {
+ this._tooltip.once("hidden", () => {
+ this.emit("popup-closed");
+ });
+
+ this._clearActiveDescendant();
+ this._activeElement = null;
+ this._tooltip.hide();
+ },
+
+ /**
+ * Check if the autocomplete popup is open.
+ */
+ get isOpen() {
+ return this._tooltip && this._tooltip.isVisible();
+ },
+
+ /**
+ * Destroy the object instance. Please note that the panel DOM elements remain
+ * in the DOM, because they might still be in use by other instances of the
+ * same code. It is the responsability of the client code to perform DOM
+ * cleanup.
+ */
+ destroy: function () {
+ if (this.isOpen) {
+ this.hidePopup();
+ }
+
+ this._list.removeEventListener("click", this.onClick, false);
+
+ if (this.autoThemeEnabled) {
+ gDevTools.off("pref-changed", this._handleThemeChange);
+ }
+
+ this._list.remove();
+ this._listClone.remove();
+ this._tooltip.destroy();
+ this._document = null;
+ this._list = null;
+ this._tooltip = null;
+ },
+
+ /**
+ * Get the autocomplete items array.
+ *
+ * @param {Number} index
+ * The index of the item what is wanted.
+ *
+ * @return {Object} The autocomplete item at index index.
+ */
+ getItemAtIndex: function (index) {
+ return this.items[index];
+ },
+
+ /**
+ * Get the autocomplete items array.
+ *
+ * @return {Array} The array of autocomplete items.
+ */
+ getItems: function () {
+ // Return a copy of the array to avoid side effects from the caller code.
+ return this.items.slice(0);
+ },
+
+ /**
+ * Set the autocomplete items list, in one go.
+ *
+ * @param {Array} items
+ * The list of items you want displayed in the popup list.
+ * @param {Number} index
+ * The position of the item to select.
+ */
+ setItems: function (items, index) {
+ this.clearItems();
+ items.forEach(this.appendItem, this);
+
+ if (this.isOpen && this.autoSelect) {
+ this.selectItemAtIndex(index);
+ }
+ },
+
+ __maxLabelLength: -1,
+
+ get _maxLabelLength() {
+ if (this.__maxLabelLength !== -1) {
+ return this.__maxLabelLength;
+ }
+
+ let max = 0;
+ for (let {label, count} of this.items) {
+ if (count) {
+ label += count + "";
+ }
+ max = Math.max(label.length, max);
+ }
+
+ this.__maxLabelLength = max;
+ return this.__maxLabelLength;
+ },
+
+ /**
+ * Update the panel size to fit the content.
+ */
+ _updateSize: function () {
+ if (!this._tooltip) {
+ return;
+ }
+
+ this._list.style.width = (this._maxLabelLength + 3) + "ch";
+ let selectedItem = this.selectedItem;
+ if (selectedItem) {
+ this._scrollElementIntoViewIfNeeded(this.elements.get(selectedItem));
+ }
+ },
+
+ _scrollElementIntoViewIfNeeded: function (element) {
+ let quads = element.getBoxQuads({relativeTo: this._tooltip.panel});
+ if (!quads || !quads[0]) {
+ return;
+ }
+
+ let {top, height} = quads[0].bounds;
+ let containerHeight = this._tooltip.panel.getBoundingClientRect().height;
+ if (top < 0) {
+ // Element is above container.
+ element.scrollIntoView(true);
+ } else if ((top + height) > containerHeight) {
+ // Element is beloew container.
+ element.scrollIntoView(false);
+ }
+ },
+
+ /**
+ * Clear all the items from the autocomplete list.
+ */
+ clearItems: function () {
+ // Reset the selectedIndex to -1 before clearing the list
+ this.selectedIndex = -1;
+ this._list.innerHTML = "";
+ this.__maxLabelLength = -1;
+ this.items = [];
+ this.elements = new WeakMap();
+ },
+
+ /**
+ * Getter for the index of the selected item.
+ *
+ * @type {Number}
+ */
+ get selectedIndex() {
+ return this._selectedIndex;
+ },
+
+ /**
+ * Setter for the selected index.
+ *
+ * @param {Number} index
+ * The number (index) of the item you want to select in the list.
+ */
+ set selectedIndex(index) {
+ let previousSelected = this._list.querySelector(".autocomplete-selected");
+ if (previousSelected) {
+ previousSelected.classList.remove("autocomplete-selected");
+ }
+
+ let item = this.items[index];
+ if (this.isOpen && item) {
+ let element = this.elements.get(item);
+
+ element.classList.add("autocomplete-selected");
+ this._scrollElementIntoViewIfNeeded(element);
+ this._setActiveDescendant(element.id);
+ } else {
+ this._clearActiveDescendant();
+ }
+ this._selectedIndex = index;
+
+ if (this.isOpen && item && this.onSelectCallback) {
+ // Call the user-defined select callback if defined.
+ this.onSelectCallback();
+ }
+ },
+
+ /**
+ * Getter for the selected item.
+ * @type Object
+ */
+ get selectedItem() {
+ return this.items[this._selectedIndex];
+ },
+
+ /**
+ * Setter for the selected item.
+ *
+ * @param {Object} item
+ * The object you want selected in the list.
+ */
+ set selectedItem(item) {
+ let index = this.items.indexOf(item);
+ if (index !== -1 && this.isOpen) {
+ this.selectedIndex = index;
+ }
+ },
+
+ /**
+ * Update the aria-activedescendant attribute on the current active element for
+ * accessibility.
+ *
+ * @param {String} id
+ * The id (as in DOM id) of the currently selected autocomplete suggestion
+ */
+ _setActiveDescendant: function (id) {
+ if (!this._activeElement) {
+ return;
+ }
+
+ // Make sure the list clone is in the same document as the anchor.
+ let anchorDoc = this._activeElement.ownerDocument;
+ if (!this._listClone.parentNode || this._listClone.ownerDocument !== anchorDoc) {
+ anchorDoc.documentElement.appendChild(this._listClone);
+ }
+
+ // Update the clone content to match the current list content.
+ this._listClone.innerHTML = this._list.innerHTML;
+
+ this._activeElement.setAttribute("aria-activedescendant", id);
+ },
+
+ /**
+ * Clear the aria-activedescendant attribute on the current active element.
+ */
+ _clearActiveDescendant: function () {
+ if (!this._activeElement) {
+ return;
+ }
+
+ this._activeElement.removeAttribute("aria-activedescendant");
+ },
+
+ /**
+ * Append an item into the autocomplete list.
+ *
+ * @param {Object} item
+ * The item you want appended to the list.
+ * The item object can have the following properties:
+ * - label {String} Property which is used as the displayed value.
+ * - preLabel {String} [Optional] The String that will be displayed
+ * before the label indicating that this is the already
+ * present text in the input box, and label is the text
+ * that will be auto completed. When this property is
+ * present, |preLabel.length| starting characters will be
+ * removed from label.
+ * - count {Number} [Optional] The number to represent the count of
+ * autocompleted label.
+ */
+ appendItem: function (item) {
+ let listItem = this._document.createElementNS(HTML_NS, "li");
+ // Items must have an id for accessibility.
+ listItem.setAttribute("id", "autocomplete-item-" + itemIdCounter++);
+ listItem.className = "autocomplete-item";
+ listItem.setAttribute("data-index", this.items.length);
+ if (this.direction) {
+ listItem.setAttribute("dir", this.direction);
+ }
+ let label = this._document.createElementNS(HTML_NS, "span");
+ label.textContent = item.label;
+ label.className = "autocomplete-value";
+ if (item.preLabel) {
+ let preDesc = this._document.createElementNS(HTML_NS, "span");
+ preDesc.textContent = item.preLabel;
+ preDesc.className = "initial-value";
+ listItem.appendChild(preDesc);
+ label.textContent = item.label.slice(item.preLabel.length);
+ }
+ listItem.appendChild(label);
+ if (item.count && item.count > 1) {
+ let countDesc = this._document.createElementNS(HTML_NS, "span");
+ countDesc.textContent = item.count;
+ countDesc.setAttribute("flex", "1");
+ countDesc.className = "autocomplete-count";
+ listItem.appendChild(countDesc);
+ }
+
+ this._list.appendChild(listItem);
+ this.items.push(item);
+ this.elements.set(item, listItem);
+ },
+
+ /**
+ * Remove an item from the popup list.
+ *
+ * @param {Object} item
+ * The item you want removed.
+ */
+ removeItem: function (item) {
+ if (!this.items.includes(item)) {
+ return;
+ }
+
+ let itemIndex = this.items.indexOf(item);
+ let selectedIndex = this.selectedIndex;
+
+ // Remove autocomplete item.
+ this.items.splice(itemIndex, 1);
+
+ // Remove corresponding DOM element from the elements WeakMap and from the DOM.
+ let elementToRemove = this.elements.get(item);
+ this.elements.delete(elementToRemove);
+ elementToRemove.remove();
+
+ if (itemIndex <= selectedIndex) {
+ // If the removed item index was before or equal to the selected index, shift the
+ // selected index by 1.
+ this.selectedIndex = Math.max(0, selectedIndex - 1);
+ }
+ },
+
+ /**
+ * Getter for the number of items in the popup.
+ * @type {Number}
+ */
+ get itemCount() {
+ return this.items.length;
+ },
+
+ /**
+ * Getter for the height of each item in the list.
+ *
+ * @type {Number}
+ */
+ get _itemsPerPane() {
+ if (this.items.length) {
+ let listHeight = this._tooltip.panel.clientHeight;
+ let element = this.elements.get(this.items[0]);
+ let elementHeight = element.getBoundingClientRect().height;
+ return Math.floor(listHeight / elementHeight);
+ }
+ return 0;
+ },
+
+ /**
+ * Select the next item in the list.
+ *
+ * @return {Object}
+ * The newly selected item object.
+ */
+ selectNextItem: function () {
+ if (this.selectedIndex < (this.items.length - 1)) {
+ this.selectedIndex++;
+ } else {
+ this.selectedIndex = 0;
+ }
+ return this.selectedItem;
+ },
+
+ /**
+ * Select the previous item in the list.
+ *
+ * @return {Object}
+ * The newly-selected item object.
+ */
+ selectPreviousItem: function () {
+ if (this.selectedIndex > 0) {
+ this.selectedIndex--;
+ } else {
+ this.selectedIndex = this.items.length - 1;
+ }
+
+ return this.selectedItem;
+ },
+
+ /**
+ * Select the top-most item in the next page of items or
+ * the last item in the list.
+ *
+ * @return {Object}
+ * The newly-selected item object.
+ */
+ selectNextPageItem: function () {
+ let nextPageIndex = this.selectedIndex + this._itemsPerPane + 1;
+ this.selectedIndex = Math.min(nextPageIndex, this.itemCount - 1);
+ return this.selectedItem;
+ },
+
+ /**
+ * Select the bottom-most item in the previous page of items,
+ * or the first item in the list.
+ *
+ * @return {Object}
+ * The newly-selected item object.
+ */
+ selectPreviousPageItem: function () {
+ let prevPageIndex = this.selectedIndex - this._itemsPerPane - 1;
+ this.selectedIndex = Math.max(prevPageIndex, 0);
+ return this.selectedItem;
+ },
+
+ /**
+ * Manages theme switching for the popup based on the devtools.theme pref.
+ *
+ * @private
+ *
+ * @param {String} event
+ * The name of the event. In this case, "pref-changed".
+ * @param {Object} data
+ * An object passed by the emitter of the event. In this case, the
+ * object consists of three properties:
+ * - pref {String} The name of the preference that was modified.
+ * - newValue {Object} The new value of the preference.
+ * - oldValue {Object} The old value of the preference.
+ */
+ _handleThemeChange: function (event, data) {
+ if (data.pref === "devtools.theme") {
+ this._tooltip.panel.classList.toggle(data.oldValue + "-theme", false);
+ this._tooltip.panel.classList.toggle(data.newValue + "-theme", true);
+ this._list.classList.toggle(data.oldValue + "-theme", false);
+ this._list.classList.toggle(data.newValue + "-theme", true);
+ }
+ },
+
+ /**
+ * Used by tests.
+ */
+ get _panel() {
+ return this._tooltip.panel;
+ },
+
+ /**
+ * Used by tests.
+ */
+ get _window() {
+ return this._document.defaultView;
+ },
+};
diff --git a/devtools/client/shared/browser-loader.js b/devtools/client/shared/browser-loader.js
new file mode 100644
index 000000000..f5cac31e7
--- /dev/null
+++ b/devtools/client/shared/browser-loader.js
@@ -0,0 +1,235 @@
+/* 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 Cu = Components.utils;
+const loaders = Cu.import("resource://gre/modules/commonjs/toolkit/loader.js", {});
+const { devtools } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const { joinURI } = devtools.require("devtools/shared/path");
+const { assert } = devtools.require("devtools/shared/DevToolsUtils");
+const Services = devtools.require("Services");
+const { AppConstants } = devtools.require("resource://gre/modules/AppConstants.jsm");
+
+const BROWSER_BASED_DIRS = [
+ "resource://devtools/client/inspector/layout",
+ "resource://devtools/client/jsonview",
+ "resource://devtools/client/shared/vendor",
+ "resource://devtools/client/shared/redux",
+];
+
+// Any directory that matches the following regular expression
+// is also considered as browser based module directory.
+// ('resource://devtools/client/.*/components/')
+//
+// An example:
+// * `resource://devtools/client/inspector/components`
+// * `resource://devtools/client/inspector/shared/components`
+const browserBasedDirsRegExp =
+ /^resource\:\/\/devtools\/client\/\S*\/components\//;
+
+function clearCache() {
+ Services.obs.notifyObservers(null, "startupcache-invalidate", null);
+}
+
+/*
+ * Create a loader to be used in a browser environment. This evaluates
+ * modules in their own environment, but sets window (the normal
+ * global object) as the sandbox prototype, so when a variable is not
+ * defined it checks `window` before throwing an error. This makes all
+ * browser APIs available to modules by default, like a normal browser
+ * environment, but modules are still evaluated in their own scope.
+ *
+ * Another very important feature of this loader is that it *only*
+ * deals with modules loaded from under `baseURI`. Anything loaded
+ * outside of that path will still be loaded from the devtools loader,
+ * so all system modules are still shared and cached across instances.
+ * An exception to this is anything under
+ * `devtools/client/shared/{vendor/components}`, which is where shared libraries
+ * and React components live that should be evaluated in a browser environment.
+ *
+ * @param string baseURI
+ * Base path to load modules from. If null or undefined, only
+ * the shared vendor/components modules are loaded with the browser
+ * loader.
+ * @param Object window
+ * The window instance to evaluate modules within
+ * @param Boolean useOnlyShared
+ * If true, ignores `baseURI` and only loads the shared
+ * BROWSER_BASED_DIRS via BrowserLoader.
+ * @return Object
+ * An object with two properties:
+ * - loader: the Loader instance
+ * - require: a function to require modules with
+ */
+function BrowserLoader(options) {
+ const browserLoaderBuilder = new BrowserLoaderBuilder(options);
+ return {
+ loader: browserLoaderBuilder.loader,
+ require: browserLoaderBuilder.require
+ };
+}
+
+/**
+ * Private class used to build the Loader instance and require method returned
+ * by BrowserLoader(baseURI, window).
+ *
+ * @param string baseURI
+ * Base path to load modules from.
+ * @param Object window
+ * The window instance to evaluate modules within
+ * @param Boolean useOnlyShared
+ * If true, ignores `baseURI` and only loads the shared
+ * BROWSER_BASED_DIRS via BrowserLoader.
+ */
+function BrowserLoaderBuilder({ baseURI, window, useOnlyShared }) {
+ assert(!!baseURI !== !!useOnlyShared,
+ "Cannot use both `baseURI` and `useOnlyShared`.");
+
+ const loaderOptions = devtools.require("@loader/options");
+ const dynamicPaths = {};
+ const componentProxies = new Map();
+
+ if (AppConstants.DEBUG || AppConstants.DEBUG_JS_MODULES) {
+ dynamicPaths["devtools/client/shared/vendor/react"] =
+ "resource://devtools/client/shared/vendor/react-dev";
+ }
+
+ const opts = {
+ id: "browser-loader",
+ sharedGlobal: true,
+ sandboxPrototype: window,
+ paths: Object.assign({}, dynamicPaths, loaderOptions.paths),
+ invisibleToDebugger: loaderOptions.invisibleToDebugger,
+ requireHook: (id, require) => {
+ // If |id| requires special handling, simply defer to devtools
+ // immediately.
+ if (devtools.isLoaderPluginId(id)) {
+ return devtools.require(id);
+ }
+
+ const uri = require.resolve(id);
+ let isBrowserDir = BROWSER_BASED_DIRS.filter(dir => {
+ return uri.startsWith(dir);
+ }).length > 0;
+
+ // If the URI doesn't match hardcoded paths try the regexp.
+ if (!isBrowserDir) {
+ isBrowserDir = uri.match(browserBasedDirsRegExp) != null;
+ }
+
+ if ((useOnlyShared || !uri.startsWith(baseURI)) && !isBrowserDir) {
+ return devtools.require(uri);
+ }
+
+ return require(uri);
+ },
+ globals: {
+ // Allow modules to use the window's console to ensure logs appear in a
+ // tab toolbox, if one exists, instead of just the browser console.
+ console: window.console,
+ // Make sure `define` function exists. This allows defining some modules
+ // in AMD format while retaining CommonJS compatibility through this hook.
+ // JSON Viewer needs modules in AMD format, as it currently uses RequireJS
+ // from a content document and can't access our usual loaders. So, any
+ // modules shared with the JSON Viewer should include a define wrapper:
+ //
+ // // Make this available to both AMD and CJS environments
+ // define(function(require, exports, module) {
+ // ... code ...
+ // });
+ //
+ // Bug 1248830 will work out a better plan here for our content module
+ // loading needs, especially as we head towards devtools.html.
+ define(factory) {
+ factory(this.require, this.exports, this.module);
+ },
+ // Allow modules to use the DevToolsLoader lazy loading helpers.
+ loader: {
+ lazyGetter: devtools.lazyGetter,
+ lazyImporter: devtools.lazyImporter,
+ lazyServiceGetter: devtools.lazyServiceGetter,
+ lazyRequireGetter: this.lazyRequireGetter.bind(this),
+ },
+ }
+ };
+
+ if (Services.prefs.getBoolPref("devtools.loader.hotreload")) {
+ opts.loadModuleHook = (module, require) => {
+ const { uri, exports } = module;
+
+ if (exports.prototype &&
+ exports.prototype.isReactComponent) {
+ const { createProxy, getForceUpdate } =
+ require("devtools/client/shared/vendor/react-proxy");
+ const React = require("devtools/client/shared/vendor/react");
+
+ if (!componentProxies.get(uri)) {
+ const proxy = createProxy(exports);
+ componentProxies.set(uri, proxy);
+ module.exports = proxy.get();
+ } else {
+ const proxy = componentProxies.get(uri);
+ const instances = proxy.update(exports);
+ instances.forEach(getForceUpdate(React));
+ module.exports = proxy.get();
+ }
+ }
+ return exports;
+ };
+ const watcher = devtools.require("devtools/client/shared/devtools-file-watcher");
+ let onFileChanged = (_, relativePath, path) => {
+ this.hotReloadFile(componentProxies, "resource://devtools/" + relativePath);
+ };
+ watcher.on("file-changed", onFileChanged);
+ window.addEventListener("unload", () => {
+ watcher.off("file-changed", onFileChanged);
+ });
+ }
+
+ const mainModule = loaders.Module(baseURI, joinURI(baseURI, "main.js"));
+ this.loader = loaders.Loader(opts);
+ this.require = loaders.Require(this.loader, mainModule);
+}
+
+BrowserLoaderBuilder.prototype = {
+ /**
+ * Define a getter property on the given object that requires the given
+ * module. This enables delaying importing modules until the module is
+ * actually used.
+ *
+ * @param Object obj
+ * The object to define the property on.
+ * @param String property
+ * The property name.
+ * @param String module
+ * The module path.
+ * @param Boolean destructure
+ * Pass true if the property name is a member of the module's exports.
+ */
+ lazyRequireGetter: function (obj, property, module, destructure) {
+ devtools.lazyGetter(obj, property, () => {
+ return destructure
+ ? this.require(module)[property]
+ : this.require(module || property);
+ });
+ },
+
+ hotReloadFile: function (componentProxies, fileURI) {
+ if (fileURI.match(/\.js$/)) {
+ // Test for React proxy components
+ const proxy = componentProxies.get(fileURI);
+ if (proxy) {
+ // Remove the old module and re-require the new one; the require
+ // hook in the loader will take care of the rest
+ delete this.loader.modules[fileURI];
+ clearCache();
+ this.require(fileURI);
+ }
+ }
+ }
+};
+
+this.BrowserLoader = BrowserLoader;
+
+this.EXPORTED_SYMBOLS = ["BrowserLoader"];
diff --git a/devtools/client/shared/components/.eslintrc.js b/devtools/client/shared/components/.eslintrc.js
new file mode 100644
index 000000000..3112895e9
--- /dev/null
+++ b/devtools/client/shared/components/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "globals": {
+ "define": true,
+ }
+};
diff --git a/devtools/client/shared/components/frame.js b/devtools/client/shared/components/frame.js
new file mode 100644
index 000000000..5abe5f057
--- /dev/null
+++ b/devtools/client/shared/components/frame.js
@@ -0,0 +1,239 @@
+/* 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 { DOM: dom, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
+const { getSourceNames, parseURL,
+ isScratchpadScheme, getSourceMappedFile } = require("devtools/client/shared/source-utils");
+const { LocalizationHelper } = require("devtools/shared/l10n");
+
+const l10n = new LocalizationHelper("devtools/client/locales/components.properties");
+const webl10n = new LocalizationHelper("devtools/client/locales/webconsole.properties");
+
+module.exports = createClass({
+ displayName: "Frame",
+
+ propTypes: {
+ // SavedFrame, or an object containing all the required properties.
+ frame: PropTypes.shape({
+ functionDisplayName: PropTypes.string,
+ source: PropTypes.string.isRequired,
+ line: PropTypes.oneOfType([ PropTypes.string, PropTypes.number ]),
+ column: PropTypes.oneOfType([ PropTypes.string, PropTypes.number ]),
+ }).isRequired,
+ // Clicking on the frame link -- probably should link to the debugger.
+ onClick: PropTypes.func.isRequired,
+ // Option to display a function name before the source link.
+ showFunctionName: PropTypes.bool,
+ // Option to display a function name even if it's anonymous.
+ showAnonymousFunctionName: PropTypes.bool,
+ // Option to display a host name after the source link.
+ showHost: PropTypes.bool,
+ // Option to display a host name if the filename is empty or just '/'
+ showEmptyPathAsHost: PropTypes.bool,
+ // Option to display a full source instead of just the filename.
+ showFullSourceUrl: PropTypes.bool,
+ // Service to enable the source map feature for console.
+ sourceMapService: PropTypes.object,
+ },
+
+ getDefaultProps() {
+ return {
+ showFunctionName: false,
+ showAnonymousFunctionName: false,
+ showHost: false,
+ showEmptyPathAsHost: false,
+ showFullSourceUrl: false,
+ };
+ },
+
+ componentWillMount() {
+ const sourceMapService = this.props.sourceMapService;
+ if (sourceMapService) {
+ const source = this.getSource();
+ sourceMapService.subscribe(source, this.onSourceUpdated);
+ }
+ },
+
+ componentWillUnmount() {
+ const sourceMapService = this.props.sourceMapService;
+ if (sourceMapService) {
+ const source = this.getSource();
+ sourceMapService.unsubscribe(source, this.onSourceUpdated);
+ }
+ },
+
+ /**
+ * Component method to update the FrameView when a resolved location is available
+ * @param event
+ * @param location
+ */
+ onSourceUpdated(event, location, resolvedLocation) {
+ const frame = this.getFrame(resolvedLocation);
+ this.setState({
+ frame,
+ isSourceMapped: true,
+ });
+ },
+
+ /**
+ * Utility method to convert the Frame object to the
+ * Source Object model required by SourceMapService
+ * @param frame
+ * @returns {{url: *, line: *, column: *}}
+ */
+ getSource(frame) {
+ frame = frame || this.props.frame;
+ const { source, line, column } = frame;
+ return {
+ url: source,
+ line,
+ column,
+ };
+ },
+
+ /**
+ * Utility method to convert the Source object model to the
+ * Frame object model required by FrameView class.
+ * @param source
+ * @returns {{source: *, line: *, column: *, functionDisplayName: *}}
+ */
+ getFrame(source) {
+ const { url, line, column } = source;
+ return {
+ source: url,
+ line,
+ column,
+ functionDisplayName: this.props.frame.functionDisplayName,
+ };
+ },
+
+ render() {
+ let frame, isSourceMapped;
+ let {
+ onClick,
+ showFunctionName,
+ showAnonymousFunctionName,
+ showHost,
+ showEmptyPathAsHost,
+ showFullSourceUrl
+ } = this.props;
+
+ if (this.state && this.state.isSourceMapped) {
+ frame = this.state.frame;
+ isSourceMapped = this.state.isSourceMapped;
+ } else {
+ frame = this.props.frame;
+ }
+
+ let source = frame.source ? String(frame.source) : "";
+ let line = frame.line != void 0 ? Number(frame.line) : null;
+ let column = frame.column != void 0 ? Number(frame.column) : null;
+
+ const { short, long, host } = getSourceNames(source);
+ // Reparse the URL to determine if we should link this; `getSourceNames`
+ // has already cached this indirectly. We don't want to attempt to
+ // link to "self-hosted" and "(unknown)". However, we do want to link
+ // to Scratchpad URIs.
+ // Source mapped sources might not necessary linkable, but they
+ // are still valid in the debugger.
+ const isLinkable = !!(isScratchpadScheme(source) || parseURL(source))
+ || isSourceMapped;
+ const elements = [];
+ const sourceElements = [];
+ let sourceEl;
+
+ let tooltip = long;
+
+ // Exclude all falsy values, including `0`, as line numbers start with 1.
+ if (line) {
+ tooltip += `:${line}`;
+ // Intentionally exclude 0
+ if (column) {
+ tooltip += `:${column}`;
+ }
+ }
+
+ let attributes = {
+ "data-url": long,
+ className: "frame-link",
+ };
+
+ if (showFunctionName) {
+ let functionDisplayName = frame.functionDisplayName;
+ if (!functionDisplayName && showAnonymousFunctionName) {
+ functionDisplayName = webl10n.getStr("stacktrace.anonymousFunction");
+ }
+
+ if (functionDisplayName) {
+ elements.push(
+ dom.span({ className: "frame-link-function-display-name" },
+ functionDisplayName),
+ " "
+ );
+ }
+ }
+
+ let displaySource = showFullSourceUrl ? long : short;
+ if (isSourceMapped) {
+ displaySource = getSourceMappedFile(displaySource);
+ } else if (showEmptyPathAsHost && (displaySource === "" || displaySource === "/")) {
+ displaySource = host;
+ }
+
+ sourceElements.push(dom.span({
+ className: "frame-link-filename",
+ }, displaySource));
+
+ // If we have a line number > 0.
+ if (line) {
+ let lineInfo = `:${line}`;
+ // Add `data-line` attribute for testing
+ attributes["data-line"] = line;
+
+ // Intentionally exclude 0
+ if (column) {
+ lineInfo += `:${column}`;
+ // Add `data-column` attribute for testing
+ attributes["data-column"] = column;
+ }
+
+ sourceElements.push(dom.span({ className: "frame-link-line" }, lineInfo));
+ }
+
+ // Inner el is useful for achieving ellipsis on the left and correct LTR/RTL
+ // ordering. See CSS styles for frame-link-source-[inner] and bug 1290056.
+ let sourceInnerEl = dom.span({
+ className: "frame-link-source-inner",
+ title: isLinkable ?
+ l10n.getFormatStr("frame.viewsourceindebugger", tooltip) : tooltip,
+ }, sourceElements);
+
+ // If source is not a URL (self-hosted, eval, etc.), don't make
+ // it an anchor link, as we can't link to it.
+ if (isLinkable) {
+ sourceEl = dom.a({
+ onClick: e => {
+ e.preventDefault();
+ onClick(this.getSource(frame));
+ },
+ href: source,
+ className: "frame-link-source",
+ draggable: false,
+ }, sourceInnerEl);
+ } else {
+ sourceEl = dom.span({
+ className: "frame-link-source",
+ }, sourceInnerEl);
+ }
+ elements.push(sourceEl);
+
+ if (showHost && host) {
+ elements.push(" ", dom.span({ className: "frame-link-host" }, host));
+ }
+
+ return dom.span(attributes, ...elements);
+ }
+});
diff --git a/devtools/client/shared/components/h-split-box.js b/devtools/client/shared/components/h-split-box.js
new file mode 100644
index 000000000..d804a6ba1
--- /dev/null
+++ b/devtools/client/shared/components/h-split-box.js
@@ -0,0 +1,154 @@
+/* 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/. */
+
+/* eslint-env browser */
+"use strict";
+
+// A box with a start and a end pane, separated by a dragable splitter that
+// allows the user to resize the relative widths of the panes.
+//
+// +-----------------------+---------------------+
+// | | |
+// | | |
+// | S |
+// | Start Pane p End Pane |
+// | l |
+// | i |
+// | t |
+// | t |
+// | e |
+// | r |
+// | | |
+// | | |
+// +-----------------------+---------------------+
+
+const {
+ DOM: dom,
+ createClass,
+ PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const { assert } = require("devtools/shared/DevToolsUtils");
+
+module.exports = createClass({
+ displayName: "HSplitBox",
+
+ propTypes: {
+ // The contents of the start pane.
+ start: PropTypes.any.isRequired,
+
+ // The contents of the end pane.
+ end: PropTypes.any.isRequired,
+
+ // The relative width of the start pane, expressed as a number between 0 and
+ // 1. The relative width of the end pane is 1 - startWidth. For example,
+ // with startWidth = .5, both panes are of equal width; with startWidth =
+ // .25, the start panel will take up 1/4 width and the end panel will take
+ // up 3/4 width.
+ startWidth: PropTypes.number,
+
+ // A minimum css width value for the start and end panes.
+ minStartWidth: PropTypes.any,
+ minEndWidth: PropTypes.any,
+
+ // A callback fired when the user drags the splitter to resize the relative
+ // pane widths. The function is passed the startWidth value that would put
+ // the splitter underneath the users mouse.
+ onResize: PropTypes.func.isRequired,
+ },
+
+ getDefaultProps() {
+ return {
+ startWidth: 0.5,
+ minStartWidth: "20px",
+ minEndWidth: "20px",
+ };
+ },
+
+ getInitialState() {
+ return {
+ mouseDown: false
+ };
+ },
+
+ componentDidMount() {
+ document.defaultView.top.addEventListener("mouseup", this._onMouseUp,
+ false);
+ document.defaultView.top.addEventListener("mousemove", this._onMouseMove,
+ false);
+ },
+
+ componentWillUnmount() {
+ document.defaultView.top.removeEventListener("mouseup", this._onMouseUp,
+ false);
+ document.defaultView.top.removeEventListener("mousemove", this._onMouseMove,
+ false);
+ },
+
+ _onMouseDown(event) {
+ if (event.button !== 0) {
+ return;
+ }
+
+ this.setState({ mouseDown: true });
+ event.preventDefault();
+ },
+
+ _onMouseUp(event) {
+ if (event.button !== 0 || !this.state.mouseDown) {
+ return;
+ }
+
+ this.setState({ mouseDown: false });
+ event.preventDefault();
+ },
+
+ _onMouseMove(event) {
+ if (!this.state.mouseDown) {
+ return;
+ }
+
+ const rect = this.refs.box.getBoundingClientRect();
+ const { left, right } = rect;
+ const width = right - left;
+ const relative = event.clientX - left;
+ this.props.onResize(relative / width);
+
+ event.preventDefault();
+ },
+
+ render() {
+ /* eslint-disable no-shadow */
+ const { start, end, startWidth, minStartWidth, minEndWidth } = this.props;
+ assert(startWidth => 0 && startWidth <= 1,
+ "0 <= this.props.startWidth <= 1");
+ /* eslint-enable */
+ return dom.div(
+ {
+ className: "h-split-box",
+ ref: "box",
+ },
+
+ dom.div(
+ {
+ className: "h-split-box-pane",
+ style: { flex: startWidth, minWidth: minStartWidth },
+ },
+ start
+ ),
+
+ dom.div({
+ className: "devtools-side-splitter",
+ onMouseDown: this._onMouseDown,
+ }),
+
+ dom.div(
+ {
+ className: "h-split-box-pane",
+ style: { flex: 1 - startWidth, minWidth: minEndWidth },
+ },
+ end
+ )
+ );
+ }
+});
diff --git a/devtools/client/shared/components/moz.build b/devtools/client/shared/components/moz.build
new file mode 100644
index 000000000..0d67e90b5
--- /dev/null
+++ b/devtools/client/shared/components/moz.build
@@ -0,0 +1,27 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DIRS += [
+ 'reps',
+ 'splitter',
+ 'tabs',
+ 'tree'
+]
+
+DevToolsModules(
+ 'frame.js',
+ 'h-split-box.js',
+ 'notification-box.css',
+ 'notification-box.js',
+ 'search-box.js',
+ 'sidebar-toggle.css',
+ 'sidebar-toggle.js',
+ 'stack-trace.js',
+ 'tree.js',
+)
+
+MOCHITEST_CHROME_MANIFESTS += ['test/mochitest/chrome.ini']
+BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
diff --git a/devtools/client/shared/components/notification-box.css b/devtools/client/shared/components/notification-box.css
new file mode 100644
index 000000000..83c29b616
--- /dev/null
+++ b/devtools/client/shared/components/notification-box.css
@@ -0,0 +1,95 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+/* Layout */
+
+.notificationbox .notificationInner {
+ display: flex;
+ flex-direction: row;
+}
+
+.notificationbox .details {
+ flex-grow: 1;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+}
+
+.notificationbox .notification-button {
+ text-align: right;
+}
+
+.notificationbox .messageText {
+ flex-grow: 1;
+}
+
+.notificationbox .details:-moz-dir(rtl)
+.notificationbox .notificationInner:-moz-dir(rtl) {
+ flex-direction: row-reverse;
+}
+
+/* Style */
+
+.notificationbox .notification {
+ background-color: InfoBackground;
+ text-shadow: none;
+ border-top: 1px solid ThreeDShadow;
+ border-bottom: 1px solid ThreeDShadow;
+}
+
+.notificationbox .notification[data-type="info"] {
+ color: -moz-DialogText;
+ background-color: -moz-Dialog;
+}
+
+.notificationbox .notification[data-type="critical"] {
+ color: white;
+ background-image: linear-gradient(rgb(212,0,0), rgb(152,0,0));
+}
+
+.notificationbox .messageImage {
+ display: inline-block;
+ width: 16px;
+ height: 16px;
+ margin: 6px;
+}
+
+/* Default icons for notifications */
+
+.notificationbox .messageImage[data-type="info"] {
+ background-image: url("chrome://global/skin/icons/information-16.png");
+}
+
+.notificationbox .messageImage[data-type="warning"] {
+ background-image: url("chrome://global/skin/icons/warning-16.png");
+}
+
+.notificationbox .messageImage[data-type="critical"] {
+ background-image: url("chrome://global/skin/icons/error-16.png");
+}
+
+/* Close button */
+
+.notificationbox .messageCloseButton {
+ width: 20px;
+ height: 20px;
+ margin: 4px;
+ margin-inline-end: 8px;
+ background-image: url("chrome://devtools/skin/images/close.svg");
+ background-position: center;
+ background-color: transparent;
+ background-repeat: no-repeat;
+ border-radius: 11px;
+ filter: invert(0);
+}
+
+.notificationbox .messageCloseButton:hover {
+ background-color: gray;
+ filter: invert(1);
+}
+
+.notificationbox .messageCloseButton:active {
+ background-color: rgba(170, 170, 170, .4); /* --toolbar-tab-hover-active */
+}
diff --git a/devtools/client/shared/components/notification-box.js b/devtools/client/shared/components/notification-box.js
new file mode 100644
index 000000000..87fc76cd6
--- /dev/null
+++ b/devtools/client/shared/components/notification-box.js
@@ -0,0 +1,263 @@
+/* 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 React = require("devtools/client/shared/vendor/react");
+const Immutable = require("devtools/client/shared/vendor/immutable");
+const { LocalizationHelper } = require("devtools/shared/l10n");
+const l10n = new LocalizationHelper("devtools/client/locales/components.properties");
+
+// Shortcuts
+const { PropTypes, createClass, DOM } = React;
+const { div, span, button } = DOM;
+
+// Priority Levels
+const PriorityLevels = {
+ PRIORITY_INFO_LOW: 1,
+ PRIORITY_INFO_MEDIUM: 2,
+ PRIORITY_INFO_HIGH: 3,
+ PRIORITY_WARNING_LOW: 4,
+ PRIORITY_WARNING_MEDIUM: 5,
+ PRIORITY_WARNING_HIGH: 6,
+ PRIORITY_CRITICAL_LOW: 7,
+ PRIORITY_CRITICAL_MEDIUM: 8,
+ PRIORITY_CRITICAL_HIGH: 9,
+ PRIORITY_CRITICAL_BLOCK: 10,
+};
+
+/**
+ * This component represents Notification Box - HTML alternative for
+ * <xul:notifictionbox> binding.
+ *
+ * See also MDN for more info about <xul:notificationbox>:
+ * https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL/notificationbox
+ */
+var NotificationBox = createClass({
+ displayName: "NotificationBox",
+
+ propTypes: {
+ // List of notifications appended into the box.
+ notifications: PropTypes.arrayOf(PropTypes.shape({
+ // label to appear on the notification.
+ label: PropTypes.string.isRequired,
+
+ // Value used to identify the notification
+ value: PropTypes.string.isRequired,
+
+ // URL of image to appear on the notification. If "" then an icon
+ // appropriate for the priority level is used.
+ image: PropTypes.string.isRequired,
+
+ // Notification priority; see Priority Levels.
+ priority: PropTypes.number.isRequired,
+
+ // Array of button descriptions to appear on the notification.
+ buttons: PropTypes.arrayOf(PropTypes.shape({
+ // Function to be called when the button is activated.
+ // This function is passed three arguments:
+ // 1) the NotificationBox component the button is associated with
+ // 2) the button description as passed to appendNotification.
+ // 3) the element which was the target of the button press event.
+ // If the return value from this function is not True, then the
+ // notification is closed. The notification is also not closed
+ // if an error is thrown.
+ callback: PropTypes.func.isRequired,
+
+ // The label to appear on the button.
+ label: PropTypes.string.isRequired,
+
+ // The accesskey attribute set on the <button> element.
+ accesskey: PropTypes.string,
+ })),
+
+ // A function to call to notify you of interesting things that happen
+ // with the notification box.
+ eventCallback: PropTypes.func,
+ })),
+
+ // Message that should be shown when hovering over the close button
+ closeButtonTooltip: PropTypes.string
+ },
+
+ getDefaultProps() {
+ return {
+ closeButtonTooltip: l10n.getStr("notificationBox.closeTooltip")
+ };
+ },
+
+ getInitialState() {
+ return {
+ notifications: new Immutable.OrderedMap()
+ };
+ },
+
+ /**
+ * Create a new notification and display it. If another notification is
+ * already present with a higher priority, the new notification will be
+ * added behind it. See `propTypes` for arguments description.
+ */
+ appendNotification(label, value, image, priority, buttons = [],
+ eventCallback) {
+ // Priority level must be within expected interval
+ // (see priority levels at the top of this file).
+ if (priority < PriorityLevels.PRIORITY_INFO_LOW ||
+ priority > PriorityLevels.PRIORITY_CRITICAL_BLOCK) {
+ throw new Error("Invalid notification priority " + priority);
+ }
+
+ // Custom image URL is not supported yet.
+ if (image) {
+ throw new Error("Custom image URL is not supported yet");
+ }
+
+ let type = "warning";
+ if (priority >= PriorityLevels.PRIORITY_CRITICAL_LOW) {
+ type = "critical";
+ } else if (priority <= PriorityLevels.PRIORITY_INFO_HIGH) {
+ type = "info";
+ }
+
+ let notifications = this.state.notifications.set(value, {
+ label: label,
+ value: value,
+ image: image,
+ priority: priority,
+ type: type,
+ buttons: buttons,
+ eventCallback: eventCallback,
+ });
+
+ // High priorities must be on top.
+ notifications = notifications.sortBy((val, key) => {
+ return -val.priority;
+ });
+
+ this.setState({
+ notifications: notifications
+ });
+ },
+
+ /**
+ * Remove specific notification from the list.
+ */
+ removeNotification(notification) {
+ this.close(this.state.notifications.get(notification.value));
+ },
+
+ /**
+ * Returns an object that represents a notification. It can be
+ * used to close it.
+ */
+ getNotificationWithValue(value) {
+ let notification = this.state.notifications.get(value);
+ if (!notification) {
+ return null;
+ }
+
+ // Return an object that can be used to remove the notification
+ // later (using `removeNotification` method) or directly close it.
+ return Object.assign({}, notification, {
+ close: () => {
+ this.close(notification);
+ }
+ });
+ },
+
+ getCurrentNotification() {
+ return this.state.notifications.first();
+ },
+
+ /**
+ * Close specified notification.
+ */
+ close(notification) {
+ if (!notification) {
+ return;
+ }
+
+ if (notification.eventCallback) {
+ notification.eventCallback("removed");
+ }
+
+ this.setState({
+ notifications: this.state.notifications.remove(notification.value)
+ });
+ },
+
+ /**
+ * Render a button. A notification can have a set of custom buttons.
+ * These are used to execute custom callback.
+ */
+ renderButton(props, notification) {
+ let onClick = event => {
+ if (props.callback) {
+ let result = props.callback(this, props, event.target);
+ if (!result) {
+ this.close(notification);
+ }
+ event.stopPropagation();
+ }
+ };
+
+ return (
+ button({
+ key: props.label,
+ className: "notification-button",
+ accesskey: props.accesskey,
+ onClick: onClick},
+ props.label
+ )
+ );
+ },
+
+ /**
+ * Render a notification.
+ */
+ renderNotification(notification) {
+ return (
+ div({
+ key: notification.value,
+ className: "notification",
+ "data-type": notification.type},
+ div({className: "notificationInner"},
+ div({className: "details"},
+ div({
+ className: "messageImage",
+ "data-type": notification.type}),
+ span({className: "messageText"},
+ notification.label
+ ),
+ notification.buttons.map(props =>
+ this.renderButton(props, notification)
+ )
+ ),
+ div({
+ className: "messageCloseButton",
+ title: this.props.closeButtonTooltip,
+ onClick: this.close.bind(this, notification)}
+ )
+ )
+ )
+ );
+ },
+
+ /**
+ * Render the top (highest priority) notification. Only one
+ * notification is rendered at a time.
+ */
+ render() {
+ let notification = this.state.notifications.first();
+ let content = notification ?
+ this.renderNotification(notification) :
+ null;
+
+ return div({className: "notificationbox"},
+ content
+ );
+ },
+});
+
+module.exports.NotificationBox = NotificationBox;
+module.exports.PriorityLevels = PriorityLevels;
diff --git a/devtools/client/shared/components/reps/array.js b/devtools/client/shared/components/reps/array.js
new file mode 100644
index 000000000..8ec1443e1
--- /dev/null
+++ b/devtools/client/shared/components/reps/array.js
@@ -0,0 +1,186 @@
+/* -*- 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const React = require("devtools/client/shared/vendor/react");
+ const { createFactories } = require("./rep-utils");
+ const { Caption } = createFactories(require("./caption"));
+
+ // Shortcuts
+ const DOM = React.DOM;
+
+ /**
+ * Renders an array. The array is enclosed by left and right bracket
+ * and the max number of rendered items depends on the current mode.
+ */
+ let ArrayRep = React.createClass({
+ displayName: "ArrayRep",
+
+ getTitle: function (object, context) {
+ return "[" + object.length + "]";
+ },
+
+ arrayIterator: function (array, max) {
+ let items = [];
+ let delim;
+
+ for (let i = 0; i < array.length && i < max; i++) {
+ try {
+ let value = array[i];
+
+ delim = (i == array.length - 1 ? "" : ", ");
+
+ items.push(ItemRep({
+ object: value,
+ // Hardcode tiny mode to avoid recursive handling.
+ mode: "tiny",
+ delim: delim
+ }));
+ } catch (exc) {
+ items.push(ItemRep({
+ object: exc,
+ mode: "tiny",
+ delim: delim
+ }));
+ }
+ }
+
+ if (array.length > max) {
+ let objectLink = this.props.objectLink || DOM.span;
+ items.push(Caption({
+ object: objectLink({
+ object: this.props.object
+ }, (array.length - max) + " more…")
+ }));
+ }
+
+ return items;
+ },
+
+ /**
+ * Returns true if the passed object is an array with additional (custom)
+ * properties, otherwise returns false. Custom properties should be
+ * displayed in extra expandable section.
+ *
+ * Example array with a custom property.
+ * let arr = [0, 1];
+ * arr.myProp = "Hello";
+ *
+ * @param {Array} array The array object.
+ */
+ hasSpecialProperties: function (array) {
+ function isInteger(x) {
+ let y = parseInt(x, 10);
+ if (isNaN(y)) {
+ return false;
+ }
+ return x === y.toString();
+ }
+
+ let props = Object.getOwnPropertyNames(array);
+ for (let i = 0; i < props.length; i++) {
+ let p = props[i];
+
+ // Valid indexes are skipped
+ if (isInteger(p)) {
+ continue;
+ }
+
+ // Ignore standard 'length' property, anything else is custom.
+ if (p != "length") {
+ return true;
+ }
+ }
+
+ return false;
+ },
+
+ // Event Handlers
+
+ onToggleProperties: function (event) {
+ },
+
+ onClickBracket: function (event) {
+ },
+
+ render: function () {
+ let mode = this.props.mode || "short";
+ let object = this.props.object;
+ let items;
+ let brackets;
+ let needSpace = function (space) {
+ return space ? { left: "[ ", right: " ]"} : { left: "[", right: "]"};
+ };
+
+ if (mode == "tiny") {
+ let isEmpty = object.length === 0;
+ items = [DOM.span({className: "length"}, isEmpty ? "" : object.length)];
+ brackets = needSpace(false);
+ } else {
+ let max = (mode == "short") ? 3 : 300;
+ items = this.arrayIterator(object, max);
+ brackets = needSpace(items.length > 0);
+ }
+
+ let objectLink = this.props.objectLink || DOM.span;
+
+ return (
+ DOM.span({
+ className: "objectBox objectBox-array"},
+ objectLink({
+ className: "arrayLeftBracket",
+ object: object
+ }, brackets.left),
+ ...items,
+ objectLink({
+ className: "arrayRightBracket",
+ object: object
+ }, brackets.right),
+ DOM.span({
+ className: "arrayProperties",
+ role: "group"}
+ )
+ )
+ );
+ },
+ });
+
+ /**
+ * Renders array item. Individual values are separated by a comma.
+ */
+ let ItemRep = React.createFactory(React.createClass({
+ displayName: "ItemRep",
+
+ render: function () {
+ const { Rep } = createFactories(require("./rep"));
+
+ let object = this.props.object;
+ let delim = this.props.delim;
+ let mode = this.props.mode;
+ return (
+ DOM.span({},
+ Rep({object: object, mode: mode}),
+ delim
+ )
+ );
+ }
+ }));
+
+ function supportsObject(object, type) {
+ return Array.isArray(object) ||
+ Object.prototype.toString.call(object) === "[object Arguments]";
+ }
+
+ // Exports from this module
+ exports.ArrayRep = {
+ rep: ArrayRep,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/attribute.js b/devtools/client/shared/components/reps/attribute.js
new file mode 100644
index 000000000..f57ed0380
--- /dev/null
+++ b/devtools/client/shared/components/reps/attribute.js
@@ -0,0 +1,70 @@
+/* -*- 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Reps
+ const { createFactories, isGrip } = require("./rep-utils");
+ const { StringRep } = require("./string");
+
+ // Shortcuts
+ const { span } = React.DOM;
+ const { rep: StringRepFactory } = createFactories(StringRep);
+
+ /**
+ * Renders DOM attribute
+ */
+ let Attribute = React.createClass({
+ displayName: "Attr",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired
+ },
+
+ getTitle: function (grip) {
+ return grip.preview.nodeName;
+ },
+
+ render: function () {
+ let grip = this.props.object;
+ let value = grip.preview.value;
+ let objectLink = this.props.objectLink || span;
+
+ return (
+ objectLink({className: "objectLink-Attr"},
+ span({},
+ span({className: "attrTitle"},
+ this.getTitle(grip)
+ ),
+ span({className: "attrEqual"},
+ "="
+ ),
+ StringRepFactory({object: value})
+ )
+ )
+ );
+ },
+ });
+
+ // Registration
+
+ function supportsObject(grip, type) {
+ if (!isGrip(grip)) {
+ return false;
+ }
+
+ return (type == "Attr" && grip.preview);
+ }
+
+ exports.Attribute = {
+ rep: Attribute,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/caption.js b/devtools/client/shared/components/reps/caption.js
new file mode 100644
index 000000000..7f00b01e8
--- /dev/null
+++ b/devtools/client/shared/components/reps/caption.js
@@ -0,0 +1,31 @@
+/* -*- 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const React = require("devtools/client/shared/vendor/react");
+ const DOM = React.DOM;
+
+ /**
+ * Renders a caption. This template is used by other components
+ * that needs to distinguish between a simple text/value and a label.
+ */
+ const Caption = React.createClass({
+ displayName: "Caption",
+
+ render: function () {
+ return (
+ DOM.span({"className": "caption"}, this.props.object)
+ );
+ },
+ });
+
+ // Exports from this module
+ exports.Caption = Caption;
+});
diff --git a/devtools/client/shared/components/reps/comment-node.js b/devtools/client/shared/components/reps/comment-node.js
new file mode 100644
index 000000000..2c69c1414
--- /dev/null
+++ b/devtools/client/shared/components/reps/comment-node.js
@@ -0,0 +1,60 @@
+/* -*- 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const React = require("devtools/client/shared/vendor/react");
+ const { isGrip, cropString, cropMultipleLines } = require("./rep-utils");
+
+ // Utils
+ const nodeConstants = require("devtools/shared/dom-node-constants");
+
+ // Shortcuts
+ const { span } = React.DOM;
+
+ /**
+ * Renders DOM comment node.
+ */
+ const CommentNode = React.createClass({
+ displayName: "CommentNode",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired,
+ mode: React.PropTypes.string,
+ },
+
+ render: function () {
+ let {object} = this.props;
+
+ let mode = this.props.mode || "short";
+
+ let {textContent} = object.preview;
+ if (mode === "tiny") {
+ textContent = cropMultipleLines(textContent, 30);
+ } else if (mode === "short") {
+ textContent = cropString(textContent, 50);
+ }
+
+ return span({className: "objectBox theme-comment"}, `<!-- ${textContent} -->`);
+ },
+ });
+
+ // Registration
+ function supportsObject(object, type) {
+ if (!isGrip(object)) {
+ return false;
+ }
+ return object.preview && object.preview.nodeType === nodeConstants.COMMENT_NODE;
+ }
+
+ // Exports from this module
+ exports.CommentNode = {
+ rep: CommentNode,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/date-time.js b/devtools/client/shared/components/reps/date-time.js
new file mode 100644
index 000000000..55dfb7d2d
--- /dev/null
+++ b/devtools/client/shared/components/reps/date-time.js
@@ -0,0 +1,70 @@
+/* -*- 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Reps
+ const { isGrip } = require("./rep-utils");
+
+ // Shortcuts
+ const { span } = React.DOM;
+
+ /**
+ * Used to render JS built-in Date() object.
+ */
+ let DateTime = React.createClass({
+ displayName: "Date",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired
+ },
+
+ getTitle: function (grip) {
+ if (this.props.objectLink) {
+ return this.props.objectLink({
+ object: grip
+ }, grip.class + " ");
+ }
+ return "";
+ },
+
+ render: function () {
+ let grip = this.props.object;
+ let date;
+ try {
+ date = span({className: "objectBox"},
+ this.getTitle(grip),
+ span({className: "Date"},
+ new Date(grip.preview.timestamp).toISOString()
+ )
+ );
+ } catch (e) {
+ date = span({className: "objectBox"}, "Invalid Date");
+ }
+ return date;
+ },
+ });
+
+ // Registration
+
+ function supportsObject(grip, type) {
+ if (!isGrip(grip)) {
+ return false;
+ }
+
+ return (type == "Date" && grip.preview);
+ }
+
+ // Exports from this module
+ exports.DateTime = {
+ rep: DateTime,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/document.js b/devtools/client/shared/components/reps/document.js
new file mode 100644
index 000000000..25e42609f
--- /dev/null
+++ b/devtools/client/shared/components/reps/document.js
@@ -0,0 +1,78 @@
+/* -*- 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Reps
+ const { isGrip, getURLDisplayString } = require("./rep-utils");
+
+ // Shortcuts
+ const { span } = React.DOM;
+
+ /**
+ * Renders DOM document object.
+ */
+ let Document = React.createClass({
+ displayName: "Document",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired
+ },
+
+ getLocation: function (grip) {
+ let location = grip.preview.location;
+ return location ? getURLDisplayString(location) : "";
+ },
+
+ getTitle: function (grip) {
+ if (this.props.objectLink) {
+ return span({className: "objectBox"},
+ this.props.objectLink({
+ object: grip
+ }, grip.class + " ")
+ );
+ }
+ return "";
+ },
+
+ getTooltip: function (doc) {
+ return doc.location.href;
+ },
+
+ render: function () {
+ let grip = this.props.object;
+
+ return (
+ span({className: "objectBox objectBox-object"},
+ this.getTitle(grip),
+ span({className: "objectPropValue"},
+ this.getLocation(grip)
+ )
+ )
+ );
+ },
+ });
+
+ // Registration
+
+ function supportsObject(object, type) {
+ if (!isGrip(object)) {
+ return false;
+ }
+
+ return (object.preview && type == "HTMLDocument");
+ }
+
+ // Exports from this module
+ exports.Document = {
+ rep: Document,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/element-node.js b/devtools/client/shared/components/reps/element-node.js
new file mode 100644
index 000000000..6315fb5b1
--- /dev/null
+++ b/devtools/client/shared/components/reps/element-node.js
@@ -0,0 +1,114 @@
+/* -*- 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const React = require("devtools/client/shared/vendor/react");
+ const { isGrip } = require("./rep-utils");
+
+ // Utils
+ const nodeConstants = require("devtools/shared/dom-node-constants");
+
+ // Shortcuts
+ const { span } = React.DOM;
+
+ /**
+ * Renders DOM element node.
+ */
+ const ElementNode = React.createClass({
+ displayName: "ElementNode",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired,
+ mode: React.PropTypes.string,
+ },
+
+ getElements: function (grip, mode) {
+ let {attributes, nodeName} = grip.preview;
+ const nodeNameElement = span({
+ className: "tag-name theme-fg-color3"
+ }, nodeName);
+
+ if (mode === "tiny") {
+ let elements = [nodeNameElement];
+ if (attributes.id) {
+ elements.push(
+ span({className: "attr-name theme-fg-color2"}, `#${attributes.id}`));
+ }
+ if (attributes.class) {
+ elements.push(
+ span({className: "attr-name theme-fg-color2"},
+ attributes.class
+ .replace(/(^\s+)|(\s+$)/g, "")
+ .split(" ")
+ .map(cls => `.${cls}`)
+ .join("")
+ )
+ );
+ }
+ return elements;
+ }
+ let attributeElements = Object.keys(attributes)
+ .sort(function getIdAndClassFirst(a1, a2) {
+ if ([a1, a2].includes("id")) {
+ return 3 * (a1 === "id" ? -1 : 1);
+ }
+ if ([a1, a2].includes("class")) {
+ return 2 * (a1 === "class" ? -1 : 1);
+ }
+
+ // `id` and `class` excepted,
+ // we want to keep the same order that in `attributes`.
+ return 0;
+ })
+ .reduce((arr, name, i, keys) => {
+ let value = attributes[name];
+ let attribute = span({},
+ span({className: "attr-name theme-fg-color2"}, `${name}`),
+ `="`,
+ span({className: "attr-value theme-fg-color6"}, `${value}`),
+ `"`
+ );
+
+ return arr.concat([" ", attribute]);
+ }, []);
+
+ return [
+ "<",
+ nodeNameElement,
+ ...attributeElements,
+ ">",
+ ];
+ },
+
+ render: function () {
+ let {object, mode} = this.props;
+ let elements = this.getElements(object, mode);
+ const baseElement = span({className: "objectBox"}, ...elements);
+
+ if (this.props.objectLink) {
+ return this.props.objectLink({object}, baseElement);
+ }
+ return baseElement;
+ },
+ });
+
+ // Registration
+ function supportsObject(object, type) {
+ if (!isGrip(object)) {
+ return false;
+ }
+ return object.preview && object.preview.nodeType === nodeConstants.ELEMENT_NODE;
+ }
+
+ // Exports from this module
+ exports.ElementNode = {
+ rep: ElementNode,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/event.js b/devtools/client/shared/components/reps/event.js
new file mode 100644
index 000000000..1d37e0150
--- /dev/null
+++ b/devtools/client/shared/components/reps/event.js
@@ -0,0 +1,81 @@
+/* -*- 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Reps
+ const { createFactories, isGrip } = require("./rep-utils");
+ const { rep } = createFactories(require("./grip").Grip);
+
+ /**
+ * Renders DOM event objects.
+ */
+ let Event = React.createClass({
+ displayName: "event",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired
+ },
+
+ render: function () {
+ // Use `Object.assign` to keep `this.props` without changes because:
+ // 1. JSON.stringify/JSON.parse is slow.
+ // 2. Immutable.js is planned for the future.
+ let props = Object.assign({}, this.props);
+ props.object = Object.assign({}, this.props.object);
+ props.object.preview = Object.assign({}, this.props.object.preview);
+ props.object.preview.ownProperties = props.object.preview.properties;
+ delete props.object.preview.properties;
+ props.object.ownPropertyLength =
+ Object.keys(props.object.preview.ownProperties).length;
+
+ switch (props.object.class) {
+ case "MouseEvent":
+ props.isInterestingProp = (type, value, name) => {
+ return (name == "clientX" ||
+ name == "clientY" ||
+ name == "layerX" ||
+ name == "layerY");
+ };
+ break;
+ case "KeyboardEvent":
+ props.isInterestingProp = (type, value, name) => {
+ return (name == "key" ||
+ name == "charCode" ||
+ name == "keyCode");
+ };
+ break;
+ case "MessageEvent":
+ props.isInterestingProp = (type, value, name) => {
+ return (name == "isTrusted" ||
+ name == "data");
+ };
+ break;
+ }
+ return rep(props);
+ }
+ });
+
+ // Registration
+
+ function supportsObject(grip, type) {
+ if (!isGrip(grip)) {
+ return false;
+ }
+
+ return (grip.preview && grip.preview.kind == "DOMEvent");
+ }
+
+ // Exports from this module
+ exports.Event = {
+ rep: Event,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/function.js b/devtools/client/shared/components/reps/function.js
new file mode 100644
index 000000000..fd20dc318
--- /dev/null
+++ b/devtools/client/shared/components/reps/function.js
@@ -0,0 +1,73 @@
+/* -*- 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Reps
+ const { isGrip, cropString } = require("./rep-utils");
+
+ // Shortcuts
+ const { span } = React.DOM;
+
+ /**
+ * This component represents a template for Function objects.
+ */
+ let Func = React.createClass({
+ displayName: "Func",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired
+ },
+
+ getTitle: function (grip) {
+ if (this.props.objectLink) {
+ return this.props.objectLink({
+ object: grip
+ }, "function ");
+ }
+ return "";
+ },
+
+ summarizeFunction: function (grip) {
+ let name = grip.userDisplayName || grip.displayName || grip.name || "function";
+ return cropString(name + "()", 100);
+ },
+
+ render: function () {
+ let grip = this.props.object;
+
+ return (
+ // Set dir="ltr" to prevent function parentheses from
+ // appearing in the wrong direction
+ span({dir: "ltr", className: "objectBox objectBox-function"},
+ this.getTitle(grip),
+ this.summarizeFunction(grip)
+ )
+ );
+ },
+ });
+
+ // Registration
+
+ function supportsObject(grip, type) {
+ if (!isGrip(grip)) {
+ return (type == "function");
+ }
+
+ return (type == "Function");
+ }
+
+ // Exports from this module
+
+ exports.Func = {
+ rep: Func,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/grip-array.js b/devtools/client/shared/components/reps/grip-array.js
new file mode 100644
index 000000000..04a5603bb
--- /dev/null
+++ b/devtools/client/shared/components/reps/grip-array.js
@@ -0,0 +1,198 @@
+/* -*- 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const React = require("devtools/client/shared/vendor/react");
+ const { createFactories, isGrip } = require("./rep-utils");
+ const { Caption } = createFactories(require("./caption"));
+
+ // Shortcuts
+ const { span } = React.DOM;
+
+ /**
+ * Renders an array. The array is enclosed by left and right bracket
+ * and the max number of rendered items depends on the current mode.
+ */
+ let GripArray = React.createClass({
+ displayName: "GripArray",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired,
+ mode: React.PropTypes.string,
+ provider: React.PropTypes.object,
+ },
+
+ getLength: function (grip) {
+ if (!grip.preview) {
+ return 0;
+ }
+
+ return grip.preview.length || grip.preview.childNodesLength || 0;
+ },
+
+ getTitle: function (object, context) {
+ let objectLink = this.props.objectLink || span;
+ if (this.props.mode != "tiny") {
+ return objectLink({
+ object: object
+ }, object.class + " ");
+ }
+ return "";
+ },
+
+ getPreviewItems: function (grip) {
+ if (!grip.preview) {
+ return null;
+ }
+
+ return grip.preview.items || grip.preview.childNodes || null;
+ },
+
+ arrayIterator: function (grip, max) {
+ let items = [];
+ const gripLength = this.getLength(grip);
+
+ if (!gripLength) {
+ return items;
+ }
+
+ const previewItems = this.getPreviewItems(grip);
+ if (!previewItems) {
+ return items;
+ }
+
+ let delim;
+ // number of grip preview items is limited to 10, but we may have more
+ // items in grip-array.
+ let delimMax = gripLength > previewItems.length ?
+ previewItems.length : previewItems.length - 1;
+ let provider = this.props.provider;
+
+ for (let i = 0; i < previewItems.length && i < max; i++) {
+ try {
+ let itemGrip = previewItems[i];
+ let value = provider ? provider.getValue(itemGrip) : itemGrip;
+
+ delim = (i == delimMax ? "" : ", ");
+
+ items.push(GripArrayItem(Object.assign({}, this.props, {
+ object: value,
+ delim: delim
+ })));
+ } catch (exc) {
+ items.push(GripArrayItem(Object.assign({}, this.props, {
+ object: exc,
+ delim: delim
+ })));
+ }
+ }
+ if (previewItems.length > max || gripLength > previewItems.length) {
+ let objectLink = this.props.objectLink || span;
+ let leftItemNum = gripLength - max > 0 ?
+ gripLength - max : gripLength - previewItems.length;
+ items.push(Caption({
+ object: objectLink({
+ object: this.props.object
+ }, leftItemNum + " more…")
+ }));
+ }
+
+ return items;
+ },
+
+ render: function () {
+ let mode = this.props.mode || "short";
+ let object = this.props.object;
+
+ let items;
+ let brackets;
+ let needSpace = function (space) {
+ return space ? { left: "[ ", right: " ]"} : { left: "[", right: "]"};
+ };
+
+ if (mode == "tiny") {
+ let objectLength = this.getLength(object);
+ let isEmpty = objectLength === 0;
+ items = [span({className: "length"}, isEmpty ? "" : objectLength)];
+ brackets = needSpace(false);
+ } else {
+ let max = (mode == "short") ? 3 : 300;
+ items = this.arrayIterator(object, max);
+ brackets = needSpace(items.length > 0);
+ }
+
+ let objectLink = this.props.objectLink || span;
+ let title = this.getTitle(object);
+
+ return (
+ span({
+ className: "objectBox objectBox-array"},
+ title,
+ objectLink({
+ className: "arrayLeftBracket",
+ object: object
+ }, brackets.left),
+ ...items,
+ objectLink({
+ className: "arrayRightBracket",
+ object: object
+ }, brackets.right),
+ span({
+ className: "arrayProperties",
+ role: "group"}
+ )
+ )
+ );
+ },
+ });
+
+ /**
+ * Renders array item. Individual values are separated by
+ * a delimiter (a comma by default).
+ */
+ let GripArrayItem = React.createFactory(React.createClass({
+ displayName: "GripArrayItem",
+
+ propTypes: {
+ delim: React.PropTypes.string,
+ },
+
+ render: function () {
+ let { Rep } = createFactories(require("./rep"));
+
+ return (
+ span({},
+ Rep(Object.assign({}, this.props, {
+ mode: "tiny"
+ })),
+ this.props.delim
+ )
+ );
+ }
+ }));
+
+ function supportsObject(grip, type) {
+ if (!isGrip(grip)) {
+ return false;
+ }
+
+ return (grip.preview && (
+ grip.preview.kind == "ArrayLike" ||
+ type === "DocumentFragment"
+ )
+ );
+ }
+
+ // Exports from this module
+ exports.GripArray = {
+ rep: GripArray,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/grip-map.js b/devtools/client/shared/components/reps/grip-map.js
new file mode 100644
index 000000000..df673d005
--- /dev/null
+++ b/devtools/client/shared/components/reps/grip-map.js
@@ -0,0 +1,193 @@
+/* -*- 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";
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const React = require("devtools/client/shared/vendor/react");
+ const { createFactories, isGrip } = require("./rep-utils");
+ const { Caption } = createFactories(require("./caption"));
+ const { PropRep } = createFactories(require("./prop-rep"));
+
+ // Shortcuts
+ const { span } = React.DOM;
+ /**
+ * Renders an map. A map is represented by a list of its
+ * entries enclosed in curly brackets.
+ */
+ const GripMap = React.createClass({
+ displayName: "GripMap",
+
+ propTypes: {
+ object: React.PropTypes.object,
+ mode: React.PropTypes.string,
+ },
+
+ getTitle: function (object) {
+ let title = object && object.class ? object.class : "Map";
+ if (this.props.objectLink) {
+ return this.props.objectLink({
+ object: object
+ }, title);
+ }
+ return title;
+ },
+
+ safeEntriesIterator: function (object, max) {
+ max = (typeof max === "undefined") ? 3 : max;
+ try {
+ return this.entriesIterator(object, max);
+ } catch (err) {
+ console.error(err);
+ }
+ return [];
+ },
+
+ entriesIterator: function (object, max) {
+ // Entry filter. Show only interesting entries to the user.
+ let isInterestingEntry = this.props.isInterestingEntry || ((type, value) => {
+ return (
+ type == "boolean" ||
+ type == "number" ||
+ (type == "string" && value.length != 0)
+ );
+ });
+
+ let mapEntries = object.preview && object.preview.entries
+ ? object.preview.entries : [];
+
+ let indexes = this.getEntriesIndexes(mapEntries, max, isInterestingEntry);
+ if (indexes.length < max && indexes.length < mapEntries.length) {
+ // There are not enough entries yet, so we add uninteresting entries.
+ indexes = indexes.concat(
+ this.getEntriesIndexes(mapEntries, max - indexes.length, (t, value, name) => {
+ return !isInterestingEntry(t, value, name);
+ })
+ );
+ }
+
+ let entries = this.getEntries(mapEntries, indexes);
+ if (entries.length < mapEntries.length) {
+ // There are some undisplayed entries. Then display "more…".
+ let objectLink = this.props.objectLink || span;
+
+ entries.push(Caption({
+ key: "more",
+ object: objectLink({
+ object: object
+ }, `${mapEntries.length - max} more…`)
+ }));
+ }
+
+ return entries;
+ },
+
+ /**
+ * Get entries ordered by index.
+ *
+ * @param {Array} entries Entries array.
+ * @param {Array} indexes Indexes of entries.
+ * @return {Array} Array of PropRep.
+ */
+ getEntries: function (entries, indexes) {
+ // Make indexes ordered by ascending.
+ indexes.sort(function (a, b) {
+ return a - b;
+ });
+
+ return indexes.map((index, i) => {
+ let [key, entryValue] = entries[index];
+ let value = entryValue.value !== undefined ? entryValue.value : entryValue;
+
+ return PropRep({
+ // key,
+ name: key,
+ equal: ": ",
+ object: value,
+ // Do not add a trailing comma on the last entry
+ // if there won't be a "more..." item.
+ delim: (i < indexes.length - 1 || indexes.length < entries.length) ? ", " : "",
+ mode: "tiny",
+ objectLink: this.props.objectLink,
+ });
+ });
+ },
+
+ /**
+ * Get the indexes of entries in the map.
+ *
+ * @param {Array} entries Entries array.
+ * @param {Number} max The maximum length of indexes array.
+ * @param {Function} filter Filter the entry you want.
+ * @return {Array} Indexes of filtered entries in the map.
+ */
+ getEntriesIndexes: function (entries, max, filter) {
+ return entries
+ .reduce((indexes, [key, entry], i) => {
+ if (indexes.length < max) {
+ let value = (entry && entry.value !== undefined) ? entry.value : entry;
+ // Type is specified in grip's "class" field and for primitive
+ // values use typeof.
+ let type = (value && value.class ? value.class : typeof value).toLowerCase();
+
+ if (filter(type, value, key)) {
+ indexes.push(i);
+ }
+ }
+
+ return indexes;
+ }, []);
+ },
+
+ render: function () {
+ let object = this.props.object;
+ let props = this.safeEntriesIterator(object,
+ (this.props.mode == "long") ? 100 : 3);
+
+ let objectLink = this.props.objectLink || span;
+ if (this.props.mode == "tiny") {
+ return (
+ span({className: "objectBox objectBox-object"},
+ this.getTitle(object),
+ objectLink({
+ className: "objectLeftBrace",
+ object: object
+ }, "")
+ )
+ );
+ }
+
+ return (
+ span({className: "objectBox objectBox-object"},
+ this.getTitle(object),
+ objectLink({
+ className: "objectLeftBrace",
+ object: object
+ }, " { "),
+ props,
+ objectLink({
+ className: "objectRightBrace",
+ object: object
+ }, " }")
+ )
+ );
+ },
+ });
+
+ function supportsObject(grip, type) {
+ if (!isGrip(grip)) {
+ return false;
+ }
+ return (grip.preview && grip.preview.kind == "MapLike");
+ }
+
+ // Exports from this module
+ exports.GripMap = {
+ rep: GripMap,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/grip.js b/devtools/client/shared/components/reps/grip.js
new file mode 100644
index 000000000..c63ee19f3
--- /dev/null
+++ b/devtools/client/shared/components/reps/grip.js
@@ -0,0 +1,247 @@
+/* -*- 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const React = require("devtools/client/shared/vendor/react");
+ // Dependencies
+ const { createFactories, isGrip } = require("./rep-utils");
+ const { Caption } = createFactories(require("./caption"));
+ const { PropRep } = createFactories(require("./prop-rep"));
+ // Shortcuts
+ const { span } = React.DOM;
+
+ /**
+ * Renders generic grip. Grip is client representation
+ * of remote JS object and is used as an input object
+ * for this rep component.
+ */
+ const GripRep = React.createClass({
+ displayName: "Grip",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired,
+ mode: React.PropTypes.string,
+ isInterestingProp: React.PropTypes.func
+ },
+
+ getTitle: function (object) {
+ if (this.props.objectLink) {
+ return this.props.objectLink({
+ object: object
+ }, object.class);
+ }
+ return object.class || "Object";
+ },
+
+ safePropIterator: function (object, max) {
+ max = (typeof max === "undefined") ? 3 : max;
+ try {
+ return this.propIterator(object, max);
+ } catch (err) {
+ console.error(err);
+ }
+ return [];
+ },
+
+ propIterator: function (object, max) {
+ if (object.preview && Object.keys(object.preview).includes("wrappedValue")) {
+ const { Rep } = createFactories(require("./rep"));
+
+ return [Rep({
+ object: object.preview.wrappedValue,
+ mode: this.props.mode || "tiny",
+ defaultRep: Grip,
+ })];
+ }
+
+ // Property filter. Show only interesting properties to the user.
+ let isInterestingProp = this.props.isInterestingProp || ((type, value) => {
+ return (
+ type == "boolean" ||
+ type == "number" ||
+ (type == "string" && value.length != 0)
+ );
+ });
+
+ let properties = object.preview
+ ? object.preview.ownProperties
+ : {};
+ let propertiesLength = object.preview && object.preview.ownPropertiesLength
+ ? object.preview.ownPropertiesLength
+ : object.ownPropertyLength;
+
+ if (object.preview && object.preview.safeGetterValues) {
+ properties = Object.assign({}, properties, object.preview.safeGetterValues);
+ propertiesLength += Object.keys(object.preview.safeGetterValues).length;
+ }
+
+ let indexes = this.getPropIndexes(properties, max, isInterestingProp);
+ if (indexes.length < max && indexes.length < propertiesLength) {
+ // There are not enough props yet. Then add uninteresting props to display them.
+ indexes = indexes.concat(
+ this.getPropIndexes(properties, max - indexes.length, (t, value, name) => {
+ return !isInterestingProp(t, value, name);
+ })
+ );
+ }
+
+ const truncate = Object.keys(properties).length > max;
+ let props = this.getProps(properties, indexes, truncate);
+ if (truncate) {
+ // There are some undisplayed props. Then display "more...".
+ let objectLink = this.props.objectLink || span;
+
+ props.push(Caption({
+ object: objectLink({
+ object: object
+ }, `${object.ownPropertyLength - max} more…`)
+ }));
+ }
+
+ return props;
+ },
+
+ /**
+ * Get props ordered by index.
+ *
+ * @param {Object} properties Props object.
+ * @param {Array} indexes Indexes of props.
+ * @param {Boolean} truncate true if the grip will be truncated.
+ * @return {Array} Props.
+ */
+ getProps: function (properties, indexes, truncate) {
+ let props = [];
+
+ // Make indexes ordered by ascending.
+ indexes.sort(function (a, b) {
+ return a - b;
+ });
+
+ indexes.forEach((i) => {
+ let name = Object.keys(properties)[i];
+ let value = this.getPropValue(properties[name]);
+
+ props.push(PropRep(Object.assign({}, this.props, {
+ mode: "tiny",
+ name: name,
+ object: value,
+ equal: ": ",
+ delim: i !== indexes.length - 1 || truncate ? ", " : "",
+ defaultRep: Grip
+ })));
+ });
+
+ return props;
+ },
+
+ /**
+ * Get the indexes of props in the object.
+ *
+ * @param {Object} properties Props object.
+ * @param {Number} max The maximum length of indexes array.
+ * @param {Function} filter Filter the props you want.
+ * @return {Array} Indexes of interesting props in the object.
+ */
+ getPropIndexes: function (properties, max, filter) {
+ let indexes = [];
+
+ try {
+ let i = 0;
+ for (let name in properties) {
+ if (indexes.length >= max) {
+ return indexes;
+ }
+
+ // Type is specified in grip's "class" field and for primitive
+ // values use typeof.
+ let value = this.getPropValue(properties[name]);
+ let type = (value.class || typeof value);
+ type = type.toLowerCase();
+
+ if (filter(type, value, name)) {
+ indexes.push(i);
+ }
+ i++;
+ }
+ } catch (err) {
+ console.error(err);
+ }
+ return indexes;
+ },
+
+ /**
+ * Get the actual value of a property.
+ *
+ * @param {Object} property
+ * @return {Object} Value of the property.
+ */
+ getPropValue: function (property) {
+ let value = property;
+ if (typeof property === "object") {
+ let keys = Object.keys(property);
+ if (keys.includes("value")) {
+ value = property.value;
+ } else if (keys.includes("getterValue")) {
+ value = property.getterValue;
+ }
+ }
+ return value;
+ },
+
+ render: function () {
+ let object = this.props.object;
+ let props = this.safePropIterator(object,
+ (this.props.mode == "long") ? 100 : 3);
+
+ let objectLink = this.props.objectLink || span;
+ if (this.props.mode == "tiny") {
+ return (
+ span({className: "objectBox objectBox-object"},
+ this.getTitle(object),
+ objectLink({
+ className: "objectLeftBrace",
+ object: object
+ }, "")
+ )
+ );
+ }
+
+ return (
+ span({className: "objectBox objectBox-object"},
+ this.getTitle(object),
+ objectLink({
+ className: "objectLeftBrace",
+ object: object
+ }, " { "),
+ ...props,
+ objectLink({
+ className: "objectRightBrace",
+ object: object
+ }, " }")
+ )
+ );
+ },
+ });
+
+ // Registration
+ function supportsObject(object, type) {
+ if (!isGrip(object)) {
+ return false;
+ }
+ return (object.preview && object.preview.ownProperties);
+ }
+
+ let Grip = {
+ rep: GripRep,
+ supportsObject: supportsObject
+ };
+
+ // Exports from this module
+ exports.Grip = Grip;
+});
diff --git a/devtools/client/shared/components/reps/infinity.js b/devtools/client/shared/components/reps/infinity.js
new file mode 100644
index 000000000..604e31f06
--- /dev/null
+++ b/devtools/client/shared/components/reps/infinity.js
@@ -0,0 +1,41 @@
+/* -*- 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Shortcuts
+ const { span } = React.DOM;
+
+ /**
+ * Renders a Infinity object
+ */
+ const InfinityRep = React.createClass({
+ displayName: "Infinity",
+
+ render: function () {
+ return (
+ span({className: "objectBox objectBox-number"},
+ this.props.object.type
+ )
+ );
+ }
+ });
+
+ function supportsObject(object, type) {
+ return type == "Infinity" || type == "-Infinity";
+ }
+
+ // Exports from this module
+ exports.InfinityRep = {
+ rep: InfinityRep,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/long-string.js b/devtools/client/shared/components/reps/long-string.js
new file mode 100644
index 000000000..f19f020dc
--- /dev/null
+++ b/devtools/client/shared/components/reps/long-string.js
@@ -0,0 +1,71 @@
+/* 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const React = require("devtools/client/shared/vendor/react");
+ const { sanitizeString, isGrip } = require("./rep-utils");
+ // Shortcuts
+ const { span } = React.DOM;
+
+ /**
+ * Renders a long string grip.
+ */
+ const LongStringRep = React.createClass({
+ displayName: "LongStringRep",
+
+ propTypes: {
+ useQuotes: React.PropTypes.bool,
+ style: React.PropTypes.object,
+ },
+
+ getDefaultProps: function () {
+ return {
+ useQuotes: true,
+ };
+ },
+
+ render: function () {
+ let {
+ cropLimit,
+ member,
+ object,
+ style,
+ useQuotes
+ } = this.props;
+ let {fullText, initial, length} = object;
+
+ let config = {className: "objectBox objectBox-string"};
+ if (style) {
+ config.style = style;
+ }
+
+ let string = member && member.open
+ ? fullText || initial
+ : initial.substring(0, cropLimit);
+
+ if (string.length < length) {
+ string += "\u2026";
+ }
+ let formattedString = useQuotes ? `"${string}"` : string;
+ return span(config, sanitizeString(formattedString));
+ },
+ });
+
+ function supportsObject(object, type) {
+ if (!isGrip(object)) {
+ return false;
+ }
+ return object.type === "longString";
+ }
+
+ // Exports from this module
+ exports.LongStringRep = {
+ rep: LongStringRep,
+ supportsObject: supportsObject,
+ };
+});
diff --git a/devtools/client/shared/components/reps/moz.build b/devtools/client/shared/components/reps/moz.build
new file mode 100644
index 000000000..f5df589f7
--- /dev/null
+++ b/devtools/client/shared/components/reps/moz.build
@@ -0,0 +1,40 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'array.js',
+ 'attribute.js',
+ 'caption.js',
+ 'comment-node.js',
+ 'date-time.js',
+ 'document.js',
+ 'element-node.js',
+ 'event.js',
+ 'function.js',
+ 'grip-array.js',
+ 'grip-map.js',
+ 'grip.js',
+ 'infinity.js',
+ 'long-string.js',
+ 'nan.js',
+ 'null.js',
+ 'number.js',
+ 'object-with-text.js',
+ 'object-with-url.js',
+ 'object.js',
+ 'promise.js',
+ 'prop-rep.js',
+ 'regexp.js',
+ 'rep-utils.js',
+ 'rep.js',
+ 'reps.css',
+ 'string.js',
+ 'stylesheet.js',
+ 'symbol.js',
+ 'text-node.js',
+ 'undefined.js',
+ 'window.js',
+)
diff --git a/devtools/client/shared/components/reps/nan.js b/devtools/client/shared/components/reps/nan.js
new file mode 100644
index 000000000..b76a5cfd3
--- /dev/null
+++ b/devtools/client/shared/components/reps/nan.js
@@ -0,0 +1,41 @@
+/* -*- 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Shortcuts
+ const { span } = React.DOM;
+
+ /**
+ * Renders a NaN object
+ */
+ const NaNRep = React.createClass({
+ displayName: "NaN",
+
+ render: function () {
+ return (
+ span({className: "objectBox objectBox-nan"},
+ "NaN"
+ )
+ );
+ }
+ });
+
+ function supportsObject(object, type) {
+ return type == "NaN";
+ }
+
+ // Exports from this module
+ exports.NaNRep = {
+ rep: NaNRep,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/null.js b/devtools/client/shared/components/reps/null.js
new file mode 100644
index 000000000..5de00f026
--- /dev/null
+++ b/devtools/client/shared/components/reps/null.js
@@ -0,0 +1,46 @@
+/* -*- 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Shortcuts
+ const { span } = React.DOM;
+
+ /**
+ * Renders null value
+ */
+ const Null = React.createClass({
+ displayName: "NullRep",
+
+ render: function () {
+ return (
+ span({className: "objectBox objectBox-null"},
+ "null"
+ )
+ );
+ },
+ });
+
+ function supportsObject(object, type) {
+ if (object && object.type && object.type == "null") {
+ return true;
+ }
+
+ return (object == null);
+ }
+
+ // Exports from this module
+
+ exports.Null = {
+ rep: Null,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/number.js b/devtools/client/shared/components/reps/number.js
new file mode 100644
index 000000000..31be3009b
--- /dev/null
+++ b/devtools/client/shared/components/reps/number.js
@@ -0,0 +1,51 @@
+/* -*- 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Shortcuts
+ const { span } = React.DOM;
+
+ /**
+ * Renders a number
+ */
+ const Number = React.createClass({
+ displayName: "Number",
+
+ stringify: function (object) {
+ let isNegativeZero = Object.is(object, -0) ||
+ (object.type && object.type == "-0");
+
+ return (isNegativeZero ? "-0" : String(object));
+ },
+
+ render: function () {
+ let value = this.props.object;
+
+ return (
+ span({className: "objectBox objectBox-number"},
+ this.stringify(value)
+ )
+ );
+ }
+ });
+
+ function supportsObject(object, type) {
+ return ["boolean", "number", "-0"].includes(type);
+ }
+
+ // Exports from this module
+
+ exports.Number = {
+ rep: Number,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/object-with-text.js b/devtools/client/shared/components/reps/object-with-text.js
new file mode 100644
index 000000000..85168ce78
--- /dev/null
+++ b/devtools/client/shared/components/reps/object-with-text.js
@@ -0,0 +1,76 @@
+/* -*- 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Reps
+ const { isGrip } = require("./rep-utils");
+
+ // Shortcuts
+ const { span } = React.DOM;
+
+ /**
+ * Renders a grip object with textual data.
+ */
+ let ObjectWithText = React.createClass({
+ displayName: "ObjectWithText",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired,
+ },
+
+ getTitle: function (grip) {
+ if (this.props.objectLink) {
+ return span({className: "objectBox"},
+ this.props.objectLink({
+ object: grip
+ }, this.getType(grip) + " ")
+ );
+ }
+ return "";
+ },
+
+ getType: function (grip) {
+ return grip.class;
+ },
+
+ getDescription: function (grip) {
+ return "\"" + grip.preview.text + "\"";
+ },
+
+ render: function () {
+ let grip = this.props.object;
+ return (
+ span({className: "objectBox objectBox-" + this.getType(grip)},
+ this.getTitle(grip),
+ span({className: "objectPropValue"},
+ this.getDescription(grip)
+ )
+ )
+ );
+ },
+ });
+
+ // Registration
+
+ function supportsObject(grip, type) {
+ if (!isGrip(grip)) {
+ return false;
+ }
+
+ return (grip.preview && grip.preview.kind == "ObjectWithText");
+ }
+
+ // Exports from this module
+ exports.ObjectWithText = {
+ rep: ObjectWithText,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/object-with-url.js b/devtools/client/shared/components/reps/object-with-url.js
new file mode 100644
index 000000000..9c4b9a229
--- /dev/null
+++ b/devtools/client/shared/components/reps/object-with-url.js
@@ -0,0 +1,76 @@
+/* -*- 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Reps
+ const { isGrip, getURLDisplayString } = require("./rep-utils");
+
+ // Shortcuts
+ const { span } = React.DOM;
+
+ /**
+ * Renders a grip object with URL data.
+ */
+ let ObjectWithURL = React.createClass({
+ displayName: "ObjectWithURL",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired,
+ },
+
+ getTitle: function (grip) {
+ if (this.props.objectLink) {
+ return span({className: "objectBox"},
+ this.props.objectLink({
+ object: grip
+ }, this.getType(grip) + " ")
+ );
+ }
+ return "";
+ },
+
+ getType: function (grip) {
+ return grip.class;
+ },
+
+ getDescription: function (grip) {
+ return getURLDisplayString(grip.preview.url);
+ },
+
+ render: function () {
+ let grip = this.props.object;
+ return (
+ span({className: "objectBox objectBox-" + this.getType(grip)},
+ this.getTitle(grip),
+ span({className: "objectPropValue"},
+ this.getDescription(grip)
+ )
+ )
+ );
+ },
+ });
+
+ // Registration
+
+ function supportsObject(grip, type) {
+ if (!isGrip(grip)) {
+ return false;
+ }
+
+ return (grip.preview && grip.preview.kind == "ObjectWithURL");
+ }
+
+ // Exports from this module
+ exports.ObjectWithURL = {
+ rep: ObjectWithURL,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/object.js b/devtools/client/shared/components/reps/object.js
new file mode 100644
index 000000000..ffb1d1525
--- /dev/null
+++ b/devtools/client/shared/components/reps/object.js
@@ -0,0 +1,171 @@
+/* -*- 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";
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const React = require("devtools/client/shared/vendor/react");
+ const { createFactories } = require("./rep-utils");
+ const { Caption } = createFactories(require("./caption"));
+ const { PropRep } = createFactories(require("./prop-rep"));
+ // Shortcuts
+ const { span } = React.DOM;
+ /**
+ * Renders an object. An object is represented by a list of its
+ * properties enclosed in curly brackets.
+ */
+ const Obj = React.createClass({
+ displayName: "Obj",
+
+ propTypes: {
+ object: React.PropTypes.object,
+ mode: React.PropTypes.string,
+ },
+
+ getTitle: function (object) {
+ let className = object && object.class ? object.class : "Object";
+ if (this.props.objectLink) {
+ return this.props.objectLink({
+ object: object
+ }, className);
+ }
+ return className;
+ },
+
+ safePropIterator: function (object, max) {
+ max = (typeof max === "undefined") ? 3 : max;
+ try {
+ return this.propIterator(object, max);
+ } catch (err) {
+ console.error(err);
+ }
+ return [];
+ },
+
+ propIterator: function (object, max) {
+ let isInterestingProp = (t, value) => {
+ // Do not pick objects, it could cause recursion.
+ return (t == "boolean" || t == "number" || (t == "string" && value));
+ };
+
+ // Work around https://bugzilla.mozilla.org/show_bug.cgi?id=945377
+ if (Object.prototype.toString.call(object) === "[object Generator]") {
+ object = Object.getPrototypeOf(object);
+ }
+
+ // Object members with non-empty values are preferred since it gives the
+ // user a better overview of the object.
+ let props = this.getProps(object, max, isInterestingProp);
+
+ if (props.length <= max) {
+ // There are not enough props yet (or at least, not enough props to
+ // be able to know whether we should print "more…" or not).
+ // Let's display also empty members and functions.
+ props = props.concat(this.getProps(object, max, (t, value) => {
+ return !isInterestingProp(t, value);
+ }));
+ }
+
+ if (props.length > max) {
+ props.pop();
+ let objectLink = this.props.objectLink || span;
+
+ props.push(Caption({
+ object: objectLink({
+ object: object
+ }, (Object.keys(object).length - max) + " more…")
+ }));
+ } else if (props.length > 0) {
+ // Remove the last comma.
+ props[props.length - 1] = React.cloneElement(
+ props[props.length - 1], { delim: "" });
+ }
+
+ return props;
+ },
+
+ getProps: function (object, max, filter) {
+ let props = [];
+
+ max = max || 3;
+ if (!object) {
+ return props;
+ }
+
+ // Hardcode tiny mode to avoid recursive handling.
+ let mode = "tiny";
+
+ try {
+ for (let name in object) {
+ if (props.length > max) {
+ return props;
+ }
+
+ let value;
+ try {
+ value = object[name];
+ } catch (exc) {
+ continue;
+ }
+
+ let t = typeof value;
+ if (filter(t, value)) {
+ props.push(PropRep({
+ mode: mode,
+ name: name,
+ object: value,
+ equal: ": ",
+ delim: ", ",
+ }));
+ }
+ }
+ } catch (err) {
+ console.error(err);
+ }
+
+ return props;
+ },
+
+ render: function () {
+ let object = this.props.object;
+ let props = this.safePropIterator(object);
+ let objectLink = this.props.objectLink || span;
+
+ if (this.props.mode == "tiny" || !props.length) {
+ return (
+ span({className: "objectBox objectBox-object"},
+ objectLink({className: "objectTitle"}, this.getTitle(object))
+ )
+ );
+ }
+
+ return (
+ span({className: "objectBox objectBox-object"},
+ this.getTitle(object),
+ objectLink({
+ className: "objectLeftBrace",
+ object: object
+ }, " { "),
+ ...props,
+ objectLink({
+ className: "objectRightBrace",
+ object: object
+ }, " }")
+ )
+ );
+ },
+ });
+ function supportsObject(object, type) {
+ return true;
+ }
+
+ // Exports from this module
+ exports.Obj = {
+ rep: Obj,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/promise.js b/devtools/client/shared/components/reps/promise.js
new file mode 100644
index 000000000..0a903d366
--- /dev/null
+++ b/devtools/client/shared/components/reps/promise.js
@@ -0,0 +1,111 @@
+/* -*- 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const React = require("devtools/client/shared/vendor/react");
+ // Dependencies
+ const { createFactories, isGrip } = require("./rep-utils");
+ const { PropRep } = createFactories(require("./prop-rep"));
+ // Shortcuts
+ const { span } = React.DOM;
+
+ /**
+ * Renders a DOM Promise object.
+ */
+ const PromiseRep = React.createClass({
+ displayName: "Promise",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired,
+ mode: React.PropTypes.string,
+ },
+
+ getTitle: function (object) {
+ const title = object.class;
+ if (this.props.objectLink) {
+ return this.props.objectLink({
+ object: object
+ }, title);
+ }
+ return title;
+ },
+
+ getProps: function (promiseState) {
+ const keys = ["state"];
+ if (Object.keys(promiseState).includes("value")) {
+ keys.push("value");
+ }
+
+ return keys.map((key, i) => {
+ return PropRep(Object.assign({}, this.props, {
+ mode: "tiny",
+ name: `<${key}>`,
+ object: promiseState[key],
+ equal: ": ",
+ delim: i < keys.length - 1 ? ", " : ""
+ }));
+ });
+ },
+
+ render: function () {
+ const object = this.props.object;
+ const {promiseState} = object;
+ let objectLink = this.props.objectLink || span;
+
+ if (this.props.mode == "tiny") {
+ let { Rep } = createFactories(require("./rep"));
+
+ return (
+ span({className: "objectBox objectBox-object"},
+ this.getTitle(object),
+ objectLink({
+ className: "objectLeftBrace",
+ object: object
+ }, " { "),
+ Rep({object: promiseState.state}),
+ objectLink({
+ className: "objectRightBrace",
+ object: object
+ }, " }")
+ )
+ );
+ }
+
+ const props = this.getProps(promiseState);
+ return (
+ span({className: "objectBox objectBox-object"},
+ this.getTitle(object),
+ objectLink({
+ className: "objectLeftBrace",
+ object: object
+ }, " { "),
+ ...props,
+ objectLink({
+ className: "objectRightBrace",
+ object: object
+ }, " }")
+ )
+ );
+ },
+ });
+
+ // Registration
+ function supportsObject(object, type) {
+ if (!isGrip(object)) {
+ return false;
+ }
+ return type === "Promise";
+ }
+
+ // Exports from this module
+ exports.PromiseRep = {
+ rep: PromiseRep,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/prop-rep.js b/devtools/client/shared/components/reps/prop-rep.js
new file mode 100644
index 000000000..775dfea2b
--- /dev/null
+++ b/devtools/client/shared/components/reps/prop-rep.js
@@ -0,0 +1,70 @@
+/* -*- 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ const React = require("devtools/client/shared/vendor/react");
+ const { createFactories } = require("./rep-utils");
+ const { span } = React.DOM;
+
+ /**
+ * Property for Obj (local JS objects), Grip (remote JS objects)
+ * and GripMap (remote JS maps and weakmaps) reps.
+ * It's used to render object properties.
+ */
+ let PropRep = React.createFactory(React.createClass({
+ displayName: "PropRep",
+
+ propTypes: {
+ // Property name.
+ name: React.PropTypes.oneOfType([
+ React.PropTypes.string,
+ React.PropTypes.object,
+ ]).isRequired,
+ // Equal character rendered between property name and value.
+ equal: React.PropTypes.string,
+ // Delimiter character used to separate individual properties.
+ delim: React.PropTypes.string,
+ mode: React.PropTypes.string,
+ },
+
+ render: function () {
+ const { Grip } = require("./grip");
+ let { Rep } = createFactories(require("./rep"));
+
+ let key;
+ // The key can be a simple string, for plain objects,
+ // or another object for maps and weakmaps.
+ if (typeof this.props.name === "string") {
+ key = span({"className": "nodeName"}, this.props.name);
+ } else {
+ key = Rep({
+ object: this.props.name,
+ mode: this.props.mode || "tiny",
+ defaultRep: Grip,
+ objectLink: this.props.objectLink,
+ });
+ }
+
+ return (
+ span({},
+ key,
+ span({
+ "className": "objectEqual"
+ }, this.props.equal),
+ Rep(this.props),
+ span({
+ "className": "objectComma"
+ }, this.props.delim)
+ )
+ );
+ }
+ }));
+
+ // Exports from this module
+ exports.PropRep = PropRep;
+});
diff --git a/devtools/client/shared/components/reps/regexp.js b/devtools/client/shared/components/reps/regexp.js
new file mode 100644
index 000000000..2f9212658
--- /dev/null
+++ b/devtools/client/shared/components/reps/regexp.js
@@ -0,0 +1,63 @@
+/* -*- 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Reps
+ const { isGrip } = require("./rep-utils");
+
+ // Shortcuts
+ const { span } = React.DOM;
+
+ /**
+ * Renders a grip object with regular expression.
+ */
+ let RegExp = React.createClass({
+ displayName: "regexp",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired,
+ },
+
+ getSource: function (grip) {
+ return grip.displayString;
+ },
+
+ render: function () {
+ let grip = this.props.object;
+ let objectLink = this.props.objectLink || span;
+
+ return (
+ span({className: "objectBox objectBox-regexp"},
+ objectLink({
+ object: grip,
+ className: "regexpSource"
+ }, this.getSource(grip))
+ )
+ );
+ },
+ });
+
+ // Registration
+
+ function supportsObject(object, type) {
+ if (!isGrip(object)) {
+ return false;
+ }
+
+ return (type == "RegExp");
+ }
+
+ // Exports from this module
+ exports.RegExp = {
+ rep: RegExp,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/rep-utils.js b/devtools/client/shared/components/reps/rep-utils.js
new file mode 100644
index 000000000..d9580ac8d
--- /dev/null
+++ b/devtools/client/shared/components/reps/rep-utils.js
@@ -0,0 +1,160 @@
+/* globals URLSearchParams */
+/* -*- 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const React = require("devtools/client/shared/vendor/react");
+
+ /**
+ * Create React factories for given arguments.
+ * Example:
+ * const { Rep } = createFactories(require("./rep"));
+ */
+ function createFactories(args) {
+ let result = {};
+ for (let p in args) {
+ result[p] = React.createFactory(args[p]);
+ }
+ return result;
+ }
+
+ /**
+ * Returns true if the given object is a grip (see RDP protocol)
+ */
+ function isGrip(object) {
+ return object && object.actor;
+ }
+
+ function escapeNewLines(value) {
+ return value.replace(/\r/gm, "\\r").replace(/\n/gm, "\\n");
+ }
+
+ function cropMultipleLines(text, limit) {
+ return escapeNewLines(cropString(text, limit));
+ }
+
+ function cropString(text, limit, alternativeText) {
+ if (!alternativeText) {
+ alternativeText = "\u2026";
+ }
+
+ // Make sure it's a string and sanitize it.
+ text = sanitizeString(text + "");
+
+ // Crop the string only if a limit is actually specified.
+ if (!limit || limit <= 0) {
+ return text;
+ }
+
+ // Set the limit at least to the length of the alternative text
+ // plus one character of the original text.
+ if (limit <= alternativeText.length) {
+ limit = alternativeText.length + 1;
+ }
+
+ let halfLimit = (limit - alternativeText.length) / 2;
+
+ if (text.length > limit) {
+ return text.substr(0, Math.ceil(halfLimit)) + alternativeText +
+ text.substr(text.length - Math.floor(halfLimit));
+ }
+
+ return text;
+ }
+
+ function sanitizeString(text) {
+ // Replace all non-printable characters, except of
+ // (horizontal) tab (HT: \x09) and newline (LF: \x0A, CR: \x0D),
+ // with unicode replacement character (u+fffd).
+ // eslint-disable-next-line no-control-regex
+ let re = new RegExp("[\x00-\x08\x0B\x0C\x0E-\x1F\x80-\x9F]", "g");
+ return text.replace(re, "\ufffd");
+ }
+
+ function parseURLParams(url) {
+ url = new URL(url);
+ return parseURLEncodedText(url.searchParams);
+ }
+
+ function parseURLEncodedText(text) {
+ let params = [];
+
+ // In case the text is empty just return the empty parameters
+ if (text == "") {
+ return params;
+ }
+
+ let searchParams = new URLSearchParams(text);
+ let entries = [...searchParams.entries()];
+ return entries.map(entry => {
+ return {
+ name: entry[0],
+ value: entry[1]
+ };
+ });
+ }
+
+ function getFileName(url) {
+ let split = splitURLBase(url);
+ return split.name;
+ }
+
+ function splitURLBase(url) {
+ if (!isDataURL(url)) {
+ return splitURLTrue(url);
+ }
+ return {};
+ }
+
+ function getURLDisplayString(url) {
+ return cropString(url);
+ }
+
+ function isDataURL(url) {
+ return (url && url.substr(0, 5) == "data:");
+ }
+
+ function splitURLTrue(url) {
+ const reSplitFile = /(.*?):\/{2,3}([^\/]*)(.*?)([^\/]*?)($|\?.*)/;
+ let m = reSplitFile.exec(url);
+
+ if (!m) {
+ return {
+ name: url,
+ path: url
+ };
+ } else if (m[4] == "" && m[5] == "") {
+ return {
+ protocol: m[1],
+ domain: m[2],
+ path: m[3],
+ name: m[3] != "/" ? m[3] : m[2]
+ };
+ }
+
+ return {
+ protocol: m[1],
+ domain: m[2],
+ path: m[2] + m[3],
+ name: m[4] + m[5]
+ };
+ }
+
+ // Exports from this module
+ exports.createFactories = createFactories;
+ exports.isGrip = isGrip;
+ exports.cropString = cropString;
+ exports.cropMultipleLines = cropMultipleLines;
+ exports.parseURLParams = parseURLParams;
+ exports.parseURLEncodedText = parseURLEncodedText;
+ exports.getFileName = getFileName;
+ exports.getURLDisplayString = getURLDisplayString;
+ exports.sanitizeString = sanitizeString;
+});
diff --git a/devtools/client/shared/components/reps/rep.js b/devtools/client/shared/components/reps/rep.js
new file mode 100644
index 000000000..0891fe0ce
--- /dev/null
+++ b/devtools/client/shared/components/reps/rep.js
@@ -0,0 +1,144 @@
+/* -*- 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const React = require("devtools/client/shared/vendor/react");
+
+ const { isGrip } = require("./rep-utils");
+
+ // Load all existing rep templates
+ const { Undefined } = require("./undefined");
+ const { Null } = require("./null");
+ const { StringRep } = require("./string");
+ const { LongStringRep } = require("./long-string");
+ const { Number } = require("./number");
+ const { ArrayRep } = require("./array");
+ const { Obj } = require("./object");
+ const { SymbolRep } = require("./symbol");
+ const { InfinityRep } = require("./infinity");
+ const { NaNRep } = require("./nan");
+
+ // DOM types (grips)
+ const { Attribute } = require("./attribute");
+ const { DateTime } = require("./date-time");
+ const { Document } = require("./document");
+ const { Event } = require("./event");
+ const { Func } = require("./function");
+ const { PromiseRep } = require("./promise");
+ const { RegExp } = require("./regexp");
+ const { StyleSheet } = require("./stylesheet");
+ const { CommentNode } = require("./comment-node");
+ const { ElementNode } = require("./element-node");
+ const { TextNode } = require("./text-node");
+ const { Window } = require("./window");
+ const { ObjectWithText } = require("./object-with-text");
+ const { ObjectWithURL } = require("./object-with-url");
+ const { GripArray } = require("./grip-array");
+ const { GripMap } = require("./grip-map");
+ const { Grip } = require("./grip");
+
+ // List of all registered template.
+ // XXX there should be a way for extensions to register a new
+ // or modify an existing rep.
+ let reps = [
+ RegExp,
+ StyleSheet,
+ Event,
+ DateTime,
+ CommentNode,
+ ElementNode,
+ TextNode,
+ Attribute,
+ LongStringRep,
+ Func,
+ PromiseRep,
+ ArrayRep,
+ Document,
+ Window,
+ ObjectWithText,
+ ObjectWithURL,
+ GripArray,
+ GripMap,
+ Grip,
+ Undefined,
+ Null,
+ StringRep,
+ Number,
+ SymbolRep,
+ InfinityRep,
+ NaNRep,
+ ];
+
+ /**
+ * Generic rep that is using for rendering native JS types or an object.
+ * The right template used for rendering is picked automatically according
+ * to the current value type. The value must be passed is as 'object'
+ * property.
+ */
+ const Rep = React.createClass({
+ displayName: "Rep",
+
+ propTypes: {
+ object: React.PropTypes.any,
+ defaultRep: React.PropTypes.object,
+ mode: React.PropTypes.string
+ },
+
+ render: function () {
+ let rep = getRep(this.props.object, this.props.defaultRep);
+ return rep(this.props);
+ },
+ });
+
+ // Helpers
+
+ /**
+ * Return a rep object that is responsible for rendering given
+ * object.
+ *
+ * @param object {Object} Object to be rendered in the UI. This
+ * can be generic JS object as well as a grip (handle to a remote
+ * debuggee object).
+ *
+ * @param defaultObject {React.Component} The default template
+ * that should be used to render given object if none is found.
+ */
+ function getRep(object, defaultRep = Obj) {
+ let type = typeof object;
+ if (type == "object" && object instanceof String) {
+ type = "string";
+ } else if (object && type == "object" && object.type) {
+ type = object.type;
+ }
+
+ if (isGrip(object)) {
+ type = object.class;
+ }
+
+ for (let i = 0; i < reps.length; i++) {
+ let rep = reps[i];
+ try {
+ // supportsObject could return weight (not only true/false
+ // but a number), which would allow to priorities templates and
+ // support better extensibility.
+ if (rep.supportsObject(object, type)) {
+ return React.createFactory(rep.rep);
+ }
+ } catch (err) {
+ console.error(err);
+ }
+ }
+
+ return React.createFactory(defaultRep.rep);
+ }
+
+ // Exports from this module
+ exports.Rep = Rep;
+});
diff --git a/devtools/client/shared/components/reps/reps.css b/devtools/client/shared/components/reps/reps.css
new file mode 100644
index 000000000..61e5e3dac
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps.css
@@ -0,0 +1,174 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+.theme-dark,
+.theme-light {
+ --number-color: var(--theme-highlight-green);
+ --string-color: var(--theme-highlight-orange);
+ --null-color: var(--theme-comment);
+ --object-color: var(--theme-body-color);
+ --caption-color: var(--theme-highlight-blue);
+ --location-color: var(--theme-content-color1);
+ --source-link-color: var(--theme-highlight-blue);
+ --node-color: var(--theme-highlight-bluegrey);
+ --reference-color: var(--theme-highlight-purple);
+}
+
+.theme-firebug {
+ --number-color: #000088;
+ --string-color: #FF0000;
+ --null-color: #787878;
+ --object-color: DarkGreen;
+ --caption-color: #444444;
+ --location-color: #555555;
+ --source-link-color: blue;
+ --node-color: rgb(0, 0, 136);
+ --reference-color: rgb(102, 102, 255);
+}
+
+/******************************************************************************/
+
+.objectLink:hover {
+ cursor: pointer;
+ text-decoration: underline;
+}
+
+.inline {
+ display: inline;
+ white-space: normal;
+}
+
+.objectBox-object {
+ font-weight: bold;
+ color: var(--object-color);
+ white-space: pre-wrap;
+}
+
+.objectBox-string,
+.objectBox-symbol,
+.objectBox-text,
+.objectLink-textNode,
+.objectBox-table {
+ white-space: pre-wrap;
+}
+
+.objectBox-number,
+.objectLink-styleRule,
+.objectLink-element,
+.objectLink-textNode,
+.objectBox-array > .length {
+ color: var(--number-color);
+}
+
+.objectBox-textNode,
+.objectBox-string,
+.objectBox-symbol {
+ color: var(--string-color);
+}
+
+.objectLink-function,
+.objectBox-stackTrace,
+.objectLink-profile {
+ color: var(--object-color);
+}
+
+.objectLink-Location {
+ font-style: italic;
+ color: var(--location-color);
+}
+
+.objectBox-null,
+.objectBox-undefined,
+.objectBox-hint,
+.logRowHint {
+ font-style: italic;
+ color: var(--null-color);
+}
+
+.objectLink-sourceLink {
+ position: absolute;
+ right: 4px;
+ top: 2px;
+ padding-left: 8px;
+ font-weight: bold;
+ color: var(--source-link-color);
+}
+
+/******************************************************************************/
+
+.objectLink-event,
+.objectLink-eventLog,
+.objectLink-regexp,
+.objectLink-object,
+.objectLink-Date {
+ font-weight: bold;
+ color: var(--object-color);
+ white-space: pre-wrap;
+}
+
+/******************************************************************************/
+
+.objectLink-object .nodeName,
+.objectLink-NamedNodeMap .nodeName,
+.objectLink-NamedNodeMap .objectEqual,
+.objectLink-NamedNodeMap .arrayLeftBracket,
+.objectLink-NamedNodeMap .arrayRightBracket,
+.objectLink-Attr .attrEqual,
+.objectLink-Attr .attrTitle {
+ color: var(--node-color);
+}
+
+.objectLink-object .nodeName {
+ font-weight: normal;
+}
+
+/******************************************************************************/
+
+.objectLeftBrace,
+.objectRightBrace,
+.arrayLeftBracket,
+.arrayRightBracket {
+ cursor: pointer;
+ font-weight: bold;
+}
+
+/******************************************************************************/
+/* Cycle reference*/
+
+.objectLink-Reference {
+ font-weight: bold;
+ color: var(--reference-color);
+}
+
+.objectBox-array > .objectTitle {
+ font-weight: bold;
+ color: var(--object-color);
+}
+
+.caption {
+ font-weight: bold;
+ color: var(--caption-color);
+}
+
+/******************************************************************************/
+/* Themes */
+
+.theme-dark .objectBox-null,
+.theme-dark .objectBox-undefined,
+.theme-light .objectBox-null,
+.theme-light .objectBox-undefined {
+ font-style: normal;
+}
+
+.theme-dark .objectBox-object,
+.theme-light .objectBox-object {
+ font-weight: normal;
+ white-space: pre-wrap;
+}
+
+.theme-dark .caption,
+.theme-light .caption {
+ font-weight: normal;
+}
diff --git a/devtools/client/shared/components/reps/string.js b/devtools/client/shared/components/reps/string.js
new file mode 100644
index 000000000..f8b4b1986
--- /dev/null
+++ b/devtools/client/shared/components/reps/string.js
@@ -0,0 +1,69 @@
+/* -*- 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const React = require("devtools/client/shared/vendor/react");
+ const { cropString } = require("./rep-utils");
+
+ // Shortcuts
+ const { span } = React.DOM;
+
+ /**
+ * Renders a string. String value is enclosed within quotes.
+ */
+ const StringRep = React.createClass({
+ displayName: "StringRep",
+
+ propTypes: {
+ useQuotes: React.PropTypes.bool,
+ style: React.PropTypes.object,
+ },
+
+ getDefaultProps: function () {
+ return {
+ useQuotes: true,
+ };
+ },
+
+ render: function () {
+ let text = this.props.object;
+ let member = this.props.member;
+ let style = this.props.style;
+
+ let config = {className: "objectBox objectBox-string"};
+ if (style) {
+ config.style = style;
+ }
+
+ if (member && member.open) {
+ return span(config, "\"" + text + "\"");
+ }
+
+ let croppedString = this.props.cropLimit ?
+ cropString(text, this.props.cropLimit) : cropString(text);
+
+ let formattedString = this.props.useQuotes ?
+ "\"" + croppedString + "\"" : croppedString;
+
+ return span(config, formattedString);
+ },
+ });
+
+ function supportsObject(object, type) {
+ return (type == "string");
+ }
+
+ // Exports from this module
+
+ exports.StringRep = {
+ rep: StringRep,
+ supportsObject: supportsObject,
+ };
+});
diff --git a/devtools/client/shared/components/reps/stylesheet.js b/devtools/client/shared/components/reps/stylesheet.js
new file mode 100644
index 000000000..c1fc7f1be
--- /dev/null
+++ b/devtools/client/shared/components/reps/stylesheet.js
@@ -0,0 +1,77 @@
+/* -*- 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Reps
+ const { isGrip, getURLDisplayString } = require("./rep-utils");
+
+ // Shortcuts
+ const DOM = React.DOM;
+
+ /**
+ * Renders a grip representing CSSStyleSheet
+ */
+ let StyleSheet = React.createClass({
+ displayName: "object",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired,
+ },
+
+ getTitle: function (grip) {
+ let title = "StyleSheet ";
+ if (this.props.objectLink) {
+ return DOM.span({className: "objectBox"},
+ this.props.objectLink({
+ object: grip
+ }, title)
+ );
+ }
+ return title;
+ },
+
+ getLocation: function (grip) {
+ // Embedded stylesheets don't have URL and so, no preview.
+ let url = grip.preview ? grip.preview.url : "";
+ return url ? getURLDisplayString(url) : "";
+ },
+
+ render: function () {
+ let grip = this.props.object;
+
+ return (
+ DOM.span({className: "objectBox objectBox-object"},
+ this.getTitle(grip),
+ DOM.span({className: "objectPropValue"},
+ this.getLocation(grip)
+ )
+ )
+ );
+ },
+ });
+
+ // Registration
+
+ function supportsObject(object, type) {
+ if (!isGrip(object)) {
+ return false;
+ }
+
+ return (type == "CSSStyleSheet");
+ }
+
+ // Exports from this module
+
+ exports.StyleSheet = {
+ rep: StyleSheet,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/symbol.js b/devtools/client/shared/components/reps/symbol.js
new file mode 100644
index 000000000..111794008
--- /dev/null
+++ b/devtools/client/shared/components/reps/symbol.js
@@ -0,0 +1,48 @@
+/* -*- 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Shortcuts
+ const { span } = React.DOM;
+
+ /**
+ * Renders a symbol.
+ */
+ const SymbolRep = React.createClass({
+ displayName: "SymbolRep",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired
+ },
+
+ render: function () {
+ let {object} = this.props;
+ let {name} = object;
+
+ return (
+ span({className: "objectBox objectBox-symbol"},
+ `Symbol(${name || ""})`
+ )
+ );
+ },
+ });
+
+ function supportsObject(object, type) {
+ return (type == "symbol");
+ }
+
+ // Exports from this module
+ exports.SymbolRep = {
+ rep: SymbolRep,
+ supportsObject: supportsObject,
+ };
+});
diff --git a/devtools/client/shared/components/reps/text-node.js b/devtools/client/shared/components/reps/text-node.js
new file mode 100644
index 000000000..d80545cea
--- /dev/null
+++ b/devtools/client/shared/components/reps/text-node.js
@@ -0,0 +1,94 @@
+/* -*- 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Reps
+ const { isGrip, cropString } = require("./rep-utils");
+
+ // Shortcuts
+ const DOM = React.DOM;
+
+ /**
+ * Renders DOM #text node.
+ */
+ let TextNode = React.createClass({
+ displayName: "TextNode",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired,
+ mode: React.PropTypes.string,
+ },
+
+ getTextContent: function (grip) {
+ return cropString(grip.preview.textContent);
+ },
+
+ getTitle: function (grip) {
+ if (this.props.objectLink) {
+ return this.props.objectLink({
+ object: grip
+ }, "#text ");
+ }
+ return "";
+ },
+
+ render: function () {
+ let grip = this.props.object;
+ let mode = this.props.mode || "short";
+
+ if (mode == "short" || mode == "tiny") {
+ return (
+ DOM.span({className: "objectBox objectBox-textNode"},
+ this.getTitle(grip),
+ DOM.span({className: "nodeValue"},
+ "\"" + this.getTextContent(grip) + "\""
+ )
+ )
+ );
+ }
+
+ let objectLink = this.props.objectLink || DOM.span;
+ return (
+ DOM.span({className: "objectBox objectBox-textNode"},
+ this.getTitle(grip),
+ objectLink({
+ object: grip
+ }, "<"),
+ DOM.span({className: "nodeTag"}, "TextNode"),
+ " textContent=\"",
+ DOM.span({className: "nodeValue"},
+ this.getTextContent(grip)
+ ),
+ "\"",
+ objectLink({
+ object: grip
+ }, ">;")
+ )
+ );
+ },
+ });
+
+ // Registration
+
+ function supportsObject(grip, type) {
+ if (!isGrip(grip)) {
+ return false;
+ }
+
+ return (grip.preview && grip.class == "Text");
+ }
+
+ // Exports from this module
+ exports.TextNode = {
+ rep: TextNode,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/undefined.js b/devtools/client/shared/components/reps/undefined.js
new file mode 100644
index 000000000..c4e64a12c
--- /dev/null
+++ b/devtools/client/shared/components/reps/undefined.js
@@ -0,0 +1,46 @@
+/* -*- 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Shortcuts
+ const { span } = React.DOM;
+
+ /**
+ * Renders undefined value
+ */
+ const Undefined = React.createClass({
+ displayName: "UndefinedRep",
+
+ render: function () {
+ return (
+ span({className: "objectBox objectBox-undefined"},
+ "undefined"
+ )
+ );
+ },
+ });
+
+ function supportsObject(object, type) {
+ if (object && object.type && object.type == "undefined") {
+ return true;
+ }
+
+ return (type == "undefined");
+ }
+
+ // Exports from this module
+
+ exports.Undefined = {
+ rep: Undefined,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/window.js b/devtools/client/shared/components/reps/window.js
new file mode 100644
index 000000000..628d69562
--- /dev/null
+++ b/devtools/client/shared/components/reps/window.js
@@ -0,0 +1,73 @@
+/* -*- 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Reps
+ const { isGrip, getURLDisplayString } = require("./rep-utils");
+
+ // Shortcuts
+ const DOM = React.DOM;
+
+ /**
+ * Renders a grip representing a window.
+ */
+ let Window = React.createClass({
+ displayName: "Window",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired,
+ },
+
+ getTitle: function (grip) {
+ if (this.props.objectLink) {
+ return DOM.span({className: "objectBox"},
+ this.props.objectLink({
+ object: grip
+ }, grip.class + " ")
+ );
+ }
+ return "";
+ },
+
+ getLocation: function (grip) {
+ return getURLDisplayString(grip.preview.url);
+ },
+
+ render: function () {
+ let grip = this.props.object;
+
+ return (
+ DOM.span({className: "objectBox objectBox-Window"},
+ this.getTitle(grip),
+ DOM.span({className: "objectPropValue"},
+ this.getLocation(grip)
+ )
+ )
+ );
+ },
+ });
+
+ // Registration
+
+ function supportsObject(object, type) {
+ if (!isGrip(object)) {
+ return false;
+ }
+
+ return (object.preview && type == "Window");
+ }
+
+ // Exports from this module
+ exports.Window = {
+ rep: Window,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/search-box.js b/devtools/client/shared/components/search-box.js
new file mode 100644
index 000000000..bd572f8b2
--- /dev/null
+++ b/devtools/client/shared/components/search-box.js
@@ -0,0 +1,110 @@
+/* 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/. */
+
+/* global window */
+
+"use strict";
+
+const { DOM: dom, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
+const {KeyShortcuts} = require("devtools/client/shared/key-shortcuts");
+
+/**
+ * A generic search box component for use across devtools
+ */
+module.exports = createClass({
+ displayName: "SearchBox",
+
+ propTypes: {
+ delay: PropTypes.number,
+ keyShortcut: PropTypes.string,
+ onChange: PropTypes.func,
+ placeholder: PropTypes.string,
+ type: PropTypes.string
+ },
+
+ getInitialState() {
+ return {
+ value: ""
+ };
+ },
+
+ componentDidMount() {
+ if (!this.props.keyShortcut) {
+ return;
+ }
+
+ this.shortcuts = new KeyShortcuts({
+ window
+ });
+ this.shortcuts.on(this.props.keyShortcut, (name, event) => {
+ event.preventDefault();
+ this.refs.input.focus();
+ });
+ },
+
+ componentWillUnmount() {
+ if (this.shortcuts) {
+ this.shortcuts.destroy();
+ }
+
+ // Clean up an existing timeout.
+ if (this.searchTimeout) {
+ clearTimeout(this.searchTimeout);
+ }
+ },
+
+ onChange() {
+ if (this.state.value !== this.refs.input.value) {
+ this.setState({ value: this.refs.input.value });
+ }
+
+ if (!this.props.delay) {
+ this.props.onChange(this.state.value);
+ return;
+ }
+
+ // Clean up an existing timeout before creating a new one.
+ if (this.searchTimeout) {
+ clearTimeout(this.searchTimeout);
+ }
+
+ // Execute the search after a timeout. It makes the UX
+ // smoother if the user is typing quickly.
+ this.searchTimeout = setTimeout(() => {
+ this.searchTimeout = null;
+ this.props.onChange(this.state.value);
+ }, this.props.delay);
+ },
+
+ onClearButtonClick() {
+ this.refs.input.value = "";
+ this.onChange();
+ },
+
+ render() {
+ let { type = "search", placeholder } = this.props;
+ let { value } = this.state;
+ let divClassList = ["devtools-searchbox", "has-clear-btn"];
+ let inputClassList = [`devtools-${type}input`];
+
+ if (value !== "") {
+ inputClassList.push("filled");
+ }
+ return dom.div(
+ { className: divClassList.join(" ") },
+ dom.input({
+ className: inputClassList.join(" "),
+ onChange: this.onChange,
+ placeholder,
+ ref: "input",
+ value
+ }),
+ dom.button({
+ className: "devtools-searchinput-clear",
+ hidden: value == "",
+ onClick: this.onClearButtonClick
+ })
+ );
+ }
+});
diff --git a/devtools/client/shared/components/sidebar-toggle.css b/devtools/client/shared/components/sidebar-toggle.css
new file mode 100644
index 000000000..659a3d23f
--- /dev/null
+++ b/devtools/client/shared/components/sidebar-toggle.css
@@ -0,0 +1,32 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+.sidebar-toggle {
+ display: block;
+}
+
+.sidebar-toggle::before,
+.sidebar-toggle.pane-collapsed:dir(rtl)::before {
+ background-image: var(--theme-pane-collapse-image);
+}
+
+.sidebar-toggle.pane-collapsed::before,
+.sidebar-toggle:dir(rtl)::before {
+ background-image: var(--theme-pane-expand-image);
+}
+
+/* Rotate button icon 90deg if the toolbox container is
+ in vertical mode (sidebar displayed under the main panel) */
+@media (max-width: 700px) {
+ .sidebar-toggle::before {
+ transform: rotate(90deg);
+ }
+
+ /* Since RTL swaps the used images, we need to flip them
+ the other way round */
+ .sidebar-toggle:dir(rtl)::before {
+ transform: rotate(-90deg);
+ }
+}
diff --git a/devtools/client/shared/components/sidebar-toggle.js b/devtools/client/shared/components/sidebar-toggle.js
new file mode 100644
index 000000000..013e95f3d
--- /dev/null
+++ b/devtools/client/shared/components/sidebar-toggle.js
@@ -0,0 +1,66 @@
+/* -*- 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 { DOM, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
+
+// Shortcuts
+const { button } = DOM;
+
+/**
+ * Sidebar toggle button. This button is used to exapand
+ * and collapse Sidebar.
+ */
+var SidebarToggle = createClass({
+ displayName: "SidebarToggle",
+
+ propTypes: {
+ // Set to true if collapsed.
+ collapsed: PropTypes.bool.isRequired,
+ // Tooltip text used when the button indicates expanded state.
+ collapsePaneTitle: PropTypes.string.isRequired,
+ // Tooltip text used when the button indicates collapsed state.
+ expandPaneTitle: PropTypes.string.isRequired,
+ // Click callback
+ onClick: PropTypes.func.isRequired,
+ },
+
+ getInitialState: function () {
+ return {
+ collapsed: this.props.collapsed,
+ };
+ },
+
+ // Events
+
+ onClick: function (event) {
+ this.props.onClick(event);
+ },
+
+ // Rendering
+
+ render: function () {
+ let title = this.state.collapsed ?
+ this.props.expandPaneTitle :
+ this.props.collapsePaneTitle;
+
+ let classNames = ["devtools-button", "sidebar-toggle"];
+ if (this.state.collapsed) {
+ classNames.push("pane-collapsed");
+ }
+
+ return (
+ button({
+ className: classNames.join(" "),
+ title: title,
+ onClick: this.onClick
+ })
+ );
+ }
+});
+
+module.exports = SidebarToggle;
diff --git a/devtools/client/shared/components/splitter/draggable.js b/devtools/client/shared/components/splitter/draggable.js
new file mode 100644
index 000000000..9caf93089
--- /dev/null
+++ b/devtools/client/shared/components/splitter/draggable.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 React = require("devtools/client/shared/vendor/react");
+const ReactDOM = require("devtools/client/shared/vendor/react-dom");
+const { DOM: dom, PropTypes } = React;
+
+const Draggable = React.createClass({
+ displayName: "Draggable",
+
+ propTypes: {
+ onMove: PropTypes.func.isRequired,
+ onStart: PropTypes.func,
+ onStop: PropTypes.func,
+ style: PropTypes.object,
+ className: PropTypes.string
+ },
+
+ startDragging(ev) {
+ ev.preventDefault();
+ const doc = ReactDOM.findDOMNode(this).ownerDocument;
+ doc.addEventListener("mousemove", this.onMove);
+ doc.addEventListener("mouseup", this.onUp);
+ this.props.onStart && this.props.onStart();
+ },
+
+ onMove(ev) {
+ ev.preventDefault();
+ // Use viewport coordinates so, moving mouse over iframes
+ // doesn't mangle (relative) coordinates.
+ this.props.onMove(ev.clientX, ev.clientY);
+ },
+
+ onUp(ev) {
+ ev.preventDefault();
+ const doc = ReactDOM.findDOMNode(this).ownerDocument;
+ doc.removeEventListener("mousemove", this.onMove);
+ doc.removeEventListener("mouseup", this.onUp);
+ this.props.onStop && this.props.onStop();
+ },
+
+ render() {
+ return dom.div({
+ style: this.props.style,
+ className: this.props.className,
+ onMouseDown: this.startDragging
+ });
+ }
+});
+
+module.exports = Draggable;
diff --git a/devtools/client/shared/components/splitter/moz.build b/devtools/client/shared/components/splitter/moz.build
new file mode 100644
index 000000000..924732aea
--- /dev/null
+++ b/devtools/client/shared/components/splitter/moz.build
@@ -0,0 +1,11 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'draggable.js',
+ 'split-box.css',
+ 'split-box.js',
+)
diff --git a/devtools/client/shared/components/splitter/split-box.css b/devtools/client/shared/components/splitter/split-box.css
new file mode 100644
index 000000000..ea8fdaa6f
--- /dev/null
+++ b/devtools/client/shared/components/splitter/split-box.css
@@ -0,0 +1,88 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+.split-box {
+ display: flex;
+ flex: 1;
+ min-width: 0;
+ height: 100%;
+ width: 100%;
+}
+
+.split-box.vert {
+ flex-direction: row;
+}
+
+.split-box.horz {
+ flex-direction: column;
+}
+
+.split-box > .uncontrolled {
+ display: flex;
+ flex: 1;
+ min-width: 0;
+ overflow: auto;
+}
+
+.split-box > .controlled {
+ display: flex;
+ overflow: auto;
+}
+
+.split-box > .splitter {
+ background-image: none;
+ border: 0;
+ border-style: solid;
+ border-color: transparent;
+ background-color: var(--theme-splitter-color);
+ background-clip: content-box;
+ position: relative;
+
+ box-sizing: border-box;
+
+ /* Positive z-index positions the splitter on top of its siblings and makes
+ it clickable on both sides. */
+ z-index: 1;
+}
+
+.split-box.vert > .splitter {
+ min-width: calc(var(--devtools-splitter-inline-start-width) +
+ var(--devtools-splitter-inline-end-width) + 1px);
+
+ border-inline-start-width: var(--devtools-splitter-inline-start-width);
+ border-inline-end-width: var(--devtools-splitter-inline-end-width);
+
+ margin-inline-start: calc(-1 * var(--devtools-splitter-inline-start-width) - 1px);
+ margin-inline-end: calc(-1 * var(--devtools-splitter-inline-end-width));
+
+ cursor: ew-resize;
+}
+
+.split-box.horz > .splitter {
+ min-height: calc(var(--devtools-splitter-top-width) +
+ var(--devtools-splitter-bottom-width) + 1px);
+
+ border-top-width: var(--devtools-splitter-top-width);
+ border-bottom-width: var(--devtools-splitter-bottom-width);
+
+ margin-top: calc(-1 * var(--devtools-splitter-top-width) - 1px);
+ margin-bottom: calc(-1 * var(--devtools-splitter-bottom-width));
+
+ cursor: ns-resize;
+}
+
+.split-box.disabled {
+ pointer-events: none;
+}
+
+/**
+ * Make sure splitter panels are not processing any mouse
+ * events. This is good for performance during splitter
+ * bar dragging.
+ */
+.split-box.dragging > .controlled,
+.split-box.dragging > .uncontrolled {
+ pointer-events: none;
+}
diff --git a/devtools/client/shared/components/splitter/split-box.js b/devtools/client/shared/components/splitter/split-box.js
new file mode 100644
index 000000000..85835f3e1
--- /dev/null
+++ b/devtools/client/shared/components/splitter/split-box.js
@@ -0,0 +1,205 @@
+/* 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 React = require("devtools/client/shared/vendor/react");
+const ReactDOM = require("devtools/client/shared/vendor/react-dom");
+const Draggable = React.createFactory(require("devtools/client/shared/components/splitter/draggable"));
+const { DOM: dom, PropTypes } = React;
+
+/**
+ * This component represents a Splitter. The splitter supports vertical
+ * as well as horizontal mode.
+ */
+const SplitBox = React.createClass({
+ displayName: "SplitBox",
+
+ propTypes: {
+ // Custom class name. You can use more names separated by a space.
+ className: PropTypes.string,
+ // Initial size of controlled panel.
+ initialSize: PropTypes.number,
+ // Left/top panel
+ startPanel: PropTypes.any,
+ // Min panel size.
+ minSize: PropTypes.number,
+ // Max panel size.
+ maxSize: PropTypes.number,
+ // Right/bottom panel
+ endPanel: PropTypes.any,
+ // True if the right/bottom panel should be controlled.
+ endPanelControl: PropTypes.bool,
+ // Size of the splitter handle bar.
+ splitterSize: PropTypes.number,
+ // True if the splitter bar is vertical (default is vertical).
+ vert: PropTypes.bool
+ },
+
+ getDefaultProps() {
+ return {
+ splitterSize: 5,
+ vert: true,
+ endPanelControl: false
+ };
+ },
+
+ /**
+ * The state stores the current orientation (vertical or horizontal)
+ * and the current size (width/height). All these values can change
+ * during the component's life time.
+ */
+ getInitialState() {
+ return {
+ vert: this.props.vert,
+ width: this.props.initialWidth || this.props.initialSize,
+ height: this.props.initialHeight || this.props.initialSize
+ };
+ },
+
+ // Dragging Events
+
+ /**
+ * Set 'resizing' cursor on entire document during splitter dragging.
+ * This avoids cursor-flickering that happens when the mouse leaves
+ * the splitter bar area (happens frequently).
+ */
+ onStartMove() {
+ const splitBox = ReactDOM.findDOMNode(this);
+ const doc = splitBox.ownerDocument;
+ let defaultCursor = doc.documentElement.style.cursor;
+ doc.documentElement.style.cursor = (this.state.vert ? "ew-resize" : "ns-resize");
+
+ splitBox.classList.add("dragging");
+
+ this.setState({
+ defaultCursor: defaultCursor
+ });
+ },
+
+ onStopMove() {
+ const splitBox = ReactDOM.findDOMNode(this);
+ const doc = splitBox.ownerDocument;
+ doc.documentElement.style.cursor = this.state.defaultCursor;
+
+ splitBox.classList.remove("dragging");
+ },
+
+ /**
+ * Adjust size of the controlled panel. Depending on the current
+ * orientation we either remember the width or height of
+ * the splitter box.
+ */
+ onMove(x, y) {
+ const node = ReactDOM.findDOMNode(this);
+ const doc = node.ownerDocument;
+ const win = doc.defaultView;
+
+ let size;
+ let { endPanelControl } = this.props;
+
+ if (this.state.vert) {
+ // Switch the control flag in case of RTL. Note that RTL
+ // has impact on vertical splitter only.
+ let dir = win.getComputedStyle(doc.documentElement).direction;
+ if (dir == "rtl") {
+ endPanelControl = !endPanelControl;
+ }
+
+ size = endPanelControl ?
+ (node.offsetLeft + node.offsetWidth) - x :
+ x - node.offsetLeft;
+
+ this.setState({
+ width: size
+ });
+ } else {
+ size = endPanelControl ?
+ (node.offsetTop + node.offsetHeight) - y :
+ y - node.offsetTop;
+
+ this.setState({
+ height: size
+ });
+ }
+ },
+
+ // Rendering
+
+ render() {
+ const vert = this.state.vert;
+ const { startPanel, endPanel, endPanelControl, minSize,
+ maxSize, splitterSize } = this.props;
+
+ let style = Object.assign({}, this.props.style);
+
+ // Calculate class names list.
+ let classNames = ["split-box"];
+ classNames.push(vert ? "vert" : "horz");
+ if (this.props.className) {
+ classNames = classNames.concat(this.props.className.split(" "));
+ }
+
+ let leftPanelStyle;
+ let rightPanelStyle;
+
+ // Set proper size for panels depending on the current state.
+ if (vert) {
+ leftPanelStyle = {
+ maxWidth: endPanelControl ? null : maxSize,
+ minWidth: endPanelControl ? null : minSize,
+ width: endPanelControl ? null : this.state.width
+ };
+ rightPanelStyle = {
+ maxWidth: endPanelControl ? maxSize : null,
+ minWidth: endPanelControl ? minSize : null,
+ width: endPanelControl ? this.state.width : null
+ };
+ } else {
+ leftPanelStyle = {
+ maxHeight: endPanelControl ? null : maxSize,
+ minHeight: endPanelControl ? null : minSize,
+ height: endPanelControl ? null : this.state.height
+ };
+ rightPanelStyle = {
+ maxHeight: endPanelControl ? maxSize : null,
+ minHeight: endPanelControl ? minSize : null,
+ height: endPanelControl ? this.state.height : null
+ };
+ }
+
+ // Calculate splitter size
+ let splitterStyle = {
+ flex: "0 0 " + splitterSize + "px"
+ };
+
+ return (
+ dom.div({
+ className: classNames.join(" "),
+ style: style },
+ startPanel ?
+ dom.div({
+ className: endPanelControl ? "uncontrolled" : "controlled",
+ style: leftPanelStyle},
+ startPanel
+ ) : null,
+ Draggable({
+ className: "splitter",
+ style: splitterStyle,
+ onStart: this.onStartMove,
+ onStop: this.onStopMove,
+ onMove: this.onMove
+ }),
+ endPanel ?
+ dom.div({
+ className: endPanelControl ? "controlled" : "uncontrolled",
+ style: rightPanelStyle},
+ endPanel
+ ) : null
+ )
+ );
+ }
+});
+
+module.exports = SplitBox;
diff --git a/devtools/client/shared/components/stack-trace.js b/devtools/client/shared/components/stack-trace.js
new file mode 100644
index 000000000..43d0b8716
--- /dev/null
+++ b/devtools/client/shared/components/stack-trace.js
@@ -0,0 +1,68 @@
+/* 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 React = require("devtools/client/shared/vendor/react");
+const { DOM: dom, createClass, createFactory, PropTypes } = React;
+const { LocalizationHelper } = require("devtools/shared/l10n");
+const Frame = createFactory(require("./frame"));
+
+const l10n = new LocalizationHelper("devtools/client/locales/webconsole.properties");
+
+const AsyncFrame = createFactory(createClass({
+ displayName: "AsyncFrame",
+
+ PropTypes: {
+ asyncCause: PropTypes.string.isRequired
+ },
+
+ render() {
+ let { asyncCause } = this.props;
+
+ return dom.span(
+ { className: "frame-link-async-cause" },
+ l10n.getFormatStr("stacktrace.asyncStack", asyncCause)
+ );
+ }
+}));
+
+const StackTrace = createClass({
+ displayName: "StackTrace",
+
+ PropTypes: {
+ stacktrace: PropTypes.array.isRequired,
+ onViewSourceInDebugger: PropTypes.func.isRequired
+ },
+
+ render() {
+ let { stacktrace, onViewSourceInDebugger } = this.props;
+
+ let frames = [];
+ stacktrace.forEach(s => {
+ if (s.asyncCause) {
+ frames.push("\t", AsyncFrame({
+ asyncCause: s.asyncCause
+ }), "\n");
+ }
+
+ frames.push("\t", Frame({
+ frame: {
+ functionDisplayName: s.functionName,
+ source: s.filename.split(" -> ").pop(),
+ line: s.lineNumber,
+ column: s.columnNumber,
+ },
+ showFunctionName: true,
+ showAnonymousFunctionName: true,
+ showFullSourceUrl: true,
+ onClick: onViewSourceInDebugger
+ }), "\n");
+ });
+
+ return dom.div({ className: "stack-trace" }, frames);
+ }
+});
+
+module.exports = StackTrace;
diff --git a/devtools/client/shared/components/tabs/moz.build b/devtools/client/shared/components/tabs/moz.build
new file mode 100644
index 000000000..d4d5dc35d
--- /dev/null
+++ b/devtools/client/shared/components/tabs/moz.build
@@ -0,0 +1,12 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'tabbar.css',
+ 'tabbar.js',
+ 'tabs.css',
+ 'tabs.js',
+)
diff --git a/devtools/client/shared/components/tabs/tabbar.css b/devtools/client/shared/components/tabs/tabbar.css
new file mode 100644
index 000000000..72445e43e
--- /dev/null
+++ b/devtools/client/shared/components/tabs/tabbar.css
@@ -0,0 +1,53 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+.tabs .tabs-navigation {
+ line-height: 15px;
+}
+
+.tabs .tabs-navigation {
+ height: 24px;
+}
+
+.tabs .tabs-menu-item:first-child {
+ border-inline-start-width: 0;
+}
+
+.tabs .tabs-navigation .tabs-menu-item:focus {
+ outline: var(--theme-focus-outline);
+ outline-offset: -2px;
+}
+
+.tabs .tabs-menu-item.is-active {
+ height: 23px;
+}
+
+/* Firebug theme is using slightly different height. */
+.theme-firebug .tabs .tabs-navigation {
+ height: 24px;
+}
+
+/* The tab takes entire horizontal space and individual tabs
+ should stretch accordingly. Use flexbox for the behavior.
+ Use also `overflow: hidden` so, 'overflow' and 'underflow'
+ events are fired (it's utilized by the all-tabs-menu). */
+.tabs .tabs-navigation .tabs-menu {
+ overflow: hidden;
+ display: flex;
+}
+
+.tabs .tabs-navigation .tabs-menu-item {
+ flex-grow: 1;
+}
+
+.tabs .tabs-navigation .tabs-menu-item a {
+ text-align: center;
+}
+
+/* Firebug theme doesn't stretch the tabs. */
+.theme-firebug .tabs .tabs-navigation .tabs-menu-item {
+ flex-grow: 0;
+}
+
diff --git a/devtools/client/shared/components/tabs/tabbar.js b/devtools/client/shared/components/tabs/tabbar.js
new file mode 100644
index 000000000..1e3aa4617
--- /dev/null
+++ b/devtools/client/shared/components/tabs/tabbar.js
@@ -0,0 +1,204 @@
+/* -*- 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 { DOM, createClass, PropTypes, createFactory } = require("devtools/client/shared/vendor/react");
+const Tabs = createFactory(require("devtools/client/shared/components/tabs/tabs").Tabs);
+
+const Menu = require("devtools/client/framework/menu");
+const MenuItem = require("devtools/client/framework/menu-item");
+
+// Shortcuts
+const { div } = DOM;
+
+/**
+ * Renders Tabbar component.
+ */
+let Tabbar = createClass({
+ displayName: "Tabbar",
+
+ propTypes: {
+ onSelect: PropTypes.func,
+ showAllTabsMenu: PropTypes.bool,
+ toolbox: PropTypes.object,
+ },
+
+ getDefaultProps: function () {
+ return {
+ showAllTabsMenu: false,
+ };
+ },
+
+ getInitialState: function () {
+ return {
+ tabs: [],
+ activeTab: 0
+ };
+ },
+
+ // Public API
+
+ addTab: function (id, title, selected = false, panel, url) {
+ let tabs = this.state.tabs.slice();
+ tabs.push({id, title, panel, url});
+
+ let newState = Object.assign({}, this.state, {
+ tabs: tabs,
+ });
+
+ if (selected) {
+ newState.activeTab = tabs.length - 1;
+ }
+
+ this.setState(newState, () => {
+ if (this.props.onSelect && selected) {
+ this.props.onSelect(id);
+ }
+ });
+ },
+
+ toggleTab: function (tabId, isVisible) {
+ let index = this.getTabIndex(tabId);
+ if (index < 0) {
+ return;
+ }
+
+ let tabs = this.state.tabs.slice();
+ tabs[index] = Object.assign({}, tabs[index], {
+ isVisible: isVisible
+ });
+
+ this.setState(Object.assign({}, this.state, {
+ tabs: tabs,
+ }));
+ },
+
+ removeTab: function (tabId) {
+ let index = this.getTabIndex(tabId);
+ if (index < 0) {
+ return;
+ }
+
+ let tabs = this.state.tabs.slice();
+ tabs.splice(index, 1);
+
+ this.setState(Object.assign({}, this.state, {
+ tabs: tabs,
+ }));
+ },
+
+ select: function (tabId) {
+ let index = this.getTabIndex(tabId);
+ if (index < 0) {
+ return;
+ }
+
+ let newState = Object.assign({}, this.state, {
+ activeTab: index,
+ });
+
+ this.setState(newState, () => {
+ if (this.props.onSelect) {
+ this.props.onSelect(tabId);
+ }
+ });
+ },
+
+ // Helpers
+
+ getTabIndex: function (tabId) {
+ let tabIndex = -1;
+ this.state.tabs.forEach((tab, index) => {
+ if (tab.id == tabId) {
+ tabIndex = index;
+ }
+ });
+ return tabIndex;
+ },
+
+ getTabId: function (index) {
+ return this.state.tabs[index].id;
+ },
+
+ getCurrentTabId: function () {
+ return this.state.tabs[this.state.activeTab].id;
+ },
+
+ // Event Handlers
+
+ onTabChanged: function (index) {
+ this.setState({
+ activeTab: index
+ });
+
+ if (this.props.onSelect) {
+ this.props.onSelect(this.state.tabs[index].id);
+ }
+ },
+
+ onAllTabsMenuClick: function (event) {
+ let menu = new Menu();
+ let target = event.target;
+
+ // Generate list of menu items from the list of tabs.
+ this.state.tabs.forEach(tab => {
+ menu.append(new MenuItem({
+ label: tab.title,
+ type: "checkbox",
+ checked: this.getCurrentTabId() == tab.id,
+ click: () => this.select(tab.id),
+ }));
+ });
+
+ // Show a drop down menu with frames.
+ // XXX Missing menu API for specifying target (anchor)
+ // and relative position to it. See also:
+ // https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL/Method/openPopup
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1274551
+ let rect = target.getBoundingClientRect();
+ let screenX = target.ownerDocument.defaultView.mozInnerScreenX;
+ let screenY = target.ownerDocument.defaultView.mozInnerScreenY;
+ menu.popup(rect.left + screenX, rect.bottom + screenY, this.props.toolbox);
+
+ return menu;
+ },
+
+ // Rendering
+
+ renderTab: function (tab) {
+ if (typeof tab.panel === "function") {
+ return tab.panel({
+ key: tab.id,
+ title: tab.title,
+ id: tab.id,
+ url: tab.url,
+ });
+ }
+
+ return tab.panel;
+ },
+
+ render: function () {
+ let tabs = this.state.tabs.map(tab => {
+ return this.renderTab(tab);
+ });
+
+ return (
+ div({className: "devtools-sidebar-tabs"},
+ Tabs({
+ onAllTabsMenuClick: this.onAllTabsMenuClick,
+ showAllTabsMenu: this.props.showAllTabsMenu,
+ tabActive: this.state.activeTab,
+ onAfterChange: this.onTabChanged},
+ tabs
+ )
+ )
+ );
+ },
+});
+
+module.exports = Tabbar;
diff --git a/devtools/client/shared/components/tabs/tabs.css b/devtools/client/shared/components/tabs/tabs.css
new file mode 100644
index 000000000..0e70549c5
--- /dev/null
+++ b/devtools/client/shared/components/tabs/tabs.css
@@ -0,0 +1,183 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+/* Tabs General Styles */
+
+.tabs {
+ height: 100%;
+}
+
+.tabs .tabs-menu {
+ display: table;
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+.tabs .tabs-menu-item {
+ display: inline-block;
+}
+
+.tabs .tabs-menu-item a {
+ display: block;
+ color: #A9A9A9;
+ padding: 4px 8px;
+ border: 1px solid transparent;
+ text-decoration: none;
+ white-space: nowrap;
+}
+
+.tabs .tabs-menu-item a {
+ cursor: default;
+}
+
+/* Make sure panel content takes entire vertical space.
+ (minus the height of the tab bar) */
+.tabs .panels {
+ height: calc(100% - 24px);
+}
+
+.tabs .tab-panel {
+ height: 100%;
+}
+
+.tabs .all-tabs-menu {
+ position: absolute;
+ top: 0;
+ offset-inline-end: 0;
+ width: 15px;
+ height: 100%;
+ border-inline-start: 1px solid var(--theme-splitter-color);
+ background: url("chrome://devtools/skin/images/dropmarker.svg");
+ background-repeat: no-repeat;
+ background-position: center;
+ background-color: var(--theme-tab-toolbar-background);
+}
+
+/* Light Theme */
+
+.theme-dark .tabs,
+.theme-light .tabs {
+ background: var(--theme-body-background);
+}
+
+.theme-dark .tabs .tabs-navigation,
+.theme-light .tabs .tabs-navigation {
+ position: relative;
+ border-bottom: 1px solid var(--theme-splitter-color);
+ background: var(--theme-tab-toolbar-background);
+}
+
+.theme-dark .tabs .tabs-menu-item,
+.theme-light .tabs .tabs-menu-item {
+ margin: 0;
+ padding: 0;
+ border-style: solid;
+ border-width: 0;
+ border-inline-start-width: 1px;
+ border-color: var(--theme-splitter-color);
+}
+
+.theme-dark .tabs .tabs-menu-item:last-child,
+.theme-light:not(.theme-firebug) .tabs .tabs-menu-item:last-child {
+ border-inline-end-width: 1px;
+}
+
+.theme-dark .tabs .tabs-menu-item a,
+.theme-light .tabs .tabs-menu-item a {
+ color: var(--theme-content-color1);
+ padding: 3px 15px;
+}
+
+.theme-dark .tabs .tabs-menu-item:hover:not(.is-active),
+.theme-light .tabs .tabs-menu-item:hover:not(.is-active) {
+ background-color: var(--toolbar-tab-hover);
+}
+
+.theme-dark .tabs .tabs-menu-item:hover:active:not(.is-active),
+.theme-light .tabs .tabs-menu-item:hover:active:not(.is-active) {
+ background-color: var(--toolbar-tab-hover-active);
+}
+
+.theme-dark .tabs .tabs-menu-item.is-active,
+.theme-light .tabs .tabs-menu-item.is-active {
+ background-color: var(--theme-selection-background);
+}
+
+.theme-dark .tabs .tabs-menu-item.is-active a,
+.theme-light .tabs .tabs-menu-item.is-active a {
+ color: var(--theme-selection-color);
+}
+
+/* Dark Theme */
+
+.theme-dark .tabs .tabs-menu-item a {
+ color: var(--theme-body-color-alt);
+}
+
+.theme-dark .tabs .tabs-menu-item:hover:not(.is-active) a {
+ color: #CED3D9;
+}
+
+.theme-dark .tabs .tabs-menu-item:hover:active a {
+ color: var(--theme-selection-color);
+}
+
+/* Firebug Theme */
+
+.theme-firebug .tabs .tabs-navigation {
+ background-image: linear-gradient(rgba(253, 253, 253, 0.2), rgba(253, 253, 253, 0));
+ padding-top: 3px;
+ padding-left: 3px;
+ border-bottom: 1px solid rgb(170, 188, 207);
+}
+
+.theme-firebug .tabs .tabs-menu {
+ margin-bottom: -1px;
+}
+
+.theme-firebug .tabs .tabs-menu-item.is-active,
+.theme-firebug .tabs .tabs-menu-item.is-active:hover {
+ background-color: transparent;
+}
+
+.theme-firebug .tabs .tabs-menu-item {
+ position: relative;
+ border-inline-start-width: 0;
+}
+
+.theme-firebug .tabs .tabs-menu-item a {
+ font-family: var(--proportional-font-family);
+ font-weight: bold;
+ color: var(--theme-body-color);
+ border-radius: 4px 4px 0 0;
+}
+
+.theme-firebug .tabs .tabs-menu-item:hover:not(.is-active) a {
+ border: 1px solid #C8C8C8;
+ border-bottom: 1px solid transparent;
+ background-color: transparent;
+}
+
+.theme-firebug .tabs .tabs-menu-item.is-active a {
+ background-color: rgb(247, 251, 254);
+ border: 1px solid rgb(170, 188, 207);
+ border-bottom-color: transparent;
+ color: var(--theme-body-color);
+}
+
+.theme-firebug .tabs .tabs-menu-item:hover:active a {
+ background-color: var(--toolbar-tab-hover-active);
+}
+
+.theme-firebug .tabs .tabs-menu-item.is-active:hover:active a {
+ background-color: var(--theme-selection-background);
+ color: var(--theme-selection-color);
+}
+
+.theme-firebug .tabs .tabs-menu-item a {
+ border: 1px solid transparent;
+ padding: 4px 8px;
+}
diff --git a/devtools/client/shared/components/tabs/tabs.js b/devtools/client/shared/components/tabs/tabs.js
new file mode 100644
index 000000000..eaa0738b3
--- /dev/null
+++ b/devtools/client/shared/components/tabs/tabs.js
@@ -0,0 +1,369 @@
+/* -*- 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";
+
+define(function (require, exports, module) {
+ const React = require("devtools/client/shared/vendor/react");
+ const { DOM } = React;
+ const { findDOMNode } = require("devtools/client/shared/vendor/react-dom");
+
+ /**
+ * Renders simple 'tab' widget.
+ *
+ * Based on ReactSimpleTabs component
+ * https://github.com/pedronauck/react-simpletabs
+ *
+ * Component markup (+CSS) example:
+ *
+ * <div class='tabs'>
+ * <nav class='tabs-navigation'>
+ * <ul class='tabs-menu'>
+ * <li class='tabs-menu-item is-active'>Tab #1</li>
+ * <li class='tabs-menu-item'>Tab #2</li>
+ * </ul>
+ * </nav>
+ * <div class='panels'>
+ * The content of active panel here
+ * </div>
+ * <div>
+ */
+ let Tabs = React.createClass({
+ displayName: "Tabs",
+
+ propTypes: {
+ className: React.PropTypes.oneOfType([
+ React.PropTypes.array,
+ React.PropTypes.string,
+ React.PropTypes.object
+ ]),
+ tabActive: React.PropTypes.number,
+ onMount: React.PropTypes.func,
+ onBeforeChange: React.PropTypes.func,
+ onAfterChange: React.PropTypes.func,
+ children: React.PropTypes.oneOfType([
+ React.PropTypes.array,
+ React.PropTypes.element
+ ]).isRequired,
+ showAllTabsMenu: React.PropTypes.bool,
+ onAllTabsMenuClick: React.PropTypes.func,
+ },
+
+ getDefaultProps: function () {
+ return {
+ tabActive: 0,
+ showAllTabsMenu: false,
+ };
+ },
+
+ getInitialState: function () {
+ return {
+ tabActive: this.props.tabActive,
+
+ // This array is used to store an information whether a tab
+ // at specific index has already been created (e.g. selected
+ // at least once).
+ // If yes, it's rendered even if not currently selected.
+ // This is because in some cases we don't want to re-create
+ // tab content when it's being unselected/selected.
+ // E.g. in case of an iframe being used as a tab-content
+ // we want the iframe to stay in the DOM.
+ created: [],
+
+ // True if tabs can't fit into available horizontal space.
+ overflow: false,
+ };
+ },
+
+ componentDidMount: function () {
+ let node = findDOMNode(this);
+ node.addEventListener("keydown", this.onKeyDown, false);
+
+ // Register overflow listeners to manage visibility
+ // of all-tabs-menu. This menu is displayed when there
+ // is not enough h-space to render all tabs.
+ // It allows the user to select a tab even if it's hidden.
+ if (this.props.showAllTabsMenu) {
+ node.addEventListener("overflow", this.onOverflow, false);
+ node.addEventListener("underflow", this.onUnderflow, false);
+ }
+
+ let index = this.state.tabActive;
+ if (this.props.onMount) {
+ this.props.onMount(index);
+ }
+ },
+
+ componentWillReceiveProps: function (newProps) {
+ // Check type of 'tabActive' props to see if it's valid
+ // (it's 0-based index).
+ if (typeof newProps.tabActive == "number") {
+ let created = [...this.state.created];
+ created[newProps.tabActive] = true;
+
+ this.setState(Object.assign({}, this.state, {
+ tabActive: newProps.tabActive,
+ created: created,
+ }));
+ }
+ },
+
+ componentWillUnmount: function () {
+ let node = findDOMNode(this);
+ node.removeEventListener("keydown", this.onKeyDown, false);
+
+ if (this.props.showAllTabsMenu) {
+ node.removeEventListener("overflow", this.onOverflow, false);
+ node.removeEventListener("underflow", this.onUnderflow, false);
+ }
+ },
+
+ // DOM Events
+
+ onOverflow: function (event) {
+ if (event.target.classList.contains("tabs-menu")) {
+ this.setState({
+ overflow: true
+ });
+ }
+ },
+
+ onUnderflow: function (event) {
+ if (event.target.classList.contains("tabs-menu")) {
+ this.setState({
+ overflow: false
+ });
+ }
+ },
+
+ onKeyDown: function (event) {
+ // Bail out if the focus isn't on a tab.
+ if (!event.target.closest(".tabs-menu-item")) {
+ return;
+ }
+
+ let tabActive = this.state.tabActive;
+ let tabCount = this.props.children.length;
+
+ switch (event.code) {
+ case "ArrowRight":
+ tabActive = Math.min(tabCount - 1, tabActive + 1);
+ break;
+ case "ArrowLeft":
+ tabActive = Math.max(0, tabActive - 1);
+ break;
+ }
+
+ if (this.state.tabActive != tabActive) {
+ this.setActive(tabActive);
+ }
+ },
+
+ onClickTab: function (index, event) {
+ this.setActive(index);
+ event.preventDefault();
+ },
+
+ onAllTabsMenuClick: function (event) {
+ if (this.props.onAllTabsMenuClick) {
+ this.props.onAllTabsMenuClick(event);
+ }
+ },
+
+ // API
+
+ setActive: function (index) {
+ let onAfterChange = this.props.onAfterChange;
+ let onBeforeChange = this.props.onBeforeChange;
+
+ if (onBeforeChange) {
+ let cancel = onBeforeChange(index);
+ if (cancel) {
+ return;
+ }
+ }
+
+ let created = [...this.state.created];
+ created[index] = true;
+
+ let newState = Object.assign({}, this.state, {
+ tabActive: index,
+ created: created
+ });
+
+ this.setState(newState, () => {
+ // Properly set focus on selected tab.
+ let node = findDOMNode(this);
+ let selectedTab = node.querySelector(".is-active > a");
+ if (selectedTab) {
+ selectedTab.focus();
+ }
+
+ if (onAfterChange) {
+ onAfterChange(index);
+ }
+ });
+ },
+
+ // Rendering
+
+ renderMenuItems: function () {
+ if (!this.props.children) {
+ throw new Error("There must be at least one Tab");
+ }
+
+ if (!Array.isArray(this.props.children)) {
+ this.props.children = [this.props.children];
+ }
+
+ let tabs = this.props.children
+ .map(tab => {
+ return typeof tab === "function" ? tab() : tab;
+ }).filter(tab => {
+ return tab;
+ }).map((tab, index) => {
+ let ref = ("tab-menu-" + index);
+ let title = tab.props.title;
+ let tabClassName = tab.props.className;
+ let isTabSelected = this.state.tabActive === index;
+
+ let classes = [
+ "tabs-menu-item",
+ tabClassName,
+ isTabSelected ? "is-active" : ""
+ ].join(" ");
+
+ // Set tabindex to -1 (except the selected tab) so, it's focusable,
+ // but not reachable via sequential tab-key navigation.
+ // Changing selected tab (and so, moving focus) is done through
+ // left and right arrow keys.
+ // See also `onKeyDown()` event handler.
+ return (
+ DOM.li({
+ ref: ref,
+ key: index,
+ id: "tab-" + index,
+ className: classes,
+ role: "presentation",
+ },
+ DOM.a({
+ tabIndex: this.state.tabActive === index ? 0 : -1,
+ "aria-controls": "panel-" + index,
+ "aria-selected": isTabSelected,
+ role: "tab",
+ onClick: this.onClickTab.bind(this, index),
+ },
+ title
+ )
+ )
+ );
+ });
+
+ // Display the menu only if there is not enough horizontal
+ // space for all tabs (and overflow happened).
+ let allTabsMenu = this.state.overflow ? (
+ DOM.div({
+ className: "all-tabs-menu",
+ onClick: this.props.onAllTabsMenuClick
+ })
+ ) : null;
+
+ return (
+ DOM.nav({className: "tabs-navigation"},
+ DOM.ul({className: "tabs-menu", role: "tablist"},
+ tabs
+ ),
+ allTabsMenu
+ )
+ );
+ },
+
+ renderPanels: function () {
+ if (!this.props.children) {
+ throw new Error("There must be at least one Tab");
+ }
+
+ if (!Array.isArray(this.props.children)) {
+ this.props.children = [this.props.children];
+ }
+
+ let selectedIndex = this.state.tabActive;
+
+ let panels = this.props.children
+ .map(tab => {
+ return typeof tab === "function" ? tab() : tab;
+ }).filter(tab => {
+ return tab;
+ }).map((tab, index) => {
+ let selected = selectedIndex == index;
+
+ // Use 'visibility:hidden' + 'width/height:0' for hiding
+ // content of non-selected tab. It's faster (not sure why)
+ // than display:none and visibility:collapse.
+ let style = {
+ visibility: selected ? "visible" : "hidden",
+ height: selected ? "100%" : "0",
+ width: selected ? "100%" : "0",
+ };
+
+ return (
+ DOM.div({
+ key: index,
+ id: "panel-" + index,
+ style: style,
+ className: "tab-panel-box",
+ role: "tabpanel",
+ "aria-labelledby": "tab-" + index,
+ },
+ (selected || this.state.created[index]) ? tab : null
+ )
+ );
+ });
+
+ return (
+ DOM.div({className: "panels"},
+ panels
+ )
+ );
+ },
+
+ render: function () {
+ let classNames = ["tabs", this.props.className].join(" ");
+
+ return (
+ DOM.div({className: classNames},
+ this.renderMenuItems(),
+ this.renderPanels()
+ )
+ );
+ },
+ });
+
+ /**
+ * Renders simple tab 'panel'.
+ */
+ let Panel = React.createClass({
+ displayName: "Panel",
+
+ propTypes: {
+ title: React.PropTypes.string.isRequired,
+ children: React.PropTypes.oneOfType([
+ React.PropTypes.array,
+ React.PropTypes.element
+ ]).isRequired
+ },
+
+ render: function () {
+ return DOM.div({className: "tab-panel"},
+ this.props.children
+ );
+ }
+ });
+
+ // Exports from this module
+ exports.TabPanel = Panel;
+ exports.Tabs = Tabs;
+});
diff --git a/devtools/client/shared/components/test/browser/.eslintrc.js b/devtools/client/shared/components/test/browser/.eslintrc.js
new file mode 100644
index 000000000..76904829d
--- /dev/null
+++ b/devtools/client/shared/components/test/browser/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../../../../.eslintrc.mochitests.js",
+};
diff --git a/devtools/client/shared/components/test/browser/browser.ini b/devtools/client/shared/components/test/browser/browser.ini
new file mode 100644
index 000000000..9db9eca66
--- /dev/null
+++ b/devtools/client/shared/components/test/browser/browser.ini
@@ -0,0 +1,7 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ !/devtools/client/framework/test/shared-head.js
+
+[browser_notification_box_basic.js]
diff --git a/devtools/client/shared/components/test/browser/browser_notification_box_basic.js b/devtools/client/shared/components/test/browser/browser_notification_box_basic.js
new file mode 100644
index 000000000..b7c6a669b
--- /dev/null
+++ b/devtools/client/shared/components/test/browser/browser_notification_box_basic.js
@@ -0,0 +1,36 @@
+/* -*- 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";
+
+/* import-globals-from ../../../../framework/test/shared-head.js */
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js", this);
+
+const TEST_URI = "data:text/html;charset=utf-8,Test page";
+
+/**
+ * Basic test that checks existence of the Notification box.
+ */
+add_task(function* () {
+ info("Test Notification box basic started");
+
+ let toolbox = yield openNewTabAndToolbox(TEST_URI, "webconsole");
+
+ // Append a notification
+ let notificationBox = toolbox.getNotificationBox();
+ notificationBox.appendNotification(
+ "Info message",
+ "id1",
+ null,
+ notificationBox.PRIORITY_INFO_HIGH
+ );
+
+ // Verify existence of one notification.
+ let parentNode = toolbox.doc.getElementById("toolbox-notificationbox");
+ let nodes = parentNode.querySelectorAll(".notification");
+ is(nodes.length, 1, "There must be one notification");
+});
diff --git a/devtools/client/shared/components/test/mochitest/.eslintrc.js b/devtools/client/shared/components/test/mochitest/.eslintrc.js
new file mode 100644
index 000000000..677cbb424
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../../../../.eslintrc.mochitests.js"
+};
diff --git a/devtools/client/shared/components/test/mochitest/chrome.ini b/devtools/client/shared/components/test/mochitest/chrome.ini
new file mode 100644
index 000000000..27a4be137
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/chrome.ini
@@ -0,0 +1,51 @@
+[DEFAULT]
+support-files =
+ head.js
+
+[test_frame_01.html]
+[test_HSplitBox_01.html]
+[test_notification_box_01.html]
+[test_notification_box_02.html]
+[test_notification_box_03.html]
+[test_reps_array.html]
+[test_reps_attribute.html]
+[test_reps_comment-node.html]
+[test_reps_date-time.html]
+[test_reps_document.html]
+[test_reps_element-node.html]
+[test_reps_event.html]
+[test_reps_function.html]
+[test_reps_grip.html]
+[test_reps_grip-array.html]
+[test_reps_grip-map.html]
+[test_reps_infinity.html]
+[test_reps_long-string.html]
+[test_reps_nan.html]
+[test_reps_null.html]
+[test_reps_number.html]
+[test_reps_object.html]
+[test_reps_object-with-text.html]
+[test_reps_object-with-url.html]
+[test_reps_promise.html]
+[test_reps_regexp.html]
+[test_reps_string.html]
+[test_reps_stylesheet.html]
+[test_reps_symbol.html]
+[test_reps_text-node.html]
+[test_reps_undefined.html]
+[test_reps_window.html]
+[test_sidebar_toggle.html]
+[test_stack-trace.html]
+[test_tabs_accessibility.html]
+[test_tabs_menu.html]
+[test_tree_01.html]
+[test_tree_02.html]
+[test_tree_03.html]
+[test_tree_04.html]
+[test_tree_05.html]
+[test_tree_06.html]
+[test_tree_07.html]
+[test_tree_08.html]
+[test_tree_09.html]
+[test_tree_10.html]
+[test_tree_11.html]
diff --git a/devtools/client/shared/components/test/mochitest/head.js b/devtools/client/shared/components/test/mochitest/head.js
new file mode 100644
index 000000000..b66b72814
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/head.js
@@ -0,0 +1,217 @@
+/* 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/. */
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+
+"use strict";
+
+var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+var { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+var { Assert } = require("resource://testing-common/Assert.jsm");
+var { gDevTools } = require("devtools/client/framework/devtools");
+var { BrowserLoader } = Cu.import("resource://devtools/client/shared/browser-loader.js", {});
+var promise = require("promise");
+var defer = require("devtools/shared/defer");
+var Services = require("Services");
+var { DebuggerServer } = require("devtools/server/main");
+var { DebuggerClient } = require("devtools/shared/client/main");
+var DevToolsUtils = require("devtools/shared/DevToolsUtils");
+var flags = require("devtools/shared/flags");
+var { Task } = require("devtools/shared/task");
+var { TargetFactory } = require("devtools/client/framework/target");
+var { Toolbox } = require("devtools/client/framework/toolbox");
+
+flags.testing = true;
+var { require: browserRequire } = BrowserLoader({
+ baseURI: "resource://devtools/client/shared/",
+ window
+});
+
+let ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+let React = browserRequire("devtools/client/shared/vendor/react");
+var TestUtils = React.addons.TestUtils;
+
+var EXAMPLE_URL = "http://example.com/browser/browser/devtools/shared/test/";
+
+function forceRender(comp) {
+ return setState(comp, {})
+ .then(() => setState(comp, {}));
+}
+
+// All tests are asynchronous.
+SimpleTest.waitForExplicitFinish();
+
+function onNextAnimationFrame(fn) {
+ return () =>
+ requestAnimationFrame(() =>
+ requestAnimationFrame(fn));
+}
+
+function setState(component, newState) {
+ return new Promise(resolve => {
+ component.setState(newState, onNextAnimationFrame(resolve));
+ });
+}
+
+function setProps(component, newProps) {
+ return new Promise(resolve => {
+ component.setProps(newProps, onNextAnimationFrame(resolve));
+ });
+}
+
+function dumpn(msg) {
+ dump(`SHARED-COMPONENTS-TEST: ${msg}\n`);
+}
+
+/**
+ * Tree
+ */
+
+var TEST_TREE_INTERFACE = {
+ getParent: x => TEST_TREE.parent[x],
+ getChildren: x => TEST_TREE.children[x],
+ renderItem: (x, depth, focused) => "-".repeat(depth) + x + ":" + focused + "\n",
+ getRoots: () => ["A", "M"],
+ getKey: x => "key-" + x,
+ itemHeight: 1,
+ onExpand: x => TEST_TREE.expanded.add(x),
+ onCollapse: x => TEST_TREE.expanded.delete(x),
+ isExpanded: x => TEST_TREE.expanded.has(x),
+};
+
+function isRenderedTree(actual, expectedDescription, msg) {
+ const expected = expectedDescription.map(x => x + "\n").join("");
+ dumpn(`Expected tree:\n${expected}`);
+ dumpn(`Actual tree:\n${actual}`);
+ is(actual, expected, msg);
+}
+
+// Encoding of the following tree/forest:
+//
+// A
+// |-- B
+// | |-- E
+// | | |-- K
+// | | `-- L
+// | |-- F
+// | `-- G
+// |-- C
+// | |-- H
+// | `-- I
+// `-- D
+// `-- J
+// M
+// `-- N
+// `-- O
+var TEST_TREE = {
+ children: {
+ A: ["B", "C", "D"],
+ B: ["E", "F", "G"],
+ C: ["H", "I"],
+ D: ["J"],
+ E: ["K", "L"],
+ F: [],
+ G: [],
+ H: [],
+ I: [],
+ J: [],
+ K: [],
+ L: [],
+ M: ["N"],
+ N: ["O"],
+ O: []
+ },
+ parent: {
+ A: null,
+ B: "A",
+ C: "A",
+ D: "A",
+ E: "B",
+ F: "B",
+ G: "B",
+ H: "C",
+ I: "C",
+ J: "D",
+ K: "E",
+ L: "E",
+ M: null,
+ N: "M",
+ O: "N"
+ },
+ expanded: new Set(),
+};
+
+/**
+ * Frame
+ */
+function checkFrameString({
+ el, file, line, column, source, functionName, shouldLink, tooltip
+}) {
+ let $ = selector => el.querySelector(selector);
+
+ let $func = $(".frame-link-function-display-name");
+ let $source = $(".frame-link-source");
+ let $sourceInner = $(".frame-link-source-inner");
+ let $filename = $(".frame-link-filename");
+ let $line = $(".frame-link-line");
+
+ is($filename.textContent, file, "Correct filename");
+ is(el.getAttribute("data-line"), line ? `${line}` : null, "Expected `data-line` found");
+ is(el.getAttribute("data-column"),
+ column ? `${column}` : null, "Expected `data-column` found");
+ is($sourceInner.getAttribute("title"), tooltip, "Correct tooltip");
+ is($source.tagName, shouldLink ? "A" : "SPAN", "Correct linkable status");
+ if (shouldLink) {
+ is($source.getAttribute("href"), source, "Correct source");
+ }
+
+ if (line != null) {
+ let lineText = `:${line}`;
+ if (column != null) {
+ lineText += `:${column}`;
+ }
+
+ is($line.textContent, lineText, "Correct line number");
+ } else {
+ ok(!$line, "Should not have an element for `line`");
+ }
+
+ if (functionName != null) {
+ is($func.textContent, functionName, "Correct function name");
+ } else {
+ ok(!$func, "Should not have an element for `functionName`");
+ }
+}
+
+function renderComponent(component, props) {
+ const el = React.createElement(component, props, {});
+ // By default, renderIntoDocument() won't work for stateless components, but
+ // it will work if the stateless component is wrapped in a stateful one.
+ // See https://github.com/facebook/react/issues/4839
+ const wrappedEl = React.DOM.span({}, [el]);
+ const renderedComponent = TestUtils.renderIntoDocument(wrappedEl);
+ return ReactDOM.findDOMNode(renderedComponent).children[0];
+}
+
+function shallowRenderComponent(component, props) {
+ const el = React.createElement(component, props);
+ const renderer = TestUtils.createRenderer();
+ renderer.render(el, {});
+ return renderer.getRenderOutput();
+}
+
+/**
+ * Test that a rep renders correctly across different modes.
+ */
+function testRepRenderModes(modeTests, testName, componentUnderTest, gripStub) {
+ modeTests.forEach(({mode, expectedOutput, message}) => {
+ const modeString = typeof mode === "undefined" ? "no mode" : mode;
+ if (!message) {
+ message = `${testName}: ${modeString} renders correctly.`;
+ }
+
+ const rendered = renderComponent(componentUnderTest.rep, { object: gripStub, mode });
+ is(rendered.textContent, expectedOutput, message);
+ });
+}
diff --git a/devtools/client/shared/components/test/mochitest/test_HSplitBox_01.html b/devtools/client/shared/components/test/mochitest/test_HSplitBox_01.html
new file mode 100644
index 000000000..7a7187de6
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_HSplitBox_01.html
@@ -0,0 +1,126 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Basic tests for the HSplitBox component.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript "src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+ <link rel="stylesheet" href="resource://devtools/client/themes/splitters.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://devtools/skin/components-h-split-box.css" type="text/css"/>
+ <style>
+ html {
+ --theme-splitter-color: black;
+ }
+ </style>
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+const FUDGE_FACTOR = .1;
+function aboutEq(a, b) {
+ dumpn(`Checking ${a} ~= ${b}`);
+ return Math.abs(a - b) < FUDGE_FACTOR;
+}
+
+window.onload = Task.async(function* () {
+ try {
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+
+ let HSplitBox = React.createFactory(browserRequire("devtools/client/shared/components/h-split-box"));
+ ok(HSplitBox, "Should get HSplitBox");
+
+ const newSizes = [];
+ const box = ReactDOM.render(HSplitBox({
+ start: "hello!",
+ end: "world!",
+ startWidth: .5,
+ onResize(newSize) {
+ newSizes.push(newSize);
+ },
+ }), window.document.body);
+
+ // Test that we properly rendered our two panes.
+
+ let panes = document.querySelectorAll(".h-split-box-pane");
+ is(panes.length, 2, "Should get two panes");
+ is(panes[0].style.flexGrow, "0.5", "Each pane should have .5 width");
+ is(panes[1].style.flexGrow, "0.5", "Each pane should have .5 width");
+ is(panes[0].textContent.trim(), "hello!", "First pane should be hello");
+ is(panes[1].textContent.trim(), "world!", "Second pane should be world");
+
+ // Now change the left width and assert that the changes are reflected.
+
+ yield setProps(box, { startWidth: .25 });
+ panes = document.querySelectorAll(".h-split-box-pane");
+ is(panes.length, 2, "Should still have two panes");
+ is(panes[0].style.flexGrow, "0.25", "First pane's width should be .25");
+ is(panes[1].style.flexGrow, "0.75", "Second pane's width should be .75");
+
+ // Mouse moves without having grabbed the splitter should have no effect.
+
+ let container = document.querySelector(".h-split-box");
+ ok(container, "Should get our container .h-split-box");
+
+ const { left, top, width } = container.getBoundingClientRect();
+ const middle = left + width / 2;
+ const oneQuarter = left + width / 4;
+ const threeQuarters = left + 3 * width / 4;
+
+ synthesizeMouse(container, middle, top, { type: "mousemove" }, window);
+ is(newSizes.length, 0, "Mouse moves without dragging the splitter should have no effect");
+
+ // Send a mouse down on the splitter, and then move the mouse a couple
+ // times. Now we should get resizes.
+
+ const splitter = document.querySelector(".devtools-side-splitter");
+ ok(splitter, "Should get our splitter");
+
+ synthesizeMouseAtCenter(splitter, { button: 0, type: "mousedown" }, window);
+
+ function mouseMove(clientX) {
+ const event = new MouseEvent("mousemove", { clientX });
+ document.defaultView.top.dispatchEvent(event);
+ }
+
+ mouseMove(middle);
+ is(newSizes.length, 1, "Should get 1 resize");
+ ok(aboutEq(newSizes[0], .5), "New size should be ~.5");
+
+ mouseMove(left);
+ is(newSizes.length, 2, "Should get 2 resizes");
+ ok(aboutEq(newSizes[1], 0), "New size should be ~0");
+
+ mouseMove(oneQuarter);
+ is(newSizes.length, 3, "Sould get 3 resizes");
+ ok(aboutEq(newSizes[2], .25), "New size should be ~.25");
+
+ mouseMove(threeQuarters);
+ is(newSizes.length, 4, "Should get 4 resizes");
+ ok(aboutEq(newSizes[3], .75), "New size should be ~.75");
+
+ synthesizeMouseAtCenter(splitter, { button: 0, type: "mouseup" }, window);
+
+ // Now that we have let go of the splitter, mouse moves should not result in resizes.
+
+ synthesizeMouse(container, middle, top, { type: "mousemove" }, window);
+ is(newSizes.length, 4, "Should still have 4 resizes");
+
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_frame_01.html b/devtools/client/shared/components/test/mochitest/test_frame_01.html
new file mode 100644
index 000000000..ed3bc90c2
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_frame_01.html
@@ -0,0 +1,309 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test the formatting of the file name, line and columns are correct in frame components,
+with optional columns, unknown and non-URL sources.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Frame component test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ let ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ let React = browserRequire("devtools/client/shared/vendor/react");
+ let Frame = React.createFactory(browserRequire("devtools/client/shared/components/frame"));
+ ok(Frame, "Should get Frame");
+
+ // Check when there's a column
+ yield checkFrameComponent({
+ frame: {
+ source: "http://myfile.com/mahscripts.js",
+ line: 55,
+ column: 10,
+ }
+ }, {
+ file: "mahscripts.js",
+ line: 55,
+ column: 10,
+ shouldLink: true,
+ tooltip: "View source in Debugger → http://myfile.com/mahscripts.js:55:10",
+ });
+
+ // Check when there's no column
+ yield checkFrameComponent({
+ frame: {
+ source: "http://myfile.com/mahscripts.js",
+ line: 55,
+ }
+ }, {
+ file: "mahscripts.js",
+ line: 55,
+ shouldLink: true,
+ tooltip: "View source in Debugger → http://myfile.com/mahscripts.js:55",
+ });
+
+ // Check when column === 0
+ yield checkFrameComponent({
+ frame: {
+ source: "http://myfile.com/mahscripts.js",
+ line: 55,
+ column: 0,
+ }
+ }, {
+ file: "mahscripts.js",
+ line: 55,
+ shouldLink: true,
+ tooltip: "View source in Debugger → http://myfile.com/mahscripts.js:55",
+ });
+
+ // Check when there's no parseable URL source;
+ // should not link but should render line/columns
+ yield checkFrameComponent({
+ frame: {
+ source: "self-hosted",
+ line: 1,
+ }
+ }, {
+ file: "self-hosted",
+ line: "1",
+ shouldLink: false,
+ tooltip: "self-hosted:1",
+ });
+ yield checkFrameComponent({
+ frame: {
+ source: "self-hosted",
+ line: 1,
+ column: 10,
+ }
+ }, {
+ file: "self-hosted",
+ line: "1",
+ column: "10",
+ shouldLink: false,
+ tooltip: "self-hosted:1:10",
+ });
+
+ // Check when there's no source;
+ // should not link but should render line/columns
+ yield checkFrameComponent({
+ frame: {
+ line: 1,
+ }
+ }, {
+ file: "(unknown)",
+ line: "1",
+ shouldLink: false,
+ tooltip: "(unknown):1",
+ });
+ yield checkFrameComponent({
+ frame: {
+ line: 1,
+ column: 10,
+ }
+ }, {
+ file: "(unknown)",
+ line: "1",
+ column: "10",
+ shouldLink: false,
+ tooltip: "(unknown):1:10",
+ });
+
+ // Check when there's a column, but no line;
+ // no line/column info should render
+ yield checkFrameComponent({
+ frame: {
+ source: "http://myfile.com/mahscripts.js",
+ column: 55,
+ }
+ }, {
+ file: "mahscripts.js",
+ shouldLink: true,
+ tooltip: "View source in Debugger → http://myfile.com/mahscripts.js",
+ });
+
+ // Check when line is 0; this should be an invalid
+ // line option, so don't render line/column
+ yield checkFrameComponent({
+ frame: {
+ source: "http://myfile.com/mahscripts.js",
+ line: 0,
+ column: 55,
+ }
+ }, {
+ file: "mahscripts.js",
+ shouldLink: true,
+ tooltip: "View source in Debugger → http://myfile.com/mahscripts.js",
+ });
+
+ // Check when source is via Scratchpad; we should render out the
+ // lines and columns as this is linkable.
+ yield checkFrameComponent({
+ frame: {
+ source: "Scratchpad/1",
+ line: 10,
+ column: 50,
+ }
+ }, {
+ file: "Scratchpad/1",
+ line: 10,
+ column: 50,
+ shouldLink: true,
+ tooltip: "View source in Debugger → Scratchpad/1:10:50",
+ });
+
+ // Check that line and column can be strings
+ yield checkFrameComponent({
+ frame: {
+ source: "http://myfile.com/mahscripts.js",
+ line: "10",
+ column: "55",
+ }
+ }, {
+ file: "mahscripts.js",
+ line: 10,
+ column: 55,
+ shouldLink: true,
+ tooltip: "View source in Debugger → http://myfile.com/mahscripts.js:10:55",
+ });
+
+ // Check that line and column can be strings,
+ // and that the `0` rendering rules apply when they are strings as well
+ yield checkFrameComponent({
+ frame: {
+ source: "http://myfile.com/mahscripts.js",
+ line: "0",
+ column: "55",
+ }
+ }, {
+ file: "mahscripts.js",
+ shouldLink: true,
+ tooltip: "View source in Debugger → http://myfile.com/mahscripts.js",
+ });
+
+ // Check that the showFullSourceUrl option works correctly
+ yield checkFrameComponent({
+ frame: {
+ source: "http://myfile.com/mahscripts.js",
+ line: 0,
+ },
+ showFullSourceUrl: true
+ }, {
+ file: "http://myfile.com/mahscripts.js",
+ shouldLink: true,
+ tooltip: "View source in Debugger → http://myfile.com/mahscripts.js",
+ });
+
+ // Check that the showFunctionName option works correctly
+ yield checkFrameComponent({
+ frame: {
+ functionDisplayName: "myfun",
+ source: "http://myfile.com/mahscripts.js",
+ line: 0,
+ }
+ }, {
+ functionName: null,
+ file: "mahscripts.js",
+ shouldLink: true,
+ tooltip: "View source in Debugger → http://myfile.com/mahscripts.js",
+ });
+
+ yield checkFrameComponent({
+ frame: {
+ functionDisplayName: "myfun",
+ source: "http://myfile.com/mahscripts.js",
+ line: 0,
+ },
+ showFunctionName: true
+ }, {
+ functionName: "myfun",
+ file: "mahscripts.js",
+ shouldLink: true,
+ tooltip: "View source in Debugger → http://myfile.com/mahscripts.js",
+ });
+
+ // Check that anonymous function name is not displayed unless explicitly enabled
+ yield checkFrameComponent({
+ frame: {
+ source: "http://myfile.com/mahscripts.js",
+ line: 0,
+ },
+ showFunctionName: true
+ }, {
+ functionName: null,
+ file: "mahscripts.js",
+ shouldLink: true,
+ tooltip: "View source in Debugger → http://myfile.com/mahscripts.js",
+ });
+
+ yield checkFrameComponent({
+ frame: {
+ source: "http://myfile.com/mahscripts.js",
+ line: 0,
+ },
+ showFunctionName: true,
+ showAnonymousFunctionName: true
+ }, {
+ functionName: "<anonymous>",
+ file: "mahscripts.js",
+ shouldLink: true,
+ tooltip: "View source in Debugger → http://myfile.com/mahscripts.js",
+ });
+
+ // Check if file is rendered with "/" for root documents when showEmptyPathAsHost is false
+ yield checkFrameComponent({
+ frame: {
+ source: "http://www.cnn.com/",
+ line: "1",
+ },
+ showEmptyPathAsHost: false,
+ }, {
+ file: "/",
+ line: "1",
+ shouldLink: true,
+ tooltip: "View source in Debugger → http://www.cnn.com/:1",
+ });
+
+ // Check if file is rendered with hostname for root documents when showEmptyPathAsHost is true
+ yield checkFrameComponent({
+ frame: {
+ source: "http://www.cnn.com/",
+ line: "1",
+ },
+ showEmptyPathAsHost: true,
+ }, {
+ file: "www.cnn.com",
+ line: "1",
+ shouldLink: true,
+ tooltip: "View source in Debugger → http://www.cnn.com/:1",
+ });
+
+ function* checkFrameComponent(input, expected) {
+ let props = Object.assign({ onClick: () => {} }, input);
+ let frame = ReactDOM.render(Frame(props), window.document.body);
+ yield forceRender(frame);
+
+ let el = frame.getDOMNode();
+ let { source } = input.frame;
+ checkFrameString(Object.assign({ el, source }, expected));
+ }
+
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_notification_box_01.html b/devtools/client/shared/components/test/mochitest/test_notification_box_01.html
new file mode 100644
index 000000000..947fb9803
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_notification_box_01.html
@@ -0,0 +1,108 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test for Notification Box. The test is checking:
+* Basic rendering
+* Appending a notification
+* Notification priority
+* Closing notification
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Notification Box</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ let ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ let React = browserRequire("devtools/client/shared/vendor/react");
+ let { NotificationBox, PriorityLevels } = browserRequire("devtools/client/shared/components/notification-box");
+
+ const renderedBox = shallowRenderComponent(NotificationBox, {});
+ is(renderedBox.type, "div", "NotificationBox is rendered as <div>");
+
+ // Test rendering
+ let boxElement = React.createElement(NotificationBox);
+ let notificationBox = TestUtils.renderIntoDocument(boxElement);
+ let notificationNode = ReactDOM.findDOMNode(notificationBox);
+
+ is(notificationNode.className, "notificationbox",
+ "NotificationBox has expected classname");
+ is(notificationNode.textContent, "",
+ "Empty NotificationBox has no text content");
+
+ checkNumberOfNotifications(notificationBox, 0);
+
+ // Append a notification
+ notificationBox.appendNotification(
+ "Info message",
+ "id1",
+ null,
+ PriorityLevels.PRIORITY_INFO_HIGH
+ );
+
+ is (notificationNode.textContent, "Info message",
+ "The box must display notification message");
+ checkNumberOfNotifications(notificationBox, 1);
+
+ // Append more important notification
+ notificationBox.appendNotification(
+ "Critical message",
+ "id2",
+ null,
+ PriorityLevels.PRIORITY_CRITICAL_BLOCK
+ );
+
+ checkNumberOfNotifications(notificationBox, 1);
+
+ is (notificationNode.textContent, "Critical message",
+ "The box must display more important notification message");
+
+ // Append less important notification
+ notificationBox.appendNotification(
+ "Warning message",
+ "id3",
+ null,
+ PriorityLevels.PRIORITY_WARNING_HIGH
+ );
+
+ checkNumberOfNotifications(notificationBox, 1);
+
+ is (notificationNode.textContent, "Critical message",
+ "The box must still display the more important notification");
+
+ ok(notificationBox.getCurrentNotification(),
+ "There must be current notification");
+
+ notificationBox.getNotificationWithValue("id1").close();
+ checkNumberOfNotifications(notificationBox, 1);
+
+ notificationBox.getNotificationWithValue("id2").close();
+ checkNumberOfNotifications(notificationBox, 1);
+
+ notificationBox.getNotificationWithValue("id3").close();
+ checkNumberOfNotifications(notificationBox, 0);
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+});
+
+function checkNumberOfNotifications(notificationBox, expected) {
+ is(TestUtils.scryRenderedDOMComponentsWithClass(
+ notificationBox, "notification").length, expected,
+ "The notification box must have expected number of notifications");
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_notification_box_02.html b/devtools/client/shared/components/test/mochitest/test_notification_box_02.html
new file mode 100644
index 000000000..ebeb0400d
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_notification_box_02.html
@@ -0,0 +1,70 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test for Notification Box. The test is checking:
+* Using custom callback in a notification
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Notification Box</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ let ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ let React = browserRequire("devtools/client/shared/vendor/react");
+ let { NotificationBox, PriorityLevels } = browserRequire("devtools/client/shared/components/notification-box");
+
+ // Test rendering
+ let boxElement = React.createElement(NotificationBox);
+ let notificationBox = TestUtils.renderIntoDocument(boxElement);
+ let notificationNode = ReactDOM.findDOMNode(notificationBox);
+
+ let callbackExecuted = false;
+
+ // Append a notification.
+ notificationBox.appendNotification(
+ "Info message",
+ "id1",
+ null,
+ PriorityLevels.PRIORITY_INFO_LOW,
+ undefined,
+ (reason) => {
+ callbackExecuted = true;
+ is(reason, "removed", "The reason must be expected string");
+ }
+ );
+
+ is(TestUtils.scryRenderedDOMComponentsWithClass(
+ notificationBox, "notification").length, 1,
+ "There must be one notification");
+
+ let closeButton = notificationNode.querySelector(
+ ".messageCloseButton");
+
+ // Click the close button to close the notification.
+ TestUtils.Simulate.click(closeButton);
+
+ is(TestUtils.scryRenderedDOMComponentsWithClass(
+ notificationBox, "notification").length, 0,
+ "The notification box must be empty now");
+
+ ok(callbackExecuted, "Event callback must be executed.");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_notification_box_03.html b/devtools/client/shared/components/test/mochitest/test_notification_box_03.html
new file mode 100644
index 000000000..d7fc146fe
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_notification_box_03.html
@@ -0,0 +1,84 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test for Notification Box. The test is checking:
+* Using custom buttons in a notification
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Notification Box</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ let ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ let React = browserRequire("devtools/client/shared/vendor/react");
+ let { NotificationBox, PriorityLevels } = browserRequire("devtools/client/shared/components/notification-box");
+
+ // Test rendering
+ let boxElement = React.createElement(NotificationBox);
+ let notificationBox = TestUtils.renderIntoDocument(boxElement);
+ let notificationNode = ReactDOM.findDOMNode(notificationBox);
+
+ let buttonCallbackExecuted = false;
+ var buttons = [{
+ label: "Button1",
+ callback: () => {
+ buttonCallbackExecuted = true;
+
+ // Do not close the notification
+ return true;
+ },
+ }, {
+ label: "Button2",
+ callback: () => {
+ // Close the notification (return value undefined)
+ },
+ }];
+
+ // Append a notification with buttons.
+ notificationBox.appendNotification(
+ "Info message",
+ "id1",
+ null,
+ PriorityLevels.PRIORITY_INFO_LOW,
+ buttons
+ );
+
+ let buttonNodes = notificationNode.querySelectorAll(
+ ".notification-button");
+
+ is(buttonNodes.length, 2, "There must be two buttons");
+
+ // Click the first button
+ TestUtils.Simulate.click(buttonNodes[0]);
+ ok(buttonCallbackExecuted, "Button callback must be executed.");
+
+ is(TestUtils.scryRenderedDOMComponentsWithClass(
+ notificationBox, "notification").length, 1,
+ "There must be one notification");
+
+ // Click the second button (closing the notification)
+ TestUtils.Simulate.click(buttonNodes[1]);
+
+ is(TestUtils.scryRenderedDOMComponentsWithClass(
+ notificationBox, "notification").length, 0,
+ "The notification box must be empty now");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_array.html b/devtools/client/shared/components/test/mochitest/test_reps_array.html
new file mode 100644
index 000000000..6ad7f2c43
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_array.html
@@ -0,0 +1,259 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test ArrayRep rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - ArrayRep</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+"use strict";
+/* import-globals-from head.js */
+
+window.onload = Task.async(function* () {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { ArrayRep } = browserRequire("devtools/client/shared/components/reps/array");
+
+ let componentUnderTest = ArrayRep;
+ const maxLength = {
+ short: 3,
+ long: 300
+ };
+
+ try {
+ yield testBasic();
+
+ // Test property iterator
+ yield testMaxProps();
+ yield testMoreThanShortMaxProps();
+ yield testMoreThanLongMaxProps();
+ yield testRecursiveArray();
+
+ // Test that properties are rendered as expected by ItemRep
+ yield testNested();
+
+ yield testArray();
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+
+ function testBasic() {
+ // Test that correct rep is chosen
+ const stub = [];
+ const renderedRep = shallowRenderComponent(Rep, { object: stub });
+ is(renderedRep.type, ArrayRep.rep,
+ `Rep correctly selects ${ArrayRep.rep.displayName}`);
+
+
+ // Test rendering
+ const defaultOutput = `[]`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `[]`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, "testBasic", componentUnderTest, stub);
+ }
+
+ function testMaxProps() {
+ const stub = [1, "foo", {}];
+ const defaultOutput = `[ 1, "foo", Object ]`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `[3]`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, "testMaxProps", componentUnderTest, stub);
+ }
+
+ function testMoreThanShortMaxProps() {
+ const stub = Array(maxLength.short + 1).fill("foo");
+ const defaultShortOutput = `[ ${Array(maxLength.short).fill("\"foo\"").join(", ")}, 1 more… ]`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultShortOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `[${maxLength.short + 1}]`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultShortOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: `[ ${Array(maxLength.short + 1).fill("\"foo\"").join(", ")} ]`,
+ }
+ ];
+
+ testRepRenderModes(modeTests, "testMoreThanMaxProps", componentUnderTest, stub);
+ }
+
+ function testMoreThanLongMaxProps() {
+ const stub = Array(maxLength.long + 1).fill("foo");
+ const defaultShortOutput = `[ ${Array(maxLength.short).fill("\"foo\"").join(", ")}, ${maxLength.long + 1 - maxLength.short} more… ]`;
+ const defaultLongOutput = `[ ${Array(maxLength.long).fill("\"foo\"").join(", ")}, 1 more… ]`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultShortOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `[${maxLength.long + 1}]`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultShortOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultLongOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, "testMoreThanMaxProps", componentUnderTest, stub);
+ }
+
+ function testRecursiveArray() {
+ let stub = [1];
+ stub.push(stub);
+ const defaultOutput = `[ 1, [2] ]`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `[2]`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, "testRecursiveArray", componentUnderTest, stub);
+ }
+
+ function testNested() {
+ let stub = [
+ {
+ p1: "s1",
+ p2: ["a1", "a2", "a3"],
+ p3: "s3",
+ p4: "s4"
+ }
+ ];
+ const defaultOutput = `[ Object ]`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `[1]`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, "testNested", componentUnderTest, stub);
+ }
+
+ function testArray() {
+ let stub = [
+ "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"
+ ];
+
+ const defaultOutput = `[ "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" ]`;
+ const shortOutput = `[ "a", "b", "c", 23 more… ]`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: shortOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `[26]`,
+ },
+ {
+ mode: "short",
+ expectedOutput: shortOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, "testNested", componentUnderTest, stub);
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_attribute.html b/devtools/client/shared/components/test/mochitest/test_reps_attribute.html
new file mode 100644
index 000000000..aa8a5dfad
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_attribute.html
@@ -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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test Attribute rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - Attribute</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { Attribute } = browserRequire("devtools/client/shared/components/reps/attribute");
+
+ let gripStub = {
+ "type": "object",
+ "class": "Attr",
+ "actor": "server1.conn19.obj65",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 2,
+ "nodeName": "class",
+ "value": "autocomplete-suggestions"
+ }
+ };
+
+ // Test that correct rep is chosen
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, Attribute.rep, `Rep correctly selects ${Attribute.rep.displayName}`);
+
+ // Test rendering
+ const renderedComponent = renderComponent(Attribute.rep, { object: gripStub });
+ is(renderedComponent.textContent, "class=\"autocomplete-suggestions\"", "Attribute rep has expected text content");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_comment-node.html b/devtools/client/shared/components/test/mochitest/test_reps_comment-node.html
new file mode 100644
index 000000000..4e03d8b30
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_comment-node.html
@@ -0,0 +1,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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test comment-node rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - comment-node</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+"use strict";
+
+window.onload = Task.async(function* () {
+ try {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { CommentNode } = browserRequire("devtools/client/shared/components/reps/comment-node");
+
+ let gripStub = {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj47",
+ "class": "Comment",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 8,
+ "nodeName": "#comment",
+ "textContent": "test\nand test\nand test\nand test\nand test\nand test\nand test"
+ }
+ };
+
+ // Test that correct rep is chosen.
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, CommentNode.rep,
+ `Rep correctly selects ${CommentNode.rep.displayName}`);
+
+ // Test rendering.
+ const renderedComponent = renderComponent(CommentNode.rep, { object: gripStub });
+ is(renderedComponent.className, "objectBox theme-comment",
+ "CommentNode rep has expected class names");
+ is(renderedComponent.textContent,
+ `<!-- test\nand test\nand test\nan…d test\nand test\nand test -->`,
+ "CommentNode rep has expected text content");
+
+ // Test tiny rendering.
+ const tinyRenderedComponent = renderComponent(CommentNode.rep, {
+ object: gripStub,
+ mode: "tiny"
+ });
+ is(tinyRenderedComponent.textContent,
+ `<!-- test\\nand test\\na… test\\nand test -->`,
+ "CommentNode rep has expected text content in tiny mode");
+
+ // Test long rendering.
+ const longRenderedComponent = renderComponent(CommentNode.rep, {
+ object: gripStub,
+ mode: "long"
+ });
+ is(longRenderedComponent.textContent, `<!-- ${gripStub.preview.textContent} -->`,
+ "CommentNode rep has expected text content in long mode");
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_date-time.html b/devtools/client/shared/components/test/mochitest/test_reps_date-time.html
new file mode 100644
index 000000000..a82783b6b
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_date-time.html
@@ -0,0 +1,79 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test DateTime rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - DateTime</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { DateTime } = browserRequire("devtools/client/shared/components/reps/date-time");
+
+ try {
+ testValid();
+ testInvalid();
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+
+ function testValid() {
+ let gripStub = {
+ "type": "object",
+ "class": "Date",
+ "actor": "server1.conn0.child1/obj32",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "timestamp": 1459372644859
+ }
+ };
+
+ // Test that correct rep is chosen
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, DateTime.rep, `Rep correctly selects ${DateTime.rep.displayName}`);
+
+ // Test rendering
+ const renderedComponent = renderComponent(DateTime.rep, { object: gripStub });
+ is(renderedComponent.textContent, "2016-03-30T21:17:24.859Z", "DateTime rep has expected text content for valid date");
+ }
+
+ function testInvalid() {
+ let gripStub = {
+ "type": "object",
+ "actor": "server1.conn0.child1/obj32",
+ "class": "Date",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "timestamp": {
+ "type": "NaN"
+ }
+ }
+ };
+
+ // Test rendering
+ const renderedComponent = renderComponent(DateTime.rep, { object: gripStub });
+ is(renderedComponent.textContent, "Invalid Date", "DateTime rep has expected text content for invalid date");
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_document.html b/devtools/client/shared/components/test/mochitest/test_reps_document.html
new file mode 100644
index 000000000..2afabca44
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_document.html
@@ -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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test Document rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - Document</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { Document } = browserRequire("devtools/client/shared/components/reps/document");
+
+ try {
+ let gripStub = {
+ "type": "object",
+ "class": "HTMLDocument",
+ "actor": "server1.conn17.obj115",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 1,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 9,
+ "nodeName": "#document",
+ "location": "https://www.mozilla.org/en-US/firefox/new/"
+ }
+ };
+
+ // Test that correct rep is chosen
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, Document.rep, `Rep correctly selects ${Document.rep.displayName}`);
+
+ // Test rendering
+ const renderedComponent = renderComponent(Document.rep, { object: gripStub });
+ is(renderedComponent.textContent, "https://www.mozilla.org/en-US/firefox/new/", "Document rep has expected text content");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_element-node.html b/devtools/client/shared/components/test/mochitest/test_reps_element-node.html
new file mode 100644
index 000000000..d4e22c7ab
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_element-node.html
@@ -0,0 +1,341 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test Element node rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - Element node</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+"use strict";
+
+window.onload = Task.async(function* () {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { ElementNode } = browserRequire("devtools/client/shared/components/reps/element-node");
+
+ try {
+ yield testBodyNode();
+ yield testDocumentElement();
+ yield testNode();
+ yield testNodeWithLeadingAndTrailingSpacesClassName();
+ yield testNodeWithoutAttributes();
+ yield testLotsOfAttributes();
+ yield testSvgNode();
+ yield testSvgNodeInXHTML();
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+
+ function testBodyNode() {
+ const stub = getGripStub("testBodyNode");
+ const renderedRep = shallowRenderComponent(Rep, { object: stub });
+ is(renderedRep.type, ElementNode.rep,
+ `Rep correctly selects ${ElementNode.rep.displayName} for body node`);
+
+ const renderedComponent = renderComponent(ElementNode.rep, { object: stub });
+ is(renderedComponent.textContent, `<body id="body-id" class="body-class">`,
+ "Element node rep has expected text content for body node");
+
+ const tinyRenderedComponent = renderComponent(
+ ElementNode.rep, { object: stub, mode: "tiny" });
+ is(tinyRenderedComponent.textContent, `body#body-id.body-class`,
+ "Element node rep has expected text content for body node in tiny mode");
+ }
+
+ function testDocumentElement() {
+ const stub = getGripStub("testDocumentElement");
+ const renderedRep = shallowRenderComponent(Rep, { object: stub });
+ is(renderedRep.type, ElementNode.rep,
+ `Rep correctly selects ${ElementNode.rep.displayName} for document element node`);
+
+ const renderedComponent = renderComponent(ElementNode.rep, { object: stub });
+ is(renderedComponent.textContent, `<html dir="ltr" lang="en-US">`,
+ "Element node rep has expected text content for document element node");
+
+ const tinyRenderedComponent = renderComponent(
+ ElementNode.rep, { object: stub, mode: "tiny" });
+ is(tinyRenderedComponent.textContent, `html`,
+ "Element node rep has expected text content for document element in tiny mode");
+ }
+
+ function testNode() {
+ const stub = getGripStub("testNode");
+ const renderedRep = shallowRenderComponent(Rep, { object: stub });
+ is(renderedRep.type, ElementNode.rep,
+ `Rep correctly selects ${ElementNode.rep.displayName} for element node`);
+
+ const renderedComponent = renderComponent(ElementNode.rep, { object: stub });
+ is(renderedComponent.textContent,
+ `<input id="newtab-customize-button" class="bar baz" dir="ltr" ` +
+ `title="Customize your New Tab page" value="foo" type="button">`,
+ "Element node rep has expected text content for element node");
+
+ const tinyRenderedComponent = renderComponent(
+ ElementNode.rep, { object: stub, mode: "tiny" });
+ is(tinyRenderedComponent.textContent,
+ `input#newtab-customize-button.bar.baz`,
+ "Element node rep has expected text content for element node in tiny mode");
+ }
+
+ function testNodeWithLeadingAndTrailingSpacesClassName() {
+ const stub = getGripStub("testNodeWithLeadingAndTrailingSpacesClassName");
+ const renderedRep = shallowRenderComponent(Rep, { object: stub });
+ is(renderedRep.type, ElementNode.rep,
+ `Rep correctly selects ${ElementNode.rep.displayName} for element node`);
+
+ const renderedComponent = renderComponent(ElementNode.rep, { object: stub });
+ is(renderedComponent.textContent,
+ `<body id="nightly-whatsnew" class=" html-ltr ">`,
+ "Element node rep output element node with the class trailing spaces");
+
+ const tinyRenderedComponent = renderComponent(
+ ElementNode.rep, { object: stub, mode: "tiny" });
+ is(tinyRenderedComponent.textContent,
+ `body#nightly-whatsnew.html-ltr`,
+ "Element node rep does not show leading nor trailing spaces " +
+ "on class attribute in tiny mode");
+ }
+
+ function testNodeWithoutAttributes() {
+ const stub = getGripStub("testNodeWithoutAttributes");
+
+ const renderedComponent = renderComponent(ElementNode.rep, { object: stub });
+ is(renderedComponent.textContent, "<p>",
+ "Element node rep has expected text content for element node without attributes");
+
+ const tinyRenderedComponent = renderComponent(
+ ElementNode.rep, { object: stub, mode: "tiny" });
+ is(tinyRenderedComponent.textContent, `p`,
+ "Element node rep has expected text content for element node without attributes");
+ }
+
+ function testLotsOfAttributes() {
+ const stub = getGripStub("testLotsOfAttributes");
+
+ const renderedComponent = renderComponent(ElementNode.rep, { object: stub });
+ is(renderedComponent.textContent,
+ '<p id="lots-of-attributes" a="" b="" c="" d="" e="" f="" g="" ' +
+ 'h="" i="" j="" k="" l="" m="" n="">',
+ "Element node rep has expected text content for node with lots of attributes");
+
+ const tinyRenderedComponent = renderComponent(
+ ElementNode.rep, { object: stub, mode: "tiny" });
+ is(tinyRenderedComponent.textContent, `p#lots-of-attributes`,
+ "Element node rep has expected text content for node in tiny mode");
+ }
+
+ function testSvgNode() {
+ const stub = getGripStub("testSvgNode");
+
+ const renderedRep = shallowRenderComponent(Rep, { object: stub });
+ is(renderedRep.type, ElementNode.rep,
+ `Rep correctly selects ${ElementNode.rep.displayName} for SVG element node`);
+
+ const renderedComponent = renderComponent(ElementNode.rep, { object: stub });
+ is(renderedComponent.textContent,
+ '<clipPath id="clip" class="svg-element">',
+ "Element node rep has expected text content for SVG element node");
+
+ const tinyRenderedComponent = renderComponent(
+ ElementNode.rep, { object: stub, mode: "tiny" });
+ is(tinyRenderedComponent.textContent, `clipPath#clip.svg-element`,
+ "Element node rep has expected text content for SVG element node in tiny mode");
+ }
+
+ function testSvgNodeInXHTML() {
+ const stub = getGripStub("testSvgNodeInXHTML");
+
+ const renderedRep = shallowRenderComponent(Rep, { object: stub });
+ is(renderedRep.type, ElementNode.rep,
+ `Rep correctly selects ${ElementNode.rep.displayName} for XHTML SVG element node`);
+
+ const renderedComponent = renderComponent(ElementNode.rep, { object: stub });
+ is(renderedComponent.textContent,
+ '<svg:circle class="svg-element" cx="0" cy="0" r="5">',
+ "Element node rep has expected text content for XHTML SVG element node");
+
+ const tinyRenderedComponent = renderComponent(
+ ElementNode.rep, { object: stub, mode: "tiny" });
+ is(tinyRenderedComponent.textContent, `svg:circle.svg-element`,
+ "Element node rep has expected text content for XHTML SVG element in tiny mode");
+ }
+
+ function getGripStub(name) {
+ switch (name) {
+ case "testBodyNode":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj30",
+ "class": "HTMLBodyElement",
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 1,
+ "nodeName": "body",
+ "attributes": {
+ "class": "body-class",
+ "id": "body-id"
+ },
+ "attributesLength": 2
+ }
+ };
+ case "testDocumentElement":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj40",
+ "class": "HTMLHtmlElement",
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 1,
+ "nodeName": "html",
+ "attributes": {
+ "dir": "ltr",
+ "lang": "en-US"
+ },
+ "attributesLength": 2
+ }
+ };
+ case "testNode":
+ return {
+ "type": "object",
+ "actor": "server1.conn2.child1/obj116",
+ "class": "HTMLInputElement",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 1,
+ "nodeName": "input",
+ "attributes": {
+ "id": "newtab-customize-button",
+ "dir": "ltr",
+ "title": "Customize your New Tab page",
+ "class": "bar baz",
+ "value": "foo",
+ "type": "button"
+ },
+ "attributesLength": 6
+ }
+ };
+ case "testNodeWithLeadingAndTrailingSpacesClassName":
+ return {
+ "type": "object",
+ "actor": "server1.conn3.child1/obj59",
+ "class": "HTMLBodyElement",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 1,
+ "nodeName": "body",
+ "attributes": {
+ "id": "nightly-whatsnew",
+ "class": " html-ltr "
+ },
+ "attributesLength": 2
+ }
+ };
+ case "testNodeWithoutAttributes":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj32",
+ "class": "HTMLParagraphElement",
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 1,
+ "nodeName": "p",
+ "attributes": {},
+ "attributesLength": 1
+ }
+ };
+ case "testLotsOfAttributes":
+ return {
+ "type": "object",
+ "actor": "server1.conn2.child1/obj30",
+ "class": "HTMLParagraphElement",
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 1,
+ "nodeName": "p",
+ "attributes": {
+ "id": "lots-of-attributes",
+ "a": "",
+ "b": "",
+ "c": "",
+ "d": "",
+ "e": "",
+ "f": "",
+ "g": "",
+ "h": "",
+ "i": "",
+ "j": "",
+ "k": "",
+ "l": "",
+ "m": "",
+ "n": ""
+ },
+ "attributesLength": 15
+ }
+ };
+ case "testSvgNode":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj42",
+ "class": "SVGClipPathElement",
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 1,
+ "nodeName": "clipPath",
+ "attributes": {
+ "id": "clip",
+ "class": "svg-element"
+ },
+ "attributesLength": 0
+ }
+ };
+ case "testSvgNodeInXHTML":
+ return {
+ "type": "object",
+ "actor": "server1.conn3.child1/obj34",
+ "class": "SVGCircleElement",
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 1,
+ "nodeName": "svg:circle",
+ "attributes": {
+ "class": "svg-element",
+ "cx": "0",
+ "cy": "0",
+ "r": "5"
+ },
+ "attributesLength": 3
+ }
+ };
+ }
+ return null;
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_event.html b/devtools/client/shared/components/test/mochitest/test_reps_event.html
new file mode 100644
index 000000000..7dfe72d6f
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_event.html
@@ -0,0 +1,300 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test Event rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - Event</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { Event } = browserRequire("devtools/client/shared/components/reps/event");
+
+ try {
+ // Test that correct rep is chosen
+ const renderedRep = shallowRenderComponent(Rep, { object: getGripStub("testEvent") });
+ is(renderedRep.type, Event.rep, `Rep correctly selects ${Event.rep.displayName}`);
+
+ yield testEvent();
+ yield testMouseEvent();
+ yield testKeyboardEvent();
+ yield testMessageEvent();
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+
+ function testEvent() {
+ const renderedComponent = renderComponent(Event.rep, { object: getGripStub("testEvent") });
+ is(renderedComponent.textContent,
+ "Event { isTrusted: true, eventPhase: 2, bubbles: false, 7 more… }",
+ "Event rep has expected text content for an event");
+ }
+
+ function testMouseEvent() {
+ const renderedComponent = renderComponent(Event.rep, { object: getGripStub("testMouseEvent") });
+ is(renderedComponent.textContent,
+ "MouseEvent { clientX: 62, clientY: 18, layerX: 0, 2 more… }",
+ "Event rep has expected text content for a mouse event");
+ }
+
+ function testKeyboardEvent() {
+ const renderedComponent = renderComponent(Event.rep, { object: getGripStub("testKeyboardEvent") });
+ is(renderedComponent.textContent,
+ "KeyboardEvent { key: \"Control\", charCode: 0, keyCode: 17 }",
+ "Event rep has expected text content for a keyboard event");
+ }
+
+ function testMessageEvent() {
+ const renderedComponent = renderComponent(Event.rep, { object: getGripStub("testMessageEvent") });
+ is(renderedComponent.textContent,
+ "MessageEvent { isTrusted: false, data: \"test data\", origin: \"null\", 7 more… }",
+ "Event rep has expected text content for a message event");
+ }
+
+ function getGripStub(name) {
+ switch (name) {
+ case "testEvent":
+ return {
+ "type": "object",
+ "class": "Event",
+ "actor": "server1.conn23.obj35",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 1,
+ "preview": {
+ "kind": "DOMEvent",
+ "type": "beforeprint",
+ "properties": {
+ "isTrusted": true,
+ "currentTarget": {
+ "type": "object",
+ "class": "Window",
+ "actor": "server1.conn23.obj37",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 760,
+ "preview": {
+ "kind": "ObjectWithURL",
+ "url": "http://example.com"
+ }
+ },
+ "eventPhase": 2,
+ "bubbles": false,
+ "cancelable": false,
+ "defaultPrevented": false,
+ "timeStamp": 1466780008434005,
+ "originalTarget": {
+ "type": "object",
+ "class": "Window",
+ "actor": "server1.conn23.obj38",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 760,
+ "preview": {
+ "kind": "ObjectWithURL",
+ "url": "http://example.com"
+ }
+ },
+ "explicitOriginalTarget": {
+ "type": "object",
+ "class": "Window",
+ "actor": "server1.conn23.obj39",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 760,
+ "preview": {
+ "kind": "ObjectWithURL",
+ "url": "http://example.com"
+ }
+ },
+ "NONE": 0
+ },
+ "target": {
+ "type": "object",
+ "class": "Window",
+ "actor": "server1.conn23.obj36",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 760,
+ "preview": {
+ "kind": "ObjectWithURL",
+ "url": "http://example.com"
+ }
+ }
+ }
+ };
+
+ case "testMouseEvent":
+ return {
+ "type": "object",
+ "class": "MouseEvent",
+ "actor": "server1.conn20.obj39",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 1,
+ "preview": {
+ "kind": "DOMEvent",
+ "type": "click",
+ "properties": {
+ "buttons": 0,
+ "clientX": 62,
+ "clientY": 18,
+ "layerX": 0,
+ "layerY": 0
+ },
+ "target": {
+ "type": "object",
+ "class": "HTMLDivElement",
+ "actor": "server1.conn20.obj40",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 1,
+ "nodeName": "div",
+ "attributes": {
+ "id": "test"
+ },
+ "attributesLength": 1
+ }
+ }
+ }
+ };
+
+ case "testKeyboardEvent":
+ return {
+ "type": "object",
+ "class": "KeyboardEvent",
+ "actor": "server1.conn21.obj49",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 1,
+ "preview": {
+ "kind": "DOMEvent",
+ "type": "keyup",
+ "properties": {
+ "key": "Control",
+ "charCode": 0,
+ "keyCode": 17
+ },
+ "target": {
+ "type": "object",
+ "class": "HTMLBodyElement",
+ "actor": "server1.conn21.obj50",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 1,
+ "nodeName": "body",
+ "attributes": {},
+ "attributesLength": 0
+ }
+ },
+ "eventKind": "key",
+ "modifiers": []
+ }
+ };
+
+ case "testMessageEvent":
+ return {
+ "type": "object",
+ "class": "MessageEvent",
+ "actor": "server1.conn3.obj34",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 1,
+ "preview": {
+ "kind": "DOMEvent",
+ "type": "message",
+ "properties": {
+ "isTrusted": false,
+ "data": "test data",
+ "origin": "null",
+ "lastEventId": "",
+ "source": {
+ "type": "object",
+ "class": "Window",
+ "actor": "server1.conn3.obj36",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 760,
+ "preview": {
+ "kind": "ObjectWithURL",
+ "url": ""
+ }
+ },
+ "ports": {
+ "type": "object",
+ "class": "Array",
+ "actor": "server1.conn3.obj37",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0
+ },
+ "currentTarget": {
+ "type": "object",
+ "class": "Window",
+ "actor": "server1.conn3.obj38",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 760,
+ "preview": {
+ "kind": "ObjectWithURL",
+ "url": ""
+ }
+ },
+ "eventPhase": 2,
+ "bubbles": false,
+ "cancelable": false
+ },
+ "target": {
+ "type": "object",
+ "class": "Window",
+ "actor": "server1.conn3.obj35",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 760,
+ "preview": {
+ "kind": "ObjectWithURL",
+ "url": ""
+ }
+ }
+ }
+ };
+
+ }
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_function.html b/devtools/client/shared/components/test/mochitest/test_reps_function.html
new file mode 100644
index 000000000..ede694329
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_function.html
@@ -0,0 +1,206 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test Func rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - Func</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { Func } = browserRequire("devtools/client/shared/components/reps/function");
+
+ const componentUnderTest = Func;
+
+ try {
+ // Test that correct rep is chosen
+ const gripStub = getGripStub("testNamed");
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, Func.rep, `Rep correctly selects ${Func.rep.displayName}`);
+
+ yield testNamed();
+ yield testVarNamed();
+ yield testAnon();
+ yield testLongName();
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+
+ function testNamed() {
+ // Test declaration: `function testName{ let innerVar = "foo" }`
+ const testName = "testNamed";
+
+ const defaultOutput = `testName()`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testUserNamed() {
+ // Test declaration: `function testName{ let innerVar = "foo" }`
+ const testName = "testUserNamed";
+
+ const defaultOutput = `testUserName()`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testVarNamed() {
+ // Test declaration: `let testVarName = function() { }`
+ const testName = "testVarNamed";
+
+ const defaultOutput = `testVarName()`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testAnon() {
+ // Test declaration: `() => {}`
+ const testName = "testAnon";
+
+ const defaultOutput = `function()`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testLongName() {
+ // Test declaration: `let f = function loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong() { }`
+ const testName = "testLongName";
+
+ const defaultOutput = `looooooooooooooooooooooooooooooooooooooooooooooooo\u2026ooooooooooooooooooooooooooooooooooooooooooooong()`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function getGripStub(functionName) {
+ switch (functionName) {
+ case "testNamed":
+ return {
+ "type": "object",
+ "class": "Function",
+ "actor": "server1.conn6.obj35",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "name": "testName",
+ "displayName": "testName",
+ "location": {
+ "url": "debugger eval code",
+ "line": 1
+ }
+ };
+
+ case "testUserNamed":
+ return {
+ "type": "object",
+ "class": "Function",
+ "actor": "server1.conn6.obj35",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "name": "testName",
+ "userDisplayName": "testUserName",
+ "displayName": "testName",
+ "location": {
+ "url": "debugger eval code",
+ "line": 1
+ }
+ };
+
+ case "testVarNamed":
+ return {
+ "type": "object",
+ "class": "Function",
+ "actor": "server1.conn7.obj41",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "displayName": "testVarName",
+ "location": {
+ "url": "debugger eval code",
+ "line": 1
+ }
+ };
+
+ case "testAnon":
+ return {
+ "type": "object",
+ "class": "Function",
+ "actor": "server1.conn7.obj45",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "location": {
+ "url": "debugger eval code",
+ "line": 1
+ }
+ };
+
+ case "testLongName":
+ return {
+ "type": "object",
+ "class": "Function",
+ "actor": "server1.conn7.obj67",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "name": "loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong",
+ "displayName": "loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong",
+ "location": {
+ "url": "debugger eval code",
+ "line": 1
+ }
+ };
+ }
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_grip-array.html b/devtools/client/shared/components/test/mochitest/test_reps_grip-array.html
new file mode 100644
index 000000000..db4f0296e
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_grip-array.html
@@ -0,0 +1,707 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test GripArray rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - GripArray</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { GripArray } = browserRequire("devtools/client/shared/components/reps/grip-array");
+
+ let componentUnderTest = GripArray;
+ const maxLength = {
+ short: 3,
+ long: 300
+ };
+
+ try {
+ yield testBasic();
+
+ // Test property iterator
+ yield testMaxProps();
+ yield testMoreThanShortMaxProps();
+ yield testMoreThanLongMaxProps();
+ yield testRecursiveArray();
+ yield testPreviewLimit();
+ yield testNamedNodeMap();
+ yield testNodeList();
+ yield testDocumentFragment();
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+
+ function testBasic() {
+ // Test array: `[]`
+ const testName = "testBasic";
+
+ // Test that correct rep is chosen
+ const gripStub = getGripStub("testBasic");
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, GripArray.rep, `Rep correctly selects ${GripArray.rep.displayName}`);
+
+ // Test rendering
+ const defaultOutput = `Array []`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `[]`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testMaxProps() {
+ // Test array: `[1, "foo", {}]`;
+ const testName = "testMaxProps";
+
+ const defaultOutput = `Array [ 1, "foo", Object ]`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `[3]`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testMoreThanShortMaxProps() {
+ // Test array = `["test string"…] //4 items`
+ const testName = "testMoreThanShortMaxProps";
+
+ const defaultOutput = `Array [ ${Array(maxLength.short).fill("\"test string\"").join(", ")}, 1 more… ]`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `[${maxLength.short + 1}]`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: `Array [ ${Array(maxLength.short + 1).fill("\"test string\"").join(", ")} ]`,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testMoreThanLongMaxProps() {
+ // Test array = `["test string"…] //301 items`
+ const testName = "testMoreThanLongMaxProps";
+
+ const defaultShortOutput = `Array [ ${Array(maxLength.short).fill("\"test string\"").join(", ")}, ${maxLength.long + 1 - maxLength.short} more… ]`;
+ const defaultLongOutput = `Array [ ${Array(maxLength.long).fill("\"test string\"").join(", ")}, 1 more… ]`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultShortOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `[${maxLength.long + 1}]`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultShortOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultLongOutput
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testRecursiveArray() {
+ // Test array = `let a = []; a = [a]`
+ const testName = "testRecursiveArray";
+
+ const defaultOutput = `Array [ [1] ]`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `[1]`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testPreviewLimit() {
+ const testName = "testPreviewLimit";
+
+ const shortOutput = `Array [ 0, 1, 2, 8 more… ]`;
+ const defaultOutput = `Array [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 1 more… ]`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: shortOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `[11]`,
+ },
+ {
+ mode: "short",
+ expectedOutput: shortOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testNamedNodeMap() {
+ const testName = "testNamedNodeMap";
+
+ const defaultOutput = `NamedNodeMap [ class="myclass", cellpadding="7", border="3" ]`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `[3]`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testNodeList() {
+ const testName = "testNodeList";
+ const defaultOutput = "NodeList [ button#btn-1.btn.btn-log, " +
+ "button#btn-2.btn.btn-err, button#btn-3.btn.btn-count ]";
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `[3]`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testDocumentFragment() {
+ const testName = "testDocumentFragment";
+
+ const defaultOutput = "DocumentFragment [ li#li-0.list-element, " +
+ "li#li-1.list-element, li#li-2.list-element, 2 more… ]";
+
+ const longOutput = "DocumentFragment [ " +
+ "li#li-0.list-element, li#li-1.list-element, li#li-2.list-element, " +
+ "li#li-3.list-element, li#li-4.list-element ]";
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `[5]`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: longOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function getGripStub(functionName) {
+ switch (functionName) {
+ case "testBasic":
+ return {
+ "type": "object",
+ "class": "Array",
+ "actor": "server1.conn0.obj35",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 1,
+ "preview": {
+ "kind": "ArrayLike",
+ "length": 0,
+ "items": []
+ }
+ };
+
+ case "testMaxProps":
+ return {
+ "type": "object",
+ "class": "Array",
+ "actor": "server1.conn1.obj35",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 4,
+ "preview": {
+ "kind": "ArrayLike",
+ "length": 3,
+ "items": [
+ 1,
+ "foo",
+ {
+ "type": "object",
+ "class": "Object",
+ "actor": "server1.conn1.obj36",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0
+ }
+ ]
+ }
+ };
+
+ case "testMoreThanShortMaxProps":
+ let shortArrayGrip = {
+ "type": "object",
+ "class": "Array",
+ "actor": "server1.conn1.obj35",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 4,
+ "preview": {
+ "kind": "ArrayLike",
+ "length": maxLength.short + 1,
+ "items": []
+ }
+ };
+
+ // Generate array grip with length 4, which is more that the maximum
+ // limit in case of the 'short' mode.
+ for (let i = 0; i < maxLength.short + 1; i++) {
+ shortArrayGrip.preview.items.push("test string");
+ }
+
+ return shortArrayGrip;
+
+ case "testMoreThanLongMaxProps":
+ let longArrayGrip = {
+ "type": "object",
+ "class": "Array",
+ "actor": "server1.conn1.obj35",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 4,
+ "preview": {
+ "kind": "ArrayLike",
+ "length": maxLength.long + 1,
+ "items": []
+ }
+ };
+
+ // Generate array grip with length 301, which is more that the maximum
+ // limit in case of the 'long' mode.
+ for (let i = 0; i < maxLength.long + 1; i++) {
+ longArrayGrip.preview.items.push("test string");
+ }
+
+ return longArrayGrip;
+
+ case "testPreviewLimit":
+ return {
+ "type": "object",
+ "class": "Array",
+ "actor": "server1.conn1.obj31",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 12,
+ "preview": {
+ "kind": "ArrayLike",
+ "length": 11,
+ "items": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
+ }
+ };
+
+ case "testRecursiveArray":
+ return {
+ "type": "object",
+ "class": "Array",
+ "actor": "server1.conn3.obj42",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 2,
+ "preview": {
+ "kind": "ArrayLike",
+ "length": 1,
+ "items": [
+ {
+ "type": "object",
+ "class": "Array",
+ "actor": "server1.conn3.obj43",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 2,
+ "preview": {
+ "kind": "ArrayLike",
+ "length": 1
+ }
+ }
+ ]
+ }
+ };
+
+ case "testNamedNodeMap":
+ return {
+ "type": "object",
+ "class": "NamedNodeMap",
+ "actor": "server1.conn3.obj42",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 6,
+ "preview": {
+ "kind": "ArrayLike",
+ "length": 3,
+ "items": [
+ {
+ "type": "object",
+ "class": "Attr",
+ "actor": "server1.conn3.obj43",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 2,
+ "nodeName": "class",
+ "value": "myclass"
+ }
+ },
+ {
+ "type": "object",
+ "class": "Attr",
+ "actor": "server1.conn3.obj44",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 2,
+ "nodeName": "cellpadding",
+ "value": "7"
+ }
+ },
+ {
+ "type": "object",
+ "class": "Attr",
+ "actor": "server1.conn3.obj44",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 2,
+ "nodeName": "border",
+ "value": "3"
+ }
+ }
+ ]
+ }
+ };
+
+ case "testNodeList":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj51",
+ "class": "NodeList",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 3,
+ "preview": {
+ "kind": "ArrayLike",
+ "length": 3,
+ "items": [
+ {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj52",
+ "class": "HTMLButtonElement",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 1,
+ "nodeName": "button",
+ "attributes": {
+ "id": "btn-1",
+ "class": "btn btn-log",
+ "type": "button"
+ },
+ "attributesLength": 3
+ }
+ },
+ {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj53",
+ "class": "HTMLButtonElement",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 1,
+ "nodeName": "button",
+ "attributes": {
+ "id": "btn-2",
+ "class": "btn btn-err",
+ "type": "button"
+ },
+ "attributesLength": 3
+ }
+ },
+ {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj54",
+ "class": "HTMLButtonElement",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 1,
+ "nodeName": "button",
+ "attributes": {
+ "id": "btn-3",
+ "class": "btn btn-count",
+ "type": "button"
+ },
+ "attributesLength": 3
+ }
+ }
+ ]
+ }
+ };
+
+ case "testDocumentFragment":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj45",
+ "class": "DocumentFragment",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 11,
+ "nodeName": "#document-fragment",
+ "childNodesLength": 5,
+ "childNodes": [
+ {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj46",
+ "class": "HTMLLIElement",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 1,
+ "nodeName": "li",
+ "attributes": {
+ "id": "li-0",
+ "class": "list-element"
+ },
+ "attributesLength": 2
+ }
+ },
+ {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj47",
+ "class": "HTMLLIElement",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 1,
+ "nodeName": "li",
+ "attributes": {
+ "id": "li-1",
+ "class": "list-element"
+ },
+ "attributesLength": 2
+ }
+ },
+ {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj48",
+ "class": "HTMLLIElement",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 1,
+ "nodeName": "li",
+ "attributes": {
+ "id": "li-2",
+ "class": "list-element"
+ },
+ "attributesLength": 2
+ }
+ },
+ {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj49",
+ "class": "HTMLLIElement",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 1,
+ "nodeName": "li",
+ "attributes": {
+ "id": "li-3",
+ "class": "list-element"
+ },
+ "attributesLength": 2
+ }
+ },
+ {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj50",
+ "class": "HTMLLIElement",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 1,
+ "nodeName": "li",
+ "attributes": {
+ "id": "li-4",
+ "class": "list-element"
+ },
+ "attributesLength": 2
+ }
+ }
+ ]
+ }
+ };
+ }
+ return null;
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_grip-map.html b/devtools/client/shared/components/test/mochitest/test_reps_grip-map.html
new file mode 100644
index 000000000..18470367c
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_grip-map.html
@@ -0,0 +1,405 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test GripMap rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - GripMap</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+"use strict";
+
+window.onload = Task.async(function* () {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { GripMap } = browserRequire("devtools/client/shared/components/reps/grip-map");
+
+ const componentUnderTest = GripMap;
+
+ try {
+ yield testEmptyMap();
+ yield testSymbolKeyedMap();
+ yield testWeakMap();
+
+ // // Test entries iterator
+ yield testMaxEntries();
+ yield testMoreThanMaxEntries();
+ yield testUninterestingEntries();
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+
+ function testEmptyMap() {
+ // Test object: `new Map()`
+ const testName = "testEmptyMap";
+
+ // Test that correct rep is chosen
+ const gripStub = getGripStub("testEmptyMap");
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, GripMap.rep, `Rep correctly selects ${GripMap.rep.displayName}`);
+
+ // Test rendering
+ const defaultOutput = `Map { }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: "Map",
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testSymbolKeyedMap() {
+ // Test object:
+ // `new Map([[Symbol("a"), "value-a"], [Symbol("b"), "value-b"]])`
+ const testName = "testSymbolKeyedMap";
+
+ const defaultOutput = `Map { Symbol(a): "value-a", Symbol(b): "value-b" }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: "Map",
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testWeakMap() {
+ // Test object: `new WeakMap([[{a: "key-a"}, "value-a"]])`
+ const testName = "testWeakMap";
+
+ // Test that correct rep is chosen
+ const gripStub = getGripStub("testWeakMap");
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, GripMap.rep, `Rep correctly selects ${GripMap.rep.displayName}`);
+
+ // Test rendering
+ const defaultOutput = `WeakMap { Object: "value-a" }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: "WeakMap",
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testMaxEntries() {
+ // Test object:
+ // `new Map([["key-a","value-a"], ["key-b","value-b"], ["key-c","value-c"]])`
+ const testName = "testMaxEntries";
+
+ const defaultOutput = `Map { key-a: "value-a", key-b: "value-b", key-c: "value-c" }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: "Map",
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testMoreThanMaxEntries() {
+ // Test object = `new Map(
+ // [["key-0", "value-0"], ["key-1", "value-1"]], …, ["key-100", "value-100"]]}`
+ const testName = "testMoreThanMaxEntries";
+
+ const defaultOutput =
+ `Map { key-0: "value-0", key-1: "value-1", key-2: "value-2", 98 more… }`;
+
+ // Generate string with 101 entries, which is the max limit for 'long' mode.
+ let longString = Array.from({length: 100}).map((_, i) => `key-${i}: "value-${i}"`);
+ const longOutput = `Map { ${longString.join(", ")}, 1 more… }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Map`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: longOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testUninterestingEntries() {
+ // Test object:
+ // `new Map([["key-a",null], ["key-b",undefined], ["key-c","value-c"], ["key-d",4]])`
+ const testName = "testUninterestingEntries";
+
+ const defaultOutput =
+ `Map { key-a: null, key-c: "value-c", key-d: 4, 1 more… }`;
+ const longOutput =
+ `Map { key-a: null, key-b: undefined, key-c: "value-c", key-d: 4 }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Map`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: longOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function getGripStub(functionName) {
+ switch (functionName) {
+ case "testEmptyMap":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj97",
+ "class": "Map",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "MapLike",
+ "size": 0,
+ "entries": []
+ }
+ };
+
+ case "testSymbolKeyedMap":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj118",
+ "class": "Map",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "MapLike",
+ "size": 2,
+ "entries": [
+ [
+ {
+ "type": "symbol",
+ "name": "a"
+ },
+ "value-a"
+ ],
+ [
+ {
+ "type": "symbol",
+ "name": "b"
+ },
+ "value-b"
+ ]
+ ]
+ }
+ };
+
+ case "testWeakMap":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj115",
+ "class": "WeakMap",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "MapLike",
+ "size": 1,
+ "entries": [
+ [
+ {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj116",
+ "class": "Object",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 1
+ },
+ "value-a"
+ ]
+ ]
+ }
+ };
+
+ case "testMaxEntries":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj109",
+ "class": "Map",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "MapLike",
+ "size": 3,
+ "entries": [
+ [
+ "key-a",
+ "value-a"
+ ],
+ [
+ "key-b",
+ "value-b"
+ ],
+ [
+ "key-c",
+ "value-c"
+ ]
+ ]
+ }
+ };
+
+ case "testMoreThanMaxEntries": {
+ let entryNb = 101;
+ return {
+ "type": "object",
+ "class": "Map",
+ "actor": "server1.conn0.obj332",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "MapLike",
+ "size": entryNb,
+ // Generate 101 entries, which is more that the maximum
+ // limit in case of the 'long' mode.
+ "entries": Array.from({length: entryNb}).map((_, i) => {
+ return [`key-${i}`, `value-${i}`];
+ })
+ }
+ };
+ }
+
+ case "testUninterestingEntries":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj111",
+ "class": "Map",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "MapLike",
+ "size": 4,
+ "entries": [
+ [
+ "key-a",
+ {
+ "type": "null"
+ }
+ ],
+ [
+ "key-b",
+ {
+ "type": "undefined"
+ }
+ ],
+ [
+ "key-c",
+ "value-c"
+ ],
+ [
+ "key-d",
+ 4
+ ]
+ ]
+ }
+ };
+ }
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_grip.html b/devtools/client/shared/components/test/mochitest/test_reps_grip.html
new file mode 100644
index 000000000..15d4e1d25
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_grip.html
@@ -0,0 +1,887 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test grip rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - grip</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { Grip } = browserRequire("devtools/client/shared/components/reps/grip");
+
+ const componentUnderTest = Grip;
+
+ try {
+ yield testBasic();
+ yield testBooleanObject();
+ yield testNumberObject();
+ yield testStringObject();
+ yield testProxy();
+ yield testArrayBuffer();
+ yield testSharedArrayBuffer();
+
+ // Test property iterator
+ yield testMaxProps();
+ yield testMoreThanMaxProps();
+ yield testUninterestingProps();
+ yield testNonEnumerableProps();
+
+ // Test that properties are rendered as expected by PropRep
+ yield testNestedObject();
+ yield testNestedArray();
+
+ // Test that 'more' property doesn't clobber the caption.
+ yield testMoreProp();
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+
+ function testBasic() {
+ // Test object: `{}`
+ const testName = "testBasic";
+
+ // Test that correct rep is chosen
+ const gripStub = getGripStub("testBasic");
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, Grip.rep, `Rep correctly selects ${Grip.rep.displayName}`);
+
+ // Test rendering
+ const defaultOutput = `Object { }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Object`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testBooleanObject() {
+ // Test object: `new Boolean(true)`
+ const testName = "testBooleanObject";
+
+ // Test that correct rep is chosen
+ const gripStub = getGripStub(testName);
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, Grip.rep, `Rep correctly selects ${Grip.rep.displayName}`);
+
+ // Test rendering
+ const defaultOutput = `Boolean { true }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Boolean`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testNumberObject() {
+ // Test object: `new Number(42)`
+ const testName = "testNumberObject";
+
+ // Test that correct rep is chosen
+ const gripStub = getGripStub(testName);
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, Grip.rep, `Rep correctly selects ${Grip.rep.displayName}`);
+
+ // Test rendering
+ const defaultOutput = `Number { 42 }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Number`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testStringObject() {
+ // Test object: `new String("foo")`
+ const testName = "testStringObject";
+
+ // Test that correct rep is chosen
+ const gripStub = getGripStub(testName);
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, Grip.rep, `Rep correctly selects ${Grip.rep.displayName}`);
+
+ // Test rendering
+ const defaultOutput = `String { "foo" }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `String`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testProxy() {
+ // Test object: `new Proxy({a:1},[1,2,3])`
+ const testName = "testProxy";
+
+ // Test that correct rep is chosen
+ const gripStub = getGripStub(testName);
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, Grip.rep, `Rep correctly selects ${Grip.rep.displayName}`);
+
+ // Test rendering
+ const defaultOutput = `Proxy { <target>: Object, <handler>: [3] }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Proxy`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testArrayBuffer() {
+ // Test object: `new ArrayBuffer(10)`
+ const testName = "testArrayBuffer";
+
+ // Test that correct rep is chosen
+ const gripStub = getGripStub(testName);
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, Grip.rep, `Rep correctly selects ${Grip.rep.displayName}`);
+
+ // Test rendering
+ const defaultOutput = `ArrayBuffer { byteLength: 10 }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `ArrayBuffer`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testSharedArrayBuffer() {
+ // Test object: `new SharedArrayBuffer(5)`
+ const testName = "testSharedArrayBuffer";
+
+ // Test that correct rep is chosen
+ const gripStub = getGripStub(testName);
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, Grip.rep, `Rep correctly selects ${Grip.rep.displayName}`);
+
+ // Test rendering
+ const defaultOutput = `SharedArrayBuffer { byteLength: 5 }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `SharedArrayBuffer`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testMaxProps() {
+ // Test object: `{a: "a", b: "b", c: "c"}`;
+ const testName = "testMaxProps";
+
+ const defaultOutput = `Object { a: "a", b: "b", c: "c" }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Object`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testMoreThanMaxProps() {
+ // Test object = `{p0: "0", p1: "1", p2: "2", …, p100: "100"}`
+ const testName = "testMoreThanMaxProps";
+
+ const defaultOutput = `Object { p0: "0", p1: "1", p2: "2", 98 more… }`;
+
+ // Generate string with 100 properties, which is the max limit
+ // for 'long' mode.
+ let props = "";
+ for (let i = 0; i < 100; i++) {
+ props += "p" + i + ": \"" + i + "\", ";
+ }
+
+ const longOutput = `Object { ${props}1 more… }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Object`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: longOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testUninterestingProps() {
+ // Test object: `{a: undefined, b: undefined, c: "c", d: 1}`
+ // @TODO This is not how we actually want the preview to be output.
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=1276376
+ const expectedOutput = `Object { a: undefined, b: undefined, c: "c", 1 more… }`;
+ }
+
+ function testNonEnumerableProps() {
+ // Test object: `Object.defineProperty({}, "foo", {enumerable : false});`
+ const testName = "testNonEnumerableProps";
+
+ // Test that correct rep is chosen
+ const gripStub = getGripStub("testNonEnumerableProps");
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, Grip.rep, `Rep correctly selects ${Grip.rep.displayName}`);
+
+ // Test rendering
+ const defaultOutput = `Object { }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Object`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testNestedObject() {
+ // Test object: `{objProp: {id: 1}, strProp: "test string"}`
+ const testName = "testNestedObject";
+
+ const defaultOutput = `Object { objProp: Object, strProp: "test string" }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Object`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testNestedArray() {
+ // Test object: `{arrProp: ["foo", "bar", "baz"]}`
+ const testName = "testNestedArray";
+
+ const defaultOutput = `Object { arrProp: [3] }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Object`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testMoreProp() {
+ // Test object: `{a: undefined, b: 1, more: 2, d: 3}`;
+ const testName = "testMoreProp";
+
+ const defaultOutput = `Object { b: 1, more: 2, d: 3, 1 more… }`;
+ const longOutput = `Object { a: undefined, b: 1, more: 2, d: 3 }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Object`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: longOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function getGripStub(functionName) {
+ switch (functionName) {
+ case "testBasic":
+ return {
+ "type": "object",
+ "class": "Object",
+ "actor": "server1.conn0.obj304",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {},
+ "ownPropertiesLength": 0,
+ "safeGetterValues": {}
+ }
+ };
+
+ case "testMaxProps":
+ return {
+ "type": "object",
+ "class": "Object",
+ "actor": "server1.conn0.obj337",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 3,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {
+ "a": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": "a"
+ },
+ "b": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": "b"
+ },
+ "c": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": "c"
+ }
+ },
+ "ownPropertiesLength": 3,
+ "safeGetterValues": {}
+ }
+ };
+
+ case "testMoreThanMaxProps": {
+ let grip = {
+ "type": "object",
+ "class": "Object",
+ "actor": "server1.conn0.obj332",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 101,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {},
+ "ownPropertiesLength": 101,
+ "safeGetterValues": {}
+ }
+ };
+
+ // Generate 101 properties, which is more that the maximum
+ // limit in case of the 'long' mode.
+ for (let i = 0; i < 101; i++) {
+ grip.preview.ownProperties["p" + i] = {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": i + ""
+ };
+ }
+
+ return grip;
+ }
+
+ case "testUninterestingProps":
+ return {
+ "type": "object",
+ "class": "Object",
+ "actor": "server1.conn0.obj342",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 4,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {
+ "a": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": {
+ "type": "undefined"
+ }
+ },
+ "b": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": {
+ "type": "undefined"
+ }
+ },
+ "c": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": "c"
+ },
+ "d": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": 1
+ }
+ },
+ "ownPropertiesLength": 4,
+ "safeGetterValues": {}
+ }
+ };
+ case "testNonEnumerableProps":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj30",
+ "class": "Object",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 1,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {},
+ "ownPropertiesLength": 1,
+ "safeGetterValues": {}
+ }
+ };
+ case "testNestedObject":
+ return {
+ "type": "object",
+ "class": "Object",
+ "actor": "server1.conn0.obj145",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 2,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {
+ "objProp": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": {
+ "type": "object",
+ "class": "Object",
+ "actor": "server1.conn0.obj146",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 1
+ }
+ },
+ "strProp": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": "test string"
+ }
+ },
+ "ownPropertiesLength": 2,
+ "safeGetterValues": {}
+ }
+ };
+
+ case "testNestedArray":
+ return {
+ "type": "object",
+ "class": "Object",
+ "actor": "server1.conn0.obj326",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 1,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {
+ "arrProp": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": {
+ "type": "object",
+ "class": "Array",
+ "actor": "server1.conn0.obj327",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 4,
+ "preview": {
+ "kind": "ArrayLike",
+ "length": 3
+ }
+ }
+ }
+ },
+ "ownPropertiesLength": 1,
+ "safeGetterValues": {}
+ },
+ };
+
+ case "testMoreProp":
+ return {
+ "type": "object",
+ "class": "Object",
+ "actor": "server1.conn0.obj342",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 4,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {
+ "a": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": {
+ "type": "undefined"
+ }
+ },
+ "b": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": 1
+ },
+ "more": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": 2
+ },
+ "d": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": 3
+ }
+ },
+ "ownPropertiesLength": 4,
+ "safeGetterValues": {}
+ }
+ };
+ case "testBooleanObject":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj57",
+ "class": "Boolean",
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {},
+ "ownPropertiesLength": 0,
+ "safeGetterValues": {},
+ "wrappedValue": true
+ }
+ };
+ case "testNumberObject":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj59",
+ "class": "Number",
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {},
+ "ownPropertiesLength": 0,
+ "safeGetterValues": {},
+ "wrappedValue": 42
+ }
+ };
+ case "testStringObject":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj61",
+ "class": "String",
+ "ownPropertyLength": 4,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {},
+ "ownPropertiesLength": 4,
+ "safeGetterValues": {},
+ "wrappedValue": "foo"
+ }
+ };
+ case "testProxy":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj47",
+ "class": "Proxy",
+ "proxyTarget": {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj48",
+ "class": "Object",
+ "ownPropertyLength": 1
+ },
+ "proxyHandler": {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj49",
+ "class": "Array",
+ "ownPropertyLength": 4,
+ "preview": {
+ "kind": "ArrayLike",
+ "length": 3
+ }
+ },
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {
+ "<target>": {
+ "value": {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj48",
+ "class": "Object",
+ "ownPropertyLength": 1
+ }
+ },
+ "<handler>": {
+ "value": {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj49",
+ "class": "Array",
+ "ownPropertyLength": 4,
+ "preview": {
+ "kind": "ArrayLike",
+ "length": 3
+ }
+ }
+ }
+ },
+ "ownPropertiesLength": 2
+ }
+ };
+ case "testArrayBuffer":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj170",
+ "class": "ArrayBuffer",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {},
+ "ownPropertiesLength": 0,
+ "safeGetterValues": {
+ "byteLength": {
+ "getterValue": 10,
+ "getterPrototypeLevel": 1,
+ "enumerable": false,
+ "writable": true
+ }
+ }
+ }
+ };
+ case "testSharedArrayBuffer":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj171",
+ "class": "SharedArrayBuffer",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {},
+ "ownPropertiesLength": 0,
+ "safeGetterValues": {
+ "byteLength": {
+ "getterValue": 5,
+ "getterPrototypeLevel": 1,
+ "enumerable": false,
+ "writable": true
+ }
+ }
+ }
+ };
+ }
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_infinity.html b/devtools/client/shared/components/test/mochitest/test_reps_infinity.html
new file mode 100644
index 000000000..e3a7e871f
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_infinity.html
@@ -0,0 +1,73 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test Infinity rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - Infinity</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+"use strict";
+
+window.onload = Task.async(function* () {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { InfinityRep } = browserRequire("devtools/client/shared/components/reps/infinity");
+
+ try {
+ yield testInfinity();
+ yield testNegativeInfinity();
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+
+ function testInfinity() {
+ const stub = getGripStub("testInfinity");
+ const renderedRep = shallowRenderComponent(Rep, { object: stub });
+ is(renderedRep.type, InfinityRep.rep,
+ `Rep correctly selects ${InfinityRep.rep.displayName} for Infinity value`);
+
+ const renderedComponent = renderComponent(InfinityRep.rep, { object: stub });
+ is(renderedComponent.textContent, "Infinity",
+ "Infinity rep has expected text content for Infinity");
+ }
+
+ function testNegativeInfinity() {
+ const stub = getGripStub("testNegativeInfinity");
+ const renderedRep = shallowRenderComponent(Rep, { object: stub });
+ is(renderedRep.type, InfinityRep.rep,
+ `Rep correctly selects ${InfinityRep.rep.displayName} for negative Infinity value`);
+
+ const renderedComponent = renderComponent(InfinityRep.rep, { object: stub });
+ is(renderedComponent.textContent, "-Infinity",
+ "Infinity rep has expected text content for negative Infinity");
+ }
+
+ function getGripStub(name) {
+ switch (name) {
+ case "testInfinity":
+ return {
+ type: "Infinity"
+ };
+ case "testNegativeInfinity":
+ return {
+ type: "-Infinity"
+ };
+ }
+ return null;
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_long-string.html b/devtools/client/shared/components/test/mochitest/test_reps_long-string.html
new file mode 100644
index 000000000..3caaac913
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_long-string.html
@@ -0,0 +1,125 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test LongString rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - LongString</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { LongStringRep } = browserRequire("devtools/client/shared/components/reps/long-string");
+
+ try {
+ // Test that correct rep is chosen
+ const renderedRep = shallowRenderComponent(Rep, { object: getGripStub("testMultiline") });
+ is(renderedRep.type, LongStringRep.rep,
+ `Rep correctly selects ${LongStringRep.rep.displayName}`);
+
+ // Test rendering
+ yield testMultiline();
+ yield testMultilineOpen();
+ yield testFullText();
+ yield testMultilineLimit();
+ yield testUseQuotes();
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+
+ function testMultiline() {
+ const stub = getGripStub("testMultiline");
+ const renderedComponent = renderComponent(
+ LongStringRep.rep, { object: stub });
+
+ is(renderedComponent.textContent, `"${stub.initial}…"`,
+ "LongString rep has expected text content for multiline string");
+ }
+
+ function testMultilineLimit() {
+ const renderedComponent = renderComponent(
+ LongStringRep.rep, { object: getGripStub("testMultiline"), cropLimit: 20 });
+
+ is(
+ renderedComponent.textContent,
+ `"a\naaaaaaaaaaaaaaaaaa…"`,
+ "LongString rep has expected text content for multiline string " +
+ "with specified number of characters");
+ }
+
+ function testMultilineOpen() {
+ const stub = getGripStub("testMultiline");
+ const renderedComponent = renderComponent(
+ LongStringRep.rep, { object: stub, member: {open: true}, cropLimit: 20 });
+
+ is(renderedComponent.textContent, `"${stub.initial}…"`,
+ "LongString rep has expected text content for multiline string when open");
+ }
+
+ function testFullText() {
+ const stub = getGripStub("testFullText");
+ const renderedComponentOpen = renderComponent(
+ LongStringRep.rep, { object: stub, member: {open: true}, cropLimit: 20 });
+
+ is(renderedComponentOpen.textContent, `"${stub.fullText}"`,
+ "LongString rep has expected text content when grip has a fullText " +
+ "property and is open");
+
+ const renderedComponentNotOpen = renderComponent(
+ LongStringRep.rep, { object: stub, cropLimit: 20 });
+
+ is(renderedComponentNotOpen.textContent,
+ `"a\naaaaaaaaaaaaaaaaaa…"`,
+ "LongString rep has expected text content when grip has a fullText " +
+ "property and is not open");
+ }
+
+ function testUseQuotes() {
+ const renderedComponent = renderComponent(LongStringRep.rep,
+ { object: getGripStub("testMultiline"), cropLimit: 20, useQuotes: false });
+
+ is(renderedComponent.textContent,
+ "a\naaaaaaaaaaaaaaaaaa…",
+ "LongString rep was expected to omit quotes");
+ }
+
+ function getGripStub(name) {
+ const multilineFullText = "a\n" + Array(20000).fill("a").join("");
+ const fullTextLength = multilineFullText.length;
+ const initialText = multilineFullText.substring(0, 10000);
+
+ switch (name) {
+ case "testMultiline":
+ return {
+ "type": "longString",
+ "initial": initialText,
+ "length": fullTextLength,
+ "actor": "server1.conn1.child1/longString58"
+ };
+ case "testFullText":
+ return {
+ "type": "longString",
+ "fullText": multilineFullText,
+ "initial": initialText,
+ "length": fullTextLength,
+ "actor": "server1.conn1.child1/longString58"
+ };
+ }
+ return null;
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_nan.html b/devtools/client/shared/components/test/mochitest/test_reps_nan.html
new file mode 100644
index 000000000..35dc5a08f
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_nan.html
@@ -0,0 +1,48 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test NaN rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - NaN</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+"use strict";
+
+window.onload = Task.async(function* () {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { NaNRep } = browserRequire("devtools/client/shared/components/reps/nan");
+
+ try {
+ yield testNaN();
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+
+ function testNaN() {
+ const stub = {
+ type: "NaN"
+ };
+ const renderedRep = shallowRenderComponent(Rep, {object: stub});
+ is(renderedRep.type, NaNRep.rep,
+ `Rep correctly selects ${NaNRep.rep.displayName} for NaN value`);
+
+ const renderedComponent = renderComponent(NaNRep.rep, {object: stub});
+ is(renderedComponent.textContent, "NaN", "NaN rep has expected text content");
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_null.html b/devtools/client/shared/components/test/mochitest/test_reps_null.html
new file mode 100644
index 000000000..99a06fed1
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_null.html
@@ -0,0 +1,44 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test Null rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - Null</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { Null } = browserRequire("devtools/client/shared/components/reps/null");
+
+ let gripStub = {
+ "type": "null"
+ };
+
+ // Test that correct rep is chosen
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, Null.rep, `Rep correctly selects ${Null.rep.displayName}`);
+
+ // Test rendering
+ const renderedComponent = renderComponent(Null.rep, { object: gripStub });
+ is(renderedComponent.textContent, "null", "Null rep has expected text content");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_number.html b/devtools/client/shared/components/test/mochitest/test_reps_number.html
new file mode 100644
index 000000000..50f91d8b0
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_number.html
@@ -0,0 +1,97 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test Number rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - Number</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { Number } = browserRequire("devtools/client/shared/components/reps/number");
+
+ try {
+ yield testInt();
+ yield testBoolean();
+ yield testNegativeZero();
+ yield testUnsafeInt();
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+
+
+ function testInt() {
+ const renderedRep = shallowRenderComponent(Rep, { object: getGripStub("testInt") });
+ is(renderedRep.type, Number.rep, `Rep correctly selects ${Number.rep.displayName} for integer value`);
+
+ const renderedComponent = renderComponent(Number.rep, { object: getGripStub("testInt") });
+ is(renderedComponent.textContent, "5", "Number rep has expected text content for integer");
+ }
+
+ function testBoolean() {
+ const renderedRep = shallowRenderComponent(Rep, { object: getGripStub("testTrue") });
+ is(renderedRep.type, Number.rep, `Rep correctly selects ${Number.rep.displayName} for boolean value`);
+
+ let renderedComponent = renderComponent(Number.rep, { object: getGripStub("testTrue") });
+ is(renderedComponent.textContent, "true", "Number rep has expected text content for boolean true");
+
+ renderedComponent = renderComponent(Number.rep, { object: getGripStub("testFalse") });
+ is(renderedComponent.textContent, "false", "Number rep has expected text content for boolean false");
+ }
+
+ function testNegativeZero() {
+ const renderedRep = shallowRenderComponent(Rep, { object: getGripStub("testNegZeroGrip") });
+ is(renderedRep.type, Number.rep, `Rep correctly selects ${Number.rep.displayName} for negative zero value`);
+
+ let renderedComponent = renderComponent(Number.rep, { object: getGripStub("testNegZeroGrip") });
+ is(renderedComponent.textContent, "-0", "Number rep has expected text content for negative zero grip");
+
+ renderedComponent = renderComponent(Number.rep, { object: getGripStub("testNegZeroValue") });
+ is(renderedComponent.textContent, "-0", "Number rep has expected text content for negative zero value");
+ }
+
+ function testUnsafeInt() {
+ const renderedComponent = renderComponent(Number.rep, { object: getGripStub("testUnsafeInt") });
+ is(renderedComponent.textContent, "900719925474099100", "Number rep has expected text content for a long number");
+ }
+
+ function getGripStub(name) {
+ switch (name) {
+ case "testInt":
+ return 5;
+
+ case "testTrue":
+ return true;
+
+ case "testFalse":
+ return false;
+
+ case "testNegZeroValue":
+ return -0;
+
+ case "testNegZeroGrip":
+ return {
+ "type": "-0"
+ };
+
+ case "testUnsafeInt":
+ return 900719925474099122;
+ }
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_object-with-text.html b/devtools/client/shared/components/test/mochitest/test_reps_object-with-text.html
new file mode 100644
index 000000000..eeb4aa325
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_object-with-text.html
@@ -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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test ObjectWithText rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - ObjectWithText</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { ObjectWithText } = browserRequire("devtools/client/shared/components/reps/object-with-text");
+
+ let gripStub = {
+ "type": "object",
+ "class": "CSSStyleRule",
+ "actor": "server1.conn3.obj273",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "ObjectWithText",
+ "text": ".Shadow"
+ }
+ };
+
+ // Test that correct rep is chosen
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, ObjectWithText.rep, `Rep correctly selects ${ObjectWithText.rep.displayName}`);
+
+ // Test rendering
+ const renderedComponent = renderComponent(ObjectWithText.rep, { object: gripStub });
+ is(renderedComponent.textContent, "\".Shadow\"", "ObjectWithText rep has expected text content");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_object-with-url.html b/devtools/client/shared/components/test/mochitest/test_reps_object-with-url.html
new file mode 100644
index 000000000..488c28dc2
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_object-with-url.html
@@ -0,0 +1,60 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test ObjectWithURL rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - ObjectWithURL</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ let ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ let React = browserRequire("devtools/client/shared/vendor/react");
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { ObjectWithURL } = browserRequire("devtools/client/shared/components/reps/object-with-url");
+
+ let gripStub = {
+ "type": "object",
+ "class": "Location",
+ "actor": "server1.conn2.obj272",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 15,
+ "preview": {
+ "kind": "ObjectWithURL",
+ "url": "https://www.mozilla.org/en-US/"
+ }
+ };
+
+ // Test that correct rep is chosen
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, ObjectWithURL.rep, `Rep correctly selects ${ObjectWithURL.rep.displayName}`);
+
+ // Test rendering
+ const renderedComponent = renderComponent(ObjectWithURL.rep, { object: gripStub });
+ ok(renderedComponent.className.includes("objectBox-Location"), "ObjectWithURL rep has expected class name");
+ const innerNode = renderedComponent.querySelector(".objectPropValue");
+ is(innerNode.textContent, "https://www.mozilla.org/en-US/", "ObjectWithURL rep has expected inner HTML structure and text content");
+
+ // @TODO test link once Bug 1245303 has been implemented.
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_object.html b/devtools/client/shared/components/test/mochitest/test_reps_object.html
new file mode 100644
index 000000000..c3332361d
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_object.html
@@ -0,0 +1,225 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test Obj rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - Obj</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { Obj } = browserRequire("devtools/client/shared/components/reps/object");
+
+ const componentUnderTest = Obj;
+
+ try {
+ yield testBasic();
+
+ // Test property iterator
+ yield testMaxProps();
+ yield testMoreThanMaxProps();
+ yield testUninterestingProps();
+
+ // Test that properties are rendered as expected by PropRep
+ yield testNested();
+
+ // Test that 'more' property doesn't clobber the caption.
+ yield testMoreProp();
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+
+ function testBasic() {
+ const stub = {};
+
+ // Test that correct rep is chosen
+ const renderedRep = shallowRenderComponent(Rep, { object: stub });
+ is(renderedRep.type, Obj.rep, `Rep correctly selects ${Obj.rep.displayName}`);
+
+ // Test rendering
+ const defaultOutput = `Object`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, "testBasic", componentUnderTest, stub);
+ }
+
+ function testMaxProps() {
+ const testName = "testMaxProps";
+
+ const stub = {a: "a", b: "b", c: "c"};
+ const defaultOutput = `Object { a: "a", b: "b", c: "c" }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Object`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, "testMaxProps", componentUnderTest, stub);
+ }
+
+ function testMoreThanMaxProps() {
+ let stub = {};
+ for (let i = 0; i<100; i++) {
+ stub[`p${i}`] = i
+ }
+ const defaultOutput = `Object { p0: 0, p1: 1, p2: 2, 97 more… }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Object`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, "testMoreThanMaxProps", componentUnderTest, stub);
+ }
+
+ function testUninterestingProps() {
+ const stub = {a:undefined, b:undefined, c:"c", d:0};
+ const defaultOutput = `Object { c: "c", d: 0, a: undefined, 1 more… }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Object`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, "testUninterestingProps", componentUnderTest, stub);
+ }
+
+ function testNested() {
+ const stub = {
+ objProp: {
+ id: 1,
+ arr: [2]
+ },
+ strProp: "test string",
+ arrProp: [1]
+ };
+ const defaultOutput = `Object { strProp: "test string", objProp: Object, arrProp: [1] }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Object`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, "testNestedObject", componentUnderTest, stub);
+ }
+
+ function testMoreProp() {
+ const stub = {
+ a: undefined,
+ b: 1,
+ 'more': 2,
+ d: 3
+ };
+ const defaultOutput = `Object { b: 1, more: 2, d: 3, 1 more… }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Object`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, "testMoreProp", componentUnderTest, stub);
+ }});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_promise.html b/devtools/client/shared/components/test/mochitest/test_reps_promise.html
new file mode 100644
index 000000000..31de7136d
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_promise.html
@@ -0,0 +1,333 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test Promise rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - Promise</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+"use strict";
+
+window.onload = Task.async(function* () {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { PromiseRep } = browserRequire("devtools/client/shared/components/reps/promise");
+
+ const componentUnderTest = PromiseRep;
+
+ try {
+ yield testPending();
+ yield testFulfilledWithNumber();
+ yield testFulfilledWithString();
+ yield testFulfilledWithObject();
+ yield testFulfilledWithArray();
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+
+ function testPending() {
+ // Test object = `new Promise((resolve, reject) => true)`
+ const stub = getGripStub("testPending");
+
+ // Test that correct rep is chosen.
+ const renderedRep = shallowRenderComponent(Rep, { object: stub });
+ is(renderedRep.type, PromiseRep.rep,
+ `Rep correctly selects ${PromiseRep.rep.displayName} for pending Promise`);
+
+ // Test rendering
+ const defaultOutput = `Promise { <state>: "pending" }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Promise { "pending" }`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, "testPending", componentUnderTest, stub);
+ }
+ function testFulfilledWithNumber() {
+ // Test object = `Promise.resolve(42)`
+ const stub = getGripStub("testFulfilledWithNumber");
+
+ // Test that correct rep is chosen.
+ const renderedRep = shallowRenderComponent(Rep, { object: stub });
+ const {displayName} = PromiseRep.rep;
+ is(renderedRep.type, PromiseRep.rep,
+ `Rep correctly selects ${displayName} for Promise fulfilled with a number`);
+
+ // Test rendering
+ const defaultOutput = `Promise { <state>: "fulfilled", <value>: 42 }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Promise { "fulfilled" }`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, "testFulfilledWithNumber", componentUnderTest, stub);
+ }
+ function testFulfilledWithString() {
+ // Test object = `Promise.resolve("foo")`
+ const stub = getGripStub("testFulfilledWithString");
+
+ // Test that correct rep is chosen.
+ const renderedRep = shallowRenderComponent(Rep, { object: stub });
+ const {displayName} = PromiseRep.rep;
+ is(renderedRep.type, PromiseRep.rep,
+ `Rep correctly selects ${displayName} for Promise fulfilled with a string`);
+
+ // Test rendering
+ const defaultOutput = `Promise { <state>: "fulfilled", <value>: "foo" }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Promise { "fulfilled" }`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, "testFulfilledWithString", componentUnderTest, stub);
+ }
+
+ function testFulfilledWithObject() {
+ // Test object = `Promise.resolve({foo: "bar", baz: "boo"})`
+ const stub = getGripStub("testFulfilledWithObject");
+
+ // Test that correct rep is chosen.
+ const renderedRep = shallowRenderComponent(Rep, { object: stub });
+ const {displayName} = PromiseRep.rep;
+ is(renderedRep.type, PromiseRep.rep,
+ `Rep correctly selects ${displayName} for Promise fulfilled with an object`);
+
+ // Test rendering
+ const defaultOutput = `Promise { <state>: "fulfilled", <value>: Object }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Promise { "fulfilled" }`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, "testFulfilledWithObject", componentUnderTest, stub);
+ }
+
+ function testFulfilledWithArray() {
+ // Test object = `Promise.resolve([1,2,3])`
+ const stub = getGripStub("testFulfilledWithArray");
+
+ // Test that correct rep is chosen.
+ const renderedRep = shallowRenderComponent(Rep, { object: stub });
+ const {displayName} = PromiseRep.rep;
+ is(renderedRep.type, PromiseRep.rep,
+ `Rep correctly selects ${displayName} for Promise fulfilled with an array`);
+
+ // Test rendering
+ const defaultOutput = `Promise { <state>: "fulfilled", <value>: [3] }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Promise { "fulfilled" }`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, "testFulfilledWithArray", componentUnderTest, stub);
+ }
+
+ function getGripStub(name) {
+ switch (name) {
+ case "testPending":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj54",
+ "class": "Promise",
+ "promiseState": {
+ "state": "pending",
+ "creationTimestamp": 1477327760242.5752
+ },
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {},
+ "ownPropertiesLength": 0,
+ "safeGetterValues": {}
+ }
+ };
+ case "testFulfilledWithNumber":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj55",
+ "class": "Promise",
+ "promiseState": {
+ "state": "fulfilled",
+ "value": 42,
+ "creationTimestamp": 1477327760242.721,
+ "timeToSettle": 0.018497000000479602
+ },
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {},
+ "ownPropertiesLength": 0,
+ "safeGetterValues": {}
+ }
+ };
+ case "testFulfilledWithString":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj56",
+ "class": "Promise",
+ "promiseState": {
+ "state": "fulfilled",
+ "value": "foo",
+ "creationTimestamp": 1477327760243.2483,
+ "timeToSettle": 0.0019969999998465937
+ },
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {},
+ "ownPropertiesLength": 0,
+ "safeGetterValues": {}
+ }
+ };
+ case "testFulfilledWithObject":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj59",
+ "class": "Promise",
+ "promiseState": {
+ "state": "fulfilled",
+ "value": {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj60",
+ "class": "Object",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 2
+ },
+ "creationTimestamp": 1477327760243.2214,
+ "timeToSettle": 0.002035999999861815
+ },
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {},
+ "ownPropertiesLength": 0,
+ "safeGetterValues": {}
+ }
+ };
+ case "testFulfilledWithArray":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj57",
+ "class": "Promise",
+ "promiseState": {
+ "state": "fulfilled",
+ "value": {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj58",
+ "class": "Array",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 4,
+ "preview": {
+ "kind": "ArrayLike",
+ "length": 3
+ }
+ },
+ "creationTimestamp": 1477327760242.9597,
+ "timeToSettle": 0.006158000000141328
+ },
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {},
+ "ownPropertiesLength": 0,
+ "safeGetterValues": {}
+ }
+ };
+ }
+ return null;
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_regexp.html b/devtools/client/shared/components/test/mochitest/test_reps_regexp.html
new file mode 100644
index 000000000..074948494
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_regexp.html
@@ -0,0 +1,51 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test RegExp rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - RegExp</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { RegExp } = browserRequire("devtools/client/shared/components/reps/regexp");
+
+ let gripStub = {
+ "type": "object",
+ "class": "RegExp",
+ "actor": "server1.conn22.obj39",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 1,
+ "displayString": "/ab+c/i"
+ };
+
+ // Test that correct rep is chosen
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, RegExp.rep, `Rep correctly selects ${RegExp.rep.displayName}`);
+
+ // Test rendering
+ const renderedComponent = renderComponent(RegExp.rep, { object: gripStub });
+ is(renderedComponent.textContent, "/ab+c/i", "RegExp rep has expected text content");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_string.html b/devtools/client/shared/components/test/mochitest/test_reps_string.html
new file mode 100644
index 000000000..f9fc9e5b0
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_string.html
@@ -0,0 +1,79 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test String rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - String</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { StringRep } = browserRequire("devtools/client/shared/components/reps/string");
+
+ try {
+ // Test that correct rep is chosen
+ const renderedRep = shallowRenderComponent(Rep, { object: getGripStub("testMultiline") });
+ is(renderedRep.type, StringRep.rep, `Rep correctly selects ${StringRep.rep.displayName}`);
+
+ // Test rendering
+ yield testMultiline();
+ yield testMultilineOpen();
+ yield testMultilineLimit();
+ yield testUseQuotes();
+ yield testNonPritableCharacters();
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+
+ function testMultiline() {
+ const renderedComponent = renderComponent(StringRep.rep, { object: getGripStub("testMultiline") });
+ is(renderedComponent.textContent, "\"aaaaaaaaaaaaaaaaaaaaa\nbbbbbbbbbbbbbbbbbbb\ncccccccccccccccc\n\"", "String rep has expected text content for multiline string");
+ }
+
+ function testMultilineLimit() {
+ const renderedComponent = renderComponent(StringRep.rep, { object: getGripStub("testMultiline"), cropLimit: 20 });
+ is(renderedComponent.textContent, "\"aaaaaaaaaa…cccccccc\n\"", "String rep has expected text content for multiline string with specified number of characters");
+ }
+
+ function testMultilineOpen() {
+ const renderedComponent = renderComponent(StringRep.rep, { object: getGripStub("testMultiline"), member: {open: true} });
+ is(renderedComponent.textContent, "\"aaaaaaaaaaaaaaaaaaaaa\nbbbbbbbbbbbbbbbbbbb\ncccccccccccccccc\n\"", "String rep has expected text content for multiline string when open");
+ }
+
+ function testUseQuotes(){
+ const renderedComponent = renderComponent(StringRep.rep, { object: getGripStub("testUseQuotes"), useQuotes: false });
+ is(renderedComponent.textContent, "abc", "String rep was expected to omit quotes");
+ }
+
+ function testNonPritableCharacters(){
+ const renderedComponent = renderComponent(StringRep.rep, { object: getGripStub("testNonPritableCharacters"), useQuotes: false });
+ is(renderedComponent.textContent, "a\ufffdb", "String rep was expected to omit non printable characters");
+ }
+
+ function getGripStub(name) {
+ switch (name) {
+ case "testMultiline":
+ return "aaaaaaaaaaaaaaaaaaaaa\nbbbbbbbbbbbbbbbbbbb\ncccccccccccccccc\n";
+ case "testUseQuotes":
+ return "abc";
+ case "testNonPritableCharacters":
+ return "a\x01b";
+ }
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_stylesheet.html b/devtools/client/shared/components/test/mochitest/test_reps_stylesheet.html
new file mode 100644
index 000000000..6f54dee48
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_stylesheet.html
@@ -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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test Stylesheet rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - Stylesheet</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { StyleSheet } = browserRequire("devtools/client/shared/components/reps/stylesheet");
+
+ let gripStub = {
+ "type": "object",
+ "class": "CSSStyleSheet",
+ "actor": "server1.conn2.obj1067",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "ObjectWithURL",
+ "url": "https://example.com/styles.css"
+ }
+ };
+
+ // Test that correct rep is chosen
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, StyleSheet.rep, `Rep correctly selects ${StyleSheet.rep.displayName}`);
+
+ // Test rendering
+ const renderedComponent = renderComponent(StyleSheet.rep, { object: gripStub });
+ is(renderedComponent.textContent, "StyleSheet https://example.com/styles.css", "StyleSheet rep has expected text content");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_symbol.html b/devtools/client/shared/components/test/mochitest/test_reps_symbol.html
new file mode 100644
index 000000000..0112eac0f
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_symbol.html
@@ -0,0 +1,77 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test Symbol rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - String</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+"use strict";
+/* import-globals-from head.js */
+
+window.onload = Task.async(function* () {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { SymbolRep } = browserRequire("devtools/client/shared/components/reps/symbol");
+
+ let gripStubs = new Map();
+ gripStubs.set("testSymbolFoo", {
+ type: "symbol",
+ name: "foo"
+ });
+ gripStubs.set("testSymbolWithoutIdentifier", {
+ type: "symbol"
+ });
+
+ try {
+ // Test that correct rep is chosen
+ const renderedRep = shallowRenderComponent(
+ Rep,
+ { object: gripStubs.get("testSymbolFoo")}
+ );
+
+ is(renderedRep.type, SymbolRep.rep,
+ `Rep correctly selects ${SymbolRep.rep.displayName}`);
+
+ // Test rendering
+ yield testSymbol();
+ yield testSymbolWithoutIdentifier();
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+
+ function testSymbol() {
+ const renderedComponent = renderComponent(
+ SymbolRep.rep,
+ { object: gripStubs.get("testSymbolFoo") }
+ );
+
+ is(renderedComponent.textContent, "Symbol(foo)",
+ "Symbol rep has expected text content");
+ }
+
+ function testSymbolWithoutIdentifier() {
+ const renderedComponent = renderComponent(
+ SymbolRep.rep,
+ { object: gripStubs.get("testSymbolWithoutIdentifier") }
+ );
+
+ is(renderedComponent.textContent, "Symbol()",
+ "Symbol rep without identifier has expected text content");
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_text-node.html b/devtools/client/shared/components/test/mochitest/test_reps_text-node.html
new file mode 100644
index 000000000..f64902a63
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_text-node.html
@@ -0,0 +1,115 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test text-node rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - text-node</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+"use strict";
+
+window.onload = Task.async(function* () {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { TextNode } = browserRequire("devtools/client/shared/components/reps/text-node");
+
+ let gripStubs = new Map();
+ gripStubs.set("testRendering", {
+ "class": "Text",
+ "actor": "server1.conn1.child1/obj50",
+ "preview": {
+ "textContent": "hello world"
+ }
+ });
+ gripStubs.set("testRenderingWithEOL", {
+ "class": "Text",
+ "actor": "server1.conn1.child1/obj50",
+ "preview": {
+ "textContent": "hello\nworld"
+ }
+ });
+
+ try {
+ // Test that correct rep is chosen
+ const renderedRep = shallowRenderComponent(Rep, {
+ object: gripStubs.get("testRendering")
+ });
+
+ is(renderedRep.type, TextNode.rep,
+ `Rep correctly selects ${TextNode.rep.displayName}`);
+
+ yield testRendering();
+ yield testRenderingWithEOL();
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+
+ function testRendering() {
+ const stub = gripStubs.get("testRendering");
+ const defaultShortOutput = `"hello world"`;
+ const defaultLongOutput = `<TextNode textContent="hello world">;`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultShortOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: defaultShortOutput,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultShortOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultLongOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, "testRendering", TextNode, stub);
+ }
+
+ function testRenderingWithEOL() {
+ const stub = gripStubs.get("testRenderingWithEOL");
+ const defaultShortOutput = `"hello\nworld"`;
+ const defaultLongOutput = `<TextNode textContent="hello\nworld">;`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultShortOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: defaultShortOutput,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultShortOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultLongOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, "testRenderingWithEOL", TextNode, stub);
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_undefined.html b/devtools/client/shared/components/test/mochitest/test_reps_undefined.html
new file mode 100644
index 000000000..26b3345ac
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_undefined.html
@@ -0,0 +1,47 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test undefined rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - undefined</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ let ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ let React = browserRequire("devtools/client/shared/vendor/react");
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { Undefined } = browserRequire("devtools/client/shared/components/reps/undefined");
+
+ let gripStub = {
+ "type": "undefined"
+ };
+
+ // Test that correct rep is chosen
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, Undefined.rep, `Rep correctly selects ${Undefined.rep.displayName}`);
+
+ // Test rendering
+ const renderedComponent = renderComponent(Undefined.rep, {});
+ is(renderedComponent.className, "objectBox objectBox-undefined", "Undefined rep has expected class names");
+ is(renderedComponent.textContent, "undefined", "Undefined rep has expected text content");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_window.html b/devtools/client/shared/components/test/mochitest/test_reps_window.html
new file mode 100644
index 000000000..55d60e9f5
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_window.html
@@ -0,0 +1,58 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test window rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep tests - window</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ let ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ let React = browserRequire("devtools/client/shared/vendor/react");
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { Window } = browserRequire("devtools/client/shared/components/reps/window");
+
+ let gripStub = {
+ "type": "object",
+ "class": "Window",
+ "actor": "server1.conn3.obj198",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 887,
+ "preview": {
+ "kind": "ObjectWithURL",
+ "url": "about:newtab"
+ }
+ };
+
+ // Test that correct rep is chosen
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, Window.rep, `Rep correctly selects ${Window.rep.displayName}`);
+
+ // Test rendering
+ const renderedComponent = renderComponent(Window.rep, { object: gripStub });
+ ok(renderedComponent.className.includes("objectBox-Window"), "Window rep has expected class name");
+ const innerNode = renderedComponent.querySelector(".objectPropValue");
+ is(innerNode.textContent, "about:newtab", "Window rep has expected inner HTML structure and text content");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_sidebar_toggle.html b/devtools/client/shared/components/test/mochitest/test_sidebar_toggle.html
new file mode 100644
index 000000000..252f2fbb1
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_sidebar_toggle.html
@@ -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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test sidebar toggle button
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Sidebar toggle button test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ let SidebarToggle = browserRequire("devtools/client/shared/components/sidebar-toggle.js");
+
+ try {
+ yield test();
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+
+ function test() {
+ const output1 = shallowRenderComponent(SidebarToggle, {
+ collapsed: false,
+ collapsePaneTitle: "Expand",
+ expandPaneTitle: "Collapse"
+ });
+
+ is(output1.type, "button", "Output is a button element");
+ is(output1.props.title, "Expand", "Proper title is set");
+ is(output1.props.className.indexOf("pane-collapsed"), -1,
+ "Proper class name is set");
+
+ const output2 = shallowRenderComponent(SidebarToggle, {
+ collapsed: true,
+ collapsePaneTitle: "Expand",
+ expandPaneTitle: "Collapse"
+ });
+
+ is(output2.props.title, "Collapse", "Proper title is set");
+ ok(output2.props.className.indexOf("pane-collapsed") >= 0,
+ "Proper class name is set");
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_stack-trace.html b/devtools/client/shared/components/test/mochitest/test_stack-trace.html
new file mode 100644
index 000000000..121316cb4
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_stack-trace.html
@@ -0,0 +1,102 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test the rendering of a stack trace
+-->
+<head>
+ <meta charset="utf-8">
+ <title>StackTrace component test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<script src="head.js"></script>
+<script>
+/* import-globals-from head.js */
+"use strict";
+
+window.onload = function () {
+ let ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ let React = browserRequire("devtools/client/shared/vendor/react");
+ let StackTrace = React.createFactory(
+ browserRequire("devtools/client/shared/components/stack-trace")
+ );
+ ok(StackTrace, "Got the StackTrace factory");
+
+ add_task(function* () {
+ let stacktrace = [
+ {
+ filename: "http://myfile.com/mahscripts.js",
+ lineNumber: 55,
+ columnNumber: 10
+ },
+ {
+ asyncCause: "because",
+ functionName: "loadFunc",
+ filename: "http://myfile.com/loader.js -> http://myfile.com/loadee.js",
+ lineNumber: 10
+ }
+ ];
+
+ let props = {
+ stacktrace,
+ onViewSourceInDebugger: () => {}
+ };
+
+ let trace = ReactDOM.render(StackTrace(props), window.document.body);
+ yield forceRender(trace);
+
+ let traceEl = trace.getDOMNode();
+ ok(traceEl, "Rendered StackTrace has an element");
+
+ // Get the child nodes and filter out the text-only whitespace ones
+ let frameEls = Array.from(traceEl.childNodes)
+ .filter(n => n.className.includes("frame"));
+ ok(frameEls, "Rendered StackTrace has frames");
+ is(frameEls.length, 3, "StackTrace has 3 frames");
+
+ // Check the top frame, function name should be anonymous
+ checkFrameString({
+ el: frameEls[0],
+ functionName: "<anonymous>",
+ source: "http://myfile.com/mahscripts.js",
+ file: "http://myfile.com/mahscripts.js",
+ line: 55,
+ column: 10,
+ shouldLink: true,
+ tooltip: "View source in Debugger → http://myfile.com/mahscripts.js:55:10",
+ });
+
+ // Check the async cause node
+ is(frameEls[1].className, "frame-link-async-cause",
+ "Async cause has the right class");
+ is(frameEls[1].textContent, "(Async: because)", "Async cause has the right label");
+
+ // Check the third frame, the source should be parsed into a valid source URL
+ checkFrameString({
+ el: frameEls[2],
+ functionName: "loadFunc",
+ source: "http://myfile.com/loadee.js",
+ file: "http://myfile.com/loadee.js",
+ line: 10,
+ column: null,
+ shouldLink: true,
+ tooltip: "View source in Debugger → http://myfile.com/loadee.js:10",
+ });
+
+ // Check the tabs and newlines in the stack trace textContent
+ let traceText = traceEl.textContent;
+ let traceLines = traceText.split("\n");
+ ok(traceLines.length > 0, "There are newlines in the stack trace text");
+ is(traceLines.pop(), "", "There is a newline at the end of the stack trace text");
+ is(traceLines.length, 3, "The stack trace text has 3 lines");
+ ok(traceLines.every(l => l[0] == "\t"), "Every stack trace line starts with tab");
+ });
+};
+</script>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_tabs_accessibility.html b/devtools/client/shared/components/test/mochitest/test_tabs_accessibility.html
new file mode 100644
index 000000000..a86082187
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_tabs_accessibility.html
@@ -0,0 +1,79 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test tabs accessibility.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tabs component accessibility test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const { Simulate } = React.addons.TestUtils;
+ const InspectorTabPanel = React.createFactory(browserRequire("devtools/client/inspector/components/inspector-tab-panel"));
+ const Tabbar = React.createFactory(browserRequire("devtools/client/shared/components/tabs/tabbar"));
+ const tabbar = Tabbar();
+ const tabbarReact = ReactDOM.render(tabbar, window.document.body);
+ const tabbarEl = ReactDOM.findDOMNode(tabbarReact);
+
+ // Setup for InspectorTabPanel
+ const tabpanels = document.createElement("div");
+ tabpanels.id = "tabpanels";
+ document.body.appendChild(tabpanels);
+
+ yield addTabWithPanel(0);
+ yield addTabWithPanel(1);
+
+ const tabAnchors = tabbarEl.querySelectorAll("li.tabs-menu-item a");
+
+ is(tabAnchors[0].parentElement.getAttribute("role"), "presentation", "li role is set correctly");
+ is(tabAnchors[0].getAttribute("role"), "tab", "Anchor role is set correctly");
+ is(tabAnchors[0].getAttribute("aria-selected"), "true", "Anchor aria-selected is set correctly by default");
+ is(tabAnchors[0].getAttribute("aria-controls"), "panel-0", "Anchor aria-controls is set correctly");
+ is(tabAnchors[1].parentElement.getAttribute("role"), "presentation", "li role is set correctly");
+ is(tabAnchors[1].getAttribute("role"), "tab", "Anchor role is set correctly");
+ is(tabAnchors[1].getAttribute("aria-selected"), "false", "Anchor aria-selected is set correctly by default");
+ is(tabAnchors[1].getAttribute("aria-controls"), "panel-1", "Anchor aria-controls is set correctly");
+
+ yield setState(tabbarReact, Object.assign({}, tabbarReact.state, {
+ activeTab: 1
+ }));
+
+ is(tabAnchors[0].getAttribute("aria-selected"), "false", "Anchor aria-selected is reset correctly");
+ is(tabAnchors[1].getAttribute("aria-selected"), "true", "Anchor aria-selected is reset correctly");
+
+ function addTabWithPanel(tabId) {
+ // Setup for InspectorTabPanel
+ let panel = document.createElement("div");
+ panel.id = `sidebar-panel-${tabId}`;
+ document.body.appendChild(panel);
+
+ return setState(tabbarReact, Object.assign({}, tabbarReact.state, {
+ tabs: tabbarReact.state.tabs.concat({
+ id: `sidebar-panel-${tabId}`,
+ title: `tab-${tabId}`,
+ panel: InspectorTabPanel
+ }),
+ }));
+ }
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_tabs_menu.html b/devtools/client/shared/components/test/mochitest/test_tabs_menu.html
new file mode 100644
index 000000000..ac8e99289
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_tabs_menu.html
@@ -0,0 +1,81 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html class="theme-light">
+<!--
+Test all-tabs menu.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tabs component All-tabs menu test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <link rel="stylesheet" type="text/css" href="resource://devtools/client/themes/variables.css">
+ <link rel="stylesheet" type="text/css" href="resource://devtools/client/themes/common.css">
+ <link rel="stylesheet" type="text/css" href="resource://devtools/client/themes/light-theme.css">
+ <link rel="stylesheet" type="text/css" href="resource://devtools/client/shared/components/tabs/tabs.css">
+ <link rel="stylesheet" type="text/css" href="resource://devtools/client/shared/components/tabs/tabbar.css">
+ <link rel="stylesheet" type="text/css" href="resource://devtools/client/inspector/components/side-panel.css">
+ <link rel="stylesheet" type="text/css" href="resource://devtools/client/inspector/components/inspector-tab-panel.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const Tabbar = React.createFactory(browserRequire("devtools/client/shared/components/tabs/tabbar"));
+
+ // Create container for the TabBar. Set smaller width
+ // to ensure that tabs won't fit and the all-tabs menu
+ // needs to appear.
+ const tabBarBox = document.createElement("div");
+ tabBarBox.style.width = "200px";
+ tabBarBox.style.height = "200px";
+ tabBarBox.style.border = "1px solid lightgray";
+ document.body.appendChild(tabBarBox);
+
+ // Render the tab-bar.
+ const tabbar = Tabbar({
+ showAllTabsMenu: true,
+ });
+
+ const tabbarReact = ReactDOM.render(tabbar, tabBarBox);
+
+ // Test panel.
+ let TabPanel = React.createFactory(React.createClass({
+ render: function () {
+ return React.DOM.div({}, "content");
+ }
+ }));
+
+ // Create a few panels.
+ yield addTabWithPanel(1);
+ yield addTabWithPanel(2);
+ yield addTabWithPanel(3);
+ yield addTabWithPanel(4);
+ yield addTabWithPanel(5);
+
+ // Make sure the all-tabs menu is there.
+ const allTabsMenu = tabBarBox.querySelector(".all-tabs-menu");
+ ok(allTabsMenu, "All-tabs menu must be rendered");
+
+ function addTabWithPanel(tabId) {
+ return setState(tabbarReact, Object.assign({}, tabbarReact.state, {
+ tabs: tabbarReact.state.tabs.concat({id: `${tabId}`,
+ title: `tab-${tabId}`, panel: TabPanel}),
+ }));
+ }
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_tree_01.html b/devtools/client/shared/components/test/mochitest/test_tree_01.html
new file mode 100644
index 000000000..dfd666348
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_tree_01.html
@@ -0,0 +1,64 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test trees get displayed with the items in correct order and at the correct
+depth.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ let ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ let React = browserRequire("devtools/client/shared/vendor/react");
+ let Tree = React.createFactory(browserRequire("devtools/client/shared/components/tree"));
+
+ ok(React, "Should get React");
+ ok(Tree, "Should get Tree");
+
+ const t = Tree(TEST_TREE_INTERFACE);
+ ok(t, "Should be able to create Tree instances");
+
+ const tree = ReactDOM.render(t, window.document.body);
+ ok(tree, "Should be able to mount Tree instances");
+
+ TEST_TREE.expanded = new Set("ABCDEFGHIJKLMNO".split(""));
+ yield forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "Should get the items rendered and indented as expected");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_tree_02.html b/devtools/client/shared/components/test/mochitest/test_tree_02.html
new file mode 100644
index 000000000..a1fc33a38
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_tree_02.html
@@ -0,0 +1,45 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that collapsed subtrees aren't rendered.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ let ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ let React = browserRequire("devtools/client/shared/vendor/react");
+ let Tree = React.createFactory(browserRequire("devtools/client/shared/components/tree"));
+
+ const tree = ReactDOM.render(Tree(TEST_TREE_INTERFACE), window.document.body);
+
+ TEST_TREE.expanded = new Set("MNO".split(""));
+ yield forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "Collapsed subtrees shouldn't be rendered");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_tree_03.html b/devtools/client/shared/components/test/mochitest/test_tree_03.html
new file mode 100644
index 000000000..feabc7e0a
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_tree_03.html
@@ -0,0 +1,46 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test Tree's autoExpandDepth.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ let ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ let React = browserRequire("devtools/client/shared/vendor/react");
+ let Tree = React.createFactory(browserRequire("devtools/client/shared/components/tree"));
+
+ const tree = ReactDOM.render(Tree(Object.assign({}, TEST_TREE_INTERFACE, {
+ autoExpandDepth: 1
+ })), window.document.body);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "-C:false",
+ "-D:false",
+ "M:false",
+ "-N:false",
+ ], "Tree should be auto expanded one level");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_tree_04.html b/devtools/client/shared/components/test/mochitest/test_tree_04.html
new file mode 100644
index 000000000..24948c003
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_tree_04.html
@@ -0,0 +1,128 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that we only render visible tree items.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ function getSpacerHeights() {
+ return {
+ top: document.querySelector(".tree > div:first-of-type").clientHeight,
+ bottom: document.querySelector(".tree > div:last-of-type").clientHeight,
+ };
+ }
+
+ const ITEM_HEIGHT = 3;
+
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const Tree = React.createFactory(browserRequire("devtools/client/shared/components/tree"));
+
+ const tree = ReactDOM.render(
+ Tree(Object.assign({}, TEST_TREE_INTERFACE, { itemHeight: ITEM_HEIGHT })),
+ window.document.body);
+
+ TEST_TREE.expanded = new Set("ABCDEFGHIJKLMNO".split(""));
+
+ yield setState(tree, {
+ height: 3 * ITEM_HEIGHT,
+ scroll: 1 * ITEM_HEIGHT
+ });
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ "---L:false",
+ ], "Tree should show the 2nd, 3rd, and 4th items + buffer of 1 item at each end");
+
+ let spacers = getSpacerHeights();
+ is(spacers.top, 0, "Top spacer has the correct height");
+ is(spacers.bottom, 10 * ITEM_HEIGHT, "Bottom spacer has the correct height");
+
+ yield setState(tree, {
+ height: 2 * ITEM_HEIGHT,
+ scroll: 3 * ITEM_HEIGHT
+ });
+
+ isRenderedTree(document.body.textContent, [
+ "--E:false",
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ ], "Tree should show the 4th and 5th item + buffer of 1 item at each end");
+
+ spacers = getSpacerHeights();
+ is(spacers.top, 2 * ITEM_HEIGHT, "Top spacer has the correct height");
+ is(spacers.bottom, 9 * ITEM_HEIGHT, "Bottom spacer has the correct height");
+
+ // Set height to 2 items + 1 pixel at each end, scroll so that 4 items are visible
+ // (2 fully, 2 partially with 1 visible pixel)
+ yield setState(tree, {
+ height: 2 * ITEM_HEIGHT + 2,
+ scroll: 3 * ITEM_HEIGHT - 1
+ });
+
+ isRenderedTree(document.body.textContent, [
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ ], "Tree should show the 4 visible items + buffer of 1 item at each end");
+
+ spacers = getSpacerHeights();
+ is(spacers.top, 1 * ITEM_HEIGHT, "Top spacer has the correct height");
+ is(spacers.bottom, 8 * ITEM_HEIGHT, "Bottom spacer has the correct height");
+
+ yield setState(tree, {
+ height: 20 * ITEM_HEIGHT,
+ scroll: 0
+ });
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "Tree should show all rows");
+
+ spacers = getSpacerHeights();
+ is(spacers.top, 0, "Top spacer has zero height");
+ is(spacers.bottom, 0, "Bottom spacer has zero height");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_tree_05.html b/devtools/client/shared/components/test/mochitest/test_tree_05.html
new file mode 100644
index 000000000..76116ab51
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_tree_05.html
@@ -0,0 +1,83 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test focusing with the Tree component.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+
+window.onload = Task.async(function* () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const { Simulate } = React.addons.TestUtils;
+ const Tree = React.createFactory(browserRequire("devtools/client/shared/components/tree"));
+ const tree = ReactDOM.render(Tree(Object.assign({}, TEST_TREE_INTERFACE, {
+ onFocus: x => setProps(tree, { focused: x }),
+ })), window.document.body);
+
+ TEST_TREE.expanded = new Set("ABCDEFGHIJKLMNO".split(""));
+ yield setProps(tree, {
+ focused: "G",
+ });
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ "--G:true",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "G should be focused");
+
+ // Click the first tree node
+ Simulate.click(document.querySelector(".tree-node"));
+ yield forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:true",
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "A should be focused");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_tree_06.html b/devtools/client/shared/components/test/mochitest/test_tree_06.html
new file mode 100644
index 000000000..1d8f28ec9
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_tree_06.html
@@ -0,0 +1,320 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test keyboard navigation with the Tree component.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const { Simulate } = React.addons.TestUtils;
+ const Tree = React.createFactory(browserRequire("devtools/client/shared/components/tree"));
+ const tree = ReactDOM.render(Tree(Object.assign({}, TEST_TREE_INTERFACE, {
+ onFocus: x => setProps(tree, { focused: x }),
+ })), window.document.body);
+
+ TEST_TREE.expanded = new Set("ABCDEFGHIJKLMNO".split(""));
+
+ // UP ----------------------------------------------------------------------
+
+ info("Up to the previous sibling.");
+
+ yield setProps(tree, {
+ focused: "L"
+ });
+ Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowUp" });
+ yield forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:false",
+ "---K:true",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "After the UP, K should be focused.");
+
+ info("Up to the parent.");
+
+ Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowUp" });
+ yield forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:true",
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "After the UP, E should be focused.");
+
+ info("Try and navigate up, past the first item.");
+
+ yield setProps(tree, {
+ focused: "A"
+ });
+ Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowUp" });
+ yield forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:true",
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "After the UP, A should be focused and we shouldn't have overflowed past it.");
+
+ // DOWN --------------------------------------------------------------------
+
+ yield setProps(tree, {
+ focused: "K"
+ });
+
+ info("Down to next sibling.");
+
+ Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowDown" });
+ yield forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ "---L:true",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "After the DOWN, L should be focused.");
+
+ info("Down to parent's next sibling.");
+
+ Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowDown" });
+ yield forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ "---L:false",
+ "--F:true",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "After the DOWN, F should be focused.");
+
+ info("Try and go down past the last item.");
+
+ yield setProps(tree, {
+ focused: "O"
+ });
+ Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowDown" });
+ yield forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:true",
+ ], "After the DOWN, O should still be focused and we shouldn't have overflowed past it.");
+
+ // LEFT --------------------------------------------------------------------
+
+ info("Left to go to parent.");
+
+ yield setProps(tree, {
+ focused: "L"
+ })
+ Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowLeft" });
+ yield forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:true",
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "After the LEFT, E should be focused.");
+
+ info("Left to collapse children.");
+
+ Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowLeft" });
+ yield forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:true",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "After the LEFT, E's children should be collapsed.");
+
+ // RIGHT -------------------------------------------------------------------
+
+ info("Right to expand children.");
+
+ Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowRight" });
+ yield forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:true",
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "After the RIGHT, E's children should be expanded again.");
+
+ info("Right to go to next item.");
+
+ Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowRight" });
+ yield forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:false",
+ "---K:true",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "After the RIGHT, K should be focused.");
+
+ // Check that keys are ignored if any modifier is present.
+ let keysWithModifier = [
+ { key: "ArrowDown", altKey: true },
+ { key: "ArrowDown", ctrlKey: true },
+ { key: "ArrowDown", metaKey: true },
+ { key: "ArrowDown", shiftKey: true },
+ ];
+ for (let key of keysWithModifier) {
+ Simulate.keyDown(document.querySelector(".tree"), key);
+ yield forceRender(tree);
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:false",
+ "---K:true",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "After DOWN + (alt|ctrl|meta|shift), K should remain focused.");
+ }
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_tree_07.html b/devtools/client/shared/components/test/mochitest/test_tree_07.html
new file mode 100644
index 000000000..178ac77e3
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_tree_07.html
@@ -0,0 +1,64 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that arrows get the open attribute when their item's children are expanded.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <link rel="stylesheet" href="chrome://devtools/skin/light-theme.css" type="text/css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const Tree = React.createFactory(browserRequire("devtools/client/shared/components/tree"));
+ const tree = ReactDOM.render(Tree(TEST_TREE_INTERFACE), window.document.body);
+
+ yield setProps(tree, {
+ renderItem: (item, depth, focused, arrow) => {
+ return React.DOM.div(
+ {
+ id: item,
+ style: { marginLeft: depth * 16 + "px" }
+ },
+ arrow,
+ item
+ );
+ }
+ });
+
+ TEST_TREE.expanded = new Set("ABCDEFGHIJKLMNO".split(""));
+ yield forceRender(tree);
+
+ let arrows = document.querySelectorAll(".arrow");
+ for (let a of arrows) {
+ ok(a.classList.contains("open"), "Every arrow should be open.");
+ }
+
+ TEST_TREE.expanded = new Set();
+ yield forceRender(tree);
+
+ arrows = document.querySelectorAll(".arrow");
+ for (let a of arrows) {
+ ok(!a.classList.contains("open"), "Every arrow should be closed.");
+ }
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_tree_08.html b/devtools/client/shared/components/test/mochitest/test_tree_08.html
new file mode 100644
index 000000000..d024f37f8
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_tree_08.html
@@ -0,0 +1,51 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that when an item in the Tree component is clicked, it steals focus from
+other inputs.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <link rel="stylesheet" href="chrome://devtools/skin/light-theme.css" type="text/css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const { Simulate } = React.addons.TestUtils;
+ const Tree = React.createFactory(browserRequire("devtools/client/shared/components/tree"));
+ const tree = ReactDOM.render(Tree(Object.assign({}, TEST_TREE_INTERFACE, {
+ onFocus: x => setProps(tree, { focused: x }),
+ })), window.document.body);
+
+ const input = document.createElement("input");
+ document.body.appendChild(input);
+
+ input.focus();
+ is(document.activeElement, input, "The text input should be focused.");
+
+ Simulate.click(document.querySelector(".tree-node"));
+ yield forceRender(tree);
+
+ isnot(document.activeElement, input,
+ "The input should have had it's focus stolen by clicking on a tree item.");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_tree_09.html b/devtools/client/shared/components/test/mochitest/test_tree_09.html
new file mode 100644
index 000000000..96650134b
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_tree_09.html
@@ -0,0 +1,77 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that when an item in the Tree component is expanded or collapsed the appropriate event handler fires.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <link rel="stylesheet" href="chrome://devtools/skin/light-theme.css" type="text/css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const { Simulate } = React.addons.TestUtils;
+ const Tree = React.createFactory(browserRequire("devtools/client/shared/components/tree"));
+
+ let numberOfExpands = 0;
+ let lastExpandedItem = null;
+
+ let numberOfCollapses = 0;
+ let lastCollapsedItem = null;
+
+ const tree = ReactDOM.render(Tree(Object.assign({}, TEST_TREE_INTERFACE, {
+ autoExpandDepth: 0,
+ onExpand: item => {
+ lastExpandedItem = item;
+ numberOfExpands++;
+ TEST_TREE.expanded.add(item);
+ },
+ onCollapse: item => {
+ lastCollapsedItem = item;
+ numberOfCollapses++;
+ TEST_TREE.expanded.delete(item);
+ },
+ onFocus: item => setProps(tree, { focused: item }),
+ })), window.document.body);
+
+ yield setProps(tree, {
+ focused: "A"
+ });
+
+ is(lastExpandedItem, null);
+ is(lastCollapsedItem, null);
+
+ // Expand "A" via the keyboard and then let the component re-render.
+ Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowRight" });
+ yield forceRender(tree);
+
+ is(lastExpandedItem, "A", "Our onExpand callback should have been fired.");
+ is(numberOfExpands, 1);
+
+ // Now collapse "A" via the keyboard and then let the component re-render.
+ Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowLeft" });
+ yield forceRender(tree);
+
+ is(lastCollapsedItem, "A", "Our onCollapsed callback should have been fired.");
+ is(numberOfCollapses, 1);
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_tree_10.html b/devtools/client/shared/components/test/mochitest/test_tree_10.html
new file mode 100644
index 000000000..34f8a2633
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_tree_10.html
@@ -0,0 +1,52 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that when an item in the Tree component is expanded or collapsed the appropriate event handler fires.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <link rel="stylesheet" href="chrome://devtools/skin/light-theme.css" type="text/css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const { Simulate } = React.addons.TestUtils;
+ const Tree = React.createFactory(browserRequire("devtools/client/shared/components/tree"));
+
+ const tree = ReactDOM.render(Tree(Object.assign({
+ autoExpandDepth: 1
+ }, TEST_TREE_INTERFACE)), window.document.body);
+
+ yield setProps(tree, {
+ focused: "A"
+ });
+
+ isRenderedTree(document.body.textContent, [
+ "A:true",
+ "-B:false",
+ "-C:false",
+ "-D:false",
+ "M:false",
+ "-N:false",
+ ], "Should have auto-expanded one level.");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_tree_11.html b/devtools/client/shared/components/test/mochitest/test_tree_11.html
new file mode 100644
index 000000000..1611f7d26
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_tree_11.html
@@ -0,0 +1,92 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that when an item in the Tree component is focused by arrow key, the view is scrolled.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <link rel="stylesheet" href="chrome://devtools/skin/light-theme.css" type="text/css">
+ <style>
+ * {
+ margin: 0;
+ padding: 0;
+ height: 30px;
+ max-height: 30px;
+ min-height: 30px;
+ font-size: 10px;
+ overflow: auto;
+ }
+ </style>
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const { Simulate } = React.addons.TestUtils;
+ const Tree = React.createFactory(browserRequire("devtools/client/shared/components/tree"));
+
+ TEST_TREE.expanded = new Set("ABCDEFGHIJKLMNO".split(""));
+
+ const tree = ReactDOM.render(Tree(TEST_TREE_INTERFACE), window.document.body);
+
+ yield setProps(tree, {
+ itemHeight: 10,
+ onFocus: item => setProps(tree, { focused: item }),
+ focused: "K",
+ });
+ yield setState(tree, {
+ scroll: 10,
+ });
+ yield forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:false",
+ "---K:true",
+ "---L:false",
+ ], "Should render initial correctly");
+
+ yield new Promise(resolve => {
+ const treeElem = document.querySelector(".tree");
+ treeElem.addEventListener("scroll", function onScroll() {
+ dumpn("Got scroll event");
+ treeElem.removeEventListener("scroll", onScroll);
+ resolve();
+ });
+
+ dumpn("Sending ArrowDown key");
+ Simulate.keyDown(treeElem, { key: "ArrowDown" });
+ });
+
+ dumpn("Forcing re-render");
+ yield forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ "---L:true",
+ "--F:false",
+ ], "Should have scrolled down one");
+
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/tree.js b/devtools/client/shared/components/tree.js
new file mode 100644
index 000000000..49b5d1497
--- /dev/null
+++ b/devtools/client/shared/components/tree.js
@@ -0,0 +1,773 @@
+/* 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/. */
+/* eslint-env browser */
+"use strict";
+
+const { DOM: dom, createClass, createFactory, PropTypes } = require("devtools/client/shared/vendor/react");
+
+const AUTO_EXPAND_DEPTH = 0;
+const NUMBER_OF_OFFSCREEN_ITEMS = 1;
+
+/**
+ * A fast, generic, expandable and collapsible tree component.
+ *
+ * This tree component is fast: it can handle trees with *many* items. It only
+ * renders the subset of those items which are visible in the viewport. It's
+ * been battle tested on huge trees in the memory panel. We've optimized tree
+ * traversal and rendering, even in the presence of cross-compartment wrappers.
+ *
+ * This tree component doesn't make any assumptions about the structure of your
+ * tree data. Whether children are computed on demand, or stored in an array in
+ * the parent's `_children` property, it doesn't matter. We only require the
+ * implementation of `getChildren`, `getRoots`, `getParent`, and `isExpanded`
+ * functions.
+ *
+ * This tree component is well tested and reliable. See
+ * devtools/client/shared/components/test/mochitest/test_tree_* and its usage in
+ * the performance and memory panels.
+ *
+ * This tree component doesn't make any assumptions about how to render items in
+ * the tree. You provide a `renderItem` function, and this component will ensure
+ * that only those items whose parents are expanded and which are visible in the
+ * viewport are rendered. The `renderItem` function could render the items as a
+ * "traditional" tree or as rows in a table or anything else. It doesn't
+ * restrict you to only one certain kind of tree.
+ *
+ * The only requirement is that every item in the tree render as the same
+ * height. This is required in order to compute which items are visible in the
+ * viewport in constant time.
+ *
+ * ### Example Usage
+ *
+ * Suppose we have some tree data where each item has this form:
+ *
+ * {
+ * id: Number,
+ * label: String,
+ * parent: Item or null,
+ * children: Array of child items,
+ * expanded: bool,
+ * }
+ *
+ * Here is how we could render that data with this component:
+ *
+ * const MyTree = createClass({
+ * displayName: "MyTree",
+ *
+ * propTypes: {
+ * // The root item of the tree, with the form described above.
+ * root: PropTypes.object.isRequired
+ * },
+ *
+ * render() {
+ * return Tree({
+ * itemHeight: 20, // px
+ *
+ * getRoots: () => [this.props.root],
+ *
+ * getParent: item => item.parent,
+ * getChildren: item => item.children,
+ * getKey: item => item.id,
+ * isExpanded: item => item.expanded,
+ *
+ * renderItem: (item, depth, isFocused, arrow, isExpanded) => {
+ * let className = "my-tree-item";
+ * if (isFocused) {
+ * className += " focused";
+ * }
+ * return dom.div(
+ * {
+ * className,
+ * // Apply 10px nesting per expansion depth.
+ * style: { marginLeft: depth * 10 + "px" }
+ * },
+ * // Here is the expando arrow so users can toggle expansion and
+ * // collapse state.
+ * arrow,
+ * // And here is the label for this item.
+ * dom.span({ className: "my-tree-item-label" }, item.label)
+ * );
+ * },
+ *
+ * onExpand: item => dispatchExpandActionToRedux(item),
+ * onCollapse: item => dispatchCollapseActionToRedux(item),
+ * });
+ * }
+ * });
+ */
+module.exports = createClass({
+ displayName: "Tree",
+
+ propTypes: {
+ // Required props
+
+ // A function to get an item's parent, or null if it is a root.
+ //
+ // Type: getParent(item: Item) -> Maybe<Item>
+ //
+ // Example:
+ //
+ // // The parent of this item is stored in its `parent` property.
+ // getParent: item => item.parent
+ getParent: PropTypes.func.isRequired,
+
+ // A function to get an item's children.
+ //
+ // Type: getChildren(item: Item) -> [Item]
+ //
+ // Example:
+ //
+ // // This item's children are stored in its `children` property.
+ // getChildren: item => item.children
+ getChildren: PropTypes.func.isRequired,
+
+ // A function which takes an item and ArrowExpander component instance and
+ // returns a component, or text, or anything else that React considers
+ // renderable.
+ //
+ // Type: renderItem(item: Item,
+ // depth: Number,
+ // isFocused: Boolean,
+ // arrow: ReactComponent,
+ // isExpanded: Boolean) -> ReactRenderable
+ //
+ // Example:
+ //
+ // renderItem: (item, depth, isFocused, arrow, isExpanded) => {
+ // let className = "my-tree-item";
+ // if (isFocused) {
+ // className += " focused";
+ // }
+ // return dom.div(
+ // {
+ // className,
+ // style: { marginLeft: depth * 10 + "px" }
+ // },
+ // arrow,
+ // dom.span({ className: "my-tree-item-label" }, item.label)
+ // );
+ // },
+ renderItem: PropTypes.func.isRequired,
+
+ // A function which returns the roots of the tree (forest).
+ //
+ // Type: getRoots() -> [Item]
+ //
+ // Example:
+ //
+ // // In this case, we only have one top level, root item. You could
+ // // return multiple items if you have many top level items in your
+ // // tree.
+ // getRoots: () => [this.props.rootOfMyTree]
+ getRoots: PropTypes.func.isRequired,
+
+ // A function to get a unique key for the given item. This helps speed up
+ // React's rendering a *TON*.
+ //
+ // Type: getKey(item: Item) -> String
+ //
+ // Example:
+ //
+ // getKey: item => `my-tree-item-${item.uniqueId}`
+ getKey: PropTypes.func.isRequired,
+
+ // A function to get whether an item is expanded or not. If an item is not
+ // expanded, then it must be collapsed.
+ //
+ // Type: isExpanded(item: Item) -> Boolean
+ //
+ // Example:
+ //
+ // isExpanded: item => item.expanded,
+ isExpanded: PropTypes.func.isRequired,
+
+ // The height of an item in the tree including margin and padding, in
+ // pixels.
+ itemHeight: PropTypes.number.isRequired,
+
+ // Optional props
+
+ // The currently focused item, if any such item exists.
+ focused: PropTypes.any,
+
+ // Handle when a new item is focused.
+ onFocus: PropTypes.func,
+
+ // The depth to which we should automatically expand new items.
+ autoExpandDepth: PropTypes.number,
+
+ // Optional event handlers for when items are expanded or collapsed. Useful
+ // for dispatching redux events and updating application state, maybe lazily
+ // loading subtrees from a worker, etc.
+ //
+ // Type:
+ // onExpand(item: Item)
+ // onCollapse(item: Item)
+ //
+ // Example:
+ //
+ // onExpand: item => dispatchExpandActionToRedux(item)
+ onExpand: PropTypes.func,
+ onCollapse: PropTypes.func,
+ },
+
+ getDefaultProps() {
+ return {
+ autoExpandDepth: AUTO_EXPAND_DEPTH,
+ };
+ },
+
+ getInitialState() {
+ return {
+ scroll: 0,
+ height: window.innerHeight,
+ seen: new Set(),
+ };
+ },
+
+ componentDidMount() {
+ window.addEventListener("resize", this._updateHeight);
+ this._autoExpand();
+ this._updateHeight();
+ },
+
+ componentWillReceiveProps(nextProps) {
+ this._autoExpand();
+ this._updateHeight();
+ },
+
+ componentWillUnmount() {
+ window.removeEventListener("resize", this._updateHeight);
+ },
+
+ _autoExpand() {
+ if (!this.props.autoExpandDepth) {
+ return;
+ }
+
+ // Automatically expand the first autoExpandDepth levels for new items. Do
+ // not use the usual DFS infrastructure because we don't want to ignore
+ // collapsed nodes.
+ const autoExpand = (item, currentDepth) => {
+ if (currentDepth >= this.props.autoExpandDepth ||
+ this.state.seen.has(item)) {
+ return;
+ }
+
+ this.props.onExpand(item);
+ this.state.seen.add(item);
+
+ const children = this.props.getChildren(item);
+ const length = children.length;
+ for (let i = 0; i < length; i++) {
+ autoExpand(children[i], currentDepth + 1);
+ }
+ };
+
+ const roots = this.props.getRoots();
+ const length = roots.length;
+ for (let i = 0; i < length; i++) {
+ autoExpand(roots[i], 0);
+ }
+ },
+
+ _preventArrowKeyScrolling(e) {
+ switch (e.key) {
+ case "ArrowUp":
+ case "ArrowDown":
+ case "ArrowLeft":
+ case "ArrowRight":
+ e.preventDefault();
+ e.stopPropagation();
+ if (e.nativeEvent) {
+ if (e.nativeEvent.preventDefault) {
+ e.nativeEvent.preventDefault();
+ }
+ if (e.nativeEvent.stopPropagation) {
+ e.nativeEvent.stopPropagation();
+ }
+ }
+ }
+ },
+
+ /**
+ * Updates the state's height based on clientHeight.
+ */
+ _updateHeight() {
+ this.setState({
+ height: this.refs.tree.clientHeight
+ });
+ },
+
+ /**
+ * Perform a pre-order depth-first search from item.
+ */
+ _dfs(item, maxDepth = Infinity, traversal = [], _depth = 0) {
+ traversal.push({ item, depth: _depth });
+
+ if (!this.props.isExpanded(item)) {
+ return traversal;
+ }
+
+ const nextDepth = _depth + 1;
+
+ if (nextDepth > maxDepth) {
+ return traversal;
+ }
+
+ const children = this.props.getChildren(item);
+ const length = children.length;
+ for (let i = 0; i < length; i++) {
+ this._dfs(children[i], maxDepth, traversal, nextDepth);
+ }
+
+ return traversal;
+ },
+
+ /**
+ * Perform a pre-order depth-first search over the whole forest.
+ */
+ _dfsFromRoots(maxDepth = Infinity) {
+ const traversal = [];
+
+ const roots = this.props.getRoots();
+ const length = roots.length;
+ for (let i = 0; i < length; i++) {
+ this._dfs(roots[i], maxDepth, traversal);
+ }
+
+ return traversal;
+ },
+
+ /**
+ * Expands current row.
+ *
+ * @param {Object} item
+ * @param {Boolean} expandAllChildren
+ */
+ _onExpand: oncePerAnimationFrame(function (item, expandAllChildren) {
+ if (this.props.onExpand) {
+ this.props.onExpand(item);
+
+ if (expandAllChildren) {
+ const children = this._dfs(item);
+ const length = children.length;
+ for (let i = 0; i < length; i++) {
+ this.props.onExpand(children[i].item);
+ }
+ }
+ }
+ }),
+
+ /**
+ * Collapses current row.
+ *
+ * @param {Object} item
+ */
+ _onCollapse: oncePerAnimationFrame(function (item) {
+ if (this.props.onCollapse) {
+ this.props.onCollapse(item);
+ }
+ }),
+
+ /**
+ * Sets the passed in item to be the focused item.
+ *
+ * @param {Number} index
+ * The index of the item in a full DFS traversal (ignoring collapsed
+ * nodes). Ignored if `item` is undefined.
+ *
+ * @param {Object|undefined} item
+ * The item to be focused, or undefined to focus no item.
+ */
+ _focus(index, item) {
+ if (item !== undefined) {
+ const itemStartPosition = index * this.props.itemHeight;
+ const itemEndPosition = (index + 1) * this.props.itemHeight;
+
+ // Note that if the height of the viewport (this.state.height) is less
+ // than `this.props.itemHeight`, we could accidentally try and scroll both
+ // up and down in a futile attempt to make both the item's start and end
+ // positions visible. Instead, give priority to the start of the item by
+ // checking its position first, and then using an "else if", rather than
+ // a separate "if", for the end position.
+ if (this.state.scroll > itemStartPosition) {
+ this.refs.tree.scrollTo(0, itemStartPosition);
+ } else if ((this.state.scroll + this.state.height) < itemEndPosition) {
+ this.refs.tree.scrollTo(0, itemEndPosition - this.state.height);
+ }
+ }
+
+ if (this.props.onFocus) {
+ this.props.onFocus(item);
+ }
+ },
+
+ /**
+ * Sets the state to have no focused item.
+ */
+ _onBlur() {
+ this._focus(0, undefined);
+ },
+
+ /**
+ * Fired on a scroll within the tree's container, updates
+ * the stored position of the view port to handle virtual view rendering.
+ *
+ * @param {Event} e
+ */
+ _onScroll: oncePerAnimationFrame(function (e) {
+ this.setState({
+ scroll: Math.max(this.refs.tree.scrollTop, 0),
+ height: this.refs.tree.clientHeight
+ });
+ }),
+
+ /**
+ * Handles key down events in the tree's container.
+ *
+ * @param {Event} e
+ */
+ _onKeyDown(e) {
+ if (this.props.focused == null) {
+ return;
+ }
+
+ // Allow parent nodes to use navigation arrows with modifiers.
+ if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) {
+ return;
+ }
+
+ this._preventArrowKeyScrolling(e);
+
+ switch (e.key) {
+ case "ArrowUp":
+ this._focusPrevNode();
+ return;
+
+ case "ArrowDown":
+ this._focusNextNode();
+ return;
+
+ case "ArrowLeft":
+ if (this.props.isExpanded(this.props.focused)
+ && this.props.getChildren(this.props.focused).length) {
+ this._onCollapse(this.props.focused);
+ } else {
+ this._focusParentNode();
+ }
+ return;
+
+ case "ArrowRight":
+ if (!this.props.isExpanded(this.props.focused)) {
+ this._onExpand(this.props.focused);
+ } else {
+ this._focusNextNode();
+ }
+ return;
+ }
+ },
+
+ /**
+ * Sets the previous node relative to the currently focused item, to focused.
+ */
+ _focusPrevNode: oncePerAnimationFrame(function () {
+ // Start a depth first search and keep going until we reach the currently
+ // focused node. Focus the previous node in the DFS, if it exists. If it
+ // doesn't exist, we're at the first node already.
+
+ let prev;
+ let prevIndex;
+
+ const traversal = this._dfsFromRoots();
+ const length = traversal.length;
+ for (let i = 0; i < length; i++) {
+ const item = traversal[i].item;
+ if (item === this.props.focused) {
+ break;
+ }
+ prev = item;
+ prevIndex = i;
+ }
+
+ if (prev === undefined) {
+ return;
+ }
+
+ this._focus(prevIndex, prev);
+ }),
+
+ /**
+ * Handles the down arrow key which will focus either the next child
+ * or sibling row.
+ */
+ _focusNextNode: oncePerAnimationFrame(function () {
+ // Start a depth first search and keep going until we reach the currently
+ // focused node. Focus the next node in the DFS, if it exists. If it
+ // doesn't exist, we're at the last node already.
+
+ const traversal = this._dfsFromRoots();
+ const length = traversal.length;
+ let i = 0;
+
+ while (i < length) {
+ if (traversal[i].item === this.props.focused) {
+ break;
+ }
+ i++;
+ }
+
+ if (i + 1 < traversal.length) {
+ this._focus(i + 1, traversal[i + 1].item);
+ }
+ }),
+
+ /**
+ * Handles the left arrow key, going back up to the current rows'
+ * parent row.
+ */
+ _focusParentNode: oncePerAnimationFrame(function () {
+ const parent = this.props.getParent(this.props.focused);
+ if (!parent) {
+ return;
+ }
+
+ const traversal = this._dfsFromRoots();
+ const length = traversal.length;
+ let parentIndex = 0;
+ for (; parentIndex < length; parentIndex++) {
+ if (traversal[parentIndex].item === parent) {
+ break;
+ }
+ }
+
+ this._focus(parentIndex, parent);
+ }),
+
+ render() {
+ const traversal = this._dfsFromRoots();
+
+ // 'begin' and 'end' are the index of the first (at least partially) visible item
+ // and the index after the last (at least partially) visible item, respectively.
+ // `NUMBER_OF_OFFSCREEN_ITEMS` is removed from `begin` and added to `end` so that
+ // the top and bottom of the page are filled with the `NUMBER_OF_OFFSCREEN_ITEMS`
+ // previous and next items respectively, which helps the user to see fewer empty
+ // gaps when scrolling quickly.
+ const { itemHeight } = this.props;
+ const { scroll, height } = this.state;
+ const begin = Math.max(((scroll / itemHeight) | 0) - NUMBER_OF_OFFSCREEN_ITEMS, 0);
+ const end = Math.ceil((scroll + height) / itemHeight) + NUMBER_OF_OFFSCREEN_ITEMS;
+ const toRender = traversal.slice(begin, end);
+ const topSpacerHeight = begin * itemHeight;
+ const bottomSpacerHeight = Math.max(traversal.length - end, 0) * itemHeight;
+
+ const nodes = [
+ dom.div({
+ key: "top-spacer",
+ style: {
+ padding: 0,
+ margin: 0,
+ height: topSpacerHeight + "px"
+ }
+ })
+ ];
+
+ for (let i = 0; i < toRender.length; i++) {
+ const index = begin + i;
+ const first = index == 0;
+ const last = index == traversal.length - 1;
+ const { item, depth } = toRender[i];
+ nodes.push(TreeNode({
+ key: this.props.getKey(item),
+ index,
+ first,
+ last,
+ item,
+ depth,
+ renderItem: this.props.renderItem,
+ focused: this.props.focused === item,
+ expanded: this.props.isExpanded(item),
+ hasChildren: !!this.props.getChildren(item).length,
+ onExpand: this._onExpand,
+ onCollapse: this._onCollapse,
+ onFocus: () => this._focus(begin + i, item),
+ onFocusedNodeUnmount: () => this.refs.tree && this.refs.tree.focus(),
+ }));
+ }
+
+ nodes.push(dom.div({
+ key: "bottom-spacer",
+ style: {
+ padding: 0,
+ margin: 0,
+ height: bottomSpacerHeight + "px"
+ }
+ }));
+
+ return dom.div(
+ {
+ className: "tree",
+ ref: "tree",
+ onKeyDown: this._onKeyDown,
+ onKeyPress: this._preventArrowKeyScrolling,
+ onKeyUp: this._preventArrowKeyScrolling,
+ onScroll: this._onScroll,
+ style: {
+ padding: 0,
+ margin: 0
+ }
+ },
+ nodes
+ );
+ }
+});
+
+/**
+ * An arrow that displays whether its node is expanded (â–¼) or collapsed
+ * (â–¶). When its node has no children, it is hidden.
+ */
+const ArrowExpander = createFactory(createClass({
+ displayName: "ArrowExpander",
+
+ shouldComponentUpdate(nextProps, nextState) {
+ return this.props.item !== nextProps.item
+ || this.props.visible !== nextProps.visible
+ || this.props.expanded !== nextProps.expanded;
+ },
+
+ render() {
+ const attrs = {
+ className: "arrow theme-twisty",
+ onClick: this.props.expanded
+ ? () => this.props.onCollapse(this.props.item)
+ : e => this.props.onExpand(this.props.item, e.altKey)
+ };
+
+ if (this.props.expanded) {
+ attrs.className += " open";
+ }
+
+ if (!this.props.visible) {
+ attrs.style = {
+ visibility: "hidden"
+ };
+ }
+
+ return dom.div(attrs);
+ }
+}));
+
+const TreeNode = createFactory(createClass({
+ componentDidMount() {
+ if (this.props.focused) {
+ this.refs.button.focus();
+ }
+ },
+
+ componentDidUpdate() {
+ if (this.props.focused) {
+ this.refs.button.focus();
+ }
+ },
+
+ componentWillUnmount() {
+ // If this node is being destroyed and has focus, transfer the focus manually
+ // to the parent tree component. Otherwise, the focus will get lost and keyboard
+ // navigation in the tree will stop working. This is a workaround for a XUL bug.
+ // See bugs 1259228 and 1152441 for details.
+ // DE-XUL: Remove this hack once all usages are only in HTML documents.
+ if (this.props.focused) {
+ this.refs.button.blur();
+ if (this.props.onFocusedNodeUnmount) {
+ this.props.onFocusedNodeUnmount();
+ }
+ }
+ },
+
+ _buttonAttrs: {
+ ref: "button",
+ style: {
+ opacity: 0,
+ width: "0 !important",
+ height: "0 !important",
+ padding: "0 !important",
+ outline: "none",
+ MozAppearance: "none",
+ // XXX: Despite resetting all of the above properties (and margin), the
+ // button still ends up with ~79px width, so we set a large negative
+ // margin to completely hide it.
+ MozMarginStart: "-1000px !important",
+ }
+ },
+
+ render() {
+ const arrow = ArrowExpander({
+ item: this.props.item,
+ expanded: this.props.expanded,
+ visible: this.props.hasChildren,
+ onExpand: this.props.onExpand,
+ onCollapse: this.props.onCollapse,
+ });
+
+ let classList = [ "tree-node", "div" ];
+ if (this.props.index % 2) {
+ classList.push("tree-node-odd");
+ }
+ if (this.props.first) {
+ classList.push("tree-node-first");
+ }
+ if (this.props.last) {
+ classList.push("tree-node-last");
+ }
+
+ return dom.div(
+ {
+ className: classList.join(" "),
+ onFocus: this.props.onFocus,
+ onClick: this.props.onFocus,
+ onBlur: this.props.onBlur,
+ "data-expanded": this.props.expanded ? "" : undefined,
+ "data-depth": this.props.depth,
+ style: {
+ padding: 0,
+ margin: 0
+ }
+ },
+
+ this.props.renderItem(this.props.item,
+ this.props.depth,
+ this.props.focused,
+ arrow,
+ this.props.expanded),
+
+ // XXX: OSX won't focus/blur regular elements even if you set tabindex
+ // unless there is an input/button child.
+ dom.button(this._buttonAttrs)
+ );
+ }
+}));
+
+/**
+ * Create a function that calls the given function `fn` only once per animation
+ * frame.
+ *
+ * @param {Function} fn
+ * @returns {Function}
+ */
+function oncePerAnimationFrame(fn) {
+ let animationId = null;
+ let argsToPass = null;
+ return function (...args) {
+ argsToPass = args;
+ if (animationId !== null) {
+ return;
+ }
+
+ animationId = requestAnimationFrame(() => {
+ fn.call(this, ...argsToPass);
+ animationId = null;
+ argsToPass = null;
+ });
+ };
+}
diff --git a/devtools/client/shared/components/tree/label-cell.js b/devtools/client/shared/components/tree/label-cell.js
new file mode 100644
index 000000000..e14875b4d
--- /dev/null
+++ b/devtools/client/shared/components/tree/label-cell.js
@@ -0,0 +1,66 @@
+/* -*- 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Shortcuts
+ const { td, span } = React.DOM;
+ const PropTypes = React.PropTypes;
+
+ /**
+ * Render the default cell used for toggle buttons
+ */
+ let LabelCell = React.createClass({
+ displayName: "LabelCell",
+
+ // See the TreeView component for details related
+ // to the 'member' object.
+ propTypes: {
+ member: PropTypes.object.isRequired
+ },
+
+ render: function () {
+ let member = this.props.member;
+ let level = member.level || 0;
+
+ // Compute indentation dynamically. The deeper the item is
+ // inside the hierarchy, the bigger is the left padding.
+ let rowStyle = {
+ "paddingInlineStart": (level * 16) + "px",
+ };
+
+ let iconClassList = ["treeIcon"];
+ if (member.hasChildren && member.loading) {
+ iconClassList.push("devtools-throbber");
+ } else if (member.hasChildren) {
+ iconClassList.push("theme-twisty");
+ }
+ if (member.open) {
+ iconClassList.push("open");
+ }
+
+ return (
+ td({
+ className: "treeLabelCell",
+ key: "default",
+ style: rowStyle},
+ span({ className: iconClassList.join(" ") }),
+ span({
+ className: "treeLabel " + member.type + "Label",
+ "data-level": level
+ }, member.name)
+ )
+ );
+ }
+ });
+
+ // Exports from this module
+ module.exports = LabelCell;
+});
diff --git a/devtools/client/shared/components/tree/moz.build b/devtools/client/shared/components/tree/moz.build
new file mode 100644
index 000000000..a7413f25a
--- /dev/null
+++ b/devtools/client/shared/components/tree/moz.build
@@ -0,0 +1,14 @@
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'label-cell.js',
+ 'object-provider.js',
+ 'tree-cell.js',
+ 'tree-header.js',
+ 'tree-row.js',
+ 'tree-view.css',
+ 'tree-view.js',
+)
diff --git a/devtools/client/shared/components/tree/object-provider.js b/devtools/client/shared/components/tree/object-provider.js
new file mode 100644
index 000000000..58519f81f
--- /dev/null
+++ b/devtools/client/shared/components/tree/object-provider.js
@@ -0,0 +1,90 @@
+/* -*- 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ /**
+ * Implementation of the default data provider. A provider is state less
+ * object responsible for transformation data (usually a state) to
+ * a structure that can be directly consumed by the tree-view component.
+ */
+ let ObjectProvider = {
+ getChildren: function (object) {
+ let children = [];
+
+ if (object instanceof ObjectProperty) {
+ object = object.value;
+ }
+
+ if (!object) {
+ return [];
+ }
+
+ if (typeof (object) == "string") {
+ return [];
+ }
+
+ for (let prop in object) {
+ try {
+ children.push(new ObjectProperty(prop, object[prop]));
+ } catch (e) {
+ console.error(e);
+ }
+ }
+ return children;
+ },
+
+ hasChildren: function (object) {
+ if (object instanceof ObjectProperty) {
+ object = object.value;
+ }
+
+ if (!object) {
+ return false;
+ }
+
+ if (typeof object == "string") {
+ return false;
+ }
+
+ if (typeof object !== "object") {
+ return false;
+ }
+
+ return Object.keys(object).length > 0;
+ },
+
+ getLabel: function (object) {
+ return (object instanceof ObjectProperty) ?
+ object.name : null;
+ },
+
+ getValue: function (object) {
+ return (object instanceof ObjectProperty) ?
+ object.value : null;
+ },
+
+ getKey: function (object) {
+ return (object instanceof ObjectProperty) ?
+ object.name : null;
+ },
+
+ getType: function (object) {
+ return (object instanceof ObjectProperty) ?
+ typeof object.value : typeof object;
+ }
+ };
+
+ function ObjectProperty(name, value) {
+ this.name = name;
+ this.value = value;
+ }
+
+ // Exports from this module
+ exports.ObjectProperty = ObjectProperty;
+ exports.ObjectProvider = ObjectProvider;
+});
diff --git a/devtools/client/shared/components/tree/tree-cell.js b/devtools/client/shared/components/tree/tree-cell.js
new file mode 100644
index 000000000..f3c48510f
--- /dev/null
+++ b/devtools/client/shared/components/tree/tree-cell.js
@@ -0,0 +1,101 @@
+/* -*- 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Shortcuts
+ const { td, span } = React.DOM;
+ const PropTypes = React.PropTypes;
+
+ /**
+ * This template represents a cell in TreeView row. It's rendered
+ * using <td> element (the row is <tr> and the entire tree is <table>).
+ */
+ let TreeCell = React.createClass({
+ displayName: "TreeCell",
+
+ // See TreeView component for detailed property explanation.
+ propTypes: {
+ value: PropTypes.any,
+ decorator: PropTypes.object,
+ id: PropTypes.string.isRequired,
+ member: PropTypes.object.isRequired,
+ renderValue: PropTypes.func.isRequired
+ },
+
+ /**
+ * Optimize cell rendering. Rerender cell content only if
+ * the value or expanded state changes.
+ */
+ shouldComponentUpdate: function (nextProps) {
+ return (this.props.value != nextProps.value) ||
+ (this.props.member.open != nextProps.member.open);
+ },
+
+ getCellClass: function (object, id) {
+ let decorator = this.props.decorator;
+ if (!decorator || !decorator.getCellClass) {
+ return [];
+ }
+
+ // Decorator can return a simple string or array of strings.
+ let classNames = decorator.getCellClass(object, id);
+ if (!classNames) {
+ return [];
+ }
+
+ if (typeof classNames == "string") {
+ classNames = [classNames];
+ }
+
+ return classNames;
+ },
+
+ render: function () {
+ let member = this.props.member;
+ let type = member.type || "";
+ let id = this.props.id;
+ let value = this.props.value;
+ let decorator = this.props.decorator;
+
+ // Compute class name list for the <td> element.
+ let classNames = this.getCellClass(member.object, id) || [];
+ classNames.push("treeValueCell");
+ classNames.push(type + "Cell");
+
+ // Render value using a default render function or custom
+ // provided function from props or a decorator.
+ let renderValue = this.props.renderValue || defaultRenderValue;
+ if (decorator && decorator.renderValue) {
+ renderValue = decorator.renderValue(member.object, id) || renderValue;
+ }
+
+ let props = Object.assign({}, this.props, {
+ object: value,
+ });
+
+ // Render me!
+ return (
+ td({ className: classNames.join(" ") },
+ span({}, renderValue(props))
+ )
+ );
+ }
+ });
+
+ // Default value rendering.
+ let defaultRenderValue = props => {
+ return (
+ props.object + ""
+ );
+ };
+
+ // Exports from this module
+ module.exports = TreeCell;
+});
diff --git a/devtools/client/shared/components/tree/tree-header.js b/devtools/client/shared/components/tree/tree-header.js
new file mode 100644
index 000000000..eec5363dd
--- /dev/null
+++ b/devtools/client/shared/components/tree/tree-header.js
@@ -0,0 +1,100 @@
+/* -*- 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Shortcuts
+ const { thead, tr, td, div } = React.DOM;
+ const PropTypes = React.PropTypes;
+
+ /**
+ * This component is responsible for rendering tree header.
+ * It's based on <thead> element.
+ */
+ let TreeHeader = React.createClass({
+ displayName: "TreeHeader",
+
+ // See also TreeView component for detailed info about properties.
+ propTypes: {
+ // Custom tree decorator
+ decorator: PropTypes.object,
+ // True if the header should be visible
+ header: PropTypes.bool,
+ // Array with column definition
+ columns: PropTypes.array
+ },
+
+ getDefaultProps: function () {
+ return {
+ columns: [{
+ id: "default"
+ }]
+ };
+ },
+
+ getHeaderClass: function (colId) {
+ let decorator = this.props.decorator;
+ if (!decorator || !decorator.getHeaderClass) {
+ return [];
+ }
+
+ // Decorator can return a simple string or array of strings.
+ let classNames = decorator.getHeaderClass(colId);
+ if (!classNames) {
+ return [];
+ }
+
+ if (typeof classNames == "string") {
+ classNames = [classNames];
+ }
+
+ return classNames;
+ },
+
+ render: function () {
+ let cells = [];
+ let visible = this.props.header;
+
+ // Render the rest of the columns (if any)
+ this.props.columns.forEach(col => {
+ let cellStyle = {
+ "width": col.width ? col.width : "",
+ };
+
+ let classNames = [];
+
+ if (visible) {
+ classNames = this.getHeaderClass(col.id);
+ classNames.push("treeHeaderCell");
+ }
+
+ cells.push(
+ td({
+ className: classNames.join(" "),
+ style: cellStyle,
+ key: col.id},
+ div({ className: visible ? "treeHeaderCellBox" : "" },
+ visible ? col.title : ""
+ )
+ )
+ );
+ });
+
+ return (
+ thead({}, tr({ className: visible ? "treeHeaderRow" : "" },
+ cells
+ ))
+ );
+ }
+ });
+
+ // Exports from this module
+ module.exports = TreeHeader;
+});
diff --git a/devtools/client/shared/components/tree/tree-row.js b/devtools/client/shared/components/tree/tree-row.js
new file mode 100644
index 000000000..adfb1f3ae
--- /dev/null
+++ b/devtools/client/shared/components/tree/tree-row.js
@@ -0,0 +1,184 @@
+/* -*- 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const React = require("devtools/client/shared/vendor/react");
+ const ReactDOM = require("devtools/client/shared/vendor/react-dom");
+
+ // Tree
+ const TreeCell = React.createFactory(require("./tree-cell"));
+ const LabelCell = React.createFactory(require("./label-cell"));
+
+ // Shortcuts
+ const { tr } = React.DOM;
+ const PropTypes = React.PropTypes;
+
+ /**
+ * This template represents a node in TreeView component. It's rendered
+ * using <tr> element (the entire tree is one big <table>).
+ */
+ let TreeRow = React.createClass({
+ displayName: "TreeRow",
+
+ // See TreeView component for more details about the props and
+ // the 'member' object.
+ propTypes: {
+ member: PropTypes.shape({
+ object: PropTypes.obSject,
+ name: PropTypes.sring,
+ type: PropTypes.string.isRequired,
+ rowClass: PropTypes.string.isRequired,
+ level: PropTypes.number.isRequired,
+ hasChildren: PropTypes.bool,
+ value: PropTypes.any,
+ open: PropTypes.bool.isRequired,
+ path: PropTypes.string.isRequired,
+ hidden: PropTypes.bool,
+ }),
+ decorator: PropTypes.object,
+ renderCell: PropTypes.object,
+ renderLabelCell: PropTypes.object,
+ columns: PropTypes.array.isRequired,
+ provider: PropTypes.object.isRequired,
+ onClick: PropTypes.func.isRequired
+ },
+
+ componentWillReceiveProps(nextProps) {
+ // I don't like accessing the underlying DOM elements directly,
+ // but this optimization makes the filtering so damn fast!
+ // The row doesn't have to be re-rendered, all we really need
+ // to do is toggling a class name.
+ // The important part is that DOM elements don't need to be
+ // re-created when they should appear again.
+ if (nextProps.member.hidden != this.props.member.hidden) {
+ let row = ReactDOM.findDOMNode(this);
+ row.classList.toggle("hidden");
+ }
+ },
+
+ /**
+ * Optimize row rendering. If props are the same do not render.
+ * This makes the rendering a lot faster!
+ */
+ shouldComponentUpdate: function (nextProps) {
+ let props = ["name", "open", "value", "loading"];
+ for (let p in props) {
+ if (nextProps.member[props[p]] != this.props.member[props[p]]) {
+ return true;
+ }
+ }
+
+ return false;
+ },
+
+ getRowClass: function (object) {
+ let decorator = this.props.decorator;
+ if (!decorator || !decorator.getRowClass) {
+ return [];
+ }
+
+ // Decorator can return a simple string or array of strings.
+ let classNames = decorator.getRowClass(object);
+ if (!classNames) {
+ return [];
+ }
+
+ if (typeof classNames == "string") {
+ classNames = [classNames];
+ }
+
+ return classNames;
+ },
+
+ render: function () {
+ let member = this.props.member;
+ let decorator = this.props.decorator;
+
+ // Compute class name list for the <tr> element.
+ let classNames = this.getRowClass(member.object) || [];
+ classNames.push("treeRow");
+ classNames.push(member.type + "Row");
+
+ if (member.hasChildren) {
+ classNames.push("hasChildren");
+ }
+
+ if (member.open) {
+ classNames.push("opened");
+ }
+
+ if (member.loading) {
+ classNames.push("loading");
+ }
+
+ if (member.hidden) {
+ classNames.push("hidden");
+ }
+
+ // The label column (with toggle buttons) is usually
+ // the first one, but there might be cases (like in
+ // the Memory panel) where the toggling is done
+ // in the last column.
+ let cells = [];
+
+ // Get components for rendering cells.
+ let renderCell = this.props.renderCell || RenderCell;
+ let renderLabelCell = this.props.renderLabelCell || RenderLabelCell;
+ if (decorator && decorator.renderLabelCell) {
+ renderLabelCell = decorator.renderLabelCell(member.object) ||
+ renderLabelCell;
+ }
+
+ // Render a cell for every column.
+ this.props.columns.forEach(col => {
+ let props = Object.assign({}, this.props, {
+ key: col.id,
+ id: col.id,
+ value: this.props.provider.getValue(member.object, col.id)
+ });
+
+ if (decorator && decorator.renderCell) {
+ renderCell = decorator.renderCell(member.object, col.id);
+ }
+
+ let render = (col.id == "default") ? renderLabelCell : renderCell;
+
+ // Some cells don't have to be rendered. This happens when some
+ // other cells span more columns. Note that the label cells contains
+ // toggle buttons and should be usually there unless we are rendering
+ // a simple non-expandable table.
+ if (render) {
+ cells.push(render(props));
+ }
+ });
+
+ // Render tree row
+ return (
+ tr({
+ className: classNames.join(" "),
+ onClick: this.props.onClick},
+ cells
+ )
+ );
+ }
+ });
+
+ // Helpers
+
+ let RenderCell = props => {
+ return TreeCell(props);
+ };
+
+ let RenderLabelCell = props => {
+ return LabelCell(props);
+ };
+
+ // Exports from this module
+ module.exports = TreeRow;
+});
diff --git a/devtools/client/shared/components/tree/tree-view.css b/devtools/client/shared/components/tree/tree-view.css
new file mode 100644
index 000000000..850533872
--- /dev/null
+++ b/devtools/client/shared/components/tree/tree-view.css
@@ -0,0 +1,157 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+@import url('resource://devtools/client/shared/components/reps/reps.css');
+
+/******************************************************************************/
+/* TreeView Colors */
+
+:root {
+ --tree-link-color: blue;
+ --tree-header-background: #C8D2DC;
+ --tree-header-sorted-background: #AAC3DC;
+}
+
+/******************************************************************************/
+/* TreeView Table*/
+
+.treeTable .treeLabelCell {
+ padding: 2px 0;
+ vertical-align: top;
+ white-space: nowrap;
+}
+
+.treeTable .treeLabelCell::after {
+ content: ":";
+ color: var(--object-color);
+}
+
+.treeTable .treeValueCell {
+ padding: 2px 0;
+ padding-inline-start: 5px;
+ overflow: hidden;
+}
+
+.treeTable .treeLabel {
+ cursor: default;
+ overflow: hidden;
+ padding-inline-start: 4px;
+ white-space: nowrap;
+}
+
+/* No paddding if there is actually no label */
+.treeTable .treeLabel:empty {
+ padding-inline-start: 0;
+}
+
+.treeTable .treeRow.hasChildren > .treeLabelCell > .treeLabel:hover {
+ cursor: pointer;
+ color: var(--tree-link-color);
+ text-decoration: underline;
+}
+
+/* Filtering */
+.treeTable .treeRow.hidden {
+ display: none;
+}
+
+/******************************************************************************/
+/* Toggle Icon */
+
+.treeTable .treeRow .treeIcon {
+ height: 14px;
+ width: 14px;
+ font-size: 10px; /* Set the size of loading spinner */
+ display: inline-block;
+ vertical-align: bottom;
+ margin-inline-start: 3px;
+ padding-top: 1px;
+}
+
+/* All expanded/collapsed styles need to apply on immediate children
+ since there might be nested trees within a tree. */
+.treeTable .treeRow.hasChildren > .treeLabelCell > .treeIcon {
+ cursor: pointer;
+ background-repeat: no-repeat;
+}
+
+/******************************************************************************/
+/* Header */
+
+.treeTable .treeHeaderRow {
+ height: 18px;
+}
+
+.treeTable .treeHeaderCell {
+ cursor: pointer;
+ -moz-user-select: none;
+ border-bottom: 1px solid rgba(0, 0, 0, 0.2);
+ padding: 0 !important;
+ background: linear-gradient(
+ rgba(255, 255, 255, 0.05),
+ rgba(0, 0, 0, 0.05)),
+ radial-gradient(1px 60% at right,
+ rgba(0, 0, 0, 0.8) 0%,
+ transparent 80%) repeat-x var(--tree-header-background);
+ color: var(--theme-body-color);
+ white-space: nowrap;
+}
+
+.treeTable .treeHeaderCellBox {
+ padding: 2px 0;
+ padding-inline-start: 10px;
+ padding-inline-end: 14px;
+}
+
+.treeTable .treeHeaderRow > .treeHeaderCell:first-child > .treeHeaderCellBox {
+ padding: 0;
+}
+
+.treeTable .treeHeaderSorted {
+ background-color: var(--tree-header-sorted-background);
+}
+
+.treeTable .treeHeaderSorted > .treeHeaderCellBox {
+ background: url(chrome://devtools/skin/images/firebug/arrow-down.svg) no-repeat calc(100% - 4px);
+}
+
+.treeTable .treeHeaderSorted.sortedAscending > .treeHeaderCellBox {
+ background-image: url(chrome://devtools/skin/images/firebug/arrow-up.svg);
+}
+
+.treeTable .treeHeaderCell:hover:active {
+ background-image: linear-gradient(
+ rgba(0, 0, 0, 0.1),
+ transparent),
+ radial-gradient(1px 60% at right,
+ rgba(0, 0, 0, 0.8) 0%,
+ transparent 80%);
+}
+
+/******************************************************************************/
+/* Themes */
+
+.theme-light .treeTable .treeRow:hover,
+.theme-dark .treeTable .treeRow:hover {
+ background-color: var(--theme-selection-background-semitransparent) !important;
+}
+
+.theme-firebug .treeTable .treeRow:hover {
+ background-color: var(--theme-body-background);
+}
+
+.theme-light .treeTable .treeLabel,
+.theme-dark .treeTable .treeLabel {
+ color: var(--theme-highlight-pink);
+}
+
+.theme-light .treeTable .treeRow.hasChildren > .treeLabelCell > .treeLabel:hover,
+.theme-dark .treeTable .treeRow.hasChildren > .treeLabelCell > .treeLabel:hover {
+ color: var(--theme-highlight-pink);
+}
+
+.theme-firebug .treeTable .treeLabel {
+ color: var(--theme-body-color);
+}
diff --git a/devtools/client/shared/components/tree/tree-view.js b/devtools/client/shared/components/tree/tree-view.js
new file mode 100644
index 000000000..9fae9addb
--- /dev/null
+++ b/devtools/client/shared/components/tree/tree-view.js
@@ -0,0 +1,352 @@
+/* -*- 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Reps
+ const { ObjectProvider } = require("./object-provider");
+ const TreeRow = React.createFactory(require("./tree-row"));
+ const TreeHeader = React.createFactory(require("./tree-header"));
+
+ // Shortcuts
+ const DOM = React.DOM;
+ const PropTypes = React.PropTypes;
+
+ /**
+ * This component represents a tree view with expandable/collapsible nodes.
+ * The tree is rendered using <table> element where every node is represented
+ * by <tr> element. The tree is one big table where nodes (rows) are properly
+ * indented from the left to mimic hierarchical structure of the data.
+ *
+ * The tree can have arbitrary number of columns and so, might be use
+ * as an expandable tree-table UI widget as well. By default, there is
+ * one column for node label and one for node value.
+ *
+ * The tree is maintaining its (presentation) state, which consists
+ * from list of expanded nodes and list of columns.
+ *
+ * Complete data provider interface:
+ * var TreeProvider = {
+ * getChildren: function(object);
+ * hasChildren: function(object);
+ * getLabel: function(object, colId);
+ * getValue: function(object, colId);
+ * getKey: function(object);
+ * getType: function(object);
+ * }
+ *
+ * Complete tree decorator interface:
+ * var TreeDecorator = {
+ * getRowClass: function(object);
+ * getCellClass: function(object, colId);
+ * getHeaderClass: function(colId);
+ * renderValue: function(object, colId);
+ * renderRow: function(object);
+ * renderCelL: function(object, colId);
+ * renderLabelCell: function(object);
+ * }
+ */
+ let TreeView = React.createClass({
+ displayName: "TreeView",
+
+ // The only required property (not set by default) is the input data
+ // object that is used to puputate the tree.
+ propTypes: {
+ // The input data object.
+ object: PropTypes.any,
+ className: PropTypes.string,
+ // Data provider (see also the interface above)
+ provider: PropTypes.shape({
+ getChildren: PropTypes.func,
+ hasChildren: PropTypes.func,
+ getLabel: PropTypes.func,
+ getValue: PropTypes.func,
+ getKey: PropTypes.func,
+ getType: PropTypes.func,
+ }).isRequired,
+ // Tree decorator (see also the interface above)
+ decorator: PropTypes.shape({
+ getRowClass: PropTypes.func,
+ getCellClass: PropTypes.func,
+ getHeaderClass: PropTypes.func,
+ renderValue: PropTypes.func,
+ renderRow: PropTypes.func,
+ renderCelL: PropTypes.func,
+ renderLabelCell: PropTypes.func,
+ }),
+ // Custom tree row (node) renderer
+ renderRow: PropTypes.func,
+ // Custom cell renderer
+ renderCell: PropTypes.func,
+ // Custom value renderef
+ renderValue: PropTypes.func,
+ // Custom tree label (including a toggle button) renderer
+ renderLabelCell: PropTypes.func,
+ // Set of expanded nodes
+ expandedNodes: PropTypes.object,
+ // Custom filtering callback
+ onFilter: PropTypes.func,
+ // Custom sorting callback
+ onSort: PropTypes.func,
+ // A header is displayed if set to true
+ header: PropTypes.bool,
+ // Array of columns
+ columns: PropTypes.arrayOf(PropTypes.shape({
+ id: PropTypes.string.isRequired,
+ title: PropTypes.string,
+ width: PropTypes.string
+ }))
+ },
+
+ getDefaultProps: function () {
+ return {
+ object: null,
+ renderRow: null,
+ provider: ObjectProvider,
+ expandedNodes: new Set(),
+ columns: []
+ };
+ },
+
+ getInitialState: function () {
+ return {
+ expandedNodes: this.props.expandedNodes,
+ columns: ensureDefaultColumn(this.props.columns)
+ };
+ },
+
+ // Node expand/collapse
+
+ toggle: function (nodePath) {
+ let nodes = this.state.expandedNodes;
+ if (this.isExpanded(nodePath)) {
+ nodes.delete(nodePath);
+ } else {
+ nodes.add(nodePath);
+ }
+
+ // Compute new state and update the tree.
+ this.setState(Object.assign({}, this.state, {
+ expandedNodes: nodes
+ }));
+ },
+
+ isExpanded: function (nodePath) {
+ return this.state.expandedNodes.has(nodePath);
+ },
+
+ // Event Handlers
+
+ onClickRow: function (nodePath, event) {
+ event.stopPropagation();
+ this.toggle(nodePath);
+ },
+
+ // Filtering & Sorting
+
+ /**
+ * Filter out nodes that don't correspond to the current filter.
+ * @return {Boolean} true if the node should be visible otherwise false.
+ */
+ onFilter: function (object) {
+ let onFilter = this.props.onFilter;
+ return onFilter ? onFilter(object) : true;
+ },
+
+ onSort: function (parent, children) {
+ let onSort = this.props.onSort;
+ return onSort ? onSort(parent, children) : children;
+ },
+
+ // Members
+
+ /**
+ * Return children node objects (so called 'members') for given
+ * parent object.
+ */
+ getMembers: function (parent, level, path) {
+ // Strings don't have children. Note that 'long' strings are using
+ // the expander icon (+/-) to display the entire original value,
+ // but there are no child items.
+ if (typeof parent == "string") {
+ return [];
+ }
+
+ let provider = this.props.provider;
+ let children = provider.getChildren(parent) || [];
+
+ // If the return value is non-array, the children
+ // are being loaded asynchronously.
+ if (!Array.isArray(children)) {
+ return children;
+ }
+
+ children = this.onSort(parent, children) || children;
+
+ return children.map(child => {
+ let key = provider.getKey(child);
+ let nodePath = path + "/" + key;
+ let type = provider.getType(child);
+ let hasChildren = provider.hasChildren(child);
+
+ // Value with no column specified is used for optimization.
+ // The row is re-rendered only if this value changes.
+ // Value for actual column is get when a cell is rendered.
+ let value = provider.getValue(child);
+
+ if (isLongString(value)) {
+ hasChildren = true;
+ }
+
+ // Return value is a 'member' object containing meta-data about
+ // tree node. It describes node label, value, type, etc.
+ return {
+ // An object associated with this node.
+ object: child,
+ // A label for the child node
+ name: provider.getLabel(child),
+ // Data type of the child node (used for CSS customization)
+ type: type,
+ // Class attribute computed from the type.
+ rowClass: "treeRow-" + type,
+ // Level of the child within the hierarchy (top == 0)
+ level: level,
+ // True if this node has children.
+ hasChildren: hasChildren,
+ // Value associated with this node (as provided by the data provider)
+ value: value,
+ // True if the node is expanded.
+ open: this.isExpanded(nodePath),
+ // Node path
+ path: nodePath,
+ // True if the node is hidden (used for filtering)
+ hidden: !this.onFilter(child)
+ };
+ });
+ },
+
+ /**
+ * Render tree rows/nodes.
+ */
+ renderRows: function (parent, level = 0, path = "") {
+ let rows = [];
+ let decorator = this.props.decorator;
+ let renderRow = this.props.renderRow || TreeRow;
+
+ // Get children for given parent node, iterate over them and render
+ // a row for every one. Use row template (a component) from properties.
+ // If the return value is non-array, the children are being loaded
+ // asynchronously.
+ let members = this.getMembers(parent, level, path);
+ if (!Array.isArray(members)) {
+ return members;
+ }
+
+ members.forEach(member => {
+ if (decorator && decorator.renderRow) {
+ renderRow = decorator.renderRow(member.object) || renderRow;
+ }
+
+ let props = Object.assign({}, this.props, {
+ key: member.path,
+ member: member,
+ columns: this.state.columns,
+ onClick: this.onClickRow.bind(this, member.path)
+ });
+
+ // Render single row.
+ rows.push(renderRow(props));
+
+ // If a child node is expanded render its rows too.
+ if (member.hasChildren && member.open) {
+ let childRows = this.renderRows(member.object, level + 1,
+ member.path);
+
+ // If children needs to be asynchronously fetched first,
+ // set 'loading' property to the parent row. Otherwise
+ // just append children rows to the array of all rows.
+ if (!Array.isArray(childRows)) {
+ let lastIndex = rows.length - 1;
+ props.member.loading = true;
+ rows[lastIndex] = React.cloneElement(rows[lastIndex], props);
+ } else {
+ rows = rows.concat(childRows);
+ }
+ }
+ });
+
+ return rows;
+ },
+
+ render: function () {
+ let root = this.props.object;
+ let classNames = ["treeTable"];
+
+ // Use custom class name from props.
+ let className = this.props.className;
+ if (className) {
+ classNames.push(...className.split(" "));
+ }
+
+ // Alright, let's render all tree rows. The tree is one big <table>.
+ let rows = this.renderRows(root, 0, "");
+
+ // This happens when the view needs to do initial asynchronous
+ // fetch for the root object. The tree might provide a hook API
+ // for rendering animated spinner (just like for tree nodes).
+ if (!Array.isArray(rows)) {
+ rows = [];
+ }
+
+ let props = Object.assign({}, this.props, {
+ columns: this.state.columns
+ });
+
+ return (
+ DOM.table({
+ className: classNames.join(" "),
+ cellPadding: 0,
+ cellSpacing: 0},
+ TreeHeader(props),
+ DOM.tbody({},
+ rows
+ )
+ )
+ );
+ }
+ });
+
+ // Helpers
+
+ /**
+ * There should always be at least one column (the one with toggle buttons)
+ * and this function ensures that it's true.
+ */
+ function ensureDefaultColumn(columns) {
+ if (!columns) {
+ columns = [];
+ }
+
+ let defaultColumn = columns.filter(col => col.id == "default");
+ if (defaultColumn.length) {
+ return columns;
+ }
+
+ // The default column is usually the first one.
+ return [{id: "default"}, ...columns];
+ }
+
+ function isLongString(value) {
+ return typeof value == "string" && value.length > 50;
+ }
+
+ // Exports from this module
+ module.exports = TreeView;
+});
diff --git a/devtools/client/shared/css-angle.js b/devtools/client/shared/css-angle.js
new file mode 100644
index 000000000..f3612ed84
--- /dev/null
+++ b/devtools/client/shared/css-angle.js
@@ -0,0 +1,345 @@
+/* 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 {CSS_ANGLEUNIT} = require("devtools/shared/css/properties-db");
+
+const SPECIALVALUES = new Set([
+ "initial",
+ "inherit",
+ "unset"
+]);
+
+const {getCSSLexer} = require("devtools/shared/css/lexer");
+
+/**
+ * This module is used to convert between various angle units.
+ *
+ * Usage:
+ * let {angleUtils} = require("devtools/client/shared/css-angle");
+ * let angle = new angleUtils.CssAngle("180deg");
+ *
+ * angle.authored === "180deg"
+ * angle.valid === true
+ * angle.rad === "3,14rad"
+ * angle.grad === "200grad"
+ * angle.turn === "0.5turn"
+ *
+ * angle.toString() === "180deg"; // Outputs the angle value and its unit
+ * // Angle objects can be reused
+ * angle.newAngle("-1TURN") === "-1TURN"; // true
+ */
+
+function CssAngle(angleValue) {
+ this.newAngle(angleValue);
+}
+
+module.exports.angleUtils = {
+ CssAngle: CssAngle,
+ classifyAngle: classifyAngle
+};
+
+CssAngle.ANGLEUNIT = CSS_ANGLEUNIT;
+
+CssAngle.prototype = {
+ _angleUnit: null,
+ _angleUnitUppercase: false,
+
+ // The value as-authored.
+ authored: null,
+ // A lower-cased copy of |authored|.
+ lowerCased: null,
+
+ get angleUnit() {
+ if (this._angleUnit === null) {
+ this._angleUnit = classifyAngle(this.authored);
+ }
+ return this._angleUnit;
+ },
+
+ set angleUnit(unit) {
+ this._angleUnit = unit;
+ },
+
+ get valid() {
+ let token = getCSSLexer(this.authored).nextToken();
+ if (!token) {
+ return false;
+ }
+ return (token.tokenType === "dimension"
+ && token.text.toLowerCase() in CssAngle.ANGLEUNIT);
+ },
+
+ get specialValue() {
+ return SPECIALVALUES.has(this.lowerCased) ? this.authored : null;
+ },
+
+ get deg() {
+ let invalidOrSpecialValue = this._getInvalidOrSpecialValue();
+ if (invalidOrSpecialValue !== false) {
+ return invalidOrSpecialValue;
+ }
+
+ let angleUnit = classifyAngle(this.authored);
+ if (angleUnit === CssAngle.ANGLEUNIT.deg) {
+ // The angle is valid and is in degree.
+ return this.authored;
+ }
+
+ let degValue;
+ if (angleUnit === CssAngle.ANGLEUNIT.rad) {
+ // The angle is valid and is in radian.
+ degValue = this.authoredAngleValue / (Math.PI / 180);
+ }
+
+ if (angleUnit === CssAngle.ANGLEUNIT.grad) {
+ // The angle is valid and is in gradian.
+ degValue = this.authoredAngleValue * 0.9;
+ }
+
+ if (angleUnit === CssAngle.ANGLEUNIT.turn) {
+ // The angle is valid and is in turn.
+ degValue = this.authoredAngleValue * 360;
+ }
+
+ let unitStr = CssAngle.ANGLEUNIT.deg;
+ if (this._angleUnitUppercase === true) {
+ unitStr = unitStr.toUpperCase();
+ }
+ return `${Math.round(degValue * 100) / 100}${unitStr}`;
+ },
+
+ get rad() {
+ let invalidOrSpecialValue = this._getInvalidOrSpecialValue();
+ if (invalidOrSpecialValue !== false) {
+ return invalidOrSpecialValue;
+ }
+
+ let unit = classifyAngle(this.authored);
+ if (unit === CssAngle.ANGLEUNIT.rad) {
+ // The angle is valid and is in radian.
+ return this.authored;
+ }
+
+ let radValue;
+ if (unit === CssAngle.ANGLEUNIT.deg) {
+ // The angle is valid and is in degree.
+ radValue = this.authoredAngleValue * (Math.PI / 180);
+ }
+
+ if (unit === CssAngle.ANGLEUNIT.grad) {
+ // The angle is valid and is in gradian.
+ radValue = this.authoredAngleValue * 0.9 * (Math.PI / 180);
+ }
+
+ if (unit === CssAngle.ANGLEUNIT.turn) {
+ // The angle is valid and is in turn.
+ radValue = this.authoredAngleValue * 360 * (Math.PI / 180);
+ }
+
+ let unitStr = CssAngle.ANGLEUNIT.rad;
+ if (this._angleUnitUppercase === true) {
+ unitStr = unitStr.toUpperCase();
+ }
+ return `${Math.round(radValue * 10000) / 10000}${unitStr}`;
+ },
+
+ get grad() {
+ let invalidOrSpecialValue = this._getInvalidOrSpecialValue();
+ if (invalidOrSpecialValue !== false) {
+ return invalidOrSpecialValue;
+ }
+
+ let unit = classifyAngle(this.authored);
+ if (unit === CssAngle.ANGLEUNIT.grad) {
+ // The angle is valid and is in gradian
+ return this.authored;
+ }
+
+ let gradValue;
+ if (unit === CssAngle.ANGLEUNIT.deg) {
+ // The angle is valid and is in degree
+ gradValue = this.authoredAngleValue / 0.9;
+ }
+
+ if (unit === CssAngle.ANGLEUNIT.rad) {
+ // The angle is valid and is in radian
+ gradValue = this.authoredAngleValue / 0.9 / (Math.PI / 180);
+ }
+
+ if (unit === CssAngle.ANGLEUNIT.turn) {
+ // The angle is valid and is in turn
+ gradValue = this.authoredAngleValue * 400;
+ }
+
+ let unitStr = CssAngle.ANGLEUNIT.grad;
+ if (this._angleUnitUppercase === true) {
+ unitStr = unitStr.toUpperCase();
+ }
+ return `${Math.round(gradValue * 100) / 100}${unitStr}`;
+ },
+
+ get turn() {
+ let invalidOrSpecialValue = this._getInvalidOrSpecialValue();
+ if (invalidOrSpecialValue !== false) {
+ return invalidOrSpecialValue;
+ }
+
+ let unit = classifyAngle(this.authored);
+ if (unit === CssAngle.ANGLEUNIT.turn) {
+ // The angle is valid and is in turn
+ return this.authored;
+ }
+
+ let turnValue;
+ if (unit === CssAngle.ANGLEUNIT.deg) {
+ // The angle is valid and is in degree
+ turnValue = this.authoredAngleValue / 360;
+ }
+
+ if (unit === CssAngle.ANGLEUNIT.rad) {
+ // The angle is valid and is in radian
+ turnValue = (this.authoredAngleValue / (Math.PI / 180)) / 360;
+ }
+
+ if (unit === CssAngle.ANGLEUNIT.grad) {
+ // The angle is valid and is in gradian
+ turnValue = this.authoredAngleValue / 400;
+ }
+
+ let unitStr = CssAngle.ANGLEUNIT.turn;
+ if (this._angleUnitUppercase === true) {
+ unitStr = unitStr.toUpperCase();
+ }
+ return `${Math.round(turnValue * 100) / 100}${unitStr}`;
+ },
+
+ /**
+ * Check whether the angle value is in the special list e.g.
+ * inherit or invalid.
+ *
+ * @return {String|Boolean}
+ * - If the current angle is a special value e.g. "inherit" then
+ * return the angle.
+ * - If the angle is invalid return an empty string.
+ * - If the angle is a regular angle e.g. 90deg so we return false
+ * to indicate that the angle is neither invalid nor special.
+ */
+ _getInvalidOrSpecialValue: function () {
+ if (this.specialValue) {
+ return this.specialValue;
+ }
+ if (!this.valid) {
+ return "";
+ }
+ return false;
+ },
+
+ /**
+ * Change angle
+ *
+ * @param {String} angle
+ * Any valid angle value + unit string
+ */
+ newAngle: function (angle) {
+ // Store a lower-cased version of the angle to help with format
+ // testing. The original text is kept as well so it can be
+ // returned when needed.
+ this.lowerCased = angle.toLowerCase();
+ this._angleUnitUppercase = (angle === angle.toUpperCase());
+ this.authored = angle;
+
+ let reg = new RegExp(
+ `(${Object.keys(CssAngle.ANGLEUNIT).join("|")})$`, "i");
+ let unitStartIdx = angle.search(reg);
+ this.authoredAngleValue = angle.substring(0, unitStartIdx);
+ this.authoredAngleUnit = angle.substring(unitStartIdx, angle.length);
+
+ return this;
+ },
+
+ nextAngleUnit: function () {
+ // Get a reordered array from the formats object
+ // to have the current format at the front so we can cycle through.
+ let formats = Object.keys(CssAngle.ANGLEUNIT);
+ let putOnEnd = formats.splice(0, formats.indexOf(this.angleUnit));
+ formats = formats.concat(putOnEnd);
+ let currentDisplayedValue = this[formats[0]];
+
+ for (let format of formats) {
+ if (this[format].toLowerCase() !== currentDisplayedValue.toLowerCase()) {
+ this.angleUnit = CssAngle.ANGLEUNIT[format];
+ break;
+ }
+ }
+ return this.toString();
+ },
+
+ /**
+ * Return a string representing a angle
+ */
+ toString: function () {
+ let angle;
+
+ switch (this.angleUnit) {
+ case CssAngle.ANGLEUNIT.deg:
+ angle = this.deg;
+ break;
+ case CssAngle.ANGLEUNIT.rad:
+ angle = this.rad;
+ break;
+ case CssAngle.ANGLEUNIT.grad:
+ angle = this.grad;
+ break;
+ case CssAngle.ANGLEUNIT.turn:
+ angle = this.turn;
+ break;
+ default:
+ angle = this.deg;
+ }
+
+ if (this._angleUnitUppercase &&
+ this.angleUnit != CssAngle.ANGLEUNIT.authored) {
+ angle = angle.toUpperCase();
+ }
+ return angle;
+ },
+
+ /**
+ * This method allows comparison of CssAngle objects using ===.
+ */
+ valueOf: function () {
+ return this.deg;
+ },
+};
+
+/**
+ * Given a color, classify its type as one of the possible angle
+ * units, as known by |CssAngle.angleUnit|.
+ *
+ * @param {String} value
+ * The angle, in any form accepted by CSS.
+ * @return {String}
+ * The angle classification, one of "deg", "rad", "grad", or "turn".
+ */
+function classifyAngle(value) {
+ value = value.toLowerCase();
+ if (value.endsWith("deg")) {
+ return CssAngle.ANGLEUNIT.deg;
+ }
+
+ if (value.endsWith("grad")) {
+ return CssAngle.ANGLEUNIT.grad;
+ }
+
+ if (value.endsWith("rad")) {
+ return CssAngle.ANGLEUNIT.rad;
+ }
+ if (value.endsWith("turn")) {
+ return CssAngle.ANGLEUNIT.turn;
+ }
+
+ return CssAngle.ANGLEUNIT.deg;
+}
diff --git a/devtools/client/shared/css-reload.js b/devtools/client/shared/css-reload.js
new file mode 100644
index 000000000..de82c6c5f
--- /dev/null
+++ b/devtools/client/shared/css-reload.js
@@ -0,0 +1,142 @@
+/* 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 { Services } = require("resource://gre/modules/Services.jsm");
+const { getTheme } = require("devtools/client/shared/theme");
+
+function iterStyleNodes(window, func) {
+ for (let node of window.document.childNodes) {
+ // Look for ProcessingInstruction nodes.
+ if (node.nodeType === 7) {
+ func(node);
+ }
+ }
+
+ const links = window.document.getElementsByTagNameNS(
+ "http://www.w3.org/1999/xhtml", "link"
+ );
+ for (let node of links) {
+ func(node);
+ }
+}
+
+function replaceCSS(window, fileURI) {
+ const document = window.document;
+ const randomKey = Math.random();
+ Services.obs.notifyObservers(null, "startupcache-invalidate", null);
+
+ // Scan every CSS tag and reload ones that match the file we are
+ // looking for.
+ iterStyleNodes(window, node => {
+ if (node.nodeType === 7) {
+ // xml-stylesheet declaration
+ if (node.data.includes(fileURI)) {
+ const newNode = window.document.createProcessingInstruction(
+ "xml-stylesheet",
+ `href="${fileURI}?s=${randomKey}" type="text/css"`
+ );
+ document.insertBefore(newNode, node);
+ document.removeChild(node);
+ }
+ } else if (node.href.includes(fileURI)) {
+ const parentNode = node.parentNode;
+ const newNode = window.document.createElementNS(
+ "http://www.w3.org/1999/xhtml",
+ "link"
+ );
+ newNode.rel = "stylesheet";
+ newNode.type = "text/css";
+ newNode.href = fileURI + "?s=" + randomKey;
+
+ parentNode.insertBefore(newNode, node);
+ parentNode.removeChild(node);
+ }
+ });
+}
+
+function _replaceResourceInSheet(sheet, filename, randomKey) {
+ for (let i = 0; i < sheet.cssRules.length; i++) {
+ const rule = sheet.cssRules[i];
+ if (rule.type === rule.IMPORT_RULE) {
+ _replaceResourceInSheet(rule.styleSheet, filename);
+ } else if (rule.cssText.includes(filename)) {
+ // Strip off any existing query strings. This might lose
+ // updates for files if there are multiple resources
+ // referenced in the same rule, but the chances of someone hot
+ // reloading multiple resources in the same rule is very low.
+ const text = rule.cssText.replace(/\?s=0.\d+/g, "");
+ const newRule = (
+ text.replace(filename, filename + "?s=" + randomKey)
+ );
+
+ sheet.deleteRule(i);
+ sheet.insertRule(newRule, i);
+ }
+ }
+}
+
+function replaceCSSResource(window, fileURI) {
+ const document = window.document;
+ const randomKey = Math.random();
+
+ // Only match the filename. False positives are much better than
+ // missing updates, as all that would happen is we reload more
+ // resources than we need. We do this because many resources only
+ // use relative paths.
+ const parts = fileURI.split("/");
+ const file = parts[parts.length - 1];
+
+ // Scan every single rule in the entire page for any reference to
+ // this resource, and re-insert the rule to force it to update.
+ for (let sheet of document.styleSheets) {
+ _replaceResourceInSheet(sheet, file, randomKey);
+ }
+
+ for (let node of document.querySelectorAll("img,image")) {
+ if (node.src.startsWith(fileURI)) {
+ node.src = fileURI + "?s=" + randomKey;
+ }
+ }
+}
+
+function watchCSS(window) {
+ if (Services.prefs.getBoolPref("devtools.loader.hotreload")) {
+ const watcher = require("devtools/client/shared/devtools-file-watcher");
+
+ function onFileChanged(_, relativePath) {
+ if (relativePath.match(/\.css$/)) {
+ if (relativePath.startsWith("client/themes")) {
+ let path = relativePath.replace(/^client\/themes\//, "");
+
+ // Special-case a few files that get imported from other CSS
+ // files. We just manually hot reload the parent CSS file.
+ if (path === "variables.css" || path === "toolbars.css" ||
+ path === "common.css" || path === "splitters.css") {
+ replaceCSS(window, "chrome://devtools/skin/" + getTheme() + "-theme.css");
+ } else {
+ replaceCSS(window, "chrome://devtools/skin/" + path);
+ }
+ return;
+ }
+
+ replaceCSS(
+ window,
+ "chrome://devtools/content/" + relativePath.replace(/^client\//, "")
+ );
+ replaceCSS(window, "resource://devtools/" + relativePath);
+ } else if (relativePath.match(/\.(svg|png)$/)) {
+ relativePath = relativePath.replace(/^client\/themes\//, "");
+ replaceCSSResource(window, "chrome://devtools/skin/" + relativePath);
+ }
+ }
+ watcher.on("file-changed", onFileChanged);
+
+ window.addEventListener("unload", () => {
+ watcher.off("file-changed", onFileChanged);
+ });
+ }
+}
+
+module.exports = { watchCSS };
diff --git a/devtools/client/shared/curl.js b/devtools/client/shared/curl.js
new file mode 100644
index 000000000..978cbad9c
--- /dev/null
+++ b/devtools/client/shared/curl.js
@@ -0,0 +1,401 @@
+/* -*- 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/. */
+
+/*
+ * Copyright (C) 2007, 2008 Apple Inc. All rights reserved.
+ * Copyright (C) 2008, 2009 Anthony Ricaud <rik@webkit.org>
+ * Copyright (C) 2011 Google Inc. All rights reserved.
+ * Copyright (C) 2009 Mozilla Foundation. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+ * its contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+"use strict";
+
+const Services = require("Services");
+
+const DEFAULT_HTTP_VERSION = "HTTP/1.1";
+
+const Curl = {
+ /**
+ * Generates a cURL command string which can be used from the command line etc.
+ *
+ * @param object data
+ * Datasource to create the command from.
+ * The object must contain the following properties:
+ * - url:string, the URL of the request.
+ * - method:string, the request method upper cased. HEAD / GET / POST etc.
+ * - headers:array, an array of request headers {name:x, value:x} tuples.
+ * - httpVersion:string, http protocol version rfc2616 formatted. Eg. "HTTP/1.1"
+ * - postDataText:string, optional - the request payload.
+ *
+ * @return string
+ * A cURL command.
+ */
+ generateCommand: function (data) {
+ let utils = CurlUtils;
+
+ let command = ["curl"];
+ let ignoredHeaders = new Set();
+
+ // The cURL command is expected to run on the same platform that Firefox runs
+ // (it may be different from the inspected page platform).
+ let escapeString = Services.appinfo.OS == "WINNT" ?
+ utils.escapeStringWin : utils.escapeStringPosix;
+
+ // Add URL.
+ command.push(escapeString(data.url));
+
+ let postDataText = null;
+ let multipartRequest = utils.isMultipartRequest(data);
+
+ // Create post data.
+ let postData = [];
+ if (utils.isUrlEncodedRequest(data) || data.method == "PUT") {
+ postDataText = data.postDataText;
+ postData.push("--data");
+ postData.push(escapeString(utils.writePostDataTextParams(postDataText)));
+ ignoredHeaders.add("Content-Length");
+ } else if (multipartRequest) {
+ postDataText = data.postDataText;
+ postData.push("--data-binary");
+ let boundary = utils.getMultipartBoundary(data);
+ let text = utils.removeBinaryDataFromMultipartText(postDataText, boundary);
+ postData.push(escapeString(text));
+ ignoredHeaders.add("Content-Length");
+ }
+
+ // Add method.
+ // For GET and POST requests this is not necessary as GET is the
+ // default. If --data or --binary is added POST is the default.
+ if (!(data.method == "GET" || data.method == "POST")) {
+ command.push("-X");
+ command.push(data.method);
+ }
+
+ // Add -I (HEAD)
+ // For servers that supports HEAD.
+ // This will fetch the header of a document only.
+ if (data.method == "HEAD") {
+ command.push("-I");
+ }
+
+ // Add http version.
+ if (data.httpVersion && data.httpVersion != DEFAULT_HTTP_VERSION) {
+ command.push("--" + data.httpVersion.split("/")[1]);
+ }
+
+ // Add request headers.
+ let headers = data.headers;
+ if (multipartRequest) {
+ let multipartHeaders = utils.getHeadersFromMultipartText(postDataText);
+ headers = headers.concat(multipartHeaders);
+ }
+ for (let i = 0; i < headers.length; i++) {
+ let header = headers[i];
+ if (header.name === "Accept-Encoding") {
+ command.push("--compressed");
+ continue;
+ }
+ if (ignoredHeaders.has(header.name)) {
+ continue;
+ }
+ command.push("-H");
+ command.push(escapeString(header.name + ": " + header.value));
+ }
+
+ // Add post data.
+ command = command.concat(postData);
+
+ return command.join(" ");
+ }
+};
+
+exports.Curl = Curl;
+
+/**
+ * Utility functions for the Curl command generator.
+ */
+const CurlUtils = {
+ /**
+ * Check if the request is an URL encoded request.
+ *
+ * @param object data
+ * The data source. See the description in the Curl object.
+ * @return boolean
+ * True if the request is URL encoded, false otherwise.
+ */
+ isUrlEncodedRequest: function (data) {
+ let postDataText = data.postDataText;
+ if (!postDataText) {
+ return false;
+ }
+
+ postDataText = postDataText.toLowerCase();
+ if (postDataText.includes("content-type: application/x-www-form-urlencoded")) {
+ return true;
+ }
+
+ let contentType = this.findHeader(data.headers, "content-type");
+
+ return (contentType &&
+ contentType.toLowerCase().includes("application/x-www-form-urlencoded"));
+ },
+
+ /**
+ * Check if the request is a multipart request.
+ *
+ * @param object data
+ * The data source.
+ * @return boolean
+ * True if the request is multipart reqeust, false otherwise.
+ */
+ isMultipartRequest: function (data) {
+ let postDataText = data.postDataText;
+ if (!postDataText) {
+ return false;
+ }
+
+ postDataText = postDataText.toLowerCase();
+ if (postDataText.includes("content-type: multipart/form-data")) {
+ return true;
+ }
+
+ let contentType = this.findHeader(data.headers, "content-type");
+
+ return (contentType &&
+ contentType.toLowerCase().includes("multipart/form-data;"));
+ },
+
+ /**
+ * Write out paramters from post data text.
+ *
+ * @param object postDataText
+ * Post data text.
+ * @return string
+ * Post data parameters.
+ */
+ writePostDataTextParams: function (postDataText) {
+ let lines = postDataText.split("\r\n");
+ return lines[lines.length - 1];
+ },
+
+ /**
+ * Finds the header with the given name in the headers array.
+ *
+ * @param array headers
+ * Array of headers info {name:x, value:x}.
+ * @param string name
+ * The header name to find.
+ * @return string
+ * The found header value or null if not found.
+ */
+ findHeader: function (headers, name) {
+ if (!headers) {
+ return null;
+ }
+
+ name = name.toLowerCase();
+ for (let header of headers) {
+ if (name == header.name.toLowerCase()) {
+ return header.value;
+ }
+ }
+
+ return null;
+ },
+
+ /**
+ * Returns the boundary string for a multipart request.
+ *
+ * @param string data
+ * The data source. See the description in the Curl object.
+ * @return string
+ * The boundary string for the request.
+ */
+ getMultipartBoundary: function (data) {
+ let boundaryRe = /\bboundary=(-{3,}\w+)/i;
+
+ // Get the boundary string from the Content-Type request header.
+ let contentType = this.findHeader(data.headers, "Content-Type");
+ if (boundaryRe.test(contentType)) {
+ return contentType.match(boundaryRe)[1];
+ }
+ // Temporary workaround. As of 2014-03-11 the requestHeaders array does not
+ // always contain the Content-Type header for mulitpart requests. See bug 978144.
+ // Find the header from the request payload.
+ let boundaryString = data.postDataText.match(boundaryRe)[1];
+ if (boundaryString) {
+ return boundaryString;
+ }
+
+ return null;
+ },
+
+ /**
+ * Removes the binary data from multipart text.
+ *
+ * @param string multipartText
+ * Multipart form data text.
+ * @param string boundary
+ * The boundary string.
+ * @return string
+ * The multipart text without the binary data.
+ */
+ removeBinaryDataFromMultipartText: function (multipartText, boundary) {
+ let result = "";
+ boundary = "--" + boundary;
+ let parts = multipartText.split(boundary);
+ for (let part of parts) {
+ // Each part is expected to have a content disposition line.
+ let contentDispositionLine = part.trimLeft().split("\r\n")[0];
+ if (!contentDispositionLine) {
+ continue;
+ }
+ contentDispositionLine = contentDispositionLine.toLowerCase();
+ if (contentDispositionLine.includes("content-disposition: form-data")) {
+ if (contentDispositionLine.includes("filename=")) {
+ // The header lines and the binary blob is separated by 2 CRLF's.
+ // Add only the headers to the result.
+ let headers = part.split("\r\n\r\n")[0];
+ result += boundary + "\r\n" + headers + "\r\n\r\n";
+ } else {
+ result += boundary + "\r\n" + part;
+ }
+ }
+ }
+ result += boundary + "--\r\n";
+
+ return result;
+ },
+
+ /**
+ * Get the headers from a multipart post data text.
+ *
+ * @param string multipartText
+ * Multipart post text.
+ * @return array
+ * An array of header objects {name:x, value:x}
+ */
+ getHeadersFromMultipartText: function (multipartText) {
+ let headers = [];
+ if (!multipartText || multipartText.startsWith("---")) {
+ return headers;
+ }
+
+ // Get the header section.
+ let index = multipartText.indexOf("\r\n\r\n");
+ if (index == -1) {
+ return headers;
+ }
+
+ // Parse the header lines.
+ let headersText = multipartText.substring(0, index);
+ let headerLines = headersText.split("\r\n");
+ let lastHeaderName = null;
+
+ for (let line of headerLines) {
+ // Create a header for each line in fields that spans across multiple lines.
+ // Subsquent lines always begins with at least one space or tab character.
+ // (rfc2616)
+ if (lastHeaderName && /^\s+/.test(line)) {
+ headers.push({ name: lastHeaderName, value: line.trim() });
+ continue;
+ }
+
+ let indexOfColon = line.indexOf(":");
+ if (indexOfColon == -1) {
+ continue;
+ }
+
+ let header = [line.slice(0, indexOfColon), line.slice(indexOfColon + 1)];
+ if (header.length != 2) {
+ continue;
+ }
+ lastHeaderName = header[0].trim();
+ headers.push({ name: lastHeaderName, value: header[1].trim() });
+ }
+
+ return headers;
+ },
+
+ /**
+ * Escape util function for POSIX oriented operating systems.
+ * Credit: Google DevTools
+ */
+ escapeStringPosix: function (str) {
+ function escapeCharacter(x) {
+ let code = x.charCodeAt(0);
+ if (code < 256) {
+ // Add leading zero when needed to not care about the next character.
+ return code < 16 ? "\\x0" + code.toString(16) : "\\x" + code.toString(16);
+ }
+ code = code.toString(16);
+ return "\\u" + ("0000" + code).substr(code.length, 4);
+ }
+
+ if (/[^\x20-\x7E]|\'/.test(str)) {
+ // Use ANSI-C quoting syntax.
+ return "$\'" + str.replace(/\\/g, "\\\\")
+ .replace(/\'/g, "\\\'")
+ .replace(/\n/g, "\\n")
+ .replace(/\r/g, "\\r")
+ .replace(/[^\x20-\x7E]/g, escapeCharacter) + "'";
+ }
+
+ // Use single quote syntax.
+ return "'" + str + "'";
+ },
+
+ /**
+ * Escape util function for Windows systems.
+ * Credit: Google DevTools
+ */
+ escapeStringWin: function (str) {
+ /* Replace quote by double quote (but not by \") because it is
+ recognized by both cmd.exe and MS Crt arguments parser.
+
+ Replace % by "%" because it could be expanded to an environment
+ variable value. So %% becomes "%""%". Even if an env variable ""
+ (2 doublequotes) is declared, the cmd.exe will not
+ substitute it with its value.
+
+ Replace each backslash with double backslash to make sure
+ MS Crt arguments parser won't collapse them.
+
+ Replace new line outside of quotes since cmd.exe doesn't let
+ to do it inside.
+ */
+ return "\"" + str.replace(/"/g, "\"\"")
+ .replace(/%/g, "\"%\"")
+ .replace(/\\/g, "\\\\")
+ .replace(/[\r\n]+/g, "\"^$&\"") + "\"";
+ }
+};
+
+exports.CurlUtils = CurlUtils;
diff --git a/devtools/client/shared/demangle.js b/devtools/client/shared/demangle.js
new file mode 100644
index 000000000..e6792cceb
--- /dev/null
+++ b/devtools/client/shared/demangle.js
@@ -0,0 +1,64 @@
+/**
+ * Exposes a function that demangles function names.
+ * Can be found at: https://github.com/kripken/cxx_demangle
+ */
+var demangle = (function() {
+ // In Firefox CommonJS environment, the module boilerplate thinks it's node,
+ // but `process` does not exist.
+ if (typeof process !== "object" && typeof window !== "object" && typeof require === "function") {
+ // null out `require` since no filesystem is necessary in this module, and this
+ // way the boilerplate thinks its in a shell.
+ require = null;
+ // The `print` function only exists in scope when in a node environment,
+ // and there doesn't seem to account for when in a shell environment and NOT node.js,
+ // so just stub out the print function.
+ var print = function(){}
+ }
+
+var Module = function(Module) {
+ Module = Module || {};
+
+var Module;if(!Module)Module=(typeof Module!=="undefined"?Module:null)||{};var moduleOverrides={};for(var key in Module){if(Module.hasOwnProperty(key)){moduleOverrides[key]=Module[key]}}var ENVIRONMENT_IS_WEB=typeof window==="object";var ENVIRONMENT_IS_WORKER=typeof importScripts==="function";var ENVIRONMENT_IS_NODE=typeof process==="object"&&typeof require==="function"&&!ENVIRONMENT_IS_WEB&&!ENVIRONMENT_IS_WORKER;var ENVIRONMENT_IS_SHELL=!ENVIRONMENT_IS_WEB&&!ENVIRONMENT_IS_NODE&&!ENVIRONMENT_IS_WORKER;if(ENVIRONMENT_IS_NODE){if(!Module["print"])Module["print"]=function print(x){process["stdout"].write(x+"\n")};if(!Module["printErr"])Module["printErr"]=function printErr(x){process["stderr"].write(x+"\n")};var nodeFS=require("fs");var nodePath=require("path");Module["read"]=function read(filename,binary){filename=nodePath["normalize"](filename);var ret=nodeFS["readFileSync"](filename);if(!ret&&filename!=nodePath["resolve"](filename)){filename=path.join(__dirname,"..","src",filename);ret=nodeFS["readFileSync"](filename)}if(ret&&!binary)ret=ret.toString();return ret};Module["readBinary"]=function readBinary(filename){var ret=Module["read"](filename,true);if(!ret.buffer){ret=new Uint8Array(ret)}assert(ret.buffer);return ret};Module["load"]=function load(f){globalEval(read(f))};if(!Module["thisProgram"]){if(process["argv"].length>1){Module["thisProgram"]=process["argv"][1].replace(/\\/g,"/")}else{Module["thisProgram"]="unknown-program"}}Module["arguments"]=process["argv"].slice(2);if(typeof module!=="undefined"){module["exports"]=Module}process["on"]("uncaughtException",(function(ex){if(!(ex instanceof ExitStatus)){throw ex}}));Module["inspect"]=(function(){return"[Emscripten Module object]"})}else if(ENVIRONMENT_IS_SHELL){if(!Module["print"])Module["print"]=print;if(typeof printErr!="undefined")Module["printErr"]=printErr;if(typeof read!="undefined"){Module["read"]=read}else{Module["read"]=function read(){throw"no read() available (jsc?)"}}Module["readBinary"]=function readBinary(f){if(typeof readbuffer==="function"){return new Uint8Array(readbuffer(f))}var data=read(f,"binary");assert(typeof data==="object");return data};if(typeof scriptArgs!="undefined"){Module["arguments"]=scriptArgs}else if(typeof arguments!="undefined"){Module["arguments"]=arguments}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){Module["read"]=function read(url){var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.send(null);return xhr.responseText};if(typeof arguments!="undefined"){Module["arguments"]=arguments}if(typeof console!=="undefined"){if(!Module["print"])Module["print"]=function print(x){console.log(x)};if(!Module["printErr"])Module["printErr"]=function printErr(x){console.log(x)}}else{var TRY_USE_DUMP=false;if(!Module["print"])Module["print"]=TRY_USE_DUMP&&typeof dump!=="undefined"?(function(x){dump(x)}):(function(x){})}if(ENVIRONMENT_IS_WORKER){Module["load"]=importScripts}if(typeof Module["setWindowTitle"]==="undefined"){Module["setWindowTitle"]=(function(title){document.title=title})}}else{throw"Unknown runtime environment. Where are we?"}function globalEval(x){eval.call(null,x)}if(!Module["load"]&&Module["read"]){Module["load"]=function load(f){globalEval(Module["read"](f))}}if(!Module["print"]){Module["print"]=(function(){})}if(!Module["printErr"]){Module["printErr"]=Module["print"]}if(!Module["arguments"]){Module["arguments"]=[]}if(!Module["thisProgram"]){Module["thisProgram"]="./this.program"}Module.print=Module["print"];Module.printErr=Module["printErr"];Module["preRun"]=[];Module["postRun"]=[];for(var key in moduleOverrides){if(moduleOverrides.hasOwnProperty(key)){Module[key]=moduleOverrides[key]}}var Runtime={setTempRet0:(function(value){tempRet0=value}),getTempRet0:(function(){return tempRet0}),stackSave:(function(){return STACKTOP}),stackRestore:(function(stackTop){STACKTOP=stackTop}),getNativeTypeSize:(function(type){switch(type){case"i1":case"i8":return 1;case"i16":return 2;case"i32":return 4;case"i64":return 8;case"float":return 4;case"double":return 8;default:{if(type[type.length-1]==="*"){return Runtime.QUANTUM_SIZE}else if(type[0]==="i"){var bits=parseInt(type.substr(1));assert(bits%8===0);return bits/8}else{return 0}}}}),getNativeFieldSize:(function(type){return Math.max(Runtime.getNativeTypeSize(type),Runtime.QUANTUM_SIZE)}),STACK_ALIGN:16,prepVararg:(function(ptr,type){if(type==="double"||type==="i64"){if(ptr&7){assert((ptr&7)===4);ptr+=4}}else{assert((ptr&3)===0)}return ptr}),getAlignSize:(function(type,size,vararg){if(!vararg&&(type=="i64"||type=="double"))return 8;if(!type)return Math.min(size,8);return Math.min(size||(type?Runtime.getNativeFieldSize(type):0),Runtime.QUANTUM_SIZE)}),dynCall:(function(sig,ptr,args){if(args&&args.length){if(!args.splice)args=Array.prototype.slice.call(args);args.splice(0,0,ptr);return Module["dynCall_"+sig].apply(null,args)}else{return Module["dynCall_"+sig].call(null,ptr)}}),functionPointers:[],addFunction:(function(func){for(var i=0;i<Runtime.functionPointers.length;i++){if(!Runtime.functionPointers[i]){Runtime.functionPointers[i]=func;return 2*(1+i)}}throw"Finished up all reserved function pointers. Use a higher value for RESERVED_FUNCTION_POINTERS."}),removeFunction:(function(index){Runtime.functionPointers[(index-2)/2]=null}),warnOnce:(function(text){if(!Runtime.warnOnce.shown)Runtime.warnOnce.shown={};if(!Runtime.warnOnce.shown[text]){Runtime.warnOnce.shown[text]=1;Module.printErr(text)}}),funcWrappers:{},getFuncWrapper:(function(func,sig){assert(sig);if(!Runtime.funcWrappers[sig]){Runtime.funcWrappers[sig]={}}var sigCache=Runtime.funcWrappers[sig];if(!sigCache[func]){sigCache[func]=function dynCall_wrapper(){return Runtime.dynCall(sig,func,arguments)}}return sigCache[func]}),getCompilerSetting:(function(name){throw"You must build with -s RETAIN_COMPILER_SETTINGS=1 for Runtime.getCompilerSetting or emscripten_get_compiler_setting to work"}),stackAlloc:(function(size){var ret=STACKTOP;STACKTOP=STACKTOP+size|0;STACKTOP=STACKTOP+15&-16;return ret}),staticAlloc:(function(size){var ret=STATICTOP;STATICTOP=STATICTOP+size|0;STATICTOP=STATICTOP+15&-16;return ret}),dynamicAlloc:(function(size){var ret=DYNAMICTOP;DYNAMICTOP=DYNAMICTOP+size|0;DYNAMICTOP=DYNAMICTOP+15&-16;if(DYNAMICTOP>=TOTAL_MEMORY){var success=enlargeMemory();if(!success){DYNAMICTOP=ret;return 0}}return ret}),alignMemory:(function(size,quantum){var ret=size=Math.ceil(size/(quantum?quantum:16))*(quantum?quantum:16);return ret}),makeBigInt:(function(low,high,unsigned){var ret=unsigned?+(low>>>0)+ +(high>>>0)*+4294967296:+(low>>>0)+ +(high|0)*+4294967296;return ret}),GLOBAL_BASE:8,QUANTUM_SIZE:4,__dummy__:0};var __THREW__=0;var ABORT=false;var EXITSTATUS=0;var undef=0;var tempValue,tempInt,tempBigInt,tempInt2,tempBigInt2,tempPair,tempBigIntI,tempBigIntR,tempBigIntS,tempBigIntP,tempBigIntD,tempDouble,tempFloat;var tempI64,tempI64b;var tempRet0,tempRet1,tempRet2,tempRet3,tempRet4,tempRet5,tempRet6,tempRet7,tempRet8,tempRet9;function assert(condition,text){if(!condition){abort("Assertion failed: "+text)}}var globalScope=this;function getCFunc(ident){var func=Module["_"+ident];if(!func){try{func=eval("_"+ident)}catch(e){}}assert(func,"Cannot call unknown function "+ident+" (perhaps LLVM optimizations or closure removed it?)");return func}var cwrap,ccall;((function(){var JSfuncs={"stackSave":(function(){Runtime.stackSave()}),"stackRestore":(function(){Runtime.stackRestore()}),"arrayToC":(function(arr){var ret=Runtime.stackAlloc(arr.length);writeArrayToMemory(arr,ret);return ret}),"stringToC":(function(str){var ret=0;if(str!==null&&str!==undefined&&str!==0){ret=Runtime.stackAlloc((str.length<<2)+1);writeStringToMemory(str,ret)}return ret})};var toC={"string":JSfuncs["stringToC"],"array":JSfuncs["arrayToC"]};ccall=function ccallFunc(ident,returnType,argTypes,args,opts){var func=getCFunc(ident);var cArgs=[];var stack=0;if(args){for(var i=0;i<args.length;i++){var converter=toC[argTypes[i]];if(converter){if(stack===0)stack=Runtime.stackSave();cArgs[i]=converter(args[i])}else{cArgs[i]=args[i]}}}var ret=func.apply(null,cArgs);if(returnType==="string")ret=Pointer_stringify(ret);if(stack!==0){if(opts&&opts.async){EmterpreterAsync.asyncFinalizers.push((function(){Runtime.stackRestore(stack)}));return}Runtime.stackRestore(stack)}return ret};var sourceRegex=/^function\s*\(([^)]*)\)\s*{\s*([^*]*?)[\s;]*(?:return\s*(.*?)[;\s]*)?}$/;function parseJSFunc(jsfunc){var parsed=jsfunc.toString().match(sourceRegex).slice(1);return{arguments:parsed[0],body:parsed[1],returnValue:parsed[2]}}var JSsource={};for(var fun in JSfuncs){if(JSfuncs.hasOwnProperty(fun)){JSsource[fun]=parseJSFunc(JSfuncs[fun])}}cwrap=function cwrap(ident,returnType,argTypes){argTypes=argTypes||[];var cfunc=getCFunc(ident);var numericArgs=argTypes.every((function(type){return type==="number"}));var numericRet=returnType!=="string";if(numericRet&&numericArgs){return cfunc}var argNames=argTypes.map((function(x,i){return"$"+i}));var funcstr="(function("+argNames.join(",")+") {";var nargs=argTypes.length;if(!numericArgs){funcstr+="var stack = "+JSsource["stackSave"].body+";";for(var i=0;i<nargs;i++){var arg=argNames[i],type=argTypes[i];if(type==="number")continue;var convertCode=JSsource[type+"ToC"];funcstr+="var "+convertCode.arguments+" = "+arg+";";funcstr+=convertCode.body+";";funcstr+=arg+"=("+convertCode.returnValue+");"}}var cfuncname=parseJSFunc((function(){return cfunc})).returnValue;funcstr+="var ret = "+cfuncname+"("+argNames.join(",")+");";if(!numericRet){var strgfy=parseJSFunc((function(){return Pointer_stringify})).returnValue;funcstr+="ret = "+strgfy+"(ret);"}if(!numericArgs){funcstr+=JSsource["stackRestore"].body.replace("()","(stack)")+";"}funcstr+="return ret})";return eval(funcstr)}}))();function setValue(ptr,value,type,noSafe){type=type||"i8";if(type.charAt(type.length-1)==="*")type="i32";switch(type){case"i1":HEAP8[ptr>>0]=value;break;case"i8":HEAP8[ptr>>0]=value;break;case"i16":HEAP16[ptr>>1]=value;break;case"i32":HEAP32[ptr>>2]=value;break;case"i64":tempI64=[value>>>0,(tempDouble=value,+Math_abs(tempDouble)>=+1?tempDouble>+0?(Math_min(+Math_floor(tempDouble/+4294967296),+4294967295)|0)>>>0:~~+Math_ceil((tempDouble- +(~~tempDouble>>>0))/+4294967296)>>>0:0)],HEAP32[ptr>>2]=tempI64[0],HEAP32[ptr+4>>2]=tempI64[1];break;case"float":HEAPF32[ptr>>2]=value;break;case"double":HEAPF64[ptr>>3]=value;break;default:abort("invalid type for setValue: "+type)}}function getValue(ptr,type,noSafe){type=type||"i8";if(type.charAt(type.length-1)==="*")type="i32";switch(type){case"i1":return HEAP8[ptr>>0];case"i8":return HEAP8[ptr>>0];case"i16":return HEAP16[ptr>>1];case"i32":return HEAP32[ptr>>2];case"i64":return HEAP32[ptr>>2];case"float":return HEAPF32[ptr>>2];case"double":return HEAPF64[ptr>>3];default:abort("invalid type for setValue: "+type)}return null}var ALLOC_NORMAL=0;var ALLOC_STACK=1;var ALLOC_STATIC=2;var ALLOC_DYNAMIC=3;var ALLOC_NONE=4;function allocate(slab,types,allocator,ptr){var zeroinit,size;if(typeof slab==="number"){zeroinit=true;size=slab}else{zeroinit=false;size=slab.length}var singleType=typeof types==="string"?types:null;var ret;if(allocator==ALLOC_NONE){ret=ptr}else{ret=[typeof _malloc==="function"?_malloc:null,Runtime.stackAlloc,Runtime.staticAlloc,Runtime.dynamicAlloc][allocator===undefined?ALLOC_STATIC:allocator](Math.max(size,singleType?1:types.length))}if(zeroinit){var ptr=ret,stop;assert((ret&3)==0);stop=ret+(size&~3);for(;ptr<stop;ptr+=4){HEAP32[ptr>>2]=0}stop=ret+size;while(ptr<stop){HEAP8[ptr++>>0]=0}return ret}if(singleType==="i8"){if(slab.subarray||slab.slice){HEAPU8.set(slab,ret)}else{HEAPU8.set(new Uint8Array(slab),ret)}return ret}var i=0,type,typeSize,previousType;while(i<size){var curr=slab[i];if(typeof curr==="function"){curr=Runtime.getFunctionIndex(curr)}type=singleType||types[i];if(type===0){i++;continue}if(type=="i64")type="i32";setValue(ret+i,curr,type);if(previousType!==type){typeSize=Runtime.getNativeTypeSize(type);previousType=type}i+=typeSize}return ret}function getMemory(size){if(!staticSealed)return Runtime.staticAlloc(size);if(typeof _sbrk!=="undefined"&&!_sbrk.called||!runtimeInitialized)return Runtime.dynamicAlloc(size);return _malloc(size)}function Pointer_stringify(ptr,length){if(length===0||!ptr)return"";var hasUtf=0;var t;var i=0;while(1){t=HEAPU8[ptr+i>>0];hasUtf|=t;if(t==0&&!length)break;i++;if(length&&i==length)break}if(!length)length=i;var ret="";if(hasUtf<128){var MAX_CHUNK=1024;var curr;while(length>0){curr=String.fromCharCode.apply(String,HEAPU8.subarray(ptr,ptr+Math.min(length,MAX_CHUNK)));ret=ret?ret+curr:curr;ptr+=MAX_CHUNK;length-=MAX_CHUNK}return ret}return Module["UTF8ToString"](ptr)}Module["Pointer_stringify"]=Pointer_stringify;function AsciiToString(ptr){var str="";while(1){var ch=HEAP8[ptr++>>0];if(!ch)return str;str+=String.fromCharCode(ch)}}function stringToAscii(str,outPtr){return writeAsciiToMemory(str,outPtr,false)}function UTF8ArrayToString(u8Array,idx){var u0,u1,u2,u3,u4,u5;var str="";while(1){u0=u8Array[idx++];if(!u0)return str;if(!(u0&128)){str+=String.fromCharCode(u0);continue}u1=u8Array[idx++]&63;if((u0&224)==192){str+=String.fromCharCode((u0&31)<<6|u1);continue}u2=u8Array[idx++]&63;if((u0&240)==224){u0=(u0&15)<<12|u1<<6|u2}else{u3=u8Array[idx++]&63;if((u0&248)==240){u0=(u0&7)<<18|u1<<12|u2<<6|u3}else{u4=u8Array[idx++]&63;if((u0&252)==248){u0=(u0&3)<<24|u1<<18|u2<<12|u3<<6|u4}else{u5=u8Array[idx++]&63;u0=(u0&1)<<30|u1<<24|u2<<18|u3<<12|u4<<6|u5}}}if(u0<65536){str+=String.fromCharCode(u0)}else{var ch=u0-65536;str+=String.fromCharCode(55296|ch>>10,56320|ch&1023)}}}function UTF8ToString(ptr){return UTF8ArrayToString(HEAPU8,ptr)}function stringToUTF8Array(str,outU8Array,outIdx,maxBytesToWrite){if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i<str.length;++i){var u=str.charCodeAt(i);if(u>=55296&&u<=57343)u=65536+((u&1023)<<10)|str.charCodeAt(++i)&1023;if(u<=127){if(outIdx>=endIdx)break;outU8Array[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;outU8Array[outIdx++]=192|u>>6;outU8Array[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;outU8Array[outIdx++]=224|u>>12;outU8Array[outIdx++]=128|u>>6&63;outU8Array[outIdx++]=128|u&63}else if(u<=2097151){if(outIdx+3>=endIdx)break;outU8Array[outIdx++]=240|u>>18;outU8Array[outIdx++]=128|u>>12&63;outU8Array[outIdx++]=128|u>>6&63;outU8Array[outIdx++]=128|u&63}else if(u<=67108863){if(outIdx+4>=endIdx)break;outU8Array[outIdx++]=248|u>>24;outU8Array[outIdx++]=128|u>>18&63;outU8Array[outIdx++]=128|u>>12&63;outU8Array[outIdx++]=128|u>>6&63;outU8Array[outIdx++]=128|u&63}else{if(outIdx+5>=endIdx)break;outU8Array[outIdx++]=252|u>>30;outU8Array[outIdx++]=128|u>>24&63;outU8Array[outIdx++]=128|u>>18&63;outU8Array[outIdx++]=128|u>>12&63;outU8Array[outIdx++]=128|u>>6&63;outU8Array[outIdx++]=128|u&63}}outU8Array[outIdx]=0;return outIdx-startIdx}function stringToUTF8(str,outPtr,maxBytesToWrite){return stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite)}function lengthBytesUTF8(str){var len=0;for(var i=0;i<str.length;++i){var u=str.charCodeAt(i);if(u>=55296&&u<=57343)u=65536+((u&1023)<<10)|str.charCodeAt(++i)&1023;if(u<=127){++len}else if(u<=2047){len+=2}else if(u<=65535){len+=3}else if(u<=2097151){len+=4}else if(u<=67108863){len+=5}else{len+=6}}return len}function UTF16ToString(ptr){var i=0;var str="";while(1){var codeUnit=HEAP16[ptr+i*2>>1];if(codeUnit==0)return str;++i;str+=String.fromCharCode(codeUnit)}}function stringToUTF16(str,outPtr,maxBytesToWrite){if(maxBytesToWrite===undefined){maxBytesToWrite=2147483647}if(maxBytesToWrite<2)return 0;maxBytesToWrite-=2;var startPtr=outPtr;var numCharsToWrite=maxBytesToWrite<str.length*2?maxBytesToWrite/2:str.length;for(var i=0;i<numCharsToWrite;++i){var codeUnit=str.charCodeAt(i);HEAP16[outPtr>>1]=codeUnit;outPtr+=2}HEAP16[outPtr>>1]=0;return outPtr-startPtr}function lengthBytesUTF16(str){return str.length*2}function UTF32ToString(ptr){var i=0;var str="";while(1){var utf32=HEAP32[ptr+i*4>>2];if(utf32==0)return str;++i;if(utf32>=65536){var ch=utf32-65536;str+=String.fromCharCode(55296|ch>>10,56320|ch&1023)}else{str+=String.fromCharCode(utf32)}}}function stringToUTF32(str,outPtr,maxBytesToWrite){if(maxBytesToWrite===undefined){maxBytesToWrite=2147483647}if(maxBytesToWrite<4)return 0;var startPtr=outPtr;var endPtr=startPtr+maxBytesToWrite-4;for(var i=0;i<str.length;++i){var codeUnit=str.charCodeAt(i);if(codeUnit>=55296&&codeUnit<=57343){var trailSurrogate=str.charCodeAt(++i);codeUnit=65536+((codeUnit&1023)<<10)|trailSurrogate&1023}HEAP32[outPtr>>2]=codeUnit;outPtr+=4;if(outPtr+4>endPtr)break}HEAP32[outPtr>>2]=0;return outPtr-startPtr}function lengthBytesUTF32(str){var len=0;for(var i=0;i<str.length;++i){var codeUnit=str.charCodeAt(i);if(codeUnit>=55296&&codeUnit<=57343)++i;len+=4}return len}function demangle(func){var hasLibcxxabi=!!Module["___cxa_demangle"];if(hasLibcxxabi){try{var buf=_malloc(func.length);writeStringToMemory(func.substr(1),buf);var status=_malloc(4);var ret=Module["___cxa_demangle"](buf,0,0,status);if(getValue(status,"i32")===0&&ret){return Pointer_stringify(ret)}}catch(e){}finally{if(buf)_free(buf);if(status)_free(status);if(ret)_free(ret)}}var i=3;var basicTypes={"v":"void","b":"bool","c":"char","s":"short","i":"int","l":"long","f":"float","d":"double","w":"wchar_t","a":"signed char","h":"unsigned char","t":"unsigned short","j":"unsigned int","m":"unsigned long","x":"long long","y":"unsigned long long","z":"..."};var subs=[];var first=true;function dump(x){if(x)Module.print(x);Module.print(func);var pre="";for(var a=0;a<i;a++)pre+=" ";Module.print(pre+"^")}function parseNested(){i++;if(func[i]==="K")i++;var parts=[];while(func[i]!=="E"){if(func[i]==="S"){i++;var next=func.indexOf("_",i);var num=func.substring(i,next)||0;parts.push(subs[num]||"?");i=next+1;continue}if(func[i]==="C"){parts.push(parts[parts.length-1]);i+=2;continue}var size=parseInt(func.substr(i));var pre=size.toString().length;if(!size||!pre){i--;break}var curr=func.substr(i+pre,size);parts.push(curr);subs.push(curr);i+=pre+size}i++;return parts}function parse(rawList,limit,allowVoid){limit=limit||Infinity;var ret="",list=[];function flushList(){return"("+list.join(", ")+")"}var name;if(func[i]==="N"){name=parseNested().join("::");limit--;if(limit===0)return rawList?[name]:name}else{if(func[i]==="K"||first&&func[i]==="L")i++;var size=parseInt(func.substr(i));if(size){var pre=size.toString().length;name=func.substr(i+pre,size);i+=pre+size}}first=false;if(func[i]==="I"){i++;var iList=parse(true);var iRet=parse(true,1,true);ret+=iRet[0]+" "+name+"<"+iList.join(", ")+">"}else{ret=name}paramLoop:while(i<func.length&&limit-->0){var c=func[i++];if(c in basicTypes){list.push(basicTypes[c])}else{switch(c){case"P":list.push(parse(true,1,true)[0]+"*");break;case"R":list.push(parse(true,1,true)[0]+"&");break;case"L":{i++;var end=func.indexOf("E",i);var size=end-i;list.push(func.substr(i,size));i+=size+2;break};case"A":{var size=parseInt(func.substr(i));i+=size.toString().length;if(func[i]!=="_")throw"?";i++;list.push(parse(true,1,true)[0]+" ["+size+"]");break};case"E":break paramLoop;default:ret+="?"+c;break paramLoop}}}if(!allowVoid&&list.length===1&&list[0]==="void")list=[];if(rawList){if(ret){list.push(ret+"?")}return list}else{return ret+flushList()}}var parsed=func;try{if(func=="Object._main"||func=="_main"){return"main()"}if(typeof func==="number")func=Pointer_stringify(func);if(func[0]!=="_")return func;if(func[1]!=="_")return func;if(func[2]!=="Z")return func;switch(func[3]){case"n":return"operator new()";case"d":return"operator delete()"}parsed=parse()}catch(e){parsed+="?"}if(parsed.indexOf("?")>=0&&!hasLibcxxabi){Runtime.warnOnce("warning: a problem occurred in builtin C++ name demangling; build with -s DEMANGLE_SUPPORT=1 to link in libcxxabi demangling")}return parsed}function demangleAll(text){return text.replace(/__Z[\w\d_]+/g,(function(x){var y=demangle(x);return x===y?x:x+" ["+y+"]"}))}function jsStackTrace(){var err=new Error;if(!err.stack){try{throw new Error(0)}catch(e){err=e}if(!err.stack){return"(no stack trace available)"}}return err.stack.toString()}function stackTrace(){return demangleAll(jsStackTrace())}var PAGE_SIZE=4096;function alignMemoryPage(x){if(x%4096>0){x+=4096-x%4096}return x}var HEAP;var buffer;var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;function updateGlobalBuffer(buf){Module["buffer"]=buffer=buf}function updateGlobalBufferViews(){Module["HEAP8"]=HEAP8=new Int8Array(buffer);Module["HEAP16"]=HEAP16=new Int16Array(buffer);Module["HEAP32"]=HEAP32=new Int32Array(buffer);Module["HEAPU8"]=HEAPU8=new Uint8Array(buffer);Module["HEAPU16"]=HEAPU16=new Uint16Array(buffer);Module["HEAPU32"]=HEAPU32=new Uint32Array(buffer);Module["HEAPF32"]=HEAPF32=new Float32Array(buffer);Module["HEAPF64"]=HEAPF64=new Float64Array(buffer)}var STATIC_BASE=0,STATICTOP=0,staticSealed=false;var STACK_BASE=0,STACKTOP=0,STACK_MAX=0;var DYNAMIC_BASE=0,DYNAMICTOP=0;function abortOnCannotGrowMemory(){abort("Cannot enlarge memory arrays. Either (1) compile with -s TOTAL_MEMORY=X with X higher than the current value "+TOTAL_MEMORY+", (2) compile with -s ALLOW_MEMORY_GROWTH=1 which adjusts the size at runtime but prevents some optimizations, (3) set Module.TOTAL_MEMORY to a higher value before the program runs, or if you want malloc to return NULL (0) instead of this abort, compile with -s ABORTING_MALLOC=0 ")}function enlargeMemory(){abortOnCannotGrowMemory()}var TOTAL_STACK=Module["TOTAL_STACK"]||65536;var TOTAL_MEMORY=Module["TOTAL_MEMORY"]||1048576;var totalMemory=64*1024;while(totalMemory<TOTAL_MEMORY||totalMemory<2*TOTAL_STACK){if(totalMemory<16*1024*1024){totalMemory*=2}else{totalMemory+=16*1024*1024}}if(totalMemory!==TOTAL_MEMORY){TOTAL_MEMORY=totalMemory}assert(typeof Int32Array!=="undefined"&&typeof Float64Array!=="undefined"&&!!(new Int32Array(1))["subarray"]&&!!(new Int32Array(1))["set"],"JS engine does not provide full typed array support");if(Module["buffer"]){buffer=Module["buffer"];assert(buffer.byteLength===TOTAL_MEMORY,"provided buffer should be "+TOTAL_MEMORY+" bytes, but it is "+buffer.byteLength)}else{buffer=new ArrayBuffer(TOTAL_MEMORY)}updateGlobalBufferViews();HEAP32[0]=255;assert(HEAPU8[0]===255&&HEAPU8[3]===0,"Typed arrays 2 must be run on a little-endian system");Module["HEAP"]=HEAP;Module["buffer"]=buffer;Module["HEAP8"]=HEAP8;Module["HEAP16"]=HEAP16;Module["HEAP32"]=HEAP32;Module["HEAPU8"]=HEAPU8;Module["HEAPU16"]=HEAPU16;Module["HEAPU32"]=HEAPU32;Module["HEAPF32"]=HEAPF32;Module["HEAPF64"]=HEAPF64;function callRuntimeCallbacks(callbacks){while(callbacks.length>0){var callback=callbacks.shift();if(typeof callback=="function"){callback();continue}var func=callback.func;if(typeof func==="number"){if(callback.arg===undefined){Runtime.dynCall("v",func)}else{Runtime.dynCall("vi",func,[callback.arg])}}else{func(callback.arg===undefined?null:callback.arg)}}}var __ATPRERUN__=[];var __ATINIT__=[];var __ATMAIN__=[];var __ATEXIT__=[];var __ATPOSTRUN__=[];var runtimeInitialized=false;var runtimeExited=false;function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(__ATPRERUN__)}function ensureInitRuntime(){if(runtimeInitialized)return;runtimeInitialized=true;callRuntimeCallbacks(__ATINIT__)}function preMain(){callRuntimeCallbacks(__ATMAIN__)}function exitRuntime(){callRuntimeCallbacks(__ATEXIT__);runtimeExited=true}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(__ATPOSTRUN__)}function addOnPreRun(cb){__ATPRERUN__.unshift(cb)}function addOnInit(cb){__ATINIT__.unshift(cb)}function addOnPreMain(cb){__ATMAIN__.unshift(cb)}function addOnExit(cb){__ATEXIT__.unshift(cb)}function addOnPostRun(cb){__ATPOSTRUN__.unshift(cb)}function intArrayFromString(stringy,dontAddNull,length){var len=length>0?length:lengthBytesUTF8(stringy)+1;var u8array=new Array(len);var numBytesWritten=stringToUTF8Array(stringy,u8array,0,u8array.length);if(dontAddNull)u8array.length=numBytesWritten;return u8array}function intArrayToString(array){var ret=[];for(var i=0;i<array.length;i++){var chr=array[i];if(chr>255){chr&=255}ret.push(String.fromCharCode(chr))}return ret.join("")}function writeStringToMemory(string,buffer,dontAddNull){var array=intArrayFromString(string,dontAddNull);var i=0;while(i<array.length){var chr=array[i];HEAP8[buffer+i>>0]=chr;i=i+1}}Module["writeStringToMemory"]=writeStringToMemory;function writeArrayToMemory(array,buffer){for(var i=0;i<array.length;i++){HEAP8[buffer++>>0]=array[i]}}function writeAsciiToMemory(str,buffer,dontAddNull){for(var i=0;i<str.length;++i){HEAP8[buffer++>>0]=str.charCodeAt(i)}if(!dontAddNull)HEAP8[buffer>>0]=0}function unSign(value,bits,ignore){if(value>=0){return value}return bits<=32?2*Math.abs(1<<bits-1)+value:Math.pow(2,bits)+value}function reSign(value,bits,ignore){if(value<=0){return value}var half=bits<=32?Math.abs(1<<bits-1):Math.pow(2,bits-1);if(value>=half&&(bits<=32||value>half)){value=-2*half+value}return value}if(!Math["imul"]||Math["imul"](4294967295,5)!==-5)Math["imul"]=function imul(a,b){var ah=a>>>16;var al=a&65535;var bh=b>>>16;var bl=b&65535;return al*bl+(ah*bl+al*bh<<16)|0};Math.imul=Math["imul"];if(!Math["clz32"])Math["clz32"]=(function(x){x=x>>>0;for(var i=0;i<32;i++){if(x&1<<31-i)return i}return 32});Math.clz32=Math["clz32"];var Math_abs=Math.abs;var Math_cos=Math.cos;var Math_sin=Math.sin;var Math_tan=Math.tan;var Math_acos=Math.acos;var Math_asin=Math.asin;var Math_atan=Math.atan;var Math_atan2=Math.atan2;var Math_exp=Math.exp;var Math_log=Math.log;var Math_sqrt=Math.sqrt;var Math_ceil=Math.ceil;var Math_floor=Math.floor;var Math_pow=Math.pow;var Math_imul=Math.imul;var Math_fround=Math.fround;var Math_min=Math.min;var Math_clz32=Math.clz32;var runDependencies=0;var runDependencyWatcher=null;var dependenciesFulfilled=null;function getUniqueRunDependency(id){return id}function addRunDependency(id){runDependencies++;if(Module["monitorRunDependencies"]){Module["monitorRunDependencies"](runDependencies)}}function removeRunDependency(id){runDependencies--;if(Module["monitorRunDependencies"]){Module["monitorRunDependencies"](runDependencies)}if(runDependencies==0){if(runDependencyWatcher!==null){clearInterval(runDependencyWatcher);runDependencyWatcher=null}if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}}Module["preloadedImages"]={};Module["preloadedAudios"]={};var memoryInitializer=null;var ASM_CONSTS=[];STATIC_BASE=8;STATICTOP=STATIC_BASE+5360;__ATINIT__.push();allocate([0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,33,34,118,101,99,116,111,114,32,108,101,110,103,116,104,95,101,114,114,111,114,34,0,47,109,101,100,105,97,47,97,108,111,110,47,100,54,57,100,100,57,98,50,45,52,55,57,49,45,52,98,56,101,45,97,101,98,51,45,102,54,51,53,51,98,52,53,100,55,49,48,47,104,111,109,101,47,97,108,111,110,47,68,101,118,47,101,109,115,99,114,105,112,116,101,110,47,115,121,115,116,101,109,47,105,110,99,108,117,100,101,47,108,105,98,99,120,120,47,118,101,99,116,111,114,0,95,95,116,104,114,111,119,95,108,101,110,103,116,104,95,101,114,114,111,114,0,32,99,111,110,115,116,0,33,34,98,97,115,105,99,95,115,116,114,105,110,103,32,111,117,116,95,111,102,95,114,97,110,103,101,34,0,47,109,101,100,105,97,47,97,108,111,110,47,100,54,57,100,100,57,98,50,45,52,55,57,49,45,52,98,56,101,45,97,101,98,51,45,102,54,51,53,51,98,52,53,100,55,49,48,47,104,111,109,101,47,97,108,111,110,47,68,101,118,47,101,109,115,99,114,105,112,116,101,110,47,115,121,115,116,101,109,47,105,110,99,108,117,100,101,47,108,105,98,99,120,120,47,115,116,114,105,110,103,0,95,95,116,104,114,111,119,95,111,117,116,95,111,102,95,114,97,110,103,101,0,33,34,98,97,115,105,99,95,115,116,114,105,110,103,32,108,101,110,103,116,104,95,101,114,114,111,114,34,0,32,118,111,108,97,116,105,108,101,0,32,114,101,115,116,114,105,99,116,0,118,111,105,100,0,119,99,104,97,114,95,116,0,98,111,111,108,0,99,104,97,114,0,115,105,103,110,101,100,32,99,104,97,114,0,117,110,115,105,103,110,101,100,32,99,104,97,114,0,115,104,111,114,116,0,117,110,115,105,103,110,101,100,32,115,104,111,114,116,0,105,110,116,0,117,110,115,105,103,110,101,100,32,105,110,116,0,108,111,110,103,0,117,110,115,105,103,110,101,100,32,108,111,110,103,0,108,111,110,103,32,108,111,110,103,0,117,110,115,105,103,110,101,100,32,108,111,110,103,32,108,111,110,103,0,95,95,105,110,116,49,50,56,0,117,110,115,105,103,110,101,100,32,95,95,105,110,116,49,50,56,0,102,108,111,97,116,0,100,111,117,98,108,101,0,108,111,110,103,32,100,111,117,98,108,101,0,95,95,102,108,111,97,116,49,50,56,0,46,46,46,0,95,71,76,79,66,65,76,95,95,78,0,40,97,110,111,110,121,109,111,117,115,32,110,97,109,101,115,112,97,99,101,41,0,100,101,99,105,109,97,108,54,52,0,100,101,99,105,109,97,108,49,50,56,0,100,101,99,105,109,97,108,51,50,0,100,101,99,105,109,97,108,49,54,0,99,104,97,114,51,50,95,116,0,99,104,97,114,49,54,95,116,0,97,117,116,111,0,115,116,100,58,58,110,117,108,108,112,116,114,95,116,0,32,91,0,32,91,93,0,40,0,41,0,102,97,108,115,101,0,116,114,117,101,0,117,0,108,0,117,108,0,108,108,0,117,108,108,0,37,97,102,0,37,97,0,37,76,97,76,0,102,112,0,38,38,0,62,0,41,32,0,32,40,0,38,0,38,61,0,61,0,97,108,105,103,110,111,102,32,40,0,99,111,110,115,116,95,99,97,115,116,60,0,62,40,0,44,0,126,0,41,40,0,58,58,0,100,101,108,101,116,101,91,93,32,0,100,121,110,97,109,105,99,95,99,97,115,116,60,0,100,101,108,101,116,101,32,0,111,112,101,114,97,116,111,114,38,38,0,111,112,101,114,97,116,111,114,38,0,111,112,101,114,97,116,111,114,38,61,0,111,112,101,114,97,116,111,114,61,0,111,112,101,114,97,116,111,114,40,41,0,111,112,101,114,97,116,111,114,44,0,111,112,101,114,97,116,111,114,126,0,111,112,101,114,97,116,111,114,32,0,111,112,101,114,97,116,111,114,32,100,101,108,101,116,101,91,93,0,111,112,101,114,97,116,111,114,42,0,111,112,101,114,97,116,111,114,32,100,101,108,101,116,101,0,111,112,101,114,97,116,111,114,47,0,111,112,101,114,97,116,111,114,47,61,0,111,112,101,114,97,116,111,114,94,0,111,112,101,114,97,116,111,114,94,61,0,111,112,101,114,97,116,111,114,61,61,0,111,112,101,114,97,116,111,114,62,61,0,111,112,101,114,97,116,111,114,62,0,111,112,101,114,97,116,111,114,91,93,0,111,112,101,114,97,116,111,114,60,61,0,111,112,101,114,97,116,111,114,34,34,32,0,111,112,101,114,97,116,111,114,60,60,0,111,112,101,114,97,116,111,114,60,60,61,0,111,112,101,114,97,116,111,114,60,0,111,112,101,114,97,116,111,114,45,0,111,112,101,114,97,116,111,114,45,61,0,111,112,101,114,97,116,111,114,42,61,0,111,112,101,114,97,116,111,114,45,45,0,111,112,101,114,97,116,111,114,32,110,101,119,91,93,0,111,112,101,114,97,116,111,114,33,61,0,111,112,101,114,97,116,111,114,33,0,111,112,101,114,97,116,111,114,32,110,101,119,0,111,112,101,114,97,116,111,114,124,124,0,111,112,101,114,97,116,111,114,124,0,111,112,101,114,97,116,111,114,124,61,0,111,112,101,114,97,116,111,114,45,62,42,0,111,112,101,114,97,116,111,114,43,0,111,112,101,114,97,116,111,114,43,61,0,111,112,101,114,97,116,111,114,43,43,0,111,112,101,114,97,116,111,114,45,62,0,111,112,101,114,97,116,111,114,63,0,111,112,101,114,97,116,111,114,37,0,111,112,101,114,97,116,111,114,37,61,0,111,112,101,114,97,116,111,114,62,62,0,111,112,101,114,97,116,111,114,62,62,61,0,60,0,44,32,0,32,62,0,100,101,99,108,116,121,112,101,40,0,115,116,100,58,58,97,108,108,111,99,97,116,111,114,0,115,116,100,58,58,98,97,115,105,99,95,115,116,114,105,110,103,0,115,116,100,58,58,115,116,114,105,110,103,0,115,116,100,58,58,105,115,116,114,101,97,109,0,115,116,100,58,58,111,115,116,114,101,97,109,0,115,116,100,58,58,105,111,115,116,114,101,97,109,0,115,116,100,58,58,98,97,115,105,99,95,115,116,114,105,110,103,60,99,104,97,114,44,32,115,116,100,58,58,99,104,97,114,95,116,114,97,105,116,115,60,99,104,97,114,62,44,32,115,116,100,58,58,97,108,108,111,99,97,116,111,114,60,99,104,97,114,62,32,62,0,98,97,115,105,99,95,115,116,114,105,110,103,0,115,116,100,58,58,98,97,115,105,99,95,105,115,116,114,101,97,109,60,99,104,97,114,44,32,115,116,100,58,58,99,104,97,114,95,116,114,97,105,116,115,60,99,104,97,114,62,32,62,0,98,97,115,105,99,95,105,115,116,114,101,97,109,0,115,116,100,58,58,98,97,115,105,99,95,111,115,116,114,101,97,109,60,99,104,97,114,44,32,115,116,100,58,58,99,104,97,114,95,116,114,97,105,116,115,60,99,104,97,114,62,32,62,0,98,97,115,105,99,95,111,115,116,114,101,97,109,0,115,116,100,58,58,98,97,115,105,99,95,105,111,115,116,114,101,97,109,60,99,104,97,114,44,32,115,116,100,58,58,99,104,97,114,95,116,114,97,105,116,115,60,99,104,97,114,62,32,62,0,98,97,115,105,99,95,105,111,115,116,114,101,97,109,0,39,117,110,110,97,109,101,100,0,39,108,97,109,98,100,97,39,40,0,115,116,100,58,58,0,46,42,0,47,61,0,94,0,94,61,0,61,61,0,62,61,0,41,91,0,60,61,0,60,60,0,60,60,61,0,45,0,45,61,0,42,61,0,45,45,0,41,45,45,0,91,93,32,0,32,0,33,61,0,33,0,110,111,101,120,99,101,112,116,32,40,0,124,124,0,124,0,124,61,0,45,62,42,0,43,0,43,61,0,43,43,0,41,43,43,0,45,62,0,41,32,63,32,40,0,41,32,58,32,40,0,114,101,105,110,116,101,114,112,114,101,116,95,99,97,115,116,60,0,37,0,37,61,0,62,62,0,62,62,61,0,115,116,97,116,105,99,95,99,97,115,116,60,0,115,105,122,101,111,102,32,40,0,115,105,122,101,111,102,46,46,46,40,0,116,121,112,101,105,100,40,0,116,104,114,111,119,0,116,104,114,111,119,32,0,32,99,111,109,112,108,101,120,0,32,38,0,32,38,38,0,32,105,109,97,103,105,110,97,114,121,0,58,58,42,0,111,98,106,99,95,111,98,106,101,99,116,60,0,105,100,0,111,98,106,99,112,114,111,116,111,0,115,116,100,0,58,58,115,116,114,105,110,103,32,108,105,116,101,114,97,108,0,32,118,101,99,116,111,114,91,0,112,105,120,101,108,32,118,101,99,116,111,114,91,0,118,116,97,98,108,101,32,102,111,114,32,0,86,84,84,32,102,111,114,32,0,116,121,112,101,105,110,102,111,32,102,111,114,32,0,116,121,112,101,105,110,102,111,32,110,97,109,101,32,102,111,114,32,0,99,111,118,97,114,105,97,110,116,32,114,101,116,117,114,110,32,116,104,117,110,107,32,116,111,32,0,99,111,110,115,116,114,117,99,116,105,111,110,32,118,116,97,98,108,101,32,102,111,114,32,0,45,105,110,45,0,118,105,114,116,117,97,108,32,116,104,117,110,107,32,116,111,32,0,110,111,110,45,118,105,114,116,117,97,108,32,116,104,117,110,107,32,116,111,32,0,103,117,97,114,100,32,118,97,114,105,97,98,108,101,32,102,111,114,32,0,114,101,102,101,114,101,110,99,101,32,116,101,109,112,111,114,97,114,121,32,102,111,114,32,0,95,98,108,111,99,107,95,105,110,118,111,107,101,0,105,110,118,111,99,97,116,105,111,110,32,102,117,110,99,116,105,111,110,32,102,111,114,32,98,108,111,99,107,32,105,110,32,0,47,0,84,33,34,25,13,1,2,3,17,75,28,12,16,4,11,29,18,30,39,104,110,111,112,113,98,32,5,6,15,19,20,21,26,8,22,7,40,36,23,24,9,10,14,27,31,37,35,131,130,125,38,42,43,60,61,62,63,67,71,74,77,88,89,90,91,92,93,94,95,96,97,99,100,101,102,103,105,106,107,108,114,115,116,121,122,123,124,0,73,108,108,101,103,97,108,32,98,121,116,101,32,115,101,113,117,101,110,99,101,0,68,111,109,97,105,110,32,101,114,114,111,114,0,82,101,115,117,108,116,32,110,111,116,32,114,101,112,114,101,115,101,110,116,97,98,108,101,0,78,111,116,32,97,32,116,116,121,0,80,101,114,109,105,115,115,105,111,110,32,100,101,110,105,101,100,0,79,112,101,114,97,116,105,111,110,32,110,111,116,32,112,101,114,109,105,116,116,101,100,0,78,111,32,115,117,99,104,32,102,105,108,101,32,111,114,32,100,105,114,101,99,116,111,114,121,0,78,111,32,115,117,99,104,32,112,114,111,99,101,115,115,0,70,105,108,101,32,101,120,105,115,116,115,0,86,97,108,117,101,32,116,111,111,32,108,97,114,103,101,32,102,111,114,32,100,97,116,97,32,116,121,112,101,0,78,111,32,115,112,97,99,101,32,108,101,102,116,32,111,110,32,100,101,118,105,99,101,0,79,117,116,32,111,102,32,109,101,109,111,114,121,0,82,101,115,111,117,114,99,101,32,98,117,115,121,0,73,110,116,101,114,114,117,112,116,101,100,32,115,121,115,116,101,109,32,99,97,108,108,0,82,101,115,111,117,114,99,101,32,116,101,109,112,111,114,97,114,105,108,121,32,117,110,97,118,97,105,108,97,98,108,101,0,73,110,118,97,108,105,100,32,115,101,101,107,0,67,114,111,115,115,45,100,101,118,105,99,101,32,108,105,110,107,0,82,101,97,100,45,111,110,108,121,32,102,105,108,101,32,115,121,115,116,101,109,0,68,105,114,101,99,116,111,114,121,32,110,111,116,32,101,109,112,116,121,0,67,111,110,110,101,99,116,105,111,110,32,114,101,115,101,116,32,98,121,32,112,101,101,114,0,79,112,101,114,97,116,105,111,110,32,116,105,109,101,100,32,111,117,116,0,67,111,110,110,101,99,116,105,111,110,32,114,101,102,117,115,101,100,0,72,111,115,116,32,105,115,32,100,111,119,110,0,72,111,115,116,32,105,115,32,117,110,114,101,97,99,104,97,98,108,101,0,65,100,100,114,101,115,115,32,105,110,32,117,115,101,0,66,114,111,107,101,110,32,112,105,112,101,0,73,47,79,32,101,114,114,111,114,0,78,111,32,115,117,99,104,32,100,101,118,105,99,101,32,111,114,32,97,100,100,114,101,115,115,0,66,108,111,99,107,32,100,101,118,105,99,101,32,114,101,113,117,105,114,101,100,0,78,111,32,115,117,99,104,32,100,101,118,105,99,101,0,78,111,116,32,97,32,100,105,114,101,99,116,111,114,121,0,73,115,32,97,32,100,105,114,101,99,116,111,114,121,0,84,101,120,116,32,102,105,108,101,32,98,117,115,121,0,69,120,101,99,32,102,111,114,109,97,116,32,101,114,114,111,114,0,73,110,118,97,108,105,100,32,97,114,103,117,109,101,110,116,0,65,114,103,117,109,101,110,116,32,108,105,115,116,32,116,111,111,32,108,111,110,103,0,83,121,109,98,111,108,105,99,32,108,105,110,107,32,108,111,111,112,0,70,105,108,101,110,97,109,101,32,116,111,111,32,108,111,110,103,0,84,111,111,32,109,97,110,121,32,111,112,101,110,32,102,105,108,101,115,32,105,110,32,115,121,115,116,101,109,0,78,111,32,102,105,108,101,32,100,101,115,99,114,105,112,116,111,114,115,32,97,118,97,105,108,97,98,108,101,0,66,97,100,32,102,105,108,101,32,100,101,115,99,114,105,112,116,111,114,0,78,111,32,99,104,105,108,100,32,112,114,111,99,101,115,115,0,66,97,100,32,97,100,100,114,101,115,115,0,70,105,108,101,32,116,111,111,32,108,97,114,103,101,0,84,111,111,32,109,97,110,121,32,108,105,110,107,115,0,78,111,32,108,111,99,107,115,32,97,118,97,105,108,97,98,108,101,0,82,101,115,111,117,114,99,101,32,100,101,97,100,108,111,99,107,32,119,111,117,108,100,32,111,99,99,117,114,0,83,116,97,116,101,32,110,111,116,32,114,101,99,111,118,101,114,97,98,108,101,0,80,114,101,118,105,111,117,115,32,111,119,110,101,114,32,100,105,101,100,0,79,112,101,114,97,116,105,111,110,32,99,97,110,99,101,108,101,100,0,70,117,110,99,116,105,111,110,32,110,111,116,32,105,109,112,108,101,109,101,110,116,101,100,0,78,111,32,109,101,115,115,97,103,101,32,111,102,32,100,101,115,105,114,101,100,32,116,121,112,101,0,73,100,101,110,116,105,102,105,101,114,32,114,101,109,111,118,101,100,0,68,101,118,105,99,101,32,110,111,116,32,97,32,115,116,114,101,97,109,0,78,111,32,100,97,116,97,32,97,118,97,105,108,97,98,108,101,0,68,101,118,105,99,101,32,116,105,109,101,111,117,116,0,79,117,116,32,111,102,32,115,116,114,101,97,109,115,32,114,101,115,111,117,114,99,101,115,0,76,105,110,107,32,104,97,115,32,98,101,101,110,32,115,101,118,101,114,101,100,0,80,114,111,116,111,99,111,108,32,101,114,114,111,114,0,66,97,100,32,109,101,115,115,97,103,101,0,70,105,108,101,32,100,101,115,99,114,105,112,116,111,114,32,105,110,32,98,97,100,32,115,116,97,116,101,0,78,111,116,32,97,32,115,111,99,107,101,116,0,68,101,115,116,105,110,97,116,105,111,110,32,97,100,100,114,101,115,115,32,114,101,113,117,105,114,101,100,0,77,101,115,115,97,103,101,32,116,111,111,32,108,97,114,103,101,0,80,114,111,116,111,99,111,108,32,119,114,111,110,103,32,116,121,112,101,32,102,111,114,32,115,111,99,107,101,116,0,80,114,111,116,111,99,111,108,32,110,111,116,32,97,118,97,105,108,97,98,108,101,0,80,114,111,116,111,99,111,108,32,110,111,116,32,115,117,112,112,111,114,116,101,100,0,83,111,99,107,101,116,32,116,121,112,101,32,110,111,116,32,115,117,112,112,111,114,116,101,100,0,78,111,116,32,115,117,112,112,111,114,116,101,100,0,80,114,111,116,111,99,111,108,32,102,97,109,105,108,121,32,110,111,116,32,115,117,112,112,111,114,116,101,100,0,65,100,100,114,101,115,115,32,102,97,109,105,108,121,32,110,111,116,32,115,117,112,112,111,114,116,101,100,32,98,121,32,112,114,111,116,111,99,111,108,0,65,100,100,114,101,115,115,32,110,111,116,32,97,118,97,105,108,97,98,108,101,0,78,101,116,119,111,114,107,32,105,115,32,100,111,119,110,0,78,101,116,119,111,114,107,32,117,110,114,101,97,99,104,97,98,108,101,0,67,111,110,110,101,99,116,105,111,110,32,114,101,115,101,116,32,98,121,32,110,101,116,119,111,114,107,0,67,111,110,110,101,99,116,105,111,110,32,97,98,111,114,116,101,100,0,78,111,32,98,117,102,102,101,114,32,115,112,97,99,101,32,97,118,97,105,108,97,98,108,101,0,83,111,99,107,101,116,32,105,115,32,99,111,110,110,101,99,116,101,100,0,83,111,99,107,101,116,32,110,111,116,32,99,111,110,110,101,99,116,101,100,0,67,97,110,110,111,116,32,115,101,110,100,32,97,102,116,101,114,32,115,111,99,107,101,116,32,115,104,117,116,100,111,119,110,0,79,112,101,114,97,116,105,111,110,32,97,108,114,101,97,100,121,32,105,110,32,112,114,111,103,114,101,115,115,0,79,112,101,114,97,116,105,111,110,32,105,110,32,112,114,111,103,114,101,115,115,0,83,116,97,108,101,32,102,105,108,101,32,104,97,110,100,108,101,0,82,101,109,111,116,101,32,73,47,79,32,101,114,114,111,114,0,81,117,111,116,97,32,101,120,99,101,101,100,101,100,0,78,111,32,109,101,100,105,117,109,32,102,111,117,110,100,0,87,114,111,110,103,32,109,101,100,105,117,109,32,116,121,112,101,0,78,111,32,101,114,114,111,114,32,105,110,102,111,114,109,97,116,105,111,110,0,0,42,0,93,0,17,0,10,0,17,17,17,0,0,0,0,5,0,0,0,0,0,0,9,0,0,0,0,11,0,0,0,0,0,0,0,0,17,0,15,10,17,17,17,3,10,7,0,1,19,9,11,11,0,0,9,6,11,0,0,11,0,6,17,0,0,0,17,17,17,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,11,0,0,0,0,0,0,0,0,17,0,10,10,17,17,17,0,10,0,0,2,0,9,11,0,0,0,9,0,11,0,0,11,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,12,0,0,0,0,0,0,0,0,0,0,0,12,0,0,0,0,12,0,0,0,0,9,12,0,0,0,0,0,12,0,0,12,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14,0,0,0,0,0,0,0,0,0,0,0,13,0,0,0,4,13,0,0,0,0,9,14,0,0,0,0,0,14,0,0,14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,16,0,0,0,0,0,0,0,0,0,0,0,15,0,0,0,0,15,0,0,0,0,9,16,0,0,0,0,0,16,0,0,16,0,0,18,0,0,0,18,18,18,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,18,0,0,0,18,18,18,0,0,0,0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,11,0,0,0,0,0,0,0,0,0,0,0,10,0,0,0,0,10,0,0,0,0,9,11,0,0,0,0,0,11,0,0,11,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,12,0,0,0,0,0,0,0,0,0,0,0,12,0,0,0,0,12,0,0,0,0,9,12,0,0,0,0,0,12,0,0,12,0,0,48,49,50,51,52,53,54,55,56,57,65,66,67,68,69,70,45,43,32,32,32,48,88,48,120,0,40,110,117,108,108,41,0,45,48,88,43,48,88,32,48,88,45,48,120,43,48,120,32,48,120,0,105,110,102,0,73,78,70,0,110,97,110,0,78,65,78,0,46,0],"i8",ALLOC_NONE,Runtime.GLOBAL_BASE);var tempDoublePtr=Runtime.alignMemory(allocate(12,"i8",ALLOC_STATIC),8);assert(tempDoublePtr%8==0);function copyTempFloat(ptr){HEAP8[tempDoublePtr]=HEAP8[ptr];HEAP8[tempDoublePtr+1]=HEAP8[ptr+1];HEAP8[tempDoublePtr+2]=HEAP8[ptr+2];HEAP8[tempDoublePtr+3]=HEAP8[ptr+3]}function copyTempDouble(ptr){HEAP8[tempDoublePtr]=HEAP8[ptr];HEAP8[tempDoublePtr+1]=HEAP8[ptr+1];HEAP8[tempDoublePtr+2]=HEAP8[ptr+2];HEAP8[tempDoublePtr+3]=HEAP8[ptr+3];HEAP8[tempDoublePtr+4]=HEAP8[ptr+4];HEAP8[tempDoublePtr+5]=HEAP8[ptr+5];HEAP8[tempDoublePtr+6]=HEAP8[ptr+6];HEAP8[tempDoublePtr+7]=HEAP8[ptr+7]}Module["_memset"]=_memset;Module["_i64Subtract"]=_i64Subtract;function ___setErrNo(value){if(Module["___errno_location"])HEAP32[Module["___errno_location"]()>>2]=value;return value}var ERRNO_CODES={EPERM:1,ENOENT:2,ESRCH:3,EINTR:4,EIO:5,ENXIO:6,E2BIG:7,ENOEXEC:8,EBADF:9,ECHILD:10,EAGAIN:11,EWOULDBLOCK:11,ENOMEM:12,EACCES:13,EFAULT:14,ENOTBLK:15,EBUSY:16,EEXIST:17,EXDEV:18,ENODEV:19,ENOTDIR:20,EISDIR:21,EINVAL:22,ENFILE:23,EMFILE:24,ENOTTY:25,ETXTBSY:26,EFBIG:27,ENOSPC:28,ESPIPE:29,EROFS:30,EMLINK:31,EPIPE:32,EDOM:33,ERANGE:34,ENOMSG:42,EIDRM:43,ECHRNG:44,EL2NSYNC:45,EL3HLT:46,EL3RST:47,ELNRNG:48,EUNATCH:49,ENOCSI:50,EL2HLT:51,EDEADLK:35,ENOLCK:37,EBADE:52,EBADR:53,EXFULL:54,ENOANO:55,EBADRQC:56,EBADSLT:57,EDEADLOCK:35,EBFONT:59,ENOSTR:60,ENODATA:61,ETIME:62,ENOSR:63,ENONET:64,ENOPKG:65,EREMOTE:66,ENOLINK:67,EADV:68,ESRMNT:69,ECOMM:70,EPROTO:71,EMULTIHOP:72,EDOTDOT:73,EBADMSG:74,ENOTUNIQ:76,EBADFD:77,EREMCHG:78,ELIBACC:79,ELIBBAD:80,ELIBSCN:81,ELIBMAX:82,ELIBEXEC:83,ENOSYS:38,ENOTEMPTY:39,ENAMETOOLONG:36,ELOOP:40,EOPNOTSUPP:95,EPFNOSUPPORT:96,ECONNRESET:104,ENOBUFS:105,EAFNOSUPPORT:97,EPROTOTYPE:91,ENOTSOCK:88,ENOPROTOOPT:92,ESHUTDOWN:108,ECONNREFUSED:111,EADDRINUSE:98,ECONNABORTED:103,ENETUNREACH:101,ENETDOWN:100,ETIMEDOUT:110,EHOSTDOWN:112,EHOSTUNREACH:113,EINPROGRESS:115,EALREADY:114,EDESTADDRREQ:89,EMSGSIZE:90,EPROTONOSUPPORT:93,ESOCKTNOSUPPORT:94,EADDRNOTAVAIL:99,ENETRESET:102,EISCONN:106,ENOTCONN:107,ETOOMANYREFS:109,EUSERS:87,EDQUOT:122,ESTALE:116,ENOTSUP:95,ENOMEDIUM:123,EILSEQ:84,EOVERFLOW:75,ECANCELED:125,ENOTRECOVERABLE:131,EOWNERDEAD:130,ESTRPIPE:86};function _sysconf(name){switch(name){case 30:return PAGE_SIZE;case 85:return totalMemory/PAGE_SIZE;case 132:case 133:case 12:case 137:case 138:case 15:case 235:case 16:case 17:case 18:case 19:case 20:case 149:case 13:case 10:case 236:case 153:case 9:case 21:case 22:case 159:case 154:case 14:case 77:case 78:case 139:case 80:case 81:case 82:case 68:case 67:case 164:case 11:case 29:case 47:case 48:case 95:case 52:case 51:case 46:return 200809;case 79:return 0;case 27:case 246:case 127:case 128:case 23:case 24:case 160:case 161:case 181:case 182:case 242:case 183:case 184:case 243:case 244:case 245:case 165:case 178:case 179:case 49:case 50:case 168:case 169:case 175:case 170:case 171:case 172:case 97:case 76:case 32:case 173:case 35:return-1;case 176:case 177:case 7:case 155:case 8:case 157:case 125:case 126:case 92:case 93:case 129:case 130:case 131:case 94:case 91:return 1;case 74:case 60:case 69:case 70:case 4:return 1024;case 31:case 42:case 72:return 32;case 87:case 26:case 33:return 2147483647;case 34:case 1:return 47839;case 38:case 36:return 99;case 43:case 37:return 2048;case 0:return 2097152;case 3:return 65536;case 28:return 32768;case 44:return 32767;case 75:return 16384;case 39:return 1e3;case 89:return 700;case 71:return 256;case 40:return 255;case 2:return 100;case 180:return 64;case 25:return 20;case 5:return 16;case 6:return 6;case 73:return 4;case 84:{if(typeof navigator==="object")return navigator["hardwareConcurrency"]||1;return 1}}___setErrNo(ERRNO_CODES.EINVAL);return-1}var _BDtoIHigh=true;var _BDtoILow=true;Module["_bitshift64Lshr"]=_bitshift64Lshr;var _BItoD=true;Module["_bitshift64Shl"]=_bitshift64Shl;function _abort(){Module["abort"]()}function _emscripten_memcpy_big(dest,src,num){HEAPU8.set(HEAPU8.subarray(src,src+num),dest);return dest}Module["_memcpy"]=_memcpy;Module["_i64Add"]=_i64Add;function ___assert_fail(condition,filename,line,func){ABORT=true;throw"Assertion failed: "+Pointer_stringify(condition)+", at: "+[filename?Pointer_stringify(filename):"unknown filename",line,func?Pointer_stringify(func):"unknown function"]+" at "+stackTrace()}function _sbrk(bytes){var self=_sbrk;if(!self.called){DYNAMICTOP=alignMemoryPage(DYNAMICTOP);self.called=true;assert(Runtime.dynamicAlloc);self.alloc=Runtime.dynamicAlloc;Runtime.dynamicAlloc=(function(){abort("cannot dynamically allocate, sbrk now has control")})}var ret=DYNAMICTOP;if(bytes!=0){var success=self.alloc(bytes);if(!success)return-1>>>0}return ret}Module["_memmove"]=_memmove;function __ZSt18uncaught_exceptionv(){return!!__ZSt18uncaught_exceptionv.uncaught_exception}var EXCEPTIONS={last:0,caught:[],infos:{},deAdjust:(function(adjusted){if(!adjusted||EXCEPTIONS.infos[adjusted])return adjusted;for(var ptr in EXCEPTIONS.infos){var info=EXCEPTIONS.infos[ptr];if(info.adjusted===adjusted){return ptr}}return adjusted}),addRef:(function(ptr){if(!ptr)return;var info=EXCEPTIONS.infos[ptr];info.refcount++}),decRef:(function(ptr){if(!ptr)return;var info=EXCEPTIONS.infos[ptr];assert(info.refcount>0);info.refcount--;if(info.refcount===0){if(info.destructor){Runtime.dynCall("vi",info.destructor,[ptr])}delete EXCEPTIONS.infos[ptr];___cxa_free_exception(ptr)}}),clearRef:(function(ptr){if(!ptr)return;var info=EXCEPTIONS.infos[ptr];info.refcount=0})};function ___resumeException(ptr){if(!EXCEPTIONS.last){EXCEPTIONS.last=ptr}EXCEPTIONS.clearRef(EXCEPTIONS.deAdjust(ptr));throw ptr+" - Exception catching is disabled, this exception cannot be caught. Compile with -s DISABLE_EXCEPTION_CATCHING=0 or DISABLE_EXCEPTION_CATCHING=2 to catch."}function ___cxa_find_matching_catch(){var thrown=EXCEPTIONS.last;if(!thrown){return(asm["setTempRet0"](0),0)|0}var info=EXCEPTIONS.infos[thrown];var throwntype=info.type;if(!throwntype){return(asm["setTempRet0"](0),thrown)|0}var typeArray=Array.prototype.slice.call(arguments);var pointer=Module["___cxa_is_pointer_type"](throwntype);if(!___cxa_find_matching_catch.buffer)___cxa_find_matching_catch.buffer=_malloc(4);HEAP32[___cxa_find_matching_catch.buffer>>2]=thrown;thrown=___cxa_find_matching_catch.buffer;for(var i=0;i<typeArray.length;i++){if(typeArray[i]&&Module["___cxa_can_catch"](typeArray[i],throwntype,thrown)){thrown=HEAP32[thrown>>2];info.adjusted=thrown;return(asm["setTempRet0"](typeArray[i]),thrown)|0}}thrown=HEAP32[thrown>>2];return(asm["setTempRet0"](throwntype),thrown)|0}function ___gxx_personality_v0(){}function _time(ptr){var ret=Date.now()/1e3|0;if(ptr){HEAP32[ptr>>2]=ret}return ret}function _pthread_self(){return 0}STACK_BASE=STACKTOP=Runtime.alignMemory(STATICTOP);staticSealed=true;STACK_MAX=STACK_BASE+TOTAL_STACK;DYNAMIC_BASE=DYNAMICTOP=Runtime.alignMemory(STACK_MAX);assert(DYNAMIC_BASE<TOTAL_MEMORY,"TOTAL_MEMORY not big enough for stack");var cttz_i8=allocate([8,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,4,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,5,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,4,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,6,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,4,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,5,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,4,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,7,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,4,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,5,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,4,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,6,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,4,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,5,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,4,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0],"i8",ALLOC_DYNAMIC);function invoke_iiii(index,a1,a2,a3){try{return Module["dynCall_iiii"](index,a1,a2,a3)}catch(e){if(typeof e!=="number"&&e!=="longjmp")throw e;asm["setThrew"](1,0)}}Module.asmGlobalArg={"Math":Math,"Int8Array":Int8Array,"Int16Array":Int16Array,"Int32Array":Int32Array,"Uint8Array":Uint8Array,"Uint16Array":Uint16Array,"Uint32Array":Uint32Array,"Float32Array":Float32Array,"Float64Array":Float64Array,"NaN":NaN,"Infinity":Infinity};Module.asmLibraryArg={"abort":abort,"assert":assert,"invoke_iiii":invoke_iiii,"_sysconf":_sysconf,"_pthread_self":_pthread_self,"_abort":_abort,"___setErrNo":___setErrNo,"_sbrk":_sbrk,"_time":_time,"_emscripten_memcpy_big":_emscripten_memcpy_big,"___gxx_personality_v0":___gxx_personality_v0,"___resumeException":___resumeException,"__ZSt18uncaught_exceptionv":__ZSt18uncaught_exceptionv,"___assert_fail":___assert_fail,"___cxa_find_matching_catch":___cxa_find_matching_catch,"STACKTOP":STACKTOP,"STACK_MAX":STACK_MAX,"tempDoublePtr":tempDoublePtr,"ABORT":ABORT,"cttz_i8":cttz_i8};// EMSCRIPTEN_START_ASM
+var asm=(function(global,env,buffer) {
+"use asm";var a=new global.Int8Array(buffer);var b=new global.Int16Array(buffer);var c=new global.Int32Array(buffer);var d=new global.Uint8Array(buffer);var e=new global.Uint16Array(buffer);var f=new global.Uint32Array(buffer);var g=new global.Float32Array(buffer);var h=new global.Float64Array(buffer);var i=env.STACKTOP|0;var j=env.STACK_MAX|0;var k=env.tempDoublePtr|0;var l=env.ABORT|0;var m=env.cttz_i8|0;var n=0;var o=0;var p=0;var q=0;var r=global.NaN,s=global.Infinity;var t=0,u=0,v=0,w=0,x=0.0,y=0,z=0,A=0,B=0.0;var C=0;var D=0;var E=0;var F=0;var G=0;var H=0;var I=0;var J=0;var K=0;var L=0;var M=global.Math.floor;var N=global.Math.abs;var O=global.Math.sqrt;var P=global.Math.pow;var Q=global.Math.cos;var R=global.Math.sin;var S=global.Math.tan;var T=global.Math.acos;var U=global.Math.asin;var V=global.Math.atan;var W=global.Math.atan2;var X=global.Math.exp;var Y=global.Math.log;var Z=global.Math.ceil;var _=global.Math.imul;var $=global.Math.min;var aa=global.Math.clz32;var ba=env.abort;var ca=env.assert;var da=env.invoke_iiii;var ea=env._sysconf;var fa=env._pthread_self;var ga=env._abort;var ha=env.___setErrNo;var ia=env._sbrk;var ja=env._time;var ka=env._emscripten_memcpy_big;var la=env.___gxx_personality_v0;var ma=env.___resumeException;var na=env.__ZSt18uncaught_exceptionv;var oa=env.___assert_fail;var pa=env.___cxa_find_matching_catch;var qa=0.0;
+// EMSCRIPTEN_START_FUNCS
+function ub(b,d,e){b=b|0;d=d|0;e=e|0;var f=0,g=0,h=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0,s=0,t=0,u=0,v=0,w=0,x=0,y=0,z=0,A=0,B=0,C=0,D=0,E=0,F=0,G=0,H=0,I=0,J=0,K=0,L=0,M=0,N=0,O=0,P=0,Q=0,R=0,S=0,T=0,U=0,V=0,W=0,X=0,Y=0,Z=0,_=0,$=0,aa=0,ba=0,ca=0,da=0,ea=0,fa=0,ga=0,ha=0,ia=0,ja=0,ka=0,la=0,ma=0,na=0,oa=0,pa=0,qa=0,ra=0,sa=0,ta=0,ua=0,va=0,wa=0,xa=0,ya=0,za=0,Aa=0,Ba=0,Ca=0,Da=0,Ea=0,Fa=0,Ga=0,Ha=0,Ka=0,La=0,Ma=0,Oa=0,Qa=0,Ra=0,Sa=0,Ua=0,Va=0,Wa=0,Xa=0,_a=0,eb=0,fb=0,gb=0,hb=0,jb=0,kb=0,lb=0,mb=0,nb=0,ob=0,pb=0,qb=0,sb=0,tb=0,wb=0,yb=0,zb=0,Ab=0,Bb=0,Ib=0,Kb=0,Lb=0,Mb=0,Nb=0,Ob=0,Pb=0,Qb=0,Rb=0,Sb=0,Tb=0,Vb=0,Wb=0,Xb=0,Yb=0,Zb=0,_b=0,$b=0,ac=0,bc=0,cc=0,dc=0,ec=0,fc=0;fc=i;i=i+1104|0;dc=fc+1072|0;ec=fc+1048|0;cc=fc+1032|0;bc=fc+1020|0;$b=fc+1008|0;_b=fc+984|0;ac=fc+972|0;Sb=fc+596|0;Tb=fc+572|0;Xb=fc+548|0;Wb=fc+524|0;Yb=fc+488|0;Zb=fc+460|0;f=fc+960|0;k=fc+948|0;n=fc+936|0;r=fc+924|0;u=fc+912|0;w=fc+900|0;x=fc+888|0;Ib=fc+876|0;Kb=fc+864|0;Lb=fc+852|0;Mb=fc+840|0;y=fc+828|0;Nb=fc+816|0;Ob=fc+804|0;Pb=fc+792|0;Qb=fc+780|0;B=fc+768|0;C=fc+756|0;D=fc+744|0;F=fc+732|0;G=fc+720|0;H=fc+708|0;I=fc+696|0;fb=fc+672|0;gb=fc+656|0;hb=fc+644|0;jb=fc+632|0;kb=fc+620|0;J=fc+608|0;K=fc+584|0;L=fc+560|0;M=fc+536|0;N=fc+512|0;O=fc+472|0;P=fc+448|0;Q=fc+436|0;na=fc+424|0;Ga=fc+400|0;Ha=fc+384|0;Ka=fc+372|0;La=fc+360|0;R=fc+348|0;S=fc+336|0;T=fc+324|0;U=fc+312|0;V=fc+300|0;W=fc+288|0;X=fc+276|0;_=fc+264|0;$=fc+252|0;oa=fc+240|0;Ma=fc+216|0;Oa=fc+204|0;Qa=fc+192|0;Ra=fc+180|0;aa=fc+168|0;sb=fc+144|0;tb=fc+132|0;wb=fc+120|0;yb=fc+108|0;zb=fc+96|0;Ab=fc+84|0;Bb=fc+72|0;da=fc+60|0;fa=fc+48|0;ha=fc+36|0;ia=fc+24|0;Sa=fc;lb=d;ja=lb-b|0;a:do if((ja|0)>1){ka=(ja|0)>3;if(ka?(a[b>>0]|0)==103:0){la=(a[b+1>>0]|0)==115;Ua=la;la=la?b+2|0:b}else{Ua=0;la=b}do switch(a[la>>0]|0){case 76:{b=vb(b,d,e)|0;break a}case 84:{b=Eb(b,d,e)|0;break a}case 102:{b=Fb(b,d,e)|0;break a}case 97:switch(a[la+1>>0]|0){case 97:{dc=b+2|0;$a(f,841,2);ec=Gb(dc,d,f,e)|0;Ja(f);b=(ec|0)==(dc|0)?b:ec;break a}case 100:{dc=b+2|0;$a(k,852,1);ec=Hb(dc,d,k,e)|0;Ja(k);b=(ec|0)==(dc|0)?b:ec;break a}case 110:{dc=b+2|0;$a(n,852,1);ec=Gb(dc,d,n,e)|0;Ja(n);b=(ec|0)==(dc|0)?b:ec;break a}case 78:{dc=b+2|0;$a(r,854,2);ec=Gb(dc,d,r,e)|0;Ja(r);b=(ec|0)==(dc|0)?b:ec;break a}case 83:{dc=b+2|0;$a(u,857,1);ec=Gb(dc,d,u,e)|0;Ja(u);b=(ec|0)==(dc|0)?b:ec;break a}case 116:{if(((((ja|0)>2?(a[b>>0]|0)==97:0)?(a[b+1>>0]|0)==116:0)?(bc=b+2|0,nb=Na(bc,d,e)|0,(nb|0)!=(bc|0)):0)?(Da=c[e+4>>2]|0,(c[e>>2]|0)!=(Da|0)):0){o=Da+-24|0;Cb(cc,o);b=Ta(cc,0,859)|0;c[ec>>2]=c[b>>2];c[ec+4>>2]=c[b+4>>2];c[ec+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}b=Ya(ec,799)|0;c[dc>>2]=c[b>>2];c[dc+4>>2]=c[b+4>>2];c[dc+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}do if(a[o>>0]&1){n=Da+-16|0;a[c[n>>2]>>0]=0;k=Da+-20|0;c[k>>2]=0;b=a[o>>0]|0;if(!(b&1))j=10;else{j=c[o>>2]|0;b=j&255;j=(j&-2)+-1|0}if(!(b&1)){f=(b&255)>>>1;if((b&255)<22){h=10;m=1;l=f}else{h=(f+16&240)+-1|0;m=1;l=f}}else{h=10;m=0;l=0}if((h|0)!=(j|0)){if((h|0)==10){g=o+1|0;f=c[n>>2]|0;if(m){Fc(g|0,f|0,((b&255)>>>1)+1|0)|0;wc(f)}else{a[g>>0]=a[f>>0]|0;wc(f)}a[o>>0]=l<<1;break}f=h+1|0;g=vc(f)|0;if(!(h>>>0<=j>>>0&(g|0)==0)){if(m)Fc(g|0,o+1|0,((b&255)>>>1)+1|0)|0;else{bc=c[n>>2]|0;a[g>>0]=a[bc>>0]|0;wc(bc)}c[o>>2]=f|1;c[k>>2]=l;c[n>>2]=g}}}else{a[o+1>>0]=0;a[o>>0]=0}while(0);c[o>>2]=c[dc>>2];c[o+4>>2]=c[dc+4>>2];c[o+8>>2]=c[dc+8>>2];b=0;while(1){if((b|0)==3)break;c[dc+(b<<2)>>2]=0;b=b+1|0}Ja(dc);Ja(ec);Ja(cc);b=nb}break a}case 122:{if(((((ja|0)>2?(a[b>>0]|0)==97:0)?(a[b+1>>0]|0)==122:0)?(bc=b+2|0,ob=ub(bc,d,e)|0,(ob|0)!=(bc|0)):0)?(Ea=c[e+4>>2]|0,(c[e>>2]|0)!=(Ea|0)):0){o=Ea+-24|0;Cb(cc,o);b=Ta(cc,0,859)|0;c[ec>>2]=c[b>>2];c[ec+4>>2]=c[b+4>>2];c[ec+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}b=Ya(ec,799)|0;c[dc>>2]=c[b>>2];c[dc+4>>2]=c[b+4>>2];c[dc+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}do if(a[o>>0]&1){n=Ea+-16|0;a[c[n>>2]>>0]=0;k=Ea+-20|0;c[k>>2]=0;b=a[o>>0]|0;if(!(b&1))j=10;else{j=c[o>>2]|0;b=j&255;j=(j&-2)+-1|0}if(!(b&1)){f=(b&255)>>>1;if((b&255)<22){m=1;h=10;l=f}else{m=1;h=(f+16&240)+-1|0;l=f}}else{m=0;h=10;l=0}if((h|0)!=(j|0)){if((h|0)==10){g=o+1|0;f=c[n>>2]|0;if(m){Fc(g|0,f|0,((b&255)>>>1)+1|0)|0;wc(f)}else{a[g>>0]=a[f>>0]|0;wc(f)}a[o>>0]=l<<1;break}f=h+1|0;g=vc(f)|0;if(!(h>>>0<=j>>>0&(g|0)==0)){if(m)Fc(g|0,o+1|0,((b&255)>>>1)+1|0)|0;else{bc=c[n>>2]|0;a[g>>0]=a[bc>>0]|0;wc(bc)}c[o>>2]=f|1;c[k>>2]=l;c[n>>2]=g}}}else{a[o+1>>0]=0;a[o>>0]=0}while(0);c[o>>2]=c[dc>>2];c[o+4>>2]=c[dc+4>>2];c[o+8>>2]=c[dc+8>>2];b=0;while(1){if((b|0)==3)break;c[dc+(b<<2)>>2]=0;b=b+1|0}Ja(dc);Ja(ec);Ja(cc);b=ob}break a}default:break a}case 99:switch(a[la+1>>0]|0){case 99:{if((((((ja|0)>2?(a[b>>0]|0)==99:0)?(a[b+1>>0]|0)==99:0)?(Zb=b+2|0,z=Na(Zb,d,e)|0,(z|0)!=(Zb|0)):0)?(Xa=ub(z,d,e)|0,(Xa|0)!=(z|0)):0)?(ua=e+4|0,A=c[ua>>2]|0,((A-(c[e>>2]|0)|0)/24|0)>>>0>=2):0){Cb(dc,A+-24|0);b=c[ua>>2]|0;f=b+-24|0;g=b;while(1){if((g|0)==(f|0))break;e=g+-24|0;c[ua>>2]=e;Ia(e);g=c[ua>>2]|0}g=b+-48|0;Cb(ac,g);b=Ta(ac,0,869)|0;c[_b>>2]=c[b>>2];c[_b+4>>2]=c[b+4>>2];c[_b+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}b=Ya(_b,881)|0;c[$b>>2]=c[b>>2];c[$b+4>>2]=c[b+4>>2];c[$b+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}b=a[dc>>0]|0;f=(b&1)==0;b=Za($b,f?dc+1|0:c[dc+8>>2]|0,f?(b&255)>>>1:c[dc+4>>2]|0)|0;c[bc>>2]=c[b>>2];c[bc+4>>2]=c[b+4>>2];c[bc+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}b=Ya(bc,799)|0;c[cc>>2]=c[b>>2];c[cc+4>>2]=c[b+4>>2];c[cc+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}rb(ec,cc);Db(g,ec);Ia(ec);Ja(cc);Ja(bc);Ja($b);Ja(_b);Ja(ac);Ja(dc);b=Xa}break a}case 108:{b:do if((((ka?(a[b>>0]|0)==99:0)?(a[b+1>>0]|0)==108:0)?(cc=b+2|0,pb=ub(cc,d,e)|0,!((pb|0)==(cc|0)|(pb|0)==(d|0))):0)?(Rb=e+4|0,E=c[Rb>>2]|0,(c[e>>2]|0)!=(E|0)):0){cc=E+-12|0;g=a[cc>>0]|0;f=(g&1)==0;Za(E+-24|0,f?cc+1|0:c[E+-4>>2]|0,f?(g&255)>>>1:c[E+-8>>2]|0)|0;g=c[Rb>>2]|0;f=0;while(1){if((f|0)==3)break;c[dc+(f<<2)>>2]=0;f=f+1|0}p=g+-12|0;do if(a[p>>0]&1){o=g+-4|0;a[c[o>>2]>>0]=0;l=g+-8|0;c[l>>2]=0;f=a[p>>0]|0;if(!(f&1))k=10;else{k=c[p>>2]|0;f=k&255;k=(k&-2)+-1|0}if(!(f&1)){g=(f&255)>>>1;if((f&255)<22){n=1;j=10;m=g}else{n=1;j=(g+16&240)+-1|0;m=g}}else{n=0;j=10;m=0}if((j|0)!=(k|0)){if((j|0)==10){h=p+1|0;g=c[o>>2]|0;if(n){Fc(h|0,g|0,((f&255)>>>1)+1|0)|0;wc(g)}else{a[h>>0]=a[g>>0]|0;wc(g)}a[p>>0]=m<<1;break}g=j+1|0;h=vc(g)|0;if(!(j>>>0<=k>>>0&(h|0)==0)){if(n)Fc(h|0,p+1|0,((f&255)>>>1)+1|0)|0;else{cc=c[o>>2]|0;a[h>>0]=a[cc>>0]|0;wc(cc)}c[p>>2]=g|1;c[l>>2]=m;c[o>>2]=h}}}else{a[p+1>>0]=0;a[p>>0]=0}while(0);c[p>>2]=c[dc>>2];c[p+4>>2]=c[dc+4>>2];c[p+8>>2]=c[dc+8>>2];f=0;while(1){if((f|0)==3)break;c[dc+(f<<2)>>2]=0;f=f+1|0}Ja(dc);Ya((c[Rb>>2]|0)+-24|0,797)|0;l=ec+4|0;m=ec+8|0;n=ec+1|0;g=pb;while(1){if((a[g>>0]|0)==69)break;k=ub(g,d,e)|0;if((k|0)==(g|0)|(k|0)==(d|0))break b;f=c[Rb>>2]|0;if((c[e>>2]|0)==(f|0))break b;Cb(ec,f+-24|0);h=c[Rb>>2]|0;j=h+-24|0;f=h;while(1){if((f|0)==(j|0))break;dc=f+-24|0;c[Rb>>2]=dc;Ia(dc);f=c[Rb>>2]|0}g=a[ec>>0]|0;f=(g&1)==0;g=f?(g&255)>>>1:c[l>>2]|0;if(g){if((c[e>>2]|0)==(j|0)){Vb=147;break}Za(h+-48|0,f?n:c[m>>2]|0,g)|0}Ja(ec);g=k}if((Vb|0)==147){Ja(ec);break}f=c[Rb>>2]|0;if((c[e>>2]|0)!=(f|0)){Ya(f+-24|0,799)|0;b=g+1|0}}while(0);break a}case 109:{dc=b+2|0;$a(w,884,1);ec=Gb(dc,d,w,e)|0;Ja(w);b=(ec|0)==(dc|0)?b:ec;break a}case 111:{dc=b+2|0;$a(x,886,1);ec=Hb(dc,d,x,e)|0;Ja(x);b=(ec|0)==(dc|0)?b:ec;break a}case 118:{c:do if((((ja|0)>2?(a[b>>0]|0)==99:0)?(a[b+1>>0]|0)==118:0)?(Yb=e+63|0,Xb=a[Yb>>0]|0,a[Yb>>0]=0,Zb=b+2|0,ma=Na(Zb,d,e)|0,a[Yb>>0]=Xb,!((ma|0)==(Zb|0)|(ma|0)==(d|0))):0){if((a[ma>>0]|0)!=95){f=ub(ma,d,e)|0;if((f|0)==(ma|0))break}else{f=ma+1|0;if((f|0)==(d|0))break;g=a[f>>0]|0;d:do if(g<<24>>24==69){j=e+4|0;h=c[j>>2]|0;Zb=c[e+8>>2]|0;k=Zb;if(h>>>0<Zb>>>0){c[h>>2]=0;c[h+4>>2]=0;c[h+8>>2]=0;c[h+12>>2]=0;c[h+16>>2]=0;c[h+20>>2]=0;g=0;while(1){if((g|0)==3)break;c[h+(g<<2)>>2]=0;g=g+1|0}g=h+12|0;h=0;while(1){if((h|0)==3)break;c[g+(h<<2)>>2]=0;h=h+1|0}c[j>>2]=(c[j>>2]|0)+24;break}g=c[e>>2]|0;Zb=h-g|0;j=(Zb|0)/24|0;h=j+1|0;if((Zb|0)<-24)Pa();g=(k-g|0)/24|0;if(g>>>0<1073741823){g=g<<1;g=g>>>0<h>>>0?h:g}else g=2147483647;ab(dc,g,j,e+12|0);j=dc+8|0;k=c[j>>2]|0;c[k>>2]=0;c[k+4>>2]=0;c[k+8>>2]=0;c[k+12>>2]=0;c[k+16>>2]=0;c[k+20>>2]=0;g=0;while(1){if((g|0)==3)break;c[k+(g<<2)>>2]=0;g=g+1|0}g=k+12|0;h=0;while(1){if((h|0)==3)break;c[g+(h<<2)>>2]=0;h=h+1|0}c[j>>2]=k+24;cb(e,dc);bb(dc)}else while(1){if(g<<24>>24==69)break d;h=ub(f,d,e)|0;if((h|0)==(f|0)|(h|0)==(d|0))break c;g=a[h>>0]|0;f=h}while(0);f=f+1|0}j=e+4|0;g=c[j>>2]|0;if(((g-(c[e>>2]|0)|0)/24|0)>>>0>=2){Cb(dc,g+-24|0);b=c[j>>2]|0;g=b+-24|0;h=b;while(1){if((h|0)==(g|0))break;e=h+-24|0;c[j>>2]=e;Ia(e);h=c[j>>2]|0}h=b+-48|0;Cb(ac,h);b=Ta(ac,0,797)|0;c[_b>>2]=c[b>>2];c[_b+4>>2]=c[b+4>>2];c[_b+8>>2]=c[b+8>>2];g=0;while(1){if((g|0)==3)break;c[b+(g<<2)>>2]=0;g=g+1|0}b=Ya(_b,888)|0;c[$b>>2]=c[b>>2];c[$b+4>>2]=c[b+4>>2];c[$b+8>>2]=c[b+8>>2];g=0;while(1){if((g|0)==3)break;c[b+(g<<2)>>2]=0;g=g+1|0}b=a[dc>>0]|0;g=(b&1)==0;b=Za($b,g?dc+1|0:c[dc+8>>2]|0,g?(b&255)>>>1:c[dc+4>>2]|0)|0;c[bc>>2]=c[b>>2];c[bc+4>>2]=c[b+4>>2];c[bc+8>>2]=c[b+8>>2];g=0;while(1){if((g|0)==3)break;c[b+(g<<2)>>2]=0;g=g+1|0}b=Ya(bc,799)|0;c[cc>>2]=c[b>>2];c[cc+4>>2]=c[b+4>>2];c[cc+8>>2]=c[b+8>>2];g=0;while(1){if((g|0)==3)break;c[b+(g<<2)>>2]=0;g=g+1|0}rb(ec,cc);Db(h,ec);Ia(ec);Ja(cc);Ja(bc);Ja($b);Ja(_b);Ja(ac);Ja(dc);b=f}}while(0);break a}default:break a}case 100:switch(a[la+1>>0]|0){case 97:{ec=la+2|0;p=ub(ec,d,e)|0;if((p|0)==(ec|0))break a;g=e+4|0;h=c[g>>2]|0;if((c[e>>2]|0)==(h|0))break a;o=h+-24|0;e:do if(Ua)$a(Lb,891,2);else{b=0;while(1){if((b|0)==3)break e;c[Lb+(b<<2)>>2]=0;b=b+1|0}}while(0);b=Ya(Lb,894)|0;c[Kb>>2]=c[b>>2];c[Kb+4>>2]=c[b+4>>2];c[Kb+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}Cb(Mb,(c[g>>2]|0)+-24|0);b=a[Mb>>0]|0;f=(b&1)==0;b=Za(Kb,f?Mb+1|0:c[Mb+8>>2]|0,f?(b&255)>>>1:c[Mb+4>>2]|0)|0;c[Ib>>2]=c[b>>2];c[Ib+4>>2]=c[b+4>>2];c[Ib+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}do if(a[o>>0]&1){n=h+-16|0;a[c[n>>2]>>0]=0;k=h+-20|0;c[k>>2]=0;b=a[o>>0]|0;if(!(b&1))j=10;else{j=c[o>>2]|0;b=j&255;j=(j&-2)+-1|0}if(!(b&1)){f=(b&255)>>>1;if((b&255)<22){m=1;h=10;l=f}else{m=1;h=(f+16&240)+-1|0;l=f}}else{m=0;h=10;l=0}if((h|0)!=(j|0)){if((h|0)==10){g=o+1|0;f=c[n>>2]|0;if(m){Fc(g|0,f|0,((b&255)>>>1)+1|0)|0;wc(f)}else{a[g>>0]=a[f>>0]|0;wc(f)}a[o>>0]=l<<1;break}f=h+1|0;g=vc(f)|0;if(!(h>>>0<=j>>>0&(g|0)==0)){if(m)Fc(g|0,o+1|0,((b&255)>>>1)+1|0)|0;else{ec=c[n>>2]|0;a[g>>0]=a[ec>>0]|0;wc(ec)}c[o>>2]=f|1;c[k>>2]=l;c[n>>2]=g}}}else{a[o+1>>0]=0;a[o>>0]=0}while(0);c[o>>2]=c[Ib>>2];c[o+4>>2]=c[Ib+4>>2];c[o+8>>2]=c[Ib+8>>2];b=0;while(1){if((b|0)==3)break;c[Ib+(b<<2)>>2]=0;b=b+1|0}Ja(Ib);Ja(Mb);Ja(Kb);Ja(Lb);b=p;break a}case 99:{if((((((ja|0)>2?(a[b>>0]|0)==100:0)?(a[b+1>>0]|0)==99:0)?(Zb=b+2|0,Y=Na(Zb,d,e)|0,(Y|0)!=(Zb|0)):0)?(_a=ub(Y,d,e)|0,(_a|0)!=(Y|0)):0)?(va=e+4|0,Z=c[va>>2]|0,((Z-(c[e>>2]|0)|0)/24|0)>>>0>=2):0){Cb(dc,Z+-24|0);b=c[va>>2]|0;f=b+-24|0;g=b;while(1){if((g|0)==(f|0))break;e=g+-24|0;c[va>>2]=e;Ia(e);g=c[va>>2]|0}g=b+-48|0;Cb(ac,g);b=Ta(ac,0,904)|0;c[_b>>2]=c[b>>2];c[_b+4>>2]=c[b+4>>2];c[_b+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}b=Ya(_b,881)|0;c[$b>>2]=c[b>>2];c[$b+4>>2]=c[b+4>>2];c[$b+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}b=a[dc>>0]|0;f=(b&1)==0;b=Za($b,f?dc+1|0:c[dc+8>>2]|0,f?(b&255)>>>1:c[dc+4>>2]|0)|0;c[bc>>2]=c[b>>2];c[bc+4>>2]=c[b+4>>2];c[bc+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}b=Ya(bc,799)|0;c[cc>>2]=c[b>>2];c[cc+4>>2]=c[b+4>>2];c[cc+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}rb(ec,cc);Db(g,ec);Ia(ec);Ja(cc);Ja(bc);Ja($b);Ja(_b);Ja(ac);Ja(dc);b=_a}break a}case 101:{dc=b+2|0;$a(y,4262,1);ec=Hb(dc,d,y,e)|0;Ja(y);b=(ec|0)==(dc|0)?b:ec;break a}case 108:{ec=la+2|0;p=ub(ec,d,e)|0;if((p|0)==(ec|0))break a;g=e+4|0;h=c[g>>2]|0;if((c[e>>2]|0)==(h|0))break a;o=h+-24|0;f:do if(Ua)$a(Pb,891,2);else{b=0;while(1){if((b|0)==3)break f;c[Pb+(b<<2)>>2]=0;b=b+1|0}}while(0);b=Ya(Pb,918)|0;c[Ob>>2]=c[b>>2];c[Ob+4>>2]=c[b+4>>2];c[Ob+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}Cb(Qb,(c[g>>2]|0)+-24|0);b=a[Qb>>0]|0;f=(b&1)==0;b=Za(Ob,f?Qb+1|0:c[Qb+8>>2]|0,f?(b&255)>>>1:c[Qb+4>>2]|0)|0;c[Nb>>2]=c[b>>2];c[Nb+4>>2]=c[b+4>>2];c[Nb+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}do if(a[o>>0]&1){n=h+-16|0;a[c[n>>2]>>0]=0;k=h+-20|0;c[k>>2]=0;b=a[o>>0]|0;if(!(b&1))j=10;else{j=c[o>>2]|0;b=j&255;j=(j&-2)+-1|0}if(!(b&1)){f=(b&255)>>>1;if((b&255)<22){m=1;h=10;l=f}else{m=1;h=(f+16&240)+-1|0;l=f}}else{m=0;h=10;l=0}if((h|0)!=(j|0)){if((h|0)==10){g=o+1|0;f=c[n>>2]|0;if(m){Fc(g|0,f|0,((b&255)>>>1)+1|0)|0;wc(f)}else{a[g>>0]=a[f>>0]|0;wc(f)}a[o>>0]=l<<1;break}f=h+1|0;g=vc(f)|0;if(!(h>>>0<=j>>>0&(g|0)==0)){if(m)Fc(g|0,o+1|0,((b&255)>>>1)+1|0)|0;else{ec=c[n>>2]|0;a[g>>0]=a[ec>>0]|0;wc(ec)}c[o>>2]=f|1;c[k>>2]=l;c[n>>2]=g}}}else{a[o+1>>0]=0;a[o>>0]=0}while(0);c[o>>2]=c[Nb>>2];c[o+4>>2]=c[Nb+4>>2];c[o+8>>2]=c[Nb+8>>2];b=0;while(1){if((b|0)==3)break;c[Nb+(b<<2)>>2]=0;b=b+1|0}Ja(Nb);Ja(Qb);Ja(Ob);Ja(Pb);b=p;break a}case 110:{b=Jb(b,d,e)|0;break a}case 115:{if((((((ja|0)>2?(a[b>>0]|0)==100:0)?(a[b+1>>0]|0)==115:0)?(cc=b+2|0,ba=ub(cc,d,e)|0,(ba|0)!=(cc|0)):0)?(wa=ub(ba,d,e)|0,(wa|0)!=(ba|0)):0)?(xa=e+4|0,ca=c[xa>>2]|0,((ca-(c[e>>2]|0)|0)/24|0)>>>0>=2):0){Cb(dc,ca+-24|0);b=c[xa>>2]|0;f=b+-24|0;g=b;while(1){if((g|0)==(f|0))break;cc=g+-24|0;c[xa>>2]=cc;Ia(cc);g=c[xa>>2]|0}xb(ec,1833,dc);cc=a[ec>>0]|0;bc=(cc&1)==0;Za(b+-48|0,bc?ec+1|0:c[ec+8>>2]|0,bc?(cc&255)>>>1:c[ec+4>>2]|0)|0;Ja(ec);Ja(dc);b=wa}break a}case 116:{if((((((ja|0)>2?(a[b>>0]|0)==100:0)?(a[b+1>>0]|0)==116:0)?(cc=b+2|0,ea=ub(cc,d,e)|0,(ea|0)!=(cc|0)):0)?(ya=Jb(ea,d,e)|0,(ya|0)!=(ea|0)):0)?(za=e+4|0,ga=c[za>>2]|0,((ga-(c[e>>2]|0)|0)/24|0)>>>0>=2):0){Cb(dc,ga+-24|0);b=c[za>>2]|0;f=b+-24|0;g=b;while(1){if((g|0)==(f|0))break;cc=g+-24|0;c[za>>2]=cc;Ia(cc);g=c[za>>2]|0}xb(ec,4798,dc);cc=a[ec>>0]|0;bc=(cc&1)==0;Za(b+-48|0,bc?ec+1|0:c[ec+8>>2]|0,bc?(cc&255)>>>1:c[ec+4>>2]|0)|0;Ja(ec);Ja(dc);b=ya}break a}case 118:{dc=b+2|0;$a(B,2368,1);ec=Gb(dc,d,B,e)|0;Ja(B);b=(ec|0)==(dc|0)?b:ec;break a}case 86:{dc=b+2|0;$a(C,1836,2);ec=Gb(dc,d,C,e)|0;Ja(C);b=(ec|0)==(dc|0)?b:ec;break a}default:break a}case 101:switch(a[la+1>>0]|0){case 111:{dc=b+2|0;$a(D,1839,1);ec=Gb(dc,d,D,e)|0;Ja(D);b=(ec|0)==(dc|0)?b:ec;break a}case 79:{dc=b+2|0;$a(F,1841,2);ec=Gb(dc,d,F,e)|0;Ja(F);b=(ec|0)==(dc|0)?b:ec;break a}case 113:{dc=b+2|0;$a(G,1844,2);ec=Gb(dc,d,G,e)|0;Ja(G);b=(ec|0)==(dc|0)?b:ec;break a}default:break a}case 103:switch(a[la+1>>0]|0){case 101:{dc=b+2|0;$a(H,1847,2);ec=Gb(dc,d,H,e)|0;Ja(H);b=(ec|0)==(dc|0)?b:ec;break a}case 116:{dc=b+2|0;$a(I,844,1);ec=Gb(dc,d,I,e)|0;Ja(I);b=(ec|0)==(dc|0)?b:ec;break a}default:break a}case 105:{if((a[la+1>>0]|0)!=120)break a;cc=b+2|0;f=ub(cc,d,e)|0;if((f|0)==(cc|0))break a;j=ub(f,d,e)|0;h=e+4|0;if((j|0)==(f|0)){g=c[h>>2]|0;f=g+-24|0;while(1){if((g|0)==(f|0))break a;ec=g+-24|0;c[h>>2]=ec;Ia(ec);g=c[h>>2]|0}}f=c[h>>2]|0;if(((f-(c[e>>2]|0)|0)/24|0)>>>0<2)break a;Cb(dc,f+-24|0);b=c[h>>2]|0;f=b+-24|0;g=b;while(1){if((g|0)==(f|0))break;cc=g+-24|0;c[h>>2]=cc;Ia(cc);g=c[h>>2]|0}Cb(ec,b+-48|0);g=(c[h>>2]|0)+-24|0;xb(kb,797,ec);b=Ya(kb,1850)|0;c[jb>>2]=c[b>>2];c[jb+4>>2]=c[b+4>>2];c[jb+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}b=a[dc>>0]|0;f=(b&1)==0;b=Za(jb,f?dc+1|0:c[dc+8>>2]|0,f?(b&255)>>>1:c[dc+4>>2]|0)|0;c[hb>>2]=c[b>>2];c[hb+4>>2]=c[b+4>>2];c[hb+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}b=Ya(hb,4264)|0;c[gb>>2]=c[b>>2];c[gb+4>>2]=c[b+4>>2];c[gb+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}rb(fb,gb);Db(g,fb);Ia(fb);Ja(gb);Ja(hb);Ja(jb);Ja(kb);Ja(ec);Ja(dc);b=j;break a}case 108:switch(a[la+1>>0]|0){case 101:{dc=b+2|0;$a(J,1853,2);ec=Gb(dc,d,J,e)|0;Ja(J);b=(ec|0)==(dc|0)?b:ec;break a}case 115:{dc=b+2|0;$a(K,1856,2);ec=Gb(dc,d,K,e)|0;Ja(K);b=(ec|0)==(dc|0)?b:ec;break a}case 83:{dc=b+2|0;$a(L,1859,3);ec=Gb(dc,d,L,e)|0;Ja(L);b=(ec|0)==(dc|0)?b:ec;break a}case 116:{dc=b+2|0;$a(M,1427,1);ec=Gb(dc,d,M,e)|0;Ja(M);b=(ec|0)==(dc|0)?b:ec;break a}default:break a}case 109:switch(a[la+1>>0]|0){case 105:{dc=b+2|0;$a(N,1863,1);ec=Gb(dc,d,N,e)|0;Ja(N);b=(ec|0)==(dc|0)?b:ec;break a}case 73:{dc=b+2|0;$a(O,1865,2);ec=Gb(dc,d,O,e)|0;Ja(O);b=(ec|0)==(dc|0)?b:ec;break a}case 108:{dc=b+2|0;$a(P,4262,1);ec=Gb(dc,d,P,e)|0;Ja(P);b=(ec|0)==(dc|0)?b:ec;break a}case 76:{dc=b+2|0;$a(Q,1868,2);ec=Gb(dc,d,Q,e)|0;Ja(Q);b=(ec|0)==(dc|0)?b:ec;break a}case 109:{f=b+2|0;if((f|0)!=(d|0)?(a[f>>0]|0)==95:0){dc=b+3|0;$a(na,1871,2);ec=Hb(dc,d,na,e)|0;Ja(na);b=(ec|0)==(dc|0)?b:ec;break a}h=ub(f,d,e)|0;if((h|0)==(f|0))break a;f=c[e+4>>2]|0;if((c[e>>2]|0)==(f|0))break a;g=f+-24|0;Cb(La,g);b=Ta(La,0,797)|0;c[Ka>>2]=c[b>>2];c[Ka+4>>2]=c[b+4>>2];c[Ka+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}b=Ya(Ka,1874)|0;c[Ha>>2]=c[b>>2];c[Ha+4>>2]=c[b+4>>2];c[Ha+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}rb(Ga,Ha);Db(g,Ga);Ia(Ga);Ja(Ha);Ja(Ka);Ja(La);b=h;break a}default:break a}case 110:switch(a[la+1>>0]|0){case 119:case 97:{g:do if(ka){f=a[b>>0]|0;if(f<<24>>24==103){s=(a[b+1>>0]|0)==115;g=s?b+2|0:b;f=a[g>>0]|0}else{s=0;g=b}if(f<<24>>24==110){f=a[g+1>>0]|0;switch(f<<24>>24){case 97:case 119:break;default:break g}q=f<<24>>24==97;f=g+2|0;h:do if((f|0)!=(d|0)){p=0;while(1){if((a[f>>0]|0)==95)break;h=ub(f,d,e)|0;f=(h|0)==(f|0);g=(h|0)==(d|0);if(f|g)break h;else{p=p|(f|g)^1;f=h}}Rb=f+1|0;g=Na(Rb,d,e)|0;if(!((g|0)==(Rb|0)|(g|0)==(d|0))){f=a[g>>0]|0;i:do if(!((lb-g|0)>2&f<<24>>24==112))if(f<<24>>24==69){o=0;r=g}else break h;else{if((a[g+1>>0]|0)!=105)break h;f=g+2|0;while(1){if((a[f>>0]|0)==69){o=1;r=f;break i}Rb=f;f=ub(f,d,e)|0;if((f|0)==(Rb|0)|(f|0)==(d|0))break h}}while(0);f=0;while(1){if((f|0)==3)break;c[ec+(f<<2)>>2]=0;f=f+1|0}j:do if(o){n=e+4|0;f=c[n>>2]|0;if((c[e>>2]|0)==(f|0)){g=b;f=1}else{Cb(cc,f+-24|0);k:do if(!(a[ec>>0]&1)){a[ec+1>>0]=0;a[ec>>0]=0}else{k=ec+8|0;g=c[k>>2]|0;a[g>>0]=0;l=ec+4|0;c[l>>2]=0;f=c[ec>>2]|0;m=(f&-2)+-1|0;h=f&255;do if(!(h&1)){f=f>>>1&127;if((h&255)<22){Fc(ec+1|0,g|0,f+1|0)|0;wc(g);break}g=f+16&240;j=g+-1|0;if((j|0)==(m|0))break k;h=vc(g)|0;if(j>>>0<=m>>>0&(h|0)==0)break k;Fc(h|0,ec+1|0,f+1|0)|0;c[ec>>2]=g|1;c[l>>2]=f;c[k>>2]=h;break k}else{a[ec+1>>0]=0;wc(g);f=0}while(0);a[ec>>0]=f<<1}while(0);c[ec>>2]=c[cc>>2];c[ec+4>>2]=c[cc+4>>2];c[ec+8>>2]=c[cc+8>>2];f=0;while(1){if((f|0)==3)break;c[cc+(f<<2)>>2]=0;f=f+1|0}Ja(cc);f=c[n>>2]|0;g=f+-24|0;while(1){if((f|0)==(g|0)){j=e;f=g;Vb=409;break j}cc=f+-24|0;c[n>>2]=cc;Ia(cc);f=c[n>>2]|0}}}else{f=e+4|0;n=f;j=e;f=c[f>>2]|0;Vb=409}while(0);if((Vb|0)==409)if((c[j>>2]|0)==(f|0)){g=b;f=1}else{Cb(bc,f+-24|0);g=c[n>>2]|0;h=g+-24|0;f=g;while(1){if((f|0)==(h|0))break;cc=f+-24|0;c[n>>2]=cc;Ia(cc);f=c[n>>2]|0}f=0;while(1){if((f|0)==3)break;c[$b+(f<<2)>>2]=0;f=f+1|0}l:do if(p)if((c[j>>2]|0)==(h|0)){g=b;f=1}else{Cb(_b,g+-48|0);m:do if(!(a[$b>>0]&1)){a[$b+1>>0]=0;a[$b>>0]=0}else{k=$b+8|0;g=c[k>>2]|0;a[g>>0]=0;l=$b+4|0;c[l>>2]=0;f=c[$b>>2]|0;m=(f&-2)+-1|0;h=f&255;do if(!(h&1)){f=f>>>1&127;if((h&255)<22){Fc($b+1|0,g|0,f+1|0)|0;wc(g);break}g=f+16&240;j=g+-1|0;if((j|0)==(m|0))break m;h=vc(g)|0;if(j>>>0<=m>>>0&(h|0)==0)break m;Fc(h|0,$b+1|0,f+1|0)|0;c[$b>>2]=g|1;c[l>>2]=f;c[k>>2]=h;break m}else{a[$b+1>>0]=0;wc(g);f=0}while(0);a[$b>>0]=f<<1}while(0);c[$b>>2]=c[_b>>2];c[$b+4>>2]=c[_b+4>>2];c[$b+8>>2]=c[_b+8>>2];f=0;while(1){if((f|0)==3)break;c[_b+(f<<2)>>2]=0;f=f+1|0}Ja(_b);g=c[n>>2]|0;f=g+-24|0;while(1){if((g|0)==(f|0)){Vb=434;break l}cc=g+-24|0;c[n>>2]=cc;Ia(cc);g=c[n>>2]|0}}else Vb=434;while(0);if((Vb|0)==434){f=0;while(1){if((f|0)==3)break;c[ac+(f<<2)>>2]=0;f=f+1|0}if(s)Ub(ac,891,2);if(q)Ya(ac,1878)|0;else Ya(ac,1882)|0;if(p){xb(Tb,797,$b);f=Ya(Tb,846)|0;c[Sb>>2]=c[f>>2];c[Sb+4>>2]=c[f+4>>2];c[Sb+8>>2]=c[f+8>>2];g=0;while(1){if((g|0)==3)break;c[f+(g<<2)>>2]=0;g=g+1|0}cc=a[Sb>>0]|0;_b=(cc&1)==0;Za(ac,_b?Sb+1|0:c[Sb+8>>2]|0,_b?(cc&255)>>>1:c[Sb+4>>2]|0)|0;Ja(Sb);Ja(Tb)}cc=a[bc>>0]|0;_b=(cc&1)==0;Za(ac,_b?bc+1|0:c[bc+8>>2]|0,_b?(cc&255)>>>1:c[bc+4>>2]|0)|0;if(o){xb(Wb,849,ec);f=Ya(Wb,799)|0;c[Xb>>2]=c[f>>2];c[Xb+4>>2]=c[f+4>>2];c[Xb+8>>2]=c[f+8>>2];g=0;while(1){if((g|0)==3)break;c[f+(g<<2)>>2]=0;g=g+1|0}cc=a[Xb>>0]|0;_b=(cc&1)==0;Za(ac,_b?Xb+1|0:c[Xb+8>>2]|0,_b?(cc&255)>>>1:c[Xb+4>>2]|0)|0;Ja(Xb);Ja(Wb)};c[Zb>>2]=c[ac>>2];c[Zb+4>>2]=c[ac+4>>2];c[Zb+8>>2]=c[ac+8>>2];f=0;while(1){if((f|0)==3)break;c[ac+(f<<2)>>2]=0;f=f+1|0}rb(Yb,Zb);f=c[n>>2]|0;cc=c[e+8>>2]|0;j=cc;if(f>>>0<cc>>>0){db(f,Yb);c[n>>2]=(c[n>>2]|0)+24}else{g=c[e>>2]|0;cc=f-g|0;k=(cc|0)/24|0;h=k+1|0;if((cc|0)<-24)Pa();f=(j-g|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<h>>>0?h:f}else f=2147483647;ab(dc,f,k,e+12|0);cc=dc+8|0;_b=c[cc>>2]|0;db(_b,Yb);c[cc>>2]=_b+24;cb(e,dc);bb(dc)}Ia(Yb);Ja(Zb);Ja(ac);g=r+1|0;f=0}Ja($b);Ja(bc)}Ja(ec);if(!f){b=g;break g}}}while(0)}}while(0);break a}case 101:{dc=b+2|0;$a(R,1884,2);ec=Gb(dc,d,R,e)|0;Ja(R);b=(ec|0)==(dc|0)?b:ec;break a}case 103:{dc=b+2|0;$a(S,1863,1);ec=Hb(dc,d,S,e)|0;Ja(S);b=(ec|0)==(dc|0)?b:ec;break a}case 116:{dc=b+2|0;$a(T,1887,1);ec=Hb(dc,d,T,e)|0;Ja(T);b=(ec|0)==(dc|0)?b:ec;break a}case 120:{r=b+2|0;f=ub(r,d,e)|0;if((f|0)!=(r|0)?(Fa=c[e+4>>2]|0,(c[e>>2]|0)!=(Fa|0)):0){q=Fa+-24|0;Cb(cc,q);g=Ta(cc,0,1889)|0;c[ec>>2]=c[g>>2];c[ec+4>>2]=c[g+4>>2];c[ec+8>>2]=c[g+8>>2];h=0;while(1){if((h|0)==3)break;c[g+(h<<2)>>2]=0;h=h+1|0}g=Ya(ec,799)|0;c[dc>>2]=c[g>>2];c[dc+4>>2]=c[g+4>>2];c[dc+8>>2]=c[g+8>>2];h=0;while(1){if((h|0)==3)break;c[g+(h<<2)>>2]=0;h=h+1|0}do if(a[q>>0]&1){p=Fa+-16|0;a[c[p>>2]>>0]=0;m=Fa+-20|0;c[m>>2]=0;g=a[q>>0]|0;if(!(g&1))l=10;else{l=c[q>>2]|0;g=l&255;l=(l&-2)+-1|0}if(!(g&1)){h=(g&255)>>>1;if((g&255)<22){k=10;n=h;o=1}else{k=(h+16&240)+-1|0;n=h;o=1}}else{k=10;n=0;o=0}if((k|0)!=(l|0)){if((k|0)==10){j=q+1|0;h=c[p>>2]|0;if(o){Fc(j|0,h|0,((g&255)>>>1)+1|0)|0;wc(h)}else{a[j>>0]=a[h>>0]|0;wc(h)}a[q>>0]=n<<1;break}h=k+1|0;j=vc(h)|0;if(!(k>>>0<=l>>>0&(j|0)==0)){if(o)Fc(j|0,q+1|0,((g&255)>>>1)+1|0)|0;else{bc=c[p>>2]|0;a[j>>0]=a[bc>>0]|0;wc(bc)}c[q>>2]=h|1;c[m>>2]=n;c[p>>2]=j}}}else{a[q+1>>0]=0;a[q>>0]=0}while(0);c[q>>2]=c[dc>>2];c[q+4>>2]=c[dc+4>>2];c[q+8>>2]=c[dc+8>>2];g=0;while(1){if((g|0)==3)break;c[dc+(g<<2)>>2]=0;g=g+1|0}Ja(dc);Ja(ec);Ja(cc)}else f=r;b=(f|0)==(r|0)?b:f;break a}default:break a}case 111:switch(a[la+1>>0]|0){case 110:{b=Jb(b,d,e)|0;break a}case 111:{dc=b+2|0;$a(U,1900,2);ec=Gb(dc,d,U,e)|0;Ja(U);b=(ec|0)==(dc|0)?b:ec;break a}case 114:{dc=b+2|0;$a(V,1903,1);ec=Gb(dc,d,V,e)|0;Ja(V);b=(ec|0)==(dc|0)?b:ec;break a}case 82:{dc=b+2|0;$a(W,1905,2);ec=Gb(dc,d,W,e)|0;Ja(W);b=(ec|0)==(dc|0)?b:ec;break a}default:break a}case 112:switch(a[la+1>>0]|0){case 109:{dc=b+2|0;$a(X,1908,3);ec=Gb(dc,d,X,e)|0;Ja(X);b=(ec|0)==(dc|0)?b:ec;break a}case 108:{dc=b+2|0;$a(_,1912,1);ec=Gb(dc,d,_,e)|0;Ja(_);b=(ec|0)==(dc|0)?b:ec;break a}case 76:{dc=b+2|0;$a($,1914,2);ec=Gb(dc,d,$,e)|0;Ja($);b=(ec|0)==(dc|0)?b:ec;break a}case 112:{f=b+2|0;if((f|0)!=(d|0)?(a[f>>0]|0)==95:0){dc=b+3|0;$a(oa,1917,2);ec=Hb(dc,d,oa,e)|0;Ja(oa);b=(ec|0)==(dc|0)?b:ec;break a}h=ub(f,d,e)|0;if((h|0)==(f|0))break a;f=c[e+4>>2]|0;if((c[e>>2]|0)==(f|0))break a;g=f+-24|0;Cb(Ra,g);b=Ta(Ra,0,797)|0;c[Qa>>2]=c[b>>2];c[Qa+4>>2]=c[b+4>>2];c[Qa+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}b=Ya(Qa,1920)|0;c[Oa>>2]=c[b>>2];c[Oa+4>>2]=c[b+4>>2];c[Oa+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}rb(Ma,Oa);Db(g,Ma);Ia(Ma);Ja(Oa);Ja(Qa);Ja(Ra);b=h;break a}case 115:{dc=b+2|0;$a(aa,1912,1);ec=Hb(dc,d,aa,e)|0;Ja(aa);b=(ec|0)==(dc|0)?b:ec;break a}case 116:{if((ja|0)<=2)break a;if((a[b>>0]|0)!=112)break a;if((a[b+1>>0]|0)!=116)break a;ec=b+2|0;f=ub(ec,d,e)|0;if((f|0)==(ec|0))break a;j=ub(f,d,e)|0;if((j|0)==(f|0))break a;h=e+4|0;f=c[h>>2]|0;if(((f-(c[e>>2]|0)|0)/24|0)>>>0<2)break a;Cb(dc,f+-24|0);b=c[h>>2]|0;f=b+-24|0;g=b;while(1){if((g|0)==(f|0))break;ec=g+-24|0;c[h>>2]=ec;Ia(ec);g=c[h>>2]|0}Ya(b+-48|0,1924)|0;b=a[dc>>0]|0;ec=(b&1)==0;Za((c[h>>2]|0)+-24|0,ec?dc+1|0:c[dc+8>>2]|0,ec?(b&255)>>>1:c[dc+4>>2]|0)|0;Ja(dc);b=j;break a}default:break a}case 113:{if((a[la+1>>0]|0)!=117)break a;bc=b+2|0;f=ub(bc,d,e)|0;if((f|0)==(bc|0))break a;g=ub(f,d,e)|0;if((g|0)==(f|0)){f=e+4|0;h=c[f>>2]|0;g=h+-24|0;while(1){if((h|0)==(g|0))break a;ec=h+-24|0;c[f>>2]=ec;Ia(ec);h=c[f>>2]|0}}h=ub(g,d,e)|0;j=e+4|0;if((h|0)==(g|0)){g=c[j>>2]|0;f=g+-24|0;h=g;while(1){if((h|0)==(f|0))break;ec=h+-24|0;c[j>>2]=ec;Ia(ec);h=c[j>>2]|0}g=g+-48|0;while(1){if((f|0)==(g|0))break a;ec=f+-24|0;c[j>>2]=ec;Ia(ec);f=c[j>>2]|0}}f=c[j>>2]|0;if(((f-(c[e>>2]|0)|0)/24|0)>>>0<3)break a;Cb(dc,f+-24|0);b=c[j>>2]|0;f=b+-24|0;g=b;while(1){if((g|0)==(f|0))break;bc=g+-24|0;c[j>>2]=bc;Ia(bc);g=c[j>>2]|0}Cb(ec,b+-48|0);b=c[j>>2]|0;f=b+-24|0;g=b;while(1){if((g|0)==(f|0))break;bc=g+-24|0;c[j>>2]=bc;Ia(bc);g=c[j>>2]|0}Cb(cc,b+-48|0);g=(c[j>>2]|0)+-24|0;xb(Bb,797,cc);b=Ya(Bb,1927)|0;c[Ab>>2]=c[b>>2];c[Ab+4>>2]=c[b+4>>2];c[Ab+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}b=a[ec>>0]|0;f=(b&1)==0;b=Za(Ab,f?ec+1|0:c[ec+8>>2]|0,f?(b&255)>>>1:c[ec+4>>2]|0)|0;c[zb>>2]=c[b>>2];c[zb+4>>2]=c[b+4>>2];c[zb+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}b=Ya(zb,1933)|0;c[yb>>2]=c[b>>2];c[yb+4>>2]=c[b+4>>2];c[yb+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}b=a[dc>>0]|0;f=(b&1)==0;b=Za(yb,f?dc+1|0:c[dc+8>>2]|0,f?(b&255)>>>1:c[dc+4>>2]|0)|0;c[wb>>2]=c[b>>2];c[wb+4>>2]=c[b+4>>2];c[wb+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}b=Ya(wb,799)|0;c[tb>>2]=c[b>>2];c[tb+4>>2]=c[b+4>>2];c[tb+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}rb(sb,tb);Db(g,sb);Ia(sb);Ja(tb);Ja(wb);Ja(yb);Ja(zb);Ja(Ab);Ja(Bb);Ja(cc);Ja(ec);Ja(dc);b=h;break a}case 114:switch(a[la+1>>0]|0){case 99:{if((((((ja|0)>2?(a[b>>0]|0)==114:0)?(a[b+1>>0]|0)==99:0)?(Zb=b+2|0,h=Na(Zb,d,e)|0,(h|0)!=(Zb|0)):0)?(Va=ub(h,d,e)|0,(Va|0)!=(h|0)):0)?(pa=e+4|0,j=c[pa>>2]|0,((j-(c[e>>2]|0)|0)/24|0)>>>0>=2):0){Cb(dc,j+-24|0);b=c[pa>>2]|0;f=b+-24|0;g=b;while(1){if((g|0)==(f|0))break;e=g+-24|0;c[pa>>2]=e;Ia(e);g=c[pa>>2]|0}g=b+-48|0;Cb(ac,g);b=Ta(ac,0,1939)|0;c[_b>>2]=c[b>>2];c[_b+4>>2]=c[b+4>>2];c[_b+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}b=Ya(_b,881)|0;c[$b>>2]=c[b>>2];c[$b+4>>2]=c[b+4>>2];c[$b+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}b=a[dc>>0]|0;f=(b&1)==0;b=Za($b,f?dc+1|0:c[dc+8>>2]|0,f?(b&255)>>>1:c[dc+4>>2]|0)|0;c[bc>>2]=c[b>>2];c[bc+4>>2]=c[b+4>>2];c[bc+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}b=Ya(bc,799)|0;c[cc>>2]=c[b>>2];c[cc+4>>2]=c[b+4>>2];c[cc+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}rb(ec,cc);Db(g,ec);Ia(ec);Ja(cc);Ja(bc);Ja($b);Ja(_b);Ja(ac);Ja(dc);b=Va}break a}case 109:{dc=b+2|0;$a(da,1957,1);ec=Gb(dc,d,da,e)|0;Ja(da);b=(ec|0)==(dc|0)?b:ec;break a}case 77:{dc=b+2|0;$a(fa,1959,2);ec=Gb(dc,d,fa,e)|0;Ja(fa);b=(ec|0)==(dc|0)?b:ec;break a}case 115:{dc=b+2|0;$a(ha,1962,2);ec=Gb(dc,d,ha,e)|0;Ja(ha);b=(ec|0)==(dc|0)?b:ec;break a}case 83:{dc=b+2|0;$a(ia,1965,3);ec=Gb(dc,d,ia,e)|0;Ja(ia);b=(ec|0)==(dc|0)?b:ec;break a}default:break a}case 115:switch(a[la+1>>0]|0){case 99:{if((((((ja|0)>2?(a[b>>0]|0)==115:0)?(a[b+1>>0]|0)==99:0)?(Zb=b+2|0,l=Na(Zb,d,e)|0,(l|0)!=(Zb|0)):0)?(Wa=ub(l,d,e)|0,(Wa|0)!=(l|0)):0)?(qa=e+4|0,m=c[qa>>2]|0,((m-(c[e>>2]|0)|0)/24|0)>>>0>=2):0){Cb(dc,m+-24|0);b=c[qa>>2]|0;f=b+-24|0;g=b;while(1){if((g|0)==(f|0))break;e=g+-24|0;c[qa>>2]=e;Ia(e);g=c[qa>>2]|0}g=b+-48|0;Cb(ac,g);b=Ta(ac,0,1969)|0;c[_b>>2]=c[b>>2];c[_b+4>>2]=c[b+4>>2];c[_b+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}b=Ya(_b,881)|0;c[$b>>2]=c[b>>2];c[$b+4>>2]=c[b+4>>2];c[$b+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}b=a[dc>>0]|0;f=(b&1)==0;b=Za($b,f?dc+1|0:c[dc+8>>2]|0,f?(b&255)>>>1:c[dc+4>>2]|0)|0;c[bc>>2]=c[b>>2];c[bc+4>>2]=c[b+4>>2];c[bc+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}b=Ya(bc,799)|0;c[cc>>2]=c[b>>2];c[cc+4>>2]=c[b+4>>2];c[cc+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}rb(ec,cc);Db(g,ec);Ia(ec);Ja(cc);Ja(bc);Ja($b);Ja(_b);Ja(ac);Ja(dc);b=Wa}break a}case 112:{if((ja|0)<=2)break a;if((a[b>>0]|0)!=115)break a;if((a[b+1>>0]|0)!=112)break a;dc=b+2|0;ec=ub(dc,d,e)|0;b=(ec|0)==(dc|0)?b:ec;break a}case 114:{b=Jb(b,d,e)|0;break a}case 116:{if(((((ja|0)>2?(a[b>>0]|0)==115:0)?(a[b+1>>0]|0)==116:0)?(ac=b+2|0,Aa=Na(ac,d,e)|0,(Aa|0)!=(ac|0)):0)?(o=c[e+4>>2]|0,(c[e>>2]|0)!=(o|0)):0){g=o+-24|0;Cb(bc,g);b=Ta(bc,0,1982)|0;c[cc>>2]=c[b>>2];c[cc+4>>2]=c[b+4>>2];c[cc+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}b=Ya(cc,799)|0;c[ec>>2]=c[b>>2];c[ec+4>>2]=c[b+4>>2];c[ec+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}rb(dc,ec);Db(g,dc);Ia(dc);Ja(ec);Ja(cc);Ja(bc);b=Aa}break a}case 122:{if(((((ja|0)>2?(a[b>>0]|0)==115:0)?(a[b+1>>0]|0)==122:0)?(ac=b+2|0,Ba=ub(ac,d,e)|0,(Ba|0)!=(ac|0)):0)?(p=c[e+4>>2]|0,(c[e>>2]|0)!=(p|0)):0){g=p+-24|0;Cb(bc,g);b=Ta(bc,0,1982)|0;c[cc>>2]=c[b>>2];c[cc+4>>2]=c[b+4>>2];c[cc+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}b=Ya(cc,799)|0;c[ec>>2]=c[b>>2];c[ec+4>>2]=c[b+4>>2];c[ec+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}rb(dc,ec);Db(g,dc);Ia(dc);Ja(ec);Ja(cc);Ja(bc);b=Ba}break a}case 90:{if((lb-la|0)<=2)break a;switch(a[la+2>>0]|0){case 84:break;case 102:{if((((((ja|0)>2?(a[b>>0]|0)==115:0)?(a[b+1>>0]|0)==90:0)?(s=b+2|0,(a[s>>0]|0)==102):0)?(Ca=Fb(s,d,e)|0,(Ca|0)!=(s|0)):0)?(t=c[e+4>>2]|0,(c[e>>2]|0)!=(t|0)):0){g=t+-24|0;Cb(bc,g);b=Ta(bc,0,1991)|0;c[cc>>2]=c[b>>2];c[cc+4>>2]=c[b+4>>2];c[cc+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}b=Ya(cc,799)|0;c[ec>>2]=c[b>>2];c[ec+4>>2]=c[b+4>>2];c[ec+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}rb(dc,ec);Db(g,dc);Ia(dc);Ja(ec);Ja(cc);Ja(bc);b=Ca}break a}default:break a}if(((((ja|0)>2?(a[b>>0]|0)==115:0)?(a[b+1>>0]|0)==90:0)?(q=b+2|0,(a[q>>0]|0)==84):0)?(mb=e+4|0,eb=((c[mb>>2]|0)-(c[e>>2]|0)|0)/24|0,qb=Eb(q,d,e)|0,ra=c[e>>2]|0,g=((c[mb>>2]|0)-ra|0)/24|0,ra,(qb|0)!=(q|0)):0){a[ec>>0]=20;b=ec+1|0;f=1991;h=b+10|0;do{a[b>>0]=a[f>>0]|0;b=b+1|0;f=f+1|0}while((b|0)<(h|0));a[ec+11>>0]=0;n:do if((eb|0)!=(g|0)){Cb(cc,ra+(eb*24|0)|0);j=a[cc>>0]|0;k=(j&1)==0;Za(ec,k?cc+1|0:c[cc+8>>2]|0,k?(j&255)>>>1:c[cc+4>>2]|0)|0;Ja(cc);j=bc+8|0;k=bc+1|0;l=bc+4|0;b=eb;while(1){b=b+1|0;if((b|0)==(g|0))break n;Cb($b,(c[e>>2]|0)+(b*24|0)|0);f=Ta($b,0,1429)|0;c[bc>>2]=c[f>>2];c[bc+4>>2]=c[f+4>>2];c[bc+8>>2]=c[f+8>>2];h=0;while(1){if((h|0)==3)break;c[f+(h<<2)>>2]=0;h=h+1|0}cc=a[bc>>0]|0;Zb=(cc&1)==0;Za(ec,Zb?k:c[j>>2]|0,Zb?(cc&255)>>>1:c[l>>2]|0)|0;Ja(bc);Ja($b)}}while(0);Ya(ec,799)|0;while(1){if((g|0)==(eb|0))break;f=c[mb>>2]|0;b=f+-24|0;while(1){if((f|0)==(b|0))break;cc=f+-24|0;c[mb>>2]=cc;Ia(cc);f=c[mb>>2]|0}g=g+-1|0}c[ac>>2]=c[ec>>2];c[ac+4>>2]=c[ec+4>>2];c[ac+8>>2]=c[ec+8>>2];b=0;while(1){if((b|0)==3)break;c[ec+(b<<2)>>2]=0;b=b+1|0}rb(_b,ac);b=c[mb>>2]|0;cc=c[e+8>>2]|0;h=cc;if(b>>>0<cc>>>0){db(b,_b);c[mb>>2]=(c[mb>>2]|0)+24}else{f=c[e>>2]|0;cc=b-f|0;j=(cc|0)/24|0;g=j+1|0;if((cc|0)<-24)Pa();b=(h-f|0)/24|0;if(b>>>0<1073741823){b=b<<1;b=b>>>0<g>>>0?g:b}else b=2147483647;ab(dc,b,j,e+12|0);cc=dc+8|0;bc=c[cc>>2]|0;db(bc,_b);c[cc>>2]=bc+24;cb(e,dc);bb(dc)}Ia(_b);Ja(ac);Ja(ec);b=qb}break a}default:break a}case 116:switch(a[la+1>>0]|0){case 105:case 101:{o:do if((ja|0)>2?(a[b>>0]|0)==116:0){f=a[b+1>>0]|0;switch(f<<24>>24){case 105:case 101:break;default:break o}g=b+2|0;if(f<<24>>24==101)h=ub(g,d,e)|0;else h=Na(g,d,e)|0;if((h|0)!=(g|0)?(sa=c[e+4>>2]|0,(c[e>>2]|0)!=(sa|0)):0){g=sa+-24|0;Cb(bc,g);b=Ta(bc,0,2002)|0;c[cc>>2]=c[b>>2];c[cc+4>>2]=c[b+4>>2];c[cc+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}b=Ya(cc,799)|0;c[ec>>2]=c[b>>2];c[ec+4>>2]=c[b+4>>2];c[ec+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}rb(dc,ec);Db(g,dc);Ia(dc);Ja(ec);Ja(cc);Ja(bc);b=h}}while(0);break a}case 114:{ib(Sa,2010);f=e+4|0;g=c[f>>2]|0;ec=c[e+8>>2]|0;h=ec;if(g>>>0<ec>>>0){db(g,Sa);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;ec=g-f|0;j=(ec|0)/24|0;g=j+1|0;if((ec|0)<-24)Pa();f=(h-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<g>>>0?g:f}else f=2147483647;ab(dc,f,j,e+12|0);ec=dc+8|0;cc=c[ec>>2]|0;db(cc,Sa);c[ec>>2]=cc+24;cb(e,dc);bb(dc)}Ia(Sa);b=b+2|0;break a}case 119:{if(((((ja|0)>2?(a[b>>0]|0)==116:0)?(a[b+1>>0]|0)==119:0)?(bc=b+2|0,ta=ub(bc,d,e)|0,(ta|0)!=(bc|0)):0)?(v=c[e+4>>2]|0,(c[e>>2]|0)!=(v|0)):0){b=v+-24|0;Cb(cc,b);f=Ta(cc,0,2016)|0;c[ec>>2]=c[f>>2];c[ec+4>>2]=c[f+4>>2];c[ec+8>>2]=c[f+8>>2];g=0;while(1){if((g|0)==3)break;c[f+(g<<2)>>2]=0;g=g+1|0}rb(dc,ec);Db(b,dc);Ia(dc);Ja(ec);Ja(cc);b=ta}break a}default:break a}case 57:case 56:case 55:case 54:case 53:case 52:case 51:case 50:case 49:{b=Jb(b,d,e)|0;break a}default:break a}while(0)}while(0);i=fc;return b|0}function vb(b,d,e){b=b|0;d=d|0;e=e|0;var f=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0,s=0,t=0,u=0,v=0,w=0,x=0,y=0,z=0,A=0,B=0,C=0,D=0,E=0,F=0,G=0,H=0,I=0,J=0,K=0,L=0,M=0,N=0,O=0;O=i;i=i+480|0;J=O+72|0;I=O+48|0;H=O+24|0;L=O;K=O+432|0;M=O+408|0;N=O+384|0;r=O+396|0;y=O+360|0;z=O+336|0;s=O+320|0;t=O+308|0;u=O+296|0;v=O+284|0;f=O+272|0;j=O+260|0;k=O+248|0;l=O+236|0;m=O+224|0;n=O+212|0;o=O+200|0;p=O+188|0;q=O+176|0;A=O+152|0;B=O+140|0;C=O+128|0;D=O+116|0;E=O+104|0;F=O+92|0;x=d;a:do if((x-b|0)>3?(a[b>>0]|0)==76:0){w=b+1|0;do switch(a[w>>0]|0){case 84:break a;case 119:{N=b+2|0;$a(r,481,7);e=wb(N,d,r,e)|0;Ja(r);b=(e|0)==(N|0)?b:e;break a}case 98:{if((a[b+3>>0]|0)!=69)break a;switch(a[b+2>>0]|0){case 48:{ib(y,801);f=e+4|0;j=c[f>>2]|0;N=c[e+8>>2]|0;k=N;if(j>>>0<N>>>0){db(j,y);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;N=j-f|0;l=(N|0)/24|0;j=l+1|0;if((N|0)<-24)Pa();f=(k-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<j>>>0?j:f}else f=2147483647;ab(L,f,l,e+12|0);N=L+8|0;M=c[N>>2]|0;db(M,y);c[N>>2]=M+24;cb(e,L);bb(L)}Ia(y);b=b+4|0;break a}case 49:{fb(z,807);f=e+4|0;j=c[f>>2]|0;N=c[e+8>>2]|0;k=N;if(j>>>0<N>>>0){db(j,z);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;N=j-f|0;l=(N|0)/24|0;j=l+1|0;if((N|0)<-24)Pa();f=(k-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<j>>>0?j:f}else f=2147483647;ab(L,f,l,e+12|0);N=L+8|0;M=c[N>>2]|0;db(M,z);c[N>>2]=M+24;cb(e,L);bb(L)}Ia(z);b=b+4|0;break a}default:break a}}case 99:{N=b+2|0;$a(s,494,4);e=wb(N,d,s,e)|0;Ja(s);b=(e|0)==(N|0)?b:e;break a}case 97:{N=b+2|0;$a(t,499,11);e=wb(N,d,t,e)|0;Ja(t);b=(e|0)==(N|0)?b:e;break a}case 104:{N=b+2|0;$a(u,511,13);e=wb(N,d,u,e)|0;Ja(u);b=(e|0)==(N|0)?b:e;break a}case 115:{N=b+2|0;$a(v,525,5);e=wb(N,d,v,e)|0;Ja(v);b=(e|0)==(N|0)?b:e;break a}case 116:{N=b+2|0;$a(f,531,14);e=wb(N,d,f,e)|0;Ja(f);b=(e|0)==(N|0)?b:e;break a}case 105:{N=b+2|0;$a(j,5344,0);e=wb(N,d,j,e)|0;Ja(j);b=(e|0)==(N|0)?b:e;break a}case 106:{N=b+2|0;$a(k,812,1);e=wb(N,d,k,e)|0;Ja(k);b=(e|0)==(N|0)?b:e;break a}case 108:{N=b+2|0;$a(l,814,1);e=wb(N,d,l,e)|0;Ja(l);b=(e|0)==(N|0)?b:e;break a}case 109:{N=b+2|0;$a(m,816,2);e=wb(N,d,m,e)|0;Ja(m);b=(e|0)==(N|0)?b:e;break a}case 120:{N=b+2|0;$a(n,819,2);e=wb(N,d,n,e)|0;Ja(n);b=(e|0)==(N|0)?b:e;break a}case 121:{N=b+2|0;$a(o,822,3);e=wb(N,d,o,e)|0;Ja(o);b=(e|0)==(N|0)?b:e;break a}case 110:{N=b+2|0;$a(p,611,8);e=wb(N,d,p,e)|0;Ja(p);b=(e|0)==(N|0)?b:e;break a}case 111:{N=b+2|0;$a(q,620,17);e=wb(N,d,q,e)|0;Ja(q);b=(e|0)==(N|0)?b:e;break a}case 102:{m=b+2|0;b:do if((x-m|0)>>>0>8){k=b+10|0;f=L;l=m;while(1){j=a[l>>0]|0;if((l|0)==(k|0)){G=41;break}j=j<<24>>24;if(!(fc(j)|0))break;J=a[l+1>>0]|0;a[f>>0]=(((J<<24>>24)+-48|0)>>>0<10?208:169)+(J&255)+(((j+-48|0)>>>0<10?0:9)+j<<4);f=f+1|0;l=l+2|0}do if((G|0)==41){if(j<<24>>24==69){c:do if((L|0)!=(f|0)){j=L;while(1){f=f+-1|0;if(j>>>0>=f>>>0)break c;J=a[j>>0]|0;a[j>>0]=a[f>>0]|0;a[f>>0]=J;j=j+1|0}}while(0);f=K;j=f+24|0;do{a[f>>0]=0;f=f+1|0}while((f|0)<(j|0));h[H>>3]=+g[L>>2];f=jc(K,24,826,H)|0;if(f>>>0>23)break;$a(N,K,f);rb(M,N);f=e+4|0;j=c[f>>2]|0;L=c[e+8>>2]|0;k=L;if(j>>>0<L>>>0){db(j,M);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;L=j-f|0;l=(L|0)/24|0;j=l+1|0;if((L|0)<-24)Pa();f=(k-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<j>>>0?j:f}else f=2147483647;ab(H,f,l,e+12|0);L=H+8|0;K=c[L>>2]|0;db(K,M);c[L>>2]=K+24;cb(e,H);bb(H)}Ia(M);Ja(N);f=b+11|0}else f=m;break b}while(0);f=m}else f=m;while(0);b=(f|0)==(m|0)?b:f;break a}case 100:{m=b+2|0;d:do if((x-m|0)>>>0>16){k=b+18|0;f=L;l=m;while(1){j=a[l>>0]|0;if((l|0)==(k|0)){G=63;break}j=j<<24>>24;if(!(fc(j)|0))break;J=a[l+1>>0]|0;a[f>>0]=(((J<<24>>24)+-48|0)>>>0<10?208:169)+(J&255)+(((j+-48|0)>>>0<10?0:9)+j<<4);f=f+1|0;l=l+2|0}do if((G|0)==63){if(j<<24>>24==69){e:do if((L|0)!=(f|0)){j=L;while(1){f=f+-1|0;if(j>>>0>=f>>>0)break e;J=a[j>>0]|0;a[j>>0]=a[f>>0]|0;a[f>>0]=J;j=j+1|0}}while(0);f=K;j=f+32|0;do{a[f>>0]=0;f=f+1|0}while((f|0)<(j|0));h[I>>3]=+h[L>>3];f=jc(K,32,830,I)|0;if(f>>>0>31)break;$a(N,K,f);rb(M,N);f=e+4|0;j=c[f>>2]|0;L=c[e+8>>2]|0;k=L;if(j>>>0<L>>>0){db(j,M);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;L=j-f|0;l=(L|0)/24|0;j=l+1|0;if((L|0)<-24)Pa();f=(k-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<j>>>0?j:f}else f=2147483647;ab(I,f,l,e+12|0);L=I+8|0;K=c[L>>2]|0;db(K,M);c[L>>2]=K+24;cb(e,I);bb(I)}Ia(M);Ja(N);f=b+19|0}else f=m;break d}while(0);f=m}else f=m;while(0);b=(f|0)==(m|0)?b:f;break a}case 101:{m=b+2|0;f:do if((x-m|0)>>>0>20){k=b+22|0;f=L;l=m;while(1){j=a[l>>0]|0;if((l|0)==(k|0)){G=85;break}j=j<<24>>24;if(!(fc(j)|0))break;I=a[l+1>>0]|0;a[f>>0]=(((I<<24>>24)+-48|0)>>>0<10?208:169)+(I&255)+(((j+-48|0)>>>0<10?0:9)+j<<4);f=f+1|0;l=l+2|0}do if((G|0)==85){if(j<<24>>24==69){g:do if((L|0)!=(f|0)){j=L;while(1){f=f+-1|0;if(j>>>0>=f>>>0)break g;I=a[j>>0]|0;a[j>>0]=a[f>>0]|0;a[f>>0]=I;j=j+1|0}}while(0);f=K;j=f+40|0;do{a[f>>0]=0;f=f+1|0}while((f|0)<(j|0));h[J>>3]=+h[L>>3];f=jc(K,40,833,J)|0;if(f>>>0>39)break;$a(N,K,f);rb(M,N);f=e+4|0;j=c[f>>2]|0;L=c[e+8>>2]|0;k=L;if(j>>>0<L>>>0){db(j,M);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;L=j-f|0;l=(L|0)/24|0;j=l+1|0;if((L|0)<-24)Pa();f=(k-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<j>>>0?j:f}else f=2147483647;ab(J,f,l,e+12|0);L=J+8|0;K=c[L>>2]|0;db(K,M);c[L>>2]=K+24;cb(e,J);bb(J)}Ia(M);Ja(N);f=b+23|0}else f=m;break f}while(0);f=m}else f=m;while(0);b=(f|0)==(m|0)?b:f;break a}case 95:{if((a[b+2>>0]|0)!=90)break a;N=b+3|0;f=Ma(N,d,e)|0;if((f|0)==(N|0)|(f|0)==(d|0))break a;b=(a[f>>0]|0)==69?f+1|0:b;break a}default:{m=Na(w,d,e)|0;if((m|0)==(w|0)|(m|0)==(d|0))break a;if((a[m>>0]|0)==69){b=m+1|0;break a}else n=m;while(1){if((n|0)==(d|0))break a;f=a[n>>0]|0;if(((f<<24>>24)+-48|0)>>>0>=10)break;n=n+1|0}if(!((n|0)!=(m|0)&f<<24>>24==69))break a;f=c[e+4>>2]|0;if((c[e>>2]|0)==(f|0))break a;l=f+-24|0;Cb(E,l);b=Ta(E,0,797)|0;c[D>>2]=c[b>>2];c[D+4>>2]=c[b+4>>2];c[D+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}b=Ya(D,799)|0;c[C>>2]=c[b>>2];c[C+4>>2]=c[b+4>>2];c[C+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}j=n-m|0;if(j>>>0>4294967279)Xa();if(j>>>0<11){a[F>>0]=j<<1;k=F+1|0}else{e=j+16&-16;k=vc(e)|0;c[F+8>>2]=k;c[F>>2]=e|1;c[F+4>>2]=j}b=m;f=k;while(1){if((b|0)==(n|0))break;a[f>>0]=a[b>>0]|0;b=b+1|0;f=f+1|0}a[k+j>>0]=0;b=a[F>>0]|0;f=(b&1)==0;b=Za(C,f?F+1|0:c[F+8>>2]|0,f?(b&255)>>>1:c[F+4>>2]|0)|0;c[B>>2]=c[b>>2];c[B+4>>2]=c[b+4>>2];c[B+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}rb(A,B);Db(l,A);Ia(A);Ja(B);Ja(F);Ja(C);Ja(D);Ja(E);b=n+1|0;break a}}while(0)}while(0);i=O;return b|0}function wb(b,d,e,f){b=b|0;d=d|0;e=e|0;f=f|0;var g=0,h=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0,s=0;s=i;i=i+80|0;p=s+48|0;m=s+24|0;n=s+12|0;o=s;r=tb(b,d)|0;if(!((r|0)==(b|0)|(r|0)==(d|0))?(a[r>>0]|0)==69:0){l=a[e>>0]|0;q=e+4|0;do if(((l&1)==0?(l&255)>>>1:c[q>>2]|0)>>>0>3){xb(o,797,e);d=Ya(o,799)|0;c[n>>2]=c[d>>2];c[n+4>>2]=c[d+4>>2];c[n+8>>2]=c[d+8>>2];g=0;while(1){if((g|0)==3)break;c[d+(g<<2)>>2]=0;g=g+1|0}rb(m,n);g=f+4|0;d=c[g>>2]|0;l=c[f+8>>2]|0;h=l;if(d>>>0<l>>>0){db(d,m);c[g>>2]=(c[g>>2]|0)+24}else{j=c[f>>2]|0;d=d-j|0;l=(d|0)/24|0;k=l+1|0;if((d|0)<-24)Pa();d=(h-j|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<k>>>0?k:d}else d=2147483647;ab(p,d,l,f+12|0);l=p+8|0;k=c[l>>2]|0;db(k,m);c[l>>2]=k+24;cb(f,p);bb(p)}Ia(m);Ja(n);Ja(o)}else{k=f+4|0;g=c[k>>2]|0;o=c[f+8>>2]|0;d=o;if(g>>>0<o>>>0){c[g>>2]=0;c[g+4>>2]=0;c[g+8>>2]=0;c[g+12>>2]=0;c[g+16>>2]=0;c[g+20>>2]=0;d=0;while(1){if((d|0)==3)break;c[g+(d<<2)>>2]=0;d=d+1|0}d=g+12|0;g=0;while(1){if((g|0)==3)break;c[d+(g<<2)>>2]=0;g=g+1|0}c[k>>2]=(c[k>>2]|0)+24;g=k;break}h=c[f>>2]|0;o=g-h|0;j=(o|0)/24|0;g=j+1|0;if((o|0)<-24)Pa();d=(d-h|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<g>>>0?g:d}else d=2147483647;ab(p,d,j,f+12|0);h=p+8|0;j=c[h>>2]|0;c[j>>2]=0;c[j+4>>2]=0;c[j+8>>2]=0;c[j+12>>2]=0;c[j+16>>2]=0;c[j+20>>2]=0;d=0;while(1){if((d|0)==3)break;c[j+(d<<2)>>2]=0;d=d+1|0}d=j+12|0;g=0;while(1){if((g|0)==3)break;c[d+(g<<2)>>2]=0;g=g+1|0}c[h>>2]=j+24;cb(f,p);bb(p);g=k}while(0);if((a[b>>0]|0)==110){zb((c[g>>2]|0)+-24|0,45);b=b+1|0}Bb((c[g>>2]|0)+-24|0,b,r);b=a[e>>0]|0;d=(b&1)==0;b=d?(b&255)>>>1:c[q>>2]|0;if(b>>>0<4)Za((c[g>>2]|0)+-24|0,d?e+1|0:c[e+8>>2]|0,b)|0;b=r+1|0}i=s;return b|0}function xb(b,d,e){b=b|0;d=d|0;e=e|0;var f=0,g=0;f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}g=bc(d)|0;f=a[e>>0]|0;f=(f&1)==0?(f&255)>>>1:c[e+4>>2]|0;yb(b,d,g,f+g|0);Za(b,(a[e>>0]&1)==0?e+1|0:c[e+8>>2]|0,f)|0;return}function yb(b,d,e,f){b=b|0;d=d|0;e=e|0;f=f|0;var g=0;if(f>>>0>4294967279)Xa();if(f>>>0<11){a[b>>0]=e<<1;f=b+1|0}else{g=f+16&-16;f=vc(g)|0;c[b+8>>2]=f;c[b>>2]=g|1;c[b+4>>2]=e}Fc(f|0,d|0,e|0)|0;a[f+e>>0]=0;return}function zb(b,d){b=b|0;d=d|0;var e=0,f=0,g=0,h=0;e=a[b>>0]|0;f=(e&1)!=0;if(f){g=(c[b>>2]&-2)+-1|0;h=c[b+4>>2]|0}else{g=10;h=(e&255)>>>1}if((h|0)==(g|0)){Ab(b,g,1,g,g,0);if(!(a[b>>0]&1))f=7;else f=8}else if(f)f=8;else f=7;if((f|0)==7){a[b>>0]=(h<<1)+2;e=b+1|0}else if((f|0)==8){e=c[b+8>>2]|0;c[b+4>>2]=h+1}b=e+h|0;a[b>>0]=d;a[b+1>>0]=0;return}function Ab(b,d,e,f,g,h){b=b|0;d=d|0;e=e|0;f=f|0;g=g|0;h=h|0;var i=0,j=0;if((-17-d|0)>>>0<e>>>0)Xa();if(!(a[b>>0]&1))j=b+1|0;else j=c[b+8>>2]|0;if(d>>>0<2147483623){e=e+d|0;i=d<<1;e=e>>>0<i>>>0?i:e;e=e>>>0<11?11:e+16&-16}else e=-17;i=vc(e)|0;if(g)Fc(i|0,j|0,g|0)|0;if((f|0)!=(g|0))Fc(i+g+h|0,j+g|0,f-g|0)|0;if((d|0)!=10)wc(j);c[b+8>>2]=i;c[b>>2]=e|1;return}function Bb(b,d,e){b=b|0;d=d|0;e=e|0;var f=0,g=0,h=0,i=0,j=0,k=0;h=d;f=a[b>>0]|0;if(!(f&1)){k=(f&255)>>>1;g=10}else{f=c[b>>2]|0;k=c[b+4>>2]|0;g=(f&-2)+-1|0;f=f&255}j=e-h|0;do if((e|0)!=(d|0)){if((g-k|0)>>>0<j>>>0){Ab(b,g,k+j-g|0,k,k,0);f=a[b>>0]|0}if(!(f&1))i=b+1|0;else i=c[b+8>>2]|0;h=e+(k-h)|0;f=d;g=i+k|0;while(1){if((f|0)==(e|0))break;a[g>>0]=a[f>>0]|0;f=f+1|0;g=g+1|0}a[i+h>>0]=0;f=k+j|0;if(!(a[b>>0]&1)){a[b>>0]=f<<1;break}else{c[b+4>>2]=f;break}}while(0);return}function Cb(b,d){b=b|0;d=d|0;var e=0,f=0,g=0;g=d+12|0;e=a[g>>0]|0;f=(e&1)==0;e=Za(d,f?g+1|0:c[d+20>>2]|0,f?(e&255)>>>1:c[d+16>>2]|0)|0;c[b>>2]=c[e>>2];c[b+4>>2]=c[e+4>>2];c[b+8>>2]=c[e+8>>2];d=0;while(1){if((d|0)==3)break;c[e+(d<<2)>>2]=0;d=d+1|0}return}function Db(b,d){b=b|0;d=d|0;var e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0;do if(a[b>>0]&1){m=b+8|0;a[c[m>>2]>>0]=0;k=b+4|0;c[k>>2]=0;e=a[b>>0]|0;if(!(e&1))i=10;else{i=c[b>>2]|0;e=i&255;i=(i&-2)+-1|0}if(!(e&1)){f=(e&255)>>>1;if((e&255)<22){h=10;j=f;l=1}else{h=(f+16&240)+-1|0;j=f;l=1}}else{h=10;j=0;l=0}if((h|0)!=(i|0)){if((h|0)==10){g=b+1|0;f=c[m>>2]|0;if(l){Fc(g|0,f|0,((e&255)>>>1)+1|0)|0;wc(f)}else{a[g>>0]=a[f>>0]|0;wc(f)}a[b>>0]=j<<1;break}f=h+1|0;g=vc(f)|0;if(!(h>>>0<=i>>>0&(g|0)==0)){if(l)Fc(g|0,b+1|0,((e&255)>>>1)+1|0)|0;else{n=c[m>>2]|0;a[g>>0]=a[n>>0]|0;wc(n)}c[b>>2]=f|1;c[k>>2]=j;c[m>>2]=g}}}else{a[b+1>>0]=0;a[b>>0]=0}while(0);c[b>>2]=c[d>>2];c[b+4>>2]=c[d+4>>2];c[b+8>>2]=c[d+8>>2];e=0;while(1){if((e|0)==3)break;c[d+(e<<2)>>2]=0;e=e+1|0}n=b+12|0;d=d+12|0;do if(a[n>>0]&1){m=b+20|0;a[c[m>>2]>>0]=0;j=b+16|0;c[j>>2]=0;e=a[n>>0]|0;if(!(e&1))i=10;else{i=c[n>>2]|0;e=i&255;i=(i&-2)+-1|0}if(!(e&1)){f=(e&255)>>>1;if((e&255)<22){h=10;k=f;l=1}else{h=(f+16&240)+-1|0;k=f;l=1}}else{h=10;k=0;l=0}if((h|0)!=(i|0)){if((h|0)==10){g=n+1|0;f=c[m>>2]|0;if(l){Fc(g|0,f|0,((e&255)>>>1)+1|0)|0;wc(f)}else{a[g>>0]=a[f>>0]|0;wc(f)}a[n>>0]=k<<1;break}f=h+1|0;g=vc(f)|0;if(!(h>>>0<=i>>>0&(g|0)==0)){if(l)Fc(g|0,n+1|0,((e&255)>>>1)+1|0)|0;else{b=c[m>>2]|0;a[g>>0]=a[b>>0]|0;wc(b)}c[n>>2]=f|1;c[j>>2]=k;c[m>>2]=g}}}else{a[n+1>>0]=0;a[n>>0]=0}while(0);c[n>>2]=c[d>>2];c[n+4>>2]=c[d+4>>2];c[n+8>>2]=c[d+8>>2];e=0;while(1){if((e|0)==3)break;c[d+(e<<2)>>2]=0;e=e+1|0}return}function Eb(b,d,e){b=b|0;d=d|0;e=e|0;var f=0,g=0,h=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0,s=0,t=0,u=0,v=0,w=0,x=0;x=i;i=i+96|0;w=x+64|0;k=x+40|0;u=x+16|0;v=x;t=b;a:do if((d-t|0)>1?(a[b>>0]|0)==84:0){r=a[b+1>>0]|0;if(r<<24>>24==95){f=c[e+36>>2]|0;if((c[e+32>>2]|0)==(f|0)){f=b;break}g=c[f+-16>>2]|0;if((g|0)!=(c[f+-12>>2]|0)){m=c[g+4>>2]|0;n=e+4|0;o=e+8|0;p=e+12|0;q=w+8|0;l=c[g>>2]|0;while(1){if((l|0)==(m|0)){f=8;break}f=c[n>>2]|0;k=c[o>>2]|0;g=k;if((f|0)==(k|0)){h=c[e>>2]|0;f=f-h|0;k=(f|0)/24|0;j=k+1|0;if((f|0)<-24){f=12;break}f=(g-h|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<j>>>0?j:f}else f=2147483647;ab(w,f,k,p);k=c[q>>2]|0;_a(k,l);_a(k+12|0,l+12|0);c[q>>2]=k+24;cb(e,w);bb(w)}else{_a(f,l);_a(f+12|0,l+12|0);c[n>>2]=(c[n>>2]|0)+24}l=l+24|0}if((f|0)==8){f=b+2|0;break}else if((f|0)==12)Pa()}else{a[k>>0]=4;f=k+1|0;a[f>>0]=84;a[f+1>>0]=95;a[k+3>>0]=0;f=k+12|0;g=0;while(1){if((g|0)==3)break;c[f+(g<<2)>>2]=0;g=g+1|0}f=e+4|0;g=c[f>>2]|0;v=c[e+8>>2]|0;h=v;if(g>>>0<v>>>0){db(g,k);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;v=g-f|0;j=(v|0)/24|0;g=j+1|0;if((v|0)<-24)Pa();f=(h-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<g>>>0?g:f}else f=2147483647;ab(w,f,j,e+12|0);v=w+8|0;u=c[v>>2]|0;db(u,k);c[v>>2]=u+24;cb(e,w);bb(w)}Ia(k);a[e+62>>0]=1;f=b+2|0;break}}f=(r<<24>>24)+-48|0;if(f>>>0<10){r=b+2|0;while(1){if((r|0)==(d|0)){f=b;break a}g=a[r>>0]|0;h=(g<<24>>24)+-48|0;if(h>>>0>=10)break;f=h+(f*10|0)|0;r=r+1|0}if(g<<24>>24==95?(s=c[e+36>>2]|0,(c[e+32>>2]|0)!=(s|0)):0){f=f+1|0;d=c[s+-16>>2]|0;g=d;if(f>>>0<(c[s+-12>>2]|0)-d>>4>>>0){m=c[g+(f<<4)+4>>2]|0;n=e+4|0;o=e+8|0;p=e+12|0;q=w+8|0;l=c[g+(f<<4)>>2]|0;while(1){if((l|0)==(m|0)){f=38;break}f=c[n>>2]|0;s=c[o>>2]|0;g=s;if((f|0)==(s|0)){h=c[e>>2]|0;s=f-h|0;k=(s|0)/24|0;j=k+1|0;if((s|0)<-24){f=42;break}f=(g-h|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<j>>>0?j:f}else f=2147483647;ab(w,f,k,p);s=c[q>>2]|0;_a(s,l);_a(s+12|0,l+12|0);c[q>>2]=s+24;cb(e,w);bb(w)}else{_a(f,l);_a(f+12|0,l+12|0);c[n>>2]=(c[n>>2]|0)+24}l=l+24|0}if((f|0)==38){f=r+1|0;break}else if((f|0)==42)Pa()}f=r+1|0;j=f-t|0;if(j>>>0>4294967279)Xa();if(j>>>0<11){a[v>>0]=j<<1;k=v+1|0}else{t=j+16&-16;k=vc(t)|0;c[v+8>>2]=k;c[v>>2]=t|1;c[v+4>>2]=j}g=b;h=k;while(1){if((g|0)==(f|0))break;a[h>>0]=a[g>>0]|0;g=g+1|0;h=h+1|0}a[k+j>>0]=0;rb(u,v);g=e+4|0;h=c[g>>2]|0;b=c[e+8>>2]|0;j=b;if(h>>>0<b>>>0){db(h,u);c[g>>2]=(c[g>>2]|0)+24}else{g=c[e>>2]|0;b=h-g|0;k=(b|0)/24|0;h=k+1|0;if((b|0)<-24)Pa();g=(j-g|0)/24|0;if(g>>>0<1073741823){g=g<<1;g=g>>>0<h>>>0?h:g}else g=2147483647;ab(w,g,k,e+12|0);b=w+8|0;t=c[b>>2]|0;db(t,u);c[b>>2]=t+24;cb(e,w);bb(w)}Ia(u);Ja(v);a[e+62>>0]=1}else f=b}else f=b}else f=b;while(0);i=x;return f|0}function Fb(b,d,e){b=b|0;d=d|0;e=e|0;var f=0,g=0,h=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0;r=i;i=i+128|0;q=r+104|0;g=r+72|0;n=r+80|0;o=r+60|0;p=r+48|0;j=r+24|0;k=r+12|0;l=r;a:do if((d-b|0)>2?(a[b>>0]|0)==102:0){switch(a[b+1>>0]|0){case 112:{f=Oa(b+2|0,d,g)|0;h=tb(f,d)|0;if((h|0)!=(d|0)?(a[h>>0]|0)==95:0){g=h-f|0;if(g>>>0>4294967279)Xa();if(g>>>0<11){a[p>>0]=g<<1;d=p+1|0}else{m=g+16&-16;d=vc(m)|0;c[p+8>>2]=d;c[p>>2]=m|1;c[p+4>>2]=g}b=f;f=d;while(1){if((b|0)==(h|0))break;a[f>>0]=a[b>>0]|0;b=b+1|0;f=f+1|0}a[d+g>>0]=0;b=Ta(p,0,838)|0;c[o>>2]=c[b>>2];c[o+4>>2]=c[b+4>>2];c[o+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}rb(n,o);b=e+4|0;f=c[b>>2]|0;m=c[e+8>>2]|0;g=m;if(f>>>0<m>>>0){db(f,n);c[b>>2]=(c[b>>2]|0)+24}else{b=c[e>>2]|0;m=f-b|0;d=(m|0)/24|0;f=d+1|0;if((m|0)<-24)Pa();b=(g-b|0)/24|0;if(b>>>0<1073741823){b=b<<1;b=b>>>0<f>>>0?f:b}else b=2147483647;ab(q,b,d,e+12|0);m=q+8|0;l=c[m>>2]|0;db(l,n);c[m>>2]=l+24;cb(e,q);bb(q)}Ia(n);Ja(o);Ja(p);b=h+1|0}break a}case 76:break;default:break a}f=tb(b+2|0,d)|0;if((((f|0)!=(d|0)?(a[f>>0]|0)==112:0)?(h=Oa(f+1|0,d,g)|0,m=tb(h,d)|0,(m|0)!=(d|0)):0)?(a[m>>0]|0)==95:0){g=m-h|0;if(g>>>0>4294967279)Xa();if(g>>>0<11){a[l>>0]=g<<1;d=l+1|0}else{p=g+16&-16;d=vc(p)|0;c[l+8>>2]=d;c[l>>2]=p|1;c[l+4>>2]=g}b=h;f=d;while(1){if((b|0)==(m|0))break;a[f>>0]=a[b>>0]|0;b=b+1|0;f=f+1|0}a[d+g>>0]=0;b=Ta(l,0,838)|0;c[k>>2]=c[b>>2];c[k+4>>2]=c[b+4>>2];c[k+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}rb(j,k);b=e+4|0;f=c[b>>2]|0;p=c[e+8>>2]|0;g=p;if(f>>>0<p>>>0){db(f,j);c[b>>2]=(c[b>>2]|0)+24}else{b=c[e>>2]|0;p=f-b|0;d=(p|0)/24|0;f=d+1|0;if((p|0)<-24)Pa();b=(g-b|0)/24|0;if(b>>>0<1073741823){b=b<<1;b=b>>>0<f>>>0?f:b}else b=2147483647;ab(q,b,d,e+12|0);p=q+8|0;o=c[p>>2]|0;db(o,j);c[p>>2]=o+24;cb(e,q);bb(q)}Ia(j);Ja(k);Ja(l);b=m+1|0}}while(0);i=r;return b|0}function Gb(b,d,e,f){b=b|0;d=d|0;e=e|0;f=f|0;var g=0,h=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0,s=0,t=0,u=0;t=i;i=i+96|0;s=t+84|0;r=t+72|0;l=t+60|0;m=t+48|0;n=t+36|0;o=t+24|0;p=t+12|0;q=t;g=ub(b,d,f)|0;a:do if((g|0)!=(b|0)){d=ub(g,d,f)|0;h=f+4|0;if((d|0)==(g|0)){g=c[h>>2]|0;d=g+-24|0;while(1){if((g|0)==(d|0)){d=b;break a}s=g+-24|0;c[h>>2]=s;Ia(s);g=c[h>>2]|0}}g=c[h>>2]|0;if(((g-(c[f>>2]|0)|0)/24|0)>>>0>=2){Cb(s,g+-24|0);g=c[h>>2]|0;f=g+-24|0;b=g;while(1){if((b|0)==(f|0))break;k=b+-24|0;c[h>>2]=k;Ia(k);b=c[h>>2]|0}Cb(r,g+-48|0);g=c[h>>2]|0;k=g+-24|0;if(!(a[k>>0]&1)){a[k+1>>0]=0;a[k>>0]=0}else{a[c[g+-16>>2]>>0]=0;c[g+-20>>2]=0}u=a[e>>0]|0;f=(u&1)==0;b=e+4|0;u=f?(u&255)>>>1:c[b>>2]|0;h=e+8|0;j=e+1|0;g=u>>>0>1;f=ac(f?j:c[h>>2]|0,844,g?1:u)|0;if(!(((f|0)==0?((u|0)==0?-1:g&1):f)|0))zb(k,40);xb(q,797,r);g=Ya(q,846)|0;c[p>>2]=c[g>>2];c[p+4>>2]=c[g+4>>2];c[p+8>>2]=c[g+8>>2];f=0;while(1){if((f|0)==3)break;c[g+(f<<2)>>2]=0;f=f+1|0}g=a[e>>0]|0;f=(g&1)==0;g=Za(p,f?j:c[h>>2]|0,f?(g&255)>>>1:c[b>>2]|0)|0;c[o>>2]=c[g>>2];c[o+4>>2]=c[g+4>>2];c[o+8>>2]=c[g+8>>2];f=0;while(1){if((f|0)==3)break;c[g+(f<<2)>>2]=0;f=f+1|0}g=Ya(o,849)|0;c[n>>2]=c[g>>2];c[n+4>>2]=c[g+4>>2];c[n+8>>2]=c[g+8>>2];f=0;while(1){if((f|0)==3)break;c[g+(f<<2)>>2]=0;f=f+1|0}g=a[s>>0]|0;f=(g&1)==0;g=Za(n,f?s+1|0:c[s+8>>2]|0,f?(g&255)>>>1:c[s+4>>2]|0)|0;c[m>>2]=c[g>>2];c[m+4>>2]=c[g+4>>2];c[m+8>>2]=c[g+8>>2];f=0;while(1){if((f|0)==3)break;c[g+(f<<2)>>2]=0;f=f+1|0}g=Ya(m,799)|0;c[l>>2]=c[g>>2];c[l+4>>2]=c[g+4>>2];c[l+8>>2]=c[g+8>>2];f=0;while(1){if((f|0)==3)break;c[g+(f<<2)>>2]=0;f=f+1|0}u=a[l>>0]|0;f=(u&1)==0;Za(k,f?l+1|0:c[l+8>>2]|0,f?(u&255)>>>1:c[l+4>>2]|0)|0;Ja(l);Ja(m);Ja(n);Ja(o);Ja(p);Ja(q);q=a[e>>0]|0;u=(q&1)==0;q=u?(q&255)>>>1:c[b>>2]|0;e=q>>>0>1;u=ac(u?j:c[h>>2]|0,844,e?1:q)|0;if(!(((u|0)==0?((q|0)==0?-1:e&1):u)|0))zb(k,41);Ja(r);Ja(s)}else d=b}else d=b;while(0);i=t;return d|0}function Hb(b,d,e,f){b=b|0;d=d|0;e=e|0;f=f|0;var g=0,h=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0,s=0;s=i;i=i+48|0;o=s+36|0;p=s+24|0;q=s+12|0;r=s;d=ub(b,d,f)|0;if((d|0)!=(b|0)?(g=f+4|0,h=c[g>>2]|0,(c[f>>2]|0)!=(h|0)):0){n=h+-24|0;Ib(q,e,797);Cb(r,(c[g>>2]|0)+-24|0);f=a[r>>0]|0;b=(f&1)==0;f=Za(q,b?r+1|0:c[r+8>>2]|0,b?(f&255)>>>1:c[r+4>>2]|0)|0;c[p>>2]=c[f>>2];c[p+4>>2]=c[f+4>>2];c[p+8>>2]=c[f+8>>2];b=0;while(1){if((b|0)==3)break;c[f+(b<<2)>>2]=0;b=b+1|0}f=Ya(p,799)|0;c[o>>2]=c[f>>2];c[o+4>>2]=c[f+4>>2];c[o+8>>2]=c[f+8>>2];b=0;while(1){if((b|0)==3)break;c[f+(b<<2)>>2]=0;b=b+1|0}do if(a[n>>0]&1){m=h+-16|0;a[c[m>>2]>>0]=0;j=h+-20|0;c[j>>2]=0;f=a[n>>0]|0;if(!(f&1))h=10;else{h=c[n>>2]|0;f=h&255;h=(h&-2)+-1|0}if(!(f&1)){b=(f&255)>>>1;if((f&255)<22){e=10;k=b;l=1}else{e=(b+16&240)+-1|0;k=b;l=1}}else{e=10;k=0;l=0}if((e|0)!=(h|0)){if((e|0)==10){g=n+1|0;b=c[m>>2]|0;if(l){Fc(g|0,b|0,((f&255)>>>1)+1|0)|0;wc(b)}else{a[g>>0]=a[b>>0]|0;wc(b)}a[n>>0]=k<<1;break}b=e+1|0;g=vc(b)|0;if(!(e>>>0<=h>>>0&(g|0)==0)){if(l)Fc(g|0,n+1|0,((f&255)>>>1)+1|0)|0;else{l=c[m>>2]|0;a[g>>0]=a[l>>0]|0;wc(l)}c[n>>2]=b|1;c[j>>2]=k;c[m>>2]=g}}}else{a[n+1>>0]=0;a[n>>0]=0}while(0);c[n>>2]=c[o>>2];c[n+4>>2]=c[o+4>>2];c[n+8>>2]=c[o+8>>2];f=0;while(1){if((f|0)==3)break;c[o+(f<<2)>>2]=0;f=f+1|0}Ja(o);Ja(p);Ja(r);Ja(q)}else d=b;i=s;return d|0}function Ib(b,d,e){b=b|0;d=d|0;e=e|0;var f=0,g=0,h=0;f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}g=a[d>>0]|0;h=(g&1)==0;g=h?(g&255)>>>1:c[d+4>>2]|0;f=bc(e)|0;yb(b,h?d+1|0:c[d+8>>2]|0,g,g+f|0);Za(b,e,f)|0;return}function Jb(b,d,e){b=b|0;d=d|0;e=e|0;var f=0,g=0,h=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0,s=0,t=0;t=i;i=i+80|0;s=t+60|0;p=t+48|0;r=t+36|0;l=t+24|0;o=t+12|0;q=t;g=d;a:do if((g-b|0)>2){if((a[b>>0]|0)==103){h=(a[b+1>>0]|0)==115;j=h;h=h?b+2|0:b}else{j=0;h=b}f=Kb(h,d,e)|0;if((f|0)!=(h|0)){if(!j)break;g=c[e+4>>2]|0;if((c[e>>2]|0)==(g|0)){f=b;break}Ta(g+-24|0,0,891)|0;break}if(((g-h|0)>2?(a[h>>0]|0)==115:0)?(a[h+1>>0]|0)==114:0){f=h+2|0;if((a[f>>0]|0)==78){q=h+3|0;f=Ob(q,d,e)|0;if((f|0)==(q|0)|(f|0)==(d|0)){f=b;break}j=Mb(f,d,e)|0;o=e+4|0;do if((j|0)==(f|0))n=e;else{f=c[o>>2]|0;if(((f-(c[e>>2]|0)|0)/24|0)>>>0<2){f=b;break a}Cb(s,f+-24|0);f=c[o>>2]|0;g=f+-24|0;h=f;while(1){if((h|0)==(g|0))break;q=h+-24|0;c[o>>2]=q;Ia(q);h=c[o>>2]|0}q=a[s>>0]|0;n=(q&1)==0;Za(f+-48|0,n?s+1|0:c[s+8>>2]|0,n?(q&255)>>>1:c[s+4>>2]|0)|0;if((j|0)!=(d|0)){Ja(s);n=e;f=j;break}g=c[o>>2]|0;f=g+-24|0;while(1){if((g|0)==(f|0))break;e=g+-24|0;c[o>>2]=e;Ia(e);g=c[o>>2]|0}Ja(s);f=b;break a}while(0);k=p+8|0;l=p+1|0;m=p+4|0;while(1){if((a[f>>0]|0)==69)break;j=Vb(f,d,e)|0;if((j|0)==(f|0)|(j|0)==(d|0)){f=b;break a}f=c[o>>2]|0;if(((f-(c[n>>2]|0)|0)/24|0)>>>0<2){f=b;break a}Cb(s,f+-24|0);h=c[o>>2]|0;f=h+-24|0;g=h;while(1){if((g|0)==(f|0))break;q=g+-24|0;c[o>>2]=q;Ia(q);g=c[o>>2]|0}f=Ta(s,0,891)|0;c[p>>2]=c[f>>2];c[p+4>>2]=c[f+4>>2];c[p+8>>2]=c[f+8>>2];g=0;while(1){if((g|0)==3)break;c[f+(g<<2)>>2]=0;g=g+1|0}f=a[p>>0]|0;q=(f&1)==0;Za(h+-48|0,q?l:c[k>>2]|0,q?(f&255)>>>1:c[m>>2]|0)|0;Ja(p);Ja(s);f=j}q=f+1|0;f=Kb(q,d,e)|0;if((f|0)==(q|0)){f=c[o>>2]|0;if((c[e>>2]|0)==(f|0)){f=b;break}g=f+-24|0;while(1){if((f|0)==(g|0)){f=b;break a}s=f+-24|0;c[o>>2]=s;Ia(s);f=c[o>>2]|0}}g=c[o>>2]|0;if(((g-(c[n>>2]|0)|0)/24|0)>>>0<2){f=b;break}Cb(s,g+-24|0);j=c[o>>2]|0;g=j+-24|0;h=j;while(1){if((h|0)==(g|0))break;b=h+-24|0;c[o>>2]=b;Ia(b);h=c[o>>2]|0}g=Ta(s,0,891)|0;c[r>>2]=c[g>>2];c[r+4>>2]=c[g+4>>2];c[r+8>>2]=c[g+8>>2];h=0;while(1){if((h|0)==3)break;c[g+(h<<2)>>2]=0;h=h+1|0}b=a[r>>0]|0;e=(b&1)==0;Za(j+-48|0,e?r+1|0:c[r+8>>2]|0,e?(b&255)>>>1:c[r+4>>2]|0)|0;Ja(r);Ja(s);break}g=Ob(f,d,e)|0;if((g|0)!=(f|0)){k=Mb(g,d,e)|0;if((k|0)!=(g|0)){j=e+4|0;f=c[j>>2]|0;if(((f-(c[e>>2]|0)|0)/24|0)>>>0<2){f=b;break}Cb(s,f+-24|0);f=c[j>>2]|0;g=f+-24|0;h=f;while(1){if((h|0)==(g|0))break;r=h+-24|0;c[j>>2]=r;Ia(r);h=c[j>>2]|0}g=a[s>>0]|0;r=(g&1)==0;Za(f+-48|0,r?s+1|0:c[s+8>>2]|0,r?(g&255)>>>1:c[s+4>>2]|0)|0;Ja(s);g=k}f=Kb(g,d,e)|0;if((f|0)==(g|0)){h=e+4|0;f=c[h>>2]|0;if((c[e>>2]|0)==(f|0)){f=b;break}g=f+-24|0;while(1){if((f|0)==(g|0)){f=b;break a}s=f+-24|0;c[h>>2]=s;Ia(s);f=c[h>>2]|0}}k=e+4|0;g=c[k>>2]|0;if(((g-(c[e>>2]|0)|0)/24|0)>>>0<2){f=b;break}Cb(s,g+-24|0);j=c[k>>2]|0;g=j+-24|0;h=j;while(1){if((h|0)==(g|0))break;b=h+-24|0;c[k>>2]=b;Ia(b);h=c[k>>2]|0}g=Ta(s,0,891)|0;c[l>>2]=c[g>>2];c[l+4>>2]=c[g+4>>2];c[l+8>>2]=c[g+8>>2];h=0;while(1){if((h|0)==3)break;c[g+(h<<2)>>2]=0;h=h+1|0}b=a[l>>0]|0;e=(b&1)==0;Za(j+-48|0,e?l+1|0:c[l+8>>2]|0,e?(b&255)>>>1:c[l+4>>2]|0)|0;Ja(l);Ja(s);break}h=Vb(f,d,e)|0;if(!((h|0)==(f|0)|(h|0)==(d|0))){if(j){f=e+4|0;g=c[f>>2]|0;if((c[e>>2]|0)==(g|0)){f=b;break}Ta(g+-24|0,0,891)|0;n=f}else n=e+4|0;k=o+8|0;l=o+1|0;m=o+4|0;f=h;while(1){if((a[f>>0]|0)==69)break;j=Vb(f,d,e)|0;if((j|0)==(f|0)|(j|0)==(d|0)){f=b;break a}f=c[n>>2]|0;if(((f-(c[e>>2]|0)|0)/24|0)>>>0<2){f=b;break a}Cb(s,f+-24|0);h=c[n>>2]|0;f=h+-24|0;g=h;while(1){if((g|0)==(f|0))break;r=g+-24|0;c[n>>2]=r;Ia(r);g=c[n>>2]|0}f=Ta(s,0,891)|0;c[o>>2]=c[f>>2];c[o+4>>2]=c[f+4>>2];c[o+8>>2]=c[f+8>>2];g=0;while(1){if((g|0)==3)break;c[f+(g<<2)>>2]=0;g=g+1|0}f=a[o>>0]|0;r=(f&1)==0;Za(h+-48|0,r?l:c[k>>2]|0,r?(f&255)>>>1:c[m>>2]|0)|0;Ja(o);Ja(s);f=j}r=f+1|0;f=Kb(r,d,e)|0;if((f|0)==(r|0)){f=c[n>>2]|0;if((c[e>>2]|0)==(f|0)){f=b;break}g=f+-24|0;while(1){if((f|0)==(g|0)){f=b;break a}s=f+-24|0;c[n>>2]=s;Ia(s);f=c[n>>2]|0}}g=c[n>>2]|0;if(((g-(c[e>>2]|0)|0)/24|0)>>>0>=2){Cb(s,g+-24|0);j=c[n>>2]|0;g=j+-24|0;h=j;while(1){if((h|0)==(g|0))break;b=h+-24|0;c[n>>2]=b;Ia(b);h=c[n>>2]|0}g=Ta(s,0,891)|0;c[q>>2]=c[g>>2];c[q+4>>2]=c[g+4>>2];c[q+8>>2]=c[g+8>>2];h=0;while(1){if((h|0)==3)break;c[g+(h<<2)>>2]=0;h=h+1|0}b=a[q>>0]|0;e=(b&1)==0;Za(j+-48|0,e?q+1|0:c[q+8>>2]|0,e?(b&255)>>>1:c[q+4>>2]|0)|0;Ja(q);Ja(s)}else f=b}else f=b}else f=b}else f=b;while(0);i=t;return f|0}function Kb(b,d,e){b=b|0;d=d|0;e=e|0;var f=0,g=0,h=0,j=0,k=0;k=i;i=i+16|0;j=k;a:do if((d-b|0)>1){f=a[b>>0]|0;switch(f<<24>>24){case 100:case 111:{if((a[b+1>>0]|0)==110){h=b+2|0;if(f<<24>>24==111){f=Lb(h,d,e)|0;if((f|0)==(h|0)){f=b;break a}b=Mb(f,d,e)|0;if((b|0)==(f|0))break a;d=e+4|0;f=c[d>>2]|0;if(((f-(c[e>>2]|0)|0)/24|0)>>>0<2){f=b;break a}Cb(j,f+-24|0);f=c[d>>2]|0;g=f+-24|0;h=f;while(1){if((h|0)==(g|0))break;e=h+-24|0;c[d>>2]=e;Ia(e);h=c[d>>2]|0}e=a[j>>0]|0;d=(e&1)==0;Za(f+-48|0,d?j+1|0:c[j+8>>2]|0,d?(e&255)>>>1:c[j+4>>2]|0)|0;Ja(j);f=b;break a}else{if((h|0)!=(d|0)){f=Ob(h,d,e)|0;if((f|0)==(h|0))f=Vb(h,d,e)|0;if((f|0)!=(h|0)?(g=c[e+4>>2]|0,(c[e>>2]|0)!=(g|0)):0)Ta(g+-24|0,0,886)|0;else f=h}else f=d;f=(f|0)==(h|0)?b:f;break a}}break}default:{}}f=Vb(b,d,e)|0;if((f|0)==(b|0)){f=Lb(b,d,e)|0;if((f|0)!=(b|0)){b=Mb(f,d,e)|0;if((b|0)!=(f|0)){d=e+4|0;f=c[d>>2]|0;if(((f-(c[e>>2]|0)|0)/24|0)>>>0<2)f=b;else{Cb(j,f+-24|0);f=c[d>>2]|0;g=f+-24|0;h=f;while(1){if((h|0)==(g|0))break;e=h+-24|0;c[d>>2]=e;Ia(e);h=c[d>>2]|0}e=a[j>>0]|0;d=(e&1)==0;Za(f+-48|0,d?j+1|0:c[j+8>>2]|0,d?(e&255)>>>1:c[j+4>>2]|0)|0;Ja(j);f=b}}}else f=b}}else f=b;while(0);i=k;return f|0}function Lb(b,d,e){b=b|0;d=d|0;e=e|0;var f=0,g=0,h=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0,s=0,t=0,u=0,v=0,w=0,x=0,y=0,z=0,A=0,B=0,C=0,D=0,E=0,F=0,G=0,H=0,I=0,J=0,K=0,L=0,M=0,N=0,O=0,P=0,Q=0,R=0,S=0,T=0,U=0,V=0,W=0,X=0,Y=0,Z=0,_=0,$=0,aa=0,ba=0,ca=0;ca=i;i=i+1136|0;ba=ca+1104|0;j=ca+1080|0;k=ca+1056|0;v=ca+1032|0;G=ca+1008|0;R=ca+984|0;Y=ca+960|0;Z=ca+936|0;_=ca+912|0;$=ca+888|0;aa=ca+864|0;l=ca+840|0;m=ca+816|0;n=ca+792|0;o=ca+768|0;p=ca+744|0;q=ca+720|0;r=ca+696|0;s=ca+672|0;t=ca+648|0;u=ca+624|0;w=ca+600|0;x=ca+576|0;y=ca+552|0;z=ca+528|0;A=ca+504|0;B=ca+480|0;C=ca+456|0;D=ca+432|0;E=ca+408|0;F=ca+384|0;H=ca+360|0;I=ca+336|0;J=ca+312|0;K=ca+288|0;L=ca+264|0;M=ca+240|0;N=ca+216|0;O=ca+192|0;P=ca+168|0;Q=ca+144|0;S=ca+120|0;T=ca+96|0;U=ca+72|0;V=ca+48|0;W=ca+24|0;X=ca;a:do if((d-b|0)>1)do switch(a[b>>0]|0){case 97:switch(a[b+1>>0]|0){case 97:{pb(j,926);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,j);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,j);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(j);d=b+2|0;break a}case 110:case 100:{mb(k,937);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,k);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,k);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(k);d=b+2|0;break a}case 78:{pb(v,947);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,v);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,v);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(v);d=b+2|0;break a}case 83:{mb(G,958);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,G);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,G);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(G);d=b+2|0;break a}default:{d=b;break a}}case 99:switch(a[b+1>>0]|0){case 108:{pb(R,968);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,R);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,R);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(R);d=b+2|0;break a}case 109:{mb(Y,979);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,Y);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,Y);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(Y);d=b+2|0;break a}case 111:{mb(Z,989);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,Z);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,Z);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(Z);d=b+2|0;break a}case 118:{aa=e+63|0;$=a[aa>>0]|0;a[aa>>0]=0;ba=b+2|0;d=Na(ba,d,e)|0;a[aa>>0]=$;if((d|0)==(ba|0)){d=b;break a}f=c[e+4>>2]|0;if((c[e>>2]|0)==(f|0)){d=b;break a}Ta(f+-24|0,0,999)|0;a[e+60>>0]=1;break a}default:{d=b;break a}}case 100:switch(a[b+1>>0]|0){case 97:{ob(_,1009);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,_);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,_);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(_);d=b+2|0;break a}case 101:{mb($,1027);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,$);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;_=c[aa>>2]|0;db(_,$);c[aa>>2]=_+24;cb(e,ba);bb(ba)}Ia($);d=b+2|0;break a}case 108:{d=vc(16)|0;c[aa+8>>2]=d;c[aa>>2]=17;c[aa+4>>2]=15;f=d;g=1037;h=f+15|0;do{a[f>>0]=a[g>>0]|0;f=f+1|0;g=g+1|0}while((f|0)<(h|0));a[d+15>>0]=0;d=aa+12|0;f=0;while(1){if((f|0)==3)break;c[d+(f<<2)>>2]=0;f=f+1|0}d=e+4|0;f=c[d>>2]|0;$=c[e+8>>2]|0;g=$;if(f>>>0<$>>>0){db(f,aa);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;$=f-d|0;h=($|0)/24|0;f=h+1|0;if(($|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);$=ba+8|0;_=c[$>>2]|0;db(_,aa);c[$>>2]=_+24;cb(e,ba);bb(ba)}Ia(aa);d=b+2|0;break a}case 118:{mb(l,1053);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,l);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,l);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(l);d=b+2|0;break a}case 86:{pb(m,1063);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,m);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,m);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(m);d=b+2|0;break a}default:{d=b;break a}}case 101:switch(a[b+1>>0]|0){case 111:{mb(n,1074);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,n);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,n);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(n);d=b+2|0;break a}case 79:{pb(o,1084);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,o);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,o);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(o);d=b+2|0;break a}case 113:{pb(p,1095);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,p);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,p);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(p);d=b+2|0;break a}default:{d=b;break a}}case 103:switch(a[b+1>>0]|0){case 101:{pb(q,1106);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,q);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,q);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(q);d=b+2|0;break a}case 116:{mb(r,1117);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,r);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,r);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(r);d=b+2|0;break a}default:{d=b;break a}}case 105:{if((a[b+1>>0]|0)!=120){d=b;break a}pb(s,1127);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,s);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,s);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(s);d=b+2|0;break a}case 108:switch(a[b+1>>0]|0){case 101:{pb(t,1138);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,t);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,t);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(t);d=b+2|0;break a}case 105:{ba=b+2|0;d=qb(ba,d,e)|0;if((d|0)==(ba|0)){d=b;break a}f=c[e+4>>2]|0;if((c[e>>2]|0)==(f|0)){d=b;break a}Ta(f+-24|0,0,1149)|0;break a}case 115:{pb(u,1161);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,u);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,u);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(u);d=b+2|0;break a}case 83:{gb(w,1172);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,w);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,w);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(w);d=b+2|0;break a}case 116:{mb(x,1184);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,x);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,x);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(x);d=b+2|0;break a}default:{d=b;break a}}case 109:switch(a[b+1>>0]|0){case 105:{mb(y,1194);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,y);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,y);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(y);d=b+2|0;break a}case 73:{pb(z,1204);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,z);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,z);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(z);d=b+2|0;break a}case 108:{mb(A,1027);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,A);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,A);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(A);d=b+2|0;break a}case 76:{pb(B,1215);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,B);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,B);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(B);d=b+2|0;break a}case 109:{pb(C,1226);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,C);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,C);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(C);d=b+2|0;break a}default:{d=b;break a}}case 110:switch(a[b+1>>0]|0){case 97:{jb(D,1237);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,D);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,D);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(D);d=b+2|0;break a}case 101:{pb(E,1252);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,E);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,E);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(E);d=b+2|0;break a}case 103:{mb(F,1194);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,F);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,F);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(F);d=b+2|0;break a}case 116:{mb(H,1263);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,H);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,H);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(H);d=b+2|0;break a}case 119:{lb(I,1273);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,I);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,I);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(I);d=b+2|0;break a}default:{d=b;break a}}case 111:switch(a[b+1>>0]|0){case 111:{pb(J,1286);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,J);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,J);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(J);d=b+2|0;break a}case 114:{mb(K,1297);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,K);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,K);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(K);d=b+2|0;break a}case 82:{pb(L,1307);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,L);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,L);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(L);d=b+2|0;break a}default:{d=b;break a}}case 112:switch(a[b+1>>0]|0){case 109:{gb(M,1318);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,M);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,M);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(M);d=b+2|0;break a}case 108:{mb(N,1330);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,N);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,N);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(N);d=b+2|0;break a}case 76:{pb(O,1340);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,O);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,O);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(O);d=b+2|0;break a}case 112:{pb(P,1351);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,P);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,P);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(P);d=b+2|0;break a}case 115:{mb(Q,1330);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,Q);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,Q);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(Q);d=b+2|0;break a}case 116:{pb(S,1362);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,S);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,S);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(S);d=b+2|0;break a}default:{d=b;break a}}case 113:{if((a[b+1>>0]|0)!=117){d=b;break a}mb(T,1373);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,T);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,T);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(T);d=b+2|0;break a}case 114:switch(a[b+1>>0]|0){case 109:{mb(U,1383);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,U);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,U);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(U);d=b+2|0;break a}case 77:{pb(V,1393);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,V);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,V);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(V);d=b+2|0;break a}case 115:{pb(W,1404);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,W);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,W);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(W);d=b+2|0;break a}case 83:{gb(X,1415);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,X);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,X);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(X);d=b+2|0;break a}default:{d=b;break a}}case 118:{if(((a[b+1>>0]|0)+-48|0)>>>0>=10){d=b;break a}ba=b+2|0;d=qb(ba,d,e)|0;if((d|0)==(ba|0)){d=b;break a}f=c[e+4>>2]|0;if((c[e>>2]|0)==(f|0)){d=b;break a}Ta(f+-24|0,0,999)|0;break a}default:{d=b;break a}}while(0);else d=b;while(0);i=ca;return d|0}function Mb(b,d,e){b=b|0;d=d|0;e=e|0;var f=0,g=0,h=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0,s=0,t=0,u=0,v=0,w=0,x=0,y=0,z=0,A=0,B=0,C=0,D=0,E=0,F=0,G=0,H=0,I=0,J=0,K=0,L=0,M=0,N=0;M=i;i=i+80|0;K=M+60|0;L=M;F=M+48|0;I=M+24|0;J=M+12|0;do if((d-b|0)>1?(a[b>>0]|0)==73:0){G=e+61|0;E=e+36|0;a:do if(a[G>>0]|0){g=c[E>>2]|0;f=c[g+-16>>2]|0;g=g+-12|0;while(1){h=c[g>>2]|0;if((h|0)==(f|0))break a;H=h+-16|0;c[g>>2]=H;Ha(H)}}while(0);$a(L,1427,1);H=e+4|0;t=e+12|0;u=K+8|0;v=K+8|0;D=L+4|0;w=F+8|0;x=F+1|0;y=F+4|0;z=e+32|0;A=e+40|0;B=e+44|0;C=K+8|0;n=b+1|0;b:while(1){if((a[n>>0]|0)==69){f=48;break}do if(a[G>>0]|0){m=c[t>>2]|0;f=c[E>>2]|0;g=c[A>>2]|0;if(f>>>0<g>>>0){c[f>>2]=0;c[f+4>>2]=0;c[f+8>>2]=0;c[f+12>>2]=m;c[E>>2]=(c[E>>2]|0)+16;break}h=c[z>>2]|0;s=f-h|0;k=s>>4;j=k+1|0;if((s|0)<-16){f=13;break b}f=g-h|0;if(f>>4>>>0<1073741823){f=f>>3;f=f>>>0<j>>>0?j:f}else f=2147483647;Ca(K,f,k,B);s=c[C>>2]|0;c[s>>2]=0;c[s+4>>2]=0;c[s+8>>2]=0;c[s+12>>2]=m;c[C>>2]=s+16;Ea(z,K);Fa(K)}while(0);r=((c[H>>2]|0)-(c[e>>2]|0)|0)/24|0;s=Nb(n,d,e)|0;h=((c[H>>2]|0)-(c[e>>2]|0)|0)/24|0;c:do if(a[G>>0]|0){g=c[E>>2]|0;f=g+-16|0;while(1){if((g|0)==(f|0))break c;q=g+-16|0;c[E>>2]=q;Ga(q);g=c[E>>2]|0}}while(0);if((s|0)==(n|0)|(s|0)==(d|0)){f=62;break}d:do if(!(a[G>>0]|0))f=r;else{m=c[E>>2]|0;n=m+-16|0;o=c[t>>2]|0;f=m+-12|0;g=c[f>>2]|0;q=c[m+-8>>2]|0;k=q;if(g>>>0<q>>>0){c[g>>2]=0;c[g+4>>2]=0;c[g+8>>2]=0;c[g+12>>2]=o;c[f>>2]=(c[f>>2]|0)+16;q=r}else{f=c[n>>2]|0;q=g-f|0;j=q>>4;g=j+1|0;if((q|0)<-16){f=26;break b}f=k-f|0;if(f>>4>>>0<1073741823){f=f>>3;f=f>>>0<g>>>0?g:f}else f=2147483647;Qa(K,f,j,m+-4|0);q=c[u>>2]|0;c[q>>2]=0;c[q+4>>2]=0;c[q+8>>2]=0;c[q+12>>2]=o;c[u>>2]=q+16;Ra(n,K);Sa(K);q=r}while(1){if(q>>>0>=h>>>0){f=r;break d}m=c[(c[E>>2]|0)+-12>>2]|0;n=m+-16|0;o=c[e>>2]|0;p=o+(q*24|0)|0;f=m+-12|0;g=c[f>>2]|0;k=c[m+-8>>2]|0;j=k;if((g|0)==(k|0)){f=c[n>>2]|0;N=g-f|0;k=(N|0)/24|0;g=k+1|0;if((N|0)<-24){f=34;break b}f=(j-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<g>>>0?g:f}else f=2147483647;ab(K,f,k,m+-4|0);N=c[v>>2]|0;_a(N,p);_a(N+12|0,o+(q*24|0)+12|0);c[v>>2]=N+24;cb(n,K);bb(K)}else{_a(g,p);_a(g+12|0,o+(q*24|0)+12|0);c[f>>2]=(c[f>>2]|0)+24}q=q+1|0}}while(0);while(1){if(f>>>0>=h>>>0)break;N=a[L>>0]|0;if(((N&1)==0?(N&255)>>>1:c[D>>2]|0)>>>0>1)Ya(L,1429)|0;Cb(F,(c[e>>2]|0)+(f*24|0)|0);N=a[F>>0]|0;q=(N&1)==0;Za(L,q?x:c[w>>2]|0,q?(N&255)>>>1:c[y>>2]|0)|0;Ja(F);f=f+1|0}while(1){if((h|0)==(r|0)){n=s;continue b}g=c[H>>2]|0;f=g+-24|0;while(1){if((g|0)==(f|0))break;N=g+-24|0;c[H>>2]=N;Ia(N);g=c[H>>2]|0}h=h+-1|0}}if((f|0)==13)Pa();else if((f|0)==26)Pa();else if((f|0)==34)Pa();else if((f|0)==48){l=n+1|0;N=a[L>>0]|0;b=(N&1)==0;if((a[(b?L+1|0:c[L+8>>2]|0)+(b?(N&255)>>>1:c[D>>2]|0)+-1>>0]|0)==62)Ya(L,1432)|0;else Ya(L,844)|0;c[J>>2]=c[L>>2];c[J+4>>2]=c[L+4>>2];c[J+8>>2]=c[L+8>>2];f=0;while(1){if((f|0)==3)break;c[L+(f<<2)>>2]=0;f=f+1|0}rb(I,J);f=c[H>>2]|0;N=c[e+8>>2]|0;j=N;if(f>>>0<N>>>0){db(f,I);c[H>>2]=(c[H>>2]|0)+24}else{g=c[e>>2]|0;N=f-g|0;k=(N|0)/24|0;h=k+1|0;if((N|0)<-24)Pa();f=(j-g|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<h>>>0?h:f}else f=2147483647;ab(K,f,k,e+12|0);N=K+8|0;H=c[N>>2]|0;db(H,I);c[N>>2]=H+24;cb(e,K);bb(K)}Ia(I);Ja(J);Ja(L);break}else if((f|0)==62){Ja(L);l=b;break}}else l=b;while(0);i=M;return l|0}function Nb(b,c,d){b=b|0;c=c|0;d=d|0;var e=0,f=0;a:do if((b|0)!=(c|0))switch(a[b>>0]|0){case 88:{f=b+1|0;e=ub(f,c,d)|0;if((e|0)==(f|0)|(e|0)==(c|0))break a;b=(a[e>>0]|0)==69?e+1|0:b;break a}case 74:{e=b+1|0;if((e|0)==(c|0))break a;while(1){if((a[e>>0]|0)==69)break;f=Nb(e,c,d)|0;if((f|0)==(e|0))break a;else e=f}b=e+1|0;break a}case 76:{f=b+1|0;if((f|0)!=(c|0)?(a[f>>0]|0)==90:0){f=b+2|0;e=Ma(f,c,d)|0;if((e|0)==(f|0)|(e|0)==(c|0))break a;b=(a[e>>0]|0)==69?e+1|0:b;break a}b=vb(b,c,d)|0;break a}default:{b=Na(b,c,d)|0;break a}}while(0);return b|0}function Ob(b,d,e){b=b|0;d=d|0;e=e|0;var f=0,g=0,h=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0;p=i;i=i+96|0;o=p+72|0;n=p+56|0;k=p+48|0;l=p+32|0;g=p+24|0;m=p+8|0;h=p;a:do if((b|0)==(d|0))f=b;else switch(a[b>>0]|0){case 84:{j=e+4|0;h=((c[j>>2]|0)-(c[e>>2]|0)|0)/24|0;f=Eb(b,d,e)|0;d=c[j>>2]|0;g=(d-(c[e>>2]|0)|0)/24|0;if(!((f|0)!=(b|0)&(g|0)==(h+1|0))){f=d;while(1){if((g|0)==(h|0)){f=b;break a}d=f+-24|0;while(1){if((f|0)==(d|0))break;e=f+-24|0;c[j>>2]=e;Ia(e);f=c[j>>2]|0}f=d;g=g+-1|0}}b=e+16|0;c[k>>2]=c[e+12>>2];Pb(n,d+-24|0,k);d=e+20|0;g=c[d>>2]|0;m=c[e+24>>2]|0;h=m;if(g>>>0<m>>>0){c[g+12>>2]=c[n+12>>2];c[g>>2]=c[n>>2];e=n+4|0;c[g+4>>2]=c[e>>2];o=n+8|0;c[g+8>>2]=c[o>>2];c[o>>2]=0;c[e>>2]=0;c[n>>2]=0;c[d>>2]=(c[d>>2]|0)+16}else{d=c[b>>2]|0;m=g-d|0;j=m>>4;g=j+1|0;if((m|0)<-16)Pa();d=h-d|0;if(d>>4>>>0<1073741823){d=d>>3;d=d>>>0<g>>>0?g:d}else d=2147483647;Qa(o,d,j,e+28|0);e=o+8|0;m=c[e>>2]|0;c[m+12>>2]=c[n+12>>2];c[m>>2]=c[n>>2];l=n+4|0;c[m+4>>2]=c[l>>2];k=n+8|0;c[m+8>>2]=c[k>>2];c[k>>2]=0;c[l>>2]=0;c[n>>2]=0;c[e>>2]=m+16;Ra(b,o);Sa(o)}Ha(n);break a}case 68:{f=Qb(b,d,e)|0;if((f|0)==(b|0)){f=b;break a}d=c[e+4>>2]|0;if((c[e>>2]|0)==(d|0)){f=b;break a}b=e+16|0;c[g>>2]=c[e+12>>2];Pb(l,d+-24|0,g);d=e+20|0;g=c[d>>2]|0;n=c[e+24>>2]|0;j=n;if(g>>>0<n>>>0){c[g+12>>2]=c[l+12>>2];c[g>>2]=c[l>>2];e=l+4|0;c[g+4>>2]=c[e>>2];o=l+8|0;c[g+8>>2]=c[o>>2];c[o>>2]=0;c[e>>2]=0;c[l>>2]=0;c[d>>2]=(c[d>>2]|0)+16}else{d=c[b>>2]|0;n=g-d|0;h=n>>4;g=h+1|0;if((n|0)<-16)Pa();d=j-d|0;if(d>>4>>>0<1073741823){d=d>>3;d=d>>>0<g>>>0?g:d}else d=2147483647;Qa(o,d,h,e+28|0);e=o+8|0;n=c[e>>2]|0;c[n+12>>2]=c[l+12>>2];c[n>>2]=c[l>>2];m=l+4|0;c[n+4>>2]=c[m>>2];k=l+8|0;c[n+8>>2]=c[k>>2];c[k>>2]=0;c[m>>2]=0;c[l>>2]=0;c[e>>2]=n+16;Ra(b,o);Sa(o)}Ha(l);break a}case 83:{f=Rb(b,d,e)|0;if((f|0)!=(b|0))break a;if((d-b|0)<=2){f=b;break a}if((a[b+1>>0]|0)!=116){f=b;break a}n=b+2|0;f=Sb(n,d,e)|0;if((f|0)==(n|0)){f=b;break a}g=e+4|0;d=c[g>>2]|0;if((c[e>>2]|0)==(d|0)){f=b;break a}Ta(d+-24|0,0,1827)|0;b=e+16|0;d=(c[g>>2]|0)+-24|0;c[h>>2]=c[e+12>>2];Pb(m,d,h);d=e+20|0;g=c[d>>2]|0;n=c[e+24>>2]|0;h=n;if(g>>>0<n>>>0){c[g+12>>2]=c[m+12>>2];c[g>>2]=c[m>>2];e=m+4|0;c[g+4>>2]=c[e>>2];o=m+8|0;c[g+8>>2]=c[o>>2];c[o>>2]=0;c[e>>2]=0;c[m>>2]=0;c[d>>2]=(c[d>>2]|0)+16}else{d=c[b>>2]|0;n=g-d|0;j=n>>4;g=j+1|0;if((n|0)<-16)Pa();d=h-d|0;if(d>>4>>>0<1073741823){d=d>>3;d=d>>>0<g>>>0?g:d}else d=2147483647;Qa(o,d,j,e+28|0);e=o+8|0;n=c[e>>2]|0;c[n+12>>2]=c[m+12>>2];c[n>>2]=c[m>>2];l=m+4|0;c[n+4>>2]=c[l>>2];k=m+8|0;c[n+8>>2]=c[k>>2];c[k>>2]=0;c[l>>2]=0;c[m>>2]=0;c[e>>2]=n+16;Ra(b,o);Sa(o)}Ha(m);break a}default:{f=b;break a}}while(0);i=p;return f|0}function Pb(a,b,d){a=a|0;b=b|0;d=d|0;var e=0;c[a>>2]=0;e=a+4|0;c[e>>2]=0;d=c[d>>2]|0;c[a+8>>2]=0;c[a+12>>2]=d;d=Da(d,24)|0;c[e>>2]=d;c[a>>2]=d;c[a+8>>2]=d+24;_a(d,b);_a(d+12|0,b+12|0);c[e>>2]=(c[e>>2]|0)+24;return}function Qb(b,d,e){b=b|0;d=d|0;e=e|0;var f=0,g=0,h=0,j=0,k=0,l=0,m=0,n=0;m=i;i=i+64|0;g=m+40|0;h=m+24|0;k=m+12|0;l=m;a:do if((d-b|0)>3?(a[b>>0]|0)==68:0){switch(a[b+1>>0]|0){case 84:case 116:break;default:break a}n=b+2|0;j=ub(n,d,e)|0;if((!((j|0)==(n|0)|(j|0)==(d|0))?(a[j>>0]|0)==69:0)?(f=c[e+4>>2]|0,(c[e>>2]|0)!=(f|0)):0){e=f+-24|0;Cb(l,e);b=Ta(l,0,1435)|0;c[k>>2]=c[b>>2];c[k+4>>2]=c[b+4>>2];c[k+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}b=Ya(k,799)|0;c[h>>2]=c[b>>2];c[h+4>>2]=c[b+4>>2];c[h+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}rb(g,h);Db(e,g);Ia(g);Ja(h);Ja(k);Ja(l);b=j+1|0}}while(0);i=m;return b|0}function Rb(b,d,e){b=b|0;d=d|0;e=e|0;var f=0,g=0,h=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0,s=0,t=0;t=i;i=i+176|0;s=t+144|0;k=t+120|0;l=t+96|0;m=t+72|0;n=t+48|0;o=t+24|0;p=t;a:do if((d-b|0)>1?(a[b>>0]|0)==83:0){h=a[b+1>>0]|0;switch(h|0){case 97:{jb(k,1445);f=e+4|0;g=c[f>>2]|0;r=c[e+8>>2]|0;h=r;if(g>>>0<r>>>0){db(g,k);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;r=g-f|0;j=(r|0)/24|0;g=j+1|0;if((r|0)<-24)Pa();f=(h-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<g>>>0?g:f}else f=2147483647;ab(s,f,j,e+12|0);r=s+8|0;q=c[r>>2]|0;db(q,k);c[r>>2]=q+24;cb(e,s);bb(s)}Ia(k);r=b+2|0;break a}case 98:{ob(l,1460);f=e+4|0;g=c[f>>2]|0;r=c[e+8>>2]|0;h=r;if(g>>>0<r>>>0){db(g,l);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;r=g-f|0;j=(r|0)/24|0;g=j+1|0;if((r|0)<-24)Pa();f=(h-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<g>>>0?g:f}else f=2147483647;ab(s,f,j,e+12|0);r=s+8|0;q=c[r>>2]|0;db(q,l);c[r>>2]=q+24;cb(e,s);bb(s)}Ia(l);r=b+2|0;break a}case 115:{gb(m,1478);f=e+4|0;g=c[f>>2]|0;r=c[e+8>>2]|0;h=r;if(g>>>0<r>>>0){db(g,m);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;r=g-f|0;j=(r|0)/24|0;g=j+1|0;if((r|0)<-24)Pa();f=(h-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<g>>>0?g:f}else f=2147483647;ab(s,f,j,e+12|0);r=s+8|0;q=c[r>>2]|0;db(q,m);c[r>>2]=q+24;cb(e,s);bb(s)}Ia(m);r=b+2|0;break a}case 105:{lb(n,1490);f=e+4|0;g=c[f>>2]|0;r=c[e+8>>2]|0;j=r;if(g>>>0<r>>>0){db(g,n);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;r=g-f|0;h=(r|0)/24|0;g=h+1|0;if((r|0)<-24)Pa();f=(j-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<g>>>0?g:f}else f=2147483647;ab(s,f,h,e+12|0);r=s+8|0;q=c[r>>2]|0;db(q,n);c[r>>2]=q+24;cb(e,s);bb(s)}Ia(n);r=b+2|0;break a}case 111:{lb(o,1503);f=e+4|0;g=c[f>>2]|0;r=c[e+8>>2]|0;h=r;if(g>>>0<r>>>0){db(g,o);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;r=g-f|0;j=(r|0)/24|0;g=j+1|0;if((r|0)<-24)Pa();f=(h-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<g>>>0?g:f}else f=2147483647;ab(s,f,j,e+12|0);r=s+8|0;q=c[r>>2]|0;db(q,o);c[r>>2]=q+24;cb(e,s);bb(s)}Ia(o);r=b+2|0;break a}case 100:{hb(p,1516);f=e+4|0;g=c[f>>2]|0;r=c[e+8>>2]|0;h=r;if(g>>>0<r>>>0){db(g,p);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;r=g-f|0;j=(r|0)/24|0;g=j+1|0;if((r|0)<-24)Pa();f=(h-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<g>>>0?g:f}else f=2147483647;ab(s,f,j,e+12|0);r=s+8|0;q=c[r>>2]|0;db(q,p);c[r>>2]=q+24;cb(e,s);bb(s)}Ia(p);r=b+2|0;break a}case 95:{f=c[e+16>>2]|0;if((f|0)==(c[e+20>>2]|0)){r=b;break a}m=c[f+4>>2]|0;n=e+4|0;o=e+8|0;p=e+12|0;d=s+8|0;l=c[f>>2]|0;while(1){if((l|0)==(m|0)){f=55;break}f=c[n>>2]|0;q=c[o>>2]|0;g=q;if((f|0)==(q|0)){h=c[e>>2]|0;q=f-h|0;k=(q|0)/24|0;j=k+1|0;if((q|0)<-24){f=59;break}f=(g-h|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<j>>>0?j:f}else f=2147483647;ab(s,f,k,p);q=c[d>>2]|0;_a(q,l);_a(q+12|0,l+12|0);c[d>>2]=q+24;cb(e,s);bb(s)}else{_a(f,l);_a(f+12|0,l+12|0);c[n>>2]=(c[n>>2]|0)+24}l=l+24|0}if((f|0)==55){r=b+2|0;break a}else if((f|0)==59)Pa();break}default:{g=h+-48|0;f=g>>>0<10;if(!f?(gc(h)|0)==0:0){r=b;break a}k=f?g:h+-55|0;q=b+2|0;while(1){if((q|0)==(d|0)){r=b;break a}f=a[q>>0]|0;g=f<<24>>24;j=g+-48|0;h=j>>>0<10;if(!h?(gc(g)|0)==0:0)break;k=(h?j:g+-55|0)+(k*36|0)|0;q=q+1|0}if(f<<24>>24!=95){r=b;break a}f=k+1|0;d=c[e+16>>2]|0;g=d;if(f>>>0>=(c[e+20>>2]|0)-d>>4>>>0){r=b;break a}m=c[g+(f<<4)+4>>2]|0;n=e+4|0;o=e+8|0;p=e+12|0;d=s+8|0;l=c[g+(f<<4)>>2]|0;while(1){if((l|0)==(m|0)){f=75;break}f=c[n>>2]|0;b=c[o>>2]|0;g=b;if((f|0)==(b|0)){h=c[e>>2]|0;b=f-h|0;k=(b|0)/24|0;j=k+1|0;if((b|0)<-24){f=79;break}f=(g-h|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<j>>>0?j:f}else f=2147483647;ab(s,f,k,p);b=c[d>>2]|0;_a(b,l);_a(b+12|0,l+12|0);c[d>>2]=b+24;cb(e,s);bb(s)}else{_a(f,l);_a(f+12|0,l+12|0);c[n>>2]=(c[n>>2]|0)+24}l=l+24|0}if((f|0)==75){r=q+1|0;break a}else if((f|0)==79)Pa()}}}else r=b;while(0);i=t;return r|0}
+function sa(a){a=a|0;var b=0;b=i;i=i+a|0;i=i+15&-16;return b|0}function ta(){return i|0}function ua(a){a=a|0;i=a}function va(a,b){a=a|0;b=b|0;i=a;j=b}function wa(a,b){a=a|0;b=b|0;if(!n){n=a;o=b}}function xa(b){b=b|0;a[k>>0]=a[b>>0];a[k+1>>0]=a[b+1>>0];a[k+2>>0]=a[b+2>>0];a[k+3>>0]=a[b+3>>0]}function ya(b){b=b|0;a[k>>0]=a[b>>0];a[k+1>>0]=a[b+1>>0];a[k+2>>0]=a[b+2>>0];a[k+3>>0]=a[b+3>>0];a[k+4>>0]=a[b+4>>0];a[k+5>>0]=a[b+5>>0];a[k+6>>0]=a[b+6>>0];a[k+7>>0]=a[b+7>>0]}function za(a){a=a|0;C=a}function Aa(){return C|0}function Ba(b,d,e,f){b=b|0;d=d|0;e=e|0;f=f|0;var g=0,h=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0,s=0,t=0,u=0,v=0;v=i;i=i+4208|0;t=v+4176|0;h=v;u=v+4112|0;if((b|0)!=0?(g=(d|0)!=0,s=(e|0)==0,!(g&s)):0){if(g)q=c[e>>2]|0;else q=0;c[h+4096>>2]=h;g=h;c[u>>2]=0;r=u+4|0;c[r>>2]=0;c[u+8>>2]=0;c[u+12>>2]=g;l=u+16|0;c[l>>2]=0;m=u+20|0;c[m>>2]=0;c[u+24>>2]=0;c[u+28>>2]=g;c[u+32>>2]=0;h=u+36|0;c[h>>2]=0;c[u+40>>2]=0;n=u+44|0;c[n>>2]=g;k=u+48|0;j=u+61|0;c[k>>2]=0;c[k+4>>2]=0;c[k+8>>2]=0;a[k+12>>0]=0;a[j>>0]=1;k=u+32|0;Ca(t,1,0,n);n=t+8|0;o=c[n>>2]|0;c[o>>2]=0;c[o+4>>2]=0;c[o+8>>2]=0;c[o+12>>2]=g;c[n>>2]=o+16;Ea(k,t);Fa(t);n=u+62|0;a[n>>0]=0;a[u+63>>0]=1;c[t>>2]=0;o=b+(bc(b)|0)|0;La(b,o,u,t);g=c[t>>2]|0;do if(!((g|0)!=0|(a[n>>0]|0)==0)){k=c[k>>2]|0;if((k|0)!=(c[h>>2]|0)?(c[k>>2]|0)!=(c[k+4>>2]|0):0){a[n>>0]=0;a[j>>0]=0;g=c[u>>2]|0;while(1){h=c[r>>2]|0;if((h|0)==(g|0))break;k=h+-24|0;c[r>>2]=k;Ia(k)}g=c[l>>2]|0;while(1){h=c[m>>2]|0;if((h|0)==(g|0))break;l=h+-16|0;c[m>>2]=l;Ha(l)}La(b,o,u,t);if(!(a[n>>0]|0)){g=c[t>>2]|0;p=19;break}else{c[t>>2]=-2;d=0;g=-2;break}}else p=20}else p=19;while(0);if((p|0)==19)if(!g)p=20;else d=0;do if((p|0)==20){h=c[r>>2]|0;g=a[h+-24>>0]|0;if(!(g&1))j=(g&255)>>>1;else j=c[h+-20>>2]|0;g=a[h+-12>>0]|0;if(!(g&1))g=(g&255)>>>1;else g=c[h+-8>>2]|0;j=g+j|0;g=j+1|0;if(g>>>0>q>>>0){d=xc(d,g)|0;if(!d){c[t>>2]=-1;d=0;g=-1;break}if(!s)c[e>>2]=g}else if(!d){d=0;g=0;break}g=c[r>>2]|0;t=g+-12|0;h=a[t>>0]|0;e=(h&1)==0;Za(g+-24|0,e?t+1|0:c[g+-4>>2]|0,e?(h&255)>>>1:c[g+-8>>2]|0)|0;g=c[r>>2]|0;h=g+-24|0;if(!(a[h>>0]&1))g=h+1|0;else g=c[g+-16>>2]|0;Fc(d|0,g|0,j|0)|0;a[d+j>>0]=0;g=0}while(0);if(f)c[f>>2]=g;_b(u)}else if(!f)d=0;else{c[f>>2]=-3;d=0}i=v;return d|0}function Ca(a,b,d,e){a=a|0;b=b|0;d=d|0;e=e|0;c[a+12>>2]=0;c[a+16>>2]=e;if(!b)e=0;else e=Da(c[e>>2]|0,b<<4)|0;c[a>>2]=e;d=e+(d<<4)|0;c[a+8>>2]=d;c[a+4>>2]=d;c[a+12>>2]=e+(b<<4);return}function Da(a,b){a=a|0;b=b|0;var d=0,e=0;d=b+15&-16;e=a+4096|0;b=c[e>>2]|0;if((a+4096-b|0)>>>0<d>>>0)b=vc(d)|0;else c[e>>2]=b+d;return b|0}function Ea(a,b){a=a|0;b=b|0;var d=0,e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0;e=c[a>>2]|0;f=a+4|0;g=b+4|0;d=c[f>>2]|0;while(1){if((d|0)==(e|0))break;k=c[g>>2]|0;i=k+-16|0;h=d+-16|0;c[i>>2]=0;j=k+-12|0;c[j>>2]=0;l=c[d+-4>>2]|0;c[k+-8>>2]=0;c[k+-4>>2]=l;c[i>>2]=c[h>>2];i=d+-12|0;c[j>>2]=c[i>>2];j=d+-8|0;c[k+-8>>2]=c[j>>2];c[j>>2]=0;c[i>>2]=0;c[h>>2]=0;c[g>>2]=(c[g>>2]|0)+-16;d=h}j=c[a>>2]|0;c[a>>2]=c[g>>2];c[g>>2]=j;j=b+8|0;l=c[f>>2]|0;c[f>>2]=c[j>>2];c[j>>2]=l;j=a+8|0;l=b+12|0;k=c[j>>2]|0;c[j>>2]=c[l>>2];c[l>>2]=k;c[b>>2]=c[g>>2];return}function Fa(a){a=a|0;var b=0,d=0,e=0;b=c[a+4>>2]|0;d=a+8|0;while(1){e=c[d>>2]|0;if((e|0)==(b|0))break;e=e+-16|0;c[d>>2]=e;Ga(e)}b=c[a>>2]|0;if(b)Ka(c[c[a+16>>2]>>2]|0,b,(c[a+12>>2]|0)-b|0);return}function Ga(a){a=a|0;var b=0,d=0,e=0;b=c[a>>2]|0;if(b){d=a+4|0;while(1){e=c[d>>2]|0;if((e|0)==(b|0))break;e=e+-16|0;c[d>>2]=e;Ha(e)}e=c[a>>2]|0;Ka(c[a+12>>2]|0,e,(c[a+8>>2]|0)-e|0)}return}function Ha(a){a=a|0;var b=0,d=0,e=0;b=c[a>>2]|0;if(b){d=a+4|0;while(1){e=c[d>>2]|0;if((e|0)==(b|0))break;e=e+-24|0;c[d>>2]=e;Ia(e)}e=c[a>>2]|0;Ka(c[a+12>>2]|0,e,(c[a+8>>2]|0)-e|0)}return}function Ia(a){a=a|0;Ja(a+12|0);Ja(a);return}function Ja(b){b=b|0;if(a[b>>0]&1)wc(c[b+8>>2]|0);return}function Ka(a,b,d){a=a|0;b=b|0;d=d|0;if(a>>>0<=b>>>0&(a+4096|0)>>>0>=b>>>0){a=a+4096|0;if((b+(d+15&-16)|0)==(c[a>>2]|0))c[a>>2]=b}else wc(b);return}function La(b,d,e,f){b=b|0;d=d|0;e=e|0;f=f|0;var g=0,h=0,j=0,k=0,l=0,m=0,n=0,o=0;o=i;i=i+48|0;l=o+24|0;m=o+12|0;n=o;a:do if(b>>>0<d>>>0){b:do if((a[b>>0]|0)!=95){if((Na(b,d,e)|0)!=(d|0)){c[f>>2]=-2;break a}}else{h=d;if((h-b|0)<=3){c[f>>2]=-2;break a}switch(a[b+1>>0]|0){case 90:{k=b+2|0;b=Ma(k,d,e)|0;if(!((b|0)==(k|0)|(b|0)==(d|0))?(a[b>>0]|0)==46:0){g=c[e+4>>2]|0;if((c[e>>2]|0)!=(g|0)){k=g+-24|0;h=h-b|0;if(h>>>0>4294967279)Xa();if(h>>>0<11){a[n>>0]=h<<1;j=n+1|0}else{g=h+16&-16;j=vc(g)|0;c[n+8>>2]=j;c[n>>2]=g|1;c[n+4>>2]=h}g=j;while(1){if((b|0)==(d|0))break;a[g>>0]=a[b>>0]|0;b=b+1|0;g=g+1|0}a[j+h>>0]=0;b=Ta(n,0,849)|0;c[m>>2]=c[b>>2];c[m+4>>2]=c[b+4>>2];c[m+8>>2]=c[b+8>>2];g=0;while(1){if((g|0)==3)break;c[b+(g<<2)>>2]=0;g=g+1|0}b=Ya(m,799)|0;c[l>>2]=c[b>>2];c[l+4>>2]=c[b+4>>2];c[l+8>>2]=c[b+8>>2];g=0;while(1){if((g|0)==3)break;c[b+(g<<2)>>2]=0;g=g+1|0}b=a[l>>0]|0;j=(b&1)==0;Za(k,j?l+1|0:c[l+8>>2]|0,j?(b&255)>>>1:c[l+4>>2]|0)|0;Ja(l);Ja(m);Ja(n);b=d}}if((b|0)==(d|0))break b;c[f>>2]=-2;break a}case 95:{if((a[b+2>>0]|0)==95?(a[b+3>>0]|0)==90:0){n=b+4|0;b=Ma(n,d,e)|0;if((b|0)==(n|0)|(b|0)==(d|0)){c[f>>2]=-2;break a}c:do if((h-b|0)>12){h=0;g=b;while(1){if((h|0)>=13)break;if((a[g>>0]|0)!=(a[2320+h>>0]|0))break c;h=h+1|0;g=g+1|0}d:do if((g|0)==(d|0))g=d;else{if((a[g>>0]|0)==95){h=g+1|0;if((h|0)==(d|0))break c;if(((a[h>>0]|0)+-48|0)>>>0>=10)break c;g=g+2|0}while(1){if((g|0)==(d|0)){g=d;break d}if(((a[g>>0]|0)+-48|0)>>>0>=10)break d;g=g+1|0}}while(0);h=c[e+4>>2]|0;if((c[e>>2]|0)!=(h|0)){Ta(h+-24|0,0,2334)|0;b=g}}while(0);if((b|0)==(d|0))break b;c[f>>2]=-2;break a}break}default:{}}c[f>>2]=-2;break a}while(0);if((c[f>>2]|0)==0?(c[e>>2]|0)==(c[e+4>>2]|0):0)c[f>>2]=-2}else c[f>>2]=-2;while(0);i=o;return}function Ma(b,d,e){b=b|0;d=d|0;e=e|0;var f=0,g=0,h=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0,s=0,t=0,u=0,v=0,w=0,x=0,y=0,z=0,A=0,B=0,C=0,D=0,E=0;E=i;i=i+80|0;z=E+60|0;y=E+48|0;r=E+36|0;s=E+24|0;t=E+12|0;w=E;a:do if((b|0)==(d|0))f=b;else{B=e+56|0;C=c[B>>2]|0;x=C+1|0;c[B>>2]=x;D=e+61|0;A=a[D>>0]|0;if(x>>>0>1)a[D>>0]=1;f=a[b>>0]|0;b:do switch(f|0){case 84:case 71:{c:do if((d-b|0)>2){switch(f|0){case 84:break;case 71:switch(a[b+1>>0]|0){case 86:{z=b+2|0;f=Wb(z,d,e)|0;if((f|0)==(z|0)){f=b;break c}g=c[e+4>>2]|0;if((c[e>>2]|0)==(g|0)){f=b;break c}Ta(g+-24|0,0,2275)|0;break c}case 82:{z=b+2|0;f=Wb(z,d,e)|0;if((f|0)==(z|0)){f=b;break c}g=c[e+4>>2]|0;if((c[e>>2]|0)==(g|0)){f=b;break c}Ta(g+-24|0,0,2295)|0;break c}default:{f=b;break c}}default:{f=b;break c}}f=b+1|0;switch(a[f>>0]|0){case 86:{z=b+2|0;f=Na(z,d,e)|0;if((f|0)==(z|0)){f=b;break c}g=c[e+4>>2]|0;if((c[e>>2]|0)==(g|0)){f=b;break c}Ta(g+-24|0,0,2124)|0;break c}case 84:{z=b+2|0;f=Na(z,d,e)|0;if((f|0)==(z|0)){f=b;break c}g=c[e+4>>2]|0;if((c[e>>2]|0)==(g|0)){f=b;break c}Ta(g+-24|0,0,2136)|0;break c}case 73:{z=b+2|0;f=Na(z,d,e)|0;if((f|0)==(z|0)){f=b;break c}g=c[e+4>>2]|0;if((c[e>>2]|0)==(g|0)){f=b;break c}Ta(g+-24|0,0,2145)|0;break c}case 83:{z=b+2|0;f=Na(z,d,e)|0;if((f|0)==(z|0)){f=b;break c}g=c[e+4>>2]|0;if((c[e>>2]|0)==(g|0)){f=b;break c}Ta(g+-24|0,0,2159)|0;break c}case 99:{z=b+2|0;f=Zb(z,d)|0;if((f|0)==(z|0)){f=b;break c}g=Zb(f,d)|0;if((g|0)==(f|0)){f=b;break c}f=Ma(g,d,e)|0;if((f|0)==(g|0)){f=b;break c}g=c[e+4>>2]|0;if((c[e>>2]|0)==(g|0)){f=b;break c}Ta(g+-24|0,0,2178)|0;break c}case 67:{x=b+2|0;f=Na(x,d,e)|0;if((f|0)==(x|0)){f=b;break c}g=tb(f,d)|0;if((g|0)==(f|0)|(g|0)==(d|0)){f=b;break c}if((a[g>>0]|0)!=95){f=b;break c}x=g+1|0;f=Na(x,d,e)|0;if((f|0)==(x|0)){f=b;break c}j=e+4|0;g=c[j>>2]|0;if(((g-(c[e>>2]|0)|0)/24|0)>>>0<2){f=b;break c}Cb(z,g+-24|0);k=c[j>>2]|0;g=k+-24|0;h=k;while(1){if((h|0)==(g|0))break;b=h+-24|0;c[j>>2]=b;Ia(b);h=c[j>>2]|0}q=k+-48|0;g=Ta(z,0,2205)|0;c[s>>2]=c[g>>2];c[s+4>>2]=c[g+4>>2];c[s+8>>2]=c[g+8>>2];h=0;while(1){if((h|0)==3)break;c[g+(h<<2)>>2]=0;h=h+1|0}g=Ya(s,2230)|0;c[r>>2]=c[g>>2];c[r+4>>2]=c[g+4>>2];c[r+8>>2]=c[g+8>>2];h=0;while(1){if((h|0)==3)break;c[g+(h<<2)>>2]=0;h=h+1|0}Cb(t,(c[j>>2]|0)+-24|0);g=a[t>>0]|0;h=(g&1)==0;g=Za(r,h?t+1|0:c[t+8>>2]|0,h?(g&255)>>>1:c[t+4>>2]|0)|0;c[y>>2]=c[g>>2];c[y+4>>2]=c[g+4>>2];c[y+8>>2]=c[g+8>>2];h=0;while(1){if((h|0)==3)break;c[g+(h<<2)>>2]=0;h=h+1|0}do if(a[q>>0]&1){p=k+-40|0;a[c[p>>2]>>0]=0;m=k+-44|0;c[m>>2]=0;g=a[q>>0]|0;if(!(g&1))l=10;else{l=c[q>>2]|0;g=l&255;l=(l&-2)+-1|0}if(!(g&1)){h=(g&255)>>>1;if((g&255)<22){k=10;n=h;o=1}else{k=(h+16&240)+-1|0;n=h;o=1}}else{k=10;n=0;o=0}if((k|0)!=(l|0)){if((k|0)==10){j=q+1|0;h=c[p>>2]|0;if(o){Fc(j|0,h|0,((g&255)>>>1)+1|0)|0;wc(h)}else{a[j>>0]=a[h>>0]|0;wc(h)}a[q>>0]=n<<1;break}h=k+1|0;j=vc(h)|0;if(!(k>>>0<=l>>>0&(j|0)==0)){if(o)Fc(j|0,q+1|0,((g&255)>>>1)+1|0)|0;else{b=c[p>>2]|0;a[j>>0]=a[b>>0]|0;wc(b)}c[q>>2]=h|1;c[m>>2]=n;c[p>>2]=j}}}else{a[q+1>>0]=0;a[q>>0]=0}while(0);c[q>>2]=c[y>>2];c[q+4>>2]=c[y+4>>2];c[q+8>>2]=c[y+8>>2];g=0;while(1){if((g|0)==3)break;c[y+(g<<2)>>2]=0;g=g+1|0}Ja(y);Ja(t);Ja(r);Ja(s);Ja(z);break c}default:{g=Zb(f,d)|0;if((g|0)==(f|0)){f=b;break c}f=Ma(g,d,e)|0;if((f|0)==(g|0)){f=b;break c}g=c[e+4>>2]|0;if((c[e>>2]|0)==(g|0)){f=b;break c}g=g+-24|0;if((a[b+2>>0]|0)==118){Ta(g,0,2235)|0;break c}else{Ta(g,0,2253)|0;break c}}}}else f=b;while(0);break}default:{f=Wb(b,d,e)|0;u=c[e+48>>2]|0;v=c[e+52>>2]|0;if((f|0)!=(b|0))if((f|0)!=(d|0)){switch(a[f>>0]|0){case 46:case 69:break b;default:{}}x=a[D>>0]|0;a[D>>0]=0;g=0;while(1){if((g|0)==3)break;c[z+(g<<2)>>2]=0;g=g+1|0}t=e+4|0;m=c[t>>2]|0;d:do if((c[e>>2]|0)!=(m|0)){l=m+-24|0;j=a[l>>0]|0;k=(j&1)==0;if(k)g=(j&255)>>>1;else g=c[m+-20>>2]|0;if(g){if(!(a[e+60>>0]|0)){if(k){g=l+1|0;h=(j&255)>>>1}else{g=c[m+-16>>2]|0;h=c[m+-20>>2]|0}if((a[g+h+-1>>0]|0)==62){if(k){g=(j&255)>>>1;h=l+1|0}else{g=c[m+-20>>2]|0;h=c[m+-16>>2]|0}if((a[h+(g+-2)>>0]|0)!=45){if(k){h=(j&255)>>>1;g=l+1|0}else{h=c[m+-20>>2]|0;g=c[m+-16>>2]|0}if((a[g+(h+-2)>>0]|0)!=62){o=Na(f,d,e)|0;if((o|0)==(f|0)){f=b;g=0;break}s=c[t>>2]|0;f=s;if(((s-(c[e>>2]|0)|0)/24|0)>>>0<2){f=b;g=0;break}g=f+-24|0;c[y>>2]=c[g>>2];c[y+4>>2]=c[g+4>>2];c[y+8>>2]=c[g+8>>2];h=0;while(1){if((h|0)==3)break;c[g+(h<<2)>>2]=0;h=h+1|0}n=f+-12|0;e:do if(!(a[z>>0]&1)){a[z+1>>0]=0;a[z>>0]=0}else{k=z+8|0;g=c[k>>2]|0;a[g>>0]=0;l=z+4|0;c[l>>2]=0;f=c[z>>2]|0;m=(f&-2)+-1|0;h=f&255;do if(!(h&1)){f=f>>>1&127;if((h&255)<22){Fc(z+1|0,g|0,f+1|0)|0;wc(g);break}g=f+16&240;j=g+-1|0;if((j|0)==(m|0))break e;h=vc(g)|0;if(j>>>0<=m>>>0&(h|0)==0)break e;Fc(h|0,z+1|0,f+1|0)|0;c[z>>2]=g|1;c[l>>2]=f;c[k>>2]=h;break e}else{a[z+1>>0]=0;wc(g);f=0}while(0);a[z>>0]=f<<1}while(0);c[z>>2]=c[n>>2];c[z+4>>2]=c[n+4>>2];c[z+8>>2]=c[n+8>>2];f=0;while(1){if((f|0)==3)break;c[n+(f<<2)>>2]=0;f=f+1|0}s=a[z>>0]|0;if(!(((s&1)==0?(s&255)>>>1:c[z+4>>2]|0)|0))zb(y,32);f=c[t>>2]|0;g=f+-24|0;h=f;while(1){if((h|0)==(g|0))break;s=h+-24|0;c[t>>2]=s;Ia(s);h=c[t>>2]|0}g=a[y>>0]|0;s=(g&1)==0;Ua(f+-48|0,0,s?y+1|0:c[y+8>>2]|0,s?(g&255)>>>1:c[y+4>>2]|0)|0;Ja(y);g=c[t>>2]|0;f=o}else g=m}else g=m}else g=m}else g=m;zb(g+-24|0,40);if((f|0)!=(d|0)?(a[f>>0]|0)==118:0){h=c[e>>2]|0;g=c[t>>2]|0;f=f+1|0}else p=128;do if((p|0)==128){n=y+4|0;o=w+8|0;p=w+1|0;q=w+4|0;r=y+8|0;s=y+1|0;l=1;f:while(1){h=c[e>>2]|0;g=c[t>>2]|0;while(1){j=(g-h|0)/24|0;m=Na(f,d,e)|0;g=c[t>>2]|0;h=c[e>>2]|0;k=(g-h|0)/24|0;if((m|0)==(f|0)){p=151;break f}if(k>>>0>j>>>0)break;else f=m}f=0;while(1){if((f|0)==3){f=j;break}c[y+(f<<2)>>2]=0;f=f+1|0}while(1){if(f>>>0>=k>>>0){h=j;break}h=a[y>>0]|0;if(((h&1)==0?(h&255)>>>1:c[n>>2]|0)|0)Ya(y,1429)|0;Cb(w,(c[e>>2]|0)+(f*24|0)|0);h=a[w>>0]|0;g=(h&1)==0;Za(y,g?p:c[o>>2]|0,g?(h&255)>>>1:c[q>>2]|0)|0;Ja(w);f=f+1|0}while(1){if(h>>>0>=k>>>0)break;g=c[t>>2]|0;f=g+-24|0;while(1){if((g|0)==(f|0))break;j=g+-24|0;c[t>>2]=j;Ia(j);g=c[t>>2]|0}h=h+1|0}h=a[y>>0]|0;f=c[n>>2]|0;if(!(((h&1)==0?(h&255)>>>1:f)|0))f=l;else{g=c[t>>2]|0;if((c[e>>2]|0)==(g|0)){p=163;break}if(!l){Ya(g+-24|0,1429)|0;g=c[t>>2]|0;h=a[y>>0]|0;f=c[n>>2]|0}l=(h&1)==0;Za(g+-24|0,l?s:c[r>>2]|0,l?(h&255)>>>1:f)|0;f=0}Ja(y);l=f;f=m}if((p|0)==151)break;else if((p|0)==163){Ja(y);f=b;g=0;break d}}while(0);if((h|0)!=(g|0)){zb(g+-24|0,41);if(u&1)Ya((c[t>>2]|0)+-24|0,267)|0;if(u&2)Ya((c[t>>2]|0)+-24|0,456)|0;if(u&4)Ya((c[t>>2]|0)+-24|0,466)|0;switch(v|0){case 1:{Ya((c[t>>2]|0)+-24|0,2032)|0;break}case 2:{Ya((c[t>>2]|0)+-24|0,2035)|0;break}default:{}}g=a[z>>0]|0;y=(g&1)==0;Za((c[t>>2]|0)+-24|0,y?z+1|0:c[z+8>>2]|0,y?(g&255)>>>1:c[z+4>>2]|0)|0;g=1}else{f=b;g=0}}else{f=b;g=0}}else{f=b;g=0}while(0);Ja(z);a[D>>0]=x;if(!g){a[D>>0]=A;c[B>>2]=C;f=b;break a}}else f=d;else f=b}}while(0);a[D>>0]=A;c[B>>2]=C}while(0);i=E;return f|0}function Na(d,e,f){d=d|0;e=e|0;f=f|0;var g=0,h=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0,s=0,t=0,u=0,v=0,w=0,x=0,y=0,z=0,A=0,B=0,C=0,D=0,E=0,F=0,G=0,H=0,I=0,J=0,K=0,L=0,M=0,N=0,O=0,P=0,Q=0,R=0,S=0,T=0,U=0,V=0,W=0,X=0,Y=0,Z=0,_=0,$=0,aa=0,ba=0,ca=0,da=0,ea=0,fa=0,ga=0,ha=0,ia=0,ja=0,ka=0,la=0,ma=0,na=0,oa=0,pa=0,qa=0,ra=0,sa=0,ta=0,ua=0,va=0,wa=0,xa=0,ya=0,za=0;za=i;i=i+736|0;ya=za+704|0;xa=za+680|0;la=za+668|0;ca=za+656|0;fa=za+632|0;pa=za+608|0;sa=za+584|0;ia=za+572|0;oa=za+560|0;qa=za+548|0;ra=za+536|0;$=za+384|0;ja=za+520|0;ha=za+512|0;A=za+496|0;o=za+488|0;S=za+472|0;O=za+464|0;B=za+448|0;p=za+440|0;na=za+424|0;ma=za+420|0;T=za+408|0;da=za+396|0;ea=za+372|0;U=za+360|0;X=za+344|0;V=za+340|0;t=za+328|0;v=za+304|0;w=za+288|0;x=za+276|0;y=za+264|0;E=za+240|0;F=za+228|0;G=za+216|0;H=za+204|0;I=za+192|0;L=za+168|0;M=za+156|0;N=za+144|0;W=za+128|0;R=za+120|0;z=za+104|0;n=za+96|0;D=za+80|0;s=za+72|0;C=za+56|0;r=za+48|0;ga=za+32|0;ba=za+24|0;wa=za+8|0;va=za;a:do if((d|0)!=(e|0)){switch(a[d>>0]|0){case 75:case 86:case 114:{c[xa>>2]=0;h=Oa(d,e,xa)|0;b:do if((h|0)!=(d|0)?(j=a[h>>0]|0,Z=f+4|0,q=((c[Z>>2]|0)-(c[f>>2]|0)|0)/24|0,Y=Na(h,e,f)|0,Z=((c[Z>>2]|0)-(c[f>>2]|0)|0)/24|0,(Y|0)!=(h|0)):0){v=j<<24>>24==70;w=f+20|0;h=c[w>>2]|0;c:do if(v){j=h+-16|0;while(1){if((h|0)==(j|0)){h=j;break c}d=h+-16|0;c[w>>2]=d;Ha(d);h=c[w>>2]|0}}while(0);n=f+16|0;o=c[f+12>>2]|0;d=c[f+24>>2]|0;j=d;if(h>>>0<d>>>0){c[h>>2]=0;c[h+4>>2]=0;c[h+8>>2]=0;c[h+12>>2]=o;c[w>>2]=(c[w>>2]|0)+16}else{k=c[n>>2]|0;d=h-k|0;m=d>>4;l=m+1|0;if((d|0)<-16)Pa();h=j-k|0;if(h>>4>>>0<1073741823){h=h>>3;h=h>>>0<l>>>0?l:h}else h=2147483647;Qa(ya,h,m,f+28|0);d=ya+8|0;e=c[d>>2]|0;c[e>>2]=0;c[e+4>>2]=0;c[e+8>>2]=0;c[e+12>>2]=o;c[d>>2]=e+16;Ra(n,ya);Sa(ya)}t=c[xa>>2]|0;r=(t&1|0)==0;s=(t&2|0)==0;t=(t&4|0)==0;u=ya+8|0;while(1){if(q>>>0>=Z>>>0){g=Y;break b}if(v){k=c[f>>2]|0;o=k+(q*24|0)+12|0;l=a[o>>0]|0;h=(l&1)==0;if(h){m=(l&255)>>>1;j=o+1|0}else{m=c[k+(q*24|0)+16>>2]|0;j=c[k+(q*24|0)+20>>2]|0}n=m+-2|0;if((a[j+n>>0]|0)==38)h=m+-3|0;else{if(h){j=o+1|0;h=(l&255)>>>1}else{j=c[k+(q*24|0)+20>>2]|0;h=c[k+(q*24|0)+16>>2]|0}h=(a[j+h+-1>>0]|0)==38?n:m}if(!r){Ta(o,h,267)|0;h=h+6|0}if(!s){Ta((c[f>>2]|0)+(q*24|0)+12|0,h,456)|0;h=h+9|0}if(!t)Ta((c[f>>2]|0)+(q*24|0)+12|0,h,466)|0}else{if(!r)Ya((c[f>>2]|0)+(q*24|0)|0,267)|0;if(!s)Ya((c[f>>2]|0)+(q*24|0)|0,456)|0;if(!t)Ya((c[f>>2]|0)+(q*24|0)|0,466)|0}m=c[w>>2]|0;n=m+-16|0;o=c[f>>2]|0;p=o+(q*24|0)|0;h=m+-12|0;j=c[h>>2]|0;d=c[m+-8>>2]|0;k=d;if((j|0)==(d|0)){h=c[n>>2]|0;d=j-h|0;l=(d|0)/24|0;j=l+1|0;if((d|0)<-24)break;h=(k-h|0)/24|0;if(h>>>0<1073741823){h=h<<1;h=h>>>0<j>>>0?j:h}else h=2147483647;ab(ya,h,l,m+-4|0);d=c[u>>2]|0;_a(d,p);_a(d+12|0,o+(q*24|0)+12|0);c[u>>2]=d+24;cb(n,ya);bb(ya)}else{_a(j,p);_a(j+12|0,o+(q*24|0)+12|0);c[h>>2]=(c[h>>2]|0)+24}q=q+1|0}Pa()}else g=d;while(0);break a}default:{}}g=eb(d,e,f)|0;if((g|0)==(d|0)){h=a[d>>0]|0;d:do switch(h<<24>>24|0){case 65:{do if(h<<24>>24==65?(u=d+1|0,(u|0)!=(e|0)):0){g=a[u>>0]|0;if(g<<24>>24==95){xa=d+2|0;g=Na(xa,e,f)|0;if((g|0)==(xa|0)){g=d;break}h=f+4|0;j=c[h>>2]|0;if((c[f>>2]|0)==(j|0)){g=d;break}e=j+-12|0;wa=a[e>>0]|0;xa=(wa&1)==0;wa=xa?(wa&255)>>>1:c[j+-8>>2]|0;$a(ya,xa?e+1|0:c[j+-4>>2]|0,wa>>>0<2?wa:2);wa=a[ya>>0]|0;e=(wa&1)==0;wa=e?(wa&255)>>>1:c[ya+4>>2]|0;xa=wa>>>0>2;e=ac(e?ya+1|0:c[ya+8>>2]|0,790,xa?2:wa)|0;Ja(ya);if(!(((e|0)==0?(wa>>>0<2?-1:xa&1):e)|0))sb((c[h>>2]|0)+-12|0);Ta((c[h>>2]|0)+-12|0,0,793)|0;break}if((g+-49&255)<9){m=tb(u,e)|0;if((m|0)==(e|0)){g=d;break}if((a[m>>0]|0)!=95){g=d;break}wa=m+1|0;g=Na(wa,e,f)|0;if((g|0)==(wa|0)){g=d;break}h=f+4|0;j=c[h>>2]|0;if((c[f>>2]|0)==(j|0)){g=d;break}e=j+-12|0;va=a[e>>0]|0;wa=(va&1)==0;va=wa?(va&255)>>>1:c[j+-8>>2]|0;$a(xa,wa?e+1|0:c[j+-4>>2]|0,va>>>0<2?va:2);va=a[xa>>0]|0;e=(va&1)==0;va=e?(va&255)>>>1:c[xa+4>>2]|0;wa=va>>>0>2;e=ac(e?xa+1|0:c[xa+8>>2]|0,790,wa?2:va)|0;Ja(xa);if(!(((e|0)==0?(va>>>0<2?-1:wa&1):e)|0))sb((c[h>>2]|0)+-12|0);n=(c[h>>2]|0)+-12|0;k=m-u|0;if(k>>>0>4294967279)Xa();if(k>>>0<11){a[fa>>0]=k<<1;l=fa+1|0}else{e=k+16&-16;l=vc(e)|0;c[fa+8>>2]=l;c[fa>>2]=e|1;c[fa+4>>2]=k}h=u;j=l;while(1){if((h|0)==(m|0))break;a[j>>0]=a[h>>0]|0;h=h+1|0;j=j+1|0}a[l+k>>0]=0;h=Ta(fa,0,790)|0;c[ca>>2]=c[h>>2];c[ca+4>>2]=c[h+4>>2];c[ca+8>>2]=c[h+8>>2];j=0;while(1){if((j|0)==3)break;c[h+(j<<2)>>2]=0;j=j+1|0}h=Ya(ca,4264)|0;c[la>>2]=c[h>>2];c[la+4>>2]=c[h+4>>2];c[la+8>>2]=c[h+8>>2];j=0;while(1){if((j|0)==3)break;c[h+(j<<2)>>2]=0;j=j+1|0}e=a[la>>0]|0;xa=(e&1)==0;Ua(n,0,xa?la+1|0:c[la+8>>2]|0,xa?(e&255)>>>1:c[la+4>>2]|0)|0;Ja(la);Ja(ca);Ja(fa);break}g=ub(u,e,f)|0;if(((!((g|0)==(u|0)|(g|0)==(e|0))?(a[g>>0]|0)==95:0)?(xa=g+1|0,aa=Na(xa,e,f)|0,(aa|0)!=(xa|0)):0)?(P=f+4|0,m=c[P>>2]|0,((m-(c[f>>2]|0)|0)/24|0)>>>0>=2):0){db(pa,m+-24|0);g=c[P>>2]|0;h=g+-24|0;j=g;while(1){if((j|0)==(h|0))break;e=j+-24|0;c[P>>2]=e;Ia(e);j=c[P>>2]|0}db(sa,g+-48|0);g=c[P>>2]|0;q=g+-24|0;do if(a[q>>0]&1){p=g+-16|0;a[c[p>>2]>>0]=0;m=g+-20|0;c[m>>2]=0;g=a[q>>0]|0;if(!(g&1))l=10;else{l=c[q>>2]|0;g=l&255;l=(l&-2)+-1|0}if(!(g&1)){h=(g&255)>>>1;if((g&255)<22){o=1;k=10;n=h}else{o=1;k=(h+16&240)+-1|0;n=h}}else{o=0;k=10;n=0}if((k|0)!=(l|0)){if((k|0)==10){j=q+1|0;h=c[p>>2]|0;if(o){Fc(j|0,h|0,((g&255)>>>1)+1|0)|0;wc(h)}else{a[j>>0]=a[h>>0]|0;wc(h)}a[q>>0]=n<<1;break}h=k+1|0;j=vc(h)|0;if(!(k>>>0<=l>>>0&(j|0)==0)){if(o)Fc(j|0,q+1|0,((g&255)>>>1)+1|0)|0;else{e=c[p>>2]|0;a[j>>0]=a[e>>0]|0;wc(e)}c[q>>2]=h|1;c[m>>2]=n;c[p>>2]=j}}}else{a[q+1>>0]=0;a[q>>0]=0}while(0);c[q>>2]=c[pa>>2];c[q+4>>2]=c[pa+4>>2];c[q+8>>2]=c[pa+8>>2];g=0;while(1){if((g|0)==3)break;c[pa+(g<<2)>>2]=0;g=g+1|0}j=pa+12|0;wa=a[j>>0]|0;e=(wa&1)==0;k=pa+16|0;wa=e?(wa&255)>>>1:c[k>>2]|0;l=pa+20|0;m=j+1|0;$a(ia,e?m:c[l>>2]|0,wa>>>0<2?wa:2);wa=a[ia>>0]|0;e=(wa&1)==0;wa=e?(wa&255)>>>1:c[ia+4>>2]|0;xa=wa>>>0>2;e=ac(e?ia+1|0:c[ia+8>>2]|0,790,xa?2:wa)|0;Ja(ia);if(!(((e|0)==0?(wa>>>0<2?-1:xa&1):e)|0))sb(j);n=c[P>>2]|0;q=n+-12|0;Cb($,sa);g=Ta($,0,790)|0;c[ra>>2]=c[g>>2];c[ra+4>>2]=c[g+4>>2];c[ra+8>>2]=c[g+8>>2];h=0;while(1){if((h|0)==3)break;c[g+(h<<2)>>2]=0;h=h+1|0}g=Ya(ra,4264)|0;c[qa>>2]=c[g>>2];c[qa+4>>2]=c[g+4>>2];c[qa+8>>2]=c[g+8>>2];h=0;while(1){if((h|0)==3)break;c[g+(h<<2)>>2]=0;h=h+1|0}g=a[j>>0]|0;h=(g&1)==0;g=Za(qa,h?m:c[l>>2]|0,h?(g&255)>>>1:c[k>>2]|0)|0;c[oa>>2]=c[g>>2];c[oa+4>>2]=c[g+4>>2];c[oa+8>>2]=c[g+8>>2];h=0;while(1){if((h|0)==3)break;c[g+(h<<2)>>2]=0;h=h+1|0}do if(a[q>>0]&1){p=n+-4|0;a[c[p>>2]>>0]=0;m=n+-8|0;c[m>>2]=0;g=a[q>>0]|0;if(!(g&1))l=10;else{l=c[q>>2]|0;g=l&255;l=(l&-2)+-1|0}do if(!(g&1)){h=(g&255)>>>1;if((g&255)<22){o=1;k=10;n=h;break}o=1;k=(h+16&240)+-1|0;n=h}else{o=0;k=10;n=0}while(0);if((k|0)!=(l|0)){if((k|0)==10){j=q+1|0;h=c[p>>2]|0;if(o){Fc(j|0,h|0,((g&255)>>>1)+1|0)|0;wc(h)}else{a[j>>0]=a[h>>0]|0;wc(h)}a[q>>0]=n<<1;break}h=k+1|0;j=vc(h)|0;if(k>>>0<=l>>>0&(j|0)==0)break;if(o)Fc(j|0,q+1|0,((g&255)>>>1)+1|0)|0;else{e=c[p>>2]|0;a[j>>0]=a[e>>0]|0;wc(e)}c[q>>2]=h|1;c[m>>2]=n;c[p>>2]=j}}else{a[q+1>>0]=0;a[q>>0]=0}while(0);c[q>>2]=c[oa>>2];c[q+4>>2]=c[oa+4>>2];c[q+8>>2]=c[oa+8>>2];g=0;while(1){if((g|0)==3)break;c[oa+(g<<2)>>2]=0;g=g+1|0}Ja(oa);Ja(qa);Ja(ra);Ja($);Ia(sa);Ia(pa);g=aa}else g=d}else g=d;while(0);if((g|0)==(d|0)){g=d;break a}h=c[f+4>>2]|0;if((c[f>>2]|0)==(h|0)){g=d;break a}m=f+16|0;c[ha>>2]=c[f+12>>2];Pb(ja,h+-24|0,ha);h=f+20|0;j=c[h>>2]|0;d=c[f+24>>2]|0;k=d;if(j>>>0<d>>>0){c[j+12>>2]=c[ja+12>>2];c[j>>2]=c[ja>>2];ya=ja+4|0;c[j+4>>2]=c[ya>>2];f=ja+8|0;c[j+8>>2]=c[f>>2];c[f>>2]=0;c[ya>>2]=0;c[ja>>2]=0;c[h>>2]=(c[h>>2]|0)+16}else{h=c[m>>2]|0;d=j-h|0;l=d>>4;j=l+1|0;if((d|0)<-16)Pa();h=k-h|0;if(h>>4>>>0<1073741823){h=h>>3;h=h>>>0<j>>>0?j:h}else h=2147483647;Qa(ya,h,l,f+28|0);f=ya+8|0;d=c[f>>2]|0;c[d+12>>2]=c[ja+12>>2];c[d>>2]=c[ja>>2];e=ja+4|0;c[d+4>>2]=c[e>>2];xa=ja+8|0;c[d+8>>2]=c[xa>>2];c[xa>>2]=0;c[e>>2]=0;c[ja>>2]=0;c[f>>2]=d+16;Ra(m,ya);Sa(ya)}Ha(ja);break a}case 67:{xa=d+1|0;g=Na(xa,e,f)|0;if((g|0)==(xa|0)){g=d;break a}j=f+4|0;h=c[j>>2]|0;if((c[f>>2]|0)==(h|0)){g=d;break a}Ya(h+-24|0,2023)|0;m=f+16|0;h=(c[j>>2]|0)+-24|0;c[o>>2]=c[f+12>>2];Pb(A,h,o);h=f+20|0;j=c[h>>2]|0;d=c[f+24>>2]|0;k=d;if(j>>>0<d>>>0){c[j+12>>2]=c[A+12>>2];c[j>>2]=c[A>>2];ya=A+4|0;c[j+4>>2]=c[ya>>2];f=A+8|0;c[j+8>>2]=c[f>>2];c[f>>2]=0;c[ya>>2]=0;c[A>>2]=0;c[h>>2]=(c[h>>2]|0)+16}else{h=c[m>>2]|0;d=j-h|0;l=d>>4;j=l+1|0;if((d|0)<-16)Pa();h=k-h|0;if(h>>4>>>0<1073741823){h=h>>3;h=h>>>0<j>>>0?j:h}else h=2147483647;Qa(ya,h,l,f+28|0);f=ya+8|0;d=c[f>>2]|0;c[d+12>>2]=c[A+12>>2];c[d>>2]=c[A>>2];e=A+4|0;c[d+4>>2]=c[e>>2];xa=A+8|0;c[d+8>>2]=c[xa>>2];c[xa>>2]=0;c[e>>2]=0;c[A>>2]=0;c[f>>2]=d+16;Ra(m,ya);Sa(ya)}Ha(A);break a}case 70:{do if(h<<24>>24==70){g=d+1|0;if((g|0)!=(e|0)){if((a[g>>0]|0)==89){g=d+2|0;if((g|0)==(e|0))break}h=Na(g,e,f)|0;if((h|0)!=(g|0)){$a(ya,797,1);r=f+4|0;q=ya+4|0;m=xa+8|0;n=xa+1|0;o=xa+4|0;p=0;g=h;e:while(1){j=g;f:while(1){if((j|0)==(e|0)){ta=170;break e}switch(a[j>>0]|0){case 69:{ta=174;break e}case 118:{j=j+1|0;continue f}case 82:{g=j+1|0;if((g|0)!=(e|0)?(a[g>>0]|0)==69:0){p=1;continue e}break}case 79:{g=j+1|0;if((g|0)!=(e|0)?(a[g>>0]|0)==69:0){p=2;continue e}break}default:{}}h=((c[r>>2]|0)-(c[f>>2]|0)|0)/24|0;k=Na(j,e,f)|0;l=((c[r>>2]|0)-(c[f>>2]|0)|0)/24|0;if((k|0)==(j|0)|(k|0)==(e|0))break e;else g=h;while(1){if(g>>>0>=l>>>0)break;wa=a[ya>>0]|0;if(((wa&1)==0?(wa&255)>>>1:c[q>>2]|0)>>>0>1)Ya(ya,1429)|0;Cb(xa,(c[f>>2]|0)+(g*24|0)|0);wa=a[xa>>0]|0;va=(wa&1)==0;Za(ya,va?n:c[m>>2]|0,va?(wa&255)>>>1:c[o>>2]|0)|0;Ja(xa);g=g+1|0}while(1){if(h>>>0>=l>>>0){j=k;continue f}j=c[r>>2]|0;g=j+-24|0;while(1){if((j|0)==(g|0))break;wa=j+-24|0;c[r>>2]=wa;Ia(wa);j=c[r>>2]|0}h=h+1|0}}}g:do if((ta|0)==170){h=c[r>>2]|0;g=h+-24|0;while(1){if((h|0)==(g|0))break g;f=h+-24|0;c[r>>2]=f;Ia(f);h=c[r>>2]|0}}else if((ta|0)==174){g=j+1|0;Ya(ya,799)|0;switch(p|0){case 1:{Ya(ya,2032)|0;break}case 2:{Ya(ya,2035)|0;break}default:{}}h=c[r>>2]|0;if((c[f>>2]|0)!=(h|0)){Ya(h+-24|0,1882)|0;e=a[ya>>0]|0;xa=(e&1)==0;Ua((c[r>>2]|0)+-12|0,0,xa?ya+1|0:c[ya+8>>2]|0,xa?(e&255)>>>1:c[q>>2]|0)|0;Ja(ya);if((g|0)==(d|0)){g=d;break a}h=c[r>>2]|0;if((c[f>>2]|0)==(h|0)){g=d;break a}m=f+16|0;c[O>>2]=c[f+12>>2];Pb(S,h+-24|0,O);h=f+20|0;j=c[h>>2]|0;d=c[f+24>>2]|0;k=d;if(j>>>0<d>>>0){c[j+12>>2]=c[S+12>>2];c[j>>2]=c[S>>2];ya=S+4|0;c[j+4>>2]=c[ya>>2];f=S+8|0;c[j+8>>2]=c[f>>2];c[f>>2]=0;c[ya>>2]=0;c[S>>2]=0;c[h>>2]=(c[h>>2]|0)+16}else{h=c[m>>2]|0;d=j-h|0;l=d>>4;j=l+1|0;if((d|0)<-16)Pa();h=k-h|0;if(h>>4>>>0<1073741823){h=h>>3;h=h>>>0<j>>>0?j:h}else h=2147483647;Qa(ya,h,l,f+28|0);f=ya+8|0;d=c[f>>2]|0;c[d+12>>2]=c[S+12>>2];c[d>>2]=c[S>>2];e=S+4|0;c[d+4>>2]=c[e>>2];xa=S+8|0;c[d+8>>2]=c[xa>>2];c[xa>>2]=0;c[e>>2]=0;c[S>>2]=0;c[f>>2]=d+16;Ra(m,ya);Sa(ya)}Ha(S);break a}}while(0);Ja(ya);break}}g=d;break a}while(0);g=d;break a}case 71:{xa=d+1|0;g=Na(xa,e,f)|0;if((g|0)==(xa|0)){g=d;break a}j=f+4|0;h=c[j>>2]|0;if((c[f>>2]|0)==(h|0)){g=d;break a}Ya(h+-24|0,2039)|0;m=f+16|0;h=(c[j>>2]|0)+-24|0;c[p>>2]=c[f+12>>2];Pb(B,h,p);h=f+20|0;j=c[h>>2]|0;d=c[f+24>>2]|0;k=d;if(j>>>0<d>>>0){c[j+12>>2]=c[B+12>>2];c[j>>2]=c[B>>2];ya=B+4|0;c[j+4>>2]=c[ya>>2];f=B+8|0;c[j+8>>2]=c[f>>2];c[f>>2]=0;c[ya>>2]=0;c[B>>2]=0;c[h>>2]=(c[h>>2]|0)+16}else{h=c[m>>2]|0;d=j-h|0;l=d>>4;j=l+1|0;if((d|0)<-16)Pa();h=k-h|0;if(h>>4>>>0<1073741823){h=h>>3;h=h>>>0<j>>>0?j:h}else h=2147483647;Qa(ya,h,l,f+28|0);f=ya+8|0;d=c[f>>2]|0;c[d+12>>2]=c[B+12>>2];c[d>>2]=c[B>>2];e=B+4|0;c[d+4>>2]=c[e>>2];xa=B+8|0;c[d+8>>2]=c[xa>>2];c[xa>>2]=0;c[e>>2]=0;c[B>>2]=0;c[f>>2]=d+16;Ra(m,ya);Sa(ya)}Ha(B);break a}case 77:{if(((h<<24>>24==77?(wa=d+1|0,k=Na(wa,e,f)|0,(k|0)!=(wa|0)):0)?(ka=Na(k,e,f)|0,(ka|0)!=(k|0)):0)?(_=f+4|0,l=c[_>>2]|0,((l-(c[f>>2]|0)|0)/24|0)>>>0>=2):0){db(ya,l+-24|0);g=c[_>>2]|0;h=g+-24|0;j=g;while(1){if((j|0)==(h|0))break;e=j+-24|0;c[_>>2]=e;Ia(e);j=c[_>>2]|0}db(xa,g+-48|0);r=ya+12|0;j=c[_>>2]|0;q=j+-24|0;h:do if((a[((a[r>>0]&1)==0?r+1|0:c[ya+20>>2]|0)>>0]|0)==40){g=Ya(ya,797)|0;c[fa>>2]=c[g>>2];c[fa+4>>2]=c[g+4>>2];c[fa+8>>2]=c[g+8>>2];h=0;while(1){if((h|0)==3)break;c[g+(h<<2)>>2]=0;h=h+1|0}Cb(pa,xa);g=a[pa>>0]|0;h=(g&1)==0;g=Za(fa,h?pa+1|0:c[pa+8>>2]|0,h?(g&255)>>>1:c[pa+4>>2]|0)|0;c[ca>>2]=c[g>>2];c[ca+4>>2]=c[g+4>>2];c[ca+8>>2]=c[g+8>>2];h=0;while(1){if((h|0)==3)break;c[g+(h<<2)>>2]=0;h=h+1|0}g=Ya(ca,2050)|0;c[la>>2]=c[g>>2];c[la+4>>2]=c[g+4>>2];c[la+8>>2]=c[g+8>>2];h=0;while(1){if((h|0)==3)break;c[g+(h<<2)>>2]=0;h=h+1|0}do if(a[q>>0]&1){p=j+-16|0;a[c[p>>2]>>0]=0;m=j+-20|0;c[m>>2]=0;g=a[q>>0]|0;if(!(g&1))l=10;else{l=c[q>>2]|0;g=l&255;l=(l&-2)+-1|0}if(!(g&1)){h=(g&255)>>>1;if((g&255)<22){o=1;k=10;n=h}else{o=1;k=(h+16&240)+-1|0;n=h}}else{o=0;k=10;n=0}if((k|0)!=(l|0)){if((k|0)==10){j=q+1|0;h=c[p>>2]|0;if(o){Fc(j|0,h|0,((g&255)>>>1)+1|0)|0;wc(h)}else{a[j>>0]=a[h>>0]|0;wc(h)}a[q>>0]=n<<1;break}h=k+1|0;j=vc(h)|0;if(!(k>>>0<=l>>>0&(j|0)==0)){if(o)Fc(j|0,q+1|0,((g&255)>>>1)+1|0)|0;else{e=c[p>>2]|0;a[j>>0]=a[e>>0]|0;wc(e)}c[q>>2]=h|1;c[m>>2]=n;c[p>>2]=j}}}else{a[q+1>>0]=0;a[q>>0]=0}while(0);c[q>>2]=c[la>>2];c[q+4>>2]=c[la+4>>2];c[q+8>>2]=c[la+8>>2];g=0;while(1){if((g|0)==3)break;c[la+(g<<2)>>2]=0;g=g+1|0}Ja(la);Ja(ca);Ja(pa);Ja(fa);j=c[_>>2]|0;g=Ta(r,0,799)|0;c[sa>>2]=c[g>>2];c[sa+4>>2]=c[g+4>>2];c[sa+8>>2]=c[g+8>>2];h=0;while(1){if((h|0)==3)break;c[g+(h<<2)>>2]=0;h=h+1|0}q=j+-12|0;do if(a[q>>0]&1){p=j+-4|0;a[c[p>>2]>>0]=0;m=j+-8|0;c[m>>2]=0;g=a[q>>0]|0;if(!(g&1))l=10;else{l=c[q>>2]|0;g=l&255;l=(l&-2)+-1|0}if(!(g&1)){h=(g&255)>>>1;if((g&255)<22){o=1;k=10;n=h}else{o=1;k=(h+16&240)+-1|0;n=h}}else{o=0;k=10;n=0}if((k|0)!=(l|0)){if((k|0)==10){j=q+1|0;h=c[p>>2]|0;if(o){Fc(j|0,h|0,((g&255)>>>1)+1|0)|0;wc(h)}else{a[j>>0]=a[h>>0]|0;wc(h)}a[q>>0]=n<<1;break}h=k+1|0;j=vc(h)|0;if(!(k>>>0<=l>>>0&(j|0)==0)){if(o)Fc(j|0,q+1|0,((g&255)>>>1)+1|0)|0;else{e=c[p>>2]|0;a[j>>0]=a[e>>0]|0;wc(e)}c[q>>2]=h|1;c[m>>2]=n;c[p>>2]=j}}}else{a[q+1>>0]=0;a[q>>0]=0}while(0);c[q>>2]=c[sa>>2];c[q+4>>2]=c[sa+4>>2];c[q+8>>2]=c[sa+8>>2];g=0;while(1){if((g|0)==3)break;c[sa+(g<<2)>>2]=0;g=g+1|0}Ja(sa)}else{g=Ya(ya,1882)|0;c[qa>>2]=c[g>>2];c[qa+4>>2]=c[g+4>>2];c[qa+8>>2]=c[g+8>>2];h=0;while(1){if((h|0)==3)break;c[g+(h<<2)>>2]=0;h=h+1|0}Cb(ra,xa);g=a[ra>>0]|0;h=(g&1)==0;g=Za(qa,h?ra+1|0:c[ra+8>>2]|0,h?(g&255)>>>1:c[ra+4>>2]|0)|0;c[oa>>2]=c[g>>2];c[oa+4>>2]=c[g+4>>2];c[oa+8>>2]=c[g+8>>2];h=0;while(1){if((h|0)==3)break;c[g+(h<<2)>>2]=0;h=h+1|0}g=Ya(oa,2050)|0;c[ia>>2]=c[g>>2];c[ia+4>>2]=c[g+4>>2];c[ia+8>>2]=c[g+8>>2];h=0;while(1){if((h|0)==3)break;c[g+(h<<2)>>2]=0;h=h+1|0}do if(a[q>>0]&1){p=j+-16|0;a[c[p>>2]>>0]=0;m=j+-20|0;c[m>>2]=0;g=a[q>>0]|0;if(!(g&1))l=10;else{g=c[q>>2]|0;l=(g&-2)+-1|0;g=g&255}if(!(g&1)){h=(g&255)>>>1;if((g&255)<22){n=h;o=1;k=10}else{n=h;o=1;k=(h+16&240)+-1|0}}else{n=0;o=0;k=10}if((k|0)!=(l|0)){if((k|0)==10){j=q+1|0;h=c[p>>2]|0;if(o){Fc(j|0,h|0,((g&255)>>>1)+1|0)|0;wc(h)}else{a[j>>0]=a[h>>0]|0;wc(h)}a[q>>0]=n<<1;break}h=k+1|0;j=vc(h)|0;if(!(k>>>0<=l>>>0&(j|0)==0)){if(o)Fc(j|0,q+1|0,((g&255)>>>1)+1|0)|0;else{e=c[p>>2]|0;a[j>>0]=a[e>>0]|0;wc(e)}c[q>>2]=h|1;c[m>>2]=n;c[p>>2]=j}}}else{a[q+1>>0]=0;a[q>>0]=0}while(0);c[q>>2]=c[ia>>2];c[q+4>>2]=c[ia+4>>2];c[q+8>>2]=c[ia+8>>2];g=0;while(1){if((g|0)==3)break;c[ia+(g<<2)>>2]=0;g=g+1|0}Ja(ia);Ja(oa);Ja(ra);Ja(qa);g=c[_>>2]|0;q=g+-12|0;do if(a[q>>0]&1){p=g+-4|0;a[c[p>>2]>>0]=0;m=g+-8|0;c[m>>2]=0;g=a[q>>0]|0;if(!(g&1))l=10;else{l=c[q>>2]|0;g=l&255;l=(l&-2)+-1|0}if(!(g&1)){h=(g&255)>>>1;if((g&255)<22){k=10;n=h;o=1}else{k=(h+16&240)+-1|0;n=h;o=1}}else{k=10;n=0;o=0}if((k|0)!=(l|0)){if((k|0)==10){j=q+1|0;h=c[p>>2]|0;if(o){Fc(j|0,h|0,((g&255)>>>1)+1|0)|0;wc(h)}else{a[j>>0]=a[h>>0]|0;wc(h)}a[q>>0]=n<<1;break}h=k+1|0;j=vc(h)|0;if(!(k>>>0<=l>>>0&(j|0)==0)){if(o)Fc(j|0,q+1|0,((g&255)>>>1)+1|0)|0;else{e=c[p>>2]|0;a[j>>0]=a[e>>0]|0;wc(e)}c[q>>2]=h|1;c[m>>2]=n;c[p>>2]=j}}}else{a[q+1>>0]=0;a[q>>0]=0}while(0);c[q>>2]=c[r>>2];c[q+4>>2]=c[r+4>>2];c[q+8>>2]=c[r+8>>2];g=0;while(1){if((g|0)==3)break h;c[r+(g<<2)>>2]=0;g=g+1|0}}while(0);Ia(xa);Ia(ya);g=ka}else g=d;if((g|0)==(d|0)){g=d;break a}h=c[f+4>>2]|0;if((c[f>>2]|0)==(h|0)){g=d;break a}m=f+16|0;c[ma>>2]=c[f+12>>2];Pb(na,h+-24|0,ma);h=f+20|0;j=c[h>>2]|0;d=c[f+24>>2]|0;k=d;if(j>>>0<d>>>0){c[j+12>>2]=c[na+12>>2];c[j>>2]=c[na>>2];ya=na+4|0;c[j+4>>2]=c[ya>>2];f=na+8|0;c[j+8>>2]=c[f>>2];c[f>>2]=0;c[ya>>2]=0;c[na>>2]=0;c[h>>2]=(c[h>>2]|0)+16}else{h=c[m>>2]|0;d=j-h|0;l=d>>4;j=l+1|0;if((d|0)<-16)Pa();h=k-h|0;if(h>>4>>>0<1073741823){h=h>>3;h=h>>>0<j>>>0?j:h}else h=2147483647;Qa(ya,h,l,f+28|0);f=ya+8|0;d=c[f>>2]|0;c[d+12>>2]=c[na+12>>2];c[d>>2]=c[na>>2];e=na+4|0;c[d+4>>2]=c[e>>2];xa=na+8|0;c[d+8>>2]=c[xa>>2];c[xa>>2]=0;c[e>>2]=0;c[na>>2]=0;c[f>>2]=d+16;Ra(m,ya);Sa(ya)}Ha(na);break a}case 79:{v=f+4|0;p=((c[v>>2]|0)-(c[f>>2]|0)|0)/24|0;xa=d+1|0;g=Na(xa,e,f)|0;v=((c[v>>2]|0)-(c[f>>2]|0)|0)/24|0;if((g|0)==(xa|0)){g=d;break a}n=f+16|0;o=c[f+12>>2]|0;w=f+20|0;h=c[w>>2]|0;xa=c[f+24>>2]|0;j=xa;if(h>>>0<xa>>>0){c[h>>2]=0;c[h+4>>2]=0;c[h+8>>2]=0;c[h+12>>2]=o;c[w>>2]=(c[w>>2]|0)+16}else{k=c[n>>2]|0;xa=h-k|0;m=xa>>4;l=m+1|0;if((xa|0)<-16)Pa();h=j-k|0;if(h>>4>>>0<1073741823){h=h>>3;h=h>>>0<l>>>0?l:h}else h=2147483647;Qa(ya,h,m,f+28|0);xa=ya+8|0;ta=c[xa>>2]|0;c[ta>>2]=0;c[ta+4>>2]=0;c[ta+8>>2]=0;c[ta+12>>2]=o;c[xa>>2]=ta+16;Ra(n,ya);Sa(ya)}r=T+4|0;s=T+8|0;t=T+1|0;u=ya+8|0;while(1){if(p>>>0>=v>>>0)break a;xa=c[f>>2]|0;ta=xa+(p*24|0)+12|0;sa=a[ta>>0]|0;j=(sa&1)==0;sa=j?(sa&255)>>>1:c[xa+(p*24|0)+16>>2]|0;$a(T,j?ta+1|0:c[xa+(p*24|0)+20>>2]|0,sa>>>0<2?sa:2);sa=a[T>>0]|0;xa=(sa&1)==0;sa=xa?(sa&255)>>>1:c[r>>2]|0;ta=sa>>>0>2;xa=ac(xa?t:c[s>>2]|0,790,ta?2:sa)|0;Ja(T);j=c[f>>2]|0;if(((xa|0)==0?(sa>>>0<2?-1:ta&1):xa)|0){h=b[j+(p*24|0)+12>>1]|0;if(!(h&1))h=(h&65535)>>>8&255;else h=a[c[j+(p*24|0)+20>>2]>>0]|0;if(h<<24>>24==40){Ya(j+(p*24|0)|0,797)|0;Ta((c[f>>2]|0)+(p*24|0)+12|0,0,799)|0}}else{Ya(j+(p*24|0)|0,849)|0;Ta((c[f>>2]|0)+(p*24|0)+12|0,0,799)|0}Ya((c[f>>2]|0)+(p*24|0)|0,841)|0;m=c[w>>2]|0;n=m+-16|0;o=c[f>>2]|0;q=o+(p*24|0)|0;h=m+-12|0;j=c[h>>2]|0;xa=c[m+-8>>2]|0;k=xa;if((j|0)==(xa|0)){h=c[n>>2]|0;xa=j-h|0;l=(xa|0)/24|0;j=l+1|0;if((xa|0)<-24)break;h=(k-h|0)/24|0;if(h>>>0<1073741823){h=h<<1;h=h>>>0<j>>>0?j:h}else h=2147483647;ab(ya,h,l,m+-4|0);xa=c[u>>2]|0;_a(xa,q);_a(xa+12|0,o+(p*24|0)+12|0);c[u>>2]=xa+24;cb(n,ya);bb(ya)}else{_a(j,q);_a(j+12|0,o+(p*24|0)+12|0);c[h>>2]=(c[h>>2]|0)+24}p=p+1|0}Pa();break}case 80:{B=f+4|0;p=((c[B>>2]|0)-(c[f>>2]|0)|0)/24|0;A=d+1|0;g=Na(A,e,f)|0;B=((c[B>>2]|0)-(c[f>>2]|0)|0)/24|0;if((g|0)==(A|0)){g=d;break a}n=f+16|0;o=c[f+12>>2]|0;C=f+20|0;h=c[C>>2]|0;xa=c[f+24>>2]|0;j=xa;if(h>>>0<xa>>>0){c[h>>2]=0;c[h+4>>2]=0;c[h+8>>2]=0;c[h+12>>2]=o;c[C>>2]=(c[C>>2]|0)+16}else{k=c[n>>2]|0;xa=h-k|0;m=xa>>4;l=m+1|0;if((xa|0)<-16)Pa();h=j-k|0;if(h>>4>>>0<1073741823){h=h>>3;h=h>>>0<l>>>0?l:h}else h=2147483647;Qa(ya,h,m,f+28|0);xa=ya+8|0;sa=c[xa>>2]|0;c[sa>>2]=0;c[sa+4>>2]=0;c[sa+8>>2]=0;c[sa+12>>2]=o;c[xa>>2]=sa+16;Ra(n,ya);Sa(ya)}t=da+4|0;u=da+8|0;v=da+1|0;w=ea+4|0;x=ea+8|0;y=ea+1|0;z=ya+8|0;while(1){if(p>>>0>=B>>>0)break a;xa=c[f>>2]|0;sa=xa+(p*24|0)+12|0;ra=a[sa>>0]|0;j=(ra&1)==0;ra=j?(ra&255)>>>1:c[xa+(p*24|0)+16>>2]|0;$a(da,j?sa+1|0:c[xa+(p*24|0)+20>>2]|0,ra>>>0<2?ra:2);ra=a[da>>0]|0;xa=(ra&1)==0;ra=xa?(ra&255)>>>1:c[t>>2]|0;sa=ra>>>0>2;xa=ac(xa?v:c[u>>2]|0,790,sa?2:ra)|0;Ja(da);j=c[f>>2]|0;if(((xa|0)==0?(ra>>>0<2?-1:sa&1):xa)|0){h=b[j+(p*24|0)+12>>1]|0;if(!(h&1))h=(h&65535)>>>8&255;else h=a[c[j+(p*24|0)+20>>2]>>0]|0;if(h<<24>>24==40){Ya(j+(p*24|0)|0,797)|0;Ta((c[f>>2]|0)+(p*24|0)+12|0,0,799)|0}}else{Ya(j+(p*24|0)|0,849)|0;Ta((c[f>>2]|0)+(p*24|0)+12|0,0,799)|0}j=c[f>>2]|0;h=j+(p*24|0)|0;do if((a[A>>0]|0)==85){ra=a[h>>0]|0;xa=(ra&1)==0;ra=xa?(ra&255)>>>1:c[j+(p*24|0)+4>>2]|0;$a(ea,xa?h+1|0:c[j+(p*24|0)+8>>2]|0,ra>>>0<12?ra:12);ra=a[ea>>0]|0;xa=(ra&1)==0;ra=xa?(ra&255)>>>1:c[w>>2]|0;sa=ra>>>0>12;xa=ac(xa?y:c[x>>2]|0,2054,sa?12:ra)|0;Ja(ea);s=c[f>>2]|0;h=s+(p*24|0)|0;if(!(((xa|0)==0?(ra>>>0<12?-1:sa&1):xa)|0)){j=a[h>>0]|0;if(!(j&1)){o=(j&255)>>>1;r=o;o=o>>>0<11?o:11;k=10}else{o=c[s+(p*24|0)+4>>2]|0;j=c[h>>2]|0;r=o;o=o>>>0<11?o:11;k=(j&-2)+-1|0;j=j&255}if((o-r+k|0)>>>0<2){Wa(h,k,2-o+r-k|0,r,0,o,2,2067);break}if(!(j&1))q=h+1|0;else q=c[s+(p*24|0)+8>>2]|0;do if((o|0)!=2){n=r-o|0;if((r|0)==(o|0)){k=o;m=0;l=2067;j=2;ta=402}else{if(o>>>0>2){a[q>>0]=105;a[q+1>>0]=100;Hc(q+2|0,q+o|0,n|0)|0;k=o;j=2;break}do if(q>>>0<2067>>>0&(q+r|0)>>>0>2067>>>0)if((q+o|0)>>>0>2067>>>0){Fc(q|0,2067,o|0)|0;m=o;l=2069;k=0;j=2-o|0;break}else{m=0;l=2067+(2-o)|0;k=o;j=2;break}else{m=0;l=2067;k=o;j=2}while(0);ta=q+m|0;Hc(ta+j|0,ta+k|0,n|0)|0;ta=402}}else{k=2;m=0;l=2067;j=2;ta=402}while(0);if((ta|0)==402){ta=0;Hc(q+m|0,l|0,j|0)|0}j=j-k+r|0;if(!(a[h>>0]&1))a[h>>0]=j<<1;else c[s+(p*24|0)+4>>2]=j;a[q+j>>0]=0}else ta=385}else ta=385;while(0);if((ta|0)==385){ta=0;Ya(h,4262)|0}m=c[C>>2]|0;n=m+-16|0;o=c[f>>2]|0;q=o+(p*24|0)|0;h=m+-12|0;j=c[h>>2]|0;xa=c[m+-8>>2]|0;k=xa;if((j|0)==(xa|0)){h=c[n>>2]|0;xa=j-h|0;l=(xa|0)/24|0;j=l+1|0;if((xa|0)<-24)break;h=(k-h|0)/24|0;if(h>>>0<1073741823){h=h<<1;h=h>>>0<j>>>0?j:h}else h=2147483647;ab(ya,h,l,m+-4|0);xa=c[z>>2]|0;_a(xa,q);_a(xa+12|0,o+(p*24|0)+12|0);c[z>>2]=xa+24;cb(n,ya);bb(ya)}else{_a(j,q);_a(j+12|0,o+(p*24|0)+12|0);c[h>>2]=(c[h>>2]|0)+24}p=p+1|0}Pa();break}case 82:{v=f+4|0;p=((c[v>>2]|0)-(c[f>>2]|0)|0)/24|0;xa=d+1|0;g=Na(xa,e,f)|0;v=((c[v>>2]|0)-(c[f>>2]|0)|0)/24|0;if((g|0)==(xa|0)){g=d;break a}n=f+16|0;o=c[f+12>>2]|0;w=f+20|0;h=c[w>>2]|0;xa=c[f+24>>2]|0;j=xa;if(h>>>0<xa>>>0){c[h>>2]=0;c[h+4>>2]=0;c[h+8>>2]=0;c[h+12>>2]=o;c[w>>2]=(c[w>>2]|0)+16}else{k=c[n>>2]|0;xa=h-k|0;m=xa>>4;l=m+1|0;if((xa|0)<-16)Pa();h=j-k|0;if(h>>4>>>0<1073741823){h=h>>3;h=h>>>0<l>>>0?l:h}else h=2147483647;Qa(ya,h,m,f+28|0);xa=ya+8|0;ta=c[xa>>2]|0;c[ta>>2]=0;c[ta+4>>2]=0;c[ta+8>>2]=0;c[ta+12>>2]=o;c[xa>>2]=ta+16;Ra(n,ya);Sa(ya)}r=U+4|0;s=U+8|0;t=U+1|0;u=ya+8|0;while(1){if(p>>>0>=v>>>0)break a;xa=c[f>>2]|0;ta=xa+(p*24|0)+12|0;sa=a[ta>>0]|0;j=(sa&1)==0;sa=j?(sa&255)>>>1:c[xa+(p*24|0)+16>>2]|0;$a(U,j?ta+1|0:c[xa+(p*24|0)+20>>2]|0,sa>>>0<2?sa:2);sa=a[U>>0]|0;xa=(sa&1)==0;sa=xa?(sa&255)>>>1:c[r>>2]|0;ta=sa>>>0>2;xa=ac(xa?t:c[s>>2]|0,790,ta?2:sa)|0;Ja(U);j=c[f>>2]|0;if(((xa|0)==0?(sa>>>0<2?-1:ta&1):xa)|0){h=b[j+(p*24|0)+12>>1]|0;if(!(h&1))h=(h&65535)>>>8&255;else h=a[c[j+(p*24|0)+20>>2]>>0]|0;if(h<<24>>24==40){Ya(j+(p*24|0)|0,797)|0;Ta((c[f>>2]|0)+(p*24|0)+12|0,0,799)|0}}else{Ya(j+(p*24|0)|0,849)|0;Ta((c[f>>2]|0)+(p*24|0)+12|0,0,799)|0}Ya((c[f>>2]|0)+(p*24|0)|0,852)|0;m=c[w>>2]|0;n=m+-16|0;o=c[f>>2]|0;q=o+(p*24|0)|0;h=m+-12|0;j=c[h>>2]|0;xa=c[m+-8>>2]|0;k=xa;if((j|0)==(xa|0)){h=c[n>>2]|0;xa=j-h|0;l=(xa|0)/24|0;j=l+1|0;if((xa|0)<-24)break;h=(k-h|0)/24|0;if(h>>>0<1073741823){h=h<<1;h=h>>>0<j>>>0?j:h}else h=2147483647;ab(ya,h,l,m+-4|0);xa=c[u>>2]|0;_a(xa,q);_a(xa+12|0,o+(p*24|0)+12|0);c[u>>2]=xa+24;cb(n,ya);bb(ya)}else{_a(j,q);_a(j+12|0,o+(p*24|0)+12|0);c[h>>2]=(c[h>>2]|0)+24}p=p+1|0}Pa();break}case 84:{v=f+4|0;s=((c[v>>2]|0)-(c[f>>2]|0)|0)/24|0;g=Eb(d,e,f)|0;t=((c[v>>2]|0)-(c[f>>2]|0)|0)/24|0;if((g|0)==(d|0)){g=d;break a}y=f+16|0;u=f+12|0;n=c[u>>2]|0;x=f+20|0;h=c[x>>2]|0;w=f+24|0;d=c[w>>2]|0;j=d;if(h>>>0<d>>>0){c[h>>2]=0;c[h+4>>2]=0;c[h+8>>2]=0;c[h+12>>2]=n;c[x>>2]=(c[x>>2]|0)+16}else{k=c[y>>2]|0;d=h-k|0;m=d>>4;l=m+1|0;if((d|0)<-16)Pa();h=j-k|0;if(h>>4>>>0<1073741823){h=h>>3;h=h>>>0<l>>>0?l:h}else h=2147483647;Qa(ya,h,m,f+28|0);d=ya+8|0;wa=c[d>>2]|0;c[wa>>2]=0;c[wa+4>>2]=0;c[wa+8>>2]=0;c[wa+12>>2]=n;c[d>>2]=wa+16;Ra(y,ya);Sa(ya)}m=ya+8|0;r=s;while(1){if(r>>>0>=t>>>0)break;n=c[x>>2]|0;o=n+-16|0;p=c[f>>2]|0;q=p+(r*24|0)|0;h=n+-12|0;j=c[h>>2]|0;d=c[n+-8>>2]|0;k=d;if((j|0)==(d|0)){h=c[o>>2]|0;d=j-h|0;l=(d|0)/24|0;j=l+1|0;if((d|0)<-24){ta=455;break}h=(k-h|0)/24|0;if(h>>>0<1073741823){h=h<<1;h=h>>>0<j>>>0?j:h}else h=2147483647;ab(ya,h,l,n+-4|0);d=c[m>>2]|0;_a(d,q);_a(d+12|0,p+(r*24|0)+12|0);c[m>>2]=d+24;cb(o,ya);bb(ya)}else{_a(j,q);_a(j+12|0,p+(r*24|0)+12|0);c[h>>2]=(c[h>>2]|0)+24}r=r+1|0}if((ta|0)==455)Pa();if(!((t|0)==(s+1|0)&(a[f+63>>0]|0)!=0))break a;m=Mb(g,e,f)|0;if((m|0)==(g|0))break a;Cb(xa,(c[v>>2]|0)+-24|0);g=c[v>>2]|0;h=g+-24|0;j=g;while(1){if((j|0)==(h|0))break;d=j+-24|0;c[v>>2]=d;Ia(d);j=c[v>>2]|0}d=a[xa>>0]|0;k=(d&1)==0;Za(g+-48|0,k?xa+1|0:c[xa+8>>2]|0,k?(d&255)>>>1:c[xa+4>>2]|0)|0;g=(c[v>>2]|0)+-24|0;c[V>>2]=c[u>>2];Pb(X,g,V);g=c[x>>2]|0;d=c[w>>2]|0;k=d;if(g>>>0<d>>>0){c[g+12>>2]=c[X+12>>2];c[g>>2]=c[X>>2];ya=X+4|0;c[g+4>>2]=c[ya>>2];f=X+8|0;c[g+8>>2]=c[f>>2];c[f>>2]=0;c[ya>>2]=0;c[X>>2]=0;c[x>>2]=(c[x>>2]|0)+16}else{h=c[y>>2]|0;d=g-h|0;l=d>>4;j=l+1|0;if((d|0)<-16)Pa();g=k-h|0;if(g>>4>>>0<1073741823){g=g>>3;g=g>>>0<j>>>0?j:g}else g=2147483647;Qa(ya,g,l,f+28|0);f=ya+8|0;d=c[f>>2]|0;c[d+12>>2]=c[X+12>>2];c[d>>2]=c[X>>2];e=X+4|0;c[d+4>>2]=c[e>>2];wa=X+8|0;c[d+8>>2]=c[wa>>2];c[wa>>2]=0;c[e>>2]=0;c[X>>2]=0;c[f>>2]=d+16;Ra(y,ya);Sa(ya)}Ha(X);Ja(xa);g=m;break a}case 85:{g=d+1|0;if((g|0)==(e|0)){g=d;break a}h=qb(g,e,f)|0;if((h|0)==(g|0)){g=d;break a}g=Na(h,e,f)|0;if((g|0)==(h|0)){g=d;break a}n=f+4|0;h=c[n>>2]|0;if(((h-(c[f>>2]|0)|0)/24|0)>>>0<2){g=d;break a}Cb(xa,h+-24|0);h=c[n>>2]|0;j=h+-24|0;k=h;while(1){if((k|0)==(j|0))break;d=k+-24|0;c[n>>2]=d;Ia(d);k=c[n>>2]|0}d=h+-48|0;wa=a[d>>0]|0;e=(wa&1)==0;wa=e?(wa&255)>>>1:c[h+-44>>2]|0;$a(t,e?d+1|0:c[h+-40>>2]|0,wa>>>0<9?wa:9);wa=a[t>>0]|0;d=(wa&1)==0;wa=d?(wa&255)>>>1:c[t+4>>2]|0;e=wa>>>0>9;d=ac(d?t+1|0:c[t+8>>2]|0,2070,e?9:wa)|0;Ja(t);if(!(((d|0)==0?(wa>>>0<9?-1:e&1):d)|0)){Cb(la,(c[n>>2]|0)+-24|0);j=c[n>>2]|0;h=j+-24|0;while(1){if((j|0)==(h|0))break;d=j+-24|0;c[n>>2]=d;Ia(d);j=c[n>>2]|0}d=a[la>>0]|0;e=(d&1)==0;h=la+8|0;j=la+1|0;wa=e?j:c[h>>2]|0;k=la+4|0;d=qb(wa+9|0,wa+(e?(d&255)>>>1:c[k>>2]|0)|0,f)|0;if((d|0)==(((a[la>>0]&1)==0?j:c[h>>2]|0)+9|0)){Ib(N,xa,1882);d=a[la>>0]|0;e=(d&1)==0;h=Za(N,e?j:c[h>>2]|0,e?(d&255)>>>1:c[k>>2]|0)|0;c[M>>2]=c[h>>2];c[M+4>>2]=c[h+4>>2];c[M+8>>2]=c[h+8>>2];j=0;while(1){if((j|0)==3)break;c[h+(j<<2)>>2]=0;j=j+1|0}rb(L,M);h=c[n>>2]|0;d=c[f+8>>2]|0;j=d;if(h>>>0<d>>>0){db(h,L);c[n>>2]=(c[n>>2]|0)+24}else{k=c[f>>2]|0;d=h-k|0;m=(d|0)/24|0;l=m+1|0;if((d|0)<-24)Pa();h=(j-k|0)/24|0;if(h>>>0<1073741823){h=h<<1;h=h>>>0<l>>>0?l:h}else h=2147483647;ab(ya,h,m,f+12|0);d=ya+8|0;e=c[d>>2]|0;db(e,L);c[d>>2]=e+24;cb(f,ya);bb(ya)}Ia(L);Ja(M);Ja(N)}else{k=(c[n>>2]|0)+-24|0;Ib(H,xa,1427);Cb(I,(c[n>>2]|0)+-24|0);h=a[I>>0]|0;j=(h&1)==0;h=Za(H,j?I+1|0:c[I+8>>2]|0,j?(h&255)>>>1:c[I+4>>2]|0)|0;c[G>>2]=c[h>>2];c[G+4>>2]=c[h+4>>2];c[G+8>>2]=c[h+8>>2];j=0;while(1){if((j|0)==3)break;c[h+(j<<2)>>2]=0;j=j+1|0}h=Ya(G,844)|0;c[F>>2]=c[h>>2];c[F+4>>2]=c[h+4>>2];c[F+8>>2]=c[h+8>>2];j=0;while(1){if((j|0)==3)break;c[h+(j<<2)>>2]=0;j=j+1|0}rb(E,F);Db(k,E);Ia(E);Ja(F);Ja(G);Ja(I);Ja(H)}Ja(la)}else{h=(c[n>>2]|0)+-24|0;Ib(x,xa,1882);Cb(y,(c[n>>2]|0)+-24|0);j=a[y>>0]|0;k=(j&1)==0;j=Za(x,k?y+1|0:c[y+8>>2]|0,k?(j&255)>>>1:c[y+4>>2]|0)|0;c[w>>2]=c[j>>2];c[w+4>>2]=c[j+4>>2];c[w+8>>2]=c[j+8>>2];k=0;while(1){if((k|0)==3)break;c[j+(k<<2)>>2]=0;k=k+1|0}rb(v,w);Db(h,v);Ia(v);Ja(w);Ja(y);Ja(x)}m=(c[n>>2]|0)+-24|0;c[R>>2]=c[f+12>>2];Pb(W,m,R);m=f+16|0;h=f+20|0;j=c[h>>2]|0;d=c[f+24>>2]|0;k=d;if(j>>>0<d>>>0){c[j+12>>2]=c[W+12>>2];c[j>>2]=c[W>>2];ya=W+4|0;c[j+4>>2]=c[ya>>2];f=W+8|0;c[j+8>>2]=c[f>>2];c[f>>2]=0;c[ya>>2]=0;c[W>>2]=0;c[h>>2]=(c[h>>2]|0)+16}else{h=c[m>>2]|0;d=j-h|0;l=d>>4;j=l+1|0;if((d|0)<-16)Pa();h=k-h|0;if(h>>4>>>0<1073741823){h=h>>3;h=h>>>0<j>>>0?j:h}else h=2147483647;Qa(ya,h,l,f+28|0);f=ya+8|0;d=c[f>>2]|0;c[d+12>>2]=c[W+12>>2];c[d>>2]=c[W>>2];e=W+4|0;c[d+4>>2]=c[e>>2];wa=W+8|0;c[d+8>>2]=c[wa>>2];c[wa>>2]=0;c[e>>2]=0;c[W>>2]=0;c[f>>2]=d+16;Ra(m,ya);Sa(ya)}Ha(W);Ja(xa);break a}case 83:{wa=d+1|0;if((wa|0)!=(e|0)?(a[wa>>0]|0)==116:0){g=Wb(d,e,f)|0;if((g|0)==(d|0)){g=d;break a}h=c[f+4>>2]|0;if((c[f>>2]|0)==(h|0)){g=d;break a}m=f+16|0;c[n>>2]=c[f+12>>2];Pb(z,h+-24|0,n);h=f+20|0;j=c[h>>2]|0;d=c[f+24>>2]|0;k=d;if(j>>>0<d>>>0){c[j+12>>2]=c[z+12>>2];c[j>>2]=c[z>>2];ya=z+4|0;c[j+4>>2]=c[ya>>2];f=z+8|0;c[j+8>>2]=c[f>>2];c[f>>2]=0;c[ya>>2]=0;c[z>>2]=0;c[h>>2]=(c[h>>2]|0)+16}else{h=c[m>>2]|0;d=j-h|0;l=d>>4;j=l+1|0;if((d|0)<-16)Pa();h=k-h|0;if(h>>4>>>0<1073741823){h=h>>3;h=h>>>0<j>>>0?j:h}else h=2147483647;Qa(ya,h,l,f+28|0);f=ya+8|0;d=c[f>>2]|0;c[d+12>>2]=c[z+12>>2];c[d>>2]=c[z>>2];e=z+4|0;c[d+4>>2]=c[e>>2];xa=z+8|0;c[d+8>>2]=c[xa>>2];c[xa>>2]=0;c[e>>2]=0;c[z>>2]=0;c[f>>2]=d+16;Ra(m,ya);Sa(ya)}Ha(z);break a}g=Rb(d,e,f)|0;if((g|0)==(d|0)){g=d;break a}m=Mb(g,e,f)|0;if((m|0)==(g|0))break a;k=f+4|0;h=c[k>>2]|0;if(((h-(c[f>>2]|0)|0)/24|0)>>>0<2)break a;Cb(xa,h+-24|0);g=c[k>>2]|0;h=g+-24|0;j=g;while(1){if((j|0)==(h|0))break;d=j+-24|0;c[k>>2]=d;Ia(d);j=c[k>>2]|0}l=a[xa>>0]|0;h=(l&1)==0;Za(g+-48|0,h?xa+1|0:c[xa+8>>2]|0,h?(l&255)>>>1:c[xa+4>>2]|0)|0;l=(c[k>>2]|0)+-24|0;c[s>>2]=c[f+12>>2];Pb(D,l,s);l=f+16|0;g=f+20|0;h=c[g>>2]|0;d=c[f+24>>2]|0;j=d;if(h>>>0<d>>>0){c[h+12>>2]=c[D+12>>2];c[h>>2]=c[D>>2];ya=D+4|0;c[h+4>>2]=c[ya>>2];f=D+8|0;c[h+8>>2]=c[f>>2];c[f>>2]=0;c[ya>>2]=0;c[D>>2]=0;c[g>>2]=(c[g>>2]|0)+16}else{g=c[l>>2]|0;d=h-g|0;k=d>>4;h=k+1|0;if((d|0)<-16)Pa();g=j-g|0;if(g>>4>>>0<1073741823){g=g>>3;g=g>>>0<h>>>0?h:g}else g=2147483647;Qa(ya,g,k,f+28|0);f=ya+8|0;d=c[f>>2]|0;c[d+12>>2]=c[D+12>>2];c[d>>2]=c[D>>2];e=D+4|0;c[d+4>>2]=c[e>>2];wa=D+8|0;c[d+8>>2]=c[wa>>2];c[wa>>2]=0;c[e>>2]=0;c[D>>2]=0;c[f>>2]=d+16;Ra(l,ya);Sa(ya)}Ha(D);Ja(xa);g=m;break a}case 68:{g=d+1|0;if((g|0)!=(e|0)){g=a[g>>0]|0;switch(g<<24>>24|0){case 112:{s=f+4|0;p=((c[s>>2]|0)-(c[f>>2]|0)|0)/24|0;xa=d+2|0;g=Na(xa,e,f)|0;s=((c[s>>2]|0)-(c[f>>2]|0)|0)/24|0;if((g|0)==(xa|0))break d;n=f+16|0;o=c[f+12>>2]|0;t=f+20|0;h=c[t>>2]|0;xa=c[f+24>>2]|0;j=xa;if(h>>>0<xa>>>0){c[h>>2]=0;c[h+4>>2]=0;c[h+8>>2]=0;c[h+12>>2]=o;c[t>>2]=(c[t>>2]|0)+16}else{k=c[n>>2]|0;xa=h-k|0;m=xa>>4;l=m+1|0;if((xa|0)<-16)Pa();h=j-k|0;if(h>>4>>>0<1073741823){h=h>>3;h=h>>>0<l>>>0?l:h}else h=2147483647;Qa(ya,h,m,f+28|0);xa=ya+8|0;ta=c[xa>>2]|0;c[ta>>2]=0;c[ta+4>>2]=0;c[ta+8>>2]=0;c[ta+12>>2]=o;c[xa>>2]=ta+16;Ra(n,ya);Sa(ya)}r=ya+8|0;while(1){if(p>>>0>=s>>>0)break a;m=c[t>>2]|0;n=m+-16|0;o=c[f>>2]|0;q=o+(p*24|0)|0;h=m+-12|0;j=c[h>>2]|0;xa=c[m+-8>>2]|0;k=xa;if((j|0)==(xa|0)){h=c[n>>2]|0;xa=j-h|0;l=(xa|0)/24|0;j=l+1|0;if((xa|0)<-24)break;h=(k-h|0)/24|0;if(h>>>0<1073741823){h=h<<1;h=h>>>0<j>>>0?j:h}else h=2147483647;ab(ya,h,l,m+-4|0);xa=c[r>>2]|0;_a(xa,q);_a(xa+12|0,o+(p*24|0)+12|0);c[r>>2]=xa+24;cb(n,ya);bb(ya)}else{_a(j,q);_a(j+12|0,o+(p*24|0)+12|0);c[h>>2]=(c[h>>2]|0)+24}p=p+1|0}Pa();break}case 84:case 116:{g=Qb(d,e,f)|0;if((g|0)==(d|0))break d;h=c[f+4>>2]|0;if((c[f>>2]|0)==(h|0)){g=d;break a}m=f+16|0;c[r>>2]=c[f+12>>2];Pb(C,h+-24|0,r);h=f+20|0;j=c[h>>2]|0;d=c[f+24>>2]|0;k=d;if(j>>>0<d>>>0){c[j+12>>2]=c[C+12>>2];c[j>>2]=c[C>>2];ya=C+4|0;c[j+4>>2]=c[ya>>2];f=C+8|0;c[j+8>>2]=c[f>>2];c[f>>2]=0;c[ya>>2]=0;c[C>>2]=0;c[h>>2]=(c[h>>2]|0)+16}else{h=c[m>>2]|0;d=j-h|0;l=d>>4;j=l+1|0;if((d|0)<-16)Pa();h=k-h|0;if(h>>4>>>0<1073741823){h=h>>3;h=h>>>0<j>>>0?j:h}else h=2147483647;Qa(ya,h,l,f+28|0);f=ya+8|0;d=c[f>>2]|0;c[d+12>>2]=c[C+12>>2];c[d>>2]=c[C>>2];e=C+4|0;c[d+4>>2]=c[e>>2];xa=C+8|0;c[d+8>>2]=c[xa>>2];c[xa>>2]=0;c[e>>2]=0;c[C>>2]=0;c[f>>2]=d+16;Ra(m,ya);Sa(ya)}Ha(C);break a}case 118:{i:do if((e-d|0)>3&h<<24>>24==68&g<<24>>24==118){l=d+2|0;h=a[l>>0]|0;if((h+-49&255)<9){g=tb(l,e)|0;if((g|0)==(e|0)){g=d;break}if((a[g>>0]|0)!=95){g=d;break}j=g-l|0;h=g+1|0;if((h|0)==(e|0)){g=d;break}if((a[h>>0]|0)!=112){g=Na(h,e,f)|0;if((g|0)==(h|0)){g=d;break}h=c[f+4>>2]|0;if((c[f>>2]|0)==(h|0)){g=d;break}k=h+-24|0;$a(ca,l,j);h=Ta(ca,0,2101)|0;c[la>>2]=c[h>>2];c[la+4>>2]=c[h+4>>2];c[la+8>>2]=c[h+8>>2];j=0;while(1){if((j|0)==3)break;c[h+(j<<2)>>2]=0;j=j+1|0}h=Ya(la,4264)|0;c[xa>>2]=c[h>>2];c[xa+4>>2]=c[h+4>>2];c[xa+8>>2]=c[h+8>>2];j=0;while(1){if((j|0)==3)break;c[h+(j<<2)>>2]=0;j=j+1|0}ta=a[xa>>0]|0;sa=(ta&1)==0;Za(k,sa?xa+1|0:c[xa+8>>2]|0,sa?(ta&255)>>>1:c[xa+4>>2]|0)|0;Ja(xa);Ja(la);Ja(ca);break}g=g+2|0;$a(ia,l,j);h=Ta(ia,0,2110)|0;c[sa>>2]=c[h>>2];c[sa+4>>2]=c[h+4>>2];c[sa+8>>2]=c[h+8>>2];j=0;while(1){if((j|0)==3)break;c[h+(j<<2)>>2]=0;j=j+1|0}h=Ya(sa,4264)|0;c[pa>>2]=c[h>>2];c[pa+4>>2]=c[h+4>>2];c[pa+8>>2]=c[h+8>>2];j=0;while(1){if((j|0)==3)break;c[h+(j<<2)>>2]=0;j=j+1|0}rb(fa,pa);h=f+4|0;j=c[h>>2]|0;xa=c[f+8>>2]|0;k=xa;if(j>>>0<xa>>>0){db(j,fa);c[h>>2]=(c[h>>2]|0)+24}else{h=c[f>>2]|0;xa=j-h|0;l=(xa|0)/24|0;j=l+1|0;if((xa|0)<-24)Pa();h=(k-h|0)/24|0;if(h>>>0<1073741823){h=h<<1;h=h>>>0<j>>>0?j:h}else h=2147483647;ab(ya,h,l,f+12|0);xa=ya+8|0;ta=c[xa>>2]|0;db(ta,fa);c[xa>>2]=ta+24;cb(f,ya);bb(ya)}Ia(fa);Ja(pa);Ja(sa);Ja(ia);break}g=0;while(1){if((g|0)==3)break;c[ya+(g<<2)>>2]=0;g=g+1|0}j:do if(h<<24>>24!=95?(J=ub(l,e,f)|0,(J|0)!=(l|0)):0){o=f+4|0;g=c[o>>2]|0;if((c[f>>2]|0)!=(g|0)){Cb(oa,g+-24|0);k:do if(!(a[ya>>0]&1)){a[ya+1>>0]=0;a[ya>>0]=0}else{l=ya+8|0;h=c[l>>2]|0;a[h>>0]=0;m=ya+4|0;c[m>>2]=0;g=c[ya>>2]|0;n=(g&-2)+-1|0;j=g&255;do if(!(j&1)){g=g>>>1&127;if((j&255)<22){Fc(ya+1|0,h|0,g+1|0)|0;wc(h);break}h=g+16&240;k=h+-1|0;if((k|0)==(n|0))break k;j=vc(h)|0;if(k>>>0<=n>>>0&(j|0)==0)break k;Fc(j|0,ya+1|0,g+1|0)|0;c[ya>>2]=h|1;c[m>>2]=g;c[l>>2]=j;break k}else{a[ya+1>>0]=0;wc(h);g=0}while(0);a[ya>>0]=g<<1}while(0);c[ya>>2]=c[oa>>2];c[ya+4>>2]=c[oa+4>>2];c[ya+8>>2]=c[oa+8>>2];g=0;while(1){if((g|0)==3)break;c[oa+(g<<2)>>2]=0;g=g+1|0}Ja(oa);h=c[o>>2]|0;g=h+-24|0;while(1){if((h|0)==(g|0)){g=J;ta=622;break j}xa=h+-24|0;c[o>>2]=xa;Ia(xa);h=c[o>>2]|0}}}else{g=l;ta=622}while(0);do if((ta|0)==622){if((((g|0)!=(e|0)?(a[g>>0]|0)==95:0)?(K=g+1|0,(K|0)!=(e|0)):0)?(Q=Na(K,e,f)|0,(Q|0)!=(K|0)):0){g=c[f+4>>2]|0;if((c[f>>2]|0)==(g|0))break;g=g+-24|0;xb(ra,2101,ya);h=Ya(ra,4264)|0;c[qa>>2]=c[h>>2];c[qa+4>>2]=c[h+4>>2];c[qa+8>>2]=c[h+8>>2];j=0;while(1){if((j|0)==3)break;c[h+(j<<2)>>2]=0;j=j+1|0}xa=a[qa>>0]|0;ta=(xa&1)==0;Za(g,ta?qa+1|0:c[qa+8>>2]|0,ta?(xa&255)>>>1:c[qa+4>>2]|0)|0;Ja(qa);Ja(ra);g=Q}else g=d;Ja(ya);break i}while(0);Ja(ya);g=d}else g=d;while(0);if((g|0)==(d|0))break d;h=c[f+4>>2]|0;if((c[f>>2]|0)==(h|0)){g=d;break a}m=f+16|0;c[ba>>2]=c[f+12>>2];Pb(ga,h+-24|0,ba);h=f+20|0;j=c[h>>2]|0;d=c[f+24>>2]|0;k=d;if(j>>>0<d>>>0){c[j+12>>2]=c[ga+12>>2];c[j>>2]=c[ga>>2];ya=ga+4|0;c[j+4>>2]=c[ya>>2];f=ga+8|0;c[j+8>>2]=c[f>>2];c[f>>2]=0;c[ya>>2]=0;c[ga>>2]=0;c[h>>2]=(c[h>>2]|0)+16}else{h=c[m>>2]|0;d=j-h|0;l=d>>4;j=l+1|0;if((d|0)<-16)Pa();h=k-h|0;if(h>>4>>>0<1073741823){h=h>>3;h=h>>>0<j>>>0?j:h}else h=2147483647;Qa(ya,h,l,f+28|0);f=ya+8|0;d=c[f>>2]|0;c[d+12>>2]=c[ga+12>>2];c[d>>2]=c[ga>>2];e=ga+4|0;c[d+4>>2]=c[e>>2];xa=ga+8|0;c[d+8>>2]=c[xa>>2];c[xa>>2]=0;c[e>>2]=0;c[ga>>2]=0;c[f>>2]=d+16;Ra(m,ya);Sa(ya)}Ha(ga);break a}default:break d}}break}default:{}}while(0);g=eb(d,e,f)|0;if((g|0)==(d|0)){g=Wb(d,e,f)|0;if((g|0)!=(d|0)?(ua=c[f+4>>2]|0,(c[f>>2]|0)!=(ua|0)):0){m=f+16|0;c[va>>2]=c[f+12>>2];Pb(wa,ua+-24|0,va);h=f+20|0;j=c[h>>2]|0;d=c[f+24>>2]|0;k=d;if(j>>>0<d>>>0){c[j+12>>2]=c[wa+12>>2];c[j>>2]=c[wa>>2];ya=wa+4|0;c[j+4>>2]=c[ya>>2];f=wa+8|0;c[j+8>>2]=c[f>>2];c[f>>2]=0;c[ya>>2]=0;c[wa>>2]=0;c[h>>2]=(c[h>>2]|0)+16}else{h=c[m>>2]|0;d=j-h|0;l=d>>4;j=l+1|0;if((d|0)<-16)Pa();h=k-h|0;if(h>>4>>>0<1073741823){h=h>>3;h=h>>>0<j>>>0?j:h}else h=2147483647;Qa(ya,h,l,f+28|0);f=ya+8|0;d=c[f>>2]|0;c[d+12>>2]=c[wa+12>>2];c[d>>2]=c[wa>>2];e=wa+4|0;c[d+4>>2]=c[e>>2];xa=wa+8|0;c[d+8>>2]=c[xa>>2];c[xa>>2]=0;c[e>>2]=0;c[wa>>2]=0;c[f>>2]=d+16;Ra(m,ya);Sa(ya)}Ha(wa)}else g=d}}}else g=d;while(0);i=za;return g|0}function Oa(b,d,e){b=b|0;d=d|0;e=e|0;var f=0;c[e>>2]=0;if((b|0)!=(d|0)){d=a[b>>0]|0;if(d<<24>>24==114){c[e>>2]=4;d=b+1|0;b=d;d=a[d>>0]|0;f=4}else f=0;if(d<<24>>24==86){f=f|2;c[e>>2]=f;d=b+1|0;b=d;d=a[d>>0]|0}if(d<<24>>24==75){c[e>>2]=f|1;b=b+1|0}}return b|0}function Pa(){oa(120,143,303,246)}function Qa(a,b,d,e){a=a|0;b=b|0;d=d|0;e=e|0;c[a+12>>2]=0;c[a+16>>2]=e;if(!b)e=0;else e=Da(c[e>>2]|0,b<<4)|0;c[a>>2]=e;d=e+(d<<4)|0;c[a+8>>2]=d;c[a+4>>2]=d;c[a+12>>2]=e+(b<<4);return}function Ra(a,b){a=a|0;b=b|0;var d=0,e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0;e=c[a>>2]|0;f=a+4|0;g=b+4|0;d=c[f>>2]|0;while(1){if((d|0)==(e|0))break;k=c[g>>2]|0;i=k+-16|0;h=d+-16|0;c[i>>2]=0;j=k+-12|0;c[j>>2]=0;l=c[d+-4>>2]|0;c[k+-8>>2]=0;c[k+-4>>2]=l;c[i>>2]=c[h>>2];i=d+-12|0;c[j>>2]=c[i>>2];j=d+-8|0;c[k+-8>>2]=c[j>>2];c[j>>2]=0;c[i>>2]=0;c[h>>2]=0;c[g>>2]=(c[g>>2]|0)+-16;d=h}j=c[a>>2]|0;c[a>>2]=c[g>>2];c[g>>2]=j;j=b+8|0;l=c[f>>2]|0;c[f>>2]=c[j>>2];c[j>>2]=l;j=a+8|0;l=b+12|0;k=c[j>>2]|0;c[j>>2]=c[l>>2];c[l>>2]=k;c[b>>2]=c[g>>2];return}function Sa(a){a=a|0;var b=0,d=0,e=0;b=c[a+4>>2]|0;d=a+8|0;while(1){e=c[d>>2]|0;if((e|0)==(b|0))break;e=e+-16|0;c[d>>2]=e;Ha(e)}b=c[a>>2]|0;if(b)Ka(c[c[a+16>>2]>>2]|0,b,(c[a+12>>2]|0)-b|0);return}function Ta(a,b,c){a=a|0;b=b|0;c=c|0;return Ua(a,b,c,bc(c)|0)|0}function Ua(b,d,e,f){b=b|0;d=d|0;e=e|0;f=f|0;var g=0,h=0,i=0;g=a[b>>0]|0;h=(g&1)==0;if(h)i=(g&255)>>>1;else i=c[b+4>>2]|0;if(i>>>0<d>>>0)Va();if(h)h=10;else{g=c[b>>2]|0;h=(g&-2)+-1|0;g=g&255}if((h-i|0)>>>0>=f>>>0){if(f){if(!(g&1))h=b+1|0;else h=c[b+8>>2]|0;if((i|0)==(d|0))g=h+d|0;else{g=h+d|0;Hc(g+f|0,g|0,i-d|0)|0;e=g>>>0<=e>>>0&(h+i|0)>>>0>e>>>0?e+f|0:e}Hc(g|0,e|0,f|0)|0;g=i+f|0;if(!(a[b>>0]&1))a[b>>0]=g<<1;else c[b+4>>2]=g;a[h+g>>0]=0}}else Wa(b,h,i+f-h|0,i,d,0,f,e);return b|0}function Va(){oa(274,303,1175,406)}function Wa(b,d,e,f,g,h,i,j){b=b|0;d=d|0;e=e|0;f=f|0;g=g|0;h=h|0;i=i|0;j=j|0;var k=0,l=0,m=0;if((-18-d|0)>>>0<e>>>0)Xa();if(!(a[b>>0]&1))m=b+1|0;else m=c[b+8>>2]|0;if(d>>>0<2147483623){k=e+d|0;l=d<<1;k=k>>>0<l>>>0?l:k;k=k>>>0<11?11:k+16&-16}else k=-17;l=vc(k)|0;if(g)Fc(l|0,m|0,g|0)|0;if(i)Fc(l+g|0,j|0,i|0)|0;e=f-h|0;if((e|0)!=(g|0))Fc(l+g+i|0,m+g+h|0,e-g|0)|0;if((d|0)!=10)wc(m);c[b+8>>2]=l;c[b>>2]=k|1;d=e+i|0;c[b+4>>2]=d;a[l+d>>0]=0;return}function Xa(){oa(427,303,1164,246)}function Ya(a,b){a=a|0;b=b|0;return Za(a,b,bc(b)|0)|0}function Za(b,d,e){b=b|0;d=d|0;e=e|0;var f=0,g=0,h=0;f=a[b>>0]|0;if(!(f&1))h=10;else{f=c[b>>2]|0;h=(f&-2)+-1|0;f=f&255}g=(f&1)==0;if(g)f=(f&255)>>>1;else f=c[b+4>>2]|0;if((h-f|0)>>>0>=e>>>0){if(e){if(g)g=b+1|0;else g=c[b+8>>2]|0;Fc(g+f|0,d|0,e|0)|0;f=f+e|0;if(!(a[b>>0]&1))a[b>>0]=f<<1;else c[b+4>>2]=f;a[g+f>>0]=0}}else Wa(b,h,e-h+f|0,f,f,0,e,d);return b|0}function _a(b,d){b=b|0;d=d|0;if(!(a[d>>0]&1)){c[b>>2]=c[d>>2];c[b+4>>2]=c[d+4>>2];c[b+8>>2]=c[d+8>>2]}else $a(b,c[d+8>>2]|0,c[d+4>>2]|0);return}function $a(b,d,e){b=b|0;d=d|0;e=e|0;var f=0,g=0;if(e>>>0>4294967279)Xa();if(e>>>0<11){a[b>>0]=e<<1;b=b+1|0}else{g=e+16&-16;f=vc(g)|0;c[b+8>>2]=f;c[b>>2]=g|1;c[b+4>>2]=e;b=f}Fc(b|0,d|0,e|0)|0;a[b+e>>0]=0;return}function ab(a,b,d,e){a=a|0;b=b|0;d=d|0;e=e|0;c[a+12>>2]=0;c[a+16>>2]=e;if(!b)e=0;else e=Da(c[e>>2]|0,b*24|0)|0;c[a>>2]=e;d=e+(d*24|0)|0;c[a+8>>2]=d;c[a+4>>2]=d;c[a+12>>2]=e+(b*24|0);return}function bb(a){a=a|0;var b=0,d=0,e=0;b=c[a+4>>2]|0;d=a+8|0;while(1){e=c[d>>2]|0;if((e|0)==(b|0))break;e=e+-24|0;c[d>>2]=e;Ia(e)}b=c[a>>2]|0;if(b)Ka(c[c[a+16>>2]>>2]|0,b,(c[a+12>>2]|0)-b|0);return}function cb(a,b){a=a|0;b=b|0;var d=0,e=0,f=0,g=0,h=0;e=c[a>>2]|0;f=a+4|0;g=b+4|0;d=c[f>>2]|0;while(1){if((d|0)==(e|0))break;h=d+-24|0;db((c[g>>2]|0)+-24|0,h);c[g>>2]=(c[g>>2]|0)+-24;d=h}h=c[a>>2]|0;c[a>>2]=c[g>>2];c[g>>2]=h;h=b+8|0;e=c[f>>2]|0;c[f>>2]=c[h>>2];c[h>>2]=e;f=a+8|0;h=b+12|0;a=c[f>>2]|0;c[f>>2]=c[h>>2];c[h>>2]=a;c[b>>2]=c[g>>2];return}function db(a,b){a=a|0;b=b|0;var d=0;c[a>>2]=c[b>>2];c[a+4>>2]=c[b+4>>2];c[a+8>>2]=c[b+8>>2];d=0;while(1){if((d|0)==3)break;c[b+(d<<2)>>2]=0;d=d+1|0}d=a+12|0;b=b+12|0;c[d>>2]=c[b>>2];c[d+4>>2]=c[b+4>>2];c[d+8>>2]=c[b+8>>2];d=0;while(1){if((d|0)==3)break;c[b+(d<<2)>>2]=0;d=d+1|0}return}function eb(b,d,e){b=b|0;d=d|0;e=e|0;var f=0,g=0,h=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0,s=0,t=0,u=0,v=0,w=0,x=0,y=0,z=0,A=0,B=0,C=0,D=0,E=0,F=0,G=0,H=0,I=0,J=0,K=0,L=0,M=0,N=0;N=i;i=i+720|0;M=N+696|0;j=N+672|0;J=N+648|0;s=N+624|0;u=N+600|0;v=N+576|0;w=N+552|0;x=N+528|0;y=N+504|0;z=N+480|0;A=N+456|0;k=N+432|0;l=N+408|0;m=N+384|0;L=N+360|0;n=N+336|0;o=N+312|0;p=N+288|0;K=N+264|0;q=N+240|0;r=N+216|0;t=N+192|0;B=N+168|0;C=N+144|0;D=N+120|0;E=N+96|0;F=N+72|0;G=N+48|0;H=N+24|0;I=N;a:do if((b|0)!=(d|0))do switch(a[b>>0]|0){case 118:{fb(j,476);f=e+4|0;d=c[f>>2]|0;L=c[e+8>>2]|0;g=L;if(d>>>0<L>>>0){db(d,j);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;L=d-f|0;h=(L|0)/24|0;d=h+1|0;if((L|0)<-24)Pa();f=(g-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<d>>>0?d:f}else f=2147483647;ab(M,f,h,e+12|0);L=M+8|0;K=c[L>>2]|0;db(K,j);c[L>>2]=K+24;cb(e,M);bb(M)}Ia(j);b=b+1|0;break a}case 119:{a[J>>0]=14;f=J+1|0;a[f>>0]=a[481]|0;a[f+1>>0]=a[482]|0;a[f+2>>0]=a[483]|0;a[f+3>>0]=a[484]|0;a[f+4>>0]=a[485]|0;a[f+5>>0]=a[486]|0;a[f+6>>0]=a[487]|0;a[J+8>>0]=0;f=J+12|0;d=0;while(1){if((d|0)==3)break;c[f+(d<<2)>>2]=0;d=d+1|0}f=e+4|0;d=c[f>>2]|0;L=c[e+8>>2]|0;g=L;if(d>>>0<L>>>0){db(d,J);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;L=d-f|0;h=(L|0)/24|0;d=h+1|0;if((L|0)<-24)Pa();f=(g-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<d>>>0?d:f}else f=2147483647;ab(M,f,h,e+12|0);L=M+8|0;K=c[L>>2]|0;db(K,J);c[L>>2]=K+24;cb(e,M);bb(M)}Ia(J);b=b+1|0;break a}case 98:{fb(s,489);f=e+4|0;d=c[f>>2]|0;L=c[e+8>>2]|0;g=L;if(d>>>0<L>>>0){db(d,s);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;L=d-f|0;h=(L|0)/24|0;d=h+1|0;if((L|0)<-24)Pa();f=(g-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<d>>>0?d:f}else f=2147483647;ab(M,f,h,e+12|0);L=M+8|0;K=c[L>>2]|0;db(K,s);c[L>>2]=K+24;cb(e,M);bb(M)}Ia(s);b=b+1|0;break a}case 99:{fb(u,494);f=e+4|0;d=c[f>>2]|0;L=c[e+8>>2]|0;g=L;if(d>>>0<L>>>0){db(d,u);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;L=d-f|0;h=(L|0)/24|0;d=h+1|0;if((L|0)<-24)Pa();f=(g-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<d>>>0?d:f}else f=2147483647;ab(M,f,h,e+12|0);L=M+8|0;K=c[L>>2]|0;db(K,u);c[L>>2]=K+24;cb(e,M);bb(M)}Ia(u);b=b+1|0;break a}case 97:{gb(v,499);f=e+4|0;d=c[f>>2]|0;L=c[e+8>>2]|0;g=L;if(d>>>0<L>>>0){db(d,v);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;L=d-f|0;h=(L|0)/24|0;d=h+1|0;if((L|0)<-24)Pa();f=(g-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<d>>>0?d:f}else f=2147483647;ab(M,f,h,e+12|0);L=M+8|0;K=c[L>>2]|0;db(K,v);c[L>>2]=K+24;cb(e,M);bb(M)}Ia(v);b=b+1|0;break a}case 104:{hb(w,511);f=e+4|0;d=c[f>>2]|0;L=c[e+8>>2]|0;g=L;if(d>>>0<L>>>0){db(d,w);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;L=d-f|0;h=(L|0)/24|0;d=h+1|0;if((L|0)<-24)Pa();f=(g-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<d>>>0?d:f}else f=2147483647;ab(M,f,h,e+12|0);L=M+8|0;K=c[L>>2]|0;db(K,w);c[L>>2]=K+24;cb(e,M);bb(M)}Ia(w);b=b+1|0;break a}case 115:{ib(x,525);f=e+4|0;d=c[f>>2]|0;L=c[e+8>>2]|0;g=L;if(d>>>0<L>>>0){db(d,x);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;L=d-f|0;h=(L|0)/24|0;d=h+1|0;if((L|0)<-24)Pa();f=(g-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<d>>>0?d:f}else f=2147483647;ab(M,f,h,e+12|0);L=M+8|0;K=c[L>>2]|0;db(K,x);c[L>>2]=K+24;cb(e,M);bb(M)}Ia(x);b=b+1|0;break a}case 116:{jb(y,531);f=e+4|0;d=c[f>>2]|0;L=c[e+8>>2]|0;g=L;if(d>>>0<L>>>0){db(d,y);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;L=d-f|0;h=(L|0)/24|0;d=h+1|0;if((L|0)<-24)Pa();f=(g-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<d>>>0?d:f}else f=2147483647;ab(M,f,h,e+12|0);L=M+8|0;K=c[L>>2]|0;db(K,y);c[L>>2]=K+24;cb(e,M);bb(M)}Ia(y);b=b+1|0;break a}case 105:{kb(z,546);f=e+4|0;d=c[f>>2]|0;L=c[e+8>>2]|0;g=L;if(d>>>0<L>>>0){db(d,z);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;L=d-f|0;h=(L|0)/24|0;d=h+1|0;if((L|0)<-24)Pa();f=(g-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<d>>>0?d:f}else f=2147483647;ab(M,f,h,e+12|0);L=M+8|0;K=c[L>>2]|0;db(K,z);c[L>>2]=K+24;cb(e,M);bb(M)}Ia(z);b=b+1|0;break a}case 106:{lb(A,550);f=e+4|0;d=c[f>>2]|0;L=c[e+8>>2]|0;g=L;if(d>>>0<L>>>0){db(d,A);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;L=d-f|0;h=(L|0)/24|0;d=h+1|0;if((L|0)<-24)Pa();f=(g-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<d>>>0?d:f}else f=2147483647;ab(M,f,h,e+12|0);L=M+8|0;K=c[L>>2]|0;db(K,A);c[L>>2]=K+24;cb(e,M);bb(M)}Ia(A);b=b+1|0;break a}case 108:{fb(k,563);f=e+4|0;d=c[f>>2]|0;L=c[e+8>>2]|0;g=L;if(d>>>0<L>>>0){db(d,k);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;L=d-f|0;h=(L|0)/24|0;d=h+1|0;if((L|0)<-24)Pa();f=(g-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<d>>>0?d:f}else f=2147483647;ab(M,f,h,e+12|0);L=M+8|0;K=c[L>>2]|0;db(K,k);c[L>>2]=K+24;cb(e,M);bb(M)}Ia(k);b=b+1|0;break a}case 109:{hb(l,568);f=e+4|0;d=c[f>>2]|0;L=c[e+8>>2]|0;g=L;if(d>>>0<L>>>0){db(d,l);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;L=d-f|0;h=(L|0)/24|0;d=h+1|0;if((L|0)<-24)Pa();f=(g-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<d>>>0?d:f}else f=2147483647;ab(M,f,h,e+12|0);L=M+8|0;K=c[L>>2]|0;db(K,l);c[L>>2]=K+24;cb(e,M);bb(M)}Ia(l);b=b+1|0;break a}case 120:{mb(m,582);f=e+4|0;d=c[f>>2]|0;L=c[e+8>>2]|0;g=L;if(d>>>0<L>>>0){db(d,m);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;L=d-f|0;h=(L|0)/24|0;d=h+1|0;if((L|0)<-24)Pa();f=(g-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<d>>>0?d:f}else f=2147483647;ab(M,f,h,e+12|0);L=M+8|0;K=c[L>>2]|0;db(K,m);c[L>>2]=K+24;cb(e,M);bb(M)}Ia(m);b=b+1|0;break a}case 121:{f=vc(32)|0;c[L+8>>2]=f;c[L>>2]=33;c[L+4>>2]=18;d=f;g=592;h=d+18|0;do{a[d>>0]=a[g>>0]|0;d=d+1|0;g=g+1|0}while((d|0)<(h|0));a[f+18>>0]=0;f=L+12|0;d=0;while(1){if((d|0)==3)break;c[f+(d<<2)>>2]=0;d=d+1|0}f=e+4|0;d=c[f>>2]|0;K=c[e+8>>2]|0;g=K;if(d>>>0<K>>>0){db(d,L);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;K=d-f|0;h=(K|0)/24|0;d=h+1|0;if((K|0)<-24)Pa();f=(g-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<d>>>0?d:f}else f=2147483647;ab(M,f,h,e+12|0);K=M+8|0;J=c[K>>2]|0;db(J,L);c[K>>2]=J+24;cb(e,M);bb(M)}Ia(L);b=b+1|0;break a}case 110:{nb(n,611);f=e+4|0;d=c[f>>2]|0;L=c[e+8>>2]|0;g=L;if(d>>>0<L>>>0){db(d,n);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;L=d-f|0;h=(L|0)/24|0;d=h+1|0;if((L|0)<-24)Pa();f=(g-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<d>>>0?d:f}else f=2147483647;ab(M,f,h,e+12|0);L=M+8|0;K=c[L>>2]|0;db(K,n);c[L>>2]=K+24;cb(e,M);bb(M)}Ia(n);b=b+1|0;break a}case 111:{ob(o,620);f=e+4|0;d=c[f>>2]|0;L=c[e+8>>2]|0;g=L;if(d>>>0<L>>>0){db(d,o);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;L=d-f|0;h=(L|0)/24|0;d=h+1|0;if((L|0)<-24)Pa();f=(g-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<d>>>0?d:f}else f=2147483647;ab(M,f,h,e+12|0);L=M+8|0;K=c[L>>2]|0;db(K,o);c[L>>2]=K+24;cb(e,M);bb(M)}Ia(o);b=b+1|0;break a}case 102:{ib(p,638);f=e+4|0;d=c[f>>2]|0;L=c[e+8>>2]|0;g=L;if(d>>>0<L>>>0){db(d,p);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;L=d-f|0;h=(L|0)/24|0;d=h+1|0;if((L|0)<-24)Pa();f=(g-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<d>>>0?d:f}else f=2147483647;ab(M,f,h,e+12|0);L=M+8|0;K=c[L>>2]|0;db(K,p);c[L>>2]=K+24;cb(e,M);bb(M)}Ia(p);b=b+1|0;break a}case 100:{a[K>>0]=12;f=K+1|0;a[f>>0]=a[644]|0;a[f+1>>0]=a[645]|0;a[f+2>>0]=a[646]|0;a[f+3>>0]=a[647]|0;a[f+4>>0]=a[648]|0;a[f+5>>0]=a[649]|0;a[K+7>>0]=0;f=K+12|0;d=0;while(1){if((d|0)==3)break;c[f+(d<<2)>>2]=0;d=d+1|0}f=e+4|0;d=c[f>>2]|0;L=c[e+8>>2]|0;g=L;if(d>>>0<L>>>0){db(d,K);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;L=d-f|0;h=(L|0)/24|0;d=h+1|0;if((L|0)<-24)Pa();f=(g-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<d>>>0?d:f}else f=2147483647;ab(M,f,h,e+12|0);L=M+8|0;J=c[L>>2]|0;db(J,K);c[L>>2]=J+24;cb(e,M);bb(M)}Ia(K);b=b+1|0;break a}case 101:{gb(q,651);f=e+4|0;d=c[f>>2]|0;L=c[e+8>>2]|0;g=L;if(d>>>0<L>>>0){db(d,q);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;L=d-f|0;h=(L|0)/24|0;d=h+1|0;if((L|0)<-24)Pa();f=(g-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<d>>>0?d:f}else f=2147483647;ab(M,f,h,e+12|0);L=M+8|0;K=c[L>>2]|0;db(K,q);c[L>>2]=K+24;cb(e,M);bb(M)}Ia(q);b=b+1|0;break a}case 103:{pb(r,663);f=e+4|0;d=c[f>>2]|0;L=c[e+8>>2]|0;g=L;if(d>>>0<L>>>0){db(d,r);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;L=d-f|0;h=(L|0)/24|0;d=h+1|0;if((L|0)<-24)Pa();f=(g-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<d>>>0?d:f}else f=2147483647;ab(M,f,h,e+12|0);L=M+8|0;K=c[L>>2]|0;db(K,r);c[L>>2]=K+24;cb(e,M);bb(M)}Ia(r);b=b+1|0;break a}case 122:{kb(t,674);f=e+4|0;d=c[f>>2]|0;L=c[e+8>>2]|0;g=L;if(d>>>0<L>>>0){db(d,t);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;L=d-f|0;h=(L|0)/24|0;d=h+1|0;if((L|0)<-24)Pa();f=(g-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<d>>>0?d:f}else f=2147483647;ab(M,f,h,e+12|0);L=M+8|0;K=c[L>>2]|0;db(K,t);c[L>>2]=K+24;cb(e,M);bb(M)}Ia(t);b=b+1|0;break a}case 117:{M=b+1|0;e=qb(M,d,e)|0;b=(e|0)==(M|0)?b:e;break a}case 68:{f=b+1|0;if((f|0)==(d|0))break a;switch(a[f>>0]|0){case 100:{mb(B,711);f=e+4|0;d=c[f>>2]|0;L=c[e+8>>2]|0;g=L;if(d>>>0<L>>>0){db(d,B);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;L=d-f|0;h=(L|0)/24|0;d=h+1|0;if((L|0)<-24)Pa();f=(g-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<d>>>0?d:f}else f=2147483647;ab(M,f,h,e+12|0);L=M+8|0;K=c[L>>2]|0;db(K,B);c[L>>2]=K+24;cb(e,M);bb(M)}Ia(B);b=b+2|0;break a}case 101:{pb(C,721);f=e+4|0;d=c[f>>2]|0;L=c[e+8>>2]|0;g=L;if(d>>>0<L>>>0){db(d,C);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;L=d-f|0;h=(L|0)/24|0;d=h+1|0;if((L|0)<-24)Pa();f=(g-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<d>>>0?d:f}else f=2147483647;ab(M,f,h,e+12|0);L=M+8|0;K=c[L>>2]|0;db(K,C);c[L>>2]=K+24;cb(e,M);bb(M)}Ia(C);b=b+2|0;break a}case 102:{mb(D,732);f=e+4|0;d=c[f>>2]|0;L=c[e+8>>2]|0;g=L;if(d>>>0<L>>>0){db(d,D);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;L=d-f|0;h=(L|0)/24|0;d=h+1|0;if((L|0)<-24)Pa();f=(g-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<d>>>0?d:f}else f=2147483647;ab(M,f,h,e+12|0);L=M+8|0;K=c[L>>2]|0;db(K,D);c[L>>2]=K+24;cb(e,M);bb(M)}Ia(D);b=b+2|0;break a}case 104:{mb(E,742);f=e+4|0;d=c[f>>2]|0;L=c[e+8>>2]|0;g=L;if(d>>>0<L>>>0){db(d,E);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;L=d-f|0;h=(L|0)/24|0;d=h+1|0;if((L|0)<-24)Pa();f=(g-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<d>>>0?d:f}else f=2147483647;ab(M,f,h,e+12|0);L=M+8|0;K=c[L>>2]|0;db(K,E);c[L>>2]=K+24;cb(e,M);bb(M)}Ia(E);b=b+2|0;break a}case 105:{nb(F,752);f=e+4|0;d=c[f>>2]|0;L=c[e+8>>2]|0;g=L;if(d>>>0<L>>>0){db(d,F);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;L=d-f|0;h=(L|0)/24|0;d=h+1|0;if((L|0)<-24)Pa();f=(g-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<d>>>0?d:f}else f=2147483647;ab(M,f,h,e+12|0);L=M+8|0;K=c[L>>2]|0;db(K,F);c[L>>2]=K+24;cb(e,M);bb(M)}Ia(F);b=b+2|0;break a}case 115:{nb(G,761);f=e+4|0;d=c[f>>2]|0;L=c[e+8>>2]|0;g=L;if(d>>>0<L>>>0){db(d,G);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;L=d-f|0;h=(L|0)/24|0;d=h+1|0;if((L|0)<-24)Pa();f=(g-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<d>>>0?d:f}else f=2147483647;ab(M,f,h,e+12|0);L=M+8|0;K=c[L>>2]|0;db(K,G);c[L>>2]=K+24;cb(e,M);bb(M)}Ia(G);b=b+2|0;break a}case 97:{fb(H,770);f=e+4|0;d=c[f>>2]|0;L=c[e+8>>2]|0;g=L;if(d>>>0<L>>>0){db(d,H);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;L=d-f|0;h=(L|0)/24|0;d=h+1|0;if((L|0)<-24)Pa();f=(g-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<d>>>0?d:f}else f=2147483647;ab(M,f,h,e+12|0);L=M+8|0;K=c[L>>2]|0;db(K,H);c[L>>2]=K+24;cb(e,M);bb(M)}Ia(H);b=b+2|0;break a}case 110:{jb(I,775);f=e+4|0;d=c[f>>2]|0;L=c[e+8>>2]|0;g=L;if(d>>>0<L>>>0){db(d,I);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;L=d-f|0;h=(L|0)/24|0;d=h+1|0;if((L|0)<-24)Pa();f=(g-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<d>>>0?d:f}else f=2147483647;ab(M,f,h,e+12|0);L=M+8|0;K=c[L>>2]|0;db(K,I);c[L>>2]=K+24;cb(e,M);bb(M)}Ia(I);b=b+2|0;break a}default:break a}}default:break a}while(0);while(0);i=N;return b|0}function fb(b,e){b=b|0;e=e|0;var f=0;a[b>>0]=8;f=b+1|0;e=d[e>>0]|d[e+1>>0]<<8|d[e+2>>0]<<16|d[e+3>>0]<<24;a[f>>0]=e;a[f+1>>0]=e>>8;a[f+2>>0]=e>>16;a[f+3>>0]=e>>24;a[b+5>>0]=0;e=b+12|0;b=0;while(1){if((b|0)==3)break;c[e+(b<<2)>>2]=0;b=b+1|0}return}function gb(a,b){a=a|0;b=b|0;$a(a,b,11);b=a+12|0;a=0;while(1){if((a|0)==3)break;c[b+(a<<2)>>2]=0;a=a+1|0}return}function hb(a,b){a=a|0;b=b|0;$a(a,b,13);b=a+12|0;a=0;while(1){if((a|0)==3)break;c[b+(a<<2)>>2]=0;a=a+1|0}return}function ib(b,d){b=b|0;d=d|0;var e=0;a[b>>0]=10;e=b+1|0;a[e>>0]=a[d>>0]|0;a[e+1>>0]=a[d+1>>0]|0;a[e+2>>0]=a[d+2>>0]|0;a[e+3>>0]=a[d+3>>0]|0;a[e+4>>0]=a[d+4>>0]|0;a[b+6>>0]=0;d=b+12|0;b=0;while(1){if((b|0)==3)break;c[d+(b<<2)>>2]=0;b=b+1|0}return}function jb(a,b){a=a|0;b=b|0;$a(a,b,14);b=a+12|0;a=0;while(1){if((a|0)==3)break;c[b+(a<<2)>>2]=0;a=a+1|0}return}function kb(b,d){b=b|0;d=d|0;var e=0;a[b>>0]=6;e=b+1|0;a[e>>0]=a[d>>0]|0;a[e+1>>0]=a[d+1>>0]|0;a[e+2>>0]=a[d+2>>0]|0;a[b+4>>0]=0;d=b+12|0;b=0;while(1){if((b|0)==3)break;c[d+(b<<2)>>2]=0;b=b+1|0}return}function lb(a,b){a=a|0;b=b|0;$a(a,b,12);b=a+12|0;a=0;while(1){if((a|0)==3)break;c[b+(a<<2)>>2]=0;a=a+1|0}return}function mb(b,d){b=b|0;d=d|0;var e=0,f=0;a[b>>0]=18;f=b+1|0;e=f+9|0;do{a[f>>0]=a[d>>0]|0;f=f+1|0;d=d+1|0}while((f|0)<(e|0));a[b+10>>0]=0;d=b+12|0;e=0;while(1){if((e|0)==3)break;c[d+(e<<2)>>2]=0;e=e+1|0}return}function nb(b,e){b=b|0;e=e|0;var f=0,g=0,h=0;a[b>>0]=16;f=e;h=f;h=d[h>>0]|d[h+1>>0]<<8|d[h+2>>0]<<16|d[h+3>>0]<<24;f=f+4|0;f=d[f>>0]|d[f+1>>0]<<8|d[f+2>>0]<<16|d[f+3>>0]<<24;e=b+1|0;g=e;a[g>>0]=h;a[g+1>>0]=h>>8;a[g+2>>0]=h>>16;a[g+3>>0]=h>>24;e=e+4|0;a[e>>0]=f;a[e+1>>0]=f>>8;a[e+2>>0]=f>>16;a[e+3>>0]=f>>24;a[b+9>>0]=0;e=b+12|0;b=0;while(1){if((b|0)==3)break;c[e+(b<<2)>>2]=0;b=b+1|0}return}function ob(a,b){a=a|0;b=b|0;$a(a,b,17);b=a+12|0;a=0;while(1){if((a|0)==3)break;c[b+(a<<2)>>2]=0;a=a+1|0}return}function pb(b,d){b=b|0;d=d|0;var e=0,f=0;a[b>>0]=20;f=b+1|0;e=f+10|0;do{a[f>>0]=a[d>>0]|0;f=f+1|0;d=d+1|0}while((f|0)<(e|0));a[b+11>>0]=0;d=b+12|0;e=0;while(1){if((e|0)==3)break;c[d+(e<<2)>>2]=0;e=e+1|0}return}function qb(b,d,e){b=b|0;d=d|0;e=e|0;var f=0,g=0,h=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0;q=i;i=i+112|0;o=q+88|0;p=q+64|0;h=q+76|0;l=q+40|0;j=q+16|0;k=q;a:do if(((b|0)!=(d|0)?(g=(a[b>>0]|0)+-48|0,g>>>0<10):0)?(f=b+1|0,(f|0)!=(d|0)):0){m=f;n=g;while(1){g=(a[m>>0]|0)+-48|0;if(g>>>0>=10)break;f=m+1|0;if((f|0)==(d|0))break a;m=f;n=g+(n*10|0)|0}if((d-m|0)>>>0>=n>>>0){$a(p,m,n);f=a[p>>0]|0;d=(f&1)==0;f=d?(f&255)>>>1:c[p+4>>2]|0;$a(h,d?p+1|0:c[p+8>>2]|0,f>>>0<10?f:10);f=a[h>>0]|0;d=(f&1)==0;f=d?(f&255)>>>1:c[h+4>>2]|0;g=f>>>0>10;d=ac(d?h+1|0:c[h+8>>2]|0,678,g?10:f)|0;Ja(h);if(!(((d|0)==0?(f>>>0<10?-1:g&1):d)|0)){b=vc(32)|0;c[l+8>>2]=b;c[l>>2]=33;c[l+4>>2]=21;f=b;g=689;h=f+21|0;do{a[f>>0]=a[g>>0]|0;f=f+1|0;g=g+1|0}while((f|0)<(h|0));a[b+21>>0]=0;b=l+12|0;f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}b=e+4|0;f=c[b>>2]|0;k=c[e+8>>2]|0;g=k;if(f>>>0<k>>>0){db(f,l);c[b>>2]=(c[b>>2]|0)+24}else{b=c[e>>2]|0;k=f-b|0;h=(k|0)/24|0;f=h+1|0;if((k|0)<-24)Pa();b=(g-b|0)/24|0;if(b>>>0<1073741823){b=b<<1;b=b>>>0<f>>>0?f:b}else b=2147483647;ab(o,b,h,e+12|0);k=o+8|0;j=c[k>>2]|0;db(j,l);c[k>>2]=j+24;cb(e,o);bb(o)}Ia(l)}else{c[k>>2]=c[p>>2];c[k+4>>2]=c[p+4>>2];c[k+8>>2]=c[p+8>>2];b=0;while(1){if((b|0)==3)break;c[p+(b<<2)>>2]=0;b=b+1|0}rb(j,k);b=e+4|0;f=c[b>>2]|0;l=c[e+8>>2]|0;g=l;if(f>>>0<l>>>0){db(f,j);c[b>>2]=(c[b>>2]|0)+24}else{b=c[e>>2]|0;l=f-b|0;h=(l|0)/24|0;f=h+1|0;if((l|0)<-24)Pa();b=(g-b|0)/24|0;if(b>>>0<1073741823){b=b<<1;b=b>>>0<f>>>0?f:b}else b=2147483647;ab(o,b,h,e+12|0);l=o+8|0;d=c[l>>2]|0;db(d,j);c[l>>2]=d+24;cb(e,o);bb(o)}Ia(j);Ja(k)}Ja(p);b=m+n|0}}while(0);i=q;return b|0}function rb(a,b){a=a|0;b=b|0;var d=0;c[a>>2]=c[b>>2];c[a+4>>2]=c[b+4>>2];c[a+8>>2]=c[b+8>>2];d=0;while(1){if((d|0)==3)break;c[b+(d<<2)>>2]=0;d=d+1|0}d=a+12|0;b=0;while(1){if((b|0)==3)break;c[d+(b<<2)>>2]=0;b=b+1|0}return}function sb(b){b=b|0;var d=0,e=0,f=0,g=0,h=0;d=a[b>>0]|0;if(!(d&1)){e=(d&255)>>>1;h=b+1|0}else{e=c[b+4>>2]|0;h=c[b+8>>2]|0}f=(e|0)!=0&1;g=e-f|0;if((e|0)!=(f|0)){Hc(h|0,h+f|0,g|0)|0;d=a[b>>0]|0}if(!(d&1))a[b>>0]=g<<1;else c[b+4>>2]=g;a[h+g>>0]=0;return}function tb(b,c){b=b|0;c=c|0;var d=0,e=0;a:do if((b|0)!=(c|0)?(d=(a[b>>0]|0)==110?b+1|0:b,(d|0)!=(c|0)):0){e=a[d>>0]|0;if(e<<24>>24==48){d=d+1|0;break}if((e+-49&255)<9)do{d=d+1|0;if((d|0)==(c|0)){d=c;break a}}while(((a[d>>0]|0)+-48|0)>>>0<10);else d=b}else d=b;while(0);return d|0}
+function Sb(b,d,e){b=b|0;d=d|0;e=e|0;var f=0,g=0,h=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0,s=0;s=i;i=i+112|0;r=s+88|0;m=s+64|0;n=s+48|0;l=s+24|0;o=s+12|0;p=s;a:do if((b|0)!=(d|0)){g=a[b>>0]|0;h=g<<24>>24;switch(h|0){case 68:case 67:{b:do if((d-b|0)>1?(k=e+4|0,f=c[k>>2]|0,(c[e>>2]|0)!=(f|0)):0){switch(h|0){case 67:{switch(a[b+1>>0]|0){case 53:case 51:case 50:case 49:break;default:break b}Tb(n,f+-24|0);rb(m,n);f=c[k>>2]|0;d=c[e+8>>2]|0;j=d;if(f>>>0<d>>>0){db(f,m);c[k>>2]=(c[k>>2]|0)+24}else{g=c[e>>2]|0;d=f-g|0;k=(d|0)/24|0;h=k+1|0;if((d|0)<-24)Pa();f=(j-g|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<h>>>0?h:f}else f=2147483647;ab(r,f,k,e+12|0);d=r+8|0;q=c[d>>2]|0;db(q,m);c[d>>2]=q+24;cb(e,r);bb(r)}Ia(m);Ja(n);a[e+60>>0]=1;b=b+2|0;break b}case 68:break;default:break b}switch(a[b+1>>0]|0){case 53:case 50:case 49:case 48:break;default:break b}Tb(p,f+-24|0);f=Ta(p,0,886)|0;c[o>>2]=c[f>>2];c[o+4>>2]=c[f+4>>2];c[o+8>>2]=c[f+8>>2];g=0;while(1){if((g|0)==3)break;c[f+(g<<2)>>2]=0;g=g+1|0}rb(l,o);f=c[k>>2]|0;d=c[e+8>>2]|0;j=d;if(f>>>0<d>>>0){db(f,l);c[k>>2]=(c[k>>2]|0)+24}else{g=c[e>>2]|0;d=f-g|0;k=(d|0)/24|0;h=k+1|0;if((d|0)<-24)Pa();f=(j-g|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<h>>>0?h:f}else f=2147483647;ab(r,f,k,e+12|0);d=r+8|0;q=c[d>>2]|0;db(q,l);c[d>>2]=q+24;cb(e,r);bb(r)}Ia(l);Ja(o);Ja(p);a[e+60>>0]=1;b=b+2|0}while(0);break a}case 85:{c:do if((d-b|0)>2&g<<24>>24==85){switch(a[b+1>>0]|0){case 116:{$a(n,1808,8);rb(m,n);l=e+4|0;f=c[l>>2]|0;q=c[e+8>>2]|0;g=q;if(f>>>0<q>>>0){db(f,m);c[l>>2]=(c[l>>2]|0)+24}else{h=c[e>>2]|0;q=f-h|0;k=(q|0)/24|0;j=k+1|0;if((q|0)<-24)Pa();f=(g-h|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<j>>>0?j:f}else f=2147483647;ab(r,f,k,e+12|0);q=r+8|0;p=c[q>>2]|0;db(p,m);c[q>>2]=p+24;cb(e,r);bb(r)}Ia(m);Ja(n);f=b+2|0;if((f|0)==(d|0)){g=c[l>>2]|0;f=g+-24|0;while(1){if((g|0)==(f|0))break c;d=g+-24|0;c[l>>2]=d;Ia(d);g=c[l>>2]|0}}if(((a[f>>0]|0)+-48|0)>>>0<10){g=b+3|0;while(1){if((g|0)==(d|0)){g=d;break}if(((a[g>>0]|0)+-48|0)>>>0>=10)break;g=g+1|0}Bb((c[l>>2]|0)+-24|0,f,g);f=g}zb((c[l>>2]|0)+-24|0,39);if((f|0)!=(d|0)?(a[f>>0]|0)==95:0){b=f+1|0;break c}g=c[l>>2]|0;f=g+-24|0;while(1){if((g|0)==(f|0))break c;d=g+-24|0;c[l>>2]=d;Ia(d);g=c[l>>2]|0}}case 108:break;default:break c}$a(o,1817,9);rb(l,o);q=e+4|0;f=c[q>>2]|0;n=c[e+8>>2]|0;g=n;if(f>>>0<n>>>0){db(f,l);c[q>>2]=(c[q>>2]|0)+24}else{h=c[e>>2]|0;n=f-h|0;k=(n|0)/24|0;j=k+1|0;if((n|0)<-24)Pa();f=(g-h|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<j>>>0?j:f}else f=2147483647;ab(r,f,k,e+12|0);n=r+8|0;m=c[n>>2]|0;db(m,l);c[n>>2]=m+24;cb(e,r);bb(r)}Ia(l);Ja(o);f=b+2|0;do if((a[f>>0]|0)!=118){g=Na(f,d,e)|0;if((g|0)==(f|0)){g=c[q>>2]|0;f=g+-24|0;while(1){if((g|0)==(f|0))break c;d=g+-24|0;c[q>>2]=d;Ia(d);g=c[q>>2]|0}}f=c[q>>2]|0;if(((f-(c[e>>2]|0)|0)/24|0)>>>0<2)break c;Cb(r,f+-24|0);j=c[q>>2]|0;f=j+-24|0;h=j;while(1){if((h|0)==(f|0))break;o=h+-24|0;c[q>>2]=o;Ia(o);h=c[q>>2]|0}h=a[r>>0]|0;l=(h&1)==0;m=r+8|0;n=r+1|0;o=r+4|0;Za(j+-48|0,l?n:c[m>>2]|0,l?(h&255)>>>1:c[o>>2]|0)|0;while(1){l=Na(g,d,e)|0;if((l|0)==(g|0)){f=91;break}f=c[q>>2]|0;if(((f-(c[e>>2]|0)|0)/24|0)>>>0<2){f=129;break}Cb(p,f+-24|0);d:do if(!(h&1)){a[n>>0]=0;a[r>>0]=0}else{g=c[m>>2]|0;a[g>>0]=0;c[o>>2]=0;f=c[r>>2]|0;k=(f&-2)+-1|0;h=f&255;do if(!(h&1)){f=f>>>1&127;if((h&255)<22){Fc(n|0,g|0,f+1|0)|0;wc(g);break}g=f+16&240;j=g+-1|0;if((j|0)==(k|0))break d;h=vc(g)|0;if(j>>>0<=k>>>0&(h|0)==0)break d;Fc(h|0,n|0,f+1|0)|0;c[r>>2]=g|1;c[o>>2]=f;c[m>>2]=h;break d}else{a[n>>0]=0;wc(g);f=0}while(0);a[r>>0]=f<<1}while(0);c[r>>2]=c[p>>2];c[r+4>>2]=c[p+4>>2];c[r+8>>2]=c[p+8>>2];f=0;while(1){if((f|0)==3)break;c[p+(f<<2)>>2]=0;f=f+1|0}Ja(p);j=c[q>>2]|0;f=j+-24|0;g=j;while(1){if((g|0)==(f|0))break;k=g+-24|0;c[q>>2]=k;Ia(k);g=c[q>>2]|0}h=a[r>>0]|0;f=(h&1)==0;g=f?(h&255)>>>1:c[o>>2]|0;if(!g){g=l;continue}Ya(j+-48|0,1429)|0;Za((c[q>>2]|0)+-24|0,f?n:c[m>>2]|0,g)|0;g=l}if((f|0)==91){Ya((c[q>>2]|0)+-24|0,799)|0;Ja(r);break}else if((f|0)==129){Ja(r);break c}}else{zb((c[q>>2]|0)+-24|0,41);g=b+3|0}while(0);if((g|0)!=(d|0)?(a[g>>0]|0)==69:0){f=g+1|0;if((f|0)==(d|0)){g=c[q>>2]|0;f=g+-24|0;while(1){if((g|0)==(f|0))break c;d=g+-24|0;c[q>>2]=d;Ia(d);g=c[q>>2]|0}}e:do if(((a[f>>0]|0)+-48|0)>>>0<10){g=g+2|0;while(1){if((g|0)==(d|0)){g=d;break}if(((a[g>>0]|0)+-48|0)>>>0>=10)break;g=g+1|0}p=c[q>>2]|0;e=p+-24|0;h=a[e>>0]|0;l=p+-16|0;if(!(h&1)){j=l;k=e+1|0;o=(h&255)>>>1;m=10}else{k=c[l>>2]|0;h=c[e>>2]|0;j=k+7|0;o=c[p+-20>>2]|0;m=(h&-2)+-1|0;h=h&255}n=j-k|0;k=g-f|0;if((g|0)!=(f|0)){if((m-o|0)>>>0>=k>>>0){if(!(h&1))h=e+1|0;else h=c[l>>2]|0;if((o|0)==(n|0))j=h;else{j=h+n|0;Hc(j+k|0,j|0,o-n|0)|0;j=h}}else{Ab(e,m,o+k-m|0,o,n,k);j=c[l>>2]|0}h=o+k|0;if(!(a[e>>0]&1))a[e>>0]=h<<1;else c[p+-20>>2]=h;a[j+h>>0]=0;h=j+n|0;while(1){if((f|0)==(g|0)){f=g;break e}a[h>>0]=a[f>>0]|0;f=f+1|0;h=h+1|0}}}while(0);if((f|0)!=(d|0)?(a[f>>0]|0)==95:0){b=f+1|0;break}g=c[q>>2]|0;f=g+-24|0;while(1){if((g|0)==(f|0))break c;d=g+-24|0;c[q>>2]=d;Ia(d);g=c[q>>2]|0}}g=c[q>>2]|0;f=g+-24|0;while(1){if((g|0)==(f|0))break c;d=g+-24|0;c[q>>2]=d;Ia(d);g=c[q>>2]|0}}while(0);break a}case 57:case 56:case 55:case 54:case 53:case 52:case 51:case 50:case 49:{b=qb(b,d,e)|0;break a}default:{d=Lb(b,d,e)|0;i=s;return d|0}}}while(0);i=s;return b|0}function Tb(b,d){b=b|0;d=d|0;var e=0,f=0,g=0,h=0,i=0;h=a[d>>0]|0;e=(h&1)==0;h=e?(h&255)>>>1:c[d+4>>2]|0;a:do if(!h)_a(b,d);else{f=e?d+1|0:c[d+8>>2]|0;e=h>>>0>11;g=ac(f,1478,e?11:h)|0;if(!(((g|0)==0?(h>>>0<11?-1:e&1):g)|0)){Ub(d,1530,70);$a(b,1601,12);break}e=h>>>0>12;g=e?12:h;i=ac(f,1490,g)|0;e=h>>>0<12?-1:e&1;if(!(((i|0)==0?e:i)|0)){Ub(d,1614,49);$a(b,1664,13);break}i=ac(f,1503,g)|0;if(!(((i|0)==0?e:i)|0)){Ub(d,1678,49);$a(b,1728,13);break}g=h>>>0>13;i=ac(f,1516,g?13:h)|0;if(!(((i|0)==0?(h>>>0<13?-1:g&1):i)|0)){Ub(d,1742,50);$a(b,1793,14);break}e=f+h|0;b:do if((a[e+-1>>0]|0)==62){g=1;c:while(1){h=e;d:while(1){e=h+-1|0;if((e|0)==(f|0))break c;h=h+-2|0;switch(a[h>>0]|0){case 60:{d=18;break d}case 62:{d=19;break d}default:h=e}}if((d|0)==18){g=g+-1|0;if(!g){e=h;break b}else continue}else if((d|0)==19){g=g+1|0;continue}}e=0;while(1){if((e|0)==3)break a;c[b+(e<<2)>>2]=0;e=e+1|0}}while(0);h=e;while(1){g=h+-1|0;if((g|0)==(f|0))break;if((a[g>>0]|0)==58){f=h;break}else h=g}d=e-f|0;if(d>>>0>4294967279)Xa();if(d>>>0<11){a[b>>0]=d<<1;h=b+1|0}else{i=d+16&-16;h=vc(i)|0;c[b+8>>2]=h;c[b>>2]=i|1;c[b+4>>2]=d}g=h;while(1){if((f|0)==(e|0))break;a[g>>0]=a[f>>0]|0;f=f+1|0;g=g+1|0}a[h+d>>0]=0}while(0);return}function Ub(b,d,e){b=b|0;d=d|0;e=e|0;var f=0,g=0,h=0;f=a[b>>0]|0;if(!(f&1))h=10;else{f=c[b>>2]|0;h=(f&-2)+-1|0;f=f&255}g=(f&1)==0;do if(h>>>0>=e>>>0){if(g)f=b+1|0;else f=c[b+8>>2]|0;Hc(f|0,d|0,e|0)|0;a[f+e>>0]=0;if(!(a[b>>0]&1)){a[b>>0]=e<<1;break}else{c[b+4>>2]=e;break}}else{if(g)f=(f&255)>>>1;else f=c[b+4>>2]|0;Wa(b,h,e-h|0,f,0,f,e,d)}while(0);return}function Vb(b,d,e){b=b|0;d=d|0;e=e|0;var f=0,g=0,h=0,j=0,k=0;k=i;i=i+16|0;j=k;if((b|0)!=(d|0)?(f=qb(b,d,e)|0,(f|0)!=(b|0)):0){h=Mb(f,d,e)|0;if((h|0)!=(f|0)){g=e+4|0;f=c[g>>2]|0;if(((f-(c[e>>2]|0)|0)/24|0)>>>0<2)f=b;else{Cb(j,f+-24|0);f=c[g>>2]|0;d=f+-24|0;e=f;while(1){if((e|0)==(d|0))break;b=e+-24|0;c[g>>2]=b;Ia(b);e=c[g>>2]|0}g=a[j>>0]|0;b=(g&1)==0;Za(f+-48|0,b?j+1|0:c[j+8>>2]|0,b?(g&255)>>>1:c[j+4>>2]|0)|0;Ja(j);f=h}}}else f=b;i=k;return f|0}function Wb(b,d,e){b=b|0;d=d|0;e=e|0;var f=0,g=0,h=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0,s=0,t=0,u=0,v=0,w=0,x=0,y=0,z=0,A=0,B=0,C=0,D=0,E=0,F=0,G=0,H=0,I=0,J=0,K=0,L=0,M=0,N=0,O=0,P=0,Q=0,R=0,S=0,T=0,U=0,V=0,W=0,X=0,Y=0,Z=0,_=0,$=0,aa=0,ba=0,ca=0,da=0,ea=0,fa=0,ga=0,ha=0,ia=0,ja=0,ka=0,la=0,ma=0,na=0,oa=0,pa=0,qa=0,ra=0,sa=0,ta=0,ua=0;ua=i;i=i+208|0;ta=ua+188|0;ra=ua+184|0;pa=ua+172|0;ba=ua+160|0;ca=ua+144|0;ha=ua+140|0;ia=ua+128|0;ja=ua+112|0;ka=ua+108|0;la=ua+96|0;ma=ua+64|0;na=ua+56|0;oa=ua+40|0;da=ua+36|0;ea=ua+24|0;fa=ua+8|0;ga=ua;n=ua+80|0;k=ua+60|0;m=d;a:do if((m-b|0)>1){sa=(a[b>>0]|0)==76?b+1|0:b;f=a[sa>>0]|0;switch(f<<24>>24|0){case 78:{b:do if((sa|0)!=(d|0))if(f<<24>>24==78){f=Oa(sa+1|0,d,ra)|0;c:do if((f|0)!=(d|0)){h=e+52|0;c[h>>2]=0;switch(a[f>>0]|0){case 82:{c[h>>2]=1;f=f+1|0;break}case 79:{c[h>>2]=2;f=f+1|0;break}default:{}}aa=e+4|0;j=c[aa>>2]|0;$=c[e+8>>2]|0;h=$;if(j>>>0<$>>>0){c[j>>2]=0;c[j+4>>2]=0;c[j+8>>2]=0;c[j+12>>2]=0;c[j+16>>2]=0;c[j+20>>2]=0;h=0;while(1){if((h|0)==3)break;c[j+(h<<2)>>2]=0;h=h+1|0}h=j+12|0;j=0;while(1){if((j|0)==3)break;c[h+(j<<2)>>2]=0;j=j+1|0}c[aa>>2]=(c[aa>>2]|0)+24}else{k=c[e>>2]|0;$=j-k|0;l=($|0)/24|0;j=l+1|0;if(($|0)<-24)Pa();h=(h-k|0)/24|0;if(h>>>0<1073741823){h=h<<1;h=h>>>0<j>>>0?j:h}else h=2147483647;ab(ta,h,l,e+12|0);k=ta+8|0;l=c[k>>2]|0;c[l>>2]=0;c[l+4>>2]=0;c[l+8>>2]=0;c[l+12>>2]=0;c[l+16>>2]=0;c[l+20>>2]=0;h=0;while(1){if((h|0)==3)break;c[l+(h<<2)>>2]=0;h=h+1|0}h=l+12|0;j=0;while(1){if((j|0)==3)break;c[h+(j<<2)>>2]=0;j=j+1|0}c[k>>2]=l+24;cb(e,ta);bb(ta)}if(((m-f|0)>1?(a[f>>0]|0)==83:0)?(a[f+1>>0]|0)==116:0){Ub((c[aa>>2]|0)+-24|0,2080,3);f=f+2|0}if((f|0)==(d|0)){g=c[aa>>2]|0;f=g+-24|0;while(1){if((g|0)==(f|0))break c;ta=g+-24|0;c[aa>>2]=ta;Ia(ta);g=c[aa>>2]|0}}I=pa+8|0;J=pa+1|0;K=pa+4|0;L=e+12|0;M=e+16|0;$=e+20|0;N=e+24|0;O=oa+12|0;P=oa+4|0;Q=oa+8|0;R=e+28|0;S=ta+8|0;T=fa+12|0;U=fa+4|0;V=fa+8|0;W=ta+8|0;X=ea+8|0;Y=ea+1|0;Z=ea+4|0;_=ba+8|0;o=ba+1|0;p=ba+4|0;q=ca+12|0;r=ca+4|0;s=ca+8|0;t=ta+8|0;u=ja+12|0;v=ja+4|0;w=ja+8|0;x=ta+8|0;y=ia+8|0;z=ia+1|0;A=ia+4|0;B=ma+12|0;C=ma+4|0;D=ma+8|0;E=ta+8|0;F=la+8|0;G=la+1|0;H=la+4|0;n=0;d:while(1){h=f;e:while(1){f=a[h>>0]|0;if(f<<24>>24==69){qa=129;break d}switch(f<<24>>24|0){case 83:{qa=39;break e}case 84:{qa=59;break e}case 68:{qa=77;break e}case 73:break;case 76:{f=h+1|0;if((f|0)==(d|0))break c;else{h=f;continue e}}default:break e}m=Mb(h,d,e)|0;if((m|0)==(h|0)|(m|0)==(d|0))break c;Cb(pa,(c[aa>>2]|0)+-24|0);f=c[aa>>2]|0;h=f+-24|0;j=f;while(1){if((j|0)==(h|0))break;l=j+-24|0;c[aa>>2]=l;Ia(l);j=c[aa>>2]|0}l=a[pa>>0]|0;h=(l&1)==0;Za(f+-48|0,h?J:c[I>>2]|0,h?(l&255)>>>1:c[K>>2]|0)|0;f=(c[aa>>2]|0)+-24|0;c[da>>2]=c[L>>2];Pb(oa,f,da);f=c[$>>2]|0;l=c[N>>2]|0;h=l;if(f>>>0<l>>>0){c[f+12>>2]=c[O>>2];c[f>>2]=c[oa>>2];c[f+4>>2]=c[P>>2];c[f+8>>2]=c[Q>>2];c[Q>>2]=0;c[P>>2]=0;c[oa>>2]=0;c[$>>2]=(c[$>>2]|0)+16}else{j=c[M>>2]|0;f=f-j|0;l=f>>4;k=l+1|0;if((f|0)<-16){qa=104;break d}f=h-j|0;if(f>>4>>>0<1073741823){f=f>>3;f=f>>>0<k>>>0?k:f}else f=2147483647;Qa(ta,f,l,R);l=c[S>>2]|0;c[l+12>>2]=c[O>>2];c[l>>2]=c[oa>>2];c[l+4>>2]=c[P>>2];c[l+8>>2]=c[Q>>2];c[Q>>2]=0;c[P>>2]=0;c[oa>>2]=0;c[S>>2]=l+16;Ra(M,ta);Sa(ta)}Ha(oa);Ja(pa);h=m}f:do if((qa|0)==39){qa=0;n=h+1|0;if((n|0)!=(d|0)?(a[n>>0]|0)==116:0)break;f=Rb(h,d,e)|0;if((f|0)==(h|0)|(f|0)==(d|0))break c;Cb(pa,(c[aa>>2]|0)+-24|0);k=c[aa>>2]|0;h=k+-24|0;j=k;while(1){if((j|0)==(h|0))break;n=j+-24|0;c[aa>>2]=n;Ia(n);j=c[aa>>2]|0}j=k+-48|0;h=a[j>>0]|0;if(!(h&1))h=(h&255)>>>1;else h=c[k+-44>>2]|0;if(!h)Xb(j,pa);else{xb(ba,891,pa);h=a[ba>>0]|0;n=(h&1)==0;Za(j,n?o:c[_>>2]|0,n?(h&255)>>>1:c[p>>2]|0)|0;Ja(ba);h=(c[aa>>2]|0)+-24|0;c[ha>>2]=c[L>>2];Pb(ca,h,ha);h=c[$>>2]|0;n=c[N>>2]|0;j=n;if(h>>>0<n>>>0){c[h+12>>2]=c[q>>2];c[h>>2]=c[ca>>2];c[h+4>>2]=c[r>>2];c[h+8>>2]=c[s>>2];c[s>>2]=0;c[r>>2]=0;c[ca>>2]=0;c[$>>2]=(c[$>>2]|0)+16}else{k=c[M>>2]|0;n=h-k|0;m=n>>4;l=m+1|0;if((n|0)<-16){qa=52;break d}h=j-k|0;if(h>>4>>>0<1073741823){h=h>>3;h=h>>>0<l>>>0?l:h}else h=2147483647;Qa(ta,h,m,R);n=c[t>>2]|0;c[n+12>>2]=c[q>>2];c[n>>2]=c[ca>>2];c[n+4>>2]=c[r>>2];c[n+8>>2]=c[s>>2];c[s>>2]=0;c[r>>2]=0;c[ca>>2]=0;c[t>>2]=n+16;Ra(M,ta);Sa(ta)}Ha(ca)}Ja(pa);n=1;continue d}else if((qa|0)==59){qa=0;f=Eb(h,d,e)|0;if((f|0)==(h|0)|(f|0)==(d|0))break c;Cb(pa,(c[aa>>2]|0)+-24|0);k=c[aa>>2]|0;h=k+-24|0;j=k;while(1){if((j|0)==(h|0))break;n=j+-24|0;c[aa>>2]=n;Ia(n);j=c[aa>>2]|0}j=k+-48|0;h=a[j>>0]|0;if(!(h&1))h=(h&255)>>>1;else h=c[k+-44>>2]|0;if(!h)Xb(j,pa);else{xb(ia,891,pa);n=a[ia>>0]|0;m=(n&1)==0;Za(j,m?z:c[y>>2]|0,m?(n&255)>>>1:c[A>>2]|0)|0;Ja(ia)}h=(c[aa>>2]|0)+-24|0;c[ka>>2]=c[L>>2];Pb(ja,h,ka);h=c[$>>2]|0;n=c[N>>2]|0;j=n;if(h>>>0<n>>>0){c[h+12>>2]=c[u>>2];c[h>>2]=c[ja>>2];c[h+4>>2]=c[v>>2];c[h+8>>2]=c[w>>2];c[w>>2]=0;c[v>>2]=0;c[ja>>2]=0;c[$>>2]=(c[$>>2]|0)+16}else{k=c[M>>2]|0;n=h-k|0;m=n>>4;l=m+1|0;if((n|0)<-16){qa=72;break d}h=j-k|0;if(h>>4>>>0<1073741823){h=h>>3;h=h>>>0<l>>>0?l:h}else h=2147483647;Qa(ta,h,m,R);n=c[x>>2]|0;c[n+12>>2]=c[u>>2];c[n>>2]=c[ja>>2];c[n+4>>2]=c[v>>2];c[n+8>>2]=c[w>>2];c[w>>2]=0;c[v>>2]=0;c[ja>>2]=0;c[x>>2]=n+16;Ra(M,ta);Sa(ta)}Ha(ja);Ja(pa);n=1;continue d}else if((qa|0)==77){qa=0;f=h+1|0;if((f|0)!=(d|0))switch(a[f>>0]|0){case 84:case 116:break;default:break f}f=Qb(h,d,e)|0;if((f|0)==(h|0)|(f|0)==(d|0))break c;Cb(pa,(c[aa>>2]|0)+-24|0);k=c[aa>>2]|0;h=k+-24|0;j=k;while(1){if((j|0)==(h|0))break;n=j+-24|0;c[aa>>2]=n;Ia(n);j=c[aa>>2]|0}j=k+-48|0;h=a[j>>0]|0;if(!(h&1))h=(h&255)>>>1;else h=c[k+-44>>2]|0;if(!h)Xb(j,pa);else{xb(la,891,pa);n=a[la>>0]|0;m=(n&1)==0;Za(j,m?G:c[F>>2]|0,m?(n&255)>>>1:c[H>>2]|0)|0;Ja(la)}h=(c[aa>>2]|0)+-24|0;c[na>>2]=c[L>>2];Pb(ma,h,na);h=c[$>>2]|0;n=c[N>>2]|0;j=n;if(h>>>0<n>>>0){c[h+12>>2]=c[B>>2];c[h>>2]=c[ma>>2];c[h+4>>2]=c[C>>2];c[h+8>>2]=c[D>>2];c[D>>2]=0;c[C>>2]=0;c[ma>>2]=0;c[$>>2]=(c[$>>2]|0)+16}else{k=c[M>>2]|0;n=h-k|0;m=n>>4;l=m+1|0;if((n|0)<-16){qa=92;break d}h=j-k|0;if(h>>4>>>0<1073741823){h=h>>3;h=h>>>0<l>>>0?l:h}else h=2147483647;Qa(ta,h,m,R);n=c[E>>2]|0;c[n+12>>2]=c[B>>2];c[n>>2]=c[ma>>2];c[n+4>>2]=c[C>>2];c[n+8>>2]=c[D>>2];c[D>>2]=0;c[C>>2]=0;c[ma>>2]=0;c[E>>2]=n+16;Ra(M,ta);Sa(ta)}Ha(ma);Ja(pa);n=1;continue d}while(0);f=Sb(h,d,e)|0;if((f|0)==(h|0)|(f|0)==(d|0))break c;Cb(pa,(c[aa>>2]|0)+-24|0);k=c[aa>>2]|0;h=k+-24|0;j=k;while(1){if((j|0)==(h|0))break;n=j+-24|0;c[aa>>2]=n;Ia(n);j=c[aa>>2]|0}j=k+-48|0;h=a[j>>0]|0;if(!(h&1))h=(h&255)>>>1;else h=c[k+-44>>2]|0;if(!h)Xb(j,pa);else{xb(ea,891,pa);n=a[ea>>0]|0;m=(n&1)==0;Za(j,m?Y:c[X>>2]|0,m?(n&255)>>>1:c[Z>>2]|0)|0;Ja(ea)}h=(c[aa>>2]|0)+-24|0;c[ga>>2]=c[L>>2];Pb(fa,h,ga);h=c[$>>2]|0;n=c[N>>2]|0;j=n;if(h>>>0<n>>>0){c[h+12>>2]=c[T>>2];c[h>>2]=c[fa>>2];c[h+4>>2]=c[U>>2];c[h+8>>2]=c[V>>2];c[V>>2]=0;c[U>>2]=0;c[fa>>2]=0;c[$>>2]=(c[$>>2]|0)+16}else{k=c[M>>2]|0;n=h-k|0;m=n>>4;l=m+1|0;if((n|0)<-16){qa=123;break}h=j-k|0;if(h>>4>>>0<1073741823){h=h>>3;h=h>>>0<l>>>0?l:h}else h=2147483647;Qa(ta,h,m,R);n=c[W>>2]|0;c[n+12>>2]=c[T>>2];c[n>>2]=c[fa>>2];c[n+4>>2]=c[U>>2];c[n+8>>2]=c[V>>2];c[V>>2]=0;c[U>>2]=0;c[fa>>2]=0;c[W>>2]=n+16;Ra(M,ta);Sa(ta)}Ha(fa);Ja(pa);n=1}if((qa|0)==52)Pa();else if((qa|0)==72)Pa();else if((qa|0)==92)Pa();else if((qa|0)==104)Pa();else if((qa|0)==123)Pa();else if((qa|0)==129){f=h+1|0;c[e+48>>2]=c[ra>>2];g:do if(n?(g=c[$>>2]|0,(c[e+16>>2]|0)!=(g|0)):0){h=g+-16|0;while(1){if((g|0)==(h|0))break g;ta=g+-16|0;c[$>>2]=ta;Ha(ta);g=c[$>>2]|0}}while(0);break b}}while(0);f=sa}else f=sa;else f=d;while(0);f=(f|0)==(sa|0)?b:f;break a}case 90:{h:do if(((f<<24>>24==90&(sa|0)!=(d|0)?(ra=sa+1|0,h=Ma(ra,d,e)|0,!((h|0)==(ra|0)|(h|0)==(d|0))):0)?(a[h>>0]|0)==69:0)?(j=h+1|0,(j|0)!=(d|0)):0)switch(a[j>>0]|0){case 115:{f=Yb(h+2|0,d)|0;g=c[e+4>>2]|0;if((c[e>>2]|0)==(g|0))break h;Ya(g+-24|0,2084)|0;break h}case 100:{f=h+2|0;if((f|0)==(d|0)){f=sa;break h}f=tb(f,d)|0;if((f|0)==(d|0)){f=sa;break h}if((a[f>>0]|0)!=95){f=sa;break h}ra=f+1|0;f=Wb(ra,d,e)|0;k=e+4|0;if((f|0)==(ra|0)){g=c[k>>2]|0;f=g+-24|0;while(1){if((g|0)==(f|0)){f=sa;break h}ta=g+-24|0;c[k>>2]=ta;Ia(ta);g=c[k>>2]|0}}g=c[k>>2]|0;if(((g-(c[e>>2]|0)|0)/24|0)>>>0<2){f=sa;break h}Cb(ta,g+-24|0);g=c[k>>2]|0;h=g+-24|0;j=g;while(1){if((j|0)==(h|0))break;e=j+-24|0;c[k>>2]=e;Ia(e);j=c[k>>2]|0}Ya(g+-48|0,891)|0;e=a[ta>>0]|0;d=(e&1)==0;Za((c[k>>2]|0)+-24|0,d?ta+1|0:c[ta+8>>2]|0,d?(e&255)>>>1:c[ta+4>>2]|0)|0;Ja(ta);break h}default:{f=Wb(j,d,e)|0;if((f|0)==(j|0)){f=e+4|0;h=c[f>>2]|0;g=h+-24|0;while(1){if((h|0)==(g|0)){f=sa;break h}ta=h+-24|0;c[f>>2]=ta;Ia(ta);h=c[f>>2]|0}}f=Yb(f,d)|0;k=e+4|0;g=c[k>>2]|0;if(((g-(c[e>>2]|0)|0)/24|0)>>>0<2)break h;Cb(ta,g+-24|0);g=c[k>>2]|0;h=g+-24|0;j=g;while(1){if((j|0)==(h|0))break;e=j+-24|0;c[k>>2]=e;Ia(e);j=c[k>>2]|0}Ya(g+-48|0,891)|0;e=a[ta>>0]|0;d=(e&1)==0;Za((c[k>>2]|0)+-24|0,d?ta+1|0:c[ta+8>>2]|0,d?(e&255)>>>1:c[ta+4>>2]|0)|0;Ja(ta);break h}}else f=sa;while(0);f=(f|0)==(sa|0)?b:f;break a}default:{do if((m-sa|0)>1){if(f<<24>>24==83?(a[sa+1>>0]|0)==116:0){f=sa+2|0;if((f|0)==(d|0)){h=0;g=d}else{h=0;g=(a[f>>0]|0)==76?sa+3|0:f}}else{h=1;g=sa}f=Sb(g,d,e)|0;g=(f|0)==(g|0);if(h|g)f=g?sa:f;else{g=c[e+4>>2]|0;if((c[e>>2]|0)==(g|0))break;Ta(g+-24|0,0,1827)|0}if((f|0)!=(sa|0)){if((f|0)==(d|0)){f=d;break a}if((a[f>>0]|0)!=73)break a;m=e+4|0;g=c[m>>2]|0;if((c[e>>2]|0)==(g|0)){f=b;break a}l=e+16|0;c[k>>2]=c[e+12>>2];Pb(n,g+-24|0,k);g=e+20|0;h=c[g>>2]|0;sa=c[e+24>>2]|0;j=sa;if(h>>>0<sa>>>0){c[h+12>>2]=c[n+12>>2];c[h>>2]=c[n>>2];sa=n+4|0;c[h+4>>2]=c[sa>>2];ra=n+8|0;c[h+8>>2]=c[ra>>2];c[ra>>2]=0;c[sa>>2]=0;c[n>>2]=0;c[g>>2]=(c[g>>2]|0)+16}else{g=c[l>>2]|0;sa=h-g|0;k=sa>>4;h=k+1|0;if((sa|0)<-16)Pa();g=j-g|0;if(g>>4>>>0<1073741823){g=g>>3;g=g>>>0<h>>>0?h:g}else g=2147483647;Qa(ta,g,k,e+28|0);sa=ta+8|0;ra=c[sa>>2]|0;c[ra+12>>2]=c[n+12>>2];c[ra>>2]=c[n>>2];qa=n+4|0;c[ra+4>>2]=c[qa>>2];pa=n+8|0;c[ra+8>>2]=c[pa>>2];c[pa>>2]=0;c[qa>>2]=0;c[n>>2]=0;c[sa>>2]=ra+16;Ra(l,ta);Sa(ta)}Ha(n);j=Mb(f,d,e)|0;if((j|0)==(f|0)){f=b;break a}f=c[m>>2]|0;if(((f-(c[e>>2]|0)|0)/24|0)>>>0<2){f=b;break a}Cb(ta,f+-24|0);f=c[m>>2]|0;g=f+-24|0;h=f;while(1){if((h|0)==(g|0))break;b=h+-24|0;c[m>>2]=b;Ia(b);h=c[m>>2]|0}b=a[ta>>0]|0;sa=(b&1)==0;Za(f+-48|0,sa?ta+1|0:c[ta+8>>2]|0,sa?(b&255)>>>1:c[ta+4>>2]|0)|0;Ja(ta);f=j;break a}}while(0);g=Rb(sa,d,e)|0;if((g|0)==(sa|0)|(g|0)==(d|0)){f=b;break a}if((a[g>>0]|0)!=73){f=b;break a}f=Mb(g,d,e)|0;if((f|0)==(g|0)){f=b;break a}k=e+4|0;g=c[k>>2]|0;if(((g-(c[e>>2]|0)|0)/24|0)>>>0<2){f=b;break a}Cb(ta,g+-24|0);g=c[k>>2]|0;h=g+-24|0;j=g;while(1){if((j|0)==(h|0))break;b=j+-24|0;c[k>>2]=b;Ia(b);j=c[k>>2]|0}b=a[ta>>0]|0;sa=(b&1)==0;Za(g+-48|0,sa?ta+1|0:c[ta+8>>2]|0,sa?(b&255)>>>1:c[ta+4>>2]|0)|0;Ja(ta);break a}}}else f=b;while(0);i=ua;return f|0}function Xb(b,d){b=b|0;d=d|0;var e=0,f=0;if((b|0)!=(d|0)){e=a[d>>0]|0;f=(e&1)==0;Ub(b,f?d+1|0:c[d+8>>2]|0,f?(e&255)>>>1:c[d+4>>2]|0)}return}function Yb(b,c){b=b|0;c=c|0;var d=0,e=0;a:do if((b|0)!=(c|0)){d=a[b>>0]|0;if(d<<24>>24!=95){if(((d<<24>>24)+-48|0)>>>0>=10)break;while(1){b=b+1|0;if((b|0)==(c|0)){b=c;break a}if(((a[b>>0]|0)+-48|0)>>>0>=10)break a}}d=b+1|0;if((d|0)!=(c|0)){d=a[d>>0]|0;if(((d<<24>>24)+-48|0)>>>0<10){b=b+2|0;break}if(d<<24>>24==95){e=b+2|0;while(1){if((e|0)==(c|0))break a;d=a[e>>0]|0;if(((d<<24>>24)+-48|0)>>>0>=10)break;e=e+1|0}return (d<<24>>24==95?e+1|0:b)|0}}}while(0);return b|0}function Zb(b,c){b=b|0;c=c|0;var d=0,e=0,f=0;a:do if((b|0)!=(c|0)){switch(a[b>>0]|0){case 104:{e=b+1|0;d=tb(e,c)|0;if((d|0)==(e|0)|(d|0)==(c|0))break a;return ((a[d>>0]|0)==95?d+1|0:b)|0}case 118:break;default:break a}f=b+1|0;d=tb(f,c)|0;if((!((d|0)==(f|0)|(d|0)==(c|0))?(a[d>>0]|0)==95:0)?(f=d+1|0,e=tb(f,c)|0,!((e|0)==(f|0)|(e|0)==(c|0))):0)b=(a[e>>0]|0)==95?e+1|0:b}while(0);return b|0}function _b(a){a=a|0;$b(a+32|0);Ga(a+16|0);Ha(a);return}function $b(a){a=a|0;var b=0,d=0,e=0;b=c[a>>2]|0;if(b){d=a+4|0;while(1){e=c[d>>2]|0;if((e|0)==(b|0))break;e=e+-16|0;c[d>>2]=e;Ga(e)}e=c[a>>2]|0;Ka(c[a+12>>2]|0,e,(c[a+8>>2]|0)-e|0)}return}function ac(b,c,d){b=b|0;c=c|0;d=d|0;var e=0,f=0;a:do if(!d)d=0;else{while(1){e=a[b>>0]|0;f=a[c>>0]|0;if(e<<24>>24!=f<<24>>24)break;d=d+-1|0;if(!d){d=0;break a}else{b=b+1|0;c=c+1|0}}d=(e&255)-(f&255)|0}while(0);return d|0}function bc(b){b=b|0;var d=0,e=0,f=0;f=b;a:do if(!(f&3))e=4;else{d=b;b=f;while(1){if(!(a[d>>0]|0))break a;d=d+1|0;b=d;if(!(b&3)){b=d;e=4;break}}}while(0);if((e|0)==4){while(1){d=c[b>>2]|0;if(!((d&-2139062144^-2139062144)&d+-16843009))b=b+4|0;else break}if((d&255)<<24>>24)do b=b+1|0;while((a[b>>0]|0)!=0)}return b-f|0}function cc(b,d,e){b=b|0;d=d|0;e=e|0;var f=0,g=0,h=0,i=0;h=d&255;f=(e|0)!=0;a:do if(f&(b&3|0)!=0){g=d&255;while(1){if((a[b>>0]|0)==g<<24>>24)break a;b=b+1|0;e=e+-1|0;f=(e|0)!=0;if(!(f&(b&3|0)!=0)){i=5;break}}}else i=5;while(0);b:do if((i|0)==5)if(f){g=d&255;if((a[b>>0]|0)!=g<<24>>24){f=_(h,16843009)|0;c:do if(e>>>0>3)while(1){h=c[b>>2]^f;if((h&-2139062144^-2139062144)&h+-16843009)break;b=b+4|0;e=e+-4|0;if(e>>>0<=3){i=11;break c}}else i=11;while(0);if((i|0)==11)if(!e){e=0;break}while(1){if((a[b>>0]|0)==g<<24>>24)break b;b=b+1|0;e=e+-1|0;if(!e){e=0;break}}}}else e=0;while(0);return ((e|0)!=0?b:0)|0}function dc(b){b=b|0;var c=0,e=0;c=0;while(1){if((d[2370+c>>0]|0)==(b|0)){e=2;break}c=c+1|0;if((c|0)==87){c=87;b=2458;e=5;break}}if((e|0)==2)if(!c)c=2458;else{b=2458;e=5}if((e|0)==5)while(1){do{e=b;b=b+1|0}while((a[e>>0]|0)!=0);c=c+-1|0;if(!c){c=b;break}else e=5}return c|0}function ec(){var a=0;if(!(c[1200]|0))a=4844;else a=c[(fa()|0)+60>>2]|0;return a|0}function fc(a){a=a|0;if((a+-48|0)>>>0<10)a=1;else a=((a|32)+-97|0)>>>0<6;return a&1|0}function gc(a){a=a|0;return (a+-65|0)>>>0<26|0}function hc(b,d,e,f){b=b|0;d=d|0;e=e|0;f=f|0;var g=0,h=0,j=0,k=0,l=0,m=0,n=0;n=i;i=i+128|0;g=n+112|0;m=n;h=m;j=8;k=h+112|0;do{c[h>>2]=c[j>>2];h=h+4|0;j=j+4|0}while((h|0)<(k|0));if((d+-1|0)>>>0>2147483646)if(!d){d=1;l=4}else{c[(ec()|0)>>2]=75;d=-1}else{g=b;l=4}if((l|0)==4){l=-2-g|0;l=d>>>0>l>>>0?l:d;c[m+48>>2]=l;b=m+20|0;c[b>>2]=g;c[m+44>>2]=g;d=g+l|0;g=m+16|0;c[g>>2]=d;c[m+28>>2]=d;d=lc(m,e,f)|0;if(l){e=c[b>>2]|0;a[e+(((e|0)==(c[g>>2]|0))<<31>>31)>>0]=0}}i=n;return d|0}function ic(b,d,e){b=b|0;d=d|0;e=e|0;var f=0,g=0,h=0,i=0;f=e+16|0;g=c[f>>2]|0;if(!g)if(!(kc(e)|0)){g=c[f>>2]|0;h=5}else f=0;else h=5;a:do if((h|0)==5){i=e+20|0;f=c[i>>2]|0;h=f;if((g-f|0)>>>0<d>>>0){f=ra[c[e+36>>2]&1](e,b,d)|0;break}b:do if((a[e+75>>0]|0)>-1){f=d;while(1){if(!f){g=h;f=0;break b}g=f+-1|0;if((a[b+g>>0]|0)==10)break;else f=g}if((ra[c[e+36>>2]&1](e,b,f)|0)>>>0<f>>>0)break a;d=d-f|0;b=b+f|0;g=c[i>>2]|0}else{g=h;f=0}while(0);Fc(g|0,b|0,d|0)|0;c[i>>2]=(c[i>>2]|0)+d;f=f+d|0}while(0);return f|0}function jc(a,b,d,e){a=a|0;b=b|0;d=d|0;e=e|0;var f=0,g=0;f=i;i=i+16|0;g=f;c[g>>2]=e;e=hc(a,b,d,g)|0;i=f;return e|0}function kc(b){b=b|0;var d=0,e=0;d=b+74|0;e=a[d>>0]|0;a[d>>0]=e+255|e;d=c[b>>2]|0;if(!(d&8)){c[b+8>>2]=0;c[b+4>>2]=0;d=c[b+44>>2]|0;c[b+28>>2]=d;c[b+20>>2]=d;c[b+16>>2]=d+(c[b+48>>2]|0);d=0}else{c[b>>2]=d|32;d=-1}return d|0}function lc(b,d,e){b=b|0;d=d|0;e=e|0;var f=0,g=0,h=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0;r=i;i=i+224|0;n=r+120|0;q=r+80|0;p=r;o=r+136|0;f=q;g=f+40|0;do{c[f>>2]=0;f=f+4|0}while((f|0)<(g|0));c[n>>2]=c[e>>2];if((rc(0,d,n,p,q)|0)<0)e=-1;else{e=c[b>>2]|0;m=e&32;if((a[b+74>>0]|0)<1)c[b>>2]=e&-33;l=b+48|0;if(!(c[l>>2]|0)){f=b+44|0;g=c[f>>2]|0;c[f>>2]=o;h=b+28|0;c[h>>2]=o;j=b+20|0;c[j>>2]=o;c[l>>2]=80;k=b+16|0;c[k>>2]=o+80;e=rc(b,d,n,p,q)|0;if(g){ra[c[b+36>>2]&1](b,0,0)|0;e=(c[j>>2]|0)==0?-1:e;c[f>>2]=g;c[l>>2]=0;c[k>>2]=0;c[h>>2]=0;c[j>>2]=0}}else e=rc(b,d,n,p,q)|0;q=c[b>>2]|0;c[b>>2]=q|m;e=(q&32|0)==0?e:-1}i=r;return e|0}function mc(b,d){b=b|0;d=d|0;do if(b){if(d>>>0<128){a[b>>0]=d;b=1;break}if(d>>>0<2048){a[b>>0]=d>>>6|192;a[b+1>>0]=d&63|128;b=2;break}if(d>>>0<55296|(d&-8192|0)==57344){a[b>>0]=d>>>12|224;a[b+1>>0]=d>>>6&63|128;a[b+2>>0]=d&63|128;b=3;break}if((d+-65536|0)>>>0<1048576){a[b>>0]=d>>>18|240;a[b+1>>0]=d>>>12&63|128;a[b+2>>0]=d>>>6&63|128;a[b+3>>0]=d&63|128;b=4;break}else{c[(ec()|0)>>2]=84;b=-1;break}}else b=1;while(0);return b|0}function nc(a,b){a=a|0;b=b|0;if(!a)a=0;else a=mc(a,b)|0;return a|0}function oc(a,b){a=+a;b=b|0;return +(+pc(a,b))}function pc(a,b){a=+a;b=b|0;var d=0,e=0,f=0;h[k>>3]=a;d=c[k>>2]|0;e=c[k+4>>2]|0;f=Dc(d|0,e|0,52)|0;f=f&2047;switch(f|0){case 0:{if(a!=0.0){a=+pc(a*18446744073709551616.0,b);d=(c[b>>2]|0)+-64|0}else d=0;c[b>>2]=d;break}case 2047:break;default:{c[b>>2]=f+-1022;c[k>>2]=d;c[k+4>>2]=e&-2146435073|1071644672;a=+h[k>>3]}}return +a}function qc(a,b,d){a=a|0;b=b|0;d=d|0;var e=0,f=0;e=a+20|0;f=c[e>>2]|0;a=(c[a+16>>2]|0)-f|0;a=a>>>0>d>>>0?d:a;Fc(f|0,b|0,a|0)|0;c[e>>2]=(c[e>>2]|0)+a;return d|0}function rc(e,f,g,j,l){e=e|0;f=f|0;g=g|0;j=j|0;l=l|0;var m=0,n=0,o=0,p=0.0,q=0,r=0,s=0,t=0,u=0,v=0,w=0.0,x=0,y=0,z=0,A=0,B=0,D=0,E=0,F=0,G=0,H=0,I=0,J=0,K=0,L=0,M=0,N=0,O=0,P=0,Q=0,R=0,S=0,T=0,U=0,V=0,W=0,X=0,Y=0,Z=0,$=0,aa=0,ba=0,ca=0,da=0,ea=0,fa=0,ga=0,ha=0,ia=0;ia=i;i=i+624|0;da=ia+24|0;fa=ia+16|0;ea=ia+588|0;aa=ia+576|0;ca=ia;W=ia+536|0;ha=ia+8|0;ga=ia+528|0;M=(e|0)!=0;N=W+40|0;V=N;W=W+39|0;X=ha+4|0;Y=ea;Z=0-Y|0;$=aa+12|0;aa=aa+11|0;ba=$;O=ba-Y|0;P=-2-Y|0;Q=ba+2|0;R=da+288|0;S=ea+9|0;T=S;U=ea+8|0;m=0;n=0;r=0;x=f;a:while(1){do if((m|0)>-1)if((n|0)>(2147483647-m|0)){c[(ec()|0)>>2]=75;m=-1;break}else{m=n+m|0;break}while(0);f=a[x>>0]|0;if(!(f<<24>>24)){L=244;break}else n=x;b:while(1){switch(f<<24>>24){case 37:{f=n;L=9;break b}case 0:{f=n;break b}default:{}}K=n+1|0;f=a[K>>0]|0;n=K}c:do if((L|0)==9)while(1){L=0;if((a[f+1>>0]|0)!=37)break c;n=n+1|0;f=f+2|0;if((a[f>>0]|0)==37)L=9;else break}while(0);v=n-x|0;if(M?(c[e>>2]&32|0)==0:0)ic(x,v,e)|0;if((n|0)!=(x|0)){n=v;x=f;continue}q=f+1|0;n=a[q>>0]|0;o=(n<<24>>24)+-48|0;if(o>>>0<10){K=(a[f+2>>0]|0)==36;q=K?f+3|0:q;n=a[q>>0]|0;t=K?o:-1;r=K?1:r}else t=-1;f=n<<24>>24;d:do if((f&-32|0)==32){o=0;do{if(!(1<<f+-32&75913))break d;o=1<<(n<<24>>24)+-32|o;q=q+1|0;n=a[q>>0]|0;f=n<<24>>24}while((f&-32|0)==32)}else o=0;while(0);do if(n<<24>>24==42){n=q+1|0;f=(a[n>>0]|0)+-48|0;if(f>>>0<10?(a[q+2>>0]|0)==36:0){c[l+(f<<2)>>2]=10;f=1;q=q+3|0;n=c[j+((a[n>>0]|0)+-48<<3)>>2]|0}else{if(r){m=-1;break a}if(!M){u=o;K=0;q=n;J=0;break}f=(c[g>>2]|0)+(4-1)&~(4-1);K=c[f>>2]|0;c[g>>2]=f+4;f=0;q=n;n=K}if((n|0)<0){u=o|8192;K=f;J=0-n|0}else{u=o;K=f;J=n}}else{f=(n<<24>>24)+-48|0;if(f>>>0<10){n=0;do{n=(n*10|0)+f|0;q=q+1|0;f=(a[q>>0]|0)+-48|0}while(f>>>0<10);if((n|0)<0){m=-1;break a}else{u=o;K=r;J=n}}else{u=o;K=r;J=0}}while(0);e:do if((a[q>>0]|0)==46){f=q+1|0;n=a[f>>0]|0;if(n<<24>>24!=42){o=(n<<24>>24)+-48|0;if(o>>>0<10)n=0;else{r=0;break}while(1){n=(n*10|0)+o|0;f=f+1|0;o=(a[f>>0]|0)+-48|0;if(o>>>0>=10){r=n;break e}}}f=q+2|0;n=(a[f>>0]|0)+-48|0;if(n>>>0<10?(a[q+3>>0]|0)==36:0){c[l+(n<<2)>>2]=10;r=c[j+((a[f>>0]|0)+-48<<3)>>2]|0;f=q+4|0;break}if(K){m=-1;break a}if(M){I=(c[g>>2]|0)+(4-1)&~(4-1);r=c[I>>2]|0;c[g>>2]=I+4}else r=0}else{r=-1;f=q}while(0);s=0;while(1){n=(a[f>>0]|0)+-65|0;if(n>>>0>57){m=-1;break a}I=f+1|0;n=a[4266+(s*58|0)+n>>0]|0;o=n&255;if((o+-1|0)>>>0<8){f=I;s=o}else break}if(!(n<<24>>24)){m=-1;break}q=(t|0)>-1;do if(n<<24>>24==19)if(q){m=-1;break a}else L=52;else{if(q){c[l+(t<<2)>>2]=o;G=j+(t<<3)|0;H=c[G+4>>2]|0;L=ca;c[L>>2]=c[G>>2];c[L+4>>2]=H;L=52;break}if(!M){m=0;break a}sc(ca,o,g)}while(0);if((L|0)==52?(L=0,!M):0){n=v;r=K;x=I;continue}t=a[f>>0]|0;t=(s|0)!=0&(t&15|0)==3?t&-33:t;o=u&-65537;H=(u&8192|0)==0?u:o;f:do switch(t|0){case 110:switch(s|0){case 0:{c[c[ca>>2]>>2]=m;n=v;r=K;x=I;continue a}case 1:{c[c[ca>>2]>>2]=m;n=v;r=K;x=I;continue a}case 2:{n=c[ca>>2]|0;c[n>>2]=m;c[n+4>>2]=((m|0)<0)<<31>>31;n=v;r=K;x=I;continue a}case 3:{b[c[ca>>2]>>1]=m;n=v;r=K;x=I;continue a}case 4:{a[c[ca>>2]>>0]=m;n=v;r=K;x=I;continue a}case 6:{c[c[ca>>2]>>2]=m;n=v;r=K;x=I;continue a}case 7:{n=c[ca>>2]|0;c[n>>2]=m;c[n+4>>2]=((m|0)<0)<<31>>31;n=v;r=K;x=I;continue a}default:{n=v;r=K;x=I;continue a}}case 112:{s=H|8;r=r>>>0>8?r:8;t=120;L=64;break}case 88:case 120:{s=H;L=64;break}case 111:{o=ca;n=c[o>>2]|0;o=c[o+4>>2]|0;if((n|0)==0&(o|0)==0)f=N;else{f=N;do{f=f+-1|0;a[f>>0]=n&7|48;n=Dc(n|0,o|0,3)|0;o=C}while(!((n|0)==0&(o|0)==0))}if(!(H&8)){n=H;s=0;q=4746;L=77}else{s=V-f|0;n=H;r=(r|0)>(s|0)?r:s+1|0;s=0;q=4746;L=77}break}case 105:case 100:{n=ca;f=c[n>>2]|0;n=c[n+4>>2]|0;if((n|0)<0){f=Cc(0,0,f|0,n|0)|0;n=C;o=ca;c[o>>2]=f;c[o+4>>2]=n;o=1;q=4746;L=76;break f}if(!(H&2048)){q=H&1;o=q;q=(q|0)==0?4746:4748;L=76}else{o=1;q=4747;L=76}break}case 117:{n=ca;f=c[n>>2]|0;n=c[n+4>>2]|0;o=0;q=4746;L=76;break}case 99:{a[W>>0]=c[ca>>2];f=W;t=1;v=0;u=4746;n=N;break}case 109:{n=dc(c[(ec()|0)>>2]|0)|0;L=82;break}case 115:{n=c[ca>>2]|0;n=(n|0)!=0?n:4756;L=82;break}case 67:{c[ha>>2]=c[ca>>2];c[X>>2]=0;c[ca>>2]=ha;f=ha;r=-1;L=86;break}case 83:{f=c[ca>>2]|0;if(!r){uc(e,32,J,0,H);f=0;L=97}else L=86;break}case 65:case 71:case 70:case 69:case 97:case 103:case 102:case 101:{p=+h[ca>>3];c[fa>>2]=0;h[k>>3]=p;if((c[k+4>>2]|0)>=0)if(!(H&2048)){G=H&1;F=G;G=(G|0)==0?4764:4769}else{F=1;G=4766}else{p=-p;F=1;G=4763}h[k>>3]=p;E=c[k+4>>2]&2146435072;do if(E>>>0<2146435072|(E|0)==2146435072&0<0){w=+oc(p,fa)*2.0;n=w!=0.0;if(n)c[fa>>2]=(c[fa>>2]|0)+-1;B=t|32;if((B|0)==97){u=t&32;x=(u|0)==0?G:G+9|0;v=F|2;f=12-r|0;do if(!(r>>>0>11|(f|0)==0)){p=8.0;do{f=f+-1|0;p=p*16.0}while((f|0)!=0);if((a[x>>0]|0)==45){p=-(p+(-w-p));break}else{p=w+p-p;break}}else p=w;while(0);n=c[fa>>2]|0;f=(n|0)<0?0-n|0:n;f=tc(f,((f|0)<0)<<31>>31,$)|0;if((f|0)==($|0)){a[aa>>0]=48;f=aa}a[f+-1>>0]=(n>>31&2)+43;s=f+-2|0;a[s>>0]=t+15;q=(r|0)<1;o=(H&8|0)==0;n=ea;while(1){G=~~p;f=n+1|0;a[n>>0]=d[4730+G>>0]|u;p=(p-+(G|0))*16.0;do if((f-Y|0)==1){if(o&(q&p==0.0))break;a[f>>0]=46;f=n+2|0}while(0);if(!(p!=0.0))break;else n=f}o=s;r=(r|0)!=0&(P+f|0)<(r|0)?Q+r-o|0:O-o+f|0;q=r+v|0;uc(e,32,J,q,H);if(!(c[e>>2]&32))ic(x,v,e)|0;uc(e,48,J,q,H^65536);n=f-Y|0;if(!(c[e>>2]&32))ic(ea,n,e)|0;f=ba-o|0;uc(e,48,r-(n+f)|0,0,0);if(!(c[e>>2]&32))ic(s,f,e)|0;uc(e,32,J,q,H^8192);f=(q|0)<(J|0)?J:q;break}f=(r|0)<0?6:r;if(n){n=(c[fa>>2]|0)+-28|0;c[fa>>2]=n;p=w*268435456.0}else{p=w;n=c[fa>>2]|0}E=(n|0)<0?da:R;D=E;o=E;do{A=~~p>>>0;c[o>>2]=A;o=o+4|0;p=(p-+(A>>>0))*1.0e9}while(p!=0.0);n=c[fa>>2]|0;if((n|0)>0){q=E;r=o;while(1){s=(n|0)>29?29:n;n=r+-4|0;do if(n>>>0>=q>>>0){o=0;do{z=Ec(c[n>>2]|0,0,s|0)|0;z=Gc(z|0,C|0,o|0,0)|0;A=C;y=Pc(z|0,A|0,1e9,0)|0;c[n>>2]=y;o=Oc(z|0,A|0,1e9,0)|0;n=n+-4|0}while(n>>>0>=q>>>0);if(!o)break;q=q+-4|0;c[q>>2]=o}while(0);o=r;while(1){if(o>>>0<=q>>>0)break;n=o+-4|0;if(!(c[n>>2]|0))o=n;else break}n=(c[fa>>2]|0)-s|0;c[fa>>2]=n;if((n|0)>0)r=o;else break}}else q=E;if((n|0)<0){x=((f+25|0)/9|0)+1|0;y=(B|0)==102;do{v=0-n|0;v=(v|0)>9?9:v;do if(q>>>0<o>>>0){n=(1<<v)+-1|0;r=1e9>>>v;u=0;s=q;do{A=c[s>>2]|0;c[s>>2]=(A>>>v)+u;u=_(A&n,r)|0;s=s+4|0}while(s>>>0<o>>>0);n=(c[q>>2]|0)==0?q+4|0:q;if(!u){q=n;n=o;break}c[o>>2]=u;q=n;n=o+4|0}else{q=(c[q>>2]|0)==0?q+4|0:q;n=o}while(0);o=y?E:q;o=(n-o>>2|0)>(x|0)?o+(x<<2)|0:n;n=(c[fa>>2]|0)+v|0;c[fa>>2]=n}while((n|0)<0);x=q;y=o}else{x=q;y=o}do if(x>>>0<y>>>0){n=(D-x>>2)*9|0;q=c[x>>2]|0;if(q>>>0<10)break;else o=10;do{o=o*10|0;n=n+1|0}while(q>>>0>=o>>>0)}else n=0;while(0);z=(B|0)==103;A=(f|0)!=0;o=f-((B|0)!=102?n:0)+((A&z)<<31>>31)|0;if((o|0)<(((y-D>>2)*9|0)+-9|0)){r=o+9216|0;o=E+4+(((r|0)/9|0)+-1024<<2)|0;r=((r|0)%9|0)+1|0;if((r|0)<9){q=10;do{q=q*10|0;r=r+1|0}while((r|0)!=9)}else q=10;u=c[o>>2]|0;v=(u>>>0)%(q>>>0)|0;r=(o+4|0)==(y|0);do if(r&(v|0)==0)q=x;else{w=(((u>>>0)/(q>>>0)|0)&1|0)==0?9007199254740992.0:9007199254740994.0;s=(q|0)/2|0;if(v>>>0<s>>>0)p=.5;else p=r&(v|0)==(s|0)?1.0:1.5;do if(F){if((a[G>>0]|0)!=45)break;w=-w;p=-p}while(0);r=u-v|0;c[o>>2]=r;if(!(w+p!=w)){q=x;break}B=r+q|0;c[o>>2]=B;if(B>>>0>999999999){n=x;while(1){q=o+-4|0;c[o>>2]=0;if(q>>>0<n>>>0){n=n+-4|0;c[n>>2]=0}B=(c[q>>2]|0)+1|0;c[q>>2]=B;if(B>>>0>999999999)o=q;else{s=n;o=q;break}}}else s=x;n=(D-s>>2)*9|0;r=c[s>>2]|0;if(r>>>0<10){q=s;break}else q=10;do{q=q*10|0;n=n+1|0}while(r>>>0>=q>>>0);q=s}while(0);o=o+4|0;x=q;o=y>>>0>o>>>0?o:y}else o=y;v=0-n|0;B=o;while(1){if(B>>>0<=x>>>0){y=0;break}o=B+-4|0;if(!(c[o>>2]|0))B=o;else{y=1;break}}do if(z){f=(A&1^1)+f|0;if((f|0)>(n|0)&(n|0)>-5){t=t+-1|0;f=f+-1-n|0}else{t=t+-2|0;f=f+-1|0}o=H&8;if(o)break;do if(y){o=c[B+-4>>2]|0;if(!o){q=9;break}if(!((o>>>0)%10|0)){r=10;q=0}else{q=0;break}do{r=r*10|0;q=q+1|0}while(((o>>>0)%(r>>>0)|0|0)==0)}else q=9;while(0);o=((B-D>>2)*9|0)+-9|0;if((t|32|0)==102){o=o-q|0;o=(o|0)<0?0:o;f=(f|0)<(o|0)?f:o;o=0;break}else{o=o+n-q|0;o=(o|0)<0?0:o;f=(f|0)<(o|0)?f:o;o=0;break}}else o=H&8;while(0);u=f|o;r=(u|0)!=0&1;s=(t|32|0)==102;if(s){n=(n|0)>0?n:0;t=0}else{q=(n|0)<0?v:n;q=tc(q,((q|0)<0)<<31>>31,$)|0;if((ba-q|0)<2)do{q=q+-1|0;a[q>>0]=48}while((ba-q|0)<2);a[q+-1>>0]=(n>>31&2)+43;D=q+-2|0;a[D>>0]=t;n=ba-D|0;t=D}v=F+1+f+r+n|0;uc(e,32,J,v,H);if(!(c[e>>2]&32))ic(G,F,e)|0;uc(e,48,J,v,H^65536);do if(s){q=x>>>0>E>>>0?E:x;o=q;do{n=tc(c[o>>2]|0,0,S)|0;do if((o|0)==(q|0)){if((n|0)!=(S|0))break;a[U>>0]=48;n=U}else{if(n>>>0<=ea>>>0)break;Bc(ea|0,48,n-Y|0)|0;do n=n+-1|0;while(n>>>0>ea>>>0)}while(0);if(!(c[e>>2]&32))ic(n,T-n|0,e)|0;o=o+4|0}while(o>>>0<=E>>>0);do if(u){if(c[e>>2]&32)break;ic(4798,1,e)|0}while(0);if((f|0)>0&o>>>0<B>>>0)while(1){n=tc(c[o>>2]|0,0,S)|0;if(n>>>0>ea>>>0){Bc(ea|0,48,n-Y|0)|0;do n=n+-1|0;while(n>>>0>ea>>>0)}if(!(c[e>>2]&32))ic(n,(f|0)>9?9:f,e)|0;o=o+4|0;n=f+-9|0;if(!((f|0)>9&o>>>0<B>>>0)){f=n;break}else f=n}uc(e,48,f+9|0,9,0)}else{s=y?B:x+4|0;if((f|0)>-1){r=(o|0)==0;q=x;do{n=tc(c[q>>2]|0,0,S)|0;if((n|0)==(S|0)){a[U>>0]=48;n=U}do if((q|0)==(x|0)){o=n+1|0;if(!(c[e>>2]&32))ic(n,1,e)|0;if(r&(f|0)<1){n=o;break}if(c[e>>2]&32){n=o;break}ic(4798,1,e)|0;n=o}else{if(n>>>0<=ea>>>0)break;Bc(ea|0,48,n+Z|0)|0;do n=n+-1|0;while(n>>>0>ea>>>0)}while(0);o=T-n|0;if(!(c[e>>2]&32))ic(n,(f|0)>(o|0)?o:f,e)|0;f=f-o|0;q=q+4|0}while(q>>>0<s>>>0&(f|0)>-1)}uc(e,48,f+18|0,18,0);if(c[e>>2]&32)break;ic(t,ba-t|0,e)|0}while(0);uc(e,32,J,v,H^8192);f=(v|0)<(J|0)?J:v}else{s=(t&32|0)!=0;r=p!=p|0.0!=0.0;n=r?0:F;q=n+3|0;uc(e,32,J,q,o);f=c[e>>2]|0;if(!(f&32)){ic(G,n,e)|0;f=c[e>>2]|0}if(!(f&32))ic(r?(s?4790:4794):s?4782:4786,3,e)|0;uc(e,32,J,q,H^8192);f=(q|0)<(J|0)?J:q}while(0);n=f;r=K;x=I;continue a}default:{f=x;o=H;t=r;v=0;u=4746;n=N}}while(0);g:do if((L|0)==64){o=ca;n=c[o>>2]|0;o=c[o+4>>2]|0;q=t&32;if(!((n|0)==0&(o|0)==0)){f=N;do{f=f+-1|0;a[f>>0]=d[4730+(n&15)>>0]|q;n=Dc(n|0,o|0,4)|0;o=C}while(!((n|0)==0&(o|0)==0));L=ca;if((s&8|0)==0|(c[L>>2]|0)==0&(c[L+4>>2]|0)==0){n=s;s=0;q=4746;L=77}else{n=s;s=2;q=4746+(t>>4)|0;L=77}}else{f=N;n=s;s=0;q=4746;L=77}}else if((L|0)==76){f=tc(f,n,N)|0;n=H;s=o;L=77}else if((L|0)==82){L=0;H=cc(n,0,r)|0;G=(H|0)==0;f=n;t=G?r:H-n|0;v=0;u=4746;n=G?n+r|0:H}else if((L|0)==86){L=0;o=0;n=0;s=f;while(1){q=c[s>>2]|0;if(!q)break;n=nc(ga,q)|0;if((n|0)<0|n>>>0>(r-o|0)>>>0)break;o=n+o|0;if(r>>>0>o>>>0)s=s+4|0;else break}if((n|0)<0){m=-1;break a}uc(e,32,J,o,H);if(!o){f=0;L=97}else{q=0;while(1){n=c[f>>2]|0;if(!n){f=o;L=97;break g}n=nc(ga,n)|0;q=n+q|0;if((q|0)>(o|0)){f=o;L=97;break g}if(!(c[e>>2]&32))ic(ga,n,e)|0;if(q>>>0>=o>>>0){f=o;L=97;break}else f=f+4|0}}}while(0);if((L|0)==97){L=0;uc(e,32,J,f,H^8192);n=(J|0)>(f|0)?J:f;r=K;x=I;continue}if((L|0)==77){L=0;o=(r|0)>-1?n&-65537:n;n=ca;n=(c[n>>2]|0)!=0|(c[n+4>>2]|0)!=0;if((r|0)!=0|n){t=(n&1^1)+(V-f)|0;t=(r|0)>(t|0)?r:t;v=s;u=q;n=N}else{f=N;t=0;v=s;u=q;n=N}}s=n-f|0;q=(t|0)<(s|0)?s:t;r=v+q|0;n=(J|0)<(r|0)?r:J;uc(e,32,n,r,o);if(!(c[e>>2]&32))ic(u,v,e)|0;uc(e,48,n,r,o^65536);uc(e,48,q,s,0);if(!(c[e>>2]&32))ic(f,s,e)|0;uc(e,32,n,r,o^8192);r=K;x=I}h:do if((L|0)==244)if(!e)if(!r)m=0;else{m=1;while(1){f=c[l+(m<<2)>>2]|0;if(!f){f=0;break}sc(j+(m<<3)|0,f,g);m=m+1|0;if((m|0)>=10){m=1;break h}}while(1){m=m+1|0;if(f){m=-1;break h}if((m|0)>=10){m=1;break h}f=c[l+(m<<2)>>2]|0}}while(0);i=ia;return m|0}function sc(a,b,d){a=a|0;b=b|0;d=d|0;var e=0,f=0,g=0.0;a:do if(b>>>0<=20)do switch(b|0){case 9:{e=(c[d>>2]|0)+(4-1)&~(4-1);b=c[e>>2]|0;c[d>>2]=e+4;c[a>>2]=b;break a}case 10:{e=(c[d>>2]|0)+(4-1)&~(4-1);b=c[e>>2]|0;c[d>>2]=e+4;e=a;c[e>>2]=b;c[e+4>>2]=((b|0)<0)<<31>>31;break a}case 11:{e=(c[d>>2]|0)+(4-1)&~(4-1);b=c[e>>2]|0;c[d>>2]=e+4;e=a;c[e>>2]=b;c[e+4>>2]=0;break a}case 12:{e=(c[d>>2]|0)+(8-1)&~(8-1);b=e;f=c[b>>2]|0;b=c[b+4>>2]|0;c[d>>2]=e+8;e=a;c[e>>2]=f;c[e+4>>2]=b;break a}case 13:{f=(c[d>>2]|0)+(4-1)&~(4-1);e=c[f>>2]|0;c[d>>2]=f+4;e=(e&65535)<<16>>16;f=a;c[f>>2]=e;c[f+4>>2]=((e|0)<0)<<31>>31;break a}case 14:{f=(c[d>>2]|0)+(4-1)&~(4-1);e=c[f>>2]|0;c[d>>2]=f+4;f=a;c[f>>2]=e&65535;c[f+4>>2]=0;break a}case 15:{f=(c[d>>2]|0)+(4-1)&~(4-1);e=c[f>>2]|0;c[d>>2]=f+4;e=(e&255)<<24>>24;f=a;c[f>>2]=e;c[f+4>>2]=((e|0)<0)<<31>>31;break a}case 16:{f=(c[d>>2]|0)+(4-1)&~(4-1);e=c[f>>2]|0;c[d>>2]=f+4;f=a;c[f>>2]=e&255;c[f+4>>2]=0;break a}case 17:{f=(c[d>>2]|0)+(8-1)&~(8-1);g=+h[f>>3];c[d>>2]=f+8;h[a>>3]=g;break a}case 18:{f=(c[d>>2]|0)+(8-1)&~(8-1);g=+h[f>>3];c[d>>2]=f+8;h[a>>3]=g;break a}default:break a}while(0);while(0);return}function tc(b,c,d){b=b|0;c=c|0;d=d|0;var e=0;if(c>>>0>0|(c|0)==0&b>>>0>4294967295)while(1){e=Pc(b|0,c|0,10,0)|0;d=d+-1|0;a[d>>0]=e|48;e=b;b=Oc(b|0,c|0,10,0)|0;if(!(c>>>0>9|(c|0)==9&e>>>0>4294967295))break;else c=C}if(b)while(1){d=d+-1|0;a[d>>0]=(b>>>0)%10|0|48;if(b>>>0<10)break;else b=(b>>>0)/10|0}return d|0}function uc(a,b,d,e,f){a=a|0;b=b|0;d=d|0;e=e|0;f=f|0;var g=0,h=0;h=i;i=i+256|0;g=h;do if((d|0)>(e|0)&(f&73728|0)==0){f=d-e|0;Bc(g|0,b|0,(f>>>0>256?256:f)|0)|0;e=c[a>>2]|0;d=(e&32|0)==0;if(f>>>0>255){b=f;do{if(d){ic(g,256,a)|0;e=c[a>>2]|0}b=b+-256|0;d=(e&32|0)==0}while(b>>>0>255);if(d)f=f&255;else break}else if(!d)break;ic(g,f,a)|0}while(0);i=h;return}function vc(a){a=a|0;var b=0,d=0,e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0,s=0,t=0,u=0,v=0,w=0,x=0,y=0,z=0,A=0,B=0,C=0,D=0,E=0,F=0,G=0;do if(a>>>0<245){o=a>>>0<11?16:a+11&-8;a=o>>>3;j=c[1212]|0;b=j>>>a;if(b&3){b=(b&1^1)+a|0;d=4888+(b<<1<<2)|0;e=d+8|0;f=c[e>>2]|0;g=f+8|0;h=c[g>>2]|0;do if((d|0)!=(h|0)){if(h>>>0<(c[1216]|0)>>>0)ga();a=h+12|0;if((c[a>>2]|0)==(f|0)){c[a>>2]=d;c[e>>2]=h;break}else ga()}else c[1212]=j&~(1<<b);while(0);G=b<<3;c[f+4>>2]=G|3;G=f+G+4|0;c[G>>2]=c[G>>2]|1;G=g;return G|0}h=c[1214]|0;if(o>>>0>h>>>0){if(b){d=2<<a;d=b<<a&(d|0-d);d=(d&0-d)+-1|0;i=d>>>12&16;d=d>>>i;f=d>>>5&8;d=d>>>f;g=d>>>2&4;d=d>>>g;e=d>>>1&2;d=d>>>e;b=d>>>1&1;b=(f|i|g|e|b)+(d>>>b)|0;d=4888+(b<<1<<2)|0;e=d+8|0;g=c[e>>2]|0;i=g+8|0;f=c[i>>2]|0;do if((d|0)!=(f|0)){if(f>>>0<(c[1216]|0)>>>0)ga();a=f+12|0;if((c[a>>2]|0)==(g|0)){c[a>>2]=d;c[e>>2]=f;k=c[1214]|0;break}else ga()}else{c[1212]=j&~(1<<b);k=h}while(0);h=(b<<3)-o|0;c[g+4>>2]=o|3;e=g+o|0;c[e+4>>2]=h|1;c[e+h>>2]=h;if(k){f=c[1217]|0;b=k>>>3;d=4888+(b<<1<<2)|0;a=c[1212]|0;b=1<<b;if(a&b){a=d+8|0;b=c[a>>2]|0;if(b>>>0<(c[1216]|0)>>>0)ga();else{l=a;m=b}}else{c[1212]=a|b;l=d+8|0;m=d}c[l>>2]=f;c[m+12>>2]=f;c[f+8>>2]=m;c[f+12>>2]=d}c[1214]=h;c[1217]=e;G=i;return G|0}a=c[1213]|0;if(a){i=(a&0-a)+-1|0;F=i>>>12&16;i=i>>>F;E=i>>>5&8;i=i>>>E;G=i>>>2&4;i=i>>>G;b=i>>>1&2;i=i>>>b;j=i>>>1&1;j=c[5152+((E|F|G|b|j)+(i>>>j)<<2)>>2]|0;i=(c[j+4>>2]&-8)-o|0;b=j;while(1){a=c[b+16>>2]|0;if(!a){a=c[b+20>>2]|0;if(!a)break}b=(c[a+4>>2]&-8)-o|0;G=b>>>0<i>>>0;i=G?b:i;b=a;j=G?a:j}f=c[1216]|0;if(j>>>0<f>>>0)ga();h=j+o|0;if(j>>>0>=h>>>0)ga();g=c[j+24>>2]|0;d=c[j+12>>2]|0;do if((d|0)==(j|0)){b=j+20|0;a=c[b>>2]|0;if(!a){b=j+16|0;a=c[b>>2]|0;if(!a){n=0;break}}while(1){d=a+20|0;e=c[d>>2]|0;if(e){a=e;b=d;continue}d=a+16|0;e=c[d>>2]|0;if(!e)break;else{a=e;b=d}}if(b>>>0<f>>>0)ga();else{c[b>>2]=0;n=a;break}}else{e=c[j+8>>2]|0;if(e>>>0<f>>>0)ga();a=e+12|0;if((c[a>>2]|0)!=(j|0))ga();b=d+8|0;if((c[b>>2]|0)==(j|0)){c[a>>2]=d;c[b>>2]=e;n=d;break}else ga()}while(0);do if(g){a=c[j+28>>2]|0;b=5152+(a<<2)|0;if((j|0)==(c[b>>2]|0)){c[b>>2]=n;if(!n){c[1213]=c[1213]&~(1<<a);break}}else{if(g>>>0<(c[1216]|0)>>>0)ga();a=g+16|0;if((c[a>>2]|0)==(j|0))c[a>>2]=n;else c[g+20>>2]=n;if(!n)break}b=c[1216]|0;if(n>>>0<b>>>0)ga();c[n+24>>2]=g;a=c[j+16>>2]|0;do if(a)if(a>>>0<b>>>0)ga();else{c[n+16>>2]=a;c[a+24>>2]=n;break}while(0);a=c[j+20>>2]|0;if(a)if(a>>>0<(c[1216]|0)>>>0)ga();else{c[n+20>>2]=a;c[a+24>>2]=n;break}}while(0);if(i>>>0<16){G=i+o|0;c[j+4>>2]=G|3;G=j+G+4|0;c[G>>2]=c[G>>2]|1}else{c[j+4>>2]=o|3;c[h+4>>2]=i|1;c[h+i>>2]=i;a=c[1214]|0;if(a){e=c[1217]|0;b=a>>>3;d=4888+(b<<1<<2)|0;a=c[1212]|0;b=1<<b;if(a&b){a=d+8|0;b=c[a>>2]|0;if(b>>>0<(c[1216]|0)>>>0)ga();else{p=a;q=b}}else{c[1212]=a|b;p=d+8|0;q=d}c[p>>2]=e;c[q+12>>2]=e;c[e+8>>2]=q;c[e+12>>2]=d}c[1214]=i;c[1217]=h}G=j+8|0;return G|0}}}else if(a>>>0<=4294967231){a=a+11|0;o=a&-8;k=c[1213]|0;if(k){d=0-o|0;a=a>>>8;if(a)if(o>>>0>16777215)j=31;else{q=(a+1048320|0)>>>16&8;z=a<<q;p=(z+520192|0)>>>16&4;z=z<<p;j=(z+245760|0)>>>16&2;j=14-(p|q|j)+(z<<j>>>15)|0;j=o>>>(j+7|0)&1|j<<1}else j=0;b=c[5152+(j<<2)>>2]|0;a:do if(!b){a=0;b=0;z=86}else{f=d;a=0;h=o<<((j|0)==31?0:25-(j>>>1)|0);i=b;b=0;while(1){e=c[i+4>>2]&-8;d=e-o|0;if(d>>>0<f>>>0)if((e|0)==(o|0)){a=i;b=i;z=90;break a}else b=i;else d=f;e=c[i+20>>2]|0;i=c[i+16+(h>>>31<<2)>>2]|0;a=(e|0)==0|(e|0)==(i|0)?a:e;e=(i|0)==0;if(e){z=86;break}else{f=d;h=h<<(e&1^1)}}}while(0);if((z|0)==86){if((a|0)==0&(b|0)==0){a=2<<j;a=k&(a|0-a);if(!a)break;q=(a&0-a)+-1|0;m=q>>>12&16;q=q>>>m;l=q>>>5&8;q=q>>>l;n=q>>>2&4;q=q>>>n;p=q>>>1&2;q=q>>>p;a=q>>>1&1;a=c[5152+((l|m|n|p|a)+(q>>>a)<<2)>>2]|0}if(!a){i=d;j=b}else z=90}if((z|0)==90)while(1){z=0;q=(c[a+4>>2]&-8)-o|0;e=q>>>0<d>>>0;d=e?q:d;b=e?a:b;e=c[a+16>>2]|0;if(e){a=e;z=90;continue}a=c[a+20>>2]|0;if(!a){i=d;j=b;break}else z=90}if((j|0)!=0?i>>>0<((c[1214]|0)-o|0)>>>0:0){f=c[1216]|0;if(j>>>0<f>>>0)ga();h=j+o|0;if(j>>>0>=h>>>0)ga();g=c[j+24>>2]|0;d=c[j+12>>2]|0;do if((d|0)==(j|0)){b=j+20|0;a=c[b>>2]|0;if(!a){b=j+16|0;a=c[b>>2]|0;if(!a){s=0;break}}while(1){d=a+20|0;e=c[d>>2]|0;if(e){a=e;b=d;continue}d=a+16|0;e=c[d>>2]|0;if(!e)break;else{a=e;b=d}}if(b>>>0<f>>>0)ga();else{c[b>>2]=0;s=a;break}}else{e=c[j+8>>2]|0;if(e>>>0<f>>>0)ga();a=e+12|0;if((c[a>>2]|0)!=(j|0))ga();b=d+8|0;if((c[b>>2]|0)==(j|0)){c[a>>2]=d;c[b>>2]=e;s=d;break}else ga()}while(0);do if(g){a=c[j+28>>2]|0;b=5152+(a<<2)|0;if((j|0)==(c[b>>2]|0)){c[b>>2]=s;if(!s){c[1213]=c[1213]&~(1<<a);break}}else{if(g>>>0<(c[1216]|0)>>>0)ga();a=g+16|0;if((c[a>>2]|0)==(j|0))c[a>>2]=s;else c[g+20>>2]=s;if(!s)break}b=c[1216]|0;if(s>>>0<b>>>0)ga();c[s+24>>2]=g;a=c[j+16>>2]|0;do if(a)if(a>>>0<b>>>0)ga();else{c[s+16>>2]=a;c[a+24>>2]=s;break}while(0);a=c[j+20>>2]|0;if(a)if(a>>>0<(c[1216]|0)>>>0)ga();else{c[s+20>>2]=a;c[a+24>>2]=s;break}}while(0);do if(i>>>0>=16){c[j+4>>2]=o|3;c[h+4>>2]=i|1;c[h+i>>2]=i;a=i>>>3;if(i>>>0<256){d=4888+(a<<1<<2)|0;b=c[1212]|0;a=1<<a;if(b&a){a=d+8|0;b=c[a>>2]|0;if(b>>>0<(c[1216]|0)>>>0)ga();else{t=a;v=b}}else{c[1212]=b|a;t=d+8|0;v=d}c[t>>2]=h;c[v+12>>2]=h;c[h+8>>2]=v;c[h+12>>2]=d;break}a=i>>>8;if(a)if(i>>>0>16777215)d=31;else{F=(a+1048320|0)>>>16&8;G=a<<F;E=(G+520192|0)>>>16&4;G=G<<E;d=(G+245760|0)>>>16&2;d=14-(E|F|d)+(G<<d>>>15)|0;d=i>>>(d+7|0)&1|d<<1}else d=0;e=5152+(d<<2)|0;c[h+28>>2]=d;a=h+16|0;c[a+4>>2]=0;c[a>>2]=0;a=c[1213]|0;b=1<<d;if(!(a&b)){c[1213]=a|b;c[e>>2]=h;c[h+24>>2]=e;c[h+12>>2]=h;c[h+8>>2]=h;break}d=i<<((d|0)==31?0:25-(d>>>1)|0);e=c[e>>2]|0;while(1){if((c[e+4>>2]&-8|0)==(i|0)){z=148;break}b=e+16+(d>>>31<<2)|0;a=c[b>>2]|0;if(!a){z=145;break}else{d=d<<1;e=a}}if((z|0)==145)if(b>>>0<(c[1216]|0)>>>0)ga();else{c[b>>2]=h;c[h+24>>2]=e;c[h+12>>2]=h;c[h+8>>2]=h;break}else if((z|0)==148){a=e+8|0;b=c[a>>2]|0;G=c[1216]|0;if(b>>>0>=G>>>0&e>>>0>=G>>>0){c[b+12>>2]=h;c[a>>2]=h;c[h+8>>2]=b;c[h+12>>2]=e;c[h+24>>2]=0;break}else ga()}}else{G=i+o|0;c[j+4>>2]=G|3;G=j+G+4|0;c[G>>2]=c[G>>2]|1}while(0);G=j+8|0;return G|0}}}else o=-1;while(0);d=c[1214]|0;if(d>>>0>=o>>>0){a=d-o|0;b=c[1217]|0;if(a>>>0>15){G=b+o|0;c[1217]=G;c[1214]=a;c[G+4>>2]=a|1;c[G+a>>2]=a;c[b+4>>2]=o|3}else{c[1214]=0;c[1217]=0;c[b+4>>2]=d|3;G=b+d+4|0;c[G>>2]=c[G>>2]|1}G=b+8|0;return G|0}a=c[1215]|0;if(a>>>0>o>>>0){E=a-o|0;c[1215]=E;G=c[1218]|0;F=G+o|0;c[1218]=F;c[F+4>>2]=E|1;c[G+4>>2]=o|3;G=G+8|0;return G|0}do if(!(c[1330]|0)){a=ea(30)|0;if(!(a+-1&a)){c[1332]=a;c[1331]=a;c[1333]=-1;c[1334]=-1;c[1335]=0;c[1323]=0;c[1330]=(ja(0)|0)&-16^1431655768;break}else ga()}while(0);h=o+48|0;e=c[1332]|0;i=o+47|0;d=e+i|0;e=0-e|0;j=d&e;if(j>>>0<=o>>>0){G=0;return G|0}a=c[1322]|0;if((a|0)!=0?(t=c[1320]|0,v=t+j|0,v>>>0<=t>>>0|v>>>0>a>>>0):0){G=0;return G|0}b:do if(!(c[1323]&4)){b=c[1218]|0;c:do if(b){f=5296;while(1){a=c[f>>2]|0;if(a>>>0<=b>>>0?(r=f+4|0,(a+(c[r>>2]|0)|0)>>>0>b>>>0):0)break;a=c[f+8>>2]|0;if(!a){z=173;break c}else f=a}a=d-(c[1215]|0)&e;if(a>>>0<2147483647){b=ia(a|0)|0;if((b|0)==((c[f>>2]|0)+(c[r>>2]|0)|0)){if((b|0)!=(-1|0)){h=b;g=a;z=193;break b}}else z=183}}else z=173;while(0);do if((z|0)==173?(u=ia(0)|0,(u|0)!=(-1|0)):0){a=u;b=c[1331]|0;d=b+-1|0;if(!(d&a))a=j;else a=j-a+(d+a&0-b)|0;b=c[1320]|0;d=b+a|0;if(a>>>0>o>>>0&a>>>0<2147483647){v=c[1322]|0;if((v|0)!=0?d>>>0<=b>>>0|d>>>0>v>>>0:0)break;b=ia(a|0)|0;if((b|0)==(u|0)){h=u;g=a;z=193;break b}else z=183}}while(0);d:do if((z|0)==183){d=0-a|0;do if(h>>>0>a>>>0&(a>>>0<2147483647&(b|0)!=(-1|0))?(w=c[1332]|0,w=i-a+w&0-w,w>>>0<2147483647):0)if((ia(w|0)|0)==(-1|0)){ia(d|0)|0;break d}else{a=w+a|0;break}while(0);if((b|0)!=(-1|0)){h=b;g=a;z=193;break b}}while(0);c[1323]=c[1323]|4;z=190}else z=190;while(0);if((((z|0)==190?j>>>0<2147483647:0)?(x=ia(j|0)|0,y=ia(0)|0,x>>>0<y>>>0&((x|0)!=(-1|0)&(y|0)!=(-1|0))):0)?(g=y-x|0,g>>>0>(o+40|0)>>>0):0){h=x;z=193}if((z|0)==193){a=(c[1320]|0)+g|0;c[1320]=a;if(a>>>0>(c[1321]|0)>>>0)c[1321]=a;k=c[1218]|0;do if(k){f=5296;while(1){a=c[f>>2]|0;b=f+4|0;d=c[b>>2]|0;if((h|0)==(a+d|0)){z=203;break}e=c[f+8>>2]|0;if(!e)break;else f=e}if(((z|0)==203?(c[f+12>>2]&8|0)==0:0)?k>>>0<h>>>0&k>>>0>=a>>>0:0){c[b>>2]=d+g;G=k+8|0;G=(G&7|0)==0?0:0-G&7;F=k+G|0;G=g-G+(c[1215]|0)|0;c[1218]=F;c[1215]=G;c[F+4>>2]=G|1;c[F+G+4>>2]=40;c[1219]=c[1334];break}a=c[1216]|0;if(h>>>0<a>>>0){c[1216]=h;i=h}else i=a;b=h+g|0;a=5296;while(1){if((c[a>>2]|0)==(b|0)){z=211;break}a=c[a+8>>2]|0;if(!a){b=5296;break}}if((z|0)==211)if(!(c[a+12>>2]&8)){c[a>>2]=h;m=a+4|0;c[m>>2]=(c[m>>2]|0)+g;m=h+8|0;m=h+((m&7|0)==0?0:0-m&7)|0;a=b+8|0;a=b+((a&7|0)==0?0:0-a&7)|0;l=m+o|0;j=a-m-o|0;c[m+4>>2]=o|3;do if((a|0)!=(k|0)){if((a|0)==(c[1217]|0)){G=(c[1214]|0)+j|0;c[1214]=G;c[1217]=l;c[l+4>>2]=G|1;c[l+G>>2]=G;break}b=c[a+4>>2]|0;if((b&3|0)==1){h=b&-8;f=b>>>3;e:do if(b>>>0>=256){g=c[a+24>>2]|0;e=c[a+12>>2]|0;do if((e|0)==(a|0)){e=a+16|0;d=e+4|0;b=c[d>>2]|0;if(!b){b=c[e>>2]|0;if(!b){E=0;break}else d=e}while(1){e=b+20|0;f=c[e>>2]|0;if(f){b=f;d=e;continue}e=b+16|0;f=c[e>>2]|0;if(!f)break;else{b=f;d=e}}if(d>>>0<i>>>0)ga();else{c[d>>2]=0;E=b;break}}else{f=c[a+8>>2]|0;if(f>>>0<i>>>0)ga();b=f+12|0;if((c[b>>2]|0)!=(a|0))ga();d=e+8|0;if((c[d>>2]|0)==(a|0)){c[b>>2]=e;c[d>>2]=f;E=e;break}else ga()}while(0);if(!g)break;b=c[a+28>>2]|0;d=5152+(b<<2)|0;do if((a|0)!=(c[d>>2]|0)){if(g>>>0<(c[1216]|0)>>>0)ga();b=g+16|0;if((c[b>>2]|0)==(a|0))c[b>>2]=E;else c[g+20>>2]=E;if(!E)break e}else{c[d>>2]=E;if(E)break;c[1213]=c[1213]&~(1<<b);break e}while(0);e=c[1216]|0;if(E>>>0<e>>>0)ga();c[E+24>>2]=g;b=a+16|0;d=c[b>>2]|0;do if(d)if(d>>>0<e>>>0)ga();else{c[E+16>>2]=d;c[d+24>>2]=E;break}while(0);b=c[b+4>>2]|0;if(!b)break;if(b>>>0<(c[1216]|0)>>>0)ga();else{c[E+20>>2]=b;c[b+24>>2]=E;break}}else{d=c[a+8>>2]|0;e=c[a+12>>2]|0;b=4888+(f<<1<<2)|0;do if((d|0)!=(b|0)){if(d>>>0<i>>>0)ga();if((c[d+12>>2]|0)==(a|0))break;ga()}while(0);if((e|0)==(d|0)){c[1212]=c[1212]&~(1<<f);break}do if((e|0)==(b|0))B=e+8|0;else{if(e>>>0<i>>>0)ga();b=e+8|0;if((c[b>>2]|0)==(a|0)){B=b;break}ga()}while(0);c[d+12>>2]=e;c[B>>2]=d}while(0);a=a+h|0;f=h+j|0}else f=j;a=a+4|0;c[a>>2]=c[a>>2]&-2;c[l+4>>2]=f|1;c[l+f>>2]=f;a=f>>>3;if(f>>>0<256){d=4888+(a<<1<<2)|0;b=c[1212]|0;a=1<<a;do if(!(b&a)){c[1212]=b|a;F=d+8|0;G=d}else{a=d+8|0;b=c[a>>2]|0;if(b>>>0>=(c[1216]|0)>>>0){F=a;G=b;break}ga()}while(0);c[F>>2]=l;c[G+12>>2]=l;c[l+8>>2]=G;c[l+12>>2]=d;break}a=f>>>8;do if(!a)d=0;else{if(f>>>0>16777215){d=31;break}F=(a+1048320|0)>>>16&8;G=a<<F;E=(G+520192|0)>>>16&4;G=G<<E;d=(G+245760|0)>>>16&2;d=14-(E|F|d)+(G<<d>>>15)|0;d=f>>>(d+7|0)&1|d<<1}while(0);e=5152+(d<<2)|0;c[l+28>>2]=d;a=l+16|0;c[a+4>>2]=0;c[a>>2]=0;a=c[1213]|0;b=1<<d;if(!(a&b)){c[1213]=a|b;c[e>>2]=l;c[l+24>>2]=e;c[l+12>>2]=l;c[l+8>>2]=l;break}d=f<<((d|0)==31?0:25-(d>>>1)|0);e=c[e>>2]|0;while(1){if((c[e+4>>2]&-8|0)==(f|0)){z=281;break}b=e+16+(d>>>31<<2)|0;a=c[b>>2]|0;if(!a){z=278;break}else{d=d<<1;e=a}}if((z|0)==278)if(b>>>0<(c[1216]|0)>>>0)ga();else{c[b>>2]=l;c[l+24>>2]=e;c[l+12>>2]=l;c[l+8>>2]=l;break}else if((z|0)==281){a=e+8|0;b=c[a>>2]|0;G=c[1216]|0;if(b>>>0>=G>>>0&e>>>0>=G>>>0){c[b+12>>2]=l;c[a>>2]=l;c[l+8>>2]=b;c[l+12>>2]=e;c[l+24>>2]=0;break}else ga()}}else{G=(c[1215]|0)+j|0;c[1215]=G;c[1218]=l;c[l+4>>2]=G|1}while(0);G=m+8|0;return G|0}else b=5296;while(1){a=c[b>>2]|0;if(a>>>0<=k>>>0?(A=a+(c[b+4>>2]|0)|0,A>>>0>k>>>0):0)break;b=c[b+8>>2]|0}f=A+-47|0;b=f+8|0;b=f+((b&7|0)==0?0:0-b&7)|0;f=k+16|0;b=b>>>0<f>>>0?k:b;a=b+8|0;d=h+8|0;d=(d&7|0)==0?0:0-d&7;G=h+d|0;d=g+-40-d|0;c[1218]=G;c[1215]=d;c[G+4>>2]=d|1;c[G+d+4>>2]=40;c[1219]=c[1334];d=b+4|0;c[d>>2]=27;c[a>>2]=c[1324];c[a+4>>2]=c[1325];c[a+8>>2]=c[1326];c[a+12>>2]=c[1327];c[1324]=h;c[1325]=g;c[1327]=0;c[1326]=a;a=b+24|0;do{a=a+4|0;c[a>>2]=7}while((a+4|0)>>>0<A>>>0);if((b|0)!=(k|0)){g=b-k|0;c[d>>2]=c[d>>2]&-2;c[k+4>>2]=g|1;c[b>>2]=g;a=g>>>3;if(g>>>0<256){d=4888+(a<<1<<2)|0;b=c[1212]|0;a=1<<a;if(b&a){a=d+8|0;b=c[a>>2]|0;if(b>>>0<(c[1216]|0)>>>0)ga();else{C=a;D=b}}else{c[1212]=b|a;C=d+8|0;D=d}c[C>>2]=k;c[D+12>>2]=k;c[k+8>>2]=D;c[k+12>>2]=d;break}a=g>>>8;if(a)if(g>>>0>16777215)d=31;else{F=(a+1048320|0)>>>16&8;G=a<<F;E=(G+520192|0)>>>16&4;G=G<<E;d=(G+245760|0)>>>16&2;d=14-(E|F|d)+(G<<d>>>15)|0;d=g>>>(d+7|0)&1|d<<1}else d=0;e=5152+(d<<2)|0;c[k+28>>2]=d;c[k+20>>2]=0;c[f>>2]=0;a=c[1213]|0;b=1<<d;if(!(a&b)){c[1213]=a|b;c[e>>2]=k;c[k+24>>2]=e;c[k+12>>2]=k;c[k+8>>2]=k;break}d=g<<((d|0)==31?0:25-(d>>>1)|0);e=c[e>>2]|0;while(1){if((c[e+4>>2]&-8|0)==(g|0)){z=307;break}b=e+16+(d>>>31<<2)|0;a=c[b>>2]|0;if(!a){z=304;break}else{d=d<<1;e=a}}if((z|0)==304)if(b>>>0<(c[1216]|0)>>>0)ga();else{c[b>>2]=k;c[k+24>>2]=e;c[k+12>>2]=k;c[k+8>>2]=k;break}else if((z|0)==307){a=e+8|0;b=c[a>>2]|0;G=c[1216]|0;if(b>>>0>=G>>>0&e>>>0>=G>>>0){c[b+12>>2]=k;c[a>>2]=k;c[k+8>>2]=b;c[k+12>>2]=e;c[k+24>>2]=0;break}else ga()}}}else{G=c[1216]|0;if((G|0)==0|h>>>0<G>>>0)c[1216]=h;c[1324]=h;c[1325]=g;c[1327]=0;c[1221]=c[1330];c[1220]=-1;a=0;do{G=4888+(a<<1<<2)|0;c[G+12>>2]=G;c[G+8>>2]=G;a=a+1|0}while((a|0)!=32);G=h+8|0;G=(G&7|0)==0?0:0-G&7;F=h+G|0;G=g+-40-G|0;c[1218]=F;c[1215]=G;c[F+4>>2]=G|1;c[F+G+4>>2]=40;c[1219]=c[1334]}while(0);a=c[1215]|0;if(a>>>0>o>>>0){E=a-o|0;c[1215]=E;G=c[1218]|0;F=G+o|0;c[1218]=F;c[F+4>>2]=E|1;c[G+4>>2]=o|3;G=G+8|0;return G|0}}c[(ec()|0)>>2]=12;G=0;return G|0}function wc(a){a=a|0;var b=0,d=0,e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0;if(!a)return;d=a+-8|0;h=c[1216]|0;if(d>>>0<h>>>0)ga();a=c[a+-4>>2]|0;b=a&3;if((b|0)==1)ga();e=a&-8;m=d+e|0;do if(!(a&1)){a=c[d>>2]|0;if(!b)return;k=d+(0-a)|0;j=a+e|0;if(k>>>0<h>>>0)ga();if((k|0)==(c[1217]|0)){a=m+4|0;b=c[a>>2]|0;if((b&3|0)!=3){q=k;f=j;break}c[1214]=j;c[a>>2]=b&-2;c[k+4>>2]=j|1;c[k+j>>2]=j;return}e=a>>>3;if(a>>>0<256){b=c[k+8>>2]|0;d=c[k+12>>2]|0;a=4888+(e<<1<<2)|0;if((b|0)!=(a|0)){if(b>>>0<h>>>0)ga();if((c[b+12>>2]|0)!=(k|0))ga()}if((d|0)==(b|0)){c[1212]=c[1212]&~(1<<e);q=k;f=j;break}if((d|0)!=(a|0)){if(d>>>0<h>>>0)ga();a=d+8|0;if((c[a>>2]|0)==(k|0))g=a;else ga()}else g=d+8|0;c[b+12>>2]=d;c[g>>2]=b;q=k;f=j;break}g=c[k+24>>2]|0;d=c[k+12>>2]|0;do if((d|0)==(k|0)){d=k+16|0;b=d+4|0;a=c[b>>2]|0;if(!a){a=c[d>>2]|0;if(!a){i=0;break}else b=d}while(1){d=a+20|0;e=c[d>>2]|0;if(e){a=e;b=d;continue}d=a+16|0;e=c[d>>2]|0;if(!e)break;else{a=e;b=d}}if(b>>>0<h>>>0)ga();else{c[b>>2]=0;i=a;break}}else{e=c[k+8>>2]|0;if(e>>>0<h>>>0)ga();a=e+12|0;if((c[a>>2]|0)!=(k|0))ga();b=d+8|0;if((c[b>>2]|0)==(k|0)){c[a>>2]=d;c[b>>2]=e;i=d;break}else ga()}while(0);if(g){a=c[k+28>>2]|0;b=5152+(a<<2)|0;if((k|0)==(c[b>>2]|0)){c[b>>2]=i;if(!i){c[1213]=c[1213]&~(1<<a);q=k;f=j;break}}else{if(g>>>0<(c[1216]|0)>>>0)ga();a=g+16|0;if((c[a>>2]|0)==(k|0))c[a>>2]=i;else c[g+20>>2]=i;if(!i){q=k;f=j;break}}d=c[1216]|0;if(i>>>0<d>>>0)ga();c[i+24>>2]=g;a=k+16|0;b=c[a>>2]|0;do if(b)if(b>>>0<d>>>0)ga();else{c[i+16>>2]=b;c[b+24>>2]=i;break}while(0);a=c[a+4>>2]|0;if(a)if(a>>>0<(c[1216]|0)>>>0)ga();else{c[i+20>>2]=a;c[a+24>>2]=i;q=k;f=j;break}else{q=k;f=j}}else{q=k;f=j}}else{q=d;f=e}while(0);if(q>>>0>=m>>>0)ga();a=m+4|0;b=c[a>>2]|0;if(!(b&1))ga();if(!(b&2)){if((m|0)==(c[1218]|0)){p=(c[1215]|0)+f|0;c[1215]=p;c[1218]=q;c[q+4>>2]=p|1;if((q|0)!=(c[1217]|0))return;c[1217]=0;c[1214]=0;return}if((m|0)==(c[1217]|0)){p=(c[1214]|0)+f|0;c[1214]=p;c[1217]=q;c[q+4>>2]=p|1;c[q+p>>2]=p;return}f=(b&-8)+f|0;e=b>>>3;do if(b>>>0>=256){g=c[m+24>>2]|0;a=c[m+12>>2]|0;do if((a|0)==(m|0)){d=m+16|0;b=d+4|0;a=c[b>>2]|0;if(!a){a=c[d>>2]|0;if(!a){n=0;break}else b=d}while(1){d=a+20|0;e=c[d>>2]|0;if(e){a=e;b=d;continue}d=a+16|0;e=c[d>>2]|0;if(!e)break;else{a=e;b=d}}if(b>>>0<(c[1216]|0)>>>0)ga();else{c[b>>2]=0;n=a;break}}else{b=c[m+8>>2]|0;if(b>>>0<(c[1216]|0)>>>0)ga();d=b+12|0;if((c[d>>2]|0)!=(m|0))ga();e=a+8|0;if((c[e>>2]|0)==(m|0)){c[d>>2]=a;c[e>>2]=b;n=a;break}else ga()}while(0);if(g){a=c[m+28>>2]|0;b=5152+(a<<2)|0;if((m|0)==(c[b>>2]|0)){c[b>>2]=n;if(!n){c[1213]=c[1213]&~(1<<a);break}}else{if(g>>>0<(c[1216]|0)>>>0)ga();a=g+16|0;if((c[a>>2]|0)==(m|0))c[a>>2]=n;else c[g+20>>2]=n;if(!n)break}d=c[1216]|0;if(n>>>0<d>>>0)ga();c[n+24>>2]=g;a=m+16|0;b=c[a>>2]|0;do if(b)if(b>>>0<d>>>0)ga();else{c[n+16>>2]=b;c[b+24>>2]=n;break}while(0);a=c[a+4>>2]|0;if(a)if(a>>>0<(c[1216]|0)>>>0)ga();else{c[n+20>>2]=a;c[a+24>>2]=n;break}}}else{b=c[m+8>>2]|0;d=c[m+12>>2]|0;a=4888+(e<<1<<2)|0;if((b|0)!=(a|0)){if(b>>>0<(c[1216]|0)>>>0)ga();if((c[b+12>>2]|0)!=(m|0))ga()}if((d|0)==(b|0)){c[1212]=c[1212]&~(1<<e);break}if((d|0)!=(a|0)){if(d>>>0<(c[1216]|0)>>>0)ga();a=d+8|0;if((c[a>>2]|0)==(m|0))l=a;else ga()}else l=d+8|0;c[b+12>>2]=d;c[l>>2]=b}while(0);c[q+4>>2]=f|1;c[q+f>>2]=f;if((q|0)==(c[1217]|0)){c[1214]=f;return}}else{c[a>>2]=b&-2;c[q+4>>2]=f|1;c[q+f>>2]=f}a=f>>>3;if(f>>>0<256){d=4888+(a<<1<<2)|0;b=c[1212]|0;a=1<<a;if(b&a){a=d+8|0;b=c[a>>2]|0;if(b>>>0<(c[1216]|0)>>>0)ga();else{o=a;p=b}}else{c[1212]=b|a;o=d+8|0;p=d}c[o>>2]=q;c[p+12>>2]=q;c[q+8>>2]=p;c[q+12>>2]=d;return}a=f>>>8;if(a)if(f>>>0>16777215)d=31;else{o=(a+1048320|0)>>>16&8;p=a<<o;n=(p+520192|0)>>>16&4;p=p<<n;d=(p+245760|0)>>>16&2;d=14-(n|o|d)+(p<<d>>>15)|0;d=f>>>(d+7|0)&1|d<<1}else d=0;e=5152+(d<<2)|0;c[q+28>>2]=d;c[q+20>>2]=0;c[q+16>>2]=0;a=c[1213]|0;b=1<<d;do if(a&b){d=f<<((d|0)==31?0:25-(d>>>1)|0);e=c[e>>2]|0;while(1){if((c[e+4>>2]&-8|0)==(f|0)){a=130;break}b=e+16+(d>>>31<<2)|0;a=c[b>>2]|0;if(!a){a=127;break}else{d=d<<1;e=a}}if((a|0)==127)if(b>>>0<(c[1216]|0)>>>0)ga();else{c[b>>2]=q;c[q+24>>2]=e;c[q+12>>2]=q;c[q+8>>2]=q;break}else if((a|0)==130){a=e+8|0;b=c[a>>2]|0;p=c[1216]|0;if(b>>>0>=p>>>0&e>>>0>=p>>>0){c[b+12>>2]=q;c[a>>2]=q;c[q+8>>2]=b;c[q+12>>2]=e;c[q+24>>2]=0;break}else ga()}}else{c[1213]=a|b;c[e>>2]=q;c[q+24>>2]=e;c[q+12>>2]=q;c[q+8>>2]=q}while(0);q=(c[1220]|0)+-1|0;c[1220]=q;if(!q)a=5304;else return;while(1){a=c[a>>2]|0;if(!a)break;else a=a+8|0}c[1220]=-1;return}function xc(a,b){a=a|0;b=b|0;var d=0,e=0;if(!a){a=vc(b)|0;return a|0}if(b>>>0>4294967231){c[(ec()|0)>>2]=12;a=0;return a|0}d=yc(a+-8|0,b>>>0<11?16:b+11&-8)|0;if(d){a=d+8|0;return a|0}d=vc(b)|0;if(!d){a=0;return a|0}e=c[a+-4>>2]|0;e=(e&-8)-((e&3|0)==0?8:4)|0;Fc(d|0,a|0,(e>>>0<b>>>0?e:b)|0)|0;wc(a);a=d;return a|0}function yc(a,b){a=a|0;b=b|0;var d=0,e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0,o=0;n=a+4|0;o=c[n>>2]|0;d=o&-8;k=a+d|0;i=c[1216]|0;e=o&3;if(!((e|0)!=1&a>>>0>=i>>>0&a>>>0<k>>>0))ga();f=c[k+4>>2]|0;if(!(f&1))ga();if(!e){if(b>>>0<256){a=0;return a|0}if(d>>>0>=(b+4|0)>>>0?(d-b|0)>>>0<=c[1332]<<1>>>0:0)return a|0;a=0;return a|0}if(d>>>0>=b>>>0){d=d-b|0;if(d>>>0<=15)return a|0;m=a+b|0;c[n>>2]=o&1|b|2;c[m+4>>2]=d|3;b=m+d+4|0;c[b>>2]=c[b>>2]|1;zc(m,d);return a|0}if((k|0)==(c[1218]|0)){d=(c[1215]|0)+d|0;if(d>>>0<=b>>>0){a=0;return a|0}m=d-b|0;l=a+b|0;c[n>>2]=o&1|b|2;c[l+4>>2]=m|1;c[1218]=l;c[1215]=m;return a|0}if((k|0)==(c[1217]|0)){e=(c[1214]|0)+d|0;if(e>>>0<b>>>0){a=0;return a|0}d=e-b|0;if(d>>>0>15){e=a+b|0;m=e+d|0;c[n>>2]=o&1|b|2;c[e+4>>2]=d|1;c[m>>2]=d;b=m+4|0;c[b>>2]=c[b>>2]&-2}else{c[n>>2]=o&1|e|2;e=a+e+4|0;c[e>>2]=c[e>>2]|1;e=0;d=0}c[1214]=d;c[1217]=e;return a|0}if(f&2){a=0;return a|0}l=(f&-8)+d|0;if(l>>>0<b>>>0){a=0;return a|0}m=l-b|0;g=f>>>3;do if(f>>>0>=256){h=c[k+24>>2]|0;f=c[k+12>>2]|0;do if((f|0)==(k|0)){f=k+16|0;e=f+4|0;d=c[e>>2]|0;if(!d){d=c[f>>2]|0;if(!d){j=0;break}else e=f}while(1){f=d+20|0;g=c[f>>2]|0;if(g){d=g;e=f;continue}f=d+16|0;g=c[f>>2]|0;if(!g)break;else{d=g;e=f}}if(e>>>0<i>>>0)ga();else{c[e>>2]=0;j=d;break}}else{g=c[k+8>>2]|0;if(g>>>0<i>>>0)ga();d=g+12|0;if((c[d>>2]|0)!=(k|0))ga();e=f+8|0;if((c[e>>2]|0)==(k|0)){c[d>>2]=f;c[e>>2]=g;j=f;break}else ga()}while(0);if(h){d=c[k+28>>2]|0;e=5152+(d<<2)|0;if((k|0)==(c[e>>2]|0)){c[e>>2]=j;if(!j){c[1213]=c[1213]&~(1<<d);break}}else{if(h>>>0<(c[1216]|0)>>>0)ga();d=h+16|0;if((c[d>>2]|0)==(k|0))c[d>>2]=j;else c[h+20>>2]=j;if(!j)break}f=c[1216]|0;if(j>>>0<f>>>0)ga();c[j+24>>2]=h;d=k+16|0;e=c[d>>2]|0;do if(e)if(e>>>0<f>>>0)ga();else{c[j+16>>2]=e;c[e+24>>2]=j;break}while(0);d=c[d+4>>2]|0;if(d)if(d>>>0<(c[1216]|0)>>>0)ga();else{c[j+20>>2]=d;c[d+24>>2]=j;break}}}else{e=c[k+8>>2]|0;f=c[k+12>>2]|0;d=4888+(g<<1<<2)|0;if((e|0)!=(d|0)){if(e>>>0<i>>>0)ga();if((c[e+12>>2]|0)!=(k|0))ga()}if((f|0)==(e|0)){c[1212]=c[1212]&~(1<<g);break}if((f|0)!=(d|0)){if(f>>>0<i>>>0)ga();d=f+8|0;if((c[d>>2]|0)==(k|0))h=d;else ga()}else h=f+8|0;c[e+12>>2]=f;c[h>>2]=e}while(0);if(m>>>0<16){c[n>>2]=l|o&1|2;b=a+l+4|0;c[b>>2]=c[b>>2]|1;return a|0}else{l=a+b|0;c[n>>2]=o&1|b|2;c[l+4>>2]=m|3;b=l+m+4|0;c[b>>2]=c[b>>2]|1;zc(l,m);return a|0}return 0}function zc(a,b){a=a|0;b=b|0;var d=0,e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0;o=a+b|0;d=c[a+4>>2]|0;do if(!(d&1)){g=c[a>>2]|0;if(!(d&3))return;l=a+(0-g)|0;k=g+b|0;i=c[1216]|0;if(l>>>0<i>>>0)ga();if((l|0)==(c[1217]|0)){a=o+4|0;d=c[a>>2]|0;if((d&3|0)!=3){r=l;f=k;break}c[1214]=k;c[a>>2]=d&-2;c[l+4>>2]=k|1;c[l+k>>2]=k;return}e=g>>>3;if(g>>>0<256){a=c[l+8>>2]|0;b=c[l+12>>2]|0;d=4888+(e<<1<<2)|0;if((a|0)!=(d|0)){if(a>>>0<i>>>0)ga();if((c[a+12>>2]|0)!=(l|0))ga()}if((b|0)==(a|0)){c[1212]=c[1212]&~(1<<e);r=l;f=k;break}if((b|0)!=(d|0)){if(b>>>0<i>>>0)ga();d=b+8|0;if((c[d>>2]|0)==(l|0))h=d;else ga()}else h=b+8|0;c[a+12>>2]=b;c[h>>2]=a;r=l;f=k;break}g=c[l+24>>2]|0;b=c[l+12>>2]|0;do if((b|0)==(l|0)){b=l+16|0;a=b+4|0;d=c[a>>2]|0;if(!d){d=c[b>>2]|0;if(!d){j=0;break}else a=b}while(1){b=d+20|0;e=c[b>>2]|0;if(e){d=e;a=b;continue}b=d+16|0;e=c[b>>2]|0;if(!e)break;else{d=e;a=b}}if(a>>>0<i>>>0)ga();else{c[a>>2]=0;j=d;break}}else{e=c[l+8>>2]|0;if(e>>>0<i>>>0)ga();d=e+12|0;if((c[d>>2]|0)!=(l|0))ga();a=b+8|0;if((c[a>>2]|0)==(l|0)){c[d>>2]=b;c[a>>2]=e;j=b;break}else ga()}while(0);if(g){d=c[l+28>>2]|0;a=5152+(d<<2)|0;if((l|0)==(c[a>>2]|0)){c[a>>2]=j;if(!j){c[1213]=c[1213]&~(1<<d);r=l;f=k;break}}else{if(g>>>0<(c[1216]|0)>>>0)ga();d=g+16|0;if((c[d>>2]|0)==(l|0))c[d>>2]=j;else c[g+20>>2]=j;if(!j){r=l;f=k;break}}b=c[1216]|0;if(j>>>0<b>>>0)ga();c[j+24>>2]=g;d=l+16|0;a=c[d>>2]|0;do if(a)if(a>>>0<b>>>0)ga();else{c[j+16>>2]=a;c[a+24>>2]=j;break}while(0);d=c[d+4>>2]|0;if(d)if(d>>>0<(c[1216]|0)>>>0)ga();else{c[j+20>>2]=d;c[d+24>>2]=j;r=l;f=k;break}else{r=l;f=k}}else{r=l;f=k}}else{r=a;f=b}while(0);h=c[1216]|0;if(o>>>0<h>>>0)ga();d=o+4|0;a=c[d>>2]|0;if(!(a&2)){if((o|0)==(c[1218]|0)){q=(c[1215]|0)+f|0;c[1215]=q;c[1218]=r;c[r+4>>2]=q|1;if((r|0)!=(c[1217]|0))return;c[1217]=0;c[1214]=0;return}if((o|0)==(c[1217]|0)){q=(c[1214]|0)+f|0;c[1214]=q;c[1217]=r;c[r+4>>2]=q|1;c[r+q>>2]=q;return}f=(a&-8)+f|0;e=a>>>3;do if(a>>>0>=256){g=c[o+24>>2]|0;b=c[o+12>>2]|0;do if((b|0)==(o|0)){b=o+16|0;a=b+4|0;d=c[a>>2]|0;if(!d){d=c[b>>2]|0;if(!d){n=0;break}else a=b}while(1){b=d+20|0;e=c[b>>2]|0;if(e){d=e;a=b;continue}b=d+16|0;e=c[b>>2]|0;if(!e)break;else{d=e;a=b}}if(a>>>0<h>>>0)ga();else{c[a>>2]=0;n=d;break}}else{e=c[o+8>>2]|0;if(e>>>0<h>>>0)ga();d=e+12|0;if((c[d>>2]|0)!=(o|0))ga();a=b+8|0;if((c[a>>2]|0)==(o|0)){c[d>>2]=b;c[a>>2]=e;n=b;break}else ga()}while(0);if(g){d=c[o+28>>2]|0;a=5152+(d<<2)|0;if((o|0)==(c[a>>2]|0)){c[a>>2]=n;if(!n){c[1213]=c[1213]&~(1<<d);break}}else{if(g>>>0<(c[1216]|0)>>>0)ga();d=g+16|0;if((c[d>>2]|0)==(o|0))c[d>>2]=n;else c[g+20>>2]=n;if(!n)break}b=c[1216]|0;if(n>>>0<b>>>0)ga();c[n+24>>2]=g;d=o+16|0;a=c[d>>2]|0;do if(a)if(a>>>0<b>>>0)ga();else{c[n+16>>2]=a;c[a+24>>2]=n;break}while(0);d=c[d+4>>2]|0;if(d)if(d>>>0<(c[1216]|0)>>>0)ga();else{c[n+20>>2]=d;c[d+24>>2]=n;break}}}else{a=c[o+8>>2]|0;b=c[o+12>>2]|0;d=4888+(e<<1<<2)|0;if((a|0)!=(d|0)){if(a>>>0<h>>>0)ga();if((c[a+12>>2]|0)!=(o|0))ga()}if((b|0)==(a|0)){c[1212]=c[1212]&~(1<<e);break}if((b|0)!=(d|0)){if(b>>>0<h>>>0)ga();d=b+8|0;if((c[d>>2]|0)==(o|0))m=d;else ga()}else m=b+8|0;c[a+12>>2]=b;c[m>>2]=a}while(0);c[r+4>>2]=f|1;c[r+f>>2]=f;if((r|0)==(c[1217]|0)){c[1214]=f;return}}else{c[d>>2]=a&-2;c[r+4>>2]=f|1;c[r+f>>2]=f}d=f>>>3;if(f>>>0<256){b=4888+(d<<1<<2)|0;a=c[1212]|0;d=1<<d;if(a&d){d=b+8|0;a=c[d>>2]|0;if(a>>>0<(c[1216]|0)>>>0)ga();else{p=d;q=a}}else{c[1212]=a|d;p=b+8|0;q=b}c[p>>2]=r;c[q+12>>2]=r;c[r+8>>2]=q;c[r+12>>2]=b;return}d=f>>>8;if(d)if(f>>>0>16777215)b=31;else{p=(d+1048320|0)>>>16&8;q=d<<p;o=(q+520192|0)>>>16&4;q=q<<o;b=(q+245760|0)>>>16&2;b=14-(o|p|b)+(q<<b>>>15)|0;b=f>>>(b+7|0)&1|b<<1}else b=0;e=5152+(b<<2)|0;c[r+28>>2]=b;c[r+20>>2]=0;c[r+16>>2]=0;d=c[1213]|0;a=1<<b;if(!(d&a)){c[1213]=d|a;c[e>>2]=r;c[r+24>>2]=e;c[r+12>>2]=r;c[r+8>>2]=r;return}b=f<<((b|0)==31?0:25-(b>>>1)|0);e=c[e>>2]|0;while(1){if((c[e+4>>2]&-8|0)==(f|0)){d=127;break}a=e+16+(b>>>31<<2)|0;d=c[a>>2]|0;if(!d){d=124;break}else{b=b<<1;e=d}}if((d|0)==124){if(a>>>0<(c[1216]|0)>>>0)ga();c[a>>2]=r;c[r+24>>2]=e;c[r+12>>2]=r;c[r+8>>2]=r;return}else if((d|0)==127){d=e+8|0;a=c[d>>2]|0;q=c[1216]|0;if(!(a>>>0>=q>>>0&e>>>0>=q>>>0))ga();c[a+12>>2]=r;c[d>>2]=r;c[r+8>>2]=a;c[r+12>>2]=e;c[r+24>>2]=0;return}}function Ac(){}function Bc(b,d,e){b=b|0;d=d|0;e=e|0;var f=0,g=0,h=0,i=0;f=b+e|0;if((e|0)>=20){d=d&255;h=b&3;i=d|d<<8|d<<16|d<<24;g=f&~3;if(h){h=b+4-h|0;while((b|0)<(h|0)){a[b>>0]=d;b=b+1|0}}while((b|0)<(g|0)){c[b>>2]=i;b=b+4|0}}while((b|0)<(f|0)){a[b>>0]=d;b=b+1|0}return b-e|0}function Cc(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;d=b-d-(c>>>0>a>>>0|0)>>>0;return (C=d,a-c>>>0|0)|0}function Dc(a,b,c){a=a|0;b=b|0;c=c|0;if((c|0)<32){C=b>>>c;return a>>>c|(b&(1<<c)-1)<<32-c}C=0;return b>>>c-32|0}function Ec(a,b,c){a=a|0;b=b|0;c=c|0;if((c|0)<32){C=b<<c|(a&(1<<c)-1<<32-c)>>>32-c;return a<<c}C=a<<c-32;return 0}function Fc(b,d,e){b=b|0;d=d|0;e=e|0;var f=0;if((e|0)>=4096)return ka(b|0,d|0,e|0)|0;f=b|0;if((b&3)==(d&3)){while(b&3){if(!e)return f|0;a[b>>0]=a[d>>0]|0;b=b+1|0;d=d+1|0;e=e-1|0}while((e|0)>=4){c[b>>2]=c[d>>2];b=b+4|0;d=d+4|0;e=e-4|0}}while((e|0)>0){a[b>>0]=a[d>>0]|0;b=b+1|0;d=d+1|0;e=e-1|0}return f|0}function Gc(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;c=a+c>>>0;return (C=b+d+(c>>>0<a>>>0|0)>>>0,c|0)|0}function Hc(b,c,d){b=b|0;c=c|0;d=d|0;var e=0;if((c|0)<(b|0)&(b|0)<(c+d|0)){e=b;c=c+d|0;b=b+d|0;while((d|0)>0){b=b-1|0;c=c-1|0;d=d-1|0;a[b>>0]=a[c>>0]|0}b=e}else Fc(b,c,d)|0;return b|0}function Ic(a,b,c){a=a|0;b=b|0;c=c|0;if((c|0)<32){C=b>>c;return a>>>c|(b&(1<<c)-1)<<32-c}C=(b|0)<0?-1:0;return b>>c-32|0}function Jc(b){b=b|0;var c=0;c=a[m+(b&255)>>0]|0;if((c|0)<8)return c|0;c=a[m+(b>>8&255)>>0]|0;if((c|0)<8)return c+8|0;c=a[m+(b>>16&255)>>0]|0;if((c|0)<8)return c+16|0;return (a[m+(b>>>24)>>0]|0)+24|0}function Kc(a,b){a=a|0;b=b|0;var c=0,d=0,e=0,f=0;f=a&65535;e=b&65535;c=_(e,f)|0;d=a>>>16;a=(c>>>16)+(_(e,d)|0)|0;e=b>>>16;b=_(e,f)|0;return (C=(a>>>16)+(_(e,d)|0)+(((a&65535)+b|0)>>>16)|0,a+b<<16|c&65535|0)|0}function Lc(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;var e=0,f=0,g=0,h=0,i=0,j=0;j=b>>31|((b|0)<0?-1:0)<<1;i=((b|0)<0?-1:0)>>31|((b|0)<0?-1:0)<<1;f=d>>31|((d|0)<0?-1:0)<<1;e=((d|0)<0?-1:0)>>31|((d|0)<0?-1:0)<<1;h=Cc(j^a,i^b,j,i)|0;g=C;a=f^j;b=e^i;return Cc((Qc(h,g,Cc(f^c,e^d,f,e)|0,C,0)|0)^a,C^b,a,b)|0}function Mc(a,b,d,e){a=a|0;b=b|0;d=d|0;e=e|0;var f=0,g=0,h=0,j=0,k=0,l=0;f=i;i=i+16|0;j=f|0;h=b>>31|((b|0)<0?-1:0)<<1;g=((b|0)<0?-1:0)>>31|((b|0)<0?-1:0)<<1;l=e>>31|((e|0)<0?-1:0)<<1;k=((e|0)<0?-1:0)>>31|((e|0)<0?-1:0)<<1;a=Cc(h^a,g^b,h,g)|0;b=C;Qc(a,b,Cc(l^d,k^e,l,k)|0,C,j)|0;e=Cc(c[j>>2]^h,c[j+4>>2]^g,h,g)|0;d=C;i=f;return (C=d,e)|0}function Nc(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;var e=0,f=0;e=a;f=c;c=Kc(e,f)|0;a=C;return (C=(_(b,f)|0)+(_(d,e)|0)+a|a&0,c|0|0)|0}function Oc(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;return Qc(a,b,c,d,0)|0}function Pc(a,b,d,e){a=a|0;b=b|0;d=d|0;e=e|0;var f=0,g=0;g=i;i=i+16|0;f=g|0;Qc(a,b,d,e,f)|0;i=g;return (C=c[f+4>>2]|0,c[f>>2]|0)|0}function Qc(a,b,d,e,f){a=a|0;b=b|0;d=d|0;e=e|0;f=f|0;var g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0;l=a;j=b;k=j;h=d;n=e;i=n;if(!k){g=(f|0)!=0;if(!i){if(g){c[f>>2]=(l>>>0)%(h>>>0);c[f+4>>2]=0}n=0;f=(l>>>0)/(h>>>0)>>>0;return (C=n,f)|0}else{if(!g){n=0;f=0;return (C=n,f)|0}c[f>>2]=a|0;c[f+4>>2]=b&0;n=0;f=0;return (C=n,f)|0}}g=(i|0)==0;do if(h){if(!g){g=(aa(i|0)|0)-(aa(k|0)|0)|0;if(g>>>0<=31){m=g+1|0;i=31-g|0;b=g-31>>31;h=m;a=l>>>(m>>>0)&b|k<<i;b=k>>>(m>>>0)&b;g=0;i=l<<i;break}if(!f){n=0;f=0;return (C=n,f)|0}c[f>>2]=a|0;c[f+4>>2]=j|b&0;n=0;f=0;return (C=n,f)|0}g=h-1|0;if(g&h){i=(aa(h|0)|0)+33-(aa(k|0)|0)|0;p=64-i|0;m=32-i|0;j=m>>31;o=i-32|0;b=o>>31;h=i;a=m-1>>31&k>>>(o>>>0)|(k<<m|l>>>(i>>>0))&b;b=b&k>>>(i>>>0);g=l<<p&j;i=(k<<p|l>>>(o>>>0))&j|l<<m&i-33>>31;break}if(f){c[f>>2]=g&l;c[f+4>>2]=0}if((h|0)==1){o=j|b&0;p=a|0|0;return (C=o,p)|0}else{p=Jc(h|0)|0;o=k>>>(p>>>0)|0;p=k<<32-p|l>>>(p>>>0)|0;return (C=o,p)|0}}else{if(g){if(f){c[f>>2]=(k>>>0)%(h>>>0);c[f+4>>2]=0}o=0;p=(k>>>0)/(h>>>0)>>>0;return (C=o,p)|0}if(!l){if(f){c[f>>2]=0;c[f+4>>2]=(k>>>0)%(i>>>0)}o=0;p=(k>>>0)/(i>>>0)>>>0;return (C=o,p)|0}g=i-1|0;if(!(g&i)){if(f){c[f>>2]=a|0;c[f+4>>2]=g&k|b&0}o=0;p=k>>>((Jc(i|0)|0)>>>0);return (C=o,p)|0}g=(aa(i|0)|0)-(aa(k|0)|0)|0;if(g>>>0<=30){b=g+1|0;i=31-g|0;h=b;a=k<<i|l>>>(b>>>0);b=k>>>(b>>>0);g=0;i=l<<i;break}if(!f){o=0;p=0;return (C=o,p)|0}c[f>>2]=a|0;c[f+4>>2]=j|b&0;o=0;p=0;return (C=o,p)|0}while(0);if(!h){k=i;j=0;i=0}else{m=d|0|0;l=n|e&0;k=Gc(m|0,l|0,-1,-1)|0;d=C;j=i;i=0;do{e=j;j=g>>>31|j<<1;g=i|g<<1;e=a<<1|e>>>31|0;n=a>>>31|b<<1|0;Cc(k,d,e,n)|0;p=C;o=p>>31|((p|0)<0?-1:0)<<1;i=o&1;a=Cc(e,n,o&m,(((p|0)<0?-1:0)>>31|((p|0)<0?-1:0)<<1)&l)|0;b=C;h=h-1|0}while((h|0)!=0);k=j;j=0}h=0;if(f){c[f>>2]=a;c[f+4>>2]=b}o=(g|0)>>>31|(k|h)<<1|(h<<1|g>>>31)&0|j;p=(g<<1|0>>>31)&-2|i;return (C=o,p)|0}function Rc(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;return ra[a&1](b|0,c|0,d|0)|0}function Sc(a,b,c){a=a|0;b=b|0;c=c|0;ba(0);return 0}
+
+// EMSCRIPTEN_END_FUNCS
+var ra=[Sc,qc];return{_malloc:vc,_i64Subtract:Cc,_free:wc,_i64Add:Gc,_memmove:Hc,_memset:Bc,___cxa_demangle:Ba,_memcpy:Fc,_bitshift64Lshr:Dc,_bitshift64Shl:Ec,runPostSets:Ac,stackAlloc:sa,stackSave:ta,stackRestore:ua,establishStackSpace:va,setThrew:wa,setTempRet0:za,getTempRet0:Aa,dynCall_iiii:Rc}})
+
+
+// EMSCRIPTEN_END_ASM
+(Module.asmGlobalArg,Module.asmLibraryArg,buffer);var ___cxa_demangle=Module["___cxa_demangle"]=asm["___cxa_demangle"];var _i64Subtract=Module["_i64Subtract"]=asm["_i64Subtract"];var _free=Module["_free"]=asm["_free"];var runPostSets=Module["runPostSets"]=asm["runPostSets"];var _i64Add=Module["_i64Add"]=asm["_i64Add"];var _memmove=Module["_memmove"]=asm["_memmove"];var _memset=Module["_memset"]=asm["_memset"];var _malloc=Module["_malloc"]=asm["_malloc"];var _memcpy=Module["_memcpy"]=asm["_memcpy"];var _bitshift64Lshr=Module["_bitshift64Lshr"]=asm["_bitshift64Lshr"];var _bitshift64Shl=Module["_bitshift64Shl"]=asm["_bitshift64Shl"];var dynCall_iiii=Module["dynCall_iiii"]=asm["dynCall_iiii"];Runtime.stackAlloc=asm["stackAlloc"];Runtime.stackSave=asm["stackSave"];Runtime.stackRestore=asm["stackRestore"];Runtime.establishStackSpace=asm["establishStackSpace"];Runtime.setTempRet0=asm["setTempRet0"];Runtime.getTempRet0=asm["getTempRet0"];function ExitStatus(status){this.name="ExitStatus";this.message="Program terminated with exit("+status+")";this.status=status}ExitStatus.prototype=new Error;ExitStatus.prototype.constructor=ExitStatus;var initialStackTop;var preloadStartTime=null;var calledMain=false;dependenciesFulfilled=function runCaller(){if(!Module["calledRun"])run();if(!Module["calledRun"])dependenciesFulfilled=runCaller};Module["callMain"]=Module.callMain=function callMain(args){assert(runDependencies==0,"cannot call main when async dependencies remain! (listen on __ATMAIN__)");assert(__ATPRERUN__.length==0,"cannot call main when preRun functions remain to be called");args=args||[];ensureInitRuntime();var argc=args.length+1;function pad(){for(var i=0;i<4-1;i++){argv.push(0)}}var argv=[allocate(intArrayFromString(Module["thisProgram"]),"i8",ALLOC_NORMAL)];pad();for(var i=0;i<argc-1;i=i+1){argv.push(allocate(intArrayFromString(args[i]),"i8",ALLOC_NORMAL));pad()}argv.push(0);argv=allocate(argv,"i32",ALLOC_NORMAL);try{var ret=Module["_main"](argc,argv,0);exit(ret,true)}catch(e){if(e instanceof ExitStatus){return}else if(e=="SimulateInfiniteLoop"){Module["noExitRuntime"]=true;return}else{if(e&&typeof e==="object"&&e.stack)Module.printErr("exception thrown: "+[e,e.stack]);throw e}}finally{calledMain=true}};function run(args){args=args||Module["arguments"];if(preloadStartTime===null)preloadStartTime=Date.now();if(runDependencies>0){return}preRun();if(runDependencies>0)return;if(Module["calledRun"])return;function doRun(){if(Module["calledRun"])return;Module["calledRun"]=true;if(ABORT)return;ensureInitRuntime();preMain();if(Module["onRuntimeInitialized"])Module["onRuntimeInitialized"]();if(Module["_main"]&&shouldRunNow)Module["callMain"](args);postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout((function(){setTimeout((function(){Module["setStatus"]("")}),1);doRun()}),1)}else{doRun()}}Module["run"]=Module.run=run;function exit(status,implicit){if(implicit&&Module["noExitRuntime"]){return}if(Module["noExitRuntime"]){}else{ABORT=true;EXITSTATUS=status;STACKTOP=initialStackTop;exitRuntime();if(Module["onExit"])Module["onExit"](status)}if(ENVIRONMENT_IS_NODE){process["stdout"]["once"]("drain",(function(){process["exit"](status)}));console.log(" ");setTimeout((function(){process["exit"](status)}),500)}else if(ENVIRONMENT_IS_SHELL&&typeof quit==="function"){quit(status)}throw new ExitStatus(status)}Module["exit"]=Module.exit=exit;var abortDecorators=[];function abort(what){if(what!==undefined){Module.print(what);Module.printErr(what);what=JSON.stringify(what)}else{what=""}ABORT=true;EXITSTATUS=1;var extra="\nIf this abort() is unexpected, build with -s ASSERTIONS=1 which can give more information.";var output="abort("+what+") at "+stackTrace()+extra;if(abortDecorators){abortDecorators.forEach((function(decorator){output=decorator(output,what)}))}throw output}Module["abort"]=Module.abort=abort;if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].pop()()}}var shouldRunNow=true;if(Module["noInitialRun"]){shouldRunNow=false}run()
+
+
+
+
+
+ return Module;
+};
+
+ var m = Module();
+ var status = m._malloc(4);
+ var buf = m._malloc(2048);
+
+ return function(func) {
+ if (func.length >= 2048) return null;
+ m.writeStringToMemory(func.substr(1), buf);
+ var ret = m['___cxa_demangle'](buf, 0, 0, status);
+ var result = null;
+ if (m.HEAP32[status >> 2] === 0 && ret) {
+ result = m.Pointer_stringify(ret);
+ m._free(ret);
+ }
+ return result;
+ };
+})();
+
+// The emscripten compiler exports the Module object; we just want
+// the demangle function
+if (typeof module === "object" && typeof module.exports === "object") {
+ module.exports = demangle;
+}
diff --git a/devtools/client/shared/developer-toolbar.js b/devtools/client/shared/developer-toolbar.js
new file mode 100644
index 000000000..2528591a6
--- /dev/null
+++ b/devtools/client/shared/developer-toolbar.js
@@ -0,0 +1,1397 @@
+/* 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 promise = require("promise");
+const defer = require("devtools/shared/defer");
+const Services = require("Services");
+const { TargetFactory } = require("devtools/client/framework/target");
+const Telemetry = require("devtools/client/shared/telemetry");
+const {ViewHelpers} = require("devtools/client/shared/widgets/view-helpers");
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper("devtools/client/locales/toolbox.properties");
+const {Task} = require("devtools/shared/task");
+
+const NS_XHTML = "http://www.w3.org/1999/xhtml";
+
+const { PluralForm } = require("devtools/shared/plural-form");
+
+loader.lazyGetter(this, "prefBranch", function () {
+ return Services.prefs.getBranch(null)
+ .QueryInterface(Ci.nsIPrefBranch2);
+});
+
+loader.lazyRequireGetter(this, "gcliInit", "devtools/shared/gcli/commands/index");
+loader.lazyRequireGetter(this, "util", "gcli/util/util");
+loader.lazyRequireGetter(this, "ConsoleServiceListener", "devtools/server/actors/utils/webconsole-utils", true);
+loader.lazyRequireGetter(this, "gDevTools", "devtools/client/framework/devtools", true);
+loader.lazyRequireGetter(this, "gDevToolsBrowser", "devtools/client/framework/devtools-browser", true);
+loader.lazyRequireGetter(this, "nodeConstants", "devtools/shared/dom-node-constants");
+loader.lazyRequireGetter(this, "EventEmitter", "devtools/shared/event-emitter");
+
+/**
+ * A collection of utilities to help working with commands
+ */
+var CommandUtils = {
+ /**
+ * Utility to ensure that things are loaded in the correct order
+ */
+ createRequisition: function (target, options) {
+ if (!gcliInit) {
+ return promise.reject("Unable to load gcli");
+ }
+ return gcliInit.getSystem(target).then(system => {
+ let Requisition = require("gcli/cli").Requisition;
+ return new Requisition(system, options);
+ });
+ },
+
+ /**
+ * Destroy the remote side of the requisition as well as the local side
+ */
+ destroyRequisition: function (requisition, target) {
+ requisition.destroy();
+ gcliInit.releaseSystem(target);
+ },
+
+ /**
+ * Read a toolbarSpec from preferences
+ * @param pref The name of the preference to read
+ */
+ getCommandbarSpec: function (pref) {
+ let value = prefBranch.getComplexValue(pref, Ci.nsISupportsString).data;
+ return JSON.parse(value);
+ },
+
+ /**
+ * A toolbarSpec is an array of strings each of which is a GCLI command.
+ *
+ * Warning: this method uses the unload event of the window that owns the
+ * buttons that are of type checkbox. this means that we don't properly
+ * unregister event handlers until the window is destroyed.
+ */
+ createButtons: function (toolbarSpec, target, document, requisition) {
+ return util.promiseEach(toolbarSpec, typed => {
+ // Ask GCLI to parse the typed string (doesn't execute it)
+ return requisition.update(typed).then(() => {
+ let button = document.createElementNS(NS_XHTML, "button");
+
+ // Ignore invalid commands
+ let command = requisition.commandAssignment.value;
+ if (command == null) {
+ throw new Error("No command '" + typed + "'");
+ }
+
+ if (command.buttonId != null) {
+ button.id = command.buttonId;
+ if (command.buttonClass != null) {
+ button.className = command.buttonClass;
+ }
+ } else {
+ button.setAttribute("text-as-image", "true");
+ button.setAttribute("label", command.name);
+ }
+
+ button.classList.add("devtools-button");
+
+ if (command.tooltipText != null) {
+ button.setAttribute("title", command.tooltipText);
+ } else if (command.description != null) {
+ button.setAttribute("title", command.description);
+ }
+
+ button.addEventListener("click",
+ requisition.updateExec.bind(requisition, typed));
+
+ button.addEventListener("keypress", (event) => {
+ if (ViewHelpers.isSpaceOrReturn(event)) {
+ event.preventDefault();
+ requisition.updateExec(typed);
+ }
+ }, false);
+
+ // Allow the command button to be toggleable
+ let onChange = null;
+ if (command.state) {
+ button.setAttribute("autocheck", false);
+
+ /**
+ * The onChange event should be called with an event object that
+ * contains a target property which specifies which target the event
+ * applies to. For legacy reasons the event object can also contain
+ * a tab property.
+ */
+ onChange = (eventName, ev) => {
+ if (ev.target == target || ev.tab == target.tab) {
+ let updateChecked = (checked) => {
+ if (checked) {
+ button.setAttribute("checked", true);
+ } else if (button.hasAttribute("checked")) {
+ button.removeAttribute("checked");
+ }
+ };
+
+ // isChecked would normally be synchronous. An annoying quirk
+ // of the 'csscoverage toggle' command forces us to accept a
+ // promise here, but doing Promise.resolve(reply).then(...) here
+ // makes this async for everyone, which breaks some tests so we
+ // treat non-promise replies separately to keep then synchronous.
+ let reply = command.state.isChecked(target);
+ if (typeof reply.then == "function") {
+ reply.then(updateChecked, console.error);
+ } else {
+ updateChecked(reply);
+ }
+ }
+ };
+
+ command.state.onChange(target, onChange);
+ onChange("", { target: target });
+ }
+ document.defaultView.addEventListener("unload", function (event) {
+ if (onChange && command.state.offChange) {
+ command.state.offChange(target, onChange);
+ }
+ button.remove();
+ button = null;
+ }, { once: true });
+
+ requisition.clear();
+
+ return button;
+ });
+ });
+ },
+
+ /**
+ * A helper function to create the environment object that is passed to
+ * GCLI commands.
+ * @param targetContainer An object containing a 'target' property which
+ * reflects the current debug target
+ */
+ createEnvironment: function (container, targetProperty = "target") {
+ if (!container[targetProperty].toString ||
+ !/TabTarget/.test(container[targetProperty].toString())) {
+ throw new Error("Missing target");
+ }
+
+ return {
+ get target() {
+ if (!container[targetProperty].toString ||
+ !/TabTarget/.test(container[targetProperty].toString())) {
+ throw new Error("Removed target");
+ }
+
+ return container[targetProperty];
+ },
+
+ get chromeWindow() {
+ return this.target.tab.ownerDocument.defaultView;
+ },
+
+ get chromeDocument() {
+ return this.target.tab.ownerDocument.defaultView.document;
+ },
+
+ get window() {
+ // throw new
+ // Error("environment.window is not available in runAt:client commands");
+ return this.chromeWindow.gBrowser.contentWindowAsCPOW;
+ },
+
+ get document() {
+ // throw new
+ // Error("environment.document is not available in runAt:client commands");
+ return this.chromeWindow.gBrowser.contentDocumentAsCPOW;
+ }
+ };
+ },
+};
+
+exports.CommandUtils = CommandUtils;
+
+/**
+ * Due to a number of panel bugs we need a way to check if we are running on
+ * Linux. See the comments for TooltipPanel and OutputPanel for further details.
+ *
+ * When bug 780102 is fixed all isLinux checks can be removed and we can revert
+ * to using panels.
+ */
+loader.lazyGetter(this, "isLinux", function () {
+ return Services.appinfo.OS == "Linux";
+});
+loader.lazyGetter(this, "isMac", function () {
+ return Services.appinfo.OS == "Darwin";
+});
+
+/**
+ * A component to manage the global developer toolbar, which contains a GCLI
+ * and buttons for various developer tools.
+ * @param chromeWindow The browser window to which this toolbar is attached
+ */
+function DeveloperToolbar(chromeWindow) {
+ this._chromeWindow = chromeWindow;
+
+ // Will be setup when show() is called
+ this.target = null;
+
+ this._doc = chromeWindow.document;
+
+ this._telemetry = new Telemetry();
+ this._errorsCount = {};
+ this._warningsCount = {};
+ this._errorListeners = {};
+
+ this._onToolboxReady = this._onToolboxReady.bind(this);
+ this._onToolboxDestroyed = this._onToolboxDestroyed.bind(this);
+
+ EventEmitter.decorate(this);
+}
+exports.DeveloperToolbar = DeveloperToolbar;
+
+/**
+ * Inspector notifications dispatched through the nsIObserverService
+ */
+const NOTIFICATIONS = {
+ /** DeveloperToolbar.show() has been called, and we're working on it */
+ LOAD: "developer-toolbar-load",
+
+ /** DeveloperToolbar.show() has completed */
+ SHOW: "developer-toolbar-show",
+
+ /** DeveloperToolbar.hide() has been called */
+ HIDE: "developer-toolbar-hide"
+};
+
+/**
+ * Attach notification constants to the object prototype so tests etc can
+ * use them without needing to import anything
+ */
+DeveloperToolbar.prototype.NOTIFICATIONS = NOTIFICATIONS;
+
+/**
+ * Is the toolbar open?
+ */
+Object.defineProperty(DeveloperToolbar.prototype, "visible", {
+ get: function () {
+ return this._element && !this._element.hidden;
+ },
+ enumerable: true
+});
+
+var _gSequenceId = 0;
+
+/**
+ * Getter for a unique ID.
+ */
+Object.defineProperty(DeveloperToolbar.prototype, "sequenceId", {
+ get: function () {
+ return _gSequenceId++;
+ },
+ enumerable: true
+});
+
+/**
+ * Create the <toolbar> element to insert within browser UI
+ */
+DeveloperToolbar.prototype.createToolbar = function () {
+ if (this._element) {
+ return;
+ }
+ let toolbar = this._doc.createElement("toolbar");
+ toolbar.setAttribute("id", "developer-toolbar");
+ toolbar.setAttribute("hidden", "true");
+
+ let close = this._doc.createElement("toolbarbutton");
+ close.setAttribute("id", "developer-toolbar-closebutton");
+ close.setAttribute("class", "close-icon");
+ close.setAttribute("oncommand", "DeveloperToolbar.hide();");
+ let closeTooltip = L10N.getStr("toolbar.closeButton.tooltip");
+ close.setAttribute("tooltiptext", closeTooltip);
+
+ let stack = this._doc.createElement("stack");
+ stack.setAttribute("flex", "1");
+
+ let input = this._doc.createElement("textbox");
+ input.setAttribute("class", "gclitoolbar-input-node");
+ input.setAttribute("rows", "1");
+ stack.appendChild(input);
+
+ let hbox = this._doc.createElement("hbox");
+ hbox.setAttribute("class", "gclitoolbar-complete-node");
+ stack.appendChild(hbox);
+
+ let toolboxBtn = this._doc.createElement("toolbarbutton");
+ toolboxBtn.setAttribute("id", "developer-toolbar-toolbox-button");
+ toolboxBtn.setAttribute("class", "developer-toolbar-button");
+ let toolboxTooltip = L10N.getStr("toolbar.toolsButton.tooltip");
+ toolboxBtn.setAttribute("tooltiptext", toolboxTooltip);
+ let toolboxOpen = gDevToolsBrowser.hasToolboxOpened(this._chromeWindow);
+ toolboxBtn.setAttribute("checked", toolboxOpen);
+ toolboxBtn.addEventListener("command", function (event) {
+ let window = event.target.ownerDocument.defaultView;
+ gDevToolsBrowser.toggleToolboxCommand(window.gBrowser);
+ });
+ this._errorCounterButton = toolboxBtn;
+ this._errorCounterButton._defaultTooltipText = toolboxTooltip;
+
+ // On Mac, the close button is on the left,
+ // while it is on the right on every other platforms.
+ if (isMac) {
+ toolbar.appendChild(close);
+ toolbar.appendChild(stack);
+ toolbar.appendChild(toolboxBtn);
+ } else {
+ toolbar.appendChild(stack);
+ toolbar.appendChild(toolboxBtn);
+ toolbar.appendChild(close);
+ }
+
+ this._element = toolbar;
+ let bottomBox = this._doc.getElementById("browser-bottombox");
+ if (bottomBox) {
+ bottomBox.appendChild(this._element);
+ } else {
+ // SeaMonkey does not have a "browser-bottombox".
+ let statusBar = this._doc.getElementById("status-bar");
+ if (statusBar) {
+ statusBar.parentNode.insertBefore(this._element, statusBar);
+ }
+ }
+};
+
+/**
+ * Called from browser.xul in response to menu-click or keyboard shortcut to
+ * toggle the toolbar
+ */
+DeveloperToolbar.prototype.toggle = function () {
+ if (this.visible) {
+ return this.hide().catch(console.error);
+ }
+ return this.show(true).catch(console.error);
+};
+
+/**
+ * Called from browser.xul in response to menu-click or keyboard shortcut to
+ * toggle the toolbar
+ */
+DeveloperToolbar.prototype.focus = function () {
+ if (this.visible) {
+ this._input.focus();
+ return promise.resolve();
+ }
+ return this.show(true);
+};
+
+/**
+ * Called from browser.xul in response to menu-click or keyboard shortcut to
+ * toggle the toolbar
+ */
+DeveloperToolbar.prototype.focusToggle = function () {
+ if (this.visible) {
+ // If we have focus then the active element is the HTML input contained
+ // inside the xul input element
+ let active = this._chromeWindow.document.activeElement;
+ let position = this._input.compareDocumentPosition(active);
+ if (position & nodeConstants.DOCUMENT_POSITION_CONTAINED_BY) {
+ this.hide();
+ } else {
+ this._input.focus();
+ }
+ } else {
+ this.show(true);
+ }
+};
+
+/**
+ * Even if the user has not clicked on 'Got it' in the intro, we only show it
+ * once per session.
+ * Warning this is slightly messed up because this.DeveloperToolbar is not the
+ * same as this.DeveloperToolbar when in browser.js context.
+ */
+DeveloperToolbar.introShownThisSession = false;
+
+/**
+ * Show the developer toolbar
+ */
+DeveloperToolbar.prototype.show = function (focus) {
+ if (this._showPromise != null) {
+ return this._showPromise;
+ }
+
+ this._showPromise = Task.spawn((function* () {
+ // hide() is async, so ensure we don't need to wait for hide() to
+ // finish. We unconditionally yield here, even if _hidePromise is
+ // null, so that the spawn call returns a promise before starting
+ // to do any real work.
+ yield this._hidePromise;
+
+ this.createToolbar();
+
+ Services.prefs.setBoolPref("devtools.toolbar.visible", true);
+
+ this._telemetry.toolOpened("developertoolbar");
+
+ this._notify(NOTIFICATIONS.LOAD);
+
+ this._input = this._doc.querySelector(".gclitoolbar-input-node");
+
+ // Initializing GCLI can only be done when we've got content windows to
+ // write to, so this needs to be done asynchronously.
+ let panelPromises = [
+ TooltipPanel.create(this),
+ OutputPanel.create(this)
+ ];
+ let panels = yield promise.all(panelPromises);
+
+ [ this.tooltipPanel, this.outputPanel ] = panels;
+
+ this._doc.getElementById("menu_devToolbar").setAttribute("checked", "true");
+
+ this.target = TargetFactory.forTab(this._chromeWindow.gBrowser.selectedTab);
+ const options = {
+ environment: CommandUtils.createEnvironment(this, "target"),
+ document: this.outputPanel.document,
+ };
+ let requisition = yield CommandUtils.createRequisition(this.target, options);
+ this.requisition = requisition;
+
+ // The <textbox> `value` may still be undefined on the XUL binding if
+ // we fetch it early
+ let value = this._input.value || "";
+ yield this.requisition.update(value);
+
+ const Inputter = require("gcli/mozui/inputter").Inputter;
+ const Completer = require("gcli/mozui/completer").Completer;
+ const Tooltip = require("gcli/mozui/tooltip").Tooltip;
+ const FocusManager = require("gcli/ui/focus").FocusManager;
+
+ this.onOutput = this.requisition.commandOutputManager.onOutput;
+
+ this.focusManager = new FocusManager(this._doc, requisition.system.settings);
+
+ this.inputter = new Inputter({
+ requisition: this.requisition,
+ focusManager: this.focusManager,
+ element: this._input,
+ });
+
+ this.completer = new Completer({
+ requisition: this.requisition,
+ inputter: this.inputter,
+ backgroundElement: this._doc.querySelector(".gclitoolbar-stack-node"),
+ element: this._doc.querySelector(".gclitoolbar-complete-node"),
+ });
+
+ this.tooltip = new Tooltip({
+ requisition: this.requisition,
+ focusManager: this.focusManager,
+ inputter: this.inputter,
+ element: this.tooltipPanel.hintElement,
+ });
+
+ this.inputter.tooltip = this.tooltip;
+
+ this.focusManager.addMonitoredElement(this.outputPanel._frame);
+ this.focusManager.addMonitoredElement(this._element);
+
+ this.focusManager.onVisibilityChange.add(this.outputPanel._visibilityChanged,
+ this.outputPanel);
+ this.focusManager.onVisibilityChange.add(this.tooltipPanel._visibilityChanged,
+ this.tooltipPanel);
+ this.onOutput.add(this.outputPanel._outputChanged, this.outputPanel);
+
+ let tabbrowser = this._chromeWindow.gBrowser;
+ tabbrowser.tabContainer.addEventListener("TabSelect", this, false);
+ tabbrowser.tabContainer.addEventListener("TabClose", this, false);
+ tabbrowser.addEventListener("load", this, true);
+ tabbrowser.addEventListener("beforeunload", this, true);
+
+ gDevTools.on("toolbox-ready", this._onToolboxReady);
+ gDevTools.on("toolbox-destroyed", this._onToolboxDestroyed);
+
+ this._initErrorsCount(tabbrowser.selectedTab);
+
+ this._element.hidden = false;
+
+ if (focus) {
+ // If the toolbar was just inserted, the <textbox> may still have
+ // its binding in process of being applied and not be focusable yet
+ let waitForBinding = () => {
+ // Bail out if the toolbar has been destroyed in the meantime
+ if (!this._input) {
+ return;
+ }
+ // mInputField is a xbl field of <xul:textbox>
+ if (typeof this._input.mInputField != "undefined") {
+ this._input.focus();
+ this._notify(NOTIFICATIONS.SHOW);
+ } else {
+ this._input.ownerDocument.defaultView.setTimeout(waitForBinding, 50);
+ }
+ };
+ waitForBinding();
+ } else {
+ this._notify(NOTIFICATIONS.SHOW);
+ }
+
+ if (!DeveloperToolbar.introShownThisSession) {
+ let intro = require("gcli/ui/intro");
+ intro.maybeShowIntro(this.requisition.commandOutputManager,
+ this.requisition.conversionContext,
+ this.outputPanel);
+ DeveloperToolbar.introShownThisSession = true;
+ }
+
+ this._showPromise = null;
+ }).bind(this));
+
+ return this._showPromise;
+};
+
+/**
+ * Hide the developer toolbar.
+ */
+DeveloperToolbar.prototype.hide = function () {
+ // If we're already in the process of hiding, just use the other promise
+ if (this._hidePromise != null) {
+ return this._hidePromise;
+ }
+
+ // show() is async, so ensure we don't need to wait for show() to finish
+ let waitPromise = this._showPromise || promise.resolve();
+
+ this._hidePromise = waitPromise.then(() => {
+ this._element.hidden = true;
+
+ Services.prefs.setBoolPref("devtools.toolbar.visible", false);
+
+ this._doc.getElementById("menu_devToolbar").setAttribute("checked", "false");
+ this.destroy();
+
+ this._telemetry.toolClosed("developertoolbar");
+ this._notify(NOTIFICATIONS.HIDE);
+
+ this._hidePromise = null;
+ });
+
+ return this._hidePromise;
+};
+
+/**
+ * Initialize the listeners needed for tracking the number of errors for a given
+ * tab.
+ *
+ * @private
+ * @param nsIDOMNode tab the xul:tab for which you want to track the number of
+ * errors.
+ */
+DeveloperToolbar.prototype._initErrorsCount = function (tab) {
+ let tabId = tab.linkedPanel;
+ if (tabId in this._errorsCount) {
+ this._updateErrorsCount();
+ return;
+ }
+
+ let window = tab.linkedBrowser.contentWindow;
+ let listener = new ConsoleServiceListener(window, {
+ onConsoleServiceMessage: this._onPageError.bind(this, tabId),
+ });
+ listener.init();
+
+ this._errorListeners[tabId] = listener;
+ this._errorsCount[tabId] = 0;
+ this._warningsCount[tabId] = 0;
+
+ let messages = listener.getCachedMessages();
+ messages.forEach(this._onPageError.bind(this, tabId));
+
+ this._updateErrorsCount();
+};
+
+/**
+ * Stop the listeners needed for tracking the number of errors for a given
+ * tab.
+ *
+ * @private
+ * @param nsIDOMNode tab the xul:tab for which you want to stop tracking the
+ * number of errors.
+ */
+DeveloperToolbar.prototype._stopErrorsCount = function (tab) {
+ let tabId = tab.linkedPanel;
+ if (!(tabId in this._errorsCount) || !(tabId in this._warningsCount)) {
+ this._updateErrorsCount();
+ return;
+ }
+
+ this._errorListeners[tabId].destroy();
+ delete this._errorListeners[tabId];
+ delete this._errorsCount[tabId];
+ delete this._warningsCount[tabId];
+
+ this._updateErrorsCount();
+};
+
+/**
+ * Hide the developer toolbar
+ */
+DeveloperToolbar.prototype.destroy = function () {
+ if (this._input == null) {
+ // Already destroyed
+ return;
+ }
+
+ let tabbrowser = this._chromeWindow.gBrowser;
+ tabbrowser.tabContainer.removeEventListener("TabSelect", this, false);
+ tabbrowser.tabContainer.removeEventListener("TabClose", this, false);
+ tabbrowser.removeEventListener("load", this, true);
+ tabbrowser.removeEventListener("beforeunload", this, true);
+
+ gDevTools.off("toolbox-ready", this._onToolboxReady);
+ gDevTools.off("toolbox-destroyed", this._onToolboxDestroyed);
+
+ Array.prototype.forEach.call(tabbrowser.tabs, this._stopErrorsCount, this);
+
+ this.focusManager.removeMonitoredElement(this.outputPanel._frame);
+ this.focusManager.removeMonitoredElement(this._element);
+
+ this.focusManager.onVisibilityChange.remove(this.outputPanel._visibilityChanged,
+ this.outputPanel);
+ this.focusManager.onVisibilityChange.remove(this.tooltipPanel._visibilityChanged,
+ this.tooltipPanel);
+ this.onOutput.remove(this.outputPanel._outputChanged, this.outputPanel);
+
+ this.tooltip.destroy();
+ this.completer.destroy();
+ this.inputter.destroy();
+ this.focusManager.destroy();
+
+ this.outputPanel.destroy();
+ this.tooltipPanel.destroy();
+ delete this._input;
+
+ CommandUtils.destroyRequisition(this.requisition, this.target);
+ this.target = undefined;
+
+ this._element.remove();
+ delete this._element;
+};
+
+/**
+ * Utility for sending notifications
+ * @param topic a NOTIFICATION constant
+ */
+DeveloperToolbar.prototype._notify = function (topic) {
+ let data = { toolbar: this };
+ data.wrappedJSObject = data;
+ Services.obs.notifyObservers(data, topic, null);
+};
+
+/**
+ * Update various parts of the UI when the current tab changes
+ */
+DeveloperToolbar.prototype.handleEvent = function (ev) {
+ if (ev.type == "TabSelect" || ev.type == "load") {
+ if (this.visible) {
+ let tab = this._chromeWindow.gBrowser.selectedTab;
+ this.target = TargetFactory.forTab(tab);
+ gcliInit.getSystem(this.target).then(system => {
+ this.requisition.system = system;
+ }, error => {
+ if (!this._chromeWindow.gBrowser.getBrowserForTab(tab)) {
+ // The tab was closed, suppress the error and print a warning as the
+ // destroyed tab was likely the cause.
+ console.warn("An error occurred as the tab was closed while " +
+ "updating Developer Toolbar state. The error was: ", error);
+ return;
+ }
+
+ // Propagate other errors as they're more likely to cause real issues
+ // and thus should cause tests to fail.
+ throw error;
+ });
+
+ if (ev.type == "TabSelect") {
+ let toolboxOpen = gDevToolsBrowser.hasToolboxOpened(this._chromeWindow);
+ this._errorCounterButton.setAttribute("checked", toolboxOpen);
+ this._initErrorsCount(ev.target);
+ }
+ }
+ } else if (ev.type == "TabClose") {
+ this._stopErrorsCount(ev.target);
+ } else if (ev.type == "beforeunload") {
+ this._onPageBeforeUnload(ev);
+ }
+};
+
+/**
+ * Update toolbox toggle button when toolbox goes on and off
+ */
+DeveloperToolbar.prototype._onToolboxReady = function () {
+ this._errorCounterButton.setAttribute("checked", "true");
+};
+DeveloperToolbar.prototype._onToolboxDestroyed = function () {
+ this._errorCounterButton.setAttribute("checked", "false");
+};
+
+/**
+ * Count a page error received for the currently selected tab. This
+ * method counts the JavaScript exceptions received and CSS errors/warnings.
+ *
+ * @private
+ * @param string tabId the ID of the tab from where the page error comes.
+ * @param object pageError the page error object received from the
+ * PageErrorListener.
+ */
+DeveloperToolbar.prototype._onPageError = function (tabId, pageError) {
+ if (pageError.category == "CSS Parser" ||
+ pageError.category == "CSS Loader") {
+ return;
+ }
+ if ((pageError.flags & pageError.warningFlag) ||
+ (pageError.flags & pageError.strictFlag)) {
+ this._warningsCount[tabId]++;
+ } else {
+ this._errorsCount[tabId]++;
+ }
+ this._updateErrorsCount(tabId);
+};
+
+/**
+ * The |beforeunload| event handler. This function resets the errors count when
+ * a different page starts loading.
+ *
+ * @private
+ * @param nsIDOMEvent ev the beforeunload DOM event.
+ */
+DeveloperToolbar.prototype._onPageBeforeUnload = function (ev) {
+ let window = ev.target.defaultView;
+ if (window.top !== window) {
+ return;
+ }
+
+ let tabs = this._chromeWindow.gBrowser.tabs;
+ Array.prototype.some.call(tabs, function (tab) {
+ if (tab.linkedBrowser.contentWindow === window) {
+ let tabId = tab.linkedPanel;
+ if (tabId in this._errorsCount || tabId in this._warningsCount) {
+ this._errorsCount[tabId] = 0;
+ this._warningsCount[tabId] = 0;
+ this._updateErrorsCount(tabId);
+ }
+ return true;
+ }
+ return false;
+ }, this);
+};
+
+/**
+ * Update the page errors count displayed in the Web Console button for the
+ * currently selected tab.
+ *
+ * @private
+ * @param string [changedTabId] Optional. The tab ID that had its page errors
+ * count changed. If this is provided and it doesn't match the currently
+ * selected tab, then the button is not updated.
+ */
+DeveloperToolbar.prototype._updateErrorsCount = function (changedTabId) {
+ let tabId = this._chromeWindow.gBrowser.selectedTab.linkedPanel;
+ if (changedTabId && tabId != changedTabId) {
+ return;
+ }
+
+ let errors = this._errorsCount[tabId];
+ let warnings = this._warningsCount[tabId];
+ let btn = this._errorCounterButton;
+ if (errors) {
+ let errorsText = L10N.getStr("toolboxToggleButton.errors");
+ errorsText = PluralForm.get(errors, errorsText).replace("#1", errors);
+
+ let warningsText = L10N.getStr("toolboxToggleButton.warnings");
+ warningsText = PluralForm.get(warnings, warningsText).replace("#1", warnings);
+
+ let tooltiptext = L10N.getFormatStr("toolboxToggleButton.tooltip",
+ errorsText, warningsText);
+
+ btn.setAttribute("error-count", errors);
+ btn.setAttribute("tooltiptext", tooltiptext);
+ } else {
+ btn.removeAttribute("error-count");
+ btn.setAttribute("tooltiptext", btn._defaultTooltipText);
+ }
+
+ this.emit("errors-counter-updated");
+};
+
+/**
+ * Reset the errors counter for the given tab.
+ *
+ * @param nsIDOMElement tab The xul:tab for which you want to reset the page
+ * errors counters.
+ */
+DeveloperToolbar.prototype.resetErrorsCount = function (tab) {
+ let tabId = tab.linkedPanel;
+ if (tabId in this._errorsCount || tabId in this._warningsCount) {
+ this._errorsCount[tabId] = 0;
+ this._warningsCount[tabId] = 0;
+ this._updateErrorsCount(tabId);
+ }
+};
+
+/**
+ * Creating a OutputPanel is asynchronous
+ */
+function OutputPanel() {
+ throw new Error("Use OutputPanel.create()");
+}
+
+/**
+ * Panel to handle command line output.
+ *
+ * There is a tooltip bug on Windows and OSX that prevents tooltips from being
+ * positioned properly (bug 786975). There is a Gnome panel bug on Linux that
+ * causes ugly focus issues (https://bugzilla.gnome.org/show_bug.cgi?id=621848).
+ * We now use a tooltip on Linux and a panel on OSX & Windows.
+ *
+ * If a panel has no content and no height it is not shown when openPopup is
+ * called on Windows and OSX (bug 692348) ... this prevents the panel from
+ * appearing the first time it is shown. Setting the panel's height to 1px
+ * before calling openPopup works around this issue as we resize it ourselves
+ * anyway.
+ *
+ * @param devtoolbar The parent DeveloperToolbar object
+ */
+OutputPanel.create = function (devtoolbar) {
+ let outputPanel = Object.create(OutputPanel.prototype);
+ return outputPanel._init(devtoolbar);
+};
+
+/**
+ * @private See OutputPanel.create
+ */
+OutputPanel.prototype._init = function (devtoolbar) {
+ this._devtoolbar = devtoolbar;
+ this._input = this._devtoolbar._input;
+ this._toolbar = this._devtoolbar._doc.getElementById("developer-toolbar");
+
+ /*
+ <tooltip|panel id="gcli-output"
+ noautofocus="true"
+ noautohide="true"
+ class="gcli-panel">
+ <html:iframe xmlns:html="http://www.w3.org/1999/xhtml"
+ id="gcli-output-frame"
+ src="chrome://devtools/content/commandline/commandlineoutput.xhtml"
+ sandbox="allow-same-origin"/>
+ </tooltip|panel>
+ */
+
+ // TODO: Switch back from tooltip to panel when metacity focus issue is fixed:
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=780102
+ this._panel = this._devtoolbar._doc.createElement(isLinux ? "tooltip" : "panel");
+
+ this._panel.id = "gcli-output";
+ this._panel.classList.add("gcli-panel");
+
+ if (isLinux) {
+ this.canHide = false;
+ this._onpopuphiding = this._onpopuphiding.bind(this);
+ this._panel.addEventListener("popuphiding", this._onpopuphiding, true);
+ } else {
+ this._panel.setAttribute("noautofocus", "true");
+ this._panel.setAttribute("noautohide", "true");
+
+ // Bug 692348: On Windows and OSX if a panel has no content and no height
+ // openPopup fails to display it. Setting the height to 1px alows the panel
+ // to be displayed before has content or a real height i.e. the first time
+ // it is displayed.
+ this._panel.setAttribute("height", "1px");
+ }
+
+ this._toolbar.parentElement.insertBefore(this._panel, this._toolbar);
+
+ this._frame = this._devtoolbar._doc.createElementNS(NS_XHTML, "iframe");
+ this._frame.id = "gcli-output-frame";
+ this._frame.setAttribute("src", "chrome://devtools/content/commandline/commandlineoutput.xhtml");
+ this._frame.setAttribute("sandbox", "allow-same-origin");
+ this._panel.appendChild(this._frame);
+
+ this.displayedOutput = undefined;
+
+ this._update = this._update.bind(this);
+
+ // Wire up the element from the iframe, and resolve the promise
+ let deferred = defer();
+ let onload = () => {
+ this._frame.removeEventListener("load", onload, true);
+
+ this.document = this._frame.contentDocument;
+ this._copyTheme();
+
+ this._div = this.document.getElementById("gcli-output-root");
+ this._div.classList.add("gcli-row-out");
+ this._div.setAttribute("aria-live", "assertive");
+
+ let styles = this._toolbar.ownerDocument.defaultView
+ .getComputedStyle(this._toolbar);
+ this._div.setAttribute("dir", styles.direction);
+
+ deferred.resolve(this);
+ };
+ this._frame.addEventListener("load", onload, true);
+
+ return deferred.promise;
+};
+
+/* Copy the current devtools theme attribute into the iframe,
+ so it can be styled correctly. */
+OutputPanel.prototype._copyTheme = function () {
+ if (this.document) {
+ let theme =
+ this._devtoolbar._doc.documentElement.getAttribute("devtoolstheme");
+ this.document.documentElement.setAttribute("devtoolstheme", theme);
+ }
+};
+
+/**
+ * Prevent the popup from hiding if it is not permitted via this.canHide.
+ */
+OutputPanel.prototype._onpopuphiding = function (ev) {
+ // TODO: When we switch back from tooltip to panel we can remove this hack:
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=780102
+ if (isLinux && !this.canHide) {
+ ev.preventDefault();
+ }
+};
+
+/**
+ * Display the OutputPanel.
+ */
+OutputPanel.prototype.show = function () {
+ if (isLinux) {
+ this.canHide = false;
+ }
+
+ // We need to reset the iframe size in order for future size calculations to
+ // be correct
+ this._frame.style.minHeight = this._frame.style.maxHeight = 0;
+ this._frame.style.minWidth = 0;
+
+ this._copyTheme();
+ this._panel.openPopup(this._input, "before_start", 0, 0, false, false, null);
+ this._resize();
+
+ this._input.focus();
+};
+
+/**
+ * Internal helper to set the height of the output panel to fit the available
+ * content;
+ */
+OutputPanel.prototype._resize = function () {
+ if (this._panel == null || this.document == null || !this._panel.state == "closed") {
+ return;
+ }
+
+ // Set max panel width to match any content with a max of the width of the
+ // browser window.
+ let maxWidth = this._panel.ownerDocument.documentElement.clientWidth;
+
+ // Adjust max width according to OS.
+ // We'd like to put this in CSS but we can't:
+ // body { width: calc(min(-5px, max-content)); }
+ // #_panel { max-width: -5px; }
+ switch (Services.appinfo.OS) {
+ case "Linux":
+ maxWidth -= 5;
+ break;
+ case "Darwin":
+ maxWidth -= 25;
+ break;
+ case "WINNT":
+ maxWidth -= 5;
+ break;
+ }
+
+ this.document.body.style.width = "-moz-max-content";
+ let style = this._frame.contentWindow.getComputedStyle(this.document.body);
+ let frameWidth = parseInt(style.width, 10);
+ let width = Math.min(maxWidth, frameWidth);
+ this.document.body.style.width = width + "px";
+
+ // Set the width of the iframe.
+ this._frame.style.minWidth = width + "px";
+ this._panel.style.maxWidth = maxWidth + "px";
+
+ // browserAdjustment is used to correct the panel height according to the
+ // browsers borders etc.
+ const browserAdjustment = 15;
+
+ // Set max panel height to match any content with a max of the height of the
+ // browser window.
+ let maxHeight =
+ this._panel.ownerDocument.documentElement.clientHeight - browserAdjustment;
+ let height = Math.min(maxHeight, this.document.documentElement.scrollHeight);
+
+ // Set the height of the iframe. Setting iframe.height does not work.
+ this._frame.style.minHeight = this._frame.style.maxHeight = height + "px";
+
+ // Set the height and width of the panel to match the iframe.
+ this._panel.sizeTo(width, height);
+
+ // Move the panel to the correct position in the case that it has been
+ // positioned incorrectly.
+ let screenX = this._input.boxObject.screenX;
+ let screenY = this._toolbar.boxObject.screenY;
+ this._panel.moveTo(screenX, screenY - height);
+};
+
+/**
+ * Called by GCLI when a command is executed.
+ */
+OutputPanel.prototype._outputChanged = function (ev) {
+ if (ev.output.hidden) {
+ return;
+ }
+
+ this.remove();
+
+ this.displayedOutput = ev.output;
+
+ if (this.displayedOutput.completed) {
+ this._update();
+ } else {
+ this.displayedOutput.promise.then(this._update, this._update)
+ .then(null, console.error);
+ }
+};
+
+/**
+ * Called when displayed Output says it's changed or from outputChanged, which
+ * happens when there is a new displayed Output.
+ */
+OutputPanel.prototype._update = function () {
+ // destroy has been called, bail out
+ if (this._div == null) {
+ return;
+ }
+
+ // Empty this._div
+ while (this._div.hasChildNodes()) {
+ this._div.removeChild(this._div.firstChild);
+ }
+
+ if (this.displayedOutput.data != null) {
+ let context = this._devtoolbar.requisition.conversionContext;
+ this.displayedOutput.convert("dom", context).then(node => {
+ if (node == null) {
+ return;
+ }
+
+ while (this._div.hasChildNodes()) {
+ this._div.removeChild(this._div.firstChild);
+ }
+
+ let links = node.querySelectorAll("*[href]");
+ for (let i = 0; i < links.length; i++) {
+ links[i].setAttribute("target", "_blank");
+ }
+
+ this._div.appendChild(node);
+ this.show();
+ });
+ }
+};
+
+/**
+ * Detach listeners from the currently displayed Output.
+ */
+OutputPanel.prototype.remove = function () {
+ if (isLinux) {
+ this.canHide = true;
+ }
+
+ if (this._panel && this._panel.hidePopup) {
+ this._panel.hidePopup();
+ }
+
+ if (this.displayedOutput) {
+ delete this.displayedOutput;
+ }
+};
+
+/**
+ * Detach listeners from the currently displayed Output.
+ */
+OutputPanel.prototype.destroy = function () {
+ this.remove();
+
+ this._panel.removeEventListener("popuphiding", this._onpopuphiding, true);
+
+ this._panel.removeChild(this._frame);
+ this._toolbar.parentElement.removeChild(this._panel);
+
+ delete this._devtoolbar;
+ delete this._input;
+ delete this._toolbar;
+ delete this._onpopuphiding;
+ delete this._panel;
+ delete this._frame;
+ delete this._content;
+ delete this._div;
+ delete this.document;
+};
+
+/**
+ * Called by GCLI to indicate that we should show or hide one either the
+ * tooltip panel or the output panel.
+ */
+OutputPanel.prototype._visibilityChanged = function (ev) {
+ if (ev.outputVisible === true) {
+ // this.show is called by _outputChanged
+ } else {
+ if (isLinux) {
+ this.canHide = true;
+ }
+ this._panel.hidePopup();
+ }
+};
+
+/**
+ * Creating a TooltipPanel is asynchronous
+ */
+function TooltipPanel() {
+ throw new Error("Use TooltipPanel.create()");
+}
+
+/**
+ * Panel to handle tooltips.
+ *
+ * There is a tooltip bug on Windows and OSX that prevents tooltips from being
+ * positioned properly (bug 786975). There is a Gnome panel bug on Linux that
+ * causes ugly focus issues (https://bugzilla.gnome.org/show_bug.cgi?id=621848).
+ * We now use a tooltip on Linux and a panel on OSX & Windows.
+ *
+ * If a panel has no content and no height it is not shown when openPopup is
+ * called on Windows and OSX (bug 692348) ... this prevents the panel from
+ * appearing the first time it is shown. Setting the panel's height to 1px
+ * before calling openPopup works around this issue as we resize it ourselves
+ * anyway.
+ *
+ * @param devtoolbar The parent DeveloperToolbar object
+ */
+TooltipPanel.create = function (devtoolbar) {
+ let tooltipPanel = Object.create(TooltipPanel.prototype);
+ return tooltipPanel._init(devtoolbar);
+};
+
+/**
+ * @private See TooltipPanel.create
+ */
+TooltipPanel.prototype._init = function (devtoolbar) {
+ let deferred = defer();
+
+ this._devtoolbar = devtoolbar;
+ this._input = devtoolbar._doc.querySelector(".gclitoolbar-input-node");
+ this._toolbar = devtoolbar._doc.querySelector("#developer-toolbar");
+ this._dimensions = { start: 0, end: 0 };
+
+ /*
+ <tooltip|panel id="gcli-tooltip"
+ type="arrow"
+ noautofocus="true"
+ noautohide="true"
+ class="gcli-panel">
+ <html:iframe xmlns:html="http://www.w3.org/1999/xhtml"
+ id="gcli-tooltip-frame"
+ src="chrome://devtools/content/commandline/commandlinetooltip.xhtml"
+ flex="1"
+ sandbox="allow-same-origin"/>
+ </tooltip|panel>
+ */
+
+ // TODO: Switch back from tooltip to panel when metacity focus issue is fixed:
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=780102
+ this._panel = devtoolbar._doc.createElement(isLinux ? "tooltip" : "panel");
+
+ this._panel.id = "gcli-tooltip";
+ this._panel.classList.add("gcli-panel");
+
+ if (isLinux) {
+ this.canHide = false;
+ this._onpopuphiding = this._onpopuphiding.bind(this);
+ this._panel.addEventListener("popuphiding", this._onpopuphiding, true);
+ } else {
+ this._panel.setAttribute("noautofocus", "true");
+ this._panel.setAttribute("noautohide", "true");
+
+ // Bug 692348: On Windows and OSX if a panel has no content and no height
+ // openPopup fails to display it. Setting the height to 1px alows the panel
+ // to be displayed before has content or a real height i.e. the first time
+ // it is displayed.
+ this._panel.setAttribute("height", "1px");
+ }
+
+ this._toolbar.parentElement.insertBefore(this._panel, this._toolbar);
+
+ this._frame = devtoolbar._doc.createElementNS(NS_XHTML, "iframe");
+ this._frame.id = "gcli-tooltip-frame";
+ this._frame.setAttribute("src", "chrome://devtools/content/commandline/commandlinetooltip.xhtml");
+ this._frame.setAttribute("flex", "1");
+ this._frame.setAttribute("sandbox", "allow-same-origin");
+ this._panel.appendChild(this._frame);
+
+ /**
+ * Wire up the element from the iframe, and resolve the promise.
+ */
+ let onload = () => {
+ this._frame.removeEventListener("load", onload, true);
+
+ this.document = this._frame.contentDocument;
+ this._copyTheme();
+ this.hintElement = this.document.getElementById("gcli-tooltip-root");
+ this._connector = this.document.getElementById("gcli-tooltip-connector");
+
+ let styles = this._toolbar.ownerDocument.defaultView
+ .getComputedStyle(this._toolbar);
+ this.hintElement.setAttribute("dir", styles.direction);
+
+ deferred.resolve(this);
+ };
+ this._frame.addEventListener("load", onload, true);
+
+ return deferred.promise;
+};
+
+/* Copy the current devtools theme attribute into the iframe,
+ so it can be styled correctly. */
+TooltipPanel.prototype._copyTheme = function () {
+ if (this.document) {
+ let theme =
+ this._devtoolbar._doc.documentElement.getAttribute("devtoolstheme");
+ this.document.documentElement.setAttribute("devtoolstheme", theme);
+ }
+};
+
+/**
+ * Prevent the popup from hiding if it is not permitted via this.canHide.
+ */
+TooltipPanel.prototype._onpopuphiding = function (ev) {
+ // TODO: When we switch back from tooltip to panel we can remove this hack:
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=780102
+ if (isLinux && !this.canHide) {
+ ev.preventDefault();
+ }
+};
+
+/**
+ * Display the TooltipPanel.
+ */
+TooltipPanel.prototype.show = function (dimensions) {
+ if (!dimensions) {
+ dimensions = { start: 0, end: 0 };
+ }
+ this._dimensions = dimensions;
+
+ // This is nasty, but displaying the panel causes it to re-flow, which can
+ // change the size it should be, so we need to resize the iframe after the
+ // panel has displayed
+ this._panel.ownerDocument.defaultView.setTimeout(() => {
+ this._resize();
+ }, 0);
+
+ if (isLinux) {
+ this.canHide = false;
+ }
+
+ this._copyTheme();
+ this._resize();
+ this._panel.openPopup(this._input, "before_start", dimensions.start * 10, 0,
+ false, false, null);
+ this._input.focus();
+};
+
+/**
+ * One option is to spend lots of time taking an average width of characters
+ * in the current font, dynamically, and weighting for the frequency of use of
+ * various characters, or even to render the given string off screen, and then
+ * measure the width.
+ * Or we could do this...
+ */
+const AVE_CHAR_WIDTH = 4.5;
+
+/**
+ * Display the TooltipPanel.
+ */
+TooltipPanel.prototype._resize = function () {
+ if (this._panel == null || this.document == null || !this._panel.state == "closed") {
+ return;
+ }
+
+ let offset = 10 + Math.floor(this._dimensions.start * AVE_CHAR_WIDTH);
+ this._panel.style.marginLeft = offset + "px";
+
+ /*
+ // Bug 744906: UX review - Not sure if we want this code to fatten connector
+ // with param width
+ let width = Math.floor(this._dimensions.end * AVE_CHAR_WIDTH);
+ width = Math.min(width, 100);
+ width = Math.max(width, 10);
+ this._connector.style.width = width + "px";
+ */
+
+ this._frame.height = this.document.body.scrollHeight;
+};
+
+/**
+ * Hide the TooltipPanel.
+ */
+TooltipPanel.prototype.remove = function () {
+ if (isLinux) {
+ this.canHide = true;
+ }
+ if (this._panel && this._panel.hidePopup) {
+ this._panel.hidePopup();
+ }
+};
+
+/**
+ * Hide the TooltipPanel.
+ */
+TooltipPanel.prototype.destroy = function () {
+ this.remove();
+
+ this._panel.removeEventListener("popuphiding", this._onpopuphiding, true);
+
+ this._panel.removeChild(this._frame);
+ this._toolbar.parentElement.removeChild(this._panel);
+
+ delete this._connector;
+ delete this._dimensions;
+ delete this._input;
+ delete this._onpopuphiding;
+ delete this._panel;
+ delete this._frame;
+ delete this._toolbar;
+ delete this._content;
+ delete this.document;
+ delete this.hintElement;
+};
+
+/**
+ * Called by GCLI to indicate that we should show or hide one either the
+ * tooltip panel or the output panel.
+ */
+TooltipPanel.prototype._visibilityChanged = function (ev) {
+ if (ev.tooltipVisible === true) {
+ this.show(ev.dimensions);
+ } else {
+ if (isLinux) {
+ this.canHide = true;
+ }
+ this._panel.hidePopup();
+ }
+};
diff --git a/devtools/client/shared/devices.js b/devtools/client/shared/devices.js
new file mode 100644
index 000000000..82bd493c4
--- /dev/null
+++ b/devtools/client/shared/devices.js
@@ -0,0 +1,88 @@
+/* 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 { getJSON } = require("devtools/client/shared/getjson");
+
+const DEVICES_URL = "devtools.devices.url";
+const { LocalizationHelper } = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper("devtools/client/locales/device.properties");
+
+/* This is a catalog of common web-enabled devices and their properties,
+ * intended for (mobile) device emulation.
+ *
+ * The properties of a device are:
+ * - name: brand and model(s).
+ * - width: viewport width.
+ * - height: viewport height.
+ * - pixelRatio: ratio from viewport to physical screen pixels.
+ * - userAgent: UA string of the device's browser.
+ * - touch: whether it has a touch screen.
+ * - firefoxOS: whether Firefox OS is supported.
+ *
+ * The device types are:
+ * ["phones", "tablets", "laptops", "televisions", "consoles", "watches"].
+ *
+ * You can easily add more devices to this catalog from your own code (e.g. an
+ * addon) like so:
+ *
+ * var myPhone = { name: "My Phone", ... };
+ * require("devtools/client/shared/devices").addDevice(myPhone, "phones");
+ */
+
+// Local devices catalog that addons can add to.
+let localDevices = {};
+
+// Add a device to the local catalog.
+function addDevice(device, type = "phones") {
+ let list = localDevices[type];
+ if (!list) {
+ list = localDevices[type] = [];
+ }
+ list.push(device);
+}
+exports.addDevice = addDevice;
+
+// Remove a device from the local catalog.
+// returns `true` if the device is removed, `false` otherwise.
+function removeDevice(device, type = "phones") {
+ let list = localDevices[type];
+ if (!list) {
+ return false;
+ }
+
+ let index = list.findIndex(item => device);
+
+ if (index === -1) {
+ return false;
+ }
+
+ list.splice(index, 1);
+
+ return true;
+}
+exports.removeDevice = removeDevice;
+
+// Get the complete devices catalog.
+function getDevices() {
+ // Fetch common devices from Mozilla's CDN.
+ return getJSON(DEVICES_URL).then(devices => {
+ for (let type in localDevices) {
+ if (!devices[type]) {
+ devices.TYPES.push(type);
+ devices[type] = [];
+ }
+ devices[type] = localDevices[type].concat(devices[type]);
+ }
+ return devices;
+ });
+}
+exports.getDevices = getDevices;
+
+// Get the localized string for a device type.
+function getDeviceString(deviceType) {
+ return L10N.getStr("device." + deviceType);
+}
+exports.getDeviceString = getDeviceString;
diff --git a/devtools/client/shared/devtools-file-watcher.js b/devtools/client/shared/devtools-file-watcher.js
new file mode 100644
index 000000000..59ec1b136
--- /dev/null
+++ b/devtools/client/shared/devtools-file-watcher.js
@@ -0,0 +1,78 @@
+/* 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 EventEmitter = require("devtools/shared/event-emitter");
+
+loader.lazyImporter(this, "OS", "resource://gre/modules/osfile.jsm");
+
+const HOTRELOAD_PREF = "devtools.loader.hotreload";
+
+function resolveResourcePath(uri) {
+ const handler = Services.io.getProtocolHandler("resource")
+ .QueryInterface(Ci.nsIResProtocolHandler);
+ const resolved = handler.resolveURI(Services.io.newURI(uri, null, null));
+ return resolved.replace(/file:\/\//, "");
+}
+
+function findSourceDir(path) {
+ if (path === "" || path === "/") {
+ return Promise.resolve(null);
+ }
+
+ return OS.File.exists(
+ OS.Path.join(path, "devtools/client/shared/file-watcher.js")
+ ).then(exists => {
+ if (exists) {
+ return path;
+ }
+ return findSourceDir(OS.Path.dirname(path));
+ });
+}
+
+let worker = null;
+const onPrefChange = function () {
+ // We need to figure out a src dir to watch. These are the actual
+ // files the user is working with, not the files in the obj dir. We
+ // do this by walking up the filesystem and looking for the devtools
+ // directories, and falling back to the raw path. This means none of
+ // this will work for users who store their obj dirs outside of the
+ // src dir.
+ //
+ // We take care not to mess with the `devtoolsPath` if that's what
+ // we end up using, because it might be intentionally mapped to a
+ // specific place on the filesystem for loading devtools externally.
+ //
+ // `devtoolsPath` is currently the devtools directory inside of the
+ // obj dir, and we search for `devtools/client`, so go up 2 levels
+ // to skip that devtools dir and start searching for the src dir.
+ if (Services.prefs.getBoolPref(HOTRELOAD_PREF) && !worker) {
+ const devtoolsPath = resolveResourcePath("resource://devtools")
+ .replace(/\/$/, "");
+ const searchPoint = OS.Path.dirname(OS.Path.dirname(devtoolsPath));
+ findSourceDir(searchPoint)
+ .then(srcPath => {
+ const rootPath = srcPath ? OS.Path.join(srcPath, "devtools")
+ : devtoolsPath;
+ const watchPath = OS.Path.join(rootPath, "client");
+ const { watchFiles } = require("devtools/client/shared/file-watcher");
+ worker = watchFiles(watchPath, path => {
+ let relativePath = path.replace(rootPath + "/", "");
+ module.exports.emit("file-changed", relativePath, path);
+ });
+ });
+ } else if (worker) {
+ worker.terminate();
+ worker = null;
+ }
+};
+
+Services.prefs.addObserver(HOTRELOAD_PREF, {
+ observe: onPrefChange
+}, false);
+onPrefChange();
+
+EventEmitter.decorate(module.exports);
diff --git a/devtools/client/shared/doorhanger.js b/devtools/client/shared/doorhanger.js
new file mode 100644
index 000000000..fc2767966
--- /dev/null
+++ b/devtools/client/shared/doorhanger.js
@@ -0,0 +1,164 @@
+/* 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, Cc } = require("chrome");
+const Services = require("Services");
+const { DOMHelpers } = require("resource://devtools/client/shared/DOMHelpers.jsm");
+const { Task } = require("devtools/shared/task");
+const defer = require("devtools/shared/defer");
+const { getMostRecentBrowserWindow } = require("sdk/window/utils");
+
+const XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+const DEV_EDITION_PROMO_URL = "chrome://devtools/content/framework/dev-edition-promo/dev-edition-promo.xul";
+const DEV_EDITION_PROMO_ENABLED_PREF = "devtools.devedition.promo.enabled";
+const DEV_EDITION_PROMO_SHOWN_PREF = "devtools.devedition.promo.shown";
+const DEV_EDITION_PROMO_URL_PREF = "devtools.devedition.promo.url";
+const LOCALE = Cc["@mozilla.org/chrome/chrome-registry;1"]
+ .getService(Ci.nsIXULChromeRegistry)
+ .getSelectedLocale("global");
+
+/**
+ * Only show Dev Edition promo if it's enabled (beta channel),
+ * if it has not been shown before, and it's a locale build
+ * for `en-US`
+ */
+function shouldDevEditionPromoShow() {
+ return Services.prefs.getBoolPref(DEV_EDITION_PROMO_ENABLED_PREF) &&
+ !Services.prefs.getBoolPref(DEV_EDITION_PROMO_SHOWN_PREF) &&
+ LOCALE === "en-US";
+}
+
+var TYPES = {
+ // The Developer Edition promo doorhanger, called by
+ // opening the toolbox, browser console, WebIDE, or responsive design mode
+ // in Beta releases. Only displayed once per profile.
+ deveditionpromo: {
+ predicate: shouldDevEditionPromoShow,
+ success: () => {
+ return Services.prefs.setBoolPref(DEV_EDITION_PROMO_SHOWN_PREF, true);
+ },
+ action: () => {
+ let url = Services.prefs.getCharPref(DEV_EDITION_PROMO_URL_PREF);
+ getGBrowser().selectedTab = getGBrowser().addTab(url);
+ },
+ url: DEV_EDITION_PROMO_URL
+ }
+};
+
+var panelAttrs = {
+ orient: "vertical",
+ hidden: "false",
+ consumeoutsideclicks: "true",
+ noautofocus: "true",
+ align: "start",
+ role: "alert"
+};
+
+/**
+ * Helper to call a doorhanger, defined in `TYPES`, with defined conditions,
+ * success handlers and loads its own XUL in a frame. Takes an object with
+ * several properties:
+ *
+ * @param {XULWindow} window
+ * The window that should house the doorhanger.
+ * @param {String} type
+ * The type of doorhanger to be displayed is, using the `TYPES`
+ * definition.
+ * @param {String} selector
+ * The selector that the doorhanger should be appended to within
+ * `window`. Defaults to a XUL Document's `window` element.
+ */
+exports.showDoorhanger = Task.async(function* ({ window, type, anchor }) {
+ let { predicate, success, url, action } = TYPES[type];
+ // Abort if predicate fails
+ if (!predicate()) {
+ return;
+ }
+
+ // Call success function to set preferences/cleanup immediately,
+ // so if triggered multiple times, only happens once (Windows/Linux)
+ success();
+
+ // Wait 200ms to prevent flickering where the popup is displayed
+ // before the underlying window (Windows 7, 64bit)
+ yield wait(200);
+
+ let document = window.document;
+
+ let panel = document.createElementNS(XULNS, "panel");
+ let frame = document.createElementNS(XULNS, "iframe");
+ let parentEl = document.querySelector("window");
+
+ frame.setAttribute("src", url);
+ let close = () => parentEl.removeChild(panel);
+
+ setDoorhangerStyle(panel, frame);
+
+ panel.appendChild(frame);
+ parentEl.appendChild(panel);
+
+ yield onFrameLoad(frame);
+
+ panel.openPopup(anchor);
+
+ let closeBtn = frame.contentDocument.querySelector("#close");
+ if (closeBtn) {
+ closeBtn.addEventListener("click", close);
+ }
+
+ let goBtn = frame.contentDocument.querySelector("#go");
+ if (goBtn) {
+ goBtn.addEventListener("click", () => {
+ if (action) {
+ action();
+ }
+ close();
+ });
+ }
+});
+
+function setDoorhangerStyle(panel, frame) {
+ Object.keys(panelAttrs).forEach(prop => {
+ return panel.setAttribute(prop, panelAttrs[prop]);
+ });
+ panel.style.margin = "20px";
+ panel.style.borderRadius = "5px";
+ panel.style.border = "none";
+ panel.style.MozAppearance = "none";
+ panel.style.backgroundColor = "transparent";
+
+ frame.style.borderRadius = "5px";
+ frame.setAttribute("flex", "1");
+ frame.setAttribute("width", "450");
+ frame.setAttribute("height", "179");
+}
+
+function onFrameLoad(frame) {
+ let { resolve, promise } = defer();
+
+ if (frame.contentWindow) {
+ let domHelper = new DOMHelpers(frame.contentWindow);
+ domHelper.onceDOMReady(resolve);
+ } else {
+ let callback = () => {
+ frame.removeEventListener("DOMContentLoaded", callback);
+ resolve();
+ };
+ frame.addEventListener("DOMContentLoaded", callback);
+ }
+
+ return promise;
+}
+
+function getGBrowser() {
+ return getMostRecentBrowserWindow().gBrowser;
+}
+
+function wait(n) {
+ let { resolve, promise } = defer();
+ setTimeout(resolve, n);
+ return promise;
+}
diff --git a/devtools/client/shared/file-watcher-worker.js b/devtools/client/shared/file-watcher-worker.js
new file mode 100644
index 000000000..c9edd6127
--- /dev/null
+++ b/devtools/client/shared/file-watcher-worker.js
@@ -0,0 +1,81 @@
+/* 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";
+
+/* eslint-env worker */
+/* global OS */
+importScripts("resource://gre/modules/osfile.jsm");
+
+const modifiedTimes = new Map();
+
+function gatherFiles(path, fileRegex) {
+ let files = [];
+ const iterator = new OS.File.DirectoryIterator(path);
+
+ try {
+ for (let child in iterator) {
+ // Don't descend into test directories. Saves us some time and
+ // there's no reason to.
+ if (child.isDir && !child.path.endsWith("/test")) {
+ files = files.concat(gatherFiles(child.path, fileRegex));
+ } else if (child.path.match(fileRegex)) {
+ let info;
+ try {
+ info = OS.File.stat(child.path);
+ } catch (e) {
+ // Just ignore it.
+ continue;
+ }
+
+ files.push(child.path);
+ modifiedTimes.set(child.path, info.lastModificationDate.getTime());
+ }
+ }
+ } finally {
+ iterator.close();
+ }
+
+ return files;
+}
+
+function scanFiles(files, onChangedFile) {
+ files.forEach(file => {
+ let info;
+ try {
+ info = OS.File.stat(file);
+ } catch (e) {
+ // Just ignore it. It was probably deleted.
+ return;
+ }
+
+ const lastTime = modifiedTimes.get(file);
+
+ if (info.lastModificationDate.getTime() > lastTime) {
+ modifiedTimes.set(file, info.lastModificationDate.getTime());
+ onChangedFile(file);
+ }
+ });
+}
+
+onmessage = function (event) {
+ const { path, fileRegex } = event.data;
+
+ const info = OS.File.stat(path);
+ if (!info.isDir) {
+ throw new Error("Watcher expects a directory as root path");
+ }
+
+ // We get a list of all the files upfront, which means we don't
+ // support adding new files. But you need to rebuild Firefox when
+ // adding a new file anyway.
+ const files = gatherFiles(path, fileRegex || /.*/);
+
+ // Every second, scan for file changes by stat-ing each of them and
+ // comparing modification time.
+ setInterval(() => {
+ scanFiles(files, changedFile => {
+ postMessage({ path: changedFile });
+ });
+ }, 1000);
+};
diff --git a/devtools/client/shared/file-watcher.js b/devtools/client/shared/file-watcher.js
new file mode 100644
index 000000000..7799422f1
--- /dev/null
+++ b/devtools/client/shared/file-watcher.js
@@ -0,0 +1,28 @@
+/* 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 { ChromeWorker } = require("chrome");
+
+function watchFiles(path, onFileChanged) {
+ const watchWorker = new ChromeWorker(
+ "resource://devtools/client/shared/file-watcher-worker.js"
+ );
+
+ watchWorker.onmessage = event => {
+ // We need to turn a local path back into a resource URI (or
+ // chrome). This means that this system will only work when built
+ // files are symlinked, so that these URIs actually read from
+ // local sources. There might be a better way to do this.
+ const { path: newPath } = event.data;
+ onFileChanged(newPath);
+ };
+
+ watchWorker.postMessage({
+ path,
+ fileRegex: /\.(js|css|svg|png)$/
+ });
+ return watchWorker;
+}
+exports.watchFiles = watchFiles;
diff --git a/devtools/client/shared/frame-script-utils.js b/devtools/client/shared/frame-script-utils.js
new file mode 100644
index 000000000..3db7ed9ab
--- /dev/null
+++ b/devtools/client/shared/frame-script-utils.js
@@ -0,0 +1,206 @@
+/* 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/. */
+
+/* eslint-env browser */
+/* global addMessageListener, sendAsyncMessage, content */
+"use strict";
+var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+const {require, loader} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const defer = require("devtools/shared/defer");
+const { Task } = require("devtools/shared/task");
+
+loader.lazyGetter(this, "nsIProfilerModule", () => {
+ return Cc["@mozilla.org/tools/profiler;1"].getService(Ci.nsIProfiler);
+});
+
+addMessageListener("devtools:test:history", function ({ data }) {
+ content.history[data.direction]();
+});
+
+addMessageListener("devtools:test:navigate", function ({ data }) {
+ content.location = data.location;
+});
+
+addMessageListener("devtools:test:reload", function ({ data }) {
+ data = data || {};
+ content.location.reload(data.forceget);
+});
+
+addMessageListener("devtools:test:console", function ({ data }) {
+ let { method, args, id } = data;
+ content.console[method].apply(content.console, args);
+ sendAsyncMessage("devtools:test:console:response", { id });
+});
+
+/**
+ * Performs a single XMLHttpRequest and returns a promise that resolves once
+ * the request has loaded.
+ *
+ * @param Object data
+ * { method: the request method (default: "GET"),
+ * url: the url to request (default: content.location.href),
+ * body: the request body to send (default: ""),
+ * nocache: append an unique token to the query string (default: true),
+ * requestHeaders: set request headers (default: none)
+ * }
+ *
+ * @return Promise A promise that's resolved with object
+ * { status: XMLHttpRequest.status,
+ * response: XMLHttpRequest.response }
+ *
+ */
+function promiseXHR(data) {
+ let xhr = new content.XMLHttpRequest();
+
+ let method = data.method || "GET";
+ let url = data.url || content.location.href;
+ let body = data.body || "";
+
+ if (data.nocache) {
+ url += "?devtools-cachebust=" + Math.random();
+ }
+
+ let deferred = defer();
+ xhr.addEventListener("loadend", function loadend(event) {
+ xhr.removeEventListener("loadend", loadend);
+ deferred.resolve({ status: xhr.status, response: xhr.response });
+ });
+
+ xhr.open(method, url);
+
+ // Set request headers
+ if (data.requestHeaders) {
+ data.requestHeaders.forEach(header => {
+ xhr.setRequestHeader(header.name, header.value);
+ });
+ }
+
+ xhr.send(body);
+ return deferred.promise;
+}
+
+/**
+ * Performs XMLHttpRequest request(s) in the context of the page. The data
+ * parameter can be either a single object or an array of objects described
+ * below. The requests will be performed one at a time in the order they appear
+ * in the data.
+ *
+ * The objects should have following form (any of them can be omitted; defaults
+ * shown below):
+ * {
+ * method: "GET",
+ * url: content.location.href,
+ * body: "",
+ * nocache: true, // Adds a cache busting random token to the URL,
+ * requestHeaders: [{
+ * name: "Content-Type",
+ * value: "application/json"
+ * }]
+ * }
+ *
+ * The handler will respond with devtools:test:xhr message after all requests
+ * have finished. Following data will be available for each requests
+ * (in the same order as requests):
+ * {
+ * status: XMLHttpRequest.status
+ * response: XMLHttpRequest.response
+ * }
+ */
+addMessageListener("devtools:test:xhr", Task.async(function* ({ data }) {
+ let requests = Array.isArray(data) ? data : [data];
+ let responses = [];
+
+ for (let request of requests) {
+ let response = yield promiseXHR(request);
+ responses.push(response);
+ }
+
+ sendAsyncMessage("devtools:test:xhr", responses);
+}));
+
+addMessageListener("devtools:test:profiler", function ({ data }) {
+ let { method, args, id } = data;
+ let result = nsIProfilerModule[method](...args);
+ sendAsyncMessage("devtools:test:profiler:response", {
+ data: result,
+ id: id
+ });
+});
+
+// To eval in content, look at `evalInDebuggee` in the shared-head.js.
+addMessageListener("devtools:test:eval", function ({ data }) {
+ sendAsyncMessage("devtools:test:eval:response", {
+ value: content.eval(data.script),
+ id: data.id
+ });
+});
+
+addEventListener("load", function () {
+ sendAsyncMessage("devtools:test:load");
+}, true);
+
+/**
+ * Set a given style property value on a node.
+ * @param {Object} data
+ * - {String} selector The CSS selector to get the node (can be a "super"
+ * selector).
+ * - {String} propertyName The name of the property to set.
+ * - {String} propertyValue The value for the property.
+ */
+addMessageListener("devtools:test:setStyle", function (msg) {
+ let {selector, propertyName, propertyValue} = msg.data;
+ let node = superQuerySelector(selector);
+ if (!node) {
+ return;
+ }
+
+ node.style[propertyName] = propertyValue;
+
+ sendAsyncMessage("devtools:test:setStyle");
+});
+
+/**
+ * Set a given attribute value on a node.
+ * @param {Object} data
+ * - {String} selector The CSS selector to get the node (can be a "super"
+ * selector).
+ * - {String} attributeName The name of the attribute to set.
+ * - {String} attributeValue The value for the attribute.
+ */
+addMessageListener("devtools:test:setAttribute", function (msg) {
+ let {selector, attributeName, attributeValue} = msg.data;
+ let node = superQuerySelector(selector);
+ if (!node) {
+ return;
+ }
+
+ node.setAttribute(attributeName, attributeValue);
+
+ sendAsyncMessage("devtools:test:setAttribute");
+});
+
+/**
+ * Like document.querySelector but can go into iframes too.
+ * ".container iframe || .sub-container div" will first try to find the node
+ * matched by ".container iframe" in the root document, then try to get the
+ * content document inside it, and then try to match ".sub-container div" inside
+ * this document.
+ * Any selector coming before the || separator *MUST* match a frame node.
+ * @param {String} superSelector.
+ * @return {DOMNode} The node, or null if not found.
+ */
+function superQuerySelector(superSelector, root = content.document) {
+ let frameIndex = superSelector.indexOf("||");
+ if (frameIndex === -1) {
+ return root.querySelector(superSelector);
+ }
+ let rootSelector = superSelector.substring(0, frameIndex).trim();
+ let childSelector = superSelector.substring(frameIndex + 2).trim();
+ root = root.querySelector(rootSelector);
+ if (!root || !root.contentWindow) {
+ return null;
+ }
+
+ return superQuerySelector(childSelector, root.contentWindow.document);
+}
diff --git a/devtools/client/shared/getjson.js b/devtools/client/shared/getjson.js
new file mode 100644
index 000000000..3c4d48e07
--- /dev/null
+++ b/devtools/client/shared/getjson.js
@@ -0,0 +1,76 @@
+/* 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} = require("chrome");
+const defer = require("devtools/shared/defer");
+const promise = require("promise");
+const Services = require("Services");
+
+loader.lazyRequireGetter(this, "asyncStorage", "devtools/shared/async-storage");
+
+const XMLHttpRequest = CC("@mozilla.org/xmlextras/xmlhttprequest;1");
+
+/**
+ * Downloads and caches a JSON file from an URL given by a pref.
+ *
+ * @param {String} prefName
+ * The preference for the target URL
+ *
+ * @return {Promise}
+ * - Resolved with the JSON object in case of successful request
+ * or cache hit
+ * - Rejected with an error message in case of failure
+ */
+exports.getJSON = function (prefName) {
+ let deferred = defer();
+ let xhr = new XMLHttpRequest();
+
+ // We used to store cached data in preferences, but now we use asyncStorage
+ // Migration step: if it still exists, move this now useless preference in its
+ // new location and clear it
+ if (Services.prefs.prefHasUserValue(prefName + "_cache")) {
+ let json = Services.prefs.getCharPref(prefName + "_cache");
+ asyncStorage.setItem(prefName + "_cache", json).catch(function (e) {
+ // Could not move the cache, let's log the error but continue
+ console.error(e);
+ });
+ Services.prefs.clearUserPref(prefName + "_cache");
+ }
+
+ function readFromStorage(networkError) {
+ asyncStorage.getItem(prefName + "_cache").then(function (json) {
+ if (!json) {
+ return promise.reject("Empty cache for " + prefName);
+ }
+ return deferred.resolve(json);
+ }).catch(function (e) {
+ deferred.reject("JSON not available, CDN error: " + networkError +
+ ", storage error: " + e);
+ });
+ }
+
+ xhr.onload = () => {
+ try {
+ let json = JSON.parse(xhr.responseText);
+ asyncStorage.setItem(prefName + "_cache", json).catch(function (e) {
+ // Could not update cache, let's log the error but continue
+ console.error(e);
+ });
+ deferred.resolve(json);
+ } catch (e) {
+ readFromStorage(e);
+ }
+ };
+
+ xhr.onerror = (e) => {
+ readFromStorage(e);
+ };
+
+ xhr.open("get", Services.prefs.getCharPref(prefName));
+ xhr.send();
+
+ return deferred.promise;
+};
diff --git a/devtools/client/shared/inplace-editor.js b/devtools/client/shared/inplace-editor.js
new file mode 100644
index 000000000..652163233
--- /dev/null
+++ b/devtools/client/shared/inplace-editor.js
@@ -0,0 +1,1566 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set 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/. */
+
+/**
+ * Basic use:
+ * let spanToEdit = document.getElementById("somespan");
+ *
+ * editableField({
+ * element: spanToEdit,
+ * done: function(value, commit, direction) {
+ * if (commit) {
+ * spanToEdit.textContent = value;
+ * }
+ * },
+ * trigger: "dblclick"
+ * });
+ *
+ * See editableField() for more options.
+ */
+
+"use strict";
+
+const Services = require("Services");
+const focusManager = Services.focus;
+const {KeyCodes} = require("devtools/client/shared/keycodes");
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const CONTENT_TYPES = {
+ PLAIN_TEXT: 0,
+ CSS_VALUE: 1,
+ CSS_MIXED: 2,
+ CSS_PROPERTY: 3,
+};
+
+// The limit of 500 autocomplete suggestions should not be reached but is kept
+// for safety.
+const MAX_POPUP_ENTRIES = 500;
+
+const FOCUS_FORWARD = focusManager.MOVEFOCUS_FORWARD;
+const FOCUS_BACKWARD = focusManager.MOVEFOCUS_BACKWARD;
+
+const EventEmitter = require("devtools/shared/event-emitter");
+const { findMostRelevantCssPropertyIndex } = require("./suggestion-picker");
+
+/**
+ * Helper to check if the provided key matches one of the expected keys.
+ * Keys will be prefixed with DOM_VK_ and should match a key in KeyCodes.
+ *
+ * @param {String} key
+ * the key to check (can be a keyCode).
+ * @param {...String} keys
+ * list of possible keys allowed.
+ * @return {Boolean} true if the key matches one of the keys.
+ */
+function isKeyIn(key, ...keys) {
+ return keys.some(expectedKey => {
+ return key === KeyCodes["DOM_VK_" + expectedKey];
+ });
+}
+
+/**
+ * Mark a span editable. |editableField| will listen for the span to
+ * be focused and create an InlineEditor to handle text input.
+ * Changes will be committed when the InlineEditor's input is blurred
+ * or dropped when the user presses escape.
+ *
+ * @param {Object} options
+ * Options for the editable field, including:
+ * {Element} element:
+ * (required) The span to be edited on focus.
+ * {Function} canEdit:
+ * Will be called before creating the inplace editor. Editor
+ * won't be created if canEdit returns false.
+ * {Function} start:
+ * Will be called when the inplace editor is initialized.
+ * {Function} change:
+ * Will be called when the text input changes. Will be called
+ * with the current value of the text input.
+ * {Function} done:
+ * Called when input is committed or blurred. Called with
+ * current value, a boolean telling the caller whether to
+ * commit the change, and the direction of the next element to be
+ * selected. Direction may be one of Services.focus.MOVEFOCUS_FORWARD,
+ * Services.focus.MOVEFOCUS_BACKWARD, or null (no movement).
+ * This function is called before the editor has been torn down.
+ * {Function} destroy:
+ * Called when the editor is destroyed and has been torn down.
+ * {Function} contextMenu:
+ * Called when the user triggers a contextmenu event on the input.
+ * {Object} advanceChars:
+ * This can be either a string or a function.
+ * If it is a string, then if any characters in it are typed,
+ * focus will advance to the next element.
+ * Otherwise, if it is a function, then the function will
+ * be called with three arguments: a key code, the current text,
+ * and the insertion point. If the function returns true,
+ * then the focus advance takes place. If it returns false,
+ * then the character is inserted instead.
+ * {Boolean} stopOnReturn:
+ * If true, the return key will not advance the editor to the next
+ * focusable element.
+ * {Boolean} stopOnTab:
+ * If true, the tab key will not advance the editor to the next
+ * focusable element.
+ * {Boolean} stopOnShiftTab:
+ * If true, shift tab will not advance the editor to the previous
+ * focusable element.
+ * {String} trigger: The DOM event that should trigger editing,
+ * defaults to "click"
+ * {Boolean} multiline: Should the editor be a multiline textarea?
+ * defaults to false
+ * {Function or Number} maxWidth:
+ * Should the editor wrap to remain below the provided max width. Only
+ * available if multiline is true. If a function is provided, it will be
+ * called when replacing the element by the inplace input.
+ * {Boolean} trimOutput: Should the returned string be trimmed?
+ * defaults to true
+ * {Boolean} preserveTextStyles: If true, do not copy text-related styles
+ * from `element` to the new input.
+ * defaults to false
+ * {Object} cssProperties: An instance of CSSProperties.
+ */
+function editableField(options) {
+ return editableItem(options, function (element, event) {
+ if (!options.element.inplaceEditor) {
+ new InplaceEditor(options, event);
+ }
+ });
+}
+
+exports.editableField = editableField;
+
+/**
+ * Handle events for an element that should respond to
+ * clicks and sit in the editing tab order, and call
+ * a callback when it is activated.
+ *
+ * @param {Object} options
+ * The options for this editor, including:
+ * {Element} element: The DOM element.
+ * {String} trigger: The DOM event that should trigger editing,
+ * defaults to "click"
+ * @param {Function} callback
+ * Called when the editor is activated.
+ * @return {Function} function which calls callback
+ */
+function editableItem(options, callback) {
+ let trigger = options.trigger || "click";
+ let element = options.element;
+ element.addEventListener(trigger, function (evt) {
+ if (evt.target.nodeName !== "a") {
+ let win = this.ownerDocument.defaultView;
+ let selection = win.getSelection();
+ if (trigger != "click" || selection.isCollapsed) {
+ callback(element, evt);
+ }
+ evt.stopPropagation();
+ }
+ }, false);
+
+ // If focused by means other than a click, start editing by
+ // pressing enter or space.
+ element.addEventListener("keypress", function (evt) {
+ if (isKeyIn(evt.keyCode, "RETURN") || isKeyIn(evt.charCode, "SPACE")) {
+ callback(element);
+ }
+ }, true);
+
+ // Ugly workaround - the element is focused on mousedown but
+ // the editor is activated on click/mouseup. This leads
+ // to an ugly flash of the focus ring before showing the editor.
+ // So hide the focus ring while the mouse is down.
+ element.addEventListener("mousedown", function (evt) {
+ if (evt.target.nodeName !== "a") {
+ let cleanup = function () {
+ element.style.removeProperty("outline-style");
+ element.removeEventListener("mouseup", cleanup, false);
+ element.removeEventListener("mouseout", cleanup, false);
+ };
+ element.style.setProperty("outline-style", "none");
+ element.addEventListener("mouseup", cleanup, false);
+ element.addEventListener("mouseout", cleanup, false);
+ }
+ }, false);
+
+ // Mark the element editable field for tab
+ // navigation while editing.
+ element._editable = true;
+
+ // Save the trigger type so we can dispatch this later
+ element._trigger = trigger;
+
+ // Add button semantics to the element, to indicate that it can be activated.
+ element.setAttribute("role", "button");
+
+ return function turnOnEditMode() {
+ callback(element);
+ };
+}
+
+exports.editableItem = editableItem;
+
+/*
+ * Various API consumers (especially tests) sometimes want to grab the
+ * inplaceEditor expando off span elements. However, when each global has its
+ * own compartment, those expandos live on Xray wrappers that are only visible
+ * within this JSM. So we provide a little workaround here.
+ */
+
+function getInplaceEditorForSpan(span) {
+ return span.inplaceEditor;
+}
+
+exports.getInplaceEditorForSpan = getInplaceEditorForSpan;
+
+function InplaceEditor(options, event) {
+ this.elt = options.element;
+ let doc = this.elt.ownerDocument;
+ this.doc = doc;
+ this.elt.inplaceEditor = this;
+ this.cssProperties = options.cssProperties;
+ this.change = options.change;
+ this.done = options.done;
+ this.contextMenu = options.contextMenu;
+ this.destroy = options.destroy;
+ this.initial = options.initial ? options.initial : this.elt.textContent;
+ this.multiline = options.multiline || false;
+ this.maxWidth = options.maxWidth;
+ if (typeof this.maxWidth == "function") {
+ this.maxWidth = this.maxWidth();
+ }
+
+ this.trimOutput = options.trimOutput === undefined
+ ? true
+ : !!options.trimOutput;
+ this.stopOnShiftTab = !!options.stopOnShiftTab;
+ this.stopOnTab = !!options.stopOnTab;
+ this.stopOnReturn = !!options.stopOnReturn;
+ this.contentType = options.contentType || CONTENT_TYPES.PLAIN_TEXT;
+ this.property = options.property;
+ this.popup = options.popup;
+ this.preserveTextStyles = options.preserveTextStyles === undefined
+ ? false
+ : !!options.preserveTextStyles;
+
+ this._onBlur = this._onBlur.bind(this);
+ this._onWindowBlur = this._onWindowBlur.bind(this);
+ this._onKeyPress = this._onKeyPress.bind(this);
+ this._onInput = this._onInput.bind(this);
+ this._onKeyup = this._onKeyup.bind(this);
+ this._onAutocompletePopupClick = this._onAutocompletePopupClick.bind(this);
+ this._onContextMenu = this._onContextMenu.bind(this);
+
+ this._createInput();
+
+ // Hide the provided element and add our editor.
+ this.originalDisplay = this.elt.style.display;
+ this.elt.style.display = "none";
+ this.elt.parentNode.insertBefore(this.input, this.elt);
+
+ // After inserting the input to have all CSS styles applied, start autosizing.
+ this._autosize();
+
+ this.inputCharDimensions = this._getInputCharDimensions();
+ // Pull out character codes for advanceChars, listing the
+ // characters that should trigger a blur.
+ if (typeof options.advanceChars === "function") {
+ this._advanceChars = options.advanceChars;
+ } else {
+ let advanceCharcodes = {};
+ let advanceChars = options.advanceChars || "";
+ for (let i = 0; i < advanceChars.length; i++) {
+ advanceCharcodes[advanceChars.charCodeAt(i)] = true;
+ }
+ this._advanceChars = charCode => charCode in advanceCharcodes;
+ }
+
+ this.input.focus();
+
+ if (typeof options.selectAll == "undefined" || options.selectAll) {
+ this.input.select();
+ }
+
+ if (this.contentType == CONTENT_TYPES.CSS_VALUE && this.input.value == "") {
+ this._maybeSuggestCompletion(false);
+ }
+
+ this.input.addEventListener("blur", this._onBlur, false);
+ this.input.addEventListener("keypress", this._onKeyPress, false);
+ this.input.addEventListener("input", this._onInput, false);
+ this.input.addEventListener("dblclick", this._stopEventPropagation, false);
+ this.input.addEventListener("click", this._stopEventPropagation, false);
+ this.input.addEventListener("mousedown", this._stopEventPropagation, false);
+ this.input.addEventListener("contextmenu", this._onContextMenu, false);
+ this.doc.defaultView.addEventListener("blur", this._onWindowBlur, false);
+
+ this.validate = options.validate;
+
+ if (this.validate) {
+ this.input.addEventListener("keyup", this._onKeyup, false);
+ }
+
+ this._updateSize();
+
+ EventEmitter.decorate(this);
+
+ if (options.start) {
+ options.start(this, event);
+ }
+}
+
+exports.InplaceEditor = InplaceEditor;
+
+InplaceEditor.CONTENT_TYPES = CONTENT_TYPES;
+
+InplaceEditor.prototype = {
+
+ get currentInputValue() {
+ let val = this.trimOutput ? this.input.value.trim() : this.input.value;
+ return val;
+ },
+
+ _createInput: function () {
+ this.input =
+ this.doc.createElementNS(HTML_NS, this.multiline ? "textarea" : "input");
+ this.input.inplaceEditor = this;
+
+ if (this.multiline) {
+ // Hide the textarea resize handle.
+ this.input.style.resize = "none";
+ this.input.style.overflow = "hidden";
+ }
+
+ this.input.classList.add("styleinspector-propertyeditor");
+ this.input.value = this.initial;
+ if (!this.preserveTextStyles) {
+ copyTextStyles(this.elt, this.input);
+ }
+ },
+
+ /**
+ * Get rid of the editor.
+ */
+ _clear: function () {
+ if (!this.input) {
+ // Already cleared.
+ return;
+ }
+
+ this.input.removeEventListener("blur", this._onBlur, false);
+ this.input.removeEventListener("keypress", this._onKeyPress, false);
+ this.input.removeEventListener("keyup", this._onKeyup, false);
+ this.input.removeEventListener("input", this._onInput, false);
+ this.input.removeEventListener("dblclick", this._stopEventPropagation, false);
+ this.input.removeEventListener("click", this._stopEventPropagation, false);
+ this.input.removeEventListener("mousedown", this._stopEventPropagation, false);
+ this.input.removeEventListener("contextmenu", this._onContextMenu, false);
+ this.doc.defaultView.removeEventListener("blur", this._onWindowBlur, false);
+
+ this._stopAutosize();
+
+ this.elt.style.display = this.originalDisplay;
+
+ if (this.doc.activeElement == this.input) {
+ this.elt.focus();
+ }
+
+ this.input.remove();
+ this.input = null;
+
+ delete this.elt.inplaceEditor;
+ delete this.elt;
+
+ if (this.destroy) {
+ this.destroy();
+ }
+ },
+
+ /**
+ * Keeps the editor close to the size of its input string. This is pretty
+ * crappy, suggestions for improvement welcome.
+ */
+ _autosize: function () {
+ // Create a hidden, absolutely-positioned span to measure the text
+ // in the input. Boo.
+
+ // We can't just measure the original element because a) we don't
+ // change the underlying element's text ourselves (we leave that
+ // up to the client), and b) without tweaking the style of the
+ // original element, it might wrap differently or something.
+ this._measurement =
+ this.doc.createElementNS(HTML_NS, this.multiline ? "pre" : "span");
+ this._measurement.className = "autosizer";
+ this.elt.parentNode.appendChild(this._measurement);
+ let style = this._measurement.style;
+ style.visibility = "hidden";
+ style.position = "absolute";
+ style.top = "0";
+ style.left = "0";
+
+ if (this.multiline) {
+ style.whiteSpace = "pre-wrap";
+ style.wordWrap = "break-word";
+ if (this.maxWidth) {
+ style.maxWidth = this.maxWidth + "px";
+ // Use position fixed to measure dimensions without any influence from
+ // the container of the editor.
+ style.position = "fixed";
+ }
+ }
+
+ copyAllStyles(this.input, this._measurement);
+ this._updateSize();
+ },
+
+ /**
+ * Clean up the mess created by _autosize().
+ */
+ _stopAutosize: function () {
+ if (!this._measurement) {
+ return;
+ }
+ this._measurement.remove();
+ delete this._measurement;
+ },
+
+ /**
+ * Size the editor to fit its current contents.
+ */
+ _updateSize: function () {
+ // Replace spaces with non-breaking spaces. Otherwise setting
+ // the span's textContent will collapse spaces and the measurement
+ // will be wrong.
+ let content = this.input.value;
+ let unbreakableSpace = "\u00a0";
+
+ // Make sure the content is not empty.
+ if (content === "") {
+ content = unbreakableSpace;
+ }
+
+ // If content ends with a new line, add a blank space to force the autosize
+ // element to adapt its height.
+ if (content.lastIndexOf("\n") === content.length - 1) {
+ content = content + unbreakableSpace;
+ }
+
+ if (!this.multiline) {
+ content = content.replace(/ /g, unbreakableSpace);
+ }
+
+ this._measurement.textContent = content;
+
+ // Do not use offsetWidth: it will round floating width values.
+ let width = this._measurement.getBoundingClientRect().width + 2;
+ if (this.multiline) {
+ if (this.maxWidth) {
+ width = Math.min(this.maxWidth, width);
+ }
+ let height = this._measurement.getBoundingClientRect().height;
+ this.input.style.height = height + "px";
+ }
+ this.input.style.width = width + "px";
+ },
+
+ /**
+ * Get the width and height of a single character in the input to properly
+ * position the autocompletion popup.
+ */
+ _getInputCharDimensions: function () {
+ // Just make the text content to be 'x' to get the width and height of any
+ // character in a monospace font.
+ this._measurement.textContent = "x";
+ let width = this._measurement.clientWidth;
+ let height = this._measurement.clientHeight;
+ return { width, height };
+ },
+
+ /**
+ * Increment property values in rule view.
+ *
+ * @param {Number} increment
+ * The amount to increase/decrease the property value.
+ * @return {Boolean} true if value has been incremented.
+ */
+ _incrementValue: function (increment) {
+ let value = this.input.value;
+ let selectionStart = this.input.selectionStart;
+ let selectionEnd = this.input.selectionEnd;
+
+ let newValue = this._incrementCSSValue(value, increment, selectionStart,
+ selectionEnd);
+
+ if (!newValue) {
+ return false;
+ }
+
+ this.input.value = newValue.value;
+ this.input.setSelectionRange(newValue.start, newValue.end);
+ this._doValidation();
+
+ // Call the user's change handler if available.
+ if (this.change) {
+ this.change(this.currentInputValue);
+ }
+
+ return true;
+ },
+
+ /**
+ * Increment the property value based on the property type.
+ *
+ * @param {String} value
+ * Property value.
+ * @param {Number} increment
+ * Amount to increase/decrease the property value.
+ * @param {Number} selStart
+ * Starting index of the value.
+ * @param {Number} selEnd
+ * Ending index of the value.
+ * @return {Object} object with properties 'value', 'start', and 'end'.
+ */
+ _incrementCSSValue: function (value, increment, selStart, selEnd) {
+ let range = this._parseCSSValue(value, selStart);
+ let type = (range && range.type) || "";
+ let rawValue = range ? value.substring(range.start, range.end) : "";
+ let preRawValue = range ? value.substr(0, range.start) : "";
+ let postRawValue = range ? value.substr(range.end) : "";
+ let info;
+
+ let incrementedValue = null, selection;
+ if (type === "num") {
+ if (rawValue == "0") {
+ info = {};
+ info.units = this._findCompatibleUnit(preRawValue, postRawValue);
+ }
+
+ let newValue = this._incrementRawValue(rawValue, increment, info);
+ if (newValue !== null) {
+ incrementedValue = newValue;
+ selection = [0, incrementedValue.length];
+ }
+ } else if (type === "hex") {
+ let exprOffset = selStart - range.start;
+ let exprOffsetEnd = selEnd - range.start;
+ let newValue = this._incHexColor(rawValue, increment, exprOffset,
+ exprOffsetEnd);
+ if (newValue) {
+ incrementedValue = newValue.value;
+ selection = newValue.selection;
+ }
+ } else {
+ if (type === "rgb" || type === "hsl") {
+ info = {};
+ let part = value.substring(range.start, selStart).split(",").length - 1;
+ if (part === 3) {
+ // alpha
+ info.minValue = 0;
+ info.maxValue = 1;
+ } else if (type === "rgb") {
+ info.minValue = 0;
+ info.maxValue = 255;
+ } else if (part !== 0) {
+ // hsl percentage
+ info.minValue = 0;
+ info.maxValue = 100;
+
+ // select the previous number if the selection is at the end of a
+ // percentage sign.
+ if (value.charAt(selStart - 1) === "%") {
+ --selStart;
+ }
+ }
+ }
+ return this._incrementGenericValue(value, increment, selStart, selEnd,
+ info);
+ }
+
+ if (incrementedValue === null) {
+ return null;
+ }
+
+ return {
+ value: preRawValue + incrementedValue + postRawValue,
+ start: range.start + selection[0],
+ end: range.start + selection[1]
+ };
+ },
+
+ /**
+ * Find a compatible unit to use for a CSS number value inserted between the
+ * provided beforeValue and afterValue. The compatible unit will be picked
+ * from a selection of default units corresponding to supported CSS value
+ * dimensions (distance, angle, duration).
+ *
+ * @param {String} beforeValue
+ * The string preceeding the number value in the current property
+ * value.
+ * @param {String} afterValue
+ * The string following the number value in the current property value.
+ * @return {String} a valid unit that can be used for this number value or
+ * empty string if no match could be found.
+ */
+ _findCompatibleUnit: function (beforeValue, afterValue) {
+ if (!this.property || !this.property.name) {
+ return "";
+ }
+
+ // A DOM element is used to test the validity of various units. This is to
+ // avoid having to do an async call to the server to get this information.
+ let el = this.doc.createElement("div");
+ let units = ["px", "deg", "s"];
+ for (let unit of units) {
+ let value = beforeValue + "1" + unit + afterValue;
+ el.style.setProperty(this.property.name, "");
+ el.style.setProperty(this.property.name, value);
+ if (el.style.getPropertyValue(this.property.name) !== "") {
+ return unit;
+ }
+ }
+ return "";
+ },
+
+ /**
+ * Parses the property value and type.
+ *
+ * @param {String} value
+ * Property value.
+ * @param {Number} offset
+ * Starting index of value.
+ * @return {Object} object with properties 'value', 'start', 'end', and
+ * 'type'.
+ */
+ _parseCSSValue: function (value, offset) {
+ /* eslint-disable max-len */
+ const reSplitCSS = /(url\("?[^"\)]+"?\)?)|(rgba?\([^)]*\)?)|(hsla?\([^)]*\)?)|(#[\dA-Fa-f]+)|(-?\d*\.?\d+(%|[a-z]{1,4})?)|"([^"]*)"?|'([^']*)'?|([^,\s\/!\(\)]+)|(!(.*)?)/;
+ /* eslint-enable */
+ let start = 0;
+ let m;
+
+ // retreive values from left to right until we find the one at our offset
+ while ((m = reSplitCSS.exec(value)) &&
+ (m.index + m[0].length < offset)) {
+ value = value.substr(m.index + m[0].length);
+ start += m.index + m[0].length;
+ offset -= m.index + m[0].length;
+ }
+
+ if (!m) {
+ return null;
+ }
+
+ let type;
+ if (m[1]) {
+ type = "url";
+ } else if (m[2]) {
+ type = "rgb";
+ } else if (m[3]) {
+ type = "hsl";
+ } else if (m[4]) {
+ type = "hex";
+ } else if (m[5]) {
+ type = "num";
+ }
+
+ return {
+ value: m[0],
+ start: start + m.index,
+ end: start + m.index + m[0].length,
+ type: type
+ };
+ },
+
+ /**
+ * Increment the property value for types other than
+ * number or hex, such as rgb, hsl, and file names.
+ *
+ * @param {String} value
+ * Property value.
+ * @param {Number} increment
+ * Amount to increment/decrement.
+ * @param {Number} offset
+ * Starting index of the property value.
+ * @param {Number} offsetEnd
+ * Ending index of the property value.
+ * @param {Object} info
+ * Object with details about the property value.
+ * @return {Object} object with properties 'value', 'start', and 'end'.
+ */
+ _incrementGenericValue: function (value, increment, offset, offsetEnd, info) {
+ // Try to find a number around the cursor to increment.
+ let start, end;
+ // Check if we are incrementing in a non-number context (such as a URL)
+ if (/^-?[0-9.]/.test(value.substring(offset, offsetEnd)) &&
+ !(/\d/.test(value.charAt(offset - 1) + value.charAt(offsetEnd)))) {
+ // We have a number selected, possibly with a suffix, and we are not in
+ // the disallowed case of just part of a known number being selected.
+ // Use that number.
+ start = offset;
+ end = offsetEnd;
+ } else {
+ // Parse periods as belonging to the number only if we are in a known
+ // number context. (This makes incrementing the 1 in 'image1.gif' work.)
+ let pattern = "[" + (info ? "0-9." : "0-9") + "]*";
+ let before = new RegExp(pattern + "$")
+ .exec(value.substr(0, offset))[0].length;
+ let after = new RegExp("^" + pattern)
+ .exec(value.substr(offset))[0].length;
+
+ start = offset - before;
+ end = offset + after;
+
+ // Expand the number to contain an initial minus sign if it seems
+ // free-standing.
+ if (value.charAt(start - 1) === "-" &&
+ (start - 1 === 0 || /[ (:,='"]/.test(value.charAt(start - 2)))) {
+ --start;
+ }
+ }
+
+ if (start !== end) {
+ // Include percentages as part of the incremented number (they are
+ // common enough).
+ if (value.charAt(end) === "%") {
+ ++end;
+ }
+
+ let first = value.substr(0, start);
+ let mid = value.substring(start, end);
+ let last = value.substr(end);
+
+ mid = this._incrementRawValue(mid, increment, info);
+
+ if (mid !== null) {
+ return {
+ value: first + mid + last,
+ start: start,
+ end: start + mid.length
+ };
+ }
+ }
+
+ return null;
+ },
+
+ /**
+ * Increment the property value for numbers.
+ *
+ * @param {String} rawValue
+ * Raw value to increment.
+ * @param {Number} increment
+ * Amount to increase/decrease the raw value.
+ * @param {Object} info
+ * Object with info about the property value.
+ * @return {String} the incremented value.
+ */
+ _incrementRawValue: function (rawValue, increment, info) {
+ let num = parseFloat(rawValue);
+
+ if (isNaN(num)) {
+ return null;
+ }
+
+ let number = /\d+(\.\d+)?/.exec(rawValue);
+
+ let units = rawValue.substr(number.index + number[0].length);
+ if (info && "units" in info) {
+ units = info.units;
+ }
+
+ // avoid rounding errors
+ let newValue = Math.round((num + increment) * 1000) / 1000;
+
+ if (info && "minValue" in info) {
+ newValue = Math.max(newValue, info.minValue);
+ }
+ if (info && "maxValue" in info) {
+ newValue = Math.min(newValue, info.maxValue);
+ }
+
+ newValue = newValue.toString();
+
+ return newValue + units;
+ },
+
+ /**
+ * Increment the property value for hex.
+ *
+ * @param {String} value
+ * Property value.
+ * @param {Number} increment
+ * Amount to increase/decrease the property value.
+ * @param {Number} offset
+ * Starting index of the property value.
+ * @param {Number} offsetEnd
+ * Ending index of the property value.
+ * @return {Object} object with properties 'value' and 'selection'.
+ */
+ _incHexColor: function (rawValue, increment, offset, offsetEnd) {
+ // Return early if no part of the rawValue is selected.
+ if (offsetEnd > rawValue.length && offset >= rawValue.length) {
+ return null;
+ }
+ if (offset < 1 && offsetEnd <= 1) {
+ return null;
+ }
+ // Ignore the leading #.
+ rawValue = rawValue.substr(1);
+ --offset;
+ --offsetEnd;
+
+ // Clamp the selection to within the actual value.
+ offset = Math.max(offset, 0);
+ offsetEnd = Math.min(offsetEnd, rawValue.length);
+ offsetEnd = Math.max(offsetEnd, offset);
+
+ // Normalize #ABC -> #AABBCC.
+ if (rawValue.length === 3) {
+ rawValue = rawValue.charAt(0) + rawValue.charAt(0) +
+ rawValue.charAt(1) + rawValue.charAt(1) +
+ rawValue.charAt(2) + rawValue.charAt(2);
+ offset *= 2;
+ offsetEnd *= 2;
+ }
+
+ // Normalize #ABCD -> #AABBCCDD.
+ if (rawValue.length === 4) {
+ rawValue = rawValue.charAt(0) + rawValue.charAt(0) +
+ rawValue.charAt(1) + rawValue.charAt(1) +
+ rawValue.charAt(2) + rawValue.charAt(2) +
+ rawValue.charAt(3) + rawValue.charAt(3);
+ offset *= 2;
+ offsetEnd *= 2;
+ }
+
+ if (rawValue.length !== 6 && rawValue.length !== 8) {
+ return null;
+ }
+
+ // If no selection, increment an adjacent color, preferably one to the left.
+ if (offset === offsetEnd) {
+ if (offset === 0) {
+ offsetEnd = 1;
+ } else {
+ offset = offsetEnd - 1;
+ }
+ }
+
+ // Make the selection cover entire parts.
+ offset -= offset % 2;
+ offsetEnd += offsetEnd % 2;
+
+ // Remap the increments from [0.1, 1, 10] to [1, 1, 16].
+ if (increment > -1 && increment < 1) {
+ increment = (increment < 0 ? -1 : 1);
+ }
+ if (Math.abs(increment) === 10) {
+ increment = (increment < 0 ? -16 : 16);
+ }
+
+ let isUpper = (rawValue.toUpperCase() === rawValue);
+
+ for (let pos = offset; pos < offsetEnd; pos += 2) {
+ // Increment the part in [pos, pos+2).
+ let mid = rawValue.substr(pos, 2);
+ let value = parseInt(mid, 16);
+
+ if (isNaN(value)) {
+ return null;
+ }
+
+ mid = Math.min(Math.max(value + increment, 0), 255).toString(16);
+
+ while (mid.length < 2) {
+ mid = "0" + mid;
+ }
+ if (isUpper) {
+ mid = mid.toUpperCase();
+ }
+
+ rawValue = rawValue.substr(0, pos) + mid + rawValue.substr(pos + 2);
+ }
+
+ return {
+ value: "#" + rawValue,
+ selection: [offset + 1, offsetEnd + 1]
+ };
+ },
+
+ /**
+ * Cycle through the autocompletion suggestions in the popup.
+ *
+ * @param {Boolean} reverse
+ * true to select previous item from the popup.
+ * @param {Boolean} noSelect
+ * true to not select the text after selecting the newly selectedItem
+ * from the popup.
+ */
+ _cycleCSSSuggestion: function (reverse, noSelect) {
+ // selectedItem can be null when nothing is selected in an empty editor.
+ let {label, preLabel} = this.popup.selectedItem ||
+ {label: "", preLabel: ""};
+ if (reverse) {
+ this.popup.selectPreviousItem();
+ } else {
+ this.popup.selectNextItem();
+ }
+
+ this._selectedIndex = this.popup.selectedIndex;
+ let input = this.input;
+ let pre = "";
+
+ if (input.selectionStart < input.selectionEnd) {
+ pre = input.value.slice(0, input.selectionStart);
+ } else {
+ pre = input.value.slice(0, input.selectionStart - label.length +
+ preLabel.length);
+ }
+
+ let post = input.value.slice(input.selectionEnd, input.value.length);
+ let item = this.popup.selectedItem;
+ let toComplete = item.label.slice(item.preLabel.length);
+ input.value = pre + toComplete + post;
+
+ if (!noSelect) {
+ input.setSelectionRange(pre.length, pre.length + toComplete.length);
+ } else {
+ input.setSelectionRange(pre.length + toComplete.length,
+ pre.length + toComplete.length);
+ }
+
+ this._updateSize();
+ // This emit is mainly for the purpose of making the test flow simpler.
+ this.emit("after-suggest");
+ },
+
+ /**
+ * Call the client's done handler and clear out.
+ */
+ _apply: function (event, direction) {
+ if (this._applied) {
+ return null;
+ }
+
+ this._applied = true;
+
+ if (this.done) {
+ let val = this.cancelled ? this.initial : this.currentInputValue;
+ return this.done(val, !this.cancelled, direction);
+ }
+
+ return null;
+ },
+
+ /**
+ * Hide the popup and cancel any pending popup opening.
+ */
+ _onWindowBlur: function () {
+ if (this.popup && this.popup.isOpen) {
+ this.popup.hidePopup();
+ }
+
+ if (this._openPopupTimeout) {
+ this.doc.defaultView.clearTimeout(this._openPopupTimeout);
+ }
+ },
+
+ /**
+ * Event handler called when the inplace-editor's input loses focus.
+ */
+ _onBlur: function (event) {
+ if (event && this.popup && this.popup.isOpen &&
+ this.popup.selectedIndex >= 0) {
+ this._acceptPopupSuggestion();
+ } else {
+ this._apply();
+ this._clear();
+ }
+ },
+
+ /**
+ * Event handler called by the autocomplete popup when receiving a click
+ * event.
+ */
+ _onAutocompletePopupClick: function () {
+ this._acceptPopupSuggestion();
+ },
+
+ _acceptPopupSuggestion: function () {
+ let label, preLabel;
+
+ if (this._selectedIndex === undefined) {
+ ({label, preLabel} = this.popup.getItemAtIndex(this.popup.selectedIndex));
+ } else {
+ ({label, preLabel} = this.popup.getItemAtIndex(this._selectedIndex));
+ }
+
+ let input = this.input;
+
+ let pre = "";
+
+ // CSS_MIXED needs special treatment here to make it so that
+ // multiple presses of tab will cycle through completions, but
+ // without selecting the completed text. However, this same
+ // special treatment will do the wrong thing for other editing
+ // styles.
+ if (input.selectionStart < input.selectionEnd ||
+ this.contentType !== CONTENT_TYPES.CSS_MIXED) {
+ pre = input.value.slice(0, input.selectionStart);
+ } else {
+ pre = input.value.slice(0, input.selectionStart - label.length +
+ preLabel.length);
+ }
+ let post = input.value.slice(input.selectionEnd, input.value.length);
+ let item = this.popup.selectedItem;
+ this._selectedIndex = this.popup.selectedIndex;
+ let toComplete = item.label.slice(item.preLabel.length);
+ input.value = pre + toComplete + post;
+ input.setSelectionRange(pre.length + toComplete.length,
+ pre.length + toComplete.length);
+ this._updateSize();
+ // Wait for the popup to hide and then focus input async otherwise it does
+ // not work.
+ let onPopupHidden = () => {
+ this.popup.off("popup-closed", onPopupHidden);
+ this.doc.defaultView.setTimeout(()=> {
+ input.focus();
+ this.emit("after-suggest");
+ }, 0);
+ };
+ this.popup.on("popup-closed", onPopupHidden);
+ this._hideAutocompletePopup();
+ },
+
+ /**
+ * Handle the input field's keypress event.
+ */
+ _onKeyPress: function (event) {
+ let prevent = false;
+
+ let key = event.keyCode;
+ let input = this.input;
+
+ let multilineNavigation = !this._isSingleLine() &&
+ isKeyIn(key, "UP", "DOWN", "LEFT", "RIGHT");
+ let isPlainText = this.contentType == CONTENT_TYPES.PLAIN_TEXT;
+ let isPopupOpen = this.popup && this.popup.isOpen;
+
+ let increment = 0;
+ if (!isPlainText && !multilineNavigation) {
+ increment = this._getIncrement(event);
+ }
+
+ if (isKeyIn(key, "PAGE_UP", "PAGE_DOWN")) {
+ this._preventSuggestions = true;
+ }
+
+ let cycling = false;
+ if (increment && this._incrementValue(increment)) {
+ this._updateSize();
+ prevent = true;
+ cycling = true;
+ }
+
+ if (isPopupOpen && isKeyIn(key, "UP", "DOWN", "PAGE_UP", "PAGE_DOWN")) {
+ prevent = true;
+ cycling = true;
+ this._cycleCSSSuggestion(isKeyIn(key, "UP", "PAGE_UP"));
+ this._doValidation();
+ }
+
+ if (isKeyIn(key, "BACK_SPACE", "DELETE", "LEFT", "RIGHT", "HOME", "END")) {
+ if (isPopupOpen) {
+ this._hideAutocompletePopup();
+ }
+ } else if (!cycling && !multilineNavigation &&
+ !event.metaKey && !event.altKey && !event.ctrlKey) {
+ this._maybeSuggestCompletion(true);
+ }
+
+ if (this.multiline && event.shiftKey && isKeyIn(key, "RETURN")) {
+ prevent = false;
+ } else if (
+ this._advanceChars(event.charCode, input.value, input.selectionStart) ||
+ isKeyIn(key, "RETURN", "TAB")) {
+ prevent = true;
+
+ let direction;
+ if ((this.stopOnReturn && isKeyIn(key, "RETURN")) ||
+ (this.stopOnTab && !event.shiftKey && isKeyIn(key, "TAB")) ||
+ (this.stopOnShiftTab && event.shiftKey && isKeyIn(key, "TAB"))) {
+ direction = null;
+ } else if (event.shiftKey && isKeyIn(key, "TAB")) {
+ direction = FOCUS_BACKWARD;
+ } else {
+ direction = FOCUS_FORWARD;
+ }
+
+ // Now we don't want to suggest anything as we are moving out.
+ this._preventSuggestions = true;
+ // But we still want to show suggestions for css values. i.e. moving out
+ // of css property input box in forward direction
+ if (this.contentType == CONTENT_TYPES.CSS_PROPERTY &&
+ direction == FOCUS_FORWARD) {
+ this._preventSuggestions = false;
+ }
+
+ if (isKeyIn(key, "TAB") && this.contentType == CONTENT_TYPES.CSS_MIXED) {
+ if (this.popup && input.selectionStart < input.selectionEnd) {
+ event.preventDefault();
+ input.setSelectionRange(input.selectionEnd, input.selectionEnd);
+ this.emit("after-suggest");
+ return;
+ } else if (this.popup && this.popup.isOpen) {
+ event.preventDefault();
+ this._cycleCSSSuggestion(event.shiftKey, true);
+ return;
+ }
+ }
+
+ this._apply(event, direction);
+
+ // Close the popup if open
+ if (this.popup && this.popup.isOpen) {
+ this._hideAutocompletePopup();
+ }
+
+ if (direction !== null && focusManager.focusedElement === input) {
+ // If the focused element wasn't changed by the done callback,
+ // move the focus as requested.
+ let next = moveFocus(this.doc.defaultView, direction);
+
+ // If the next node to be focused has been tagged as an editable
+ // node, trigger editing using the configured event
+ if (next && next.ownerDocument === this.doc && next._editable) {
+ let e = this.doc.createEvent("Event");
+ e.initEvent(next._trigger, true, true);
+ next.dispatchEvent(e);
+ }
+ }
+
+ this._clear();
+ } else if (isKeyIn(key, "ESCAPE")) {
+ // Cancel and blur ourselves.
+ // Now we don't want to suggest anything as we are moving out.
+ this._preventSuggestions = true;
+ // Close the popup if open
+ if (this.popup && this.popup.isOpen) {
+ this._hideAutocompletePopup();
+ }
+ prevent = true;
+ this.cancelled = true;
+ this._apply();
+ this._clear();
+ event.stopPropagation();
+ } else if (isKeyIn(key, "SPACE")) {
+ // No need for leading spaces here. This is particularly
+ // noticable when adding a property: it's very natural to type
+ // <name>: (which advances to the next property) then spacebar.
+ prevent = !input.value;
+ }
+
+ if (prevent) {
+ event.preventDefault();
+ }
+ },
+
+ _onContextMenu: function (event) {
+ if (this.contextMenu) {
+ this.contextMenu(event);
+ }
+ },
+
+ /**
+ * Open the autocomplete popup, adding a custom click handler and classname.
+ *
+ * @param {Number} offset
+ * X-offset relative to the input starting edge.
+ * @param {Number} selectedIndex
+ * The index of the item that should be selected. Use -1 to have no
+ * item selected.
+ */
+ _openAutocompletePopup: function (offset, selectedIndex) {
+ this.popup.on("popup-click", this._onAutocompletePopupClick);
+ this.popup.openPopup(this.input, offset, 0, selectedIndex);
+ },
+
+ /**
+ * Remove the custom classname and click handler and close the autocomplete
+ * popup.
+ */
+ _hideAutocompletePopup: function () {
+ this.popup.off("popup-click", this._onAutocompletePopupClick);
+ this.popup.hidePopup();
+ },
+
+ /**
+ * Get the increment/decrement step to use for the provided key event.
+ */
+ _getIncrement: function (event) {
+ const largeIncrement = 100;
+ const mediumIncrement = 10;
+ const smallIncrement = 0.1;
+
+ let increment = 0;
+ let key = event.keyCode;
+
+ if (isKeyIn(key, "UP", "PAGE_UP")) {
+ increment = 1;
+ } else if (isKeyIn(key, "DOWN", "PAGE_DOWN")) {
+ increment = -1;
+ }
+
+ if (event.shiftKey && !event.altKey) {
+ if (isKeyIn(key, "PAGE_UP", "PAGE_DOWN")) {
+ increment *= largeIncrement;
+ } else {
+ increment *= mediumIncrement;
+ }
+ } else if (event.altKey && !event.shiftKey) {
+ increment *= smallIncrement;
+ }
+
+ return increment;
+ },
+
+ /**
+ * Handle the input field's keyup event.
+ */
+ _onKeyup: function () {
+ this._applied = false;
+ },
+
+ /**
+ * Handle changes to the input text.
+ */
+ _onInput: function () {
+ // Validate the entered value.
+ this._doValidation();
+
+ // Update size if we're autosizing.
+ if (this._measurement) {
+ this._updateSize();
+ }
+
+ // Call the user's change handler if available.
+ if (this.change) {
+ this.change(this.currentInputValue);
+ }
+ },
+
+ /**
+ * Stop propagation on the provided event
+ */
+ _stopEventPropagation: function (e) {
+ e.stopPropagation();
+ },
+
+ /**
+ * Fire validation callback with current input
+ */
+ _doValidation: function () {
+ if (this.validate && this.input) {
+ this.validate(this.input.value);
+ }
+ },
+
+ /**
+ * Handles displaying suggestions based on the current input.
+ *
+ * @param {Boolean} autoInsert
+ * Pass true to automatically insert the most relevant suggestion.
+ */
+ _maybeSuggestCompletion: function (autoInsert) {
+ // Input can be null in cases when you intantaneously switch out of it.
+ if (!this.input) {
+ return;
+ }
+ let preTimeoutQuery = this.input.value;
+
+ // Since we are calling this method from a keypress event handler, the
+ // |input.value| does not include currently typed character. Thus we perform
+ // this method async.
+ this._openPopupTimeout = this.doc.defaultView.setTimeout(() => {
+ if (this._preventSuggestions) {
+ this._preventSuggestions = false;
+ return;
+ }
+ if (this.contentType == CONTENT_TYPES.PLAIN_TEXT) {
+ return;
+ }
+ if (!this.input) {
+ return;
+ }
+ let input = this.input;
+ // The length of input.value should be increased by 1
+ if (input.value.length - preTimeoutQuery.length > 1) {
+ return;
+ }
+ let query = input.value.slice(0, input.selectionStart);
+ let startCheckQuery = query;
+ if (query == null) {
+ return;
+ }
+ // If nothing is selected and there is a word (\w) character after the cursor, do
+ // not autocomplete.
+ if (input.selectionStart == input.selectionEnd &&
+ input.selectionStart < input.value.length) {
+ let nextChar = input.value.slice(input.selectionStart)[0];
+ // Check if the next character is a valid word character, no suggestion should be
+ // provided when preceeding a word.
+ if (/[\w-]/.test(nextChar)) {
+ // This emit is mainly to make the test flow simpler.
+ this.emit("after-suggest", "nothing to autocomplete");
+ return;
+ }
+ }
+ let list = [];
+ if (this.contentType == CONTENT_TYPES.CSS_PROPERTY) {
+ list = this._getCSSPropertyList();
+ } else if (this.contentType == CONTENT_TYPES.CSS_VALUE) {
+ // Get the last query to be completed before the caret.
+ let match = /([^\s,.\/]+$)/.exec(query);
+ if (match) {
+ startCheckQuery = match[0];
+ } else {
+ startCheckQuery = "";
+ }
+
+ list =
+ ["!important",
+ ...this._getCSSValuesForPropertyName(this.property.name)];
+
+ if (query == "") {
+ // Do not suggest '!important' without any manually typed character.
+ list.splice(0, 1);
+ }
+ } else if (this.contentType == CONTENT_TYPES.CSS_MIXED &&
+ /^\s*style\s*=/.test(query)) {
+ // Check if the style attribute is closed before the selection.
+ let styleValue = query.replace(/^\s*style\s*=\s*/, "");
+ // Look for a quote matching the opening quote (single or double).
+ if (/^("[^"]*"|'[^']*')/.test(styleValue)) {
+ // This emit is mainly to make the test flow simpler.
+ this.emit("after-suggest", "nothing to autocomplete");
+ return;
+ }
+
+ // Detecting if cursor is at property or value;
+ let match = query.match(/([:;"'=]?)\s*([^"';:=]+)?$/);
+ if (match && match.length >= 2) {
+ if (match[1] == ":") {
+ // We are in CSS value completion
+ let propertyName =
+ query.match(/[;"'=]\s*([^"';:= ]+)\s*:\s*[^"';:=]*$/)[1];
+ list =
+ ["!important;",
+ ...this._getCSSValuesForPropertyName(propertyName)];
+ let matchLastQuery = /([^\s,.\/]+$)/.exec(match[2] || "");
+ if (matchLastQuery) {
+ startCheckQuery = matchLastQuery[0];
+ } else {
+ startCheckQuery = "";
+ }
+ if (!match[2]) {
+ // Don't suggest '!important' without any manually typed character
+ list.splice(0, 1);
+ }
+ } else if (match[1]) {
+ // We are in CSS property name completion
+ list = this._getCSSPropertyList();
+ startCheckQuery = match[2];
+ }
+ if (startCheckQuery == null) {
+ // This emit is mainly to make the test flow simpler.
+ this.emit("after-suggest", "nothing to autocomplete");
+ return;
+ }
+ }
+ }
+
+ if (!this.popup) {
+ // This emit is mainly to make the test flow simpler.
+ this.emit("after-suggest", "no popup");
+ return;
+ }
+
+ let finalList = [];
+ let length = list.length;
+ for (let i = 0, count = 0; i < length && count < MAX_POPUP_ENTRIES; i++) {
+ if (startCheckQuery != null && list[i].startsWith(startCheckQuery)) {
+ count++;
+ finalList.push({
+ preLabel: startCheckQuery,
+ label: list[i]
+ });
+ } else if (count > 0) {
+ // Since count was incremented, we had already crossed the entries
+ // which would have started with query, assuming that list is sorted.
+ break;
+ } else if (startCheckQuery != null && list[i][0] > startCheckQuery[0]) {
+ // We have crossed all possible matches alphabetically.
+ break;
+ }
+ }
+
+ // Sort items starting with [a-z0-9] first, to make sure vendor-prefixed
+ // values and "!important" are suggested only after standard values.
+ finalList.sort((item1, item2) => {
+ // Get the expected alphabetical comparison between the items.
+ let comparison = item1.label.localeCompare(item2.label);
+ if (/^\w/.test(item1.label) != /^\w/.test(item2.label)) {
+ // One starts with [a-z0-9], one does not: flip the comparison.
+ comparison = -1 * comparison;
+ }
+ return comparison;
+ });
+
+ let index = 0;
+ if (startCheckQuery) {
+ // Only select a "best" suggestion when the user started a query.
+ let cssValues = finalList.map(item => item.label);
+ index = findMostRelevantCssPropertyIndex(cssValues);
+ }
+
+ // Insert the most relevant item from the final list as the input value.
+ if (autoInsert && finalList[index]) {
+ let item = finalList[index].label;
+ input.value = query + item.slice(startCheckQuery.length) +
+ input.value.slice(query.length);
+ input.setSelectionRange(query.length, query.length + item.length -
+ startCheckQuery.length);
+ this._updateSize();
+ }
+
+ // Display the list of suggestions if there are more than one.
+ if (finalList.length > 1) {
+ // Calculate the popup horizontal offset.
+ let indent = this.input.selectionStart - startCheckQuery.length;
+ let offset = indent * this.inputCharDimensions.width;
+ offset = this._isSingleLine() ? offset : 0;
+
+ // Select the most relevantItem if autoInsert is allowed
+ let selectedIndex = autoInsert ? index : -1;
+
+ // Open the suggestions popup.
+ this.popup.setItems(finalList);
+ this._openAutocompletePopup(offset, selectedIndex);
+ } else {
+ this._hideAutocompletePopup();
+ }
+ // This emit is mainly for the purpose of making the test flow simpler.
+ this.emit("after-suggest");
+ this._doValidation();
+ }, 0);
+ },
+
+ /**
+ * Check if the current input is displaying more than one line of text.
+ *
+ * @return {Boolean} true if the input has a single line of text
+ */
+ _isSingleLine: function () {
+ let inputRect = this.input.getBoundingClientRect();
+ return inputRect.height < 2 * this.inputCharDimensions.height;
+ },
+
+ /**
+ * Returns the list of CSS properties to use for the autocompletion. This
+ * method is overridden by tests in order to use mocked suggestion lists.
+ *
+ * @return {Array} array of CSS property names (Strings)
+ */
+ _getCSSPropertyList: function () {
+ return this.cssProperties.getNames().sort();
+ },
+
+ /**
+ * Returns a list of CSS values valid for a provided property name to use for
+ * the autocompletion. This method is overridden by tests in order to use
+ * mocked suggestion lists.
+ *
+ * @param {String} propertyName
+ * @return {Array} array of CSS property values (Strings)
+ */
+ _getCSSValuesForPropertyName: function (propertyName) {
+ return this.cssProperties.getValues(propertyName);
+ },
+};
+
+/**
+ * Copy text-related styles from one element to another.
+ */
+function copyTextStyles(from, to) {
+ let win = from.ownerDocument.defaultView;
+ let style = win.getComputedStyle(from);
+ let getCssText = name => style.getPropertyCSSValue(name).cssText;
+
+ to.style.fontFamily = getCssText("font-family");
+ to.style.fontSize = getCssText("font-size");
+ to.style.fontWeight = getCssText("font-weight");
+ to.style.fontStyle = getCssText("font-style");
+}
+
+/**
+ * Copy all styles which could have an impact on the element size.
+ */
+function copyAllStyles(from, to) {
+ let win = from.ownerDocument.defaultView;
+ let style = win.getComputedStyle(from);
+ let getCssText = name => style.getPropertyCSSValue(name).cssText;
+
+ copyTextStyles(from, to);
+ to.style.lineHeight = getCssText("line-height");
+
+ // If box-sizing is set to border-box, box model styles also need to be
+ // copied.
+ let boxSizing = getCssText("box-sizing");
+ if (boxSizing === "border-box") {
+ to.style.boxSizing = boxSizing;
+ copyBoxModelStyles(from, to);
+ }
+}
+
+/**
+ * Copy box model styles that can impact width and height measurements when box-
+ * sizing is set to "border-box" instead of "content-box".
+ *
+ * @param {DOMNode} from
+ * the element from which styles are copied
+ * @param {DOMNode} to
+ * the element on which copied styles are applied
+ */
+function copyBoxModelStyles(from, to) {
+ let win = from.ownerDocument.defaultView;
+ let style = win.getComputedStyle(from);
+ let getCssText = name => style.getPropertyCSSValue(name).cssText;
+
+ // Copy all paddings.
+ to.style.paddingTop = getCssText("padding-top");
+ to.style.paddingRight = getCssText("padding-right");
+ to.style.paddingBottom = getCssText("padding-bottom");
+ to.style.paddingLeft = getCssText("padding-left");
+
+ // Copy border styles.
+ to.style.borderTopStyle = getCssText("border-top-style");
+ to.style.borderRightStyle = getCssText("border-right-style");
+ to.style.borderBottomStyle = getCssText("border-bottom-style");
+ to.style.borderLeftStyle = getCssText("border-left-style");
+
+ // Copy border widths.
+ to.style.borderTopWidth = getCssText("border-top-width");
+ to.style.borderRightWidth = getCssText("border-right-width");
+ to.style.borderBottomWidth = getCssText("border-bottom-width");
+ to.style.borderLeftWidth = getCssText("border-left-width");
+}
+
+/**
+ * Trigger a focus change similar to pressing tab/shift-tab.
+ */
+function moveFocus(win, direction) {
+ return focusManager.moveFocus(win, null, direction, 0);
+}
diff --git a/devtools/client/shared/key-shortcuts.js b/devtools/client/shared/key-shortcuts.js
new file mode 100644
index 000000000..ec7d30bcb
--- /dev/null
+++ b/devtools/client/shared/key-shortcuts.js
@@ -0,0 +1,251 @@
+/* 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 Services = require("Services");
+const EventEmitter = require("devtools/shared/event-emitter");
+const isOSX = Services.appinfo.OS === "Darwin";
+const {KeyCodes} = require("devtools/client/shared/keycodes");
+
+// List of electron keys mapped to DOM API (DOM_VK_*) key code
+const ElectronKeysMapping = {
+ "F1": "DOM_VK_F1",
+ "F2": "DOM_VK_F2",
+ "F3": "DOM_VK_F3",
+ "F4": "DOM_VK_F4",
+ "F5": "DOM_VK_F5",
+ "F6": "DOM_VK_F6",
+ "F7": "DOM_VK_F7",
+ "F8": "DOM_VK_F8",
+ "F9": "DOM_VK_F9",
+ "F10": "DOM_VK_F10",
+ "F11": "DOM_VK_F11",
+ "F12": "DOM_VK_F12",
+ "F13": "DOM_VK_F13",
+ "F14": "DOM_VK_F14",
+ "F15": "DOM_VK_F15",
+ "F16": "DOM_VK_F16",
+ "F17": "DOM_VK_F17",
+ "F18": "DOM_VK_F18",
+ "F19": "DOM_VK_F19",
+ "F20": "DOM_VK_F20",
+ "F21": "DOM_VK_F21",
+ "F22": "DOM_VK_F22",
+ "F23": "DOM_VK_F23",
+ "F24": "DOM_VK_F24",
+ "Space": "DOM_VK_SPACE",
+ "Backspace": "DOM_VK_BACK_SPACE",
+ "Delete": "DOM_VK_DELETE",
+ "Insert": "DOM_VK_INSERT",
+ "Return": "DOM_VK_RETURN",
+ "Enter": "DOM_VK_RETURN",
+ "Up": "DOM_VK_UP",
+ "Down": "DOM_VK_DOWN",
+ "Left": "DOM_VK_LEFT",
+ "Right": "DOM_VK_RIGHT",
+ "Home": "DOM_VK_HOME",
+ "End": "DOM_VK_END",
+ "PageUp": "DOM_VK_PAGE_UP",
+ "PageDown": "DOM_VK_PAGE_DOWN",
+ "Escape": "DOM_VK_ESCAPE",
+ "Esc": "DOM_VK_ESCAPE",
+ "Tab": "DOM_VK_TAB",
+ "VolumeUp": "DOM_VK_VOLUME_UP",
+ "VolumeDown": "DOM_VK_VOLUME_DOWN",
+ "VolumeMute": "DOM_VK_VOLUME_MUTE",
+ "PrintScreen": "DOM_VK_PRINTSCREEN",
+};
+
+/**
+ * Helper to listen for keyboard events decribed in .properties file.
+ *
+ * let shortcuts = new KeyShortcuts({
+ * window
+ * });
+ * shortcuts.on("Ctrl+F", event => {
+ * // `event` is the KeyboardEvent which relates to the key shortcuts
+ * });
+ *
+ * @param DOMWindow window
+ * The window object of the document to listen events from.
+ * @param DOMElement target
+ * Optional DOM Element on which we should listen events from.
+ * If omitted, we listen for all events fired on `window`.
+ */
+function KeyShortcuts({ window, target }) {
+ this.window = window;
+ this.target = target || window;
+ this.keys = new Map();
+ this.eventEmitter = new EventEmitter();
+ this.target.addEventListener("keydown", this);
+}
+
+/*
+ * Parse an electron-like key string and return a normalized object which
+ * allow efficient match on DOM key event. The normalized object matches DOM
+ * API.
+ *
+ * @param DOMWindow window
+ * Any DOM Window object, just to fetch its `KeyboardEvent` object
+ * @param String str
+ * The shortcut string to parse, following this document:
+ * https://github.com/electron/electron/blob/master/docs/api/accelerator.md
+ */
+KeyShortcuts.parseElectronKey = function (window, str) {
+ let modifiers = str.split("+");
+ let key = modifiers.pop();
+
+ let shortcut = {
+ ctrl: false,
+ meta: false,
+ alt: false,
+ shift: false,
+ // Set for character keys
+ key: undefined,
+ // Set for non-character keys
+ keyCode: undefined,
+ };
+ for (let mod of modifiers) {
+ if (mod === "Alt") {
+ shortcut.alt = true;
+ } else if (["Command", "Cmd"].includes(mod)) {
+ shortcut.meta = true;
+ } else if (["CommandOrControl", "CmdOrCtrl"].includes(mod)) {
+ if (isOSX) {
+ shortcut.meta = true;
+ } else {
+ shortcut.ctrl = true;
+ }
+ } else if (["Control", "Ctrl"].includes(mod)) {
+ shortcut.ctrl = true;
+ } else if (mod === "Shift") {
+ shortcut.shift = true;
+ } else {
+ console.error("Unsupported modifier:", mod, "from key:", str);
+ return null;
+ }
+ }
+
+ // Plus is a special case. It's a character key and shouldn't be matched
+ // against a keycode as it is only accessible via Shift/Capslock
+ if (key === "Plus") {
+ key = "+";
+ }
+
+ if (typeof key === "string" && key.length === 1) {
+ // Match any single character
+ shortcut.key = key.toLowerCase();
+ } else if (key in ElectronKeysMapping) {
+ // Maps the others manually to DOM API DOM_VK_*
+ key = ElectronKeysMapping[key];
+ shortcut.keyCode = KeyCodes[key];
+ // Used only to stringify the shortcut
+ shortcut.keyCodeString = key;
+ shortcut.key = key;
+ } else {
+ console.error("Unsupported key:", key);
+ return null;
+ }
+
+ return shortcut;
+};
+
+KeyShortcuts.stringify = function (shortcut) {
+ let list = [];
+ if (shortcut.alt) {
+ list.push("Alt");
+ }
+ if (shortcut.ctrl) {
+ list.push("Ctrl");
+ }
+ if (shortcut.meta) {
+ list.push("Cmd");
+ }
+ if (shortcut.shift) {
+ list.push("Shift");
+ }
+ let key;
+ if (shortcut.key) {
+ key = shortcut.key.toUpperCase();
+ } else {
+ key = shortcut.keyCodeString;
+ }
+ list.push(key);
+ return list.join("+");
+};
+
+KeyShortcuts.prototype = {
+ destroy() {
+ this.target.removeEventListener("keydown", this);
+ this.keys.clear();
+ },
+
+ doesEventMatchShortcut(event, shortcut) {
+ if (shortcut.meta != event.metaKey) {
+ return false;
+ }
+ if (shortcut.ctrl != event.ctrlKey) {
+ return false;
+ }
+ if (shortcut.alt != event.altKey) {
+ return false;
+ }
+ if (shortcut.shift != event.shiftKey) {
+ // Shift is a special modifier, it may implicitely be required if the expected key
+ // is a special character accessible via shift.
+ let isAlphabetical = event.key && event.key.match(/[a-zA-Z]/);
+ // OSX: distinguish cmd+[key] from cmd+shift+[key] shortcuts (Bug 1300458)
+ let cmdShortcut = shortcut.meta && !shortcut.alt && !shortcut.ctrl;
+ if (isAlphabetical || cmdShortcut) {
+ return false;
+ }
+ }
+
+ if (shortcut.keyCode) {
+ return event.keyCode == shortcut.keyCode;
+ } else if (event.key in ElectronKeysMapping) {
+ return ElectronKeysMapping[event.key] === shortcut.key;
+ }
+
+ // get the key from the keyCode if key is not provided.
+ let key = event.key || String.fromCharCode(event.keyCode);
+
+ // For character keys, we match if the final character is the expected one.
+ // But for digits we also accept indirect match to please azerty keyboard,
+ // which requires Shift to be pressed to get digits.
+ return key.toLowerCase() == shortcut.key ||
+ (shortcut.key.match(/[0-9]/) &&
+ event.keyCode == shortcut.key.charCodeAt(0));
+ },
+
+ handleEvent(event) {
+ for (let [key, shortcut] of this.keys) {
+ if (this.doesEventMatchShortcut(event, shortcut)) {
+ this.eventEmitter.emit(key, event);
+ }
+ }
+ },
+
+ on(key, listener) {
+ if (typeof listener !== "function") {
+ throw new Error("KeyShortcuts.on() expects a function as " +
+ "second argument");
+ }
+ if (!this.keys.has(key)) {
+ let shortcut = KeyShortcuts.parseElectronKey(this.window, key);
+ // The key string is wrong and we were unable to compute the key shortcut
+ if (!shortcut) {
+ return;
+ }
+ this.keys.set(key, shortcut);
+ }
+ this.eventEmitter.on(key, listener);
+ },
+
+ off(key, listener) {
+ this.eventEmitter.off(key, listener);
+ },
+};
+exports.KeyShortcuts = KeyShortcuts;
diff --git a/devtools/client/shared/keycodes.js b/devtools/client/shared/keycodes.js
new file mode 100644
index 000000000..fe4764dbe
--- /dev/null
+++ b/devtools/client/shared/keycodes.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";
+
+// This was copied (and slightly modified) from
+// devtools/shared/gcli/source/lib/gcli/util/util.js, which in turn
+// says:
+
+/**
+ * Keyboard handling is a mess. http://unixpapa.com/js/key.html
+ * It would be good to use DOM L3 Keyboard events,
+ * http://www.w3.org/TR/2010/WD-DOM-Level-3-Events-20100907/#events-keyboardevents
+ * however only Webkit supports them, and there isn't a shim on Modernizr:
+ * https://github.com/Modernizr/Modernizr/wiki/HTML5-Cross-browser-Polyfills
+ * and when the code that uses this KeyEvent was written, nothing was clear,
+ * so instead, we're using this unmodern shim:
+ * http://stackoverflow.com/questions/5681146/chrome-10-keyevent-or-something-similar-to-firefoxs-keyevent
+ * See BUG 664991: GCLI's keyboard handling should be updated to use DOM-L3
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=664991
+ */
+
+exports.KeyCodes = {
+ DOM_VK_CANCEL: 3,
+ DOM_VK_HELP: 6,
+ DOM_VK_BACK_SPACE: 8,
+ DOM_VK_TAB: 9,
+ DOM_VK_CLEAR: 12,
+ DOM_VK_RETURN: 13,
+ DOM_VK_SHIFT: 16,
+ DOM_VK_CONTROL: 17,
+ DOM_VK_ALT: 18,
+ DOM_VK_PAUSE: 19,
+ DOM_VK_CAPS_LOCK: 20,
+ DOM_VK_ESCAPE: 27,
+ DOM_VK_SPACE: 32,
+ DOM_VK_PAGE_UP: 33,
+ DOM_VK_PAGE_DOWN: 34,
+ DOM_VK_END: 35,
+ DOM_VK_HOME: 36,
+ DOM_VK_LEFT: 37,
+ DOM_VK_UP: 38,
+ DOM_VK_RIGHT: 39,
+ DOM_VK_DOWN: 40,
+ DOM_VK_PRINTSCREEN: 44,
+ DOM_VK_INSERT: 45,
+ DOM_VK_DELETE: 46,
+ DOM_VK_0: 48,
+ DOM_VK_1: 49,
+ DOM_VK_2: 50,
+ DOM_VK_3: 51,
+ DOM_VK_4: 52,
+ DOM_VK_5: 53,
+ DOM_VK_6: 54,
+ DOM_VK_7: 55,
+ DOM_VK_8: 56,
+ DOM_VK_9: 57,
+ DOM_VK_SEMICOLON: 59,
+ DOM_VK_EQUALS: 61,
+ DOM_VK_A: 65,
+ DOM_VK_B: 66,
+ DOM_VK_C: 67,
+ DOM_VK_D: 68,
+ DOM_VK_E: 69,
+ DOM_VK_F: 70,
+ DOM_VK_G: 71,
+ DOM_VK_H: 72,
+ DOM_VK_I: 73,
+ DOM_VK_J: 74,
+ DOM_VK_K: 75,
+ DOM_VK_L: 76,
+ DOM_VK_M: 77,
+ DOM_VK_N: 78,
+ DOM_VK_O: 79,
+ DOM_VK_P: 80,
+ DOM_VK_Q: 81,
+ DOM_VK_R: 82,
+ DOM_VK_S: 83,
+ DOM_VK_T: 84,
+ DOM_VK_U: 85,
+ DOM_VK_V: 86,
+ DOM_VK_W: 87,
+ DOM_VK_X: 88,
+ DOM_VK_Y: 89,
+ DOM_VK_Z: 90,
+ DOM_VK_CONTEXT_MENU: 93,
+ DOM_VK_NUMPAD0: 96,
+ DOM_VK_NUMPAD1: 97,
+ DOM_VK_NUMPAD2: 98,
+ DOM_VK_NUMPAD3: 99,
+ DOM_VK_NUMPAD4: 100,
+ DOM_VK_NUMPAD5: 101,
+ DOM_VK_NUMPAD6: 102,
+ DOM_VK_NUMPAD7: 103,
+ DOM_VK_NUMPAD8: 104,
+ DOM_VK_NUMPAD9: 105,
+ DOM_VK_MULTIPLY: 106,
+ DOM_VK_ADD: 107,
+ DOM_VK_SEPARATOR: 108,
+ DOM_VK_SUBTRACT: 109,
+ DOM_VK_DECIMAL: 110,
+ DOM_VK_DIVIDE: 111,
+ DOM_VK_F1: 112,
+ DOM_VK_F2: 113,
+ DOM_VK_F3: 114,
+ DOM_VK_F4: 115,
+ DOM_VK_F5: 116,
+ DOM_VK_F6: 117,
+ DOM_VK_F7: 118,
+ DOM_VK_F8: 119,
+ DOM_VK_F9: 120,
+ DOM_VK_F10: 121,
+ DOM_VK_F11: 122,
+ DOM_VK_F12: 123,
+ DOM_VK_F13: 124,
+ DOM_VK_F14: 125,
+ DOM_VK_F15: 126,
+ DOM_VK_F16: 127,
+ DOM_VK_F17: 128,
+ DOM_VK_F18: 129,
+ DOM_VK_F19: 130,
+ DOM_VK_F20: 131,
+ DOM_VK_F21: 132,
+ DOM_VK_F22: 133,
+ DOM_VK_F23: 134,
+ DOM_VK_F24: 135,
+ DOM_VK_NUM_LOCK: 144,
+ DOM_VK_SCROLL_LOCK: 145,
+ DOM_VK_COMMA: 188,
+ DOM_VK_PERIOD: 190,
+ DOM_VK_SLASH: 191,
+ DOM_VK_BACK_QUOTE: 192,
+ DOM_VK_OPEN_BRACKET: 219,
+ DOM_VK_BACK_SLASH: 220,
+ DOM_VK_CLOSE_BRACKET: 221,
+ DOM_VK_QUOTE: 222,
+ DOM_VK_META: 224,
+
+ // A few that did not appear in gcli, but that are apparently used
+ // in devtools.
+ DOM_VK_COLON: 58,
+ DOM_VK_VOLUME_MUTE: 181,
+ DOM_VK_VOLUME_DOWN: 182,
+ DOM_VK_VOLUME_UP: 183,
+};
diff --git a/devtools/client/shared/moz.build b/devtools/client/shared/moz.build
new file mode 100644
index 000000000..1c61970c0
--- /dev/null
+++ b/devtools/client/shared/moz.build
@@ -0,0 +1,54 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
+XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
+
+DIRS += [
+ 'components',
+ 'redux',
+ 'shim',
+ 'vendor',
+ 'widgets',
+]
+
+DevToolsModules(
+ 'AppCacheUtils.jsm',
+ 'autocomplete-popup.js',
+ 'browser-loader.js',
+ 'css-angle.js',
+ 'css-reload.js',
+ 'curl.js',
+ 'demangle.js',
+ 'developer-toolbar.js',
+ 'devices.js',
+ 'devtools-file-watcher.js',
+ 'DOMHelpers.jsm',
+ 'doorhanger.js',
+ 'file-watcher-worker.js',
+ 'file-watcher.js',
+ 'getjson.js',
+ 'inplace-editor.js',
+ 'Jsbeautify.jsm',
+ 'key-shortcuts.js',
+ 'keycodes.js',
+ 'network-throttling-profiles.js',
+ 'node-attribute-parser.js',
+ 'options-view.js',
+ 'output-parser.js',
+ 'poller.js',
+ 'prefs.js',
+ 'scroll.js',
+ 'source-utils.js',
+ 'SplitView.jsm',
+ 'suggestion-picker.js',
+ 'telemetry.js',
+ 'theme.js',
+ 'undo.js',
+ 'view-source.js',
+ 'webgl-utils.js',
+ 'zoom-keys.js',
+)
diff --git a/devtools/client/shared/network-throttling-profiles.js b/devtools/client/shared/network-throttling-profiles.js
new file mode 100644
index 000000000..ef139fda6
--- /dev/null
+++ b/devtools/client/shared/network-throttling-profiles.js
@@ -0,0 +1,68 @@
+/* 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 K = 1024;
+const M = 1024 * 1024;
+const Bps = 1 / 8;
+const KBps = K * Bps;
+const MBps = M * Bps;
+
+/**
+ * Predefined network throttling profiles.
+ * Speeds are in bytes per second. Latency is in ms.
+ */
+/* eslint-disable key-spacing */
+module.exports = [
+ {
+ id: "GPRS",
+ download: 50 * KBps,
+ upload: 20 * KBps,
+ latency: 500,
+ },
+ {
+ id: "Regular 2G",
+ download: 250 * KBps,
+ upload: 50 * KBps,
+ latency: 300,
+ },
+ {
+ id: "Good 2G",
+ download: 450 * KBps,
+ upload: 150 * KBps,
+ latency: 150,
+ },
+ {
+ id: "Regular 3G",
+ download: 750 * KBps,
+ upload: 250 * KBps,
+ latency: 100,
+ },
+ {
+ id: "Good 3G",
+ download: 1.5 * MBps,
+ upload: 750 * KBps,
+ latency: 40,
+ },
+ {
+ id: "Regular 4G / LTE",
+ download: 4 * MBps,
+ upload: 3 * MBps,
+ latency: 20,
+ },
+ {
+ id: "DSL",
+ download: 2 * MBps,
+ upload: 1 * MBps,
+ latency: 5,
+ },
+ {
+ id: "Wi-Fi",
+ download: 30 * MBps,
+ upload: 15 * MBps,
+ latency: 2,
+ },
+];
+/* eslint-enable key-spacing */
diff --git a/devtools/client/shared/node-attribute-parser.js b/devtools/client/shared/node-attribute-parser.js
new file mode 100644
index 000000000..aaf866fca
--- /dev/null
+++ b/devtools/client/shared/node-attribute-parser.js
@@ -0,0 +1,294 @@
+/* -*- 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";
+
+/**
+ * This module contains a small element attribute value parser. It's primary
+ * goal is to extract link information from attribute values (like the href in
+ * <a href="/some/link.html"> for example).
+ *
+ * There are several types of linkable attribute values:
+ * - TYPE_URI: a URI (e.g. <a href="uri">).
+ * - TYPE_URI_LIST: a space separated list of URIs (e.g. <a ping="uri1 uri2">).
+ * - TYPE_IDREF: a reference to an other element in the same document via its id
+ * (e.g. <label for="input-id"> or <key command="command-id">).
+ * - TYPE_IDREF_LIST: a space separated list of IDREFs (e.g.
+ * <output for="id1 id2">).
+ * - TYPE_JS_RESOURCE_URI: a URI to a javascript resource that can be opened in
+ * the devtools (e.g. <script src="uri">).
+ * - TYPE_CSS_RESOURCE_URI: a URI to a css resource that can be opened in the
+ * devtools (e.g. <link href="uri">).
+ *
+ * parseAttribute is the parser entry function, exported on this module.
+ */
+
+const TYPE_STRING = "string";
+const TYPE_URI = "uri";
+const TYPE_URI_LIST = "uriList";
+const TYPE_IDREF = "idref";
+const TYPE_IDREF_LIST = "idrefList";
+const TYPE_JS_RESOURCE_URI = "jsresource";
+const TYPE_CSS_RESOURCE_URI = "cssresource";
+
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+
+/* eslint-disable max-len */
+const ATTRIBUTE_TYPES = [
+ {namespaceURI: HTML_NS, attributeName: "action", tagName: "form", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "background", tagName: "body", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "cite", tagName: "blockquote", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "cite", tagName: "q", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "cite", tagName: "del", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "cite", tagName: "ins", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "classid", tagName: "object", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "codebase", tagName: "object", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "codebase", tagName: "applet", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "command", tagName: "menuitem", type: TYPE_IDREF},
+ {namespaceURI: "*", attributeName: "contextmenu", tagName: "*", type: TYPE_IDREF},
+ {namespaceURI: HTML_NS, attributeName: "data", tagName: "object", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "for", tagName: "label", type: TYPE_IDREF},
+ {namespaceURI: HTML_NS, attributeName: "for", tagName: "output", type: TYPE_IDREF_LIST},
+ {namespaceURI: HTML_NS, attributeName: "form", tagName: "button", type: TYPE_IDREF},
+ {namespaceURI: HTML_NS, attributeName: "form", tagName: "fieldset", type: TYPE_IDREF},
+ {namespaceURI: HTML_NS, attributeName: "form", tagName: "input", type: TYPE_IDREF},
+ {namespaceURI: HTML_NS, attributeName: "form", tagName: "keygen", type: TYPE_IDREF},
+ {namespaceURI: HTML_NS, attributeName: "form", tagName: "label", type: TYPE_IDREF},
+ {namespaceURI: HTML_NS, attributeName: "form", tagName: "object", type: TYPE_IDREF},
+ {namespaceURI: HTML_NS, attributeName: "form", tagName: "output", type: TYPE_IDREF},
+ {namespaceURI: HTML_NS, attributeName: "form", tagName: "select", type: TYPE_IDREF},
+ {namespaceURI: HTML_NS, attributeName: "form", tagName: "textarea", type: TYPE_IDREF},
+ {namespaceURI: HTML_NS, attributeName: "formaction", tagName: "button", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "formaction", tagName: "input", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "headers", tagName: "td", type: TYPE_IDREF_LIST},
+ {namespaceURI: HTML_NS, attributeName: "headers", tagName: "th", type: TYPE_IDREF_LIST},
+ {namespaceURI: HTML_NS, attributeName: "href", tagName: "a", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "href", tagName: "area", type: TYPE_URI},
+ {namespaceURI: "*", attributeName: "href", tagName: "link", type: TYPE_CSS_RESOURCE_URI,
+ /* eslint-enable */
+ isValid: (namespaceURI, tagName, attributes) => {
+ return getAttribute(attributes, "rel") === "stylesheet";
+ }},
+ /* eslint-disable max-len */
+ {namespaceURI: "*", attributeName: "href", tagName: "link", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "href", tagName: "base", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "icon", tagName: "menuitem", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "list", tagName: "input", type: TYPE_IDREF},
+ {namespaceURI: HTML_NS, attributeName: "longdesc", tagName: "img", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "longdesc", tagName: "frame", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "longdesc", tagName: "iframe", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "manifest", tagName: "html", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "menu", tagName: "button", type: TYPE_IDREF},
+ {namespaceURI: HTML_NS, attributeName: "ping", tagName: "a", type: TYPE_URI_LIST},
+ {namespaceURI: HTML_NS, attributeName: "ping", tagName: "area", type: TYPE_URI_LIST},
+ {namespaceURI: HTML_NS, attributeName: "poster", tagName: "video", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "profile", tagName: "head", type: TYPE_URI},
+ {namespaceURI: "*", attributeName: "src", tagName: "script", type: TYPE_JS_RESOURCE_URI},
+ {namespaceURI: HTML_NS, attributeName: "src", tagName: "input", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "src", tagName: "frame", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "src", tagName: "iframe", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "src", tagName: "img", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "src", tagName: "audio", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "src", tagName: "embed", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "src", tagName: "source", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "src", tagName: "track", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "src", tagName: "video", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "usemap", tagName: "img", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "usemap", tagName: "input", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "usemap", tagName: "object", type: TYPE_URI},
+ {namespaceURI: "*", attributeName: "xmlns", tagName: "*", type: TYPE_URI},
+ {namespaceURI: XUL_NS, attributeName: "command", tagName: "key", type: TYPE_IDREF},
+ {namespaceURI: XUL_NS, attributeName: "containment", tagName: "*", type: TYPE_URI},
+ {namespaceURI: XUL_NS, attributeName: "context", tagName: "*", type: TYPE_IDREF},
+ {namespaceURI: XUL_NS, attributeName: "datasources", tagName: "*", type: TYPE_URI_LIST},
+ {namespaceURI: XUL_NS, attributeName: "insertafter", tagName: "*", type: TYPE_IDREF},
+ {namespaceURI: XUL_NS, attributeName: "insertbefore", tagName: "*", type: TYPE_IDREF},
+ {namespaceURI: XUL_NS, attributeName: "menu", tagName: "*", type: TYPE_IDREF},
+ {namespaceURI: XUL_NS, attributeName: "observes", tagName: "*", type: TYPE_IDREF},
+ {namespaceURI: XUL_NS, attributeName: "popup", tagName: "*", type: TYPE_IDREF},
+ {namespaceURI: XUL_NS, attributeName: "ref", tagName: "*", type: TYPE_URI},
+ {namespaceURI: XUL_NS, attributeName: "removeelement", tagName: "*", type: TYPE_IDREF},
+ {namespaceURI: XUL_NS, attributeName: "sortResource", tagName: "*", type: TYPE_URI},
+ {namespaceURI: XUL_NS, attributeName: "sortResource2", tagName: "*", type: TYPE_URI},
+ {namespaceURI: XUL_NS, attributeName: "src", tagName: "stringbundle", type: TYPE_URI},
+ {namespaceURI: XUL_NS, attributeName: "template", tagName: "*", type: TYPE_IDREF},
+ {namespaceURI: XUL_NS, attributeName: "tooltip", tagName: "*", type: TYPE_IDREF},
+ /* eslint-enable */
+ // SVG links aren't handled yet, see bug 1158831.
+ // {namespaceURI: SVG_NS, attributeName: "fill", tagName: "*", type: },
+ // {namespaceURI: SVG_NS, attributeName: "stroke", tagName: "*", type: },
+ // {namespaceURI: SVG_NS, attributeName: "markerstart", tagName: "*", type: },
+ // {namespaceURI: SVG_NS, attributeName: "markermid", tagName: "*", type: },
+ // {namespaceURI: SVG_NS, attributeName: "markerend", tagName: "*", type: },
+ // {namespaceURI: SVG_NS, attributeName: "xlink:href", tagName: "*", type: }
+];
+
+var parsers = {
+ [TYPE_URI]: function (attributeValue) {
+ return [{
+ type: TYPE_URI,
+ value: attributeValue
+ }];
+ },
+ [TYPE_URI_LIST]: function (attributeValue) {
+ let data = splitBy(attributeValue, " ");
+ for (let token of data) {
+ if (!token.type) {
+ token.type = TYPE_URI;
+ }
+ }
+ return data;
+ },
+ [TYPE_JS_RESOURCE_URI]: function (attributeValue) {
+ return [{
+ type: TYPE_JS_RESOURCE_URI,
+ value: attributeValue
+ }];
+ },
+ [TYPE_CSS_RESOURCE_URI]: function (attributeValue) {
+ return [{
+ type: TYPE_CSS_RESOURCE_URI,
+ value: attributeValue
+ }];
+ },
+ [TYPE_IDREF]: function (attributeValue) {
+ return [{
+ type: TYPE_IDREF,
+ value: attributeValue
+ }];
+ },
+ [TYPE_IDREF_LIST]: function (attributeValue) {
+ let data = splitBy(attributeValue, " ");
+ for (let token of data) {
+ if (!token.type) {
+ token.type = TYPE_IDREF;
+ }
+ }
+ return data;
+ }
+};
+
+/**
+ * Parse an attribute value.
+ * @param {String} namespaceURI The namespaceURI of the node that has the
+ * attribute.
+ * @param {String} tagName The tagName of the node that has the attribute.
+ * @param {Array} attributes The list of all attributes of the node. This should
+ * be an array of {name, value} objects.
+ * @param {String} attributeName The name of the attribute to parse.
+ * @return {Array} An array of tokens that represents the value. Each token is
+ * an object {type: [string|uri|jsresource|cssresource|idref], value}.
+ * For instance parsing the ping attribute in <a ping="uri1 uri2"> returns:
+ * [
+ * {type: "uri", value: "uri2"},
+ * {type: "string", value: " "},
+ * {type: "uri", value: "uri1"}
+ * ]
+ */
+function parseAttribute(namespaceURI, tagName, attributes, attributeName) {
+ if (!hasAttribute(attributes, attributeName)) {
+ throw new Error(`Attribute ${attributeName} isn't part of the ` +
+ "provided attributes");
+ }
+
+ let type = getType(namespaceURI, tagName, attributes, attributeName);
+ if (!type) {
+ return [{
+ type: TYPE_STRING,
+ value: getAttribute(attributes, attributeName)
+ }];
+ }
+
+ return parsers[type](getAttribute(attributes, attributeName));
+}
+
+/**
+ * Get the type for links in this attribute if any.
+ * @param {String} namespaceURI The node's namespaceURI.
+ * @param {String} tagName The node's tagName.
+ * @param {Array} attributes The node's attributes, as a list of {name, value}
+ * objects.
+ * @param {String} attributeName The name of the attribute to get the type for.
+ * @return {Object} null if no type exist for this attribute on this node, the
+ * type object otherwise.
+ */
+function getType(namespaceURI, tagName, attributes, attributeName) {
+ for (let typeData of ATTRIBUTE_TYPES) {
+ let containsAttribute = attributeName === typeData.attributeName ||
+ typeData.attributeName === "*";
+ let hasNamespace = namespaceURI === typeData.namespaceURI ||
+ typeData.namespaceURI === "*";
+ let hasTagName = tagName.toLowerCase() === typeData.tagName ||
+ typeData.tagName === "*";
+ let isValid = typeData.isValid
+ ? typeData.isValid(namespaceURI,
+ tagName,
+ attributes,
+ attributeName)
+ : true;
+
+ if (containsAttribute && hasNamespace && hasTagName && isValid) {
+ return typeData.type;
+ }
+ }
+
+ return null;
+}
+
+function getAttribute(attributes, attributeName) {
+ for (let {name, value} of attributes) {
+ if (name === attributeName) {
+ return value;
+ }
+ }
+ return null;
+}
+
+function hasAttribute(attributes, attributeName) {
+ for (let {name} of attributes) {
+ if (name === attributeName) {
+ return true;
+ }
+ }
+ return false;
+}
+
+/**
+ * Split a string by a given character and return an array of objects parts.
+ * The array will contain objects for the split character too, marked with
+ * TYPE_STRING type.
+ * @param {String} value The string to parse.
+ * @param {String} splitChar A 1 length split character.
+ * @return {Array}
+ */
+function splitBy(value, splitChar) {
+ let data = [], i = 0, buffer = "";
+ while (i <= value.length) {
+ if (i === value.length && buffer) {
+ data.push({value: buffer});
+ }
+ if (value[i] === splitChar) {
+ if (buffer) {
+ data.push({value: buffer});
+ }
+ data.push({
+ type: TYPE_STRING,
+ value: splitChar
+ });
+ buffer = "";
+ } else {
+ buffer += value[i];
+ }
+
+ i++;
+ }
+ return data;
+}
+
+exports.parseAttribute = parseAttribute;
+// Exported for testing only.
+exports.splitBy = splitBy;
diff --git a/devtools/client/shared/options-view.js b/devtools/client/shared/options-view.js
new file mode 100644
index 000000000..bb583eaee
--- /dev/null
+++ b/devtools/client/shared/options-view.js
@@ -0,0 +1,186 @@
+"use strict";
+
+const EventEmitter = require("devtools/shared/event-emitter");
+const Services = require("Services");
+const { Preferences } = require("resource://gre/modules/Preferences.jsm");
+const OPTIONS_SHOWN_EVENT = "options-shown";
+const OPTIONS_HIDDEN_EVENT = "options-hidden";
+const PREF_CHANGE_EVENT = "pref-changed";
+
+/**
+ * OptionsView constructor. Takes several options, all required:
+ * - branchName: The name of the prefs branch, like "devtools.debugger."
+ * - menupopup: The XUL `menupopup` item that contains the pref buttons.
+ *
+ * Fires an event, PREF_CHANGE_EVENT, with the preference name that changed as
+ * the second argument. Fires events on opening/closing the XUL panel
+ * (OPTIONS_SHOW_EVENT, OPTIONS_HIDDEN_EVENT) as the second argument in the
+ * listener, used for tests mostly.
+ */
+const OptionsView = function (options = {}) {
+ this.branchName = options.branchName;
+ this.menupopup = options.menupopup;
+ this.window = this.menupopup.ownerDocument.defaultView;
+ let { document } = this.window;
+ this.$ = document.querySelector.bind(document);
+ this.$$ = (selector, parent = document) => parent.querySelectorAll(selector);
+ // Get the corresponding button that opens the popup by looking
+ // for an element with a `popup` attribute matching the menu's ID
+ this.button = this.$(`[popup=${this.menupopup.getAttribute("id")}]`);
+
+ this.prefObserver = new PrefObserver(this.branchName);
+
+ EventEmitter.decorate(this);
+};
+exports.OptionsView = OptionsView;
+
+OptionsView.prototype = {
+ /**
+ * Binds the events and observers for the OptionsView.
+ */
+ initialize: function () {
+ let { MutationObserver } = this.window;
+ this._onPrefChange = this._onPrefChange.bind(this);
+ this._onOptionChange = this._onOptionChange.bind(this);
+ this._onPopupShown = this._onPopupShown.bind(this);
+ this._onPopupHidden = this._onPopupHidden.bind(this);
+
+ // We use a mutation observer instead of a click handler
+ // because the click handler is fired before the XUL menuitem updates its
+ // checked status, which cascades incorrectly with the Preference observer.
+ this.mutationObserver = new MutationObserver(this._onOptionChange);
+ let observerConfig = { attributes: true, attributeFilter: ["checked"]};
+
+ // Sets observers and default options for all options
+ for (let $el of this.$$("menuitem", this.menupopup)) {
+ let prefName = $el.getAttribute("data-pref");
+
+ if (this.prefObserver.get(prefName)) {
+ $el.setAttribute("checked", "true");
+ } else {
+ $el.removeAttribute("checked");
+ }
+ this.mutationObserver.observe($el, observerConfig);
+ }
+
+ // Listen to any preference change in the specified branch
+ this.prefObserver.register();
+ this.prefObserver.on(PREF_CHANGE_EVENT, this._onPrefChange);
+
+ // Bind to menupopup's open and close event
+ this.menupopup.addEventListener("popupshown", this._onPopupShown);
+ this.menupopup.addEventListener("popuphidden", this._onPopupHidden);
+ },
+
+ /**
+ * Removes event handlers for all of the option buttons and
+ * preference observer.
+ */
+ destroy: function () {
+ this.mutationObserver.disconnect();
+ this.prefObserver.off(PREF_CHANGE_EVENT, this._onPrefChange);
+ this.menupopup.removeEventListener("popupshown", this._onPopupShown);
+ this.menupopup.removeEventListener("popuphidden", this._onPopupHidden);
+ },
+
+ /**
+ * Returns the value for the specified `prefName`
+ */
+ getPref: function (prefName) {
+ return this.prefObserver.get(prefName);
+ },
+
+ /**
+ * Called when a preference is changed (either via clicking an option
+ * button or by changing it in about:config). Updates the checked status
+ * of the corresponding button.
+ */
+ _onPrefChange: function (_, prefName) {
+ let $el = this.$(`menuitem[data-pref="${prefName}"]`, this.menupopup);
+ let value = this.prefObserver.get(prefName);
+
+ // If options panel does not contain a menuitem for the
+ // pref, emit an event and do nothing.
+ if (!$el) {
+ this.emit(PREF_CHANGE_EVENT, prefName);
+ return;
+ }
+
+ if (value) {
+ $el.setAttribute("checked", value);
+ } else {
+ $el.removeAttribute("checked");
+ }
+
+ this.emit(PREF_CHANGE_EVENT, prefName);
+ },
+
+ /**
+ * Mutation handler for handling a change on an options button.
+ * Sets the preference accordingly.
+ */
+ _onOptionChange: function (mutations) {
+ let { target } = mutations[0];
+ let prefName = target.getAttribute("data-pref");
+ let value = target.getAttribute("checked") === "true";
+
+ this.prefObserver.set(prefName, value);
+ },
+
+ /**
+ * Fired when the `menupopup` is opened, bound via XUL.
+ * Fires an event used in tests.
+ */
+ _onPopupShown: function () {
+ this.button.setAttribute("open", true);
+ this.emit(OPTIONS_SHOWN_EVENT);
+ },
+
+ /**
+ * Fired when the `menupopup` is closed, bound via XUL.
+ * Fires an event used in tests.
+ */
+ _onPopupHidden: function () {
+ this.button.removeAttribute("open");
+ this.emit(OPTIONS_HIDDEN_EVENT);
+ }
+};
+
+/**
+ * Constructor for PrefObserver. Small helper for observing changes
+ * on a preference branch. Takes a `branchName`, like "devtools.debugger."
+ *
+ * Fires an event of PREF_CHANGE_EVENT with the preference name that changed
+ * as the second argument in the listener.
+ */
+const PrefObserver = function (branchName) {
+ this.branchName = branchName;
+ this.branch = Services.prefs.getBranch(branchName);
+ EventEmitter.decorate(this);
+};
+
+PrefObserver.prototype = {
+ /**
+ * Returns `prefName`'s value. Does not require the branch name.
+ */
+ get: function (prefName) {
+ let fullName = this.branchName + prefName;
+ return Preferences.get(fullName);
+ },
+ /**
+ * Sets `prefName`'s `value`. Does not require the branch name.
+ */
+ set: function (prefName, value) {
+ let fullName = this.branchName + prefName;
+ Preferences.set(fullName, value);
+ },
+ register: function () {
+ this.branch.addObserver("", this, false);
+ },
+ unregister: function () {
+ this.branch.removeObserver("", this);
+ },
+ observe: function (subject, topic, prefName) {
+ this.emit(PREF_CHANGE_EVENT, prefName);
+ }
+};
diff --git a/devtools/client/shared/output-parser.js b/devtools/client/shared/output-parser.js
new file mode 100644
index 000000000..726c93b8b
--- /dev/null
+++ b/devtools/client/shared/output-parser.js
@@ -0,0 +1,695 @@
+/* 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 {angleUtils} = require("devtools/client/shared/css-angle");
+const {colorUtils} = require("devtools/shared/css/color");
+const {getCSSLexer} = require("devtools/shared/css/lexer");
+const EventEmitter = require("devtools/shared/event-emitter");
+const {
+ ANGLE_TAKING_FUNCTIONS,
+ BEZIER_KEYWORDS,
+ COLOR_TAKING_FUNCTIONS,
+ CSS_TYPES
+} = require("devtools/shared/css/properties-db");
+const Services = require("Services");
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const CSS_GRID_ENABLED_PREF = "layout.css.grid.enabled";
+
+/**
+ * This module is used to process text for output by developer tools. This means
+ * linking JS files with the debugger, CSS files with the style editor, JS
+ * functions with the debugger, placing color swatches next to colors and
+ * adding doorhanger previews where possible (images, angles, lengths,
+ * border radius, cubic-bezier etc.).
+ *
+ * Usage:
+ * const {OutputParser} = require("devtools/client/shared/output-parser");
+ *
+ * let parser = new OutputParser(document, supportsType);
+ *
+ * parser.parseCssProperty("color", "red"); // Returns document fragment.
+ *
+ * @param {Document} document Used to create DOM nodes.
+ * @param {Function} supportsTypes - A function that returns a boolean when asked if a css
+ * property name supports a given css type.
+ * The function is executed like supportsType("color", CSS_TYPES.COLOR)
+ * where CSS_TYPES is defined in devtools/shared/css/properties-db.js
+ * @param {Function} isValidOnClient - A function that checks if a css property
+ * name/value combo is valid.
+ */
+function OutputParser(document, {supportsType, isValidOnClient}) {
+ this.parsed = [];
+ this.doc = document;
+ this.supportsType = supportsType;
+ this.isValidOnClient = isValidOnClient;
+ this.colorSwatches = new WeakMap();
+ this.angleSwatches = new WeakMap();
+ this._onColorSwatchMouseDown = this._onColorSwatchMouseDown.bind(this);
+ this._onAngleSwatchMouseDown = this._onAngleSwatchMouseDown.bind(this);
+}
+
+exports.OutputParser = OutputParser;
+
+OutputParser.prototype = {
+ /**
+ * Parse a CSS property value given a property name.
+ *
+ * @param {String} name
+ * CSS Property Name
+ * @param {String} value
+ * CSS Property value
+ * @param {Object} [options]
+ * Options object. For valid options and default values see
+ * _mergeOptions().
+ * @return {DocumentFragment}
+ * A document fragment containing color swatches etc.
+ */
+ parseCssProperty: function (name, value, options = {}) {
+ options = this._mergeOptions(options);
+
+ options.expectCubicBezier = this.supportsType(name, CSS_TYPES.TIMING_FUNCTION);
+ options.expectDisplay = name === "display";
+ options.expectFilter = name === "filter";
+ options.supportsColor = this.supportsType(name, CSS_TYPES.COLOR) ||
+ this.supportsType(name, CSS_TYPES.GRADIENT);
+
+ // The filter property is special in that we want to show the
+ // swatch even if the value is invalid, because this way the user
+ // can easily use the editor to fix it.
+ if (options.expectFilter || this._cssPropertySupportsValue(name, value)) {
+ return this._parse(value, options);
+ }
+ this._appendTextNode(value);
+
+ return this._toDOM();
+ },
+
+ /**
+ * Given an initial FUNCTION token, read tokens from |tokenStream|
+ * and collect all the (non-comment) text. Return the collected
+ * text. The function token and the close paren are included in the
+ * result.
+ *
+ * @param {CSSToken} initialToken
+ * The FUNCTION token.
+ * @param {String} text
+ * The original CSS text.
+ * @param {CSSLexer} tokenStream
+ * The token stream from which to read.
+ * @return {String}
+ * The text of body of the function call.
+ */
+ _collectFunctionText: function (initialToken, text, tokenStream) {
+ let result = text.substring(initialToken.startOffset,
+ initialToken.endOffset);
+ let depth = 1;
+ while (depth > 0) {
+ let token = tokenStream.nextToken();
+ if (!token) {
+ break;
+ }
+ if (token.tokenType === "comment") {
+ continue;
+ }
+ result += text.substring(token.startOffset, token.endOffset);
+ if (token.tokenType === "symbol") {
+ if (token.text === "(") {
+ ++depth;
+ } else if (token.text === ")") {
+ --depth;
+ }
+ } else if (token.tokenType === "function") {
+ ++depth;
+ }
+ }
+ return result;
+ },
+
+ /**
+ * Parse a string.
+ *
+ * @param {String} text
+ * Text to parse.
+ * @param {Object} [options]
+ * Options object. For valid options and default values see
+ * _mergeOptions().
+ * @return {DocumentFragment}
+ * A document fragment.
+ */
+ _parse: function (text, options = {}) {
+ text = text.trim();
+ this.parsed.length = 0;
+
+ let tokenStream = getCSSLexer(text);
+ let parenDepth = 0;
+ let outerMostFunctionTakesColor = false;
+
+ let colorOK = function () {
+ return options.supportsColor ||
+ (options.expectFilter && parenDepth === 1 &&
+ outerMostFunctionTakesColor);
+ };
+
+ let angleOK = function (angle) {
+ return (new angleUtils.CssAngle(angle)).valid;
+ };
+
+ while (true) {
+ let token = tokenStream.nextToken();
+ if (!token) {
+ break;
+ }
+ if (token.tokenType === "comment") {
+ continue;
+ }
+
+ switch (token.tokenType) {
+ case "function": {
+ if (COLOR_TAKING_FUNCTIONS.includes(token.text) ||
+ ANGLE_TAKING_FUNCTIONS.includes(token.text)) {
+ // The function can accept a color or an angle argument, and we know
+ // it isn't special in some other way. So, we let it
+ // through to the ordinary parsing loop so that the value
+ // can be handled in a single place.
+ this._appendTextNode(text.substring(token.startOffset,
+ token.endOffset));
+ if (parenDepth === 0) {
+ outerMostFunctionTakesColor = COLOR_TAKING_FUNCTIONS.includes(
+ token.text);
+ }
+ ++parenDepth;
+ } else {
+ let functionText = this._collectFunctionText(token, text,
+ tokenStream);
+
+ if (options.expectCubicBezier && token.text === "cubic-bezier") {
+ this._appendCubicBezier(functionText, options);
+ } else if (colorOK() && colorUtils.isValidCSSColor(functionText)) {
+ this._appendColor(functionText, options);
+ } else {
+ this._appendTextNode(functionText);
+ }
+ }
+ break;
+ }
+
+ case "ident":
+ if (options.expectCubicBezier &&
+ BEZIER_KEYWORDS.indexOf(token.text) >= 0) {
+ this._appendCubicBezier(token.text, options);
+ } else if (Services.prefs.getBoolPref(CSS_GRID_ENABLED_PREF) &&
+ options.expectDisplay && token.text === "grid" &&
+ text === token.text) {
+ this._appendGrid(token.text, options);
+ } else if (colorOK() && colorUtils.isValidCSSColor(token.text)) {
+ this._appendColor(token.text, options);
+ } else if (angleOK(token.text)) {
+ this._appendAngle(token.text, options);
+ } else {
+ this._appendTextNode(text.substring(token.startOffset,
+ token.endOffset));
+ }
+ break;
+
+ case "id":
+ case "hash": {
+ let original = text.substring(token.startOffset, token.endOffset);
+ if (colorOK() && colorUtils.isValidCSSColor(original)) {
+ this._appendColor(original, options);
+ } else {
+ this._appendTextNode(original);
+ }
+ break;
+ }
+ case "dimension":
+ let value = text.substring(token.startOffset, token.endOffset);
+ if (angleOK(value)) {
+ this._appendAngle(value, options);
+ } else {
+ this._appendTextNode(value);
+ }
+ break;
+ case "url":
+ case "bad_url":
+ this._appendURL(text.substring(token.startOffset, token.endOffset),
+ token.text, options);
+ break;
+
+ case "symbol":
+ if (token.text === "(") {
+ ++parenDepth;
+ } else if (token.text === ")") {
+ --parenDepth;
+ if (parenDepth === 0) {
+ outerMostFunctionTakesColor = false;
+ }
+ }
+ // falls through
+ default:
+ this._appendTextNode(
+ text.substring(token.startOffset, token.endOffset));
+ break;
+ }
+ }
+
+ let result = this._toDOM();
+
+ if (options.expectFilter && !options.filterSwatch) {
+ result = this._wrapFilter(text, options, result);
+ }
+
+ return result;
+ },
+
+ /**
+ * Append a cubic-bezier timing function value to the output
+ *
+ * @param {String} bezier
+ * The cubic-bezier timing function
+ * @param {Object} options
+ * Options object. For valid options and default values see
+ * _mergeOptions()
+ */
+ _appendCubicBezier: function (bezier, options) {
+ let container = this._createNode("span", {
+ "data-bezier": bezier
+ });
+
+ if (options.bezierSwatchClass) {
+ let swatch = this._createNode("span", {
+ class: options.bezierSwatchClass
+ });
+ container.appendChild(swatch);
+ }
+
+ let value = this._createNode("span", {
+ class: options.bezierClass
+ }, bezier);
+
+ container.appendChild(value);
+ this.parsed.push(container);
+ },
+
+ /**
+ * Append a CSS Grid highlighter toggle icon next to the value in a
+ * 'display: grid' declaration
+ *
+ * @param {String} grid
+ * The grid text value to append
+ * @param {Object} options
+ * Options object. For valid options and default values see
+ * _mergeOptions()
+ */
+ _appendGrid: function (grid, options) {
+ let container = this._createNode("span", {});
+
+ let toggle = this._createNode("span", {
+ class: options.gridClass
+ });
+
+ let value = this._createNode("span", {});
+ value.textContent = grid;
+
+ container.appendChild(toggle);
+ container.appendChild(value);
+ this.parsed.push(container);
+ },
+
+ /**
+ * Append a angle value to the output
+ *
+ * @param {String} angle
+ * angle to append
+ * @param {Object} options
+ * Options object. For valid options and default values see
+ * _mergeOptions()
+ */
+ _appendAngle: function (angle, options) {
+ let angleObj = new angleUtils.CssAngle(angle);
+ let container = this._createNode("span", {
+ "data-angle": angle
+ });
+
+ if (options.angleSwatchClass) {
+ let swatch = this._createNode("span", {
+ class: options.angleSwatchClass
+ });
+ this.angleSwatches.set(swatch, angleObj);
+ swatch.addEventListener("mousedown", this._onAngleSwatchMouseDown, false);
+
+ // Add click listener to stop event propagation when shift key is pressed
+ // in order to prevent the value input to be focused.
+ // Bug 711942 will add a tooltip to edit angle values and we should
+ // be able to move this listener to Tooltip.js when it'll be implemented.
+ swatch.addEventListener("click", function (event) {
+ if (event.shiftKey) {
+ event.stopPropagation();
+ }
+ }, false);
+ EventEmitter.decorate(swatch);
+ container.appendChild(swatch);
+ }
+
+ let value = this._createNode("span", {
+ class: options.angleClass
+ }, angle);
+
+ container.appendChild(value);
+ this.parsed.push(container);
+ },
+
+ /**
+ * Check if a CSS property supports a specific value.
+ *
+ * @param {String} name
+ * CSS Property name to check
+ * @param {String} value
+ * CSS Property value to check
+ */
+ _cssPropertySupportsValue: function (name, value) {
+ return this.isValidOnClient(name, value, this.doc);
+ },
+
+ /**
+ * Tests if a given colorObject output by CssColor is valid for parsing.
+ * Valid means it's really a color, not any of the CssColor SPECIAL_VALUES
+ * except transparent
+ */
+ _isValidColor: function (colorObj) {
+ return colorObj.valid &&
+ (!colorObj.specialValue || colorObj.specialValue === "transparent");
+ },
+
+ /**
+ * Append a color to the output.
+ *
+ * @param {String} color
+ * Color to append
+ * @param {Object} [options]
+ * Options object. For valid options and default values see
+ * _mergeOptions().
+ */
+ _appendColor: function (color, options = {}) {
+ let colorObj = new colorUtils.CssColor(color);
+
+ if (this._isValidColor(colorObj)) {
+ let container = this._createNode("span", {
+ "data-color": color
+ });
+
+ if (options.colorSwatchClass) {
+ let swatch = this._createNode("span", {
+ class: options.colorSwatchClass,
+ style: "background-color:" + color
+ });
+ this.colorSwatches.set(swatch, colorObj);
+ swatch.addEventListener("mousedown", this._onColorSwatchMouseDown,
+ false);
+ EventEmitter.decorate(swatch);
+ container.appendChild(swatch);
+ }
+
+ if (options.defaultColorType) {
+ color = colorObj.toString();
+ container.dataset.color = color;
+ }
+
+ let value = this._createNode("span", {
+ class: options.colorClass
+ }, color);
+
+ container.appendChild(value);
+ this.parsed.push(container);
+ } else {
+ this._appendTextNode(color);
+ }
+ },
+
+ /**
+ * Wrap some existing nodes in a filter editor.
+ *
+ * @param {String} filters
+ * The full text of the "filter" property.
+ * @param {object} options
+ * The options object passed to parseCssProperty().
+ * @param {object} nodes
+ * Nodes created by _toDOM().
+ *
+ * @returns {object}
+ * A new node that supplies a filter swatch and that wraps |nodes|.
+ */
+ _wrapFilter: function (filters, options, nodes) {
+ let container = this._createNode("span", {
+ "data-filters": filters
+ });
+
+ if (options.filterSwatchClass) {
+ let swatch = this._createNode("span", {
+ class: options.filterSwatchClass
+ });
+ container.appendChild(swatch);
+ }
+
+ let value = this._createNode("span", {
+ class: options.filterClass
+ });
+ value.appendChild(nodes);
+ container.appendChild(value);
+
+ return container;
+ },
+
+ _onColorSwatchMouseDown: function (event) {
+ if (!event.shiftKey) {
+ return;
+ }
+
+ // Prevent click event to be fired to not show the tooltip
+ event.stopPropagation();
+
+ let swatch = event.target;
+ let color = this.colorSwatches.get(swatch);
+ let val = color.nextColorUnit();
+
+ swatch.nextElementSibling.textContent = val;
+ swatch.emit("unit-change", val);
+ },
+
+ _onAngleSwatchMouseDown: function (event) {
+ if (!event.shiftKey) {
+ return;
+ }
+
+ event.stopPropagation();
+
+ let swatch = event.target;
+ let angle = this.angleSwatches.get(swatch);
+ let val = angle.nextAngleUnit();
+
+ swatch.nextElementSibling.textContent = val;
+ swatch.emit("unit-change", val);
+ },
+
+ /**
+ * A helper function that sanitizes a possibly-unterminated URL.
+ */
+ _sanitizeURL: function (url) {
+ // Re-lex the URL and add any needed termination characters.
+ let urlTokenizer = getCSSLexer(url);
+ // Just read until EOF; there will only be a single token.
+ while (urlTokenizer.nextToken()) {
+ // Nothing.
+ }
+
+ return urlTokenizer.performEOFFixup(url, true);
+ },
+
+ /**
+ * Append a URL to the output.
+ *
+ * @param {String} match
+ * Complete match that may include "url(xxx)"
+ * @param {String} url
+ * Actual URL
+ * @param {Object} [options]
+ * Options object. For valid options and default values see
+ * _mergeOptions().
+ */
+ _appendURL: function (match, url, options) {
+ if (options.urlClass) {
+ // Sanitize the URL. Note that if we modify the URL, we just
+ // leave the termination characters. This isn't strictly
+ // "as-authored", but it makes a bit more sense.
+ match = this._sanitizeURL(match);
+ // This regexp matches a URL token. It puts the "url(", any
+ // leading whitespace, and any opening quote into |leader|; the
+ // URL text itself into |body|, and any trailing quote, trailing
+ // whitespace, and the ")" into |trailer|. We considered adding
+ // functionality for this to CSSLexer, in some way, but this
+ // seemed simpler on the whole.
+ let [, leader, , body, trailer] =
+ /^(url\([ \t\r\n\f]*(["']?))(.*?)(\2[ \t\r\n\f]*\))$/i.exec(match);
+
+ this._appendTextNode(leader);
+
+ let href = url;
+ if (options.baseURI) {
+ try {
+ href = new URL(url, options.baseURI).href;
+ } catch (e) {
+ // Ignore.
+ }
+ }
+
+ this._appendNode("a", {
+ target: "_blank",
+ class: options.urlClass,
+ href: href
+ }, body);
+
+ this._appendTextNode(trailer);
+ } else {
+ this._appendTextNode(match);
+ }
+ },
+
+ /**
+ * Create a node.
+ *
+ * @param {String} tagName
+ * Tag type e.g. "div"
+ * @param {Object} attributes
+ * e.g. {class: "someClass", style: "cursor:pointer"};
+ * @param {String} [value]
+ * If a value is included it will be appended as a text node inside
+ * the tag. This is useful e.g. for span tags.
+ * @return {Node} Newly created Node.
+ */
+ _createNode: function (tagName, attributes, value = "") {
+ let node = this.doc.createElementNS(HTML_NS, tagName);
+ let attrs = Object.getOwnPropertyNames(attributes);
+
+ for (let attr of attrs) {
+ if (attributes[attr]) {
+ node.setAttribute(attr, attributes[attr]);
+ }
+ }
+
+ if (value) {
+ let textNode = this.doc.createTextNode(value);
+ node.appendChild(textNode);
+ }
+
+ return node;
+ },
+
+ /**
+ * Append a node to the output.
+ *
+ * @param {String} tagName
+ * Tag type e.g. "div"
+ * @param {Object} attributes
+ * e.g. {class: "someClass", style: "cursor:pointer"};
+ * @param {String} [value]
+ * If a value is included it will be appended as a text node inside
+ * the tag. This is useful e.g. for span tags.
+ */
+ _appendNode: function (tagName, attributes, value = "") {
+ let node = this._createNode(tagName, attributes, value);
+ this.parsed.push(node);
+ },
+
+ /**
+ * Append a text node to the output. If the previously output item was a text
+ * node then we append the text to that node.
+ *
+ * @param {String} text
+ * Text to append
+ */
+ _appendTextNode: function (text) {
+ let lastItem = this.parsed[this.parsed.length - 1];
+ if (typeof lastItem === "string") {
+ this.parsed[this.parsed.length - 1] = lastItem + text;
+ } else {
+ this.parsed.push(text);
+ }
+ },
+
+ /**
+ * Take all output and append it into a single DocumentFragment.
+ *
+ * @return {DocumentFragment}
+ * Document Fragment
+ */
+ _toDOM: function () {
+ let frag = this.doc.createDocumentFragment();
+
+ for (let item of this.parsed) {
+ if (typeof item === "string") {
+ frag.appendChild(this.doc.createTextNode(item));
+ } else {
+ frag.appendChild(item);
+ }
+ }
+
+ this.parsed.length = 0;
+ return frag;
+ },
+
+ /**
+ * Merges options objects. Default values are set here.
+ *
+ * @param {Object} overrides
+ * The option values to override e.g. _mergeOptions({colors: false})
+ *
+ * Valid options are:
+ * - defaultColorType: true // Convert colors to the default type
+ * // selected in the options panel.
+ * - angleClass: "" // The class to use for the angle value
+ * // that follows the swatch.
+ * - angleSwatchClass: "" // The class to use for angle swatches.
+ * - bezierClass: "" // The class to use for the bezier value
+ * // that follows the swatch.
+ * - bezierSwatchClass: "" // The class to use for bezier swatches.
+ * - colorClass: "" // The class to use for the color value
+ * // that follows the swatch.
+ * - colorSwatchClass: "" // The class to use for color swatches.
+ * - filterSwatch: false // A special case for parsing a
+ * // "filter" property, causing the
+ * // parser to skip the call to
+ * // _wrapFilter. Used only for
+ * // previewing with the filter swatch.
+ * - gridClass: "" // The class to use for the grid icon.
+ * - supportsColor: false // Does the CSS property support colors?
+ * - urlClass: "" // The class to be used for url() links.
+ * - baseURI: undefined // A string used to resolve
+ * // relative links.
+ * @return {Object}
+ * Overridden options object
+ */
+ _mergeOptions: function (overrides) {
+ let defaults = {
+ defaultColorType: true,
+ angleClass: "",
+ angleSwatchClass: "",
+ bezierClass: "",
+ bezierSwatchClass: "",
+ colorClass: "",
+ colorSwatchClass: "",
+ filterSwatch: false,
+ gridClass: "",
+ supportsColor: false,
+ urlClass: "",
+ baseURI: undefined,
+ };
+
+ for (let item in overrides) {
+ defaults[item] = overrides[item];
+ }
+ return defaults;
+ }
+};
diff --git a/devtools/client/shared/poller.js b/devtools/client/shared/poller.js
new file mode 100644
index 000000000..961f81a27
--- /dev/null
+++ b/devtools/client/shared/poller.js
@@ -0,0 +1,114 @@
+/* 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";
+loader.lazyRequireGetter(this, "defer",
+ "promise", true);
+
+/**
+ * @constructor Poller
+ * Takes a function that is to be called on an interval,
+ * and can be turned on and off via methods to execute `fn` on the interval
+ * specified during `on`. If `fn` returns a promise, the polling waits for
+ * that promise to resolve before waiting the interval to call again.
+ *
+ * Specify the `wait` duration between polling here, and optionally
+ * an `immediate` boolean, indicating whether the function should be called
+ * immediately when toggling on.
+ *
+ * @param {function} fn
+ * @param {number} wait
+ * @param {boolean?} immediate
+ */
+function Poller(fn, wait, immediate) {
+ this._fn = fn;
+ this._wait = wait;
+ this._immediate = immediate;
+ this._poll = this._poll.bind(this);
+ this._preparePoll = this._preparePoll.bind(this);
+}
+exports.Poller = Poller;
+
+/**
+ * Returns a boolean indicating whether or not poller
+ * is polling.
+ *
+ * @return {boolean}
+ */
+Poller.prototype.isPolling = function pollerIsPolling() {
+ return !!this._timer;
+};
+
+/**
+ * Turns polling on.
+ *
+ * @return {Poller}
+ */
+Poller.prototype.on = function pollerOn() {
+ if (this._destroyed) {
+ throw Error("Poller cannot be turned on after destruction.");
+ }
+ if (this._timer) {
+ this.off();
+ }
+ this._immediate ? this._poll() : this._preparePoll();
+ return this;
+};
+
+/**
+ * Turns off polling. Returns a promise that resolves when
+ * the last outstanding `fn` call finishes if it's an async function.
+ *
+ * @return {Promise}
+ */
+Poller.prototype.off = function pollerOff() {
+ let { resolve, promise } = defer();
+ if (this._timer) {
+ clearTimeout(this._timer);
+ this._timer = null;
+ }
+
+ // Settle an inflight poll call before resolving
+ // if using a promise-backed poll function
+ if (this._inflight) {
+ this._inflight.then(resolve);
+ } else {
+ resolve();
+ }
+ return promise;
+};
+
+/**
+ * Turns off polling and removes the reference to the poller function.
+ * Resolves when the last outstanding `fn` call finishes if it's an async
+ * function.
+ */
+Poller.prototype.destroy = function pollerDestroy() {
+ return this.off().then(() => {
+ this._destroyed = true;
+ this._fn = null;
+ });
+};
+
+Poller.prototype._preparePoll = function pollerPrepare() {
+ this._timer = setTimeout(this._poll, this._wait);
+};
+
+Poller.prototype._poll = function pollerPoll() {
+ let response = this._fn();
+ if (response && typeof response.then === "function") {
+ // Store the most recent in-flight polling
+ // call so we can clean it up when disabling
+ this._inflight = response;
+ response.then(() => {
+ // Only queue up the next call if poller was not turned off
+ // while this async poll call was in flight.
+ if (this._timer) {
+ this._preparePoll();
+ }
+ });
+ } else {
+ this._preparePoll();
+ }
+};
diff --git a/devtools/client/shared/prefs.js b/devtools/client/shared/prefs.js
new file mode 100644
index 000000000..9b44d4d58
--- /dev/null
+++ b/devtools/client/shared/prefs.js
@@ -0,0 +1,178 @@
+/* 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 Services = require("Services");
+const EventEmitter = require("devtools/shared/event-emitter");
+
+/**
+ * Shortcuts for lazily accessing and setting various preferences.
+ * Usage:
+ * let prefs = new Prefs("root.path.to.branch", {
+ * myIntPref: ["Int", "leaf.path.to.my-int-pref"],
+ * myCharPref: ["Char", "leaf.path.to.my-char-pref"],
+ * myJsonPref: ["Json", "leaf.path.to.my-json-pref"],
+ * myFloatPref: ["Float", "leaf.path.to.my-float-pref"]
+ * ...
+ * });
+ *
+ * Get/set:
+ * prefs.myCharPref = "foo";
+ * let aux = prefs.myCharPref;
+ *
+ * Observe:
+ * prefs.registerObserver();
+ * prefs.on("pref-changed", (prefName, prefValue) => {
+ * ...
+ * });
+ *
+ * @param string prefsRoot
+ * The root path to the required preferences branch.
+ * @param object prefsBlueprint
+ * An object containing { accessorName: [prefType, prefName] } keys.
+ */
+function PrefsHelper(prefsRoot = "", prefsBlueprint = {}) {
+ EventEmitter.decorate(this);
+
+ let cache = new Map();
+
+ for (let accessorName in prefsBlueprint) {
+ let [prefType, prefName] = prefsBlueprint[accessorName];
+ map(this, cache, accessorName, prefType, prefsRoot, prefName);
+ }
+
+ let observer = makeObserver(this, cache, prefsRoot, prefsBlueprint);
+ this.registerObserver = () => observer.register();
+ this.unregisterObserver = () => observer.unregister();
+}
+
+/**
+ * Helper method for getting a pref value.
+ *
+ * @param Map cache
+ * @param string prefType
+ * @param string prefsRoot
+ * @param string prefName
+ * @return any
+ */
+function get(cache, prefType, prefsRoot, prefName) {
+ let cachedPref = cache.get(prefName);
+ if (cachedPref !== undefined) {
+ return cachedPref;
+ }
+ let value = Services.prefs["get" + prefType + "Pref"](
+ [prefsRoot, prefName].join(".")
+ );
+ cache.set(prefName, value);
+ return value;
+}
+
+/**
+ * Helper method for setting a pref value.
+ *
+ * @param Map cache
+ * @param string prefType
+ * @param string prefsRoot
+ * @param string prefName
+ * @param any value
+ */
+function set(cache, prefType, prefsRoot, prefName, value) {
+ Services.prefs["set" + prefType + "Pref"](
+ [prefsRoot, prefName].join("."),
+ value
+ );
+ cache.set(prefName, value);
+}
+
+/**
+ * Maps a property name to a pref, defining lazy getters and setters.
+ * Supported types are "Bool", "Char", "Int", "Float" (sugar around "Char"
+ * type and casting), and "Json" (which is basically just sugar for "Char"
+ * using the standard JSON serializer).
+ *
+ * @param PrefsHelper self
+ * @param Map cache
+ * @param string accessorName
+ * @param string prefType
+ * @param string prefsRoot
+ * @param string prefName
+ * @param array serializer [optional]
+ */
+function map(self, cache, accessorName, prefType, prefsRoot, prefName,
+ serializer = { in: e => e, out: e => e }) {
+ if (prefName in self) {
+ throw new Error(`Can't use ${prefName} because it overrides a property` +
+ "on the instance.");
+ }
+ if (prefType == "Json") {
+ map(self, cache, accessorName, "Char", prefsRoot, prefName, {
+ in: JSON.parse,
+ out: JSON.stringify
+ });
+ return;
+ }
+ if (prefType == "Float") {
+ map(self, cache, accessorName, "Char", prefsRoot, prefName, {
+ in: Number.parseFloat,
+ out: (n) => n + ""
+ });
+ return;
+ }
+
+ Object.defineProperty(self, accessorName, {
+ get: () => serializer.in(get(cache, prefType, prefsRoot, prefName)),
+ set: (e) => set(cache, prefType, prefsRoot, prefName, serializer.out(e))
+ });
+}
+
+/**
+ * Finds the accessor for the provided pref, based on the blueprint object
+ * used in the constructor.
+ *
+ * @param PrefsHelper self
+ * @param object prefsBlueprint
+ * @return string
+ */
+function accessorNameForPref(somePrefName, prefsBlueprint) {
+ for (let accessorName in prefsBlueprint) {
+ let [, prefName] = prefsBlueprint[accessorName];
+ if (somePrefName == prefName) {
+ return accessorName;
+ }
+ }
+ return "";
+}
+
+/**
+ * Creates a pref observer for `self`.
+ *
+ * @param PrefsHelper self
+ * @param Map cache
+ * @param string prefsRoot
+ * @param object prefsBlueprint
+ * @return object
+ */
+function makeObserver(self, cache, prefsRoot, prefsBlueprint) {
+ return {
+ register: function () {
+ this._branch = Services.prefs.getBranch(prefsRoot + ".");
+ this._branch.addObserver("", this, false);
+ },
+ unregister: function () {
+ this._branch.removeObserver("", this);
+ },
+ observe: function (subject, topic, prefName) {
+ // If this particular pref isn't handled by the blueprint object,
+ // even though it's in the specified branch, ignore it.
+ let accessorName = accessorNameForPref(prefName, prefsBlueprint);
+ if (!(accessorName in self)) {
+ return;
+ }
+ cache.delete(prefName);
+ self.emit("pref-changed", accessorName, self[accessorName]);
+ }
+ };
+}
+
+exports.PrefsHelper = PrefsHelper;
diff --git a/devtools/client/shared/redux/create-store.js b/devtools/client/shared/redux/create-store.js
new file mode 100644
index 000000000..baacf428e
--- /dev/null
+++ b/devtools/client/shared/redux/create-store.js
@@ -0,0 +1,51 @@
+/* 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 { createStore, applyMiddleware } = require("devtools/client/shared/vendor/redux");
+const { thunk } = require("./middleware/thunk");
+const { waitUntilService } = require("./middleware/wait-service");
+const { task } = require("./middleware/task");
+const { log } = require("./middleware/log");
+const { promise } = require("./middleware/promise");
+const { history } = require("./middleware/history");
+
+/**
+ * This creates a dispatcher with all the standard middleware in place
+ * that all code requires. It can also be optionally configured in
+ * various ways, such as logging and recording.
+ *
+ * @param {object} opts:
+ * - log: log all dispatched actions to console
+ * - history: an array to store every action in. Should only be
+ * used in tests.
+ * - middleware: array of middleware to be included in the redux store
+ */
+module.exports = (opts = {}) => {
+ const middleware = [
+ task,
+ thunk,
+ promise,
+
+ // Order is important: services must go last as they always
+ // operate on "already transformed" actions. Actions going through
+ // them shouldn't have any special fields like promises, they
+ // should just be normal JSON objects.
+ waitUntilService
+ ];
+
+ if (opts.history) {
+ middleware.push(history(opts.history));
+ }
+
+ if (opts.middleware) {
+ opts.middleware.forEach(fn => middleware.push(fn));
+ }
+
+ if (opts.log) {
+ middleware.push(log);
+ }
+
+ return applyMiddleware(...middleware)(createStore);
+};
diff --git a/devtools/client/shared/redux/middleware/history.js b/devtools/client/shared/redux/middleware/history.js
new file mode 100644
index 000000000..dba88c045
--- /dev/null
+++ b/devtools/client/shared/redux/middleware/history.js
@@ -0,0 +1,23 @@
+/* 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 flags = require("devtools/shared/flags");
+
+/**
+ * A middleware that stores every action coming through the store in the passed
+ * in logging object. Should only be used for tests, as it collects all
+ * action information, which will cause memory bloat.
+ */
+exports.history = (log = []) => ({ dispatch, getState }) => {
+ if (!flags.testing) {
+ console.warn("Using history middleware stores all actions in state for " +
+ "testing and devtools is not currently running in test " +
+ "mode. Be sure this is intentional.");
+ }
+ return next => action => {
+ log.push(action);
+ next(action);
+ };
+};
diff --git a/devtools/client/shared/redux/middleware/log.js b/devtools/client/shared/redux/middleware/log.js
new file mode 100644
index 000000000..f812f793b
--- /dev/null
+++ b/devtools/client/shared/redux/middleware/log.js
@@ -0,0 +1,17 @@
+/* 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";
+
+/**
+ * A middleware that logs all actions coming through the system
+ * to the console.
+ */
+function log({ dispatch, getState }) {
+ return next => action => {
+ console.log("[DISPATCH]", JSON.stringify(action, null, 2));
+ next(action);
+ };
+}
+
+exports.log = log;
diff --git a/devtools/client/shared/redux/middleware/moz.build b/devtools/client/shared/redux/middleware/moz.build
new file mode 100644
index 000000000..a25bfd518
--- /dev/null
+++ b/devtools/client/shared/redux/middleware/moz.build
@@ -0,0 +1,16 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'history.js',
+ 'log.js',
+ 'promise.js',
+ 'task.js',
+ 'thunk.js',
+ 'wait-service.js',
+)
+
+XPCSHELL_TESTS_MANIFESTS += ['test/xpcshell.ini']
diff --git a/devtools/client/shared/redux/middleware/promise.js b/devtools/client/shared/redux/middleware/promise.js
new file mode 100644
index 000000000..237e41eef
--- /dev/null
+++ b/devtools/client/shared/redux/middleware/promise.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 uuidgen = require("sdk/util/uuid").uuid;
+const defer = require("devtools/shared/defer");
+const {
+ entries, toObject, executeSoon
+} = require("devtools/shared/DevToolsUtils");
+const PROMISE = exports.PROMISE = "@@dispatch/promise";
+
+function promiseMiddleware({ dispatch, getState }) {
+ return next => action => {
+ if (!(PROMISE in action)) {
+ return next(action);
+ }
+
+ const promiseInst = action[PROMISE];
+ const seqId = uuidgen().toString();
+
+ // Create a new action that doesn't have the promise field and has
+ // the `seqId` field that represents the sequence id
+ action = Object.assign(
+ toObject(entries(action).filter(pair => pair[0] !== PROMISE)), { seqId }
+ );
+
+ dispatch(Object.assign({}, action, { status: "start" }));
+
+ // Return the promise so action creators can still compose if they
+ // want to.
+ const deferred = defer();
+ promiseInst.then(value => {
+ executeSoon(() => {
+ dispatch(Object.assign({}, action, {
+ status: "done",
+ value: value
+ }));
+ deferred.resolve(value);
+ });
+ }, error => {
+ executeSoon(() => {
+ dispatch(Object.assign({}, action, {
+ status: "error",
+ error: error.message || error
+ }));
+ deferred.reject(error);
+ });
+ });
+ return deferred.promise;
+ };
+}
+
+exports.promise = promiseMiddleware;
diff --git a/devtools/client/shared/redux/middleware/task.js b/devtools/client/shared/redux/middleware/task.js
new file mode 100644
index 000000000..c1dd262ee
--- /dev/null
+++ b/devtools/client/shared/redux/middleware/task.js
@@ -0,0 +1,42 @@
+/* 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 { Task } = require("devtools/shared/task");
+const { executeSoon, isGenerator, reportException } = require("devtools/shared/DevToolsUtils");
+const ERROR_TYPE = exports.ERROR_TYPE = "@@redux/middleware/task#error";
+
+/**
+ * A middleware that allows generator thunks (functions) and promise
+ * to be dispatched. If it's a generator, it is called with `dispatch`
+ * and `getState`, allowing the action to create multiple actions (most likely
+ * asynchronously) and yield on each. If called with a promise, calls `dispatch`
+ * on the results.
+ */
+
+function task({ dispatch, getState }) {
+ return next => action => {
+ if (isGenerator(action)) {
+ return Task.spawn(action.bind(null, dispatch, getState))
+ .then(null, handleError.bind(null, dispatch));
+ }
+
+ /*
+ if (isPromise(action)) {
+ return action.then(dispatch, handleError.bind(null, dispatch));
+ }
+ */
+
+ return next(action);
+ };
+}
+
+function handleError(dispatch, error) {
+ executeSoon(() => {
+ reportException(ERROR_TYPE, error);
+ dispatch({ type: ERROR_TYPE, error });
+ });
+}
+
+exports.task = task;
diff --git a/devtools/client/shared/redux/middleware/test/.eslintrc.js b/devtools/client/shared/redux/middleware/test/.eslintrc.js
new file mode 100644
index 000000000..0d12cd9a3
--- /dev/null
+++ b/devtools/client/shared/redux/middleware/test/.eslintrc.js
@@ -0,0 +1,17 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../../../../.eslintrc.mochitests.js",
+ "globals": {
+ "run_test": true,
+ "run_next_test": true,
+ "equal": true,
+ "do_print": true,
+ "waitUntilState": true
+ },
+ "rules": {
+ // Stop giving errors for run_test
+ "camelcase": "off"
+ }
+};
diff --git a/devtools/client/shared/redux/middleware/test/head.js b/devtools/client/shared/redux/middleware/test/head.js
new file mode 100644
index 000000000..1e5cbff7a
--- /dev/null
+++ b/devtools/client/shared/redux/middleware/test/head.js
@@ -0,0 +1,27 @@
+/* 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/. */
+
+/* exported waitUntilState */
+
+"use strict";
+
+const { require } = Components.utils.import("resource://devtools/shared/Loader.jsm", {});
+const flags = require("devtools/shared/flags");
+
+flags.testing = true;
+
+function waitUntilState(store, predicate) {
+ return new Promise(resolve => {
+ let unsubscribe = store.subscribe(check);
+ function check() {
+ if (predicate(store.getState())) {
+ unsubscribe();
+ resolve();
+ }
+ }
+
+ // Fire the check immediately incase the action has already occurred
+ check();
+ });
+}
diff --git a/devtools/client/shared/redux/middleware/test/test_middleware-task-01.js b/devtools/client/shared/redux/middleware/test/test_middleware-task-01.js
new file mode 100644
index 000000000..be94560cb
--- /dev/null
+++ b/devtools/client/shared/redux/middleware/test/test_middleware-task-01.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/. */
+
+"use strict";
+
+const { createStore, applyMiddleware } = require("devtools/client/shared/vendor/redux");
+const { task } = require("devtools/client/shared/redux/middleware/task");
+
+/**
+ * Tests that task middleware allows dispatching generators, promises and objects
+ * that return actions;
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ let store = applyMiddleware(task)(createStore)(reducer);
+
+ store.dispatch(fetch1("generator"));
+ yield waitUntilState(store, () => store.getState().length === 1);
+ equal(store.getState()[0].data, "generator",
+ "task middleware async dispatches an action via generator");
+
+ store.dispatch(fetch2("sync"));
+ yield waitUntilState(store, () => store.getState().length === 2);
+ equal(store.getState()[1].data, "sync",
+ "task middleware sync dispatches an action via sync");
+});
+
+function fetch1(data) {
+ return function* (dispatch, getState) {
+ equal(getState().length, 0, "`getState` is accessible in a generator action");
+ let moreData = yield new Promise(resolve => resolve(data));
+ // Ensure it handles more than one yield
+ moreData = yield new Promise(resolve => resolve(data));
+ dispatch({ type: "fetch1", data: moreData });
+ };
+}
+
+function fetch2(data) {
+ return {
+ type: "fetch2",
+ data
+ };
+}
+
+function reducer(state = [], action) {
+ do_print("Action called: " + action.type);
+ if (["fetch1", "fetch2"].includes(action.type)) {
+ state.push(action);
+ }
+ return [...state];
+}
diff --git a/devtools/client/shared/redux/middleware/test/test_middleware-task-02.js b/devtools/client/shared/redux/middleware/test/test_middleware-task-02.js
new file mode 100644
index 000000000..7e2a88d2c
--- /dev/null
+++ b/devtools/client/shared/redux/middleware/test/test_middleware-task-02.js
@@ -0,0 +1,67 @@
+/* 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";
+
+/**
+ * Tests that task middleware allows dispatching generators that dispatch
+ * additional sync and async actions.
+ */
+
+const { createStore, applyMiddleware } = require("devtools/client/shared/vendor/redux");
+const { task } = require("devtools/client/shared/redux/middleware/task");
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ let store = applyMiddleware(task)(createStore)(reducer);
+
+ store.dispatch(comboAction());
+ yield waitUntilState(store, () => store.getState().length === 3);
+
+ equal(store.getState()[0].type, "fetchAsync-start",
+ "Async dispatched actions in a generator task are fired");
+ equal(store.getState()[1].type, "fetchAsync-end",
+ "Async dispatched actions in a generator task are fired");
+ equal(store.getState()[2].type, "fetchSync",
+ "Return values of yielded sync dispatched actions are correct");
+ equal(store.getState()[3].type, "fetch-done",
+ "Return values of yielded async dispatched actions are correct");
+ equal(store.getState()[3].data.sync.data, "sync",
+ "Return values of dispatched sync values are correct");
+ equal(store.getState()[3].data.async, "async",
+ "Return values of dispatched async values are correct");
+});
+
+function comboAction() {
+ return function* (dispatch, getState) {
+ let data = {};
+ data.async = yield dispatch(fetchAsync("async"));
+ data.sync = yield dispatch(fetchSync("sync"));
+ dispatch({ type: "fetch-done", data });
+ };
+}
+
+function fetchSync(data) {
+ return { type: "fetchSync", data };
+}
+
+function fetchAsync(data) {
+ return function* (dispatch) {
+ dispatch({ type: "fetchAsync-start" });
+ let val = yield new Promise(resolve => resolve(data));
+ dispatch({ type: "fetchAsync-end" });
+ return val;
+ };
+}
+
+function reducer(state = [], action) {
+ do_print("Action called: " + action.type);
+ if (/fetch/.test(action.type)) {
+ state.push(action);
+ }
+ return [...state];
+}
diff --git a/devtools/client/shared/redux/middleware/test/test_middleware-task-03.js b/devtools/client/shared/redux/middleware/test/test_middleware-task-03.js
new file mode 100644
index 000000000..7dc0e5c9d
--- /dev/null
+++ b/devtools/client/shared/redux/middleware/test/test_middleware-task-03.js
@@ -0,0 +1,42 @@
+/* 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 { createStore, applyMiddleware } = require("devtools/client/shared/vendor/redux");
+const { task, ERROR_TYPE } = require("devtools/client/shared/redux/middleware/task");
+
+/**
+ * Tests that the middleware handles errors thrown in tasks, and rejected promises.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ let store = applyMiddleware(task)(createStore)(reducer);
+
+ store.dispatch(generatorError());
+ yield waitUntilState(store, () => store.getState().length === 1);
+ equal(store.getState()[0].type, ERROR_TYPE,
+ "generator errors dispatch ERROR_TYPE actions");
+ equal(store.getState()[0].error, "task-middleware-error-generator",
+ "generator errors dispatch ERROR_TYPE actions with error");
+});
+
+function generatorError() {
+ return function* (dispatch, getState) {
+ let error = "task-middleware-error-generator";
+ throw error;
+ };
+}
+
+function reducer(state = [], action) {
+ do_print("Action called: " + action.type);
+ if (action.type === ERROR_TYPE) {
+ state.push(action);
+ }
+ return [...state];
+}
diff --git a/devtools/client/shared/redux/middleware/test/xpcshell.ini b/devtools/client/shared/redux/middleware/test/xpcshell.ini
new file mode 100644
index 000000000..3836ed1fd
--- /dev/null
+++ b/devtools/client/shared/redux/middleware/test/xpcshell.ini
@@ -0,0 +1,10 @@
+[DEFAULT]
+tags = devtools
+head = head.js
+tail =
+firefox-appdir = browser
+skip-if = toolkit == 'android'
+
+[test_middleware-task-01.js]
+[test_middleware-task-02.js]
+[test_middleware-task-03.js]
diff --git a/devtools/client/shared/redux/middleware/thunk.js b/devtools/client/shared/redux/middleware/thunk.js
new file mode 100644
index 000000000..8f564a033
--- /dev/null
+++ b/devtools/client/shared/redux/middleware/thunk.js
@@ -0,0 +1,19 @@
+/* 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";
+
+/**
+ * A middleware that allows thunks (functions) to be dispatched.
+ * If it's a thunk, it is called with `dispatch` and `getState`,
+ * allowing the action to create multiple actions (most likely
+ * asynchronously).
+ */
+function thunk({ dispatch, getState }) {
+ return next => action => {
+ return (typeof action === "function")
+ ? action(dispatch, getState)
+ : next(action);
+ };
+}
+exports.thunk = thunk;
diff --git a/devtools/client/shared/redux/middleware/wait-service.js b/devtools/client/shared/redux/middleware/wait-service.js
new file mode 100644
index 000000000..93878a312
--- /dev/null
+++ b/devtools/client/shared/redux/middleware/wait-service.js
@@ -0,0 +1,64 @@
+/* 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";
+
+/**
+ * A middleware which acts like a service, because it is stateful
+ * and "long-running" in the background. It provides the ability
+ * for actions to install a function to be run once when a specific
+ * condition is met by an action coming through the system. Think of
+ * it as a thunk that blocks until the condition is met. Example:
+ *
+ * ```js
+ * const services = { WAIT_UNTIL: require('wait-service').NAME };
+ *
+ * { type: services.WAIT_UNTIL,
+ * predicate: action => action.type === constants.ADD_ITEM,
+ * run: (dispatch, getState, action) => {
+ * // Do anything here. You only need to accept the arguments
+ * // if you need them. `action` is the action that satisfied
+ * // the predicate.
+ * }
+ * }
+ * ```
+ */
+const NAME = exports.NAME = "@@service/waitUntil";
+
+function waitUntilService({ dispatch, getState }) {
+ let pending = [];
+
+ function checkPending(action) {
+ let readyRequests = [];
+ let stillPending = [];
+
+ // Find the pending requests whose predicates are satisfied with
+ // this action. Wait to run the requests until after we update the
+ // pending queue because the request handler may synchronously
+ // dispatch again and run this service (that use case is
+ // completely valid).
+ for (let request of pending) {
+ if (request.predicate(action)) {
+ readyRequests.push(request);
+ } else {
+ stillPending.push(request);
+ }
+ }
+
+ pending = stillPending;
+ for (let request of readyRequests) {
+ request.run(dispatch, getState, action);
+ }
+ }
+
+ return next => action => {
+ if (action.type === NAME) {
+ pending.push(action);
+ return null;
+ }
+ let result = next(action);
+ checkPending(action);
+ return result;
+ };
+}
+exports.waitUntilService = waitUntilService;
diff --git a/devtools/client/shared/redux/moz.build b/devtools/client/shared/redux/moz.build
new file mode 100644
index 000000000..02b1f6bd6
--- /dev/null
+++ b/devtools/client/shared/redux/moz.build
@@ -0,0 +1,14 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DIRS += [
+ 'middleware',
+]
+
+DevToolsModules(
+ 'create-store.js',
+ 'non-react-subscriber.js',
+)
diff --git a/devtools/client/shared/redux/non-react-subscriber.js b/devtools/client/shared/redux/non-react-subscriber.js
new file mode 100644
index 000000000..0bb3f0b8f
--- /dev/null
+++ b/devtools/client/shared/redux/non-react-subscriber.js
@@ -0,0 +1,153 @@
+/* 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";
+
+/**
+ * This file defines functions to add the ability for redux reducers
+ * to broadcast specific state changes to a non-React UI. You should
+ * *never* use this for new code that uses React, as it violates the
+ * core principals of a functional UI. This should only be used when
+ * migrating old code to redux, because it allows you to use redux
+ * with event-listening UI elements. The typical way to set all of
+ * this up is this:
+ *
+ * const emitter = makeEmitter();
+ * let store = createStore(combineEmittingReducers(
+ * reducers,
+ * emitter.emit
+ * ));
+ * store = enhanceStoreWithEmitter(store, emitter);
+ *
+ * Now reducers will receive a 3rd argument, `emit`, for emitting
+ * events, and the store has an `on` function for listening to them.
+ * For example, a reducer can now do this:
+ *
+ * function update(state = initialState, action, emitChange) {
+ * if (action.type === constants.ADD_BREAKPOINT) {
+ * const id = action.breakpoint.id;
+ * emitChange('add-breakpoint', action.breakpoint);
+ * return state.merge({ [id]: action.breakpoint });
+ * }
+ * return state;
+ * }
+ *
+ * `emitChange` is *not* synchronous, the state changes will be
+ * broadcasted *after* all reducers are run and the state has been
+ * updated.
+ *
+ * Now, a non-React widget can do this:
+ *
+ * store.on('add-breakpoint', breakpoint => { ... });
+ */
+
+const { combineReducers } = require("devtools/client/shared/vendor/redux");
+
+/**
+ * Make an emitter that is meant to be used in redux reducers. This
+ * does not run listeners immediately when an event is emitted; it
+ * waits until all reducers have run and the store has updated the
+ * state, and then fires any enqueued events. Events *are* fired
+ * synchronously, but just later in the process.
+ *
+ * This is important because you never want the UI to be updating in
+ * the middle of a reducing process. Reducers will fire these events
+ * in the middle of generating new state, but the new state is *not*
+ * available from the store yet. So if the UI executes in the middle
+ * of the reducing process and calls `getState()` to get something
+ * from the state, it will get stale state.
+ *
+ * We want the reducing and the UI updating phases to execute
+ * atomically and independent from each other.
+ *
+ * @param {Function} stillAliveFunc
+ * A function that indicates the app is still active. If this
+ * returns false, changes will stop being broadcasted.
+ */
+function makeStateBroadcaster(stillAliveFunc) {
+ const listeners = {};
+ let enqueuedChanges = [];
+
+ return {
+ onChange: (name, cb) => {
+ if (!listeners[name]) {
+ listeners[name] = [];
+ }
+ listeners[name].push(cb);
+ },
+
+ offChange: (name, cb) => {
+ listeners[name] = listeners[name].filter(listener => listener !== cb);
+ },
+
+ emitChange: (name, payload) => {
+ enqueuedChanges.push([name, payload]);
+ },
+
+ subscribeToStore: store => {
+ store.subscribe(() => {
+ if (stillAliveFunc()) {
+ enqueuedChanges.forEach(([name, payload]) => {
+ if (listeners[name]) {
+ listeners[name].forEach(listener => {
+ listener(payload);
+ });
+ }
+ });
+ enqueuedChanges = [];
+ }
+ });
+ }
+ };
+}
+
+/**
+ * Make a store fire any enqueued events whenever the state changes,
+ * and add an `on` function to allow users to listen for specific
+ * events.
+ *
+ * @param {Object} store
+ * @param {Object} broadcaster
+ * @return {Object}
+ */
+function enhanceStoreWithBroadcaster(store, broadcaster) {
+ broadcaster.subscribeToStore(store);
+ store.onChange = broadcaster.onChange;
+ store.offChange = broadcaster.offChange;
+ return store;
+}
+
+/**
+ * Function that takes a hash of reducers, like `combineReducers`, and
+ * an `emitChange` function and returns a function to be used as a
+ * reducer for a Redux store. This allows all reducers defined here to
+ * receive a third argument, the `emitChange` function, for
+ * event-based subscriptions from within reducers.
+ *
+ * @param {Object} reducers
+ * @param {Function} emitChange
+ * @return {Function}
+ */
+function combineBroadcastingReducers(reducers, emitChange) {
+ // Wrap each reducer with a wrapper function that calls
+ // the reducer with a third argument, an `emitChange` function.
+ // Use this rather than a new custom top level reducer that would ultimately
+ // have to replicate redux's `combineReducers` so we only pass in correct
+ // state, the error checking, and other edge cases.
+ function wrapReduce(newReducers, key) {
+ newReducers[key] = (state, action) => {
+ return reducers[key](state, action, emitChange);
+ };
+ return newReducers;
+ }
+
+ return combineReducers(
+ Object.keys(reducers).reduce(wrapReduce, Object.create(null))
+ );
+}
+
+module.exports = {
+ makeStateBroadcaster,
+ enhanceStoreWithBroadcaster,
+ combineBroadcastingReducers
+};
diff --git a/devtools/client/shared/scroll.js b/devtools/client/shared/scroll.js
new file mode 100644
index 000000000..ee591e014
--- /dev/null
+++ b/devtools/client/shared/scroll.js
@@ -0,0 +1,52 @@
+/* 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";
+
+/**
+ * Scroll the document so that the element "elem" appears in the viewport.
+ *
+ * @param {DOMNode} elem
+ * The element that needs to appear in the viewport.
+ * @param {Boolean} centered
+ * true if you want it centered, false if you want it to appear on the
+ * top of the viewport. It is true by default, and that is usually what
+ * you want.
+ */
+function scrollIntoViewIfNeeded(elem, centered = true) {
+ let win = elem.ownerDocument.defaultView;
+ let clientRect = elem.getBoundingClientRect();
+
+ // The following are always from the {top, bottom}
+ // of the viewport, to the {top, …} of the box.
+ // Think of them as geometrical vectors, it helps.
+ // The origin is at the top left.
+
+ let topToBottom = clientRect.bottom;
+ let bottomToTop = clientRect.top - win.innerHeight;
+ // We allow one translation on the y axis.
+ let yAllowed = true;
+
+ // Whatever `centered` is, the behavior is the same if the box is
+ // (even partially) visible.
+ if ((topToBottom > 0 || !centered) && topToBottom <= elem.offsetHeight) {
+ win.scrollBy(0, topToBottom - elem.offsetHeight);
+ yAllowed = false;
+ } else if ((bottomToTop < 0 || !centered) &&
+ bottomToTop >= -elem.offsetHeight) {
+ win.scrollBy(0, bottomToTop + elem.offsetHeight);
+ yAllowed = false;
+ }
+
+ // If we want it centered, and the box is completely hidden,
+ // then we center it explicitly.
+ if (centered) {
+ if (yAllowed && (topToBottom <= 0 || bottomToTop >= 0)) {
+ win.scroll(win.scrollX,
+ win.scrollY + clientRect.top
+ - (win.innerHeight - elem.offsetHeight) / 2);
+ }
+ }
+}
+exports.scrollIntoViewIfNeeded = scrollIntoViewIfNeeded;
diff --git a/devtools/client/shared/shim/Services.js b/devtools/client/shared/shim/Services.js
new file mode 100644
index 000000000..97ea51433
--- /dev/null
+++ b/devtools/client/shared/shim/Services.js
@@ -0,0 +1,620 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set 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";
+
+/* globals localStorage, window, document, NodeFilter */
+
+// Some constants from nsIPrefBranch.idl.
+const PREF_INVALID = 0;
+const PREF_STRING = 32;
+const PREF_INT = 64;
+const PREF_BOOL = 128;
+const NS_PREFBRANCH_PREFCHANGE_TOPIC_ID = "nsPref:changed";
+
+// We prefix all our local storage items with this.
+const PREFIX = "Services.prefs:";
+
+/**
+ * Create a new preference branch. This object conforms largely to
+ * nsIPrefBranch and nsIPrefService, though it only implements the
+ * subset needed by devtools. A preference branch can hold child
+ * preferences while also holding a preference value itself.
+ *
+ * @param {PrefBranch} parent the parent branch, or null for the root
+ * branch.
+ * @param {String} name the base name of this branch
+ * @param {String} fullName the fully-qualified name of this branch
+ */
+function PrefBranch(parent, name, fullName) {
+ this._parent = parent;
+ this._name = name;
+ this._fullName = fullName;
+ this._observers = {};
+ this._children = {};
+
+ // Properties used when this branch has a value as well.
+ this._defaultValue = null;
+ this._hasUserValue = false;
+ this._userValue = null;
+ this._type = PREF_INVALID;
+}
+
+PrefBranch.prototype = {
+ PREF_INVALID: PREF_INVALID,
+ PREF_STRING: PREF_STRING,
+ PREF_INT: PREF_INT,
+ PREF_BOOL: PREF_BOOL,
+
+ /** @see nsIPrefBranch.root. */
+ get root() {
+ return this._fullName;
+ },
+
+ /** @see nsIPrefBranch.getPrefType. */
+ getPrefType: function (prefName) {
+ return this._findPref(prefName)._type;
+ },
+
+ /** @see nsIPrefBranch.getBoolPref. */
+ getBoolPref: function (prefName) {
+ let thePref = this._findPref(prefName);
+ if (thePref._type !== PREF_BOOL) {
+ throw new Error(`${prefName} does not have bool type`);
+ }
+ return thePref._get();
+ },
+
+ /** @see nsIPrefBranch.setBoolPref. */
+ setBoolPref: function (prefName, value) {
+ if (typeof value !== "boolean") {
+ throw new Error("non-bool passed to setBoolPref");
+ }
+ let thePref = this._findOrCreatePref(prefName, value, true, value);
+ if (thePref._type !== PREF_BOOL) {
+ throw new Error(`${prefName} does not have bool type`);
+ }
+ thePref._set(value);
+ },
+
+ /** @see nsIPrefBranch.getCharPref. */
+ getCharPref: function (prefName) {
+ let thePref = this._findPref(prefName);
+ if (thePref._type !== PREF_STRING) {
+ throw new Error(`${prefName} does not have string type`);
+ }
+ return thePref._get();
+ },
+
+ /** @see nsIPrefBranch.setCharPref. */
+ setCharPref: function (prefName, value) {
+ if (typeof value !== "string") {
+ throw new Error("non-string passed to setCharPref");
+ }
+ let thePref = this._findOrCreatePref(prefName, value, true, value);
+ if (thePref._type !== PREF_STRING) {
+ throw new Error(`${prefName} does not have string type`);
+ }
+ thePref._set(value);
+ },
+
+ /** @see nsIPrefBranch.getIntPref. */
+ getIntPref: function (prefName) {
+ let thePref = this._findPref(prefName);
+ if (thePref._type !== PREF_INT) {
+ throw new Error(`${prefName} does not have int type`);
+ }
+ return thePref._get();
+ },
+
+ /** @see nsIPrefBranch.setIntPref. */
+ setIntPref: function (prefName, value) {
+ if (typeof value !== "number") {
+ throw new Error("non-number passed to setIntPref");
+ }
+ let thePref = this._findOrCreatePref(prefName, value, true, value);
+ if (thePref._type !== PREF_INT) {
+ throw new Error(`${prefName} does not have int type`);
+ }
+ thePref._set(value);
+ },
+
+ /** @see nsIPrefBranch.clearUserPref */
+ clearUserPref: function (prefName) {
+ let thePref = this._findPref(prefName);
+ thePref._clearUserValue();
+ },
+
+ /** @see nsIPrefBranch.prefHasUserValue */
+ prefHasUserValue: function (prefName) {
+ let thePref = this._findPref(prefName);
+ return thePref._hasUserValue;
+ },
+
+ /** @see nsIPrefBranch.addObserver */
+ addObserver: function (domain, observer, holdWeak) {
+ if (holdWeak) {
+ throw new Error("shim prefs only supports strong observers");
+ }
+
+ if (!(domain in this._observers)) {
+ this._observers[domain] = [];
+ }
+ this._observers[domain].push(observer);
+ },
+
+ /** @see nsIPrefBranch.removeObserver */
+ removeObserver: function (domain, observer) {
+ if (!(domain in this._observers)) {
+ return;
+ }
+ let index = this._observers[domain].indexOf(observer);
+ if (index >= 0) {
+ this._observers[domain].splice(index, 1);
+ }
+ },
+
+ /** @see nsIPrefService.savePrefFile */
+ savePrefFile: function (file) {
+ if (file) {
+ throw new Error("shim prefs only supports null file in savePrefFile");
+ }
+ // Nothing to do - this implementation always writes back.
+ },
+
+ /** @see nsIPrefService.getBranch */
+ getBranch: function (prefRoot) {
+ if (!prefRoot) {
+ return this;
+ }
+ if (prefRoot.endsWith(".")) {
+ prefRoot = prefRoot.slice(0, -1);
+ }
+ // This is a bit weird since it could erroneously return a pref,
+ // not a pref branch.
+ return this._findPref(prefRoot);
+ },
+
+ /**
+ * Return this preference's current value.
+ *
+ * @return {Any} The current value of this preference. This may
+ * return a string, a number, or a boolean depending on the
+ * preference's type.
+ */
+ _get: function () {
+ if (this._hasUserValue) {
+ return this._userValue;
+ }
+ return this._defaultValue;
+ },
+
+ /**
+ * Set the preference's value. The new value is assumed to be a
+ * user value. After setting the value, this function emits a
+ * change notification.
+ *
+ * @param {Any} value the new value
+ */
+ _set: function (value) {
+ if (!this._hasUserValue || value !== this._userValue) {
+ this._userValue = value;
+ this._hasUserValue = true;
+ this._saveAndNotify();
+ }
+ },
+
+ /**
+ * Set the default value for this preference, and emit a
+ * notification if this results in a visible change.
+ *
+ * @param {Any} value the new default value
+ */
+ _setDefault: function (value) {
+ if (this._defaultValue !== value) {
+ this._defaultValue = value;
+ if (!this._hasUserValue) {
+ this._saveAndNotify();
+ }
+ }
+ },
+
+ /**
+ * If this preference has a user value, clear it. If a change was
+ * made, emit a change notification.
+ */
+ _clearUserValue: function () {
+ if (this._hasUserValue) {
+ this._userValue = null;
+ this._hasUserValue = false;
+ this._saveAndNotify();
+ }
+ },
+
+ /**
+ * Helper function to write the preference's value to local storage
+ * and then emit a change notification.
+ */
+ _saveAndNotify: function () {
+ let store = {
+ type: this._type,
+ defaultValue: this._defaultValue,
+ hasUserValue: this._hasUserValue,
+ userValue: this._userValue,
+ };
+
+ localStorage.setItem(PREFIX + this._fullName, JSON.stringify(store));
+ this._parent._notify(this._name);
+ },
+
+ /**
+ * Change this preference's value without writing it back to local
+ * storage. This is used to handle changes to local storage that
+ * were made externally.
+ *
+ * @param {Number} type one of the PREF_* values
+ * @param {Any} userValue the user value to use if the pref does not exist
+ * @param {Any} defaultValue the default value to use if the pref
+ * does not exist
+ * @param {Boolean} hasUserValue if a new pref is created, whether
+ * the default value is also a user value
+ * @param {Object} store the new value of the preference. It should
+ * be of the form {type, defaultValue, hasUserValue, userValue};
+ * where |type| is one of the PREF_* type constants; |defaultValue|
+ * and |userValue| are the default and user values, respectively;
+ * and |hasUserValue| is a boolean indicating whether the user value
+ * is valid
+ */
+ _storageUpdated: function (type, userValue, hasUserValue, defaultValue) {
+ this._type = type;
+ this._defaultValue = defaultValue;
+ this._hasUserValue = hasUserValue;
+ this._userValue = userValue;
+ // There's no need to write this back to local storage, since it
+ // came from there; and this avoids infinite event loops.
+ this._parent._notify(this._name);
+ },
+
+ /**
+ * Helper function to find either a Preference or PrefBranch object
+ * given its name. If the name is not found, throws an exception.
+ *
+ * @param {String} prefName the fully-qualified preference name
+ * @return {Object} Either a Preference or PrefBranch object
+ */
+ _findPref: function (prefName) {
+ let branchNames = prefName.split(".");
+ let branch = this;
+
+ for (let branchName of branchNames) {
+ branch = branch._children[branchName];
+ if (!branch) {
+ throw new Error("could not find pref branch " + prefName);
+ }
+ }
+
+ return branch;
+ },
+
+ /**
+ * Helper function to notify any observers when a preference has
+ * changed. This will also notify the parent branch for further
+ * reporting.
+ *
+ * @param {String} relativeName the name of the updated pref,
+ * relative to this branch
+ */
+ _notify: function (relativeName) {
+ for (let domain in this._observers) {
+ if (relativeName === domain || domain === "" ||
+ (domain.endsWith(".") && relativeName.startsWith(domain))) {
+ // Allow mutation while walking.
+ let localList = this._observers[domain].slice();
+ for (let observer of localList) {
+ try {
+ observer.observe(this, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID,
+ relativeName);
+ } catch (e) {
+ console.error(e);
+ }
+ }
+ }
+ }
+
+ if (this._parent) {
+ this._parent._notify(this._name + "." + relativeName);
+ }
+ },
+
+ /**
+ * Helper function to create a branch given an array of branch names
+ * representing the path of the new branch.
+ *
+ * @param {Array} branchList an array of strings, one per component
+ * of the branch to be created
+ * @return {PrefBranch} the new branch
+ */
+ _createBranch: function (branchList) {
+ let parent = this;
+ for (let branch of branchList) {
+ if (!parent._children[branch]) {
+ let isParentRoot = !parent.parent;
+ let branchName = (isParentRoot ? "" : parent.root + ".") + branch;
+ parent._children[branch] = new PrefBranch(parent, branch, branchName);
+ }
+ parent = parent._children[branch];
+ }
+ return parent;
+ },
+
+ /**
+ * Create a new preference. The new preference is assumed to be in
+ * local storage already, and the new value is taken from there.
+ *
+ * @param {String} keyName the full-qualified name of the preference.
+ * This is also the name of the key in local storage.
+ * @param {Any} userValue the user value to use if the pref does not exist
+ * @param {Boolean} hasUserValue if a new pref is created, whether
+ * the default value is also a user value
+ * @param {Any} defaultValue the default value to use if the pref
+ * does not exist
+ * @param {Boolean} init if true, then this call is initialization
+ * from local storage and should override the default prefs
+ */
+ _findOrCreatePref: function (keyName, userValue, hasUserValue, defaultValue,
+ init = false) {
+ let branch = this._createBranch(keyName.split("."));
+
+ if (hasUserValue && typeof (userValue) !== typeof (defaultValue)) {
+ throw new Error("inconsistent values when creating " + keyName);
+ }
+
+ let type;
+ switch (typeof (defaultValue)) {
+ case "boolean":
+ type = PREF_BOOL;
+ break;
+ case "number":
+ type = PREF_INT;
+ break;
+ case "string":
+ type = PREF_STRING;
+ break;
+ default:
+ throw new Error("unhandled argument type: " + typeof (defaultValue));
+ }
+
+ if (init || branch._type === PREF_INVALID) {
+ branch._storageUpdated(type, userValue, hasUserValue, defaultValue);
+ } else if (branch._type !== type) {
+ throw new Error("attempt to change type of pref " + keyName);
+ }
+
+ return branch;
+ },
+
+ /**
+ * Helper function that is called when local storage changes. This
+ * updates the preferences and notifies pref observers as needed.
+ *
+ * @param {StorageEvent} event the event representing the local
+ * storage change
+ */
+ _onStorageChange: function (event) {
+ if (event.storageArea !== localStorage) {
+ return;
+ }
+ // Ignore delete events. Not clear what's correct.
+ if (event.key === null || event.newValue === null) {
+ return;
+ }
+
+ let {type, userValue, hasUserValue, defaultValue} =
+ JSON.parse(event.newValue);
+ if (event.oldValue === null) {
+ this._findOrCreatePref(event.key, userValue, hasUserValue, defaultValue);
+ } else {
+ let thePref = this._findPref(event.key);
+ thePref._storageUpdated(type, userValue, hasUserValue, defaultValue);
+ }
+ },
+
+ /**
+ * Helper function to initialize the root PrefBranch.
+ */
+ _initializeRoot: function () {
+ if (Services._defaultPrefsEnabled) {
+ /* eslint-disable no-eval */
+ let devtools = require("raw!prefs!devtools/client/preferences/devtools");
+ eval(devtools);
+ let all = require("raw!prefs!modules/libpref/init/all");
+ eval(all);
+ /* eslint-enable no-eval */
+ }
+
+ // Read the prefs from local storage and create the local
+ // representations.
+ for (let i = 0; i < localStorage.length; ++i) {
+ let keyName = localStorage.key(i);
+ if (keyName.startsWith(PREFIX)) {
+ let {userValue, hasUserValue, defaultValue} =
+ JSON.parse(localStorage.getItem(keyName));
+ this._findOrCreatePref(keyName.slice(PREFIX.length), userValue,
+ hasUserValue, defaultValue, true);
+ }
+ }
+
+ this._onStorageChange = this._onStorageChange.bind(this);
+ window.addEventListener("storage", this._onStorageChange);
+ },
+};
+
+const Services = {
+ _prefs: null,
+
+ // For use by tests. If set to false before Services.prefs is used,
+ // this will disable the reading of the default prefs.
+ _defaultPrefsEnabled: true,
+
+ /**
+ * An implementation of nsIPrefService that is based on local
+ * storage. Only the subset of nsIPrefService that is actually used
+ * by devtools is implemented here. This is lazily instantiated so
+ * that the tests have a chance to disable the loading of default
+ * prefs.
+ */
+ get prefs() {
+ if (!this._prefs) {
+ this._prefs = new PrefBranch(null, "", "");
+ this._prefs._initializeRoot();
+ }
+ return this._prefs;
+ },
+
+ /**
+ * An implementation of Services.appinfo that holds just the
+ * properties needed by devtools.
+ */
+ appinfo: {
+ get OS() {
+ const os = window.navigator.userAgent;
+ if (os) {
+ if (os.includes("Linux")) {
+ return "Linux";
+ } else if (os.includes("Windows")) {
+ return "WINNT";
+ } else if (os.includes("Mac")) {
+ return "Darwin";
+ }
+ }
+ return "Unknown";
+ },
+
+ // It's fine for this to be an approximation.
+ get name() {
+ return window.navigator.userAgent;
+ },
+
+ // It's fine for this to be an approximation.
+ get version() {
+ return window.navigator.appVersion;
+ },
+
+ // This is only used by telemetry, which is disabled for the
+ // content case. So, being totally wrong is ok.
+ get is64Bit() {
+ return true;
+ },
+ },
+
+ /**
+ * A no-op implementation of Services.telemetry. This supports just
+ * the subset of Services.telemetry that is used by devtools.
+ */
+ telemetry: {
+ getHistogramById: function (name) {
+ return {
+ add: () => {}
+ };
+ },
+
+ getKeyedHistogramById: function (name) {
+ return {
+ add: () => {}
+ };
+ },
+ },
+
+ /**
+ * An implementation of Services.focus that holds just the
+ * properties and methods needed by devtools.
+ * @see nsIFocusManager.idl for details.
+ */
+ focus: {
+ // These values match nsIFocusManager in order to make testing a
+ // bit simpler.
+ MOVEFOCUS_FORWARD: 1,
+ MOVEFOCUS_BACKWARD: 2,
+
+ get focusedElement() {
+ if (!document.hasFocus()) {
+ return null;
+ }
+ return document.activeElement;
+ },
+
+ moveFocus: function (window, startElement, type, flags) {
+ if (flags !== 0) {
+ throw new Error("shim Services.focus.moveFocus only accepts flags===0");
+ }
+ if (type !== Services.focus.MOVEFOCUS_FORWARD
+ && type !== Services.focus.MOVEFOCUS_BACKWARD) {
+ throw new Error("shim Services.focus.moveFocus only supports " +
+ " MOVEFOCUS_FORWARD and MOVEFOCUS_BACKWARD");
+ }
+
+ if (!startElement) {
+ startElement = document.activeElement || document;
+ }
+
+ let iter = document.createTreeWalker(document, NodeFilter.SHOW_ELEMENT, {
+ acceptNode: function (node) {
+ let tabIndex = node.getAttribute("tabindex");
+ if (tabIndex === "-1") {
+ return NodeFilter.FILTER_SKIP;
+ }
+ node.focus();
+ if (document.activeElement == node) {
+ return NodeFilter.FILTER_ACCEPT;
+ }
+ return NodeFilter.FILTER_SKIP;
+ }
+ });
+
+ iter.currentNode = startElement;
+
+ // Sets the focus via side effect in the filter.
+ if (type === Services.focus.MOVEFOCUS_FORWARD) {
+ iter.nextNode();
+ } else {
+ iter.previousNode();
+ }
+ },
+ },
+
+ /**
+ * An implementation of Services.wm that provides a shim for
+ * getMostRecentWindow.
+ */
+ wm: {
+ getMostRecentWindow: function () {
+ // Having the returned object implement openUILinkIn is
+ // sufficient for our purposes.
+ return {
+ openUILinkIn: function (url) {
+ window.open(url, "_blank");
+ },
+ };
+ },
+ },
+};
+
+/**
+ * Create a new preference. This is used during startup (see
+ * devtools/client/preferences/devtools.js) to install the
+ * default preferences.
+ *
+ * @param {String} name the name of the preference
+ * @param {Any} value the default value of the preference
+ */
+function pref(name, value) {
+ let thePref = Services.prefs._findOrCreatePref(name, value, true, value);
+ thePref._setDefault(value);
+}
+
+module.exports = Services;
+// This is exported to silence eslint.
+exports.pref = pref;
diff --git a/devtools/client/shared/shim/moz.build b/devtools/client/shared/shim/moz.build
new file mode 100644
index 000000000..dff3e903c
--- /dev/null
+++ b/devtools/client/shared/shim/moz.build
@@ -0,0 +1,13 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'Services.js',
+)
+
+MOCHITEST_MANIFESTS += [
+ 'test/mochitest.ini',
+]
diff --git a/devtools/client/shared/shim/test/.eslintrc.js b/devtools/client/shared/shim/test/.eslintrc.js
new file mode 100644
index 000000000..698ae9181
--- /dev/null
+++ b/devtools/client/shared/shim/test/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../../../.eslintrc.mochitests.js"
+};
diff --git a/devtools/client/shared/shim/test/file_service_wm.html b/devtools/client/shared/shim/test/file_service_wm.html
new file mode 100644
index 000000000..24753e710
--- /dev/null
+++ b/devtools/client/shared/shim/test/file_service_wm.html
@@ -0,0 +1,20 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<script>
+// Silence eslint complaint about the onload below.
+/* eslint-disable no-unused-vars */
+
+"use strict";
+
+function load() {
+ (window.opener || window.parent).hurray();
+ window.close();
+}
+</script>
+</head>
+
+<body onload='load()'>
+</body>
+
+</html>
diff --git a/devtools/client/shared/shim/test/mochitest.ini b/devtools/client/shared/shim/test/mochitest.ini
new file mode 100644
index 000000000..4ba5cd6c1
--- /dev/null
+++ b/devtools/client/shared/shim/test/mochitest.ini
@@ -0,0 +1,10 @@
+[DEFAULT]
+support-files =
+ file_service_wm.html
+ prefs-wrapper.js
+
+[test_service_appinfo.html]
+[test_service_focus.html]
+[test_service_prefs.html]
+[test_service_prefs_defaults.html]
+[test_service_wm.html]
diff --git a/devtools/client/shared/shim/test/prefs-wrapper.js b/devtools/client/shared/shim/test/prefs-wrapper.js
new file mode 100644
index 000000000..057e12d17
--- /dev/null
+++ b/devtools/client/shared/shim/test/prefs-wrapper.js
@@ -0,0 +1,80 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set 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/. */
+
+// A wrapper for Services.prefs that compares our shim content
+// implementation with the real service.
+
+// We assume we're loaded in a global where Services was already loaded.
+/* globals isDeeply, Services */
+
+"use strict";
+
+function setMethod(methodName, prefName, value) {
+ let savedException;
+ let prefThrew = false;
+ try {
+ Services.prefs[methodName](prefName, value);
+ } catch (e) {
+ prefThrew = true;
+ savedException = e;
+ }
+
+ let realThrew = false;
+ try {
+ SpecialPowers[methodName](prefName, value);
+ } catch (e) {
+ realThrew = true;
+ savedException = e;
+ }
+
+ is(prefThrew, realThrew, methodName + " [throw check]");
+ if (prefThrew || realThrew) {
+ throw savedException;
+ }
+}
+
+function getMethod(methodName, prefName) {
+ let prefThrew = false;
+ let prefValue = undefined;
+ let savedException;
+ try {
+ prefValue = Services.prefs[methodName](prefName);
+ } catch (e) {
+ prefThrew = true;
+ savedException = e;
+ }
+
+ let realValue = undefined;
+ let realThrew = false;
+ try {
+ realValue = SpecialPowers[methodName](prefName);
+ } catch (e) {
+ realThrew = true;
+ savedException = e;
+ }
+
+ is(prefThrew, realThrew, methodName + " [throw check]");
+ isDeeply(prefValue, realValue, methodName + " [equality]");
+ if (prefThrew || realThrew) {
+ throw savedException;
+ }
+
+ return prefValue;
+}
+
+var WrappedPrefs = {};
+
+for (let method of ["getPrefType", "getBoolPref", "getCharPref", "getIntPref",
+ "clearUserPref"]) {
+ WrappedPrefs[method] = getMethod.bind(null, method);
+}
+
+for (let method of ["setBoolPref", "setCharPref", "setIntPref"]) {
+ WrappedPrefs[method] = setMethod.bind(null, method);
+}
+
+// Silence eslint.
+exports.WrappedPrefs = WrappedPrefs;
diff --git a/devtools/client/shared/shim/test/test_service_appinfo.html b/devtools/client/shared/shim/test/test_service_appinfo.html
new file mode 100644
index 000000000..bc659cb42
--- /dev/null
+++ b/devtools/client/shared/shim/test/test_service_appinfo.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1265802
+-->
+<head>
+ <title>Test for Bug 1265802 - replace Services.appinfo</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css">
+
+ <script type="application/javascript;version=1.8">
+ "use strict";
+ var exports = {}
+ var module = {exports};
+ </script>
+
+ <script type="application/javascript;version=1.8"
+ src="resource://devtools/client/shared/shim/Services.js"></script>
+</head>
+<body>
+<script type="application/javascript;version=1.8">
+"use strict";
+
+is(Services.appinfo.OS, SpecialPowers.Services.appinfo.OS,
+ "check that Services.appinfo.OS shim matches platform");
+</script>
+</body>
diff --git a/devtools/client/shared/shim/test/test_service_focus.html b/devtools/client/shared/shim/test/test_service_focus.html
new file mode 100644
index 000000000..d720e0b53
--- /dev/null
+++ b/devtools/client/shared/shim/test/test_service_focus.html
@@ -0,0 +1,78 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1278473
+-->
+<head>
+ <title>Test for Bug 1278473 - replace Services.focus</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css">
+
+ <script type="application/javascript;version=1.8">
+ "use strict";
+ var exports = {}
+ var module = {exports};
+ </script>
+
+ <script type="application/javascript;version=1.8"
+ src="resource://devtools/client/shared/shim/Services.js"></script>
+</head>
+<body>
+ <span>
+ <span id="start" testvalue="0" tabindex="0"> </span>
+ <label>
+ <input testvalue="1" type="radio">Hi</input>
+ </label>
+ <label>
+ <input type="radio" tabindex="-1">Bye</input>
+ </label>
+ <label style="display: none">
+ <input id="button3" type="radio" tabindex="-1">Invisible</input>
+ </label>
+ <input id="button4" type="radio" disabled="true">Disabled</input>
+ <span testvalue="2" tabindex="0"> </span>
+ </span>
+
+<script type="application/javascript;version=1.8">
+ "use strict";
+
+ // The test assumes these are identical, so assert it here.
+ is(Services.focus.MOVEFOCUS_BACKWARD, SpecialPowers.Services.focus.MOVEFOCUS_BACKWARD,
+ "check MOVEFOCUS_BACKWARD");
+ is(Services.focus.MOVEFOCUS_FORWARD, SpecialPowers.Services.focus.MOVEFOCUS_FORWARD,
+ "check MOVEFOCUS_FORWARD");
+
+ function moveFocus(element, type, expect) {
+ let current = document.activeElement;
+ const suffix = "(type=" + type + ", to=" + expect + ")";
+
+ // First try with the platform implementation.
+ SpecialPowers.Services.focus.moveFocus(window, element, type, 0);
+ is(document.activeElement.getAttribute("testvalue"), expect,
+ "platform moveFocus " + suffix);
+
+ // Reset the focus and try again with the shim.
+ current.focus();
+ is(document.activeElement, current, "reset " + suffix);
+
+ Services.focus.moveFocus(window, element, type, 0);
+ is(document.activeElement.getAttribute("testvalue"), expect,
+ "shim moveFocus " + suffix);
+ }
+
+ let start = document.querySelector("#start");
+ start.focus();
+ is(document.activeElement.getAttribute("testvalue"), "0", "initial focus");
+
+ moveFocus(null, Services.focus.MOVEFOCUS_FORWARD, "1");
+ moveFocus(null, Services.focus.MOVEFOCUS_FORWARD, "2");
+ let end = document.activeElement;
+ moveFocus(null, Services.focus.MOVEFOCUS_BACKWARD, "1");
+ moveFocus(null, Services.focus.MOVEFOCUS_BACKWARD, "0");
+
+ moveFocus(start, Services.focus.MOVEFOCUS_FORWARD, "1");
+ moveFocus(end, Services.focus.MOVEFOCUS_BACKWARD, "1");
+</script>
+</body>
diff --git a/devtools/client/shared/shim/test/test_service_prefs.html b/devtools/client/shared/shim/test/test_service_prefs.html
new file mode 100644
index 000000000..99e827dfd
--- /dev/null
+++ b/devtools/client/shared/shim/test/test_service_prefs.html
@@ -0,0 +1,244 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1265808
+-->
+<head>
+ <title>Test for Bug 1265808 - replace Services.prefs</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css">
+
+<script type="application/javascript;version=1.8">
+"use strict";
+var exports = {}
+var module = {exports};
+
+ // Add some starter prefs.
+localStorage.setItem("Services.prefs:devtools.branch1.somebool", JSON.stringify({
+ // bool
+ type: 128,
+ defaultValue: false,
+ hasUserValue: false,
+ userValue: false
+}));
+
+localStorage.setItem("Services.prefs:devtools.branch1.somestring", JSON.stringify({
+ // string
+ type: 32,
+ defaultValue: "dinosaurs",
+ hasUserValue: true,
+ userValue: "elephants"
+}));
+
+localStorage.setItem("Services.prefs:devtools.branch2.someint", JSON.stringify({
+ // string
+ type: 64,
+ defaultValue: -16,
+ hasUserValue: false,
+ userValue: null
+}));
+
+</script>
+
+ <script type="application/javascript;version=1.8"
+ src="prefs-wrapper.js"></script>
+ <script type="application/javascript;version=1.8"
+ src="resource://devtools/client/shared/shim/Services.js"></script>
+</head>
+<body>
+<script type="application/javascript;version=1.8">
+"use strict";
+
+function do_tests() {
+ // We can't load the defaults in this context.
+ Services._defaultPrefsEnabled = false;
+
+ is(Services.prefs.getBoolPref("devtools.branch1.somebool"), false,
+ "bool pref value");
+ Services.prefs.setBoolPref("devtools.branch1.somebool", true);
+ is(Services.prefs.getBoolPref("devtools.branch1.somebool"), true,
+ "bool pref value after setting");
+
+ let threw;
+
+ try {
+ threw = false;
+ WrappedPrefs.getIntPref("devtools.branch1.somebool");
+ } catch (e) {
+ threw = true;
+ }
+ ok(threw, "type-checking for bool pref");
+
+ try {
+ threw = false;
+ Services.prefs.setIntPref("devtools.branch1.somebool", 27);
+ } catch (e) {
+ threw = true;
+ }
+ ok(threw, "type-checking for setting bool pref");
+
+ try {
+ threw = false;
+ Services.prefs.setBoolPref("devtools.branch1.somebool", 27);
+ } catch (e) {
+ threw = true;
+ }
+ ok(threw, "setting bool pref to wrong type");
+
+ try {
+ threw = false;
+ Services.prefs.getCharPref("devtools.branch2.someint");
+ } catch (e) {
+ threw = true;
+ }
+ ok(threw, "type-checking for int pref");
+
+ try {
+ threw = false;
+ Services.prefs.setCharPref("devtools.branch2.someint", "whatever");
+ } catch (e) {
+ threw = true;
+ }
+ ok(threw, "type-checking for setting int pref");
+
+ try {
+ threw = false;
+ Services.prefs.setIntPref("devtools.branch2.someint", "whatever");
+ } catch (e) {
+ threw = true;
+ }
+ ok(threw, "setting int pref to wrong type");
+
+ try {
+ threw = false;
+ Services.prefs.getBoolPref("devtools.branch1.somestring");
+ } catch (e) {
+ threw = true;
+ }
+ ok(threw, "type-checking for char pref");
+
+ try {
+ threw = false;
+ Services.prefs.setBoolPref("devtools.branch1.somestring", true);
+ } catch (e) {
+ threw = true;
+ }
+ ok(threw, "type-checking for setting char pref");
+
+ try {
+ threw = false;
+ Services.prefs.setCharPref("devtools.branch1.somestring", true);
+ } catch (e) {
+ threw = true;
+ }
+ ok(threw, "setting char pref to wrong type");
+
+ is(Services.prefs.getPrefType("devtools.branch1.somebool"),
+ Services.prefs.PREF_BOOL, "type of bool pref");
+ is(Services.prefs.getPrefType("devtools.branch2.someint"),
+ Services.prefs.PREF_INT, "type of int pref");
+ is(Services.prefs.getPrefType("devtools.branch1.somestring"),
+ Services.prefs.PREF_STRING, "type of string pref");
+
+ WrappedPrefs.setBoolPref("devtools.branch1.somebool", true);
+ ok(WrappedPrefs.getBoolPref("devtools.branch1.somebool"), "set bool pref");
+ WrappedPrefs.setIntPref("devtools.branch2.someint", -93);
+ is(WrappedPrefs.getIntPref("devtools.branch2.someint"), -93, "set int pref");
+ WrappedPrefs.setCharPref("devtools.branch1.somestring", "hello");
+ is(WrappedPrefs.getCharPref("devtools.branch1.somestring"), "hello",
+ "set string pref");
+
+ Services.prefs.clearUserPref("devtools.branch1.somestring");
+ is(Services.prefs.getCharPref("devtools.branch1.somestring"), "dinosaurs",
+ "clear string pref");
+
+ ok(Services.prefs.prefHasUserValue("devtools.branch1.somebool"),
+ "bool pref has user value");
+ ok(!Services.prefs.prefHasUserValue("devtools.branch1.somestring"),
+ "string pref does not have user value");
+
+
+ Services.prefs.savePrefFile(null);
+ ok(true, "saved pref file without error");
+
+
+ let branch0 = Services.prefs.getBranch(null);
+ let branch1 = Services.prefs.getBranch("devtools.branch1.");
+
+ branch1.setCharPref("somestring", "octopus");
+ Services.prefs.setCharPref("devtools.branch1.somestring", "octopus");
+ is(Services.prefs.getCharPref("devtools.branch1.somestring"), "octopus",
+ "set correctly via branch");
+ is(branch0.getCharPref("devtools.branch1.somestring"), "octopus",
+ "get via base branch");
+ is(branch1.getCharPref("somestring"), "octopus", "get via branch");
+
+
+ let notifications = {};
+ let clearNotificationList = () => { notifications = {}; }
+ let observer = {
+ observe: function (subject, topic, data) {
+ notifications[data] = true;
+ }
+ };
+
+ branch0.addObserver("devtools.branch1", null, null);
+ branch0.addObserver("devtools.branch1.", observer, false);
+ branch1.addObserver("", observer, false);
+
+ Services.prefs.setCharPref("devtools.branch1.somestring", "elf owl");
+ isDeeply(notifications, {
+ "devtools.branch1.somestring": true,
+ "somestring": true
+ }, "notifications sent to two listeners");
+
+ clearNotificationList();
+ Services.prefs.setIntPref("devtools.branch2.someint", 1729);
+ isDeeply(notifications, {}, "no notifications sent");
+
+ clearNotificationList();
+ branch0.removeObserver("devtools.branch1.", observer);
+ Services.prefs.setCharPref("devtools.branch1.somestring", "tapir");
+ isDeeply(notifications, {
+ "somestring": true
+ }, "removeObserver worked");
+
+ clearNotificationList();
+ branch0.addObserver("devtools.branch1.somestring", observer, false);
+ Services.prefs.setCharPref("devtools.branch1.somestring", "northern shoveler");
+ isDeeply(notifications, {
+ "devtools.branch1.somestring": true,
+ "somestring": true
+ }, "notifications sent to two listeners");
+ branch0.removeObserver("devtools.branch1.somestring", observer);
+
+ // Make sure we update if the pref change comes from somewhere else.
+ clearNotificationList();
+ pref("devtools.branch1.someotherstring", "lazuli bunting");
+ isDeeply(notifications, {
+ "someotherstring": true
+ }, "pref worked");
+
+ // Regression test for bug 1296427.
+ pref("devtools.hud.loglimit", 1000);
+ pref("devtools.hud.loglimit.network", 1000);
+
+ // Clean up.
+ localStorage.clear();
+
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPrefEnv(
+ {"set": [
+ ["devtools.branch1.somestring", "elephants"],
+ ["devtools.branch1.somebool", false],
+ ["devtools.branch2.someint", "-16"],
+ ]},
+ do_tests);
+
+</script>
+</body>
diff --git a/devtools/client/shared/shim/test/test_service_prefs_defaults.html b/devtools/client/shared/shim/test/test_service_prefs_defaults.html
new file mode 100644
index 000000000..d8933b74a
--- /dev/null
+++ b/devtools/client/shared/shim/test/test_service_prefs_defaults.html
@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1309384
+-->
+<head>
+ <title>Test for Bug 1309384 - Services.prefs replacement defaults handling</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css">
+
+<script type="application/javascript;version=1.8">
+"use strict";
+var exports = {}
+var module = {exports};
+
+// Allow one require("raw!prefs...") to return some defaults, with the
+// others being ignored.
+var firstTime = true;
+function require(something) {
+ if (!something.startsWith("raw!prefs!")) {
+ throw new Error("whoops");
+ }
+ if (!firstTime) {
+ return "";
+ }
+ firstTime = false;
+ return "pref('pref1', 'pref1default');\n" +
+ "pref('pref2', 'pref2default');\n" +
+ "pref('pref3', 'pref3default');\n";
+}
+
+// Pretend that one of the prefs was modifed by the user in an earlier session.
+localStorage.setItem("Services.prefs:pref3", JSON.stringify({
+ // string
+ type: 32,
+ defaultValue: "pref3default",
+ hasUserValue: true,
+ userValue: "glass winged butterfly"
+}));
+
+</script>
+
+ <script type="application/javascript;version=1.8"
+ src="resource://devtools/client/shared/shim/Services.js"></script>
+</head>
+<body>
+<script type="application/javascript;version=1.8">
+"use strict";
+
+is(Services.prefs.getCharPref("pref1"), "pref1default", "pref1 value");
+is(Services.prefs.getCharPref("pref2"), "pref2default", "pref2 value");
+is(Services.prefs.getCharPref("pref3"), "glass winged butterfly", "pref3 value");
+
+// Only pref3 should be in local storage at this point.
+is(localStorage.length, 1, "local storage is correct");
+
+Services.prefs.setCharPref("pref2", "pref2override");
+
+// Check that a default pref can be overridden properly
+
+// Workaround to reset the prefs helper and force it to read defaults & overrides again.
+Services._prefs = null;
+is(Services.prefs.getCharPref("pref2"), "pref2override", "pref2 value overridden");
+
+// Clean up.
+localStorage.clear();
+
+</script>
+</body>
diff --git a/devtools/client/shared/shim/test/test_service_wm.html b/devtools/client/shared/shim/test/test_service_wm.html
new file mode 100644
index 000000000..4db602f7e
--- /dev/null
+++ b/devtools/client/shared/shim/test/test_service_wm.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1310279
+-->
+<head>
+ <title>Test for Bug 1310279 - replace Services.wm</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css">
+
+ <script type="application/javascript;version=1.8">
+ "use strict";
+ var exports = {}
+ var module = {exports};
+ </script>
+
+ <script type="application/javascript;version=1.8"
+ src="resource://devtools/client/shared/shim/Services.js"></script>
+</head>
+<body>
+
+<script type="application/javascript;version=1.8">
+ "use strict";
+
+ function hurray(window) {
+ ok(true, "window loaded");
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ Services.wm.getMostRecentWindow().openUILinkIn("file_service_wm.html");
+
+</script>
+</body>
diff --git a/devtools/client/shared/source-utils.js b/devtools/client/shared/source-utils.js
new file mode 100644
index 000000000..974fd272d
--- /dev/null
+++ b/devtools/client/shared/source-utils.js
@@ -0,0 +1,328 @@
+/* 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 { LocalizationHelper } = require("devtools/shared/l10n");
+
+const l10n = new LocalizationHelper("devtools/client/locales/components.properties");
+const UNKNOWN_SOURCE_STRING = l10n.getStr("frame.unknownSource");
+
+// Character codes used in various parsing helper functions.
+const CHAR_CODE_A = "a".charCodeAt(0);
+const CHAR_CODE_C = "c".charCodeAt(0);
+const CHAR_CODE_D = "d".charCodeAt(0);
+const CHAR_CODE_E = "e".charCodeAt(0);
+const CHAR_CODE_F = "f".charCodeAt(0);
+const CHAR_CODE_H = "h".charCodeAt(0);
+const CHAR_CODE_I = "i".charCodeAt(0);
+const CHAR_CODE_J = "j".charCodeAt(0);
+const CHAR_CODE_L = "l".charCodeAt(0);
+const CHAR_CODE_M = "m".charCodeAt(0);
+const CHAR_CODE_O = "o".charCodeAt(0);
+const CHAR_CODE_P = "p".charCodeAt(0);
+const CHAR_CODE_R = "r".charCodeAt(0);
+const CHAR_CODE_S = "s".charCodeAt(0);
+const CHAR_CODE_T = "t".charCodeAt(0);
+const CHAR_CODE_U = "u".charCodeAt(0);
+const CHAR_CODE_COLON = ":".charCodeAt(0);
+const CHAR_CODE_SLASH = "/".charCodeAt(0);
+const CHAR_CODE_CAP_S = "S".charCodeAt(0);
+
+// The cache used in the `parseURL` function.
+const gURLStore = new Map();
+// The cache used in the `getSourceNames` function.
+const gSourceNamesStore = new Map();
+
+/**
+ * Takes a string and returns an object containing all the properties
+ * available on an URL instance, with additional properties (fileName),
+ * Leverages caching.
+ *
+ * @param {String} location
+ * @return {Object?} An object containing most properties available
+ * in https://developer.mozilla.org/en-US/docs/Web/API/URL
+ */
+
+function parseURL(location) {
+ let url = gURLStore.get(location);
+
+ if (url !== void 0) {
+ return url;
+ }
+
+ try {
+ url = new URL(location);
+ // The callers were generally written to expect a URL from
+ // sdk/url, which is subtly different. So, work around some
+ // important differences here.
+ url = {
+ href: url.href,
+ protocol: url.protocol,
+ host: url.host,
+ hostname: url.hostname,
+ port: url.port || null,
+ pathname: url.pathname,
+ search: url.search,
+ hash: url.hash,
+ username: url.username,
+ password: url.password,
+ origin: url.origin,
+ };
+
+ // Definitions:
+ // Example: https://foo.com:8888/file.js
+ // `hostname`: "foo.com"
+ // `host`: "foo.com:8888"
+ let isChrome = isChromeScheme(location);
+
+ url.fileName = url.pathname ?
+ (url.pathname.slice(url.pathname.lastIndexOf("/") + 1) || "/") :
+ "/";
+
+ if (isChrome) {
+ url.hostname = null;
+ url.host = null;
+ }
+
+ gURLStore.set(location, url);
+ return url;
+ } catch (e) {
+ gURLStore.set(location, null);
+ return null;
+ }
+}
+
+/**
+ * Parse a source into a short and long name as well as a host name.
+ *
+ * @param {String} source
+ * The source to parse. Can be a URI or names like "(eval)" or
+ * "self-hosted".
+ * @return {Object}
+ * An object with the following properties:
+ * - {String} short: A short name for the source.
+ * - "http://page.com/test.js#go?q=query" -> "test.js"
+ * - {String} long: The full, long name for the source, with
+ hash/query stripped.
+ * - "http://page.com/test.js#go?q=query" -> "http://page.com/test.js"
+ * - {String?} host: If available, the host name for the source.
+ * - "http://page.com/test.js#go?q=query" -> "page.com"
+ */
+function getSourceNames(source) {
+ let data = gSourceNamesStore.get(source);
+
+ if (data) {
+ return data;
+ }
+
+ let short, long, host;
+ const sourceStr = source ? String(source) : "";
+
+ // If `data:...` uri
+ if (isDataScheme(sourceStr)) {
+ let commaIndex = sourceStr.indexOf(",");
+ if (commaIndex > -1) {
+ // The `short` name for a data URI becomes `data:` followed by the actual
+ // encoded content, omitting the MIME type, and charset.
+ short = `data:${sourceStr.substring(commaIndex + 1)}`.slice(0, 100);
+ let result = { short, long: sourceStr };
+ gSourceNamesStore.set(source, result);
+ return result;
+ }
+ }
+
+ // If Scratchpad URI, like "Scratchpad/1"; no modifications,
+ // and short/long are the same.
+ if (isScratchpadScheme(sourceStr)) {
+ let result = { short: sourceStr, long: sourceStr };
+ gSourceNamesStore.set(source, result);
+ return result;
+ }
+
+ const parsedUrl = parseURL(sourceStr);
+
+ if (!parsedUrl) {
+ // Malformed URI.
+ long = sourceStr;
+ short = sourceStr.slice(0, 100);
+ } else {
+ host = parsedUrl.host;
+
+ long = parsedUrl.href;
+ if (parsedUrl.hash) {
+ long = long.replace(parsedUrl.hash, "");
+ }
+ if (parsedUrl.search) {
+ long = long.replace(parsedUrl.search, "");
+ }
+
+ short = parsedUrl.fileName;
+ // If `short` is just a slash, and we actually have a path,
+ // strip the slash and parse again to get a more useful short name.
+ // e.g. "http://foo.com/bar/" -> "bar", rather than "/"
+ if (short === "/" && parsedUrl.pathname !== "/") {
+ short = parseURL(long.replace(/\/$/, "")).fileName;
+ }
+ }
+
+ if (!short) {
+ if (!long) {
+ long = UNKNOWN_SOURCE_STRING;
+ }
+ short = long.slice(0, 100);
+ }
+
+ let result = { short, long, host };
+ gSourceNamesStore.set(source, result);
+ return result;
+}
+
+// For the functions below, we assume that we will never access the location
+// argument out of bounds, which is indeed the vast majority of cases.
+//
+// They are written this way because they are hot. Each frame is checked for
+// being content or chrome when processing the profile.
+
+function isColonSlashSlash(location, i = 0) {
+ return location.charCodeAt(++i) === CHAR_CODE_COLON &&
+ location.charCodeAt(++i) === CHAR_CODE_SLASH &&
+ location.charCodeAt(++i) === CHAR_CODE_SLASH;
+}
+
+/**
+ * Checks for a Scratchpad URI, like "Scratchpad/1"
+ */
+function isScratchpadScheme(location, i = 0) {
+ return location.charCodeAt(i) === CHAR_CODE_CAP_S &&
+ location.charCodeAt(++i) === CHAR_CODE_C &&
+ location.charCodeAt(++i) === CHAR_CODE_R &&
+ location.charCodeAt(++i) === CHAR_CODE_A &&
+ location.charCodeAt(++i) === CHAR_CODE_T &&
+ location.charCodeAt(++i) === CHAR_CODE_C &&
+ location.charCodeAt(++i) === CHAR_CODE_H &&
+ location.charCodeAt(++i) === CHAR_CODE_P &&
+ location.charCodeAt(++i) === CHAR_CODE_A &&
+ location.charCodeAt(++i) === CHAR_CODE_D &&
+ location.charCodeAt(++i) === CHAR_CODE_SLASH;
+}
+
+function isDataScheme(location, i = 0) {
+ return location.charCodeAt(i) === CHAR_CODE_D &&
+ location.charCodeAt(++i) === CHAR_CODE_A &&
+ location.charCodeAt(++i) === CHAR_CODE_T &&
+ location.charCodeAt(++i) === CHAR_CODE_A &&
+ location.charCodeAt(++i) === CHAR_CODE_COLON;
+}
+
+function isContentScheme(location, i = 0) {
+ let firstChar = location.charCodeAt(i);
+
+ switch (firstChar) {
+ // "http://" or "https://"
+ case CHAR_CODE_H:
+ if (location.charCodeAt(++i) === CHAR_CODE_T &&
+ location.charCodeAt(++i) === CHAR_CODE_T &&
+ location.charCodeAt(++i) === CHAR_CODE_P) {
+ if (location.charCodeAt(i + 1) === CHAR_CODE_S) {
+ ++i;
+ }
+ return isColonSlashSlash(location, i);
+ }
+ return false;
+
+ // "file://"
+ case CHAR_CODE_F:
+ if (location.charCodeAt(++i) === CHAR_CODE_I &&
+ location.charCodeAt(++i) === CHAR_CODE_L &&
+ location.charCodeAt(++i) === CHAR_CODE_E) {
+ return isColonSlashSlash(location, i);
+ }
+ return false;
+
+ // "app://"
+ case CHAR_CODE_A:
+ if (location.charCodeAt(++i) == CHAR_CODE_P &&
+ location.charCodeAt(++i) == CHAR_CODE_P) {
+ return isColonSlashSlash(location, i);
+ }
+ return false;
+
+ default:
+ return false;
+ }
+}
+
+function isChromeScheme(location, i = 0) {
+ let firstChar = location.charCodeAt(i);
+
+ switch (firstChar) {
+ // "chrome://"
+ case CHAR_CODE_C:
+ if (location.charCodeAt(++i) === CHAR_CODE_H &&
+ location.charCodeAt(++i) === CHAR_CODE_R &&
+ location.charCodeAt(++i) === CHAR_CODE_O &&
+ location.charCodeAt(++i) === CHAR_CODE_M &&
+ location.charCodeAt(++i) === CHAR_CODE_E) {
+ return isColonSlashSlash(location, i);
+ }
+ return false;
+
+ // "resource://"
+ case CHAR_CODE_R:
+ if (location.charCodeAt(++i) === CHAR_CODE_E &&
+ location.charCodeAt(++i) === CHAR_CODE_S &&
+ location.charCodeAt(++i) === CHAR_CODE_O &&
+ location.charCodeAt(++i) === CHAR_CODE_U &&
+ location.charCodeAt(++i) === CHAR_CODE_R &&
+ location.charCodeAt(++i) === CHAR_CODE_C &&
+ location.charCodeAt(++i) === CHAR_CODE_E) {
+ return isColonSlashSlash(location, i);
+ }
+ return false;
+
+ // "jar:file://"
+ case CHAR_CODE_J:
+ if (location.charCodeAt(++i) === CHAR_CODE_A &&
+ location.charCodeAt(++i) === CHAR_CODE_R &&
+ location.charCodeAt(++i) === CHAR_CODE_COLON &&
+ location.charCodeAt(++i) === CHAR_CODE_F &&
+ location.charCodeAt(++i) === CHAR_CODE_I &&
+ location.charCodeAt(++i) === CHAR_CODE_L &&
+ location.charCodeAt(++i) === CHAR_CODE_E) {
+ return isColonSlashSlash(location, i);
+ }
+ return false;
+
+ default:
+ return false;
+ }
+}
+
+/**
+ * A utility method to get the file name from a sourcemapped location
+ * The sourcemap location can be in any form. This method returns a
+ * formatted file name for different cases like Windows or OSX.
+ * @param source
+ * @returns String
+ */
+function getSourceMappedFile(source) {
+ // If sourcemapped source is a OSX path, return
+ // the characters after last "/".
+ // If sourcemapped source is a Windowss path, return
+ // the characters after last "\\".
+ if (source.lastIndexOf("/") >= 0) {
+ source = source.slice(source.lastIndexOf("/") + 1);
+ } else if (source.lastIndexOf("\\") >= 0) {
+ source = source.slice(source.lastIndexOf("\\") + 1);
+ }
+ return source;
+}
+
+exports.parseURL = parseURL;
+exports.getSourceNames = getSourceNames;
+exports.isScratchpadScheme = isScratchpadScheme;
+exports.isChromeScheme = isChromeScheme;
+exports.isContentScheme = isContentScheme;
+exports.isDataScheme = isDataScheme;
+exports.getSourceMappedFile = getSourceMappedFile;
diff --git a/devtools/client/shared/splitview.css b/devtools/client/shared/splitview.css
new file mode 100644
index 000000000..de9c4e330
--- /dev/null
+++ b/devtools/client/shared/splitview.css
@@ -0,0 +1,83 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+box,
+.splitview-nav {
+ -moz-box-flex: 1;
+ -moz-box-orient: vertical;
+}
+
+.splitview-nav-container {
+ -moz-box-pack: center;
+}
+
+.loading .splitview-nav-container > .placeholder {
+ display: none !important;
+}
+
+.splitview-controller,
+.splitview-main {
+ -moz-box-flex: 0;
+}
+
+.splitview-controller {
+ min-height: 3em;
+ max-height: 14em;
+ max-width: 400px;
+ min-width: 200px;
+}
+
+.splitview-nav {
+ display: -moz-box;
+ overflow-x: hidden;
+ overflow-y: auto;
+}
+
+/* only the active details pane is shown */
+.splitview-side-details > * {
+ display: none;
+}
+.splitview-side-details > .splitview-active {
+ display: -moz-box;
+}
+
+/* this is to keep in sync with SplitView.jsm's LANDSCAPE_MEDIA_QUERY */
+@media (min-width: 701px) {
+ .splitview-root {
+ -moz-box-orient: horizontal;
+ }
+ .splitview-controller {
+ max-height: none;
+ }
+ .splitview-details {
+ display: none;
+ }
+ .splitview-details.splitview-active {
+ display: -moz-box;
+ }
+}
+
+/* filtered items are hidden */
+ol.splitview-nav > li.splitview-filtered {
+ display: none;
+}
+
+/* "empty list" and "all filtered" placeholders are hidden */
+.splitview-nav:empty,
+.splitview-nav.splitview-all-filtered,
+.splitview-nav + .splitview-nav.placeholder {
+ display: none;
+}
+.splitview-nav.splitview-all-filtered ~ .splitview-nav.placeholder.all-filtered,
+.splitview-nav:empty ~ .splitview-nav.placeholder.empty {
+ display: -moz-box;
+}
+
+/* portrait mode */
+@media (max-width: 700px) {
+ .splitview-controller {
+ max-width: none;
+ }
+}
diff --git a/devtools/client/shared/suggestion-picker.js b/devtools/client/shared/suggestion-picker.js
new file mode 100644
index 000000000..6155eb3bf
--- /dev/null
+++ b/devtools/client/shared/suggestion-picker.js
@@ -0,0 +1,176 @@
+/* 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";
+
+/**
+ * Allows to find the lowest ranking index in an index
+ * of suggestions, by comparing it to another array of "most relevant" items
+ * which has been sorted by relevance.
+ *
+ * Example usage:
+ * let sortedBrowsers = ["firefox", "safari", "edge", "chrome"];
+ * let myBrowsers = ["brave", "chrome", "firefox"];
+ * let bestBrowserIndex = findMostRelevantIndex(myBrowsers, sortedBrowsers);
+ * // returns "2", the index of firefox in myBrowsers array
+ *
+ * @param {Array} items
+ * Array of items to compare against sortedItems.
+ * @param {Array} sortedItems
+ * Array of sorted items that suggestions are evaluated against. Array
+ * should be sorted by relevance, most relevant item first.
+ * @return {Number}
+ */
+function findMostRelevantIndex(items, sortedItems) {
+ if (!Array.isArray(items) || !Array.isArray(sortedItems)) {
+ throw new Error("Please provide valid items and sortedItems arrays.");
+ }
+
+ // If the items array is empty, no valid index can be found.
+ if (!items.length) {
+ return -1;
+ }
+
+ // Return 0 if no match was found in the suggestion list.
+ let bestIndex = 0;
+ let lowestIndex = Infinity;
+ items.forEach((item, i) => {
+ let index = sortedItems.indexOf(item);
+ if (index !== -1 && index <= lowestIndex) {
+ lowestIndex = index;
+ bestIndex = i;
+ }
+ });
+
+ return bestIndex;
+}
+
+/**
+ * Top 100 CSS property names sorted by relevance, most relevant first.
+ *
+ * List based on the one used by Chrome devtools :
+ * https://code.google.com/p/chromium/codesearch#chromium/src/third_party/
+ * WebKit/Source/devtools/front_end/sdk/CSSMetadata.js&q=CSSMetadata&
+ * sq=package:chromium&type=cs&l=676
+ *
+ * The data is a mix of https://www.chromestatus.com/metrics/css and usage
+ * metrics from popular sites collected via https://gist.github.com/NV/3751436
+ *
+ * @type {Array}
+ */
+const SORTED_CSS_PROPERTIES = [
+ "width",
+ "margin",
+ "height",
+ "padding",
+ "font-size",
+ "border",
+ "display",
+ "position",
+ "text-align",
+ "background",
+ "background-color",
+ "top",
+ "font-weight",
+ "color",
+ "overflow",
+ "font-family",
+ "margin-top",
+ "float",
+ "opacity",
+ "cursor",
+ "left",
+ "text-decoration",
+ "background-image",
+ "right",
+ "line-height",
+ "margin-left",
+ "visibility",
+ "margin-bottom",
+ "padding-top",
+ "z-index",
+ "margin-right",
+ "background-position",
+ "vertical-align",
+ "padding-left",
+ "background-repeat",
+ "border-bottom",
+ "padding-right",
+ "border-top",
+ "padding-bottom",
+ "clear",
+ "white-space",
+ "bottom",
+ "border-color",
+ "max-width",
+ "border-radius",
+ "border-right",
+ "outline",
+ "border-left",
+ "font-style",
+ "content",
+ "min-width",
+ "min-height",
+ "box-sizing",
+ "list-style",
+ "border-width",
+ "box-shadow",
+ "font",
+ "border-collapse",
+ "text-shadow",
+ "text-indent",
+ "border-style",
+ "max-height",
+ "text-overflow",
+ "background-size",
+ "text-transform",
+ "zoom",
+ "list-style-type",
+ "border-spacing",
+ "word-wrap",
+ "overflow-y",
+ "transition",
+ "border-top-color",
+ "border-bottom-color",
+ "border-top-right-radius",
+ "letter-spacing",
+ "border-top-left-radius",
+ "border-bottom-left-radius",
+ "border-bottom-right-radius",
+ "overflow-x",
+ "pointer-events",
+ "border-right-color",
+ "transform",
+ "border-top-width",
+ "border-bottom-width",
+ "border-right-width",
+ "direction",
+ "animation",
+ "border-left-color",
+ "clip",
+ "border-left-width",
+ "table-layout",
+ "src",
+ "resize",
+ "word-break",
+ "background-clip",
+ "transform-origin",
+ "font-variant",
+ "filter",
+ "quotes",
+ "word-spacing"
+];
+
+/**
+ * Helper to find the most relevant CSS property name in a provided array.
+ *
+ * @param items {Array}
+ * Array of CSS property names.
+ */
+function findMostRelevantCssPropertyIndex(items) {
+ return findMostRelevantIndex(items, SORTED_CSS_PROPERTIES);
+}
+
+exports.findMostRelevantIndex = findMostRelevantIndex;
+exports.findMostRelevantCssPropertyIndex = findMostRelevantCssPropertyIndex;
diff --git a/devtools/client/shared/telemetry.js b/devtools/client/shared/telemetry.js
new file mode 100644
index 000000000..64a299581
--- /dev/null
+++ b/devtools/client/shared/telemetry.js
@@ -0,0 +1,341 @@
+/* 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/. */
+
+/**
+ * Telemetry.
+ *
+ * To add metrics for a tool:
+ *
+ * 1. Create count, flag, and exponential entries in
+ * toolkit/components/telemetry/Histograms.json. Each type is optional but it
+ * is best if all three can be included.
+ *
+ * 2. Add your chart entries to devtools/client/shared/telemetry.js
+ * (Telemetry.prototype._histograms):
+ * mytoolname: {
+ * histogram: "DEVTOOLS_MYTOOLNAME_OPENED_COUNT",
+ * timerHistogram: "DEVTOOLS_MYTOOLNAME_TIME_ACTIVE_SECONDS"
+ * },
+ *
+ * 3. Include this module at the top of your tool. Use:
+ * let Telemetry = require("devtools/client/shared/telemetry")
+ *
+ * 4. Create a telemetry instance in your tool's constructor:
+ * this._telemetry = new Telemetry();
+ *
+ * 5. When your tool is opened call:
+ * this._telemetry.toolOpened("mytoolname");
+ *
+ * 6. When your tool is closed call:
+ * this._telemetry.toolClosed("mytoolname");
+ *
+ * Note:
+ * You can view telemetry stats for your local Firefox instance via
+ * about:telemetry.
+ *
+ * You can view telemetry stats for large groups of Firefox users at
+ * telemetry.mozilla.org.
+ */
+
+"use strict";
+
+const TOOLS_OPENED_PREF = "devtools.telemetry.tools.opened.version";
+
+function Telemetry() {
+ // Bind pretty much all functions so that callers do not need to.
+ this.toolOpened = this.toolOpened.bind(this);
+ this.toolClosed = this.toolClosed.bind(this);
+ this.log = this.log.bind(this);
+ this.logOncePerBrowserVersion = this.logOncePerBrowserVersion.bind(this);
+ this.destroy = this.destroy.bind(this);
+
+ this._timers = new Map();
+}
+
+module.exports = Telemetry;
+
+var Services = require("Services");
+
+Telemetry.prototype = {
+ _histograms: {
+ toolbox: {
+ histogram: "DEVTOOLS_TOOLBOX_OPENED_COUNT",
+ timerHistogram: "DEVTOOLS_TOOLBOX_TIME_ACTIVE_SECONDS"
+ },
+ options: {
+ histogram: "DEVTOOLS_OPTIONS_OPENED_COUNT",
+ timerHistogram: "DEVTOOLS_OPTIONS_TIME_ACTIVE_SECONDS"
+ },
+ webconsole: {
+ histogram: "DEVTOOLS_WEBCONSOLE_OPENED_COUNT",
+ timerHistogram: "DEVTOOLS_WEBCONSOLE_TIME_ACTIVE_SECONDS"
+ },
+ browserconsole: {
+ histogram: "DEVTOOLS_BROWSERCONSOLE_OPENED_COUNT",
+ timerHistogram: "DEVTOOLS_BROWSERCONSOLE_TIME_ACTIVE_SECONDS"
+ },
+ inspector: {
+ histogram: "DEVTOOLS_INSPECTOR_OPENED_COUNT",
+ timerHistogram: "DEVTOOLS_INSPECTOR_TIME_ACTIVE_SECONDS"
+ },
+ ruleview: {
+ histogram: "DEVTOOLS_RULEVIEW_OPENED_COUNT",
+ timerHistogram: "DEVTOOLS_RULEVIEW_TIME_ACTIVE_SECONDS"
+ },
+ computedview: {
+ histogram: "DEVTOOLS_COMPUTEDVIEW_OPENED_COUNT",
+ timerHistogram: "DEVTOOLS_COMPUTEDVIEW_TIME_ACTIVE_SECONDS"
+ },
+ fontinspector: {
+ histogram: "DEVTOOLS_FONTINSPECTOR_OPENED_COUNT",
+ timerHistogram: "DEVTOOLS_FONTINSPECTOR_TIME_ACTIVE_SECONDS"
+ },
+ animationinspector: {
+ histogram: "DEVTOOLS_ANIMATIONINSPECTOR_OPENED_COUNT",
+ timerHistogram: "DEVTOOLS_ANIMATIONINSPECTOR_TIME_ACTIVE_SECONDS"
+ },
+ jsdebugger: {
+ histogram: "DEVTOOLS_JSDEBUGGER_OPENED_COUNT",
+ timerHistogram: "DEVTOOLS_JSDEBUGGER_TIME_ACTIVE_SECONDS"
+ },
+ jsbrowserdebugger: {
+ histogram: "DEVTOOLS_JSBROWSERDEBUGGER_OPENED_COUNT",
+ timerHistogram: "DEVTOOLS_JSBROWSERDEBUGGER_TIME_ACTIVE_SECONDS"
+ },
+ styleeditor: {
+ histogram: "DEVTOOLS_STYLEEDITOR_OPENED_COUNT",
+ timerHistogram: "DEVTOOLS_STYLEEDITOR_TIME_ACTIVE_SECONDS"
+ },
+ shadereditor: {
+ histogram: "DEVTOOLS_SHADEREDITOR_OPENED_COUNT",
+ timerHistogram: "DEVTOOLS_SHADEREDITOR_TIME_ACTIVE_SECONDS"
+ },
+ webaudioeditor: {
+ histogram: "DEVTOOLS_WEBAUDIOEDITOR_OPENED_COUNT",
+ timerHistogram: "DEVTOOLS_WEBAUDIOEDITOR_TIME_ACTIVE_SECONDS"
+ },
+ canvasdebugger: {
+ histogram: "DEVTOOLS_CANVASDEBUGGER_OPENED_COUNT",
+ timerHistogram: "DEVTOOLS_CANVASDEBUGGER_TIME_ACTIVE_SECONDS"
+ },
+ performance: {
+ histogram: "DEVTOOLS_JSPROFILER_OPENED_COUNT",
+ timerHistogram: "DEVTOOLS_JSPROFILER_TIME_ACTIVE_SECONDS"
+ },
+ memory: {
+ histogram: "DEVTOOLS_MEMORY_OPENED_COUNT",
+ timerHistogram: "DEVTOOLS_MEMORY_TIME_ACTIVE_SECONDS"
+ },
+ netmonitor: {
+ histogram: "DEVTOOLS_NETMONITOR_OPENED_COUNT",
+ timerHistogram: "DEVTOOLS_NETMONITOR_TIME_ACTIVE_SECONDS"
+ },
+ storage: {
+ histogram: "DEVTOOLS_STORAGE_OPENED_COUNT",
+ timerHistogram: "DEVTOOLS_STORAGE_TIME_ACTIVE_SECONDS"
+ },
+ paintflashing: {
+ histogram: "DEVTOOLS_PAINTFLASHING_OPENED_COUNT",
+ timerHistogram: "DEVTOOLS_PAINTFLASHING_TIME_ACTIVE_SECONDS"
+ },
+ scratchpad: {
+ histogram: "DEVTOOLS_SCRATCHPAD_OPENED_COUNT",
+ timerHistogram: "DEVTOOLS_SCRATCHPAD_TIME_ACTIVE_SECONDS"
+ },
+ "scratchpad-window": {
+ histogram: "DEVTOOLS_SCRATCHPAD_WINDOW_OPENED_COUNT",
+ timerHistogram: "DEVTOOLS_SCRATCHPAD_WINDOW_TIME_ACTIVE_SECONDS"
+ },
+ responsive: {
+ histogram: "DEVTOOLS_RESPONSIVE_OPENED_COUNT",
+ timerHistogram: "DEVTOOLS_RESPONSIVE_TIME_ACTIVE_SECONDS"
+ },
+ eyedropper: {
+ histogram: "DEVTOOLS_EYEDROPPER_OPENED_COUNT",
+ },
+ menueyedropper: {
+ histogram: "DEVTOOLS_MENU_EYEDROPPER_OPENED_COUNT",
+ },
+ pickereyedropper: {
+ histogram: "DEVTOOLS_PICKER_EYEDROPPER_OPENED_COUNT",
+ },
+ toolbareyedropper: {
+ histogram: "DEVTOOLS_TOOLBAR_EYEDROPPER_OPENED_COUNT",
+ },
+ developertoolbar: {
+ histogram: "DEVTOOLS_DEVELOPERTOOLBAR_OPENED_COUNT",
+ timerHistogram: "DEVTOOLS_DEVELOPERTOOLBAR_TIME_ACTIVE_SECONDS"
+ },
+ aboutdebugging: {
+ histogram: "DEVTOOLS_ABOUTDEBUGGING_OPENED_COUNT",
+ timerHistogram: "DEVTOOLS_ABOUTDEBUGGING_TIME_ACTIVE_SECONDS"
+ },
+ webide: {
+ histogram: "DEVTOOLS_WEBIDE_OPENED_COUNT",
+ timerHistogram: "DEVTOOLS_WEBIDE_TIME_ACTIVE_SECONDS"
+ },
+ webideProjectEditor: {
+ histogram: "DEVTOOLS_WEBIDE_PROJECT_EDITOR_OPENED_COUNT",
+ timerHistogram: "DEVTOOLS_WEBIDE_PROJECT_EDITOR_TIME_ACTIVE_SECONDS"
+ },
+ webideProjectEditorSave: {
+ histogram: "DEVTOOLS_WEBIDE_PROJECT_EDITOR_SAVE_COUNT",
+ },
+ webideNewProject: {
+ histogram: "DEVTOOLS_WEBIDE_NEW_PROJECT_COUNT",
+ },
+ webideImportProject: {
+ histogram: "DEVTOOLS_WEBIDE_IMPORT_PROJECT_COUNT",
+ },
+ custom: {
+ histogram: "DEVTOOLS_CUSTOM_OPENED_COUNT",
+ timerHistogram: "DEVTOOLS_CUSTOM_TIME_ACTIVE_SECONDS"
+ },
+ reloadAddonInstalled: {
+ histogram: "DEVTOOLS_RELOAD_ADDON_INSTALLED_COUNT",
+ },
+ reloadAddonReload: {
+ histogram: "DEVTOOLS_RELOAD_ADDON_RELOAD_COUNT",
+ },
+ },
+
+ /**
+ * Add an entry to a histogram.
+ *
+ * @param {String} id
+ * Used to look up the relevant histogram ID and log true to that
+ * histogram.
+ */
+ toolOpened: function (id) {
+ let charts = this._histograms[id] || this._histograms.custom;
+
+ if (charts.histogram) {
+ this.log(charts.histogram, true);
+ }
+ if (charts.timerHistogram) {
+ this.startTimer(charts.timerHistogram);
+ }
+ },
+
+ /**
+ * Record that an action occurred. Aliases to `toolOpened`, so it's just for
+ * readability at the call site for cases where we aren't actually opening
+ * tools.
+ */
+ actionOccurred(id) {
+ this.toolOpened(id);
+ },
+
+ toolClosed: function (id) {
+ let charts = this._histograms[id];
+
+ if (!charts || !charts.timerHistogram) {
+ return;
+ }
+
+ this.stopTimer(charts.timerHistogram);
+ },
+
+ /**
+ * Record the start time for a timing-based histogram entry.
+ *
+ * @param String histogramId
+ * Histogram in which the data is to be stored.
+ */
+ startTimer: function (histogramId) {
+ this._timers.set(histogramId, new Date());
+ },
+
+ /**
+ * Stop the timer and log elasped time for a timing-based histogram entry.
+ *
+ * @param String histogramId
+ * Histogram in which the data is to be stored.
+ * @param String key [optional]
+ * Optional key for a keyed histogram.
+ */
+ stopTimer: function (histogramId, key) {
+ let startTime = this._timers.get(histogramId);
+ if (startTime) {
+ let time = (new Date() - startTime) / 1000;
+ if (!key) {
+ this.log(histogramId, time);
+ } else {
+ this.logKeyed(histogramId, key, time);
+ }
+ this._timers.delete(histogramId);
+ }
+ },
+
+ /**
+ * Log a value to a histogram.
+ *
+ * @param {String} histogramId
+ * Histogram in which the data is to be stored.
+ * @param value
+ * Value to store.
+ */
+ log: function (histogramId, value) {
+ if (histogramId) {
+ try {
+ let histogram = Services.telemetry.getHistogramById(histogramId);
+ histogram.add(value);
+ } catch (e) {
+ dump("Warning: An attempt was made to write to the " + histogramId +
+ " histogram, which is not defined in Histograms.json\n");
+ }
+ }
+ },
+
+ /**
+ * Log a value to a keyed histogram.
+ *
+ * @param {String} histogramId
+ * Histogram in which the data is to be stored.
+ * @param {String} key
+ * The key within the single histogram.
+ * @param value
+ * Value to store.
+ */
+ logKeyed: function (histogramId, key, value) {
+ if (histogramId) {
+ try {
+ let histogram = Services.telemetry.getKeyedHistogramById(histogramId);
+ histogram.add(key, value);
+ } catch (e) {
+ dump("Warning: An attempt was made to write to the " + histogramId +
+ " histogram, which is not defined in Histograms.json\n");
+ }
+ }
+ },
+
+ /**
+ * Log info about usage once per browser version. This allows us to discover
+ * how many individual users are using our tools for each browser version.
+ *
+ * @param {String} perUserHistogram
+ * Histogram in which the data is to be stored.
+ */
+ logOncePerBrowserVersion: function (perUserHistogram, value) {
+ let currentVersion = Services.appinfo.version;
+ let latest = Services.prefs.getCharPref(TOOLS_OPENED_PREF);
+ let latestObj = JSON.parse(latest);
+
+ let lastVersionHistogramUpdated = latestObj[perUserHistogram];
+
+ if (typeof lastVersionHistogramUpdated == "undefined" ||
+ lastVersionHistogramUpdated !== currentVersion) {
+ latestObj[perUserHistogram] = currentVersion;
+ latest = JSON.stringify(latestObj);
+ Services.prefs.setCharPref(TOOLS_OPENED_PREF, latest);
+ this.log(perUserHistogram, value);
+ }
+ },
+
+ destroy: function () {
+ for (let histogramId of this._timers.keys()) {
+ this.stopTimer(histogramId);
+ }
+ }
+};
diff --git a/devtools/client/shared/test/.eslintrc.js b/devtools/client/shared/test/.eslintrc.js
new file mode 100644
index 000000000..ed80d6d12
--- /dev/null
+++ b/devtools/client/shared/test/.eslintrc.js
@@ -0,0 +1,9 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../../.eslintrc.mochitests.js",
+ "globals": {
+ "DeveloperToolbar": true
+ }
+};
diff --git a/devtools/client/shared/test/browser.ini b/devtools/client/shared/test/browser.ini
new file mode 100644
index 000000000..ad3f52fbd
--- /dev/null
+++ b/devtools/client/shared/test/browser.ini
@@ -0,0 +1,188 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ browser_layoutHelpers.html
+ browser_layoutHelpers-getBoxQuads.html
+ browser_templater_basic.html
+ browser_toolbar_basic.html
+ browser_toolbar_webconsole_errors_count.html
+ browser_devices.json
+ doc_options-view.xul
+ head.js
+ helper_color_data.js
+ helper_html_tooltip.js
+ helper_inplace_editor.js
+ html-mdn-css-basic-testing.html
+ html-mdn-css-no-summary.html
+ html-mdn-css-no-summary-or-syntax.html
+ html-mdn-css-no-syntax.html
+ html-mdn-css-syntax-old-style.html
+ leakhunt.js
+ test-actor.js
+ test-actor-registry.js
+ !/devtools/client/framework/test/shared-head.js
+
+[browser_css_angle.js]
+[browser_css_color.js]
+[browser_cubic-bezier-01.js]
+[browser_cubic-bezier-02.js]
+[browser_cubic-bezier-03.js]
+[browser_cubic-bezier-04.js]
+[browser_cubic-bezier-05.js]
+[browser_cubic-bezier-06.js]
+[browser_filter-editor-01.js]
+[browser_filter-editor-02.js]
+[browser_filter-editor-03.js]
+[browser_filter-editor-04.js]
+[browser_filter-editor-05.js]
+[browser_filter-editor-06.js]
+[browser_filter-editor-07.js]
+[browser_filter-editor-08.js]
+[browser_filter-editor-09.js]
+[browser_filter-editor-10.js]
+[browser_filter-presets-01.js]
+[browser_filter-presets-02.js]
+[browser_filter-presets-03.js]
+[browser_flame-graph-01.js]
+[browser_flame-graph-02.js]
+[browser_flame-graph-03a.js]
+[browser_flame-graph-03b.js]
+[browser_flame-graph-03c.js]
+[browser_flame-graph-04.js]
+[browser_flame-graph-05.js]
+[browser_flame-graph-utils-01.js]
+[browser_flame-graph-utils-02.js]
+[browser_flame-graph-utils-03.js]
+[browser_flame-graph-utils-04.js]
+[browser_flame-graph-utils-05.js]
+[browser_flame-graph-utils-06.js]
+[browser_flame-graph-utils-hash.js]
+[browser_graphs-01.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-02.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-03.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-04.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-05.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-06.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-07a.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-07b.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-07c.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-07d.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-07e.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-08.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-09a.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-09b.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-09c.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-09d.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-09e.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-09f.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-10a.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-10b.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-10c.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-11a.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-11b.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-12.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-13.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-14.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-15.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-16.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_html_tooltip-01.js]
+[browser_html_tooltip-02.js]
+[browser_html_tooltip-03.js]
+[browser_html_tooltip-04.js]
+[browser_html_tooltip-05.js]
+[browser_html_tooltip_arrow-01.js]
+[browser_html_tooltip_arrow-02.js]
+[browser_html_tooltip_consecutive-show.js]
+[browser_html_tooltip_hover.js]
+[browser_html_tooltip_offset.js]
+[browser_html_tooltip_rtl.js]
+[browser_html_tooltip_variable-height.js]
+[browser_html_tooltip_width-auto.js]
+[browser_html_tooltip_xul-wrapper.js]
+[browser_inplace-editor-01.js]
+[browser_inplace-editor-02.js]
+[browser_inplace-editor_autocomplete_01.js]
+[browser_inplace-editor_autocomplete_02.js]
+[browser_inplace-editor_autocomplete_offset.js]
+[browser_inplace-editor_maxwidth.js]
+[browser_keycodes.js]
+[browser_key_shortcuts.js]
+[browser_layoutHelpers.js]
+skip-if = e10s # Layouthelpers test should not run in a content page.
+[browser_layoutHelpers-getBoxQuads.js]
+skip-if = e10s # Layouthelpers test should not run in a content page.
+[browser_mdn-docs-01.js]
+[browser_mdn-docs-02.js]
+[browser_mdn-docs-03.js]
+[browser_num-l10n.js]
+[browser_options-view-01.js]
+[browser_outputparser.js]
+skip-if = e10s # Test intermittently fails with e10s. Bug 1124162.
+[browser_poller.js]
+[browser_prefs-01.js]
+[browser_prefs-02.js]
+[browser_require_raw.js]
+[browser_spectrum.js]
+[browser_theme.js]
+[browser_tableWidget_basic.js]
+[browser_tableWidget_keyboard_interaction.js]
+[browser_tableWidget_mouse_interaction.js]
+[browser_telemetry_button_eyedropper.js]
+[browser_telemetry_button_paintflashing.js]
+skip-if = e10s # Bug 937167 - e10s paintflashing
+[browser_telemetry_button_responsive.js]
+skip-if = e10s # Bug 1067145 - e10s responsiveview
+[browser_telemetry_button_scratchpad.js]
+[browser_telemetry_sidebar.js]
+[browser_telemetry_toolbox.js]
+[browser_telemetry_toolboxtabs_canvasdebugger.js]
+[browser_telemetry_toolboxtabs_inspector.js]
+[browser_telemetry_toolboxtabs_jsdebugger.js]
+[browser_telemetry_toolboxtabs_jsprofiler.js]
+[browser_telemetry_toolboxtabs_netmonitor.js]
+[browser_telemetry_toolboxtabs_options.js]
+[browser_telemetry_toolboxtabs_shadereditor.js]
+[browser_telemetry_toolboxtabs_storage.js]
+[browser_telemetry_toolboxtabs_styleeditor.js]
+[browser_telemetry_toolboxtabs_webaudioeditor.js]
+[browser_telemetry_toolboxtabs_webconsole.js]
+[browser_templater_basic.js]
+[browser_toolbar_basic.js]
+skip-if = (e10s && debug) # Bug 1253035
+[browser_toolbar_tooltip.js]
+[browser_toolbar_webconsole_errors_count.js]
+skip-if = e10s # The developertoolbar error count isn't correct with e10s
+[browser_treeWidget_basic.js]
+[browser_treeWidget_keyboard_interaction.js]
+[browser_treeWidget_mouse_interaction.js]
+[browser_devices.js]
+[browser_theme_switching.js]
diff --git a/devtools/client/shared/test/browser_css_angle.js b/devtools/client/shared/test/browser_css_angle.js
new file mode 100644
index 000000000..903be44d8
--- /dev/null
+++ b/devtools/client/shared/test/browser_css_angle.js
@@ -0,0 +1,176 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* import-globals-from head.js */
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8,browser_css_angle.js";
+var {angleUtils} = require("devtools/client/shared/css-angle");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ let [host] = yield createHost("bottom", TEST_URI);
+
+ info("Starting the test");
+ testAngleUtils();
+ testAngleValidity();
+
+ host.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+function testAngleUtils() {
+ let data = getTestData();
+
+ for (let {authored, deg, rad, grad, turn} of data) {
+ let angle = new angleUtils.CssAngle(authored);
+
+ // Check all values.
+ info("Checking values for " + authored);
+ is(angle.deg, deg, "color.deg === deg");
+ is(angle.rad, rad, "color.rad === rad");
+ is(angle.grad, grad, "color.grad === grad");
+ is(angle.turn, turn, "color.turn === turn");
+
+ testToString(angle, deg, rad, grad, turn);
+ }
+}
+
+function testAngleValidity() {
+ let data = getAngleValidityData();
+
+ for (let {angle, result} of data) {
+ let testAngle = new angleUtils.CssAngle(angle);
+ let validString = testAngle.valid ? " a valid" : "an invalid";
+
+ is(testAngle.valid, result,
+ `Testing that "${angle}" is ${validString} angle`);
+ }
+}
+
+function testToString(angle, deg, rad, grad, turn) {
+ angle.angleUnit = angleUtils.CssAngle.ANGLEUNIT.deg;
+ is(angle.toString(), deg, "toString() with deg type");
+
+ angle.angleUnit = angleUtils.CssAngle.ANGLEUNIT.rad;
+ is(angle.toString(), rad, "toString() with rad type");
+
+ angle.angleUnit = angleUtils.CssAngle.ANGLEUNIT.grad;
+ is(angle.toString(), grad, "toString() with grad type");
+
+ angle.angleUnit = angleUtils.CssAngle.ANGLEUNIT.turn;
+ is(angle.toString(), turn, "toString() with turn type");
+}
+
+function getAngleValidityData() {
+ return [{
+ angle: "0.2turn",
+ result: true
+ }, {
+ angle: "-0.2turn",
+ result: true
+ }, {
+ angle: "-.2turn",
+ result: true
+ }, {
+ angle: "1e02turn",
+ result: true
+ }, {
+ angle: "-2e2turn",
+ result: true
+ }, {
+ angle: ".2turn",
+ result: true
+ }, {
+ angle: "0.2aaturn",
+ result: false
+ }, {
+ angle: "2dega",
+ result: false
+ }, {
+ angle: "0.deg",
+ result: false
+ }, {
+ angle: ".deg",
+ result: false
+ }, {
+ angle: "..2turn",
+ result: false
+ }];
+}
+
+function getTestData() {
+ return [{
+ authored: "0deg",
+ deg: "0deg",
+ rad: "0rad",
+ grad: "0grad",
+ turn: "0turn"
+ }, {
+ authored: "180deg",
+ deg: "180deg",
+ rad: `${Math.round(Math.PI * 10000) / 10000}rad`,
+ grad: "200grad",
+ turn: "0.5turn"
+ }, {
+ authored: "180DEG",
+ deg: "180DEG",
+ rad: `${Math.round(Math.PI * 10000) / 10000}RAD`,
+ grad: "200GRAD",
+ turn: "0.5TURN"
+ }, {
+ authored: `-${Math.PI}rad`,
+ deg: "-180deg",
+ rad: `-${Math.PI}rad`,
+ grad: "-200grad",
+ turn: "-0.5turn"
+ }, {
+ authored: `-${Math.PI}RAD`,
+ deg: "-180DEG",
+ rad: `-${Math.PI}RAD`,
+ grad: "-200GRAD",
+ turn: "-0.5TURN"
+ }, {
+ authored: "100grad",
+ deg: "90deg",
+ rad: `${Math.round(Math.PI / 2 * 10000) / 10000}rad`,
+ grad: "100grad",
+ turn: "0.25turn"
+ }, {
+ authored: "100GRAD",
+ deg: "90DEG",
+ rad: `${Math.round(Math.PI / 2 * 10000) / 10000}RAD`,
+ grad: "100GRAD",
+ turn: "0.25TURN"
+ }, {
+ authored: "-1turn",
+ deg: "-360deg",
+ rad: `${-1 * Math.round(Math.PI * 2 * 10000) / 10000}rad`,
+ grad: "-400grad",
+ turn: "-1turn"
+ }, {
+ authored: "-10TURN",
+ deg: "-3600DEG",
+ rad: `${-1 * Math.round(Math.PI * 2 * 10 * 10000) / 10000}RAD`,
+ grad: "-4000GRAD",
+ turn: "-10TURN"
+ }, {
+ authored: "inherit",
+ deg: "inherit",
+ rad: "inherit",
+ grad: "inherit",
+ turn: "inherit"
+ }, {
+ authored: "initial",
+ deg: "initial",
+ rad: "initial",
+ grad: "initial",
+ turn: "initial"
+ }, {
+ authored: "unset",
+ deg: "unset",
+ rad: "unset",
+ grad: "unset",
+ turn: "unset"
+ }];
+}
diff --git a/devtools/client/shared/test/browser_css_color.js b/devtools/client/shared/test/browser_css_color.js
new file mode 100644
index 000000000..c0846c362
--- /dev/null
+++ b/devtools/client/shared/test/browser_css_color.js
@@ -0,0 +1,137 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8,browser_css_color.js";
+var {colorUtils} = require("devtools/shared/css/color");
+/* global getFixtureColorData */
+loadHelperScript("helper_color_data.js");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ let [host,, doc] = yield createHost("bottom", TEST_URI);
+
+ info("Creating a test canvas element to test colors");
+ let canvas = createTestCanvas(doc);
+ info("Starting the test");
+ testColorUtils(canvas);
+
+ host.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+function createTestCanvas(doc) {
+ let canvas = doc.createElement("canvas");
+ canvas.width = canvas.height = 10;
+ doc.body.appendChild(canvas);
+ return canvas;
+}
+
+function testColorUtils(canvas) {
+ let data = getFixtureColorData();
+
+ for (let {authored, name, hex, hsl, rgb} of data) {
+ let color = new colorUtils.CssColor(authored);
+
+ // Check all values.
+ info("Checking values for " + authored);
+ is(color.name, name, "color.name === name");
+ is(color.hex, hex, "color.hex === hex");
+ is(color.hsl, hsl, "color.hsl === hsl");
+ is(color.rgb, rgb, "color.rgb === rgb");
+
+ testToString(color, name, hex, hsl, rgb);
+ testColorMatch(name, hex, hsl, rgb, color.rgba, canvas);
+ }
+
+ testSetAlpha();
+}
+
+function testToString(color, name, hex, hsl, rgb) {
+ color.colorUnit = colorUtils.CssColor.COLORUNIT.name;
+ is(color.toString(), name, "toString() with authored type");
+
+ color.colorUnit = colorUtils.CssColor.COLORUNIT.hex;
+ is(color.toString(), hex, "toString() with hex type");
+
+ color.colorUnit = colorUtils.CssColor.COLORUNIT.hsl;
+ is(color.toString(), hsl, "toString() with hsl type");
+
+ color.colorUnit = colorUtils.CssColor.COLORUNIT.rgb;
+ is(color.toString(), rgb, "toString() with rgb type");
+}
+
+function testColorMatch(name, hex, hsl, rgb, rgba, canvas) {
+ let target;
+ let ctx = canvas.getContext("2d");
+
+ let clearCanvas = function () {
+ canvas.width = 1;
+ };
+ let setColor = function (color) {
+ ctx.fillStyle = color;
+ ctx.fillRect(0, 0, 1, 1);
+ };
+ let setTargetColor = function () {
+ clearCanvas();
+ // All colors have rgba so we can use this to compare against.
+ setColor(rgba);
+ let [r, g, b, a] = ctx.getImageData(0, 0, 1, 1).data;
+ target = {r: r, g: g, b: b, a: a};
+ };
+ let test = function (color, type) {
+ // hsla -> rgba -> hsla produces inaccurate results so we
+ // need some tolerence here.
+ let tolerance = 3;
+ clearCanvas();
+
+ setColor(color);
+ let [r, g, b, a] = ctx.getImageData(0, 0, 1, 1).data;
+
+ let rgbFail = Math.abs(r - target.r) > tolerance ||
+ Math.abs(g - target.g) > tolerance ||
+ Math.abs(b - target.b) > tolerance;
+ ok(!rgbFail, "color " + rgba + " matches target. Type: " + type);
+ if (rgbFail) {
+ info(`target: ${target.toSource()}, color: [r: ${r}, g: ${g}, b: ${b}, a: ${a}]`);
+ }
+
+ let alphaFail = a !== target.a;
+ ok(!alphaFail, "color " + rgba + " alpha value matches target.");
+ };
+
+ setTargetColor();
+
+ test(name, "name");
+ test(hex, "hex");
+ test(hsl, "hsl");
+ test(rgb, "rgb");
+}
+
+function testSetAlpha() {
+ let values = [
+ ["longhex", "#ff0000", 0.5, "rgba(255, 0, 0, 0.5)"],
+ ["hex", "#f0f", 0.2, "rgba(255, 0, 255, 0.2)"],
+ ["rgba", "rgba(120, 34, 23, 1)", 0.25, "rgba(120, 34, 23, 0.25)"],
+ ["rgb", "rgb(120, 34, 23)", 0.25, "rgba(120, 34, 23, 0.25)"],
+ ["hsl", "hsl(208, 100%, 97%)", 0.75, "rgba(240, 248, 255, 0.75)"],
+ ["hsla", "hsla(208, 100%, 97%, 1)", 0.75, "rgba(240, 248, 255, 0.75)"],
+ ["alphahex", "#f08f", 0.6, "rgba(255, 0, 136, 0.6)"],
+ ["longalphahex", "#00ff80ff", 0.2, "rgba(0, 255, 128, 0.2)"]
+ ];
+ values.forEach(([type, value, alpha, expected]) => {
+ is(colorUtils.setAlpha(value, alpha), expected,
+ "correctly sets alpha value for " + type);
+ });
+
+ try {
+ colorUtils.setAlpha("rgb(24, 25%, 45, 1)", 1);
+ ok(false, "Should fail when passing in an invalid color.");
+ } catch (e) {
+ ok(true, "Fails when setAlpha receives an invalid color.");
+ }
+
+ is(colorUtils.setAlpha("#fff"), "rgba(255, 255, 255, 1)",
+ "sets alpha to 1 if invalid.");
+}
diff --git a/devtools/client/shared/test/browser_cubic-bezier-01.js b/devtools/client/shared/test/browser_cubic-bezier-01.js
new file mode 100644
index 000000000..4c32590b2
--- /dev/null
+++ b/devtools/client/shared/test/browser_cubic-bezier-01.js
@@ -0,0 +1,38 @@
+/* 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";
+
+// Tests that the CubicBezierWidget generates content in a given parent node
+
+const {CubicBezierWidget} =
+ require("devtools/client/shared/widgets/CubicBezierWidget");
+
+const TEST_URI = `data:text/html,<div id="cubic-bezier-container" />`;
+
+add_task(function* () {
+ let [host,, doc] = yield createHost("bottom", TEST_URI);
+
+ info("Checking that the graph markup is created in the parent");
+ let container = doc.querySelector("#cubic-bezier-container");
+ let w = new CubicBezierWidget(container);
+
+ ok(container.querySelector(".display-wrap"),
+ "The display has been added");
+
+ ok(container.querySelector(".coordinate-plane"),
+ "The coordinate plane has been added");
+ let buttons = container.querySelectorAll("button");
+ is(buttons.length, 2,
+ "The 2 control points have been added");
+ is(buttons[0].className, "control-point");
+ is(buttons[1].className, "control-point");
+ ok(container.querySelector("canvas"), "The curve canvas has been added");
+
+ info("Destroying the widget");
+ w.destroy();
+ is(container.children.length, 0, "All nodes have been removed");
+
+ host.destroy();
+});
diff --git a/devtools/client/shared/test/browser_cubic-bezier-02.js b/devtools/client/shared/test/browser_cubic-bezier-02.js
new file mode 100644
index 000000000..f5e21e4d4
--- /dev/null
+++ b/devtools/client/shared/test/browser_cubic-bezier-02.js
@@ -0,0 +1,200 @@
+/* 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";
+
+// Tests the CubicBezierWidget events
+
+const {CubicBezierWidget} =
+ require("devtools/client/shared/widgets/CubicBezierWidget");
+const {PREDEFINED} = require("devtools/client/shared/widgets/CubicBezierPresets");
+
+// In this test we have to use a slightly more complete HTML tree, with <body>
+// in order to remove its margin and prevent shifted positions
+const TEST_URI = `data:text/html,
+ <html><body>
+ <div id="cubic-bezier-container"/>
+ </body></html>`;
+
+add_task(function* () {
+ let [host, win, doc] = yield createHost("bottom", TEST_URI);
+
+ // Required or widget will be clipped inside of 'bottom'
+ // host by -14. Setting `fixed` zeroes this which is needed for
+ // calculating offsets. Occurs in test env only.
+ doc.body.setAttribute("style", "position: fixed; margin: 0;");
+
+ let container = doc.querySelector("#cubic-bezier-container");
+ let w = new CubicBezierWidget(container, PREDEFINED.linear);
+
+ let rect = w.curve.getBoundingClientRect();
+ rect.graphTop = rect.height * w.bezierCanvas.padding[0];
+ rect.graphBottom = rect.height - rect.graphTop;
+ rect.graphHeight = rect.graphBottom - rect.graphTop;
+
+ yield pointsCanBeDragged(w, win, doc, rect);
+ yield curveCanBeClicked(w, win, doc, rect);
+ yield pointsCanBeMovedWithKeyboard(w, win, doc, rect);
+
+ w.destroy();
+ host.destroy();
+});
+
+function* pointsCanBeDragged(widget, win, doc, offsets) {
+ info("Checking that the control points can be dragged with the mouse");
+
+ info("Listening for the update event");
+ let onUpdated = widget.once("updated");
+
+ info("Generating a mousedown/move/up on P1");
+ widget._onPointMouseDown({target: widget.p1});
+ doc.onmousemove({pageX: offsets.left, pageY: offsets.graphTop});
+ doc.onmouseup();
+
+ let bezier = yield onUpdated;
+ ok(true, "The widget fired the updated event");
+ ok(bezier, "The updated event contains a bezier argument");
+ is(bezier.P1[0], 0, "The new P1 time coordinate is correct");
+ is(bezier.P1[1], 1, "The new P1 progress coordinate is correct");
+
+ info("Listening for the update event");
+ onUpdated = widget.once("updated");
+
+ info("Generating a mousedown/move/up on P2");
+ widget._onPointMouseDown({target: widget.p2});
+ doc.onmousemove({pageX: offsets.right, pageY: offsets.graphBottom});
+ doc.onmouseup();
+
+ bezier = yield onUpdated;
+ is(bezier.P2[0], 1, "The new P2 time coordinate is correct");
+ is(bezier.P2[1], 0, "The new P2 progress coordinate is correct");
+}
+
+function* curveCanBeClicked(widget, win, doc, offsets) {
+ info("Checking that clicking on the curve moves the closest control point");
+
+ info("Listening for the update event");
+ let onUpdated = widget.once("updated");
+
+ info("Click close to P1");
+ let x = offsets.left + (offsets.width / 4.0);
+ let y = offsets.graphTop + (offsets.graphHeight / 4.0);
+ widget._onCurveClick({pageX: x, pageY: y});
+
+ let bezier = yield onUpdated;
+ ok(true, "The widget fired the updated event");
+ is(bezier.P1[0], 0.25, "The new P1 time coordinate is correct");
+ is(bezier.P1[1], 0.75, "The new P1 progress coordinate is correct");
+ is(bezier.P2[0], 1, "P2 time coordinate remained unchanged");
+ is(bezier.P2[1], 0, "P2 progress coordinate remained unchanged");
+
+ info("Listening for the update event");
+ onUpdated = widget.once("updated");
+
+ info("Click close to P2");
+ x = offsets.right - (offsets.width / 4);
+ y = offsets.graphBottom - (offsets.graphHeight / 4);
+ widget._onCurveClick({pageX: x, pageY: y});
+
+ bezier = yield onUpdated;
+ is(bezier.P2[0], 0.75, "The new P2 time coordinate is correct");
+ is(bezier.P2[1], 0.25, "The new P2 progress coordinate is correct");
+ is(bezier.P1[0], 0.25, "P1 time coordinate remained unchanged");
+ is(bezier.P1[1], 0.75, "P1 progress coordinate remained unchanged");
+}
+
+function* pointsCanBeMovedWithKeyboard(widget, win, doc, offsets) {
+ info("Checking that points respond to keyboard events");
+
+ let singleStep = 3;
+ let shiftStep = 30;
+
+ info("Moving P1 to the left");
+ let newOffset = parseInt(widget.p1.style.left, 10) - singleStep;
+ let x = widget.bezierCanvas
+ .offsetsToCoordinates({style: {left: newOffset}})[0];
+
+ let onUpdated = widget.once("updated");
+ widget._onPointKeyDown(getKeyEvent(widget.p1, 37));
+ let bezier = yield onUpdated;
+
+ is(bezier.P1[0], x, "The new P1 time coordinate is correct");
+ is(bezier.P1[1], 0.75, "The new P1 progress coordinate is correct");
+
+ info("Moving P1 to the left, fast");
+ newOffset = parseInt(widget.p1.style.left, 10) - shiftStep;
+ x = widget.bezierCanvas
+ .offsetsToCoordinates({style: {left: newOffset}})[0];
+
+ onUpdated = widget.once("updated");
+ widget._onPointKeyDown(getKeyEvent(widget.p1, 37, true));
+ bezier = yield onUpdated;
+ is(bezier.P1[0], x, "The new P1 time coordinate is correct");
+ is(bezier.P1[1], 0.75, "The new P1 progress coordinate is correct");
+
+ info("Moving P1 to the right, fast");
+ newOffset = parseInt(widget.p1.style.left, 10) + shiftStep;
+ x = widget.bezierCanvas
+ .offsetsToCoordinates({style: {left: newOffset}})[0];
+
+ onUpdated = widget.once("updated");
+ widget._onPointKeyDown(getKeyEvent(widget.p1, 39, true));
+ bezier = yield onUpdated;
+ is(bezier.P1[0], x, "The new P1 time coordinate is correct");
+ is(bezier.P1[1], 0.75, "The new P1 progress coordinate is correct");
+
+ info("Moving P1 to the bottom");
+ newOffset = parseInt(widget.p1.style.top, 10) + singleStep;
+ let y = widget.bezierCanvas
+ .offsetsToCoordinates({style: {top: newOffset}})[1];
+
+ onUpdated = widget.once("updated");
+ widget._onPointKeyDown(getKeyEvent(widget.p1, 40));
+ bezier = yield onUpdated;
+ is(bezier.P1[0], x, "The new P1 time coordinate is correct");
+ is(bezier.P1[1], y, "The new P1 progress coordinate is correct");
+
+ info("Moving P1 to the bottom, fast");
+ newOffset = parseInt(widget.p1.style.top, 10) + shiftStep;
+ y = widget.bezierCanvas
+ .offsetsToCoordinates({style: {top: newOffset}})[1];
+
+ onUpdated = widget.once("updated");
+ widget._onPointKeyDown(getKeyEvent(widget.p1, 40, true));
+ bezier = yield onUpdated;
+ is(bezier.P1[0], x, "The new P1 time coordinate is correct");
+ is(bezier.P1[1], y, "The new P1 progress coordinate is correct");
+
+ info("Moving P1 to the top, fast");
+ newOffset = parseInt(widget.p1.style.top, 10) - shiftStep;
+ y = widget.bezierCanvas
+ .offsetsToCoordinates({style: {top: newOffset}})[1];
+
+ onUpdated = widget.once("updated");
+ widget._onPointKeyDown(getKeyEvent(widget.p1, 38, true));
+ bezier = yield onUpdated;
+ is(bezier.P1[0], x, "The new P1 time coordinate is correct");
+ is(bezier.P1[1], y, "The new P1 progress coordinate is correct");
+
+ info("Checking that keyboard events also work with P2");
+ info("Moving P2 to the left");
+ newOffset = parseInt(widget.p2.style.left, 10) - singleStep;
+ x = widget.bezierCanvas
+ .offsetsToCoordinates({style: {left: newOffset}})[0];
+
+ onUpdated = widget.once("updated");
+ widget._onPointKeyDown(getKeyEvent(widget.p2, 37));
+ bezier = yield onUpdated;
+ is(bezier.P2[0], x, "The new P2 time coordinate is correct");
+ is(bezier.P2[1], 0.25, "The new P2 progress coordinate is correct");
+}
+
+function getKeyEvent(target, keyCode, shift = false) {
+ return {
+ target: target,
+ keyCode: keyCode,
+ shiftKey: shift,
+ preventDefault: () => {}
+ };
+}
diff --git a/devtools/client/shared/test/browser_cubic-bezier-03.js b/devtools/client/shared/test/browser_cubic-bezier-03.js
new file mode 100644
index 000000000..274ed81ef
--- /dev/null
+++ b/devtools/client/shared/test/browser_cubic-bezier-03.js
@@ -0,0 +1,68 @@
+/* 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";
+
+// Tests that coordinates can be changed programatically in the CubicBezierWidget
+
+const {CubicBezierWidget} =
+ require("devtools/client/shared/widgets/CubicBezierWidget");
+const {PREDEFINED} = require("devtools/client/shared/widgets/CubicBezierPresets");
+
+const TEST_URI = `data:text/html,<div id="cubic-bezier-container" />`;
+
+add_task(function* () {
+ let [host,, doc] = yield createHost("bottom", TEST_URI);
+
+ let container = doc.querySelector("#cubic-bezier-container");
+ let w = new CubicBezierWidget(container, PREDEFINED.linear);
+
+ yield coordinatesCanBeChangedByProvidingAnArray(w);
+ yield coordinatesCanBeChangedByProvidingAValue(w);
+
+ w.destroy();
+ host.destroy();
+});
+
+function* coordinatesCanBeChangedByProvidingAnArray(widget) {
+ info("Listening for the update event");
+ let onUpdated = widget.once("updated");
+
+ info("Setting new coordinates");
+ widget.coordinates = [0, 1, 1, 0];
+
+ let bezier = yield onUpdated;
+ ok(true, "The updated event was fired as a result of setting coordinates");
+
+ is(bezier.P1[0], 0, "The new P1 time coordinate is correct");
+ is(bezier.P1[1], 1, "The new P1 progress coordinate is correct");
+ is(bezier.P2[0], 1, "The new P2 time coordinate is correct");
+ is(bezier.P2[1], 0, "The new P2 progress coordinate is correct");
+}
+
+function* coordinatesCanBeChangedByProvidingAValue(widget) {
+ info("Listening for the update event");
+ let onUpdated = widget.once("updated");
+
+ info("Setting linear css value");
+ widget.cssCubicBezierValue = "linear";
+ let bezier = yield onUpdated;
+ ok(true, "The updated event was fired as a result of setting cssValue");
+
+ is(bezier.P1[0], 0, "The new P1 time coordinate is correct");
+ is(bezier.P1[1], 0, "The new P1 progress coordinate is correct");
+ is(bezier.P2[0], 1, "The new P2 time coordinate is correct");
+ is(bezier.P2[1], 1, "The new P2 progress coordinate is correct");
+
+ info("Setting a custom cubic-bezier css value");
+ onUpdated = widget.once("updated");
+ widget.cssCubicBezierValue = "cubic-bezier(.25,-0.5, 1, 1.25)";
+ bezier = yield onUpdated;
+ ok(true, "The updated event was fired as a result of setting cssValue");
+
+ is(bezier.P1[0], .25, "The new P1 time coordinate is correct");
+ is(bezier.P1[1], -.5, "The new P1 progress coordinate is correct");
+ is(bezier.P2[0], 1, "The new P2 time coordinate is correct");
+ is(bezier.P2[1], 1.25, "The new P2 progress coordinate is correct");
+}
diff --git a/devtools/client/shared/test/browser_cubic-bezier-04.js b/devtools/client/shared/test/browser_cubic-bezier-04.js
new file mode 100644
index 000000000..102428035
--- /dev/null
+++ b/devtools/client/shared/test/browser_cubic-bezier-04.js
@@ -0,0 +1,50 @@
+/* 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";
+
+// Tests that the CubicBezierPresetWidget generates markup.
+
+const {CubicBezierPresetWidget} =
+ require("devtools/client/shared/widgets/CubicBezierWidget");
+const {PRESETS} = require("devtools/client/shared/widgets/CubicBezierPresets");
+
+const TEST_URI = `data:text/html,<div id="cubic-bezier-container" />`;
+
+add_task(function* () {
+ let [host,, doc] = yield createHost("bottom", TEST_URI);
+
+ let container = doc.querySelector("#cubic-bezier-container");
+ let w = new CubicBezierPresetWidget(container);
+
+ info("Checking that the presets are created in the parent");
+ ok(container.querySelector(".preset-pane"),
+ "The preset pane has been added");
+
+ ok(container.querySelector("#preset-categories"),
+ "The preset categories have been added");
+ let categories = container.querySelectorAll(".category");
+ is(categories.length, Object.keys(PRESETS).length,
+ "The preset categories have been added");
+ Object.keys(PRESETS).forEach(category => {
+ ok(container.querySelector("#" + category), `${category} has been added`);
+ ok(container.querySelector("#preset-category-" + category),
+ `The preset list for ${category} has been added.`);
+ });
+
+ info("Checking that each of the presets and its preview have been added");
+ Object.keys(PRESETS).forEach(category => {
+ Object.keys(PRESETS[category]).forEach(presetLabel => {
+ let preset = container.querySelector("#" + presetLabel);
+ ok(preset, `${presetLabel} has been added`);
+ ok(preset.querySelector("canvas"),
+ `${presetLabel}'s canvas preview has been added`);
+ ok(preset.querySelector("p"),
+ `${presetLabel}'s label has been added`);
+ });
+ });
+
+ w.destroy();
+ host.destroy();
+});
diff --git a/devtools/client/shared/test/browser_cubic-bezier-05.js b/devtools/client/shared/test/browser_cubic-bezier-05.js
new file mode 100644
index 000000000..b9cdab294
--- /dev/null
+++ b/devtools/client/shared/test/browser_cubic-bezier-05.js
@@ -0,0 +1,48 @@
+/* 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";
+
+// Tests that the CubicBezierPresetWidget cycles menus
+
+const {CubicBezierPresetWidget} =
+ require("devtools/client/shared/widgets/CubicBezierWidget");
+const {PREDEFINED, PRESETS, DEFAULT_PRESET_CATEGORY} =
+ require("devtools/client/shared/widgets/CubicBezierPresets");
+
+const TEST_URI = `data:text/html,<div id="cubic-bezier-container" />`;
+
+add_task(function* () {
+ let [host,, doc] = yield createHost("bottom", TEST_URI);
+
+ let container = doc.querySelector("#cubic-bezier-container");
+ let w = new CubicBezierPresetWidget(container);
+
+ info("Checking that preset is selected if coordinates are known");
+
+ w.refreshMenu([0, 0, 0, 0]);
+ is(w.activeCategory, container.querySelector(`#${DEFAULT_PRESET_CATEGORY}`),
+ "The default category is selected");
+ is(w._activePreset, null, "There is no selected category");
+
+ w.refreshMenu(PREDEFINED.linear);
+ is(w.activeCategory, container.querySelector("#ease-in-out"),
+ "The ease-in-out category is active");
+ is(w._activePreset, container.querySelector("#ease-in-out-linear"),
+ "The ease-in-out-linear preset is active");
+
+ w.refreshMenu(PRESETS["ease-out"]["ease-out-sine"]);
+ is(w.activeCategory, container.querySelector("#ease-out"),
+ "The ease-out category is active");
+ is(w._activePreset, container.querySelector("#ease-out-sine"),
+ "The ease-out-sine preset is active");
+
+ w.refreshMenu([0, 0, 0, 0]);
+ is(w.activeCategory, container.querySelector("#ease-out"),
+ "The ease-out category is still active");
+ is(w._activePreset, null, "No preset is active");
+
+ w.destroy();
+ host.destroy();
+});
diff --git a/devtools/client/shared/test/browser_cubic-bezier-06.js b/devtools/client/shared/test/browser_cubic-bezier-06.js
new file mode 100644
index 000000000..150949929
--- /dev/null
+++ b/devtools/client/shared/test/browser_cubic-bezier-06.js
@@ -0,0 +1,79 @@
+
+/* 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";
+
+// Tests the integration between CubicBezierWidget and CubicBezierPresets
+
+const {CubicBezierWidget} =
+ require("devtools/client/shared/widgets/CubicBezierWidget");
+const {PRESETS} = require("devtools/client/shared/widgets/CubicBezierPresets");
+
+const TEST_URI = `data:text/html,<div id="cubic-bezier-container" />`;
+
+add_task(function* () {
+ let [host, win, doc] = yield createHost("bottom", TEST_URI);
+
+ let container = doc.querySelector("#cubic-bezier-container");
+ let w = new CubicBezierWidget(container,
+ PRESETS["ease-in"]["ease-in-sine"]);
+ w.presets.refreshMenu(PRESETS["ease-in"]["ease-in-sine"]);
+
+ let rect = w.curve.getBoundingClientRect();
+ rect.graphTop = rect.height * w.bezierCanvas.padding[0];
+
+ yield adjustingBezierUpdatesPreset(w, win, doc, rect);
+ yield selectingPresetUpdatesBezier(w, win, doc, rect);
+
+ w.destroy();
+ host.destroy();
+});
+
+function* adjustingBezierUpdatesPreset(widget, win, doc, rect) {
+ info("Checking that changing the bezier refreshes the preset menu");
+
+ is(widget.presets.activeCategory,
+ doc.querySelector("#ease-in"),
+ "The selected category is ease-in");
+
+ is(widget.presets._activePreset,
+ doc.querySelector("#ease-in-sine"),
+ "The selected preset is ease-in-sine");
+
+ info("Generating custom bezier curve by dragging");
+ widget._onPointMouseDown({target: widget.p1});
+ doc.onmousemove({pageX: rect.left, pageY: rect.graphTop});
+ doc.onmouseup();
+
+ is(widget.presets.activeCategory,
+ doc.querySelector("#ease-in"),
+ "The selected category is still ease-in");
+
+ is(widget.presets._activePreset, null,
+ "There is no active preset");
+}
+
+function* selectingPresetUpdatesBezier(widget, win, doc, rect) {
+ info("Checking that selecting a preset updates bezier curve");
+
+ info("Listening for the new coordinates event");
+ let onNewCoordinates = widget.presets.once("new-coordinates");
+ let onUpdated = widget.once("updated");
+
+ info("Click a preset");
+ let preset = doc.querySelector("#ease-in-sine");
+ widget.presets._onPresetClick({currentTarget: preset});
+
+ yield onNewCoordinates;
+ ok(true, "The preset widget fired the new-coordinates event");
+
+ let bezier = yield onUpdated;
+ ok(true, "The bezier canvas fired the updated event");
+
+ is(bezier.P1[0], preset.coordinates[0], "The new P1 time coordinate is correct");
+ is(bezier.P1[1], preset.coordinates[1], "The new P1 progress coordinate is correct");
+ is(bezier.P2[0], preset.coordinates[2], "P2 time coordinate is correct ");
+ is(bezier.P2[1], preset.coordinates[3], "P2 progress coordinate is correct");
+}
diff --git a/devtools/client/shared/test/browser_devices.js b/devtools/client/shared/test/browser_devices.js
new file mode 100644
index 000000000..0bf52fe8e
--- /dev/null
+++ b/devtools/client/shared/test/browser_devices.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {
+ getDevices,
+ getDeviceString,
+ addDevice
+} = require("devtools/client/shared/devices");
+
+add_task(function* () {
+ Services.prefs.setCharPref("devtools.devices.url",
+ TEST_URI_ROOT + "browser_devices.json");
+
+ let devices = yield getDevices();
+
+ is(devices.TYPES.length, 1, "Found 1 device type.");
+
+ let type1 = devices.TYPES[0];
+
+ is(devices[type1].length, 2, "Found 2 devices of type #1.");
+
+ let string = getDeviceString(type1);
+ ok(typeof string === "string" && string.length > 0, "Able to localize type #1.");
+
+ let device1 = {
+ name: "SquarePhone",
+ width: 320,
+ height: 320,
+ pixelRatio: 2,
+ userAgent: "Mozilla/5.0 (Mobile; rv:42.0)",
+ touch: true,
+ firefoxOS: true
+ };
+ addDevice(device1, type1);
+ devices = yield getDevices();
+
+ is(devices[type1].length, 3, "Added new device of type #1.");
+ ok(devices[type1].filter(d => d.name === device1.name), "Found the new device.");
+
+ let type2 = "appliances";
+ let device2 = {
+ name: "Mr Freezer",
+ width: 800,
+ height: 600,
+ pixelRatio: 5,
+ userAgent: "Mozilla/5.0 (Appliance; rv:42.0)",
+ touch: true,
+ firefoxOS: true
+ };
+ addDevice(device2, type2);
+ devices = yield getDevices();
+
+ is(devices.TYPES.length, 2, "Added device type #2.");
+ is(devices[type2].length, 1, "Added new device of type #2.");
+});
diff --git a/devtools/client/shared/test/browser_devices.json b/devtools/client/shared/test/browser_devices.json
new file mode 100644
index 000000000..cc7722a7f
--- /dev/null
+++ b/devtools/client/shared/test/browser_devices.json
@@ -0,0 +1,23 @@
+{
+ "TYPES": [ "phones" ],
+ "phones": [
+ {
+ "name": "Small Phone",
+ "width": 320,
+ "height": 480,
+ "pixelRatio": 1,
+ "userAgent": "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
+ "touch": true,
+ "firefoxOS": true
+ },
+ {
+ "name": "Big Phone",
+ "width": 360,
+ "height": 640,
+ "pixelRatio": 3,
+ "userAgent": "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
+ "touch": true,
+ "firefoxOS": true
+ }
+ ]
+}
diff --git a/devtools/client/shared/test/browser_filter-editor-01.js b/devtools/client/shared/test/browser_filter-editor-01.js
new file mode 100644
index 000000000..1a5beb454
--- /dev/null
+++ b/devtools/client/shared/test/browser_filter-editor-01.js
@@ -0,0 +1,114 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the Filter Editor Widget parses filter values correctly (setCssValue)
+
+const {CSSFilterEditorWidget} = require("devtools/client/shared/widgets/FilterWidget");
+const DOMUtils =
+ Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
+
+const TEST_URI = `data:text/html,<div id="filter-container" />`;
+const {getClientCssProperties} = require("devtools/shared/fronts/css-properties");
+
+// Verify that the given string consists of a valid CSS URL token.
+// Return true on success, false on error.
+function verifyURL(string) {
+ let lexer = DOMUtils.getCSSLexer(string);
+
+ let token = lexer.nextToken();
+ if (!token || token.tokenType !== "url") {
+ return false;
+ }
+
+ return lexer.nextToken() === null;
+}
+
+add_task(function* () {
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+ const cssIsValid = getClientCssProperties().getValidityChecker(doc);
+
+ const container = doc.querySelector("#filter-container");
+ let widget = new CSSFilterEditorWidget(container, "none", cssIsValid);
+
+ info("Test parsing of a valid CSS Filter value");
+ widget.setCssValue("blur(2px) contrast(200%)");
+ is(widget.getCssValue(),
+ "blur(2px) contrast(200%)",
+ "setCssValue should work for computed values");
+
+ info("Test parsing of space-filled value");
+ widget.setCssValue("blur( 2px ) contrast( 2 )");
+ is(widget.getCssValue(),
+ "blur(2px) contrast(200%)",
+ "setCssValue should work for spaced values");
+
+ info("Test parsing of string-typed values");
+ widget.setCssValue("drop-shadow( 2px 1px 5px black) url( example.svg#filter )");
+
+ is(widget.getCssValue(),
+ "drop-shadow(2px 1px 5px black) url(example.svg#filter)",
+ "setCssValue should work for string-typed values");
+
+ info("Test parsing of mixed-case function names");
+ widget.setCssValue("BLUR(2px) Contrast(200%) Drop-Shadow(2px 1px 5px Black)");
+ is(widget.getCssValue(),
+ "BLUR(2px) Contrast(200%) Drop-Shadow(2px 1px 5px Black)",
+ "setCssValue should work for mixed-case function names");
+
+ info("Test parsing of invalid filter value");
+ widget.setCssValue("totallyinvalid");
+ is(widget.getCssValue(), "none",
+ "setCssValue should turn completely invalid value to 'none'");
+
+ info("Test parsing of invalid function argument");
+ widget.setCssValue("blur('hello')");
+ is(widget.getCssValue(), "blur(0px)",
+ "setCssValue should replace invalid function argument with default");
+
+ info("Test parsing of invalid function argument #2");
+ widget.setCssValue("drop-shadow(whatever)");
+ is(widget.getCssValue(), "drop-shadow()",
+ "setCssValue should replace invalid drop-shadow argument with empty string");
+
+ info("Test parsing of mixed invalid argument");
+ widget.setCssValue("contrast(5%) whatever invert('xxx')");
+ is(widget.getCssValue(), "contrast(5%) invert(0%)",
+ "setCssValue should handle multiple errors");
+
+ info("Test parsing of 'unset'");
+ widget.setCssValue("unset");
+ is(widget.getCssValue(), "unset", "setCssValue should handle 'unset'");
+ info("Test parsing of 'initial'");
+ widget.setCssValue("initial");
+ is(widget.getCssValue(), "initial", "setCssValue should handle 'initial'");
+ info("Test parsing of 'inherit'");
+ widget.setCssValue("inherit");
+ is(widget.getCssValue(), "inherit", "setCssValue should handle 'inherit'");
+
+ info("Test parsing of quoted URL");
+ widget.setCssValue("url('invalid ) when ) unquoted')");
+ is(widget.getCssValue(), "url('invalid ) when ) unquoted')",
+ "setCssValue should re-quote single-quoted URL contents");
+ widget.setCssValue("url(\"invalid ) when ) unquoted\")");
+ is(widget.getCssValue(), "url(\"invalid ) when ) unquoted\")",
+ "setCssValue should re-quote double-quoted URL contents");
+ widget.setCssValue("url(ordinary)");
+ is(widget.getCssValue(), "url(ordinary)",
+ "setCssValue should not quote ordinary unquoted URL contents");
+
+ let quotedurl =
+ "url(invalid\\ \\)\\ {\\\twhen\\ }\\ ;\\ \\\\unquoted\\'\\\")";
+ ok(verifyURL(quotedurl), "weird URL is valid");
+ widget.setCssValue(quotedurl);
+ is(widget.getCssValue(), quotedurl,
+ "setCssValue should re-quote weird unquoted URL contents");
+
+ let dataurl = "url(data:image/svg+xml;utf8,<svg\\ " +
+ "xmlns=\\\"http://www.w3.org/2000/svg\\\"><filter\\ id=\\\"blur\\\">" +
+ "<feGaussianBlur\\ stdDeviation=\\\"3\\\"/></filter></svg>#blur)";
+ ok(verifyURL(dataurl), "data URL is valid");
+ widget.setCssValue(dataurl);
+ is(widget.getCssValue(), dataurl, "setCssValue should not mangle data urls");
+});
diff --git a/devtools/client/shared/test/browser_filter-editor-02.js b/devtools/client/shared/test/browser_filter-editor-02.js
new file mode 100644
index 000000000..7c0ec270a
--- /dev/null
+++ b/devtools/client/shared/test/browser_filter-editor-02.js
@@ -0,0 +1,107 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the Filter Editor Widget renders filters correctly
+
+const {CSSFilterEditorWidget} = require("devtools/client/shared/widgets/FilterWidget");
+const {getClientCssProperties} = require("devtools/shared/fronts/css-properties");
+
+const { LocalizationHelper } = require("devtools/shared/l10n");
+const STRINGS_URI = "devtools/client/locales/filterwidget.properties";
+const L10N = new LocalizationHelper(STRINGS_URI);
+
+const TEST_URI = `data:text/html,<div id="filter-container" />`;
+
+add_task(function* () {
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+ const cssIsValid = getClientCssProperties().getValidityChecker(doc);
+
+ const TEST_DATA = [
+ {
+ cssValue: "blur(2px) contrast(200%) hue-rotate(20.2deg) drop-shadow(5px 5px black)",
+ expected: [
+ {
+ label: "blur",
+ value: "2",
+ unit: "px"
+ },
+ {
+ label: "contrast",
+ value: "200",
+ unit: "%"
+ },
+ {
+ label: "hue-rotate",
+ value: "20.2",
+ unit: "deg"
+ },
+ {
+ label: "drop-shadow",
+ value: "5px 5px black",
+ unit: null
+ }
+ ]
+ },
+ {
+ cssValue: "hue-rotate(420.2deg)",
+ expected: [
+ {
+ label: "hue-rotate",
+ value: "420.2",
+ unit: "deg"
+ }
+ ]
+ },
+ {
+ cssValue: "url(example.svg)",
+ expected: [
+ {
+ label: "url",
+ value: "example.svg",
+ unit: null
+ }
+ ]
+ },
+ {
+ cssValue: "none",
+ expected: []
+ }
+ ];
+
+ const container = doc.querySelector("#filter-container");
+ let widget = new CSSFilterEditorWidget(container, "none", cssIsValid);
+
+ info("Test rendering of different types");
+
+ for (let {cssValue, expected} of TEST_DATA) {
+ widget.setCssValue(cssValue);
+
+ if (cssValue === "none") {
+ const text = container.querySelector("#filters").textContent;
+ ok(text.indexOf(L10N.getStr("emptyFilterList")) > -1,
+ "Contains |emptyFilterList| string when given value 'none'");
+ ok(text.indexOf(L10N.getStr("addUsingList")) > -1,
+ "Contains |addUsingList| string when given value 'none'");
+ continue;
+ }
+ const filters = container.querySelectorAll(".filter");
+ testRenderedFilters(filters, expected);
+ }
+});
+
+function testRenderedFilters(filters, expected) {
+ for (let [index, filter] of [...filters].entries()) {
+ let [name, value] = filter.children,
+ label = name.children[1],
+ [input, unit] = value.children;
+
+ const eq = expected[index];
+ is(label.textContent, eq.label, "Label should match");
+ is(input.value, eq.value, "Values should match");
+ if (eq.unit) {
+ is(unit.textContent, eq.unit, "Unit should match");
+ }
+ }
+}
diff --git a/devtools/client/shared/test/browser_filter-editor-03.js b/devtools/client/shared/test/browser_filter-editor-03.js
new file mode 100644
index 000000000..67d36b6b2
--- /dev/null
+++ b/devtools/client/shared/test/browser_filter-editor-03.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests the Filter Editor Widget add, removeAt, updateAt, getValueAt methods
+
+const {CSSFilterEditorWidget} = require("devtools/client/shared/widgets/FilterWidget");
+const {getClientCssProperties} = require("devtools/shared/fronts/css-properties");
+const GRAYSCALE_MAX = 100;
+const INVERT_MIN = 0;
+
+const TEST_URI = `data:text/html,<div id="filter-container" />`;
+
+add_task(function* () {
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+ const cssIsValid = getClientCssProperties().getValidityChecker(doc);
+
+ const container = doc.querySelector("#filter-container");
+ let widget = new CSSFilterEditorWidget(container, "none", cssIsValid);
+
+ info("Test add method");
+ const blur = widget.add("blur", "10.2px");
+ is(widget.getCssValue(), "blur(10.2px)",
+ "Should add filters");
+
+ const url = widget.add("url", "test.svg");
+ is(widget.getCssValue(), "blur(10.2px) url(test.svg)",
+ "Should add filters in order");
+
+ info("Test updateValueAt method");
+ widget.updateValueAt(url, "test2.svg");
+ widget.updateValueAt(blur, 5);
+ is(widget.getCssValue(), "blur(5px) url(test2.svg)",
+ "Should update values correctly");
+
+ info("Test getValueAt method");
+ is(widget.getValueAt(blur), "5px",
+ "Should return value + unit");
+ is(widget.getValueAt(url), "test2.svg",
+ "Should return value for string-type filters");
+
+ info("Test removeAt method");
+ widget.removeAt(url);
+ is(widget.getCssValue(), "blur(5px)",
+ "Should remove the specified filter");
+
+ info("Test add method applying filter range to value");
+ const grayscale = widget.add("grayscale", GRAYSCALE_MAX + 1);
+ is(widget.getValueAt(grayscale), `${GRAYSCALE_MAX}%`,
+ "Shouldn't allow values higher than max");
+
+ const invert = widget.add("invert", INVERT_MIN - 1);
+ is(widget.getValueAt(invert), `${INVERT_MIN}%`,
+ "Shouldn't allow values less than INVERT_MIN");
+
+ info("Test updateValueAt method applying filter range to value");
+ widget.updateValueAt(grayscale, GRAYSCALE_MAX + 1);
+ is(widget.getValueAt(grayscale), `${GRAYSCALE_MAX}%`,
+ "Shouldn't allow values higher than max");
+
+ widget.updateValueAt(invert, INVERT_MIN - 1);
+ is(widget.getValueAt(invert), `${INVERT_MIN}%`,
+ "Shouldn't allow values less than INVERT_MIN");
+});
diff --git a/devtools/client/shared/test/browser_filter-editor-04.js b/devtools/client/shared/test/browser_filter-editor-04.js
new file mode 100644
index 000000000..c1c8f4380
--- /dev/null
+++ b/devtools/client/shared/test/browser_filter-editor-04.js
@@ -0,0 +1,87 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests the Filter Editor Widget's drag-drop re-ordering
+
+const {CSSFilterEditorWidget} = require("devtools/client/shared/widgets/FilterWidget");
+const {getClientCssProperties} = require("devtools/shared/fronts/css-properties");
+const LIST_ITEM_HEIGHT = 32;
+
+const TEST_URI = `data:text/html,<div id="filter-container" />`;
+
+add_task(function* () {
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+ const cssIsValid = getClientCssProperties().getValidityChecker(doc);
+
+ const container = doc.querySelector("#filter-container");
+ const initialValue = "blur(2px) contrast(200%) brightness(200%)";
+ let widget = new CSSFilterEditorWidget(container, initialValue, cssIsValid);
+
+ const filters = widget.el.querySelector("#filters");
+ function first() {
+ return filters.children[0];
+ }
+ function mid() {
+ return filters.children[1];
+ }
+ function last() {
+ return filters.children[2];
+ }
+
+ info("Test re-ordering neighbour filters");
+ widget._mouseDown({
+ target: first().querySelector("i"),
+ pageY: 0
+ });
+ widget._mouseMove({ pageY: LIST_ITEM_HEIGHT });
+
+ // Element re-ordering should be instant
+ is(mid().querySelector("label").textContent, "blur",
+ "Should reorder elements correctly");
+
+ widget._mouseUp();
+
+ is(widget.getCssValue(), "contrast(200%) blur(2px) brightness(200%)",
+ "Should reorder filters objects correctly");
+
+ info("Test re-ordering first and last filters");
+ widget._mouseDown({
+ target: first().querySelector("i"),
+ pageY: 0
+ });
+ widget._mouseMove({ pageY: LIST_ITEM_HEIGHT * 2 });
+
+ // Element re-ordering should be instant
+ is(last().querySelector("label").textContent, "contrast",
+ "Should reorder elements correctly");
+ widget._mouseUp();
+
+ is(widget.getCssValue(), "brightness(200%) blur(2px) contrast(200%)",
+ "Should reorder filters objects correctly");
+
+ info("Test dragging first element out of list");
+ const boundaries = filters.getBoundingClientRect();
+
+ widget._mouseDown({
+ target: first().querySelector("i"),
+ pageY: 0
+ });
+ widget._mouseMove({ pageY: -LIST_ITEM_HEIGHT * 5 });
+ ok(first().getBoundingClientRect().top >= boundaries.top,
+ "First filter should not move outside filter list");
+
+ widget._mouseUp();
+
+ info("Test dragging last element out of list");
+ widget._mouseDown({
+ target: last().querySelector("i"),
+ pageY: 0
+ });
+ widget._mouseMove({ pageY: -LIST_ITEM_HEIGHT * 5 });
+ ok(last().getBoundingClientRect().bottom <= boundaries.bottom,
+ "Last filter should not move outside filter list");
+
+ widget._mouseUp();
+});
diff --git a/devtools/client/shared/test/browser_filter-editor-05.js b/devtools/client/shared/test/browser_filter-editor-05.js
new file mode 100644
index 000000000..a18429542
--- /dev/null
+++ b/devtools/client/shared/test/browser_filter-editor-05.js
@@ -0,0 +1,148 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+requestLongerTimeout(2);
+
+// Tests the Filter Editor Widget's label-dragging
+
+const {CSSFilterEditorWidget} = require("devtools/client/shared/widgets/FilterWidget");
+const {getClientCssProperties} = require("devtools/shared/fronts/css-properties");
+
+const FAST_VALUE_MULTIPLIER = 10;
+const SLOW_VALUE_MULTIPLIER = 0.1;
+const DEFAULT_VALUE_MULTIPLIER = 1;
+
+const GRAYSCALE_MAX = 100,
+ GRAYSCALE_MIN = 0;
+
+const TEST_URI = `data:text/html,<div id="filter-container" />`;
+
+add_task(function* () {
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+ const cssIsValid = getClientCssProperties().getValidityChecker(doc);
+
+ const container = doc.querySelector("#filter-container");
+ let widget = new CSSFilterEditorWidget(
+ container, "grayscale(0%) url(test.svg)", cssIsValid
+ );
+
+ const filters = widget.el.querySelector("#filters");
+ const grayscale = filters.children[0];
+ const url = filters.children[1];
+
+ info("Test label-dragging on number-type filters without modifiers");
+ widget._mouseDown({
+ target: grayscale.querySelector("label"),
+ pageX: 0,
+ altKey: false,
+ shiftKey: false
+ });
+
+ widget._mouseMove({
+ pageX: 12,
+ altKey: false,
+ shiftKey: false
+ });
+ let expected = DEFAULT_VALUE_MULTIPLIER * 12;
+ is(widget.getValueAt(0),
+ `${expected}%`,
+ "Should update value correctly without modifiers");
+
+ info("Test label-dragging on number-type filters with alt");
+ widget._mouseMove({
+ // 20 - 12 = 8
+ pageX: 20,
+ altKey: true,
+ shiftKey: false
+ });
+
+ expected = expected + SLOW_VALUE_MULTIPLIER * 8;
+ is(widget.getValueAt(0),
+ `${expected}%`,
+ "Should update value correctly with alt key");
+
+ info("Test label-dragging on number-type filters with shift");
+ widget._mouseMove({
+ // 25 - 20 = 5
+ pageX: 25,
+ altKey: false,
+ shiftKey: true
+ });
+
+ expected = expected + FAST_VALUE_MULTIPLIER * 5;
+ is(widget.getValueAt(0),
+ `${expected}%`,
+ "Should update value correctly with shift key");
+
+ info("Test releasing mouse and dragging again");
+
+ widget._mouseUp();
+
+ widget._mouseDown({
+ target: grayscale.querySelector("label"),
+ pageX: 0,
+ altKey: false,
+ shiftKey: false
+ });
+
+ widget._mouseMove({
+ pageX: 5,
+ altKey: false,
+ shiftKey: false
+ });
+
+ expected = expected + DEFAULT_VALUE_MULTIPLIER * 5;
+ is(widget.getValueAt(0),
+ `${expected}%`,
+ "Should reset multiplier to default");
+
+ info("Test value ranges");
+
+ widget._mouseMove({
+ // 30 - 25 = 5
+ pageX: 30,
+ altKey: false,
+ shiftKey: true
+ });
+
+ expected = GRAYSCALE_MAX;
+ is(widget.getValueAt(0),
+ `${expected}%`,
+ "Shouldn't allow values higher than max");
+
+ widget._mouseMove({
+ pageX: -11,
+ altKey: false,
+ shiftKey: true
+ });
+
+ expected = GRAYSCALE_MIN;
+ is(widget.getValueAt(0),
+ `${expected}%`,
+ "Shouldn't allow values less than min");
+
+ widget._mouseUp();
+
+ info("Test label-dragging on string-type filters");
+ widget._mouseDown({
+ target: url.querySelector("label"),
+ pageX: 0,
+ altKey: false,
+ shiftKey: false
+ });
+
+ ok(!widget.isDraggingLabel,
+ "Label-dragging should not work for string-type filters");
+
+ widget._mouseMove({
+ pageX: -11,
+ altKey: false,
+ shiftKey: true
+ });
+
+ is(widget.getValueAt(1),
+ "test.svg",
+ "Label-dragging on string-type filters shouldn't affect their value");
+});
diff --git a/devtools/client/shared/test/browser_filter-editor-06.js b/devtools/client/shared/test/browser_filter-editor-06.js
new file mode 100644
index 000000000..1e1a6c914
--- /dev/null
+++ b/devtools/client/shared/test/browser_filter-editor-06.js
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests the Filter Editor Widget's add button
+
+const {CSSFilterEditorWidget} = require("devtools/client/shared/widgets/FilterWidget");
+const {getClientCssProperties} = require("devtools/shared/fronts/css-properties");
+
+const { LocalizationHelper } = require("devtools/shared/l10n");
+const STRINGS_URI = "devtools/client/locales/filterwidget.properties";
+const L10N = new LocalizationHelper(STRINGS_URI);
+
+const TEST_URI = `data:text/html,<div id="filter-container" />`;
+
+add_task(function* () {
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+ const cssIsValid = getClientCssProperties().getValidityChecker(doc);
+
+ const container = doc.querySelector("#filter-container");
+ let widget = new CSSFilterEditorWidget(container, "none", cssIsValid);
+
+ const select = widget.el.querySelector("select"),
+ add = widget.el.querySelector("#add-filter");
+
+ const TEST_DATA = [
+ {
+ name: "blur",
+ unit: "px",
+ type: "length"
+ },
+ {
+ name: "contrast",
+ unit: "%",
+ type: "percentage"
+ },
+ {
+ name: "hue-rotate",
+ unit: "deg",
+ type: "angle"
+ },
+ {
+ name: "drop-shadow",
+ placeholder: L10N.getStr("dropShadowPlaceholder"),
+ type: "string"
+ },
+ {
+ name: "url",
+ placeholder: "example.svg#c1",
+ type: "string"
+ }
+ ];
+
+ info("Test adding new filters with different units");
+
+ for (let [index, filter] of TEST_DATA.entries()) {
+ select.value = filter.name;
+ add.click();
+
+ if (filter.unit) {
+ is(widget.getValueAt(index), `0${filter.unit}`,
+ `Should add ${filter.unit} to ${filter.type} filters`);
+ } else if (filter.placeholder) {
+ let i = index + 1;
+ const input = widget.el.querySelector(`.filter:nth-child(${i}) input`);
+ is(input.placeholder, filter.placeholder,
+ "Should set the appropriate placeholder for string-type filters");
+ }
+ }
+});
diff --git a/devtools/client/shared/test/browser_filter-editor-07.js b/devtools/client/shared/test/browser_filter-editor-07.js
new file mode 100644
index 000000000..af2975d5c
--- /dev/null
+++ b/devtools/client/shared/test/browser_filter-editor-07.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests the Filter Editor Widget's remove button
+
+const {CSSFilterEditorWidget} = require("devtools/client/shared/widgets/FilterWidget");
+const {getClientCssProperties} = require("devtools/shared/fronts/css-properties");
+
+const TEST_URI = `data:text/html,<div id="filter-container" />`;
+
+add_task(function* () {
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+ const cssIsValid = getClientCssProperties().getValidityChecker(doc);
+
+ const container = doc.querySelector("#filter-container");
+ let widget = new CSSFilterEditorWidget(
+ container, "blur(2px) contrast(200%)", cssIsValid
+ );
+
+ info("Test removing filters with remove button");
+ widget.el.querySelector(".filter button").click();
+
+ is(widget.getCssValue(), "contrast(200%)",
+ "Should remove the clicked filter");
+});
diff --git a/devtools/client/shared/test/browser_filter-editor-08.js b/devtools/client/shared/test/browser_filter-editor-08.js
new file mode 100644
index 000000000..c30dbf299
--- /dev/null
+++ b/devtools/client/shared/test/browser_filter-editor-08.js
@@ -0,0 +1,84 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests the Filter Editor Widget inputs increase/decrease value using
+// arrow keys, applying multiplier using alt/shift on number-type filters
+
+const {CSSFilterEditorWidget} = require("devtools/client/shared/widgets/FilterWidget");
+const {getClientCssProperties} = require("devtools/shared/fronts/css-properties");
+
+const FAST_VALUE_MULTIPLIER = 10;
+const SLOW_VALUE_MULTIPLIER = 0.1;
+const DEFAULT_VALUE_MULTIPLIER = 1;
+
+const TEST_URI = `data:text/html,<div id="filter-container" />`;
+
+add_task(function* () {
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+ const cssIsValid = getClientCssProperties().getValidityChecker(doc);
+
+ const container = doc.querySelector("#filter-container");
+ const initialValue = "blur(2px)";
+ let widget = new CSSFilterEditorWidget(container, initialValue, cssIsValid);
+
+ let value = 2;
+
+ triggerKey = triggerKey.bind(widget);
+
+ info("Test simple arrow keys");
+ triggerKey(40);
+
+ value -= DEFAULT_VALUE_MULTIPLIER;
+ is(widget.getValueAt(0), `${value}px`,
+ "Should decrease value using down arrow");
+
+ triggerKey(38);
+
+ value += DEFAULT_VALUE_MULTIPLIER;
+ is(widget.getValueAt(0), `${value}px`,
+ "Should decrease value using down arrow");
+
+ info("Test shift key multiplier");
+ triggerKey(38, "shiftKey");
+
+ value += FAST_VALUE_MULTIPLIER;
+ is(widget.getValueAt(0), `${value}px`,
+ "Should increase value by fast multiplier using up arrow");
+
+ triggerKey(40, "shiftKey");
+
+ value -= FAST_VALUE_MULTIPLIER;
+ is(widget.getValueAt(0), `${value}px`,
+ "Should decrease value by fast multiplier using down arrow");
+
+ info("Test alt key multiplier");
+ triggerKey(38, "altKey");
+
+ value += SLOW_VALUE_MULTIPLIER;
+ is(widget.getValueAt(0), `${value}px`,
+ "Should increase value by slow multiplier using up arrow");
+
+ triggerKey(40, "altKey");
+
+ value -= SLOW_VALUE_MULTIPLIER;
+ is(widget.getValueAt(0), `${value}px`,
+ "Should decrease value by slow multiplier using down arrow");
+
+ triggerKey = null;
+});
+
+// Triggers the specified keyCode and modifier key on
+// first filter's input
+function triggerKey(key, modifier) {
+ const filter = this.el.querySelector("#filters").children[0];
+ const input = filter.querySelector("input");
+
+ this._keyDown({
+ target: input,
+ keyCode: key,
+ [modifier]: true,
+ preventDefault: function () {}
+ });
+}
diff --git a/devtools/client/shared/test/browser_filter-editor-09.js b/devtools/client/shared/test/browser_filter-editor-09.js
new file mode 100644
index 000000000..1a358425e
--- /dev/null
+++ b/devtools/client/shared/test/browser_filter-editor-09.js
@@ -0,0 +1,125 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests the Filter Editor Widget inputs increase/decrease value when cursor is
+// on a number using arrow keys, applying multiplier using alt/shift on strings
+
+const {CSSFilterEditorWidget} = require("devtools/client/shared/widgets/FilterWidget");
+const {getClientCssProperties} = require("devtools/shared/fronts/css-properties");
+
+const FAST_VALUE_MULTIPLIER = 10;
+const SLOW_VALUE_MULTIPLIER = 0.1;
+const DEFAULT_VALUE_MULTIPLIER = 1;
+
+const TEST_URI = `data:text/html,<div id="filter-container" />`;
+
+add_task(function* () {
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+ const cssIsValid = getClientCssProperties().getValidityChecker(doc);
+
+ const container = doc.querySelector("#filter-container");
+ const initialValue = "drop-shadow(rgb(0, 0, 0) 1px 1px 0px)";
+ let widget = new CSSFilterEditorWidget(container, initialValue, cssIsValid);
+ widget.el.querySelector("#filters input").setSelectionRange(13, 13);
+
+ let value = 1;
+
+ triggerKey = triggerKey.bind(widget);
+
+ info("Test simple arrow keys");
+ triggerKey(40);
+
+ value -= DEFAULT_VALUE_MULTIPLIER;
+ is(widget.getValueAt(0), val(value),
+ "Should decrease value using down arrow");
+
+ triggerKey(38);
+
+ value += DEFAULT_VALUE_MULTIPLIER;
+ is(widget.getValueAt(0), val(value),
+ "Should decrease value using down arrow");
+
+ info("Test shift key multiplier");
+ triggerKey(38, "shiftKey");
+
+ value += FAST_VALUE_MULTIPLIER;
+ is(widget.getValueAt(0), val(value),
+ "Should increase value by fast multiplier using up arrow");
+
+ triggerKey(40, "shiftKey");
+
+ value -= FAST_VALUE_MULTIPLIER;
+ is(widget.getValueAt(0), val(value),
+ "Should decrease value by fast multiplier using down arrow");
+
+ info("Test alt key multiplier");
+ triggerKey(38, "altKey");
+
+ value += SLOW_VALUE_MULTIPLIER;
+ is(widget.getValueAt(0), val(value),
+ "Should increase value by slow multiplier using up arrow");
+
+ triggerKey(40, "altKey");
+
+ value -= SLOW_VALUE_MULTIPLIER;
+ is(widget.getValueAt(0), val(value),
+ "Should decrease value by slow multiplier using down arrow");
+
+ triggerKey(40, "shiftKey");
+
+ value -= FAST_VALUE_MULTIPLIER;
+ is(widget.getValueAt(0), val(value),
+ "Should decrease to negative");
+
+ triggerKey(40);
+
+ value -= DEFAULT_VALUE_MULTIPLIER;
+ is(widget.getValueAt(0), val(value),
+ "Should decrease negative numbers correctly");
+
+ triggerKey(38);
+
+ value += DEFAULT_VALUE_MULTIPLIER;
+ is(widget.getValueAt(0), val(value),
+ "Should increase negative values correctly");
+
+ triggerKey(40, "altKey");
+ triggerKey(40, "altKey");
+
+ value -= SLOW_VALUE_MULTIPLIER * 2;
+ is(widget.getValueAt(0), val(value),
+ "Should decrease float numbers correctly");
+
+ triggerKey(38, "altKey");
+
+ value += SLOW_VALUE_MULTIPLIER;
+ is(widget.getValueAt(0), val(value),
+ "Should increase float numbers correctly");
+
+ triggerKey = null;
+});
+
+// Triggers the specified keyCode and modifier key on
+// first filter's input
+function triggerKey(key, modifier) {
+ const filter = this.el.querySelector("#filters").children[0];
+ const input = filter.querySelector("input");
+
+ this._keyDown({
+ target: input,
+ keyCode: key,
+ [modifier]: true,
+ preventDefault: function () {}
+ });
+}
+
+function val(value) {
+ let v = value.toFixed(1);
+
+ if (v.indexOf(".0") > -1) {
+ v = v.slice(0, -2);
+ }
+ return `rgb(0, 0, 0) ${v}px 1px 0px`;
+}
diff --git a/devtools/client/shared/test/browser_filter-editor-10.js b/devtools/client/shared/test/browser_filter-editor-10.js
new file mode 100644
index 000000000..b73c53a83
--- /dev/null
+++ b/devtools/client/shared/test/browser_filter-editor-10.js
@@ -0,0 +1,87 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests the Filter Editor Widget inputs increase/decrease value when cursor is
+// on a number using arrow keys if cursor is behind/mid/after the number strings
+
+const {CSSFilterEditorWidget} = require("devtools/client/shared/widgets/FilterWidget");
+const {getClientCssProperties} = require("devtools/shared/fronts/css-properties");
+
+const DEFAULT_VALUE_MULTIPLIER = 1;
+
+const TEST_URI = `data:text/html,<div id="filter-container" />`;
+
+add_task(function* () {
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+ const cssIsValid = getClientCssProperties().getValidityChecker(doc);
+
+ const container = doc.querySelector("#filter-container");
+ const initialValue = "drop-shadow(rgb(0, 0, 0) 10px 1px 0px)";
+ let widget = new CSSFilterEditorWidget(container, initialValue, cssIsValid);
+ const input = widget.el.querySelector("#filters input");
+
+ let value = 10;
+
+ triggerKey = triggerKey.bind(widget);
+
+ info("Test increment/decrement of string-type numbers without selection");
+
+ input.setSelectionRange(14, 14);
+ triggerKey(40);
+
+ value -= DEFAULT_VALUE_MULTIPLIER;
+ is(widget.getValueAt(0), val(value),
+ "Should work with cursor in the middle of number");
+
+ input.setSelectionRange(13, 13);
+ triggerKey(38);
+
+ value += DEFAULT_VALUE_MULTIPLIER;
+ is(widget.getValueAt(0), val(value),
+ "Should work with cursor before the number");
+
+ input.setSelectionRange(15, 15);
+ triggerKey(40);
+
+ value -= DEFAULT_VALUE_MULTIPLIER;
+ is(widget.getValueAt(0), val(value),
+ "Should work with cursor after the number");
+
+ info("Test increment/decrement of string-type numbers with a selection");
+
+ input.setSelectionRange(13, 15);
+ triggerKey(38);
+ input.setSelectionRange(13, 18);
+ triggerKey(38);
+
+ value += DEFAULT_VALUE_MULTIPLIER * 2;
+ is(widget.getValueAt(0), val(value),
+ "Should work if a there is a selection, starting with the number");
+
+ triggerKey = null;
+});
+
+// Triggers the specified keyCode and modifier key on
+// first filter's input
+function triggerKey(key, modifier) {
+ const filter = this.el.querySelector("#filters").children[0];
+ const input = filter.querySelector("input");
+
+ this._keyDown({
+ target: input,
+ keyCode: key,
+ [modifier]: true,
+ preventDefault: function () {}
+ });
+}
+
+function val(value) {
+ let v = value.toFixed(1);
+
+ if (v.indexOf(".0") > -1) {
+ v = v.slice(0, -2);
+ }
+ return `rgb(0, 0, 0) ${v}px 1px 0px`;
+}
diff --git a/devtools/client/shared/test/browser_filter-presets-01.js b/devtools/client/shared/test/browser_filter-presets-01.js
new file mode 100644
index 000000000..859f5f63e
--- /dev/null
+++ b/devtools/client/shared/test/browser_filter-presets-01.js
@@ -0,0 +1,99 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests saving presets
+
+const {CSSFilterEditorWidget} = require("devtools/client/shared/widgets/FilterWidget");
+const {getClientCssProperties} = require("devtools/shared/fronts/css-properties");
+
+const TEST_URI = `data:text/html,<div id="filter-container" />`;
+
+add_task(function* () {
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+ const cssIsValid = getClientCssProperties().getValidityChecker(doc);
+
+ const container = doc.querySelector("#filter-container");
+ let widget = new CSSFilterEditorWidget(container, "none", cssIsValid);
+ // First render
+ yield widget.once("render");
+
+ const VALUE = "blur(2px) contrast(150%)";
+ const NAME = "Test";
+
+ yield showFilterPopupPresetsAndCreatePreset(widget, NAME, VALUE);
+
+ let preset = widget.el.querySelector(".preset");
+ is(preset.querySelector("label").textContent, NAME,
+ "Should show preset name correctly");
+ is(preset.querySelector("span").textContent, VALUE,
+ "Should show preset value preview correctly");
+
+ let list = yield widget.getPresets();
+ let input = widget.el.querySelector(".presets-list .footer input");
+ let data = list[0];
+
+ is(data.name, NAME,
+ "Should add the preset to asyncStorage - name property");
+ is(data.value, VALUE,
+ "Should add the preset to asyncStorage - name property");
+
+ info("Test overriding preset by using the same name");
+
+ const VALUE_2 = "saturate(50%) brightness(10%)";
+
+ widget.setCssValue(VALUE_2);
+
+ yield savePreset(widget);
+
+ is(widget.el.querySelectorAll(".preset").length, 1,
+ "Should override the preset with the same name - render");
+
+ list = yield widget.getPresets();
+ data = list[0];
+
+ is(list.length, 1,
+ "Should override the preset with the same name - asyncStorage");
+
+ is(data.name, NAME,
+ "Should override the preset with the same name - prop name");
+ is(data.value, VALUE_2,
+ "Should override the preset with the same name - prop value");
+
+ yield widget.setPresets([]);
+
+ info("Test saving a preset without name");
+ input.value = "";
+
+ yield savePreset(widget, "preset-save-error");
+
+ list = yield widget.getPresets();
+ is(list.length, 0,
+ "Should not add a preset without name");
+
+ info("Test saving a preset without filters");
+
+ input.value = NAME;
+ widget.setCssValue("none");
+
+ yield savePreset(widget, "preset-save-error");
+
+ list = yield widget.getPresets();
+ is(list.length, 0,
+ "Should not add a preset without filters (value: none)");
+});
+
+/**
+ * Call savePreset on widget and wait for the specified event to emit
+ * @param {CSSFilterWidget} widget
+ * @param {string} expectEvent="render" The event to listen on
+ * @return {Promise}
+ */
+function savePreset(widget, expectEvent = "render") {
+ let onEvent = widget.once(expectEvent);
+ widget._savePreset({
+ preventDefault: () => {},
+ });
+ return onEvent;
+}
diff --git a/devtools/client/shared/test/browser_filter-presets-02.js b/devtools/client/shared/test/browser_filter-presets-02.js
new file mode 100644
index 000000000..5e700ea94
--- /dev/null
+++ b/devtools/client/shared/test/browser_filter-presets-02.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests loading presets
+
+const {CSSFilterEditorWidget} = require("devtools/client/shared/widgets/FilterWidget");
+const {getClientCssProperties} = require("devtools/shared/fronts/css-properties");
+
+const TEST_URI = `data:text/html,<div id="filter-container" />`;
+
+add_task(function* () {
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+ const cssIsValid = getClientCssProperties().getValidityChecker(doc);
+
+ const container = doc.querySelector("#filter-container");
+ let widget = new CSSFilterEditorWidget(container, "none", cssIsValid);
+ // First render
+ yield widget.once("render");
+
+ const VALUE = "blur(2px) contrast(150%)";
+ const NAME = "Test";
+
+ yield showFilterPopupPresetsAndCreatePreset(widget, NAME, VALUE);
+
+ let onRender = widget.once("render");
+ // reset value
+ widget.setCssValue("saturate(100%) brightness(150%)");
+ yield onRender;
+
+ let preset = widget.el.querySelector(".preset");
+
+ onRender = widget.once("render");
+ widget._presetClick({
+ target: preset
+ });
+
+ yield onRender;
+
+ is(widget.getCssValue(), VALUE,
+ "Should set widget's value correctly");
+ is(widget.el.querySelector(".presets-list .footer input").value, NAME,
+ "Should set input's value to name");
+});
diff --git a/devtools/client/shared/test/browser_filter-presets-03.js b/devtools/client/shared/test/browser_filter-presets-03.js
new file mode 100644
index 000000000..a61bf35db
--- /dev/null
+++ b/devtools/client/shared/test/browser_filter-presets-03.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests deleting presets
+
+const {CSSFilterEditorWidget} = require("devtools/client/shared/widgets/FilterWidget");
+const {getClientCssProperties} = require("devtools/shared/fronts/css-properties");
+
+const TEST_URI = `data:text/html,<div id="filter-container" />`;
+
+add_task(function* () {
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+ const cssIsValid = getClientCssProperties().getValidityChecker(doc);
+
+ const container = doc.querySelector("#filter-container");
+ let widget = new CSSFilterEditorWidget(container, "none", cssIsValid);
+ // First render
+ yield widget.once("render");
+
+ const NAME = "Test";
+ const VALUE = "blur(2px) contrast(150%)";
+
+ yield showFilterPopupPresetsAndCreatePreset(widget, NAME, VALUE);
+
+ let removeButton = widget.el.querySelector(".preset .remove-button");
+ let onRender = widget.once("render");
+ widget._presetClick({
+ target: removeButton
+ });
+
+ yield onRender;
+ is(widget.el.querySelector(".preset"), null,
+ "Should re-render after removing preset");
+
+ let list = yield widget.getPresets();
+ is(list.length, 0,
+ "Should remove presets from asyncStorage");
+});
diff --git a/devtools/client/shared/test/browser_flame-graph-01.js b/devtools/client/shared/test/browser_flame-graph-01.js
new file mode 100644
index 000000000..a32fb9fd3
--- /dev/null
+++ b/devtools/client/shared/test/browser_flame-graph-01.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that flame graph widget works properly.
+
+const {FlameGraph} = require("devtools/client/shared/widgets/FlameGraph");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ doc.body.setAttribute("style",
+ "position: fixed; width: 100%; height: 100%; margin: 0;");
+
+ let graph = new FlameGraph(doc.body);
+
+ let readyEventEmitted;
+ graph.once("ready", () => {
+ readyEventEmitted = true;
+ });
+
+ yield graph.ready();
+ ok(readyEventEmitted, "The 'ready' event should have been emitted");
+
+ testGraph(host, graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function testGraph(host, graph) {
+ ok(graph._container.classList.contains("flame-graph-widget-container"),
+ "The correct graph container was created.");
+ ok(graph._canvas.classList.contains("flame-graph-widget-canvas"),
+ "The correct graph container was created.");
+
+ let bounds = host.frame.getBoundingClientRect();
+
+ is(graph.width, bounds.width * window.devicePixelRatio,
+ "The graph has the correct width.");
+ is(graph.height, bounds.height * window.devicePixelRatio,
+ "The graph has the correct height.");
+
+ ok(graph._selection.start === null,
+ "The graph's selection start value is initially null.");
+ ok(graph._selection.end === null,
+ "The graph's selection end value is initially null.");
+
+ ok(graph._selectionDragger.origin === null,
+ "The graph's dragger origin value is initially null.");
+ ok(graph._selectionDragger.anchor.start === null,
+ "The graph's dragger anchor start value is initially null.");
+ ok(graph._selectionDragger.anchor.end === null,
+ "The graph's dragger anchor end value is initially null.");
+}
diff --git a/devtools/client/shared/test/browser_flame-graph-02.js b/devtools/client/shared/test/browser_flame-graph-02.js
new file mode 100644
index 000000000..e15c3efe0
--- /dev/null
+++ b/devtools/client/shared/test/browser_flame-graph-02.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that flame graph widgets may have a fixed width or height.
+
+const {FlameGraph} = require("devtools/client/shared/widgets/FlameGraph");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ doc.body.setAttribute("style",
+ "position: fixed; width: 100%; height: 100%; margin: 0;");
+
+ let graph = new FlameGraph(doc.body);
+ graph.fixedWidth = 200;
+ graph.fixedHeight = 100;
+
+ yield graph.ready();
+ testGraph(host, graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function testGraph(host, graph) {
+ let bounds = host.frame.getBoundingClientRect();
+
+ isnot(graph.width, bounds.width * window.devicePixelRatio,
+ "The graph should not span all the parent node's width.");
+ isnot(graph.height, bounds.height * window.devicePixelRatio,
+ "The graph should not span all the parent node's height.");
+
+ is(graph.width, graph.fixedWidth * window.devicePixelRatio,
+ "The graph has the correct width.");
+ is(graph.height, graph.fixedHeight * window.devicePixelRatio,
+ "The graph has the correct height.");
+}
diff --git a/devtools/client/shared/test/browser_flame-graph-03a.js b/devtools/client/shared/test/browser_flame-graph-03a.js
new file mode 100644
index 000000000..10fcf9457
--- /dev/null
+++ b/devtools/client/shared/test/browser_flame-graph-03a.js
@@ -0,0 +1,138 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that selections in the flame graph widget work properly.
+
+const TEST_DATA = [
+ {
+ color: "#f00",
+ blocks: [
+ { x: 0, y: 0, width: 50, height: 20, text: "FOO" },
+ { x: 50, y: 0, width: 100, height: 20, text: "BAR" }
+ ]
+ },
+ {
+ color: "#00f",
+ blocks: [
+ { x: 0, y: 30, width: 30, height: 20, text: "BAZ" }
+ ]
+ }
+];
+const TEST_BOUNDS = { startTime: 0, endTime: 150 };
+const TEST_WIDTH = 200;
+const TEST_HEIGHT = 100;
+
+const {FlameGraph} = require("devtools/client/shared/widgets/FlameGraph");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ doc.body.setAttribute("style",
+ "position: fixed; width: 100%; height: 100%; margin: 0;");
+
+ let graph = new FlameGraph(doc.body, 1);
+ graph.fixedWidth = TEST_WIDTH;
+ graph.fixedHeight = TEST_HEIGHT;
+ graph.horizontalPanThreshold = 0;
+ graph.verticalPanThreshold = 0;
+
+ yield graph.ready();
+
+ testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function testGraph(graph) {
+ graph.setData({ data: TEST_DATA, bounds: TEST_BOUNDS });
+
+ is(graph.getViewRange().startTime, 0,
+ "The selection start boundary is correct (1).");
+ is(graph.getViewRange().endTime, 150,
+ "The selection end boundary is correct (1).");
+
+ scroll(graph, 200, HORIZONTAL_AXIS, 10);
+ is(graph.getViewRange().startTime | 0, 75,
+ "The selection start boundary is correct (2).");
+ is(graph.getViewRange().endTime | 0, 150,
+ "The selection end boundary is correct (2).");
+
+ scroll(graph, -200, HORIZONTAL_AXIS, 10);
+ is(graph.getViewRange().startTime | 0, 37,
+ "The selection start boundary is correct (3).");
+ is(graph.getViewRange().endTime | 0, 112,
+ "The selection end boundary is correct (3).");
+
+ scroll(graph, 200, VERTICAL_AXIS, TEST_WIDTH / 2);
+ is(graph.getViewRange().startTime | 0, 34,
+ "The selection start boundary is correct (4).");
+ is(graph.getViewRange().endTime | 0, 115,
+ "The selection end boundary is correct (4).");
+
+ scroll(graph, -200, VERTICAL_AXIS, TEST_WIDTH / 2);
+ is(graph.getViewRange().startTime | 0, 37,
+ "The selection start boundary is correct (5).");
+ is(graph.getViewRange().endTime | 0, 112,
+ "The selection end boundary is correct (5).");
+
+ dragStart(graph, TEST_WIDTH / 2);
+ is(graph.getViewRange().startTime | 0, 37,
+ "The selection start boundary is correct (6).");
+ is(graph.getViewRange().endTime | 0, 112,
+ "The selection end boundary is correct (6).");
+
+ hover(graph, TEST_WIDTH / 2 - 10);
+ is(graph.getViewRange().startTime | 0, 41,
+ "The selection start boundary is correct (7).");
+ is(graph.getViewRange().endTime | 0, 116,
+ "The selection end boundary is correct (7).");
+
+ dragStop(graph, 10);
+ is(graph.getViewRange().startTime | 0, 71,
+ "The selection start boundary is correct (8).");
+ is(graph.getViewRange().endTime | 0, 145,
+ "The selection end boundary is correct (8).");
+}
+
+// EventUtils just doesn't work!
+
+function hover(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+}
+
+function dragStart(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseDown({ testX: x, testY: y });
+}
+
+function dragStop(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseUp({ testX: x, testY: y });
+}
+
+var HORIZONTAL_AXIS = 1;
+var VERTICAL_AXIS = 2;
+
+function scroll(graph, wheel, axis, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseWheel({ testX: x, testY: y, axis, detail: wheel,
+ HORIZONTAL_AXIS,
+ VERTICAL_AXIS
+ });
+}
diff --git a/devtools/client/shared/test/browser_flame-graph-03b.js b/devtools/client/shared/test/browser_flame-graph-03b.js
new file mode 100644
index 000000000..936eb831e
--- /dev/null
+++ b/devtools/client/shared/test/browser_flame-graph-03b.js
@@ -0,0 +1,92 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that selections in the flame graph widget work properly on HiDPI.
+
+const TEST_DATA = [
+ {
+ color: "#f00",
+ blocks: [
+ { x: 0, y: 0, width: 50, height: 20, text: "FOO" },
+ { x: 50, y: 0, width: 100, height: 20, text: "BAR" }
+ ]
+ },
+ {
+ color: "#00f",
+ blocks: [
+ { x: 0, y: 30, width: 30, height: 20, text: "BAZ" }
+ ]
+ }
+];
+const TEST_BOUNDS = { startTime: 0, endTime: 150 };
+const TEST_WIDTH = 200;
+const TEST_HEIGHT = 100;
+const TEST_DPI_DENSITIY = 2;
+
+var {FlameGraph} = require("devtools/client/shared/widgets/FlameGraph");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ doc.body.setAttribute("style",
+ "position: fixed; width: 100%; height: 100%; margin: 0;");
+
+ let graph = new FlameGraph(doc.body, TEST_DPI_DENSITIY);
+ graph.fixedWidth = TEST_WIDTH;
+ graph.fixedHeight = TEST_HEIGHT;
+
+ yield graph.ready();
+
+ testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function testGraph(graph) {
+ graph.setData({ data: TEST_DATA, bounds: TEST_BOUNDS });
+
+ is(graph.getViewRange().startTime, 0,
+ "The selection start boundary is correct on HiDPI (1).");
+ is(graph.getViewRange().endTime, 150,
+ "The selection end boundary is correct on HiDPI (1).");
+
+ is(graph.getOuterBounds().startTime, 0,
+ "The bounds start boundary is correct on HiDPI (1).");
+ is(graph.getOuterBounds().endTime, 150,
+ "The bounds end boundary is correct on HiDPI (1).");
+
+ scroll(graph, 10000, HORIZONTAL_AXIS, 1);
+
+ is(Math.round(graph.getViewRange().startTime), 150,
+ "The selection start boundary is correct on HiDPI (2).");
+ is(Math.round(graph.getViewRange().endTime), 150,
+ "The selection end boundary is correct on HiDPI (2).");
+
+ is(graph.getOuterBounds().startTime, 0,
+ "The bounds start boundary is correct on HiDPI (2).");
+ is(graph.getOuterBounds().endTime, 150,
+ "The bounds end boundary is correct on HiDPI (2).");
+}
+
+// EventUtils just doesn't work!
+
+var HORIZONTAL_AXIS = 1;
+var VERTICAL_AXIS = 2;
+
+function scroll(graph, wheel, axis, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseWheel({ testX: x, testY: y, axis, detail: wheel,
+ HORIZONTAL_AXIS,
+ VERTICAL_AXIS
+ });
+}
diff --git a/devtools/client/shared/test/browser_flame-graph-03c.js b/devtools/client/shared/test/browser_flame-graph-03c.js
new file mode 100644
index 000000000..3a6bf80ae
--- /dev/null
+++ b/devtools/client/shared/test/browser_flame-graph-03c.js
@@ -0,0 +1,155 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that vertical panning in the flame graph widget works properly.
+
+const TEST_DATA = [
+ {
+ color: "#f00",
+ blocks: [
+ { x: 0, y: 0, width: 50, height: 20, text: "FOO" },
+ { x: 50, y: 0, width: 100, height: 20, text: "BAR" }
+ ]
+ },
+ {
+ color: "#00f",
+ blocks: [
+ { x: 0, y: 30, width: 30, height: 20, text: "BAZ" }
+ ]
+ }
+];
+const TEST_BOUNDS = { startTime: 0, endTime: 150 };
+const TEST_WIDTH = 200;
+const TEST_HEIGHT = 100;
+const TEST_DPI_DENSITIY = 2;
+
+const {FlameGraph} = require("devtools/client/shared/widgets/FlameGraph");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ doc.body.setAttribute("style",
+ "position: fixed; width: 100%; height: 100%; margin: 0;");
+
+ let graph = new FlameGraph(doc.body, TEST_DPI_DENSITIY);
+ graph.fixedWidth = TEST_WIDTH;
+ graph.fixedHeight = TEST_HEIGHT;
+
+ yield graph.ready();
+
+ testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function testGraph(graph) {
+ graph.setData({ data: TEST_DATA, bounds: TEST_BOUNDS });
+
+ // Drag up vertically only.
+
+ dragStart(graph, TEST_WIDTH / 2, TEST_HEIGHT / 2);
+ is(graph.getViewRange().startTime | 0, 0,
+ "The selection start boundary is correct (1).");
+ is(graph.getViewRange().endTime | 0, 150,
+ "The selection end boundary is correct (1).");
+ is(graph.getViewRange().verticalOffset | 0, 0,
+ "The vertical offset is correct (1).");
+
+ hover(graph, TEST_WIDTH / 2, TEST_HEIGHT / 2 - 50);
+ is(graph.getViewRange().startTime | 0, 0,
+ "The selection start boundary is correct (2).");
+ is(graph.getViewRange().endTime | 0, 150,
+ "The selection end boundary is correct (2).");
+ is(graph.getViewRange().verticalOffset | 0, 17,
+ "The vertical offset is correct (2).");
+
+ dragStop(graph, TEST_WIDTH / 2, TEST_HEIGHT / 2 - 100);
+ is(graph.getViewRange().startTime | 0, 0,
+ "The selection start boundary is correct (3).");
+ is(graph.getViewRange().endTime | 0, 150,
+ "The selection end boundary is correct (3).");
+ is(graph.getViewRange().verticalOffset | 0, 42,
+ "The vertical offset is correct (3).");
+
+ // Drag down strongly vertically and slightly horizontally.
+
+ dragStart(graph, TEST_WIDTH / 2, TEST_HEIGHT / 2);
+ is(graph.getViewRange().startTime | 0, 0,
+ "The selection start boundary is correct (4).");
+ is(graph.getViewRange().endTime | 0, 150,
+ "The selection end boundary is correct (4).");
+ is(graph.getViewRange().verticalOffset | 0, 42,
+ "The vertical offset is correct (4).");
+
+ hover(graph, TEST_WIDTH / 2, TEST_HEIGHT / 2 + 50);
+ is(graph.getViewRange().startTime | 0, 0,
+ "The selection start boundary is correct (5).");
+ is(graph.getViewRange().endTime | 0, 150,
+ "The selection end boundary is correct (5).");
+ is(graph.getViewRange().verticalOffset | 0, 25,
+ "The vertical offset is correct (5).");
+
+ dragStop(graph, TEST_WIDTH / 2 + 100, TEST_HEIGHT / 2 + 500);
+ is(graph.getViewRange().startTime | 0, 0,
+ "The selection start boundary is correct (6).");
+ is(graph.getViewRange().endTime | 0, 150,
+ "The selection end boundary is correct (6).");
+ is(graph.getViewRange().verticalOffset | 0, 0,
+ "The vertical offset is correct (6).");
+
+ // Drag up slightly vertically and strongly horizontally.
+
+ dragStart(graph, TEST_WIDTH / 2, TEST_HEIGHT / 2);
+ is(graph.getViewRange().startTime | 0, 0,
+ "The selection start boundary is correct (7).");
+ is(graph.getViewRange().endTime | 0, 150,
+ "The selection end boundary is correct (7).");
+ is(graph.getViewRange().verticalOffset | 0, 0,
+ "The vertical offset is correct (7).");
+
+ hover(graph, TEST_WIDTH / 2 + 50, TEST_HEIGHT / 2);
+ is(graph.getViewRange().startTime | 0, 0,
+ "The selection start boundary is correct (8).");
+ is(graph.getViewRange().endTime | 0, 116,
+ "The selection end boundary is correct (8).");
+ is(graph.getViewRange().verticalOffset | 0, 0,
+ "The vertical offset is correct (8).");
+
+ dragStop(graph, TEST_WIDTH / 2 + 500, TEST_HEIGHT / 2 + 100);
+ is(graph.getViewRange().startTime | 0, 0,
+ "The selection start boundary is correct (9).");
+ is(graph.getViewRange().endTime | 0, 0,
+ "The selection end boundary is correct (9).");
+ is(graph.getViewRange().verticalOffset | 0, 0,
+ "The vertical offset is correct (9).");
+}
+
+// EventUtils just doesn't work!
+
+function hover(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+}
+
+function dragStart(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseDown({ testX: x, testY: y });
+}
+
+function dragStop(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseUp({ testX: x, testY: y });
+}
diff --git a/devtools/client/shared/test/browser_flame-graph-04.js b/devtools/client/shared/test/browser_flame-graph-04.js
new file mode 100644
index 000000000..5bcc112ec
--- /dev/null
+++ b/devtools/client/shared/test/browser_flame-graph-04.js
@@ -0,0 +1,90 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that text metrics in the flame graph widget work properly.
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const {ELLIPSIS} = require("devtools/shared/l10n");
+const {FlameGraph} = require("devtools/client/shared/widgets/FlameGraph");
+const {FLAME_GRAPH_BLOCK_TEXT_FONT_SIZE} = require("devtools/client/shared/widgets/FlameGraph");
+const {FLAME_GRAPH_BLOCK_TEXT_FONT_FAMILY} = require("devtools/client/shared/widgets/FlameGraph");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new FlameGraph(doc.body, 1);
+ yield graph.ready();
+
+ testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function testGraph(graph) {
+ is(graph._averageCharWidth, getAverageCharWidth(),
+ "The average char width was calculated correctly.");
+ is(graph._overflowCharWidth, getCharWidth(ELLIPSIS),
+ "The ellipsis char width was calculated correctly.");
+
+ let text = "This text is maybe overflowing";
+ let text1000px = graph._getFittedText(text, 1000);
+ let text50px = graph._getFittedText(text, 50);
+ let text10px = graph._getFittedText(text, 10);
+ let text1px = graph._getFittedText(text, 1);
+
+ is(graph._getTextWidthApprox(text), getAverageCharWidth() * text.length,
+ "The approximate width was calculated correctly.");
+
+ info("Text at 1000px width: " + text1000px);
+ info("Text at 50px width : " + text50px);
+ info("Text at 10px width : " + text10px);
+ info("Text at 1px width : " + text1px);
+
+ is(text1000px, text,
+ "The fitted text for 1000px width is correct.");
+
+ isnot(text50px, text,
+ "The fitted text for 50px width is correct (1).");
+
+ ok(text50px.includes(ELLIPSIS),
+ "The fitted text for 50px width is correct (2).");
+
+ is(graph._getFittedText(text, FLAME_GRAPH_BLOCK_TEXT_FONT_SIZE + 1), ELLIPSIS,
+ "The fitted text for text font size width is correct.");
+
+ is(graph._getFittedText(text, 1), "",
+ "The fitted text for 1px width is correct.");
+}
+
+function getAverageCharWidth() {
+ let letterWidthsSum = 0;
+
+ let start = " ".charCodeAt(0);
+ let end = "z".charCodeAt(0) + 1;
+
+ for (let i = start; i < end; i++) {
+ let char = String.fromCharCode(i);
+ letterWidthsSum += getCharWidth(char);
+ }
+
+ return letterWidthsSum / (end - start);
+}
+
+function getCharWidth(char) {
+ let canvas = document.createElementNS(HTML_NS, "canvas");
+ let ctx = canvas.getContext("2d");
+
+ let fontSize = FLAME_GRAPH_BLOCK_TEXT_FONT_SIZE;
+ let fontFamily = FLAME_GRAPH_BLOCK_TEXT_FONT_FAMILY;
+ ctx.font = fontSize + "px " + fontFamily;
+
+ return ctx.measureText(char).width;
+}
diff --git a/devtools/client/shared/test/browser_flame-graph-05.js b/devtools/client/shared/test/browser_flame-graph-05.js
new file mode 100644
index 000000000..1b30489dc
--- /dev/null
+++ b/devtools/client/shared/test/browser_flame-graph-05.js
@@ -0,0 +1,113 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that flame graph widget has proper keyboard support.
+
+const TEST_DATA = [
+ {
+ color: "#f00",
+ blocks: [
+ { x: 0, y: 0, width: 50, height: 20, text: "FOO" },
+ { x: 50, y: 0, width: 100, height: 20, text: "BAR" }
+ ]
+ },
+ {
+ color: "#00f",
+ blocks: [
+ { x: 0, y: 30, width: 30, height: 20, text: "BAZ" }
+ ]
+ }
+];
+const TEST_BOUNDS = { startTime: 0, endTime: 150 };
+const TEST_DPI_DENSITIY = 2;
+
+const KEY_CODE_UP = 38;
+const KEY_CODE_LEFT = 37;
+const KEY_CODE_RIGHT = 39;
+
+var {FlameGraph} = require("devtools/client/shared/widgets/FlameGraph");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ doc.body.setAttribute("style",
+ "position: fixed; width: 100%; height: 100%; margin: 0;");
+
+ let graph = new FlameGraph(doc.body, TEST_DPI_DENSITIY);
+ yield graph.ready();
+
+ yield testGraph(host, graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function* testGraph(host, graph) {
+ graph.setData({ data: TEST_DATA, bounds: TEST_BOUNDS });
+
+ is(graph._selection.start, 0,
+ "The graph's selection start value is initially correct.");
+ is(graph._selection.end, TEST_BOUNDS.endTime * TEST_DPI_DENSITIY,
+ "The graph's selection end value is initially correct.");
+
+ yield pressKeyForTime(graph, KEY_CODE_LEFT, 1000);
+
+ is(graph._selection.start, 0,
+ "The graph's selection start value is correct after pressing LEFT.");
+ ok(graph._selection.end < TEST_BOUNDS.endTime * TEST_DPI_DENSITIY,
+ "The graph's selection end value is correct after pressing LEFT.");
+
+ graph._selection.start = 0;
+ graph._selection.end = TEST_BOUNDS.endTime * TEST_DPI_DENSITIY;
+ info("Graph selection was reset (1).");
+
+ yield pressKeyForTime(graph, KEY_CODE_RIGHT, 1000);
+
+ ok(graph._selection.start > 0,
+ "The graph's selection start value is correct after pressing RIGHT.");
+ is(graph._selection.end, TEST_BOUNDS.endTime * TEST_DPI_DENSITIY,
+ "The graph's selection end value is correct after pressing RIGHT.");
+
+ graph._selection.start = 0;
+ graph._selection.end = TEST_BOUNDS.endTime * TEST_DPI_DENSITIY;
+ info("Graph selection was reset (2).");
+
+ yield pressKeyForTime(graph, KEY_CODE_UP, 1000);
+
+ ok(graph._selection.start > 0,
+ "The graph's selection start value is correct after pressing UP.");
+ ok(graph._selection.end < TEST_BOUNDS.endTime * TEST_DPI_DENSITIY,
+ "The graph's selection end value is correct after pressing UP.");
+
+ let distanceLeft = graph._selection.start;
+ let distanceRight = TEST_BOUNDS.endTime * TEST_DPI_DENSITIY - graph._selection.end;
+
+ ok(Math.abs(distanceRight - distanceLeft) < 0.1,
+ "The graph zoomed correctly towards the center point.");
+}
+
+function pressKeyForTime(graph, keyCode, ms) {
+ graph._onKeyDown({
+ keyCode,
+ preventDefault: () => {},
+ stopPropagation: () => {},
+ });
+
+ return new Promise(resolve => {
+ setTimeout(() => {
+ graph._onKeyUp({
+ keyCode,
+ preventDefault: () => {},
+ stopPropagation: () => {},
+ });
+ resolve();
+ }, ms);
+ });
+}
diff --git a/devtools/client/shared/test/browser_flame-graph-utils-01.js b/devtools/client/shared/test/browser_flame-graph-utils-01.js
new file mode 100644
index 000000000..6871e234c
--- /dev/null
+++ b/devtools/client/shared/test/browser_flame-graph-utils-01.js
@@ -0,0 +1,256 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that text metrics and data conversion from profiler samples
+// widget work properly in the flame graph.
+
+const {FlameGraphUtils} = require("devtools/client/shared/widgets/FlameGraph");
+const {PALLETTE_SIZE} = require("devtools/client/shared/widgets/FlameGraph");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let out = FlameGraphUtils.createFlameGraphDataFromThread(TEST_DATA);
+
+ ok(out, "Some data was outputted properly");
+ is(out.length, PALLETTE_SIZE, "The outputted length is correct.");
+
+ info("Got flame graph data:\n" + out.toSource() + "\n");
+
+ for (let i = 0; i < out.length; i++) {
+ let found = out[i];
+ let expected = EXPECTED_OUTPUT[i];
+
+ is(found.blocks.length, expected.blocks.length,
+ "The correct number of blocks were found in this bucket.");
+
+ for (let j = 0; j < found.blocks.length; j++) {
+ is(found.blocks[j].x, expected.blocks[j].x,
+ "The expected block X position is correct for this frame.");
+ is(found.blocks[j].y, expected.blocks[j].y,
+ "The expected block Y position is correct for this frame.");
+ is(found.blocks[j].width, expected.blocks[j].width,
+ "The expected block width is correct for this frame.");
+ is(found.blocks[j].height, expected.blocks[j].height,
+ "The expected block height is correct for this frame.");
+ is(found.blocks[j].text, expected.blocks[j].text,
+ "The expected block text is correct for this frame.");
+ }
+ }
+}
+
+var TEST_DATA = synthesizeProfileForTest([{
+ frames: [{
+ location: "M"
+ }, {
+ location: "N",
+ }, {
+ location: "P"
+ }],
+ time: 50,
+}, {
+ frames: [{
+ location: "A"
+ }, {
+ location: "B",
+ }, {
+ location: "C"
+ }],
+ time: 100,
+}, {
+ frames: [{
+ location: "A"
+ }, {
+ location: "B",
+ }, {
+ location: "D"
+ }],
+ time: 210,
+}, {
+ frames: [{
+ location: "A"
+ }, {
+ location: "E",
+ }, {
+ location: "F"
+ }],
+ time: 330,
+}, {
+ frames: [{
+ location: "A"
+ }, {
+ location: "B",
+ }, {
+ location: "C"
+ }],
+ time: 460,
+}, {
+ frames: [{
+ location: "X"
+ }, {
+ location: "Y",
+ }, {
+ location: "Z"
+ }],
+ time: 500
+}]);
+
+var EXPECTED_OUTPUT = [{
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: [{
+ startTime: 50,
+ frameKey: "A",
+ x: 50,
+ y: 0,
+ width: 410,
+ height: 15,
+ text: "A"
+ }]
+}, {
+ blocks: [{
+ startTime: 50,
+ frameKey: "B",
+ x: 50,
+ y: 15,
+ width: 160,
+ height: 15,
+ text: "B"
+ }, {
+ startTime: 330,
+ frameKey: "B",
+ x: 330,
+ y: 15,
+ width: 130,
+ height: 15,
+ text: "B"
+ }]
+}, {
+ blocks: [{
+ startTime: 50,
+ frameKey: "C",
+ x: 50,
+ y: 30,
+ width: 50,
+ height: 15,
+ text: "C"
+ }, {
+ startTime: 330,
+ frameKey: "C",
+ x: 330,
+ y: 30,
+ width: 130,
+ height: 15,
+ text: "C"
+ }]
+}, {
+ blocks: [{
+ startTime: 100,
+ frameKey: "D",
+ x: 100,
+ y: 30,
+ width: 110,
+ height: 15,
+ text: "D"
+ }, {
+ startTime: 460,
+ frameKey: "X",
+ x: 460,
+ y: 0,
+ width: 40,
+ height: 15,
+ text: "X"
+ }]
+}, {
+ blocks: [{
+ startTime: 210,
+ frameKey: "E",
+ x: 210,
+ y: 15,
+ width: 120,
+ height: 15,
+ text: "E"
+ }, {
+ startTime: 460,
+ frameKey: "Y",
+ x: 460,
+ y: 15,
+ width: 40,
+ height: 15,
+ text: "Y"
+ }]
+}, {
+ blocks: [{
+ startTime: 210,
+ frameKey: "F",
+ x: 210,
+ y: 30,
+ width: 120,
+ height: 15,
+ text: "F"
+ }, {
+ startTime: 460,
+ frameKey: "Z",
+ x: 460,
+ y: 30,
+ width: 40,
+ height: 15,
+ text: "Z"
+ }]
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: [{
+ startTime: 0,
+ frameKey: "M",
+ x: 0,
+ y: 0,
+ width: 50,
+ height: 15,
+ text: "M"
+ }]
+}, {
+ blocks: [{
+ startTime: 0,
+ frameKey: "N",
+ x: 0,
+ y: 15,
+ width: 50,
+ height: 15,
+ text: "N"
+ }]
+}, {
+ blocks: []
+}, {
+ blocks: [{
+ startTime: 0,
+ frameKey: "P",
+ x: 0,
+ y: 30,
+ width: 50,
+ height: 15,
+ text: "P"
+ }]
+}, {
+ blocks: []
+}, {
+ blocks: []
+}];
diff --git a/devtools/client/shared/test/browser_flame-graph-utils-02.js b/devtools/client/shared/test/browser_flame-graph-utils-02.js
new file mode 100644
index 000000000..15e9d1933
--- /dev/null
+++ b/devtools/client/shared/test/browser_flame-graph-utils-02.js
@@ -0,0 +1,130 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests consecutive duplicate frames are removed from the flame graph data.
+
+const {FlameGraphUtils} = require("devtools/client/shared/widgets/FlameGraph");
+const {PALLETTE_SIZE} = require("devtools/client/shared/widgets/FlameGraph");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let out = FlameGraphUtils.createFlameGraphDataFromThread(TEST_DATA, {
+ flattenRecursion: true
+ });
+
+ ok(out, "Some data was outputted properly");
+ is(out.length, PALLETTE_SIZE, "The outputted length is correct.");
+
+ info("Got flame graph data:\n" + out.toSource() + "\n");
+
+ for (let i = 0; i < out.length; i++) {
+ let found = out[i];
+ let expected = EXPECTED_OUTPUT[i];
+
+ is(found.blocks.length, expected.blocks.length,
+ "The correct number of blocks were found in this bucket.");
+
+ for (let j = 0; j < found.blocks.length; j++) {
+ is(found.blocks[j].x, expected.blocks[j].x,
+ "The expected block X position is correct for this frame.");
+ is(found.blocks[j].y, expected.blocks[j].y,
+ "The expected block Y position is correct for this frame.");
+ is(found.blocks[j].width, expected.blocks[j].width,
+ "The expected block width is correct for this frame.");
+ is(found.blocks[j].height, expected.blocks[j].height,
+ "The expected block height is correct for this frame.");
+ is(found.blocks[j].text, expected.blocks[j].text,
+ "The expected block text is correct for this frame.");
+ }
+ }
+}
+
+var TEST_DATA = synthesizeProfileForTest([{
+ frames: [{
+ location: "A"
+ }, {
+ location: "A"
+ }, {
+ location: "A"
+ }, {
+ location: "B",
+ }, {
+ location: "B",
+ }, {
+ location: "C"
+ }],
+ time: 50,
+}]);
+
+var EXPECTED_OUTPUT = [{
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: [{
+ startTime: 0,
+ frameKey: "A",
+ x: 0,
+ y: 0,
+ width: 50,
+ height: 15,
+ text: "A"
+ }]
+}, {
+ blocks: [{
+ startTime: 0,
+ frameKey: "B",
+ x: 0,
+ y: 15,
+ width: 50,
+ height: 15,
+ text: "B"
+ }]
+}, {
+ blocks: [{
+ startTime: 0,
+ frameKey: "C",
+ x: 0,
+ y: 30,
+ width: 50,
+ height: 15,
+ text: "C"
+ }]
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}];
diff --git a/devtools/client/shared/test/browser_flame-graph-utils-03.js b/devtools/client/shared/test/browser_flame-graph-utils-03.js
new file mode 100644
index 000000000..0f28c0afc
--- /dev/null
+++ b/devtools/client/shared/test/browser_flame-graph-utils-03.js
@@ -0,0 +1,136 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests if platform frames are removed from the flame graph data.
+
+const {FlameGraphUtils} = require("devtools/client/shared/widgets/FlameGraph");
+const {PALLETTE_SIZE} = require("devtools/client/shared/widgets/FlameGraph");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let out = FlameGraphUtils.createFlameGraphDataFromThread(TEST_DATA, {
+ contentOnly: true
+ });
+
+ ok(out, "Some data was outputted properly");
+ is(out.length, PALLETTE_SIZE, "The outputted length is correct.");
+
+ info("Got flame graph data:\n" + out.toSource() + "\n");
+
+ for (let i = 0; i < out.length; i++) {
+ let found = out[i];
+ let expected = EXPECTED_OUTPUT[i];
+
+ is(found.blocks.length, expected.blocks.length,
+ "The correct number of blocks were found in this bucket.");
+
+ for (let j = 0; j < found.blocks.length; j++) {
+ is(found.blocks[j].x, expected.blocks[j].x,
+ "The expected block X position is correct for this frame.");
+ is(found.blocks[j].y, expected.blocks[j].y,
+ "The expected block Y position is correct for this frame.");
+ is(found.blocks[j].width, expected.blocks[j].width,
+ "The expected block width is correct for this frame.");
+ is(found.blocks[j].height, expected.blocks[j].height,
+ "The expected block height is correct for this frame.");
+ is(found.blocks[j].text, expected.blocks[j].text,
+ "The expected block text is correct for this frame.");
+ }
+ }
+}
+
+var TEST_DATA = synthesizeProfileForTest([{
+ frames: [{
+ location: "http://A"
+ }, {
+ location: "https://B"
+ }, {
+ location: "file://C",
+ }, {
+ location: "chrome://D"
+ }, {
+ location: "resource://E"
+ }],
+ time: 50,
+}]);
+
+var EXPECTED_OUTPUT = [{
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: [{
+ startTime: 0,
+ frameKey: "http://A",
+ x: 0,
+ y: 0,
+ width: 50,
+ height: 15,
+ text: "http://A"
+ }]
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: [{
+ startTime: 0,
+ frameKey: "Gecko",
+ x: 0,
+ y: 45,
+ width: 50,
+ height: 15,
+ text: "Gecko"
+ }]
+}, {
+ blocks: []
+}, {
+ blocks: [{
+ startTime: 0,
+ frameKey: "https://B",
+ x: 0,
+ y: 15,
+ width: 50,
+ height: 15,
+ text: "https://B"
+ }]
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: [{
+ startTime: 0,
+ frameKey: "file://C",
+ x: 0,
+ y: 30,
+ width: 50,
+ height: 15,
+ text: "file://C"
+ }]
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}];
diff --git a/devtools/client/shared/test/browser_flame-graph-utils-04.js b/devtools/client/shared/test/browser_flame-graph-utils-04.js
new file mode 100644
index 000000000..1bf6c1f59
--- /dev/null
+++ b/devtools/client/shared/test/browser_flame-graph-utils-04.js
@@ -0,0 +1,188 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests if (idle) nodes are added when necessary in the flame graph data.
+
+const {FlameGraphUtils} = require("devtools/client/shared/widgets/FlameGraph");
+const {PALLETTE_SIZE} = require("devtools/client/shared/widgets/FlameGraph");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let out = FlameGraphUtils.createFlameGraphDataFromThread(TEST_DATA, {
+ flattenRecursion: true,
+ contentOnly: true,
+ showIdleBlocks: "\m/"
+ });
+
+ ok(out, "Some data was outputted properly");
+ is(out.length, PALLETTE_SIZE, "The outputted length is correct.");
+
+ info("Got flame graph data:\n" + out.toSource() + "\n");
+
+ for (let i = 0; i < out.length; i++) {
+ let found = out[i];
+ let expected = EXPECTED_OUTPUT[i];
+
+ is(found.blocks.length, expected.blocks.length,
+ "The correct number of blocks were found in this bucket.");
+
+ for (let j = 0; j < found.blocks.length; j++) {
+ is(found.blocks[j].x, expected.blocks[j].x,
+ "The expected block X position is correct for this frame.");
+ is(found.blocks[j].y, expected.blocks[j].y,
+ "The expected block Y position is correct for this frame.");
+ is(found.blocks[j].width, expected.blocks[j].width,
+ "The expected block width is correct for this frame.");
+ is(found.blocks[j].height, expected.blocks[j].height,
+ "The expected block height is correct for this frame.");
+ is(found.blocks[j].text, expected.blocks[j].text,
+ "The expected block text is correct for this frame.");
+ }
+ }
+}
+
+var TEST_DATA = synthesizeProfileForTest([{
+ frames: [{
+ location: "http://A"
+ }, {
+ location: "http://A"
+ }, {
+ location: "http://A"
+ }, {
+ location: "https://B"
+ }, {
+ location: "https://B"
+ }, {
+ location: "file://C",
+ }, {
+ location: "chrome://D"
+ }, {
+ location: "resource://E"
+ }],
+ time: 50
+}, {
+ frames: [],
+ time: 100
+}, {
+ frames: [{
+ location: "http://A"
+ }, {
+ location: "https://B"
+ }, {
+ location: "file://C",
+ }],
+ time: 150
+}]);
+
+var EXPECTED_OUTPUT = [{
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: [{
+ startTime: 0,
+ frameKey: "http://A",
+ x: 0,
+ y: 0,
+ width: 50,
+ height: 15,
+ text: "http://A"
+ }, {
+ startTime: 100,
+ frameKey: "http://A",
+ x: 100,
+ y: 0,
+ width: 50,
+ height: 15,
+ text: "http://A"
+ }]
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: [{
+ startTime: 0,
+ frameKey: "Gecko",
+ x: 0,
+ y: 45,
+ width: 50,
+ height: 15,
+ text: "Gecko"
+ }]
+}, {
+ blocks: []
+}, {
+ blocks: [{
+ startTime: 0,
+ frameKey: "https://B",
+ x: 0,
+ y: 15,
+ width: 50,
+ height: 15,
+ text: "https://B"
+ }, {
+ startTime: 100,
+ frameKey: "https://B",
+ x: 100,
+ y: 15,
+ width: 50,
+ height: 15,
+ text: "https://B"
+ }]
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: [{
+ startTime: 0,
+ frameKey: "file://C",
+ x: 0,
+ y: 30,
+ width: 50,
+ height: 15,
+ text: "file://C"
+ }, {
+ startTime: 100,
+ frameKey: "file://C",
+ x: 100,
+ y: 30,
+ width: 50,
+ height: 15,
+ text: "file://C"
+ }]
+}, {
+ blocks: [{
+ startTime: 50,
+ frameKey: "m/",
+ x: 50,
+ y: 0,
+ width: 50,
+ height: 15,
+ text: "m/"
+ }]
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}];
diff --git a/devtools/client/shared/test/browser_flame-graph-utils-05.js b/devtools/client/shared/test/browser_flame-graph-utils-05.js
new file mode 100644
index 000000000..5abdd708a
--- /dev/null
+++ b/devtools/client/shared/test/browser_flame-graph-utils-05.js
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that flame graph data is cached, and that the cache may be cleared.
+
+const {FlameGraphUtils} = require("devtools/client/shared/widgets/FlameGraph");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let out1 = FlameGraphUtils.createFlameGraphDataFromThread(TEST_DATA);
+ let out2 = FlameGraphUtils.createFlameGraphDataFromThread(TEST_DATA);
+ is(out1, out2, "The outputted data is identical.");
+
+ let out3 = FlameGraphUtils.createFlameGraphDataFromThread(
+ TEST_DATA, { flattenRecursion: true }
+ );
+ is(out2, out3, "The outputted data is still identical.");
+
+ FlameGraphUtils.removeFromCache(TEST_DATA);
+ let out4 = FlameGraphUtils.createFlameGraphDataFromThread(
+ TEST_DATA, { flattenRecursion: true }
+ );
+ isnot(out3, out4, "The outputted data is not identical anymore.");
+}
+
+var TEST_DATA = synthesizeProfileForTest([{
+ frames: [{
+ location: "A"
+ }, {
+ location: "A"
+ }, {
+ location: "A"
+ }, {
+ location: "B",
+ }, {
+ location: "B",
+ }, {
+ location: "C"
+ }],
+ time: 50,
+}]);
diff --git a/devtools/client/shared/test/browser_flame-graph-utils-06.js b/devtools/client/shared/test/browser_flame-graph-utils-06.js
new file mode 100644
index 000000000..886a1035b
--- /dev/null
+++ b/devtools/client/shared/test/browser_flame-graph-utils-06.js
@@ -0,0 +1,117 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the text displayed is the function name, file name and line number
+// if applicable and demangling.
+
+const {FlameGraphUtils} = require("devtools/client/shared/widgets/FlameGraph");
+const {PALLETTE_SIZE} = require("devtools/client/shared/widgets/FlameGraph");
+const MANGLED_FN = "__Z3FooIiEvv";
+const UNMANGLED_FN = "void Foo<int>()";
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let out = FlameGraphUtils.createFlameGraphDataFromThread(TEST_DATA, {
+ flattenRecursion: true
+ });
+
+ ok(out, "Some data was outputted properly");
+ is(out.length, PALLETTE_SIZE, "The outputted length is correct.");
+
+ info("Got flame graph data:\n" + out.toSource() + "\n");
+
+ for (let i = 0; i < out.length; i++) {
+ let found = out[i];
+ let expected = EXPECTED_OUTPUT[i];
+
+ is(found.blocks.length, expected.blocks.length,
+ "The correct number of blocks were found in this bucket.");
+
+ for (let j = 0; j < found.blocks.length; j++) {
+ is(found.blocks[j].x, expected.blocks[j].x,
+ "The expected block X position is correct for this frame.");
+ is(found.blocks[j].y, expected.blocks[j].y,
+ "The expected block Y position is correct for this frame.");
+ is(found.blocks[j].width, expected.blocks[j].width,
+ "The expected block width is correct for this frame.");
+ is(found.blocks[j].height, expected.blocks[j].height,
+ "The expected block height is correct for this frame.");
+ is(found.blocks[j].text, expected.blocks[j].text,
+ "The expected block text is correct for this frame.");
+ }
+ }
+}
+
+var TEST_DATA = synthesizeProfileForTest([{
+ frames: [{
+ location: "A (http://path/to/file.js:10:5)"
+ }, {
+ location: `${MANGLED_FN} (http://path/to/file.js:100:5)`
+ }],
+ time: 50,
+}]);
+
+var EXPECTED_OUTPUT = [{
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: [{
+ startTime: 0,
+ frameKey: "A (http://path/to/file.js:10:5)",
+ x: 0,
+ y: 0,
+ width: 50,
+ height: 15,
+ text: "A (file.js:10)"
+ }]
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: [{
+ startTime: 0,
+ frameKey: `${MANGLED_FN} (http://path/to/file.js:100:5)`,
+ x: 0,
+ y: 15,
+ width: 50,
+ height: 15,
+ text: `${UNMANGLED_FN} (file.js:100)`
+ }]
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}];
diff --git a/devtools/client/shared/test/browser_flame-graph-utils-hash.js b/devtools/client/shared/test/browser_flame-graph-utils-hash.js
new file mode 100644
index 000000000..6b441bbf5
--- /dev/null
+++ b/devtools/client/shared/test/browser_flame-graph-utils-hash.js
@@ -0,0 +1,24 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests if (idle) nodes are added when necessary in the flame graph data.
+
+const {FlameGraphUtils} = require("devtools/client/shared/widgets/FlameGraph");
+
+add_task(function* () {
+ let hash1 = FlameGraphUtils._getStringHash("abc");
+ let hash2 = FlameGraphUtils._getStringHash("acb");
+ let hash3 = FlameGraphUtils._getStringHash(Array.from(Array(100000)).join("a"));
+ let hash4 = FlameGraphUtils._getStringHash(Array.from(Array(100000)).join("b"));
+
+ isnot(hash1, hash2, "The hashes should not be equal (1).");
+ isnot(hash2, hash3, "The hashes should not be equal (2).");
+ isnot(hash3, hash4, "The hashes should not be equal (3).");
+
+ ok(Number.isInteger(hash1), "The hashes should be integers, not Infinity or NaN (1).");
+ ok(Number.isInteger(hash2), "The hashes should be integers, not Infinity or NaN (2).");
+ ok(Number.isInteger(hash3), "The hashes should be integers, not Infinity or NaN (3).");
+ ok(Number.isInteger(hash4), "The hashes should be integers, not Infinity or NaN (4).");
+});
diff --git a/devtools/client/shared/test/browser_graphs-01.js b/devtools/client/shared/test/browser_graphs-01.js
new file mode 100644
index 000000000..c4f5640d9
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-01.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that graph widgets works properly.
+
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+ finish();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ doc.body.setAttribute("style",
+ "position: fixed; width: 100%; height: 100%; margin: 0;");
+
+ let graph = new LineGraphWidget(doc.body, "fps");
+
+ let readyEventEmitted;
+ graph.once("ready", () => {
+ readyEventEmitted = true;
+ });
+
+ yield graph.ready();
+ ok(readyEventEmitted, "The 'ready' event should have been emitted");
+
+ testGraph(host, graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function testGraph(host, graph) {
+ ok(graph._container.classList.contains("line-graph-widget-container"),
+ "The correct graph container was created.");
+ ok(graph._canvas.classList.contains("line-graph-widget-canvas"),
+ "The correct graph container was created.");
+
+ let bounds = host.frame.getBoundingClientRect();
+
+ is(graph.width, bounds.width * window.devicePixelRatio,
+ "The graph has the correct width.");
+ is(graph.height, bounds.height * window.devicePixelRatio,
+ "The graph has the correct height.");
+
+ ok(graph._cursor.x === null,
+ "The graph's cursor X coordinate is initially null.");
+ ok(graph._cursor.y === null,
+ "The graph's cursor Y coordinate is initially null.");
+
+ ok(graph._selection.start === null,
+ "The graph's selection start value is initially null.");
+ ok(graph._selection.end === null,
+ "The graph's selection end value is initially null.");
+
+ ok(graph._selectionDragger.origin === null,
+ "The graph's dragger origin value is initially null.");
+ ok(graph._selectionDragger.anchor.start === null,
+ "The graph's dragger anchor start value is initially null.");
+ ok(graph._selectionDragger.anchor.end === null,
+ "The graph's dragger anchor end value is initially null.");
+
+ ok(graph._selectionResizer.margin === null,
+ "The graph's resizer margin value is initially null.");
+}
diff --git a/devtools/client/shared/test/browser_graphs-02.js b/devtools/client/shared/test/browser_graphs-02.js
new file mode 100644
index 000000000..def728722
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-02.js
@@ -0,0 +1,107 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that graph widgets can properly add data, regions and highlights.
+
+const TEST_DATA = [
+ { delta: 112, value: 48 }, { delta: 213, value: 59 },
+ { delta: 313, value: 60 }, { delta: 413, value: 59 },
+ { delta: 530, value: 59 }, { delta: 646, value: 58 },
+ { delta: 747, value: 60 }, { delta: 863, value: 48 },
+ { delta: 980, value: 37 }, { delta: 1097, value: 30 },
+ { delta: 1213, value: 29 }, { delta: 1330, value: 23 },
+ { delta: 1430, value: 10 }, { delta: 1534, value: 17 },
+ { delta: 1645, value: 20 }, { delta: 1746, value: 22 },
+ { delta: 1846, value: 39 }, { delta: 1963, value: 26 },
+ { delta: 2080, value: 27 }, { delta: 2197, value: 35 },
+ { delta: 2312, value: 47 }, { delta: 2412, value: 53 },
+ { delta: 2514, value: 60 }, { delta: 2630, value: 37 },
+ { delta: 2730, value: 36 }, { delta: 2830, value: 37 },
+ { delta: 2946, value: 36 }, { delta: 3046, value: 40 },
+ { delta: 3163, value: 47 }, { delta: 3280, value: 41 },
+ { delta: 3380, value: 35 }, { delta: 3480, value: 27 },
+ { delta: 3580, value: 39 }, { delta: 3680, value: 42 },
+ { delta: 3780, value: 49 }, { delta: 3880, value: 55 },
+ { delta: 3980, value: 60 }, { delta: 4080, value: 60 },
+ { delta: 4180, value: 60 }
+];
+const TEST_REGIONS = [{ start: 320, end: 460 }, { start: 780, end: 860 }];
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+ yield graph.once("ready");
+
+ testDataAndRegions(graph);
+ testHighlights(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function testDataAndRegions(graph) {
+ let thrown1;
+ try {
+ graph.setRegions(TEST_REGIONS);
+ } catch (e) {
+ thrown1 = true;
+ }
+ ok(thrown1, "Setting regions before setting data shouldn't work.");
+
+ graph.setData(TEST_DATA);
+ graph.setRegions(TEST_REGIONS);
+
+ let thrown2;
+ try {
+ graph.setRegions(TEST_REGIONS);
+ } catch (e) {
+ thrown2 = true;
+ }
+ ok(thrown2, "Setting regions twice shouldn't work.");
+
+ ok(graph.hasData(), "The graph should now have the data source set.");
+ ok(graph.hasRegions(), "The graph should now have the regions set.");
+
+ is(graph.dataScaleX,
+ // last & first tick in TEST_DATA
+ graph.width / 4180,
+ "The data scale on the X axis is correct.");
+
+ is(graph.dataScaleY,
+ // max value in TEST_DATA * GRAPH_DAMPEN_VALUES
+ graph.height / 60 * 0.85,
+ "The data scale on the Y axis is correct.");
+
+ for (let i = 0; i < TEST_REGIONS.length; i++) {
+ let original = TEST_REGIONS[i];
+ let normalized = graph._regions[i];
+
+ is(original.start * graph.dataScaleX, normalized.start,
+ "The region's start value was properly normalized.");
+ is(original.end * graph.dataScaleX, normalized.end,
+ "The region's end value was properly normalized.");
+ }
+}
+
+function testHighlights(graph) {
+ graph.setMask(TEST_REGIONS);
+ ok(graph.hasMask(),
+ "The graph should now have the highlights set.");
+
+ graph.setMask([]);
+ ok(graph.hasMask(),
+ "The graph shouldn't have anything highlighted.");
+
+ graph.setMask(null);
+ ok(!graph.hasMask(),
+ "The graph should have everything highlighted.");
+}
diff --git a/devtools/client/shared/test/browser_graphs-03.js b/devtools/client/shared/test/browser_graphs-03.js
new file mode 100644
index 000000000..b44d4620a
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-03.js
@@ -0,0 +1,111 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that graph widgets can handle clients getting/setting the
+// selection or cursor.
+
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+ yield graph.once("ready");
+
+ yield testSelection(graph);
+ yield testCursor(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function* testSelection(graph) {
+ ok(graph.getSelection().start === null,
+ "The graph's selection should initially have a null start value.");
+ ok(graph.getSelection().end === null,
+ "The graph's selection should initially have a null end value.");
+ ok(!graph.hasSelection(),
+ "There shouldn't initially be any selection.");
+
+ let selected = graph.once("selecting");
+ graph.setSelection({ start: 100, end: 200 });
+
+ yield selected;
+ ok(true, "A 'selecting' event has been fired.");
+
+ ok(graph.hasSelection(),
+ "There should now be a selection.");
+ is(graph.getSelection().start, 100,
+ "The graph's selection now has an updated start value.");
+ is(graph.getSelection().end, 200,
+ "The graph's selection now has an updated end value.");
+
+ let thrown;
+ try {
+ graph.setSelection({ start: null, end: null });
+ } catch (e) {
+ thrown = true;
+ }
+ ok(thrown, "Setting a null selection shouldn't work.");
+
+ ok(graph.hasSelection(),
+ "There should still be a selection.");
+
+ let deselected = graph.once("deselecting");
+ graph.dropSelection();
+
+ yield deselected;
+ ok(true, "A 'deselecting' event has been fired.");
+
+ ok(!graph.hasSelection(),
+ "There shouldn't be any selection anymore.");
+ ok(graph.getSelection().start === null,
+ "The graph's selection now has a null start value.");
+ ok(graph.getSelection().end === null,
+ "The graph's selection now has a null end value.");
+}
+
+function* testCursor(graph) {
+ ok(graph.getCursor().x === null,
+ "The graph's cursor should initially have a null X value.");
+ ok(graph.getCursor().y === null,
+ "The graph's cursor should initially have a null Y value.");
+ ok(!graph.hasCursor(),
+ "There shouldn't initially be any cursor.");
+
+ graph.setCursor({ x: 100, y: 50 });
+
+ ok(graph.hasCursor(),
+ "There should now be a cursor.");
+ is(graph.getCursor().x, 100,
+ "The graph's cursor now has an updated start value.");
+ is(graph.getCursor().y, 50,
+ "The graph's cursor now has an updated end value.");
+
+ let thrown;
+ try {
+ graph.setCursor({ x: null, y: null });
+ } catch (e) {
+ thrown = true;
+ }
+ ok(thrown, "Setting a null cursor shouldn't work.");
+
+ ok(graph.hasCursor(),
+ "There should still be a cursor.");
+
+ graph.dropCursor();
+
+ ok(!graph.hasCursor(),
+ "There shouldn't be any cursor anymore.");
+ ok(graph.getCursor().x === null,
+ "The graph's cursor now has a null start value.");
+ ok(graph.getCursor().y === null,
+ "The graph's cursor now has a null end value.");
+}
diff --git a/devtools/client/shared/test/browser_graphs-04.js b/devtools/client/shared/test/browser_graphs-04.js
new file mode 100644
index 000000000..452b27c4a
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-04.js
@@ -0,0 +1,69 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that graph widgets can correctly compare selections and cursors.
+
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+ yield graph.once("ready");
+
+ testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function testGraph(graph) {
+ ok(!graph.hasSelection(),
+ "There shouldn't initially be any selection.");
+ is(graph.getSelectionWidth(), 0,
+ "The selection width should be 0 when there's no selection.");
+
+ graph.setSelection({ start: 100, end: 200 });
+
+ ok(graph.hasSelection(),
+ "There should now be a selection.");
+ is(graph.getSelectionWidth(), 100,
+ "The selection width should now be 100.");
+
+ ok(graph.isSelectionDifferent({ start: 100, end: 201 }),
+ "The selection was correctly reported to be different (1).");
+ ok(graph.isSelectionDifferent({ start: 101, end: 200 }),
+ "The selection was correctly reported to be different (2).");
+ ok(graph.isSelectionDifferent({ start: null, end: null }),
+ "The selection was correctly reported to be different (3).");
+ ok(graph.isSelectionDifferent(null),
+ "The selection was correctly reported to be different (4).");
+
+ ok(!graph.isSelectionDifferent({ start: 100, end: 200 }),
+ "The selection was incorrectly reported to be different (1).");
+ ok(!graph.isSelectionDifferent(graph.getSelection()),
+ "The selection was incorrectly reported to be different (2).");
+
+ graph.setCursor({ x: 100, y: 50 });
+
+ ok(graph.isCursorDifferent({ x: 100, y: 51 }),
+ "The cursor was correctly reported to be different (1).");
+ ok(graph.isCursorDifferent({ x: 101, y: 50 }),
+ "The cursor was correctly reported to be different (2).");
+ ok(graph.isCursorDifferent({ x: null, y: null }),
+ "The cursor was correctly reported to be different (3).");
+ ok(graph.isCursorDifferent(null),
+ "The cursor was correctly reported to be different (4).");
+
+ ok(!graph.isCursorDifferent({ x: 100, y: 50 }),
+ "The cursor was incorrectly reported to be different (1).");
+ ok(!graph.isCursorDifferent(graph.getCursor()),
+ "The cursor was incorrectly reported to be different (2).");
+}
diff --git a/devtools/client/shared/test/browser_graphs-05.js b/devtools/client/shared/test/browser_graphs-05.js
new file mode 100644
index 000000000..bd3da9128
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-05.js
@@ -0,0 +1,154 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that graph widgets can correctly determine which regions are hovered.
+
+const TEST_DATA = [
+ { delta: 112, value: 48 }, { delta: 213, value: 59 },
+ { delta: 313, value: 60 }, { delta: 413, value: 59 },
+ { delta: 530, value: 59 }, { delta: 646, value: 58 },
+ { delta: 747, value: 60 }, { delta: 863, value: 48 },
+ { delta: 980, value: 37 }, { delta: 1097, value: 30 },
+ { delta: 1213, value: 29 }, { delta: 1330, value: 23 },
+ { delta: 1430, value: 10 }, { delta: 1534, value: 17 },
+ { delta: 1645, value: 20 }, { delta: 1746, value: 22 },
+ { delta: 1846, value: 39 }, { delta: 1963, value: 26 },
+ { delta: 2080, value: 27 }, { delta: 2197, value: 35 },
+ { delta: 2312, value: 47 }, { delta: 2412, value: 53 },
+ { delta: 2514, value: 60 }, { delta: 2630, value: 37 },
+ { delta: 2730, value: 36 }, { delta: 2830, value: 37 },
+ { delta: 2946, value: 36 }, { delta: 3046, value: 40 },
+ { delta: 3163, value: 47 }, { delta: 3280, value: 41 },
+ { delta: 3380, value: 35 }, { delta: 3480, value: 27 },
+ { delta: 3580, value: 39 }, { delta: 3680, value: 42 },
+ { delta: 3780, value: 49 }, { delta: 3880, value: 55 },
+ { delta: 3980, value: 60 }, { delta: 4080, value: 60 },
+ { delta: 4180, value: 60 }
+];
+const TEST_REGIONS = [{ start: 320, end: 460 }, { start: 780, end: 860 }];
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+ yield graph.once("ready");
+
+ testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function testGraph(graph) {
+ ok(!graph.getHoveredRegion(),
+ "There should be no hovered region yet because there's no regions.");
+
+ ok(!graph._isHoveringStartBoundary(),
+ "The graph start boundary should not be hovered.");
+ ok(!graph._isHoveringEndBoundary(),
+ "The graph end boundary should not be hovered.");
+ ok(!graph._isHoveringSelectionContents(),
+ "The graph contents should not be hovered.");
+ ok(!graph._isHoveringSelectionContentsOrBoundaries(),
+ "The graph contents or boundaries should not be hovered.");
+
+ graph.setData(TEST_DATA);
+ graph.setRegions(TEST_REGIONS);
+
+ ok(!graph.getHoveredRegion(),
+ "There should be no hovered region yet because there's no cursor.");
+
+ graph.setCursor({ x: TEST_REGIONS[0].start * graph.dataScaleX - 1, y: 0 });
+ ok(!graph.getHoveredRegion(),
+ "There shouldn't be any hovered region yet.");
+
+ graph.setCursor({ x: TEST_REGIONS[0].start * graph.dataScaleX + 1, y: 0 });
+ ok(graph.getHoveredRegion(),
+ "There should be a hovered region now.");
+ is(graph.getHoveredRegion().start, 320 * graph.dataScaleX,
+ "The reported hovered region is correct (1).");
+ is(graph.getHoveredRegion().end, 460 * graph.dataScaleX,
+ "The reported hovered region is correct (2).");
+
+ graph.setSelection({ start: 100, end: 200 });
+
+ info("Setting cursor over the left boundary.");
+ graph.setCursor({ x: 100, y: 0 });
+
+ ok(graph._isHoveringStartBoundary(),
+ "The graph start boundary should be hovered.");
+ ok(!graph._isHoveringEndBoundary(),
+ "The graph end boundary should not be hovered.");
+ ok(!graph._isHoveringSelectionContents(),
+ "The graph contents should not be hovered.");
+ ok(graph._isHoveringSelectionContentsOrBoundaries(),
+ "The graph contents or boundaries should be hovered.");
+
+ info("Setting cursor near the left boundary.");
+ graph.setCursor({ x: 105, y: 0 });
+
+ ok(graph._isHoveringStartBoundary(),
+ "The graph start boundary should be hovered.");
+ ok(!graph._isHoveringEndBoundary(),
+ "The graph end boundary should not be hovered.");
+ ok(graph._isHoveringSelectionContents(),
+ "The graph contents should be hovered.");
+ ok(graph._isHoveringSelectionContentsOrBoundaries(),
+ "The graph contents or boundaries should be hovered.");
+
+ info("Setting cursor over the selection.");
+ graph.setCursor({ x: 150, y: 0 });
+
+ ok(!graph._isHoveringStartBoundary(),
+ "The graph start boundary should not be hovered.");
+ ok(!graph._isHoveringEndBoundary(),
+ "The graph end boundary should not be hovered.");
+ ok(graph._isHoveringSelectionContents(),
+ "The graph contents should be hovered.");
+ ok(graph._isHoveringSelectionContentsOrBoundaries(),
+ "The graph contents or boundaries should be hovered.");
+
+ info("Setting cursor near the right boundary.");
+ graph.setCursor({ x: 195, y: 0 });
+
+ ok(!graph._isHoveringStartBoundary(),
+ "The graph start boundary should not be hovered.");
+ ok(graph._isHoveringEndBoundary(),
+ "The graph end boundary should be hovered.");
+ ok(graph._isHoveringSelectionContents(),
+ "The graph contents should be hovered.");
+ ok(graph._isHoveringSelectionContentsOrBoundaries(),
+ "The graph contents or boundaries should be hovered.");
+
+ info("Setting cursor over the right boundary.");
+ graph.setCursor({ x: 200, y: 0 });
+
+ ok(!graph._isHoveringStartBoundary(),
+ "The graph start boundary should not be hovered.");
+ ok(graph._isHoveringEndBoundary(),
+ "The graph end boundary should be hovered.");
+ ok(!graph._isHoveringSelectionContents(),
+ "The graph contents should not be hovered.");
+ ok(graph._isHoveringSelectionContentsOrBoundaries(),
+ "The graph contents or boundaries should be hovered.");
+
+ info("Setting away from the selection.");
+ graph.setCursor({ x: 300, y: 0 });
+
+ ok(!graph._isHoveringStartBoundary(),
+ "The graph start boundary should not be hovered.");
+ ok(!graph._isHoveringEndBoundary(),
+ "The graph end boundary should not be hovered.");
+ ok(!graph._isHoveringSelectionContents(),
+ "The graph contents should not be hovered.");
+ ok(!graph._isHoveringSelectionContentsOrBoundaries(),
+ "The graph contents or boundaries should not be hovered.");
+}
diff --git a/devtools/client/shared/test/browser_graphs-06.js b/devtools/client/shared/test/browser_graphs-06.js
new file mode 100644
index 000000000..596fe7702
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-06.js
@@ -0,0 +1,112 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests if clicking on regions adds a selection spanning that region.
+
+const TEST_DATA = [
+ { delta: 112, value: 48 }, { delta: 213, value: 59 },
+ { delta: 313, value: 60 }, { delta: 413, value: 59 },
+ { delta: 530, value: 59 }, { delta: 646, value: 58 },
+ { delta: 747, value: 60 }, { delta: 863, value: 48 },
+ { delta: 980, value: 37 }, { delta: 1097, value: 30 },
+ { delta: 1213, value: 29 }, { delta: 1330, value: 23 },
+ { delta: 1430, value: 10 }, { delta: 1534, value: 17 },
+ { delta: 1645, value: 20 }, { delta: 1746, value: 22 },
+ { delta: 1846, value: 39 }, { delta: 1963, value: 26 },
+ { delta: 2080, value: 27 }, { delta: 2197, value: 35 },
+ { delta: 2312, value: 47 }, { delta: 2412, value: 53 },
+ { delta: 2514, value: 60 }, { delta: 2630, value: 37 },
+ { delta: 2730, value: 36 }, { delta: 2830, value: 37 },
+ { delta: 2946, value: 36 }, { delta: 3046, value: 40 },
+ { delta: 3163, value: 47 }, { delta: 3280, value: 41 },
+ { delta: 3380, value: 35 }, { delta: 3480, value: 27 },
+ { delta: 3580, value: 39 }, { delta: 3680, value: 42 },
+ { delta: 3780, value: 49 }, { delta: 3880, value: 55 },
+ { delta: 3980, value: 60 }, { delta: 4080, value: 60 },
+ { delta: 4180, value: 60 }
+];
+const TEST_REGIONS = [{ start: 320, end: 460 }, { start: 780, end: 860 }];
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+ yield graph.once("ready");
+
+ testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function testGraph(graph) {
+ graph.setData(TEST_DATA);
+ graph.setRegions(TEST_REGIONS);
+
+ click(graph, (graph._regions[0].start + graph._regions[0].end) / 2);
+ is(graph.getSelection().start, graph._regions[0].start,
+ "The first region is now selected (1).");
+ is(graph.getSelection().end, graph._regions[0].end,
+ "The first region is now selected (2).");
+
+ let min = map(graph.getSelection().start, 0, graph.width, 112, 4180);
+ let max = map(graph.getSelection().end, 0, graph.width, 112, 4180);
+ is(graph.getMappedSelection().min, min,
+ "The mapped selection's min value is correct (1).");
+ is(graph.getMappedSelection().max, max,
+ "The mapped selection's max value is correct (2).");
+
+ click(graph, (graph._regions[1].start + graph._regions[1].end) / 2);
+ is(graph.getSelection().start, graph._regions[1].start,
+ "The second region is now selected (1).");
+ is(graph.getSelection().end, graph._regions[1].end,
+ "The second region is now selected (2).");
+
+ min = map(graph.getSelection().start, 0, graph.width, 112, 4180);
+ max = map(graph.getSelection().end, 0, graph.width, 112, 4180);
+ is(graph.getMappedSelection().min, min,
+ "The mapped selection's min value is correct (3).");
+ is(graph.getMappedSelection().max, max,
+ "The mapped selection's max value is correct (4).");
+
+ graph.setSelection({ start: graph.width, end: 0 });
+ min = map(0, 0, graph.width, 112, 4180);
+ max = map(graph.width, 0, graph.width, 112, 4180);
+ is(graph.getMappedSelection().min, min,
+ "The mapped selection's min value is correct (5).");
+ is(graph.getMappedSelection().max, max,
+ "The mapped selection's max value is correct (6).");
+
+ graph.setSelection({ start: graph.width + 100, end: -100 });
+ min = map(0, 0, graph.width, 112, 4180);
+ max = map(graph.width, 0, graph.width, 112, 4180);
+ is(graph.getMappedSelection().min, min,
+ "The mapped selection's min value is correct (7).");
+ is(graph.getMappedSelection().max, max,
+ "The mapped selection's max value is correct (8).");
+}
+
+/**
+ * Maps a value from one range to another.
+ */
+function map(value, istart, istop, ostart, ostop) {
+ return ostart + (ostop - ostart) * ((value - istart) / (istop - istart));
+}
+
+// EventUtils just doesn't work!
+
+function click(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseDown({ testX: x, testY: y });
+ graph._onMouseUp({ testX: x, testY: y });
+}
diff --git a/devtools/client/shared/test/browser_graphs-07a.js b/devtools/client/shared/test/browser_graphs-07a.js
new file mode 100644
index 000000000..44e166cc5
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-07a.js
@@ -0,0 +1,232 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests if selecting, resizing, moving selections and zooming in/out works.
+
+const TEST_DATA = [
+ { delta: 112, value: 48 }, { delta: 213, value: 59 },
+ { delta: 313, value: 60 }, { delta: 413, value: 59 },
+ { delta: 530, value: 59 }, { delta: 646, value: 58 },
+ { delta: 747, value: 60 }, { delta: 863, value: 48 },
+ { delta: 980, value: 37 }, { delta: 1097, value: 30 },
+ { delta: 1213, value: 29 }, { delta: 1330, value: 23 },
+ { delta: 1430, value: 10 }, { delta: 1534, value: 17 },
+ { delta: 1645, value: 20 }, { delta: 1746, value: 22 },
+ { delta: 1846, value: 39 }, { delta: 1963, value: 26 },
+ { delta: 2080, value: 27 }, { delta: 2197, value: 35 },
+ { delta: 2312, value: 47 }, { delta: 2412, value: 53 },
+ { delta: 2514, value: 60 }, { delta: 2630, value: 37 },
+ { delta: 2730, value: 36 }, { delta: 2830, value: 37 },
+ { delta: 2946, value: 36 }, { delta: 3046, value: 40 },
+ { delta: 3163, value: 47 }, { delta: 3280, value: 41 },
+ { delta: 3380, value: 35 }, { delta: 3480, value: 27 },
+ { delta: 3580, value: 39 }, { delta: 3680, value: 42 },
+ { delta: 3780, value: 49 }, { delta: 3880, value: 55 },
+ { delta: 3980, value: 60 }, { delta: 4080, value: 60 },
+ { delta: 4180, value: 60 }
+];
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+ yield graph.once("ready");
+ testGraph(graph, normalDragStop);
+ yield graph.destroy();
+
+ let graph2 = new LineGraphWidget(doc.body, "fps");
+ yield graph2.once("ready");
+ testGraph(graph2, buggyDragStop);
+ yield graph2.destroy();
+
+ host.destroy();
+}
+
+function testGraph(graph, dragStop) {
+ graph.setData(TEST_DATA);
+
+ info("Making a selection.");
+
+ dragStart(graph, 300);
+ ok(graph.hasSelectionInProgress(),
+ "The selection should start (1).");
+ is(graph.getSelection().start, 300,
+ "The current selection start value is correct (1).");
+ is(graph.getSelection().end, 300,
+ "The current selection end value is correct (1).");
+
+ hover(graph, 400);
+ ok(graph.hasSelectionInProgress(),
+ "The selection should still be in progress (2).");
+ is(graph.getSelection().start, 300,
+ "The current selection start value is correct (2).");
+ is(graph.getSelection().end, 400,
+ "The current selection end value is correct (2).");
+
+ dragStop(graph, 500);
+ ok(!graph.hasSelectionInProgress(),
+ "The selection should have stopped (3).");
+ is(graph.getSelection().start, 300,
+ "The current selection start value is correct (3).");
+ is(graph.getSelection().end, 500,
+ "The current selection end value is correct (3).");
+
+ info("Making a new selection.");
+
+ dragStart(graph, 200);
+ ok(graph.hasSelectionInProgress(),
+ "The selection should start (4).");
+ is(graph.getSelection().start, 200,
+ "The current selection start value is correct (4).");
+ is(graph.getSelection().end, 200,
+ "The current selection end value is correct (4).");
+
+ hover(graph, 300);
+ ok(graph.hasSelectionInProgress(),
+ "The selection should still be in progress (5).");
+ is(graph.getSelection().start, 200,
+ "The current selection start value is correct (5).");
+ is(graph.getSelection().end, 300,
+ "The current selection end value is correct (5).");
+
+ dragStop(graph, 400);
+ ok(!graph.hasSelectionInProgress(),
+ "The selection should have stopped (6).");
+ is(graph.getSelection().start, 200,
+ "The current selection start value is correct (6).");
+ is(graph.getSelection().end, 400,
+ "The current selection end value is correct (6).");
+
+ info("Resizing by dragging the end handlebar.");
+
+ dragStart(graph, 400);
+ is(graph.getSelection().start, 200,
+ "The current selection start value is correct (7).");
+ is(graph.getSelection().end, 400,
+ "The current selection end value is correct (7).");
+
+ dragStop(graph, 600);
+ is(graph.getSelection().start, 200,
+ "The current selection start value is correct (8).");
+ is(graph.getSelection().end, 600,
+ "The current selection end value is correct (8).");
+
+ info("Resizing by dragging the start handlebar.");
+
+ dragStart(graph, 200);
+ is(graph.getSelection().start, 200,
+ "The current selection start value is correct (9).");
+ is(graph.getSelection().end, 600,
+ "The current selection end value is correct (9).");
+
+ dragStop(graph, 100);
+ is(graph.getSelection().start, 100,
+ "The current selection start value is correct (10).");
+ is(graph.getSelection().end, 600,
+ "The current selection end value is correct (10).");
+
+ info("Moving by dragging the selection.");
+
+ dragStart(graph, 300);
+ hover(graph, 400);
+ is(graph.getSelection().start, 200,
+ "The current selection start value is correct (11).");
+ is(graph.getSelection().end, 700,
+ "The current selection end value is correct (11).");
+
+ dragStop(graph, 500);
+ is(graph.getSelection().start, 300,
+ "The current selection start value is correct (12).");
+ is(graph.getSelection().end, 800,
+ "The current selection end value is correct (12).");
+
+ info("Zooming in by scrolling inside the selection.");
+
+ scroll(graph, -1000, 600);
+ is(graph.getSelection().start, 525,
+ "The current selection start value is correct (13).");
+ is(graph.getSelection().end, 650,
+ "The current selection end value is correct (13).");
+
+ info("Zooming out by scrolling inside the selection.");
+
+ scroll(graph, 1000, 600);
+ is(graph.getSelection().start, 468.75,
+ "The current selection start value is correct (14).");
+ is(graph.getSelection().end, 687.5,
+ "The current selection end value is correct (14).");
+
+ info("Sliding left by scrolling outside the selection.");
+
+ scroll(graph, 100, 900);
+ is(graph.getSelection().start, 458.75,
+ "The current selection start value is correct (15).");
+ is(graph.getSelection().end, 677.5,
+ "The current selection end value is correct (15).");
+
+ info("Sliding right by scrolling outside the selection.");
+
+ scroll(graph, -100, 900);
+ is(graph.getSelection().start, 468.75,
+ "The current selection start value is correct (16).");
+ is(graph.getSelection().end, 687.5,
+ "The current selection end value is correct (16).");
+
+ info("Zooming out a lot.");
+
+ scroll(graph, Number.MAX_SAFE_INTEGER, 500);
+ is(graph.getSelection().start, 1,
+ "The current selection start value is correct (17).");
+ is(graph.getSelection().end, graph.width - 1,
+ "The current selection end value is correct (17).");
+}
+
+// EventUtils just doesn't work!
+
+function hover(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+}
+
+function dragStart(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseDown({ testX: x, testY: y });
+}
+
+function normalDragStop(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseUp({ testX: x, testY: y });
+}
+
+function buggyDragStop(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+
+ graph._onMouseMove({ testX: x, testY: y });
+
+ // Only fire a mousemove with no buttons instead of a mouseup.
+ // This happens when the mouseup happens outside of the window.
+ // Send different coordinates to make sure the selection is preserved,
+ // see Bugs 1066504 and 1144779.
+ graph._onMouseMove({ testX: x + 1, testY: y + 1, buttons: 0 });
+}
+
+function scroll(graph, wheel, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseWheel({ testX: x, testY: y, detail: wheel });
+}
diff --git a/devtools/client/shared/test/browser_graphs-07b.js b/devtools/client/shared/test/browser_graphs-07b.js
new file mode 100644
index 000000000..3b4bc5740
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-07b.js
@@ -0,0 +1,88 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests if selections can't be added via clicking, while not allowed.
+
+const TEST_DATA = [
+ { delta: 112, value: 48 }, { delta: 213, value: 59 },
+ { delta: 313, value: 60 }, { delta: 413, value: 59 },
+ { delta: 530, value: 59 }, { delta: 646, value: 58 },
+ { delta: 747, value: 60 }, { delta: 863, value: 48 },
+ { delta: 980, value: 37 }, { delta: 1097, value: 30 },
+ { delta: 1213, value: 29 }, { delta: 1330, value: 23 },
+ { delta: 1430, value: 10 }, { delta: 1534, value: 17 },
+ { delta: 1645, value: 20 }, { delta: 1746, value: 22 },
+ { delta: 1846, value: 39 }, { delta: 1963, value: 26 },
+ { delta: 2080, value: 27 }, { delta: 2197, value: 35 },
+ { delta: 2312, value: 47 }, { delta: 2412, value: 53 },
+ { delta: 2514, value: 60 }, { delta: 2630, value: 37 },
+ { delta: 2730, value: 36 }, { delta: 2830, value: 37 },
+ { delta: 2946, value: 36 }, { delta: 3046, value: 40 },
+ { delta: 3163, value: 47 }, { delta: 3280, value: 41 },
+ { delta: 3380, value: 35 }, { delta: 3480, value: 27 },
+ { delta: 3580, value: 39 }, { delta: 3680, value: 42 },
+ { delta: 3780, value: 49 }, { delta: 3880, value: 55 },
+ { delta: 3980, value: 60 }, { delta: 4080, value: 60 },
+ { delta: 4180, value: 60 }
+];
+var LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+ yield graph.once("ready");
+
+ testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function testGraph(graph) {
+ graph.setData(TEST_DATA);
+ graph.selectionEnabled = false;
+
+ info("Attempting to make a selection.");
+
+ dragStart(graph, 300);
+ is(graph.hasSelection() || graph.hasSelectionInProgress(), false,
+ "The graph shouldn't have a selection (1).");
+
+ hover(graph, 400);
+ is(graph.hasSelection() || graph.hasSelectionInProgress(), false,
+ "The graph shouldn't have a selection (2).");
+
+ dragStop(graph, 500);
+ is(graph.hasSelection() || graph.hasSelectionInProgress(), false,
+ "The graph shouldn't have a selection (3).");
+}
+
+// EventUtils just doesn't work!
+
+function hover(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+}
+
+function dragStart(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseDown({ testX: x, testY: y });
+}
+
+function dragStop(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseUp({ testX: x, testY: y });
+}
diff --git a/devtools/client/shared/test/browser_graphs-07c.js b/devtools/client/shared/test/browser_graphs-07c.js
new file mode 100644
index 000000000..1791ced12
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-07c.js
@@ -0,0 +1,139 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests if movement via event dispatching using screenX / screenY
+// works. All of the other tests directly use the graph's mouse event
+// callbacks with textX / testY for convenience.
+
+const TEST_DATA = [
+ { delta: 112, value: 48 }, { delta: 213, value: 59 },
+ { delta: 313, value: 60 }, { delta: 413, value: 59 },
+ { delta: 530, value: 59 }, { delta: 646, value: 58 },
+ { delta: 747, value: 60 }, { delta: 863, value: 48 },
+ { delta: 980, value: 37 }, { delta: 1097, value: 30 },
+ { delta: 1213, value: 29 }, { delta: 1330, value: 23 },
+ { delta: 1430, value: 10 }, { delta: 1534, value: 17 },
+ { delta: 1645, value: 20 }, { delta: 1746, value: 22 },
+ { delta: 1846, value: 39 }, { delta: 1963, value: 26 },
+ { delta: 2080, value: 27 }, { delta: 2197, value: 35 },
+ { delta: 2312, value: 47 }, { delta: 2412, value: 53 },
+ { delta: 2514, value: 60 }, { delta: 2630, value: 37 },
+ { delta: 2730, value: 36 }, { delta: 2830, value: 37 },
+ { delta: 2946, value: 36 }, { delta: 3046, value: 40 },
+ { delta: 3163, value: 47 }, { delta: 3280, value: 41 },
+ { delta: 3380, value: 35 }, { delta: 3480, value: 27 },
+ { delta: 3580, value: 39 }, { delta: 3680, value: 42 },
+ { delta: 3780, value: 49 }, { delta: 3880, value: 55 },
+ { delta: 3980, value: 60 }, { delta: 4080, value: 60 },
+ { delta: 4180, value: 60 }
+];
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+ yield graph.once("ready");
+ testGraph(graph);
+ yield graph.destroy();
+ host.destroy();
+}
+
+function testGraph(graph) {
+ graph.setData(TEST_DATA);
+
+ info("Making a selection.");
+
+ dragStart(graph, 300);
+ ok(graph.hasSelectionInProgress(),
+ "The selection should start (1).");
+ is(graph.getSelection().start, 300,
+ "The current selection start value is correct (1).");
+ is(graph.getSelection().end, 300,
+ "The current selection end value is correct (1).");
+
+ hover(graph, 400);
+ ok(graph.hasSelectionInProgress(),
+ "The selection should still be in progress (2).");
+ is(graph.getSelection().start, 300,
+ "The current selection start value is correct (2).");
+ is(graph.getSelection().end, 400,
+ "The current selection end value is correct (2).");
+
+ dragStop(graph, 500);
+ ok(!graph.hasSelectionInProgress(),
+ "The selection should have stopped (3).");
+ is(graph.getSelection().start, 300,
+ "The current selection start value is correct (3).");
+ is(graph.getSelection().end, 500,
+ "The current selection end value is correct (3).");
+
+ info("Making a new selection.");
+
+ dragStart(graph, 200);
+ ok(graph.hasSelectionInProgress(),
+ "The selection should start (4).");
+ is(graph.getSelection().start, 200,
+ "The current selection start value is correct (4).");
+ is(graph.getSelection().end, 200,
+ "The current selection end value is correct (4).");
+
+ hover(graph, 300);
+ ok(graph.hasSelectionInProgress(),
+ "The selection should still be in progress (5).");
+ is(graph.getSelection().start, 200,
+ "The current selection start value is correct (5).");
+ is(graph.getSelection().end, 300,
+ "The current selection end value is correct (5).");
+
+ dragStop(graph, 400);
+ ok(!graph.hasSelectionInProgress(),
+ "The selection should have stopped (6).");
+ is(graph.getSelection().start, 200,
+ "The current selection start value is correct (6).");
+ is(graph.getSelection().end, 400,
+ "The current selection end value is correct (6).");
+}
+
+// EventUtils just doesn't work!
+
+function dispatchEvent(graph, x, y, type) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ let quad = graph._canvas.getBoxQuads({
+ relativeTo: window.document
+ })[0];
+
+ let screenX = window.screenX + quad.p1.x + x;
+ let screenY = window.screenY + quad.p1.y + y;
+
+ graph._canvas.dispatchEvent(new MouseEvent(type, {
+ bubbles: true,
+ cancelable: true,
+ buttons: 1,
+ view: window,
+ screenX: screenX,
+ screenY: screenY,
+ }));
+}
+
+function hover(graph, x, y = 1) {
+ dispatchEvent(graph, x, y, "mousemove");
+}
+
+function dragStart(graph, x, y = 1) {
+ dispatchEvent(graph, x, y, "mousemove");
+ dispatchEvent(graph, x, y, "mousedown");
+}
+
+function dragStop(graph, x, y = 1) {
+ dispatchEvent(graph, x, y, "mousemove");
+ dispatchEvent(graph, x, y, "mouseup");
+}
diff --git a/devtools/client/shared/test/browser_graphs-07d.js b/devtools/client/shared/test/browser_graphs-07d.js
new file mode 100644
index 000000000..8a64f7d75
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-07d.js
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that selections are drawn onto the canvas.
+
+const TEST_DATA = [
+ { delta: 112, value: 48 }, { delta: 213, value: 59 },
+ { delta: 313, value: 60 }, { delta: 413, value: 59 },
+ { delta: 530, value: 59 }, { delta: 646, value: 58 },
+ { delta: 747, value: 60 }, { delta: 863, value: 48 },
+ { delta: 980, value: 37 }, { delta: 1097, value: 30 },
+ { delta: 1213, value: 29 }, { delta: 1330, value: 23 },
+ { delta: 1430, value: 10 }, { delta: 1534, value: 17 },
+ { delta: 1645, value: 20 }, { delta: 1746, value: 22 },
+ { delta: 1846, value: 39 }, { delta: 1963, value: 26 },
+ { delta: 2080, value: 27 }, { delta: 2197, value: 35 },
+ { delta: 2312, value: 47 }, { delta: 2412, value: 53 },
+ { delta: 2514, value: 60 }, { delta: 2630, value: 37 },
+ { delta: 2730, value: 36 }, { delta: 2830, value: 37 },
+ { delta: 2946, value: 36 }, { delta: 3046, value: 40 },
+ { delta: 3163, value: 47 }, { delta: 3280, value: 41 },
+ { delta: 3380, value: 35 }, { delta: 3480, value: 27 },
+ { delta: 3580, value: 39 }, { delta: 3680, value: 42 },
+ { delta: 3780, value: 49 }, { delta: 3880, value: 55 },
+ { delta: 3980, value: 60 }, { delta: 4080, value: 60 },
+ { delta: 4180, value: 60 }
+];
+const TEST_REGIONS = [{ start: 320, end: 460 }, { start: 780, end: 860 }];
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+ yield graph.once("ready");
+
+ testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function testGraph(graph) {
+ graph.setData(TEST_DATA);
+ graph.setRegions(TEST_REGIONS);
+
+ // Measure the color of the first pixel before any selection is made.
+ graph._onAnimationFrame();
+ let pixelNoSelection = graph._ctx.getImageData(1, 1, 1, 1).data;
+
+ graph.setSelection({ start: 0, end: 10 });
+ graph._onAnimationFrame();
+ let pixelNormalSelection = graph._ctx.getImageData(1, 1, 1, 1).data;
+
+ Assert.notDeepEqual(pixelNormalSelection, pixelNoSelection,
+ "The first pixel is part of the drawn selection.");
+
+ graph.setSelection({ start: graph.width + 100, end: -100 });
+ graph._onAnimationFrame();
+ let pixelFullSelection = graph._ctx.getImageData(1, 1, 1, 1).data;
+
+ Assert.deepEqual(pixelFullSelection, pixelNormalSelection,
+ "The first pixel is still part of the drawn selection.");
+}
diff --git a/devtools/client/shared/test/browser_graphs-07e.js b/devtools/client/shared/test/browser_graphs-07e.js
new file mode 100644
index 000000000..814284b9d
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-07e.js
@@ -0,0 +1,127 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that selections are drawn onto the canvas.
+
+const TEST_DATA = [
+ { delta: 112, value: 48 }, { delta: 213, value: 59 },
+ { delta: 313, value: 60 }, { delta: 413, value: 59 },
+ { delta: 530, value: 59 }, { delta: 646, value: 58 },
+ { delta: 747, value: 60 }, { delta: 863, value: 48 },
+ { delta: 980, value: 37 }, { delta: 1097, value: 30 },
+ { delta: 1213, value: 29 }, { delta: 1330, value: 23 },
+ { delta: 1430, value: 10 }, { delta: 1534, value: 17 },
+ { delta: 1645, value: 20 }, { delta: 1746, value: 22 },
+ { delta: 1846, value: 39 }, { delta: 1963, value: 26 },
+ { delta: 2080, value: 27 }, { delta: 2197, value: 35 },
+ { delta: 2312, value: 47 }, { delta: 2412, value: 53 },
+ { delta: 2514, value: 60 }, { delta: 2630, value: 37 },
+ { delta: 2730, value: 36 }, { delta: 2830, value: 37 },
+ { delta: 2946, value: 36 }, { delta: 3046, value: 40 },
+ { delta: 3163, value: 47 }, { delta: 3280, value: 41 },
+ { delta: 3380, value: 35 }, { delta: 3480, value: 27 },
+ { delta: 3580, value: 39 }, { delta: 3680, value: 42 },
+ { delta: 3780, value: 49 }, { delta: 3880, value: 55 },
+ { delta: 3980, value: 60 }, { delta: 4080, value: 60 },
+ { delta: 4180, value: 60 }
+];
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+let CURRENT_ZOOM = 1;
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+ yield graph.once("ready");
+ graph.setData(TEST_DATA);
+
+ info("Testing with normal zoom.");
+ testGraph(graph);
+
+ info("Testing while zoomed out.");
+ setZoom(host.frame, .5);
+ testGraph(graph);
+
+ info("Testing while zoomed in.");
+ setZoom(host.frame, 2);
+ testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function testGraph(graph) {
+ graph.dropSelection();
+
+ info("Making a selection.");
+
+ dragStart(graph, 100);
+ ok(graph.hasSelectionInProgress(),
+ "The selection should start (1).");
+ is(graph.getSelection().start, 100,
+ "The current selection start value is correct (1).");
+ is(graph.getSelection().end, 100,
+ "The current selection end value is correct (1).");
+
+ hover(graph, 200);
+ ok(graph.hasSelectionInProgress(),
+ "The selection should still be in progress (2).");
+ is(graph.getSelection().start, 100,
+ "The current selection start value is correct (2).");
+ is(graph.getSelection().end, 200,
+ "The current selection end value is correct (2).");
+
+ dragStop(graph, 300);
+ ok(!graph.hasSelectionInProgress(),
+ "The selection should have stopped (3).");
+ is(graph.getSelection().start, 100,
+ "The current selection start value is correct (3).");
+ is(graph.getSelection().end, 300,
+ "The current selection end value is correct (3).");
+}
+
+function setZoom(frame, zoomValue) {
+ let contViewer = frame.docShell.contentViewer;
+ CURRENT_ZOOM = contViewer.fullZoom = zoomValue;
+}
+
+// EventUtils just doesn't work!
+
+function dispatchEvent(graph, x, y, fn) {
+ x *= CURRENT_ZOOM;
+ y *= CURRENT_ZOOM;
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ let quad = graph._canvas.getBoxQuads({
+ relativeTo: window.document
+ })[0];
+
+ let screenX = (window.screenX + quad.p1.x + x);
+ let screenY = (window.screenY + quad.p1.y + y);
+
+ fn({
+ screenX: screenX,
+ screenY: screenY,
+ });
+}
+
+function hover(graph, x, y = 1) {
+ dispatchEvent(graph, x, y, graph._onMouseMove);
+}
+
+function dragStart(graph, x, y = 1) {
+ dispatchEvent(graph, x, y, graph._onMouseMove);
+ dispatchEvent(graph, x, y, graph._onMouseDown);
+}
+
+function dragStop(graph, x, y = 1) {
+ dispatchEvent(graph, x, y, graph._onMouseMove);
+ dispatchEvent(graph, x, y, graph._onMouseUp);
+}
diff --git a/devtools/client/shared/test/browser_graphs-08.js b/devtools/client/shared/test/browser_graphs-08.js
new file mode 100644
index 000000000..ae6b54fb5
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-08.js
@@ -0,0 +1,88 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests if a selection is dropped when clicking outside of it.
+
+const TEST_DATA = [
+ { delta: 112, value: 48 }, { delta: 213, value: 59 },
+ { delta: 313, value: 60 }, { delta: 413, value: 59 },
+ { delta: 530, value: 59 }, { delta: 646, value: 58 },
+ { delta: 747, value: 60 }, { delta: 863, value: 48 },
+ { delta: 980, value: 37 }, { delta: 1097, value: 30 },
+ { delta: 1213, value: 29 }, { delta: 1330, value: 23 },
+ { delta: 1430, value: 10 }, { delta: 1534, value: 17 },
+ { delta: 1645, value: 20 }, { delta: 1746, value: 22 },
+ { delta: 1846, value: 39 }, { delta: 1963, value: 26 },
+ { delta: 2080, value: 27 }, { delta: 2197, value: 35 },
+ { delta: 2312, value: 47 }, { delta: 2412, value: 53 },
+ { delta: 2514, value: 60 }, { delta: 2630, value: 37 },
+ { delta: 2730, value: 36 }, { delta: 2830, value: 37 },
+ { delta: 2946, value: 36 }, { delta: 3046, value: 40 },
+ { delta: 3163, value: 47 }, { delta: 3280, value: 41 },
+ { delta: 3380, value: 35 }, { delta: 3480, value: 27 },
+ { delta: 3580, value: 39 }, { delta: 3680, value: 42 },
+ { delta: 3780, value: 49 }, { delta: 3880, value: 55 },
+ { delta: 3980, value: 60 }, { delta: 4080, value: 60 },
+ { delta: 4180, value: 60 }
+];
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+ yield graph.once("ready");
+
+ testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function testGraph(graph) {
+ graph.setData(TEST_DATA);
+
+ dragStart(graph, 300);
+ dragStop(graph, 500);
+ ok(graph.hasSelection(),
+ "A selection should be available.");
+ is(graph.getSelection().start, 300,
+ "The current selection start value is correct.");
+ is(graph.getSelection().end, 500,
+ "The current selection end value is correct.");
+
+ click(graph, 600);
+ ok(!graph.hasSelection(),
+ "The selection should be dropped.");
+}
+
+// EventUtils just doesn't work!
+
+function click(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseDown({ testX: x, testY: y });
+ graph._onMouseUp({ testX: x, testY: y });
+}
+
+function dragStart(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseDown({ testX: x, testY: y });
+}
+
+function dragStop(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseUp({ testX: x, testY: y });
+}
diff --git a/devtools/client/shared/test/browser_graphs-09a.js b/devtools/client/shared/test/browser_graphs-09a.js
new file mode 100644
index 000000000..8e6e65c24
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-09a.js
@@ -0,0 +1,104 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that line graphs properly create the gutter and tooltips.
+
+const TEST_DATA = [
+ { delta: 112, value: 48 }, { delta: 213, value: 59 },
+ { delta: 313, value: 60 }, { delta: 413, value: 59 },
+ { delta: 530, value: 59 }, { delta: 646, value: 58 },
+ { delta: 747, value: 60 }, { delta: 863, value: 48 },
+ { delta: 980, value: 37 }, { delta: 1097, value: 30 },
+ { delta: 1213, value: 29 }, { delta: 1330, value: 23 },
+ { delta: 1430, value: 10 }, { delta: 1534, value: 17 },
+ { delta: 1645, value: 20 }, { delta: 1746, value: 22 },
+ { delta: 1846, value: 39 }, { delta: 1963, value: 26 },
+ { delta: 2080, value: 27 }, { delta: 2197, value: 35 },
+ { delta: 2312, value: 47 }, { delta: 2412, value: 53 },
+ { delta: 2514, value: 60 }, { delta: 2630, value: 37 },
+ { delta: 2730, value: 36 }, { delta: 2830, value: 37 },
+ { delta: 2946, value: 36 }, { delta: 3046, value: 40 },
+ { delta: 3163, value: 47 }, { delta: 3280, value: 41 },
+ { delta: 3380, value: 35 }, { delta: 3480, value: 27 },
+ { delta: 3580, value: 39 }, { delta: 3680, value: 42 },
+ { delta: 3780, value: 49 }, { delta: 3880, value: 55 },
+ { delta: 3980, value: 60 }, { delta: 4080, value: 60 },
+ { delta: 4180, value: 60 }
+];
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, { metric: "fps" });
+
+ yield testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function* testGraph(graph) {
+ info("Should be able to set the graph data before waiting for the ready event.");
+
+ yield graph.setDataWhenReady(TEST_DATA);
+ ok(graph.hasData(), "Data was set successfully.");
+
+ is(graph._gutter.hidden, false,
+ "The gutter should not be hidden because the tooltips have arrows.");
+ is(graph._maxTooltip.hidden, false,
+ "The max tooltip should not be hidden.");
+ is(graph._avgTooltip.hidden, false,
+ "The avg tooltip should not be hidden.");
+ is(graph._minTooltip.hidden, false,
+ "The min tooltip should not be hidden.");
+
+ is(graph._maxTooltip.getAttribute("with-arrows"), "true",
+ "The maximum tooltip has the correct 'with-arrows' attribute.");
+ is(graph._avgTooltip.getAttribute("with-arrows"), "true",
+ "The average tooltip has the correct 'with-arrows' attribute.");
+ is(graph._minTooltip.getAttribute("with-arrows"), "true",
+ "The minimum tooltip has the correct 'with-arrows' attribute.");
+
+ is(graph._maxTooltip.querySelector("[text=info]").textContent, "max",
+ "The maximum tooltip displays the correct info.");
+ is(graph._avgTooltip.querySelector("[text=info]").textContent, "avg",
+ "The average tooltip displays the correct info.");
+ is(graph._minTooltip.querySelector("[text=info]").textContent, "min",
+ "The minimum tooltip displays the correct info.");
+
+ is(graph._maxTooltip.querySelector("[text=value]").textContent, "60",
+ "The maximum tooltip displays the correct value.");
+ is(graph._avgTooltip.querySelector("[text=value]").textContent, "41.72",
+ "The average tooltip displays the correct value.");
+ is(graph._minTooltip.querySelector("[text=value]").textContent, "10",
+ "The minimum tooltip displays the correct value.");
+
+ is(graph._maxTooltip.querySelector("[text=metric]").textContent, "fps",
+ "The maximum tooltip displays the correct metric.");
+ is(graph._avgTooltip.querySelector("[text=metric]").textContent, "fps",
+ "The average tooltip displays the correct metric.");
+ is(graph._minTooltip.querySelector("[text=metric]").textContent, "fps",
+ "The minimum tooltip displays the correct metric.");
+
+ is(parseInt(graph._maxTooltip.style.top, 10), 22,
+ "The maximum tooltip is positioned correctly.");
+ is(parseInt(graph._avgTooltip.style.top, 10), 61,
+ "The average tooltip is positioned correctly.");
+ is(parseInt(graph._minTooltip.style.top, 10), 128,
+ "The minimum tooltip is positioned correctly.");
+
+ is(parseInt(graph._maxGutterLine.style.top, 10), 22,
+ "The maximum gutter line is positioned correctly.");
+ is(parseInt(graph._avgGutterLine.style.top, 10), 61,
+ "The average gutter line is positioned correctly.");
+ is(parseInt(graph._minGutterLine.style.top, 10), 128,
+ "The minimum gutter line is positioned correctly.");
+}
diff --git a/devtools/client/shared/test/browser_graphs-09b.js b/devtools/client/shared/test/browser_graphs-09b.js
new file mode 100644
index 000000000..58dc552fb
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-09b.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that line graphs properly use the tooltips configuration properties.
+
+const TEST_DATA = [
+ { delta: 112, value: 48 }, { delta: 213, value: 59 },
+ { delta: 313, value: 60 }, { delta: 413, value: 59 },
+ { delta: 530, value: 59 }, { delta: 646, value: 58 },
+ { delta: 747, value: 60 }, { delta: 863, value: 48 },
+ { delta: 980, value: 37 }, { delta: 1097, value: 30 },
+ { delta: 1213, value: 29 }, { delta: 1330, value: 23 },
+ { delta: 1430, value: 10 }, { delta: 1534, value: 17 },
+ { delta: 1645, value: 20 }, { delta: 1746, value: 22 },
+ { delta: 1846, value: 39 }, { delta: 1963, value: 26 },
+ { delta: 2080, value: 27 }, { delta: 2197, value: 35 },
+ { delta: 2312, value: 47 }, { delta: 2412, value: 53 },
+ { delta: 2514, value: 60 }, { delta: 2630, value: 37 },
+ { delta: 2730, value: 36 }, { delta: 2830, value: 37 },
+ { delta: 2946, value: 36 }, { delta: 3046, value: 40 },
+ { delta: 3163, value: 47 }, { delta: 3280, value: 41 },
+ { delta: 3380, value: 35 }, { delta: 3480, value: 27 },
+ { delta: 3580, value: 39 }, { delta: 3680, value: 42 },
+ { delta: 3780, value: 49 }, { delta: 3880, value: 55 },
+ { delta: 3980, value: 60 }, { delta: 4080, value: 60 },
+ { delta: 4180, value: 60 }
+];
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+ graph.withTooltipArrows = false;
+ graph.withFixedTooltipPositions = true;
+
+ yield testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function* testGraph(graph) {
+ yield graph.setDataWhenReady(TEST_DATA);
+
+ is(graph._gutter.hidden, false,
+ "The gutter should be visible even if the tooltips don't have arrows.");
+ is(graph._maxTooltip.hidden, false,
+ "The max tooltip should not be hidden.");
+ is(graph._avgTooltip.hidden, false,
+ "The avg tooltip should not be hidden.");
+ is(graph._minTooltip.hidden, false,
+ "The min tooltip should not be hidden.");
+
+ is(graph._maxTooltip.getAttribute("with-arrows"), "false",
+ "The maximum tooltip has the correct 'with-arrows' attribute.");
+ is(graph._avgTooltip.getAttribute("with-arrows"), "false",
+ "The average tooltip has the correct 'with-arrows' attribute.");
+ is(graph._minTooltip.getAttribute("with-arrows"), "false",
+ "The minimum tooltip has the correct 'with-arrows' attribute.");
+
+ is(parseInt(graph._maxTooltip.style.top, 10), 8,
+ "The maximum tooltip is positioned correctly.");
+ is(parseInt(graph._avgTooltip.style.top, 10), 8,
+ "The average tooltip is positioned correctly.");
+ is(parseInt(graph._minTooltip.style.top, 10), 142,
+ "The minimum tooltip is positioned correctly.");
+
+ is(parseInt(graph._maxGutterLine.style.top, 10), 22,
+ "The maximum gutter line is positioned correctly.");
+ is(parseInt(graph._avgGutterLine.style.top, 10), 61,
+ "The average gutter line is positioned correctly.");
+ is(parseInt(graph._minGutterLine.style.top, 10), 128,
+ "The minimum gutter line is positioned correctly.");
+}
diff --git a/devtools/client/shared/test/browser_graphs-09c.js b/devtools/client/shared/test/browser_graphs-09c.js
new file mode 100644
index 000000000..ba3d3c1c4
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-09c.js
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that line graphs hide the tooltips when there's no data available.
+
+const TEST_DATA = [];
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+
+ yield testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function* testGraph(graph) {
+ yield graph.setDataWhenReady(TEST_DATA);
+
+ is(graph._gutter.hidden, true,
+ "The gutter should be hidden, since there's no data available.");
+ is(graph._maxTooltip.hidden, true,
+ "The max tooltip should be hidden.");
+ is(graph._avgTooltip.hidden, true,
+ "The avg tooltip should be hidden.");
+ is(graph._minTooltip.hidden, true,
+ "The min tooltip should be hidden.");
+}
diff --git a/devtools/client/shared/test/browser_graphs-09d.js b/devtools/client/shared/test/browser_graphs-09d.js
new file mode 100644
index 000000000..7d0c01c15
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-09d.js
@@ -0,0 +1,39 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that line graphs hide the 'max' tooltip when the distance between
+// the 'min' and 'max' tooltip is too small.
+
+const TEST_DATA = [{ delta: 100, value: 60 }, { delta: 200, value: 59.9 }];
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+
+ yield testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function* testGraph(graph) {
+ yield graph.setDataWhenReady(TEST_DATA);
+
+ is(graph._gutter.hidden, false,
+ "The gutter should not be hidden.");
+ is(graph._maxTooltip.hidden, true,
+ "The max tooltip should be hidden.");
+ is(graph._avgTooltip.hidden, false,
+ "The avg tooltip should not be hidden.");
+ is(graph._minTooltip.hidden, false,
+ "The min tooltip should not be hidden.");
+}
diff --git a/devtools/client/shared/test/browser_graphs-09e.js b/devtools/client/shared/test/browser_graphs-09e.js
new file mode 100644
index 000000000..72f2f7bdd
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-09e.js
@@ -0,0 +1,84 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that line graphs hide the gutter and tooltips when there's no data,
+// but show them when there is.
+
+const NO_DATA = [];
+const TEST_DATA = [
+ { delta: 112, value: 48 }, { delta: 213, value: 59 },
+ { delta: 313, value: 60 }, { delta: 413, value: 59 },
+ { delta: 530, value: 59 }, { delta: 646, value: 58 },
+ { delta: 747, value: 60 }, { delta: 863, value: 48 },
+ { delta: 980, value: 37 }, { delta: 1097, value: 30 },
+ { delta: 1213, value: 29 }, { delta: 1330, value: 23 },
+ { delta: 1430, value: 10 }, { delta: 1534, value: 17 },
+ { delta: 1645, value: 20 }, { delta: 1746, value: 22 },
+ { delta: 1846, value: 39 }, { delta: 1963, value: 26 },
+ { delta: 2080, value: 27 }, { delta: 2197, value: 35 },
+ { delta: 2312, value: 47 }, { delta: 2412, value: 53 },
+ { delta: 2514, value: 60 }, { delta: 2630, value: 37 },
+ { delta: 2730, value: 36 }, { delta: 2830, value: 37 },
+ { delta: 2946, value: 36 }, { delta: 3046, value: 40 },
+ { delta: 3163, value: 47 }, { delta: 3280, value: 41 },
+ { delta: 3380, value: 35 }, { delta: 3480, value: 27 },
+ { delta: 3580, value: 39 }, { delta: 3680, value: 42 },
+ { delta: 3780, value: 49 }, { delta: 3880, value: 55 },
+ { delta: 3980, value: 60 }, { delta: 4080, value: 60 },
+ { delta: 4180, value: 60 }
+];
+
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+
+ yield testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function* testGraph(graph) {
+ yield graph.setDataWhenReady(NO_DATA);
+
+ is(graph._gutter.hidden, true,
+ "The gutter should be hidden when there's no data available.");
+ is(graph._maxTooltip.hidden, true,
+ "The max tooltip should be hidden when there's no data available.");
+ is(graph._avgTooltip.hidden, true,
+ "The avg tooltip should be hidden when there's no data available.");
+ is(graph._minTooltip.hidden, true,
+ "The min tooltip should be hidden when there's no data available.");
+
+ yield graph.setDataWhenReady(TEST_DATA);
+
+ is(graph._gutter.hidden, false,
+ "The gutter should be visible now.");
+ is(graph._maxTooltip.hidden, false,
+ "The max tooltip should be visible now.");
+ is(graph._avgTooltip.hidden, false,
+ "The avg tooltip should be visible now.");
+ is(graph._minTooltip.hidden, false,
+ "The min tooltip should be visible now.");
+
+ yield graph.setDataWhenReady(NO_DATA);
+
+ is(graph._gutter.hidden, true,
+ "The gutter should be hidden again.");
+ is(graph._maxTooltip.hidden, true,
+ "The max tooltip should be hidden again.");
+ is(graph._avgTooltip.hidden, true,
+ "The avg tooltip should be hidden again.");
+ is(graph._minTooltip.hidden, true,
+ "The min tooltip should be hidden again.");
+}
diff --git a/devtools/client/shared/test/browser_graphs-09f.js b/devtools/client/shared/test/browser_graphs-09f.js
new file mode 100644
index 000000000..32b5819b2
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-09f.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests the constructor options for `min`, `max` and `avg` on displaying the
+// gutter/tooltips and lines.
+
+const TEST_DATA = [{ delta: 100, value: 60 }, { delta: 200, value: 1 }];
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+
+ yield testGraph(doc.body, { avg: false });
+ yield testGraph(doc.body, { min: false });
+ yield testGraph(doc.body, { max: false });
+ yield testGraph(doc.body, { min: false, max: false, avg: false });
+ yield testGraph(doc.body, {});
+
+ host.destroy();
+}
+
+function* testGraph(parent, options) {
+ options.metric = "fps";
+ let graph = new LineGraphWidget(parent, options);
+ yield graph.setDataWhenReady(TEST_DATA);
+ let shouldGutterShow = options.min === false && options.max === false;
+
+ is(graph._gutter.hidden, shouldGutterShow,
+ `The gutter should ${shouldGutterShow ? "" : "not "}be shown`);
+
+ is(graph._maxTooltip.hidden, options.max === false,
+ `The max tooltip should ${options.max === false ? "not " : ""}be shown`);
+ is(graph._maxGutterLine.hidden, options.max === false,
+ `The max gutter should ${options.max === false ? "not " : ""}be shown`);
+ is(graph._minTooltip.hidden, options.min === false,
+ `The min tooltip should ${options.min === false ? "not " : ""}be shown`);
+ is(graph._minGutterLine.hidden, options.min === false,
+ `The min gutter should ${options.min === false ? "not " : ""}be shown`);
+ is(graph._avgTooltip.hidden, options.avg === false,
+ `The avg tooltip should ${options.avg === false ? "not " : ""}be shown`);
+ is(graph._avgGutterLine.hidden, options.avg === false,
+ `The avg gutter should ${options.avg === false ? "not " : ""}be shown`);
+
+ yield graph.destroy();
+}
diff --git a/devtools/client/shared/test/browser_graphs-10a.js b/devtools/client/shared/test/browser_graphs-10a.js
new file mode 100644
index 000000000..7f66156f4
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-10a.js
@@ -0,0 +1,162 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that graphs properly handle resizing.
+
+const TEST_DATA = [
+ { delta: 112, value: 48 }, { delta: 213, value: 59 },
+ { delta: 313, value: 60 }, { delta: 413, value: 59 },
+ { delta: 530, value: 59 }, { delta: 646, value: 58 },
+ { delta: 747, value: 60 }, { delta: 863, value: 48 },
+ { delta: 980, value: 37 }, { delta: 1097, value: 30 },
+ { delta: 1213, value: 29 }, { delta: 1330, value: 23 },
+ { delta: 1430, value: 10 }, { delta: 1534, value: 17 },
+ { delta: 1645, value: 20 }, { delta: 1746, value: 22 },
+ { delta: 1846, value: 39 }, { delta: 1963, value: 26 },
+ { delta: 2080, value: 27 }, { delta: 2197, value: 35 },
+ { delta: 2312, value: 47 }, { delta: 2412, value: 53 },
+ { delta: 2514, value: 60 }, { delta: 2630, value: 37 },
+ { delta: 2730, value: 36 }, { delta: 2830, value: 37 },
+ { delta: 2946, value: 36 }, { delta: 3046, value: 40 },
+ { delta: 3163, value: 47 }, { delta: 3280, value: 41 },
+ { delta: 3380, value: 35 }, { delta: 3480, value: 27 },
+ { delta: 3580, value: 39 }, { delta: 3680, value: 42 },
+ { delta: 3780, value: 49 }, { delta: 3880, value: 55 },
+ { delta: 3980, value: 60 }, { delta: 4080, value: 60 },
+ { delta: 4180, value: 60 }
+];
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost("window");
+ doc.body.setAttribute("style",
+ "position: fixed; width: 100%; height: 100%; margin: 0;");
+
+ let graph = new LineGraphWidget(doc.body, "fps");
+ yield graph.once("ready");
+
+ let refreshCount = 0;
+ graph.on("refresh", () => refreshCount++);
+
+ yield testGraph(host, graph);
+
+ is(refreshCount, 2, "The graph should've been refreshed 2 times.");
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function* testGraph(host, graph) {
+ graph.setData(TEST_DATA);
+ let initialBounds = host.frame.getBoundingClientRect();
+
+ host._window.resizeBy(-100, -100);
+ yield graph.once("refresh");
+ let newBounds = host.frame.getBoundingClientRect();
+
+ is(initialBounds.width - newBounds.width, 100,
+ "The window was properly resized (1).");
+ is(initialBounds.height - newBounds.height, 100,
+ "The window was properly resized (2).");
+
+ is(graph.width, newBounds.width * window.devicePixelRatio,
+ "The graph has the correct width (1).");
+ is(graph.height, newBounds.height * window.devicePixelRatio,
+ "The graph has the correct height (1).");
+
+ info("Making a selection.");
+
+ dragStart(graph, 300);
+ ok(graph.hasSelectionInProgress(),
+ "The selection should start (1).");
+ is(graph.getSelection().start, 300,
+ "The current selection start value is correct (1).");
+ is(graph.getSelection().end, 300,
+ "The current selection end value is correct (1).");
+
+ hover(graph, 400);
+ ok(graph.hasSelectionInProgress(),
+ "The selection should still be in progress (2).");
+ is(graph.getSelection().start, 300,
+ "The current selection start value is correct (2).");
+ is(graph.getSelection().end, 400,
+ "The current selection end value is correct (2).");
+
+ dragStop(graph, 500);
+ ok(!graph.hasSelectionInProgress(),
+ "The selection should have stopped (3).");
+ is(graph.getSelection().start, 300,
+ "The current selection start value is correct (3).");
+ is(graph.getSelection().end, 500,
+ "The current selection end value is correct (3).");
+
+ host._window.resizeBy(100, 100);
+ yield graph.once("refresh");
+ let newerBounds = host.frame.getBoundingClientRect();
+
+ is(initialBounds.width - newerBounds.width, 0,
+ "The window was properly resized (3).");
+ is(initialBounds.height - newerBounds.height, 0,
+ "The window was properly resized (4).");
+
+ is(graph.width, newerBounds.width * window.devicePixelRatio,
+ "The graph has the correct width (2).");
+ is(graph.height, newerBounds.height * window.devicePixelRatio,
+ "The graph has the correct height (2).");
+
+ info("Making a new selection.");
+
+ dragStart(graph, 200);
+ ok(graph.hasSelectionInProgress(),
+ "The selection should start (4).");
+ is(graph.getSelection().start, 200,
+ "The current selection start value is correct (4).");
+ is(graph.getSelection().end, 200,
+ "The current selection end value is correct (4).");
+
+ hover(graph, 300);
+ ok(graph.hasSelectionInProgress(),
+ "The selection should still be in progress (5).");
+ is(graph.getSelection().start, 200,
+ "The current selection start value is correct (5).");
+ is(graph.getSelection().end, 300,
+ "The current selection end value is correct (5).");
+
+ dragStop(graph, 400);
+ ok(!graph.hasSelectionInProgress(),
+ "The selection should have stopped (6).");
+ is(graph.getSelection().start, 200,
+ "The current selection start value is correct (6).");
+ is(graph.getSelection().end, 400,
+ "The current selection end value is correct (6).");
+}
+
+// EventUtils just doesn't work!
+
+function hover(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+}
+
+function dragStart(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseDown({ testX: x, testY: y });
+}
+
+function dragStop(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseUp({ testX: x, testY: y });
+}
diff --git a/devtools/client/shared/test/browser_graphs-10b.js b/devtools/client/shared/test/browser_graphs-10b.js
new file mode 100644
index 000000000..a29bdfd25
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-10b.js
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that graphs aren't refreshed when the owner window resizes but
+// the graph dimensions stay the same.
+
+const TEST_DATA = [
+ { delta: 112, value: 48 }, { delta: 213, value: 59 },
+ { delta: 313, value: 60 }, { delta: 413, value: 59 },
+ { delta: 530, value: 59 }, { delta: 646, value: 58 },
+ { delta: 747, value: 60 }, { delta: 863, value: 48 },
+ { delta: 980, value: 37 }, { delta: 1097, value: 30 },
+ { delta: 1213, value: 29 }, { delta: 1330, value: 23 },
+ { delta: 1430, value: 10 }, { delta: 1534, value: 17 },
+ { delta: 1645, value: 20 }, { delta: 1746, value: 22 },
+ { delta: 1846, value: 39 }, { delta: 1963, value: 26 },
+ { delta: 2080, value: 27 }, { delta: 2197, value: 35 },
+ { delta: 2312, value: 47 }, { delta: 2412, value: 53 },
+ { delta: 2514, value: 60 }, { delta: 2630, value: 37 },
+ { delta: 2730, value: 36 }, { delta: 2830, value: 37 },
+ { delta: 2946, value: 36 }, { delta: 3046, value: 40 },
+ { delta: 3163, value: 47 }, { delta: 3280, value: 41 },
+ { delta: 3380, value: 35 }, { delta: 3480, value: 27 },
+ { delta: 3580, value: 39 }, { delta: 3680, value: 42 },
+ { delta: 3780, value: 49 }, { delta: 3880, value: 55 },
+ { delta: 3980, value: 60 }, { delta: 4080, value: 60 },
+ { delta: 4180, value: 60 }
+];
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost("window");
+ doc.body.setAttribute("style",
+ "position: fixed; width: 100%; height: 100%; margin: 0;");
+
+ let graph = new LineGraphWidget(doc.body, "fps");
+ graph.fixedWidth = 200;
+ graph.fixedHeight = 100;
+ yield graph.once("ready");
+
+ let refreshCount = 0;
+ let refreshCancelledCount = 0;
+ graph.on("refresh", () => refreshCount++);
+ graph.on("refresh-cancelled", () => refreshCancelledCount++);
+
+ yield testGraph(host, graph);
+
+ is(refreshCount, 0, "The graph shouldn't have been refreshed at all.");
+ is(refreshCancelledCount, 2, "The graph should've had 2 refresh attempts.");
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function* testGraph(host, graph) {
+ graph.setData(TEST_DATA);
+
+ host._window.resizeBy(-100, -100);
+ yield graph.once("refresh-cancelled");
+
+ host._window.resizeBy(100, 100);
+ yield graph.once("refresh-cancelled");
+}
diff --git a/devtools/client/shared/test/browser_graphs-10c.js b/devtools/client/shared/test/browser_graphs-10c.js
new file mode 100644
index 000000000..f68a3e804
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-10c.js
@@ -0,0 +1,109 @@
+
+"use strict";
+
+// Tests that graphs properly handle resizing.
+
+const TEST_DATA = [
+ { delta: 112, value: 48 }, { delta: 213, value: 59 },
+ { delta: 313, value: 60 }, { delta: 413, value: 59 },
+ { delta: 530, value: 59 }, { delta: 646, value: 58 },
+ { delta: 747, value: 60 }, { delta: 863, value: 48 },
+ { delta: 980, value: 37 }, { delta: 1097, value: 30 },
+ { delta: 1213, value: 29 }, { delta: 1330, value: 23 },
+ { delta: 1430, value: 10 }, { delta: 1534, value: 17 },
+ { delta: 1645, value: 20 }, { delta: 1746, value: 22 },
+ { delta: 1846, value: 39 }, { delta: 1963, value: 26 },
+ { delta: 2080, value: 27 }, { delta: 2197, value: 35 },
+ { delta: 2312, value: 47 }, { delta: 2412, value: 53 },
+ { delta: 2514, value: 60 }, { delta: 2630, value: 37 },
+ { delta: 2730, value: 36 }, { delta: 2830, value: 37 },
+ { delta: 2946, value: 36 }, { delta: 3046, value: 40 },
+ { delta: 3163, value: 47 }, { delta: 3280, value: 41 },
+ { delta: 3380, value: 35 }, { delta: 3480, value: 27 },
+ { delta: 3580, value: 39 }, { delta: 3680, value: 42 },
+ { delta: 3780, value: 49 }, { delta: 3880, value: 55 },
+ { delta: 3980, value: 60 }, { delta: 4080, value: 60 },
+ { delta: 4180, value: 60 }
+];
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost("window");
+ doc.body.setAttribute("style",
+ "position: fixed; width: 100%; height: 100%; margin: 0;");
+
+ let graph = new LineGraphWidget(doc.body, "fps");
+ yield graph.once("ready");
+
+ let refreshCount = 0;
+ graph.on("refresh", () => refreshCount++);
+
+ yield testGraph(host, graph);
+
+ is(refreshCount, 2, "The graph should've been refreshed 2 times.");
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function* testGraph(host, graph) {
+ graph.setData(TEST_DATA);
+
+ host._window.resizeTo(500, 500);
+ yield graph.once("refresh");
+ let oldBounds = host.frame.getBoundingClientRect();
+
+ is(graph._width, oldBounds.width * window.devicePixelRatio,
+ "The window was properly resized (1).");
+ is(graph._height, oldBounds.height * window.devicePixelRatio,
+ "The window was properly resized (1).");
+
+ dragStart(graph, 100);
+ dragStop(graph, 400);
+
+ is(graph.getSelection().start, 100,
+ "The current selection start value is correct (1).");
+ is(graph.getSelection().end, 400,
+ "The current selection end value is correct (1).");
+
+ info("Making sure the selection updates when the window is resized");
+
+ host._window.resizeTo(250, 250);
+ yield graph.once("refresh");
+ let newBounds = host.frame.getBoundingClientRect();
+
+ is(graph._width, newBounds.width * window.devicePixelRatio,
+ "The window was properly resized (2).");
+ is(graph._height, newBounds.height * window.devicePixelRatio,
+ "The window was properly resized (2).");
+
+ let ratio = oldBounds.width / newBounds.width;
+ info("The window resize ratio is: " + ratio);
+
+ is(graph.getSelection().start, Math.round(100 / ratio),
+ "The current selection start value is correct (2).");
+ is(graph.getSelection().end, Math.round(400 / ratio),
+ "The current selection end value is correct (2).");
+}
+
+// EventUtils just doesn't work!
+
+function dragStart(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseDown({ testX: x, testY: y });
+}
+
+function dragStop(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseUp({ testX: x, testY: y });
+}
diff --git a/devtools/client/shared/test/browser_graphs-11a.js b/devtools/client/shared/test/browser_graphs-11a.js
new file mode 100644
index 000000000..27e5b292c
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-11a.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that bar graph create a legend as expected.
+
+const BarGraphWidget = require("devtools/client/shared/widgets/BarGraphWidget");
+
+const CATEGORIES = [
+ { color: "#46afe3", label: "Foo" },
+ { color: "#eb5368", label: "Bar" },
+ { color: "#70bf53", label: "Baz" }
+];
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new BarGraphWidget(doc.body);
+ yield graph.once("ready");
+
+ testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function testGraph(graph) {
+ graph.format = CATEGORIES;
+ graph.setData([{ delta: 0, values: [] }]);
+
+ let legendContainer = graph._document.querySelector(".bar-graph-widget-legend");
+ ok(legendContainer,
+ "A legend container should be available.");
+ is(legendContainer.childNodes.length, 3,
+ "Three legend items should have been created.");
+
+ let legendItems = graph._document.querySelectorAll(".bar-graph-widget-legend-item");
+ is(legendItems.length, 3,
+ "Three legend items should exist in the entire graph.");
+
+ is(legendItems[0].querySelector("[view=color]").style.backgroundColor,
+ "rgb(70, 175, 227)", "The first legend item has the correct color.");
+ is(legendItems[1].querySelector("[view=color]").style.backgroundColor,
+ "rgb(235, 83, 104)", "The second legend item has the correct color.");
+ is(legendItems[2].querySelector("[view=color]").style.backgroundColor,
+ "rgb(112, 191, 83)", "The third legend item has the correct color.");
+
+ is(legendItems[0].querySelector("[view=label]").textContent, "Foo",
+ "The first legend item has the correct label.");
+ is(legendItems[1].querySelector("[view=label]").textContent, "Bar",
+ "The second legend item has the correct label.");
+ is(legendItems[2].querySelector("[view=label]").textContent, "Baz",
+ "The third legend item has the correct label.");
+}
diff --git a/devtools/client/shared/test/browser_graphs-11b.js b/devtools/client/shared/test/browser_graphs-11b.js
new file mode 100644
index 000000000..4df1c4495
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-11b.js
@@ -0,0 +1,133 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that bar graph's legend items handle mouseover/mouseout.
+
+const BarGraphWidget = require("devtools/client/shared/widgets/BarGraphWidget");
+
+const CATEGORIES = [
+ { color: "#46afe3", label: "Foo" },
+ { color: "#eb5368", label: "Bar" },
+ { color: "#70bf53", label: "Baz" }
+];
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ doc.body.setAttribute("style",
+ "position: fixed; width: 100%; height: 100%; margin: 0;");
+
+ let graph = new BarGraphWidget(doc.body, 1);
+ graph.fixedWidth = 200;
+ graph.fixedHeight = 100;
+
+ yield graph.once("ready");
+ yield testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function* testGraph(graph) {
+ graph.format = CATEGORIES;
+ graph.dataOffsetX = 1000;
+ graph.setData([{
+ delta: 1100, values: [0, 2, 3]
+ }, {
+ delta: 1200, values: [1, 0, 2]
+ }, {
+ delta: 1300, values: [2, 1, 0]
+ }, {
+ delta: 1400, values: [0, 3, 1]
+ }, {
+ delta: 1500, values: [3, 0, 2]
+ }, {
+ delta: 1600, values: [3, 2, 0]
+ }]);
+
+ /* eslint-disable max-len */
+ is(graph._blocksBoundingRects.toSource(), "[{type:1, start:0, end:33.33333333333333, top:70, bottom:100}, {type:2, start:0, end:33.33333333333333, top:24, bottom:69}, {type:0, start:34.33333333333333, end:66.66666666666666, top:85, bottom:100}, {type:2, start:34.33333333333333, end:66.66666666666666, top:54, bottom:84}, {type:0, start:67.66666666666666, end:100, top:70, bottom:100}, {type:1, start:67.66666666666666, end:100, top:54, bottom:69}, {type:1, start:101, end:133.33333333333331, top:55, bottom:100}, {type:2, start:101, end:133.33333333333331, top:39, bottom:54}, {type:0, start:134.33333333333331, end:166.66666666666666, top:55, bottom:100}, {type:2, start:134.33333333333331, end:166.66666666666666, top:24, bottom:54}, {type:0, start:167.66666666666666, end:200, top:55, bottom:100}, {type:1, start:167.66666666666666, end:200, top:24, bottom:54}]",
+ "The correct blocks bounding rects were calculated for the bar graph.");
+
+ let legendItems = graph._document.querySelectorAll(".bar-graph-widget-legend-item");
+ is(legendItems.length, 3,
+ "Three legend items should exist in the entire graph.");
+
+ yield testLegend(graph, 0, {
+ highlights: "[{type:0, start:34.33333333333333, end:66.66666666666666, top:85, bottom:100}, {type:0, start:67.66666666666666, end:100, top:70, bottom:100}, {type:0, start:134.33333333333331, end:166.66666666666666, top:55, bottom:100}, {type:0, start:167.66666666666666, end:200, top:55, bottom:100}]",
+ selection: "({start:34.33333333333333, end:200})",
+ leftmost: "({type:0, start:34.33333333333333, end:66.66666666666666, top:85, bottom:100})",
+ rightmost: "({type:0, start:167.66666666666666, end:200, top:55, bottom:100})"
+ });
+ yield testLegend(graph, 1, {
+ highlights: "[{type:1, start:0, end:33.33333333333333, top:70, bottom:100}, {type:1, start:67.66666666666666, end:100, top:54, bottom:69}, {type:1, start:101, end:133.33333333333331, top:55, bottom:100}, {type:1, start:167.66666666666666, end:200, top:24, bottom:54}]",
+ selection: "({start:0, end:200})",
+ leftmost: "({type:1, start:0, end:33.33333333333333, top:70, bottom:100})",
+ rightmost: "({type:1, start:167.66666666666666, end:200, top:24, bottom:54})"
+ });
+ yield testLegend(graph, 2, {
+ highlights: "[{type:2, start:0, end:33.33333333333333, top:24, bottom:69}, {type:2, start:34.33333333333333, end:66.66666666666666, top:54, bottom:84}, {type:2, start:101, end:133.33333333333331, top:39, bottom:54}, {type:2, start:134.33333333333331, end:166.66666666666666, top:24, bottom:54}]",
+ selection: "({start:0, end:166.66666666666666})",
+ leftmost: "({type:2, start:0, end:33.33333333333333, top:24, bottom:69})",
+ rightmost: "({type:2, start:134.33333333333331, end:166.66666666666666, top:24, bottom:54})"
+ });
+ /* eslint-enable max-len */
+}
+
+function* testLegend(graph, index, { highlights, selection, leftmost, rightmost }) {
+ // Hover.
+
+ let legendItems = graph._document.querySelectorAll(".bar-graph-widget-legend-item");
+ let colorBlock = legendItems[index].querySelector("[view=color]");
+
+ let debounced = graph.once("legend-hover");
+ graph._onLegendMouseOver({ target: colorBlock });
+ ok(!graph.hasMask(), "The graph shouldn't get highlights immediately.");
+
+ let [type, rects] = yield debounced;
+ ok(graph.hasMask(), "The graph should now have highlights.");
+
+ is(type, index,
+ "The legend item was correctly hovered.");
+ is(rects.toSource(), highlights,
+ "The legend item highlighted the correct regions.");
+
+ // Unhover.
+
+ let unhovered = graph.once("legend-unhover");
+ graph._onLegendMouseOut();
+ ok(!graph.hasMask(), "The graph shouldn't have highlights anymore.");
+
+ yield unhovered;
+ ok(true, "The 'legend-mouseout' event was emitted.");
+
+ // Select.
+
+ let selected = graph.once("legend-selection");
+ graph._onLegendMouseDown(mockEvent(colorBlock));
+ ok(graph.hasSelection(), "The graph should now have a selection.");
+ is(graph.getSelection().toSource(), selection, "The graph has a correct selection.");
+
+ let [left, right] = yield selected;
+ is(left.toSource(), leftmost, "The correct leftmost data block was found.");
+ is(right.toSource(), rightmost, "The correct rightmost data block was found.");
+
+ // Deselect.
+
+ graph.dropSelection();
+}
+
+function mockEvent(node) {
+ return {
+ target: node,
+ preventDefault: () => {},
+ stopPropagation: () => {}
+ };
+}
diff --git a/devtools/client/shared/test/browser_graphs-12.js b/devtools/client/shared/test/browser_graphs-12.js
new file mode 100644
index 000000000..1836d016c
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-12.js
@@ -0,0 +1,157 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that canvas graphs can have their selection linked.
+
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+const BarGraphWidget = require("devtools/client/shared/widgets/BarGraphWidget");
+const {CanvasGraphUtils} = require("devtools/client/shared/widgets/Graphs");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ doc.body.setAttribute("style",
+ "position: fixed; width: 100%; height: 100%; margin: 0;");
+
+ let first = document.createElement("div");
+ first.setAttribute("style", "display: inline-block; width: 100%; height: 50%;");
+ doc.body.appendChild(first);
+
+ let second = document.createElement("div");
+ second.setAttribute("style", "display: inline-block; width: 100%; height: 50%;");
+ doc.body.appendChild(second);
+
+ let graph1 = new LineGraphWidget(first, "js");
+ let graph2 = new BarGraphWidget(second);
+
+ CanvasGraphUtils.linkAnimation(graph1, graph2);
+ CanvasGraphUtils.linkSelection(graph1, graph2);
+
+ yield graph1.ready();
+ yield graph2.ready();
+
+ testGraphs(graph1, graph2);
+
+ yield graph1.destroy();
+ yield graph2.destroy();
+ host.destroy();
+}
+
+function testGraphs(graph1, graph2) {
+ info("Making a selection in the first graph.");
+
+ dragStart(graph1, 300);
+ ok(graph1.hasSelectionInProgress(),
+ "The selection should start (1.1).");
+ ok(!graph2.hasSelectionInProgress(),
+ "The selection should not start yet in the second graph (1.2).");
+ is(graph1.getSelection().start, 300,
+ "The current selection start value is correct (1.1).");
+ is(graph2.getSelection().start, 300,
+ "The current selection start value is correct (1.2).");
+ is(graph1.getSelection().end, 300,
+ "The current selection end value is correct (1.1).");
+ is(graph2.getSelection().end, 300,
+ "The current selection end value is correct (1.2).");
+
+ hover(graph1, 400);
+ ok(graph1.hasSelectionInProgress(),
+ "The selection should still be in progress (2.1).");
+ ok(!graph2.hasSelectionInProgress(),
+ "The selection should not be in progress in the second graph (2.2).");
+ is(graph1.getSelection().start, 300,
+ "The current selection start value is correct (2.1).");
+ is(graph2.getSelection().start, 300,
+ "The current selection start value is correct (2.2).");
+ is(graph1.getSelection().end, 400,
+ "The current selection end value is correct (2.1).");
+ is(graph2.getSelection().end, 400,
+ "The current selection end value is correct (2.2).");
+
+ dragStop(graph1, 500);
+ ok(!graph1.hasSelectionInProgress(),
+ "The selection should have stopped (3.1).");
+ ok(!graph2.hasSelectionInProgress(),
+ "The selection should have stopped (3.2).");
+ is(graph1.getSelection().start, 300,
+ "The current selection start value is correct (3.1).");
+ is(graph2.getSelection().start, 300,
+ "The current selection start value is correct (3.2).");
+ is(graph1.getSelection().end, 500,
+ "The current selection end value is correct (3.1).");
+ is(graph2.getSelection().end, 500,
+ "The current selection end value is correct (3.2).");
+
+ info("Making a new selection in the second graph.");
+
+ dragStart(graph2, 200);
+ ok(!graph1.hasSelectionInProgress(),
+ "The selection should not start yet in the first graph (4.1).");
+ ok(graph2.hasSelectionInProgress(),
+ "The selection should start (4.2).");
+ is(graph1.getSelection().start, 200,
+ "The current selection start value is correct (4.1).");
+ is(graph2.getSelection().start, 200,
+ "The current selection start value is correct (4.2).");
+ is(graph1.getSelection().end, 200,
+ "The current selection end value is correct (4.1).");
+ is(graph2.getSelection().end, 200,
+ "The current selection end value is correct (4.2).");
+
+ hover(graph2, 300);
+ ok(!graph1.hasSelectionInProgress(),
+ "The selection should not be in progress in the first graph (2.2).");
+ ok(graph2.hasSelectionInProgress(),
+ "The selection should still be in progress (5.2).");
+ is(graph1.getSelection().start, 200,
+ "The current selection start value is correct (5.1).");
+ is(graph2.getSelection().start, 200,
+ "The current selection start value is correct (5.2).");
+ is(graph1.getSelection().end, 300,
+ "The current selection end value is correct (5.1).");
+ is(graph2.getSelection().end, 300,
+ "The current selection end value is correct (5.2).");
+
+ dragStop(graph2, 400);
+ ok(!graph1.hasSelectionInProgress(),
+ "The selection should have stopped (6.1).");
+ ok(!graph2.hasSelectionInProgress(),
+ "The selection should have stopped (6.2).");
+ is(graph1.getSelection().start, 200,
+ "The current selection start value is correct (6.1).");
+ is(graph2.getSelection().start, 200,
+ "The current selection start value is correct (6.2).");
+ is(graph1.getSelection().end, 400,
+ "The current selection end value is correct (6.1).");
+ is(graph2.getSelection().end, 400,
+ "The current selection end value is correct (6.2).");
+}
+
+// EventUtils just doesn't work!
+
+function hover(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+}
+
+function dragStart(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseDown({ testX: x, testY: y });
+}
+
+function dragStop(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseUp({ testX: x, testY: y });
+}
diff --git a/devtools/client/shared/test/browser_graphs-13.js b/devtools/client/shared/test/browser_graphs-13.js
new file mode 100644
index 000000000..d671291ed
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-13.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that graph widgets may have a fixed width or height.
+
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ doc.body.setAttribute("style",
+ "position: fixed; width: 100%; height: 100%; margin: 0;");
+
+ let graph = new LineGraphWidget(doc.body, "fps");
+ graph.fixedWidth = 200;
+ graph.fixedHeight = 100;
+
+ yield graph.ready();
+ testGraph(host, graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function testGraph(host, graph) {
+ let bounds = host.frame.getBoundingClientRect();
+
+ isnot(graph.width, bounds.width * window.devicePixelRatio,
+ "The graph should not span all the parent node's width.");
+ isnot(graph.height, bounds.height * window.devicePixelRatio,
+ "The graph should not span all the parent node's height.");
+
+ is(graph.width, graph.fixedWidth * window.devicePixelRatio,
+ "The graph has the correct width.");
+ is(graph.height, graph.fixedHeight * window.devicePixelRatio,
+ "The graph has the correct height.");
+}
diff --git a/devtools/client/shared/test/browser_graphs-14.js b/devtools/client/shared/test/browser_graphs-14.js
new file mode 100644
index 000000000..4001f8e6d
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-14.js
@@ -0,0 +1,111 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that graph widgets correctly emit mouse input events.
+
+const TEST_DATA = [
+ { delta: 112, value: 48 }, { delta: 213, value: 59 },
+ { delta: 313, value: 60 }, { delta: 413, value: 59 },
+ { delta: 530, value: 59 }, { delta: 646, value: 58 },
+ { delta: 747, value: 60 }, { delta: 863, value: 48 },
+ { delta: 980, value: 37 }, { delta: 1097, value: 30 },
+ { delta: 1213, value: 29 }, { delta: 1330, value: 23 },
+ { delta: 1430, value: 10 }, { delta: 1534, value: 17 },
+ { delta: 1645, value: 20 }, { delta: 1746, value: 22 },
+ { delta: 1846, value: 39 }, { delta: 1963, value: 26 },
+ { delta: 2080, value: 27 }, { delta: 2197, value: 35 },
+ { delta: 2312, value: 47 }, { delta: 2412, value: 53 },
+ { delta: 2514, value: 60 }, { delta: 2630, value: 37 },
+ { delta: 2730, value: 36 }, { delta: 2830, value: 37 },
+ { delta: 2946, value: 36 }, { delta: 3046, value: 40 },
+ { delta: 3163, value: 47 }, { delta: 3280, value: 41 },
+ { delta: 3380, value: 35 }, { delta: 3480, value: 27 },
+ { delta: 3580, value: 39 }, { delta: 3680, value: 42 },
+ { delta: 3780, value: 49 }, { delta: 3880, value: 55 },
+ { delta: 3980, value: 60 }, { delta: 4080, value: 60 },
+ { delta: 4180, value: 60 }
+];
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+
+ yield testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function* testGraph(graph) {
+ let mouseDownEvents = 0;
+ let mouseUpEvents = 0;
+ let scrollEvents = 0;
+ graph.on("mousedown", () => mouseDownEvents++);
+ graph.on("mouseup", () => mouseUpEvents++);
+ graph.on("scroll", () => scrollEvents++);
+
+ yield graph.setDataWhenReady(TEST_DATA);
+
+ info("Making a selection.");
+
+ dragStart(graph, 300);
+ dragStop(graph, 500);
+ is(graph.getSelection().start, 300,
+ "The current selection start value is correct (1).");
+ is(graph.getSelection().end, 500,
+ "The current selection end value is correct (1).");
+
+ is(mouseDownEvents, 1,
+ "One mousedown event should have been fired.");
+ is(mouseUpEvents, 1,
+ "One mouseup event should have been fired.");
+ is(scrollEvents, 0,
+ "No scroll event should have been fired.");
+
+ info("Zooming in by scrolling inside the selection.");
+
+ scroll(graph, -1000, 400);
+ is(graph.getSelection().start, 375,
+ "The current selection start value is correct (2).");
+ is(graph.getSelection().end, 425,
+ "The current selection end value is correct (2).");
+
+ is(mouseDownEvents, 1,
+ "No more mousedown events should have been fired.");
+ is(mouseUpEvents, 1,
+ "No more mouseup events should have been fired.");
+ is(scrollEvents, 1,
+ "One scroll event should have been fired.");
+}
+
+// EventUtils just doesn't work!
+
+function dragStart(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseDown({ testX: x, testY: y });
+}
+
+function dragStop(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseUp({ testX: x, testY: y });
+}
+
+function scroll(graph, wheel, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseWheel({ testX: x, testY: y, detail: wheel });
+}
diff --git a/devtools/client/shared/test/browser_graphs-15.js b/devtools/client/shared/test/browser_graphs-15.js
new file mode 100644
index 000000000..af2c9875e
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-15.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that graph widgets correctly emit mouse input events.
+
+const FAST_FPS = 60;
+const SLOW_FPS = 10;
+
+// Each element represents a second
+const FRAMES = [FAST_FPS, FAST_FPS, FAST_FPS, SLOW_FPS, FAST_FPS];
+const TEST_DATA = [];
+const INTERVAL = 100;
+const DURATION = 5000;
+var t = 0;
+for (let frameRate of FRAMES) {
+ for (let i = 0; i < frameRate; i++) {
+ // Duration between frames at this rate
+ let delta = Math.floor(1000 / frameRate);
+ t += delta;
+ TEST_DATA.push(t);
+ }
+}
+
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+
+ yield testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function* testGraph(graph) {
+ console.log("test data", TEST_DATA);
+ yield graph.setDataFromTimestamps(TEST_DATA, INTERVAL, DURATION);
+ is(graph._avgTooltip.querySelector("[text=value]").textContent, "50",
+ "The average tooltip displays the correct value.");
+}
diff --git a/devtools/client/shared/test/browser_graphs-16.js b/devtools/client/shared/test/browser_graphs-16.js
new file mode 100644
index 000000000..194cb751c
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-16.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that mounta graphs work as expected.
+
+const MountainGraphWidget = require("devtools/client/shared/widgets/MountainGraphWidget");
+
+const TEST_DATA = [
+ { delta: 0, values: [0.1, 0.5, 0.3] },
+ { delta: 1, values: [0.25, 0, 0.5] },
+ { delta: 2, values: [0.5, 0.25, 0.1] },
+ { delta: 3, values: [0, 0.75, 0] },
+ { delta: 4, values: [0.75, 0, 0.25] }
+];
+
+const SECTIONS = [
+ { color: "red" },
+ { color: "green" },
+ { color: "blue" }
+];
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new MountainGraphWidget(doc.body);
+ yield graph.once("ready");
+
+ testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function testGraph(graph) {
+ graph.format = SECTIONS;
+ graph.setData(TEST_DATA);
+ ok(true, "The graph didn't throw any erorrs.");
+}
diff --git a/devtools/client/shared/test/browser_html_tooltip-01.js b/devtools/client/shared/test/browser_html_tooltip-01.js
new file mode 100644
index 000000000..7752881b9
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip-01.js
@@ -0,0 +1,88 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_html_tooltip.js */
+
+"use strict";
+
+/**
+ * Test the HTMLTooltip show & hide methods.
+ */
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const TEST_URI = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
+ <?xml-stylesheet href="chrome://global/skin/global.css"?>
+ <?xml-stylesheet href="chrome://devtools/skin/tooltips.css"?>
+ <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Tooltip test">
+ <vbox flex="1">
+ <hbox id="box1" flex="1">test1</hbox>
+ <hbox id="box2" flex="1">test2</hbox>
+ <hbox id="box3" flex="1">test3</hbox>
+ <hbox id="box4" flex="1">test4</hbox>
+ </vbox>
+ </window>`;
+
+const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+loadHelperScript("helper_html_tooltip.js");
+
+let useXulWrapper;
+
+function getTooltipContent(doc) {
+ let div = doc.createElementNS(HTML_NS, "div");
+ div.style.height = "50px";
+ div.style.boxSizing = "border-box";
+ div.textContent = "tooltip";
+ return div;
+}
+
+add_task(function* () {
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+
+ info("Run tests for a Tooltip without using a XUL panel");
+ useXulWrapper = false;
+ yield runTests(doc);
+
+ info("Run tests for a Tooltip with a XUL panel");
+ useXulWrapper = true;
+ yield runTests(doc);
+});
+
+function* runTests(doc) {
+ yield addTab("about:blank");
+ let tooltip = new HTMLTooltip(doc, {useXulWrapper});
+
+ info("Set tooltip content");
+ tooltip.setContent(getTooltipContent(doc), {width: 100, height: 50});
+
+ is(tooltip.isVisible(), false, "Tooltip is not visible");
+
+ info("Show the tooltip and check the expected events are fired.");
+
+ let shown = 0;
+ tooltip.on("shown", () => shown++);
+
+ let onShown = tooltip.once("shown");
+ tooltip.show(doc.getElementById("box1"));
+
+ yield onShown;
+ is(shown, 1, "Event shown was fired once");
+
+ yield waitForReflow(tooltip);
+ is(tooltip.isVisible(), true, "Tooltip is visible");
+
+ info("Hide the tooltip and check the expected events are fired.");
+
+ let hidden = 0;
+ tooltip.on("hidden", () => hidden++);
+
+ let onPopupHidden = tooltip.once("hidden");
+ tooltip.hide();
+
+ yield onPopupHidden;
+ is(hidden, 1, "Event hidden was fired once");
+
+ yield waitForReflow(tooltip);
+ is(tooltip.isVisible(), false, "Tooltip is not visible");
+
+ tooltip.destroy();
+}
diff --git a/devtools/client/shared/test/browser_html_tooltip-02.js b/devtools/client/shared/test/browser_html_tooltip-02.js
new file mode 100644
index 000000000..4ebe9185b
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip-02.js
@@ -0,0 +1,174 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_html_tooltip.js */
+"use strict";
+
+/**
+ * Test the HTMLTooltip is closed when clicking outside of its container.
+ */
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const TEST_URI = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
+ <?xml-stylesheet href="chrome://global/skin/global.css"?>
+ <?xml-stylesheet href="chrome://devtools/skin/tooltips.css"?>
+ <window
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ htmlns="http://www.w3.org/1999/xhtml"
+ title="Tooltip test">
+ <vbox flex="1">
+ <hbox id="box1" flex="1">test1</hbox>
+ <hbox id="box2" flex="1">test2</hbox>
+ <hbox id="box3" flex="1">test3</hbox>
+ <hbox id="box4" flex="1">test4</hbox>
+ <iframe id="frame" width="200"></iframe>
+ </vbox>
+ </window>`;
+
+const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+loadHelperScript("helper_html_tooltip.js");
+
+let useXulWrapper;
+
+add_task(function* () {
+ yield addTab("about:blank");
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+
+ info("Run tests for a Tooltip without using a XUL panel");
+ useXulWrapper = false;
+ yield runTests(doc);
+
+ info("Run tests for a Tooltip with a XUL panel");
+ useXulWrapper = true;
+ yield runTests(doc);
+});
+
+function* runTests(doc) {
+ yield testClickInTooltipContent(doc);
+ yield testConsumeOutsideClicksFalse(doc);
+ yield testConsumeOutsideClicksTrue(doc);
+ yield testConsumeWithRightClick(doc);
+ yield testClickInOuterIframe(doc);
+ yield testClickInInnerIframe(doc);
+}
+
+function* testClickInTooltipContent(doc) {
+ info("Test a tooltip is not closed when clicking inside itself");
+
+ let tooltip = new HTMLTooltip(doc, {useXulWrapper});
+ tooltip.setContent(getTooltipContent(doc), {width: 100, height: 50});
+ yield showTooltip(tooltip, doc.getElementById("box1"));
+
+ let onTooltipContainerClick = once(tooltip.container, "click");
+ EventUtils.synthesizeMouseAtCenter(tooltip.container, {}, doc.defaultView);
+ yield onTooltipContainerClick;
+ is(tooltip.isVisible(), true, "Tooltip is still visible");
+
+ tooltip.destroy();
+}
+
+function* testConsumeOutsideClicksFalse(doc) {
+ info("Test closing a tooltip via click with consumeOutsideClicks: false");
+ let box4 = doc.getElementById("box4");
+
+ let tooltip = new HTMLTooltip(doc, {consumeOutsideClicks: false, useXulWrapper});
+ tooltip.setContent(getTooltipContent(doc), {width: 100, height: 50});
+ yield showTooltip(tooltip, doc.getElementById("box1"));
+
+ let onBox4Clicked = once(box4, "click");
+ let onHidden = once(tooltip, "hidden");
+ EventUtils.synthesizeMouseAtCenter(box4, {}, doc.defaultView);
+ yield onHidden;
+ yield onBox4Clicked;
+
+ is(tooltip.isVisible(), false, "Tooltip is hidden");
+
+ tooltip.destroy();
+}
+
+function* testConsumeOutsideClicksTrue(doc) {
+ info("Test closing a tooltip via click with consumeOutsideClicks: true");
+ let box4 = doc.getElementById("box4");
+
+ // Count clicks on box4
+ let box4clicks = 0;
+ box4.addEventListener("click", () => box4clicks++);
+
+ let tooltip = new HTMLTooltip(doc, {consumeOutsideClicks: true, useXulWrapper});
+ tooltip.setContent(getTooltipContent(doc), {width: 100, height: 50});
+ yield showTooltip(tooltip, doc.getElementById("box1"));
+
+ let onHidden = once(tooltip, "hidden");
+ EventUtils.synthesizeMouseAtCenter(box4, {}, doc.defaultView);
+ yield onHidden;
+
+ is(box4clicks, 0, "box4 catched no click event");
+ is(tooltip.isVisible(), false, "Tooltip is hidden");
+
+ tooltip.destroy();
+}
+
+function* testConsumeWithRightClick(doc) {
+ info("Test closing a tooltip with a right-click, with consumeOutsideClicks: true");
+ let box4 = doc.getElementById("box4");
+
+ let tooltip = new HTMLTooltip(doc, {consumeOutsideClicks: true, useXulWrapper});
+ tooltip.setContent(getTooltipContent(doc), {width: 100, height: 50});
+ yield showTooltip(tooltip, doc.getElementById("box1"));
+
+ // Only left-click events should be consumed, so we expect to catch a click when using
+ // {button: 2}, which simulates a right-click.
+ info("Right click on box4, expect tooltip to be hidden, event should not be consumed");
+ let onBox4Clicked = once(box4, "click");
+ let onHidden = once(tooltip, "hidden");
+ EventUtils.synthesizeMouseAtCenter(box4, {button: 2}, doc.defaultView);
+ yield onHidden;
+ yield onBox4Clicked;
+
+ is(tooltip.isVisible(), false, "Tooltip is hidden");
+
+ tooltip.destroy();
+}
+
+function* testClickInOuterIframe(doc) {
+ info("Test clicking an iframe outside of the tooltip closes the tooltip");
+ let frame = doc.getElementById("frame");
+
+ let tooltip = new HTMLTooltip(doc, {useXulWrapper});
+ tooltip.setContent(getTooltipContent(doc), {width: 100, height: 50});
+ yield showTooltip(tooltip, doc.getElementById("box1"));
+
+ let onHidden = once(tooltip, "hidden");
+ EventUtils.synthesizeMouseAtCenter(frame, {}, doc.defaultView);
+ yield onHidden;
+
+ is(tooltip.isVisible(), false, "Tooltip is hidden");
+ tooltip.destroy();
+}
+
+function* testClickInInnerIframe(doc) {
+ info("Test clicking an iframe inside the tooltip content does not close the tooltip");
+
+ let tooltip = new HTMLTooltip(doc, {consumeOutsideClicks: false, useXulWrapper});
+
+ let iframe = doc.createElementNS(HTML_NS, "iframe");
+ iframe.style.width = "100px";
+ iframe.style.height = "50px";
+ tooltip.setContent(iframe, {width: 100, height: 50});
+ yield showTooltip(tooltip, doc.getElementById("box1"));
+
+ let onTooltipContainerClick = once(tooltip.container, "click");
+ EventUtils.synthesizeMouseAtCenter(tooltip.container, {}, doc.defaultView);
+ yield onTooltipContainerClick;
+
+ is(tooltip.isVisible(), true, "Tooltip is still visible");
+
+ tooltip.destroy();
+}
+
+function getTooltipContent(doc) {
+ let div = doc.createElementNS(HTML_NS, "div");
+ div.style.height = "50px";
+ div.style.boxSizing = "border-box";
+ div.textContent = "tooltip";
+ return div;
+}
diff --git a/devtools/client/shared/test/browser_html_tooltip-03.js b/devtools/client/shared/test/browser_html_tooltip-03.js
new file mode 100644
index 000000000..6c189c127
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip-03.js
@@ -0,0 +1,155 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_html_tooltip.js */
+
+"use strict";
+
+/**
+ * Test the HTMLTooltip autofocus configuration option.
+ */
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const TEST_URI = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
+ <?xml-stylesheet href="chrome://global/skin/global.css"?>
+ <?xml-stylesheet href="chrome://devtools/skin/tooltips.css"?>
+ <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Tooltip test">
+ <vbox flex="1">
+ <hbox id="box1" flex="1">
+ <textbox></textbox>
+ </hbox>
+ <hbox id="box2" flex="1">test2</hbox>
+ <hbox id="box3" flex="1">
+ <textbox id="box3-input"></textbox>
+ </hbox>
+ <hbox id="box4" flex="1">
+ <textbox id="box4-input"></textbox>
+ </hbox>
+ </vbox>
+ </window>`;
+
+const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+loadHelperScript("helper_html_tooltip.js");
+
+let useXulWrapper;
+
+add_task(function* () {
+ yield addTab("about:blank");
+ let [, , doc] = yield createHost("bottom", TEST_URI);
+
+ info("Run tests for a Tooltip without using a XUL panel");
+ useXulWrapper = false;
+ yield runTests(doc);
+
+ info("Run tests for a Tooltip with a XUL panel");
+ useXulWrapper = true;
+ yield runTests(doc);
+});
+
+function* runTests(doc) {
+ yield testNoAutoFocus(doc);
+ yield testAutoFocus(doc);
+ yield testAutoFocusPreservesFocusChange(doc);
+}
+
+function* testNoAutoFocus(doc) {
+ yield focusNode(doc, "#box4-input");
+ ok(doc.activeElement.closest("#box4-input"), "Focus is in the #box4-input");
+
+ info("Test a tooltip without autofocus will not take focus");
+ let tooltip = yield createTooltip(doc, false);
+
+ yield showTooltip(tooltip, doc.getElementById("box1"));
+ ok(doc.activeElement.closest("#box4-input"), "Focus is still in the #box4-input");
+
+ yield hideTooltip(tooltip);
+ yield blurNode(doc, "#box4-input");
+
+ tooltip.destroy();
+}
+
+function* testAutoFocus(doc) {
+ yield focusNode(doc, "#box4-input");
+ ok(doc.activeElement.closest("#box4-input"), "Focus is in the #box4-input");
+
+ info("Test autofocus tooltip takes focus when displayed, " +
+ "and restores the focus when hidden");
+ let tooltip = yield createTooltip(doc, true);
+
+ yield showTooltip(tooltip, doc.getElementById("box1"));
+ ok(doc.activeElement.closest(".tooltip-content"), "Focus is in the tooltip");
+
+ yield hideTooltip(tooltip);
+ ok(doc.activeElement.closest("#box4-input"), "Focus is in the #box4-input");
+
+ info("Blur the textbox before moving to the next test to reset the state.");
+ yield blurNode(doc, "#box4-input");
+
+ tooltip.destroy();
+}
+
+function* testAutoFocusPreservesFocusChange(doc) {
+ yield focusNode(doc, "#box4-input");
+ ok(doc.activeElement.closest("#box4-input"), "Focus is still in the #box3-input");
+
+ info("Test autofocus tooltip takes focus when displayed, " +
+ "but does not try to restore the active element if it is not focused when hidden");
+ let tooltip = yield createTooltip(doc, true);
+
+ yield showTooltip(tooltip, doc.getElementById("box1"));
+ ok(doc.activeElement.closest(".tooltip-content"), "Focus is in the tooltip");
+
+ info("Move the focus to #box3-input while the tooltip is displayed");
+ yield focusNode(doc, "#box3-input");
+ ok(doc.activeElement.closest("#box3-input"), "Focus moved to the #box3-input");
+
+ yield hideTooltip(tooltip);
+ ok(doc.activeElement.closest("#box3-input"), "Focus is still in the #box3-input");
+
+ info("Blur the textbox before moving to the next test to reset the state.");
+ yield blurNode(doc, "#box3-input");
+
+ tooltip.destroy();
+}
+
+/**
+ * Fpcus the node corresponding to the provided selector in the provided document. Returns
+ * a promise that will resolve when receiving the focus event on the node.
+ */
+function focusNode(doc, selector) {
+ let node = doc.querySelector(selector);
+ let onFocus = once(node, "focus");
+ node.focus();
+ return onFocus;
+}
+
+/**
+ * Blur the node corresponding to the provided selector in the provided document. Returns
+ * a promise that will resolve when receiving the blur event on the node.
+ */
+function blurNode(doc, selector) {
+ let node = doc.querySelector(selector);
+ let onBlur = once(node, "blur");
+ node.blur();
+ return onBlur;
+}
+
+/**
+ * Create an HTMLTooltip instance with the provided autofocus setting.
+ *
+ * @param {Document} doc
+ * Document in which the tooltip should be created
+ * @param {Boolean} autofocus
+ * @return {Promise} promise that will resolve the HTMLTooltip instance created when the
+ * tooltip content will be ready.
+ */
+function* createTooltip(doc, autofocus) {
+ let tooltip = new HTMLTooltip(doc, {autofocus, useXulWrapper});
+ let div = doc.createElementNS(HTML_NS, "div");
+ div.classList.add("tooltip-content");
+ div.style.height = "50px";
+ div.innerHTML = '<input type="text"></input>';
+
+ tooltip.setContent(div, {width: 150, height: 50});
+ return tooltip;
+}
diff --git a/devtools/client/shared/test/browser_html_tooltip-04.js b/devtools/client/shared/test/browser_html_tooltip-04.js
new file mode 100644
index 000000000..90164840e
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip-04.js
@@ -0,0 +1,110 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_html_tooltip.js */
+
+"use strict";
+
+/**
+ * Test the HTMLTooltip positioning for a small tooltip element (should aways
+ * find a way to fit).
+ */
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const TEST_URI = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
+ <?xml-stylesheet href="chrome://global/skin/global.css"?>
+ <?xml-stylesheet href="chrome://devtools/skin/tooltips.css"?>
+ <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Tooltip test">
+ <vbox flex="1">
+ <hbox style="height: 10px">spacer</hbox>
+ <hbox id="box1" style="height: 50px">test1</hbox>
+ <hbox id="box2" style="height: 50px">test2</hbox>
+ <hbox flex="1">MIDDLE</hbox>
+ <hbox id="box3" style="height: 50px">test3</hbox>
+ <hbox id="box4" style="height: 50px">test4</hbox>
+ <hbox style="height: 10px">spacer</hbox>
+ </vbox>
+ </window>`;
+
+const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+loadHelperScript("helper_html_tooltip.js");
+
+const TOOLTIP_HEIGHT = 30;
+const TOOLTIP_WIDTH = 100;
+
+add_task(function* () {
+ // Force the toolbox to be 400px high;
+ yield pushPref("devtools.toolbox.footer.height", 400);
+
+ yield addTab("about:blank");
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+
+ info("Create HTML tooltip");
+ let tooltip = new HTMLTooltip(doc, {useXulWrapper: false});
+ let div = doc.createElementNS(HTML_NS, "div");
+ div.style.height = "100%";
+ tooltip.setContent(div, {width: TOOLTIP_WIDTH, height: TOOLTIP_HEIGHT});
+
+ let box1 = doc.getElementById("box1");
+ let box2 = doc.getElementById("box2");
+ let box3 = doc.getElementById("box3");
+ let box4 = doc.getElementById("box4");
+ let height = TOOLTIP_HEIGHT, width = TOOLTIP_WIDTH;
+
+ // box1: Can only fit below box1
+ info("Display the tooltip on box1.");
+ yield showTooltip(tooltip, box1);
+ let expectedTooltipGeometry = {position: "bottom", height, width};
+ checkTooltipGeometry(tooltip, box1, expectedTooltipGeometry);
+ yield hideTooltip(tooltip);
+
+ info("Try to display the tooltip on top of box1.");
+ yield showTooltip(tooltip, box1, {position: "top"});
+ expectedTooltipGeometry = {position: "bottom", height, width};
+ checkTooltipGeometry(tooltip, box1, expectedTooltipGeometry);
+ yield hideTooltip(tooltip);
+
+ // box2: Can fit above or below, will default to bottom, more height
+ // available.
+ info("Try to display the tooltip on box2.");
+ yield showTooltip(tooltip, box2);
+ expectedTooltipGeometry = {position: "bottom", height, width};
+ checkTooltipGeometry(tooltip, box2, expectedTooltipGeometry);
+ yield hideTooltip(tooltip);
+
+ info("Try to display the tooltip on top of box2.");
+ yield showTooltip(tooltip, box2, {position: "top"});
+ expectedTooltipGeometry = {position: "top", height, width};
+ checkTooltipGeometry(tooltip, box2, expectedTooltipGeometry);
+ yield hideTooltip(tooltip);
+
+ // box3: Can fit above or below, will default to top, more height available.
+ info("Try to display the tooltip on box3.");
+ yield showTooltip(tooltip, box3);
+ expectedTooltipGeometry = {position: "top", height, width};
+ checkTooltipGeometry(tooltip, box3, expectedTooltipGeometry);
+ yield hideTooltip(tooltip);
+
+ info("Try to display the tooltip on bottom of box3.");
+ yield showTooltip(tooltip, box3, {position: "bottom"});
+ expectedTooltipGeometry = {position: "bottom", height, width};
+ checkTooltipGeometry(tooltip, box3, expectedTooltipGeometry);
+ yield hideTooltip(tooltip);
+
+ // box4: Can only fit above box4
+ info("Display the tooltip on box4.");
+ yield showTooltip(tooltip, box4);
+ expectedTooltipGeometry = {position: "top", height, width};
+ checkTooltipGeometry(tooltip, box4, expectedTooltipGeometry);
+ yield hideTooltip(tooltip);
+
+ info("Try to display the tooltip on bottom of box4.");
+ yield showTooltip(tooltip, box4, {position: "bottom"});
+ expectedTooltipGeometry = {position: "top", height, width};
+ checkTooltipGeometry(tooltip, box4, expectedTooltipGeometry);
+ yield hideTooltip(tooltip);
+
+ is(tooltip.isVisible(), false, "Tooltip is not visible");
+
+ tooltip.destroy();
+});
diff --git a/devtools/client/shared/test/browser_html_tooltip-05.js b/devtools/client/shared/test/browser_html_tooltip-05.js
new file mode 100644
index 000000000..58be4f831
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip-05.js
@@ -0,0 +1,109 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_html_tooltip.js */
+
+"use strict";
+
+/**
+ * Test the HTMLTooltip positioning for a huge tooltip element (can not fit in
+ * the viewport).
+ */
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const TEST_URI = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
+ <?xml-stylesheet href="chrome://global/skin/global.css"?>
+ <?xml-stylesheet href="chrome://devtools/skin/tooltips.css"?>
+ <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Tooltip test">
+ <vbox flex="1">
+ <hbox id="box1" style="height: 50px">test1</hbox>
+ <hbox id="box2" style="height: 50px">test2</hbox>
+ <hbox id="box3" style="height: 50px">test3</hbox>
+ <hbox id="box4" style="height: 50px">test4</hbox>
+ </vbox>
+ </window>`;
+
+const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+loadHelperScript("helper_html_tooltip.js");
+
+const TOOLTIP_HEIGHT = 200;
+const TOOLTIP_WIDTH = 200;
+
+add_task(function* () {
+ // Force the toolbox to be 200px high;
+ yield pushPref("devtools.toolbox.footer.height", 200);
+ yield addTab("about:blank");
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+
+ info("Create HTML tooltip");
+ let tooltip = new HTMLTooltip(doc, {useXulWrapper: false});
+ let div = doc.createElementNS(HTML_NS, "div");
+ div.style.height = "100%";
+ tooltip.setContent(div, {width: TOOLTIP_WIDTH, height: TOOLTIP_HEIGHT});
+
+ let box1 = doc.getElementById("box1");
+ let box2 = doc.getElementById("box2");
+ let box3 = doc.getElementById("box3");
+ let box4 = doc.getElementById("box4");
+ let width = TOOLTIP_WIDTH;
+
+ // box1: Can not fit above or below box1, default to bottom with a reduced
+ // height of 150px.
+ info("Display the tooltip on box1.");
+ yield showTooltip(tooltip, box1);
+ let expectedTooltipGeometry = {position: "bottom", height: 150, width};
+ checkTooltipGeometry(tooltip, box1, expectedTooltipGeometry);
+ yield hideTooltip(tooltip);
+
+ info("Try to display the tooltip on top of box1.");
+ yield showTooltip(tooltip, box1, {position: "top"});
+ expectedTooltipGeometry = {position: "bottom", height: 150, width};
+ checkTooltipGeometry(tooltip, box1, expectedTooltipGeometry);
+ yield hideTooltip(tooltip);
+
+ // box2: Can not fit above or below box2, default to bottom with a reduced
+ // height of 100px.
+ info("Try to display the tooltip on box2.");
+ yield showTooltip(tooltip, box2);
+ expectedTooltipGeometry = {position: "bottom", height: 100, width};
+ checkTooltipGeometry(tooltip, box2, expectedTooltipGeometry);
+ yield hideTooltip(tooltip);
+
+ info("Try to display the tooltip on top of box2.");
+ yield showTooltip(tooltip, box2, {position: "top"});
+ expectedTooltipGeometry = {position: "bottom", height: 100, width};
+ checkTooltipGeometry(tooltip, box2, expectedTooltipGeometry);
+ yield hideTooltip(tooltip);
+
+ // box3: Can not fit above or below box3, default to top with a reduced height
+ // of 100px.
+ info("Try to display the tooltip on box3.");
+ yield showTooltip(tooltip, box3);
+ expectedTooltipGeometry = {position: "top", height: 100, width};
+ checkTooltipGeometry(tooltip, box3, expectedTooltipGeometry);
+ yield hideTooltip(tooltip);
+
+ info("Try to display the tooltip on bottom of box3.");
+ yield showTooltip(tooltip, box3, {position: "bottom"});
+ expectedTooltipGeometry = {position: "top", height: 100, width};
+ checkTooltipGeometry(tooltip, box3, expectedTooltipGeometry);
+ yield hideTooltip(tooltip);
+
+ // box4: Can not fit above or below box4, default to top with a reduced height
+ // of 150px.
+ info("Display the tooltip on box4.");
+ yield showTooltip(tooltip, box4);
+ expectedTooltipGeometry = {position: "top", height: 150, width};
+ checkTooltipGeometry(tooltip, box4, expectedTooltipGeometry);
+ yield hideTooltip(tooltip);
+
+ info("Try to display the tooltip on bottom of box4.");
+ yield showTooltip(tooltip, box4, {position: "bottom"});
+ expectedTooltipGeometry = {position: "top", height: 150, width};
+ checkTooltipGeometry(tooltip, box4, expectedTooltipGeometry);
+ yield hideTooltip(tooltip);
+
+ is(tooltip.isVisible(), false, "Tooltip is not visible");
+
+ tooltip.destroy();
+});
diff --git a/devtools/client/shared/test/browser_html_tooltip_arrow-01.js b/devtools/client/shared/test/browser_html_tooltip_arrow-01.js
new file mode 100644
index 000000000..a20c67529
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip_arrow-01.js
@@ -0,0 +1,108 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_html_tooltip.js */
+
+"use strict";
+
+/**
+ * Test the HTMLTooltip "arrow" type on small anchors. The arrow should remain
+ * aligned with the anchors as much as possible
+ */
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const getAnchor = function (position) {
+ return `<html:div class="anchor" style="width:10px;
+ height: 10px;
+ position: absolute;
+ background: red;
+ ${position}"></html:div>`;
+};
+
+const TEST_URI = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
+ <?xml-stylesheet href="chrome://global/skin/global.css"?>
+ <?xml-stylesheet href="chrome://devtools/skin/light-theme.css"?>
+
+ <window class="theme-light"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Tooltip test">
+ <vbox flex="1" style="position: relative">
+ ${getAnchor("top: 0; left: 0;")}
+ ${getAnchor("top: 0; left: 25px;")}
+ ${getAnchor("top: 0; left: 50px;")}
+ ${getAnchor("top: 0; left: 75px;")}
+ ${getAnchor("bottom: 0; left: 0;")}
+ ${getAnchor("bottom: 0; left: 25px;")}
+ ${getAnchor("bottom: 0; left: 50px;")}
+ ${getAnchor("bottom: 0; left: 75px;")}
+ ${getAnchor("bottom: 0; right: 0;")}
+ ${getAnchor("bottom: 0; right: 25px;")}
+ ${getAnchor("bottom: 0; right: 50px;")}
+ ${getAnchor("bottom: 0; right: 75px;")}
+ ${getAnchor("top: 0; right: 0;")}
+ ${getAnchor("top: 0; right: 25px;")}
+ ${getAnchor("top: 0; right: 50px;")}
+ ${getAnchor("top: 0; right: 75px;")}
+ </vbox>
+ </window>`;
+
+const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+loadHelperScript("helper_html_tooltip.js");
+
+let useXulWrapper;
+
+add_task(function* () {
+ // Force the toolbox to be 200px high;
+ yield pushPref("devtools.toolbox.footer.height", 200);
+
+ yield addTab("about:blank");
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+
+ info("Run tests for a Tooltip without using a XUL panel");
+ useXulWrapper = false;
+ yield runTests(doc);
+
+ info("Run tests for a Tooltip with a XUL panel");
+ useXulWrapper = true;
+ yield runTests(doc);
+});
+
+function* runTests(doc) {
+ info("Create HTML tooltip");
+ let tooltip = new HTMLTooltip(doc, {type: "arrow", useXulWrapper});
+ let div = doc.createElementNS(HTML_NS, "div");
+ div.style.height = "35px";
+ tooltip.setContent(div, {width: 200, height: 35});
+
+ let {right: docRight} = doc.documentElement.getBoundingClientRect();
+
+ let elements = [...doc.querySelectorAll(".anchor")];
+ for (let el of elements) {
+ info("Display the tooltip on an anchor.");
+ yield showTooltip(tooltip, el);
+
+ let arrow = tooltip.arrow;
+ ok(arrow, "Tooltip has an arrow");
+
+ // Get the geometry of the anchor, the tooltip panel & arrow.
+ let arrowBounds = arrow.getBoxQuads({relativeTo: doc})[0].bounds;
+ let panelBounds = tooltip.panel.getBoxQuads({relativeTo: doc})[0].bounds;
+ let anchorBounds = el.getBoxQuads({relativeTo: doc})[0].bounds;
+
+ let intersects = arrowBounds.left <= anchorBounds.right &&
+ arrowBounds.right >= anchorBounds.left;
+ let isBlockedByViewport = arrowBounds.left == 0 ||
+ arrowBounds.right == docRight;
+ ok(intersects || isBlockedByViewport,
+ "Tooltip arrow is aligned with the anchor, or stuck on viewport's edge.");
+
+ let isInPanel = arrowBounds.left >= panelBounds.left &&
+ arrowBounds.right <= panelBounds.right;
+ ok(isInPanel,
+ "The tooltip arrow remains inside the tooltip panel horizontally");
+
+ yield hideTooltip(tooltip);
+ }
+
+ tooltip.destroy();
+}
diff --git a/devtools/client/shared/test/browser_html_tooltip_arrow-02.js b/devtools/client/shared/test/browser_html_tooltip_arrow-02.js
new file mode 100644
index 000000000..098f1ac7b
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip_arrow-02.js
@@ -0,0 +1,100 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_html_tooltip.js */
+
+"use strict";
+
+/**
+ * Test the HTMLTooltip "arrow" type on wide anchors. The arrow should remain
+ * aligned with the anchors as much as possible
+ */
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const getAnchor = function (position) {
+ return `<html:div class="anchor" style="height: 5px;
+ position: absolute;
+ background: red;
+ ${position}"></html:div>`;
+};
+
+const TEST_URI = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
+ <?xml-stylesheet href="chrome://global/skin/global.css"?>
+ <?xml-stylesheet href="chrome://devtools/skin/light-theme.css"?>
+
+ <window class="theme-light"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Tooltip test">
+ <vbox flex="1" style="position: relative">
+ ${getAnchor("top: 0; left: 0; width: 50px;")}
+ ${getAnchor("top: 10px; left: 0; width: 100px;")}
+ ${getAnchor("top: 20px; left: 0; width: 150px;")}
+ ${getAnchor("top: 30px; left: 0; width: 200px;")}
+ ${getAnchor("top: 40px; left: 0; width: 250px;")}
+ ${getAnchor("top: 50px; left: 100px; width: 250px;")}
+ ${getAnchor("top: 100px; width: 50px; right: 0;")}
+ ${getAnchor("top: 110px; width: 100px; right: 0;")}
+ ${getAnchor("top: 120px; width: 150px; right: 0;")}
+ ${getAnchor("top: 130px; width: 200px; right: 0;")}
+ ${getAnchor("top: 140px; width: 250px; right: 0;")}
+ </vbox>
+ </window>`;
+
+const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+loadHelperScript("helper_html_tooltip.js");
+
+let useXulWrapper;
+
+add_task(function* () {
+ // Force the toolbox to be 200px high;
+ yield pushPref("devtools.toolbox.footer.height", 200);
+
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+
+ info("Run tests for a Tooltip without using a XUL panel");
+ useXulWrapper = false;
+ yield runTests(doc);
+
+ info("Run tests for a Tooltip with a XUL panel");
+ useXulWrapper = true;
+ yield runTests(doc);
+});
+
+function* runTests(doc) {
+ info("Create HTML tooltip");
+ let tooltip = new HTMLTooltip(doc, {type: "arrow", useXulWrapper});
+ let div = doc.createElementNS(HTML_NS, "div");
+ div.style.height = "35px";
+ tooltip.setContent(div, {width: 200, height: 35});
+
+ let {right: docRight} = doc.documentElement.getBoundingClientRect();
+
+ let elements = [...doc.querySelectorAll(".anchor")];
+ for (let el of elements) {
+ info("Display the tooltip on an anchor.");
+ yield showTooltip(tooltip, el);
+
+ let arrow = tooltip.arrow;
+ ok(arrow, "Tooltip has an arrow");
+
+ // Get the geometry of the anchor, the tooltip panel & arrow.
+ let arrowBounds = arrow.getBoxQuads({relativeTo: doc})[0].bounds;
+ let panelBounds = tooltip.panel.getBoxQuads({relativeTo: doc})[0].bounds;
+ let anchorBounds = el.getBoxQuads({relativeTo: doc})[0].bounds;
+
+ let intersects = arrowBounds.left <= anchorBounds.right &&
+ arrowBounds.right >= anchorBounds.left;
+ let isBlockedByViewport = arrowBounds.left == 0 ||
+ arrowBounds.right == docRight;
+ ok(intersects || isBlockedByViewport,
+ "Tooltip arrow is aligned with the anchor, or stuck on viewport's edge.");
+
+ let isInPanel = arrowBounds.left >= panelBounds.left &&
+ arrowBounds.right <= panelBounds.right;
+ ok(isInPanel,
+ "The tooltip arrow remains inside the tooltip panel horizontally");
+ yield hideTooltip(tooltip);
+ }
+
+ tooltip.destroy();
+}
diff --git a/devtools/client/shared/test/browser_html_tooltip_consecutive-show.js b/devtools/client/shared/test/browser_html_tooltip_consecutive-show.js
new file mode 100644
index 000000000..7ed1d6dc1
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip_consecutive-show.js
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_html_tooltip.js */
+
+"use strict";
+
+/**
+ * Test the HTMLTooltip show can be called several times. It should move according to the
+ * new anchor/options and should not leak event listeners.
+ */
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const TEST_URI = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
+ <?xml-stylesheet href="chrome://global/skin/global.css"?>
+ <?xml-stylesheet href="chrome://devtools/skin/tooltips.css"?>
+ <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Tooltip test">
+ <vbox flex="1">
+ <hbox id="box1" flex="1">test1</hbox>
+ <hbox id="box2" flex="1">test2</hbox>
+ <hbox id="box3" flex="1">test3</hbox>
+ <hbox id="box4" flex="1">test4</hbox>
+ </vbox>
+ </window>`;
+
+const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+loadHelperScript("helper_html_tooltip.js");
+
+function getTooltipContent(doc) {
+ let div = doc.createElementNS(HTML_NS, "div");
+ div.style.height = "50px";
+ div.textContent = "tooltip";
+ return div;
+}
+
+add_task(function* () {
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+
+ let box1 = doc.getElementById("box1");
+ let box2 = doc.getElementById("box2");
+ let box3 = doc.getElementById("box3");
+ let box4 = doc.getElementById("box4");
+
+ let width = 100, height = 50;
+
+ let tooltip = new HTMLTooltip(doc, {useXulWrapper: false});
+ tooltip.setContent(getTooltipContent(doc), {width, height});
+
+ info("Show the tooltip on each of the 4 hbox, without calling hide in between");
+
+ info("Show tooltip on box1");
+ tooltip.show(box1);
+ checkTooltipGeometry(tooltip, box1, {position: "bottom", width, height});
+
+ info("Show tooltip on box2");
+ tooltip.show(box2);
+ checkTooltipGeometry(tooltip, box2, {position: "bottom", width, height});
+
+ info("Show tooltip on box3");
+ tooltip.show(box3);
+ checkTooltipGeometry(tooltip, box3, {position: "top", width, height});
+
+ info("Show tooltip on box4");
+ tooltip.show(box4);
+ checkTooltipGeometry(tooltip, box4, {position: "top", width, height});
+
+ info("Hide tooltip before leaving test");
+ yield hideTooltip(tooltip);
+
+ tooltip.destroy();
+});
diff --git a/devtools/client/shared/test/browser_html_tooltip_hover.js b/devtools/client/shared/test/browser_html_tooltip_hover.js
new file mode 100644
index 000000000..8a661bbe1
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip_hover.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_html_tooltip.js */
+
+"use strict";
+
+/**
+ * Test the TooltipToggle helper class for HTMLTooltip
+ */
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const TEST_URI = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
+ <?xml-stylesheet href="chrome://global/skin/global.css"?>
+ <?xml-stylesheet href="resource://devtools/client/themes/variables.css"?>
+ <?xml-stylesheet href="chrome://devtools/skin/tooltips.css"?>
+ <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ class="theme-light" title="Tooltip hover test">
+ <vbox id="container" flex="1">
+ <hbox id="box1" flex="1"><label>test1</label></hbox>
+ <hbox id="box2" flex="1"><label>test2</label></hbox>
+ <hbox id="box3" flex="1"><label>test3</label></hbox>
+ <hbox id="box4" flex="1"><label>test4</label></hbox>
+ </vbox>
+ </window>`;
+
+const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+loadHelperScript("helper_html_tooltip.js");
+
+add_task(function* () {
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+ // Wait for full page load before synthesizing events on the page.
+ yield waitUntil(() => doc.readyState === "complete");
+
+ let width = 100, height = 50;
+ let tooltipContent = doc.createElementNS(HTML_NS, "div");
+ tooltipContent.textContent = "tooltip";
+ let tooltip = new HTMLTooltip(doc, {useXulWrapper: false});
+ tooltip.setContent(tooltipContent, {width, height});
+
+ let container = doc.getElementById("container");
+ tooltip.startTogglingOnHover(container, () => true);
+
+ info("Hover on each of the 4 boxes, expect the tooltip to appear");
+ function* showAndCheck(boxId, position) {
+ info(`Show tooltip on ${boxId}`);
+ let box = doc.getElementById(boxId);
+ let shown = tooltip.once("shown");
+ EventUtils.synthesizeMouseAtCenter(box, { type: "mousemove" }, doc.defaultView);
+ yield shown;
+ checkTooltipGeometry(tooltip, box, {position, width, height});
+ }
+
+ yield showAndCheck("box1", "bottom");
+ yield showAndCheck("box2", "bottom");
+ yield showAndCheck("box3", "top");
+ yield showAndCheck("box4", "top");
+
+ info("Move out of the container");
+ let hidden = tooltip.once("hidden");
+ EventUtils.synthesizeMouseAtCenter(container, { type: "mouseout" }, doc.defaultView);
+ yield hidden;
+
+ info("Destroy the tooltip and finish");
+ tooltip.destroy();
+});
diff --git a/devtools/client/shared/test/browser_html_tooltip_offset.js b/devtools/client/shared/test/browser_html_tooltip_offset.js
new file mode 100644
index 000000000..dfbdef723
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip_offset.js
@@ -0,0 +1,99 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_html_tooltip.js */
+"use strict";
+
+/**
+ * Test the HTMLTooltip can be displayed with vertical and horizontal offsets.
+ */
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const TEST_URI = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
+ <?xml-stylesheet href="chrome://global/skin/global.css"?>
+ <?xml-stylesheet href="chrome://devtools/skin/tooltips.css"?>
+ <window
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ htmlns="http://www.w3.org/1999/xhtml"
+ title="Tooltip test">
+ <vbox flex="1">
+ <hbox id="box1" flex="1">test1</hbox>
+ <hbox id="box2" flex="1">test2</hbox>
+ <hbox id="box3" flex="1">test3</hbox>
+ <hbox id="box4" flex="1">test4</hbox>
+ </vbox>
+ </window>`;
+
+const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+loadHelperScript("helper_html_tooltip.js");
+
+add_task(function* () {
+ // Force the toolbox to be 200px high;
+ yield pushPref("devtools.toolbox.footer.height", 200);
+
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+
+ info("Test a tooltip is not closed when clicking inside itself");
+
+ let box1 = doc.getElementById("box1");
+ let box2 = doc.getElementById("box2");
+ let box3 = doc.getElementById("box3");
+ let box4 = doc.getElementById("box4");
+
+ let tooltip = new HTMLTooltip(doc, {useXulWrapper: false});
+
+ let div = doc.createElementNS(HTML_NS, "div");
+ div.style.height = "100px";
+ div.style.boxSizing = "border-box";
+ div.textContent = "tooltip";
+ tooltip.setContent(div, {width: 50, height: 100});
+
+ info("Display the tooltip on box1.");
+ yield showTooltip(tooltip, box1, {x: 5, y: 10});
+
+ let panelRect = tooltip.container.getBoundingClientRect();
+ let anchorRect = box1.getBoundingClientRect();
+
+ // Tooltip will be displayed below box1
+ is(panelRect.top, anchorRect.bottom + 10, "Tooltip top has 10px offset");
+ is(panelRect.left, anchorRect.left + 5, "Tooltip left has 5px offset");
+ is(panelRect.height, 100, "Tooltip height is at 100px as expected");
+
+ info("Display the tooltip on box2.");
+ yield showTooltip(tooltip, box2, {x: 5, y: 10});
+
+ panelRect = tooltip.container.getBoundingClientRect();
+ anchorRect = box2.getBoundingClientRect();
+
+ // Tooltip will be displayed below box2, but can't be fully displayed because of the
+ // offset
+ is(panelRect.top, anchorRect.bottom + 10, "Tooltip top has 10px offset");
+ is(panelRect.left, anchorRect.left + 5, "Tooltip left has 5px offset");
+ is(panelRect.height, 90, "Tooltip height is only 90px");
+
+ info("Display the tooltip on box3.");
+ yield showTooltip(tooltip, box3, {x: 5, y: 10});
+
+ panelRect = tooltip.container.getBoundingClientRect();
+ anchorRect = box3.getBoundingClientRect();
+
+ // Tooltip will be displayed above box3, but can't be fully displayed because of the
+ // offset
+ is(panelRect.bottom, anchorRect.top - 10, "Tooltip bottom is 10px above anchor");
+ is(panelRect.left, anchorRect.left + 5, "Tooltip left has 5px offset");
+ is(panelRect.height, 90, "Tooltip height is only 90px");
+
+ info("Display the tooltip on box4.");
+ yield showTooltip(tooltip, box4, {x: 5, y: 10});
+
+ panelRect = tooltip.container.getBoundingClientRect();
+ anchorRect = box4.getBoundingClientRect();
+
+ // Tooltip will be displayed above box4
+ is(panelRect.bottom, anchorRect.top - 10, "Tooltip bottom is 10px above anchor");
+ is(panelRect.left, anchorRect.left + 5, "Tooltip left has 5px offset");
+ is(panelRect.height, 100, "Tooltip height is at 100px as expected");
+
+ yield hideTooltip(tooltip);
+
+ tooltip.destroy();
+});
diff --git a/devtools/client/shared/test/browser_html_tooltip_rtl.js b/devtools/client/shared/test/browser_html_tooltip_rtl.js
new file mode 100644
index 000000000..e41716c80
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip_rtl.js
@@ -0,0 +1,140 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_html_tooltip.js */
+"use strict";
+
+/**
+ * Test the HTMLTooltip anchor alignment changes with the anchor direction.
+ * - should be aligned to the right of RTL anchors
+ * - should be aligned to the left of LTR anchors
+ */
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const TEST_URI = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
+ <?xml-stylesheet href="chrome://global/skin/global.css"?>
+ <?xml-stylesheet href="chrome://devtools/skin/tooltips.css"?>
+ <window
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ htmlns="http://www.w3.org/1999/xhtml"
+ title="Tooltip test">
+ <hbox style="padding: 90px 0;" flex="1">
+ <hbox id="box1" flex="1" style="background:red; direction: rtl;">test1</hbox>
+ <hbox id="box2" flex="1" style="background:blue; direction: rtl;">test2</hbox>
+ <hbox id="box3" flex="1" style="background:red; direction: ltr;">test3</hbox>
+ <hbox id="box4" flex="1" style="background:blue; direction: ltr;">test4</hbox>
+ </hbox>
+ </window>`;
+
+const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+loadHelperScript("helper_html_tooltip.js");
+
+const TOOLBOX_WIDTH = 500;
+const TOOLTIP_WIDTH = 150;
+const TOOLTIP_HEIGHT = 30;
+
+add_task(function* () {
+ // Force the toolbox to be 500px wide (min width is 465px);
+ yield pushPref("devtools.toolbox.sidebar.width", TOOLBOX_WIDTH);
+
+ let [,, doc] = yield createHost("side", TEST_URI);
+
+ info("Test a tooltip is not closed when clicking inside itself");
+
+ let tooltip = new HTMLTooltip(doc, {useXulWrapper: false});
+ let div = doc.createElementNS(HTML_NS, "div");
+ div.textContent = "tooltip";
+ div.style.cssText = "box-sizing: border-box; border: 1px solid black";
+ tooltip.setContent(div, {width: TOOLTIP_WIDTH, height: TOOLTIP_HEIGHT});
+
+ yield testRtlAnchors(doc, tooltip);
+ yield testLtrAnchors(doc, tooltip);
+ yield hideTooltip(tooltip);
+
+ tooltip.destroy();
+});
+
+function* testRtlAnchors(doc, tooltip) {
+ /*
+ * The layout of the test page is as follows:
+ * _______________________________
+ * | toolbox |
+ * | _____ _____ _____ _____ |
+ * || | | | | | | ||
+ * || box1| | box2| | box3| | box4||
+ * ||_____| |_____| |_____| |_____||
+ * |_______________________________|
+ *
+ * - box1 is aligned with the left edge of the toolbox
+ * - box2 is displayed right after box1
+ * - total toolbox width is 500px so each box is 125px wide
+ */
+
+ let box1 = doc.getElementById("box1");
+ let box2 = doc.getElementById("box2");
+
+ info("Display the tooltip on box1.");
+ yield showTooltip(tooltip, box1, {position: "bottom"});
+
+ let panelRect = tooltip.container.getBoundingClientRect();
+ let anchorRect = box1.getBoundingClientRect();
+
+ // box1 uses RTL direction, so the tooltip should be aligned with the right edge of the
+ // anchor, but it is shifted to the right to fit in the toolbox.
+ is(panelRect.left, 0, "Tooltip is aligned with left edge of the toolbox");
+ is(panelRect.top, anchorRect.bottom, "Tooltip aligned with the anchor bottom edge");
+ is(panelRect.height, TOOLTIP_HEIGHT, "Tooltip height is at 100px as expected");
+
+ info("Display the tooltip on box2.");
+ yield showTooltip(tooltip, box2, {position: "bottom"});
+
+ panelRect = tooltip.container.getBoundingClientRect();
+ anchorRect = box2.getBoundingClientRect();
+
+ // box2 uses RTL direction, so the tooltip is aligned with the right edge of the anchor
+ is(panelRect.right, anchorRect.right, "Tooltip is aligned with right edge of anchor");
+ is(panelRect.top, anchorRect.bottom, "Tooltip aligned with the anchor bottom edge");
+ is(panelRect.height, TOOLTIP_HEIGHT, "Tooltip height is at 100px as expected");
+}
+
+function* testLtrAnchors(doc, tooltip) {
+ /*
+ * The layout of the test page is as follows:
+ * _______________________________
+ * | toolbox |
+ * | _____ _____ _____ _____ |
+ * || | | | | | | ||
+ * || box1| | box2| | box3| | box4||
+ * ||_____| |_____| |_____| |_____||
+ * |_______________________________|
+ *
+ * - box3 is is displayed right after box2
+ * - box4 is aligned with the right edge of the toolbox
+ * - total toolbox width is 500px so each box is 125px wide
+ */
+
+ let box3 = doc.getElementById("box3");
+ let box4 = doc.getElementById("box4");
+
+ info("Display the tooltip on box3.");
+ yield showTooltip(tooltip, box3, {position: "bottom"});
+
+ let panelRect = tooltip.container.getBoundingClientRect();
+ let anchorRect = box3.getBoundingClientRect();
+
+ // box3 uses LTR direction, so the tooltip is aligned with the left edge of the anchor.
+ is(panelRect.left, anchorRect.left, "Tooltip is aligned with left edge of anchor");
+ is(panelRect.top, anchorRect.bottom, "Tooltip aligned with the anchor bottom edge");
+ is(panelRect.height, TOOLTIP_HEIGHT, "Tooltip height is at 100px as expected");
+
+ info("Display the tooltip on box4.");
+ yield showTooltip(tooltip, box4, {position: "bottom"});
+
+ panelRect = tooltip.container.getBoundingClientRect();
+ anchorRect = box4.getBoundingClientRect();
+
+ // box4 uses LTR direction, so the tooltip should be aligned with the left edge of the
+ // anchor, but it is shifted to the left to fit in the toolbox.
+ is(panelRect.right, TOOLBOX_WIDTH, "Tooltip is aligned with right edge of toolbox");
+ is(panelRect.top, anchorRect.bottom, "Tooltip aligned with the anchor bottom edge");
+ is(panelRect.height, TOOLTIP_HEIGHT, "Tooltip height is at 100px as expected");
+}
diff --git a/devtools/client/shared/test/browser_html_tooltip_variable-height.js b/devtools/client/shared/test/browser_html_tooltip_variable-height.js
new file mode 100644
index 000000000..e64ec4a2e
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip_variable-height.js
@@ -0,0 +1,75 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_html_tooltip.js */
+
+"use strict";
+
+/**
+ * Test the HTMLTooltip content can have a variable height.
+ */
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const TEST_URI = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
+ <?xml-stylesheet href="chrome://global/skin/global.css"?>
+ <?xml-stylesheet href="chrome://devtools/skin/tooltips.css"?>
+ <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Tooltip test">
+ <vbox flex="1">
+ <hbox id="box1" flex="1">test1</hbox>
+ <hbox id="box2" flex="1">test2</hbox>
+ <hbox id="box3" flex="1">test3</hbox>
+ <hbox id="box4" flex="1">test4</hbox>
+ </vbox>
+ </window>`;
+
+const CONTAINER_HEIGHT = 300;
+const CONTAINER_WIDTH = 200;
+const TOOLTIP_HEIGHT = 50;
+
+const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+loadHelperScript("helper_html_tooltip.js");
+
+add_task(function* () {
+ // Force the toolbox to be 400px tall => 50px for each box.
+ yield pushPref("devtools.toolbox.footer.height", 400);
+
+ yield addTab("about:blank");
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+
+ let tooltip = new HTMLTooltip(doc, {useXulWrapper: false});
+ info("Set tooltip content 50px tall, but request a container 200px tall");
+ let tooltipContent = doc.createElementNS(HTML_NS, "div");
+ tooltipContent.style.cssText = "height: " + TOOLTIP_HEIGHT + "px; background: red;";
+ tooltip.setContent(tooltipContent, {width: CONTAINER_WIDTH, height: Infinity});
+
+ info("Show the tooltip and check the container and panel height.");
+ yield showTooltip(tooltip, doc.getElementById("box1"));
+
+ let containerRect = tooltip.container.getBoundingClientRect();
+ let panelRect = tooltip.panel.getBoundingClientRect();
+ is(containerRect.height, CONTAINER_HEIGHT,
+ "Tooltip container has the expected height.");
+ is(panelRect.height, TOOLTIP_HEIGHT, "Tooltip panel has the expected height.");
+
+ info("Click below the tooltip panel but in the tooltip filler element.");
+ let onHidden = once(tooltip, "hidden");
+ EventUtils.synthesizeMouse(tooltip.container, 100, 100, {}, doc.defaultView);
+ yield onHidden;
+
+ info("Show the tooltip one more time, and increase the content height");
+ yield showTooltip(tooltip, doc.getElementById("box1"));
+ tooltipContent.style.height = (2 * CONTAINER_HEIGHT) + "px";
+
+ info("Click at the same coordinates as earlier, this time it should hit the tooltip.");
+ let onPanelClick = once(tooltip.panel, "click");
+ EventUtils.synthesizeMouse(tooltip.container, 100, 100, {}, doc.defaultView);
+ yield onPanelClick;
+ is(tooltip.isVisible(), true, "Tooltip is still visible");
+
+ info("Click above the tooltip container, the tooltip should be closed.");
+ onHidden = once(tooltip, "hidden");
+ EventUtils.synthesizeMouse(tooltip.container, 100, -10, {}, doc.defaultView);
+ yield onHidden;
+
+ tooltip.destroy();
+});
diff --git a/devtools/client/shared/test/browser_html_tooltip_width-auto.js b/devtools/client/shared/test/browser_html_tooltip_width-auto.js
new file mode 100644
index 000000000..66e33673e
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip_width-auto.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_html_tooltip.js */
+
+"use strict";
+
+/**
+ * Test the HTMLTooltip content can automatically calculate its width based on content.
+ */
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const TEST_URI = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
+ <?xml-stylesheet href="chrome://global/skin/global.css"?>
+ <?xml-stylesheet href="chrome://devtools/skin/tooltips.css"?>
+ <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Tooltip test">
+ <vbox flex="1">
+ <hbox id="box1" flex="1">test1</hbox>
+ <hbox id="box2" flex="1">test2</hbox>
+ <hbox id="box3" flex="1">test3</hbox>
+ <hbox id="box4" flex="1">test4</hbox>
+ </vbox>
+ </window>`;
+
+const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+loadHelperScript("helper_html_tooltip.js");
+
+let useXulWrapper;
+
+add_task(function* () {
+ yield addTab("about:blank");
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+
+ info("Run tests for a Tooltip without using a XUL panel");
+ useXulWrapper = false;
+ yield runTests(doc);
+
+ info("Run tests for a Tooltip with a XUL panel");
+ useXulWrapper = true;
+ yield runTests(doc);
+});
+
+function* runTests(doc) {
+ let tooltip = new HTMLTooltip(doc, {useXulWrapper});
+ info("Create tooltip content width to 150px");
+ let tooltipContent = doc.createElementNS(HTML_NS, "div");
+ tooltipContent.style.cssText = "height: 100%; width: 150px; background: red;";
+
+ info("Set tooltip content using width:auto");
+ tooltip.setContent(tooltipContent, {width: "auto", height: 50});
+
+ info("Show the tooltip and check the tooltip panel width.");
+ yield showTooltip(tooltip, doc.getElementById("box1"));
+
+ let panelRect = tooltip.panel.getBoundingClientRect();
+ is(panelRect.width, 150, "Tooltip panel has the expected width.");
+
+ yield hideTooltip(tooltip);
+
+ tooltip.destroy();
+}
diff --git a/devtools/client/shared/test/browser_html_tooltip_xul-wrapper.js b/devtools/client/shared/test/browser_html_tooltip_xul-wrapper.js
new file mode 100644
index 000000000..5c21f21c3
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip_xul-wrapper.js
@@ -0,0 +1,80 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_html_tooltip.js */
+
+"use strict";
+
+/**
+ * Test the HTMLTooltip can overflow out of the toolbox when using a XUL panel wrapper.
+ */
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const TEST_URI = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
+ <?xml-stylesheet href="chrome://global/skin/global.css"?>
+ <?xml-stylesheet href="chrome://devtools/skin/tooltips.css"?>
+ <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Tooltip test">
+ <vbox flex="1">
+ <hbox id="box1" style="height: 50px">test1</hbox>
+ <hbox id="box2" style="height: 50px">test2</hbox>
+ <hbox id="box3" style="height: 50px">test3</hbox>
+ <hbox id="box4" style="height: 50px">test4</hbox>
+ </vbox>
+ </window>`;
+
+const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+loadHelperScript("helper_html_tooltip.js");
+
+// The test toolbox will be 200px tall, the anchors are 50px tall, therefore, the maximum
+// tooltip height that could fit in the toolbox is 150px. Setting 160px, the tooltip will
+// either have to overflow or to be resized.
+const TOOLTIP_HEIGHT = 160;
+const TOOLTIP_WIDTH = 200;
+
+add_task(function* () {
+ // Force the toolbox to be 200px high;
+ yield pushPref("devtools.toolbox.footer.height", 200);
+
+ let [, win, doc] = yield createHost("bottom", TEST_URI);
+
+ info("Resizing window to have some space below the window.");
+ let originalWidth = win.top.outerWidth;
+ let originalHeight = win.top.outerHeight;
+ win.top.resizeBy(0, -100);
+
+ info("Create HTML tooltip");
+ let tooltip = new HTMLTooltip(doc, {useXulWrapper: true});
+ let div = doc.createElementNS(HTML_NS, "div");
+ div.style.height = "200px";
+ div.style.background = "red";
+ tooltip.setContent(div, {width: TOOLTIP_WIDTH, height: TOOLTIP_HEIGHT});
+
+ let box1 = doc.getElementById("box1");
+
+ // Above box1: check that the tooltip can overflow onto the content page.
+ info("Display the tooltip above box1.");
+ yield showTooltip(tooltip, box1, {position: "top"});
+ checkTooltip(tooltip, "top", TOOLTIP_HEIGHT);
+ yield hideTooltip(tooltip);
+
+ // Below box1: check that the tooltip can overflow out of the browser window.
+ info("Display the tooltip below box1.");
+ yield showTooltip(tooltip, box1, {position: "bottom"});
+ checkTooltip(tooltip, "bottom", TOOLTIP_HEIGHT);
+ yield hideTooltip(tooltip);
+
+ is(tooltip.isVisible(), false, "Tooltip is not visible");
+
+ tooltip.destroy();
+
+ info("Restore original window dimensions.");
+ win.top.resizeTo(originalWidth, originalHeight);
+});
+
+function checkTooltip(tooltip, position, height) {
+ is(tooltip.position, position, "Actual tooltip position is " + position);
+ let rect = tooltip.container.getBoundingClientRect();
+ is(rect.height, height, "Actual tooltip height is " + height);
+ // Testing the actual left/top offsets is not relevant here as it is handled by the XUL
+ // panel.
+}
diff --git a/devtools/client/shared/test/browser_inplace-editor-01.js b/devtools/client/shared/test/browser_inplace-editor-01.js
new file mode 100644
index 000000000..6308602f1
--- /dev/null
+++ b/devtools/client/shared/test/browser_inplace-editor-01.js
@@ -0,0 +1,150 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_inplace_editor.js */
+
+"use strict";
+
+loadHelperScript("helper_inplace_editor.js");
+
+// Test the inplace-editor behavior.
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8,inline editor tests");
+ let [host, , doc] = yield createHost();
+
+ yield testMultipleInitialization(doc);
+ yield testReturnCommit(doc);
+ yield testBlurCommit(doc);
+ yield testAdvanceCharCommit(doc);
+ yield testAdvanceCharsFunction(doc);
+ yield testEscapeCancel(doc);
+
+ host.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+function testMultipleInitialization(doc) {
+ doc.body.innerHTML = "";
+ let options = {};
+ let span = options.element = createSpan(doc);
+
+ info("Creating multiple inplace-editor fields");
+ editableField(options);
+ editableField(options);
+
+ info("Clicking on the inplace-editor field to turn to edit mode");
+ span.click();
+
+ is(span.style.display, "none", "The original <span> is hidden");
+ is(doc.querySelectorAll("input").length, 1, "Only one <input>");
+ is(doc.querySelectorAll("span").length, 2,
+ "Correct number of <span> elements");
+ is(doc.querySelectorAll("span.autosizer").length, 1,
+ "There is an autosizer element");
+}
+
+function testReturnCommit(doc) {
+ info("Testing that pressing return commits the new value");
+ let def = defer();
+
+ createInplaceEditorAndClick({
+ initial: "explicit initial",
+ start: function (editor) {
+ is(editor.input.value, "explicit initial",
+ "Explicit initial value should be used.");
+ editor.input.value = "Test Value";
+ EventUtils.sendKey("return");
+ },
+ done: onDone("Test Value", true, def)
+ }, doc);
+
+ return def.promise;
+}
+
+function testBlurCommit(doc) {
+ info("Testing that bluring the field commits the new value");
+ let def = defer();
+
+ createInplaceEditorAndClick({
+ start: function (editor) {
+ is(editor.input.value, "Edit Me!", "textContent of the span used.");
+ editor.input.value = "Test Value";
+ editor.input.blur();
+ },
+ done: onDone("Test Value", true, def)
+ }, doc, "Edit Me!");
+
+ return def.promise;
+}
+
+function testAdvanceCharCommit(doc) {
+ info("Testing that configured advanceChars commit the new value");
+ let def = defer();
+
+ createInplaceEditorAndClick({
+ advanceChars: ":",
+ start: function (editor) {
+ EventUtils.sendString("Test:");
+ },
+ done: onDone("Test", true, def)
+ }, doc);
+
+ return def.promise;
+}
+
+function testAdvanceCharsFunction(doc) {
+ info("Testing advanceChars as a function");
+ let def = defer();
+
+ let firstTime = true;
+
+ createInplaceEditorAndClick({
+ initial: "",
+ advanceChars: function (charCode, text, insertionPoint) {
+ if (charCode !== Components.interfaces.nsIDOMKeyEvent.DOM_VK_COLON) {
+ return false;
+ }
+ if (firstTime) {
+ firstTime = false;
+ return false;
+ }
+
+ // Just to make sure we check it somehow.
+ return text.length > 0;
+ },
+ start: function (editor) {
+ for (let ch of ":Test:") {
+ EventUtils.sendChar(ch);
+ }
+ },
+ done: onDone(":Test", true, def)
+ }, doc);
+
+ return def.promise;
+}
+
+function testEscapeCancel(doc) {
+ info("Testing that escape cancels the new value");
+ let def = defer();
+
+ createInplaceEditorAndClick({
+ initial: "initial text",
+ start: function (editor) {
+ editor.input.value = "Test Value";
+ EventUtils.sendKey("escape");
+ },
+ done: onDone("initial text", false, def)
+ }, doc);
+
+ return def.promise;
+}
+
+function onDone(value, isCommit, def) {
+ return function (actualValue, actualCommit) {
+ info("Inplace-editor's done callback executed, checking its state");
+ is(actualValue, value, "The value is correct");
+ is(actualCommit, isCommit, "The commit boolean is correct");
+ def.resolve();
+ };
+}
diff --git a/devtools/client/shared/test/browser_inplace-editor-02.js b/devtools/client/shared/test/browser_inplace-editor-02.js
new file mode 100644
index 000000000..811c30123
--- /dev/null
+++ b/devtools/client/shared/test/browser_inplace-editor-02.js
@@ -0,0 +1,71 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_inplace_editor.js */
+
+"use strict";
+
+loadHelperScript("helper_inplace_editor.js");
+
+// Test that the trimOutput option for the inplace editor works correctly.
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8,inline editor tests");
+ let [host, , doc] = yield createHost();
+
+ yield testNonTrimmed(doc);
+ yield testTrimmed(doc);
+
+ host.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+function testNonTrimmed(doc) {
+ info("Testing the trimOutput=false option");
+ let def = defer();
+
+ let initial = "\nMultiple\nLines\n";
+ let changed = " \nMultiple\nLines\n with more whitespace ";
+ createInplaceEditorAndClick({
+ trimOutput: false,
+ multiline: true,
+ initial: initial,
+ start: function (editor) {
+ is(editor.input.value, initial, "Explicit initial value should be used.");
+ editor.input.value = changed;
+ EventUtils.sendKey("return");
+ },
+ done: onDone(changed, true, def)
+ }, doc);
+
+ return def.promise;
+}
+
+function testTrimmed(doc) {
+ info("Testing the trimOutput=true option (default value)");
+ let def = defer();
+
+ let initial = "\nMultiple\nLines\n";
+ let changed = " \nMultiple\nLines\n with more whitespace ";
+ createInplaceEditorAndClick({
+ initial: initial,
+ multiline: true,
+ start: function (editor) {
+ is(editor.input.value, initial, "Explicit initial value should be used.");
+ editor.input.value = changed;
+ EventUtils.sendKey("return");
+ },
+ done: onDone(changed.trim(), true, def)
+ }, doc);
+
+ return def.promise;
+}
+
+function onDone(value, isCommit, def) {
+ return function (actualValue, actualCommit) {
+ info("Inplace-editor's done callback executed, checking its state");
+ is(actualValue, value, "The value is correct");
+ is(actualCommit, isCommit, "The commit boolean is correct");
+ def.resolve();
+ };
+}
diff --git a/devtools/client/shared/test/browser_inplace-editor_autocomplete_01.js b/devtools/client/shared/test/browser_inplace-editor_autocomplete_01.js
new file mode 100644
index 000000000..e9ceb11ad
--- /dev/null
+++ b/devtools/client/shared/test/browser_inplace-editor_autocomplete_01.js
@@ -0,0 +1,75 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_inplace_editor.js */
+
+"use strict";
+
+const { InplaceEditor } = require("devtools/client/shared/inplace-editor");
+const { AutocompletePopup } = require("devtools/client/shared/autocomplete-popup");
+loadHelperScript("helper_inplace_editor.js");
+
+// Test the inplace-editor autocomplete popup for CSS properties suggestions.
+// Using a mocked list of CSS properties to avoid test failures linked to
+// engine changes (new property, removed property, ...).
+
+// format :
+// [
+// what key to press,
+// expected input box value after keypress,
+// selected suggestion index (-1 if popup is hidden),
+// number of suggestions in the popup (0 if popup is hidden),
+// ]
+const testData = [
+ ["b", "border", 1, 3],
+ ["VK_DOWN", "box-sizing", 2, 3],
+ ["VK_DOWN", "background", 0, 3],
+ ["VK_DOWN", "border", 1, 3],
+ ["VK_BACK_SPACE", "b", -1, 0],
+ ["VK_BACK_SPACE", "", -1, 0],
+ ["VK_DOWN", "background", 0, 6],
+ ["VK_LEFT", "background", -1, 0],
+];
+
+const mockGetCSSPropertyList = function () {
+ return [
+ "background",
+ "border",
+ "box-sizing",
+ "color",
+ "display",
+ "visibility",
+ ];
+};
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," +
+ "inplace editor CSS property autocomplete");
+ let [host, win, doc] = yield createHost();
+
+ let xulDocument = win.top.document;
+ let popup = new AutocompletePopup(xulDocument, { autoSelect: true });
+ yield new Promise(resolve => {
+ createInplaceEditorAndClick({
+ start: runPropertyAutocompletionTest,
+ contentType: InplaceEditor.CONTENT_TYPES.CSS_PROPERTY,
+ done: resolve,
+ popup: popup
+ }, doc);
+ });
+
+ popup.destroy();
+ host.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+let runPropertyAutocompletionTest = Task.async(function* (editor) {
+ info("Starting to test for css property completion");
+ editor._getCSSPropertyList = mockGetCSSPropertyList;
+
+ for (let data of testData) {
+ yield testCompletion(data, editor);
+ }
+
+ EventUtils.synthesizeKey("VK_RETURN", {}, editor.input.defaultView);
+});
diff --git a/devtools/client/shared/test/browser_inplace-editor_autocomplete_02.js b/devtools/client/shared/test/browser_inplace-editor_autocomplete_02.js
new file mode 100644
index 000000000..3100026b4
--- /dev/null
+++ b/devtools/client/shared/test/browser_inplace-editor_autocomplete_02.js
@@ -0,0 +1,80 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_inplace_editor.js */
+
+"use strict";
+
+const { InplaceEditor } = require("devtools/client/shared/inplace-editor");
+const { AutocompletePopup } = require("devtools/client/shared/autocomplete-popup");
+loadHelperScript("helper_inplace_editor.js");
+
+// Test the inplace-editor autocomplete popup for CSS values suggestions.
+// Using a mocked list of CSS properties to avoid test failures linked to
+// engine changes (new property, removed property, ...).
+
+// format :
+// [
+// what key to press,
+// expected input box value after keypress,
+// selected suggestion index (-1 if popup is hidden),
+// number of suggestions in the popup (0 if popup is hidden),
+// ]
+const testData = [
+ ["b", "block", -1, 0],
+ ["VK_BACK_SPACE", "b", -1, 0],
+ ["VK_BACK_SPACE", "", -1, 0],
+ ["i", "inline", 0, 2],
+ ["VK_DOWN", "inline-block", 1, 2],
+ ["VK_DOWN", "inline", 0, 2],
+ ["VK_LEFT", "inline", -1, 0],
+];
+
+const mockGetCSSValuesForPropertyName = function (propertyName) {
+ let values = {
+ "display": [
+ "block",
+ "flex",
+ "inline",
+ "inline-block",
+ "none",
+ ]
+ };
+ return values[propertyName] || [];
+};
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," +
+ "inplace editor CSS value autocomplete");
+ let [host, win, doc] = yield createHost();
+
+ let xulDocument = win.top.document;
+ let popup = new AutocompletePopup(xulDocument, { autoSelect: true });
+
+ yield new Promise(resolve => {
+ createInplaceEditorAndClick({
+ start: runAutocompletionTest,
+ contentType: InplaceEditor.CONTENT_TYPES.CSS_VALUE,
+ property: {
+ name: "display"
+ },
+ done: resolve,
+ popup: popup
+ }, doc);
+ });
+
+ popup.destroy();
+ host.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+let runAutocompletionTest = Task.async(function* (editor) {
+ info("Starting to test for css property completion");
+ editor._getCSSValuesForPropertyName = mockGetCSSValuesForPropertyName;
+
+ for (let data of testData) {
+ yield testCompletion(data, editor);
+ }
+
+ EventUtils.synthesizeKey("VK_RETURN", {}, editor.input.defaultView);
+});
diff --git a/devtools/client/shared/test/browser_inplace-editor_autocomplete_offset.js b/devtools/client/shared/test/browser_inplace-editor_autocomplete_offset.js
new file mode 100644
index 000000000..5d1737bfd
--- /dev/null
+++ b/devtools/client/shared/test/browser_inplace-editor_autocomplete_offset.js
@@ -0,0 +1,119 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_inplace_editor.js */
+
+"use strict";
+
+const { InplaceEditor } = require("devtools/client/shared/inplace-editor");
+const { AutocompletePopup } = require("devtools/client/shared/autocomplete-popup");
+loadHelperScript("helper_inplace_editor.js");
+
+const TEST_URI = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
+ <?xml-stylesheet href="chrome://global/skin/global.css"?>
+ <?xml-stylesheet href="resource://devtools/client/themes/common.css"?>
+ <?xml-stylesheet href="chrome://devtools/skin/tooltips.css"?>
+ <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Tooltip test">
+ </window>`;
+
+// Test the inplace-editor autocomplete popup is aligned with the completed query.
+// Which means when completing "style=display:flex; color:" the popup will aim to be
+// aligned with the ":" next to "color".
+
+// format :
+// [
+// what key to press,
+// expected input box value after keypress,
+// selected suggestion index (-1 if popup is hidden),
+// number of suggestions in the popup (0 if popup is hidden),
+// ]
+// or
+// ["checkPopupOffset"]
+// to measure and test the autocomplete popup left offset.
+const testData = [
+ ["VK_RIGHT", "style=", -1, 0],
+ ["d", "style=display", 1, 2],
+ ["checkPopupOffset"],
+ ["VK_RIGHT", "style=display", -1, 0],
+ [":", "style=display:block", 0, 3],
+ ["checkPopupOffset"],
+ ["f", "style=display:flex", -1, 0],
+ ["VK_RIGHT", "style=display:flex", -1, 0],
+ [";", "style=display:flex;", -1, 0],
+ ["c", "style=display:flex;color", 1, 2],
+ ["checkPopupOffset"],
+ ["VK_RIGHT", "style=display:flex;color", -1, 0],
+ [":", "style=display:flex;color:blue", 0, 2],
+ ["checkPopupOffset"],
+];
+
+const mockGetCSSPropertyList = function () {
+ return [
+ "clear",
+ "color",
+ "direction",
+ "display",
+ ];
+};
+
+const mockGetCSSValuesForPropertyName = function (propertyName) {
+ let values = {
+ "color": ["blue", "red"],
+ "display": ["block", "flex", "none"]
+ };
+ return values[propertyName] || [];
+};
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8,inplace editor CSS value autocomplete");
+ let [host,, doc] = yield createHost("bottom", TEST_URI);
+
+ let popup = new AutocompletePopup(doc, { autoSelect: true });
+
+ info("Create a CSS_MIXED type autocomplete");
+ yield new Promise(resolve => {
+ createInplaceEditorAndClick({
+ initial: "style=",
+ start: runAutocompletionTest,
+ contentType: InplaceEditor.CONTENT_TYPES.CSS_MIXED,
+ done: resolve,
+ popup: popup
+ }, doc);
+ });
+
+ popup.destroy();
+ host.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+let runAutocompletionTest = Task.async(function* (editor) {
+ info("Starting autocomplete test for inplace-editor popup offset");
+ editor._getCSSPropertyList = mockGetCSSPropertyList;
+ editor._getCSSValuesForPropertyName = mockGetCSSValuesForPropertyName;
+
+ let previousOffset = -1;
+ for (let data of testData) {
+ if (data[0] === "checkPopupOffset") {
+ info("Check the popup offset has been modified");
+ // We are not testing hard coded offset values here, which could be fragile. We only
+ // want to ensure the popup tries to match the position of the query in the editor
+ // input.
+ let offset = getPopupOffset(editor);
+ ok(offset > previousOffset, "New popup offset is greater than the previous one");
+ previousOffset = offset;
+ } else {
+ yield testCompletion(data, editor);
+ }
+ }
+
+ EventUtils.synthesizeKey("VK_RETURN", {}, editor.input.defaultView);
+});
+
+/**
+ * Get the autocomplete panel left offset, relative to the provided input's left offset.
+ */
+function getPopupOffset({popup, input}) {
+ let popupQuads = popup._panel.getBoxQuads({relativeTo: input});
+ return popupQuads[0].bounds.left;
+}
diff --git a/devtools/client/shared/test/browser_inplace-editor_maxwidth.js b/devtools/client/shared/test/browser_inplace-editor_maxwidth.js
new file mode 100644
index 000000000..205f4418e
--- /dev/null
+++ b/devtools/client/shared/test/browser_inplace-editor_maxwidth.js
@@ -0,0 +1,114 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_inplace_editor.js */
+
+"use strict";
+
+loadHelperScript("helper_inplace_editor.js");
+
+const MAX_WIDTH = 300;
+const START_TEXT = "Start text";
+const LONG_TEXT = "I am a long text and I will not fit in a 300px container. " +
+ "I expect the inplace editor to wrap.";
+
+// Test the inplace-editor behavior with a maxWidth configuration option
+// defined.
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8,inplace editor max width tests");
+ let [host, , doc] = yield createHost();
+
+ info("Testing the maxWidth option in pixels, to precisely check the size");
+ yield new Promise(resolve => {
+ createInplaceEditorAndClick({
+ multiline: true,
+ maxWidth: MAX_WIDTH,
+ start: testMaxWidth,
+ done: resolve
+ }, doc, START_TEXT);
+ });
+
+ host.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+let testMaxWidth = Task.async(function* (editor) {
+ is(editor.input.value, START_TEXT, "Span text content should be used");
+ ok(editor.input.offsetWidth < MAX_WIDTH,
+ "Input width should be strictly smaller than MAX_WIDTH");
+ is(getLines(editor.input), 1, "Input should display 1 line of text");
+
+ info("Check a text is on several lines if it does not fit MAX_WIDTH");
+ for (let key of LONG_TEXT) {
+ EventUtils.sendChar(key);
+ checkScrollbars(editor.input);
+ }
+
+ is(editor.input.value, LONG_TEXT, "Long text should be the input value");
+ is(editor.input.offsetWidth, MAX_WIDTH,
+ "Input width should be the same as MAX_WIDTH");
+ is(getLines(editor.input), 3, "Input should display 3 lines of text");
+ checkScrollbars(editor.input);
+
+ info("Delete all characters on line 3.");
+ while (getLines(editor.input) === 3) {
+ EventUtils.sendKey("BACK_SPACE");
+ checkScrollbars(editor.input);
+ }
+
+ is(editor.input.offsetWidth, MAX_WIDTH,
+ "Input width should be the same as MAX_WIDTH");
+ is(getLines(editor.input), 2, "Input should display 2 lines of text");
+ checkScrollbars(editor.input);
+
+ info("Delete all characters on line 2.");
+ while (getLines(editor.input) === 2) {
+ EventUtils.sendKey("BACK_SPACE");
+ checkScrollbars(editor.input);
+ }
+
+ is(getLines(editor.input), 1, "Input should display 1 line of text");
+ checkScrollbars(editor.input);
+
+ info("Delete all characters.");
+ while (editor.input.value !== "") {
+ EventUtils.sendKey("BACK_SPACE");
+ checkScrollbars(editor.input);
+ }
+
+ ok(editor.input.offsetWidth < MAX_WIDTH,
+ "Input width should again be strictly smaller than MAX_WIDTH");
+ ok(editor.input.offsetWidth > 0,
+ "Even with no content, the input has a non-zero width");
+ is(getLines(editor.input), 1, "Input should display 1 line of text");
+ checkScrollbars(editor.input);
+
+ info("Leave the inplace-editor");
+ EventUtils.sendKey("RETURN");
+});
+
+/**
+ * Retrieve the current number of lines displayed in the provided textarea.
+ *
+ * @param {DOMNode} textarea
+ * @return {Number} the number of lines
+ */
+function getLines(textarea) {
+ let win = textarea.ownerDocument.defaultView;
+ let style = win.getComputedStyle(textarea);
+ let lineHeight = style.getPropertyCSSValue("line-height").cssText;
+ return Math.floor(textarea.clientHeight / parseFloat(lineHeight));
+}
+
+/**
+ * Verify that the provided textarea has no vertical or horizontal scrollbar.
+ *
+ * @param {DOMNode} textarea
+ */
+function checkScrollbars(textarea) {
+ is(textarea.scrollHeight, textarea.clientHeight,
+ "Textarea should never have vertical scrollbars");
+ is(textarea.scrollWidth, textarea.clientWidth,
+ "Textarea should never have horizontal scrollbars");
+}
diff --git a/devtools/client/shared/test/browser_key_shortcuts.js b/devtools/client/shared/test/browser_key_shortcuts.js
new file mode 100644
index 000000000..c88782f85
--- /dev/null
+++ b/devtools/client/shared/test/browser_key_shortcuts.js
@@ -0,0 +1,425 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var isOSX = Services.appinfo.OS === "Darwin";
+
+add_task(function* () {
+ let shortcuts = new KeyShortcuts({
+ window
+ });
+
+ yield testSimple(shortcuts);
+ yield testNonLetterCharacter(shortcuts);
+ yield testPlusCharacter(shortcuts);
+ yield testFunctionKey(shortcuts);
+ yield testMixup(shortcuts);
+ yield testLooseDigits(shortcuts);
+ yield testExactModifiers(shortcuts);
+ yield testLooseShiftModifier(shortcuts);
+ yield testStrictLetterShiftModifier(shortcuts);
+ yield testAltModifier(shortcuts);
+ yield testCommandOrControlModifier(shortcuts);
+ yield testCtrlModifier(shortcuts);
+ yield testInvalidShortcutString(shortcuts);
+ yield testCmdShiftShortcut(shortcuts);
+ shortcuts.destroy();
+
+ yield testTarget();
+});
+
+// Test helper to listen to the next key press for a given key,
+// returning a promise to help using Tasks.
+function once(shortcuts, key, listener) {
+ let called = false;
+ return new Promise(done => {
+ let onShortcut = (key2, event) => {
+ shortcuts.off(key, onShortcut);
+ ok(!called, "once listener called only once (i.e. off() works)");
+ is(key, key2, "listener first argument match the key we listen");
+ called = true;
+ listener(key2, event);
+ done();
+ };
+ shortcuts.on(key, onShortcut);
+ });
+}
+
+function* testSimple(shortcuts) {
+ info("Test simple key shortcuts");
+
+ let onKey = once(shortcuts, "0", (key, event) => {
+ is(event.key, "0");
+
+ // Display another key press to ensure that once() correctly stop listening
+ EventUtils.synthesizeKey("0", {}, window);
+ });
+
+ EventUtils.synthesizeKey("0", {}, window);
+ yield onKey;
+}
+
+function* testNonLetterCharacter(shortcuts) {
+ info("Test non-naive character key shortcuts");
+
+ let onKey = once(shortcuts, "[", (key, event) => {
+ is(event.key, "[");
+ });
+
+ EventUtils.synthesizeKey("[", {}, window);
+ yield onKey;
+}
+
+function* testFunctionKey(shortcuts) {
+ info("Test function key shortcuts");
+
+ let onKey = once(shortcuts, "F12", (key, event) => {
+ is(event.key, "F12");
+ });
+
+ EventUtils.synthesizeKey("F12", { keyCode: 123 }, window);
+ yield onKey;
+}
+
+// Plus is special. It's keycode is the one for "=". That's because it requires
+// shift to be pressed and is behind "=" key. So it should be considered as a
+// character key
+function* testPlusCharacter(shortcuts) {
+ info("Test 'Plus' key shortcuts");
+
+ let onKey = once(shortcuts, "Plus", (key, event) => {
+ is(event.key, "+");
+ });
+
+ EventUtils.synthesizeKey("+", { keyCode: 61, shiftKey: true }, window);
+ yield onKey;
+}
+
+// Test they listeners are not mixed up between shortcuts
+function* testMixup(shortcuts) {
+ info("Test possible listener mixup");
+
+ let hitFirst = false, hitSecond = false;
+ let onFirstKey = once(shortcuts, "0", (key, event) => {
+ is(event.key, "0");
+ hitFirst = true;
+ });
+ let onSecondKey = once(shortcuts, "Alt+A", (key, event) => {
+ is(event.key, "a");
+ ok(event.altKey);
+ hitSecond = true;
+ });
+
+ // Dispatch the first shortcut and expect only this one to be notified
+ ok(!hitFirst, "First shortcut isn't notified before firing the key event");
+ EventUtils.synthesizeKey("0", {}, window);
+ yield onFirstKey;
+ ok(hitFirst, "Got the first shortcut notified");
+ ok(!hitSecond, "No mixup, second shortcut is still not notified (1/2)");
+
+ // Wait an extra time, just to be sure this isn't racy
+ yield new Promise(done => {
+ window.setTimeout(done, 0);
+ });
+ ok(!hitSecond, "No mixup, second shortcut is still not notified (2/2)");
+
+ // Finally dispatch the second shortcut
+ EventUtils.synthesizeKey("a", { altKey: true }, window);
+ yield onSecondKey;
+ ok(hitSecond, "Got the second shortcut notified once it is actually fired");
+}
+
+// On azerty keyboard, digits are only available by pressing Shift/Capslock,
+// but we accept them even if we omit doing that.
+function* testLooseDigits(shortcuts) {
+ info("Test Loose digits");
+ let onKey = once(shortcuts, "0", (key, event) => {
+ is(event.key, "à");
+ ok(!event.altKey);
+ ok(!event.ctrlKey);
+ ok(!event.metaKey);
+ ok(!event.shiftKey);
+ });
+ // Simulate a press on the "0" key, without shift pressed on a french
+ // keyboard
+ EventUtils.synthesizeKey(
+ "à",
+ { keyCode: 48 },
+ window);
+ yield onKey;
+
+ onKey = once(shortcuts, "0", (key, event) => {
+ is(event.key, "0");
+ ok(!event.altKey);
+ ok(!event.ctrlKey);
+ ok(!event.metaKey);
+ ok(event.shiftKey);
+ });
+ // Simulate the same press with shift pressed
+ EventUtils.synthesizeKey(
+ "0",
+ { keyCode: 48, shiftKey: true },
+ window);
+ yield onKey;
+}
+
+// Test that shortcuts is notified only when the modifiers match exactly
+function* testExactModifiers(shortcuts) {
+ info("Test exact modifiers match");
+
+ let hit = false;
+ let onKey = once(shortcuts, "Alt+A", (key, event) => {
+ is(event.key, "a");
+ ok(event.altKey);
+ ok(!event.ctrlKey);
+ ok(!event.metaKey);
+ ok(!event.shiftKey);
+ hit = true;
+ });
+
+ // Dispatch with unexpected set of modifiers
+ ok(!hit, "Shortcut isn't notified before firing the key event");
+ EventUtils.synthesizeKey("a",
+ { accelKey: true, altKey: true, shiftKey: true },
+ window);
+ EventUtils.synthesizeKey(
+ "a",
+ { accelKey: true, altKey: false, shiftKey: false },
+ window);
+ EventUtils.synthesizeKey(
+ "a",
+ { accelKey: false, altKey: false, shiftKey: true },
+ window);
+ EventUtils.synthesizeKey(
+ "a",
+ { accelKey: false, altKey: false, shiftKey: false },
+ window);
+
+ // Wait an extra time to let a chance to call the listener
+ yield new Promise(done => {
+ window.setTimeout(done, 0);
+ });
+ ok(!hit, "Listener isn't called when modifiers aren't exactly matching");
+
+ // Dispatch the expected modifiers
+ EventUtils.synthesizeKey("a", { accelKey: false, altKey: true, shiftKey: false},
+ window);
+ yield onKey;
+ ok(hit, "Got shortcut notified once it is actually fired");
+}
+
+// Some keys are only accessible via shift and listener should also be called
+// even if the key didn't explicitely requested Shift modifier.
+// For example, `%` on french keyboards is only accessible via Shift.
+// Same thing for `@` on US keybords.
+function* testLooseShiftModifier(shortcuts) {
+ info("Test Loose shift modifier");
+ let onKey = once(shortcuts, "%", (key, event) => {
+ is(event.key, "%");
+ ok(!event.altKey);
+ ok(!event.ctrlKey);
+ ok(!event.metaKey);
+ ok(event.shiftKey);
+ });
+ EventUtils.synthesizeKey(
+ "%",
+ { accelKey: false, altKey: false, ctrlKey: false, shiftKey: true},
+ window);
+ yield onKey;
+
+ onKey = once(shortcuts, "@", (key, event) => {
+ is(event.key, "@");
+ ok(!event.altKey);
+ ok(!event.ctrlKey);
+ ok(!event.metaKey);
+ ok(event.shiftKey);
+ });
+ EventUtils.synthesizeKey(
+ "@",
+ { accelKey: false, altKey: false, ctrlKey: false, shiftKey: true},
+ window);
+ yield onKey;
+}
+
+// But Shift modifier is strict on all letter characters (a to Z)
+function* testStrictLetterShiftModifier(shortcuts) {
+ info("Test strict shift modifier on letters");
+ let hitFirst = false;
+ let onKey = once(shortcuts, "a", (key, event) => {
+ is(event.key, "a");
+ ok(!event.altKey);
+ ok(!event.ctrlKey);
+ ok(!event.metaKey);
+ ok(!event.shiftKey);
+ hitFirst = true;
+ });
+ let onShiftKey = once(shortcuts, "Shift+a", (key, event) => {
+ is(event.key, "a");
+ ok(!event.altKey);
+ ok(!event.ctrlKey);
+ ok(!event.metaKey);
+ ok(event.shiftKey);
+ });
+ EventUtils.synthesizeKey(
+ "a",
+ { shiftKey: true},
+ window);
+ yield onShiftKey;
+ ok(!hitFirst, "Didn't fire the explicit shift+a");
+
+ EventUtils.synthesizeKey(
+ "a",
+ { shiftKey: false},
+ window);
+ yield onKey;
+}
+
+function* testAltModifier(shortcuts) {
+ info("Test Alt modifier");
+ let onKey = once(shortcuts, "Alt+F1", (key, event) => {
+ is(event.keyCode, window.KeyboardEvent.DOM_VK_F1);
+ ok(event.altKey);
+ ok(!event.ctrlKey);
+ ok(!event.metaKey);
+ ok(!event.shiftKey);
+ });
+ EventUtils.synthesizeKey(
+ "VK_F1",
+ { altKey: true },
+ window);
+ yield onKey;
+}
+
+function* testCommandOrControlModifier(shortcuts) {
+ info("Test CommandOrControl modifier");
+ let onKey = once(shortcuts, "CommandOrControl+F1", (key, event) => {
+ is(event.keyCode, window.KeyboardEvent.DOM_VK_F1);
+ ok(!event.altKey);
+ if (isOSX) {
+ ok(!event.ctrlKey);
+ ok(event.metaKey);
+ } else {
+ ok(event.ctrlKey);
+ ok(!event.metaKey);
+ }
+ ok(!event.shiftKey);
+ });
+ let onKeyAlias = once(shortcuts, "CmdOrCtrl+F1", (key, event) => {
+ is(event.keyCode, window.KeyboardEvent.DOM_VK_F1);
+ ok(!event.altKey);
+ if (isOSX) {
+ ok(!event.ctrlKey);
+ ok(event.metaKey);
+ } else {
+ ok(event.ctrlKey);
+ ok(!event.metaKey);
+ }
+ ok(!event.shiftKey);
+ });
+ if (isOSX) {
+ EventUtils.synthesizeKey(
+ "VK_F1",
+ { metaKey: true },
+ window);
+ } else {
+ EventUtils.synthesizeKey(
+ "VK_F1",
+ { ctrlKey: true },
+ window);
+ }
+ yield onKey;
+ yield onKeyAlias;
+}
+
+function* testCtrlModifier(shortcuts) {
+ info("Test Ctrl modifier");
+ let onKey = once(shortcuts, "Ctrl+F1", (key, event) => {
+ is(event.keyCode, window.KeyboardEvent.DOM_VK_F1);
+ ok(!event.altKey);
+ ok(event.ctrlKey);
+ ok(!event.metaKey);
+ ok(!event.shiftKey);
+ });
+ let onKeyAlias = once(shortcuts, "Control+F1", (key, event) => {
+ is(event.keyCode, window.KeyboardEvent.DOM_VK_F1);
+ ok(!event.altKey);
+ ok(event.ctrlKey);
+ ok(!event.metaKey);
+ ok(!event.shiftKey);
+ });
+ EventUtils.synthesizeKey(
+ "VK_F1",
+ { ctrlKey: true },
+ window);
+ yield onKey;
+ yield onKeyAlias;
+}
+
+function* testCmdShiftShortcut(shortcuts) {
+ if (!isOSX) {
+ // This test is OSX only (Bug 1300458).
+ return;
+ }
+
+ let onCmdKey = once(shortcuts, "CmdOrCtrl+[", (key, event) => {
+ is(event.key, "[");
+ ok(!event.altKey);
+ ok(!event.ctrlKey);
+ ok(event.metaKey);
+ ok(!event.shiftKey);
+ });
+ let onCmdShiftKey = once(shortcuts, "CmdOrCtrl+Shift+[", (key, event) => {
+ is(event.key, "[");
+ ok(!event.altKey);
+ ok(!event.ctrlKey);
+ ok(event.metaKey);
+ ok(event.shiftKey);
+ });
+
+ EventUtils.synthesizeKey(
+ "[",
+ { metaKey: true, shiftKey: true },
+ window);
+ EventUtils.synthesizeKey(
+ "[",
+ { metaKey: true },
+ window);
+
+ yield onCmdKey;
+ yield onCmdShiftKey;
+}
+
+function* testTarget() {
+ info("Test KeyShortcuts with target argument");
+
+ let target = document.createElementNS("http://www.w3.org/1999/xhtml",
+ "input");
+ document.documentElement.appendChild(target);
+ target.focus();
+
+ let shortcuts = new KeyShortcuts({
+ window,
+ target
+ });
+ let onKey = once(shortcuts, "0", (key, event) => {
+ is(event.key, "0");
+ is(event.target, target);
+ });
+ EventUtils.synthesizeKey("0", {}, window);
+ yield onKey;
+
+ target.remove();
+
+ shortcuts.destroy();
+}
+
+function testInvalidShortcutString(shortcuts) {
+ info("Test wrong shortcut string");
+
+ let shortcut = KeyShortcuts.parseElectronKey(window, "Cmmd+F");
+ ok(!shortcut, "Passing a invalid shortcut string should return a null object");
+
+ shortcuts.on("Cmmd+F", function () {});
+ ok(true, "on() shouldn't throw when passing invalid shortcut string");
+}
diff --git a/devtools/client/shared/test/browser_keycodes.js b/devtools/client/shared/test/browser_keycodes.js
new file mode 100644
index 000000000..9e6b4a4ee
--- /dev/null
+++ b/devtools/client/shared/test/browser_keycodes.js
@@ -0,0 +1,12 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {KeyCodes} = require("devtools/client/shared/keycodes");
+
+add_task(function* () {
+ for (let key in KeyCodes) {
+ is(KeyCodes[key], Ci.nsIDOMKeyEvent[key], "checking value for " + key);
+ }
+});
diff --git a/devtools/client/shared/test/browser_layoutHelpers-getBoxQuads.html b/devtools/client/shared/test/browser_layoutHelpers-getBoxQuads.html
new file mode 100644
index 000000000..070792b9a
--- /dev/null
+++ b/devtools/client/shared/test/browser_layoutHelpers-getBoxQuads.html
@@ -0,0 +1,65 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Layout Helpers</title>
+<style id="styles">
+ body {
+ margin: 0;
+ padding: 0;
+ }
+
+ #hidden-node {
+ display: none;
+ }
+
+ #simple-node-with-margin-padding-border {
+ width: 200px;
+ height: 200px;
+ background: #f06;
+
+ padding: 20px;
+ margin: 50px;
+ border: 10px solid black;
+ }
+
+ #scrolled-node {
+ position: absolute;
+ top: 0;
+ left: 0;
+
+ width: 300px;
+ height: 100px;
+ overflow: scroll;
+ background: linear-gradient(red, pink);
+ }
+
+ #sub-scrolled-node {
+ width: 200px;
+ height: 200px;
+ overflow: scroll;
+ background: linear-gradient(yellow, green);
+ }
+
+ #inner-scrolled-node {
+ width: 100px;
+ height: 400px;
+ background: linear-gradient(black, white);
+ }
+</style>
+<div id="hidden-node"></div>
+<div id="simple-node-with-margin-padding-border"></div>
+<!-- The inline encoded code below corresponds to:
+<iframe style="margin:10px;border:0;width:300px;height:300px;">
+ <iframe style="margin:10px;border:0;width:200px;height:200px;">
+ <div id="inner-node" style="width:100px;height:100px;border:10px solid red;margin:10px;padding:10px;"></div>
+ </iframe>
+</iframe>
+ -->
+<iframe src="data:text/html,%3Cstyle%3Ebody%7Bmargin:0;padding:0;%7D%3C/style%3E%3Ciframe%20src=%22data:text/html,%253Cstyle%253Ebody%257Bmargin:0;padding:0;%257D%253C/style%253E%253Cdiv%2520id='inner-node'%2520style='width:100px;height:100px;border:10px%2520solid%2520red;margin:10px;padding:10px;'%253E%253C/div%253E%22%20style=%22margin:10px;border:0;width:200px;height:200px;%22%3E%3C/iframe%3E" style="margin:10px;border:0;width:300px;height:300px;"></iframe>
+<div id="scrolled-node">
+ <div id="sub-scrolled-node">
+ <div id="inner-scrolled-node"></div>
+ </div>
+</div>
+<span id="inline">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus porttitor luctus sem id scelerisque. Cras quis velit sed risus euismod lacinia. Donec viverra enim eu ligula efficitur, quis vulputate metus cursus. Duis sed interdum risus. Ut blandit velit vitae faucibus efficitur. Lorem ipsum dolor sit amet, consectetur adipiscing elit.<br/ >
+Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Sed vitae dolor metus. Aliquam sed velit sit amet libero vestibulum aliquam vel a lorem. Integer eget ex eget justo auctor ullamcorper.<br/ >
+Praesent tristique maximus lacus, nec ultricies neque ultrices non. Phasellus vel lobortis justo. </span> \ No newline at end of file
diff --git a/devtools/client/shared/test/browser_layoutHelpers-getBoxQuads.js b/devtools/client/shared/test/browser_layoutHelpers-getBoxQuads.js
new file mode 100644
index 000000000..221127a11
--- /dev/null
+++ b/devtools/client/shared/test/browser_layoutHelpers-getBoxQuads.js
@@ -0,0 +1,219 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests getAdjustedQuads works properly in a variety of use cases including
+// iframes, scroll and zoom
+
+"use strict";
+
+const {getAdjustedQuads} = require("devtools/shared/layout/utils");
+
+const TEST_URI = TEST_URI_ROOT + "browser_layoutHelpers-getBoxQuads.html";
+
+add_task(function* () {
+ let tab = yield addTab(TEST_URI);
+ let doc = tab.linkedBrowser.contentDocument;
+
+ ok(typeof getAdjustedQuads === "function", "getAdjustedQuads is defined");
+
+ info("Running tests");
+
+ returnsTheRightDataStructure(doc);
+ isEmptyForMissingNode(doc);
+ isEmptyForHiddenNodes(doc);
+ defaultsToBorderBoxIfNoneProvided(doc);
+ returnsLikeGetBoxQuadsInSimpleCase(doc);
+ takesIframesOffsetsIntoAccount(doc);
+ takesScrollingIntoAccount(doc);
+ yield takesZoomIntoAccount(doc);
+ returnsMultipleItemsForWrappingInlineElements(doc);
+
+ gBrowser.removeCurrentTab();
+});
+
+function returnsTheRightDataStructure(doc) {
+ info("Checks that the returned data contains bounds and 4 points");
+
+ let node = doc.querySelector("body");
+ let [res] = getAdjustedQuads(doc.defaultView, node, "content");
+
+ ok("bounds" in res, "The returned data has a bounds property");
+ ok("p1" in res, "The returned data has a p1 property");
+ ok("p2" in res, "The returned data has a p2 property");
+ ok("p3" in res, "The returned data has a p3 property");
+ ok("p4" in res, "The returned data has a p4 property");
+
+ for (let boundProp of
+ ["bottom", "top", "right", "left", "width", "height", "x", "y"]) {
+ ok(boundProp in res.bounds, "The bounds has a " + boundProp + " property");
+ }
+
+ for (let point of ["p1", "p2", "p3", "p4"]) {
+ for (let pointProp of ["x", "y", "z", "w"]) {
+ ok(pointProp in res[point], point + " has a " + pointProp + " property");
+ }
+ }
+}
+
+function isEmptyForMissingNode(doc) {
+ info("Checks that null is returned for invalid nodes");
+
+ for (let input of [null, undefined, "", 0]) {
+ is(getAdjustedQuads(doc.defaultView, input).length, 0,
+ "A 0-length array is returned for input " + input);
+ }
+}
+
+function isEmptyForHiddenNodes(doc) {
+ info("Checks that null is returned for nodes that aren't rendered");
+
+ let style = doc.querySelector("#styles");
+ is(getAdjustedQuads(doc.defaultView, style).length, 0,
+ "null is returned for a <style> node");
+
+ let hidden = doc.querySelector("#hidden-node");
+ is(getAdjustedQuads(doc.defaultView, hidden).length, 0,
+ "null is returned for a hidden node");
+}
+
+function defaultsToBorderBoxIfNoneProvided(doc) {
+ info("Checks that if no boxtype is passed, then border is the default one");
+
+ let node = doc.querySelector("#simple-node-with-margin-padding-border");
+ let [withBoxType] = getAdjustedQuads(doc.defaultView, node, "border");
+ let [withoutBoxType] = getAdjustedQuads(doc.defaultView, node);
+
+ for (let boundProp of
+ ["bottom", "top", "right", "left", "width", "height", "x", "y"]) {
+ is(withBoxType.bounds[boundProp], withoutBoxType.bounds[boundProp],
+ boundProp + " bound is equal with or without the border box type");
+ }
+
+ for (let point of ["p1", "p2", "p3", "p4"]) {
+ for (let pointProp of ["x", "y", "z", "w"]) {
+ is(withBoxType[point][pointProp], withoutBoxType[point][pointProp],
+ point + "." + pointProp +
+ " is equal with or without the border box type");
+ }
+ }
+}
+
+function returnsLikeGetBoxQuadsInSimpleCase(doc) {
+ info("Checks that for an element in the main frame, without scroll nor zoom" +
+ "that the returned value is similar to the returned value of getBoxQuads");
+
+ let node = doc.querySelector("#simple-node-with-margin-padding-border");
+
+ for (let region of ["content", "padding", "border", "margin"]) {
+ let expected = node.getBoxQuads({
+ box: region
+ })[0];
+ let [actual] = getAdjustedQuads(doc.defaultView, node, region);
+
+ for (let boundProp of
+ ["bottom", "top", "right", "left", "width", "height", "x", "y"]) {
+ is(actual.bounds[boundProp], expected.bounds[boundProp],
+ boundProp + " bound is equal to the one returned by getBoxQuads for " +
+ region + " box");
+ }
+
+ for (let point of ["p1", "p2", "p3", "p4"]) {
+ for (let pointProp of ["x", "y", "z", "w"]) {
+ is(actual[point][pointProp], expected[point][pointProp],
+ point + "." + pointProp +
+ " is equal to the one returned by getBoxQuads for " + region + " box");
+ }
+ }
+ }
+}
+
+function takesIframesOffsetsIntoAccount(doc) {
+ info("Checks that the quad returned for a node inside iframes that have " +
+ "margins takes those offsets into account");
+
+ let rootIframe = doc.querySelector("iframe");
+ let subIframe = rootIframe.contentDocument.querySelector("iframe");
+ let innerNode = subIframe.contentDocument.querySelector("#inner-node");
+
+ let [quad] = getAdjustedQuads(doc.defaultView, innerNode, "content");
+
+ // rootIframe margin + subIframe margin + node margin + node border + node padding
+ let p1x = 10 + 10 + 10 + 10 + 10;
+ is(quad.p1.x, p1x, "The inner node's p1 x position is correct");
+
+ // Same as p1x + the inner node width
+ let p2x = p1x + 100;
+ is(quad.p2.x, p2x, "The inner node's p2 x position is correct");
+}
+
+function takesScrollingIntoAccount(doc) {
+ info("Checks that the quad returned for a node inside multiple scrolled " +
+ "containers takes the scroll values into account");
+
+ // For info, the container being tested here is absolutely positioned at 0 0
+ // to simplify asserting the coordinates
+
+ info("Scroll the container nodes down");
+ let scrolledNode = doc.querySelector("#scrolled-node");
+ scrolledNode.scrollTop = 100;
+ let subScrolledNode = doc.querySelector("#sub-scrolled-node");
+ subScrolledNode.scrollTop = 200;
+ let innerNode = doc.querySelector("#inner-scrolled-node");
+
+ let [quad] = getAdjustedQuads(doc.defaultView, innerNode, "content");
+ is(quad.p1.x, 0, "p1.x of the scrolled node is correct after scrolling down");
+ is(quad.p1.y, -300, "p1.y of the scrolled node is correct after scrolling down");
+
+ info("Scrolling back up");
+ scrolledNode.scrollTop = 0;
+ subScrolledNode.scrollTop = 0;
+
+ [quad] = getAdjustedQuads(doc.defaultView, innerNode, "content");
+ is(quad.p1.x, 0, "p1.x of the scrolled node is correct after scrolling up");
+ is(quad.p1.y, 0, "p1.y of the scrolled node is correct after scrolling up");
+}
+
+function* takesZoomIntoAccount(doc) {
+ info("Checks that if the page is zoomed in/out, the quad returned is correct");
+
+ // Hard-coding coordinates in this zoom test is a bad idea as it can vary
+ // depending on the platform, so we simply test that zooming in produces a
+ // bigger quad and zooming out produces a smaller quad
+
+ let node = doc.querySelector("#simple-node-with-margin-padding-border");
+ let [defaultQuad] = getAdjustedQuads(doc.defaultView, node);
+
+ info("Zoom in");
+ window.FullZoom.enlarge();
+ let [zoomedInQuad] = getAdjustedQuads(doc.defaultView, node);
+
+ ok(zoomedInQuad.bounds.width > defaultQuad.bounds.width,
+ "The zoomed in quad is bigger than the default one");
+ ok(zoomedInQuad.bounds.height > defaultQuad.bounds.height,
+ "The zoomed in quad is bigger than the default one");
+
+ info("Zoom out");
+ yield window.FullZoom.reset();
+ window.FullZoom.reduce();
+ let [zoomedOutQuad] = getAdjustedQuads(doc.defaultView, node);
+
+ ok(zoomedOutQuad.bounds.width < defaultQuad.bounds.width,
+ "The zoomed out quad is smaller than the default one");
+ ok(zoomedOutQuad.bounds.height < defaultQuad.bounds.height,
+ "The zoomed out quad is smaller than the default one");
+
+ yield window.FullZoom.reset();
+}
+
+function returnsMultipleItemsForWrappingInlineElements(doc) {
+ info("Checks that several quads are returned " +
+ "for inline elements that span line-breaks");
+
+ let node = doc.querySelector("#inline");
+ let quads = getAdjustedQuads(doc.defaultView, node, "content");
+ // At least 3 because of the 2 <br />, maybe more depending on the window size.
+ ok(quads.length >= 3, "Multiple quads were returned");
+
+ is(quads.length, node.getBoxQuads().length,
+ "The same number of boxes as getBoxQuads was returned");
+}
diff --git a/devtools/client/shared/test/browser_layoutHelpers.html b/devtools/client/shared/test/browser_layoutHelpers.html
new file mode 100644
index 000000000..f50bfffdf
--- /dev/null
+++ b/devtools/client/shared/test/browser_layoutHelpers.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<meta charset=utf-8>
+<title> Layout Helpers </title>
+
+<style>
+ html {
+ height: 300%;
+ width: 300%;
+ }
+ div#some {
+ position: absolute;
+ background: black;
+ width: 2px;
+ height: 2px;
+ }
+ iframe {
+ position: absolute;
+ width: 40px;
+ height: 40px;
+ border: 0;
+ }
+</style>
+
+<div id=some></div>
diff --git a/devtools/client/shared/test/browser_layoutHelpers.js b/devtools/client/shared/test/browser_layoutHelpers.js
new file mode 100644
index 000000000..3274ad686
--- /dev/null
+++ b/devtools/client/shared/test/browser_layoutHelpers.js
@@ -0,0 +1,93 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that scrollIntoViewIfNeeded works properly.
+const {scrollIntoViewIfNeeded} = require("devtools/client/shared/scroll");
+
+const TEST_URI = TEST_URI_ROOT + "browser_layoutHelpers.html";
+
+add_task(function* () {
+ let [host, win] = yield createHost("bottom", TEST_URI);
+ runTest(win);
+ host.destroy();
+});
+
+function runTest(win) {
+ let some = win.document.getElementById("some");
+
+ some.style.top = win.innerHeight + "px";
+ some.style.left = win.innerWidth + "px";
+ // The tests start with a black 2x2 pixels square below bottom right.
+ // Do not resize the window during the tests.
+
+ let xPos = Math.floor(win.innerWidth / 2);
+ // Above the viewport.
+ win.scroll(xPos, win.innerHeight + 2);
+ scrollIntoViewIfNeeded(some);
+ is(win.scrollY, Math.floor(win.innerHeight / 2) + 1,
+ "Element completely hidden above should appear centered.");
+ is(win.scrollX, xPos,
+ "scrollX position has not changed.");
+
+ // On the top edge.
+ win.scroll(win.innerWidth / 2, win.innerHeight + 1);
+ scrollIntoViewIfNeeded(some);
+ is(win.scrollY, win.innerHeight,
+ "Element partially visible above should appear above.");
+ is(win.scrollX, xPos,
+ "scrollX position has not changed.");
+
+ // Just below the viewport.
+ win.scroll(win.innerWidth / 2, 0);
+ scrollIntoViewIfNeeded(some);
+ is(win.scrollY, Math.floor(win.innerHeight / 2) + 1,
+ "Element completely hidden below should appear centered.");
+ is(win.scrollX, xPos,
+ "scrollX position has not changed.");
+
+ // On the bottom edge.
+ win.scroll(win.innerWidth / 2, 1);
+ scrollIntoViewIfNeeded(some);
+ is(win.scrollY, 2,
+ "Element partially visible below should appear below.");
+ is(win.scrollX, xPos,
+ "scrollX position has not changed.");
+
+ // Above the viewport.
+ win.scroll(win.innerWidth / 2, win.innerHeight + 2);
+ scrollIntoViewIfNeeded(some, false);
+ is(win.scrollY, win.innerHeight,
+ "Element completely hidden above should appear above " +
+ "if parameter is false.");
+ is(win.scrollX, xPos,
+ "scrollX position has not changed.");
+
+ // On the top edge.
+ win.scroll(win.innerWidth / 2, win.innerHeight + 1);
+ scrollIntoViewIfNeeded(some, false);
+ is(win.scrollY, win.innerHeight,
+ "Element partially visible above should appear above " +
+ "if parameter is false.");
+ is(win.scrollX, xPos,
+ "scrollX position has not changed.");
+
+ // Below the viewport.
+ win.scroll(win.innerWidth / 2, 0);
+ scrollIntoViewIfNeeded(some, false);
+ is(win.scrollY, 2,
+ "Element completely hidden below should appear below " +
+ "if parameter is false.");
+ is(win.scrollX, xPos,
+ "scrollX position has not changed.");
+
+ // On the bottom edge.
+ win.scroll(win.innerWidth / 2, 1);
+ scrollIntoViewIfNeeded(some, false);
+ is(win.scrollY, 2,
+ "Element partially visible below should appear below " +
+ "if parameter is false.");
+ is(win.scrollX, xPos,
+ "scrollX position has not changed.");
+}
diff --git a/devtools/client/shared/test/browser_mdn-docs-01.js b/devtools/client/shared/test/browser_mdn-docs-01.js
new file mode 100644
index 000000000..6490dfef7
--- /dev/null
+++ b/devtools/client/shared/test/browser_mdn-docs-01.js
@@ -0,0 +1,168 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This file tests the MdnDocsWidget object, and specifically its
+ * loadCssDocs() function.
+ *
+ * The MdnDocsWidget is initialized with a document which has a specific
+ * structure. You then call loadCssDocs(), passing in a CSS property name.
+ * MdnDocsWidget then fetches docs for that property by making an XHR to
+ * a docs page, and loads the results into the document. While the XHR is
+ * still not resolved the document is put into an "initializing" state in
+ * which the devtools throbber is displayed.
+ *
+ * In this file we test:
+ * - the initial state of the document before the docs have loaded
+ * - the state of the document after the docs have loaded
+ */
+
+"use strict";
+
+const {setBaseCssDocsUrl, MdnDocsWidget} = require("devtools/client/shared/widgets/MdnDocsWidget");
+
+/**
+ * Test properties
+ *
+ * In the real tooltip, a CSS property name is used to look up an MDN page
+ * for that property.
+ * In the test code, the names defined here is used to look up a page
+ * served by the test server.
+ */
+const BASIC_TESTING_PROPERTY = "html-mdn-css-basic-testing.html";
+
+const BASIC_EXPECTED_SUMMARY = "A summary of the property.";
+const BASIC_EXPECTED_SYNTAX = [{type: "comment", text: "/* The part we want */"},
+ {type: "text", text: "\n"},
+ {type: "property-name", text: "this"},
+ {type: "text", text: ":"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "is-the-part-we-want"},
+ {type: "text", text: ";"}];
+
+const URI_PARAMS =
+ "?utm_source=mozilla&utm_medium=firefox-inspector&utm_campaign=default";
+
+add_task(function* () {
+ setBaseCssDocsUrl(TEST_URI_ROOT);
+
+ yield addTab("about:blank");
+ let [host, win] = yield createHost("bottom", "data:text/html," +
+ "<div class='mdn-container'></div>");
+ let widget = new MdnDocsWidget(win.document.querySelector("div"));
+
+ yield testTheBasics(widget);
+
+ host.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+/**
+ * Test all the basics
+ * - initial content, before docs have loaded, is as expected
+ * - throbber is set before docs have loaded
+ * - contents are as expected after docs have loaded
+ * - throbber is gone after docs have loaded
+ * - mdn link text is correct and onclick behavior is correct
+ */
+function* testTheBasics(widget) {
+ info("Test all the basic functionality in the widget");
+
+ info("Get the widget state before docs have loaded");
+ let promise = widget.loadCssDocs(BASIC_TESTING_PROPERTY);
+
+ info("Check initial contents before docs have loaded");
+ checkTooltipContents(widget.elements, {
+ propertyName: BASIC_TESTING_PROPERTY,
+ summary: "",
+ syntax: ""
+ });
+
+ // throbber is set
+ ok(widget.elements.info.classList.contains("devtools-throbber"),
+ "Throbber is set");
+
+ info("Now let the widget finish loading");
+ yield promise;
+
+ info("Check contents after docs have loaded");
+ checkTooltipContents(widget.elements, {
+ propertyName: BASIC_TESTING_PROPERTY,
+ summary: BASIC_EXPECTED_SUMMARY,
+ syntax: BASIC_EXPECTED_SYNTAX
+ });
+
+ // throbber is gone
+ ok(!widget.elements.info.classList.contains("devtools-throbber"),
+ "Throbber is not set");
+
+ info("Check that MDN link text is correct and onclick behavior is correct");
+
+ let mdnLink = widget.elements.linkToMdn;
+ let expectedHref = TEST_URI_ROOT + BASIC_TESTING_PROPERTY + URI_PARAMS;
+ is(mdnLink.href, expectedHref, "MDN link href is correct");
+
+ let uri = yield checkLinkClick(mdnLink);
+ is(uri, expectedHref, "New tab opened with the expected URI");
+}
+
+ /**
+ * Clicking the "Visit MDN Page" in the tooltip panel
+ * should open a new browser tab with the page loaded.
+ *
+ * To test this we'll listen for a new tab opening, and
+ * when it does, add a listener to that new tab to tell
+ * us when it has loaded.
+ *
+ * Then we click the link.
+ *
+ * In the tab's load listener, we'll resolve the promise
+ * with the URI, which is expected to match the href
+ * in the orginal link.
+ *
+ * One complexity is that when you open a new tab,
+ * "about:blank" is first loaded into the tab before the
+ * actual page. So we ignore that first load event, and keep
+ * listening until "load" is triggered for a different URI.
+ */
+function checkLinkClick(link) {
+ function loadListener(tab) {
+ let browser = getBrowser().getBrowserForTab(tab);
+ let uri = browser.currentURI.spec;
+
+ info("New browser tab has loaded");
+ gBrowser.removeTab(tab);
+ info("Resolve promise with new tab URI");
+ deferred.resolve(uri);
+ }
+
+ function newTabListener(e) {
+ gBrowser.tabContainer.removeEventListener("TabOpen", newTabListener);
+ let tab = e.target;
+ BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, url => url != "about:blank")
+ .then(url => loadListener(tab));
+ }
+
+ let deferred = defer();
+ info("Check that clicking the link opens a new tab with the correct URI");
+ gBrowser.tabContainer.addEventListener("TabOpen", newTabListener, false);
+ info("Click the link to MDN");
+ link.click();
+ return deferred.promise;
+}
+
+/**
+ * Utility function to check content of the tooltip.
+ */
+function checkTooltipContents(doc, expected) {
+ is(doc.heading.textContent,
+ expected.propertyName,
+ "Property name is correct");
+
+ is(doc.summary.textContent,
+ expected.summary,
+ "Summary is correct");
+
+ checkCssSyntaxHighlighterOutput(expected.syntax, doc.syntax);
+}
diff --git a/devtools/client/shared/test/browser_mdn-docs-02.js b/devtools/client/shared/test/browser_mdn-docs-02.js
new file mode 100644
index 000000000..000dc7261
--- /dev/null
+++ b/devtools/client/shared/test/browser_mdn-docs-02.js
@@ -0,0 +1,128 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This file tests the MdnDocsWidget object, and specifically its
+ * loadCssDocs() function.
+ *
+ * The MdnDocsWidget is initialized with a document which has a specific
+ * structure. You then call loadCssDocs(), passing in a CSS property name.
+ * MdnDocsWidget then fetches docs for that property by making an XHR to
+ * a docs page, and loads the results into the document.
+ *
+ * In this file we test that the tooltip can properly handle the different
+ * structures that the docs page might have, including variant structures and
+ * error conditions like parts of the document being missing.
+ *
+ * We also test that the tooltip properly handles the case where the page
+ * doesn't exist at all.
+ */
+
+"use strict";
+
+const {
+ setBaseCssDocsUrl,
+ MdnDocsWidget
+} = require("devtools/client/shared/widgets/MdnDocsWidget");
+
+const BASIC_EXPECTED_SUMMARY = "A summary of the property.";
+const BASIC_EXPECTED_SYNTAX = [{type: "comment", text: "/* The part we want */"},
+ {type: "text", text: "\n"},
+ {type: "property-name", text: "this"},
+ {type: "text", text: ":"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "is-the-part-we-want"},
+ {type: "text", text: ";"}];
+
+const ERROR_MESSAGE = "Could not load docs page.";
+
+/**
+ * Test properties
+ *
+ * In the real tooltip, a CSS property name is used to look up an MDN page
+ * for that property.
+ * In the test code, the names defined here are used to look up a page
+ * served by the test server. We have different properties to test
+ * different ways that the docs pages might be constructed, including errors
+ * like pages that don't include docs where we expect.
+ */
+const SYNTAX_OLD_STYLE = "html-mdn-css-syntax-old-style.html";
+const NO_SUMMARY = "html-mdn-css-no-summary.html";
+const NO_SYNTAX = "html-mdn-css-no-syntax.html";
+const NO_SUMMARY_OR_SYNTAX = "html-mdn-css-no-summary-or-syntax.html";
+
+const TEST_DATA = [{
+ desc: "Test a property for which we don't have a page",
+ docsPageUrl: "i-dont-exist.html",
+ expectedContents: {
+ propertyName: "i-dont-exist.html",
+ summary: ERROR_MESSAGE,
+ syntax: []
+ }
+}, {
+ desc: "Test a property whose syntax section is specified using an old-style page",
+ docsPageUrl: SYNTAX_OLD_STYLE,
+ expectedContents: {
+ propertyName: SYNTAX_OLD_STYLE,
+ summary: BASIC_EXPECTED_SUMMARY,
+ syntax: BASIC_EXPECTED_SYNTAX
+ }
+}, {
+ desc: "Test a property whose page doesn't have a summary",
+ docsPageUrl: NO_SUMMARY,
+ expectedContents: {
+ propertyName: NO_SUMMARY,
+ summary: "",
+ syntax: BASIC_EXPECTED_SYNTAX
+ }
+}, {
+ desc: "Test a property whose page doesn't have a syntax",
+ docsPageUrl: NO_SYNTAX,
+ expectedContents: {
+ propertyName: NO_SYNTAX,
+ summary: BASIC_EXPECTED_SUMMARY,
+ syntax: []
+ }
+}, {
+ desc: "Test a property whose page doesn't have a summary or a syntax",
+ docsPageUrl: NO_SUMMARY_OR_SYNTAX,
+ expectedContents: {
+ propertyName: NO_SUMMARY_OR_SYNTAX,
+ summary: ERROR_MESSAGE,
+ syntax: []
+ }
+}
+];
+
+add_task(function* () {
+ setBaseCssDocsUrl(TEST_URI_ROOT);
+
+ yield addTab("about:blank");
+ let [host, win] = yield createHost("bottom", "data:text/html," +
+ "<div class='mdn-container'></div>");
+ let widget = new MdnDocsWidget(win.document.querySelector("div"));
+
+ for (let {desc, docsPageUrl, expectedContents} of TEST_DATA) {
+ info(desc);
+ yield widget.loadCssDocs(docsPageUrl);
+ checkTooltipContents(widget.elements, expectedContents);
+ }
+ host.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+/*
+ * Utility function to check content of the tooltip.
+ */
+function checkTooltipContents(doc, expected) {
+ is(doc.heading.textContent,
+ expected.propertyName,
+ "Property name is correct");
+
+ is(doc.summary.textContent,
+ expected.summary,
+ "Summary is correct");
+
+ checkCssSyntaxHighlighterOutput(expected.syntax, doc.syntax);
+}
diff --git a/devtools/client/shared/test/browser_mdn-docs-03.js b/devtools/client/shared/test/browser_mdn-docs-03.js
new file mode 100644
index 000000000..c686aa6a9
--- /dev/null
+++ b/devtools/client/shared/test/browser_mdn-docs-03.js
@@ -0,0 +1,277 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This file tests the CSS syntax highlighter in the MdnDocsWidget object.
+ *
+ * The CSS syntax highlighter accepts:
+ * - a string containing CSS
+ * - a DOM node
+ *
+ * It parses the string and creates a collection of DOM nodes for different
+ * CSS token types. These DOM nodes have CSS classes applied to them,
+ * to apply the right style for that particular token type. The DOM nodes
+ * are returned as children of the node that was passed to the function.
+ *
+ * This test code defines a number of different strings containing valid and
+ * invalid CSS in various forms. For each string it defines the DOM nodes
+ * that it expects to get from the syntax highlighter.
+ *
+ * It then calls the syntax highlighter, and checks that the resulting
+ * collection of DOM nodes is what we expected.
+ */
+
+"use strict";
+
+const {appendSyntaxHighlightedCSS} = require("devtools/client/shared/widgets/MdnDocsWidget");
+
+/**
+ * An array containing the actual test cases.
+ *
+ * The test code tests every case in the array. If you want to add more
+ * test cases, just add more items to the array.
+ *
+ * Each test case consists of:
+ * - description: string describing the salient features of this test case
+ * - example: the string to test
+ * - expected: an array of objects, one for each DOM node we expect, that
+ * captures the information about the node that we expect to test.
+ */
+const TEST_DATA = [{
+ description: "Valid syntax, string value.",
+ example: "name: stringValue;",
+ expected: [{type: "property-name", text: "name"},
+ {type: "text", text: ":"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "stringValue"},
+ {type: "text", text: ";"}
+ ]}, {
+ description: "Valid syntax, numeric value.",
+ example: "name: 1;",
+ expected: [{type: "property-name", text: "name"},
+ {type: "text", text: ":"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "1"},
+ {type: "text", text: ";"}
+ ]}, {
+ description: "Valid syntax, url value.",
+ example: "name: url(./name);",
+ expected: [{type: "property-name", text: "name"},
+ {type: "text", text: ":"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "url(./name)"},
+ {type: "text", text: ";"}
+ ]}, {
+ description: "Valid syntax, space before ':'.",
+ example: "name : stringValue;",
+ expected: [{type: "property-name", text: "name"},
+ {type: "text", text: " "},
+ {type: "text", text: ":"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "stringValue"},
+ {type: "text", text: ";"}
+ ]}, {
+ description: "Valid syntax, space before ';'.",
+ example: "name: stringValue ;",
+ expected: [{type: "property-name", text: "name"},
+ {type: "text", text: ":"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "stringValue"},
+ {type: "text", text: " "},
+ {type: "text", text: ";"}
+ ]}, {
+ description: "Valid syntax, trailing space.",
+ example: "name: stringValue; ",
+ expected: [{type: "property-name", text: "name"},
+ {type: "text", text: ":"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "stringValue"},
+ {type: "text", text: ";"},
+ {type: "text", text: " "}
+ ]}, {
+ description: "Valid syntax, leading space.",
+ example: " name: stringValue;",
+ expected: [{type: "text", text: " "},
+ {type: "property-name", text: "name"},
+ {type: "text", text: ":"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "stringValue"},
+ {type: "text", text: ";"}
+ ]}, {
+ description: "Valid syntax, two spaces.",
+ example: "name: stringValue;",
+ expected: [{type: "property-name", text: "name"},
+ {type: "text", text: ":"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "stringValue"},
+ {type: "text", text: ";"}
+ ]}, {
+ description: "Valid syntax, no spaces.",
+ example: "name:stringValue;",
+ expected: [{type: "property-name", text: "name"},
+ {type: "text", text: ":"},
+ {type: "property-value", text: "stringValue"},
+ {type: "text", text: ";"}
+ ]}, {
+ description: "Valid syntax, two-part value.",
+ example: "name: stringValue 1;",
+ expected: [{type: "property-name", text: "name"},
+ {type: "text", text: ":"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "stringValue"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "1"},
+ {type: "text", text: ";"}
+ ]}, {
+ description: "Valid syntax, two declarations.",
+ example: "name: stringValue;\n" +
+ "name: 1;",
+ expected: [{type: "property-name", text: "name"},
+ {type: "text", text: ":"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "stringValue"},
+ {type: "text", text: ";"},
+ {type: "text", text: "\n"},
+ {type: "property-name", text: "name"},
+ {type: "text", text: ":"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "1"},
+ {type: "text", text: ";"}
+ ]}, {
+ description: "Valid syntax, commented, numeric value.",
+ example: "/* comment */\n" +
+ "name: 1;",
+ expected: [{type: "comment", text: "/* comment */"},
+ {type: "text", text: "\n"},
+ {type: "property-name", text: "name"},
+ {type: "text", text: ":"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "1"},
+ {type: "text", text: ";"}
+ ]}, {
+ description: "Valid syntax, multiline commented, string value.",
+ example: "/* multiline \n" +
+ "comment */\n" +
+ "name: stringValue;",
+ expected: [{type: "comment", text: "/* multiline \ncomment */"},
+ {type: "text", text: "\n"},
+ {type: "property-name", text: "name"},
+ {type: "text", text: ":"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "stringValue"},
+ {type: "text", text: ";"}
+ ]}, {
+ description: "Valid syntax, commented, two declarations.",
+ example: "/* comment 1 */\n" +
+ "name: 1;\n" +
+ "/* comment 2 */\n" +
+ "name: stringValue;",
+ expected: [{type: "comment", text: "/* comment 1 */"},
+ {type: "text", text: "\n"},
+ {type: "property-name", text: "name"},
+ {type: "text", text: ":"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "1"},
+ {type: "text", text: ";"},
+ {type: "text", text: "\n"},
+ {type: "comment", text: "/* comment 2 */"},
+ {type: "text", text: "\n"},
+ {type: "property-name", text: "name"},
+ {type: "text", text: ":"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "stringValue"},
+ {type: "text", text: ";"}
+ ]}, {
+ description: "Valid syntax, multiline.",
+ example: "name: \n" +
+ "stringValue;",
+ expected: [{type: "property-name", text: "name"},
+ {type: "text", text: ":"},
+ {type: "text", text: " \n"},
+ {type: "property-value", text: "stringValue"},
+ {type: "text", text: ";"}
+ ]}, {
+ description: "Valid syntax, multiline, two declarations.",
+ example: "name: \n" +
+ "stringValue \n" +
+ "stringValue2;",
+ expected: [{type: "property-name", text: "name"},
+ {type: "text", text: ":"},
+ {type: "text", text: " \n"},
+ {type: "property-value", text: "stringValue"},
+ {type: "text", text: " \n"},
+ {type: "property-value", text: "stringValue2"},
+ {type: "text", text: ";"}
+ ]}, {
+ description: "Invalid: not CSS at all.",
+ example: "not CSS at all",
+ expected: [{type: "property-name", text: "not"},
+ {type: "text", text: " "},
+ {type: "property-name", text: "CSS"},
+ {type: "text", text: " "},
+ {type: "property-name", text: "at"},
+ {type: "text", text: " "},
+ {type: "property-name", text: "all"}
+ ]}, {
+ description: "Invalid: switched ':' and ';'.",
+ example: "name; stringValue:",
+ expected: [{type: "property-name", text: "name"},
+ {type: "text", text: ";"},
+ {type: "text", text: " "},
+ {type: "property-name", text: "stringValue"},
+ {type: "text", text: ":"}
+ ]}, {
+ description: "Invalid: unterminated comment.",
+ example: "/* unterminated comment\n" +
+ "name: stringValue;",
+ expected: [{type: "comment", text: "/* unterminated comment\nname: stringValue;"}
+ ]}, {
+ description: "Invalid: bad comment syntax.",
+ example: "// invalid comment\n" +
+ "name: stringValue;",
+ expected: [{type: "text", text: "/"},
+ {type: "text", text: "/"},
+ {type: "text", text: " "},
+ {type: "property-name", text: "invalid"},
+ {type: "text", text: " "},
+ {type: "property-name", text: "comment"},
+ {type: "text", text: "\n"},
+ {type: "property-name", text: "name"},
+ {type: "text", text: ":"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "stringValue"},
+ {type: "text", text: ";"}
+ ]}, {
+ description: "Invalid: no trailing ';'.",
+ example: "name: stringValue\n" +
+ "name: stringValue2",
+ expected: [{type: "property-name", text: "name"},
+ {type: "text", text: ":"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "stringValue"},
+ {type: "text", text: "\n"},
+ {type: "property-value", text: "name"},
+ {type: "text", text: ":"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "stringValue2"},
+ ]}
+];
+
+/**
+ * Iterate through every test case, calling the syntax highlighter,
+ * then calling a helper function to check the output.
+ */
+add_task(function* () {
+ let doc = gBrowser.selectedTab.ownerDocument;
+ let parent = doc.createElement("div");
+ info("Testing all CSS syntax highlighter test cases");
+ for (let {description, example, expected} of TEST_DATA) {
+ info("Testing: " + description);
+ appendSyntaxHighlightedCSS(example, parent);
+ checkCssSyntaxHighlighterOutput(expected, parent);
+ while (parent.firstChild) {
+ parent.firstChild.remove();
+ }
+ }
+});
diff --git a/devtools/client/shared/test/browser_num-l10n.js b/devtools/client/shared/test/browser_num-l10n.js
new file mode 100644
index 000000000..fb4ef6cc7
--- /dev/null
+++ b/devtools/client/shared/test/browser_num-l10n.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the localization utils work properly.
+
+const { LocalizationHelper } = require("devtools/shared/l10n");
+
+function test() {
+ let l10n = new LocalizationHelper();
+
+ is(l10n.numberWithDecimals(1234.56789, 2), "1,234.57",
+ "The first number was properly localized.");
+ is(l10n.numberWithDecimals(0.0001, 2), "0",
+ "The second number was properly localized.");
+ is(l10n.numberWithDecimals(1.0001, 2), "1",
+ "The third number was properly localized.");
+ is(l10n.numberWithDecimals(NaN, 2), "0",
+ "NaN was properly localized.");
+ is(l10n.numberWithDecimals(null, 2), "0",
+ "`null` was properly localized.");
+ is(l10n.numberWithDecimals(undefined, 2), "0",
+ "`undefined` was properly localized.");
+
+ finish();
+}
diff --git a/devtools/client/shared/test/browser_options-view-01.js b/devtools/client/shared/test/browser_options-view-01.js
new file mode 100644
index 000000000..7dae05a3c
--- /dev/null
+++ b/devtools/client/shared/test/browser_options-view-01.js
@@ -0,0 +1,110 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that options-view OptionsView responds to events correctly.
+
+const {OptionsView} = require("devtools/client/shared/options-view");
+
+const BRANCH = "devtools.debugger.";
+const BLACK_BOX_PREF = "auto-black-box";
+const PRETTY_PRINT_PREF = "auto-pretty-print";
+
+const originalBlackBox = Services.prefs.getBoolPref(BRANCH + BLACK_BOX_PREF);
+const originalPrettyPrint = Services.prefs.getBoolPref(BRANCH + PRETTY_PRINT_PREF);
+
+add_task(function* () {
+ info("Setting a couple of preferences");
+ Services.prefs.setBoolPref(BRANCH + BLACK_BOX_PREF, false);
+ Services.prefs.setBoolPref(BRANCH + PRETTY_PRINT_PREF, true);
+
+ info("Opening a test tab and a toolbox host to create the options view in");
+ yield addTab("about:blank");
+ let [host, win] = yield createHost("bottom", OPTIONS_VIEW_URL);
+
+ yield testOptionsView(win);
+
+ info("Closing the host and current tab");
+ host.destroy();
+ gBrowser.removeCurrentTab();
+
+ info("Resetting the preferences");
+ Services.prefs.setBoolPref(BRANCH + BLACK_BOX_PREF, originalBlackBox);
+ Services.prefs.setBoolPref(BRANCH + PRETTY_PRINT_PREF, originalPrettyPrint);
+});
+
+function* testOptionsView(win) {
+ let events = [];
+ let options = createOptionsView(win);
+ yield options.initialize();
+
+ let $ = win.document.querySelector.bind(win.document);
+
+ options.on("pref-changed", (_, pref) => events.push(pref));
+
+ let ppEl = $("menuitem[data-pref='auto-pretty-print']");
+ let bbEl = $("menuitem[data-pref='auto-black-box']");
+
+ // Test default config
+ is(ppEl.getAttribute("checked"), "true", "`true` prefs are checked on start");
+ is(bbEl.getAttribute("checked"), "", "`false` prefs are unchecked on start");
+
+ // Test buttons update when preferences update outside of the menu
+ Services.prefs.setBoolPref(BRANCH + PRETTY_PRINT_PREF, false);
+ Services.prefs.setBoolPref(BRANCH + BLACK_BOX_PREF, true);
+
+ is(options.getPref(PRETTY_PRINT_PREF), false, "getPref returns correct value");
+ is(options.getPref(BLACK_BOX_PREF), true, "getPref returns correct value");
+
+ is(ppEl.getAttribute("checked"), "", "menuitems update when preferences change");
+ is(bbEl.getAttribute("checked"), "true", "menuitems update when preferences change");
+
+ // Tests events are fired when preferences update outside of the menu
+ is(events.length, 2, "two 'pref-changed' events fired");
+ is(events[0], "auto-pretty-print",
+ "correct pref passed in 'pref-changed' event (auto-pretty-print)");
+ is(events[1], "auto-black-box",
+ "correct pref passed in 'pref-changed' event (auto-black-box)");
+
+ // Test buttons update when clicked and preferences are updated
+ yield click(options, win, ppEl);
+ is(ppEl.getAttribute("checked"), "true", "menuitems update when clicked");
+ is(Services.prefs.getBoolPref(BRANCH + PRETTY_PRINT_PREF),
+ true, "preference updated via click");
+
+ yield click(options, win, bbEl);
+ is(bbEl.getAttribute("checked"), "", "menuitems update when clicked");
+ is(Services.prefs.getBoolPref(BRANCH + BLACK_BOX_PREF),
+ false, "preference updated via click");
+
+ // Tests events are fired when preferences updated via click
+ is(events.length, 4, "two 'pref-changed' events fired");
+ is(events[2], "auto-pretty-print",
+ "correct pref passed in 'pref-changed' event (auto-pretty-print)");
+ is(events[3], "auto-black-box",
+ "correct pref passed in 'pref-changed' event (auto-black-box)");
+
+ yield options.destroy();
+}
+
+function createOptionsView(win) {
+ return new OptionsView({
+ branchName: BRANCH,
+ menupopup: win.document.querySelector("#options-menupopup")
+ });
+}
+
+function* click(view, win, menuitem) {
+ let opened = view.once("options-shown");
+ let closed = view.once("options-hidden");
+
+ let button = win.document.querySelector("#options-button");
+ EventUtils.synthesizeMouseAtCenter(button, {}, win);
+ yield opened;
+ is(button.getAttribute("open"), "true", "button has `open` attribute");
+
+ EventUtils.synthesizeMouseAtCenter(menuitem, {}, win);
+ yield closed;
+ ok(!button.hasAttribute("open"), "button does not have `open` attribute");
+}
diff --git a/devtools/client/shared/test/browser_outputparser.js b/devtools/client/shared/test/browser_outputparser.js
new file mode 100644
index 000000000..a231ad903
--- /dev/null
+++ b/devtools/client/shared/test/browser_outputparser.js
@@ -0,0 +1,292 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {OutputParser} = require("devtools/client/shared/output-parser");
+const {initCssProperties, getCssProperties} = require("devtools/shared/fronts/css-properties");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host, , doc] = yield createHost("bottom", "data:text/html," +
+ "<h1>browser_outputParser.js</h1><div></div>");
+
+ // Mock the toolbox that initCssProperties expect so we get the fallback css properties.
+ let toolbox = {target: {client: {}, hasActor: () => false}};
+ yield initCssProperties(toolbox);
+ let cssProperties = getCssProperties(toolbox);
+
+ let parser = new OutputParser(doc, cssProperties);
+ testParseCssProperty(doc, parser);
+ testParseCssVar(doc, parser);
+ testParseURL(doc, parser);
+ testParseFilter(doc, parser);
+ testParseAngle(doc, parser);
+
+ host.destroy();
+}
+
+// Class name used in color swatch.
+var COLOR_TEST_CLASS = "test-class";
+
+// Create a new CSS color-parsing test. |name| is the name of the CSS
+// property. |value| is the CSS text to use. |segments| is an array
+// describing the expected result. If an element of |segments| is a
+// string, it is simply appended to the expected string. Otherwise,
+// it must be an object with a |name| property, which is the color
+// name as it appears in the input.
+//
+// This approach is taken to reduce boilerplate and to make it simpler
+// to modify the test when the parseCssProperty output changes.
+function makeColorTest(name, value, segments) {
+ let result = {
+ name,
+ value,
+ expected: ""
+ };
+
+ for (let segment of segments) {
+ if (typeof (segment) === "string") {
+ result.expected += segment;
+ } else {
+ result.expected += "<span data-color=\"" + segment.name + "\">" +
+ "<span class=\"" + COLOR_TEST_CLASS + "\" style=\"background-color:" +
+ segment.name + "\"></span><span>" +
+ segment.name + "</span></span>";
+ }
+ }
+
+ result.desc = "Testing " + name + ": " + value;
+
+ return result;
+}
+
+function testParseCssProperty(doc, parser) {
+ let tests = [
+ makeColorTest("border", "1px solid red",
+ ["1px solid ", {name: "red"}]),
+
+ makeColorTest("background-image",
+ "linear-gradient(to right, #F60 10%, rgba(0,0,0,1))",
+ ["linear-gradient(to right, ", {name: "#F60"},
+ " 10%, ", {name: "rgba(0,0,0,1)"},
+ ")"]),
+
+ // In "arial black", "black" is a font, not a color.
+ makeColorTest("font-family", "arial black", ["arial black"]),
+
+ makeColorTest("box-shadow", "0 0 1em red",
+ ["0 0 1em ", {name: "red"}]),
+
+ makeColorTest("box-shadow",
+ "0 0 1em red, 2px 2px 0 0 rgba(0,0,0,.5)",
+ ["0 0 1em ", {name: "red"},
+ ", 2px 2px 0 0 ",
+ {name: "rgba(0,0,0,.5)"}]),
+
+ makeColorTest("content", "\"red\"", ["\"red\""]),
+
+ // Invalid property names should not cause exceptions.
+ makeColorTest("hellothere", "'red'", ["'red'"]),
+
+ makeColorTest("filter",
+ "blur(1px) drop-shadow(0 0 0 blue) url(red.svg#blue)",
+ ["<span data-filters=\"blur(1px) drop-shadow(0 0 0 blue) ",
+ "url(red.svg#blue)\"><span>",
+ "blur(1px) drop-shadow(0 0 0 ",
+ {name: "blue"},
+ ") url(red.svg#blue)</span></span>"]),
+
+ makeColorTest("color", "currentColor", ["currentColor"]),
+
+ // Test a very long property.
+ makeColorTest("background-image",
+ /* eslint-disable max-len */
+ "linear-gradient(to left, transparent 0, transparent 5%,#F00 0, #F00 10%,#FF0 0, #FF0 15%,#0F0 0, #0F0 20%,#0FF 0, #0FF 25%,#00F 0, #00F 30%,#800 0, #800 35%,#880 0, #880 40%,#080 0, #080 45%,#088 0, #088 50%,#008 0, #008 55%,#FFF 0, #FFF 60%,#EEE 0, #EEE 65%,#CCC 0, #CCC 70%,#999 0, #999 75%,#666 0, #666 80%,#333 0, #333 85%,#111 0, #111 90%,#000 0, #000 95%,transparent 0, transparent 100%)",
+ /* eslint-enable max-len */
+ ["linear-gradient(to left, ", {name: "transparent"},
+ " 0, ", {name: "transparent"},
+ " 5%,", {name: "#F00"},
+ " 0, ", {name: "#F00"},
+ " 10%,", {name: "#FF0"},
+ " 0, ", {name: "#FF0"},
+ " 15%,", {name: "#0F0"},
+ " 0, ", {name: "#0F0"},
+ " 20%,", {name: "#0FF"},
+ " 0, ", {name: "#0FF"},
+ " 25%,", {name: "#00F"},
+ " 0, ", {name: "#00F"},
+ " 30%,", {name: "#800"},
+ " 0, ", {name: "#800"},
+ " 35%,", {name: "#880"},
+ " 0, ", {name: "#880"},
+ " 40%,", {name: "#080"},
+ " 0, ", {name: "#080"},
+ " 45%,", {name: "#088"},
+ " 0, ", {name: "#088"},
+ " 50%,", {name: "#008"},
+ " 0, ", {name: "#008"},
+ " 55%,", {name: "#FFF"},
+ " 0, ", {name: "#FFF"},
+ " 60%,", {name: "#EEE"},
+ " 0, ", {name: "#EEE"},
+ " 65%,", {name: "#CCC"},
+ " 0, ", {name: "#CCC"},
+ " 70%,", {name: "#999"},
+ " 0, ", {name: "#999"},
+ " 75%,", {name: "#666"},
+ " 0, ", {name: "#666"},
+ " 80%,", {name: "#333"},
+ " 0, ", {name: "#333"},
+ " 85%,", {name: "#111"},
+ " 0, ", {name: "#111"},
+ " 90%,", {name: "#000"},
+ " 0, ", {name: "#000"},
+ " 95%,", {name: "transparent"},
+ " 0, ", {name: "transparent"},
+ " 100%)"]),
+ ];
+
+ let target = doc.querySelector("div");
+ ok(target, "captain, we have the div");
+
+ for (let test of tests) {
+ info(test.desc);
+
+ let frag = parser.parseCssProperty(test.name, test.value, {
+ colorSwatchClass: COLOR_TEST_CLASS
+ });
+
+ target.appendChild(frag);
+
+ is(target.innerHTML, test.expected,
+ "CSS property correctly parsed for " + test.name + ": " + test.value);
+
+ target.innerHTML = "";
+ }
+}
+
+function testParseCssVar(doc, parser) {
+ let frag = parser.parseCssProperty("color", "var(--some-kind-of-green)", {
+ colorSwatchClass: "test-colorswatch"
+ });
+
+ let target = doc.querySelector("div");
+ ok(target, "captain, we have the div");
+ target.appendChild(frag);
+
+ is(target.innerHTML, "var(--some-kind-of-green)",
+ "CSS property correctly parsed");
+
+ target.innerHTML = "";
+}
+
+function testParseURL(doc, parser) {
+ info("Test that URL parsing preserves quoting style");
+
+ const tests = [
+ {
+ desc: "simple test without quotes",
+ leader: "url(",
+ trailer: ")",
+ },
+ {
+ desc: "simple test with single quotes",
+ leader: "url('",
+ trailer: "')",
+ },
+ {
+ desc: "simple test with double quotes",
+ leader: "url(\"",
+ trailer: "\")",
+ },
+ {
+ desc: "test with single quotes and whitespace",
+ leader: "url( \t'",
+ trailer: "'\r\n\f)",
+ },
+ {
+ desc: "simple test with uppercase",
+ leader: "URL(",
+ trailer: ")",
+ },
+ {
+ desc: "bad url, missing paren",
+ leader: "url(",
+ trailer: "",
+ expectedTrailer: ")"
+ },
+ {
+ desc: "bad url, missing paren, with baseURI",
+ baseURI: "data:text/html,<style></style>",
+ leader: "url(",
+ trailer: "",
+ expectedTrailer: ")"
+ },
+ {
+ desc: "bad url, double quote, missing paren",
+ leader: "url(\"",
+ trailer: "\"",
+ expectedTrailer: "\")",
+ },
+ {
+ desc: "bad url, single quote, missing paren and quote",
+ leader: "url('",
+ trailer: "",
+ expectedTrailer: "')"
+ }
+ ];
+
+ for (let test of tests) {
+ let url = test.leader + "something.jpg" + test.trailer;
+ let frag = parser.parseCssProperty("background", url, {
+ urlClass: "test-urlclass",
+ baseURI: test.baseURI,
+ });
+
+ let target = doc.querySelector("div");
+ target.appendChild(frag);
+
+ let expectedTrailer = test.expectedTrailer || test.trailer;
+
+ let expected = test.leader +
+ "<a target=\"_blank\" class=\"test-urlclass\" " +
+ "href=\"something.jpg\">something.jpg</a>" +
+ expectedTrailer;
+
+ is(target.innerHTML, expected, test.desc);
+
+ target.innerHTML = "";
+ }
+}
+
+function testParseFilter(doc, parser) {
+ let frag = parser.parseCssProperty("filter", "something invalid", {
+ filterSwatchClass: "test-filterswatch"
+ });
+
+ let swatchCount = frag.querySelectorAll(".test-filterswatch").length;
+ is(swatchCount, 1, "filter swatch was created");
+}
+
+function testParseAngle(doc, parser) {
+ let frag = parser.parseCssProperty("image-orientation", "90deg", {
+ angleSwatchClass: "test-angleswatch"
+ });
+
+ let swatchCount = frag.querySelectorAll(".test-angleswatch").length;
+ is(swatchCount, 1, "angle swatch was created");
+
+ frag = parser.parseCssProperty("background-image",
+ "linear-gradient(90deg, red, blue", {
+ angleSwatchClass: "test-angleswatch"
+ });
+
+ swatchCount = frag.querySelectorAll(".test-angleswatch").length;
+ is(swatchCount, 1, "angle swatch was created");
+}
diff --git a/devtools/client/shared/test/browser_poller.js b/devtools/client/shared/test/browser_poller.js
new file mode 100644
index 000000000..281d055ff
--- /dev/null
+++ b/devtools/client/shared/test/browser_poller.js
@@ -0,0 +1,136 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests the Poller class.
+
+const { Poller } = require("devtools/client/shared/poller");
+
+add_task(function* () {
+ let count1 = 0, count2 = 0, count3 = 0;
+
+ let poller1 = new Poller(function () {
+ count1++;
+ }, 1000000000, true);
+ let poller2 = new Poller(function () {
+ count2++;
+ }, 10);
+ let poller3 = new Poller(function () {
+ count3++;
+ }, 1000000000);
+
+ poller2.on();
+
+ ok(!poller1.isPolling(), "isPolling() returns false for an off poller");
+ ok(poller2.isPolling(), "isPolling() returns true for an on poller");
+
+ yield waitUntil(() => count2 > 10);
+
+ ok(count2 > 10, "poller that was turned on polled several times");
+ ok(count1 === 0, "poller that was never turned on never polled");
+
+ yield poller2.off();
+ let currentCount2 = count2;
+
+ // Really high poll time!
+ poller1.on();
+ poller3.on();
+
+ yield waitUntil(() => count1 === 1);
+ ok(true, "Poller calls fn immediately when `immediate` is true");
+ ok(count3 === 0, "Poller does not call fn immediately when `immediate` is not set");
+
+ ok(count2 === currentCount2, "a turned off poller does not continue to poll");
+ yield poller2.off();
+ yield poller2.off();
+ yield poller2.off();
+ ok(true, "Poller.prototype.off() is idempotent");
+
+ // This should still have not polled a second time
+ is(count1, 1, "wait time works");
+
+ ok(poller1.isPolling(), "isPolling() returns true for an on poller");
+ ok(!poller2.isPolling(), "isPolling() returns false for an off poller");
+});
+
+add_task(function* () {
+ let count = -1;
+ // Create a poller that returns a promise.
+ // The promise is resolved asynchronously after adding 9 to the count, ensuring
+ // that on every poll, we have a multiple of 10.
+ let asyncPoller = new Poller(function () {
+ count++;
+ ok(!(count % 10), `Async poller called with a multiple of 10: ${count}`);
+ return new Promise(function (resolve, reject) {
+ let add9 = 9;
+ let interval = setInterval(() => {
+ if (add9--) {
+ count++;
+ } else {
+ clearInterval(interval);
+ resolve();
+ }
+ }, 10);
+ });
+ });
+
+ asyncPoller.on(1);
+ yield waitUntil(() => count > 50);
+ yield asyncPoller.off();
+});
+
+add_task(function* () {
+ // Create a poller that returns a promise. This poll call
+ // is called immediately, and then subsequently turned off.
+ // The call to `off` should not resolve until the inflight call
+ // finishes.
+ let inflightFinished = null;
+ let pollCalls = 0;
+ let asyncPoller = new Poller(function () {
+ pollCalls++;
+ return new Promise(function (resolve, reject) {
+ setTimeout(() => {
+ inflightFinished = true;
+ resolve();
+ }, 1000);
+ });
+ }, 1, true);
+ asyncPoller.on();
+
+ yield asyncPoller.off();
+ ok(inflightFinished,
+ "off() method does not resolve until remaining inflight poll calls finish");
+ is(pollCalls, 1, "should only be one poll call to occur before turning off polling");
+});
+
+add_task(function* () {
+ // Create a poller that returns a promise. This poll call
+ // is called immediately, and then subsequently turned off.
+ // The call to `off` should not resolve until the inflight call
+ // finishes.
+ let inflightFinished = null;
+ let pollCalls = 0;
+ let asyncPoller = new Poller(function () {
+ pollCalls++;
+ return new Promise(function (resolve, reject) {
+ setTimeout(() => {
+ inflightFinished = true;
+ resolve();
+ }, 1000);
+ });
+ }, 1, true);
+ asyncPoller.on();
+
+ yield asyncPoller.destroy();
+ ok(inflightFinished,
+ "destroy() method does not resolve until remaining inflight poll calls finish");
+ is(pollCalls, 1, "should only be one poll call to occur before destroying polling");
+
+ try {
+ asyncPoller.on();
+ ok(false, "Calling on() after destruction should throw");
+ } catch (e) {
+ ok(true, "Calling on() after destruction should throw");
+ }
+});
diff --git a/devtools/client/shared/test/browser_prefs-01.js b/devtools/client/shared/test/browser_prefs-01.js
new file mode 100644
index 000000000..193348361
--- /dev/null
+++ b/devtools/client/shared/test/browser_prefs-01.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the preference helpers work properly.
+
+const { PrefsHelper } = require("devtools/client/shared/prefs");
+
+function test() {
+ let Prefs = new PrefsHelper("devtools.debugger", {
+ "foo": ["Bool", "enabled"]
+ });
+
+ let originalPrefValue = Services.prefs.getBoolPref("devtools.debugger.enabled");
+ is(Prefs.foo, originalPrefValue, "The pref value was correctly fetched.");
+
+ Prefs.foo = !originalPrefValue;
+ is(Prefs.foo, !originalPrefValue,
+ "The pref was was correctly changed (1).");
+ is(Services.prefs.getBoolPref("devtools.debugger.enabled"), !originalPrefValue,
+ "The pref was was correctly changed (2).");
+
+ Services.prefs.setBoolPref("devtools.debugger.enabled", originalPrefValue);
+ info("The pref value was reset (1).");
+ is(Prefs.foo, !originalPrefValue,
+ "The cached pref value hasn't changed yet (1).");
+
+ Services.prefs.setBoolPref("devtools.debugger.enabled", !originalPrefValue);
+ info("The pref value was reset (2).");
+ is(Prefs.foo, !originalPrefValue,
+ "The cached pref value hasn't changed yet (2).");
+
+ Prefs.registerObserver();
+
+ Services.prefs.setBoolPref("devtools.debugger.enabled", originalPrefValue);
+ info("The pref value was reset (3).");
+ is(Prefs.foo, originalPrefValue,
+ "The cached pref value has changed now.");
+
+ Prefs.unregisterObserver();
+
+ finish();
+}
diff --git a/devtools/client/shared/test/browser_prefs-02.js b/devtools/client/shared/test/browser_prefs-02.js
new file mode 100644
index 000000000..f0f638d63
--- /dev/null
+++ b/devtools/client/shared/test/browser_prefs-02.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that preference helpers work properly with custom types of Float and Json.
+
+const { PrefsHelper } = require("devtools/client/shared/prefs");
+
+function test() {
+ let originalJson = Services.prefs.getCharPref(
+ "devtools.performance.timeline.hidden-markers");
+ let originalFloat = Services.prefs.getCharPref(
+ "devtools.performance.memory.sample-probability");
+
+ let Prefs = new PrefsHelper("devtools.performance", {
+ "float": ["Float", "memory.sample-probability"],
+ "json": ["Json", "timeline.hidden-markers"]
+ });
+
+ Prefs.registerObserver();
+
+ // Float
+ Services.prefs.setCharPref("devtools.performance.timeline.hidden-markers", "{\"a\":1}");
+ is(Prefs.json.a, 1, "The JSON pref value is correctly casted on get.");
+
+ Prefs.json = { b: 2 };
+ is(Prefs.json.a, undefined, "The JSON pref value is correctly casted on set (1).");
+ is(Prefs.json.b, 2, "The JSON pref value is correctly casted on set (2).");
+
+ // Float
+ Services.prefs.setCharPref("devtools.performance.memory.sample-probability", "3.14");
+ is(Prefs.float, 3.14, "The float pref value is correctly casted on get.");
+
+ Prefs.float = 6.28;
+ is(Prefs.float, 6.28, "The float pref value is correctly casted on set.");
+
+ Prefs.unregisterObserver();
+
+ Services.prefs.setCharPref("devtools.performance.timeline.hidden-markers",
+ originalJson);
+ Services.prefs.setCharPref("devtools.performance.memory.sample-probability",
+ originalFloat);
+ finish();
+}
diff --git a/devtools/client/shared/test/browser_require_raw.js b/devtools/client/shared/test/browser_require_raw.js
new file mode 100644
index 000000000..d40b84c35
--- /dev/null
+++ b/devtools/client/shared/test/browser_require_raw.js
@@ -0,0 +1,20 @@
+/* -*- 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 { BrowserLoader } = Cu.import("resource://devtools/client/shared/browser-loader.js", {});
+
+const { require: browserRequire } = BrowserLoader({
+ baseURI: "resource://devtools/client/shared/",
+ window
+});
+
+const variableFileContents = browserRequire("raw!devtools/client/themes/variables.css");
+
+function test() {
+ ok(variableFileContents.length > 0, "raw browserRequire worked");
+ finish();
+}
diff --git a/devtools/client/shared/test/browser_spectrum.js b/devtools/client/shared/test/browser_spectrum.js
new file mode 100644
index 000000000..9e72ef621
--- /dev/null
+++ b/devtools/client/shared/test/browser_spectrum.js
@@ -0,0 +1,114 @@
+/* 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";
+
+// Tests that the spectrum color picker works correctly
+
+const {Spectrum} = require("devtools/client/shared/widgets/Spectrum");
+
+const TEST_URI = `data:text/html,
+ <link rel="stylesheet" href="chrome://devtools/content/shared/widgets/spectrum.css" type="text/css"/>
+ <div id="spectrum-container" />`;
+
+add_task(function* () {
+ let [host,, doc] = yield createHost("bottom", TEST_URI);
+
+ let container = doc.getElementById("spectrum-container");
+
+ yield testCreateAndDestroyShouldAppendAndRemoveElements(container);
+ yield testPassingAColorAtInitShouldSetThatColor(container);
+ yield testSettingAndGettingANewColor(container);
+ yield testChangingColorShouldEmitEvents(container);
+ yield testSettingColorShoudUpdateTheUI(container);
+
+ host.destroy();
+});
+
+function testCreateAndDestroyShouldAppendAndRemoveElements(container) {
+ ok(container, "We have the root node to append spectrum to");
+ is(container.childElementCount, 0, "Root node is empty");
+
+ let s = new Spectrum(container, [255, 126, 255, 1]);
+ s.show();
+ ok(container.childElementCount > 0, "Spectrum has appended elements");
+
+ s.destroy();
+ is(container.childElementCount, 0, "Destroying spectrum removed all nodes");
+}
+
+function testPassingAColorAtInitShouldSetThatColor(container) {
+ let initRgba = [255, 126, 255, 1];
+
+ let s = new Spectrum(container, initRgba);
+ s.show();
+
+ let setRgba = s.rgb;
+
+ is(initRgba[0], setRgba[0], "Spectrum initialized with the right color");
+ is(initRgba[1], setRgba[1], "Spectrum initialized with the right color");
+ is(initRgba[2], setRgba[2], "Spectrum initialized with the right color");
+ is(initRgba[3], setRgba[3], "Spectrum initialized with the right color");
+
+ s.destroy();
+}
+
+function testSettingAndGettingANewColor(container) {
+ let s = new Spectrum(container, [0, 0, 0, 1]);
+ s.show();
+
+ let colorToSet = [255, 255, 255, 1];
+ s.rgb = colorToSet;
+ let newColor = s.rgb;
+
+ is(colorToSet[0], newColor[0], "Spectrum set with the right color");
+ is(colorToSet[1], newColor[1], "Spectrum set with the right color");
+ is(colorToSet[2], newColor[2], "Spectrum set with the right color");
+ is(colorToSet[3], newColor[3], "Spectrum set with the right color");
+
+ s.destroy();
+}
+
+function testChangingColorShouldEmitEvents(container) {
+ return new Promise(resolve => {
+ let s = new Spectrum(container, [255, 255, 255, 1]);
+ s.show();
+
+ s.once("changed", (event, rgba, color) => {
+ ok(true, "Changed event was emitted on color change");
+ is(rgba[0], 128, "New color is correct");
+ is(rgba[1], 64, "New color is correct");
+ is(rgba[2], 64, "New color is correct");
+ is(rgba[3], 1, "New color is correct");
+ is(`rgba(${rgba.join(", ")})`, color, "RGBA and css color correspond");
+
+ s.destroy();
+ resolve();
+ });
+
+ // Simulate a drag move event by calling the handler directly.
+ s.onDraggerMove(s.dragger.offsetWidth / 2, s.dragger.offsetHeight / 2);
+ });
+}
+
+function testSettingColorShoudUpdateTheUI(container) {
+ let s = new Spectrum(container, [255, 255, 255, 1]);
+ s.show();
+ let dragHelperOriginalPos = [s.dragHelper.style.top, s.dragHelper.style.left];
+ let alphaHelperOriginalPos = s.alphaSliderHelper.style.left;
+
+ s.rgb = [50, 240, 234, .2];
+ s.updateUI();
+
+ ok(s.alphaSliderHelper.style.left != alphaHelperOriginalPos, "Alpha helper has moved");
+ ok(s.dragHelper.style.top !== dragHelperOriginalPos[0], "Drag helper has moved");
+ ok(s.dragHelper.style.left !== dragHelperOriginalPos[1], "Drag helper has moved");
+
+ s.rgb = [240, 32, 124, 0];
+ s.updateUI();
+ is(s.alphaSliderHelper.style.left, -(s.alphaSliderHelper.offsetWidth / 2) + "px",
+ "Alpha range UI has been updated again");
+
+ s.destroy();
+}
diff --git a/devtools/client/shared/test/browser_tableWidget_basic.js b/devtools/client/shared/test/browser_tableWidget_basic.js
new file mode 100644
index 000000000..684ca99bb
--- /dev/null
+++ b/devtools/client/shared/test/browser_tableWidget_basic.js
@@ -0,0 +1,390 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the table widget api works fine
+
+"use strict";
+
+const TEST_URI = "data:text/xml;charset=UTF-8,<?xml version='1.0'?>" +
+ "<?xml-stylesheet href='chrome://global/skin/global.css'?>" +
+
+ // Uncomment these lines to help with visual debugging. When uncommented they
+ // dump a couple of thousand errors in the log (bug 1258285)
+ // "<?xml-stylesheet href='chrome://devtools/skin/light-theme.css'?>" +
+ // "<?xml-stylesheet href='chrome://devtools/skin/widgets.css'?>" +
+
+ "<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'" +
+ " title='Table Widget' width='600' height='500'>" +
+ "<box flex='1' class='theme-light'/></window>";
+
+const {TableWidget} = require("devtools/client/shared/widgets/TableWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ let [host, , doc] = yield createHost("bottom", TEST_URI);
+
+ let table = new TableWidget(doc.querySelector("box"), {
+ initialColumns: {
+ col1: "Column 1",
+ col2: "Column 2",
+ col3: "Column 3",
+ col4: "Column 4"
+ },
+ uniqueId: "col1",
+ emptyText: "This is dummy empty text",
+ highlightUpdated: true,
+ removableColumns: true,
+ firstColumn: "col4"
+ });
+
+ startTests(doc, table);
+ endTests(doc, host, table);
+});
+
+function startTests(doc, table) {
+ populateTable(doc, table);
+
+ testTreeItemInsertedCorrectly(doc, table);
+ testAPI(doc, table);
+}
+
+function endTests(doc, host, table) {
+ table.destroy();
+ host.destroy();
+ gBrowser.removeCurrentTab();
+ table = null;
+ finish();
+}
+
+function populateTable(doc, table) {
+ table.push({
+ col1: "id1",
+ col2: "value10",
+ col3: "value20",
+ col4: "value30"
+ });
+ table.push({
+ col1: "id2",
+ col2: "value14",
+ col3: "value29",
+ col4: "value32"
+ });
+ table.push({
+ col1: "id3",
+ col2: "value17",
+ col3: "value21",
+ col4: "value31",
+ extraData: "foobar",
+ extraData2: 42
+ });
+ table.push({
+ col1: "id4",
+ col2: "value12",
+ col3: "value26",
+ col4: "value33"
+ });
+ table.push({
+ col1: "id5",
+ col2: "value19",
+ col3: "value26",
+ col4: "value37"
+ });
+ table.push({
+ col1: "id6",
+ col2: "value15",
+ col3: "value25",
+ col4: "value37"
+ });
+ table.push({
+ col1: "id7",
+ col2: "value18",
+ col3: "value21",
+ col4: "value36",
+ somethingExtra: "Hello World!"
+ });
+ table.push({
+ col1: "id8",
+ col2: "value11",
+ col3: "value27",
+ col4: "value34"
+ });
+
+ let span = doc.createElement("span");
+ span.textContent = "domnode";
+
+ table.push({
+ col1: "id9",
+ col2: "value11",
+ col3: "value23",
+ col4: span
+ });
+}
+
+/**
+ * Test if the nodes are inserted correctly in the table.
+ */
+function testTreeItemInsertedCorrectly(doc, table) {
+ // double because of splitters
+ is(table.tbody.children.length, 4 * 2, "4 columns exist");
+
+ // Test firstColumn option and check if the nodes are inserted correctly
+ is(table.tbody.children[0].firstChild.children.length, 9 + 1,
+ "Correct rows in column 4");
+ is(table.tbody.children[0].firstChild.firstChild.value, "Column 4",
+ "Correct column header value");
+
+ for (let i = 1; i < 4; i++) {
+ is(table.tbody.children[i * 2].firstChild.children.length, 9 + 1,
+ `Correct rows in column ${i}`);
+ is(table.tbody.children[i * 2].firstChild.firstChild.value, `Column ${i}`,
+ "Correct column header value");
+ }
+ for (let i = 1; i < 10; i++) {
+ is(table.tbody.children[2].firstChild.children[i].value, `id${i}`,
+ `Correct value in row ${i}`);
+ }
+
+ // Remove firstColumn option and reset the table
+ table.clear();
+ table.firstColumn = "";
+ table.setColumns({
+ col1: "Column 1",
+ col2: "Column 2",
+ col3: "Column 3",
+ col4: "Column 4"
+ });
+ populateTable(doc, table);
+
+ // Check if the nodes are inserted correctly without firstColumn option
+ for (let i = 0; i < 4; i++) {
+ is(table.tbody.children[i * 2].firstChild.children.length, 9 + 1,
+ `Correct rows in column ${i}`);
+ is(table.tbody.children[i * 2].firstChild.firstChild.value,
+ `Column ${i + 1}`,
+ "Correct column header value");
+ }
+
+ for (let i = 1; i < 10; i++) {
+ is(table.tbody.firstChild.firstChild.children[i].value, `id${i}`,
+ `Correct value in row ${i}`);
+ }
+}
+
+/**
+ * Tests if the API exposed by TableWidget works properly
+ */
+function testAPI(doc, table) {
+ info("Testing TableWidget API");
+ // Check if selectRow and selectedRow setter works as expected
+ // Nothing should be selected beforehand
+ ok(!doc.querySelector(".theme-selected"), "Nothing is selected");
+ table.selectRow("id4");
+ let node = doc.querySelector(".theme-selected");
+ ok(!!node, "Somthing got selected");
+ is(node.getAttribute("data-id"), "id4", "Correct node selected");
+
+ table.selectRow("id7");
+ let node2 = doc.querySelector(".theme-selected");
+ ok(!!node2, "Somthing is still selected");
+ isnot(node, node2, "Newly selected node is different from previous");
+ is(node2.getAttribute("data-id"), "id7", "Correct node selected");
+
+ // test if selectedIRow getter works
+ is(table.selectedRow.col1, "id7", "Correct result of selectedRow getter");
+
+ // test if isSelected works
+ ok(table.isSelected("id7"), "isSelected with column id works");
+ ok(table.isSelected({
+ col1: "id7",
+ col2: "value18",
+ col3: "value21",
+ col4: "value36",
+ somethingExtra: "Hello World!"
+ }), "isSelected with json works");
+
+ table.selectedRow = "id4";
+ let node3 = doc.querySelector(".theme-selected");
+ ok(!!node3, "Somthing is still selected");
+ isnot(node2, node3, "Newly selected node is different from previous");
+ is(node3, node, "First and third selected nodes should be same");
+ is(node3.getAttribute("data-id"), "id4", "Correct node selected");
+
+ // test if selectedRow getter works
+ is(table.selectedRow.col1, "id4", "Correct result of selectedRow getter");
+
+ // test if clear selection works
+ table.clearSelection();
+ ok(!doc.querySelector(".theme-selected"),
+ "Nothing selected after clear selection call");
+
+ // test if selectNextRow and selectPreviousRow work
+ table.selectedRow = "id7";
+ ok(table.isSelected("id7"), "Correct row selected");
+ table.selectNextRow();
+ ok(table.isSelected("id8"), "Correct row selected after selectNextRow call");
+
+ table.selectNextRow();
+ ok(table.isSelected("id9"), "Correct row selected after selectNextRow call");
+
+ table.selectNextRow();
+ ok(table.isSelected("id1"),
+ "Properly cycled to first row after selectNextRow call on last row");
+
+ table.selectNextRow();
+ ok(table.isSelected("id2"), "Correct row selected after selectNextRow call");
+
+ table.selectPreviousRow();
+ ok(table.isSelected("id1"),
+ "Correct row selected after selectPreviousRow call");
+
+ table.selectPreviousRow();
+ ok(table.isSelected("id9"),
+ "Properly cycled to last row after selectPreviousRow call on first row");
+
+ // test if remove works
+ ok(doc.querySelector("[data-id='id4']"), "id4 row exists before removal");
+ table.remove("id4");
+ ok(!doc.querySelector("[data-id='id4']"),
+ "id4 row does not exist after removal through id");
+
+ ok(doc.querySelector("[data-id='id6']"), "id6 row exists before removal");
+ table.remove({
+ col1: "id6",
+ col2: "value15",
+ col3: "value25",
+ col4: "value37"
+ });
+ ok(!doc.querySelector("[data-id='id6']"),
+ "id6 row does not exist after removal through json");
+
+ table.push({
+ col1: "id4",
+ col2: "value12",
+ col3: "value26",
+ col4: "value33"
+ });
+ table.push({
+ col1: "id6",
+ col2: "value15",
+ col3: "value25",
+ col4: "value37"
+ });
+
+ // test if selectedIndex getter setter works
+ table.selectedIndex = 2;
+ ok(table.isSelected("id3"), "Correct row selected by selectedIndex setter");
+
+ table.selectedIndex = 4;
+ ok(table.isSelected("id5"), "Correct row selected by selectedIndex setter");
+
+ table.selectRow("id8");
+ is(table.selectedIndex, 7, "Correct value of selectedIndex getter");
+
+ // testing if clear works
+ table.clear();
+ // double because splitters
+ is(table.tbody.children.length, 4 * 2,
+ "4 columns exist even after clear");
+ for (let i = 0; i < 4; i++) {
+ is(table.tbody.children[i * 2].firstChild.children.length, 1,
+ `Only header in the column ${i} after clear call`);
+ is(table.tbody.children[i * 2].firstChild.firstChild.value,
+ `Column ${i + 1}`,
+ "Correct column header value");
+ }
+
+ // testing if setColumns work
+ table.setColumns({
+ col1: "Foobar",
+ col2: "Testing"
+ });
+
+ // double because splitters
+ is(table.tbody.children.length, 2 * 2,
+ "2 columns exist after setColumn call");
+ is(table.tbody.children[0].firstChild.firstChild.value, "Foobar",
+ "Correct column header value for first column");
+ is(table.tbody.children[2].firstChild.firstChild.value, "Testing",
+ "Correct column header value for second column");
+
+ table.setColumns({
+ col1: "Column 1",
+ col2: "Column 2",
+ col3: "Column 3",
+ col4: "Column 4"
+ });
+ // double because splitters
+ is(table.tbody.children.length, 4 * 2,
+ "4 columns exist after second setColumn call");
+
+ populateTable(doc, table);
+
+ // testing if update works
+ is(doc.querySelectorAll("[data-id='id4']")[1].value, "value12",
+ "Correct value before update");
+ table.update({
+ col1: "id4",
+ col2: "UPDATED",
+ col3: "value26",
+ col4: "value33"
+ });
+ is(doc.querySelectorAll("[data-id='id4']")[1].value, "UPDATED",
+ "Correct value after update");
+
+ // testing if sorting works by calling it once on an already sorted column
+ // should sort descending
+ table.sortBy("col1");
+ for (let i = 1; i < 10; i++) {
+ is(table.tbody.firstChild.firstChild.children[i].value, `id${10 - i}`,
+ `Correct value in row ${i} after descending sort by on col1`);
+ }
+ // Calling it on an unsorted column should sort by it in ascending manner
+ table.sortBy("col2");
+ let cell = table.tbody.children[2].firstChild.children[2];
+ checkAscendingOrder(cell);
+
+ // Calling it again should sort by it in descending manner
+ table.sortBy("col2");
+ cell = table.tbody.children[2].firstChild.lastChild.previousSibling;
+ checkDescendingOrder(cell);
+
+ // Calling it again should sort by it in ascending manner
+ table.sortBy("col2");
+ cell = table.tbody.children[2].firstChild.children[2];
+ checkAscendingOrder(cell);
+
+ table.clear();
+ populateTable(doc, table);
+
+ // testing if sorting works should sort by ascending manner
+ table.sortBy("col4");
+ cell = table.tbody.children[6].firstChild.children[1];
+ is(cell.textContent, "domnode", "DOMNode sorted correctly");
+ checkAscendingOrder(cell.nextSibling);
+
+ // Calling it again should sort it in descending order
+ table.sortBy("col4");
+ cell = table.tbody.children[6].firstChild.children[9];
+ is(cell.textContent, "domnode", "DOMNode sorted correctly");
+ checkDescendingOrder(cell.previousSibling);
+}
+
+function checkAscendingOrder(cell) {
+ while (cell) {
+ let currentCell = cell.value || cell.textContent;
+ let prevCell = cell.previousSibling.value ||
+ cell.previousSibling.textContent;
+ ok(currentCell >= prevCell, "Sorting is in ascending order");
+ cell = cell.nextSibling;
+ }
+}
+
+function checkDescendingOrder(cell) {
+ while (cell != cell.parentNode.firstChild) {
+ let currentCell = cell.value || cell.textContent;
+ let nextCell = cell.nextSibling.value || cell.nextSibling.textContent;
+ ok(currentCell >= nextCell, "Sorting is in descending order");
+ cell = cell.previousSibling;
+ }
+}
diff --git a/devtools/client/shared/test/browser_tableWidget_keyboard_interaction.js b/devtools/client/shared/test/browser_tableWidget_keyboard_interaction.js
new file mode 100644
index 000000000..ce052bd88
--- /dev/null
+++ b/devtools/client/shared/test/browser_tableWidget_keyboard_interaction.js
@@ -0,0 +1,194 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that keyboard interaction works fine with the table widget
+
+"use strict";
+
+const TEST_URI = "data:text/xml;charset=UTF-8,<?xml version='1.0'?>" +
+ "<?xml-stylesheet href='chrome://global/skin/global.css'?>" +
+
+ // Uncomment these lines to help with visual debugging. When uncommented they
+ // dump a couple of thousand errors in the log (bug 1258285)
+ // "<?xml-stylesheet href='chrome://devtools/skin/light-theme.css'?>" +
+ // "<?xml-stylesheet href='chrome://devtools/skin/widgets.css'?>" +
+
+ "<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'" +
+ " title='Table Widget' width='600' height='500'>" +
+ "<box flex='1' class='theme-light'/></window>";
+const TEST_OPT = "chrome,titlebar,toolbar,centerscreen,resizable,dialog=no";
+
+const {TableWidget} = require("devtools/client/shared/widgets/TableWidget");
+
+var doc, table;
+
+function test() {
+ waitForExplicitFinish();
+ let win = Services.ww.openWindow(null, TEST_URI, "_blank", TEST_OPT, null);
+
+ win.addEventListener("load", function onLoad() {
+ win.removeEventListener("load", onLoad, false);
+
+ waitForFocus(function () {
+ doc = win.document;
+ table = new TableWidget(doc.querySelector("box"), {
+ initialColumns: {
+ col1: "Column 1",
+ col2: "Column 2",
+ col3: "Column 3",
+ col4: "Column 4"
+ },
+ uniqueId: "col1",
+ emptyText: "This is dummy empty text",
+ highlightUpdated: true,
+ removableColumns: true,
+ });
+ startTests();
+ });
+ });
+}
+
+function endTests() {
+ table.destroy();
+ doc.defaultView.close();
+ doc = table = null;
+ finish();
+}
+
+var startTests = Task.async(function* () {
+ populateTable();
+ yield testKeyboardInteraction();
+ endTests();
+});
+
+function populateTable() {
+ table.push({
+ col1: "id1",
+ col2: "value10",
+ col3: "value20",
+ col4: "value30"
+ });
+ table.push({
+ col1: "id2",
+ col2: "value14",
+ col3: "value29",
+ col4: "value32"
+ });
+ table.push({
+ col1: "id3",
+ col2: "value17",
+ col3: "value21",
+ col4: "value31",
+ extraData: "foobar",
+ extraData2: 42
+ });
+ table.push({
+ col1: "id4",
+ col2: "value12",
+ col3: "value26",
+ col4: "value33"
+ });
+ table.push({
+ col1: "id5",
+ col2: "value19",
+ col3: "value26",
+ col4: "value37"
+ });
+ table.push({
+ col1: "id6",
+ col2: "value15",
+ col3: "value25",
+ col4: "value37"
+ });
+ table.push({
+ col1: "id7",
+ col2: "value18",
+ col3: "value21",
+ col4: "value36",
+ somethingExtra: "Hello World!"
+ });
+ table.push({
+ col1: "id8",
+ col2: "value11",
+ col3: "value27",
+ col4: "value34"
+ });
+ table.push({
+ col1: "id9",
+ col2: "value11",
+ col3: "value23",
+ col4: "value38"
+ });
+}
+
+// Sends a click event on the passed DOM node in an async manner
+function click(node, button = 0) {
+ if (button == 0) {
+ executeSoon(() => EventUtils.synthesizeMouseAtCenter(node, {},
+ doc.defaultView));
+ } else {
+ executeSoon(() => EventUtils.synthesizeMouseAtCenter(node, {
+ button: button,
+ type: "contextmenu"
+ }, doc.defaultView));
+ }
+}
+
+function getNodeByValue(value) {
+ return table.tbody.querySelector("[value=" + value + "]");
+}
+
+/**
+ * Tests if pressing navigation keys on the table items does the expected
+ * behavior.
+ */
+var testKeyboardInteraction = Task.async(function* () {
+ info("Testing keyboard interaction with the table");
+ info("clicking on the row containing id2");
+ let node = getNodeByValue("id2");
+ let event = table.once(TableWidget.EVENTS.ROW_SELECTED);
+ click(node);
+ yield event;
+
+ yield testRow("id3", "DOWN", "next row");
+ yield testRow("id4", "DOWN", "next row");
+ yield testRow("id3", "UP", "previous row");
+ yield testRow("id4", "DOWN", "next row");
+ yield testRow("id5", "DOWN", "next row");
+ yield testRow("id6", "DOWN", "next row");
+ yield testRow("id5", "UP", "previous row");
+ yield testRow("id4", "UP", "previous row");
+ yield testRow("id3", "UP", "previous row");
+
+ // selecting last item node to test edge navigation cycling case
+ table.selectedRow = "id9";
+
+ // pressing down on last row should move to first row.
+ yield testRow("id1", "DOWN", "first row");
+
+ // pressing up now should move to last row.
+ yield testRow("id9", "UP", "last row");
+});
+
+function* testRow(id, key, destination) {
+ let node = getNodeByValue(id);
+ // node should not have selected class
+ ok(!node.classList.contains("theme-selected"),
+ "Row should not have selected class");
+ info(`Pressing ${key} to select ${destination}`);
+
+ let event = table.once(TableWidget.EVENTS.ROW_SELECTED);
+ EventUtils.sendKey(key, doc.defaultView);
+
+ let uniqueId = yield event;
+ is(id, uniqueId, `Correct row was selected after pressing ${key}`);
+
+ ok(node.classList.contains("theme-selected"), "row has selected class");
+
+ let nodes = doc.querySelectorAll(".theme-selected");
+ for (let i = 0; i < nodes.length; i++) {
+ is(nodes[i].getAttribute("data-id"), id,
+ "Correct cell selected in all columns");
+ }
+}
diff --git a/devtools/client/shared/test/browser_tableWidget_mouse_interaction.js b/devtools/client/shared/test/browser_tableWidget_mouse_interaction.js
new file mode 100644
index 000000000..4d7de94e3
--- /dev/null
+++ b/devtools/client/shared/test/browser_tableWidget_mouse_interaction.js
@@ -0,0 +1,317 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that mosue interaction works fine with the table widget
+
+"use strict";
+
+const TEST_URI = "data:text/xml;charset=UTF-8,<?xml version='1.0'?>" +
+ "<?xml-stylesheet href='chrome://global/skin/global.css'?>" +
+
+ // Uncomment these lines to help with visual debugging. When uncommented they
+ // dump a couple of thousand errors in the log (bug 1258285)
+ // "<?xml-stylesheet href='chrome://devtools/skin/light-theme.css'?>" +
+ // "<?xml-stylesheet href='chrome://devtools/skin/widgets.css'?>" +
+
+ "<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'" +
+ " title='Table Widget' width='600' height='500'>" +
+ "<box flex='1' class='theme-light'/></window>";
+const TEST_OPT = "chrome,titlebar,toolbar,centerscreen,resizable,dialog=no";
+
+const {TableWidget} = require("devtools/client/shared/widgets/TableWidget");
+
+var doc, table;
+
+function test() {
+ waitForExplicitFinish();
+ let win = Services.ww.openWindow(null, TEST_URI, "_blank", TEST_OPT, null);
+
+ win.addEventListener("load", function onLoad() {
+ win.removeEventListener("load", onLoad, false);
+
+ waitForFocus(function () {
+ doc = win.document;
+ table = new TableWidget(doc.querySelector("box"), {
+ initialColumns: {
+ col1: "Column 1",
+ col2: "Column 2",
+ col3: "Column 3",
+ col4: "Column 4"
+ },
+ uniqueId: "col1",
+ emptyText: "This is dummy empty text",
+ highlightUpdated: true,
+ removableColumns: true,
+ wrapTextInElements: true,
+ });
+ startTests();
+ });
+ });
+}
+
+function endTests() {
+ table.destroy();
+ doc.defaultView.close();
+ doc = table = null;
+ finish();
+}
+
+var startTests = Task.async(function* () {
+ populateTable();
+ yield testMouseInteraction();
+ endTests();
+});
+
+function populateTable() {
+ table.push({
+ col1: "id1",
+ col2: "value10",
+ col3: "value20",
+ col4: "value30"
+ });
+ table.push({
+ col1: "id2",
+ col2: "value14",
+ col3: "value29",
+ col4: "value32"
+ });
+ table.push({
+ col1: "id3",
+ col2: "value17",
+ col3: "value21",
+ col4: "value31",
+ extraData: "foobar",
+ extraData2: 42
+ });
+ table.push({
+ col1: "id4",
+ col2: "value12",
+ col3: "value26",
+ col4: "value33"
+ });
+ table.push({
+ col1: "id5",
+ col2: "value19",
+ col3: "value26",
+ col4: "value37"
+ });
+ table.push({
+ col1: "id6",
+ col2: "value15",
+ col3: "value25",
+ col4: "value37"
+ });
+ table.push({
+ col1: "id7",
+ col2: "value18",
+ col3: "value21",
+ col4: "value36",
+ somethingExtra: "Hello World!"
+ });
+ table.push({
+ col1: "id8",
+ col2: "value11",
+ col3: "value27",
+ col4: "value34"
+ });
+ table.push({
+ col1: "id9",
+ col2: "value11",
+ col3: "value23",
+ col4: "value38"
+ });
+}
+
+// Sends a click event on the passed DOM node in an async manner
+function click(node, button = 0) {
+ if (button == 0) {
+ executeSoon(() => EventUtils.synthesizeMouseAtCenter(node, {},
+ doc.defaultView));
+ } else {
+ executeSoon(() => EventUtils.synthesizeMouseAtCenter(node, {
+ button: button,
+ type: "contextmenu"
+ }, doc.defaultView));
+ }
+}
+
+/**
+ * Tests if clicking the table items does the expected behavior
+ */
+var testMouseInteraction = Task.async(function* () {
+ info("Testing mouse interaction with the table");
+ ok(!table.selectedRow, "Nothing should be selected beforehand");
+
+ let event = table.once(TableWidget.EVENTS.ROW_SELECTED);
+ let firstColumnFirstRowCell = table.tbody.firstChild.firstChild.children[1];
+ info("clicking on the first row");
+ ok(!firstColumnFirstRowCell.classList.contains("theme-selected"),
+ "Node should not have selected class before clicking");
+ click(firstColumnFirstRowCell);
+ let id = yield event;
+ ok(firstColumnFirstRowCell.classList.contains("theme-selected"),
+ "Node has selected class after click");
+ is(id, "id1", "Correct row was selected");
+
+ info("clicking on second row to select it");
+ event = table.once(TableWidget.EVENTS.ROW_SELECTED);
+ let firstColumnSecondRowCell = table.tbody.firstChild.firstChild.children[2];
+ // node should not have selected class
+ ok(!firstColumnSecondRowCell.classList.contains("theme-selected"),
+ "New node should not have selected class before clicking");
+ click(firstColumnSecondRowCell);
+ id = yield event;
+ ok(firstColumnSecondRowCell.classList.contains("theme-selected"),
+ "New node has selected class after clicking");
+ is(id, "id2", "Correct table path is emitted for new node");
+ isnot(firstColumnFirstRowCell, firstColumnSecondRowCell,
+ "Old and new node are different");
+ ok(!firstColumnFirstRowCell.classList.contains("theme-selected"),
+ "Old node should not have selected class after the click on new node");
+
+ info("clicking on the third row cell content to select third row");
+ event = table.once(TableWidget.EVENTS.ROW_SELECTED);
+ let firstColumnThirdRowCell = table.tbody.firstChild.firstChild.children[3];
+ let firstColumnThirdRowCellInnerNode = firstColumnThirdRowCell.querySelector("span");
+ // node should not have selected class
+ ok(!firstColumnThirdRowCell.classList.contains("theme-selected"),
+ "New node should not have selected class before clicking");
+ click(firstColumnThirdRowCellInnerNode);
+ id = yield event;
+ ok(firstColumnThirdRowCell.classList.contains("theme-selected"),
+ "New node has selected class after clicking the cell content");
+ is(id, "id3", "Correct table path is emitted for new node");
+
+ // clicking on table header to sort by it
+ event = table.once(TableWidget.EVENTS.COLUMN_SORTED);
+ let node = table.tbody.children[6].firstChild.children[0];
+ info("clicking on the 4th coulmn header to sort the table by it");
+ ok(!node.hasAttribute("sorted"),
+ "Node should not have sorted attribute before clicking");
+ ok(doc.querySelector("[sorted]"),
+ "Although, something else should be sorted on");
+ isnot(doc.querySelector("[sorted]"), node, "Which is not equal to this node");
+ click(node);
+ id = yield event;
+ is(id, "col4", "Correct column was sorted on");
+ ok(node.hasAttribute("sorted"),
+ "Node should now have sorted attribute after clicking");
+ is(doc.querySelectorAll("[sorted]").length, 1,
+ "Now only one column should be sorted on");
+ is(doc.querySelector("[sorted]"), node, "Which should be this column");
+
+ // test context menu opening.
+ // hiding second column
+ // event listener for popupshown
+ info("right click on the first column header");
+ node = table.tbody.firstChild.firstChild.firstChild;
+ let onPopupShown = once(table.menupopup, "popupshown");
+ click(node, 2);
+ yield onPopupShown;
+
+ is(table.menupopup.querySelectorAll("[disabled]").length, 1,
+ "Only 1 menuitem is disabled");
+ is(table.menupopup.querySelector("[disabled]"),
+ table.menupopup.querySelector("[data-id='col1']"),
+ "Which is the unique column");
+ // popup should be open now
+ // clicking on second column label
+ let onPopupHidden = once(table.menupopup, "popuphidden");
+ event = table.once(TableWidget.EVENTS.HEADER_CONTEXT_MENU);
+ node = table.menupopup.querySelector("[data-id='col2']");
+ info("selecting to hide the second column");
+ ok(!table.tbody.children[2].hasAttribute("hidden"),
+ "Column is not hidden before hiding it");
+ click(node);
+ id = yield event;
+ yield onPopupHidden;
+ is(id, "col2", "Correct column was triggered to be hidden");
+ is(table.tbody.children[2].getAttribute("hidden"), "true",
+ "Column is hidden after hiding it");
+
+ // hiding third column
+ // event listener for popupshown
+ info("right clicking on the first column header");
+ node = table.tbody.firstChild.firstChild.firstChild;
+ onPopupShown = once(table.menupopup, "popupshown");
+ click(node, 2);
+ yield onPopupShown;
+
+ is(table.menupopup.querySelectorAll("[disabled]").length, 1,
+ "Only 1 menuitem is disabled");
+ // popup should be open now
+ // clicking on second column label
+ onPopupHidden = once(table.menupopup, "popuphidden");
+ event = table.once(TableWidget.EVENTS.HEADER_CONTEXT_MENU);
+ node = table.menupopup.querySelector("[data-id='col3']");
+ info("selecting to hide the second column");
+ ok(!table.tbody.children[4].hasAttribute("hidden"),
+ "Column is not hidden before hiding it");
+ click(node);
+ id = yield event;
+ yield onPopupHidden;
+ is(id, "col3", "Correct column was triggered to be hidden");
+ is(table.tbody.children[4].getAttribute("hidden"), "true",
+ "Column is hidden after hiding it");
+
+ // opening again to see if 2 items are disabled now
+ // event listener for popupshown
+ info("right clicking on the first column header");
+ node = table.tbody.firstChild.firstChild.firstChild;
+ onPopupShown = once(table.menupopup, "popupshown");
+ click(node, 2);
+ yield onPopupShown;
+
+ is(table.menupopup.querySelectorAll("[disabled]").length, 2,
+ "2 menuitems are disabled now as only 2 columns remain visible");
+ is(table.menupopup.querySelectorAll("[disabled]")[0],
+ table.menupopup.querySelector("[data-id='col1']"),
+ "First is the unique column");
+ is(table.menupopup.querySelectorAll("[disabled]")[1],
+ table.menupopup.querySelector("[data-id='col4']"),
+ "Second is the last column");
+
+ // showing back 2nd column
+ // popup should be open now
+ // clicking on second column label
+ onPopupHidden = once(table.menupopup, "popuphidden");
+ event = table.once(TableWidget.EVENTS.HEADER_CONTEXT_MENU);
+ node = table.menupopup.querySelector("[data-id='col2']");
+ info("selecting to hide the second column");
+ is(table.tbody.children[2].getAttribute("hidden"), "true",
+ "Column is hidden before unhiding it");
+ click(node);
+ id = yield event;
+ yield onPopupHidden;
+ is(id, "col2", "Correct column was triggered to be hidden");
+ ok(!table.tbody.children[2].hasAttribute("hidden"),
+ "Column is not hidden after unhiding it");
+
+ // showing back 3rd column
+ // event listener for popupshown
+ info("right clicking on the first column header");
+ node = table.tbody.firstChild.firstChild.firstChild;
+ onPopupShown = once(table.menupopup, "popupshown");
+ click(node, 2);
+ yield onPopupShown;
+
+ // popup should be open now
+ // clicking on second column label
+ onPopupHidden = once(table.menupopup, "popuphidden");
+ event = table.once(TableWidget.EVENTS.HEADER_CONTEXT_MENU);
+ node = table.menupopup.querySelector("[data-id='col3']");
+ info("selecting to hide the second column");
+ is(table.tbody.children[4].getAttribute("hidden"), "true",
+ "Column is hidden before unhiding it");
+ click(node);
+ id = yield event;
+ yield onPopupHidden;
+ is(id, "col3", "Correct column was triggered to be hidden");
+ ok(!table.tbody.children[4].hasAttribute("hidden"),
+ "Column is not hidden after unhiding it");
+
+ // reset table state
+ table.clearSelection();
+ table.sortBy("col1");
+});
diff --git a/devtools/client/shared/test/browser_telemetry_button_eyedropper.js b/devtools/client/shared/test/browser_telemetry_button_eyedropper.js
new file mode 100644
index 000000000..76546ce83
--- /dev/null
+++ b/devtools/client/shared/test/browser_telemetry_button_eyedropper.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<p>browser_telemetry_button_eyedropper.js</p><div>test</div>";
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ let toolbox = yield gDevTools.showToolbox(target, "inspector");
+ info("inspector opened");
+
+ info("testing the eyedropper button");
+ yield testButton(toolbox, Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ yield gDevTools.closeToolbox(target);
+ gBrowser.removeCurrentTab();
+});
+
+function* testButton(toolbox, Telemetry) {
+ info("Calling the eyedropper button's callback");
+ // We call the button callback directly because we don't need to test the UI here, we're
+ // only concerned about testing the telemetry probe.
+ yield toolbox.getPanel("inspector").showEyeDropper();
+
+ checkResults("_EYEDROPPER_", Telemetry);
+}
+
+function checkResults(histIdFocus, Telemetry) {
+ let result = Telemetry.prototype.telemetryInfo;
+
+ for (let [histId, value] of Object.entries(result)) {
+ if (histId.startsWith("DEVTOOLS_INSPECTOR_") ||
+ !histId.includes(histIdFocus)) {
+ // Inspector stats are tested in
+ // browser_telemetry_toolboxtabs_{toolname}.js so we skip them here
+ // because we only open the inspector once for this test.
+ continue;
+ }
+
+ if (histId.endsWith("OPENED_COUNT")) {
+ is(value.length, 1, histId + " has one entry");
+
+ let okay = value.every(element => element === true);
+ ok(okay, "All " + histId + " entries are === true");
+ }
+ }
+}
diff --git a/devtools/client/shared/test/browser_telemetry_button_paintflashing.js b/devtools/client/shared/test/browser_telemetry_button_paintflashing.js
new file mode 100644
index 000000000..dcce8f738
--- /dev/null
+++ b/devtools/client/shared/test/browser_telemetry_button_paintflashing.js
@@ -0,0 +1,89 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<p>browser_telemetry_button_paintflashing.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ let toolbox = yield gDevTools.showToolbox(target, "inspector");
+ info("inspector opened");
+
+ info("testing the paintflashing button");
+ yield testButton(toolbox, Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ yield gDevTools.closeToolbox(target);
+ gBrowser.removeCurrentTab();
+});
+
+function* testButton(toolbox, Telemetry) {
+ info("Testing command-button-paintflashing");
+
+ let button = toolbox.doc.querySelector("#command-button-paintflashing");
+ ok(button, "Captain, we have the button");
+
+ yield* delayedClicks(toolbox, button, 4);
+ checkResults("_PAINTFLASHING_", Telemetry);
+}
+
+function* delayedClicks(toolbox, node, clicks) {
+ for (let i = 0; i < clicks; i++) {
+ yield new Promise(resolve => {
+ // See TOOL_DELAY for why we need setTimeout here
+ setTimeout(() => resolve(), TOOL_DELAY);
+ });
+
+ // this event will fire once the command execution starts and
+ // the output object is created
+ let clicked = toolbox._requisition.commandOutputManager.onOutput.once();
+
+ info("Clicking button " + node.id);
+ node.click();
+
+ let outputEvent = yield clicked;
+ // promise gets resolved once execution finishes and output is ready
+ yield outputEvent.output.promise;
+ }
+}
+
+function checkResults(histIdFocus, Telemetry) {
+ let result = Telemetry.prototype.telemetryInfo;
+
+ for (let [histId, value] of Object.entries(result)) {
+ if (histId.startsWith("DEVTOOLS_INSPECTOR_") ||
+ !histId.includes(histIdFocus)) {
+ // Inspector stats are tested in
+ // browser_telemetry_toolboxtabs_{toolname}.js so we skip them here
+ // because we only open the inspector once for this test.
+ continue;
+ }
+
+ if (histId.endsWith("OPENED_COUNT")) {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function (element) {
+ return element === true;
+ });
+
+ ok(okay, "All " + histId + " entries are === true");
+ } else if (histId.endsWith("TIME_ACTIVE_SECONDS")) {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function (element) {
+ return element > 0;
+ });
+
+ ok(okay, "All " + histId + " entries have time > 0");
+ }
+ }
+}
diff --git a/devtools/client/shared/test/browser_telemetry_button_responsive.js b/devtools/client/shared/test/browser_telemetry_button_responsive.js
new file mode 100644
index 000000000..41e53f0f8
--- /dev/null
+++ b/devtools/client/shared/test/browser_telemetry_button_responsive.js
@@ -0,0 +1,95 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<p>browser_telemetry_button_responsive.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+const { ResponsiveUIManager } = Cu.import("resource://devtools/client/responsivedesign/responsivedesign.jsm", {});
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ let toolbox = yield gDevTools.showToolbox(target, "inspector");
+ info("inspector opened");
+
+ info("testing the responsivedesign button");
+ yield testButton(toolbox, Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ yield gDevTools.closeToolbox(target);
+ gBrowser.removeCurrentTab();
+});
+
+function* testButton(toolbox, Telemetry) {
+ info("Testing command-button-responsive");
+
+ let button = toolbox.doc.querySelector("#command-button-responsive");
+ ok(button, "Captain, we have the button");
+
+ yield delayedClicks(button, 4);
+
+ checkResults("_RESPONSIVE_", Telemetry);
+}
+
+function waitForToggle() {
+ return new Promise(resolve => {
+ let handler = () => {
+ ResponsiveUIManager.off("on", handler);
+ ResponsiveUIManager.off("off", handler);
+ resolve();
+ };
+ ResponsiveUIManager.on("on", handler);
+ ResponsiveUIManager.on("off", handler);
+ });
+}
+
+var delayedClicks = Task.async(function* (node, clicks) {
+ for (let i = 0; i < clicks; i++) {
+ info("Clicking button " + node.id);
+ let toggled = waitForToggle();
+ node.click();
+ yield toggled;
+ // See TOOL_DELAY for why we need setTimeout here
+ yield DevToolsUtils.waitForTime(TOOL_DELAY);
+ }
+});
+
+function checkResults(histIdFocus, Telemetry) {
+ let result = Telemetry.prototype.telemetryInfo;
+
+ for (let [histId, value] of Object.entries(result)) {
+ if (histId.startsWith("DEVTOOLS_INSPECTOR_") ||
+ !histId.includes(histIdFocus)) {
+ // Inspector stats are tested in
+ // browser_telemetry_toolboxtabs_{toolname}.js so we skip them here
+ // because we only open the inspector once for this test.
+ continue;
+ }
+
+ if (histId.endsWith("OPENED_COUNT")) {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function (element) {
+ return element === true;
+ });
+
+ ok(okay, "All " + histId + " entries are === true");
+ } else if (histId.endsWith("TIME_ACTIVE_SECONDS")) {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function (element) {
+ return element > 0;
+ });
+
+ ok(okay, "All " + histId + " entries have time > 0");
+ }
+ }
+}
diff --git a/devtools/client/shared/test/browser_telemetry_button_scratchpad.js b/devtools/client/shared/test/browser_telemetry_button_scratchpad.js
new file mode 100644
index 000000000..e191bb257
--- /dev/null
+++ b/devtools/client/shared/test/browser_telemetry_button_scratchpad.js
@@ -0,0 +1,127 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<p>browser_telemetry_button_scratchpad.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ let toolbox = yield gDevTools.showToolbox(target, "inspector");
+ info("inspector opened");
+
+ let onAllWindowsOpened = trackScratchpadWindows();
+
+ info("testing the scratchpad button");
+ yield testButton(toolbox, Telemetry);
+ yield onAllWindowsOpened;
+
+ checkResults("_SCRATCHPAD_", Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ yield gDevTools.closeToolbox(target);
+ gBrowser.removeCurrentTab();
+});
+
+function trackScratchpadWindows() {
+ info("register the window observer to track when scratchpad windows open");
+
+ let numScratchpads = 0;
+
+ return new Promise(resolve => {
+ Services.ww.registerNotification(function observer(subject, topic) {
+ if (topic == "domwindowopened") {
+ let win = subject.QueryInterface(Ci.nsIDOMWindow);
+ win.addEventListener("load", function onLoad() {
+ win.removeEventListener("load", onLoad, false);
+
+ if (win.Scratchpad) {
+ win.Scratchpad.addObserver({
+ onReady: function () {
+ win.Scratchpad.removeObserver(this);
+ numScratchpads++;
+ win.close();
+
+ info("another scratchpad was opened and closed, " +
+ `count is now ${numScratchpads}`);
+
+ if (numScratchpads === 4) {
+ Services.ww.unregisterNotification(observer);
+ info("4 scratchpads have been opened and closed, checking results");
+ resolve();
+ }
+ },
+ });
+ }
+ }, false);
+ }
+ });
+ });
+}
+
+function* testButton(toolbox, Telemetry) {
+ info("Testing command-button-scratchpad");
+ let button = toolbox.doc.querySelector("#command-button-scratchpad");
+ ok(button, "Captain, we have the button");
+
+ yield delayedClicks(button, 4);
+}
+
+function delayedClicks(node, clicks) {
+ return new Promise(resolve => {
+ let clicked = 0;
+
+ // See TOOL_DELAY for why we need setTimeout here
+ setTimeout(function delayedClick() {
+ info("Clicking button " + node.id);
+ node.click();
+ clicked++;
+
+ if (clicked >= clicks) {
+ resolve(node);
+ } else {
+ setTimeout(delayedClick, TOOL_DELAY);
+ }
+ }, TOOL_DELAY);
+ });
+}
+
+function checkResults(histIdFocus, Telemetry) {
+ let result = Telemetry.prototype.telemetryInfo;
+
+ for (let [histId, value] of Object.entries(result)) {
+ if (histId.startsWith("DEVTOOLS_INSPECTOR_") ||
+ !histId.includes(histIdFocus)) {
+ // Inspector stats are tested in
+ // browser_telemetry_toolboxtabs_{toolname}.js so we skip them here
+ // because we only open the inspector once for this test.
+ continue;
+ }
+
+ if (histId.endsWith("OPENED_COUNT")) {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function (element) {
+ return element === true;
+ });
+
+ ok(okay, "All " + histId + " entries are === true");
+ } else if (histId.endsWith("TIME_ACTIVE_SECONDS")) {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function (element) {
+ return element > 0;
+ });
+
+ ok(okay, "All " + histId + " entries have time > 0");
+ }
+ }
+}
diff --git a/devtools/client/shared/test/browser_telemetry_sidebar.js b/devtools/client/shared/test/browser_telemetry_sidebar.js
new file mode 100644
index 000000000..8a8f35578
--- /dev/null
+++ b/devtools/client/shared/test/browser_telemetry_sidebar.js
@@ -0,0 +1,84 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8,<p>browser_telemetry_sidebar.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ let toolbox = yield gDevTools.showToolbox(target, "inspector");
+ info("inspector opened");
+
+ yield testSidebar(toolbox);
+ checkResults(Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ yield gDevTools.closeToolbox(target);
+ gBrowser.removeCurrentTab();
+});
+
+function* testSidebar(toolbox) {
+ info("Testing sidebar");
+
+ let inspector = toolbox.getCurrentPanel();
+ let sidebarTools = ["ruleview", "computedview", "fontinspector",
+ "animationinspector"];
+
+ // Concatenate the array with itself so that we can open each tool twice.
+ sidebarTools.push.apply(sidebarTools, sidebarTools);
+
+ return new Promise(resolve => {
+ // See TOOL_DELAY for why we need setTimeout here
+ setTimeout(function selectSidebarTab() {
+ let tool = sidebarTools.pop();
+ if (tool) {
+ inspector.sidebar.select(tool);
+ setTimeout(function () {
+ setTimeout(selectSidebarTab, TOOL_DELAY);
+ }, TOOL_DELAY);
+ } else {
+ resolve();
+ }
+ }, TOOL_DELAY);
+ });
+}
+
+function checkResults(Telemetry) {
+ let result = Telemetry.prototype.telemetryInfo;
+
+ for (let [histId, value] of Object.entries(result)) {
+ if (histId.startsWith("DEVTOOLS_INSPECTOR_")) {
+ // Inspector stats are tested in browser_telemetry_toolboxtabs.js so we
+ // skip them here because we only open the inspector once for this test.
+ continue;
+ }
+
+ if (histId === "DEVTOOLS_TOOLBOX_OPENED_COUNT") {
+ is(value.length, 1, histId + " has only one entry");
+ } else if (histId.endsWith("OPENED_COUNT")) {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function (element) {
+ return element === true;
+ });
+
+ ok(okay, "All " + histId + " entries are === true");
+ } else if (histId.endsWith("TIME_ACTIVE_SECONDS")) {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function (element) {
+ return element > 0;
+ });
+
+ ok(okay, "All " + histId + " entries have time > 0");
+ }
+ }
+}
diff --git a/devtools/client/shared/test/browser_telemetry_toolbox.js b/devtools/client/shared/test/browser_telemetry_toolbox.js
new file mode 100644
index 000000000..85328cf14
--- /dev/null
+++ b/devtools/client/shared/test/browser_telemetry_toolbox.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<p>browser_telemetry_toolbox.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ yield openAndCloseToolbox(3, TOOL_DELAY, "inspector");
+ checkTelemetryResults(Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/client/shared/test/browser_telemetry_toolboxtabs_canvasdebugger.js b/devtools/client/shared/test/browser_telemetry_toolboxtabs_canvasdebugger.js
new file mode 100644
index 000000000..81ab9470c
--- /dev/null
+++ b/devtools/client/shared/test/browser_telemetry_toolboxtabs_canvasdebugger.js
@@ -0,0 +1,29 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<p>browser_telemetry_toolboxtabs_canvasdebugger.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+add_task(function* () {
+ info("Activate the canvasdebugger");
+ let originalPref = Services.prefs.getBoolPref("devtools.canvasdebugger.enabled");
+ Services.prefs.setBoolPref("devtools.canvasdebugger.enabled", true);
+
+ yield addTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ yield openAndCloseToolbox(2, TOOL_DELAY, "canvasdebugger");
+ checkTelemetryResults(Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ gBrowser.removeCurrentTab();
+
+ info("De-activate the canvasdebugger");
+ Services.prefs.setBoolPref("devtools.canvasdebugger.enabled", originalPref);
+});
diff --git a/devtools/client/shared/test/browser_telemetry_toolboxtabs_inspector.js b/devtools/client/shared/test/browser_telemetry_toolboxtabs_inspector.js
new file mode 100644
index 000000000..a50c8d203
--- /dev/null
+++ b/devtools/client/shared/test/browser_telemetry_toolboxtabs_inspector.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<p>browser_telemetry_toolboxtabs_inspector.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ yield openAndCloseToolbox(2, TOOL_DELAY, "inspector");
+ checkTelemetryResults(Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/client/shared/test/browser_telemetry_toolboxtabs_jsdebugger.js b/devtools/client/shared/test/browser_telemetry_toolboxtabs_jsdebugger.js
new file mode 100644
index 000000000..ba1c26643
--- /dev/null
+++ b/devtools/client/shared/test/browser_telemetry_toolboxtabs_jsdebugger.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<p>browser_telemetry_toolboxtabs_jsdebugger.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ yield openAndCloseToolbox(2, TOOL_DELAY, "jsdebugger");
+ checkTelemetryResults(Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/client/shared/test/browser_telemetry_toolboxtabs_jsprofiler.js b/devtools/client/shared/test/browser_telemetry_toolboxtabs_jsprofiler.js
new file mode 100644
index 000000000..0a5dfb048
--- /dev/null
+++ b/devtools/client/shared/test/browser_telemetry_toolboxtabs_jsprofiler.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<p>browser_telemetry_toolboxtabs_jsprofiler.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ yield openAndCloseToolbox(2, TOOL_DELAY, "performance");
+ checkTelemetryResults(Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/client/shared/test/browser_telemetry_toolboxtabs_netmonitor.js b/devtools/client/shared/test/browser_telemetry_toolboxtabs_netmonitor.js
new file mode 100644
index 000000000..6d5292b11
--- /dev/null
+++ b/devtools/client/shared/test/browser_telemetry_toolboxtabs_netmonitor.js
@@ -0,0 +1,23 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<p>browser_telemetry_toolboxtabs_netmonitor.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ yield openAndCloseToolbox(2, TOOL_DELAY, "netmonitor");
+ checkTelemetryResults(Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ gBrowser.removeCurrentTab();
+});
+
diff --git a/devtools/client/shared/test/browser_telemetry_toolboxtabs_options.js b/devtools/client/shared/test/browser_telemetry_toolboxtabs_options.js
new file mode 100644
index 000000000..26b3bf77c
--- /dev/null
+++ b/devtools/client/shared/test/browser_telemetry_toolboxtabs_options.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<p>browser_telemetry_toolboxtabs_options.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ yield openAndCloseToolbox(2, TOOL_DELAY, "options");
+ checkTelemetryResults(Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/client/shared/test/browser_telemetry_toolboxtabs_shadereditor.js b/devtools/client/shared/test/browser_telemetry_toolboxtabs_shadereditor.js
new file mode 100644
index 000000000..5cf1eb0a6
--- /dev/null
+++ b/devtools/client/shared/test/browser_telemetry_toolboxtabs_shadereditor.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+//
+// Whitelisting this test.
+// As part of bug 1077403, the leaking uncaught rejection should be fixed.
+//
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed(
+ "Error: Shader Editor is still waiting for a WebGL context to be created.");
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<p>browser_telemetry_toolboxtabs_shadereditor.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+const TOOL_PREF = "devtools.shadereditor.enabled";
+
+add_task(function* () {
+ info("Active the sharer editor");
+ let originalPref = Services.prefs.getBoolPref(TOOL_PREF);
+ Services.prefs.setBoolPref(TOOL_PREF, true);
+
+ yield addTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ yield openAndCloseToolbox(2, TOOL_DELAY, "shadereditor");
+ checkTelemetryResults(Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ gBrowser.removeCurrentTab();
+
+ info("De-activate the sharer editor");
+ Services.prefs.setBoolPref(TOOL_PREF, originalPref);
+});
diff --git a/devtools/client/shared/test/browser_telemetry_toolboxtabs_storage.js b/devtools/client/shared/test/browser_telemetry_toolboxtabs_storage.js
new file mode 100644
index 000000000..838b06fcb
--- /dev/null
+++ b/devtools/client/shared/test/browser_telemetry_toolboxtabs_storage.js
@@ -0,0 +1,28 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<p>browser_telemetry_toolboxtabs_storage.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 1000;
+
+add_task(function* () {
+ info("Activating the storage inspector");
+ Services.prefs.setBoolPref("devtools.storage.enabled", true);
+
+ yield addTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ yield openAndCloseToolbox(2, TOOL_DELAY, "storage");
+ checkTelemetryResults(Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ gBrowser.removeCurrentTab();
+
+ info("De-activating the storage inspector");
+ Services.prefs.clearUserPref("devtools.storage.enabled");
+});
diff --git a/devtools/client/shared/test/browser_telemetry_toolboxtabs_styleeditor.js b/devtools/client/shared/test/browser_telemetry_toolboxtabs_styleeditor.js
new file mode 100644
index 000000000..cdd9e3fb3
--- /dev/null
+++ b/devtools/client/shared/test/browser_telemetry_toolboxtabs_styleeditor.js
@@ -0,0 +1,23 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<p>browser_telemetry_toolboxtabs_styleeditor.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ yield openAndCloseToolbox(2, TOOL_DELAY, "styleeditor");
+ checkTelemetryResults(Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ gBrowser.removeCurrentTab();
+});
+
diff --git a/devtools/client/shared/test/browser_telemetry_toolboxtabs_webaudioeditor.js b/devtools/client/shared/test/browser_telemetry_toolboxtabs_webaudioeditor.js
new file mode 100644
index 000000000..a75ebad1d
--- /dev/null
+++ b/devtools/client/shared/test/browser_telemetry_toolboxtabs_webaudioeditor.js
@@ -0,0 +1,29 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<p>browser_telemetry_toolboxtabs_webaudioeditor.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+add_task(function* () {
+ info("Activating the webaudioeditor");
+ let originalPref = Services.prefs.getBoolPref("devtools.webaudioeditor.enabled");
+ Services.prefs.setBoolPref("devtools.webaudioeditor.enabled", true);
+
+ yield addTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ yield openAndCloseToolbox(2, TOOL_DELAY, "webaudioeditor");
+ checkTelemetryResults(Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ gBrowser.removeCurrentTab();
+
+ info("De-activating the webaudioeditor");
+ Services.prefs.setBoolPref("devtools.webaudioeditor.enabled", originalPref);
+});
diff --git a/devtools/client/shared/test/browser_telemetry_toolboxtabs_webconsole.js b/devtools/client/shared/test/browser_telemetry_toolboxtabs_webconsole.js
new file mode 100644
index 000000000..4e15dbf4b
--- /dev/null
+++ b/devtools/client/shared/test/browser_telemetry_toolboxtabs_webconsole.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<p>browser_telemetry_toolboxtabs_styleeditor_webconsole.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ yield openAndCloseToolbox(2, TOOL_DELAY, "webconsole");
+ checkTelemetryResults(Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/client/shared/test/browser_templater_basic.html b/devtools/client/shared/test/browser_templater_basic.html
new file mode 100644
index 000000000..473c731f3
--- /dev/null
+++ b/devtools/client/shared/test/browser_templater_basic.html
@@ -0,0 +1,13 @@
+<!doctype html>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<html>
+<head>
+ <title>DOM Template Tests</title>
+</head>
+<body>
+
+</body>
+</html>
+
diff --git a/devtools/client/shared/test/browser_templater_basic.js b/devtools/client/shared/test/browser_templater_basic.js
new file mode 100644
index 000000000..256900cf5
--- /dev/null
+++ b/devtools/client/shared/test/browser_templater_basic.js
@@ -0,0 +1,286 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the DOM Template engine works properly
+
+/*
+ * These tests run both in Mozilla/Mochitest and plain browsers (as does
+ * domtemplate)
+ * We should endevour to keep the source in sync.
+ */
+
+const {template} = require("devtools/shared/gcli/templater");
+
+const TEST_URI = TEST_URI_ROOT + "browser_templater_basic.html";
+
+var test = Task.async(function* () {
+ yield addTab("about:blank");
+ let [host,, doc] = yield createHost("bottom", TEST_URI);
+
+ info("Starting DOM Templater Tests");
+ runTest(0, host, doc);
+});
+
+function runTest(index, host, doc) {
+ let options = tests[index] = tests[index]();
+ let holder = doc.createElement("div");
+ holder.id = options.name;
+ let body = doc.body;
+ body.appendChild(holder);
+ holder.innerHTML = options.template;
+
+ info("Running " + options.name);
+ template(holder, options.data, options.options);
+
+ if (typeof options.result == "string") {
+ is(holder.innerHTML, options.result, options.name);
+ } else {
+ ok(holder.innerHTML.match(options.result) != null,
+ options.name + " result='" + holder.innerHTML + "'");
+ }
+
+ if (options.also) {
+ options.also(options);
+ }
+
+ function runNextTest() {
+ index++;
+ if (index < tests.length) {
+ runTest(index, host, doc);
+ } else {
+ finished(host);
+ }
+ }
+
+ if (options.later) {
+ let ais = is.bind(this);
+
+ function createTester(testHolder, testOptions) {
+ return () => {
+ ais(testHolder.innerHTML, testOptions.later, testOptions.name + " later");
+ runNextTest();
+ };
+ }
+
+ executeSoon(createTester(holder, options));
+ } else {
+ runNextTest();
+ }
+}
+
+function finished(host) {
+ host.destroy();
+ gBrowser.removeCurrentTab();
+ info("Finishing DOM Templater Tests");
+ tests = null;
+ finish();
+}
+
+/**
+ * Why have an array of functions that return data rather than just an array
+ * of the data itself? Some of these tests contain calls to delayReply() which
+ * sets up async processing using executeSoon(). Since the execution of these
+ * tests is asynchronous, the delayed reply will probably arrive before the
+ * test is executed, making the test be synchronous. So we wrap the data in a
+ * function so we only set it up just before we use it.
+ */
+var tests = [
+ () => ({
+ name: "simpleNesting",
+ template: '<div id="ex1">${nested.value}</div>',
+ data: { nested: { value: "pass 1" } },
+ result: '<div id="ex1">pass 1</div>'
+ }),
+
+ () => ({
+ name: "returnDom",
+ template: '<div id="ex2">${__element.ownerDocument.createTextNode(\'pass 2\')}</div>',
+ options: { allowEval: true },
+ data: {},
+ result: '<div id="ex2">pass 2</div>'
+ }),
+
+ () => ({
+ name: "srcChange",
+ template: '<img _src="${fred}" id="ex3">',
+ data: { fred: "green.png" },
+ result: /<img( id="ex3")? src="green.png"( id="ex3")?>/
+ }),
+
+ () => ({
+ name: "ifTrue",
+ template: '<p if="${name !== \'jim\'}">hello ${name}</p>',
+ options: { allowEval: true },
+ data: { name: "fred" },
+ result: "<p>hello fred</p>"
+ }),
+
+ () => ({
+ name: "ifFalse",
+ template: '<p if="${name !== \'jim\'}">hello ${name}</p>',
+ options: { allowEval: true },
+ data: { name: "jim" },
+ result: ""
+ }),
+
+ () => ({
+ name: "simpleLoop",
+ template: '<p foreach="index in ${[ 1, 2, 3 ]}">${index}</p>',
+ options: { allowEval: true },
+ data: {},
+ result: "<p>1</p><p>2</p><p>3</p>"
+ }),
+
+ () => ({
+ name: "loopElement",
+ template: '<loop foreach="i in ${array}">${i}</loop>',
+ data: { array: [ 1, 2, 3 ] },
+ result: "123"
+ }),
+
+ // Bug 692028: DOMTemplate memory leak with asynchronous arrays
+ // Bug 692031: DOMTemplate async loops do not drop the loop element
+ () => ({
+ name: "asyncLoopElement",
+ template: '<loop foreach="i in ${array}">${i}</loop>',
+ data: { array: delayReply([1, 2, 3]) },
+ result: "<span></span>",
+ later: "123"
+ }),
+
+ () => ({
+ name: "saveElement",
+ template: '<p save="${element}">${name}</p>',
+ data: { name: "pass 8" },
+ result: "<p>pass 8</p>",
+ also: function (options) {
+ ok(options.data.element.innerHTML, "pass 9", "saveElement saved");
+ delete options.data.element;
+ }
+ }),
+
+ () => ({
+ name: "useElement",
+ template: '<p id="pass9">${adjust(__element)}</p>',
+ options: { allowEval: true },
+ data: {
+ adjust: function (element) {
+ is("pass9", element.id, "useElement adjust");
+ return "pass 9b";
+ }
+ },
+ result: '<p id="pass9">pass 9b</p>'
+ }),
+
+ () => ({
+ name: "asyncInline",
+ template: "${delayed}",
+ data: { delayed: delayReply("inline") },
+ result: "<span></span>",
+ later: "inline"
+ }),
+
+ // Bug 692028: DOMTemplate memory leak with asynchronous arrays
+ () => ({
+ name: "asyncArray",
+ template: '<p foreach="i in ${delayed}">${i}</p>',
+ data: { delayed: delayReply([1, 2, 3]) },
+ result: "<span></span>",
+ later: "<p>1</p><p>2</p><p>3</p>"
+ }),
+
+ () => ({
+ name: "asyncMember",
+ template: '<p foreach="i in ${delayed}">${i}</p>',
+ data: { delayed: [delayReply(4), delayReply(5), delayReply(6)] },
+ result: "<span></span><span></span><span></span>",
+ later: "<p>4</p><p>5</p><p>6</p>"
+ }),
+
+ // Bug 692028: DOMTemplate memory leak with asynchronous arrays
+ () => ({
+ name: "asyncBoth",
+ template: '<p foreach="i in ${delayed}">${i}</p>',
+ data: {
+ delayed: delayReply([
+ delayReply(4),
+ delayReply(5),
+ delayReply(6)
+ ])
+ },
+ result: "<span></span>",
+ later: "<p>4</p><p>5</p><p>6</p>"
+ }),
+
+ // Bug 701762: DOMTemplate fails when ${foo()} returns undefined
+ () => ({
+ name: "functionReturningUndefiend",
+ template: "<p>${foo()}</p>",
+ options: { allowEval: true },
+ data: {
+ foo: function () {}
+ },
+ result: "<p>undefined</p>"
+ }),
+
+ // Bug 702642: DOMTemplate is relatively slow when evaluating JS ${}
+ () => ({
+ name: "propertySimple",
+ template: "<p>${a.b.c}</p>",
+ data: { a: { b: { c: "hello" } } },
+ result: "<p>hello</p>"
+ }),
+
+ () => ({
+ name: "propertyPass",
+ template: "<p>${Math.max(1, 2)}</p>",
+ options: { allowEval: true },
+ result: "<p>2</p>"
+ }),
+
+ () => ({
+ name: "propertyFail",
+ template: "<p>${Math.max(1, 2)}</p>",
+ result: "<p>${Math.max(1, 2)}</p>"
+ }),
+
+ // Bug 723431: DOMTemplate should allow customisation of display of
+ // null/undefined values
+ () => ({
+ name: "propertyUndefAttrFull",
+ template: "<p>${nullvar}|${undefinedvar1}|${undefinedvar2}</p>",
+ data: { nullvar: null, undefinedvar1: undefined },
+ result: "<p>null|undefined|undefined</p>"
+ }),
+
+ () => ({
+ name: "propertyUndefAttrBlank",
+ template: "<p>${nullvar}|${undefinedvar1}|${undefinedvar2}</p>",
+ data: { nullvar: null, undefinedvar1: undefined },
+ options: { blankNullUndefined: true },
+ result: "<p>||</p>"
+ }),
+
+ /* eslint-disable max-len */
+ () => ({
+ name: "propertyUndefAttrFull",
+ template: '<div><p value="${nullvar}"></p><p value="${undefinedvar1}"></p><p value="${undefinedvar2}"></p></div>',
+ data: { nullvar: null, undefinedvar1: undefined },
+ result: '<div><p value="null"></p><p value="undefined"></p><p value="undefined"></p></div>'
+ }),
+
+ () => ({
+ name: "propertyUndefAttrBlank",
+ template: '<div><p value="${nullvar}"></p><p value="${undefinedvar1}"></p><p value="${undefinedvar2}"></p></div>',
+ data: { nullvar: null, undefinedvar1: undefined },
+ options: { blankNullUndefined: true },
+ result: '<div><p value=""></p><p value=""></p><p value=""></p></div>'
+ })
+ /* eslint-enable max-len */
+];
+
+function delayReply(data) {
+ return new Promise(resolve => resolve(data));
+}
diff --git a/devtools/client/shared/test/browser_theme.js b/devtools/client/shared/test/browser_theme.js
new file mode 100644
index 000000000..174e5aeec
--- /dev/null
+++ b/devtools/client/shared/test/browser_theme.js
@@ -0,0 +1,98 @@
+/* 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";
+
+// Tests that theme utilities work
+
+const {getColor, getTheme, setTheme} = require("devtools/client/shared/theme");
+
+add_task(function* () {
+ testGetTheme();
+ testSetTheme();
+ testGetColor();
+ testColorExistence();
+});
+
+function testGetTheme() {
+ let originalTheme = getTheme();
+ ok(originalTheme, "has some theme to start with.");
+ Services.prefs.setCharPref("devtools.theme", "light");
+ is(getTheme(), "light", "getTheme() correctly returns light theme");
+ Services.prefs.setCharPref("devtools.theme", "dark");
+ is(getTheme(), "dark", "getTheme() correctly returns dark theme");
+ Services.prefs.setCharPref("devtools.theme", "firebug");
+ is(getTheme(), "firebug", "getTheme() correctly returns firebug theme");
+ Services.prefs.setCharPref("devtools.theme", "unknown");
+ is(getTheme(), "unknown", "getTheme() correctly returns an unknown theme");
+ Services.prefs.setCharPref("devtools.theme", originalTheme);
+}
+
+function testSetTheme() {
+ let originalTheme = getTheme();
+ gDevTools.once("pref-changed", (_, { pref, oldValue, newValue }) => {
+ is(pref, "devtools.theme",
+ "The 'pref-changed' event triggered by setTheme has correct pref.");
+ is(oldValue, originalTheme,
+ "The 'pref-changed' event triggered by setTheme has correct oldValue.");
+ is(newValue, "dark",
+ "The 'pref-changed' event triggered by setTheme has correct newValue.");
+ });
+ setTheme("dark");
+ is(Services.prefs.getCharPref("devtools.theme"), "dark",
+ "setTheme() correctly sets dark theme.");
+ setTheme("light");
+ is(Services.prefs.getCharPref("devtools.theme"), "light",
+ "setTheme() correctly sets light theme.");
+ setTheme("firebug");
+ is(Services.prefs.getCharPref("devtools.theme"), "firebug",
+ "setTheme() correctly sets firebug theme.");
+ setTheme("unknown");
+ is(Services.prefs.getCharPref("devtools.theme"), "unknown",
+ "setTheme() correctly sets an unknown theme.");
+ Services.prefs.setCharPref("devtools.theme", originalTheme);
+}
+
+function testGetColor() {
+ let BLUE_DARK = "#46afe3";
+ let BLUE_LIGHT = "#0088cc";
+ let BLUE_FIREBUG = "#3455db";
+ let originalTheme = getTheme();
+
+ setTheme("dark");
+ is(getColor("highlight-blue"), BLUE_DARK, "correctly gets color for enabled theme.");
+ setTheme("light");
+ is(getColor("highlight-blue"), BLUE_LIGHT, "correctly gets color for enabled theme.");
+ setTheme("firebug");
+ is(getColor("highlight-blue"), BLUE_FIREBUG, "correctly gets color for enabled theme.");
+ setTheme("metal");
+ is(getColor("highlight-blue"), BLUE_LIGHT,
+ "correctly uses light for default theme if enabled theme not found");
+
+ is(getColor("highlight-blue", "dark"), BLUE_DARK,
+ "if provided and found, uses the provided theme.");
+ is(getColor("highlight-blue", "firebug"), BLUE_FIREBUG,
+ "if provided and found, uses the provided theme.");
+ is(getColor("highlight-blue", "metal"), BLUE_LIGHT,
+ "if provided and not found, defaults to light theme.");
+ is(getColor("somecomponents"), null, "if a type cannot be found, should return null.");
+
+ setTheme(originalTheme);
+}
+
+function testColorExistence() {
+ const vars = ["body-background", "sidebar-background", "contrast-background",
+ "tab-toolbar-background", "toolbar-background", "selection-background",
+ "selection-color", "selection-background-semitransparent", "splitter-color", "comment",
+ "body-color", "body-color-alt", "content-color1", "content-color2", "content-color3",
+ "highlight-green", "highlight-blue", "highlight-bluegrey", "highlight-purple",
+ "highlight-lightorange", "highlight-orange", "highlight-red", "highlight-pink"
+ ];
+
+ for (let type of vars) {
+ ok(getColor(type, "light"), `${type} is a valid color in light theme`);
+ ok(getColor(type, "dark"), `${type} is a valid color in light theme`);
+ ok(getColor(type, "firebug"), `${type} is a valid color in light theme`);
+ }
+}
diff --git a/devtools/client/shared/test/browser_theme_switching.js b/devtools/client/shared/test/browser_theme_switching.js
new file mode 100644
index 000000000..392462a67
--- /dev/null
+++ b/devtools/client/shared/test/browser_theme_switching.js
@@ -0,0 +1,53 @@
+/* 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";
+
+add_task(function* () {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ let toolbox = yield gDevTools.showToolbox(target);
+ let doc = toolbox.doc;
+ let root = doc.documentElement;
+
+ let platform = root.getAttribute("platform");
+ let expectedPlatform = getPlatform();
+ is(platform, expectedPlatform, ":root[platform] is correct");
+
+ let theme = Services.prefs.getCharPref("devtools.theme");
+ let className = "theme-" + theme;
+ ok(root.classList.contains(className),
+ ":root has " + className + " class (current theme)");
+
+ // Convert the xpath result into an array of strings
+ // like `href="{URL}" type="text/css"`
+ let sheetsIterator = doc.evaluate("processing-instruction('xml-stylesheet')",
+ doc, null, XPathResult.ANY_TYPE, null);
+ let sheetsInDOM = [];
+
+ /* eslint-disable no-cond-assign */
+ let sheet;
+ while (sheet = sheetsIterator.iterateNext()) {
+ sheetsInDOM.push(sheet.data);
+ }
+ /* eslint-enable no-cond-assign */
+
+ let sheetsFromTheme = gDevTools.getThemeDefinition(theme).stylesheets;
+ info("Checking for existence of " + sheetsInDOM.length + " sheets");
+ for (let themeSheet of sheetsFromTheme) {
+ ok(sheetsInDOM.some(s => s.includes(themeSheet)),
+ "There is a stylesheet for " + themeSheet);
+ }
+
+ yield toolbox.destroy();
+});
+
+function getPlatform() {
+ let {OS} = Services.appinfo;
+ if (OS == "WINNT") {
+ return "win";
+ } else if (OS == "Darwin") {
+ return "mac";
+ }
+ return "linux";
+}
diff --git a/devtools/client/shared/test/browser_toolbar_basic.html b/devtools/client/shared/test/browser_toolbar_basic.html
new file mode 100644
index 000000000..2ea3773b0
--- /dev/null
+++ b/devtools/client/shared/test/browser_toolbar_basic.html
@@ -0,0 +1,40 @@
+<!doctype html>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>Developer Toolbar Tests</title>
+ <style type="text/css">
+ #single { color: red; }
+ </style>
+ <script type="text/javascript">
+ /* eslint-disable */
+ var a = 1;
+ </script>
+</head>
+<body>
+
+<p id=single>
+1
+</p>
+
+<p class=twin>
+2a
+</p>
+
+<p class=twin>
+2b
+</p>
+
+<style>
+.twin { color: blue; }
+</style>
+<script>
+/* eslint-disable */
+var b = 2;
+</script>
+
+</body>
+</html>
diff --git a/devtools/client/shared/test/browser_toolbar_basic.js b/devtools/client/shared/test/browser_toolbar_basic.js
new file mode 100644
index 000000000..12da27ab1
--- /dev/null
+++ b/devtools/client/shared/test/browser_toolbar_basic.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the developer toolbar works properly
+
+const TEST_URI = TEST_URI_ROOT + "browser_toolbar_basic.html";
+
+add_task(function* () {
+ info("Starting browser_toolbar_basic.js");
+ yield addTab(TEST_URI);
+
+ ok(!DeveloperToolbar.visible, "DeveloperToolbar is not visible in to start");
+
+ let shown = oneTimeObserve(DeveloperToolbar.NOTIFICATIONS.SHOW);
+ document.getElementById("menu_devToolbar").doCommand();
+ yield shown;
+ ok(DeveloperToolbar.visible, "DeveloperToolbar is visible in checkOpen");
+
+ let close = document.getElementById("developer-toolbar-closebutton");
+ ok(close, "Close button exists");
+
+ let toggleToolbox =
+ document.getElementById("menu_devToolbox");
+ ok(!isChecked(toggleToolbox), "toggle toolbox button is not checked");
+
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ yield gDevTools.showToolbox(target, "inspector");
+ ok(isChecked(toggleToolbox), "toggle toolbox button is checked");
+
+ yield addTab("about:blank");
+ info("Opened a new tab");
+
+ ok(!isChecked(toggleToolbox), "toggle toolbox button is not checked");
+
+ gBrowser.removeCurrentTab();
+
+ let hidden = oneTimeObserve(DeveloperToolbar.NOTIFICATIONS.HIDE);
+ document.getElementById("menu_devToolbar").doCommand();
+ yield hidden;
+ ok(!DeveloperToolbar.visible, "DeveloperToolbar is not visible in hidden");
+
+ shown = oneTimeObserve(DeveloperToolbar.NOTIFICATIONS.SHOW);
+ document.getElementById("menu_devToolbar").doCommand();
+ yield shown;
+ ok(DeveloperToolbar.visible, "DeveloperToolbar is visible in after open");
+
+ ok(isChecked(toggleToolbox), "toggle toolbox button is checked");
+
+ hidden = oneTimeObserve(DeveloperToolbar.NOTIFICATIONS.HIDE);
+ document.getElementById("developer-toolbar-closebutton").doCommand();
+ yield hidden;
+
+ ok(!DeveloperToolbar.visible, "DeveloperToolbar is not visible after re-close");
+});
+
+function isChecked(b) {
+ return b.getAttribute("checked") == "true";
+}
diff --git a/devtools/client/shared/test/browser_toolbar_tooltip.js b/devtools/client/shared/test/browser_toolbar_tooltip.js
new file mode 100644
index 000000000..bc09f705c
--- /dev/null
+++ b/devtools/client/shared/test/browser_toolbar_tooltip.js
@@ -0,0 +1,111 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the developer toolbar works properly
+
+//
+// Whitelisting this test.
+// As part of bug 1077403, the leaking uncaught rejection should be fixed.
+//
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed(
+ "Protocol error (unknownError): Error: Got an invalid root window in DocumentWalker"
+);
+
+const TEST_URI = "data:text/html;charset=utf-8,<p>Tooltip Tests</p>";
+const PREF_DEVTOOLS_THEME = "devtools.theme";
+
+registerCleanupFunction(() => {
+ // Set preferences back to their original values
+ Services.prefs.clearUserPref(PREF_DEVTOOLS_THEME);
+});
+
+add_task(function* showToolbar() {
+ yield addTab(TEST_URI);
+
+ info("Starting browser_toolbar_tooltip.js");
+
+ ok(!DeveloperToolbar.visible, "DeveloperToolbar is not visible in runTest");
+
+ let showPromise = observeOnce(DeveloperToolbar.NOTIFICATIONS.SHOW);
+ document.getElementById("menu_devToolbar").doCommand();
+ yield showPromise;
+});
+
+add_task(function* testDimensions() {
+ let tooltipPanel = DeveloperToolbar.tooltipPanel;
+
+ DeveloperToolbar.focusManager.helpRequest();
+ yield DeveloperToolbar.inputter.setInput("help help");
+
+ DeveloperToolbar.inputter.setCursor({ start: "help help".length });
+ is(tooltipPanel._dimensions.start, "help ".length,
+ "search param start, when cursor at end");
+ ok(getLeftMargin() > 30, "tooltip offset, when cursor at end");
+
+ DeveloperToolbar.inputter.setCursor({ start: "help".length });
+ is(tooltipPanel._dimensions.start, 0,
+ "search param start, when cursor at end of command");
+ ok(getLeftMargin() > 9, "tooltip offset, when cursor at end of command");
+
+ DeveloperToolbar.inputter.setCursor({ start: "help help".length - 1 });
+ is(tooltipPanel._dimensions.start, "help ".length,
+ "search param start, when cursor at penultimate position");
+ ok(getLeftMargin() > 30, "tooltip offset, when cursor at penultimate position");
+
+ DeveloperToolbar.inputter.setCursor({ start: 0 });
+ is(tooltipPanel._dimensions.start, 0,
+ "search param start, when cursor at start");
+ ok(getLeftMargin() > 9, "tooltip offset, when cursor at start");
+});
+
+add_task(function* testThemes() {
+ let tooltipPanel = DeveloperToolbar.tooltipPanel;
+ ok(tooltipPanel.document, "Tooltip panel is initialized");
+
+ Services.prefs.setCharPref(PREF_DEVTOOLS_THEME, "dark");
+
+ yield DeveloperToolbar.inputter.setInput("");
+ yield DeveloperToolbar.inputter.setInput("help help");
+ is(tooltipPanel.document.documentElement.getAttribute("devtoolstheme"),
+ "dark", "Tooltip panel has correct theme");
+
+ Services.prefs.setCharPref(PREF_DEVTOOLS_THEME, "light");
+
+ yield DeveloperToolbar.inputter.setInput("");
+ yield DeveloperToolbar.inputter.setInput("help help");
+ is(tooltipPanel.document.documentElement.getAttribute("devtoolstheme"),
+ "light", "Tooltip panel has correct theme");
+});
+
+add_task(function* hideToolbar() {
+ info("Ending browser_toolbar_tooltip.js");
+ yield DeveloperToolbar.inputter.setInput("");
+
+ ok(DeveloperToolbar.visible, "DeveloperToolbar is visible in hideToolbar");
+
+ info("Hide toolbar");
+ let hidePromise = observeOnce(DeveloperToolbar.NOTIFICATIONS.HIDE);
+ document.getElementById("menu_devToolbar").doCommand();
+ yield hidePromise;
+
+ ok(!DeveloperToolbar.visible, "DeveloperToolbar is not visible in hideToolbar");
+
+ info("Done test");
+});
+
+function getLeftMargin() {
+ let style = DeveloperToolbar.tooltipPanel._panel.style.marginLeft;
+ return parseInt(style.slice(0, -2), 10);
+}
+
+function observeOnce(topic, ownsWeak = false) {
+ return new Promise(function (resolve, reject) {
+ let resolver = function (subject) {
+ Services.obs.removeObserver(resolver, topic);
+ resolve(subject);
+ };
+ Services.obs.addObserver(resolver, topic, ownsWeak);
+ });
+}
diff --git a/devtools/client/shared/test/browser_toolbar_webconsole_errors_count.html b/devtools/client/shared/test/browser_toolbar_webconsole_errors_count.html
new file mode 100644
index 000000000..d09902af0
--- /dev/null
+++ b/devtools/client/shared/test/browser_toolbar_webconsole_errors_count.html
@@ -0,0 +1,33 @@
+<!DOCTYPE HTML>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>Developer Toolbar Tests - errors count in the Web Console button</title>
+ <script type="text/javascript">
+ "use strict";
+ console.log("foobarBug762996consoleLog");
+ window.onload = function () {
+ window.foobarBug762996load();
+ };
+ window.foobarBug762996a();
+ </script>
+ <script type="text/javascript">
+ window.foobarBug762996b();
+ </script>
+</head>
+<body>
+ <p>Hello world! Test for errors count in the Web Console button (developer
+ toolbar).</p>
+ <p style="color: foobarBug762996css"><button>click me</button></p>
+ <script type="text/javascript;version=1.8">
+ "use strict";
+ let testObj = {};
+ document.querySelector("button").onclick = function() {
+ let test = testObj.fooBug788445 + "warning";
+ window.foobarBug762996click();
+ };
+ </script>
+</body>
+</html>
diff --git a/devtools/client/shared/test/browser_toolbar_webconsole_errors_count.js b/devtools/client/shared/test/browser_toolbar_webconsole_errors_count.js
new file mode 100644
index 000000000..c61666585
--- /dev/null
+++ b/devtools/client/shared/test/browser_toolbar_webconsole_errors_count.js
@@ -0,0 +1,256 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* eslint-disable mozilla/no-cpows-in-tests */
+
+"use strict";
+
+// Tests that the developer toolbar errors count works properly.
+
+// Use the old webconsole since this is directly accessing old DOM, and
+// the error count isn't reset when pressing the clear button in new one
+// See Bug 1304794.
+Services.prefs.setBoolPref("devtools.webconsole.new-frontend-enabled", false);
+registerCleanupFunction(function* () {
+ Services.prefs.clearUserPref("devtools.webconsole.new-frontend-enabled");
+});
+
+function test() {
+ const TEST_URI = TEST_URI_ROOT + "browser_toolbar_webconsole_errors_count.html";
+
+ let tab1, tab2, webconsole;
+
+ Services.prefs.setBoolPref("javascript.options.strict", true);
+
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("javascript.options.strict");
+ });
+
+ ignoreAllUncaughtExceptions();
+ addTab(TEST_URI).then(openToolbar);
+
+ function openToolbar(tab) {
+ tab1 = tab;
+ ignoreAllUncaughtExceptions(false);
+
+ expectUncaughtException();
+
+ if (!DeveloperToolbar.visible) {
+ DeveloperToolbar.show(true).then(onOpenToolbar);
+ } else {
+ onOpenToolbar();
+ }
+ }
+
+ function onOpenToolbar() {
+ ok(DeveloperToolbar.visible, "DeveloperToolbar is visible");
+ webconsole = document.getElementById("developer-toolbar-toolbox-button");
+
+ waitForButtonUpdate({
+ name: "web console button shows page errors",
+ errors: 3,
+ warnings: 0,
+ callback: addErrors,
+ });
+ }
+
+ function addErrors() {
+ expectUncaughtException();
+
+ waitForFocus(function () {
+ let button = content.document.querySelector("button");
+ executeSoon(function () {
+ EventUtils.synthesizeMouse(button, 3, 2, {}, content);
+ });
+ }, content);
+
+ waitForButtonUpdate({
+ name: "button shows one more error after click in page",
+ errors: 4,
+ warnings: 1,
+ callback: () => {
+ ignoreAllUncaughtExceptions();
+ addTab(TEST_URI).then(onOpenSecondTab);
+ },
+ });
+ }
+
+ function onOpenSecondTab(tab) {
+ tab2 = tab;
+
+ ignoreAllUncaughtExceptions(false);
+ expectUncaughtException();
+
+ waitForButtonUpdate({
+ name: "button shows correct number of errors after new tab is open",
+ errors: 3,
+ warnings: 0,
+ callback: switchToTab1,
+ });
+ }
+
+ function switchToTab1() {
+ gBrowser.selectedTab = tab1;
+ waitForButtonUpdate({
+ name: "button shows the page errors from tab 1",
+ errors: 4,
+ warnings: 1,
+ callback: openWebConsole.bind(null, tab1, onWebConsoleOpen),
+ });
+ }
+
+ function onWebConsoleOpen(hud) {
+ dump("lolz!!\n");
+ waitForValue({
+ name: "web console shows the page errors",
+ validator: function () {
+ let selector = ".message[category=exception][severity=error]";
+ return hud.outputNode.querySelectorAll(selector).length;
+ },
+ value: 4,
+ success: checkConsoleOutput.bind(null, hud),
+ failure: () => {
+ finish();
+ },
+ });
+ }
+
+ function checkConsoleOutput(hud) {
+ let msgs = ["foobarBug762996a", "foobarBug762996b", "foobarBug762996load",
+ "foobarBug762996click", "foobarBug762996consoleLog",
+ "foobarBug762996css", "fooBug788445"];
+ msgs.forEach(function (msg) {
+ isnot(hud.outputNode.textContent.indexOf(msg), -1,
+ msg + " found in the Web Console output");
+ });
+
+ hud.jsterm.clearOutput();
+
+ is(hud.outputNode.textContent.indexOf("foobarBug762996color"), -1,
+ "clearOutput() worked");
+
+ expectUncaughtException();
+ let button = content.document.querySelector("button");
+ EventUtils.synthesizeMouse(button, 2, 2, {}, content);
+
+ waitForButtonUpdate({
+ name: "button shows one more error after another click in page",
+ errors: 5,
+ // warnings are not repeated by the js engine
+ warnings: 1,
+ callback: () => waitForValue(waitForNewError),
+ });
+
+ let waitForNewError = {
+ name: "the Web Console displays the new error",
+ validator: function () {
+ return hud.outputNode.textContent.indexOf("foobarBug762996click") > -1;
+ },
+ success: doClearConsoleButton.bind(null, hud),
+ failure: finish,
+ };
+ }
+
+ function doClearConsoleButton(hud) {
+ let clearButton = hud.ui.rootElement
+ .querySelector(".webconsole-clear-console-button");
+ EventUtils.synthesizeMouse(clearButton, 2, 2, {}, hud.iframeWindow);
+
+ is(hud.outputNode.textContent.indexOf("foobarBug762996click"), -1,
+ "clear console button worked");
+ is(getErrorsCount(), 0, "page errors counter has been reset");
+ let tooltip = getTooltipValues();
+ is(tooltip[1], 0, "page warnings counter has been reset");
+
+ doPageReload(hud);
+ }
+
+ function doPageReload(hud) {
+ tab1.linkedBrowser.addEventListener("load", onReload, true);
+
+ ignoreAllUncaughtExceptions();
+ content.location.reload();
+
+ function onReload() {
+ tab1.linkedBrowser.removeEventListener("load", onReload, true);
+ ignoreAllUncaughtExceptions(false);
+ expectUncaughtException();
+
+ waitForButtonUpdate({
+ name: "the Web Console button count has been reset after page reload",
+ errors: 3,
+ warnings: 0,
+ callback: waitForValue.bind(null, waitForConsoleOutputAfterReload),
+ });
+ }
+
+ let waitForConsoleOutputAfterReload = {
+ name: "the Web Console displays the correct number of errors after reload",
+ validator: function () {
+ let selector = ".message[category=exception][severity=error]";
+ return hud.outputNode.querySelectorAll(selector).length;
+ },
+ value: 3,
+ success: function () {
+ isnot(hud.outputNode.textContent.indexOf("foobarBug762996load"), -1,
+ "foobarBug762996load found in console output after page reload");
+ testEnd();
+ },
+ failure: testEnd,
+ };
+ }
+
+ function testEnd() {
+ document.getElementById("developer-toolbar-closebutton").doCommand();
+ let target1 = TargetFactory.forTab(tab1);
+ gDevTools.closeToolbox(target1).then(() => {
+ gBrowser.removeTab(tab1);
+ gBrowser.removeTab(tab2);
+ finish();
+ });
+ }
+
+ // Utility functions
+
+ function getErrorsCount() {
+ let count = webconsole.getAttribute("error-count");
+ return count ? count : "0";
+ }
+
+ function getTooltipValues() {
+ let matches = webconsole.getAttribute("tooltiptext")
+ .match(/(\d+) errors?, (\d+) warnings?/);
+ return matches ? [matches[1], matches[2]] : [0, 0];
+ }
+
+ function waitForButtonUpdate(options) {
+ function check() {
+ let errors = getErrorsCount();
+ let tooltip = getTooltipValues();
+ let result = errors == options.errors && tooltip[1] == options.warnings;
+ if (result) {
+ ok(true, options.name);
+ is(errors, tooltip[0], "button error-count is the same as in the tooltip");
+
+ // Get out of the toolbar event execution loop.
+ executeSoon(options.callback);
+ }
+ return result;
+ }
+
+ if (!check()) {
+ info("wait for: " + options.name);
+ DeveloperToolbar.on("errors-counter-updated", function onUpdate(event) {
+ if (check()) {
+ DeveloperToolbar.off(event, onUpdate);
+ }
+ });
+ }
+ }
+
+ function openWebConsole(tab, callback) {
+ let target = TargetFactory.forTab(tab);
+ gDevTools.showToolbox(target, "webconsole").then((toolbox) =>
+ callback(toolbox.getCurrentPanel().hud));
+ }
+}
diff --git a/devtools/client/shared/test/browser_treeWidget_basic.js b/devtools/client/shared/test/browser_treeWidget_basic.js
new file mode 100644
index 000000000..b1d7772f7
--- /dev/null
+++ b/devtools/client/shared/test/browser_treeWidget_basic.js
@@ -0,0 +1,267 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the tree widget api works fine
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8,<head>" +
+ "<link rel='stylesheet' type='text/css' href='chrome://devtools/skin/widg" +
+ "ets.css'></head><body><div></div><span></span></body>";
+const {TreeWidget} = require("devtools/client/shared/widgets/TreeWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ let [host,, doc] = yield createHost("bottom", TEST_URI);
+
+ let tree = new TreeWidget(doc.querySelector("div"), {
+ defaultType: "store"
+ });
+
+ populateTree(tree, doc);
+ testTreeItemInsertedCorrectly(tree, doc);
+ testAPI(tree, doc);
+ populateUnsortedTree(tree, doc);
+ testUnsortedTreeItemInsertedCorrectly(tree, doc);
+
+ tree.destroy();
+ host.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+function populateTree(tree, doc) {
+ tree.add([{
+ id: "level1",
+ label: "Level 1"
+ }, {
+ id: "level2-1",
+ label: "Level 2"
+ }, {
+ id: "level3-1",
+ label: "Level 3 - Child 1",
+ type: "dir"
+ }]);
+ tree.add(["level1", "level2-1", {
+ id: "level3-2",
+ label: "Level 3 - Child 2"
+ }]);
+ tree.add(["level1", "level2-1", {
+ id: "level3-3",
+ label: "Level 3 - Child 3"
+ }]);
+ tree.add(["level1", {
+ id: "level2-2",
+ label: "Level 2.1"
+ }, {
+ id: "level3-1",
+ label: "Level 3.1"
+ }]);
+ tree.add([{
+ id: "level1",
+ label: "Level 1"
+ }, {
+ id: "level2",
+ label: "Level 2"
+ }, {
+ id: "level3",
+ label: "Level 3",
+ type: "js"
+ }]);
+ tree.add(["level1.1", "level2", {id: "level3", type: "url"}]);
+}
+
+/**
+ * Test if the nodes are inserted correctly in the tree.
+ */
+function testTreeItemInsertedCorrectly(tree, doc) {
+ is(tree.root.children.children.length, 2,
+ "Number of top level elements match");
+ is(tree.root.children.firstChild.lastChild.children.length, 3,
+ "Number of first second level elements match");
+ is(tree.root.children.lastChild.lastChild.children.length, 1,
+ "Number of second second level elements match");
+
+ ok(tree.root.items.has("level1"), "Level1 top level element exists");
+ is(tree.root.children.firstChild.dataset.id, JSON.stringify(["level1"]),
+ "Data id of first top level element matches");
+ is(tree.root.children.firstChild.firstChild.textContent, "Level 1",
+ "Text content of first top level element matches");
+
+ ok(tree.root.items.has("level1.1"), "Level1.1 top level element exists");
+ is(tree.root.children.firstChild.nextSibling.dataset.id,
+ JSON.stringify(["level1.1"]),
+ "Data id of second top level element matches");
+ is(tree.root.children.firstChild.nextSibling.firstChild.textContent,
+ "level1.1",
+ "Text content of second top level element matches");
+
+ // Adding a new non text item in the tree.
+ let node = doc.createElement("div");
+ node.textContent = "Foo Bar";
+ node.className = "foo bar";
+ tree.add([{
+ id: "level1.2",
+ node: node,
+ attachment: {
+ foo: "bar"
+ }
+ }]);
+
+ is(tree.root.children.children.length, 3,
+ "Number of top level elements match after update");
+ ok(tree.root.items.has("level1.2"), "New level node got added");
+ ok(tree.attachments.has(JSON.stringify(["level1.2"])),
+ "Attachment is present for newly added node");
+ // The item should be added before level1 and level 1.1 as lexical sorting
+ is(tree.root.children.firstChild.dataset.id, JSON.stringify(["level1.2"]),
+ "Data id of last top level element matches");
+ is(tree.root.children.firstChild.firstChild.firstChild, node,
+ "Newly added node is inserted at the right location");
+}
+
+/**
+ * Populate the unsorted tree.
+ */
+function populateUnsortedTree(tree, doc) {
+ tree.sorted = false;
+
+ tree.add([{ id: "g-1", label: "g-1"}]);
+ tree.add(["g-1", { id: "d-2", label: "d-2.1"}]);
+ tree.add(["g-1", { id: "b-2", label: "b-2.2"}]);
+ tree.add(["g-1", { id: "a-2", label: "a-2.3"}]);
+}
+
+/**
+ * Test if the nodes are inserted correctly in the unsorted tree.
+ */
+function testUnsortedTreeItemInsertedCorrectly(tree, doc) {
+ ok(tree.root.items.has("g-1"), "g-1 top level element exists");
+
+ is(tree.root.children.firstChild.lastChild.children.length, 3,
+ "Number of children for g-1 matches");
+ is(tree.root.children.firstChild.dataset.id, JSON.stringify(["g-1"]),
+ "Data id of g-1 matches");
+ is(tree.root.children.firstChild.firstChild.textContent, "g-1",
+ "Text content of g-1 matches");
+ is(tree.root.children.firstChild.lastChild.firstChild.dataset.id,
+ JSON.stringify(["g-1", "d-2"]),
+ "Data id of d-2 matches");
+ is(tree.root.children.firstChild.lastChild.firstChild.textContent, "d-2.1",
+ "Text content of d-2 matches");
+ is(tree.root.children.firstChild.lastChild.firstChild.nextSibling.textContent,
+ "b-2.2", "Text content of b-2 matches");
+ is(tree.root.children.firstChild.lastChild.lastChild.textContent, "a-2.3",
+ "Text content of a-2 matches");
+}
+
+/**
+ * Tests if the API exposed by TreeWidget works properly
+ */
+function testAPI(tree, doc) {
+ info("Testing TreeWidget API");
+ // Check if selectItem and selectedItem setter works as expected
+ // Nothing should be selected beforehand
+ ok(!doc.querySelector(".theme-selected"), "Nothing is selected");
+ tree.selectItem(["level1"]);
+ let node = doc.querySelector(".theme-selected");
+ ok(!!node, "Something got selected");
+ is(node.parentNode.dataset.id, JSON.stringify(["level1"]),
+ "Correct node selected");
+
+ tree.selectItem(["level1", "level2"]);
+ let node2 = doc.querySelector(".theme-selected");
+ ok(!!node2, "Something is still selected");
+ isnot(node, node2, "Newly selected node is different from previous");
+ is(node2.parentNode.dataset.id, JSON.stringify(["level1", "level2"]),
+ "Correct node selected");
+
+ // test if selectedItem getter works
+ is(tree.selectedItem.length, 2, "Correct length of selected item");
+ is(tree.selectedItem[0], "level1", "Correct selected item");
+ is(tree.selectedItem[1], "level2", "Correct selected item");
+
+ // test if isSelected works
+ ok(tree.isSelected(["level1", "level2"]), "isSelected works");
+
+ tree.selectedItem = ["level1"];
+ let node3 = doc.querySelector(".theme-selected");
+ ok(!!node3, "Something is still selected");
+ isnot(node2, node3, "Newly selected node is different from previous");
+ is(node3, node, "First and third selected nodes should be same");
+ is(node3.parentNode.dataset.id, JSON.stringify(["level1"]),
+ "Correct node selected");
+
+ // test if selectedItem getter works
+ is(tree.selectedItem.length, 1, "Correct length of selected item");
+ is(tree.selectedItem[0], "level1", "Correct selected item");
+
+ // test if clear selection works
+ tree.clearSelection();
+ ok(!doc.querySelector(".theme-selected"),
+ "Nothing selected after clear selection call");
+
+ // test if collapseAll/expandAll work
+ ok(doc.querySelectorAll("[expanded]").length > 0,
+ "Some nodes are expanded");
+ tree.collapseAll();
+ is(doc.querySelectorAll("[expanded]").length, 0,
+ "Nothing is expanded after collapseAll call");
+ tree.expandAll();
+ is(doc.querySelectorAll("[expanded]").length, 13,
+ "All tree items expanded after expandAll call");
+
+ // test if selectNextItem and selectPreviousItem work
+ tree.selectedItem = ["level1", "level2"];
+ ok(tree.isSelected(["level1", "level2"]), "Correct item selected");
+ tree.selectNextItem();
+ ok(tree.isSelected(["level1", "level2", "level3"]),
+ "Correct item selected after selectNextItem call");
+
+ tree.selectNextItem();
+ ok(tree.isSelected(["level1", "level2-1"]),
+ "Correct item selected after second selectNextItem call");
+
+ tree.selectNextItem();
+ ok(tree.isSelected(["level1", "level2-1", "level3-1"]),
+ "Correct item selected after third selectNextItem call");
+
+ tree.selectPreviousItem();
+ ok(tree.isSelected(["level1", "level2-1"]),
+ "Correct item selected after selectPreviousItem call");
+
+ tree.selectPreviousItem();
+ ok(tree.isSelected(["level1", "level2", "level3"]),
+ "Correct item selected after second selectPreviousItem call");
+
+ // test if remove works
+ ok(doc.querySelector("[data-id='" +
+ JSON.stringify(["level1", "level2", "level3"]) + "']"),
+ "level1-level2-level3 item exists before removing");
+ tree.remove(["level1", "level2", "level3"]);
+ ok(!doc.querySelector("[data-id='" +
+ JSON.stringify(["level1", "level2", "level3"]) + "']"),
+ "level1-level2-level3 item does not exist after removing");
+ let level2item = doc.querySelector("[data-id='" +
+ JSON.stringify(["level1", "level2"]) + "'] > .tree-widget-item");
+ ok(level2item.hasAttribute("empty"),
+ "level1-level2 item is marked as empty after removing");
+
+ tree.add([{
+ id: "level1",
+ label: "Level 1"
+ }, {
+ id: "level2",
+ label: "Level 2"
+ }, {
+ id: "level3",
+ label: "Level 3",
+ type: "js"
+ }]);
+
+ // test if clearing the tree works
+ is(doc.querySelectorAll("[level='1']").length, 3,
+ "Correct number of top level items before clearing");
+ tree.clear();
+ is(doc.querySelectorAll("[level='1']").length, 0,
+ "No top level item after clearing the tree");
+}
diff --git a/devtools/client/shared/test/browser_treeWidget_keyboard_interaction.js b/devtools/client/shared/test/browser_treeWidget_keyboard_interaction.js
new file mode 100644
index 000000000..9b214fe3f
--- /dev/null
+++ b/devtools/client/shared/test/browser_treeWidget_keyboard_interaction.js
@@ -0,0 +1,228 @@
+/* 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";
+
+// Tests that keyboard interaction works fine with the tree widget
+
+const TEST_URI = "data:text/html;charset=utf-8,<head>" +
+ "<link rel='stylesheet' type='text/css' href='chrome://devtools/skin/widg" +
+ "ets.css'></head><body><div></div><span></span></body>";
+const {TreeWidget} = require("devtools/client/shared/widgets/TreeWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ let [host, win, doc] = yield createHost("bottom", TEST_URI);
+
+ let tree = new TreeWidget(doc.querySelector("div"), {
+ defaultType: "store"
+ });
+
+ populateTree(tree, doc);
+ yield testKeyboardInteraction(tree, win);
+
+ tree.destroy();
+ host.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+function populateTree(tree, doc) {
+ tree.add([{
+ id: "level1",
+ label: "Level 1"
+ }, {
+ id: "level2-1",
+ label: "Level 2"
+ }, {
+ id: "level3-1",
+ label: "Level 3 - Child 1",
+ type: "dir"
+ }]);
+ tree.add(["level1", "level2-1", { id: "level3-2", label: "Level 3 - Child 2"}]);
+ tree.add(["level1", "level2-1", { id: "level3-3", label: "Level 3 - Child 3"}]);
+ tree.add(["level1", {
+ id: "level2-2",
+ label: "Level 2.1"
+ }, {
+ id: "level3-1",
+ label: "Level 3.1"
+ }]);
+ tree.add([{
+ id: "level1",
+ label: "Level 1"
+ }, {
+ id: "level2",
+ label: "Level 2"
+ }, {
+ id: "level3",
+ label: "Level 3",
+ type: "js"
+ }]);
+ tree.add(["level1.1", "level2", {id: "level3", type: "url"}]);
+
+ // Adding a new non text item in the tree.
+ let node = doc.createElement("div");
+ node.textContent = "Foo Bar";
+ node.className = "foo bar";
+ tree.add([{
+ id: "level1.2",
+ node: node,
+ attachment: {
+ foo: "bar"
+ }
+ }]);
+}
+
+// Sends a click event on the passed DOM node in an async manner
+function click(node) {
+ let win = node.ownerDocument.defaultView;
+ executeSoon(() => EventUtils.synthesizeMouseAtCenter(node, {}, win));
+}
+
+/**
+ * Tests if pressing navigation keys on the tree items does the expected behavior
+ */
+function* testKeyboardInteraction(tree, win) {
+ info("Testing keyboard interaction with the tree");
+ let event;
+ let pass = (e, d, a) => event.resolve([e, d, a]);
+
+ info("clicking on first top level item");
+ let node = tree.root.children.firstChild.firstChild;
+ event = defer();
+ tree.once("select", pass);
+ click(node);
+ yield event.promise;
+ node = tree.root.children.firstChild.nextSibling.firstChild;
+ // node should not have selected class
+ ok(!node.classList.contains("theme-selected"), "Node should not have selected class");
+ ok(!node.hasAttribute("expanded"), "Node is not expanded");
+
+ info("Pressing down key to select next item");
+ event = defer();
+ tree.once("select", pass);
+ EventUtils.sendKey("DOWN", win);
+ let [name, data, attachment] = yield event.promise;
+ is(name, "select", "Select event was fired after pressing down");
+ is(data[0], "level1", "Correct item was selected after pressing down");
+ ok(!attachment, "null attachment was emitted");
+ ok(node.classList.contains("theme-selected"), "Node has selected class");
+ ok(node.hasAttribute("expanded"), "Node is expanded now");
+
+ info("Pressing down key again to select next item");
+ event = defer();
+ tree.once("select", pass);
+ EventUtils.sendKey("DOWN", win);
+ [name, data, attachment] = yield event.promise;
+ is(data.length, 2, "Correct level item was selected after second down keypress");
+ is(data[0], "level1", "Correct parent level");
+ is(data[1], "level2", "Correct second level");
+
+ info("Pressing down key again to select next item");
+ event = defer();
+ tree.once("select", pass);
+ EventUtils.sendKey("DOWN", win);
+ [name, data, attachment] = yield event.promise;
+ is(data.length, 3, "Correct level item was selected after third down keypress");
+ is(data[0], "level1", "Correct parent level");
+ is(data[1], "level2", "Correct second level");
+ is(data[2], "level3", "Correct third level");
+
+ info("Pressing down key again to select next item");
+ event = defer();
+ tree.once("select", pass);
+ EventUtils.sendKey("DOWN", win);
+ [name, data, attachment] = yield event.promise;
+ is(data.length, 2, "Correct level item was selected after fourth down keypress");
+ is(data[0], "level1", "Correct parent level");
+ is(data[1], "level2-1", "Correct second level");
+
+ // pressing left to check expand collapse feature.
+ // This does not emit any event, so listening for keypress
+ tree.root.children.addEventListener("keypress", function onClick() {
+ tree.root.children.removeEventListener("keypress", onClick);
+ // executeSoon so that other listeners on the same method are executed first
+ executeSoon(() => event.resolve(null));
+ });
+ info("Pressing left key to collapse the item");
+ event = defer();
+ node = tree._selectedLabel;
+ ok(node.hasAttribute("expanded"), "Item is expanded before left keypress");
+ EventUtils.sendKey("LEFT", win);
+ yield event.promise;
+
+ ok(!node.hasAttribute("expanded"), "Item is not expanded after left keypress");
+
+ // pressing left on collapsed item should select the previous item
+
+ info("Pressing left key on collapsed item to select previous");
+ tree.once("select", pass);
+ event = defer();
+ // parent node should have no effect of this keypress
+ node = tree.root.children.firstChild.nextSibling.firstChild;
+ ok(node.hasAttribute("expanded"), "Parent is expanded");
+ EventUtils.sendKey("LEFT", win);
+ [name, data] = yield event.promise;
+ is(data.length, 3, "Correct level item was selected after second left keypress");
+ is(data[0], "level1", "Correct parent level");
+ is(data[1], "level2", "Correct second level");
+ is(data[2], "level3", "Correct third level");
+ ok(node.hasAttribute("expanded"), "Parent is still expanded after left keypress");
+
+ // pressing down again
+
+ info("Pressing down key to select next item");
+ event = defer();
+ tree.once("select", pass);
+ EventUtils.sendKey("DOWN", win);
+ [name, data, attachment] = yield event.promise;
+ is(data.length, 2, "Correct level item was selected after fifth down keypress");
+ is(data[0], "level1", "Correct parent level");
+ is(data[1], "level2-1", "Correct second level");
+
+ // collapsing the item to check expand feature.
+
+ tree.root.children.addEventListener("keypress", function onClick() {
+ tree.root.children.removeEventListener("keypress", onClick);
+ executeSoon(() => event.resolve(null));
+ });
+ info("Pressing left key to collapse the item");
+ event = defer();
+ node = tree._selectedLabel;
+ ok(node.hasAttribute("expanded"), "Item is expanded before left keypress");
+ EventUtils.sendKey("LEFT", win);
+ yield event.promise;
+ ok(!node.hasAttribute("expanded"), "Item is collapsed after left keypress");
+
+ // pressing right should expand this now.
+
+ tree.root.children.addEventListener("keypress", function onClick() {
+ tree.root.children.removeEventListener("keypress", onClick);
+ executeSoon(() => event.resolve(null));
+ });
+ info("Pressing right key to expend the collapsed item");
+ event = defer();
+ node = tree._selectedLabel;
+ ok(!node.hasAttribute("expanded"), "Item is collapsed before right keypress");
+ EventUtils.sendKey("RIGHT", win);
+ yield event.promise;
+ ok(node.hasAttribute("expanded"), "Item is expanded after right keypress");
+
+ // selecting last item node to test edge navigation case
+
+ tree.selectedItem = ["level1.1", "level2", "level3"];
+ node = tree._selectedLabel;
+ // pressing down again should not change selection
+ event = defer();
+ tree.root.children.addEventListener("keypress", function onClick() {
+ tree.root.children.removeEventListener("keypress", onClick);
+ executeSoon(() => event.resolve(null));
+ });
+ info("Pressing down key on last item of the tree");
+ EventUtils.sendKey("DOWN", win);
+ yield event.promise;
+
+ ok(tree.isSelected(["level1.1", "level2", "level3"]),
+ "Last item is still selected after pressing down on last item of the tree");
+}
diff --git a/devtools/client/shared/test/browser_treeWidget_mouse_interaction.js b/devtools/client/shared/test/browser_treeWidget_mouse_interaction.js
new file mode 100644
index 000000000..f42fd16ad
--- /dev/null
+++ b/devtools/client/shared/test/browser_treeWidget_mouse_interaction.js
@@ -0,0 +1,135 @@
+/* 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";
+
+// Tests that mouse interaction works fine with tree widget
+
+const TEST_URI = "data:text/html;charset=utf-8,<head>" +
+ "<link rel='stylesheet' type='text/css' href='chrome://devtools/skin/widg" +
+ "ets.css'></head><body><div></div><span></span></body>";
+const {TreeWidget} = require("devtools/client/shared/widgets/TreeWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ let [host,, doc] = yield createHost("bottom", TEST_URI);
+
+ let tree = new TreeWidget(doc.querySelector("div"), {
+ defaultType: "store"
+ });
+
+ populateTree(tree, doc);
+ yield testMouseInteraction(tree);
+
+ tree.destroy();
+ host.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+function populateTree(tree, doc) {
+ tree.add([{
+ id: "level1",
+ label: "Level 1"
+ }, {
+ id: "level2-1",
+ label: "Level 2"
+ }, {
+ id: "level3-1",
+ label: "Level 3 - Child 1",
+ type: "dir"
+ }]);
+ tree.add(["level1", "level2-1", { id: "level3-2", label: "Level 3 - Child 2"}]);
+ tree.add(["level1", "level2-1", { id: "level3-3", label: "Level 3 - Child 3"}]);
+ tree.add(["level1", {
+ id: "level2-2",
+ label: "Level 2.1"
+ }, {
+ id: "level3-1",
+ label: "Level 3.1"
+ }]);
+ tree.add([{
+ id: "level1",
+ label: "Level 1"
+ }, {
+ id: "level2",
+ label: "Level 2"
+ }, {
+ id: "level3",
+ label: "Level 3",
+ type: "js"
+ }]);
+ tree.add(["level1.1", "level2", {id: "level3", type: "url"}]);
+
+ // Adding a new non text item in the tree.
+ let node = doc.createElement("div");
+ node.textContent = "Foo Bar";
+ node.className = "foo bar";
+ tree.add([{
+ id: "level1.2",
+ node: node,
+ attachment: {
+ foo: "bar"
+ }
+ }]);
+}
+
+// Sends a click event on the passed DOM node in an async manner
+function click(node) {
+ let win = node.ownerDocument.defaultView;
+ executeSoon(() => EventUtils.synthesizeMouseAtCenter(node, {}, win));
+}
+
+/**
+ * Tests if clicking the tree items does the expected behavior
+ */
+function* testMouseInteraction(tree) {
+ info("Testing mouse interaction with the tree");
+ let event;
+ let pass = (e, d, a) => event.resolve([e, d, a]);
+
+ ok(!tree.selectedItem, "Nothing should be selected beforehand");
+
+ tree.once("select", pass);
+ let node = tree.root.children.firstChild.firstChild;
+ info("clicking on first top level item");
+ event = defer();
+ ok(!node.classList.contains("theme-selected"),
+ "Node should not have selected class before clicking");
+ click(node);
+ let [, data, attachment] = yield event.promise;
+ ok(node.classList.contains("theme-selected"),
+ "Node has selected class after click");
+ is(data[0], "level1.2", "Correct tree path is emitted");
+ ok(attachment && attachment.foo, "Correct attachment is emitted");
+ is(attachment.foo, "bar", "Correct attachment value is emitted");
+
+ info("clicking second top level item with children to check if it expands");
+ let node2 = tree.root.children.firstChild.nextSibling.firstChild;
+ event = defer();
+ // node should not have selected class
+ ok(!node2.classList.contains("theme-selected"),
+ "New node should not have selected class before clicking");
+ ok(!node2.hasAttribute("expanded"), "New node is not expanded before clicking");
+ tree.once("select", pass);
+ click(node2);
+ [, data, attachment] = yield event.promise;
+ ok(node2.classList.contains("theme-selected"),
+ "New node has selected class after clicking");
+ is(data[0], "level1", "Correct tree path is emitted for new node");
+ ok(!attachment, "null attachment should be emitted for new node");
+ ok(node2.hasAttribute("expanded"), "New node expanded after click");
+
+ ok(!node.classList.contains("theme-selected"),
+ "Old node should not have selected class after the click on new node");
+
+ // clicking again should just collapse
+ // this will not emit "select" event
+ event = defer();
+ node2.addEventListener("click", () => {
+ executeSoon(() => event.resolve(null));
+ }, { once: true });
+ click(node2);
+ yield event.promise;
+ ok(!node2.hasAttribute("expanded"), "New node collapsed after click again");
+}
diff --git a/devtools/client/shared/test/doc_options-view.xul b/devtools/client/shared/test/doc_options-view.xul
new file mode 100644
index 000000000..06ae1cbf1
--- /dev/null
+++ b/devtools/client/shared/test/doc_options-view.xul
@@ -0,0 +1,26 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/skin/widgets.css" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/content/shared/widgets/widgets.css" type="text/css"?>
+<!DOCTYPE window []>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <popupset id="options-popupset">
+ <menupopup id="options-menupopup" position="before_end">
+ <menuitem id="option-autoprettyprint"
+ type="checkbox"
+ data-pref="auto-pretty-print"
+ label="pretty print"/>
+ <menuitem id="option-autoblackbox"
+ type="checkbox"
+ data-pref="auto-black-box"
+ label="black box"/>
+ </menupopup>
+ </popupset>
+ <button id="options-button"
+ popup="options-menupopup"/>
+</window>
diff --git a/devtools/client/shared/test/head.js b/devtools/client/shared/test/head.js
new file mode 100644
index 000000000..fbf0e84af
--- /dev/null
+++ b/devtools/client/shared/test/head.js
@@ -0,0 +1,346 @@
+/* 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/. */
+/* eslint no-unused-vars: [2, {"vars": "local", "args": "none"}] */
+/* import-globals-from ../../framework/test/shared-head.js */
+
+"use strict";
+
+// shared-head.js handles imports, constants, and utility functions
+Services.scriptloader.loadSubScript("chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js", this);
+
+const {DOMHelpers} = Cu.import("resource://devtools/client/shared/DOMHelpers.jsm", {});
+const {Hosts} = require("devtools/client/framework/toolbox-hosts");
+
+const TEST_URI_ROOT = "http://example.com/browser/devtools/client/shared/test/";
+const OPTIONS_VIEW_URL = TEST_URI_ROOT + "doc_options-view.xul";
+
+function catchFail(func) {
+ return function () {
+ try {
+ return func.apply(null, arguments);
+ } catch (ex) {
+ ok(false, ex);
+ console.error(ex);
+ finish();
+ throw ex;
+ }
+ };
+}
+
+/**
+ * Polls a given function waiting for the given value.
+ *
+ * @param object options
+ * Options object with the following properties:
+ * - validator
+ * A validator function that should return the expected value. This is
+ * called every few milliseconds to check if the result is the expected
+ * one. When the returned result is the expected one, then the |success|
+ * function is called and polling stops. If |validator| never returns
+ * the expected value, then polling timeouts after several tries and
+ * a failure is recorded - the given |failure| function is invoked.
+ * - success
+ * A function called when the validator function returns the expected
+ * value.
+ * - failure
+ * A function called if the validator function timeouts - fails to return
+ * the expected value in the given time.
+ * - name
+ * Name of test. This is used to generate the success and failure
+ * messages.
+ * - timeout
+ * Timeout for validator function, in milliseconds. Default is 5000 ms.
+ * - value
+ * The expected value. If this option is omitted then the |validator|
+ * function must return a trueish value.
+ * Each of the provided callback functions will receive two arguments:
+ * the |options| object and the last value returned by |validator|.
+ */
+function waitForValue(options) {
+ let start = Date.now();
+ let timeout = options.timeout || 5000;
+ let lastValue;
+
+ function wait(validatorFn, successFn, failureFn) {
+ if ((Date.now() - start) > timeout) {
+ // Log the failure.
+ ok(false, "Timed out while waiting for: " + options.name);
+ let expected = "value" in options ?
+ "'" + options.value + "'" :
+ "a trueish value";
+ info("timeout info :: got '" + lastValue + "', expected " + expected);
+ failureFn(options, lastValue);
+ return;
+ }
+
+ lastValue = validatorFn(options, lastValue);
+ let successful = "value" in options ?
+ lastValue == options.value :
+ lastValue;
+ if (successful) {
+ ok(true, options.name);
+ successFn(options, lastValue);
+ } else {
+ setTimeout(() => {
+ wait(validatorFn, successFn, failureFn);
+ }, 100);
+ }
+ }
+
+ wait(options.validator, options.success, options.failure);
+}
+
+function oneTimeObserve(name, callback) {
+ return new Promise((resolve) => {
+ let func = function () {
+ Services.obs.removeObserver(func, name);
+ if (callback) {
+ callback();
+ }
+ resolve();
+ };
+ Services.obs.addObserver(func, name, false);
+ });
+}
+
+let createHost =
+Task.async(function* (type = "bottom", src = "data:text/html;charset=utf-8,") {
+ let host = new Hosts[type](gBrowser.selectedTab);
+ let iframe = yield host.create();
+
+ yield new Promise(resolve => {
+ let domHelper = new DOMHelpers(iframe.contentWindow);
+ iframe.setAttribute("src", src);
+ domHelper.onceDOMReady(resolve);
+ });
+
+ return [host, iframe.contentWindow, iframe.contentDocument];
+});
+
+/**
+ * Check the correctness of the data recorded in Telemetry after
+ * loadTelemetryAndRecordLogs was called.
+ */
+function checkTelemetryResults(Telemetry) {
+ let result = Telemetry.prototype.telemetryInfo;
+
+ for (let histId in result) {
+ let value = result[histId];
+
+ if (histId.endsWith("OPENED_COUNT")) {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function (element) {
+ return element === true;
+ });
+
+ ok(okay, "All " + histId + " entries are === true");
+ } else if (histId.endsWith("TIME_ACTIVE_SECONDS")) {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function (element) {
+ return element > 0;
+ });
+
+ ok(okay, "All " + histId + " entries have time > 0");
+ }
+ }
+}
+
+/**
+ * Open and close the toolbox in the current browser tab, several times, waiting
+ * some amount of time in between.
+ * @param {Number} nbOfTimes
+ * @param {Number} usageTime in milliseconds
+ * @param {String} toolId
+ */
+function* openAndCloseToolbox(nbOfTimes, usageTime, toolId) {
+ for (let i = 0; i < nbOfTimes; i++) {
+ info("Opening toolbox " + (i + 1));
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ yield gDevTools.showToolbox(target, toolId);
+
+ // We use a timeout to check the toolbox's active time
+ yield new Promise(resolve => setTimeout(resolve, usageTime));
+
+ info("Closing toolbox " + (i + 1));
+ yield gDevTools.closeToolbox(target);
+ }
+}
+
+/**
+ * Synthesize a profile for testing.
+ */
+function synthesizeProfileForTest(samples) {
+ const RecordingUtils = require("devtools/shared/performance/recording-utils");
+
+ samples.unshift({
+ time: 0,
+ frames: []
+ });
+
+ let uniqueStacks = new RecordingUtils.UniqueStacks();
+ return RecordingUtils.deflateThread({
+ samples: samples,
+ markers: []
+ }, uniqueStacks);
+}
+
+/**
+ * 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);
+ });
+}
+
+/**
+ * Show the presets list sidebar in the cssfilter widget popup
+ * @param {CSSFilterWidget} widget
+ * @return {Promise}
+ */
+function showFilterPopupPresets(widget) {
+ let onRender = widget.once("render");
+ widget._togglePresets();
+ return onRender;
+}
+
+/**
+ * Show presets list and create a sample preset with the name and value provided
+ * @param {CSSFilterWidget} widget
+ * @param {string} name
+ * @param {string} value
+ * @return {Promise}
+ */
+let showFilterPopupPresetsAndCreatePreset =
+Task.async(function* (widget, name, value) {
+ yield showFilterPopupPresets(widget);
+
+ let onRender = widget.once("render");
+ widget.setCssValue(value);
+ yield onRender;
+
+ let footer = widget.el.querySelector(".presets-list .footer");
+ footer.querySelector("input").value = name;
+
+ onRender = widget.once("render");
+ widget._savePreset({
+ preventDefault: () => {}
+ });
+
+ yield onRender;
+});
+
+/**
+ * Utility function for testing CSS code samples that have been
+ * syntax-highlighted.
+ *
+ * The CSS syntax highlighter emits a collection of DOM nodes that have
+ * CSS classes applied to them. This function checks that those nodes
+ * are what we expect.
+ *
+ * @param {array} expectedNodes
+ * A representation of the nodes we expect to see.
+ * Each node is an object containing two properties:
+ * - type: a string which can be one of:
+ * - text, comment, property-name, property-value
+ * - text: the textContent of the node
+ *
+ * For example, given a string like this:
+ * "<comment> The part we want </comment>\n this: is-the-part-we-want;"
+ *
+ * we would represent the expected output like this:
+ * [{type: "comment", text: "<comment> The part we want </comment>"},
+ * {type: "text", text: "\n"},
+ * {type: "property-name", text: "this"},
+ * {type: "text", text: ":"},
+ * {type: "text", text: " "},
+ * {type: "property-value", text: "is-the-part-we-want"},
+ * {type: "text", text: ";"}];
+ *
+ * @param {Node} parent
+ * The DOM node whose children are the output of the syntax highlighter.
+ */
+function checkCssSyntaxHighlighterOutput(expectedNodes, parent) {
+ /**
+ * The classes applied to the output nodes by the syntax highlighter.
+ * These must be same as the definitions in MdnDocsWidget.js.
+ */
+ const PROPERTY_NAME_COLOR = "theme-fg-color5";
+ const PROPERTY_VALUE_COLOR = "theme-fg-color1";
+ const COMMENT_COLOR = "theme-comment";
+
+ /**
+ * Check the type and content of a single node.
+ */
+ function checkNode(expected, actual) {
+ ok(actual.textContent == expected.text,
+ "Check that node has the expected textContent");
+ info("Expected text content: [" + expected.text + "]");
+ info("Actual text content: [" + actual.textContent + "]");
+
+ info("Check that node has the expected type");
+ if (expected.type == "text") {
+ ok(actual.nodeType == 3, "Check that node is a text node");
+ } else {
+ ok(actual.tagName.toUpperCase() == "SPAN", "Check that node is a SPAN");
+ }
+
+ info("Check that node has the expected className");
+
+ let expectedClassName = null;
+ let actualClassName = null;
+
+ switch (expected.type) {
+ case "property-name":
+ expectedClassName = PROPERTY_NAME_COLOR;
+ break;
+ case "property-value":
+ expectedClassName = PROPERTY_VALUE_COLOR;
+ break;
+ case "comment":
+ expectedClassName = COMMENT_COLOR;
+ break;
+ default:
+ ok(!actual.classList, "No className expected");
+ return;
+ }
+
+ ok(actual.classList.length == 1, "One className expected");
+ actualClassName = actual.classList[0];
+
+ ok(expectedClassName == actualClassName, "Check className value");
+ info("Expected className: " + expectedClassName);
+ info("Actual className: " + actualClassName);
+ }
+
+ info("Logging the actual nodes we have:");
+ for (let j = 0; j < parent.childNodes.length; j++) {
+ let n = parent.childNodes[j];
+ info(j + " / " +
+ "nodeType: " + n.nodeType + " / " +
+ "textContent: " + n.textContent);
+ }
+
+ ok(parent.childNodes.length == parent.childNodes.length,
+ "Check we have the expected number of nodes");
+ info("Expected node count " + expectedNodes.length);
+ info("Actual node count " + expectedNodes.length);
+
+ for (let i = 0; i < expectedNodes.length; i++) {
+ info("Check node " + i);
+ checkNode(expectedNodes[i], parent.childNodes[i]);
+ }
+}
diff --git a/devtools/client/shared/test/helper_color_data.js b/devtools/client/shared/test/helper_color_data.js
new file mode 100644
index 000000000..3407102ae
--- /dev/null
+++ b/devtools/client/shared/test/helper_color_data.js
@@ -0,0 +1,175 @@
+"use strict";
+
+/* eslint-disable max-len */
+function getFixtureColorData() {
+ return [
+ {authored: "aliceblue", name: "aliceblue", hex: "#f0f8ff", hsl: "hsl(208, 100%, 97.1%)", rgb: "rgb(240, 248, 255)", cycle: 4},
+ {authored: "antiquewhite", name: "antiquewhite", hex: "#faebd7", hsl: "hsl(34.3, 77.8%, 91.2%)", rgb: "rgb(250, 235, 215)", cycle: 4},
+ {authored: "aqua", name: "aqua", hex: "#0ff", hsl: "hsl(180, 100%, 50%)", rgb: "rgb(0, 255, 255)", cycle: 4},
+ {authored: "aquamarine", name: "aquamarine", hex: "#7fffd4", hsl: "hsl(159.8, 100%, 74.9%)", rgb: "rgb(127, 255, 212)", cycle: 4},
+ {authored: "azure", name: "azure", hex: "#f0ffff", hsl: "hsl(180, 100%, 97.1%)", rgb: "rgb(240, 255, 255)", cycle: 4},
+ {authored: "beige", name: "beige", hex: "#f5f5dc", hsl: "hsl(60, 55.6%, 91.2%)", rgb: "rgb(245, 245, 220)", cycle: 4},
+ {authored: "bisque", name: "bisque", hex: "#ffe4c4", hsl: "hsl(32.5, 100%, 88.4%)", rgb: "rgb(255, 228, 196)", cycle: 4},
+ {authored: "black", name: "black", hex: "#000", hsl: "hsl(0, 0%, 0%)", rgb: "rgb(0, 0, 0)", cycle: 4},
+ {authored: "blanchedalmond", name: "blanchedalmond", hex: "#ffebcd", hsl: "hsl(36, 100%, 90.2%)", rgb: "rgb(255, 235, 205)", cycle: 4},
+ {authored: "blue", name: "blue", hex: "#00f", hsl: "hsl(240, 100%, 50%)", rgb: "rgb(0, 0, 255)", cycle: 4},
+ {authored: "blueviolet", name: "blueviolet", hex: "#8a2be2", hsl: "hsl(271.1, 75.9%, 52.7%)", rgb: "rgb(138, 43, 226)", cycle: 4},
+ {authored: "brown", name: "brown", hex: "#a52a2a", hsl: "hsl(0, 59.4%, 40.6%)", rgb: "rgb(165, 42, 42)", cycle: 4},
+ {authored: "burlywood", name: "burlywood", hex: "#deb887", hsl: "hsl(33.8, 56.9%, 70%)", rgb: "rgb(222, 184, 135)", cycle: 4},
+ {authored: "cadetblue", name: "cadetblue", hex: "#5f9ea0", hsl: "hsl(181.8, 25.5%, 50%)", rgb: "rgb(95, 158, 160)", cycle: 4},
+ {authored: "chartreuse", name: "chartreuse", hex: "#7fff00", hsl: "hsl(90.1, 100%, 50%)", rgb: "rgb(127, 255, 0)", cycle: 4},
+ {authored: "chocolate", name: "chocolate", hex: "#d2691e", hsl: "hsl(25, 75%, 47.1%)", rgb: "rgb(210, 105, 30)", cycle: 4},
+ {authored: "coral", name: "coral", hex: "#ff7f50", hsl: "hsl(16.1, 100%, 65.7%)", rgb: "rgb(255, 127, 80)", cycle: 4},
+ {authored: "cornflowerblue", name: "cornflowerblue", hex: "#6495ed", hsl: "hsl(218.5, 79.2%, 66.1%)", rgb: "rgb(100, 149, 237)", cycle: 4},
+ {authored: "cornsilk", name: "cornsilk", hex: "#fff8dc", hsl: "hsl(48, 100%, 93.1%)", rgb: "rgb(255, 248, 220)", cycle: 4},
+ {authored: "crimson", name: "crimson", hex: "#dc143c", hsl: "hsl(348, 83.3%, 47.1%)", rgb: "rgb(220, 20, 60)", cycle: 4},
+ {authored: "cyan", name: "aqua", hex: "#0ff", hsl: "hsl(180, 100%, 50%)", rgb: "rgb(0, 255, 255)", cycle: 4},
+ {authored: "darkblue", name: "darkblue", hex: "#00008b", hsl: "hsl(240, 100%, 27.3%)", rgb: "rgb(0, 0, 139)", cycle: 4},
+ {authored: "darkcyan", name: "darkcyan", hex: "#008b8b", hsl: "hsl(180, 100%, 27.3%)", rgb: "rgb(0, 139, 139)", cycle: 4},
+ {authored: "darkgoldenrod", name: "darkgoldenrod", hex: "#b8860b", hsl: "hsl(42.7, 88.7%, 38.2%)", rgb: "rgb(184, 134, 11)", cycle: 4},
+ {authored: "darkgray", name: "darkgray", hex: "#a9a9a9", hsl: "hsl(0, 0%, 66.3%)", rgb: "rgb(169, 169, 169)", cycle: 4},
+ {authored: "darkgreen", name: "darkgreen", hex: "#006400", hsl: "hsl(120, 100%, 19.6%)", rgb: "rgb(0, 100, 0)", cycle: 4},
+ {authored: "darkgrey", name: "darkgray", hex: "#a9a9a9", hsl: "hsl(0, 0%, 66.3%)", rgb: "rgb(169, 169, 169)", cycle: 4},
+ {authored: "darkkhaki", name: "darkkhaki", hex: "#bdb76b", hsl: "hsl(55.6, 38.3%, 58%)", rgb: "rgb(189, 183, 107)", cycle: 4},
+ {authored: "darkmagenta", name: "darkmagenta", hex: "#8b008b", hsl: "hsl(300, 100%, 27.3%)", rgb: "rgb(139, 0, 139)", cycle: 4},
+ {authored: "darkolivegreen", name: "darkolivegreen", hex: "#556b2f", hsl: "hsl(82, 39%, 30.2%)", rgb: "rgb(85, 107, 47)", cycle: 4},
+ {authored: "darkorange", name: "darkorange", hex: "#ff8c00", hsl: "hsl(32.9, 100%, 50%)", rgb: "rgb(255, 140, 0)", cycle: 4},
+ {authored: "darkorchid", name: "darkorchid", hex: "#9932cc", hsl: "hsl(280.1, 60.6%, 49.8%)", rgb: "rgb(153, 50, 204)", cycle: 4},
+ {authored: "darkred", name: "darkred", hex: "#8b0000", hsl: "hsl(0, 100%, 27.3%)", rgb: "rgb(139, 0, 0)", cycle: 4},
+ {authored: "darksalmon", name: "darksalmon", hex: "#e9967a", hsl: "hsl(15.1, 71.6%, 69.6%)", rgb: "rgb(233, 150, 122)", cycle: 4},
+ {authored: "darkseagreen", name: "darkseagreen", hex: "#8fbc8f", hsl: "hsl(120, 25.1%, 64.9%)", rgb: "rgb(143, 188, 143)", cycle: 4},
+ {authored: "darkslateblue", name: "darkslateblue", hex: "#483d8b", hsl: "hsl(248.5, 39%, 39.2%)", rgb: "rgb(72, 61, 139)", cycle: 4},
+ {authored: "darkslategray", name: "darkslategray", hex: "#2f4f4f", hsl: "hsl(180, 25.4%, 24.7%)", rgb: "rgb(47, 79, 79)", cycle: 4},
+ {authored: "darkslategrey", name: "darkslategray", hex: "#2f4f4f", hsl: "hsl(180, 25.4%, 24.7%)", rgb: "rgb(47, 79, 79)", cycle: 4},
+ {authored: "darkturquoise", name: "darkturquoise", hex: "#00ced1", hsl: "hsl(180.9, 100%, 41%)", rgb: "rgb(0, 206, 209)", cycle: 4},
+ {authored: "darkviolet", name: "darkviolet", hex: "#9400d3", hsl: "hsl(282.1, 100%, 41.4%)", rgb: "rgb(148, 0, 211)", cycle: 4},
+ {authored: "deeppink", name: "deeppink", hex: "#ff1493", hsl: "hsl(327.6, 100%, 53.9%)", rgb: "rgb(255, 20, 147)", cycle: 4},
+ {authored: "deepskyblue", name: "deepskyblue", hex: "#00bfff", hsl: "hsl(195.1, 100%, 50%)", rgb: "rgb(0, 191, 255)", cycle: 4},
+ {authored: "dimgray", name: "dimgray", hex: "#696969", hsl: "hsl(0, 0%, 41.2%)", rgb: "rgb(105, 105, 105)", cycle: 4},
+ {authored: "dodgerblue", name: "dodgerblue", hex: "#1e90ff", hsl: "hsl(209.6, 100%, 55.9%)", rgb: "rgb(30, 144, 255)", cycle: 4},
+ {authored: "firebrick", name: "firebrick", hex: "#b22222", hsl: "hsl(0, 67.9%, 41.6%)", rgb: "rgb(178, 34, 34)", cycle: 4},
+ {authored: "floralwhite", name: "floralwhite", hex: "#fffaf0", hsl: "hsl(40, 100%, 97.1%)", rgb: "rgb(255, 250, 240)", cycle: 4},
+ {authored: "forestgreen", name: "forestgreen", hex: "#228b22", hsl: "hsl(120, 60.7%, 33.9%)", rgb: "rgb(34, 139, 34)", cycle: 4},
+ {authored: "fuchsia", name: "fuchsia", hex: "#f0f", hsl: "hsl(300, 100%, 50%)", rgb: "rgb(255, 0, 255)", cycle: 4},
+ {authored: "gainsboro", name: "gainsboro", hex: "#dcdcdc", hsl: "hsl(0, 0%, 86.3%)", rgb: "rgb(220, 220, 220)", cycle: 4},
+ {authored: "ghostwhite", name: "ghostwhite", hex: "#f8f8ff", hsl: "hsl(240, 100%, 98.6%)", rgb: "rgb(248, 248, 255)", cycle: 4},
+ {authored: "gold", name: "gold", hex: "#ffd700", hsl: "hsl(50.6, 100%, 50%)", rgb: "rgb(255, 215, 0)", cycle: 4},
+ {authored: "goldenrod", name: "goldenrod", hex: "#daa520", hsl: "hsl(42.9, 74.4%, 49%)", rgb: "rgb(218, 165, 32)", cycle: 4},
+ {authored: "gray", name: "gray", hex: "#808080", hsl: "hsl(0, 0%, 50.2%)", rgb: "rgb(128, 128, 128)", cycle: 4},
+ {authored: "green", name: "green", hex: "#008000", hsl: "hsl(120, 100%, 25.1%)", rgb: "rgb(0, 128, 0)", cycle: 4},
+ {authored: "greenyellow", name: "greenyellow", hex: "#adff2f", hsl: "hsl(83.7, 100%, 59.2%)", rgb: "rgb(173, 255, 47)", cycle: 4},
+ {authored: "grey", name: "gray", hex: "#808080", hsl: "hsl(0, 0%, 50.2%)", rgb: "rgb(128, 128, 128)", cycle: 4},
+ {authored: "honeydew", name: "honeydew", hex: "#f0fff0", hsl: "hsl(120, 100%, 97.1%)", rgb: "rgb(240, 255, 240)", cycle: 4},
+ {authored: "hotpink", name: "hotpink", hex: "#ff69b4", hsl: "hsl(330, 100%, 70.6%)", rgb: "rgb(255, 105, 180)", cycle: 4},
+ {authored: "indianred", name: "indianred", hex: "#cd5c5c", hsl: "hsl(0, 53.1%, 58.2%)", rgb: "rgb(205, 92, 92)", cycle: 4},
+ {authored: "indigo", name: "indigo", hex: "#4b0082", hsl: "hsl(274.6, 100%, 25.5%)", rgb: "rgb(75, 0, 130)", cycle: 4},
+ {authored: "ivory", name: "ivory", hex: "#fffff0", hsl: "hsl(60, 100%, 97.1%)", rgb: "rgb(255, 255, 240)", cycle: 4},
+ {authored: "khaki", name: "khaki", hex: "#f0e68c", hsl: "hsl(54, 76.9%, 74.5%)", rgb: "rgb(240, 230, 140)", cycle: 4},
+ {authored: "lavender", name: "lavender", hex: "#e6e6fa", hsl: "hsl(240, 66.7%, 94.1%)", rgb: "rgb(230, 230, 250)", cycle: 4},
+ {authored: "lavenderblush", name: "lavenderblush", hex: "#fff0f5", hsl: "hsl(340, 100%, 97.1%)", rgb: "rgb(255, 240, 245)", cycle: 4},
+ {authored: "lawngreen", name: "lawngreen", hex: "#7cfc00", hsl: "hsl(90.5, 100%, 49.4%)", rgb: "rgb(124, 252, 0)", cycle: 4},
+ {authored: "lemonchiffon", name: "lemonchiffon", hex: "#fffacd", hsl: "hsl(54, 100%, 90.2%)", rgb: "rgb(255, 250, 205)", cycle: 4},
+ {authored: "lightblue", name: "lightblue", hex: "#add8e6", hsl: "hsl(194.7, 53.3%, 79%)", rgb: "rgb(173, 216, 230)", cycle: 4},
+ {authored: "lightcoral", name: "lightcoral", hex: "#f08080", hsl: "hsl(0, 78.9%, 72.2%)", rgb: "rgb(240, 128, 128)", cycle: 4},
+ {authored: "lightcyan", name: "lightcyan", hex: "#e0ffff", hsl: "hsl(180, 100%, 93.9%)", rgb: "rgb(224, 255, 255)", cycle: 4},
+ {authored: "lightgoldenrodyellow", name: "lightgoldenrodyellow", hex: "#fafad2", hsl: "hsl(60, 80%, 90.2%)", rgb: "rgb(250, 250, 210)", cycle: 4},
+ {authored: "lightgray", name: "lightgray", hex: "#d3d3d3", hsl: "hsl(0, 0%, 82.7%)", rgb: "rgb(211, 211, 211)", cycle: 4},
+ {authored: "lightgreen", name: "lightgreen", hex: "#90ee90", hsl: "hsl(120, 73.4%, 74.9%)", rgb: "rgb(144, 238, 144)", cycle: 4},
+ {authored: "lightgrey", name: "lightgray", hex: "#d3d3d3", hsl: "hsl(0, 0%, 82.7%)", rgb: "rgb(211, 211, 211)", cycle: 4},
+ {authored: "lightpink", name: "lightpink", hex: "#ffb6c1", hsl: "hsl(351, 100%, 85.7%)", rgb: "rgb(255, 182, 193)", cycle: 4},
+ {authored: "lightsalmon", name: "lightsalmon", hex: "#ffa07a", hsl: "hsl(17.1, 100%, 73.9%)", rgb: "rgb(255, 160, 122)", cycle: 4},
+ {authored: "lightseagreen", name: "lightseagreen", hex: "#20b2aa", hsl: "hsl(176.7, 69.5%, 41.2%)", rgb: "rgb(32, 178, 170)", cycle: 4},
+ {authored: "lightskyblue", name: "lightskyblue", hex: "#87cefa", hsl: "hsl(203, 92%, 75.5%)", rgb: "rgb(135, 206, 250)", cycle: 4},
+ {authored: "lightslategray", name: "lightslategray", hex: "#789", hsl: "hsl(210, 14.3%, 53.3%)", rgb: "rgb(119, 136, 153)", cycle: 4},
+ {authored: "lightslategrey", name: "lightslategray", hex: "#789", hsl: "hsl(210, 14.3%, 53.3%)", rgb: "rgb(119, 136, 153)", cycle: 4},
+ {authored: "lightsteelblue", name: "lightsteelblue", hex: "#b0c4de", hsl: "hsl(213.9, 41.1%, 78%)", rgb: "rgb(176, 196, 222)", cycle: 4},
+ {authored: "lightyellow", name: "lightyellow", hex: "#ffffe0", hsl: "hsl(60, 100%, 93.9%)", rgb: "rgb(255, 255, 224)", cycle: 4},
+ {authored: "lime", name: "lime", hex: "#0f0", hsl: "hsl(120, 100%, 50%)", rgb: "rgb(0, 255, 0)", cycle: 4},
+ {authored: "limegreen", name: "limegreen", hex: "#32cd32", hsl: "hsl(120, 60.8%, 50%)", rgb: "rgb(50, 205, 50)", cycle: 4},
+ {authored: "linen", name: "linen", hex: "#faf0e6", hsl: "hsl(30, 66.7%, 94.1%)", rgb: "rgb(250, 240, 230)", cycle: 4},
+ {authored: "magenta", name: "fuchsia", hex: "#f0f", hsl: "hsl(300, 100%, 50%)", rgb: "rgb(255, 0, 255)", cycle: 4},
+ {authored: "maroon", name: "maroon", hex: "#800000", hsl: "hsl(0, 100%, 25.1%)", rgb: "rgb(128, 0, 0)", cycle: 4},
+ {authored: "mediumaquamarine", name: "mediumaquamarine", hex: "#66cdaa", hsl: "hsl(159.6, 50.7%, 60.2%)", rgb: "rgb(102, 205, 170)", cycle: 4},
+ {authored: "mediumblue", name: "mediumblue", hex: "#0000cd", hsl: "hsl(240, 100%, 40.2%)", rgb: "rgb(0, 0, 205)", cycle: 4},
+ {authored: "mediumorchid", name: "mediumorchid", hex: "#ba55d3", hsl: "hsl(288.1, 58.9%, 58%)", rgb: "rgb(186, 85, 211)", cycle: 4},
+ {authored: "mediumpurple", name: "mediumpurple", hex: "#9370db", hsl: "hsl(259.6, 59.8%, 64.9%)", rgb: "rgb(147, 112, 219)", cycle: 4},
+ {authored: "mediumseagreen", name: "mediumseagreen", hex: "#3cb371", hsl: "hsl(146.7, 49.8%, 46.9%)", rgb: "rgb(60, 179, 113)", cycle: 4},
+ {authored: "mediumslateblue", name: "mediumslateblue", hex: "#7b68ee", hsl: "hsl(248.5, 79.8%, 67.1%)", rgb: "rgb(123, 104, 238)", cycle: 4},
+ {authored: "mediumspringgreen", name: "mediumspringgreen", hex: "#00fa9a", hsl: "hsl(157, 100%, 49%)", rgb: "rgb(0, 250, 154)", cycle: 4},
+ {authored: "mediumturquoise", name: "mediumturquoise", hex: "#48d1cc", hsl: "hsl(177.8, 59.8%, 55.1%)", rgb: "rgb(72, 209, 204)", cycle: 4},
+ {authored: "mediumvioletred", name: "mediumvioletred", hex: "#c71585", hsl: "hsl(322.2, 80.9%, 43.1%)", rgb: "rgb(199, 21, 133)", cycle: 4},
+ {authored: "midnightblue", name: "midnightblue", hex: "#191970", hsl: "hsl(240, 63.5%, 26.9%)", rgb: "rgb(25, 25, 112)", cycle: 4},
+ {authored: "mintcream", name: "mintcream", hex: "#f5fffa", hsl: "hsl(150, 100%, 98%)", rgb: "rgb(245, 255, 250)", cycle: 4},
+ {authored: "mistyrose", name: "mistyrose", hex: "#ffe4e1", hsl: "hsl(6, 100%, 94.1%)", rgb: "rgb(255, 228, 225)", cycle: 4},
+ {authored: "moccasin", name: "moccasin", hex: "#ffe4b5", hsl: "hsl(38.1, 100%, 85.5%)", rgb: "rgb(255, 228, 181)", cycle: 4},
+ {authored: "navajowhite", name: "navajowhite", hex: "#ffdead", hsl: "hsl(35.9, 100%, 83.9%)", rgb: "rgb(255, 222, 173)", cycle: 4},
+ {authored: "navy", name: "navy", hex: "#000080", hsl: "hsl(240, 100%, 25.1%)", rgb: "rgb(0, 0, 128)", cycle: 4},
+ {authored: "oldlace", name: "oldlace", hex: "#fdf5e6", hsl: "hsl(39.1, 85.2%, 94.7%)", rgb: "rgb(253, 245, 230)", cycle: 4},
+ {authored: "olive", name: "olive", hex: "#808000", hsl: "hsl(60, 100%, 25.1%)", rgb: "rgb(128, 128, 0)", cycle: 4},
+ {authored: "olivedrab", name: "olivedrab", hex: "#6b8e23", hsl: "hsl(79.6, 60.5%, 34.7%)", rgb: "rgb(107, 142, 35)", cycle: 4},
+ {authored: "orange", name: "orange", hex: "#ffa500", hsl: "hsl(38.8, 100%, 50%)", rgb: "rgb(255, 165, 0)", cycle: 4},
+ {authored: "orangered", name: "orangered", hex: "#ff4500", hsl: "hsl(16.2, 100%, 50%)", rgb: "rgb(255, 69, 0)", cycle: 4},
+ {authored: "orchid", name: "orchid", hex: "#da70d6", hsl: "hsl(302.3, 58.9%, 64.7%)", rgb: "rgb(218, 112, 214)", cycle: 4},
+ {authored: "palegoldenrod", name: "palegoldenrod", hex: "#eee8aa", hsl: "hsl(54.7, 66.7%, 80%)", rgb: "rgb(238, 232, 170)", cycle: 4},
+ {authored: "palegreen", name: "palegreen", hex: "#98fb98", hsl: "hsl(120, 92.5%, 79%)", rgb: "rgb(152, 251, 152)", cycle: 4},
+ {authored: "paleturquoise", name: "paleturquoise", hex: "#afeeee", hsl: "hsl(180, 64.9%, 81%)", rgb: "rgb(175, 238, 238)", cycle: 4},
+ {authored: "palevioletred", name: "palevioletred", hex: "#db7093", hsl: "hsl(340.4, 59.8%, 64.9%)", rgb: "rgb(219, 112, 147)", cycle: 4},
+ {authored: "papayawhip", name: "papayawhip", hex: "#ffefd5", hsl: "hsl(37.1, 100%, 91.8%)", rgb: "rgb(255, 239, 213)", cycle: 4},
+ {authored: "peachpuff", name: "peachpuff", hex: "#ffdab9", hsl: "hsl(28.3, 100%, 86.3%)", rgb: "rgb(255, 218, 185)", cycle: 4},
+ {authored: "peru", name: "peru", hex: "#cd853f", hsl: "hsl(29.6, 58.7%, 52.5%)", rgb: "rgb(205, 133, 63)", cycle: 4},
+ {authored: "pink", name: "pink", hex: "#ffc0cb", hsl: "hsl(349.5, 100%, 87.6%)", rgb: "rgb(255, 192, 203)", cycle: 4},
+ {authored: "plum", name: "plum", hex: "#dda0dd", hsl: "hsl(300, 47.3%, 74.7%)", rgb: "rgb(221, 160, 221)", cycle: 4},
+ {authored: "powderblue", name: "powderblue", hex: "#b0e0e6", hsl: "hsl(186.7, 51.9%, 79.6%)", rgb: "rgb(176, 224, 230)", cycle: 4},
+ {authored: "purple", name: "purple", hex: "#800080", hsl: "hsl(300, 100%, 25.1%)", rgb: "rgb(128, 0, 128)", cycle: 4},
+ {authored: "rebeccapurple", name: "rebeccapurple", hex: "#639", hsl: "hsl(270, 50%, 40%)", rgb: "rgb(102, 51, 153)", cycle: 4},
+ {authored: "red", name: "red", hex: "#f00", hsl: "hsl(0, 100%, 50%)", rgb: "rgb(255, 0, 0)", cycle: 4},
+ {authored: "rosybrown", name: "rosybrown", hex: "#bc8f8f", hsl: "hsl(0, 25.1%, 64.9%)", rgb: "rgb(188, 143, 143)", cycle: 4},
+ {authored: "royalblue", name: "royalblue", hex: "#4169e1", hsl: "hsl(225, 72.7%, 56.9%)", rgb: "rgb(65, 105, 225)", cycle: 4},
+ {authored: "saddlebrown", name: "saddlebrown", hex: "#8b4513", hsl: "hsl(25, 75.9%, 31%)", rgb: "rgb(139, 69, 19)", cycle: 4},
+ {authored: "salmon", name: "salmon", hex: "#fa8072", hsl: "hsl(6.2, 93.2%, 71.4%)", rgb: "rgb(250, 128, 114)", cycle: 4},
+ {authored: "sandybrown", name: "sandybrown", hex: "#f4a460", hsl: "hsl(27.6, 87.1%, 66.7%)", rgb: "rgb(244, 164, 96)", cycle: 4},
+ {authored: "seagreen", name: "seagreen", hex: "#2e8b57", hsl: "hsl(146.5, 50.3%, 36.3%)", rgb: "rgb(46, 139, 87)", cycle: 4},
+ {authored: "seashell", name: "seashell", hex: "#fff5ee", hsl: "hsl(24.7, 100%, 96.7%)", rgb: "rgb(255, 245, 238)", cycle: 4},
+ {authored: "sienna", name: "sienna", hex: "#a0522d", hsl: "hsl(19.3, 56.1%, 40.2%)", rgb: "rgb(160, 82, 45)", cycle: 4},
+ {authored: "silver", name: "silver", hex: "#c0c0c0", hsl: "hsl(0, 0%, 75.3%)", rgb: "rgb(192, 192, 192)", cycle: 4},
+ {authored: "skyblue", name: "skyblue", hex: "#87ceeb", hsl: "hsl(197.4, 71.4%, 72.5%)", rgb: "rgb(135, 206, 235)", cycle: 4},
+ {authored: "slateblue", name: "slateblue", hex: "#6a5acd", hsl: "hsl(248.3, 53.5%, 57.8%)", rgb: "rgb(106, 90, 205)", cycle: 4},
+ {authored: "slategray", name: "slategray", hex: "#708090", hsl: "hsl(210, 12.6%, 50.2%)", rgb: "rgb(112, 128, 144)", cycle: 4},
+ {authored: "slategrey", name: "slategray", hex: "#708090", hsl: "hsl(210, 12.6%, 50.2%)", rgb: "rgb(112, 128, 144)", cycle: 4},
+ {authored: "snow", name: "snow", hex: "#fffafa", hsl: "hsl(0, 100%, 99%)", rgb: "rgb(255, 250, 250)", cycle: 4},
+ {authored: "springgreen", name: "springgreen", hex: "#00ff7f", hsl: "hsl(149.9, 100%, 50%)", rgb: "rgb(0, 255, 127)", cycle: 4},
+ {authored: "steelblue", name: "steelblue", hex: "#4682b4", hsl: "hsl(207.3, 44%, 49%)", rgb: "rgb(70, 130, 180)", cycle: 4},
+ {authored: "tan", name: "tan", hex: "#d2b48c", hsl: "hsl(34.3, 43.7%, 68.6%)", rgb: "rgb(210, 180, 140)", cycle: 4},
+ {authored: "teal", name: "teal", hex: "#008080", hsl: "hsl(180, 100%, 25.1%)", rgb: "rgb(0, 128, 128)", cycle: 4},
+ {authored: "thistle", name: "thistle", hex: "#d8bfd8", hsl: "hsl(300, 24.3%, 79.8%)", rgb: "rgb(216, 191, 216)", cycle: 4},
+ {authored: "tomato", name: "tomato", hex: "#ff6347", hsl: "hsl(9.1, 100%, 63.9%)", rgb: "rgb(255, 99, 71)", cycle: 4},
+ {authored: "turquoise", name: "turquoise", hex: "#40e0d0", hsl: "hsl(174, 72.1%, 56.5%)", rgb: "rgb(64, 224, 208)", cycle: 4},
+ {authored: "violet", name: "violet", hex: "#ee82ee", hsl: "hsl(300, 76.1%, 72.2%)", rgb: "rgb(238, 130, 238)", cycle: 4},
+ {authored: "wheat", name: "wheat", hex: "#f5deb3", hsl: "hsl(39.1, 76.7%, 83.1%)", rgb: "rgb(245, 222, 179)", cycle: 4},
+ {authored: "white", name: "white", hex: "#fff", hsl: "hsl(0, 0%, 100%)", rgb: "rgb(255, 255, 255)", cycle: 4},
+ {authored: "whitesmoke", name: "whitesmoke", hex: "#f5f5f5", hsl: "hsl(0, 0%, 96.1%)", rgb: "rgb(245, 245, 245)", cycle: 4},
+ {authored: "yellow", name: "yellow", hex: "#ff0", hsl: "hsl(60, 100%, 50%)", rgb: "rgb(255, 255, 0)", cycle: 4},
+ {authored: "yellowgreen", name: "yellowgreen", hex: "#9acd32", hsl: "hsl(79.7, 60.8%, 50%)", rgb: "rgb(154, 205, 50)", cycle: 4},
+ {authored: "rgba(0, 0, 0, 0)", name: "#0000", hex: "#0000", hsl: "hsla(0, 0%, 0%, 0)", rgb: "rgba(0, 0, 0, 0)", cycle: 3},
+ {authored: "hsla(0, 0%, 0%, 0)", name: "#0000", hex: "#0000", hsl: "hsla(0, 0%, 0%, 0)", rgb: "rgba(0, 0, 0, 0)", cycle: 3},
+ {authored: "rgba(50, 60, 70, 0.5)", name: "#323c4680", hex: "#323c4680", hsl: "hsla(210, 16.7%, 23.5%, 0.5)", rgb: "rgba(50, 60, 70, 0.5)", cycle: 3},
+ {authored: "rgba(0, 0, 0, 0.3)", name: "#0000004d", hex: "#0000004d", hsl: "hsla(0, 0%, 0%, 0.3)", rgb: "rgba(0, 0, 0, 0.3)", cycle: 3},
+ {authored: "rgba(255, 255, 255, 0.6)", name: "#fff9", hex: "#fff9", hsl: "hsla(0, 0%, 100%, 0.6)", rgb: "rgba(255, 255, 255, 0.6)", cycle: 3},
+ {authored: "rgba(127, 89, 45, 1)", name: "#7f592d", hex: "#7f592d", hsl: "hsl(32.2, 47.7%, 33.7%)", rgb: "rgb(127, 89, 45)", cycle: 3},
+ {authored: "hsla(19.304, 56%, 40%, 1)", name: "#9f522d", hex: "#9f522d", hsl: "hsl(19.5, 55.9%, 40%)", rgb: "rgb(159, 82, 45)", cycle: 3},
+ {authored: "#f089", name: "#f089", hex: "#f089", hsl: "hsla(328, 100%, 50%, 0.6)", rgb: "rgba(255, 0, 136, 0.6)", cycle: 3},
+ {authored: "#00ff8080", name: "#00ff8080", hex: "#00ff8080", hsl: "hsla(150.1, 100%, 50%, 0.5)", rgb: "rgba(0, 255, 128, 0.5)", cycle: 3},
+ {authored: "currentcolor", name: "currentcolor", hex: "currentcolor", hsl: "currentcolor", rgb: "currentcolor", cycle: false},
+ {authored: "inherit", name: "inherit", hex: "inherit", hsl: "inherit", rgb: "inherit", cycle: false},
+ {authored: "initial", name: "initial", hex: "initial", hsl: "initial", rgb: "initial", cycle: false},
+ {authored: "invalidColor", name: "", hex: "", hsl: "", rgb: "", cycle: false},
+ {authored: "transparent", name: "transparent", hex: "transparent", hsl: "transparent", rgb: "transparent", cycle: false},
+ {authored: "unset", name: "unset", hex: "unset", hsl: "unset", rgb: "unset", cycle: false},
+ ];
+}
+/* eslint-enable max-len */
+
+// Allow this function to be shared on mochitests and xpcshell tests.
+if (typeof module === "object") {
+ module.exports = getFixtureColorData;
+}
diff --git a/devtools/client/shared/test/helper_html_tooltip.js b/devtools/client/shared/test/helper_html_tooltip.js
new file mode 100644
index 000000000..ffc6945f3
--- /dev/null
+++ b/devtools/client/shared/test/helper_html_tooltip.js
@@ -0,0 +1,96 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint no-unused-vars: [2, {"vars": "local", "args": "none"}] */
+
+"use strict";
+
+/**
+ * Helper methods for the HTMLTooltip integration tests.
+ */
+
+/**
+ * Display an existing HTMLTooltip on an anchor. After the tooltip "shown"
+ * event has been fired a reflow will be triggered.
+ *
+ * @param {HTMLTooltip} tooltip
+ * The tooltip instance to display
+ * @param {Node} anchor
+ * The anchor that should be used to display the tooltip
+ * @param {Object} see HTMLTooltip:show documentation
+ * @return {Promise} promise that resolves when "shown" has been fired, reflow
+ * and repaint done.
+ */
+function* showTooltip(tooltip, anchor, {position, x, y} = {}) {
+ let onShown = tooltip.once("shown");
+ tooltip.show(anchor, {position, x, y});
+ yield onShown;
+ return waitForReflow(tooltip);
+}
+
+/**
+ * Hide an existing HTMLTooltip. After the tooltip "hidden" event has been fired
+ * a reflow will be triggered.
+ *
+ * @param {HTMLTooltip} tooltip
+ * The tooltip instance to hide
+ * @return {Promise} promise that resolves when "hidden" has been fired, reflow
+ * and repaint done.
+ */
+function* hideTooltip(tooltip) {
+ let onPopupHidden = tooltip.once("hidden");
+ tooltip.hide();
+ yield onPopupHidden;
+ return waitForReflow(tooltip);
+}
+
+/**
+ * Forces the reflow of an HTMLTooltip document and waits for the next repaint.
+ *
+ * @param {HTMLTooltip} the tooltip to reflow
+ * @return {Promise} a promise that will resolve after the reflow and repaint
+ * have been executed.
+ */
+function waitForReflow(tooltip) {
+ let {doc} = tooltip;
+ return new Promise(resolve => {
+ doc.documentElement.offsetWidth;
+ doc.defaultView.requestAnimationFrame(resolve);
+ });
+}
+
+/**
+ * Test helper designed to check that a tooltip is displayed at the expected
+ * position relative to an anchor, given a set of expectations.
+ *
+ * @param {HTMLTooltip} tooltip
+ * The HTMLTooltip instance to check
+ * @param {Node} anchor
+ * The tooltip's anchor
+ * @param {Object} expected
+ * - {String} position : "top" or "bottom"
+ * - {Boolean} leftAligned
+ * - {Number} width: expected tooltip width
+ * - {Number} height: expected tooltip height
+ */
+function checkTooltipGeometry(tooltip, anchor,
+ {position, leftAligned = true, height, width} = {}) {
+ info("Check the tooltip geometry matches expected position and dimensions");
+ let tooltipRect = tooltip.container.getBoundingClientRect();
+ let anchorRect = anchor.getBoundingClientRect();
+
+ if (position === "top") {
+ is(tooltipRect.bottom, anchorRect.top, "Tooltip is above the anchor");
+ } else if (position === "bottom") {
+ is(tooltipRect.top, anchorRect.bottom, "Tooltip is below the anchor");
+ } else {
+ ok(false, "Invalid position provided to checkTooltipGeometry");
+ }
+
+ if (leftAligned) {
+ is(tooltipRect.left, anchorRect.left,
+ "Tooltip left-aligned with the anchor");
+ }
+
+ is(tooltipRect.height, height, "Tooltip has the expected height");
+ is(tooltipRect.width, width, "Tooltip has the expected width");
+}
diff --git a/devtools/client/shared/test/helper_inplace_editor.js b/devtools/client/shared/test/helper_inplace_editor.js
new file mode 100644
index 000000000..ec6b79e00
--- /dev/null
+++ b/devtools/client/shared/test/helper_inplace_editor.js
@@ -0,0 +1,115 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint no-unused-vars: [2, {"vars": "local", "args": "none"}] */
+/* import-globals-from head.js */
+
+"use strict";
+
+/**
+ * Helper methods for the HTMLTooltip integration tests.
+ */
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const { editableField } = require("devtools/client/shared/inplace-editor");
+
+/**
+ * Create an inplace editor linked to a span element and click on the span to
+ * to turn to edit mode.
+ *
+ * @param {Object} options
+ * Options passed to the InplaceEditor/editableField constructor.
+ * @param {Document} doc
+ * Document where the span element will be created.
+ * @param {String} textContent
+ * (optional) String that will be used as the text content of the span.
+ */
+const createInplaceEditorAndClick = Task.async(function* (options, doc, textContent) {
+ let span = options.element = createSpan(doc);
+ if (textContent) {
+ span.textContent = textContent;
+ }
+
+ info("Creating an inplace-editor field");
+ editableField(options);
+
+ info("Clicking on the inplace-editor field to turn to edit mode");
+ span.click();
+});
+
+/**
+ * Helper to create a span in the provided document.
+ *
+ * @param {Document} doc
+ * Document where the span element will be created.
+ * @return {Element} the created span element.
+ */
+function createSpan(doc) {
+ info("Creating a new span element");
+ let div = doc.createElementNS(HTML_NS, "div");
+ let span = doc.createElementNS(HTML_NS, "span");
+ span.setAttribute("tabindex", "0");
+ span.style.fontSize = "11px";
+ span.style.display = "inline-block";
+ span.style.width = "100px";
+ span.style.border = "1px solid red";
+ span.style.fontFamily = "monospace";
+
+ div.style.height = "100%";
+ div.style.position = "absolute";
+ div.appendChild(span);
+
+ let parent = doc.querySelector("window") || doc.body;
+ parent.appendChild(div);
+ return span;
+}
+
+/**
+ * Test helper simulating a key event in an InplaceEditor and checking that the
+ * autocompletion works as expected.
+ *
+ * @param {Array} testData
+ * - {String} key, the key to send
+ * - {String} completion, the expected value of the auto-completion
+ * - {Number} index, the index of the selected suggestion in the popup
+ * - {Number} total, the total number of suggestions in the popup
+ * @param {InplaceEditor} editor
+ * The InplaceEditor instance being tested
+ */
+function* testCompletion([key, completion, index, total], editor) {
+ info("Pressing key " + key);
+ info("Expecting " + completion);
+
+ let onVisibilityChange = null;
+ let open = total > 0;
+ if (editor.popup.isOpen != open) {
+ onVisibilityChange = editor.popup.once(open ? "popup-opened" : "popup-closed");
+ }
+
+ let onSuggest;
+ if (/(left|right|back_space|escape)/ig.test(key)) {
+ info("Adding event listener for right|back_space|escape keys");
+ onSuggest = once(editor.input, "keypress");
+ } else {
+ info("Waiting for after-suggest event on the editor");
+ onSuggest = editor.once("after-suggest");
+ }
+
+ info("Synthesizing key " + key);
+ EventUtils.synthesizeKey(key, {}, editor.input.defaultView);
+
+ yield onSuggest;
+ yield onVisibilityChange;
+ yield waitForTick();
+
+ info("Checking the state");
+ if (completion !== null) {
+ is(editor.input.value, completion, "Correct value is autocompleted");
+ }
+ if (total === 0) {
+ ok(!(editor.popup && editor.popup.isOpen), "Popup is closed");
+ } else {
+ ok(editor.popup.isOpen, "Popup is open");
+ is(editor.popup.getItems().length, total, "Number of suggestions match");
+ is(editor.popup.selectedIndex, index, "Expected item is selected");
+ }
+}
diff --git a/devtools/client/shared/test/html-mdn-css-basic-testing.html b/devtools/client/shared/test/html-mdn-css-basic-testing.html
new file mode 100644
index 000000000..182fa6d9b
--- /dev/null
+++ b/devtools/client/shared/test/html-mdn-css-basic-testing.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<html>
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+</head>
+
+<body>
+
+ <h2 id="Summary">Summary</h2>
+
+ <p>A summary of the property.</p>
+
+ <h2 id="Syntax">Syntax</h2>
+
+ <pre>/* The part we want */
+this: is-the-part-we-want;</pre>
+
+
+
+</body>
+</html> \ No newline at end of file
diff --git a/devtools/client/shared/test/html-mdn-css-no-summary-or-syntax.html b/devtools/client/shared/test/html-mdn-css-no-summary-or-syntax.html
new file mode 100644
index 000000000..8e1a7c3f6
--- /dev/null
+++ b/devtools/client/shared/test/html-mdn-css-no-summary-or-syntax.html
@@ -0,0 +1,12 @@
+<!doctype html>
+<html>
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+</head>
+
+<body>
+
+<p>This is not the summary or the syntax.</p>
+
+</body>
+</html> \ No newline at end of file
diff --git a/devtools/client/shared/test/html-mdn-css-no-summary.html b/devtools/client/shared/test/html-mdn-css-no-summary.html
new file mode 100644
index 000000000..3af52b4f6
--- /dev/null
+++ b/devtools/client/shared/test/html-mdn-css-no-summary.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<html>
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+</head>
+
+<body>
+
+ <p>This is not the summary.</p>
+
+ <h2 id="Syntax">Syntax</h2>
+
+ <pre>To be ignored.</pre>
+
+ <pre>/* The part we want */
+this: is-the-part-we-want;</pre>
+
+
+
+</body>
+</html> \ No newline at end of file
diff --git a/devtools/client/shared/test/html-mdn-css-no-syntax.html b/devtools/client/shared/test/html-mdn-css-no-syntax.html
new file mode 100644
index 000000000..4e43bc434
--- /dev/null
+++ b/devtools/client/shared/test/html-mdn-css-no-syntax.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<html>
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+</head>
+
+<body>
+
+ <h2 id="Summary">Summary</h2>
+
+ <p>A summary of the property.</p>
+
+ <p>This is not the syntax.</p>
+
+
+</body>
+</html>
diff --git a/devtools/client/shared/test/html-mdn-css-syntax-old-style.html b/devtools/client/shared/test/html-mdn-css-syntax-old-style.html
new file mode 100644
index 000000000..281267cc4
--- /dev/null
+++ b/devtools/client/shared/test/html-mdn-css-syntax-old-style.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<html>
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+</head>
+
+<body>
+
+ <h2 id="Summary">Summary</h2>
+
+ <p>A summary of the property.</p>
+
+ <h2 id="Syntax">Syntax</h2>
+
+ <pre>The part we should ignore</pre>
+
+ <pre>/* The part we want */
+this: is-the-part-we-want;</pre>
+
+
+
+</body>
+</html> \ No newline at end of file
diff --git a/devtools/client/shared/test/leakhunt.js b/devtools/client/shared/test/leakhunt.js
new file mode 100644
index 000000000..e71244955
--- /dev/null
+++ b/devtools/client/shared/test/leakhunt.js
@@ -0,0 +1,165 @@
+/* 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";
+
+/**
+ * Memory leak hunter. Walks a tree of objects looking for DOM nodes.
+ * Usage:
+ * leakHunt({
+ * thing: thing,
+ * otherthing: otherthing
+ * });
+ */
+function leakHunt(root) {
+ let path = [];
+ let seen = [];
+
+ try {
+ let output = leakHunt.inner(root, path, seen);
+ output.forEach(function (line) {
+ dump(line + "\n");
+ });
+ } catch (ex) {
+ dump(ex + "\n");
+ }
+}
+
+leakHunt.inner = function (root, path, seen) {
+ let prefix = new Array(path.length).join(" ");
+
+ let reply = [];
+ function log(msg) {
+ reply.push(msg);
+ }
+
+ let direct;
+ try {
+ direct = Object.keys(root);
+ } catch (ex) {
+ log(prefix + " Error enumerating: " + ex);
+ return reply;
+ }
+
+ try {
+ let index = 0;
+ for (let data of root) {
+ let prop = "" + index;
+ leakHunt.digProperty(prop, data, path, seen, direct, log);
+ index++;
+ }
+ } catch (ex) {
+ /* Ignore things that are not enumerable */
+ }
+
+ for (let prop in root) {
+ let data;
+ try {
+ data = root[prop];
+ } catch (ex) {
+ log(prefix + " " + prop + " = Error: " + ex.toString().substring(0, 30));
+ continue;
+ }
+
+ leakHunt.digProperty(prop, data, path, seen, direct, log);
+ }
+
+ return reply;
+};
+
+leakHunt.hide = [ /^string$/, /^number$/, /^boolean$/, /^null/, /^undefined/ ];
+
+leakHunt.noRecurse = [
+ /^string$/, /^number$/, /^boolean$/, /^null/, /^undefined/,
+ /^Window$/, /^Document$/,
+ /^XULDocument$/, /^XULElement$/,
+ /^DOMWindow$/, /^HTMLDocument$/, /^HTML.*Element$/, /^ChromeWindow$/
+];
+
+leakHunt.digProperty = function (prop, data, path, seen, direct, log) {
+ let newPath = path.slice();
+ newPath.push(prop);
+ let prefix = new Array(newPath.length).join(" ");
+
+ let recurse = true;
+ let message = leakHunt.getType(data);
+
+ if (leakHunt.matchesAnyPattern(message, leakHunt.hide)) {
+ return;
+ }
+
+ if (message === "function" && direct.indexOf(prop) == -1) {
+ return;
+ }
+
+ if (message === "string") {
+ let extra = data.length > 10 ? data.substring(0, 9) + "_" : data;
+ message += ' "' + extra.replace(/\n/g, "|") + '"';
+ recurse = false;
+ } else if (leakHunt.matchesAnyPattern(message, leakHunt.noRecurse)) {
+ message += " (no recurse)";
+ recurse = false;
+ } else if (seen.indexOf(data) !== -1) {
+ message += " (already seen)";
+ recurse = false;
+ }
+
+ if (recurse) {
+ seen.push(data);
+ let lines = leakHunt.inner(data, newPath, seen);
+ if (lines.length == 0) {
+ if (message !== "function") {
+ log(prefix + prop + " = " + message + " { }");
+ }
+ } else {
+ log(prefix + prop + " = " + message + " {");
+ lines.forEach(function (line) {
+ log(line);
+ });
+ log(prefix + "}");
+ }
+ } else {
+ log(prefix + prop + " = " + message);
+ }
+};
+
+leakHunt.matchesAnyPattern = function (str, patterns) {
+ let match = false;
+ patterns.forEach(function (pattern) {
+ if (str.match(pattern)) {
+ match = true;
+ }
+ });
+ return match;
+};
+
+leakHunt.getType = function (data) {
+ if (data === null) {
+ return "null";
+ }
+ if (data === undefined) {
+ return "undefined";
+ }
+
+ let type = typeof data;
+ if (type === "object" || type === "Object") {
+ type = leakHunt.getCtorName(data);
+ }
+
+ return type;
+};
+
+leakHunt.getCtorName = function (obj) {
+ try {
+ if (obj.constructor && obj.constructor.name) {
+ return obj.constructor.name;
+ }
+ } catch (ex) {
+ return "UnknownObject";
+ }
+
+ // If that fails, use Objects toString which sometimes gives something
+ // better than 'Object', and at least defaults to Object if nothing better
+ return Object.prototype.toString.call(obj).slice(8, -1);
+};
diff --git a/devtools/client/shared/test/test-actor-registry.js b/devtools/client/shared/test/test-actor-registry.js
new file mode 100644
index 000000000..e3b47c154
--- /dev/null
+++ b/devtools/client/shared/test/test-actor-registry.js
@@ -0,0 +1,97 @@
+/* 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";
+
+(function (exports) {
+ const Cu = Components.utils;
+ const CC = Components.Constructor;
+
+ const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+ const { fetch } = require("devtools/shared/DevToolsUtils");
+ const defer = require("devtools/shared/defer");
+ const { Task } = require("devtools/shared/task");
+
+ const TEST_URL_ROOT = "http://example.com/browser/devtools/client/shared/test/";
+ const ACTOR_URL = TEST_URL_ROOT + "test-actor.js";
+
+ // Register a test actor that can operate on the remote document
+ exports.registerTestActor = Task.async(function* (client) {
+ // First, instanciate ActorRegistryFront to be able to dynamically register an actor
+ let deferred = defer();
+ client.listTabs(deferred.resolve);
+ let response = yield deferred.promise;
+ let { ActorRegistryFront } = require("devtools/shared/fronts/actor-registry");
+ let registryFront = ActorRegistryFront(client, response);
+
+ // Then ask to register our test-actor to retrieve its front
+ let options = {
+ type: { tab: true },
+ constructor: "TestActor",
+ prefix: "testActor"
+ };
+ let testActorFront = yield registryFront.registerActor(ACTOR_URL, options);
+ return testActorFront;
+ });
+
+ // Load the test actor in a custom sandbox as we can't use SDK module loader with URIs
+ let loadFront = Task.async(function* () {
+ let sourceText = yield request(ACTOR_URL);
+ const principal = CC("@mozilla.org/systemprincipal;1", "nsIPrincipal")();
+ const sandbox = Cu.Sandbox(principal);
+ sandbox.exports = {};
+ sandbox.require = require;
+ Cu.evalInSandbox(sourceText, sandbox, "1.8", ACTOR_URL, 1);
+ return sandbox.exports;
+ });
+
+ // Ensure fetching a live TabActor form for the targeted app
+ // (helps fetching the test actor registered dynamically)
+ let getUpdatedForm = function (client, tab) {
+ return client.getTab({tab: tab})
+ .then(response => response.tab);
+ };
+
+ // Spawn an instance of the test actor for the given toolbox
+ exports.getTestActor = Task.async(function* (toolbox) {
+ let client = toolbox.target.client;
+ return getTestActor(client, toolbox.target.tab, toolbox);
+ });
+
+ // Sometimes, we need the test actor before opening or without a toolbox then just
+ // create a front for the given `tab`
+ exports.getTestActorWithoutToolbox = Task.async(function* (tab) {
+ let { DebuggerServer } = require("devtools/server/main");
+ let { DebuggerClient } = require("devtools/shared/client/main");
+
+ // We need to spawn a client instance,
+ // but for that we have to first ensure a server is running
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+ let client = new DebuggerClient(DebuggerServer.connectPipe());
+
+ yield client.connect();
+
+ // We also need to make sure the test actor is registered on the server.
+ yield exports.registerTestActor(client);
+
+ return getTestActor(client, tab);
+ });
+
+ // Fetch the content of a URI
+ let request = function (uri) {
+ return fetch(uri).then(({ content }) => content);
+ };
+
+ let getTestActor = Task.async(function* (client, tab, toolbox) {
+ // We may have to update the form in order to get the dynamically registered
+ // test actor.
+ let form = yield getUpdatedForm(client, tab);
+
+ let { TestActorFront } = yield loadFront();
+
+ return new TestActorFront(client, form, toolbox);
+ });
+})(this);
diff --git a/devtools/client/shared/test/test-actor.js b/devtools/client/shared/test/test-actor.js
new file mode 100644
index 000000000..3aab5287b
--- /dev/null
+++ b/devtools/client/shared/test/test-actor.js
@@ -0,0 +1,1138 @@
+/* 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/ */
+
+/* exported TestActor, TestActorFront */
+
+"use strict";
+
+// A helper actor for inspector and markupview tests.
+
+const { Cc, Ci, Cu } = require("chrome");
+const {getRect, getElementFromPoint, getAdjustedQuads} = require("devtools/shared/layout/utils");
+const defer = require("devtools/shared/defer");
+const {Task} = require("devtools/shared/task");
+const {isContentStylesheet} = require("devtools/shared/inspector/css-logic");
+const DOMUtils = Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
+const loader = Cc["@mozilla.org/moz/jssubscript-loader;1"]
+ .getService(Ci.mozIJSSubScriptLoader);
+
+// Set up a dummy environment so that EventUtils works. We need to be careful to
+// pass a window object into each EventUtils method we call rather than having
+// it rely on the |window| global.
+let EventUtils = {};
+EventUtils.window = {};
+EventUtils.parent = {};
+/* eslint-disable camelcase */
+EventUtils._EU_Ci = Components.interfaces;
+EventUtils._EU_Cc = Components.classes;
+/* eslint-disable camelcase */
+loader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/EventUtils.js", EventUtils);
+
+const protocol = require("devtools/shared/protocol");
+const {Arg, RetVal} = protocol;
+
+const dumpn = msg => {
+ dump(msg + "\n");
+};
+
+/**
+ * Get the instance of CanvasFrameAnonymousContentHelper used by a given
+ * highlighter actor.
+ * The instance provides methods to get/set attributes/text/style on nodes of
+ * the highlighter, inserted into the nsCanvasFrame.
+ * @see /devtools/server/actors/highlighters.js
+ * @param {String} actorID
+ */
+function getHighlighterCanvasFrameHelper(conn, actorID) {
+ let actor = conn.getActor(actorID);
+ if (actor && actor._highlighter) {
+ return actor._highlighter.markup;
+ }
+ return null;
+}
+
+var testSpec = protocol.generateActorSpec({
+ typeName: "testActor",
+
+ methods: {
+ getNumberOfElementMatches: {
+ request: {
+ selector: Arg(0, "string"),
+ },
+ response: {
+ value: RetVal("number")
+ }
+ },
+ getHighlighterAttribute: {
+ request: {
+ nodeID: Arg(0, "string"),
+ name: Arg(1, "string"),
+ actorID: Arg(2, "string")
+ },
+ response: {
+ value: RetVal("string")
+ }
+ },
+ getHighlighterNodeTextContent: {
+ request: {
+ nodeID: Arg(0, "string"),
+ actorID: Arg(1, "string")
+ },
+ response: {
+ value: RetVal("string")
+ }
+ },
+ getSelectorHighlighterBoxNb: {
+ request: {
+ highlighter: Arg(0, "string"),
+ },
+ response: {
+ value: RetVal("number")
+ }
+ },
+ changeHighlightedNodeWaitForUpdate: {
+ request: {
+ name: Arg(0, "string"),
+ value: Arg(1, "string"),
+ actorID: Arg(2, "string")
+ },
+ response: {}
+ },
+ waitForHighlighterEvent: {
+ request: {
+ event: Arg(0, "string"),
+ actorID: Arg(1, "string")
+ },
+ response: {}
+ },
+ waitForEventOnNode: {
+ request: {
+ eventName: Arg(0, "string"),
+ selector: Arg(1, "nullable:string")
+ },
+ response: {}
+ },
+ changeZoomLevel: {
+ request: {
+ level: Arg(0, "string"),
+ actorID: Arg(1, "string"),
+ },
+ response: {}
+ },
+ assertElementAtPoint: {
+ request: {
+ x: Arg(0, "number"),
+ y: Arg(1, "number"),
+ selector: Arg(2, "string")
+ },
+ response: {
+ value: RetVal("boolean")
+ }
+ },
+ getAllAdjustedQuads: {
+ request: {
+ selector: Arg(0, "string")
+ },
+ response: {
+ value: RetVal("json")
+ }
+ },
+ synthesizeMouse: {
+ request: {
+ object: Arg(0, "json")
+ },
+ response: {}
+ },
+ synthesizeKey: {
+ request: {
+ args: Arg(0, "json")
+ },
+ response: {}
+ },
+ scrollIntoView: {
+ request: {
+ args: Arg(0, "string")
+ },
+ response: {}
+ },
+ hasPseudoClassLock: {
+ request: {
+ selector: Arg(0, "string"),
+ pseudo: Arg(1, "string")
+ },
+ response: {
+ value: RetVal("boolean")
+ }
+ },
+ loadAndWaitForCustomEvent: {
+ request: {
+ url: Arg(0, "string")
+ },
+ response: {}
+ },
+ hasNode: {
+ request: {
+ selector: Arg(0, "string")
+ },
+ response: {
+ value: RetVal("boolean")
+ }
+ },
+ getBoundingClientRect: {
+ request: {
+ selector: Arg(0, "string"),
+ },
+ response: {
+ value: RetVal("json")
+ }
+ },
+ setProperty: {
+ request: {
+ selector: Arg(0, "string"),
+ property: Arg(1, "string"),
+ value: Arg(2, "string")
+ },
+ response: {}
+ },
+ getProperty: {
+ request: {
+ selector: Arg(0, "string"),
+ property: Arg(1, "string")
+ },
+ response: {
+ value: RetVal("string")
+ }
+ },
+ getAttribute: {
+ request: {
+ selector: Arg(0, "string"),
+ property: Arg(1, "string")
+ },
+ response: {
+ value: RetVal("string")
+ }
+ },
+ setAttribute: {
+ request: {
+ selector: Arg(0, "string"),
+ property: Arg(1, "string"),
+ value: Arg(2, "string")
+ },
+ response: {}
+ },
+ removeAttribute: {
+ request: {
+ selector: Arg(0, "string"),
+ property: Arg(1, "string")
+ },
+ response: {}
+ },
+ reload: {
+ request: {},
+ response: {}
+ },
+ reloadFrame: {
+ request: {
+ selector: Arg(0, "string"),
+ },
+ response: {}
+ },
+ eval: {
+ request: {
+ js: Arg(0, "string")
+ },
+ response: {
+ value: RetVal("nullable:json")
+ }
+ },
+ scrollWindow: {
+ request: {
+ x: Arg(0, "number"),
+ y: Arg(1, "number"),
+ relative: Arg(2, "nullable:boolean"),
+ },
+ response: {
+ value: RetVal("json")
+ }
+ },
+ reflow: {},
+ getNodeRect: {
+ request: {
+ selector: Arg(0, "string")
+ },
+ response: {
+ value: RetVal("json")
+ }
+ },
+ getTextNodeRect: {
+ request: {
+ parentSelector: Arg(0, "string"),
+ childNodeIndex: Arg(1, "number")
+ },
+ response: {
+ value: RetVal("json")
+ }
+ },
+ getNodeInfo: {
+ request: {
+ selector: Arg(0, "string")
+ },
+ response: {
+ value: RetVal("json")
+ }
+ },
+ getStyleSheetsInfoForNode: {
+ request: {
+ selector: Arg(0, "string")
+ },
+ response: {
+ value: RetVal("json")
+ }
+ }
+ }
+});
+
+var TestActor = exports.TestActor = protocol.ActorClassWithSpec(testSpec, {
+ initialize: function (conn, tabActor, options) {
+ this.conn = conn;
+ this.tabActor = tabActor;
+ },
+
+ get content() {
+ return this.tabActor.window;
+ },
+
+ /**
+ * Helper to retrieve a DOM element.
+ * @param {string | array} selector Either a regular selector string
+ * or a selector array. If an array, each item, except the last one
+ * are considered matching an iframe, so that we can query element
+ * within deep iframes.
+ */
+ _querySelector: function (selector) {
+ let document = this.content.document;
+ if (Array.isArray(selector)) {
+ let fullSelector = selector.join(" >> ");
+ while (selector.length > 1) {
+ let str = selector.shift();
+ let iframe = document.querySelector(str);
+ if (!iframe) {
+ throw new Error("Unable to find element with selector \"" + str + "\"" +
+ " (full selector:" + fullSelector + ")");
+ }
+ if (!iframe.contentWindow) {
+ throw new Error("Iframe selector doesn't target an iframe \"" + str + "\"" +
+ " (full selector:" + fullSelector + ")");
+ }
+ document = iframe.contentWindow.document;
+ }
+ selector = selector.shift();
+ }
+ let node = document.querySelector(selector);
+ if (!node) {
+ throw new Error("Unable to find element with selector \"" + selector + "\"");
+ }
+ return node;
+ },
+ /**
+ * Helper to get the number of elements matching a selector
+ * @param {string} CSS selector.
+ */
+ getNumberOfElementMatches: function (selector, root = this.content.document) {
+ return root.querySelectorAll(selector).length;
+ },
+
+ /**
+ * Get a value for a given attribute name, on one of the elements of the box
+ * model highlighter, given its ID.
+ * @param {Object} msg The msg.data part expects the following properties
+ * - {String} nodeID The full ID of the element to get the attribute for
+ * - {String} name The name of the attribute to get
+ * - {String} actorID The highlighter actor ID
+ * @return {String} The value, if found, null otherwise
+ */
+ getHighlighterAttribute: function (nodeID, name, actorID) {
+ let helper = getHighlighterCanvasFrameHelper(this.conn, actorID);
+ if (helper) {
+ return helper.getAttributeForElement(nodeID, name);
+ }
+ return null;
+ },
+
+ /**
+ * Get the textcontent of one of the elements of the box model highlighter,
+ * given its ID.
+ * @param {String} nodeID The full ID of the element to get the attribute for
+ * @param {String} actorID The highlighter actor ID
+ * @return {String} The textcontent value
+ */
+ getHighlighterNodeTextContent: function (nodeID, actorID) {
+ let value;
+ let helper = getHighlighterCanvasFrameHelper(this.conn, actorID);
+ if (helper) {
+ value = helper.getTextContentForElement(nodeID);
+ }
+ return value;
+ },
+
+ /**
+ * Get the number of box-model highlighters created by the SelectorHighlighter
+ * @param {String} actorID The highlighter actor ID
+ * @return {Number} The number of box-model highlighters created, or null if the
+ * SelectorHighlighter was not found.
+ */
+ getSelectorHighlighterBoxNb: function (actorID) {
+ let highlighter = this.conn.getActor(actorID);
+ let {_highlighter: h} = highlighter;
+ if (!h || !h._highlighters) {
+ return null;
+ }
+ return h._highlighters.length;
+ },
+
+ /**
+ * Subscribe to the box-model highlighter's update event, modify an attribute of
+ * the currently highlighted node and send a message when the highlighter has
+ * updated.
+ * @param {String} the name of the attribute to be changed
+ * @param {String} the new value for the attribute
+ * @param {String} actorID The highlighter actor ID
+ */
+ changeHighlightedNodeWaitForUpdate: function (name, value, actorID) {
+ return new Promise(resolve => {
+ let highlighter = this.conn.getActor(actorID);
+ let {_highlighter: h} = highlighter;
+
+ h.once("updated", resolve);
+
+ h.currentNode.setAttribute(name, value);
+ });
+ },
+
+ /**
+ * Subscribe to a given highlighter event and respond when the event is received.
+ * @param {String} event The name of the highlighter event to listen to
+ * @param {String} actorID The highlighter actor ID
+ */
+ waitForHighlighterEvent: function (event, actorID) {
+ let highlighter = this.conn.getActor(actorID);
+ let {_highlighter: h} = highlighter;
+
+ return h.once(event);
+ },
+
+ /**
+ * Wait for a specific event on a node matching the provided selector.
+ * @param {String} eventName The name of the event to listen to
+ * @param {String} selector Optional: css selector of the node which should
+ * trigger the event. If ommitted, target will be the content window
+ */
+ waitForEventOnNode: function (eventName, selector) {
+ return new Promise(resolve => {
+ let node = selector ? this._querySelector(selector) : this.content;
+ node.addEventListener(eventName, function onEvent() {
+ node.removeEventListener(eventName, onEvent);
+ resolve();
+ });
+ });
+ },
+
+ /**
+ * Change the zoom level of the page.
+ * Optionally subscribe to the box-model highlighter's update event and waiting
+ * for it to refresh before responding.
+ * @param {Number} level The new zoom level
+ * @param {String} actorID Optional. The highlighter actor ID
+ */
+ changeZoomLevel: function (level, actorID) {
+ dumpn("Zooming page to " + level);
+ return new Promise(resolve => {
+ if (actorID) {
+ let actor = this.conn.getActor(actorID);
+ let {_highlighter: h} = actor;
+ h.once("updated", resolve);
+ } else {
+ resolve();
+ }
+
+ let docShell = this.content.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShell);
+ docShell.contentViewer.fullZoom = level;
+ });
+ },
+
+ assertElementAtPoint: function (x, y, selector) {
+ let elementAtPoint = getElementFromPoint(this.content.document, x, y);
+ if (!elementAtPoint) {
+ throw new Error("Unable to find element at (" + x + ", " + y + ")");
+ }
+ let node = this._querySelector(selector);
+ return node == elementAtPoint;
+ },
+
+ /**
+ * Get all box-model regions' adjusted boxquads for the given element
+ * @param {String} selector The node selector to target a given element
+ * @return {Object} An object with each property being a box-model region, each
+ * of them being an object with the p1/p2/p3/p4 properties
+ */
+ getAllAdjustedQuads: function (selector) {
+ let regions = {};
+ let node = this._querySelector(selector);
+ for (let boxType of ["content", "padding", "border", "margin"]) {
+ regions[boxType] = getAdjustedQuads(this.content, node, boxType);
+ }
+
+ return regions;
+ },
+
+ /**
+ * Synthesize a mouse event on an element, after ensuring that it is visible
+ * in the viewport. This handler doesn't send a message back. Consumers
+ * should listen to specific events on the inspector/highlighter to know when
+ * the event got synthesized.
+ * @param {String} selector The node selector to get the node target for the event
+ * @param {Number} x
+ * @param {Number} y
+ * @param {Boolean} center If set to true, x/y will be ignored and
+ * synthesizeMouseAtCenter will be used instead
+ * @param {Object} options Other event options
+ */
+ synthesizeMouse: function ({ selector, x, y, center, options }) {
+ let node = this._querySelector(selector);
+ node.scrollIntoView();
+ if (center) {
+ EventUtils.synthesizeMouseAtCenter(node, options, node.ownerDocument.defaultView);
+ } else {
+ EventUtils.synthesizeMouse(node, x, y, options, node.ownerDocument.defaultView);
+ }
+ },
+
+ /**
+ * Synthesize a key event for an element. This handler doesn't send a message
+ * back. Consumers should listen to specific events on the inspector/highlighter
+ * to know when the event got synthesized.
+ */
+ synthesizeKey: function ({key, options, content}) {
+ EventUtils.synthesizeKey(key, options, this.content);
+ },
+
+ /**
+ * Scroll an element into view.
+ * @param {String} selector The selector for the node to scroll into view.
+ */
+ scrollIntoView: function (selector) {
+ let node = this._querySelector(selector);
+ node.scrollIntoView();
+ },
+
+ /**
+ * Check that an element currently has a pseudo-class lock.
+ * @param {String} selector The node selector to get the pseudo-class from
+ * @param {String} pseudo The pseudoclass to check for
+ * @return {Boolean}
+ */
+ hasPseudoClassLock: function (selector, pseudo) {
+ let node = this._querySelector(selector);
+ return DOMUtils.hasPseudoClassLock(node, pseudo);
+ },
+
+ loadAndWaitForCustomEvent: function (url) {
+ return new Promise(resolve => {
+ // Wait for DOMWindowCreated first, as listening on the current outerwindow
+ // doesn't allow receiving test-page-processing-done.
+ this.tabActor.chromeEventHandler.addEventListener("DOMWindowCreated", () => {
+ this.content.addEventListener(
+ "test-page-processing-done", resolve, { once: true }
+ );
+ }, { once: true });
+
+ this.content.location = url;
+ });
+ },
+
+ hasNode: function (selector) {
+ try {
+ // _querySelector throws if the node doesn't exists
+ this._querySelector(selector);
+ return true;
+ } catch (e) {
+ return false;
+ }
+ },
+
+ /**
+ * Get the bounding rect for a given DOM node once.
+ * @param {String} selector selector identifier to select the DOM node
+ * @return {json} the bounding rect info
+ */
+ getBoundingClientRect: function (selector) {
+ let node = this._querySelector(selector);
+ let rect = node.getBoundingClientRect();
+ // DOMRect can't be stringified directly, so return a simple object instead.
+ return {
+ x: rect.x,
+ y: rect.y,
+ width: rect.width,
+ height: rect.height,
+ top: rect.top,
+ right: rect.right,
+ bottom: rect.bottom,
+ left: rect.left
+ };
+ },
+
+ /**
+ * Set a JS property on a DOM Node.
+ * @param {String} selector The node selector
+ * @param {String} property The property name
+ * @param {String} value The attribute value
+ */
+ setProperty: function (selector, property, value) {
+ let node = this._querySelector(selector);
+ node[property] = value;
+ },
+
+ /**
+ * Get a JS property on a DOM Node.
+ * @param {String} selector The node selector
+ * @param {String} property The property name
+ * @return {String} value The attribute value
+ */
+ getProperty: function (selector, property) {
+ let node = this._querySelector(selector);
+ return node[property];
+ },
+
+ /**
+ * Get an attribute on a DOM Node.
+ * @param {String} selector The node selector
+ * @param {String} attribute The attribute name
+ * @return {String} value The attribute value
+ */
+ getAttribute: function (selector, attribute) {
+ let node = this._querySelector(selector);
+ return node.getAttribute(attribute);
+ },
+
+ /**
+ * Set an attribute on a DOM Node.
+ * @param {String} selector The node selector
+ * @param {String} attribute The attribute name
+ * @param {String} value The attribute value
+ */
+ setAttribute: function (selector, attribute, value) {
+ let node = this._querySelector(selector);
+ node.setAttribute(attribute, value);
+ },
+
+ /**
+ * Remove an attribute from a DOM Node.
+ * @param {String} selector The node selector
+ * @param {String} attribute The attribute name
+ */
+ removeAttribute: function (selector, attribute) {
+ let node = this._querySelector(selector);
+ node.removeAttribute(attribute);
+ },
+
+ /**
+ * Reload the content window.
+ */
+ reload: function () {
+ this.content.location.reload();
+ },
+
+ /**
+ * Reload an iframe and wait for its load event.
+ * @param {String} selector The node selector
+ */
+ reloadFrame: function (selector) {
+ let node = this._querySelector(selector);
+
+ let deferred = defer();
+
+ let onLoad = function () {
+ node.removeEventListener("load", onLoad);
+ deferred.resolve();
+ };
+ node.addEventListener("load", onLoad);
+
+ node.contentWindow.location.reload();
+ return deferred.promise;
+ },
+
+ /**
+ * Evaluate a JS string in the context of the content document.
+ * @param {String} js JS string to evaluate
+ * @return {json} The evaluation result
+ */
+ eval: function (js) {
+ // We have to use a sandbox, as CSP prevent us from using eval on apps...
+ let sb = Cu.Sandbox(this.content, { sandboxPrototype: this.content });
+ return Cu.evalInSandbox(js, sb);
+ },
+
+ /**
+ * Scrolls the window to a particular set of coordinates in the document, or
+ * by the given amount if `relative` is set to `true`.
+ *
+ * @param {Number} x
+ * @param {Number} y
+ * @param {Boolean} relative
+ *
+ * @return {Object} An object with x / y properties, representing the number
+ * of pixels that the document has been scrolled horizontally and vertically.
+ */
+ scrollWindow: function (x, y, relative) {
+ if (isNaN(x) || isNaN(y)) {
+ return {};
+ }
+
+ let deferred = defer();
+ this.content.addEventListener("scroll", function onScroll(event) {
+ this.removeEventListener("scroll", onScroll);
+
+ let data = {x: this.content.scrollX, y: this.content.scrollY};
+ deferred.resolve(data);
+ });
+
+ this.content[relative ? "scrollBy" : "scrollTo"](x, y);
+
+ return deferred.promise;
+ },
+
+ /**
+ * Forces the reflow and waits for the next repaint.
+ */
+ reflow: function () {
+ let deferred = defer();
+ this.content.document.documentElement.offsetWidth;
+ this.content.requestAnimationFrame(deferred.resolve);
+
+ return deferred.promise;
+ },
+
+ getNodeRect: Task.async(function* (selector) {
+ let node = this._querySelector(selector);
+ return getRect(this.content, node, this.content);
+ }),
+
+ getTextNodeRect: Task.async(function* (parentSelector, childNodeIndex) {
+ let parentNode = this._querySelector(parentSelector);
+ let node = parentNode.childNodes[childNodeIndex];
+ return getAdjustedQuads(this.content, node)[0].bounds;
+ }),
+
+ /**
+ * Get information about a DOM element, identified by a selector.
+ * @param {String} selector The CSS selector to get the node (can be an array
+ * of selectors to get elements in an iframe).
+ * @return {Object} data Null if selector didn't match any node, otherwise:
+ * - {String} tagName.
+ * - {String} namespaceURI.
+ * - {Number} numChildren The number of children in the element.
+ * - {Array} attributes An array of {name, value, namespaceURI} objects.
+ * - {String} outerHTML.
+ * - {String} innerHTML.
+ * - {String} textContent.
+ */
+ getNodeInfo: function (selector) {
+ let node = this._querySelector(selector);
+ let info = null;
+
+ if (node) {
+ info = {
+ tagName: node.tagName,
+ namespaceURI: node.namespaceURI,
+ numChildren: node.children.length,
+ numNodes: node.childNodes.length,
+ attributes: [...node.attributes].map(({name, value, namespaceURI}) => {
+ return {name, value, namespaceURI};
+ }),
+ outerHTML: node.outerHTML,
+ innerHTML: node.innerHTML,
+ textContent: node.textContent
+ };
+ }
+
+ return info;
+ },
+
+ /**
+ * Get information about the stylesheets which have CSS rules that apply to a given DOM
+ * element, identified by a selector.
+ * @param {String} selector The CSS selector to get the node (can be an array
+ * of selectors to get elements in an iframe).
+ * @return {Array} A list of stylesheet objects, each having the following properties:
+ * - {String} href.
+ * - {Boolean} isContentSheet.
+ */
+ getStyleSheetsInfoForNode: function (selector) {
+ let node = this._querySelector(selector);
+ let domRules = DOMUtils.getCSSStyleRules(node);
+
+ let sheets = [];
+
+ for (let i = 0, n = domRules.Count(); i < n; i++) {
+ let sheet = domRules.GetElementAt(i).parentStyleSheet;
+ sheets.push({
+ href: sheet.href,
+ isContentSheet: isContentStylesheet(sheet)
+ });
+ }
+
+ return sheets;
+ }
+});
+
+var TestActorFront = exports.TestActorFront = protocol.FrontClassWithSpec(testSpec, {
+ initialize: function (client, { testActor }, toolbox) {
+ protocol.Front.prototype.initialize.call(this, client, { actor: testActor });
+ this.manage(this);
+ this.toolbox = toolbox;
+ },
+
+ /**
+ * Zoom the current page to a given level.
+ * @param {Number} level The new zoom level.
+ * @return {Promise} The returned promise will only resolve when the
+ * highlighter has updated to the new zoom level.
+ */
+ zoomPageTo: function (level) {
+ return this.changeZoomLevel(level, this.toolbox.highlighter.actorID);
+ },
+
+ /* eslint-disable max-len */
+ changeHighlightedNodeWaitForUpdate: protocol.custom(function (name, value, highlighter) {
+ /* eslint-enable max-len */
+ return this._changeHighlightedNodeWaitForUpdate(
+ name, value, (highlighter || this.toolbox.highlighter).actorID
+ );
+ }, {
+ impl: "_changeHighlightedNodeWaitForUpdate"
+ }),
+
+ /**
+ * Get the value of an attribute on one of the highlighter's node.
+ * @param {String} nodeID The Id of the node in the highlighter.
+ * @param {String} name The name of the attribute.
+ * @param {Object} highlighter Optional custom highlither to target
+ * @return {String} value
+ */
+ getHighlighterNodeAttribute: function (nodeID, name, highlighter) {
+ return this.getHighlighterAttribute(
+ nodeID, name, (highlighter || this.toolbox.highlighter).actorID
+ );
+ },
+
+ getHighlighterNodeTextContent: protocol.custom(function (nodeID, highlighter) {
+ return this._getHighlighterNodeTextContent(
+ nodeID, (highlighter || this.toolbox.highlighter).actorID
+ );
+ }, {
+ impl: "_getHighlighterNodeTextContent"
+ }),
+
+ /**
+ * Is the highlighter currently visible on the page?
+ */
+ isHighlighting: function () {
+ return this.getHighlighterNodeAttribute("box-model-elements", "hidden")
+ .then(value => value === null);
+ },
+
+ /**
+ * Assert that the box-model highlighter's current position corresponds to the
+ * given node boxquads.
+ * @param {String} selector The node selector to get the boxQuads from
+ * @param {Function} is assertion function to call for equality checks
+ * @param {String} prefix An optional prefix for logging information to the
+ * console.
+ */
+ isNodeCorrectlyHighlighted: Task.async(function* (selector, is, prefix = "") {
+ prefix += (prefix ? " " : "") + selector + " ";
+
+ let boxModel = yield this._getBoxModelStatus();
+ let regions = yield this.getAllAdjustedQuads(selector);
+
+ for (let boxType of ["content", "padding", "border", "margin"]) {
+ let [quad] = regions[boxType];
+ for (let point in boxModel[boxType].points) {
+ is(boxModel[boxType].points[point].x, quad[point].x,
+ prefix + boxType + " point " + point + " x coordinate is correct");
+ is(boxModel[boxType].points[point].y, quad[point].y,
+ prefix + boxType + " point " + point + " y coordinate is correct");
+ }
+ }
+ }),
+
+ /**
+ * Get the current rect of the border region of the box-model highlighter
+ */
+ getSimpleBorderRect: Task.async(function* (toolbox) {
+ let {border} = yield this._getBoxModelStatus(toolbox);
+ let {p1, p2, p4} = border.points;
+
+ return {
+ top: p1.y,
+ left: p1.x,
+ width: p2.x - p1.x,
+ height: p4.y - p1.y
+ };
+ }),
+
+ /**
+ * Get the current positions and visibility of the various box-model highlighter
+ * elements.
+ */
+ _getBoxModelStatus: Task.async(function* () {
+ let isVisible = yield this.isHighlighting();
+
+ let ret = {
+ visible: isVisible
+ };
+
+ for (let region of ["margin", "border", "padding", "content"]) {
+ let points = yield this._getPointsForRegion(region);
+ let visible = yield this._isRegionHidden(region);
+ ret[region] = {points, visible};
+ }
+
+ ret.guides = {};
+ for (let guide of ["top", "right", "bottom", "left"]) {
+ ret.guides[guide] = yield this._getGuideStatus(guide);
+ }
+
+ return ret;
+ }),
+
+ /**
+ * Check that the box-model highlighter is currently highlighting the node matching the
+ * given selector.
+ * @param {String} selector
+ * @return {Boolean}
+ */
+ assertHighlightedNode: Task.async(function* (selector) {
+ let rect = yield this.getNodeRect(selector);
+ return yield this.isNodeRectHighlighted(rect);
+ }),
+
+ /**
+ * Check that the box-model highlighter is currently highlighting the text node that can
+ * be found at a given index within the list of childNodes of a parent element matching
+ * the given selector.
+ * @param {String} parentSelector
+ * @param {Number} childNodeIndex
+ * @return {Boolean}
+ */
+ assertHighlightedTextNode: Task.async(function* (parentSelector, childNodeIndex) {
+ let rect = yield this.getTextNodeRect(parentSelector, childNodeIndex);
+ return yield this.isNodeRectHighlighted(rect);
+ }),
+
+ /**
+ * Check that the box-model highlighter is currently highlighting the given rect.
+ * @param {Object} rect
+ * @return {Boolean}
+ */
+ isNodeRectHighlighted: Task.async(function* ({ left, top, width, height }) {
+ let {visible, border} = yield this._getBoxModelStatus();
+ let points = border.points;
+ if (!visible) {
+ return false;
+ }
+
+ // Check that the node is within the box model
+ let right = left + width;
+ let bottom = top + height;
+
+ // Converts points dictionnary into an array
+ let list = [];
+ for (let i = 1; i <= 4; i++) {
+ let p = points["p" + i];
+ list.push([p.x, p.y]);
+ }
+ points = list;
+
+ // Check that each point of the node is within the box model
+ return isInside([left, top], points) &&
+ isInside([right, top], points) &&
+ isInside([right, bottom], points) &&
+ isInside([left, bottom], points);
+ }),
+
+ /**
+ * Get the coordinate (points attribute) from one of the polygon elements in the
+ * box model highlighter.
+ */
+ _getPointsForRegion: Task.async(function* (region) {
+ let d = yield this.getHighlighterNodeAttribute("box-model-" + region, "d");
+
+ let polygons = d.match(/M[^M]+/g);
+ if (!polygons) {
+ return null;
+ }
+
+ let points = polygons[0].trim().split(" ").map(i => {
+ return i.replace(/M|L/, "").split(",");
+ });
+
+ return {
+ p1: {
+ x: parseFloat(points[0][0]),
+ y: parseFloat(points[0][1])
+ },
+ p2: {
+ x: parseFloat(points[1][0]),
+ y: parseFloat(points[1][1])
+ },
+ p3: {
+ x: parseFloat(points[2][0]),
+ y: parseFloat(points[2][1])
+ },
+ p4: {
+ x: parseFloat(points[3][0]),
+ y: parseFloat(points[3][1])
+ }
+ };
+ }),
+
+ /**
+ * Is a given region polygon element of the box-model highlighter currently
+ * hidden?
+ */
+ _isRegionHidden: Task.async(function* (region) {
+ let value = yield this.getHighlighterNodeAttribute("box-model-" + region, "hidden");
+ return value !== null;
+ }),
+
+ _getGuideStatus: Task.async(function* (location) {
+ let id = "box-model-guide-" + location;
+
+ let hidden = yield this.getHighlighterNodeAttribute(id, "hidden");
+ let x1 = yield this.getHighlighterNodeAttribute(id, "x1");
+ let y1 = yield this.getHighlighterNodeAttribute(id, "y1");
+ let x2 = yield this.getHighlighterNodeAttribute(id, "x2");
+ let y2 = yield this.getHighlighterNodeAttribute(id, "y2");
+
+ return {
+ visible: !hidden,
+ x1: x1,
+ y1: y1,
+ x2: x2,
+ y2: y2
+ };
+ }),
+
+ /**
+ * Get the coordinates of the rectangle that is defined by the 4 guides displayed
+ * in the toolbox box-model highlighter.
+ * @return {Object} Null if at least one guide is hidden. Otherwise an object
+ * with p1, p2, p3, p4 properties being {x, y} objects.
+ */
+ getGuidesRectangle: Task.async(function* () {
+ let tGuide = yield this._getGuideStatus("top");
+ let rGuide = yield this._getGuideStatus("right");
+ let bGuide = yield this._getGuideStatus("bottom");
+ let lGuide = yield this._getGuideStatus("left");
+
+ if (!tGuide.visible || !rGuide.visible || !bGuide.visible || !lGuide.visible) {
+ return null;
+ }
+
+ return {
+ p1: {x: lGuide.x1, y: tGuide.y1},
+ p2: {x: rGuide.x1, y: tGuide. y1},
+ p3: {x: rGuide.x1, y: bGuide.y1},
+ p4: {x: lGuide.x1, y: bGuide.y1}
+ };
+ }),
+
+ waitForHighlighterEvent: protocol.custom(function (event) {
+ return this._waitForHighlighterEvent(event, this.toolbox.highlighter.actorID);
+ }, {
+ impl: "_waitForHighlighterEvent"
+ }),
+
+ /**
+ * Get the "d" attribute value for one of the box-model highlighter's region
+ * <path> elements, and parse it to a list of points.
+ * @param {String} region The box model region name.
+ * @param {Front} highlighter The front of the highlighter.
+ * @return {Object} The object returned has the following form:
+ * - d {String} the d attribute value
+ * - points {Array} an array of all the polygons defined by the path. Each box
+ * is itself an Array of points, themselves being [x,y] coordinates arrays.
+ */
+ getHighlighterRegionPath: Task.async(function* (region, highlighter) {
+ let d = yield this.getHighlighterNodeAttribute(
+ `box-model-${region}`, "d", highlighter
+ );
+ if (!d) {
+ return {d: null};
+ }
+
+ let polygons = d.match(/M[^M]+/g);
+ if (!polygons) {
+ return {d};
+ }
+
+ let points = [];
+ for (let polygon of polygons) {
+ points.push(polygon.trim().split(" ").map(i => {
+ return i.replace(/M|L/, "").split(",");
+ }));
+ }
+
+ return {d, points};
+ })
+});
+
+/**
+ * Check whether a point is included in a polygon.
+ * Taken and tweaked from:
+ * https://github.com/iominh/point-in-polygon-extended/blob/master/src/index.js#L30-L85
+ * @param {Array} point [x,y] coordinates
+ * @param {Array} polygon An array of [x,y] points
+ * @return {Boolean}
+ */
+function isInside(point, polygon) {
+ if (polygon.length === 0) {
+ return false;
+ }
+
+ const n = polygon.length;
+ const newPoints = polygon.slice(0);
+ newPoints.push(polygon[0]);
+ let wn = 0;
+
+ // loop through all edges of the polygon
+ for (let i = 0; i < n; i++) {
+ // Accept points on the edges
+ let r = isLeft(newPoints[i], newPoints[i + 1], point);
+ if (r === 0) {
+ return true;
+ }
+ if (newPoints[i][1] <= point[1]) {
+ if (newPoints[i + 1][1] > point[1] && r > 0) {
+ wn++;
+ }
+ } else if (newPoints[i + 1][1] <= point[1] && r < 0) {
+ wn--;
+ }
+ }
+ if (wn === 0) {
+ dumpn(JSON.stringify(point) + " is outside of " + JSON.stringify(polygon));
+ }
+ // the point is outside only when this winding number wn===0, otherwise it's inside
+ return wn !== 0;
+}
+
+function isLeft(p0, p1, p2) {
+ let l = ((p1[0] - p0[0]) * (p2[1] - p0[1])) -
+ ((p2[0] - p0[0]) * (p1[1] - p0[1]));
+ return l;
+}
diff --git a/devtools/client/shared/test/unit/.eslintrc.js b/devtools/client/shared/test/unit/.eslintrc.js
new file mode 100644
index 000000000..59adf410a
--- /dev/null
+++ b/devtools/client/shared/test/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/client/shared/test/unit/test_VariablesView_filtering-without-controller.js b/devtools/client/shared/test/unit/test_VariablesView_filtering-without-controller.js
new file mode 100644
index 000000000..5f4438234
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_VariablesView_filtering-without-controller.js
@@ -0,0 +1,36 @@
+/* -*- 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";
+
+// Test that VariablesView._doSearch() works even without an attached
+// VariablesViewController (bug 1196341).
+
+var Cu = Components.utils;
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+const DOMParser = Cc["@mozilla.org/xmlextras/domparser;1"]
+ .createInstance(Ci.nsIDOMParser);
+const { VariablesView } =
+ Cu.import("resource://devtools/client/shared/widgets/VariablesView.jsm", {});
+
+function run_test() {
+ let doc = DOMParser.parseFromString("<div>", "text/html");
+ let container = doc.body.firstChild;
+ ok(container, "Got a container.");
+
+ let vv = new VariablesView(container, { searchEnabled: true });
+ let scope = vv.addScope("Test scope");
+ let item1 = scope.addItem("a", { value: "1" });
+ let item2 = scope.addItem("b", { value: "2" });
+
+ do_print("Performing a search without a controller.");
+ vv._doSearch("a");
+
+ equal(item1.target.hasAttribute("unmatched"), false,
+ "First item that matched the filter is visible.");
+ equal(item2.target.hasAttribute("unmatched"), true,
+ "The second item that did not match the filter is hidden.");
+}
diff --git a/devtools/client/shared/test/unit/test_VariablesView_getString_promise.js b/devtools/client/shared/test/unit/test_VariablesView_getString_promise.js
new file mode 100644
index 000000000..a70c870bb
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_VariablesView_getString_promise.js
@@ -0,0 +1,76 @@
+/* -*- 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 { VariablesView } = Components.utils.import("resource://devtools/client/shared/widgets/VariablesView.jsm", {});
+
+const PENDING = {
+ "type": "object",
+ "class": "Promise",
+ "actor": "conn0.pausedobj35",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "promiseState": {
+ "state": "pending"
+ },
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {},
+ "ownPropertiesLength": 0,
+ "safeGetterValues": {}
+ }
+};
+
+const FULFILLED = {
+ "type": "object",
+ "class": "Promise",
+ "actor": "conn0.pausedobj35",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "promiseState": {
+ "state": "fulfilled",
+ "value": 10
+ },
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {},
+ "ownPropertiesLength": 0,
+ "safeGetterValues": {}
+ }
+};
+
+const REJECTED = {
+ "type": "object",
+ "class": "Promise",
+ "actor": "conn0.pausedobj35",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "promiseState": {
+ "state": "rejected",
+ "reason": 10
+ },
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {},
+ "ownPropertiesLength": 0,
+ "safeGetterValues": {}
+ }
+};
+
+function run_test() {
+ equal(VariablesView.getString(PENDING, { concise: true }), "Promise");
+ equal(VariablesView.getString(PENDING), 'Promise {<state>: "pending"}');
+
+ equal(VariablesView.getString(FULFILLED, { concise: true }), "Promise");
+ equal(VariablesView.getString(FULFILLED),
+ 'Promise {<state>: "fulfilled", <value>: 10}');
+
+ equal(VariablesView.getString(REJECTED, { concise: true }), "Promise");
+ equal(VariablesView.getString(REJECTED), 'Promise {<state>: "rejected", <reason>: 10}');
+}
diff --git a/devtools/client/shared/test/unit/test_advanceValidate.js b/devtools/client/shared/test/unit/test_advanceValidate.js
new file mode 100644
index 000000000..2b3122a6f
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_advanceValidate.js
@@ -0,0 +1,31 @@
+/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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";
+
+// Tests the advanceValidate function from rule-view.js.
+
+const {utils: Cu, interfaces: Ci} = Components;
+const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const {advanceValidate} = require("devtools/client/inspector/shared/utils");
+
+// 1 2 3
+// 0123456789012345678901234567890
+const sampleInput = '\\symbol "string" url(somewhere)';
+
+function testInsertion(where, result, testName) {
+ do_print(testName);
+ equal(advanceValidate(Ci.nsIDOMKeyEvent.DOM_VK_SEMICOLON, sampleInput, where),
+ result, "testing advanceValidate at " + where);
+}
+
+function run_test() {
+ testInsertion(4, true, "inside a symbol");
+ testInsertion(1, false, "after a backslash");
+ testInsertion(8, true, "after whitespace");
+ testInsertion(11, false, "inside a string");
+ testInsertion(24, false, "inside a URL");
+ testInsertion(31, true, "at the end");
+}
diff --git a/devtools/client/shared/test/unit/test_attribute-parsing-01.js b/devtools/client/shared/test/unit/test_attribute-parsing-01.js
new file mode 100644
index 000000000..b6b1a301d
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_attribute-parsing-01.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test splitBy from node-attribute-parser.js
+
+const {require} = Components.utils.import("resource://devtools/shared/Loader.jsm", {});
+const {splitBy} = require("devtools/client/shared/node-attribute-parser");
+
+const TEST_DATA = [{
+ value: "this is a test",
+ splitChar: " ",
+ expected: [
+ {value: "this"},
+ {value: " ", type: "string"},
+ {value: "is"},
+ {value: " ", type: "string"},
+ {value: "a"},
+ {value: " ", type: "string"},
+ {value: "test"}
+ ]
+}, {
+ value: "/path/to/handler",
+ splitChar: " ",
+ expected: [
+ {value: "/path/to/handler"}
+ ]
+}, {
+ value: "test",
+ splitChar: " ",
+ expected: [
+ {value: "test"}
+ ]
+}, {
+ value: " test ",
+ splitChar: " ",
+ expected: [
+ {value: " ", type: "string"},
+ {value: "test"},
+ {value: " ", type: "string"}
+ ]
+}, {
+ value: "",
+ splitChar: " ",
+ expected: []
+}, {
+ value: " ",
+ splitChar: " ",
+ expected: [
+ {value: " ", type: "string"},
+ {value: " ", type: "string"},
+ {value: " ", type: "string"}
+ ]
+}];
+
+function run_test() {
+ for (let {value, splitChar, expected} of TEST_DATA) {
+ do_print("Splitting string: " + value);
+ let tokens = splitBy(value, splitChar);
+
+ do_print("Checking that the number of parsed tokens is correct");
+ do_check_eq(tokens.length, expected.length);
+
+ for (let i = 0; i < tokens.length; i++) {
+ do_print("Checking the data in token " + i);
+ do_check_eq(tokens[i].value, expected[i].value);
+ if (expected[i].type) {
+ do_check_eq(tokens[i].type, expected[i].type);
+ }
+ }
+ }
+}
diff --git a/devtools/client/shared/test/unit/test_attribute-parsing-02.js b/devtools/client/shared/test/unit/test_attribute-parsing-02.js
new file mode 100644
index 000000000..2c24d8f05
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_attribute-parsing-02.js
@@ -0,0 +1,134 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test parseAttribute from node-attribute-parser.js
+
+const {require} = Components.utils.import("resource://devtools/shared/Loader.jsm", {});
+const {parseAttribute} = require("devtools/client/shared/node-attribute-parser");
+
+const TEST_DATA = [{
+ tagName: "body",
+ namespaceURI: "http://www.w3.org/1999/xhtml",
+ attributeName: "class",
+ attributeValue: "some css class names",
+ expected: [
+ {value: "some css class names", type: "string"}
+ ]
+}, {
+ tagName: "box",
+ namespaceURI: "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
+ attributeName: "datasources",
+ attributeValue: "/url/1?test=1#test http://mozilla.org/wow",
+ expected: [
+ {value: "/url/1?test=1#test", type: "uri"},
+ {value: " ", type: "string"},
+ {value: "http://mozilla.org/wow", type: "uri"}
+ ]
+}, {
+ tagName: "form",
+ namespaceURI: "http://www.w3.org/1999/xhtml",
+ attributeName: "action",
+ attributeValue: "/path/to/handler",
+ expected: [
+ {value: "/path/to/handler", type: "uri"}
+ ]
+}, {
+ tagName: "a",
+ namespaceURI: "http://www.w3.org/1999/xhtml",
+ attributeName: "ping",
+ attributeValue: "http://analytics.com/track?id=54 http://analytics.com/track?id=55",
+ expected: [
+ {value: "http://analytics.com/track?id=54", type: "uri"},
+ {value: " ", type: "string"},
+ {value: "http://analytics.com/track?id=55", type: "uri"}
+ ]
+}, {
+ tagName: "link",
+ namespaceURI: "http://www.w3.org/1999/xhtml",
+ attributeName: "href",
+ attributeValue: "styles.css",
+ otherAttributes: [{name: "rel", value: "stylesheet"}],
+ expected: [
+ {value: "styles.css", type: "cssresource"}
+ ]
+}, {
+ tagName: "link",
+ namespaceURI: "http://www.w3.org/1999/xhtml",
+ attributeName: "href",
+ attributeValue: "styles.css",
+ expected: [
+ {value: "styles.css", type: "uri"}
+ ]
+}, {
+ tagName: "output",
+ namespaceURI: "http://www.w3.org/1999/xhtml",
+ attributeName: "for",
+ attributeValue: "element-id something id",
+ expected: [
+ {value: "element-id", type: "idref"},
+ {value: " ", type: "string"},
+ {value: "something", type: "idref"},
+ {value: " ", type: "string"},
+ {value: "id", type: "idref"}
+ ]
+}, {
+ tagName: "img",
+ namespaceURI: "http://www.w3.org/1999/xhtml",
+ attributeName: "contextmenu",
+ attributeValue: "id-of-menu",
+ expected: [
+ {value: "id-of-menu", type: "idref"}
+ ]
+}, {
+ tagName: "img",
+ namespaceURI: "http://www.w3.org/1999/xhtml",
+ attributeName: "src",
+ attributeValue: "omg-thats-so-funny.gif",
+ expected: [
+ {value: "omg-thats-so-funny.gif", type: "uri"}
+ ]
+}, {
+ tagName: "key",
+ namespaceURI: "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
+ attributeName: "command",
+ attributeValue: "some_command_id",
+ expected: [
+ {value: "some_command_id", type: "idref"}
+ ]
+}, {
+ tagName: "script",
+ namespaceURI: "whatever",
+ attributeName: "src",
+ attributeValue: "script.js",
+ expected: [
+ {value: "script.js", type: "jsresource"}
+ ]
+}];
+
+function run_test() {
+ for (let {tagName, namespaceURI, attributeName,
+ otherAttributes, attributeValue, expected} of TEST_DATA) {
+ do_print("Testing <" + tagName + " " + attributeName + "='" + attributeValue + "'>");
+
+ let attributes = [
+ ...otherAttributes || [],
+ { name: attributeName, value: attributeValue }
+ ];
+ let tokens = parseAttribute(namespaceURI, tagName, attributes, attributeName);
+ if (!expected) {
+ do_check_true(!tokens);
+ continue;
+ }
+
+ do_print("Checking that the number of parsed tokens is correct");
+ do_check_eq(tokens.length, expected.length);
+
+ for (let i = 0; i < tokens.length; i++) {
+ do_print("Checking the data in token " + i);
+ do_check_eq(tokens[i].value, expected[i].value);
+ do_check_eq(tokens[i].type, expected[i].type);
+ }
+ }
+}
diff --git a/devtools/client/shared/test/unit/test_bezierCanvas.js b/devtools/client/shared/test/unit/test_bezierCanvas.js
new file mode 100644
index 000000000..1decceebb
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_bezierCanvas.js
@@ -0,0 +1,117 @@
+/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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";
+
+// Tests the BezierCanvas API in the CubicBezierWidget module
+
+var Cu = Components.utils;
+var {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+var {CubicBezier, BezierCanvas} = require("devtools/client/shared/widgets/CubicBezierWidget");
+
+function run_test() {
+ offsetsGetterReturnsData();
+ convertsOffsetsToCoordinates();
+ plotsCanvas();
+}
+
+function offsetsGetterReturnsData() {
+ do_print("offsets getter returns an array of 2 offset objects");
+
+ let b = new BezierCanvas(getCanvasMock(), getCubicBezier(), [.25, 0]);
+ let offsets = b.offsets;
+
+ do_check_eq(offsets.length, 2);
+
+ do_check_true("top" in offsets[0]);
+ do_check_true("left" in offsets[0]);
+ do_check_true("top" in offsets[1]);
+ do_check_true("left" in offsets[1]);
+
+ do_check_eq(offsets[0].top, "300px");
+ do_check_eq(offsets[0].left, "0px");
+ do_check_eq(offsets[1].top, "100px");
+ do_check_eq(offsets[1].left, "200px");
+
+ do_print("offsets getter returns data according to current padding");
+
+ b = new BezierCanvas(getCanvasMock(), getCubicBezier(), [0, 0]);
+ offsets = b.offsets;
+
+ do_check_eq(offsets[0].top, "400px");
+ do_check_eq(offsets[0].left, "0px");
+ do_check_eq(offsets[1].top, "0px");
+ do_check_eq(offsets[1].left, "200px");
+}
+
+function convertsOffsetsToCoordinates() {
+ do_print("Converts offsets to coordinates");
+
+ let b = new BezierCanvas(getCanvasMock(), getCubicBezier(), [.25, 0]);
+
+ let coordinates = b.offsetsToCoordinates({style: {
+ left: "0px",
+ top: "0px"
+ }});
+ do_check_eq(coordinates.length, 2);
+ do_check_eq(coordinates[0], 0);
+ do_check_eq(coordinates[1], 1.5);
+
+ coordinates = b.offsetsToCoordinates({style: {
+ left: "0px",
+ top: "300px"
+ }});
+ do_check_eq(coordinates[0], 0);
+ do_check_eq(coordinates[1], 0);
+
+ coordinates = b.offsetsToCoordinates({style: {
+ left: "200px",
+ top: "100px"
+ }});
+ do_check_eq(coordinates[0], 1);
+ do_check_eq(coordinates[1], 1);
+}
+
+function plotsCanvas() {
+ do_print("Plots the curve to the canvas");
+
+ let hasDrawnCurve = false;
+ let b = new BezierCanvas(getCanvasMock(), getCubicBezier(), [.25, 0]);
+ b.ctx.bezierCurveTo = () => {
+ hasDrawnCurve = true;
+ };
+ b.plot();
+
+ do_check_true(hasDrawnCurve);
+}
+
+function getCubicBezier() {
+ return new CubicBezier([0, 0, 1, 1]);
+}
+
+function getCanvasMock(w = 200, h = 400) {
+ return {
+ getContext: function () {
+ return {
+ scale: () => {},
+ translate: () => {},
+ clearRect: () => {},
+ beginPath: () => {},
+ closePath: () => {},
+ moveTo: () => {},
+ lineTo: () => {},
+ stroke: () => {},
+ arc: () => {},
+ fill: () => {},
+ bezierCurveTo: () => {},
+ save: () => {},
+ restore: () => {},
+ setTransform: () => {}
+ };
+ },
+ width: w,
+ height: h
+ };
+}
diff --git a/devtools/client/shared/test/unit/test_cssAngle.js b/devtools/client/shared/test/unit/test_cssAngle.js
new file mode 100644
index 000000000..ecb93bc8f
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_cssAngle.js
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test classifyAngle.
+
+"use strict";
+
+var Cu = Components.utils;
+var {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+
+const {angleUtils} = require("devtools/client/shared/css-angle");
+
+const CLASSIFY_TESTS = [
+ { input: "180deg", output: "deg" },
+ { input: "-180deg", output: "deg" },
+ { input: "180DEG", output: "deg" },
+ { input: "200rad", output: "rad" },
+ { input: "-200rad", output: "rad" },
+ { input: "200RAD", output: "rad" },
+ { input: "0.5grad", output: "grad" },
+ { input: "-0.5grad", output: "grad" },
+ { input: "0.5GRAD", output: "grad" },
+ { input: "0.33turn", output: "turn" },
+ { input: "0.33TURN", output: "turn" },
+ { input: "-0.33turn", output: "turn" }
+];
+
+function run_test() {
+ for (let test of CLASSIFY_TESTS) {
+ let result = angleUtils.classifyAngle(test.input);
+ equal(result, test.output, "test classifyAngle(" + test.input + ")");
+ }
+}
diff --git a/devtools/client/shared/test/unit/test_cssColor-01.js b/devtools/client/shared/test/unit/test_cssColor-01.js
new file mode 100644
index 000000000..13b9b5fa0
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_cssColor-01.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test classifyColor.
+
+"use strict";
+
+var Cu = Components.utils;
+var Ci = Components.interfaces;
+var Cc = Components.classes;
+
+var {require, loader} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const {colorUtils} = require("devtools/shared/css/color");
+
+loader.lazyGetter(this, "DOMUtils", function () {
+ return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
+});
+
+const CLASSIFY_TESTS = [
+ { input: "rgb(255,0,192)", output: "rgb" },
+ { input: "RGB(255,0,192)", output: "rgb" },
+ { input: "RGB(100%,0%,83%)", output: "rgb" },
+ { input: "rgba(255,0,192, 0.25)", output: "rgb" },
+ { input: "hsl(5, 5%, 5%)", output: "hsl" },
+ { input: "hsla(5, 5%, 5%, 0.25)", output: "hsl" },
+ { input: "hSlA(5, 5%, 5%, 0.25)", output: "hsl" },
+ { input: "#f0c", output: "hex" },
+ { input: "#f0c0", output: "hex" },
+ { input: "#fe01cb", output: "hex" },
+ { input: "#fe01cb80", output: "hex" },
+ { input: "#FE01CB", output: "hex" },
+ { input: "#FE01CB80", output: "hex" },
+ { input: "blue", output: "name" },
+ { input: "orange", output: "name" }
+];
+
+function compareWithDomutils(input, isColor) {
+ let ours = colorUtils.colorToRGBA(input);
+ let platform = DOMUtils.colorToRGBA(input);
+ deepEqual(ours, platform, "color " + input + " matches DOMUtils");
+ if (isColor) {
+ ok(ours !== null, "'" + input + "' is a color");
+ } else {
+ ok(ours === null, "'" + input + "' is not a color");
+ }
+}
+
+function run_test() {
+ for (let test of CLASSIFY_TESTS) {
+ let result = colorUtils.classifyColor(test.input);
+ equal(result, test.output, "test classifyColor(" + test.input + ")");
+
+ let obj = new colorUtils.CssColor("purple");
+ obj.setAuthoredUnitFromColor(test.input);
+ equal(obj.colorUnit, test.output,
+ "test setAuthoredUnitFromColor(" + test.input + ")");
+
+ // Check that our implementation matches DOMUtils.
+ compareWithDomutils(test.input, true);
+
+ // And check some obvious errors.
+ compareWithDomutils("mumble" + test.input, false);
+ compareWithDomutils(test.input + "trailingstuff", false);
+ }
+
+ // Regression test for bug 1303826.
+ let black = new colorUtils.CssColor("#000");
+ black.colorUnit = "name";
+ equal(black.toString(), "black", "test non-upper-case color cycling");
+
+ let upper = new colorUtils.CssColor("BLACK");
+ upper.colorUnit = "hex";
+ equal(upper.toString(), "#000", "test upper-case color cycling");
+ upper.colorUnit = "name";
+ equal(upper.toString(), "BLACK", "test upper-case color preservation");
+}
diff --git a/devtools/client/shared/test/unit/test_cssColor-02.js b/devtools/client/shared/test/unit/test_cssColor-02.js
new file mode 100644
index 000000000..c6a039028
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_cssColor-02.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Test color cycling regression - Bug 1303748.
+ *
+ * Values should cycle from a starting value, back to their original values. This can
+ * potentially be a little flaky due to the precision of different color representations.
+ */
+
+const {require} = Components.utils.import("resource://devtools/shared/Loader.jsm", {});
+const {colorUtils} = require("devtools/shared/css/color");
+const getFixtureColorData = require("resource://test/helper_color_data.js");
+
+function run_test() {
+ getFixtureColorData().forEach(({authored, name, hex, hsl, rgb, cycle}) => {
+ if (cycle) {
+ const nameCycled = runCycle(name, cycle);
+ const hexCycled = runCycle(hex, cycle);
+ const hslCycled = runCycle(hsl, cycle);
+ const rgbCycled = runCycle(rgb, cycle);
+ // Cut down on log output by only reporting a single pass/fail for the color.
+ ok(nameCycled && hexCycled && hslCycled && rgbCycled,
+ `${authored} was able to cycle back to the original value`);
+ }
+ });
+}
+
+/**
+ * Test a color cycle to see if a color cycles back to its original value in a fixed
+ * number of steps.
+ *
+ * @param {string} value - The color value, e.g. "#000".
+ * @param {integer) times - The number of times it takes to cycle back to the
+ * original color.
+ */
+function runCycle(value, times) {
+ let color = new colorUtils.CssColor(value);
+ for (let i = 0; i < times; i++) {
+ color.nextColorUnit();
+ color = new colorUtils.CssColor(color.toString());
+ }
+ return color.toString() === value;
+}
diff --git a/devtools/client/shared/test/unit/test_cssColor-03.js b/devtools/client/shared/test/unit/test_cssColor-03.js
new file mode 100644
index 000000000..c3ef5a5c2
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_cssColor-03.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test css-color-4 color function syntax and old-style syntax.
+
+"use strict";
+
+var Cu = Components.utils;
+var Ci = Components.interfaces;
+var Cc = Components.classes;
+
+var {require, loader} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const {colorUtils} = require("devtools/shared/css/color");
+
+loader.lazyGetter(this, "DOMUtils", function () {
+ return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
+});
+
+const OLD_STYLE_TESTS = [
+ "rgb(255,0,192)",
+ "RGB(255,0,192)",
+ "RGB(100%,0%,83%)",
+ "rgba(255,0,192,0.25)",
+ "hsl(120, 100%, 40%)",
+ "hsla(120, 100%, 40%, 0.25)",
+ "hSlA(240, 100%, 50%, 0.25)",
+];
+
+const CSS_COLOR_4_TESTS = [
+ "rgb(255.0,0.0,192.0)",
+ "RGB(255 0 192)",
+ "RGB(100% 0% 83% / 0.5)",
+ "RGB(100%,0%,83%,0.5)",
+ "RGB(100%,0%,83%,50%)",
+ "rgba(255,0,192)",
+ "hsl(50deg,15%,25%)",
+ "hsl(240 25% 33%)",
+ "hsl(50deg 25% 33% / 0.25)",
+ "hsl(60 120% 60% / 0.25)",
+ "hSlA(5turn 40% 4%)",
+];
+
+function run_test() {
+ for (let test of OLD_STYLE_TESTS) {
+ let ours = colorUtils.colorToRGBA(test, true);
+ let platform = DOMUtils.colorToRGBA(test);
+ deepEqual(ours, platform, "color " + test + " matches DOMUtils");
+ ok(ours !== null, "'" + test + "' is a color");
+ }
+
+ for (let test of CSS_COLOR_4_TESTS) {
+ let oursOld = colorUtils.colorToRGBA(test, true);
+ let oursNew = colorUtils.colorToRGBA(test, false);
+ let platform = DOMUtils.colorToRGBA(test);
+ notEqual(oursOld, platform, "old style parser for color " + test +
+ " should not match DOMUtils");
+ ok(oursOld === null, "'" + test + "' is not a color with old parser");
+ deepEqual(oursNew, platform, `css-color-4 parser for color ${test} matches DOMUtils`);
+ ok(oursNew !== null, "'" + test + "' is a color with css-color-4 parser");
+ }
+}
diff --git a/devtools/client/shared/test/unit/test_cssColorDatabase.js b/devtools/client/shared/test/unit/test_cssColorDatabase.js
new file mode 100644
index 000000000..eb6363ba4
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_cssColorDatabase.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that css-color-db matches platform.
+
+"use strict";
+
+var Cu = Components.utils;
+var Ci = Components.interfaces;
+var Cc = Components.classes;
+
+var {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+
+const DOMUtils = Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
+
+const {colorUtils} = require("devtools/shared/css/color");
+const {cssColors} = require("devtools/shared/css/color-db");
+
+function isValid(colorName) {
+ ok(colorUtils.isValidCSSColor(colorName),
+ colorName + " is valid in database");
+ ok(DOMUtils.isValidCSSColor(colorName),
+ colorName + " is valid in DOMUtils");
+}
+
+function checkOne(colorName, checkName) {
+ let ours = colorUtils.colorToRGBA(colorName);
+ let fromDom = DOMUtils.colorToRGBA(colorName);
+ deepEqual(ours, fromDom, colorName + " agrees with DOMUtils");
+
+ isValid(colorName);
+
+ if (checkName) {
+ let {r, g, b} = ours;
+
+ // The color we got might not map back to the same name; but our
+ // implementation should agree with DOMUtils about which name is
+ // canonical.
+ let ourName = colorUtils.rgbToColorName(r, g, b);
+ let domName = DOMUtils.rgbToColorName(r, g, b);
+
+ equal(ourName, domName,
+ colorName + " canonical name agrees with DOMUtils");
+ }
+}
+
+function run_test() {
+ for (let name in cssColors) {
+ checkOne(name, true);
+ }
+ checkOne("transparent", false);
+
+ // Now check that platform didn't add a new name when we weren't
+ // looking.
+ let names = DOMUtils.getCSSValuesForProperty("background-color");
+ for (let name of names) {
+ if (name !== "hsl" && name !== "hsla" &&
+ name !== "rgb" && name !== "rgba" &&
+ name !== "inherit" && name !== "initial" && name !== "unset") {
+ checkOne(name, true);
+ }
+ }
+}
diff --git a/devtools/client/shared/test/unit/test_cubicBezier.js b/devtools/client/shared/test/unit/test_cubicBezier.js
new file mode 100644
index 000000000..9ed6c4eb1
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_cubicBezier.js
@@ -0,0 +1,146 @@
+/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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";
+
+// Tests the CubicBezier API in the CubicBezierWidget module
+
+var Cu = Components.utils;
+var {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+var {CubicBezier, _parseTimingFunction} = require("devtools/client/shared/widgets/CubicBezierWidget");
+
+function run_test() {
+ throwsWhenMissingCoordinates();
+ throwsWhenIncorrectCoordinates();
+ convertsStringCoordinates();
+ coordinatesToStringOutputsAString();
+ pointGettersReturnPointCoordinatesArrays();
+ toStringOutputsCubicBezierValue();
+ toStringOutputsCssPresetValues();
+ testParseTimingFunction();
+}
+
+function throwsWhenMissingCoordinates() {
+ do_check_throws(() => {
+ new CubicBezier();
+ }, "Throws an exception when coordinates are missing");
+}
+
+function throwsWhenIncorrectCoordinates() {
+ do_check_throws(() => {
+ new CubicBezier([]);
+ }, "Throws an exception when coordinates are incorrect (empty array)");
+
+ do_check_throws(() => {
+ new CubicBezier([0, 0]);
+ }, "Throws an exception when coordinates are incorrect (incomplete array)");
+
+ do_check_throws(() => {
+ new CubicBezier(["a", "b", "c", "d"]);
+ }, "Throws an exception when coordinates are incorrect (invalid type)");
+
+ do_check_throws(() => {
+ new CubicBezier([1.5, 0, 1.5, 0]);
+ }, "Throws an exception when coordinates are incorrect (time range invalid)");
+
+ do_check_throws(() => {
+ new CubicBezier([-0.5, 0, -0.5, 0]);
+ }, "Throws an exception when coordinates are incorrect (time range invalid)");
+}
+
+function convertsStringCoordinates() {
+ do_print("Converts string coordinates to numbers");
+ let c = new CubicBezier(["0", "1", ".5", "-2"]);
+
+ do_check_eq(c.coordinates[0], 0);
+ do_check_eq(c.coordinates[1], 1);
+ do_check_eq(c.coordinates[2], .5);
+ do_check_eq(c.coordinates[3], -2);
+}
+
+function coordinatesToStringOutputsAString() {
+ do_print("coordinates.toString() outputs a string representation");
+
+ let c = new CubicBezier(["0", "1", "0.5", "-2"]);
+ let string = c.coordinates.toString();
+ do_check_eq(string, "0,1,.5,-2");
+
+ c = new CubicBezier([1, 1, 1, 1]);
+ string = c.coordinates.toString();
+ do_check_eq(string, "1,1,1,1");
+}
+
+function pointGettersReturnPointCoordinatesArrays() {
+ do_print("Points getters return arrays of coordinates");
+
+ let c = new CubicBezier([0, .2, .5, 1]);
+ do_check_eq(c.P1[0], 0);
+ do_check_eq(c.P1[1], .2);
+ do_check_eq(c.P2[0], .5);
+ do_check_eq(c.P2[1], 1);
+}
+
+function toStringOutputsCubicBezierValue() {
+ do_print("toString() outputs the cubic-bezier() value");
+
+ let c = new CubicBezier([0, 1, 1, 0]);
+ do_check_eq(c.toString(), "cubic-bezier(0,1,1,0)");
+}
+
+function toStringOutputsCssPresetValues() {
+ do_print("toString() outputs the css predefined values");
+
+ let c = new CubicBezier([0, 0, 1, 1]);
+ do_check_eq(c.toString(), "linear");
+
+ c = new CubicBezier([0.25, 0.1, 0.25, 1]);
+ do_check_eq(c.toString(), "ease");
+
+ c = new CubicBezier([0.42, 0, 1, 1]);
+ do_check_eq(c.toString(), "ease-in");
+
+ c = new CubicBezier([0, 0, 0.58, 1]);
+ do_check_eq(c.toString(), "ease-out");
+
+ c = new CubicBezier([0.42, 0, 0.58, 1]);
+ do_check_eq(c.toString(), "ease-in-out");
+}
+
+function testParseTimingFunction() {
+ do_print("test parseTimingFunction");
+
+ for (let test of ["ease", "linear", "ease-in", "ease-out", "ease-in-out"]) {
+ ok(_parseTimingFunction(test), test);
+ }
+
+ ok(!_parseTimingFunction("something"), "non-function token");
+ ok(!_parseTimingFunction("something()"), "non-cubic-bezier function");
+ ok(!_parseTimingFunction("cubic-bezier(something)",
+ "cubic-bezier with non-numeric argument"));
+ ok(!_parseTimingFunction("cubic-bezier(1,2,3:7)",
+ "did not see comma"));
+ ok(!_parseTimingFunction("cubic-bezier(1,2,3,7:",
+ "did not see close paren"));
+ ok(!_parseTimingFunction("cubic-bezier(1,2", "early EOF after number"));
+ ok(!_parseTimingFunction("cubic-bezier(1,2,", "early EOF after comma"));
+ deepEqual(_parseTimingFunction("cubic-bezier(1,2,3,7)"), [1, 2, 3, 7],
+ "correct invocation");
+ deepEqual(_parseTimingFunction("cubic-bezier(1, /* */ 2,3, 7 )"),
+ [1, 2, 3, 7],
+ "correct with comments and whitespace");
+}
+
+function do_check_throws(cb, info) {
+ do_print(info);
+
+ let hasThrown = false;
+ try {
+ cb();
+ } catch (e) {
+ hasThrown = true;
+ }
+
+ do_check_true(hasThrown);
+}
diff --git a/devtools/client/shared/test/unit/test_escapeCSSComment.js b/devtools/client/shared/test/unit/test_escapeCSSComment.js
new file mode 100644
index 000000000..19d8a2902
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_escapeCSSComment.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var Cu = Components.utils;
+const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const {escapeCSSComment, _unescapeCSSComment} = require("devtools/shared/css/parsing-utils");
+
+const TEST_DATA = [
+ {
+ input: "simple",
+ expected: "simple"
+ },
+ {
+ input: "/* comment */",
+ expected: "/\\* comment *\\/"
+ },
+ {
+ input: "/* two *//* comments */",
+ expected: "/\\* two *\\//\\* comments *\\/"
+ },
+ {
+ input: "/* nested /\\* comment *\\/ */",
+ expected: "/\\* nested /\\\\* comment *\\\\/ *\\/",
+ }
+];
+
+function run_test() {
+ let i = 0;
+ for (let test of TEST_DATA) {
+ ++i;
+ do_print("Test #" + i);
+
+ let escaped = escapeCSSComment(test.input);
+ equal(escaped, test.expected);
+ let unescaped = _unescapeCSSComment(escaped);
+ equal(unescaped, test.input);
+ }
+}
diff --git a/devtools/client/shared/test/unit/test_parseDeclarations.js b/devtools/client/shared/test/unit/test_parseDeclarations.js
new file mode 100644
index 000000000..d400a5359
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_parseDeclarations.js
@@ -0,0 +1,439 @@
+/* -*- 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 {require} = Components.utils.import("resource://devtools/shared/Loader.jsm", {});
+const {parseDeclarations, _parseCommentDeclarations} = require("devtools/shared/css/parsing-utils");
+const {isCssPropertyKnown} = require("devtools/server/actors/css-properties");
+
+const TEST_DATA = [
+ // Simple test
+ {
+ input: "p:v;",
+ expected: [{name: "p", value: "v", priority: "", offsets: [0, 4]}]
+ },
+ // Simple test
+ {
+ input: "this:is;a:test;",
+ expected: [
+ {name: "this", value: "is", priority: "", offsets: [0, 8]},
+ {name: "a", value: "test", priority: "", offsets: [8, 15]}
+ ]
+ },
+ // Test a single declaration with semi-colon
+ {
+ input: "name:value;",
+ expected: [{name: "name", value: "value", priority: "", offsets: [0, 11]}]
+ },
+ // Test a single declaration without semi-colon
+ {
+ input: "name:value",
+ expected: [{name: "name", value: "value", priority: "", offsets: [0, 10]}]
+ },
+ // Test multiple declarations separated by whitespaces and carriage
+ // returns and tabs
+ {
+ input: "p1 : v1 ; \t\t \n p2:v2; \n\n\n\n\t p3 : v3;",
+ expected: [
+ {name: "p1", value: "v1", priority: "", offsets: [0, 9]},
+ {name: "p2", value: "v2", priority: "", offsets: [16, 22]},
+ {name: "p3", value: "v3", priority: "", offsets: [32, 45]},
+ ]
+ },
+ // Test simple priority
+ {
+ input: "p1: v1; p2: v2 !important;",
+ expected: [
+ {name: "p1", value: "v1", priority: "", offsets: [0, 7]},
+ {name: "p2", value: "v2", priority: "important", offsets: [8, 26]}
+ ]
+ },
+ // Test simple priority
+ {
+ input: "p1: v1 !important; p2: v2",
+ expected: [
+ {name: "p1", value: "v1", priority: "important", offsets: [0, 18]},
+ {name: "p2", value: "v2", priority: "", offsets: [19, 25]}
+ ]
+ },
+ // Test simple priority
+ {
+ input: "p1: v1 ! important; p2: v2 ! important;",
+ expected: [
+ {name: "p1", value: "v1", priority: "important", offsets: [0, 20]},
+ {name: "p2", value: "v2", priority: "important", offsets: [21, 40]}
+ ]
+ },
+ // Test invalid priority
+ {
+ input: "p1: v1 important;",
+ expected: [
+ {name: "p1", value: "v1 important", priority: "", offsets: [0, 17]}
+ ]
+ },
+ // Test various types of background-image urls
+ {
+ input: "background-image: url(../../relative/image.png)",
+ expected: [{
+ name: "background-image",
+ value: "url(../../relative/image.png)",
+ priority: "",
+ offsets: [0, 47]
+ }]
+ },
+ {
+ input: "background-image: url(http://site.com/test.png)",
+ expected: [{
+ name: "background-image",
+ value: "url(http://site.com/test.png)",
+ priority: "",
+ offsets: [0, 47]
+ }]
+ },
+ {
+ input: "background-image: url(wow.gif)",
+ expected: [{
+ name: "background-image",
+ value: "url(wow.gif)",
+ priority: "",
+ offsets: [0, 30]
+ }]
+ },
+ // Test that urls with :;{} characters in them are parsed correctly
+ {
+ input: "background: red url(\"http://site.com/image{}:;.png?id=4#wat\") "
+ + "repeat top right",
+ expected: [{
+ name: "background",
+ value: "red url(\"http://site.com/image{}:;.png?id=4#wat\") " +
+ "repeat top right",
+ priority: "",
+ offsets: [0, 78]
+ }]
+ },
+ // Test that an empty string results in an empty array
+ {input: "", expected: []},
+ // Test that a string comprised only of whitespaces results in an empty array
+ {input: " \n \n \n \n \t \t\t\t ", expected: []},
+ // Test that a null input throws an exception
+ {input: null, throws: true},
+ // Test that a undefined input throws an exception
+ {input: undefined, throws: true},
+ // Test that :;{} characters in quoted content are not parsed as multiple
+ // declarations
+ {
+ input: "content: \";color:red;}selector{color:yellow;\"",
+ expected: [{
+ name: "content",
+ value: "\";color:red;}selector{color:yellow;\"",
+ priority: "",
+ offsets: [0, 45]
+ }]
+ },
+ // Test that rules aren't parsed, just declarations. So { and } found after a
+ // property name should be part of the property name, same for values.
+ {
+ input: "body {color:red;} p {color: blue;}",
+ expected: [
+ {name: "body {color", value: "red", priority: "", offsets: [0, 16]},
+ {name: "} p {color", value: "blue", priority: "", offsets: [16, 33]},
+ {name: "}", value: "", priority: "", offsets: [33, 34]}
+ ]
+ },
+ // Test unbalanced : and ;
+ {
+ input: "color :red : font : arial;",
+ expected: [
+ {name: "color", value: "red : font : arial", priority: "",
+ offsets: [0, 26]}
+ ]
+ },
+ {
+ input: "background: red;;;;;",
+ expected: [{name: "background", value: "red", priority: "",
+ offsets: [0, 16]}]
+ },
+ {
+ input: "background:;",
+ expected: [{name: "background", value: "", priority: "",
+ offsets: [0, 12]}]
+ },
+ {input: ";;;;;", expected: []},
+ {input: ":;:;", expected: []},
+ // Test name only
+ {input: "color", expected: [
+ {name: "color", value: "", priority: "", offsets: [0, 5]}
+ ]},
+ // Test trailing name without :
+ {input: "color:blue;font", expected: [
+ {name: "color", value: "blue", priority: "", offsets: [0, 11]},
+ {name: "font", value: "", priority: "", offsets: [11, 15]}
+ ]},
+ // Test trailing name with :
+ {input: "color:blue;font:", expected: [
+ {name: "color", value: "blue", priority: "", offsets: [0, 11]},
+ {name: "font", value: "", priority: "", offsets: [11, 16]}
+ ]},
+ // Test leading value
+ {input: "Arial;color:blue;", expected: [
+ {name: "", value: "Arial", priority: "", offsets: [0, 6]},
+ {name: "color", value: "blue", priority: "", offsets: [6, 17]}
+ ]},
+ // Test hex colors
+ {
+ input: "color: #333",
+ expected: [{name: "color", value: "#333", priority: "", offsets: [0, 11]}]
+ },
+ {
+ input: "color: #456789",
+ expected: [{name: "color", value: "#456789", priority: "",
+ offsets: [0, 14]}]
+ },
+ {
+ input: "wat: #XYZ",
+ expected: [{name: "wat", value: "#XYZ", priority: "", offsets: [0, 9]}]
+ },
+ // Test string/url quotes escaping
+ {
+ input: "content: \"this is a 'string'\"",
+ expected: [{name: "content", value: "\"this is a 'string'\"", priority: "",
+ offsets: [0, 29]}]
+ },
+ {
+ input: 'content: "this is a \\"string\\""',
+ expected: [{
+ name: "content",
+ value: '"this is a \\"string\\""',
+ priority: "",
+ offsets: [0, 31]}]
+ },
+ {
+ input: "content: 'this is a \"string\"'",
+ expected: [{
+ name: "content",
+ value: '\'this is a "string"\'',
+ priority: "",
+ offsets: [0, 29]
+ }]
+ },
+ {
+ input: "content: 'this is a \\'string\\''",
+ expected: [{
+ name: "content",
+ value: "'this is a \\'string\\''",
+ priority: "",
+ offsets: [0, 31],
+ }]
+ },
+ {
+ input: "content: 'this \\' is a \" really strange string'",
+ expected: [{
+ name: "content",
+ value: "'this \\' is a \" really strange string'",
+ priority: "",
+ offsets: [0, 47]
+ }]
+ },
+ {
+ input: "content: \"a not s\\ o very long title\"",
+ expected: [{
+ name: "content",
+ value: '"a not s\\ o very long title"',
+ priority: "",
+ offsets: [0, 46]
+ }]
+ },
+ // Test calc with nested parentheses
+ {
+ input: "width: calc((100% - 3em) / 2)",
+ expected: [{name: "width", value: "calc((100% - 3em) / 2)", priority: "",
+ offsets: [0, 29]}]
+ },
+
+ // Simple embedded comment test.
+ {
+ parseComments: true,
+ input: "width: 5; /* background: green; */ background: red;",
+ expected: [{name: "width", value: "5", priority: "", offsets: [0, 9]},
+ {name: "background", value: "green", priority: "",
+ offsets: [13, 31], commentOffsets: [10, 34]},
+ {name: "background", value: "red", priority: "",
+ offsets: [35, 51]}]
+ },
+
+ // Embedded comment where the parsing heuristic fails.
+ {
+ parseComments: true,
+ input: "width: 5; /* background something: green; */ background: red;",
+ expected: [{name: "width", value: "5", priority: "", offsets: [0, 9]},
+ {name: "background", value: "red", priority: "",
+ offsets: [45, 61]}]
+ },
+
+ // Embedded comment where the parsing heuristic is a bit funny.
+ {
+ parseComments: true,
+ input: "width: 5; /* background: */ background: red;",
+ expected: [{name: "width", value: "5", priority: "", offsets: [0, 9]},
+ {name: "background", value: "", priority: "",
+ offsets: [13, 24], commentOffsets: [10, 27]},
+ {name: "background", value: "red", priority: "",
+ offsets: [28, 44]}]
+ },
+
+ // Another case where the parsing heuristic says not to bother; note
+ // that there is no ";" in the comment.
+ {
+ parseComments: true,
+ input: "width: 5; /* background: yellow */ background: red;",
+ expected: [{name: "width", value: "5", priority: "", offsets: [0, 9]},
+ {name: "background", value: "yellow", priority: "",
+ offsets: [13, 31], commentOffsets: [10, 34]},
+ {name: "background", value: "red", priority: "",
+ offsets: [35, 51]}]
+ },
+
+ // Parsing a comment should yield text that has been unescaped, and
+ // the offsets should refer to the original text.
+ {
+ parseComments: true,
+ input: "/* content: '*\\/'; */",
+ expected: [{name: "content", value: "'*/'", priority: "",
+ offsets: [3, 18], commentOffsets: [0, 21]}]
+ },
+
+ // Parsing a comment should yield text that has been unescaped, and
+ // the offsets should refer to the original text. This variant
+ // tests the no-semicolon path.
+ {
+ parseComments: true,
+ input: "/* content: '*\\/' */",
+ expected: [{name: "content", value: "'*/'", priority: "",
+ offsets: [3, 17], commentOffsets: [0, 20]}]
+ },
+
+ // A comment-in-a-comment should yield the correct offsets.
+ {
+ parseComments: true,
+ input: "/* color: /\\* comment *\\/ red; */",
+ expected: [{name: "color", value: "red", priority: "",
+ offsets: [3, 30], commentOffsets: [0, 33]}]
+ },
+
+ // HTML comments are ignored.
+ {
+ parseComments: true,
+ input: "<!-- color: red; --> color: blue;",
+ expected: [{name: "color", value: "red", priority: "",
+ offsets: [5, 16]},
+ {name: "color", value: "blue", priority: "",
+ offsets: [21, 33]}]
+ },
+
+ // Don't error on an empty comment.
+ {
+ parseComments: true,
+ input: "/**/",
+ expected: []
+ },
+
+ // Parsing our special comments skips the name-check heuristic.
+ {
+ parseComments: true,
+ input: "/*! walrus: zebra; */",
+ expected: [{name: "walrus", value: "zebra", priority: "",
+ offsets: [4, 18], commentOffsets: [0, 21]}]
+ },
+
+ // Regression test for bug 1287620.
+ {
+ input: "color: blue \\9 no\\_need",
+ expected: [{name: "color", value: "blue \\9 no_need", priority: "", offsets: [0, 23]}]
+ },
+
+ // Regression test for bug 1297890 - don't paste tokens.
+ {
+ parseComments: true,
+ input: "stroke-dasharray: 1/*ThisIsAComment*/2;",
+ expected: [{name: "stroke-dasharray", value: "1 2", priority: "", offsets: [0, 39]}]
+ },
+];
+
+function run_test() {
+ run_basic_tests();
+ run_comment_tests();
+}
+
+// Test parseDeclarations.
+function run_basic_tests() {
+ for (let test of TEST_DATA) {
+ do_print("Test input string " + test.input);
+ let output;
+ try {
+ output = parseDeclarations(isCssPropertyKnown, test.input,
+ test.parseComments);
+ } catch (e) {
+ do_print("parseDeclarations 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) {
+ assertOutput(output, test.expected);
+ }
+ }
+}
+
+const COMMENT_DATA = [
+ {
+ input: "content: 'hi",
+ expected: [{name: "content", value: "'hi", priority: "", terminator: "';",
+ offsets: [2, 14], colonOffsets: [9, 11],
+ commentOffsets: [0, 16]}],
+ },
+ {
+ input: "text that once confounded the parser;",
+ expected: []
+ },
+];
+
+// Test parseCommentDeclarations.
+function run_comment_tests() {
+ for (let test of COMMENT_DATA) {
+ do_print("Test input string " + test.input);
+ let output = _parseCommentDeclarations(isCssPropertyKnown, test.input, 0,
+ test.input.length + 4);
+ deepEqual(output, test.expected);
+ }
+}
+
+function assertOutput(actual, expected) {
+ if (actual.length === expected.length) {
+ for (let i = 0; i < expected.length; i++) {
+ do_check_true(!!actual[i]);
+ do_print("Check that the output item has the expected name, " +
+ "value and priority");
+ do_check_eq(expected[i].name, actual[i].name);
+ do_check_eq(expected[i].value, actual[i].value);
+ do_check_eq(expected[i].priority, actual[i].priority);
+ deepEqual(expected[i].offsets, actual[i].offsets);
+ if ("commentOffsets" in expected[i]) {
+ deepEqual(expected[i].commentOffsets, actual[i].commentOffsets);
+ }
+ }
+ } else {
+ for (let prop of actual) {
+ do_print("Actual output contained: {name: " + prop.name + ", value: " +
+ prop.value + ", priority: " + prop.priority + "}");
+ }
+ do_check_eq(actual.length, expected.length);
+ }
+}
diff --git a/devtools/client/shared/test/unit/test_parsePseudoClassesAndAttributes.js b/devtools/client/shared/test/unit/test_parsePseudoClassesAndAttributes.js
new file mode 100644
index 000000000..ccd778c4a
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_parsePseudoClassesAndAttributes.js
@@ -0,0 +1,213 @@
+/* -*- 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";
+
+var Cu = Components.utils;
+const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const {
+ parsePseudoClassesAndAttributes,
+ SELECTOR_ATTRIBUTE,
+ SELECTOR_ELEMENT,
+ SELECTOR_PSEUDO_CLASS
+} = require("devtools/shared/css/parsing-utils");
+
+const TEST_DATA = [
+ // Test that a null input throws an exception
+ {
+ input: null,
+ throws: true
+ },
+ // Test that a undefined input throws an exception
+ {
+ input: undefined,
+ throws: true
+ },
+ {
+ input: ":root",
+ expected: [
+ { value: ":root", type: SELECTOR_PSEUDO_CLASS }
+ ]
+ },
+ {
+ input: ".testclass",
+ expected: [
+ { value: ".testclass", type: SELECTOR_ELEMENT }
+ ]
+ },
+ {
+ input: "div p",
+ expected: [
+ { value: "div p", type: SELECTOR_ELEMENT }
+ ]
+ },
+ {
+ input: "div > p",
+ expected: [
+ { value: "div > p", type: SELECTOR_ELEMENT }
+ ]
+ },
+ {
+ input: "a[hidden]",
+ expected: [
+ { value: "a", type: SELECTOR_ELEMENT },
+ { value: "[hidden]", type: SELECTOR_ATTRIBUTE }
+ ]
+ },
+ {
+ input: "a[hidden=true]",
+ expected: [
+ { value: "a", type: SELECTOR_ELEMENT },
+ { value: "[hidden=true]", type: SELECTOR_ATTRIBUTE }
+ ]
+ },
+ {
+ input: "a[hidden=true] p:hover",
+ expected: [
+ { value: "a", type: SELECTOR_ELEMENT },
+ { value: "[hidden=true]", type: SELECTOR_ATTRIBUTE },
+ { value: " p", type: SELECTOR_ELEMENT },
+ { value: ":hover", type: SELECTOR_PSEUDO_CLASS }
+ ]
+ },
+ {
+ input: "a[checked=\"true\"]",
+ expected: [
+ { value: "a", type: SELECTOR_ELEMENT },
+ { value: "[checked=\"true\"]", type: SELECTOR_ATTRIBUTE }
+ ]
+ },
+ {
+ input: "a[title~=test]",
+ expected: [
+ { value: "a", type: SELECTOR_ELEMENT },
+ { value: "[title~=test]", type: SELECTOR_ATTRIBUTE }
+ ]
+ },
+ {
+ input: "h1[hidden=\"true\"][title^=\"Important\"]",
+ expected: [
+ { value: "h1", type: SELECTOR_ELEMENT },
+ { value: "[hidden=\"true\"]", type: SELECTOR_ATTRIBUTE },
+ { value: "[title^=\"Important\"]", type: SELECTOR_ATTRIBUTE}
+ ]
+ },
+ {
+ input: "p:hover",
+ expected: [
+ { value: "p", type: SELECTOR_ELEMENT },
+ { value: ":hover", type: SELECTOR_PSEUDO_CLASS }
+ ]
+ },
+ {
+ input: "p + .testclass:hover",
+ expected: [
+ { value: "p + .testclass", type: SELECTOR_ELEMENT },
+ { value: ":hover", type: SELECTOR_PSEUDO_CLASS }
+ ]
+ },
+ {
+ input: "p::before",
+ expected: [
+ { value: "p", type: SELECTOR_ELEMENT },
+ { value: "::before", type: SELECTOR_PSEUDO_CLASS }
+ ]
+ },
+ {
+ input: "p:nth-child(2)",
+ expected: [
+ { value: "p", type: SELECTOR_ELEMENT },
+ { value: ":nth-child(2)", type: SELECTOR_PSEUDO_CLASS }
+ ]
+ },
+ {
+ input: "p:not([title=\"test\"]) .testclass",
+ expected: [
+ { value: "p", type: SELECTOR_ELEMENT },
+ { value: ":not([title=\"test\"])", type: SELECTOR_PSEUDO_CLASS },
+ { value: " .testclass", type: SELECTOR_ELEMENT }
+ ]
+ },
+ {
+ input: "a\\:hover",
+ expected: [
+ { value: "a\\:hover", type: SELECTOR_ELEMENT }
+ ]
+ },
+ {
+ input: ":not(:lang(it))",
+ expected: [
+ { value: ":not(:lang(it))", type: SELECTOR_PSEUDO_CLASS }
+ ]
+ },
+ {
+ input: "p:not(:lang(it))",
+ expected: [
+ { value: "p", type: SELECTOR_ELEMENT },
+ { value: ":not(:lang(it))", type: SELECTOR_PSEUDO_CLASS }
+ ]
+ },
+ {
+ input: "p:not(p:lang(it))",
+ expected: [
+ { value: "p", type: SELECTOR_ELEMENT },
+ { value: ":not(p:lang(it))", type: SELECTOR_PSEUDO_CLASS }
+ ]
+ },
+ {
+ input: ":not(:lang(it)",
+ expected: [
+ { value: ":not(:lang(it)", type: SELECTOR_ELEMENT }
+ ]
+ },
+ {
+ input: ":not(:lang(it)))",
+ expected: [
+ { value: ":not(:lang(it))", type: SELECTOR_PSEUDO_CLASS },
+ { value: ")", type: SELECTOR_ELEMENT }
+ ]
+ }
+];
+
+function run_test() {
+ for (let test of TEST_DATA) {
+ dump("Test input string " + test.input + "\n");
+ let output;
+
+ try {
+ output = parsePseudoClassesAndAttributes(test.input);
+ } catch (e) {
+ dump("parsePseudoClassesAndAttributes threw an exception with the " +
+ "given input string\n");
+ if (test.throws) {
+ ok(true, "Exception expected");
+ } else {
+ dump();
+ ok(false, "Exception unexpected\n" + e);
+ }
+ }
+
+ if (output) {
+ assertOutput(output, test.expected);
+ }
+ }
+}
+
+function assertOutput(actual, expected) {
+ if (actual.length === expected.length) {
+ for (let i = 0; i < expected.length; i++) {
+ dump("Check that the output item has the expected value and type\n");
+ ok(!!actual[i]);
+ equal(expected[i].value, actual[i].value);
+ equal(expected[i].type, actual[i].type);
+ }
+ } else {
+ for (let prop of actual) {
+ dump("Actual output contained: {value: " + prop.value + ", type: " +
+ prop.type + "}\n");
+ }
+ equal(actual.length, expected.length);
+ }
+}
diff --git a/devtools/client/shared/test/unit/test_parseSingleValue.js b/devtools/client/shared/test/unit/test_parseSingleValue.js
new file mode 100644
index 000000000..73e4f0ac4
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_parseSingleValue.js
@@ -0,0 +1,93 @@
+/* -*- 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";
+
+var Cu = Components.utils;
+const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const {parseSingleValue} = require("devtools/shared/css/parsing-utils");
+const {isCssPropertyKnown} = require("devtools/server/actors/css-properties");
+
+const TEST_DATA = [
+ {input: null, throws: true},
+ {input: undefined, throws: true},
+ {input: "", expected: {value: "", priority: ""}},
+ {input: " \t \t \n\n ", expected: {value: "", priority: ""}},
+ {input: "blue", expected: {value: "blue", priority: ""}},
+ {input: "blue !important", expected: {value: "blue", priority: "important"}},
+ {input: "blue!important", expected: {value: "blue", priority: "important"}},
+ {input: "blue ! important", expected: {value: "blue", priority: "important"}},
+ {
+ input: "blue ! important",
+ expected: {value: "blue", priority: "important"}
+ },
+ {input: "blue !", expected: {value: "blue", priority: ""}},
+ {input: "blue !mportant", expected: {value: "blue !mportant", priority: ""}},
+ {
+ input: " blue !important ",
+ expected: {value: "blue", priority: "important"}
+ },
+ {
+ input: "url(\"http://url.com/whyWouldYouDoThat!important.png\") !important",
+ expected: {
+ value: "url(\"http://url.com/whyWouldYouDoThat!important.png\")",
+ priority: "important"
+ }
+ },
+ {
+ input: "url(\"http://url.com/whyWouldYouDoThat!important.png\")",
+ expected: {
+ value: "url(\"http://url.com/whyWouldYouDoThat!important.png\")",
+ priority: ""
+ }
+ },
+ {
+ input: "\"content!important\" !important",
+ expected: {
+ value: "\"content!important\"",
+ priority: "important"
+ }
+ },
+ {
+ input: "\"content!important\"",
+ expected: {
+ value: "\"content!important\"",
+ priority: ""
+ }
+ },
+ {
+ input: "\"all the \\\"'\\\\ special characters\"",
+ expected: {
+ value: "\"all the \\\"'\\\\ special characters\"",
+ priority: ""
+ }
+ }
+];
+
+function run_test() {
+ for (let test of TEST_DATA) {
+ do_print("Test input value " + test.input);
+ try {
+ let output = parseSingleValue(isCssPropertyKnown, test.input);
+ assertOutput(output, test.expected);
+ } catch (e) {
+ do_print("parseSingleValue threw an exception with the given input " +
+ "value");
+ if (test.throws) {
+ do_print("Exception expected");
+ do_check_true(true);
+ } else {
+ do_print("Exception unexpected\n" + e);
+ do_check_true(false);
+ }
+ }
+ }
+}
+
+function assertOutput(actual, expected) {
+ do_print("Check that the output has the expected value and priority");
+ do_check_eq(expected.value, actual.value);
+ do_check_eq(expected.priority, actual.priority);
+}
diff --git a/devtools/client/shared/test/unit/test_rewriteDeclarations.js b/devtools/client/shared/test/unit/test_rewriteDeclarations.js
new file mode 100644
index 000000000..0183ea3c5
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_rewriteDeclarations.js
@@ -0,0 +1,529 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var Cu = Components.utils;
+const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const {RuleRewriter} = require("devtools/shared/css/parsing-utils");
+const {isCssPropertyKnown} = require("devtools/server/actors/css-properties");
+
+const TEST_DATA = [
+ {
+ desc: "simple set",
+ input: "p:v;",
+ instruction: {type: "set", name: "p", value: "N", priority: "",
+ index: 0},
+ expected: "p:N;"
+ },
+ {
+ desc: "simple set clearing !important",
+ input: "p:v !important;",
+ instruction: {type: "set", name: "p", value: "N", priority: "",
+ index: 0},
+ expected: "p:N;"
+ },
+ {
+ desc: "simple set adding !important",
+ input: "p:v;",
+ instruction: {type: "set", name: "p", value: "N", priority: "important",
+ index: 0},
+ expected: "p:N !important;"
+ },
+ {
+ desc: "simple set between comments",
+ input: "/*color:red;*/ p:v; /*color:green;*/",
+ instruction: {type: "set", name: "p", value: "N", priority: "",
+ index: 1},
+ expected: "/*color:red;*/ p:N; /*color:green;*/"
+ },
+ // The rule view can generate a "set" with a previously unknown
+ // property index; which should work like "create".
+ {
+ desc: "set at unknown index",
+ input: "a:b; e: f;",
+ instruction: {type: "set", name: "c", value: "d", priority: "",
+ index: 2},
+ expected: "a:b; e: f;c: d;"
+ },
+ {
+ desc: "simple rename",
+ input: "p:v;",
+ instruction: {type: "rename", name: "p", newName: "q", index: 0},
+ expected: "q:v;"
+ },
+ // "rename" is passed the name that the user entered, and must do
+ // any escaping necessary to ensure that this is an identifier.
+ {
+ desc: "rename requiring escape",
+ input: "p:v;",
+ instruction: {type: "rename", name: "p", newName: "a b", index: 0},
+ expected: "a\\ b:v;"
+ },
+ {
+ desc: "simple create",
+ input: "",
+ instruction: {type: "create", name: "p", value: "v", priority: "important",
+ index: 0, enabled: true},
+ expected: "p: v !important;"
+ },
+ {
+ desc: "create between two properties",
+ input: "a:b; e: f;",
+ instruction: {type: "create", name: "c", value: "d", priority: "",
+ index: 1, enabled: true},
+ expected: "a:b; c: d;e: f;"
+ },
+ // "create" is passed the name that the user entered, and must do
+ // any escaping necessary to ensure that this is an identifier.
+ {
+ desc: "create requiring escape",
+ input: "",
+ instruction: {type: "create", name: "a b", value: "d", priority: "",
+ index: 1, enabled: true},
+ expected: "a\\ b: d;"
+ },
+ {
+ desc: "simple disable",
+ input: "p:v;",
+ instruction: {type: "enable", name: "p", value: false, index: 0},
+ expected: "/*! p:v; */"
+ },
+ {
+ desc: "simple enable",
+ input: "/* color:v; */",
+ instruction: {type: "enable", name: "color", value: true, index: 0},
+ expected: "color:v;"
+ },
+ {
+ desc: "enable with following property in comment",
+ input: "/* color:red; color: blue; */",
+ instruction: {type: "enable", name: "color", value: true, index: 0},
+ expected: "color:red; /* color: blue; */"
+ },
+ {
+ desc: "enable with preceding property in comment",
+ input: "/* color:red; color: blue; */",
+ instruction: {type: "enable", name: "color", value: true, index: 1},
+ expected: "/* color:red; */ color: blue;"
+ },
+ {
+ desc: "simple remove",
+ input: "a:b;c:d;e:f;",
+ instruction: {type: "remove", name: "c", index: 1},
+ expected: "a:b;e:f;"
+ },
+ {
+ desc: "disable with comment ender in string",
+ input: "content: '*/';",
+ instruction: {type: "enable", name: "content", value: false, index: 0},
+ expected: "/*! content: '*\\/'; */"
+ },
+ {
+ desc: "enable with comment ender in string",
+ input: "/* content: '*\\/'; */",
+ instruction: {type: "enable", name: "content", value: true, index: 0},
+ expected: "content: '*/';"
+ },
+ {
+ desc: "enable requiring semicolon insertion",
+ // Note the lack of a trailing semicolon in the comment.
+ input: "/* color:red */ color: blue;",
+ instruction: {type: "enable", name: "color", value: true, index: 0},
+ expected: "color:red; color: blue;"
+ },
+ {
+ desc: "create requiring semicolon insertion",
+ // Note the lack of a trailing semicolon.
+ input: "color: red",
+ instruction: {type: "create", name: "a", value: "b", priority: "",
+ index: 1, enabled: true},
+ expected: "color: red;a: b;"
+ },
+
+ // Newline insertion.
+ {
+ desc: "simple newline insertion",
+ input: "\ncolor: red;\n",
+ instruction: {type: "create", name: "a", value: "b", priority: "",
+ index: 1, enabled: true},
+ expected: "\ncolor: red;\na: b;\n"
+ },
+ // Newline insertion.
+ {
+ desc: "semicolon insertion before newline",
+ // Note the lack of a trailing semicolon.
+ input: "\ncolor: red\n",
+ instruction: {type: "create", name: "a", value: "b", priority: "",
+ index: 1, enabled: true},
+ expected: "\ncolor: red;\na: b;\n"
+ },
+ // Newline insertion.
+ {
+ desc: "newline and semicolon insertion",
+ // Note the lack of a trailing semicolon and newline.
+ input: "\ncolor: red",
+ instruction: {type: "create", name: "a", value: "b", priority: "",
+ index: 1, enabled: true},
+ expected: "\ncolor: red;\na: b;\n"
+ },
+
+ // Newline insertion and indentation.
+ {
+ desc: "indentation with create",
+ input: "\n color: red;\n",
+ instruction: {type: "create", name: "a", value: "b", priority: "",
+ index: 1, enabled: true},
+ expected: "\n color: red;\n a: b;\n"
+ },
+ // Newline insertion and indentation.
+ {
+ desc: "indentation plus semicolon insertion before newline",
+ // Note the lack of a trailing semicolon.
+ input: "\n color: red\n",
+ instruction: {type: "create", name: "a", value: "b", priority: "",
+ index: 1, enabled: true},
+ expected: "\n color: red;\n a: b;\n"
+ },
+ {
+ desc: "indentation inserted before trailing whitespace",
+ // Note the trailing whitespace. This could come from a rule
+ // like:
+ // @supports (mumble) {
+ // body {
+ // color: red;
+ // }
+ // }
+ // Here if we create a rule we don't want it to follow
+ // the indentation of the "}".
+ input: "\n color: red;\n ",
+ instruction: {type: "create", name: "a", value: "b", priority: "",
+ index: 1, enabled: true},
+ expected: "\n color: red;\n a: b;\n "
+ },
+ // Newline insertion and indentation.
+ {
+ desc: "indentation comes from preceding comment",
+ // Note how the comment comes before the declaration.
+ input: "\n /* comment */ color: red\n",
+ instruction: {type: "create", name: "a", value: "b", priority: "",
+ index: 1, enabled: true},
+ expected: "\n /* comment */ color: red;\n a: b;\n"
+ },
+ // Default indentation.
+ {
+ desc: "use of default indentation",
+ input: "\n",
+ instruction: {type: "create", name: "a", value: "b", priority: "",
+ index: 0, enabled: true},
+ expected: "\n\ta: b;\n"
+ },
+
+ // Deletion handles newlines properly.
+ {
+ desc: "deletion removes newline",
+ input: "a:b;\nc:d;\ne:f;",
+ instruction: {type: "remove", name: "c", index: 1},
+ expected: "a:b;\ne:f;"
+ },
+ // Deletion handles newlines properly.
+ {
+ desc: "deletion remove blank line",
+ input: "\n a:b;\n c:d; \ne:f;",
+ instruction: {type: "remove", name: "c", index: 1},
+ expected: "\n a:b;\ne:f;"
+ },
+ // Deletion handles newlines properly.
+ {
+ desc: "deletion leaves comment",
+ input: "\n a:b;\n /* something */ c:d; \ne:f;",
+ instruction: {type: "remove", name: "c", index: 1},
+ expected: "\n a:b;\n /* something */ \ne:f;"
+ },
+ // Deletion handles newlines properly.
+ {
+ desc: "deletion leaves previous newline",
+ input: "\n a:b;\n c:d; \ne:f;",
+ instruction: {type: "remove", name: "e", index: 2},
+ expected: "\n a:b;\n c:d; \n"
+ },
+ // Deletion handles newlines properly.
+ {
+ desc: "deletion removes trailing whitespace",
+ input: "\n a:b;\n c:d; \n e:f;",
+ instruction: {type: "remove", name: "e", index: 2},
+ expected: "\n a:b;\n c:d; \n"
+ },
+ // Deletion handles newlines properly.
+ {
+ desc: "deletion preserves indentation",
+ input: " a:b;\n c:d; \n e:f;",
+ instruction: {type: "remove", name: "a", index: 0},
+ expected: " c:d; \n e:f;"
+ },
+
+ // Termination insertion corner case.
+ {
+ desc: "enable single quote termination",
+ input: "/* content: 'hi */ color: red;",
+ instruction: {type: "enable", name: "content", value: true, index: 0},
+ expected: "content: 'hi'; color: red;",
+ changed: {0: "'hi'"}
+ },
+ // Termination insertion corner case.
+ {
+ desc: "create single quote termination",
+ input: "content: 'hi",
+ instruction: {type: "create", name: "color", value: "red", priority: "",
+ index: 1, enabled: true},
+ expected: "content: 'hi';color: red;",
+ changed: {0: "'hi'"}
+ },
+
+ // Termination insertion corner case.
+ {
+ desc: "enable double quote termination",
+ input: "/* content: \"hi */ color: red;",
+ instruction: {type: "enable", name: "content", value: true, index: 0},
+ expected: "content: \"hi\"; color: red;",
+ changed: {0: "\"hi\""}
+ },
+ // Termination insertion corner case.
+ {
+ desc: "create double quote termination",
+ input: "content: \"hi",
+ instruction: {type: "create", name: "color", value: "red", priority: "",
+ index: 1, enabled: true},
+ expected: "content: \"hi\";color: red;",
+ changed: {0: "\"hi\""}
+ },
+
+ // Termination insertion corner case.
+ {
+ desc: "enable url termination",
+ input: "/* background-image: url(something.jpg */ color: red;",
+ instruction: {type: "enable", name: "background-image", value: true,
+ index: 0},
+ expected: "background-image: url(something.jpg); color: red;",
+ changed: {0: "url(something.jpg)"}
+ },
+ // Termination insertion corner case.
+ {
+ desc: "create url termination",
+ input: "background-image: url(something.jpg",
+ instruction: {type: "create", name: "color", value: "red", priority: "",
+ index: 1, enabled: true},
+ expected: "background-image: url(something.jpg);color: red;",
+ changed: {0: "url(something.jpg)"}
+ },
+
+ // Termination insertion corner case.
+ {
+ desc: "enable url single quote termination",
+ input: "/* background-image: url('something.jpg */ color: red;",
+ instruction: {type: "enable", name: "background-image", value: true,
+ index: 0},
+ expected: "background-image: url('something.jpg'); color: red;",
+ changed: {0: "url('something.jpg')"}
+ },
+ // Termination insertion corner case.
+ {
+ desc: "create url single quote termination",
+ input: "background-image: url('something.jpg",
+ instruction: {type: "create", name: "color", value: "red", priority: "",
+ index: 1, enabled: true},
+ expected: "background-image: url('something.jpg');color: red;",
+ changed: {0: "url('something.jpg')"}
+ },
+
+ // Termination insertion corner case.
+ {
+ desc: "create url double quote termination",
+ input: "/* background-image: url(\"something.jpg */ color: red;",
+ instruction: {type: "enable", name: "background-image", value: true,
+ index: 0},
+ expected: "background-image: url(\"something.jpg\"); color: red;",
+ changed: {0: "url(\"something.jpg\")"}
+ },
+ // Termination insertion corner case.
+ {
+ desc: "enable url double quote termination",
+ input: "background-image: url(\"something.jpg",
+ instruction: {type: "create", name: "color", value: "red", priority: "",
+ index: 1, enabled: true},
+ expected: "background-image: url(\"something.jpg\");color: red;",
+ changed: {0: "url(\"something.jpg\")"}
+ },
+
+ // Termination insertion corner case.
+ {
+ desc: "create backslash termination",
+ input: "something: \\",
+ instruction: {type: "create", name: "color", value: "red", priority: "",
+ index: 1, enabled: true},
+ expected: "something: \\\\;color: red;",
+ // The lexer rewrites the token before we see it. However this is
+ // so obscure as to be inconsequential.
+ changed: {0: "\uFFFD\\"}
+ },
+
+ // Termination insertion corner case.
+ {
+ desc: "enable backslash single quote termination",
+ input: "something: '\\",
+ instruction: {type: "create", name: "color", value: "red", priority: "",
+ index: 1, enabled: true},
+ expected: "something: '\\\\';color: red;",
+ changed: {0: "'\\\\'"}
+ },
+ {
+ desc: "enable backslash double quote termination",
+ input: "something: \"\\",
+ instruction: {type: "create", name: "color", value: "red", priority: "",
+ index: 1, enabled: true},
+ expected: "something: \"\\\\\";color: red;",
+ changed: {0: "\"\\\\\""}
+ },
+
+ // Termination insertion corner case.
+ {
+ desc: "enable comment termination",
+ input: "something: blah /* comment ",
+ instruction: {type: "create", name: "color", value: "red", priority: "",
+ index: 1, enabled: true},
+ expected: "something: blah /* comment*/; color: red;"
+ },
+
+ // Rewrite a "heuristic override" comment.
+ {
+ desc: "enable with heuristic override comment",
+ input: "/*! walrus: zebra; */",
+ instruction: {type: "enable", name: "walrus", value: true, index: 0},
+ expected: "walrus: zebra;"
+ },
+
+ // Sanitize a bad value.
+ {
+ desc: "create sanitize unpaired brace",
+ input: "",
+ instruction: {type: "create", name: "p", value: "}", priority: "",
+ index: 0, enabled: true},
+ expected: "p: \\};",
+ changed: {0: "\\}"}
+ },
+ // Sanitize a bad value.
+ {
+ desc: "set sanitize unpaired brace",
+ input: "walrus: zebra;",
+ instruction: {type: "set", name: "walrus", value: "{{}}}", priority: "",
+ index: 0},
+ expected: "walrus: {{}}\\};",
+ changed: {0: "{{}}\\}"}
+ },
+ // Sanitize a bad value.
+ {
+ desc: "enable sanitize unpaired brace",
+ input: "/*! walrus: }*/",
+ instruction: {type: "enable", name: "walrus", value: true, index: 0},
+ expected: "walrus: \\};",
+ changed: {0: "\\}"}
+ },
+
+ // Creating a new declaration does not require an attempt to
+ // terminate a previous commented declaration.
+ {
+ desc: "disabled declaration does not need semicolon insertion",
+ input: "/*! no: semicolon */\n",
+ instruction: {type: "create", name: "walrus", value: "zebra", priority: "",
+ index: 1, enabled: true},
+ expected: "/*! no: semicolon */\nwalrus: zebra;\n",
+ changed: {}
+ },
+
+ {
+ desc: "create commented-out property",
+ input: "p: v",
+ instruction: {type: "create", name: "shoveler", value: "duck", priority: "",
+ index: 1, enabled: false},
+ expected: "p: v;/*! shoveler: duck; */",
+ },
+ {
+ desc: "disabled create with comment ender in string",
+ input: "",
+ instruction: {type: "create", name: "content", value: "'*/'", priority: "",
+ index: 0, enabled: false},
+ expected: "/*! content: '*\\/'; */"
+ },
+
+ {
+ desc: "delete disabled property",
+ input: "\n a:b;\n /* color:#f0c; */\n e:f;",
+ instruction: {type: "remove", name: "color", index: 1},
+ expected: "\n a:b;\n e:f;",
+ },
+ {
+ desc: "delete heuristic-disabled property",
+ input: "\n a:b;\n /*! c:d; */\n e:f;",
+ instruction: {type: "remove", name: "c", index: 1},
+ expected: "\n a:b;\n e:f;",
+ },
+ {
+ desc: "delete disabled property leaving other disabled property",
+ input: "\n a:b;\n /* color:#f0c; background-color: seagreen; */\n e:f;",
+ instruction: {type: "remove", name: "color", index: 1},
+ expected: "\n a:b;\n /* background-color: seagreen; */\n e:f;",
+ },
+];
+
+function rewriteDeclarations(inputString, instruction, defaultIndentation) {
+ let rewriter = new RuleRewriter(isCssPropertyKnown, null, inputString);
+ rewriter.defaultIndentation = defaultIndentation;
+
+ switch (instruction.type) {
+ case "rename":
+ rewriter.renameProperty(instruction.index, instruction.name,
+ instruction.newName);
+ break;
+
+ case "enable":
+ rewriter.setPropertyEnabled(instruction.index, instruction.name,
+ instruction.value);
+ break;
+
+ case "create":
+ rewriter.createProperty(instruction.index, instruction.name,
+ instruction.value, instruction.priority,
+ instruction.enabled);
+ break;
+
+ case "set":
+ rewriter.setProperty(instruction.index, instruction.name,
+ instruction.value, instruction.priority);
+ break;
+
+ case "remove":
+ rewriter.removeProperty(instruction.index, instruction.name);
+ break;
+
+ default:
+ throw new Error("unrecognized instruction");
+ }
+
+ return rewriter.getResult();
+}
+
+function run_test() {
+ for (let test of TEST_DATA) {
+ let {changed, text} = rewriteDeclarations(test.input, test.instruction,
+ "\t");
+ equal(text, test.expected, "output for " + test.desc);
+
+ let expectChanged;
+ if ("changed" in test) {
+ expectChanged = test.changed;
+ } else {
+ expectChanged = {};
+ }
+ deepEqual(changed, expectChanged, "changed result for " + test.desc);
+ }
+}
diff --git a/devtools/client/shared/test/unit/test_source-utils.js b/devtools/client/shared/test/unit/test_source-utils.js
new file mode 100644
index 000000000..2ff55b92e
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_source-utils.js
@@ -0,0 +1,181 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests utility functions contained in `source-utils.js`
+ */
+
+const { require } = Components.utils.import("resource://devtools/shared/Loader.jsm", {});
+const sourceUtils = require("devtools/client/shared/source-utils");
+
+function run_test() {
+ run_next_test();
+}
+
+const CHROME_URLS = [
+ "chrome://foo", "resource://baz", "jar:file:///Users/root"
+];
+
+const CONTENT_URLS = [
+ "http://mozilla.org", "https://mozilla.org", "file:///Users/root", "app://fxosapp"
+];
+
+// Test `sourceUtils.parseURL`
+add_task(function* () {
+ let parsed = sourceUtils.parseURL("https://foo.com:8888/boo/bar.js?q=query");
+ equal(parsed.fileName, "bar.js", "parseURL parsed valid fileName");
+ equal(parsed.host, "foo.com:8888", "parseURL parsed valid host");
+ equal(parsed.hostname, "foo.com", "parseURL parsed valid hostname");
+ equal(parsed.port, "8888", "parseURL parsed valid port");
+ equal(parsed.href, "https://foo.com:8888/boo/bar.js?q=query", "parseURL parsed valid href");
+
+ parsed = sourceUtils.parseURL("https://foo.com");
+ equal(parsed.host, "foo.com", "parseURL parsed valid host when no port given");
+ equal(parsed.hostname, "foo.com", "parseURL parsed valid hostname when no port given");
+
+ equal(sourceUtils.parseURL("self-hosted"), null,
+ "parseURL returns `null` for invalid URLs");
+});
+
+// Test `sourceUtils.isContentScheme`.
+add_task(function* () {
+ for (let url of CHROME_URLS) {
+ ok(!sourceUtils.isContentScheme(url),
+ `${url} correctly identified as not content scheme`);
+ }
+ for (let url of CONTENT_URLS) {
+ ok(sourceUtils.isContentScheme(url), `${url} correctly identified as content scheme`);
+ }
+});
+
+// Test `sourceUtils.isChromeScheme`.
+add_task(function* () {
+ for (let url of CHROME_URLS) {
+ ok(sourceUtils.isChromeScheme(url), `${url} correctly identified as chrome scheme`);
+ }
+ for (let url of CONTENT_URLS) {
+ ok(!sourceUtils.isChromeScheme(url),
+ `${url} correctly identified as not chrome scheme`);
+ }
+});
+
+// Test `sourceUtils.isDataScheme`.
+add_task(function* () {
+ let dataURI = "data:text/html;charset=utf-8,<!DOCTYPE html></html>";
+ ok(sourceUtils.isDataScheme(dataURI), `${dataURI} correctly identified as data scheme`);
+
+ for (let url of CHROME_URLS) {
+ ok(!sourceUtils.isDataScheme(url), `${url} correctly identified as not data scheme`);
+ }
+ for (let url of CONTENT_URLS) {
+ ok(!sourceUtils.isDataScheme(url), `${url} correctly identified as not data scheme`);
+ }
+});
+
+// Test `sourceUtils.getSourceNames`.
+add_task(function* () {
+ testAbbreviation("http://example.com/foo/bar/baz/boo.js",
+ "boo.js",
+ "http://example.com/foo/bar/baz/boo.js",
+ "example.com");
+});
+
+// Test `sourceUtils.isScratchpadTheme`
+add_task(function* () {
+ ok(sourceUtils.isScratchpadScheme("Scratchpad/1"),
+ "Scratchpad/1 identified as scratchpad");
+ ok(sourceUtils.isScratchpadScheme("Scratchpad/20"),
+ "Scratchpad/20 identified as scratchpad");
+ ok(!sourceUtils.isScratchpadScheme("http://www.mozilla.org"), "http://www.mozilla.org not identified as scratchpad");
+});
+
+// Test `sourceUtils.getSourceNames`.
+add_task(function* () {
+ // Check length
+ let longMalformedURL = `example.com${new Array(100).fill("/a").join("")}/file.js`;
+ ok(sourceUtils.getSourceNames(longMalformedURL).short.length <= 100,
+ "`short` names are capped at 100 characters");
+
+ testAbbreviation("self-hosted", "self-hosted", "self-hosted");
+ testAbbreviation("", "(unknown)", "(unknown)");
+
+ // Test shortening data URIs, stripping mime/charset
+ testAbbreviation("data:text/html;charset=utf-8,<!DOCTYPE html></html>",
+ "data:<!DOCTYPE html></html>",
+ "data:text/html;charset=utf-8,<!DOCTYPE html></html>");
+
+ let longDataURI = `data:image/png;base64,${new Array(100).fill("a").join("")}`;
+ let longDataURIShort = sourceUtils.getSourceNames(longDataURI).short;
+
+ // Test shortening data URIs and that the `short` result is capped
+ ok(longDataURIShort.length <= 100,
+ "`short` names are capped at 100 characters for data URIs");
+ equal(longDataURIShort.substr(0, 10), "data:aaaaa",
+ "truncated data URI short names still have `data:...`");
+
+ // Test simple URL and cache retrieval by calling the same input multiple times.
+ let testUrl = "http://example.com/foo/bar/baz/boo.js";
+ testAbbreviation(testUrl, "boo.js", testUrl, "example.com");
+ testAbbreviation(testUrl, "boo.js", testUrl, "example.com");
+
+ // Check query and hash and port
+ testAbbreviation("http://example.com:8888/foo/bar/baz.js?q=query#go",
+ "baz.js",
+ "http://example.com:8888/foo/bar/baz.js",
+ "example.com:8888");
+
+ // Trailing "/" with nothing beyond host
+ testAbbreviation("http://example.com/",
+ "/",
+ "http://example.com/",
+ "example.com");
+
+ // Trailing "/"
+ testAbbreviation("http://example.com/foo/bar/",
+ "bar",
+ "http://example.com/foo/bar/",
+ "example.com");
+
+ // Non-extension ending
+ testAbbreviation("http://example.com/bar",
+ "bar",
+ "http://example.com/bar",
+ "example.com");
+
+ // Check query
+ testAbbreviation("http://example.com/foo.js?bar=1&baz=2",
+ "foo.js",
+ "http://example.com/foo.js",
+ "example.com");
+
+ // Check query with trailing slash
+ testAbbreviation("http://example.com/foo/?bar=1&baz=2",
+ "foo",
+ "http://example.com/foo/",
+ "example.com");
+});
+
+// Test for source mapped file name
+add_task(function* () {
+ const { getSourceMappedFile } = sourceUtils;
+ const source = "baz.js";
+ const output = getSourceMappedFile(source);
+ equal(output, "baz.js", "correctly formats file name");
+ // Test for OSX file path
+ const source1 = "/foo/bar/baz.js";
+ const output1 = getSourceMappedFile(source1);
+ equal(output1, "baz.js", "correctly formats Linux file path");
+ // Test for Windows file path
+ const source2 = "Z:\\foo\\bar\\baz.js";
+ const output2 = getSourceMappedFile(source2);
+ equal(output2, "baz.js", "correctly formats Windows file path");
+});
+
+function testAbbreviation(source, short, long, host) {
+ let results = sourceUtils.getSourceNames(source);
+ equal(results.short, short, `${source} has correct "short" name`);
+ equal(results.long, long, `${source} has correct "long" name`);
+ equal(results.host, host, `${source} has correct "host" name`);
+}
diff --git a/devtools/client/shared/test/unit/test_suggestion-picker.js b/devtools/client/shared/test/unit/test_suggestion-picker.js
new file mode 100644
index 000000000..28c9df13b
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_suggestion-picker.js
@@ -0,0 +1,149 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test the suggestion-picker helper methods.
+ */
+const {require} = Components.utils.import("resource://devtools/shared/Loader.jsm", {});
+const {
+ findMostRelevantIndex,
+ findMostRelevantCssPropertyIndex
+} = require("devtools/client/shared/suggestion-picker");
+
+/**
+ * Run all tests defined below.
+ */
+function run_test() {
+ ensureMostRelevantIndexProvidedByHelperFunction();
+ ensureMostRelevantIndexProvidedByClassMethod();
+ ensureErrorThrownWithInvalidArguments();
+}
+
+/**
+ * Generic test data.
+ */
+const TEST_DATA = [
+ {
+ // Match in sortedItems array.
+ items: ["chrome", "edge", "firefox"],
+ sortedItems: ["firefox", "chrome", "edge"],
+ expectedIndex: 2
+ }, {
+ // No match in sortedItems array.
+ items: ["apple", "oranges", "banana"],
+ sortedItems: ["kiwi", "pear", "peach"],
+ expectedIndex: 0
+ }, {
+ // Empty items array.
+ items: [],
+ sortedItems: ["empty", "arrays", "can't", "have", "relevant", "indexes"],
+ expectedIndex: -1
+ }
+];
+
+function ensureMostRelevantIndexProvidedByHelperFunction() {
+ do_print("Running ensureMostRelevantIndexProvidedByHelperFunction()");
+
+ for (let testData of TEST_DATA) {
+ let { items, sortedItems, expectedIndex } = testData;
+ let mostRelevantIndex = findMostRelevantIndex(items, sortedItems);
+ strictEqual(mostRelevantIndex, expectedIndex);
+ }
+}
+
+/**
+ * CSS properties test data.
+ */
+const CSS_TEST_DATA = [
+ {
+ items: [
+ "backface-visibility",
+ "background",
+ "background-attachment",
+ "background-blend-mode",
+ "background-clip",
+ "background-color",
+ "background-image",
+ "background-origin",
+ "background-position",
+ "background-repeat"
+ ],
+ expectedIndex: 1
+ },
+ {
+ items: [
+ "caption-side",
+ "clear",
+ "clip",
+ "clip-path",
+ "clip-rule",
+ "color",
+ "color-interpolation",
+ "color-interpolation-filters",
+ "content",
+ "counter-increment"
+ ],
+ expectedIndex: 5
+ },
+ {
+ items: [
+ "direction",
+ "display",
+ "dominant-baseline"
+ ],
+ expectedIndex: 1
+ },
+ {
+ items: [
+ "object-fit",
+ "object-position",
+ "offset-block-end",
+ "offset-block-start",
+ "offset-inline-end",
+ "offset-inline-start",
+ "opacity",
+ "order",
+ "orphans",
+ "outline"
+ ],
+ expectedIndex: 6
+ },
+ {
+ items: [
+ "white-space",
+ "widows",
+ "width",
+ "will-change",
+ "word-break",
+ "word-spacing",
+ "word-wrap",
+ "writing-mode"
+ ],
+ expectedIndex: 2
+ }
+];
+
+function ensureMostRelevantIndexProvidedByClassMethod() {
+ do_print("Running ensureMostRelevantIndexProvidedByClassMethod()");
+
+ for (let testData of CSS_TEST_DATA) {
+ let { items, expectedIndex } = testData;
+ let mostRelevantIndex = findMostRelevantCssPropertyIndex(items);
+ strictEqual(mostRelevantIndex, expectedIndex);
+ }
+}
+
+function ensureErrorThrownWithInvalidArguments() {
+ do_print("Running ensureErrorThrownWithInvalidTypeArgument()");
+
+ let expectedError = "Please provide valid items and sortedItems arrays.";
+ // No arguments passed.
+ throws(() => findMostRelevantIndex(), expectedError);
+ // Invalid arguments passed.
+ throws(() => findMostRelevantIndex([]), expectedError);
+ throws(() => findMostRelevantIndex(null, []), expectedError);
+ throws(() => findMostRelevantIndex([], "string"), expectedError);
+ throws(() => findMostRelevantIndex("string", []), expectedError);
+}
diff --git a/devtools/client/shared/test/unit/test_undoStack.js b/devtools/client/shared/test/unit/test_undoStack.js
new file mode 100644
index 000000000..7499614fd
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_undoStack.js
@@ -0,0 +1,98 @@
+/* -*- 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 {Loader} = Components.utils.import("resource://gre/modules/commonjs/toolkit/loader.js", {});
+
+const loader = new Loader.Loader({
+ paths: {
+ "": "resource://gre/modules/commonjs/",
+ "devtools": "resource://devtools",
+ },
+ globals: {},
+});
+const require = Loader.Require(loader, { id: "undo-test" });
+
+const {UndoStack} = require("devtools/client/shared/undo");
+
+const MAX_SIZE = 5;
+
+function run_test() {
+ let str = "";
+ let stack = new UndoStack(MAX_SIZE);
+
+ function add(ch) {
+ stack.do(function () {
+ str += ch;
+ }, function () {
+ str = str.slice(0, -1);
+ });
+ }
+
+ do_check_false(stack.canUndo());
+ do_check_false(stack.canRedo());
+
+ // Check adding up to the limit of the size
+ add("a");
+ do_check_true(stack.canUndo());
+ do_check_false(stack.canRedo());
+
+ add("b");
+ add("c");
+ add("d");
+ add("e");
+
+ do_check_eq(str, "abcde");
+
+ // Check a simple undo+redo
+ stack.undo();
+
+ do_check_eq(str, "abcd");
+ do_check_true(stack.canRedo());
+
+ stack.redo();
+ do_check_eq(str, "abcde");
+ do_check_false(stack.canRedo());
+
+ // Check an undo followed by a new action
+ stack.undo();
+ do_check_eq(str, "abcd");
+
+ add("q");
+ do_check_eq(str, "abcdq");
+ do_check_false(stack.canRedo());
+
+ stack.undo();
+ do_check_eq(str, "abcd");
+ stack.redo();
+ do_check_eq(str, "abcdq");
+
+ // Revert back to the beginning of the queue...
+ while (stack.canUndo()) {
+ stack.undo();
+ }
+ do_check_eq(str, "");
+
+ // Now put it all back....
+ while (stack.canRedo()) {
+ stack.redo();
+ }
+ do_check_eq(str, "abcdq");
+
+ // Now go over the undo limit...
+ add("1");
+ add("2");
+ add("3");
+
+ do_check_eq(str, "abcdq123");
+
+ // And now undoing the whole stack should only undo 5 actions.
+ while (stack.canUndo()) {
+ stack.undo();
+ }
+
+ do_check_eq(str, "abc");
+}
diff --git a/devtools/client/shared/test/unit/xpcshell.ini b/devtools/client/shared/test/unit/xpcshell.ini
new file mode 100644
index 000000000..b3c5791ec
--- /dev/null
+++ b/devtools/client/shared/test/unit/xpcshell.ini
@@ -0,0 +1,30 @@
+[DEFAULT]
+tags = devtools
+head =
+tail =
+firefox-appdir = browser
+skip-if = toolkit == 'android'
+
+support-files =
+ ../helper_color_data.js
+
+[test_advanceValidate.js]
+[test_attribute-parsing-01.js]
+[test_attribute-parsing-02.js]
+[test_bezierCanvas.js]
+[test_cssAngle.js]
+[test_cssColor-01.js]
+[test_cssColor-02.js]
+[test_cssColor-03.js]
+[test_cssColorDatabase.js]
+[test_cubicBezier.js]
+[test_escapeCSSComment.js]
+[test_parseDeclarations.js]
+[test_parsePseudoClassesAndAttributes.js]
+[test_parseSingleValue.js]
+[test_rewriteDeclarations.js]
+[test_source-utils.js]
+[test_suggestion-picker.js]
+[test_undoStack.js]
+[test_VariablesView_filtering-without-controller.js]
+[test_VariablesView_getString_promise.js]
diff --git a/devtools/client/shared/theme-switching.js b/devtools/client/shared/theme-switching.js
new file mode 100644
index 000000000..29f93c460
--- /dev/null
+++ b/devtools/client/shared/theme-switching.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/. */
+
+/* eslint-env browser */
+"use strict";
+(function () {
+ const { utils: Cu } = Components;
+ const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+ const Services = require("Services");
+ const { gDevTools } = require("devtools/client/framework/devtools");
+ const { watchCSS } = require("devtools/client/shared/css-reload");
+ let documentElement = document.documentElement;
+
+ let os;
+ let platform = navigator.platform;
+ if (platform.startsWith("Win")) {
+ os = "win";
+ } else if (platform.startsWith("Mac")) {
+ os = "mac";
+ } else {
+ os = "linux";
+ }
+
+ documentElement.setAttribute("platform", os);
+
+ // no-theme attributes allows to just est the platform attribute
+ // to have per-platform CSS working correctly.
+ if (documentElement.getAttribute("no-theme") === "true") {
+ return;
+ }
+
+ let devtoolsStyleSheets = new WeakMap();
+ let gOldTheme = "";
+
+ function forceStyle() {
+ let computedStyle = window.getComputedStyle(documentElement);
+ if (!computedStyle) {
+ // Null when documentElement is not ready. This method is anyways not
+ // required then as scrollbars would be in their state without flushing.
+ return;
+ }
+ // Save display value
+ let display = computedStyle.display;
+ documentElement.style.display = "none";
+ // Flush
+ window.getComputedStyle(documentElement).display;
+ // Restore
+ documentElement.style.display = display;
+ }
+
+ /*
+ * Append a new processing instruction and return an object with
+ * - styleSheet: DOMNode
+ * - loadPromise: Promise that resolves once the sheets loads or errors
+ */
+ function appendStyleSheet(url) {
+ let styleSheetAttr = `href="${url}" type="text/css"`;
+ let styleSheet = document.createProcessingInstruction(
+ "xml-stylesheet", styleSheetAttr);
+ let loadPromise = new Promise((resolve, reject) => {
+ function onload() {
+ styleSheet.removeEventListener("load", onload);
+ styleSheet.removeEventListener("error", onerror);
+ resolve();
+ }
+ function onerror() {
+ styleSheet.removeEventListener("load", onload);
+ styleSheet.removeEventListener("error", onerror);
+ reject("Failed to load theme file " + url);
+ }
+
+ styleSheet.addEventListener("load", onload);
+ styleSheet.addEventListener("error", onerror);
+ });
+ document.insertBefore(styleSheet, documentElement);
+ return {styleSheet, loadPromise};
+ }
+
+ /*
+ * Notify the window that a theme switch finished so tests can check the DOM
+ */
+ function notifyWindow() {
+ window.dispatchEvent(new CustomEvent("theme-switch-complete", {}));
+ }
+
+ /*
+ * Apply all the sheets from `newTheme` and remove all of the sheets
+ * from `oldTheme`
+ */
+ function switchTheme(newTheme) {
+ if (newTheme === gOldTheme) {
+ return;
+ }
+ let oldTheme = gOldTheme;
+ gOldTheme = newTheme;
+
+ let oldThemeDef = gDevTools.getThemeDefinition(oldTheme);
+ let newThemeDef = gDevTools.getThemeDefinition(newTheme);
+
+ // The theme might not be available anymore (e.g. uninstalled)
+ // Use the default one.
+ if (!newThemeDef) {
+ newThemeDef = gDevTools.getThemeDefinition("light");
+ }
+
+ // Store the sheets in a WeakMap for access later when the theme gets
+ // unapplied. It's hard to query for processing instructions so this
+ // is an easy way to access them later without storing a property on
+ // the window
+ devtoolsStyleSheets.set(newThemeDef, []);
+
+ let loadEvents = [];
+ for (let url of newThemeDef.stylesheets) {
+ let {styleSheet, loadPromise} = appendStyleSheet(url);
+ devtoolsStyleSheets.get(newThemeDef).push(styleSheet);
+ loadEvents.push(loadPromise);
+ }
+
+ try {
+ const StylesheetUtils = require("sdk/stylesheet/utils");
+ const SCROLLBARS_URL = "chrome://devtools/skin/floating-scrollbars-dark-theme.css";
+
+ // TODO: extensions might want to customize scrollbar styles too.
+ if (!Services.appShell.hiddenDOMWindow
+ .matchMedia("(-moz-overlay-scrollbars)").matches) {
+ if (newTheme == "dark") {
+ StylesheetUtils.loadSheet(window, SCROLLBARS_URL, "agent");
+ } else if (oldTheme == "dark") {
+ StylesheetUtils.removeSheet(window, SCROLLBARS_URL, "agent");
+ }
+ forceStyle();
+ }
+ } catch (e) {
+ console.warn("customize scrollbar styles is only supported in firefox");
+ }
+
+ Promise.all(loadEvents).then(() => {
+ // Unload all stylesheets and classes from the old theme.
+ if (oldThemeDef) {
+ for (let name of oldThemeDef.classList) {
+ documentElement.classList.remove(name);
+ }
+
+ for (let sheet of devtoolsStyleSheets.get(oldThemeDef) || []) {
+ sheet.remove();
+ }
+
+ if (oldThemeDef.onUnapply) {
+ oldThemeDef.onUnapply(window, newTheme);
+ }
+ }
+
+ // Load all stylesheets and classes from the new theme.
+ for (let name of newThemeDef.classList) {
+ documentElement.classList.add(name);
+ }
+
+ if (newThemeDef.onApply) {
+ newThemeDef.onApply(window, oldTheme);
+ }
+
+ // Final notification for further theme-switching related logic.
+ gDevTools.emit("theme-switched", window, newTheme, oldTheme);
+ notifyWindow();
+ }, console.error.bind(console));
+ }
+
+ function handlePrefChange() {
+ switchTheme(Services.prefs.getCharPref("devtools.theme"));
+ }
+
+ if (documentElement.hasAttribute("force-theme")) {
+ switchTheme(documentElement.getAttribute("force-theme"));
+ } else {
+ switchTheme(Services.prefs.getCharPref("devtools.theme"));
+
+ Services.prefs.addObserver("devtools.theme", handlePrefChange, false);
+ window.addEventListener("unload", function () {
+ Services.prefs.removeObserver("devtools.theme", handlePrefChange);
+ }, { once: true });
+ }
+
+ watchCSS(window);
+})();
diff --git a/devtools/client/shared/theme.js b/devtools/client/shared/theme.js
new file mode 100644
index 000000000..6ba956f64
--- /dev/null
+++ b/devtools/client/shared/theme.js
@@ -0,0 +1,84 @@
+/* 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";
+
+/**
+ * Colors for themes taken from:
+ * https://developer.mozilla.org/en-US/docs/Tools/DevToolsColors
+ */
+
+const Services = require("Services");
+const { gDevTools } = require("devtools/client/framework/devtools");
+
+const variableFileContents = require("raw!devtools/client/themes/variables.css");
+
+const THEME_SELECTOR_STRINGS = {
+ light: ":root.theme-light {",
+ dark: ":root.theme-dark {",
+ firebug: ":root.theme-firebug {"
+};
+
+/**
+ * Takes a theme name and returns the contents of its variable rule block.
+ * The first time this runs fetches the variables CSS file and caches it.
+ */
+function getThemeFile(name) {
+ // If there's no theme expected for this name, use `light` as default.
+ let selector = THEME_SELECTOR_STRINGS[name] ||
+ THEME_SELECTOR_STRINGS.light;
+
+ // This is a pretty naive way to find the contents between:
+ // selector {
+ // name: val;
+ // }
+ // There is test coverage for this feature (browser_theme.js)
+ // so if an } is introduced in the variables file it will catch that.
+ let theme = variableFileContents;
+ theme = theme.substring(theme.indexOf(selector));
+ theme = theme.substring(0, theme.indexOf("}"));
+
+ return theme;
+}
+
+/**
+ * Returns the string value of the current theme,
+ * like "dark" or "light".
+ */
+const getTheme = exports.getTheme = () => {
+ return Services.prefs.getCharPref("devtools.theme");
+};
+
+/**
+ * Returns a color indicated by `type` (like "toolbar-background", or
+ * "highlight-red"), with the ability to specify a theme, or use whatever the
+ * current theme is if left unset. If theme not found, falls back to "light"
+ * theme. Returns null if the type cannot be found for the theme given.
+ */
+/* eslint-disable no-unused-vars */
+const getColor = exports.getColor = (type, theme) => {
+ let themeName = theme || getTheme();
+ let themeFile = getThemeFile(themeName);
+ let match = themeFile.match(new RegExp("--theme-" + type + ": (.*);"));
+
+ // Return the appropriate variable in the theme, or otherwise, null.
+ return match ? match[1] : null;
+};
+
+/**
+ * Mimics selecting the theme selector in the toolbox;
+ * sets the preference and emits an event on gDevTools to trigger
+ * the themeing.
+ */
+const setTheme = exports.setTheme = (newTheme) => {
+ let oldTheme = getTheme();
+
+ Services.prefs.setCharPref("devtools.theme", newTheme);
+ gDevTools.emit("pref-changed", {
+ pref: "devtools.theme",
+ newValue: newTheme,
+ oldValue: oldTheme
+ });
+};
+/* eslint-enable */
diff --git a/devtools/client/shared/undo.js b/devtools/client/shared/undo.js
new file mode 100644
index 000000000..65791f50d
--- /dev/null
+++ b/devtools/client/shared/undo.js
@@ -0,0 +1,192 @@
+/* -*- 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";
+
+/**
+ * A simple undo stack manager.
+ *
+ * Actions are added along with the necessary code to
+ * reverse the action.
+ *
+ * @param integer maxUndo Maximum number of undo steps.
+ * defaults to 50.
+ */
+function UndoStack(maxUndo) {
+ this.maxUndo = maxUndo || 50;
+ this._stack = [];
+}
+
+exports.UndoStack = UndoStack;
+
+UndoStack.prototype = {
+ // Current index into the undo stack. Is positioned after the last
+ // currently-applied change.
+ _index: 0,
+
+ // The current batch depth (see startBatch() for details)
+ _batchDepth: 0,
+
+ destroy: function () {
+ this.uninstallController();
+ delete this._stack;
+ },
+
+ /**
+ * Start a collection of related changes. Changes will be batched
+ * together into one undo/redo item until endBatch() is called.
+ *
+ * Batches can be nested, in which case the outer batch will contain
+ * all items from the inner batches. This allows larger user
+ * actions made up of a collection of smaller actions to be
+ * undone as a single action.
+ */
+ startBatch: function () {
+ if (this._batchDepth++ === 0) {
+ this._batch = [];
+ }
+ },
+
+ /**
+ * End a batch of related changes, performing its action and adding
+ * it to the undo stack.
+ */
+ endBatch: function () {
+ if (--this._batchDepth > 0) {
+ return;
+ }
+
+ // Cut off the end of the undo stack at the current index,
+ // and the beginning to prevent a stack larger than maxUndo.
+ let start = Math.max((this._index + 1) - this.maxUndo, 0);
+ this._stack = this._stack.slice(start, this._index);
+
+ let batch = this._batch;
+ delete this._batch;
+ let entry = {
+ do: function () {
+ for (let item of batch) {
+ item.do();
+ }
+ },
+ undo: function () {
+ for (let i = batch.length - 1; i >= 0; i--) {
+ batch[i].undo();
+ }
+ }
+ };
+ this._stack.push(entry);
+ this._index = this._stack.length;
+ entry.do();
+ this._change();
+ },
+
+ /**
+ * Perform an action, adding it to the undo stack.
+ *
+ * @param function toDo Called to perform the action.
+ * @param function undo Called to reverse the action.
+ */
+ do: function (toDo, undo) {
+ this.startBatch();
+ this._batch.push({ do: toDo, undo });
+ this.endBatch();
+ },
+
+ /*
+ * Returns true if undo() will do anything.
+ */
+ canUndo: function () {
+ return this._index > 0;
+ },
+
+ /**
+ * Undo the top of the undo stack.
+ *
+ * @return true if an action was undone.
+ */
+ undo: function () {
+ if (!this.canUndo()) {
+ return false;
+ }
+ this._stack[--this._index].undo();
+ this._change();
+ return true;
+ },
+
+ /**
+ * Returns true if redo() will do anything.
+ */
+ canRedo: function () {
+ return this._stack.length > this._index;
+ },
+
+ /**
+ * Redo the most recently undone action.
+ *
+ * @return true if an action was redone.
+ */
+ redo: function () {
+ if (!this.canRedo()) {
+ return false;
+ }
+ this._stack[this._index++].do();
+ this._change();
+ return true;
+ },
+
+ _change: function () {
+ if (this._controllerWindow) {
+ this._controllerWindow.goUpdateCommand("cmd_undo");
+ this._controllerWindow.goUpdateCommand("cmd_redo");
+ }
+ },
+
+ /**
+ * ViewController implementation for undo/redo.
+ */
+
+ /**
+ * Install this object as a command controller.
+ */
+ installController: function (controllerWindow) {
+ this._controllerWindow = controllerWindow;
+ controllerWindow.controllers.appendController(this);
+ },
+
+ /**
+ * Uninstall this object from the command controller.
+ */
+ uninstallController: function () {
+ if (!this._controllerWindow) {
+ return;
+ }
+ this._controllerWindow.controllers.removeController(this);
+ },
+
+ supportsCommand: function (command) {
+ return (command == "cmd_undo" ||
+ command == "cmd_redo");
+ },
+
+ isCommandEnabled: function (command) {
+ switch (command) {
+ case "cmd_undo": return this.canUndo();
+ case "cmd_redo": return this.canRedo();
+ }
+ return false;
+ },
+
+ doCommand: function (command) {
+ switch (command) {
+ case "cmd_undo": return this.undo();
+ case "cmd_redo": return this.redo();
+ default: return null;
+ }
+ },
+
+ onEvent: function (event) {},
+};
diff --git a/devtools/client/shared/vendor/D3_LICENSE b/devtools/client/shared/vendor/D3_LICENSE
new file mode 100644
index 000000000..fb7d95d70
--- /dev/null
+++ b/devtools/client/shared/vendor/D3_LICENSE
@@ -0,0 +1,26 @@
+Copyright (c) 2014, Michael Bostock
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+* The name Michael Bostock may not be used to endorse or promote products
+ derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL MICHAEL BOSTOCK BE LIABLE FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/devtools/client/shared/vendor/DAGRE_D3_LICENSE b/devtools/client/shared/vendor/DAGRE_D3_LICENSE
new file mode 100644
index 000000000..1d64ed68c
--- /dev/null
+++ b/devtools/client/shared/vendor/DAGRE_D3_LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2013 Chris Pettitt
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/devtools/client/shared/vendor/REACT_REDUX_LICENSE b/devtools/client/shared/vendor/REACT_REDUX_LICENSE
new file mode 100644
index 000000000..af2353dca
--- /dev/null
+++ b/devtools/client/shared/vendor/REACT_REDUX_LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 Dan Abramov
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/devtools/client/shared/vendor/REACT_REDUX_UPGRADING b/devtools/client/shared/vendor/REACT_REDUX_UPGRADING
new file mode 100644
index 000000000..7739751db
--- /dev/null
+++ b/devtools/client/shared/vendor/REACT_REDUX_UPGRADING
@@ -0,0 +1,9 @@
+"react-redux" uses UMD style loading to work in many different environments.
+It assumes that "react" and "redux" are both included via `require("react")`
+as in node or browserify, but the paths to our react and redux installation are different.
+
+If upgrading react-redux, define the correct paths and replace the require statements
+for the module.exports case with the correct paths.
+
+Path to react: "devtools/client/shared/vendor/react"
+Path to redux: "devtools/client/shared/vendor/redux"
diff --git a/devtools/client/shared/vendor/REACT_UPGRADING b/devtools/client/shared/vendor/REACT_UPGRADING
new file mode 100644
index 000000000..a82b4fa8c
--- /dev/null
+++ b/devtools/client/shared/vendor/REACT_UPGRADING
@@ -0,0 +1,54 @@
+We use a version of React that has a few minor tweaks. We want to use
+an un-minified production version anyway, and because of all of this
+you need to build React yourself to upgrade it for devtools.
+
+First, clone the repo and get ready to build it. Replace `<version>`
+with the version tag you are targetting:
+
+* git clone https://github.com/facebook/react.git
+* cd react
+* git checkout <version>
+* In `src/addons/ReactWithAddons.js`, move the
+ `React.addons.TestUtils = ...` line outside of the `if`
+ block to force it be include in the production build
+
+Next, build React:
+
+* npm install
+* grunt build
+
+Unfortunately, you need to manually patch the generated JS file. We
+need to force React to always create HTML elements, and we do this by
+changing all `document.createElement` calls to `createElementNS`. It's
+much easier to do this on the generated file to make sure you update
+all dependencies as well.
+
+Open `build/react-with-addons.js` and search for all
+`document.createElement` calls and replace them with
+`document.createElementNS('http://www.w3.org/1999/xhtml', ...)`. Note
+that some code is `ownerDocument.createElement` so don't do a blind
+search/replace. There is only about ~12 places to change.
+
+Now move into our repo (note the naming of `react-dev.js`, it's the dev version):
+
+* cp build/react-with-addons.js <gecko-dev>/devtools/client/shared/vendor/react-dev.js
+
+Now we need to generate a production version of React:
+
+* NODE_ENV=production grunt build
+
+Unfortunately, you need to manually replace all the `createElement`
+calls in this version again. We know this is not ideal but WE NEED TO
+MOVE OFF XUL and we don't need to do this anymore once that happens.
+
+After patching `build/react-with-addons.js` again, copy the production
+version over:
+
+* cp build/react-with-addons.js <gecko-dev>/devtools/client/shared/vendor/react.js
+
+You also need to copy the ReactDOM package. It requires React, so
+right now we are just manually changing the path from `react` to
+`devtools/client/shared/vendor/react`.
+
+* cp build/react-dom.js <gecko-dev>/devtools/client/shared/vendor/react-dom.js
+* (change `require('react')` at the top of the file to the right path)
diff --git a/devtools/client/shared/vendor/REACT_VIRTUALIZED_UPGRADING b/devtools/client/shared/vendor/REACT_VIRTUALIZED_UPGRADING
new file mode 100644
index 000000000..50e0c1817
--- /dev/null
+++ b/devtools/client/shared/vendor/REACT_VIRTUALIZED_UPGRADING
@@ -0,0 +1,14 @@
+"react-virtualized" uses UMD style loading to work in many different environments.
+It assumes that "react", "react-addons-shallow-compare", and "react-dom" are all included
+separately via require statements. The paths to our installations are different.
+
+If upgrading:
+
+- Define the correct paths for React, etc and replace the require statements for the
+ module.exports case with the correct paths.
+- Replace any references to React.addons.shallowCompare with the webpack module id.
+- To support use in XUL documents, replace calls to createElement with
+ createElementNS("http://www.w3.org/1999/xhtml", but make sure that you aren't replacing
+ any calls to React.createElement.
+- Also required for XUL, replace document.head and document.body with
+ document.firstElementChild
diff --git a/devtools/client/shared/vendor/REDUX_LICENSE b/devtools/client/shared/vendor/REDUX_LICENSE
new file mode 100644
index 000000000..af2353dca
--- /dev/null
+++ b/devtools/client/shared/vendor/REDUX_LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 Dan Abramov
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/devtools/client/shared/vendor/REDUX_UPGRADING b/devtools/client/shared/vendor/REDUX_UPGRADING
new file mode 100644
index 000000000..fbbbfc8e3
--- /dev/null
+++ b/devtools/client/shared/vendor/REDUX_UPGRADING
@@ -0,0 +1,10 @@
+REDUX_UPGRADING
+
+Current version of redux : 3.3.0
+
+1 - grab the unminified version of redux on npm. For release 3.3.0 for instance,
+https://npmcdn.com/redux@3.3.0/dist/redux.js
+
+2 - replace the content of devtools/client/shared/vendor
+
+3 - update the current version in this file \ No newline at end of file
diff --git a/devtools/client/shared/vendor/RESELECT_LICENSE b/devtools/client/shared/vendor/RESELECT_LICENSE
new file mode 100644
index 000000000..1ca1e449f
--- /dev/null
+++ b/devtools/client/shared/vendor/RESELECT_LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2015-2016 Reselect Contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/devtools/client/shared/vendor/RESELECT_UPGRADING b/devtools/client/shared/vendor/RESELECT_UPGRADING
new file mode 100644
index 000000000..b846d05be
--- /dev/null
+++ b/devtools/client/shared/vendor/RESELECT_UPGRADING
@@ -0,0 +1,7 @@
+Follow these steps when adding/upgrading the reselect.js module:
+
+1. git clone https://github.com/reactjs/reselect - clone the repo
+2. npm install - compile the sources to a compiled JS module file
+3. cp dist/reselect.js $DEST_DIR - copy the compiled file to Firefox source tree
+
+The package version used it currently 2.5.4 (last update in bug 1310573)
diff --git a/devtools/client/shared/vendor/d3.js b/devtools/client/shared/vendor/d3.js
new file mode 100644
index 000000000..2f645354c
--- /dev/null
+++ b/devtools/client/shared/vendor/d3.js
@@ -0,0 +1,9275 @@
+!function() {
+ var d3 = {
+ version: "3.4.2"
+ };
+ if (!Date.now) Date.now = function() {
+ return +new Date();
+ };
+ var d3_arraySlice = [].slice, d3_array = function(list) {
+ return d3_arraySlice.call(list);
+ };
+ var d3_document = document, d3_documentElement = d3_document.documentElement, d3_window = window;
+ try {
+ d3_array(d3_documentElement.childNodes)[0].nodeType;
+ } catch (e) {
+ d3_array = function(list) {
+ var i = list.length, array = new Array(i);
+ while (i--) array[i] = list[i];
+ return array;
+ };
+ }
+ try {
+ d3_document.createElement("div").style.setProperty("opacity", 0, "");
+ } catch (error) {
+ var d3_element_prototype = d3_window.Element.prototype, d3_element_setAttribute = d3_element_prototype.setAttribute, d3_element_setAttributeNS = d3_element_prototype.setAttributeNS, d3_style_prototype = d3_window.CSSStyleDeclaration.prototype, d3_style_setProperty = d3_style_prototype.setProperty;
+ d3_element_prototype.setAttribute = function(name, value) {
+ d3_element_setAttribute.call(this, name, value + "");
+ };
+ d3_element_prototype.setAttributeNS = function(space, local, value) {
+ d3_element_setAttributeNS.call(this, space, local, value + "");
+ };
+ d3_style_prototype.setProperty = function(name, value, priority) {
+ d3_style_setProperty.call(this, name, value + "", priority);
+ };
+ }
+ d3.ascending = function(a, b) {
+ return a < b ? -1 : a > b ? 1 : a >= b ? 0 : NaN;
+ };
+ d3.descending = function(a, b) {
+ return b < a ? -1 : b > a ? 1 : b >= a ? 0 : NaN;
+ };
+ d3.min = function(array, f) {
+ var i = -1, n = array.length, a, b;
+ if (arguments.length === 1) {
+ while (++i < n && !((a = array[i]) != null && a <= a)) a = undefined;
+ while (++i < n) if ((b = array[i]) != null && a > b) a = b;
+ } else {
+ while (++i < n && !((a = f.call(array, array[i], i)) != null && a <= a)) a = undefined;
+ while (++i < n) if ((b = f.call(array, array[i], i)) != null && a > b) a = b;
+ }
+ return a;
+ };
+ d3.max = function(array, f) {
+ var i = -1, n = array.length, a, b;
+ if (arguments.length === 1) {
+ while (++i < n && !((a = array[i]) != null && a <= a)) a = undefined;
+ while (++i < n) if ((b = array[i]) != null && b > a) a = b;
+ } else {
+ while (++i < n && !((a = f.call(array, array[i], i)) != null && a <= a)) a = undefined;
+ while (++i < n) if ((b = f.call(array, array[i], i)) != null && b > a) a = b;
+ }
+ return a;
+ };
+ d3.extent = function(array, f) {
+ var i = -1, n = array.length, a, b, c;
+ if (arguments.length === 1) {
+ while (++i < n && !((a = c = array[i]) != null && a <= a)) a = c = undefined;
+ while (++i < n) if ((b = array[i]) != null) {
+ if (a > b) a = b;
+ if (c < b) c = b;
+ }
+ } else {
+ while (++i < n && !((a = c = f.call(array, array[i], i)) != null && a <= a)) a = undefined;
+ while (++i < n) if ((b = f.call(array, array[i], i)) != null) {
+ if (a > b) a = b;
+ if (c < b) c = b;
+ }
+ }
+ return [ a, c ];
+ };
+ d3.sum = function(array, f) {
+ var s = 0, n = array.length, a, i = -1;
+ if (arguments.length === 1) {
+ while (++i < n) if (!isNaN(a = +array[i])) s += a;
+ } else {
+ while (++i < n) if (!isNaN(a = +f.call(array, array[i], i))) s += a;
+ }
+ return s;
+ };
+ function d3_number(x) {
+ return x != null && !isNaN(x);
+ }
+ d3.mean = function(array, f) {
+ var n = array.length, a, m = 0, i = -1, j = 0;
+ if (arguments.length === 1) {
+ while (++i < n) if (d3_number(a = array[i])) m += (a - m) / ++j;
+ } else {
+ while (++i < n) if (d3_number(a = f.call(array, array[i], i))) m += (a - m) / ++j;
+ }
+ return j ? m : undefined;
+ };
+ d3.quantile = function(values, p) {
+ var H = (values.length - 1) * p + 1, h = Math.floor(H), v = +values[h - 1], e = H - h;
+ return e ? v + e * (values[h] - v) : v;
+ };
+ d3.median = function(array, f) {
+ if (arguments.length > 1) array = array.map(f);
+ array = array.filter(d3_number);
+ return array.length ? d3.quantile(array.sort(d3.ascending), .5) : undefined;
+ };
+ d3.bisector = function(f) {
+ return {
+ left: function(a, x, lo, hi) {
+ if (arguments.length < 3) lo = 0;
+ if (arguments.length < 4) hi = a.length;
+ while (lo < hi) {
+ var mid = lo + hi >>> 1;
+ if (f.call(a, a[mid], mid) < x) lo = mid + 1; else hi = mid;
+ }
+ return lo;
+ },
+ right: function(a, x, lo, hi) {
+ if (arguments.length < 3) lo = 0;
+ if (arguments.length < 4) hi = a.length;
+ while (lo < hi) {
+ var mid = lo + hi >>> 1;
+ if (x < f.call(a, a[mid], mid)) hi = mid; else lo = mid + 1;
+ }
+ return lo;
+ }
+ };
+ };
+ var d3_bisector = d3.bisector(function(d) {
+ return d;
+ });
+ d3.bisectLeft = d3_bisector.left;
+ d3.bisect = d3.bisectRight = d3_bisector.right;
+ d3.shuffle = function(array) {
+ var m = array.length, t, i;
+ while (m) {
+ i = Math.random() * m-- | 0;
+ t = array[m], array[m] = array[i], array[i] = t;
+ }
+ return array;
+ };
+ d3.permute = function(array, indexes) {
+ var i = indexes.length, permutes = new Array(i);
+ while (i--) permutes[i] = array[indexes[i]];
+ return permutes;
+ };
+ d3.pairs = function(array) {
+ var i = 0, n = array.length - 1, p0, p1 = array[0], pairs = new Array(n < 0 ? 0 : n);
+ while (i < n) pairs[i] = [ p0 = p1, p1 = array[++i] ];
+ return pairs;
+ };
+ d3.zip = function() {
+ if (!(n = arguments.length)) return [];
+ for (var i = -1, m = d3.min(arguments, d3_zipLength), zips = new Array(m); ++i < m; ) {
+ for (var j = -1, n, zip = zips[i] = new Array(n); ++j < n; ) {
+ zip[j] = arguments[j][i];
+ }
+ }
+ return zips;
+ };
+ function d3_zipLength(d) {
+ return d.length;
+ }
+ d3.transpose = function(matrix) {
+ return d3.zip.apply(d3, matrix);
+ };
+ d3.keys = function(map) {
+ var keys = [];
+ for (var key in map) keys.push(key);
+ return keys;
+ };
+ d3.values = function(map) {
+ var values = [];
+ for (var key in map) values.push(map[key]);
+ return values;
+ };
+ d3.entries = function(map) {
+ var entries = [];
+ for (var key in map) entries.push({
+ key: key,
+ value: map[key]
+ });
+ return entries;
+ };
+ d3.merge = function(arrays) {
+ var n = arrays.length, m, i = -1, j = 0, merged, array;
+ while (++i < n) j += arrays[i].length;
+ merged = new Array(j);
+ while (--n >= 0) {
+ array = arrays[n];
+ m = array.length;
+ while (--m >= 0) {
+ merged[--j] = array[m];
+ }
+ }
+ return merged;
+ };
+ var abs = Math.abs;
+ d3.range = function(start, stop, step) {
+ if (arguments.length < 3) {
+ step = 1;
+ if (arguments.length < 2) {
+ stop = start;
+ start = 0;
+ }
+ }
+ if ((stop - start) / step === Infinity) throw new Error("infinite range");
+ var range = [], k = d3_range_integerScale(abs(step)), i = -1, j;
+ start *= k, stop *= k, step *= k;
+ if (step < 0) while ((j = start + step * ++i) > stop) range.push(j / k); else while ((j = start + step * ++i) < stop) range.push(j / k);
+ return range;
+ };
+ function d3_range_integerScale(x) {
+ var k = 1;
+ while (x * k % 1) k *= 10;
+ return k;
+ }
+ function d3_class(ctor, properties) {
+ try {
+ for (var key in properties) {
+ Object.defineProperty(ctor.prototype, key, {
+ value: properties[key],
+ enumerable: false
+ });
+ }
+ } catch (e) {
+ ctor.prototype = properties;
+ }
+ }
+ d3.map = function(object) {
+ var map = new d3_Map();
+ if (object instanceof d3_Map) object.forEach(function(key, value) {
+ map.set(key, value);
+ }); else for (var key in object) map.set(key, object[key]);
+ return map;
+ };
+ function d3_Map() {}
+ d3_class(d3_Map, {
+ has: d3_map_has,
+ get: function(key) {
+ return this[d3_map_prefix + key];
+ },
+ set: function(key, value) {
+ return this[d3_map_prefix + key] = value;
+ },
+ remove: d3_map_remove,
+ keys: d3_map_keys,
+ values: function() {
+ var values = [];
+ this.forEach(function(key, value) {
+ values.push(value);
+ });
+ return values;
+ },
+ entries: function() {
+ var entries = [];
+ this.forEach(function(key, value) {
+ entries.push({
+ key: key,
+ value: value
+ });
+ });
+ return entries;
+ },
+ size: d3_map_size,
+ empty: d3_map_empty,
+ forEach: function(f) {
+ for (var key in this) if (key.charCodeAt(0) === d3_map_prefixCode) f.call(this, key.substring(1), this[key]);
+ }
+ });
+ var d3_map_prefix = "\x00", d3_map_prefixCode = d3_map_prefix.charCodeAt(0);
+ function d3_map_has(key) {
+ return d3_map_prefix + key in this;
+ }
+ function d3_map_remove(key) {
+ key = d3_map_prefix + key;
+ return key in this && delete this[key];
+ }
+ function d3_map_keys() {
+ var keys = [];
+ this.forEach(function(key) {
+ keys.push(key);
+ });
+ return keys;
+ }
+ function d3_map_size() {
+ var size = 0;
+ for (var key in this) if (key.charCodeAt(0) === d3_map_prefixCode) ++size;
+ return size;
+ }
+ function d3_map_empty() {
+ for (var key in this) if (key.charCodeAt(0) === d3_map_prefixCode) return false;
+ return true;
+ }
+ d3.nest = function() {
+ var nest = {}, keys = [], sortKeys = [], sortValues, rollup;
+ function map(mapType, array, depth) {
+ if (depth >= keys.length) return rollup ? rollup.call(nest, array) : sortValues ? array.sort(sortValues) : array;
+ var i = -1, n = array.length, key = keys[depth++], keyValue, object, setter, valuesByKey = new d3_Map(), values;
+ while (++i < n) {
+ if (values = valuesByKey.get(keyValue = key(object = array[i]))) {
+ values.push(object);
+ } else {
+ valuesByKey.set(keyValue, [ object ]);
+ }
+ }
+ if (mapType) {
+ object = mapType();
+ setter = function(keyValue, values) {
+ object.set(keyValue, map(mapType, values, depth));
+ };
+ } else {
+ object = {};
+ setter = function(keyValue, values) {
+ object[keyValue] = map(mapType, values, depth);
+ };
+ }
+ valuesByKey.forEach(setter);
+ return object;
+ }
+ function entries(map, depth) {
+ if (depth >= keys.length) return map;
+ var array = [], sortKey = sortKeys[depth++];
+ map.forEach(function(key, keyMap) {
+ array.push({
+ key: key,
+ values: entries(keyMap, depth)
+ });
+ });
+ return sortKey ? array.sort(function(a, b) {
+ return sortKey(a.key, b.key);
+ }) : array;
+ }
+ nest.map = function(array, mapType) {
+ return map(mapType, array, 0);
+ };
+ nest.entries = function(array) {
+ return entries(map(d3.map, array, 0), 0);
+ };
+ nest.key = function(d) {
+ keys.push(d);
+ return nest;
+ };
+ nest.sortKeys = function(order) {
+ sortKeys[keys.length - 1] = order;
+ return nest;
+ };
+ nest.sortValues = function(order) {
+ sortValues = order;
+ return nest;
+ };
+ nest.rollup = function(f) {
+ rollup = f;
+ return nest;
+ };
+ return nest;
+ };
+ d3.set = function(array) {
+ var set = new d3_Set();
+ if (array) for (var i = 0, n = array.length; i < n; ++i) set.add(array[i]);
+ return set;
+ };
+ function d3_Set() {}
+ d3_class(d3_Set, {
+ has: d3_map_has,
+ add: function(value) {
+ this[d3_map_prefix + value] = true;
+ return value;
+ },
+ remove: function(value) {
+ value = d3_map_prefix + value;
+ return value in this && delete this[value];
+ },
+ values: d3_map_keys,
+ size: d3_map_size,
+ empty: d3_map_empty,
+ forEach: function(f) {
+ for (var value in this) if (value.charCodeAt(0) === d3_map_prefixCode) f.call(this, value.substring(1));
+ }
+ });
+ d3.behavior = {};
+ d3.rebind = function(target, source) {
+ var i = 1, n = arguments.length, method;
+ while (++i < n) target[method = arguments[i]] = d3_rebind(target, source, source[method]);
+ return target;
+ };
+ function d3_rebind(target, source, method) {
+ return function() {
+ var value = method.apply(source, arguments);
+ return value === source ? target : value;
+ };
+ }
+ function d3_vendorSymbol(object, name) {
+ if (name in object) return name;
+ name = name.charAt(0).toUpperCase() + name.substring(1);
+ for (var i = 0, n = d3_vendorPrefixes.length; i < n; ++i) {
+ var prefixName = d3_vendorPrefixes[i] + name;
+ if (prefixName in object) return prefixName;
+ }
+ }
+ var d3_vendorPrefixes = [ "webkit", "ms", "moz", "Moz", "o", "O" ];
+ function d3_noop() {}
+ d3.dispatch = function() {
+ var dispatch = new d3_dispatch(), i = -1, n = arguments.length;
+ while (++i < n) dispatch[arguments[i]] = d3_dispatch_event(dispatch);
+ return dispatch;
+ };
+ function d3_dispatch() {}
+ d3_dispatch.prototype.on = function(type, listener) {
+ var i = type.indexOf("."), name = "";
+ if (i >= 0) {
+ name = type.substring(i + 1);
+ type = type.substring(0, i);
+ }
+ if (type) return arguments.length < 2 ? this[type].on(name) : this[type].on(name, listener);
+ if (arguments.length === 2) {
+ if (listener == null) for (type in this) {
+ if (this.hasOwnProperty(type)) this[type].on(name, null);
+ }
+ return this;
+ }
+ };
+ function d3_dispatch_event(dispatch) {
+ var listeners = [], listenerByName = new d3_Map();
+ function event() {
+ var z = listeners, i = -1, n = z.length, l;
+ while (++i < n) if (l = z[i].on) l.apply(this, arguments);
+ return dispatch;
+ }
+ event.on = function(name, listener) {
+ var l = listenerByName.get(name), i;
+ if (arguments.length < 2) return l && l.on;
+ if (l) {
+ l.on = null;
+ listeners = listeners.slice(0, i = listeners.indexOf(l)).concat(listeners.slice(i + 1));
+ listenerByName.remove(name);
+ }
+ if (listener) listeners.push(listenerByName.set(name, {
+ on: listener
+ }));
+ return dispatch;
+ };
+ return event;
+ }
+ d3.event = null;
+ function d3_eventPreventDefault() {
+ d3.event.preventDefault();
+ }
+ function d3_eventSource() {
+ var e = d3.event, s;
+ while (s = e.sourceEvent) e = s;
+ return e;
+ }
+ function d3_eventDispatch(target) {
+ var dispatch = new d3_dispatch(), i = 0, n = arguments.length;
+ while (++i < n) dispatch[arguments[i]] = d3_dispatch_event(dispatch);
+ dispatch.of = function(thiz, argumentz) {
+ return function(e1) {
+ try {
+ var e0 = e1.sourceEvent = d3.event;
+ e1.target = target;
+ d3.event = e1;
+ dispatch[e1.type].apply(thiz, argumentz);
+ } finally {
+ d3.event = e0;
+ }
+ };
+ };
+ return dispatch;
+ }
+ d3.requote = function(s) {
+ return s.replace(d3_requote_re, "\\$&");
+ };
+ var d3_requote_re = /[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g;
+ var d3_subclass = {}.__proto__ ? function(object, prototype) {
+ object.__proto__ = prototype;
+ } : function(object, prototype) {
+ for (var property in prototype) object[property] = prototype[property];
+ };
+ function d3_selection(groups) {
+ d3_subclass(groups, d3_selectionPrototype);
+ return groups;
+ }
+ var d3_select = function(s, n) {
+ return n.querySelector(s);
+ }, d3_selectAll = function(s, n) {
+ return n.querySelectorAll(s);
+ }, d3_selectMatcher = d3_documentElement[d3_vendorSymbol(d3_documentElement, "matchesSelector")], d3_selectMatches = function(n, s) {
+ return d3_selectMatcher.call(n, s);
+ };
+ if (typeof Sizzle === "function") {
+ d3_select = function(s, n) {
+ return Sizzle(s, n)[0] || null;
+ };
+ d3_selectAll = function(s, n) {
+ return Sizzle.uniqueSort(Sizzle(s, n));
+ };
+ d3_selectMatches = Sizzle.matchesSelector;
+ }
+ d3.selection = function() {
+ return d3_selectionRoot;
+ };
+ var d3_selectionPrototype = d3.selection.prototype = [];
+ d3_selectionPrototype.select = function(selector) {
+ var subgroups = [], subgroup, subnode, group, node;
+ selector = d3_selection_selector(selector);
+ for (var j = -1, m = this.length; ++j < m; ) {
+ subgroups.push(subgroup = []);
+ subgroup.parentNode = (group = this[j]).parentNode;
+ for (var i = -1, n = group.length; ++i < n; ) {
+ if (node = group[i]) {
+ subgroup.push(subnode = selector.call(node, node.__data__, i, j));
+ if (subnode && "__data__" in node) subnode.__data__ = node.__data__;
+ } else {
+ subgroup.push(null);
+ }
+ }
+ }
+ return d3_selection(subgroups);
+ };
+ function d3_selection_selector(selector) {
+ return typeof selector === "function" ? selector : function() {
+ return d3_select(selector, this);
+ };
+ }
+ d3_selectionPrototype.selectAll = function(selector) {
+ var subgroups = [], subgroup, node;
+ selector = d3_selection_selectorAll(selector);
+ for (var j = -1, m = this.length; ++j < m; ) {
+ for (var group = this[j], i = -1, n = group.length; ++i < n; ) {
+ if (node = group[i]) {
+ subgroups.push(subgroup = d3_array(selector.call(node, node.__data__, i, j)));
+ subgroup.parentNode = node;
+ }
+ }
+ }
+ return d3_selection(subgroups);
+ };
+ function d3_selection_selectorAll(selector) {
+ return typeof selector === "function" ? selector : function() {
+ return d3_selectAll(selector, this);
+ };
+ }
+ var d3_nsPrefix = {
+ svg: "http://www.w3.org/2000/svg",
+ xhtml: "http://www.w3.org/1999/xhtml",
+ xlink: "http://www.w3.org/1999/xlink",
+ xml: "http://www.w3.org/XML/1998/namespace",
+ xmlns: "http://www.w3.org/2000/xmlns/"
+ };
+ d3.ns = {
+ prefix: d3_nsPrefix,
+ qualify: function(name) {
+ var i = name.indexOf(":"), prefix = name;
+ if (i >= 0) {
+ prefix = name.substring(0, i);
+ name = name.substring(i + 1);
+ }
+ return d3_nsPrefix.hasOwnProperty(prefix) ? {
+ space: d3_nsPrefix[prefix],
+ local: name
+ } : name;
+ }
+ };
+ d3_selectionPrototype.attr = function(name, value) {
+ if (arguments.length < 2) {
+ if (typeof name === "string") {
+ var node = this.node();
+ name = d3.ns.qualify(name);
+ return name.local ? node.getAttributeNS(name.space, name.local) : node.getAttribute(name);
+ }
+ for (value in name) this.each(d3_selection_attr(value, name[value]));
+ return this;
+ }
+ return this.each(d3_selection_attr(name, value));
+ };
+ function d3_selection_attr(name, value) {
+ name = d3.ns.qualify(name);
+ function attrNull() {
+ this.removeAttribute(name);
+ }
+ function attrNullNS() {
+ this.removeAttributeNS(name.space, name.local);
+ }
+ function attrConstant() {
+ this.setAttribute(name, value);
+ }
+ function attrConstantNS() {
+ this.setAttributeNS(name.space, name.local, value);
+ }
+ function attrFunction() {
+ var x = value.apply(this, arguments);
+ if (x == null) this.removeAttribute(name); else this.setAttribute(name, x);
+ }
+ function attrFunctionNS() {
+ var x = value.apply(this, arguments);
+ if (x == null) this.removeAttributeNS(name.space, name.local); else this.setAttributeNS(name.space, name.local, x);
+ }
+ return value == null ? name.local ? attrNullNS : attrNull : typeof value === "function" ? name.local ? attrFunctionNS : attrFunction : name.local ? attrConstantNS : attrConstant;
+ }
+ function d3_collapse(s) {
+ return s.trim().replace(/\s+/g, " ");
+ }
+ d3_selectionPrototype.classed = function(name, value) {
+ if (arguments.length < 2) {
+ if (typeof name === "string") {
+ var node = this.node(), n = (name = d3_selection_classes(name)).length, i = -1;
+ if (value = node.classList) {
+ while (++i < n) if (!value.contains(name[i])) return false;
+ } else {
+ value = node.getAttribute("class");
+ while (++i < n) if (!d3_selection_classedRe(name[i]).test(value)) return false;
+ }
+ return true;
+ }
+ for (value in name) this.each(d3_selection_classed(value, name[value]));
+ return this;
+ }
+ return this.each(d3_selection_classed(name, value));
+ };
+ function d3_selection_classedRe(name) {
+ return new RegExp("(?:^|\\s+)" + d3.requote(name) + "(?:\\s+|$)", "g");
+ }
+ function d3_selection_classes(name) {
+ return name.trim().split(/^|\s+/);
+ }
+ function d3_selection_classed(name, value) {
+ name = d3_selection_classes(name).map(d3_selection_classedName);
+ var n = name.length;
+ function classedConstant() {
+ var i = -1;
+ while (++i < n) name[i](this, value);
+ }
+ function classedFunction() {
+ var i = -1, x = value.apply(this, arguments);
+ while (++i < n) name[i](this, x);
+ }
+ return typeof value === "function" ? classedFunction : classedConstant;
+ }
+ function d3_selection_classedName(name) {
+ var re = d3_selection_classedRe(name);
+ return function(node, value) {
+ if (c = node.classList) return value ? c.add(name) : c.remove(name);
+ var c = node.getAttribute("class") || "";
+ if (value) {
+ re.lastIndex = 0;
+ if (!re.test(c)) node.setAttribute("class", d3_collapse(c + " " + name));
+ } else {
+ node.setAttribute("class", d3_collapse(c.replace(re, " ")));
+ }
+ };
+ }
+ d3_selectionPrototype.style = function(name, value, priority) {
+ var n = arguments.length;
+ if (n < 3) {
+ if (typeof name !== "string") {
+ if (n < 2) value = "";
+ for (priority in name) this.each(d3_selection_style(priority, name[priority], value));
+ return this;
+ }
+ if (n < 2) return d3_window.getComputedStyle(this.node(), null).getPropertyValue(name);
+ priority = "";
+ }
+ return this.each(d3_selection_style(name, value, priority));
+ };
+ function d3_selection_style(name, value, priority) {
+ function styleNull() {
+ this.style.removeProperty(name);
+ }
+ function styleConstant() {
+ this.style.setProperty(name, value, priority);
+ }
+ function styleFunction() {
+ var x = value.apply(this, arguments);
+ if (x == null) this.style.removeProperty(name); else this.style.setProperty(name, x, priority);
+ }
+ return value == null ? styleNull : typeof value === "function" ? styleFunction : styleConstant;
+ }
+ d3_selectionPrototype.property = function(name, value) {
+ if (arguments.length < 2) {
+ if (typeof name === "string") return this.node()[name];
+ for (value in name) this.each(d3_selection_property(value, name[value]));
+ return this;
+ }
+ return this.each(d3_selection_property(name, value));
+ };
+ function d3_selection_property(name, value) {
+ function propertyNull() {
+ delete this[name];
+ }
+ function propertyConstant() {
+ this[name] = value;
+ }
+ function propertyFunction() {
+ var x = value.apply(this, arguments);
+ if (x == null) delete this[name]; else this[name] = x;
+ }
+ return value == null ? propertyNull : typeof value === "function" ? propertyFunction : propertyConstant;
+ }
+ d3_selectionPrototype.text = function(value) {
+ return arguments.length ? this.each(typeof value === "function" ? function() {
+ var v = value.apply(this, arguments);
+ this.textContent = v == null ? "" : v;
+ } : value == null ? function() {
+ this.textContent = "";
+ } : function() {
+ this.textContent = value;
+ }) : this.node().textContent;
+ };
+ d3_selectionPrototype.html = function(value) {
+ return arguments.length ? this.each(typeof value === "function" ? function() {
+ var v = value.apply(this, arguments);
+ this.innerHTML = v == null ? "" : v;
+ } : value == null ? function() {
+ this.innerHTML = "";
+ } : function() {
+ this.innerHTML = value;
+ }) : this.node().innerHTML;
+ };
+ d3_selectionPrototype.append = function(name) {
+ name = d3_selection_creator(name);
+ return this.select(function() {
+ return this.appendChild(name.apply(this, arguments));
+ });
+ };
+ function d3_selection_creator(name) {
+ return typeof name === "function" ? name : (name = d3.ns.qualify(name)).local ? function() {
+ return this.ownerDocument.createElementNS(name.space, name.local);
+ } : function() {
+ return this.ownerDocument.createElementNS(this.namespaceURI, name);
+ };
+ }
+ d3_selectionPrototype.insert = function(name, before) {
+ name = d3_selection_creator(name);
+ before = d3_selection_selector(before);
+ return this.select(function() {
+ return this.insertBefore(name.apply(this, arguments), before.apply(this, arguments) || null);
+ });
+ };
+ d3_selectionPrototype.remove = function() {
+ return this.each(function() {
+ var parent = this.parentNode;
+ if (parent) parent.removeChild(this);
+ });
+ };
+ d3_selectionPrototype.data = function(value, key) {
+ var i = -1, n = this.length, group, node;
+ if (!arguments.length) {
+ value = new Array(n = (group = this[0]).length);
+ while (++i < n) {
+ if (node = group[i]) {
+ value[i] = node.__data__;
+ }
+ }
+ return value;
+ }
+ function bind(group, groupData) {
+ var i, n = group.length, m = groupData.length, n0 = Math.min(n, m), updateNodes = new Array(m), enterNodes = new Array(m), exitNodes = new Array(n), node, nodeData;
+ if (key) {
+ var nodeByKeyValue = new d3_Map(), dataByKeyValue = new d3_Map(), keyValues = [], keyValue;
+ for (i = -1; ++i < n; ) {
+ keyValue = key.call(node = group[i], node.__data__, i);
+ if (nodeByKeyValue.has(keyValue)) {
+ exitNodes[i] = node;
+ } else {
+ nodeByKeyValue.set(keyValue, node);
+ }
+ keyValues.push(keyValue);
+ }
+ for (i = -1; ++i < m; ) {
+ keyValue = key.call(groupData, nodeData = groupData[i], i);
+ if (node = nodeByKeyValue.get(keyValue)) {
+ updateNodes[i] = node;
+ node.__data__ = nodeData;
+ } else if (!dataByKeyValue.has(keyValue)) {
+ enterNodes[i] = d3_selection_dataNode(nodeData);
+ }
+ dataByKeyValue.set(keyValue, nodeData);
+ nodeByKeyValue.remove(keyValue);
+ }
+ for (i = -1; ++i < n; ) {
+ if (nodeByKeyValue.has(keyValues[i])) {
+ exitNodes[i] = group[i];
+ }
+ }
+ } else {
+ for (i = -1; ++i < n0; ) {
+ node = group[i];
+ nodeData = groupData[i];
+ if (node) {
+ node.__data__ = nodeData;
+ updateNodes[i] = node;
+ } else {
+ enterNodes[i] = d3_selection_dataNode(nodeData);
+ }
+ }
+ for (;i < m; ++i) {
+ enterNodes[i] = d3_selection_dataNode(groupData[i]);
+ }
+ for (;i < n; ++i) {
+ exitNodes[i] = group[i];
+ }
+ }
+ enterNodes.update = updateNodes;
+ enterNodes.parentNode = updateNodes.parentNode = exitNodes.parentNode = group.parentNode;
+ enter.push(enterNodes);
+ update.push(updateNodes);
+ exit.push(exitNodes);
+ }
+ var enter = d3_selection_enter([]), update = d3_selection([]), exit = d3_selection([]);
+ if (typeof value === "function") {
+ while (++i < n) {
+ bind(group = this[i], value.call(group, group.parentNode.__data__, i));
+ }
+ } else {
+ while (++i < n) {
+ bind(group = this[i], value);
+ }
+ }
+ update.enter = function() {
+ return enter;
+ };
+ update.exit = function() {
+ return exit;
+ };
+ return update;
+ };
+ function d3_selection_dataNode(data) {
+ return {
+ __data__: data
+ };
+ }
+ d3_selectionPrototype.datum = function(value) {
+ return arguments.length ? this.property("__data__", value) : this.property("__data__");
+ };
+ d3_selectionPrototype.filter = function(filter) {
+ var subgroups = [], subgroup, group, node;
+ if (typeof filter !== "function") filter = d3_selection_filter(filter);
+ for (var j = 0, m = this.length; j < m; j++) {
+ subgroups.push(subgroup = []);
+ subgroup.parentNode = (group = this[j]).parentNode;
+ for (var i = 0, n = group.length; i < n; i++) {
+ if ((node = group[i]) && filter.call(node, node.__data__, i, j)) {
+ subgroup.push(node);
+ }
+ }
+ }
+ return d3_selection(subgroups);
+ };
+ function d3_selection_filter(selector) {
+ return function() {
+ return d3_selectMatches(this, selector);
+ };
+ }
+ d3_selectionPrototype.order = function() {
+ for (var j = -1, m = this.length; ++j < m; ) {
+ for (var group = this[j], i = group.length - 1, next = group[i], node; --i >= 0; ) {
+ if (node = group[i]) {
+ if (next && next !== node.nextSibling) next.parentNode.insertBefore(node, next);
+ next = node;
+ }
+ }
+ }
+ return this;
+ };
+ d3_selectionPrototype.sort = function(comparator) {
+ comparator = d3_selection_sortComparator.apply(this, arguments);
+ for (var j = -1, m = this.length; ++j < m; ) this[j].sort(comparator);
+ return this.order();
+ };
+ function d3_selection_sortComparator(comparator) {
+ if (!arguments.length) comparator = d3.ascending;
+ return function(a, b) {
+ return a && b ? comparator(a.__data__, b.__data__) : !a - !b;
+ };
+ }
+ d3_selectionPrototype.each = function(callback) {
+ return d3_selection_each(this, function(node, i, j) {
+ callback.call(node, node.__data__, i, j);
+ });
+ };
+ function d3_selection_each(groups, callback) {
+ for (var j = 0, m = groups.length; j < m; j++) {
+ for (var group = groups[j], i = 0, n = group.length, node; i < n; i++) {
+ if (node = group[i]) callback(node, i, j);
+ }
+ }
+ return groups;
+ }
+ d3_selectionPrototype.call = function(callback) {
+ var args = d3_array(arguments);
+ callback.apply(args[0] = this, args);
+ return this;
+ };
+ d3_selectionPrototype.empty = function() {
+ return !this.node();
+ };
+ d3_selectionPrototype.node = function() {
+ for (var j = 0, m = this.length; j < m; j++) {
+ for (var group = this[j], i = 0, n = group.length; i < n; i++) {
+ var node = group[i];
+ if (node) return node;
+ }
+ }
+ return null;
+ };
+ d3_selectionPrototype.size = function() {
+ var n = 0;
+ this.each(function() {
+ ++n;
+ });
+ return n;
+ };
+ function d3_selection_enter(selection) {
+ d3_subclass(selection, d3_selection_enterPrototype);
+ return selection;
+ }
+ var d3_selection_enterPrototype = [];
+ d3.selection.enter = d3_selection_enter;
+ d3.selection.enter.prototype = d3_selection_enterPrototype;
+ d3_selection_enterPrototype.append = d3_selectionPrototype.append;
+ d3_selection_enterPrototype.empty = d3_selectionPrototype.empty;
+ d3_selection_enterPrototype.node = d3_selectionPrototype.node;
+ d3_selection_enterPrototype.call = d3_selectionPrototype.call;
+ d3_selection_enterPrototype.size = d3_selectionPrototype.size;
+ d3_selection_enterPrototype.select = function(selector) {
+ var subgroups = [], subgroup, subnode, upgroup, group, node;
+ for (var j = -1, m = this.length; ++j < m; ) {
+ upgroup = (group = this[j]).update;
+ subgroups.push(subgroup = []);
+ subgroup.parentNode = group.parentNode;
+ for (var i = -1, n = group.length; ++i < n; ) {
+ if (node = group[i]) {
+ subgroup.push(upgroup[i] = subnode = selector.call(group.parentNode, node.__data__, i, j));
+ subnode.__data__ = node.__data__;
+ } else {
+ subgroup.push(null);
+ }
+ }
+ }
+ return d3_selection(subgroups);
+ };
+ d3_selection_enterPrototype.insert = function(name, before) {
+ if (arguments.length < 2) before = d3_selection_enterInsertBefore(this);
+ return d3_selectionPrototype.insert.call(this, name, before);
+ };
+ function d3_selection_enterInsertBefore(enter) {
+ var i0, j0;
+ return function(d, i, j) {
+ var group = enter[j].update, n = group.length, node;
+ if (j != j0) j0 = j, i0 = 0;
+ if (i >= i0) i0 = i + 1;
+ while (!(node = group[i0]) && ++i0 < n) ;
+ return node;
+ };
+ }
+ d3_selectionPrototype.transition = function() {
+ var id = d3_transitionInheritId || ++d3_transitionId, subgroups = [], subgroup, node, transition = d3_transitionInherit || {
+ time: Date.now(),
+ ease: d3_ease_cubicInOut,
+ delay: 0,
+ duration: 250
+ };
+ for (var j = -1, m = this.length; ++j < m; ) {
+ subgroups.push(subgroup = []);
+ for (var group = this[j], i = -1, n = group.length; ++i < n; ) {
+ if (node = group[i]) d3_transitionNode(node, i, id, transition);
+ subgroup.push(node);
+ }
+ }
+ return d3_transition(subgroups, id);
+ };
+ d3_selectionPrototype.interrupt = function() {
+ return this.each(d3_selection_interrupt);
+ };
+ function d3_selection_interrupt() {
+ var lock = this.__transition__;
+ if (lock) ++lock.active;
+ }
+ d3.select = function(node) {
+ var group = [ typeof node === "string" ? d3_select(node, d3_document) : node ];
+ group.parentNode = d3_documentElement;
+ return d3_selection([ group ]);
+ };
+ d3.selectAll = function(nodes) {
+ var group = d3_array(typeof nodes === "string" ? d3_selectAll(nodes, d3_document) : nodes);
+ group.parentNode = d3_documentElement;
+ return d3_selection([ group ]);
+ };
+ var d3_selectionRoot = d3.select(d3_documentElement);
+ d3_selectionPrototype.on = function(type, listener, capture) {
+ var n = arguments.length;
+ if (n < 3) {
+ if (typeof type !== "string") {
+ if (n < 2) listener = false;
+ for (capture in type) this.each(d3_selection_on(capture, type[capture], listener));
+ return this;
+ }
+ if (n < 2) return (n = this.node()["__on" + type]) && n._;
+ capture = false;
+ }
+ return this.each(d3_selection_on(type, listener, capture));
+ };
+ function d3_selection_on(type, listener, capture) {
+ var name = "__on" + type, i = type.indexOf("."), wrap = d3_selection_onListener;
+ if (i > 0) type = type.substring(0, i);
+ var filter = d3_selection_onFilters.get(type);
+ if (filter) type = filter, wrap = d3_selection_onFilter;
+ function onRemove() {
+ var l = this[name];
+ if (l) {
+ this.removeEventListener(type, l, l.$);
+ delete this[name];
+ }
+ }
+ function onAdd() {
+ var l = wrap(listener, d3_array(arguments));
+ onRemove.call(this);
+ this.addEventListener(type, this[name] = l, l.$ = capture);
+ l._ = listener;
+ }
+ function removeAll() {
+ var re = new RegExp("^__on([^.]+)" + d3.requote(type) + "$"), match;
+ for (var name in this) {
+ if (match = name.match(re)) {
+ var l = this[name];
+ this.removeEventListener(match[1], l, l.$);
+ delete this[name];
+ }
+ }
+ }
+ return i ? listener ? onAdd : onRemove : listener ? d3_noop : removeAll;
+ }
+ var d3_selection_onFilters = d3.map({
+ mouseenter: "mouseover",
+ mouseleave: "mouseout"
+ });
+ d3_selection_onFilters.forEach(function(k) {
+ if ("on" + k in d3_document) d3_selection_onFilters.remove(k);
+ });
+ function d3_selection_onListener(listener, argumentz) {
+ return function(e) {
+ var o = d3.event;
+ d3.event = e;
+ argumentz[0] = this.__data__;
+ try {
+ listener.apply(this, argumentz);
+ } finally {
+ d3.event = o;
+ }
+ };
+ }
+ function d3_selection_onFilter(listener, argumentz) {
+ var l = d3_selection_onListener(listener, argumentz);
+ return function(e) {
+ var target = this, related = e.relatedTarget;
+ if (!related || related !== target && !(related.compareDocumentPosition(target) & 8)) {
+ l.call(target, e);
+ }
+ };
+ }
+ var d3_event_dragSelect = "onselectstart" in d3_document ? null : d3_vendorSymbol(d3_documentElement.style, "userSelect"), d3_event_dragId = 0;
+ function d3_event_dragSuppress() {
+ var name = ".dragsuppress-" + ++d3_event_dragId, click = "click" + name, w = d3.select(d3_window).on("touchmove" + name, d3_eventPreventDefault).on("dragstart" + name, d3_eventPreventDefault).on("selectstart" + name, d3_eventPreventDefault);
+ if (d3_event_dragSelect) {
+ var style = d3_documentElement.style, select = style[d3_event_dragSelect];
+ style[d3_event_dragSelect] = "none";
+ }
+ return function(suppressClick) {
+ w.on(name, null);
+ if (d3_event_dragSelect) style[d3_event_dragSelect] = select;
+ if (suppressClick) {
+ function off() {
+ w.on(click, null);
+ }
+ w.on(click, function() {
+ d3_eventPreventDefault();
+ off();
+ }, true);
+ setTimeout(off, 0);
+ }
+ };
+ }
+ d3.mouse = function(container) {
+ return d3_mousePoint(container, d3_eventSource());
+ };
+ var d3_mouse_bug44083 = /WebKit/.test(d3_window.navigator.userAgent) ? -1 : 0;
+ function d3_mousePoint(container, e) {
+ if (e.changedTouches) e = e.changedTouches[0];
+ var svg = container.ownerSVGElement || container;
+ if (svg.createSVGPoint) {
+ var point = svg.createSVGPoint();
+ if (d3_mouse_bug44083 < 0 && (d3_window.scrollX || d3_window.scrollY)) {
+ svg = d3.select("body").append("svg").style({
+ position: "absolute",
+ top: 0,
+ left: 0,
+ margin: 0,
+ padding: 0,
+ border: "none"
+ }, "important");
+ var ctm = svg[0][0].getScreenCTM();
+ d3_mouse_bug44083 = !(ctm.f || ctm.e);
+ svg.remove();
+ }
+ if (d3_mouse_bug44083) point.x = e.pageX, point.y = e.pageY; else point.x = e.clientX,
+ point.y = e.clientY;
+ point = point.matrixTransform(container.getScreenCTM().inverse());
+ return [ point.x, point.y ];
+ }
+ var rect = container.getBoundingClientRect();
+ return [ e.clientX - rect.left - container.clientLeft, e.clientY - rect.top - container.clientTop ];
+ }
+ d3.touches = function(container, touches) {
+ if (arguments.length < 2) touches = d3_eventSource().touches;
+ return touches ? d3_array(touches).map(function(touch) {
+ var point = d3_mousePoint(container, touch);
+ point.identifier = touch.identifier;
+ return point;
+ }) : [];
+ };
+ d3.behavior.drag = function() {
+ var event = d3_eventDispatch(drag, "drag", "dragstart", "dragend"), origin = null, mousedown = dragstart(d3_noop, d3.mouse, "mousemove", "mouseup"), touchstart = dragstart(touchid, touchposition, "touchmove", "touchend");
+ function drag() {
+ this.on("mousedown.drag", mousedown).on("touchstart.drag", touchstart);
+ }
+ function touchid() {
+ return d3.event.changedTouches[0].identifier;
+ }
+ function touchposition(parent, id) {
+ return d3.touches(parent).filter(function(p) {
+ return p.identifier === id;
+ })[0];
+ }
+ function dragstart(id, position, move, end) {
+ return function() {
+ var target = this, parent = target.parentNode, event_ = event.of(target, arguments), eventTarget = d3.event.target, eventId = id(), drag = eventId == null ? "drag" : "drag-" + eventId, origin_ = position(parent, eventId), dragged = 0, offset, w = d3.select(d3_window).on(move + "." + drag, moved).on(end + "." + drag, ended), dragRestore = d3_event_dragSuppress();
+ if (origin) {
+ offset = origin.apply(target, arguments);
+ offset = [ offset.x - origin_[0], offset.y - origin_[1] ];
+ } else {
+ offset = [ 0, 0 ];
+ }
+ event_({
+ type: "dragstart"
+ });
+ function moved() {
+ var p = position(parent, eventId), dx = p[0] - origin_[0], dy = p[1] - origin_[1];
+ dragged |= dx | dy;
+ origin_ = p;
+ event_({
+ type: "drag",
+ x: p[0] + offset[0],
+ y: p[1] + offset[1],
+ dx: dx,
+ dy: dy
+ });
+ }
+ function ended() {
+ w.on(move + "." + drag, null).on(end + "." + drag, null);
+ dragRestore(dragged && d3.event.target === eventTarget);
+ event_({
+ type: "dragend"
+ });
+ }
+ };
+ }
+ drag.origin = function(x) {
+ if (!arguments.length) return origin;
+ origin = x;
+ return drag;
+ };
+ return d3.rebind(drag, event, "on");
+ };
+ var π = Math.PI, τ = 2 * π, halfπ = π / 2, ε = 1e-6, ε2 = ε * ε, d3_radians = π / 180, d3_degrees = 180 / π;
+ function d3_sgn(x) {
+ return x > 0 ? 1 : x < 0 ? -1 : 0;
+ }
+ function d3_cross2d(a, b, c) {
+ return (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]);
+ }
+ function d3_acos(x) {
+ return x > 1 ? 0 : x < -1 ? π : Math.acos(x);
+ }
+ function d3_asin(x) {
+ return x > 1 ? halfπ : x < -1 ? -halfπ : Math.asin(x);
+ }
+ function d3_sinh(x) {
+ return ((x = Math.exp(x)) - 1 / x) / 2;
+ }
+ function d3_cosh(x) {
+ return ((x = Math.exp(x)) + 1 / x) / 2;
+ }
+ function d3_tanh(x) {
+ return ((x = Math.exp(2 * x)) - 1) / (x + 1);
+ }
+ function d3_haversin(x) {
+ return (x = Math.sin(x / 2)) * x;
+ }
+ var Ï = Math.SQRT2, Ï2 = 2, Ï4 = 4;
+ d3.interpolateZoom = function(p0, p1) {
+ var ux0 = p0[0], uy0 = p0[1], w0 = p0[2], ux1 = p1[0], uy1 = p1[1], w1 = p1[2];
+ var dx = ux1 - ux0, dy = uy1 - uy0, d2 = dx * dx + dy * dy, d1 = Math.sqrt(d2), b0 = (w1 * w1 - w0 * w0 + Ï4 * d2) / (2 * w0 * Ï2 * d1), b1 = (w1 * w1 - w0 * w0 - Ï4 * d2) / (2 * w1 * Ï2 * d1), r0 = Math.log(Math.sqrt(b0 * b0 + 1) - b0), r1 = Math.log(Math.sqrt(b1 * b1 + 1) - b1), dr = r1 - r0, S = (dr || Math.log(w1 / w0)) / Ï;
+ function interpolate(t) {
+ var s = t * S;
+ if (dr) {
+ var coshr0 = d3_cosh(r0), u = w0 / (Ï2 * d1) * (coshr0 * d3_tanh(Ï * s + r0) - d3_sinh(r0));
+ return [ ux0 + u * dx, uy0 + u * dy, w0 * coshr0 / d3_cosh(Ï * s + r0) ];
+ }
+ return [ ux0 + t * dx, uy0 + t * dy, w0 * Math.exp(Ï * s) ];
+ }
+ interpolate.duration = S * 1e3;
+ return interpolate;
+ };
+ d3.behavior.zoom = function() {
+ var view = {
+ x: 0,
+ y: 0,
+ k: 1
+ }, translate0, center, size = [ 960, 500 ], scaleExtent = d3_behavior_zoomInfinity, mousedown = "mousedown.zoom", mousemove = "mousemove.zoom", mouseup = "mouseup.zoom", mousewheelTimer, touchstart = "touchstart.zoom", touchtime, event = d3_eventDispatch(zoom, "zoomstart", "zoom", "zoomend"), x0, x1, y0, y1;
+ function zoom(g) {
+ g.on(mousedown, mousedowned).on(d3_behavior_zoomWheel + ".zoom", mousewheeled).on(mousemove, mousewheelreset).on("dblclick.zoom", dblclicked).on(touchstart, touchstarted);
+ }
+ zoom.event = function(g) {
+ g.each(function() {
+ var event_ = event.of(this, arguments), view1 = view;
+ if (d3_transitionInheritId) {
+ d3.select(this).transition().each("start.zoom", function() {
+ view = this.__chart__ || {
+ x: 0,
+ y: 0,
+ k: 1
+ };
+ zoomstarted(event_);
+ }).tween("zoom:zoom", function() {
+ var dx = size[0], dy = size[1], cx = dx / 2, cy = dy / 2, i = d3.interpolateZoom([ (cx - view.x) / view.k, (cy - view.y) / view.k, dx / view.k ], [ (cx - view1.x) / view1.k, (cy - view1.y) / view1.k, dx / view1.k ]);
+ return function(t) {
+ var l = i(t), k = dx / l[2];
+ this.__chart__ = view = {
+ x: cx - l[0] * k,
+ y: cy - l[1] * k,
+ k: k
+ };
+ zoomed(event_);
+ };
+ }).each("end.zoom", function() {
+ zoomended(event_);
+ });
+ } else {
+ this.__chart__ = view;
+ zoomstarted(event_);
+ zoomed(event_);
+ zoomended(event_);
+ }
+ });
+ };
+ zoom.translate = function(_) {
+ if (!arguments.length) return [ view.x, view.y ];
+ view = {
+ x: +_[0],
+ y: +_[1],
+ k: view.k
+ };
+ rescale();
+ return zoom;
+ };
+ zoom.scale = function(_) {
+ if (!arguments.length) return view.k;
+ view = {
+ x: view.x,
+ y: view.y,
+ k: +_
+ };
+ rescale();
+ return zoom;
+ };
+ zoom.scaleExtent = function(_) {
+ if (!arguments.length) return scaleExtent;
+ scaleExtent = _ == null ? d3_behavior_zoomInfinity : [ +_[0], +_[1] ];
+ return zoom;
+ };
+ zoom.center = function(_) {
+ if (!arguments.length) return center;
+ center = _ && [ +_[0], +_[1] ];
+ return zoom;
+ };
+ zoom.size = function(_) {
+ if (!arguments.length) return size;
+ size = _ && [ +_[0], +_[1] ];
+ return zoom;
+ };
+ zoom.x = function(z) {
+ if (!arguments.length) return x1;
+ x1 = z;
+ x0 = z.copy();
+ view = {
+ x: 0,
+ y: 0,
+ k: 1
+ };
+ return zoom;
+ };
+ zoom.y = function(z) {
+ if (!arguments.length) return y1;
+ y1 = z;
+ y0 = z.copy();
+ view = {
+ x: 0,
+ y: 0,
+ k: 1
+ };
+ return zoom;
+ };
+ function location(p) {
+ return [ (p[0] - view.x) / view.k, (p[1] - view.y) / view.k ];
+ }
+ function point(l) {
+ return [ l[0] * view.k + view.x, l[1] * view.k + view.y ];
+ }
+ function scaleTo(s) {
+ view.k = Math.max(scaleExtent[0], Math.min(scaleExtent[1], s));
+ }
+ function translateTo(p, l) {
+ l = point(l);
+ view.x += p[0] - l[0];
+ view.y += p[1] - l[1];
+ }
+ function rescale() {
+ if (x1) x1.domain(x0.range().map(function(x) {
+ return (x - view.x) / view.k;
+ }).map(x0.invert));
+ if (y1) y1.domain(y0.range().map(function(y) {
+ return (y - view.y) / view.k;
+ }).map(y0.invert));
+ }
+ function zoomstarted(event) {
+ event({
+ type: "zoomstart"
+ });
+ }
+ function zoomed(event) {
+ rescale();
+ event({
+ type: "zoom",
+ scale: view.k,
+ translate: [ view.x, view.y ]
+ });
+ }
+ function zoomended(event) {
+ event({
+ type: "zoomend"
+ });
+ }
+ function mousedowned() {
+ var target = this, event_ = event.of(target, arguments), eventTarget = d3.event.target, dragged = 0, w = d3.select(d3_window).on(mousemove, moved).on(mouseup, ended), l = location(d3.mouse(target)), dragRestore = d3_event_dragSuppress();
+ d3_selection_interrupt.call(target);
+ zoomstarted(event_);
+ function moved() {
+ dragged = 1;
+ translateTo(d3.mouse(target), l);
+ zoomed(event_);
+ }
+ function ended() {
+ w.on(mousemove, d3_window === target ? mousewheelreset : null).on(mouseup, null);
+ dragRestore(dragged && d3.event.target === eventTarget);
+ zoomended(event_);
+ }
+ }
+ function touchstarted() {
+ var target = this, event_ = event.of(target, arguments), locations0 = {}, distance0 = 0, scale0, eventId = d3.event.changedTouches[0].identifier, touchmove = "touchmove.zoom-" + eventId, touchend = "touchend.zoom-" + eventId, w = d3.select(d3_window).on(touchmove, moved).on(touchend, ended), t = d3.select(target).on(mousedown, null).on(touchstart, started), dragRestore = d3_event_dragSuppress();
+ d3_selection_interrupt.call(target);
+ started();
+ zoomstarted(event_);
+ function relocate() {
+ var touches = d3.touches(target);
+ scale0 = view.k;
+ touches.forEach(function(t) {
+ if (t.identifier in locations0) locations0[t.identifier] = location(t);
+ });
+ return touches;
+ }
+ function started() {
+ var changed = d3.event.changedTouches;
+ for (var i = 0, n = changed.length; i < n; ++i) {
+ locations0[changed[i].identifier] = null;
+ }
+ var touches = relocate(), now = Date.now();
+ if (touches.length === 1) {
+ if (now - touchtime < 500) {
+ var p = touches[0], l = locations0[p.identifier];
+ scaleTo(view.k * 2);
+ translateTo(p, l);
+ d3_eventPreventDefault();
+ zoomed(event_);
+ }
+ touchtime = now;
+ } else if (touches.length > 1) {
+ var p = touches[0], q = touches[1], dx = p[0] - q[0], dy = p[1] - q[1];
+ distance0 = dx * dx + dy * dy;
+ }
+ }
+ function moved() {
+ var touches = d3.touches(target), p0, l0, p1, l1;
+ for (var i = 0, n = touches.length; i < n; ++i, l1 = null) {
+ p1 = touches[i];
+ if (l1 = locations0[p1.identifier]) {
+ if (l0) break;
+ p0 = p1, l0 = l1;
+ }
+ }
+ if (l1) {
+ var distance1 = (distance1 = p1[0] - p0[0]) * distance1 + (distance1 = p1[1] - p0[1]) * distance1, scale1 = distance0 && Math.sqrt(distance1 / distance0);
+ p0 = [ (p0[0] + p1[0]) / 2, (p0[1] + p1[1]) / 2 ];
+ l0 = [ (l0[0] + l1[0]) / 2, (l0[1] + l1[1]) / 2 ];
+ scaleTo(scale1 * scale0);
+ }
+ touchtime = null;
+ translateTo(p0, l0);
+ zoomed(event_);
+ }
+ function ended() {
+ if (d3.event.touches.length) {
+ var changed = d3.event.changedTouches;
+ for (var i = 0, n = changed.length; i < n; ++i) {
+ delete locations0[changed[i].identifier];
+ }
+ for (var identifier in locations0) {
+ return void relocate();
+ }
+ }
+ w.on(touchmove, null).on(touchend, null);
+ t.on(mousedown, mousedowned).on(touchstart, touchstarted);
+ dragRestore();
+ zoomended(event_);
+ }
+ }
+ function mousewheeled() {
+ var event_ = event.of(this, arguments);
+ if (mousewheelTimer) clearTimeout(mousewheelTimer); else d3_selection_interrupt.call(this),
+ zoomstarted(event_);
+ mousewheelTimer = setTimeout(function() {
+ mousewheelTimer = null;
+ zoomended(event_);
+ }, 50);
+ d3_eventPreventDefault();
+ var point = center || d3.mouse(this);
+ if (!translate0) translate0 = location(point);
+ scaleTo(Math.pow(2, d3_behavior_zoomDelta() * .002) * view.k);
+ translateTo(point, translate0);
+ zoomed(event_);
+ }
+ function mousewheelreset() {
+ translate0 = null;
+ }
+ function dblclicked() {
+ var event_ = event.of(this, arguments), p = d3.mouse(this), l = location(p), k = Math.log(view.k) / Math.LN2;
+ zoomstarted(event_);
+ scaleTo(Math.pow(2, d3.event.shiftKey ? Math.ceil(k) - 1 : Math.floor(k) + 1));
+ translateTo(p, l);
+ zoomed(event_);
+ zoomended(event_);
+ }
+ return d3.rebind(zoom, event, "on");
+ };
+ var d3_behavior_zoomInfinity = [ 0, Infinity ];
+ var d3_behavior_zoomDelta, d3_behavior_zoomWheel = "onwheel" in d3_document ? (d3_behavior_zoomDelta = function() {
+ return -d3.event.deltaY * (d3.event.deltaMode ? 120 : 1);
+ }, "wheel") : "onmousewheel" in d3_document ? (d3_behavior_zoomDelta = function() {
+ return d3.event.wheelDelta;
+ }, "mousewheel") : (d3_behavior_zoomDelta = function() {
+ return -d3.event.detail;
+ }, "MozMousePixelScroll");
+ function d3_Color() {}
+ d3_Color.prototype.toString = function() {
+ return this.rgb() + "";
+ };
+ d3.hsl = function(h, s, l) {
+ return arguments.length === 1 ? h instanceof d3_Hsl ? d3_hsl(h.h, h.s, h.l) : d3_rgb_parse("" + h, d3_rgb_hsl, d3_hsl) : d3_hsl(+h, +s, +l);
+ };
+ function d3_hsl(h, s, l) {
+ return new d3_Hsl(h, s, l);
+ }
+ function d3_Hsl(h, s, l) {
+ this.h = h;
+ this.s = s;
+ this.l = l;
+ }
+ var d3_hslPrototype = d3_Hsl.prototype = new d3_Color();
+ d3_hslPrototype.brighter = function(k) {
+ k = Math.pow(.7, arguments.length ? k : 1);
+ return d3_hsl(this.h, this.s, this.l / k);
+ };
+ d3_hslPrototype.darker = function(k) {
+ k = Math.pow(.7, arguments.length ? k : 1);
+ return d3_hsl(this.h, this.s, k * this.l);
+ };
+ d3_hslPrototype.rgb = function() {
+ return d3_hsl_rgb(this.h, this.s, this.l);
+ };
+ function d3_hsl_rgb(h, s, l) {
+ var m1, m2;
+ h = isNaN(h) ? 0 : (h %= 360) < 0 ? h + 360 : h;
+ s = isNaN(s) ? 0 : s < 0 ? 0 : s > 1 ? 1 : s;
+ l = l < 0 ? 0 : l > 1 ? 1 : l;
+ m2 = l <= .5 ? l * (1 + s) : l + s - l * s;
+ m1 = 2 * l - m2;
+ function v(h) {
+ if (h > 360) h -= 360; else if (h < 0) h += 360;
+ if (h < 60) return m1 + (m2 - m1) * h / 60;
+ if (h < 180) return m2;
+ if (h < 240) return m1 + (m2 - m1) * (240 - h) / 60;
+ return m1;
+ }
+ function vv(h) {
+ return Math.round(v(h) * 255);
+ }
+ return d3_rgb(vv(h + 120), vv(h), vv(h - 120));
+ }
+ d3.hcl = function(h, c, l) {
+ return arguments.length === 1 ? h instanceof d3_Hcl ? d3_hcl(h.h, h.c, h.l) : h instanceof d3_Lab ? d3_lab_hcl(h.l, h.a, h.b) : d3_lab_hcl((h = d3_rgb_lab((h = d3.rgb(h)).r, h.g, h.b)).l, h.a, h.b) : d3_hcl(+h, +c, +l);
+ };
+ function d3_hcl(h, c, l) {
+ return new d3_Hcl(h, c, l);
+ }
+ function d3_Hcl(h, c, l) {
+ this.h = h;
+ this.c = c;
+ this.l = l;
+ }
+ var d3_hclPrototype = d3_Hcl.prototype = new d3_Color();
+ d3_hclPrototype.brighter = function(k) {
+ return d3_hcl(this.h, this.c, Math.min(100, this.l + d3_lab_K * (arguments.length ? k : 1)));
+ };
+ d3_hclPrototype.darker = function(k) {
+ return d3_hcl(this.h, this.c, Math.max(0, this.l - d3_lab_K * (arguments.length ? k : 1)));
+ };
+ d3_hclPrototype.rgb = function() {
+ return d3_hcl_lab(this.h, this.c, this.l).rgb();
+ };
+ function d3_hcl_lab(h, c, l) {
+ if (isNaN(h)) h = 0;
+ if (isNaN(c)) c = 0;
+ return d3_lab(l, Math.cos(h *= d3_radians) * c, Math.sin(h) * c);
+ }
+ d3.lab = function(l, a, b) {
+ return arguments.length === 1 ? l instanceof d3_Lab ? d3_lab(l.l, l.a, l.b) : l instanceof d3_Hcl ? d3_hcl_lab(l.l, l.c, l.h) : d3_rgb_lab((l = d3.rgb(l)).r, l.g, l.b) : d3_lab(+l, +a, +b);
+ };
+ function d3_lab(l, a, b) {
+ return new d3_Lab(l, a, b);
+ }
+ function d3_Lab(l, a, b) {
+ this.l = l;
+ this.a = a;
+ this.b = b;
+ }
+ var d3_lab_K = 18;
+ var d3_lab_X = .95047, d3_lab_Y = 1, d3_lab_Z = 1.08883;
+ var d3_labPrototype = d3_Lab.prototype = new d3_Color();
+ d3_labPrototype.brighter = function(k) {
+ return d3_lab(Math.min(100, this.l + d3_lab_K * (arguments.length ? k : 1)), this.a, this.b);
+ };
+ d3_labPrototype.darker = function(k) {
+ return d3_lab(Math.max(0, this.l - d3_lab_K * (arguments.length ? k : 1)), this.a, this.b);
+ };
+ d3_labPrototype.rgb = function() {
+ return d3_lab_rgb(this.l, this.a, this.b);
+ };
+ function d3_lab_rgb(l, a, b) {
+ var y = (l + 16) / 116, x = y + a / 500, z = y - b / 200;
+ x = d3_lab_xyz(x) * d3_lab_X;
+ y = d3_lab_xyz(y) * d3_lab_Y;
+ z = d3_lab_xyz(z) * d3_lab_Z;
+ return d3_rgb(d3_xyz_rgb(3.2404542 * x - 1.5371385 * y - .4985314 * z), d3_xyz_rgb(-.969266 * x + 1.8760108 * y + .041556 * z), d3_xyz_rgb(.0556434 * x - .2040259 * y + 1.0572252 * z));
+ }
+ function d3_lab_hcl(l, a, b) {
+ return l > 0 ? d3_hcl(Math.atan2(b, a) * d3_degrees, Math.sqrt(a * a + b * b), l) : d3_hcl(NaN, NaN, l);
+ }
+ function d3_lab_xyz(x) {
+ return x > .206893034 ? x * x * x : (x - 4 / 29) / 7.787037;
+ }
+ function d3_xyz_lab(x) {
+ return x > .008856 ? Math.pow(x, 1 / 3) : 7.787037 * x + 4 / 29;
+ }
+ function d3_xyz_rgb(r) {
+ return Math.round(255 * (r <= .00304 ? 12.92 * r : 1.055 * Math.pow(r, 1 / 2.4) - .055));
+ }
+ d3.rgb = function(r, g, b) {
+ return arguments.length === 1 ? r instanceof d3_Rgb ? d3_rgb(r.r, r.g, r.b) : d3_rgb_parse("" + r, d3_rgb, d3_hsl_rgb) : d3_rgb(~~r, ~~g, ~~b);
+ };
+ function d3_rgbNumber(value) {
+ return d3_rgb(value >> 16, value >> 8 & 255, value & 255);
+ }
+ function d3_rgbString(value) {
+ return d3_rgbNumber(value) + "";
+ }
+ function d3_rgb(r, g, b) {
+ return new d3_Rgb(r, g, b);
+ }
+ function d3_Rgb(r, g, b) {
+ this.r = r;
+ this.g = g;
+ this.b = b;
+ }
+ var d3_rgbPrototype = d3_Rgb.prototype = new d3_Color();
+ d3_rgbPrototype.brighter = function(k) {
+ k = Math.pow(.7, arguments.length ? k : 1);
+ var r = this.r, g = this.g, b = this.b, i = 30;
+ if (!r && !g && !b) return d3_rgb(i, i, i);
+ if (r && r < i) r = i;
+ if (g && g < i) g = i;
+ if (b && b < i) b = i;
+ return d3_rgb(Math.min(255, ~~(r / k)), Math.min(255, ~~(g / k)), Math.min(255, ~~(b / k)));
+ };
+ d3_rgbPrototype.darker = function(k) {
+ k = Math.pow(.7, arguments.length ? k : 1);
+ return d3_rgb(~~(k * this.r), ~~(k * this.g), ~~(k * this.b));
+ };
+ d3_rgbPrototype.hsl = function() {
+ return d3_rgb_hsl(this.r, this.g, this.b);
+ };
+ d3_rgbPrototype.toString = function() {
+ return "#" + d3_rgb_hex(this.r) + d3_rgb_hex(this.g) + d3_rgb_hex(this.b);
+ };
+ function d3_rgb_hex(v) {
+ return v < 16 ? "0" + Math.max(0, v).toString(16) : Math.min(255, v).toString(16);
+ }
+ function d3_rgb_parse(format, rgb, hsl) {
+ var r = 0, g = 0, b = 0, m1, m2, name;
+ m1 = /([a-z]+)\((.*)\)/i.exec(format);
+ if (m1) {
+ m2 = m1[2].split(",");
+ switch (m1[1]) {
+ case "hsl":
+ {
+ return hsl(parseFloat(m2[0]), parseFloat(m2[1]) / 100, parseFloat(m2[2]) / 100);
+ }
+
+ case "rgb":
+ {
+ return rgb(d3_rgb_parseNumber(m2[0]), d3_rgb_parseNumber(m2[1]), d3_rgb_parseNumber(m2[2]));
+ }
+ }
+ }
+ if (name = d3_rgb_names.get(format)) return rgb(name.r, name.g, name.b);
+ if (format != null && format.charAt(0) === "#") {
+ if (format.length === 4) {
+ r = format.charAt(1);
+ r += r;
+ g = format.charAt(2);
+ g += g;
+ b = format.charAt(3);
+ b += b;
+ } else if (format.length === 7) {
+ r = format.substring(1, 3);
+ g = format.substring(3, 5);
+ b = format.substring(5, 7);
+ }
+ r = parseInt(r, 16);
+ g = parseInt(g, 16);
+ b = parseInt(b, 16);
+ }
+ return rgb(r, g, b);
+ }
+ function d3_rgb_hsl(r, g, b) {
+ var min = Math.min(r /= 255, g /= 255, b /= 255), max = Math.max(r, g, b), d = max - min, h, s, l = (max + min) / 2;
+ if (d) {
+ s = l < .5 ? d / (max + min) : d / (2 - max - min);
+ if (r == max) h = (g - b) / d + (g < b ? 6 : 0); else if (g == max) h = (b - r) / d + 2; else h = (r - g) / d + 4;
+ h *= 60;
+ } else {
+ h = NaN;
+ s = l > 0 && l < 1 ? 0 : h;
+ }
+ return d3_hsl(h, s, l);
+ }
+ function d3_rgb_lab(r, g, b) {
+ r = d3_rgb_xyz(r);
+ g = d3_rgb_xyz(g);
+ b = d3_rgb_xyz(b);
+ var x = d3_xyz_lab((.4124564 * r + .3575761 * g + .1804375 * b) / d3_lab_X), y = d3_xyz_lab((.2126729 * r + .7151522 * g + .072175 * b) / d3_lab_Y), z = d3_xyz_lab((.0193339 * r + .119192 * g + .9503041 * b) / d3_lab_Z);
+ return d3_lab(116 * y - 16, 500 * (x - y), 200 * (y - z));
+ }
+ function d3_rgb_xyz(r) {
+ return (r /= 255) <= .04045 ? r / 12.92 : Math.pow((r + .055) / 1.055, 2.4);
+ }
+ function d3_rgb_parseNumber(c) {
+ var f = parseFloat(c);
+ return c.charAt(c.length - 1) === "%" ? Math.round(f * 2.55) : f;
+ }
+ var d3_rgb_names = d3.map({
+ aliceblue: 15792383,
+ antiquewhite: 16444375,
+ aqua: 65535,
+ aquamarine: 8388564,
+ azure: 15794175,
+ beige: 16119260,
+ bisque: 16770244,
+ black: 0,
+ blanchedalmond: 16772045,
+ blue: 255,
+ blueviolet: 9055202,
+ brown: 10824234,
+ burlywood: 14596231,
+ cadetblue: 6266528,
+ chartreuse: 8388352,
+ chocolate: 13789470,
+ coral: 16744272,
+ cornflowerblue: 6591981,
+ cornsilk: 16775388,
+ crimson: 14423100,
+ cyan: 65535,
+ darkblue: 139,
+ darkcyan: 35723,
+ darkgoldenrod: 12092939,
+ darkgray: 11119017,
+ darkgreen: 25600,
+ darkgrey: 11119017,
+ darkkhaki: 12433259,
+ darkmagenta: 9109643,
+ darkolivegreen: 5597999,
+ darkorange: 16747520,
+ darkorchid: 10040012,
+ darkred: 9109504,
+ darksalmon: 15308410,
+ darkseagreen: 9419919,
+ darkslateblue: 4734347,
+ darkslategray: 3100495,
+ darkslategrey: 3100495,
+ darkturquoise: 52945,
+ darkviolet: 9699539,
+ deeppink: 16716947,
+ deepskyblue: 49151,
+ dimgray: 6908265,
+ dimgrey: 6908265,
+ dodgerblue: 2003199,
+ firebrick: 11674146,
+ floralwhite: 16775920,
+ forestgreen: 2263842,
+ fuchsia: 16711935,
+ gainsboro: 14474460,
+ ghostwhite: 16316671,
+ gold: 16766720,
+ goldenrod: 14329120,
+ gray: 8421504,
+ green: 32768,
+ greenyellow: 11403055,
+ grey: 8421504,
+ honeydew: 15794160,
+ hotpink: 16738740,
+ indianred: 13458524,
+ indigo: 4915330,
+ ivory: 16777200,
+ khaki: 15787660,
+ lavender: 15132410,
+ lavenderblush: 16773365,
+ lawngreen: 8190976,
+ lemonchiffon: 16775885,
+ lightblue: 11393254,
+ lightcoral: 15761536,
+ lightcyan: 14745599,
+ lightgoldenrodyellow: 16448210,
+ lightgray: 13882323,
+ lightgreen: 9498256,
+ lightgrey: 13882323,
+ lightpink: 16758465,
+ lightsalmon: 16752762,
+ lightseagreen: 2142890,
+ lightskyblue: 8900346,
+ lightslategray: 7833753,
+ lightslategrey: 7833753,
+ lightsteelblue: 11584734,
+ lightyellow: 16777184,
+ lime: 65280,
+ limegreen: 3329330,
+ linen: 16445670,
+ magenta: 16711935,
+ maroon: 8388608,
+ mediumaquamarine: 6737322,
+ mediumblue: 205,
+ mediumorchid: 12211667,
+ mediumpurple: 9662683,
+ mediumseagreen: 3978097,
+ mediumslateblue: 8087790,
+ mediumspringgreen: 64154,
+ mediumturquoise: 4772300,
+ mediumvioletred: 13047173,
+ midnightblue: 1644912,
+ mintcream: 16121850,
+ mistyrose: 16770273,
+ moccasin: 16770229,
+ navajowhite: 16768685,
+ navy: 128,
+ oldlace: 16643558,
+ olive: 8421376,
+ olivedrab: 7048739,
+ orange: 16753920,
+ orangered: 16729344,
+ orchid: 14315734,
+ palegoldenrod: 15657130,
+ palegreen: 10025880,
+ paleturquoise: 11529966,
+ palevioletred: 14381203,
+ papayawhip: 16773077,
+ peachpuff: 16767673,
+ peru: 13468991,
+ pink: 16761035,
+ plum: 14524637,
+ powderblue: 11591910,
+ purple: 8388736,
+ red: 16711680,
+ rosybrown: 12357519,
+ royalblue: 4286945,
+ saddlebrown: 9127187,
+ salmon: 16416882,
+ sandybrown: 16032864,
+ seagreen: 3050327,
+ seashell: 16774638,
+ sienna: 10506797,
+ silver: 12632256,
+ skyblue: 8900331,
+ slateblue: 6970061,
+ slategray: 7372944,
+ slategrey: 7372944,
+ snow: 16775930,
+ springgreen: 65407,
+ steelblue: 4620980,
+ tan: 13808780,
+ teal: 32896,
+ thistle: 14204888,
+ tomato: 16737095,
+ turquoise: 4251856,
+ violet: 15631086,
+ wheat: 16113331,
+ white: 16777215,
+ whitesmoke: 16119285,
+ yellow: 16776960,
+ yellowgreen: 10145074
+ });
+ d3_rgb_names.forEach(function(key, value) {
+ d3_rgb_names.set(key, d3_rgbNumber(value));
+ });
+ function d3_functor(v) {
+ return typeof v === "function" ? v : function() {
+ return v;
+ };
+ }
+ d3.functor = d3_functor;
+ function d3_identity(d) {
+ return d;
+ }
+ d3.xhr = d3_xhrType(d3_identity);
+ function d3_xhrType(response) {
+ return function(url, mimeType, callback) {
+ if (arguments.length === 2 && typeof mimeType === "function") callback = mimeType,
+ mimeType = null;
+ return d3_xhr(url, mimeType, response, callback);
+ };
+ }
+ function d3_xhr(url, mimeType, response, callback) {
+ var xhr = {}, dispatch = d3.dispatch("beforesend", "progress", "load", "error"), headers = {}, request = new XMLHttpRequest(), responseType = null;
+ if (d3_window.XDomainRequest && !("withCredentials" in request) && /^(http(s)?:)?\/\//.test(url)) request = new XDomainRequest();
+ "onload" in request ? request.onload = request.onerror = respond : request.onreadystatechange = function() {
+ request.readyState > 3 && respond();
+ };
+ function respond() {
+ var status = request.status, result;
+ if (!status && request.responseText || status >= 200 && status < 300 || status === 304) {
+ try {
+ result = response.call(xhr, request);
+ } catch (e) {
+ dispatch.error.call(xhr, e);
+ return;
+ }
+ dispatch.load.call(xhr, result);
+ } else {
+ dispatch.error.call(xhr, request);
+ }
+ }
+ request.onprogress = function(event) {
+ var o = d3.event;
+ d3.event = event;
+ try {
+ dispatch.progress.call(xhr, request);
+ } finally {
+ d3.event = o;
+ }
+ };
+ xhr.header = function(name, value) {
+ name = (name + "").toLowerCase();
+ if (arguments.length < 2) return headers[name];
+ if (value == null) delete headers[name]; else headers[name] = value + "";
+ return xhr;
+ };
+ xhr.mimeType = function(value) {
+ if (!arguments.length) return mimeType;
+ mimeType = value == null ? null : value + "";
+ return xhr;
+ };
+ xhr.responseType = function(value) {
+ if (!arguments.length) return responseType;
+ responseType = value;
+ return xhr;
+ };
+ xhr.response = function(value) {
+ response = value;
+ return xhr;
+ };
+ [ "get", "post" ].forEach(function(method) {
+ xhr[method] = function() {
+ return xhr.send.apply(xhr, [ method ].concat(d3_array(arguments)));
+ };
+ });
+ xhr.send = function(method, data, callback) {
+ if (arguments.length === 2 && typeof data === "function") callback = data, data = null;
+ request.open(method, url, true);
+ if (mimeType != null && !("accept" in headers)) headers["accept"] = mimeType + ",*/*";
+ if (request.setRequestHeader) for (var name in headers) request.setRequestHeader(name, headers[name]);
+ if (mimeType != null && request.overrideMimeType) request.overrideMimeType(mimeType);
+ if (responseType != null) request.responseType = responseType;
+ if (callback != null) xhr.on("error", callback).on("load", function(request) {
+ callback(null, request);
+ });
+ dispatch.beforesend.call(xhr, request);
+ request.send(data == null ? null : data);
+ return xhr;
+ };
+ xhr.abort = function() {
+ request.abort();
+ return xhr;
+ };
+ d3.rebind(xhr, dispatch, "on");
+ return callback == null ? xhr : xhr.get(d3_xhr_fixCallback(callback));
+ }
+ function d3_xhr_fixCallback(callback) {
+ return callback.length === 1 ? function(error, request) {
+ callback(error == null ? request : null);
+ } : callback;
+ }
+ d3.dsv = function(delimiter, mimeType) {
+ var reFormat = new RegExp('["' + delimiter + "\n]"), delimiterCode = delimiter.charCodeAt(0);
+ function dsv(url, row, callback) {
+ if (arguments.length < 3) callback = row, row = null;
+ var xhr = d3_xhr(url, mimeType, row == null ? response : typedResponse(row), callback);
+ xhr.row = function(_) {
+ return arguments.length ? xhr.response((row = _) == null ? response : typedResponse(_)) : row;
+ };
+ return xhr;
+ }
+ function response(request) {
+ return dsv.parse(request.responseText);
+ }
+ function typedResponse(f) {
+ return function(request) {
+ return dsv.parse(request.responseText, f);
+ };
+ }
+ dsv.parse = function(text, f) {
+ var o;
+ return dsv.parseRows(text, function(row, i) {
+ if (o) return o(row, i - 1);
+ var a = new Function("d", "return {" + row.map(function(name, i) {
+ return JSON.stringify(name) + ": d[" + i + "]";
+ }).join(",") + "}");
+ o = f ? function(row, i) {
+ return f(a(row), i);
+ } : a;
+ });
+ };
+ dsv.parseRows = function(text, f) {
+ var EOL = {}, EOF = {}, rows = [], N = text.length, I = 0, n = 0, t, eol;
+ function token() {
+ if (I >= N) return EOF;
+ if (eol) return eol = false, EOL;
+ var j = I;
+ if (text.charCodeAt(j) === 34) {
+ var i = j;
+ while (i++ < N) {
+ if (text.charCodeAt(i) === 34) {
+ if (text.charCodeAt(i + 1) !== 34) break;
+ ++i;
+ }
+ }
+ I = i + 2;
+ var c = text.charCodeAt(i + 1);
+ if (c === 13) {
+ eol = true;
+ if (text.charCodeAt(i + 2) === 10) ++I;
+ } else if (c === 10) {
+ eol = true;
+ }
+ return text.substring(j + 1, i).replace(/""/g, '"');
+ }
+ while (I < N) {
+ var c = text.charCodeAt(I++), k = 1;
+ if (c === 10) eol = true; else if (c === 13) {
+ eol = true;
+ if (text.charCodeAt(I) === 10) ++I, ++k;
+ } else if (c !== delimiterCode) continue;
+ return text.substring(j, I - k);
+ }
+ return text.substring(j);
+ }
+ while ((t = token()) !== EOF) {
+ var a = [];
+ while (t !== EOL && t !== EOF) {
+ a.push(t);
+ t = token();
+ }
+ if (f && !(a = f(a, n++))) continue;
+ rows.push(a);
+ }
+ return rows;
+ };
+ dsv.format = function(rows) {
+ if (Array.isArray(rows[0])) return dsv.formatRows(rows);
+ var fieldSet = new d3_Set(), fields = [];
+ rows.forEach(function(row) {
+ for (var field in row) {
+ if (!fieldSet.has(field)) {
+ fields.push(fieldSet.add(field));
+ }
+ }
+ });
+ return [ fields.map(formatValue).join(delimiter) ].concat(rows.map(function(row) {
+ return fields.map(function(field) {
+ return formatValue(row[field]);
+ }).join(delimiter);
+ })).join("\n");
+ };
+ dsv.formatRows = function(rows) {
+ return rows.map(formatRow).join("\n");
+ };
+ function formatRow(row) {
+ return row.map(formatValue).join(delimiter);
+ }
+ function formatValue(text) {
+ return reFormat.test(text) ? '"' + text.replace(/\"/g, '""') + '"' : text;
+ }
+ return dsv;
+ };
+ d3.csv = d3.dsv(",", "text/csv");
+ d3.tsv = d3.dsv(" ", "text/tab-separated-values");
+ var d3_timer_queueHead, d3_timer_queueTail, d3_timer_interval, d3_timer_timeout, d3_timer_active, d3_timer_frame = d3_window[d3_vendorSymbol(d3_window, "requestAnimationFrame")] || function(callback) {
+ setTimeout(callback, 17);
+ };
+ d3.timer = function(callback, delay, then) {
+ var n = arguments.length;
+ if (n < 2) delay = 0;
+ if (n < 3) then = Date.now();
+ var time = then + delay, timer = {
+ c: callback,
+ t: time,
+ f: false,
+ n: null
+ };
+ if (d3_timer_queueTail) d3_timer_queueTail.n = timer; else d3_timer_queueHead = timer;
+ d3_timer_queueTail = timer;
+ if (!d3_timer_interval) {
+ d3_timer_timeout = clearTimeout(d3_timer_timeout);
+ d3_timer_interval = 1;
+ d3_timer_frame(d3_timer_step);
+ }
+ };
+ function d3_timer_step() {
+ var now = d3_timer_mark(), delay = d3_timer_sweep() - now;
+ if (delay > 24) {
+ if (isFinite(delay)) {
+ clearTimeout(d3_timer_timeout);
+ d3_timer_timeout = setTimeout(d3_timer_step, delay);
+ }
+ d3_timer_interval = 0;
+ } else {
+ d3_timer_interval = 1;
+ d3_timer_frame(d3_timer_step);
+ }
+ }
+ d3.timer.flush = function() {
+ d3_timer_mark();
+ d3_timer_sweep();
+ };
+ function d3_timer_mark() {
+ var now = Date.now();
+ d3_timer_active = d3_timer_queueHead;
+ while (d3_timer_active) {
+ if (now >= d3_timer_active.t) d3_timer_active.f = d3_timer_active.c(now - d3_timer_active.t);
+ d3_timer_active = d3_timer_active.n;
+ }
+ return now;
+ }
+ function d3_timer_sweep() {
+ var t0, t1 = d3_timer_queueHead, time = Infinity;
+ while (t1) {
+ if (t1.f) {
+ t1 = t0 ? t0.n = t1.n : d3_timer_queueHead = t1.n;
+ } else {
+ if (t1.t < time) time = t1.t;
+ t1 = (t0 = t1).n;
+ }
+ }
+ d3_timer_queueTail = t0;
+ return time;
+ }
+ function d3_format_precision(x, p) {
+ return p - (x ? Math.ceil(Math.log(x) / Math.LN10) : 1);
+ }
+ d3.round = function(x, n) {
+ return n ? Math.round(x * (n = Math.pow(10, n))) / n : Math.round(x);
+ };
+ var d3_formatPrefixes = [ "y", "z", "a", "f", "p", "n", "µ", "m", "", "k", "M", "G", "T", "P", "E", "Z", "Y" ].map(d3_formatPrefix);
+ d3.formatPrefix = function(value, precision) {
+ var i = 0;
+ if (value) {
+ if (value < 0) value *= -1;
+ if (precision) value = d3.round(value, d3_format_precision(value, precision));
+ i = 1 + Math.floor(1e-12 + Math.log(value) / Math.LN10);
+ i = Math.max(-24, Math.min(24, Math.floor((i <= 0 ? i + 1 : i - 1) / 3) * 3));
+ }
+ return d3_formatPrefixes[8 + i / 3];
+ };
+ function d3_formatPrefix(d, i) {
+ var k = Math.pow(10, abs(8 - i) * 3);
+ return {
+ scale: i > 8 ? function(d) {
+ return d / k;
+ } : function(d) {
+ return d * k;
+ },
+ symbol: d
+ };
+ }
+ function d3_locale_numberFormat(locale) {
+ var locale_decimal = locale.decimal, locale_thousands = locale.thousands, locale_grouping = locale.grouping, locale_currency = locale.currency, formatGroup = locale_grouping ? function(value) {
+ var i = value.length, t = [], j = 0, g = locale_grouping[0];
+ while (i > 0 && g > 0) {
+ t.push(value.substring(i -= g, i + g));
+ g = locale_grouping[j = (j + 1) % locale_grouping.length];
+ }
+ return t.reverse().join(locale_thousands);
+ } : d3_identity;
+ return function(specifier) {
+ var match = d3_format_re.exec(specifier), fill = match[1] || " ", align = match[2] || ">", sign = match[3] || "", symbol = match[4] || "", zfill = match[5], width = +match[6], comma = match[7], precision = match[8], type = match[9], scale = 1, prefix = "", suffix = "", integer = false;
+ if (precision) precision = +precision.substring(1);
+ if (zfill || fill === "0" && align === "=") {
+ zfill = fill = "0";
+ align = "=";
+ if (comma) width -= Math.floor((width - 1) / 4);
+ }
+ switch (type) {
+ case "n":
+ comma = true;
+ type = "g";
+ break;
+
+ case "%":
+ scale = 100;
+ suffix = "%";
+ type = "f";
+ break;
+
+ case "p":
+ scale = 100;
+ suffix = "%";
+ type = "r";
+ break;
+
+ case "b":
+ case "o":
+ case "x":
+ case "X":
+ if (symbol === "#") prefix = "0" + type.toLowerCase();
+
+ case "c":
+ case "d":
+ integer = true;
+ precision = 0;
+ break;
+
+ case "s":
+ scale = -1;
+ type = "r";
+ break;
+ }
+ if (symbol === "$") prefix = locale_currency[0], suffix = locale_currency[1];
+ if (type == "r" && !precision) type = "g";
+ if (precision != null) {
+ if (type == "g") precision = Math.max(1, Math.min(21, precision)); else if (type == "e" || type == "f") precision = Math.max(0, Math.min(20, precision));
+ }
+ type = d3_format_types.get(type) || d3_format_typeDefault;
+ var zcomma = zfill && comma;
+ return function(value) {
+ var fullSuffix = suffix;
+ if (integer && value % 1) return "";
+ var negative = value < 0 || value === 0 && 1 / value < 0 ? (value = -value, "-") : sign;
+ if (scale < 0) {
+ var unit = d3.formatPrefix(value, precision);
+ value = unit.scale(value);
+ fullSuffix = unit.symbol + suffix;
+ } else {
+ value *= scale;
+ }
+ value = type(value, precision);
+ var i = value.lastIndexOf("."), before = i < 0 ? value : value.substring(0, i), after = i < 0 ? "" : locale_decimal + value.substring(i + 1);
+ if (!zfill && comma) before = formatGroup(before);
+ var length = prefix.length + before.length + after.length + (zcomma ? 0 : negative.length), padding = length < width ? new Array(length = width - length + 1).join(fill) : "";
+ if (zcomma) before = formatGroup(padding + before);
+ negative += prefix;
+ value = before + after;
+ return (align === "<" ? negative + value + padding : align === ">" ? padding + negative + value : align === "^" ? padding.substring(0, length >>= 1) + negative + value + padding.substring(length) : negative + (zcomma ? value : padding + value)) + fullSuffix;
+ };
+ };
+ }
+ var d3_format_re = /(?:([^{])?([<>=^]))?([+\- ])?([$#])?(0)?(\d+)?(,)?(\.-?\d+)?([a-z%])?/i;
+ var d3_format_types = d3.map({
+ b: function(x) {
+ return x.toString(2);
+ },
+ c: function(x) {
+ return String.fromCharCode(x);
+ },
+ o: function(x) {
+ return x.toString(8);
+ },
+ x: function(x) {
+ return x.toString(16);
+ },
+ X: function(x) {
+ return x.toString(16).toUpperCase();
+ },
+ g: function(x, p) {
+ return x.toPrecision(p);
+ },
+ e: function(x, p) {
+ return x.toExponential(p);
+ },
+ f: function(x, p) {
+ return x.toFixed(p);
+ },
+ r: function(x, p) {
+ return (x = d3.round(x, d3_format_precision(x, p))).toFixed(Math.max(0, Math.min(20, d3_format_precision(x * (1 + 1e-15), p))));
+ }
+ });
+ function d3_format_typeDefault(x) {
+ return x + "";
+ }
+ var d3_time = d3.time = {}, d3_date = Date;
+ function d3_date_utc() {
+ this._ = new Date(arguments.length > 1 ? Date.UTC.apply(this, arguments) : arguments[0]);
+ }
+ d3_date_utc.prototype = {
+ getDate: function() {
+ return this._.getUTCDate();
+ },
+ getDay: function() {
+ return this._.getUTCDay();
+ },
+ getFullYear: function() {
+ return this._.getUTCFullYear();
+ },
+ getHours: function() {
+ return this._.getUTCHours();
+ },
+ getMilliseconds: function() {
+ return this._.getUTCMilliseconds();
+ },
+ getMinutes: function() {
+ return this._.getUTCMinutes();
+ },
+ getMonth: function() {
+ return this._.getUTCMonth();
+ },
+ getSeconds: function() {
+ return this._.getUTCSeconds();
+ },
+ getTime: function() {
+ return this._.getTime();
+ },
+ getTimezoneOffset: function() {
+ return 0;
+ },
+ valueOf: function() {
+ return this._.valueOf();
+ },
+ setDate: function() {
+ d3_time_prototype.setUTCDate.apply(this._, arguments);
+ },
+ setDay: function() {
+ d3_time_prototype.setUTCDay.apply(this._, arguments);
+ },
+ setFullYear: function() {
+ d3_time_prototype.setUTCFullYear.apply(this._, arguments);
+ },
+ setHours: function() {
+ d3_time_prototype.setUTCHours.apply(this._, arguments);
+ },
+ setMilliseconds: function() {
+ d3_time_prototype.setUTCMilliseconds.apply(this._, arguments);
+ },
+ setMinutes: function() {
+ d3_time_prototype.setUTCMinutes.apply(this._, arguments);
+ },
+ setMonth: function() {
+ d3_time_prototype.setUTCMonth.apply(this._, arguments);
+ },
+ setSeconds: function() {
+ d3_time_prototype.setUTCSeconds.apply(this._, arguments);
+ },
+ setTime: function() {
+ d3_time_prototype.setTime.apply(this._, arguments);
+ }
+ };
+ var d3_time_prototype = Date.prototype;
+ function d3_time_interval(local, step, number) {
+ function round(date) {
+ var d0 = local(date), d1 = offset(d0, 1);
+ return date - d0 < d1 - date ? d0 : d1;
+ }
+ function ceil(date) {
+ step(date = local(new d3_date(date - 1)), 1);
+ return date;
+ }
+ function offset(date, k) {
+ step(date = new d3_date(+date), k);
+ return date;
+ }
+ function range(t0, t1, dt) {
+ var time = ceil(t0), times = [];
+ if (dt > 1) {
+ while (time < t1) {
+ if (!(number(time) % dt)) times.push(new Date(+time));
+ step(time, 1);
+ }
+ } else {
+ while (time < t1) times.push(new Date(+time)), step(time, 1);
+ }
+ return times;
+ }
+ function range_utc(t0, t1, dt) {
+ try {
+ d3_date = d3_date_utc;
+ var utc = new d3_date_utc();
+ utc._ = t0;
+ return range(utc, t1, dt);
+ } finally {
+ d3_date = Date;
+ }
+ }
+ local.floor = local;
+ local.round = round;
+ local.ceil = ceil;
+ local.offset = offset;
+ local.range = range;
+ var utc = local.utc = d3_time_interval_utc(local);
+ utc.floor = utc;
+ utc.round = d3_time_interval_utc(round);
+ utc.ceil = d3_time_interval_utc(ceil);
+ utc.offset = d3_time_interval_utc(offset);
+ utc.range = range_utc;
+ return local;
+ }
+ function d3_time_interval_utc(method) {
+ return function(date, k) {
+ try {
+ d3_date = d3_date_utc;
+ var utc = new d3_date_utc();
+ utc._ = date;
+ return method(utc, k)._;
+ } finally {
+ d3_date = Date;
+ }
+ };
+ }
+ d3_time.year = d3_time_interval(function(date) {
+ date = d3_time.day(date);
+ date.setMonth(0, 1);
+ return date;
+ }, function(date, offset) {
+ date.setFullYear(date.getFullYear() + offset);
+ }, function(date) {
+ return date.getFullYear();
+ });
+ d3_time.years = d3_time.year.range;
+ d3_time.years.utc = d3_time.year.utc.range;
+ d3_time.day = d3_time_interval(function(date) {
+ var day = new d3_date(2e3, 0);
+ day.setFullYear(date.getFullYear(), date.getMonth(), date.getDate());
+ return day;
+ }, function(date, offset) {
+ date.setDate(date.getDate() + offset);
+ }, function(date) {
+ return date.getDate() - 1;
+ });
+ d3_time.days = d3_time.day.range;
+ d3_time.days.utc = d3_time.day.utc.range;
+ d3_time.dayOfYear = function(date) {
+ var year = d3_time.year(date);
+ return Math.floor((date - year - (date.getTimezoneOffset() - year.getTimezoneOffset()) * 6e4) / 864e5);
+ };
+ [ "sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday" ].forEach(function(day, i) {
+ i = 7 - i;
+ var interval = d3_time[day] = d3_time_interval(function(date) {
+ (date = d3_time.day(date)).setDate(date.getDate() - (date.getDay() + i) % 7);
+ return date;
+ }, function(date, offset) {
+ date.setDate(date.getDate() + Math.floor(offset) * 7);
+ }, function(date) {
+ var day = d3_time.year(date).getDay();
+ return Math.floor((d3_time.dayOfYear(date) + (day + i) % 7) / 7) - (day !== i);
+ });
+ d3_time[day + "s"] = interval.range;
+ d3_time[day + "s"].utc = interval.utc.range;
+ d3_time[day + "OfYear"] = function(date) {
+ var day = d3_time.year(date).getDay();
+ return Math.floor((d3_time.dayOfYear(date) + (day + i) % 7) / 7);
+ };
+ });
+ d3_time.week = d3_time.sunday;
+ d3_time.weeks = d3_time.sunday.range;
+ d3_time.weeks.utc = d3_time.sunday.utc.range;
+ d3_time.weekOfYear = d3_time.sundayOfYear;
+ function d3_locale_timeFormat(locale) {
+ var locale_dateTime = locale.dateTime, locale_date = locale.date, locale_time = locale.time, locale_periods = locale.periods, locale_days = locale.days, locale_shortDays = locale.shortDays, locale_months = locale.months, locale_shortMonths = locale.shortMonths;
+ function d3_time_format(template) {
+ var n = template.length;
+ function format(date) {
+ var string = [], i = -1, j = 0, c, p, f;
+ while (++i < n) {
+ if (template.charCodeAt(i) === 37) {
+ string.push(template.substring(j, i));
+ if ((p = d3_time_formatPads[c = template.charAt(++i)]) != null) c = template.charAt(++i);
+ if (f = d3_time_formats[c]) c = f(date, p == null ? c === "e" ? " " : "0" : p);
+ string.push(c);
+ j = i + 1;
+ }
+ }
+ string.push(template.substring(j, i));
+ return string.join("");
+ }
+ format.parse = function(string) {
+ var d = {
+ y: 1900,
+ m: 0,
+ d: 1,
+ H: 0,
+ M: 0,
+ S: 0,
+ L: 0,
+ Z: null
+ }, i = d3_time_parse(d, template, string, 0);
+ if (i != string.length) return null;
+ if ("p" in d) d.H = d.H % 12 + d.p * 12;
+ var localZ = d.Z != null && d3_date !== d3_date_utc, date = new (localZ ? d3_date_utc : d3_date)();
+ if ("j" in d) date.setFullYear(d.y, 0, d.j); else if ("w" in d && ("W" in d || "U" in d)) {
+ date.setFullYear(d.y, 0, 1);
+ date.setFullYear(d.y, 0, "W" in d ? (d.w + 6) % 7 + d.W * 7 - (date.getDay() + 5) % 7 : d.w + d.U * 7 - (date.getDay() + 6) % 7);
+ } else date.setFullYear(d.y, d.m, d.d);
+ date.setHours(d.H + Math.floor(d.Z / 100), d.M + d.Z % 100, d.S, d.L);
+ return localZ ? date._ : date;
+ };
+ format.toString = function() {
+ return template;
+ };
+ return format;
+ }
+ function d3_time_parse(date, template, string, j) {
+ var c, p, t, i = 0, n = template.length, m = string.length;
+ while (i < n) {
+ if (j >= m) return -1;
+ c = template.charCodeAt(i++);
+ if (c === 37) {
+ t = template.charAt(i++);
+ p = d3_time_parsers[t in d3_time_formatPads ? template.charAt(i++) : t];
+ if (!p || (j = p(date, string, j)) < 0) return -1;
+ } else if (c != string.charCodeAt(j++)) {
+ return -1;
+ }
+ }
+ return j;
+ }
+ d3_time_format.utc = function(template) {
+ var local = d3_time_format(template);
+ function format(date) {
+ try {
+ d3_date = d3_date_utc;
+ var utc = new d3_date();
+ utc._ = date;
+ return local(utc);
+ } finally {
+ d3_date = Date;
+ }
+ }
+ format.parse = function(string) {
+ try {
+ d3_date = d3_date_utc;
+ var date = local.parse(string);
+ return date && date._;
+ } finally {
+ d3_date = Date;
+ }
+ };
+ format.toString = local.toString;
+ return format;
+ };
+ d3_time_format.multi = d3_time_format.utc.multi = d3_time_formatMulti;
+ var d3_time_periodLookup = d3.map(), d3_time_dayRe = d3_time_formatRe(locale_days), d3_time_dayLookup = d3_time_formatLookup(locale_days), d3_time_dayAbbrevRe = d3_time_formatRe(locale_shortDays), d3_time_dayAbbrevLookup = d3_time_formatLookup(locale_shortDays), d3_time_monthRe = d3_time_formatRe(locale_months), d3_time_monthLookup = d3_time_formatLookup(locale_months), d3_time_monthAbbrevRe = d3_time_formatRe(locale_shortMonths), d3_time_monthAbbrevLookup = d3_time_formatLookup(locale_shortMonths);
+ locale_periods.forEach(function(p, i) {
+ d3_time_periodLookup.set(p.toLowerCase(), i);
+ });
+ var d3_time_formats = {
+ a: function(d) {
+ return locale_shortDays[d.getDay()];
+ },
+ A: function(d) {
+ return locale_days[d.getDay()];
+ },
+ b: function(d) {
+ return locale_shortMonths[d.getMonth()];
+ },
+ B: function(d) {
+ return locale_months[d.getMonth()];
+ },
+ c: d3_time_format(locale_dateTime),
+ d: function(d, p) {
+ return d3_time_formatPad(d.getDate(), p, 2);
+ },
+ e: function(d, p) {
+ return d3_time_formatPad(d.getDate(), p, 2);
+ },
+ H: function(d, p) {
+ return d3_time_formatPad(d.getHours(), p, 2);
+ },
+ I: function(d, p) {
+ return d3_time_formatPad(d.getHours() % 12 || 12, p, 2);
+ },
+ j: function(d, p) {
+ return d3_time_formatPad(1 + d3_time.dayOfYear(d), p, 3);
+ },
+ L: function(d, p) {
+ return d3_time_formatPad(d.getMilliseconds(), p, 3);
+ },
+ m: function(d, p) {
+ return d3_time_formatPad(d.getMonth() + 1, p, 2);
+ },
+ M: function(d, p) {
+ return d3_time_formatPad(d.getMinutes(), p, 2);
+ },
+ p: function(d) {
+ return locale_periods[+(d.getHours() >= 12)];
+ },
+ S: function(d, p) {
+ return d3_time_formatPad(d.getSeconds(), p, 2);
+ },
+ U: function(d, p) {
+ return d3_time_formatPad(d3_time.sundayOfYear(d), p, 2);
+ },
+ w: function(d) {
+ return d.getDay();
+ },
+ W: function(d, p) {
+ return d3_time_formatPad(d3_time.mondayOfYear(d), p, 2);
+ },
+ x: d3_time_format(locale_date),
+ X: d3_time_format(locale_time),
+ y: function(d, p) {
+ return d3_time_formatPad(d.getFullYear() % 100, p, 2);
+ },
+ Y: function(d, p) {
+ return d3_time_formatPad(d.getFullYear() % 1e4, p, 4);
+ },
+ Z: d3_time_zone,
+ "%": function() {
+ return "%";
+ }
+ };
+ var d3_time_parsers = {
+ a: d3_time_parseWeekdayAbbrev,
+ A: d3_time_parseWeekday,
+ b: d3_time_parseMonthAbbrev,
+ B: d3_time_parseMonth,
+ c: d3_time_parseLocaleFull,
+ d: d3_time_parseDay,
+ e: d3_time_parseDay,
+ H: d3_time_parseHour24,
+ I: d3_time_parseHour24,
+ j: d3_time_parseDayOfYear,
+ L: d3_time_parseMilliseconds,
+ m: d3_time_parseMonthNumber,
+ M: d3_time_parseMinutes,
+ p: d3_time_parseAmPm,
+ S: d3_time_parseSeconds,
+ U: d3_time_parseWeekNumberSunday,
+ w: d3_time_parseWeekdayNumber,
+ W: d3_time_parseWeekNumberMonday,
+ x: d3_time_parseLocaleDate,
+ X: d3_time_parseLocaleTime,
+ y: d3_time_parseYear,
+ Y: d3_time_parseFullYear,
+ Z: d3_time_parseZone,
+ "%": d3_time_parseLiteralPercent
+ };
+ function d3_time_parseWeekdayAbbrev(date, string, i) {
+ d3_time_dayAbbrevRe.lastIndex = 0;
+ var n = d3_time_dayAbbrevRe.exec(string.substring(i));
+ return n ? (date.w = d3_time_dayAbbrevLookup.get(n[0].toLowerCase()), i + n[0].length) : -1;
+ }
+ function d3_time_parseWeekday(date, string, i) {
+ d3_time_dayRe.lastIndex = 0;
+ var n = d3_time_dayRe.exec(string.substring(i));
+ return n ? (date.w = d3_time_dayLookup.get(n[0].toLowerCase()), i + n[0].length) : -1;
+ }
+ function d3_time_parseMonthAbbrev(date, string, i) {
+ d3_time_monthAbbrevRe.lastIndex = 0;
+ var n = d3_time_monthAbbrevRe.exec(string.substring(i));
+ return n ? (date.m = d3_time_monthAbbrevLookup.get(n[0].toLowerCase()), i + n[0].length) : -1;
+ }
+ function d3_time_parseMonth(date, string, i) {
+ d3_time_monthRe.lastIndex = 0;
+ var n = d3_time_monthRe.exec(string.substring(i));
+ return n ? (date.m = d3_time_monthLookup.get(n[0].toLowerCase()), i + n[0].length) : -1;
+ }
+ function d3_time_parseLocaleFull(date, string, i) {
+ return d3_time_parse(date, d3_time_formats.c.toString(), string, i);
+ }
+ function d3_time_parseLocaleDate(date, string, i) {
+ return d3_time_parse(date, d3_time_formats.x.toString(), string, i);
+ }
+ function d3_time_parseLocaleTime(date, string, i) {
+ return d3_time_parse(date, d3_time_formats.X.toString(), string, i);
+ }
+ function d3_time_parseAmPm(date, string, i) {
+ var n = d3_time_periodLookup.get(string.substring(i, i += 2).toLowerCase());
+ return n == null ? -1 : (date.p = n, i);
+ }
+ return d3_time_format;
+ }
+ var d3_time_formatPads = {
+ "-": "",
+ _: " ",
+ "0": "0"
+ }, d3_time_numberRe = /^\s*\d+/, d3_time_percentRe = /^%/;
+ function d3_time_formatPad(value, fill, width) {
+ var sign = value < 0 ? "-" : "", string = (sign ? -value : value) + "", length = string.length;
+ return sign + (length < width ? new Array(width - length + 1).join(fill) + string : string);
+ }
+ function d3_time_formatRe(names) {
+ return new RegExp("^(?:" + names.map(d3.requote).join("|") + ")", "i");
+ }
+ function d3_time_formatLookup(names) {
+ var map = new d3_Map(), i = -1, n = names.length;
+ while (++i < n) map.set(names[i].toLowerCase(), i);
+ return map;
+ }
+ function d3_time_parseWeekdayNumber(date, string, i) {
+ d3_time_numberRe.lastIndex = 0;
+ var n = d3_time_numberRe.exec(string.substring(i, i + 1));
+ return n ? (date.w = +n[0], i + n[0].length) : -1;
+ }
+ function d3_time_parseWeekNumberSunday(date, string, i) {
+ d3_time_numberRe.lastIndex = 0;
+ var n = d3_time_numberRe.exec(string.substring(i));
+ return n ? (date.U = +n[0], i + n[0].length) : -1;
+ }
+ function d3_time_parseWeekNumberMonday(date, string, i) {
+ d3_time_numberRe.lastIndex = 0;
+ var n = d3_time_numberRe.exec(string.substring(i));
+ return n ? (date.W = +n[0], i + n[0].length) : -1;
+ }
+ function d3_time_parseFullYear(date, string, i) {
+ d3_time_numberRe.lastIndex = 0;
+ var n = d3_time_numberRe.exec(string.substring(i, i + 4));
+ return n ? (date.y = +n[0], i + n[0].length) : -1;
+ }
+ function d3_time_parseYear(date, string, i) {
+ d3_time_numberRe.lastIndex = 0;
+ var n = d3_time_numberRe.exec(string.substring(i, i + 2));
+ return n ? (date.y = d3_time_expandYear(+n[0]), i + n[0].length) : -1;
+ }
+ function d3_time_parseZone(date, string, i) {
+ return /^[+-]\d{4}$/.test(string = string.substring(i, i + 5)) ? (date.Z = +string,
+ i + 5) : -1;
+ }
+ function d3_time_expandYear(d) {
+ return d + (d > 68 ? 1900 : 2e3);
+ }
+ function d3_time_parseMonthNumber(date, string, i) {
+ d3_time_numberRe.lastIndex = 0;
+ var n = d3_time_numberRe.exec(string.substring(i, i + 2));
+ return n ? (date.m = n[0] - 1, i + n[0].length) : -1;
+ }
+ function d3_time_parseDay(date, string, i) {
+ d3_time_numberRe.lastIndex = 0;
+ var n = d3_time_numberRe.exec(string.substring(i, i + 2));
+ return n ? (date.d = +n[0], i + n[0].length) : -1;
+ }
+ function d3_time_parseDayOfYear(date, string, i) {
+ d3_time_numberRe.lastIndex = 0;
+ var n = d3_time_numberRe.exec(string.substring(i, i + 3));
+ return n ? (date.j = +n[0], i + n[0].length) : -1;
+ }
+ function d3_time_parseHour24(date, string, i) {
+ d3_time_numberRe.lastIndex = 0;
+ var n = d3_time_numberRe.exec(string.substring(i, i + 2));
+ return n ? (date.H = +n[0], i + n[0].length) : -1;
+ }
+ function d3_time_parseMinutes(date, string, i) {
+ d3_time_numberRe.lastIndex = 0;
+ var n = d3_time_numberRe.exec(string.substring(i, i + 2));
+ return n ? (date.M = +n[0], i + n[0].length) : -1;
+ }
+ function d3_time_parseSeconds(date, string, i) {
+ d3_time_numberRe.lastIndex = 0;
+ var n = d3_time_numberRe.exec(string.substring(i, i + 2));
+ return n ? (date.S = +n[0], i + n[0].length) : -1;
+ }
+ function d3_time_parseMilliseconds(date, string, i) {
+ d3_time_numberRe.lastIndex = 0;
+ var n = d3_time_numberRe.exec(string.substring(i, i + 3));
+ return n ? (date.L = +n[0], i + n[0].length) : -1;
+ }
+ function d3_time_zone(d) {
+ var z = d.getTimezoneOffset(), zs = z > 0 ? "-" : "+", zh = ~~(abs(z) / 60), zm = abs(z) % 60;
+ return zs + d3_time_formatPad(zh, "0", 2) + d3_time_formatPad(zm, "0", 2);
+ }
+ function d3_time_parseLiteralPercent(date, string, i) {
+ d3_time_percentRe.lastIndex = 0;
+ var n = d3_time_percentRe.exec(string.substring(i, i + 1));
+ return n ? i + n[0].length : -1;
+ }
+ function d3_time_formatMulti(formats) {
+ var n = formats.length, i = -1;
+ while (++i < n) formats[i][0] = this(formats[i][0]);
+ return function(date) {
+ var i = 0, f = formats[i];
+ while (!f[1](date)) f = formats[++i];
+ return f[0](date);
+ };
+ }
+ d3.locale = function(locale) {
+ return {
+ numberFormat: d3_locale_numberFormat(locale),
+ timeFormat: d3_locale_timeFormat(locale)
+ };
+ };
+ var d3_locale_enUS = d3.locale({
+ decimal: ".",
+ thousands: ",",
+ grouping: [ 3 ],
+ currency: [ "$", "" ],
+ dateTime: "%a %b %e %X %Y",
+ date: "%m/%d/%Y",
+ time: "%H:%M:%S",
+ periods: [ "AM", "PM" ],
+ days: [ "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" ],
+ shortDays: [ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" ],
+ months: [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ],
+ shortMonths: [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ]
+ });
+ d3.format = d3_locale_enUS.numberFormat;
+ d3.geo = {};
+ function d3_adder() {}
+ d3_adder.prototype = {
+ s: 0,
+ t: 0,
+ add: function(y) {
+ d3_adderSum(y, this.t, d3_adderTemp);
+ d3_adderSum(d3_adderTemp.s, this.s, this);
+ if (this.s) this.t += d3_adderTemp.t; else this.s = d3_adderTemp.t;
+ },
+ reset: function() {
+ this.s = this.t = 0;
+ },
+ valueOf: function() {
+ return this.s;
+ }
+ };
+ var d3_adderTemp = new d3_adder();
+ function d3_adderSum(a, b, o) {
+ var x = o.s = a + b, bv = x - a, av = x - bv;
+ o.t = a - av + (b - bv);
+ }
+ d3.geo.stream = function(object, listener) {
+ if (object && d3_geo_streamObjectType.hasOwnProperty(object.type)) {
+ d3_geo_streamObjectType[object.type](object, listener);
+ } else {
+ d3_geo_streamGeometry(object, listener);
+ }
+ };
+ function d3_geo_streamGeometry(geometry, listener) {
+ if (geometry && d3_geo_streamGeometryType.hasOwnProperty(geometry.type)) {
+ d3_geo_streamGeometryType[geometry.type](geometry, listener);
+ }
+ }
+ var d3_geo_streamObjectType = {
+ Feature: function(feature, listener) {
+ d3_geo_streamGeometry(feature.geometry, listener);
+ },
+ FeatureCollection: function(object, listener) {
+ var features = object.features, i = -1, n = features.length;
+ while (++i < n) d3_geo_streamGeometry(features[i].geometry, listener);
+ }
+ };
+ var d3_geo_streamGeometryType = {
+ Sphere: function(object, listener) {
+ listener.sphere();
+ },
+ Point: function(object, listener) {
+ object = object.coordinates;
+ listener.point(object[0], object[1], object[2]);
+ },
+ MultiPoint: function(object, listener) {
+ var coordinates = object.coordinates, i = -1, n = coordinates.length;
+ while (++i < n) object = coordinates[i], listener.point(object[0], object[1], object[2]);
+ },
+ LineString: function(object, listener) {
+ d3_geo_streamLine(object.coordinates, listener, 0);
+ },
+ MultiLineString: function(object, listener) {
+ var coordinates = object.coordinates, i = -1, n = coordinates.length;
+ while (++i < n) d3_geo_streamLine(coordinates[i], listener, 0);
+ },
+ Polygon: function(object, listener) {
+ d3_geo_streamPolygon(object.coordinates, listener);
+ },
+ MultiPolygon: function(object, listener) {
+ var coordinates = object.coordinates, i = -1, n = coordinates.length;
+ while (++i < n) d3_geo_streamPolygon(coordinates[i], listener);
+ },
+ GeometryCollection: function(object, listener) {
+ var geometries = object.geometries, i = -1, n = geometries.length;
+ while (++i < n) d3_geo_streamGeometry(geometries[i], listener);
+ }
+ };
+ function d3_geo_streamLine(coordinates, listener, closed) {
+ var i = -1, n = coordinates.length - closed, coordinate;
+ listener.lineStart();
+ while (++i < n) coordinate = coordinates[i], listener.point(coordinate[0], coordinate[1], coordinate[2]);
+ listener.lineEnd();
+ }
+ function d3_geo_streamPolygon(coordinates, listener) {
+ var i = -1, n = coordinates.length;
+ listener.polygonStart();
+ while (++i < n) d3_geo_streamLine(coordinates[i], listener, 1);
+ listener.polygonEnd();
+ }
+ d3.geo.area = function(object) {
+ d3_geo_areaSum = 0;
+ d3.geo.stream(object, d3_geo_area);
+ return d3_geo_areaSum;
+ };
+ var d3_geo_areaSum, d3_geo_areaRingSum = new d3_adder();
+ var d3_geo_area = {
+ sphere: function() {
+ d3_geo_areaSum += 4 * π;
+ },
+ point: d3_noop,
+ lineStart: d3_noop,
+ lineEnd: d3_noop,
+ polygonStart: function() {
+ d3_geo_areaRingSum.reset();
+ d3_geo_area.lineStart = d3_geo_areaRingStart;
+ },
+ polygonEnd: function() {
+ var area = 2 * d3_geo_areaRingSum;
+ d3_geo_areaSum += area < 0 ? 4 * π + area : area;
+ d3_geo_area.lineStart = d3_geo_area.lineEnd = d3_geo_area.point = d3_noop;
+ }
+ };
+ function d3_geo_areaRingStart() {
+ var λ00, φ00, λ0, cosφ0, sinφ0;
+ d3_geo_area.point = function(λ, φ) {
+ d3_geo_area.point = nextPoint;
+ λ0 = (λ00 = λ) * d3_radians, cosφ0 = Math.cos(φ = (φ00 = φ) * d3_radians / 2 + π / 4),
+ sinφ0 = Math.sin(φ);
+ };
+ function nextPoint(λ, φ) {
+ λ *= d3_radians;
+ φ = φ * d3_radians / 2 + π / 4;
+ var dλ = λ - λ0, cosφ = Math.cos(φ), sinφ = Math.sin(φ), k = sinφ0 * sinφ, u = cosφ0 * cosφ + k * Math.cos(dλ), v = k * Math.sin(dλ);
+ d3_geo_areaRingSum.add(Math.atan2(v, u));
+ λ0 = λ, cosφ0 = cosφ, sinφ0 = sinφ;
+ }
+ d3_geo_area.lineEnd = function() {
+ nextPoint(λ00, φ00);
+ };
+ }
+ function d3_geo_cartesian(spherical) {
+ var λ = spherical[0], φ = spherical[1], cosφ = Math.cos(φ);
+ return [ cosφ * Math.cos(λ), cosφ * Math.sin(λ), Math.sin(φ) ];
+ }
+ function d3_geo_cartesianDot(a, b) {
+ return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
+ }
+ function d3_geo_cartesianCross(a, b) {
+ return [ a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0] ];
+ }
+ function d3_geo_cartesianAdd(a, b) {
+ a[0] += b[0];
+ a[1] += b[1];
+ a[2] += b[2];
+ }
+ function d3_geo_cartesianScale(vector, k) {
+ return [ vector[0] * k, vector[1] * k, vector[2] * k ];
+ }
+ function d3_geo_cartesianNormalize(d) {
+ var l = Math.sqrt(d[0] * d[0] + d[1] * d[1] + d[2] * d[2]);
+ d[0] /= l;
+ d[1] /= l;
+ d[2] /= l;
+ }
+ function d3_geo_spherical(cartesian) {
+ return [ Math.atan2(cartesian[1], cartesian[0]), d3_asin(cartesian[2]) ];
+ }
+ function d3_geo_sphericalEqual(a, b) {
+ return abs(a[0] - b[0]) < ε && abs(a[1] - b[1]) < ε;
+ }
+ d3.geo.bounds = function() {
+ var λ0, φ0, λ1, φ1, λ_, λ__, φ__, p0, dλSum, ranges, range;
+ var bound = {
+ point: point,
+ lineStart: lineStart,
+ lineEnd: lineEnd,
+ polygonStart: function() {
+ bound.point = ringPoint;
+ bound.lineStart = ringStart;
+ bound.lineEnd = ringEnd;
+ dλSum = 0;
+ d3_geo_area.polygonStart();
+ },
+ polygonEnd: function() {
+ d3_geo_area.polygonEnd();
+ bound.point = point;
+ bound.lineStart = lineStart;
+ bound.lineEnd = lineEnd;
+ if (d3_geo_areaRingSum < 0) λ0 = -(λ1 = 180), φ0 = -(φ1 = 90); else if (dλSum > ε) φ1 = 90; else if (dλSum < -ε) φ0 = -90;
+ range[0] = λ0, range[1] = λ1;
+ }
+ };
+ function point(λ, φ) {
+ ranges.push(range = [ λ0 = λ, λ1 = λ ]);
+ if (φ < φ0) φ0 = φ;
+ if (φ > φ1) φ1 = φ;
+ }
+ function linePoint(λ, φ) {
+ var p = d3_geo_cartesian([ λ * d3_radians, φ * d3_radians ]);
+ if (p0) {
+ var normal = d3_geo_cartesianCross(p0, p), equatorial = [ normal[1], -normal[0], 0 ], inflection = d3_geo_cartesianCross(equatorial, normal);
+ d3_geo_cartesianNormalize(inflection);
+ inflection = d3_geo_spherical(inflection);
+ var dλ = λ - λ_, s = dλ > 0 ? 1 : -1, λi = inflection[0] * d3_degrees * s, antimeridian = abs(dλ) > 180;
+ if (antimeridian ^ (s * λ_ < λi && λi < s * λ)) {
+ var φi = inflection[1] * d3_degrees;
+ if (φi > φ1) φ1 = φi;
+ } else if (λi = (λi + 360) % 360 - 180, antimeridian ^ (s * λ_ < λi && λi < s * λ)) {
+ var φi = -inflection[1] * d3_degrees;
+ if (φi < φ0) φ0 = φi;
+ } else {
+ if (φ < φ0) φ0 = φ;
+ if (φ > φ1) φ1 = φ;
+ }
+ if (antimeridian) {
+ if (λ < λ_) {
+ if (angle(λ0, λ) > angle(λ0, λ1)) λ1 = λ;
+ } else {
+ if (angle(λ, λ1) > angle(λ0, λ1)) λ0 = λ;
+ }
+ } else {
+ if (λ1 >= λ0) {
+ if (λ < λ0) λ0 = λ;
+ if (λ > λ1) λ1 = λ;
+ } else {
+ if (λ > λ_) {
+ if (angle(λ0, λ) > angle(λ0, λ1)) λ1 = λ;
+ } else {
+ if (angle(λ, λ1) > angle(λ0, λ1)) λ0 = λ;
+ }
+ }
+ }
+ } else {
+ point(λ, φ);
+ }
+ p0 = p, λ_ = λ;
+ }
+ function lineStart() {
+ bound.point = linePoint;
+ }
+ function lineEnd() {
+ range[0] = λ0, range[1] = λ1;
+ bound.point = point;
+ p0 = null;
+ }
+ function ringPoint(λ, φ) {
+ if (p0) {
+ var dλ = λ - λ_;
+ dλSum += abs(dλ) > 180 ? dλ + (dλ > 0 ? 360 : -360) : dλ;
+ } else λ__ = λ, φ__ = φ;
+ d3_geo_area.point(λ, φ);
+ linePoint(λ, φ);
+ }
+ function ringStart() {
+ d3_geo_area.lineStart();
+ }
+ function ringEnd() {
+ ringPoint(λ__, φ__);
+ d3_geo_area.lineEnd();
+ if (abs(dλSum) > ε) λ0 = -(λ1 = 180);
+ range[0] = λ0, range[1] = λ1;
+ p0 = null;
+ }
+ function angle(λ0, λ1) {
+ return (λ1 -= λ0) < 0 ? λ1 + 360 : λ1;
+ }
+ function compareRanges(a, b) {
+ return a[0] - b[0];
+ }
+ function withinRange(x, range) {
+ return range[0] <= range[1] ? range[0] <= x && x <= range[1] : x < range[0] || range[1] < x;
+ }
+ return function(feature) {
+ φ1 = λ1 = -(λ0 = φ0 = Infinity);
+ ranges = [];
+ d3.geo.stream(feature, bound);
+ var n = ranges.length;
+ if (n) {
+ ranges.sort(compareRanges);
+ for (var i = 1, a = ranges[0], b, merged = [ a ]; i < n; ++i) {
+ b = ranges[i];
+ if (withinRange(b[0], a) || withinRange(b[1], a)) {
+ if (angle(a[0], b[1]) > angle(a[0], a[1])) a[1] = b[1];
+ if (angle(b[0], a[1]) > angle(a[0], a[1])) a[0] = b[0];
+ } else {
+ merged.push(a = b);
+ }
+ }
+ var best = -Infinity, dλ;
+ for (var n = merged.length - 1, i = 0, a = merged[n], b; i <= n; a = b, ++i) {
+ b = merged[i];
+ if ((dλ = angle(a[1], b[0])) > best) best = dλ, λ0 = b[0], λ1 = a[1];
+ }
+ }
+ ranges = range = null;
+ return λ0 === Infinity || φ0 === Infinity ? [ [ NaN, NaN ], [ NaN, NaN ] ] : [ [ λ0, φ0 ], [ λ1, φ1 ] ];
+ };
+ }();
+ d3.geo.centroid = function(object) {
+ d3_geo_centroidW0 = d3_geo_centroidW1 = d3_geo_centroidX0 = d3_geo_centroidY0 = d3_geo_centroidZ0 = d3_geo_centroidX1 = d3_geo_centroidY1 = d3_geo_centroidZ1 = d3_geo_centroidX2 = d3_geo_centroidY2 = d3_geo_centroidZ2 = 0;
+ d3.geo.stream(object, d3_geo_centroid);
+ var x = d3_geo_centroidX2, y = d3_geo_centroidY2, z = d3_geo_centroidZ2, m = x * x + y * y + z * z;
+ if (m < ε2) {
+ x = d3_geo_centroidX1, y = d3_geo_centroidY1, z = d3_geo_centroidZ1;
+ if (d3_geo_centroidW1 < ε) x = d3_geo_centroidX0, y = d3_geo_centroidY0, z = d3_geo_centroidZ0;
+ m = x * x + y * y + z * z;
+ if (m < ε2) return [ NaN, NaN ];
+ }
+ return [ Math.atan2(y, x) * d3_degrees, d3_asin(z / Math.sqrt(m)) * d3_degrees ];
+ };
+ var d3_geo_centroidW0, d3_geo_centroidW1, d3_geo_centroidX0, d3_geo_centroidY0, d3_geo_centroidZ0, d3_geo_centroidX1, d3_geo_centroidY1, d3_geo_centroidZ1, d3_geo_centroidX2, d3_geo_centroidY2, d3_geo_centroidZ2;
+ var d3_geo_centroid = {
+ sphere: d3_noop,
+ point: d3_geo_centroidPoint,
+ lineStart: d3_geo_centroidLineStart,
+ lineEnd: d3_geo_centroidLineEnd,
+ polygonStart: function() {
+ d3_geo_centroid.lineStart = d3_geo_centroidRingStart;
+ },
+ polygonEnd: function() {
+ d3_geo_centroid.lineStart = d3_geo_centroidLineStart;
+ }
+ };
+ function d3_geo_centroidPoint(λ, φ) {
+ λ *= d3_radians;
+ var cosφ = Math.cos(φ *= d3_radians);
+ d3_geo_centroidPointXYZ(cosφ * Math.cos(λ), cosφ * Math.sin(λ), Math.sin(φ));
+ }
+ function d3_geo_centroidPointXYZ(x, y, z) {
+ ++d3_geo_centroidW0;
+ d3_geo_centroidX0 += (x - d3_geo_centroidX0) / d3_geo_centroidW0;
+ d3_geo_centroidY0 += (y - d3_geo_centroidY0) / d3_geo_centroidW0;
+ d3_geo_centroidZ0 += (z - d3_geo_centroidZ0) / d3_geo_centroidW0;
+ }
+ function d3_geo_centroidLineStart() {
+ var x0, y0, z0;
+ d3_geo_centroid.point = function(λ, φ) {
+ λ *= d3_radians;
+ var cosφ = Math.cos(φ *= d3_radians);
+ x0 = cosφ * Math.cos(λ);
+ y0 = cosφ * Math.sin(λ);
+ z0 = Math.sin(φ);
+ d3_geo_centroid.point = nextPoint;
+ d3_geo_centroidPointXYZ(x0, y0, z0);
+ };
+ function nextPoint(λ, φ) {
+ λ *= d3_radians;
+ var cosφ = Math.cos(φ *= d3_radians), x = cosφ * Math.cos(λ), y = cosφ * Math.sin(λ), z = Math.sin(φ), w = Math.atan2(Math.sqrt((w = y0 * z - z0 * y) * w + (w = z0 * x - x0 * z) * w + (w = x0 * y - y0 * x) * w), x0 * x + y0 * y + z0 * z);
+ d3_geo_centroidW1 += w;
+ d3_geo_centroidX1 += w * (x0 + (x0 = x));
+ d3_geo_centroidY1 += w * (y0 + (y0 = y));
+ d3_geo_centroidZ1 += w * (z0 + (z0 = z));
+ d3_geo_centroidPointXYZ(x0, y0, z0);
+ }
+ }
+ function d3_geo_centroidLineEnd() {
+ d3_geo_centroid.point = d3_geo_centroidPoint;
+ }
+ function d3_geo_centroidRingStart() {
+ var λ00, φ00, x0, y0, z0;
+ d3_geo_centroid.point = function(λ, φ) {
+ λ00 = λ, φ00 = φ;
+ d3_geo_centroid.point = nextPoint;
+ λ *= d3_radians;
+ var cosφ = Math.cos(φ *= d3_radians);
+ x0 = cosφ * Math.cos(λ);
+ y0 = cosφ * Math.sin(λ);
+ z0 = Math.sin(φ);
+ d3_geo_centroidPointXYZ(x0, y0, z0);
+ };
+ d3_geo_centroid.lineEnd = function() {
+ nextPoint(λ00, φ00);
+ d3_geo_centroid.lineEnd = d3_geo_centroidLineEnd;
+ d3_geo_centroid.point = d3_geo_centroidPoint;
+ };
+ function nextPoint(λ, φ) {
+ λ *= d3_radians;
+ var cosφ = Math.cos(φ *= d3_radians), x = cosφ * Math.cos(λ), y = cosφ * Math.sin(λ), z = Math.sin(φ), cx = y0 * z - z0 * y, cy = z0 * x - x0 * z, cz = x0 * y - y0 * x, m = Math.sqrt(cx * cx + cy * cy + cz * cz), u = x0 * x + y0 * y + z0 * z, v = m && -d3_acos(u) / m, w = Math.atan2(m, u);
+ d3_geo_centroidX2 += v * cx;
+ d3_geo_centroidY2 += v * cy;
+ d3_geo_centroidZ2 += v * cz;
+ d3_geo_centroidW1 += w;
+ d3_geo_centroidX1 += w * (x0 + (x0 = x));
+ d3_geo_centroidY1 += w * (y0 + (y0 = y));
+ d3_geo_centroidZ1 += w * (z0 + (z0 = z));
+ d3_geo_centroidPointXYZ(x0, y0, z0);
+ }
+ }
+ function d3_true() {
+ return true;
+ }
+ function d3_geo_clipPolygon(segments, compare, clipStartInside, interpolate, listener) {
+ var subject = [], clip = [];
+ segments.forEach(function(segment) {
+ if ((n = segment.length - 1) <= 0) return;
+ var n, p0 = segment[0], p1 = segment[n];
+ if (d3_geo_sphericalEqual(p0, p1)) {
+ listener.lineStart();
+ for (var i = 0; i < n; ++i) listener.point((p0 = segment[i])[0], p0[1]);
+ listener.lineEnd();
+ return;
+ }
+ var a = new d3_geo_clipPolygonIntersection(p0, segment, null, true), b = new d3_geo_clipPolygonIntersection(p0, null, a, false);
+ a.o = b;
+ subject.push(a);
+ clip.push(b);
+ a = new d3_geo_clipPolygonIntersection(p1, segment, null, false);
+ b = new d3_geo_clipPolygonIntersection(p1, null, a, true);
+ a.o = b;
+ subject.push(a);
+ clip.push(b);
+ });
+ clip.sort(compare);
+ d3_geo_clipPolygonLinkCircular(subject);
+ d3_geo_clipPolygonLinkCircular(clip);
+ if (!subject.length) return;
+ for (var i = 0, entry = clipStartInside, n = clip.length; i < n; ++i) {
+ clip[i].e = entry = !entry;
+ }
+ var start = subject[0], points, point;
+ while (1) {
+ var current = start, isSubject = true;
+ while (current.v) if ((current = current.n) === start) return;
+ points = current.z;
+ listener.lineStart();
+ do {
+ current.v = current.o.v = true;
+ if (current.e) {
+ if (isSubject) {
+ for (var i = 0, n = points.length; i < n; ++i) listener.point((point = points[i])[0], point[1]);
+ } else {
+ interpolate(current.x, current.n.x, 1, listener);
+ }
+ current = current.n;
+ } else {
+ if (isSubject) {
+ points = current.p.z;
+ for (var i = points.length - 1; i >= 0; --i) listener.point((point = points[i])[0], point[1]);
+ } else {
+ interpolate(current.x, current.p.x, -1, listener);
+ }
+ current = current.p;
+ }
+ current = current.o;
+ points = current.z;
+ isSubject = !isSubject;
+ } while (!current.v);
+ listener.lineEnd();
+ }
+ }
+ function d3_geo_clipPolygonLinkCircular(array) {
+ if (!(n = array.length)) return;
+ var n, i = 0, a = array[0], b;
+ while (++i < n) {
+ a.n = b = array[i];
+ b.p = a;
+ a = b;
+ }
+ a.n = b = array[0];
+ b.p = a;
+ }
+ function d3_geo_clipPolygonIntersection(point, points, other, entry) {
+ this.x = point;
+ this.z = points;
+ this.o = other;
+ this.e = entry;
+ this.v = false;
+ this.n = this.p = null;
+ }
+ function d3_geo_clip(pointVisible, clipLine, interpolate, clipStart) {
+ return function(rotate, listener) {
+ var line = clipLine(listener), rotatedClipStart = rotate.invert(clipStart[0], clipStart[1]);
+ var clip = {
+ point: point,
+ lineStart: lineStart,
+ lineEnd: lineEnd,
+ polygonStart: function() {
+ clip.point = pointRing;
+ clip.lineStart = ringStart;
+ clip.lineEnd = ringEnd;
+ segments = [];
+ polygon = [];
+ listener.polygonStart();
+ },
+ polygonEnd: function() {
+ clip.point = point;
+ clip.lineStart = lineStart;
+ clip.lineEnd = lineEnd;
+ segments = d3.merge(segments);
+ var clipStartInside = d3_geo_pointInPolygon(rotatedClipStart, polygon);
+ if (segments.length) {
+ d3_geo_clipPolygon(segments, d3_geo_clipSort, clipStartInside, interpolate, listener);
+ } else if (clipStartInside) {
+ listener.lineStart();
+ interpolate(null, null, 1, listener);
+ listener.lineEnd();
+ }
+ listener.polygonEnd();
+ segments = polygon = null;
+ },
+ sphere: function() {
+ listener.polygonStart();
+ listener.lineStart();
+ interpolate(null, null, 1, listener);
+ listener.lineEnd();
+ listener.polygonEnd();
+ }
+ };
+ function point(λ, φ) {
+ var point = rotate(λ, φ);
+ if (pointVisible(λ = point[0], φ = point[1])) listener.point(λ, φ);
+ }
+ function pointLine(λ, φ) {
+ var point = rotate(λ, φ);
+ line.point(point[0], point[1]);
+ }
+ function lineStart() {
+ clip.point = pointLine;
+ line.lineStart();
+ }
+ function lineEnd() {
+ clip.point = point;
+ line.lineEnd();
+ }
+ var segments;
+ var buffer = d3_geo_clipBufferListener(), ringListener = clipLine(buffer), polygon, ring;
+ function pointRing(λ, φ) {
+ ring.push([ λ, φ ]);
+ var point = rotate(λ, φ);
+ ringListener.point(point[0], point[1]);
+ }
+ function ringStart() {
+ ringListener.lineStart();
+ ring = [];
+ }
+ function ringEnd() {
+ pointRing(ring[0][0], ring[0][1]);
+ ringListener.lineEnd();
+ var clean = ringListener.clean(), ringSegments = buffer.buffer(), segment, n = ringSegments.length;
+ ring.pop();
+ polygon.push(ring);
+ ring = null;
+ if (!n) return;
+ if (clean & 1) {
+ segment = ringSegments[0];
+ var n = segment.length - 1, i = -1, point;
+ listener.lineStart();
+ while (++i < n) listener.point((point = segment[i])[0], point[1]);
+ listener.lineEnd();
+ return;
+ }
+ if (n > 1 && clean & 2) ringSegments.push(ringSegments.pop().concat(ringSegments.shift()));
+ segments.push(ringSegments.filter(d3_geo_clipSegmentLength1));
+ }
+ return clip;
+ };
+ }
+ function d3_geo_clipSegmentLength1(segment) {
+ return segment.length > 1;
+ }
+ function d3_geo_clipBufferListener() {
+ var lines = [], line;
+ return {
+ lineStart: function() {
+ lines.push(line = []);
+ },
+ point: function(λ, φ) {
+ line.push([ λ, φ ]);
+ },
+ lineEnd: d3_noop,
+ buffer: function() {
+ var buffer = lines;
+ lines = [];
+ line = null;
+ return buffer;
+ },
+ rejoin: function() {
+ if (lines.length > 1) lines.push(lines.pop().concat(lines.shift()));
+ }
+ };
+ }
+ function d3_geo_clipSort(a, b) {
+ return ((a = a.x)[0] < 0 ? a[1] - halfπ - ε : halfπ - a[1]) - ((b = b.x)[0] < 0 ? b[1] - halfπ - ε : halfπ - b[1]);
+ }
+ function d3_geo_pointInPolygon(point, polygon) {
+ var meridian = point[0], parallel = point[1], meridianNormal = [ Math.sin(meridian), -Math.cos(meridian), 0 ], polarAngle = 0, winding = 0;
+ d3_geo_areaRingSum.reset();
+ for (var i = 0, n = polygon.length; i < n; ++i) {
+ var ring = polygon[i], m = ring.length;
+ if (!m) continue;
+ var point0 = ring[0], λ0 = point0[0], φ0 = point0[1] / 2 + π / 4, sinφ0 = Math.sin(φ0), cosφ0 = Math.cos(φ0), j = 1;
+ while (true) {
+ if (j === m) j = 0;
+ point = ring[j];
+ var λ = point[0], φ = point[1] / 2 + π / 4, sinφ = Math.sin(φ), cosφ = Math.cos(φ), dλ = λ - λ0, antimeridian = abs(dλ) > π, k = sinφ0 * sinφ;
+ d3_geo_areaRingSum.add(Math.atan2(k * Math.sin(dλ), cosφ0 * cosφ + k * Math.cos(dλ)));
+ polarAngle += antimeridian ? dλ + (dλ >= 0 ? τ : -τ) : dλ;
+ if (antimeridian ^ λ0 >= meridian ^ λ >= meridian) {
+ var arc = d3_geo_cartesianCross(d3_geo_cartesian(point0), d3_geo_cartesian(point));
+ d3_geo_cartesianNormalize(arc);
+ var intersection = d3_geo_cartesianCross(meridianNormal, arc);
+ d3_geo_cartesianNormalize(intersection);
+ var φarc = (antimeridian ^ dλ >= 0 ? -1 : 1) * d3_asin(intersection[2]);
+ if (parallel > φarc || parallel === φarc && (arc[0] || arc[1])) {
+ winding += antimeridian ^ dλ >= 0 ? 1 : -1;
+ }
+ }
+ if (!j++) break;
+ λ0 = λ, sinφ0 = sinφ, cosφ0 = cosφ, point0 = point;
+ }
+ }
+ return (polarAngle < -ε || polarAngle < ε && d3_geo_areaRingSum < 0) ^ winding & 1;
+ }
+ var d3_geo_clipAntimeridian = d3_geo_clip(d3_true, d3_geo_clipAntimeridianLine, d3_geo_clipAntimeridianInterpolate, [ -Ï€, -Ï€ / 2 ]);
+ function d3_geo_clipAntimeridianLine(listener) {
+ var λ0 = NaN, φ0 = NaN, sλ0 = NaN, clean;
+ return {
+ lineStart: function() {
+ listener.lineStart();
+ clean = 1;
+ },
+ point: function(λ1, φ1) {
+ var sλ1 = λ1 > 0 ? π : -π, dλ = abs(λ1 - λ0);
+ if (abs(dλ - π) < ε) {
+ listener.point(λ0, φ0 = (φ0 + φ1) / 2 > 0 ? halfπ : -halfπ);
+ listener.point(sλ0, φ0);
+ listener.lineEnd();
+ listener.lineStart();
+ listener.point(sλ1, φ0);
+ listener.point(λ1, φ0);
+ clean = 0;
+ } else if (sλ0 !== sλ1 && dλ >= π) {
+ if (abs(λ0 - sλ0) < ε) λ0 -= sλ0 * ε;
+ if (abs(λ1 - sλ1) < ε) λ1 -= sλ1 * ε;
+ φ0 = d3_geo_clipAntimeridianIntersect(λ0, φ0, λ1, φ1);
+ listener.point(sλ0, φ0);
+ listener.lineEnd();
+ listener.lineStart();
+ listener.point(sλ1, φ0);
+ clean = 0;
+ }
+ listener.point(λ0 = λ1, φ0 = φ1);
+ sλ0 = sλ1;
+ },
+ lineEnd: function() {
+ listener.lineEnd();
+ λ0 = φ0 = NaN;
+ },
+ clean: function() {
+ return 2 - clean;
+ }
+ };
+ }
+ function d3_geo_clipAntimeridianIntersect(λ0, φ0, λ1, φ1) {
+ var cosφ0, cosφ1, sinλ0_λ1 = Math.sin(λ0 - λ1);
+ return abs(sinλ0_λ1) > ε ? Math.atan((Math.sin(φ0) * (cosφ1 = Math.cos(φ1)) * Math.sin(λ1) - Math.sin(φ1) * (cosφ0 = Math.cos(φ0)) * Math.sin(λ0)) / (cosφ0 * cosφ1 * sinλ0_λ1)) : (φ0 + φ1) / 2;
+ }
+ function d3_geo_clipAntimeridianInterpolate(from, to, direction, listener) {
+ var φ;
+ if (from == null) {
+ φ = direction * halfπ;
+ listener.point(-π, φ);
+ listener.point(0, φ);
+ listener.point(π, φ);
+ listener.point(Ï€, 0);
+ listener.point(π, -φ);
+ listener.point(0, -φ);
+ listener.point(-π, -φ);
+ listener.point(-Ï€, 0);
+ listener.point(-π, φ);
+ } else if (abs(from[0] - to[0]) > ε) {
+ var s = from[0] < to[0] ? π : -π;
+ φ = direction * s / 2;
+ listener.point(-s, φ);
+ listener.point(0, φ);
+ listener.point(s, φ);
+ } else {
+ listener.point(to[0], to[1]);
+ }
+ }
+ function d3_geo_clipCircle(radius) {
+ var cr = Math.cos(radius), smallRadius = cr > 0, notHemisphere = abs(cr) > ε, interpolate = d3_geo_circleInterpolate(radius, 6 * d3_radians);
+ return d3_geo_clip(visible, clipLine, interpolate, smallRadius ? [ 0, -radius ] : [ -π, radius - π ]);
+ function visible(λ, φ) {
+ return Math.cos(λ) * Math.cos(φ) > cr;
+ }
+ function clipLine(listener) {
+ var point0, c0, v0, v00, clean;
+ return {
+ lineStart: function() {
+ v00 = v0 = false;
+ clean = 1;
+ },
+ point: function(λ, φ) {
+ var point1 = [ λ, φ ], point2, v = visible(λ, φ), c = smallRadius ? v ? 0 : code(λ, φ) : v ? code(λ + (λ < 0 ? π : -π), φ) : 0;
+ if (!point0 && (v00 = v0 = v)) listener.lineStart();
+ if (v !== v0) {
+ point2 = intersect(point0, point1);
+ if (d3_geo_sphericalEqual(point0, point2) || d3_geo_sphericalEqual(point1, point2)) {
+ point1[0] += ε;
+ point1[1] += ε;
+ v = visible(point1[0], point1[1]);
+ }
+ }
+ if (v !== v0) {
+ clean = 0;
+ if (v) {
+ listener.lineStart();
+ point2 = intersect(point1, point0);
+ listener.point(point2[0], point2[1]);
+ } else {
+ point2 = intersect(point0, point1);
+ listener.point(point2[0], point2[1]);
+ listener.lineEnd();
+ }
+ point0 = point2;
+ } else if (notHemisphere && point0 && smallRadius ^ v) {
+ var t;
+ if (!(c & c0) && (t = intersect(point1, point0, true))) {
+ clean = 0;
+ if (smallRadius) {
+ listener.lineStart();
+ listener.point(t[0][0], t[0][1]);
+ listener.point(t[1][0], t[1][1]);
+ listener.lineEnd();
+ } else {
+ listener.point(t[1][0], t[1][1]);
+ listener.lineEnd();
+ listener.lineStart();
+ listener.point(t[0][0], t[0][1]);
+ }
+ }
+ }
+ if (v && (!point0 || !d3_geo_sphericalEqual(point0, point1))) {
+ listener.point(point1[0], point1[1]);
+ }
+ point0 = point1, v0 = v, c0 = c;
+ },
+ lineEnd: function() {
+ if (v0) listener.lineEnd();
+ point0 = null;
+ },
+ clean: function() {
+ return clean | (v00 && v0) << 1;
+ }
+ };
+ }
+ function intersect(a, b, two) {
+ var pa = d3_geo_cartesian(a), pb = d3_geo_cartesian(b);
+ var n1 = [ 1, 0, 0 ], n2 = d3_geo_cartesianCross(pa, pb), n2n2 = d3_geo_cartesianDot(n2, n2), n1n2 = n2[0], determinant = n2n2 - n1n2 * n1n2;
+ if (!determinant) return !two && a;
+ var c1 = cr * n2n2 / determinant, c2 = -cr * n1n2 / determinant, n1xn2 = d3_geo_cartesianCross(n1, n2), A = d3_geo_cartesianScale(n1, c1), B = d3_geo_cartesianScale(n2, c2);
+ d3_geo_cartesianAdd(A, B);
+ var u = n1xn2, w = d3_geo_cartesianDot(A, u), uu = d3_geo_cartesianDot(u, u), t2 = w * w - uu * (d3_geo_cartesianDot(A, A) - 1);
+ if (t2 < 0) return;
+ var t = Math.sqrt(t2), q = d3_geo_cartesianScale(u, (-w - t) / uu);
+ d3_geo_cartesianAdd(q, A);
+ q = d3_geo_spherical(q);
+ if (!two) return q;
+ var λ0 = a[0], λ1 = b[0], φ0 = a[1], φ1 = b[1], z;
+ if (λ1 < λ0) z = λ0, λ0 = λ1, λ1 = z;
+ var δλ = λ1 - λ0, polar = abs(δλ - π) < ε, meridian = polar || δλ < ε;
+ if (!polar && φ1 < φ0) z = φ0, φ0 = φ1, φ1 = z;
+ if (meridian ? polar ? φ0 + φ1 > 0 ^ q[1] < (abs(q[0] - λ0) < ε ? φ0 : φ1) : φ0 <= q[1] && q[1] <= φ1 : δλ > π ^ (λ0 <= q[0] && q[0] <= λ1)) {
+ var q1 = d3_geo_cartesianScale(u, (-w + t) / uu);
+ d3_geo_cartesianAdd(q1, A);
+ return [ q, d3_geo_spherical(q1) ];
+ }
+ }
+ function code(λ, φ) {
+ var r = smallRadius ? radius : π - radius, code = 0;
+ if (λ < -r) code |= 1; else if (λ > r) code |= 2;
+ if (φ < -r) code |= 4; else if (φ > r) code |= 8;
+ return code;
+ }
+ }
+ function d3_geom_clipLine(x0, y0, x1, y1) {
+ return function(line) {
+ var a = line.a, b = line.b, ax = a.x, ay = a.y, bx = b.x, by = b.y, t0 = 0, t1 = 1, dx = bx - ax, dy = by - ay, r;
+ r = x0 - ax;
+ if (!dx && r > 0) return;
+ r /= dx;
+ if (dx < 0) {
+ if (r < t0) return;
+ if (r < t1) t1 = r;
+ } else if (dx > 0) {
+ if (r > t1) return;
+ if (r > t0) t0 = r;
+ }
+ r = x1 - ax;
+ if (!dx && r < 0) return;
+ r /= dx;
+ if (dx < 0) {
+ if (r > t1) return;
+ if (r > t0) t0 = r;
+ } else if (dx > 0) {
+ if (r < t0) return;
+ if (r < t1) t1 = r;
+ }
+ r = y0 - ay;
+ if (!dy && r > 0) return;
+ r /= dy;
+ if (dy < 0) {
+ if (r < t0) return;
+ if (r < t1) t1 = r;
+ } else if (dy > 0) {
+ if (r > t1) return;
+ if (r > t0) t0 = r;
+ }
+ r = y1 - ay;
+ if (!dy && r < 0) return;
+ r /= dy;
+ if (dy < 0) {
+ if (r > t1) return;
+ if (r > t0) t0 = r;
+ } else if (dy > 0) {
+ if (r < t0) return;
+ if (r < t1) t1 = r;
+ }
+ if (t0 > 0) line.a = {
+ x: ax + t0 * dx,
+ y: ay + t0 * dy
+ };
+ if (t1 < 1) line.b = {
+ x: ax + t1 * dx,
+ y: ay + t1 * dy
+ };
+ return line;
+ };
+ }
+ var d3_geo_clipExtentMAX = 1e9;
+ d3.geo.clipExtent = function() {
+ var x0, y0, x1, y1, stream, clip, clipExtent = {
+ stream: function(output) {
+ if (stream) stream.valid = false;
+ stream = clip(output);
+ stream.valid = true;
+ return stream;
+ },
+ extent: function(_) {
+ if (!arguments.length) return [ [ x0, y0 ], [ x1, y1 ] ];
+ clip = d3_geo_clipExtent(x0 = +_[0][0], y0 = +_[0][1], x1 = +_[1][0], y1 = +_[1][1]);
+ if (stream) stream.valid = false, stream = null;
+ return clipExtent;
+ }
+ };
+ return clipExtent.extent([ [ 0, 0 ], [ 960, 500 ] ]);
+ };
+ function d3_geo_clipExtent(x0, y0, x1, y1) {
+ return function(listener) {
+ var listener_ = listener, bufferListener = d3_geo_clipBufferListener(), clipLine = d3_geom_clipLine(x0, y0, x1, y1), segments, polygon, ring;
+ var clip = {
+ point: point,
+ lineStart: lineStart,
+ lineEnd: lineEnd,
+ polygonStart: function() {
+ listener = bufferListener;
+ segments = [];
+ polygon = [];
+ clean = true;
+ },
+ polygonEnd: function() {
+ listener = listener_;
+ segments = d3.merge(segments);
+ var clipStartInside = insidePolygon([ x0, y1 ]), inside = clean && clipStartInside, visible = segments.length;
+ if (inside || visible) {
+ listener.polygonStart();
+ if (inside) {
+ listener.lineStart();
+ interpolate(null, null, 1, listener);
+ listener.lineEnd();
+ }
+ if (visible) {
+ d3_geo_clipPolygon(segments, compare, clipStartInside, interpolate, listener);
+ }
+ listener.polygonEnd();
+ }
+ segments = polygon = ring = null;
+ }
+ };
+ function insidePolygon(p) {
+ var wn = 0, n = polygon.length, y = p[1];
+ for (var i = 0; i < n; ++i) {
+ for (var j = 1, v = polygon[i], m = v.length, a = v[0], b; j < m; ++j) {
+ b = v[j];
+ if (a[1] <= y) {
+ if (b[1] > y && d3_cross2d(a, b, p) > 0) ++wn;
+ } else {
+ if (b[1] <= y && d3_cross2d(a, b, p) < 0) --wn;
+ }
+ a = b;
+ }
+ }
+ return wn !== 0;
+ }
+ function interpolate(from, to, direction, listener) {
+ var a = 0, a1 = 0;
+ if (from == null || (a = corner(from, direction)) !== (a1 = corner(to, direction)) || comparePoints(from, to) < 0 ^ direction > 0) {
+ do {
+ listener.point(a === 0 || a === 3 ? x0 : x1, a > 1 ? y1 : y0);
+ } while ((a = (a + direction + 4) % 4) !== a1);
+ } else {
+ listener.point(to[0], to[1]);
+ }
+ }
+ function pointVisible(x, y) {
+ return x0 <= x && x <= x1 && y0 <= y && y <= y1;
+ }
+ function point(x, y) {
+ if (pointVisible(x, y)) listener.point(x, y);
+ }
+ var x__, y__, v__, x_, y_, v_, first, clean;
+ function lineStart() {
+ clip.point = linePoint;
+ if (polygon) polygon.push(ring = []);
+ first = true;
+ v_ = false;
+ x_ = y_ = NaN;
+ }
+ function lineEnd() {
+ if (segments) {
+ linePoint(x__, y__);
+ if (v__ && v_) bufferListener.rejoin();
+ segments.push(bufferListener.buffer());
+ }
+ clip.point = point;
+ if (v_) listener.lineEnd();
+ }
+ function linePoint(x, y) {
+ x = Math.max(-d3_geo_clipExtentMAX, Math.min(d3_geo_clipExtentMAX, x));
+ y = Math.max(-d3_geo_clipExtentMAX, Math.min(d3_geo_clipExtentMAX, y));
+ var v = pointVisible(x, y);
+ if (polygon) ring.push([ x, y ]);
+ if (first) {
+ x__ = x, y__ = y, v__ = v;
+ first = false;
+ if (v) {
+ listener.lineStart();
+ listener.point(x, y);
+ }
+ } else {
+ if (v && v_) listener.point(x, y); else {
+ var l = {
+ a: {
+ x: x_,
+ y: y_
+ },
+ b: {
+ x: x,
+ y: y
+ }
+ };
+ if (clipLine(l)) {
+ if (!v_) {
+ listener.lineStart();
+ listener.point(l.a.x, l.a.y);
+ }
+ listener.point(l.b.x, l.b.y);
+ if (!v) listener.lineEnd();
+ clean = false;
+ } else if (v) {
+ listener.lineStart();
+ listener.point(x, y);
+ clean = false;
+ }
+ }
+ }
+ x_ = x, y_ = y, v_ = v;
+ }
+ return clip;
+ };
+ function corner(p, direction) {
+ return abs(p[0] - x0) < ε ? direction > 0 ? 0 : 3 : abs(p[0] - x1) < ε ? direction > 0 ? 2 : 1 : abs(p[1] - y0) < ε ? direction > 0 ? 1 : 0 : direction > 0 ? 3 : 2;
+ }
+ function compare(a, b) {
+ return comparePoints(a.x, b.x);
+ }
+ function comparePoints(a, b) {
+ var ca = corner(a, 1), cb = corner(b, 1);
+ return ca !== cb ? ca - cb : ca === 0 ? b[1] - a[1] : ca === 1 ? a[0] - b[0] : ca === 2 ? a[1] - b[1] : b[0] - a[0];
+ }
+ }
+ function d3_geo_compose(a, b) {
+ function compose(x, y) {
+ return x = a(x, y), b(x[0], x[1]);
+ }
+ if (a.invert && b.invert) compose.invert = function(x, y) {
+ return x = b.invert(x, y), x && a.invert(x[0], x[1]);
+ };
+ return compose;
+ }
+ function d3_geo_conic(projectAt) {
+ var φ0 = 0, φ1 = π / 3, m = d3_geo_projectionMutator(projectAt), p = m(φ0, φ1);
+ p.parallels = function(_) {
+ if (!arguments.length) return [ φ0 / π * 180, φ1 / π * 180 ];
+ return m(φ0 = _[0] * π / 180, φ1 = _[1] * π / 180);
+ };
+ return p;
+ }
+ function d3_geo_conicEqualArea(φ0, φ1) {
+ var sinφ0 = Math.sin(φ0), n = (sinφ0 + Math.sin(φ1)) / 2, C = 1 + sinφ0 * (2 * n - sinφ0), Ï0 = Math.sqrt(C) / n;
+ function forward(λ, φ) {
+ var Ï = Math.sqrt(C - 2 * n * Math.sin(φ)) / n;
+ return [ Ï * Math.sin(λ *= n), Ï0 - Ï * Math.cos(λ) ];
+ }
+ forward.invert = function(x, y) {
+ var Ï0_y = Ï0 - y;
+ return [ Math.atan2(x, Ï0_y) / n, d3_asin((C - (x * x + Ï0_y * Ï0_y) * n * n) / (2 * n)) ];
+ };
+ return forward;
+ }
+ (d3.geo.conicEqualArea = function() {
+ return d3_geo_conic(d3_geo_conicEqualArea);
+ }).raw = d3_geo_conicEqualArea;
+ d3.geo.albers = function() {
+ return d3.geo.conicEqualArea().rotate([ 96, 0 ]).center([ -.6, 38.7 ]).parallels([ 29.5, 45.5 ]).scale(1070);
+ };
+ d3.geo.albersUsa = function() {
+ var lower48 = d3.geo.albers();
+ var alaska = d3.geo.conicEqualArea().rotate([ 154, 0 ]).center([ -2, 58.5 ]).parallels([ 55, 65 ]);
+ var hawaii = d3.geo.conicEqualArea().rotate([ 157, 0 ]).center([ -3, 19.9 ]).parallels([ 8, 18 ]);
+ var point, pointStream = {
+ point: function(x, y) {
+ point = [ x, y ];
+ }
+ }, lower48Point, alaskaPoint, hawaiiPoint;
+ function albersUsa(coordinates) {
+ var x = coordinates[0], y = coordinates[1];
+ point = null;
+ (lower48Point(x, y), point) || (alaskaPoint(x, y), point) || hawaiiPoint(x, y);
+ return point;
+ }
+ albersUsa.invert = function(coordinates) {
+ var k = lower48.scale(), t = lower48.translate(), x = (coordinates[0] - t[0]) / k, y = (coordinates[1] - t[1]) / k;
+ return (y >= .12 && y < .234 && x >= -.425 && x < -.214 ? alaska : y >= .166 && y < .234 && x >= -.214 && x < -.115 ? hawaii : lower48).invert(coordinates);
+ };
+ albersUsa.stream = function(stream) {
+ var lower48Stream = lower48.stream(stream), alaskaStream = alaska.stream(stream), hawaiiStream = hawaii.stream(stream);
+ return {
+ point: function(x, y) {
+ lower48Stream.point(x, y);
+ alaskaStream.point(x, y);
+ hawaiiStream.point(x, y);
+ },
+ sphere: function() {
+ lower48Stream.sphere();
+ alaskaStream.sphere();
+ hawaiiStream.sphere();
+ },
+ lineStart: function() {
+ lower48Stream.lineStart();
+ alaskaStream.lineStart();
+ hawaiiStream.lineStart();
+ },
+ lineEnd: function() {
+ lower48Stream.lineEnd();
+ alaskaStream.lineEnd();
+ hawaiiStream.lineEnd();
+ },
+ polygonStart: function() {
+ lower48Stream.polygonStart();
+ alaskaStream.polygonStart();
+ hawaiiStream.polygonStart();
+ },
+ polygonEnd: function() {
+ lower48Stream.polygonEnd();
+ alaskaStream.polygonEnd();
+ hawaiiStream.polygonEnd();
+ }
+ };
+ };
+ albersUsa.precision = function(_) {
+ if (!arguments.length) return lower48.precision();
+ lower48.precision(_);
+ alaska.precision(_);
+ hawaii.precision(_);
+ return albersUsa;
+ };
+ albersUsa.scale = function(_) {
+ if (!arguments.length) return lower48.scale();
+ lower48.scale(_);
+ alaska.scale(_ * .35);
+ hawaii.scale(_);
+ return albersUsa.translate(lower48.translate());
+ };
+ albersUsa.translate = function(_) {
+ if (!arguments.length) return lower48.translate();
+ var k = lower48.scale(), x = +_[0], y = +_[1];
+ lower48Point = lower48.translate(_).clipExtent([ [ x - .455 * k, y - .238 * k ], [ x + .455 * k, y + .238 * k ] ]).stream(pointStream).point;
+ alaskaPoint = alaska.translate([ x - .307 * k, y + .201 * k ]).clipExtent([ [ x - .425 * k + ε, y + .12 * k + ε ], [ x - .214 * k - ε, y + .234 * k - ε ] ]).stream(pointStream).point;
+ hawaiiPoint = hawaii.translate([ x - .205 * k, y + .212 * k ]).clipExtent([ [ x - .214 * k + ε, y + .166 * k + ε ], [ x - .115 * k - ε, y + .234 * k - ε ] ]).stream(pointStream).point;
+ return albersUsa;
+ };
+ return albersUsa.scale(1070);
+ };
+ var d3_geo_pathAreaSum, d3_geo_pathAreaPolygon, d3_geo_pathArea = {
+ point: d3_noop,
+ lineStart: d3_noop,
+ lineEnd: d3_noop,
+ polygonStart: function() {
+ d3_geo_pathAreaPolygon = 0;
+ d3_geo_pathArea.lineStart = d3_geo_pathAreaRingStart;
+ },
+ polygonEnd: function() {
+ d3_geo_pathArea.lineStart = d3_geo_pathArea.lineEnd = d3_geo_pathArea.point = d3_noop;
+ d3_geo_pathAreaSum += abs(d3_geo_pathAreaPolygon / 2);
+ }
+ };
+ function d3_geo_pathAreaRingStart() {
+ var x00, y00, x0, y0;
+ d3_geo_pathArea.point = function(x, y) {
+ d3_geo_pathArea.point = nextPoint;
+ x00 = x0 = x, y00 = y0 = y;
+ };
+ function nextPoint(x, y) {
+ d3_geo_pathAreaPolygon += y0 * x - x0 * y;
+ x0 = x, y0 = y;
+ }
+ d3_geo_pathArea.lineEnd = function() {
+ nextPoint(x00, y00);
+ };
+ }
+ var d3_geo_pathBoundsX0, d3_geo_pathBoundsY0, d3_geo_pathBoundsX1, d3_geo_pathBoundsY1;
+ var d3_geo_pathBounds = {
+ point: d3_geo_pathBoundsPoint,
+ lineStart: d3_noop,
+ lineEnd: d3_noop,
+ polygonStart: d3_noop,
+ polygonEnd: d3_noop
+ };
+ function d3_geo_pathBoundsPoint(x, y) {
+ if (x < d3_geo_pathBoundsX0) d3_geo_pathBoundsX0 = x;
+ if (x > d3_geo_pathBoundsX1) d3_geo_pathBoundsX1 = x;
+ if (y < d3_geo_pathBoundsY0) d3_geo_pathBoundsY0 = y;
+ if (y > d3_geo_pathBoundsY1) d3_geo_pathBoundsY1 = y;
+ }
+ function d3_geo_pathBuffer() {
+ var pointCircle = d3_geo_pathBufferCircle(4.5), buffer = [];
+ var stream = {
+ point: point,
+ lineStart: function() {
+ stream.point = pointLineStart;
+ },
+ lineEnd: lineEnd,
+ polygonStart: function() {
+ stream.lineEnd = lineEndPolygon;
+ },
+ polygonEnd: function() {
+ stream.lineEnd = lineEnd;
+ stream.point = point;
+ },
+ pointRadius: function(_) {
+ pointCircle = d3_geo_pathBufferCircle(_);
+ return stream;
+ },
+ result: function() {
+ if (buffer.length) {
+ var result = buffer.join("");
+ buffer = [];
+ return result;
+ }
+ }
+ };
+ function point(x, y) {
+ buffer.push("M", x, ",", y, pointCircle);
+ }
+ function pointLineStart(x, y) {
+ buffer.push("M", x, ",", y);
+ stream.point = pointLine;
+ }
+ function pointLine(x, y) {
+ buffer.push("L", x, ",", y);
+ }
+ function lineEnd() {
+ stream.point = point;
+ }
+ function lineEndPolygon() {
+ buffer.push("Z");
+ }
+ return stream;
+ }
+ function d3_geo_pathBufferCircle(radius) {
+ return "m0," + radius + "a" + radius + "," + radius + " 0 1,1 0," + -2 * radius + "a" + radius + "," + radius + " 0 1,1 0," + 2 * radius + "z";
+ }
+ var d3_geo_pathCentroid = {
+ point: d3_geo_pathCentroidPoint,
+ lineStart: d3_geo_pathCentroidLineStart,
+ lineEnd: d3_geo_pathCentroidLineEnd,
+ polygonStart: function() {
+ d3_geo_pathCentroid.lineStart = d3_geo_pathCentroidRingStart;
+ },
+ polygonEnd: function() {
+ d3_geo_pathCentroid.point = d3_geo_pathCentroidPoint;
+ d3_geo_pathCentroid.lineStart = d3_geo_pathCentroidLineStart;
+ d3_geo_pathCentroid.lineEnd = d3_geo_pathCentroidLineEnd;
+ }
+ };
+ function d3_geo_pathCentroidPoint(x, y) {
+ d3_geo_centroidX0 += x;
+ d3_geo_centroidY0 += y;
+ ++d3_geo_centroidZ0;
+ }
+ function d3_geo_pathCentroidLineStart() {
+ var x0, y0;
+ d3_geo_pathCentroid.point = function(x, y) {
+ d3_geo_pathCentroid.point = nextPoint;
+ d3_geo_pathCentroidPoint(x0 = x, y0 = y);
+ };
+ function nextPoint(x, y) {
+ var dx = x - x0, dy = y - y0, z = Math.sqrt(dx * dx + dy * dy);
+ d3_geo_centroidX1 += z * (x0 + x) / 2;
+ d3_geo_centroidY1 += z * (y0 + y) / 2;
+ d3_geo_centroidZ1 += z;
+ d3_geo_pathCentroidPoint(x0 = x, y0 = y);
+ }
+ }
+ function d3_geo_pathCentroidLineEnd() {
+ d3_geo_pathCentroid.point = d3_geo_pathCentroidPoint;
+ }
+ function d3_geo_pathCentroidRingStart() {
+ var x00, y00, x0, y0;
+ d3_geo_pathCentroid.point = function(x, y) {
+ d3_geo_pathCentroid.point = nextPoint;
+ d3_geo_pathCentroidPoint(x00 = x0 = x, y00 = y0 = y);
+ };
+ function nextPoint(x, y) {
+ var dx = x - x0, dy = y - y0, z = Math.sqrt(dx * dx + dy * dy);
+ d3_geo_centroidX1 += z * (x0 + x) / 2;
+ d3_geo_centroidY1 += z * (y0 + y) / 2;
+ d3_geo_centroidZ1 += z;
+ z = y0 * x - x0 * y;
+ d3_geo_centroidX2 += z * (x0 + x);
+ d3_geo_centroidY2 += z * (y0 + y);
+ d3_geo_centroidZ2 += z * 3;
+ d3_geo_pathCentroidPoint(x0 = x, y0 = y);
+ }
+ d3_geo_pathCentroid.lineEnd = function() {
+ nextPoint(x00, y00);
+ };
+ }
+ function d3_geo_pathContext(context) {
+ var pointRadius = 4.5;
+ var stream = {
+ point: point,
+ lineStart: function() {
+ stream.point = pointLineStart;
+ },
+ lineEnd: lineEnd,
+ polygonStart: function() {
+ stream.lineEnd = lineEndPolygon;
+ },
+ polygonEnd: function() {
+ stream.lineEnd = lineEnd;
+ stream.point = point;
+ },
+ pointRadius: function(_) {
+ pointRadius = _;
+ return stream;
+ },
+ result: d3_noop
+ };
+ function point(x, y) {
+ context.moveTo(x, y);
+ context.arc(x, y, pointRadius, 0, Ï„);
+ }
+ function pointLineStart(x, y) {
+ context.moveTo(x, y);
+ stream.point = pointLine;
+ }
+ function pointLine(x, y) {
+ context.lineTo(x, y);
+ }
+ function lineEnd() {
+ stream.point = point;
+ }
+ function lineEndPolygon() {
+ context.closePath();
+ }
+ return stream;
+ }
+ function d3_geo_resample(project) {
+ var δ2 = .5, cosMinDistance = Math.cos(30 * d3_radians), maxDepth = 16;
+ function resample(stream) {
+ return (maxDepth ? resampleRecursive : resampleNone)(stream);
+ }
+ function resampleNone(stream) {
+ return d3_geo_transformPoint(stream, function(x, y) {
+ x = project(x, y);
+ stream.point(x[0], x[1]);
+ });
+ }
+ function resampleRecursive(stream) {
+ var λ00, φ00, x00, y00, a00, b00, c00, λ0, x0, y0, a0, b0, c0;
+ var resample = {
+ point: point,
+ lineStart: lineStart,
+ lineEnd: lineEnd,
+ polygonStart: function() {
+ stream.polygonStart();
+ resample.lineStart = ringStart;
+ },
+ polygonEnd: function() {
+ stream.polygonEnd();
+ resample.lineStart = lineStart;
+ }
+ };
+ function point(x, y) {
+ x = project(x, y);
+ stream.point(x[0], x[1]);
+ }
+ function lineStart() {
+ x0 = NaN;
+ resample.point = linePoint;
+ stream.lineStart();
+ }
+ function linePoint(λ, φ) {
+ var c = d3_geo_cartesian([ λ, φ ]), p = project(λ, φ);
+ resampleLineTo(x0, y0, λ0, a0, b0, c0, x0 = p[0], y0 = p[1], λ0 = λ, a0 = c[0], b0 = c[1], c0 = c[2], maxDepth, stream);
+ stream.point(x0, y0);
+ }
+ function lineEnd() {
+ resample.point = point;
+ stream.lineEnd();
+ }
+ function ringStart() {
+ lineStart();
+ resample.point = ringPoint;
+ resample.lineEnd = ringEnd;
+ }
+ function ringPoint(λ, φ) {
+ linePoint(λ00 = λ, φ00 = φ), x00 = x0, y00 = y0, a00 = a0, b00 = b0, c00 = c0;
+ resample.point = linePoint;
+ }
+ function ringEnd() {
+ resampleLineTo(x0, y0, λ0, a0, b0, c0, x00, y00, λ00, a00, b00, c00, maxDepth, stream);
+ resample.lineEnd = lineEnd;
+ lineEnd();
+ }
+ return resample;
+ }
+ function resampleLineTo(x0, y0, λ0, a0, b0, c0, x1, y1, λ1, a1, b1, c1, depth, stream) {
+ var dx = x1 - x0, dy = y1 - y0, d2 = dx * dx + dy * dy;
+ if (d2 > 4 * δ2 && depth--) {
+ var a = a0 + a1, b = b0 + b1, c = c0 + c1, m = Math.sqrt(a * a + b * b + c * c), φ2 = Math.asin(c /= m), λ2 = abs(abs(c) - 1) < ε || abs(λ0 - λ1) < ε ? (λ0 + λ1) / 2 : Math.atan2(b, a), p = project(λ2, φ2), x2 = p[0], y2 = p[1], dx2 = x2 - x0, dy2 = y2 - y0, dz = dy * dx2 - dx * dy2;
+ if (dz * dz / d2 > δ2 || abs((dx * dx2 + dy * dy2) / d2 - .5) > .3 || a0 * a1 + b0 * b1 + c0 * c1 < cosMinDistance) {
+ resampleLineTo(x0, y0, λ0, a0, b0, c0, x2, y2, λ2, a /= m, b /= m, c, depth, stream);
+ stream.point(x2, y2);
+ resampleLineTo(x2, y2, λ2, a, b, c, x1, y1, λ1, a1, b1, c1, depth, stream);
+ }
+ }
+ }
+ resample.precision = function(_) {
+ if (!arguments.length) return Math.sqrt(δ2);
+ maxDepth = (δ2 = _ * _) > 0 && 16;
+ return resample;
+ };
+ return resample;
+ }
+ d3.geo.path = function() {
+ var pointRadius = 4.5, projection, context, projectStream, contextStream, cacheStream;
+ function path(object) {
+ if (object) {
+ if (typeof pointRadius === "function") contextStream.pointRadius(+pointRadius.apply(this, arguments));
+ if (!cacheStream || !cacheStream.valid) cacheStream = projectStream(contextStream);
+ d3.geo.stream(object, cacheStream);
+ }
+ return contextStream.result();
+ }
+ path.area = function(object) {
+ d3_geo_pathAreaSum = 0;
+ d3.geo.stream(object, projectStream(d3_geo_pathArea));
+ return d3_geo_pathAreaSum;
+ };
+ path.centroid = function(object) {
+ d3_geo_centroidX0 = d3_geo_centroidY0 = d3_geo_centroidZ0 = d3_geo_centroidX1 = d3_geo_centroidY1 = d3_geo_centroidZ1 = d3_geo_centroidX2 = d3_geo_centroidY2 = d3_geo_centroidZ2 = 0;
+ d3.geo.stream(object, projectStream(d3_geo_pathCentroid));
+ return d3_geo_centroidZ2 ? [ d3_geo_centroidX2 / d3_geo_centroidZ2, d3_geo_centroidY2 / d3_geo_centroidZ2 ] : d3_geo_centroidZ1 ? [ d3_geo_centroidX1 / d3_geo_centroidZ1, d3_geo_centroidY1 / d3_geo_centroidZ1 ] : d3_geo_centroidZ0 ? [ d3_geo_centroidX0 / d3_geo_centroidZ0, d3_geo_centroidY0 / d3_geo_centroidZ0 ] : [ NaN, NaN ];
+ };
+ path.bounds = function(object) {
+ d3_geo_pathBoundsX1 = d3_geo_pathBoundsY1 = -(d3_geo_pathBoundsX0 = d3_geo_pathBoundsY0 = Infinity);
+ d3.geo.stream(object, projectStream(d3_geo_pathBounds));
+ return [ [ d3_geo_pathBoundsX0, d3_geo_pathBoundsY0 ], [ d3_geo_pathBoundsX1, d3_geo_pathBoundsY1 ] ];
+ };
+ path.projection = function(_) {
+ if (!arguments.length) return projection;
+ projectStream = (projection = _) ? _.stream || d3_geo_pathProjectStream(_) : d3_identity;
+ return reset();
+ };
+ path.context = function(_) {
+ if (!arguments.length) return context;
+ contextStream = (context = _) == null ? new d3_geo_pathBuffer() : new d3_geo_pathContext(_);
+ if (typeof pointRadius !== "function") contextStream.pointRadius(pointRadius);
+ return reset();
+ };
+ path.pointRadius = function(_) {
+ if (!arguments.length) return pointRadius;
+ pointRadius = typeof _ === "function" ? _ : (contextStream.pointRadius(+_), +_);
+ return path;
+ };
+ function reset() {
+ cacheStream = null;
+ return path;
+ }
+ return path.projection(d3.geo.albersUsa()).context(null);
+ };
+ function d3_geo_pathProjectStream(project) {
+ var resample = d3_geo_resample(function(x, y) {
+ return project([ x * d3_degrees, y * d3_degrees ]);
+ });
+ return function(stream) {
+ return d3_geo_projectionRadians(resample(stream));
+ };
+ }
+ d3.geo.transform = function(methods) {
+ return {
+ stream: function(stream) {
+ var transform = new d3_geo_transform(stream);
+ for (var k in methods) transform[k] = methods[k];
+ return transform;
+ }
+ };
+ };
+ function d3_geo_transform(stream) {
+ this.stream = stream;
+ }
+ d3_geo_transform.prototype = {
+ point: function(x, y) {
+ this.stream.point(x, y);
+ },
+ sphere: function() {
+ this.stream.sphere();
+ },
+ lineStart: function() {
+ this.stream.lineStart();
+ },
+ lineEnd: function() {
+ this.stream.lineEnd();
+ },
+ polygonStart: function() {
+ this.stream.polygonStart();
+ },
+ polygonEnd: function() {
+ this.stream.polygonEnd();
+ }
+ };
+ function d3_geo_transformPoint(stream, point) {
+ return {
+ point: point,
+ sphere: function() {
+ stream.sphere();
+ },
+ lineStart: function() {
+ stream.lineStart();
+ },
+ lineEnd: function() {
+ stream.lineEnd();
+ },
+ polygonStart: function() {
+ stream.polygonStart();
+ },
+ polygonEnd: function() {
+ stream.polygonEnd();
+ }
+ };
+ }
+ d3.geo.projection = d3_geo_projection;
+ d3.geo.projectionMutator = d3_geo_projectionMutator;
+ function d3_geo_projection(project) {
+ return d3_geo_projectionMutator(function() {
+ return project;
+ })();
+ }
+ function d3_geo_projectionMutator(projectAt) {
+ var project, rotate, projectRotate, projectResample = d3_geo_resample(function(x, y) {
+ x = project(x, y);
+ return [ x[0] * k + δx, δy - x[1] * k ];
+ }), k = 150, x = 480, y = 250, λ = 0, φ = 0, δλ = 0, δφ = 0, δγ = 0, δx, δy, preclip = d3_geo_clipAntimeridian, postclip = d3_identity, clipAngle = null, clipExtent = null, stream;
+ function projection(point) {
+ point = projectRotate(point[0] * d3_radians, point[1] * d3_radians);
+ return [ point[0] * k + δx, δy - point[1] * k ];
+ }
+ function invert(point) {
+ point = projectRotate.invert((point[0] - δx) / k, (δy - point[1]) / k);
+ return point && [ point[0] * d3_degrees, point[1] * d3_degrees ];
+ }
+ projection.stream = function(output) {
+ if (stream) stream.valid = false;
+ stream = d3_geo_projectionRadians(preclip(rotate, projectResample(postclip(output))));
+ stream.valid = true;
+ return stream;
+ };
+ projection.clipAngle = function(_) {
+ if (!arguments.length) return clipAngle;
+ preclip = _ == null ? (clipAngle = _, d3_geo_clipAntimeridian) : d3_geo_clipCircle((clipAngle = +_) * d3_radians);
+ return invalidate();
+ };
+ projection.clipExtent = function(_) {
+ if (!arguments.length) return clipExtent;
+ clipExtent = _;
+ postclip = _ ? d3_geo_clipExtent(_[0][0], _[0][1], _[1][0], _[1][1]) : d3_identity;
+ return invalidate();
+ };
+ projection.scale = function(_) {
+ if (!arguments.length) return k;
+ k = +_;
+ return reset();
+ };
+ projection.translate = function(_) {
+ if (!arguments.length) return [ x, y ];
+ x = +_[0];
+ y = +_[1];
+ return reset();
+ };
+ projection.center = function(_) {
+ if (!arguments.length) return [ λ * d3_degrees, φ * d3_degrees ];
+ λ = _[0] % 360 * d3_radians;
+ φ = _[1] % 360 * d3_radians;
+ return reset();
+ };
+ projection.rotate = function(_) {
+ if (!arguments.length) return [ δλ * d3_degrees, δφ * d3_degrees, δγ * d3_degrees ];
+ δλ = _[0] % 360 * d3_radians;
+ δφ = _[1] % 360 * d3_radians;
+ δγ = _.length > 2 ? _[2] % 360 * d3_radians : 0;
+ return reset();
+ };
+ d3.rebind(projection, projectResample, "precision");
+ function reset() {
+ projectRotate = d3_geo_compose(rotate = d3_geo_rotation(δλ, δφ, δγ), project);
+ var center = project(λ, φ);
+ δx = x - center[0] * k;
+ δy = y + center[1] * k;
+ return invalidate();
+ }
+ function invalidate() {
+ if (stream) stream.valid = false, stream = null;
+ return projection;
+ }
+ return function() {
+ project = projectAt.apply(this, arguments);
+ projection.invert = project.invert && invert;
+ return reset();
+ };
+ }
+ function d3_geo_projectionRadians(stream) {
+ return d3_geo_transformPoint(stream, function(x, y) {
+ stream.point(x * d3_radians, y * d3_radians);
+ });
+ }
+ function d3_geo_equirectangular(λ, φ) {
+ return [ λ, φ ];
+ }
+ (d3.geo.equirectangular = function() {
+ return d3_geo_projection(d3_geo_equirectangular);
+ }).raw = d3_geo_equirectangular.invert = d3_geo_equirectangular;
+ d3.geo.rotation = function(rotate) {
+ rotate = d3_geo_rotation(rotate[0] % 360 * d3_radians, rotate[1] * d3_radians, rotate.length > 2 ? rotate[2] * d3_radians : 0);
+ function forward(coordinates) {
+ coordinates = rotate(coordinates[0] * d3_radians, coordinates[1] * d3_radians);
+ return coordinates[0] *= d3_degrees, coordinates[1] *= d3_degrees, coordinates;
+ }
+ forward.invert = function(coordinates) {
+ coordinates = rotate.invert(coordinates[0] * d3_radians, coordinates[1] * d3_radians);
+ return coordinates[0] *= d3_degrees, coordinates[1] *= d3_degrees, coordinates;
+ };
+ return forward;
+ };
+ function d3_geo_identityRotation(λ, φ) {
+ return [ λ > π ? λ - τ : λ < -π ? λ + τ : λ, φ ];
+ }
+ d3_geo_identityRotation.invert = d3_geo_equirectangular;
+ function d3_geo_rotation(δλ, δφ, δγ) {
+ return δλ ? δφ || δγ ? d3_geo_compose(d3_geo_rotationλ(δλ), d3_geo_rotationφγ(δφ, δγ)) : d3_geo_rotationλ(δλ) : δφ || δγ ? d3_geo_rotationφγ(δφ, δγ) : d3_geo_identityRotation;
+ }
+ function d3_geo_forwardRotationλ(δλ) {
+ return function(λ, φ) {
+ return λ += δλ, [ λ > π ? λ - τ : λ < -π ? λ + τ : λ, φ ];
+ };
+ }
+ function d3_geo_rotationλ(δλ) {
+ var rotation = d3_geo_forwardRotationλ(δλ);
+ rotation.invert = d3_geo_forwardRotationλ(-δλ);
+ return rotation;
+ }
+ function d3_geo_rotationφγ(δφ, δγ) {
+ var cosδφ = Math.cos(δφ), sinδφ = Math.sin(δφ), cosδγ = Math.cos(δγ), sinδγ = Math.sin(δγ);
+ function rotation(λ, φ) {
+ var cosφ = Math.cos(φ), x = Math.cos(λ) * cosφ, y = Math.sin(λ) * cosφ, z = Math.sin(φ), k = z * cosδφ + x * sinδφ;
+ return [ Math.atan2(y * cosδγ - k * sinδγ, x * cosδφ - z * sinδφ), d3_asin(k * cosδγ + y * sinδγ) ];
+ }
+ rotation.invert = function(λ, φ) {
+ var cosφ = Math.cos(φ), x = Math.cos(λ) * cosφ, y = Math.sin(λ) * cosφ, z = Math.sin(φ), k = z * cosδγ - y * sinδγ;
+ return [ Math.atan2(y * cosδγ + z * sinδγ, x * cosδφ + k * sinδφ), d3_asin(k * cosδφ - x * sinδφ) ];
+ };
+ return rotation;
+ }
+ d3.geo.circle = function() {
+ var origin = [ 0, 0 ], angle, precision = 6, interpolate;
+ function circle() {
+ var center = typeof origin === "function" ? origin.apply(this, arguments) : origin, rotate = d3_geo_rotation(-center[0] * d3_radians, -center[1] * d3_radians, 0).invert, ring = [];
+ interpolate(null, null, 1, {
+ point: function(x, y) {
+ ring.push(x = rotate(x, y));
+ x[0] *= d3_degrees, x[1] *= d3_degrees;
+ }
+ });
+ return {
+ type: "Polygon",
+ coordinates: [ ring ]
+ };
+ }
+ circle.origin = function(x) {
+ if (!arguments.length) return origin;
+ origin = x;
+ return circle;
+ };
+ circle.angle = function(x) {
+ if (!arguments.length) return angle;
+ interpolate = d3_geo_circleInterpolate((angle = +x) * d3_radians, precision * d3_radians);
+ return circle;
+ };
+ circle.precision = function(_) {
+ if (!arguments.length) return precision;
+ interpolate = d3_geo_circleInterpolate(angle * d3_radians, (precision = +_) * d3_radians);
+ return circle;
+ };
+ return circle.angle(90);
+ };
+ function d3_geo_circleInterpolate(radius, precision) {
+ var cr = Math.cos(radius), sr = Math.sin(radius);
+ return function(from, to, direction, listener) {
+ var step = direction * precision;
+ if (from != null) {
+ from = d3_geo_circleAngle(cr, from);
+ to = d3_geo_circleAngle(cr, to);
+ if (direction > 0 ? from < to : from > to) from += direction * Ï„;
+ } else {
+ from = radius + direction * Ï„;
+ to = radius - .5 * step;
+ }
+ for (var point, t = from; direction > 0 ? t > to : t < to; t -= step) {
+ listener.point((point = d3_geo_spherical([ cr, -sr * Math.cos(t), -sr * Math.sin(t) ]))[0], point[1]);
+ }
+ };
+ }
+ function d3_geo_circleAngle(cr, point) {
+ var a = d3_geo_cartesian(point);
+ a[0] -= cr;
+ d3_geo_cartesianNormalize(a);
+ var angle = d3_acos(-a[1]);
+ return ((-a[2] < 0 ? -angle : angle) + 2 * Math.PI - ε) % (2 * Math.PI);
+ }
+ d3.geo.distance = function(a, b) {
+ var Δλ = (b[0] - a[0]) * d3_radians, φ0 = a[1] * d3_radians, φ1 = b[1] * d3_radians, sinΔλ = Math.sin(Δλ), cosΔλ = Math.cos(Δλ), sinφ0 = Math.sin(φ0), cosφ0 = Math.cos(φ0), sinφ1 = Math.sin(φ1), cosφ1 = Math.cos(φ1), t;
+ return Math.atan2(Math.sqrt((t = cosφ1 * sinΔλ) * t + (t = cosφ0 * sinφ1 - sinφ0 * cosφ1 * cosΔλ) * t), sinφ0 * sinφ1 + cosφ0 * cosφ1 * cosΔλ);
+ };
+ d3.geo.graticule = function() {
+ var x1, x0, X1, X0, y1, y0, Y1, Y0, dx = 10, dy = dx, DX = 90, DY = 360, x, y, X, Y, precision = 2.5;
+ function graticule() {
+ return {
+ type: "MultiLineString",
+ coordinates: lines()
+ };
+ }
+ function lines() {
+ return d3.range(Math.ceil(X0 / DX) * DX, X1, DX).map(X).concat(d3.range(Math.ceil(Y0 / DY) * DY, Y1, DY).map(Y)).concat(d3.range(Math.ceil(x0 / dx) * dx, x1, dx).filter(function(x) {
+ return abs(x % DX) > ε;
+ }).map(x)).concat(d3.range(Math.ceil(y0 / dy) * dy, y1, dy).filter(function(y) {
+ return abs(y % DY) > ε;
+ }).map(y));
+ }
+ graticule.lines = function() {
+ return lines().map(function(coordinates) {
+ return {
+ type: "LineString",
+ coordinates: coordinates
+ };
+ });
+ };
+ graticule.outline = function() {
+ return {
+ type: "Polygon",
+ coordinates: [ X(X0).concat(Y(Y1).slice(1), X(X1).reverse().slice(1), Y(Y0).reverse().slice(1)) ]
+ };
+ };
+ graticule.extent = function(_) {
+ if (!arguments.length) return graticule.minorExtent();
+ return graticule.majorExtent(_).minorExtent(_);
+ };
+ graticule.majorExtent = function(_) {
+ if (!arguments.length) return [ [ X0, Y0 ], [ X1, Y1 ] ];
+ X0 = +_[0][0], X1 = +_[1][0];
+ Y0 = +_[0][1], Y1 = +_[1][1];
+ if (X0 > X1) _ = X0, X0 = X1, X1 = _;
+ if (Y0 > Y1) _ = Y0, Y0 = Y1, Y1 = _;
+ return graticule.precision(precision);
+ };
+ graticule.minorExtent = function(_) {
+ if (!arguments.length) return [ [ x0, y0 ], [ x1, y1 ] ];
+ x0 = +_[0][0], x1 = +_[1][0];
+ y0 = +_[0][1], y1 = +_[1][1];
+ if (x0 > x1) _ = x0, x0 = x1, x1 = _;
+ if (y0 > y1) _ = y0, y0 = y1, y1 = _;
+ return graticule.precision(precision);
+ };
+ graticule.step = function(_) {
+ if (!arguments.length) return graticule.minorStep();
+ return graticule.majorStep(_).minorStep(_);
+ };
+ graticule.majorStep = function(_) {
+ if (!arguments.length) return [ DX, DY ];
+ DX = +_[0], DY = +_[1];
+ return graticule;
+ };
+ graticule.minorStep = function(_) {
+ if (!arguments.length) return [ dx, dy ];
+ dx = +_[0], dy = +_[1];
+ return graticule;
+ };
+ graticule.precision = function(_) {
+ if (!arguments.length) return precision;
+ precision = +_;
+ x = d3_geo_graticuleX(y0, y1, 90);
+ y = d3_geo_graticuleY(x0, x1, precision);
+ X = d3_geo_graticuleX(Y0, Y1, 90);
+ Y = d3_geo_graticuleY(X0, X1, precision);
+ return graticule;
+ };
+ return graticule.majorExtent([ [ -180, -90 + ε ], [ 180, 90 - ε ] ]).minorExtent([ [ -180, -80 - ε ], [ 180, 80 + ε ] ]);
+ };
+ function d3_geo_graticuleX(y0, y1, dy) {
+ var y = d3.range(y0, y1 - ε, dy).concat(y1);
+ return function(x) {
+ return y.map(function(y) {
+ return [ x, y ];
+ });
+ };
+ }
+ function d3_geo_graticuleY(x0, x1, dx) {
+ var x = d3.range(x0, x1 - ε, dx).concat(x1);
+ return function(y) {
+ return x.map(function(x) {
+ return [ x, y ];
+ });
+ };
+ }
+ function d3_source(d) {
+ return d.source;
+ }
+ function d3_target(d) {
+ return d.target;
+ }
+ d3.geo.greatArc = function() {
+ var source = d3_source, source_, target = d3_target, target_;
+ function greatArc() {
+ return {
+ type: "LineString",
+ coordinates: [ source_ || source.apply(this, arguments), target_ || target.apply(this, arguments) ]
+ };
+ }
+ greatArc.distance = function() {
+ return d3.geo.distance(source_ || source.apply(this, arguments), target_ || target.apply(this, arguments));
+ };
+ greatArc.source = function(_) {
+ if (!arguments.length) return source;
+ source = _, source_ = typeof _ === "function" ? null : _;
+ return greatArc;
+ };
+ greatArc.target = function(_) {
+ if (!arguments.length) return target;
+ target = _, target_ = typeof _ === "function" ? null : _;
+ return greatArc;
+ };
+ greatArc.precision = function() {
+ return arguments.length ? greatArc : 0;
+ };
+ return greatArc;
+ };
+ d3.geo.interpolate = function(source, target) {
+ return d3_geo_interpolate(source[0] * d3_radians, source[1] * d3_radians, target[0] * d3_radians, target[1] * d3_radians);
+ };
+ function d3_geo_interpolate(x0, y0, x1, y1) {
+ var cy0 = Math.cos(y0), sy0 = Math.sin(y0), cy1 = Math.cos(y1), sy1 = Math.sin(y1), kx0 = cy0 * Math.cos(x0), ky0 = cy0 * Math.sin(x0), kx1 = cy1 * Math.cos(x1), ky1 = cy1 * Math.sin(x1), d = 2 * Math.asin(Math.sqrt(d3_haversin(y1 - y0) + cy0 * cy1 * d3_haversin(x1 - x0))), k = 1 / Math.sin(d);
+ var interpolate = d ? function(t) {
+ var B = Math.sin(t *= d) * k, A = Math.sin(d - t) * k, x = A * kx0 + B * kx1, y = A * ky0 + B * ky1, z = A * sy0 + B * sy1;
+ return [ Math.atan2(y, x) * d3_degrees, Math.atan2(z, Math.sqrt(x * x + y * y)) * d3_degrees ];
+ } : function() {
+ return [ x0 * d3_degrees, y0 * d3_degrees ];
+ };
+ interpolate.distance = d;
+ return interpolate;
+ }
+ d3.geo.length = function(object) {
+ d3_geo_lengthSum = 0;
+ d3.geo.stream(object, d3_geo_length);
+ return d3_geo_lengthSum;
+ };
+ var d3_geo_lengthSum;
+ var d3_geo_length = {
+ sphere: d3_noop,
+ point: d3_noop,
+ lineStart: d3_geo_lengthLineStart,
+ lineEnd: d3_noop,
+ polygonStart: d3_noop,
+ polygonEnd: d3_noop
+ };
+ function d3_geo_lengthLineStart() {
+ var λ0, sinφ0, cosφ0;
+ d3_geo_length.point = function(λ, φ) {
+ λ0 = λ * d3_radians, sinφ0 = Math.sin(φ *= d3_radians), cosφ0 = Math.cos(φ);
+ d3_geo_length.point = nextPoint;
+ };
+ d3_geo_length.lineEnd = function() {
+ d3_geo_length.point = d3_geo_length.lineEnd = d3_noop;
+ };
+ function nextPoint(λ, φ) {
+ var sinφ = Math.sin(φ *= d3_radians), cosφ = Math.cos(φ), t = abs((λ *= d3_radians) - λ0), cosΔλ = Math.cos(t);
+ d3_geo_lengthSum += Math.atan2(Math.sqrt((t = cosφ * Math.sin(t)) * t + (t = cosφ0 * sinφ - sinφ0 * cosφ * cosΔλ) * t), sinφ0 * sinφ + cosφ0 * cosφ * cosΔλ);
+ λ0 = λ, sinφ0 = sinφ, cosφ0 = cosφ;
+ }
+ }
+ function d3_geo_azimuthal(scale, angle) {
+ function azimuthal(λ, φ) {
+ var cosλ = Math.cos(λ), cosφ = Math.cos(φ), k = scale(cosλ * cosφ);
+ return [ k * cosφ * Math.sin(λ), k * Math.sin(φ) ];
+ }
+ azimuthal.invert = function(x, y) {
+ var Ï = Math.sqrt(x * x + y * y), c = angle(Ï), sinc = Math.sin(c), cosc = Math.cos(c);
+ return [ Math.atan2(x * sinc, Ï * cosc), Math.asin(Ï && y * sinc / Ï) ];
+ };
+ return azimuthal;
+ }
+ var d3_geo_azimuthalEqualArea = d3_geo_azimuthal(function(cosλcosφ) {
+ return Math.sqrt(2 / (1 + cosλcosφ));
+ }, function(Ï) {
+ return 2 * Math.asin(Ï / 2);
+ });
+ (d3.geo.azimuthalEqualArea = function() {
+ return d3_geo_projection(d3_geo_azimuthalEqualArea);
+ }).raw = d3_geo_azimuthalEqualArea;
+ var d3_geo_azimuthalEquidistant = d3_geo_azimuthal(function(cosλcosφ) {
+ var c = Math.acos(cosλcosφ);
+ return c && c / Math.sin(c);
+ }, d3_identity);
+ (d3.geo.azimuthalEquidistant = function() {
+ return d3_geo_projection(d3_geo_azimuthalEquidistant);
+ }).raw = d3_geo_azimuthalEquidistant;
+ function d3_geo_conicConformal(φ0, φ1) {
+ var cosφ0 = Math.cos(φ0), t = function(φ) {
+ return Math.tan(π / 4 + φ / 2);
+ }, n = φ0 === φ1 ? Math.sin(φ0) : Math.log(cosφ0 / Math.cos(φ1)) / Math.log(t(φ1) / t(φ0)), F = cosφ0 * Math.pow(t(φ0), n) / n;
+ if (!n) return d3_geo_mercator;
+ function forward(λ, φ) {
+ var Ï = abs(abs(φ) - halfÏ€) < ε ? 0 : F / Math.pow(t(φ), n);
+ return [ Ï * Math.sin(n * λ), F - Ï * Math.cos(n * λ) ];
+ }
+ forward.invert = function(x, y) {
+ var Ï0_y = F - y, Ï = d3_sgn(n) * Math.sqrt(x * x + Ï0_y * Ï0_y);
+ return [ Math.atan2(x, Ï0_y) / n, 2 * Math.atan(Math.pow(F / Ï, 1 / n)) - halfÏ€ ];
+ };
+ return forward;
+ }
+ (d3.geo.conicConformal = function() {
+ return d3_geo_conic(d3_geo_conicConformal);
+ }).raw = d3_geo_conicConformal;
+ function d3_geo_conicEquidistant(φ0, φ1) {
+ var cosφ0 = Math.cos(φ0), n = φ0 === φ1 ? Math.sin(φ0) : (cosφ0 - Math.cos(φ1)) / (φ1 - φ0), G = cosφ0 / n + φ0;
+ if (abs(n) < ε) return d3_geo_equirectangular;
+ function forward(λ, φ) {
+ var Ï = G - φ;
+ return [ Ï * Math.sin(n * λ), G - Ï * Math.cos(n * λ) ];
+ }
+ forward.invert = function(x, y) {
+ var Ï0_y = G - y;
+ return [ Math.atan2(x, Ï0_y) / n, G - d3_sgn(n) * Math.sqrt(x * x + Ï0_y * Ï0_y) ];
+ };
+ return forward;
+ }
+ (d3.geo.conicEquidistant = function() {
+ return d3_geo_conic(d3_geo_conicEquidistant);
+ }).raw = d3_geo_conicEquidistant;
+ var d3_geo_gnomonic = d3_geo_azimuthal(function(cosλcosφ) {
+ return 1 / cosλcosφ;
+ }, Math.atan);
+ (d3.geo.gnomonic = function() {
+ return d3_geo_projection(d3_geo_gnomonic);
+ }).raw = d3_geo_gnomonic;
+ function d3_geo_mercator(λ, φ) {
+ return [ λ, Math.log(Math.tan(π / 4 + φ / 2)) ];
+ }
+ d3_geo_mercator.invert = function(x, y) {
+ return [ x, 2 * Math.atan(Math.exp(y)) - halfπ ];
+ };
+ function d3_geo_mercatorProjection(project) {
+ var m = d3_geo_projection(project), scale = m.scale, translate = m.translate, clipExtent = m.clipExtent, clipAuto;
+ m.scale = function() {
+ var v = scale.apply(m, arguments);
+ return v === m ? clipAuto ? m.clipExtent(null) : m : v;
+ };
+ m.translate = function() {
+ var v = translate.apply(m, arguments);
+ return v === m ? clipAuto ? m.clipExtent(null) : m : v;
+ };
+ m.clipExtent = function(_) {
+ var v = clipExtent.apply(m, arguments);
+ if (v === m) {
+ if (clipAuto = _ == null) {
+ var k = π * scale(), t = translate();
+ clipExtent([ [ t[0] - k, t[1] - k ], [ t[0] + k, t[1] + k ] ]);
+ }
+ } else if (clipAuto) {
+ v = null;
+ }
+ return v;
+ };
+ return m.clipExtent(null);
+ }
+ (d3.geo.mercator = function() {
+ return d3_geo_mercatorProjection(d3_geo_mercator);
+ }).raw = d3_geo_mercator;
+ var d3_geo_orthographic = d3_geo_azimuthal(function() {
+ return 1;
+ }, Math.asin);
+ (d3.geo.orthographic = function() {
+ return d3_geo_projection(d3_geo_orthographic);
+ }).raw = d3_geo_orthographic;
+ var d3_geo_stereographic = d3_geo_azimuthal(function(cosλcosφ) {
+ return 1 / (1 + cosλcosφ);
+ }, function(Ï) {
+ return 2 * Math.atan(Ï);
+ });
+ (d3.geo.stereographic = function() {
+ return d3_geo_projection(d3_geo_stereographic);
+ }).raw = d3_geo_stereographic;
+ function d3_geo_transverseMercator(λ, φ) {
+ return [ Math.log(Math.tan(π / 4 + φ / 2)), -λ ];
+ }
+ d3_geo_transverseMercator.invert = function(x, y) {
+ return [ -y, 2 * Math.atan(Math.exp(x)) - halfπ ];
+ };
+ (d3.geo.transverseMercator = function() {
+ var projection = d3_geo_mercatorProjection(d3_geo_transverseMercator), center = projection.center, rotate = projection.rotate;
+ projection.center = function(_) {
+ return _ ? center([ -_[1], _[0] ]) : (_ = center(), [ -_[1], _[0] ]);
+ };
+ projection.rotate = function(_) {
+ return _ ? rotate([ _[0], _[1], _.length > 2 ? _[2] + 90 : 90 ]) : (_ = rotate(),
+ [ _[0], _[1], _[2] - 90 ]);
+ };
+ return projection.rotate([ 0, 0 ]);
+ }).raw = d3_geo_transverseMercator;
+ d3.geom = {};
+ function d3_geom_pointX(d) {
+ return d[0];
+ }
+ function d3_geom_pointY(d) {
+ return d[1];
+ }
+ d3.geom.hull = function(vertices) {
+ var x = d3_geom_pointX, y = d3_geom_pointY;
+ if (arguments.length) return hull(vertices);
+ function hull(data) {
+ if (data.length < 3) return [];
+ var fx = d3_functor(x), fy = d3_functor(y), i, n = data.length, points = [], flippedPoints = [];
+ for (i = 0; i < n; i++) {
+ points.push([ +fx.call(this, data[i], i), +fy.call(this, data[i], i), i ]);
+ }
+ points.sort(d3_geom_hullOrder);
+ for (i = 0; i < n; i++) flippedPoints.push([ points[i][0], -points[i][1] ]);
+ var upper = d3_geom_hullUpper(points), lower = d3_geom_hullUpper(flippedPoints);
+ var skipLeft = lower[0] === upper[0], skipRight = lower[lower.length - 1] === upper[upper.length - 1], polygon = [];
+ for (i = upper.length - 1; i >= 0; --i) polygon.push(data[points[upper[i]][2]]);
+ for (i = +skipLeft; i < lower.length - skipRight; ++i) polygon.push(data[points[lower[i]][2]]);
+ return polygon;
+ }
+ hull.x = function(_) {
+ return arguments.length ? (x = _, hull) : x;
+ };
+ hull.y = function(_) {
+ return arguments.length ? (y = _, hull) : y;
+ };
+ return hull;
+ };
+ function d3_geom_hullUpper(points) {
+ var n = points.length, hull = [ 0, 1 ], hs = 2;
+ for (var i = 2; i < n; i++) {
+ while (hs > 1 && d3_cross2d(points[hull[hs - 2]], points[hull[hs - 1]], points[i]) <= 0) --hs;
+ hull[hs++] = i;
+ }
+ return hull.slice(0, hs);
+ }
+ function d3_geom_hullOrder(a, b) {
+ return a[0] - b[0] || a[1] - b[1];
+ }
+ d3.geom.polygon = function(coordinates) {
+ d3_subclass(coordinates, d3_geom_polygonPrototype);
+ return coordinates;
+ };
+ var d3_geom_polygonPrototype = d3.geom.polygon.prototype = [];
+ d3_geom_polygonPrototype.area = function() {
+ var i = -1, n = this.length, a, b = this[n - 1], area = 0;
+ while (++i < n) {
+ a = b;
+ b = this[i];
+ area += a[1] * b[0] - a[0] * b[1];
+ }
+ return area * .5;
+ };
+ d3_geom_polygonPrototype.centroid = function(k) {
+ var i = -1, n = this.length, x = 0, y = 0, a, b = this[n - 1], c;
+ if (!arguments.length) k = -1 / (6 * this.area());
+ while (++i < n) {
+ a = b;
+ b = this[i];
+ c = a[0] * b[1] - b[0] * a[1];
+ x += (a[0] + b[0]) * c;
+ y += (a[1] + b[1]) * c;
+ }
+ return [ x * k, y * k ];
+ };
+ d3_geom_polygonPrototype.clip = function(subject) {
+ var input, closed = d3_geom_polygonClosed(subject), i = -1, n = this.length - d3_geom_polygonClosed(this), j, m, a = this[n - 1], b, c, d;
+ while (++i < n) {
+ input = subject.slice();
+ subject.length = 0;
+ b = this[i];
+ c = input[(m = input.length - closed) - 1];
+ j = -1;
+ while (++j < m) {
+ d = input[j];
+ if (d3_geom_polygonInside(d, a, b)) {
+ if (!d3_geom_polygonInside(c, a, b)) {
+ subject.push(d3_geom_polygonIntersect(c, d, a, b));
+ }
+ subject.push(d);
+ } else if (d3_geom_polygonInside(c, a, b)) {
+ subject.push(d3_geom_polygonIntersect(c, d, a, b));
+ }
+ c = d;
+ }
+ if (closed) subject.push(subject[0]);
+ a = b;
+ }
+ return subject;
+ };
+ function d3_geom_polygonInside(p, a, b) {
+ return (b[0] - a[0]) * (p[1] - a[1]) < (b[1] - a[1]) * (p[0] - a[0]);
+ }
+ function d3_geom_polygonIntersect(c, d, a, b) {
+ var x1 = c[0], x3 = a[0], x21 = d[0] - x1, x43 = b[0] - x3, y1 = c[1], y3 = a[1], y21 = d[1] - y1, y43 = b[1] - y3, ua = (x43 * (y1 - y3) - y43 * (x1 - x3)) / (y43 * x21 - x43 * y21);
+ return [ x1 + ua * x21, y1 + ua * y21 ];
+ }
+ function d3_geom_polygonClosed(coordinates) {
+ var a = coordinates[0], b = coordinates[coordinates.length - 1];
+ return !(a[0] - b[0] || a[1] - b[1]);
+ }
+ var d3_geom_voronoiEdges, d3_geom_voronoiCells, d3_geom_voronoiBeaches, d3_geom_voronoiBeachPool = [], d3_geom_voronoiFirstCircle, d3_geom_voronoiCircles, d3_geom_voronoiCirclePool = [];
+ function d3_geom_voronoiBeach() {
+ d3_geom_voronoiRedBlackNode(this);
+ this.edge = this.site = this.circle = null;
+ }
+ function d3_geom_voronoiCreateBeach(site) {
+ var beach = d3_geom_voronoiBeachPool.pop() || new d3_geom_voronoiBeach();
+ beach.site = site;
+ return beach;
+ }
+ function d3_geom_voronoiDetachBeach(beach) {
+ d3_geom_voronoiDetachCircle(beach);
+ d3_geom_voronoiBeaches.remove(beach);
+ d3_geom_voronoiBeachPool.push(beach);
+ d3_geom_voronoiRedBlackNode(beach);
+ }
+ function d3_geom_voronoiRemoveBeach(beach) {
+ var circle = beach.circle, x = circle.x, y = circle.cy, vertex = {
+ x: x,
+ y: y
+ }, previous = beach.P, next = beach.N, disappearing = [ beach ];
+ d3_geom_voronoiDetachBeach(beach);
+ var lArc = previous;
+ while (lArc.circle && abs(x - lArc.circle.x) < ε && abs(y - lArc.circle.cy) < ε) {
+ previous = lArc.P;
+ disappearing.unshift(lArc);
+ d3_geom_voronoiDetachBeach(lArc);
+ lArc = previous;
+ }
+ disappearing.unshift(lArc);
+ d3_geom_voronoiDetachCircle(lArc);
+ var rArc = next;
+ while (rArc.circle && abs(x - rArc.circle.x) < ε && abs(y - rArc.circle.cy) < ε) {
+ next = rArc.N;
+ disappearing.push(rArc);
+ d3_geom_voronoiDetachBeach(rArc);
+ rArc = next;
+ }
+ disappearing.push(rArc);
+ d3_geom_voronoiDetachCircle(rArc);
+ var nArcs = disappearing.length, iArc;
+ for (iArc = 1; iArc < nArcs; ++iArc) {
+ rArc = disappearing[iArc];
+ lArc = disappearing[iArc - 1];
+ d3_geom_voronoiSetEdgeEnd(rArc.edge, lArc.site, rArc.site, vertex);
+ }
+ lArc = disappearing[0];
+ rArc = disappearing[nArcs - 1];
+ rArc.edge = d3_geom_voronoiCreateEdge(lArc.site, rArc.site, null, vertex);
+ d3_geom_voronoiAttachCircle(lArc);
+ d3_geom_voronoiAttachCircle(rArc);
+ }
+ function d3_geom_voronoiAddBeach(site) {
+ var x = site.x, directrix = site.y, lArc, rArc, dxl, dxr, node = d3_geom_voronoiBeaches._;
+ while (node) {
+ dxl = d3_geom_voronoiLeftBreakPoint(node, directrix) - x;
+ if (dxl > ε) node = node.L; else {
+ dxr = x - d3_geom_voronoiRightBreakPoint(node, directrix);
+ if (dxr > ε) {
+ if (!node.R) {
+ lArc = node;
+ break;
+ }
+ node = node.R;
+ } else {
+ if (dxl > -ε) {
+ lArc = node.P;
+ rArc = node;
+ } else if (dxr > -ε) {
+ lArc = node;
+ rArc = node.N;
+ } else {
+ lArc = rArc = node;
+ }
+ break;
+ }
+ }
+ }
+ var newArc = d3_geom_voronoiCreateBeach(site);
+ d3_geom_voronoiBeaches.insert(lArc, newArc);
+ if (!lArc && !rArc) return;
+ if (lArc === rArc) {
+ d3_geom_voronoiDetachCircle(lArc);
+ rArc = d3_geom_voronoiCreateBeach(lArc.site);
+ d3_geom_voronoiBeaches.insert(newArc, rArc);
+ newArc.edge = rArc.edge = d3_geom_voronoiCreateEdge(lArc.site, newArc.site);
+ d3_geom_voronoiAttachCircle(lArc);
+ d3_geom_voronoiAttachCircle(rArc);
+ return;
+ }
+ if (!rArc) {
+ newArc.edge = d3_geom_voronoiCreateEdge(lArc.site, newArc.site);
+ return;
+ }
+ d3_geom_voronoiDetachCircle(lArc);
+ d3_geom_voronoiDetachCircle(rArc);
+ var lSite = lArc.site, ax = lSite.x, ay = lSite.y, bx = site.x - ax, by = site.y - ay, rSite = rArc.site, cx = rSite.x - ax, cy = rSite.y - ay, d = 2 * (bx * cy - by * cx), hb = bx * bx + by * by, hc = cx * cx + cy * cy, vertex = {
+ x: (cy * hb - by * hc) / d + ax,
+ y: (bx * hc - cx * hb) / d + ay
+ };
+ d3_geom_voronoiSetEdgeEnd(rArc.edge, lSite, rSite, vertex);
+ newArc.edge = d3_geom_voronoiCreateEdge(lSite, site, null, vertex);
+ rArc.edge = d3_geom_voronoiCreateEdge(site, rSite, null, vertex);
+ d3_geom_voronoiAttachCircle(lArc);
+ d3_geom_voronoiAttachCircle(rArc);
+ }
+ function d3_geom_voronoiLeftBreakPoint(arc, directrix) {
+ var site = arc.site, rfocx = site.x, rfocy = site.y, pby2 = rfocy - directrix;
+ if (!pby2) return rfocx;
+ var lArc = arc.P;
+ if (!lArc) return -Infinity;
+ site = lArc.site;
+ var lfocx = site.x, lfocy = site.y, plby2 = lfocy - directrix;
+ if (!plby2) return lfocx;
+ var hl = lfocx - rfocx, aby2 = 1 / pby2 - 1 / plby2, b = hl / plby2;
+ if (aby2) return (-b + Math.sqrt(b * b - 2 * aby2 * (hl * hl / (-2 * plby2) - lfocy + plby2 / 2 + rfocy - pby2 / 2))) / aby2 + rfocx;
+ return (rfocx + lfocx) / 2;
+ }
+ function d3_geom_voronoiRightBreakPoint(arc, directrix) {
+ var rArc = arc.N;
+ if (rArc) return d3_geom_voronoiLeftBreakPoint(rArc, directrix);
+ var site = arc.site;
+ return site.y === directrix ? site.x : Infinity;
+ }
+ function d3_geom_voronoiCell(site) {
+ this.site = site;
+ this.edges = [];
+ }
+ d3_geom_voronoiCell.prototype.prepare = function() {
+ var halfEdges = this.edges, iHalfEdge = halfEdges.length, edge;
+ while (iHalfEdge--) {
+ edge = halfEdges[iHalfEdge].edge;
+ if (!edge.b || !edge.a) halfEdges.splice(iHalfEdge, 1);
+ }
+ halfEdges.sort(d3_geom_voronoiHalfEdgeOrder);
+ return halfEdges.length;
+ };
+ function d3_geom_voronoiCloseCells(extent) {
+ var x0 = extent[0][0], x1 = extent[1][0], y0 = extent[0][1], y1 = extent[1][1], x2, y2, x3, y3, cells = d3_geom_voronoiCells, iCell = cells.length, cell, iHalfEdge, halfEdges, nHalfEdges, start, end;
+ while (iCell--) {
+ cell = cells[iCell];
+ if (!cell || !cell.prepare()) continue;
+ halfEdges = cell.edges;
+ nHalfEdges = halfEdges.length;
+ iHalfEdge = 0;
+ while (iHalfEdge < nHalfEdges) {
+ end = halfEdges[iHalfEdge].end(), x3 = end.x, y3 = end.y;
+ start = halfEdges[++iHalfEdge % nHalfEdges].start(), x2 = start.x, y2 = start.y;
+ if (abs(x3 - x2) > ε || abs(y3 - y2) > ε) {
+ halfEdges.splice(iHalfEdge, 0, new d3_geom_voronoiHalfEdge(d3_geom_voronoiCreateBorderEdge(cell.site, end, abs(x3 - x0) < ε && y1 - y3 > ε ? {
+ x: x0,
+ y: abs(x2 - x0) < ε ? y2 : y1
+ } : abs(y3 - y1) < ε && x1 - x3 > ε ? {
+ x: abs(y2 - y1) < ε ? x2 : x1,
+ y: y1
+ } : abs(x3 - x1) < ε && y3 - y0 > ε ? {
+ x: x1,
+ y: abs(x2 - x1) < ε ? y2 : y0
+ } : abs(y3 - y0) < ε && x3 - x0 > ε ? {
+ x: abs(y2 - y0) < ε ? x2 : x0,
+ y: y0
+ } : null), cell.site, null));
+ ++nHalfEdges;
+ }
+ }
+ }
+ }
+ function d3_geom_voronoiHalfEdgeOrder(a, b) {
+ return b.angle - a.angle;
+ }
+ function d3_geom_voronoiCircle() {
+ d3_geom_voronoiRedBlackNode(this);
+ this.x = this.y = this.arc = this.site = this.cy = null;
+ }
+ function d3_geom_voronoiAttachCircle(arc) {
+ var lArc = arc.P, rArc = arc.N;
+ if (!lArc || !rArc) return;
+ var lSite = lArc.site, cSite = arc.site, rSite = rArc.site;
+ if (lSite === rSite) return;
+ var bx = cSite.x, by = cSite.y, ax = lSite.x - bx, ay = lSite.y - by, cx = rSite.x - bx, cy = rSite.y - by;
+ var d = 2 * (ax * cy - ay * cx);
+ if (d >= -ε2) return;
+ var ha = ax * ax + ay * ay, hc = cx * cx + cy * cy, x = (cy * ha - ay * hc) / d, y = (ax * hc - cx * ha) / d, cy = y + by;
+ var circle = d3_geom_voronoiCirclePool.pop() || new d3_geom_voronoiCircle();
+ circle.arc = arc;
+ circle.site = cSite;
+ circle.x = x + bx;
+ circle.y = cy + Math.sqrt(x * x + y * y);
+ circle.cy = cy;
+ arc.circle = circle;
+ var before = null, node = d3_geom_voronoiCircles._;
+ while (node) {
+ if (circle.y < node.y || circle.y === node.y && circle.x <= node.x) {
+ if (node.L) node = node.L; else {
+ before = node.P;
+ break;
+ }
+ } else {
+ if (node.R) node = node.R; else {
+ before = node;
+ break;
+ }
+ }
+ }
+ d3_geom_voronoiCircles.insert(before, circle);
+ if (!before) d3_geom_voronoiFirstCircle = circle;
+ }
+ function d3_geom_voronoiDetachCircle(arc) {
+ var circle = arc.circle;
+ if (circle) {
+ if (!circle.P) d3_geom_voronoiFirstCircle = circle.N;
+ d3_geom_voronoiCircles.remove(circle);
+ d3_geom_voronoiCirclePool.push(circle);
+ d3_geom_voronoiRedBlackNode(circle);
+ arc.circle = null;
+ }
+ }
+ function d3_geom_voronoiClipEdges(extent) {
+ var edges = d3_geom_voronoiEdges, clip = d3_geom_clipLine(extent[0][0], extent[0][1], extent[1][0], extent[1][1]), i = edges.length, e;
+ while (i--) {
+ e = edges[i];
+ if (!d3_geom_voronoiConnectEdge(e, extent) || !clip(e) || abs(e.a.x - e.b.x) < ε && abs(e.a.y - e.b.y) < ε) {
+ e.a = e.b = null;
+ edges.splice(i, 1);
+ }
+ }
+ }
+ function d3_geom_voronoiConnectEdge(edge, extent) {
+ var vb = edge.b;
+ if (vb) return true;
+ var va = edge.a, x0 = extent[0][0], x1 = extent[1][0], y0 = extent[0][1], y1 = extent[1][1], lSite = edge.l, rSite = edge.r, lx = lSite.x, ly = lSite.y, rx = rSite.x, ry = rSite.y, fx = (lx + rx) / 2, fy = (ly + ry) / 2, fm, fb;
+ if (ry === ly) {
+ if (fx < x0 || fx >= x1) return;
+ if (lx > rx) {
+ if (!va) va = {
+ x: fx,
+ y: y0
+ }; else if (va.y >= y1) return;
+ vb = {
+ x: fx,
+ y: y1
+ };
+ } else {
+ if (!va) va = {
+ x: fx,
+ y: y1
+ }; else if (va.y < y0) return;
+ vb = {
+ x: fx,
+ y: y0
+ };
+ }
+ } else {
+ fm = (lx - rx) / (ry - ly);
+ fb = fy - fm * fx;
+ if (fm < -1 || fm > 1) {
+ if (lx > rx) {
+ if (!va) va = {
+ x: (y0 - fb) / fm,
+ y: y0
+ }; else if (va.y >= y1) return;
+ vb = {
+ x: (y1 - fb) / fm,
+ y: y1
+ };
+ } else {
+ if (!va) va = {
+ x: (y1 - fb) / fm,
+ y: y1
+ }; else if (va.y < y0) return;
+ vb = {
+ x: (y0 - fb) / fm,
+ y: y0
+ };
+ }
+ } else {
+ if (ly < ry) {
+ if (!va) va = {
+ x: x0,
+ y: fm * x0 + fb
+ }; else if (va.x >= x1) return;
+ vb = {
+ x: x1,
+ y: fm * x1 + fb
+ };
+ } else {
+ if (!va) va = {
+ x: x1,
+ y: fm * x1 + fb
+ }; else if (va.x < x0) return;
+ vb = {
+ x: x0,
+ y: fm * x0 + fb
+ };
+ }
+ }
+ }
+ edge.a = va;
+ edge.b = vb;
+ return true;
+ }
+ function d3_geom_voronoiEdge(lSite, rSite) {
+ this.l = lSite;
+ this.r = rSite;
+ this.a = this.b = null;
+ }
+ function d3_geom_voronoiCreateEdge(lSite, rSite, va, vb) {
+ var edge = new d3_geom_voronoiEdge(lSite, rSite);
+ d3_geom_voronoiEdges.push(edge);
+ if (va) d3_geom_voronoiSetEdgeEnd(edge, lSite, rSite, va);
+ if (vb) d3_geom_voronoiSetEdgeEnd(edge, rSite, lSite, vb);
+ d3_geom_voronoiCells[lSite.i].edges.push(new d3_geom_voronoiHalfEdge(edge, lSite, rSite));
+ d3_geom_voronoiCells[rSite.i].edges.push(new d3_geom_voronoiHalfEdge(edge, rSite, lSite));
+ return edge;
+ }
+ function d3_geom_voronoiCreateBorderEdge(lSite, va, vb) {
+ var edge = new d3_geom_voronoiEdge(lSite, null);
+ edge.a = va;
+ edge.b = vb;
+ d3_geom_voronoiEdges.push(edge);
+ return edge;
+ }
+ function d3_geom_voronoiSetEdgeEnd(edge, lSite, rSite, vertex) {
+ if (!edge.a && !edge.b) {
+ edge.a = vertex;
+ edge.l = lSite;
+ edge.r = rSite;
+ } else if (edge.l === rSite) {
+ edge.b = vertex;
+ } else {
+ edge.a = vertex;
+ }
+ }
+ function d3_geom_voronoiHalfEdge(edge, lSite, rSite) {
+ var va = edge.a, vb = edge.b;
+ this.edge = edge;
+ this.site = lSite;
+ this.angle = rSite ? Math.atan2(rSite.y - lSite.y, rSite.x - lSite.x) : edge.l === lSite ? Math.atan2(vb.x - va.x, va.y - vb.y) : Math.atan2(va.x - vb.x, vb.y - va.y);
+ }
+ d3_geom_voronoiHalfEdge.prototype = {
+ start: function() {
+ return this.edge.l === this.site ? this.edge.a : this.edge.b;
+ },
+ end: function() {
+ return this.edge.l === this.site ? this.edge.b : this.edge.a;
+ }
+ };
+ function d3_geom_voronoiRedBlackTree() {
+ this._ = null;
+ }
+ function d3_geom_voronoiRedBlackNode(node) {
+ node.U = node.C = node.L = node.R = node.P = node.N = null;
+ }
+ d3_geom_voronoiRedBlackTree.prototype = {
+ insert: function(after, node) {
+ var parent, grandpa, uncle;
+ if (after) {
+ node.P = after;
+ node.N = after.N;
+ if (after.N) after.N.P = node;
+ after.N = node;
+ if (after.R) {
+ after = after.R;
+ while (after.L) after = after.L;
+ after.L = node;
+ } else {
+ after.R = node;
+ }
+ parent = after;
+ } else if (this._) {
+ after = d3_geom_voronoiRedBlackFirst(this._);
+ node.P = null;
+ node.N = after;
+ after.P = after.L = node;
+ parent = after;
+ } else {
+ node.P = node.N = null;
+ this._ = node;
+ parent = null;
+ }
+ node.L = node.R = null;
+ node.U = parent;
+ node.C = true;
+ after = node;
+ while (parent && parent.C) {
+ grandpa = parent.U;
+ if (parent === grandpa.L) {
+ uncle = grandpa.R;
+ if (uncle && uncle.C) {
+ parent.C = uncle.C = false;
+ grandpa.C = true;
+ after = grandpa;
+ } else {
+ if (after === parent.R) {
+ d3_geom_voronoiRedBlackRotateLeft(this, parent);
+ after = parent;
+ parent = after.U;
+ }
+ parent.C = false;
+ grandpa.C = true;
+ d3_geom_voronoiRedBlackRotateRight(this, grandpa);
+ }
+ } else {
+ uncle = grandpa.L;
+ if (uncle && uncle.C) {
+ parent.C = uncle.C = false;
+ grandpa.C = true;
+ after = grandpa;
+ } else {
+ if (after === parent.L) {
+ d3_geom_voronoiRedBlackRotateRight(this, parent);
+ after = parent;
+ parent = after.U;
+ }
+ parent.C = false;
+ grandpa.C = true;
+ d3_geom_voronoiRedBlackRotateLeft(this, grandpa);
+ }
+ }
+ parent = after.U;
+ }
+ this._.C = false;
+ },
+ remove: function(node) {
+ if (node.N) node.N.P = node.P;
+ if (node.P) node.P.N = node.N;
+ node.N = node.P = null;
+ var parent = node.U, sibling, left = node.L, right = node.R, next, red;
+ if (!left) next = right; else if (!right) next = left; else next = d3_geom_voronoiRedBlackFirst(right);
+ if (parent) {
+ if (parent.L === node) parent.L = next; else parent.R = next;
+ } else {
+ this._ = next;
+ }
+ if (left && right) {
+ red = next.C;
+ next.C = node.C;
+ next.L = left;
+ left.U = next;
+ if (next !== right) {
+ parent = next.U;
+ next.U = node.U;
+ node = next.R;
+ parent.L = node;
+ next.R = right;
+ right.U = next;
+ } else {
+ next.U = parent;
+ parent = next;
+ node = next.R;
+ }
+ } else {
+ red = node.C;
+ node = next;
+ }
+ if (node) node.U = parent;
+ if (red) return;
+ if (node && node.C) {
+ node.C = false;
+ return;
+ }
+ do {
+ if (node === this._) break;
+ if (node === parent.L) {
+ sibling = parent.R;
+ if (sibling.C) {
+ sibling.C = false;
+ parent.C = true;
+ d3_geom_voronoiRedBlackRotateLeft(this, parent);
+ sibling = parent.R;
+ }
+ if (sibling.L && sibling.L.C || sibling.R && sibling.R.C) {
+ if (!sibling.R || !sibling.R.C) {
+ sibling.L.C = false;
+ sibling.C = true;
+ d3_geom_voronoiRedBlackRotateRight(this, sibling);
+ sibling = parent.R;
+ }
+ sibling.C = parent.C;
+ parent.C = sibling.R.C = false;
+ d3_geom_voronoiRedBlackRotateLeft(this, parent);
+ node = this._;
+ break;
+ }
+ } else {
+ sibling = parent.L;
+ if (sibling.C) {
+ sibling.C = false;
+ parent.C = true;
+ d3_geom_voronoiRedBlackRotateRight(this, parent);
+ sibling = parent.L;
+ }
+ if (sibling.L && sibling.L.C || sibling.R && sibling.R.C) {
+ if (!sibling.L || !sibling.L.C) {
+ sibling.R.C = false;
+ sibling.C = true;
+ d3_geom_voronoiRedBlackRotateLeft(this, sibling);
+ sibling = parent.L;
+ }
+ sibling.C = parent.C;
+ parent.C = sibling.L.C = false;
+ d3_geom_voronoiRedBlackRotateRight(this, parent);
+ node = this._;
+ break;
+ }
+ }
+ sibling.C = true;
+ node = parent;
+ parent = parent.U;
+ } while (!node.C);
+ if (node) node.C = false;
+ }
+ };
+ function d3_geom_voronoiRedBlackRotateLeft(tree, node) {
+ var p = node, q = node.R, parent = p.U;
+ if (parent) {
+ if (parent.L === p) parent.L = q; else parent.R = q;
+ } else {
+ tree._ = q;
+ }
+ q.U = parent;
+ p.U = q;
+ p.R = q.L;
+ if (p.R) p.R.U = p;
+ q.L = p;
+ }
+ function d3_geom_voronoiRedBlackRotateRight(tree, node) {
+ var p = node, q = node.L, parent = p.U;
+ if (parent) {
+ if (parent.L === p) parent.L = q; else parent.R = q;
+ } else {
+ tree._ = q;
+ }
+ q.U = parent;
+ p.U = q;
+ p.L = q.R;
+ if (p.L) p.L.U = p;
+ q.R = p;
+ }
+ function d3_geom_voronoiRedBlackFirst(node) {
+ while (node.L) node = node.L;
+ return node;
+ }
+ function d3_geom_voronoi(sites, bbox) {
+ var site = sites.sort(d3_geom_voronoiVertexOrder).pop(), x0, y0, circle;
+ d3_geom_voronoiEdges = [];
+ d3_geom_voronoiCells = new Array(sites.length);
+ d3_geom_voronoiBeaches = new d3_geom_voronoiRedBlackTree();
+ d3_geom_voronoiCircles = new d3_geom_voronoiRedBlackTree();
+ while (true) {
+ circle = d3_geom_voronoiFirstCircle;
+ if (site && (!circle || site.y < circle.y || site.y === circle.y && site.x < circle.x)) {
+ if (site.x !== x0 || site.y !== y0) {
+ d3_geom_voronoiCells[site.i] = new d3_geom_voronoiCell(site);
+ d3_geom_voronoiAddBeach(site);
+ x0 = site.x, y0 = site.y;
+ }
+ site = sites.pop();
+ } else if (circle) {
+ d3_geom_voronoiRemoveBeach(circle.arc);
+ } else {
+ break;
+ }
+ }
+ if (bbox) d3_geom_voronoiClipEdges(bbox), d3_geom_voronoiCloseCells(bbox);
+ var diagram = {
+ cells: d3_geom_voronoiCells,
+ edges: d3_geom_voronoiEdges
+ };
+ d3_geom_voronoiBeaches = d3_geom_voronoiCircles = d3_geom_voronoiEdges = d3_geom_voronoiCells = null;
+ return diagram;
+ }
+ function d3_geom_voronoiVertexOrder(a, b) {
+ return b.y - a.y || b.x - a.x;
+ }
+ d3.geom.voronoi = function(points) {
+ var x = d3_geom_pointX, y = d3_geom_pointY, fx = x, fy = y, clipExtent = d3_geom_voronoiClipExtent;
+ if (points) return voronoi(points);
+ function voronoi(data) {
+ var polygons = new Array(data.length), x0 = clipExtent[0][0], y0 = clipExtent[0][1], x1 = clipExtent[1][0], y1 = clipExtent[1][1];
+ d3_geom_voronoi(sites(data), clipExtent).cells.forEach(function(cell, i) {
+ var edges = cell.edges, site = cell.site, polygon = polygons[i] = edges.length ? edges.map(function(e) {
+ var s = e.start();
+ return [ s.x, s.y ];
+ }) : site.x >= x0 && site.x <= x1 && site.y >= y0 && site.y <= y1 ? [ [ x0, y1 ], [ x1, y1 ], [ x1, y0 ], [ x0, y0 ] ] : [];
+ polygon.point = data[i];
+ });
+ return polygons;
+ }
+ function sites(data) {
+ return data.map(function(d, i) {
+ return {
+ x: Math.round(fx(d, i) / ε) * ε,
+ y: Math.round(fy(d, i) / ε) * ε,
+ i: i
+ };
+ });
+ }
+ voronoi.links = function(data) {
+ return d3_geom_voronoi(sites(data)).edges.filter(function(edge) {
+ return edge.l && edge.r;
+ }).map(function(edge) {
+ return {
+ source: data[edge.l.i],
+ target: data[edge.r.i]
+ };
+ });
+ };
+ voronoi.triangles = function(data) {
+ var triangles = [];
+ d3_geom_voronoi(sites(data)).cells.forEach(function(cell, i) {
+ var site = cell.site, edges = cell.edges.sort(d3_geom_voronoiHalfEdgeOrder), j = -1, m = edges.length, e0, s0, e1 = edges[m - 1].edge, s1 = e1.l === site ? e1.r : e1.l;
+ while (++j < m) {
+ e0 = e1;
+ s0 = s1;
+ e1 = edges[j].edge;
+ s1 = e1.l === site ? e1.r : e1.l;
+ if (i < s0.i && i < s1.i && d3_geom_voronoiTriangleArea(site, s0, s1) < 0) {
+ triangles.push([ data[i], data[s0.i], data[s1.i] ]);
+ }
+ }
+ });
+ return triangles;
+ };
+ voronoi.x = function(_) {
+ return arguments.length ? (fx = d3_functor(x = _), voronoi) : x;
+ };
+ voronoi.y = function(_) {
+ return arguments.length ? (fy = d3_functor(y = _), voronoi) : y;
+ };
+ voronoi.clipExtent = function(_) {
+ if (!arguments.length) return clipExtent === d3_geom_voronoiClipExtent ? null : clipExtent;
+ clipExtent = _ == null ? d3_geom_voronoiClipExtent : _;
+ return voronoi;
+ };
+ voronoi.size = function(_) {
+ if (!arguments.length) return clipExtent === d3_geom_voronoiClipExtent ? null : clipExtent && clipExtent[1];
+ return voronoi.clipExtent(_ && [ [ 0, 0 ], _ ]);
+ };
+ return voronoi;
+ };
+ var d3_geom_voronoiClipExtent = [ [ -1e6, -1e6 ], [ 1e6, 1e6 ] ];
+ function d3_geom_voronoiTriangleArea(a, b, c) {
+ return (a.x - c.x) * (b.y - a.y) - (a.x - b.x) * (c.y - a.y);
+ }
+ d3.geom.delaunay = function(vertices) {
+ return d3.geom.voronoi().triangles(vertices);
+ };
+ d3.geom.quadtree = function(points, x1, y1, x2, y2) {
+ var x = d3_geom_pointX, y = d3_geom_pointY, compat;
+ if (compat = arguments.length) {
+ x = d3_geom_quadtreeCompatX;
+ y = d3_geom_quadtreeCompatY;
+ if (compat === 3) {
+ y2 = y1;
+ x2 = x1;
+ y1 = x1 = 0;
+ }
+ return quadtree(points);
+ }
+ function quadtree(data) {
+ var d, fx = d3_functor(x), fy = d3_functor(y), xs, ys, i, n, x1_, y1_, x2_, y2_;
+ if (x1 != null) {
+ x1_ = x1, y1_ = y1, x2_ = x2, y2_ = y2;
+ } else {
+ x2_ = y2_ = -(x1_ = y1_ = Infinity);
+ xs = [], ys = [];
+ n = data.length;
+ if (compat) for (i = 0; i < n; ++i) {
+ d = data[i];
+ if (d.x < x1_) x1_ = d.x;
+ if (d.y < y1_) y1_ = d.y;
+ if (d.x > x2_) x2_ = d.x;
+ if (d.y > y2_) y2_ = d.y;
+ xs.push(d.x);
+ ys.push(d.y);
+ } else for (i = 0; i < n; ++i) {
+ var x_ = +fx(d = data[i], i), y_ = +fy(d, i);
+ if (x_ < x1_) x1_ = x_;
+ if (y_ < y1_) y1_ = y_;
+ if (x_ > x2_) x2_ = x_;
+ if (y_ > y2_) y2_ = y_;
+ xs.push(x_);
+ ys.push(y_);
+ }
+ }
+ var dx = x2_ - x1_, dy = y2_ - y1_;
+ if (dx > dy) y2_ = y1_ + dx; else x2_ = x1_ + dy;
+ function insert(n, d, x, y, x1, y1, x2, y2) {
+ if (isNaN(x) || isNaN(y)) return;
+ if (n.leaf) {
+ var nx = n.x, ny = n.y;
+ if (nx != null) {
+ if (abs(nx - x) + abs(ny - y) < .01) {
+ insertChild(n, d, x, y, x1, y1, x2, y2);
+ } else {
+ var nPoint = n.point;
+ n.x = n.y = n.point = null;
+ insertChild(n, nPoint, nx, ny, x1, y1, x2, y2);
+ insertChild(n, d, x, y, x1, y1, x2, y2);
+ }
+ } else {
+ n.x = x, n.y = y, n.point = d;
+ }
+ } else {
+ insertChild(n, d, x, y, x1, y1, x2, y2);
+ }
+ }
+ function insertChild(n, d, x, y, x1, y1, x2, y2) {
+ var sx = (x1 + x2) * .5, sy = (y1 + y2) * .5, right = x >= sx, bottom = y >= sy, i = (bottom << 1) + right;
+ n.leaf = false;
+ n = n.nodes[i] || (n.nodes[i] = d3_geom_quadtreeNode());
+ if (right) x1 = sx; else x2 = sx;
+ if (bottom) y1 = sy; else y2 = sy;
+ insert(n, d, x, y, x1, y1, x2, y2);
+ }
+ var root = d3_geom_quadtreeNode();
+ root.add = function(d) {
+ insert(root, d, +fx(d, ++i), +fy(d, i), x1_, y1_, x2_, y2_);
+ };
+ root.visit = function(f) {
+ d3_geom_quadtreeVisit(f, root, x1_, y1_, x2_, y2_);
+ };
+ i = -1;
+ if (x1 == null) {
+ while (++i < n) {
+ insert(root, data[i], xs[i], ys[i], x1_, y1_, x2_, y2_);
+ }
+ --i;
+ } else data.forEach(root.add);
+ xs = ys = data = d = null;
+ return root;
+ }
+ quadtree.x = function(_) {
+ return arguments.length ? (x = _, quadtree) : x;
+ };
+ quadtree.y = function(_) {
+ return arguments.length ? (y = _, quadtree) : y;
+ };
+ quadtree.extent = function(_) {
+ if (!arguments.length) return x1 == null ? null : [ [ x1, y1 ], [ x2, y2 ] ];
+ if (_ == null) x1 = y1 = x2 = y2 = null; else x1 = +_[0][0], y1 = +_[0][1], x2 = +_[1][0],
+ y2 = +_[1][1];
+ return quadtree;
+ };
+ quadtree.size = function(_) {
+ if (!arguments.length) return x1 == null ? null : [ x2 - x1, y2 - y1 ];
+ if (_ == null) x1 = y1 = x2 = y2 = null; else x1 = y1 = 0, x2 = +_[0], y2 = +_[1];
+ return quadtree;
+ };
+ return quadtree;
+ };
+ function d3_geom_quadtreeCompatX(d) {
+ return d.x;
+ }
+ function d3_geom_quadtreeCompatY(d) {
+ return d.y;
+ }
+ function d3_geom_quadtreeNode() {
+ return {
+ leaf: true,
+ nodes: [],
+ point: null,
+ x: null,
+ y: null
+ };
+ }
+ function d3_geom_quadtreeVisit(f, node, x1, y1, x2, y2) {
+ if (!f(node, x1, y1, x2, y2)) {
+ var sx = (x1 + x2) * .5, sy = (y1 + y2) * .5, children = node.nodes;
+ if (children[0]) d3_geom_quadtreeVisit(f, children[0], x1, y1, sx, sy);
+ if (children[1]) d3_geom_quadtreeVisit(f, children[1], sx, y1, x2, sy);
+ if (children[2]) d3_geom_quadtreeVisit(f, children[2], x1, sy, sx, y2);
+ if (children[3]) d3_geom_quadtreeVisit(f, children[3], sx, sy, x2, y2);
+ }
+ }
+ d3.interpolateRgb = d3_interpolateRgb;
+ function d3_interpolateRgb(a, b) {
+ a = d3.rgb(a);
+ b = d3.rgb(b);
+ var ar = a.r, ag = a.g, ab = a.b, br = b.r - ar, bg = b.g - ag, bb = b.b - ab;
+ return function(t) {
+ return "#" + d3_rgb_hex(Math.round(ar + br * t)) + d3_rgb_hex(Math.round(ag + bg * t)) + d3_rgb_hex(Math.round(ab + bb * t));
+ };
+ }
+ d3.interpolateObject = d3_interpolateObject;
+ function d3_interpolateObject(a, b) {
+ var i = {}, c = {}, k;
+ for (k in a) {
+ if (k in b) {
+ i[k] = d3_interpolate(a[k], b[k]);
+ } else {
+ c[k] = a[k];
+ }
+ }
+ for (k in b) {
+ if (!(k in a)) {
+ c[k] = b[k];
+ }
+ }
+ return function(t) {
+ for (k in i) c[k] = i[k](t);
+ return c;
+ };
+ }
+ d3.interpolateNumber = d3_interpolateNumber;
+ function d3_interpolateNumber(a, b) {
+ b -= a = +a;
+ return function(t) {
+ return a + b * t;
+ };
+ }
+ d3.interpolateString = d3_interpolateString;
+ function d3_interpolateString(a, b) {
+ var m, i, j, s0 = 0, s1 = 0, s = [], q = [], n, o;
+ a = a + "", b = b + "";
+ d3_interpolate_number.lastIndex = 0;
+ for (i = 0; m = d3_interpolate_number.exec(b); ++i) {
+ if (m.index) s.push(b.substring(s0, s1 = m.index));
+ q.push({
+ i: s.length,
+ x: m[0]
+ });
+ s.push(null);
+ s0 = d3_interpolate_number.lastIndex;
+ }
+ if (s0 < b.length) s.push(b.substring(s0));
+ for (i = 0, n = q.length; (m = d3_interpolate_number.exec(a)) && i < n; ++i) {
+ o = q[i];
+ if (o.x == m[0]) {
+ if (o.i) {
+ if (s[o.i + 1] == null) {
+ s[o.i - 1] += o.x;
+ s.splice(o.i, 1);
+ for (j = i + 1; j < n; ++j) q[j].i--;
+ } else {
+ s[o.i - 1] += o.x + s[o.i + 1];
+ s.splice(o.i, 2);
+ for (j = i + 1; j < n; ++j) q[j].i -= 2;
+ }
+ } else {
+ if (s[o.i + 1] == null) {
+ s[o.i] = o.x;
+ } else {
+ s[o.i] = o.x + s[o.i + 1];
+ s.splice(o.i + 1, 1);
+ for (j = i + 1; j < n; ++j) q[j].i--;
+ }
+ }
+ q.splice(i, 1);
+ n--;
+ i--;
+ } else {
+ o.x = d3_interpolateNumber(parseFloat(m[0]), parseFloat(o.x));
+ }
+ }
+ while (i < n) {
+ o = q.pop();
+ if (s[o.i + 1] == null) {
+ s[o.i] = o.x;
+ } else {
+ s[o.i] = o.x + s[o.i + 1];
+ s.splice(o.i + 1, 1);
+ }
+ n--;
+ }
+ if (s.length === 1) {
+ return s[0] == null ? (o = q[0].x, function(t) {
+ return o(t) + "";
+ }) : function() {
+ return b;
+ };
+ }
+ return function(t) {
+ for (i = 0; i < n; ++i) s[(o = q[i]).i] = o.x(t);
+ return s.join("");
+ };
+ }
+ var d3_interpolate_number = /[-+]?(?:\d+\.?\d*|\.?\d+)(?:[eE][-+]?\d+)?/g;
+ d3.interpolate = d3_interpolate;
+ function d3_interpolate(a, b) {
+ var i = d3.interpolators.length, f;
+ while (--i >= 0 && !(f = d3.interpolators[i](a, b))) ;
+ return f;
+ }
+ d3.interpolators = [ function(a, b) {
+ var t = typeof b;
+ return (t === "string" ? d3_rgb_names.has(b) || /^(#|rgb\(|hsl\()/.test(b) ? d3_interpolateRgb : d3_interpolateString : b instanceof d3_Color ? d3_interpolateRgb : t === "object" ? Array.isArray(b) ? d3_interpolateArray : d3_interpolateObject : d3_interpolateNumber)(a, b);
+ } ];
+ d3.interpolateArray = d3_interpolateArray;
+ function d3_interpolateArray(a, b) {
+ var x = [], c = [], na = a.length, nb = b.length, n0 = Math.min(a.length, b.length), i;
+ for (i = 0; i < n0; ++i) x.push(d3_interpolate(a[i], b[i]));
+ for (;i < na; ++i) c[i] = a[i];
+ for (;i < nb; ++i) c[i] = b[i];
+ return function(t) {
+ for (i = 0; i < n0; ++i) c[i] = x[i](t);
+ return c;
+ };
+ }
+ var d3_ease_default = function() {
+ return d3_identity;
+ };
+ var d3_ease = d3.map({
+ linear: d3_ease_default,
+ poly: d3_ease_poly,
+ quad: function() {
+ return d3_ease_quad;
+ },
+ cubic: function() {
+ return d3_ease_cubic;
+ },
+ sin: function() {
+ return d3_ease_sin;
+ },
+ exp: function() {
+ return d3_ease_exp;
+ },
+ circle: function() {
+ return d3_ease_circle;
+ },
+ elastic: d3_ease_elastic,
+ back: d3_ease_back,
+ bounce: function() {
+ return d3_ease_bounce;
+ }
+ });
+ var d3_ease_mode = d3.map({
+ "in": d3_identity,
+ out: d3_ease_reverse,
+ "in-out": d3_ease_reflect,
+ "out-in": function(f) {
+ return d3_ease_reflect(d3_ease_reverse(f));
+ }
+ });
+ d3.ease = function(name) {
+ var i = name.indexOf("-"), t = i >= 0 ? name.substring(0, i) : name, m = i >= 0 ? name.substring(i + 1) : "in";
+ t = d3_ease.get(t) || d3_ease_default;
+ m = d3_ease_mode.get(m) || d3_identity;
+ return d3_ease_clamp(m(t.apply(null, d3_arraySlice.call(arguments, 1))));
+ };
+ function d3_ease_clamp(f) {
+ return function(t) {
+ return t <= 0 ? 0 : t >= 1 ? 1 : f(t);
+ };
+ }
+ function d3_ease_reverse(f) {
+ return function(t) {
+ return 1 - f(1 - t);
+ };
+ }
+ function d3_ease_reflect(f) {
+ return function(t) {
+ return .5 * (t < .5 ? f(2 * t) : 2 - f(2 - 2 * t));
+ };
+ }
+ function d3_ease_quad(t) {
+ return t * t;
+ }
+ function d3_ease_cubic(t) {
+ return t * t * t;
+ }
+ function d3_ease_cubicInOut(t) {
+ if (t <= 0) return 0;
+ if (t >= 1) return 1;
+ var t2 = t * t, t3 = t2 * t;
+ return 4 * (t < .5 ? t3 : 3 * (t - t2) + t3 - .75);
+ }
+ function d3_ease_poly(e) {
+ return function(t) {
+ return Math.pow(t, e);
+ };
+ }
+ function d3_ease_sin(t) {
+ return 1 - Math.cos(t * halfπ);
+ }
+ function d3_ease_exp(t) {
+ return Math.pow(2, 10 * (t - 1));
+ }
+ function d3_ease_circle(t) {
+ return 1 - Math.sqrt(1 - t * t);
+ }
+ function d3_ease_elastic(a, p) {
+ var s;
+ if (arguments.length < 2) p = .45;
+ if (arguments.length) s = p / Ï„ * Math.asin(1 / a); else a = 1, s = p / 4;
+ return function(t) {
+ return 1 + a * Math.pow(2, -10 * t) * Math.sin((t - s) * Ï„ / p);
+ };
+ }
+ function d3_ease_back(s) {
+ if (!s) s = 1.70158;
+ return function(t) {
+ return t * t * ((s + 1) * t - s);
+ };
+ }
+ function d3_ease_bounce(t) {
+ return t < 1 / 2.75 ? 7.5625 * t * t : t < 2 / 2.75 ? 7.5625 * (t -= 1.5 / 2.75) * t + .75 : t < 2.5 / 2.75 ? 7.5625 * (t -= 2.25 / 2.75) * t + .9375 : 7.5625 * (t -= 2.625 / 2.75) * t + .984375;
+ }
+ d3.interpolateHcl = d3_interpolateHcl;
+ function d3_interpolateHcl(a, b) {
+ a = d3.hcl(a);
+ b = d3.hcl(b);
+ var ah = a.h, ac = a.c, al = a.l, bh = b.h - ah, bc = b.c - ac, bl = b.l - al;
+ if (isNaN(bc)) bc = 0, ac = isNaN(ac) ? b.c : ac;
+ if (isNaN(bh)) bh = 0, ah = isNaN(ah) ? b.h : ah; else if (bh > 180) bh -= 360; else if (bh < -180) bh += 360;
+ return function(t) {
+ return d3_hcl_lab(ah + bh * t, ac + bc * t, al + bl * t) + "";
+ };
+ }
+ d3.interpolateHsl = d3_interpolateHsl;
+ function d3_interpolateHsl(a, b) {
+ a = d3.hsl(a);
+ b = d3.hsl(b);
+ var ah = a.h, as = a.s, al = a.l, bh = b.h - ah, bs = b.s - as, bl = b.l - al;
+ if (isNaN(bs)) bs = 0, as = isNaN(as) ? b.s : as;
+ if (isNaN(bh)) bh = 0, ah = isNaN(ah) ? b.h : ah; else if (bh > 180) bh -= 360; else if (bh < -180) bh += 360;
+ return function(t) {
+ return d3_hsl_rgb(ah + bh * t, as + bs * t, al + bl * t) + "";
+ };
+ }
+ d3.interpolateLab = d3_interpolateLab;
+ function d3_interpolateLab(a, b) {
+ a = d3.lab(a);
+ b = d3.lab(b);
+ var al = a.l, aa = a.a, ab = a.b, bl = b.l - al, ba = b.a - aa, bb = b.b - ab;
+ return function(t) {
+ return d3_lab_rgb(al + bl * t, aa + ba * t, ab + bb * t) + "";
+ };
+ }
+ d3.interpolateRound = d3_interpolateRound;
+ function d3_interpolateRound(a, b) {
+ b -= a;
+ return function(t) {
+ return Math.round(a + b * t);
+ };
+ }
+ d3.transform = function(string) {
+ var g = d3_document.createElementNS(d3.ns.prefix.svg, "g");
+ return (d3.transform = function(string) {
+ if (string != null) {
+ g.setAttribute("transform", string);
+ var t = g.transform.baseVal.consolidate();
+ }
+ return new d3_transform(t ? t.matrix : d3_transformIdentity);
+ })(string);
+ };
+ function d3_transform(m) {
+ var r0 = [ m.a, m.b ], r1 = [ m.c, m.d ], kx = d3_transformNormalize(r0), kz = d3_transformDot(r0, r1), ky = d3_transformNormalize(d3_transformCombine(r1, r0, -kz)) || 0;
+ if (r0[0] * r1[1] < r1[0] * r0[1]) {
+ r0[0] *= -1;
+ r0[1] *= -1;
+ kx *= -1;
+ kz *= -1;
+ }
+ this.rotate = (kx ? Math.atan2(r0[1], r0[0]) : Math.atan2(-r1[0], r1[1])) * d3_degrees;
+ this.translate = [ m.e, m.f ];
+ this.scale = [ kx, ky ];
+ this.skew = ky ? Math.atan2(kz, ky) * d3_degrees : 0;
+ }
+ d3_transform.prototype.toString = function() {
+ return "translate(" + this.translate + ")rotate(" + this.rotate + ")skewX(" + this.skew + ")scale(" + this.scale + ")";
+ };
+ function d3_transformDot(a, b) {
+ return a[0] * b[0] + a[1] * b[1];
+ }
+ function d3_transformNormalize(a) {
+ var k = Math.sqrt(d3_transformDot(a, a));
+ if (k) {
+ a[0] /= k;
+ a[1] /= k;
+ }
+ return k;
+ }
+ function d3_transformCombine(a, b, k) {
+ a[0] += k * b[0];
+ a[1] += k * b[1];
+ return a;
+ }
+ var d3_transformIdentity = {
+ a: 1,
+ b: 0,
+ c: 0,
+ d: 1,
+ e: 0,
+ f: 0
+ };
+ d3.interpolateTransform = d3_interpolateTransform;
+ function d3_interpolateTransform(a, b) {
+ var s = [], q = [], n, A = d3.transform(a), B = d3.transform(b), ta = A.translate, tb = B.translate, ra = A.rotate, rb = B.rotate, wa = A.skew, wb = B.skew, ka = A.scale, kb = B.scale;
+ if (ta[0] != tb[0] || ta[1] != tb[1]) {
+ s.push("translate(", null, ",", null, ")");
+ q.push({
+ i: 1,
+ x: d3_interpolateNumber(ta[0], tb[0])
+ }, {
+ i: 3,
+ x: d3_interpolateNumber(ta[1], tb[1])
+ });
+ } else if (tb[0] || tb[1]) {
+ s.push("translate(" + tb + ")");
+ } else {
+ s.push("");
+ }
+ if (ra != rb) {
+ if (ra - rb > 180) rb += 360; else if (rb - ra > 180) ra += 360;
+ q.push({
+ i: s.push(s.pop() + "rotate(", null, ")") - 2,
+ x: d3_interpolateNumber(ra, rb)
+ });
+ } else if (rb) {
+ s.push(s.pop() + "rotate(" + rb + ")");
+ }
+ if (wa != wb) {
+ q.push({
+ i: s.push(s.pop() + "skewX(", null, ")") - 2,
+ x: d3_interpolateNumber(wa, wb)
+ });
+ } else if (wb) {
+ s.push(s.pop() + "skewX(" + wb + ")");
+ }
+ if (ka[0] != kb[0] || ka[1] != kb[1]) {
+ n = s.push(s.pop() + "scale(", null, ",", null, ")");
+ q.push({
+ i: n - 4,
+ x: d3_interpolateNumber(ka[0], kb[0])
+ }, {
+ i: n - 2,
+ x: d3_interpolateNumber(ka[1], kb[1])
+ });
+ } else if (kb[0] != 1 || kb[1] != 1) {
+ s.push(s.pop() + "scale(" + kb + ")");
+ }
+ n = q.length;
+ return function(t) {
+ var i = -1, o;
+ while (++i < n) s[(o = q[i]).i] = o.x(t);
+ return s.join("");
+ };
+ }
+ function d3_uninterpolateNumber(a, b) {
+ b = b - (a = +a) ? 1 / (b - a) : 0;
+ return function(x) {
+ return (x - a) * b;
+ };
+ }
+ function d3_uninterpolateClamp(a, b) {
+ b = b - (a = +a) ? 1 / (b - a) : 0;
+ return function(x) {
+ return Math.max(0, Math.min(1, (x - a) * b));
+ };
+ }
+ d3.layout = {};
+ d3.layout.bundle = function() {
+ return function(links) {
+ var paths = [], i = -1, n = links.length;
+ while (++i < n) paths.push(d3_layout_bundlePath(links[i]));
+ return paths;
+ };
+ };
+ function d3_layout_bundlePath(link) {
+ var start = link.source, end = link.target, lca = d3_layout_bundleLeastCommonAncestor(start, end), points = [ start ];
+ while (start !== lca) {
+ start = start.parent;
+ points.push(start);
+ }
+ var k = points.length;
+ while (end !== lca) {
+ points.splice(k, 0, end);
+ end = end.parent;
+ }
+ return points;
+ }
+ function d3_layout_bundleAncestors(node) {
+ var ancestors = [], parent = node.parent;
+ while (parent != null) {
+ ancestors.push(node);
+ node = parent;
+ parent = parent.parent;
+ }
+ ancestors.push(node);
+ return ancestors;
+ }
+ function d3_layout_bundleLeastCommonAncestor(a, b) {
+ if (a === b) return a;
+ var aNodes = d3_layout_bundleAncestors(a), bNodes = d3_layout_bundleAncestors(b), aNode = aNodes.pop(), bNode = bNodes.pop(), sharedNode = null;
+ while (aNode === bNode) {
+ sharedNode = aNode;
+ aNode = aNodes.pop();
+ bNode = bNodes.pop();
+ }
+ return sharedNode;
+ }
+ d3.layout.chord = function() {
+ var chord = {}, chords, groups, matrix, n, padding = 0, sortGroups, sortSubgroups, sortChords;
+ function relayout() {
+ var subgroups = {}, groupSums = [], groupIndex = d3.range(n), subgroupIndex = [], k, x, x0, i, j;
+ chords = [];
+ groups = [];
+ k = 0, i = -1;
+ while (++i < n) {
+ x = 0, j = -1;
+ while (++j < n) {
+ x += matrix[i][j];
+ }
+ groupSums.push(x);
+ subgroupIndex.push(d3.range(n));
+ k += x;
+ }
+ if (sortGroups) {
+ groupIndex.sort(function(a, b) {
+ return sortGroups(groupSums[a], groupSums[b]);
+ });
+ }
+ if (sortSubgroups) {
+ subgroupIndex.forEach(function(d, i) {
+ d.sort(function(a, b) {
+ return sortSubgroups(matrix[i][a], matrix[i][b]);
+ });
+ });
+ }
+ k = (Ï„ - padding * n) / k;
+ x = 0, i = -1;
+ while (++i < n) {
+ x0 = x, j = -1;
+ while (++j < n) {
+ var di = groupIndex[i], dj = subgroupIndex[di][j], v = matrix[di][dj], a0 = x, a1 = x += v * k;
+ subgroups[di + "-" + dj] = {
+ index: di,
+ subindex: dj,
+ startAngle: a0,
+ endAngle: a1,
+ value: v
+ };
+ }
+ groups[di] = {
+ index: di,
+ startAngle: x0,
+ endAngle: x,
+ value: (x - x0) / k
+ };
+ x += padding;
+ }
+ i = -1;
+ while (++i < n) {
+ j = i - 1;
+ while (++j < n) {
+ var source = subgroups[i + "-" + j], target = subgroups[j + "-" + i];
+ if (source.value || target.value) {
+ chords.push(source.value < target.value ? {
+ source: target,
+ target: source
+ } : {
+ source: source,
+ target: target
+ });
+ }
+ }
+ }
+ if (sortChords) resort();
+ }
+ function resort() {
+ chords.sort(function(a, b) {
+ return sortChords((a.source.value + a.target.value) / 2, (b.source.value + b.target.value) / 2);
+ });
+ }
+ chord.matrix = function(x) {
+ if (!arguments.length) return matrix;
+ n = (matrix = x) && matrix.length;
+ chords = groups = null;
+ return chord;
+ };
+ chord.padding = function(x) {
+ if (!arguments.length) return padding;
+ padding = x;
+ chords = groups = null;
+ return chord;
+ };
+ chord.sortGroups = function(x) {
+ if (!arguments.length) return sortGroups;
+ sortGroups = x;
+ chords = groups = null;
+ return chord;
+ };
+ chord.sortSubgroups = function(x) {
+ if (!arguments.length) return sortSubgroups;
+ sortSubgroups = x;
+ chords = null;
+ return chord;
+ };
+ chord.sortChords = function(x) {
+ if (!arguments.length) return sortChords;
+ sortChords = x;
+ if (chords) resort();
+ return chord;
+ };
+ chord.chords = function() {
+ if (!chords) relayout();
+ return chords;
+ };
+ chord.groups = function() {
+ if (!groups) relayout();
+ return groups;
+ };
+ return chord;
+ };
+ d3.layout.force = function() {
+ var force = {}, event = d3.dispatch("start", "tick", "end"), size = [ 1, 1 ], drag, alpha, friction = .9, linkDistance = d3_layout_forceLinkDistance, linkStrength = d3_layout_forceLinkStrength, charge = -30, chargeDistance2 = d3_layout_forceChargeDistance2, gravity = .1, theta2 = .64, nodes = [], links = [], distances, strengths, charges;
+ function repulse(node) {
+ return function(quad, x1, _, x2) {
+ if (quad.point !== node) {
+ var dx = quad.cx - node.x, dy = quad.cy - node.y, dw = x2 - x1, dn = dx * dx + dy * dy;
+ if (dw * dw / theta2 < dn) {
+ if (dn < chargeDistance2) {
+ var k = quad.charge / dn;
+ node.px -= dx * k;
+ node.py -= dy * k;
+ }
+ return true;
+ }
+ if (quad.point && dn && dn < chargeDistance2) {
+ var k = quad.pointCharge / dn;
+ node.px -= dx * k;
+ node.py -= dy * k;
+ }
+ }
+ return !quad.charge;
+ };
+ }
+ force.tick = function() {
+ if ((alpha *= .99) < .005) {
+ event.end({
+ type: "end",
+ alpha: alpha = 0
+ });
+ return true;
+ }
+ var n = nodes.length, m = links.length, q, i, o, s, t, l, k, x, y;
+ for (i = 0; i < m; ++i) {
+ o = links[i];
+ s = o.source;
+ t = o.target;
+ x = t.x - s.x;
+ y = t.y - s.y;
+ if (l = x * x + y * y) {
+ l = alpha * strengths[i] * ((l = Math.sqrt(l)) - distances[i]) / l;
+ x *= l;
+ y *= l;
+ t.x -= x * (k = s.weight / (t.weight + s.weight));
+ t.y -= y * k;
+ s.x += x * (k = 1 - k);
+ s.y += y * k;
+ }
+ }
+ if (k = alpha * gravity) {
+ x = size[0] / 2;
+ y = size[1] / 2;
+ i = -1;
+ if (k) while (++i < n) {
+ o = nodes[i];
+ o.x += (x - o.x) * k;
+ o.y += (y - o.y) * k;
+ }
+ }
+ if (charge) {
+ d3_layout_forceAccumulate(q = d3.geom.quadtree(nodes), alpha, charges);
+ i = -1;
+ while (++i < n) {
+ if (!(o = nodes[i]).fixed) {
+ q.visit(repulse(o));
+ }
+ }
+ }
+ i = -1;
+ while (++i < n) {
+ o = nodes[i];
+ if (o.fixed) {
+ o.x = o.px;
+ o.y = o.py;
+ } else {
+ o.x -= (o.px - (o.px = o.x)) * friction;
+ o.y -= (o.py - (o.py = o.y)) * friction;
+ }
+ }
+ event.tick({
+ type: "tick",
+ alpha: alpha
+ });
+ };
+ force.nodes = function(x) {
+ if (!arguments.length) return nodes;
+ nodes = x;
+ return force;
+ };
+ force.links = function(x) {
+ if (!arguments.length) return links;
+ links = x;
+ return force;
+ };
+ force.size = function(x) {
+ if (!arguments.length) return size;
+ size = x;
+ return force;
+ };
+ force.linkDistance = function(x) {
+ if (!arguments.length) return linkDistance;
+ linkDistance = typeof x === "function" ? x : +x;
+ return force;
+ };
+ force.distance = force.linkDistance;
+ force.linkStrength = function(x) {
+ if (!arguments.length) return linkStrength;
+ linkStrength = typeof x === "function" ? x : +x;
+ return force;
+ };
+ force.friction = function(x) {
+ if (!arguments.length) return friction;
+ friction = +x;
+ return force;
+ };
+ force.charge = function(x) {
+ if (!arguments.length) return charge;
+ charge = typeof x === "function" ? x : +x;
+ return force;
+ };
+ force.chargeDistance = function(x) {
+ if (!arguments.length) return Math.sqrt(chargeDistance2);
+ chargeDistance2 = x * x;
+ return force;
+ };
+ force.gravity = function(x) {
+ if (!arguments.length) return gravity;
+ gravity = +x;
+ return force;
+ };
+ force.theta = function(x) {
+ if (!arguments.length) return Math.sqrt(theta2);
+ theta2 = x * x;
+ return force;
+ };
+ force.alpha = function(x) {
+ if (!arguments.length) return alpha;
+ x = +x;
+ if (alpha) {
+ if (x > 0) alpha = x; else alpha = 0;
+ } else if (x > 0) {
+ event.start({
+ type: "start",
+ alpha: alpha = x
+ });
+ d3.timer(force.tick);
+ }
+ return force;
+ };
+ force.start = function() {
+ var i, n = nodes.length, m = links.length, w = size[0], h = size[1], neighbors, o;
+ for (i = 0; i < n; ++i) {
+ (o = nodes[i]).index = i;
+ o.weight = 0;
+ }
+ for (i = 0; i < m; ++i) {
+ o = links[i];
+ if (typeof o.source == "number") o.source = nodes[o.source];
+ if (typeof o.target == "number") o.target = nodes[o.target];
+ ++o.source.weight;
+ ++o.target.weight;
+ }
+ for (i = 0; i < n; ++i) {
+ o = nodes[i];
+ if (isNaN(o.x)) o.x = position("x", w);
+ if (isNaN(o.y)) o.y = position("y", h);
+ if (isNaN(o.px)) o.px = o.x;
+ if (isNaN(o.py)) o.py = o.y;
+ }
+ distances = [];
+ if (typeof linkDistance === "function") for (i = 0; i < m; ++i) distances[i] = +linkDistance.call(this, links[i], i); else for (i = 0; i < m; ++i) distances[i] = linkDistance;
+ strengths = [];
+ if (typeof linkStrength === "function") for (i = 0; i < m; ++i) strengths[i] = +linkStrength.call(this, links[i], i); else for (i = 0; i < m; ++i) strengths[i] = linkStrength;
+ charges = [];
+ if (typeof charge === "function") for (i = 0; i < n; ++i) charges[i] = +charge.call(this, nodes[i], i); else for (i = 0; i < n; ++i) charges[i] = charge;
+ function position(dimension, size) {
+ if (!neighbors) {
+ neighbors = new Array(n);
+ for (j = 0; j < n; ++j) {
+ neighbors[j] = [];
+ }
+ for (j = 0; j < m; ++j) {
+ var o = links[j];
+ neighbors[o.source.index].push(o.target);
+ neighbors[o.target.index].push(o.source);
+ }
+ }
+ var candidates = neighbors[i], j = -1, m = candidates.length, x;
+ while (++j < m) if (!isNaN(x = candidates[j][dimension])) return x;
+ return Math.random() * size;
+ }
+ return force.resume();
+ };
+ force.resume = function() {
+ return force.alpha(.1);
+ };
+ force.stop = function() {
+ return force.alpha(0);
+ };
+ force.drag = function() {
+ if (!drag) drag = d3.behavior.drag().origin(d3_identity).on("dragstart.force", d3_layout_forceDragstart).on("drag.force", dragmove).on("dragend.force", d3_layout_forceDragend);
+ if (!arguments.length) return drag;
+ this.on("mouseover.force", d3_layout_forceMouseover).on("mouseout.force", d3_layout_forceMouseout).call(drag);
+ };
+ function dragmove(d) {
+ d.px = d3.event.x, d.py = d3.event.y;
+ force.resume();
+ }
+ return d3.rebind(force, event, "on");
+ };
+ function d3_layout_forceDragstart(d) {
+ d.fixed |= 2;
+ }
+ function d3_layout_forceDragend(d) {
+ d.fixed &= ~6;
+ }
+ function d3_layout_forceMouseover(d) {
+ d.fixed |= 4;
+ d.px = d.x, d.py = d.y;
+ }
+ function d3_layout_forceMouseout(d) {
+ d.fixed &= ~4;
+ }
+ function d3_layout_forceAccumulate(quad, alpha, charges) {
+ var cx = 0, cy = 0;
+ quad.charge = 0;
+ if (!quad.leaf) {
+ var nodes = quad.nodes, n = nodes.length, i = -1, c;
+ while (++i < n) {
+ c = nodes[i];
+ if (c == null) continue;
+ d3_layout_forceAccumulate(c, alpha, charges);
+ quad.charge += c.charge;
+ cx += c.charge * c.cx;
+ cy += c.charge * c.cy;
+ }
+ }
+ if (quad.point) {
+ if (!quad.leaf) {
+ quad.point.x += Math.random() - .5;
+ quad.point.y += Math.random() - .5;
+ }
+ var k = alpha * charges[quad.point.index];
+ quad.charge += quad.pointCharge = k;
+ cx += k * quad.point.x;
+ cy += k * quad.point.y;
+ }
+ quad.cx = cx / quad.charge;
+ quad.cy = cy / quad.charge;
+ }
+ var d3_layout_forceLinkDistance = 20, d3_layout_forceLinkStrength = 1, d3_layout_forceChargeDistance2 = Infinity;
+ d3.layout.hierarchy = function() {
+ var sort = d3_layout_hierarchySort, children = d3_layout_hierarchyChildren, value = d3_layout_hierarchyValue;
+ function recurse(node, depth, nodes) {
+ var childs = children.call(hierarchy, node, depth);
+ node.depth = depth;
+ nodes.push(node);
+ if (childs && (n = childs.length)) {
+ var i = -1, n, c = node.children = new Array(n), v = 0, j = depth + 1, d;
+ while (++i < n) {
+ d = c[i] = recurse(childs[i], j, nodes);
+ d.parent = node;
+ v += d.value;
+ }
+ if (sort) c.sort(sort);
+ if (value) node.value = v;
+ } else {
+ delete node.children;
+ if (value) {
+ node.value = +value.call(hierarchy, node, depth) || 0;
+ }
+ }
+ return node;
+ }
+ function revalue(node, depth) {
+ var children = node.children, v = 0;
+ if (children && (n = children.length)) {
+ var i = -1, n, j = depth + 1;
+ while (++i < n) v += revalue(children[i], j);
+ } else if (value) {
+ v = +value.call(hierarchy, node, depth) || 0;
+ }
+ if (value) node.value = v;
+ return v;
+ }
+ function hierarchy(d) {
+ var nodes = [];
+ recurse(d, 0, nodes);
+ return nodes;
+ }
+ hierarchy.sort = function(x) {
+ if (!arguments.length) return sort;
+ sort = x;
+ return hierarchy;
+ };
+ hierarchy.children = function(x) {
+ if (!arguments.length) return children;
+ children = x;
+ return hierarchy;
+ };
+ hierarchy.value = function(x) {
+ if (!arguments.length) return value;
+ value = x;
+ return hierarchy;
+ };
+ hierarchy.revalue = function(root) {
+ revalue(root, 0);
+ return root;
+ };
+ return hierarchy;
+ };
+ function d3_layout_hierarchyRebind(object, hierarchy) {
+ d3.rebind(object, hierarchy, "sort", "children", "value");
+ object.nodes = object;
+ object.links = d3_layout_hierarchyLinks;
+ return object;
+ }
+ function d3_layout_hierarchyChildren(d) {
+ return d.children;
+ }
+ function d3_layout_hierarchyValue(d) {
+ return d.value;
+ }
+ function d3_layout_hierarchySort(a, b) {
+ return b.value - a.value;
+ }
+ function d3_layout_hierarchyLinks(nodes) {
+ return d3.merge(nodes.map(function(parent) {
+ return (parent.children || []).map(function(child) {
+ return {
+ source: parent,
+ target: child
+ };
+ });
+ }));
+ }
+ d3.layout.partition = function() {
+ var hierarchy = d3.layout.hierarchy(), size = [ 1, 1 ];
+ function position(node, x, dx, dy) {
+ var children = node.children;
+ node.x = x;
+ node.y = node.depth * dy;
+ node.dx = dx;
+ node.dy = dy;
+ if (children && (n = children.length)) {
+ var i = -1, n, c, d;
+ dx = node.value ? dx / node.value : 0;
+ while (++i < n) {
+ position(c = children[i], x, d = c.value * dx, dy);
+ x += d;
+ }
+ }
+ }
+ function depth(node) {
+ var children = node.children, d = 0;
+ if (children && (n = children.length)) {
+ var i = -1, n;
+ while (++i < n) d = Math.max(d, depth(children[i]));
+ }
+ return 1 + d;
+ }
+ function partition(d, i) {
+ var nodes = hierarchy.call(this, d, i);
+ position(nodes[0], 0, size[0], size[1] / depth(nodes[0]));
+ return nodes;
+ }
+ partition.size = function(x) {
+ if (!arguments.length) return size;
+ size = x;
+ return partition;
+ };
+ return d3_layout_hierarchyRebind(partition, hierarchy);
+ };
+ d3.layout.pie = function() {
+ var value = Number, sort = d3_layout_pieSortByValue, startAngle = 0, endAngle = Ï„;
+ function pie(data) {
+ var values = data.map(function(d, i) {
+ return +value.call(pie, d, i);
+ });
+ var a = +(typeof startAngle === "function" ? startAngle.apply(this, arguments) : startAngle);
+ var k = ((typeof endAngle === "function" ? endAngle.apply(this, arguments) : endAngle) - a) / d3.sum(values);
+ var index = d3.range(data.length);
+ if (sort != null) index.sort(sort === d3_layout_pieSortByValue ? function(i, j) {
+ return values[j] - values[i];
+ } : function(i, j) {
+ return sort(data[i], data[j]);
+ });
+ var arcs = [];
+ index.forEach(function(i) {
+ var d;
+ arcs[i] = {
+ data: data[i],
+ value: d = values[i],
+ startAngle: a,
+ endAngle: a += d * k
+ };
+ });
+ return arcs;
+ }
+ pie.value = function(x) {
+ if (!arguments.length) return value;
+ value = x;
+ return pie;
+ };
+ pie.sort = function(x) {
+ if (!arguments.length) return sort;
+ sort = x;
+ return pie;
+ };
+ pie.startAngle = function(x) {
+ if (!arguments.length) return startAngle;
+ startAngle = x;
+ return pie;
+ };
+ pie.endAngle = function(x) {
+ if (!arguments.length) return endAngle;
+ endAngle = x;
+ return pie;
+ };
+ return pie;
+ };
+ var d3_layout_pieSortByValue = {};
+ d3.layout.stack = function() {
+ var values = d3_identity, order = d3_layout_stackOrderDefault, offset = d3_layout_stackOffsetZero, out = d3_layout_stackOut, x = d3_layout_stackX, y = d3_layout_stackY;
+ function stack(data, index) {
+ var series = data.map(function(d, i) {
+ return values.call(stack, d, i);
+ });
+ var points = series.map(function(d) {
+ return d.map(function(v, i) {
+ return [ x.call(stack, v, i), y.call(stack, v, i) ];
+ });
+ });
+ var orders = order.call(stack, points, index);
+ series = d3.permute(series, orders);
+ points = d3.permute(points, orders);
+ var offsets = offset.call(stack, points, index);
+ var n = series.length, m = series[0].length, i, j, o;
+ for (j = 0; j < m; ++j) {
+ out.call(stack, series[0][j], o = offsets[j], points[0][j][1]);
+ for (i = 1; i < n; ++i) {
+ out.call(stack, series[i][j], o += points[i - 1][j][1], points[i][j][1]);
+ }
+ }
+ return data;
+ }
+ stack.values = function(x) {
+ if (!arguments.length) return values;
+ values = x;
+ return stack;
+ };
+ stack.order = function(x) {
+ if (!arguments.length) return order;
+ order = typeof x === "function" ? x : d3_layout_stackOrders.get(x) || d3_layout_stackOrderDefault;
+ return stack;
+ };
+ stack.offset = function(x) {
+ if (!arguments.length) return offset;
+ offset = typeof x === "function" ? x : d3_layout_stackOffsets.get(x) || d3_layout_stackOffsetZero;
+ return stack;
+ };
+ stack.x = function(z) {
+ if (!arguments.length) return x;
+ x = z;
+ return stack;
+ };
+ stack.y = function(z) {
+ if (!arguments.length) return y;
+ y = z;
+ return stack;
+ };
+ stack.out = function(z) {
+ if (!arguments.length) return out;
+ out = z;
+ return stack;
+ };
+ return stack;
+ };
+ function d3_layout_stackX(d) {
+ return d.x;
+ }
+ function d3_layout_stackY(d) {
+ return d.y;
+ }
+ function d3_layout_stackOut(d, y0, y) {
+ d.y0 = y0;
+ d.y = y;
+ }
+ var d3_layout_stackOrders = d3.map({
+ "inside-out": function(data) {
+ var n = data.length, i, j, max = data.map(d3_layout_stackMaxIndex), sums = data.map(d3_layout_stackReduceSum), index = d3.range(n).sort(function(a, b) {
+ return max[a] - max[b];
+ }), top = 0, bottom = 0, tops = [], bottoms = [];
+ for (i = 0; i < n; ++i) {
+ j = index[i];
+ if (top < bottom) {
+ top += sums[j];
+ tops.push(j);
+ } else {
+ bottom += sums[j];
+ bottoms.push(j);
+ }
+ }
+ return bottoms.reverse().concat(tops);
+ },
+ reverse: function(data) {
+ return d3.range(data.length).reverse();
+ },
+ "default": d3_layout_stackOrderDefault
+ });
+ var d3_layout_stackOffsets = d3.map({
+ silhouette: function(data) {
+ var n = data.length, m = data[0].length, sums = [], max = 0, i, j, o, y0 = [];
+ for (j = 0; j < m; ++j) {
+ for (i = 0, o = 0; i < n; i++) o += data[i][j][1];
+ if (o > max) max = o;
+ sums.push(o);
+ }
+ for (j = 0; j < m; ++j) {
+ y0[j] = (max - sums[j]) / 2;
+ }
+ return y0;
+ },
+ wiggle: function(data) {
+ var n = data.length, x = data[0], m = x.length, i, j, k, s1, s2, s3, dx, o, o0, y0 = [];
+ y0[0] = o = o0 = 0;
+ for (j = 1; j < m; ++j) {
+ for (i = 0, s1 = 0; i < n; ++i) s1 += data[i][j][1];
+ for (i = 0, s2 = 0, dx = x[j][0] - x[j - 1][0]; i < n; ++i) {
+ for (k = 0, s3 = (data[i][j][1] - data[i][j - 1][1]) / (2 * dx); k < i; ++k) {
+ s3 += (data[k][j][1] - data[k][j - 1][1]) / dx;
+ }
+ s2 += s3 * data[i][j][1];
+ }
+ y0[j] = o -= s1 ? s2 / s1 * dx : 0;
+ if (o < o0) o0 = o;
+ }
+ for (j = 0; j < m; ++j) y0[j] -= o0;
+ return y0;
+ },
+ expand: function(data) {
+ var n = data.length, m = data[0].length, k = 1 / n, i, j, o, y0 = [];
+ for (j = 0; j < m; ++j) {
+ for (i = 0, o = 0; i < n; i++) o += data[i][j][1];
+ if (o) for (i = 0; i < n; i++) data[i][j][1] /= o; else for (i = 0; i < n; i++) data[i][j][1] = k;
+ }
+ for (j = 0; j < m; ++j) y0[j] = 0;
+ return y0;
+ },
+ zero: d3_layout_stackOffsetZero
+ });
+ function d3_layout_stackOrderDefault(data) {
+ return d3.range(data.length);
+ }
+ function d3_layout_stackOffsetZero(data) {
+ var j = -1, m = data[0].length, y0 = [];
+ while (++j < m) y0[j] = 0;
+ return y0;
+ }
+ function d3_layout_stackMaxIndex(array) {
+ var i = 1, j = 0, v = array[0][1], k, n = array.length;
+ for (;i < n; ++i) {
+ if ((k = array[i][1]) > v) {
+ j = i;
+ v = k;
+ }
+ }
+ return j;
+ }
+ function d3_layout_stackReduceSum(d) {
+ return d.reduce(d3_layout_stackSum, 0);
+ }
+ function d3_layout_stackSum(p, d) {
+ return p + d[1];
+ }
+ d3.layout.histogram = function() {
+ var frequency = true, valuer = Number, ranger = d3_layout_histogramRange, binner = d3_layout_histogramBinSturges;
+ function histogram(data, i) {
+ var bins = [], values = data.map(valuer, this), range = ranger.call(this, values, i), thresholds = binner.call(this, range, values, i), bin, i = -1, n = values.length, m = thresholds.length - 1, k = frequency ? 1 : 1 / n, x;
+ while (++i < m) {
+ bin = bins[i] = [];
+ bin.dx = thresholds[i + 1] - (bin.x = thresholds[i]);
+ bin.y = 0;
+ }
+ if (m > 0) {
+ i = -1;
+ while (++i < n) {
+ x = values[i];
+ if (x >= range[0] && x <= range[1]) {
+ bin = bins[d3.bisect(thresholds, x, 1, m) - 1];
+ bin.y += k;
+ bin.push(data[i]);
+ }
+ }
+ }
+ return bins;
+ }
+ histogram.value = function(x) {
+ if (!arguments.length) return valuer;
+ valuer = x;
+ return histogram;
+ };
+ histogram.range = function(x) {
+ if (!arguments.length) return ranger;
+ ranger = d3_functor(x);
+ return histogram;
+ };
+ histogram.bins = function(x) {
+ if (!arguments.length) return binner;
+ binner = typeof x === "number" ? function(range) {
+ return d3_layout_histogramBinFixed(range, x);
+ } : d3_functor(x);
+ return histogram;
+ };
+ histogram.frequency = function(x) {
+ if (!arguments.length) return frequency;
+ frequency = !!x;
+ return histogram;
+ };
+ return histogram;
+ };
+ function d3_layout_histogramBinSturges(range, values) {
+ return d3_layout_histogramBinFixed(range, Math.ceil(Math.log(values.length) / Math.LN2 + 1));
+ }
+ function d3_layout_histogramBinFixed(range, n) {
+ var x = -1, b = +range[0], m = (range[1] - b) / n, f = [];
+ while (++x <= n) f[x] = m * x + b;
+ return f;
+ }
+ function d3_layout_histogramRange(values) {
+ return [ d3.min(values), d3.max(values) ];
+ }
+ d3.layout.tree = function() {
+ var hierarchy = d3.layout.hierarchy().sort(null).value(null), separation = d3_layout_treeSeparation, size = [ 1, 1 ], nodeSize = false;
+ function tree(d, i) {
+ var nodes = hierarchy.call(this, d, i), root = nodes[0];
+ function firstWalk(node, previousSibling) {
+ var children = node.children, layout = node._tree;
+ if (children && (n = children.length)) {
+ var n, firstChild = children[0], previousChild, ancestor = firstChild, child, i = -1;
+ while (++i < n) {
+ child = children[i];
+ firstWalk(child, previousChild);
+ ancestor = apportion(child, previousChild, ancestor);
+ previousChild = child;
+ }
+ d3_layout_treeShift(node);
+ var midpoint = .5 * (firstChild._tree.prelim + child._tree.prelim);
+ if (previousSibling) {
+ layout.prelim = previousSibling._tree.prelim + separation(node, previousSibling);
+ layout.mod = layout.prelim - midpoint;
+ } else {
+ layout.prelim = midpoint;
+ }
+ } else {
+ if (previousSibling) {
+ layout.prelim = previousSibling._tree.prelim + separation(node, previousSibling);
+ }
+ }
+ }
+ function secondWalk(node, x) {
+ node.x = node._tree.prelim + x;
+ var children = node.children;
+ if (children && (n = children.length)) {
+ var i = -1, n;
+ x += node._tree.mod;
+ while (++i < n) {
+ secondWalk(children[i], x);
+ }
+ }
+ }
+ function apportion(node, previousSibling, ancestor) {
+ if (previousSibling) {
+ var vip = node, vop = node, vim = previousSibling, vom = node.parent.children[0], sip = vip._tree.mod, sop = vop._tree.mod, sim = vim._tree.mod, som = vom._tree.mod, shift;
+ while (vim = d3_layout_treeRight(vim), vip = d3_layout_treeLeft(vip), vim && vip) {
+ vom = d3_layout_treeLeft(vom);
+ vop = d3_layout_treeRight(vop);
+ vop._tree.ancestor = node;
+ shift = vim._tree.prelim + sim - vip._tree.prelim - sip + separation(vim, vip);
+ if (shift > 0) {
+ d3_layout_treeMove(d3_layout_treeAncestor(vim, node, ancestor), node, shift);
+ sip += shift;
+ sop += shift;
+ }
+ sim += vim._tree.mod;
+ sip += vip._tree.mod;
+ som += vom._tree.mod;
+ sop += vop._tree.mod;
+ }
+ if (vim && !d3_layout_treeRight(vop)) {
+ vop._tree.thread = vim;
+ vop._tree.mod += sim - sop;
+ }
+ if (vip && !d3_layout_treeLeft(vom)) {
+ vom._tree.thread = vip;
+ vom._tree.mod += sip - som;
+ ancestor = node;
+ }
+ }
+ return ancestor;
+ }
+ d3_layout_treeVisitAfter(root, function(node, previousSibling) {
+ node._tree = {
+ ancestor: node,
+ prelim: 0,
+ mod: 0,
+ change: 0,
+ shift: 0,
+ number: previousSibling ? previousSibling._tree.number + 1 : 0
+ };
+ });
+ firstWalk(root);
+ secondWalk(root, -root._tree.prelim);
+ var left = d3_layout_treeSearch(root, d3_layout_treeLeftmost), right = d3_layout_treeSearch(root, d3_layout_treeRightmost), deep = d3_layout_treeSearch(root, d3_layout_treeDeepest), x0 = left.x - separation(left, right) / 2, x1 = right.x + separation(right, left) / 2, y1 = deep.depth || 1;
+ d3_layout_treeVisitAfter(root, nodeSize ? function(node) {
+ node.x *= size[0];
+ node.y = node.depth * size[1];
+ delete node._tree;
+ } : function(node) {
+ node.x = (node.x - x0) / (x1 - x0) * size[0];
+ node.y = node.depth / y1 * size[1];
+ delete node._tree;
+ });
+ return nodes;
+ }
+ tree.separation = function(x) {
+ if (!arguments.length) return separation;
+ separation = x;
+ return tree;
+ };
+ tree.size = function(x) {
+ if (!arguments.length) return nodeSize ? null : size;
+ nodeSize = (size = x) == null;
+ return tree;
+ };
+ tree.nodeSize = function(x) {
+ if (!arguments.length) return nodeSize ? size : null;
+ nodeSize = (size = x) != null;
+ return tree;
+ };
+ return d3_layout_hierarchyRebind(tree, hierarchy);
+ };
+ function d3_layout_treeSeparation(a, b) {
+ return a.parent == b.parent ? 1 : 2;
+ }
+ function d3_layout_treeLeft(node) {
+ var children = node.children;
+ return children && children.length ? children[0] : node._tree.thread;
+ }
+ function d3_layout_treeRight(node) {
+ var children = node.children, n;
+ return children && (n = children.length) ? children[n - 1] : node._tree.thread;
+ }
+ function d3_layout_treeSearch(node, compare) {
+ var children = node.children;
+ if (children && (n = children.length)) {
+ var child, n, i = -1;
+ while (++i < n) {
+ if (compare(child = d3_layout_treeSearch(children[i], compare), node) > 0) {
+ node = child;
+ }
+ }
+ }
+ return node;
+ }
+ function d3_layout_treeRightmost(a, b) {
+ return a.x - b.x;
+ }
+ function d3_layout_treeLeftmost(a, b) {
+ return b.x - a.x;
+ }
+ function d3_layout_treeDeepest(a, b) {
+ return a.depth - b.depth;
+ }
+ function d3_layout_treeVisitAfter(node, callback) {
+ function visit(node, previousSibling) {
+ var children = node.children;
+ if (children && (n = children.length)) {
+ var child, previousChild = null, i = -1, n;
+ while (++i < n) {
+ child = children[i];
+ visit(child, previousChild);
+ previousChild = child;
+ }
+ }
+ callback(node, previousSibling);
+ }
+ visit(node, null);
+ }
+ function d3_layout_treeShift(node) {
+ var shift = 0, change = 0, children = node.children, i = children.length, child;
+ while (--i >= 0) {
+ child = children[i]._tree;
+ child.prelim += shift;
+ child.mod += shift;
+ shift += child.shift + (change += child.change);
+ }
+ }
+ function d3_layout_treeMove(ancestor, node, shift) {
+ ancestor = ancestor._tree;
+ node = node._tree;
+ var change = shift / (node.number - ancestor.number);
+ ancestor.change += change;
+ node.change -= change;
+ node.shift += shift;
+ node.prelim += shift;
+ node.mod += shift;
+ }
+ function d3_layout_treeAncestor(vim, node, ancestor) {
+ return vim._tree.ancestor.parent == node.parent ? vim._tree.ancestor : ancestor;
+ }
+ d3.layout.pack = function() {
+ var hierarchy = d3.layout.hierarchy().sort(d3_layout_packSort), padding = 0, size = [ 1, 1 ], radius;
+ function pack(d, i) {
+ var nodes = hierarchy.call(this, d, i), root = nodes[0], w = size[0], h = size[1], r = radius == null ? Math.sqrt : typeof radius === "function" ? radius : function() {
+ return radius;
+ };
+ root.x = root.y = 0;
+ d3_layout_treeVisitAfter(root, function(d) {
+ d.r = +r(d.value);
+ });
+ d3_layout_treeVisitAfter(root, d3_layout_packSiblings);
+ if (padding) {
+ var dr = padding * (radius ? 1 : Math.max(2 * root.r / w, 2 * root.r / h)) / 2;
+ d3_layout_treeVisitAfter(root, function(d) {
+ d.r += dr;
+ });
+ d3_layout_treeVisitAfter(root, d3_layout_packSiblings);
+ d3_layout_treeVisitAfter(root, function(d) {
+ d.r -= dr;
+ });
+ }
+ d3_layout_packTransform(root, w / 2, h / 2, radius ? 1 : 1 / Math.max(2 * root.r / w, 2 * root.r / h));
+ return nodes;
+ }
+ pack.size = function(_) {
+ if (!arguments.length) return size;
+ size = _;
+ return pack;
+ };
+ pack.radius = function(_) {
+ if (!arguments.length) return radius;
+ radius = _ == null || typeof _ === "function" ? _ : +_;
+ return pack;
+ };
+ pack.padding = function(_) {
+ if (!arguments.length) return padding;
+ padding = +_;
+ return pack;
+ };
+ return d3_layout_hierarchyRebind(pack, hierarchy);
+ };
+ function d3_layout_packSort(a, b) {
+ return a.value - b.value;
+ }
+ function d3_layout_packInsert(a, b) {
+ var c = a._pack_next;
+ a._pack_next = b;
+ b._pack_prev = a;
+ b._pack_next = c;
+ c._pack_prev = b;
+ }
+ function d3_layout_packSplice(a, b) {
+ a._pack_next = b;
+ b._pack_prev = a;
+ }
+ function d3_layout_packIntersects(a, b) {
+ var dx = b.x - a.x, dy = b.y - a.y, dr = a.r + b.r;
+ return .999 * dr * dr > dx * dx + dy * dy;
+ }
+ function d3_layout_packSiblings(node) {
+ if (!(nodes = node.children) || !(n = nodes.length)) return;
+ var nodes, xMin = Infinity, xMax = -Infinity, yMin = Infinity, yMax = -Infinity, a, b, c, i, j, k, n;
+ function bound(node) {
+ xMin = Math.min(node.x - node.r, xMin);
+ xMax = Math.max(node.x + node.r, xMax);
+ yMin = Math.min(node.y - node.r, yMin);
+ yMax = Math.max(node.y + node.r, yMax);
+ }
+ nodes.forEach(d3_layout_packLink);
+ a = nodes[0];
+ a.x = -a.r;
+ a.y = 0;
+ bound(a);
+ if (n > 1) {
+ b = nodes[1];
+ b.x = b.r;
+ b.y = 0;
+ bound(b);
+ if (n > 2) {
+ c = nodes[2];
+ d3_layout_packPlace(a, b, c);
+ bound(c);
+ d3_layout_packInsert(a, c);
+ a._pack_prev = c;
+ d3_layout_packInsert(c, b);
+ b = a._pack_next;
+ for (i = 3; i < n; i++) {
+ d3_layout_packPlace(a, b, c = nodes[i]);
+ var isect = 0, s1 = 1, s2 = 1;
+ for (j = b._pack_next; j !== b; j = j._pack_next, s1++) {
+ if (d3_layout_packIntersects(j, c)) {
+ isect = 1;
+ break;
+ }
+ }
+ if (isect == 1) {
+ for (k = a._pack_prev; k !== j._pack_prev; k = k._pack_prev, s2++) {
+ if (d3_layout_packIntersects(k, c)) {
+ break;
+ }
+ }
+ }
+ if (isect) {
+ if (s1 < s2 || s1 == s2 && b.r < a.r) d3_layout_packSplice(a, b = j); else d3_layout_packSplice(a = k, b);
+ i--;
+ } else {
+ d3_layout_packInsert(a, c);
+ b = c;
+ bound(c);
+ }
+ }
+ }
+ }
+ var cx = (xMin + xMax) / 2, cy = (yMin + yMax) / 2, cr = 0;
+ for (i = 0; i < n; i++) {
+ c = nodes[i];
+ c.x -= cx;
+ c.y -= cy;
+ cr = Math.max(cr, c.r + Math.sqrt(c.x * c.x + c.y * c.y));
+ }
+ node.r = cr;
+ nodes.forEach(d3_layout_packUnlink);
+ }
+ function d3_layout_packLink(node) {
+ node._pack_next = node._pack_prev = node;
+ }
+ function d3_layout_packUnlink(node) {
+ delete node._pack_next;
+ delete node._pack_prev;
+ }
+ function d3_layout_packTransform(node, x, y, k) {
+ var children = node.children;
+ node.x = x += k * node.x;
+ node.y = y += k * node.y;
+ node.r *= k;
+ if (children) {
+ var i = -1, n = children.length;
+ while (++i < n) d3_layout_packTransform(children[i], x, y, k);
+ }
+ }
+ function d3_layout_packPlace(a, b, c) {
+ var db = a.r + c.r, dx = b.x - a.x, dy = b.y - a.y;
+ if (db && (dx || dy)) {
+ var da = b.r + c.r, dc = dx * dx + dy * dy;
+ da *= da;
+ db *= db;
+ var x = .5 + (db - da) / (2 * dc), y = Math.sqrt(Math.max(0, 2 * da * (db + dc) - (db -= dc) * db - da * da)) / (2 * dc);
+ c.x = a.x + x * dx + y * dy;
+ c.y = a.y + x * dy - y * dx;
+ } else {
+ c.x = a.x + db;
+ c.y = a.y;
+ }
+ }
+ d3.layout.cluster = function() {
+ var hierarchy = d3.layout.hierarchy().sort(null).value(null), separation = d3_layout_treeSeparation, size = [ 1, 1 ], nodeSize = false;
+ function cluster(d, i) {
+ var nodes = hierarchy.call(this, d, i), root = nodes[0], previousNode, x = 0;
+ d3_layout_treeVisitAfter(root, function(node) {
+ var children = node.children;
+ if (children && children.length) {
+ node.x = d3_layout_clusterX(children);
+ node.y = d3_layout_clusterY(children);
+ } else {
+ node.x = previousNode ? x += separation(node, previousNode) : 0;
+ node.y = 0;
+ previousNode = node;
+ }
+ });
+ var left = d3_layout_clusterLeft(root), right = d3_layout_clusterRight(root), x0 = left.x - separation(left, right) / 2, x1 = right.x + separation(right, left) / 2;
+ d3_layout_treeVisitAfter(root, nodeSize ? function(node) {
+ node.x = (node.x - root.x) * size[0];
+ node.y = (root.y - node.y) * size[1];
+ } : function(node) {
+ node.x = (node.x - x0) / (x1 - x0) * size[0];
+ node.y = (1 - (root.y ? node.y / root.y : 1)) * size[1];
+ });
+ return nodes;
+ }
+ cluster.separation = function(x) {
+ if (!arguments.length) return separation;
+ separation = x;
+ return cluster;
+ };
+ cluster.size = function(x) {
+ if (!arguments.length) return nodeSize ? null : size;
+ nodeSize = (size = x) == null;
+ return cluster;
+ };
+ cluster.nodeSize = function(x) {
+ if (!arguments.length) return nodeSize ? size : null;
+ nodeSize = (size = x) != null;
+ return cluster;
+ };
+ return d3_layout_hierarchyRebind(cluster, hierarchy);
+ };
+ function d3_layout_clusterY(children) {
+ return 1 + d3.max(children, function(child) {
+ return child.y;
+ });
+ }
+ function d3_layout_clusterX(children) {
+ return children.reduce(function(x, child) {
+ return x + child.x;
+ }, 0) / children.length;
+ }
+ function d3_layout_clusterLeft(node) {
+ var children = node.children;
+ return children && children.length ? d3_layout_clusterLeft(children[0]) : node;
+ }
+ function d3_layout_clusterRight(node) {
+ var children = node.children, n;
+ return children && (n = children.length) ? d3_layout_clusterRight(children[n - 1]) : node;
+ }
+ d3.layout.treemap = function() {
+ var hierarchy = d3.layout.hierarchy(), round = Math.round, size = [ 1, 1 ], padding = null, pad = d3_layout_treemapPadNull, sticky = false, stickies, mode = "squarify", ratio = .5 * (1 + Math.sqrt(5));
+ function scale(children, k) {
+ var i = -1, n = children.length, child, area;
+ while (++i < n) {
+ area = (child = children[i]).value * (k < 0 ? 0 : k);
+ child.area = isNaN(area) || area <= 0 ? 0 : area;
+ }
+ }
+ function squarify(node) {
+ var children = node.children;
+ if (children && children.length) {
+ var rect = pad(node), row = [], remaining = children.slice(), child, best = Infinity, score, u = mode === "slice" ? rect.dx : mode === "dice" ? rect.dy : mode === "slice-dice" ? node.depth & 1 ? rect.dy : rect.dx : Math.min(rect.dx, rect.dy), n;
+ scale(remaining, rect.dx * rect.dy / node.value);
+ row.area = 0;
+ while ((n = remaining.length) > 0) {
+ row.push(child = remaining[n - 1]);
+ row.area += child.area;
+ if (mode !== "squarify" || (score = worst(row, u)) <= best) {
+ remaining.pop();
+ best = score;
+ } else {
+ row.area -= row.pop().area;
+ position(row, u, rect, false);
+ u = Math.min(rect.dx, rect.dy);
+ row.length = row.area = 0;
+ best = Infinity;
+ }
+ }
+ if (row.length) {
+ position(row, u, rect, true);
+ row.length = row.area = 0;
+ }
+ children.forEach(squarify);
+ }
+ }
+ function stickify(node) {
+ var children = node.children;
+ if (children && children.length) {
+ var rect = pad(node), remaining = children.slice(), child, row = [];
+ scale(remaining, rect.dx * rect.dy / node.value);
+ row.area = 0;
+ while (child = remaining.pop()) {
+ row.push(child);
+ row.area += child.area;
+ if (child.z != null) {
+ position(row, child.z ? rect.dx : rect.dy, rect, !remaining.length);
+ row.length = row.area = 0;
+ }
+ }
+ children.forEach(stickify);
+ }
+ }
+ function worst(row, u) {
+ var s = row.area, r, rmax = 0, rmin = Infinity, i = -1, n = row.length;
+ while (++i < n) {
+ if (!(r = row[i].area)) continue;
+ if (r < rmin) rmin = r;
+ if (r > rmax) rmax = r;
+ }
+ s *= s;
+ u *= u;
+ return s ? Math.max(u * rmax * ratio / s, s / (u * rmin * ratio)) : Infinity;
+ }
+ function position(row, u, rect, flush) {
+ var i = -1, n = row.length, x = rect.x, y = rect.y, v = u ? round(row.area / u) : 0, o;
+ if (u == rect.dx) {
+ if (flush || v > rect.dy) v = rect.dy;
+ while (++i < n) {
+ o = row[i];
+ o.x = x;
+ o.y = y;
+ o.dy = v;
+ x += o.dx = Math.min(rect.x + rect.dx - x, v ? round(o.area / v) : 0);
+ }
+ o.z = true;
+ o.dx += rect.x + rect.dx - x;
+ rect.y += v;
+ rect.dy -= v;
+ } else {
+ if (flush || v > rect.dx) v = rect.dx;
+ while (++i < n) {
+ o = row[i];
+ o.x = x;
+ o.y = y;
+ o.dx = v;
+ y += o.dy = Math.min(rect.y + rect.dy - y, v ? round(o.area / v) : 0);
+ }
+ o.z = false;
+ o.dy += rect.y + rect.dy - y;
+ rect.x += v;
+ rect.dx -= v;
+ }
+ }
+ function treemap(d) {
+ var nodes = stickies || hierarchy(d), root = nodes[0];
+ root.x = 0;
+ root.y = 0;
+ root.dx = size[0];
+ root.dy = size[1];
+ if (stickies) hierarchy.revalue(root);
+ scale([ root ], root.dx * root.dy / root.value);
+ (stickies ? stickify : squarify)(root);
+ if (sticky) stickies = nodes;
+ return nodes;
+ }
+ treemap.size = function(x) {
+ if (!arguments.length) return size;
+ size = x;
+ return treemap;
+ };
+ treemap.padding = function(x) {
+ if (!arguments.length) return padding;
+ function padFunction(node) {
+ var p = x.call(treemap, node, node.depth);
+ return p == null ? d3_layout_treemapPadNull(node) : d3_layout_treemapPad(node, typeof p === "number" ? [ p, p, p, p ] : p);
+ }
+ function padConstant(node) {
+ return d3_layout_treemapPad(node, x);
+ }
+ var type;
+ pad = (padding = x) == null ? d3_layout_treemapPadNull : (type = typeof x) === "function" ? padFunction : type === "number" ? (x = [ x, x, x, x ],
+ padConstant) : padConstant;
+ return treemap;
+ };
+ treemap.round = function(x) {
+ if (!arguments.length) return round != Number;
+ round = x ? Math.round : Number;
+ return treemap;
+ };
+ treemap.sticky = function(x) {
+ if (!arguments.length) return sticky;
+ sticky = x;
+ stickies = null;
+ return treemap;
+ };
+ treemap.ratio = function(x) {
+ if (!arguments.length) return ratio;
+ ratio = x;
+ return treemap;
+ };
+ treemap.mode = function(x) {
+ if (!arguments.length) return mode;
+ mode = x + "";
+ return treemap;
+ };
+ return d3_layout_hierarchyRebind(treemap, hierarchy);
+ };
+ function d3_layout_treemapPadNull(node) {
+ return {
+ x: node.x,
+ y: node.y,
+ dx: node.dx,
+ dy: node.dy
+ };
+ }
+ function d3_layout_treemapPad(node, padding) {
+ var x = node.x + padding[3], y = node.y + padding[0], dx = node.dx - padding[1] - padding[3], dy = node.dy - padding[0] - padding[2];
+ if (dx < 0) {
+ x += dx / 2;
+ dx = 0;
+ }
+ if (dy < 0) {
+ y += dy / 2;
+ dy = 0;
+ }
+ return {
+ x: x,
+ y: y,
+ dx: dx,
+ dy: dy
+ };
+ }
+ d3.random = {
+ normal: function(µ, σ) {
+ var n = arguments.length;
+ if (n < 2) σ = 1;
+ if (n < 1) µ = 0;
+ return function() {
+ var x, y, r;
+ do {
+ x = Math.random() * 2 - 1;
+ y = Math.random() * 2 - 1;
+ r = x * x + y * y;
+ } while (!r || r > 1);
+ return µ + σ * x * Math.sqrt(-2 * Math.log(r) / r);
+ };
+ },
+ logNormal: function() {
+ var random = d3.random.normal.apply(d3, arguments);
+ return function() {
+ return Math.exp(random());
+ };
+ },
+ bates: function(m) {
+ var random = d3.random.irwinHall(m);
+ return function() {
+ return random() / m;
+ };
+ },
+ irwinHall: function(m) {
+ return function() {
+ for (var s = 0, j = 0; j < m; j++) s += Math.random();
+ return s;
+ };
+ }
+ };
+ d3.scale = {};
+ function d3_scaleExtent(domain) {
+ var start = domain[0], stop = domain[domain.length - 1];
+ return start < stop ? [ start, stop ] : [ stop, start ];
+ }
+ function d3_scaleRange(scale) {
+ return scale.rangeExtent ? scale.rangeExtent() : d3_scaleExtent(scale.range());
+ }
+ function d3_scale_bilinear(domain, range, uninterpolate, interpolate) {
+ var u = uninterpolate(domain[0], domain[1]), i = interpolate(range[0], range[1]);
+ return function(x) {
+ return i(u(x));
+ };
+ }
+ function d3_scale_nice(domain, nice) {
+ var i0 = 0, i1 = domain.length - 1, x0 = domain[i0], x1 = domain[i1], dx;
+ if (x1 < x0) {
+ dx = i0, i0 = i1, i1 = dx;
+ dx = x0, x0 = x1, x1 = dx;
+ }
+ domain[i0] = nice.floor(x0);
+ domain[i1] = nice.ceil(x1);
+ return domain;
+ }
+ function d3_scale_niceStep(step) {
+ return step ? {
+ floor: function(x) {
+ return Math.floor(x / step) * step;
+ },
+ ceil: function(x) {
+ return Math.ceil(x / step) * step;
+ }
+ } : d3_scale_niceIdentity;
+ }
+ var d3_scale_niceIdentity = {
+ floor: d3_identity,
+ ceil: d3_identity
+ };
+ function d3_scale_polylinear(domain, range, uninterpolate, interpolate) {
+ var u = [], i = [], j = 0, k = Math.min(domain.length, range.length) - 1;
+ if (domain[k] < domain[0]) {
+ domain = domain.slice().reverse();
+ range = range.slice().reverse();
+ }
+ while (++j <= k) {
+ u.push(uninterpolate(domain[j - 1], domain[j]));
+ i.push(interpolate(range[j - 1], range[j]));
+ }
+ return function(x) {
+ var j = d3.bisect(domain, x, 1, k) - 1;
+ return i[j](u[j](x));
+ };
+ }
+ d3.scale.linear = function() {
+ return d3_scale_linear([ 0, 1 ], [ 0, 1 ], d3_interpolate, false);
+ };
+ function d3_scale_linear(domain, range, interpolate, clamp) {
+ var output, input;
+ function rescale() {
+ var linear = Math.min(domain.length, range.length) > 2 ? d3_scale_polylinear : d3_scale_bilinear, uninterpolate = clamp ? d3_uninterpolateClamp : d3_uninterpolateNumber;
+ output = linear(domain, range, uninterpolate, interpolate);
+ input = linear(range, domain, uninterpolate, d3_interpolate);
+ return scale;
+ }
+ function scale(x) {
+ return output(x);
+ }
+ scale.invert = function(y) {
+ return input(y);
+ };
+ scale.domain = function(x) {
+ if (!arguments.length) return domain;
+ domain = x.map(Number);
+ return rescale();
+ };
+ scale.range = function(x) {
+ if (!arguments.length) return range;
+ range = x;
+ return rescale();
+ };
+ scale.rangeRound = function(x) {
+ return scale.range(x).interpolate(d3_interpolateRound);
+ };
+ scale.clamp = function(x) {
+ if (!arguments.length) return clamp;
+ clamp = x;
+ return rescale();
+ };
+ scale.interpolate = function(x) {
+ if (!arguments.length) return interpolate;
+ interpolate = x;
+ return rescale();
+ };
+ scale.ticks = function(m) {
+ return d3_scale_linearTicks(domain, m);
+ };
+ scale.tickFormat = function(m, format) {
+ return d3_scale_linearTickFormat(domain, m, format);
+ };
+ scale.nice = function(m) {
+ d3_scale_linearNice(domain, m);
+ return rescale();
+ };
+ scale.copy = function() {
+ return d3_scale_linear(domain, range, interpolate, clamp);
+ };
+ return rescale();
+ }
+ function d3_scale_linearRebind(scale, linear) {
+ return d3.rebind(scale, linear, "range", "rangeRound", "interpolate", "clamp");
+ }
+ function d3_scale_linearNice(domain, m) {
+ return d3_scale_nice(domain, d3_scale_niceStep(d3_scale_linearTickRange(domain, m)[2]));
+ }
+ function d3_scale_linearTickRange(domain, m) {
+ if (m == null) m = 10;
+ var extent = d3_scaleExtent(domain), span = extent[1] - extent[0], step = Math.pow(10, Math.floor(Math.log(span / m) / Math.LN10)), err = m / span * step;
+ if (err <= .15) step *= 10; else if (err <= .35) step *= 5; else if (err <= .75) step *= 2;
+ extent[0] = Math.ceil(extent[0] / step) * step;
+ extent[1] = Math.floor(extent[1] / step) * step + step * .5;
+ extent[2] = step;
+ return extent;
+ }
+ function d3_scale_linearTicks(domain, m) {
+ return d3.range.apply(d3, d3_scale_linearTickRange(domain, m));
+ }
+ function d3_scale_linearTickFormat(domain, m, format) {
+ var range = d3_scale_linearTickRange(domain, m);
+ return d3.format(format ? format.replace(d3_format_re, function(a, b, c, d, e, f, g, h, i, j) {
+ return [ b, c, d, e, f, g, h, i || "." + d3_scale_linearFormatPrecision(j, range), j ].join("");
+ }) : ",." + d3_scale_linearPrecision(range[2]) + "f");
+ }
+ var d3_scale_linearFormatSignificant = {
+ s: 1,
+ g: 1,
+ p: 1,
+ r: 1,
+ e: 1
+ };
+ function d3_scale_linearPrecision(value) {
+ return -Math.floor(Math.log(value) / Math.LN10 + .01);
+ }
+ function d3_scale_linearFormatPrecision(type, range) {
+ var p = d3_scale_linearPrecision(range[2]);
+ return type in d3_scale_linearFormatSignificant ? Math.abs(p - d3_scale_linearPrecision(Math.max(Math.abs(range[0]), Math.abs(range[1])))) + +(type !== "e") : p - (type === "%") * 2;
+ }
+ d3.scale.log = function() {
+ return d3_scale_log(d3.scale.linear().domain([ 0, 1 ]), 10, true, [ 1, 10 ]);
+ };
+ function d3_scale_log(linear, base, positive, domain) {
+ function log(x) {
+ return (positive ? Math.log(x < 0 ? 0 : x) : -Math.log(x > 0 ? 0 : -x)) / Math.log(base);
+ }
+ function pow(x) {
+ return positive ? Math.pow(base, x) : -Math.pow(base, -x);
+ }
+ function scale(x) {
+ return linear(log(x));
+ }
+ scale.invert = function(x) {
+ return pow(linear.invert(x));
+ };
+ scale.domain = function(x) {
+ if (!arguments.length) return domain;
+ positive = x[0] >= 0;
+ linear.domain((domain = x.map(Number)).map(log));
+ return scale;
+ };
+ scale.base = function(_) {
+ if (!arguments.length) return base;
+ base = +_;
+ linear.domain(domain.map(log));
+ return scale;
+ };
+ scale.nice = function() {
+ var niced = d3_scale_nice(domain.map(log), positive ? Math : d3_scale_logNiceNegative);
+ linear.domain(niced);
+ domain = niced.map(pow);
+ return scale;
+ };
+ scale.ticks = function() {
+ var extent = d3_scaleExtent(domain), ticks = [], u = extent[0], v = extent[1], i = Math.floor(log(u)), j = Math.ceil(log(v)), n = base % 1 ? 2 : base;
+ if (isFinite(j - i)) {
+ if (positive) {
+ for (;i < j; i++) for (var k = 1; k < n; k++) ticks.push(pow(i) * k);
+ ticks.push(pow(i));
+ } else {
+ ticks.push(pow(i));
+ for (;i++ < j; ) for (var k = n - 1; k > 0; k--) ticks.push(pow(i) * k);
+ }
+ for (i = 0; ticks[i] < u; i++) {}
+ for (j = ticks.length; ticks[j - 1] > v; j--) {}
+ ticks = ticks.slice(i, j);
+ }
+ return ticks;
+ };
+ scale.tickFormat = function(n, format) {
+ if (!arguments.length) return d3_scale_logFormat;
+ if (arguments.length < 2) format = d3_scale_logFormat; else if (typeof format !== "function") format = d3.format(format);
+ var k = Math.max(.1, n / scale.ticks().length), f = positive ? (e = 1e-12, Math.ceil) : (e = -1e-12,
+ Math.floor), e;
+ return function(d) {
+ return d / pow(f(log(d) + e)) <= k ? format(d) : "";
+ };
+ };
+ scale.copy = function() {
+ return d3_scale_log(linear.copy(), base, positive, domain);
+ };
+ return d3_scale_linearRebind(scale, linear);
+ }
+ var d3_scale_logFormat = d3.format(".0e"), d3_scale_logNiceNegative = {
+ floor: function(x) {
+ return -Math.ceil(-x);
+ },
+ ceil: function(x) {
+ return -Math.floor(-x);
+ }
+ };
+ d3.scale.pow = function() {
+ return d3_scale_pow(d3.scale.linear(), 1, [ 0, 1 ]);
+ };
+ function d3_scale_pow(linear, exponent, domain) {
+ var powp = d3_scale_powPow(exponent), powb = d3_scale_powPow(1 / exponent);
+ function scale(x) {
+ return linear(powp(x));
+ }
+ scale.invert = function(x) {
+ return powb(linear.invert(x));
+ };
+ scale.domain = function(x) {
+ if (!arguments.length) return domain;
+ linear.domain((domain = x.map(Number)).map(powp));
+ return scale;
+ };
+ scale.ticks = function(m) {
+ return d3_scale_linearTicks(domain, m);
+ };
+ scale.tickFormat = function(m, format) {
+ return d3_scale_linearTickFormat(domain, m, format);
+ };
+ scale.nice = function(m) {
+ return scale.domain(d3_scale_linearNice(domain, m));
+ };
+ scale.exponent = function(x) {
+ if (!arguments.length) return exponent;
+ powp = d3_scale_powPow(exponent = x);
+ powb = d3_scale_powPow(1 / exponent);
+ linear.domain(domain.map(powp));
+ return scale;
+ };
+ scale.copy = function() {
+ return d3_scale_pow(linear.copy(), exponent, domain);
+ };
+ return d3_scale_linearRebind(scale, linear);
+ }
+ function d3_scale_powPow(e) {
+ return function(x) {
+ return x < 0 ? -Math.pow(-x, e) : Math.pow(x, e);
+ };
+ }
+ d3.scale.sqrt = function() {
+ return d3.scale.pow().exponent(.5);
+ };
+ d3.scale.ordinal = function() {
+ return d3_scale_ordinal([], {
+ t: "range",
+ a: [ [] ]
+ });
+ };
+ function d3_scale_ordinal(domain, ranger) {
+ var index, range, rangeBand;
+ function scale(x) {
+ return range[((index.get(x) || ranger.t === "range" && index.set(x, domain.push(x))) - 1) % range.length];
+ }
+ function steps(start, step) {
+ return d3.range(domain.length).map(function(i) {
+ return start + step * i;
+ });
+ }
+ scale.domain = function(x) {
+ if (!arguments.length) return domain;
+ domain = [];
+ index = new d3_Map();
+ var i = -1, n = x.length, xi;
+ while (++i < n) if (!index.has(xi = x[i])) index.set(xi, domain.push(xi));
+ return scale[ranger.t].apply(scale, ranger.a);
+ };
+ scale.range = function(x) {
+ if (!arguments.length) return range;
+ range = x;
+ rangeBand = 0;
+ ranger = {
+ t: "range",
+ a: arguments
+ };
+ return scale;
+ };
+ scale.rangePoints = function(x, padding) {
+ if (arguments.length < 2) padding = 0;
+ var start = x[0], stop = x[1], step = (stop - start) / (Math.max(1, domain.length - 1) + padding);
+ range = steps(domain.length < 2 ? (start + stop) / 2 : start + step * padding / 2, step);
+ rangeBand = 0;
+ ranger = {
+ t: "rangePoints",
+ a: arguments
+ };
+ return scale;
+ };
+ scale.rangeBands = function(x, padding, outerPadding) {
+ if (arguments.length < 2) padding = 0;
+ if (arguments.length < 3) outerPadding = padding;
+ var reverse = x[1] < x[0], start = x[reverse - 0], stop = x[1 - reverse], step = (stop - start) / (domain.length - padding + 2 * outerPadding);
+ range = steps(start + step * outerPadding, step);
+ if (reverse) range.reverse();
+ rangeBand = step * (1 - padding);
+ ranger = {
+ t: "rangeBands",
+ a: arguments
+ };
+ return scale;
+ };
+ scale.rangeRoundBands = function(x, padding, outerPadding) {
+ if (arguments.length < 2) padding = 0;
+ if (arguments.length < 3) outerPadding = padding;
+ var reverse = x[1] < x[0], start = x[reverse - 0], stop = x[1 - reverse], step = Math.floor((stop - start) / (domain.length - padding + 2 * outerPadding)), error = stop - start - (domain.length - padding) * step;
+ range = steps(start + Math.round(error / 2), step);
+ if (reverse) range.reverse();
+ rangeBand = Math.round(step * (1 - padding));
+ ranger = {
+ t: "rangeRoundBands",
+ a: arguments
+ };
+ return scale;
+ };
+ scale.rangeBand = function() {
+ return rangeBand;
+ };
+ scale.rangeExtent = function() {
+ return d3_scaleExtent(ranger.a[0]);
+ };
+ scale.copy = function() {
+ return d3_scale_ordinal(domain, ranger);
+ };
+ return scale.domain(domain);
+ }
+ d3.scale.category10 = function() {
+ return d3.scale.ordinal().range(d3_category10);
+ };
+ d3.scale.category20 = function() {
+ return d3.scale.ordinal().range(d3_category20);
+ };
+ d3.scale.category20b = function() {
+ return d3.scale.ordinal().range(d3_category20b);
+ };
+ d3.scale.category20c = function() {
+ return d3.scale.ordinal().range(d3_category20c);
+ };
+ var d3_category10 = [ 2062260, 16744206, 2924588, 14034728, 9725885, 9197131, 14907330, 8355711, 12369186, 1556175 ].map(d3_rgbString);
+ var d3_category20 = [ 2062260, 11454440, 16744206, 16759672, 2924588, 10018698, 14034728, 16750742, 9725885, 12955861, 9197131, 12885140, 14907330, 16234194, 8355711, 13092807, 12369186, 14408589, 1556175, 10410725 ].map(d3_rgbString);
+ var d3_category20b = [ 3750777, 5395619, 7040719, 10264286, 6519097, 9216594, 11915115, 13556636, 9202993, 12426809, 15186514, 15190932, 8666169, 11356490, 14049643, 15177372, 8077683, 10834324, 13528509, 14589654 ].map(d3_rgbString);
+ var d3_category20c = [ 3244733, 7057110, 10406625, 13032431, 15095053, 16616764, 16625259, 16634018, 3253076, 7652470, 10607003, 13101504, 7695281, 10394312, 12369372, 14342891, 6513507, 9868950, 12434877, 14277081 ].map(d3_rgbString);
+ d3.scale.quantile = function() {
+ return d3_scale_quantile([], []);
+ };
+ function d3_scale_quantile(domain, range) {
+ var thresholds;
+ function rescale() {
+ var k = 0, q = range.length;
+ thresholds = [];
+ while (++k < q) thresholds[k - 1] = d3.quantile(domain, k / q);
+ return scale;
+ }
+ function scale(x) {
+ if (!isNaN(x = +x)) return range[d3.bisect(thresholds, x)];
+ }
+ scale.domain = function(x) {
+ if (!arguments.length) return domain;
+ domain = x.filter(function(d) {
+ return !isNaN(d);
+ }).sort(d3.ascending);
+ return rescale();
+ };
+ scale.range = function(x) {
+ if (!arguments.length) return range;
+ range = x;
+ return rescale();
+ };
+ scale.quantiles = function() {
+ return thresholds;
+ };
+ scale.invertExtent = function(y) {
+ y = range.indexOf(y);
+ return y < 0 ? [ NaN, NaN ] : [ y > 0 ? thresholds[y - 1] : domain[0], y < thresholds.length ? thresholds[y] : domain[domain.length - 1] ];
+ };
+ scale.copy = function() {
+ return d3_scale_quantile(domain, range);
+ };
+ return rescale();
+ }
+ d3.scale.quantize = function() {
+ return d3_scale_quantize(0, 1, [ 0, 1 ]);
+ };
+ function d3_scale_quantize(x0, x1, range) {
+ var kx, i;
+ function scale(x) {
+ return range[Math.max(0, Math.min(i, Math.floor(kx * (x - x0))))];
+ }
+ function rescale() {
+ kx = range.length / (x1 - x0);
+ i = range.length - 1;
+ return scale;
+ }
+ scale.domain = function(x) {
+ if (!arguments.length) return [ x0, x1 ];
+ x0 = +x[0];
+ x1 = +x[x.length - 1];
+ return rescale();
+ };
+ scale.range = function(x) {
+ if (!arguments.length) return range;
+ range = x;
+ return rescale();
+ };
+ scale.invertExtent = function(y) {
+ y = range.indexOf(y);
+ y = y < 0 ? NaN : y / kx + x0;
+ return [ y, y + 1 / kx ];
+ };
+ scale.copy = function() {
+ return d3_scale_quantize(x0, x1, range);
+ };
+ return rescale();
+ }
+ d3.scale.threshold = function() {
+ return d3_scale_threshold([ .5 ], [ 0, 1 ]);
+ };
+ function d3_scale_threshold(domain, range) {
+ function scale(x) {
+ if (x <= x) return range[d3.bisect(domain, x)];
+ }
+ scale.domain = function(_) {
+ if (!arguments.length) return domain;
+ domain = _;
+ return scale;
+ };
+ scale.range = function(_) {
+ if (!arguments.length) return range;
+ range = _;
+ return scale;
+ };
+ scale.invertExtent = function(y) {
+ y = range.indexOf(y);
+ return [ domain[y - 1], domain[y] ];
+ };
+ scale.copy = function() {
+ return d3_scale_threshold(domain, range);
+ };
+ return scale;
+ }
+ d3.scale.identity = function() {
+ return d3_scale_identity([ 0, 1 ]);
+ };
+ function d3_scale_identity(domain) {
+ function identity(x) {
+ return +x;
+ }
+ identity.invert = identity;
+ identity.domain = identity.range = function(x) {
+ if (!arguments.length) return domain;
+ domain = x.map(identity);
+ return identity;
+ };
+ identity.ticks = function(m) {
+ return d3_scale_linearTicks(domain, m);
+ };
+ identity.tickFormat = function(m, format) {
+ return d3_scale_linearTickFormat(domain, m, format);
+ };
+ identity.copy = function() {
+ return d3_scale_identity(domain);
+ };
+ return identity;
+ }
+ d3.svg = {};
+ d3.svg.arc = function() {
+ var innerRadius = d3_svg_arcInnerRadius, outerRadius = d3_svg_arcOuterRadius, startAngle = d3_svg_arcStartAngle, endAngle = d3_svg_arcEndAngle;
+ function arc() {
+ var r0 = innerRadius.apply(this, arguments), r1 = outerRadius.apply(this, arguments), a0 = startAngle.apply(this, arguments) + d3_svg_arcOffset, a1 = endAngle.apply(this, arguments) + d3_svg_arcOffset, da = (a1 < a0 && (da = a0,
+ a0 = a1, a1 = da), a1 - a0), df = da < π ? "0" : "1", c0 = Math.cos(a0), s0 = Math.sin(a0), c1 = Math.cos(a1), s1 = Math.sin(a1);
+ return da >= d3_svg_arcMax ? r0 ? "M0," + r1 + "A" + r1 + "," + r1 + " 0 1,1 0," + -r1 + "A" + r1 + "," + r1 + " 0 1,1 0," + r1 + "M0," + r0 + "A" + r0 + "," + r0 + " 0 1,0 0," + -r0 + "A" + r0 + "," + r0 + " 0 1,0 0," + r0 + "Z" : "M0," + r1 + "A" + r1 + "," + r1 + " 0 1,1 0," + -r1 + "A" + r1 + "," + r1 + " 0 1,1 0," + r1 + "Z" : r0 ? "M" + r1 * c0 + "," + r1 * s0 + "A" + r1 + "," + r1 + " 0 " + df + ",1 " + r1 * c1 + "," + r1 * s1 + "L" + r0 * c1 + "," + r0 * s1 + "A" + r0 + "," + r0 + " 0 " + df + ",0 " + r0 * c0 + "," + r0 * s0 + "Z" : "M" + r1 * c0 + "," + r1 * s0 + "A" + r1 + "," + r1 + " 0 " + df + ",1 " + r1 * c1 + "," + r1 * s1 + "L0,0" + "Z";
+ }
+ arc.innerRadius = function(v) {
+ if (!arguments.length) return innerRadius;
+ innerRadius = d3_functor(v);
+ return arc;
+ };
+ arc.outerRadius = function(v) {
+ if (!arguments.length) return outerRadius;
+ outerRadius = d3_functor(v);
+ return arc;
+ };
+ arc.startAngle = function(v) {
+ if (!arguments.length) return startAngle;
+ startAngle = d3_functor(v);
+ return arc;
+ };
+ arc.endAngle = function(v) {
+ if (!arguments.length) return endAngle;
+ endAngle = d3_functor(v);
+ return arc;
+ };
+ arc.centroid = function() {
+ var r = (innerRadius.apply(this, arguments) + outerRadius.apply(this, arguments)) / 2, a = (startAngle.apply(this, arguments) + endAngle.apply(this, arguments)) / 2 + d3_svg_arcOffset;
+ return [ Math.cos(a) * r, Math.sin(a) * r ];
+ };
+ return arc;
+ };
+ var d3_svg_arcOffset = -halfπ, d3_svg_arcMax = τ - ε;
+ function d3_svg_arcInnerRadius(d) {
+ return d.innerRadius;
+ }
+ function d3_svg_arcOuterRadius(d) {
+ return d.outerRadius;
+ }
+ function d3_svg_arcStartAngle(d) {
+ return d.startAngle;
+ }
+ function d3_svg_arcEndAngle(d) {
+ return d.endAngle;
+ }
+ function d3_svg_line(projection) {
+ var x = d3_geom_pointX, y = d3_geom_pointY, defined = d3_true, interpolate = d3_svg_lineLinear, interpolateKey = interpolate.key, tension = .7;
+ function line(data) {
+ var segments = [], points = [], i = -1, n = data.length, d, fx = d3_functor(x), fy = d3_functor(y);
+ function segment() {
+ segments.push("M", interpolate(projection(points), tension));
+ }
+ while (++i < n) {
+ if (defined.call(this, d = data[i], i)) {
+ points.push([ +fx.call(this, d, i), +fy.call(this, d, i) ]);
+ } else if (points.length) {
+ segment();
+ points = [];
+ }
+ }
+ if (points.length) segment();
+ return segments.length ? segments.join("") : null;
+ }
+ line.x = function(_) {
+ if (!arguments.length) return x;
+ x = _;
+ return line;
+ };
+ line.y = function(_) {
+ if (!arguments.length) return y;
+ y = _;
+ return line;
+ };
+ line.defined = function(_) {
+ if (!arguments.length) return defined;
+ defined = _;
+ return line;
+ };
+ line.interpolate = function(_) {
+ if (!arguments.length) return interpolateKey;
+ if (typeof _ === "function") interpolateKey = interpolate = _; else interpolateKey = (interpolate = d3_svg_lineInterpolators.get(_) || d3_svg_lineLinear).key;
+ return line;
+ };
+ line.tension = function(_) {
+ if (!arguments.length) return tension;
+ tension = _;
+ return line;
+ };
+ return line;
+ }
+ d3.svg.line = function() {
+ return d3_svg_line(d3_identity);
+ };
+ var d3_svg_lineInterpolators = d3.map({
+ linear: d3_svg_lineLinear,
+ "linear-closed": d3_svg_lineLinearClosed,
+ step: d3_svg_lineStep,
+ "step-before": d3_svg_lineStepBefore,
+ "step-after": d3_svg_lineStepAfter,
+ basis: d3_svg_lineBasis,
+ "basis-open": d3_svg_lineBasisOpen,
+ "basis-closed": d3_svg_lineBasisClosed,
+ bundle: d3_svg_lineBundle,
+ cardinal: d3_svg_lineCardinal,
+ "cardinal-open": d3_svg_lineCardinalOpen,
+ "cardinal-closed": d3_svg_lineCardinalClosed,
+ monotone: d3_svg_lineMonotone
+ });
+ d3_svg_lineInterpolators.forEach(function(key, value) {
+ value.key = key;
+ value.closed = /-closed$/.test(key);
+ });
+ function d3_svg_lineLinear(points) {
+ return points.join("L");
+ }
+ function d3_svg_lineLinearClosed(points) {
+ return d3_svg_lineLinear(points) + "Z";
+ }
+ function d3_svg_lineStep(points) {
+ var i = 0, n = points.length, p = points[0], path = [ p[0], ",", p[1] ];
+ while (++i < n) path.push("H", (p[0] + (p = points[i])[0]) / 2, "V", p[1]);
+ if (n > 1) path.push("H", p[0]);
+ return path.join("");
+ }
+ function d3_svg_lineStepBefore(points) {
+ var i = 0, n = points.length, p = points[0], path = [ p[0], ",", p[1] ];
+ while (++i < n) path.push("V", (p = points[i])[1], "H", p[0]);
+ return path.join("");
+ }
+ function d3_svg_lineStepAfter(points) {
+ var i = 0, n = points.length, p = points[0], path = [ p[0], ",", p[1] ];
+ while (++i < n) path.push("H", (p = points[i])[0], "V", p[1]);
+ return path.join("");
+ }
+ function d3_svg_lineCardinalOpen(points, tension) {
+ return points.length < 4 ? d3_svg_lineLinear(points) : points[1] + d3_svg_lineHermite(points.slice(1, points.length - 1), d3_svg_lineCardinalTangents(points, tension));
+ }
+ function d3_svg_lineCardinalClosed(points, tension) {
+ return points.length < 3 ? d3_svg_lineLinear(points) : points[0] + d3_svg_lineHermite((points.push(points[0]),
+ points), d3_svg_lineCardinalTangents([ points[points.length - 2] ].concat(points, [ points[1] ]), tension));
+ }
+ function d3_svg_lineCardinal(points, tension) {
+ return points.length < 3 ? d3_svg_lineLinear(points) : points[0] + d3_svg_lineHermite(points, d3_svg_lineCardinalTangents(points, tension));
+ }
+ function d3_svg_lineHermite(points, tangents) {
+ if (tangents.length < 1 || points.length != tangents.length && points.length != tangents.length + 2) {
+ return d3_svg_lineLinear(points);
+ }
+ var quad = points.length != tangents.length, path = "", p0 = points[0], p = points[1], t0 = tangents[0], t = t0, pi = 1;
+ if (quad) {
+ path += "Q" + (p[0] - t0[0] * 2 / 3) + "," + (p[1] - t0[1] * 2 / 3) + "," + p[0] + "," + p[1];
+ p0 = points[1];
+ pi = 2;
+ }
+ if (tangents.length > 1) {
+ t = tangents[1];
+ p = points[pi];
+ pi++;
+ path += "C" + (p0[0] + t0[0]) + "," + (p0[1] + t0[1]) + "," + (p[0] - t[0]) + "," + (p[1] - t[1]) + "," + p[0] + "," + p[1];
+ for (var i = 2; i < tangents.length; i++, pi++) {
+ p = points[pi];
+ t = tangents[i];
+ path += "S" + (p[0] - t[0]) + "," + (p[1] - t[1]) + "," + p[0] + "," + p[1];
+ }
+ }
+ if (quad) {
+ var lp = points[pi];
+ path += "Q" + (p[0] + t[0] * 2 / 3) + "," + (p[1] + t[1] * 2 / 3) + "," + lp[0] + "," + lp[1];
+ }
+ return path;
+ }
+ function d3_svg_lineCardinalTangents(points, tension) {
+ var tangents = [], a = (1 - tension) / 2, p0, p1 = points[0], p2 = points[1], i = 1, n = points.length;
+ while (++i < n) {
+ p0 = p1;
+ p1 = p2;
+ p2 = points[i];
+ tangents.push([ a * (p2[0] - p0[0]), a * (p2[1] - p0[1]) ]);
+ }
+ return tangents;
+ }
+ function d3_svg_lineBasis(points) {
+ if (points.length < 3) return d3_svg_lineLinear(points);
+ var i = 1, n = points.length, pi = points[0], x0 = pi[0], y0 = pi[1], px = [ x0, x0, x0, (pi = points[1])[0] ], py = [ y0, y0, y0, pi[1] ], path = [ x0, ",", y0, "L", d3_svg_lineDot4(d3_svg_lineBasisBezier3, px), ",", d3_svg_lineDot4(d3_svg_lineBasisBezier3, py) ];
+ points.push(points[n - 1]);
+ while (++i <= n) {
+ pi = points[i];
+ px.shift();
+ px.push(pi[0]);
+ py.shift();
+ py.push(pi[1]);
+ d3_svg_lineBasisBezier(path, px, py);
+ }
+ points.pop();
+ path.push("L", pi);
+ return path.join("");
+ }
+ function d3_svg_lineBasisOpen(points) {
+ if (points.length < 4) return d3_svg_lineLinear(points);
+ var path = [], i = -1, n = points.length, pi, px = [ 0 ], py = [ 0 ];
+ while (++i < 3) {
+ pi = points[i];
+ px.push(pi[0]);
+ py.push(pi[1]);
+ }
+ path.push(d3_svg_lineDot4(d3_svg_lineBasisBezier3, px) + "," + d3_svg_lineDot4(d3_svg_lineBasisBezier3, py));
+ --i;
+ while (++i < n) {
+ pi = points[i];
+ px.shift();
+ px.push(pi[0]);
+ py.shift();
+ py.push(pi[1]);
+ d3_svg_lineBasisBezier(path, px, py);
+ }
+ return path.join("");
+ }
+ function d3_svg_lineBasisClosed(points) {
+ var path, i = -1, n = points.length, m = n + 4, pi, px = [], py = [];
+ while (++i < 4) {
+ pi = points[i % n];
+ px.push(pi[0]);
+ py.push(pi[1]);
+ }
+ path = [ d3_svg_lineDot4(d3_svg_lineBasisBezier3, px), ",", d3_svg_lineDot4(d3_svg_lineBasisBezier3, py) ];
+ --i;
+ while (++i < m) {
+ pi = points[i % n];
+ px.shift();
+ px.push(pi[0]);
+ py.shift();
+ py.push(pi[1]);
+ d3_svg_lineBasisBezier(path, px, py);
+ }
+ return path.join("");
+ }
+ function d3_svg_lineBundle(points, tension) {
+ var n = points.length - 1;
+ if (n) {
+ var x0 = points[0][0], y0 = points[0][1], dx = points[n][0] - x0, dy = points[n][1] - y0, i = -1, p, t;
+ while (++i <= n) {
+ p = points[i];
+ t = i / n;
+ p[0] = tension * p[0] + (1 - tension) * (x0 + t * dx);
+ p[1] = tension * p[1] + (1 - tension) * (y0 + t * dy);
+ }
+ }
+ return d3_svg_lineBasis(points);
+ }
+ function d3_svg_lineDot4(a, b) {
+ return a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3];
+ }
+ var d3_svg_lineBasisBezier1 = [ 0, 2 / 3, 1 / 3, 0 ], d3_svg_lineBasisBezier2 = [ 0, 1 / 3, 2 / 3, 0 ], d3_svg_lineBasisBezier3 = [ 0, 1 / 6, 2 / 3, 1 / 6 ];
+ function d3_svg_lineBasisBezier(path, x, y) {
+ path.push("C", d3_svg_lineDot4(d3_svg_lineBasisBezier1, x), ",", d3_svg_lineDot4(d3_svg_lineBasisBezier1, y), ",", d3_svg_lineDot4(d3_svg_lineBasisBezier2, x), ",", d3_svg_lineDot4(d3_svg_lineBasisBezier2, y), ",", d3_svg_lineDot4(d3_svg_lineBasisBezier3, x), ",", d3_svg_lineDot4(d3_svg_lineBasisBezier3, y));
+ }
+ function d3_svg_lineSlope(p0, p1) {
+ return (p1[1] - p0[1]) / (p1[0] - p0[0]);
+ }
+ function d3_svg_lineFiniteDifferences(points) {
+ var i = 0, j = points.length - 1, m = [], p0 = points[0], p1 = points[1], d = m[0] = d3_svg_lineSlope(p0, p1);
+ while (++i < j) {
+ m[i] = (d + (d = d3_svg_lineSlope(p0 = p1, p1 = points[i + 1]))) / 2;
+ }
+ m[i] = d;
+ return m;
+ }
+ function d3_svg_lineMonotoneTangents(points) {
+ var tangents = [], d, a, b, s, m = d3_svg_lineFiniteDifferences(points), i = -1, j = points.length - 1;
+ while (++i < j) {
+ d = d3_svg_lineSlope(points[i], points[i + 1]);
+ if (abs(d) < ε) {
+ m[i] = m[i + 1] = 0;
+ } else {
+ a = m[i] / d;
+ b = m[i + 1] / d;
+ s = a * a + b * b;
+ if (s > 9) {
+ s = d * 3 / Math.sqrt(s);
+ m[i] = s * a;
+ m[i + 1] = s * b;
+ }
+ }
+ }
+ i = -1;
+ while (++i <= j) {
+ s = (points[Math.min(j, i + 1)][0] - points[Math.max(0, i - 1)][0]) / (6 * (1 + m[i] * m[i]));
+ tangents.push([ s || 0, m[i] * s || 0 ]);
+ }
+ return tangents;
+ }
+ function d3_svg_lineMonotone(points) {
+ return points.length < 3 ? d3_svg_lineLinear(points) : points[0] + d3_svg_lineHermite(points, d3_svg_lineMonotoneTangents(points));
+ }
+ d3.svg.line.radial = function() {
+ var line = d3_svg_line(d3_svg_lineRadial);
+ line.radius = line.x, delete line.x;
+ line.angle = line.y, delete line.y;
+ return line;
+ };
+ function d3_svg_lineRadial(points) {
+ var point, i = -1, n = points.length, r, a;
+ while (++i < n) {
+ point = points[i];
+ r = point[0];
+ a = point[1] + d3_svg_arcOffset;
+ point[0] = r * Math.cos(a);
+ point[1] = r * Math.sin(a);
+ }
+ return points;
+ }
+ function d3_svg_area(projection) {
+ var x0 = d3_geom_pointX, x1 = d3_geom_pointX, y0 = 0, y1 = d3_geom_pointY, defined = d3_true, interpolate = d3_svg_lineLinear, interpolateKey = interpolate.key, interpolateReverse = interpolate, L = "L", tension = .7;
+ function area(data) {
+ var segments = [], points0 = [], points1 = [], i = -1, n = data.length, d, fx0 = d3_functor(x0), fy0 = d3_functor(y0), fx1 = x0 === x1 ? function() {
+ return x;
+ } : d3_functor(x1), fy1 = y0 === y1 ? function() {
+ return y;
+ } : d3_functor(y1), x, y;
+ function segment() {
+ segments.push("M", interpolate(projection(points1), tension), L, interpolateReverse(projection(points0.reverse()), tension), "Z");
+ }
+ while (++i < n) {
+ if (defined.call(this, d = data[i], i)) {
+ points0.push([ x = +fx0.call(this, d, i), y = +fy0.call(this, d, i) ]);
+ points1.push([ +fx1.call(this, d, i), +fy1.call(this, d, i) ]);
+ } else if (points0.length) {
+ segment();
+ points0 = [];
+ points1 = [];
+ }
+ }
+ if (points0.length) segment();
+ return segments.length ? segments.join("") : null;
+ }
+ area.x = function(_) {
+ if (!arguments.length) return x1;
+ x0 = x1 = _;
+ return area;
+ };
+ area.x0 = function(_) {
+ if (!arguments.length) return x0;
+ x0 = _;
+ return area;
+ };
+ area.x1 = function(_) {
+ if (!arguments.length) return x1;
+ x1 = _;
+ return area;
+ };
+ area.y = function(_) {
+ if (!arguments.length) return y1;
+ y0 = y1 = _;
+ return area;
+ };
+ area.y0 = function(_) {
+ if (!arguments.length) return y0;
+ y0 = _;
+ return area;
+ };
+ area.y1 = function(_) {
+ if (!arguments.length) return y1;
+ y1 = _;
+ return area;
+ };
+ area.defined = function(_) {
+ if (!arguments.length) return defined;
+ defined = _;
+ return area;
+ };
+ area.interpolate = function(_) {
+ if (!arguments.length) return interpolateKey;
+ if (typeof _ === "function") interpolateKey = interpolate = _; else interpolateKey = (interpolate = d3_svg_lineInterpolators.get(_) || d3_svg_lineLinear).key;
+ interpolateReverse = interpolate.reverse || interpolate;
+ L = interpolate.closed ? "M" : "L";
+ return area;
+ };
+ area.tension = function(_) {
+ if (!arguments.length) return tension;
+ tension = _;
+ return area;
+ };
+ return area;
+ }
+ d3_svg_lineStepBefore.reverse = d3_svg_lineStepAfter;
+ d3_svg_lineStepAfter.reverse = d3_svg_lineStepBefore;
+ d3.svg.area = function() {
+ return d3_svg_area(d3_identity);
+ };
+ d3.svg.area.radial = function() {
+ var area = d3_svg_area(d3_svg_lineRadial);
+ area.radius = area.x, delete area.x;
+ area.innerRadius = area.x0, delete area.x0;
+ area.outerRadius = area.x1, delete area.x1;
+ area.angle = area.y, delete area.y;
+ area.startAngle = area.y0, delete area.y0;
+ area.endAngle = area.y1, delete area.y1;
+ return area;
+ };
+ d3.svg.chord = function() {
+ var source = d3_source, target = d3_target, radius = d3_svg_chordRadius, startAngle = d3_svg_arcStartAngle, endAngle = d3_svg_arcEndAngle;
+ function chord(d, i) {
+ var s = subgroup(this, source, d, i), t = subgroup(this, target, d, i);
+ return "M" + s.p0 + arc(s.r, s.p1, s.a1 - s.a0) + (equals(s, t) ? curve(s.r, s.p1, s.r, s.p0) : curve(s.r, s.p1, t.r, t.p0) + arc(t.r, t.p1, t.a1 - t.a0) + curve(t.r, t.p1, s.r, s.p0)) + "Z";
+ }
+ function subgroup(self, f, d, i) {
+ var subgroup = f.call(self, d, i), r = radius.call(self, subgroup, i), a0 = startAngle.call(self, subgroup, i) + d3_svg_arcOffset, a1 = endAngle.call(self, subgroup, i) + d3_svg_arcOffset;
+ return {
+ r: r,
+ a0: a0,
+ a1: a1,
+ p0: [ r * Math.cos(a0), r * Math.sin(a0) ],
+ p1: [ r * Math.cos(a1), r * Math.sin(a1) ]
+ };
+ }
+ function equals(a, b) {
+ return a.a0 == b.a0 && a.a1 == b.a1;
+ }
+ function arc(r, p, a) {
+ return "A" + r + "," + r + " 0 " + +(a > π) + ",1 " + p;
+ }
+ function curve(r0, p0, r1, p1) {
+ return "Q 0,0 " + p1;
+ }
+ chord.radius = function(v) {
+ if (!arguments.length) return radius;
+ radius = d3_functor(v);
+ return chord;
+ };
+ chord.source = function(v) {
+ if (!arguments.length) return source;
+ source = d3_functor(v);
+ return chord;
+ };
+ chord.target = function(v) {
+ if (!arguments.length) return target;
+ target = d3_functor(v);
+ return chord;
+ };
+ chord.startAngle = function(v) {
+ if (!arguments.length) return startAngle;
+ startAngle = d3_functor(v);
+ return chord;
+ };
+ chord.endAngle = function(v) {
+ if (!arguments.length) return endAngle;
+ endAngle = d3_functor(v);
+ return chord;
+ };
+ return chord;
+ };
+ function d3_svg_chordRadius(d) {
+ return d.radius;
+ }
+ d3.svg.diagonal = function() {
+ var source = d3_source, target = d3_target, projection = d3_svg_diagonalProjection;
+ function diagonal(d, i) {
+ var p0 = source.call(this, d, i), p3 = target.call(this, d, i), m = (p0.y + p3.y) / 2, p = [ p0, {
+ x: p0.x,
+ y: m
+ }, {
+ x: p3.x,
+ y: m
+ }, p3 ];
+ p = p.map(projection);
+ return "M" + p[0] + "C" + p[1] + " " + p[2] + " " + p[3];
+ }
+ diagonal.source = function(x) {
+ if (!arguments.length) return source;
+ source = d3_functor(x);
+ return diagonal;
+ };
+ diagonal.target = function(x) {
+ if (!arguments.length) return target;
+ target = d3_functor(x);
+ return diagonal;
+ };
+ diagonal.projection = function(x) {
+ if (!arguments.length) return projection;
+ projection = x;
+ return diagonal;
+ };
+ return diagonal;
+ };
+ function d3_svg_diagonalProjection(d) {
+ return [ d.x, d.y ];
+ }
+ d3.svg.diagonal.radial = function() {
+ var diagonal = d3.svg.diagonal(), projection = d3_svg_diagonalProjection, projection_ = diagonal.projection;
+ diagonal.projection = function(x) {
+ return arguments.length ? projection_(d3_svg_diagonalRadialProjection(projection = x)) : projection;
+ };
+ return diagonal;
+ };
+ function d3_svg_diagonalRadialProjection(projection) {
+ return function() {
+ var d = projection.apply(this, arguments), r = d[0], a = d[1] + d3_svg_arcOffset;
+ return [ r * Math.cos(a), r * Math.sin(a) ];
+ };
+ }
+ d3.svg.symbol = function() {
+ var type = d3_svg_symbolType, size = d3_svg_symbolSize;
+ function symbol(d, i) {
+ return (d3_svg_symbols.get(type.call(this, d, i)) || d3_svg_symbolCircle)(size.call(this, d, i));
+ }
+ symbol.type = function(x) {
+ if (!arguments.length) return type;
+ type = d3_functor(x);
+ return symbol;
+ };
+ symbol.size = function(x) {
+ if (!arguments.length) return size;
+ size = d3_functor(x);
+ return symbol;
+ };
+ return symbol;
+ };
+ function d3_svg_symbolSize() {
+ return 64;
+ }
+ function d3_svg_symbolType() {
+ return "circle";
+ }
+ function d3_svg_symbolCircle(size) {
+ var r = Math.sqrt(size / π);
+ return "M0," + r + "A" + r + "," + r + " 0 1,1 0," + -r + "A" + r + "," + r + " 0 1,1 0," + r + "Z";
+ }
+ var d3_svg_symbols = d3.map({
+ circle: d3_svg_symbolCircle,
+ cross: function(size) {
+ var r = Math.sqrt(size / 5) / 2;
+ return "M" + -3 * r + "," + -r + "H" + -r + "V" + -3 * r + "H" + r + "V" + -r + "H" + 3 * r + "V" + r + "H" + r + "V" + 3 * r + "H" + -r + "V" + r + "H" + -3 * r + "Z";
+ },
+ diamond: function(size) {
+ var ry = Math.sqrt(size / (2 * d3_svg_symbolTan30)), rx = ry * d3_svg_symbolTan30;
+ return "M0," + -ry + "L" + rx + ",0" + " 0," + ry + " " + -rx + ",0" + "Z";
+ },
+ square: function(size) {
+ var r = Math.sqrt(size) / 2;
+ return "M" + -r + "," + -r + "L" + r + "," + -r + " " + r + "," + r + " " + -r + "," + r + "Z";
+ },
+ "triangle-down": function(size) {
+ var rx = Math.sqrt(size / d3_svg_symbolSqrt3), ry = rx * d3_svg_symbolSqrt3 / 2;
+ return "M0," + ry + "L" + rx + "," + -ry + " " + -rx + "," + -ry + "Z";
+ },
+ "triangle-up": function(size) {
+ var rx = Math.sqrt(size / d3_svg_symbolSqrt3), ry = rx * d3_svg_symbolSqrt3 / 2;
+ return "M0," + -ry + "L" + rx + "," + ry + " " + -rx + "," + ry + "Z";
+ }
+ });
+ d3.svg.symbolTypes = d3_svg_symbols.keys();
+ var d3_svg_symbolSqrt3 = Math.sqrt(3), d3_svg_symbolTan30 = Math.tan(30 * d3_radians);
+ function d3_transition(groups, id) {
+ d3_subclass(groups, d3_transitionPrototype);
+ groups.id = id;
+ return groups;
+ }
+ var d3_transitionPrototype = [], d3_transitionId = 0, d3_transitionInheritId, d3_transitionInherit;
+ d3_transitionPrototype.call = d3_selectionPrototype.call;
+ d3_transitionPrototype.empty = d3_selectionPrototype.empty;
+ d3_transitionPrototype.node = d3_selectionPrototype.node;
+ d3_transitionPrototype.size = d3_selectionPrototype.size;
+ d3.transition = function(selection) {
+ return arguments.length ? d3_transitionInheritId ? selection.transition() : selection : d3_selectionRoot.transition();
+ };
+ d3.transition.prototype = d3_transitionPrototype;
+ d3_transitionPrototype.select = function(selector) {
+ var id = this.id, subgroups = [], subgroup, subnode, node;
+ selector = d3_selection_selector(selector);
+ for (var j = -1, m = this.length; ++j < m; ) {
+ subgroups.push(subgroup = []);
+ for (var group = this[j], i = -1, n = group.length; ++i < n; ) {
+ if ((node = group[i]) && (subnode = selector.call(node, node.__data__, i, j))) {
+ if ("__data__" in node) subnode.__data__ = node.__data__;
+ d3_transitionNode(subnode, i, id, node.__transition__[id]);
+ subgroup.push(subnode);
+ } else {
+ subgroup.push(null);
+ }
+ }
+ }
+ return d3_transition(subgroups, id);
+ };
+ d3_transitionPrototype.selectAll = function(selector) {
+ var id = this.id, subgroups = [], subgroup, subnodes, node, subnode, transition;
+ selector = d3_selection_selectorAll(selector);
+ for (var j = -1, m = this.length; ++j < m; ) {
+ for (var group = this[j], i = -1, n = group.length; ++i < n; ) {
+ if (node = group[i]) {
+ transition = node.__transition__[id];
+ subnodes = selector.call(node, node.__data__, i, j);
+ subgroups.push(subgroup = []);
+ for (var k = -1, o = subnodes.length; ++k < o; ) {
+ if (subnode = subnodes[k]) d3_transitionNode(subnode, k, id, transition);
+ subgroup.push(subnode);
+ }
+ }
+ }
+ }
+ return d3_transition(subgroups, id);
+ };
+ d3_transitionPrototype.filter = function(filter) {
+ var subgroups = [], subgroup, group, node;
+ if (typeof filter !== "function") filter = d3_selection_filter(filter);
+ for (var j = 0, m = this.length; j < m; j++) {
+ subgroups.push(subgroup = []);
+ for (var group = this[j], i = 0, n = group.length; i < n; i++) {
+ if ((node = group[i]) && filter.call(node, node.__data__, i, j)) {
+ subgroup.push(node);
+ }
+ }
+ }
+ return d3_transition(subgroups, this.id);
+ };
+ d3_transitionPrototype.tween = function(name, tween) {
+ var id = this.id;
+ if (arguments.length < 2) return this.node().__transition__[id].tween.get(name);
+ return d3_selection_each(this, tween == null ? function(node) {
+ node.__transition__[id].tween.remove(name);
+ } : function(node) {
+ node.__transition__[id].tween.set(name, tween);
+ });
+ };
+ function d3_transition_tween(groups, name, value, tween) {
+ var id = groups.id;
+ return d3_selection_each(groups, typeof value === "function" ? function(node, i, j) {
+ node.__transition__[id].tween.set(name, tween(value.call(node, node.__data__, i, j)));
+ } : (value = tween(value), function(node) {
+ node.__transition__[id].tween.set(name, value);
+ }));
+ }
+ d3_transitionPrototype.attr = function(nameNS, value) {
+ if (arguments.length < 2) {
+ for (value in nameNS) this.attr(value, nameNS[value]);
+ return this;
+ }
+ var interpolate = nameNS == "transform" ? d3_interpolateTransform : d3_interpolate, name = d3.ns.qualify(nameNS);
+ function attrNull() {
+ this.removeAttribute(name);
+ }
+ function attrNullNS() {
+ this.removeAttributeNS(name.space, name.local);
+ }
+ function attrTween(b) {
+ return b == null ? attrNull : (b += "", function() {
+ var a = this.getAttribute(name), i;
+ return a !== b && (i = interpolate(a, b), function(t) {
+ this.setAttribute(name, i(t));
+ });
+ });
+ }
+ function attrTweenNS(b) {
+ return b == null ? attrNullNS : (b += "", function() {
+ var a = this.getAttributeNS(name.space, name.local), i;
+ return a !== b && (i = interpolate(a, b), function(t) {
+ this.setAttributeNS(name.space, name.local, i(t));
+ });
+ });
+ }
+ return d3_transition_tween(this, "attr." + nameNS, value, name.local ? attrTweenNS : attrTween);
+ };
+ d3_transitionPrototype.attrTween = function(nameNS, tween) {
+ var name = d3.ns.qualify(nameNS);
+ function attrTween(d, i) {
+ var f = tween.call(this, d, i, this.getAttribute(name));
+ return f && function(t) {
+ this.setAttribute(name, f(t));
+ };
+ }
+ function attrTweenNS(d, i) {
+ var f = tween.call(this, d, i, this.getAttributeNS(name.space, name.local));
+ return f && function(t) {
+ this.setAttributeNS(name.space, name.local, f(t));
+ };
+ }
+ return this.tween("attr." + nameNS, name.local ? attrTweenNS : attrTween);
+ };
+ d3_transitionPrototype.style = function(name, value, priority) {
+ var n = arguments.length;
+ if (n < 3) {
+ if (typeof name !== "string") {
+ if (n < 2) value = "";
+ for (priority in name) this.style(priority, name[priority], value);
+ return this;
+ }
+ priority = "";
+ }
+ function styleNull() {
+ this.style.removeProperty(name);
+ }
+ function styleString(b) {
+ return b == null ? styleNull : (b += "", function() {
+ var a = d3_window.getComputedStyle(this, null).getPropertyValue(name), i;
+ return a !== b && (i = d3_interpolate(a, b), function(t) {
+ this.style.setProperty(name, i(t), priority);
+ });
+ });
+ }
+ return d3_transition_tween(this, "style." + name, value, styleString);
+ };
+ d3_transitionPrototype.styleTween = function(name, tween, priority) {
+ if (arguments.length < 3) priority = "";
+ function styleTween(d, i) {
+ var f = tween.call(this, d, i, d3_window.getComputedStyle(this, null).getPropertyValue(name));
+ return f && function(t) {
+ this.style.setProperty(name, f(t), priority);
+ };
+ }
+ return this.tween("style." + name, styleTween);
+ };
+ d3_transitionPrototype.text = function(value) {
+ return d3_transition_tween(this, "text", value, d3_transition_text);
+ };
+ function d3_transition_text(b) {
+ if (b == null) b = "";
+ return function() {
+ this.textContent = b;
+ };
+ }
+ d3_transitionPrototype.remove = function() {
+ return this.each("end.transition", function() {
+ var p;
+ if (this.__transition__.count < 2 && (p = this.parentNode)) p.removeChild(this);
+ });
+ };
+ d3_transitionPrototype.ease = function(value) {
+ var id = this.id;
+ if (arguments.length < 1) return this.node().__transition__[id].ease;
+ if (typeof value !== "function") value = d3.ease.apply(d3, arguments);
+ return d3_selection_each(this, function(node) {
+ node.__transition__[id].ease = value;
+ });
+ };
+ d3_transitionPrototype.delay = function(value) {
+ var id = this.id;
+ return d3_selection_each(this, typeof value === "function" ? function(node, i, j) {
+ node.__transition__[id].delay = +value.call(node, node.__data__, i, j);
+ } : (value = +value, function(node) {
+ node.__transition__[id].delay = value;
+ }));
+ };
+ d3_transitionPrototype.duration = function(value) {
+ var id = this.id;
+ return d3_selection_each(this, typeof value === "function" ? function(node, i, j) {
+ node.__transition__[id].duration = Math.max(1, value.call(node, node.__data__, i, j));
+ } : (value = Math.max(1, value), function(node) {
+ node.__transition__[id].duration = value;
+ }));
+ };
+ d3_transitionPrototype.each = function(type, listener) {
+ var id = this.id;
+ if (arguments.length < 2) {
+ var inherit = d3_transitionInherit, inheritId = d3_transitionInheritId;
+ d3_transitionInheritId = id;
+ d3_selection_each(this, function(node, i, j) {
+ d3_transitionInherit = node.__transition__[id];
+ type.call(node, node.__data__, i, j);
+ });
+ d3_transitionInherit = inherit;
+ d3_transitionInheritId = inheritId;
+ } else {
+ d3_selection_each(this, function(node) {
+ var transition = node.__transition__[id];
+ (transition.event || (transition.event = d3.dispatch("start", "end"))).on(type, listener);
+ });
+ }
+ return this;
+ };
+ d3_transitionPrototype.transition = function() {
+ var id0 = this.id, id1 = ++d3_transitionId, subgroups = [], subgroup, group, node, transition;
+ for (var j = 0, m = this.length; j < m; j++) {
+ subgroups.push(subgroup = []);
+ for (var group = this[j], i = 0, n = group.length; i < n; i++) {
+ if (node = group[i]) {
+ transition = Object.create(node.__transition__[id0]);
+ transition.delay += transition.duration;
+ d3_transitionNode(node, i, id1, transition);
+ }
+ subgroup.push(node);
+ }
+ }
+ return d3_transition(subgroups, id1);
+ };
+ function d3_transitionNode(node, i, id, inherit) {
+ var lock = node.__transition__ || (node.__transition__ = {
+ active: 0,
+ count: 0
+ }), transition = lock[id];
+ if (!transition) {
+ var time = inherit.time;
+ transition = lock[id] = {
+ tween: new d3_Map(),
+ time: time,
+ ease: inherit.ease,
+ delay: inherit.delay,
+ duration: inherit.duration
+ };
+ ++lock.count;
+ d3.timer(function(elapsed) {
+ var d = node.__data__, ease = transition.ease, delay = transition.delay, duration = transition.duration, timer = d3_timer_active, tweened = [];
+ timer.t = delay + time;
+ if (delay <= elapsed) return start(elapsed - delay);
+ timer.c = start;
+ function start(elapsed) {
+ if (lock.active > id) return stop();
+ lock.active = id;
+ transition.event && transition.event.start.call(node, d, i);
+ transition.tween.forEach(function(key, value) {
+ if (value = value.call(node, d, i)) {
+ tweened.push(value);
+ }
+ });
+ d3.timer(function() {
+ timer.c = tick(elapsed || 1) ? d3_true : tick;
+ return 1;
+ }, 0, time);
+ }
+ function tick(elapsed) {
+ if (lock.active !== id) return stop();
+ var t = elapsed / duration, e = ease(t), n = tweened.length;
+ while (n > 0) {
+ tweened[--n].call(node, e);
+ }
+ if (t >= 1) {
+ transition.event && transition.event.end.call(node, d, i);
+ return stop();
+ }
+ }
+ function stop() {
+ if (--lock.count) delete lock[id]; else delete node.__transition__;
+ return 1;
+ }
+ }, 0, time);
+ }
+ }
+ d3.svg.axis = function() {
+ var scale = d3.scale.linear(), orient = d3_svg_axisDefaultOrient, innerTickSize = 6, outerTickSize = 6, tickPadding = 3, tickArguments_ = [ 10 ], tickValues = null, tickFormat_;
+ function axis(g) {
+ g.each(function() {
+ var g = d3.select(this);
+ var scale0 = this.__chart__ || scale, scale1 = this.__chart__ = scale.copy();
+ var ticks = tickValues == null ? scale1.ticks ? scale1.ticks.apply(scale1, tickArguments_) : scale1.domain() : tickValues, tickFormat = tickFormat_ == null ? scale1.tickFormat ? scale1.tickFormat.apply(scale1, tickArguments_) : d3_identity : tickFormat_, tick = g.selectAll(".tick").data(ticks, scale1), tickEnter = tick.enter().insert("g", ".domain").attr("class", "tick").style("opacity", ε), tickExit = d3.transition(tick.exit()).style("opacity", ε).remove(), tickUpdate = d3.transition(tick).style("opacity", 1), tickTransform;
+ var range = d3_scaleRange(scale1), path = g.selectAll(".domain").data([ 0 ]), pathUpdate = (path.enter().append("path").attr("class", "domain"),
+ d3.transition(path));
+ tickEnter.append("line");
+ tickEnter.append("text");
+ var lineEnter = tickEnter.select("line"), lineUpdate = tickUpdate.select("line"), text = tick.select("text").text(tickFormat), textEnter = tickEnter.select("text"), textUpdate = tickUpdate.select("text");
+ switch (orient) {
+ case "bottom":
+ {
+ tickTransform = d3_svg_axisX;
+ lineEnter.attr("y2", innerTickSize);
+ textEnter.attr("y", Math.max(innerTickSize, 0) + tickPadding);
+ lineUpdate.attr("x2", 0).attr("y2", innerTickSize);
+ textUpdate.attr("x", 0).attr("y", Math.max(innerTickSize, 0) + tickPadding);
+ text.attr("dy", ".71em").style("text-anchor", "middle");
+ pathUpdate.attr("d", "M" + range[0] + "," + outerTickSize + "V0H" + range[1] + "V" + outerTickSize);
+ break;
+ }
+
+ case "top":
+ {
+ tickTransform = d3_svg_axisX;
+ lineEnter.attr("y2", -innerTickSize);
+ textEnter.attr("y", -(Math.max(innerTickSize, 0) + tickPadding));
+ lineUpdate.attr("x2", 0).attr("y2", -innerTickSize);
+ textUpdate.attr("x", 0).attr("y", -(Math.max(innerTickSize, 0) + tickPadding));
+ text.attr("dy", "0em").style("text-anchor", "middle");
+ pathUpdate.attr("d", "M" + range[0] + "," + -outerTickSize + "V0H" + range[1] + "V" + -outerTickSize);
+ break;
+ }
+
+ case "left":
+ {
+ tickTransform = d3_svg_axisY;
+ lineEnter.attr("x2", -innerTickSize);
+ textEnter.attr("x", -(Math.max(innerTickSize, 0) + tickPadding));
+ lineUpdate.attr("x2", -innerTickSize).attr("y2", 0);
+ textUpdate.attr("x", -(Math.max(innerTickSize, 0) + tickPadding)).attr("y", 0);
+ text.attr("dy", ".32em").style("text-anchor", "end");
+ pathUpdate.attr("d", "M" + -outerTickSize + "," + range[0] + "H0V" + range[1] + "H" + -outerTickSize);
+ break;
+ }
+
+ case "right":
+ {
+ tickTransform = d3_svg_axisY;
+ lineEnter.attr("x2", innerTickSize);
+ textEnter.attr("x", Math.max(innerTickSize, 0) + tickPadding);
+ lineUpdate.attr("x2", innerTickSize).attr("y2", 0);
+ textUpdate.attr("x", Math.max(innerTickSize, 0) + tickPadding).attr("y", 0);
+ text.attr("dy", ".32em").style("text-anchor", "start");
+ pathUpdate.attr("d", "M" + outerTickSize + "," + range[0] + "H0V" + range[1] + "H" + outerTickSize);
+ break;
+ }
+ }
+ if (scale1.rangeBand) {
+ var x = scale1, dx = x.rangeBand() / 2;
+ scale0 = scale1 = function(d) {
+ return x(d) + dx;
+ };
+ } else if (scale0.rangeBand) {
+ scale0 = scale1;
+ } else {
+ tickExit.call(tickTransform, scale1);
+ }
+ tickEnter.call(tickTransform, scale0);
+ tickUpdate.call(tickTransform, scale1);
+ });
+ }
+ axis.scale = function(x) {
+ if (!arguments.length) return scale;
+ scale = x;
+ return axis;
+ };
+ axis.orient = function(x) {
+ if (!arguments.length) return orient;
+ orient = x in d3_svg_axisOrients ? x + "" : d3_svg_axisDefaultOrient;
+ return axis;
+ };
+ axis.ticks = function() {
+ if (!arguments.length) return tickArguments_;
+ tickArguments_ = arguments;
+ return axis;
+ };
+ axis.tickValues = function(x) {
+ if (!arguments.length) return tickValues;
+ tickValues = x;
+ return axis;
+ };
+ axis.tickFormat = function(x) {
+ if (!arguments.length) return tickFormat_;
+ tickFormat_ = x;
+ return axis;
+ };
+ axis.tickSize = function(x) {
+ var n = arguments.length;
+ if (!n) return innerTickSize;
+ innerTickSize = +x;
+ outerTickSize = +arguments[n - 1];
+ return axis;
+ };
+ axis.innerTickSize = function(x) {
+ if (!arguments.length) return innerTickSize;
+ innerTickSize = +x;
+ return axis;
+ };
+ axis.outerTickSize = function(x) {
+ if (!arguments.length) return outerTickSize;
+ outerTickSize = +x;
+ return axis;
+ };
+ axis.tickPadding = function(x) {
+ if (!arguments.length) return tickPadding;
+ tickPadding = +x;
+ return axis;
+ };
+ axis.tickSubdivide = function() {
+ return arguments.length && axis;
+ };
+ return axis;
+ };
+ var d3_svg_axisDefaultOrient = "bottom", d3_svg_axisOrients = {
+ top: 1,
+ right: 1,
+ bottom: 1,
+ left: 1
+ };
+ function d3_svg_axisX(selection, x) {
+ selection.attr("transform", function(d) {
+ return "translate(" + x(d) + ",0)";
+ });
+ }
+ function d3_svg_axisY(selection, y) {
+ selection.attr("transform", function(d) {
+ return "translate(0," + y(d) + ")";
+ });
+ }
+ d3.svg.brush = function() {
+ var event = d3_eventDispatch(brush, "brushstart", "brush", "brushend"), x = null, y = null, xExtent = [ 0, 0 ], yExtent = [ 0, 0 ], xExtentDomain, yExtentDomain, xClamp = true, yClamp = true, resizes = d3_svg_brushResizes[0];
+ function brush(g) {
+ g.each(function() {
+ var g = d3.select(this).style("pointer-events", "all").style("-webkit-tap-highlight-color", "rgba(0,0,0,0)").on("mousedown.brush", brushstart).on("touchstart.brush", brushstart);
+ var background = g.selectAll(".background").data([ 0 ]);
+ background.enter().append("rect").attr("class", "background").style("visibility", "hidden").style("cursor", "crosshair");
+ g.selectAll(".extent").data([ 0 ]).enter().append("rect").attr("class", "extent").style("cursor", "move");
+ var resize = g.selectAll(".resize").data(resizes, d3_identity);
+ resize.exit().remove();
+ resize.enter().append("g").attr("class", function(d) {
+ return "resize " + d;
+ }).style("cursor", function(d) {
+ return d3_svg_brushCursor[d];
+ }).append("rect").attr("x", function(d) {
+ return /[ew]$/.test(d) ? -3 : null;
+ }).attr("y", function(d) {
+ return /^[ns]/.test(d) ? -3 : null;
+ }).attr("width", 6).attr("height", 6).style("visibility", "hidden");
+ resize.style("display", brush.empty() ? "none" : null);
+ var gUpdate = d3.transition(g), backgroundUpdate = d3.transition(background), range;
+ if (x) {
+ range = d3_scaleRange(x);
+ backgroundUpdate.attr("x", range[0]).attr("width", range[1] - range[0]);
+ redrawX(gUpdate);
+ }
+ if (y) {
+ range = d3_scaleRange(y);
+ backgroundUpdate.attr("y", range[0]).attr("height", range[1] - range[0]);
+ redrawY(gUpdate);
+ }
+ redraw(gUpdate);
+ });
+ }
+ brush.event = function(g) {
+ g.each(function() {
+ var event_ = event.of(this, arguments), extent1 = {
+ x: xExtent,
+ y: yExtent,
+ i: xExtentDomain,
+ j: yExtentDomain
+ }, extent0 = this.__chart__ || extent1;
+ this.__chart__ = extent1;
+ if (d3_transitionInheritId) {
+ d3.select(this).transition().each("start.brush", function() {
+ xExtentDomain = extent0.i;
+ yExtentDomain = extent0.j;
+ xExtent = extent0.x;
+ yExtent = extent0.y;
+ event_({
+ type: "brushstart"
+ });
+ }).tween("brush:brush", function() {
+ var xi = d3_interpolateArray(xExtent, extent1.x), yi = d3_interpolateArray(yExtent, extent1.y);
+ xExtentDomain = yExtentDomain = null;
+ return function(t) {
+ xExtent = extent1.x = xi(t);
+ yExtent = extent1.y = yi(t);
+ event_({
+ type: "brush",
+ mode: "resize"
+ });
+ };
+ }).each("end.brush", function() {
+ xExtentDomain = extent1.i;
+ yExtentDomain = extent1.j;
+ event_({
+ type: "brush",
+ mode: "resize"
+ });
+ event_({
+ type: "brushend"
+ });
+ });
+ } else {
+ event_({
+ type: "brushstart"
+ });
+ event_({
+ type: "brush",
+ mode: "resize"
+ });
+ event_({
+ type: "brushend"
+ });
+ }
+ });
+ };
+ function redraw(g) {
+ g.selectAll(".resize").attr("transform", function(d) {
+ return "translate(" + xExtent[+/e$/.test(d)] + "," + yExtent[+/^s/.test(d)] + ")";
+ });
+ }
+ function redrawX(g) {
+ g.select(".extent").attr("x", xExtent[0]);
+ g.selectAll(".extent,.n>rect,.s>rect").attr("width", xExtent[1] - xExtent[0]);
+ }
+ function redrawY(g) {
+ g.select(".extent").attr("y", yExtent[0]);
+ g.selectAll(".extent,.e>rect,.w>rect").attr("height", yExtent[1] - yExtent[0]);
+ }
+ function brushstart() {
+ var target = this, eventTarget = d3.select(d3.event.target), event_ = event.of(target, arguments), g = d3.select(target), resizing = eventTarget.datum(), resizingX = !/^(n|s)$/.test(resizing) && x, resizingY = !/^(e|w)$/.test(resizing) && y, dragging = eventTarget.classed("extent"), dragRestore = d3_event_dragSuppress(), center, origin = d3.mouse(target), offset;
+ var w = d3.select(d3_window).on("keydown.brush", keydown).on("keyup.brush", keyup);
+ if (d3.event.changedTouches) {
+ w.on("touchmove.brush", brushmove).on("touchend.brush", brushend);
+ } else {
+ w.on("mousemove.brush", brushmove).on("mouseup.brush", brushend);
+ }
+ g.interrupt().selectAll("*").interrupt();
+ if (dragging) {
+ origin[0] = xExtent[0] - origin[0];
+ origin[1] = yExtent[0] - origin[1];
+ } else if (resizing) {
+ var ex = +/w$/.test(resizing), ey = +/^n/.test(resizing);
+ offset = [ xExtent[1 - ex] - origin[0], yExtent[1 - ey] - origin[1] ];
+ origin[0] = xExtent[ex];
+ origin[1] = yExtent[ey];
+ } else if (d3.event.altKey) center = origin.slice();
+ g.style("pointer-events", "none").selectAll(".resize").style("display", null);
+ d3.select("body").style("cursor", eventTarget.style("cursor"));
+ event_({
+ type: "brushstart"
+ });
+ brushmove();
+ function keydown() {
+ if (d3.event.keyCode == 32) {
+ if (!dragging) {
+ center = null;
+ origin[0] -= xExtent[1];
+ origin[1] -= yExtent[1];
+ dragging = 2;
+ }
+ d3_eventPreventDefault();
+ }
+ }
+ function keyup() {
+ if (d3.event.keyCode == 32 && dragging == 2) {
+ origin[0] += xExtent[1];
+ origin[1] += yExtent[1];
+ dragging = 0;
+ d3_eventPreventDefault();
+ }
+ }
+ function brushmove() {
+ var point = d3.mouse(target), moved = false;
+ if (offset) {
+ point[0] += offset[0];
+ point[1] += offset[1];
+ }
+ if (!dragging) {
+ if (d3.event.altKey) {
+ if (!center) center = [ (xExtent[0] + xExtent[1]) / 2, (yExtent[0] + yExtent[1]) / 2 ];
+ origin[0] = xExtent[+(point[0] < center[0])];
+ origin[1] = yExtent[+(point[1] < center[1])];
+ } else center = null;
+ }
+ if (resizingX && move1(point, x, 0)) {
+ redrawX(g);
+ moved = true;
+ }
+ if (resizingY && move1(point, y, 1)) {
+ redrawY(g);
+ moved = true;
+ }
+ if (moved) {
+ redraw(g);
+ event_({
+ type: "brush",
+ mode: dragging ? "move" : "resize"
+ });
+ }
+ }
+ function move1(point, scale, i) {
+ var range = d3_scaleRange(scale), r0 = range[0], r1 = range[1], position = origin[i], extent = i ? yExtent : xExtent, size = extent[1] - extent[0], min, max;
+ if (dragging) {
+ r0 -= position;
+ r1 -= size + position;
+ }
+ min = (i ? yClamp : xClamp) ? Math.max(r0, Math.min(r1, point[i])) : point[i];
+ if (dragging) {
+ max = (min += position) + size;
+ } else {
+ if (center) position = Math.max(r0, Math.min(r1, 2 * center[i] - min));
+ if (position < min) {
+ max = min;
+ min = position;
+ } else {
+ max = position;
+ }
+ }
+ if (extent[0] != min || extent[1] != max) {
+ if (i) yExtentDomain = null; else xExtentDomain = null;
+ extent[0] = min;
+ extent[1] = max;
+ return true;
+ }
+ }
+ function brushend() {
+ brushmove();
+ g.style("pointer-events", "all").selectAll(".resize").style("display", brush.empty() ? "none" : null);
+ d3.select("body").style("cursor", null);
+ w.on("mousemove.brush", null).on("mouseup.brush", null).on("touchmove.brush", null).on("touchend.brush", null).on("keydown.brush", null).on("keyup.brush", null);
+ dragRestore();
+ event_({
+ type: "brushend"
+ });
+ }
+ }
+ brush.x = function(z) {
+ if (!arguments.length) return x;
+ x = z;
+ resizes = d3_svg_brushResizes[!x << 1 | !y];
+ return brush;
+ };
+ brush.y = function(z) {
+ if (!arguments.length) return y;
+ y = z;
+ resizes = d3_svg_brushResizes[!x << 1 | !y];
+ return brush;
+ };
+ brush.clamp = function(z) {
+ if (!arguments.length) return x && y ? [ xClamp, yClamp ] : x ? xClamp : y ? yClamp : null;
+ if (x && y) xClamp = !!z[0], yClamp = !!z[1]; else if (x) xClamp = !!z; else if (y) yClamp = !!z;
+ return brush;
+ };
+ brush.extent = function(z) {
+ var x0, x1, y0, y1, t;
+ if (!arguments.length) {
+ if (x) {
+ if (xExtentDomain) {
+ x0 = xExtentDomain[0], x1 = xExtentDomain[1];
+ } else {
+ x0 = xExtent[0], x1 = xExtent[1];
+ if (x.invert) x0 = x.invert(x0), x1 = x.invert(x1);
+ if (x1 < x0) t = x0, x0 = x1, x1 = t;
+ }
+ }
+ if (y) {
+ if (yExtentDomain) {
+ y0 = yExtentDomain[0], y1 = yExtentDomain[1];
+ } else {
+ y0 = yExtent[0], y1 = yExtent[1];
+ if (y.invert) y0 = y.invert(y0), y1 = y.invert(y1);
+ if (y1 < y0) t = y0, y0 = y1, y1 = t;
+ }
+ }
+ return x && y ? [ [ x0, y0 ], [ x1, y1 ] ] : x ? [ x0, x1 ] : y && [ y0, y1 ];
+ }
+ if (x) {
+ x0 = z[0], x1 = z[1];
+ if (y) x0 = x0[0], x1 = x1[0];
+ xExtentDomain = [ x0, x1 ];
+ if (x.invert) x0 = x(x0), x1 = x(x1);
+ if (x1 < x0) t = x0, x0 = x1, x1 = t;
+ if (x0 != xExtent[0] || x1 != xExtent[1]) xExtent = [ x0, x1 ];
+ }
+ if (y) {
+ y0 = z[0], y1 = z[1];
+ if (x) y0 = y0[1], y1 = y1[1];
+ yExtentDomain = [ y0, y1 ];
+ if (y.invert) y0 = y(y0), y1 = y(y1);
+ if (y1 < y0) t = y0, y0 = y1, y1 = t;
+ if (y0 != yExtent[0] || y1 != yExtent[1]) yExtent = [ y0, y1 ];
+ }
+ return brush;
+ };
+ brush.clear = function() {
+ if (!brush.empty()) {
+ xExtent = [ 0, 0 ], yExtent = [ 0, 0 ];
+ xExtentDomain = yExtentDomain = null;
+ }
+ return brush;
+ };
+ brush.empty = function() {
+ return !!x && xExtent[0] == xExtent[1] || !!y && yExtent[0] == yExtent[1];
+ };
+ return d3.rebind(brush, event, "on");
+ };
+ var d3_svg_brushCursor = {
+ n: "ns-resize",
+ e: "ew-resize",
+ s: "ns-resize",
+ w: "ew-resize",
+ nw: "nwse-resize",
+ ne: "nesw-resize",
+ se: "nwse-resize",
+ sw: "nesw-resize"
+ };
+ var d3_svg_brushResizes = [ [ "n", "e", "s", "w", "nw", "ne", "se", "sw" ], [ "e", "w" ], [ "n", "s" ], [] ];
+ var d3_time_format = d3_time.format = d3_locale_enUS.timeFormat;
+ var d3_time_formatUtc = d3_time_format.utc;
+ var d3_time_formatIso = d3_time_formatUtc("%Y-%m-%dT%H:%M:%S.%LZ");
+ d3_time_format.iso = Date.prototype.toISOString && +new Date("2000-01-01T00:00:00.000Z") ? d3_time_formatIsoNative : d3_time_formatIso;
+ function d3_time_formatIsoNative(date) {
+ return date.toISOString();
+ }
+ d3_time_formatIsoNative.parse = function(string) {
+ var date = new Date(string);
+ return isNaN(date) ? null : date;
+ };
+ d3_time_formatIsoNative.toString = d3_time_formatIso.toString;
+ d3_time.second = d3_time_interval(function(date) {
+ return new d3_date(Math.floor(date / 1e3) * 1e3);
+ }, function(date, offset) {
+ date.setTime(date.getTime() + Math.floor(offset) * 1e3);
+ }, function(date) {
+ return date.getSeconds();
+ });
+ d3_time.seconds = d3_time.second.range;
+ d3_time.seconds.utc = d3_time.second.utc.range;
+ d3_time.minute = d3_time_interval(function(date) {
+ return new d3_date(Math.floor(date / 6e4) * 6e4);
+ }, function(date, offset) {
+ date.setTime(date.getTime() + Math.floor(offset) * 6e4);
+ }, function(date) {
+ return date.getMinutes();
+ });
+ d3_time.minutes = d3_time.minute.range;
+ d3_time.minutes.utc = d3_time.minute.utc.range;
+ d3_time.hour = d3_time_interval(function(date) {
+ var timezone = date.getTimezoneOffset() / 60;
+ return new d3_date((Math.floor(date / 36e5 - timezone) + timezone) * 36e5);
+ }, function(date, offset) {
+ date.setTime(date.getTime() + Math.floor(offset) * 36e5);
+ }, function(date) {
+ return date.getHours();
+ });
+ d3_time.hours = d3_time.hour.range;
+ d3_time.hours.utc = d3_time.hour.utc.range;
+ d3_time.month = d3_time_interval(function(date) {
+ date = d3_time.day(date);
+ date.setDate(1);
+ return date;
+ }, function(date, offset) {
+ date.setMonth(date.getMonth() + offset);
+ }, function(date) {
+ return date.getMonth();
+ });
+ d3_time.months = d3_time.month.range;
+ d3_time.months.utc = d3_time.month.utc.range;
+ function d3_time_scale(linear, methods, format) {
+ function scale(x) {
+ return linear(x);
+ }
+ scale.invert = function(x) {
+ return d3_time_scaleDate(linear.invert(x));
+ };
+ scale.domain = function(x) {
+ if (!arguments.length) return linear.domain().map(d3_time_scaleDate);
+ linear.domain(x);
+ return scale;
+ };
+ function tickMethod(extent, count) {
+ var span = extent[1] - extent[0], target = span / count, i = d3.bisect(d3_time_scaleSteps, target);
+ return i == d3_time_scaleSteps.length ? [ methods.year, d3_scale_linearTickRange(extent.map(function(d) {
+ return d / 31536e6;
+ }), count)[2] ] : !i ? [ d3_time_scaleMilliseconds, d3_scale_linearTickRange(extent, count)[2] ] : methods[target / d3_time_scaleSteps[i - 1] < d3_time_scaleSteps[i] / target ? i - 1 : i];
+ }
+ scale.nice = function(interval, skip) {
+ var domain = scale.domain(), extent = d3_scaleExtent(domain), method = interval == null ? tickMethod(extent, 10) : typeof interval === "number" && tickMethod(extent, interval);
+ if (method) interval = method[0], skip = method[1];
+ function skipped(date) {
+ return !isNaN(date) && !interval.range(date, d3_time_scaleDate(+date + 1), skip).length;
+ }
+ return scale.domain(d3_scale_nice(domain, skip > 1 ? {
+ floor: function(date) {
+ while (skipped(date = interval.floor(date))) date = d3_time_scaleDate(date - 1);
+ return date;
+ },
+ ceil: function(date) {
+ while (skipped(date = interval.ceil(date))) date = d3_time_scaleDate(+date + 1);
+ return date;
+ }
+ } : interval));
+ };
+ scale.ticks = function(interval, skip) {
+ var extent = d3_scaleExtent(scale.domain()), method = interval == null ? tickMethod(extent, 10) : typeof interval === "number" ? tickMethod(extent, interval) : !interval.range && [ {
+ range: interval
+ }, skip ];
+ if (method) interval = method[0], skip = method[1];
+ return interval.range(extent[0], d3_time_scaleDate(+extent[1] + 1), skip < 1 ? 1 : skip);
+ };
+ scale.tickFormat = function() {
+ return format;
+ };
+ scale.copy = function() {
+ return d3_time_scale(linear.copy(), methods, format);
+ };
+ return d3_scale_linearRebind(scale, linear);
+ }
+ function d3_time_scaleDate(t) {
+ return new Date(t);
+ }
+ var d3_time_scaleSteps = [ 1e3, 5e3, 15e3, 3e4, 6e4, 3e5, 9e5, 18e5, 36e5, 108e5, 216e5, 432e5, 864e5, 1728e5, 6048e5, 2592e6, 7776e6, 31536e6 ];
+ var d3_time_scaleLocalMethods = [ [ d3_time.second, 1 ], [ d3_time.second, 5 ], [ d3_time.second, 15 ], [ d3_time.second, 30 ], [ d3_time.minute, 1 ], [ d3_time.minute, 5 ], [ d3_time.minute, 15 ], [ d3_time.minute, 30 ], [ d3_time.hour, 1 ], [ d3_time.hour, 3 ], [ d3_time.hour, 6 ], [ d3_time.hour, 12 ], [ d3_time.day, 1 ], [ d3_time.day, 2 ], [ d3_time.week, 1 ], [ d3_time.month, 1 ], [ d3_time.month, 3 ], [ d3_time.year, 1 ] ];
+ var d3_time_scaleLocalFormat = d3_time_format.multi([ [ ".%L", function(d) {
+ return d.getMilliseconds();
+ } ], [ ":%S", function(d) {
+ return d.getSeconds();
+ } ], [ "%I:%M", function(d) {
+ return d.getMinutes();
+ } ], [ "%I %p", function(d) {
+ return d.getHours();
+ } ], [ "%a %d", function(d) {
+ return d.getDay() && d.getDate() != 1;
+ } ], [ "%b %d", function(d) {
+ return d.getDate() != 1;
+ } ], [ "%B", function(d) {
+ return d.getMonth();
+ } ], [ "%Y", d3_true ] ]);
+ var d3_time_scaleMilliseconds = {
+ range: function(start, stop, step) {
+ return d3.range(+start, +stop, step).map(d3_time_scaleDate);
+ },
+ floor: d3_identity,
+ ceil: d3_identity
+ };
+ d3_time_scaleLocalMethods.year = d3_time.year;
+ d3_time.scale = function() {
+ return d3_time_scale(d3.scale.linear(), d3_time_scaleLocalMethods, d3_time_scaleLocalFormat);
+ };
+ var d3_time_scaleUtcMethods = d3_time_scaleLocalMethods.map(function(m) {
+ return [ m[0].utc, m[1] ];
+ });
+ var d3_time_scaleUtcFormat = d3_time_formatUtc.multi([ [ ".%L", function(d) {
+ return d.getUTCMilliseconds();
+ } ], [ ":%S", function(d) {
+ return d.getUTCSeconds();
+ } ], [ "%I:%M", function(d) {
+ return d.getUTCMinutes();
+ } ], [ "%I %p", function(d) {
+ return d.getUTCHours();
+ } ], [ "%a %d", function(d) {
+ return d.getUTCDay() && d.getUTCDate() != 1;
+ } ], [ "%b %d", function(d) {
+ return d.getUTCDate() != 1;
+ } ], [ "%B", function(d) {
+ return d.getUTCMonth();
+ } ], [ "%Y", d3_true ] ]);
+ d3_time_scaleUtcMethods.year = d3_time.year.utc;
+ d3_time.scale.utc = function() {
+ return d3_time_scale(d3.scale.linear(), d3_time_scaleUtcMethods, d3_time_scaleUtcFormat);
+ };
+ d3.text = d3_xhrType(function(request) {
+ return request.responseText;
+ });
+ d3.json = function(url, callback) {
+ return d3_xhr(url, "application/json", d3_json, callback);
+ };
+ function d3_json(request) {
+ return JSON.parse(request.responseText);
+ }
+ d3.html = function(url, callback) {
+ return d3_xhr(url, "text/html", d3_html, callback);
+ };
+ function d3_html(request) {
+ var range = d3_document.createRange();
+ range.selectNode(d3_document.body);
+ return range.createContextualFragment(request.responseText);
+ }
+ d3.xml = d3_xhrType(function(request) {
+ return request.responseXML;
+ });
+ if (typeof define === "function" && define.amd) {
+ define(d3);
+ } else if (typeof module === "object" && module.exports) {
+ module.exports = d3;
+ } else {
+ this.d3 = d3;
+ }
+}(); \ No newline at end of file
diff --git a/devtools/client/shared/vendor/dagre-d3.js b/devtools/client/shared/vendor/dagre-d3.js
new file mode 100644
index 000000000..482ce827f
--- /dev/null
+++ b/devtools/client/shared/vendor/dagre-d3.js
@@ -0,0 +1,4560 @@
+;(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);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
+var global=self;/**
+ * @license
+ * Copyright (c) 2012-2013 Chris Pettitt
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+global.dagreD3 = require('./index');
+
+},{"./index":2}],2:[function(require,module,exports){
+/**
+ * @license
+ * Copyright (c) 2012-2013 Chris Pettitt
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+module.exports = {
+ Digraph: require('graphlib').Digraph,
+ Renderer: require('./lib/Renderer'),
+ json: require('graphlib').converter.json,
+ layout: require('dagre').layout,
+ version: require('./lib/version')
+};
+
+},{"./lib/Renderer":3,"./lib/version":4,"dagre":11,"graphlib":28}],3:[function(require,module,exports){
+var layout = require('dagre').layout;
+
+var d3;
+try { d3 = require('d3'); } catch (_) { d3 = window.d3; }
+
+module.exports = Renderer;
+
+function Renderer() {
+ // Set up defaults...
+ this._layout = layout();
+
+ this.drawNodes(defaultDrawNodes);
+ this.drawEdgeLabels(defaultDrawEdgeLabels);
+ this.drawEdgePaths(defaultDrawEdgePaths);
+ this.positionNodes(defaultPositionNodes);
+ this.positionEdgeLabels(defaultPositionEdgeLabels);
+ this.positionEdgePaths(defaultPositionEdgePaths);
+ this.transition(defaultTransition);
+ this.postLayout(defaultPostLayout);
+ this.postRender(defaultPostRender);
+
+ this.edgeInterpolate('bundle');
+ this.edgeTension(0.95);
+}
+
+Renderer.prototype.layout = function(layout) {
+ if (!arguments.length) { return this._layout; }
+ this._layout = layout;
+ return this;
+};
+
+Renderer.prototype.drawNodes = function(drawNodes) {
+ if (!arguments.length) { return this._drawNodes; }
+ this._drawNodes = bind(drawNodes, this);
+ return this;
+};
+
+Renderer.prototype.drawEdgeLabels = function(drawEdgeLabels) {
+ if (!arguments.length) { return this._drawEdgeLabels; }
+ this._drawEdgeLabels = bind(drawEdgeLabels, this);
+ return this;
+};
+
+Renderer.prototype.drawEdgePaths = function(drawEdgePaths) {
+ if (!arguments.length) { return this._drawEdgePaths; }
+ this._drawEdgePaths = bind(drawEdgePaths, this);
+ return this;
+};
+
+Renderer.prototype.positionNodes = function(positionNodes) {
+ if (!arguments.length) { return this._positionNodes; }
+ this._positionNodes = bind(positionNodes, this);
+ return this;
+};
+
+Renderer.prototype.positionEdgeLabels = function(positionEdgeLabels) {
+ if (!arguments.length) { return this._positionEdgeLabels; }
+ this._positionEdgeLabels = bind(positionEdgeLabels, this);
+ return this;
+};
+
+Renderer.prototype.positionEdgePaths = function(positionEdgePaths) {
+ if (!arguments.length) { return this._positionEdgePaths; }
+ this._positionEdgePaths = bind(positionEdgePaths, this);
+ return this;
+};
+
+Renderer.prototype.transition = function(transition) {
+ if (!arguments.length) { return this._transition; }
+ this._transition = bind(transition, this);
+ return this;
+};
+
+Renderer.prototype.postLayout = function(postLayout) {
+ if (!arguments.length) { return this._postLayout; }
+ this._postLayout = bind(postLayout, this);
+ return this;
+};
+
+Renderer.prototype.postRender = function(postRender) {
+ if (!arguments.length) { return this._postRender; }
+ this._postRender = bind(postRender, this);
+ return this;
+};
+
+Renderer.prototype.edgeInterpolate = function(edgeInterpolate) {
+ if (!arguments.length) { return this._edgeInterpolate; }
+ this._edgeInterpolate = edgeInterpolate;
+ return this;
+};
+
+Renderer.prototype.edgeTension = function(edgeTension) {
+ if (!arguments.length) { return this._edgeTension; }
+ this._edgeTension = edgeTension;
+ return this;
+};
+
+Renderer.prototype.run = function(graph, svg) {
+ // First copy the input graph so that it is not changed by the rendering
+ // process.
+ graph = copyAndInitGraph(graph);
+
+ // Create layers
+ svg
+ .selectAll('g.edgePaths, g.edgeLabels, g.nodes')
+ .data(['edgePaths', 'edgeLabels', 'nodes'])
+ .enter()
+ .append('g')
+ .attr('class', function(d) { return d; });
+
+
+ // Create node and edge roots, attach labels, and capture dimension
+ // information for use with layout.
+ var svgNodes = this._drawNodes(graph, svg.select('g.nodes'));
+ var svgEdgeLabels = this._drawEdgeLabels(graph, svg.select('g.edgeLabels'));
+
+ svgNodes.each(function(u) { calculateDimensions(this, graph.node(u)); });
+ svgEdgeLabels.each(function(e) { calculateDimensions(this, graph.edge(e)); });
+
+ // Now apply the layout function
+ var result = runLayout(graph, this._layout);
+
+ // Run any user-specified post layout processing
+ this._postLayout(result, svg);
+
+ var svgEdgePaths = this._drawEdgePaths(graph, svg.select('g.edgePaths'));
+
+ // Apply the layout information to the graph
+ this._positionNodes(result, svgNodes);
+ this._positionEdgeLabels(result, svgEdgeLabels);
+ this._positionEdgePaths(result, svgEdgePaths);
+
+ this._postRender(result, svg);
+
+ return result;
+};
+
+function copyAndInitGraph(graph) {
+ var copy = graph.copy();
+
+ // Init labels if they were not present in the source graph
+ copy.nodes().forEach(function(u) {
+ var value = copy.node(u);
+ if (value === undefined) {
+ value = {};
+ copy.node(u, value);
+ }
+ if (!('label' in value)) { value.label = ''; }
+ });
+
+ copy.edges().forEach(function(e) {
+ var value = copy.edge(e);
+ if (value === undefined) {
+ value = {};
+ copy.edge(e, value);
+ }
+ if (!('label' in value)) { value.label = ''; }
+ });
+
+ return copy;
+}
+
+function calculateDimensions(group, value) {
+ var bbox = group.getBBox();
+ value.width = bbox.width;
+ value.height = bbox.height;
+}
+
+function runLayout(graph, layout) {
+ var result = layout.run(graph);
+
+ // Copy labels to the result graph
+ graph.eachNode(function(u, value) { result.node(u).label = value.label; });
+ graph.eachEdge(function(e, u, v, value) { result.edge(e).label = value.label; });
+
+ return result;
+}
+
+function defaultDrawNodes(g, root) {
+ var nodes = g.nodes().filter(function(u) { return !isComposite(g, u); });
+
+ var svgNodes = root
+ .selectAll('g.node')
+ .classed('enter', false)
+ .data(nodes, function(u) { return u; });
+
+ svgNodes.selectAll('*').remove();
+
+ svgNodes
+ .enter()
+ .append('g')
+ .style('opacity', 0)
+ .attr('class', 'node enter');
+
+ svgNodes.each(function(u) { addLabel(g.node(u), d3.select(this), 10, 10); });
+
+ this._transition(svgNodes.exit())
+ .style('opacity', 0)
+ .remove();
+
+ return svgNodes;
+}
+
+function defaultDrawEdgeLabels(g, root) {
+ var svgEdgeLabels = root
+ .selectAll('g.edgeLabel')
+ .classed('enter', false)
+ .data(g.edges(), function (e) { return e; });
+
+ svgEdgeLabels.selectAll('*').remove();
+
+ svgEdgeLabels
+ .enter()
+ .append('g')
+ .style('opacity', 0)
+ .attr('class', 'edgeLabel enter');
+
+ svgEdgeLabels.each(function(e) { addLabel(g.edge(e), d3.select(this), 0, 0); });
+
+ this._transition(svgEdgeLabels.exit())
+ .style('opacity', 0)
+ .remove();
+
+ return svgEdgeLabels;
+}
+
+var defaultDrawEdgePaths = function(g, root) {
+ var svgEdgePaths = root
+ .selectAll('g.edgePath')
+ .classed('enter', false)
+ .data(g.edges(), function(e) { return e; });
+
+ svgEdgePaths
+ .enter()
+ .append('g')
+ .attr('class', 'edgePath enter')
+ .append('path')
+ .style('opacity', 0)
+ .attr('marker-end', 'url(#arrowhead)');
+
+ this._transition(svgEdgePaths.exit())
+ .style('opacity', 0)
+ .remove();
+
+ return svgEdgePaths;
+};
+
+function defaultPositionNodes(g, svgNodes, svgNodesEnter) {
+ function transform(u) {
+ var value = g.node(u);
+ return 'translate(' + value.x + ',' + value.y + ')';
+ }
+
+ // For entering nodes, position immediately without transition
+ svgNodes.filter('.enter').attr('transform', transform);
+
+ this._transition(svgNodes)
+ .style('opacity', 1)
+ .attr('transform', transform);
+}
+
+function defaultPositionEdgeLabels(g, svgEdgeLabels) {
+ function transform(e) {
+ var value = g.edge(e);
+ var point = findMidPoint(value.points);
+ return 'translate(' + point.x + ',' + point.y + ')';
+ }
+
+ // For entering edge labels, position immediately without transition
+ svgEdgeLabels.filter('.enter').attr('transform', transform);
+
+ this._transition(svgEdgeLabels)
+ .style('opacity', 1)
+ .attr('transform', transform);
+}
+
+function defaultPositionEdgePaths(g, svgEdgePaths) {
+ var interpolate = this._edgeInterpolate,
+ tension = this._edgeTension;
+
+ function calcPoints(e) {
+ var value = g.edge(e);
+ var source = g.node(g.incidentNodes(e)[0]);
+ var target = g.node(g.incidentNodes(e)[1]);
+ var points = value.points.slice();
+
+ var p0 = points.length === 0 ? target : points[0];
+ var p1 = points.length === 0 ? source : points[points.length - 1];
+
+ points.unshift(intersectRect(source, p0));
+ // TODO: use bpodgursky's shortening algorithm here
+ points.push(intersectRect(target, p1));
+
+ return d3.svg.line()
+ .x(function(d) { return d.x; })
+ .y(function(d) { return d.y; })
+ .interpolate(interpolate)
+ .tension(tension)
+ (points);
+ }
+
+ svgEdgePaths.filter('.enter').selectAll('path')
+ .attr('d', calcPoints);
+
+ this._transition(svgEdgePaths.selectAll('path'))
+ .attr('d', calcPoints)
+ .style('opacity', 1);
+}
+
+// By default we do not use transitions
+function defaultTransition(selection) {
+ return selection;
+}
+
+function defaultPostLayout() {
+ // Do nothing
+}
+
+function defaultPostRender(graph, root) {
+ if (graph.isDirected() && root.select('#arrowhead').empty()) {
+ root
+ .append('svg:defs')
+ .append('svg:marker')
+ .attr('id', 'arrowhead')
+ .attr('viewBox', '0 0 10 10')
+ .attr('refX', 8)
+ .attr('refY', 5)
+ .attr('markerUnits', 'strokewidth')
+ .attr('markerWidth', 8)
+ .attr('markerHeight', 5)
+ .attr('orient', 'auto')
+ .attr('style', 'fill: #333')
+ .append('svg:path')
+ .attr('d', 'M 0 0 L 10 5 L 0 10 z');
+ }
+}
+
+function addLabel(node, root, marginX, marginY) {
+ // Add the rect first so that it appears behind the label
+ var label = node.label;
+ var rect = root.append('rect');
+ var labelSvg = root.append('g');
+
+ if (label[0] === '<') {
+ addForeignObjectLabel(label, labelSvg);
+ // No margin for HTML elements
+ marginX = marginY = 0;
+ } else {
+ addTextLabel(label,
+ labelSvg,
+ Math.floor(node.labelCols),
+ node.labelCut);
+ }
+
+ var bbox = root.node().getBBox();
+
+ labelSvg.attr('transform',
+ 'translate(' + (-bbox.width / 2) + ',' + (-bbox.height / 2) + ')');
+
+ rect
+ .attr('rx', 5)
+ .attr('ry', 5)
+ .attr('x', -(bbox.width / 2 + marginX))
+ .attr('y', -(bbox.height / 2 + marginY))
+ .attr('width', bbox.width + 2 * marginX)
+ .attr('height', bbox.height + 2 * marginY);
+}
+
+function addForeignObjectLabel(label, root) {
+ var fo = root
+ .append('foreignObject')
+ .attr('width', '100000');
+
+ var w, h;
+ fo
+ .append('xhtml:div')
+ .style('float', 'left')
+ // TODO find a better way to get dimensions for foreignObjects...
+ .html(function() { return label; })
+ .each(function() {
+ w = this.clientWidth;
+ h = this.clientHeight;
+ });
+
+ fo
+ .attr('width', w)
+ .attr('height', h);
+}
+
+function addTextLabel(label, root, labelCols, labelCut) {
+ if (labelCut === undefined) labelCut = "false";
+ labelCut = (labelCut.toString().toLowerCase() === "true");
+
+ var node = root
+ .append('text')
+ .attr('text-anchor', 'left');
+
+ label = label.replace(/\\n/g, "\n");
+
+ var arr = labelCols ? wordwrap(label, labelCols, labelCut) : label;
+ arr = arr.split("\n");
+ for (var i = 0; i < arr.length; i++) {
+ node
+ .append('tspan')
+ .attr('dy', '1em')
+ .attr('x', '1')
+ .text(arr[i]);
+ }
+}
+
+// Thanks to
+// http://james.padolsey.com/javascript/wordwrap-for-javascript/
+function wordwrap (str, width, cut, brk) {
+ brk = brk || '\n';
+ width = width || 75;
+ cut = cut || false;
+
+ if (!str) { return str; }
+
+ var regex = '.{1,' +width+ '}(\\s|$)' + (cut ? '|.{' +width+ '}|.+$' : '|\\S+?(\\s|$)');
+
+ return str.match( RegExp(regex, 'g') ).join( brk );
+}
+
+function findMidPoint(points) {
+ var midIdx = points.length / 2;
+ if (points.length % 2) {
+ return points[Math.floor(midIdx)];
+ } else {
+ var p0 = points[midIdx - 1];
+ var p1 = points[midIdx];
+ return {x: (p0.x + p1.x) / 2, y: (p0.y + p1.y) / 2};
+ }
+}
+
+function intersectRect(rect, point) {
+ var x = rect.x;
+ var y = rect.y;
+
+ // For now we only support rectangles
+
+ // Rectangle intersection algorithm from:
+ // http://math.stackexchange.com/questions/108113/find-edge-between-two-boxes
+ var dx = point.x - x;
+ var dy = point.y - y;
+ var w = rect.width / 2;
+ var h = rect.height / 2;
+
+ var sx, sy;
+ if (Math.abs(dy) * w > Math.abs(dx) * h) {
+ // Intersection is top or bottom of rect.
+ if (dy < 0) {
+ h = -h;
+ }
+ sx = dy === 0 ? 0 : h * dx / dy;
+ sy = h;
+ } else {
+ // Intersection is left or right of rect.
+ if (dx < 0) {
+ w = -w;
+ }
+ sx = w;
+ sy = dx === 0 ? 0 : w * dy / dx;
+ }
+
+ return {x: x + sx, y: y + sy};
+}
+
+function isComposite(g, u) {
+ return 'children' in g && g.children(u).length;
+}
+
+function bind(func, thisArg) {
+ // For some reason PhantomJS occassionally fails when using the builtin bind,
+ // so we check if it is available and if not, use a degenerate polyfill.
+ if (func.bind) {
+ return func.bind(thisArg);
+ }
+
+ return function() {
+ return func.apply(thisArg, arguments);
+ };
+}
+
+},{"d3":10,"dagre":11}],4:[function(require,module,exports){
+module.exports = '0.1.5';
+
+},{}],5:[function(require,module,exports){
+exports.Set = require('./lib/Set');
+exports.PriorityQueue = require('./lib/PriorityQueue');
+exports.version = require('./lib/version');
+
+},{"./lib/PriorityQueue":6,"./lib/Set":7,"./lib/version":9}],6:[function(require,module,exports){
+module.exports = PriorityQueue;
+
+/**
+ * A min-priority queue data structure. This algorithm is derived from Cormen,
+ * et al., "Introduction to Algorithms". The basic idea of a min-priority
+ * queue is that you can efficiently (in O(1) time) get the smallest key in
+ * the queue. Adding and removing elements takes O(log n) time. A key can
+ * have its priority decreased in O(log n) time.
+ */
+function PriorityQueue() {
+ this._arr = [];
+ this._keyIndices = {};
+}
+
+/**
+ * Returns the number of elements in the queue. Takes `O(1)` time.
+ */
+PriorityQueue.prototype.size = function() {
+ return this._arr.length;
+};
+
+/**
+ * Returns the keys that are in the queue. Takes `O(n)` time.
+ */
+PriorityQueue.prototype.keys = function() {
+ return this._arr.map(function(x) { return x.key; });
+};
+
+/**
+ * Returns `true` if **key** is in the queue and `false` if not.
+ */
+PriorityQueue.prototype.has = function(key) {
+ return key in this._keyIndices;
+};
+
+/**
+ * Returns the priority for **key**. If **key** is not present in the queue
+ * then this function returns `undefined`. Takes `O(1)` time.
+ *
+ * @param {Object} key
+ */
+PriorityQueue.prototype.priority = function(key) {
+ var index = this._keyIndices[key];
+ if (index !== undefined) {
+ return this._arr[index].priority;
+ }
+};
+
+/**
+ * Returns the key for the minimum element in this queue. If the queue is
+ * empty this function throws an Error. Takes `O(1)` time.
+ */
+PriorityQueue.prototype.min = function() {
+ if (this.size() === 0) {
+ throw new Error("Queue underflow");
+ }
+ return this._arr[0].key;
+};
+
+/**
+ * Inserts a new key into the priority queue. If the key already exists in
+ * the queue this function returns `false`; otherwise it will return `true`.
+ * Takes `O(n)` time.
+ *
+ * @param {Object} key the key to add
+ * @param {Number} priority the initial priority for the key
+ */
+PriorityQueue.prototype.add = function(key, priority) {
+ var keyIndices = this._keyIndices;
+ if (!(key in keyIndices)) {
+ var arr = this._arr;
+ var index = arr.length;
+ keyIndices[key] = index;
+ arr.push({key: key, priority: priority});
+ this._decrease(index);
+ return true;
+ }
+ return false;
+};
+
+/**
+ * Removes and returns the smallest key in the queue. Takes `O(log n)` time.
+ */
+PriorityQueue.prototype.removeMin = function() {
+ this._swap(0, this._arr.length - 1);
+ var min = this._arr.pop();
+ delete this._keyIndices[min.key];
+ this._heapify(0);
+ return min.key;
+};
+
+/**
+ * Decreases the priority for **key** to **priority**. If the new priority is
+ * greater than the previous priority, this function will throw an Error.
+ *
+ * @param {Object} key the key for which to raise priority
+ * @param {Number} priority the new priority for the key
+ */
+PriorityQueue.prototype.decrease = function(key, priority) {
+ var index = this._keyIndices[key];
+ if (priority > this._arr[index].priority) {
+ throw new Error("New priority is greater than current priority. " +
+ "Key: " + key + " Old: " + this._arr[index].priority + " New: " + priority);
+ }
+ this._arr[index].priority = priority;
+ this._decrease(index);
+};
+
+PriorityQueue.prototype._heapify = function(i) {
+ var arr = this._arr;
+ var l = 2 * i,
+ r = l + 1,
+ largest = i;
+ if (l < arr.length) {
+ largest = arr[l].priority < arr[largest].priority ? l : largest;
+ if (r < arr.length) {
+ largest = arr[r].priority < arr[largest].priority ? r : largest;
+ }
+ if (largest !== i) {
+ this._swap(i, largest);
+ this._heapify(largest);
+ }
+ }
+};
+
+PriorityQueue.prototype._decrease = function(index) {
+ var arr = this._arr;
+ var priority = arr[index].priority;
+ var parent;
+ while (index !== 0) {
+ parent = index >> 1;
+ if (arr[parent].priority < priority) {
+ break;
+ }
+ this._swap(index, parent);
+ index = parent;
+ }
+};
+
+PriorityQueue.prototype._swap = function(i, j) {
+ var arr = this._arr;
+ var keyIndices = this._keyIndices;
+ var origArrI = arr[i];
+ var origArrJ = arr[j];
+ arr[i] = origArrJ;
+ arr[j] = origArrI;
+ keyIndices[origArrJ.key] = i;
+ keyIndices[origArrI.key] = j;
+};
+
+},{}],7:[function(require,module,exports){
+var util = require('./util');
+
+module.exports = Set;
+
+/**
+ * Constructs a new Set with an optional set of `initialKeys`.
+ *
+ * It is important to note that keys are coerced to String for most purposes
+ * with this object, similar to the behavior of JavaScript's Object. For
+ * example, the following will add only one key:
+ *
+ * var s = new Set();
+ * s.add(1);
+ * s.add("1");
+ *
+ * However, the type of the key is preserved internally so that `keys` returns
+ * the original key set uncoerced. For the above example, `keys` would return
+ * `[1]`.
+ */
+function Set(initialKeys) {
+ this._size = 0;
+ this._keys = {};
+
+ if (initialKeys) {
+ for (var i = 0, il = initialKeys.length; i < il; ++i) {
+ this.add(initialKeys[i]);
+ }
+ }
+}
+
+/**
+ * Returns a new Set that represents the set intersection of the array of given
+ * sets.
+ */
+Set.intersect = function(sets) {
+ if (sets.length === 0) {
+ return new Set();
+ }
+
+ var result = new Set(!util.isArray(sets[0]) ? sets[0].keys() : sets[0]);
+ for (var i = 1, il = sets.length; i < il; ++i) {
+ var resultKeys = result.keys(),
+ other = !util.isArray(sets[i]) ? sets[i] : new Set(sets[i]);
+ for (var j = 0, jl = resultKeys.length; j < jl; ++j) {
+ var key = resultKeys[j];
+ if (!other.has(key)) {
+ result.remove(key);
+ }
+ }
+ }
+
+ return result;
+};
+
+/**
+ * Returns a new Set that represents the set union of the array of given sets.
+ */
+Set.union = function(sets) {
+ var totalElems = util.reduce(sets, function(lhs, rhs) {
+ return lhs + (rhs.size ? rhs.size() : rhs.length);
+ }, 0);
+ var arr = new Array(totalElems);
+
+ var k = 0;
+ for (var i = 0, il = sets.length; i < il; ++i) {
+ var cur = sets[i],
+ keys = !util.isArray(cur) ? cur.keys() : cur;
+ for (var j = 0, jl = keys.length; j < jl; ++j) {
+ arr[k++] = keys[j];
+ }
+ }
+
+ return new Set(arr);
+};
+
+/**
+ * Returns the size of this set in `O(1)` time.
+ */
+Set.prototype.size = function() {
+ return this._size;
+};
+
+/**
+ * Returns the keys in this set. Takes `O(n)` time.
+ */
+Set.prototype.keys = function() {
+ return values(this._keys);
+};
+
+/**
+ * Tests if a key is present in this Set. Returns `true` if it is and `false`
+ * if not. Takes `O(1)` time.
+ */
+Set.prototype.has = function(key) {
+ return key in this._keys;
+};
+
+/**
+ * Adds a new key to this Set if it is not already present. Returns `true` if
+ * the key was added and `false` if it was already present. Takes `O(1)` time.
+ */
+Set.prototype.add = function(key) {
+ if (!(key in this._keys)) {
+ this._keys[key] = key;
+ ++this._size;
+ return true;
+ }
+ return false;
+};
+
+/**
+ * Removes a key from this Set. If the key was removed this function returns
+ * `true`. If not, it returns `false`. Takes `O(1)` time.
+ */
+Set.prototype.remove = function(key) {
+ if (key in this._keys) {
+ delete this._keys[key];
+ --this._size;
+ return true;
+ }
+ return false;
+};
+
+/*
+ * Returns an array of all values for properties of **o**.
+ */
+function values(o) {
+ var ks = Object.keys(o),
+ len = ks.length,
+ result = new Array(len),
+ i;
+ for (i = 0; i < len; ++i) {
+ result[i] = o[ks[i]];
+ }
+ return result;
+}
+
+},{"./util":8}],8:[function(require,module,exports){
+/*
+ * This polyfill comes from
+ * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray
+ */
+if(!Array.isArray) {
+ exports.isArray = function (vArg) {
+ return Object.prototype.toString.call(vArg) === '[object Array]';
+ };
+} else {
+ exports.isArray = Array.isArray;
+}
+
+/*
+ * Slightly adapted polyfill from
+ * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce
+ */
+if ('function' !== typeof Array.prototype.reduce) {
+ exports.reduce = function(array, callback, opt_initialValue) {
+ 'use strict';
+ if (null === array || 'undefined' === typeof array) {
+ // At the moment all modern browsers, that support strict mode, have
+ // native implementation of Array.prototype.reduce. For instance, IE8
+ // does not support strict mode, so this check is actually useless.
+ throw new TypeError(
+ 'Array.prototype.reduce called on null or undefined');
+ }
+ if ('function' !== typeof callback) {
+ throw new TypeError(callback + ' is not a function');
+ }
+ var index, value,
+ length = array.length >>> 0,
+ isValueSet = false;
+ if (1 < arguments.length) {
+ value = opt_initialValue;
+ isValueSet = true;
+ }
+ for (index = 0; length > index; ++index) {
+ if (array.hasOwnProperty(index)) {
+ if (isValueSet) {
+ value = callback(value, array[index], index, array);
+ }
+ else {
+ value = array[index];
+ isValueSet = true;
+ }
+ }
+ }
+ if (!isValueSet) {
+ throw new TypeError('Reduce of empty array with no initial value');
+ }
+ return value;
+ };
+} else {
+ exports.reduce = function(array, callback, opt_initialValue) {
+ return array.reduce(callback, opt_initialValue);
+ };
+}
+
+},{}],9:[function(require,module,exports){
+module.exports = '1.1.3';
+
+},{}],10:[function(require,module,exports){
+require("./d3");
+module.exports = d3;
+(function () { delete this.d3; })(); // unset global
+
+},{}],11:[function(require,module,exports){
+/*
+Copyright (c) 2012-2013 Chris Pettitt
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+exports.Digraph = require("graphlib").Digraph;
+exports.Graph = require("graphlib").Graph;
+exports.layout = require("./lib/layout");
+exports.version = require("./lib/version");
+
+},{"./lib/layout":12,"./lib/version":27,"graphlib":28}],12:[function(require,module,exports){
+var util = require('./util'),
+ rank = require('./rank'),
+ order = require('./order'),
+ CGraph = require('graphlib').CGraph,
+ CDigraph = require('graphlib').CDigraph;
+
+module.exports = function() {
+ // External configuration
+ var config = {
+ // How much debug information to include?
+ debugLevel: 0,
+ // Max number of sweeps to perform in order phase
+ orderMaxSweeps: order.DEFAULT_MAX_SWEEPS,
+ // Use network simplex algorithm in ranking
+ rankSimplex: false,
+ // Rank direction. Valid values are (TB, LR)
+ rankDir: 'TB'
+ };
+
+ // Phase functions
+ var position = require('./position')();
+
+ // This layout object
+ var self = {};
+
+ self.orderIters = util.propertyAccessor(self, config, 'orderMaxSweeps');
+
+ self.rankSimplex = util.propertyAccessor(self, config, 'rankSimplex');
+
+ self.nodeSep = delegateProperty(position.nodeSep);
+ self.edgeSep = delegateProperty(position.edgeSep);
+ self.universalSep = delegateProperty(position.universalSep);
+ self.rankSep = delegateProperty(position.rankSep);
+ self.rankDir = util.propertyAccessor(self, config, 'rankDir');
+ self.debugAlignment = delegateProperty(position.debugAlignment);
+
+ self.debugLevel = util.propertyAccessor(self, config, 'debugLevel', function(x) {
+ util.log.level = x;
+ position.debugLevel(x);
+ });
+
+ self.run = util.time('Total layout', run);
+
+ self._normalize = normalize;
+
+ return self;
+
+ /*
+ * Constructs an adjacency graph using the nodes and edges specified through
+ * config. For each node and edge we add a property `dagre` that contains an
+ * object that will hold intermediate and final layout information. Some of
+ * the contents include:
+ *
+ * 1) A generated ID that uniquely identifies the object.
+ * 2) Dimension information for nodes (copied from the source node).
+ * 3) Optional dimension information for edges.
+ *
+ * After the adjacency graph is constructed the code no longer needs to use
+ * the original nodes and edges passed in via config.
+ */
+ function initLayoutGraph(inputGraph) {
+ var g = new CDigraph();
+
+ inputGraph.eachNode(function(u, value) {
+ if (value === undefined) value = {};
+ g.addNode(u, {
+ width: value.width,
+ height: value.height
+ });
+ if (value.hasOwnProperty('rank')) {
+ g.node(u).prefRank = value.rank;
+ }
+ });
+
+ // Set up subgraphs
+ if (inputGraph.parent) {
+ inputGraph.nodes().forEach(function(u) {
+ g.parent(u, inputGraph.parent(u));
+ });
+ }
+
+ inputGraph.eachEdge(function(e, u, v, value) {
+ if (value === undefined) value = {};
+ var newValue = {
+ e: e,
+ minLen: value.minLen || 1,
+ width: value.width || 0,
+ height: value.height || 0,
+ points: []
+ };
+
+ g.addEdge(null, u, v, newValue);
+ });
+
+ // Initial graph attributes
+ var graphValue = inputGraph.graph() || {};
+ g.graph({
+ rankDir: graphValue.rankDir || config.rankDir,
+ orderRestarts: graphValue.orderRestarts
+ });
+
+ return g;
+ }
+
+ function run(inputGraph) {
+ var rankSep = self.rankSep();
+ var g;
+ try {
+ // Build internal graph
+ g = util.time('initLayoutGraph', initLayoutGraph)(inputGraph);
+
+ if (g.order() === 0) {
+ return g;
+ }
+
+ // Make space for edge labels
+ g.eachEdge(function(e, s, t, a) {
+ a.minLen *= 2;
+ });
+ self.rankSep(rankSep / 2);
+
+ // Determine the rank for each node. Nodes with a lower rank will appear
+ // above nodes of higher rank.
+ util.time('rank.run', rank.run)(g, config.rankSimplex);
+
+ // Normalize the graph by ensuring that every edge is proper (each edge has
+ // a length of 1). We achieve this by adding dummy nodes to long edges,
+ // thus shortening them.
+ util.time('normalize', normalize)(g);
+
+ // Order the nodes so that edge crossings are minimized.
+ util.time('order', order)(g, config.orderMaxSweeps);
+
+ // Find the x and y coordinates for every node in the graph.
+ util.time('position', position.run)(g);
+
+ // De-normalize the graph by removing dummy nodes and augmenting the
+ // original long edges with coordinate information.
+ util.time('undoNormalize', undoNormalize)(g);
+
+ // Reverses points for edges that are in a reversed state.
+ util.time('fixupEdgePoints', fixupEdgePoints)(g);
+
+ // Restore delete edges and reverse edges that were reversed in the rank
+ // phase.
+ util.time('rank.restoreEdges', rank.restoreEdges)(g);
+
+ // Construct final result graph and return it
+ return util.time('createFinalGraph', createFinalGraph)(g, inputGraph.isDirected());
+ } finally {
+ self.rankSep(rankSep);
+ }
+ }
+
+ /*
+ * This function is responsible for 'normalizing' the graph. The process of
+ * normalization ensures that no edge in the graph has spans more than one
+ * rank. To do this it inserts dummy nodes as needed and links them by adding
+ * dummy edges. This function keeps enough information in the dummy nodes and
+ * edges to ensure that the original graph can be reconstructed later.
+ *
+ * This method assumes that the input graph is cycle free.
+ */
+ function normalize(g) {
+ var dummyCount = 0;
+ g.eachEdge(function(e, s, t, a) {
+ var sourceRank = g.node(s).rank;
+ var targetRank = g.node(t).rank;
+ if (sourceRank + 1 < targetRank) {
+ for (var u = s, rank = sourceRank + 1, i = 0; rank < targetRank; ++rank, ++i) {
+ var v = '_D' + (++dummyCount);
+ var node = {
+ width: a.width,
+ height: a.height,
+ edge: { id: e, source: s, target: t, attrs: a },
+ rank: rank,
+ dummy: true
+ };
+
+ // If this node represents a bend then we will use it as a control
+ // point. For edges with 2 segments this will be the center dummy
+ // node. For edges with more than two segments, this will be the
+ // first and last dummy node.
+ if (i === 0) node.index = 0;
+ else if (rank + 1 === targetRank) node.index = 1;
+
+ g.addNode(v, node);
+ g.addEdge(null, u, v, {});
+ u = v;
+ }
+ g.addEdge(null, u, t, {});
+ g.delEdge(e);
+ }
+ });
+ }
+
+ /*
+ * Reconstructs the graph as it was before normalization. The positions of
+ * dummy nodes are used to build an array of points for the original 'long'
+ * edge. Dummy nodes and edges are removed.
+ */
+ function undoNormalize(g) {
+ g.eachNode(function(u, a) {
+ if (a.dummy) {
+ if ('index' in a) {
+ var edge = a.edge;
+ if (!g.hasEdge(edge.id)) {
+ g.addEdge(edge.id, edge.source, edge.target, edge.attrs);
+ }
+ var points = g.edge(edge.id).points;
+ points[a.index] = { x: a.x, y: a.y, ul: a.ul, ur: a.ur, dl: a.dl, dr: a.dr };
+ }
+ g.delNode(u);
+ }
+ });
+ }
+
+ /*
+ * For each edge that was reversed during the `acyclic` step, reverse its
+ * array of points.
+ */
+ function fixupEdgePoints(g) {
+ g.eachEdge(function(e, s, t, a) { if (a.reversed) a.points.reverse(); });
+ }
+
+ function createFinalGraph(g, isDirected) {
+ var out = isDirected ? new CDigraph() : new CGraph();
+ out.graph(g.graph());
+ g.eachNode(function(u, value) { out.addNode(u, value); });
+ g.eachNode(function(u) { out.parent(u, g.parent(u)); });
+ g.eachEdge(function(e, u, v, value) {
+ out.addEdge(value.e, u, v, value);
+ });
+
+ // Attach bounding box information
+ var maxX = 0, maxY = 0;
+ g.eachNode(function(u, value) {
+ if (!g.children(u).length) {
+ maxX = Math.max(maxX, value.x + value.width / 2);
+ maxY = Math.max(maxY, value.y + value.height / 2);
+ }
+ });
+ g.eachEdge(function(e, u, v, value) {
+ var maxXPoints = Math.max.apply(Math, value.points.map(function(p) { return p.x; }));
+ var maxYPoints = Math.max.apply(Math, value.points.map(function(p) { return p.y; }));
+ maxX = Math.max(maxX, maxXPoints + value.width / 2);
+ maxY = Math.max(maxY, maxYPoints + value.height / 2);
+ });
+ out.graph().width = maxX;
+ out.graph().height = maxY;
+
+ return out;
+ }
+
+ /*
+ * Given a function, a new function is returned that invokes the given
+ * function. The return value from the function is always the `self` object.
+ */
+ function delegateProperty(f) {
+ return function() {
+ if (!arguments.length) return f();
+ f.apply(null, arguments);
+ return self;
+ };
+ }
+};
+
+
+},{"./order":13,"./position":18,"./rank":19,"./util":26,"graphlib":28}],13:[function(require,module,exports){
+var util = require('./util'),
+ crossCount = require('./order/crossCount'),
+ initLayerGraphs = require('./order/initLayerGraphs'),
+ initOrder = require('./order/initOrder'),
+ sortLayer = require('./order/sortLayer');
+
+module.exports = order;
+
+// The maximum number of sweeps to perform before finishing the order phase.
+var DEFAULT_MAX_SWEEPS = 24;
+order.DEFAULT_MAX_SWEEPS = DEFAULT_MAX_SWEEPS;
+
+/*
+ * Runs the order phase with the specified `graph, `maxSweeps`, and
+ * `debugLevel`. If `maxSweeps` is not specified we use `DEFAULT_MAX_SWEEPS`.
+ * If `debugLevel` is not set we assume 0.
+ */
+function order(g, maxSweeps) {
+ if (arguments.length < 2) {
+ maxSweeps = DEFAULT_MAX_SWEEPS;
+ }
+
+ var restarts = g.graph().orderRestarts || 0;
+
+ var layerGraphs = initLayerGraphs(g);
+ // TODO: remove this when we add back support for ordering clusters
+ layerGraphs.forEach(function(lg) {
+ lg = lg.filterNodes(function(u) { return !g.children(u).length; });
+ });
+
+ var iters = 0,
+ currentBestCC,
+ allTimeBestCC = Number.MAX_VALUE,
+ allTimeBest = {};
+
+ function saveAllTimeBest() {
+ g.eachNode(function(u, value) { allTimeBest[u] = value.order; });
+ }
+
+ for (var j = 0; j < Number(restarts) + 1 && allTimeBestCC !== 0; ++j) {
+ currentBestCC = Number.MAX_VALUE;
+ initOrder(g, restarts > 0);
+
+ util.log(2, 'Order phase start cross count: ' + g.graph().orderInitCC);
+
+ var i, lastBest, cc;
+ for (i = 0, lastBest = 0; lastBest < 4 && i < maxSweeps && currentBestCC > 0; ++i, ++lastBest, ++iters) {
+ sweep(g, layerGraphs, i);
+ cc = crossCount(g);
+ if (cc < currentBestCC) {
+ lastBest = 0;
+ currentBestCC = cc;
+ if (cc < allTimeBestCC) {
+ saveAllTimeBest();
+ allTimeBestCC = cc;
+ }
+ }
+ util.log(3, 'Order phase start ' + j + ' iter ' + i + ' cross count: ' + cc);
+ }
+ }
+
+ Object.keys(allTimeBest).forEach(function(u) {
+ if (!g.children || !g.children(u).length) {
+ g.node(u).order = allTimeBest[u];
+ }
+ });
+ g.graph().orderCC = allTimeBestCC;
+
+ util.log(2, 'Order iterations: ' + iters);
+ util.log(2, 'Order phase best cross count: ' + g.graph().orderCC);
+}
+
+function predecessorWeights(g, nodes) {
+ var weights = {};
+ nodes.forEach(function(u) {
+ weights[u] = g.inEdges(u).map(function(e) {
+ return g.node(g.source(e)).order;
+ });
+ });
+ return weights;
+}
+
+function successorWeights(g, nodes) {
+ var weights = {};
+ nodes.forEach(function(u) {
+ weights[u] = g.outEdges(u).map(function(e) {
+ return g.node(g.target(e)).order;
+ });
+ });
+ return weights;
+}
+
+function sweep(g, layerGraphs, iter) {
+ if (iter % 2 === 0) {
+ sweepDown(g, layerGraphs, iter);
+ } else {
+ sweepUp(g, layerGraphs, iter);
+ }
+}
+
+function sweepDown(g, layerGraphs) {
+ var cg;
+ for (i = 1; i < layerGraphs.length; ++i) {
+ cg = sortLayer(layerGraphs[i], cg, predecessorWeights(g, layerGraphs[i].nodes()));
+ }
+}
+
+function sweepUp(g, layerGraphs) {
+ var cg;
+ for (i = layerGraphs.length - 2; i >= 0; --i) {
+ sortLayer(layerGraphs[i], cg, successorWeights(g, layerGraphs[i].nodes()));
+ }
+}
+
+},{"./order/crossCount":14,"./order/initLayerGraphs":15,"./order/initOrder":16,"./order/sortLayer":17,"./util":26}],14:[function(require,module,exports){
+var util = require('../util');
+
+module.exports = crossCount;
+
+/*
+ * Returns the cross count for the given graph.
+ */
+function crossCount(g) {
+ var cc = 0;
+ var ordering = util.ordering(g);
+ for (var i = 1; i < ordering.length; ++i) {
+ cc += twoLayerCrossCount(g, ordering[i-1], ordering[i]);
+ }
+ return cc;
+}
+
+/*
+ * This function searches through a ranked and ordered graph and counts the
+ * number of edges that cross. This algorithm is derived from:
+ *
+ * W. Barth et al., Bilayer Cross Counting, JGAA, 8(2) 179–194 (2004)
+ */
+function twoLayerCrossCount(g, layer1, layer2) {
+ var indices = [];
+ layer1.forEach(function(u) {
+ var nodeIndices = [];
+ g.outEdges(u).forEach(function(e) { nodeIndices.push(g.node(g.target(e)).order); });
+ nodeIndices.sort(function(x, y) { return x - y; });
+ indices = indices.concat(nodeIndices);
+ });
+
+ var firstIndex = 1;
+ while (firstIndex < layer2.length) firstIndex <<= 1;
+
+ var treeSize = 2 * firstIndex - 1;
+ firstIndex -= 1;
+
+ var tree = [];
+ for (var i = 0; i < treeSize; ++i) { tree[i] = 0; }
+
+ var cc = 0;
+ indices.forEach(function(i) {
+ var treeIndex = i + firstIndex;
+ ++tree[treeIndex];
+ while (treeIndex > 0) {
+ if (treeIndex % 2) {
+ cc += tree[treeIndex + 1];
+ }
+ treeIndex = (treeIndex - 1) >> 1;
+ ++tree[treeIndex];
+ }
+ });
+
+ return cc;
+}
+
+},{"../util":26}],15:[function(require,module,exports){
+var nodesFromList = require('graphlib').filter.nodesFromList,
+ /* jshint -W079 */
+ Set = require('cp-data').Set;
+
+module.exports = initLayerGraphs;
+
+/*
+ * This function takes a compound layered graph, g, and produces an array of
+ * layer graphs. Each entry in the array represents a subgraph of nodes
+ * relevant for performing crossing reduction on that layer.
+ */
+function initLayerGraphs(g) {
+ var ranks = [];
+
+ function dfs(u) {
+ if (u === null) {
+ g.children(u).forEach(function(v) { dfs(v); });
+ return;
+ }
+
+ var value = g.node(u);
+ value.minRank = ('rank' in value) ? value.rank : Number.MAX_VALUE;
+ value.maxRank = ('rank' in value) ? value.rank : Number.MIN_VALUE;
+ var uRanks = new Set();
+ g.children(u).forEach(function(v) {
+ var rs = dfs(v);
+ uRanks = Set.union([uRanks, rs]);
+ value.minRank = Math.min(value.minRank, g.node(v).minRank);
+ value.maxRank = Math.max(value.maxRank, g.node(v).maxRank);
+ });
+
+ if ('rank' in value) uRanks.add(value.rank);
+
+ uRanks.keys().forEach(function(r) {
+ if (!(r in ranks)) ranks[r] = [];
+ ranks[r].push(u);
+ });
+
+ return uRanks;
+ }
+ dfs(null);
+
+ var layerGraphs = [];
+ ranks.forEach(function(us, rank) {
+ layerGraphs[rank] = g.filterNodes(nodesFromList(us));
+ });
+
+ return layerGraphs;
+}
+
+},{"cp-data":5,"graphlib":28}],16:[function(require,module,exports){
+var crossCount = require('./crossCount'),
+ util = require('../util');
+
+module.exports = initOrder;
+
+/*
+ * Given a graph with a set of layered nodes (i.e. nodes that have a `rank`
+ * attribute) this function attaches an `order` attribute that uniquely
+ * arranges each node of each rank. If no constraint graph is provided the
+ * order of the nodes in each rank is entirely arbitrary.
+ */
+function initOrder(g, random) {
+ var layers = [];
+
+ g.eachNode(function(u, value) {
+ var layer = layers[value.rank];
+ if (g.children && g.children(u).length > 0) return;
+ if (!layer) {
+ layer = layers[value.rank] = [];
+ }
+ layer.push(u);
+ });
+
+ layers.forEach(function(layer) {
+ if (random) {
+ util.shuffle(layer);
+ }
+ layer.forEach(function(u, i) {
+ g.node(u).order = i;
+ });
+ });
+
+ var cc = crossCount(g);
+ g.graph().orderInitCC = cc;
+ g.graph().orderCC = Number.MAX_VALUE;
+}
+
+},{"../util":26,"./crossCount":14}],17:[function(require,module,exports){
+var util = require('../util');
+/*
+ Digraph = require('graphlib').Digraph,
+ topsort = require('graphlib').alg.topsort,
+ nodesFromList = require('graphlib').filter.nodesFromList;
+*/
+
+module.exports = sortLayer;
+
+/*
+function sortLayer(g, cg, weights) {
+ var result = sortLayerSubgraph(g, null, cg, weights);
+ result.list.forEach(function(u, i) {
+ g.node(u).order = i;
+ });
+ return result.constraintGraph;
+}
+*/
+
+function sortLayer(g, cg, weights) {
+ var ordering = [];
+ var bs = {};
+ g.eachNode(function(u, value) {
+ ordering[value.order] = u;
+ var ws = weights[u];
+ if (ws.length) {
+ bs[u] = util.sum(ws) / ws.length;
+ }
+ });
+
+ var toSort = g.nodes().filter(function(u) { return bs[u] !== undefined; });
+ toSort.sort(function(x, y) {
+ return bs[x] - bs[y] || g.node(x).order - g.node(y).order;
+ });
+
+ for (var i = 0, j = 0, jl = toSort.length; j < jl; ++i) {
+ if (bs[ordering[i]] !== undefined) {
+ g.node(toSort[j++]).order = i;
+ }
+ }
+}
+
+// TOOD: re-enable constrained sorting once we have a strategy for handling
+// undefined barycenters.
+/*
+function sortLayerSubgraph(g, sg, cg, weights) {
+ cg = cg ? cg.filterNodes(nodesFromList(g.children(sg))) : new Digraph();
+
+ var nodeData = {};
+ g.children(sg).forEach(function(u) {
+ if (g.children(u).length) {
+ nodeData[u] = sortLayerSubgraph(g, u, cg, weights);
+ nodeData[u].firstSG = u;
+ nodeData[u].lastSG = u;
+ } else {
+ var ws = weights[u];
+ nodeData[u] = {
+ degree: ws.length,
+ barycenter: ws.length > 0 ? util.sum(ws) / ws.length : 0,
+ list: [u]
+ };
+ }
+ });
+
+ resolveViolatedConstraints(g, cg, nodeData);
+
+ var keys = Object.keys(nodeData);
+ keys.sort(function(x, y) {
+ return nodeData[x].barycenter - nodeData[y].barycenter;
+ });
+
+ var result = keys.map(function(u) { return nodeData[u]; })
+ .reduce(function(lhs, rhs) { return mergeNodeData(g, lhs, rhs); });
+ return result;
+}
+
+/*
+function mergeNodeData(g, lhs, rhs) {
+ var cg = mergeDigraphs(lhs.constraintGraph, rhs.constraintGraph);
+
+ if (lhs.lastSG !== undefined && rhs.firstSG !== undefined) {
+ if (cg === undefined) {
+ cg = new Digraph();
+ }
+ if (!cg.hasNode(lhs.lastSG)) { cg.addNode(lhs.lastSG); }
+ cg.addNode(rhs.firstSG);
+ cg.addEdge(null, lhs.lastSG, rhs.firstSG);
+ }
+
+ return {
+ degree: lhs.degree + rhs.degree,
+ barycenter: (lhs.barycenter * lhs.degree + rhs.barycenter * rhs.degree) /
+ (lhs.degree + rhs.degree),
+ list: lhs.list.concat(rhs.list),
+ firstSG: lhs.firstSG !== undefined ? lhs.firstSG : rhs.firstSG,
+ lastSG: rhs.lastSG !== undefined ? rhs.lastSG : lhs.lastSG,
+ constraintGraph: cg
+ };
+}
+
+function mergeDigraphs(lhs, rhs) {
+ if (lhs === undefined) return rhs;
+ if (rhs === undefined) return lhs;
+
+ lhs = lhs.copy();
+ rhs.nodes().forEach(function(u) { lhs.addNode(u); });
+ rhs.edges().forEach(function(e, u, v) { lhs.addEdge(null, u, v); });
+ return lhs;
+}
+
+function resolveViolatedConstraints(g, cg, nodeData) {
+ // Removes nodes `u` and `v` from `cg` and makes any edges incident on them
+ // incident on `w` instead.
+ function collapseNodes(u, v, w) {
+ // TODO original paper removes self loops, but it is not obvious when this would happen
+ cg.inEdges(u).forEach(function(e) {
+ cg.delEdge(e);
+ cg.addEdge(null, cg.source(e), w);
+ });
+
+ cg.outEdges(v).forEach(function(e) {
+ cg.delEdge(e);
+ cg.addEdge(null, w, cg.target(e));
+ });
+
+ cg.delNode(u);
+ cg.delNode(v);
+ }
+
+ var violated;
+ while ((violated = findViolatedConstraint(cg, nodeData)) !== undefined) {
+ var source = cg.source(violated),
+ target = cg.target(violated);
+
+ var v;
+ while ((v = cg.addNode(null)) && g.hasNode(v)) {
+ cg.delNode(v);
+ }
+
+ // Collapse barycenter and list
+ nodeData[v] = mergeNodeData(g, nodeData[source], nodeData[target]);
+ delete nodeData[source];
+ delete nodeData[target];
+
+ collapseNodes(source, target, v);
+ if (cg.incidentEdges(v).length === 0) { cg.delNode(v); }
+ }
+}
+
+function findViolatedConstraint(cg, nodeData) {
+ var us = topsort(cg);
+ for (var i = 0; i < us.length; ++i) {
+ var u = us[i];
+ var inEdges = cg.inEdges(u);
+ for (var j = 0; j < inEdges.length; ++j) {
+ var e = inEdges[j];
+ if (nodeData[cg.source(e)].barycenter >= nodeData[u].barycenter) {
+ return e;
+ }
+ }
+ }
+}
+*/
+
+},{"../util":26}],18:[function(require,module,exports){
+var util = require('./util');
+
+/*
+ * The algorithms here are based on Brandes and Köpf, "Fast and Simple
+ * Horizontal Coordinate Assignment".
+ */
+module.exports = function() {
+ // External configuration
+ var config = {
+ nodeSep: 50,
+ edgeSep: 10,
+ universalSep: null,
+ rankSep: 30
+ };
+
+ var self = {};
+
+ self.nodeSep = util.propertyAccessor(self, config, 'nodeSep');
+ self.edgeSep = util.propertyAccessor(self, config, 'edgeSep');
+ // If not null this separation value is used for all nodes and edges
+ // regardless of their widths. `nodeSep` and `edgeSep` are ignored with this
+ // option.
+ self.universalSep = util.propertyAccessor(self, config, 'universalSep');
+ self.rankSep = util.propertyAccessor(self, config, 'rankSep');
+ self.debugLevel = util.propertyAccessor(self, config, 'debugLevel');
+
+ self.run = run;
+
+ return self;
+
+ function run(g) {
+ g = g.filterNodes(util.filterNonSubgraphs(g));
+
+ var layering = util.ordering(g);
+
+ var conflicts = findConflicts(g, layering);
+
+ var xss = {};
+ ['u', 'd'].forEach(function(vertDir) {
+ if (vertDir === 'd') layering.reverse();
+
+ ['l', 'r'].forEach(function(horizDir) {
+ if (horizDir === 'r') reverseInnerOrder(layering);
+
+ var dir = vertDir + horizDir;
+ var align = verticalAlignment(g, layering, conflicts, vertDir === 'u' ? 'predecessors' : 'successors');
+ xss[dir]= horizontalCompaction(g, layering, align.pos, align.root, align.align);
+
+ if (config.debugLevel >= 3)
+ debugPositioning(vertDir + horizDir, g, layering, xss[dir]);
+
+ if (horizDir === 'r') flipHorizontally(xss[dir]);
+
+ if (horizDir === 'r') reverseInnerOrder(layering);
+ });
+
+ if (vertDir === 'd') layering.reverse();
+ });
+
+ balance(g, layering, xss);
+
+ g.eachNode(function(v) {
+ var xs = [];
+ for (var alignment in xss) {
+ var alignmentX = xss[alignment][v];
+ posXDebug(alignment, g, v, alignmentX);
+ xs.push(alignmentX);
+ }
+ xs.sort(function(x, y) { return x - y; });
+ posX(g, v, (xs[1] + xs[2]) / 2);
+ });
+
+ // Align y coordinates with ranks
+ var y = 0, reverseY = g.graph().rankDir === 'BT' || g.graph().rankDir === 'RL';
+ layering.forEach(function(layer) {
+ var maxHeight = util.max(layer.map(function(u) { return height(g, u); }));
+ y += maxHeight / 2;
+ layer.forEach(function(u) {
+ posY(g, u, reverseY ? -y : y);
+ });
+ y += maxHeight / 2 + config.rankSep;
+ });
+
+ // Translate layout so that top left corner of bounding rectangle has
+ // coordinate (0, 0).
+ var minX = util.min(g.nodes().map(function(u) { return posX(g, u) - width(g, u) / 2; }));
+ var minY = util.min(g.nodes().map(function(u) { return posY(g, u) - height(g, u) / 2; }));
+ g.eachNode(function(u) {
+ posX(g, u, posX(g, u) - minX);
+ posY(g, u, posY(g, u) - minY);
+ });
+ }
+
+ /*
+ * Generate an ID that can be used to represent any undirected edge that is
+ * incident on `u` and `v`.
+ */
+ function undirEdgeId(u, v) {
+ return u < v
+ ? u.toString().length + ':' + u + '-' + v
+ : v.toString().length + ':' + v + '-' + u;
+ }
+
+ function findConflicts(g, layering) {
+ var conflicts = {}, // Set of conflicting edge ids
+ pos = {}, // Position of node in its layer
+ prevLayer,
+ currLayer,
+ k0, // Position of the last inner segment in the previous layer
+ l, // Current position in the current layer (for iteration up to `l1`)
+ k1; // Position of the next inner segment in the previous layer or
+ // the position of the last element in the previous layer
+
+ if (layering.length <= 2) return conflicts;
+
+ function updateConflicts(v) {
+ var k = pos[v];
+ if (k < k0 || k > k1) {
+ conflicts[undirEdgeId(currLayer[l], v)] = true;
+ }
+ }
+
+ layering[1].forEach(function(u, i) { pos[u] = i; });
+ for (var i = 1; i < layering.length - 1; ++i) {
+ prevLayer = layering[i];
+ currLayer = layering[i+1];
+ k0 = 0;
+ l = 0;
+
+ // Scan current layer for next node that is incident to an inner segement
+ // between layering[i+1] and layering[i].
+ for (var l1 = 0; l1 < currLayer.length; ++l1) {
+ var u = currLayer[l1]; // Next inner segment in the current layer or
+ // last node in the current layer
+ pos[u] = l1;
+ k1 = undefined;
+
+ if (g.node(u).dummy) {
+ var uPred = g.predecessors(u)[0];
+ // Note: In the case of self loops and sideways edges it is possible
+ // for a dummy not to have a predecessor.
+ if (uPred !== undefined && g.node(uPred).dummy)
+ k1 = pos[uPred];
+ }
+ if (k1 === undefined && l1 === currLayer.length - 1)
+ k1 = prevLayer.length - 1;
+
+ if (k1 !== undefined) {
+ for (; l <= l1; ++l) {
+ g.predecessors(currLayer[l]).forEach(updateConflicts);
+ }
+ k0 = k1;
+ }
+ }
+ }
+
+ return conflicts;
+ }
+
+ function verticalAlignment(g, layering, conflicts, relationship) {
+ var pos = {}, // Position for a node in its layer
+ root = {}, // Root of the block that the node participates in
+ align = {}; // Points to the next node in the block or, if the last
+ // element in the block, points to the first block's root
+
+ layering.forEach(function(layer) {
+ layer.forEach(function(u, i) {
+ root[u] = u;
+ align[u] = u;
+ pos[u] = i;
+ });
+ });
+
+ layering.forEach(function(layer) {
+ var prevIdx = -1;
+ layer.forEach(function(v) {
+ var related = g[relationship](v), // Adjacent nodes from the previous layer
+ mid; // The mid point in the related array
+
+ if (related.length > 0) {
+ related.sort(function(x, y) { return pos[x] - pos[y]; });
+ mid = (related.length - 1) / 2;
+ related.slice(Math.floor(mid), Math.ceil(mid) + 1).forEach(function(u) {
+ if (align[v] === v) {
+ if (!conflicts[undirEdgeId(u, v)] && prevIdx < pos[u]) {
+ align[u] = v;
+ align[v] = root[v] = root[u];
+ prevIdx = pos[u];
+ }
+ }
+ });
+ }
+ });
+ });
+
+ return { pos: pos, root: root, align: align };
+ }
+
+ // This function deviates from the standard BK algorithm in two ways. First
+ // it takes into account the size of the nodes. Second it includes a fix to
+ // the original algorithm that is described in Carstens, "Node and Label
+ // Placement in a Layered Layout Algorithm".
+ function horizontalCompaction(g, layering, pos, root, align) {
+ var sink = {}, // Mapping of node id -> sink node id for class
+ maybeShift = {}, // Mapping of sink node id -> { class node id, min shift }
+ shift = {}, // Mapping of sink node id -> shift
+ pred = {}, // Mapping of node id -> predecessor node (or null)
+ xs = {}; // Calculated X positions
+
+ layering.forEach(function(layer) {
+ layer.forEach(function(u, i) {
+ sink[u] = u;
+ maybeShift[u] = {};
+ if (i > 0)
+ pred[u] = layer[i - 1];
+ });
+ });
+
+ function updateShift(toShift, neighbor, delta) {
+ if (!(neighbor in maybeShift[toShift])) {
+ maybeShift[toShift][neighbor] = delta;
+ } else {
+ maybeShift[toShift][neighbor] = Math.min(maybeShift[toShift][neighbor], delta);
+ }
+ }
+
+ function placeBlock(v) {
+ if (!(v in xs)) {
+ xs[v] = 0;
+ var w = v;
+ do {
+ if (pos[w] > 0) {
+ var u = root[pred[w]];
+ placeBlock(u);
+ if (sink[v] === v) {
+ sink[v] = sink[u];
+ }
+ var delta = sep(g, pred[w]) + sep(g, w);
+ if (sink[v] !== sink[u]) {
+ updateShift(sink[u], sink[v], xs[v] - xs[u] - delta);
+ } else {
+ xs[v] = Math.max(xs[v], xs[u] + delta);
+ }
+ }
+ w = align[w];
+ } while (w !== v);
+ }
+ }
+
+ // Root coordinates relative to sink
+ util.values(root).forEach(function(v) {
+ placeBlock(v);
+ });
+
+ // Absolute coordinates
+ // There is an assumption here that we've resolved shifts for any classes
+ // that begin at an earlier layer. We guarantee this by visiting layers in
+ // order.
+ layering.forEach(function(layer) {
+ layer.forEach(function(v) {
+ xs[v] = xs[root[v]];
+ if (v === root[v] && v === sink[v]) {
+ var minShift = 0;
+ if (v in maybeShift && Object.keys(maybeShift[v]).length > 0) {
+ minShift = util.min(Object.keys(maybeShift[v])
+ .map(function(u) {
+ return maybeShift[v][u] + (u in shift ? shift[u] : 0);
+ }
+ ));
+ }
+ shift[v] = minShift;
+ }
+ });
+ });
+
+ layering.forEach(function(layer) {
+ layer.forEach(function(v) {
+ xs[v] += shift[sink[root[v]]] || 0;
+ });
+ });
+
+ return xs;
+ }
+
+ function findMinCoord(g, layering, xs) {
+ return util.min(layering.map(function(layer) {
+ var u = layer[0];
+ return xs[u];
+ }));
+ }
+
+ function findMaxCoord(g, layering, xs) {
+ return util.max(layering.map(function(layer) {
+ var u = layer[layer.length - 1];
+ return xs[u];
+ }));
+ }
+
+ function balance(g, layering, xss) {
+ var min = {}, // Min coordinate for the alignment
+ max = {}, // Max coordinate for the alginment
+ smallestAlignment,
+ shift = {}; // Amount to shift a given alignment
+
+ function updateAlignment(v) {
+ xss[alignment][v] += shift[alignment];
+ }
+
+ var smallest = Number.POSITIVE_INFINITY;
+ for (var alignment in xss) {
+ var xs = xss[alignment];
+ min[alignment] = findMinCoord(g, layering, xs);
+ max[alignment] = findMaxCoord(g, layering, xs);
+ var w = max[alignment] - min[alignment];
+ if (w < smallest) {
+ smallest = w;
+ smallestAlignment = alignment;
+ }
+ }
+
+ // Determine how much to adjust positioning for each alignment
+ ['u', 'd'].forEach(function(vertDir) {
+ ['l', 'r'].forEach(function(horizDir) {
+ var alignment = vertDir + horizDir;
+ shift[alignment] = horizDir === 'l'
+ ? min[smallestAlignment] - min[alignment]
+ : max[smallestAlignment] - max[alignment];
+ });
+ });
+
+ // Find average of medians for xss array
+ for (alignment in xss) {
+ g.eachNode(updateAlignment);
+ }
+ }
+
+ function flipHorizontally(xs) {
+ for (var u in xs) {
+ xs[u] = -xs[u];
+ }
+ }
+
+ function reverseInnerOrder(layering) {
+ layering.forEach(function(layer) {
+ layer.reverse();
+ });
+ }
+
+ function width(g, u) {
+ switch (g.graph().rankDir) {
+ case 'LR': return g.node(u).height;
+ case 'RL': return g.node(u).height;
+ default: return g.node(u).width;
+ }
+ }
+
+ function height(g, u) {
+ switch(g.graph().rankDir) {
+ case 'LR': return g.node(u).width;
+ case 'RL': return g.node(u).width;
+ default: return g.node(u).height;
+ }
+ }
+
+ function sep(g, u) {
+ if (config.universalSep !== null) {
+ return config.universalSep;
+ }
+ var w = width(g, u);
+ var s = g.node(u).dummy ? config.edgeSep : config.nodeSep;
+ return (w + s) / 2;
+ }
+
+ function posX(g, u, x) {
+ if (g.graph().rankDir === 'LR' || g.graph().rankDir === 'RL') {
+ if (arguments.length < 3) {
+ return g.node(u).y;
+ } else {
+ g.node(u).y = x;
+ }
+ } else {
+ if (arguments.length < 3) {
+ return g.node(u).x;
+ } else {
+ g.node(u).x = x;
+ }
+ }
+ }
+
+ function posXDebug(name, g, u, x) {
+ if (g.graph().rankDir === 'LR' || g.graph().rankDir === 'RL') {
+ if (arguments.length < 3) {
+ return g.node(u)[name];
+ } else {
+ g.node(u)[name] = x;
+ }
+ } else {
+ if (arguments.length < 3) {
+ return g.node(u)[name];
+ } else {
+ g.node(u)[name] = x;
+ }
+ }
+ }
+
+ function posY(g, u, y) {
+ if (g.graph().rankDir === 'LR' || g.graph().rankDir === 'RL') {
+ if (arguments.length < 3) {
+ return g.node(u).x;
+ } else {
+ g.node(u).x = y;
+ }
+ } else {
+ if (arguments.length < 3) {
+ return g.node(u).y;
+ } else {
+ g.node(u).y = y;
+ }
+ }
+ }
+
+ function debugPositioning(align, g, layering, xs) {
+ layering.forEach(function(l, li) {
+ var u, xU;
+ l.forEach(function(v) {
+ var xV = xs[v];
+ if (u) {
+ var s = sep(g, u) + sep(g, v);
+ if (xV - xU < s)
+ console.log('Position phase: sep violation. Align: ' + align + '. Layer: ' + li + '. ' +
+ 'U: ' + u + ' V: ' + v + '. Actual sep: ' + (xV - xU) + ' Expected sep: ' + s);
+ }
+ u = v;
+ xU = xV;
+ });
+ });
+ }
+};
+
+},{"./util":26}],19:[function(require,module,exports){
+var util = require('./util'),
+ acyclic = require('./rank/acyclic'),
+ initRank = require('./rank/initRank'),
+ feasibleTree = require('./rank/feasibleTree'),
+ constraints = require('./rank/constraints'),
+ simplex = require('./rank/simplex'),
+ components = require('graphlib').alg.components,
+ filter = require('graphlib').filter;
+
+exports.run = run;
+exports.restoreEdges = restoreEdges;
+
+/*
+ * Heuristic function that assigns a rank to each node of the input graph with
+ * the intent of minimizing edge lengths, while respecting the `minLen`
+ * attribute of incident edges.
+ *
+ * Prerequisites:
+ *
+ * * Each edge in the input graph must have an assigned 'minLen' attribute
+ */
+function run(g, useSimplex) {
+ expandSelfLoops(g);
+
+ // If there are rank constraints on nodes, then build a new graph that
+ // encodes the constraints.
+ util.time('constraints.apply', constraints.apply)(g);
+
+ expandSidewaysEdges(g);
+
+ // Reverse edges to get an acyclic graph, we keep the graph in an acyclic
+ // state until the very end.
+ util.time('acyclic', acyclic)(g);
+
+ // Convert the graph into a flat graph for ranking
+ var flatGraph = g.filterNodes(util.filterNonSubgraphs(g));
+
+ // Assign an initial ranking using DFS.
+ initRank(flatGraph);
+
+ // For each component improve the assigned ranks.
+ components(flatGraph).forEach(function(cmpt) {
+ var subgraph = flatGraph.filterNodes(filter.nodesFromList(cmpt));
+ rankComponent(subgraph, useSimplex);
+ });
+
+ // Relax original constraints
+ util.time('constraints.relax', constraints.relax(g));
+
+ // When handling nodes with constrained ranks it is possible to end up with
+ // edges that point to previous ranks. Most of the subsequent algorithms assume
+ // that edges are pointing to successive ranks only. Here we reverse any "back
+ // edges" and mark them as such. The acyclic algorithm will reverse them as a
+ // post processing step.
+ util.time('reorientEdges', reorientEdges)(g);
+}
+
+function restoreEdges(g) {
+ acyclic.undo(g);
+}
+
+/*
+ * Expand self loops into three dummy nodes. One will sit above the incident
+ * node, one will be at the same level, and one below. The result looks like:
+ *
+ * /--<--x--->--\
+ * node y
+ * \--<--z--->--/
+ *
+ * Dummy nodes x, y, z give us the shape of a loop and node y is where we place
+ * the label.
+ *
+ * TODO: consolidate knowledge of dummy node construction.
+ * TODO: support minLen = 2
+ */
+function expandSelfLoops(g) {
+ g.eachEdge(function(e, u, v, a) {
+ if (u === v) {
+ var x = addDummyNode(g, e, u, v, a, 0, false),
+ y = addDummyNode(g, e, u, v, a, 1, true),
+ z = addDummyNode(g, e, u, v, a, 2, false);
+ g.addEdge(null, x, u, {minLen: 1, selfLoop: true});
+ g.addEdge(null, x, y, {minLen: 1, selfLoop: true});
+ g.addEdge(null, u, z, {minLen: 1, selfLoop: true});
+ g.addEdge(null, y, z, {minLen: 1, selfLoop: true});
+ g.delEdge(e);
+ }
+ });
+}
+
+function expandSidewaysEdges(g) {
+ g.eachEdge(function(e, u, v, a) {
+ if (u === v) {
+ var origEdge = a.originalEdge,
+ dummy = addDummyNode(g, origEdge.e, origEdge.u, origEdge.v, origEdge.value, 0, true);
+ g.addEdge(null, u, dummy, {minLen: 1});
+ g.addEdge(null, dummy, v, {minLen: 1});
+ g.delEdge(e);
+ }
+ });
+}
+
+function addDummyNode(g, e, u, v, a, index, isLabel) {
+ return g.addNode(null, {
+ width: isLabel ? a.width : 0,
+ height: isLabel ? a.height : 0,
+ edge: { id: e, source: u, target: v, attrs: a },
+ dummy: true,
+ index: index
+ });
+}
+
+function reorientEdges(g) {
+ g.eachEdge(function(e, u, v, value) {
+ if (g.node(u).rank > g.node(v).rank) {
+ g.delEdge(e);
+ value.reversed = true;
+ g.addEdge(e, v, u, value);
+ }
+ });
+}
+
+function rankComponent(subgraph, useSimplex) {
+ var spanningTree = feasibleTree(subgraph);
+
+ if (useSimplex) {
+ util.log(1, 'Using network simplex for ranking');
+ simplex(subgraph, spanningTree);
+ }
+ normalize(subgraph);
+}
+
+function normalize(g) {
+ var m = util.min(g.nodes().map(function(u) { return g.node(u).rank; }));
+ g.eachNode(function(u, node) { node.rank -= m; });
+}
+
+},{"./rank/acyclic":20,"./rank/constraints":21,"./rank/feasibleTree":22,"./rank/initRank":23,"./rank/simplex":25,"./util":26,"graphlib":28}],20:[function(require,module,exports){
+var util = require('../util');
+
+module.exports = acyclic;
+module.exports.undo = undo;
+
+/*
+ * This function takes a directed graph that may have cycles and reverses edges
+ * as appropriate to break these cycles. Each reversed edge is assigned a
+ * `reversed` attribute with the value `true`.
+ *
+ * There should be no self loops in the graph.
+ */
+function acyclic(g) {
+ var onStack = {},
+ visited = {},
+ reverseCount = 0;
+
+ function dfs(u) {
+ if (u in visited) return;
+ visited[u] = onStack[u] = true;
+ g.outEdges(u).forEach(function(e) {
+ var t = g.target(e),
+ value;
+
+ if (u === t) {
+ console.error('Warning: found self loop "' + e + '" for node "' + u + '"');
+ } else if (t in onStack) {
+ value = g.edge(e);
+ g.delEdge(e);
+ value.reversed = true;
+ ++reverseCount;
+ g.addEdge(e, t, u, value);
+ } else {
+ dfs(t);
+ }
+ });
+
+ delete onStack[u];
+ }
+
+ g.eachNode(function(u) { dfs(u); });
+
+ util.log(2, 'Acyclic Phase: reversed ' + reverseCount + ' edge(s)');
+
+ return reverseCount;
+}
+
+/*
+ * Given a graph that has had the acyclic operation applied, this function
+ * undoes that operation. More specifically, any edge with the `reversed`
+ * attribute is again reversed to restore the original direction of the edge.
+ */
+function undo(g) {
+ g.eachEdge(function(e, s, t, a) {
+ if (a.reversed) {
+ delete a.reversed;
+ g.delEdge(e);
+ g.addEdge(e, t, s, a);
+ }
+ });
+}
+
+},{"../util":26}],21:[function(require,module,exports){
+exports.apply = function(g) {
+ function dfs(sg) {
+ var rankSets = {};
+ g.children(sg).forEach(function(u) {
+ if (g.children(u).length) {
+ dfs(u);
+ return;
+ }
+
+ var value = g.node(u),
+ prefRank = value.prefRank;
+ if (prefRank !== undefined) {
+ if (!checkSupportedPrefRank(prefRank)) { return; }
+
+ if (!(prefRank in rankSets)) {
+ rankSets.prefRank = [u];
+ } else {
+ rankSets.prefRank.push(u);
+ }
+
+ var newU = rankSets[prefRank];
+ if (newU === undefined) {
+ newU = rankSets[prefRank] = g.addNode(null, { originalNodes: [] });
+ g.parent(newU, sg);
+ }
+
+ redirectInEdges(g, u, newU, prefRank === 'min');
+ redirectOutEdges(g, u, newU, prefRank === 'max');
+
+ // Save original node and remove it from reduced graph
+ g.node(newU).originalNodes.push({ u: u, value: value, parent: sg });
+ g.delNode(u);
+ }
+ });
+
+ addLightEdgesFromMinNode(g, sg, rankSets.min);
+ addLightEdgesToMaxNode(g, sg, rankSets.max);
+ }
+
+ dfs(null);
+};
+
+function checkSupportedPrefRank(prefRank) {
+ if (prefRank !== 'min' && prefRank !== 'max' && prefRank.indexOf('same_') !== 0) {
+ console.error('Unsupported rank type: ' + prefRank);
+ return false;
+ }
+ return true;
+}
+
+function redirectInEdges(g, u, newU, reverse) {
+ g.inEdges(u).forEach(function(e) {
+ var origValue = g.edge(e),
+ value;
+ if (origValue.originalEdge) {
+ value = origValue;
+ } else {
+ value = {
+ originalEdge: { e: e, u: g.source(e), v: g.target(e), value: origValue },
+ minLen: g.edge(e).minLen
+ };
+ }
+
+ // Do not reverse edges for self-loops.
+ if (origValue.selfLoop) {
+ reverse = false;
+ }
+
+ if (reverse) {
+ // Ensure that all edges to min are reversed
+ g.addEdge(null, newU, g.source(e), value);
+ value.reversed = true;
+ } else {
+ g.addEdge(null, g.source(e), newU, value);
+ }
+ });
+}
+
+function redirectOutEdges(g, u, newU, reverse) {
+ g.outEdges(u).forEach(function(e) {
+ var origValue = g.edge(e),
+ value;
+ if (origValue.originalEdge) {
+ value = origValue;
+ } else {
+ value = {
+ originalEdge: { e: e, u: g.source(e), v: g.target(e), value: origValue },
+ minLen: g.edge(e).minLen
+ };
+ }
+
+ // Do not reverse edges for self-loops.
+ if (origValue.selfLoop) {
+ reverse = false;
+ }
+
+ if (reverse) {
+ // Ensure that all edges from max are reversed
+ g.addEdge(null, g.target(e), newU, value);
+ value.reversed = true;
+ } else {
+ g.addEdge(null, newU, g.target(e), value);
+ }
+ });
+}
+
+function addLightEdgesFromMinNode(g, sg, minNode) {
+ if (minNode !== undefined) {
+ g.children(sg).forEach(function(u) {
+ // The dummy check ensures we don't add an edge if the node is involved
+ // in a self loop or sideways edge.
+ if (u !== minNode && !g.outEdges(minNode, u).length && !g.node(u).dummy) {
+ g.addEdge(null, minNode, u, { minLen: 0 });
+ }
+ });
+ }
+}
+
+function addLightEdgesToMaxNode(g, sg, maxNode) {
+ if (maxNode !== undefined) {
+ g.children(sg).forEach(function(u) {
+ // The dummy check ensures we don't add an edge if the node is involved
+ // in a self loop or sideways edge.
+ if (u !== maxNode && !g.outEdges(u, maxNode).length && !g.node(u).dummy) {
+ g.addEdge(null, u, maxNode, { minLen: 0 });
+ }
+ });
+ }
+}
+
+/*
+ * This function "relaxes" the constraints applied previously by the "apply"
+ * function. It expands any nodes that were collapsed and assigns the rank of
+ * the collapsed node to each of the expanded nodes. It also restores the
+ * original edges and removes any dummy edges pointing at the collapsed nodes.
+ *
+ * Note that the process of removing collapsed nodes also removes dummy edges
+ * automatically.
+ */
+exports.relax = function(g) {
+ // Save original edges
+ var originalEdges = [];
+ g.eachEdge(function(e, u, v, value) {
+ var originalEdge = value.originalEdge;
+ if (originalEdge) {
+ originalEdges.push(originalEdge);
+ }
+ });
+
+ // Expand collapsed nodes
+ g.eachNode(function(u, value) {
+ var originalNodes = value.originalNodes;
+ if (originalNodes) {
+ originalNodes.forEach(function(originalNode) {
+ originalNode.value.rank = value.rank;
+ g.addNode(originalNode.u, originalNode.value);
+ g.parent(originalNode.u, originalNode.parent);
+ });
+ g.delNode(u);
+ }
+ });
+
+ // Restore original edges
+ originalEdges.forEach(function(edge) {
+ g.addEdge(edge.e, edge.u, edge.v, edge.value);
+ });
+};
+
+},{}],22:[function(require,module,exports){
+/* jshint -W079 */
+var Set = require('cp-data').Set,
+/* jshint +W079 */
+ Digraph = require('graphlib').Digraph,
+ util = require('../util');
+
+module.exports = feasibleTree;
+
+/*
+ * Given an acyclic graph with each node assigned a `rank` attribute, this
+ * function constructs and returns a spanning tree. This function may reduce
+ * the length of some edges from the initial rank assignment while maintaining
+ * the `minLen` specified by each edge.
+ *
+ * Prerequisites:
+ *
+ * * The input graph is acyclic
+ * * Each node in the input graph has an assigned `rank` attribute
+ * * Each edge in the input graph has an assigned `minLen` attribute
+ *
+ * Outputs:
+ *
+ * A feasible spanning tree for the input graph (i.e. a spanning tree that
+ * respects each graph edge's `minLen` attribute) represented as a Digraph with
+ * a `root` attribute on graph.
+ *
+ * Nodes have the same id and value as that in the input graph.
+ *
+ * Edges in the tree have arbitrarily assigned ids. The attributes for edges
+ * include `reversed`. `reversed` indicates that the edge is a
+ * back edge in the input graph.
+ */
+function feasibleTree(g) {
+ var remaining = new Set(g.nodes()),
+ tree = new Digraph();
+
+ if (remaining.size() === 1) {
+ var root = g.nodes()[0];
+ tree.addNode(root, {});
+ tree.graph({ root: root });
+ return tree;
+ }
+
+ function addTightEdges(v) {
+ var continueToScan = true;
+ g.predecessors(v).forEach(function(u) {
+ if (remaining.has(u) && !slack(g, u, v)) {
+ if (remaining.has(v)) {
+ tree.addNode(v, {});
+ remaining.remove(v);
+ tree.graph({ root: v });
+ }
+
+ tree.addNode(u, {});
+ tree.addEdge(null, u, v, { reversed: true });
+ remaining.remove(u);
+ addTightEdges(u);
+ continueToScan = false;
+ }
+ });
+
+ g.successors(v).forEach(function(w) {
+ if (remaining.has(w) && !slack(g, v, w)) {
+ if (remaining.has(v)) {
+ tree.addNode(v, {});
+ remaining.remove(v);
+ tree.graph({ root: v });
+ }
+
+ tree.addNode(w, {});
+ tree.addEdge(null, v, w, {});
+ remaining.remove(w);
+ addTightEdges(w);
+ continueToScan = false;
+ }
+ });
+ return continueToScan;
+ }
+
+ function createTightEdge() {
+ var minSlack = Number.MAX_VALUE;
+ remaining.keys().forEach(function(v) {
+ g.predecessors(v).forEach(function(u) {
+ if (!remaining.has(u)) {
+ var edgeSlack = slack(g, u, v);
+ if (Math.abs(edgeSlack) < Math.abs(minSlack)) {
+ minSlack = -edgeSlack;
+ }
+ }
+ });
+
+ g.successors(v).forEach(function(w) {
+ if (!remaining.has(w)) {
+ var edgeSlack = slack(g, v, w);
+ if (Math.abs(edgeSlack) < Math.abs(minSlack)) {
+ minSlack = edgeSlack;
+ }
+ }
+ });
+ });
+
+ tree.eachNode(function(u) { g.node(u).rank -= minSlack; });
+ }
+
+ while (remaining.size()) {
+ var nodesToSearch = !tree.order() ? remaining.keys() : tree.nodes();
+ for (var i = 0, il = nodesToSearch.length;
+ i < il && addTightEdges(nodesToSearch[i]);
+ ++i);
+ if (remaining.size()) {
+ createTightEdge();
+ }
+ }
+
+ return tree;
+}
+
+function slack(g, u, v) {
+ var rankDiff = g.node(v).rank - g.node(u).rank;
+ var maxMinLen = util.max(g.outEdges(u, v)
+ .map(function(e) { return g.edge(e).minLen; }));
+ return rankDiff - maxMinLen;
+}
+
+},{"../util":26,"cp-data":5,"graphlib":28}],23:[function(require,module,exports){
+var util = require('../util'),
+ topsort = require('graphlib').alg.topsort;
+
+module.exports = initRank;
+
+/*
+ * Assigns a `rank` attribute to each node in the input graph and ensures that
+ * this rank respects the `minLen` attribute of incident edges.
+ *
+ * Prerequisites:
+ *
+ * * The input graph must be acyclic
+ * * Each edge in the input graph must have an assigned 'minLen' attribute
+ */
+function initRank(g) {
+ var sorted = topsort(g);
+
+ sorted.forEach(function(u) {
+ var inEdges = g.inEdges(u);
+ if (inEdges.length === 0) {
+ g.node(u).rank = 0;
+ return;
+ }
+
+ var minLens = inEdges.map(function(e) {
+ return g.node(g.source(e)).rank + g.edge(e).minLen;
+ });
+ g.node(u).rank = util.max(minLens);
+ });
+}
+
+},{"../util":26,"graphlib":28}],24:[function(require,module,exports){
+module.exports = {
+ slack: slack
+};
+
+/*
+ * A helper to calculate the slack between two nodes (`u` and `v`) given a
+ * `minLen` constraint. The slack represents how much the distance between `u`
+ * and `v` could shrink while maintaining the `minLen` constraint. If the value
+ * is negative then the constraint is currently violated.
+ *
+ This function requires that `u` and `v` are in `graph` and they both have a
+ `rank` attribute.
+ */
+function slack(graph, u, v, minLen) {
+ return Math.abs(graph.node(u).rank - graph.node(v).rank) - minLen;
+}
+
+},{}],25:[function(require,module,exports){
+var util = require('../util'),
+ rankUtil = require('./rankUtil');
+
+module.exports = simplex;
+
+function simplex(graph, spanningTree) {
+ // The network simplex algorithm repeatedly replaces edges of
+ // the spanning tree with negative cut values until no such
+ // edge exists.
+ initCutValues(graph, spanningTree);
+ while (true) {
+ var e = leaveEdge(spanningTree);
+ if (e === null) break;
+ var f = enterEdge(graph, spanningTree, e);
+ exchange(graph, spanningTree, e, f);
+ }
+}
+
+/*
+ * Set the cut values of edges in the spanning tree by a depth-first
+ * postorder traversal. The cut value corresponds to the cost, in
+ * terms of a ranking's edge length sum, of lengthening an edge.
+ * Negative cut values typically indicate edges that would yield a
+ * smaller edge length sum if they were lengthened.
+ */
+function initCutValues(graph, spanningTree) {
+ computeLowLim(spanningTree);
+
+ spanningTree.eachEdge(function(id, u, v, treeValue) {
+ treeValue.cutValue = 0;
+ });
+
+ // Propagate cut values up the tree.
+ function dfs(n) {
+ var children = spanningTree.successors(n);
+ for (var c in children) {
+ var child = children[c];
+ dfs(child);
+ }
+ if (n !== spanningTree.graph().root) {
+ setCutValue(graph, spanningTree, n);
+ }
+ }
+ dfs(spanningTree.graph().root);
+}
+
+/*
+ * Perform a DFS postorder traversal, labeling each node v with
+ * its traversal order 'lim(v)' and the minimum traversal number
+ * of any of its descendants 'low(v)'. This provides an efficient
+ * way to test whether u is an ancestor of v since
+ * low(u) <= lim(v) <= lim(u) if and only if u is an ancestor.
+ */
+function computeLowLim(tree) {
+ var postOrderNum = 0;
+
+ function dfs(n) {
+ var children = tree.successors(n);
+ var low = postOrderNum;
+ for (var c in children) {
+ var child = children[c];
+ dfs(child);
+ low = Math.min(low, tree.node(child).low);
+ }
+ tree.node(n).low = low;
+ tree.node(n).lim = postOrderNum++;
+ }
+
+ dfs(tree.graph().root);
+}
+
+/*
+ * To compute the cut value of the edge parent -> child, we consider
+ * it and any other graph edges to or from the child.
+ * parent
+ * |
+ * child
+ * / \
+ * u v
+ */
+function setCutValue(graph, tree, child) {
+ var parentEdge = tree.inEdges(child)[0];
+
+ // List of child's children in the spanning tree.
+ var grandchildren = [];
+ var grandchildEdges = tree.outEdges(child);
+ for (var gce in grandchildEdges) {
+ grandchildren.push(tree.target(grandchildEdges[gce]));
+ }
+
+ var cutValue = 0;
+
+ // TODO: Replace unit increment/decrement with edge weights.
+ var E = 0; // Edges from child to grandchild's subtree.
+ var F = 0; // Edges to child from grandchild's subtree.
+ var G = 0; // Edges from child to nodes outside of child's subtree.
+ var H = 0; // Edges from nodes outside of child's subtree to child.
+
+ // Consider all graph edges from child.
+ var outEdges = graph.outEdges(child);
+ var gc;
+ for (var oe in outEdges) {
+ var succ = graph.target(outEdges[oe]);
+ for (gc in grandchildren) {
+ if (inSubtree(tree, succ, grandchildren[gc])) {
+ E++;
+ }
+ }
+ if (!inSubtree(tree, succ, child)) {
+ G++;
+ }
+ }
+
+ // Consider all graph edges to child.
+ var inEdges = graph.inEdges(child);
+ for (var ie in inEdges) {
+ var pred = graph.source(inEdges[ie]);
+ for (gc in grandchildren) {
+ if (inSubtree(tree, pred, grandchildren[gc])) {
+ F++;
+ }
+ }
+ if (!inSubtree(tree, pred, child)) {
+ H++;
+ }
+ }
+
+ // Contributions depend on the alignment of the parent -> child edge
+ // and the child -> u or v edges.
+ var grandchildCutSum = 0;
+ for (gc in grandchildren) {
+ var cv = tree.edge(grandchildEdges[gc]).cutValue;
+ if (!tree.edge(grandchildEdges[gc]).reversed) {
+ grandchildCutSum += cv;
+ } else {
+ grandchildCutSum -= cv;
+ }
+ }
+
+ if (!tree.edge(parentEdge).reversed) {
+ cutValue += grandchildCutSum - E + F - G + H;
+ } else {
+ cutValue -= grandchildCutSum - E + F - G + H;
+ }
+
+ tree.edge(parentEdge).cutValue = cutValue;
+}
+
+/*
+ * Return whether n is a node in the subtree with the given
+ * root.
+ */
+function inSubtree(tree, n, root) {
+ return (tree.node(root).low <= tree.node(n).lim &&
+ tree.node(n).lim <= tree.node(root).lim);
+}
+
+/*
+ * Return an edge from the tree with a negative cut value, or null if there
+ * is none.
+ */
+function leaveEdge(tree) {
+ var edges = tree.edges();
+ for (var n in edges) {
+ var e = edges[n];
+ var treeValue = tree.edge(e);
+ if (treeValue.cutValue < 0) {
+ return e;
+ }
+ }
+ return null;
+}
+
+/*
+ * The edge e should be an edge in the tree, with an underlying edge
+ * in the graph, with a negative cut value. Of the two nodes incident
+ * on the edge, take the lower one. enterEdge returns an edge with
+ * minimum slack going from outside of that node's subtree to inside
+ * of that node's subtree.
+ */
+function enterEdge(graph, tree, e) {
+ var source = tree.source(e);
+ var target = tree.target(e);
+ var lower = tree.node(target).lim < tree.node(source).lim ? target : source;
+
+ // Is the tree edge aligned with the graph edge?
+ var aligned = !tree.edge(e).reversed;
+
+ var minSlack = Number.POSITIVE_INFINITY;
+ var minSlackEdge;
+ if (aligned) {
+ graph.eachEdge(function(id, u, v, value) {
+ if (id !== e && inSubtree(tree, u, lower) && !inSubtree(tree, v, lower)) {
+ var slack = rankUtil.slack(graph, u, v, value.minLen);
+ if (slack < minSlack) {
+ minSlack = slack;
+ minSlackEdge = id;
+ }
+ }
+ });
+ } else {
+ graph.eachEdge(function(id, u, v, value) {
+ if (id !== e && !inSubtree(tree, u, lower) && inSubtree(tree, v, lower)) {
+ var slack = rankUtil.slack(graph, u, v, value.minLen);
+ if (slack < minSlack) {
+ minSlack = slack;
+ minSlackEdge = id;
+ }
+ }
+ });
+ }
+
+ if (minSlackEdge === undefined) {
+ var outside = [];
+ var inside = [];
+ graph.eachNode(function(id) {
+ if (!inSubtree(tree, id, lower)) {
+ outside.push(id);
+ } else {
+ inside.push(id);
+ }
+ });
+ throw new Error('No edge found from outside of tree to inside');
+ }
+
+ return minSlackEdge;
+}
+
+/*
+ * Replace edge e with edge f in the tree, recalculating the tree root,
+ * the nodes' low and lim properties and the edges' cut values.
+ */
+function exchange(graph, tree, e, f) {
+ tree.delEdge(e);
+ var source = graph.source(f);
+ var target = graph.target(f);
+
+ // Redirect edges so that target is the root of its subtree.
+ function redirect(v) {
+ var edges = tree.inEdges(v);
+ for (var i in edges) {
+ var e = edges[i];
+ var u = tree.source(e);
+ var value = tree.edge(e);
+ redirect(u);
+ tree.delEdge(e);
+ value.reversed = !value.reversed;
+ tree.addEdge(e, v, u, value);
+ }
+ }
+
+ redirect(target);
+
+ var root = source;
+ var edges = tree.inEdges(root);
+ while (edges.length > 0) {
+ root = tree.source(edges[0]);
+ edges = tree.inEdges(root);
+ }
+
+ tree.graph().root = root;
+
+ tree.addEdge(null, source, target, {cutValue: 0});
+
+ initCutValues(graph, tree);
+
+ adjustRanks(graph, tree);
+}
+
+/*
+ * Reset the ranks of all nodes based on the current spanning tree.
+ * The rank of the tree's root remains unchanged, while all other
+ * nodes are set to the sum of minimum length constraints along
+ * the path from the root.
+ */
+function adjustRanks(graph, tree) {
+ function dfs(p) {
+ var children = tree.successors(p);
+ children.forEach(function(c) {
+ var minLen = minimumLength(graph, p, c);
+ graph.node(c).rank = graph.node(p).rank + minLen;
+ dfs(c);
+ });
+ }
+
+ dfs(tree.graph().root);
+}
+
+/*
+ * If u and v are connected by some edges in the graph, return the
+ * minimum length of those edges, as a positive number if v succeeds
+ * u and as a negative number if v precedes u.
+ */
+function minimumLength(graph, u, v) {
+ var outEdges = graph.outEdges(u, v);
+ if (outEdges.length > 0) {
+ return util.max(outEdges.map(function(e) {
+ return graph.edge(e).minLen;
+ }));
+ }
+
+ var inEdges = graph.inEdges(u, v);
+ if (inEdges.length > 0) {
+ return -util.max(inEdges.map(function(e) {
+ return graph.edge(e).minLen;
+ }));
+ }
+}
+
+},{"../util":26,"./rankUtil":24}],26:[function(require,module,exports){
+/*
+ * Returns the smallest value in the array.
+ */
+exports.min = function(values) {
+ return Math.min.apply(Math, values);
+};
+
+/*
+ * Returns the largest value in the array.
+ */
+exports.max = function(values) {
+ return Math.max.apply(Math, values);
+};
+
+/*
+ * Returns `true` only if `f(x)` is `true` for all `x` in `xs`. Otherwise
+ * returns `false`. This function will return immediately if it finds a
+ * case where `f(x)` does not hold.
+ */
+exports.all = function(xs, f) {
+ for (var i = 0; i < xs.length; ++i) {
+ if (!f(xs[i])) {
+ return false;
+ }
+ }
+ return true;
+};
+
+/*
+ * Accumulates the sum of elements in the given array using the `+` operator.
+ */
+exports.sum = function(values) {
+ return values.reduce(function(acc, x) { return acc + x; }, 0);
+};
+
+/*
+ * Returns an array of all values in the given object.
+ */
+exports.values = function(obj) {
+ return Object.keys(obj).map(function(k) { return obj[k]; });
+};
+
+exports.shuffle = function(array) {
+ for (i = array.length - 1; i > 0; --i) {
+ var j = Math.floor(Math.random() * (i + 1));
+ var aj = array[j];
+ array[j] = array[i];
+ array[i] = aj;
+ }
+};
+
+exports.propertyAccessor = function(self, config, field, setHook) {
+ return function(x) {
+ if (!arguments.length) return config[field];
+ config[field] = x;
+ if (setHook) setHook(x);
+ return self;
+ };
+};
+
+/*
+ * Given a layered, directed graph with `rank` and `order` node attributes,
+ * this function returns an array of ordered ranks. Each rank contains an array
+ * of the ids of the nodes in that rank in the order specified by the `order`
+ * attribute.
+ */
+exports.ordering = function(g) {
+ var ordering = [];
+ g.eachNode(function(u, value) {
+ var rank = ordering[value.rank] || (ordering[value.rank] = []);
+ rank[value.order] = u;
+ });
+ return ordering;
+};
+
+/*
+ * A filter that can be used with `filterNodes` to get a graph that only
+ * includes nodes that do not contain others nodes.
+ */
+exports.filterNonSubgraphs = function(g) {
+ return function(u) {
+ return g.children(u).length === 0;
+ };
+};
+
+/*
+ * Returns a new function that wraps `func` with a timer. The wrapper logs the
+ * time it takes to execute the function.
+ *
+ * The timer will be enabled provided `log.level >= 1`.
+ */
+function time(name, func) {
+ return function() {
+ var start = new Date().getTime();
+ try {
+ return func.apply(null, arguments);
+ } finally {
+ log(1, name + ' time: ' + (new Date().getTime() - start) + 'ms');
+ }
+ };
+}
+time.enabled = false;
+
+exports.time = time;
+
+/*
+ * A global logger with the specification `log(level, message, ...)` that
+ * will log a message to the console if `log.level >= level`.
+ */
+function log(level) {
+ if (log.level >= level) {
+ console.log.apply(console, Array.prototype.slice.call(arguments, 1));
+ }
+}
+log.level = 0;
+
+exports.log = log;
+
+},{}],27:[function(require,module,exports){
+module.exports = '0.4.5';
+
+},{}],28:[function(require,module,exports){
+exports.Graph = require("./lib/Graph");
+exports.Digraph = require("./lib/Digraph");
+exports.CGraph = require("./lib/CGraph");
+exports.CDigraph = require("./lib/CDigraph");
+require("./lib/graph-converters");
+
+exports.alg = {
+ isAcyclic: require("./lib/alg/isAcyclic"),
+ components: require("./lib/alg/components"),
+ dijkstra: require("./lib/alg/dijkstra"),
+ dijkstraAll: require("./lib/alg/dijkstraAll"),
+ findCycles: require("./lib/alg/findCycles"),
+ floydWarshall: require("./lib/alg/floydWarshall"),
+ postorder: require("./lib/alg/postorder"),
+ preorder: require("./lib/alg/preorder"),
+ prim: require("./lib/alg/prim"),
+ tarjan: require("./lib/alg/tarjan"),
+ topsort: require("./lib/alg/topsort")
+};
+
+exports.converter = {
+ json: require("./lib/converter/json.js")
+};
+
+var filter = require("./lib/filter");
+exports.filter = {
+ all: filter.all,
+ nodesFromList: filter.nodesFromList
+};
+
+exports.version = require("./lib/version");
+
+},{"./lib/CDigraph":30,"./lib/CGraph":31,"./lib/Digraph":32,"./lib/Graph":33,"./lib/alg/components":34,"./lib/alg/dijkstra":35,"./lib/alg/dijkstraAll":36,"./lib/alg/findCycles":37,"./lib/alg/floydWarshall":38,"./lib/alg/isAcyclic":39,"./lib/alg/postorder":40,"./lib/alg/preorder":41,"./lib/alg/prim":42,"./lib/alg/tarjan":43,"./lib/alg/topsort":44,"./lib/converter/json.js":46,"./lib/filter":47,"./lib/graph-converters":48,"./lib/version":50}],29:[function(require,module,exports){
+/* jshint -W079 */
+var Set = require("cp-data").Set;
+/* jshint +W079 */
+
+module.exports = BaseGraph;
+
+function BaseGraph() {
+ // The value assigned to the graph itself.
+ this._value = undefined;
+
+ // Map of node id -> { id, value }
+ this._nodes = {};
+
+ // Map of edge id -> { id, u, v, value }
+ this._edges = {};
+
+ // Used to generate a unique id in the graph
+ this._nextId = 0;
+}
+
+// Number of nodes
+BaseGraph.prototype.order = function() {
+ return Object.keys(this._nodes).length;
+};
+
+// Number of edges
+BaseGraph.prototype.size = function() {
+ return Object.keys(this._edges).length;
+};
+
+// Accessor for graph level value
+BaseGraph.prototype.graph = function(value) {
+ if (arguments.length === 0) {
+ return this._value;
+ }
+ this._value = value;
+};
+
+BaseGraph.prototype.hasNode = function(u) {
+ return u in this._nodes;
+};
+
+BaseGraph.prototype.node = function(u, value) {
+ var node = this._strictGetNode(u);
+ if (arguments.length === 1) {
+ return node.value;
+ }
+ node.value = value;
+};
+
+BaseGraph.prototype.nodes = function() {
+ var nodes = [];
+ this.eachNode(function(id) { nodes.push(id); });
+ return nodes;
+};
+
+BaseGraph.prototype.eachNode = function(func) {
+ for (var k in this._nodes) {
+ var node = this._nodes[k];
+ func(node.id, node.value);
+ }
+};
+
+BaseGraph.prototype.hasEdge = function(e) {
+ return e in this._edges;
+};
+
+BaseGraph.prototype.edge = function(e, value) {
+ var edge = this._strictGetEdge(e);
+ if (arguments.length === 1) {
+ return edge.value;
+ }
+ edge.value = value;
+};
+
+BaseGraph.prototype.edges = function() {
+ var es = [];
+ this.eachEdge(function(id) { es.push(id); });
+ return es;
+};
+
+BaseGraph.prototype.eachEdge = function(func) {
+ for (var k in this._edges) {
+ var edge = this._edges[k];
+ func(edge.id, edge.u, edge.v, edge.value);
+ }
+};
+
+BaseGraph.prototype.incidentNodes = function(e) {
+ var edge = this._strictGetEdge(e);
+ return [edge.u, edge.v];
+};
+
+BaseGraph.prototype.addNode = function(u, value) {
+ if (u === undefined || u === null) {
+ do {
+ u = "_" + (++this._nextId);
+ } while (this.hasNode(u));
+ } else if (this.hasNode(u)) {
+ throw new Error("Graph already has node '" + u + "'");
+ }
+ this._nodes[u] = { id: u, value: value };
+ return u;
+};
+
+BaseGraph.prototype.delNode = function(u) {
+ this._strictGetNode(u);
+ this.incidentEdges(u).forEach(function(e) { this.delEdge(e); }, this);
+ delete this._nodes[u];
+};
+
+// inMap and outMap are opposite sides of an incidence map. For example, for
+// Graph these would both come from the _incidentEdges map, while for Digraph
+// they would come from _inEdges and _outEdges.
+BaseGraph.prototype._addEdge = function(e, u, v, value, inMap, outMap) {
+ this._strictGetNode(u);
+ this._strictGetNode(v);
+
+ if (e === undefined || e === null) {
+ do {
+ e = "_" + (++this._nextId);
+ } while (this.hasEdge(e));
+ }
+ else if (this.hasEdge(e)) {
+ throw new Error("Graph already has edge '" + e + "'");
+ }
+
+ this._edges[e] = { id: e, u: u, v: v, value: value };
+ addEdgeToMap(inMap[v], u, e);
+ addEdgeToMap(outMap[u], v, e);
+
+ return e;
+};
+
+// See note for _addEdge regarding inMap and outMap.
+BaseGraph.prototype._delEdge = function(e, inMap, outMap) {
+ var edge = this._strictGetEdge(e);
+ delEdgeFromMap(inMap[edge.v], edge.u, e);
+ delEdgeFromMap(outMap[edge.u], edge.v, e);
+ delete this._edges[e];
+};
+
+BaseGraph.prototype.copy = function() {
+ var copy = new this.constructor();
+ copy.graph(this.graph());
+ this.eachNode(function(u, value) { copy.addNode(u, value); });
+ this.eachEdge(function(e, u, v, value) { copy.addEdge(e, u, v, value); });
+ copy._nextId = this._nextId;
+ return copy;
+};
+
+BaseGraph.prototype.filterNodes = function(filter) {
+ var copy = new this.constructor();
+ copy.graph(this.graph());
+ this.eachNode(function(u, value) {
+ if (filter(u)) {
+ copy.addNode(u, value);
+ }
+ });
+ this.eachEdge(function(e, u, v, value) {
+ if (copy.hasNode(u) && copy.hasNode(v)) {
+ copy.addEdge(e, u, v, value);
+ }
+ });
+ return copy;
+};
+
+BaseGraph.prototype._strictGetNode = function(u) {
+ var node = this._nodes[u];
+ if (node === undefined) {
+ throw new Error("Node '" + u + "' is not in graph");
+ }
+ return node;
+};
+
+BaseGraph.prototype._strictGetEdge = function(e) {
+ var edge = this._edges[e];
+ if (edge === undefined) {
+ throw new Error("Edge '" + e + "' is not in graph");
+ }
+ return edge;
+};
+
+function addEdgeToMap(map, v, e) {
+ (map[v] || (map[v] = new Set())).add(e);
+}
+
+function delEdgeFromMap(map, v, e) {
+ var vEntry = map[v];
+ vEntry.remove(e);
+ if (vEntry.size() === 0) {
+ delete map[v];
+ }
+}
+
+
+},{"cp-data":5}],30:[function(require,module,exports){
+var Digraph = require("./Digraph"),
+ compoundify = require("./compoundify");
+
+var CDigraph = compoundify(Digraph);
+
+module.exports = CDigraph;
+
+CDigraph.fromDigraph = function(src) {
+ var g = new CDigraph(),
+ graphValue = src.graph();
+
+ if (graphValue !== undefined) {
+ g.graph(graphValue);
+ }
+
+ src.eachNode(function(u, value) {
+ if (value === undefined) {
+ g.addNode(u);
+ } else {
+ g.addNode(u, value);
+ }
+ });
+ src.eachEdge(function(e, u, v, value) {
+ if (value === undefined) {
+ g.addEdge(null, u, v);
+ } else {
+ g.addEdge(null, u, v, value);
+ }
+ });
+ return g;
+};
+
+CDigraph.prototype.toString = function() {
+ return "CDigraph " + JSON.stringify(this, null, 2);
+};
+
+},{"./Digraph":32,"./compoundify":45}],31:[function(require,module,exports){
+var Graph = require("./Graph"),
+ compoundify = require("./compoundify");
+
+var CGraph = compoundify(Graph);
+
+module.exports = CGraph;
+
+CGraph.fromGraph = function(src) {
+ var g = new CGraph(),
+ graphValue = src.graph();
+
+ if (graphValue !== undefined) {
+ g.graph(graphValue);
+ }
+
+ src.eachNode(function(u, value) {
+ if (value === undefined) {
+ g.addNode(u);
+ } else {
+ g.addNode(u, value);
+ }
+ });
+ src.eachEdge(function(e, u, v, value) {
+ if (value === undefined) {
+ g.addEdge(null, u, v);
+ } else {
+ g.addEdge(null, u, v, value);
+ }
+ });
+ return g;
+};
+
+CGraph.prototype.toString = function() {
+ return "CGraph " + JSON.stringify(this, null, 2);
+};
+
+},{"./Graph":33,"./compoundify":45}],32:[function(require,module,exports){
+/*
+ * This file is organized with in the following order:
+ *
+ * Exports
+ * Graph constructors
+ * Graph queries (e.g. nodes(), edges()
+ * Graph mutators
+ * Helper functions
+ */
+
+var util = require("./util"),
+ BaseGraph = require("./BaseGraph"),
+/* jshint -W079 */
+ Set = require("cp-data").Set;
+/* jshint +W079 */
+
+module.exports = Digraph;
+
+/*
+ * Constructor to create a new directed multi-graph.
+ */
+function Digraph() {
+ BaseGraph.call(this);
+
+ /*! Map of sourceId -> {targetId -> Set of edge ids} */
+ this._inEdges = {};
+
+ /*! Map of targetId -> {sourceId -> Set of edge ids} */
+ this._outEdges = {};
+}
+
+Digraph.prototype = new BaseGraph();
+Digraph.prototype.constructor = Digraph;
+
+/*
+ * Always returns `true`.
+ */
+Digraph.prototype.isDirected = function() {
+ return true;
+};
+
+/*
+ * Returns all successors of the node with the id `u`. That is, all nodes
+ * that have the node `u` as their source are returned.
+ *
+ * If no node `u` exists in the graph this function throws an Error.
+ *
+ * @param {String} u a node id
+ */
+Digraph.prototype.successors = function(u) {
+ this._strictGetNode(u);
+ return Object.keys(this._outEdges[u])
+ .map(function(v) { return this._nodes[v].id; }, this);
+};
+
+/*
+ * Returns all predecessors of the node with the id `u`. That is, all nodes
+ * that have the node `u` as their target are returned.
+ *
+ * If no node `u` exists in the graph this function throws an Error.
+ *
+ * @param {String} u a node id
+ */
+Digraph.prototype.predecessors = function(u) {
+ this._strictGetNode(u);
+ return Object.keys(this._inEdges[u])
+ .map(function(v) { return this._nodes[v].id; }, this);
+};
+
+/*
+ * Returns all nodes that are adjacent to the node with the id `u`. In other
+ * words, this function returns the set of all successors and predecessors of
+ * node `u`.
+ *
+ * @param {String} u a node id
+ */
+Digraph.prototype.neighbors = function(u) {
+ return Set.union([this.successors(u), this.predecessors(u)]).keys();
+};
+
+/*
+ * Returns all nodes in the graph that have no in-edges.
+ */
+Digraph.prototype.sources = function() {
+ var self = this;
+ return this._filterNodes(function(u) {
+ // This could have better space characteristics if we had an inDegree function.
+ return self.inEdges(u).length === 0;
+ });
+};
+
+/*
+ * Returns all nodes in the graph that have no out-edges.
+ */
+Digraph.prototype.sinks = function() {
+ var self = this;
+ return this._filterNodes(function(u) {
+ // This could have better space characteristics if we have an outDegree function.
+ return self.outEdges(u).length === 0;
+ });
+};
+
+/*
+ * Returns the source node incident on the edge identified by the id `e`. If no
+ * such edge exists in the graph this function throws an Error.
+ *
+ * @param {String} e an edge id
+ */
+Digraph.prototype.source = function(e) {
+ return this._strictGetEdge(e).u;
+};
+
+/*
+ * Returns the target node incident on the edge identified by the id `e`. If no
+ * such edge exists in the graph this function throws an Error.
+ *
+ * @param {String} e an edge id
+ */
+Digraph.prototype.target = function(e) {
+ return this._strictGetEdge(e).v;
+};
+
+/*
+ * Returns an array of ids for all edges in the graph that have the node
+ * `target` as their target. If the node `target` is not in the graph this
+ * function raises an Error.
+ *
+ * Optionally a `source` node can also be specified. This causes the results
+ * to be filtered such that only edges from `source` to `target` are included.
+ * If the node `source` is specified but is not in the graph then this function
+ * raises an Error.
+ *
+ * @param {String} target the target node id
+ * @param {String} [source] an optional source node id
+ */
+Digraph.prototype.inEdges = function(target, source) {
+ this._strictGetNode(target);
+ var results = Set.union(util.values(this._inEdges[target])).keys();
+ if (arguments.length > 1) {
+ this._strictGetNode(source);
+ results = results.filter(function(e) { return this.source(e) === source; }, this);
+ }
+ return results;
+};
+
+/*
+ * Returns an array of ids for all edges in the graph that have the node
+ * `source` as their source. If the node `source` is not in the graph this
+ * function raises an Error.
+ *
+ * Optionally a `target` node may also be specified. This causes the results
+ * to be filtered such that only edges from `source` to `target` are included.
+ * If the node `target` is specified but is not in the graph then this function
+ * raises an Error.
+ *
+ * @param {String} source the source node id
+ * @param {String} [target] an optional target node id
+ */
+Digraph.prototype.outEdges = function(source, target) {
+ this._strictGetNode(source);
+ var results = Set.union(util.values(this._outEdges[source])).keys();
+ if (arguments.length > 1) {
+ this._strictGetNode(target);
+ results = results.filter(function(e) { return this.target(e) === target; }, this);
+ }
+ return results;
+};
+
+/*
+ * Returns an array of ids for all edges in the graph that have the `u` as
+ * their source or their target. If the node `u` is not in the graph this
+ * function raises an Error.
+ *
+ * Optionally a `v` node may also be specified. This causes the results to be
+ * filtered such that only edges between `u` and `v` - in either direction -
+ * are included. IF the node `v` is specified but not in the graph then this
+ * function raises an Error.
+ *
+ * @param {String} u the node for which to find incident edges
+ * @param {String} [v] option node that must be adjacent to `u`
+ */
+Digraph.prototype.incidentEdges = function(u, v) {
+ if (arguments.length > 1) {
+ return Set.union([this.outEdges(u, v), this.outEdges(v, u)]).keys();
+ } else {
+ return Set.union([this.inEdges(u), this.outEdges(u)]).keys();
+ }
+};
+
+/*
+ * Returns a string representation of this graph.
+ */
+Digraph.prototype.toString = function() {
+ return "Digraph " + JSON.stringify(this, null, 2);
+};
+
+/*
+ * Adds a new node with the id `u` to the graph and assigns it the value
+ * `value`. If a node with the id is already a part of the graph this function
+ * throws an Error.
+ *
+ * @param {String} u a node id
+ * @param {Object} [value] an optional value to attach to the node
+ */
+Digraph.prototype.addNode = function(u, value) {
+ u = BaseGraph.prototype.addNode.call(this, u, value);
+ this._inEdges[u] = {};
+ this._outEdges[u] = {};
+ return u;
+};
+
+/*
+ * Removes a node from the graph that has the id `u`. Any edges incident on the
+ * node are also removed. If the graph does not contain a node with the id this
+ * function will throw an Error.
+ *
+ * @param {String} u a node id
+ */
+Digraph.prototype.delNode = function(u) {
+ BaseGraph.prototype.delNode.call(this, u);
+ delete this._inEdges[u];
+ delete this._outEdges[u];
+};
+
+/*
+ * Adds a new edge to the graph with the id `e` from a node with the id `source`
+ * to a node with an id `target` and assigns it the value `value`. This graph
+ * allows more than one edge from `source` to `target` as long as the id `e`
+ * is unique in the set of edges. If `e` is `null` the graph will assign a
+ * unique identifier to the edge.
+ *
+ * If `source` or `target` are not present in the graph this function will
+ * throw an Error.
+ *
+ * @param {String} [e] an edge id
+ * @param {String} source the source node id
+ * @param {String} target the target node id
+ * @param {Object} [value] an optional value to attach to the edge
+ */
+Digraph.prototype.addEdge = function(e, source, target, value) {
+ return BaseGraph.prototype._addEdge.call(this, e, source, target, value,
+ this._inEdges, this._outEdges);
+};
+
+/*
+ * Removes an edge in the graph with the id `e`. If no edge in the graph has
+ * the id `e` this function will throw an Error.
+ *
+ * @param {String} e an edge id
+ */
+Digraph.prototype.delEdge = function(e) {
+ BaseGraph.prototype._delEdge.call(this, e, this._inEdges, this._outEdges);
+};
+
+// Unlike BaseGraph.filterNodes, this helper just returns nodes that
+// satisfy a predicate.
+Digraph.prototype._filterNodes = function(pred) {
+ var filtered = [];
+ this.eachNode(function(u) {
+ if (pred(u)) {
+ filtered.push(u);
+ }
+ });
+ return filtered;
+};
+
+
+},{"./BaseGraph":29,"./util":49,"cp-data":5}],33:[function(require,module,exports){
+/*
+ * This file is organized with in the following order:
+ *
+ * Exports
+ * Graph constructors
+ * Graph queries (e.g. nodes(), edges()
+ * Graph mutators
+ * Helper functions
+ */
+
+var util = require("./util"),
+ BaseGraph = require("./BaseGraph"),
+/* jshint -W079 */
+ Set = require("cp-data").Set;
+/* jshint +W079 */
+
+module.exports = Graph;
+
+/*
+ * Constructor to create a new undirected multi-graph.
+ */
+function Graph() {
+ BaseGraph.call(this);
+
+ /*! Map of nodeId -> { otherNodeId -> Set of edge ids } */
+ this._incidentEdges = {};
+}
+
+Graph.prototype = new BaseGraph();
+Graph.prototype.constructor = Graph;
+
+/*
+ * Always returns `false`.
+ */
+Graph.prototype.isDirected = function() {
+ return false;
+};
+
+/*
+ * Returns all nodes that are adjacent to the node with the id `u`.
+ *
+ * @param {String} u a node id
+ */
+Graph.prototype.neighbors = function(u) {
+ this._strictGetNode(u);
+ return Object.keys(this._incidentEdges[u])
+ .map(function(v) { return this._nodes[v].id; }, this);
+};
+
+/*
+ * Returns an array of ids for all edges in the graph that are incident on `u`.
+ * If the node `u` is not in the graph this function raises an Error.
+ *
+ * Optionally a `v` node may also be specified. This causes the results to be
+ * filtered such that only edges between `u` and `v` are included. If the node
+ * `v` is specified but not in the graph then this function raises an Error.
+ *
+ * @param {String} u the node for which to find incident edges
+ * @param {String} [v] option node that must be adjacent to `u`
+ */
+Graph.prototype.incidentEdges = function(u, v) {
+ this._strictGetNode(u);
+ if (arguments.length > 1) {
+ this._strictGetNode(v);
+ return v in this._incidentEdges[u] ? this._incidentEdges[u][v].keys() : [];
+ } else {
+ return Set.union(util.values(this._incidentEdges[u])).keys();
+ }
+};
+
+/*
+ * Returns a string representation of this graph.
+ */
+Graph.prototype.toString = function() {
+ return "Graph " + JSON.stringify(this, null, 2);
+};
+
+/*
+ * Adds a new node with the id `u` to the graph and assigns it the value
+ * `value`. If a node with the id is already a part of the graph this function
+ * throws an Error.
+ *
+ * @param {String} u a node id
+ * @param {Object} [value] an optional value to attach to the node
+ */
+Graph.prototype.addNode = function(u, value) {
+ u = BaseGraph.prototype.addNode.call(this, u, value);
+ this._incidentEdges[u] = {};
+ return u;
+};
+
+/*
+ * Removes a node from the graph that has the id `u`. Any edges incident on the
+ * node are also removed. If the graph does not contain a node with the id this
+ * function will throw an Error.
+ *
+ * @param {String} u a node id
+ */
+Graph.prototype.delNode = function(u) {
+ BaseGraph.prototype.delNode.call(this, u);
+ delete this._incidentEdges[u];
+};
+
+/*
+ * Adds a new edge to the graph with the id `e` between a node with the id `u`
+ * and a node with an id `v` and assigns it the value `value`. This graph
+ * allows more than one edge between `u` and `v` as long as the id `e`
+ * is unique in the set of edges. If `e` is `null` the graph will assign a
+ * unique identifier to the edge.
+ *
+ * If `u` or `v` are not present in the graph this function will throw an
+ * Error.
+ *
+ * @param {String} [e] an edge id
+ * @param {String} u the node id of one of the adjacent nodes
+ * @param {String} v the node id of the other adjacent node
+ * @param {Object} [value] an optional value to attach to the edge
+ */
+Graph.prototype.addEdge = function(e, u, v, value) {
+ return BaseGraph.prototype._addEdge.call(this, e, u, v, value,
+ this._incidentEdges, this._incidentEdges);
+};
+
+/*
+ * Removes an edge in the graph with the id `e`. If no edge in the graph has
+ * the id `e` this function will throw an Error.
+ *
+ * @param {String} e an edge id
+ */
+Graph.prototype.delEdge = function(e) {
+ BaseGraph.prototype._delEdge.call(this, e, this._incidentEdges, this._incidentEdges);
+};
+
+
+},{"./BaseGraph":29,"./util":49,"cp-data":5}],34:[function(require,module,exports){
+/* jshint -W079 */
+var Set = require("cp-data").Set;
+/* jshint +W079 */
+
+module.exports = components;
+
+/**
+ * Finds all [connected components][] in a graph and returns an array of these
+ * components. Each component is itself an array that contains the ids of nodes
+ * in the component.
+ *
+ * This function only works with undirected Graphs.
+ *
+ * [connected components]: http://en.wikipedia.org/wiki/Connected_component_(graph_theory)
+ *
+ * @param {Graph} g the graph to search for components
+ */
+function components(g) {
+ var results = [];
+ var visited = new Set();
+
+ function dfs(v, component) {
+ if (!visited.has(v)) {
+ visited.add(v);
+ component.push(v);
+ g.neighbors(v).forEach(function(w) {
+ dfs(w, component);
+ });
+ }
+ }
+
+ g.nodes().forEach(function(v) {
+ var component = [];
+ dfs(v, component);
+ if (component.length > 0) {
+ results.push(component);
+ }
+ });
+
+ return results;
+}
+
+},{"cp-data":5}],35:[function(require,module,exports){
+var PriorityQueue = require("cp-data").PriorityQueue;
+
+module.exports = dijkstra;
+
+/**
+ * This function is an implementation of [Dijkstra's algorithm][] which finds
+ * the shortest path from **source** to all other nodes in **g**. This
+ * function returns a map of `u -> { distance, predecessor }`. The distance
+ * property holds the sum of the weights from **source** to `u` along the
+ * shortest path or `Number.POSITIVE_INFINITY` if there is no path from
+ * **source**. The predecessor property can be used to walk the individual
+ * elements of the path from **source** to **u** in reverse order.
+ *
+ * This function takes an optional `weightFunc(e)` which returns the
+ * weight of the edge `e`. If no weightFunc is supplied then each edge is
+ * assumed to have a weight of 1. This function throws an Error if any of
+ * the traversed edges have a negative edge weight.
+ *
+ * This function takes an optional `incidentFunc(u)` which returns the ids of
+ * all edges incident to the node `u` for the purposes of shortest path
+ * traversal. By default this function uses the `g.outEdges` for Digraphs and
+ * `g.incidentEdges` for Graphs.
+ *
+ * This function takes `O((|E| + |V|) * log |V|)` time.
+ *
+ * [Dijkstra's algorithm]: http://en.wikipedia.org/wiki/Dijkstra%27s_algorithm
+ *
+ * @param {Graph} g the graph to search for shortest paths from **source**
+ * @param {Object} source the source from which to start the search
+ * @param {Function} [weightFunc] optional weight function
+ * @param {Function} [incidentFunc] optional incident function
+ */
+function dijkstra(g, source, weightFunc, incidentFunc) {
+ var results = {},
+ pq = new PriorityQueue();
+
+ function updateNeighbors(e) {
+ var incidentNodes = g.incidentNodes(e),
+ v = incidentNodes[0] !== u ? incidentNodes[0] : incidentNodes[1],
+ vEntry = results[v],
+ weight = weightFunc(e),
+ distance = uEntry.distance + weight;
+
+ if (weight < 0) {
+ throw new Error("dijkstra does not allow negative edge weights. Bad edge: " + e + " Weight: " + weight);
+ }
+
+ if (distance < vEntry.distance) {
+ vEntry.distance = distance;
+ vEntry.predecessor = u;
+ pq.decrease(v, distance);
+ }
+ }
+
+ weightFunc = weightFunc || function() { return 1; };
+ incidentFunc = incidentFunc || (g.isDirected()
+ ? function(u) { return g.outEdges(u); }
+ : function(u) { return g.incidentEdges(u); });
+
+ g.eachNode(function(u) {
+ var distance = u === source ? 0 : Number.POSITIVE_INFINITY;
+ results[u] = { distance: distance };
+ pq.add(u, distance);
+ });
+
+ var u, uEntry;
+ while (pq.size() > 0) {
+ u = pq.removeMin();
+ uEntry = results[u];
+ if (uEntry.distance === Number.POSITIVE_INFINITY) {
+ break;
+ }
+
+ incidentFunc(u).forEach(updateNeighbors);
+ }
+
+ return results;
+}
+
+},{"cp-data":5}],36:[function(require,module,exports){
+var dijkstra = require("./dijkstra");
+
+module.exports = dijkstraAll;
+
+/**
+ * This function finds the shortest path from each node to every other
+ * reachable node in the graph. It is similar to [alg.dijkstra][], but
+ * instead of returning a single-source array, it returns a mapping of
+ * of `source -> alg.dijksta(g, source, weightFunc, incidentFunc)`.
+ *
+ * This function takes an optional `weightFunc(e)` which returns the
+ * weight of the edge `e`. If no weightFunc is supplied then each edge is
+ * assumed to have a weight of 1. This function throws an Error if any of
+ * the traversed edges have a negative edge weight.
+ *
+ * This function takes an optional `incidentFunc(u)` which returns the ids of
+ * all edges incident to the node `u` for the purposes of shortest path
+ * traversal. By default this function uses the `outEdges` function on the
+ * supplied graph.
+ *
+ * This function takes `O(|V| * (|E| + |V|) * log |V|)` time.
+ *
+ * [alg.dijkstra]: dijkstra.js.html#dijkstra
+ *
+ * @param {Graph} g the graph to search for shortest paths from **source**
+ * @param {Function} [weightFunc] optional weight function
+ * @param {Function} [incidentFunc] optional incident function
+ */
+function dijkstraAll(g, weightFunc, incidentFunc) {
+ var results = {};
+ g.eachNode(function(u) {
+ results[u] = dijkstra(g, u, weightFunc, incidentFunc);
+ });
+ return results;
+}
+
+},{"./dijkstra":35}],37:[function(require,module,exports){
+var tarjan = require("./tarjan");
+
+module.exports = findCycles;
+
+/*
+ * Given a Digraph **g** this function returns all nodes that are part of a
+ * cycle. Since there may be more than one cycle in a graph this function
+ * returns an array of these cycles, where each cycle is itself represented
+ * by an array of ids for each node involved in that cycle.
+ *
+ * [alg.isAcyclic][] is more efficient if you only need to determine whether
+ * a graph has a cycle or not.
+ *
+ * [alg.isAcyclic]: isAcyclic.js.html#isAcyclic
+ *
+ * @param {Digraph} g the graph to search for cycles.
+ */
+function findCycles(g) {
+ return tarjan(g).filter(function(cmpt) { return cmpt.length > 1; });
+}
+
+},{"./tarjan":43}],38:[function(require,module,exports){
+module.exports = floydWarshall;
+
+/**
+ * This function is an implementation of the [Floyd-Warshall algorithm][],
+ * which finds the shortest path from each node to every other reachable node
+ * in the graph. It is similar to [alg.dijkstraAll][], but it handles negative
+ * edge weights and is more efficient for some types of graphs. This function
+ * returns a map of `source -> { target -> { distance, predecessor }`. The
+ * distance property holds the sum of the weights from `source` to `target`
+ * along the shortest path of `Number.POSITIVE_INFINITY` if there is no path
+ * from `source`. The predecessor property can be used to walk the individual
+ * elements of the path from `source` to `target` in reverse order.
+ *
+ * This function takes an optional `weightFunc(e)` which returns the
+ * weight of the edge `e`. If no weightFunc is supplied then each edge is
+ * assumed to have a weight of 1.
+ *
+ * This function takes an optional `incidentFunc(u)` which returns the ids of
+ * all edges incident to the node `u` for the purposes of shortest path
+ * traversal. By default this function uses the `outEdges` function on the
+ * supplied graph.
+ *
+ * This algorithm takes O(|V|^3) time.
+ *
+ * [Floyd-Warshall algorithm]: https://en.wikipedia.org/wiki/Floyd-Warshall_algorithm
+ * [alg.dijkstraAll]: dijkstraAll.js.html#dijkstraAll
+ *
+ * @param {Graph} g the graph to search for shortest paths from **source**
+ * @param {Function} [weightFunc] optional weight function
+ * @param {Function} [incidentFunc] optional incident function
+ */
+function floydWarshall(g, weightFunc, incidentFunc) {
+ var results = {},
+ nodes = g.nodes();
+
+ weightFunc = weightFunc || function() { return 1; };
+ incidentFunc = incidentFunc || (g.isDirected()
+ ? function(u) { return g.outEdges(u); }
+ : function(u) { return g.incidentEdges(u); });
+
+ nodes.forEach(function(u) {
+ results[u] = {};
+ results[u][u] = { distance: 0 };
+ nodes.forEach(function(v) {
+ if (u !== v) {
+ results[u][v] = { distance: Number.POSITIVE_INFINITY };
+ }
+ });
+ incidentFunc(u).forEach(function(e) {
+ var incidentNodes = g.incidentNodes(e),
+ v = incidentNodes[0] !== u ? incidentNodes[0] : incidentNodes[1],
+ d = weightFunc(e);
+ if (d < results[u][v].distance) {
+ results[u][v] = { distance: d, predecessor: u };
+ }
+ });
+ });
+
+ nodes.forEach(function(k) {
+ var rowK = results[k];
+ nodes.forEach(function(i) {
+ var rowI = results[i];
+ nodes.forEach(function(j) {
+ var ik = rowI[k];
+ var kj = rowK[j];
+ var ij = rowI[j];
+ var altDistance = ik.distance + kj.distance;
+ if (altDistance < ij.distance) {
+ ij.distance = altDistance;
+ ij.predecessor = kj.predecessor;
+ }
+ });
+ });
+ });
+
+ return results;
+}
+
+},{}],39:[function(require,module,exports){
+var topsort = require("./topsort");
+
+module.exports = isAcyclic;
+
+/*
+ * Given a Digraph **g** this function returns `true` if the graph has no
+ * cycles and returns `false` if it does. This algorithm returns as soon as it
+ * detects the first cycle.
+ *
+ * Use [alg.findCycles][] if you need the actual list of cycles in a graph.
+ *
+ * [alg.findCycles]: findCycles.js.html#findCycles
+ *
+ * @param {Digraph} g the graph to test for cycles
+ */
+function isAcyclic(g) {
+ try {
+ topsort(g);
+ } catch (e) {
+ if (e instanceof topsort.CycleException) return false;
+ throw e;
+ }
+ return true;
+}
+
+},{"./topsort":44}],40:[function(require,module,exports){
+/* jshint -W079 */
+var Set = require("cp-data").Set;
+/* jshint +W079 */
+
+module.exports = postorder;
+
+// Postorder traversal of g, calling f for each visited node. Assumes the graph
+// is a tree.
+function postorder(g, root, f) {
+ var visited = new Set();
+ if (g.isDirected()) {
+ throw new Error("This function only works for undirected graphs");
+ }
+ function dfs(u, prev) {
+ if (visited.has(u)) {
+ throw new Error("The input graph is not a tree: " + g);
+ }
+ visited.add(u);
+ g.neighbors(u).forEach(function(v) {
+ if (v !== prev) dfs(v, u);
+ });
+ f(u);
+ }
+ dfs(root);
+}
+
+},{"cp-data":5}],41:[function(require,module,exports){
+/* jshint -W079 */
+var Set = require("cp-data").Set;
+/* jshint +W079 */
+
+module.exports = preorder;
+
+// Preorder traversal of g, calling f for each visited node. Assumes the graph
+// is a tree.
+function preorder(g, root, f) {
+ var visited = new Set();
+ if (g.isDirected()) {
+ throw new Error("This function only works for undirected graphs");
+ }
+ function dfs(u, prev) {
+ if (visited.has(u)) {
+ throw new Error("The input graph is not a tree: " + g);
+ }
+ visited.add(u);
+ f(u);
+ g.neighbors(u).forEach(function(v) {
+ if (v !== prev) dfs(v, u);
+ });
+ }
+ dfs(root);
+}
+
+},{"cp-data":5}],42:[function(require,module,exports){
+var Graph = require("../Graph"),
+ PriorityQueue = require("cp-data").PriorityQueue;
+
+module.exports = prim;
+
+/**
+ * [Prim's algorithm][] takes a connected undirected graph and generates a
+ * [minimum spanning tree][]. This function returns the minimum spanning
+ * tree as an undirected graph. This algorithm is derived from the description
+ * in "Introduction to Algorithms", Third Edition, Cormen, et al., Pg 634.
+ *
+ * This function takes a `weightFunc(e)` which returns the weight of the edge
+ * `e`. It throws an Error if the graph is not connected.
+ *
+ * This function takes `O(|E| log |V|)` time.
+ *
+ * [Prim's algorithm]: https://en.wikipedia.org/wiki/Prim's_algorithm
+ * [minimum spanning tree]: https://en.wikipedia.org/wiki/Minimum_spanning_tree
+ *
+ * @param {Graph} g the graph used to generate the minimum spanning tree
+ * @param {Function} weightFunc the weight function to use
+ */
+function prim(g, weightFunc) {
+ var result = new Graph(),
+ parents = {},
+ pq = new PriorityQueue(),
+ u;
+
+ function updateNeighbors(e) {
+ var incidentNodes = g.incidentNodes(e),
+ v = incidentNodes[0] !== u ? incidentNodes[0] : incidentNodes[1],
+ pri = pq.priority(v);
+ if (pri !== undefined) {
+ var edgeWeight = weightFunc(e);
+ if (edgeWeight < pri) {
+ parents[v] = u;
+ pq.decrease(v, edgeWeight);
+ }
+ }
+ }
+
+ if (g.order() === 0) {
+ return result;
+ }
+
+ g.eachNode(function(u) {
+ pq.add(u, Number.POSITIVE_INFINITY);
+ result.addNode(u);
+ });
+
+ // Start from an arbitrary node
+ pq.decrease(g.nodes()[0], 0);
+
+ var init = false;
+ while (pq.size() > 0) {
+ u = pq.removeMin();
+ if (u in parents) {
+ result.addEdge(null, u, parents[u]);
+ } else if (init) {
+ throw new Error("Input graph is not connected: " + g);
+ } else {
+ init = true;
+ }
+
+ g.incidentEdges(u).forEach(updateNeighbors);
+ }
+
+ return result;
+}
+
+},{"../Graph":33,"cp-data":5}],43:[function(require,module,exports){
+module.exports = tarjan;
+
+/**
+ * This function is an implementation of [Tarjan's algorithm][] which finds
+ * all [strongly connected components][] in the directed graph **g**. Each
+ * strongly connected component is composed of nodes that can reach all other
+ * nodes in the component via directed edges. A strongly connected component
+ * can consist of a single node if that node cannot both reach and be reached
+ * by any other specific node in the graph. Components of more than one node
+ * are guaranteed to have at least one cycle.
+ *
+ * This function returns an array of components. Each component is itself an
+ * array that contains the ids of all nodes in the component.
+ *
+ * [Tarjan's algorithm]: http://en.wikipedia.org/wiki/Tarjan's_strongly_connected_components_algorithm
+ * [strongly connected components]: http://en.wikipedia.org/wiki/Strongly_connected_component
+ *
+ * @param {Digraph} g the graph to search for strongly connected components
+ */
+function tarjan(g) {
+ if (!g.isDirected()) {
+ throw new Error("tarjan can only be applied to a directed graph. Bad input: " + g);
+ }
+
+ var index = 0,
+ stack = [],
+ visited = {}, // node id -> { onStack, lowlink, index }
+ results = [];
+
+ function dfs(u) {
+ var entry = visited[u] = {
+ onStack: true,
+ lowlink: index,
+ index: index++
+ };
+ stack.push(u);
+
+ g.successors(u).forEach(function(v) {
+ if (!(v in visited)) {
+ dfs(v);
+ entry.lowlink = Math.min(entry.lowlink, visited[v].lowlink);
+ } else if (visited[v].onStack) {
+ entry.lowlink = Math.min(entry.lowlink, visited[v].index);
+ }
+ });
+
+ if (entry.lowlink === entry.index) {
+ var cmpt = [],
+ v;
+ do {
+ v = stack.pop();
+ visited[v].onStack = false;
+ cmpt.push(v);
+ } while (u !== v);
+ results.push(cmpt);
+ }
+ }
+
+ g.nodes().forEach(function(u) {
+ if (!(u in visited)) {
+ dfs(u);
+ }
+ });
+
+ return results;
+}
+
+},{}],44:[function(require,module,exports){
+module.exports = topsort;
+topsort.CycleException = CycleException;
+
+/*
+ * Given a graph **g**, this function returns an ordered list of nodes such
+ * that for each edge `u -> v`, `u` appears before `v` in the list. If the
+ * graph has a cycle it is impossible to generate such a list and
+ * **CycleException** is thrown.
+ *
+ * See [topological sorting](https://en.wikipedia.org/wiki/Topological_sorting)
+ * for more details about how this algorithm works.
+ *
+ * @param {Digraph} g the graph to sort
+ */
+function topsort(g) {
+ if (!g.isDirected()) {
+ throw new Error("topsort can only be applied to a directed graph. Bad input: " + g);
+ }
+
+ var visited = {};
+ var stack = {};
+ var results = [];
+
+ function visit(node) {
+ if (node in stack) {
+ throw new CycleException();
+ }
+
+ if (!(node in visited)) {
+ stack[node] = true;
+ visited[node] = true;
+ g.predecessors(node).forEach(function(pred) {
+ visit(pred);
+ });
+ delete stack[node];
+ results.push(node);
+ }
+ }
+
+ var sinks = g.sinks();
+ if (g.order() !== 0 && sinks.length === 0) {
+ throw new CycleException();
+ }
+
+ g.sinks().forEach(function(sink) {
+ visit(sink);
+ });
+
+ return results;
+}
+
+function CycleException() {}
+
+CycleException.prototype.toString = function() {
+ return "Graph has at least one cycle";
+};
+
+},{}],45:[function(require,module,exports){
+// This file provides a helper function that mixes-in Dot behavior to an
+// existing graph prototype.
+
+/* jshint -W079 */
+var Set = require("cp-data").Set;
+/* jshint +W079 */
+
+module.exports = compoundify;
+
+// Extends the given SuperConstructor with the ability for nodes to contain
+// other nodes. A special node id `null` is used to indicate the root graph.
+function compoundify(SuperConstructor) {
+ function Constructor() {
+ SuperConstructor.call(this);
+
+ // Map of object id -> parent id (or null for root graph)
+ this._parents = {};
+
+ // Map of id (or null) -> children set
+ this._children = {};
+ this._children[null] = new Set();
+ }
+
+ Constructor.prototype = new SuperConstructor();
+ Constructor.prototype.constructor = Constructor;
+
+ Constructor.prototype.parent = function(u, parent) {
+ this._strictGetNode(u);
+
+ if (arguments.length < 2) {
+ return this._parents[u];
+ }
+
+ if (u === parent) {
+ throw new Error("Cannot make " + u + " a parent of itself");
+ }
+ if (parent !== null) {
+ this._strictGetNode(parent);
+ }
+
+ this._children[this._parents[u]].remove(u);
+ this._parents[u] = parent;
+ this._children[parent].add(u);
+ };
+
+ Constructor.prototype.children = function(u) {
+ if (u !== null) {
+ this._strictGetNode(u);
+ }
+ return this._children[u].keys();
+ };
+
+ Constructor.prototype.addNode = function(u, value) {
+ u = SuperConstructor.prototype.addNode.call(this, u, value);
+ this._parents[u] = null;
+ this._children[u] = new Set();
+ this._children[null].add(u);
+ return u;
+ };
+
+ Constructor.prototype.delNode = function(u) {
+ // Promote all children to the parent of the subgraph
+ var parent = this.parent(u);
+ this._children[u].keys().forEach(function(child) {
+ this.parent(child, parent);
+ }, this);
+
+ this._children[parent].remove(u);
+ delete this._parents[u];
+ delete this._children[u];
+
+ return SuperConstructor.prototype.delNode.call(this, u);
+ };
+
+ Constructor.prototype.copy = function() {
+ var copy = SuperConstructor.prototype.copy.call(this);
+ this.nodes().forEach(function(u) {
+ copy.parent(u, this.parent(u));
+ }, this);
+ return copy;
+ };
+
+ Constructor.prototype.filterNodes = function(filter) {
+ var self = this,
+ copy = SuperConstructor.prototype.filterNodes.call(this, filter);
+
+ var parents = {};
+ function findParent(u) {
+ var parent = self.parent(u);
+ if (parent === null || copy.hasNode(parent)) {
+ parents[u] = parent;
+ return parent;
+ } else if (parent in parents) {
+ return parents[parent];
+ } else {
+ return findParent(parent);
+ }
+ }
+
+ copy.eachNode(function(u) { copy.parent(u, findParent(u)); });
+
+ return copy;
+ };
+
+ return Constructor;
+}
+
+},{"cp-data":5}],46:[function(require,module,exports){
+var Graph = require("../Graph"),
+ Digraph = require("../Digraph"),
+ CGraph = require("../CGraph"),
+ CDigraph = require("../CDigraph");
+
+exports.decode = function(nodes, edges, Ctor) {
+ Ctor = Ctor || Digraph;
+
+ if (typeOf(nodes) !== "Array") {
+ throw new Error("nodes is not an Array");
+ }
+
+ if (typeOf(edges) !== "Array") {
+ throw new Error("edges is not an Array");
+ }
+
+ if (typeof Ctor === "string") {
+ switch(Ctor) {
+ case "graph": Ctor = Graph; break;
+ case "digraph": Ctor = Digraph; break;
+ case "cgraph": Ctor = CGraph; break;
+ case "cdigraph": Ctor = CDigraph; break;
+ default: throw new Error("Unrecognized graph type: " + Ctor);
+ }
+ }
+
+ var graph = new Ctor();
+
+ nodes.forEach(function(u) {
+ graph.addNode(u.id, u.value);
+ });
+
+ // If the graph is compound, set up children...
+ if (graph.parent) {
+ nodes.forEach(function(u) {
+ if (u.children) {
+ u.children.forEach(function(v) {
+ graph.parent(v, u.id);
+ });
+ }
+ });
+ }
+
+ edges.forEach(function(e) {
+ graph.addEdge(e.id, e.u, e.v, e.value);
+ });
+
+ return graph;
+};
+
+exports.encode = function(graph) {
+ var nodes = [];
+ var edges = [];
+
+ graph.eachNode(function(u, value) {
+ var node = {id: u, value: value};
+ if (graph.children) {
+ var children = graph.children(u);
+ if (children.length) {
+ node.children = children;
+ }
+ }
+ nodes.push(node);
+ });
+
+ graph.eachEdge(function(e, u, v, value) {
+ edges.push({id: e, u: u, v: v, value: value});
+ });
+
+ var type;
+ if (graph instanceof CDigraph) {
+ type = "cdigraph";
+ } else if (graph instanceof CGraph) {
+ type = "cgraph";
+ } else if (graph instanceof Digraph) {
+ type = "digraph";
+ } else if (graph instanceof Graph) {
+ type = "graph";
+ } else {
+ throw new Error("Couldn't determine type of graph: " + graph);
+ }
+
+ return { nodes: nodes, edges: edges, type: type };
+};
+
+function typeOf(obj) {
+ return Object.prototype.toString.call(obj).slice(8, -1);
+}
+
+},{"../CDigraph":30,"../CGraph":31,"../Digraph":32,"../Graph":33}],47:[function(require,module,exports){
+/* jshint -W079 */
+var Set = require("cp-data").Set;
+/* jshint +W079 */
+
+exports.all = function() {
+ return function() { return true; };
+};
+
+exports.nodesFromList = function(nodes) {
+ var set = new Set(nodes);
+ return function(u) {
+ return set.has(u);
+ };
+};
+
+},{"cp-data":5}],48:[function(require,module,exports){
+var Graph = require("./Graph"),
+ Digraph = require("./Digraph");
+
+// Side-effect based changes are lousy, but node doesn't seem to resolve the
+// requires cycle.
+
+/**
+ * Returns a new directed graph using the nodes and edges from this graph. The
+ * new graph will have the same nodes, but will have twice the number of edges:
+ * each edge is split into two edges with opposite directions. Edge ids,
+ * consequently, are not preserved by this transformation.
+ */
+Graph.prototype.toDigraph =
+Graph.prototype.asDirected = function() {
+ var g = new Digraph();
+ this.eachNode(function(u, value) { g.addNode(u, value); });
+ this.eachEdge(function(e, u, v, value) {
+ g.addEdge(null, u, v, value);
+ g.addEdge(null, v, u, value);
+ });
+ return g;
+};
+
+/**
+ * Returns a new undirected graph using the nodes and edges from this graph.
+ * The new graph will have the same nodes, but the edges will be made
+ * undirected. Edge ids are preserved in this transformation.
+ */
+Digraph.prototype.toGraph =
+Digraph.prototype.asUndirected = function() {
+ var g = new Graph();
+ this.eachNode(function(u, value) { g.addNode(u, value); });
+ this.eachEdge(function(e, u, v, value) {
+ g.addEdge(e, u, v, value);
+ });
+ return g;
+};
+
+},{"./Digraph":32,"./Graph":33}],49:[function(require,module,exports){
+// Returns an array of all values for properties of **o**.
+exports.values = function(o) {
+ var ks = Object.keys(o),
+ len = ks.length,
+ result = new Array(len),
+ i;
+ for (i = 0; i < len; ++i) {
+ result[i] = o[ks[i]];
+ }
+ return result;
+};
+
+},{}],50:[function(require,module,exports){
+module.exports = '0.7.4';
+
+},{}]},{},[1])
+; \ No newline at end of file
diff --git a/devtools/client/shared/vendor/immutable.js b/devtools/client/shared/vendor/immutable.js
new file mode 100644
index 000000000..4903f34b3
--- /dev/null
+++ b/devtools/client/shared/vendor/immutable.js
@@ -0,0 +1,4997 @@
+/**
+ * Copyright (c) 2014-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+(function (global, factory) {
+ typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
+ typeof define === 'function' && define.amd ? define(factory) :
+ (global.Immutable = factory());
+}(this, function () { 'use strict';var SLICE$0 = Array.prototype.slice;
+
+ function createClass(ctor, superClass) {
+ if (superClass) {
+ ctor.prototype = Object.create(superClass.prototype);
+ }
+ ctor.prototype.constructor = ctor;
+ }
+
+ function Iterable(value) {
+ return isIterable(value) ? value : Seq(value);
+ }
+
+
+ createClass(KeyedIterable, Iterable);
+ function KeyedIterable(value) {
+ return isKeyed(value) ? value : KeyedSeq(value);
+ }
+
+
+ createClass(IndexedIterable, Iterable);
+ function IndexedIterable(value) {
+ return isIndexed(value) ? value : IndexedSeq(value);
+ }
+
+
+ createClass(SetIterable, Iterable);
+ function SetIterable(value) {
+ return isIterable(value) && !isAssociative(value) ? value : SetSeq(value);
+ }
+
+
+
+ function isIterable(maybeIterable) {
+ return !!(maybeIterable && maybeIterable[IS_ITERABLE_SENTINEL]);
+ }
+
+ function isKeyed(maybeKeyed) {
+ return !!(maybeKeyed && maybeKeyed[IS_KEYED_SENTINEL]);
+ }
+
+ function isIndexed(maybeIndexed) {
+ return !!(maybeIndexed && maybeIndexed[IS_INDEXED_SENTINEL]);
+ }
+
+ function isAssociative(maybeAssociative) {
+ return isKeyed(maybeAssociative) || isIndexed(maybeAssociative);
+ }
+
+ function isOrdered(maybeOrdered) {
+ return !!(maybeOrdered && maybeOrdered[IS_ORDERED_SENTINEL]);
+ }
+
+ Iterable.isIterable = isIterable;
+ Iterable.isKeyed = isKeyed;
+ Iterable.isIndexed = isIndexed;
+ Iterable.isAssociative = isAssociative;
+ Iterable.isOrdered = isOrdered;
+
+ Iterable.Keyed = KeyedIterable;
+ Iterable.Indexed = IndexedIterable;
+ Iterable.Set = SetIterable;
+
+
+ var IS_ITERABLE_SENTINEL = '@@__IMMUTABLE_ITERABLE__@@';
+ var IS_KEYED_SENTINEL = '@@__IMMUTABLE_KEYED__@@';
+ var IS_INDEXED_SENTINEL = '@@__IMMUTABLE_INDEXED__@@';
+ var IS_ORDERED_SENTINEL = '@@__IMMUTABLE_ORDERED__@@';
+
+ // Used for setting prototype methods that IE8 chokes on.
+ var DELETE = 'delete';
+
+ // Constants describing the size of trie nodes.
+ var SHIFT = 5; // Resulted in best performance after ______?
+ var SIZE = 1 << SHIFT;
+ var MASK = SIZE - 1;
+
+ // A consistent shared value representing "not set" which equals nothing other
+ // than itself, and nothing that could be provided externally.
+ var NOT_SET = {};
+
+ // Boolean references, Rough equivalent of `bool &`.
+ var CHANGE_LENGTH = { value: false };
+ var DID_ALTER = { value: false };
+
+ function MakeRef(ref) {
+ ref.value = false;
+ return ref;
+ }
+
+ function SetRef(ref) {
+ ref && (ref.value = true);
+ }
+
+ // A function which returns a value representing an "owner" for transient writes
+ // to tries. The return value will only ever equal itself, and will not equal
+ // the return of any subsequent call of this function.
+ function OwnerID() {}
+
+ // http://jsperf.com/copy-array-inline
+ function arrCopy(arr, offset) {
+ offset = offset || 0;
+ var len = Math.max(0, arr.length - offset);
+ var newArr = new Array(len);
+ for (var ii = 0; ii < len; ii++) {
+ newArr[ii] = arr[ii + offset];
+ }
+ return newArr;
+ }
+
+ function ensureSize(iter) {
+ if (iter.size === undefined) {
+ iter.size = iter.__iterate(returnTrue);
+ }
+ return iter.size;
+ }
+
+ function wrapIndex(iter, index) {
+ // This implements "is array index" which the ECMAString spec defines as:
+ //
+ // A String property name P is an array index if and only if
+ // ToString(ToUint32(P)) is equal to P and ToUint32(P) is not equal
+ // to 2^32−1.
+ //
+ // http://www.ecma-international.org/ecma-262/6.0/#sec-array-exotic-objects
+ if (typeof index !== 'number') {
+ var uint32Index = index >>> 0; // N >>> 0 is shorthand for ToUint32
+ if ('' + uint32Index !== index || uint32Index === 4294967295) {
+ return NaN;
+ }
+ index = uint32Index;
+ }
+ return index < 0 ? ensureSize(iter) + index : index;
+ }
+
+ function returnTrue() {
+ return true;
+ }
+
+ function wholeSlice(begin, end, size) {
+ return (begin === 0 || (size !== undefined && begin <= -size)) &&
+ (end === undefined || (size !== undefined && end >= size));
+ }
+
+ function resolveBegin(begin, size) {
+ return resolveIndex(begin, size, 0);
+ }
+
+ function resolveEnd(end, size) {
+ return resolveIndex(end, size, size);
+ }
+
+ function resolveIndex(index, size, defaultIndex) {
+ return index === undefined ?
+ defaultIndex :
+ index < 0 ?
+ Math.max(0, size + index) :
+ size === undefined ?
+ index :
+ Math.min(size, index);
+ }
+
+ /* global Symbol */
+
+ var ITERATE_KEYS = 0;
+ var ITERATE_VALUES = 1;
+ var ITERATE_ENTRIES = 2;
+
+ var REAL_ITERATOR_SYMBOL = typeof Symbol === 'function' && Symbol.iterator;
+ var FAUX_ITERATOR_SYMBOL = '@@iterator';
+
+ var ITERATOR_SYMBOL = REAL_ITERATOR_SYMBOL || FAUX_ITERATOR_SYMBOL;
+
+
+ function Iterator(next) {
+ this.next = next;
+ }
+
+ Iterator.prototype.toString = function() {
+ return '[Iterator]';
+ };
+
+
+ Iterator.KEYS = ITERATE_KEYS;
+ Iterator.VALUES = ITERATE_VALUES;
+ Iterator.ENTRIES = ITERATE_ENTRIES;
+
+ Iterator.prototype.inspect =
+ Iterator.prototype.toSource = function () { return this.toString(); }
+ Iterator.prototype[ITERATOR_SYMBOL] = function () {
+ return this;
+ };
+
+
+ function iteratorValue(type, k, v, iteratorResult) {
+ var value = type === 0 ? k : type === 1 ? v : [k, v];
+ iteratorResult ? (iteratorResult.value = value) : (iteratorResult = {
+ value: value, done: false
+ });
+ return iteratorResult;
+ }
+
+ function iteratorDone() {
+ return { value: undefined, done: true };
+ }
+
+ function hasIterator(maybeIterable) {
+ return !!getIteratorFn(maybeIterable);
+ }
+
+ function isIterator(maybeIterator) {
+ return maybeIterator && typeof maybeIterator.next === 'function';
+ }
+
+ function getIterator(iterable) {
+ var iteratorFn = getIteratorFn(iterable);
+ return iteratorFn && iteratorFn.call(iterable);
+ }
+
+ function getIteratorFn(iterable) {
+ var iteratorFn = iterable && (
+ (REAL_ITERATOR_SYMBOL && iterable[REAL_ITERATOR_SYMBOL]) ||
+ iterable[FAUX_ITERATOR_SYMBOL]
+ );
+ if (typeof iteratorFn === 'function') {
+ return iteratorFn;
+ }
+ }
+
+ function isArrayLike(value) {
+ return value && typeof value.length === 'number';
+ }
+
+ createClass(Seq, Iterable);
+ function Seq(value) {
+ return value === null || value === undefined ? emptySequence() :
+ isIterable(value) ? value.toSeq() : seqFromValue(value);
+ }
+
+ Seq.of = function(/*...values*/) {
+ return Seq(arguments);
+ };
+
+ Seq.prototype.toSeq = function() {
+ return this;
+ };
+
+ Seq.prototype.toString = function() {
+ return this.__toString('Seq {', '}');
+ };
+
+ Seq.prototype.cacheResult = function() {
+ if (!this._cache && this.__iterateUncached) {
+ this._cache = this.entrySeq().toArray();
+ this.size = this._cache.length;
+ }
+ return this;
+ };
+
+ // abstract __iterateUncached(fn, reverse)
+
+ Seq.prototype.__iterate = function(fn, reverse) {
+ return seqIterate(this, fn, reverse, true);
+ };
+
+ // abstract __iteratorUncached(type, reverse)
+
+ Seq.prototype.__iterator = function(type, reverse) {
+ return seqIterator(this, type, reverse, true);
+ };
+
+
+
+ createClass(KeyedSeq, Seq);
+ function KeyedSeq(value) {
+ return value === null || value === undefined ?
+ emptySequence().toKeyedSeq() :
+ isIterable(value) ?
+ (isKeyed(value) ? value.toSeq() : value.fromEntrySeq()) :
+ keyedSeqFromValue(value);
+ }
+
+ KeyedSeq.prototype.toKeyedSeq = function() {
+ return this;
+ };
+
+
+
+ createClass(IndexedSeq, Seq);
+ function IndexedSeq(value) {
+ return value === null || value === undefined ? emptySequence() :
+ !isIterable(value) ? indexedSeqFromValue(value) :
+ isKeyed(value) ? value.entrySeq() : value.toIndexedSeq();
+ }
+
+ IndexedSeq.of = function(/*...values*/) {
+ return IndexedSeq(arguments);
+ };
+
+ IndexedSeq.prototype.toIndexedSeq = function() {
+ return this;
+ };
+
+ IndexedSeq.prototype.toString = function() {
+ return this.__toString('Seq [', ']');
+ };
+
+ IndexedSeq.prototype.__iterate = function(fn, reverse) {
+ return seqIterate(this, fn, reverse, false);
+ };
+
+ IndexedSeq.prototype.__iterator = function(type, reverse) {
+ return seqIterator(this, type, reverse, false);
+ };
+
+
+
+ createClass(SetSeq, Seq);
+ function SetSeq(value) {
+ return (
+ value === null || value === undefined ? emptySequence() :
+ !isIterable(value) ? indexedSeqFromValue(value) :
+ isKeyed(value) ? value.entrySeq() : value
+ ).toSetSeq();
+ }
+
+ SetSeq.of = function(/*...values*/) {
+ return SetSeq(arguments);
+ };
+
+ SetSeq.prototype.toSetSeq = function() {
+ return this;
+ };
+
+
+
+ Seq.isSeq = isSeq;
+ Seq.Keyed = KeyedSeq;
+ Seq.Set = SetSeq;
+ Seq.Indexed = IndexedSeq;
+
+ var IS_SEQ_SENTINEL = '@@__IMMUTABLE_SEQ__@@';
+
+ Seq.prototype[IS_SEQ_SENTINEL] = true;
+
+
+
+ createClass(ArraySeq, IndexedSeq);
+ function ArraySeq(array) {
+ this._array = array;
+ this.size = array.length;
+ }
+
+ ArraySeq.prototype.get = function(index, notSetValue) {
+ return this.has(index) ? this._array[wrapIndex(this, index)] : notSetValue;
+ };
+
+ ArraySeq.prototype.__iterate = function(fn, reverse) {
+ var array = this._array;
+ var maxIndex = array.length - 1;
+ for (var ii = 0; ii <= maxIndex; ii++) {
+ if (fn(array[reverse ? maxIndex - ii : ii], ii, this) === false) {
+ return ii + 1;
+ }
+ }
+ return ii;
+ };
+
+ ArraySeq.prototype.__iterator = function(type, reverse) {
+ var array = this._array;
+ var maxIndex = array.length - 1;
+ var ii = 0;
+ return new Iterator(function()
+ {return ii > maxIndex ?
+ iteratorDone() :
+ iteratorValue(type, ii, array[reverse ? maxIndex - ii++ : ii++])}
+ );
+ };
+
+
+
+ createClass(ObjectSeq, KeyedSeq);
+ function ObjectSeq(object) {
+ var keys = Object.keys(object);
+ this._object = object;
+ this._keys = keys;
+ this.size = keys.length;
+ }
+
+ ObjectSeq.prototype.get = function(key, notSetValue) {
+ if (notSetValue !== undefined && !this.has(key)) {
+ return notSetValue;
+ }
+ return this._object[key];
+ };
+
+ ObjectSeq.prototype.has = function(key) {
+ return this._object.hasOwnProperty(key);
+ };
+
+ ObjectSeq.prototype.__iterate = function(fn, reverse) {
+ var object = this._object;
+ var keys = this._keys;
+ var maxIndex = keys.length - 1;
+ for (var ii = 0; ii <= maxIndex; ii++) {
+ var key = keys[reverse ? maxIndex - ii : ii];
+ if (fn(object[key], key, this) === false) {
+ return ii + 1;
+ }
+ }
+ return ii;
+ };
+
+ ObjectSeq.prototype.__iterator = function(type, reverse) {
+ var object = this._object;
+ var keys = this._keys;
+ var maxIndex = keys.length - 1;
+ var ii = 0;
+ return new Iterator(function() {
+ var key = keys[reverse ? maxIndex - ii : ii];
+ return ii++ > maxIndex ?
+ iteratorDone() :
+ iteratorValue(type, key, object[key]);
+ });
+ };
+
+ ObjectSeq.prototype[IS_ORDERED_SENTINEL] = true;
+
+
+ createClass(IterableSeq, IndexedSeq);
+ function IterableSeq(iterable) {
+ this._iterable = iterable;
+ this.size = iterable.length || iterable.size;
+ }
+
+ IterableSeq.prototype.__iterateUncached = function(fn, reverse) {
+ if (reverse) {
+ return this.cacheResult().__iterate(fn, reverse);
+ }
+ var iterable = this._iterable;
+ var iterator = getIterator(iterable);
+ var iterations = 0;
+ if (isIterator(iterator)) {
+ var step;
+ while (!(step = iterator.next()).done) {
+ if (fn(step.value, iterations++, this) === false) {
+ break;
+ }
+ }
+ }
+ return iterations;
+ };
+
+ IterableSeq.prototype.__iteratorUncached = function(type, reverse) {
+ if (reverse) {
+ return this.cacheResult().__iterator(type, reverse);
+ }
+ var iterable = this._iterable;
+ var iterator = getIterator(iterable);
+ if (!isIterator(iterator)) {
+ return new Iterator(iteratorDone);
+ }
+ var iterations = 0;
+ return new Iterator(function() {
+ var step = iterator.next();
+ return step.done ? step : iteratorValue(type, iterations++, step.value);
+ });
+ };
+
+
+
+ createClass(IteratorSeq, IndexedSeq);
+ function IteratorSeq(iterator) {
+ this._iterator = iterator;
+ this._iteratorCache = [];
+ }
+
+ IteratorSeq.prototype.__iterateUncached = function(fn, reverse) {
+ if (reverse) {
+ return this.cacheResult().__iterate(fn, reverse);
+ }
+ var iterator = this._iterator;
+ var cache = this._iteratorCache;
+ var iterations = 0;
+ while (iterations < cache.length) {
+ if (fn(cache[iterations], iterations++, this) === false) {
+ return iterations;
+ }
+ }
+ var step;
+ while (!(step = iterator.next()).done) {
+ var val = step.value;
+ cache[iterations] = val;
+ if (fn(val, iterations++, this) === false) {
+ break;
+ }
+ }
+ return iterations;
+ };
+
+ IteratorSeq.prototype.__iteratorUncached = function(type, reverse) {
+ if (reverse) {
+ return this.cacheResult().__iterator(type, reverse);
+ }
+ var iterator = this._iterator;
+ var cache = this._iteratorCache;
+ var iterations = 0;
+ return new Iterator(function() {
+ if (iterations >= cache.length) {
+ var step = iterator.next();
+ if (step.done) {
+ return step;
+ }
+ cache[iterations] = step.value;
+ }
+ return iteratorValue(type, iterations, cache[iterations++]);
+ });
+ };
+
+
+
+
+ // # pragma Helper functions
+
+ function isSeq(maybeSeq) {
+ return !!(maybeSeq && maybeSeq[IS_SEQ_SENTINEL]);
+ }
+
+ var EMPTY_SEQ;
+
+ function emptySequence() {
+ return EMPTY_SEQ || (EMPTY_SEQ = new ArraySeq([]));
+ }
+
+ function keyedSeqFromValue(value) {
+ var seq =
+ Array.isArray(value) ? new ArraySeq(value).fromEntrySeq() :
+ isIterator(value) ? new IteratorSeq(value).fromEntrySeq() :
+ hasIterator(value) ? new IterableSeq(value).fromEntrySeq() :
+ typeof value === 'object' ? new ObjectSeq(value) :
+ undefined;
+ if (!seq) {
+ throw new TypeError(
+ 'Expected Array or iterable object of [k, v] entries, '+
+ 'or keyed object: ' + value
+ );
+ }
+ return seq;
+ }
+
+ function indexedSeqFromValue(value) {
+ var seq = maybeIndexedSeqFromValue(value);
+ if (!seq) {
+ throw new TypeError(
+ 'Expected Array or iterable object of values: ' + value
+ );
+ }
+ return seq;
+ }
+
+ function seqFromValue(value) {
+ var seq = maybeIndexedSeqFromValue(value) ||
+ (typeof value === 'object' && new ObjectSeq(value));
+ if (!seq) {
+ throw new TypeError(
+ 'Expected Array or iterable object of values, or keyed object: ' + value
+ );
+ }
+ return seq;
+ }
+
+ function maybeIndexedSeqFromValue(value) {
+ return (
+ isArrayLike(value) ? new ArraySeq(value) :
+ isIterator(value) ? new IteratorSeq(value) :
+ hasIterator(value) ? new IterableSeq(value) :
+ undefined
+ );
+ }
+
+ function seqIterate(seq, fn, reverse, useKeys) {
+ var cache = seq._cache;
+ if (cache) {
+ var maxIndex = cache.length - 1;
+ for (var ii = 0; ii <= maxIndex; ii++) {
+ var entry = cache[reverse ? maxIndex - ii : ii];
+ if (fn(entry[1], useKeys ? entry[0] : ii, seq) === false) {
+ return ii + 1;
+ }
+ }
+ return ii;
+ }
+ return seq.__iterateUncached(fn, reverse);
+ }
+
+ function seqIterator(seq, type, reverse, useKeys) {
+ var cache = seq._cache;
+ if (cache) {
+ var maxIndex = cache.length - 1;
+ var ii = 0;
+ return new Iterator(function() {
+ var entry = cache[reverse ? maxIndex - ii : ii];
+ return ii++ > maxIndex ?
+ iteratorDone() :
+ iteratorValue(type, useKeys ? entry[0] : ii - 1, entry[1]);
+ });
+ }
+ return seq.__iteratorUncached(type, reverse);
+ }
+
+ function fromJS(json, converter) {
+ return converter ?
+ fromJSWith(converter, json, '', {'': json}) :
+ fromJSDefault(json);
+ }
+
+ function fromJSWith(converter, json, key, parentJSON) {
+ if (Array.isArray(json)) {
+ return converter.call(parentJSON, key, IndexedSeq(json).map(function(v, k) {return fromJSWith(converter, v, k, json)}));
+ }
+ if (isPlainObj(json)) {
+ return converter.call(parentJSON, key, KeyedSeq(json).map(function(v, k) {return fromJSWith(converter, v, k, json)}));
+ }
+ return json;
+ }
+
+ function fromJSDefault(json) {
+ if (Array.isArray(json)) {
+ return IndexedSeq(json).map(fromJSDefault).toList();
+ }
+ if (isPlainObj(json)) {
+ return KeyedSeq(json).map(fromJSDefault).toMap();
+ }
+ return json;
+ }
+
+ function isPlainObj(value) {
+ return value && (value.constructor === Object || value.constructor === undefined);
+ }
+
+ /**
+ * An extension of the "same-value" algorithm as [described for use by ES6 Map
+ * and Set](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map#Key_equality)
+ *
+ * NaN is considered the same as NaN, however -0 and 0 are considered the same
+ * value, which is different from the algorithm described by
+ * [`Object.is`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is).
+ *
+ * This is extended further to allow Objects to describe the values they
+ * represent, by way of `valueOf` or `equals` (and `hashCode`).
+ *
+ * Note: because of this extension, the key equality of Immutable.Map and the
+ * value equality of Immutable.Set will differ from ES6 Map and Set.
+ *
+ * ### Defining custom values
+ *
+ * The easiest way to describe the value an object represents is by implementing
+ * `valueOf`. For example, `Date` represents a value by returning a unix
+ * timestamp for `valueOf`:
+ *
+ * var date1 = new Date(1234567890000); // Fri Feb 13 2009 ...
+ * var date2 = new Date(1234567890000);
+ * date1.valueOf(); // 1234567890000
+ * assert( date1 !== date2 );
+ * assert( Immutable.is( date1, date2 ) );
+ *
+ * Note: overriding `valueOf` may have other implications if you use this object
+ * where JavaScript expects a primitive, such as implicit string coercion.
+ *
+ * For more complex types, especially collections, implementing `valueOf` may
+ * not be performant. An alternative is to implement `equals` and `hashCode`.
+ *
+ * `equals` takes another object, presumably of similar type, and returns true
+ * if the it is equal. Equality is symmetrical, so the same result should be
+ * returned if this and the argument are flipped.
+ *
+ * assert( a.equals(b) === b.equals(a) );
+ *
+ * `hashCode` returns a 32bit integer number representing the object which will
+ * be used to determine how to store the value object in a Map or Set. You must
+ * provide both or neither methods, one must not exist without the other.
+ *
+ * Also, an important relationship between these methods must be upheld: if two
+ * values are equal, they *must* return the same hashCode. If the values are not
+ * equal, they might have the same hashCode; this is called a hash collision,
+ * and while undesirable for performance reasons, it is acceptable.
+ *
+ * if (a.equals(b)) {
+ * assert( a.hashCode() === b.hashCode() );
+ * }
+ *
+ * All Immutable collections implement `equals` and `hashCode`.
+ *
+ */
+ function is(valueA, valueB) {
+ if (valueA === valueB || (valueA !== valueA && valueB !== valueB)) {
+ return true;
+ }
+ if (!valueA || !valueB) {
+ return false;
+ }
+ if (typeof valueA.valueOf === 'function' &&
+ typeof valueB.valueOf === 'function') {
+ valueA = valueA.valueOf();
+ valueB = valueB.valueOf();
+ if (valueA === valueB || (valueA !== valueA && valueB !== valueB)) {
+ return true;
+ }
+ if (!valueA || !valueB) {
+ return false;
+ }
+ }
+ if (typeof valueA.equals === 'function' &&
+ typeof valueB.equals === 'function' &&
+ valueA.equals(valueB)) {
+ return true;
+ }
+ return false;
+ }
+
+ function deepEqual(a, b) {
+ if (a === b) {
+ return true;
+ }
+
+ if (
+ !isIterable(b) ||
+ a.size !== undefined && b.size !== undefined && a.size !== b.size ||
+ a.__hash !== undefined && b.__hash !== undefined && a.__hash !== b.__hash ||
+ isKeyed(a) !== isKeyed(b) ||
+ isIndexed(a) !== isIndexed(b) ||
+ isOrdered(a) !== isOrdered(b)
+ ) {
+ return false;
+ }
+
+ if (a.size === 0 && b.size === 0) {
+ return true;
+ }
+
+ var notAssociative = !isAssociative(a);
+
+ if (isOrdered(a)) {
+ var entries = a.entries();
+ return b.every(function(v, k) {
+ var entry = entries.next().value;
+ return entry && is(entry[1], v) && (notAssociative || is(entry[0], k));
+ }) && entries.next().done;
+ }
+
+ var flipped = false;
+
+ if (a.size === undefined) {
+ if (b.size === undefined) {
+ if (typeof a.cacheResult === 'function') {
+ a.cacheResult();
+ }
+ } else {
+ flipped = true;
+ var _ = a;
+ a = b;
+ b = _;
+ }
+ }
+
+ var allEqual = true;
+ var bSize = b.__iterate(function(v, k) {
+ if (notAssociative ? !a.has(v) :
+ flipped ? !is(v, a.get(k, NOT_SET)) : !is(a.get(k, NOT_SET), v)) {
+ allEqual = false;
+ return false;
+ }
+ });
+
+ return allEqual && a.size === bSize;
+ }
+
+ createClass(Repeat, IndexedSeq);
+
+ function Repeat(value, times) {
+ if (!(this instanceof Repeat)) {
+ return new Repeat(value, times);
+ }
+ this._value = value;
+ this.size = times === undefined ? Infinity : Math.max(0, times);
+ if (this.size === 0) {
+ if (EMPTY_REPEAT) {
+ return EMPTY_REPEAT;
+ }
+ EMPTY_REPEAT = this;
+ }
+ }
+
+ Repeat.prototype.toString = function() {
+ if (this.size === 0) {
+ return 'Repeat []';
+ }
+ return 'Repeat [ ' + this._value + ' ' + this.size + ' times ]';
+ };
+
+ Repeat.prototype.get = function(index, notSetValue) {
+ return this.has(index) ? this._value : notSetValue;
+ };
+
+ Repeat.prototype.includes = function(searchValue) {
+ return is(this._value, searchValue);
+ };
+
+ Repeat.prototype.slice = function(begin, end) {
+ var size = this.size;
+ return wholeSlice(begin, end, size) ? this :
+ new Repeat(this._value, resolveEnd(end, size) - resolveBegin(begin, size));
+ };
+
+ Repeat.prototype.reverse = function() {
+ return this;
+ };
+
+ Repeat.prototype.indexOf = function(searchValue) {
+ if (is(this._value, searchValue)) {
+ return 0;
+ }
+ return -1;
+ };
+
+ Repeat.prototype.lastIndexOf = function(searchValue) {
+ if (is(this._value, searchValue)) {
+ return this.size;
+ }
+ return -1;
+ };
+
+ Repeat.prototype.__iterate = function(fn, reverse) {
+ for (var ii = 0; ii < this.size; ii++) {
+ if (fn(this._value, ii, this) === false) {
+ return ii + 1;
+ }
+ }
+ return ii;
+ };
+
+ Repeat.prototype.__iterator = function(type, reverse) {var this$0 = this;
+ var ii = 0;
+ return new Iterator(function()
+ {return ii < this$0.size ? iteratorValue(type, ii++, this$0._value) : iteratorDone()}
+ );
+ };
+
+ Repeat.prototype.equals = function(other) {
+ return other instanceof Repeat ?
+ is(this._value, other._value) :
+ deepEqual(other);
+ };
+
+
+ var EMPTY_REPEAT;
+
+ function invariant(condition, error) {
+ if (!condition) throw new Error(error);
+ }
+
+ createClass(Range, IndexedSeq);
+
+ function Range(start, end, step) {
+ if (!(this instanceof Range)) {
+ return new Range(start, end, step);
+ }
+ invariant(step !== 0, 'Cannot step a Range by 0');
+ start = start || 0;
+ if (end === undefined) {
+ end = Infinity;
+ }
+ step = step === undefined ? 1 : Math.abs(step);
+ if (end < start) {
+ step = -step;
+ }
+ this._start = start;
+ this._end = end;
+ this._step = step;
+ this.size = Math.max(0, Math.ceil((end - start) / step - 1) + 1);
+ if (this.size === 0) {
+ if (EMPTY_RANGE) {
+ return EMPTY_RANGE;
+ }
+ EMPTY_RANGE = this;
+ }
+ }
+
+ Range.prototype.toString = function() {
+ if (this.size === 0) {
+ return 'Range []';
+ }
+ return 'Range [ ' +
+ this._start + '...' + this._end +
+ (this._step !== 1 ? ' by ' + this._step : '') +
+ ' ]';
+ };
+
+ Range.prototype.get = function(index, notSetValue) {
+ return this.has(index) ?
+ this._start + wrapIndex(this, index) * this._step :
+ notSetValue;
+ };
+
+ Range.prototype.includes = function(searchValue) {
+ var possibleIndex = (searchValue - this._start) / this._step;
+ return possibleIndex >= 0 &&
+ possibleIndex < this.size &&
+ possibleIndex === Math.floor(possibleIndex);
+ };
+
+ Range.prototype.slice = function(begin, end) {
+ if (wholeSlice(begin, end, this.size)) {
+ return this;
+ }
+ begin = resolveBegin(begin, this.size);
+ end = resolveEnd(end, this.size);
+ if (end <= begin) {
+ return new Range(0, 0);
+ }
+ return new Range(this.get(begin, this._end), this.get(end, this._end), this._step);
+ };
+
+ Range.prototype.indexOf = function(searchValue) {
+ var offsetValue = searchValue - this._start;
+ if (offsetValue % this._step === 0) {
+ var index = offsetValue / this._step;
+ if (index >= 0 && index < this.size) {
+ return index
+ }
+ }
+ return -1;
+ };
+
+ Range.prototype.lastIndexOf = function(searchValue) {
+ return this.indexOf(searchValue);
+ };
+
+ Range.prototype.__iterate = function(fn, reverse) {
+ var maxIndex = this.size - 1;
+ var step = this._step;
+ var value = reverse ? this._start + maxIndex * step : this._start;
+ for (var ii = 0; ii <= maxIndex; ii++) {
+ if (fn(value, ii, this) === false) {
+ return ii + 1;
+ }
+ value += reverse ? -step : step;
+ }
+ return ii;
+ };
+
+ Range.prototype.__iterator = function(type, reverse) {
+ var maxIndex = this.size - 1;
+ var step = this._step;
+ var value = reverse ? this._start + maxIndex * step : this._start;
+ var ii = 0;
+ return new Iterator(function() {
+ var v = value;
+ value += reverse ? -step : step;
+ return ii > maxIndex ? iteratorDone() : iteratorValue(type, ii++, v);
+ });
+ };
+
+ Range.prototype.equals = function(other) {
+ return other instanceof Range ?
+ this._start === other._start &&
+ this._end === other._end &&
+ this._step === other._step :
+ deepEqual(this, other);
+ };
+
+
+ var EMPTY_RANGE;
+
+ createClass(Collection, Iterable);
+ function Collection() {
+ throw TypeError('Abstract');
+ }
+
+
+ createClass(KeyedCollection, Collection);function KeyedCollection() {}
+
+ createClass(IndexedCollection, Collection);function IndexedCollection() {}
+
+ createClass(SetCollection, Collection);function SetCollection() {}
+
+
+ Collection.Keyed = KeyedCollection;
+ Collection.Indexed = IndexedCollection;
+ Collection.Set = SetCollection;
+
+ var imul =
+ typeof Math.imul === 'function' && Math.imul(0xffffffff, 2) === -2 ?
+ Math.imul :
+ function imul(a, b) {
+ a = a | 0; // int
+ b = b | 0; // int
+ var c = a & 0xffff;
+ var d = b & 0xffff;
+ // Shift by 0 fixes the sign on the high part.
+ return (c * d) + ((((a >>> 16) * d + c * (b >>> 16)) << 16) >>> 0) | 0; // int
+ };
+
+ // v8 has an optimization for storing 31-bit signed numbers.
+ // Values which have either 00 or 11 as the high order bits qualify.
+ // This function drops the highest order bit in a signed number, maintaining
+ // the sign bit.
+ function smi(i32) {
+ return ((i32 >>> 1) & 0x40000000) | (i32 & 0xBFFFFFFF);
+ }
+
+ function hash(o) {
+ if (o === false || o === null || o === undefined) {
+ return 0;
+ }
+ if (typeof o.valueOf === 'function') {
+ o = o.valueOf();
+ if (o === false || o === null || o === undefined) {
+ return 0;
+ }
+ }
+ if (o === true) {
+ return 1;
+ }
+ var type = typeof o;
+ if (type === 'number') {
+ var h = o | 0;
+ if (h !== o) {
+ h ^= o * 0xFFFFFFFF;
+ }
+ while (o > 0xFFFFFFFF) {
+ o /= 0xFFFFFFFF;
+ h ^= o;
+ }
+ return smi(h);
+ }
+ if (type === 'string') {
+ return o.length > STRING_HASH_CACHE_MIN_STRLEN ? cachedHashString(o) : hashString(o);
+ }
+ if (typeof o.hashCode === 'function') {
+ return o.hashCode();
+ }
+ if (type === 'object') {
+ return hashJSObj(o);
+ }
+ if (typeof o.toString === 'function') {
+ return hashString(o.toString());
+ }
+ throw new Error('Value type ' + type + ' cannot be hashed.');
+ }
+
+ function cachedHashString(string) {
+ var hash = stringHashCache[string];
+ if (hash === undefined) {
+ hash = hashString(string);
+ if (STRING_HASH_CACHE_SIZE === STRING_HASH_CACHE_MAX_SIZE) {
+ STRING_HASH_CACHE_SIZE = 0;
+ stringHashCache = {};
+ }
+ STRING_HASH_CACHE_SIZE++;
+ stringHashCache[string] = hash;
+ }
+ return hash;
+ }
+
+ // http://jsperf.com/hashing-strings
+ function hashString(string) {
+ // This is the hash from JVM
+ // The hash code for a string is computed as
+ // s[0] * 31 ^ (n - 1) + s[1] * 31 ^ (n - 2) + ... + s[n - 1],
+ // where s[i] is the ith character of the string and n is the length of
+ // the string. We "mod" the result to make it between 0 (inclusive) and 2^31
+ // (exclusive) by dropping high bits.
+ var hash = 0;
+ for (var ii = 0; ii < string.length; ii++) {
+ hash = 31 * hash + string.charCodeAt(ii) | 0;
+ }
+ return smi(hash);
+ }
+
+ function hashJSObj(obj) {
+ var hash;
+ if (usingWeakMap) {
+ hash = weakMap.get(obj);
+ if (hash !== undefined) {
+ return hash;
+ }
+ }
+
+ hash = obj[UID_HASH_KEY];
+ if (hash !== undefined) {
+ return hash;
+ }
+
+ if (!canDefineProperty) {
+ hash = obj.propertyIsEnumerable && obj.propertyIsEnumerable[UID_HASH_KEY];
+ if (hash !== undefined) {
+ return hash;
+ }
+
+ hash = getIENodeHash(obj);
+ if (hash !== undefined) {
+ return hash;
+ }
+ }
+
+ hash = ++objHashUID;
+ if (objHashUID & 0x40000000) {
+ objHashUID = 0;
+ }
+
+ if (usingWeakMap) {
+ weakMap.set(obj, hash);
+ } else if (isExtensible !== undefined && isExtensible(obj) === false) {
+ throw new Error('Non-extensible objects are not allowed as keys.');
+ } else if (canDefineProperty) {
+ Object.defineProperty(obj, UID_HASH_KEY, {
+ 'enumerable': false,
+ 'configurable': false,
+ 'writable': false,
+ 'value': hash
+ });
+ } else if (obj.propertyIsEnumerable !== undefined &&
+ obj.propertyIsEnumerable === obj.constructor.prototype.propertyIsEnumerable) {
+ // Since we can't define a non-enumerable property on the object
+ // we'll hijack one of the less-used non-enumerable properties to
+ // save our hash on it. Since this is a function it will not show up in
+ // `JSON.stringify` which is what we want.
+ obj.propertyIsEnumerable = function() {
+ return this.constructor.prototype.propertyIsEnumerable.apply(this, arguments);
+ };
+ obj.propertyIsEnumerable[UID_HASH_KEY] = hash;
+ } else if (obj.nodeType !== undefined) {
+ // At this point we couldn't get the IE `uniqueID` to use as a hash
+ // and we couldn't use a non-enumerable property to exploit the
+ // dontEnum bug so we simply add the `UID_HASH_KEY` on the node
+ // itself.
+ obj[UID_HASH_KEY] = hash;
+ } else {
+ throw new Error('Unable to set a non-enumerable property on object.');
+ }
+
+ return hash;
+ }
+
+ // Get references to ES5 object methods.
+ var isExtensible = Object.isExtensible;
+
+ // True if Object.defineProperty works as expected. IE8 fails this test.
+ var canDefineProperty = (function() {
+ try {
+ Object.defineProperty({}, '@', {});
+ return true;
+ } catch (e) {
+ return false;
+ }
+ }());
+
+ // IE has a `uniqueID` property on DOM nodes. We can construct the hash from it
+ // and avoid memory leaks from the IE cloneNode bug.
+ function getIENodeHash(node) {
+ if (node && node.nodeType > 0) {
+ switch (node.nodeType) {
+ case 1: // Element
+ return node.uniqueID;
+ case 9: // Document
+ return node.documentElement && node.documentElement.uniqueID;
+ }
+ }
+ }
+
+ // If possible, use a WeakMap.
+ var usingWeakMap = typeof WeakMap === 'function';
+ var weakMap;
+ if (usingWeakMap) {
+ weakMap = new WeakMap();
+ }
+
+ var objHashUID = 0;
+
+ var UID_HASH_KEY = '__immutablehash__';
+ if (typeof Symbol === 'function') {
+ UID_HASH_KEY = Symbol(UID_HASH_KEY);
+ }
+
+ var STRING_HASH_CACHE_MIN_STRLEN = 16;
+ var STRING_HASH_CACHE_MAX_SIZE = 255;
+ var STRING_HASH_CACHE_SIZE = 0;
+ var stringHashCache = {};
+
+ function assertNotInfinite(size) {
+ invariant(
+ size !== Infinity,
+ 'Cannot perform this action with an infinite size.'
+ );
+ }
+
+ createClass(Map, KeyedCollection);
+
+ // @pragma Construction
+
+ function Map(value) {
+ return value === null || value === undefined ? emptyMap() :
+ isMap(value) && !isOrdered(value) ? value :
+ emptyMap().withMutations(function(map ) {
+ var iter = KeyedIterable(value);
+ assertNotInfinite(iter.size);
+ iter.forEach(function(v, k) {return map.set(k, v)});
+ });
+ }
+
+ Map.of = function() {var keyValues = SLICE$0.call(arguments, 0);
+ return emptyMap().withMutations(function(map ) {
+ for (var i = 0; i < keyValues.length; i += 2) {
+ if (i + 1 >= keyValues.length) {
+ throw new Error('Missing value for key: ' + keyValues[i]);
+ }
+ map.set(keyValues[i], keyValues[i + 1]);
+ }
+ });
+ };
+
+ Map.prototype.toString = function() {
+ return this.__toString('Map {', '}');
+ };
+
+ // @pragma Access
+
+ Map.prototype.get = function(k, notSetValue) {
+ return this._root ?
+ this._root.get(0, undefined, k, notSetValue) :
+ notSetValue;
+ };
+
+ // @pragma Modification
+
+ Map.prototype.set = function(k, v) {
+ return updateMap(this, k, v);
+ };
+
+ Map.prototype.setIn = function(keyPath, v) {
+ return this.updateIn(keyPath, NOT_SET, function() {return v});
+ };
+
+ Map.prototype.remove = function(k) {
+ return updateMap(this, k, NOT_SET);
+ };
+
+ Map.prototype.deleteIn = function(keyPath) {
+ return this.updateIn(keyPath, function() {return NOT_SET});
+ };
+
+ Map.prototype.update = function(k, notSetValue, updater) {
+ return arguments.length === 1 ?
+ k(this) :
+ this.updateIn([k], notSetValue, updater);
+ };
+
+ Map.prototype.updateIn = function(keyPath, notSetValue, updater) {
+ if (!updater) {
+ updater = notSetValue;
+ notSetValue = undefined;
+ }
+ var updatedValue = updateInDeepMap(
+ this,
+ forceIterator(keyPath),
+ notSetValue,
+ updater
+ );
+ return updatedValue === NOT_SET ? undefined : updatedValue;
+ };
+
+ Map.prototype.clear = function() {
+ if (this.size === 0) {
+ return this;
+ }
+ if (this.__ownerID) {
+ this.size = 0;
+ this._root = null;
+ this.__hash = undefined;
+ this.__altered = true;
+ return this;
+ }
+ return emptyMap();
+ };
+
+ // @pragma Composition
+
+ Map.prototype.merge = function(/*...iters*/) {
+ return mergeIntoMapWith(this, undefined, arguments);
+ };
+
+ Map.prototype.mergeWith = function(merger) {var iters = SLICE$0.call(arguments, 1);
+ return mergeIntoMapWith(this, merger, iters);
+ };
+
+ Map.prototype.mergeIn = function(keyPath) {var iters = SLICE$0.call(arguments, 1);
+ return this.updateIn(
+ keyPath,
+ emptyMap(),
+ function(m ) {return typeof m.merge === 'function' ?
+ m.merge.apply(m, iters) :
+ iters[iters.length - 1]}
+ );
+ };
+
+ Map.prototype.mergeDeep = function(/*...iters*/) {
+ return mergeIntoMapWith(this, deepMerger, arguments);
+ };
+
+ Map.prototype.mergeDeepWith = function(merger) {var iters = SLICE$0.call(arguments, 1);
+ return mergeIntoMapWith(this, deepMergerWith(merger), iters);
+ };
+
+ Map.prototype.mergeDeepIn = function(keyPath) {var iters = SLICE$0.call(arguments, 1);
+ return this.updateIn(
+ keyPath,
+ emptyMap(),
+ function(m ) {return typeof m.mergeDeep === 'function' ?
+ m.mergeDeep.apply(m, iters) :
+ iters[iters.length - 1]}
+ );
+ };
+
+ Map.prototype.sort = function(comparator) {
+ // Late binding
+ return OrderedMap(sortFactory(this, comparator));
+ };
+
+ Map.prototype.sortBy = function(mapper, comparator) {
+ // Late binding
+ return OrderedMap(sortFactory(this, comparator, mapper));
+ };
+
+ // @pragma Mutability
+
+ Map.prototype.withMutations = function(fn) {
+ var mutable = this.asMutable();
+ fn(mutable);
+ return mutable.wasAltered() ? mutable.__ensureOwner(this.__ownerID) : this;
+ };
+
+ Map.prototype.asMutable = function() {
+ return this.__ownerID ? this : this.__ensureOwner(new OwnerID());
+ };
+
+ Map.prototype.asImmutable = function() {
+ return this.__ensureOwner();
+ };
+
+ Map.prototype.wasAltered = function() {
+ return this.__altered;
+ };
+
+ Map.prototype.__iterator = function(type, reverse) {
+ return new MapIterator(this, type, reverse);
+ };
+
+ Map.prototype.__iterate = function(fn, reverse) {var this$0 = this;
+ var iterations = 0;
+ this._root && this._root.iterate(function(entry ) {
+ iterations++;
+ return fn(entry[1], entry[0], this$0);
+ }, reverse);
+ return iterations;
+ };
+
+ Map.prototype.__ensureOwner = function(ownerID) {
+ if (ownerID === this.__ownerID) {
+ return this;
+ }
+ if (!ownerID) {
+ this.__ownerID = ownerID;
+ this.__altered = false;
+ return this;
+ }
+ return makeMap(this.size, this._root, ownerID, this.__hash);
+ };
+
+
+ function isMap(maybeMap) {
+ return !!(maybeMap && maybeMap[IS_MAP_SENTINEL]);
+ }
+
+ Map.isMap = isMap;
+
+ var IS_MAP_SENTINEL = '@@__IMMUTABLE_MAP__@@';
+
+ var MapPrototype = Map.prototype;
+ MapPrototype[IS_MAP_SENTINEL] = true;
+ MapPrototype[DELETE] = MapPrototype.remove;
+ MapPrototype.removeIn = MapPrototype.deleteIn;
+
+
+ // #pragma Trie Nodes
+
+
+
+ function ArrayMapNode(ownerID, entries) {
+ this.ownerID = ownerID;
+ this.entries = entries;
+ }
+
+ ArrayMapNode.prototype.get = function(shift, keyHash, key, notSetValue) {
+ var entries = this.entries;
+ for (var ii = 0, len = entries.length; ii < len; ii++) {
+ if (is(key, entries[ii][0])) {
+ return entries[ii][1];
+ }
+ }
+ return notSetValue;
+ };
+
+ ArrayMapNode.prototype.update = function(ownerID, shift, keyHash, key, value, didChangeSize, didAlter) {
+ var removed = value === NOT_SET;
+
+ var entries = this.entries;
+ var idx = 0;
+ for (var len = entries.length; idx < len; idx++) {
+ if (is(key, entries[idx][0])) {
+ break;
+ }
+ }
+ var exists = idx < len;
+
+ if (exists ? entries[idx][1] === value : removed) {
+ return this;
+ }
+
+ SetRef(didAlter);
+ (removed || !exists) && SetRef(didChangeSize);
+
+ if (removed && entries.length === 1) {
+ return; // undefined
+ }
+
+ if (!exists && !removed && entries.length >= MAX_ARRAY_MAP_SIZE) {
+ return createNodes(ownerID, entries, key, value);
+ }
+
+ var isEditable = ownerID && ownerID === this.ownerID;
+ var newEntries = isEditable ? entries : arrCopy(entries);
+
+ if (exists) {
+ if (removed) {
+ idx === len - 1 ? newEntries.pop() : (newEntries[idx] = newEntries.pop());
+ } else {
+ newEntries[idx] = [key, value];
+ }
+ } else {
+ newEntries.push([key, value]);
+ }
+
+ if (isEditable) {
+ this.entries = newEntries;
+ return this;
+ }
+
+ return new ArrayMapNode(ownerID, newEntries);
+ };
+
+
+
+
+ function BitmapIndexedNode(ownerID, bitmap, nodes) {
+ this.ownerID = ownerID;
+ this.bitmap = bitmap;
+ this.nodes = nodes;
+ }
+
+ BitmapIndexedNode.prototype.get = function(shift, keyHash, key, notSetValue) {
+ if (keyHash === undefined) {
+ keyHash = hash(key);
+ }
+ var bit = (1 << ((shift === 0 ? keyHash : keyHash >>> shift) & MASK));
+ var bitmap = this.bitmap;
+ return (bitmap & bit) === 0 ? notSetValue :
+ this.nodes[popCount(bitmap & (bit - 1))].get(shift + SHIFT, keyHash, key, notSetValue);
+ };
+
+ BitmapIndexedNode.prototype.update = function(ownerID, shift, keyHash, key, value, didChangeSize, didAlter) {
+ if (keyHash === undefined) {
+ keyHash = hash(key);
+ }
+ var keyHashFrag = (shift === 0 ? keyHash : keyHash >>> shift) & MASK;
+ var bit = 1 << keyHashFrag;
+ var bitmap = this.bitmap;
+ var exists = (bitmap & bit) !== 0;
+
+ if (!exists && value === NOT_SET) {
+ return this;
+ }
+
+ var idx = popCount(bitmap & (bit - 1));
+ var nodes = this.nodes;
+ var node = exists ? nodes[idx] : undefined;
+ var newNode = updateNode(node, ownerID, shift + SHIFT, keyHash, key, value, didChangeSize, didAlter);
+
+ if (newNode === node) {
+ return this;
+ }
+
+ if (!exists && newNode && nodes.length >= MAX_BITMAP_INDEXED_SIZE) {
+ return expandNodes(ownerID, nodes, bitmap, keyHashFrag, newNode);
+ }
+
+ if (exists && !newNode && nodes.length === 2 && isLeafNode(nodes[idx ^ 1])) {
+ return nodes[idx ^ 1];
+ }
+
+ if (exists && newNode && nodes.length === 1 && isLeafNode(newNode)) {
+ return newNode;
+ }
+
+ var isEditable = ownerID && ownerID === this.ownerID;
+ var newBitmap = exists ? newNode ? bitmap : bitmap ^ bit : bitmap | bit;
+ var newNodes = exists ? newNode ?
+ setIn(nodes, idx, newNode, isEditable) :
+ spliceOut(nodes, idx, isEditable) :
+ spliceIn(nodes, idx, newNode, isEditable);
+
+ if (isEditable) {
+ this.bitmap = newBitmap;
+ this.nodes = newNodes;
+ return this;
+ }
+
+ return new BitmapIndexedNode(ownerID, newBitmap, newNodes);
+ };
+
+
+
+
+ function HashArrayMapNode(ownerID, count, nodes) {
+ this.ownerID = ownerID;
+ this.count = count;
+ this.nodes = nodes;
+ }
+
+ HashArrayMapNode.prototype.get = function(shift, keyHash, key, notSetValue) {
+ if (keyHash === undefined) {
+ keyHash = hash(key);
+ }
+ var idx = (shift === 0 ? keyHash : keyHash >>> shift) & MASK;
+ var node = this.nodes[idx];
+ return node ? node.get(shift + SHIFT, keyHash, key, notSetValue) : notSetValue;
+ };
+
+ HashArrayMapNode.prototype.update = function(ownerID, shift, keyHash, key, value, didChangeSize, didAlter) {
+ if (keyHash === undefined) {
+ keyHash = hash(key);
+ }
+ var idx = (shift === 0 ? keyHash : keyHash >>> shift) & MASK;
+ var removed = value === NOT_SET;
+ var nodes = this.nodes;
+ var node = nodes[idx];
+
+ if (removed && !node) {
+ return this;
+ }
+
+ var newNode = updateNode(node, ownerID, shift + SHIFT, keyHash, key, value, didChangeSize, didAlter);
+ if (newNode === node) {
+ return this;
+ }
+
+ var newCount = this.count;
+ if (!node) {
+ newCount++;
+ } else if (!newNode) {
+ newCount--;
+ if (newCount < MIN_HASH_ARRAY_MAP_SIZE) {
+ return packNodes(ownerID, nodes, newCount, idx);
+ }
+ }
+
+ var isEditable = ownerID && ownerID === this.ownerID;
+ var newNodes = setIn(nodes, idx, newNode, isEditable);
+
+ if (isEditable) {
+ this.count = newCount;
+ this.nodes = newNodes;
+ return this;
+ }
+
+ return new HashArrayMapNode(ownerID, newCount, newNodes);
+ };
+
+
+
+
+ function HashCollisionNode(ownerID, keyHash, entries) {
+ this.ownerID = ownerID;
+ this.keyHash = keyHash;
+ this.entries = entries;
+ }
+
+ HashCollisionNode.prototype.get = function(shift, keyHash, key, notSetValue) {
+ var entries = this.entries;
+ for (var ii = 0, len = entries.length; ii < len; ii++) {
+ if (is(key, entries[ii][0])) {
+ return entries[ii][1];
+ }
+ }
+ return notSetValue;
+ };
+
+ HashCollisionNode.prototype.update = function(ownerID, shift, keyHash, key, value, didChangeSize, didAlter) {
+ if (keyHash === undefined) {
+ keyHash = hash(key);
+ }
+
+ var removed = value === NOT_SET;
+
+ if (keyHash !== this.keyHash) {
+ if (removed) {
+ return this;
+ }
+ SetRef(didAlter);
+ SetRef(didChangeSize);
+ return mergeIntoNode(this, ownerID, shift, keyHash, [key, value]);
+ }
+
+ var entries = this.entries;
+ var idx = 0;
+ for (var len = entries.length; idx < len; idx++) {
+ if (is(key, entries[idx][0])) {
+ break;
+ }
+ }
+ var exists = idx < len;
+
+ if (exists ? entries[idx][1] === value : removed) {
+ return this;
+ }
+
+ SetRef(didAlter);
+ (removed || !exists) && SetRef(didChangeSize);
+
+ if (removed && len === 2) {
+ return new ValueNode(ownerID, this.keyHash, entries[idx ^ 1]);
+ }
+
+ var isEditable = ownerID && ownerID === this.ownerID;
+ var newEntries = isEditable ? entries : arrCopy(entries);
+
+ if (exists) {
+ if (removed) {
+ idx === len - 1 ? newEntries.pop() : (newEntries[idx] = newEntries.pop());
+ } else {
+ newEntries[idx] = [key, value];
+ }
+ } else {
+ newEntries.push([key, value]);
+ }
+
+ if (isEditable) {
+ this.entries = newEntries;
+ return this;
+ }
+
+ return new HashCollisionNode(ownerID, this.keyHash, newEntries);
+ };
+
+
+
+
+ function ValueNode(ownerID, keyHash, entry) {
+ this.ownerID = ownerID;
+ this.keyHash = keyHash;
+ this.entry = entry;
+ }
+
+ ValueNode.prototype.get = function(shift, keyHash, key, notSetValue) {
+ return is(key, this.entry[0]) ? this.entry[1] : notSetValue;
+ };
+
+ ValueNode.prototype.update = function(ownerID, shift, keyHash, key, value, didChangeSize, didAlter) {
+ var removed = value === NOT_SET;
+ var keyMatch = is(key, this.entry[0]);
+ if (keyMatch ? value === this.entry[1] : removed) {
+ return this;
+ }
+
+ SetRef(didAlter);
+
+ if (removed) {
+ SetRef(didChangeSize);
+ return; // undefined
+ }
+
+ if (keyMatch) {
+ if (ownerID && ownerID === this.ownerID) {
+ this.entry[1] = value;
+ return this;
+ }
+ return new ValueNode(ownerID, this.keyHash, [key, value]);
+ }
+
+ SetRef(didChangeSize);
+ return mergeIntoNode(this, ownerID, shift, hash(key), [key, value]);
+ };
+
+
+
+ // #pragma Iterators
+
+ ArrayMapNode.prototype.iterate =
+ HashCollisionNode.prototype.iterate = function (fn, reverse) {
+ var entries = this.entries;
+ for (var ii = 0, maxIndex = entries.length - 1; ii <= maxIndex; ii++) {
+ if (fn(entries[reverse ? maxIndex - ii : ii]) === false) {
+ return false;
+ }
+ }
+ }
+
+ BitmapIndexedNode.prototype.iterate =
+ HashArrayMapNode.prototype.iterate = function (fn, reverse) {
+ var nodes = this.nodes;
+ for (var ii = 0, maxIndex = nodes.length - 1; ii <= maxIndex; ii++) {
+ var node = nodes[reverse ? maxIndex - ii : ii];
+ if (node && node.iterate(fn, reverse) === false) {
+ return false;
+ }
+ }
+ }
+
+ ValueNode.prototype.iterate = function (fn, reverse) {
+ return fn(this.entry);
+ }
+
+ createClass(MapIterator, Iterator);
+
+ function MapIterator(map, type, reverse) {
+ this._type = type;
+ this._reverse = reverse;
+ this._stack = map._root && mapIteratorFrame(map._root);
+ }
+
+ MapIterator.prototype.next = function() {
+ var type = this._type;
+ var stack = this._stack;
+ while (stack) {
+ var node = stack.node;
+ var index = stack.index++;
+ var maxIndex;
+ if (node.entry) {
+ if (index === 0) {
+ return mapIteratorValue(type, node.entry);
+ }
+ } else if (node.entries) {
+ maxIndex = node.entries.length - 1;
+ if (index <= maxIndex) {
+ return mapIteratorValue(type, node.entries[this._reverse ? maxIndex - index : index]);
+ }
+ } else {
+ maxIndex = node.nodes.length - 1;
+ if (index <= maxIndex) {
+ var subNode = node.nodes[this._reverse ? maxIndex - index : index];
+ if (subNode) {
+ if (subNode.entry) {
+ return mapIteratorValue(type, subNode.entry);
+ }
+ stack = this._stack = mapIteratorFrame(subNode, stack);
+ }
+ continue;
+ }
+ }
+ stack = this._stack = this._stack.__prev;
+ }
+ return iteratorDone();
+ };
+
+
+ function mapIteratorValue(type, entry) {
+ return iteratorValue(type, entry[0], entry[1]);
+ }
+
+ function mapIteratorFrame(node, prev) {
+ return {
+ node: node,
+ index: 0,
+ __prev: prev
+ };
+ }
+
+ function makeMap(size, root, ownerID, hash) {
+ var map = Object.create(MapPrototype);
+ map.size = size;
+ map._root = root;
+ map.__ownerID = ownerID;
+ map.__hash = hash;
+ map.__altered = false;
+ return map;
+ }
+
+ var EMPTY_MAP;
+ function emptyMap() {
+ return EMPTY_MAP || (EMPTY_MAP = makeMap(0));
+ }
+
+ function updateMap(map, k, v) {
+ var newRoot;
+ var newSize;
+ if (!map._root) {
+ if (v === NOT_SET) {
+ return map;
+ }
+ newSize = 1;
+ newRoot = new ArrayMapNode(map.__ownerID, [[k, v]]);
+ } else {
+ var didChangeSize = MakeRef(CHANGE_LENGTH);
+ var didAlter = MakeRef(DID_ALTER);
+ newRoot = updateNode(map._root, map.__ownerID, 0, undefined, k, v, didChangeSize, didAlter);
+ if (!didAlter.value) {
+ return map;
+ }
+ newSize = map.size + (didChangeSize.value ? v === NOT_SET ? -1 : 1 : 0);
+ }
+ if (map.__ownerID) {
+ map.size = newSize;
+ map._root = newRoot;
+ map.__hash = undefined;
+ map.__altered = true;
+ return map;
+ }
+ return newRoot ? makeMap(newSize, newRoot) : emptyMap();
+ }
+
+ function updateNode(node, ownerID, shift, keyHash, key, value, didChangeSize, didAlter) {
+ if (!node) {
+ if (value === NOT_SET) {
+ return node;
+ }
+ SetRef(didAlter);
+ SetRef(didChangeSize);
+ return new ValueNode(ownerID, keyHash, [key, value]);
+ }
+ return node.update(ownerID, shift, keyHash, key, value, didChangeSize, didAlter);
+ }
+
+ function isLeafNode(node) {
+ return node.constructor === ValueNode || node.constructor === HashCollisionNode;
+ }
+
+ function mergeIntoNode(node, ownerID, shift, keyHash, entry) {
+ if (node.keyHash === keyHash) {
+ return new HashCollisionNode(ownerID, keyHash, [node.entry, entry]);
+ }
+
+ var idx1 = (shift === 0 ? node.keyHash : node.keyHash >>> shift) & MASK;
+ var idx2 = (shift === 0 ? keyHash : keyHash >>> shift) & MASK;
+
+ var newNode;
+ var nodes = idx1 === idx2 ?
+ [mergeIntoNode(node, ownerID, shift + SHIFT, keyHash, entry)] :
+ ((newNode = new ValueNode(ownerID, keyHash, entry)), idx1 < idx2 ? [node, newNode] : [newNode, node]);
+
+ return new BitmapIndexedNode(ownerID, (1 << idx1) | (1 << idx2), nodes);
+ }
+
+ function createNodes(ownerID, entries, key, value) {
+ if (!ownerID) {
+ ownerID = new OwnerID();
+ }
+ var node = new ValueNode(ownerID, hash(key), [key, value]);
+ for (var ii = 0; ii < entries.length; ii++) {
+ var entry = entries[ii];
+ node = node.update(ownerID, 0, undefined, entry[0], entry[1]);
+ }
+ return node;
+ }
+
+ function packNodes(ownerID, nodes, count, excluding) {
+ var bitmap = 0;
+ var packedII = 0;
+ var packedNodes = new Array(count);
+ for (var ii = 0, bit = 1, len = nodes.length; ii < len; ii++, bit <<= 1) {
+ var node = nodes[ii];
+ if (node !== undefined && ii !== excluding) {
+ bitmap |= bit;
+ packedNodes[packedII++] = node;
+ }
+ }
+ return new BitmapIndexedNode(ownerID, bitmap, packedNodes);
+ }
+
+ function expandNodes(ownerID, nodes, bitmap, including, node) {
+ var count = 0;
+ var expandedNodes = new Array(SIZE);
+ for (var ii = 0; bitmap !== 0; ii++, bitmap >>>= 1) {
+ expandedNodes[ii] = bitmap & 1 ? nodes[count++] : undefined;
+ }
+ expandedNodes[including] = node;
+ return new HashArrayMapNode(ownerID, count + 1, expandedNodes);
+ }
+
+ function mergeIntoMapWith(map, merger, iterables) {
+ var iters = [];
+ for (var ii = 0; ii < iterables.length; ii++) {
+ var value = iterables[ii];
+ var iter = KeyedIterable(value);
+ if (!isIterable(value)) {
+ iter = iter.map(function(v ) {return fromJS(v)});
+ }
+ iters.push(iter);
+ }
+ return mergeIntoCollectionWith(map, merger, iters);
+ }
+
+ function deepMerger(existing, value, key) {
+ return existing && existing.mergeDeep && isIterable(value) ?
+ existing.mergeDeep(value) :
+ is(existing, value) ? existing : value;
+ }
+
+ function deepMergerWith(merger) {
+ return function(existing, value, key) {
+ if (existing && existing.mergeDeepWith && isIterable(value)) {
+ return existing.mergeDeepWith(merger, value);
+ }
+ var nextValue = merger(existing, value, key);
+ return is(existing, nextValue) ? existing : nextValue;
+ };
+ }
+
+ function mergeIntoCollectionWith(collection, merger, iters) {
+ iters = iters.filter(function(x ) {return x.size !== 0});
+ if (iters.length === 0) {
+ return collection;
+ }
+ if (collection.size === 0 && !collection.__ownerID && iters.length === 1) {
+ return collection.constructor(iters[0]);
+ }
+ return collection.withMutations(function(collection ) {
+ var mergeIntoMap = merger ?
+ function(value, key) {
+ collection.update(key, NOT_SET, function(existing )
+ {return existing === NOT_SET ? value : merger(existing, value, key)}
+ );
+ } :
+ function(value, key) {
+ collection.set(key, value);
+ }
+ for (var ii = 0; ii < iters.length; ii++) {
+ iters[ii].forEach(mergeIntoMap);
+ }
+ });
+ }
+
+ function updateInDeepMap(existing, keyPathIter, notSetValue, updater) {
+ var isNotSet = existing === NOT_SET;
+ var step = keyPathIter.next();
+ if (step.done) {
+ var existingValue = isNotSet ? notSetValue : existing;
+ var newValue = updater(existingValue);
+ return newValue === existingValue ? existing : newValue;
+ }
+ invariant(
+ isNotSet || (existing && existing.set),
+ 'invalid keyPath'
+ );
+ var key = step.value;
+ var nextExisting = isNotSet ? NOT_SET : existing.get(key, NOT_SET);
+ var nextUpdated = updateInDeepMap(
+ nextExisting,
+ keyPathIter,
+ notSetValue,
+ updater
+ );
+ return nextUpdated === nextExisting ? existing :
+ nextUpdated === NOT_SET ? existing.remove(key) :
+ (isNotSet ? emptyMap() : existing).set(key, nextUpdated);
+ }
+
+ function popCount(x) {
+ x = x - ((x >> 1) & 0x55555555);
+ x = (x & 0x33333333) + ((x >> 2) & 0x33333333);
+ x = (x + (x >> 4)) & 0x0f0f0f0f;
+ x = x + (x >> 8);
+ x = x + (x >> 16);
+ return x & 0x7f;
+ }
+
+ function setIn(array, idx, val, canEdit) {
+ var newArray = canEdit ? array : arrCopy(array);
+ newArray[idx] = val;
+ return newArray;
+ }
+
+ function spliceIn(array, idx, val, canEdit) {
+ var newLen = array.length + 1;
+ if (canEdit && idx + 1 === newLen) {
+ array[idx] = val;
+ return array;
+ }
+ var newArray = new Array(newLen);
+ var after = 0;
+ for (var ii = 0; ii < newLen; ii++) {
+ if (ii === idx) {
+ newArray[ii] = val;
+ after = -1;
+ } else {
+ newArray[ii] = array[ii + after];
+ }
+ }
+ return newArray;
+ }
+
+ function spliceOut(array, idx, canEdit) {
+ var newLen = array.length - 1;
+ if (canEdit && idx === newLen) {
+ array.pop();
+ return array;
+ }
+ var newArray = new Array(newLen);
+ var after = 0;
+ for (var ii = 0; ii < newLen; ii++) {
+ if (ii === idx) {
+ after = 1;
+ }
+ newArray[ii] = array[ii + after];
+ }
+ return newArray;
+ }
+
+ var MAX_ARRAY_MAP_SIZE = SIZE / 4;
+ var MAX_BITMAP_INDEXED_SIZE = SIZE / 2;
+ var MIN_HASH_ARRAY_MAP_SIZE = SIZE / 4;
+
+ createClass(List, IndexedCollection);
+
+ // @pragma Construction
+
+ function List(value) {
+ var empty = emptyList();
+ if (value === null || value === undefined) {
+ return empty;
+ }
+ if (isList(value)) {
+ return value;
+ }
+ var iter = IndexedIterable(value);
+ var size = iter.size;
+ if (size === 0) {
+ return empty;
+ }
+ assertNotInfinite(size);
+ if (size > 0 && size < SIZE) {
+ return makeList(0, size, SHIFT, null, new VNode(iter.toArray()));
+ }
+ return empty.withMutations(function(list ) {
+ list.setSize(size);
+ iter.forEach(function(v, i) {return list.set(i, v)});
+ });
+ }
+
+ List.of = function(/*...values*/) {
+ return this(arguments);
+ };
+
+ List.prototype.toString = function() {
+ return this.__toString('List [', ']');
+ };
+
+ // @pragma Access
+
+ List.prototype.get = function(index, notSetValue) {
+ index = wrapIndex(this, index);
+ if (index >= 0 && index < this.size) {
+ index += this._origin;
+ var node = listNodeFor(this, index);
+ return node && node.array[index & MASK];
+ }
+ return notSetValue;
+ };
+
+ // @pragma Modification
+
+ List.prototype.set = function(index, value) {
+ return updateList(this, index, value);
+ };
+
+ List.prototype.remove = function(index) {
+ return !this.has(index) ? this :
+ index === 0 ? this.shift() :
+ index === this.size - 1 ? this.pop() :
+ this.splice(index, 1);
+ };
+
+ List.prototype.insert = function(index, value) {
+ return this.splice(index, 0, value);
+ };
+
+ List.prototype.clear = function() {
+ if (this.size === 0) {
+ return this;
+ }
+ if (this.__ownerID) {
+ this.size = this._origin = this._capacity = 0;
+ this._level = SHIFT;
+ this._root = this._tail = null;
+ this.__hash = undefined;
+ this.__altered = true;
+ return this;
+ }
+ return emptyList();
+ };
+
+ List.prototype.push = function(/*...values*/) {
+ var values = arguments;
+ var oldSize = this.size;
+ return this.withMutations(function(list ) {
+ setListBounds(list, 0, oldSize + values.length);
+ for (var ii = 0; ii < values.length; ii++) {
+ list.set(oldSize + ii, values[ii]);
+ }
+ });
+ };
+
+ List.prototype.pop = function() {
+ return setListBounds(this, 0, -1);
+ };
+
+ List.prototype.unshift = function(/*...values*/) {
+ var values = arguments;
+ return this.withMutations(function(list ) {
+ setListBounds(list, -values.length);
+ for (var ii = 0; ii < values.length; ii++) {
+ list.set(ii, values[ii]);
+ }
+ });
+ };
+
+ List.prototype.shift = function() {
+ return setListBounds(this, 1);
+ };
+
+ // @pragma Composition
+
+ List.prototype.merge = function(/*...iters*/) {
+ return mergeIntoListWith(this, undefined, arguments);
+ };
+
+ List.prototype.mergeWith = function(merger) {var iters = SLICE$0.call(arguments, 1);
+ return mergeIntoListWith(this, merger, iters);
+ };
+
+ List.prototype.mergeDeep = function(/*...iters*/) {
+ return mergeIntoListWith(this, deepMerger, arguments);
+ };
+
+ List.prototype.mergeDeepWith = function(merger) {var iters = SLICE$0.call(arguments, 1);
+ return mergeIntoListWith(this, deepMergerWith(merger), iters);
+ };
+
+ List.prototype.setSize = function(size) {
+ return setListBounds(this, 0, size);
+ };
+
+ // @pragma Iteration
+
+ List.prototype.slice = function(begin, end) {
+ var size = this.size;
+ if (wholeSlice(begin, end, size)) {
+ return this;
+ }
+ return setListBounds(
+ this,
+ resolveBegin(begin, size),
+ resolveEnd(end, size)
+ );
+ };
+
+ List.prototype.__iterator = function(type, reverse) {
+ var index = 0;
+ var values = iterateList(this, reverse);
+ return new Iterator(function() {
+ var value = values();
+ return value === DONE ?
+ iteratorDone() :
+ iteratorValue(type, index++, value);
+ });
+ };
+
+ List.prototype.__iterate = function(fn, reverse) {
+ var index = 0;
+ var values = iterateList(this, reverse);
+ var value;
+ while ((value = values()) !== DONE) {
+ if (fn(value, index++, this) === false) {
+ break;
+ }
+ }
+ return index;
+ };
+
+ List.prototype.__ensureOwner = function(ownerID) {
+ if (ownerID === this.__ownerID) {
+ return this;
+ }
+ if (!ownerID) {
+ this.__ownerID = ownerID;
+ return this;
+ }
+ return makeList(this._origin, this._capacity, this._level, this._root, this._tail, ownerID, this.__hash);
+ };
+
+
+ function isList(maybeList) {
+ return !!(maybeList && maybeList[IS_LIST_SENTINEL]);
+ }
+
+ List.isList = isList;
+
+ var IS_LIST_SENTINEL = '@@__IMMUTABLE_LIST__@@';
+
+ var ListPrototype = List.prototype;
+ ListPrototype[IS_LIST_SENTINEL] = true;
+ ListPrototype[DELETE] = ListPrototype.remove;
+ ListPrototype.setIn = MapPrototype.setIn;
+ ListPrototype.deleteIn =
+ ListPrototype.removeIn = MapPrototype.removeIn;
+ ListPrototype.update = MapPrototype.update;
+ ListPrototype.updateIn = MapPrototype.updateIn;
+ ListPrototype.mergeIn = MapPrototype.mergeIn;
+ ListPrototype.mergeDeepIn = MapPrototype.mergeDeepIn;
+ ListPrototype.withMutations = MapPrototype.withMutations;
+ ListPrototype.asMutable = MapPrototype.asMutable;
+ ListPrototype.asImmutable = MapPrototype.asImmutable;
+ ListPrototype.wasAltered = MapPrototype.wasAltered;
+
+
+
+ function VNode(array, ownerID) {
+ this.array = array;
+ this.ownerID = ownerID;
+ }
+
+ // TODO: seems like these methods are very similar
+
+ VNode.prototype.removeBefore = function(ownerID, level, index) {
+ if (index === level ? 1 << level : 0 || this.array.length === 0) {
+ return this;
+ }
+ var originIndex = (index >>> level) & MASK;
+ if (originIndex >= this.array.length) {
+ return new VNode([], ownerID);
+ }
+ var removingFirst = originIndex === 0;
+ var newChild;
+ if (level > 0) {
+ var oldChild = this.array[originIndex];
+ newChild = oldChild && oldChild.removeBefore(ownerID, level - SHIFT, index);
+ if (newChild === oldChild && removingFirst) {
+ return this;
+ }
+ }
+ if (removingFirst && !newChild) {
+ return this;
+ }
+ var editable = editableVNode(this, ownerID);
+ if (!removingFirst) {
+ for (var ii = 0; ii < originIndex; ii++) {
+ editable.array[ii] = undefined;
+ }
+ }
+ if (newChild) {
+ editable.array[originIndex] = newChild;
+ }
+ return editable;
+ };
+
+ VNode.prototype.removeAfter = function(ownerID, level, index) {
+ if (index === (level ? 1 << level : 0) || this.array.length === 0) {
+ return this;
+ }
+ var sizeIndex = ((index - 1) >>> level) & MASK;
+ if (sizeIndex >= this.array.length) {
+ return this;
+ }
+
+ var newChild;
+ if (level > 0) {
+ var oldChild = this.array[sizeIndex];
+ newChild = oldChild && oldChild.removeAfter(ownerID, level - SHIFT, index);
+ if (newChild === oldChild && sizeIndex === this.array.length - 1) {
+ return this;
+ }
+ }
+
+ var editable = editableVNode(this, ownerID);
+ editable.array.splice(sizeIndex + 1);
+ if (newChild) {
+ editable.array[sizeIndex] = newChild;
+ }
+ return editable;
+ };
+
+
+
+ var DONE = {};
+
+ function iterateList(list, reverse) {
+ var left = list._origin;
+ var right = list._capacity;
+ var tailPos = getTailOffset(right);
+ var tail = list._tail;
+
+ return iterateNodeOrLeaf(list._root, list._level, 0);
+
+ function iterateNodeOrLeaf(node, level, offset) {
+ return level === 0 ?
+ iterateLeaf(node, offset) :
+ iterateNode(node, level, offset);
+ }
+
+ function iterateLeaf(node, offset) {
+ var array = offset === tailPos ? tail && tail.array : node && node.array;
+ var from = offset > left ? 0 : left - offset;
+ var to = right - offset;
+ if (to > SIZE) {
+ to = SIZE;
+ }
+ return function() {
+ if (from === to) {
+ return DONE;
+ }
+ var idx = reverse ? --to : from++;
+ return array && array[idx];
+ };
+ }
+
+ function iterateNode(node, level, offset) {
+ var values;
+ var array = node && node.array;
+ var from = offset > left ? 0 : (left - offset) >> level;
+ var to = ((right - offset) >> level) + 1;
+ if (to > SIZE) {
+ to = SIZE;
+ }
+ return function() {
+ do {
+ if (values) {
+ var value = values();
+ if (value !== DONE) {
+ return value;
+ }
+ values = null;
+ }
+ if (from === to) {
+ return DONE;
+ }
+ var idx = reverse ? --to : from++;
+ values = iterateNodeOrLeaf(
+ array && array[idx], level - SHIFT, offset + (idx << level)
+ );
+ } while (true);
+ };
+ }
+ }
+
+ function makeList(origin, capacity, level, root, tail, ownerID, hash) {
+ var list = Object.create(ListPrototype);
+ list.size = capacity - origin;
+ list._origin = origin;
+ list._capacity = capacity;
+ list._level = level;
+ list._root = root;
+ list._tail = tail;
+ list.__ownerID = ownerID;
+ list.__hash = hash;
+ list.__altered = false;
+ return list;
+ }
+
+ var EMPTY_LIST;
+ function emptyList() {
+ return EMPTY_LIST || (EMPTY_LIST = makeList(0, 0, SHIFT));
+ }
+
+ function updateList(list, index, value) {
+ index = wrapIndex(list, index);
+
+ if (index !== index) {
+ return list;
+ }
+
+ if (index >= list.size || index < 0) {
+ return list.withMutations(function(list ) {
+ index < 0 ?
+ setListBounds(list, index).set(0, value) :
+ setListBounds(list, 0, index + 1).set(index, value)
+ });
+ }
+
+ index += list._origin;
+
+ var newTail = list._tail;
+ var newRoot = list._root;
+ var didAlter = MakeRef(DID_ALTER);
+ if (index >= getTailOffset(list._capacity)) {
+ newTail = updateVNode(newTail, list.__ownerID, 0, index, value, didAlter);
+ } else {
+ newRoot = updateVNode(newRoot, list.__ownerID, list._level, index, value, didAlter);
+ }
+
+ if (!didAlter.value) {
+ return list;
+ }
+
+ if (list.__ownerID) {
+ list._root = newRoot;
+ list._tail = newTail;
+ list.__hash = undefined;
+ list.__altered = true;
+ return list;
+ }
+ return makeList(list._origin, list._capacity, list._level, newRoot, newTail);
+ }
+
+ function updateVNode(node, ownerID, level, index, value, didAlter) {
+ var idx = (index >>> level) & MASK;
+ var nodeHas = node && idx < node.array.length;
+ if (!nodeHas && value === undefined) {
+ return node;
+ }
+
+ var newNode;
+
+ if (level > 0) {
+ var lowerNode = node && node.array[idx];
+ var newLowerNode = updateVNode(lowerNode, ownerID, level - SHIFT, index, value, didAlter);
+ if (newLowerNode === lowerNode) {
+ return node;
+ }
+ newNode = editableVNode(node, ownerID);
+ newNode.array[idx] = newLowerNode;
+ return newNode;
+ }
+
+ if (nodeHas && node.array[idx] === value) {
+ return node;
+ }
+
+ SetRef(didAlter);
+
+ newNode = editableVNode(node, ownerID);
+ if (value === undefined && idx === newNode.array.length - 1) {
+ newNode.array.pop();
+ } else {
+ newNode.array[idx] = value;
+ }
+ return newNode;
+ }
+
+ function editableVNode(node, ownerID) {
+ if (ownerID && node && ownerID === node.ownerID) {
+ return node;
+ }
+ return new VNode(node ? node.array.slice() : [], ownerID);
+ }
+
+ function listNodeFor(list, rawIndex) {
+ if (rawIndex >= getTailOffset(list._capacity)) {
+ return list._tail;
+ }
+ if (rawIndex < 1 << (list._level + SHIFT)) {
+ var node = list._root;
+ var level = list._level;
+ while (node && level > 0) {
+ node = node.array[(rawIndex >>> level) & MASK];
+ level -= SHIFT;
+ }
+ return node;
+ }
+ }
+
+ function setListBounds(list, begin, end) {
+ // Sanitize begin & end using this shorthand for ToInt32(argument)
+ // http://www.ecma-international.org/ecma-262/6.0/#sec-toint32
+ if (begin !== undefined) {
+ begin = begin | 0;
+ }
+ if (end !== undefined) {
+ end = end | 0;
+ }
+ var owner = list.__ownerID || new OwnerID();
+ var oldOrigin = list._origin;
+ var oldCapacity = list._capacity;
+ var newOrigin = oldOrigin + begin;
+ var newCapacity = end === undefined ? oldCapacity : end < 0 ? oldCapacity + end : oldOrigin + end;
+ if (newOrigin === oldOrigin && newCapacity === oldCapacity) {
+ return list;
+ }
+
+ // If it's going to end after it starts, it's empty.
+ if (newOrigin >= newCapacity) {
+ return list.clear();
+ }
+
+ var newLevel = list._level;
+ var newRoot = list._root;
+
+ // New origin might need creating a higher root.
+ var offsetShift = 0;
+ while (newOrigin + offsetShift < 0) {
+ newRoot = new VNode(newRoot && newRoot.array.length ? [undefined, newRoot] : [], owner);
+ newLevel += SHIFT;
+ offsetShift += 1 << newLevel;
+ }
+ if (offsetShift) {
+ newOrigin += offsetShift;
+ oldOrigin += offsetShift;
+ newCapacity += offsetShift;
+ oldCapacity += offsetShift;
+ }
+
+ var oldTailOffset = getTailOffset(oldCapacity);
+ var newTailOffset = getTailOffset(newCapacity);
+
+ // New size might need creating a higher root.
+ while (newTailOffset >= 1 << (newLevel + SHIFT)) {
+ newRoot = new VNode(newRoot && newRoot.array.length ? [newRoot] : [], owner);
+ newLevel += SHIFT;
+ }
+
+ // Locate or create the new tail.
+ var oldTail = list._tail;
+ var newTail = newTailOffset < oldTailOffset ?
+ listNodeFor(list, newCapacity - 1) :
+ newTailOffset > oldTailOffset ? new VNode([], owner) : oldTail;
+
+ // Merge Tail into tree.
+ if (oldTail && newTailOffset > oldTailOffset && newOrigin < oldCapacity && oldTail.array.length) {
+ newRoot = editableVNode(newRoot, owner);
+ var node = newRoot;
+ for (var level = newLevel; level > SHIFT; level -= SHIFT) {
+ var idx = (oldTailOffset >>> level) & MASK;
+ node = node.array[idx] = editableVNode(node.array[idx], owner);
+ }
+ node.array[(oldTailOffset >>> SHIFT) & MASK] = oldTail;
+ }
+
+ // If the size has been reduced, there's a chance the tail needs to be trimmed.
+ if (newCapacity < oldCapacity) {
+ newTail = newTail && newTail.removeAfter(owner, 0, newCapacity);
+ }
+
+ // If the new origin is within the tail, then we do not need a root.
+ if (newOrigin >= newTailOffset) {
+ newOrigin -= newTailOffset;
+ newCapacity -= newTailOffset;
+ newLevel = SHIFT;
+ newRoot = null;
+ newTail = newTail && newTail.removeBefore(owner, 0, newOrigin);
+
+ // Otherwise, if the root has been trimmed, garbage collect.
+ } else if (newOrigin > oldOrigin || newTailOffset < oldTailOffset) {
+ offsetShift = 0;
+
+ // Identify the new top root node of the subtree of the old root.
+ while (newRoot) {
+ var beginIndex = (newOrigin >>> newLevel) & MASK;
+ if (beginIndex !== (newTailOffset >>> newLevel) & MASK) {
+ break;
+ }
+ if (beginIndex) {
+ offsetShift += (1 << newLevel) * beginIndex;
+ }
+ newLevel -= SHIFT;
+ newRoot = newRoot.array[beginIndex];
+ }
+
+ // Trim the new sides of the new root.
+ if (newRoot && newOrigin > oldOrigin) {
+ newRoot = newRoot.removeBefore(owner, newLevel, newOrigin - offsetShift);
+ }
+ if (newRoot && newTailOffset < oldTailOffset) {
+ newRoot = newRoot.removeAfter(owner, newLevel, newTailOffset - offsetShift);
+ }
+ if (offsetShift) {
+ newOrigin -= offsetShift;
+ newCapacity -= offsetShift;
+ }
+ }
+
+ if (list.__ownerID) {
+ list.size = newCapacity - newOrigin;
+ list._origin = newOrigin;
+ list._capacity = newCapacity;
+ list._level = newLevel;
+ list._root = newRoot;
+ list._tail = newTail;
+ list.__hash = undefined;
+ list.__altered = true;
+ return list;
+ }
+ return makeList(newOrigin, newCapacity, newLevel, newRoot, newTail);
+ }
+
+ function mergeIntoListWith(list, merger, iterables) {
+ var iters = [];
+ var maxSize = 0;
+ for (var ii = 0; ii < iterables.length; ii++) {
+ var value = iterables[ii];
+ var iter = IndexedIterable(value);
+ if (iter.size > maxSize) {
+ maxSize = iter.size;
+ }
+ if (!isIterable(value)) {
+ iter = iter.map(function(v ) {return fromJS(v)});
+ }
+ iters.push(iter);
+ }
+ if (maxSize > list.size) {
+ list = list.setSize(maxSize);
+ }
+ return mergeIntoCollectionWith(list, merger, iters);
+ }
+
+ function getTailOffset(size) {
+ return size < SIZE ? 0 : (((size - 1) >>> SHIFT) << SHIFT);
+ }
+
+ createClass(OrderedMap, Map);
+
+ // @pragma Construction
+
+ function OrderedMap(value) {
+ return value === null || value === undefined ? emptyOrderedMap() :
+ isOrderedMap(value) ? value :
+ emptyOrderedMap().withMutations(function(map ) {
+ var iter = KeyedIterable(value);
+ assertNotInfinite(iter.size);
+ iter.forEach(function(v, k) {return map.set(k, v)});
+ });
+ }
+
+ OrderedMap.of = function(/*...values*/) {
+ return this(arguments);
+ };
+
+ OrderedMap.prototype.toString = function() {
+ return this.__toString('OrderedMap {', '}');
+ };
+
+ // @pragma Access
+
+ OrderedMap.prototype.get = function(k, notSetValue) {
+ var index = this._map.get(k);
+ return index !== undefined ? this._list.get(index)[1] : notSetValue;
+ };
+
+ // @pragma Modification
+
+ OrderedMap.prototype.clear = function() {
+ if (this.size === 0) {
+ return this;
+ }
+ if (this.__ownerID) {
+ this.size = 0;
+ this._map.clear();
+ this._list.clear();
+ return this;
+ }
+ return emptyOrderedMap();
+ };
+
+ OrderedMap.prototype.set = function(k, v) {
+ return updateOrderedMap(this, k, v);
+ };
+
+ OrderedMap.prototype.remove = function(k) {
+ return updateOrderedMap(this, k, NOT_SET);
+ };
+
+ OrderedMap.prototype.wasAltered = function() {
+ return this._map.wasAltered() || this._list.wasAltered();
+ };
+
+ OrderedMap.prototype.__iterate = function(fn, reverse) {var this$0 = this;
+ return this._list.__iterate(
+ function(entry ) {return entry && fn(entry[1], entry[0], this$0)},
+ reverse
+ );
+ };
+
+ OrderedMap.prototype.__iterator = function(type, reverse) {
+ return this._list.fromEntrySeq().__iterator(type, reverse);
+ };
+
+ OrderedMap.prototype.__ensureOwner = function(ownerID) {
+ if (ownerID === this.__ownerID) {
+ return this;
+ }
+ var newMap = this._map.__ensureOwner(ownerID);
+ var newList = this._list.__ensureOwner(ownerID);
+ if (!ownerID) {
+ this.__ownerID = ownerID;
+ this._map = newMap;
+ this._list = newList;
+ return this;
+ }
+ return makeOrderedMap(newMap, newList, ownerID, this.__hash);
+ };
+
+
+ function isOrderedMap(maybeOrderedMap) {
+ return isMap(maybeOrderedMap) && isOrdered(maybeOrderedMap);
+ }
+
+ OrderedMap.isOrderedMap = isOrderedMap;
+
+ OrderedMap.prototype[IS_ORDERED_SENTINEL] = true;
+ OrderedMap.prototype[DELETE] = OrderedMap.prototype.remove;
+
+
+
+ function makeOrderedMap(map, list, ownerID, hash) {
+ var omap = Object.create(OrderedMap.prototype);
+ omap.size = map ? map.size : 0;
+ omap._map = map;
+ omap._list = list;
+ omap.__ownerID = ownerID;
+ omap.__hash = hash;
+ return omap;
+ }
+
+ var EMPTY_ORDERED_MAP;
+ function emptyOrderedMap() {
+ return EMPTY_ORDERED_MAP || (EMPTY_ORDERED_MAP = makeOrderedMap(emptyMap(), emptyList()));
+ }
+
+ function updateOrderedMap(omap, k, v) {
+ var map = omap._map;
+ var list = omap._list;
+ var i = map.get(k);
+ var has = i !== undefined;
+ var newMap;
+ var newList;
+ if (v === NOT_SET) { // removed
+ if (!has) {
+ return omap;
+ }
+ if (list.size >= SIZE && list.size >= map.size * 2) {
+ newList = list.filter(function(entry, idx) {return entry !== undefined && i !== idx});
+ newMap = newList.toKeyedSeq().map(function(entry ) {return entry[0]}).flip().toMap();
+ if (omap.__ownerID) {
+ newMap.__ownerID = newList.__ownerID = omap.__ownerID;
+ }
+ } else {
+ newMap = map.remove(k);
+ newList = i === list.size - 1 ? list.pop() : list.set(i, undefined);
+ }
+ } else {
+ if (has) {
+ if (v === list.get(i)[1]) {
+ return omap;
+ }
+ newMap = map;
+ newList = list.set(i, [k, v]);
+ } else {
+ newMap = map.set(k, list.size);
+ newList = list.set(list.size, [k, v]);
+ }
+ }
+ if (omap.__ownerID) {
+ omap.size = newMap.size;
+ omap._map = newMap;
+ omap._list = newList;
+ omap.__hash = undefined;
+ return omap;
+ }
+ return makeOrderedMap(newMap, newList);
+ }
+
+ createClass(ToKeyedSequence, KeyedSeq);
+ function ToKeyedSequence(indexed, useKeys) {
+ this._iter = indexed;
+ this._useKeys = useKeys;
+ this.size = indexed.size;
+ }
+
+ ToKeyedSequence.prototype.get = function(key, notSetValue) {
+ return this._iter.get(key, notSetValue);
+ };
+
+ ToKeyedSequence.prototype.has = function(key) {
+ return this._iter.has(key);
+ };
+
+ ToKeyedSequence.prototype.valueSeq = function() {
+ return this._iter.valueSeq();
+ };
+
+ ToKeyedSequence.prototype.reverse = function() {var this$0 = this;
+ var reversedSequence = reverseFactory(this, true);
+ if (!this._useKeys) {
+ reversedSequence.valueSeq = function() {return this$0._iter.toSeq().reverse()};
+ }
+ return reversedSequence;
+ };
+
+ ToKeyedSequence.prototype.map = function(mapper, context) {var this$0 = this;
+ var mappedSequence = mapFactory(this, mapper, context);
+ if (!this._useKeys) {
+ mappedSequence.valueSeq = function() {return this$0._iter.toSeq().map(mapper, context)};
+ }
+ return mappedSequence;
+ };
+
+ ToKeyedSequence.prototype.__iterate = function(fn, reverse) {var this$0 = this;
+ var ii;
+ return this._iter.__iterate(
+ this._useKeys ?
+ function(v, k) {return fn(v, k, this$0)} :
+ ((ii = reverse ? resolveSize(this) : 0),
+ function(v ) {return fn(v, reverse ? --ii : ii++, this$0)}),
+ reverse
+ );
+ };
+
+ ToKeyedSequence.prototype.__iterator = function(type, reverse) {
+ if (this._useKeys) {
+ return this._iter.__iterator(type, reverse);
+ }
+ var iterator = this._iter.__iterator(ITERATE_VALUES, reverse);
+ var ii = reverse ? resolveSize(this) : 0;
+ return new Iterator(function() {
+ var step = iterator.next();
+ return step.done ? step :
+ iteratorValue(type, reverse ? --ii : ii++, step.value, step);
+ });
+ };
+
+ ToKeyedSequence.prototype[IS_ORDERED_SENTINEL] = true;
+
+
+ createClass(ToIndexedSequence, IndexedSeq);
+ function ToIndexedSequence(iter) {
+ this._iter = iter;
+ this.size = iter.size;
+ }
+
+ ToIndexedSequence.prototype.includes = function(value) {
+ return this._iter.includes(value);
+ };
+
+ ToIndexedSequence.prototype.__iterate = function(fn, reverse) {var this$0 = this;
+ var iterations = 0;
+ return this._iter.__iterate(function(v ) {return fn(v, iterations++, this$0)}, reverse);
+ };
+
+ ToIndexedSequence.prototype.__iterator = function(type, reverse) {
+ var iterator = this._iter.__iterator(ITERATE_VALUES, reverse);
+ var iterations = 0;
+ return new Iterator(function() {
+ var step = iterator.next();
+ return step.done ? step :
+ iteratorValue(type, iterations++, step.value, step)
+ });
+ };
+
+
+
+ createClass(ToSetSequence, SetSeq);
+ function ToSetSequence(iter) {
+ this._iter = iter;
+ this.size = iter.size;
+ }
+
+ ToSetSequence.prototype.has = function(key) {
+ return this._iter.includes(key);
+ };
+
+ ToSetSequence.prototype.__iterate = function(fn, reverse) {var this$0 = this;
+ return this._iter.__iterate(function(v ) {return fn(v, v, this$0)}, reverse);
+ };
+
+ ToSetSequence.prototype.__iterator = function(type, reverse) {
+ var iterator = this._iter.__iterator(ITERATE_VALUES, reverse);
+ return new Iterator(function() {
+ var step = iterator.next();
+ return step.done ? step :
+ iteratorValue(type, step.value, step.value, step);
+ });
+ };
+
+
+
+ createClass(FromEntriesSequence, KeyedSeq);
+ function FromEntriesSequence(entries) {
+ this._iter = entries;
+ this.size = entries.size;
+ }
+
+ FromEntriesSequence.prototype.entrySeq = function() {
+ return this._iter.toSeq();
+ };
+
+ FromEntriesSequence.prototype.__iterate = function(fn, reverse) {var this$0 = this;
+ return this._iter.__iterate(function(entry ) {
+ // Check if entry exists first so array access doesn't throw for holes
+ // in the parent iteration.
+ if (entry) {
+ validateEntry(entry);
+ var indexedIterable = isIterable(entry);
+ return fn(
+ indexedIterable ? entry.get(1) : entry[1],
+ indexedIterable ? entry.get(0) : entry[0],
+ this$0
+ );
+ }
+ }, reverse);
+ };
+
+ FromEntriesSequence.prototype.__iterator = function(type, reverse) {
+ var iterator = this._iter.__iterator(ITERATE_VALUES, reverse);
+ return new Iterator(function() {
+ while (true) {
+ var step = iterator.next();
+ if (step.done) {
+ return step;
+ }
+ var entry = step.value;
+ // Check if entry exists first so array access doesn't throw for holes
+ // in the parent iteration.
+ if (entry) {
+ validateEntry(entry);
+ var indexedIterable = isIterable(entry);
+ return iteratorValue(
+ type,
+ indexedIterable ? entry.get(0) : entry[0],
+ indexedIterable ? entry.get(1) : entry[1],
+ step
+ );
+ }
+ }
+ });
+ };
+
+
+ ToIndexedSequence.prototype.cacheResult =
+ ToKeyedSequence.prototype.cacheResult =
+ ToSetSequence.prototype.cacheResult =
+ FromEntriesSequence.prototype.cacheResult =
+ cacheResultThrough;
+
+
+ function flipFactory(iterable) {
+ var flipSequence = makeSequence(iterable);
+ flipSequence._iter = iterable;
+ flipSequence.size = iterable.size;
+ flipSequence.flip = function() {return iterable};
+ flipSequence.reverse = function () {
+ var reversedSequence = iterable.reverse.apply(this); // super.reverse()
+ reversedSequence.flip = function() {return iterable.reverse()};
+ return reversedSequence;
+ };
+ flipSequence.has = function(key ) {return iterable.includes(key)};
+ flipSequence.includes = function(key ) {return iterable.has(key)};
+ flipSequence.cacheResult = cacheResultThrough;
+ flipSequence.__iterateUncached = function (fn, reverse) {var this$0 = this;
+ return iterable.__iterate(function(v, k) {return fn(k, v, this$0) !== false}, reverse);
+ }
+ flipSequence.__iteratorUncached = function(type, reverse) {
+ if (type === ITERATE_ENTRIES) {
+ var iterator = iterable.__iterator(type, reverse);
+ return new Iterator(function() {
+ var step = iterator.next();
+ if (!step.done) {
+ var k = step.value[0];
+ step.value[0] = step.value[1];
+ step.value[1] = k;
+ }
+ return step;
+ });
+ }
+ return iterable.__iterator(
+ type === ITERATE_VALUES ? ITERATE_KEYS : ITERATE_VALUES,
+ reverse
+ );
+ }
+ return flipSequence;
+ }
+
+
+ function mapFactory(iterable, mapper, context) {
+ var mappedSequence = makeSequence(iterable);
+ mappedSequence.size = iterable.size;
+ mappedSequence.has = function(key ) {return iterable.has(key)};
+ mappedSequence.get = function(key, notSetValue) {
+ var v = iterable.get(key, NOT_SET);
+ return v === NOT_SET ?
+ notSetValue :
+ mapper.call(context, v, key, iterable);
+ };
+ mappedSequence.__iterateUncached = function (fn, reverse) {var this$0 = this;
+ return iterable.__iterate(
+ function(v, k, c) {return fn(mapper.call(context, v, k, c), k, this$0) !== false},
+ reverse
+ );
+ }
+ mappedSequence.__iteratorUncached = function (type, reverse) {
+ var iterator = iterable.__iterator(ITERATE_ENTRIES, reverse);
+ return new Iterator(function() {
+ var step = iterator.next();
+ if (step.done) {
+ return step;
+ }
+ var entry = step.value;
+ var key = entry[0];
+ return iteratorValue(
+ type,
+ key,
+ mapper.call(context, entry[1], key, iterable),
+ step
+ );
+ });
+ }
+ return mappedSequence;
+ }
+
+
+ function reverseFactory(iterable, useKeys) {
+ var reversedSequence = makeSequence(iterable);
+ reversedSequence._iter = iterable;
+ reversedSequence.size = iterable.size;
+ reversedSequence.reverse = function() {return iterable};
+ if (iterable.flip) {
+ reversedSequence.flip = function () {
+ var flipSequence = flipFactory(iterable);
+ flipSequence.reverse = function() {return iterable.flip()};
+ return flipSequence;
+ };
+ }
+ reversedSequence.get = function(key, notSetValue)
+ {return iterable.get(useKeys ? key : -1 - key, notSetValue)};
+ reversedSequence.has = function(key )
+ {return iterable.has(useKeys ? key : -1 - key)};
+ reversedSequence.includes = function(value ) {return iterable.includes(value)};
+ reversedSequence.cacheResult = cacheResultThrough;
+ reversedSequence.__iterate = function (fn, reverse) {var this$0 = this;
+ return iterable.__iterate(function(v, k) {return fn(v, k, this$0)}, !reverse);
+ };
+ reversedSequence.__iterator =
+ function(type, reverse) {return iterable.__iterator(type, !reverse)};
+ return reversedSequence;
+ }
+
+
+ function filterFactory(iterable, predicate, context, useKeys) {
+ var filterSequence = makeSequence(iterable);
+ if (useKeys) {
+ filterSequence.has = function(key ) {
+ var v = iterable.get(key, NOT_SET);
+ return v !== NOT_SET && !!predicate.call(context, v, key, iterable);
+ };
+ filterSequence.get = function(key, notSetValue) {
+ var v = iterable.get(key, NOT_SET);
+ return v !== NOT_SET && predicate.call(context, v, key, iterable) ?
+ v : notSetValue;
+ };
+ }
+ filterSequence.__iterateUncached = function (fn, reverse) {var this$0 = this;
+ var iterations = 0;
+ iterable.__iterate(function(v, k, c) {
+ if (predicate.call(context, v, k, c)) {
+ iterations++;
+ return fn(v, useKeys ? k : iterations - 1, this$0);
+ }
+ }, reverse);
+ return iterations;
+ };
+ filterSequence.__iteratorUncached = function (type, reverse) {
+ var iterator = iterable.__iterator(ITERATE_ENTRIES, reverse);
+ var iterations = 0;
+ return new Iterator(function() {
+ while (true) {
+ var step = iterator.next();
+ if (step.done) {
+ return step;
+ }
+ var entry = step.value;
+ var key = entry[0];
+ var value = entry[1];
+ if (predicate.call(context, value, key, iterable)) {
+ return iteratorValue(type, useKeys ? key : iterations++, value, step);
+ }
+ }
+ });
+ }
+ return filterSequence;
+ }
+
+
+ function countByFactory(iterable, grouper, context) {
+ var groups = Map().asMutable();
+ iterable.__iterate(function(v, k) {
+ groups.update(
+ grouper.call(context, v, k, iterable),
+ 0,
+ function(a ) {return a + 1}
+ );
+ });
+ return groups.asImmutable();
+ }
+
+
+ function groupByFactory(iterable, grouper, context) {
+ var isKeyedIter = isKeyed(iterable);
+ var groups = (isOrdered(iterable) ? OrderedMap() : Map()).asMutable();
+ iterable.__iterate(function(v, k) {
+ groups.update(
+ grouper.call(context, v, k, iterable),
+ function(a ) {return (a = a || [], a.push(isKeyedIter ? [k, v] : v), a)}
+ );
+ });
+ var coerce = iterableClass(iterable);
+ return groups.map(function(arr ) {return reify(iterable, coerce(arr))});
+ }
+
+
+ function sliceFactory(iterable, begin, end, useKeys) {
+ var originalSize = iterable.size;
+
+ // Sanitize begin & end using this shorthand for ToInt32(argument)
+ // http://www.ecma-international.org/ecma-262/6.0/#sec-toint32
+ if (begin !== undefined) {
+ begin = begin | 0;
+ }
+ if (end !== undefined) {
+ end = end | 0;
+ }
+
+ if (wholeSlice(begin, end, originalSize)) {
+ return iterable;
+ }
+
+ var resolvedBegin = resolveBegin(begin, originalSize);
+ var resolvedEnd = resolveEnd(end, originalSize);
+
+ // begin or end will be NaN if they were provided as negative numbers and
+ // this iterable's size is unknown. In that case, cache first so there is
+ // a known size and these do not resolve to NaN.
+ if (resolvedBegin !== resolvedBegin || resolvedEnd !== resolvedEnd) {
+ return sliceFactory(iterable.toSeq().cacheResult(), begin, end, useKeys);
+ }
+
+ // Note: resolvedEnd is undefined when the original sequence's length is
+ // unknown and this slice did not supply an end and should contain all
+ // elements after resolvedBegin.
+ // In that case, resolvedSize will be NaN and sliceSize will remain undefined.
+ var resolvedSize = resolvedEnd - resolvedBegin;
+ var sliceSize;
+ if (resolvedSize === resolvedSize) {
+ sliceSize = resolvedSize < 0 ? 0 : resolvedSize;
+ }
+
+ var sliceSeq = makeSequence(iterable);
+
+ // If iterable.size is undefined, the size of the realized sliceSeq is
+ // unknown at this point unless the number of items to slice is 0
+ sliceSeq.size = sliceSize === 0 ? sliceSize : iterable.size && sliceSize || undefined;
+
+ if (!useKeys && isSeq(iterable) && sliceSize >= 0) {
+ sliceSeq.get = function (index, notSetValue) {
+ index = wrapIndex(this, index);
+ return index >= 0 && index < sliceSize ?
+ iterable.get(index + resolvedBegin, notSetValue) :
+ notSetValue;
+ }
+ }
+
+ sliceSeq.__iterateUncached = function(fn, reverse) {var this$0 = this;
+ if (sliceSize === 0) {
+ return 0;
+ }
+ if (reverse) {
+ return this.cacheResult().__iterate(fn, reverse);
+ }
+ var skipped = 0;
+ var isSkipping = true;
+ var iterations = 0;
+ iterable.__iterate(function(v, k) {
+ if (!(isSkipping && (isSkipping = skipped++ < resolvedBegin))) {
+ iterations++;
+ return fn(v, useKeys ? k : iterations - 1, this$0) !== false &&
+ iterations !== sliceSize;
+ }
+ });
+ return iterations;
+ };
+
+ sliceSeq.__iteratorUncached = function(type, reverse) {
+ if (sliceSize !== 0 && reverse) {
+ return this.cacheResult().__iterator(type, reverse);
+ }
+ // Don't bother instantiating parent iterator if taking 0.
+ var iterator = sliceSize !== 0 && iterable.__iterator(type, reverse);
+ var skipped = 0;
+ var iterations = 0;
+ return new Iterator(function() {
+ while (skipped++ < resolvedBegin) {
+ iterator.next();
+ }
+ if (++iterations > sliceSize) {
+ return iteratorDone();
+ }
+ var step = iterator.next();
+ if (useKeys || type === ITERATE_VALUES) {
+ return step;
+ } else if (type === ITERATE_KEYS) {
+ return iteratorValue(type, iterations - 1, undefined, step);
+ } else {
+ return iteratorValue(type, iterations - 1, step.value[1], step);
+ }
+ });
+ }
+
+ return sliceSeq;
+ }
+
+
+ function takeWhileFactory(iterable, predicate, context) {
+ var takeSequence = makeSequence(iterable);
+ takeSequence.__iterateUncached = function(fn, reverse) {var this$0 = this;
+ if (reverse) {
+ return this.cacheResult().__iterate(fn, reverse);
+ }
+ var iterations = 0;
+ iterable.__iterate(function(v, k, c)
+ {return predicate.call(context, v, k, c) && ++iterations && fn(v, k, this$0)}
+ );
+ return iterations;
+ };
+ takeSequence.__iteratorUncached = function(type, reverse) {var this$0 = this;
+ if (reverse) {
+ return this.cacheResult().__iterator(type, reverse);
+ }
+ var iterator = iterable.__iterator(ITERATE_ENTRIES, reverse);
+ var iterating = true;
+ return new Iterator(function() {
+ if (!iterating) {
+ return iteratorDone();
+ }
+ var step = iterator.next();
+ if (step.done) {
+ return step;
+ }
+ var entry = step.value;
+ var k = entry[0];
+ var v = entry[1];
+ if (!predicate.call(context, v, k, this$0)) {
+ iterating = false;
+ return iteratorDone();
+ }
+ return type === ITERATE_ENTRIES ? step :
+ iteratorValue(type, k, v, step);
+ });
+ };
+ return takeSequence;
+ }
+
+
+ function skipWhileFactory(iterable, predicate, context, useKeys) {
+ var skipSequence = makeSequence(iterable);
+ skipSequence.__iterateUncached = function (fn, reverse) {var this$0 = this;
+ if (reverse) {
+ return this.cacheResult().__iterate(fn, reverse);
+ }
+ var isSkipping = true;
+ var iterations = 0;
+ iterable.__iterate(function(v, k, c) {
+ if (!(isSkipping && (isSkipping = predicate.call(context, v, k, c)))) {
+ iterations++;
+ return fn(v, useKeys ? k : iterations - 1, this$0);
+ }
+ });
+ return iterations;
+ };
+ skipSequence.__iteratorUncached = function(type, reverse) {var this$0 = this;
+ if (reverse) {
+ return this.cacheResult().__iterator(type, reverse);
+ }
+ var iterator = iterable.__iterator(ITERATE_ENTRIES, reverse);
+ var skipping = true;
+ var iterations = 0;
+ return new Iterator(function() {
+ var step, k, v;
+ do {
+ step = iterator.next();
+ if (step.done) {
+ if (useKeys || type === ITERATE_VALUES) {
+ return step;
+ } else if (type === ITERATE_KEYS) {
+ return iteratorValue(type, iterations++, undefined, step);
+ } else {
+ return iteratorValue(type, iterations++, step.value[1], step);
+ }
+ }
+ var entry = step.value;
+ k = entry[0];
+ v = entry[1];
+ skipping && (skipping = predicate.call(context, v, k, this$0));
+ } while (skipping);
+ return type === ITERATE_ENTRIES ? step :
+ iteratorValue(type, k, v, step);
+ });
+ };
+ return skipSequence;
+ }
+
+
+ function concatFactory(iterable, values) {
+ var isKeyedIterable = isKeyed(iterable);
+ var iters = [iterable].concat(values).map(function(v ) {
+ if (!isIterable(v)) {
+ v = isKeyedIterable ?
+ keyedSeqFromValue(v) :
+ indexedSeqFromValue(Array.isArray(v) ? v : [v]);
+ } else if (isKeyedIterable) {
+ v = KeyedIterable(v);
+ }
+ return v;
+ }).filter(function(v ) {return v.size !== 0});
+
+ if (iters.length === 0) {
+ return iterable;
+ }
+
+ if (iters.length === 1) {
+ var singleton = iters[0];
+ if (singleton === iterable ||
+ isKeyedIterable && isKeyed(singleton) ||
+ isIndexed(iterable) && isIndexed(singleton)) {
+ return singleton;
+ }
+ }
+
+ var concatSeq = new ArraySeq(iters);
+ if (isKeyedIterable) {
+ concatSeq = concatSeq.toKeyedSeq();
+ } else if (!isIndexed(iterable)) {
+ concatSeq = concatSeq.toSetSeq();
+ }
+ concatSeq = concatSeq.flatten(true);
+ concatSeq.size = iters.reduce(
+ function(sum, seq) {
+ if (sum !== undefined) {
+ var size = seq.size;
+ if (size !== undefined) {
+ return sum + size;
+ }
+ }
+ },
+ 0
+ );
+ return concatSeq;
+ }
+
+
+ function flattenFactory(iterable, depth, useKeys) {
+ var flatSequence = makeSequence(iterable);
+ flatSequence.__iterateUncached = function(fn, reverse) {
+ var iterations = 0;
+ var stopped = false;
+ function flatDeep(iter, currentDepth) {var this$0 = this;
+ iter.__iterate(function(v, k) {
+ if ((!depth || currentDepth < depth) && isIterable(v)) {
+ flatDeep(v, currentDepth + 1);
+ } else if (fn(v, useKeys ? k : iterations++, this$0) === false) {
+ stopped = true;
+ }
+ return !stopped;
+ }, reverse);
+ }
+ flatDeep(iterable, 0);
+ return iterations;
+ }
+ flatSequence.__iteratorUncached = function(type, reverse) {
+ var iterator = iterable.__iterator(type, reverse);
+ var stack = [];
+ var iterations = 0;
+ return new Iterator(function() {
+ while (iterator) {
+ var step = iterator.next();
+ if (step.done !== false) {
+ iterator = stack.pop();
+ continue;
+ }
+ var v = step.value;
+ if (type === ITERATE_ENTRIES) {
+ v = v[1];
+ }
+ if ((!depth || stack.length < depth) && isIterable(v)) {
+ stack.push(iterator);
+ iterator = v.__iterator(type, reverse);
+ } else {
+ return useKeys ? step : iteratorValue(type, iterations++, v, step);
+ }
+ }
+ return iteratorDone();
+ });
+ }
+ return flatSequence;
+ }
+
+
+ function flatMapFactory(iterable, mapper, context) {
+ var coerce = iterableClass(iterable);
+ return iterable.toSeq().map(
+ function(v, k) {return coerce(mapper.call(context, v, k, iterable))}
+ ).flatten(true);
+ }
+
+
+ function interposeFactory(iterable, separator) {
+ var interposedSequence = makeSequence(iterable);
+ interposedSequence.size = iterable.size && iterable.size * 2 -1;
+ interposedSequence.__iterateUncached = function(fn, reverse) {var this$0 = this;
+ var iterations = 0;
+ iterable.__iterate(function(v, k)
+ {return (!iterations || fn(separator, iterations++, this$0) !== false) &&
+ fn(v, iterations++, this$0) !== false},
+ reverse
+ );
+ return iterations;
+ };
+ interposedSequence.__iteratorUncached = function(type, reverse) {
+ var iterator = iterable.__iterator(ITERATE_VALUES, reverse);
+ var iterations = 0;
+ var step;
+ return new Iterator(function() {
+ if (!step || iterations % 2) {
+ step = iterator.next();
+ if (step.done) {
+ return step;
+ }
+ }
+ return iterations % 2 ?
+ iteratorValue(type, iterations++, separator) :
+ iteratorValue(type, iterations++, step.value, step);
+ });
+ };
+ return interposedSequence;
+ }
+
+
+ function sortFactory(iterable, comparator, mapper) {
+ if (!comparator) {
+ comparator = defaultComparator;
+ }
+ var isKeyedIterable = isKeyed(iterable);
+ var index = 0;
+ var entries = iterable.toSeq().map(
+ function(v, k) {return [k, v, index++, mapper ? mapper(v, k, iterable) : v]}
+ ).toArray();
+ entries.sort(function(a, b) {return comparator(a[3], b[3]) || a[2] - b[2]}).forEach(
+ isKeyedIterable ?
+ function(v, i) { entries[i].length = 2; } :
+ function(v, i) { entries[i] = v[1]; }
+ );
+ return isKeyedIterable ? KeyedSeq(entries) :
+ isIndexed(iterable) ? IndexedSeq(entries) :
+ SetSeq(entries);
+ }
+
+
+ function maxFactory(iterable, comparator, mapper) {
+ if (!comparator) {
+ comparator = defaultComparator;
+ }
+ if (mapper) {
+ var entry = iterable.toSeq()
+ .map(function(v, k) {return [v, mapper(v, k, iterable)]})
+ .reduce(function(a, b) {return maxCompare(comparator, a[1], b[1]) ? b : a});
+ return entry && entry[0];
+ } else {
+ return iterable.reduce(function(a, b) {return maxCompare(comparator, a, b) ? b : a});
+ }
+ }
+
+ function maxCompare(comparator, a, b) {
+ var comp = comparator(b, a);
+ // b is considered the new max if the comparator declares them equal, but
+ // they are not equal and b is in fact a nullish value.
+ return (comp === 0 && b !== a && (b === undefined || b === null || b !== b)) || comp > 0;
+ }
+
+
+ function zipWithFactory(keyIter, zipper, iters) {
+ var zipSequence = makeSequence(keyIter);
+ zipSequence.size = new ArraySeq(iters).map(function(i ) {return i.size}).min();
+ // Note: this a generic base implementation of __iterate in terms of
+ // __iterator which may be more generically useful in the future.
+ zipSequence.__iterate = function(fn, reverse) {
+ /* generic:
+ var iterator = this.__iterator(ITERATE_ENTRIES, reverse);
+ var step;
+ var iterations = 0;
+ while (!(step = iterator.next()).done) {
+ iterations++;
+ if (fn(step.value[1], step.value[0], this) === false) {
+ break;
+ }
+ }
+ return iterations;
+ */
+ // indexed:
+ var iterator = this.__iterator(ITERATE_VALUES, reverse);
+ var step;
+ var iterations = 0;
+ while (!(step = iterator.next()).done) {
+ if (fn(step.value, iterations++, this) === false) {
+ break;
+ }
+ }
+ return iterations;
+ };
+ zipSequence.__iteratorUncached = function(type, reverse) {
+ var iterators = iters.map(function(i )
+ {return (i = Iterable(i), getIterator(reverse ? i.reverse() : i))}
+ );
+ var iterations = 0;
+ var isDone = false;
+ return new Iterator(function() {
+ var steps;
+ if (!isDone) {
+ steps = iterators.map(function(i ) {return i.next()});
+ isDone = steps.some(function(s ) {return s.done});
+ }
+ if (isDone) {
+ return iteratorDone();
+ }
+ return iteratorValue(
+ type,
+ iterations++,
+ zipper.apply(null, steps.map(function(s ) {return s.value}))
+ );
+ });
+ };
+ return zipSequence
+ }
+
+
+ // #pragma Helper Functions
+
+ function reify(iter, seq) {
+ return isSeq(iter) ? seq : iter.constructor(seq);
+ }
+
+ function validateEntry(entry) {
+ if (entry !== Object(entry)) {
+ throw new TypeError('Expected [K, V] tuple: ' + entry);
+ }
+ }
+
+ function resolveSize(iter) {
+ assertNotInfinite(iter.size);
+ return ensureSize(iter);
+ }
+
+ function iterableClass(iterable) {
+ return isKeyed(iterable) ? KeyedIterable :
+ isIndexed(iterable) ? IndexedIterable :
+ SetIterable;
+ }
+
+ function makeSequence(iterable) {
+ return Object.create(
+ (
+ isKeyed(iterable) ? KeyedSeq :
+ isIndexed(iterable) ? IndexedSeq :
+ SetSeq
+ ).prototype
+ );
+ }
+
+ function cacheResultThrough() {
+ if (this._iter.cacheResult) {
+ this._iter.cacheResult();
+ this.size = this._iter.size;
+ return this;
+ } else {
+ return Seq.prototype.cacheResult.call(this);
+ }
+ }
+
+ function defaultComparator(a, b) {
+ return a > b ? 1 : a < b ? -1 : 0;
+ }
+
+ function forceIterator(keyPath) {
+ var iter = getIterator(keyPath);
+ if (!iter) {
+ // Array might not be iterable in this environment, so we need a fallback
+ // to our wrapped type.
+ if (!isArrayLike(keyPath)) {
+ throw new TypeError('Expected iterable or array-like: ' + keyPath);
+ }
+ iter = getIterator(Iterable(keyPath));
+ }
+ return iter;
+ }
+
+ createClass(Record, KeyedCollection);
+
+ function Record(defaultValues, name) {
+ var hasInitialized;
+
+ var RecordType = function Record(values) {
+ if (values instanceof RecordType) {
+ return values;
+ }
+ if (!(this instanceof RecordType)) {
+ return new RecordType(values);
+ }
+ if (!hasInitialized) {
+ hasInitialized = true;
+ var keys = Object.keys(defaultValues);
+ setProps(RecordTypePrototype, keys);
+ RecordTypePrototype.size = keys.length;
+ RecordTypePrototype._name = name;
+ RecordTypePrototype._keys = keys;
+ RecordTypePrototype._defaultValues = defaultValues;
+ }
+ this._map = Map(values);
+ };
+
+ var RecordTypePrototype = RecordType.prototype = Object.create(RecordPrototype);
+ RecordTypePrototype.constructor = RecordType;
+
+ return RecordType;
+ }
+
+ Record.prototype.toString = function() {
+ return this.__toString(recordName(this) + ' {', '}');
+ };
+
+ // @pragma Access
+
+ Record.prototype.has = function(k) {
+ return this._defaultValues.hasOwnProperty(k);
+ };
+
+ Record.prototype.get = function(k, notSetValue) {
+ if (!this.has(k)) {
+ return notSetValue;
+ }
+ var defaultVal = this._defaultValues[k];
+ return this._map ? this._map.get(k, defaultVal) : defaultVal;
+ };
+
+ // @pragma Modification
+
+ Record.prototype.clear = function() {
+ if (this.__ownerID) {
+ this._map && this._map.clear();
+ return this;
+ }
+ var RecordType = this.constructor;
+ return RecordType._empty || (RecordType._empty = makeRecord(this, emptyMap()));
+ };
+
+ Record.prototype.set = function(k, v) {
+ if (!this.has(k)) {
+ throw new Error('Cannot set unknown key "' + k + '" on ' + recordName(this));
+ }
+ if (this._map && !this._map.has(k)) {
+ var defaultVal = this._defaultValues[k];
+ if (v === defaultVal) {
+ return this;
+ }
+ }
+ var newMap = this._map && this._map.set(k, v);
+ if (this.__ownerID || newMap === this._map) {
+ return this;
+ }
+ return makeRecord(this, newMap);
+ };
+
+ Record.prototype.remove = function(k) {
+ if (!this.has(k)) {
+ return this;
+ }
+ var newMap = this._map && this._map.remove(k);
+ if (this.__ownerID || newMap === this._map) {
+ return this;
+ }
+ return makeRecord(this, newMap);
+ };
+
+ Record.prototype.wasAltered = function() {
+ return this._map.wasAltered();
+ };
+
+ Record.prototype.__iterator = function(type, reverse) {var this$0 = this;
+ return KeyedIterable(this._defaultValues).map(function(_, k) {return this$0.get(k)}).__iterator(type, reverse);
+ };
+
+ Record.prototype.__iterate = function(fn, reverse) {var this$0 = this;
+ return KeyedIterable(this._defaultValues).map(function(_, k) {return this$0.get(k)}).__iterate(fn, reverse);
+ };
+
+ Record.prototype.__ensureOwner = function(ownerID) {
+ if (ownerID === this.__ownerID) {
+ return this;
+ }
+ var newMap = this._map && this._map.__ensureOwner(ownerID);
+ if (!ownerID) {
+ this.__ownerID = ownerID;
+ this._map = newMap;
+ return this;
+ }
+ return makeRecord(this, newMap, ownerID);
+ };
+
+
+ var RecordPrototype = Record.prototype;
+ RecordPrototype[DELETE] = RecordPrototype.remove;
+ RecordPrototype.deleteIn =
+ RecordPrototype.removeIn = MapPrototype.removeIn;
+ RecordPrototype.merge = MapPrototype.merge;
+ RecordPrototype.mergeWith = MapPrototype.mergeWith;
+ RecordPrototype.mergeIn = MapPrototype.mergeIn;
+ RecordPrototype.mergeDeep = MapPrototype.mergeDeep;
+ RecordPrototype.mergeDeepWith = MapPrototype.mergeDeepWith;
+ RecordPrototype.mergeDeepIn = MapPrototype.mergeDeepIn;
+ RecordPrototype.setIn = MapPrototype.setIn;
+ RecordPrototype.update = MapPrototype.update;
+ RecordPrototype.updateIn = MapPrototype.updateIn;
+ RecordPrototype.withMutations = MapPrototype.withMutations;
+ RecordPrototype.asMutable = MapPrototype.asMutable;
+ RecordPrototype.asImmutable = MapPrototype.asImmutable;
+
+
+ function makeRecord(likeRecord, map, ownerID) {
+ var record = Object.create(Object.getPrototypeOf(likeRecord));
+ record._map = map;
+ record.__ownerID = ownerID;
+ return record;
+ }
+
+ function recordName(record) {
+ return record._name || record.constructor.name || 'Record';
+ }
+
+ function setProps(prototype, names) {
+ try {
+ names.forEach(setProp.bind(undefined, prototype));
+ } catch (error) {
+ // Object.defineProperty failed. Probably IE8.
+ }
+ }
+
+ function setProp(prototype, name) {
+ Object.defineProperty(prototype, name, {
+ get: function() {
+ return this.get(name);
+ },
+ set: function(value) {
+ invariant(this.__ownerID, 'Cannot set on an immutable record.');
+ this.set(name, value);
+ }
+ });
+ }
+
+ createClass(Set, SetCollection);
+
+ // @pragma Construction
+
+ function Set(value) {
+ return value === null || value === undefined ? emptySet() :
+ isSet(value) && !isOrdered(value) ? value :
+ emptySet().withMutations(function(set ) {
+ var iter = SetIterable(value);
+ assertNotInfinite(iter.size);
+ iter.forEach(function(v ) {return set.add(v)});
+ });
+ }
+
+ Set.of = function(/*...values*/) {
+ return this(arguments);
+ };
+
+ Set.fromKeys = function(value) {
+ return this(KeyedIterable(value).keySeq());
+ };
+
+ Set.prototype.toString = function() {
+ return this.__toString('Set {', '}');
+ };
+
+ // @pragma Access
+
+ Set.prototype.has = function(value) {
+ return this._map.has(value);
+ };
+
+ // @pragma Modification
+
+ Set.prototype.add = function(value) {
+ return updateSet(this, this._map.set(value, true));
+ };
+
+ Set.prototype.remove = function(value) {
+ return updateSet(this, this._map.remove(value));
+ };
+
+ Set.prototype.clear = function() {
+ return updateSet(this, this._map.clear());
+ };
+
+ // @pragma Composition
+
+ Set.prototype.union = function() {var iters = SLICE$0.call(arguments, 0);
+ iters = iters.filter(function(x ) {return x.size !== 0});
+ if (iters.length === 0) {
+ return this;
+ }
+ if (this.size === 0 && !this.__ownerID && iters.length === 1) {
+ return this.constructor(iters[0]);
+ }
+ return this.withMutations(function(set ) {
+ for (var ii = 0; ii < iters.length; ii++) {
+ SetIterable(iters[ii]).forEach(function(value ) {return set.add(value)});
+ }
+ });
+ };
+
+ Set.prototype.intersect = function() {var iters = SLICE$0.call(arguments, 0);
+ if (iters.length === 0) {
+ return this;
+ }
+ iters = iters.map(function(iter ) {return SetIterable(iter)});
+ var originalSet = this;
+ return this.withMutations(function(set ) {
+ originalSet.forEach(function(value ) {
+ if (!iters.every(function(iter ) {return iter.includes(value)})) {
+ set.remove(value);
+ }
+ });
+ });
+ };
+
+ Set.prototype.subtract = function() {var iters = SLICE$0.call(arguments, 0);
+ if (iters.length === 0) {
+ return this;
+ }
+ iters = iters.map(function(iter ) {return SetIterable(iter)});
+ var originalSet = this;
+ return this.withMutations(function(set ) {
+ originalSet.forEach(function(value ) {
+ if (iters.some(function(iter ) {return iter.includes(value)})) {
+ set.remove(value);
+ }
+ });
+ });
+ };
+
+ Set.prototype.merge = function() {
+ return this.union.apply(this, arguments);
+ };
+
+ Set.prototype.mergeWith = function(merger) {var iters = SLICE$0.call(arguments, 1);
+ return this.union.apply(this, iters);
+ };
+
+ Set.prototype.sort = function(comparator) {
+ // Late binding
+ return OrderedSet(sortFactory(this, comparator));
+ };
+
+ Set.prototype.sortBy = function(mapper, comparator) {
+ // Late binding
+ return OrderedSet(sortFactory(this, comparator, mapper));
+ };
+
+ Set.prototype.wasAltered = function() {
+ return this._map.wasAltered();
+ };
+
+ Set.prototype.__iterate = function(fn, reverse) {var this$0 = this;
+ return this._map.__iterate(function(_, k) {return fn(k, k, this$0)}, reverse);
+ };
+
+ Set.prototype.__iterator = function(type, reverse) {
+ return this._map.map(function(_, k) {return k}).__iterator(type, reverse);
+ };
+
+ Set.prototype.__ensureOwner = function(ownerID) {
+ if (ownerID === this.__ownerID) {
+ return this;
+ }
+ var newMap = this._map.__ensureOwner(ownerID);
+ if (!ownerID) {
+ this.__ownerID = ownerID;
+ this._map = newMap;
+ return this;
+ }
+ return this.__make(newMap, ownerID);
+ };
+
+
+ function isSet(maybeSet) {
+ return !!(maybeSet && maybeSet[IS_SET_SENTINEL]);
+ }
+
+ Set.isSet = isSet;
+
+ var IS_SET_SENTINEL = '@@__IMMUTABLE_SET__@@';
+
+ var SetPrototype = Set.prototype;
+ SetPrototype[IS_SET_SENTINEL] = true;
+ SetPrototype[DELETE] = SetPrototype.remove;
+ SetPrototype.mergeDeep = SetPrototype.merge;
+ SetPrototype.mergeDeepWith = SetPrototype.mergeWith;
+ SetPrototype.withMutations = MapPrototype.withMutations;
+ SetPrototype.asMutable = MapPrototype.asMutable;
+ SetPrototype.asImmutable = MapPrototype.asImmutable;
+
+ SetPrototype.__empty = emptySet;
+ SetPrototype.__make = makeSet;
+
+ function updateSet(set, newMap) {
+ if (set.__ownerID) {
+ set.size = newMap.size;
+ set._map = newMap;
+ return set;
+ }
+ return newMap === set._map ? set :
+ newMap.size === 0 ? set.__empty() :
+ set.__make(newMap);
+ }
+
+ function makeSet(map, ownerID) {
+ var set = Object.create(SetPrototype);
+ set.size = map ? map.size : 0;
+ set._map = map;
+ set.__ownerID = ownerID;
+ return set;
+ }
+
+ var EMPTY_SET;
+ function emptySet() {
+ return EMPTY_SET || (EMPTY_SET = makeSet(emptyMap()));
+ }
+
+ createClass(OrderedSet, Set);
+
+ // @pragma Construction
+
+ function OrderedSet(value) {
+ return value === null || value === undefined ? emptyOrderedSet() :
+ isOrderedSet(value) ? value :
+ emptyOrderedSet().withMutations(function(set ) {
+ var iter = SetIterable(value);
+ assertNotInfinite(iter.size);
+ iter.forEach(function(v ) {return set.add(v)});
+ });
+ }
+
+ OrderedSet.of = function(/*...values*/) {
+ return this(arguments);
+ };
+
+ OrderedSet.fromKeys = function(value) {
+ return this(KeyedIterable(value).keySeq());
+ };
+
+ OrderedSet.prototype.toString = function() {
+ return this.__toString('OrderedSet {', '}');
+ };
+
+
+ function isOrderedSet(maybeOrderedSet) {
+ return isSet(maybeOrderedSet) && isOrdered(maybeOrderedSet);
+ }
+
+ OrderedSet.isOrderedSet = isOrderedSet;
+
+ var OrderedSetPrototype = OrderedSet.prototype;
+ OrderedSetPrototype[IS_ORDERED_SENTINEL] = true;
+
+ OrderedSetPrototype.__empty = emptyOrderedSet;
+ OrderedSetPrototype.__make = makeOrderedSet;
+
+ function makeOrderedSet(map, ownerID) {
+ var set = Object.create(OrderedSetPrototype);
+ set.size = map ? map.size : 0;
+ set._map = map;
+ set.__ownerID = ownerID;
+ return set;
+ }
+
+ var EMPTY_ORDERED_SET;
+ function emptyOrderedSet() {
+ return EMPTY_ORDERED_SET || (EMPTY_ORDERED_SET = makeOrderedSet(emptyOrderedMap()));
+ }
+
+ createClass(Stack, IndexedCollection);
+
+ // @pragma Construction
+
+ function Stack(value) {
+ return value === null || value === undefined ? emptyStack() :
+ isStack(value) ? value :
+ emptyStack().unshiftAll(value);
+ }
+
+ Stack.of = function(/*...values*/) {
+ return this(arguments);
+ };
+
+ Stack.prototype.toString = function() {
+ return this.__toString('Stack [', ']');
+ };
+
+ // @pragma Access
+
+ Stack.prototype.get = function(index, notSetValue) {
+ var head = this._head;
+ index = wrapIndex(this, index);
+ while (head && index--) {
+ head = head.next;
+ }
+ return head ? head.value : notSetValue;
+ };
+
+ Stack.prototype.peek = function() {
+ return this._head && this._head.value;
+ };
+
+ // @pragma Modification
+
+ Stack.prototype.push = function(/*...values*/) {
+ if (arguments.length === 0) {
+ return this;
+ }
+ var newSize = this.size + arguments.length;
+ var head = this._head;
+ for (var ii = arguments.length - 1; ii >= 0; ii--) {
+ head = {
+ value: arguments[ii],
+ next: head
+ };
+ }
+ if (this.__ownerID) {
+ this.size = newSize;
+ this._head = head;
+ this.__hash = undefined;
+ this.__altered = true;
+ return this;
+ }
+ return makeStack(newSize, head);
+ };
+
+ Stack.prototype.pushAll = function(iter) {
+ iter = IndexedIterable(iter);
+ if (iter.size === 0) {
+ return this;
+ }
+ assertNotInfinite(iter.size);
+ var newSize = this.size;
+ var head = this._head;
+ iter.reverse().forEach(function(value ) {
+ newSize++;
+ head = {
+ value: value,
+ next: head
+ };
+ });
+ if (this.__ownerID) {
+ this.size = newSize;
+ this._head = head;
+ this.__hash = undefined;
+ this.__altered = true;
+ return this;
+ }
+ return makeStack(newSize, head);
+ };
+
+ Stack.prototype.pop = function() {
+ return this.slice(1);
+ };
+
+ Stack.prototype.unshift = function(/*...values*/) {
+ return this.push.apply(this, arguments);
+ };
+
+ Stack.prototype.unshiftAll = function(iter) {
+ return this.pushAll(iter);
+ };
+
+ Stack.prototype.shift = function() {
+ return this.pop.apply(this, arguments);
+ };
+
+ Stack.prototype.clear = function() {
+ if (this.size === 0) {
+ return this;
+ }
+ if (this.__ownerID) {
+ this.size = 0;
+ this._head = undefined;
+ this.__hash = undefined;
+ this.__altered = true;
+ return this;
+ }
+ return emptyStack();
+ };
+
+ Stack.prototype.slice = function(begin, end) {
+ if (wholeSlice(begin, end, this.size)) {
+ return this;
+ }
+ var resolvedBegin = resolveBegin(begin, this.size);
+ var resolvedEnd = resolveEnd(end, this.size);
+ if (resolvedEnd !== this.size) {
+ // super.slice(begin, end);
+ return IndexedCollection.prototype.slice.call(this, begin, end);
+ }
+ var newSize = this.size - resolvedBegin;
+ var head = this._head;
+ while (resolvedBegin--) {
+ head = head.next;
+ }
+ if (this.__ownerID) {
+ this.size = newSize;
+ this._head = head;
+ this.__hash = undefined;
+ this.__altered = true;
+ return this;
+ }
+ return makeStack(newSize, head);
+ };
+
+ // @pragma Mutability
+
+ Stack.prototype.__ensureOwner = function(ownerID) {
+ if (ownerID === this.__ownerID) {
+ return this;
+ }
+ if (!ownerID) {
+ this.__ownerID = ownerID;
+ this.__altered = false;
+ return this;
+ }
+ return makeStack(this.size, this._head, ownerID, this.__hash);
+ };
+
+ // @pragma Iteration
+
+ Stack.prototype.__iterate = function(fn, reverse) {
+ if (reverse) {
+ return this.reverse().__iterate(fn);
+ }
+ var iterations = 0;
+ var node = this._head;
+ while (node) {
+ if (fn(node.value, iterations++, this) === false) {
+ break;
+ }
+ node = node.next;
+ }
+ return iterations;
+ };
+
+ Stack.prototype.__iterator = function(type, reverse) {
+ if (reverse) {
+ return this.reverse().__iterator(type);
+ }
+ var iterations = 0;
+ var node = this._head;
+ return new Iterator(function() {
+ if (node) {
+ var value = node.value;
+ node = node.next;
+ return iteratorValue(type, iterations++, value);
+ }
+ return iteratorDone();
+ });
+ };
+
+
+ function isStack(maybeStack) {
+ return !!(maybeStack && maybeStack[IS_STACK_SENTINEL]);
+ }
+
+ Stack.isStack = isStack;
+
+ var IS_STACK_SENTINEL = '@@__IMMUTABLE_STACK__@@';
+
+ var StackPrototype = Stack.prototype;
+ StackPrototype[IS_STACK_SENTINEL] = true;
+ StackPrototype.withMutations = MapPrototype.withMutations;
+ StackPrototype.asMutable = MapPrototype.asMutable;
+ StackPrototype.asImmutable = MapPrototype.asImmutable;
+ StackPrototype.wasAltered = MapPrototype.wasAltered;
+
+
+ function makeStack(size, head, ownerID, hash) {
+ var map = Object.create(StackPrototype);
+ map.size = size;
+ map._head = head;
+ map.__ownerID = ownerID;
+ map.__hash = hash;
+ map.__altered = false;
+ return map;
+ }
+
+ var EMPTY_STACK;
+ function emptyStack() {
+ return EMPTY_STACK || (EMPTY_STACK = makeStack(0));
+ }
+
+ /**
+ * Contributes additional methods to a constructor
+ */
+ function mixin(ctor, methods) {
+ var keyCopier = function(key ) { ctor.prototype[key] = methods[key]; };
+ Object.keys(methods).forEach(keyCopier);
+ Object.getOwnPropertySymbols &&
+ Object.getOwnPropertySymbols(methods).forEach(keyCopier);
+ return ctor;
+ }
+
+ Iterable.Iterator = Iterator;
+
+ mixin(Iterable, {
+
+ // ### Conversion to other types
+
+ toArray: function() {
+ assertNotInfinite(this.size);
+ var array = new Array(this.size || 0);
+ this.valueSeq().__iterate(function(v, i) { array[i] = v; });
+ return array;
+ },
+
+ toIndexedSeq: function() {
+ return new ToIndexedSequence(this);
+ },
+
+ toJS: function() {
+ return this.toSeq().map(
+ function(value ) {return value && typeof value.toJS === 'function' ? value.toJS() : value}
+ ).__toJS();
+ },
+
+ toJSON: function() {
+ return this.toSeq().map(
+ function(value ) {return value && typeof value.toJSON === 'function' ? value.toJSON() : value}
+ ).__toJS();
+ },
+
+ toKeyedSeq: function() {
+ return new ToKeyedSequence(this, true);
+ },
+
+ toMap: function() {
+ // Use Late Binding here to solve the circular dependency.
+ return Map(this.toKeyedSeq());
+ },
+
+ toObject: function() {
+ assertNotInfinite(this.size);
+ var object = {};
+ this.__iterate(function(v, k) { object[k] = v; });
+ return object;
+ },
+
+ toOrderedMap: function() {
+ // Use Late Binding here to solve the circular dependency.
+ return OrderedMap(this.toKeyedSeq());
+ },
+
+ toOrderedSet: function() {
+ // Use Late Binding here to solve the circular dependency.
+ return OrderedSet(isKeyed(this) ? this.valueSeq() : this);
+ },
+
+ toSet: function() {
+ // Use Late Binding here to solve the circular dependency.
+ return Set(isKeyed(this) ? this.valueSeq() : this);
+ },
+
+ toSetSeq: function() {
+ return new ToSetSequence(this);
+ },
+
+ toSeq: function() {
+ return isIndexed(this) ? this.toIndexedSeq() :
+ isKeyed(this) ? this.toKeyedSeq() :
+ this.toSetSeq();
+ },
+
+ toStack: function() {
+ // Use Late Binding here to solve the circular dependency.
+ return Stack(isKeyed(this) ? this.valueSeq() : this);
+ },
+
+ toList: function() {
+ // Use Late Binding here to solve the circular dependency.
+ return List(isKeyed(this) ? this.valueSeq() : this);
+ },
+
+
+ // ### Common JavaScript methods and properties
+
+ toString: function() {
+ return '[Iterable]';
+ },
+
+ __toString: function(head, tail) {
+ if (this.size === 0) {
+ return head + tail;
+ }
+ return head + ' ' + this.toSeq().map(this.__toStringMapper).join(', ') + ' ' + tail;
+ },
+
+
+ // ### ES6 Collection methods (ES6 Array and Map)
+
+ concat: function() {var values = SLICE$0.call(arguments, 0);
+ return reify(this, concatFactory(this, values));
+ },
+
+ includes: function(searchValue) {
+ return this.some(function(value ) {return is(value, searchValue)});
+ },
+
+ entries: function() {
+ return this.__iterator(ITERATE_ENTRIES);
+ },
+
+ every: function(predicate, context) {
+ assertNotInfinite(this.size);
+ var returnValue = true;
+ this.__iterate(function(v, k, c) {
+ if (!predicate.call(context, v, k, c)) {
+ returnValue = false;
+ return false;
+ }
+ });
+ return returnValue;
+ },
+
+ filter: function(predicate, context) {
+ return reify(this, filterFactory(this, predicate, context, true));
+ },
+
+ find: function(predicate, context, notSetValue) {
+ var entry = this.findEntry(predicate, context);
+ return entry ? entry[1] : notSetValue;
+ },
+
+ findEntry: function(predicate, context) {
+ var found;
+ this.__iterate(function(v, k, c) {
+ if (predicate.call(context, v, k, c)) {
+ found = [k, v];
+ return false;
+ }
+ });
+ return found;
+ },
+
+ findLastEntry: function(predicate, context) {
+ return this.toSeq().reverse().findEntry(predicate, context);
+ },
+
+ forEach: function(sideEffect, context) {
+ assertNotInfinite(this.size);
+ return this.__iterate(context ? sideEffect.bind(context) : sideEffect);
+ },
+
+ join: function(separator) {
+ assertNotInfinite(this.size);
+ separator = separator !== undefined ? '' + separator : ',';
+ var joined = '';
+ var isFirst = true;
+ this.__iterate(function(v ) {
+ isFirst ? (isFirst = false) : (joined += separator);
+ joined += v !== null && v !== undefined ? v.toString() : '';
+ });
+ return joined;
+ },
+
+ keys: function() {
+ return this.__iterator(ITERATE_KEYS);
+ },
+
+ map: function(mapper, context) {
+ return reify(this, mapFactory(this, mapper, context));
+ },
+
+ reduce: function(reducer, initialReduction, context) {
+ assertNotInfinite(this.size);
+ var reduction;
+ var useFirst;
+ if (arguments.length < 2) {
+ useFirst = true;
+ } else {
+ reduction = initialReduction;
+ }
+ this.__iterate(function(v, k, c) {
+ if (useFirst) {
+ useFirst = false;
+ reduction = v;
+ } else {
+ reduction = reducer.call(context, reduction, v, k, c);
+ }
+ });
+ return reduction;
+ },
+
+ reduceRight: function(reducer, initialReduction, context) {
+ var reversed = this.toKeyedSeq().reverse();
+ return reversed.reduce.apply(reversed, arguments);
+ },
+
+ reverse: function() {
+ return reify(this, reverseFactory(this, true));
+ },
+
+ slice: function(begin, end) {
+ return reify(this, sliceFactory(this, begin, end, true));
+ },
+
+ some: function(predicate, context) {
+ return !this.every(not(predicate), context);
+ },
+
+ sort: function(comparator) {
+ return reify(this, sortFactory(this, comparator));
+ },
+
+ values: function() {
+ return this.__iterator(ITERATE_VALUES);
+ },
+
+
+ // ### More sequential methods
+
+ butLast: function() {
+ return this.slice(0, -1);
+ },
+
+ isEmpty: function() {
+ return this.size !== undefined ? this.size === 0 : !this.some(function() {return true});
+ },
+
+ count: function(predicate, context) {
+ return ensureSize(
+ predicate ? this.toSeq().filter(predicate, context) : this
+ );
+ },
+
+ countBy: function(grouper, context) {
+ return countByFactory(this, grouper, context);
+ },
+
+ equals: function(other) {
+ return deepEqual(this, other);
+ },
+
+ entrySeq: function() {
+ var iterable = this;
+ if (iterable._cache) {
+ // We cache as an entries array, so we can just return the cache!
+ return new ArraySeq(iterable._cache);
+ }
+ var entriesSequence = iterable.toSeq().map(entryMapper).toIndexedSeq();
+ entriesSequence.fromEntrySeq = function() {return iterable.toSeq()};
+ return entriesSequence;
+ },
+
+ filterNot: function(predicate, context) {
+ return this.filter(not(predicate), context);
+ },
+
+ findLast: function(predicate, context, notSetValue) {
+ return this.toKeyedSeq().reverse().find(predicate, context, notSetValue);
+ },
+
+ first: function() {
+ return this.find(returnTrue);
+ },
+
+ flatMap: function(mapper, context) {
+ return reify(this, flatMapFactory(this, mapper, context));
+ },
+
+ flatten: function(depth) {
+ return reify(this, flattenFactory(this, depth, true));
+ },
+
+ fromEntrySeq: function() {
+ return new FromEntriesSequence(this);
+ },
+
+ get: function(searchKey, notSetValue) {
+ return this.find(function(_, key) {return is(key, searchKey)}, undefined, notSetValue);
+ },
+
+ getIn: function(searchKeyPath, notSetValue) {
+ var nested = this;
+ // Note: in an ES6 environment, we would prefer:
+ // for (var key of searchKeyPath) {
+ var iter = forceIterator(searchKeyPath);
+ var step;
+ while (!(step = iter.next()).done) {
+ var key = step.value;
+ nested = nested && nested.get ? nested.get(key, NOT_SET) : NOT_SET;
+ if (nested === NOT_SET) {
+ return notSetValue;
+ }
+ }
+ return nested;
+ },
+
+ groupBy: function(grouper, context) {
+ return groupByFactory(this, grouper, context);
+ },
+
+ has: function(searchKey) {
+ return this.get(searchKey, NOT_SET) !== NOT_SET;
+ },
+
+ hasIn: function(searchKeyPath) {
+ return this.getIn(searchKeyPath, NOT_SET) !== NOT_SET;
+ },
+
+ isSubset: function(iter) {
+ iter = typeof iter.includes === 'function' ? iter : Iterable(iter);
+ return this.every(function(value ) {return iter.includes(value)});
+ },
+
+ isSuperset: function(iter) {
+ iter = typeof iter.isSubset === 'function' ? iter : Iterable(iter);
+ return iter.isSubset(this);
+ },
+
+ keySeq: function() {
+ return this.toSeq().map(keyMapper).toIndexedSeq();
+ },
+
+ last: function() {
+ return this.toSeq().reverse().first();
+ },
+
+ max: function(comparator) {
+ return maxFactory(this, comparator);
+ },
+
+ maxBy: function(mapper, comparator) {
+ return maxFactory(this, comparator, mapper);
+ },
+
+ min: function(comparator) {
+ return maxFactory(this, comparator ? neg(comparator) : defaultNegComparator);
+ },
+
+ minBy: function(mapper, comparator) {
+ return maxFactory(this, comparator ? neg(comparator) : defaultNegComparator, mapper);
+ },
+
+ rest: function() {
+ return this.slice(1);
+ },
+
+ skip: function(amount) {
+ return this.slice(Math.max(0, amount));
+ },
+
+ skipLast: function(amount) {
+ return reify(this, this.toSeq().reverse().skip(amount).reverse());
+ },
+
+ skipWhile: function(predicate, context) {
+ return reify(this, skipWhileFactory(this, predicate, context, true));
+ },
+
+ skipUntil: function(predicate, context) {
+ return this.skipWhile(not(predicate), context);
+ },
+
+ sortBy: function(mapper, comparator) {
+ return reify(this, sortFactory(this, comparator, mapper));
+ },
+
+ take: function(amount) {
+ return this.slice(0, Math.max(0, amount));
+ },
+
+ takeLast: function(amount) {
+ return reify(this, this.toSeq().reverse().take(amount).reverse());
+ },
+
+ takeWhile: function(predicate, context) {
+ return reify(this, takeWhileFactory(this, predicate, context));
+ },
+
+ takeUntil: function(predicate, context) {
+ return this.takeWhile(not(predicate), context);
+ },
+
+ valueSeq: function() {
+ return this.toIndexedSeq();
+ },
+
+
+ // ### Hashable Object
+
+ hashCode: function() {
+ return this.__hash || (this.__hash = hashIterable(this));
+ }
+
+
+ // ### Internal
+
+ // abstract __iterate(fn, reverse)
+
+ // abstract __iterator(type, reverse)
+ });
+
+ // var IS_ITERABLE_SENTINEL = '@@__IMMUTABLE_ITERABLE__@@';
+ // var IS_KEYED_SENTINEL = '@@__IMMUTABLE_KEYED__@@';
+ // var IS_INDEXED_SENTINEL = '@@__IMMUTABLE_INDEXED__@@';
+ // var IS_ORDERED_SENTINEL = '@@__IMMUTABLE_ORDERED__@@';
+
+ var IterablePrototype = Iterable.prototype;
+ IterablePrototype[IS_ITERABLE_SENTINEL] = true;
+ IterablePrototype[ITERATOR_SYMBOL] = IterablePrototype.values;
+ IterablePrototype.__toJS = IterablePrototype.toArray;
+ IterablePrototype.__toStringMapper = quoteString;
+ IterablePrototype.inspect =
+ IterablePrototype.toSource = function() { return this.toString(); };
+ IterablePrototype.chain = IterablePrototype.flatMap;
+ IterablePrototype.contains = IterablePrototype.includes;
+
+ // Temporary warning about using length
+ (function () {
+ try {
+ Object.defineProperty(IterablePrototype, 'length', {
+ get: function () {
+ if (!Iterable.noLengthWarning) {
+ var stack;
+ try {
+ throw new Error();
+ } catch (error) {
+ stack = error.stack;
+ }
+ if (stack.indexOf('_wrapObject') === -1) {
+ console && console.warn && console.warn(
+ 'iterable.length has been deprecated, '+
+ 'use iterable.size or iterable.count(). '+
+ 'This warning will become a silent error in a future version. ' +
+ stack
+ );
+ return this.size;
+ }
+ }
+ }
+ });
+ } catch (e) {}
+ })();
+
+
+
+ mixin(KeyedIterable, {
+
+ // ### More sequential methods
+
+ flip: function() {
+ return reify(this, flipFactory(this));
+ },
+
+ findKey: function(predicate, context) {
+ var entry = this.findEntry(predicate, context);
+ return entry && entry[0];
+ },
+
+ findLastKey: function(predicate, context) {
+ return this.toSeq().reverse().findKey(predicate, context);
+ },
+
+ keyOf: function(searchValue) {
+ return this.findKey(function(value ) {return is(value, searchValue)});
+ },
+
+ lastKeyOf: function(searchValue) {
+ return this.findLastKey(function(value ) {return is(value, searchValue)});
+ },
+
+ mapEntries: function(mapper, context) {var this$0 = this;
+ var iterations = 0;
+ return reify(this,
+ this.toSeq().map(
+ function(v, k) {return mapper.call(context, [k, v], iterations++, this$0)}
+ ).fromEntrySeq()
+ );
+ },
+
+ mapKeys: function(mapper, context) {var this$0 = this;
+ return reify(this,
+ this.toSeq().flip().map(
+ function(k, v) {return mapper.call(context, k, v, this$0)}
+ ).flip()
+ );
+ }
+
+ });
+
+ var KeyedIterablePrototype = KeyedIterable.prototype;
+ KeyedIterablePrototype[IS_KEYED_SENTINEL] = true;
+ KeyedIterablePrototype[ITERATOR_SYMBOL] = IterablePrototype.entries;
+ KeyedIterablePrototype.__toJS = IterablePrototype.toObject;
+ KeyedIterablePrototype.__toStringMapper = function(v, k) {return JSON.stringify(k) + ': ' + quoteString(v)};
+
+
+
+ mixin(IndexedIterable, {
+
+ // ### Conversion to other types
+
+ toKeyedSeq: function() {
+ return new ToKeyedSequence(this, false);
+ },
+
+
+ // ### ES6 Collection methods (ES6 Array and Map)
+
+ filter: function(predicate, context) {
+ return reify(this, filterFactory(this, predicate, context, false));
+ },
+
+ findIndex: function(predicate, context) {
+ var entry = this.findEntry(predicate, context);
+ return entry ? entry[0] : -1;
+ },
+
+ indexOf: function(searchValue) {
+ var key = this.toKeyedSeq().keyOf(searchValue);
+ return key === undefined ? -1 : key;
+ },
+
+ lastIndexOf: function(searchValue) {
+ var key = this.toKeyedSeq().reverse().keyOf(searchValue);
+ return key === undefined ? -1 : key;
+ },
+
+ reverse: function() {
+ return reify(this, reverseFactory(this, false));
+ },
+
+ slice: function(begin, end) {
+ return reify(this, sliceFactory(this, begin, end, false));
+ },
+
+ splice: function(index, removeNum /*, ...values*/) {
+ var numArgs = arguments.length;
+ removeNum = Math.max(removeNum | 0, 0);
+ if (numArgs === 0 || (numArgs === 2 && !removeNum)) {
+ return this;
+ }
+ // If index is negative, it should resolve relative to the size of the
+ // collection. However size may be expensive to compute if not cached, so
+ // only call count() if the number is in fact negative.
+ index = resolveBegin(index, index < 0 ? this.count() : this.size);
+ var spliced = this.slice(0, index);
+ return reify(
+ this,
+ numArgs === 1 ?
+ spliced :
+ spliced.concat(arrCopy(arguments, 2), this.slice(index + removeNum))
+ );
+ },
+
+
+ // ### More collection methods
+
+ findLastIndex: function(predicate, context) {
+ var key = this.toKeyedSeq().findLastKey(predicate, context);
+ return key === undefined ? -1 : key;
+ },
+
+ first: function() {
+ return this.get(0);
+ },
+
+ flatten: function(depth) {
+ return reify(this, flattenFactory(this, depth, false));
+ },
+
+ get: function(index, notSetValue) {
+ index = wrapIndex(this, index);
+ return (index < 0 || (this.size === Infinity ||
+ (this.size !== undefined && index > this.size))) ?
+ notSetValue :
+ this.find(function(_, key) {return key === index}, undefined, notSetValue);
+ },
+
+ has: function(index) {
+ index = wrapIndex(this, index);
+ return index >= 0 && (this.size !== undefined ?
+ this.size === Infinity || index < this.size :
+ this.indexOf(index) !== -1
+ );
+ },
+
+ interpose: function(separator) {
+ return reify(this, interposeFactory(this, separator));
+ },
+
+ interleave: function(/*...iterables*/) {
+ var iterables = [this].concat(arrCopy(arguments));
+ var zipped = zipWithFactory(this.toSeq(), IndexedSeq.of, iterables);
+ var interleaved = zipped.flatten(true);
+ if (zipped.size) {
+ interleaved.size = zipped.size * iterables.length;
+ }
+ return reify(this, interleaved);
+ },
+
+ last: function() {
+ return this.get(-1);
+ },
+
+ skipWhile: function(predicate, context) {
+ return reify(this, skipWhileFactory(this, predicate, context, false));
+ },
+
+ zip: function(/*, ...iterables */) {
+ var iterables = [this].concat(arrCopy(arguments));
+ return reify(this, zipWithFactory(this, defaultZipper, iterables));
+ },
+
+ zipWith: function(zipper/*, ...iterables */) {
+ var iterables = arrCopy(arguments);
+ iterables[0] = this;
+ return reify(this, zipWithFactory(this, zipper, iterables));
+ }
+
+ });
+
+ IndexedIterable.prototype[IS_INDEXED_SENTINEL] = true;
+ IndexedIterable.prototype[IS_ORDERED_SENTINEL] = true;
+
+
+
+ mixin(SetIterable, {
+
+ // ### ES6 Collection methods (ES6 Array and Map)
+
+ get: function(value, notSetValue) {
+ return this.has(value) ? value : notSetValue;
+ },
+
+ includes: function(value) {
+ return this.has(value);
+ },
+
+
+ // ### More sequential methods
+
+ keySeq: function() {
+ return this.valueSeq();
+ }
+
+ });
+
+ SetIterable.prototype.has = IterablePrototype.includes;
+ SetIterable.prototype.contains = SetIterable.prototype.includes;
+
+
+ // Mixin subclasses
+
+ mixin(KeyedSeq, KeyedIterable.prototype);
+ mixin(IndexedSeq, IndexedIterable.prototype);
+ mixin(SetSeq, SetIterable.prototype);
+
+ mixin(KeyedCollection, KeyedIterable.prototype);
+ mixin(IndexedCollection, IndexedIterable.prototype);
+ mixin(SetCollection, SetIterable.prototype);
+
+
+ // #pragma Helper functions
+
+ function keyMapper(v, k) {
+ return k;
+ }
+
+ function entryMapper(v, k) {
+ return [k, v];
+ }
+
+ function not(predicate) {
+ return function() {
+ return !predicate.apply(this, arguments);
+ }
+ }
+
+ function neg(predicate) {
+ return function() {
+ return -predicate.apply(this, arguments);
+ }
+ }
+
+ function quoteString(value) {
+ return typeof value === 'string' ? JSON.stringify(value) : value;
+ }
+
+ function defaultZipper() {
+ return arrCopy(arguments);
+ }
+
+ function defaultNegComparator(a, b) {
+ return a < b ? 1 : a > b ? -1 : 0;
+ }
+
+ function hashIterable(iterable) {
+ if (iterable.size === Infinity) {
+ return 0;
+ }
+ var ordered = isOrdered(iterable);
+ var keyed = isKeyed(iterable);
+ var h = ordered ? 1 : 0;
+ var size = iterable.__iterate(
+ keyed ?
+ ordered ?
+ function(v, k) { h = 31 * h + hashMerge(hash(v), hash(k)) | 0; } :
+ function(v, k) { h = h + hashMerge(hash(v), hash(k)) | 0; } :
+ ordered ?
+ function(v ) { h = 31 * h + hash(v) | 0; } :
+ function(v ) { h = h + hash(v) | 0; }
+ );
+ return murmurHashOfSize(size, h);
+ }
+
+ function murmurHashOfSize(size, h) {
+ h = imul(h, 0xCC9E2D51);
+ h = imul(h << 15 | h >>> -15, 0x1B873593);
+ h = imul(h << 13 | h >>> -13, 5);
+ h = (h + 0xE6546B64 | 0) ^ size;
+ h = imul(h ^ h >>> 16, 0x85EBCA6B);
+ h = imul(h ^ h >>> 13, 0xC2B2AE35);
+ h = smi(h ^ h >>> 16);
+ return h;
+ }
+
+ function hashMerge(a, b) {
+ return a ^ b + 0x9E3779B9 + (a << 6) + (a >> 2) | 0; // int
+ }
+
+ var Immutable = {
+
+ Iterable: Iterable,
+
+ Seq: Seq,
+ Collection: Collection,
+ Map: Map,
+ OrderedMap: OrderedMap,
+ List: List,
+ Stack: Stack,
+ Set: Set,
+ OrderedSet: OrderedSet,
+
+ Record: Record,
+ Range: Range,
+ Repeat: Repeat,
+
+ is: is,
+ fromJS: fromJS
+
+ };
+
+ return Immutable;
+
+})); \ No newline at end of file
diff --git a/devtools/client/shared/vendor/jsol.js b/devtools/client/shared/vendor/jsol.js
new file mode 100755
index 000000000..a87948c43
--- /dev/null
+++ b/devtools/client/shared/vendor/jsol.js
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2010, Google Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+(function () {
+ /**
+ JSOL stands for JavaScript Object Literal which is a string representing
+ an object in JavaScript syntax.
+
+ For example:
+
+ {foo:"bar"} is equivalent to {"foo":"bar"} in JavaScript. Both are valid JSOL.
+
+ Note that {"foo":"bar"} is proper JSON[1] therefore you can use one of the many
+ JSON parsers out there like json2.js[2] or even the native browser's JSON parser,
+ if available.
+
+ However, {foo:"bar"} is NOT proper JSON but valid Javascript syntax for
+ representing an object with one key, "foo" and its value, "bar".
+ Using a JSON parser is not an option since this is NOT proper JSON.
+
+ You can use JSOL.parse to safely parse any string that reprsents a JavaScript Object Literal.
+ JSOL.parse will throw an Invalid JSOL exception on function calls, function declarations and variable references.
+
+ Examples:
+
+ JSOL.parse('{foo:"bar"}'); // valid
+
+ JSOL.parse('{evil:(function(){alert("I\'m evil");})()}'); // invalid function calls
+
+ JSOL.parse('{fn:function() { }}'); // invalid function declarations
+
+ var bar = "bar";
+ JSOL.parse('{foo:bar}'); // invalid variable references
+
+ [1] http://www.json.org
+ [2] http://www.json.org/json2.js
+ */
+ var trim = /^(\s|\u00A0)+|(\s|\u00A0)+$/g; // Used for trimming whitespace
+ var JSOL = {
+ parse: function(text) {
+ // make sure text is a "string"
+ if (typeof text !== "string" || !text) {
+ return null;
+ }
+ // Make sure leading/trailing whitespace is removed
+ text = text.replace(trim, "");
+ // Make sure the incoming text is actual JSOL (or Javascript Object Literal)
+ // Logic borrowed from http://json.org/json2.js
+ if ( /^[\],:{}\s]*$/.test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, "@")
+ .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, "]")
+ .replace(/(?:^|:|,)(?:\s*\[)+/g, ":")
+ /** everything up to this point is json2.js **/
+ /** this is the 5th stage where it accepts unquoted keys **/
+ .replace(/\w*\s*\:/g, ":")) ) {
+ return (new Function("return " + text))();
+ }
+ else {
+ throw("Invalid JSOL: " + text);
+ }
+ }
+ };
+
+ if (typeof define === "function" && define.amd) {
+ define(JSOL);
+ } else if (typeof module === "object" && module.exports) {
+ module.exports = JSOL;
+ } else {
+ this.JSOL = JSOL;
+ }
+})();
diff --git a/devtools/client/shared/vendor/moz.build b/devtools/client/shared/vendor/moz.build
new file mode 100644
index 000000000..e04221293
--- /dev/null
+++ b/devtools/client/shared/vendor/moz.build
@@ -0,0 +1,29 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+modules = []
+modules += [
+ 'immutable.js',
+ 'jsol.js',
+ 'react-addons-shallow-compare.js',
+]
+
+# react-dev is used if either debug mode is enabled,
+# so include it for both
+if CONFIG['DEBUG_JS_MODULES'] or CONFIG['MOZ_DEBUG']:
+ modules += ['react-dev.js']
+
+modules += [
+ 'react-dom.js',
+ 'react-proxy.js',
+ 'react-redux.js',
+ 'react-virtualized.js',
+ 'react.js',
+ 'redux.js',
+ 'reselect.js',
+ 'seamless-immutable.js',
+]
+
+DevToolsModules(*modules)
diff --git a/devtools/client/shared/vendor/react-addons-shallow-compare.js b/devtools/client/shared/vendor/react-addons-shallow-compare.js
new file mode 100644
index 000000000..6a1c723cc
--- /dev/null
+++ b/devtools/client/shared/vendor/react-addons-shallow-compare.js
@@ -0,0 +1,9 @@
+/* -*- 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";
+
+module.exports = require("devtools/client/shared/vendor/react").addons.shallowCompare;
diff --git a/devtools/client/shared/vendor/react-dev.js b/devtools/client/shared/vendor/react-dev.js
new file mode 100644
index 000000000..90e5e177a
--- /dev/null
+++ b/devtools/client/shared/vendor/react-dev.js
@@ -0,0 +1,20763 @@
+ /**
+ * React (with addons) v0.14.6
+ */
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.React = f()}})(function(){var define,module,exports;return (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<r.length;o++)s(r[o]);return s})({1:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactWithAddons
+ */
+
+/**
+ * This module exists purely in the open source project, and is meant as a way
+ * to create a separate standalone build of React. This build has "addons", or
+ * functionality we've built and think might be useful but doesn't have a good
+ * place to live inside React core.
+ */
+
+'use strict';
+
+var LinkedStateMixin = _dereq_(22);
+var React = _dereq_(26);
+var ReactComponentWithPureRenderMixin = _dereq_(37);
+var ReactCSSTransitionGroup = _dereq_(29);
+var ReactFragment = _dereq_(64);
+var ReactTransitionGroup = _dereq_(94);
+var ReactUpdates = _dereq_(96);
+
+var cloneWithProps = _dereq_(118);
+var shallowCompare = _dereq_(140);
+var update = _dereq_(143);
+var warning = _dereq_(173);
+
+var warnedAboutBatchedUpdates = false;
+
+React.addons = {
+ CSSTransitionGroup: ReactCSSTransitionGroup,
+ LinkedStateMixin: LinkedStateMixin,
+ PureRenderMixin: ReactComponentWithPureRenderMixin,
+ TransitionGroup: ReactTransitionGroup,
+
+ batchedUpdates: function () {
+ if ("development" !== 'production') {
+ "development" !== 'production' ? warning(warnedAboutBatchedUpdates, 'React.addons.batchedUpdates is deprecated. Use ' + 'ReactDOM.unstable_batchedUpdates instead.') : undefined;
+ warnedAboutBatchedUpdates = true;
+ }
+ return ReactUpdates.batchedUpdates.apply(this, arguments);
+ },
+ cloneWithProps: cloneWithProps,
+ createFragment: ReactFragment.create,
+ shallowCompare: shallowCompare,
+ update: update
+};
+
+React.addons.TestUtils = _dereq_(91);
+
+if ("development" !== 'production') {
+ React.addons.Perf = _dereq_(55);
+}
+
+module.exports = React;
+},{"118":118,"140":140,"143":143,"173":173,"22":22,"26":26,"29":29,"37":37,"55":55,"64":64,"91":91,"94":94,"96":96}],2:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule AutoFocusUtils
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var ReactMount = _dereq_(72);
+
+var findDOMNode = _dereq_(122);
+var focusNode = _dereq_(155);
+
+var Mixin = {
+ componentDidMount: function () {
+ if (this.props.autoFocus) {
+ focusNode(findDOMNode(this));
+ }
+ }
+};
+
+var AutoFocusUtils = {
+ Mixin: Mixin,
+
+ focusDOMComponent: function () {
+ focusNode(ReactMount.getNode(this._rootNodeID));
+ }
+};
+
+module.exports = AutoFocusUtils;
+},{"122":122,"155":155,"72":72}],3:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015 Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule BeforeInputEventPlugin
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var EventConstants = _dereq_(15);
+var EventPropagators = _dereq_(19);
+var ExecutionEnvironment = _dereq_(147);
+var FallbackCompositionState = _dereq_(20);
+var SyntheticCompositionEvent = _dereq_(103);
+var SyntheticInputEvent = _dereq_(107);
+
+var keyOf = _dereq_(166);
+
+var END_KEYCODES = [9, 13, 27, 32]; // Tab, Return, Esc, Space
+var START_KEYCODE = 229;
+
+var canUseCompositionEvent = ExecutionEnvironment.canUseDOM && 'CompositionEvent' in window;
+
+var documentMode = null;
+if (ExecutionEnvironment.canUseDOM && 'documentMode' in document) {
+ documentMode = document.documentMode;
+}
+
+// Webkit offers a very useful `textInput` event that can be used to
+// directly represent `beforeInput`. The IE `textinput` event is not as
+// useful, so we don't use it.
+var canUseTextInputEvent = ExecutionEnvironment.canUseDOM && 'TextEvent' in window && !documentMode && !isPresto();
+
+// In IE9+, we have access to composition events, but the data supplied
+// by the native compositionend event may be incorrect. Japanese ideographic
+// spaces, for instance (\u3000) are not recorded correctly.
+var useFallbackCompositionData = ExecutionEnvironment.canUseDOM && (!canUseCompositionEvent || documentMode && documentMode > 8 && documentMode <= 11);
+
+/**
+ * Opera <= 12 includes TextEvent in window, but does not fire
+ * text input events. Rely on keypress instead.
+ */
+function isPresto() {
+ var opera = window.opera;
+ return typeof opera === 'object' && typeof opera.version === 'function' && parseInt(opera.version(), 10) <= 12;
+}
+
+var SPACEBAR_CODE = 32;
+var SPACEBAR_CHAR = String.fromCharCode(SPACEBAR_CODE);
+
+var topLevelTypes = EventConstants.topLevelTypes;
+
+// Events and their corresponding property names.
+var eventTypes = {
+ beforeInput: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onBeforeInput: null }),
+ captured: keyOf({ onBeforeInputCapture: null })
+ },
+ dependencies: [topLevelTypes.topCompositionEnd, topLevelTypes.topKeyPress, topLevelTypes.topTextInput, topLevelTypes.topPaste]
+ },
+ compositionEnd: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onCompositionEnd: null }),
+ captured: keyOf({ onCompositionEndCapture: null })
+ },
+ dependencies: [topLevelTypes.topBlur, topLevelTypes.topCompositionEnd, topLevelTypes.topKeyDown, topLevelTypes.topKeyPress, topLevelTypes.topKeyUp, topLevelTypes.topMouseDown]
+ },
+ compositionStart: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onCompositionStart: null }),
+ captured: keyOf({ onCompositionStartCapture: null })
+ },
+ dependencies: [topLevelTypes.topBlur, topLevelTypes.topCompositionStart, topLevelTypes.topKeyDown, topLevelTypes.topKeyPress, topLevelTypes.topKeyUp, topLevelTypes.topMouseDown]
+ },
+ compositionUpdate: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onCompositionUpdate: null }),
+ captured: keyOf({ onCompositionUpdateCapture: null })
+ },
+ dependencies: [topLevelTypes.topBlur, topLevelTypes.topCompositionUpdate, topLevelTypes.topKeyDown, topLevelTypes.topKeyPress, topLevelTypes.topKeyUp, topLevelTypes.topMouseDown]
+ }
+};
+
+// Track whether we've ever handled a keypress on the space key.
+var hasSpaceKeypress = false;
+
+/**
+ * Return whether a native keypress event is assumed to be a command.
+ * This is required because Firefox fires `keypress` events for key commands
+ * (cut, copy, select-all, etc.) even though no character is inserted.
+ */
+function isKeypressCommand(nativeEvent) {
+ return (nativeEvent.ctrlKey || nativeEvent.altKey || nativeEvent.metaKey) &&
+ // ctrlKey && altKey is equivalent to AltGr, and is not a command.
+ !(nativeEvent.ctrlKey && nativeEvent.altKey);
+}
+
+/**
+ * Translate native top level events into event types.
+ *
+ * @param {string} topLevelType
+ * @return {object}
+ */
+function getCompositionEventType(topLevelType) {
+ switch (topLevelType) {
+ case topLevelTypes.topCompositionStart:
+ return eventTypes.compositionStart;
+ case topLevelTypes.topCompositionEnd:
+ return eventTypes.compositionEnd;
+ case topLevelTypes.topCompositionUpdate:
+ return eventTypes.compositionUpdate;
+ }
+}
+
+/**
+ * Does our fallback best-guess model think this event signifies that
+ * composition has begun?
+ *
+ * @param {string} topLevelType
+ * @param {object} nativeEvent
+ * @return {boolean}
+ */
+function isFallbackCompositionStart(topLevelType, nativeEvent) {
+ return topLevelType === topLevelTypes.topKeyDown && nativeEvent.keyCode === START_KEYCODE;
+}
+
+/**
+ * Does our fallback mode think that this event is the end of composition?
+ *
+ * @param {string} topLevelType
+ * @param {object} nativeEvent
+ * @return {boolean}
+ */
+function isFallbackCompositionEnd(topLevelType, nativeEvent) {
+ switch (topLevelType) {
+ case topLevelTypes.topKeyUp:
+ // Command keys insert or clear IME input.
+ return END_KEYCODES.indexOf(nativeEvent.keyCode) !== -1;
+ case topLevelTypes.topKeyDown:
+ // Expect IME keyCode on each keydown. If we get any other
+ // code we must have exited earlier.
+ return nativeEvent.keyCode !== START_KEYCODE;
+ case topLevelTypes.topKeyPress:
+ case topLevelTypes.topMouseDown:
+ case topLevelTypes.topBlur:
+ // Events are not possible without cancelling IME.
+ return true;
+ default:
+ return false;
+ }
+}
+
+/**
+ * Google Input Tools provides composition data via a CustomEvent,
+ * with the `data` property populated in the `detail` object. If this
+ * is available on the event object, use it. If not, this is a plain
+ * composition event and we have nothing special to extract.
+ *
+ * @param {object} nativeEvent
+ * @return {?string}
+ */
+function getDataFromCustomEvent(nativeEvent) {
+ var detail = nativeEvent.detail;
+ if (typeof detail === 'object' && 'data' in detail) {
+ return detail.data;
+ }
+ return null;
+}
+
+// Track the current IME composition fallback object, if any.
+var currentComposition = null;
+
+/**
+ * @param {string} topLevelType Record from `EventConstants`.
+ * @param {DOMEventTarget} topLevelTarget The listening component root node.
+ * @param {string} topLevelTargetID ID of `topLevelTarget`.
+ * @param {object} nativeEvent Native browser event.
+ * @return {?object} A SyntheticCompositionEvent.
+ */
+function extractCompositionEvent(topLevelType, topLevelTarget, topLevelTargetID, nativeEvent, nativeEventTarget) {
+ var eventType;
+ var fallbackData;
+
+ if (canUseCompositionEvent) {
+ eventType = getCompositionEventType(topLevelType);
+ } else if (!currentComposition) {
+ if (isFallbackCompositionStart(topLevelType, nativeEvent)) {
+ eventType = eventTypes.compositionStart;
+ }
+ } else if (isFallbackCompositionEnd(topLevelType, nativeEvent)) {
+ eventType = eventTypes.compositionEnd;
+ }
+
+ if (!eventType) {
+ return null;
+ }
+
+ if (useFallbackCompositionData) {
+ // The current composition is stored statically and must not be
+ // overwritten while composition continues.
+ if (!currentComposition && eventType === eventTypes.compositionStart) {
+ currentComposition = FallbackCompositionState.getPooled(topLevelTarget);
+ } else if (eventType === eventTypes.compositionEnd) {
+ if (currentComposition) {
+ fallbackData = currentComposition.getData();
+ }
+ }
+ }
+
+ var event = SyntheticCompositionEvent.getPooled(eventType, topLevelTargetID, nativeEvent, nativeEventTarget);
+
+ if (fallbackData) {
+ // Inject data generated from fallback path into the synthetic event.
+ // This matches the property of native CompositionEventInterface.
+ event.data = fallbackData;
+ } else {
+ var customData = getDataFromCustomEvent(nativeEvent);
+ if (customData !== null) {
+ event.data = customData;
+ }
+ }
+
+ EventPropagators.accumulateTwoPhaseDispatches(event);
+ return event;
+}
+
+/**
+ * @param {string} topLevelType Record from `EventConstants`.
+ * @param {object} nativeEvent Native browser event.
+ * @return {?string} The string corresponding to this `beforeInput` event.
+ */
+function getNativeBeforeInputChars(topLevelType, nativeEvent) {
+ switch (topLevelType) {
+ case topLevelTypes.topCompositionEnd:
+ return getDataFromCustomEvent(nativeEvent);
+ case topLevelTypes.topKeyPress:
+ /**
+ * If native `textInput` events are available, our goal is to make
+ * use of them. However, there is a special case: the spacebar key.
+ * In Webkit, preventing default on a spacebar `textInput` event
+ * cancels character insertion, but it *also* causes the browser
+ * to fall back to its default spacebar behavior of scrolling the
+ * page.
+ *
+ * Tracking at:
+ * https://code.google.com/p/chromium/issues/detail?id=355103
+ *
+ * To avoid this issue, use the keypress event as if no `textInput`
+ * event is available.
+ */
+ var which = nativeEvent.which;
+ if (which !== SPACEBAR_CODE) {
+ return null;
+ }
+
+ hasSpaceKeypress = true;
+ return SPACEBAR_CHAR;
+
+ case topLevelTypes.topTextInput:
+ // Record the characters to be added to the DOM.
+ var chars = nativeEvent.data;
+
+ // If it's a spacebar character, assume that we have already handled
+ // it at the keypress level and bail immediately. Android Chrome
+ // doesn't give us keycodes, so we need to blacklist it.
+ if (chars === SPACEBAR_CHAR && hasSpaceKeypress) {
+ return null;
+ }
+
+ return chars;
+
+ default:
+ // For other native event types, do nothing.
+ return null;
+ }
+}
+
+/**
+ * For browsers that do not provide the `textInput` event, extract the
+ * appropriate string to use for SyntheticInputEvent.
+ *
+ * @param {string} topLevelType Record from `EventConstants`.
+ * @param {object} nativeEvent Native browser event.
+ * @return {?string} The fallback string for this `beforeInput` event.
+ */
+function getFallbackBeforeInputChars(topLevelType, nativeEvent) {
+ // If we are currently composing (IME) and using a fallback to do so,
+ // try to extract the composed characters from the fallback object.
+ if (currentComposition) {
+ if (topLevelType === topLevelTypes.topCompositionEnd || isFallbackCompositionEnd(topLevelType, nativeEvent)) {
+ var chars = currentComposition.getData();
+ FallbackCompositionState.release(currentComposition);
+ currentComposition = null;
+ return chars;
+ }
+ return null;
+ }
+
+ switch (topLevelType) {
+ case topLevelTypes.topPaste:
+ // If a paste event occurs after a keypress, throw out the input
+ // chars. Paste events should not lead to BeforeInput events.
+ return null;
+ case topLevelTypes.topKeyPress:
+ /**
+ * As of v27, Firefox may fire keypress events even when no character
+ * will be inserted. A few possibilities:
+ *
+ * - `which` is `0`. Arrow keys, Esc key, etc.
+ *
+ * - `which` is the pressed key code, but no char is available.
+ * Ex: 'AltGr + d` in Polish. There is no modified character for
+ * this key combination and no character is inserted into the
+ * document, but FF fires the keypress for char code `100` anyway.
+ * No `input` event will occur.
+ *
+ * - `which` is the pressed key code, but a command combination is
+ * being used. Ex: `Cmd+C`. No character is inserted, and no
+ * `input` event will occur.
+ */
+ if (nativeEvent.which && !isKeypressCommand(nativeEvent)) {
+ return String.fromCharCode(nativeEvent.which);
+ }
+ return null;
+ case topLevelTypes.topCompositionEnd:
+ return useFallbackCompositionData ? null : nativeEvent.data;
+ default:
+ return null;
+ }
+}
+
+/**
+ * Extract a SyntheticInputEvent for `beforeInput`, based on either native
+ * `textInput` or fallback behavior.
+ *
+ * @param {string} topLevelType Record from `EventConstants`.
+ * @param {DOMEventTarget} topLevelTarget The listening component root node.
+ * @param {string} topLevelTargetID ID of `topLevelTarget`.
+ * @param {object} nativeEvent Native browser event.
+ * @return {?object} A SyntheticInputEvent.
+ */
+function extractBeforeInputEvent(topLevelType, topLevelTarget, topLevelTargetID, nativeEvent, nativeEventTarget) {
+ var chars;
+
+ if (canUseTextInputEvent) {
+ chars = getNativeBeforeInputChars(topLevelType, nativeEvent);
+ } else {
+ chars = getFallbackBeforeInputChars(topLevelType, nativeEvent);
+ }
+
+ // If no characters are being inserted, no BeforeInput event should
+ // be fired.
+ if (!chars) {
+ return null;
+ }
+
+ var event = SyntheticInputEvent.getPooled(eventTypes.beforeInput, topLevelTargetID, nativeEvent, nativeEventTarget);
+
+ event.data = chars;
+ EventPropagators.accumulateTwoPhaseDispatches(event);
+ return event;
+}
+
+/**
+ * Create an `onBeforeInput` event to match
+ * http://www.w3.org/TR/2013/WD-DOM-Level-3-Events-20131105/#events-inputevents.
+ *
+ * This event plugin is based on the native `textInput` event
+ * available in Chrome, Safari, Opera, and IE. This event fires after
+ * `onKeyPress` and `onCompositionEnd`, but before `onInput`.
+ *
+ * `beforeInput` is spec'd but not implemented in any browsers, and
+ * the `input` event does not provide any useful information about what has
+ * actually been added, contrary to the spec. Thus, `textInput` is the best
+ * available event to identify the characters that have actually been inserted
+ * into the target node.
+ *
+ * This plugin is also responsible for emitting `composition` events, thus
+ * allowing us to share composition fallback code for both `beforeInput` and
+ * `composition` event types.
+ */
+var BeforeInputEventPlugin = {
+
+ eventTypes: eventTypes,
+
+ /**
+ * @param {string} topLevelType Record from `EventConstants`.
+ * @param {DOMEventTarget} topLevelTarget The listening component root node.
+ * @param {string} topLevelTargetID ID of `topLevelTarget`.
+ * @param {object} nativeEvent Native browser event.
+ * @return {*} An accumulation of synthetic events.
+ * @see {EventPluginHub.extractEvents}
+ */
+ extractEvents: function (topLevelType, topLevelTarget, topLevelTargetID, nativeEvent, nativeEventTarget) {
+ return [extractCompositionEvent(topLevelType, topLevelTarget, topLevelTargetID, nativeEvent, nativeEventTarget), extractBeforeInputEvent(topLevelType, topLevelTarget, topLevelTargetID, nativeEvent, nativeEventTarget)];
+ }
+};
+
+module.exports = BeforeInputEventPlugin;
+},{"103":103,"107":107,"147":147,"15":15,"166":166,"19":19,"20":20}],4:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule CSSProperty
+ */
+
+'use strict';
+
+/**
+ * CSS properties which accept numbers but are not in units of "px".
+ */
+var isUnitlessNumber = {
+ animationIterationCount: true,
+ boxFlex: true,
+ boxFlexGroup: true,
+ boxOrdinalGroup: true,
+ columnCount: true,
+ flex: true,
+ flexGrow: true,
+ flexPositive: true,
+ flexShrink: true,
+ flexNegative: true,
+ flexOrder: true,
+ fontWeight: true,
+ lineClamp: true,
+ lineHeight: true,
+ opacity: true,
+ order: true,
+ orphans: true,
+ tabSize: true,
+ widows: true,
+ zIndex: true,
+ zoom: true,
+
+ // SVG-related properties
+ fillOpacity: true,
+ stopOpacity: true,
+ strokeDashoffset: true,
+ strokeOpacity: true,
+ strokeWidth: true
+};
+
+/**
+ * @param {string} prefix vendor-specific prefix, eg: Webkit
+ * @param {string} key style name, eg: transitionDuration
+ * @return {string} style name prefixed with `prefix`, properly camelCased, eg:
+ * WebkitTransitionDuration
+ */
+function prefixKey(prefix, key) {
+ return prefix + key.charAt(0).toUpperCase() + key.substring(1);
+}
+
+/**
+ * Support style names that may come passed in prefixed by adding permutations
+ * of vendor prefixes.
+ */
+var prefixes = ['Webkit', 'ms', 'Moz', 'O'];
+
+// Using Object.keys here, or else the vanilla for-in loop makes IE8 go into an
+// infinite loop, because it iterates over the newly added props too.
+Object.keys(isUnitlessNumber).forEach(function (prop) {
+ prefixes.forEach(function (prefix) {
+ isUnitlessNumber[prefixKey(prefix, prop)] = isUnitlessNumber[prop];
+ });
+});
+
+/**
+ * Most style properties can be unset by doing .style[prop] = '' but IE8
+ * doesn't like doing that with shorthand properties so for the properties that
+ * IE8 breaks on, which are listed here, we instead unset each of the
+ * individual properties. See http://bugs.jquery.com/ticket/12385.
+ * The 4-value 'clock' properties like margin, padding, border-width seem to
+ * behave without any problems. Curiously, list-style works too without any
+ * special prodding.
+ */
+var shorthandPropertyExpansions = {
+ background: {
+ backgroundAttachment: true,
+ backgroundColor: true,
+ backgroundImage: true,
+ backgroundPositionX: true,
+ backgroundPositionY: true,
+ backgroundRepeat: true
+ },
+ backgroundPosition: {
+ backgroundPositionX: true,
+ backgroundPositionY: true
+ },
+ border: {
+ borderWidth: true,
+ borderStyle: true,
+ borderColor: true
+ },
+ borderBottom: {
+ borderBottomWidth: true,
+ borderBottomStyle: true,
+ borderBottomColor: true
+ },
+ borderLeft: {
+ borderLeftWidth: true,
+ borderLeftStyle: true,
+ borderLeftColor: true
+ },
+ borderRight: {
+ borderRightWidth: true,
+ borderRightStyle: true,
+ borderRightColor: true
+ },
+ borderTop: {
+ borderTopWidth: true,
+ borderTopStyle: true,
+ borderTopColor: true
+ },
+ font: {
+ fontStyle: true,
+ fontVariant: true,
+ fontWeight: true,
+ fontSize: true,
+ lineHeight: true,
+ fontFamily: true
+ },
+ outline: {
+ outlineWidth: true,
+ outlineStyle: true,
+ outlineColor: true
+ }
+};
+
+var CSSProperty = {
+ isUnitlessNumber: isUnitlessNumber,
+ shorthandPropertyExpansions: shorthandPropertyExpansions
+};
+
+module.exports = CSSProperty;
+},{}],5:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule CSSPropertyOperations
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var CSSProperty = _dereq_(4);
+var ExecutionEnvironment = _dereq_(147);
+var ReactPerf = _dereq_(78);
+
+var camelizeStyleName = _dereq_(149);
+var dangerousStyleValue = _dereq_(119);
+var hyphenateStyleName = _dereq_(160);
+var memoizeStringOnly = _dereq_(168);
+var warning = _dereq_(173);
+
+var processStyleName = memoizeStringOnly(function (styleName) {
+ return hyphenateStyleName(styleName);
+});
+
+var hasShorthandPropertyBug = false;
+var styleFloatAccessor = 'cssFloat';
+if (ExecutionEnvironment.canUseDOM) {
+ var tempStyle = document.createElementNS('http://www.w3.org/1999/xhtml', 'div').style;
+ try {
+ // IE8 throws "Invalid argument." if resetting shorthand style properties.
+ tempStyle.font = '';
+ } catch (e) {
+ hasShorthandPropertyBug = true;
+ }
+ // IE8 only supports accessing cssFloat (standard) as styleFloat
+ if (document.documentElement.style.cssFloat === undefined) {
+ styleFloatAccessor = 'styleFloat';
+ }
+}
+
+if ("development" !== 'production') {
+ // 'msTransform' is correct, but the other prefixes should be capitalized
+ var badVendoredStyleNamePattern = /^(?:webkit|moz|o)[A-Z]/;
+
+ // style values shouldn't contain a semicolon
+ var badStyleValueWithSemicolonPattern = /;\s*$/;
+
+ var warnedStyleNames = {};
+ var warnedStyleValues = {};
+
+ var warnHyphenatedStyleName = function (name) {
+ if (warnedStyleNames.hasOwnProperty(name) && warnedStyleNames[name]) {
+ return;
+ }
+
+ warnedStyleNames[name] = true;
+ "development" !== 'production' ? warning(false, 'Unsupported style property %s. Did you mean %s?', name, camelizeStyleName(name)) : undefined;
+ };
+
+ var warnBadVendoredStyleName = function (name) {
+ if (warnedStyleNames.hasOwnProperty(name) && warnedStyleNames[name]) {
+ return;
+ }
+
+ warnedStyleNames[name] = true;
+ "development" !== 'production' ? warning(false, 'Unsupported vendor-prefixed style property %s. Did you mean %s?', name, name.charAt(0).toUpperCase() + name.slice(1)) : undefined;
+ };
+
+ var warnStyleValueWithSemicolon = function (name, value) {
+ if (warnedStyleValues.hasOwnProperty(value) && warnedStyleValues[value]) {
+ return;
+ }
+
+ warnedStyleValues[value] = true;
+ "development" !== 'production' ? warning(false, 'Style property values shouldn\'t contain a semicolon. ' + 'Try "%s: %s" instead.', name, value.replace(badStyleValueWithSemicolonPattern, '')) : undefined;
+ };
+
+ /**
+ * @param {string} name
+ * @param {*} value
+ */
+ var warnValidStyle = function (name, value) {
+ if (name.indexOf('-') > -1) {
+ warnHyphenatedStyleName(name);
+ } else if (badVendoredStyleNamePattern.test(name)) {
+ warnBadVendoredStyleName(name);
+ } else if (badStyleValueWithSemicolonPattern.test(value)) {
+ warnStyleValueWithSemicolon(name, value);
+ }
+ };
+}
+
+/**
+ * Operations for dealing with CSS properties.
+ */
+var CSSPropertyOperations = {
+
+ /**
+ * Serializes a mapping of style properties for use as inline styles:
+ *
+ * > createMarkupForStyles({width: '200px', height: 0})
+ * "width:200px;height:0;"
+ *
+ * Undefined values are ignored so that declarative programming is easier.
+ * The result should be HTML-escaped before insertion into the DOM.
+ *
+ * @param {object} styles
+ * @return {?string}
+ */
+ createMarkupForStyles: function (styles) {
+ var serialized = '';
+ for (var styleName in styles) {
+ if (!styles.hasOwnProperty(styleName)) {
+ continue;
+ }
+ var styleValue = styles[styleName];
+ if ("development" !== 'production') {
+ warnValidStyle(styleName, styleValue);
+ }
+ if (styleValue != null) {
+ serialized += processStyleName(styleName) + ':';
+ serialized += dangerousStyleValue(styleName, styleValue) + ';';
+ }
+ }
+ return serialized || null;
+ },
+
+ /**
+ * Sets the value for multiple styles on a node. If a value is specified as
+ * '' (empty string), the corresponding style property will be unset.
+ *
+ * @param {DOMElement} node
+ * @param {object} styles
+ */
+ setValueForStyles: function (node, styles) {
+ var style = node.style;
+ for (var styleName in styles) {
+ if (!styles.hasOwnProperty(styleName)) {
+ continue;
+ }
+ if ("development" !== 'production') {
+ warnValidStyle(styleName, styles[styleName]);
+ }
+ var styleValue = dangerousStyleValue(styleName, styles[styleName]);
+ if (styleName === 'float') {
+ styleName = styleFloatAccessor;
+ }
+ if (styleValue) {
+ style[styleName] = styleValue;
+ } else {
+ var expansion = hasShorthandPropertyBug && CSSProperty.shorthandPropertyExpansions[styleName];
+ if (expansion) {
+ // Shorthand property that IE8 won't like unsetting, so unset each
+ // component to placate it
+ for (var individualStyleName in expansion) {
+ style[individualStyleName] = '';
+ }
+ } else {
+ style[styleName] = '';
+ }
+ }
+ }
+ }
+
+};
+
+ReactPerf.measureMethods(CSSPropertyOperations, 'CSSPropertyOperations', {
+ setValueForStyles: 'setValueForStyles'
+});
+
+module.exports = CSSPropertyOperations;
+},{"119":119,"147":147,"149":149,"160":160,"168":168,"173":173,"4":4,"78":78}],6:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule CallbackQueue
+ */
+
+'use strict';
+
+var PooledClass = _dereq_(25);
+
+var assign = _dereq_(24);
+var invariant = _dereq_(161);
+
+/**
+ * A specialized pseudo-event module to help keep track of components waiting to
+ * be notified when their DOM representations are available for use.
+ *
+ * This implements `PooledClass`, so you should never need to instantiate this.
+ * Instead, use `CallbackQueue.getPooled()`.
+ *
+ * @class ReactMountReady
+ * @implements PooledClass
+ * @internal
+ */
+function CallbackQueue() {
+ this._callbacks = null;
+ this._contexts = null;
+}
+
+assign(CallbackQueue.prototype, {
+
+ /**
+ * Enqueues a callback to be invoked when `notifyAll` is invoked.
+ *
+ * @param {function} callback Invoked when `notifyAll` is invoked.
+ * @param {?object} context Context to call `callback` with.
+ * @internal
+ */
+ enqueue: function (callback, context) {
+ this._callbacks = this._callbacks || [];
+ this._contexts = this._contexts || [];
+ this._callbacks.push(callback);
+ this._contexts.push(context);
+ },
+
+ /**
+ * Invokes all enqueued callbacks and clears the queue. This is invoked after
+ * the DOM representation of a component has been created or updated.
+ *
+ * @internal
+ */
+ notifyAll: function () {
+ var callbacks = this._callbacks;
+ var contexts = this._contexts;
+ if (callbacks) {
+ !(callbacks.length === contexts.length) ? "development" !== 'production' ? invariant(false, 'Mismatched list of contexts in callback queue') : invariant(false) : undefined;
+ this._callbacks = null;
+ this._contexts = null;
+ for (var i = 0; i < callbacks.length; i++) {
+ callbacks[i].call(contexts[i]);
+ }
+ callbacks.length = 0;
+ contexts.length = 0;
+ }
+ },
+
+ /**
+ * Resets the internal queue.
+ *
+ * @internal
+ */
+ reset: function () {
+ this._callbacks = null;
+ this._contexts = null;
+ },
+
+ /**
+ * `PooledClass` looks for this.
+ */
+ destructor: function () {
+ this.reset();
+ }
+
+});
+
+PooledClass.addPoolingTo(CallbackQueue);
+
+module.exports = CallbackQueue;
+},{"161":161,"24":24,"25":25}],7:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ChangeEventPlugin
+ */
+
+'use strict';
+
+var EventConstants = _dereq_(15);
+var EventPluginHub = _dereq_(16);
+var EventPropagators = _dereq_(19);
+var ExecutionEnvironment = _dereq_(147);
+var ReactUpdates = _dereq_(96);
+var SyntheticEvent = _dereq_(105);
+
+var getEventTarget = _dereq_(128);
+var isEventSupported = _dereq_(133);
+var isTextInputElement = _dereq_(134);
+var keyOf = _dereq_(166);
+
+var topLevelTypes = EventConstants.topLevelTypes;
+
+var eventTypes = {
+ change: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onChange: null }),
+ captured: keyOf({ onChangeCapture: null })
+ },
+ dependencies: [topLevelTypes.topBlur, topLevelTypes.topChange, topLevelTypes.topClick, topLevelTypes.topFocus, topLevelTypes.topInput, topLevelTypes.topKeyDown, topLevelTypes.topKeyUp, topLevelTypes.topSelectionChange]
+ }
+};
+
+/**
+ * For IE shims
+ */
+var activeElement = null;
+var activeElementID = null;
+var activeElementValue = null;
+var activeElementValueProp = null;
+
+/**
+ * SECTION: handle `change` event
+ */
+function shouldUseChangeEvent(elem) {
+ var nodeName = elem.nodeName && elem.nodeName.toLowerCase();
+ return nodeName === 'select' || nodeName === 'input' && elem.type === 'file';
+}
+
+var doesChangeEventBubble = false;
+if (ExecutionEnvironment.canUseDOM) {
+ // See `handleChange` comment below
+ doesChangeEventBubble = isEventSupported('change') && (!('documentMode' in document) || document.documentMode > 8);
+}
+
+function manualDispatchChangeEvent(nativeEvent) {
+ var event = SyntheticEvent.getPooled(eventTypes.change, activeElementID, nativeEvent, getEventTarget(nativeEvent));
+ EventPropagators.accumulateTwoPhaseDispatches(event);
+
+ // If change and propertychange bubbled, we'd just bind to it like all the
+ // other events and have it go through ReactBrowserEventEmitter. Since it
+ // doesn't, we manually listen for the events and so we have to enqueue and
+ // process the abstract event manually.
+ //
+ // Batching is necessary here in order to ensure that all event handlers run
+ // before the next rerender (including event handlers attached to ancestor
+ // elements instead of directly on the input). Without this, controlled
+ // components don't work properly in conjunction with event bubbling because
+ // the component is rerendered and the value reverted before all the event
+ // handlers can run. See https://github.com/facebook/react/issues/708.
+ ReactUpdates.batchedUpdates(runEventInBatch, event);
+}
+
+function runEventInBatch(event) {
+ EventPluginHub.enqueueEvents(event);
+ EventPluginHub.processEventQueue(false);
+}
+
+function startWatchingForChangeEventIE8(target, targetID) {
+ activeElement = target;
+ activeElementID = targetID;
+ activeElement.attachEvent('onchange', manualDispatchChangeEvent);
+}
+
+function stopWatchingForChangeEventIE8() {
+ if (!activeElement) {
+ return;
+ }
+ activeElement.detachEvent('onchange', manualDispatchChangeEvent);
+ activeElement = null;
+ activeElementID = null;
+}
+
+function getTargetIDForChangeEvent(topLevelType, topLevelTarget, topLevelTargetID) {
+ if (topLevelType === topLevelTypes.topChange) {
+ return topLevelTargetID;
+ }
+}
+function handleEventsForChangeEventIE8(topLevelType, topLevelTarget, topLevelTargetID) {
+ if (topLevelType === topLevelTypes.topFocus) {
+ // stopWatching() should be a noop here but we call it just in case we
+ // missed a blur event somehow.
+ stopWatchingForChangeEventIE8();
+ startWatchingForChangeEventIE8(topLevelTarget, topLevelTargetID);
+ } else if (topLevelType === topLevelTypes.topBlur) {
+ stopWatchingForChangeEventIE8();
+ }
+}
+
+/**
+ * SECTION: handle `input` event
+ */
+var isInputEventSupported = false;
+if (ExecutionEnvironment.canUseDOM) {
+ // IE9 claims to support the input event but fails to trigger it when
+ // deleting text, so we ignore its input events
+ isInputEventSupported = isEventSupported('input') && (!('documentMode' in document) || document.documentMode > 9);
+}
+
+/**
+ * (For old IE.) Replacement getter/setter for the `value` property that gets
+ * set on the active element.
+ */
+var newValueProp = {
+ get: function () {
+ return activeElementValueProp.get.call(this);
+ },
+ set: function (val) {
+ // Cast to a string so we can do equality checks.
+ activeElementValue = '' + val;
+ activeElementValueProp.set.call(this, val);
+ }
+};
+
+/**
+ * (For old IE.) Starts tracking propertychange events on the passed-in element
+ * and override the value property so that we can distinguish user events from
+ * value changes in JS.
+ */
+function startWatchingForValueChange(target, targetID) {
+ activeElement = target;
+ activeElementID = targetID;
+ activeElementValue = target.value;
+ activeElementValueProp = Object.getOwnPropertyDescriptor(target.constructor.prototype, 'value');
+
+ // Not guarded in a canDefineProperty check: IE8 supports defineProperty only
+ // on DOM elements
+ Object.defineProperty(activeElement, 'value', newValueProp);
+ activeElement.attachEvent('onpropertychange', handlePropertyChange);
+}
+
+/**
+ * (For old IE.) Removes the event listeners from the currently-tracked element,
+ * if any exists.
+ */
+function stopWatchingForValueChange() {
+ if (!activeElement) {
+ return;
+ }
+
+ // delete restores the original property definition
+ delete activeElement.value;
+ activeElement.detachEvent('onpropertychange', handlePropertyChange);
+
+ activeElement = null;
+ activeElementID = null;
+ activeElementValue = null;
+ activeElementValueProp = null;
+}
+
+/**
+ * (For old IE.) Handles a propertychange event, sending a `change` event if
+ * the value of the active element has changed.
+ */
+function handlePropertyChange(nativeEvent) {
+ if (nativeEvent.propertyName !== 'value') {
+ return;
+ }
+ var value = nativeEvent.srcElement.value;
+ if (value === activeElementValue) {
+ return;
+ }
+ activeElementValue = value;
+
+ manualDispatchChangeEvent(nativeEvent);
+}
+
+/**
+ * If a `change` event should be fired, returns the target's ID.
+ */
+function getTargetIDForInputEvent(topLevelType, topLevelTarget, topLevelTargetID) {
+ if (topLevelType === topLevelTypes.topInput) {
+ // In modern browsers (i.e., not IE8 or IE9), the input event is exactly
+ // what we want so fall through here and trigger an abstract event
+ return topLevelTargetID;
+ }
+}
+
+// For IE8 and IE9.
+function handleEventsForInputEventIE(topLevelType, topLevelTarget, topLevelTargetID) {
+ if (topLevelType === topLevelTypes.topFocus) {
+ // In IE8, we can capture almost all .value changes by adding a
+ // propertychange handler and looking for events with propertyName
+ // equal to 'value'
+ // In IE9, propertychange fires for most input events but is buggy and
+ // doesn't fire when text is deleted, but conveniently, selectionchange
+ // appears to fire in all of the remaining cases so we catch those and
+ // forward the event if the value has changed
+ // In either case, we don't want to call the event handler if the value
+ // is changed from JS so we redefine a setter for `.value` that updates
+ // our activeElementValue variable, allowing us to ignore those changes
+ //
+ // stopWatching() should be a noop here but we call it just in case we
+ // missed a blur event somehow.
+ stopWatchingForValueChange();
+ startWatchingForValueChange(topLevelTarget, topLevelTargetID);
+ } else if (topLevelType === topLevelTypes.topBlur) {
+ stopWatchingForValueChange();
+ }
+}
+
+// For IE8 and IE9.
+function getTargetIDForInputEventIE(topLevelType, topLevelTarget, topLevelTargetID) {
+ if (topLevelType === topLevelTypes.topSelectionChange || topLevelType === topLevelTypes.topKeyUp || topLevelType === topLevelTypes.topKeyDown) {
+ // On the selectionchange event, the target is just document which isn't
+ // helpful for us so just check activeElement instead.
+ //
+ // 99% of the time, keydown and keyup aren't necessary. IE8 fails to fire
+ // propertychange on the first input event after setting `value` from a
+ // script and fires only keydown, keypress, keyup. Catching keyup usually
+ // gets it and catching keydown lets us fire an event for the first
+ // keystroke if user does a key repeat (it'll be a little delayed: right
+ // before the second keystroke). Other input methods (e.g., paste) seem to
+ // fire selectionchange normally.
+ if (activeElement && activeElement.value !== activeElementValue) {
+ activeElementValue = activeElement.value;
+ return activeElementID;
+ }
+ }
+}
+
+/**
+ * SECTION: handle `click` event
+ */
+function shouldUseClickEvent(elem) {
+ // Use the `click` event to detect changes to checkbox and radio inputs.
+ // This approach works across all browsers, whereas `change` does not fire
+ // until `blur` in IE8.
+ return elem.nodeName && elem.nodeName.toLowerCase() === 'input' && (elem.type === 'checkbox' || elem.type === 'radio');
+}
+
+function getTargetIDForClickEvent(topLevelType, topLevelTarget, topLevelTargetID) {
+ if (topLevelType === topLevelTypes.topClick) {
+ return topLevelTargetID;
+ }
+}
+
+/**
+ * This plugin creates an `onChange` event that normalizes change events
+ * across form elements. This event fires at a time when it's possible to
+ * change the element's value without seeing a flicker.
+ *
+ * Supported elements are:
+ * - input (see `isTextInputElement`)
+ * - textarea
+ * - select
+ */
+var ChangeEventPlugin = {
+
+ eventTypes: eventTypes,
+
+ /**
+ * @param {string} topLevelType Record from `EventConstants`.
+ * @param {DOMEventTarget} topLevelTarget The listening component root node.
+ * @param {string} topLevelTargetID ID of `topLevelTarget`.
+ * @param {object} nativeEvent Native browser event.
+ * @return {*} An accumulation of synthetic events.
+ * @see {EventPluginHub.extractEvents}
+ */
+ extractEvents: function (topLevelType, topLevelTarget, topLevelTargetID, nativeEvent, nativeEventTarget) {
+
+ var getTargetIDFunc, handleEventFunc;
+ if (shouldUseChangeEvent(topLevelTarget)) {
+ if (doesChangeEventBubble) {
+ getTargetIDFunc = getTargetIDForChangeEvent;
+ } else {
+ handleEventFunc = handleEventsForChangeEventIE8;
+ }
+ } else if (isTextInputElement(topLevelTarget)) {
+ if (isInputEventSupported) {
+ getTargetIDFunc = getTargetIDForInputEvent;
+ } else {
+ getTargetIDFunc = getTargetIDForInputEventIE;
+ handleEventFunc = handleEventsForInputEventIE;
+ }
+ } else if (shouldUseClickEvent(topLevelTarget)) {
+ getTargetIDFunc = getTargetIDForClickEvent;
+ }
+
+ if (getTargetIDFunc) {
+ var targetID = getTargetIDFunc(topLevelType, topLevelTarget, topLevelTargetID);
+ if (targetID) {
+ var event = SyntheticEvent.getPooled(eventTypes.change, targetID, nativeEvent, nativeEventTarget);
+ event.type = 'change';
+ EventPropagators.accumulateTwoPhaseDispatches(event);
+ return event;
+ }
+ }
+
+ if (handleEventFunc) {
+ handleEventFunc(topLevelType, topLevelTarget, topLevelTargetID);
+ }
+ }
+
+};
+
+module.exports = ChangeEventPlugin;
+},{"105":105,"128":128,"133":133,"134":134,"147":147,"15":15,"16":16,"166":166,"19":19,"96":96}],8:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ClientReactRootIndex
+ * @typechecks
+ */
+
+'use strict';
+
+var nextReactRootIndex = 0;
+
+var ClientReactRootIndex = {
+ createReactRootIndex: function () {
+ return nextReactRootIndex++;
+ }
+};
+
+module.exports = ClientReactRootIndex;
+},{}],9:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule DOMChildrenOperations
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var Danger = _dereq_(12);
+var ReactMultiChildUpdateTypes = _dereq_(74);
+var ReactPerf = _dereq_(78);
+
+var setInnerHTML = _dereq_(138);
+var setTextContent = _dereq_(139);
+var invariant = _dereq_(161);
+
+/**
+ * Inserts `childNode` as a child of `parentNode` at the `index`.
+ *
+ * @param {DOMElement} parentNode Parent node in which to insert.
+ * @param {DOMElement} childNode Child node to insert.
+ * @param {number} index Index at which to insert the child.
+ * @internal
+ */
+function insertChildAt(parentNode, childNode, index) {
+ // By exploiting arrays returning `undefined` for an undefined index, we can
+ // rely exclusively on `insertBefore(node, null)` instead of also using
+ // `appendChild(node)`. However, using `undefined` is not allowed by all
+ // browsers so we must replace it with `null`.
+
+ // fix render order error in safari
+ // IE8 will throw error when index out of list size.
+ var beforeChild = index >= parentNode.childNodes.length ? null : parentNode.childNodes.item(index);
+
+ parentNode.insertBefore(childNode, beforeChild);
+}
+
+/**
+ * Operations for updating with DOM children.
+ */
+var DOMChildrenOperations = {
+
+ dangerouslyReplaceNodeWithMarkup: Danger.dangerouslyReplaceNodeWithMarkup,
+
+ updateTextContent: setTextContent,
+
+ /**
+ * Updates a component's children by processing a series of updates. The
+ * update configurations are each expected to have a `parentNode` property.
+ *
+ * @param {array<object>} updates List of update configurations.
+ * @param {array<string>} markupList List of markup strings.
+ * @internal
+ */
+ processUpdates: function (updates, markupList) {
+ var update;
+ // Mapping from parent IDs to initial child orderings.
+ var initialChildren = null;
+ // List of children that will be moved or removed.
+ var updatedChildren = null;
+
+ for (var i = 0; i < updates.length; i++) {
+ update = updates[i];
+ if (update.type === ReactMultiChildUpdateTypes.MOVE_EXISTING || update.type === ReactMultiChildUpdateTypes.REMOVE_NODE) {
+ var updatedIndex = update.fromIndex;
+ var updatedChild = update.parentNode.childNodes[updatedIndex];
+ var parentID = update.parentID;
+
+ !updatedChild ? "development" !== 'production' ? invariant(false, 'processUpdates(): Unable to find child %s of element. This ' + 'probably means the DOM was unexpectedly mutated (e.g., by the ' + 'browser), usually due to forgetting a <tbody> when using tables, ' + 'nesting tags like <form>, <p>, or <a>, or using non-SVG elements ' + 'in an <svg> parent. Try inspecting the child nodes of the element ' + 'with React ID `%s`.', updatedIndex, parentID) : invariant(false) : undefined;
+
+ initialChildren = initialChildren || {};
+ initialChildren[parentID] = initialChildren[parentID] || [];
+ initialChildren[parentID][updatedIndex] = updatedChild;
+
+ updatedChildren = updatedChildren || [];
+ updatedChildren.push(updatedChild);
+ }
+ }
+
+ var renderedMarkup;
+ // markupList is either a list of markup or just a list of elements
+ if (markupList.length && typeof markupList[0] === 'string') {
+ renderedMarkup = Danger.dangerouslyRenderMarkup(markupList);
+ } else {
+ renderedMarkup = markupList;
+ }
+
+ // Remove updated children first so that `toIndex` is consistent.
+ if (updatedChildren) {
+ for (var j = 0; j < updatedChildren.length; j++) {
+ updatedChildren[j].parentNode.removeChild(updatedChildren[j]);
+ }
+ }
+
+ for (var k = 0; k < updates.length; k++) {
+ update = updates[k];
+ switch (update.type) {
+ case ReactMultiChildUpdateTypes.INSERT_MARKUP:
+ insertChildAt(update.parentNode, renderedMarkup[update.markupIndex], update.toIndex);
+ break;
+ case ReactMultiChildUpdateTypes.MOVE_EXISTING:
+ insertChildAt(update.parentNode, initialChildren[update.parentID][update.fromIndex], update.toIndex);
+ break;
+ case ReactMultiChildUpdateTypes.SET_MARKUP:
+ setInnerHTML(update.parentNode, update.content);
+ break;
+ case ReactMultiChildUpdateTypes.TEXT_CONTENT:
+ setTextContent(update.parentNode, update.content);
+ break;
+ case ReactMultiChildUpdateTypes.REMOVE_NODE:
+ // Already removed by the for-loop above.
+ break;
+ }
+ }
+ }
+
+};
+
+ReactPerf.measureMethods(DOMChildrenOperations, 'DOMChildrenOperations', {
+ updateTextContent: 'updateTextContent'
+});
+
+module.exports = DOMChildrenOperations;
+},{"12":12,"138":138,"139":139,"161":161,"74":74,"78":78}],10:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule DOMProperty
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var invariant = _dereq_(161);
+
+function checkMask(value, bitmask) {
+ return (value & bitmask) === bitmask;
+}
+
+var DOMPropertyInjection = {
+ /**
+ * Mapping from normalized, camelcased property names to a configuration that
+ * specifies how the associated DOM property should be accessed or rendered.
+ */
+ MUST_USE_ATTRIBUTE: 0x1,
+ MUST_USE_PROPERTY: 0x2,
+ HAS_SIDE_EFFECTS: 0x4,
+ HAS_BOOLEAN_VALUE: 0x8,
+ HAS_NUMERIC_VALUE: 0x10,
+ HAS_POSITIVE_NUMERIC_VALUE: 0x20 | 0x10,
+ HAS_OVERLOADED_BOOLEAN_VALUE: 0x40,
+
+ /**
+ * Inject some specialized knowledge about the DOM. This takes a config object
+ * with the following properties:
+ *
+ * isCustomAttribute: function that given an attribute name will return true
+ * if it can be inserted into the DOM verbatim. Useful for data-* or aria-*
+ * attributes where it's impossible to enumerate all of the possible
+ * attribute names,
+ *
+ * Properties: object mapping DOM property name to one of the
+ * DOMPropertyInjection constants or null. If your attribute isn't in here,
+ * it won't get written to the DOM.
+ *
+ * DOMAttributeNames: object mapping React attribute name to the DOM
+ * attribute name. Attribute names not specified use the **lowercase**
+ * normalized name.
+ *
+ * DOMAttributeNamespaces: object mapping React attribute name to the DOM
+ * attribute namespace URL. (Attribute names not specified use no namespace.)
+ *
+ * DOMPropertyNames: similar to DOMAttributeNames but for DOM properties.
+ * Property names not specified use the normalized name.
+ *
+ * DOMMutationMethods: Properties that require special mutation methods. If
+ * `value` is undefined, the mutation method should unset the property.
+ *
+ * @param {object} domPropertyConfig the config as described above.
+ */
+ injectDOMPropertyConfig: function (domPropertyConfig) {
+ var Injection = DOMPropertyInjection;
+ var Properties = domPropertyConfig.Properties || {};
+ var DOMAttributeNamespaces = domPropertyConfig.DOMAttributeNamespaces || {};
+ var DOMAttributeNames = domPropertyConfig.DOMAttributeNames || {};
+ var DOMPropertyNames = domPropertyConfig.DOMPropertyNames || {};
+ var DOMMutationMethods = domPropertyConfig.DOMMutationMethods || {};
+
+ if (domPropertyConfig.isCustomAttribute) {
+ DOMProperty._isCustomAttributeFunctions.push(domPropertyConfig.isCustomAttribute);
+ }
+
+ for (var propName in Properties) {
+ !!DOMProperty.properties.hasOwnProperty(propName) ? "development" !== 'production' ? invariant(false, 'injectDOMPropertyConfig(...): You\'re trying to inject DOM property ' + '\'%s\' which has already been injected. You may be accidentally ' + 'injecting the same DOM property config twice, or you may be ' + 'injecting two configs that have conflicting property names.', propName) : invariant(false) : undefined;
+
+ var lowerCased = propName.toLowerCase();
+ var propConfig = Properties[propName];
+
+ var propertyInfo = {
+ attributeName: lowerCased,
+ attributeNamespace: null,
+ propertyName: propName,
+ mutationMethod: null,
+
+ mustUseAttribute: checkMask(propConfig, Injection.MUST_USE_ATTRIBUTE),
+ mustUseProperty: checkMask(propConfig, Injection.MUST_USE_PROPERTY),
+ hasSideEffects: checkMask(propConfig, Injection.HAS_SIDE_EFFECTS),
+ hasBooleanValue: checkMask(propConfig, Injection.HAS_BOOLEAN_VALUE),
+ hasNumericValue: checkMask(propConfig, Injection.HAS_NUMERIC_VALUE),
+ hasPositiveNumericValue: checkMask(propConfig, Injection.HAS_POSITIVE_NUMERIC_VALUE),
+ hasOverloadedBooleanValue: checkMask(propConfig, Injection.HAS_OVERLOADED_BOOLEAN_VALUE)
+ };
+
+ !(!propertyInfo.mustUseAttribute || !propertyInfo.mustUseProperty) ? "development" !== 'production' ? invariant(false, 'DOMProperty: Cannot require using both attribute and property: %s', propName) : invariant(false) : undefined;
+ !(propertyInfo.mustUseProperty || !propertyInfo.hasSideEffects) ? "development" !== 'production' ? invariant(false, 'DOMProperty: Properties that have side effects must use property: %s', propName) : invariant(false) : undefined;
+ !(propertyInfo.hasBooleanValue + propertyInfo.hasNumericValue + propertyInfo.hasOverloadedBooleanValue <= 1) ? "development" !== 'production' ? invariant(false, 'DOMProperty: Value can be one of boolean, overloaded boolean, or ' + 'numeric value, but not a combination: %s', propName) : invariant(false) : undefined;
+
+ if ("development" !== 'production') {
+ DOMProperty.getPossibleStandardName[lowerCased] = propName;
+ }
+
+ if (DOMAttributeNames.hasOwnProperty(propName)) {
+ var attributeName = DOMAttributeNames[propName];
+ propertyInfo.attributeName = attributeName;
+ if ("development" !== 'production') {
+ DOMProperty.getPossibleStandardName[attributeName] = propName;
+ }
+ }
+
+ if (DOMAttributeNamespaces.hasOwnProperty(propName)) {
+ propertyInfo.attributeNamespace = DOMAttributeNamespaces[propName];
+ }
+
+ if (DOMPropertyNames.hasOwnProperty(propName)) {
+ propertyInfo.propertyName = DOMPropertyNames[propName];
+ }
+
+ if (DOMMutationMethods.hasOwnProperty(propName)) {
+ propertyInfo.mutationMethod = DOMMutationMethods[propName];
+ }
+
+ DOMProperty.properties[propName] = propertyInfo;
+ }
+ }
+};
+var defaultValueCache = {};
+
+/**
+ * DOMProperty exports lookup objects that can be used like functions:
+ *
+ * > DOMProperty.isValid['id']
+ * true
+ * > DOMProperty.isValid['foobar']
+ * undefined
+ *
+ * Although this may be confusing, it performs better in general.
+ *
+ * @see http://jsperf.com/key-exists
+ * @see http://jsperf.com/key-missing
+ */
+var DOMProperty = {
+
+ ID_ATTRIBUTE_NAME: 'data-reactid',
+
+ /**
+ * Map from property "standard name" to an object with info about how to set
+ * the property in the DOM. Each object contains:
+ *
+ * attributeName:
+ * Used when rendering markup or with `*Attribute()`.
+ * attributeNamespace
+ * propertyName:
+ * Used on DOM node instances. (This includes properties that mutate due to
+ * external factors.)
+ * mutationMethod:
+ * If non-null, used instead of the property or `setAttribute()` after
+ * initial render.
+ * mustUseAttribute:
+ * Whether the property must be accessed and mutated using `*Attribute()`.
+ * (This includes anything that fails `<propName> in <element>`.)
+ * mustUseProperty:
+ * Whether the property must be accessed and mutated as an object property.
+ * hasSideEffects:
+ * Whether or not setting a value causes side effects such as triggering
+ * resources to be loaded or text selection changes. If true, we read from
+ * the DOM before updating to ensure that the value is only set if it has
+ * changed.
+ * hasBooleanValue:
+ * Whether the property should be removed when set to a falsey value.
+ * hasNumericValue:
+ * Whether the property must be numeric or parse as a numeric and should be
+ * removed when set to a falsey value.
+ * hasPositiveNumericValue:
+ * Whether the property must be positive numeric or parse as a positive
+ * numeric and should be removed when set to a falsey value.
+ * hasOverloadedBooleanValue:
+ * Whether the property can be used as a flag as well as with a value.
+ * Removed when strictly equal to false; present without a value when
+ * strictly equal to true; present with a value otherwise.
+ */
+ properties: {},
+
+ /**
+ * Mapping from lowercase property names to the properly cased version, used
+ * to warn in the case of missing properties. Available only in __DEV__.
+ * @type {Object}
+ */
+ getPossibleStandardName: "development" !== 'production' ? {} : null,
+
+ /**
+ * All of the isCustomAttribute() functions that have been injected.
+ */
+ _isCustomAttributeFunctions: [],
+
+ /**
+ * Checks whether a property name is a custom attribute.
+ * @method
+ */
+ isCustomAttribute: function (attributeName) {
+ for (var i = 0; i < DOMProperty._isCustomAttributeFunctions.length; i++) {
+ var isCustomAttributeFn = DOMProperty._isCustomAttributeFunctions[i];
+ if (isCustomAttributeFn(attributeName)) {
+ return true;
+ }
+ }
+ return false;
+ },
+
+ /**
+ * Returns the default property value for a DOM property (i.e., not an
+ * attribute). Most default values are '' or false, but not all. Worse yet,
+ * some (in particular, `type`) vary depending on the type of element.
+ *
+ * TODO: Is it better to grab all the possible properties when creating an
+ * element to avoid having to create the same element twice?
+ */
+ getDefaultValueForProperty: function (nodeName, prop) {
+ var nodeDefaults = defaultValueCache[nodeName];
+ var testElement;
+ if (!nodeDefaults) {
+ defaultValueCache[nodeName] = nodeDefaults = {};
+ }
+ if (!(prop in nodeDefaults)) {
+ testElement = document.createElementNS('http://www.w3.org/1999/xhtml', nodeName);
+ nodeDefaults[prop] = testElement[prop];
+ }
+ return nodeDefaults[prop];
+ },
+
+ injection: DOMPropertyInjection
+};
+
+module.exports = DOMProperty;
+},{"161":161}],11:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule DOMPropertyOperations
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var DOMProperty = _dereq_(10);
+var ReactPerf = _dereq_(78);
+
+var quoteAttributeValueForBrowser = _dereq_(136);
+var warning = _dereq_(173);
+
+// Simplified subset
+var VALID_ATTRIBUTE_NAME_REGEX = /^[a-zA-Z_][\w\.\-]*$/;
+var illegalAttributeNameCache = {};
+var validatedAttributeNameCache = {};
+
+function isAttributeNameSafe(attributeName) {
+ if (validatedAttributeNameCache.hasOwnProperty(attributeName)) {
+ return true;
+ }
+ if (illegalAttributeNameCache.hasOwnProperty(attributeName)) {
+ return false;
+ }
+ if (VALID_ATTRIBUTE_NAME_REGEX.test(attributeName)) {
+ validatedAttributeNameCache[attributeName] = true;
+ return true;
+ }
+ illegalAttributeNameCache[attributeName] = true;
+ "development" !== 'production' ? warning(false, 'Invalid attribute name: `%s`', attributeName) : undefined;
+ return false;
+}
+
+function shouldIgnoreValue(propertyInfo, value) {
+ return value == null || propertyInfo.hasBooleanValue && !value || propertyInfo.hasNumericValue && isNaN(value) || propertyInfo.hasPositiveNumericValue && value < 1 || propertyInfo.hasOverloadedBooleanValue && value === false;
+}
+
+if ("development" !== 'production') {
+ var reactProps = {
+ children: true,
+ dangerouslySetInnerHTML: true,
+ key: true,
+ ref: true
+ };
+ var warnedProperties = {};
+
+ var warnUnknownProperty = function (name) {
+ if (reactProps.hasOwnProperty(name) && reactProps[name] || warnedProperties.hasOwnProperty(name) && warnedProperties[name]) {
+ return;
+ }
+
+ warnedProperties[name] = true;
+ var lowerCasedName = name.toLowerCase();
+
+ // data-* attributes should be lowercase; suggest the lowercase version
+ var standardName = DOMProperty.isCustomAttribute(lowerCasedName) ? lowerCasedName : DOMProperty.getPossibleStandardName.hasOwnProperty(lowerCasedName) ? DOMProperty.getPossibleStandardName[lowerCasedName] : null;
+
+ // For now, only warn when we have a suggested correction. This prevents
+ // logging too much when using transferPropsTo.
+ "development" !== 'production' ? warning(standardName == null, 'Unknown DOM property %s. Did you mean %s?', name, standardName) : undefined;
+ };
+}
+
+/**
+ * Operations for dealing with DOM properties.
+ */
+var DOMPropertyOperations = {
+
+ /**
+ * Creates markup for the ID property.
+ *
+ * @param {string} id Unescaped ID.
+ * @return {string} Markup string.
+ */
+ createMarkupForID: function (id) {
+ return DOMProperty.ID_ATTRIBUTE_NAME + '=' + quoteAttributeValueForBrowser(id);
+ },
+
+ setAttributeForID: function (node, id) {
+ node.setAttribute(DOMProperty.ID_ATTRIBUTE_NAME, id);
+ },
+
+ /**
+ * Creates markup for a property.
+ *
+ * @param {string} name
+ * @param {*} value
+ * @return {?string} Markup string, or null if the property was invalid.
+ */
+ createMarkupForProperty: function (name, value) {
+ var propertyInfo = DOMProperty.properties.hasOwnProperty(name) ? DOMProperty.properties[name] : null;
+ if (propertyInfo) {
+ if (shouldIgnoreValue(propertyInfo, value)) {
+ return '';
+ }
+ var attributeName = propertyInfo.attributeName;
+ if (propertyInfo.hasBooleanValue || propertyInfo.hasOverloadedBooleanValue && value === true) {
+ return attributeName + '=""';
+ }
+ return attributeName + '=' + quoteAttributeValueForBrowser(value);
+ } else if (DOMProperty.isCustomAttribute(name)) {
+ if (value == null) {
+ return '';
+ }
+ return name + '=' + quoteAttributeValueForBrowser(value);
+ } else if ("development" !== 'production') {
+ warnUnknownProperty(name);
+ }
+ return null;
+ },
+
+ /**
+ * Creates markup for a custom property.
+ *
+ * @param {string} name
+ * @param {*} value
+ * @return {string} Markup string, or empty string if the property was invalid.
+ */
+ createMarkupForCustomAttribute: function (name, value) {
+ if (!isAttributeNameSafe(name) || value == null) {
+ return '';
+ }
+ return name + '=' + quoteAttributeValueForBrowser(value);
+ },
+
+ /**
+ * Sets the value for a property on a node.
+ *
+ * @param {DOMElement} node
+ * @param {string} name
+ * @param {*} value
+ */
+ setValueForProperty: function (node, name, value) {
+ var propertyInfo = DOMProperty.properties.hasOwnProperty(name) ? DOMProperty.properties[name] : null;
+ if (propertyInfo) {
+ var mutationMethod = propertyInfo.mutationMethod;
+ if (mutationMethod) {
+ mutationMethod(node, value);
+ } else if (shouldIgnoreValue(propertyInfo, value)) {
+ this.deleteValueForProperty(node, name);
+ } else if (propertyInfo.mustUseAttribute) {
+ var attributeName = propertyInfo.attributeName;
+ var namespace = propertyInfo.attributeNamespace;
+ // `setAttribute` with objects becomes only `[object]` in IE8/9,
+ // ('' + value) makes it output the correct toString()-value.
+ if (namespace) {
+ node.setAttributeNS(namespace, attributeName, '' + value);
+ } else if (propertyInfo.hasBooleanValue || propertyInfo.hasOverloadedBooleanValue && value === true) {
+ node.setAttribute(attributeName, '');
+ } else {
+ node.setAttribute(attributeName, '' + value);
+ }
+ } else {
+ var propName = propertyInfo.propertyName;
+ // Must explicitly cast values for HAS_SIDE_EFFECTS-properties to the
+ // property type before comparing; only `value` does and is string.
+ if (!propertyInfo.hasSideEffects || '' + node[propName] !== '' + value) {
+ // Contrary to `setAttribute`, object properties are properly
+ // `toString`ed by IE8/9.
+ node[propName] = value;
+ }
+ }
+ } else if (DOMProperty.isCustomAttribute(name)) {
+ DOMPropertyOperations.setValueForAttribute(node, name, value);
+ } else if ("development" !== 'production') {
+ warnUnknownProperty(name);
+ }
+ },
+
+ setValueForAttribute: function (node, name, value) {
+ if (!isAttributeNameSafe(name)) {
+ return;
+ }
+ if (value == null) {
+ node.removeAttribute(name);
+ } else {
+ node.setAttribute(name, '' + value);
+ }
+ },
+
+ /**
+ * Deletes the value for a property on a node.
+ *
+ * @param {DOMElement} node
+ * @param {string} name
+ */
+ deleteValueForProperty: function (node, name) {
+ var propertyInfo = DOMProperty.properties.hasOwnProperty(name) ? DOMProperty.properties[name] : null;
+ if (propertyInfo) {
+ var mutationMethod = propertyInfo.mutationMethod;
+ if (mutationMethod) {
+ mutationMethod(node, undefined);
+ } else if (propertyInfo.mustUseAttribute) {
+ node.removeAttribute(propertyInfo.attributeName);
+ } else {
+ var propName = propertyInfo.propertyName;
+ var defaultValue = DOMProperty.getDefaultValueForProperty(node.nodeName, propName);
+ if (!propertyInfo.hasSideEffects || '' + node[propName] !== defaultValue) {
+ node[propName] = defaultValue;
+ }
+ }
+ } else if (DOMProperty.isCustomAttribute(name)) {
+ node.removeAttribute(name);
+ } else if ("development" !== 'production') {
+ warnUnknownProperty(name);
+ }
+ }
+
+};
+
+ReactPerf.measureMethods(DOMPropertyOperations, 'DOMPropertyOperations', {
+ setValueForProperty: 'setValueForProperty',
+ setValueForAttribute: 'setValueForAttribute',
+ deleteValueForProperty: 'deleteValueForProperty'
+});
+
+module.exports = DOMPropertyOperations;
+},{"10":10,"136":136,"173":173,"78":78}],12:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule Danger
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var ExecutionEnvironment = _dereq_(147);
+
+var createNodesFromMarkup = _dereq_(152);
+var emptyFunction = _dereq_(153);
+var getMarkupWrap = _dereq_(157);
+var invariant = _dereq_(161);
+
+var OPEN_TAG_NAME_EXP = /^(<[^ \/>]+)/;
+var RESULT_INDEX_ATTR = 'data-danger-index';
+
+/**
+ * Extracts the `nodeName` from a string of markup.
+ *
+ * NOTE: Extracting the `nodeName` does not require a regular expression match
+ * because we make assumptions about React-generated markup (i.e. there are no
+ * spaces surrounding the opening tag and there is at least one attribute).
+ *
+ * @param {string} markup String of markup.
+ * @return {string} Node name of the supplied markup.
+ * @see http://jsperf.com/extract-nodename
+ */
+function getNodeName(markup) {
+ return markup.substring(1, markup.indexOf(' '));
+}
+
+var Danger = {
+
+ /**
+ * Renders markup into an array of nodes. The markup is expected to render
+ * into a list of root nodes. Also, the length of `resultList` and
+ * `markupList` should be the same.
+ *
+ * @param {array<string>} markupList List of markup strings to render.
+ * @return {array<DOMElement>} List of rendered nodes.
+ * @internal
+ */
+ dangerouslyRenderMarkup: function (markupList) {
+ !ExecutionEnvironment.canUseDOM ? "development" !== 'production' ? invariant(false, 'dangerouslyRenderMarkup(...): Cannot render markup in a worker ' + 'thread. Make sure `window` and `document` are available globally ' + 'before requiring React when unit testing or use ' + 'ReactDOMServer.renderToString for server rendering.') : invariant(false) : undefined;
+ var nodeName;
+ var markupByNodeName = {};
+ // Group markup by `nodeName` if a wrap is necessary, else by '*'.
+ for (var i = 0; i < markupList.length; i++) {
+ !markupList[i] ? "development" !== 'production' ? invariant(false, 'dangerouslyRenderMarkup(...): Missing markup.') : invariant(false) : undefined;
+ nodeName = getNodeName(markupList[i]);
+ nodeName = getMarkupWrap(nodeName) ? nodeName : '*';
+ markupByNodeName[nodeName] = markupByNodeName[nodeName] || [];
+ markupByNodeName[nodeName][i] = markupList[i];
+ }
+ var resultList = [];
+ var resultListAssignmentCount = 0;
+ for (nodeName in markupByNodeName) {
+ if (!markupByNodeName.hasOwnProperty(nodeName)) {
+ continue;
+ }
+ var markupListByNodeName = markupByNodeName[nodeName];
+
+ // This for-in loop skips the holes of the sparse array. The order of
+ // iteration should follow the order of assignment, which happens to match
+ // numerical index order, but we don't rely on that.
+ var resultIndex;
+ for (resultIndex in markupListByNodeName) {
+ if (markupListByNodeName.hasOwnProperty(resultIndex)) {
+ var markup = markupListByNodeName[resultIndex];
+
+ // Push the requested markup with an additional RESULT_INDEX_ATTR
+ // attribute. If the markup does not start with a < character, it
+ // will be discarded below (with an appropriate console.error).
+ markupListByNodeName[resultIndex] = markup.replace(OPEN_TAG_NAME_EXP,
+ // This index will be parsed back out below.
+ '$1 ' + RESULT_INDEX_ATTR + '="' + resultIndex + '" ');
+ }
+ }
+
+ // Render each group of markup with similar wrapping `nodeName`.
+ var renderNodes = createNodesFromMarkup(markupListByNodeName.join(''), emptyFunction // Do nothing special with <script> tags.
+ );
+
+ for (var j = 0; j < renderNodes.length; ++j) {
+ var renderNode = renderNodes[j];
+ if (renderNode.hasAttribute && renderNode.hasAttribute(RESULT_INDEX_ATTR)) {
+
+ resultIndex = +renderNode.getAttribute(RESULT_INDEX_ATTR);
+ renderNode.removeAttribute(RESULT_INDEX_ATTR);
+
+ !!resultList.hasOwnProperty(resultIndex) ? "development" !== 'production' ? invariant(false, 'Danger: Assigning to an already-occupied result index.') : invariant(false) : undefined;
+
+ resultList[resultIndex] = renderNode;
+
+ // This should match resultList.length and markupList.length when
+ // we're done.
+ resultListAssignmentCount += 1;
+ } else if ("development" !== 'production') {
+ console.error('Danger: Discarding unexpected node:', renderNode);
+ }
+ }
+ }
+
+ // Although resultList was populated out of order, it should now be a dense
+ // array.
+ !(resultListAssignmentCount === resultList.length) ? "development" !== 'production' ? invariant(false, 'Danger: Did not assign to every index of resultList.') : invariant(false) : undefined;
+
+ !(resultList.length === markupList.length) ? "development" !== 'production' ? invariant(false, 'Danger: Expected markup to render %s nodes, but rendered %s.', markupList.length, resultList.length) : invariant(false) : undefined;
+
+ return resultList;
+ },
+
+ /**
+ * Replaces a node with a string of markup at its current position within its
+ * parent. The markup must render into a single root node.
+ *
+ * @param {DOMElement} oldChild Child node to replace.
+ * @param {string} markup Markup to render in place of the child node.
+ * @internal
+ */
+ dangerouslyReplaceNodeWithMarkup: function (oldChild, markup) {
+ !ExecutionEnvironment.canUseDOM ? "development" !== 'production' ? invariant(false, 'dangerouslyReplaceNodeWithMarkup(...): Cannot render markup in a ' + 'worker thread. Make sure `window` and `document` are available ' + 'globally before requiring React when unit testing or use ' + 'ReactDOMServer.renderToString() for server rendering.') : invariant(false) : undefined;
+ !markup ? "development" !== 'production' ? invariant(false, 'dangerouslyReplaceNodeWithMarkup(...): Missing markup.') : invariant(false) : undefined;
+ !(oldChild.tagName.toLowerCase() !== 'html') ? "development" !== 'production' ? invariant(false, 'dangerouslyReplaceNodeWithMarkup(...): Cannot replace markup of the ' + '<html> node. This is because browser quirks make this unreliable ' + 'and/or slow. If you want to render to the root you must use ' + 'server rendering. See ReactDOMServer.renderToString().') : invariant(false) : undefined;
+
+ var newChild;
+ if (typeof markup === 'string') {
+ newChild = createNodesFromMarkup(markup, emptyFunction)[0];
+ } else {
+ newChild = markup;
+ }
+ oldChild.parentNode.replaceChild(newChild, oldChild);
+ }
+
+};
+
+module.exports = Danger;
+},{"147":147,"152":152,"153":153,"157":157,"161":161}],13:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule DefaultEventPluginOrder
+ */
+
+'use strict';
+
+var keyOf = _dereq_(166);
+
+/**
+ * Module that is injectable into `EventPluginHub`, that specifies a
+ * deterministic ordering of `EventPlugin`s. A convenient way to reason about
+ * plugins, without having to package every one of them. This is better than
+ * having plugins be ordered in the same order that they are injected because
+ * that ordering would be influenced by the packaging order.
+ * `ResponderEventPlugin` must occur before `SimpleEventPlugin` so that
+ * preventing default on events is convenient in `SimpleEventPlugin` handlers.
+ */
+var DefaultEventPluginOrder = [keyOf({ ResponderEventPlugin: null }), keyOf({ SimpleEventPlugin: null }), keyOf({ TapEventPlugin: null }), keyOf({ EnterLeaveEventPlugin: null }), keyOf({ ChangeEventPlugin: null }), keyOf({ SelectEventPlugin: null }), keyOf({ BeforeInputEventPlugin: null })];
+
+module.exports = DefaultEventPluginOrder;
+},{"166":166}],14:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule EnterLeaveEventPlugin
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var EventConstants = _dereq_(15);
+var EventPropagators = _dereq_(19);
+var SyntheticMouseEvent = _dereq_(109);
+
+var ReactMount = _dereq_(72);
+var keyOf = _dereq_(166);
+
+var topLevelTypes = EventConstants.topLevelTypes;
+var getFirstReactDOM = ReactMount.getFirstReactDOM;
+
+var eventTypes = {
+ mouseEnter: {
+ registrationName: keyOf({ onMouseEnter: null }),
+ dependencies: [topLevelTypes.topMouseOut, topLevelTypes.topMouseOver]
+ },
+ mouseLeave: {
+ registrationName: keyOf({ onMouseLeave: null }),
+ dependencies: [topLevelTypes.topMouseOut, topLevelTypes.topMouseOver]
+ }
+};
+
+var extractedEvents = [null, null];
+
+var EnterLeaveEventPlugin = {
+
+ eventTypes: eventTypes,
+
+ /**
+ * For almost every interaction we care about, there will be both a top-level
+ * `mouseover` and `mouseout` event that occurs. Only use `mouseout` so that
+ * we do not extract duplicate events. However, moving the mouse into the
+ * browser from outside will not fire a `mouseout` event. In this case, we use
+ * the `mouseover` top-level event.
+ *
+ * @param {string} topLevelType Record from `EventConstants`.
+ * @param {DOMEventTarget} topLevelTarget The listening component root node.
+ * @param {string} topLevelTargetID ID of `topLevelTarget`.
+ * @param {object} nativeEvent Native browser event.
+ * @return {*} An accumulation of synthetic events.
+ * @see {EventPluginHub.extractEvents}
+ */
+ extractEvents: function (topLevelType, topLevelTarget, topLevelTargetID, nativeEvent, nativeEventTarget) {
+ if (topLevelType === topLevelTypes.topMouseOver && (nativeEvent.relatedTarget || nativeEvent.fromElement)) {
+ return null;
+ }
+ if (topLevelType !== topLevelTypes.topMouseOut && topLevelType !== topLevelTypes.topMouseOver) {
+ // Must not be a mouse in or mouse out - ignoring.
+ return null;
+ }
+
+ var win;
+ if (topLevelTarget.window === topLevelTarget) {
+ // `topLevelTarget` is probably a window object.
+ win = topLevelTarget;
+ } else {
+ // TODO: Figure out why `ownerDocument` is sometimes undefined in IE8.
+ var doc = topLevelTarget.ownerDocument;
+ if (doc) {
+ win = doc.defaultView || doc.parentWindow;
+ } else {
+ win = window;
+ }
+ }
+
+ var from;
+ var to;
+ var fromID = '';
+ var toID = '';
+ if (topLevelType === topLevelTypes.topMouseOut) {
+ from = topLevelTarget;
+ fromID = topLevelTargetID;
+ to = getFirstReactDOM(nativeEvent.relatedTarget || nativeEvent.toElement);
+ if (to) {
+ toID = ReactMount.getID(to);
+ } else {
+ to = win;
+ }
+ to = to || win;
+ } else {
+ from = win;
+ to = topLevelTarget;
+ toID = topLevelTargetID;
+ }
+
+ if (from === to) {
+ // Nothing pertains to our managed components.
+ return null;
+ }
+
+ var leave = SyntheticMouseEvent.getPooled(eventTypes.mouseLeave, fromID, nativeEvent, nativeEventTarget);
+ leave.type = 'mouseleave';
+ leave.target = from;
+ leave.relatedTarget = to;
+
+ var enter = SyntheticMouseEvent.getPooled(eventTypes.mouseEnter, toID, nativeEvent, nativeEventTarget);
+ enter.type = 'mouseenter';
+ enter.target = to;
+ enter.relatedTarget = from;
+
+ EventPropagators.accumulateEnterLeaveDispatches(leave, enter, fromID, toID);
+
+ extractedEvents[0] = leave;
+ extractedEvents[1] = enter;
+
+ return extractedEvents;
+ }
+
+};
+
+module.exports = EnterLeaveEventPlugin;
+},{"109":109,"15":15,"166":166,"19":19,"72":72}],15:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule EventConstants
+ */
+
+'use strict';
+
+var keyMirror = _dereq_(165);
+
+var PropagationPhases = keyMirror({ bubbled: null, captured: null });
+
+/**
+ * Types of raw signals from the browser caught at the top level.
+ */
+var topLevelTypes = keyMirror({
+ topAbort: null,
+ topBlur: null,
+ topCanPlay: null,
+ topCanPlayThrough: null,
+ topChange: null,
+ topClick: null,
+ topCompositionEnd: null,
+ topCompositionStart: null,
+ topCompositionUpdate: null,
+ topContextMenu: null,
+ topCopy: null,
+ topCut: null,
+ topDoubleClick: null,
+ topDrag: null,
+ topDragEnd: null,
+ topDragEnter: null,
+ topDragExit: null,
+ topDragLeave: null,
+ topDragOver: null,
+ topDragStart: null,
+ topDrop: null,
+ topDurationChange: null,
+ topEmptied: null,
+ topEncrypted: null,
+ topEnded: null,
+ topError: null,
+ topFocus: null,
+ topInput: null,
+ topKeyDown: null,
+ topKeyPress: null,
+ topKeyUp: null,
+ topLoad: null,
+ topLoadedData: null,
+ topLoadedMetadata: null,
+ topLoadStart: null,
+ topMouseDown: null,
+ topMouseMove: null,
+ topMouseOut: null,
+ topMouseOver: null,
+ topMouseUp: null,
+ topPaste: null,
+ topPause: null,
+ topPlay: null,
+ topPlaying: null,
+ topProgress: null,
+ topRateChange: null,
+ topReset: null,
+ topScroll: null,
+ topSeeked: null,
+ topSeeking: null,
+ topSelectionChange: null,
+ topStalled: null,
+ topSubmit: null,
+ topSuspend: null,
+ topTextInput: null,
+ topTimeUpdate: null,
+ topTouchCancel: null,
+ topTouchEnd: null,
+ topTouchMove: null,
+ topTouchStart: null,
+ topVolumeChange: null,
+ topWaiting: null,
+ topWheel: null
+});
+
+var EventConstants = {
+ topLevelTypes: topLevelTypes,
+ PropagationPhases: PropagationPhases
+};
+
+module.exports = EventConstants;
+},{"165":165}],16:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule EventPluginHub
+ */
+
+'use strict';
+
+var EventPluginRegistry = _dereq_(17);
+var EventPluginUtils = _dereq_(18);
+var ReactErrorUtils = _dereq_(61);
+
+var accumulateInto = _dereq_(115);
+var forEachAccumulated = _dereq_(124);
+var invariant = _dereq_(161);
+var warning = _dereq_(173);
+
+/**
+ * Internal store for event listeners
+ */
+var listenerBank = {};
+
+/**
+ * Internal queue of events that have accumulated their dispatches and are
+ * waiting to have their dispatches executed.
+ */
+var eventQueue = null;
+
+/**
+ * Dispatches an event and releases it back into the pool, unless persistent.
+ *
+ * @param {?object} event Synthetic event to be dispatched.
+ * @param {boolean} simulated If the event is simulated (changes exn behavior)
+ * @private
+ */
+var executeDispatchesAndRelease = function (event, simulated) {
+ if (event) {
+ EventPluginUtils.executeDispatchesInOrder(event, simulated);
+
+ if (!event.isPersistent()) {
+ event.constructor.release(event);
+ }
+ }
+};
+var executeDispatchesAndReleaseSimulated = function (e) {
+ return executeDispatchesAndRelease(e, true);
+};
+var executeDispatchesAndReleaseTopLevel = function (e) {
+ return executeDispatchesAndRelease(e, false);
+};
+
+/**
+ * - `InstanceHandle`: [required] Module that performs logical traversals of DOM
+ * hierarchy given ids of the logical DOM elements involved.
+ */
+var InstanceHandle = null;
+
+function validateInstanceHandle() {
+ var valid = InstanceHandle && InstanceHandle.traverseTwoPhase && InstanceHandle.traverseEnterLeave;
+ "development" !== 'production' ? warning(valid, 'InstanceHandle not injected before use!') : undefined;
+}
+
+/**
+ * This is a unified interface for event plugins to be installed and configured.
+ *
+ * Event plugins can implement the following properties:
+ *
+ * `extractEvents` {function(string, DOMEventTarget, string, object): *}
+ * Required. When a top-level event is fired, this method is expected to
+ * extract synthetic events that will in turn be queued and dispatched.
+ *
+ * `eventTypes` {object}
+ * Optional, plugins that fire events must publish a mapping of registration
+ * names that are used to register listeners. Values of this mapping must
+ * be objects that contain `registrationName` or `phasedRegistrationNames`.
+ *
+ * `executeDispatch` {function(object, function, string)}
+ * Optional, allows plugins to override how an event gets dispatched. By
+ * default, the listener is simply invoked.
+ *
+ * Each plugin that is injected into `EventsPluginHub` is immediately operable.
+ *
+ * @public
+ */
+var EventPluginHub = {
+
+ /**
+ * Methods for injecting dependencies.
+ */
+ injection: {
+
+ /**
+ * @param {object} InjectedMount
+ * @public
+ */
+ injectMount: EventPluginUtils.injection.injectMount,
+
+ /**
+ * @param {object} InjectedInstanceHandle
+ * @public
+ */
+ injectInstanceHandle: function (InjectedInstanceHandle) {
+ InstanceHandle = InjectedInstanceHandle;
+ if ("development" !== 'production') {
+ validateInstanceHandle();
+ }
+ },
+
+ getInstanceHandle: function () {
+ if ("development" !== 'production') {
+ validateInstanceHandle();
+ }
+ return InstanceHandle;
+ },
+
+ /**
+ * @param {array} InjectedEventPluginOrder
+ * @public
+ */
+ injectEventPluginOrder: EventPluginRegistry.injectEventPluginOrder,
+
+ /**
+ * @param {object} injectedNamesToPlugins Map from names to plugin modules.
+ */
+ injectEventPluginsByName: EventPluginRegistry.injectEventPluginsByName
+
+ },
+
+ eventNameDispatchConfigs: EventPluginRegistry.eventNameDispatchConfigs,
+
+ registrationNameModules: EventPluginRegistry.registrationNameModules,
+
+ /**
+ * Stores `listener` at `listenerBank[registrationName][id]`. Is idempotent.
+ *
+ * @param {string} id ID of the DOM element.
+ * @param {string} registrationName Name of listener (e.g. `onClick`).
+ * @param {?function} listener The callback to store.
+ */
+ putListener: function (id, registrationName, listener) {
+ !(typeof listener === 'function') ? "development" !== 'production' ? invariant(false, 'Expected %s listener to be a function, instead got type %s', registrationName, typeof listener) : invariant(false) : undefined;
+
+ var bankForRegistrationName = listenerBank[registrationName] || (listenerBank[registrationName] = {});
+ bankForRegistrationName[id] = listener;
+
+ var PluginModule = EventPluginRegistry.registrationNameModules[registrationName];
+ if (PluginModule && PluginModule.didPutListener) {
+ PluginModule.didPutListener(id, registrationName, listener);
+ }
+ },
+
+ /**
+ * @param {string} id ID of the DOM element.
+ * @param {string} registrationName Name of listener (e.g. `onClick`).
+ * @return {?function} The stored callback.
+ */
+ getListener: function (id, registrationName) {
+ var bankForRegistrationName = listenerBank[registrationName];
+ return bankForRegistrationName && bankForRegistrationName[id];
+ },
+
+ /**
+ * Deletes a listener from the registration bank.
+ *
+ * @param {string} id ID of the DOM element.
+ * @param {string} registrationName Name of listener (e.g. `onClick`).
+ */
+ deleteListener: function (id, registrationName) {
+ var PluginModule = EventPluginRegistry.registrationNameModules[registrationName];
+ if (PluginModule && PluginModule.willDeleteListener) {
+ PluginModule.willDeleteListener(id, registrationName);
+ }
+
+ var bankForRegistrationName = listenerBank[registrationName];
+ // TODO: This should never be null -- when is it?
+ if (bankForRegistrationName) {
+ delete bankForRegistrationName[id];
+ }
+ },
+
+ /**
+ * Deletes all listeners for the DOM element with the supplied ID.
+ *
+ * @param {string} id ID of the DOM element.
+ */
+ deleteAllListeners: function (id) {
+ for (var registrationName in listenerBank) {
+ if (!listenerBank[registrationName][id]) {
+ continue;
+ }
+
+ var PluginModule = EventPluginRegistry.registrationNameModules[registrationName];
+ if (PluginModule && PluginModule.willDeleteListener) {
+ PluginModule.willDeleteListener(id, registrationName);
+ }
+
+ delete listenerBank[registrationName][id];
+ }
+ },
+
+ /**
+ * Allows registered plugins an opportunity to extract events from top-level
+ * native browser events.
+ *
+ * @param {string} topLevelType Record from `EventConstants`.
+ * @param {DOMEventTarget} topLevelTarget The listening component root node.
+ * @param {string} topLevelTargetID ID of `topLevelTarget`.
+ * @param {object} nativeEvent Native browser event.
+ * @return {*} An accumulation of synthetic events.
+ * @internal
+ */
+ extractEvents: function (topLevelType, topLevelTarget, topLevelTargetID, nativeEvent, nativeEventTarget) {
+ var events;
+ var plugins = EventPluginRegistry.plugins;
+ for (var i = 0; i < plugins.length; i++) {
+ // Not every plugin in the ordering may be loaded at runtime.
+ var possiblePlugin = plugins[i];
+ if (possiblePlugin) {
+ var extractedEvents = possiblePlugin.extractEvents(topLevelType, topLevelTarget, topLevelTargetID, nativeEvent, nativeEventTarget);
+ if (extractedEvents) {
+ events = accumulateInto(events, extractedEvents);
+ }
+ }
+ }
+ return events;
+ },
+
+ /**
+ * Enqueues a synthetic event that should be dispatched when
+ * `processEventQueue` is invoked.
+ *
+ * @param {*} events An accumulation of synthetic events.
+ * @internal
+ */
+ enqueueEvents: function (events) {
+ if (events) {
+ eventQueue = accumulateInto(eventQueue, events);
+ }
+ },
+
+ /**
+ * Dispatches all synthetic events on the event queue.
+ *
+ * @internal
+ */
+ processEventQueue: function (simulated) {
+ // Set `eventQueue` to null before processing it so that we can tell if more
+ // events get enqueued while processing.
+ var processingEventQueue = eventQueue;
+ eventQueue = null;
+ if (simulated) {
+ forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseSimulated);
+ } else {
+ forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel);
+ }
+ !!eventQueue ? "development" !== 'production' ? invariant(false, 'processEventQueue(): Additional events were enqueued while processing ' + 'an event queue. Support for this has not yet been implemented.') : invariant(false) : undefined;
+ // This would be a good time to rethrow if any of the event handlers threw.
+ ReactErrorUtils.rethrowCaughtError();
+ },
+
+ /**
+ * These are needed for tests only. Do not use!
+ */
+ __purge: function () {
+ listenerBank = {};
+ },
+
+ __getListenerBank: function () {
+ return listenerBank;
+ }
+
+};
+
+module.exports = EventPluginHub;
+},{"115":115,"124":124,"161":161,"17":17,"173":173,"18":18,"61":61}],17:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule EventPluginRegistry
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var invariant = _dereq_(161);
+
+/**
+ * Injectable ordering of event plugins.
+ */
+var EventPluginOrder = null;
+
+/**
+ * Injectable mapping from names to event plugin modules.
+ */
+var namesToPlugins = {};
+
+/**
+ * Recomputes the plugin list using the injected plugins and plugin ordering.
+ *
+ * @private
+ */
+function recomputePluginOrdering() {
+ if (!EventPluginOrder) {
+ // Wait until an `EventPluginOrder` is injected.
+ return;
+ }
+ for (var pluginName in namesToPlugins) {
+ var PluginModule = namesToPlugins[pluginName];
+ var pluginIndex = EventPluginOrder.indexOf(pluginName);
+ !(pluginIndex > -1) ? "development" !== 'production' ? invariant(false, 'EventPluginRegistry: Cannot inject event plugins that do not exist in ' + 'the plugin ordering, `%s`.', pluginName) : invariant(false) : undefined;
+ if (EventPluginRegistry.plugins[pluginIndex]) {
+ continue;
+ }
+ !PluginModule.extractEvents ? "development" !== 'production' ? invariant(false, 'EventPluginRegistry: Event plugins must implement an `extractEvents` ' + 'method, but `%s` does not.', pluginName) : invariant(false) : undefined;
+ EventPluginRegistry.plugins[pluginIndex] = PluginModule;
+ var publishedEvents = PluginModule.eventTypes;
+ for (var eventName in publishedEvents) {
+ !publishEventForPlugin(publishedEvents[eventName], PluginModule, eventName) ? "development" !== 'production' ? invariant(false, 'EventPluginRegistry: Failed to publish event `%s` for plugin `%s`.', eventName, pluginName) : invariant(false) : undefined;
+ }
+ }
+}
+
+/**
+ * Publishes an event so that it can be dispatched by the supplied plugin.
+ *
+ * @param {object} dispatchConfig Dispatch configuration for the event.
+ * @param {object} PluginModule Plugin publishing the event.
+ * @return {boolean} True if the event was successfully published.
+ * @private
+ */
+function publishEventForPlugin(dispatchConfig, PluginModule, eventName) {
+ !!EventPluginRegistry.eventNameDispatchConfigs.hasOwnProperty(eventName) ? "development" !== 'production' ? invariant(false, 'EventPluginHub: More than one plugin attempted to publish the same ' + 'event name, `%s`.', eventName) : invariant(false) : undefined;
+ EventPluginRegistry.eventNameDispatchConfigs[eventName] = dispatchConfig;
+
+ var phasedRegistrationNames = dispatchConfig.phasedRegistrationNames;
+ if (phasedRegistrationNames) {
+ for (var phaseName in phasedRegistrationNames) {
+ if (phasedRegistrationNames.hasOwnProperty(phaseName)) {
+ var phasedRegistrationName = phasedRegistrationNames[phaseName];
+ publishRegistrationName(phasedRegistrationName, PluginModule, eventName);
+ }
+ }
+ return true;
+ } else if (dispatchConfig.registrationName) {
+ publishRegistrationName(dispatchConfig.registrationName, PluginModule, eventName);
+ return true;
+ }
+ return false;
+}
+
+/**
+ * Publishes a registration name that is used to identify dispatched events and
+ * can be used with `EventPluginHub.putListener` to register listeners.
+ *
+ * @param {string} registrationName Registration name to add.
+ * @param {object} PluginModule Plugin publishing the event.
+ * @private
+ */
+function publishRegistrationName(registrationName, PluginModule, eventName) {
+ !!EventPluginRegistry.registrationNameModules[registrationName] ? "development" !== 'production' ? invariant(false, 'EventPluginHub: More than one plugin attempted to publish the same ' + 'registration name, `%s`.', registrationName) : invariant(false) : undefined;
+ EventPluginRegistry.registrationNameModules[registrationName] = PluginModule;
+ EventPluginRegistry.registrationNameDependencies[registrationName] = PluginModule.eventTypes[eventName].dependencies;
+}
+
+/**
+ * Registers plugins so that they can extract and dispatch events.
+ *
+ * @see {EventPluginHub}
+ */
+var EventPluginRegistry = {
+
+ /**
+ * Ordered list of injected plugins.
+ */
+ plugins: [],
+
+ /**
+ * Mapping from event name to dispatch config
+ */
+ eventNameDispatchConfigs: {},
+
+ /**
+ * Mapping from registration name to plugin module
+ */
+ registrationNameModules: {},
+
+ /**
+ * Mapping from registration name to event name
+ */
+ registrationNameDependencies: {},
+
+ /**
+ * Injects an ordering of plugins (by plugin name). This allows the ordering
+ * to be decoupled from injection of the actual plugins so that ordering is
+ * always deterministic regardless of packaging, on-the-fly injection, etc.
+ *
+ * @param {array} InjectedEventPluginOrder
+ * @internal
+ * @see {EventPluginHub.injection.injectEventPluginOrder}
+ */
+ injectEventPluginOrder: function (InjectedEventPluginOrder) {
+ !!EventPluginOrder ? "development" !== 'production' ? invariant(false, 'EventPluginRegistry: Cannot inject event plugin ordering more than ' + 'once. You are likely trying to load more than one copy of React.') : invariant(false) : undefined;
+ // Clone the ordering so it cannot be dynamically mutated.
+ EventPluginOrder = Array.prototype.slice.call(InjectedEventPluginOrder);
+ recomputePluginOrdering();
+ },
+
+ /**
+ * Injects plugins to be used by `EventPluginHub`. The plugin names must be
+ * in the ordering injected by `injectEventPluginOrder`.
+ *
+ * Plugins can be injected as part of page initialization or on-the-fly.
+ *
+ * @param {object} injectedNamesToPlugins Map from names to plugin modules.
+ * @internal
+ * @see {EventPluginHub.injection.injectEventPluginsByName}
+ */
+ injectEventPluginsByName: function (injectedNamesToPlugins) {
+ var isOrderingDirty = false;
+ for (var pluginName in injectedNamesToPlugins) {
+ if (!injectedNamesToPlugins.hasOwnProperty(pluginName)) {
+ continue;
+ }
+ var PluginModule = injectedNamesToPlugins[pluginName];
+ if (!namesToPlugins.hasOwnProperty(pluginName) || namesToPlugins[pluginName] !== PluginModule) {
+ !!namesToPlugins[pluginName] ? "development" !== 'production' ? invariant(false, 'EventPluginRegistry: Cannot inject two different event plugins ' + 'using the same name, `%s`.', pluginName) : invariant(false) : undefined;
+ namesToPlugins[pluginName] = PluginModule;
+ isOrderingDirty = true;
+ }
+ }
+ if (isOrderingDirty) {
+ recomputePluginOrdering();
+ }
+ },
+
+ /**
+ * Looks up the plugin for the supplied event.
+ *
+ * @param {object} event A synthetic event.
+ * @return {?object} The plugin that created the supplied event.
+ * @internal
+ */
+ getPluginModuleForEvent: function (event) {
+ var dispatchConfig = event.dispatchConfig;
+ if (dispatchConfig.registrationName) {
+ return EventPluginRegistry.registrationNameModules[dispatchConfig.registrationName] || null;
+ }
+ for (var phase in dispatchConfig.phasedRegistrationNames) {
+ if (!dispatchConfig.phasedRegistrationNames.hasOwnProperty(phase)) {
+ continue;
+ }
+ var PluginModule = EventPluginRegistry.registrationNameModules[dispatchConfig.phasedRegistrationNames[phase]];
+ if (PluginModule) {
+ return PluginModule;
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Exposed for unit testing.
+ * @private
+ */
+ _resetEventPlugins: function () {
+ EventPluginOrder = null;
+ for (var pluginName in namesToPlugins) {
+ if (namesToPlugins.hasOwnProperty(pluginName)) {
+ delete namesToPlugins[pluginName];
+ }
+ }
+ EventPluginRegistry.plugins.length = 0;
+
+ var eventNameDispatchConfigs = EventPluginRegistry.eventNameDispatchConfigs;
+ for (var eventName in eventNameDispatchConfigs) {
+ if (eventNameDispatchConfigs.hasOwnProperty(eventName)) {
+ delete eventNameDispatchConfigs[eventName];
+ }
+ }
+
+ var registrationNameModules = EventPluginRegistry.registrationNameModules;
+ for (var registrationName in registrationNameModules) {
+ if (registrationNameModules.hasOwnProperty(registrationName)) {
+ delete registrationNameModules[registrationName];
+ }
+ }
+ }
+
+};
+
+module.exports = EventPluginRegistry;
+},{"161":161}],18:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule EventPluginUtils
+ */
+
+'use strict';
+
+var EventConstants = _dereq_(15);
+var ReactErrorUtils = _dereq_(61);
+
+var invariant = _dereq_(161);
+var warning = _dereq_(173);
+
+/**
+ * Injected dependencies:
+ */
+
+/**
+ * - `Mount`: [required] Module that can convert between React dom IDs and
+ * actual node references.
+ */
+var injection = {
+ Mount: null,
+ injectMount: function (InjectedMount) {
+ injection.Mount = InjectedMount;
+ if ("development" !== 'production') {
+ "development" !== 'production' ? warning(InjectedMount && InjectedMount.getNode && InjectedMount.getID, 'EventPluginUtils.injection.injectMount(...): Injected Mount ' + 'module is missing getNode or getID.') : undefined;
+ }
+ }
+};
+
+var topLevelTypes = EventConstants.topLevelTypes;
+
+function isEndish(topLevelType) {
+ return topLevelType === topLevelTypes.topMouseUp || topLevelType === topLevelTypes.topTouchEnd || topLevelType === topLevelTypes.topTouchCancel;
+}
+
+function isMoveish(topLevelType) {
+ return topLevelType === topLevelTypes.topMouseMove || topLevelType === topLevelTypes.topTouchMove;
+}
+function isStartish(topLevelType) {
+ return topLevelType === topLevelTypes.topMouseDown || topLevelType === topLevelTypes.topTouchStart;
+}
+
+var validateEventDispatches;
+if ("development" !== 'production') {
+ validateEventDispatches = function (event) {
+ var dispatchListeners = event._dispatchListeners;
+ var dispatchIDs = event._dispatchIDs;
+
+ var listenersIsArr = Array.isArray(dispatchListeners);
+ var idsIsArr = Array.isArray(dispatchIDs);
+ var IDsLen = idsIsArr ? dispatchIDs.length : dispatchIDs ? 1 : 0;
+ var listenersLen = listenersIsArr ? dispatchListeners.length : dispatchListeners ? 1 : 0;
+
+ "development" !== 'production' ? warning(idsIsArr === listenersIsArr && IDsLen === listenersLen, 'EventPluginUtils: Invalid `event`.') : undefined;
+ };
+}
+
+/**
+ * Dispatch the event to the listener.
+ * @param {SyntheticEvent} event SyntheticEvent to handle
+ * @param {boolean} simulated If the event is simulated (changes exn behavior)
+ * @param {function} listener Application-level callback
+ * @param {string} domID DOM id to pass to the callback.
+ */
+function executeDispatch(event, simulated, listener, domID) {
+ var type = event.type || 'unknown-event';
+ event.currentTarget = injection.Mount.getNode(domID);
+ if (simulated) {
+ ReactErrorUtils.invokeGuardedCallbackWithCatch(type, listener, event, domID);
+ } else {
+ ReactErrorUtils.invokeGuardedCallback(type, listener, event, domID);
+ }
+ event.currentTarget = null;
+}
+
+/**
+ * Standard/simple iteration through an event's collected dispatches.
+ */
+function executeDispatchesInOrder(event, simulated) {
+ var dispatchListeners = event._dispatchListeners;
+ var dispatchIDs = event._dispatchIDs;
+ if ("development" !== 'production') {
+ validateEventDispatches(event);
+ }
+ if (Array.isArray(dispatchListeners)) {
+ for (var i = 0; i < dispatchListeners.length; i++) {
+ if (event.isPropagationStopped()) {
+ break;
+ }
+ // Listeners and IDs are two parallel arrays that are always in sync.
+ executeDispatch(event, simulated, dispatchListeners[i], dispatchIDs[i]);
+ }
+ } else if (dispatchListeners) {
+ executeDispatch(event, simulated, dispatchListeners, dispatchIDs);
+ }
+ event._dispatchListeners = null;
+ event._dispatchIDs = null;
+}
+
+/**
+ * Standard/simple iteration through an event's collected dispatches, but stops
+ * at the first dispatch execution returning true, and returns that id.
+ *
+ * @return {?string} id of the first dispatch execution who's listener returns
+ * true, or null if no listener returned true.
+ */
+function executeDispatchesInOrderStopAtTrueImpl(event) {
+ var dispatchListeners = event._dispatchListeners;
+ var dispatchIDs = event._dispatchIDs;
+ if ("development" !== 'production') {
+ validateEventDispatches(event);
+ }
+ if (Array.isArray(dispatchListeners)) {
+ for (var i = 0; i < dispatchListeners.length; i++) {
+ if (event.isPropagationStopped()) {
+ break;
+ }
+ // Listeners and IDs are two parallel arrays that are always in sync.
+ if (dispatchListeners[i](event, dispatchIDs[i])) {
+ return dispatchIDs[i];
+ }
+ }
+ } else if (dispatchListeners) {
+ if (dispatchListeners(event, dispatchIDs)) {
+ return dispatchIDs;
+ }
+ }
+ return null;
+}
+
+/**
+ * @see executeDispatchesInOrderStopAtTrueImpl
+ */
+function executeDispatchesInOrderStopAtTrue(event) {
+ var ret = executeDispatchesInOrderStopAtTrueImpl(event);
+ event._dispatchIDs = null;
+ event._dispatchListeners = null;
+ return ret;
+}
+
+/**
+ * Execution of a "direct" dispatch - there must be at most one dispatch
+ * accumulated on the event or it is considered an error. It doesn't really make
+ * sense for an event with multiple dispatches (bubbled) to keep track of the
+ * return values at each dispatch execution, but it does tend to make sense when
+ * dealing with "direct" dispatches.
+ *
+ * @return {*} The return value of executing the single dispatch.
+ */
+function executeDirectDispatch(event) {
+ if ("development" !== 'production') {
+ validateEventDispatches(event);
+ }
+ var dispatchListener = event._dispatchListeners;
+ var dispatchID = event._dispatchIDs;
+ !!Array.isArray(dispatchListener) ? "development" !== 'production' ? invariant(false, 'executeDirectDispatch(...): Invalid `event`.') : invariant(false) : undefined;
+ var res = dispatchListener ? dispatchListener(event, dispatchID) : null;
+ event._dispatchListeners = null;
+ event._dispatchIDs = null;
+ return res;
+}
+
+/**
+ * @param {SyntheticEvent} event
+ * @return {boolean} True iff number of dispatches accumulated is greater than 0.
+ */
+function hasDispatches(event) {
+ return !!event._dispatchListeners;
+}
+
+/**
+ * General utilities that are useful in creating custom Event Plugins.
+ */
+var EventPluginUtils = {
+ isEndish: isEndish,
+ isMoveish: isMoveish,
+ isStartish: isStartish,
+
+ executeDirectDispatch: executeDirectDispatch,
+ executeDispatchesInOrder: executeDispatchesInOrder,
+ executeDispatchesInOrderStopAtTrue: executeDispatchesInOrderStopAtTrue,
+ hasDispatches: hasDispatches,
+
+ getNode: function (id) {
+ return injection.Mount.getNode(id);
+ },
+ getID: function (node) {
+ return injection.Mount.getID(node);
+ },
+
+ injection: injection
+};
+
+module.exports = EventPluginUtils;
+},{"15":15,"161":161,"173":173,"61":61}],19:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule EventPropagators
+ */
+
+'use strict';
+
+var EventConstants = _dereq_(15);
+var EventPluginHub = _dereq_(16);
+
+var warning = _dereq_(173);
+
+var accumulateInto = _dereq_(115);
+var forEachAccumulated = _dereq_(124);
+
+var PropagationPhases = EventConstants.PropagationPhases;
+var getListener = EventPluginHub.getListener;
+
+/**
+ * Some event types have a notion of different registration names for different
+ * "phases" of propagation. This finds listeners by a given phase.
+ */
+function listenerAtPhase(id, event, propagationPhase) {
+ var registrationName = event.dispatchConfig.phasedRegistrationNames[propagationPhase];
+ return getListener(id, registrationName);
+}
+
+/**
+ * Tags a `SyntheticEvent` with dispatched listeners. Creating this function
+ * here, allows us to not have to bind or create functions for each event.
+ * Mutating the event's members allows us to not have to create a wrapping
+ * "dispatch" object that pairs the event with the listener.
+ */
+function accumulateDirectionalDispatches(domID, upwards, event) {
+ if ("development" !== 'production') {
+ "development" !== 'production' ? warning(domID, 'Dispatching id must not be null') : undefined;
+ }
+ var phase = upwards ? PropagationPhases.bubbled : PropagationPhases.captured;
+ var listener = listenerAtPhase(domID, event, phase);
+ if (listener) {
+ event._dispatchListeners = accumulateInto(event._dispatchListeners, listener);
+ event._dispatchIDs = accumulateInto(event._dispatchIDs, domID);
+ }
+}
+
+/**
+ * Collect dispatches (must be entirely collected before dispatching - see unit
+ * tests). Lazily allocate the array to conserve memory. We must loop through
+ * each event and perform the traversal for each one. We cannot perform a
+ * single traversal for the entire collection of events because each event may
+ * have a different target.
+ */
+function accumulateTwoPhaseDispatchesSingle(event) {
+ if (event && event.dispatchConfig.phasedRegistrationNames) {
+ EventPluginHub.injection.getInstanceHandle().traverseTwoPhase(event.dispatchMarker, accumulateDirectionalDispatches, event);
+ }
+}
+
+/**
+ * Same as `accumulateTwoPhaseDispatchesSingle`, but skips over the targetID.
+ */
+function accumulateTwoPhaseDispatchesSingleSkipTarget(event) {
+ if (event && event.dispatchConfig.phasedRegistrationNames) {
+ EventPluginHub.injection.getInstanceHandle().traverseTwoPhaseSkipTarget(event.dispatchMarker, accumulateDirectionalDispatches, event);
+ }
+}
+
+/**
+ * Accumulates without regard to direction, does not look for phased
+ * registration names. Same as `accumulateDirectDispatchesSingle` but without
+ * requiring that the `dispatchMarker` be the same as the dispatched ID.
+ */
+function accumulateDispatches(id, ignoredDirection, event) {
+ if (event && event.dispatchConfig.registrationName) {
+ var registrationName = event.dispatchConfig.registrationName;
+ var listener = getListener(id, registrationName);
+ if (listener) {
+ event._dispatchListeners = accumulateInto(event._dispatchListeners, listener);
+ event._dispatchIDs = accumulateInto(event._dispatchIDs, id);
+ }
+ }
+}
+
+/**
+ * Accumulates dispatches on an `SyntheticEvent`, but only for the
+ * `dispatchMarker`.
+ * @param {SyntheticEvent} event
+ */
+function accumulateDirectDispatchesSingle(event) {
+ if (event && event.dispatchConfig.registrationName) {
+ accumulateDispatches(event.dispatchMarker, null, event);
+ }
+}
+
+function accumulateTwoPhaseDispatches(events) {
+ forEachAccumulated(events, accumulateTwoPhaseDispatchesSingle);
+}
+
+function accumulateTwoPhaseDispatchesSkipTarget(events) {
+ forEachAccumulated(events, accumulateTwoPhaseDispatchesSingleSkipTarget);
+}
+
+function accumulateEnterLeaveDispatches(leave, enter, fromID, toID) {
+ EventPluginHub.injection.getInstanceHandle().traverseEnterLeave(fromID, toID, accumulateDispatches, leave, enter);
+}
+
+function accumulateDirectDispatches(events) {
+ forEachAccumulated(events, accumulateDirectDispatchesSingle);
+}
+
+/**
+ * A small set of propagation patterns, each of which will accept a small amount
+ * of information, and generate a set of "dispatch ready event objects" - which
+ * are sets of events that have already been annotated with a set of dispatched
+ * listener functions/ids. The API is designed this way to discourage these
+ * propagation strategies from actually executing the dispatches, since we
+ * always want to collect the entire set of dispatches before executing event a
+ * single one.
+ *
+ * @constructor EventPropagators
+ */
+var EventPropagators = {
+ accumulateTwoPhaseDispatches: accumulateTwoPhaseDispatches,
+ accumulateTwoPhaseDispatchesSkipTarget: accumulateTwoPhaseDispatchesSkipTarget,
+ accumulateDirectDispatches: accumulateDirectDispatches,
+ accumulateEnterLeaveDispatches: accumulateEnterLeaveDispatches
+};
+
+module.exports = EventPropagators;
+},{"115":115,"124":124,"15":15,"16":16,"173":173}],20:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule FallbackCompositionState
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var PooledClass = _dereq_(25);
+
+var assign = _dereq_(24);
+var getTextContentAccessor = _dereq_(131);
+
+/**
+ * This helper class stores information about text content of a target node,
+ * allowing comparison of content before and after a given event.
+ *
+ * Identify the node where selection currently begins, then observe
+ * both its text content and its current position in the DOM. Since the
+ * browser may natively replace the target node during composition, we can
+ * use its position to find its replacement.
+ *
+ * @param {DOMEventTarget} root
+ */
+function FallbackCompositionState(root) {
+ this._root = root;
+ this._startText = this.getText();
+ this._fallbackText = null;
+}
+
+assign(FallbackCompositionState.prototype, {
+ destructor: function () {
+ this._root = null;
+ this._startText = null;
+ this._fallbackText = null;
+ },
+
+ /**
+ * Get current text of input.
+ *
+ * @return {string}
+ */
+ getText: function () {
+ if ('value' in this._root) {
+ return this._root.value;
+ }
+ return this._root[getTextContentAccessor()];
+ },
+
+ /**
+ * Determine the differing substring between the initially stored
+ * text content and the current content.
+ *
+ * @return {string}
+ */
+ getData: function () {
+ if (this._fallbackText) {
+ return this._fallbackText;
+ }
+
+ var start;
+ var startValue = this._startText;
+ var startLength = startValue.length;
+ var end;
+ var endValue = this.getText();
+ var endLength = endValue.length;
+
+ for (start = 0; start < startLength; start++) {
+ if (startValue[start] !== endValue[start]) {
+ break;
+ }
+ }
+
+ var minEnd = startLength - start;
+ for (end = 1; end <= minEnd; end++) {
+ if (startValue[startLength - end] !== endValue[endLength - end]) {
+ break;
+ }
+ }
+
+ var sliceTail = end > 1 ? 1 - end : undefined;
+ this._fallbackText = endValue.slice(start, sliceTail);
+ return this._fallbackText;
+ }
+});
+
+PooledClass.addPoolingTo(FallbackCompositionState);
+
+module.exports = FallbackCompositionState;
+},{"131":131,"24":24,"25":25}],21:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule HTMLDOMPropertyConfig
+ */
+
+'use strict';
+
+var DOMProperty = _dereq_(10);
+var ExecutionEnvironment = _dereq_(147);
+
+var MUST_USE_ATTRIBUTE = DOMProperty.injection.MUST_USE_ATTRIBUTE;
+var MUST_USE_PROPERTY = DOMProperty.injection.MUST_USE_PROPERTY;
+var HAS_BOOLEAN_VALUE = DOMProperty.injection.HAS_BOOLEAN_VALUE;
+var HAS_SIDE_EFFECTS = DOMProperty.injection.HAS_SIDE_EFFECTS;
+var HAS_NUMERIC_VALUE = DOMProperty.injection.HAS_NUMERIC_VALUE;
+var HAS_POSITIVE_NUMERIC_VALUE = DOMProperty.injection.HAS_POSITIVE_NUMERIC_VALUE;
+var HAS_OVERLOADED_BOOLEAN_VALUE = DOMProperty.injection.HAS_OVERLOADED_BOOLEAN_VALUE;
+
+var hasSVG;
+if (ExecutionEnvironment.canUseDOM) {
+ var implementation = document.implementation;
+ hasSVG = implementation && implementation.hasFeature && implementation.hasFeature('http://www.w3.org/TR/SVG11/feature#BasicStructure', '1.1');
+}
+
+var HTMLDOMPropertyConfig = {
+ isCustomAttribute: RegExp.prototype.test.bind(/^(data|aria)-[a-z_][a-z\d_.\-]*$/),
+ Properties: {
+ /**
+ * Standard Properties
+ */
+ accept: null,
+ acceptCharset: null,
+ accessKey: null,
+ action: null,
+ allowFullScreen: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE,
+ allowTransparency: MUST_USE_ATTRIBUTE,
+ alt: null,
+ async: HAS_BOOLEAN_VALUE,
+ autoComplete: null,
+ // autoFocus is polyfilled/normalized by AutoFocusUtils
+ // autoFocus: HAS_BOOLEAN_VALUE,
+ autoPlay: HAS_BOOLEAN_VALUE,
+ capture: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE,
+ cellPadding: null,
+ cellSpacing: null,
+ charSet: MUST_USE_ATTRIBUTE,
+ challenge: MUST_USE_ATTRIBUTE,
+ checked: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,
+ classID: MUST_USE_ATTRIBUTE,
+ // To set className on SVG elements, it's necessary to use .setAttribute;
+ // this works on HTML elements too in all browsers except IE8. Conveniently,
+ // IE8 doesn't support SVG and so we can simply use the attribute in
+ // browsers that support SVG and the property in browsers that don't,
+ // regardless of whether the element is HTML or SVG.
+ className: hasSVG ? MUST_USE_ATTRIBUTE : MUST_USE_PROPERTY,
+ cols: MUST_USE_ATTRIBUTE | HAS_POSITIVE_NUMERIC_VALUE,
+ colSpan: null,
+ content: null,
+ contentEditable: null,
+ contextMenu: MUST_USE_ATTRIBUTE,
+ controls: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,
+ coords: null,
+ crossOrigin: null,
+ data: null, // For `<object />` acts as `src`.
+ dateTime: MUST_USE_ATTRIBUTE,
+ 'default': HAS_BOOLEAN_VALUE,
+ defer: HAS_BOOLEAN_VALUE,
+ dir: null,
+ disabled: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE,
+ download: HAS_OVERLOADED_BOOLEAN_VALUE,
+ draggable: null,
+ encType: null,
+ form: MUST_USE_ATTRIBUTE,
+ formAction: MUST_USE_ATTRIBUTE,
+ formEncType: MUST_USE_ATTRIBUTE,
+ formMethod: MUST_USE_ATTRIBUTE,
+ formNoValidate: HAS_BOOLEAN_VALUE,
+ formTarget: MUST_USE_ATTRIBUTE,
+ frameBorder: MUST_USE_ATTRIBUTE,
+ headers: null,
+ height: MUST_USE_ATTRIBUTE,
+ hidden: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE,
+ high: null,
+ href: null,
+ hrefLang: null,
+ htmlFor: null,
+ httpEquiv: null,
+ icon: null,
+ id: MUST_USE_PROPERTY,
+ inputMode: MUST_USE_ATTRIBUTE,
+ integrity: null,
+ is: MUST_USE_ATTRIBUTE,
+ keyParams: MUST_USE_ATTRIBUTE,
+ keyType: MUST_USE_ATTRIBUTE,
+ kind: null,
+ label: null,
+ lang: null,
+ list: MUST_USE_ATTRIBUTE,
+ loop: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,
+ low: null,
+ manifest: MUST_USE_ATTRIBUTE,
+ marginHeight: null,
+ marginWidth: null,
+ max: null,
+ maxLength: MUST_USE_ATTRIBUTE,
+ media: MUST_USE_ATTRIBUTE,
+ mediaGroup: null,
+ method: null,
+ min: null,
+ minLength: MUST_USE_ATTRIBUTE,
+ multiple: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,
+ muted: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,
+ name: null,
+ nonce: MUST_USE_ATTRIBUTE,
+ noValidate: HAS_BOOLEAN_VALUE,
+ open: HAS_BOOLEAN_VALUE,
+ optimum: null,
+ pattern: null,
+ placeholder: null,
+ poster: null,
+ preload: null,
+ radioGroup: null,
+ readOnly: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,
+ rel: null,
+ required: HAS_BOOLEAN_VALUE,
+ reversed: HAS_BOOLEAN_VALUE,
+ role: MUST_USE_ATTRIBUTE,
+ rows: MUST_USE_ATTRIBUTE | HAS_POSITIVE_NUMERIC_VALUE,
+ rowSpan: null,
+ sandbox: null,
+ scope: null,
+ scoped: HAS_BOOLEAN_VALUE,
+ scrolling: null,
+ seamless: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE,
+ selected: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,
+ shape: null,
+ size: MUST_USE_ATTRIBUTE | HAS_POSITIVE_NUMERIC_VALUE,
+ sizes: MUST_USE_ATTRIBUTE,
+ span: HAS_POSITIVE_NUMERIC_VALUE,
+ spellCheck: null,
+ src: null,
+ srcDoc: MUST_USE_PROPERTY,
+ srcLang: null,
+ srcSet: MUST_USE_ATTRIBUTE,
+ start: HAS_NUMERIC_VALUE,
+ step: null,
+ style: null,
+ summary: null,
+ tabIndex: null,
+ target: null,
+ title: null,
+ type: null,
+ useMap: null,
+ value: MUST_USE_PROPERTY | HAS_SIDE_EFFECTS,
+ width: MUST_USE_ATTRIBUTE,
+ wmode: MUST_USE_ATTRIBUTE,
+ wrap: null,
+
+ /**
+ * RDFa Properties
+ */
+ about: MUST_USE_ATTRIBUTE,
+ datatype: MUST_USE_ATTRIBUTE,
+ inlist: MUST_USE_ATTRIBUTE,
+ prefix: MUST_USE_ATTRIBUTE,
+ // property is also supported for OpenGraph in meta tags.
+ property: MUST_USE_ATTRIBUTE,
+ resource: MUST_USE_ATTRIBUTE,
+ 'typeof': MUST_USE_ATTRIBUTE,
+ vocab: MUST_USE_ATTRIBUTE,
+
+ /**
+ * Non-standard Properties
+ */
+ // autoCapitalize and autoCorrect are supported in Mobile Safari for
+ // keyboard hints.
+ autoCapitalize: MUST_USE_ATTRIBUTE,
+ autoCorrect: MUST_USE_ATTRIBUTE,
+ // autoSave allows WebKit/Blink to persist values of input fields on page reloads
+ autoSave: null,
+ // color is for Safari mask-icon link
+ color: null,
+ // itemProp, itemScope, itemType are for
+ // Microdata support. See http://schema.org/docs/gs.html
+ itemProp: MUST_USE_ATTRIBUTE,
+ itemScope: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE,
+ itemType: MUST_USE_ATTRIBUTE,
+ // itemID and itemRef are for Microdata support as well but
+ // only specified in the the WHATWG spec document. See
+ // https://html.spec.whatwg.org/multipage/microdata.html#microdata-dom-api
+ itemID: MUST_USE_ATTRIBUTE,
+ itemRef: MUST_USE_ATTRIBUTE,
+ // results show looking glass icon and recent searches on input
+ // search fields in WebKit/Blink
+ results: null,
+ // IE-only attribute that specifies security restrictions on an iframe
+ // as an alternative to the sandbox attribute on IE<10
+ security: MUST_USE_ATTRIBUTE,
+ // IE-only attribute that controls focus behavior
+ unselectable: MUST_USE_ATTRIBUTE
+ },
+ DOMAttributeNames: {
+ acceptCharset: 'accept-charset',
+ className: 'class',
+ htmlFor: 'for',
+ httpEquiv: 'http-equiv'
+ },
+ DOMPropertyNames: {
+ autoComplete: 'autocomplete',
+ autoFocus: 'autofocus',
+ autoPlay: 'autoplay',
+ autoSave: 'autosave',
+ // `encoding` is equivalent to `enctype`, IE8 lacks an `enctype` setter.
+ // http://www.w3.org/TR/html5/forms.html#dom-fs-encoding
+ encType: 'encoding',
+ hrefLang: 'hreflang',
+ radioGroup: 'radiogroup',
+ spellCheck: 'spellcheck',
+ srcDoc: 'srcdoc',
+ srcSet: 'srcset'
+ }
+};
+
+module.exports = HTMLDOMPropertyConfig;
+},{"10":10,"147":147}],22:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule LinkedStateMixin
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var ReactLink = _dereq_(70);
+var ReactStateSetters = _dereq_(90);
+
+/**
+ * A simple mixin around ReactLink.forState().
+ */
+var LinkedStateMixin = {
+ /**
+ * Create a ReactLink that's linked to part of this component's state. The
+ * ReactLink will have the current value of this.state[key] and will call
+ * setState() when a change is requested.
+ *
+ * @param {string} key state key to update. Note: you may want to use keyOf()
+ * if you're using Google Closure Compiler advanced mode.
+ * @return {ReactLink} ReactLink instance linking to the state.
+ */
+ linkState: function (key) {
+ return new ReactLink(this.state[key], ReactStateSetters.createStateKeySetter(this, key));
+ }
+};
+
+module.exports = LinkedStateMixin;
+},{"70":70,"90":90}],23:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule LinkedValueUtils
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var ReactPropTypes = _dereq_(82);
+var ReactPropTypeLocations = _dereq_(81);
+
+var invariant = _dereq_(161);
+var warning = _dereq_(173);
+
+var hasReadOnlyValue = {
+ 'button': true,
+ 'checkbox': true,
+ 'image': true,
+ 'hidden': true,
+ 'radio': true,
+ 'reset': true,
+ 'submit': true
+};
+
+function _assertSingleLink(inputProps) {
+ !(inputProps.checkedLink == null || inputProps.valueLink == null) ? "development" !== 'production' ? invariant(false, 'Cannot provide a checkedLink and a valueLink. If you want to use ' + 'checkedLink, you probably don\'t want to use valueLink and vice versa.') : invariant(false) : undefined;
+}
+function _assertValueLink(inputProps) {
+ _assertSingleLink(inputProps);
+ !(inputProps.value == null && inputProps.onChange == null) ? "development" !== 'production' ? invariant(false, 'Cannot provide a valueLink and a value or onChange event. If you want ' + 'to use value or onChange, you probably don\'t want to use valueLink.') : invariant(false) : undefined;
+}
+
+function _assertCheckedLink(inputProps) {
+ _assertSingleLink(inputProps);
+ !(inputProps.checked == null && inputProps.onChange == null) ? "development" !== 'production' ? invariant(false, 'Cannot provide a checkedLink and a checked property or onChange event. ' + 'If you want to use checked or onChange, you probably don\'t want to ' + 'use checkedLink') : invariant(false) : undefined;
+}
+
+var propTypes = {
+ value: function (props, propName, componentName) {
+ if (!props[propName] || hasReadOnlyValue[props.type] || props.onChange || props.readOnly || props.disabled) {
+ return null;
+ }
+ return new Error('You provided a `value` prop to a form field without an ' + '`onChange` handler. This will render a read-only field. If ' + 'the field should be mutable use `defaultValue`. Otherwise, ' + 'set either `onChange` or `readOnly`.');
+ },
+ checked: function (props, propName, componentName) {
+ if (!props[propName] || props.onChange || props.readOnly || props.disabled) {
+ return null;
+ }
+ return new Error('You provided a `checked` prop to a form field without an ' + '`onChange` handler. This will render a read-only field. If ' + 'the field should be mutable use `defaultChecked`. Otherwise, ' + 'set either `onChange` or `readOnly`.');
+ },
+ onChange: ReactPropTypes.func
+};
+
+var loggedTypeFailures = {};
+function getDeclarationErrorAddendum(owner) {
+ if (owner) {
+ var name = owner.getName();
+ if (name) {
+ return ' Check the render method of `' + name + '`.';
+ }
+ }
+ return '';
+}
+
+/**
+ * Provide a linked `value` attribute for controlled forms. You should not use
+ * this outside of the ReactDOM controlled form components.
+ */
+var LinkedValueUtils = {
+ checkPropTypes: function (tagName, props, owner) {
+ for (var propName in propTypes) {
+ if (propTypes.hasOwnProperty(propName)) {
+ var error = propTypes[propName](props, propName, tagName, ReactPropTypeLocations.prop);
+ }
+ if (error instanceof Error && !(error.message in loggedTypeFailures)) {
+ // Only monitor this failure once because there tends to be a lot of the
+ // same error.
+ loggedTypeFailures[error.message] = true;
+
+ var addendum = getDeclarationErrorAddendum(owner);
+ "development" !== 'production' ? warning(false, 'Failed form propType: %s%s', error.message, addendum) : undefined;
+ }
+ }
+ },
+
+ /**
+ * @param {object} inputProps Props for form component
+ * @return {*} current value of the input either from value prop or link.
+ */
+ getValue: function (inputProps) {
+ if (inputProps.valueLink) {
+ _assertValueLink(inputProps);
+ return inputProps.valueLink.value;
+ }
+ return inputProps.value;
+ },
+
+ /**
+ * @param {object} inputProps Props for form component
+ * @return {*} current checked status of the input either from checked prop
+ * or link.
+ */
+ getChecked: function (inputProps) {
+ if (inputProps.checkedLink) {
+ _assertCheckedLink(inputProps);
+ return inputProps.checkedLink.value;
+ }
+ return inputProps.checked;
+ },
+
+ /**
+ * @param {object} inputProps Props for form component
+ * @param {SyntheticEvent} event change event to handle
+ */
+ executeOnChange: function (inputProps, event) {
+ if (inputProps.valueLink) {
+ _assertValueLink(inputProps);
+ return inputProps.valueLink.requestChange(event.target.value);
+ } else if (inputProps.checkedLink) {
+ _assertCheckedLink(inputProps);
+ return inputProps.checkedLink.requestChange(event.target.checked);
+ } else if (inputProps.onChange) {
+ return inputProps.onChange.call(undefined, event);
+ }
+ }
+};
+
+module.exports = LinkedValueUtils;
+},{"161":161,"173":173,"81":81,"82":82}],24:[function(_dereq_,module,exports){
+/**
+ * Copyright 2014-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule Object.assign
+ */
+
+// https://people.mozilla.org/~jorendorff/es6-draft.html#sec-object.assign
+
+'use strict';
+
+function assign(target, sources) {
+ if (target == null) {
+ throw new TypeError('Object.assign target cannot be null or undefined');
+ }
+
+ var to = Object(target);
+ var hasOwnProperty = Object.prototype.hasOwnProperty;
+
+ for (var nextIndex = 1; nextIndex < arguments.length; nextIndex++) {
+ var nextSource = arguments[nextIndex];
+ if (nextSource == null) {
+ continue;
+ }
+
+ var from = Object(nextSource);
+
+ // We don't currently support accessors nor proxies. Therefore this
+ // copy cannot throw. If we ever supported this then we must handle
+ // exceptions and side-effects. We don't support symbols so they won't
+ // be transferred.
+
+ for (var key in from) {
+ if (hasOwnProperty.call(from, key)) {
+ to[key] = from[key];
+ }
+ }
+ }
+
+ return to;
+}
+
+module.exports = assign;
+},{}],25:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule PooledClass
+ */
+
+'use strict';
+
+var invariant = _dereq_(161);
+
+/**
+ * Static poolers. Several custom versions for each potential number of
+ * arguments. A completely generic pooler is easy to implement, but would
+ * require accessing the `arguments` object. In each of these, `this` refers to
+ * the Class itself, not an instance. If any others are needed, simply add them
+ * here, or in their own files.
+ */
+var oneArgumentPooler = function (copyFieldsFrom) {
+ var Klass = this;
+ if (Klass.instancePool.length) {
+ var instance = Klass.instancePool.pop();
+ Klass.call(instance, copyFieldsFrom);
+ return instance;
+ } else {
+ return new Klass(copyFieldsFrom);
+ }
+};
+
+var twoArgumentPooler = function (a1, a2) {
+ var Klass = this;
+ if (Klass.instancePool.length) {
+ var instance = Klass.instancePool.pop();
+ Klass.call(instance, a1, a2);
+ return instance;
+ } else {
+ return new Klass(a1, a2);
+ }
+};
+
+var threeArgumentPooler = function (a1, a2, a3) {
+ var Klass = this;
+ if (Klass.instancePool.length) {
+ var instance = Klass.instancePool.pop();
+ Klass.call(instance, a1, a2, a3);
+ return instance;
+ } else {
+ return new Klass(a1, a2, a3);
+ }
+};
+
+var fourArgumentPooler = function (a1, a2, a3, a4) {
+ var Klass = this;
+ if (Klass.instancePool.length) {
+ var instance = Klass.instancePool.pop();
+ Klass.call(instance, a1, a2, a3, a4);
+ return instance;
+ } else {
+ return new Klass(a1, a2, a3, a4);
+ }
+};
+
+var fiveArgumentPooler = function (a1, a2, a3, a4, a5) {
+ var Klass = this;
+ if (Klass.instancePool.length) {
+ var instance = Klass.instancePool.pop();
+ Klass.call(instance, a1, a2, a3, a4, a5);
+ return instance;
+ } else {
+ return new Klass(a1, a2, a3, a4, a5);
+ }
+};
+
+var standardReleaser = function (instance) {
+ var Klass = this;
+ !(instance instanceof Klass) ? "development" !== 'production' ? invariant(false, 'Trying to release an instance into a pool of a different type.') : invariant(false) : undefined;
+ instance.destructor();
+ if (Klass.instancePool.length < Klass.poolSize) {
+ Klass.instancePool.push(instance);
+ }
+};
+
+var DEFAULT_POOL_SIZE = 10;
+var DEFAULT_POOLER = oneArgumentPooler;
+
+/**
+ * Augments `CopyConstructor` to be a poolable class, augmenting only the class
+ * itself (statically) not adding any prototypical fields. Any CopyConstructor
+ * you give this may have a `poolSize` property, and will look for a
+ * prototypical `destructor` on instances (optional).
+ *
+ * @param {Function} CopyConstructor Constructor that can be used to reset.
+ * @param {Function} pooler Customizable pooler.
+ */
+var addPoolingTo = function (CopyConstructor, pooler) {
+ var NewKlass = CopyConstructor;
+ NewKlass.instancePool = [];
+ NewKlass.getPooled = pooler || DEFAULT_POOLER;
+ if (!NewKlass.poolSize) {
+ NewKlass.poolSize = DEFAULT_POOL_SIZE;
+ }
+ NewKlass.release = standardReleaser;
+ return NewKlass;
+};
+
+var PooledClass = {
+ addPoolingTo: addPoolingTo,
+ oneArgumentPooler: oneArgumentPooler,
+ twoArgumentPooler: twoArgumentPooler,
+ threeArgumentPooler: threeArgumentPooler,
+ fourArgumentPooler: fourArgumentPooler,
+ fiveArgumentPooler: fiveArgumentPooler
+};
+
+module.exports = PooledClass;
+},{"161":161}],26:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule React
+ */
+
+'use strict';
+
+var ReactDOM = _dereq_(40);
+var ReactDOMServer = _dereq_(50);
+var ReactIsomorphic = _dereq_(69);
+
+var assign = _dereq_(24);
+var deprecated = _dereq_(120);
+
+// `version` will be added here by ReactIsomorphic.
+var React = {};
+
+assign(React, ReactIsomorphic);
+
+assign(React, {
+ // ReactDOM
+ findDOMNode: deprecated('findDOMNode', 'ReactDOM', 'react-dom', ReactDOM, ReactDOM.findDOMNode),
+ render: deprecated('render', 'ReactDOM', 'react-dom', ReactDOM, ReactDOM.render),
+ unmountComponentAtNode: deprecated('unmountComponentAtNode', 'ReactDOM', 'react-dom', ReactDOM, ReactDOM.unmountComponentAtNode),
+
+ // ReactDOMServer
+ renderToString: deprecated('renderToString', 'ReactDOMServer', 'react-dom/server', ReactDOMServer, ReactDOMServer.renderToString),
+ renderToStaticMarkup: deprecated('renderToStaticMarkup', 'ReactDOMServer', 'react-dom/server', ReactDOMServer, ReactDOMServer.renderToStaticMarkup)
+});
+
+React.__SECRET_DOM_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = ReactDOM;
+React.__SECRET_DOM_SERVER_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = ReactDOMServer;
+
+module.exports = React;
+},{"120":120,"24":24,"40":40,"50":50,"69":69}],27:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactBrowserComponentMixin
+ */
+
+'use strict';
+
+var ReactInstanceMap = _dereq_(68);
+
+var findDOMNode = _dereq_(122);
+var warning = _dereq_(173);
+
+var didWarnKey = '_getDOMNodeDidWarn';
+
+var ReactBrowserComponentMixin = {
+ /**
+ * Returns the DOM node rendered by this component.
+ *
+ * @return {DOMElement} The root node of this component.
+ * @final
+ * @protected
+ */
+ getDOMNode: function () {
+ "development" !== 'production' ? warning(this.constructor[didWarnKey], '%s.getDOMNode(...) is deprecated. Please use ' + 'ReactDOM.findDOMNode(instance) instead.', ReactInstanceMap.get(this).getName() || this.tagName || 'Unknown') : undefined;
+ this.constructor[didWarnKey] = true;
+ return findDOMNode(this);
+ }
+};
+
+module.exports = ReactBrowserComponentMixin;
+},{"122":122,"173":173,"68":68}],28:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactBrowserEventEmitter
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var EventConstants = _dereq_(15);
+var EventPluginHub = _dereq_(16);
+var EventPluginRegistry = _dereq_(17);
+var ReactEventEmitterMixin = _dereq_(62);
+var ReactPerf = _dereq_(78);
+var ViewportMetrics = _dereq_(114);
+
+var assign = _dereq_(24);
+var isEventSupported = _dereq_(133);
+
+/**
+ * Summary of `ReactBrowserEventEmitter` event handling:
+ *
+ * - Top-level delegation is used to trap most native browser events. This
+ * may only occur in the main thread and is the responsibility of
+ * ReactEventListener, which is injected and can therefore support pluggable
+ * event sources. This is the only work that occurs in the main thread.
+ *
+ * - We normalize and de-duplicate events to account for browser quirks. This
+ * may be done in the worker thread.
+ *
+ * - Forward these native events (with the associated top-level type used to
+ * trap it) to `EventPluginHub`, which in turn will ask plugins if they want
+ * to extract any synthetic events.
+ *
+ * - The `EventPluginHub` will then process each event by annotating them with
+ * "dispatches", a sequence of listeners and IDs that care about that event.
+ *
+ * - The `EventPluginHub` then dispatches the events.
+ *
+ * Overview of React and the event system:
+ *
+ * +------------+ .
+ * | DOM | .
+ * +------------+ .
+ * | .
+ * v .
+ * +------------+ .
+ * | ReactEvent | .
+ * | Listener | .
+ * +------------+ . +-----------+
+ * | . +--------+|SimpleEvent|
+ * | . | |Plugin |
+ * +-----|------+ . v +-----------+
+ * | | | . +--------------+ +------------+
+ * | +-----------.--->|EventPluginHub| | Event |
+ * | | . | | +-----------+ | Propagators|
+ * | ReactEvent | . | | |TapEvent | |------------|
+ * | Emitter | . | |<---+|Plugin | |other plugin|
+ * | | . | | +-----------+ | utilities |
+ * | +-----------.--->| | +------------+
+ * | | | . +--------------+
+ * +-----|------+ . ^ +-----------+
+ * | . | |Enter/Leave|
+ * + . +-------+|Plugin |
+ * +-------------+ . +-----------+
+ * | application | .
+ * |-------------| .
+ * | | .
+ * | | .
+ * +-------------+ .
+ * .
+ * React Core . General Purpose Event Plugin System
+ */
+
+var alreadyListeningTo = {};
+var isMonitoringScrollValue = false;
+var reactTopListenersCounter = 0;
+
+// For events like 'submit' which don't consistently bubble (which we trap at a
+// lower node than `document`), binding at `document` would cause duplicate
+// events so we don't include them here
+var topEventMapping = {
+ topAbort: 'abort',
+ topBlur: 'blur',
+ topCanPlay: 'canplay',
+ topCanPlayThrough: 'canplaythrough',
+ topChange: 'change',
+ topClick: 'click',
+ topCompositionEnd: 'compositionend',
+ topCompositionStart: 'compositionstart',
+ topCompositionUpdate: 'compositionupdate',
+ topContextMenu: 'contextmenu',
+ topCopy: 'copy',
+ topCut: 'cut',
+ topDoubleClick: 'dblclick',
+ topDrag: 'drag',
+ topDragEnd: 'dragend',
+ topDragEnter: 'dragenter',
+ topDragExit: 'dragexit',
+ topDragLeave: 'dragleave',
+ topDragOver: 'dragover',
+ topDragStart: 'dragstart',
+ topDrop: 'drop',
+ topDurationChange: 'durationchange',
+ topEmptied: 'emptied',
+ topEncrypted: 'encrypted',
+ topEnded: 'ended',
+ topError: 'error',
+ topFocus: 'focus',
+ topInput: 'input',
+ topKeyDown: 'keydown',
+ topKeyPress: 'keypress',
+ topKeyUp: 'keyup',
+ topLoadedData: 'loadeddata',
+ topLoadedMetadata: 'loadedmetadata',
+ topLoadStart: 'loadstart',
+ topMouseDown: 'mousedown',
+ topMouseMove: 'mousemove',
+ topMouseOut: 'mouseout',
+ topMouseOver: 'mouseover',
+ topMouseUp: 'mouseup',
+ topPaste: 'paste',
+ topPause: 'pause',
+ topPlay: 'play',
+ topPlaying: 'playing',
+ topProgress: 'progress',
+ topRateChange: 'ratechange',
+ topScroll: 'scroll',
+ topSeeked: 'seeked',
+ topSeeking: 'seeking',
+ topSelectionChange: 'selectionchange',
+ topStalled: 'stalled',
+ topSuspend: 'suspend',
+ topTextInput: 'textInput',
+ topTimeUpdate: 'timeupdate',
+ topTouchCancel: 'touchcancel',
+ topTouchEnd: 'touchend',
+ topTouchMove: 'touchmove',
+ topTouchStart: 'touchstart',
+ topVolumeChange: 'volumechange',
+ topWaiting: 'waiting',
+ topWheel: 'wheel'
+};
+
+/**
+ * To ensure no conflicts with other potential React instances on the page
+ */
+var topListenersIDKey = '_reactListenersID' + String(Math.random()).slice(2);
+
+function getListeningForDocument(mountAt) {
+ // In IE8, `mountAt` is a host object and doesn't have `hasOwnProperty`
+ // directly.
+ if (!Object.prototype.hasOwnProperty.call(mountAt, topListenersIDKey)) {
+ mountAt[topListenersIDKey] = reactTopListenersCounter++;
+ alreadyListeningTo[mountAt[topListenersIDKey]] = {};
+ }
+ return alreadyListeningTo[mountAt[topListenersIDKey]];
+}
+
+/**
+ * `ReactBrowserEventEmitter` is used to attach top-level event listeners. For
+ * example:
+ *
+ * ReactBrowserEventEmitter.putListener('myID', 'onClick', myFunction);
+ *
+ * This would allocate a "registration" of `('onClick', myFunction)` on 'myID'.
+ *
+ * @internal
+ */
+var ReactBrowserEventEmitter = assign({}, ReactEventEmitterMixin, {
+
+ /**
+ * Injectable event backend
+ */
+ ReactEventListener: null,
+
+ injection: {
+ /**
+ * @param {object} ReactEventListener
+ */
+ injectReactEventListener: function (ReactEventListener) {
+ ReactEventListener.setHandleTopLevel(ReactBrowserEventEmitter.handleTopLevel);
+ ReactBrowserEventEmitter.ReactEventListener = ReactEventListener;
+ }
+ },
+
+ /**
+ * Sets whether or not any created callbacks should be enabled.
+ *
+ * @param {boolean} enabled True if callbacks should be enabled.
+ */
+ setEnabled: function (enabled) {
+ if (ReactBrowserEventEmitter.ReactEventListener) {
+ ReactBrowserEventEmitter.ReactEventListener.setEnabled(enabled);
+ }
+ },
+
+ /**
+ * @return {boolean} True if callbacks are enabled.
+ */
+ isEnabled: function () {
+ return !!(ReactBrowserEventEmitter.ReactEventListener && ReactBrowserEventEmitter.ReactEventListener.isEnabled());
+ },
+
+ /**
+ * We listen for bubbled touch events on the document object.
+ *
+ * Firefox v8.01 (and possibly others) exhibited strange behavior when
+ * mounting `onmousemove` events at some node that was not the document
+ * element. The symptoms were that if your mouse is not moving over something
+ * contained within that mount point (for example on the background) the
+ * top-level listeners for `onmousemove` won't be called. However, if you
+ * register the `mousemove` on the document object, then it will of course
+ * catch all `mousemove`s. This along with iOS quirks, justifies restricting
+ * top-level listeners to the document object only, at least for these
+ * movement types of events and possibly all events.
+ *
+ * @see http://www.quirksmode.org/blog/archives/2010/09/click_event_del.html
+ *
+ * Also, `keyup`/`keypress`/`keydown` do not bubble to the window on IE, but
+ * they bubble to document.
+ *
+ * @param {string} registrationName Name of listener (e.g. `onClick`).
+ * @param {object} contentDocumentHandle Document which owns the container
+ */
+ listenTo: function (registrationName, contentDocumentHandle) {
+ var mountAt = contentDocumentHandle;
+ var isListening = getListeningForDocument(mountAt);
+ var dependencies = EventPluginRegistry.registrationNameDependencies[registrationName];
+
+ var topLevelTypes = EventConstants.topLevelTypes;
+ for (var i = 0; i < dependencies.length; i++) {
+ var dependency = dependencies[i];
+ if (!(isListening.hasOwnProperty(dependency) && isListening[dependency])) {
+ if (dependency === topLevelTypes.topWheel) {
+ if (isEventSupported('wheel')) {
+ ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(topLevelTypes.topWheel, 'wheel', mountAt);
+ } else if (isEventSupported('mousewheel')) {
+ ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(topLevelTypes.topWheel, 'mousewheel', mountAt);
+ } else {
+ // Firefox needs to capture a different mouse scroll event.
+ // @see http://www.quirksmode.org/dom/events/tests/scroll.html
+ ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(topLevelTypes.topWheel, 'DOMMouseScroll', mountAt);
+ }
+ } else if (dependency === topLevelTypes.topScroll) {
+
+ if (isEventSupported('scroll', true)) {
+ ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent(topLevelTypes.topScroll, 'scroll', mountAt);
+ } else {
+ ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(topLevelTypes.topScroll, 'scroll', ReactBrowserEventEmitter.ReactEventListener.WINDOW_HANDLE);
+ }
+ } else if (dependency === topLevelTypes.topFocus || dependency === topLevelTypes.topBlur) {
+
+ if (isEventSupported('focus', true)) {
+ ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent(topLevelTypes.topFocus, 'focus', mountAt);
+ ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent(topLevelTypes.topBlur, 'blur', mountAt);
+ } else if (isEventSupported('focusin')) {
+ // IE has `focusin` and `focusout` events which bubble.
+ // @see http://www.quirksmode.org/blog/archives/2008/04/delegating_the.html
+ ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(topLevelTypes.topFocus, 'focusin', mountAt);
+ ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(topLevelTypes.topBlur, 'focusout', mountAt);
+ }
+
+ // to make sure blur and focus event listeners are only attached once
+ isListening[topLevelTypes.topBlur] = true;
+ isListening[topLevelTypes.topFocus] = true;
+ } else if (topEventMapping.hasOwnProperty(dependency)) {
+ ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(dependency, topEventMapping[dependency], mountAt);
+ }
+
+ isListening[dependency] = true;
+ }
+ }
+ },
+
+ trapBubbledEvent: function (topLevelType, handlerBaseName, handle) {
+ return ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(topLevelType, handlerBaseName, handle);
+ },
+
+ trapCapturedEvent: function (topLevelType, handlerBaseName, handle) {
+ return ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent(topLevelType, handlerBaseName, handle);
+ },
+
+ /**
+ * Listens to window scroll and resize events. We cache scroll values so that
+ * application code can access them without triggering reflows.
+ *
+ * NOTE: Scroll events do not bubble.
+ *
+ * @see http://www.quirksmode.org/dom/events/scroll.html
+ */
+ ensureScrollValueMonitoring: function () {
+ if (!isMonitoringScrollValue) {
+ var refresh = ViewportMetrics.refreshScrollValues;
+ ReactBrowserEventEmitter.ReactEventListener.monitorScrollValue(refresh);
+ isMonitoringScrollValue = true;
+ }
+ },
+
+ eventNameDispatchConfigs: EventPluginHub.eventNameDispatchConfigs,
+
+ registrationNameModules: EventPluginHub.registrationNameModules,
+
+ putListener: EventPluginHub.putListener,
+
+ getListener: EventPluginHub.getListener,
+
+ deleteListener: EventPluginHub.deleteListener,
+
+ deleteAllListeners: EventPluginHub.deleteAllListeners
+
+});
+
+ReactPerf.measureMethods(ReactBrowserEventEmitter, 'ReactBrowserEventEmitter', {
+ putListener: 'putListener',
+ deleteListener: 'deleteListener'
+});
+
+module.exports = ReactBrowserEventEmitter;
+},{"114":114,"133":133,"15":15,"16":16,"17":17,"24":24,"62":62,"78":78}],29:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @typechecks
+ * @providesModule ReactCSSTransitionGroup
+ */
+
+'use strict';
+
+var React = _dereq_(26);
+
+var assign = _dereq_(24);
+
+var ReactTransitionGroup = _dereq_(94);
+var ReactCSSTransitionGroupChild = _dereq_(30);
+
+function createTransitionTimeoutPropValidator(transitionType) {
+ var timeoutPropName = 'transition' + transitionType + 'Timeout';
+ var enabledPropName = 'transition' + transitionType;
+
+ return function (props) {
+ // If the transition is enabled
+ if (props[enabledPropName]) {
+ // If no timeout duration is provided
+ if (props[timeoutPropName] == null) {
+ return new Error(timeoutPropName + ' wasn\'t supplied to ReactCSSTransitionGroup: ' + 'this can cause unreliable animations and won\'t be supported in ' + 'a future version of React. See ' + 'https://fb.me/react-animation-transition-group-timeout for more ' + 'information.');
+
+ // If the duration isn't a number
+ } else if (typeof props[timeoutPropName] !== 'number') {
+ return new Error(timeoutPropName + ' must be a number (in milliseconds)');
+ }
+ }
+ };
+}
+
+var ReactCSSTransitionGroup = React.createClass({
+ displayName: 'ReactCSSTransitionGroup',
+
+ propTypes: {
+ transitionName: ReactCSSTransitionGroupChild.propTypes.name,
+
+ transitionAppear: React.PropTypes.bool,
+ transitionEnter: React.PropTypes.bool,
+ transitionLeave: React.PropTypes.bool,
+ transitionAppearTimeout: createTransitionTimeoutPropValidator('Appear'),
+ transitionEnterTimeout: createTransitionTimeoutPropValidator('Enter'),
+ transitionLeaveTimeout: createTransitionTimeoutPropValidator('Leave')
+ },
+
+ getDefaultProps: function () {
+ return {
+ transitionAppear: false,
+ transitionEnter: true,
+ transitionLeave: true
+ };
+ },
+
+ _wrapChild: function (child) {
+ // We need to provide this childFactory so that
+ // ReactCSSTransitionGroupChild can receive updates to name, enter, and
+ // leave while it is leaving.
+ return React.createElement(ReactCSSTransitionGroupChild, {
+ name: this.props.transitionName,
+ appear: this.props.transitionAppear,
+ enter: this.props.transitionEnter,
+ leave: this.props.transitionLeave,
+ appearTimeout: this.props.transitionAppearTimeout,
+ enterTimeout: this.props.transitionEnterTimeout,
+ leaveTimeout: this.props.transitionLeaveTimeout
+ }, child);
+ },
+
+ render: function () {
+ return React.createElement(ReactTransitionGroup, assign({}, this.props, { childFactory: this._wrapChild }));
+ }
+});
+
+module.exports = ReactCSSTransitionGroup;
+},{"24":24,"26":26,"30":30,"94":94}],30:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @typechecks
+ * @providesModule ReactCSSTransitionGroupChild
+ */
+
+'use strict';
+
+var React = _dereq_(26);
+var ReactDOM = _dereq_(40);
+
+var CSSCore = _dereq_(145);
+var ReactTransitionEvents = _dereq_(93);
+
+var onlyChild = _dereq_(135);
+
+// We don't remove the element from the DOM until we receive an animationend or
+// transitionend event. If the user screws up and forgets to add an animation
+// their node will be stuck in the DOM forever, so we detect if an animation
+// does not start and if it doesn't, we just call the end listener immediately.
+var TICK = 17;
+
+var ReactCSSTransitionGroupChild = React.createClass({
+ displayName: 'ReactCSSTransitionGroupChild',
+
+ propTypes: {
+ name: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.shape({
+ enter: React.PropTypes.string,
+ leave: React.PropTypes.string,
+ active: React.PropTypes.string
+ }), React.PropTypes.shape({
+ enter: React.PropTypes.string,
+ enterActive: React.PropTypes.string,
+ leave: React.PropTypes.string,
+ leaveActive: React.PropTypes.string,
+ appear: React.PropTypes.string,
+ appearActive: React.PropTypes.string
+ })]).isRequired,
+
+ // Once we require timeouts to be specified, we can remove the
+ // boolean flags (appear etc.) and just accept a number
+ // or a bool for the timeout flags (appearTimeout etc.)
+ appear: React.PropTypes.bool,
+ enter: React.PropTypes.bool,
+ leave: React.PropTypes.bool,
+ appearTimeout: React.PropTypes.number,
+ enterTimeout: React.PropTypes.number,
+ leaveTimeout: React.PropTypes.number
+ },
+
+ transition: function (animationType, finishCallback, userSpecifiedDelay) {
+ var node = ReactDOM.findDOMNode(this);
+
+ if (!node) {
+ if (finishCallback) {
+ finishCallback();
+ }
+ return;
+ }
+
+ var className = this.props.name[animationType] || this.props.name + '-' + animationType;
+ var activeClassName = this.props.name[animationType + 'Active'] || className + '-active';
+ var timeout = null;
+
+ var endListener = function (e) {
+ if (e && e.target !== node) {
+ return;
+ }
+
+ clearTimeout(timeout);
+
+ CSSCore.removeClass(node, className);
+ CSSCore.removeClass(node, activeClassName);
+
+ ReactTransitionEvents.removeEndEventListener(node, endListener);
+
+ // Usually this optional callback is used for informing an owner of
+ // a leave animation and telling it to remove the child.
+ if (finishCallback) {
+ finishCallback();
+ }
+ };
+
+ CSSCore.addClass(node, className);
+
+ // Need to do this to actually trigger a transition.
+ this.queueClass(activeClassName);
+
+ // If the user specified a timeout delay.
+ if (userSpecifiedDelay) {
+ // Clean-up the animation after the specified delay
+ timeout = setTimeout(endListener, userSpecifiedDelay);
+ this.transitionTimeouts.push(timeout);
+ } else {
+ // DEPRECATED: this listener will be removed in a future version of react
+ ReactTransitionEvents.addEndEventListener(node, endListener);
+ }
+ },
+
+ queueClass: function (className) {
+ this.classNameQueue.push(className);
+
+ if (!this.timeout) {
+ this.timeout = setTimeout(this.flushClassNameQueue, TICK);
+ }
+ },
+
+ flushClassNameQueue: function () {
+ if (this.isMounted()) {
+ this.classNameQueue.forEach(CSSCore.addClass.bind(CSSCore, ReactDOM.findDOMNode(this)));
+ }
+ this.classNameQueue.length = 0;
+ this.timeout = null;
+ },
+
+ componentWillMount: function () {
+ this.classNameQueue = [];
+ this.transitionTimeouts = [];
+ },
+
+ componentWillUnmount: function () {
+ if (this.timeout) {
+ clearTimeout(this.timeout);
+ }
+ this.transitionTimeouts.forEach(function (timeout) {
+ clearTimeout(timeout);
+ });
+ },
+
+ componentWillAppear: function (done) {
+ if (this.props.appear) {
+ this.transition('appear', done, this.props.appearTimeout);
+ } else {
+ done();
+ }
+ },
+
+ componentWillEnter: function (done) {
+ if (this.props.enter) {
+ this.transition('enter', done, this.props.enterTimeout);
+ } else {
+ done();
+ }
+ },
+
+ componentWillLeave: function (done) {
+ if (this.props.leave) {
+ this.transition('leave', done, this.props.leaveTimeout);
+ } else {
+ done();
+ }
+ },
+
+ render: function () {
+ return onlyChild(this.props.children);
+ }
+});
+
+module.exports = ReactCSSTransitionGroupChild;
+},{"135":135,"145":145,"26":26,"40":40,"93":93}],31:[function(_dereq_,module,exports){
+/**
+ * Copyright 2014-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactChildReconciler
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var ReactReconciler = _dereq_(84);
+
+var instantiateReactComponent = _dereq_(132);
+var shouldUpdateReactComponent = _dereq_(141);
+var traverseAllChildren = _dereq_(142);
+var warning = _dereq_(173);
+
+function instantiateChild(childInstances, child, name) {
+ // We found a component instance.
+ var keyUnique = childInstances[name] === undefined;
+ if ("development" !== 'production') {
+ "development" !== 'production' ? warning(keyUnique, 'flattenChildren(...): Encountered two children with the same key, ' + '`%s`. Child keys must be unique; when two children share a key, only ' + 'the first child will be used.', name) : undefined;
+ }
+ if (child != null && keyUnique) {
+ childInstances[name] = instantiateReactComponent(child, null);
+ }
+}
+
+/**
+ * ReactChildReconciler provides helpers for initializing or updating a set of
+ * children. Its output is suitable for passing it onto ReactMultiChild which
+ * does diffed reordering and insertion.
+ */
+var ReactChildReconciler = {
+ /**
+ * Generates a "mount image" for each of the supplied children. In the case
+ * of `ReactDOMComponent`, a mount image is a string of markup.
+ *
+ * @param {?object} nestedChildNodes Nested child maps.
+ * @return {?object} A set of child instances.
+ * @internal
+ */
+ instantiateChildren: function (nestedChildNodes, transaction, context) {
+ if (nestedChildNodes == null) {
+ return null;
+ }
+ var childInstances = {};
+ traverseAllChildren(nestedChildNodes, instantiateChild, childInstances);
+ return childInstances;
+ },
+
+ /**
+ * Updates the rendered children and returns a new set of children.
+ *
+ * @param {?object} prevChildren Previously initialized set of children.
+ * @param {?object} nextChildren Flat child element maps.
+ * @param {ReactReconcileTransaction} transaction
+ * @param {object} context
+ * @return {?object} A new set of child instances.
+ * @internal
+ */
+ updateChildren: function (prevChildren, nextChildren, transaction, context) {
+ // We currently don't have a way to track moves here but if we use iterators
+ // instead of for..in we can zip the iterators and check if an item has
+ // moved.
+ // TODO: If nothing has changed, return the prevChildren object so that we
+ // can quickly bailout if nothing has changed.
+ if (!nextChildren && !prevChildren) {
+ return null;
+ }
+ var name;
+ for (name in nextChildren) {
+ if (!nextChildren.hasOwnProperty(name)) {
+ continue;
+ }
+ var prevChild = prevChildren && prevChildren[name];
+ var prevElement = prevChild && prevChild._currentElement;
+ var nextElement = nextChildren[name];
+ if (prevChild != null && shouldUpdateReactComponent(prevElement, nextElement)) {
+ ReactReconciler.receiveComponent(prevChild, nextElement, transaction, context);
+ nextChildren[name] = prevChild;
+ } else {
+ if (prevChild) {
+ ReactReconciler.unmountComponent(prevChild, name);
+ }
+ // The child must be instantiated before it's mounted.
+ var nextChildInstance = instantiateReactComponent(nextElement, null);
+ nextChildren[name] = nextChildInstance;
+ }
+ }
+ // Unmount children that are no longer present.
+ for (name in prevChildren) {
+ if (prevChildren.hasOwnProperty(name) && !(nextChildren && nextChildren.hasOwnProperty(name))) {
+ ReactReconciler.unmountComponent(prevChildren[name]);
+ }
+ }
+ return nextChildren;
+ },
+
+ /**
+ * Unmounts all rendered children. This should be used to clean up children
+ * when this component is unmounted.
+ *
+ * @param {?object} renderedChildren Previously initialized set of children.
+ * @internal
+ */
+ unmountChildren: function (renderedChildren) {
+ for (var name in renderedChildren) {
+ if (renderedChildren.hasOwnProperty(name)) {
+ var renderedChild = renderedChildren[name];
+ ReactReconciler.unmountComponent(renderedChild);
+ }
+ }
+ }
+
+};
+
+module.exports = ReactChildReconciler;
+},{"132":132,"141":141,"142":142,"173":173,"84":84}],32:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactChildren
+ */
+
+'use strict';
+
+var PooledClass = _dereq_(25);
+var ReactElement = _dereq_(57);
+
+var emptyFunction = _dereq_(153);
+var traverseAllChildren = _dereq_(142);
+
+var twoArgumentPooler = PooledClass.twoArgumentPooler;
+var fourArgumentPooler = PooledClass.fourArgumentPooler;
+
+var userProvidedKeyEscapeRegex = /\/(?!\/)/g;
+function escapeUserProvidedKey(text) {
+ return ('' + text).replace(userProvidedKeyEscapeRegex, '//');
+}
+
+/**
+ * PooledClass representing the bookkeeping associated with performing a child
+ * traversal. Allows avoiding binding callbacks.
+ *
+ * @constructor ForEachBookKeeping
+ * @param {!function} forEachFunction Function to perform traversal with.
+ * @param {?*} forEachContext Context to perform context with.
+ */
+function ForEachBookKeeping(forEachFunction, forEachContext) {
+ this.func = forEachFunction;
+ this.context = forEachContext;
+ this.count = 0;
+}
+ForEachBookKeeping.prototype.destructor = function () {
+ this.func = null;
+ this.context = null;
+ this.count = 0;
+};
+PooledClass.addPoolingTo(ForEachBookKeeping, twoArgumentPooler);
+
+function forEachSingleChild(bookKeeping, child, name) {
+ var func = bookKeeping.func;
+ var context = bookKeeping.context;
+
+ func.call(context, child, bookKeeping.count++);
+}
+
+/**
+ * Iterates through children that are typically specified as `props.children`.
+ *
+ * The provided forEachFunc(child, index) will be called for each
+ * leaf child.
+ *
+ * @param {?*} children Children tree container.
+ * @param {function(*, int)} forEachFunc
+ * @param {*} forEachContext Context for forEachContext.
+ */
+function forEachChildren(children, forEachFunc, forEachContext) {
+ if (children == null) {
+ return children;
+ }
+ var traverseContext = ForEachBookKeeping.getPooled(forEachFunc, forEachContext);
+ traverseAllChildren(children, forEachSingleChild, traverseContext);
+ ForEachBookKeeping.release(traverseContext);
+}
+
+/**
+ * PooledClass representing the bookkeeping associated with performing a child
+ * mapping. Allows avoiding binding callbacks.
+ *
+ * @constructor MapBookKeeping
+ * @param {!*} mapResult Object containing the ordered map of results.
+ * @param {!function} mapFunction Function to perform mapping with.
+ * @param {?*} mapContext Context to perform mapping with.
+ */
+function MapBookKeeping(mapResult, keyPrefix, mapFunction, mapContext) {
+ this.result = mapResult;
+ this.keyPrefix = keyPrefix;
+ this.func = mapFunction;
+ this.context = mapContext;
+ this.count = 0;
+}
+MapBookKeeping.prototype.destructor = function () {
+ this.result = null;
+ this.keyPrefix = null;
+ this.func = null;
+ this.context = null;
+ this.count = 0;
+};
+PooledClass.addPoolingTo(MapBookKeeping, fourArgumentPooler);
+
+function mapSingleChildIntoContext(bookKeeping, child, childKey) {
+ var result = bookKeeping.result;
+ var keyPrefix = bookKeeping.keyPrefix;
+ var func = bookKeeping.func;
+ var context = bookKeeping.context;
+
+ var mappedChild = func.call(context, child, bookKeeping.count++);
+ if (Array.isArray(mappedChild)) {
+ mapIntoWithKeyPrefixInternal(mappedChild, result, childKey, emptyFunction.thatReturnsArgument);
+ } else if (mappedChild != null) {
+ if (ReactElement.isValidElement(mappedChild)) {
+ mappedChild = ReactElement.cloneAndReplaceKey(mappedChild,
+ // Keep both the (mapped) and old keys if they differ, just as
+ // traverseAllChildren used to do for objects as children
+ keyPrefix + (mappedChild !== child ? escapeUserProvidedKey(mappedChild.key || '') + '/' : '') + childKey);
+ }
+ result.push(mappedChild);
+ }
+}
+
+function mapIntoWithKeyPrefixInternal(children, array, prefix, func, context) {
+ var escapedPrefix = '';
+ if (prefix != null) {
+ escapedPrefix = escapeUserProvidedKey(prefix) + '/';
+ }
+ var traverseContext = MapBookKeeping.getPooled(array, escapedPrefix, func, context);
+ traverseAllChildren(children, mapSingleChildIntoContext, traverseContext);
+ MapBookKeeping.release(traverseContext);
+}
+
+/**
+ * Maps children that are typically specified as `props.children`.
+ *
+ * The provided mapFunction(child, key, index) will be called for each
+ * leaf child.
+ *
+ * @param {?*} children Children tree container.
+ * @param {function(*, int)} func The map function.
+ * @param {*} context Context for mapFunction.
+ * @return {object} Object containing the ordered map of results.
+ */
+function mapChildren(children, func, context) {
+ if (children == null) {
+ return children;
+ }
+ var result = [];
+ mapIntoWithKeyPrefixInternal(children, result, null, func, context);
+ return result;
+}
+
+function forEachSingleChildDummy(traverseContext, child, name) {
+ return null;
+}
+
+/**
+ * Count the number of children that are typically specified as
+ * `props.children`.
+ *
+ * @param {?*} children Children tree container.
+ * @return {number} The number of children.
+ */
+function countChildren(children, context) {
+ return traverseAllChildren(children, forEachSingleChildDummy, null);
+}
+
+/**
+ * Flatten a children object (typically specified as `props.children`) and
+ * return an array with appropriately re-keyed children.
+ */
+function toArray(children) {
+ var result = [];
+ mapIntoWithKeyPrefixInternal(children, result, null, emptyFunction.thatReturnsArgument);
+ return result;
+}
+
+var ReactChildren = {
+ forEach: forEachChildren,
+ map: mapChildren,
+ mapIntoWithKeyPrefixInternal: mapIntoWithKeyPrefixInternal,
+ count: countChildren,
+ toArray: toArray
+};
+
+module.exports = ReactChildren;
+},{"142":142,"153":153,"25":25,"57":57}],33:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactClass
+ */
+
+'use strict';
+
+var ReactComponent = _dereq_(34);
+var ReactElement = _dereq_(57);
+var ReactPropTypeLocations = _dereq_(81);
+var ReactPropTypeLocationNames = _dereq_(80);
+var ReactNoopUpdateQueue = _dereq_(76);
+
+var assign = _dereq_(24);
+var emptyObject = _dereq_(154);
+var invariant = _dereq_(161);
+var keyMirror = _dereq_(165);
+var keyOf = _dereq_(166);
+var warning = _dereq_(173);
+
+var MIXINS_KEY = keyOf({ mixins: null });
+
+/**
+ * Policies that describe methods in `ReactClassInterface`.
+ */
+var SpecPolicy = keyMirror({
+ /**
+ * These methods may be defined only once by the class specification or mixin.
+ */
+ DEFINE_ONCE: null,
+ /**
+ * These methods may be defined by both the class specification and mixins.
+ * Subsequent definitions will be chained. These methods must return void.
+ */
+ DEFINE_MANY: null,
+ /**
+ * These methods are overriding the base class.
+ */
+ OVERRIDE_BASE: null,
+ /**
+ * These methods are similar to DEFINE_MANY, except we assume they return
+ * objects. We try to merge the keys of the return values of all the mixed in
+ * functions. If there is a key conflict we throw.
+ */
+ DEFINE_MANY_MERGED: null
+});
+
+var injectedMixins = [];
+
+var warnedSetProps = false;
+function warnSetProps() {
+ if (!warnedSetProps) {
+ warnedSetProps = true;
+ "development" !== 'production' ? warning(false, 'setProps(...) and replaceProps(...) are deprecated. ' + 'Instead, call render again at the top level.') : undefined;
+ }
+}
+
+/**
+ * Composite components are higher-level components that compose other composite
+ * or native components.
+ *
+ * To create a new type of `ReactClass`, pass a specification of
+ * your new class to `React.createClass`. The only requirement of your class
+ * specification is that you implement a `render` method.
+ *
+ * var MyComponent = React.createClass({
+ * render: function() {
+ * return <div>Hello World</div>;
+ * }
+ * });
+ *
+ * The class specification supports a specific protocol of methods that have
+ * special meaning (e.g. `render`). See `ReactClassInterface` for
+ * more the comprehensive protocol. Any other properties and methods in the
+ * class specification will be available on the prototype.
+ *
+ * @interface ReactClassInterface
+ * @internal
+ */
+var ReactClassInterface = {
+
+ /**
+ * An array of Mixin objects to include when defining your component.
+ *
+ * @type {array}
+ * @optional
+ */
+ mixins: SpecPolicy.DEFINE_MANY,
+
+ /**
+ * An object containing properties and methods that should be defined on
+ * the component's constructor instead of its prototype (static methods).
+ *
+ * @type {object}
+ * @optional
+ */
+ statics: SpecPolicy.DEFINE_MANY,
+
+ /**
+ * Definition of prop types for this component.
+ *
+ * @type {object}
+ * @optional
+ */
+ propTypes: SpecPolicy.DEFINE_MANY,
+
+ /**
+ * Definition of context types for this component.
+ *
+ * @type {object}
+ * @optional
+ */
+ contextTypes: SpecPolicy.DEFINE_MANY,
+
+ /**
+ * Definition of context types this component sets for its children.
+ *
+ * @type {object}
+ * @optional
+ */
+ childContextTypes: SpecPolicy.DEFINE_MANY,
+
+ // ==== Definition methods ====
+
+ /**
+ * Invoked when the component is mounted. Values in the mapping will be set on
+ * `this.props` if that prop is not specified (i.e. using an `in` check).
+ *
+ * This method is invoked before `getInitialState` and therefore cannot rely
+ * on `this.state` or use `this.setState`.
+ *
+ * @return {object}
+ * @optional
+ */
+ getDefaultProps: SpecPolicy.DEFINE_MANY_MERGED,
+
+ /**
+ * Invoked once before the component is mounted. The return value will be used
+ * as the initial value of `this.state`.
+ *
+ * getInitialState: function() {
+ * return {
+ * isOn: false,
+ * fooBaz: new BazFoo()
+ * }
+ * }
+ *
+ * @return {object}
+ * @optional
+ */
+ getInitialState: SpecPolicy.DEFINE_MANY_MERGED,
+
+ /**
+ * @return {object}
+ * @optional
+ */
+ getChildContext: SpecPolicy.DEFINE_MANY_MERGED,
+
+ /**
+ * Uses props from `this.props` and state from `this.state` to render the
+ * structure of the component.
+ *
+ * No guarantees are made about when or how often this method is invoked, so
+ * it must not have side effects.
+ *
+ * render: function() {
+ * var name = this.props.name;
+ * return <div>Hello, {name}!</div>;
+ * }
+ *
+ * @return {ReactComponent}
+ * @nosideeffects
+ * @required
+ */
+ render: SpecPolicy.DEFINE_ONCE,
+
+ // ==== Delegate methods ====
+
+ /**
+ * Invoked when the component is initially created and about to be mounted.
+ * This may have side effects, but any external subscriptions or data created
+ * by this method must be cleaned up in `componentWillUnmount`.
+ *
+ * @optional
+ */
+ componentWillMount: SpecPolicy.DEFINE_MANY,
+
+ /**
+ * Invoked when the component has been mounted and has a DOM representation.
+ * However, there is no guarantee that the DOM node is in the document.
+ *
+ * Use this as an opportunity to operate on the DOM when the component has
+ * been mounted (initialized and rendered) for the first time.
+ *
+ * @param {DOMElement} rootNode DOM element representing the component.
+ * @optional
+ */
+ componentDidMount: SpecPolicy.DEFINE_MANY,
+
+ /**
+ * Invoked before the component receives new props.
+ *
+ * Use this as an opportunity to react to a prop transition by updating the
+ * state using `this.setState`. Current props are accessed via `this.props`.
+ *
+ * componentWillReceiveProps: function(nextProps, nextContext) {
+ * this.setState({
+ * likesIncreasing: nextProps.likeCount > this.props.likeCount
+ * });
+ * }
+ *
+ * NOTE: There is no equivalent `componentWillReceiveState`. An incoming prop
+ * transition may cause a state change, but the opposite is not true. If you
+ * need it, you are probably looking for `componentWillUpdate`.
+ *
+ * @param {object} nextProps
+ * @optional
+ */
+ componentWillReceiveProps: SpecPolicy.DEFINE_MANY,
+
+ /**
+ * Invoked while deciding if the component should be updated as a result of
+ * receiving new props, state and/or context.
+ *
+ * Use this as an opportunity to `return false` when you're certain that the
+ * transition to the new props/state/context will not require a component
+ * update.
+ *
+ * shouldComponentUpdate: function(nextProps, nextState, nextContext) {
+ * return !equal(nextProps, this.props) ||
+ * !equal(nextState, this.state) ||
+ * !equal(nextContext, this.context);
+ * }
+ *
+ * @param {object} nextProps
+ * @param {?object} nextState
+ * @param {?object} nextContext
+ * @return {boolean} True if the component should update.
+ * @optional
+ */
+ shouldComponentUpdate: SpecPolicy.DEFINE_ONCE,
+
+ /**
+ * Invoked when the component is about to update due to a transition from
+ * `this.props`, `this.state` and `this.context` to `nextProps`, `nextState`
+ * and `nextContext`.
+ *
+ * Use this as an opportunity to perform preparation before an update occurs.
+ *
+ * NOTE: You **cannot** use `this.setState()` in this method.
+ *
+ * @param {object} nextProps
+ * @param {?object} nextState
+ * @param {?object} nextContext
+ * @param {ReactReconcileTransaction} transaction
+ * @optional
+ */
+ componentWillUpdate: SpecPolicy.DEFINE_MANY,
+
+ /**
+ * Invoked when the component's DOM representation has been updated.
+ *
+ * Use this as an opportunity to operate on the DOM when the component has
+ * been updated.
+ *
+ * @param {object} prevProps
+ * @param {?object} prevState
+ * @param {?object} prevContext
+ * @param {DOMElement} rootNode DOM element representing the component.
+ * @optional
+ */
+ componentDidUpdate: SpecPolicy.DEFINE_MANY,
+
+ /**
+ * Invoked when the component is about to be removed from its parent and have
+ * its DOM representation destroyed.
+ *
+ * Use this as an opportunity to deallocate any external resources.
+ *
+ * NOTE: There is no `componentDidUnmount` since your component will have been
+ * destroyed by that point.
+ *
+ * @optional
+ */
+ componentWillUnmount: SpecPolicy.DEFINE_MANY,
+
+ // ==== Advanced methods ====
+
+ /**
+ * Updates the component's currently mounted DOM representation.
+ *
+ * By default, this implements React's rendering and reconciliation algorithm.
+ * Sophisticated clients may wish to override this.
+ *
+ * @param {ReactReconcileTransaction} transaction
+ * @internal
+ * @overridable
+ */
+ updateComponent: SpecPolicy.OVERRIDE_BASE
+
+};
+
+/**
+ * Mapping from class specification keys to special processing functions.
+ *
+ * Although these are declared like instance properties in the specification
+ * when defining classes using `React.createClass`, they are actually static
+ * and are accessible on the constructor instead of the prototype. Despite
+ * being static, they must be defined outside of the "statics" key under
+ * which all other static methods are defined.
+ */
+var RESERVED_SPEC_KEYS = {
+ displayName: function (Constructor, displayName) {
+ Constructor.displayName = displayName;
+ },
+ mixins: function (Constructor, mixins) {
+ if (mixins) {
+ for (var i = 0; i < mixins.length; i++) {
+ mixSpecIntoComponent(Constructor, mixins[i]);
+ }
+ }
+ },
+ childContextTypes: function (Constructor, childContextTypes) {
+ if ("development" !== 'production') {
+ validateTypeDef(Constructor, childContextTypes, ReactPropTypeLocations.childContext);
+ }
+ Constructor.childContextTypes = assign({}, Constructor.childContextTypes, childContextTypes);
+ },
+ contextTypes: function (Constructor, contextTypes) {
+ if ("development" !== 'production') {
+ validateTypeDef(Constructor, contextTypes, ReactPropTypeLocations.context);
+ }
+ Constructor.contextTypes = assign({}, Constructor.contextTypes, contextTypes);
+ },
+ /**
+ * Special case getDefaultProps which should move into statics but requires
+ * automatic merging.
+ */
+ getDefaultProps: function (Constructor, getDefaultProps) {
+ if (Constructor.getDefaultProps) {
+ Constructor.getDefaultProps = createMergedResultFunction(Constructor.getDefaultProps, getDefaultProps);
+ } else {
+ Constructor.getDefaultProps = getDefaultProps;
+ }
+ },
+ propTypes: function (Constructor, propTypes) {
+ if ("development" !== 'production') {
+ validateTypeDef(Constructor, propTypes, ReactPropTypeLocations.prop);
+ }
+ Constructor.propTypes = assign({}, Constructor.propTypes, propTypes);
+ },
+ statics: function (Constructor, statics) {
+ mixStaticSpecIntoComponent(Constructor, statics);
+ },
+ autobind: function () {} };
+
+// noop
+function validateTypeDef(Constructor, typeDef, location) {
+ for (var propName in typeDef) {
+ if (typeDef.hasOwnProperty(propName)) {
+ // use a warning instead of an invariant so components
+ // don't show up in prod but not in __DEV__
+ "development" !== 'production' ? warning(typeof typeDef[propName] === 'function', '%s: %s type `%s` is invalid; it must be a function, usually from ' + 'React.PropTypes.', Constructor.displayName || 'ReactClass', ReactPropTypeLocationNames[location], propName) : undefined;
+ }
+ }
+}
+
+function validateMethodOverride(proto, name) {
+ var specPolicy = ReactClassInterface.hasOwnProperty(name) ? ReactClassInterface[name] : null;
+
+ // Disallow overriding of base class methods unless explicitly allowed.
+ if (ReactClassMixin.hasOwnProperty(name)) {
+ !(specPolicy === SpecPolicy.OVERRIDE_BASE) ? "development" !== 'production' ? invariant(false, 'ReactClassInterface: You are attempting to override ' + '`%s` from your class specification. Ensure that your method names ' + 'do not overlap with React methods.', name) : invariant(false) : undefined;
+ }
+
+ // Disallow defining methods more than once unless explicitly allowed.
+ if (proto.hasOwnProperty(name)) {
+ !(specPolicy === SpecPolicy.DEFINE_MANY || specPolicy === SpecPolicy.DEFINE_MANY_MERGED) ? "development" !== 'production' ? invariant(false, 'ReactClassInterface: You are attempting to define ' + '`%s` on your component more than once. This conflict may be due ' + 'to a mixin.', name) : invariant(false) : undefined;
+ }
+}
+
+/**
+ * Mixin helper which handles policy validation and reserved
+ * specification keys when building React classses.
+ */
+function mixSpecIntoComponent(Constructor, spec) {
+ if (!spec) {
+ return;
+ }
+
+ !(typeof spec !== 'function') ? "development" !== 'production' ? invariant(false, 'ReactClass: You\'re attempting to ' + 'use a component class as a mixin. Instead, just use a regular object.') : invariant(false) : undefined;
+ !!ReactElement.isValidElement(spec) ? "development" !== 'production' ? invariant(false, 'ReactClass: You\'re attempting to ' + 'use a component as a mixin. Instead, just use a regular object.') : invariant(false) : undefined;
+
+ var proto = Constructor.prototype;
+
+ // By handling mixins before any other properties, we ensure the same
+ // chaining order is applied to methods with DEFINE_MANY policy, whether
+ // mixins are listed before or after these methods in the spec.
+ if (spec.hasOwnProperty(MIXINS_KEY)) {
+ RESERVED_SPEC_KEYS.mixins(Constructor, spec.mixins);
+ }
+
+ for (var name in spec) {
+ if (!spec.hasOwnProperty(name)) {
+ continue;
+ }
+
+ if (name === MIXINS_KEY) {
+ // We have already handled mixins in a special case above.
+ continue;
+ }
+
+ var property = spec[name];
+ validateMethodOverride(proto, name);
+
+ if (RESERVED_SPEC_KEYS.hasOwnProperty(name)) {
+ RESERVED_SPEC_KEYS[name](Constructor, property);
+ } else {
+ // Setup methods on prototype:
+ // The following member methods should not be automatically bound:
+ // 1. Expected ReactClass methods (in the "interface").
+ // 2. Overridden methods (that were mixed in).
+ var isReactClassMethod = ReactClassInterface.hasOwnProperty(name);
+ var isAlreadyDefined = proto.hasOwnProperty(name);
+ var isFunction = typeof property === 'function';
+ var shouldAutoBind = isFunction && !isReactClassMethod && !isAlreadyDefined && spec.autobind !== false;
+
+ if (shouldAutoBind) {
+ if (!proto.__reactAutoBindMap) {
+ proto.__reactAutoBindMap = {};
+ }
+ proto.__reactAutoBindMap[name] = property;
+ proto[name] = property;
+ } else {
+ if (isAlreadyDefined) {
+ var specPolicy = ReactClassInterface[name];
+
+ // These cases should already be caught by validateMethodOverride.
+ !(isReactClassMethod && (specPolicy === SpecPolicy.DEFINE_MANY_MERGED || specPolicy === SpecPolicy.DEFINE_MANY)) ? "development" !== 'production' ? invariant(false, 'ReactClass: Unexpected spec policy %s for key %s ' + 'when mixing in component specs.', specPolicy, name) : invariant(false) : undefined;
+
+ // For methods which are defined more than once, call the existing
+ // methods before calling the new property, merging if appropriate.
+ if (specPolicy === SpecPolicy.DEFINE_MANY_MERGED) {
+ proto[name] = createMergedResultFunction(proto[name], property);
+ } else if (specPolicy === SpecPolicy.DEFINE_MANY) {
+ proto[name] = createChainedFunction(proto[name], property);
+ }
+ } else {
+ proto[name] = property;
+ if ("development" !== 'production') {
+ // Add verbose displayName to the function, which helps when looking
+ // at profiling tools.
+ if (typeof property === 'function' && spec.displayName) {
+ proto[name].displayName = spec.displayName + '_' + name;
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+function mixStaticSpecIntoComponent(Constructor, statics) {
+ if (!statics) {
+ return;
+ }
+ for (var name in statics) {
+ var property = statics[name];
+ if (!statics.hasOwnProperty(name)) {
+ continue;
+ }
+
+ var isReserved = (name in RESERVED_SPEC_KEYS);
+ !!isReserved ? "development" !== 'production' ? invariant(false, 'ReactClass: You are attempting to define a reserved ' + 'property, `%s`, that shouldn\'t be on the "statics" key. Define it ' + 'as an instance property instead; it will still be accessible on the ' + 'constructor.', name) : invariant(false) : undefined;
+
+ var isInherited = (name in Constructor);
+ !!isInherited ? "development" !== 'production' ? invariant(false, 'ReactClass: You are attempting to define ' + '`%s` on your component more than once. This conflict may be ' + 'due to a mixin.', name) : invariant(false) : undefined;
+ Constructor[name] = property;
+ }
+}
+
+/**
+ * Merge two objects, but throw if both contain the same key.
+ *
+ * @param {object} one The first object, which is mutated.
+ * @param {object} two The second object
+ * @return {object} one after it has been mutated to contain everything in two.
+ */
+function mergeIntoWithNoDuplicateKeys(one, two) {
+ !(one && two && typeof one === 'object' && typeof two === 'object') ? "development" !== 'production' ? invariant(false, 'mergeIntoWithNoDuplicateKeys(): Cannot merge non-objects.') : invariant(false) : undefined;
+
+ for (var key in two) {
+ if (two.hasOwnProperty(key)) {
+ !(one[key] === undefined) ? "development" !== 'production' ? invariant(false, 'mergeIntoWithNoDuplicateKeys(): ' + 'Tried to merge two objects with the same key: `%s`. This conflict ' + 'may be due to a mixin; in particular, this may be caused by two ' + 'getInitialState() or getDefaultProps() methods returning objects ' + 'with clashing keys.', key) : invariant(false) : undefined;
+ one[key] = two[key];
+ }
+ }
+ return one;
+}
+
+/**
+ * Creates a function that invokes two functions and merges their return values.
+ *
+ * @param {function} one Function to invoke first.
+ * @param {function} two Function to invoke second.
+ * @return {function} Function that invokes the two argument functions.
+ * @private
+ */
+function createMergedResultFunction(one, two) {
+ return function mergedResult() {
+ var a = one.apply(this, arguments);
+ var b = two.apply(this, arguments);
+ if (a == null) {
+ return b;
+ } else if (b == null) {
+ return a;
+ }
+ var c = {};
+ mergeIntoWithNoDuplicateKeys(c, a);
+ mergeIntoWithNoDuplicateKeys(c, b);
+ return c;
+ };
+}
+
+/**
+ * Creates a function that invokes two functions and ignores their return vales.
+ *
+ * @param {function} one Function to invoke first.
+ * @param {function} two Function to invoke second.
+ * @return {function} Function that invokes the two argument functions.
+ * @private
+ */
+function createChainedFunction(one, two) {
+ return function chainedFunction() {
+ one.apply(this, arguments);
+ two.apply(this, arguments);
+ };
+}
+
+/**
+ * Binds a method to the component.
+ *
+ * @param {object} component Component whose method is going to be bound.
+ * @param {function} method Method to be bound.
+ * @return {function} The bound method.
+ */
+function bindAutoBindMethod(component, method) {
+ var boundMethod = method.bind(component);
+ if ("development" !== 'production') {
+ boundMethod.__reactBoundContext = component;
+ boundMethod.__reactBoundMethod = method;
+ boundMethod.__reactBoundArguments = null;
+ var componentName = component.constructor.displayName;
+ var _bind = boundMethod.bind;
+ /* eslint-disable block-scoped-var, no-undef */
+ boundMethod.bind = function (newThis) {
+ for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
+ args[_key - 1] = arguments[_key];
+ }
+
+ // User is trying to bind() an autobound method; we effectively will
+ // ignore the value of "this" that the user is trying to use, so
+ // let's warn.
+ if (newThis !== component && newThis !== null) {
+ "development" !== 'production' ? warning(false, 'bind(): React component methods may only be bound to the ' + 'component instance. See %s', componentName) : undefined;
+ } else if (!args.length) {
+ "development" !== 'production' ? warning(false, 'bind(): You are binding a component method to the component. ' + 'React does this for you automatically in a high-performance ' + 'way, so you can safely remove this call. See %s', componentName) : undefined;
+ return boundMethod;
+ }
+ var reboundMethod = _bind.apply(boundMethod, arguments);
+ reboundMethod.__reactBoundContext = component;
+ reboundMethod.__reactBoundMethod = method;
+ reboundMethod.__reactBoundArguments = args;
+ return reboundMethod;
+ /* eslint-enable */
+ };
+ }
+ return boundMethod;
+}
+
+/**
+ * Binds all auto-bound methods in a component.
+ *
+ * @param {object} component Component whose method is going to be bound.
+ */
+function bindAutoBindMethods(component) {
+ for (var autoBindKey in component.__reactAutoBindMap) {
+ if (component.__reactAutoBindMap.hasOwnProperty(autoBindKey)) {
+ var method = component.__reactAutoBindMap[autoBindKey];
+ component[autoBindKey] = bindAutoBindMethod(component, method);
+ }
+ }
+}
+
+/**
+ * Add more to the ReactClass base class. These are all legacy features and
+ * therefore not already part of the modern ReactComponent.
+ */
+var ReactClassMixin = {
+
+ /**
+ * TODO: This will be deprecated because state should always keep a consistent
+ * type signature and the only use case for this, is to avoid that.
+ */
+ replaceState: function (newState, callback) {
+ this.updater.enqueueReplaceState(this, newState);
+ if (callback) {
+ this.updater.enqueueCallback(this, callback);
+ }
+ },
+
+ /**
+ * Checks whether or not this composite component is mounted.
+ * @return {boolean} True if mounted, false otherwise.
+ * @protected
+ * @final
+ */
+ isMounted: function () {
+ return this.updater.isMounted(this);
+ },
+
+ /**
+ * Sets a subset of the props.
+ *
+ * @param {object} partialProps Subset of the next props.
+ * @param {?function} callback Called after props are updated.
+ * @final
+ * @public
+ * @deprecated
+ */
+ setProps: function (partialProps, callback) {
+ if ("development" !== 'production') {
+ warnSetProps();
+ }
+ this.updater.enqueueSetProps(this, partialProps);
+ if (callback) {
+ this.updater.enqueueCallback(this, callback);
+ }
+ },
+
+ /**
+ * Replace all the props.
+ *
+ * @param {object} newProps Subset of the next props.
+ * @param {?function} callback Called after props are updated.
+ * @final
+ * @public
+ * @deprecated
+ */
+ replaceProps: function (newProps, callback) {
+ if ("development" !== 'production') {
+ warnSetProps();
+ }
+ this.updater.enqueueReplaceProps(this, newProps);
+ if (callback) {
+ this.updater.enqueueCallback(this, callback);
+ }
+ }
+};
+
+var ReactClassComponent = function () {};
+assign(ReactClassComponent.prototype, ReactComponent.prototype, ReactClassMixin);
+
+/**
+ * Module for creating composite components.
+ *
+ * @class ReactClass
+ */
+var ReactClass = {
+
+ /**
+ * Creates a composite component class given a class specification.
+ *
+ * @param {object} spec Class specification (which must define `render`).
+ * @return {function} Component constructor function.
+ * @public
+ */
+ createClass: function (spec) {
+ var Constructor = function (props, context, updater) {
+ // This constructor is overridden by mocks. The argument is used
+ // by mocks to assert on what gets mounted.
+
+ if ("development" !== 'production') {
+ "development" !== 'production' ? warning(this instanceof Constructor, 'Something is calling a React component directly. Use a factory or ' + 'JSX instead. See: https://fb.me/react-legacyfactory') : undefined;
+ }
+
+ // Wire up auto-binding
+ if (this.__reactAutoBindMap) {
+ bindAutoBindMethods(this);
+ }
+
+ this.props = props;
+ this.context = context;
+ this.refs = emptyObject;
+ this.updater = updater || ReactNoopUpdateQueue;
+
+ this.state = null;
+
+ // ReactClasses doesn't have constructors. Instead, they use the
+ // getInitialState and componentWillMount methods for initialization.
+
+ var initialState = this.getInitialState ? this.getInitialState() : null;
+ if ("development" !== 'production') {
+ // We allow auto-mocks to proceed as if they're returning null.
+ if (typeof initialState === 'undefined' && this.getInitialState._isMockFunction) {
+ // This is probably bad practice. Consider warning here and
+ // deprecating this convenience.
+ initialState = null;
+ }
+ }
+ !(typeof initialState === 'object' && !Array.isArray(initialState)) ? "development" !== 'production' ? invariant(false, '%s.getInitialState(): must return an object or null', Constructor.displayName || 'ReactCompositeComponent') : invariant(false) : undefined;
+
+ this.state = initialState;
+ };
+ Constructor.prototype = new ReactClassComponent();
+ Constructor.prototype.constructor = Constructor;
+
+ injectedMixins.forEach(mixSpecIntoComponent.bind(null, Constructor));
+
+ mixSpecIntoComponent(Constructor, spec);
+
+ // Initialize the defaultProps property after all mixins have been merged.
+ if (Constructor.getDefaultProps) {
+ Constructor.defaultProps = Constructor.getDefaultProps();
+ }
+
+ if ("development" !== 'production') {
+ // This is a tag to indicate that the use of these method names is ok,
+ // since it's used with createClass. If it's not, then it's likely a
+ // mistake so we'll warn you to use the static property, property
+ // initializer or constructor respectively.
+ if (Constructor.getDefaultProps) {
+ Constructor.getDefaultProps.isReactClassApproved = {};
+ }
+ if (Constructor.prototype.getInitialState) {
+ Constructor.prototype.getInitialState.isReactClassApproved = {};
+ }
+ }
+
+ !Constructor.prototype.render ? "development" !== 'production' ? invariant(false, 'createClass(...): Class specification must implement a `render` method.') : invariant(false) : undefined;
+
+ if ("development" !== 'production') {
+ "development" !== 'production' ? warning(!Constructor.prototype.componentShouldUpdate, '%s has a method called ' + 'componentShouldUpdate(). Did you mean shouldComponentUpdate()? ' + 'The name is phrased as a question because the function is ' + 'expected to return a value.', spec.displayName || 'A component') : undefined;
+ "development" !== 'production' ? warning(!Constructor.prototype.componentWillRecieveProps, '%s has a method called ' + 'componentWillRecieveProps(). Did you mean componentWillReceiveProps()?', spec.displayName || 'A component') : undefined;
+ }
+
+ // Reduce time spent doing lookups by setting these on the prototype.
+ for (var methodName in ReactClassInterface) {
+ if (!Constructor.prototype[methodName]) {
+ Constructor.prototype[methodName] = null;
+ }
+ }
+
+ return Constructor;
+ },
+
+ injection: {
+ injectMixin: function (mixin) {
+ injectedMixins.push(mixin);
+ }
+ }
+
+};
+
+module.exports = ReactClass;
+},{"154":154,"161":161,"165":165,"166":166,"173":173,"24":24,"34":34,"57":57,"76":76,"80":80,"81":81}],34:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactComponent
+ */
+
+'use strict';
+
+var ReactNoopUpdateQueue = _dereq_(76);
+
+var canDefineProperty = _dereq_(117);
+var emptyObject = _dereq_(154);
+var invariant = _dereq_(161);
+var warning = _dereq_(173);
+
+/**
+ * Base class helpers for the updating state of a component.
+ */
+function ReactComponent(props, context, updater) {
+ this.props = props;
+ this.context = context;
+ this.refs = emptyObject;
+ // We initialize the default updater but the real one gets injected by the
+ // renderer.
+ this.updater = updater || ReactNoopUpdateQueue;
+}
+
+ReactComponent.prototype.isReactComponent = {};
+
+/**
+ * Sets a subset of the state. Always use this to mutate
+ * state. You should treat `this.state` as immutable.
+ *
+ * There is no guarantee that `this.state` will be immediately updated, so
+ * accessing `this.state` after calling this method may return the old value.
+ *
+ * There is no guarantee that calls to `setState` will run synchronously,
+ * as they may eventually be batched together. You can provide an optional
+ * callback that will be executed when the call to setState is actually
+ * completed.
+ *
+ * When a function is provided to setState, it will be called at some point in
+ * the future (not synchronously). It will be called with the up to date
+ * component arguments (state, props, context). These values can be different
+ * from this.* because your function may be called after receiveProps but before
+ * shouldComponentUpdate, and this new state, props, and context will not yet be
+ * assigned to this.
+ *
+ * @param {object|function} partialState Next partial state or function to
+ * produce next partial state to be merged with current state.
+ * @param {?function} callback Called after state is updated.
+ * @final
+ * @protected
+ */
+ReactComponent.prototype.setState = function (partialState, callback) {
+ !(typeof partialState === 'object' || typeof partialState === 'function' || partialState == null) ? "development" !== 'production' ? invariant(false, 'setState(...): takes an object of state variables to update or a ' + 'function which returns an object of state variables.') : invariant(false) : undefined;
+ if ("development" !== 'production') {
+ "development" !== 'production' ? warning(partialState != null, 'setState(...): You passed an undefined or null state object; ' + 'instead, use forceUpdate().') : undefined;
+ }
+ this.updater.enqueueSetState(this, partialState);
+ if (callback) {
+ this.updater.enqueueCallback(this, callback);
+ }
+};
+
+/**
+ * Forces an update. This should only be invoked when it is known with
+ * certainty that we are **not** in a DOM transaction.
+ *
+ * You may want to call this when you know that some deeper aspect of the
+ * component's state has changed but `setState` was not called.
+ *
+ * This will not invoke `shouldComponentUpdate`, but it will invoke
+ * `componentWillUpdate` and `componentDidUpdate`.
+ *
+ * @param {?function} callback Called after update is complete.
+ * @final
+ * @protected
+ */
+ReactComponent.prototype.forceUpdate = function (callback) {
+ this.updater.enqueueForceUpdate(this);
+ if (callback) {
+ this.updater.enqueueCallback(this, callback);
+ }
+};
+
+/**
+ * Deprecated APIs. These APIs used to exist on classic React classes but since
+ * we would like to deprecate them, we're not going to move them over to this
+ * modern base class. Instead, we define a getter that warns if it's accessed.
+ */
+if ("development" !== 'production') {
+ var deprecatedAPIs = {
+ getDOMNode: ['getDOMNode', 'Use ReactDOM.findDOMNode(component) instead.'],
+ isMounted: ['isMounted', 'Instead, make sure to clean up subscriptions and pending requests in ' + 'componentWillUnmount to prevent memory leaks.'],
+ replaceProps: ['replaceProps', 'Instead, call render again at the top level.'],
+ replaceState: ['replaceState', 'Refactor your code to use setState instead (see ' + 'https://github.com/facebook/react/issues/3236).'],
+ setProps: ['setProps', 'Instead, call render again at the top level.']
+ };
+ var defineDeprecationWarning = function (methodName, info) {
+ if (canDefineProperty) {
+ Object.defineProperty(ReactComponent.prototype, methodName, {
+ get: function () {
+ "development" !== 'production' ? warning(false, '%s(...) is deprecated in plain JavaScript React classes. %s', info[0], info[1]) : undefined;
+ return undefined;
+ }
+ });
+ }
+ };
+ for (var fnName in deprecatedAPIs) {
+ if (deprecatedAPIs.hasOwnProperty(fnName)) {
+ defineDeprecationWarning(fnName, deprecatedAPIs[fnName]);
+ }
+ }
+}
+
+module.exports = ReactComponent;
+},{"117":117,"154":154,"161":161,"173":173,"76":76}],35:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactComponentBrowserEnvironment
+ */
+
+'use strict';
+
+var ReactDOMIDOperations = _dereq_(45);
+var ReactMount = _dereq_(72);
+
+/**
+ * Abstracts away all functionality of the reconciler that requires knowledge of
+ * the browser context. TODO: These callers should be refactored to avoid the
+ * need for this injection.
+ */
+var ReactComponentBrowserEnvironment = {
+
+ processChildrenUpdates: ReactDOMIDOperations.dangerouslyProcessChildrenUpdates,
+
+ replaceNodeWithMarkupByID: ReactDOMIDOperations.dangerouslyReplaceNodeWithMarkupByID,
+
+ /**
+ * If a particular environment requires that some resources be cleaned up,
+ * specify this in the injected Mixin. In the DOM, we would likely want to
+ * purge any cached node ID lookups.
+ *
+ * @private
+ */
+ unmountIDFromEnvironment: function (rootNodeID) {
+ ReactMount.purgeID(rootNodeID);
+ }
+
+};
+
+module.exports = ReactComponentBrowserEnvironment;
+},{"45":45,"72":72}],36:[function(_dereq_,module,exports){
+/**
+ * Copyright 2014-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactComponentEnvironment
+ */
+
+'use strict';
+
+var invariant = _dereq_(161);
+
+var injected = false;
+
+var ReactComponentEnvironment = {
+
+ /**
+ * Optionally injectable environment dependent cleanup hook. (server vs.
+ * browser etc). Example: A browser system caches DOM nodes based on component
+ * ID and must remove that cache entry when this instance is unmounted.
+ */
+ unmountIDFromEnvironment: null,
+
+ /**
+ * Optionally injectable hook for swapping out mount images in the middle of
+ * the tree.
+ */
+ replaceNodeWithMarkupByID: null,
+
+ /**
+ * Optionally injectable hook for processing a queue of child updates. Will
+ * later move into MultiChildComponents.
+ */
+ processChildrenUpdates: null,
+
+ injection: {
+ injectEnvironment: function (environment) {
+ !!injected ? "development" !== 'production' ? invariant(false, 'ReactCompositeComponent: injectEnvironment() can only be called once.') : invariant(false) : undefined;
+ ReactComponentEnvironment.unmountIDFromEnvironment = environment.unmountIDFromEnvironment;
+ ReactComponentEnvironment.replaceNodeWithMarkupByID = environment.replaceNodeWithMarkupByID;
+ ReactComponentEnvironment.processChildrenUpdates = environment.processChildrenUpdates;
+ injected = true;
+ }
+ }
+
+};
+
+module.exports = ReactComponentEnvironment;
+},{"161":161}],37:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactComponentWithPureRenderMixin
+ */
+
+'use strict';
+
+var shallowCompare = _dereq_(140);
+
+/**
+ * If your React component's render function is "pure", e.g. it will render the
+ * same result given the same props and state, provide this Mixin for a
+ * considerable performance boost.
+ *
+ * Most React components have pure render functions.
+ *
+ * Example:
+ *
+ * var ReactComponentWithPureRenderMixin =
+ * require('ReactComponentWithPureRenderMixin');
+ * React.createClass({
+ * mixins: [ReactComponentWithPureRenderMixin],
+ *
+ * render: function() {
+ * return <div className={this.props.className}>foo</div>;
+ * }
+ * });
+ *
+ * Note: This only checks shallow equality for props and state. If these contain
+ * complex data structures this mixin may have false-negatives for deeper
+ * differences. Only mixin to components which have simple props and state, or
+ * use `forceUpdate()` when you know deep data structures have changed.
+ */
+var ReactComponentWithPureRenderMixin = {
+ shouldComponentUpdate: function (nextProps, nextState) {
+ return shallowCompare(this, nextProps, nextState);
+ }
+};
+
+module.exports = ReactComponentWithPureRenderMixin;
+},{"140":140}],38:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactCompositeComponent
+ */
+
+'use strict';
+
+var ReactComponentEnvironment = _dereq_(36);
+var ReactCurrentOwner = _dereq_(39);
+var ReactElement = _dereq_(57);
+var ReactInstanceMap = _dereq_(68);
+var ReactPerf = _dereq_(78);
+var ReactPropTypeLocations = _dereq_(81);
+var ReactPropTypeLocationNames = _dereq_(80);
+var ReactReconciler = _dereq_(84);
+var ReactUpdateQueue = _dereq_(95);
+
+var assign = _dereq_(24);
+var emptyObject = _dereq_(154);
+var invariant = _dereq_(161);
+var shouldUpdateReactComponent = _dereq_(141);
+var warning = _dereq_(173);
+
+function getDeclarationErrorAddendum(component) {
+ var owner = component._currentElement._owner || null;
+ if (owner) {
+ var name = owner.getName();
+ if (name) {
+ return ' Check the render method of `' + name + '`.';
+ }
+ }
+ return '';
+}
+
+function StatelessComponent(Component) {}
+StatelessComponent.prototype.render = function () {
+ var Component = ReactInstanceMap.get(this)._currentElement.type;
+ return Component(this.props, this.context, this.updater);
+};
+
+/**
+ * ------------------ The Life-Cycle of a Composite Component ------------------
+ *
+ * - constructor: Initialization of state. The instance is now retained.
+ * - componentWillMount
+ * - render
+ * - [children's constructors]
+ * - [children's componentWillMount and render]
+ * - [children's componentDidMount]
+ * - componentDidMount
+ *
+ * Update Phases:
+ * - componentWillReceiveProps (only called if parent updated)
+ * - shouldComponentUpdate
+ * - componentWillUpdate
+ * - render
+ * - [children's constructors or receive props phases]
+ * - componentDidUpdate
+ *
+ * - componentWillUnmount
+ * - [children's componentWillUnmount]
+ * - [children destroyed]
+ * - (destroyed): The instance is now blank, released by React and ready for GC.
+ *
+ * -----------------------------------------------------------------------------
+ */
+
+/**
+ * An incrementing ID assigned to each component when it is mounted. This is
+ * used to enforce the order in which `ReactUpdates` updates dirty components.
+ *
+ * @private
+ */
+var nextMountID = 1;
+
+/**
+ * @lends {ReactCompositeComponent.prototype}
+ */
+var ReactCompositeComponentMixin = {
+
+ /**
+ * Base constructor for all composite component.
+ *
+ * @param {ReactElement} element
+ * @final
+ * @internal
+ */
+ construct: function (element) {
+ this._currentElement = element;
+ this._rootNodeID = null;
+ this._instance = null;
+
+ // See ReactUpdateQueue
+ this._pendingElement = null;
+ this._pendingStateQueue = null;
+ this._pendingReplaceState = false;
+ this._pendingForceUpdate = false;
+
+ this._renderedComponent = null;
+
+ this._context = null;
+ this._mountOrder = 0;
+ this._topLevelWrapper = null;
+
+ // See ReactUpdates and ReactUpdateQueue.
+ this._pendingCallbacks = null;
+ },
+
+ /**
+ * Initializes the component, renders markup, and registers event listeners.
+ *
+ * @param {string} rootID DOM ID of the root node.
+ * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
+ * @return {?string} Rendered markup to be inserted into the DOM.
+ * @final
+ * @internal
+ */
+ mountComponent: function (rootID, transaction, context) {
+ this._context = context;
+ this._mountOrder = nextMountID++;
+ this._rootNodeID = rootID;
+
+ var publicProps = this._processProps(this._currentElement.props);
+ var publicContext = this._processContext(context);
+
+ var Component = this._currentElement.type;
+
+ // Initialize the public class
+ var inst;
+ var renderedElement;
+
+ // This is a way to detect if Component is a stateless arrow function
+ // component, which is not newable. It might not be 100% reliable but is
+ // something we can do until we start detecting that Component extends
+ // React.Component. We already assume that typeof Component === 'function'.
+ var canInstantiate = ('prototype' in Component);
+
+ if (canInstantiate) {
+ if ("development" !== 'production') {
+ ReactCurrentOwner.current = this;
+ try {
+ inst = new Component(publicProps, publicContext, ReactUpdateQueue);
+ } finally {
+ ReactCurrentOwner.current = null;
+ }
+ } else {
+ inst = new Component(publicProps, publicContext, ReactUpdateQueue);
+ }
+ }
+
+ if (!canInstantiate || inst === null || inst === false || ReactElement.isValidElement(inst)) {
+ renderedElement = inst;
+ inst = new StatelessComponent(Component);
+ }
+
+ if ("development" !== 'production') {
+ // This will throw later in _renderValidatedComponent, but add an early
+ // warning now to help debugging
+ if (inst.render == null) {
+ "development" !== 'production' ? warning(false, '%s(...): No `render` method found on the returned component ' + 'instance: you may have forgotten to define `render`, returned ' + 'null/false from a stateless component, or tried to render an ' + 'element whose type is a function that isn\'t a React component.', Component.displayName || Component.name || 'Component') : undefined;
+ } else {
+ // We support ES6 inheriting from React.Component, the module pattern,
+ // and stateless components, but not ES6 classes that don't extend
+ "development" !== 'production' ? warning(Component.prototype && Component.prototype.isReactComponent || !canInstantiate || !(inst instanceof Component), '%s(...): React component classes must extend React.Component.', Component.displayName || Component.name || 'Component') : undefined;
+ }
+ }
+
+ // These should be set up in the constructor, but as a convenience for
+ // simpler class abstractions, we set them up after the fact.
+ inst.props = publicProps;
+ inst.context = publicContext;
+ inst.refs = emptyObject;
+ inst.updater = ReactUpdateQueue;
+
+ this._instance = inst;
+
+ // Store a reference from the instance back to the internal representation
+ ReactInstanceMap.set(inst, this);
+
+ if ("development" !== 'production') {
+ // Since plain JS classes are defined without any special initialization
+ // logic, we can not catch common errors early. Therefore, we have to
+ // catch them here, at initialization time, instead.
+ "development" !== 'production' ? warning(!inst.getInitialState || inst.getInitialState.isReactClassApproved, 'getInitialState was defined on %s, a plain JavaScript class. ' + 'This is only supported for classes created using React.createClass. ' + 'Did you mean to define a state property instead?', this.getName() || 'a component') : undefined;
+ "development" !== 'production' ? warning(!inst.getDefaultProps || inst.getDefaultProps.isReactClassApproved, 'getDefaultProps was defined on %s, a plain JavaScript class. ' + 'This is only supported for classes created using React.createClass. ' + 'Use a static property to define defaultProps instead.', this.getName() || 'a component') : undefined;
+ "development" !== 'production' ? warning(!inst.propTypes, 'propTypes was defined as an instance property on %s. Use a static ' + 'property to define propTypes instead.', this.getName() || 'a component') : undefined;
+ "development" !== 'production' ? warning(!inst.contextTypes, 'contextTypes was defined as an instance property on %s. Use a ' + 'static property to define contextTypes instead.', this.getName() || 'a component') : undefined;
+ "development" !== 'production' ? warning(typeof inst.componentShouldUpdate !== 'function', '%s has a method called ' + 'componentShouldUpdate(). Did you mean shouldComponentUpdate()? ' + 'The name is phrased as a question because the function is ' + 'expected to return a value.', this.getName() || 'A component') : undefined;
+ "development" !== 'production' ? warning(typeof inst.componentDidUnmount !== 'function', '%s has a method called ' + 'componentDidUnmount(). But there is no such lifecycle method. ' + 'Did you mean componentWillUnmount()?', this.getName() || 'A component') : undefined;
+ "development" !== 'production' ? warning(typeof inst.componentWillRecieveProps !== 'function', '%s has a method called ' + 'componentWillRecieveProps(). Did you mean componentWillReceiveProps()?', this.getName() || 'A component') : undefined;
+ }
+
+ var initialState = inst.state;
+ if (initialState === undefined) {
+ inst.state = initialState = null;
+ }
+ !(typeof initialState === 'object' && !Array.isArray(initialState)) ? "development" !== 'production' ? invariant(false, '%s.state: must be set to an object or null', this.getName() || 'ReactCompositeComponent') : invariant(false) : undefined;
+
+ this._pendingStateQueue = null;
+ this._pendingReplaceState = false;
+ this._pendingForceUpdate = false;
+
+ if (inst.componentWillMount) {
+ inst.componentWillMount();
+ // When mounting, calls to `setState` by `componentWillMount` will set
+ // `this._pendingStateQueue` without triggering a re-render.
+ if (this._pendingStateQueue) {
+ inst.state = this._processPendingState(inst.props, inst.context);
+ }
+ }
+
+ // If not a stateless component, we now render
+ if (renderedElement === undefined) {
+ renderedElement = this._renderValidatedComponent();
+ }
+
+ this._renderedComponent = this._instantiateReactComponent(renderedElement);
+
+ var markup = ReactReconciler.mountComponent(this._renderedComponent, rootID, transaction, this._processChildContext(context));
+ if (inst.componentDidMount) {
+ transaction.getReactMountReady().enqueue(inst.componentDidMount, inst);
+ }
+
+ return markup;
+ },
+
+ /**
+ * Releases any resources allocated by `mountComponent`.
+ *
+ * @final
+ * @internal
+ */
+ unmountComponent: function () {
+ var inst = this._instance;
+
+ if (inst.componentWillUnmount) {
+ inst.componentWillUnmount();
+ }
+
+ ReactReconciler.unmountComponent(this._renderedComponent);
+ this._renderedComponent = null;
+ this._instance = null;
+
+ // Reset pending fields
+ // Even if this component is scheduled for another update in ReactUpdates,
+ // it would still be ignored because these fields are reset.
+ this._pendingStateQueue = null;
+ this._pendingReplaceState = false;
+ this._pendingForceUpdate = false;
+ this._pendingCallbacks = null;
+ this._pendingElement = null;
+
+ // These fields do not really need to be reset since this object is no
+ // longer accessible.
+ this._context = null;
+ this._rootNodeID = null;
+ this._topLevelWrapper = null;
+
+ // Delete the reference from the instance to this internal representation
+ // which allow the internals to be properly cleaned up even if the user
+ // leaks a reference to the public instance.
+ ReactInstanceMap.remove(inst);
+
+ // Some existing components rely on inst.props even after they've been
+ // destroyed (in event handlers).
+ // TODO: inst.props = null;
+ // TODO: inst.state = null;
+ // TODO: inst.context = null;
+ },
+
+ /**
+ * Filters the context object to only contain keys specified in
+ * `contextTypes`
+ *
+ * @param {object} context
+ * @return {?object}
+ * @private
+ */
+ _maskContext: function (context) {
+ var maskedContext = null;
+ var Component = this._currentElement.type;
+ var contextTypes = Component.contextTypes;
+ if (!contextTypes) {
+ return emptyObject;
+ }
+ maskedContext = {};
+ for (var contextName in contextTypes) {
+ maskedContext[contextName] = context[contextName];
+ }
+ return maskedContext;
+ },
+
+ /**
+ * Filters the context object to only contain keys specified in
+ * `contextTypes`, and asserts that they are valid.
+ *
+ * @param {object} context
+ * @return {?object}
+ * @private
+ */
+ _processContext: function (context) {
+ var maskedContext = this._maskContext(context);
+ if ("development" !== 'production') {
+ var Component = this._currentElement.type;
+ if (Component.contextTypes) {
+ this._checkPropTypes(Component.contextTypes, maskedContext, ReactPropTypeLocations.context);
+ }
+ }
+ return maskedContext;
+ },
+
+ /**
+ * @param {object} currentContext
+ * @return {object}
+ * @private
+ */
+ _processChildContext: function (currentContext) {
+ var Component = this._currentElement.type;
+ var inst = this._instance;
+ var childContext = inst.getChildContext && inst.getChildContext();
+ if (childContext) {
+ !(typeof Component.childContextTypes === 'object') ? "development" !== 'production' ? invariant(false, '%s.getChildContext(): childContextTypes must be defined in order to ' + 'use getChildContext().', this.getName() || 'ReactCompositeComponent') : invariant(false) : undefined;
+ if ("development" !== 'production') {
+ this._checkPropTypes(Component.childContextTypes, childContext, ReactPropTypeLocations.childContext);
+ }
+ for (var name in childContext) {
+ !(name in Component.childContextTypes) ? "development" !== 'production' ? invariant(false, '%s.getChildContext(): key "%s" is not defined in childContextTypes.', this.getName() || 'ReactCompositeComponent', name) : invariant(false) : undefined;
+ }
+ return assign({}, currentContext, childContext);
+ }
+ return currentContext;
+ },
+
+ /**
+ * Processes props by setting default values for unspecified props and
+ * asserting that the props are valid. Does not mutate its argument; returns
+ * a new props object with defaults merged in.
+ *
+ * @param {object} newProps
+ * @return {object}
+ * @private
+ */
+ _processProps: function (newProps) {
+ if ("development" !== 'production') {
+ var Component = this._currentElement.type;
+ if (Component.propTypes) {
+ this._checkPropTypes(Component.propTypes, newProps, ReactPropTypeLocations.prop);
+ }
+ }
+ return newProps;
+ },
+
+ /**
+ * Assert that the props are valid
+ *
+ * @param {object} propTypes Map of prop name to a ReactPropType
+ * @param {object} props
+ * @param {string} location e.g. "prop", "context", "child context"
+ * @private
+ */
+ _checkPropTypes: function (propTypes, props, location) {
+ // TODO: Stop validating prop types here and only use the element
+ // validation.
+ var componentName = this.getName();
+ for (var propName in propTypes) {
+ if (propTypes.hasOwnProperty(propName)) {
+ var error;
+ try {
+ // This is intentionally an invariant that gets caught. It's the same
+ // behavior as without this statement except with a better message.
+ !(typeof propTypes[propName] === 'function') ? "development" !== 'production' ? invariant(false, '%s: %s type `%s` is invalid; it must be a function, usually ' + 'from React.PropTypes.', componentName || 'React class', ReactPropTypeLocationNames[location], propName) : invariant(false) : undefined;
+ error = propTypes[propName](props, propName, componentName, location);
+ } catch (ex) {
+ error = ex;
+ }
+ if (error instanceof Error) {
+ // We may want to extend this logic for similar errors in
+ // top-level render calls, so I'm abstracting it away into
+ // a function to minimize refactoring in the future
+ var addendum = getDeclarationErrorAddendum(this);
+
+ if (location === ReactPropTypeLocations.prop) {
+ // Preface gives us something to blacklist in warning module
+ "development" !== 'production' ? warning(false, 'Failed Composite propType: %s%s', error.message, addendum) : undefined;
+ } else {
+ "development" !== 'production' ? warning(false, 'Failed Context Types: %s%s', error.message, addendum) : undefined;
+ }
+ }
+ }
+ }
+ },
+
+ receiveComponent: function (nextElement, transaction, nextContext) {
+ var prevElement = this._currentElement;
+ var prevContext = this._context;
+
+ this._pendingElement = null;
+
+ this.updateComponent(transaction, prevElement, nextElement, prevContext, nextContext);
+ },
+
+ /**
+ * If any of `_pendingElement`, `_pendingStateQueue`, or `_pendingForceUpdate`
+ * is set, update the component.
+ *
+ * @param {ReactReconcileTransaction} transaction
+ * @internal
+ */
+ performUpdateIfNecessary: function (transaction) {
+ if (this._pendingElement != null) {
+ ReactReconciler.receiveComponent(this, this._pendingElement || this._currentElement, transaction, this._context);
+ }
+
+ if (this._pendingStateQueue !== null || this._pendingForceUpdate) {
+ this.updateComponent(transaction, this._currentElement, this._currentElement, this._context, this._context);
+ }
+ },
+
+ /**
+ * Perform an update to a mounted component. The componentWillReceiveProps and
+ * shouldComponentUpdate methods are called, then (assuming the update isn't
+ * skipped) the remaining update lifecycle methods are called and the DOM
+ * representation is updated.
+ *
+ * By default, this implements React's rendering and reconciliation algorithm.
+ * Sophisticated clients may wish to override this.
+ *
+ * @param {ReactReconcileTransaction} transaction
+ * @param {ReactElement} prevParentElement
+ * @param {ReactElement} nextParentElement
+ * @internal
+ * @overridable
+ */
+ updateComponent: function (transaction, prevParentElement, nextParentElement, prevUnmaskedContext, nextUnmaskedContext) {
+ var inst = this._instance;
+
+ var nextContext = this._context === nextUnmaskedContext ? inst.context : this._processContext(nextUnmaskedContext);
+ var nextProps;
+
+ // Distinguish between a props update versus a simple state update
+ if (prevParentElement === nextParentElement) {
+ // Skip checking prop types again -- we don't read inst.props to avoid
+ // warning for DOM component props in this upgrade
+ nextProps = nextParentElement.props;
+ } else {
+ nextProps = this._processProps(nextParentElement.props);
+ // An update here will schedule an update but immediately set
+ // _pendingStateQueue which will ensure that any state updates gets
+ // immediately reconciled instead of waiting for the next batch.
+
+ if (inst.componentWillReceiveProps) {
+ inst.componentWillReceiveProps(nextProps, nextContext);
+ }
+ }
+
+ var nextState = this._processPendingState(nextProps, nextContext);
+
+ var shouldUpdate = this._pendingForceUpdate || !inst.shouldComponentUpdate || inst.shouldComponentUpdate(nextProps, nextState, nextContext);
+
+ if ("development" !== 'production') {
+ "development" !== 'production' ? warning(typeof shouldUpdate !== 'undefined', '%s.shouldComponentUpdate(): Returned undefined instead of a ' + 'boolean value. Make sure to return true or false.', this.getName() || 'ReactCompositeComponent') : undefined;
+ }
+
+ if (shouldUpdate) {
+ this._pendingForceUpdate = false;
+ // Will set `this.props`, `this.state` and `this.context`.
+ this._performComponentUpdate(nextParentElement, nextProps, nextState, nextContext, transaction, nextUnmaskedContext);
+ } else {
+ // If it's determined that a component should not update, we still want
+ // to set props and state but we shortcut the rest of the update.
+ this._currentElement = nextParentElement;
+ this._context = nextUnmaskedContext;
+ inst.props = nextProps;
+ inst.state = nextState;
+ inst.context = nextContext;
+ }
+ },
+
+ _processPendingState: function (props, context) {
+ var inst = this._instance;
+ var queue = this._pendingStateQueue;
+ var replace = this._pendingReplaceState;
+ this._pendingReplaceState = false;
+ this._pendingStateQueue = null;
+
+ if (!queue) {
+ return inst.state;
+ }
+
+ if (replace && queue.length === 1) {
+ return queue[0];
+ }
+
+ var nextState = assign({}, replace ? queue[0] : inst.state);
+ for (var i = replace ? 1 : 0; i < queue.length; i++) {
+ var partial = queue[i];
+ assign(nextState, typeof partial === 'function' ? partial.call(inst, nextState, props, context) : partial);
+ }
+
+ return nextState;
+ },
+
+ /**
+ * Merges new props and state, notifies delegate methods of update and
+ * performs update.
+ *
+ * @param {ReactElement} nextElement Next element
+ * @param {object} nextProps Next public object to set as properties.
+ * @param {?object} nextState Next object to set as state.
+ * @param {?object} nextContext Next public object to set as context.
+ * @param {ReactReconcileTransaction} transaction
+ * @param {?object} unmaskedContext
+ * @private
+ */
+ _performComponentUpdate: function (nextElement, nextProps, nextState, nextContext, transaction, unmaskedContext) {
+ var inst = this._instance;
+
+ var hasComponentDidUpdate = Boolean(inst.componentDidUpdate);
+ var prevProps;
+ var prevState;
+ var prevContext;
+ if (hasComponentDidUpdate) {
+ prevProps = inst.props;
+ prevState = inst.state;
+ prevContext = inst.context;
+ }
+
+ if (inst.componentWillUpdate) {
+ inst.componentWillUpdate(nextProps, nextState, nextContext);
+ }
+
+ this._currentElement = nextElement;
+ this._context = unmaskedContext;
+ inst.props = nextProps;
+ inst.state = nextState;
+ inst.context = nextContext;
+
+ this._updateRenderedComponent(transaction, unmaskedContext);
+
+ if (hasComponentDidUpdate) {
+ transaction.getReactMountReady().enqueue(inst.componentDidUpdate.bind(inst, prevProps, prevState, prevContext), inst);
+ }
+ },
+
+ /**
+ * Call the component's `render` method and update the DOM accordingly.
+ *
+ * @param {ReactReconcileTransaction} transaction
+ * @internal
+ */
+ _updateRenderedComponent: function (transaction, context) {
+ var prevComponentInstance = this._renderedComponent;
+ var prevRenderedElement = prevComponentInstance._currentElement;
+ var nextRenderedElement = this._renderValidatedComponent();
+ if (shouldUpdateReactComponent(prevRenderedElement, nextRenderedElement)) {
+ ReactReconciler.receiveComponent(prevComponentInstance, nextRenderedElement, transaction, this._processChildContext(context));
+ } else {
+ // These two IDs are actually the same! But nothing should rely on that.
+ var thisID = this._rootNodeID;
+ var prevComponentID = prevComponentInstance._rootNodeID;
+ ReactReconciler.unmountComponent(prevComponentInstance);
+
+ this._renderedComponent = this._instantiateReactComponent(nextRenderedElement);
+ var nextMarkup = ReactReconciler.mountComponent(this._renderedComponent, thisID, transaction, this._processChildContext(context));
+ this._replaceNodeWithMarkupByID(prevComponentID, nextMarkup);
+ }
+ },
+
+ /**
+ * @protected
+ */
+ _replaceNodeWithMarkupByID: function (prevComponentID, nextMarkup) {
+ ReactComponentEnvironment.replaceNodeWithMarkupByID(prevComponentID, nextMarkup);
+ },
+
+ /**
+ * @protected
+ */
+ _renderValidatedComponentWithoutOwnerOrContext: function () {
+ var inst = this._instance;
+ var renderedComponent = inst.render();
+ if ("development" !== 'production') {
+ // We allow auto-mocks to proceed as if they're returning null.
+ if (typeof renderedComponent === 'undefined' && inst.render._isMockFunction) {
+ // This is probably bad practice. Consider warning here and
+ // deprecating this convenience.
+ renderedComponent = null;
+ }
+ }
+
+ return renderedComponent;
+ },
+
+ /**
+ * @private
+ */
+ _renderValidatedComponent: function () {
+ var renderedComponent;
+ ReactCurrentOwner.current = this;
+ try {
+ renderedComponent = this._renderValidatedComponentWithoutOwnerOrContext();
+ } finally {
+ ReactCurrentOwner.current = null;
+ }
+ !(
+ // TODO: An `isValidNode` function would probably be more appropriate
+ renderedComponent === null || renderedComponent === false || ReactElement.isValidElement(renderedComponent)) ? "development" !== 'production' ? invariant(false, '%s.render(): A valid ReactComponent must be returned. You may have ' + 'returned undefined, an array or some other invalid object.', this.getName() || 'ReactCompositeComponent') : invariant(false) : undefined;
+ return renderedComponent;
+ },
+
+ /**
+ * Lazily allocates the refs object and stores `component` as `ref`.
+ *
+ * @param {string} ref Reference name.
+ * @param {component} component Component to store as `ref`.
+ * @final
+ * @private
+ */
+ attachRef: function (ref, component) {
+ var inst = this.getPublicInstance();
+ !(inst != null) ? "development" !== 'production' ? invariant(false, 'Stateless function components cannot have refs.') : invariant(false) : undefined;
+ var publicComponentInstance = component.getPublicInstance();
+ if ("development" !== 'production') {
+ var componentName = component && component.getName ? component.getName() : 'a component';
+ "development" !== 'production' ? warning(publicComponentInstance != null, 'Stateless function components cannot be given refs ' + '(See ref "%s" in %s created by %s). ' + 'Attempts to access this ref will fail.', ref, componentName, this.getName()) : undefined;
+ }
+ var refs = inst.refs === emptyObject ? inst.refs = {} : inst.refs;
+ refs[ref] = publicComponentInstance;
+ },
+
+ /**
+ * Detaches a reference name.
+ *
+ * @param {string} ref Name to dereference.
+ * @final
+ * @private
+ */
+ detachRef: function (ref) {
+ var refs = this.getPublicInstance().refs;
+ delete refs[ref];
+ },
+
+ /**
+ * Get a text description of the component that can be used to identify it
+ * in error messages.
+ * @return {string} The name or null.
+ * @internal
+ */
+ getName: function () {
+ var type = this._currentElement.type;
+ var constructor = this._instance && this._instance.constructor;
+ return type.displayName || constructor && constructor.displayName || type.name || constructor && constructor.name || null;
+ },
+
+ /**
+ * Get the publicly accessible representation of this component - i.e. what
+ * is exposed by refs and returned by render. Can be null for stateless
+ * components.
+ *
+ * @return {ReactComponent} the public component instance.
+ * @internal
+ */
+ getPublicInstance: function () {
+ var inst = this._instance;
+ if (inst instanceof StatelessComponent) {
+ return null;
+ }
+ return inst;
+ },
+
+ // Stub
+ _instantiateReactComponent: null
+
+};
+
+ReactPerf.measureMethods(ReactCompositeComponentMixin, 'ReactCompositeComponent', {
+ mountComponent: 'mountComponent',
+ updateComponent: 'updateComponent',
+ _renderValidatedComponent: '_renderValidatedComponent'
+});
+
+var ReactCompositeComponent = {
+
+ Mixin: ReactCompositeComponentMixin
+
+};
+
+module.exports = ReactCompositeComponent;
+},{"141":141,"154":154,"161":161,"173":173,"24":24,"36":36,"39":39,"57":57,"68":68,"78":78,"80":80,"81":81,"84":84,"95":95}],39:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactCurrentOwner
+ */
+
+'use strict';
+
+/**
+ * Keeps track of the current owner.
+ *
+ * The current owner is the component who should own any components that are
+ * currently being constructed.
+ */
+var ReactCurrentOwner = {
+
+ /**
+ * @internal
+ * @type {ReactComponent}
+ */
+ current: null
+
+};
+
+module.exports = ReactCurrentOwner;
+},{}],40:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDOM
+ */
+
+/* globals __REACT_DEVTOOLS_GLOBAL_HOOK__*/
+
+'use strict';
+
+var ReactCurrentOwner = _dereq_(39);
+var ReactDOMTextComponent = _dereq_(51);
+var ReactDefaultInjection = _dereq_(54);
+var ReactInstanceHandles = _dereq_(67);
+var ReactMount = _dereq_(72);
+var ReactPerf = _dereq_(78);
+var ReactReconciler = _dereq_(84);
+var ReactUpdates = _dereq_(96);
+var ReactVersion = _dereq_(97);
+
+var findDOMNode = _dereq_(122);
+var renderSubtreeIntoContainer = _dereq_(137);
+var warning = _dereq_(173);
+
+ReactDefaultInjection.inject();
+
+var render = ReactPerf.measure('React', 'render', ReactMount.render);
+
+var React = {
+ findDOMNode: findDOMNode,
+ render: render,
+ unmountComponentAtNode: ReactMount.unmountComponentAtNode,
+ version: ReactVersion,
+
+ /* eslint-disable camelcase */
+ unstable_batchedUpdates: ReactUpdates.batchedUpdates,
+ unstable_renderSubtreeIntoContainer: renderSubtreeIntoContainer
+};
+
+// Inject the runtime into a devtools global hook regardless of browser.
+// Allows for debugging when the hook is injected on the page.
+/* eslint-enable camelcase */
+if (typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ !== 'undefined' && typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.inject === 'function') {
+ __REACT_DEVTOOLS_GLOBAL_HOOK__.inject({
+ CurrentOwner: ReactCurrentOwner,
+ InstanceHandles: ReactInstanceHandles,
+ Mount: ReactMount,
+ Reconciler: ReactReconciler,
+ TextComponent: ReactDOMTextComponent
+ });
+}
+
+if ("development" !== 'production') {
+ var ExecutionEnvironment = _dereq_(147);
+ if (ExecutionEnvironment.canUseDOM && window.top === window.self) {
+
+ // First check if devtools is not installed
+ if (typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ === 'undefined') {
+ // If we're in Chrome or Firefox, provide a download link if not installed.
+ if (navigator.userAgent.indexOf('Chrome') > -1 && navigator.userAgent.indexOf('Edge') === -1 || navigator.userAgent.indexOf('Firefox') > -1) {
+ console.debug('Download the React DevTools for a better development experience: ' + 'https://fb.me/react-devtools');
+ }
+ }
+
+ // If we're in IE8, check to see if we are in compatibility mode and provide
+ // information on preventing compatibility mode
+ var ieCompatibilityMode = document.documentMode && document.documentMode < 8;
+
+ "development" !== 'production' ? warning(!ieCompatibilityMode, 'Internet Explorer is running in compatibility mode; please add the ' + 'following tag to your HTML to prevent this from happening: ' + '<meta http-equiv="X-UA-Compatible" content="IE=edge" />') : undefined;
+
+ var expectedFeatures = [
+ // shims
+ Array.isArray, Array.prototype.every, Array.prototype.forEach, Array.prototype.indexOf, Array.prototype.map, Date.now, Function.prototype.bind, Object.keys, String.prototype.split, String.prototype.trim,
+
+ // shams
+ Object.create, Object.freeze];
+
+ for (var i = 0; i < expectedFeatures.length; i++) {
+ if (!expectedFeatures[i]) {
+ console.error('One or more ES5 shim/shams expected by React are not available: ' + 'https://fb.me/react-warning-polyfills');
+ break;
+ }
+ }
+ }
+}
+
+module.exports = React;
+},{"122":122,"137":137,"147":147,"173":173,"39":39,"51":51,"54":54,"67":67,"72":72,"78":78,"84":84,"96":96,"97":97}],41:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDOMButton
+ */
+
+'use strict';
+
+var mouseListenerNames = {
+ onClick: true,
+ onDoubleClick: true,
+ onMouseDown: true,
+ onMouseMove: true,
+ onMouseUp: true,
+
+ onClickCapture: true,
+ onDoubleClickCapture: true,
+ onMouseDownCapture: true,
+ onMouseMoveCapture: true,
+ onMouseUpCapture: true
+};
+
+/**
+ * Implements a <button> native component that does not receive mouse events
+ * when `disabled` is set.
+ */
+var ReactDOMButton = {
+ getNativeProps: function (inst, props, context) {
+ if (!props.disabled) {
+ return props;
+ }
+
+ // Copy the props, except the mouse listeners
+ var nativeProps = {};
+ for (var key in props) {
+ if (props.hasOwnProperty(key) && !mouseListenerNames[key]) {
+ nativeProps[key] = props[key];
+ }
+ }
+
+ return nativeProps;
+ }
+};
+
+module.exports = ReactDOMButton;
+},{}],42:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDOMComponent
+ * @typechecks static-only
+ */
+
+/* global hasOwnProperty:true */
+
+'use strict';
+
+var AutoFocusUtils = _dereq_(2);
+var CSSPropertyOperations = _dereq_(5);
+var DOMProperty = _dereq_(10);
+var DOMPropertyOperations = _dereq_(11);
+var EventConstants = _dereq_(15);
+var ReactBrowserEventEmitter = _dereq_(28);
+var ReactComponentBrowserEnvironment = _dereq_(35);
+var ReactDOMButton = _dereq_(41);
+var ReactDOMInput = _dereq_(46);
+var ReactDOMOption = _dereq_(47);
+var ReactDOMSelect = _dereq_(48);
+var ReactDOMTextarea = _dereq_(52);
+var ReactMount = _dereq_(72);
+var ReactMultiChild = _dereq_(73);
+var ReactPerf = _dereq_(78);
+var ReactUpdateQueue = _dereq_(95);
+
+var assign = _dereq_(24);
+var canDefineProperty = _dereq_(117);
+var escapeTextContentForBrowser = _dereq_(121);
+var invariant = _dereq_(161);
+var isEventSupported = _dereq_(133);
+var keyOf = _dereq_(166);
+var setInnerHTML = _dereq_(138);
+var setTextContent = _dereq_(139);
+var shallowEqual = _dereq_(171);
+var validateDOMNesting = _dereq_(144);
+var warning = _dereq_(173);
+
+var deleteListener = ReactBrowserEventEmitter.deleteListener;
+var listenTo = ReactBrowserEventEmitter.listenTo;
+var registrationNameModules = ReactBrowserEventEmitter.registrationNameModules;
+
+// For quickly matching children type, to test if can be treated as content.
+var CONTENT_TYPES = { 'string': true, 'number': true };
+
+var CHILDREN = keyOf({ children: null });
+var STYLE = keyOf({ style: null });
+var HTML = keyOf({ __html: null });
+
+var ELEMENT_NODE_TYPE = 1;
+
+function getDeclarationErrorAddendum(internalInstance) {
+ if (internalInstance) {
+ var owner = internalInstance._currentElement._owner || null;
+ if (owner) {
+ var name = owner.getName();
+ if (name) {
+ return ' This DOM node was rendered by `' + name + '`.';
+ }
+ }
+ }
+ return '';
+}
+
+var legacyPropsDescriptor;
+if ("development" !== 'production') {
+ legacyPropsDescriptor = {
+ props: {
+ enumerable: false,
+ get: function () {
+ var component = this._reactInternalComponent;
+ "development" !== 'production' ? warning(false, 'ReactDOMComponent: Do not access .props of a DOM node; instead, ' + 'recreate the props as `render` did originally or read the DOM ' + 'properties/attributes directly from this node (e.g., ' + 'this.refs.box.className).%s', getDeclarationErrorAddendum(component)) : undefined;
+ return component._currentElement.props;
+ }
+ }
+ };
+}
+
+function legacyGetDOMNode() {
+ if ("development" !== 'production') {
+ var component = this._reactInternalComponent;
+ "development" !== 'production' ? warning(false, 'ReactDOMComponent: Do not access .getDOMNode() of a DOM node; ' + 'instead, use the node directly.%s', getDeclarationErrorAddendum(component)) : undefined;
+ }
+ return this;
+}
+
+function legacyIsMounted() {
+ var component = this._reactInternalComponent;
+ if ("development" !== 'production') {
+ "development" !== 'production' ? warning(false, 'ReactDOMComponent: Do not access .isMounted() of a DOM node.%s', getDeclarationErrorAddendum(component)) : undefined;
+ }
+ return !!component;
+}
+
+function legacySetStateEtc() {
+ if ("development" !== 'production') {
+ var component = this._reactInternalComponent;
+ "development" !== 'production' ? warning(false, 'ReactDOMComponent: Do not access .setState(), .replaceState(), or ' + '.forceUpdate() of a DOM node. This is a no-op.%s', getDeclarationErrorAddendum(component)) : undefined;
+ }
+}
+
+function legacySetProps(partialProps, callback) {
+ var component = this._reactInternalComponent;
+ if ("development" !== 'production') {
+ "development" !== 'production' ? warning(false, 'ReactDOMComponent: Do not access .setProps() of a DOM node. ' + 'Instead, call ReactDOM.render again at the top level.%s', getDeclarationErrorAddendum(component)) : undefined;
+ }
+ if (!component) {
+ return;
+ }
+ ReactUpdateQueue.enqueueSetPropsInternal(component, partialProps);
+ if (callback) {
+ ReactUpdateQueue.enqueueCallbackInternal(component, callback);
+ }
+}
+
+function legacyReplaceProps(partialProps, callback) {
+ var component = this._reactInternalComponent;
+ if ("development" !== 'production') {
+ "development" !== 'production' ? warning(false, 'ReactDOMComponent: Do not access .replaceProps() of a DOM node. ' + 'Instead, call ReactDOM.render again at the top level.%s', getDeclarationErrorAddendum(component)) : undefined;
+ }
+ if (!component) {
+ return;
+ }
+ ReactUpdateQueue.enqueueReplacePropsInternal(component, partialProps);
+ if (callback) {
+ ReactUpdateQueue.enqueueCallbackInternal(component, callback);
+ }
+}
+
+function friendlyStringify(obj) {
+ if (typeof obj === 'object') {
+ if (Array.isArray(obj)) {
+ return '[' + obj.map(friendlyStringify).join(', ') + ']';
+ } else {
+ var pairs = [];
+ for (var key in obj) {
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
+ var keyEscaped = /^[a-z$_][\w$_]*$/i.test(key) ? key : JSON.stringify(key);
+ pairs.push(keyEscaped + ': ' + friendlyStringify(obj[key]));
+ }
+ }
+ return '{' + pairs.join(', ') + '}';
+ }
+ } else if (typeof obj === 'string') {
+ return JSON.stringify(obj);
+ } else if (typeof obj === 'function') {
+ return '[function object]';
+ }
+ // Differs from JSON.stringify in that undefined becauses undefined and that
+ // inf and nan don't become null
+ return String(obj);
+}
+
+var styleMutationWarning = {};
+
+function checkAndWarnForMutatedStyle(style1, style2, component) {
+ if (style1 == null || style2 == null) {
+ return;
+ }
+ if (shallowEqual(style1, style2)) {
+ return;
+ }
+
+ var componentName = component._tag;
+ var owner = component._currentElement._owner;
+ var ownerName;
+ if (owner) {
+ ownerName = owner.getName();
+ }
+
+ var hash = ownerName + '|' + componentName;
+
+ if (styleMutationWarning.hasOwnProperty(hash)) {
+ return;
+ }
+
+ styleMutationWarning[hash] = true;
+
+ "development" !== 'production' ? warning(false, '`%s` was passed a style object that has previously been mutated. ' + 'Mutating `style` is deprecated. Consider cloning it beforehand. Check ' + 'the `render` %s. Previous style: %s. Mutated style: %s.', componentName, owner ? 'of `' + ownerName + '`' : 'using <' + componentName + '>', friendlyStringify(style1), friendlyStringify(style2)) : undefined;
+}
+
+/**
+ * @param {object} component
+ * @param {?object} props
+ */
+function assertValidProps(component, props) {
+ if (!props) {
+ return;
+ }
+ // Note the use of `==` which checks for null or undefined.
+ if ("development" !== 'production') {
+ if (voidElementTags[component._tag]) {
+ "development" !== 'production' ? warning(props.children == null && props.dangerouslySetInnerHTML == null, '%s is a void element tag and must not have `children` or ' + 'use `props.dangerouslySetInnerHTML`.%s', component._tag, component._currentElement._owner ? ' Check the render method of ' + component._currentElement._owner.getName() + '.' : '') : undefined;
+ }
+ }
+ if (props.dangerouslySetInnerHTML != null) {
+ !(props.children == null) ? "development" !== 'production' ? invariant(false, 'Can only set one of `children` or `props.dangerouslySetInnerHTML`.') : invariant(false) : undefined;
+ !(typeof props.dangerouslySetInnerHTML === 'object' && HTML in props.dangerouslySetInnerHTML) ? "development" !== 'production' ? invariant(false, '`props.dangerouslySetInnerHTML` must be in the form `{__html: ...}`. ' + 'Please visit https://fb.me/react-invariant-dangerously-set-inner-html ' + 'for more information.') : invariant(false) : undefined;
+ }
+ if ("development" !== 'production') {
+ "development" !== 'production' ? warning(props.innerHTML == null, 'Directly setting property `innerHTML` is not permitted. ' + 'For more information, lookup documentation on `dangerouslySetInnerHTML`.') : undefined;
+ "development" !== 'production' ? warning(!props.contentEditable || props.children == null, 'A component is `contentEditable` and contains `children` managed by ' + 'React. It is now your responsibility to guarantee that none of ' + 'those nodes are unexpectedly modified or duplicated. This is ' + 'probably not intentional.') : undefined;
+ }
+ !(props.style == null || typeof props.style === 'object') ? "development" !== 'production' ? invariant(false, 'The `style` prop expects a mapping from style properties to values, ' + 'not a string. For example, style={{marginRight: spacing + \'em\'}} when ' + 'using JSX.%s', getDeclarationErrorAddendum(component)) : invariant(false) : undefined;
+}
+
+function enqueuePutListener(id, registrationName, listener, transaction) {
+ if ("development" !== 'production') {
+ // IE8 has no API for event capturing and the `onScroll` event doesn't
+ // bubble.
+ "development" !== 'production' ? warning(registrationName !== 'onScroll' || isEventSupported('scroll', true), 'This browser doesn\'t support the `onScroll` event') : undefined;
+ }
+ var container = ReactMount.findReactContainerForID(id);
+ if (container) {
+ var doc = container.nodeType === ELEMENT_NODE_TYPE ? container.ownerDocument : container;
+ listenTo(registrationName, doc);
+ }
+ transaction.getReactMountReady().enqueue(putListener, {
+ id: id,
+ registrationName: registrationName,
+ listener: listener
+ });
+}
+
+function putListener() {
+ var listenerToPut = this;
+ ReactBrowserEventEmitter.putListener(listenerToPut.id, listenerToPut.registrationName, listenerToPut.listener);
+}
+
+// There are so many media events, it makes sense to just
+// maintain a list rather than create a `trapBubbledEvent` for each
+var mediaEvents = {
+ topAbort: 'abort',
+ topCanPlay: 'canplay',
+ topCanPlayThrough: 'canplaythrough',
+ topDurationChange: 'durationchange',
+ topEmptied: 'emptied',
+ topEncrypted: 'encrypted',
+ topEnded: 'ended',
+ topError: 'error',
+ topLoadedData: 'loadeddata',
+ topLoadedMetadata: 'loadedmetadata',
+ topLoadStart: 'loadstart',
+ topPause: 'pause',
+ topPlay: 'play',
+ topPlaying: 'playing',
+ topProgress: 'progress',
+ topRateChange: 'ratechange',
+ topSeeked: 'seeked',
+ topSeeking: 'seeking',
+ topStalled: 'stalled',
+ topSuspend: 'suspend',
+ topTimeUpdate: 'timeupdate',
+ topVolumeChange: 'volumechange',
+ topWaiting: 'waiting'
+};
+
+function trapBubbledEventsLocal() {
+ var inst = this;
+ // If a component renders to null or if another component fatals and causes
+ // the state of the tree to be corrupted, `node` here can be null.
+ !inst._rootNodeID ? "development" !== 'production' ? invariant(false, 'Must be mounted to trap events') : invariant(false) : undefined;
+ var node = ReactMount.getNode(inst._rootNodeID);
+ !node ? "development" !== 'production' ? invariant(false, 'trapBubbledEvent(...): Requires node to be rendered.') : invariant(false) : undefined;
+
+ switch (inst._tag) {
+ case 'iframe':
+ inst._wrapperState.listeners = [ReactBrowserEventEmitter.trapBubbledEvent(EventConstants.topLevelTypes.topLoad, 'load', node)];
+ break;
+ case 'video':
+ case 'audio':
+
+ inst._wrapperState.listeners = [];
+ // create listener for each media event
+ for (var event in mediaEvents) {
+ if (mediaEvents.hasOwnProperty(event)) {
+ inst._wrapperState.listeners.push(ReactBrowserEventEmitter.trapBubbledEvent(EventConstants.topLevelTypes[event], mediaEvents[event], node));
+ }
+ }
+
+ break;
+ case 'img':
+ inst._wrapperState.listeners = [ReactBrowserEventEmitter.trapBubbledEvent(EventConstants.topLevelTypes.topError, 'error', node), ReactBrowserEventEmitter.trapBubbledEvent(EventConstants.topLevelTypes.topLoad, 'load', node)];
+ break;
+ case 'form':
+ inst._wrapperState.listeners = [ReactBrowserEventEmitter.trapBubbledEvent(EventConstants.topLevelTypes.topReset, 'reset', node), ReactBrowserEventEmitter.trapBubbledEvent(EventConstants.topLevelTypes.topSubmit, 'submit', node)];
+ break;
+ }
+}
+
+function mountReadyInputWrapper() {
+ ReactDOMInput.mountReadyWrapper(this);
+}
+
+function postUpdateSelectWrapper() {
+ ReactDOMSelect.postUpdateWrapper(this);
+}
+
+// For HTML, certain tags should omit their close tag. We keep a whitelist for
+// those special cased tags.
+
+var omittedCloseTags = {
+ 'area': true,
+ 'base': true,
+ 'br': true,
+ 'col': true,
+ 'embed': true,
+ 'hr': true,
+ 'img': true,
+ 'input': true,
+ 'keygen': true,
+ 'link': true,
+ 'meta': true,
+ 'param': true,
+ 'source': true,
+ 'track': true,
+ 'wbr': true
+};
+
+// NOTE: menuitem's close tag should be omitted, but that causes problems.
+var newlineEatingTags = {
+ 'listing': true,
+ 'pre': true,
+ 'textarea': true
+};
+
+// For HTML, certain tags cannot have children. This has the same purpose as
+// `omittedCloseTags` except that `menuitem` should still have its closing tag.
+
+var voidElementTags = assign({
+ 'menuitem': true
+}, omittedCloseTags);
+
+// We accept any tag to be rendered but since this gets injected into arbitrary
+// HTML, we want to make sure that it's a safe tag.
+// http://www.w3.org/TR/REC-xml/#NT-Name
+
+var VALID_TAG_REGEX = /^[a-zA-Z][a-zA-Z:_\.\-\d]*$/; // Simplified subset
+var validatedTagCache = {};
+var hasOwnProperty = ({}).hasOwnProperty;
+
+function validateDangerousTag(tag) {
+ if (!hasOwnProperty.call(validatedTagCache, tag)) {
+ !VALID_TAG_REGEX.test(tag) ? "development" !== 'production' ? invariant(false, 'Invalid tag: %s', tag) : invariant(false) : undefined;
+ validatedTagCache[tag] = true;
+ }
+}
+
+function processChildContextDev(context, inst) {
+ // Pass down our tag name to child components for validation purposes
+ context = assign({}, context);
+ var info = context[validateDOMNesting.ancestorInfoContextKey];
+ context[validateDOMNesting.ancestorInfoContextKey] = validateDOMNesting.updatedAncestorInfo(info, inst._tag, inst);
+ return context;
+}
+
+function isCustomComponent(tagName, props) {
+ return tagName.indexOf('-') >= 0 || props.is != null;
+}
+
+/**
+ * Creates a new React class that is idempotent and capable of containing other
+ * React components. It accepts event listeners and DOM properties that are
+ * valid according to `DOMProperty`.
+ *
+ * - Event listeners: `onClick`, `onMouseDown`, etc.
+ * - DOM properties: `className`, `name`, `title`, etc.
+ *
+ * The `style` property functions differently from the DOM API. It accepts an
+ * object mapping of style properties to values.
+ *
+ * @constructor ReactDOMComponent
+ * @extends ReactMultiChild
+ */
+function ReactDOMComponent(tag) {
+ validateDangerousTag(tag);
+ this._tag = tag.toLowerCase();
+ this._renderedChildren = null;
+ this._previousStyle = null;
+ this._previousStyleCopy = null;
+ this._rootNodeID = null;
+ this._wrapperState = null;
+ this._topLevelWrapper = null;
+ this._nodeWithLegacyProperties = null;
+ if ("development" !== 'production') {
+ this._unprocessedContextDev = null;
+ this._processedContextDev = null;
+ }
+}
+
+ReactDOMComponent.displayName = 'ReactDOMComponent';
+
+ReactDOMComponent.Mixin = {
+
+ construct: function (element) {
+ this._currentElement = element;
+ },
+
+ /**
+ * Generates root tag markup then recurses. This method has side effects and
+ * is not idempotent.
+ *
+ * @internal
+ * @param {string} rootID The root DOM ID for this node.
+ * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
+ * @param {object} context
+ * @return {string} The computed markup.
+ */
+ mountComponent: function (rootID, transaction, context) {
+ this._rootNodeID = rootID;
+
+ var props = this._currentElement.props;
+
+ switch (this._tag) {
+ case 'iframe':
+ case 'img':
+ case 'form':
+ case 'video':
+ case 'audio':
+ this._wrapperState = {
+ listeners: null
+ };
+ transaction.getReactMountReady().enqueue(trapBubbledEventsLocal, this);
+ break;
+ case 'button':
+ props = ReactDOMButton.getNativeProps(this, props, context);
+ break;
+ case 'input':
+ ReactDOMInput.mountWrapper(this, props, context);
+ props = ReactDOMInput.getNativeProps(this, props, context);
+ break;
+ case 'option':
+ ReactDOMOption.mountWrapper(this, props, context);
+ props = ReactDOMOption.getNativeProps(this, props, context);
+ break;
+ case 'select':
+ ReactDOMSelect.mountWrapper(this, props, context);
+ props = ReactDOMSelect.getNativeProps(this, props, context);
+ context = ReactDOMSelect.processChildContext(this, props, context);
+ break;
+ case 'textarea':
+ ReactDOMTextarea.mountWrapper(this, props, context);
+ props = ReactDOMTextarea.getNativeProps(this, props, context);
+ break;
+ }
+
+ assertValidProps(this, props);
+ if ("development" !== 'production') {
+ if (context[validateDOMNesting.ancestorInfoContextKey]) {
+ validateDOMNesting(this._tag, this, context[validateDOMNesting.ancestorInfoContextKey]);
+ }
+ }
+
+ if ("development" !== 'production') {
+ this._unprocessedContextDev = context;
+ this._processedContextDev = processChildContextDev(context, this);
+ context = this._processedContextDev;
+ }
+
+ var mountImage;
+ if (transaction.useCreateElement) {
+ var ownerDocument = context[ReactMount.ownerDocumentContextKey];
+ var el = ownerDocument.createElementNS('http://www.w3.org/1999/xhtml', this._currentElement.type);
+ DOMPropertyOperations.setAttributeForID(el, this._rootNodeID);
+ // Populate node cache
+ ReactMount.getID(el);
+ this._updateDOMProperties({}, props, transaction, el);
+ this._createInitialChildren(transaction, props, context, el);
+ mountImage = el;
+ } else {
+ var tagOpen = this._createOpenTagMarkupAndPutListeners(transaction, props);
+ var tagContent = this._createContentMarkup(transaction, props, context);
+ if (!tagContent && omittedCloseTags[this._tag]) {
+ mountImage = tagOpen + '/>';
+ } else {
+ mountImage = tagOpen + '>' + tagContent + '</' + this._currentElement.type + '>';
+ }
+ }
+
+ switch (this._tag) {
+ case 'input':
+ transaction.getReactMountReady().enqueue(mountReadyInputWrapper, this);
+ // falls through
+ case 'button':
+ case 'select':
+ case 'textarea':
+ if (props.autoFocus) {
+ transaction.getReactMountReady().enqueue(AutoFocusUtils.focusDOMComponent, this);
+ }
+ break;
+ }
+
+ return mountImage;
+ },
+
+ /**
+ * Creates markup for the open tag and all attributes.
+ *
+ * This method has side effects because events get registered.
+ *
+ * Iterating over object properties is faster than iterating over arrays.
+ * @see http://jsperf.com/obj-vs-arr-iteration
+ *
+ * @private
+ * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
+ * @param {object} props
+ * @return {string} Markup of opening tag.
+ */
+ _createOpenTagMarkupAndPutListeners: function (transaction, props) {
+ var ret = '<' + this._currentElement.type;
+
+ for (var propKey in props) {
+ if (!props.hasOwnProperty(propKey)) {
+ continue;
+ }
+ var propValue = props[propKey];
+ if (propValue == null) {
+ continue;
+ }
+ if (registrationNameModules.hasOwnProperty(propKey)) {
+ if (propValue) {
+ enqueuePutListener(this._rootNodeID, propKey, propValue, transaction);
+ }
+ } else {
+ if (propKey === STYLE) {
+ if (propValue) {
+ if ("development" !== 'production') {
+ // See `_updateDOMProperties`. style block
+ this._previousStyle = propValue;
+ }
+ propValue = this._previousStyleCopy = assign({}, props.style);
+ }
+ propValue = CSSPropertyOperations.createMarkupForStyles(propValue);
+ }
+ var markup = null;
+ if (this._tag != null && isCustomComponent(this._tag, props)) {
+ if (propKey !== CHILDREN) {
+ markup = DOMPropertyOperations.createMarkupForCustomAttribute(propKey, propValue);
+ }
+ } else {
+ markup = DOMPropertyOperations.createMarkupForProperty(propKey, propValue);
+ }
+ if (markup) {
+ ret += ' ' + markup;
+ }
+ }
+ }
+
+ // For static pages, no need to put React ID and checksum. Saves lots of
+ // bytes.
+ if (transaction.renderToStaticMarkup) {
+ return ret;
+ }
+
+ var markupForID = DOMPropertyOperations.createMarkupForID(this._rootNodeID);
+ return ret + ' ' + markupForID;
+ },
+
+ /**
+ * Creates markup for the content between the tags.
+ *
+ * @private
+ * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
+ * @param {object} props
+ * @param {object} context
+ * @return {string} Content markup.
+ */
+ _createContentMarkup: function (transaction, props, context) {
+ var ret = '';
+
+ // Intentional use of != to avoid catching zero/false.
+ var innerHTML = props.dangerouslySetInnerHTML;
+ if (innerHTML != null) {
+ if (innerHTML.__html != null) {
+ ret = innerHTML.__html;
+ }
+ } else {
+ var contentToUse = CONTENT_TYPES[typeof props.children] ? props.children : null;
+ var childrenToUse = contentToUse != null ? null : props.children;
+ if (contentToUse != null) {
+ // TODO: Validate that text is allowed as a child of this node
+ ret = escapeTextContentForBrowser(contentToUse);
+ } else if (childrenToUse != null) {
+ var mountImages = this.mountChildren(childrenToUse, transaction, context);
+ ret = mountImages.join('');
+ }
+ }
+ if (newlineEatingTags[this._tag] && ret.charAt(0) === '\n') {
+ // text/html ignores the first character in these tags if it's a newline
+ // Prefer to break application/xml over text/html (for now) by adding
+ // a newline specifically to get eaten by the parser. (Alternately for
+ // textareas, replacing "^\n" with "\r\n" doesn't get eaten, and the first
+ // \r is normalized out by HTMLTextAreaElement#value.)
+ // See: <http://www.w3.org/TR/html-polyglot/#newlines-in-textarea-and-pre>
+ // See: <http://www.w3.org/TR/html5/syntax.html#element-restrictions>
+ // See: <http://www.w3.org/TR/html5/syntax.html#newlines>
+ // See: Parsing of "textarea" "listing" and "pre" elements
+ // from <http://www.w3.org/TR/html5/syntax.html#parsing-main-inbody>
+ return '\n' + ret;
+ } else {
+ return ret;
+ }
+ },
+
+ _createInitialChildren: function (transaction, props, context, el) {
+ // Intentional use of != to avoid catching zero/false.
+ var innerHTML = props.dangerouslySetInnerHTML;
+ if (innerHTML != null) {
+ if (innerHTML.__html != null) {
+ setInnerHTML(el, innerHTML.__html);
+ }
+ } else {
+ var contentToUse = CONTENT_TYPES[typeof props.children] ? props.children : null;
+ var childrenToUse = contentToUse != null ? null : props.children;
+ if (contentToUse != null) {
+ // TODO: Validate that text is allowed as a child of this node
+ setTextContent(el, contentToUse);
+ } else if (childrenToUse != null) {
+ var mountImages = this.mountChildren(childrenToUse, transaction, context);
+ for (var i = 0; i < mountImages.length; i++) {
+ el.appendChild(mountImages[i]);
+ }
+ }
+ }
+ },
+
+ /**
+ * Receives a next element and updates the component.
+ *
+ * @internal
+ * @param {ReactElement} nextElement
+ * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
+ * @param {object} context
+ */
+ receiveComponent: function (nextElement, transaction, context) {
+ var prevElement = this._currentElement;
+ this._currentElement = nextElement;
+ this.updateComponent(transaction, prevElement, nextElement, context);
+ },
+
+ /**
+ * Updates a native DOM component after it has already been allocated and
+ * attached to the DOM. Reconciles the root DOM node, then recurses.
+ *
+ * @param {ReactReconcileTransaction} transaction
+ * @param {ReactElement} prevElement
+ * @param {ReactElement} nextElement
+ * @internal
+ * @overridable
+ */
+ updateComponent: function (transaction, prevElement, nextElement, context) {
+ var lastProps = prevElement.props;
+ var nextProps = this._currentElement.props;
+
+ switch (this._tag) {
+ case 'button':
+ lastProps = ReactDOMButton.getNativeProps(this, lastProps);
+ nextProps = ReactDOMButton.getNativeProps(this, nextProps);
+ break;
+ case 'input':
+ ReactDOMInput.updateWrapper(this);
+ lastProps = ReactDOMInput.getNativeProps(this, lastProps);
+ nextProps = ReactDOMInput.getNativeProps(this, nextProps);
+ break;
+ case 'option':
+ lastProps = ReactDOMOption.getNativeProps(this, lastProps);
+ nextProps = ReactDOMOption.getNativeProps(this, nextProps);
+ break;
+ case 'select':
+ lastProps = ReactDOMSelect.getNativeProps(this, lastProps);
+ nextProps = ReactDOMSelect.getNativeProps(this, nextProps);
+ break;
+ case 'textarea':
+ ReactDOMTextarea.updateWrapper(this);
+ lastProps = ReactDOMTextarea.getNativeProps(this, lastProps);
+ nextProps = ReactDOMTextarea.getNativeProps(this, nextProps);
+ break;
+ }
+
+ if ("development" !== 'production') {
+ // If the context is reference-equal to the old one, pass down the same
+ // processed object so the update bailout in ReactReconciler behaves
+ // correctly (and identically in dev and prod). See #5005.
+ if (this._unprocessedContextDev !== context) {
+ this._unprocessedContextDev = context;
+ this._processedContextDev = processChildContextDev(context, this);
+ }
+ context = this._processedContextDev;
+ }
+
+ assertValidProps(this, nextProps);
+ this._updateDOMProperties(lastProps, nextProps, transaction, null);
+ this._updateDOMChildren(lastProps, nextProps, transaction, context);
+
+ if (!canDefineProperty && this._nodeWithLegacyProperties) {
+ this._nodeWithLegacyProperties.props = nextProps;
+ }
+
+ if (this._tag === 'select') {
+ // <select> value update needs to occur after <option> children
+ // reconciliation
+ transaction.getReactMountReady().enqueue(postUpdateSelectWrapper, this);
+ }
+ },
+
+ /**
+ * Reconciles the properties by detecting differences in property values and
+ * updating the DOM as necessary. This function is probably the single most
+ * critical path for performance optimization.
+ *
+ * TODO: Benchmark whether checking for changed values in memory actually
+ * improves performance (especially statically positioned elements).
+ * TODO: Benchmark the effects of putting this at the top since 99% of props
+ * do not change for a given reconciliation.
+ * TODO: Benchmark areas that can be improved with caching.
+ *
+ * @private
+ * @param {object} lastProps
+ * @param {object} nextProps
+ * @param {ReactReconcileTransaction} transaction
+ * @param {?DOMElement} node
+ */
+ _updateDOMProperties: function (lastProps, nextProps, transaction, node) {
+ var propKey;
+ var styleName;
+ var styleUpdates;
+ for (propKey in lastProps) {
+ if (nextProps.hasOwnProperty(propKey) || !lastProps.hasOwnProperty(propKey)) {
+ continue;
+ }
+ if (propKey === STYLE) {
+ var lastStyle = this._previousStyleCopy;
+ for (styleName in lastStyle) {
+ if (lastStyle.hasOwnProperty(styleName)) {
+ styleUpdates = styleUpdates || {};
+ styleUpdates[styleName] = '';
+ }
+ }
+ this._previousStyleCopy = null;
+ } else if (registrationNameModules.hasOwnProperty(propKey)) {
+ if (lastProps[propKey]) {
+ // Only call deleteListener if there was a listener previously or
+ // else willDeleteListener gets called when there wasn't actually a
+ // listener (e.g., onClick={null})
+ deleteListener(this._rootNodeID, propKey);
+ }
+ } else if (DOMProperty.properties[propKey] || DOMProperty.isCustomAttribute(propKey)) {
+ if (!node) {
+ node = ReactMount.getNode(this._rootNodeID);
+ }
+ DOMPropertyOperations.deleteValueForProperty(node, propKey);
+ }
+ }
+ for (propKey in nextProps) {
+ var nextProp = nextProps[propKey];
+ var lastProp = propKey === STYLE ? this._previousStyleCopy : lastProps[propKey];
+ if (!nextProps.hasOwnProperty(propKey) || nextProp === lastProp) {
+ continue;
+ }
+ if (propKey === STYLE) {
+ if (nextProp) {
+ if ("development" !== 'production') {
+ checkAndWarnForMutatedStyle(this._previousStyleCopy, this._previousStyle, this);
+ this._previousStyle = nextProp;
+ }
+ nextProp = this._previousStyleCopy = assign({}, nextProp);
+ } else {
+ this._previousStyleCopy = null;
+ }
+ if (lastProp) {
+ // Unset styles on `lastProp` but not on `nextProp`.
+ for (styleName in lastProp) {
+ if (lastProp.hasOwnProperty(styleName) && (!nextProp || !nextProp.hasOwnProperty(styleName))) {
+ styleUpdates = styleUpdates || {};
+ styleUpdates[styleName] = '';
+ }
+ }
+ // Update styles that changed since `lastProp`.
+ for (styleName in nextProp) {
+ if (nextProp.hasOwnProperty(styleName) && lastProp[styleName] !== nextProp[styleName]) {
+ styleUpdates = styleUpdates || {};
+ styleUpdates[styleName] = nextProp[styleName];
+ }
+ }
+ } else {
+ // Relies on `updateStylesByID` not mutating `styleUpdates`.
+ styleUpdates = nextProp;
+ }
+ } else if (registrationNameModules.hasOwnProperty(propKey)) {
+ if (nextProp) {
+ enqueuePutListener(this._rootNodeID, propKey, nextProp, transaction);
+ } else if (lastProp) {
+ deleteListener(this._rootNodeID, propKey);
+ }
+ } else if (isCustomComponent(this._tag, nextProps)) {
+ if (!node) {
+ node = ReactMount.getNode(this._rootNodeID);
+ }
+ if (propKey === CHILDREN) {
+ nextProp = null;
+ }
+ DOMPropertyOperations.setValueForAttribute(node, propKey, nextProp);
+ } else if (DOMProperty.properties[propKey] || DOMProperty.isCustomAttribute(propKey)) {
+ if (!node) {
+ node = ReactMount.getNode(this._rootNodeID);
+ }
+ // If we're updating to null or undefined, we should remove the property
+ // from the DOM node instead of inadvertantly setting to a string. This
+ // brings us in line with the same behavior we have on initial render.
+ if (nextProp != null) {
+ DOMPropertyOperations.setValueForProperty(node, propKey, nextProp);
+ } else {
+ DOMPropertyOperations.deleteValueForProperty(node, propKey);
+ }
+ }
+ }
+ if (styleUpdates) {
+ if (!node) {
+ node = ReactMount.getNode(this._rootNodeID);
+ }
+ CSSPropertyOperations.setValueForStyles(node, styleUpdates);
+ }
+ },
+
+ /**
+ * Reconciles the children with the various properties that affect the
+ * children content.
+ *
+ * @param {object} lastProps
+ * @param {object} nextProps
+ * @param {ReactReconcileTransaction} transaction
+ * @param {object} context
+ */
+ _updateDOMChildren: function (lastProps, nextProps, transaction, context) {
+ var lastContent = CONTENT_TYPES[typeof lastProps.children] ? lastProps.children : null;
+ var nextContent = CONTENT_TYPES[typeof nextProps.children] ? nextProps.children : null;
+
+ var lastHtml = lastProps.dangerouslySetInnerHTML && lastProps.dangerouslySetInnerHTML.__html;
+ var nextHtml = nextProps.dangerouslySetInnerHTML && nextProps.dangerouslySetInnerHTML.__html;
+
+ // Note the use of `!=` which checks for null or undefined.
+ var lastChildren = lastContent != null ? null : lastProps.children;
+ var nextChildren = nextContent != null ? null : nextProps.children;
+
+ // If we're switching from children to content/html or vice versa, remove
+ // the old content
+ var lastHasContentOrHtml = lastContent != null || lastHtml != null;
+ var nextHasContentOrHtml = nextContent != null || nextHtml != null;
+ if (lastChildren != null && nextChildren == null) {
+ this.updateChildren(null, transaction, context);
+ } else if (lastHasContentOrHtml && !nextHasContentOrHtml) {
+ this.updateTextContent('');
+ }
+
+ if (nextContent != null) {
+ if (lastContent !== nextContent) {
+ this.updateTextContent('' + nextContent);
+ }
+ } else if (nextHtml != null) {
+ if (lastHtml !== nextHtml) {
+ this.updateMarkup('' + nextHtml);
+ }
+ } else if (nextChildren != null) {
+ this.updateChildren(nextChildren, transaction, context);
+ }
+ },
+
+ /**
+ * Destroys all event registrations for this instance. Does not remove from
+ * the DOM. That must be done by the parent.
+ *
+ * @internal
+ */
+ unmountComponent: function () {
+ switch (this._tag) {
+ case 'iframe':
+ case 'img':
+ case 'form':
+ case 'video':
+ case 'audio':
+ var listeners = this._wrapperState.listeners;
+ if (listeners) {
+ for (var i = 0; i < listeners.length; i++) {
+ listeners[i].remove();
+ }
+ }
+ break;
+ case 'input':
+ ReactDOMInput.unmountWrapper(this);
+ break;
+ case 'html':
+ case 'head':
+ case 'body':
+ /**
+ * Components like <html> <head> and <body> can't be removed or added
+ * easily in a cross-browser way, however it's valuable to be able to
+ * take advantage of React's reconciliation for styling and <title>
+ * management. So we just document it and throw in dangerous cases.
+ */
+ !false ? "development" !== 'production' ? invariant(false, '<%s> tried to unmount. Because of cross-browser quirks it is ' + 'impossible to unmount some top-level components (eg <html>, ' + '<head>, and <body>) reliably and efficiently. To fix this, have a ' + 'single top-level component that never unmounts render these ' + 'elements.', this._tag) : invariant(false) : undefined;
+ break;
+ }
+
+ this.unmountChildren();
+ ReactBrowserEventEmitter.deleteAllListeners(this._rootNodeID);
+ ReactComponentBrowserEnvironment.unmountIDFromEnvironment(this._rootNodeID);
+ this._rootNodeID = null;
+ this._wrapperState = null;
+ if (this._nodeWithLegacyProperties) {
+ var node = this._nodeWithLegacyProperties;
+ node._reactInternalComponent = null;
+ this._nodeWithLegacyProperties = null;
+ }
+ },
+
+ getPublicInstance: function () {
+ if (!this._nodeWithLegacyProperties) {
+ var node = ReactMount.getNode(this._rootNodeID);
+
+ node._reactInternalComponent = this;
+ node.getDOMNode = legacyGetDOMNode;
+ node.isMounted = legacyIsMounted;
+ node.setState = legacySetStateEtc;
+ node.replaceState = legacySetStateEtc;
+ node.forceUpdate = legacySetStateEtc;
+ node.setProps = legacySetProps;
+ node.replaceProps = legacyReplaceProps;
+
+ if ("development" !== 'production') {
+ if (canDefineProperty) {
+ Object.defineProperties(node, legacyPropsDescriptor);
+ } else {
+ // updateComponent will update this property on subsequent renders
+ node.props = this._currentElement.props;
+ }
+ } else {
+ // updateComponent will update this property on subsequent renders
+ node.props = this._currentElement.props;
+ }
+
+ this._nodeWithLegacyProperties = node;
+ }
+ return this._nodeWithLegacyProperties;
+ }
+
+};
+
+ReactPerf.measureMethods(ReactDOMComponent, 'ReactDOMComponent', {
+ mountComponent: 'mountComponent',
+ updateComponent: 'updateComponent'
+});
+
+assign(ReactDOMComponent.prototype, ReactDOMComponent.Mixin, ReactMultiChild.Mixin);
+
+module.exports = ReactDOMComponent;
+},{"10":10,"11":11,"117":117,"121":121,"133":133,"138":138,"139":139,"144":144,"15":15,"161":161,"166":166,"171":171,"173":173,"2":2,"24":24,"28":28,"35":35,"41":41,"46":46,"47":47,"48":48,"5":5,"52":52,"72":72,"73":73,"78":78,"95":95}],43:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDOMFactories
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var ReactElement = _dereq_(57);
+var ReactElementValidator = _dereq_(58);
+
+var mapObject = _dereq_(167);
+
+/**
+ * Create a factory that creates HTML tag elements.
+ *
+ * @param {string} tag Tag name (e.g. `div`).
+ * @private
+ */
+function createDOMFactory(tag) {
+ if ("development" !== 'production') {
+ return ReactElementValidator.createFactory(tag);
+ }
+ return ReactElement.createFactory(tag);
+}
+
+/**
+ * Creates a mapping from supported HTML tags to `ReactDOMComponent` classes.
+ * This is also accessible via `React.DOM`.
+ *
+ * @public
+ */
+var ReactDOMFactories = mapObject({
+ a: 'a',
+ abbr: 'abbr',
+ address: 'address',
+ area: 'area',
+ article: 'article',
+ aside: 'aside',
+ audio: 'audio',
+ b: 'b',
+ base: 'base',
+ bdi: 'bdi',
+ bdo: 'bdo',
+ big: 'big',
+ blockquote: 'blockquote',
+ body: 'body',
+ br: 'br',
+ button: 'button',
+ canvas: 'canvas',
+ caption: 'caption',
+ cite: 'cite',
+ code: 'code',
+ col: 'col',
+ colgroup: 'colgroup',
+ data: 'data',
+ datalist: 'datalist',
+ dd: 'dd',
+ del: 'del',
+ details: 'details',
+ dfn: 'dfn',
+ dialog: 'dialog',
+ div: 'div',
+ dl: 'dl',
+ dt: 'dt',
+ em: 'em',
+ embed: 'embed',
+ fieldset: 'fieldset',
+ figcaption: 'figcaption',
+ figure: 'figure',
+ footer: 'footer',
+ form: 'form',
+ h1: 'h1',
+ h2: 'h2',
+ h3: 'h3',
+ h4: 'h4',
+ h5: 'h5',
+ h6: 'h6',
+ head: 'head',
+ header: 'header',
+ hgroup: 'hgroup',
+ hr: 'hr',
+ html: 'html',
+ i: 'i',
+ iframe: 'iframe',
+ img: 'img',
+ input: 'input',
+ ins: 'ins',
+ kbd: 'kbd',
+ keygen: 'keygen',
+ label: 'label',
+ legend: 'legend',
+ li: 'li',
+ link: 'link',
+ main: 'main',
+ map: 'map',
+ mark: 'mark',
+ menu: 'menu',
+ menuitem: 'menuitem',
+ meta: 'meta',
+ meter: 'meter',
+ nav: 'nav',
+ noscript: 'noscript',
+ object: 'object',
+ ol: 'ol',
+ optgroup: 'optgroup',
+ option: 'option',
+ output: 'output',
+ p: 'p',
+ param: 'param',
+ picture: 'picture',
+ pre: 'pre',
+ progress: 'progress',
+ q: 'q',
+ rp: 'rp',
+ rt: 'rt',
+ ruby: 'ruby',
+ s: 's',
+ samp: 'samp',
+ script: 'script',
+ section: 'section',
+ select: 'select',
+ small: 'small',
+ source: 'source',
+ span: 'span',
+ strong: 'strong',
+ style: 'style',
+ sub: 'sub',
+ summary: 'summary',
+ sup: 'sup',
+ table: 'table',
+ tbody: 'tbody',
+ td: 'td',
+ textarea: 'textarea',
+ tfoot: 'tfoot',
+ th: 'th',
+ thead: 'thead',
+ time: 'time',
+ title: 'title',
+ tr: 'tr',
+ track: 'track',
+ u: 'u',
+ ul: 'ul',
+ 'var': 'var',
+ video: 'video',
+ wbr: 'wbr',
+
+ // SVG
+ circle: 'circle',
+ clipPath: 'clipPath',
+ defs: 'defs',
+ ellipse: 'ellipse',
+ g: 'g',
+ image: 'image',
+ line: 'line',
+ linearGradient: 'linearGradient',
+ mask: 'mask',
+ path: 'path',
+ pattern: 'pattern',
+ polygon: 'polygon',
+ polyline: 'polyline',
+ radialGradient: 'radialGradient',
+ rect: 'rect',
+ stop: 'stop',
+ svg: 'svg',
+ text: 'text',
+ tspan: 'tspan'
+
+}, createDOMFactory);
+
+module.exports = ReactDOMFactories;
+},{"167":167,"57":57,"58":58}],44:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDOMFeatureFlags
+ */
+
+'use strict';
+
+var ReactDOMFeatureFlags = {
+ useCreateElement: false
+};
+
+module.exports = ReactDOMFeatureFlags;
+},{}],45:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDOMIDOperations
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var DOMChildrenOperations = _dereq_(9);
+var DOMPropertyOperations = _dereq_(11);
+var ReactMount = _dereq_(72);
+var ReactPerf = _dereq_(78);
+
+var invariant = _dereq_(161);
+
+/**
+ * Errors for properties that should not be updated with `updatePropertyByID()`.
+ *
+ * @type {object}
+ * @private
+ */
+var INVALID_PROPERTY_ERRORS = {
+ dangerouslySetInnerHTML: '`dangerouslySetInnerHTML` must be set using `updateInnerHTMLByID()`.',
+ style: '`style` must be set using `updateStylesByID()`.'
+};
+
+/**
+ * Operations used to process updates to DOM nodes.
+ */
+var ReactDOMIDOperations = {
+
+ /**
+ * Updates a DOM node with new property values. This should only be used to
+ * update DOM properties in `DOMProperty`.
+ *
+ * @param {string} id ID of the node to update.
+ * @param {string} name A valid property name, see `DOMProperty`.
+ * @param {*} value New value of the property.
+ * @internal
+ */
+ updatePropertyByID: function (id, name, value) {
+ var node = ReactMount.getNode(id);
+ !!INVALID_PROPERTY_ERRORS.hasOwnProperty(name) ? "development" !== 'production' ? invariant(false, 'updatePropertyByID(...): %s', INVALID_PROPERTY_ERRORS[name]) : invariant(false) : undefined;
+
+ // If we're updating to null or undefined, we should remove the property
+ // from the DOM node instead of inadvertantly setting to a string. This
+ // brings us in line with the same behavior we have on initial render.
+ if (value != null) {
+ DOMPropertyOperations.setValueForProperty(node, name, value);
+ } else {
+ DOMPropertyOperations.deleteValueForProperty(node, name);
+ }
+ },
+
+ /**
+ * Replaces a DOM node that exists in the document with markup.
+ *
+ * @param {string} id ID of child to be replaced.
+ * @param {string} markup Dangerous markup to inject in place of child.
+ * @internal
+ * @see {Danger.dangerouslyReplaceNodeWithMarkup}
+ */
+ dangerouslyReplaceNodeWithMarkupByID: function (id, markup) {
+ var node = ReactMount.getNode(id);
+ DOMChildrenOperations.dangerouslyReplaceNodeWithMarkup(node, markup);
+ },
+
+ /**
+ * Updates a component's children by processing a series of updates.
+ *
+ * @param {array<object>} updates List of update configurations.
+ * @param {array<string>} markup List of markup strings.
+ * @internal
+ */
+ dangerouslyProcessChildrenUpdates: function (updates, markup) {
+ for (var i = 0; i < updates.length; i++) {
+ updates[i].parentNode = ReactMount.getNode(updates[i].parentID);
+ }
+ DOMChildrenOperations.processUpdates(updates, markup);
+ }
+};
+
+ReactPerf.measureMethods(ReactDOMIDOperations, 'ReactDOMIDOperations', {
+ dangerouslyReplaceNodeWithMarkupByID: 'dangerouslyReplaceNodeWithMarkupByID',
+ dangerouslyProcessChildrenUpdates: 'dangerouslyProcessChildrenUpdates'
+});
+
+module.exports = ReactDOMIDOperations;
+},{"11":11,"161":161,"72":72,"78":78,"9":9}],46:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDOMInput
+ */
+
+'use strict';
+
+var ReactDOMIDOperations = _dereq_(45);
+var LinkedValueUtils = _dereq_(23);
+var ReactMount = _dereq_(72);
+var ReactUpdates = _dereq_(96);
+
+var assign = _dereq_(24);
+var invariant = _dereq_(161);
+
+var instancesByReactID = {};
+
+function forceUpdateIfMounted() {
+ if (this._rootNodeID) {
+ // DOM component is still mounted; update
+ ReactDOMInput.updateWrapper(this);
+ }
+}
+
+/**
+ * Implements an <input> native component that allows setting these optional
+ * props: `checked`, `value`, `defaultChecked`, and `defaultValue`.
+ *
+ * If `checked` or `value` are not supplied (or null/undefined), user actions
+ * that affect the checked state or value will trigger updates to the element.
+ *
+ * If they are supplied (and not null/undefined), the rendered element will not
+ * trigger updates to the element. Instead, the props must change in order for
+ * the rendered element to be updated.
+ *
+ * The rendered element will be initialized as unchecked (or `defaultChecked`)
+ * with an empty value (or `defaultValue`).
+ *
+ * @see http://www.w3.org/TR/2012/WD-html5-20121025/the-input-element.html
+ */
+var ReactDOMInput = {
+ getNativeProps: function (inst, props, context) {
+ var value = LinkedValueUtils.getValue(props);
+ var checked = LinkedValueUtils.getChecked(props);
+
+ var nativeProps = assign({}, props, {
+ defaultChecked: undefined,
+ defaultValue: undefined,
+ value: value != null ? value : inst._wrapperState.initialValue,
+ checked: checked != null ? checked : inst._wrapperState.initialChecked,
+ onChange: inst._wrapperState.onChange
+ });
+
+ return nativeProps;
+ },
+
+ mountWrapper: function (inst, props) {
+ if ("development" !== 'production') {
+ LinkedValueUtils.checkPropTypes('input', props, inst._currentElement._owner);
+ }
+
+ var defaultValue = props.defaultValue;
+ inst._wrapperState = {
+ initialChecked: props.defaultChecked || false,
+ initialValue: defaultValue != null ? defaultValue : null,
+ onChange: _handleChange.bind(inst)
+ };
+ },
+
+ mountReadyWrapper: function (inst) {
+ // Can't be in mountWrapper or else server rendering leaks.
+ instancesByReactID[inst._rootNodeID] = inst;
+ },
+
+ unmountWrapper: function (inst) {
+ delete instancesByReactID[inst._rootNodeID];
+ },
+
+ updateWrapper: function (inst) {
+ var props = inst._currentElement.props;
+
+ // TODO: Shouldn't this be getChecked(props)?
+ var checked = props.checked;
+ if (checked != null) {
+ ReactDOMIDOperations.updatePropertyByID(inst._rootNodeID, 'checked', checked || false);
+ }
+
+ var value = LinkedValueUtils.getValue(props);
+ if (value != null) {
+ // Cast `value` to a string to ensure the value is set correctly. While
+ // browsers typically do this as necessary, jsdom doesn't.
+ ReactDOMIDOperations.updatePropertyByID(inst._rootNodeID, 'value', '' + value);
+ }
+ }
+};
+
+function _handleChange(event) {
+ var props = this._currentElement.props;
+
+ var returnValue = LinkedValueUtils.executeOnChange(props, event);
+
+ // Here we use asap to wait until all updates have propagated, which
+ // is important when using controlled components within layers:
+ // https://github.com/facebook/react/issues/1698
+ ReactUpdates.asap(forceUpdateIfMounted, this);
+
+ var name = props.name;
+ if (props.type === 'radio' && name != null) {
+ var rootNode = ReactMount.getNode(this._rootNodeID);
+ var queryRoot = rootNode;
+
+ while (queryRoot.parentNode) {
+ queryRoot = queryRoot.parentNode;
+ }
+
+ // If `rootNode.form` was non-null, then we could try `form.elements`,
+ // but that sometimes behaves strangely in IE8. We could also try using
+ // `form.getElementsByName`, but that will only return direct children
+ // and won't include inputs that use the HTML5 `form=` attribute. Since
+ // the input might not even be in a form, let's just use the global
+ // `querySelectorAll` to ensure we don't miss anything.
+ var group = queryRoot.querySelectorAll('input[name=' + JSON.stringify('' + name) + '][type="radio"]');
+
+ for (var i = 0; i < group.length; i++) {
+ var otherNode = group[i];
+ if (otherNode === rootNode || otherNode.form !== rootNode.form) {
+ continue;
+ }
+ // This will throw if radio buttons rendered by different copies of React
+ // and the same name are rendered into the same form (same as #1939).
+ // That's probably okay; we don't support it just as we don't support
+ // mixing React with non-React.
+ var otherID = ReactMount.getID(otherNode);
+ !otherID ? "development" !== 'production' ? invariant(false, 'ReactDOMInput: Mixing React and non-React radio inputs with the ' + 'same `name` is not supported.') : invariant(false) : undefined;
+ var otherInstance = instancesByReactID[otherID];
+ !otherInstance ? "development" !== 'production' ? invariant(false, 'ReactDOMInput: Unknown radio button ID %s.', otherID) : invariant(false) : undefined;
+ // If this is a controlled radio button group, forcing the input that
+ // was previously checked to update will cause it to be come re-checked
+ // as appropriate.
+ ReactUpdates.asap(forceUpdateIfMounted, otherInstance);
+ }
+ }
+
+ return returnValue;
+}
+
+module.exports = ReactDOMInput;
+},{"161":161,"23":23,"24":24,"45":45,"72":72,"96":96}],47:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDOMOption
+ */
+
+'use strict';
+
+var ReactChildren = _dereq_(32);
+var ReactDOMSelect = _dereq_(48);
+
+var assign = _dereq_(24);
+var warning = _dereq_(173);
+
+var valueContextKey = ReactDOMSelect.valueContextKey;
+
+/**
+ * Implements an <option> native component that warns when `selected` is set.
+ */
+var ReactDOMOption = {
+ mountWrapper: function (inst, props, context) {
+ // TODO (yungsters): Remove support for `selected` in <option>.
+ if ("development" !== 'production') {
+ "development" !== 'production' ? warning(props.selected == null, 'Use the `defaultValue` or `value` props on <select> instead of ' + 'setting `selected` on <option>.') : undefined;
+ }
+
+ // Look up whether this option is 'selected' via context
+ var selectValue = context[valueContextKey];
+
+ // If context key is null (e.g., no specified value or after initial mount)
+ // or missing (e.g., for <datalist>), we don't change props.selected
+ var selected = null;
+ if (selectValue != null) {
+ selected = false;
+ if (Array.isArray(selectValue)) {
+ // multiple
+ for (var i = 0; i < selectValue.length; i++) {
+ if ('' + selectValue[i] === '' + props.value) {
+ selected = true;
+ break;
+ }
+ }
+ } else {
+ selected = '' + selectValue === '' + props.value;
+ }
+ }
+
+ inst._wrapperState = { selected: selected };
+ },
+
+ getNativeProps: function (inst, props, context) {
+ var nativeProps = assign({ selected: undefined, children: undefined }, props);
+
+ // Read state only from initial mount because <select> updates value
+ // manually; we need the initial state only for server rendering
+ if (inst._wrapperState.selected != null) {
+ nativeProps.selected = inst._wrapperState.selected;
+ }
+
+ var content = '';
+
+ // Flatten children and warn if they aren't strings or numbers;
+ // invalid types are ignored.
+ ReactChildren.forEach(props.children, function (child) {
+ if (child == null) {
+ return;
+ }
+ if (typeof child === 'string' || typeof child === 'number') {
+ content += child;
+ } else {
+ "development" !== 'production' ? warning(false, 'Only strings and numbers are supported as <option> children.') : undefined;
+ }
+ });
+
+ nativeProps.children = content;
+ return nativeProps;
+ }
+
+};
+
+module.exports = ReactDOMOption;
+},{"173":173,"24":24,"32":32,"48":48}],48:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDOMSelect
+ */
+
+'use strict';
+
+var LinkedValueUtils = _dereq_(23);
+var ReactMount = _dereq_(72);
+var ReactUpdates = _dereq_(96);
+
+var assign = _dereq_(24);
+var warning = _dereq_(173);
+
+var valueContextKey = '__ReactDOMSelect_value$' + Math.random().toString(36).slice(2);
+
+function updateOptionsIfPendingUpdateAndMounted() {
+ if (this._rootNodeID && this._wrapperState.pendingUpdate) {
+ this._wrapperState.pendingUpdate = false;
+
+ var props = this._currentElement.props;
+ var value = LinkedValueUtils.getValue(props);
+
+ if (value != null) {
+ updateOptions(this, Boolean(props.multiple), value);
+ }
+ }
+}
+
+function getDeclarationErrorAddendum(owner) {
+ if (owner) {
+ var name = owner.getName();
+ if (name) {
+ return ' Check the render method of `' + name + '`.';
+ }
+ }
+ return '';
+}
+
+var valuePropNames = ['value', 'defaultValue'];
+
+/**
+ * Validation function for `value` and `defaultValue`.
+ * @private
+ */
+function checkSelectPropTypes(inst, props) {
+ var owner = inst._currentElement._owner;
+ LinkedValueUtils.checkPropTypes('select', props, owner);
+
+ for (var i = 0; i < valuePropNames.length; i++) {
+ var propName = valuePropNames[i];
+ if (props[propName] == null) {
+ continue;
+ }
+ if (props.multiple) {
+ "development" !== 'production' ? warning(Array.isArray(props[propName]), 'The `%s` prop supplied to <select> must be an array if ' + '`multiple` is true.%s', propName, getDeclarationErrorAddendum(owner)) : undefined;
+ } else {
+ "development" !== 'production' ? warning(!Array.isArray(props[propName]), 'The `%s` prop supplied to <select> must be a scalar ' + 'value if `multiple` is false.%s', propName, getDeclarationErrorAddendum(owner)) : undefined;
+ }
+ }
+}
+
+/**
+ * @param {ReactDOMComponent} inst
+ * @param {boolean} multiple
+ * @param {*} propValue A stringable (with `multiple`, a list of stringables).
+ * @private
+ */
+function updateOptions(inst, multiple, propValue) {
+ var selectedValue, i;
+ var options = ReactMount.getNode(inst._rootNodeID).options;
+
+ if (multiple) {
+ selectedValue = {};
+ for (i = 0; i < propValue.length; i++) {
+ selectedValue['' + propValue[i]] = true;
+ }
+ for (i = 0; i < options.length; i++) {
+ var selected = selectedValue.hasOwnProperty(options[i].value);
+ if (options[i].selected !== selected) {
+ options[i].selected = selected;
+ }
+ }
+ } else {
+ // Do not set `select.value` as exact behavior isn't consistent across all
+ // browsers for all cases.
+ selectedValue = '' + propValue;
+ for (i = 0; i < options.length; i++) {
+ if (options[i].value === selectedValue) {
+ options[i].selected = true;
+ return;
+ }
+ }
+ if (options.length) {
+ options[0].selected = true;
+ }
+ }
+}
+
+/**
+ * Implements a <select> native component that allows optionally setting the
+ * props `value` and `defaultValue`. If `multiple` is false, the prop must be a
+ * stringable. If `multiple` is true, the prop must be an array of stringables.
+ *
+ * If `value` is not supplied (or null/undefined), user actions that change the
+ * selected option will trigger updates to the rendered options.
+ *
+ * If it is supplied (and not null/undefined), the rendered options will not
+ * update in response to user actions. Instead, the `value` prop must change in
+ * order for the rendered options to update.
+ *
+ * If `defaultValue` is provided, any options with the supplied values will be
+ * selected.
+ */
+var ReactDOMSelect = {
+ valueContextKey: valueContextKey,
+
+ getNativeProps: function (inst, props, context) {
+ return assign({}, props, {
+ onChange: inst._wrapperState.onChange,
+ value: undefined
+ });
+ },
+
+ mountWrapper: function (inst, props) {
+ if ("development" !== 'production') {
+ checkSelectPropTypes(inst, props);
+ }
+
+ var value = LinkedValueUtils.getValue(props);
+ inst._wrapperState = {
+ pendingUpdate: false,
+ initialValue: value != null ? value : props.defaultValue,
+ onChange: _handleChange.bind(inst),
+ wasMultiple: Boolean(props.multiple)
+ };
+ },
+
+ processChildContext: function (inst, props, context) {
+ // Pass down initial value so initial generated markup has correct
+ // `selected` attributes
+ var childContext = assign({}, context);
+ childContext[valueContextKey] = inst._wrapperState.initialValue;
+ return childContext;
+ },
+
+ postUpdateWrapper: function (inst) {
+ var props = inst._currentElement.props;
+
+ // After the initial mount, we control selected-ness manually so don't pass
+ // the context value down
+ inst._wrapperState.initialValue = undefined;
+
+ var wasMultiple = inst._wrapperState.wasMultiple;
+ inst._wrapperState.wasMultiple = Boolean(props.multiple);
+
+ var value = LinkedValueUtils.getValue(props);
+ if (value != null) {
+ inst._wrapperState.pendingUpdate = false;
+ updateOptions(inst, Boolean(props.multiple), value);
+ } else if (wasMultiple !== Boolean(props.multiple)) {
+ // For simplicity, reapply `defaultValue` if `multiple` is toggled.
+ if (props.defaultValue != null) {
+ updateOptions(inst, Boolean(props.multiple), props.defaultValue);
+ } else {
+ // Revert the select back to its default unselected state.
+ updateOptions(inst, Boolean(props.multiple), props.multiple ? [] : '');
+ }
+ }
+ }
+};
+
+function _handleChange(event) {
+ var props = this._currentElement.props;
+ var returnValue = LinkedValueUtils.executeOnChange(props, event);
+
+ this._wrapperState.pendingUpdate = true;
+ ReactUpdates.asap(updateOptionsIfPendingUpdateAndMounted, this);
+ return returnValue;
+}
+
+module.exports = ReactDOMSelect;
+},{"173":173,"23":23,"24":24,"72":72,"96":96}],49:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDOMSelection
+ */
+
+'use strict';
+
+var ExecutionEnvironment = _dereq_(147);
+
+var getNodeForCharacterOffset = _dereq_(130);
+var getTextContentAccessor = _dereq_(131);
+
+/**
+ * While `isCollapsed` is available on the Selection object and `collapsed`
+ * is available on the Range object, IE11 sometimes gets them wrong.
+ * If the anchor/focus nodes and offsets are the same, the range is collapsed.
+ */
+function isCollapsed(anchorNode, anchorOffset, focusNode, focusOffset) {
+ return anchorNode === focusNode && anchorOffset === focusOffset;
+}
+
+/**
+ * Get the appropriate anchor and focus node/offset pairs for IE.
+ *
+ * The catch here is that IE's selection API doesn't provide information
+ * about whether the selection is forward or backward, so we have to
+ * behave as though it's always forward.
+ *
+ * IE text differs from modern selection in that it behaves as though
+ * block elements end with a new line. This means character offsets will
+ * differ between the two APIs.
+ *
+ * @param {DOMElement} node
+ * @return {object}
+ */
+function getIEOffsets(node) {
+ var selection = document.selection;
+ var selectedRange = selection.createRange();
+ var selectedLength = selectedRange.text.length;
+
+ // Duplicate selection so we can move range without breaking user selection.
+ var fromStart = selectedRange.duplicate();
+ fromStart.moveToElementText(node);
+ fromStart.setEndPoint('EndToStart', selectedRange);
+
+ var startOffset = fromStart.text.length;
+ var endOffset = startOffset + selectedLength;
+
+ return {
+ start: startOffset,
+ end: endOffset
+ };
+}
+
+/**
+ * @param {DOMElement} node
+ * @return {?object}
+ */
+function getModernOffsets(node) {
+ var selection = window.getSelection && window.getSelection();
+
+ if (!selection || selection.rangeCount === 0) {
+ return null;
+ }
+
+ var anchorNode = selection.anchorNode;
+ var anchorOffset = selection.anchorOffset;
+ var focusNode = selection.focusNode;
+ var focusOffset = selection.focusOffset;
+
+ var currentRange = selection.getRangeAt(0);
+
+ // In Firefox, range.startContainer and range.endContainer can be "anonymous
+ // divs", e.g. the up/down buttons on an <input type="number">. Anonymous
+ // divs do not seem to expose properties, triggering a "Permission denied
+ // error" if any of its properties are accessed. The only seemingly possible
+ // way to avoid erroring is to access a property that typically works for
+ // non-anonymous divs and catch any error that may otherwise arise. See
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=208427
+ try {
+ /* eslint-disable no-unused-expressions */
+ currentRange.startContainer.nodeType;
+ currentRange.endContainer.nodeType;
+ /* eslint-enable no-unused-expressions */
+ } catch (e) {
+ return null;
+ }
+
+ // If the node and offset values are the same, the selection is collapsed.
+ // `Selection.isCollapsed` is available natively, but IE sometimes gets
+ // this value wrong.
+ var isSelectionCollapsed = isCollapsed(selection.anchorNode, selection.anchorOffset, selection.focusNode, selection.focusOffset);
+
+ var rangeLength = isSelectionCollapsed ? 0 : currentRange.toString().length;
+
+ var tempRange = currentRange.cloneRange();
+ tempRange.selectNodeContents(node);
+ tempRange.setEnd(currentRange.startContainer, currentRange.startOffset);
+
+ var isTempRangeCollapsed = isCollapsed(tempRange.startContainer, tempRange.startOffset, tempRange.endContainer, tempRange.endOffset);
+
+ var start = isTempRangeCollapsed ? 0 : tempRange.toString().length;
+ var end = start + rangeLength;
+
+ // Detect whether the selection is backward.
+ var detectionRange = document.createRange();
+ detectionRange.setStart(anchorNode, anchorOffset);
+ detectionRange.setEnd(focusNode, focusOffset);
+ var isBackward = detectionRange.collapsed;
+
+ return {
+ start: isBackward ? end : start,
+ end: isBackward ? start : end
+ };
+}
+
+/**
+ * @param {DOMElement|DOMTextNode} node
+ * @param {object} offsets
+ */
+function setIEOffsets(node, offsets) {
+ var range = document.selection.createRange().duplicate();
+ var start, end;
+
+ if (typeof offsets.end === 'undefined') {
+ start = offsets.start;
+ end = start;
+ } else if (offsets.start > offsets.end) {
+ start = offsets.end;
+ end = offsets.start;
+ } else {
+ start = offsets.start;
+ end = offsets.end;
+ }
+
+ range.moveToElementText(node);
+ range.moveStart('character', start);
+ range.setEndPoint('EndToStart', range);
+ range.moveEnd('character', end - start);
+ range.select();
+}
+
+/**
+ * In modern non-IE browsers, we can support both forward and backward
+ * selections.
+ *
+ * Note: IE10+ supports the Selection object, but it does not support
+ * the `extend` method, which means that even in modern IE, it's not possible
+ * to programatically create a backward selection. Thus, for all IE
+ * versions, we use the old IE API to create our selections.
+ *
+ * @param {DOMElement|DOMTextNode} node
+ * @param {object} offsets
+ */
+function setModernOffsets(node, offsets) {
+ if (!window.getSelection) {
+ return;
+ }
+
+ var selection = window.getSelection();
+ var length = node[getTextContentAccessor()].length;
+ var start = Math.min(offsets.start, length);
+ var end = typeof offsets.end === 'undefined' ? start : Math.min(offsets.end, length);
+
+ // IE 11 uses modern selection, but doesn't support the extend method.
+ // Flip backward selections, so we can set with a single range.
+ if (!selection.extend && start > end) {
+ var temp = end;
+ end = start;
+ start = temp;
+ }
+
+ var startMarker = getNodeForCharacterOffset(node, start);
+ var endMarker = getNodeForCharacterOffset(node, end);
+
+ if (startMarker && endMarker) {
+ var range = document.createRange();
+ range.setStart(startMarker.node, startMarker.offset);
+ selection.removeAllRanges();
+
+ if (start > end) {
+ selection.addRange(range);
+ selection.extend(endMarker.node, endMarker.offset);
+ } else {
+ range.setEnd(endMarker.node, endMarker.offset);
+ selection.addRange(range);
+ }
+ }
+}
+
+var useIEOffsets = ExecutionEnvironment.canUseDOM && 'selection' in document && !('getSelection' in window);
+
+var ReactDOMSelection = {
+ /**
+ * @param {DOMElement} node
+ */
+ getOffsets: useIEOffsets ? getIEOffsets : getModernOffsets,
+
+ /**
+ * @param {DOMElement|DOMTextNode} node
+ * @param {object} offsets
+ */
+ setOffsets: useIEOffsets ? setIEOffsets : setModernOffsets
+};
+
+module.exports = ReactDOMSelection;
+},{"130":130,"131":131,"147":147}],50:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDOMServer
+ */
+
+'use strict';
+
+var ReactDefaultInjection = _dereq_(54);
+var ReactServerRendering = _dereq_(88);
+var ReactVersion = _dereq_(97);
+
+ReactDefaultInjection.inject();
+
+var ReactDOMServer = {
+ renderToString: ReactServerRendering.renderToString,
+ renderToStaticMarkup: ReactServerRendering.renderToStaticMarkup,
+ version: ReactVersion
+};
+
+module.exports = ReactDOMServer;
+},{"54":54,"88":88,"97":97}],51:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDOMTextComponent
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var DOMChildrenOperations = _dereq_(9);
+var DOMPropertyOperations = _dereq_(11);
+var ReactComponentBrowserEnvironment = _dereq_(35);
+var ReactMount = _dereq_(72);
+
+var assign = _dereq_(24);
+var escapeTextContentForBrowser = _dereq_(121);
+var setTextContent = _dereq_(139);
+var validateDOMNesting = _dereq_(144);
+
+/**
+ * Text nodes violate a couple assumptions that React makes about components:
+ *
+ * - When mounting text into the DOM, adjacent text nodes are merged.
+ * - Text nodes cannot be assigned a React root ID.
+ *
+ * This component is used to wrap strings in elements so that they can undergo
+ * the same reconciliation that is applied to elements.
+ *
+ * TODO: Investigate representing React components in the DOM with text nodes.
+ *
+ * @class ReactDOMTextComponent
+ * @extends ReactComponent
+ * @internal
+ */
+var ReactDOMTextComponent = function (props) {
+ // This constructor and its argument is currently used by mocks.
+};
+
+assign(ReactDOMTextComponent.prototype, {
+
+ /**
+ * @param {ReactText} text
+ * @internal
+ */
+ construct: function (text) {
+ // TODO: This is really a ReactText (ReactNode), not a ReactElement
+ this._currentElement = text;
+ this._stringText = '' + text;
+
+ // Properties
+ this._rootNodeID = null;
+ this._mountIndex = 0;
+ },
+
+ /**
+ * Creates the markup for this text node. This node is not intended to have
+ * any features besides containing text content.
+ *
+ * @param {string} rootID DOM ID of the root node.
+ * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
+ * @return {string} Markup for this text node.
+ * @internal
+ */
+ mountComponent: function (rootID, transaction, context) {
+ if ("development" !== 'production') {
+ if (context[validateDOMNesting.ancestorInfoContextKey]) {
+ validateDOMNesting('span', null, context[validateDOMNesting.ancestorInfoContextKey]);
+ }
+ }
+
+ this._rootNodeID = rootID;
+ if (transaction.useCreateElement) {
+ var ownerDocument = context[ReactMount.ownerDocumentContextKey];
+ var el = ownerDocument.createElementNS('http://www.w3.org/1999/xhtml', 'span');
+ DOMPropertyOperations.setAttributeForID(el, rootID);
+ // Populate node cache
+ ReactMount.getID(el);
+ setTextContent(el, this._stringText);
+ return el;
+ } else {
+ var escapedText = escapeTextContentForBrowser(this._stringText);
+
+ if (transaction.renderToStaticMarkup) {
+ // Normally we'd wrap this in a `span` for the reasons stated above, but
+ // since this is a situation where React won't take over (static pages),
+ // we can simply return the text as it is.
+ return escapedText;
+ }
+
+ return '<span ' + DOMPropertyOperations.createMarkupForID(rootID) + '>' + escapedText + '</span>';
+ }
+ },
+
+ /**
+ * Updates this component by updating the text content.
+ *
+ * @param {ReactText} nextText The next text content
+ * @param {ReactReconcileTransaction} transaction
+ * @internal
+ */
+ receiveComponent: function (nextText, transaction) {
+ if (nextText !== this._currentElement) {
+ this._currentElement = nextText;
+ var nextStringText = '' + nextText;
+ if (nextStringText !== this._stringText) {
+ // TODO: Save this as pending props and use performUpdateIfNecessary
+ // and/or updateComponent to do the actual update for consistency with
+ // other component types?
+ this._stringText = nextStringText;
+ var node = ReactMount.getNode(this._rootNodeID);
+ DOMChildrenOperations.updateTextContent(node, nextStringText);
+ }
+ }
+ },
+
+ unmountComponent: function () {
+ ReactComponentBrowserEnvironment.unmountIDFromEnvironment(this._rootNodeID);
+ }
+
+});
+
+module.exports = ReactDOMTextComponent;
+},{"11":11,"121":121,"139":139,"144":144,"24":24,"35":35,"72":72,"9":9}],52:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDOMTextarea
+ */
+
+'use strict';
+
+var LinkedValueUtils = _dereq_(23);
+var ReactDOMIDOperations = _dereq_(45);
+var ReactUpdates = _dereq_(96);
+
+var assign = _dereq_(24);
+var invariant = _dereq_(161);
+var warning = _dereq_(173);
+
+function forceUpdateIfMounted() {
+ if (this._rootNodeID) {
+ // DOM component is still mounted; update
+ ReactDOMTextarea.updateWrapper(this);
+ }
+}
+
+/**
+ * Implements a <textarea> native component that allows setting `value`, and
+ * `defaultValue`. This differs from the traditional DOM API because value is
+ * usually set as PCDATA children.
+ *
+ * If `value` is not supplied (or null/undefined), user actions that affect the
+ * value will trigger updates to the element.
+ *
+ * If `value` is supplied (and not null/undefined), the rendered element will
+ * not trigger updates to the element. Instead, the `value` prop must change in
+ * order for the rendered element to be updated.
+ *
+ * The rendered element will be initialized with an empty value, the prop
+ * `defaultValue` if specified, or the children content (deprecated).
+ */
+var ReactDOMTextarea = {
+ getNativeProps: function (inst, props, context) {
+ !(props.dangerouslySetInnerHTML == null) ? "development" !== 'production' ? invariant(false, '`dangerouslySetInnerHTML` does not make sense on <textarea>.') : invariant(false) : undefined;
+
+ // Always set children to the same thing. In IE9, the selection range will
+ // get reset if `textContent` is mutated.
+ var nativeProps = assign({}, props, {
+ defaultValue: undefined,
+ value: undefined,
+ children: inst._wrapperState.initialValue,
+ onChange: inst._wrapperState.onChange
+ });
+
+ return nativeProps;
+ },
+
+ mountWrapper: function (inst, props) {
+ if ("development" !== 'production') {
+ LinkedValueUtils.checkPropTypes('textarea', props, inst._currentElement._owner);
+ }
+
+ var defaultValue = props.defaultValue;
+ // TODO (yungsters): Remove support for children content in <textarea>.
+ var children = props.children;
+ if (children != null) {
+ if ("development" !== 'production') {
+ "development" !== 'production' ? warning(false, 'Use the `defaultValue` or `value` props instead of setting ' + 'children on <textarea>.') : undefined;
+ }
+ !(defaultValue == null) ? "development" !== 'production' ? invariant(false, 'If you supply `defaultValue` on a <textarea>, do not pass children.') : invariant(false) : undefined;
+ if (Array.isArray(children)) {
+ !(children.length <= 1) ? "development" !== 'production' ? invariant(false, '<textarea> can only have at most one child.') : invariant(false) : undefined;
+ children = children[0];
+ }
+
+ defaultValue = '' + children;
+ }
+ if (defaultValue == null) {
+ defaultValue = '';
+ }
+ var value = LinkedValueUtils.getValue(props);
+
+ inst._wrapperState = {
+ // We save the initial value so that `ReactDOMComponent` doesn't update
+ // `textContent` (unnecessary since we update value).
+ // The initial value can be a boolean or object so that's why it's
+ // forced to be a string.
+ initialValue: '' + (value != null ? value : defaultValue),
+ onChange: _handleChange.bind(inst)
+ };
+ },
+
+ updateWrapper: function (inst) {
+ var props = inst._currentElement.props;
+ var value = LinkedValueUtils.getValue(props);
+ if (value != null) {
+ // Cast `value` to a string to ensure the value is set correctly. While
+ // browsers typically do this as necessary, jsdom doesn't.
+ ReactDOMIDOperations.updatePropertyByID(inst._rootNodeID, 'value', '' + value);
+ }
+ }
+};
+
+function _handleChange(event) {
+ var props = this._currentElement.props;
+ var returnValue = LinkedValueUtils.executeOnChange(props, event);
+ ReactUpdates.asap(forceUpdateIfMounted, this);
+ return returnValue;
+}
+
+module.exports = ReactDOMTextarea;
+},{"161":161,"173":173,"23":23,"24":24,"45":45,"96":96}],53:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDefaultBatchingStrategy
+ */
+
+'use strict';
+
+var ReactUpdates = _dereq_(96);
+var Transaction = _dereq_(113);
+
+var assign = _dereq_(24);
+var emptyFunction = _dereq_(153);
+
+var RESET_BATCHED_UPDATES = {
+ initialize: emptyFunction,
+ close: function () {
+ ReactDefaultBatchingStrategy.isBatchingUpdates = false;
+ }
+};
+
+var FLUSH_BATCHED_UPDATES = {
+ initialize: emptyFunction,
+ close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)
+};
+
+var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];
+
+function ReactDefaultBatchingStrategyTransaction() {
+ this.reinitializeTransaction();
+}
+
+assign(ReactDefaultBatchingStrategyTransaction.prototype, Transaction.Mixin, {
+ getTransactionWrappers: function () {
+ return TRANSACTION_WRAPPERS;
+ }
+});
+
+var transaction = new ReactDefaultBatchingStrategyTransaction();
+
+var ReactDefaultBatchingStrategy = {
+ isBatchingUpdates: false,
+
+ /**
+ * Call the provided function in a context within which calls to `setState`
+ * and friends are batched such that components aren't updated unnecessarily.
+ */
+ batchedUpdates: function (callback, a, b, c, d, e) {
+ var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
+
+ ReactDefaultBatchingStrategy.isBatchingUpdates = true;
+
+ // The code is written this way to avoid extra allocations
+ if (alreadyBatchingUpdates) {
+ callback(a, b, c, d, e);
+ } else {
+ transaction.perform(callback, null, a, b, c, d, e);
+ }
+ }
+};
+
+module.exports = ReactDefaultBatchingStrategy;
+},{"113":113,"153":153,"24":24,"96":96}],54:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDefaultInjection
+ */
+
+'use strict';
+
+var BeforeInputEventPlugin = _dereq_(3);
+var ChangeEventPlugin = _dereq_(7);
+var ClientReactRootIndex = _dereq_(8);
+var DefaultEventPluginOrder = _dereq_(13);
+var EnterLeaveEventPlugin = _dereq_(14);
+var ExecutionEnvironment = _dereq_(147);
+var HTMLDOMPropertyConfig = _dereq_(21);
+var ReactBrowserComponentMixin = _dereq_(27);
+var ReactComponentBrowserEnvironment = _dereq_(35);
+var ReactDefaultBatchingStrategy = _dereq_(53);
+var ReactDOMComponent = _dereq_(42);
+var ReactDOMTextComponent = _dereq_(51);
+var ReactEventListener = _dereq_(63);
+var ReactInjection = _dereq_(65);
+var ReactInstanceHandles = _dereq_(67);
+var ReactMount = _dereq_(72);
+var ReactReconcileTransaction = _dereq_(83);
+var SelectEventPlugin = _dereq_(99);
+var ServerReactRootIndex = _dereq_(100);
+var SimpleEventPlugin = _dereq_(101);
+var SVGDOMPropertyConfig = _dereq_(98);
+
+var alreadyInjected = false;
+
+function inject() {
+ if (alreadyInjected) {
+ // TODO: This is currently true because these injections are shared between
+ // the client and the server package. They should be built independently
+ // and not share any injection state. Then this problem will be solved.
+ return;
+ }
+ alreadyInjected = true;
+
+ ReactInjection.EventEmitter.injectReactEventListener(ReactEventListener);
+
+ /**
+ * Inject modules for resolving DOM hierarchy and plugin ordering.
+ */
+ ReactInjection.EventPluginHub.injectEventPluginOrder(DefaultEventPluginOrder);
+ ReactInjection.EventPluginHub.injectInstanceHandle(ReactInstanceHandles);
+ ReactInjection.EventPluginHub.injectMount(ReactMount);
+
+ /**
+ * Some important event plugins included by default (without having to require
+ * them).
+ */
+ ReactInjection.EventPluginHub.injectEventPluginsByName({
+ SimpleEventPlugin: SimpleEventPlugin,
+ EnterLeaveEventPlugin: EnterLeaveEventPlugin,
+ ChangeEventPlugin: ChangeEventPlugin,
+ SelectEventPlugin: SelectEventPlugin,
+ BeforeInputEventPlugin: BeforeInputEventPlugin
+ });
+
+ ReactInjection.NativeComponent.injectGenericComponentClass(ReactDOMComponent);
+
+ ReactInjection.NativeComponent.injectTextComponentClass(ReactDOMTextComponent);
+
+ ReactInjection.Class.injectMixin(ReactBrowserComponentMixin);
+
+ ReactInjection.DOMProperty.injectDOMPropertyConfig(HTMLDOMPropertyConfig);
+ ReactInjection.DOMProperty.injectDOMPropertyConfig(SVGDOMPropertyConfig);
+
+ ReactInjection.EmptyComponent.injectEmptyComponent('noscript');
+
+ ReactInjection.Updates.injectReconcileTransaction(ReactReconcileTransaction);
+ ReactInjection.Updates.injectBatchingStrategy(ReactDefaultBatchingStrategy);
+
+ ReactInjection.RootIndex.injectCreateReactRootIndex(ExecutionEnvironment.canUseDOM ? ClientReactRootIndex.createReactRootIndex : ServerReactRootIndex.createReactRootIndex);
+
+ ReactInjection.Component.injectEnvironment(ReactComponentBrowserEnvironment);
+
+ if ("development" !== 'production') {
+ var url = ExecutionEnvironment.canUseDOM && window.location.href || '';
+ if (/[?&]react_perf\b/.test(url)) {
+ var ReactDefaultPerf = _dereq_(55);
+ ReactDefaultPerf.start();
+ }
+ }
+}
+
+module.exports = {
+ inject: inject
+};
+},{"100":100,"101":101,"13":13,"14":14,"147":147,"21":21,"27":27,"3":3,"35":35,"42":42,"51":51,"53":53,"55":55,"63":63,"65":65,"67":67,"7":7,"72":72,"8":8,"83":83,"98":98,"99":99}],55:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDefaultPerf
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var DOMProperty = _dereq_(10);
+var ReactDefaultPerfAnalysis = _dereq_(56);
+var ReactMount = _dereq_(72);
+var ReactPerf = _dereq_(78);
+
+var performanceNow = _dereq_(170);
+
+function roundFloat(val) {
+ return Math.floor(val * 100) / 100;
+}
+
+function addValue(obj, key, val) {
+ obj[key] = (obj[key] || 0) + val;
+}
+
+var ReactDefaultPerf = {
+ _allMeasurements: [], // last item in the list is the current one
+ _mountStack: [0],
+ _injected: false,
+
+ start: function () {
+ if (!ReactDefaultPerf._injected) {
+ ReactPerf.injection.injectMeasure(ReactDefaultPerf.measure);
+ }
+
+ ReactDefaultPerf._allMeasurements.length = 0;
+ ReactPerf.enableMeasure = true;
+ },
+
+ stop: function () {
+ ReactPerf.enableMeasure = false;
+ },
+
+ getLastMeasurements: function () {
+ return ReactDefaultPerf._allMeasurements;
+ },
+
+ printExclusive: function (measurements) {
+ measurements = measurements || ReactDefaultPerf._allMeasurements;
+ var summary = ReactDefaultPerfAnalysis.getExclusiveSummary(measurements);
+ console.table(summary.map(function (item) {
+ return {
+ 'Component class name': item.componentName,
+ 'Total inclusive time (ms)': roundFloat(item.inclusive),
+ 'Exclusive mount time (ms)': roundFloat(item.exclusive),
+ 'Exclusive render time (ms)': roundFloat(item.render),
+ 'Mount time per instance (ms)': roundFloat(item.exclusive / item.count),
+ 'Render time per instance (ms)': roundFloat(item.render / item.count),
+ 'Instances': item.count
+ };
+ }));
+ // TODO: ReactDefaultPerfAnalysis.getTotalTime() does not return the correct
+ // number.
+ },
+
+ printInclusive: function (measurements) {
+ measurements = measurements || ReactDefaultPerf._allMeasurements;
+ var summary = ReactDefaultPerfAnalysis.getInclusiveSummary(measurements);
+ console.table(summary.map(function (item) {
+ return {
+ 'Owner > component': item.componentName,
+ 'Inclusive time (ms)': roundFloat(item.time),
+ 'Instances': item.count
+ };
+ }));
+ console.log('Total time:', ReactDefaultPerfAnalysis.getTotalTime(measurements).toFixed(2) + ' ms');
+ },
+
+ getMeasurementsSummaryMap: function (measurements) {
+ var summary = ReactDefaultPerfAnalysis.getInclusiveSummary(measurements, true);
+ return summary.map(function (item) {
+ return {
+ 'Owner > component': item.componentName,
+ 'Wasted time (ms)': item.time,
+ 'Instances': item.count
+ };
+ });
+ },
+
+ printWasted: function (measurements) {
+ measurements = measurements || ReactDefaultPerf._allMeasurements;
+ console.table(ReactDefaultPerf.getMeasurementsSummaryMap(measurements));
+ console.log('Total time:', ReactDefaultPerfAnalysis.getTotalTime(measurements).toFixed(2) + ' ms');
+ },
+
+ printDOM: function (measurements) {
+ measurements = measurements || ReactDefaultPerf._allMeasurements;
+ var summary = ReactDefaultPerfAnalysis.getDOMSummary(measurements);
+ console.table(summary.map(function (item) {
+ var result = {};
+ result[DOMProperty.ID_ATTRIBUTE_NAME] = item.id;
+ result.type = item.type;
+ result.args = JSON.stringify(item.args);
+ return result;
+ }));
+ console.log('Total time:', ReactDefaultPerfAnalysis.getTotalTime(measurements).toFixed(2) + ' ms');
+ },
+
+ _recordWrite: function (id, fnName, totalTime, args) {
+ // TODO: totalTime isn't that useful since it doesn't count paints/reflows
+ var writes = ReactDefaultPerf._allMeasurements[ReactDefaultPerf._allMeasurements.length - 1].writes;
+ writes[id] = writes[id] || [];
+ writes[id].push({
+ type: fnName,
+ time: totalTime,
+ args: args
+ });
+ },
+
+ measure: function (moduleName, fnName, func) {
+ return function () {
+ for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
+ args[_key] = arguments[_key];
+ }
+
+ var totalTime;
+ var rv;
+ var start;
+
+ if (fnName === '_renderNewRootComponent' || fnName === 'flushBatchedUpdates') {
+ // A "measurement" is a set of metrics recorded for each flush. We want
+ // to group the metrics for a given flush together so we can look at the
+ // components that rendered and the DOM operations that actually
+ // happened to determine the amount of "wasted work" performed.
+ ReactDefaultPerf._allMeasurements.push({
+ exclusive: {},
+ inclusive: {},
+ render: {},
+ counts: {},
+ writes: {},
+ displayNames: {},
+ totalTime: 0,
+ created: {}
+ });
+ start = performanceNow();
+ rv = func.apply(this, args);
+ ReactDefaultPerf._allMeasurements[ReactDefaultPerf._allMeasurements.length - 1].totalTime = performanceNow() - start;
+ return rv;
+ } else if (fnName === '_mountImageIntoNode' || moduleName === 'ReactBrowserEventEmitter' || moduleName === 'ReactDOMIDOperations' || moduleName === 'CSSPropertyOperations' || moduleName === 'DOMChildrenOperations' || moduleName === 'DOMPropertyOperations') {
+ start = performanceNow();
+ rv = func.apply(this, args);
+ totalTime = performanceNow() - start;
+
+ if (fnName === '_mountImageIntoNode') {
+ var mountID = ReactMount.getID(args[1]);
+ ReactDefaultPerf._recordWrite(mountID, fnName, totalTime, args[0]);
+ } else if (fnName === 'dangerouslyProcessChildrenUpdates') {
+ // special format
+ args[0].forEach(function (update) {
+ var writeArgs = {};
+ if (update.fromIndex !== null) {
+ writeArgs.fromIndex = update.fromIndex;
+ }
+ if (update.toIndex !== null) {
+ writeArgs.toIndex = update.toIndex;
+ }
+ if (update.textContent !== null) {
+ writeArgs.textContent = update.textContent;
+ }
+ if (update.markupIndex !== null) {
+ writeArgs.markup = args[1][update.markupIndex];
+ }
+ ReactDefaultPerf._recordWrite(update.parentID, update.type, totalTime, writeArgs);
+ });
+ } else {
+ // basic format
+ var id = args[0];
+ if (typeof id === 'object') {
+ id = ReactMount.getID(args[0]);
+ }
+ ReactDefaultPerf._recordWrite(id, fnName, totalTime, Array.prototype.slice.call(args, 1));
+ }
+ return rv;
+ } else if (moduleName === 'ReactCompositeComponent' && (fnName === 'mountComponent' || fnName === 'updateComponent' || // TODO: receiveComponent()?
+ fnName === '_renderValidatedComponent')) {
+
+ if (this._currentElement.type === ReactMount.TopLevelWrapper) {
+ return func.apply(this, args);
+ }
+
+ var rootNodeID = fnName === 'mountComponent' ? args[0] : this._rootNodeID;
+ var isRender = fnName === '_renderValidatedComponent';
+ var isMount = fnName === 'mountComponent';
+
+ var mountStack = ReactDefaultPerf._mountStack;
+ var entry = ReactDefaultPerf._allMeasurements[ReactDefaultPerf._allMeasurements.length - 1];
+
+ if (isRender) {
+ addValue(entry.counts, rootNodeID, 1);
+ } else if (isMount) {
+ entry.created[rootNodeID] = true;
+ mountStack.push(0);
+ }
+
+ start = performanceNow();
+ rv = func.apply(this, args);
+ totalTime = performanceNow() - start;
+
+ if (isRender) {
+ addValue(entry.render, rootNodeID, totalTime);
+ } else if (isMount) {
+ var subMountTime = mountStack.pop();
+ mountStack[mountStack.length - 1] += totalTime;
+ addValue(entry.exclusive, rootNodeID, totalTime - subMountTime);
+ addValue(entry.inclusive, rootNodeID, totalTime);
+ } else {
+ addValue(entry.inclusive, rootNodeID, totalTime);
+ }
+
+ entry.displayNames[rootNodeID] = {
+ current: this.getName(),
+ owner: this._currentElement._owner ? this._currentElement._owner.getName() : '<root>'
+ };
+
+ return rv;
+ } else {
+ return func.apply(this, args);
+ }
+ };
+ }
+};
+
+module.exports = ReactDefaultPerf;
+},{"10":10,"170":170,"56":56,"72":72,"78":78}],56:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDefaultPerfAnalysis
+ */
+
+'use strict';
+
+var assign = _dereq_(24);
+
+// Don't try to save users less than 1.2ms (a number I made up)
+var DONT_CARE_THRESHOLD = 1.2;
+var DOM_OPERATION_TYPES = {
+ '_mountImageIntoNode': 'set innerHTML',
+ INSERT_MARKUP: 'set innerHTML',
+ MOVE_EXISTING: 'move',
+ REMOVE_NODE: 'remove',
+ SET_MARKUP: 'set innerHTML',
+ TEXT_CONTENT: 'set textContent',
+ 'setValueForProperty': 'update attribute',
+ 'setValueForAttribute': 'update attribute',
+ 'deleteValueForProperty': 'remove attribute',
+ 'setValueForStyles': 'update styles',
+ 'replaceNodeWithMarkup': 'replace',
+ 'updateTextContent': 'set textContent'
+};
+
+function getTotalTime(measurements) {
+ // TODO: return number of DOM ops? could be misleading.
+ // TODO: measure dropped frames after reconcile?
+ // TODO: log total time of each reconcile and the top-level component
+ // class that triggered it.
+ var totalTime = 0;
+ for (var i = 0; i < measurements.length; i++) {
+ var measurement = measurements[i];
+ totalTime += measurement.totalTime;
+ }
+ return totalTime;
+}
+
+function getDOMSummary(measurements) {
+ var items = [];
+ measurements.forEach(function (measurement) {
+ Object.keys(measurement.writes).forEach(function (id) {
+ measurement.writes[id].forEach(function (write) {
+ items.push({
+ id: id,
+ type: DOM_OPERATION_TYPES[write.type] || write.type,
+ args: write.args
+ });
+ });
+ });
+ });
+ return items;
+}
+
+function getExclusiveSummary(measurements) {
+ var candidates = {};
+ var displayName;
+
+ for (var i = 0; i < measurements.length; i++) {
+ var measurement = measurements[i];
+ var allIDs = assign({}, measurement.exclusive, measurement.inclusive);
+
+ for (var id in allIDs) {
+ displayName = measurement.displayNames[id].current;
+
+ candidates[displayName] = candidates[displayName] || {
+ componentName: displayName,
+ inclusive: 0,
+ exclusive: 0,
+ render: 0,
+ count: 0
+ };
+ if (measurement.render[id]) {
+ candidates[displayName].render += measurement.render[id];
+ }
+ if (measurement.exclusive[id]) {
+ candidates[displayName].exclusive += measurement.exclusive[id];
+ }
+ if (measurement.inclusive[id]) {
+ candidates[displayName].inclusive += measurement.inclusive[id];
+ }
+ if (measurement.counts[id]) {
+ candidates[displayName].count += measurement.counts[id];
+ }
+ }
+ }
+
+ // Now make a sorted array with the results.
+ var arr = [];
+ for (displayName in candidates) {
+ if (candidates[displayName].exclusive >= DONT_CARE_THRESHOLD) {
+ arr.push(candidates[displayName]);
+ }
+ }
+
+ arr.sort(function (a, b) {
+ return b.exclusive - a.exclusive;
+ });
+
+ return arr;
+}
+
+function getInclusiveSummary(measurements, onlyClean) {
+ var candidates = {};
+ var inclusiveKey;
+
+ for (var i = 0; i < measurements.length; i++) {
+ var measurement = measurements[i];
+ var allIDs = assign({}, measurement.exclusive, measurement.inclusive);
+ var cleanComponents;
+
+ if (onlyClean) {
+ cleanComponents = getUnchangedComponents(measurement);
+ }
+
+ for (var id in allIDs) {
+ if (onlyClean && !cleanComponents[id]) {
+ continue;
+ }
+
+ var displayName = measurement.displayNames[id];
+
+ // Inclusive time is not useful for many components without knowing where
+ // they are instantiated. So we aggregate inclusive time with both the
+ // owner and current displayName as the key.
+ inclusiveKey = displayName.owner + ' > ' + displayName.current;
+
+ candidates[inclusiveKey] = candidates[inclusiveKey] || {
+ componentName: inclusiveKey,
+ time: 0,
+ count: 0
+ };
+
+ if (measurement.inclusive[id]) {
+ candidates[inclusiveKey].time += measurement.inclusive[id];
+ }
+ if (measurement.counts[id]) {
+ candidates[inclusiveKey].count += measurement.counts[id];
+ }
+ }
+ }
+
+ // Now make a sorted array with the results.
+ var arr = [];
+ for (inclusiveKey in candidates) {
+ if (candidates[inclusiveKey].time >= DONT_CARE_THRESHOLD) {
+ arr.push(candidates[inclusiveKey]);
+ }
+ }
+
+ arr.sort(function (a, b) {
+ return b.time - a.time;
+ });
+
+ return arr;
+}
+
+function getUnchangedComponents(measurement) {
+ // For a given reconcile, look at which components did not actually
+ // render anything to the DOM and return a mapping of their ID to
+ // the amount of time it took to render the entire subtree.
+ var cleanComponents = {};
+ var dirtyLeafIDs = Object.keys(measurement.writes);
+ var allIDs = assign({}, measurement.exclusive, measurement.inclusive);
+
+ for (var id in allIDs) {
+ var isDirty = false;
+ // For each component that rendered, see if a component that triggered
+ // a DOM op is in its subtree.
+ for (var i = 0; i < dirtyLeafIDs.length; i++) {
+ if (dirtyLeafIDs[i].indexOf(id) === 0) {
+ isDirty = true;
+ break;
+ }
+ }
+ // check if component newly created
+ if (measurement.created[id]) {
+ isDirty = true;
+ }
+ if (!isDirty && measurement.counts[id] > 0) {
+ cleanComponents[id] = true;
+ }
+ }
+ return cleanComponents;
+}
+
+var ReactDefaultPerfAnalysis = {
+ getExclusiveSummary: getExclusiveSummary,
+ getInclusiveSummary: getInclusiveSummary,
+ getDOMSummary: getDOMSummary,
+ getTotalTime: getTotalTime
+};
+
+module.exports = ReactDefaultPerfAnalysis;
+},{"24":24}],57:[function(_dereq_,module,exports){
+/**
+ * Copyright 2014-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactElement
+ */
+
+'use strict';
+
+var ReactCurrentOwner = _dereq_(39);
+
+var assign = _dereq_(24);
+var canDefineProperty = _dereq_(117);
+
+// The Symbol used to tag the ReactElement type. If there is no native Symbol
+// nor polyfill, then a plain number is used for performance.
+var REACT_ELEMENT_TYPE = typeof Symbol === 'function' && Symbol['for'] && Symbol['for']('react.element') || 0xeac7;
+
+var RESERVED_PROPS = {
+ key: true,
+ ref: true,
+ __self: true,
+ __source: true
+};
+
+/**
+ * Base constructor for all React elements. This is only used to make this
+ * work with a dynamic instanceof check. Nothing should live on this prototype.
+ *
+ * @param {*} type
+ * @param {*} key
+ * @param {string|object} ref
+ * @param {*} self A *temporary* helper to detect places where `this` is
+ * different from the `owner` when React.createElement is called, so that we
+ * can warn. We want to get rid of owner and replace string `ref`s with arrow
+ * functions, and as long as `this` and owner are the same, there will be no
+ * change in behavior.
+ * @param {*} source An annotation object (added by a transpiler or otherwise)
+ * indicating filename, line number, and/or other information.
+ * @param {*} owner
+ * @param {*} props
+ * @internal
+ */
+var ReactElement = function (type, key, ref, self, source, owner, props) {
+ var element = {
+ // This tag allow us to uniquely identify this as a React Element
+ $$typeof: REACT_ELEMENT_TYPE,
+
+ // Built-in properties that belong on the element
+ type: type,
+ key: key,
+ ref: ref,
+ props: props,
+
+ // Record the component responsible for creating this element.
+ _owner: owner
+ };
+
+ if ("development" !== 'production') {
+ // The validation flag is currently mutative. We put it on
+ // an external backing store so that we can freeze the whole object.
+ // This can be replaced with a WeakMap once they are implemented in
+ // commonly used development environments.
+ element._store = {};
+
+ // To make comparing ReactElements easier for testing purposes, we make
+ // the validation flag non-enumerable (where possible, which should
+ // include every environment we run tests in), so the test framework
+ // ignores it.
+ if (canDefineProperty) {
+ Object.defineProperty(element._store, 'validated', {
+ configurable: false,
+ enumerable: false,
+ writable: true,
+ value: false
+ });
+ // self and source are DEV only properties.
+ Object.defineProperty(element, '_self', {
+ configurable: false,
+ enumerable: false,
+ writable: false,
+ value: self
+ });
+ // Two elements created in two different places should be considered
+ // equal for testing purposes and therefore we hide it from enumeration.
+ Object.defineProperty(element, '_source', {
+ configurable: false,
+ enumerable: false,
+ writable: false,
+ value: source
+ });
+ } else {
+ element._store.validated = false;
+ element._self = self;
+ element._source = source;
+ }
+ Object.freeze(element.props);
+ Object.freeze(element);
+ }
+
+ return element;
+};
+
+ReactElement.createElement = function (type, config, children) {
+ var propName;
+
+ // Reserved names are extracted
+ var props = {};
+
+ var key = null;
+ var ref = null;
+ var self = null;
+ var source = null;
+
+ if (config != null) {
+ ref = config.ref === undefined ? null : config.ref;
+ key = config.key === undefined ? null : '' + config.key;
+ self = config.__self === undefined ? null : config.__self;
+ source = config.__source === undefined ? null : config.__source;
+ // Remaining properties are added to a new props object
+ for (propName in config) {
+ if (config.hasOwnProperty(propName) && !RESERVED_PROPS.hasOwnProperty(propName)) {
+ props[propName] = config[propName];
+ }
+ }
+ }
+
+ // Children can be more than one argument, and those are transferred onto
+ // the newly allocated props object.
+ var childrenLength = arguments.length - 2;
+ if (childrenLength === 1) {
+ props.children = children;
+ } else if (childrenLength > 1) {
+ var childArray = Array(childrenLength);
+ for (var i = 0; i < childrenLength; i++) {
+ childArray[i] = arguments[i + 2];
+ }
+ props.children = childArray;
+ }
+
+ // Resolve default props
+ if (type && type.defaultProps) {
+ var defaultProps = type.defaultProps;
+ for (propName in defaultProps) {
+ if (typeof props[propName] === 'undefined') {
+ props[propName] = defaultProps[propName];
+ }
+ }
+ }
+
+ return ReactElement(type, key, ref, self, source, ReactCurrentOwner.current, props);
+};
+
+ReactElement.createFactory = function (type) {
+ var factory = ReactElement.createElement.bind(null, type);
+ // Expose the type on the factory and the prototype so that it can be
+ // easily accessed on elements. E.g. `<Foo />.type === Foo`.
+ // This should not be named `constructor` since this may not be the function
+ // that created the element, and it may not even be a constructor.
+ // Legacy hook TODO: Warn if this is accessed
+ factory.type = type;
+ return factory;
+};
+
+ReactElement.cloneAndReplaceKey = function (oldElement, newKey) {
+ var newElement = ReactElement(oldElement.type, newKey, oldElement.ref, oldElement._self, oldElement._source, oldElement._owner, oldElement.props);
+
+ return newElement;
+};
+
+ReactElement.cloneAndReplaceProps = function (oldElement, newProps) {
+ var newElement = ReactElement(oldElement.type, oldElement.key, oldElement.ref, oldElement._self, oldElement._source, oldElement._owner, newProps);
+
+ if ("development" !== 'production') {
+ // If the key on the original is valid, then the clone is valid
+ newElement._store.validated = oldElement._store.validated;
+ }
+
+ return newElement;
+};
+
+ReactElement.cloneElement = function (element, config, children) {
+ var propName;
+
+ // Original props are copied
+ var props = assign({}, element.props);
+
+ // Reserved names are extracted
+ var key = element.key;
+ var ref = element.ref;
+ // Self is preserved since the owner is preserved.
+ var self = element._self;
+ // Source is preserved since cloneElement is unlikely to be targeted by a
+ // transpiler, and the original source is probably a better indicator of the
+ // true owner.
+ var source = element._source;
+
+ // Owner will be preserved, unless ref is overridden
+ var owner = element._owner;
+
+ if (config != null) {
+ if (config.ref !== undefined) {
+ // Silently steal the ref from the parent.
+ ref = config.ref;
+ owner = ReactCurrentOwner.current;
+ }
+ if (config.key !== undefined) {
+ key = '' + config.key;
+ }
+ // Remaining properties override existing props
+ for (propName in config) {
+ if (config.hasOwnProperty(propName) && !RESERVED_PROPS.hasOwnProperty(propName)) {
+ props[propName] = config[propName];
+ }
+ }
+ }
+
+ // Children can be more than one argument, and those are transferred onto
+ // the newly allocated props object.
+ var childrenLength = arguments.length - 2;
+ if (childrenLength === 1) {
+ props.children = children;
+ } else if (childrenLength > 1) {
+ var childArray = Array(childrenLength);
+ for (var i = 0; i < childrenLength; i++) {
+ childArray[i] = arguments[i + 2];
+ }
+ props.children = childArray;
+ }
+
+ return ReactElement(element.type, key, ref, self, source, owner, props);
+};
+
+/**
+ * @param {?object} object
+ * @return {boolean} True if `object` is a valid component.
+ * @final
+ */
+ReactElement.isValidElement = function (object) {
+ return typeof object === 'object' && object !== null && object.$$typeof === REACT_ELEMENT_TYPE;
+};
+
+module.exports = ReactElement;
+},{"117":117,"24":24,"39":39}],58:[function(_dereq_,module,exports){
+/**
+ * Copyright 2014-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactElementValidator
+ */
+
+/**
+ * ReactElementValidator provides a wrapper around a element factory
+ * which validates the props passed to the element. This is intended to be
+ * used only in DEV and could be replaced by a static type checker for languages
+ * that support it.
+ */
+
+'use strict';
+
+var ReactElement = _dereq_(57);
+var ReactPropTypeLocations = _dereq_(81);
+var ReactPropTypeLocationNames = _dereq_(80);
+var ReactCurrentOwner = _dereq_(39);
+
+var canDefineProperty = _dereq_(117);
+var getIteratorFn = _dereq_(129);
+var invariant = _dereq_(161);
+var warning = _dereq_(173);
+
+function getDeclarationErrorAddendum() {
+ if (ReactCurrentOwner.current) {
+ var name = ReactCurrentOwner.current.getName();
+ if (name) {
+ return ' Check the render method of `' + name + '`.';
+ }
+ }
+ return '';
+}
+
+/**
+ * Warn if there's no key explicitly set on dynamic arrays of children or
+ * object keys are not valid. This allows us to keep track of children between
+ * updates.
+ */
+var ownerHasKeyUseWarning = {};
+
+var loggedTypeFailures = {};
+
+/**
+ * Warn if the element doesn't have an explicit key assigned to it.
+ * This element is in an array. The array could grow and shrink or be
+ * reordered. All children that haven't already been validated are required to
+ * have a "key" property assigned to it.
+ *
+ * @internal
+ * @param {ReactElement} element Element that requires a key.
+ * @param {*} parentType element's parent's type.
+ */
+function validateExplicitKey(element, parentType) {
+ if (!element._store || element._store.validated || element.key != null) {
+ return;
+ }
+ element._store.validated = true;
+
+ var addenda = getAddendaForKeyUse('uniqueKey', element, parentType);
+ if (addenda === null) {
+ // we already showed the warning
+ return;
+ }
+ "development" !== 'production' ? warning(false, 'Each child in an array or iterator should have a unique "key" prop.' + '%s%s%s', addenda.parentOrOwner || '', addenda.childOwner || '', addenda.url || '') : undefined;
+}
+
+/**
+ * Shared warning and monitoring code for the key warnings.
+ *
+ * @internal
+ * @param {string} messageType A key used for de-duping warnings.
+ * @param {ReactElement} element Component that requires a key.
+ * @param {*} parentType element's parent's type.
+ * @returns {?object} A set of addenda to use in the warning message, or null
+ * if the warning has already been shown before (and shouldn't be shown again).
+ */
+function getAddendaForKeyUse(messageType, element, parentType) {
+ var addendum = getDeclarationErrorAddendum();
+ if (!addendum) {
+ var parentName = typeof parentType === 'string' ? parentType : parentType.displayName || parentType.name;
+ if (parentName) {
+ addendum = ' Check the top-level render call using <' + parentName + '>.';
+ }
+ }
+
+ var memoizer = ownerHasKeyUseWarning[messageType] || (ownerHasKeyUseWarning[messageType] = {});
+ if (memoizer[addendum]) {
+ return null;
+ }
+ memoizer[addendum] = true;
+
+ var addenda = {
+ parentOrOwner: addendum,
+ url: ' See https://fb.me/react-warning-keys for more information.',
+ childOwner: null
+ };
+
+ // Usually the current owner is the offender, but if it accepts children as a
+ // property, it may be the creator of the child that's responsible for
+ // assigning it a key.
+ if (element && element._owner && element._owner !== ReactCurrentOwner.current) {
+ // Give the component that originally created this child.
+ addenda.childOwner = ' It was passed a child from ' + element._owner.getName() + '.';
+ }
+
+ return addenda;
+}
+
+/**
+ * Ensure that every element either is passed in a static location, in an
+ * array with an explicit keys property defined, or in an object literal
+ * with valid key property.
+ *
+ * @internal
+ * @param {ReactNode} node Statically passed child of any type.
+ * @param {*} parentType node's parent's type.
+ */
+function validateChildKeys(node, parentType) {
+ if (typeof node !== 'object') {
+ return;
+ }
+ if (Array.isArray(node)) {
+ for (var i = 0; i < node.length; i++) {
+ var child = node[i];
+ if (ReactElement.isValidElement(child)) {
+ validateExplicitKey(child, parentType);
+ }
+ }
+ } else if (ReactElement.isValidElement(node)) {
+ // This element was passed in a valid location.
+ if (node._store) {
+ node._store.validated = true;
+ }
+ } else if (node) {
+ var iteratorFn = getIteratorFn(node);
+ // Entry iterators provide implicit keys.
+ if (iteratorFn) {
+ if (iteratorFn !== node.entries) {
+ var iterator = iteratorFn.call(node);
+ var step;
+ while (!(step = iterator.next()).done) {
+ if (ReactElement.isValidElement(step.value)) {
+ validateExplicitKey(step.value, parentType);
+ }
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Assert that the props are valid
+ *
+ * @param {string} componentName Name of the component for error messages.
+ * @param {object} propTypes Map of prop name to a ReactPropType
+ * @param {object} props
+ * @param {string} location e.g. "prop", "context", "child context"
+ * @private
+ */
+function checkPropTypes(componentName, propTypes, props, location) {
+ for (var propName in propTypes) {
+ if (propTypes.hasOwnProperty(propName)) {
+ var error;
+ // Prop type validation may throw. In case they do, we don't want to
+ // fail the render phase where it didn't fail before. So we log it.
+ // After these have been cleaned up, we'll let them throw.
+ try {
+ // This is intentionally an invariant that gets caught. It's the same
+ // behavior as without this statement except with a better message.
+ !(typeof propTypes[propName] === 'function') ? "development" !== 'production' ? invariant(false, '%s: %s type `%s` is invalid; it must be a function, usually from ' + 'React.PropTypes.', componentName || 'React class', ReactPropTypeLocationNames[location], propName) : invariant(false) : undefined;
+ error = propTypes[propName](props, propName, componentName, location);
+ } catch (ex) {
+ error = ex;
+ }
+ "development" !== 'production' ? warning(!error || error instanceof Error, '%s: type specification of %s `%s` is invalid; the type checker ' + 'function must return `null` or an `Error` but returned a %s. ' + 'You may have forgotten to pass an argument to the type checker ' + 'creator (arrayOf, instanceOf, objectOf, oneOf, oneOfType, and ' + 'shape all require an argument).', componentName || 'React class', ReactPropTypeLocationNames[location], propName, typeof error) : undefined;
+ if (error instanceof Error && !(error.message in loggedTypeFailures)) {
+ // Only monitor this failure once because there tends to be a lot of the
+ // same error.
+ loggedTypeFailures[error.message] = true;
+
+ var addendum = getDeclarationErrorAddendum();
+ "development" !== 'production' ? warning(false, 'Failed propType: %s%s', error.message, addendum) : undefined;
+ }
+ }
+ }
+}
+
+/**
+ * Given an element, validate that its props follow the propTypes definition,
+ * provided by the type.
+ *
+ * @param {ReactElement} element
+ */
+function validatePropTypes(element) {
+ var componentClass = element.type;
+ if (typeof componentClass !== 'function') {
+ return;
+ }
+ var name = componentClass.displayName || componentClass.name;
+ if (componentClass.propTypes) {
+ checkPropTypes(name, componentClass.propTypes, element.props, ReactPropTypeLocations.prop);
+ }
+ if (typeof componentClass.getDefaultProps === 'function') {
+ "development" !== 'production' ? warning(componentClass.getDefaultProps.isReactClassApproved, 'getDefaultProps is only used on classic React.createClass ' + 'definitions. Use a static property named `defaultProps` instead.') : undefined;
+ }
+}
+
+var ReactElementValidator = {
+
+ createElement: function (type, props, children) {
+ var validType = typeof type === 'string' || typeof type === 'function';
+ // We warn in this case but don't throw. We expect the element creation to
+ // succeed and there will likely be errors in render.
+ "development" !== 'production' ? warning(validType, 'React.createElement: type should not be null, undefined, boolean, or ' + 'number. It should be a string (for DOM elements) or a ReactClass ' + '(for composite components).%s', getDeclarationErrorAddendum()) : undefined;
+
+ var element = ReactElement.createElement.apply(this, arguments);
+
+ // The result can be nullish if a mock or a custom function is used.
+ // TODO: Drop this when these are no longer allowed as the type argument.
+ if (element == null) {
+ return element;
+ }
+
+ // Skip key warning if the type isn't valid since our key validation logic
+ // doesn't expect a non-string/function type and can throw confusing errors.
+ // We don't want exception behavior to differ between dev and prod.
+ // (Rendering will throw with a helpful message and as soon as the type is
+ // fixed, the key warnings will appear.)
+ if (validType) {
+ for (var i = 2; i < arguments.length; i++) {
+ validateChildKeys(arguments[i], type);
+ }
+ }
+
+ validatePropTypes(element);
+
+ return element;
+ },
+
+ createFactory: function (type) {
+ var validatedFactory = ReactElementValidator.createElement.bind(null, type);
+ // Legacy hook TODO: Warn if this is accessed
+ validatedFactory.type = type;
+
+ if ("development" !== 'production') {
+ if (canDefineProperty) {
+ Object.defineProperty(validatedFactory, 'type', {
+ enumerable: false,
+ get: function () {
+ "development" !== 'production' ? warning(false, 'Factory.type is deprecated. Access the class directly ' + 'before passing it to createFactory.') : undefined;
+ Object.defineProperty(this, 'type', {
+ value: type
+ });
+ return type;
+ }
+ });
+ }
+ }
+
+ return validatedFactory;
+ },
+
+ cloneElement: function (element, props, children) {
+ var newElement = ReactElement.cloneElement.apply(this, arguments);
+ for (var i = 2; i < arguments.length; i++) {
+ validateChildKeys(arguments[i], newElement.type);
+ }
+ validatePropTypes(newElement);
+ return newElement;
+ }
+
+};
+
+module.exports = ReactElementValidator;
+},{"117":117,"129":129,"161":161,"173":173,"39":39,"57":57,"80":80,"81":81}],59:[function(_dereq_,module,exports){
+/**
+ * Copyright 2014-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactEmptyComponent
+ */
+
+'use strict';
+
+var ReactElement = _dereq_(57);
+var ReactEmptyComponentRegistry = _dereq_(60);
+var ReactReconciler = _dereq_(84);
+
+var assign = _dereq_(24);
+
+var placeholderElement;
+
+var ReactEmptyComponentInjection = {
+ injectEmptyComponent: function (component) {
+ placeholderElement = ReactElement.createElement(component);
+ }
+};
+
+var ReactEmptyComponent = function (instantiate) {
+ this._currentElement = null;
+ this._rootNodeID = null;
+ this._renderedComponent = instantiate(placeholderElement);
+};
+assign(ReactEmptyComponent.prototype, {
+ construct: function (element) {},
+ mountComponent: function (rootID, transaction, context) {
+ ReactEmptyComponentRegistry.registerNullComponentID(rootID);
+ this._rootNodeID = rootID;
+ return ReactReconciler.mountComponent(this._renderedComponent, rootID, transaction, context);
+ },
+ receiveComponent: function () {},
+ unmountComponent: function (rootID, transaction, context) {
+ ReactReconciler.unmountComponent(this._renderedComponent);
+ ReactEmptyComponentRegistry.deregisterNullComponentID(this._rootNodeID);
+ this._rootNodeID = null;
+ this._renderedComponent = null;
+ }
+});
+
+ReactEmptyComponent.injection = ReactEmptyComponentInjection;
+
+module.exports = ReactEmptyComponent;
+},{"24":24,"57":57,"60":60,"84":84}],60:[function(_dereq_,module,exports){
+/**
+ * Copyright 2014-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactEmptyComponentRegistry
+ */
+
+'use strict';
+
+// This registry keeps track of the React IDs of the components that rendered to
+// `null` (in reality a placeholder such as `noscript`)
+var nullComponentIDsRegistry = {};
+
+/**
+ * @param {string} id Component's `_rootNodeID`.
+ * @return {boolean} True if the component is rendered to null.
+ */
+function isNullComponentID(id) {
+ return !!nullComponentIDsRegistry[id];
+}
+
+/**
+ * Mark the component as having rendered to null.
+ * @param {string} id Component's `_rootNodeID`.
+ */
+function registerNullComponentID(id) {
+ nullComponentIDsRegistry[id] = true;
+}
+
+/**
+ * Unmark the component as having rendered to null: it renders to something now.
+ * @param {string} id Component's `_rootNodeID`.
+ */
+function deregisterNullComponentID(id) {
+ delete nullComponentIDsRegistry[id];
+}
+
+var ReactEmptyComponentRegistry = {
+ isNullComponentID: isNullComponentID,
+ registerNullComponentID: registerNullComponentID,
+ deregisterNullComponentID: deregisterNullComponentID
+};
+
+module.exports = ReactEmptyComponentRegistry;
+},{}],61:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactErrorUtils
+ * @typechecks
+ */
+
+'use strict';
+
+var caughtError = null;
+
+/**
+ * Call a function while guarding against errors that happens within it.
+ *
+ * @param {?String} name of the guard to use for logging or debugging
+ * @param {Function} func The function to invoke
+ * @param {*} a First argument
+ * @param {*} b Second argument
+ */
+function invokeGuardedCallback(name, func, a, b) {
+ try {
+ return func(a, b);
+ } catch (x) {
+ if (caughtError === null) {
+ caughtError = x;
+ }
+ return undefined;
+ }
+}
+
+var ReactErrorUtils = {
+ invokeGuardedCallback: invokeGuardedCallback,
+
+ /**
+ * Invoked by ReactTestUtils.Simulate so that any errors thrown by the event
+ * handler are sure to be rethrown by rethrowCaughtError.
+ */
+ invokeGuardedCallbackWithCatch: invokeGuardedCallback,
+
+ /**
+ * During execution of guarded functions we will capture the first error which
+ * we will rethrow to be handled by the top level error handler.
+ */
+ rethrowCaughtError: function () {
+ if (caughtError) {
+ var error = caughtError;
+ caughtError = null;
+ throw error;
+ }
+ }
+};
+
+if ("development" !== 'production') {
+ /**
+ * To help development we can get better devtools integration by simulating a
+ * real browser event.
+ */
+ if (typeof window !== 'undefined' && typeof window.dispatchEvent === 'function' && typeof document !== 'undefined' && typeof document.createEvent === 'function') {
+ var fakeNode = document.createElementNS('http://www.w3.org/1999/xhtml', 'react');
+ ReactErrorUtils.invokeGuardedCallback = function (name, func, a, b) {
+ var boundFunc = func.bind(null, a, b);
+ var evtType = 'react-' + name;
+ fakeNode.addEventListener(evtType, boundFunc, false);
+ var evt = document.createEvent('Event');
+ evt.initEvent(evtType, false, false);
+ fakeNode.dispatchEvent(evt);
+ fakeNode.removeEventListener(evtType, boundFunc, false);
+ };
+ }
+}
+
+module.exports = ReactErrorUtils;
+},{}],62:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactEventEmitterMixin
+ */
+
+'use strict';
+
+var EventPluginHub = _dereq_(16);
+
+function runEventQueueInBatch(events) {
+ EventPluginHub.enqueueEvents(events);
+ EventPluginHub.processEventQueue(false);
+}
+
+var ReactEventEmitterMixin = {
+
+ /**
+ * Streams a fired top-level event to `EventPluginHub` where plugins have the
+ * opportunity to create `ReactEvent`s to be dispatched.
+ *
+ * @param {string} topLevelType Record from `EventConstants`.
+ * @param {object} topLevelTarget The listening component root node.
+ * @param {string} topLevelTargetID ID of `topLevelTarget`.
+ * @param {object} nativeEvent Native environment event.
+ */
+ handleTopLevel: function (topLevelType, topLevelTarget, topLevelTargetID, nativeEvent, nativeEventTarget) {
+ var events = EventPluginHub.extractEvents(topLevelType, topLevelTarget, topLevelTargetID, nativeEvent, nativeEventTarget);
+ runEventQueueInBatch(events);
+ }
+};
+
+module.exports = ReactEventEmitterMixin;
+},{"16":16}],63:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactEventListener
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var EventListener = _dereq_(146);
+var ExecutionEnvironment = _dereq_(147);
+var PooledClass = _dereq_(25);
+var ReactInstanceHandles = _dereq_(67);
+var ReactMount = _dereq_(72);
+var ReactUpdates = _dereq_(96);
+
+var assign = _dereq_(24);
+var getEventTarget = _dereq_(128);
+var getUnboundedScrollPosition = _dereq_(158);
+
+var DOCUMENT_FRAGMENT_NODE_TYPE = 11;
+
+/**
+ * Finds the parent React component of `node`.
+ *
+ * @param {*} node
+ * @return {?DOMEventTarget} Parent container, or `null` if the specified node
+ * is not nested.
+ */
+function findParent(node) {
+ // TODO: It may be a good idea to cache this to prevent unnecessary DOM
+ // traversal, but caching is difficult to do correctly without using a
+ // mutation observer to listen for all DOM changes.
+ var nodeID = ReactMount.getID(node);
+ var rootID = ReactInstanceHandles.getReactRootIDFromNodeID(nodeID);
+ var container = ReactMount.findReactContainerForID(rootID);
+ var parent = ReactMount.getFirstReactDOM(container);
+ return parent;
+}
+
+// Used to store ancestor hierarchy in top level callback
+function TopLevelCallbackBookKeeping(topLevelType, nativeEvent) {
+ this.topLevelType = topLevelType;
+ this.nativeEvent = nativeEvent;
+ this.ancestors = [];
+}
+assign(TopLevelCallbackBookKeeping.prototype, {
+ destructor: function () {
+ this.topLevelType = null;
+ this.nativeEvent = null;
+ this.ancestors.length = 0;
+ }
+});
+PooledClass.addPoolingTo(TopLevelCallbackBookKeeping, PooledClass.twoArgumentPooler);
+
+function handleTopLevelImpl(bookKeeping) {
+ // TODO: Re-enable event.path handling
+ //
+ // if (bookKeeping.nativeEvent.path && bookKeeping.nativeEvent.path.length > 1) {
+ // // New browsers have a path attribute on native events
+ // handleTopLevelWithPath(bookKeeping);
+ // } else {
+ // // Legacy browsers don't have a path attribute on native events
+ // handleTopLevelWithoutPath(bookKeeping);
+ // }
+
+ void handleTopLevelWithPath; // temporarily unused
+ handleTopLevelWithoutPath(bookKeeping);
+}
+
+// Legacy browsers don't have a path attribute on native events
+function handleTopLevelWithoutPath(bookKeeping) {
+ var topLevelTarget = ReactMount.getFirstReactDOM(getEventTarget(bookKeeping.nativeEvent)) || window;
+
+ // Loop through the hierarchy, in case there's any nested components.
+ // It's important that we build the array of ancestors before calling any
+ // event handlers, because event handlers can modify the DOM, leading to
+ // inconsistencies with ReactMount's node cache. See #1105.
+ var ancestor = topLevelTarget;
+ while (ancestor) {
+ bookKeeping.ancestors.push(ancestor);
+ ancestor = findParent(ancestor);
+ }
+
+ for (var i = 0; i < bookKeeping.ancestors.length; i++) {
+ topLevelTarget = bookKeeping.ancestors[i];
+ var topLevelTargetID = ReactMount.getID(topLevelTarget) || '';
+ ReactEventListener._handleTopLevel(bookKeeping.topLevelType, topLevelTarget, topLevelTargetID, bookKeeping.nativeEvent, getEventTarget(bookKeeping.nativeEvent));
+ }
+}
+
+// New browsers have a path attribute on native events
+function handleTopLevelWithPath(bookKeeping) {
+ var path = bookKeeping.nativeEvent.path;
+ var currentNativeTarget = path[0];
+ var eventsFired = 0;
+ for (var i = 0; i < path.length; i++) {
+ var currentPathElement = path[i];
+ if (currentPathElement.nodeType === DOCUMENT_FRAGMENT_NODE_TYPE) {
+ currentNativeTarget = path[i + 1];
+ }
+ // TODO: slow
+ var reactParent = ReactMount.getFirstReactDOM(currentPathElement);
+ if (reactParent === currentPathElement) {
+ var currentPathElementID = ReactMount.getID(currentPathElement);
+ var newRootID = ReactInstanceHandles.getReactRootIDFromNodeID(currentPathElementID);
+ bookKeeping.ancestors.push(currentPathElement);
+
+ var topLevelTargetID = ReactMount.getID(currentPathElement) || '';
+ eventsFired++;
+ ReactEventListener._handleTopLevel(bookKeeping.topLevelType, currentPathElement, topLevelTargetID, bookKeeping.nativeEvent, currentNativeTarget);
+
+ // Jump to the root of this React render tree
+ while (currentPathElementID !== newRootID) {
+ i++;
+ currentPathElement = path[i];
+ currentPathElementID = ReactMount.getID(currentPathElement);
+ }
+ }
+ }
+ if (eventsFired === 0) {
+ ReactEventListener._handleTopLevel(bookKeeping.topLevelType, window, '', bookKeeping.nativeEvent, getEventTarget(bookKeeping.nativeEvent));
+ }
+}
+
+function scrollValueMonitor(cb) {
+ var scrollPosition = getUnboundedScrollPosition(window);
+ cb(scrollPosition);
+}
+
+var ReactEventListener = {
+ _enabled: true,
+ _handleTopLevel: null,
+
+ WINDOW_HANDLE: ExecutionEnvironment.canUseDOM ? window : null,
+
+ setHandleTopLevel: function (handleTopLevel) {
+ ReactEventListener._handleTopLevel = handleTopLevel;
+ },
+
+ setEnabled: function (enabled) {
+ ReactEventListener._enabled = !!enabled;
+ },
+
+ isEnabled: function () {
+ return ReactEventListener._enabled;
+ },
+
+ /**
+ * Traps top-level events by using event bubbling.
+ *
+ * @param {string} topLevelType Record from `EventConstants`.
+ * @param {string} handlerBaseName Event name (e.g. "click").
+ * @param {object} handle Element on which to attach listener.
+ * @return {?object} An object with a remove function which will forcefully
+ * remove the listener.
+ * @internal
+ */
+ trapBubbledEvent: function (topLevelType, handlerBaseName, handle) {
+ var element = handle;
+ if (!element) {
+ return null;
+ }
+ return EventListener.listen(element, handlerBaseName, ReactEventListener.dispatchEvent.bind(null, topLevelType));
+ },
+
+ /**
+ * Traps a top-level event by using event capturing.
+ *
+ * @param {string} topLevelType Record from `EventConstants`.
+ * @param {string} handlerBaseName Event name (e.g. "click").
+ * @param {object} handle Element on which to attach listener.
+ * @return {?object} An object with a remove function which will forcefully
+ * remove the listener.
+ * @internal
+ */
+ trapCapturedEvent: function (topLevelType, handlerBaseName, handle) {
+ var element = handle;
+ if (!element) {
+ return null;
+ }
+ return EventListener.capture(element, handlerBaseName, ReactEventListener.dispatchEvent.bind(null, topLevelType));
+ },
+
+ monitorScrollValue: function (refresh) {
+ var callback = scrollValueMonitor.bind(null, refresh);
+ EventListener.listen(window, 'scroll', callback);
+ },
+
+ dispatchEvent: function (topLevelType, nativeEvent) {
+ if (!ReactEventListener._enabled) {
+ return;
+ }
+
+ var bookKeeping = TopLevelCallbackBookKeeping.getPooled(topLevelType, nativeEvent);
+ try {
+ // Event queue being processed in the same cycle allows
+ // `preventDefault`.
+ ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping);
+ } finally {
+ TopLevelCallbackBookKeeping.release(bookKeeping);
+ }
+ }
+};
+
+module.exports = ReactEventListener;
+},{"128":128,"146":146,"147":147,"158":158,"24":24,"25":25,"67":67,"72":72,"96":96}],64:[function(_dereq_,module,exports){
+/**
+ * Copyright 2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactFragment
+ */
+
+'use strict';
+
+var ReactChildren = _dereq_(32);
+var ReactElement = _dereq_(57);
+
+var emptyFunction = _dereq_(153);
+var invariant = _dereq_(161);
+var warning = _dereq_(173);
+
+/**
+ * We used to allow keyed objects to serve as a collection of ReactElements,
+ * or nested sets. This allowed us a way to explicitly key a set a fragment of
+ * components. This is now being replaced with an opaque data structure.
+ * The upgrade path is to call React.addons.createFragment({ key: value }) to
+ * create a keyed fragment. The resulting data structure is an array.
+ */
+
+var numericPropertyRegex = /^\d+$/;
+
+var warnedAboutNumeric = false;
+
+var ReactFragment = {
+ // Wrap a keyed object in an opaque proxy that warns you if you access any
+ // of its properties.
+ create: function (object) {
+ if (typeof object !== 'object' || !object || Array.isArray(object)) {
+ "development" !== 'production' ? warning(false, 'React.addons.createFragment only accepts a single object. Got: %s', object) : undefined;
+ return object;
+ }
+ if (ReactElement.isValidElement(object)) {
+ "development" !== 'production' ? warning(false, 'React.addons.createFragment does not accept a ReactElement ' + 'without a wrapper object.') : undefined;
+ return object;
+ }
+
+ !(object.nodeType !== 1) ? "development" !== 'production' ? invariant(false, 'React.addons.createFragment(...): Encountered an invalid child; DOM ' + 'elements are not valid children of React components.') : invariant(false) : undefined;
+
+ var result = [];
+
+ for (var key in object) {
+ if ("development" !== 'production') {
+ if (!warnedAboutNumeric && numericPropertyRegex.test(key)) {
+ "development" !== 'production' ? warning(false, 'React.addons.createFragment(...): Child objects should have ' + 'non-numeric keys so ordering is preserved.') : undefined;
+ warnedAboutNumeric = true;
+ }
+ }
+ ReactChildren.mapIntoWithKeyPrefixInternal(object[key], result, key, emptyFunction.thatReturnsArgument);
+ }
+
+ return result;
+ }
+};
+
+module.exports = ReactFragment;
+},{"153":153,"161":161,"173":173,"32":32,"57":57}],65:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactInjection
+ */
+
+'use strict';
+
+var DOMProperty = _dereq_(10);
+var EventPluginHub = _dereq_(16);
+var ReactComponentEnvironment = _dereq_(36);
+var ReactClass = _dereq_(33);
+var ReactEmptyComponent = _dereq_(59);
+var ReactBrowserEventEmitter = _dereq_(28);
+var ReactNativeComponent = _dereq_(75);
+var ReactPerf = _dereq_(78);
+var ReactRootIndex = _dereq_(86);
+var ReactUpdates = _dereq_(96);
+
+var ReactInjection = {
+ Component: ReactComponentEnvironment.injection,
+ Class: ReactClass.injection,
+ DOMProperty: DOMProperty.injection,
+ EmptyComponent: ReactEmptyComponent.injection,
+ EventPluginHub: EventPluginHub.injection,
+ EventEmitter: ReactBrowserEventEmitter.injection,
+ NativeComponent: ReactNativeComponent.injection,
+ Perf: ReactPerf.injection,
+ RootIndex: ReactRootIndex.injection,
+ Updates: ReactUpdates.injection
+};
+
+module.exports = ReactInjection;
+},{"10":10,"16":16,"28":28,"33":33,"36":36,"59":59,"75":75,"78":78,"86":86,"96":96}],66:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactInputSelection
+ */
+
+'use strict';
+
+var ReactDOMSelection = _dereq_(49);
+
+var containsNode = _dereq_(150);
+var focusNode = _dereq_(155);
+var getActiveElement = _dereq_(156);
+
+function isInDocument(node) {
+ return containsNode(document.documentElement, node);
+}
+
+/**
+ * @ReactInputSelection: React input selection module. Based on Selection.js,
+ * but modified to be suitable for react and has a couple of bug fixes (doesn't
+ * assume buttons have range selections allowed).
+ * Input selection module for React.
+ */
+var ReactInputSelection = {
+
+ hasSelectionCapabilities: function (elem) {
+ var nodeName = elem && elem.nodeName && elem.nodeName.toLowerCase();
+ return nodeName && (nodeName === 'input' && elem.type === 'text' || nodeName === 'textarea' || elem.contentEditable === 'true');
+ },
+
+ getSelectionInformation: function () {
+ var focusedElem = getActiveElement();
+ return {
+ focusedElem: focusedElem,
+ selectionRange: ReactInputSelection.hasSelectionCapabilities(focusedElem) ? ReactInputSelection.getSelection(focusedElem) : null
+ };
+ },
+
+ /**
+ * @restoreSelection: If any selection information was potentially lost,
+ * restore it. This is useful when performing operations that could remove dom
+ * nodes and place them back in, resulting in focus being lost.
+ */
+ restoreSelection: function (priorSelectionInformation) {
+ var curFocusedElem = getActiveElement();
+ var priorFocusedElem = priorSelectionInformation.focusedElem;
+ var priorSelectionRange = priorSelectionInformation.selectionRange;
+ if (curFocusedElem !== priorFocusedElem && isInDocument(priorFocusedElem)) {
+ if (ReactInputSelection.hasSelectionCapabilities(priorFocusedElem)) {
+ ReactInputSelection.setSelection(priorFocusedElem, priorSelectionRange);
+ }
+ focusNode(priorFocusedElem);
+ }
+ },
+
+ /**
+ * @getSelection: Gets the selection bounds of a focused textarea, input or
+ * contentEditable node.
+ * -@input: Look up selection bounds of this input
+ * -@return {start: selectionStart, end: selectionEnd}
+ */
+ getSelection: function (input) {
+ var selection;
+
+ if ('selectionStart' in input) {
+ // Modern browser with input or textarea.
+ selection = {
+ start: input.selectionStart,
+ end: input.selectionEnd
+ };
+ } else if (document.selection && (input.nodeName && input.nodeName.toLowerCase() === 'input')) {
+ // IE8 input.
+ var range = document.selection.createRange();
+ // There can only be one selection per document in IE, so it must
+ // be in our element.
+ if (range.parentElement() === input) {
+ selection = {
+ start: -range.moveStart('character', -input.value.length),
+ end: -range.moveEnd('character', -input.value.length)
+ };
+ }
+ } else {
+ // Content editable or old IE textarea.
+ selection = ReactDOMSelection.getOffsets(input);
+ }
+
+ return selection || { start: 0, end: 0 };
+ },
+
+ /**
+ * @setSelection: Sets the selection bounds of a textarea or input and focuses
+ * the input.
+ * -@input Set selection bounds of this input or textarea
+ * -@offsets Object of same form that is returned from get*
+ */
+ setSelection: function (input, offsets) {
+ var start = offsets.start;
+ var end = offsets.end;
+ if (typeof end === 'undefined') {
+ end = start;
+ }
+
+ if ('selectionStart' in input) {
+ input.selectionStart = start;
+ input.selectionEnd = Math.min(end, input.value.length);
+ } else if (document.selection && (input.nodeName && input.nodeName.toLowerCase() === 'input')) {
+ var range = input.createTextRange();
+ range.collapse(true);
+ range.moveStart('character', start);
+ range.moveEnd('character', end - start);
+ range.select();
+ } else {
+ ReactDOMSelection.setOffsets(input, offsets);
+ }
+ }
+};
+
+module.exports = ReactInputSelection;
+},{"150":150,"155":155,"156":156,"49":49}],67:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactInstanceHandles
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var ReactRootIndex = _dereq_(86);
+
+var invariant = _dereq_(161);
+
+var SEPARATOR = '.';
+var SEPARATOR_LENGTH = SEPARATOR.length;
+
+/**
+ * Maximum depth of traversals before we consider the possibility of a bad ID.
+ */
+var MAX_TREE_DEPTH = 10000;
+
+/**
+ * Creates a DOM ID prefix to use when mounting React components.
+ *
+ * @param {number} index A unique integer
+ * @return {string} React root ID.
+ * @internal
+ */
+function getReactRootIDString(index) {
+ return SEPARATOR + index.toString(36);
+}
+
+/**
+ * Checks if a character in the supplied ID is a separator or the end.
+ *
+ * @param {string} id A React DOM ID.
+ * @param {number} index Index of the character to check.
+ * @return {boolean} True if the character is a separator or end of the ID.
+ * @private
+ */
+function isBoundary(id, index) {
+ return id.charAt(index) === SEPARATOR || index === id.length;
+}
+
+/**
+ * Checks if the supplied string is a valid React DOM ID.
+ *
+ * @param {string} id A React DOM ID, maybe.
+ * @return {boolean} True if the string is a valid React DOM ID.
+ * @private
+ */
+function isValidID(id) {
+ return id === '' || id.charAt(0) === SEPARATOR && id.charAt(id.length - 1) !== SEPARATOR;
+}
+
+/**
+ * Checks if the first ID is an ancestor of or equal to the second ID.
+ *
+ * @param {string} ancestorID
+ * @param {string} descendantID
+ * @return {boolean} True if `ancestorID` is an ancestor of `descendantID`.
+ * @internal
+ */
+function isAncestorIDOf(ancestorID, descendantID) {
+ return descendantID.indexOf(ancestorID) === 0 && isBoundary(descendantID, ancestorID.length);
+}
+
+/**
+ * Gets the parent ID of the supplied React DOM ID, `id`.
+ *
+ * @param {string} id ID of a component.
+ * @return {string} ID of the parent, or an empty string.
+ * @private
+ */
+function getParentID(id) {
+ return id ? id.substr(0, id.lastIndexOf(SEPARATOR)) : '';
+}
+
+/**
+ * Gets the next DOM ID on the tree path from the supplied `ancestorID` to the
+ * supplied `destinationID`. If they are equal, the ID is returned.
+ *
+ * @param {string} ancestorID ID of an ancestor node of `destinationID`.
+ * @param {string} destinationID ID of the destination node.
+ * @return {string} Next ID on the path from `ancestorID` to `destinationID`.
+ * @private
+ */
+function getNextDescendantID(ancestorID, destinationID) {
+ !(isValidID(ancestorID) && isValidID(destinationID)) ? "development" !== 'production' ? invariant(false, 'getNextDescendantID(%s, %s): Received an invalid React DOM ID.', ancestorID, destinationID) : invariant(false) : undefined;
+ !isAncestorIDOf(ancestorID, destinationID) ? "development" !== 'production' ? invariant(false, 'getNextDescendantID(...): React has made an invalid assumption about ' + 'the DOM hierarchy. Expected `%s` to be an ancestor of `%s`.', ancestorID, destinationID) : invariant(false) : undefined;
+ if (ancestorID === destinationID) {
+ return ancestorID;
+ }
+ // Skip over the ancestor and the immediate separator. Traverse until we hit
+ // another separator or we reach the end of `destinationID`.
+ var start = ancestorID.length + SEPARATOR_LENGTH;
+ var i;
+ for (i = start; i < destinationID.length; i++) {
+ if (isBoundary(destinationID, i)) {
+ break;
+ }
+ }
+ return destinationID.substr(0, i);
+}
+
+/**
+ * Gets the nearest common ancestor ID of two IDs.
+ *
+ * Using this ID scheme, the nearest common ancestor ID is the longest common
+ * prefix of the two IDs that immediately preceded a "marker" in both strings.
+ *
+ * @param {string} oneID
+ * @param {string} twoID
+ * @return {string} Nearest common ancestor ID, or the empty string if none.
+ * @private
+ */
+function getFirstCommonAncestorID(oneID, twoID) {
+ var minLength = Math.min(oneID.length, twoID.length);
+ if (minLength === 0) {
+ return '';
+ }
+ var lastCommonMarkerIndex = 0;
+ // Use `<=` to traverse until the "EOL" of the shorter string.
+ for (var i = 0; i <= minLength; i++) {
+ if (isBoundary(oneID, i) && isBoundary(twoID, i)) {
+ lastCommonMarkerIndex = i;
+ } else if (oneID.charAt(i) !== twoID.charAt(i)) {
+ break;
+ }
+ }
+ var longestCommonID = oneID.substr(0, lastCommonMarkerIndex);
+ !isValidID(longestCommonID) ? "development" !== 'production' ? invariant(false, 'getFirstCommonAncestorID(%s, %s): Expected a valid React DOM ID: %s', oneID, twoID, longestCommonID) : invariant(false) : undefined;
+ return longestCommonID;
+}
+
+/**
+ * Traverses the parent path between two IDs (either up or down). The IDs must
+ * not be the same, and there must exist a parent path between them. If the
+ * callback returns `false`, traversal is stopped.
+ *
+ * @param {?string} start ID at which to start traversal.
+ * @param {?string} stop ID at which to end traversal.
+ * @param {function} cb Callback to invoke each ID with.
+ * @param {*} arg Argument to invoke the callback with.
+ * @param {?boolean} skipFirst Whether or not to skip the first node.
+ * @param {?boolean} skipLast Whether or not to skip the last node.
+ * @private
+ */
+function traverseParentPath(start, stop, cb, arg, skipFirst, skipLast) {
+ start = start || '';
+ stop = stop || '';
+ !(start !== stop) ? "development" !== 'production' ? invariant(false, 'traverseParentPath(...): Cannot traverse from and to the same ID, `%s`.', start) : invariant(false) : undefined;
+ var traverseUp = isAncestorIDOf(stop, start);
+ !(traverseUp || isAncestorIDOf(start, stop)) ? "development" !== 'production' ? invariant(false, 'traverseParentPath(%s, %s, ...): Cannot traverse from two IDs that do ' + 'not have a parent path.', start, stop) : invariant(false) : undefined;
+ // Traverse from `start` to `stop` one depth at a time.
+ var depth = 0;
+ var traverse = traverseUp ? getParentID : getNextDescendantID;
+ for (var id = start;; /* until break */id = traverse(id, stop)) {
+ var ret;
+ if ((!skipFirst || id !== start) && (!skipLast || id !== stop)) {
+ ret = cb(id, traverseUp, arg);
+ }
+ if (ret === false || id === stop) {
+ // Only break //after// visiting `stop`.
+ break;
+ }
+ !(depth++ < MAX_TREE_DEPTH) ? "development" !== 'production' ? invariant(false, 'traverseParentPath(%s, %s, ...): Detected an infinite loop while ' + 'traversing the React DOM ID tree. This may be due to malformed IDs: %s', start, stop, id) : invariant(false) : undefined;
+ }
+}
+
+/**
+ * Manages the IDs assigned to DOM representations of React components. This
+ * uses a specific scheme in order to traverse the DOM efficiently (e.g. in
+ * order to simulate events).
+ *
+ * @internal
+ */
+var ReactInstanceHandles = {
+
+ /**
+ * Constructs a React root ID
+ * @return {string} A React root ID.
+ */
+ createReactRootID: function () {
+ return getReactRootIDString(ReactRootIndex.createReactRootIndex());
+ },
+
+ /**
+ * Constructs a React ID by joining a root ID with a name.
+ *
+ * @param {string} rootID Root ID of a parent component.
+ * @param {string} name A component's name (as flattened children).
+ * @return {string} A React ID.
+ * @internal
+ */
+ createReactID: function (rootID, name) {
+ return rootID + name;
+ },
+
+ /**
+ * Gets the DOM ID of the React component that is the root of the tree that
+ * contains the React component with the supplied DOM ID.
+ *
+ * @param {string} id DOM ID of a React component.
+ * @return {?string} DOM ID of the React component that is the root.
+ * @internal
+ */
+ getReactRootIDFromNodeID: function (id) {
+ if (id && id.charAt(0) === SEPARATOR && id.length > 1) {
+ var index = id.indexOf(SEPARATOR, 1);
+ return index > -1 ? id.substr(0, index) : id;
+ }
+ return null;
+ },
+
+ /**
+ * Traverses the ID hierarchy and invokes the supplied `cb` on any IDs that
+ * should would receive a `mouseEnter` or `mouseLeave` event.
+ *
+ * NOTE: Does not invoke the callback on the nearest common ancestor because
+ * nothing "entered" or "left" that element.
+ *
+ * @param {string} leaveID ID being left.
+ * @param {string} enterID ID being entered.
+ * @param {function} cb Callback to invoke on each entered/left ID.
+ * @param {*} upArg Argument to invoke the callback with on left IDs.
+ * @param {*} downArg Argument to invoke the callback with on entered IDs.
+ * @internal
+ */
+ traverseEnterLeave: function (leaveID, enterID, cb, upArg, downArg) {
+ var ancestorID = getFirstCommonAncestorID(leaveID, enterID);
+ if (ancestorID !== leaveID) {
+ traverseParentPath(leaveID, ancestorID, cb, upArg, false, true);
+ }
+ if (ancestorID !== enterID) {
+ traverseParentPath(ancestorID, enterID, cb, downArg, true, false);
+ }
+ },
+
+ /**
+ * Simulates the traversal of a two-phase, capture/bubble event dispatch.
+ *
+ * NOTE: This traversal happens on IDs without touching the DOM.
+ *
+ * @param {string} targetID ID of the target node.
+ * @param {function} cb Callback to invoke.
+ * @param {*} arg Argument to invoke the callback with.
+ * @internal
+ */
+ traverseTwoPhase: function (targetID, cb, arg) {
+ if (targetID) {
+ traverseParentPath('', targetID, cb, arg, true, false);
+ traverseParentPath(targetID, '', cb, arg, false, true);
+ }
+ },
+
+ /**
+ * Same as `traverseTwoPhase` but skips the `targetID`.
+ */
+ traverseTwoPhaseSkipTarget: function (targetID, cb, arg) {
+ if (targetID) {
+ traverseParentPath('', targetID, cb, arg, true, true);
+ traverseParentPath(targetID, '', cb, arg, true, true);
+ }
+ },
+
+ /**
+ * Traverse a node ID, calling the supplied `cb` for each ancestor ID. For
+ * example, passing `.0.$row-0.1` would result in `cb` getting called
+ * with `.0`, `.0.$row-0`, and `.0.$row-0.1`.
+ *
+ * NOTE: This traversal happens on IDs without touching the DOM.
+ *
+ * @param {string} targetID ID of the target node.
+ * @param {function} cb Callback to invoke.
+ * @param {*} arg Argument to invoke the callback with.
+ * @internal
+ */
+ traverseAncestors: function (targetID, cb, arg) {
+ traverseParentPath('', targetID, cb, arg, true, false);
+ },
+
+ getFirstCommonAncestorID: getFirstCommonAncestorID,
+
+ /**
+ * Exposed for unit testing.
+ * @private
+ */
+ _getNextDescendantID: getNextDescendantID,
+
+ isAncestorIDOf: isAncestorIDOf,
+
+ SEPARATOR: SEPARATOR
+
+};
+
+module.exports = ReactInstanceHandles;
+},{"161":161,"86":86}],68:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactInstanceMap
+ */
+
+'use strict';
+
+/**
+ * `ReactInstanceMap` maintains a mapping from a public facing stateful
+ * instance (key) and the internal representation (value). This allows public
+ * methods to accept the user facing instance as an argument and map them back
+ * to internal methods.
+ */
+
+// TODO: Replace this with ES6: var ReactInstanceMap = new Map();
+var ReactInstanceMap = {
+
+ /**
+ * This API should be called `delete` but we'd have to make sure to always
+ * transform these to strings for IE support. When this transform is fully
+ * supported we can rename it.
+ */
+ remove: function (key) {
+ key._reactInternalInstance = undefined;
+ },
+
+ get: function (key) {
+ return key._reactInternalInstance;
+ },
+
+ has: function (key) {
+ return key._reactInternalInstance !== undefined;
+ },
+
+ set: function (key, value) {
+ key._reactInternalInstance = value;
+ }
+
+};
+
+module.exports = ReactInstanceMap;
+},{}],69:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactIsomorphic
+ */
+
+'use strict';
+
+var ReactChildren = _dereq_(32);
+var ReactComponent = _dereq_(34);
+var ReactClass = _dereq_(33);
+var ReactDOMFactories = _dereq_(43);
+var ReactElement = _dereq_(57);
+var ReactElementValidator = _dereq_(58);
+var ReactPropTypes = _dereq_(82);
+var ReactVersion = _dereq_(97);
+
+var assign = _dereq_(24);
+var onlyChild = _dereq_(135);
+
+var createElement = ReactElement.createElement;
+var createFactory = ReactElement.createFactory;
+var cloneElement = ReactElement.cloneElement;
+
+if ("development" !== 'production') {
+ createElement = ReactElementValidator.createElement;
+ createFactory = ReactElementValidator.createFactory;
+ cloneElement = ReactElementValidator.cloneElement;
+}
+
+var React = {
+
+ // Modern
+
+ Children: {
+ map: ReactChildren.map,
+ forEach: ReactChildren.forEach,
+ count: ReactChildren.count,
+ toArray: ReactChildren.toArray,
+ only: onlyChild
+ },
+
+ Component: ReactComponent,
+
+ createElement: createElement,
+ cloneElement: cloneElement,
+ isValidElement: ReactElement.isValidElement,
+
+ // Classic
+
+ PropTypes: ReactPropTypes,
+ createClass: ReactClass.createClass,
+ createFactory: createFactory,
+ createMixin: function (mixin) {
+ // Currently a noop. Will be used to validate and trace mixins.
+ return mixin;
+ },
+
+ // This looks DOM specific but these are actually isomorphic helpers
+ // since they are just generating DOM strings.
+ DOM: ReactDOMFactories,
+
+ version: ReactVersion,
+
+ // Hook for JSX spread, don't use this for anything else.
+ __spread: assign
+};
+
+module.exports = React;
+},{"135":135,"24":24,"32":32,"33":33,"34":34,"43":43,"57":57,"58":58,"82":82,"97":97}],70:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactLink
+ * @typechecks static-only
+ */
+
+'use strict';
+
+/**
+ * ReactLink encapsulates a common pattern in which a component wants to modify
+ * a prop received from its parent. ReactLink allows the parent to pass down a
+ * value coupled with a callback that, when invoked, expresses an intent to
+ * modify that value. For example:
+ *
+ * React.createClass({
+ * getInitialState: function() {
+ * return {value: ''};
+ * },
+ * render: function() {
+ * var valueLink = new ReactLink(this.state.value, this._handleValueChange);
+ * return <input valueLink={valueLink} />;
+ * },
+ * _handleValueChange: function(newValue) {
+ * this.setState({value: newValue});
+ * }
+ * });
+ *
+ * We have provided some sugary mixins to make the creation and
+ * consumption of ReactLink easier; see LinkedValueUtils and LinkedStateMixin.
+ */
+
+var React = _dereq_(26);
+
+/**
+ * @param {*} value current value of the link
+ * @param {function} requestChange callback to request a change
+ */
+function ReactLink(value, requestChange) {
+ this.value = value;
+ this.requestChange = requestChange;
+}
+
+/**
+ * Creates a PropType that enforces the ReactLink API and optionally checks the
+ * type of the value being passed inside the link. Example:
+ *
+ * MyComponent.propTypes = {
+ * tabIndexLink: ReactLink.PropTypes.link(React.PropTypes.number)
+ * }
+ */
+function createLinkTypeChecker(linkType) {
+ var shapes = {
+ value: typeof linkType === 'undefined' ? React.PropTypes.any.isRequired : linkType.isRequired,
+ requestChange: React.PropTypes.func.isRequired
+ };
+ return React.PropTypes.shape(shapes);
+}
+
+ReactLink.PropTypes = {
+ link: createLinkTypeChecker
+};
+
+module.exports = ReactLink;
+},{"26":26}],71:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactMarkupChecksum
+ */
+
+'use strict';
+
+var adler32 = _dereq_(116);
+
+var TAG_END = /\/?>/;
+
+var ReactMarkupChecksum = {
+ CHECKSUM_ATTR_NAME: 'data-react-checksum',
+
+ /**
+ * @param {string} markup Markup string
+ * @return {string} Markup string with checksum attribute attached
+ */
+ addChecksumToMarkup: function (markup) {
+ var checksum = adler32(markup);
+
+ // Add checksum (handle both parent tags and self-closing tags)
+ return markup.replace(TAG_END, ' ' + ReactMarkupChecksum.CHECKSUM_ATTR_NAME + '="' + checksum + '"$&');
+ },
+
+ /**
+ * @param {string} markup to use
+ * @param {DOMElement} element root React element
+ * @returns {boolean} whether or not the markup is the same
+ */
+ canReuseMarkup: function (markup, element) {
+ var existingChecksum = element.getAttribute(ReactMarkupChecksum.CHECKSUM_ATTR_NAME);
+ existingChecksum = existingChecksum && parseInt(existingChecksum, 10);
+ var markupChecksum = adler32(markup);
+ return markupChecksum === existingChecksum;
+ }
+};
+
+module.exports = ReactMarkupChecksum;
+},{"116":116}],72:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactMount
+ */
+
+'use strict';
+
+var DOMProperty = _dereq_(10);
+var ReactBrowserEventEmitter = _dereq_(28);
+var ReactCurrentOwner = _dereq_(39);
+var ReactDOMFeatureFlags = _dereq_(44);
+var ReactElement = _dereq_(57);
+var ReactEmptyComponentRegistry = _dereq_(60);
+var ReactInstanceHandles = _dereq_(67);
+var ReactInstanceMap = _dereq_(68);
+var ReactMarkupChecksum = _dereq_(71);
+var ReactPerf = _dereq_(78);
+var ReactReconciler = _dereq_(84);
+var ReactUpdateQueue = _dereq_(95);
+var ReactUpdates = _dereq_(96);
+
+var assign = _dereq_(24);
+var emptyObject = _dereq_(154);
+var containsNode = _dereq_(150);
+var instantiateReactComponent = _dereq_(132);
+var invariant = _dereq_(161);
+var setInnerHTML = _dereq_(138);
+var shouldUpdateReactComponent = _dereq_(141);
+var validateDOMNesting = _dereq_(144);
+var warning = _dereq_(173);
+
+var ATTR_NAME = DOMProperty.ID_ATTRIBUTE_NAME;
+var nodeCache = {};
+
+var ELEMENT_NODE_TYPE = 1;
+var DOC_NODE_TYPE = 9;
+var DOCUMENT_FRAGMENT_NODE_TYPE = 11;
+
+var ownerDocumentContextKey = '__ReactMount_ownerDocument$' + Math.random().toString(36).slice(2);
+
+/** Mapping from reactRootID to React component instance. */
+var instancesByReactRootID = {};
+
+/** Mapping from reactRootID to `container` nodes. */
+var containersByReactRootID = {};
+
+if ("development" !== 'production') {
+ /** __DEV__-only mapping from reactRootID to root elements. */
+ var rootElementsByReactRootID = {};
+}
+
+// Used to store breadth-first search state in findComponentRoot.
+var findComponentRootReusableArray = [];
+
+/**
+ * Finds the index of the first character
+ * that's not common between the two given strings.
+ *
+ * @return {number} the index of the character where the strings diverge
+ */
+function firstDifferenceIndex(string1, string2) {
+ var minLen = Math.min(string1.length, string2.length);
+ for (var i = 0; i < minLen; i++) {
+ if (string1.charAt(i) !== string2.charAt(i)) {
+ return i;
+ }
+ }
+ return string1.length === string2.length ? -1 : minLen;
+}
+
+/**
+ * @param {DOMElement|DOMDocument} container DOM element that may contain
+ * a React component
+ * @return {?*} DOM element that may have the reactRoot ID, or null.
+ */
+function getReactRootElementInContainer(container) {
+ if (!container) {
+ return null;
+ }
+
+ if (container.nodeType === DOC_NODE_TYPE) {
+ return container.documentElement;
+ } else {
+ return container.firstChild;
+ }
+}
+
+/**
+ * @param {DOMElement} container DOM element that may contain a React component.
+ * @return {?string} A "reactRoot" ID, if a React component is rendered.
+ */
+function getReactRootID(container) {
+ var rootElement = getReactRootElementInContainer(container);
+ return rootElement && ReactMount.getID(rootElement);
+}
+
+/**
+ * Accessing node[ATTR_NAME] or calling getAttribute(ATTR_NAME) on a form
+ * element can return its control whose name or ID equals ATTR_NAME. All
+ * DOM nodes support `getAttributeNode` but this can also get called on
+ * other objects so just return '' if we're given something other than a
+ * DOM node (such as window).
+ *
+ * @param {?DOMElement|DOMWindow|DOMDocument|DOMTextNode} node DOM node.
+ * @return {string} ID of the supplied `domNode`.
+ */
+function getID(node) {
+ var id = internalGetID(node);
+ if (id) {
+ if (nodeCache.hasOwnProperty(id)) {
+ var cached = nodeCache[id];
+ if (cached !== node) {
+ !!isValid(cached, id) ? "development" !== 'production' ? invariant(false, 'ReactMount: Two valid but unequal nodes with the same `%s`: %s', ATTR_NAME, id) : invariant(false) : undefined;
+
+ nodeCache[id] = node;
+ }
+ } else {
+ nodeCache[id] = node;
+ }
+ }
+
+ return id;
+}
+
+function internalGetID(node) {
+ // If node is something like a window, document, or text node, none of
+ // which support attributes or a .getAttribute method, gracefully return
+ // the empty string, as if the attribute were missing.
+ return node && node.getAttribute && node.getAttribute(ATTR_NAME) || '';
+}
+
+/**
+ * Sets the React-specific ID of the given node.
+ *
+ * @param {DOMElement} node The DOM node whose ID will be set.
+ * @param {string} id The value of the ID attribute.
+ */
+function setID(node, id) {
+ var oldID = internalGetID(node);
+ if (oldID !== id) {
+ delete nodeCache[oldID];
+ }
+ node.setAttribute(ATTR_NAME, id);
+ nodeCache[id] = node;
+}
+
+/**
+ * Finds the node with the supplied React-generated DOM ID.
+ *
+ * @param {string} id A React-generated DOM ID.
+ * @return {DOMElement} DOM node with the suppled `id`.
+ * @internal
+ */
+function getNode(id) {
+ if (!nodeCache.hasOwnProperty(id) || !isValid(nodeCache[id], id)) {
+ nodeCache[id] = ReactMount.findReactNodeByID(id);
+ }
+ return nodeCache[id];
+}
+
+/**
+ * Finds the node with the supplied public React instance.
+ *
+ * @param {*} instance A public React instance.
+ * @return {?DOMElement} DOM node with the suppled `id`.
+ * @internal
+ */
+function getNodeFromInstance(instance) {
+ var id = ReactInstanceMap.get(instance)._rootNodeID;
+ if (ReactEmptyComponentRegistry.isNullComponentID(id)) {
+ return null;
+ }
+ if (!nodeCache.hasOwnProperty(id) || !isValid(nodeCache[id], id)) {
+ nodeCache[id] = ReactMount.findReactNodeByID(id);
+ }
+ return nodeCache[id];
+}
+
+/**
+ * A node is "valid" if it is contained by a currently mounted container.
+ *
+ * This means that the node does not have to be contained by a document in
+ * order to be considered valid.
+ *
+ * @param {?DOMElement} node The candidate DOM node.
+ * @param {string} id The expected ID of the node.
+ * @return {boolean} Whether the node is contained by a mounted container.
+ */
+function isValid(node, id) {
+ if (node) {
+ !(internalGetID(node) === id) ? "development" !== 'production' ? invariant(false, 'ReactMount: Unexpected modification of `%s`', ATTR_NAME) : invariant(false) : undefined;
+
+ var container = ReactMount.findReactContainerForID(id);
+ if (container && containsNode(container, node)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/**
+ * Causes the cache to forget about one React-specific ID.
+ *
+ * @param {string} id The ID to forget.
+ */
+function purgeID(id) {
+ delete nodeCache[id];
+}
+
+var deepestNodeSoFar = null;
+function findDeepestCachedAncestorImpl(ancestorID) {
+ var ancestor = nodeCache[ancestorID];
+ if (ancestor && isValid(ancestor, ancestorID)) {
+ deepestNodeSoFar = ancestor;
+ } else {
+ // This node isn't populated in the cache, so presumably none of its
+ // descendants are. Break out of the loop.
+ return false;
+ }
+}
+
+/**
+ * Return the deepest cached node whose ID is a prefix of `targetID`.
+ */
+function findDeepestCachedAncestor(targetID) {
+ deepestNodeSoFar = null;
+ ReactInstanceHandles.traverseAncestors(targetID, findDeepestCachedAncestorImpl);
+
+ var foundNode = deepestNodeSoFar;
+ deepestNodeSoFar = null;
+ return foundNode;
+}
+
+/**
+ * Mounts this component and inserts it into the DOM.
+ *
+ * @param {ReactComponent} componentInstance The instance to mount.
+ * @param {string} rootID DOM ID of the root node.
+ * @param {DOMElement} container DOM element to mount into.
+ * @param {ReactReconcileTransaction} transaction
+ * @param {boolean} shouldReuseMarkup If true, do not insert markup
+ */
+function mountComponentIntoNode(componentInstance, rootID, container, transaction, shouldReuseMarkup, context) {
+ if (ReactDOMFeatureFlags.useCreateElement) {
+ context = assign({}, context);
+ if (container.nodeType === DOC_NODE_TYPE) {
+ context[ownerDocumentContextKey] = container;
+ } else {
+ context[ownerDocumentContextKey] = container.ownerDocument;
+ }
+ }
+ if ("development" !== 'production') {
+ if (context === emptyObject) {
+ context = {};
+ }
+ var tag = container.nodeName.toLowerCase();
+ context[validateDOMNesting.ancestorInfoContextKey] = validateDOMNesting.updatedAncestorInfo(null, tag, null);
+ }
+ var markup = ReactReconciler.mountComponent(componentInstance, rootID, transaction, context);
+ componentInstance._renderedComponent._topLevelWrapper = componentInstance;
+ ReactMount._mountImageIntoNode(markup, container, shouldReuseMarkup, transaction);
+}
+
+/**
+ * Batched mount.
+ *
+ * @param {ReactComponent} componentInstance The instance to mount.
+ * @param {string} rootID DOM ID of the root node.
+ * @param {DOMElement} container DOM element to mount into.
+ * @param {boolean} shouldReuseMarkup If true, do not insert markup
+ */
+function batchedMountComponentIntoNode(componentInstance, rootID, container, shouldReuseMarkup, context) {
+ var transaction = ReactUpdates.ReactReconcileTransaction.getPooled(
+ /* forceHTML */shouldReuseMarkup);
+ transaction.perform(mountComponentIntoNode, null, componentInstance, rootID, container, transaction, shouldReuseMarkup, context);
+ ReactUpdates.ReactReconcileTransaction.release(transaction);
+}
+
+/**
+ * Unmounts a component and removes it from the DOM.
+ *
+ * @param {ReactComponent} instance React component instance.
+ * @param {DOMElement} container DOM element to unmount from.
+ * @final
+ * @internal
+ * @see {ReactMount.unmountComponentAtNode}
+ */
+function unmountComponentFromNode(instance, container) {
+ ReactReconciler.unmountComponent(instance);
+
+ if (container.nodeType === DOC_NODE_TYPE) {
+ container = container.documentElement;
+ }
+
+ // http://jsperf.com/emptying-a-node
+ while (container.lastChild) {
+ container.removeChild(container.lastChild);
+ }
+}
+
+/**
+ * True if the supplied DOM node has a direct React-rendered child that is
+ * not a React root element. Useful for warning in `render`,
+ * `unmountComponentAtNode`, etc.
+ *
+ * @param {?DOMElement} node The candidate DOM node.
+ * @return {boolean} True if the DOM element contains a direct child that was
+ * rendered by React but is not a root element.
+ * @internal
+ */
+function hasNonRootReactChild(node) {
+ var reactRootID = getReactRootID(node);
+ return reactRootID ? reactRootID !== ReactInstanceHandles.getReactRootIDFromNodeID(reactRootID) : false;
+}
+
+/**
+ * Returns the first (deepest) ancestor of a node which is rendered by this copy
+ * of React.
+ */
+function findFirstReactDOMImpl(node) {
+ // This node might be from another React instance, so we make sure not to
+ // examine the node cache here
+ for (; node && node.parentNode !== node; node = node.parentNode) {
+ if (node.nodeType !== 1) {
+ // Not a DOMElement, therefore not a React component
+ continue;
+ }
+ var nodeID = internalGetID(node);
+ if (!nodeID) {
+ continue;
+ }
+ var reactRootID = ReactInstanceHandles.getReactRootIDFromNodeID(nodeID);
+
+ // If containersByReactRootID contains the container we find by crawling up
+ // the tree, we know that this instance of React rendered the node.
+ // nb. isValid's strategy (with containsNode) does not work because render
+ // trees may be nested and we don't want a false positive in that case.
+ var current = node;
+ var lastID;
+ do {
+ lastID = internalGetID(current);
+ current = current.parentNode;
+ if (current == null) {
+ // The passed-in node has been detached from the container it was
+ // originally rendered into.
+ return null;
+ }
+ } while (lastID !== reactRootID);
+
+ if (current === containersByReactRootID[reactRootID]) {
+ return node;
+ }
+ }
+ return null;
+}
+
+/**
+ * Temporary (?) hack so that we can store all top-level pending updates on
+ * composites instead of having to worry about different types of components
+ * here.
+ */
+var TopLevelWrapper = function () {};
+TopLevelWrapper.prototype.isReactComponent = {};
+if ("development" !== 'production') {
+ TopLevelWrapper.displayName = 'TopLevelWrapper';
+}
+TopLevelWrapper.prototype.render = function () {
+ // this.props is actually a ReactElement
+ return this.props;
+};
+
+/**
+ * Mounting is the process of initializing a React component by creating its
+ * representative DOM elements and inserting them into a supplied `container`.
+ * Any prior content inside `container` is destroyed in the process.
+ *
+ * ReactMount.render(
+ * component,
+ * document.getElementById('container')
+ * );
+ *
+ * <div id="container"> <-- Supplied `container`.
+ * <div data-reactid=".3"> <-- Rendered reactRoot of React
+ * // ... component.
+ * </div>
+ * </div>
+ *
+ * Inside of `container`, the first element rendered is the "reactRoot".
+ */
+var ReactMount = {
+
+ TopLevelWrapper: TopLevelWrapper,
+
+ /** Exposed for debugging purposes **/
+ _instancesByReactRootID: instancesByReactRootID,
+
+ /**
+ * This is a hook provided to support rendering React components while
+ * ensuring that the apparent scroll position of its `container` does not
+ * change.
+ *
+ * @param {DOMElement} container The `container` being rendered into.
+ * @param {function} renderCallback This must be called once to do the render.
+ */
+ scrollMonitor: function (container, renderCallback) {
+ renderCallback();
+ },
+
+ /**
+ * Take a component that's already mounted into the DOM and replace its props
+ * @param {ReactComponent} prevComponent component instance already in the DOM
+ * @param {ReactElement} nextElement component instance to render
+ * @param {DOMElement} container container to render into
+ * @param {?function} callback function triggered on completion
+ */
+ _updateRootComponent: function (prevComponent, nextElement, container, callback) {
+ ReactMount.scrollMonitor(container, function () {
+ ReactUpdateQueue.enqueueElementInternal(prevComponent, nextElement);
+ if (callback) {
+ ReactUpdateQueue.enqueueCallbackInternal(prevComponent, callback);
+ }
+ });
+
+ if ("development" !== 'production') {
+ // Record the root element in case it later gets transplanted.
+ rootElementsByReactRootID[getReactRootID(container)] = getReactRootElementInContainer(container);
+ }
+
+ return prevComponent;
+ },
+
+ /**
+ * Register a component into the instance map and starts scroll value
+ * monitoring
+ * @param {ReactComponent} nextComponent component instance to render
+ * @param {DOMElement} container container to render into
+ * @return {string} reactRoot ID prefix
+ */
+ _registerComponent: function (nextComponent, container) {
+ !(container && (container.nodeType === ELEMENT_NODE_TYPE || container.nodeType === DOC_NODE_TYPE || container.nodeType === DOCUMENT_FRAGMENT_NODE_TYPE)) ? "development" !== 'production' ? invariant(false, '_registerComponent(...): Target container is not a DOM element.') : invariant(false) : undefined;
+
+ ReactBrowserEventEmitter.ensureScrollValueMonitoring();
+
+ var reactRootID = ReactMount.registerContainer(container);
+ instancesByReactRootID[reactRootID] = nextComponent;
+ return reactRootID;
+ },
+
+ /**
+ * Render a new component into the DOM.
+ * @param {ReactElement} nextElement element to render
+ * @param {DOMElement} container container to render into
+ * @param {boolean} shouldReuseMarkup if we should skip the markup insertion
+ * @return {ReactComponent} nextComponent
+ */
+ _renderNewRootComponent: function (nextElement, container, shouldReuseMarkup, context) {
+ // Various parts of our code (such as ReactCompositeComponent's
+ // _renderValidatedComponent) assume that calls to render aren't nested;
+ // verify that that's the case.
+ "development" !== 'production' ? warning(ReactCurrentOwner.current == null, '_renderNewRootComponent(): Render methods should be a pure function ' + 'of props and state; triggering nested component updates from ' + 'render is not allowed. If necessary, trigger nested updates in ' + 'componentDidUpdate. Check the render method of %s.', ReactCurrentOwner.current && ReactCurrentOwner.current.getName() || 'ReactCompositeComponent') : undefined;
+
+ var componentInstance = instantiateReactComponent(nextElement, null);
+ var reactRootID = ReactMount._registerComponent(componentInstance, container);
+
+ // The initial render is synchronous but any updates that happen during
+ // rendering, in componentWillMount or componentDidMount, will be batched
+ // according to the current batching strategy.
+
+ ReactUpdates.batchedUpdates(batchedMountComponentIntoNode, componentInstance, reactRootID, container, shouldReuseMarkup, context);
+
+ if ("development" !== 'production') {
+ // Record the root element in case it later gets transplanted.
+ rootElementsByReactRootID[reactRootID] = getReactRootElementInContainer(container);
+ }
+
+ return componentInstance;
+ },
+
+ /**
+ * Renders a React component into the DOM in the supplied `container`.
+ *
+ * If the React component was previously rendered into `container`, this will
+ * perform an update on it and only mutate the DOM as necessary to reflect the
+ * latest React component.
+ *
+ * @param {ReactComponent} parentComponent The conceptual parent of this render tree.
+ * @param {ReactElement} nextElement Component element to render.
+ * @param {DOMElement} container DOM element to render into.
+ * @param {?function} callback function triggered on completion
+ * @return {ReactComponent} Component instance rendered in `container`.
+ */
+ renderSubtreeIntoContainer: function (parentComponent, nextElement, container, callback) {
+ !(parentComponent != null && parentComponent._reactInternalInstance != null) ? "development" !== 'production' ? invariant(false, 'parentComponent must be a valid React Component') : invariant(false) : undefined;
+ return ReactMount._renderSubtreeIntoContainer(parentComponent, nextElement, container, callback);
+ },
+
+ _renderSubtreeIntoContainer: function (parentComponent, nextElement, container, callback) {
+ !ReactElement.isValidElement(nextElement) ? "development" !== 'production' ? invariant(false, 'ReactDOM.render(): Invalid component element.%s', typeof nextElement === 'string' ? ' Instead of passing an element string, make sure to instantiate ' + 'it by passing it to React.createElement.' : typeof nextElement === 'function' ? ' Instead of passing a component class, make sure to instantiate ' + 'it by passing it to React.createElement.' :
+ // Check if it quacks like an element
+ nextElement != null && nextElement.props !== undefined ? ' This may be caused by unintentionally loading two independent ' + 'copies of React.' : '') : invariant(false) : undefined;
+
+ "development" !== 'production' ? warning(!container || !container.tagName || container.tagName.toUpperCase() !== 'BODY', 'render(): Rendering components directly into document.body is ' + 'discouraged, since its children are often manipulated by third-party ' + 'scripts and browser extensions. This may lead to subtle ' + 'reconciliation issues. Try rendering into a container element created ' + 'for your app.') : undefined;
+
+ var nextWrappedElement = new ReactElement(TopLevelWrapper, null, null, null, null, null, nextElement);
+
+ var prevComponent = instancesByReactRootID[getReactRootID(container)];
+
+ if (prevComponent) {
+ var prevWrappedElement = prevComponent._currentElement;
+ var prevElement = prevWrappedElement.props;
+ if (shouldUpdateReactComponent(prevElement, nextElement)) {
+ var publicInst = prevComponent._renderedComponent.getPublicInstance();
+ var updatedCallback = callback && function () {
+ callback.call(publicInst);
+ };
+ ReactMount._updateRootComponent(prevComponent, nextWrappedElement, container, updatedCallback);
+ return publicInst;
+ } else {
+ ReactMount.unmountComponentAtNode(container);
+ }
+ }
+
+ var reactRootElement = getReactRootElementInContainer(container);
+ var containerHasReactMarkup = reactRootElement && !!internalGetID(reactRootElement);
+ var containerHasNonRootReactChild = hasNonRootReactChild(container);
+
+ if ("development" !== 'production') {
+ "development" !== 'production' ? warning(!containerHasNonRootReactChild, 'render(...): Replacing React-rendered children with a new root ' + 'component. If you intended to update the children of this node, ' + 'you should instead have the existing children update their state ' + 'and render the new components instead of calling ReactDOM.render.') : undefined;
+
+ if (!containerHasReactMarkup || reactRootElement.nextSibling) {
+ var rootElementSibling = reactRootElement;
+ while (rootElementSibling) {
+ if (internalGetID(rootElementSibling)) {
+ "development" !== 'production' ? warning(false, 'render(): Target node has markup rendered by React, but there ' + 'are unrelated nodes as well. This is most commonly caused by ' + 'white-space inserted around server-rendered markup.') : undefined;
+ break;
+ }
+ rootElementSibling = rootElementSibling.nextSibling;
+ }
+ }
+ }
+
+ var shouldReuseMarkup = containerHasReactMarkup && !prevComponent && !containerHasNonRootReactChild;
+ var component = ReactMount._renderNewRootComponent(nextWrappedElement, container, shouldReuseMarkup, parentComponent != null ? parentComponent._reactInternalInstance._processChildContext(parentComponent._reactInternalInstance._context) : emptyObject)._renderedComponent.getPublicInstance();
+ if (callback) {
+ callback.call(component);
+ }
+ return component;
+ },
+
+ /**
+ * Renders a React component into the DOM in the supplied `container`.
+ *
+ * If the React component was previously rendered into `container`, this will
+ * perform an update on it and only mutate the DOM as necessary to reflect the
+ * latest React component.
+ *
+ * @param {ReactElement} nextElement Component element to render.
+ * @param {DOMElement} container DOM element to render into.
+ * @param {?function} callback function triggered on completion
+ * @return {ReactComponent} Component instance rendered in `container`.
+ */
+ render: function (nextElement, container, callback) {
+ return ReactMount._renderSubtreeIntoContainer(null, nextElement, container, callback);
+ },
+
+ /**
+ * Registers a container node into which React components will be rendered.
+ * This also creates the "reactRoot" ID that will be assigned to the element
+ * rendered within.
+ *
+ * @param {DOMElement} container DOM element to register as a container.
+ * @return {string} The "reactRoot" ID of elements rendered within.
+ */
+ registerContainer: function (container) {
+ var reactRootID = getReactRootID(container);
+ if (reactRootID) {
+ // If one exists, make sure it is a valid "reactRoot" ID.
+ reactRootID = ReactInstanceHandles.getReactRootIDFromNodeID(reactRootID);
+ }
+ if (!reactRootID) {
+ // No valid "reactRoot" ID found, create one.
+ reactRootID = ReactInstanceHandles.createReactRootID();
+ }
+ containersByReactRootID[reactRootID] = container;
+ return reactRootID;
+ },
+
+ /**
+ * Unmounts and destroys the React component rendered in the `container`.
+ *
+ * @param {DOMElement} container DOM element containing a React component.
+ * @return {boolean} True if a component was found in and unmounted from
+ * `container`
+ */
+ unmountComponentAtNode: function (container) {
+ // Various parts of our code (such as ReactCompositeComponent's
+ // _renderValidatedComponent) assume that calls to render aren't nested;
+ // verify that that's the case. (Strictly speaking, unmounting won't cause a
+ // render but we still don't expect to be in a render call here.)
+ "development" !== 'production' ? warning(ReactCurrentOwner.current == null, 'unmountComponentAtNode(): Render methods should be a pure function ' + 'of props and state; triggering nested component updates from render ' + 'is not allowed. If necessary, trigger nested updates in ' + 'componentDidUpdate. Check the render method of %s.', ReactCurrentOwner.current && ReactCurrentOwner.current.getName() || 'ReactCompositeComponent') : undefined;
+
+ !(container && (container.nodeType === ELEMENT_NODE_TYPE || container.nodeType === DOC_NODE_TYPE || container.nodeType === DOCUMENT_FRAGMENT_NODE_TYPE)) ? "development" !== 'production' ? invariant(false, 'unmountComponentAtNode(...): Target container is not a DOM element.') : invariant(false) : undefined;
+
+ var reactRootID = getReactRootID(container);
+ var component = instancesByReactRootID[reactRootID];
+ if (!component) {
+ // Check if the node being unmounted was rendered by React, but isn't a
+ // root node.
+ var containerHasNonRootReactChild = hasNonRootReactChild(container);
+
+ // Check if the container itself is a React root node.
+ var containerID = internalGetID(container);
+ var isContainerReactRoot = containerID && containerID === ReactInstanceHandles.getReactRootIDFromNodeID(containerID);
+
+ if ("development" !== 'production') {
+ "development" !== 'production' ? warning(!containerHasNonRootReactChild, 'unmountComponentAtNode(): The node you\'re attempting to unmount ' + 'was rendered by React and is not a top-level container. %s', isContainerReactRoot ? 'You may have accidentally passed in a React root node instead ' + 'of its container.' : 'Instead, have the parent component update its state and ' + 'rerender in order to remove this component.') : undefined;
+ }
+
+ return false;
+ }
+ ReactUpdates.batchedUpdates(unmountComponentFromNode, component, container);
+ delete instancesByReactRootID[reactRootID];
+ delete containersByReactRootID[reactRootID];
+ if ("development" !== 'production') {
+ delete rootElementsByReactRootID[reactRootID];
+ }
+ return true;
+ },
+
+ /**
+ * Finds the container DOM element that contains React component to which the
+ * supplied DOM `id` belongs.
+ *
+ * @param {string} id The ID of an element rendered by a React component.
+ * @return {?DOMElement} DOM element that contains the `id`.
+ */
+ findReactContainerForID: function (id) {
+ var reactRootID = ReactInstanceHandles.getReactRootIDFromNodeID(id);
+ var container = containersByReactRootID[reactRootID];
+
+ if ("development" !== 'production') {
+ var rootElement = rootElementsByReactRootID[reactRootID];
+ if (rootElement && rootElement.parentNode !== container) {
+ "development" !== 'production' ? warning(
+ // Call internalGetID here because getID calls isValid which calls
+ // findReactContainerForID (this function).
+ internalGetID(rootElement) === reactRootID, 'ReactMount: Root element ID differed from reactRootID.') : undefined;
+ var containerChild = container.firstChild;
+ if (containerChild && reactRootID === internalGetID(containerChild)) {
+ // If the container has a new child with the same ID as the old
+ // root element, then rootElementsByReactRootID[reactRootID] is
+ // just stale and needs to be updated. The case that deserves a
+ // warning is when the container is empty.
+ rootElementsByReactRootID[reactRootID] = containerChild;
+ } else {
+ "development" !== 'production' ? warning(false, 'ReactMount: Root element has been removed from its original ' + 'container. New container: %s', rootElement.parentNode) : undefined;
+ }
+ }
+ }
+
+ return container;
+ },
+
+ /**
+ * Finds an element rendered by React with the supplied ID.
+ *
+ * @param {string} id ID of a DOM node in the React component.
+ * @return {DOMElement} Root DOM node of the React component.
+ */
+ findReactNodeByID: function (id) {
+ var reactRoot = ReactMount.findReactContainerForID(id);
+ return ReactMount.findComponentRoot(reactRoot, id);
+ },
+
+ /**
+ * Traverses up the ancestors of the supplied node to find a node that is a
+ * DOM representation of a React component rendered by this copy of React.
+ *
+ * @param {*} node
+ * @return {?DOMEventTarget}
+ * @internal
+ */
+ getFirstReactDOM: function (node) {
+ return findFirstReactDOMImpl(node);
+ },
+
+ /**
+ * Finds a node with the supplied `targetID` inside of the supplied
+ * `ancestorNode`. Exploits the ID naming scheme to perform the search
+ * quickly.
+ *
+ * @param {DOMEventTarget} ancestorNode Search from this root.
+ * @pararm {string} targetID ID of the DOM representation of the component.
+ * @return {DOMEventTarget} DOM node with the supplied `targetID`.
+ * @internal
+ */
+ findComponentRoot: function (ancestorNode, targetID) {
+ var firstChildren = findComponentRootReusableArray;
+ var childIndex = 0;
+
+ var deepestAncestor = findDeepestCachedAncestor(targetID) || ancestorNode;
+
+ if ("development" !== 'production') {
+ // This will throw on the next line; give an early warning
+ "development" !== 'production' ? warning(deepestAncestor != null, 'React can\'t find the root component node for data-reactid value ' + '`%s`. If you\'re seeing this message, it probably means that ' + 'you\'ve loaded two copies of React on the page. At this time, only ' + 'a single copy of React can be loaded at a time.', targetID) : undefined;
+ }
+
+ firstChildren[0] = deepestAncestor.firstChild;
+ firstChildren.length = 1;
+
+ while (childIndex < firstChildren.length) {
+ var child = firstChildren[childIndex++];
+ var targetChild;
+
+ while (child) {
+ var childID = ReactMount.getID(child);
+ if (childID) {
+ // Even if we find the node we're looking for, we finish looping
+ // through its siblings to ensure they're cached so that we don't have
+ // to revisit this node again. Otherwise, we make n^2 calls to getID
+ // when visiting the many children of a single node in order.
+
+ if (targetID === childID) {
+ targetChild = child;
+ } else if (ReactInstanceHandles.isAncestorIDOf(childID, targetID)) {
+ // If we find a child whose ID is an ancestor of the given ID,
+ // then we can be sure that we only want to search the subtree
+ // rooted at this child, so we can throw out the rest of the
+ // search state.
+ firstChildren.length = childIndex = 0;
+ firstChildren.push(child.firstChild);
+ }
+ } else {
+ // If this child had no ID, then there's a chance that it was
+ // injected automatically by the browser, as when a `<table>`
+ // element sprouts an extra `<tbody>` child as a side effect of
+ // `.innerHTML` parsing. Optimistically continue down this
+ // branch, but not before examining the other siblings.
+ firstChildren.push(child.firstChild);
+ }
+
+ child = child.nextSibling;
+ }
+
+ if (targetChild) {
+ // Emptying firstChildren/findComponentRootReusableArray is
+ // not necessary for correctness, but it helps the GC reclaim
+ // any nodes that were left at the end of the search.
+ firstChildren.length = 0;
+
+ return targetChild;
+ }
+ }
+
+ firstChildren.length = 0;
+
+ !false ? "development" !== 'production' ? invariant(false, 'findComponentRoot(..., %s): Unable to find element. This probably ' + 'means the DOM was unexpectedly mutated (e.g., by the browser), ' + 'usually due to forgetting a <tbody> when using tables, nesting tags ' + 'like <form>, <p>, or <a>, or using non-SVG elements in an <svg> ' + 'parent. ' + 'Try inspecting the child nodes of the element with React ID `%s`.', targetID, ReactMount.getID(ancestorNode)) : invariant(false) : undefined;
+ },
+
+ _mountImageIntoNode: function (markup, container, shouldReuseMarkup, transaction) {
+ !(container && (container.nodeType === ELEMENT_NODE_TYPE || container.nodeType === DOC_NODE_TYPE || container.nodeType === DOCUMENT_FRAGMENT_NODE_TYPE)) ? "development" !== 'production' ? invariant(false, 'mountComponentIntoNode(...): Target container is not valid.') : invariant(false) : undefined;
+
+ if (shouldReuseMarkup) {
+ var rootElement = getReactRootElementInContainer(container);
+ if (ReactMarkupChecksum.canReuseMarkup(markup, rootElement)) {
+ return;
+ } else {
+ var checksum = rootElement.getAttribute(ReactMarkupChecksum.CHECKSUM_ATTR_NAME);
+ rootElement.removeAttribute(ReactMarkupChecksum.CHECKSUM_ATTR_NAME);
+
+ var rootMarkup = rootElement.outerHTML;
+ rootElement.setAttribute(ReactMarkupChecksum.CHECKSUM_ATTR_NAME, checksum);
+
+ var normalizedMarkup = markup;
+ if ("development" !== 'production') {
+ // because rootMarkup is retrieved from the DOM, various normalizations
+ // will have occurred which will not be present in `markup`. Here,
+ // insert markup into a <div> or <iframe> depending on the container
+ // type to perform the same normalizations before comparing.
+ var normalizer;
+ if (container.nodeType === ELEMENT_NODE_TYPE) {
+ normalizer = document.createElementNS('http://www.w3.org/1999/xhtml', 'div');
+ normalizer.innerHTML = markup;
+ normalizedMarkup = normalizer.innerHTML;
+ } else {
+ normalizer = document.createElementNS('http://www.w3.org/1999/xhtml', 'iframe');
+ document.body.appendChild(normalizer);
+ normalizer.contentDocument.write(markup);
+ normalizedMarkup = normalizer.contentDocument.documentElement.outerHTML;
+ document.body.removeChild(normalizer);
+ }
+ }
+
+ var diffIndex = firstDifferenceIndex(normalizedMarkup, rootMarkup);
+ var difference = ' (client) ' + normalizedMarkup.substring(diffIndex - 20, diffIndex + 20) + '\n (server) ' + rootMarkup.substring(diffIndex - 20, diffIndex + 20);
+
+ !(container.nodeType !== DOC_NODE_TYPE) ? "development" !== 'production' ? invariant(false, 'You\'re trying to render a component to the document using ' + 'server rendering but the checksum was invalid. This usually ' + 'means you rendered a different component type or props on ' + 'the client from the one on the server, or your render() ' + 'methods are impure. React cannot handle this case due to ' + 'cross-browser quirks by rendering at the document root. You ' + 'should look for environment dependent code in your components ' + 'and ensure the props are the same client and server side:\n%s', difference) : invariant(false) : undefined;
+
+ if ("development" !== 'production') {
+ "development" !== 'production' ? warning(false, 'React attempted to reuse markup in a container but the ' + 'checksum was invalid. This generally means that you are ' + 'using server rendering and the markup generated on the ' + 'server was not what the client was expecting. React injected ' + 'new markup to compensate which works but you have lost many ' + 'of the benefits of server rendering. Instead, figure out ' + 'why the markup being generated is different on the client ' + 'or server:\n%s', difference) : undefined;
+ }
+ }
+ }
+
+ !(container.nodeType !== DOC_NODE_TYPE) ? "development" !== 'production' ? invariant(false, 'You\'re trying to render a component to the document but ' + 'you didn\'t use server rendering. We can\'t do this ' + 'without using server rendering due to cross-browser quirks. ' + 'See ReactDOMServer.renderToString() for server rendering.') : invariant(false) : undefined;
+
+ if (transaction.useCreateElement) {
+ while (container.lastChild) {
+ container.removeChild(container.lastChild);
+ }
+ container.appendChild(markup);
+ } else {
+ setInnerHTML(container, markup);
+ }
+ },
+
+ ownerDocumentContextKey: ownerDocumentContextKey,
+
+ /**
+ * React ID utilities.
+ */
+
+ getReactRootID: getReactRootID,
+
+ getID: getID,
+
+ setID: setID,
+
+ getNode: getNode,
+
+ getNodeFromInstance: getNodeFromInstance,
+
+ isValid: isValid,
+
+ purgeID: purgeID
+};
+
+ReactPerf.measureMethods(ReactMount, 'ReactMount', {
+ _renderNewRootComponent: '_renderNewRootComponent',
+ _mountImageIntoNode: '_mountImageIntoNode'
+});
+
+module.exports = ReactMount;
+},{"10":10,"132":132,"138":138,"141":141,"144":144,"150":150,"154":154,"161":161,"173":173,"24":24,"28":28,"39":39,"44":44,"57":57,"60":60,"67":67,"68":68,"71":71,"78":78,"84":84,"95":95,"96":96}],73:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactMultiChild
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var ReactComponentEnvironment = _dereq_(36);
+var ReactMultiChildUpdateTypes = _dereq_(74);
+
+var ReactCurrentOwner = _dereq_(39);
+var ReactReconciler = _dereq_(84);
+var ReactChildReconciler = _dereq_(31);
+
+var flattenChildren = _dereq_(123);
+
+/**
+ * Updating children of a component may trigger recursive updates. The depth is
+ * used to batch recursive updates to render markup more efficiently.
+ *
+ * @type {number}
+ * @private
+ */
+var updateDepth = 0;
+
+/**
+ * Queue of update configuration objects.
+ *
+ * Each object has a `type` property that is in `ReactMultiChildUpdateTypes`.
+ *
+ * @type {array<object>}
+ * @private
+ */
+var updateQueue = [];
+
+/**
+ * Queue of markup to be rendered.
+ *
+ * @type {array<string>}
+ * @private
+ */
+var markupQueue = [];
+
+/**
+ * Enqueues markup to be rendered and inserted at a supplied index.
+ *
+ * @param {string} parentID ID of the parent component.
+ * @param {string} markup Markup that renders into an element.
+ * @param {number} toIndex Destination index.
+ * @private
+ */
+function enqueueInsertMarkup(parentID, markup, toIndex) {
+ // NOTE: Null values reduce hidden classes.
+ updateQueue.push({
+ parentID: parentID,
+ parentNode: null,
+ type: ReactMultiChildUpdateTypes.INSERT_MARKUP,
+ markupIndex: markupQueue.push(markup) - 1,
+ content: null,
+ fromIndex: null,
+ toIndex: toIndex
+ });
+}
+
+/**
+ * Enqueues moving an existing element to another index.
+ *
+ * @param {string} parentID ID of the parent component.
+ * @param {number} fromIndex Source index of the existing element.
+ * @param {number} toIndex Destination index of the element.
+ * @private
+ */
+function enqueueMove(parentID, fromIndex, toIndex) {
+ // NOTE: Null values reduce hidden classes.
+ updateQueue.push({
+ parentID: parentID,
+ parentNode: null,
+ type: ReactMultiChildUpdateTypes.MOVE_EXISTING,
+ markupIndex: null,
+ content: null,
+ fromIndex: fromIndex,
+ toIndex: toIndex
+ });
+}
+
+/**
+ * Enqueues removing an element at an index.
+ *
+ * @param {string} parentID ID of the parent component.
+ * @param {number} fromIndex Index of the element to remove.
+ * @private
+ */
+function enqueueRemove(parentID, fromIndex) {
+ // NOTE: Null values reduce hidden classes.
+ updateQueue.push({
+ parentID: parentID,
+ parentNode: null,
+ type: ReactMultiChildUpdateTypes.REMOVE_NODE,
+ markupIndex: null,
+ content: null,
+ fromIndex: fromIndex,
+ toIndex: null
+ });
+}
+
+/**
+ * Enqueues setting the markup of a node.
+ *
+ * @param {string} parentID ID of the parent component.
+ * @param {string} markup Markup that renders into an element.
+ * @private
+ */
+function enqueueSetMarkup(parentID, markup) {
+ // NOTE: Null values reduce hidden classes.
+ updateQueue.push({
+ parentID: parentID,
+ parentNode: null,
+ type: ReactMultiChildUpdateTypes.SET_MARKUP,
+ markupIndex: null,
+ content: markup,
+ fromIndex: null,
+ toIndex: null
+ });
+}
+
+/**
+ * Enqueues setting the text content.
+ *
+ * @param {string} parentID ID of the parent component.
+ * @param {string} textContent Text content to set.
+ * @private
+ */
+function enqueueTextContent(parentID, textContent) {
+ // NOTE: Null values reduce hidden classes.
+ updateQueue.push({
+ parentID: parentID,
+ parentNode: null,
+ type: ReactMultiChildUpdateTypes.TEXT_CONTENT,
+ markupIndex: null,
+ content: textContent,
+ fromIndex: null,
+ toIndex: null
+ });
+}
+
+/**
+ * Processes any enqueued updates.
+ *
+ * @private
+ */
+function processQueue() {
+ if (updateQueue.length) {
+ ReactComponentEnvironment.processChildrenUpdates(updateQueue, markupQueue);
+ clearQueue();
+ }
+}
+
+/**
+ * Clears any enqueued updates.
+ *
+ * @private
+ */
+function clearQueue() {
+ updateQueue.length = 0;
+ markupQueue.length = 0;
+}
+
+/**
+ * ReactMultiChild are capable of reconciling multiple children.
+ *
+ * @class ReactMultiChild
+ * @internal
+ */
+var ReactMultiChild = {
+
+ /**
+ * Provides common functionality for components that must reconcile multiple
+ * children. This is used by `ReactDOMComponent` to mount, update, and
+ * unmount child components.
+ *
+ * @lends {ReactMultiChild.prototype}
+ */
+ Mixin: {
+
+ _reconcilerInstantiateChildren: function (nestedChildren, transaction, context) {
+ if ("development" !== 'production') {
+ if (this._currentElement) {
+ try {
+ ReactCurrentOwner.current = this._currentElement._owner;
+ return ReactChildReconciler.instantiateChildren(nestedChildren, transaction, context);
+ } finally {
+ ReactCurrentOwner.current = null;
+ }
+ }
+ }
+ return ReactChildReconciler.instantiateChildren(nestedChildren, transaction, context);
+ },
+
+ _reconcilerUpdateChildren: function (prevChildren, nextNestedChildrenElements, transaction, context) {
+ var nextChildren;
+ if ("development" !== 'production') {
+ if (this._currentElement) {
+ try {
+ ReactCurrentOwner.current = this._currentElement._owner;
+ nextChildren = flattenChildren(nextNestedChildrenElements);
+ } finally {
+ ReactCurrentOwner.current = null;
+ }
+ return ReactChildReconciler.updateChildren(prevChildren, nextChildren, transaction, context);
+ }
+ }
+ nextChildren = flattenChildren(nextNestedChildrenElements);
+ return ReactChildReconciler.updateChildren(prevChildren, nextChildren, transaction, context);
+ },
+
+ /**
+ * Generates a "mount image" for each of the supplied children. In the case
+ * of `ReactDOMComponent`, a mount image is a string of markup.
+ *
+ * @param {?object} nestedChildren Nested child maps.
+ * @return {array} An array of mounted representations.
+ * @internal
+ */
+ mountChildren: function (nestedChildren, transaction, context) {
+ var children = this._reconcilerInstantiateChildren(nestedChildren, transaction, context);
+ this._renderedChildren = children;
+ var mountImages = [];
+ var index = 0;
+ for (var name in children) {
+ if (children.hasOwnProperty(name)) {
+ var child = children[name];
+ // Inlined for performance, see `ReactInstanceHandles.createReactID`.
+ var rootID = this._rootNodeID + name;
+ var mountImage = ReactReconciler.mountComponent(child, rootID, transaction, context);
+ child._mountIndex = index++;
+ mountImages.push(mountImage);
+ }
+ }
+ return mountImages;
+ },
+
+ /**
+ * Replaces any rendered children with a text content string.
+ *
+ * @param {string} nextContent String of content.
+ * @internal
+ */
+ updateTextContent: function (nextContent) {
+ updateDepth++;
+ var errorThrown = true;
+ try {
+ var prevChildren = this._renderedChildren;
+ // Remove any rendered children.
+ ReactChildReconciler.unmountChildren(prevChildren);
+ // TODO: The setTextContent operation should be enough
+ for (var name in prevChildren) {
+ if (prevChildren.hasOwnProperty(name)) {
+ this._unmountChild(prevChildren[name]);
+ }
+ }
+ // Set new text content.
+ this.setTextContent(nextContent);
+ errorThrown = false;
+ } finally {
+ updateDepth--;
+ if (!updateDepth) {
+ if (errorThrown) {
+ clearQueue();
+ } else {
+ processQueue();
+ }
+ }
+ }
+ },
+
+ /**
+ * Replaces any rendered children with a markup string.
+ *
+ * @param {string} nextMarkup String of markup.
+ * @internal
+ */
+ updateMarkup: function (nextMarkup) {
+ updateDepth++;
+ var errorThrown = true;
+ try {
+ var prevChildren = this._renderedChildren;
+ // Remove any rendered children.
+ ReactChildReconciler.unmountChildren(prevChildren);
+ for (var name in prevChildren) {
+ if (prevChildren.hasOwnProperty(name)) {
+ this._unmountChildByName(prevChildren[name], name);
+ }
+ }
+ this.setMarkup(nextMarkup);
+ errorThrown = false;
+ } finally {
+ updateDepth--;
+ if (!updateDepth) {
+ if (errorThrown) {
+ clearQueue();
+ } else {
+ processQueue();
+ }
+ }
+ }
+ },
+
+ /**
+ * Updates the rendered children with new children.
+ *
+ * @param {?object} nextNestedChildrenElements Nested child element maps.
+ * @param {ReactReconcileTransaction} transaction
+ * @internal
+ */
+ updateChildren: function (nextNestedChildrenElements, transaction, context) {
+ updateDepth++;
+ var errorThrown = true;
+ try {
+ this._updateChildren(nextNestedChildrenElements, transaction, context);
+ errorThrown = false;
+ } finally {
+ updateDepth--;
+ if (!updateDepth) {
+ if (errorThrown) {
+ clearQueue();
+ } else {
+ processQueue();
+ }
+ }
+ }
+ },
+
+ /**
+ * Improve performance by isolating this hot code path from the try/catch
+ * block in `updateChildren`.
+ *
+ * @param {?object} nextNestedChildrenElements Nested child element maps.
+ * @param {ReactReconcileTransaction} transaction
+ * @final
+ * @protected
+ */
+ _updateChildren: function (nextNestedChildrenElements, transaction, context) {
+ var prevChildren = this._renderedChildren;
+ var nextChildren = this._reconcilerUpdateChildren(prevChildren, nextNestedChildrenElements, transaction, context);
+ this._renderedChildren = nextChildren;
+ if (!nextChildren && !prevChildren) {
+ return;
+ }
+ var name;
+ // `nextIndex` will increment for each child in `nextChildren`, but
+ // `lastIndex` will be the last index visited in `prevChildren`.
+ var lastIndex = 0;
+ var nextIndex = 0;
+ for (name in nextChildren) {
+ if (!nextChildren.hasOwnProperty(name)) {
+ continue;
+ }
+ var prevChild = prevChildren && prevChildren[name];
+ var nextChild = nextChildren[name];
+ if (prevChild === nextChild) {
+ this.moveChild(prevChild, nextIndex, lastIndex);
+ lastIndex = Math.max(prevChild._mountIndex, lastIndex);
+ prevChild._mountIndex = nextIndex;
+ } else {
+ if (prevChild) {
+ // Update `lastIndex` before `_mountIndex` gets unset by unmounting.
+ lastIndex = Math.max(prevChild._mountIndex, lastIndex);
+ this._unmountChild(prevChild);
+ }
+ // The child must be instantiated before it's mounted.
+ this._mountChildByNameAtIndex(nextChild, name, nextIndex, transaction, context);
+ }
+ nextIndex++;
+ }
+ // Remove children that are no longer present.
+ for (name in prevChildren) {
+ if (prevChildren.hasOwnProperty(name) && !(nextChildren && nextChildren.hasOwnProperty(name))) {
+ this._unmountChild(prevChildren[name]);
+ }
+ }
+ },
+
+ /**
+ * Unmounts all rendered children. This should be used to clean up children
+ * when this component is unmounted.
+ *
+ * @internal
+ */
+ unmountChildren: function () {
+ var renderedChildren = this._renderedChildren;
+ ReactChildReconciler.unmountChildren(renderedChildren);
+ this._renderedChildren = null;
+ },
+
+ /**
+ * Moves a child component to the supplied index.
+ *
+ * @param {ReactComponent} child Component to move.
+ * @param {number} toIndex Destination index of the element.
+ * @param {number} lastIndex Last index visited of the siblings of `child`.
+ * @protected
+ */
+ moveChild: function (child, toIndex, lastIndex) {
+ // If the index of `child` is less than `lastIndex`, then it needs to
+ // be moved. Otherwise, we do not need to move it because a child will be
+ // inserted or moved before `child`.
+ if (child._mountIndex < lastIndex) {
+ enqueueMove(this._rootNodeID, child._mountIndex, toIndex);
+ }
+ },
+
+ /**
+ * Creates a child component.
+ *
+ * @param {ReactComponent} child Component to create.
+ * @param {string} mountImage Markup to insert.
+ * @protected
+ */
+ createChild: function (child, mountImage) {
+ enqueueInsertMarkup(this._rootNodeID, mountImage, child._mountIndex);
+ },
+
+ /**
+ * Removes a child component.
+ *
+ * @param {ReactComponent} child Child to remove.
+ * @protected
+ */
+ removeChild: function (child) {
+ enqueueRemove(this._rootNodeID, child._mountIndex);
+ },
+
+ /**
+ * Sets this text content string.
+ *
+ * @param {string} textContent Text content to set.
+ * @protected
+ */
+ setTextContent: function (textContent) {
+ enqueueTextContent(this._rootNodeID, textContent);
+ },
+
+ /**
+ * Sets this markup string.
+ *
+ * @param {string} markup Markup to set.
+ * @protected
+ */
+ setMarkup: function (markup) {
+ enqueueSetMarkup(this._rootNodeID, markup);
+ },
+
+ /**
+ * Mounts a child with the supplied name.
+ *
+ * NOTE: This is part of `updateChildren` and is here for readability.
+ *
+ * @param {ReactComponent} child Component to mount.
+ * @param {string} name Name of the child.
+ * @param {number} index Index at which to insert the child.
+ * @param {ReactReconcileTransaction} transaction
+ * @private
+ */
+ _mountChildByNameAtIndex: function (child, name, index, transaction, context) {
+ // Inlined for performance, see `ReactInstanceHandles.createReactID`.
+ var rootID = this._rootNodeID + name;
+ var mountImage = ReactReconciler.mountComponent(child, rootID, transaction, context);
+ child._mountIndex = index;
+ this.createChild(child, mountImage);
+ },
+
+ /**
+ * Unmounts a rendered child.
+ *
+ * NOTE: This is part of `updateChildren` and is here for readability.
+ *
+ * @param {ReactComponent} child Component to unmount.
+ * @private
+ */
+ _unmountChild: function (child) {
+ this.removeChild(child);
+ child._mountIndex = null;
+ }
+
+ }
+
+};
+
+module.exports = ReactMultiChild;
+},{"123":123,"31":31,"36":36,"39":39,"74":74,"84":84}],74:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactMultiChildUpdateTypes
+ */
+
+'use strict';
+
+var keyMirror = _dereq_(165);
+
+/**
+ * When a component's children are updated, a series of update configuration
+ * objects are created in order to batch and serialize the required changes.
+ *
+ * Enumerates all the possible types of update configurations.
+ *
+ * @internal
+ */
+var ReactMultiChildUpdateTypes = keyMirror({
+ INSERT_MARKUP: null,
+ MOVE_EXISTING: null,
+ REMOVE_NODE: null,
+ SET_MARKUP: null,
+ TEXT_CONTENT: null
+});
+
+module.exports = ReactMultiChildUpdateTypes;
+},{"165":165}],75:[function(_dereq_,module,exports){
+/**
+ * Copyright 2014-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactNativeComponent
+ */
+
+'use strict';
+
+var assign = _dereq_(24);
+var invariant = _dereq_(161);
+
+var autoGenerateWrapperClass = null;
+var genericComponentClass = null;
+// This registry keeps track of wrapper classes around native tags.
+var tagToComponentClass = {};
+var textComponentClass = null;
+
+var ReactNativeComponentInjection = {
+ // This accepts a class that receives the tag string. This is a catch all
+ // that can render any kind of tag.
+ injectGenericComponentClass: function (componentClass) {
+ genericComponentClass = componentClass;
+ },
+ // This accepts a text component class that takes the text string to be
+ // rendered as props.
+ injectTextComponentClass: function (componentClass) {
+ textComponentClass = componentClass;
+ },
+ // This accepts a keyed object with classes as values. Each key represents a
+ // tag. That particular tag will use this class instead of the generic one.
+ injectComponentClasses: function (componentClasses) {
+ assign(tagToComponentClass, componentClasses);
+ }
+};
+
+/**
+ * Get a composite component wrapper class for a specific tag.
+ *
+ * @param {ReactElement} element The tag for which to get the class.
+ * @return {function} The React class constructor function.
+ */
+function getComponentClassForElement(element) {
+ if (typeof element.type === 'function') {
+ return element.type;
+ }
+ var tag = element.type;
+ var componentClass = tagToComponentClass[tag];
+ if (componentClass == null) {
+ tagToComponentClass[tag] = componentClass = autoGenerateWrapperClass(tag);
+ }
+ return componentClass;
+}
+
+/**
+ * Get a native internal component class for a specific tag.
+ *
+ * @param {ReactElement} element The element to create.
+ * @return {function} The internal class constructor function.
+ */
+function createInternalComponent(element) {
+ !genericComponentClass ? "development" !== 'production' ? invariant(false, 'There is no registered component for the tag %s', element.type) : invariant(false) : undefined;
+ return new genericComponentClass(element.type, element.props);
+}
+
+/**
+ * @param {ReactText} text
+ * @return {ReactComponent}
+ */
+function createInstanceForText(text) {
+ return new textComponentClass(text);
+}
+
+/**
+ * @param {ReactComponent} component
+ * @return {boolean}
+ */
+function isTextComponent(component) {
+ return component instanceof textComponentClass;
+}
+
+var ReactNativeComponent = {
+ getComponentClassForElement: getComponentClassForElement,
+ createInternalComponent: createInternalComponent,
+ createInstanceForText: createInstanceForText,
+ isTextComponent: isTextComponent,
+ injection: ReactNativeComponentInjection
+};
+
+module.exports = ReactNativeComponent;
+},{"161":161,"24":24}],76:[function(_dereq_,module,exports){
+/**
+ * Copyright 2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactNoopUpdateQueue
+ */
+
+'use strict';
+
+var warning = _dereq_(173);
+
+function warnTDZ(publicInstance, callerName) {
+ if ("development" !== 'production') {
+ "development" !== 'production' ? warning(false, '%s(...): Can only update a mounted or mounting component. ' + 'This usually means you called %s() on an unmounted component. ' + 'This is a no-op. Please check the code for the %s component.', callerName, callerName, publicInstance.constructor && publicInstance.constructor.displayName || '') : undefined;
+ }
+}
+
+/**
+ * This is the abstract API for an update queue.
+ */
+var ReactNoopUpdateQueue = {
+
+ /**
+ * Checks whether or not this composite component is mounted.
+ * @param {ReactClass} publicInstance The instance we want to test.
+ * @return {boolean} True if mounted, false otherwise.
+ * @protected
+ * @final
+ */
+ isMounted: function (publicInstance) {
+ return false;
+ },
+
+ /**
+ * Enqueue a callback that will be executed after all the pending updates
+ * have processed.
+ *
+ * @param {ReactClass} publicInstance The instance to use as `this` context.
+ * @param {?function} callback Called after state is updated.
+ * @internal
+ */
+ enqueueCallback: function (publicInstance, callback) {},
+
+ /**
+ * Forces an update. This should only be invoked when it is known with
+ * certainty that we are **not** in a DOM transaction.
+ *
+ * You may want to call this when you know that some deeper aspect of the
+ * component's state has changed but `setState` was not called.
+ *
+ * This will not invoke `shouldComponentUpdate`, but it will invoke
+ * `componentWillUpdate` and `componentDidUpdate`.
+ *
+ * @param {ReactClass} publicInstance The instance that should rerender.
+ * @internal
+ */
+ enqueueForceUpdate: function (publicInstance) {
+ warnTDZ(publicInstance, 'forceUpdate');
+ },
+
+ /**
+ * Replaces all of the state. Always use this or `setState` to mutate state.
+ * You should treat `this.state` as immutable.
+ *
+ * There is no guarantee that `this.state` will be immediately updated, so
+ * accessing `this.state` after calling this method may return the old value.
+ *
+ * @param {ReactClass} publicInstance The instance that should rerender.
+ * @param {object} completeState Next state.
+ * @internal
+ */
+ enqueueReplaceState: function (publicInstance, completeState) {
+ warnTDZ(publicInstance, 'replaceState');
+ },
+
+ /**
+ * Sets a subset of the state. This only exists because _pendingState is
+ * internal. This provides a merging strategy that is not available to deep
+ * properties which is confusing. TODO: Expose pendingState or don't use it
+ * during the merge.
+ *
+ * @param {ReactClass} publicInstance The instance that should rerender.
+ * @param {object} partialState Next partial state to be merged with state.
+ * @internal
+ */
+ enqueueSetState: function (publicInstance, partialState) {
+ warnTDZ(publicInstance, 'setState');
+ },
+
+ /**
+ * Sets a subset of the props.
+ *
+ * @param {ReactClass} publicInstance The instance that should rerender.
+ * @param {object} partialProps Subset of the next props.
+ * @internal
+ */
+ enqueueSetProps: function (publicInstance, partialProps) {
+ warnTDZ(publicInstance, 'setProps');
+ },
+
+ /**
+ * Replaces all of the props.
+ *
+ * @param {ReactClass} publicInstance The instance that should rerender.
+ * @param {object} props New props.
+ * @internal
+ */
+ enqueueReplaceProps: function (publicInstance, props) {
+ warnTDZ(publicInstance, 'replaceProps');
+ }
+
+};
+
+module.exports = ReactNoopUpdateQueue;
+},{"173":173}],77:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactOwner
+ */
+
+'use strict';
+
+var invariant = _dereq_(161);
+
+/**
+ * ReactOwners are capable of storing references to owned components.
+ *
+ * All components are capable of //being// referenced by owner components, but
+ * only ReactOwner components are capable of //referencing// owned components.
+ * The named reference is known as a "ref".
+ *
+ * Refs are available when mounted and updated during reconciliation.
+ *
+ * var MyComponent = React.createClass({
+ * render: function() {
+ * return (
+ * <div onClick={this.handleClick}>
+ * <CustomComponent ref="custom" />
+ * </div>
+ * );
+ * },
+ * handleClick: function() {
+ * this.refs.custom.handleClick();
+ * },
+ * componentDidMount: function() {
+ * this.refs.custom.initialize();
+ * }
+ * });
+ *
+ * Refs should rarely be used. When refs are used, they should only be done to
+ * control data that is not handled by React's data flow.
+ *
+ * @class ReactOwner
+ */
+var ReactOwner = {
+
+ /**
+ * @param {?object} object
+ * @return {boolean} True if `object` is a valid owner.
+ * @final
+ */
+ isValidOwner: function (object) {
+ return !!(object && typeof object.attachRef === 'function' && typeof object.detachRef === 'function');
+ },
+
+ /**
+ * Adds a component by ref to an owner component.
+ *
+ * @param {ReactComponent} component Component to reference.
+ * @param {string} ref Name by which to refer to the component.
+ * @param {ReactOwner} owner Component on which to record the ref.
+ * @final
+ * @internal
+ */
+ addComponentAsRefTo: function (component, ref, owner) {
+ !ReactOwner.isValidOwner(owner) ? "development" !== 'production' ? invariant(false, 'addComponentAsRefTo(...): Only a ReactOwner can have refs. You might ' + 'be adding a ref to a component that was not created inside a component\'s ' + '`render` method, or you have multiple copies of React loaded ' + '(details: https://fb.me/react-refs-must-have-owner).') : invariant(false) : undefined;
+ owner.attachRef(ref, component);
+ },
+
+ /**
+ * Removes a component by ref from an owner component.
+ *
+ * @param {ReactComponent} component Component to dereference.
+ * @param {string} ref Name of the ref to remove.
+ * @param {ReactOwner} owner Component on which the ref is recorded.
+ * @final
+ * @internal
+ */
+ removeComponentAsRefFrom: function (component, ref, owner) {
+ !ReactOwner.isValidOwner(owner) ? "development" !== 'production' ? invariant(false, 'removeComponentAsRefFrom(...): Only a ReactOwner can have refs. You might ' + 'be removing a ref to a component that was not created inside a component\'s ' + '`render` method, or you have multiple copies of React loaded ' + '(details: https://fb.me/react-refs-must-have-owner).') : invariant(false) : undefined;
+ // Check that `component` is still the current ref because we do not want to
+ // detach the ref if another component stole it.
+ if (owner.getPublicInstance().refs[ref] === component.getPublicInstance()) {
+ owner.detachRef(ref);
+ }
+ }
+
+};
+
+module.exports = ReactOwner;
+},{"161":161}],78:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactPerf
+ * @typechecks static-only
+ */
+
+'use strict';
+
+/**
+ * ReactPerf is a general AOP system designed to measure performance. This
+ * module only has the hooks: see ReactDefaultPerf for the analysis tool.
+ */
+var ReactPerf = {
+ /**
+ * Boolean to enable/disable measurement. Set to false by default to prevent
+ * accidental logging and perf loss.
+ */
+ enableMeasure: false,
+
+ /**
+ * Holds onto the measure function in use. By default, don't measure
+ * anything, but we'll override this if we inject a measure function.
+ */
+ storedMeasure: _noMeasure,
+
+ /**
+ * @param {object} object
+ * @param {string} objectName
+ * @param {object<string>} methodNames
+ */
+ measureMethods: function (object, objectName, methodNames) {
+ if ("development" !== 'production') {
+ for (var key in methodNames) {
+ if (!methodNames.hasOwnProperty(key)) {
+ continue;
+ }
+ object[key] = ReactPerf.measure(objectName, methodNames[key], object[key]);
+ }
+ }
+ },
+
+ /**
+ * Use this to wrap methods you want to measure. Zero overhead in production.
+ *
+ * @param {string} objName
+ * @param {string} fnName
+ * @param {function} func
+ * @return {function}
+ */
+ measure: function (objName, fnName, func) {
+ if ("development" !== 'production') {
+ var measuredFunc = null;
+ var wrapper = function () {
+ if (ReactPerf.enableMeasure) {
+ if (!measuredFunc) {
+ measuredFunc = ReactPerf.storedMeasure(objName, fnName, func);
+ }
+ return measuredFunc.apply(this, arguments);
+ }
+ return func.apply(this, arguments);
+ };
+ wrapper.displayName = objName + '_' + fnName;
+ return wrapper;
+ }
+ return func;
+ },
+
+ injection: {
+ /**
+ * @param {function} measure
+ */
+ injectMeasure: function (measure) {
+ ReactPerf.storedMeasure = measure;
+ }
+ }
+};
+
+/**
+ * Simply passes through the measured function, without measuring it.
+ *
+ * @param {string} objName
+ * @param {string} fnName
+ * @param {function} func
+ * @return {function}
+ */
+function _noMeasure(objName, fnName, func) {
+ return func;
+}
+
+module.exports = ReactPerf;
+},{}],79:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactPropTransferer
+ */
+
+'use strict';
+
+var assign = _dereq_(24);
+var emptyFunction = _dereq_(153);
+var joinClasses = _dereq_(164);
+
+/**
+ * Creates a transfer strategy that will merge prop values using the supplied
+ * `mergeStrategy`. If a prop was previously unset, this just sets it.
+ *
+ * @param {function} mergeStrategy
+ * @return {function}
+ */
+function createTransferStrategy(mergeStrategy) {
+ return function (props, key, value) {
+ if (!props.hasOwnProperty(key)) {
+ props[key] = value;
+ } else {
+ props[key] = mergeStrategy(props[key], value);
+ }
+ };
+}
+
+var transferStrategyMerge = createTransferStrategy(function (a, b) {
+ // `merge` overrides the first object's (`props[key]` above) keys using the
+ // second object's (`value`) keys. An object's style's existing `propA` would
+ // get overridden. Flip the order here.
+ return assign({}, b, a);
+});
+
+/**
+ * Transfer strategies dictate how props are transferred by `transferPropsTo`.
+ * NOTE: if you add any more exceptions to this list you should be sure to
+ * update `cloneWithProps()` accordingly.
+ */
+var TransferStrategies = {
+ /**
+ * Never transfer `children`.
+ */
+ children: emptyFunction,
+ /**
+ * Transfer the `className` prop by merging them.
+ */
+ className: createTransferStrategy(joinClasses),
+ /**
+ * Transfer the `style` prop (which is an object) by merging them.
+ */
+ style: transferStrategyMerge
+};
+
+/**
+ * Mutates the first argument by transferring the properties from the second
+ * argument.
+ *
+ * @param {object} props
+ * @param {object} newProps
+ * @return {object}
+ */
+function transferInto(props, newProps) {
+ for (var thisKey in newProps) {
+ if (!newProps.hasOwnProperty(thisKey)) {
+ continue;
+ }
+
+ var transferStrategy = TransferStrategies[thisKey];
+
+ if (transferStrategy && TransferStrategies.hasOwnProperty(thisKey)) {
+ transferStrategy(props, thisKey, newProps[thisKey]);
+ } else if (!props.hasOwnProperty(thisKey)) {
+ props[thisKey] = newProps[thisKey];
+ }
+ }
+ return props;
+}
+
+/**
+ * ReactPropTransferer are capable of transferring props to another component
+ * using a `transferPropsTo` method.
+ *
+ * @class ReactPropTransferer
+ */
+var ReactPropTransferer = {
+
+ /**
+ * Merge two props objects using TransferStrategies.
+ *
+ * @param {object} oldProps original props (they take precedence)
+ * @param {object} newProps new props to merge in
+ * @return {object} a new object containing both sets of props merged.
+ */
+ mergeProps: function (oldProps, newProps) {
+ return transferInto(assign({}, oldProps), newProps);
+ }
+
+};
+
+module.exports = ReactPropTransferer;
+},{"153":153,"164":164,"24":24}],80:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactPropTypeLocationNames
+ */
+
+'use strict';
+
+var ReactPropTypeLocationNames = {};
+
+if ("development" !== 'production') {
+ ReactPropTypeLocationNames = {
+ prop: 'prop',
+ context: 'context',
+ childContext: 'child context'
+ };
+}
+
+module.exports = ReactPropTypeLocationNames;
+},{}],81:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactPropTypeLocations
+ */
+
+'use strict';
+
+var keyMirror = _dereq_(165);
+
+var ReactPropTypeLocations = keyMirror({
+ prop: null,
+ context: null,
+ childContext: null
+});
+
+module.exports = ReactPropTypeLocations;
+},{"165":165}],82:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactPropTypes
+ */
+
+'use strict';
+
+var ReactElement = _dereq_(57);
+var ReactPropTypeLocationNames = _dereq_(80);
+
+var emptyFunction = _dereq_(153);
+var getIteratorFn = _dereq_(129);
+
+/**
+ * Collection of methods that allow declaration and validation of props that are
+ * supplied to React components. Example usage:
+ *
+ * var Props = require('ReactPropTypes');
+ * var MyArticle = React.createClass({
+ * propTypes: {
+ * // An optional string prop named "description".
+ * description: Props.string,
+ *
+ * // A required enum prop named "category".
+ * category: Props.oneOf(['News','Photos']).isRequired,
+ *
+ * // A prop named "dialog" that requires an instance of Dialog.
+ * dialog: Props.instanceOf(Dialog).isRequired
+ * },
+ * render: function() { ... }
+ * });
+ *
+ * A more formal specification of how these methods are used:
+ *
+ * type := array|bool|func|object|number|string|oneOf([...])|instanceOf(...)
+ * decl := ReactPropTypes.{type}(.isRequired)?
+ *
+ * Each and every declaration produces a function with the same signature. This
+ * allows the creation of custom validation functions. For example:
+ *
+ * var MyLink = React.createClass({
+ * propTypes: {
+ * // An optional string or URI prop named "href".
+ * href: function(props, propName, componentName) {
+ * var propValue = props[propName];
+ * if (propValue != null && typeof propValue !== 'string' &&
+ * !(propValue instanceof URI)) {
+ * return new Error(
+ * 'Expected a string or an URI for ' + propName + ' in ' +
+ * componentName
+ * );
+ * }
+ * }
+ * },
+ * render: function() {...}
+ * });
+ *
+ * @internal
+ */
+
+var ANONYMOUS = '<<anonymous>>';
+
+var ReactPropTypes = {
+ array: createPrimitiveTypeChecker('array'),
+ bool: createPrimitiveTypeChecker('boolean'),
+ func: createPrimitiveTypeChecker('function'),
+ number: createPrimitiveTypeChecker('number'),
+ object: createPrimitiveTypeChecker('object'),
+ string: createPrimitiveTypeChecker('string'),
+
+ any: createAnyTypeChecker(),
+ arrayOf: createArrayOfTypeChecker,
+ element: createElementTypeChecker(),
+ instanceOf: createInstanceTypeChecker,
+ node: createNodeChecker(),
+ objectOf: createObjectOfTypeChecker,
+ oneOf: createEnumTypeChecker,
+ oneOfType: createUnionTypeChecker,
+ shape: createShapeTypeChecker
+};
+
+function createChainableTypeChecker(validate) {
+ function checkType(isRequired, props, propName, componentName, location, propFullName) {
+ componentName = componentName || ANONYMOUS;
+ propFullName = propFullName || propName;
+ if (props[propName] == null) {
+ var locationName = ReactPropTypeLocationNames[location];
+ if (isRequired) {
+ return new Error('Required ' + locationName + ' `' + propFullName + '` was not specified in ' + ('`' + componentName + '`.'));
+ }
+ return null;
+ } else {
+ return validate(props, propName, componentName, location, propFullName);
+ }
+ }
+
+ var chainedCheckType = checkType.bind(null, false);
+ chainedCheckType.isRequired = checkType.bind(null, true);
+
+ return chainedCheckType;
+}
+
+function createPrimitiveTypeChecker(expectedType) {
+ function validate(props, propName, componentName, location, propFullName) {
+ var propValue = props[propName];
+ var propType = getPropType(propValue);
+ if (propType !== expectedType) {
+ var locationName = ReactPropTypeLocationNames[location];
+ // `propValue` being instance of, say, date/regexp, pass the 'object'
+ // check, but we can offer a more precise error message here rather than
+ // 'of type `object`'.
+ var preciseType = getPreciseType(propValue);
+
+ return new Error('Invalid ' + locationName + ' `' + propFullName + '` of type ' + ('`' + preciseType + '` supplied to `' + componentName + '`, expected ') + ('`' + expectedType + '`.'));
+ }
+ return null;
+ }
+ return createChainableTypeChecker(validate);
+}
+
+function createAnyTypeChecker() {
+ return createChainableTypeChecker(emptyFunction.thatReturns(null));
+}
+
+function createArrayOfTypeChecker(typeChecker) {
+ function validate(props, propName, componentName, location, propFullName) {
+ var propValue = props[propName];
+ if (!Array.isArray(propValue)) {
+ var locationName = ReactPropTypeLocationNames[location];
+ var propType = getPropType(propValue);
+ return new Error('Invalid ' + locationName + ' `' + propFullName + '` of type ' + ('`' + propType + '` supplied to `' + componentName + '`, expected an array.'));
+ }
+ for (var i = 0; i < propValue.length; i++) {
+ var error = typeChecker(propValue, i, componentName, location, propFullName + '[' + i + ']');
+ if (error instanceof Error) {
+ return error;
+ }
+ }
+ return null;
+ }
+ return createChainableTypeChecker(validate);
+}
+
+function createElementTypeChecker() {
+ function validate(props, propName, componentName, location, propFullName) {
+ if (!ReactElement.isValidElement(props[propName])) {
+ var locationName = ReactPropTypeLocationNames[location];
+ return new Error('Invalid ' + locationName + ' `' + propFullName + '` supplied to ' + ('`' + componentName + '`, expected a single ReactElement.'));
+ }
+ return null;
+ }
+ return createChainableTypeChecker(validate);
+}
+
+function createInstanceTypeChecker(expectedClass) {
+ function validate(props, propName, componentName, location, propFullName) {
+ if (!(props[propName] instanceof expectedClass)) {
+ var locationName = ReactPropTypeLocationNames[location];
+ var expectedClassName = expectedClass.name || ANONYMOUS;
+ var actualClassName = getClassName(props[propName]);
+ return new Error('Invalid ' + locationName + ' `' + propFullName + '` of type ' + ('`' + actualClassName + '` supplied to `' + componentName + '`, expected ') + ('instance of `' + expectedClassName + '`.'));
+ }
+ return null;
+ }
+ return createChainableTypeChecker(validate);
+}
+
+function createEnumTypeChecker(expectedValues) {
+ if (!Array.isArray(expectedValues)) {
+ return createChainableTypeChecker(function () {
+ return new Error('Invalid argument supplied to oneOf, expected an instance of array.');
+ });
+ }
+
+ function validate(props, propName, componentName, location, propFullName) {
+ var propValue = props[propName];
+ for (var i = 0; i < expectedValues.length; i++) {
+ if (propValue === expectedValues[i]) {
+ return null;
+ }
+ }
+
+ var locationName = ReactPropTypeLocationNames[location];
+ var valuesString = JSON.stringify(expectedValues);
+ return new Error('Invalid ' + locationName + ' `' + propFullName + '` of value `' + propValue + '` ' + ('supplied to `' + componentName + '`, expected one of ' + valuesString + '.'));
+ }
+ return createChainableTypeChecker(validate);
+}
+
+function createObjectOfTypeChecker(typeChecker) {
+ function validate(props, propName, componentName, location, propFullName) {
+ var propValue = props[propName];
+ var propType = getPropType(propValue);
+ if (propType !== 'object') {
+ var locationName = ReactPropTypeLocationNames[location];
+ return new Error('Invalid ' + locationName + ' `' + propFullName + '` of type ' + ('`' + propType + '` supplied to `' + componentName + '`, expected an object.'));
+ }
+ for (var key in propValue) {
+ if (propValue.hasOwnProperty(key)) {
+ var error = typeChecker(propValue, key, componentName, location, propFullName + '.' + key);
+ if (error instanceof Error) {
+ return error;
+ }
+ }
+ }
+ return null;
+ }
+ return createChainableTypeChecker(validate);
+}
+
+function createUnionTypeChecker(arrayOfTypeCheckers) {
+ if (!Array.isArray(arrayOfTypeCheckers)) {
+ return createChainableTypeChecker(function () {
+ return new Error('Invalid argument supplied to oneOfType, expected an instance of array.');
+ });
+ }
+
+ function validate(props, propName, componentName, location, propFullName) {
+ for (var i = 0; i < arrayOfTypeCheckers.length; i++) {
+ var checker = arrayOfTypeCheckers[i];
+ if (checker(props, propName, componentName, location, propFullName) == null) {
+ return null;
+ }
+ }
+
+ var locationName = ReactPropTypeLocationNames[location];
+ return new Error('Invalid ' + locationName + ' `' + propFullName + '` supplied to ' + ('`' + componentName + '`.'));
+ }
+ return createChainableTypeChecker(validate);
+}
+
+function createNodeChecker() {
+ function validate(props, propName, componentName, location, propFullName) {
+ if (!isNode(props[propName])) {
+ var locationName = ReactPropTypeLocationNames[location];
+ return new Error('Invalid ' + locationName + ' `' + propFullName + '` supplied to ' + ('`' + componentName + '`, expected a ReactNode.'));
+ }
+ return null;
+ }
+ return createChainableTypeChecker(validate);
+}
+
+function createShapeTypeChecker(shapeTypes) {
+ function validate(props, propName, componentName, location, propFullName) {
+ var propValue = props[propName];
+ var propType = getPropType(propValue);
+ if (propType !== 'object') {
+ var locationName = ReactPropTypeLocationNames[location];
+ return new Error('Invalid ' + locationName + ' `' + propFullName + '` of type `' + propType + '` ' + ('supplied to `' + componentName + '`, expected `object`.'));
+ }
+ for (var key in shapeTypes) {
+ var checker = shapeTypes[key];
+ if (!checker) {
+ continue;
+ }
+ var error = checker(propValue, key, componentName, location, propFullName + '.' + key);
+ if (error) {
+ return error;
+ }
+ }
+ return null;
+ }
+ return createChainableTypeChecker(validate);
+}
+
+function isNode(propValue) {
+ switch (typeof propValue) {
+ case 'number':
+ case 'string':
+ case 'undefined':
+ return true;
+ case 'boolean':
+ return !propValue;
+ case 'object':
+ if (Array.isArray(propValue)) {
+ return propValue.every(isNode);
+ }
+ if (propValue === null || ReactElement.isValidElement(propValue)) {
+ return true;
+ }
+
+ var iteratorFn = getIteratorFn(propValue);
+ if (iteratorFn) {
+ var iterator = iteratorFn.call(propValue);
+ var step;
+ if (iteratorFn !== propValue.entries) {
+ while (!(step = iterator.next()).done) {
+ if (!isNode(step.value)) {
+ return false;
+ }
+ }
+ } else {
+ // Iterator will provide entry [k,v] tuples rather than values.
+ while (!(step = iterator.next()).done) {
+ var entry = step.value;
+ if (entry) {
+ if (!isNode(entry[1])) {
+ return false;
+ }
+ }
+ }
+ }
+ } else {
+ return false;
+ }
+
+ return true;
+ default:
+ return false;
+ }
+}
+
+// Equivalent of `typeof` but with special handling for array and regexp.
+function getPropType(propValue) {
+ var propType = typeof propValue;
+ if (Array.isArray(propValue)) {
+ return 'array';
+ }
+ if (propValue instanceof RegExp) {
+ // Old webkits (at least until Android 4.0) return 'function' rather than
+ // 'object' for typeof a RegExp. We'll normalize this here so that /bla/
+ // passes PropTypes.object.
+ return 'object';
+ }
+ return propType;
+}
+
+// This handles more types than `getPropType`. Only used for error messages.
+// See `createPrimitiveTypeChecker`.
+function getPreciseType(propValue) {
+ var propType = getPropType(propValue);
+ if (propType === 'object') {
+ if (propValue instanceof Date) {
+ return 'date';
+ } else if (propValue instanceof RegExp) {
+ return 'regexp';
+ }
+ }
+ return propType;
+}
+
+// Returns class name of the object, if any.
+function getClassName(propValue) {
+ if (!propValue.constructor || !propValue.constructor.name) {
+ return '<<anonymous>>';
+ }
+ return propValue.constructor.name;
+}
+
+module.exports = ReactPropTypes;
+},{"129":129,"153":153,"57":57,"80":80}],83:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactReconcileTransaction
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var CallbackQueue = _dereq_(6);
+var PooledClass = _dereq_(25);
+var ReactBrowserEventEmitter = _dereq_(28);
+var ReactDOMFeatureFlags = _dereq_(44);
+var ReactInputSelection = _dereq_(66);
+var Transaction = _dereq_(113);
+
+var assign = _dereq_(24);
+
+/**
+ * Ensures that, when possible, the selection range (currently selected text
+ * input) is not disturbed by performing the transaction.
+ */
+var SELECTION_RESTORATION = {
+ /**
+ * @return {Selection} Selection information.
+ */
+ initialize: ReactInputSelection.getSelectionInformation,
+ /**
+ * @param {Selection} sel Selection information returned from `initialize`.
+ */
+ close: ReactInputSelection.restoreSelection
+};
+
+/**
+ * Suppresses events (blur/focus) that could be inadvertently dispatched due to
+ * high level DOM manipulations (like temporarily removing a text input from the
+ * DOM).
+ */
+var EVENT_SUPPRESSION = {
+ /**
+ * @return {boolean} The enabled status of `ReactBrowserEventEmitter` before
+ * the reconciliation.
+ */
+ initialize: function () {
+ var currentlyEnabled = ReactBrowserEventEmitter.isEnabled();
+ ReactBrowserEventEmitter.setEnabled(false);
+ return currentlyEnabled;
+ },
+
+ /**
+ * @param {boolean} previouslyEnabled Enabled status of
+ * `ReactBrowserEventEmitter` before the reconciliation occurred. `close`
+ * restores the previous value.
+ */
+ close: function (previouslyEnabled) {
+ ReactBrowserEventEmitter.setEnabled(previouslyEnabled);
+ }
+};
+
+/**
+ * Provides a queue for collecting `componentDidMount` and
+ * `componentDidUpdate` callbacks during the the transaction.
+ */
+var ON_DOM_READY_QUEUEING = {
+ /**
+ * Initializes the internal `onDOMReady` queue.
+ */
+ initialize: function () {
+ this.reactMountReady.reset();
+ },
+
+ /**
+ * After DOM is flushed, invoke all registered `onDOMReady` callbacks.
+ */
+ close: function () {
+ this.reactMountReady.notifyAll();
+ }
+};
+
+/**
+ * Executed within the scope of the `Transaction` instance. Consider these as
+ * being member methods, but with an implied ordering while being isolated from
+ * each other.
+ */
+var TRANSACTION_WRAPPERS = [SELECTION_RESTORATION, EVENT_SUPPRESSION, ON_DOM_READY_QUEUEING];
+
+/**
+ * Currently:
+ * - The order that these are listed in the transaction is critical:
+ * - Suppresses events.
+ * - Restores selection range.
+ *
+ * Future:
+ * - Restore document/overflow scroll positions that were unintentionally
+ * modified via DOM insertions above the top viewport boundary.
+ * - Implement/integrate with customized constraint based layout system and keep
+ * track of which dimensions must be remeasured.
+ *
+ * @class ReactReconcileTransaction
+ */
+function ReactReconcileTransaction(forceHTML) {
+ this.reinitializeTransaction();
+ // Only server-side rendering really needs this option (see
+ // `ReactServerRendering`), but server-side uses
+ // `ReactServerRenderingTransaction` instead. This option is here so that it's
+ // accessible and defaults to false when `ReactDOMComponent` and
+ // `ReactTextComponent` checks it in `mountComponent`.`
+ this.renderToStaticMarkup = false;
+ this.reactMountReady = CallbackQueue.getPooled(null);
+ this.useCreateElement = !forceHTML && ReactDOMFeatureFlags.useCreateElement;
+}
+
+var Mixin = {
+ /**
+ * @see Transaction
+ * @abstract
+ * @final
+ * @return {array<object>} List of operation wrap procedures.
+ * TODO: convert to array<TransactionWrapper>
+ */
+ getTransactionWrappers: function () {
+ return TRANSACTION_WRAPPERS;
+ },
+
+ /**
+ * @return {object} The queue to collect `onDOMReady` callbacks with.
+ */
+ getReactMountReady: function () {
+ return this.reactMountReady;
+ },
+
+ /**
+ * `PooledClass` looks for this, and will invoke this before allowing this
+ * instance to be reused.
+ */
+ destructor: function () {
+ CallbackQueue.release(this.reactMountReady);
+ this.reactMountReady = null;
+ }
+};
+
+assign(ReactReconcileTransaction.prototype, Transaction.Mixin, Mixin);
+
+PooledClass.addPoolingTo(ReactReconcileTransaction);
+
+module.exports = ReactReconcileTransaction;
+},{"113":113,"24":24,"25":25,"28":28,"44":44,"6":6,"66":66}],84:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactReconciler
+ */
+
+'use strict';
+
+var ReactRef = _dereq_(85);
+
+/**
+ * Helper to call ReactRef.attachRefs with this composite component, split out
+ * to avoid allocations in the transaction mount-ready queue.
+ */
+function attachRefs() {
+ ReactRef.attachRefs(this, this._currentElement);
+}
+
+var ReactReconciler = {
+
+ /**
+ * Initializes the component, renders markup, and registers event listeners.
+ *
+ * @param {ReactComponent} internalInstance
+ * @param {string} rootID DOM ID of the root node.
+ * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
+ * @return {?string} Rendered markup to be inserted into the DOM.
+ * @final
+ * @internal
+ */
+ mountComponent: function (internalInstance, rootID, transaction, context) {
+ var markup = internalInstance.mountComponent(rootID, transaction, context);
+ if (internalInstance._currentElement && internalInstance._currentElement.ref != null) {
+ transaction.getReactMountReady().enqueue(attachRefs, internalInstance);
+ }
+ return markup;
+ },
+
+ /**
+ * Releases any resources allocated by `mountComponent`.
+ *
+ * @final
+ * @internal
+ */
+ unmountComponent: function (internalInstance) {
+ ReactRef.detachRefs(internalInstance, internalInstance._currentElement);
+ internalInstance.unmountComponent();
+ },
+
+ /**
+ * Update a component using a new element.
+ *
+ * @param {ReactComponent} internalInstance
+ * @param {ReactElement} nextElement
+ * @param {ReactReconcileTransaction} transaction
+ * @param {object} context
+ * @internal
+ */
+ receiveComponent: function (internalInstance, nextElement, transaction, context) {
+ var prevElement = internalInstance._currentElement;
+
+ if (nextElement === prevElement && context === internalInstance._context) {
+ // Since elements are immutable after the owner is rendered,
+ // we can do a cheap identity compare here to determine if this is a
+ // superfluous reconcile. It's possible for state to be mutable but such
+ // change should trigger an update of the owner which would recreate
+ // the element. We explicitly check for the existence of an owner since
+ // it's possible for an element created outside a composite to be
+ // deeply mutated and reused.
+
+ // TODO: Bailing out early is just a perf optimization right?
+ // TODO: Removing the return statement should affect correctness?
+ return;
+ }
+
+ var refsChanged = ReactRef.shouldUpdateRefs(prevElement, nextElement);
+
+ if (refsChanged) {
+ ReactRef.detachRefs(internalInstance, prevElement);
+ }
+
+ internalInstance.receiveComponent(nextElement, transaction, context);
+
+ if (refsChanged && internalInstance._currentElement && internalInstance._currentElement.ref != null) {
+ transaction.getReactMountReady().enqueue(attachRefs, internalInstance);
+ }
+ },
+
+ /**
+ * Flush any dirty changes in a component.
+ *
+ * @param {ReactComponent} internalInstance
+ * @param {ReactReconcileTransaction} transaction
+ * @internal
+ */
+ performUpdateIfNecessary: function (internalInstance, transaction) {
+ internalInstance.performUpdateIfNecessary(transaction);
+ }
+
+};
+
+module.exports = ReactReconciler;
+},{"85":85}],85:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactRef
+ */
+
+'use strict';
+
+var ReactOwner = _dereq_(77);
+
+var ReactRef = {};
+
+function attachRef(ref, component, owner) {
+ if (typeof ref === 'function') {
+ ref(component.getPublicInstance());
+ } else {
+ // Legacy ref
+ ReactOwner.addComponentAsRefTo(component, ref, owner);
+ }
+}
+
+function detachRef(ref, component, owner) {
+ if (typeof ref === 'function') {
+ ref(null);
+ } else {
+ // Legacy ref
+ ReactOwner.removeComponentAsRefFrom(component, ref, owner);
+ }
+}
+
+ReactRef.attachRefs = function (instance, element) {
+ if (element === null || element === false) {
+ return;
+ }
+ var ref = element.ref;
+ if (ref != null) {
+ attachRef(ref, instance, element._owner);
+ }
+};
+
+ReactRef.shouldUpdateRefs = function (prevElement, nextElement) {
+ // If either the owner or a `ref` has changed, make sure the newest owner
+ // has stored a reference to `this`, and the previous owner (if different)
+ // has forgotten the reference to `this`. We use the element instead
+ // of the public this.props because the post processing cannot determine
+ // a ref. The ref conceptually lives on the element.
+
+ // TODO: Should this even be possible? The owner cannot change because
+ // it's forbidden by shouldUpdateReactComponent. The ref can change
+ // if you swap the keys of but not the refs. Reconsider where this check
+ // is made. It probably belongs where the key checking and
+ // instantiateReactComponent is done.
+
+ var prevEmpty = prevElement === null || prevElement === false;
+ var nextEmpty = nextElement === null || nextElement === false;
+
+ return(
+ // This has a few false positives w/r/t empty components.
+ prevEmpty || nextEmpty || nextElement._owner !== prevElement._owner || nextElement.ref !== prevElement.ref
+ );
+};
+
+ReactRef.detachRefs = function (instance, element) {
+ if (element === null || element === false) {
+ return;
+ }
+ var ref = element.ref;
+ if (ref != null) {
+ detachRef(ref, instance, element._owner);
+ }
+};
+
+module.exports = ReactRef;
+},{"77":77}],86:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactRootIndex
+ * @typechecks
+ */
+
+'use strict';
+
+var ReactRootIndexInjection = {
+ /**
+ * @param {function} _createReactRootIndex
+ */
+ injectCreateReactRootIndex: function (_createReactRootIndex) {
+ ReactRootIndex.createReactRootIndex = _createReactRootIndex;
+ }
+};
+
+var ReactRootIndex = {
+ createReactRootIndex: null,
+ injection: ReactRootIndexInjection
+};
+
+module.exports = ReactRootIndex;
+},{}],87:[function(_dereq_,module,exports){
+/**
+ * Copyright 2014-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactServerBatchingStrategy
+ * @typechecks
+ */
+
+'use strict';
+
+var ReactServerBatchingStrategy = {
+ isBatchingUpdates: false,
+ batchedUpdates: function (callback) {
+ // Don't do anything here. During the server rendering we don't want to
+ // schedule any updates. We will simply ignore them.
+ }
+};
+
+module.exports = ReactServerBatchingStrategy;
+},{}],88:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @typechecks static-only
+ * @providesModule ReactServerRendering
+ */
+'use strict';
+
+var ReactDefaultBatchingStrategy = _dereq_(53);
+var ReactElement = _dereq_(57);
+var ReactInstanceHandles = _dereq_(67);
+var ReactMarkupChecksum = _dereq_(71);
+var ReactServerBatchingStrategy = _dereq_(87);
+var ReactServerRenderingTransaction = _dereq_(89);
+var ReactUpdates = _dereq_(96);
+
+var emptyObject = _dereq_(154);
+var instantiateReactComponent = _dereq_(132);
+var invariant = _dereq_(161);
+
+/**
+ * @param {ReactElement} element
+ * @return {string} the HTML markup
+ */
+function renderToString(element) {
+ !ReactElement.isValidElement(element) ? "development" !== 'production' ? invariant(false, 'renderToString(): You must pass a valid ReactElement.') : invariant(false) : undefined;
+
+ var transaction;
+ try {
+ ReactUpdates.injection.injectBatchingStrategy(ReactServerBatchingStrategy);
+
+ var id = ReactInstanceHandles.createReactRootID();
+ transaction = ReactServerRenderingTransaction.getPooled(false);
+
+ return transaction.perform(function () {
+ var componentInstance = instantiateReactComponent(element, null);
+ var markup = componentInstance.mountComponent(id, transaction, emptyObject);
+ return ReactMarkupChecksum.addChecksumToMarkup(markup);
+ }, null);
+ } finally {
+ ReactServerRenderingTransaction.release(transaction);
+ // Revert to the DOM batching strategy since these two renderers
+ // currently share these stateful modules.
+ ReactUpdates.injection.injectBatchingStrategy(ReactDefaultBatchingStrategy);
+ }
+}
+
+/**
+ * @param {ReactElement} element
+ * @return {string} the HTML markup, without the extra React ID and checksum
+ * (for generating static pages)
+ */
+function renderToStaticMarkup(element) {
+ !ReactElement.isValidElement(element) ? "development" !== 'production' ? invariant(false, 'renderToStaticMarkup(): You must pass a valid ReactElement.') : invariant(false) : undefined;
+
+ var transaction;
+ try {
+ ReactUpdates.injection.injectBatchingStrategy(ReactServerBatchingStrategy);
+
+ var id = ReactInstanceHandles.createReactRootID();
+ transaction = ReactServerRenderingTransaction.getPooled(true);
+
+ return transaction.perform(function () {
+ var componentInstance = instantiateReactComponent(element, null);
+ return componentInstance.mountComponent(id, transaction, emptyObject);
+ }, null);
+ } finally {
+ ReactServerRenderingTransaction.release(transaction);
+ // Revert to the DOM batching strategy since these two renderers
+ // currently share these stateful modules.
+ ReactUpdates.injection.injectBatchingStrategy(ReactDefaultBatchingStrategy);
+ }
+}
+
+module.exports = {
+ renderToString: renderToString,
+ renderToStaticMarkup: renderToStaticMarkup
+};
+},{"132":132,"154":154,"161":161,"53":53,"57":57,"67":67,"71":71,"87":87,"89":89,"96":96}],89:[function(_dereq_,module,exports){
+/**
+ * Copyright 2014-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactServerRenderingTransaction
+ * @typechecks
+ */
+
+'use strict';
+
+var PooledClass = _dereq_(25);
+var CallbackQueue = _dereq_(6);
+var Transaction = _dereq_(113);
+
+var assign = _dereq_(24);
+var emptyFunction = _dereq_(153);
+
+/**
+ * Provides a `CallbackQueue` queue for collecting `onDOMReady` callbacks
+ * during the performing of the transaction.
+ */
+var ON_DOM_READY_QUEUEING = {
+ /**
+ * Initializes the internal `onDOMReady` queue.
+ */
+ initialize: function () {
+ this.reactMountReady.reset();
+ },
+
+ close: emptyFunction
+};
+
+/**
+ * Executed within the scope of the `Transaction` instance. Consider these as
+ * being member methods, but with an implied ordering while being isolated from
+ * each other.
+ */
+var TRANSACTION_WRAPPERS = [ON_DOM_READY_QUEUEING];
+
+/**
+ * @class ReactServerRenderingTransaction
+ * @param {boolean} renderToStaticMarkup
+ */
+function ReactServerRenderingTransaction(renderToStaticMarkup) {
+ this.reinitializeTransaction();
+ this.renderToStaticMarkup = renderToStaticMarkup;
+ this.reactMountReady = CallbackQueue.getPooled(null);
+ this.useCreateElement = false;
+}
+
+var Mixin = {
+ /**
+ * @see Transaction
+ * @abstract
+ * @final
+ * @return {array} Empty list of operation wrap procedures.
+ */
+ getTransactionWrappers: function () {
+ return TRANSACTION_WRAPPERS;
+ },
+
+ /**
+ * @return {object} The queue to collect `onDOMReady` callbacks with.
+ */
+ getReactMountReady: function () {
+ return this.reactMountReady;
+ },
+
+ /**
+ * `PooledClass` looks for this, and will invoke this before allowing this
+ * instance to be reused.
+ */
+ destructor: function () {
+ CallbackQueue.release(this.reactMountReady);
+ this.reactMountReady = null;
+ }
+};
+
+assign(ReactServerRenderingTransaction.prototype, Transaction.Mixin, Mixin);
+
+PooledClass.addPoolingTo(ReactServerRenderingTransaction);
+
+module.exports = ReactServerRenderingTransaction;
+},{"113":113,"153":153,"24":24,"25":25,"6":6}],90:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactStateSetters
+ */
+
+'use strict';
+
+var ReactStateSetters = {
+ /**
+ * Returns a function that calls the provided function, and uses the result
+ * of that to set the component's state.
+ *
+ * @param {ReactCompositeComponent} component
+ * @param {function} funcReturningState Returned callback uses this to
+ * determine how to update state.
+ * @return {function} callback that when invoked uses funcReturningState to
+ * determined the object literal to setState.
+ */
+ createStateSetter: function (component, funcReturningState) {
+ return function (a, b, c, d, e, f) {
+ var partialState = funcReturningState.call(component, a, b, c, d, e, f);
+ if (partialState) {
+ component.setState(partialState);
+ }
+ };
+ },
+
+ /**
+ * Returns a single-argument callback that can be used to update a single
+ * key in the component's state.
+ *
+ * Note: this is memoized function, which makes it inexpensive to call.
+ *
+ * @param {ReactCompositeComponent} component
+ * @param {string} key The key in the state that you should update.
+ * @return {function} callback of 1 argument which calls setState() with
+ * the provided keyName and callback argument.
+ */
+ createStateKeySetter: function (component, key) {
+ // Memoize the setters.
+ var cache = component.__keySetters || (component.__keySetters = {});
+ return cache[key] || (cache[key] = createStateKeySetter(component, key));
+ }
+};
+
+function createStateKeySetter(component, key) {
+ // Partial state is allocated outside of the function closure so it can be
+ // reused with every call, avoiding memory allocation when this function
+ // is called.
+ var partialState = {};
+ return function stateKeySetter(value) {
+ partialState[key] = value;
+ component.setState(partialState);
+ };
+}
+
+ReactStateSetters.Mixin = {
+ /**
+ * Returns a function that calls the provided function, and uses the result
+ * of that to set the component's state.
+ *
+ * For example, these statements are equivalent:
+ *
+ * this.setState({x: 1});
+ * this.createStateSetter(function(xValue) {
+ * return {x: xValue};
+ * })(1);
+ *
+ * @param {function} funcReturningState Returned callback uses this to
+ * determine how to update state.
+ * @return {function} callback that when invoked uses funcReturningState to
+ * determined the object literal to setState.
+ */
+ createStateSetter: function (funcReturningState) {
+ return ReactStateSetters.createStateSetter(this, funcReturningState);
+ },
+
+ /**
+ * Returns a single-argument callback that can be used to update a single
+ * key in the component's state.
+ *
+ * For example, these statements are equivalent:
+ *
+ * this.setState({x: 1});
+ * this.createStateKeySetter('x')(1);
+ *
+ * Note: this is memoized function, which makes it inexpensive to call.
+ *
+ * @param {string} key The key in the state that you should update.
+ * @return {function} callback of 1 argument which calls setState() with
+ * the provided keyName and callback argument.
+ */
+ createStateKeySetter: function (key) {
+ return ReactStateSetters.createStateKeySetter(this, key);
+ }
+};
+
+module.exports = ReactStateSetters;
+},{}],91:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactTestUtils
+ */
+
+'use strict';
+
+var EventConstants = _dereq_(15);
+var EventPluginHub = _dereq_(16);
+var EventPropagators = _dereq_(19);
+var React = _dereq_(26);
+var ReactDOM = _dereq_(40);
+var ReactElement = _dereq_(57);
+var ReactBrowserEventEmitter = _dereq_(28);
+var ReactCompositeComponent = _dereq_(38);
+var ReactInstanceHandles = _dereq_(67);
+var ReactInstanceMap = _dereq_(68);
+var ReactMount = _dereq_(72);
+var ReactUpdates = _dereq_(96);
+var SyntheticEvent = _dereq_(105);
+
+var assign = _dereq_(24);
+var emptyObject = _dereq_(154);
+var findDOMNode = _dereq_(122);
+var invariant = _dereq_(161);
+
+var topLevelTypes = EventConstants.topLevelTypes;
+
+function Event(suffix) {}
+
+/**
+ * @class ReactTestUtils
+ */
+
+function findAllInRenderedTreeInternal(inst, test) {
+ if (!inst || !inst.getPublicInstance) {
+ return [];
+ }
+ var publicInst = inst.getPublicInstance();
+ var ret = test(publicInst) ? [publicInst] : [];
+ var currentElement = inst._currentElement;
+ if (ReactTestUtils.isDOMComponent(publicInst)) {
+ var renderedChildren = inst._renderedChildren;
+ var key;
+ for (key in renderedChildren) {
+ if (!renderedChildren.hasOwnProperty(key)) {
+ continue;
+ }
+ ret = ret.concat(findAllInRenderedTreeInternal(renderedChildren[key], test));
+ }
+ } else if (ReactElement.isValidElement(currentElement) && typeof currentElement.type === 'function') {
+ ret = ret.concat(findAllInRenderedTreeInternal(inst._renderedComponent, test));
+ }
+ return ret;
+}
+
+/**
+ * Todo: Support the entire DOM.scry query syntax. For now, these simple
+ * utilities will suffice for testing purposes.
+ * @lends ReactTestUtils
+ */
+var ReactTestUtils = {
+ renderIntoDocument: function (instance) {
+ var div = document.createElementNS('http://www.w3.org/1999/xhtml', 'div');
+ // None of our tests actually require attaching the container to the
+ // DOM, and doing so creates a mess that we rely on test isolation to
+ // clean up, so we're going to stop honoring the name of this method
+ // (and probably rename it eventually) if no problems arise.
+ // document.documentElement.appendChild(div);
+ return ReactDOM.render(instance, div);
+ },
+
+ isElement: function (element) {
+ return ReactElement.isValidElement(element);
+ },
+
+ isElementOfType: function (inst, convenienceConstructor) {
+ return ReactElement.isValidElement(inst) && inst.type === convenienceConstructor;
+ },
+
+ isDOMComponent: function (inst) {
+ return !!(inst && inst.nodeType === 1 && inst.tagName);
+ },
+
+ isDOMComponentElement: function (inst) {
+ return !!(inst && ReactElement.isValidElement(inst) && !!inst.tagName);
+ },
+
+ isCompositeComponent: function (inst) {
+ if (ReactTestUtils.isDOMComponent(inst)) {
+ // Accessing inst.setState warns; just return false as that'll be what
+ // this returns when we have DOM nodes as refs directly
+ return false;
+ }
+ return inst != null && typeof inst.render === 'function' && typeof inst.setState === 'function';
+ },
+
+ isCompositeComponentWithType: function (inst, type) {
+ if (!ReactTestUtils.isCompositeComponent(inst)) {
+ return false;
+ }
+ var internalInstance = ReactInstanceMap.get(inst);
+ var constructor = internalInstance._currentElement.type;
+
+ return constructor === type;
+ },
+
+ isCompositeComponentElement: function (inst) {
+ if (!ReactElement.isValidElement(inst)) {
+ return false;
+ }
+ // We check the prototype of the type that will get mounted, not the
+ // instance itself. This is a future proof way of duck typing.
+ var prototype = inst.type.prototype;
+ return typeof prototype.render === 'function' && typeof prototype.setState === 'function';
+ },
+
+ isCompositeComponentElementWithType: function (inst, type) {
+ var internalInstance = ReactInstanceMap.get(inst);
+ var constructor = internalInstance._currentElement.type;
+
+ return !!(ReactTestUtils.isCompositeComponentElement(inst) && constructor === type);
+ },
+
+ getRenderedChildOfCompositeComponent: function (inst) {
+ if (!ReactTestUtils.isCompositeComponent(inst)) {
+ return null;
+ }
+ var internalInstance = ReactInstanceMap.get(inst);
+ return internalInstance._renderedComponent.getPublicInstance();
+ },
+
+ findAllInRenderedTree: function (inst, test) {
+ if (!inst) {
+ return [];
+ }
+ !ReactTestUtils.isCompositeComponent(inst) ? "development" !== 'production' ? invariant(false, 'findAllInRenderedTree(...): instance must be a composite component') : invariant(false) : undefined;
+ return findAllInRenderedTreeInternal(ReactInstanceMap.get(inst), test);
+ },
+
+ /**
+ * Finds all instance of components in the rendered tree that are DOM
+ * components with the class name matching `className`.
+ * @return {array} an array of all the matches.
+ */
+ scryRenderedDOMComponentsWithClass: function (root, classNames) {
+ if (!Array.isArray(classNames)) {
+ classNames = classNames.split(/\s+/);
+ }
+ return ReactTestUtils.findAllInRenderedTree(root, function (inst) {
+ if (ReactTestUtils.isDOMComponent(inst)) {
+ var className = inst.className;
+ if (typeof className !== 'string') {
+ // SVG, probably.
+ className = inst.getAttribute('class') || '';
+ }
+ var classList = className.split(/\s+/);
+ return classNames.every(function (name) {
+ return classList.indexOf(name) !== -1;
+ });
+ }
+ return false;
+ });
+ },
+
+ /**
+ * Like scryRenderedDOMComponentsWithClass but expects there to be one result,
+ * and returns that one result, or throws exception if there is any other
+ * number of matches besides one.
+ * @return {!ReactDOMComponent} The one match.
+ */
+ findRenderedDOMComponentWithClass: function (root, className) {
+ var all = ReactTestUtils.scryRenderedDOMComponentsWithClass(root, className);
+ if (all.length !== 1) {
+ throw new Error('Did not find exactly one match ' + '(found: ' + all.length + ') for class:' + className);
+ }
+ return all[0];
+ },
+
+ /**
+ * Finds all instance of components in the rendered tree that are DOM
+ * components with the tag name matching `tagName`.
+ * @return {array} an array of all the matches.
+ */
+ scryRenderedDOMComponentsWithTag: function (root, tagName) {
+ return ReactTestUtils.findAllInRenderedTree(root, function (inst) {
+ return ReactTestUtils.isDOMComponent(inst) && inst.tagName.toUpperCase() === tagName.toUpperCase();
+ });
+ },
+
+ /**
+ * Like scryRenderedDOMComponentsWithTag but expects there to be one result,
+ * and returns that one result, or throws exception if there is any other
+ * number of matches besides one.
+ * @return {!ReactDOMComponent} The one match.
+ */
+ findRenderedDOMComponentWithTag: function (root, tagName) {
+ var all = ReactTestUtils.scryRenderedDOMComponentsWithTag(root, tagName);
+ if (all.length !== 1) {
+ throw new Error('Did not find exactly one match for tag:' + tagName);
+ }
+ return all[0];
+ },
+
+ /**
+ * Finds all instances of components with type equal to `componentType`.
+ * @return {array} an array of all the matches.
+ */
+ scryRenderedComponentsWithType: function (root, componentType) {
+ return ReactTestUtils.findAllInRenderedTree(root, function (inst) {
+ return ReactTestUtils.isCompositeComponentWithType(inst, componentType);
+ });
+ },
+
+ /**
+ * Same as `scryRenderedComponentsWithType` but expects there to be one result
+ * and returns that one result, or throws exception if there is any other
+ * number of matches besides one.
+ * @return {!ReactComponent} The one match.
+ */
+ findRenderedComponentWithType: function (root, componentType) {
+ var all = ReactTestUtils.scryRenderedComponentsWithType(root, componentType);
+ if (all.length !== 1) {
+ throw new Error('Did not find exactly one match for componentType:' + componentType + ' (found ' + all.length + ')');
+ }
+ return all[0];
+ },
+
+ /**
+ * Pass a mocked component module to this method to augment it with
+ * useful methods that allow it to be used as a dummy React component.
+ * Instead of rendering as usual, the component will become a simple
+ * <div> containing any provided children.
+ *
+ * @param {object} module the mock function object exported from a
+ * module that defines the component to be mocked
+ * @param {?string} mockTagName optional dummy root tag name to return
+ * from render method (overrides
+ * module.mockTagName if provided)
+ * @return {object} the ReactTestUtils object (for chaining)
+ */
+ mockComponent: function (module, mockTagName) {
+ mockTagName = mockTagName || module.mockTagName || 'div';
+
+ module.prototype.render.mockImplementation(function () {
+ return React.createElement(mockTagName, null, this.props.children);
+ });
+
+ return this;
+ },
+
+ /**
+ * Simulates a top level event being dispatched from a raw event that occurred
+ * on an `Element` node.
+ * @param {Object} topLevelType A type from `EventConstants.topLevelTypes`
+ * @param {!Element} node The dom to simulate an event occurring on.
+ * @param {?Event} fakeNativeEvent Fake native event to use in SyntheticEvent.
+ */
+ simulateNativeEventOnNode: function (topLevelType, node, fakeNativeEvent) {
+ fakeNativeEvent.target = node;
+ ReactBrowserEventEmitter.ReactEventListener.dispatchEvent(topLevelType, fakeNativeEvent);
+ },
+
+ /**
+ * Simulates a top level event being dispatched from a raw event that occurred
+ * on the `ReactDOMComponent` `comp`.
+ * @param {Object} topLevelType A type from `EventConstants.topLevelTypes`.
+ * @param {!ReactDOMComponent} comp
+ * @param {?Event} fakeNativeEvent Fake native event to use in SyntheticEvent.
+ */
+ simulateNativeEventOnDOMComponent: function (topLevelType, comp, fakeNativeEvent) {
+ ReactTestUtils.simulateNativeEventOnNode(topLevelType, findDOMNode(comp), fakeNativeEvent);
+ },
+
+ nativeTouchData: function (x, y) {
+ return {
+ touches: [{ pageX: x, pageY: y }]
+ };
+ },
+
+ createRenderer: function () {
+ return new ReactShallowRenderer();
+ },
+
+ Simulate: null,
+ SimulateNative: {}
+};
+
+/**
+ * @class ReactShallowRenderer
+ */
+var ReactShallowRenderer = function () {
+ this._instance = null;
+};
+
+ReactShallowRenderer.prototype.getRenderOutput = function () {
+ return this._instance && this._instance._renderedComponent && this._instance._renderedComponent._renderedOutput || null;
+};
+
+var NoopInternalComponent = function (element) {
+ this._renderedOutput = element;
+ this._currentElement = element;
+};
+
+NoopInternalComponent.prototype = {
+
+ mountComponent: function () {},
+
+ receiveComponent: function (element) {
+ this._renderedOutput = element;
+ this._currentElement = element;
+ },
+
+ unmountComponent: function () {},
+
+ getPublicInstance: function () {
+ return null;
+ }
+};
+
+var ShallowComponentWrapper = function () {};
+assign(ShallowComponentWrapper.prototype, ReactCompositeComponent.Mixin, {
+ _instantiateReactComponent: function (element) {
+ return new NoopInternalComponent(element);
+ },
+ _replaceNodeWithMarkupByID: function () {},
+ _renderValidatedComponent: ReactCompositeComponent.Mixin._renderValidatedComponentWithoutOwnerOrContext
+});
+
+ReactShallowRenderer.prototype.render = function (element, context) {
+ !ReactElement.isValidElement(element) ? "development" !== 'production' ? invariant(false, 'ReactShallowRenderer render(): Invalid component element.%s', typeof element === 'function' ? ' Instead of passing a component class, make sure to instantiate ' + 'it by passing it to React.createElement.' : '') : invariant(false) : undefined;
+ !(typeof element.type !== 'string') ? "development" !== 'production' ? invariant(false, 'ReactShallowRenderer render(): Shallow rendering works only with custom ' + 'components, not primitives (%s). Instead of calling `.render(el)` and ' + 'inspecting the rendered output, look at `el.props` directly instead.', element.type) : invariant(false) : undefined;
+
+ if (!context) {
+ context = emptyObject;
+ }
+ var transaction = ReactUpdates.ReactReconcileTransaction.getPooled(false);
+ this._render(element, transaction, context);
+ ReactUpdates.ReactReconcileTransaction.release(transaction);
+};
+
+ReactShallowRenderer.prototype.unmount = function () {
+ if (this._instance) {
+ this._instance.unmountComponent();
+ }
+};
+
+ReactShallowRenderer.prototype._render = function (element, transaction, context) {
+ if (this._instance) {
+ this._instance.receiveComponent(element, transaction, context);
+ } else {
+ var rootID = ReactInstanceHandles.createReactRootID();
+ var instance = new ShallowComponentWrapper(element.type);
+ instance.construct(element);
+
+ instance.mountComponent(rootID, transaction, context);
+
+ this._instance = instance;
+ }
+};
+
+/**
+ * Exports:
+ *
+ * - `ReactTestUtils.Simulate.click(Element/ReactDOMComponent)`
+ * - `ReactTestUtils.Simulate.mouseMove(Element/ReactDOMComponent)`
+ * - `ReactTestUtils.Simulate.change(Element/ReactDOMComponent)`
+ * - ... (All keys from event plugin `eventTypes` objects)
+ */
+function makeSimulator(eventType) {
+ return function (domComponentOrNode, eventData) {
+ var node;
+ if (ReactTestUtils.isDOMComponent(domComponentOrNode)) {
+ node = findDOMNode(domComponentOrNode);
+ } else if (domComponentOrNode.tagName) {
+ node = domComponentOrNode;
+ }
+
+ var dispatchConfig = ReactBrowserEventEmitter.eventNameDispatchConfigs[eventType];
+
+ var fakeNativeEvent = new Event();
+ fakeNativeEvent.target = node;
+ // We don't use SyntheticEvent.getPooled in order to not have to worry about
+ // properly destroying any properties assigned from `eventData` upon release
+ var event = new SyntheticEvent(dispatchConfig, ReactMount.getID(node), fakeNativeEvent, node);
+ assign(event, eventData);
+
+ if (dispatchConfig.phasedRegistrationNames) {
+ EventPropagators.accumulateTwoPhaseDispatches(event);
+ } else {
+ EventPropagators.accumulateDirectDispatches(event);
+ }
+
+ ReactUpdates.batchedUpdates(function () {
+ EventPluginHub.enqueueEvents(event);
+ EventPluginHub.processEventQueue(true);
+ });
+ };
+}
+
+function buildSimulators() {
+ ReactTestUtils.Simulate = {};
+
+ var eventType;
+ for (eventType in ReactBrowserEventEmitter.eventNameDispatchConfigs) {
+ /**
+ * @param {!Element|ReactDOMComponent} domComponentOrNode
+ * @param {?object} eventData Fake event data to use in SyntheticEvent.
+ */
+ ReactTestUtils.Simulate[eventType] = makeSimulator(eventType);
+ }
+}
+
+// Rebuild ReactTestUtils.Simulate whenever event plugins are injected
+var oldInjectEventPluginOrder = EventPluginHub.injection.injectEventPluginOrder;
+EventPluginHub.injection.injectEventPluginOrder = function () {
+ oldInjectEventPluginOrder.apply(this, arguments);
+ buildSimulators();
+};
+var oldInjectEventPlugins = EventPluginHub.injection.injectEventPluginsByName;
+EventPluginHub.injection.injectEventPluginsByName = function () {
+ oldInjectEventPlugins.apply(this, arguments);
+ buildSimulators();
+};
+
+buildSimulators();
+
+/**
+ * Exports:
+ *
+ * - `ReactTestUtils.SimulateNative.click(Element/ReactDOMComponent)`
+ * - `ReactTestUtils.SimulateNative.mouseMove(Element/ReactDOMComponent)`
+ * - `ReactTestUtils.SimulateNative.mouseIn/ReactDOMComponent)`
+ * - `ReactTestUtils.SimulateNative.mouseOut(Element/ReactDOMComponent)`
+ * - ... (All keys from `EventConstants.topLevelTypes`)
+ *
+ * Note: Top level event types are a subset of the entire set of handler types
+ * (which include a broader set of "synthetic" events). For example, onDragDone
+ * is a synthetic event. Except when testing an event plugin or React's event
+ * handling code specifically, you probably want to use ReactTestUtils.Simulate
+ * to dispatch synthetic events.
+ */
+
+function makeNativeSimulator(eventType) {
+ return function (domComponentOrNode, nativeEventData) {
+ var fakeNativeEvent = new Event(eventType);
+ assign(fakeNativeEvent, nativeEventData);
+ if (ReactTestUtils.isDOMComponent(domComponentOrNode)) {
+ ReactTestUtils.simulateNativeEventOnDOMComponent(eventType, domComponentOrNode, fakeNativeEvent);
+ } else if (domComponentOrNode.tagName) {
+ // Will allow on actual dom nodes.
+ ReactTestUtils.simulateNativeEventOnNode(eventType, domComponentOrNode, fakeNativeEvent);
+ }
+ };
+}
+
+Object.keys(topLevelTypes).forEach(function (eventType) {
+ // Event type is stored as 'topClick' - we transform that to 'click'
+ var convenienceName = eventType.indexOf('top') === 0 ? eventType.charAt(3).toLowerCase() + eventType.substr(4) : eventType;
+ /**
+ * @param {!Element|ReactDOMComponent} domComponentOrNode
+ * @param {?Event} nativeEventData Fake native event to use in SyntheticEvent.
+ */
+ ReactTestUtils.SimulateNative[convenienceName] = makeNativeSimulator(eventType);
+});
+
+module.exports = ReactTestUtils;
+},{"105":105,"122":122,"15":15,"154":154,"16":16,"161":161,"19":19,"24":24,"26":26,"28":28,"38":38,"40":40,"57":57,"67":67,"68":68,"72":72,"96":96}],92:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @typechecks static-only
+ * @providesModule ReactTransitionChildMapping
+ */
+
+'use strict';
+
+var flattenChildren = _dereq_(123);
+
+var ReactTransitionChildMapping = {
+ /**
+ * Given `this.props.children`, return an object mapping key to child. Just
+ * simple syntactic sugar around flattenChildren().
+ *
+ * @param {*} children `this.props.children`
+ * @return {object} Mapping of key to child
+ */
+ getChildMapping: function (children) {
+ if (!children) {
+ return children;
+ }
+ return flattenChildren(children);
+ },
+
+ /**
+ * When you're adding or removing children some may be added or removed in the
+ * same render pass. We want to show *both* since we want to simultaneously
+ * animate elements in and out. This function takes a previous set of keys
+ * and a new set of keys and merges them with its best guess of the correct
+ * ordering. In the future we may expose some of the utilities in
+ * ReactMultiChild to make this easy, but for now React itself does not
+ * directly have this concept of the union of prevChildren and nextChildren
+ * so we implement it here.
+ *
+ * @param {object} prev prev children as returned from
+ * `ReactTransitionChildMapping.getChildMapping()`.
+ * @param {object} next next children as returned from
+ * `ReactTransitionChildMapping.getChildMapping()`.
+ * @return {object} a key set that contains all keys in `prev` and all keys
+ * in `next` in a reasonable order.
+ */
+ mergeChildMappings: function (prev, next) {
+ prev = prev || {};
+ next = next || {};
+
+ function getValueForKey(key) {
+ if (next.hasOwnProperty(key)) {
+ return next[key];
+ } else {
+ return prev[key];
+ }
+ }
+
+ // For each key of `next`, the list of keys to insert before that key in
+ // the combined list
+ var nextKeysPending = {};
+
+ var pendingKeys = [];
+ for (var prevKey in prev) {
+ if (next.hasOwnProperty(prevKey)) {
+ if (pendingKeys.length) {
+ nextKeysPending[prevKey] = pendingKeys;
+ pendingKeys = [];
+ }
+ } else {
+ pendingKeys.push(prevKey);
+ }
+ }
+
+ var i;
+ var childMapping = {};
+ for (var nextKey in next) {
+ if (nextKeysPending.hasOwnProperty(nextKey)) {
+ for (i = 0; i < nextKeysPending[nextKey].length; i++) {
+ var pendingNextKey = nextKeysPending[nextKey][i];
+ childMapping[nextKeysPending[nextKey][i]] = getValueForKey(pendingNextKey);
+ }
+ }
+ childMapping[nextKey] = getValueForKey(nextKey);
+ }
+
+ // Finally, add the keys which didn't appear before any key in `next`
+ for (i = 0; i < pendingKeys.length; i++) {
+ childMapping[pendingKeys[i]] = getValueForKey(pendingKeys[i]);
+ }
+
+ return childMapping;
+ }
+};
+
+module.exports = ReactTransitionChildMapping;
+},{"123":123}],93:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactTransitionEvents
+ */
+
+'use strict';
+
+var ExecutionEnvironment = _dereq_(147);
+
+/**
+ * EVENT_NAME_MAP is used to determine which event fired when a
+ * transition/animation ends, based on the style property used to
+ * define that event.
+ */
+var EVENT_NAME_MAP = {
+ transitionend: {
+ 'transition': 'transitionend',
+ 'WebkitTransition': 'webkitTransitionEnd',
+ 'MozTransition': 'mozTransitionEnd',
+ 'OTransition': 'oTransitionEnd',
+ 'msTransition': 'MSTransitionEnd'
+ },
+
+ animationend: {
+ 'animation': 'animationend',
+ 'WebkitAnimation': 'webkitAnimationEnd',
+ 'MozAnimation': 'mozAnimationEnd',
+ 'OAnimation': 'oAnimationEnd',
+ 'msAnimation': 'MSAnimationEnd'
+ }
+};
+
+var endEvents = [];
+
+function detectEvents() {
+ var testEl = document.createElementNS('http://www.w3.org/1999/xhtml', 'div');
+ var style = testEl.style;
+
+ // On some platforms, in particular some releases of Android 4.x,
+ // the un-prefixed "animation" and "transition" properties are defined on the
+ // style object but the events that fire will still be prefixed, so we need
+ // to check if the un-prefixed events are useable, and if not remove them
+ // from the map
+ if (!('AnimationEvent' in window)) {
+ delete EVENT_NAME_MAP.animationend.animation;
+ }
+
+ if (!('TransitionEvent' in window)) {
+ delete EVENT_NAME_MAP.transitionend.transition;
+ }
+
+ for (var baseEventName in EVENT_NAME_MAP) {
+ var baseEvents = EVENT_NAME_MAP[baseEventName];
+ for (var styleName in baseEvents) {
+ if (styleName in style) {
+ endEvents.push(baseEvents[styleName]);
+ break;
+ }
+ }
+ }
+}
+
+if (ExecutionEnvironment.canUseDOM) {
+ detectEvents();
+}
+
+// We use the raw {add|remove}EventListener() call because EventListener
+// does not know how to remove event listeners and we really should
+// clean up. Also, these events are not triggered in older browsers
+// so we should be A-OK here.
+
+function addEventListener(node, eventName, eventListener) {
+ node.addEventListener(eventName, eventListener, false);
+}
+
+function removeEventListener(node, eventName, eventListener) {
+ node.removeEventListener(eventName, eventListener, false);
+}
+
+var ReactTransitionEvents = {
+ addEndEventListener: function (node, eventListener) {
+ if (endEvents.length === 0) {
+ // If CSS transitions are not supported, trigger an "end animation"
+ // event immediately.
+ window.setTimeout(eventListener, 0);
+ return;
+ }
+ endEvents.forEach(function (endEvent) {
+ addEventListener(node, endEvent, eventListener);
+ });
+ },
+
+ removeEndEventListener: function (node, eventListener) {
+ if (endEvents.length === 0) {
+ return;
+ }
+ endEvents.forEach(function (endEvent) {
+ removeEventListener(node, endEvent, eventListener);
+ });
+ }
+};
+
+module.exports = ReactTransitionEvents;
+},{"147":147}],94:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactTransitionGroup
+ */
+
+'use strict';
+
+var React = _dereq_(26);
+var ReactTransitionChildMapping = _dereq_(92);
+
+var assign = _dereq_(24);
+var emptyFunction = _dereq_(153);
+
+var ReactTransitionGroup = React.createClass({
+ displayName: 'ReactTransitionGroup',
+
+ propTypes: {
+ component: React.PropTypes.any,
+ childFactory: React.PropTypes.func
+ },
+
+ getDefaultProps: function () {
+ return {
+ component: 'span',
+ childFactory: emptyFunction.thatReturnsArgument
+ };
+ },
+
+ getInitialState: function () {
+ return {
+ children: ReactTransitionChildMapping.getChildMapping(this.props.children)
+ };
+ },
+
+ componentWillMount: function () {
+ this.currentlyTransitioningKeys = {};
+ this.keysToEnter = [];
+ this.keysToLeave = [];
+ },
+
+ componentDidMount: function () {
+ var initialChildMapping = this.state.children;
+ for (var key in initialChildMapping) {
+ if (initialChildMapping[key]) {
+ this.performAppear(key);
+ }
+ }
+ },
+
+ componentWillReceiveProps: function (nextProps) {
+ var nextChildMapping = ReactTransitionChildMapping.getChildMapping(nextProps.children);
+ var prevChildMapping = this.state.children;
+
+ this.setState({
+ children: ReactTransitionChildMapping.mergeChildMappings(prevChildMapping, nextChildMapping)
+ });
+
+ var key;
+
+ for (key in nextChildMapping) {
+ var hasPrev = prevChildMapping && prevChildMapping.hasOwnProperty(key);
+ if (nextChildMapping[key] && !hasPrev && !this.currentlyTransitioningKeys[key]) {
+ this.keysToEnter.push(key);
+ }
+ }
+
+ for (key in prevChildMapping) {
+ var hasNext = nextChildMapping && nextChildMapping.hasOwnProperty(key);
+ if (prevChildMapping[key] && !hasNext && !this.currentlyTransitioningKeys[key]) {
+ this.keysToLeave.push(key);
+ }
+ }
+
+ // If we want to someday check for reordering, we could do it here.
+ },
+
+ componentDidUpdate: function () {
+ var keysToEnter = this.keysToEnter;
+ this.keysToEnter = [];
+ keysToEnter.forEach(this.performEnter);
+
+ var keysToLeave = this.keysToLeave;
+ this.keysToLeave = [];
+ keysToLeave.forEach(this.performLeave);
+ },
+
+ performAppear: function (key) {
+ this.currentlyTransitioningKeys[key] = true;
+
+ var component = this.refs[key];
+
+ if (component.componentWillAppear) {
+ component.componentWillAppear(this._handleDoneAppearing.bind(this, key));
+ } else {
+ this._handleDoneAppearing(key);
+ }
+ },
+
+ _handleDoneAppearing: function (key) {
+ var component = this.refs[key];
+ if (component.componentDidAppear) {
+ component.componentDidAppear();
+ }
+
+ delete this.currentlyTransitioningKeys[key];
+
+ var currentChildMapping = ReactTransitionChildMapping.getChildMapping(this.props.children);
+
+ if (!currentChildMapping || !currentChildMapping.hasOwnProperty(key)) {
+ // This was removed before it had fully appeared. Remove it.
+ this.performLeave(key);
+ }
+ },
+
+ performEnter: function (key) {
+ this.currentlyTransitioningKeys[key] = true;
+
+ var component = this.refs[key];
+
+ if (component.componentWillEnter) {
+ component.componentWillEnter(this._handleDoneEntering.bind(this, key));
+ } else {
+ this._handleDoneEntering(key);
+ }
+ },
+
+ _handleDoneEntering: function (key) {
+ var component = this.refs[key];
+ if (component.componentDidEnter) {
+ component.componentDidEnter();
+ }
+
+ delete this.currentlyTransitioningKeys[key];
+
+ var currentChildMapping = ReactTransitionChildMapping.getChildMapping(this.props.children);
+
+ if (!currentChildMapping || !currentChildMapping.hasOwnProperty(key)) {
+ // This was removed before it had fully entered. Remove it.
+ this.performLeave(key);
+ }
+ },
+
+ performLeave: function (key) {
+ this.currentlyTransitioningKeys[key] = true;
+
+ var component = this.refs[key];
+ if (component.componentWillLeave) {
+ component.componentWillLeave(this._handleDoneLeaving.bind(this, key));
+ } else {
+ // Note that this is somewhat dangerous b/c it calls setState()
+ // again, effectively mutating the component before all the work
+ // is done.
+ this._handleDoneLeaving(key);
+ }
+ },
+
+ _handleDoneLeaving: function (key) {
+ var component = this.refs[key];
+
+ if (component.componentDidLeave) {
+ component.componentDidLeave();
+ }
+
+ delete this.currentlyTransitioningKeys[key];
+
+ var currentChildMapping = ReactTransitionChildMapping.getChildMapping(this.props.children);
+
+ if (currentChildMapping && currentChildMapping.hasOwnProperty(key)) {
+ // This entered again before it fully left. Add it again.
+ this.performEnter(key);
+ } else {
+ this.setState(function (state) {
+ var newChildren = assign({}, state.children);
+ delete newChildren[key];
+ return { children: newChildren };
+ });
+ }
+ },
+
+ render: function () {
+ // TODO: we could get rid of the need for the wrapper node
+ // by cloning a single child
+ var childrenToRender = [];
+ for (var key in this.state.children) {
+ var child = this.state.children[key];
+ if (child) {
+ // You may need to apply reactive updates to a child as it is leaving.
+ // The normal React way to do it won't work since the child will have
+ // already been removed. In case you need this behavior you can provide
+ // a childFactory function to wrap every child, even the ones that are
+ // leaving.
+ childrenToRender.push(React.cloneElement(this.props.childFactory(child), { ref: key, key: key }));
+ }
+ }
+ return React.createElement(this.props.component, this.props, childrenToRender);
+ }
+});
+
+module.exports = ReactTransitionGroup;
+},{"153":153,"24":24,"26":26,"92":92}],95:[function(_dereq_,module,exports){
+/**
+ * Copyright 2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactUpdateQueue
+ */
+
+'use strict';
+
+var ReactCurrentOwner = _dereq_(39);
+var ReactElement = _dereq_(57);
+var ReactInstanceMap = _dereq_(68);
+var ReactUpdates = _dereq_(96);
+
+var assign = _dereq_(24);
+var invariant = _dereq_(161);
+var warning = _dereq_(173);
+
+function enqueueUpdate(internalInstance) {
+ ReactUpdates.enqueueUpdate(internalInstance);
+}
+
+function getInternalInstanceReadyForUpdate(publicInstance, callerName) {
+ var internalInstance = ReactInstanceMap.get(publicInstance);
+ if (!internalInstance) {
+ if ("development" !== 'production') {
+ // Only warn when we have a callerName. Otherwise we should be silent.
+ // We're probably calling from enqueueCallback. We don't want to warn
+ // there because we already warned for the corresponding lifecycle method.
+ "development" !== 'production' ? warning(!callerName, '%s(...): Can only update a mounted or mounting component. ' + 'This usually means you called %s() on an unmounted component. ' + 'This is a no-op. Please check the code for the %s component.', callerName, callerName, publicInstance.constructor.displayName) : undefined;
+ }
+ return null;
+ }
+
+ if ("development" !== 'production') {
+ "development" !== 'production' ? warning(ReactCurrentOwner.current == null, '%s(...): Cannot update during an existing state transition ' + '(such as within `render`). Render methods should be a pure function ' + 'of props and state.', callerName) : undefined;
+ }
+
+ return internalInstance;
+}
+
+/**
+ * ReactUpdateQueue allows for state updates to be scheduled into a later
+ * reconciliation step.
+ */
+var ReactUpdateQueue = {
+
+ /**
+ * Checks whether or not this composite component is mounted.
+ * @param {ReactClass} publicInstance The instance we want to test.
+ * @return {boolean} True if mounted, false otherwise.
+ * @protected
+ * @final
+ */
+ isMounted: function (publicInstance) {
+ if ("development" !== 'production') {
+ var owner = ReactCurrentOwner.current;
+ if (owner !== null) {
+ "development" !== 'production' ? warning(owner._warnedAboutRefsInRender, '%s is accessing isMounted inside its render() function. ' + 'render() should be a pure function of props and state. It should ' + 'never access something that requires stale data from the previous ' + 'render, such as refs. Move this logic to componentDidMount and ' + 'componentDidUpdate instead.', owner.getName() || 'A component') : undefined;
+ owner._warnedAboutRefsInRender = true;
+ }
+ }
+ var internalInstance = ReactInstanceMap.get(publicInstance);
+ if (internalInstance) {
+ // During componentWillMount and render this will still be null but after
+ // that will always render to something. At least for now. So we can use
+ // this hack.
+ return !!internalInstance._renderedComponent;
+ } else {
+ return false;
+ }
+ },
+
+ /**
+ * Enqueue a callback that will be executed after all the pending updates
+ * have processed.
+ *
+ * @param {ReactClass} publicInstance The instance to use as `this` context.
+ * @param {?function} callback Called after state is updated.
+ * @internal
+ */
+ enqueueCallback: function (publicInstance, callback) {
+ !(typeof callback === 'function') ? "development" !== 'production' ? invariant(false, 'enqueueCallback(...): You called `setProps`, `replaceProps`, ' + '`setState`, `replaceState`, or `forceUpdate` with a callback that ' + 'isn\'t callable.') : invariant(false) : undefined;
+ var internalInstance = getInternalInstanceReadyForUpdate(publicInstance);
+
+ // Previously we would throw an error if we didn't have an internal
+ // instance. Since we want to make it a no-op instead, we mirror the same
+ // behavior we have in other enqueue* methods.
+ // We also need to ignore callbacks in componentWillMount. See
+ // enqueueUpdates.
+ if (!internalInstance) {
+ return null;
+ }
+
+ if (internalInstance._pendingCallbacks) {
+ internalInstance._pendingCallbacks.push(callback);
+ } else {
+ internalInstance._pendingCallbacks = [callback];
+ }
+ // TODO: The callback here is ignored when setState is called from
+ // componentWillMount. Either fix it or disallow doing so completely in
+ // favor of getInitialState. Alternatively, we can disallow
+ // componentWillMount during server-side rendering.
+ enqueueUpdate(internalInstance);
+ },
+
+ enqueueCallbackInternal: function (internalInstance, callback) {
+ !(typeof callback === 'function') ? "development" !== 'production' ? invariant(false, 'enqueueCallback(...): You called `setProps`, `replaceProps`, ' + '`setState`, `replaceState`, or `forceUpdate` with a callback that ' + 'isn\'t callable.') : invariant(false) : undefined;
+ if (internalInstance._pendingCallbacks) {
+ internalInstance._pendingCallbacks.push(callback);
+ } else {
+ internalInstance._pendingCallbacks = [callback];
+ }
+ enqueueUpdate(internalInstance);
+ },
+
+ /**
+ * Forces an update. This should only be invoked when it is known with
+ * certainty that we are **not** in a DOM transaction.
+ *
+ * You may want to call this when you know that some deeper aspect of the
+ * component's state has changed but `setState` was not called.
+ *
+ * This will not invoke `shouldComponentUpdate`, but it will invoke
+ * `componentWillUpdate` and `componentDidUpdate`.
+ *
+ * @param {ReactClass} publicInstance The instance that should rerender.
+ * @internal
+ */
+ enqueueForceUpdate: function (publicInstance) {
+ var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'forceUpdate');
+
+ if (!internalInstance) {
+ return;
+ }
+
+ internalInstance._pendingForceUpdate = true;
+
+ enqueueUpdate(internalInstance);
+ },
+
+ /**
+ * Replaces all of the state. Always use this or `setState` to mutate state.
+ * You should treat `this.state` as immutable.
+ *
+ * There is no guarantee that `this.state` will be immediately updated, so
+ * accessing `this.state` after calling this method may return the old value.
+ *
+ * @param {ReactClass} publicInstance The instance that should rerender.
+ * @param {object} completeState Next state.
+ * @internal
+ */
+ enqueueReplaceState: function (publicInstance, completeState) {
+ var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'replaceState');
+
+ if (!internalInstance) {
+ return;
+ }
+
+ internalInstance._pendingStateQueue = [completeState];
+ internalInstance._pendingReplaceState = true;
+
+ enqueueUpdate(internalInstance);
+ },
+
+ /**
+ * Sets a subset of the state. This only exists because _pendingState is
+ * internal. This provides a merging strategy that is not available to deep
+ * properties which is confusing. TODO: Expose pendingState or don't use it
+ * during the merge.
+ *
+ * @param {ReactClass} publicInstance The instance that should rerender.
+ * @param {object} partialState Next partial state to be merged with state.
+ * @internal
+ */
+ enqueueSetState: function (publicInstance, partialState) {
+ var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');
+
+ if (!internalInstance) {
+ return;
+ }
+
+ var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
+ queue.push(partialState);
+
+ enqueueUpdate(internalInstance);
+ },
+
+ /**
+ * Sets a subset of the props.
+ *
+ * @param {ReactClass} publicInstance The instance that should rerender.
+ * @param {object} partialProps Subset of the next props.
+ * @internal
+ */
+ enqueueSetProps: function (publicInstance, partialProps) {
+ var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setProps');
+ if (!internalInstance) {
+ return;
+ }
+ ReactUpdateQueue.enqueueSetPropsInternal(internalInstance, partialProps);
+ },
+
+ enqueueSetPropsInternal: function (internalInstance, partialProps) {
+ var topLevelWrapper = internalInstance._topLevelWrapper;
+ !topLevelWrapper ? "development" !== 'production' ? invariant(false, 'setProps(...): You called `setProps` on a ' + 'component with a parent. This is an anti-pattern since props will ' + 'get reactively updated when rendered. Instead, change the owner\'s ' + '`render` method to pass the correct value as props to the component ' + 'where it is created.') : invariant(false) : undefined;
+
+ // Merge with the pending element if it exists, otherwise with existing
+ // element props.
+ var wrapElement = topLevelWrapper._pendingElement || topLevelWrapper._currentElement;
+ var element = wrapElement.props;
+ var props = assign({}, element.props, partialProps);
+ topLevelWrapper._pendingElement = ReactElement.cloneAndReplaceProps(wrapElement, ReactElement.cloneAndReplaceProps(element, props));
+
+ enqueueUpdate(topLevelWrapper);
+ },
+
+ /**
+ * Replaces all of the props.
+ *
+ * @param {ReactClass} publicInstance The instance that should rerender.
+ * @param {object} props New props.
+ * @internal
+ */
+ enqueueReplaceProps: function (publicInstance, props) {
+ var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'replaceProps');
+ if (!internalInstance) {
+ return;
+ }
+ ReactUpdateQueue.enqueueReplacePropsInternal(internalInstance, props);
+ },
+
+ enqueueReplacePropsInternal: function (internalInstance, props) {
+ var topLevelWrapper = internalInstance._topLevelWrapper;
+ !topLevelWrapper ? "development" !== 'production' ? invariant(false, 'replaceProps(...): You called `replaceProps` on a ' + 'component with a parent. This is an anti-pattern since props will ' + 'get reactively updated when rendered. Instead, change the owner\'s ' + '`render` method to pass the correct value as props to the component ' + 'where it is created.') : invariant(false) : undefined;
+
+ // Merge with the pending element if it exists, otherwise with existing
+ // element props.
+ var wrapElement = topLevelWrapper._pendingElement || topLevelWrapper._currentElement;
+ var element = wrapElement.props;
+ topLevelWrapper._pendingElement = ReactElement.cloneAndReplaceProps(wrapElement, ReactElement.cloneAndReplaceProps(element, props));
+
+ enqueueUpdate(topLevelWrapper);
+ },
+
+ enqueueElementInternal: function (internalInstance, newElement) {
+ internalInstance._pendingElement = newElement;
+ enqueueUpdate(internalInstance);
+ }
+
+};
+
+module.exports = ReactUpdateQueue;
+},{"161":161,"173":173,"24":24,"39":39,"57":57,"68":68,"96":96}],96:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactUpdates
+ */
+
+'use strict';
+
+var CallbackQueue = _dereq_(6);
+var PooledClass = _dereq_(25);
+var ReactPerf = _dereq_(78);
+var ReactReconciler = _dereq_(84);
+var Transaction = _dereq_(113);
+
+var assign = _dereq_(24);
+var invariant = _dereq_(161);
+
+var dirtyComponents = [];
+var asapCallbackQueue = CallbackQueue.getPooled();
+var asapEnqueued = false;
+
+var batchingStrategy = null;
+
+function ensureInjected() {
+ !(ReactUpdates.ReactReconcileTransaction && batchingStrategy) ? "development" !== 'production' ? invariant(false, 'ReactUpdates: must inject a reconcile transaction class and batching ' + 'strategy') : invariant(false) : undefined;
+}
+
+var NESTED_UPDATES = {
+ initialize: function () {
+ this.dirtyComponentsLength = dirtyComponents.length;
+ },
+ close: function () {
+ if (this.dirtyComponentsLength !== dirtyComponents.length) {
+ // Additional updates were enqueued by componentDidUpdate handlers or
+ // similar; before our own UPDATE_QUEUEING wrapper closes, we want to run
+ // these new updates so that if A's componentDidUpdate calls setState on
+ // B, B will update before the callback A's updater provided when calling
+ // setState.
+ dirtyComponents.splice(0, this.dirtyComponentsLength);
+ flushBatchedUpdates();
+ } else {
+ dirtyComponents.length = 0;
+ }
+ }
+};
+
+var UPDATE_QUEUEING = {
+ initialize: function () {
+ this.callbackQueue.reset();
+ },
+ close: function () {
+ this.callbackQueue.notifyAll();
+ }
+};
+
+var TRANSACTION_WRAPPERS = [NESTED_UPDATES, UPDATE_QUEUEING];
+
+function ReactUpdatesFlushTransaction() {
+ this.reinitializeTransaction();
+ this.dirtyComponentsLength = null;
+ this.callbackQueue = CallbackQueue.getPooled();
+ this.reconcileTransaction = ReactUpdates.ReactReconcileTransaction.getPooled( /* forceHTML */false);
+}
+
+assign(ReactUpdatesFlushTransaction.prototype, Transaction.Mixin, {
+ getTransactionWrappers: function () {
+ return TRANSACTION_WRAPPERS;
+ },
+
+ destructor: function () {
+ this.dirtyComponentsLength = null;
+ CallbackQueue.release(this.callbackQueue);
+ this.callbackQueue = null;
+ ReactUpdates.ReactReconcileTransaction.release(this.reconcileTransaction);
+ this.reconcileTransaction = null;
+ },
+
+ perform: function (method, scope, a) {
+ // Essentially calls `this.reconcileTransaction.perform(method, scope, a)`
+ // with this transaction's wrappers around it.
+ return Transaction.Mixin.perform.call(this, this.reconcileTransaction.perform, this.reconcileTransaction, method, scope, a);
+ }
+});
+
+PooledClass.addPoolingTo(ReactUpdatesFlushTransaction);
+
+function batchedUpdates(callback, a, b, c, d, e) {
+ ensureInjected();
+ batchingStrategy.batchedUpdates(callback, a, b, c, d, e);
+}
+
+/**
+ * Array comparator for ReactComponents by mount ordering.
+ *
+ * @param {ReactComponent} c1 first component you're comparing
+ * @param {ReactComponent} c2 second component you're comparing
+ * @return {number} Return value usable by Array.prototype.sort().
+ */
+function mountOrderComparator(c1, c2) {
+ return c1._mountOrder - c2._mountOrder;
+}
+
+function runBatchedUpdates(transaction) {
+ var len = transaction.dirtyComponentsLength;
+ !(len === dirtyComponents.length) ? "development" !== 'production' ? invariant(false, 'Expected flush transaction\'s stored dirty-components length (%s) to ' + 'match dirty-components array length (%s).', len, dirtyComponents.length) : invariant(false) : undefined;
+
+ // Since reconciling a component higher in the owner hierarchy usually (not
+ // always -- see shouldComponentUpdate()) will reconcile children, reconcile
+ // them before their children by sorting the array.
+ dirtyComponents.sort(mountOrderComparator);
+
+ for (var i = 0; i < len; i++) {
+ // If a component is unmounted before pending changes apply, it will still
+ // be here, but we assume that it has cleared its _pendingCallbacks and
+ // that performUpdateIfNecessary is a noop.
+ var component = dirtyComponents[i];
+
+ // If performUpdateIfNecessary happens to enqueue any new updates, we
+ // shouldn't execute the callbacks until the next render happens, so
+ // stash the callbacks first
+ var callbacks = component._pendingCallbacks;
+ component._pendingCallbacks = null;
+
+ ReactReconciler.performUpdateIfNecessary(component, transaction.reconcileTransaction);
+
+ if (callbacks) {
+ for (var j = 0; j < callbacks.length; j++) {
+ transaction.callbackQueue.enqueue(callbacks[j], component.getPublicInstance());
+ }
+ }
+ }
+}
+
+var flushBatchedUpdates = function () {
+ // ReactUpdatesFlushTransaction's wrappers will clear the dirtyComponents
+ // array and perform any updates enqueued by mount-ready handlers (i.e.,
+ // componentDidUpdate) but we need to check here too in order to catch
+ // updates enqueued by setState callbacks and asap calls.
+ while (dirtyComponents.length || asapEnqueued) {
+ if (dirtyComponents.length) {
+ var transaction = ReactUpdatesFlushTransaction.getPooled();
+ transaction.perform(runBatchedUpdates, null, transaction);
+ ReactUpdatesFlushTransaction.release(transaction);
+ }
+
+ if (asapEnqueued) {
+ asapEnqueued = false;
+ var queue = asapCallbackQueue;
+ asapCallbackQueue = CallbackQueue.getPooled();
+ queue.notifyAll();
+ CallbackQueue.release(queue);
+ }
+ }
+};
+flushBatchedUpdates = ReactPerf.measure('ReactUpdates', 'flushBatchedUpdates', flushBatchedUpdates);
+
+/**
+ * Mark a component as needing a rerender, adding an optional callback to a
+ * list of functions which will be executed once the rerender occurs.
+ */
+function enqueueUpdate(component) {
+ ensureInjected();
+
+ // Various parts of our code (such as ReactCompositeComponent's
+ // _renderValidatedComponent) assume that calls to render aren't nested;
+ // verify that that's the case. (This is called by each top-level update
+ // function, like setProps, setState, forceUpdate, etc.; creation and
+ // destruction of top-level components is guarded in ReactMount.)
+
+ if (!batchingStrategy.isBatchingUpdates) {
+ batchingStrategy.batchedUpdates(enqueueUpdate, component);
+ return;
+ }
+
+ dirtyComponents.push(component);
+}
+
+/**
+ * Enqueue a callback to be run at the end of the current batching cycle. Throws
+ * if no updates are currently being performed.
+ */
+function asap(callback, context) {
+ !batchingStrategy.isBatchingUpdates ? "development" !== 'production' ? invariant(false, 'ReactUpdates.asap: Can\'t enqueue an asap callback in a context where' + 'updates are not being batched.') : invariant(false) : undefined;
+ asapCallbackQueue.enqueue(callback, context);
+ asapEnqueued = true;
+}
+
+var ReactUpdatesInjection = {
+ injectReconcileTransaction: function (ReconcileTransaction) {
+ !ReconcileTransaction ? "development" !== 'production' ? invariant(false, 'ReactUpdates: must provide a reconcile transaction class') : invariant(false) : undefined;
+ ReactUpdates.ReactReconcileTransaction = ReconcileTransaction;
+ },
+
+ injectBatchingStrategy: function (_batchingStrategy) {
+ !_batchingStrategy ? "development" !== 'production' ? invariant(false, 'ReactUpdates: must provide a batching strategy') : invariant(false) : undefined;
+ !(typeof _batchingStrategy.batchedUpdates === 'function') ? "development" !== 'production' ? invariant(false, 'ReactUpdates: must provide a batchedUpdates() function') : invariant(false) : undefined;
+ !(typeof _batchingStrategy.isBatchingUpdates === 'boolean') ? "development" !== 'production' ? invariant(false, 'ReactUpdates: must provide an isBatchingUpdates boolean attribute') : invariant(false) : undefined;
+ batchingStrategy = _batchingStrategy;
+ }
+};
+
+var ReactUpdates = {
+ /**
+ * React references `ReactReconcileTransaction` using this property in order
+ * to allow dependency injection.
+ *
+ * @internal
+ */
+ ReactReconcileTransaction: null,
+
+ batchedUpdates: batchedUpdates,
+ enqueueUpdate: enqueueUpdate,
+ flushBatchedUpdates: flushBatchedUpdates,
+ injection: ReactUpdatesInjection,
+ asap: asap
+};
+
+module.exports = ReactUpdates;
+},{"113":113,"161":161,"24":24,"25":25,"6":6,"78":78,"84":84}],97:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactVersion
+ */
+
+'use strict';
+
+module.exports = '0.14.6';
+},{}],98:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule SVGDOMPropertyConfig
+ */
+
+'use strict';
+
+var DOMProperty = _dereq_(10);
+
+var MUST_USE_ATTRIBUTE = DOMProperty.injection.MUST_USE_ATTRIBUTE;
+
+var NS = {
+ xlink: 'http://www.w3.org/1999/xlink',
+ xml: 'http://www.w3.org/XML/1998/namespace'
+};
+
+var SVGDOMPropertyConfig = {
+ Properties: {
+ clipPath: MUST_USE_ATTRIBUTE,
+ cx: MUST_USE_ATTRIBUTE,
+ cy: MUST_USE_ATTRIBUTE,
+ d: MUST_USE_ATTRIBUTE,
+ dx: MUST_USE_ATTRIBUTE,
+ dy: MUST_USE_ATTRIBUTE,
+ fill: MUST_USE_ATTRIBUTE,
+ fillOpacity: MUST_USE_ATTRIBUTE,
+ fontFamily: MUST_USE_ATTRIBUTE,
+ fontSize: MUST_USE_ATTRIBUTE,
+ fx: MUST_USE_ATTRIBUTE,
+ fy: MUST_USE_ATTRIBUTE,
+ gradientTransform: MUST_USE_ATTRIBUTE,
+ gradientUnits: MUST_USE_ATTRIBUTE,
+ markerEnd: MUST_USE_ATTRIBUTE,
+ markerMid: MUST_USE_ATTRIBUTE,
+ markerStart: MUST_USE_ATTRIBUTE,
+ offset: MUST_USE_ATTRIBUTE,
+ opacity: MUST_USE_ATTRIBUTE,
+ patternContentUnits: MUST_USE_ATTRIBUTE,
+ patternUnits: MUST_USE_ATTRIBUTE,
+ points: MUST_USE_ATTRIBUTE,
+ preserveAspectRatio: MUST_USE_ATTRIBUTE,
+ r: MUST_USE_ATTRIBUTE,
+ rx: MUST_USE_ATTRIBUTE,
+ ry: MUST_USE_ATTRIBUTE,
+ spreadMethod: MUST_USE_ATTRIBUTE,
+ stopColor: MUST_USE_ATTRIBUTE,
+ stopOpacity: MUST_USE_ATTRIBUTE,
+ stroke: MUST_USE_ATTRIBUTE,
+ strokeDasharray: MUST_USE_ATTRIBUTE,
+ strokeLinecap: MUST_USE_ATTRIBUTE,
+ strokeOpacity: MUST_USE_ATTRIBUTE,
+ strokeWidth: MUST_USE_ATTRIBUTE,
+ textAnchor: MUST_USE_ATTRIBUTE,
+ transform: MUST_USE_ATTRIBUTE,
+ version: MUST_USE_ATTRIBUTE,
+ viewBox: MUST_USE_ATTRIBUTE,
+ x1: MUST_USE_ATTRIBUTE,
+ x2: MUST_USE_ATTRIBUTE,
+ x: MUST_USE_ATTRIBUTE,
+ xlinkActuate: MUST_USE_ATTRIBUTE,
+ xlinkArcrole: MUST_USE_ATTRIBUTE,
+ xlinkHref: MUST_USE_ATTRIBUTE,
+ xlinkRole: MUST_USE_ATTRIBUTE,
+ xlinkShow: MUST_USE_ATTRIBUTE,
+ xlinkTitle: MUST_USE_ATTRIBUTE,
+ xlinkType: MUST_USE_ATTRIBUTE,
+ xmlBase: MUST_USE_ATTRIBUTE,
+ xmlLang: MUST_USE_ATTRIBUTE,
+ xmlSpace: MUST_USE_ATTRIBUTE,
+ y1: MUST_USE_ATTRIBUTE,
+ y2: MUST_USE_ATTRIBUTE,
+ y: MUST_USE_ATTRIBUTE
+ },
+ DOMAttributeNamespaces: {
+ xlinkActuate: NS.xlink,
+ xlinkArcrole: NS.xlink,
+ xlinkHref: NS.xlink,
+ xlinkRole: NS.xlink,
+ xlinkShow: NS.xlink,
+ xlinkTitle: NS.xlink,
+ xlinkType: NS.xlink,
+ xmlBase: NS.xml,
+ xmlLang: NS.xml,
+ xmlSpace: NS.xml
+ },
+ DOMAttributeNames: {
+ clipPath: 'clip-path',
+ fillOpacity: 'fill-opacity',
+ fontFamily: 'font-family',
+ fontSize: 'font-size',
+ gradientTransform: 'gradientTransform',
+ gradientUnits: 'gradientUnits',
+ markerEnd: 'marker-end',
+ markerMid: 'marker-mid',
+ markerStart: 'marker-start',
+ patternContentUnits: 'patternContentUnits',
+ patternUnits: 'patternUnits',
+ preserveAspectRatio: 'preserveAspectRatio',
+ spreadMethod: 'spreadMethod',
+ stopColor: 'stop-color',
+ stopOpacity: 'stop-opacity',
+ strokeDasharray: 'stroke-dasharray',
+ strokeLinecap: 'stroke-linecap',
+ strokeOpacity: 'stroke-opacity',
+ strokeWidth: 'stroke-width',
+ textAnchor: 'text-anchor',
+ viewBox: 'viewBox',
+ xlinkActuate: 'xlink:actuate',
+ xlinkArcrole: 'xlink:arcrole',
+ xlinkHref: 'xlink:href',
+ xlinkRole: 'xlink:role',
+ xlinkShow: 'xlink:show',
+ xlinkTitle: 'xlink:title',
+ xlinkType: 'xlink:type',
+ xmlBase: 'xml:base',
+ xmlLang: 'xml:lang',
+ xmlSpace: 'xml:space'
+ }
+};
+
+module.exports = SVGDOMPropertyConfig;
+},{"10":10}],99:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule SelectEventPlugin
+ */
+
+'use strict';
+
+var EventConstants = _dereq_(15);
+var EventPropagators = _dereq_(19);
+var ExecutionEnvironment = _dereq_(147);
+var ReactInputSelection = _dereq_(66);
+var SyntheticEvent = _dereq_(105);
+
+var getActiveElement = _dereq_(156);
+var isTextInputElement = _dereq_(134);
+var keyOf = _dereq_(166);
+var shallowEqual = _dereq_(171);
+
+var topLevelTypes = EventConstants.topLevelTypes;
+
+var skipSelectionChangeEvent = ExecutionEnvironment.canUseDOM && 'documentMode' in document && document.documentMode <= 11;
+
+var eventTypes = {
+ select: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onSelect: null }),
+ captured: keyOf({ onSelectCapture: null })
+ },
+ dependencies: [topLevelTypes.topBlur, topLevelTypes.topContextMenu, topLevelTypes.topFocus, topLevelTypes.topKeyDown, topLevelTypes.topMouseDown, topLevelTypes.topMouseUp, topLevelTypes.topSelectionChange]
+ }
+};
+
+var activeElement = null;
+var activeElementID = null;
+var lastSelection = null;
+var mouseDown = false;
+
+// Track whether a listener exists for this plugin. If none exist, we do
+// not extract events.
+var hasListener = false;
+var ON_SELECT_KEY = keyOf({ onSelect: null });
+
+/**
+ * Get an object which is a unique representation of the current selection.
+ *
+ * The return value will not be consistent across nodes or browsers, but
+ * two identical selections on the same node will return identical objects.
+ *
+ * @param {DOMElement} node
+ * @return {object}
+ */
+function getSelection(node) {
+ if ('selectionStart' in node && ReactInputSelection.hasSelectionCapabilities(node)) {
+ return {
+ start: node.selectionStart,
+ end: node.selectionEnd
+ };
+ } else if (window.getSelection) {
+ var selection = window.getSelection();
+ return {
+ anchorNode: selection.anchorNode,
+ anchorOffset: selection.anchorOffset,
+ focusNode: selection.focusNode,
+ focusOffset: selection.focusOffset
+ };
+ } else if (document.selection) {
+ var range = document.selection.createRange();
+ return {
+ parentElement: range.parentElement(),
+ text: range.text,
+ top: range.boundingTop,
+ left: range.boundingLeft
+ };
+ }
+}
+
+/**
+ * Poll selection to see whether it's changed.
+ *
+ * @param {object} nativeEvent
+ * @return {?SyntheticEvent}
+ */
+function constructSelectEvent(nativeEvent, nativeEventTarget) {
+ // Ensure we have the right element, and that the user is not dragging a
+ // selection (this matches native `select` event behavior). In HTML5, select
+ // fires only on input and textarea thus if there's no focused element we
+ // won't dispatch.
+ if (mouseDown || activeElement == null || activeElement !== getActiveElement()) {
+ return null;
+ }
+
+ // Only fire when selection has actually changed.
+ var currentSelection = getSelection(activeElement);
+ if (!lastSelection || !shallowEqual(lastSelection, currentSelection)) {
+ lastSelection = currentSelection;
+
+ var syntheticEvent = SyntheticEvent.getPooled(eventTypes.select, activeElementID, nativeEvent, nativeEventTarget);
+
+ syntheticEvent.type = 'select';
+ syntheticEvent.target = activeElement;
+
+ EventPropagators.accumulateTwoPhaseDispatches(syntheticEvent);
+
+ return syntheticEvent;
+ }
+
+ return null;
+}
+
+/**
+ * This plugin creates an `onSelect` event that normalizes select events
+ * across form elements.
+ *
+ * Supported elements are:
+ * - input (see `isTextInputElement`)
+ * - textarea
+ * - contentEditable
+ *
+ * This differs from native browser implementations in the following ways:
+ * - Fires on contentEditable fields as well as inputs.
+ * - Fires for collapsed selection.
+ * - Fires after user input.
+ */
+var SelectEventPlugin = {
+
+ eventTypes: eventTypes,
+
+ /**
+ * @param {string} topLevelType Record from `EventConstants`.
+ * @param {DOMEventTarget} topLevelTarget The listening component root node.
+ * @param {string} topLevelTargetID ID of `topLevelTarget`.
+ * @param {object} nativeEvent Native browser event.
+ * @return {*} An accumulation of synthetic events.
+ * @see {EventPluginHub.extractEvents}
+ */
+ extractEvents: function (topLevelType, topLevelTarget, topLevelTargetID, nativeEvent, nativeEventTarget) {
+ if (!hasListener) {
+ return null;
+ }
+
+ switch (topLevelType) {
+ // Track the input node that has focus.
+ case topLevelTypes.topFocus:
+ if (isTextInputElement(topLevelTarget) || topLevelTarget.contentEditable === 'true') {
+ activeElement = topLevelTarget;
+ activeElementID = topLevelTargetID;
+ lastSelection = null;
+ }
+ break;
+ case topLevelTypes.topBlur:
+ activeElement = null;
+ activeElementID = null;
+ lastSelection = null;
+ break;
+
+ // Don't fire the event while the user is dragging. This matches the
+ // semantics of the native select event.
+ case topLevelTypes.topMouseDown:
+ mouseDown = true;
+ break;
+ case topLevelTypes.topContextMenu:
+ case topLevelTypes.topMouseUp:
+ mouseDown = false;
+ return constructSelectEvent(nativeEvent, nativeEventTarget);
+
+ // Chrome and IE fire non-standard event when selection is changed (and
+ // sometimes when it hasn't). IE's event fires out of order with respect
+ // to key and input events on deletion, so we discard it.
+ //
+ // Firefox doesn't support selectionchange, so check selection status
+ // after each key entry. The selection changes after keydown and before
+ // keyup, but we check on keydown as well in the case of holding down a
+ // key, when multiple keydown events are fired but only one keyup is.
+ // This is also our approach for IE handling, for the reason above.
+ case topLevelTypes.topSelectionChange:
+ if (skipSelectionChangeEvent) {
+ break;
+ }
+ // falls through
+ case topLevelTypes.topKeyDown:
+ case topLevelTypes.topKeyUp:
+ return constructSelectEvent(nativeEvent, nativeEventTarget);
+ }
+
+ return null;
+ },
+
+ didPutListener: function (id, registrationName, listener) {
+ if (registrationName === ON_SELECT_KEY) {
+ hasListener = true;
+ }
+ }
+};
+
+module.exports = SelectEventPlugin;
+},{"105":105,"134":134,"147":147,"15":15,"156":156,"166":166,"171":171,"19":19,"66":66}],100:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ServerReactRootIndex
+ * @typechecks
+ */
+
+'use strict';
+
+/**
+ * Size of the reactRoot ID space. We generate random numbers for React root
+ * IDs and if there's a collision the events and DOM update system will
+ * get confused. In the future we need a way to generate GUIDs but for
+ * now this will work on a smaller scale.
+ */
+var GLOBAL_MOUNT_POINT_MAX = Math.pow(2, 53);
+
+var ServerReactRootIndex = {
+ createReactRootIndex: function () {
+ return Math.ceil(Math.random() * GLOBAL_MOUNT_POINT_MAX);
+ }
+};
+
+module.exports = ServerReactRootIndex;
+},{}],101:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule SimpleEventPlugin
+ */
+
+'use strict';
+
+var EventConstants = _dereq_(15);
+var EventListener = _dereq_(146);
+var EventPropagators = _dereq_(19);
+var ReactMount = _dereq_(72);
+var SyntheticClipboardEvent = _dereq_(102);
+var SyntheticEvent = _dereq_(105);
+var SyntheticFocusEvent = _dereq_(106);
+var SyntheticKeyboardEvent = _dereq_(108);
+var SyntheticMouseEvent = _dereq_(109);
+var SyntheticDragEvent = _dereq_(104);
+var SyntheticTouchEvent = _dereq_(110);
+var SyntheticUIEvent = _dereq_(111);
+var SyntheticWheelEvent = _dereq_(112);
+
+var emptyFunction = _dereq_(153);
+var getEventCharCode = _dereq_(125);
+var invariant = _dereq_(161);
+var keyOf = _dereq_(166);
+
+var topLevelTypes = EventConstants.topLevelTypes;
+
+var eventTypes = {
+ abort: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onAbort: true }),
+ captured: keyOf({ onAbortCapture: true })
+ }
+ },
+ blur: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onBlur: true }),
+ captured: keyOf({ onBlurCapture: true })
+ }
+ },
+ canPlay: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onCanPlay: true }),
+ captured: keyOf({ onCanPlayCapture: true })
+ }
+ },
+ canPlayThrough: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onCanPlayThrough: true }),
+ captured: keyOf({ onCanPlayThroughCapture: true })
+ }
+ },
+ click: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onClick: true }),
+ captured: keyOf({ onClickCapture: true })
+ }
+ },
+ contextMenu: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onContextMenu: true }),
+ captured: keyOf({ onContextMenuCapture: true })
+ }
+ },
+ copy: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onCopy: true }),
+ captured: keyOf({ onCopyCapture: true })
+ }
+ },
+ cut: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onCut: true }),
+ captured: keyOf({ onCutCapture: true })
+ }
+ },
+ doubleClick: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onDoubleClick: true }),
+ captured: keyOf({ onDoubleClickCapture: true })
+ }
+ },
+ drag: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onDrag: true }),
+ captured: keyOf({ onDragCapture: true })
+ }
+ },
+ dragEnd: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onDragEnd: true }),
+ captured: keyOf({ onDragEndCapture: true })
+ }
+ },
+ dragEnter: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onDragEnter: true }),
+ captured: keyOf({ onDragEnterCapture: true })
+ }
+ },
+ dragExit: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onDragExit: true }),
+ captured: keyOf({ onDragExitCapture: true })
+ }
+ },
+ dragLeave: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onDragLeave: true }),
+ captured: keyOf({ onDragLeaveCapture: true })
+ }
+ },
+ dragOver: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onDragOver: true }),
+ captured: keyOf({ onDragOverCapture: true })
+ }
+ },
+ dragStart: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onDragStart: true }),
+ captured: keyOf({ onDragStartCapture: true })
+ }
+ },
+ drop: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onDrop: true }),
+ captured: keyOf({ onDropCapture: true })
+ }
+ },
+ durationChange: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onDurationChange: true }),
+ captured: keyOf({ onDurationChangeCapture: true })
+ }
+ },
+ emptied: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onEmptied: true }),
+ captured: keyOf({ onEmptiedCapture: true })
+ }
+ },
+ encrypted: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onEncrypted: true }),
+ captured: keyOf({ onEncryptedCapture: true })
+ }
+ },
+ ended: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onEnded: true }),
+ captured: keyOf({ onEndedCapture: true })
+ }
+ },
+ error: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onError: true }),
+ captured: keyOf({ onErrorCapture: true })
+ }
+ },
+ focus: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onFocus: true }),
+ captured: keyOf({ onFocusCapture: true })
+ }
+ },
+ input: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onInput: true }),
+ captured: keyOf({ onInputCapture: true })
+ }
+ },
+ keyDown: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onKeyDown: true }),
+ captured: keyOf({ onKeyDownCapture: true })
+ }
+ },
+ keyPress: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onKeyPress: true }),
+ captured: keyOf({ onKeyPressCapture: true })
+ }
+ },
+ keyUp: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onKeyUp: true }),
+ captured: keyOf({ onKeyUpCapture: true })
+ }
+ },
+ load: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onLoad: true }),
+ captured: keyOf({ onLoadCapture: true })
+ }
+ },
+ loadedData: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onLoadedData: true }),
+ captured: keyOf({ onLoadedDataCapture: true })
+ }
+ },
+ loadedMetadata: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onLoadedMetadata: true }),
+ captured: keyOf({ onLoadedMetadataCapture: true })
+ }
+ },
+ loadStart: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onLoadStart: true }),
+ captured: keyOf({ onLoadStartCapture: true })
+ }
+ },
+ // Note: We do not allow listening to mouseOver events. Instead, use the
+ // onMouseEnter/onMouseLeave created by `EnterLeaveEventPlugin`.
+ mouseDown: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onMouseDown: true }),
+ captured: keyOf({ onMouseDownCapture: true })
+ }
+ },
+ mouseMove: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onMouseMove: true }),
+ captured: keyOf({ onMouseMoveCapture: true })
+ }
+ },
+ mouseOut: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onMouseOut: true }),
+ captured: keyOf({ onMouseOutCapture: true })
+ }
+ },
+ mouseOver: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onMouseOver: true }),
+ captured: keyOf({ onMouseOverCapture: true })
+ }
+ },
+ mouseUp: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onMouseUp: true }),
+ captured: keyOf({ onMouseUpCapture: true })
+ }
+ },
+ paste: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onPaste: true }),
+ captured: keyOf({ onPasteCapture: true })
+ }
+ },
+ pause: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onPause: true }),
+ captured: keyOf({ onPauseCapture: true })
+ }
+ },
+ play: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onPlay: true }),
+ captured: keyOf({ onPlayCapture: true })
+ }
+ },
+ playing: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onPlaying: true }),
+ captured: keyOf({ onPlayingCapture: true })
+ }
+ },
+ progress: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onProgress: true }),
+ captured: keyOf({ onProgressCapture: true })
+ }
+ },
+ rateChange: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onRateChange: true }),
+ captured: keyOf({ onRateChangeCapture: true })
+ }
+ },
+ reset: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onReset: true }),
+ captured: keyOf({ onResetCapture: true })
+ }
+ },
+ scroll: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onScroll: true }),
+ captured: keyOf({ onScrollCapture: true })
+ }
+ },
+ seeked: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onSeeked: true }),
+ captured: keyOf({ onSeekedCapture: true })
+ }
+ },
+ seeking: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onSeeking: true }),
+ captured: keyOf({ onSeekingCapture: true })
+ }
+ },
+ stalled: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onStalled: true }),
+ captured: keyOf({ onStalledCapture: true })
+ }
+ },
+ submit: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onSubmit: true }),
+ captured: keyOf({ onSubmitCapture: true })
+ }
+ },
+ suspend: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onSuspend: true }),
+ captured: keyOf({ onSuspendCapture: true })
+ }
+ },
+ timeUpdate: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onTimeUpdate: true }),
+ captured: keyOf({ onTimeUpdateCapture: true })
+ }
+ },
+ touchCancel: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onTouchCancel: true }),
+ captured: keyOf({ onTouchCancelCapture: true })
+ }
+ },
+ touchEnd: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onTouchEnd: true }),
+ captured: keyOf({ onTouchEndCapture: true })
+ }
+ },
+ touchMove: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onTouchMove: true }),
+ captured: keyOf({ onTouchMoveCapture: true })
+ }
+ },
+ touchStart: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onTouchStart: true }),
+ captured: keyOf({ onTouchStartCapture: true })
+ }
+ },
+ volumeChange: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onVolumeChange: true }),
+ captured: keyOf({ onVolumeChangeCapture: true })
+ }
+ },
+ waiting: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onWaiting: true }),
+ captured: keyOf({ onWaitingCapture: true })
+ }
+ },
+ wheel: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onWheel: true }),
+ captured: keyOf({ onWheelCapture: true })
+ }
+ }
+};
+
+var topLevelEventsToDispatchConfig = {
+ topAbort: eventTypes.abort,
+ topBlur: eventTypes.blur,
+ topCanPlay: eventTypes.canPlay,
+ topCanPlayThrough: eventTypes.canPlayThrough,
+ topClick: eventTypes.click,
+ topContextMenu: eventTypes.contextMenu,
+ topCopy: eventTypes.copy,
+ topCut: eventTypes.cut,
+ topDoubleClick: eventTypes.doubleClick,
+ topDrag: eventTypes.drag,
+ topDragEnd: eventTypes.dragEnd,
+ topDragEnter: eventTypes.dragEnter,
+ topDragExit: eventTypes.dragExit,
+ topDragLeave: eventTypes.dragLeave,
+ topDragOver: eventTypes.dragOver,
+ topDragStart: eventTypes.dragStart,
+ topDrop: eventTypes.drop,
+ topDurationChange: eventTypes.durationChange,
+ topEmptied: eventTypes.emptied,
+ topEncrypted: eventTypes.encrypted,
+ topEnded: eventTypes.ended,
+ topError: eventTypes.error,
+ topFocus: eventTypes.focus,
+ topInput: eventTypes.input,
+ topKeyDown: eventTypes.keyDown,
+ topKeyPress: eventTypes.keyPress,
+ topKeyUp: eventTypes.keyUp,
+ topLoad: eventTypes.load,
+ topLoadedData: eventTypes.loadedData,
+ topLoadedMetadata: eventTypes.loadedMetadata,
+ topLoadStart: eventTypes.loadStart,
+ topMouseDown: eventTypes.mouseDown,
+ topMouseMove: eventTypes.mouseMove,
+ topMouseOut: eventTypes.mouseOut,
+ topMouseOver: eventTypes.mouseOver,
+ topMouseUp: eventTypes.mouseUp,
+ topPaste: eventTypes.paste,
+ topPause: eventTypes.pause,
+ topPlay: eventTypes.play,
+ topPlaying: eventTypes.playing,
+ topProgress: eventTypes.progress,
+ topRateChange: eventTypes.rateChange,
+ topReset: eventTypes.reset,
+ topScroll: eventTypes.scroll,
+ topSeeked: eventTypes.seeked,
+ topSeeking: eventTypes.seeking,
+ topStalled: eventTypes.stalled,
+ topSubmit: eventTypes.submit,
+ topSuspend: eventTypes.suspend,
+ topTimeUpdate: eventTypes.timeUpdate,
+ topTouchCancel: eventTypes.touchCancel,
+ topTouchEnd: eventTypes.touchEnd,
+ topTouchMove: eventTypes.touchMove,
+ topTouchStart: eventTypes.touchStart,
+ topVolumeChange: eventTypes.volumeChange,
+ topWaiting: eventTypes.waiting,
+ topWheel: eventTypes.wheel
+};
+
+for (var type in topLevelEventsToDispatchConfig) {
+ topLevelEventsToDispatchConfig[type].dependencies = [type];
+}
+
+var ON_CLICK_KEY = keyOf({ onClick: null });
+var onClickListeners = {};
+
+var SimpleEventPlugin = {
+
+ eventTypes: eventTypes,
+
+ /**
+ * @param {string} topLevelType Record from `EventConstants`.
+ * @param {DOMEventTarget} topLevelTarget The listening component root node.
+ * @param {string} topLevelTargetID ID of `topLevelTarget`.
+ * @param {object} nativeEvent Native browser event.
+ * @return {*} An accumulation of synthetic events.
+ * @see {EventPluginHub.extractEvents}
+ */
+ extractEvents: function (topLevelType, topLevelTarget, topLevelTargetID, nativeEvent, nativeEventTarget) {
+ var dispatchConfig = topLevelEventsToDispatchConfig[topLevelType];
+ if (!dispatchConfig) {
+ return null;
+ }
+ var EventConstructor;
+ switch (topLevelType) {
+ case topLevelTypes.topAbort:
+ case topLevelTypes.topCanPlay:
+ case topLevelTypes.topCanPlayThrough:
+ case topLevelTypes.topDurationChange:
+ case topLevelTypes.topEmptied:
+ case topLevelTypes.topEncrypted:
+ case topLevelTypes.topEnded:
+ case topLevelTypes.topError:
+ case topLevelTypes.topInput:
+ case topLevelTypes.topLoad:
+ case topLevelTypes.topLoadedData:
+ case topLevelTypes.topLoadedMetadata:
+ case topLevelTypes.topLoadStart:
+ case topLevelTypes.topPause:
+ case topLevelTypes.topPlay:
+ case topLevelTypes.topPlaying:
+ case topLevelTypes.topProgress:
+ case topLevelTypes.topRateChange:
+ case topLevelTypes.topReset:
+ case topLevelTypes.topSeeked:
+ case topLevelTypes.topSeeking:
+ case topLevelTypes.topStalled:
+ case topLevelTypes.topSubmit:
+ case topLevelTypes.topSuspend:
+ case topLevelTypes.topTimeUpdate:
+ case topLevelTypes.topVolumeChange:
+ case topLevelTypes.topWaiting:
+ // HTML Events
+ // @see http://www.w3.org/TR/html5/index.html#events-0
+ EventConstructor = SyntheticEvent;
+ break;
+ case topLevelTypes.topKeyPress:
+ // FireFox creates a keypress event for function keys too. This removes
+ // the unwanted keypress events. Enter is however both printable and
+ // non-printable. One would expect Tab to be as well (but it isn't).
+ if (getEventCharCode(nativeEvent) === 0) {
+ return null;
+ }
+ /* falls through */
+ case topLevelTypes.topKeyDown:
+ case topLevelTypes.topKeyUp:
+ EventConstructor = SyntheticKeyboardEvent;
+ break;
+ case topLevelTypes.topBlur:
+ case topLevelTypes.topFocus:
+ EventConstructor = SyntheticFocusEvent;
+ break;
+ case topLevelTypes.topClick:
+ // Firefox creates a click event on right mouse clicks. This removes the
+ // unwanted click events.
+ if (nativeEvent.button === 2) {
+ return null;
+ }
+ /* falls through */
+ case topLevelTypes.topContextMenu:
+ case topLevelTypes.topDoubleClick:
+ case topLevelTypes.topMouseDown:
+ case topLevelTypes.topMouseMove:
+ case topLevelTypes.topMouseOut:
+ case topLevelTypes.topMouseOver:
+ case topLevelTypes.topMouseUp:
+ EventConstructor = SyntheticMouseEvent;
+ break;
+ case topLevelTypes.topDrag:
+ case topLevelTypes.topDragEnd:
+ case topLevelTypes.topDragEnter:
+ case topLevelTypes.topDragExit:
+ case topLevelTypes.topDragLeave:
+ case topLevelTypes.topDragOver:
+ case topLevelTypes.topDragStart:
+ case topLevelTypes.topDrop:
+ EventConstructor = SyntheticDragEvent;
+ break;
+ case topLevelTypes.topTouchCancel:
+ case topLevelTypes.topTouchEnd:
+ case topLevelTypes.topTouchMove:
+ case topLevelTypes.topTouchStart:
+ EventConstructor = SyntheticTouchEvent;
+ break;
+ case topLevelTypes.topScroll:
+ EventConstructor = SyntheticUIEvent;
+ break;
+ case topLevelTypes.topWheel:
+ EventConstructor = SyntheticWheelEvent;
+ break;
+ case topLevelTypes.topCopy:
+ case topLevelTypes.topCut:
+ case topLevelTypes.topPaste:
+ EventConstructor = SyntheticClipboardEvent;
+ break;
+ }
+ !EventConstructor ? "development" !== 'production' ? invariant(false, 'SimpleEventPlugin: Unhandled event type, `%s`.', topLevelType) : invariant(false) : undefined;
+ var event = EventConstructor.getPooled(dispatchConfig, topLevelTargetID, nativeEvent, nativeEventTarget);
+ EventPropagators.accumulateTwoPhaseDispatches(event);
+ return event;
+ },
+
+ didPutListener: function (id, registrationName, listener) {
+ // Mobile Safari does not fire properly bubble click events on
+ // non-interactive elements, which means delegated click listeners do not
+ // fire. The workaround for this bug involves attaching an empty click
+ // listener on the target node.
+ if (registrationName === ON_CLICK_KEY) {
+ var node = ReactMount.getNode(id);
+ if (!onClickListeners[id]) {
+ onClickListeners[id] = EventListener.listen(node, 'click', emptyFunction);
+ }
+ }
+ },
+
+ willDeleteListener: function (id, registrationName) {
+ if (registrationName === ON_CLICK_KEY) {
+ onClickListeners[id].remove();
+ delete onClickListeners[id];
+ }
+ }
+
+};
+
+module.exports = SimpleEventPlugin;
+},{"102":102,"104":104,"105":105,"106":106,"108":108,"109":109,"110":110,"111":111,"112":112,"125":125,"146":146,"15":15,"153":153,"161":161,"166":166,"19":19,"72":72}],102:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule SyntheticClipboardEvent
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var SyntheticEvent = _dereq_(105);
+
+/**
+ * @interface Event
+ * @see http://www.w3.org/TR/clipboard-apis/
+ */
+var ClipboardEventInterface = {
+ clipboardData: function (event) {
+ return 'clipboardData' in event ? event.clipboardData : window.clipboardData;
+ }
+};
+
+/**
+ * @param {object} dispatchConfig Configuration used to dispatch this event.
+ * @param {string} dispatchMarker Marker identifying the event target.
+ * @param {object} nativeEvent Native browser event.
+ * @extends {SyntheticUIEvent}
+ */
+function SyntheticClipboardEvent(dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget) {
+ SyntheticEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget);
+}
+
+SyntheticEvent.augmentClass(SyntheticClipboardEvent, ClipboardEventInterface);
+
+module.exports = SyntheticClipboardEvent;
+},{"105":105}],103:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule SyntheticCompositionEvent
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var SyntheticEvent = _dereq_(105);
+
+/**
+ * @interface Event
+ * @see http://www.w3.org/TR/DOM-Level-3-Events/#events-compositionevents
+ */
+var CompositionEventInterface = {
+ data: null
+};
+
+/**
+ * @param {object} dispatchConfig Configuration used to dispatch this event.
+ * @param {string} dispatchMarker Marker identifying the event target.
+ * @param {object} nativeEvent Native browser event.
+ * @extends {SyntheticUIEvent}
+ */
+function SyntheticCompositionEvent(dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget) {
+ SyntheticEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget);
+}
+
+SyntheticEvent.augmentClass(SyntheticCompositionEvent, CompositionEventInterface);
+
+module.exports = SyntheticCompositionEvent;
+},{"105":105}],104:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule SyntheticDragEvent
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var SyntheticMouseEvent = _dereq_(109);
+
+/**
+ * @interface DragEvent
+ * @see http://www.w3.org/TR/DOM-Level-3-Events/
+ */
+var DragEventInterface = {
+ dataTransfer: null
+};
+
+/**
+ * @param {object} dispatchConfig Configuration used to dispatch this event.
+ * @param {string} dispatchMarker Marker identifying the event target.
+ * @param {object} nativeEvent Native browser event.
+ * @extends {SyntheticUIEvent}
+ */
+function SyntheticDragEvent(dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget) {
+ SyntheticMouseEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget);
+}
+
+SyntheticMouseEvent.augmentClass(SyntheticDragEvent, DragEventInterface);
+
+module.exports = SyntheticDragEvent;
+},{"109":109}],105:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule SyntheticEvent
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var PooledClass = _dereq_(25);
+
+var assign = _dereq_(24);
+var emptyFunction = _dereq_(153);
+var warning = _dereq_(173);
+
+/**
+ * @interface Event
+ * @see http://www.w3.org/TR/DOM-Level-3-Events/
+ */
+var EventInterface = {
+ type: null,
+ // currentTarget is set when dispatching; no use in copying it here
+ currentTarget: emptyFunction.thatReturnsNull,
+ eventPhase: null,
+ bubbles: null,
+ cancelable: null,
+ timeStamp: function (event) {
+ return event.timeStamp || Date.now();
+ },
+ defaultPrevented: null,
+ isTrusted: null
+};
+
+/**
+ * Synthetic events are dispatched by event plugins, typically in response to a
+ * top-level event delegation handler.
+ *
+ * These systems should generally use pooling to reduce the frequency of garbage
+ * collection. The system should check `isPersistent` to determine whether the
+ * event should be released into the pool after being dispatched. Users that
+ * need a persisted event should invoke `persist`.
+ *
+ * Synthetic events (and subclasses) implement the DOM Level 3 Events API by
+ * normalizing browser quirks. Subclasses do not necessarily have to implement a
+ * DOM interface; custom application-specific events can also subclass this.
+ *
+ * @param {object} dispatchConfig Configuration used to dispatch this event.
+ * @param {string} dispatchMarker Marker identifying the event target.
+ * @param {object} nativeEvent Native browser event.
+ */
+function SyntheticEvent(dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget) {
+ this.dispatchConfig = dispatchConfig;
+ this.dispatchMarker = dispatchMarker;
+ this.nativeEvent = nativeEvent;
+ this.target = nativeEventTarget;
+ this.currentTarget = nativeEventTarget;
+
+ var Interface = this.constructor.Interface;
+ for (var propName in Interface) {
+ if (!Interface.hasOwnProperty(propName)) {
+ continue;
+ }
+ var normalize = Interface[propName];
+ if (normalize) {
+ this[propName] = normalize(nativeEvent);
+ } else {
+ this[propName] = nativeEvent[propName];
+ }
+ }
+
+ var defaultPrevented = nativeEvent.defaultPrevented != null ? nativeEvent.defaultPrevented : nativeEvent.returnValue === false;
+ if (defaultPrevented) {
+ this.isDefaultPrevented = emptyFunction.thatReturnsTrue;
+ } else {
+ this.isDefaultPrevented = emptyFunction.thatReturnsFalse;
+ }
+ this.isPropagationStopped = emptyFunction.thatReturnsFalse;
+}
+
+assign(SyntheticEvent.prototype, {
+
+ preventDefault: function () {
+ this.defaultPrevented = true;
+ var event = this.nativeEvent;
+ if ("development" !== 'production') {
+ "development" !== 'production' ? warning(event, 'This synthetic event is reused for performance reasons. If you\'re ' + 'seeing this, you\'re calling `preventDefault` on a ' + 'released/nullified synthetic event. This is a no-op. See ' + 'https://fb.me/react-event-pooling for more information.') : undefined;
+ }
+ if (!event) {
+ return;
+ }
+
+ if (event.preventDefault) {
+ event.preventDefault();
+ } else {
+ event.returnValue = false;
+ }
+ this.isDefaultPrevented = emptyFunction.thatReturnsTrue;
+ },
+
+ stopPropagation: function () {
+ var event = this.nativeEvent;
+ if ("development" !== 'production') {
+ "development" !== 'production' ? warning(event, 'This synthetic event is reused for performance reasons. If you\'re ' + 'seeing this, you\'re calling `stopPropagation` on a ' + 'released/nullified synthetic event. This is a no-op. See ' + 'https://fb.me/react-event-pooling for more information.') : undefined;
+ }
+ if (!event) {
+ return;
+ }
+
+ if (event.stopPropagation) {
+ event.stopPropagation();
+ } else {
+ event.cancelBubble = true;
+ }
+ this.isPropagationStopped = emptyFunction.thatReturnsTrue;
+ },
+
+ /**
+ * We release all dispatched `SyntheticEvent`s after each event loop, adding
+ * them back into the pool. This allows a way to hold onto a reference that
+ * won't be added back into the pool.
+ */
+ persist: function () {
+ this.isPersistent = emptyFunction.thatReturnsTrue;
+ },
+
+ /**
+ * Checks if this event should be released back into the pool.
+ *
+ * @return {boolean} True if this should not be released, false otherwise.
+ */
+ isPersistent: emptyFunction.thatReturnsFalse,
+
+ /**
+ * `PooledClass` looks for `destructor` on each instance it releases.
+ */
+ destructor: function () {
+ var Interface = this.constructor.Interface;
+ for (var propName in Interface) {
+ this[propName] = null;
+ }
+ this.dispatchConfig = null;
+ this.dispatchMarker = null;
+ this.nativeEvent = null;
+ }
+
+});
+
+SyntheticEvent.Interface = EventInterface;
+
+/**
+ * Helper to reduce boilerplate when creating subclasses.
+ *
+ * @param {function} Class
+ * @param {?object} Interface
+ */
+SyntheticEvent.augmentClass = function (Class, Interface) {
+ var Super = this;
+
+ var prototype = Object.create(Super.prototype);
+ assign(prototype, Class.prototype);
+ Class.prototype = prototype;
+ Class.prototype.constructor = Class;
+
+ Class.Interface = assign({}, Super.Interface, Interface);
+ Class.augmentClass = Super.augmentClass;
+
+ PooledClass.addPoolingTo(Class, PooledClass.fourArgumentPooler);
+};
+
+PooledClass.addPoolingTo(SyntheticEvent, PooledClass.fourArgumentPooler);
+
+module.exports = SyntheticEvent;
+},{"153":153,"173":173,"24":24,"25":25}],106:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule SyntheticFocusEvent
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var SyntheticUIEvent = _dereq_(111);
+
+/**
+ * @interface FocusEvent
+ * @see http://www.w3.org/TR/DOM-Level-3-Events/
+ */
+var FocusEventInterface = {
+ relatedTarget: null
+};
+
+/**
+ * @param {object} dispatchConfig Configuration used to dispatch this event.
+ * @param {string} dispatchMarker Marker identifying the event target.
+ * @param {object} nativeEvent Native browser event.
+ * @extends {SyntheticUIEvent}
+ */
+function SyntheticFocusEvent(dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget) {
+ SyntheticUIEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget);
+}
+
+SyntheticUIEvent.augmentClass(SyntheticFocusEvent, FocusEventInterface);
+
+module.exports = SyntheticFocusEvent;
+},{"111":111}],107:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule SyntheticInputEvent
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var SyntheticEvent = _dereq_(105);
+
+/**
+ * @interface Event
+ * @see http://www.w3.org/TR/2013/WD-DOM-Level-3-Events-20131105
+ * /#events-inputevents
+ */
+var InputEventInterface = {
+ data: null
+};
+
+/**
+ * @param {object} dispatchConfig Configuration used to dispatch this event.
+ * @param {string} dispatchMarker Marker identifying the event target.
+ * @param {object} nativeEvent Native browser event.
+ * @extends {SyntheticUIEvent}
+ */
+function SyntheticInputEvent(dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget) {
+ SyntheticEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget);
+}
+
+SyntheticEvent.augmentClass(SyntheticInputEvent, InputEventInterface);
+
+module.exports = SyntheticInputEvent;
+},{"105":105}],108:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule SyntheticKeyboardEvent
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var SyntheticUIEvent = _dereq_(111);
+
+var getEventCharCode = _dereq_(125);
+var getEventKey = _dereq_(126);
+var getEventModifierState = _dereq_(127);
+
+/**
+ * @interface KeyboardEvent
+ * @see http://www.w3.org/TR/DOM-Level-3-Events/
+ */
+var KeyboardEventInterface = {
+ key: getEventKey,
+ location: null,
+ ctrlKey: null,
+ shiftKey: null,
+ altKey: null,
+ metaKey: null,
+ repeat: null,
+ locale: null,
+ getModifierState: getEventModifierState,
+ // Legacy Interface
+ charCode: function (event) {
+ // `charCode` is the result of a KeyPress event and represents the value of
+ // the actual printable character.
+
+ // KeyPress is deprecated, but its replacement is not yet final and not
+ // implemented in any major browser. Only KeyPress has charCode.
+ if (event.type === 'keypress') {
+ return getEventCharCode(event);
+ }
+ return 0;
+ },
+ keyCode: function (event) {
+ // `keyCode` is the result of a KeyDown/Up event and represents the value of
+ // physical keyboard key.
+
+ // The actual meaning of the value depends on the users' keyboard layout
+ // which cannot be detected. Assuming that it is a US keyboard layout
+ // provides a surprisingly accurate mapping for US and European users.
+ // Due to this, it is left to the user to implement at this time.
+ if (event.type === 'keydown' || event.type === 'keyup') {
+ return event.keyCode;
+ }
+ return 0;
+ },
+ which: function (event) {
+ // `which` is an alias for either `keyCode` or `charCode` depending on the
+ // type of the event.
+ if (event.type === 'keypress') {
+ return getEventCharCode(event);
+ }
+ if (event.type === 'keydown' || event.type === 'keyup') {
+ return event.keyCode;
+ }
+ return 0;
+ }
+};
+
+/**
+ * @param {object} dispatchConfig Configuration used to dispatch this event.
+ * @param {string} dispatchMarker Marker identifying the event target.
+ * @param {object} nativeEvent Native browser event.
+ * @extends {SyntheticUIEvent}
+ */
+function SyntheticKeyboardEvent(dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget) {
+ SyntheticUIEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget);
+}
+
+SyntheticUIEvent.augmentClass(SyntheticKeyboardEvent, KeyboardEventInterface);
+
+module.exports = SyntheticKeyboardEvent;
+},{"111":111,"125":125,"126":126,"127":127}],109:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule SyntheticMouseEvent
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var SyntheticUIEvent = _dereq_(111);
+var ViewportMetrics = _dereq_(114);
+
+var getEventModifierState = _dereq_(127);
+
+/**
+ * @interface MouseEvent
+ * @see http://www.w3.org/TR/DOM-Level-3-Events/
+ */
+var MouseEventInterface = {
+ screenX: null,
+ screenY: null,
+ clientX: null,
+ clientY: null,
+ ctrlKey: null,
+ shiftKey: null,
+ altKey: null,
+ metaKey: null,
+ getModifierState: getEventModifierState,
+ button: function (event) {
+ // Webkit, Firefox, IE9+
+ // which: 1 2 3
+ // button: 0 1 2 (standard)
+ var button = event.button;
+ if ('which' in event) {
+ return button;
+ }
+ // IE<9
+ // which: undefined
+ // button: 0 0 0
+ // button: 1 4 2 (onmouseup)
+ return button === 2 ? 2 : button === 4 ? 1 : 0;
+ },
+ buttons: null,
+ relatedTarget: function (event) {
+ return event.relatedTarget || (event.fromElement === event.srcElement ? event.toElement : event.fromElement);
+ },
+ // "Proprietary" Interface.
+ pageX: function (event) {
+ return 'pageX' in event ? event.pageX : event.clientX + ViewportMetrics.currentScrollLeft;
+ },
+ pageY: function (event) {
+ return 'pageY' in event ? event.pageY : event.clientY + ViewportMetrics.currentScrollTop;
+ }
+};
+
+/**
+ * @param {object} dispatchConfig Configuration used to dispatch this event.
+ * @param {string} dispatchMarker Marker identifying the event target.
+ * @param {object} nativeEvent Native browser event.
+ * @extends {SyntheticUIEvent}
+ */
+function SyntheticMouseEvent(dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget) {
+ SyntheticUIEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget);
+}
+
+SyntheticUIEvent.augmentClass(SyntheticMouseEvent, MouseEventInterface);
+
+module.exports = SyntheticMouseEvent;
+},{"111":111,"114":114,"127":127}],110:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule SyntheticTouchEvent
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var SyntheticUIEvent = _dereq_(111);
+
+var getEventModifierState = _dereq_(127);
+
+/**
+ * @interface TouchEvent
+ * @see http://www.w3.org/TR/touch-events/
+ */
+var TouchEventInterface = {
+ touches: null,
+ targetTouches: null,
+ changedTouches: null,
+ altKey: null,
+ metaKey: null,
+ ctrlKey: null,
+ shiftKey: null,
+ getModifierState: getEventModifierState
+};
+
+/**
+ * @param {object} dispatchConfig Configuration used to dispatch this event.
+ * @param {string} dispatchMarker Marker identifying the event target.
+ * @param {object} nativeEvent Native browser event.
+ * @extends {SyntheticUIEvent}
+ */
+function SyntheticTouchEvent(dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget) {
+ SyntheticUIEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget);
+}
+
+SyntheticUIEvent.augmentClass(SyntheticTouchEvent, TouchEventInterface);
+
+module.exports = SyntheticTouchEvent;
+},{"111":111,"127":127}],111:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule SyntheticUIEvent
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var SyntheticEvent = _dereq_(105);
+
+var getEventTarget = _dereq_(128);
+
+/**
+ * @interface UIEvent
+ * @see http://www.w3.org/TR/DOM-Level-3-Events/
+ */
+var UIEventInterface = {
+ view: function (event) {
+ if (event.view) {
+ return event.view;
+ }
+
+ var target = getEventTarget(event);
+ if (target != null && target.window === target) {
+ // target is a window object
+ return target;
+ }
+
+ var doc = target.ownerDocument;
+ // TODO: Figure out why `ownerDocument` is sometimes undefined in IE8.
+ if (doc) {
+ return doc.defaultView || doc.parentWindow;
+ } else {
+ return window;
+ }
+ },
+ detail: function (event) {
+ return event.detail || 0;
+ }
+};
+
+/**
+ * @param {object} dispatchConfig Configuration used to dispatch this event.
+ * @param {string} dispatchMarker Marker identifying the event target.
+ * @param {object} nativeEvent Native browser event.
+ * @extends {SyntheticEvent}
+ */
+function SyntheticUIEvent(dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget) {
+ SyntheticEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget);
+}
+
+SyntheticEvent.augmentClass(SyntheticUIEvent, UIEventInterface);
+
+module.exports = SyntheticUIEvent;
+},{"105":105,"128":128}],112:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule SyntheticWheelEvent
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var SyntheticMouseEvent = _dereq_(109);
+
+/**
+ * @interface WheelEvent
+ * @see http://www.w3.org/TR/DOM-Level-3-Events/
+ */
+var WheelEventInterface = {
+ deltaX: function (event) {
+ return 'deltaX' in event ? event.deltaX :
+ // Fallback to `wheelDeltaX` for Webkit and normalize (right is positive).
+ 'wheelDeltaX' in event ? -event.wheelDeltaX : 0;
+ },
+ deltaY: function (event) {
+ return 'deltaY' in event ? event.deltaY :
+ // Fallback to `wheelDeltaY` for Webkit and normalize (down is positive).
+ 'wheelDeltaY' in event ? -event.wheelDeltaY :
+ // Fallback to `wheelDelta` for IE<9 and normalize (down is positive).
+ 'wheelDelta' in event ? -event.wheelDelta : 0;
+ },
+ deltaZ: null,
+
+ // Browsers without "deltaMode" is reporting in raw wheel delta where one
+ // notch on the scroll is always +/- 120, roughly equivalent to pixels.
+ // A good approximation of DOM_DELTA_LINE (1) is 5% of viewport size or
+ // ~40 pixels, for DOM_DELTA_SCREEN (2) it is 87.5% of viewport size.
+ deltaMode: null
+};
+
+/**
+ * @param {object} dispatchConfig Configuration used to dispatch this event.
+ * @param {string} dispatchMarker Marker identifying the event target.
+ * @param {object} nativeEvent Native browser event.
+ * @extends {SyntheticMouseEvent}
+ */
+function SyntheticWheelEvent(dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget) {
+ SyntheticMouseEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget);
+}
+
+SyntheticMouseEvent.augmentClass(SyntheticWheelEvent, WheelEventInterface);
+
+module.exports = SyntheticWheelEvent;
+},{"109":109}],113:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule Transaction
+ */
+
+'use strict';
+
+var invariant = _dereq_(161);
+
+/**
+ * `Transaction` creates a black box that is able to wrap any method such that
+ * certain invariants are maintained before and after the method is invoked
+ * (Even if an exception is thrown while invoking the wrapped method). Whoever
+ * instantiates a transaction can provide enforcers of the invariants at
+ * creation time. The `Transaction` class itself will supply one additional
+ * automatic invariant for you - the invariant that any transaction instance
+ * should not be run while it is already being run. You would typically create a
+ * single instance of a `Transaction` for reuse multiple times, that potentially
+ * is used to wrap several different methods. Wrappers are extremely simple -
+ * they only require implementing two methods.
+ *
+ * <pre>
+ * wrappers (injected at creation time)
+ * + +
+ * | |
+ * +-----------------|--------|--------------+
+ * | v | |
+ * | +---------------+ | |
+ * | +--| wrapper1 |---|----+ |
+ * | | +---------------+ v | |
+ * | | +-------------+ | |
+ * | | +----| wrapper2 |--------+ |
+ * | | | +-------------+ | | |
+ * | | | | | |
+ * | v v v v | wrapper
+ * | +---+ +---+ +---------+ +---+ +---+ | invariants
+ * perform(anyMethod) | | | | | | | | | | | | maintained
+ * +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|-------->
+ * | | | | | | | | | | | |
+ * | | | | | | | | | | | |
+ * | | | | | | | | | | | |
+ * | +---+ +---+ +---------+ +---+ +---+ |
+ * | initialize close |
+ * +-----------------------------------------+
+ * </pre>
+ *
+ * Use cases:
+ * - Preserving the input selection ranges before/after reconciliation.
+ * Restoring selection even in the event of an unexpected error.
+ * - Deactivating events while rearranging the DOM, preventing blurs/focuses,
+ * while guaranteeing that afterwards, the event system is reactivated.
+ * - Flushing a queue of collected DOM mutations to the main UI thread after a
+ * reconciliation takes place in a worker thread.
+ * - Invoking any collected `componentDidUpdate` callbacks after rendering new
+ * content.
+ * - (Future use case): Wrapping particular flushes of the `ReactWorker` queue
+ * to preserve the `scrollTop` (an automatic scroll aware DOM).
+ * - (Future use case): Layout calculations before and after DOM updates.
+ *
+ * Transactional plugin API:
+ * - A module that has an `initialize` method that returns any precomputation.
+ * - and a `close` method that accepts the precomputation. `close` is invoked
+ * when the wrapped process is completed, or has failed.
+ *
+ * @param {Array<TransactionalWrapper>} transactionWrapper Wrapper modules
+ * that implement `initialize` and `close`.
+ * @return {Transaction} Single transaction for reuse in thread.
+ *
+ * @class Transaction
+ */
+var Mixin = {
+ /**
+ * Sets up this instance so that it is prepared for collecting metrics. Does
+ * so such that this setup method may be used on an instance that is already
+ * initialized, in a way that does not consume additional memory upon reuse.
+ * That can be useful if you decide to make your subclass of this mixin a
+ * "PooledClass".
+ */
+ reinitializeTransaction: function () {
+ this.transactionWrappers = this.getTransactionWrappers();
+ if (this.wrapperInitData) {
+ this.wrapperInitData.length = 0;
+ } else {
+ this.wrapperInitData = [];
+ }
+ this._isInTransaction = false;
+ },
+
+ _isInTransaction: false,
+
+ /**
+ * @abstract
+ * @return {Array<TransactionWrapper>} Array of transaction wrappers.
+ */
+ getTransactionWrappers: null,
+
+ isInTransaction: function () {
+ return !!this._isInTransaction;
+ },
+
+ /**
+ * Executes the function within a safety window. Use this for the top level
+ * methods that result in large amounts of computation/mutations that would
+ * need to be safety checked. The optional arguments helps prevent the need
+ * to bind in many cases.
+ *
+ * @param {function} method Member of scope to call.
+ * @param {Object} scope Scope to invoke from.
+ * @param {Object?=} a Argument to pass to the method.
+ * @param {Object?=} b Argument to pass to the method.
+ * @param {Object?=} c Argument to pass to the method.
+ * @param {Object?=} d Argument to pass to the method.
+ * @param {Object?=} e Argument to pass to the method.
+ * @param {Object?=} f Argument to pass to the method.
+ *
+ * @return {*} Return value from `method`.
+ */
+ perform: function (method, scope, a, b, c, d, e, f) {
+ !!this.isInTransaction() ? "development" !== 'production' ? invariant(false, 'Transaction.perform(...): Cannot initialize a transaction when there ' + 'is already an outstanding transaction.') : invariant(false) : undefined;
+ var errorThrown;
+ var ret;
+ try {
+ this._isInTransaction = true;
+ // Catching errors makes debugging more difficult, so we start with
+ // errorThrown set to true before setting it to false after calling
+ // close -- if it's still set to true in the finally block, it means
+ // one of these calls threw.
+ errorThrown = true;
+ this.initializeAll(0);
+ ret = method.call(scope, a, b, c, d, e, f);
+ errorThrown = false;
+ } finally {
+ try {
+ if (errorThrown) {
+ // If `method` throws, prefer to show that stack trace over any thrown
+ // by invoking `closeAll`.
+ try {
+ this.closeAll(0);
+ } catch (err) {}
+ } else {
+ // Since `method` didn't throw, we don't want to silence the exception
+ // here.
+ this.closeAll(0);
+ }
+ } finally {
+ this._isInTransaction = false;
+ }
+ }
+ return ret;
+ },
+
+ initializeAll: function (startIndex) {
+ var transactionWrappers = this.transactionWrappers;
+ for (var i = startIndex; i < transactionWrappers.length; i++) {
+ var wrapper = transactionWrappers[i];
+ try {
+ // Catching errors makes debugging more difficult, so we start with the
+ // OBSERVED_ERROR state before overwriting it with the real return value
+ // of initialize -- if it's still set to OBSERVED_ERROR in the finally
+ // block, it means wrapper.initialize threw.
+ this.wrapperInitData[i] = Transaction.OBSERVED_ERROR;
+ this.wrapperInitData[i] = wrapper.initialize ? wrapper.initialize.call(this) : null;
+ } finally {
+ if (this.wrapperInitData[i] === Transaction.OBSERVED_ERROR) {
+ // The initializer for wrapper i threw an error; initialize the
+ // remaining wrappers but silence any exceptions from them to ensure
+ // that the first error is the one to bubble up.
+ try {
+ this.initializeAll(i + 1);
+ } catch (err) {}
+ }
+ }
+ }
+ },
+
+ /**
+ * Invokes each of `this.transactionWrappers.close[i]` functions, passing into
+ * them the respective return values of `this.transactionWrappers.init[i]`
+ * (`close`rs that correspond to initializers that failed will not be
+ * invoked).
+ */
+ closeAll: function (startIndex) {
+ !this.isInTransaction() ? "development" !== 'production' ? invariant(false, 'Transaction.closeAll(): Cannot close transaction when none are open.') : invariant(false) : undefined;
+ var transactionWrappers = this.transactionWrappers;
+ for (var i = startIndex; i < transactionWrappers.length; i++) {
+ var wrapper = transactionWrappers[i];
+ var initData = this.wrapperInitData[i];
+ var errorThrown;
+ try {
+ // Catching errors makes debugging more difficult, so we start with
+ // errorThrown set to true before setting it to false after calling
+ // close -- if it's still set to true in the finally block, it means
+ // wrapper.close threw.
+ errorThrown = true;
+ if (initData !== Transaction.OBSERVED_ERROR && wrapper.close) {
+ wrapper.close.call(this, initData);
+ }
+ errorThrown = false;
+ } finally {
+ if (errorThrown) {
+ // The closer for wrapper i threw an error; close the remaining
+ // wrappers but silence any exceptions from them to ensure that the
+ // first error is the one to bubble up.
+ try {
+ this.closeAll(i + 1);
+ } catch (e) {}
+ }
+ }
+ }
+ this.wrapperInitData.length = 0;
+ }
+};
+
+var Transaction = {
+
+ Mixin: Mixin,
+
+ /**
+ * Token to look for to determine if an error occurred.
+ */
+ OBSERVED_ERROR: {}
+
+};
+
+module.exports = Transaction;
+},{"161":161}],114:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ViewportMetrics
+ */
+
+'use strict';
+
+var ViewportMetrics = {
+
+ currentScrollLeft: 0,
+
+ currentScrollTop: 0,
+
+ refreshScrollValues: function (scrollPosition) {
+ ViewportMetrics.currentScrollLeft = scrollPosition.x;
+ ViewportMetrics.currentScrollTop = scrollPosition.y;
+ }
+
+};
+
+module.exports = ViewportMetrics;
+},{}],115:[function(_dereq_,module,exports){
+/**
+ * Copyright 2014-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule accumulateInto
+ */
+
+'use strict';
+
+var invariant = _dereq_(161);
+
+/**
+ *
+ * Accumulates items that must not be null or undefined into the first one. This
+ * is used to conserve memory by avoiding array allocations, and thus sacrifices
+ * API cleanness. Since `current` can be null before being passed in and not
+ * null after this function, make sure to assign it back to `current`:
+ *
+ * `a = accumulateInto(a, b);`
+ *
+ * This API should be sparingly used. Try `accumulate` for something cleaner.
+ *
+ * @return {*|array<*>} An accumulation of items.
+ */
+
+function accumulateInto(current, next) {
+ !(next != null) ? "development" !== 'production' ? invariant(false, 'accumulateInto(...): Accumulated items must not be null or undefined.') : invariant(false) : undefined;
+ if (current == null) {
+ return next;
+ }
+
+ // Both are not empty. Warning: Never call x.concat(y) when you are not
+ // certain that x is an Array (x could be a string with concat method).
+ var currentIsArray = Array.isArray(current);
+ var nextIsArray = Array.isArray(next);
+
+ if (currentIsArray && nextIsArray) {
+ current.push.apply(current, next);
+ return current;
+ }
+
+ if (currentIsArray) {
+ current.push(next);
+ return current;
+ }
+
+ if (nextIsArray) {
+ // A bit too dangerous to mutate `next`.
+ return [current].concat(next);
+ }
+
+ return [current, next];
+}
+
+module.exports = accumulateInto;
+},{"161":161}],116:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule adler32
+ */
+
+'use strict';
+
+var MOD = 65521;
+
+// adler32 is not cryptographically strong, and is only used to sanity check that
+// markup generated on the server matches the markup generated on the client.
+// This implementation (a modified version of the SheetJS version) has been optimized
+// for our use case, at the expense of conforming to the adler32 specification
+// for non-ascii inputs.
+function adler32(data) {
+ var a = 1;
+ var b = 0;
+ var i = 0;
+ var l = data.length;
+ var m = l & ~0x3;
+ while (i < m) {
+ for (; i < Math.min(i + 4096, m); i += 4) {
+ b += (a += data.charCodeAt(i)) + (a += data.charCodeAt(i + 1)) + (a += data.charCodeAt(i + 2)) + (a += data.charCodeAt(i + 3));
+ }
+ a %= MOD;
+ b %= MOD;
+ }
+ for (; i < l; i++) {
+ b += a += data.charCodeAt(i);
+ }
+ a %= MOD;
+ b %= MOD;
+ return a | b << 16;
+}
+
+module.exports = adler32;
+},{}],117:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule canDefineProperty
+ */
+
+'use strict';
+
+var canDefineProperty = false;
+if ("development" !== 'production') {
+ try {
+ Object.defineProperty({}, 'x', { get: function () {} });
+ canDefineProperty = true;
+ } catch (x) {
+ // IE will fail on defineProperty
+ }
+}
+
+module.exports = canDefineProperty;
+},{}],118:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @typechecks static-only
+ * @providesModule cloneWithProps
+ */
+
+'use strict';
+
+var ReactElement = _dereq_(57);
+var ReactPropTransferer = _dereq_(79);
+
+var keyOf = _dereq_(166);
+var warning = _dereq_(173);
+
+var CHILDREN_PROP = keyOf({ children: null });
+
+var didDeprecatedWarn = false;
+
+/**
+ * Sometimes you want to change the props of a child passed to you. Usually
+ * this is to add a CSS class.
+ *
+ * @param {ReactElement} child child element you'd like to clone
+ * @param {object} props props you'd like to modify. className and style will be
+ * merged automatically.
+ * @return {ReactElement} a clone of child with props merged in.
+ * @deprecated
+ */
+function cloneWithProps(child, props) {
+ if ("development" !== 'production') {
+ "development" !== 'production' ? warning(didDeprecatedWarn, 'cloneWithProps(...) is deprecated. ' + 'Please use React.cloneElement instead.') : undefined;
+ didDeprecatedWarn = true;
+ "development" !== 'production' ? warning(!child.ref, 'You are calling cloneWithProps() on a child with a ref. This is ' + 'dangerous because you\'re creating a new child which will not be ' + 'added as a ref to its parent.') : undefined;
+ }
+
+ var newProps = ReactPropTransferer.mergeProps(props, child.props);
+
+ // Use `child.props.children` if it is provided.
+ if (!newProps.hasOwnProperty(CHILDREN_PROP) && child.props.hasOwnProperty(CHILDREN_PROP)) {
+ newProps.children = child.props.children;
+ }
+
+ // The current API doesn't retain _owner, which is why this
+ // doesn't use ReactElement.cloneAndReplaceProps.
+ return ReactElement.createElement(child.type, newProps);
+}
+
+module.exports = cloneWithProps;
+},{"166":166,"173":173,"57":57,"79":79}],119:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule dangerousStyleValue
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var CSSProperty = _dereq_(4);
+
+var isUnitlessNumber = CSSProperty.isUnitlessNumber;
+
+/**
+ * Convert a value into the proper css writable value. The style name `name`
+ * should be logical (no hyphens), as specified
+ * in `CSSProperty.isUnitlessNumber`.
+ *
+ * @param {string} name CSS property name such as `topMargin`.
+ * @param {*} value CSS property value such as `10px`.
+ * @return {string} Normalized style value with dimensions applied.
+ */
+function dangerousStyleValue(name, value) {
+ // Note that we've removed escapeTextForBrowser() calls here since the
+ // whole string will be escaped when the attribute is injected into
+ // the markup. If you provide unsafe user data here they can inject
+ // arbitrary CSS which may be problematic (I couldn't repro this):
+ // https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet
+ // http://www.thespanner.co.uk/2007/11/26/ultimate-xss-css-injection/
+ // This is not an XSS hole but instead a potential CSS injection issue
+ // which has lead to a greater discussion about how we're going to
+ // trust URLs moving forward. See #2115901
+
+ var isEmpty = value == null || typeof value === 'boolean' || value === '';
+ if (isEmpty) {
+ return '';
+ }
+
+ var isNonNumeric = isNaN(value);
+ if (isNonNumeric || value === 0 || isUnitlessNumber.hasOwnProperty(name) && isUnitlessNumber[name]) {
+ return '' + value; // cast to string
+ }
+
+ if (typeof value === 'string') {
+ value = value.trim();
+ }
+ return value + 'px';
+}
+
+module.exports = dangerousStyleValue;
+},{"4":4}],120:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule deprecated
+ */
+
+'use strict';
+
+var assign = _dereq_(24);
+var warning = _dereq_(173);
+
+/**
+ * This will log a single deprecation notice per function and forward the call
+ * on to the new API.
+ *
+ * @param {string} fnName The name of the function
+ * @param {string} newModule The module that fn will exist in
+ * @param {string} newPackage The module that fn will exist in
+ * @param {*} ctx The context this forwarded call should run in
+ * @param {function} fn The function to forward on to
+ * @return {function} The function that will warn once and then call fn
+ */
+function deprecated(fnName, newModule, newPackage, ctx, fn) {
+ var warned = false;
+ if ("development" !== 'production') {
+ var newFn = function () {
+ "development" !== 'production' ? warning(warned,
+ // Require examples in this string must be split to prevent React's
+ // build tools from mistaking them for real requires.
+ // Otherwise the build tools will attempt to build a '%s' module.
+ 'React.%s is deprecated. Please use %s.%s from require' + '(\'%s\') ' + 'instead.', fnName, newModule, fnName, newPackage) : undefined;
+ warned = true;
+ return fn.apply(ctx, arguments);
+ };
+ // We need to make sure all properties of the original fn are copied over.
+ // In particular, this is needed to support PropTypes
+ return assign(newFn, fn);
+ }
+
+ return fn;
+}
+
+module.exports = deprecated;
+},{"173":173,"24":24}],121:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule escapeTextContentForBrowser
+ */
+
+'use strict';
+
+var ESCAPE_LOOKUP = {
+ '&': '&amp;',
+ '>': '&gt;',
+ '<': '&lt;',
+ '"': '&quot;',
+ '\'': '&#x27;'
+};
+
+var ESCAPE_REGEX = /[&><"']/g;
+
+function escaper(match) {
+ return ESCAPE_LOOKUP[match];
+}
+
+/**
+ * Escapes text to prevent scripting attacks.
+ *
+ * @param {*} text Text value to escape.
+ * @return {string} An escaped string.
+ */
+function escapeTextContentForBrowser(text) {
+ return ('' + text).replace(ESCAPE_REGEX, escaper);
+}
+
+module.exports = escapeTextContentForBrowser;
+},{}],122:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule findDOMNode
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var ReactCurrentOwner = _dereq_(39);
+var ReactInstanceMap = _dereq_(68);
+var ReactMount = _dereq_(72);
+
+var invariant = _dereq_(161);
+var warning = _dereq_(173);
+
+/**
+ * Returns the DOM node rendered by this element.
+ *
+ * @param {ReactComponent|DOMElement} componentOrElement
+ * @return {?DOMElement} The root node of this element.
+ */
+function findDOMNode(componentOrElement) {
+ if ("development" !== 'production') {
+ var owner = ReactCurrentOwner.current;
+ if (owner !== null) {
+ "development" !== 'production' ? warning(owner._warnedAboutRefsInRender, '%s is accessing getDOMNode or findDOMNode inside its render(). ' + 'render() should be a pure function of props and state. It should ' + 'never access something that requires stale data from the previous ' + 'render, such as refs. Move this logic to componentDidMount and ' + 'componentDidUpdate instead.', owner.getName() || 'A component') : undefined;
+ owner._warnedAboutRefsInRender = true;
+ }
+ }
+ if (componentOrElement == null) {
+ return null;
+ }
+ if (componentOrElement.nodeType === 1) {
+ return componentOrElement;
+ }
+ if (ReactInstanceMap.has(componentOrElement)) {
+ return ReactMount.getNodeFromInstance(componentOrElement);
+ }
+ !(componentOrElement.render == null || typeof componentOrElement.render !== 'function') ? "development" !== 'production' ? invariant(false, 'findDOMNode was called on an unmounted component.') : invariant(false) : undefined;
+ !false ? "development" !== 'production' ? invariant(false, 'Element appears to be neither ReactComponent nor DOMNode (keys: %s)', Object.keys(componentOrElement)) : invariant(false) : undefined;
+}
+
+module.exports = findDOMNode;
+},{"161":161,"173":173,"39":39,"68":68,"72":72}],123:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule flattenChildren
+ */
+
+'use strict';
+
+var traverseAllChildren = _dereq_(142);
+var warning = _dereq_(173);
+
+/**
+ * @param {function} traverseContext Context passed through traversal.
+ * @param {?ReactComponent} child React child component.
+ * @param {!string} name String name of key path to child.
+ */
+function flattenSingleChildIntoContext(traverseContext, child, name) {
+ // We found a component instance.
+ var result = traverseContext;
+ var keyUnique = result[name] === undefined;
+ if ("development" !== 'production') {
+ "development" !== 'production' ? warning(keyUnique, 'flattenChildren(...): Encountered two children with the same key, ' + '`%s`. Child keys must be unique; when two children share a key, only ' + 'the first child will be used.', name) : undefined;
+ }
+ if (keyUnique && child != null) {
+ result[name] = child;
+ }
+}
+
+/**
+ * Flattens children that are typically specified as `props.children`. Any null
+ * children will not be included in the resulting object.
+ * @return {!object} flattened children keyed by name.
+ */
+function flattenChildren(children) {
+ if (children == null) {
+ return children;
+ }
+ var result = {};
+ traverseAllChildren(children, flattenSingleChildIntoContext, result);
+ return result;
+}
+
+module.exports = flattenChildren;
+},{"142":142,"173":173}],124:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule forEachAccumulated
+ */
+
+'use strict';
+
+/**
+ * @param {array} arr an "accumulation" of items which is either an Array or
+ * a single item. Useful when paired with the `accumulate` module. This is a
+ * simple utility that allows us to reason about a collection of items, but
+ * handling the case when there is exactly one item (and we do not need to
+ * allocate an array).
+ */
+var forEachAccumulated = function (arr, cb, scope) {
+ if (Array.isArray(arr)) {
+ arr.forEach(cb, scope);
+ } else if (arr) {
+ cb.call(scope, arr);
+ }
+};
+
+module.exports = forEachAccumulated;
+},{}],125:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule getEventCharCode
+ * @typechecks static-only
+ */
+
+'use strict';
+
+/**
+ * `charCode` represents the actual "character code" and is safe to use with
+ * `String.fromCharCode`. As such, only keys that correspond to printable
+ * characters produce a valid `charCode`, the only exception to this is Enter.
+ * The Tab-key is considered non-printable and does not have a `charCode`,
+ * presumably because it does not produce a tab-character in browsers.
+ *
+ * @param {object} nativeEvent Native browser event.
+ * @return {number} Normalized `charCode` property.
+ */
+function getEventCharCode(nativeEvent) {
+ var charCode;
+ var keyCode = nativeEvent.keyCode;
+
+ if ('charCode' in nativeEvent) {
+ charCode = nativeEvent.charCode;
+
+ // FF does not set `charCode` for the Enter-key, check against `keyCode`.
+ if (charCode === 0 && keyCode === 13) {
+ charCode = 13;
+ }
+ } else {
+ // IE8 does not implement `charCode`, but `keyCode` has the correct value.
+ charCode = keyCode;
+ }
+
+ // Some non-printable keys are reported in `charCode`/`keyCode`, discard them.
+ // Must not discard the (non-)printable Enter-key.
+ if (charCode >= 32 || charCode === 13) {
+ return charCode;
+ }
+
+ return 0;
+}
+
+module.exports = getEventCharCode;
+},{}],126:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule getEventKey
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var getEventCharCode = _dereq_(125);
+
+/**
+ * Normalization of deprecated HTML5 `key` values
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent#Key_names
+ */
+var normalizeKey = {
+ 'Esc': 'Escape',
+ 'Spacebar': ' ',
+ 'Left': 'ArrowLeft',
+ 'Up': 'ArrowUp',
+ 'Right': 'ArrowRight',
+ 'Down': 'ArrowDown',
+ 'Del': 'Delete',
+ 'Win': 'OS',
+ 'Menu': 'ContextMenu',
+ 'Apps': 'ContextMenu',
+ 'Scroll': 'ScrollLock',
+ 'MozPrintableKey': 'Unidentified'
+};
+
+/**
+ * Translation from legacy `keyCode` to HTML5 `key`
+ * Only special keys supported, all others depend on keyboard layout or browser
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent#Key_names
+ */
+var translateToKey = {
+ 8: 'Backspace',
+ 9: 'Tab',
+ 12: 'Clear',
+ 13: 'Enter',
+ 16: 'Shift',
+ 17: 'Control',
+ 18: 'Alt',
+ 19: 'Pause',
+ 20: 'CapsLock',
+ 27: 'Escape',
+ 32: ' ',
+ 33: 'PageUp',
+ 34: 'PageDown',
+ 35: 'End',
+ 36: 'Home',
+ 37: 'ArrowLeft',
+ 38: 'ArrowUp',
+ 39: 'ArrowRight',
+ 40: 'ArrowDown',
+ 45: 'Insert',
+ 46: 'Delete',
+ 112: 'F1', 113: 'F2', 114: 'F3', 115: 'F4', 116: 'F5', 117: 'F6',
+ 118: 'F7', 119: 'F8', 120: 'F9', 121: 'F10', 122: 'F11', 123: 'F12',
+ 144: 'NumLock',
+ 145: 'ScrollLock',
+ 224: 'Meta'
+};
+
+/**
+ * @param {object} nativeEvent Native browser event.
+ * @return {string} Normalized `key` property.
+ */
+function getEventKey(nativeEvent) {
+ if (nativeEvent.key) {
+ // Normalize inconsistent values reported by browsers due to
+ // implementations of a working draft specification.
+
+ // FireFox implements `key` but returns `MozPrintableKey` for all
+ // printable characters (normalized to `Unidentified`), ignore it.
+ var key = normalizeKey[nativeEvent.key] || nativeEvent.key;
+ if (key !== 'Unidentified') {
+ return key;
+ }
+ }
+
+ // Browser does not implement `key`, polyfill as much of it as we can.
+ if (nativeEvent.type === 'keypress') {
+ var charCode = getEventCharCode(nativeEvent);
+
+ // The enter-key is technically both printable and non-printable and can
+ // thus be captured by `keypress`, no other non-printable key should.
+ return charCode === 13 ? 'Enter' : String.fromCharCode(charCode);
+ }
+ if (nativeEvent.type === 'keydown' || nativeEvent.type === 'keyup') {
+ // While user keyboard layout determines the actual meaning of each
+ // `keyCode` value, almost all function keys have a universal value.
+ return translateToKey[nativeEvent.keyCode] || 'Unidentified';
+ }
+ return '';
+}
+
+module.exports = getEventKey;
+},{"125":125}],127:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule getEventModifierState
+ * @typechecks static-only
+ */
+
+'use strict';
+
+/**
+ * Translation from modifier key to the associated property in the event.
+ * @see http://www.w3.org/TR/DOM-Level-3-Events/#keys-Modifiers
+ */
+
+var modifierKeyToProp = {
+ 'Alt': 'altKey',
+ 'Control': 'ctrlKey',
+ 'Meta': 'metaKey',
+ 'Shift': 'shiftKey'
+};
+
+// IE8 does not implement getModifierState so we simply map it to the only
+// modifier keys exposed by the event itself, does not support Lock-keys.
+// Currently, all major browsers except Chrome seems to support Lock-keys.
+function modifierStateGetter(keyArg) {
+ var syntheticEvent = this;
+ var nativeEvent = syntheticEvent.nativeEvent;
+ if (nativeEvent.getModifierState) {
+ return nativeEvent.getModifierState(keyArg);
+ }
+ var keyProp = modifierKeyToProp[keyArg];
+ return keyProp ? !!nativeEvent[keyProp] : false;
+}
+
+function getEventModifierState(nativeEvent) {
+ return modifierStateGetter;
+}
+
+module.exports = getEventModifierState;
+},{}],128:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule getEventTarget
+ * @typechecks static-only
+ */
+
+'use strict';
+
+/**
+ * Gets the target node from a native browser event by accounting for
+ * inconsistencies in browser DOM APIs.
+ *
+ * @param {object} nativeEvent Native browser event.
+ * @return {DOMEventTarget} Target node.
+ */
+function getEventTarget(nativeEvent) {
+ var target = nativeEvent.target || nativeEvent.srcElement || window;
+ // Safari may fire events on text nodes (Node.TEXT_NODE is 3).
+ // @see http://www.quirksmode.org/js/events_properties.html
+ return target.nodeType === 3 ? target.parentNode : target;
+}
+
+module.exports = getEventTarget;
+},{}],129:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule getIteratorFn
+ * @typechecks static-only
+ */
+
+'use strict';
+
+/* global Symbol */
+var ITERATOR_SYMBOL = typeof Symbol === 'function' && Symbol.iterator;
+var FAUX_ITERATOR_SYMBOL = '@@iterator'; // Before Symbol spec.
+
+/**
+ * Returns the iterator method function contained on the iterable object.
+ *
+ * Be sure to invoke the function with the iterable as context:
+ *
+ * var iteratorFn = getIteratorFn(myIterable);
+ * if (iteratorFn) {
+ * var iterator = iteratorFn.call(myIterable);
+ * ...
+ * }
+ *
+ * @param {?object} maybeIterable
+ * @return {?function}
+ */
+function getIteratorFn(maybeIterable) {
+ var iteratorFn = maybeIterable && (ITERATOR_SYMBOL && maybeIterable[ITERATOR_SYMBOL] || maybeIterable[FAUX_ITERATOR_SYMBOL]);
+ if (typeof iteratorFn === 'function') {
+ return iteratorFn;
+ }
+}
+
+module.exports = getIteratorFn;
+},{}],130:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule getNodeForCharacterOffset
+ */
+
+'use strict';
+
+/**
+ * Given any node return the first leaf node without children.
+ *
+ * @param {DOMElement|DOMTextNode} node
+ * @return {DOMElement|DOMTextNode}
+ */
+function getLeafNode(node) {
+ while (node && node.firstChild) {
+ node = node.firstChild;
+ }
+ return node;
+}
+
+/**
+ * Get the next sibling within a container. This will walk up the
+ * DOM if a node's siblings have been exhausted.
+ *
+ * @param {DOMElement|DOMTextNode} node
+ * @return {?DOMElement|DOMTextNode}
+ */
+function getSiblingNode(node) {
+ while (node) {
+ if (node.nextSibling) {
+ return node.nextSibling;
+ }
+ node = node.parentNode;
+ }
+}
+
+/**
+ * Get object describing the nodes which contain characters at offset.
+ *
+ * @param {DOMElement|DOMTextNode} root
+ * @param {number} offset
+ * @return {?object}
+ */
+function getNodeForCharacterOffset(root, offset) {
+ var node = getLeafNode(root);
+ var nodeStart = 0;
+ var nodeEnd = 0;
+
+ while (node) {
+ if (node.nodeType === 3) {
+ nodeEnd = nodeStart + node.textContent.length;
+
+ if (nodeStart <= offset && nodeEnd >= offset) {
+ return {
+ node: node,
+ offset: offset - nodeStart
+ };
+ }
+
+ nodeStart = nodeEnd;
+ }
+
+ node = getLeafNode(getSiblingNode(node));
+ }
+}
+
+module.exports = getNodeForCharacterOffset;
+},{}],131:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule getTextContentAccessor
+ */
+
+'use strict';
+
+var ExecutionEnvironment = _dereq_(147);
+
+var contentKey = null;
+
+/**
+ * Gets the key used to access text content on a DOM node.
+ *
+ * @return {?string} Key used to access text content.
+ * @internal
+ */
+function getTextContentAccessor() {
+ if (!contentKey && ExecutionEnvironment.canUseDOM) {
+ // Prefer textContent to innerText because many browsers support both but
+ // SVG <text> elements don't support innerText even when <div> does.
+ contentKey = 'textContent' in document.documentElement ? 'textContent' : 'innerText';
+ }
+ return contentKey;
+}
+
+module.exports = getTextContentAccessor;
+},{"147":147}],132:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule instantiateReactComponent
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var ReactCompositeComponent = _dereq_(38);
+var ReactEmptyComponent = _dereq_(59);
+var ReactNativeComponent = _dereq_(75);
+
+var assign = _dereq_(24);
+var invariant = _dereq_(161);
+var warning = _dereq_(173);
+
+// To avoid a cyclic dependency, we create the final class in this module
+var ReactCompositeComponentWrapper = function () {};
+assign(ReactCompositeComponentWrapper.prototype, ReactCompositeComponent.Mixin, {
+ _instantiateReactComponent: instantiateReactComponent
+});
+
+function getDeclarationErrorAddendum(owner) {
+ if (owner) {
+ var name = owner.getName();
+ if (name) {
+ return ' Check the render method of `' + name + '`.';
+ }
+ }
+ return '';
+}
+
+/**
+ * Check if the type reference is a known internal type. I.e. not a user
+ * provided composite type.
+ *
+ * @param {function} type
+ * @return {boolean} Returns true if this is a valid internal type.
+ */
+function isInternalComponentType(type) {
+ return typeof type === 'function' && typeof type.prototype !== 'undefined' && typeof type.prototype.mountComponent === 'function' && typeof type.prototype.receiveComponent === 'function';
+}
+
+/**
+ * Given a ReactNode, create an instance that will actually be mounted.
+ *
+ * @param {ReactNode} node
+ * @return {object} A new instance of the element's constructor.
+ * @protected
+ */
+function instantiateReactComponent(node) {
+ var instance;
+
+ if (node === null || node === false) {
+ instance = new ReactEmptyComponent(instantiateReactComponent);
+ } else if (typeof node === 'object') {
+ var element = node;
+ !(element && (typeof element.type === 'function' || typeof element.type === 'string')) ? "development" !== 'production' ? invariant(false, 'Element type is invalid: expected a string (for built-in components) ' + 'or a class/function (for composite components) but got: %s.%s', element.type == null ? element.type : typeof element.type, getDeclarationErrorAddendum(element._owner)) : invariant(false) : undefined;
+
+ // Special case string values
+ if (typeof element.type === 'string') {
+ instance = ReactNativeComponent.createInternalComponent(element);
+ } else if (isInternalComponentType(element.type)) {
+ // This is temporarily available for custom components that are not string
+ // representations. I.e. ART. Once those are updated to use the string
+ // representation, we can drop this code path.
+ instance = new element.type(element);
+ } else {
+ instance = new ReactCompositeComponentWrapper();
+ }
+ } else if (typeof node === 'string' || typeof node === 'number') {
+ instance = ReactNativeComponent.createInstanceForText(node);
+ } else {
+ !false ? "development" !== 'production' ? invariant(false, 'Encountered invalid React node of type %s', typeof node) : invariant(false) : undefined;
+ }
+
+ if ("development" !== 'production') {
+ "development" !== 'production' ? warning(typeof instance.construct === 'function' && typeof instance.mountComponent === 'function' && typeof instance.receiveComponent === 'function' && typeof instance.unmountComponent === 'function', 'Only React Components can be mounted.') : undefined;
+ }
+
+ // Sets up the instance. This can probably just move into the constructor now.
+ instance.construct(node);
+
+ // These two fields are used by the DOM and ART diffing algorithms
+ // respectively. Instead of using expandos on components, we should be
+ // storing the state needed by the diffing algorithms elsewhere.
+ instance._mountIndex = 0;
+ instance._mountImage = null;
+
+ if ("development" !== 'production') {
+ instance._isOwnerNecessary = false;
+ instance._warnedAboutRefsInRender = false;
+ }
+
+ // Internal instances should fully constructed at this point, so they should
+ // not get any new fields added to them at this point.
+ if ("development" !== 'production') {
+ if (Object.preventExtensions) {
+ Object.preventExtensions(instance);
+ }
+ }
+
+ return instance;
+}
+
+module.exports = instantiateReactComponent;
+},{"161":161,"173":173,"24":24,"38":38,"59":59,"75":75}],133:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule isEventSupported
+ */
+
+'use strict';
+
+var ExecutionEnvironment = _dereq_(147);
+
+var useHasFeature;
+if (ExecutionEnvironment.canUseDOM) {
+ useHasFeature = document.implementation && document.implementation.hasFeature &&
+ // always returns true in newer browsers as per the standard.
+ // @see http://dom.spec.whatwg.org/#dom-domimplementation-hasfeature
+ document.implementation.hasFeature('', '') !== true;
+}
+
+/**
+ * Checks if an event is supported in the current execution environment.
+ *
+ * NOTE: This will not work correctly for non-generic events such as `change`,
+ * `reset`, `load`, `error`, and `select`.
+ *
+ * Borrows from Modernizr.
+ *
+ * @param {string} eventNameSuffix Event name, e.g. "click".
+ * @param {?boolean} capture Check if the capture phase is supported.
+ * @return {boolean} True if the event is supported.
+ * @internal
+ * @license Modernizr 3.0.0pre (Custom Build) | MIT
+ */
+function isEventSupported(eventNameSuffix, capture) {
+ if (!ExecutionEnvironment.canUseDOM || capture && !('addEventListener' in document)) {
+ return false;
+ }
+
+ var eventName = 'on' + eventNameSuffix;
+ var isSupported = (eventName in document);
+
+ if (!isSupported) {
+ var element = document.createElementNS('http://www.w3.org/1999/xhtml', 'div');
+ element.setAttribute(eventName, 'return;');
+ isSupported = typeof element[eventName] === 'function';
+ }
+
+ if (!isSupported && useHasFeature && eventNameSuffix === 'wheel') {
+ // This is the only way to test support for the `wheel` event in IE9+.
+ isSupported = document.implementation.hasFeature('Events.wheel', '3.0');
+ }
+
+ return isSupported;
+}
+
+module.exports = isEventSupported;
+},{"147":147}],134:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule isTextInputElement
+ */
+
+'use strict';
+
+/**
+ * @see http://www.whatwg.org/specs/web-apps/current-work/multipage/the-input-element.html#input-type-attr-summary
+ */
+var supportedInputTypes = {
+ 'color': true,
+ 'date': true,
+ 'datetime': true,
+ 'datetime-local': true,
+ 'email': true,
+ 'month': true,
+ 'number': true,
+ 'password': true,
+ 'range': true,
+ 'search': true,
+ 'tel': true,
+ 'text': true,
+ 'time': true,
+ 'url': true,
+ 'week': true
+};
+
+function isTextInputElement(elem) {
+ var nodeName = elem && elem.nodeName && elem.nodeName.toLowerCase();
+ return nodeName && (nodeName === 'input' && supportedInputTypes[elem.type] || nodeName === 'textarea');
+}
+
+module.exports = isTextInputElement;
+},{}],135:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule onlyChild
+ */
+'use strict';
+
+var ReactElement = _dereq_(57);
+
+var invariant = _dereq_(161);
+
+/**
+ * Returns the first child in a collection of children and verifies that there
+ * is only one child in the collection. The current implementation of this
+ * function assumes that a single child gets passed without a wrapper, but the
+ * purpose of this helper function is to abstract away the particular structure
+ * of children.
+ *
+ * @param {?object} children Child collection structure.
+ * @return {ReactComponent} The first and only `ReactComponent` contained in the
+ * structure.
+ */
+function onlyChild(children) {
+ !ReactElement.isValidElement(children) ? "development" !== 'production' ? invariant(false, 'onlyChild must be passed a children with exactly one child.') : invariant(false) : undefined;
+ return children;
+}
+
+module.exports = onlyChild;
+},{"161":161,"57":57}],136:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule quoteAttributeValueForBrowser
+ */
+
+'use strict';
+
+var escapeTextContentForBrowser = _dereq_(121);
+
+/**
+ * Escapes attribute value to prevent scripting attacks.
+ *
+ * @param {*} value Value to escape.
+ * @return {string} An escaped string.
+ */
+function quoteAttributeValueForBrowser(value) {
+ return '"' + escapeTextContentForBrowser(value) + '"';
+}
+
+module.exports = quoteAttributeValueForBrowser;
+},{"121":121}],137:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+* @providesModule renderSubtreeIntoContainer
+*/
+
+'use strict';
+
+var ReactMount = _dereq_(72);
+
+module.exports = ReactMount.renderSubtreeIntoContainer;
+},{"72":72}],138:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule setInnerHTML
+ */
+
+/* globals MSApp */
+
+'use strict';
+
+var ExecutionEnvironment = _dereq_(147);
+
+var WHITESPACE_TEST = /^[ \r\n\t\f]/;
+var NONVISIBLE_TEST = /<(!--|link|noscript|meta|script|style)[ \r\n\t\f\/>]/;
+
+/**
+ * Set the innerHTML property of a node, ensuring that whitespace is preserved
+ * even in IE8.
+ *
+ * @param {DOMElement} node
+ * @param {string} html
+ * @internal
+ */
+var setInnerHTML = function (node, html) {
+ node.innerHTML = html;
+};
+
+// Win8 apps: Allow all html to be inserted
+if (typeof MSApp !== 'undefined' && MSApp.execUnsafeLocalFunction) {
+ setInnerHTML = function (node, html) {
+ MSApp.execUnsafeLocalFunction(function () {
+ node.innerHTML = html;
+ });
+ };
+}
+
+if (ExecutionEnvironment.canUseDOM) {
+ // IE8: When updating a just created node with innerHTML only leading
+ // whitespace is removed. When updating an existing node with innerHTML
+ // whitespace in root TextNodes is also collapsed.
+ // @see quirksmode.org/bugreports/archives/2004/11/innerhtml_and_t.html
+
+ // Feature detection; only IE8 is known to behave improperly like this.
+ var testElement = document.createElementNS('http://www.w3.org/1999/xhtml', 'div');
+ testElement.innerHTML = ' ';
+ if (testElement.innerHTML === '') {
+ setInnerHTML = function (node, html) {
+ // Magic theory: IE8 supposedly differentiates between added and updated
+ // nodes when processing innerHTML, innerHTML on updated nodes suffers
+ // from worse whitespace behavior. Re-adding a node like this triggers
+ // the initial and more favorable whitespace behavior.
+ // TODO: What to do on a detached node?
+ if (node.parentNode) {
+ node.parentNode.replaceChild(node, node);
+ }
+
+ // We also implement a workaround for non-visible tags disappearing into
+ // thin air on IE8, this only happens if there is no visible text
+ // in-front of the non-visible tags. Piggyback on the whitespace fix
+ // and simply check if any non-visible tags appear in the source.
+ if (WHITESPACE_TEST.test(html) || html[0] === '<' && NONVISIBLE_TEST.test(html)) {
+ // Recover leading whitespace by temporarily prepending any character.
+ // \uFEFF has the potential advantage of being zero-width/invisible.
+ // UglifyJS drops U+FEFF chars when parsing, so use String.fromCharCode
+ // in hopes that this is preserved even if "\uFEFF" is transformed to
+ // the actual Unicode character (by Babel, for example).
+ // https://github.com/mishoo/UglifyJS2/blob/v2.4.20/lib/parse.js#L216
+ node.innerHTML = String.fromCharCode(0xFEFF) + html;
+
+ // deleteData leaves an empty `TextNode` which offsets the index of all
+ // children. Definitely want to avoid this.
+ var textNode = node.firstChild;
+ if (textNode.data.length === 1) {
+ node.removeChild(textNode);
+ } else {
+ textNode.deleteData(0, 1);
+ }
+ } else {
+ node.innerHTML = html;
+ }
+ };
+ }
+}
+
+module.exports = setInnerHTML;
+},{"147":147}],139:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule setTextContent
+ */
+
+'use strict';
+
+var ExecutionEnvironment = _dereq_(147);
+var escapeTextContentForBrowser = _dereq_(121);
+var setInnerHTML = _dereq_(138);
+
+/**
+ * Set the textContent property of a node, ensuring that whitespace is preserved
+ * even in IE8. innerText is a poor substitute for textContent and, among many
+ * issues, inserts <br> instead of the literal newline chars. innerHTML behaves
+ * as it should.
+ *
+ * @param {DOMElement} node
+ * @param {string} text
+ * @internal
+ */
+var setTextContent = function (node, text) {
+ node.textContent = text;
+};
+
+if (ExecutionEnvironment.canUseDOM) {
+ if (!('textContent' in document.documentElement)) {
+ setTextContent = function (node, text) {
+ setInnerHTML(node, escapeTextContentForBrowser(text));
+ };
+ }
+}
+
+module.exports = setTextContent;
+},{"121":121,"138":138,"147":147}],140:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+* @providesModule shallowCompare
+*/
+
+'use strict';
+
+var shallowEqual = _dereq_(171);
+
+/**
+ * Does a shallow comparison for props and state.
+ * See ReactComponentWithPureRenderMixin
+ */
+function shallowCompare(instance, nextProps, nextState) {
+ return !shallowEqual(instance.props, nextProps) || !shallowEqual(instance.state, nextState);
+}
+
+module.exports = shallowCompare;
+},{"171":171}],141:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule shouldUpdateReactComponent
+ * @typechecks static-only
+ */
+
+'use strict';
+
+/**
+ * Given a `prevElement` and `nextElement`, determines if the existing
+ * instance should be updated as opposed to being destroyed or replaced by a new
+ * instance. Both arguments are elements. This ensures that this logic can
+ * operate on stateless trees without any backing instance.
+ *
+ * @param {?object} prevElement
+ * @param {?object} nextElement
+ * @return {boolean} True if the existing instance should be updated.
+ * @protected
+ */
+function shouldUpdateReactComponent(prevElement, nextElement) {
+ var prevEmpty = prevElement === null || prevElement === false;
+ var nextEmpty = nextElement === null || nextElement === false;
+ if (prevEmpty || nextEmpty) {
+ return prevEmpty === nextEmpty;
+ }
+
+ var prevType = typeof prevElement;
+ var nextType = typeof nextElement;
+ if (prevType === 'string' || prevType === 'number') {
+ return nextType === 'string' || nextType === 'number';
+ } else {
+ return nextType === 'object' && prevElement.type === nextElement.type && prevElement.key === nextElement.key;
+ }
+ return false;
+}
+
+module.exports = shouldUpdateReactComponent;
+},{}],142:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule traverseAllChildren
+ */
+
+'use strict';
+
+var ReactCurrentOwner = _dereq_(39);
+var ReactElement = _dereq_(57);
+var ReactInstanceHandles = _dereq_(67);
+
+var getIteratorFn = _dereq_(129);
+var invariant = _dereq_(161);
+var warning = _dereq_(173);
+
+var SEPARATOR = ReactInstanceHandles.SEPARATOR;
+var SUBSEPARATOR = ':';
+
+/**
+ * TODO: Test that a single child and an array with one item have the same key
+ * pattern.
+ */
+
+var userProvidedKeyEscaperLookup = {
+ '=': '=0',
+ '.': '=1',
+ ':': '=2'
+};
+
+var userProvidedKeyEscapeRegex = /[=.:]/g;
+
+var didWarnAboutMaps = false;
+
+function userProvidedKeyEscaper(match) {
+ return userProvidedKeyEscaperLookup[match];
+}
+
+/**
+ * Generate a key string that identifies a component within a set.
+ *
+ * @param {*} component A component that could contain a manual key.
+ * @param {number} index Index that is used if a manual key is not provided.
+ * @return {string}
+ */
+function getComponentKey(component, index) {
+ if (component && component.key != null) {
+ // Explicit key
+ return wrapUserProvidedKey(component.key);
+ }
+ // Implicit key determined by the index in the set
+ return index.toString(36);
+}
+
+/**
+ * Escape a component key so that it is safe to use in a reactid.
+ *
+ * @param {*} text Component key to be escaped.
+ * @return {string} An escaped string.
+ */
+function escapeUserProvidedKey(text) {
+ return ('' + text).replace(userProvidedKeyEscapeRegex, userProvidedKeyEscaper);
+}
+
+/**
+ * Wrap a `key` value explicitly provided by the user to distinguish it from
+ * implicitly-generated keys generated by a component's index in its parent.
+ *
+ * @param {string} key Value of a user-provided `key` attribute
+ * @return {string}
+ */
+function wrapUserProvidedKey(key) {
+ return '$' + escapeUserProvidedKey(key);
+}
+
+/**
+ * @param {?*} children Children tree container.
+ * @param {!string} nameSoFar Name of the key path so far.
+ * @param {!function} callback Callback to invoke with each child found.
+ * @param {?*} traverseContext Used to pass information throughout the traversal
+ * process.
+ * @return {!number} The number of children in this subtree.
+ */
+function traverseAllChildrenImpl(children, nameSoFar, callback, traverseContext) {
+ var type = typeof children;
+
+ if (type === 'undefined' || type === 'boolean') {
+ // All of the above are perceived as null.
+ children = null;
+ }
+
+ if (children === null || type === 'string' || type === 'number' || ReactElement.isValidElement(children)) {
+ callback(traverseContext, children,
+ // If it's the only child, treat the name as if it was wrapped in an array
+ // so that it's consistent if the number of children grows.
+ nameSoFar === '' ? SEPARATOR + getComponentKey(children, 0) : nameSoFar);
+ return 1;
+ }
+
+ var child;
+ var nextName;
+ var subtreeCount = 0; // Count of children found in the current subtree.
+ var nextNamePrefix = nameSoFar === '' ? SEPARATOR : nameSoFar + SUBSEPARATOR;
+
+ if (Array.isArray(children)) {
+ for (var i = 0; i < children.length; i++) {
+ child = children[i];
+ nextName = nextNamePrefix + getComponentKey(child, i);
+ subtreeCount += traverseAllChildrenImpl(child, nextName, callback, traverseContext);
+ }
+ } else {
+ var iteratorFn = getIteratorFn(children);
+ if (iteratorFn) {
+ var iterator = iteratorFn.call(children);
+ var step;
+ if (iteratorFn !== children.entries) {
+ var ii = 0;
+ while (!(step = iterator.next()).done) {
+ child = step.value;
+ nextName = nextNamePrefix + getComponentKey(child, ii++);
+ subtreeCount += traverseAllChildrenImpl(child, nextName, callback, traverseContext);
+ }
+ } else {
+ if ("development" !== 'production') {
+ "development" !== 'production' ? warning(didWarnAboutMaps, 'Using Maps as children is not yet fully supported. It is an ' + 'experimental feature that might be removed. Convert it to a ' + 'sequence / iterable of keyed ReactElements instead.') : undefined;
+ didWarnAboutMaps = true;
+ }
+ // Iterator will provide entry [k,v] tuples rather than values.
+ while (!(step = iterator.next()).done) {
+ var entry = step.value;
+ if (entry) {
+ child = entry[1];
+ nextName = nextNamePrefix + wrapUserProvidedKey(entry[0]) + SUBSEPARATOR + getComponentKey(child, 0);
+ subtreeCount += traverseAllChildrenImpl(child, nextName, callback, traverseContext);
+ }
+ }
+ }
+ } else if (type === 'object') {
+ var addendum = '';
+ if ("development" !== 'production') {
+ addendum = ' If you meant to render a collection of children, use an array ' + 'instead or wrap the object using createFragment(object) from the ' + 'React add-ons.';
+ if (children._isReactElement) {
+ addendum = ' It looks like you\'re using an element created by a different ' + 'version of React. Make sure to use only one copy of React.';
+ }
+ if (ReactCurrentOwner.current) {
+ var name = ReactCurrentOwner.current.getName();
+ if (name) {
+ addendum += ' Check the render method of `' + name + '`.';
+ }
+ }
+ }
+ var childrenString = String(children);
+ !false ? "development" !== 'production' ? invariant(false, 'Objects are not valid as a React child (found: %s).%s', childrenString === '[object Object]' ? 'object with keys {' + Object.keys(children).join(', ') + '}' : childrenString, addendum) : invariant(false) : undefined;
+ }
+ }
+
+ return subtreeCount;
+}
+
+/**
+ * Traverses children that are typically specified as `props.children`, but
+ * might also be specified through attributes:
+ *
+ * - `traverseAllChildren(this.props.children, ...)`
+ * - `traverseAllChildren(this.props.leftPanelChildren, ...)`
+ *
+ * The `traverseContext` is an optional argument that is passed through the
+ * entire traversal. It can be used to store accumulations or anything else that
+ * the callback might find relevant.
+ *
+ * @param {?*} children Children tree object.
+ * @param {!function} callback To invoke upon traversing each child.
+ * @param {?*} traverseContext Context for traversal.
+ * @return {!number} The number of children in this subtree.
+ */
+function traverseAllChildren(children, callback, traverseContext) {
+ if (children == null) {
+ return 0;
+ }
+
+ return traverseAllChildrenImpl(children, '', callback, traverseContext);
+}
+
+module.exports = traverseAllChildren;
+},{"129":129,"161":161,"173":173,"39":39,"57":57,"67":67}],143:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule update
+ */
+
+/* global hasOwnProperty:true */
+
+'use strict';
+
+var assign = _dereq_(24);
+var keyOf = _dereq_(166);
+var invariant = _dereq_(161);
+var hasOwnProperty = ({}).hasOwnProperty;
+
+function shallowCopy(x) {
+ if (Array.isArray(x)) {
+ return x.concat();
+ } else if (x && typeof x === 'object') {
+ return assign(new x.constructor(), x);
+ } else {
+ return x;
+ }
+}
+
+var COMMAND_PUSH = keyOf({ $push: null });
+var COMMAND_UNSHIFT = keyOf({ $unshift: null });
+var COMMAND_SPLICE = keyOf({ $splice: null });
+var COMMAND_SET = keyOf({ $set: null });
+var COMMAND_MERGE = keyOf({ $merge: null });
+var COMMAND_APPLY = keyOf({ $apply: null });
+
+var ALL_COMMANDS_LIST = [COMMAND_PUSH, COMMAND_UNSHIFT, COMMAND_SPLICE, COMMAND_SET, COMMAND_MERGE, COMMAND_APPLY];
+
+var ALL_COMMANDS_SET = {};
+
+ALL_COMMANDS_LIST.forEach(function (command) {
+ ALL_COMMANDS_SET[command] = true;
+});
+
+function invariantArrayCase(value, spec, command) {
+ !Array.isArray(value) ? "development" !== 'production' ? invariant(false, 'update(): expected target of %s to be an array; got %s.', command, value) : invariant(false) : undefined;
+ var specValue = spec[command];
+ !Array.isArray(specValue) ? "development" !== 'production' ? invariant(false, 'update(): expected spec of %s to be an array; got %s. ' + 'Did you forget to wrap your parameter in an array?', command, specValue) : invariant(false) : undefined;
+}
+
+function update(value, spec) {
+ !(typeof spec === 'object') ? "development" !== 'production' ? invariant(false, 'update(): You provided a key path to update() that did not contain one ' + 'of %s. Did you forget to include {%s: ...}?', ALL_COMMANDS_LIST.join(', '), COMMAND_SET) : invariant(false) : undefined;
+
+ if (hasOwnProperty.call(spec, COMMAND_SET)) {
+ !(Object.keys(spec).length === 1) ? "development" !== 'production' ? invariant(false, 'Cannot have more than one key in an object with %s', COMMAND_SET) : invariant(false) : undefined;
+
+ return spec[COMMAND_SET];
+ }
+
+ var nextValue = shallowCopy(value);
+
+ if (hasOwnProperty.call(spec, COMMAND_MERGE)) {
+ var mergeObj = spec[COMMAND_MERGE];
+ !(mergeObj && typeof mergeObj === 'object') ? "development" !== 'production' ? invariant(false, 'update(): %s expects a spec of type \'object\'; got %s', COMMAND_MERGE, mergeObj) : invariant(false) : undefined;
+ !(nextValue && typeof nextValue === 'object') ? "development" !== 'production' ? invariant(false, 'update(): %s expects a target of type \'object\'; got %s', COMMAND_MERGE, nextValue) : invariant(false) : undefined;
+ assign(nextValue, spec[COMMAND_MERGE]);
+ }
+
+ if (hasOwnProperty.call(spec, COMMAND_PUSH)) {
+ invariantArrayCase(value, spec, COMMAND_PUSH);
+ spec[COMMAND_PUSH].forEach(function (item) {
+ nextValue.push(item);
+ });
+ }
+
+ if (hasOwnProperty.call(spec, COMMAND_UNSHIFT)) {
+ invariantArrayCase(value, spec, COMMAND_UNSHIFT);
+ spec[COMMAND_UNSHIFT].forEach(function (item) {
+ nextValue.unshift(item);
+ });
+ }
+
+ if (hasOwnProperty.call(spec, COMMAND_SPLICE)) {
+ !Array.isArray(value) ? "development" !== 'production' ? invariant(false, 'Expected %s target to be an array; got %s', COMMAND_SPLICE, value) : invariant(false) : undefined;
+ !Array.isArray(spec[COMMAND_SPLICE]) ? "development" !== 'production' ? invariant(false, 'update(): expected spec of %s to be an array of arrays; got %s. ' + 'Did you forget to wrap your parameters in an array?', COMMAND_SPLICE, spec[COMMAND_SPLICE]) : invariant(false) : undefined;
+ spec[COMMAND_SPLICE].forEach(function (args) {
+ !Array.isArray(args) ? "development" !== 'production' ? invariant(false, 'update(): expected spec of %s to be an array of arrays; got %s. ' + 'Did you forget to wrap your parameters in an array?', COMMAND_SPLICE, spec[COMMAND_SPLICE]) : invariant(false) : undefined;
+ nextValue.splice.apply(nextValue, args);
+ });
+ }
+
+ if (hasOwnProperty.call(spec, COMMAND_APPLY)) {
+ !(typeof spec[COMMAND_APPLY] === 'function') ? "development" !== 'production' ? invariant(false, 'update(): expected spec of %s to be a function; got %s.', COMMAND_APPLY, spec[COMMAND_APPLY]) : invariant(false) : undefined;
+ nextValue = spec[COMMAND_APPLY](nextValue);
+ }
+
+ for (var k in spec) {
+ if (!(ALL_COMMANDS_SET.hasOwnProperty(k) && ALL_COMMANDS_SET[k])) {
+ nextValue[k] = update(value[k], spec[k]);
+ }
+ }
+
+ return nextValue;
+}
+
+module.exports = update;
+},{"161":161,"166":166,"24":24}],144:[function(_dereq_,module,exports){
+/**
+ * Copyright 2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule validateDOMNesting
+ */
+
+'use strict';
+
+var assign = _dereq_(24);
+var emptyFunction = _dereq_(153);
+var warning = _dereq_(173);
+
+var validateDOMNesting = emptyFunction;
+
+if ("development" !== 'production') {
+ // This validation code was written based on the HTML5 parsing spec:
+ // https://html.spec.whatwg.org/multipage/syntax.html#has-an-element-in-scope
+ //
+ // Note: this does not catch all invalid nesting, nor does it try to (as it's
+ // not clear what practical benefit doing so provides); instead, we warn only
+ // for cases where the parser will give a parse tree differing from what React
+ // intended. For example, <b><div></div></b> is invalid but we don't warn
+ // because it still parses correctly; we do warn for other cases like nested
+ // <p> tags where the beginning of the second element implicitly closes the
+ // first, causing a confusing mess.
+
+ // https://html.spec.whatwg.org/multipage/syntax.html#special
+ var specialTags = ['address', 'applet', 'area', 'article', 'aside', 'base', 'basefont', 'bgsound', 'blockquote', 'body', 'br', 'button', 'caption', 'center', 'col', 'colgroup', 'dd', 'details', 'dir', 'div', 'dl', 'dt', 'embed', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'frame', 'frameset', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'iframe', 'img', 'input', 'isindex', 'li', 'link', 'listing', 'main', 'marquee', 'menu', 'menuitem', 'meta', 'nav', 'noembed', 'noframes', 'noscript', 'object', 'ol', 'p', 'param', 'plaintext', 'pre', 'script', 'section', 'select', 'source', 'style', 'summary', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'title', 'tr', 'track', 'ul', 'wbr', 'xmp'];
+
+ // https://html.spec.whatwg.org/multipage/syntax.html#has-an-element-in-scope
+ var inScopeTags = ['applet', 'caption', 'html', 'table', 'td', 'th', 'marquee', 'object', 'template',
+
+ // https://html.spec.whatwg.org/multipage/syntax.html#html-integration-point
+ // TODO: Distinguish by namespace here -- for <title>, including it here
+ // errs on the side of fewer warnings
+ 'foreignObject', 'desc', 'title'];
+
+ // https://html.spec.whatwg.org/multipage/syntax.html#has-an-element-in-button-scope
+ var buttonScopeTags = inScopeTags.concat(['button']);
+
+ // https://html.spec.whatwg.org/multipage/syntax.html#generate-implied-end-tags
+ var impliedEndTags = ['dd', 'dt', 'li', 'option', 'optgroup', 'p', 'rp', 'rt'];
+
+ var emptyAncestorInfo = {
+ parentTag: null,
+
+ formTag: null,
+ aTagInScope: null,
+ buttonTagInScope: null,
+ nobrTagInScope: null,
+ pTagInButtonScope: null,
+
+ listItemTagAutoclosing: null,
+ dlItemTagAutoclosing: null
+ };
+
+ var updatedAncestorInfo = function (oldInfo, tag, instance) {
+ var ancestorInfo = assign({}, oldInfo || emptyAncestorInfo);
+ var info = { tag: tag, instance: instance };
+
+ if (inScopeTags.indexOf(tag) !== -1) {
+ ancestorInfo.aTagInScope = null;
+ ancestorInfo.buttonTagInScope = null;
+ ancestorInfo.nobrTagInScope = null;
+ }
+ if (buttonScopeTags.indexOf(tag) !== -1) {
+ ancestorInfo.pTagInButtonScope = null;
+ }
+
+ // See rules for 'li', 'dd', 'dt' start tags in
+ // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inbody
+ if (specialTags.indexOf(tag) !== -1 && tag !== 'address' && tag !== 'div' && tag !== 'p') {
+ ancestorInfo.listItemTagAutoclosing = null;
+ ancestorInfo.dlItemTagAutoclosing = null;
+ }
+
+ ancestorInfo.parentTag = info;
+
+ if (tag === 'form') {
+ ancestorInfo.formTag = info;
+ }
+ if (tag === 'a') {
+ ancestorInfo.aTagInScope = info;
+ }
+ if (tag === 'button') {
+ ancestorInfo.buttonTagInScope = info;
+ }
+ if (tag === 'nobr') {
+ ancestorInfo.nobrTagInScope = info;
+ }
+ if (tag === 'p') {
+ ancestorInfo.pTagInButtonScope = info;
+ }
+ if (tag === 'li') {
+ ancestorInfo.listItemTagAutoclosing = info;
+ }
+ if (tag === 'dd' || tag === 'dt') {
+ ancestorInfo.dlItemTagAutoclosing = info;
+ }
+
+ return ancestorInfo;
+ };
+
+ /**
+ * Returns whether
+ */
+ var isTagValidWithParent = function (tag, parentTag) {
+ // First, let's check if we're in an unusual parsing mode...
+ switch (parentTag) {
+ // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inselect
+ case 'select':
+ return tag === 'option' || tag === 'optgroup' || tag === '#text';
+ case 'optgroup':
+ return tag === 'option' || tag === '#text';
+ // Strictly speaking, seeing an <option> doesn't mean we're in a <select>
+ // but
+ case 'option':
+ return tag === '#text';
+
+ // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intd
+ // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-incaption
+ // No special behavior since these rules fall back to "in body" mode for
+ // all except special table nodes which cause bad parsing behavior anyway.
+
+ // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intr
+ case 'tr':
+ return tag === 'th' || tag === 'td' || tag === 'style' || tag === 'script' || tag === 'template';
+
+ // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intbody
+ case 'tbody':
+ case 'thead':
+ case 'tfoot':
+ return tag === 'tr' || tag === 'style' || tag === 'script' || tag === 'template';
+
+ // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-incolgroup
+ case 'colgroup':
+ return tag === 'col' || tag === 'template';
+
+ // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intable
+ case 'table':
+ return tag === 'caption' || tag === 'colgroup' || tag === 'tbody' || tag === 'tfoot' || tag === 'thead' || tag === 'style' || tag === 'script' || tag === 'template';
+
+ // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inhead
+ case 'head':
+ return tag === 'base' || tag === 'basefont' || tag === 'bgsound' || tag === 'link' || tag === 'meta' || tag === 'title' || tag === 'noscript' || tag === 'noframes' || tag === 'style' || tag === 'script' || tag === 'template';
+
+ // https://html.spec.whatwg.org/multipage/semantics.html#the-html-element
+ case 'html':
+ return tag === 'head' || tag === 'body';
+ }
+
+ // Probably in the "in body" parsing mode, so we outlaw only tag combos
+ // where the parsing rules cause implicit opens or closes to be added.
+ // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inbody
+ switch (tag) {
+ case 'h1':
+ case 'h2':
+ case 'h3':
+ case 'h4':
+ case 'h5':
+ case 'h6':
+ return parentTag !== 'h1' && parentTag !== 'h2' && parentTag !== 'h3' && parentTag !== 'h4' && parentTag !== 'h5' && parentTag !== 'h6';
+
+ case 'rp':
+ case 'rt':
+ return impliedEndTags.indexOf(parentTag) === -1;
+
+ case 'caption':
+ case 'col':
+ case 'colgroup':
+ case 'frame':
+ case 'head':
+ case 'tbody':
+ case 'td':
+ case 'tfoot':
+ case 'th':
+ case 'thead':
+ case 'tr':
+ // These tags are only valid with a few parents that have special child
+ // parsing rules -- if we're down here, then none of those matched and
+ // so we allow it only if we don't know what the parent is, as all other
+ // cases are invalid.
+ return parentTag == null;
+ }
+
+ return true;
+ };
+
+ /**
+ * Returns whether
+ */
+ var findInvalidAncestorForTag = function (tag, ancestorInfo) {
+ switch (tag) {
+ case 'address':
+ case 'article':
+ case 'aside':
+ case 'blockquote':
+ case 'center':
+ case 'details':
+ case 'dialog':
+ case 'dir':
+ case 'div':
+ case 'dl':
+ case 'fieldset':
+ case 'figcaption':
+ case 'figure':
+ case 'footer':
+ case 'header':
+ case 'hgroup':
+ case 'main':
+ case 'menu':
+ case 'nav':
+ case 'ol':
+ case 'p':
+ case 'section':
+ case 'summary':
+ case 'ul':
+
+ case 'pre':
+ case 'listing':
+
+ case 'table':
+
+ case 'hr':
+
+ case 'xmp':
+
+ case 'h1':
+ case 'h2':
+ case 'h3':
+ case 'h4':
+ case 'h5':
+ case 'h6':
+ return ancestorInfo.pTagInButtonScope;
+
+ case 'form':
+ return ancestorInfo.formTag || ancestorInfo.pTagInButtonScope;
+
+ case 'li':
+ return ancestorInfo.listItemTagAutoclosing;
+
+ case 'dd':
+ case 'dt':
+ return ancestorInfo.dlItemTagAutoclosing;
+
+ case 'button':
+ return ancestorInfo.buttonTagInScope;
+
+ case 'a':
+ // Spec says something about storing a list of markers, but it sounds
+ // equivalent to this check.
+ return ancestorInfo.aTagInScope;
+
+ case 'nobr':
+ return ancestorInfo.nobrTagInScope;
+ }
+
+ return null;
+ };
+
+ /**
+ * Given a ReactCompositeComponent instance, return a list of its recursive
+ * owners, starting at the root and ending with the instance itself.
+ */
+ var findOwnerStack = function (instance) {
+ if (!instance) {
+ return [];
+ }
+
+ var stack = [];
+ /*eslint-disable space-after-keywords */
+ do {
+ /*eslint-enable space-after-keywords */
+ stack.push(instance);
+ } while (instance = instance._currentElement._owner);
+ stack.reverse();
+ return stack;
+ };
+
+ var didWarn = {};
+
+ validateDOMNesting = function (childTag, childInstance, ancestorInfo) {
+ ancestorInfo = ancestorInfo || emptyAncestorInfo;
+ var parentInfo = ancestorInfo.parentTag;
+ var parentTag = parentInfo && parentInfo.tag;
+
+ var invalidParent = isTagValidWithParent(childTag, parentTag) ? null : parentInfo;
+ var invalidAncestor = invalidParent ? null : findInvalidAncestorForTag(childTag, ancestorInfo);
+ var problematic = invalidParent || invalidAncestor;
+
+ if (problematic) {
+ var ancestorTag = problematic.tag;
+ var ancestorInstance = problematic.instance;
+
+ var childOwner = childInstance && childInstance._currentElement._owner;
+ var ancestorOwner = ancestorInstance && ancestorInstance._currentElement._owner;
+
+ var childOwners = findOwnerStack(childOwner);
+ var ancestorOwners = findOwnerStack(ancestorOwner);
+
+ var minStackLen = Math.min(childOwners.length, ancestorOwners.length);
+ var i;
+
+ var deepestCommon = -1;
+ for (i = 0; i < minStackLen; i++) {
+ if (childOwners[i] === ancestorOwners[i]) {
+ deepestCommon = i;
+ } else {
+ break;
+ }
+ }
+
+ var UNKNOWN = '(unknown)';
+ var childOwnerNames = childOwners.slice(deepestCommon + 1).map(function (inst) {
+ return inst.getName() || UNKNOWN;
+ });
+ var ancestorOwnerNames = ancestorOwners.slice(deepestCommon + 1).map(function (inst) {
+ return inst.getName() || UNKNOWN;
+ });
+ var ownerInfo = [].concat(
+ // If the parent and child instances have a common owner ancestor, start
+ // with that -- otherwise we just start with the parent's owners.
+ deepestCommon !== -1 ? childOwners[deepestCommon].getName() || UNKNOWN : [], ancestorOwnerNames, ancestorTag,
+ // If we're warning about an invalid (non-parent) ancestry, add '...'
+ invalidAncestor ? ['...'] : [], childOwnerNames, childTag).join(' > ');
+
+ var warnKey = !!invalidParent + '|' + childTag + '|' + ancestorTag + '|' + ownerInfo;
+ if (didWarn[warnKey]) {
+ return;
+ }
+ didWarn[warnKey] = true;
+
+ if (invalidParent) {
+ var info = '';
+ if (ancestorTag === 'table' && childTag === 'tr') {
+ info += ' Add a <tbody> to your code to match the DOM tree generated by ' + 'the browser.';
+ }
+ "development" !== 'production' ? warning(false, 'validateDOMNesting(...): <%s> cannot appear as a child of <%s>. ' + 'See %s.%s', childTag, ancestorTag, ownerInfo, info) : undefined;
+ } else {
+ "development" !== 'production' ? warning(false, 'validateDOMNesting(...): <%s> cannot appear as a descendant of ' + '<%s>. See %s.', childTag, ancestorTag, ownerInfo) : undefined;
+ }
+ }
+ };
+
+ validateDOMNesting.ancestorInfoContextKey = '__validateDOMNesting_ancestorInfo$' + Math.random().toString(36).slice(2);
+
+ validateDOMNesting.updatedAncestorInfo = updatedAncestorInfo;
+
+ // For testing
+ validateDOMNesting.isTagValidInContext = function (tag, ancestorInfo) {
+ ancestorInfo = ancestorInfo || emptyAncestorInfo;
+ var parentInfo = ancestorInfo.parentTag;
+ var parentTag = parentInfo && parentInfo.tag;
+ return isTagValidWithParent(tag, parentTag) && !findInvalidAncestorForTag(tag, ancestorInfo);
+ };
+}
+
+module.exports = validateDOMNesting;
+},{"153":153,"173":173,"24":24}],145:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule CSSCore
+ * @typechecks
+ */
+
+'use strict';
+
+var invariant = _dereq_(161);
+
+/**
+ * The CSSCore module specifies the API (and implements most of the methods)
+ * that should be used when dealing with the display of elements (via their
+ * CSS classes and visibility on screen. It is an API focused on mutating the
+ * display and not reading it as no logical state should be encoded in the
+ * display of elements.
+ */
+
+var CSSCore = {
+
+ /**
+ * Adds the class passed in to the element if it doesn't already have it.
+ *
+ * @param {DOMElement} element the element to set the class on
+ * @param {string} className the CSS className
+ * @return {DOMElement} the element passed in
+ */
+ addClass: function (element, className) {
+ !!/\s/.test(className) ? "development" !== 'production' ? invariant(false, 'CSSCore.addClass takes only a single class name. "%s" contains ' + 'multiple classes.', className) : invariant(false) : undefined;
+
+ if (className) {
+ if (element.classList) {
+ element.classList.add(className);
+ } else if (!CSSCore.hasClass(element, className)) {
+ element.className = element.className + ' ' + className;
+ }
+ }
+ return element;
+ },
+
+ /**
+ * Removes the class passed in from the element
+ *
+ * @param {DOMElement} element the element to set the class on
+ * @param {string} className the CSS className
+ * @return {DOMElement} the element passed in
+ */
+ removeClass: function (element, className) {
+ !!/\s/.test(className) ? "development" !== 'production' ? invariant(false, 'CSSCore.removeClass takes only a single class name. "%s" contains ' + 'multiple classes.', className) : invariant(false) : undefined;
+
+ if (className) {
+ if (element.classList) {
+ element.classList.remove(className);
+ } else if (CSSCore.hasClass(element, className)) {
+ element.className = element.className.replace(new RegExp('(^|\\s)' + className + '(?:\\s|$)', 'g'), '$1').replace(/\s+/g, ' ') // multiple spaces to one
+ .replace(/^\s*|\s*$/g, ''); // trim the ends
+ }
+ }
+ return element;
+ },
+
+ /**
+ * Helper to add or remove a class from an element based on a condition.
+ *
+ * @param {DOMElement} element the element to set the class on
+ * @param {string} className the CSS className
+ * @param {*} bool condition to whether to add or remove the class
+ * @return {DOMElement} the element passed in
+ */
+ conditionClass: function (element, className, bool) {
+ return (bool ? CSSCore.addClass : CSSCore.removeClass)(element, className);
+ },
+
+ /**
+ * Tests whether the element has the class specified.
+ *
+ * @param {DOMNode|DOMWindow} element the element to set the class on
+ * @param {string} className the CSS className
+ * @return {boolean} true if the element has the class, false if not
+ */
+ hasClass: function (element, className) {
+ !!/\s/.test(className) ? "development" !== 'production' ? invariant(false, 'CSS.hasClass takes only a single class name.') : invariant(false) : undefined;
+ if (element.classList) {
+ return !!className && element.classList.contains(className);
+ }
+ return (' ' + element.className + ' ').indexOf(' ' + className + ' ') > -1;
+ }
+
+};
+
+module.exports = CSSCore;
+},{"161":161}],146:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * @providesModule EventListener
+ * @typechecks
+ */
+
+'use strict';
+
+var emptyFunction = _dereq_(153);
+
+/**
+ * Upstream version of event listener. Does not take into account specific
+ * nature of platform.
+ */
+var EventListener = {
+ /**
+ * Listen to DOM events during the bubble phase.
+ *
+ * @param {DOMEventTarget} target DOM element to register listener on.
+ * @param {string} eventType Event type, e.g. 'click' or 'mouseover'.
+ * @param {function} callback Callback function.
+ * @return {object} Object with a `remove` method.
+ */
+ listen: function (target, eventType, callback) {
+ if (target.addEventListener) {
+ target.addEventListener(eventType, callback, false);
+ return {
+ remove: function () {
+ target.removeEventListener(eventType, callback, false);
+ }
+ };
+ } else if (target.attachEvent) {
+ target.attachEvent('on' + eventType, callback);
+ return {
+ remove: function () {
+ target.detachEvent('on' + eventType, callback);
+ }
+ };
+ }
+ },
+
+ /**
+ * Listen to DOM events during the capture phase.
+ *
+ * @param {DOMEventTarget} target DOM element to register listener on.
+ * @param {string} eventType Event type, e.g. 'click' or 'mouseover'.
+ * @param {function} callback Callback function.
+ * @return {object} Object with a `remove` method.
+ */
+ capture: function (target, eventType, callback) {
+ if (target.addEventListener) {
+ target.addEventListener(eventType, callback, true);
+ return {
+ remove: function () {
+ target.removeEventListener(eventType, callback, true);
+ }
+ };
+ } else {
+ if ("development" !== 'production') {
+ console.error('Attempted to listen to events during the capture phase on a ' + 'browser that does not support the capture phase. Your application ' + 'will not receive some events.');
+ }
+ return {
+ remove: emptyFunction
+ };
+ }
+ },
+
+ registerDefault: function () {}
+};
+
+module.exports = EventListener;
+},{"153":153}],147:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ExecutionEnvironment
+ */
+
+'use strict';
+
+var canUseDOM = !!(typeof window !== 'undefined' && window.document && window.document.createElement);
+
+/**
+ * Simple, lightweight module assisting with the detection and context of
+ * Worker. Helps avoid circular dependencies and allows code to reason about
+ * whether or not they are in a Worker, even if they never include the main
+ * `ReactWorker` dependency.
+ */
+var ExecutionEnvironment = {
+
+ canUseDOM: canUseDOM,
+
+ canUseWorkers: typeof Worker !== 'undefined',
+
+ canUseEventListeners: canUseDOM && !!(window.addEventListener || window.attachEvent),
+
+ canUseViewport: canUseDOM && !!window.screen,
+
+ isInWorker: !canUseDOM // For now, this is true - might change in the future.
+
+};
+
+module.exports = ExecutionEnvironment;
+},{}],148:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule camelize
+ * @typechecks
+ */
+
+"use strict";
+
+var _hyphenPattern = /-(.)/g;
+
+/**
+ * Camelcases a hyphenated string, for example:
+ *
+ * > camelize('background-color')
+ * < "backgroundColor"
+ *
+ * @param {string} string
+ * @return {string}
+ */
+function camelize(string) {
+ return string.replace(_hyphenPattern, function (_, character) {
+ return character.toUpperCase();
+ });
+}
+
+module.exports = camelize;
+},{}],149:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule camelizeStyleName
+ * @typechecks
+ */
+
+'use strict';
+
+var camelize = _dereq_(148);
+
+var msPattern = /^-ms-/;
+
+/**
+ * Camelcases a hyphenated CSS property name, for example:
+ *
+ * > camelizeStyleName('background-color')
+ * < "backgroundColor"
+ * > camelizeStyleName('-moz-transition')
+ * < "MozTransition"
+ * > camelizeStyleName('-ms-transition')
+ * < "msTransition"
+ *
+ * As Andi Smith suggests
+ * (http://www.andismith.com/blog/2012/02/modernizr-prefixed/), an `-ms` prefix
+ * is converted to lowercase `ms`.
+ *
+ * @param {string} string
+ * @return {string}
+ */
+function camelizeStyleName(string) {
+ return camelize(string.replace(msPattern, 'ms-'));
+}
+
+module.exports = camelizeStyleName;
+},{"148":148}],150:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule containsNode
+ * @typechecks
+ */
+
+'use strict';
+
+var isTextNode = _dereq_(163);
+
+/*eslint-disable no-bitwise */
+
+/**
+ * Checks if a given DOM node contains or is another DOM node.
+ *
+ * @param {?DOMNode} outerNode Outer DOM node.
+ * @param {?DOMNode} innerNode Inner DOM node.
+ * @return {boolean} True if `outerNode` contains or is `innerNode`.
+ */
+function containsNode(_x, _x2) {
+ var _again = true;
+
+ _function: while (_again) {
+ var outerNode = _x,
+ innerNode = _x2;
+ _again = false;
+
+ if (!outerNode || !innerNode) {
+ return false;
+ } else if (outerNode === innerNode) {
+ return true;
+ } else if (isTextNode(outerNode)) {
+ return false;
+ } else if (isTextNode(innerNode)) {
+ _x = outerNode;
+ _x2 = innerNode.parentNode;
+ _again = true;
+ continue _function;
+ } else if (outerNode.contains) {
+ return outerNode.contains(innerNode);
+ } else if (outerNode.compareDocumentPosition) {
+ return !!(outerNode.compareDocumentPosition(innerNode) & 16);
+ } else {
+ return false;
+ }
+ }
+}
+
+module.exports = containsNode;
+},{"163":163}],151:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule createArrayFromMixed
+ * @typechecks
+ */
+
+'use strict';
+
+var toArray = _dereq_(172);
+
+/**
+ * Perform a heuristic test to determine if an object is "array-like".
+ *
+ * A monk asked Joshu, a Zen master, "Has a dog Buddha nature?"
+ * Joshu replied: "Mu."
+ *
+ * This function determines if its argument has "array nature": it returns
+ * true if the argument is an actual array, an `arguments' object, or an
+ * HTMLCollection (e.g. node.childNodes or node.getElementsByTagName()).
+ *
+ * It will return false for other array-like objects like Filelist.
+ *
+ * @param {*} obj
+ * @return {boolean}
+ */
+function hasArrayNature(obj) {
+ return(
+ // not null/false
+ !!obj && (
+ // arrays are objects, NodeLists are functions in Safari
+ typeof obj == 'object' || typeof obj == 'function') &&
+ // quacks like an array
+ 'length' in obj &&
+ // not window
+ !('setInterval' in obj) &&
+ // no DOM node should be considered an array-like
+ // a 'select' element has 'length' and 'item' properties on IE8
+ typeof obj.nodeType != 'number' && (
+ // a real array
+ Array.isArray(obj) ||
+ // arguments
+ 'callee' in obj ||
+ // HTMLCollection/NodeList
+ 'item' in obj)
+ );
+}
+
+/**
+ * Ensure that the argument is an array by wrapping it in an array if it is not.
+ * Creates a copy of the argument if it is already an array.
+ *
+ * This is mostly useful idiomatically:
+ *
+ * var createArrayFromMixed = require('createArrayFromMixed');
+ *
+ * function takesOneOrMoreThings(things) {
+ * things = createArrayFromMixed(things);
+ * ...
+ * }
+ *
+ * This allows you to treat `things' as an array, but accept scalars in the API.
+ *
+ * If you need to convert an array-like object, like `arguments`, into an array
+ * use toArray instead.
+ *
+ * @param {*} obj
+ * @return {array}
+ */
+function createArrayFromMixed(obj) {
+ if (!hasArrayNature(obj)) {
+ return [obj];
+ } else if (Array.isArray(obj)) {
+ return obj.slice();
+ } else {
+ return toArray(obj);
+ }
+}
+
+module.exports = createArrayFromMixed;
+},{"172":172}],152:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule createNodesFromMarkup
+ * @typechecks
+ */
+
+/*eslint-disable fb-www/unsafe-html*/
+
+'use strict';
+
+var ExecutionEnvironment = _dereq_(147);
+
+var createArrayFromMixed = _dereq_(151);
+var getMarkupWrap = _dereq_(157);
+var invariant = _dereq_(161);
+
+/**
+ * Dummy container used to render all markup.
+ */
+var dummyNode = ExecutionEnvironment.canUseDOM ? document.createElementNS('http://www.w3.org/1999/xhtml', 'div') : null;
+
+/**
+ * Pattern used by `getNodeName`.
+ */
+var nodeNamePattern = /^\s*<(\w+)/;
+
+/**
+ * Extracts the `nodeName` of the first element in a string of markup.
+ *
+ * @param {string} markup String of markup.
+ * @return {?string} Node name of the supplied markup.
+ */
+function getNodeName(markup) {
+ var nodeNameMatch = markup.match(nodeNamePattern);
+ return nodeNameMatch && nodeNameMatch[1].toLowerCase();
+}
+
+/**
+ * Creates an array containing the nodes rendered from the supplied markup. The
+ * optionally supplied `handleScript` function will be invoked once for each
+ * <script> element that is rendered. If no `handleScript` function is supplied,
+ * an exception is thrown if any <script> elements are rendered.
+ *
+ * @param {string} markup A string of valid HTML markup.
+ * @param {?function} handleScript Invoked once for each rendered <script>.
+ * @return {array<DOMElement|DOMTextNode>} An array of rendered nodes.
+ */
+function createNodesFromMarkup(markup, handleScript) {
+ var node = dummyNode;
+ !!!dummyNode ? "development" !== 'production' ? invariant(false, 'createNodesFromMarkup dummy not initialized') : invariant(false) : undefined;
+ var nodeName = getNodeName(markup);
+
+ var wrap = nodeName && getMarkupWrap(nodeName);
+ if (wrap) {
+ node.innerHTML = wrap[1] + markup + wrap[2];
+
+ var wrapDepth = wrap[0];
+ while (wrapDepth--) {
+ node = node.lastChild;
+ }
+ } else {
+ node.innerHTML = markup;
+ }
+
+ var scripts = node.getElementsByTagName('script');
+ if (scripts.length) {
+ !handleScript ? "development" !== 'production' ? invariant(false, 'createNodesFromMarkup(...): Unexpected <script> element rendered.') : invariant(false) : undefined;
+ createArrayFromMixed(scripts).forEach(handleScript);
+ }
+
+ var nodes = createArrayFromMixed(node.childNodes);
+ while (node.lastChild) {
+ node.removeChild(node.lastChild);
+ }
+ return nodes;
+}
+
+module.exports = createNodesFromMarkup;
+
+},{"147":147,"151":151,"157":157,"161":161}],153:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule emptyFunction
+ */
+
+"use strict";
+
+function makeEmptyFunction(arg) {
+ return function () {
+ return arg;
+ };
+}
+
+/**
+ * This function accepts and discards inputs; it has no side effects. This is
+ * primarily useful idiomatically for overridable function endpoints which
+ * always need to be callable, since JS lacks a null-call idiom ala Cocoa.
+ */
+function emptyFunction() {}
+
+emptyFunction.thatReturns = makeEmptyFunction;
+emptyFunction.thatReturnsFalse = makeEmptyFunction(false);
+emptyFunction.thatReturnsTrue = makeEmptyFunction(true);
+emptyFunction.thatReturnsNull = makeEmptyFunction(null);
+emptyFunction.thatReturnsThis = function () {
+ return this;
+};
+emptyFunction.thatReturnsArgument = function (arg) {
+ return arg;
+};
+
+module.exports = emptyFunction;
+},{}],154:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule emptyObject
+ */
+
+'use strict';
+
+var emptyObject = {};
+
+if ("development" !== 'production') {
+ Object.freeze(emptyObject);
+}
+
+module.exports = emptyObject;
+},{}],155:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule focusNode
+ */
+
+'use strict';
+
+/**
+ * @param {DOMElement} node input/textarea to focus
+ */
+function focusNode(node) {
+ // IE8 can throw "Can't move focus to the control because it is invisible,
+ // not enabled, or of a type that does not accept the focus." for all kinds of
+ // reasons that are too expensive and fragile to test.
+ try {
+ node.focus();
+ } catch (e) {}
+}
+
+module.exports = focusNode;
+},{}],156:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule getActiveElement
+ * @typechecks
+ */
+
+/* eslint-disable fb-www/typeof-undefined */
+
+/**
+ * Same as document.activeElement but wraps in a try-catch block. In IE it is
+ * not safe to call document.activeElement if there is nothing focused.
+ *
+ * The activeElement will be null only if the document or document body is not
+ * yet defined.
+ */
+'use strict';
+
+function getActiveElement() /*?DOMElement*/{
+ if (typeof document === 'undefined') {
+ return null;
+ }
+ try {
+ return document.activeElement || document.body;
+ } catch (e) {
+ return document.body;
+ }
+}
+
+module.exports = getActiveElement;
+},{}],157:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule getMarkupWrap
+ */
+
+/*eslint-disable fb-www/unsafe-html */
+
+'use strict';
+
+var ExecutionEnvironment = _dereq_(147);
+
+var invariant = _dereq_(161);
+
+/**
+ * Dummy container used to detect which wraps are necessary.
+ */
+var dummyNode = ExecutionEnvironment.canUseDOM ? document.createElementNS('http://www.w3.org/1999/xhtml', 'div') : null;
+
+/**
+ * Some browsers cannot use `innerHTML` to render certain elements standalone,
+ * so we wrap them, render the wrapped nodes, then extract the desired node.
+ *
+ * In IE8, certain elements cannot render alone, so wrap all elements ('*').
+ */
+
+var shouldWrap = {};
+
+var selectWrap = [1, '<select multiple="true">', '</select>'];
+var tableWrap = [1, '<table>', '</table>'];
+var trWrap = [3, '<table><tbody><tr>', '</tr></tbody></table>'];
+
+var svgWrap = [1, '<svg xmlns="http://www.w3.org/2000/svg">', '</svg>'];
+
+var markupWrap = {
+ '*': [1, '?<div>', '</div>'],
+
+ 'area': [1, '<map>', '</map>'],
+ 'col': [2, '<table><tbody></tbody><colgroup>', '</colgroup></table>'],
+ 'legend': [1, '<fieldset>', '</fieldset>'],
+ 'param': [1, '<object>', '</object>'],
+ 'tr': [2, '<table><tbody>', '</tbody></table>'],
+
+ 'optgroup': selectWrap,
+ 'option': selectWrap,
+
+ 'caption': tableWrap,
+ 'colgroup': tableWrap,
+ 'tbody': tableWrap,
+ 'tfoot': tableWrap,
+ 'thead': tableWrap,
+
+ 'td': trWrap,
+ 'th': trWrap
+};
+
+// Initialize the SVG elements since we know they'll always need to be wrapped
+// consistently. If they are created inside a <div> they will be initialized in
+// the wrong namespace (and will not display).
+var svgElements = ['circle', 'clipPath', 'defs', 'ellipse', 'g', 'image', 'line', 'linearGradient', 'mask', 'path', 'pattern', 'polygon', 'polyline', 'radialGradient', 'rect', 'stop', 'text', 'tspan'];
+svgElements.forEach(function (nodeName) {
+ markupWrap[nodeName] = svgWrap;
+ shouldWrap[nodeName] = true;
+});
+
+/**
+ * Gets the markup wrap configuration for the supplied `nodeName`.
+ *
+ * NOTE: This lazily detects which wraps are necessary for the current browser.
+ *
+ * @param {string} nodeName Lowercase `nodeName`.
+ * @return {?array} Markup wrap configuration, if applicable.
+ */
+function getMarkupWrap(nodeName) {
+ !!!dummyNode ? "development" !== 'production' ? invariant(false, 'Markup wrapping node not initialized') : invariant(false) : undefined;
+ if (!markupWrap.hasOwnProperty(nodeName)) {
+ nodeName = '*';
+ }
+ if (!shouldWrap.hasOwnProperty(nodeName)) {
+ if (nodeName === '*') {
+ dummyNode.innerHTML = '<link />';
+ } else {
+ dummyNode.innerHTML = '<' + nodeName + '></' + nodeName + '>';
+ }
+ shouldWrap[nodeName] = !dummyNode.firstChild;
+ }
+ return shouldWrap[nodeName] ? markupWrap[nodeName] : null;
+}
+
+module.exports = getMarkupWrap;
+},{"147":147,"161":161}],158:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule getUnboundedScrollPosition
+ * @typechecks
+ */
+
+'use strict';
+
+/**
+ * Gets the scroll position of the supplied element or window.
+ *
+ * The return values are unbounded, unlike `getScrollPosition`. This means they
+ * may be negative or exceed the element boundaries (which is possible using
+ * inertial scrolling).
+ *
+ * @param {DOMWindow|DOMElement} scrollable
+ * @return {object} Map with `x` and `y` keys.
+ */
+function getUnboundedScrollPosition(scrollable) {
+ if (scrollable === window) {
+ return {
+ x: window.pageXOffset || document.documentElement.scrollLeft,
+ y: window.pageYOffset || document.documentElement.scrollTop
+ };
+ }
+ return {
+ x: scrollable.scrollLeft,
+ y: scrollable.scrollTop
+ };
+}
+
+module.exports = getUnboundedScrollPosition;
+},{}],159:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule hyphenate
+ * @typechecks
+ */
+
+'use strict';
+
+var _uppercasePattern = /([A-Z])/g;
+
+/**
+ * Hyphenates a camelcased string, for example:
+ *
+ * > hyphenate('backgroundColor')
+ * < "background-color"
+ *
+ * For CSS style names, use `hyphenateStyleName` instead which works properly
+ * with all vendor prefixes, including `ms`.
+ *
+ * @param {string} string
+ * @return {string}
+ */
+function hyphenate(string) {
+ return string.replace(_uppercasePattern, '-$1').toLowerCase();
+}
+
+module.exports = hyphenate;
+},{}],160:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule hyphenateStyleName
+ * @typechecks
+ */
+
+'use strict';
+
+var hyphenate = _dereq_(159);
+
+var msPattern = /^ms-/;
+
+/**
+ * Hyphenates a camelcased CSS property name, for example:
+ *
+ * > hyphenateStyleName('backgroundColor')
+ * < "background-color"
+ * > hyphenateStyleName('MozTransition')
+ * < "-moz-transition"
+ * > hyphenateStyleName('msTransition')
+ * < "-ms-transition"
+ *
+ * As Modernizr suggests (http://modernizr.com/docs/#prefixed), an `ms` prefix
+ * is converted to `-ms-`.
+ *
+ * @param {string} string
+ * @return {string}
+ */
+function hyphenateStyleName(string) {
+ return hyphenate(string).replace(msPattern, '-ms-');
+}
+
+module.exports = hyphenateStyleName;
+},{"159":159}],161:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule invariant
+ */
+
+'use strict';
+
+/**
+ * Use invariant() to assert state which your program assumes to be true.
+ *
+ * Provide sprintf-style format (only %s is supported) and arguments
+ * to provide information about what broke and what you were
+ * expecting.
+ *
+ * The invariant message will be stripped in production, but the invariant
+ * will remain to ensure logic does not differ in production.
+ */
+
+function invariant(condition, format, a, b, c, d, e, f) {
+ if ("development" !== 'production') {
+ if (format === undefined) {
+ throw new Error('invariant requires an error message argument');
+ }
+ }
+
+ if (!condition) {
+ var error;
+ if (format === undefined) {
+ error = new Error('Minified exception occurred; use the non-minified dev environment ' + 'for the full error message and additional helpful warnings.');
+ } else {
+ var args = [a, b, c, d, e, f];
+ var argIndex = 0;
+ error = new Error(format.replace(/%s/g, function () {
+ return args[argIndex++];
+ }));
+ error.name = 'Invariant Violation';
+ }
+
+ error.framesToPop = 1; // we don't care about invariant's own frame
+ throw error;
+ }
+}
+
+module.exports = invariant;
+},{}],162:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule isNode
+ * @typechecks
+ */
+
+/**
+ * @param {*} object The object to check.
+ * @return {boolean} Whether or not the object is a DOM node.
+ */
+'use strict';
+
+function isNode(object) {
+ return !!(object && (typeof Node === 'function' ? object instanceof Node : typeof object === 'object' && typeof object.nodeType === 'number' && typeof object.nodeName === 'string'));
+}
+
+module.exports = isNode;
+},{}],163:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule isTextNode
+ * @typechecks
+ */
+
+'use strict';
+
+var isNode = _dereq_(162);
+
+/**
+ * @param {*} object The object to check.
+ * @return {boolean} Whether or not the object is a DOM text node.
+ */
+function isTextNode(object) {
+ return isNode(object) && object.nodeType == 3;
+}
+
+module.exports = isTextNode;
+},{"162":162}],164:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule joinClasses
+ * @typechecks static-only
+ */
+
+'use strict';
+
+/**
+ * Combines multiple className strings into one.
+ * http://jsperf.com/joinclasses-args-vs-array
+ *
+ * @param {...?string} className
+ * @return {string}
+ */
+function joinClasses(className /*, ... */) {
+ if (!className) {
+ className = '';
+ }
+ var nextClass;
+ var argLength = arguments.length;
+ if (argLength > 1) {
+ for (var ii = 1; ii < argLength; ii++) {
+ nextClass = arguments[ii];
+ if (nextClass) {
+ className = (className ? className + ' ' : '') + nextClass;
+ }
+ }
+ }
+ return className;
+}
+
+module.exports = joinClasses;
+},{}],165:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule keyMirror
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var invariant = _dereq_(161);
+
+/**
+ * Constructs an enumeration with keys equal to their value.
+ *
+ * For example:
+ *
+ * var COLORS = keyMirror({blue: null, red: null});
+ * var myColor = COLORS.blue;
+ * var isColorValid = !!COLORS[myColor];
+ *
+ * The last line could not be performed if the values of the generated enum were
+ * not equal to their keys.
+ *
+ * Input: {key1: val1, key2: val2}
+ * Output: {key1: key1, key2: key2}
+ *
+ * @param {object} obj
+ * @return {object}
+ */
+var keyMirror = function (obj) {
+ var ret = {};
+ var key;
+ !(obj instanceof Object && !Array.isArray(obj)) ? "development" !== 'production' ? invariant(false, 'keyMirror(...): Argument must be an object.') : invariant(false) : undefined;
+ for (key in obj) {
+ if (!obj.hasOwnProperty(key)) {
+ continue;
+ }
+ ret[key] = key;
+ }
+ return ret;
+};
+
+module.exports = keyMirror;
+},{"161":161}],166:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule keyOf
+ */
+
+/**
+ * Allows extraction of a minified key. Let's the build system minify keys
+ * without losing the ability to dynamically use key strings as values
+ * themselves. Pass in an object with a single key/val pair and it will return
+ * you the string key of that single record. Suppose you want to grab the
+ * value for a key 'className' inside of an object. Key/val minification may
+ * have aliased that key to be 'xa12'. keyOf({className: null}) will return
+ * 'xa12' in that case. Resolve keys you want to use once at startup time, then
+ * reuse those resolutions.
+ */
+"use strict";
+
+var keyOf = function (oneKeyObj) {
+ var key;
+ for (key in oneKeyObj) {
+ if (!oneKeyObj.hasOwnProperty(key)) {
+ continue;
+ }
+ return key;
+ }
+ return null;
+};
+
+module.exports = keyOf;
+},{}],167:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule mapObject
+ */
+
+'use strict';
+
+var hasOwnProperty = Object.prototype.hasOwnProperty;
+
+/**
+ * Executes the provided `callback` once for each enumerable own property in the
+ * object and constructs a new object from the results. The `callback` is
+ * invoked with three arguments:
+ *
+ * - the property value
+ * - the property name
+ * - the object being traversed
+ *
+ * Properties that are added after the call to `mapObject` will not be visited
+ * by `callback`. If the values of existing properties are changed, the value
+ * passed to `callback` will be the value at the time `mapObject` visits them.
+ * Properties that are deleted before being visited are not visited.
+ *
+ * @grep function objectMap()
+ * @grep function objMap()
+ *
+ * @param {?object} object
+ * @param {function} callback
+ * @param {*} context
+ * @return {?object}
+ */
+function mapObject(object, callback, context) {
+ if (!object) {
+ return null;
+ }
+ var result = {};
+ for (var name in object) {
+ if (hasOwnProperty.call(object, name)) {
+ result[name] = callback.call(context, object[name], name, object);
+ }
+ }
+ return result;
+}
+
+module.exports = mapObject;
+},{}],168:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule memoizeStringOnly
+ * @typechecks static-only
+ */
+
+'use strict';
+
+/**
+ * Memoizes the return value of a function that accepts one string argument.
+ *
+ * @param {function} callback
+ * @return {function}
+ */
+function memoizeStringOnly(callback) {
+ var cache = {};
+ return function (string) {
+ if (!cache.hasOwnProperty(string)) {
+ cache[string] = callback.call(this, string);
+ }
+ return cache[string];
+ };
+}
+
+module.exports = memoizeStringOnly;
+},{}],169:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule performance
+ * @typechecks
+ */
+
+'use strict';
+
+var ExecutionEnvironment = _dereq_(147);
+
+var performance;
+
+if (ExecutionEnvironment.canUseDOM) {
+ performance = window.performance || window.msPerformance || window.webkitPerformance;
+}
+
+module.exports = performance || {};
+},{"147":147}],170:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule performanceNow
+ * @typechecks
+ */
+
+'use strict';
+
+var performance = _dereq_(169);
+
+var performanceNow;
+
+/**
+ * Detect if we can use `window.performance.now()` and gracefully fallback to
+ * `Date.now()` if it doesn't exist. We need to support Firefox < 15 for now
+ * because of Facebook's testing infrastructure.
+ */
+if (performance.now) {
+ performanceNow = function () {
+ return performance.now();
+ };
+} else {
+ performanceNow = function () {
+ return Date.now();
+ };
+}
+
+module.exports = performanceNow;
+},{"169":169}],171:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule shallowEqual
+ * @typechecks
+ *
+ */
+
+'use strict';
+
+var hasOwnProperty = Object.prototype.hasOwnProperty;
+
+/**
+ * Performs equality by iterating through keys on an object and returning false
+ * when any key has values which are not strictly equal between the arguments.
+ * Returns true when the values of all keys are strictly equal.
+ */
+function shallowEqual(objA, objB) {
+ if (objA === objB) {
+ return true;
+ }
+
+ if (typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null) {
+ return false;
+ }
+
+ var keysA = Object.keys(objA);
+ var keysB = Object.keys(objB);
+
+ if (keysA.length !== keysB.length) {
+ return false;
+ }
+
+ // Test for A's keys different from B.
+ var bHasOwnProperty = hasOwnProperty.bind(objB);
+ for (var i = 0; i < keysA.length; i++) {
+ if (!bHasOwnProperty(keysA[i]) || objA[keysA[i]] !== objB[keysA[i]]) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+module.exports = shallowEqual;
+},{}],172:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule toArray
+ * @typechecks
+ */
+
+'use strict';
+
+var invariant = _dereq_(161);
+
+/**
+ * Convert array-like objects to arrays.
+ *
+ * This API assumes the caller knows the contents of the data type. For less
+ * well defined inputs use createArrayFromMixed.
+ *
+ * @param {object|function|filelist} obj
+ * @return {array}
+ */
+function toArray(obj) {
+ var length = obj.length;
+
+ // Some browse builtin objects can report typeof 'function' (e.g. NodeList in
+ // old versions of Safari).
+ !(!Array.isArray(obj) && (typeof obj === 'object' || typeof obj === 'function')) ? "development" !== 'production' ? invariant(false, 'toArray: Array-like object expected') : invariant(false) : undefined;
+
+ !(typeof length === 'number') ? "development" !== 'production' ? invariant(false, 'toArray: Object needs a length property') : invariant(false) : undefined;
+
+ !(length === 0 || length - 1 in obj) ? "development" !== 'production' ? invariant(false, 'toArray: Object should have keys for indices') : invariant(false) : undefined;
+
+ // Old IE doesn't give collections access to hasOwnProperty. Assume inputs
+ // without method will throw during the slice call and skip straight to the
+ // fallback.
+ if (obj.hasOwnProperty) {
+ try {
+ return Array.prototype.slice.call(obj);
+ } catch (e) {
+ // IE < 9 does not support Array#slice on collections objects
+ }
+ }
+
+ // Fall back to copying key by key. This assumes all keys have a value,
+ // so will not preserve sparsely populated inputs.
+ var ret = Array(length);
+ for (var ii = 0; ii < length; ii++) {
+ ret[ii] = obj[ii];
+ }
+ return ret;
+}
+
+module.exports = toArray;
+},{"161":161}],173:[function(_dereq_,module,exports){
+/**
+ * Copyright 2014-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule warning
+ */
+
+'use strict';
+
+var emptyFunction = _dereq_(153);
+
+/**
+ * Similar to invariant but only logs a warning if the condition is not met.
+ * This can be used to log issues in development environments in critical
+ * paths. Removing the logging code for production environments will keep the
+ * same logic and follow the same code paths.
+ */
+
+var warning = emptyFunction;
+
+if ("development" !== 'production') {
+ warning = function (condition, format) {
+ for (var _len = arguments.length, args = Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) {
+ args[_key - 2] = arguments[_key];
+ }
+
+ if (format === undefined) {
+ throw new Error('`warning(condition, format, ...args)` requires a warning ' + 'message argument');
+ }
+
+ if (format.indexOf('Failed Composite propType: ') === 0) {
+ return; // Ignore CompositeComponent proptype check.
+ }
+
+ if (!condition) {
+ var argIndex = 0;
+ var message = 'Warning: ' + format.replace(/%s/g, function () {
+ return args[argIndex++];
+ });
+ if (typeof console !== 'undefined') {
+ console.error(message);
+ }
+ try {
+ // --- Welcome to debugging React ---
+ // This error was thrown as a convenience so that you can use this stack
+ // to find the callsite that caused this warning to fire.
+ throw new Error(message);
+ } catch (x) {}
+ }
+ };
+}
+
+module.exports = warning;
+},{"153":153}]},{},[1])(1)
+});
diff --git a/devtools/client/shared/vendor/react-dom.js b/devtools/client/shared/vendor/react-dom.js
new file mode 100644
index 000000000..f7c77bd90
--- /dev/null
+++ b/devtools/client/shared/vendor/react-dom.js
@@ -0,0 +1,42 @@
+/**
+ * ReactDOM v0.14.1
+ *
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ */
+// Based off https://github.com/ForbesLindesay/umd/blob/master/template.js
+;(function(f) {
+ // CommonJS
+ if (typeof exports === "object" && typeof module !== "undefined") {
+ module.exports = f(require('devtools/client/shared/vendor/react'));
+
+ // RequireJS
+ } else if (typeof define === "function" && define.amd) {
+ define(['devtools/client/shared/vendor/react'], f);
+
+ // <script>
+ } else {
+ var g
+ if (typeof window !== "undefined") {
+ g = window;
+ } else if (typeof global !== "undefined") {
+ g = global;
+ } else if (typeof self !== "undefined") {
+ g = self;
+ } else {
+ // works providing we're not in "use strict";
+ // needed for Java 8 Nashorn
+ // see https://github.com/facebook/react/issues/3037
+ g = this;
+ }
+ g.ReactDOM = f(g.React);
+ }
+
+})(function(React) {
+ return React.__SECRET_DOM_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;
+});
diff --git a/devtools/client/shared/vendor/react-proxy.js b/devtools/client/shared/vendor/react-proxy.js
new file mode 100644
index 000000000..95346a026
--- /dev/null
+++ b/devtools/client/shared/vendor/react-proxy.js
@@ -0,0 +1,1909 @@
+(function webpackUniversalModuleDefinition(root, factory) {
+ if(typeof exports === 'object' && typeof module === 'object')
+ module.exports = factory();
+ else if(typeof define === 'function' && define.amd)
+ define(factory);
+ else if(typeof exports === 'object')
+ exports["ReactProxy"] = factory();
+ else
+ root["ReactProxy"] = factory();
+})(this, function() {
+return /******/ (function(modules) { // webpackBootstrap
+/******/ // The module cache
+/******/ var installedModules = {};
+/******/
+/******/ // The require function
+/******/ function __webpack_require__(moduleId) {
+/******/
+/******/ // Check if module is in cache
+/******/ if(installedModules[moduleId])
+/******/ return installedModules[moduleId].exports;
+/******/
+/******/ // Create a new module (and put it into the cache)
+/******/ var module = installedModules[moduleId] = {
+/******/ exports: {},
+/******/ id: moduleId,
+/******/ loaded: false
+/******/ };
+/******/
+/******/ // Execute the module function
+/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
+/******/
+/******/ // Flag the module as loaded
+/******/ module.loaded = true;
+/******/
+/******/ // Return the exports of the module
+/******/ return module.exports;
+/******/ }
+/******/
+/******/
+/******/ // expose the modules object (__webpack_modules__)
+/******/ __webpack_require__.m = modules;
+/******/
+/******/ // expose the module cache
+/******/ __webpack_require__.c = installedModules;
+/******/
+/******/ // __webpack_public_path__
+/******/ __webpack_require__.p = "";
+/******/
+/******/ // Load entry module and return exports
+/******/ return __webpack_require__(0);
+/******/ })
+/************************************************************************/
+/******/ ([
+/* 0 */
+/***/ function(module, exports, __webpack_require__) {
+
+ 'use strict';
+
+ Object.defineProperty(exports, '__esModule', {
+ value: true
+ });
+
+ function _interopRequire(obj) { return obj && obj.__esModule ? obj['default'] : obj; }
+
+ var _createClassProxy = __webpack_require__(12);
+
+ exports.createProxy = _interopRequire(_createClassProxy);
+
+ var _reactDeepForceUpdate = __webpack_require__(39);
+
+ exports.getForceUpdate = _interopRequire(_reactDeepForceUpdate);
+
+/***/ },
+/* 1 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /**
+ * Checks if `value` is the [language type](https://es5.github.io/#x8) of `Object`.
+ * (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`)
+ *
+ * @static
+ * @memberOf _
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is an object, else `false`.
+ * @example
+ *
+ * _.isObject({});
+ * // => true
+ *
+ * _.isObject([1, 2, 3]);
+ * // => true
+ *
+ * _.isObject(1);
+ * // => false
+ */
+ function isObject(value) {
+ // Avoid a V8 JIT bug in Chrome 19-20.
+ // See https://code.google.com/p/v8/issues/detail?id=2291 for more details.
+ var type = typeof value;
+ return !!value && (type == 'object' || type == 'function');
+ }
+
+ module.exports = isObject;
+
+
+/***/ },
+/* 2 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var getLength = __webpack_require__(30),
+ isLength = __webpack_require__(5);
+
+ /**
+ * Checks if `value` is array-like.
+ *
+ * @private
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is array-like, else `false`.
+ */
+ function isArrayLike(value) {
+ return value != null && isLength(getLength(value));
+ }
+
+ module.exports = isArrayLike;
+
+
+/***/ },
+/* 3 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /**
+ * Checks if `value` is object-like.
+ *
+ * @private
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is object-like, else `false`.
+ */
+ function isObjectLike(value) {
+ return !!value && typeof value == 'object';
+ }
+
+ module.exports = isObjectLike;
+
+
+/***/ },
+/* 4 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var isNative = __webpack_require__(35);
+
+ /**
+ * Gets the native function at `key` of `object`.
+ *
+ * @private
+ * @param {Object} object The object to query.
+ * @param {string} key The key of the method to get.
+ * @returns {*} Returns the function if it's native, else `undefined`.
+ */
+ function getNative(object, key) {
+ var value = object == null ? undefined : object[key];
+ return isNative(value) ? value : undefined;
+ }
+
+ module.exports = getNative;
+
+
+/***/ },
+/* 5 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /**
+ * Used as the [maximum length](http://ecma-international.org/ecma-262/6.0/#sec-number.max_safe_integer)
+ * of an array-like value.
+ */
+ var MAX_SAFE_INTEGER = 9007199254740991;
+
+ /**
+ * Checks if `value` is a valid array-like length.
+ *
+ * **Note:** This function is based on [`ToLength`](http://ecma-international.org/ecma-262/6.0/#sec-tolength).
+ *
+ * @private
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is a valid length, else `false`.
+ */
+ function isLength(value) {
+ return typeof value == 'number' && value > -1 && value % 1 == 0 && value <= MAX_SAFE_INTEGER;
+ }
+
+ module.exports = isLength;
+
+
+/***/ },
+/* 6 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /** Used to detect unsigned integer values. */
+ var reIsUint = /^\d+$/;
+
+ /**
+ * Used as the [maximum length](http://ecma-international.org/ecma-262/6.0/#sec-number.max_safe_integer)
+ * of an array-like value.
+ */
+ var MAX_SAFE_INTEGER = 9007199254740991;
+
+ /**
+ * Checks if `value` is a valid array-like index.
+ *
+ * @private
+ * @param {*} value The value to check.
+ * @param {number} [length=MAX_SAFE_INTEGER] The upper bounds of a valid index.
+ * @returns {boolean} Returns `true` if `value` is a valid index, else `false`.
+ */
+ function isIndex(value, length) {
+ value = (typeof value == 'number' || reIsUint.test(value)) ? +value : -1;
+ length = length == null ? MAX_SAFE_INTEGER : length;
+ return value > -1 && value % 1 == 0 && value < length;
+ }
+
+ module.exports = isIndex;
+
+
+/***/ },
+/* 7 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var isArrayLike = __webpack_require__(2),
+ isObjectLike = __webpack_require__(3);
+
+ /** Used for native method references. */
+ var objectProto = Object.prototype;
+
+ /** Used to check objects for own properties. */
+ var hasOwnProperty = objectProto.hasOwnProperty;
+
+ /** Native method references. */
+ var propertyIsEnumerable = objectProto.propertyIsEnumerable;
+
+ /**
+ * Checks if `value` is classified as an `arguments` object.
+ *
+ * @static
+ * @memberOf _
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is correctly classified, else `false`.
+ * @example
+ *
+ * _.isArguments(function() { return arguments; }());
+ * // => true
+ *
+ * _.isArguments([1, 2, 3]);
+ * // => false
+ */
+ function isArguments(value) {
+ return isObjectLike(value) && isArrayLike(value) &&
+ hasOwnProperty.call(value, 'callee') && !propertyIsEnumerable.call(value, 'callee');
+ }
+
+ module.exports = isArguments;
+
+
+/***/ },
+/* 8 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var getNative = __webpack_require__(4),
+ isLength = __webpack_require__(5),
+ isObjectLike = __webpack_require__(3);
+
+ /** `Object#toString` result references. */
+ var arrayTag = '[object Array]';
+
+ /** Used for native method references. */
+ var objectProto = Object.prototype;
+
+ /**
+ * Used to resolve the [`toStringTag`](http://ecma-international.org/ecma-262/6.0/#sec-object.prototype.tostring)
+ * of values.
+ */
+ var objToString = objectProto.toString;
+
+ /* Native method references for those with the same name as other `lodash` methods. */
+ var nativeIsArray = getNative(Array, 'isArray');
+
+ /**
+ * Checks if `value` is classified as an `Array` object.
+ *
+ * @static
+ * @memberOf _
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is correctly classified, else `false`.
+ * @example
+ *
+ * _.isArray([1, 2, 3]);
+ * // => true
+ *
+ * _.isArray(function() { return arguments; }());
+ * // => false
+ */
+ var isArray = nativeIsArray || function(value) {
+ return isObjectLike(value) && isLength(value.length) && objToString.call(value) == arrayTag;
+ };
+
+ module.exports = isArray;
+
+
+/***/ },
+/* 9 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /** Used as the `TypeError` message for "Functions" methods. */
+ var FUNC_ERROR_TEXT = 'Expected a function';
+
+ /* Native method references for those with the same name as other `lodash` methods. */
+ var nativeMax = Math.max;
+
+ /**
+ * Creates a function that invokes `func` with the `this` binding of the
+ * created function and arguments from `start` and beyond provided as an array.
+ *
+ * **Note:** This method is based on the [rest parameter](https://developer.mozilla.org/Web/JavaScript/Reference/Functions/rest_parameters).
+ *
+ * @static
+ * @memberOf _
+ * @category Function
+ * @param {Function} func The function to apply a rest parameter to.
+ * @param {number} [start=func.length-1] The start position of the rest parameter.
+ * @returns {Function} Returns the new function.
+ * @example
+ *
+ * var say = _.restParam(function(what, names) {
+ * return what + ' ' + _.initial(names).join(', ') +
+ * (_.size(names) > 1 ? ', & ' : '') + _.last(names);
+ * });
+ *
+ * say('hello', 'fred', 'barney', 'pebbles');
+ * // => 'hello fred, barney, & pebbles'
+ */
+ function restParam(func, start) {
+ if (typeof func != 'function') {
+ throw new TypeError(FUNC_ERROR_TEXT);
+ }
+ start = nativeMax(start === undefined ? (func.length - 1) : (+start || 0), 0);
+ return function() {
+ var args = arguments,
+ index = -1,
+ length = nativeMax(args.length - start, 0),
+ rest = Array(length);
+
+ while (++index < length) {
+ rest[index] = args[start + index];
+ }
+ switch (start) {
+ case 0: return func.call(this, rest);
+ case 1: return func.call(this, args[0], rest);
+ case 2: return func.call(this, args[0], args[1], rest);
+ }
+ var otherArgs = Array(start + 1);
+ index = -1;
+ while (++index < start) {
+ otherArgs[index] = args[index];
+ }
+ otherArgs[start] = rest;
+ return func.apply(this, otherArgs);
+ };
+ }
+
+ module.exports = restParam;
+
+
+/***/ },
+/* 10 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var getNative = __webpack_require__(4),
+ isArrayLike = __webpack_require__(2),
+ isObject = __webpack_require__(1),
+ shimKeys = __webpack_require__(33);
+
+ /* Native method references for those with the same name as other `lodash` methods. */
+ var nativeKeys = getNative(Object, 'keys');
+
+ /**
+ * Creates an array of the own enumerable property names of `object`.
+ *
+ * **Note:** Non-object values are coerced to objects. See the
+ * [ES spec](http://ecma-international.org/ecma-262/6.0/#sec-object.keys)
+ * for more details.
+ *
+ * @static
+ * @memberOf _
+ * @category Object
+ * @param {Object} object The object to query.
+ * @returns {Array} Returns the array of property names.
+ * @example
+ *
+ * function Foo() {
+ * this.a = 1;
+ * this.b = 2;
+ * }
+ *
+ * Foo.prototype.c = 3;
+ *
+ * _.keys(new Foo);
+ * // => ['a', 'b'] (iteration order is not guaranteed)
+ *
+ * _.keys('hi');
+ * // => ['0', '1']
+ */
+ var keys = !nativeKeys ? shimKeys : function(object) {
+ var Ctor = object == null ? undefined : object.constructor;
+ if ((typeof Ctor == 'function' && Ctor.prototype === object) ||
+ (typeof object != 'function' && isArrayLike(object))) {
+ return shimKeys(object);
+ }
+ return isObject(object) ? nativeKeys(object) : [];
+ };
+
+ module.exports = keys;
+
+
+/***/ },
+/* 11 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of React source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * Original:
+ * https://github.com/facebook/react/blob/6508b1ad273a6f371e8d90ae676e5390199461b4/src/isomorphic/classic/class/ReactClass.js#L650-L713
+ */
+
+ 'use strict';
+
+ Object.defineProperty(exports, '__esModule', {
+ value: true
+ });
+ exports['default'] = bindAutoBindMethods;
+ function bindAutoBindMethod(component, method) {
+ var boundMethod = method.bind(component);
+
+ boundMethod.__reactBoundContext = component;
+ boundMethod.__reactBoundMethod = method;
+ boundMethod.__reactBoundArguments = null;
+
+ var componentName = component.constructor.displayName,
+ _bind = boundMethod.bind;
+
+ boundMethod.bind = function (newThis) {
+ var args = Array.prototype.slice.call(arguments, 1);
+ if (newThis !== component && newThis !== null) {
+ console.warn('bind(): React component methods may only be bound to the ' + 'component instance. See ' + componentName);
+ } else if (!args.length) {
+ console.warn('bind(): You are binding a component method to the component. ' + 'React does this for you automatically in a high-performance ' + 'way, so you can safely remove this call. See ' + componentName);
+ return boundMethod;
+ }
+
+ var reboundMethod = _bind.apply(boundMethod, arguments);
+ reboundMethod.__reactBoundContext = component;
+ reboundMethod.__reactBoundMethod = method;
+ reboundMethod.__reactBoundArguments = args;
+
+ return reboundMethod;
+ };
+
+ return boundMethod;
+ }
+
+ function bindAutoBindMethods(component) {
+ for (var autoBindKey in component.__reactAutoBindMap) {
+ if (!component.__reactAutoBindMap.hasOwnProperty(autoBindKey)) {
+ return;
+ }
+
+ // Tweak: skip methods that are already bound.
+ // This is to preserve method reference in case it is used
+ // as a subscription handler that needs to be detached later.
+ if (component.hasOwnProperty(autoBindKey) && component[autoBindKey].__reactBoundContext === component) {
+ continue;
+ }
+
+ var method = component.__reactAutoBindMap[autoBindKey];
+ component[autoBindKey] = bindAutoBindMethod(component, method);
+ }
+ }
+
+ ;
+ module.exports = exports['default'];
+
+/***/ },
+/* 12 */
+/***/ function(module, exports, __webpack_require__) {
+
+ 'use strict';
+
+ Object.defineProperty(exports, '__esModule', {
+ value: true
+ });
+
+ var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
+
+ exports['default'] = proxyClass;
+
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
+
+ var _createPrototypeProxy = __webpack_require__(13);
+
+ var _createPrototypeProxy2 = _interopRequireDefault(_createPrototypeProxy);
+
+ var _bindAutoBindMethods = __webpack_require__(11);
+
+ var _bindAutoBindMethods2 = _interopRequireDefault(_bindAutoBindMethods);
+
+ var _deleteUnknownAutoBindMethods = __webpack_require__(14);
+
+ var _deleteUnknownAutoBindMethods2 = _interopRequireDefault(_deleteUnknownAutoBindMethods);
+
+ var RESERVED_STATICS = ['length', 'name', 'arguments', 'caller', 'prototype', 'toString'];
+
+ function isEqualDescriptor(a, b) {
+ if (!a && !b) {
+ return true;
+ }
+ if (!a || !b) {
+ return false;
+ }
+ for (var key in a) {
+ if (a[key] !== b[key]) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ function proxyClass(InitialClass) {
+ // Prevent double wrapping.
+ // Given a proxy class, return the existing proxy managing it.
+ if (Object.prototype.hasOwnProperty.call(InitialClass, '__reactPatchProxy')) {
+ return InitialClass.__reactPatchProxy;
+ }
+
+ var prototypeProxy = (0, _createPrototypeProxy2['default'])();
+ var CurrentClass = undefined;
+
+ var staticDescriptors = {};
+ function wasStaticModifiedByUser(key) {
+ // Compare the descriptor with the one we previously set ourselves.
+ var currentDescriptor = Object.getOwnPropertyDescriptor(ProxyClass, key);
+ return !isEqualDescriptor(staticDescriptors[key], currentDescriptor);
+ }
+
+ var ProxyClass = undefined;
+ try {
+ // Create a proxy constructor with matching name
+ ProxyClass = new Function('getCurrentClass', 'return function ' + (InitialClass.name || 'ProxyClass') + '() {\n return getCurrentClass().apply(this, arguments);\n }')(function () {
+ return CurrentClass;
+ });
+ } catch (err) {
+ // Some environments may forbid dynamic evaluation
+ ProxyClass = function () {
+ return CurrentClass.apply(this, arguments);
+ };
+ }
+
+ // Point proxy constructor to the proxy prototype
+ ProxyClass.prototype = prototypeProxy.get();
+
+ // Proxy toString() to the current constructor
+ ProxyClass.toString = function toString() {
+ return CurrentClass.toString();
+ };
+
+ function update(_x) {
+ var _again = true;
+
+ _function: while (_again) {
+ var NextClass = _x;
+ mountedInstances = undefined;
+ _again = false;
+
+ if (typeof NextClass !== 'function') {
+ throw new Error('Expected a constructor.');
+ }
+
+ // Prevent proxy cycles
+ if (Object.prototype.hasOwnProperty.call(NextClass, '__reactPatchProxy')) {
+ _x = NextClass.__reactPatchProxy.__getCurrent();
+ _again = true;
+ continue _function;
+ }
+
+ // Save the next constructor so we call it
+ CurrentClass = NextClass;
+
+ // Update the prototype proxy with new methods
+ var mountedInstances = prototypeProxy.update(NextClass.prototype);
+
+ // Set up the constructor property so accessing the statics work
+ ProxyClass.prototype.constructor = ProxyClass;
+
+ // Set up the same prototype for inherited statics
+ ProxyClass.__proto__ = NextClass.__proto__;
+
+ // Copy static methods and properties
+ Object.getOwnPropertyNames(NextClass).forEach(function (key) {
+ if (RESERVED_STATICS.indexOf(key) > -1) {
+ return;
+ }
+
+ var staticDescriptor = _extends({}, Object.getOwnPropertyDescriptor(NextClass, key), {
+ configurable: true
+ });
+
+ // Copy static unless user has redefined it at runtime
+ if (!wasStaticModifiedByUser(key)) {
+ Object.defineProperty(ProxyClass, key, staticDescriptor);
+ staticDescriptors[key] = staticDescriptor;
+ }
+ });
+
+ // Remove old static methods and properties
+ Object.getOwnPropertyNames(ProxyClass).forEach(function (key) {
+ if (RESERVED_STATICS.indexOf(key) > -1) {
+ return;
+ }
+
+ // Skip statics that exist on the next class
+ if (NextClass.hasOwnProperty(key)) {
+ return;
+ }
+
+ // Skip non-configurable statics
+ var descriptor = Object.getOwnPropertyDescriptor(ProxyClass, key);
+ if (descriptor && !descriptor.configurable) {
+ return;
+ }
+
+ // Delete static unless user has redefined it at runtime
+ if (!wasStaticModifiedByUser(key)) {
+ delete ProxyClass[key];
+ delete staticDescriptors[key];
+ }
+ });
+
+ // Try to infer displayName
+ ProxyClass.displayName = NextClass.displayName || NextClass.name;
+
+ // We might have added new methods that need to be auto-bound
+ mountedInstances.forEach(_bindAutoBindMethods2['default']);
+ mountedInstances.forEach(_deleteUnknownAutoBindMethods2['default']);
+
+ // Let the user take care of redrawing
+ return mountedInstances;
+ }
+ };
+
+ function get() {
+ return ProxyClass;
+ }
+
+ function getCurrent() {
+ return CurrentClass;
+ }
+
+ update(InitialClass);
+
+ var proxy = { get: get, update: update };
+
+ Object.defineProperty(proxy, '__getCurrent', {
+ configurable: false,
+ writable: false,
+ enumerable: false,
+ value: getCurrent
+ });
+
+ Object.defineProperty(ProxyClass, '__reactPatchProxy', {
+ configurable: false,
+ writable: false,
+ enumerable: false,
+ value: proxy
+ });
+
+ return proxy;
+ }
+
+ module.exports = exports['default'];
+
+/***/ },
+/* 13 */
+/***/ function(module, exports, __webpack_require__) {
+
+ 'use strict';
+
+ Object.defineProperty(exports, '__esModule', {
+ value: true
+ });
+ exports['default'] = createPrototypeProxy;
+
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
+
+ var _lodashObjectAssign = __webpack_require__(36);
+
+ var _lodashObjectAssign2 = _interopRequireDefault(_lodashObjectAssign);
+
+ var _lodashArrayDifference = __webpack_require__(15);
+
+ var _lodashArrayDifference2 = _interopRequireDefault(_lodashArrayDifference);
+
+ function createPrototypeProxy() {
+ var proxy = {};
+ var current = null;
+ var mountedInstances = [];
+
+ /**
+ * Creates a proxied toString() method pointing to the current version's toString().
+ */
+ function proxyToString(name) {
+ // Wrap to always call the current version
+ return function toString() {
+ if (typeof current[name] === 'function') {
+ return current[name].toString();
+ } else {
+ return '<method was deleted>';
+ }
+ };
+ }
+
+ /**
+ * Creates a proxied method that calls the current version, whenever available.
+ */
+ function proxyMethod(name) {
+ // Wrap to always call the current version
+ var proxiedMethod = function proxiedMethod() {
+ if (typeof current[name] === 'function') {
+ return current[name].apply(this, arguments);
+ }
+ };
+
+ // Copy properties of the original function, if any
+ (0, _lodashObjectAssign2['default'])(proxiedMethod, current[name]);
+ proxiedMethod.toString = proxyToString(name);
+
+ return proxiedMethod;
+ }
+
+ /**
+ * Augments the original componentDidMount with instance tracking.
+ */
+ function proxiedComponentDidMount() {
+ mountedInstances.push(this);
+ if (typeof current.componentDidMount === 'function') {
+ return current.componentDidMount.apply(this, arguments);
+ }
+ }
+ proxiedComponentDidMount.toString = proxyToString('componentDidMount');
+
+ /**
+ * Augments the original componentWillUnmount with instance tracking.
+ */
+ function proxiedComponentWillUnmount() {
+ var index = mountedInstances.indexOf(this);
+ // Unless we're in a weird environment without componentDidMount
+ if (index !== -1) {
+ mountedInstances.splice(index, 1);
+ }
+ if (typeof current.componentWillUnmount === 'function') {
+ return current.componentWillUnmount.apply(this, arguments);
+ }
+ }
+ proxiedComponentWillUnmount.toString = proxyToString('componentWillUnmount');
+
+ /**
+ * Defines a property on the proxy.
+ */
+ function defineProxyProperty(name, descriptor) {
+ Object.defineProperty(proxy, name, descriptor);
+ }
+
+ /**
+ * Defines a property, attempting to keep the original descriptor configuration.
+ */
+ function defineProxyPropertyWithValue(name, value) {
+ var _ref = Object.getOwnPropertyDescriptor(current, name) || {};
+
+ var _ref$enumerable = _ref.enumerable;
+ var enumerable = _ref$enumerable === undefined ? false : _ref$enumerable;
+ var _ref$writable = _ref.writable;
+ var writable = _ref$writable === undefined ? true : _ref$writable;
+
+ defineProxyProperty(name, {
+ configurable: true,
+ enumerable: enumerable,
+ writable: writable,
+ value: value
+ });
+ }
+
+ /**
+ * Creates an auto-bind map mimicking the original map, but directed at proxy.
+ */
+ function createAutoBindMap() {
+ if (!current.__reactAutoBindMap) {
+ return;
+ }
+
+ var __reactAutoBindMap = {};
+ for (var _name in current.__reactAutoBindMap) {
+ if (current.__reactAutoBindMap.hasOwnProperty(_name)) {
+ __reactAutoBindMap[_name] = proxy[_name];
+ }
+ }
+
+ return __reactAutoBindMap;
+ }
+
+ /**
+ * Applies the updated prototype.
+ */
+ function update(next) {
+ // Save current source of truth
+ current = next;
+
+ // Find changed property names
+ var currentNames = Object.getOwnPropertyNames(current);
+ var previousName = Object.getOwnPropertyNames(proxy);
+ var addedNames = (0, _lodashArrayDifference2['default'])(currentNames, previousName);
+ var removedNames = (0, _lodashArrayDifference2['default'])(previousName, currentNames);
+
+ // Remove properties and methods that are no longer there
+ removedNames.forEach(function (name) {
+ delete proxy[name];
+ });
+
+ // Copy every descriptor
+ currentNames.forEach(function (name) {
+ var descriptor = Object.getOwnPropertyDescriptor(current, name);
+ if (typeof descriptor.value === 'function') {
+ // Functions require additional wrapping so they can be bound later
+ defineProxyPropertyWithValue(name, proxyMethod(name));
+ } else {
+ // Other values can be copied directly
+ defineProxyProperty(name, descriptor);
+ }
+ });
+
+ // Track mounting and unmounting
+ defineProxyPropertyWithValue('componentDidMount', proxiedComponentDidMount);
+ defineProxyPropertyWithValue('componentWillUnmount', proxiedComponentWillUnmount);
+ defineProxyPropertyWithValue('__reactAutoBindMap', createAutoBindMap());
+
+ // Set up the prototype chain
+ proxy.__proto__ = next;
+
+ return mountedInstances;
+ }
+
+ /**
+ * Returns the up-to-date proxy prototype.
+ */
+ function get() {
+ return proxy;
+ }
+
+ return {
+ update: update,
+ get: get
+ };
+ }
+
+ ;
+ module.exports = exports['default'];
+
+/***/ },
+/* 14 */
+/***/ function(module, exports, __webpack_require__) {
+
+ 'use strict';
+
+ Object.defineProperty(exports, '__esModule', {
+ value: true
+ });
+ exports['default'] = deleteUnknownAutoBindMethods;
+ function shouldDeleteClassicInstanceMethod(component, name) {
+ if (component.__reactAutoBindMap.hasOwnProperty(name)) {
+ // It's a known autobound function, keep it
+ return false;
+ }
+
+ if (component[name].__reactBoundArguments !== null) {
+ // It's a function bound to specific args, keep it
+ return false;
+ }
+
+ // It's a cached bound method for a function
+ // that was deleted by user, so we delete it from component.
+ return true;
+ }
+
+ function shouldDeleteModernInstanceMethod(component, name) {
+ var prototype = component.constructor.prototype;
+
+ var prototypeDescriptor = Object.getOwnPropertyDescriptor(prototype, name);
+
+ if (!prototypeDescriptor || !prototypeDescriptor.get) {
+ // This is definitely not an autobinding getter
+ return false;
+ }
+
+ if (prototypeDescriptor.get().length !== component[name].length) {
+ // The length doesn't match, bail out
+ return false;
+ }
+
+ // This seems like a method bound using an autobinding getter on the prototype
+ // Hopefully we won't run into too many false positives.
+ return true;
+ }
+
+ function shouldDeleteInstanceMethod(component, name) {
+ var descriptor = Object.getOwnPropertyDescriptor(component, name);
+ if (typeof descriptor.value !== 'function') {
+ // Not a function, or something fancy: bail out
+ return;
+ }
+
+ if (component.__reactAutoBindMap) {
+ // Classic
+ return shouldDeleteClassicInstanceMethod(component, name);
+ } else {
+ // Modern
+ return shouldDeleteModernInstanceMethod(component, name);
+ }
+ }
+
+ /**
+ * Deletes autobound methods from the instance.
+ *
+ * For classic React classes, we only delete the methods that no longer exist in map.
+ * This means the user actually deleted them in code.
+ *
+ * For modern classes, we delete methods that exist on prototype with the same length,
+ * and which have getters on prototype, but are normal values on the instance.
+ * This is usually an indication that an autobinding decorator is being used,
+ * and the getter will re-generate the memoized handler on next access.
+ */
+
+ function deleteUnknownAutoBindMethods(component) {
+ var names = Object.getOwnPropertyNames(component);
+
+ names.forEach(function (name) {
+ if (shouldDeleteInstanceMethod(component, name)) {
+ delete component[name];
+ }
+ });
+ }
+
+ module.exports = exports['default'];
+
+/***/ },
+/* 15 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var baseDifference = __webpack_require__(21),
+ baseFlatten = __webpack_require__(22),
+ isArrayLike = __webpack_require__(2),
+ isObjectLike = __webpack_require__(3),
+ restParam = __webpack_require__(9);
+
+ /**
+ * Creates an array of unique `array` values not included in the other
+ * provided arrays using [`SameValueZero`](http://ecma-international.org/ecma-262/6.0/#sec-samevaluezero)
+ * for equality comparisons.
+ *
+ * @static
+ * @memberOf _
+ * @category Array
+ * @param {Array} array The array to inspect.
+ * @param {...Array} [values] The arrays of values to exclude.
+ * @returns {Array} Returns the new array of filtered values.
+ * @example
+ *
+ * _.difference([1, 2, 3], [4, 2]);
+ * // => [1, 3]
+ */
+ var difference = restParam(function(array, values) {
+ return (isObjectLike(array) && isArrayLike(array))
+ ? baseDifference(array, baseFlatten(values, false, true))
+ : [];
+ });
+
+ module.exports = difference;
+
+
+/***/ },
+/* 16 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /* WEBPACK VAR INJECTION */(function(global) {var cachePush = __webpack_require__(27),
+ getNative = __webpack_require__(4);
+
+ /** Native method references. */
+ var Set = getNative(global, 'Set');
+
+ /* Native method references for those with the same name as other `lodash` methods. */
+ var nativeCreate = getNative(Object, 'create');
+
+ /**
+ *
+ * Creates a cache object to store unique values.
+ *
+ * @private
+ * @param {Array} [values] The values to cache.
+ */
+ function SetCache(values) {
+ var length = values ? values.length : 0;
+
+ this.data = { 'hash': nativeCreate(null), 'set': new Set };
+ while (length--) {
+ this.push(values[length]);
+ }
+ }
+
+ // Add functions to the `Set` cache.
+ SetCache.prototype.push = cachePush;
+
+ module.exports = SetCache;
+
+ /* WEBPACK VAR INJECTION */}.call(exports, (function() { return this; }())))
+
+/***/ },
+/* 17 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /**
+ * Appends the elements of `values` to `array`.
+ *
+ * @private
+ * @param {Array} array The array to modify.
+ * @param {Array} values The values to append.
+ * @returns {Array} Returns `array`.
+ */
+ function arrayPush(array, values) {
+ var index = -1,
+ length = values.length,
+ offset = array.length;
+
+ while (++index < length) {
+ array[offset + index] = values[index];
+ }
+ return array;
+ }
+
+ module.exports = arrayPush;
+
+
+/***/ },
+/* 18 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var keys = __webpack_require__(10);
+
+ /**
+ * A specialized version of `_.assign` for customizing assigned values without
+ * support for argument juggling, multiple sources, and `this` binding `customizer`
+ * functions.
+ *
+ * @private
+ * @param {Object} object The destination object.
+ * @param {Object} source The source object.
+ * @param {Function} customizer The function to customize assigned values.
+ * @returns {Object} Returns `object`.
+ */
+ function assignWith(object, source, customizer) {
+ var index = -1,
+ props = keys(source),
+ length = props.length;
+
+ while (++index < length) {
+ var key = props[index],
+ value = object[key],
+ result = customizer(value, source[key], key, object, source);
+
+ if ((result === result ? (result !== value) : (value === value)) ||
+ (value === undefined && !(key in object))) {
+ object[key] = result;
+ }
+ }
+ return object;
+ }
+
+ module.exports = assignWith;
+
+
+/***/ },
+/* 19 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var baseCopy = __webpack_require__(20),
+ keys = __webpack_require__(10);
+
+ /**
+ * The base implementation of `_.assign` without support for argument juggling,
+ * multiple sources, and `customizer` functions.
+ *
+ * @private
+ * @param {Object} object The destination object.
+ * @param {Object} source The source object.
+ * @returns {Object} Returns `object`.
+ */
+ function baseAssign(object, source) {
+ return source == null
+ ? object
+ : baseCopy(source, keys(source), object);
+ }
+
+ module.exports = baseAssign;
+
+
+/***/ },
+/* 20 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /**
+ * Copies properties of `source` to `object`.
+ *
+ * @private
+ * @param {Object} source The object to copy properties from.
+ * @param {Array} props The property names to copy.
+ * @param {Object} [object={}] The object to copy properties to.
+ * @returns {Object} Returns `object`.
+ */
+ function baseCopy(source, props, object) {
+ object || (object = {});
+
+ var index = -1,
+ length = props.length;
+
+ while (++index < length) {
+ var key = props[index];
+ object[key] = source[key];
+ }
+ return object;
+ }
+
+ module.exports = baseCopy;
+
+
+/***/ },
+/* 21 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var baseIndexOf = __webpack_require__(23),
+ cacheIndexOf = __webpack_require__(26),
+ createCache = __webpack_require__(29);
+
+ /** Used as the size to enable large array optimizations. */
+ var LARGE_ARRAY_SIZE = 200;
+
+ /**
+ * The base implementation of `_.difference` which accepts a single array
+ * of values to exclude.
+ *
+ * @private
+ * @param {Array} array The array to inspect.
+ * @param {Array} values The values to exclude.
+ * @returns {Array} Returns the new array of filtered values.
+ */
+ function baseDifference(array, values) {
+ var length = array ? array.length : 0,
+ result = [];
+
+ if (!length) {
+ return result;
+ }
+ var index = -1,
+ indexOf = baseIndexOf,
+ isCommon = true,
+ cache = (isCommon && values.length >= LARGE_ARRAY_SIZE) ? createCache(values) : null,
+ valuesLength = values.length;
+
+ if (cache) {
+ indexOf = cacheIndexOf;
+ isCommon = false;
+ values = cache;
+ }
+ outer:
+ while (++index < length) {
+ var value = array[index];
+
+ if (isCommon && value === value) {
+ var valuesIndex = valuesLength;
+ while (valuesIndex--) {
+ if (values[valuesIndex] === value) {
+ continue outer;
+ }
+ }
+ result.push(value);
+ }
+ else if (indexOf(values, value, 0) < 0) {
+ result.push(value);
+ }
+ }
+ return result;
+ }
+
+ module.exports = baseDifference;
+
+
+/***/ },
+/* 22 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var arrayPush = __webpack_require__(17),
+ isArguments = __webpack_require__(7),
+ isArray = __webpack_require__(8),
+ isArrayLike = __webpack_require__(2),
+ isObjectLike = __webpack_require__(3);
+
+ /**
+ * The base implementation of `_.flatten` with added support for restricting
+ * flattening and specifying the start index.
+ *
+ * @private
+ * @param {Array} array The array to flatten.
+ * @param {boolean} [isDeep] Specify a deep flatten.
+ * @param {boolean} [isStrict] Restrict flattening to arrays-like objects.
+ * @param {Array} [result=[]] The initial result value.
+ * @returns {Array} Returns the new flattened array.
+ */
+ function baseFlatten(array, isDeep, isStrict, result) {
+ result || (result = []);
+
+ var index = -1,
+ length = array.length;
+
+ while (++index < length) {
+ var value = array[index];
+ if (isObjectLike(value) && isArrayLike(value) &&
+ (isStrict || isArray(value) || isArguments(value))) {
+ if (isDeep) {
+ // Recursively flatten arrays (susceptible to call stack limits).
+ baseFlatten(value, isDeep, isStrict, result);
+ } else {
+ arrayPush(result, value);
+ }
+ } else if (!isStrict) {
+ result[result.length] = value;
+ }
+ }
+ return result;
+ }
+
+ module.exports = baseFlatten;
+
+
+/***/ },
+/* 23 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var indexOfNaN = __webpack_require__(31);
+
+ /**
+ * The base implementation of `_.indexOf` without support for binary searches.
+ *
+ * @private
+ * @param {Array} array The array to search.
+ * @param {*} value The value to search for.
+ * @param {number} fromIndex The index to search from.
+ * @returns {number} Returns the index of the matched value, else `-1`.
+ */
+ function baseIndexOf(array, value, fromIndex) {
+ if (value !== value) {
+ return indexOfNaN(array, fromIndex);
+ }
+ var index = fromIndex - 1,
+ length = array.length;
+
+ while (++index < length) {
+ if (array[index] === value) {
+ return index;
+ }
+ }
+ return -1;
+ }
+
+ module.exports = baseIndexOf;
+
+
+/***/ },
+/* 24 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /**
+ * The base implementation of `_.property` without support for deep paths.
+ *
+ * @private
+ * @param {string} key The key of the property to get.
+ * @returns {Function} Returns the new function.
+ */
+ function baseProperty(key) {
+ return function(object) {
+ return object == null ? undefined : object[key];
+ };
+ }
+
+ module.exports = baseProperty;
+
+
+/***/ },
+/* 25 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var identity = __webpack_require__(38);
+
+ /**
+ * A specialized version of `baseCallback` which only supports `this` binding
+ * and specifying the number of arguments to provide to `func`.
+ *
+ * @private
+ * @param {Function} func The function to bind.
+ * @param {*} thisArg The `this` binding of `func`.
+ * @param {number} [argCount] The number of arguments to provide to `func`.
+ * @returns {Function} Returns the callback.
+ */
+ function bindCallback(func, thisArg, argCount) {
+ if (typeof func != 'function') {
+ return identity;
+ }
+ if (thisArg === undefined) {
+ return func;
+ }
+ switch (argCount) {
+ case 1: return function(value) {
+ return func.call(thisArg, value);
+ };
+ case 3: return function(value, index, collection) {
+ return func.call(thisArg, value, index, collection);
+ };
+ case 4: return function(accumulator, value, index, collection) {
+ return func.call(thisArg, accumulator, value, index, collection);
+ };
+ case 5: return function(value, other, key, object, source) {
+ return func.call(thisArg, value, other, key, object, source);
+ };
+ }
+ return function() {
+ return func.apply(thisArg, arguments);
+ };
+ }
+
+ module.exports = bindCallback;
+
+
+/***/ },
+/* 26 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var isObject = __webpack_require__(1);
+
+ /**
+ * Checks if `value` is in `cache` mimicking the return signature of
+ * `_.indexOf` by returning `0` if the value is found, else `-1`.
+ *
+ * @private
+ * @param {Object} cache The cache to search.
+ * @param {*} value The value to search for.
+ * @returns {number} Returns `0` if `value` is found, else `-1`.
+ */
+ function cacheIndexOf(cache, value) {
+ var data = cache.data,
+ result = (typeof value == 'string' || isObject(value)) ? data.set.has(value) : data.hash[value];
+
+ return result ? 0 : -1;
+ }
+
+ module.exports = cacheIndexOf;
+
+
+/***/ },
+/* 27 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var isObject = __webpack_require__(1);
+
+ /**
+ * Adds `value` to the cache.
+ *
+ * @private
+ * @name push
+ * @memberOf SetCache
+ * @param {*} value The value to cache.
+ */
+ function cachePush(value) {
+ var data = this.data;
+ if (typeof value == 'string' || isObject(value)) {
+ data.set.add(value);
+ } else {
+ data.hash[value] = true;
+ }
+ }
+
+ module.exports = cachePush;
+
+
+/***/ },
+/* 28 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var bindCallback = __webpack_require__(25),
+ isIterateeCall = __webpack_require__(32),
+ restParam = __webpack_require__(9);
+
+ /**
+ * Creates a `_.assign`, `_.defaults`, or `_.merge` function.
+ *
+ * @private
+ * @param {Function} assigner The function to assign values.
+ * @returns {Function} Returns the new assigner function.
+ */
+ function createAssigner(assigner) {
+ return restParam(function(object, sources) {
+ var index = -1,
+ length = object == null ? 0 : sources.length,
+ customizer = length > 2 ? sources[length - 2] : undefined,
+ guard = length > 2 ? sources[2] : undefined,
+ thisArg = length > 1 ? sources[length - 1] : undefined;
+
+ if (typeof customizer == 'function') {
+ customizer = bindCallback(customizer, thisArg, 5);
+ length -= 2;
+ } else {
+ customizer = typeof thisArg == 'function' ? thisArg : undefined;
+ length -= (customizer ? 1 : 0);
+ }
+ if (guard && isIterateeCall(sources[0], sources[1], guard)) {
+ customizer = length < 3 ? undefined : customizer;
+ length = 1;
+ }
+ while (++index < length) {
+ var source = sources[index];
+ if (source) {
+ assigner(object, source, customizer);
+ }
+ }
+ return object;
+ });
+ }
+
+ module.exports = createAssigner;
+
+
+/***/ },
+/* 29 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /* WEBPACK VAR INJECTION */(function(global) {var SetCache = __webpack_require__(16),
+ getNative = __webpack_require__(4);
+
+ /** Native method references. */
+ var Set = getNative(global, 'Set');
+
+ /* Native method references for those with the same name as other `lodash` methods. */
+ var nativeCreate = getNative(Object, 'create');
+
+ /**
+ * Creates a `Set` cache object to optimize linear searches of large arrays.
+ *
+ * @private
+ * @param {Array} [values] The values to cache.
+ * @returns {null|Object} Returns the new cache object if `Set` is supported, else `null`.
+ */
+ function createCache(values) {
+ return (nativeCreate && Set) ? new SetCache(values) : null;
+ }
+
+ module.exports = createCache;
+
+ /* WEBPACK VAR INJECTION */}.call(exports, (function() { return this; }())))
+
+/***/ },
+/* 30 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var baseProperty = __webpack_require__(24);
+
+ /**
+ * Gets the "length" property value of `object`.
+ *
+ * **Note:** This function is used to avoid a [JIT bug](https://bugs.webkit.org/show_bug.cgi?id=142792)
+ * that affects Safari on at least iOS 8.1-8.3 ARM64.
+ *
+ * @private
+ * @param {Object} object The object to query.
+ * @returns {*} Returns the "length" value.
+ */
+ var getLength = baseProperty('length');
+
+ module.exports = getLength;
+
+
+/***/ },
+/* 31 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /**
+ * Gets the index at which the first occurrence of `NaN` is found in `array`.
+ *
+ * @private
+ * @param {Array} array The array to search.
+ * @param {number} fromIndex The index to search from.
+ * @param {boolean} [fromRight] Specify iterating from right to left.
+ * @returns {number} Returns the index of the matched `NaN`, else `-1`.
+ */
+ function indexOfNaN(array, fromIndex, fromRight) {
+ var length = array.length,
+ index = fromIndex + (fromRight ? 0 : -1);
+
+ while ((fromRight ? index-- : ++index < length)) {
+ var other = array[index];
+ if (other !== other) {
+ return index;
+ }
+ }
+ return -1;
+ }
+
+ module.exports = indexOfNaN;
+
+
+/***/ },
+/* 32 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var isArrayLike = __webpack_require__(2),
+ isIndex = __webpack_require__(6),
+ isObject = __webpack_require__(1);
+
+ /**
+ * Checks if the provided arguments are from an iteratee call.
+ *
+ * @private
+ * @param {*} value The potential iteratee value argument.
+ * @param {*} index The potential iteratee index or key argument.
+ * @param {*} object The potential iteratee object argument.
+ * @returns {boolean} Returns `true` if the arguments are from an iteratee call, else `false`.
+ */
+ function isIterateeCall(value, index, object) {
+ if (!isObject(object)) {
+ return false;
+ }
+ var type = typeof index;
+ if (type == 'number'
+ ? (isArrayLike(object) && isIndex(index, object.length))
+ : (type == 'string' && index in object)) {
+ var other = object[index];
+ return value === value ? (value === other) : (other !== other);
+ }
+ return false;
+ }
+
+ module.exports = isIterateeCall;
+
+
+/***/ },
+/* 33 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var isArguments = __webpack_require__(7),
+ isArray = __webpack_require__(8),
+ isIndex = __webpack_require__(6),
+ isLength = __webpack_require__(5),
+ keysIn = __webpack_require__(37);
+
+ /** Used for native method references. */
+ var objectProto = Object.prototype;
+
+ /** Used to check objects for own properties. */
+ var hasOwnProperty = objectProto.hasOwnProperty;
+
+ /**
+ * A fallback implementation of `Object.keys` which creates an array of the
+ * own enumerable property names of `object`.
+ *
+ * @private
+ * @param {Object} object The object to query.
+ * @returns {Array} Returns the array of property names.
+ */
+ function shimKeys(object) {
+ var props = keysIn(object),
+ propsLength = props.length,
+ length = propsLength && object.length;
+
+ var allowIndexes = !!length && isLength(length) &&
+ (isArray(object) || isArguments(object));
+
+ var index = -1,
+ result = [];
+
+ while (++index < propsLength) {
+ var key = props[index];
+ if ((allowIndexes && isIndex(key, length)) || hasOwnProperty.call(object, key)) {
+ result.push(key);
+ }
+ }
+ return result;
+ }
+
+ module.exports = shimKeys;
+
+
+/***/ },
+/* 34 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var isObject = __webpack_require__(1);
+
+ /** `Object#toString` result references. */
+ var funcTag = '[object Function]';
+
+ /** Used for native method references. */
+ var objectProto = Object.prototype;
+
+ /**
+ * Used to resolve the [`toStringTag`](http://ecma-international.org/ecma-262/6.0/#sec-object.prototype.tostring)
+ * of values.
+ */
+ var objToString = objectProto.toString;
+
+ /**
+ * Checks if `value` is classified as a `Function` object.
+ *
+ * @static
+ * @memberOf _
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is correctly classified, else `false`.
+ * @example
+ *
+ * _.isFunction(_);
+ * // => true
+ *
+ * _.isFunction(/abc/);
+ * // => false
+ */
+ function isFunction(value) {
+ // The use of `Object#toString` avoids issues with the `typeof` operator
+ // in older versions of Chrome and Safari which return 'function' for regexes
+ // and Safari 8 which returns 'object' for typed array constructors.
+ return isObject(value) && objToString.call(value) == funcTag;
+ }
+
+ module.exports = isFunction;
+
+
+/***/ },
+/* 35 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var isFunction = __webpack_require__(34),
+ isObjectLike = __webpack_require__(3);
+
+ /** Used to detect host constructors (Safari > 5). */
+ var reIsHostCtor = /^\[object .+?Constructor\]$/;
+
+ /** Used for native method references. */
+ var objectProto = Object.prototype;
+
+ /** Used to resolve the decompiled source of functions. */
+ var fnToString = Function.prototype.toString;
+
+ /** Used to check objects for own properties. */
+ var hasOwnProperty = objectProto.hasOwnProperty;
+
+ /** Used to detect if a method is native. */
+ var reIsNative = RegExp('^' +
+ fnToString.call(hasOwnProperty).replace(/[\\^$.*+?()[\]{}|]/g, '\\$&')
+ .replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g, '$1.*?') + '$'
+ );
+
+ /**
+ * Checks if `value` is a native function.
+ *
+ * @static
+ * @memberOf _
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is a native function, else `false`.
+ * @example
+ *
+ * _.isNative(Array.prototype.push);
+ * // => true
+ *
+ * _.isNative(_);
+ * // => false
+ */
+ function isNative(value) {
+ if (value == null) {
+ return false;
+ }
+ if (isFunction(value)) {
+ return reIsNative.test(fnToString.call(value));
+ }
+ return isObjectLike(value) && reIsHostCtor.test(value);
+ }
+
+ module.exports = isNative;
+
+
+/***/ },
+/* 36 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var assignWith = __webpack_require__(18),
+ baseAssign = __webpack_require__(19),
+ createAssigner = __webpack_require__(28);
+
+ /**
+ * Assigns own enumerable properties of source object(s) to the destination
+ * object. Subsequent sources overwrite property assignments of previous sources.
+ * If `customizer` is provided it's invoked to produce the assigned values.
+ * The `customizer` is bound to `thisArg` and invoked with five arguments:
+ * (objectValue, sourceValue, key, object, source).
+ *
+ * **Note:** This method mutates `object` and is based on
+ * [`Object.assign`](http://ecma-international.org/ecma-262/6.0/#sec-object.assign).
+ *
+ * @static
+ * @memberOf _
+ * @alias extend
+ * @category Object
+ * @param {Object} object The destination object.
+ * @param {...Object} [sources] The source objects.
+ * @param {Function} [customizer] The function to customize assigned values.
+ * @param {*} [thisArg] The `this` binding of `customizer`.
+ * @returns {Object} Returns `object`.
+ * @example
+ *
+ * _.assign({ 'user': 'barney' }, { 'age': 40 }, { 'user': 'fred' });
+ * // => { 'user': 'fred', 'age': 40 }
+ *
+ * // using a customizer callback
+ * var defaults = _.partialRight(_.assign, function(value, other) {
+ * return _.isUndefined(value) ? other : value;
+ * });
+ *
+ * defaults({ 'user': 'barney' }, { 'age': 36 }, { 'user': 'fred' });
+ * // => { 'user': 'barney', 'age': 36 }
+ */
+ var assign = createAssigner(function(object, source, customizer) {
+ return customizer
+ ? assignWith(object, source, customizer)
+ : baseAssign(object, source);
+ });
+
+ module.exports = assign;
+
+
+/***/ },
+/* 37 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var isArguments = __webpack_require__(7),
+ isArray = __webpack_require__(8),
+ isIndex = __webpack_require__(6),
+ isLength = __webpack_require__(5),
+ isObject = __webpack_require__(1);
+
+ /** Used for native method references. */
+ var objectProto = Object.prototype;
+
+ /** Used to check objects for own properties. */
+ var hasOwnProperty = objectProto.hasOwnProperty;
+
+ /**
+ * Creates an array of the own and inherited enumerable property names of `object`.
+ *
+ * **Note:** Non-object values are coerced to objects.
+ *
+ * @static
+ * @memberOf _
+ * @category Object
+ * @param {Object} object The object to query.
+ * @returns {Array} Returns the array of property names.
+ * @example
+ *
+ * function Foo() {
+ * this.a = 1;
+ * this.b = 2;
+ * }
+ *
+ * Foo.prototype.c = 3;
+ *
+ * _.keysIn(new Foo);
+ * // => ['a', 'b', 'c'] (iteration order is not guaranteed)
+ */
+ function keysIn(object) {
+ if (object == null) {
+ return [];
+ }
+ if (!isObject(object)) {
+ object = Object(object);
+ }
+ var length = object.length;
+ length = (length && isLength(length) &&
+ (isArray(object) || isArguments(object)) && length) || 0;
+
+ var Ctor = object.constructor,
+ index = -1,
+ isProto = typeof Ctor == 'function' && Ctor.prototype === object,
+ result = Array(length),
+ skipIndexes = length > 0;
+
+ while (++index < length) {
+ result[index] = (index + '');
+ }
+ for (var key in object) {
+ if (!(skipIndexes && isIndex(key, length)) &&
+ !(key == 'constructor' && (isProto || !hasOwnProperty.call(object, key)))) {
+ result.push(key);
+ }
+ }
+ return result;
+ }
+
+ module.exports = keysIn;
+
+
+/***/ },
+/* 38 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /**
+ * This method returns the first argument provided to it.
+ *
+ * @static
+ * @memberOf _
+ * @category Utility
+ * @param {*} value Any value.
+ * @returns {*} Returns `value`.
+ * @example
+ *
+ * var object = { 'user': 'fred' };
+ *
+ * _.identity(object) === object;
+ * // => true
+ */
+ function identity(value) {
+ return value;
+ }
+
+ module.exports = identity;
+
+
+/***/ },
+/* 39 */
+/***/ function(module, exports, __webpack_require__) {
+
+ "use strict";
+
+ exports.__esModule = true;
+ exports["default"] = getForceUpdate;
+ function traverseRenderedChildren(internalInstance, callback, argument) {
+ callback(internalInstance, argument);
+
+ if (internalInstance._renderedComponent) {
+ traverseRenderedChildren(internalInstance._renderedComponent, callback, argument);
+ } else {
+ for (var key in internalInstance._renderedChildren) {
+ if (internalInstance._renderedChildren.hasOwnProperty(key)) {
+ traverseRenderedChildren(internalInstance._renderedChildren[key], callback, argument);
+ }
+ }
+ }
+ }
+
+ function setPendingForceUpdate(internalInstance) {
+ if (internalInstance._pendingForceUpdate === false) {
+ internalInstance._pendingForceUpdate = true;
+ }
+ }
+
+ function forceUpdateIfPending(internalInstance, React) {
+ if (internalInstance._pendingForceUpdate === true) {
+ var publicInstance = internalInstance._instance;
+ React.Component.prototype.forceUpdate.call(publicInstance);
+ }
+ }
+
+ function getForceUpdate(React) {
+ return function (instance) {
+ var internalInstance = instance._reactInternalInstance;
+ traverseRenderedChildren(internalInstance, setPendingForceUpdate);
+ traverseRenderedChildren(internalInstance, forceUpdateIfPending, React);
+ };
+ }
+
+ module.exports = exports["default"];
+
+/***/ }
+/******/ ])
+});
diff --git a/devtools/client/shared/vendor/react-redux.js b/devtools/client/shared/vendor/react-redux.js
new file mode 100644
index 000000000..562f4916f
--- /dev/null
+++ b/devtools/client/shared/vendor/react-redux.js
@@ -0,0 +1,724 @@
+var REACT_PATH = "devtools/client/shared/vendor/react";
+var REDUX_PATH = "devtools/client/shared/vendor/redux";
+
+(function webpackUniversalModuleDefinition(root, factory) {
+ if(typeof exports === 'object' && typeof module === 'object')
+ module.exports = factory(require(REACT_PATH), require(REDUX_PATH));
+ else if(typeof define === 'function' && define.amd)
+ define(["react", "redux"], factory);
+ else if(typeof exports === 'object')
+ exports["ReactRedux"] = factory(require("react"), require("redux"));
+ else
+ root["ReactRedux"] = factory(root["React"], root["Redux"]);
+})(this, function(__WEBPACK_EXTERNAL_MODULE_10__, __WEBPACK_EXTERNAL_MODULE_11__) {
+return /******/ (function(modules) { // webpackBootstrap
+/******/ // The module cache
+/******/ var installedModules = {};
+
+/******/ // The require function
+/******/ function __webpack_require__(moduleId) {
+
+/******/ // Check if module is in cache
+/******/ if(installedModules[moduleId])
+/******/ return installedModules[moduleId].exports;
+
+/******/ // Create a new module (and put it into the cache)
+/******/ var module = installedModules[moduleId] = {
+/******/ exports: {},
+/******/ id: moduleId,
+/******/ loaded: false
+/******/ };
+
+/******/ // Execute the module function
+/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
+
+/******/ // Flag the module as loaded
+/******/ module.loaded = true;
+
+/******/ // Return the exports of the module
+/******/ return module.exports;
+/******/ }
+
+
+/******/ // expose the modules object (__webpack_modules__)
+/******/ __webpack_require__.m = modules;
+
+/******/ // expose the module cache
+/******/ __webpack_require__.c = installedModules;
+
+/******/ // __webpack_public_path__
+/******/ __webpack_require__.p = "";
+
+/******/ // Load entry module and return exports
+/******/ return __webpack_require__(0);
+/******/ })
+/************************************************************************/
+/******/ ([
+/* 0 */
+/***/ function(module, exports, __webpack_require__) {
+
+ 'use strict';
+
+ exports.__esModule = true;
+
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
+
+ var _react = __webpack_require__(10);
+
+ var _react2 = _interopRequireDefault(_react);
+
+ var _componentsCreateAll = __webpack_require__(2);
+
+ var _componentsCreateAll2 = _interopRequireDefault(_componentsCreateAll);
+
+ var _createAll = _componentsCreateAll2['default'](_react2['default']);
+
+ var Provider = _createAll.Provider;
+ var connect = _createAll.connect;
+ exports.Provider = Provider;
+ exports.connect = connect;
+
+/***/ },
+/* 1 */
+/***/ function(module, exports) {
+
+ "use strict";
+
+ exports.__esModule = true;
+ exports["default"] = createStoreShape;
+
+ function createStoreShape(PropTypes) {
+ return PropTypes.shape({
+ subscribe: PropTypes.func.isRequired,
+ dispatch: PropTypes.func.isRequired,
+ getState: PropTypes.func.isRequired
+ });
+ }
+
+ module.exports = exports["default"];
+
+/***/ },
+/* 2 */
+/***/ function(module, exports, __webpack_require__) {
+
+ 'use strict';
+
+ exports.__esModule = true;
+ exports['default'] = createAll;
+
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
+
+ var _createProvider = __webpack_require__(4);
+
+ var _createProvider2 = _interopRequireDefault(_createProvider);
+
+ var _createConnect = __webpack_require__(3);
+
+ var _createConnect2 = _interopRequireDefault(_createConnect);
+
+ function createAll(React) {
+ var Provider = _createProvider2['default'](React);
+ var connect = _createConnect2['default'](React);
+
+ return { Provider: Provider, connect: connect };
+ }
+
+ module.exports = exports['default'];
+
+/***/ },
+/* 3 */
+/***/ function(module, exports, __webpack_require__) {
+
+ 'use strict';
+
+ exports.__esModule = true;
+
+ var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
+
+ exports['default'] = createConnect;
+
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
+
+ function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }
+
+ function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
+
+ var _utilsCreateStoreShape = __webpack_require__(1);
+
+ var _utilsCreateStoreShape2 = _interopRequireDefault(_utilsCreateStoreShape);
+
+ var _utilsShallowEqual = __webpack_require__(6);
+
+ var _utilsShallowEqual2 = _interopRequireDefault(_utilsShallowEqual);
+
+ var _utilsIsPlainObject = __webpack_require__(5);
+
+ var _utilsIsPlainObject2 = _interopRequireDefault(_utilsIsPlainObject);
+
+ var _utilsWrapActionCreators = __webpack_require__(7);
+
+ var _utilsWrapActionCreators2 = _interopRequireDefault(_utilsWrapActionCreators);
+
+ var _hoistNonReactStatics = __webpack_require__(8);
+
+ var _hoistNonReactStatics2 = _interopRequireDefault(_hoistNonReactStatics);
+
+ var _invariant = __webpack_require__(9);
+
+ var _invariant2 = _interopRequireDefault(_invariant);
+
+ var defaultMapStateToProps = function defaultMapStateToProps() {
+ return {};
+ };
+ var defaultMapDispatchToProps = function defaultMapDispatchToProps(dispatch) {
+ return { dispatch: dispatch };
+ };
+ var defaultMergeProps = function defaultMergeProps(stateProps, dispatchProps, parentProps) {
+ return _extends({}, parentProps, stateProps, dispatchProps);
+ };
+
+ function getDisplayName(Component) {
+ return Component.displayName || Component.name || 'Component';
+ }
+
+ // Helps track hot reloading.
+ var nextVersion = 0;
+
+ function createConnect(React) {
+ var Component = React.Component;
+ var PropTypes = React.PropTypes;
+
+ var storeShape = _utilsCreateStoreShape2['default'](PropTypes);
+
+ return function connect(mapStateToProps, mapDispatchToProps, mergeProps) {
+ var options = arguments.length <= 3 || arguments[3] === undefined ? {} : arguments[3];
+
+ var shouldSubscribe = Boolean(mapStateToProps);
+ var finalMapStateToProps = mapStateToProps || defaultMapStateToProps;
+ var finalMapDispatchToProps = _utilsIsPlainObject2['default'](mapDispatchToProps) ? _utilsWrapActionCreators2['default'](mapDispatchToProps) : mapDispatchToProps || defaultMapDispatchToProps;
+ var finalMergeProps = mergeProps || defaultMergeProps;
+ var shouldUpdateStateProps = finalMapStateToProps.length > 1;
+ var shouldUpdateDispatchProps = finalMapDispatchToProps.length > 1;
+ var _options$pure = options.pure;
+ var pure = _options$pure === undefined ? true : _options$pure;
+
+ // Helps track hot reloading.
+ var version = nextVersion++;
+
+ function computeStateProps(store, props) {
+ var state = store.getState();
+ var stateProps = shouldUpdateStateProps ? finalMapStateToProps(state, props) : finalMapStateToProps(state);
+
+ _invariant2['default'](_utilsIsPlainObject2['default'](stateProps), '`mapStateToProps` must return an object. Instead received %s.', stateProps);
+ return stateProps;
+ }
+
+ function computeDispatchProps(store, props) {
+ var dispatch = store.dispatch;
+
+ var dispatchProps = shouldUpdateDispatchProps ? finalMapDispatchToProps(dispatch, props) : finalMapDispatchToProps(dispatch);
+
+ _invariant2['default'](_utilsIsPlainObject2['default'](dispatchProps), '`mapDispatchToProps` must return an object. Instead received %s.', dispatchProps);
+ return dispatchProps;
+ }
+
+ function _computeNextState(stateProps, dispatchProps, parentProps) {
+ var mergedProps = finalMergeProps(stateProps, dispatchProps, parentProps);
+ _invariant2['default'](_utilsIsPlainObject2['default'](mergedProps), '`mergeProps` must return an object. Instead received %s.', mergedProps);
+ return mergedProps;
+ }
+
+ return function wrapWithConnect(WrappedComponent) {
+ var Connect = (function (_Component) {
+ _inherits(Connect, _Component);
+
+ Connect.prototype.shouldComponentUpdate = function shouldComponentUpdate(nextProps, nextState) {
+ if (!pure) {
+ this.updateStateProps(nextProps);
+ this.updateDispatchProps(nextProps);
+ this.updateState(nextProps);
+ return true;
+ }
+
+ var storeChanged = nextState.storeState !== this.state.storeState;
+ var propsChanged = !_utilsShallowEqual2['default'](nextProps, this.props);
+ var mapStateProducedChange = false;
+ var dispatchPropsChanged = false;
+
+ if (storeChanged || propsChanged && shouldUpdateStateProps) {
+ mapStateProducedChange = this.updateStateProps(nextProps);
+ }
+
+ if (propsChanged && shouldUpdateDispatchProps) {
+ dispatchPropsChanged = this.updateDispatchProps(nextProps);
+ }
+
+ if (propsChanged || mapStateProducedChange || dispatchPropsChanged) {
+ this.updateState(nextProps);
+ return true;
+ }
+
+ return false;
+ };
+
+ function Connect(props, context) {
+ _classCallCheck(this, Connect);
+
+ _Component.call(this, props, context);
+ this.version = version;
+ this.store = props.store || context.store;
+
+ _invariant2['default'](this.store, 'Could not find "store" in either the context or ' + ('props of "' + this.constructor.displayName + '". ') + 'Either wrap the root component in a <Provider>, ' + ('or explicitly pass "store" as a prop to "' + this.constructor.displayName + '".'));
+
+ this.stateProps = computeStateProps(this.store, props);
+ this.dispatchProps = computeDispatchProps(this.store, props);
+ this.state = { storeState: null };
+ this.updateState();
+ }
+
+ Connect.prototype.computeNextState = function computeNextState() {
+ var props = arguments.length <= 0 || arguments[0] === undefined ? this.props : arguments[0];
+
+ return _computeNextState(this.stateProps, this.dispatchProps, props);
+ };
+
+ Connect.prototype.updateStateProps = function updateStateProps() {
+ var props = arguments.length <= 0 || arguments[0] === undefined ? this.props : arguments[0];
+
+ var nextStateProps = computeStateProps(this.store, props);
+ if (_utilsShallowEqual2['default'](nextStateProps, this.stateProps)) {
+ return false;
+ }
+
+ this.stateProps = nextStateProps;
+ return true;
+ };
+
+ Connect.prototype.updateDispatchProps = function updateDispatchProps() {
+ var props = arguments.length <= 0 || arguments[0] === undefined ? this.props : arguments[0];
+
+ var nextDispatchProps = computeDispatchProps(this.store, props);
+ if (_utilsShallowEqual2['default'](nextDispatchProps, this.dispatchProps)) {
+ return false;
+ }
+
+ this.dispatchProps = nextDispatchProps;
+ return true;
+ };
+
+ Connect.prototype.updateState = function updateState() {
+ var props = arguments.length <= 0 || arguments[0] === undefined ? this.props : arguments[0];
+
+ this.nextState = this.computeNextState(props);
+ };
+
+ Connect.prototype.isSubscribed = function isSubscribed() {
+ return typeof this.unsubscribe === 'function';
+ };
+
+ Connect.prototype.trySubscribe = function trySubscribe() {
+ if (shouldSubscribe && !this.unsubscribe) {
+ this.unsubscribe = this.store.subscribe(this.handleChange.bind(this));
+ this.handleChange();
+ }
+ };
+
+ Connect.prototype.tryUnsubscribe = function tryUnsubscribe() {
+ if (this.unsubscribe) {
+ this.unsubscribe();
+ this.unsubscribe = null;
+ }
+ };
+
+ Connect.prototype.componentDidMount = function componentDidMount() {
+ this.trySubscribe();
+ };
+
+ Connect.prototype.componentWillUnmount = function componentWillUnmount() {
+ this.tryUnsubscribe();
+ };
+
+ Connect.prototype.handleChange = function handleChange() {
+ if (!this.unsubscribe) {
+ return;
+ }
+
+ this.setState({
+ storeState: this.store.getState()
+ });
+ };
+
+ Connect.prototype.getWrappedInstance = function getWrappedInstance() {
+ return this.refs.wrappedInstance;
+ };
+
+ Connect.prototype.render = function render() {
+ return React.createElement(WrappedComponent, _extends({ ref: 'wrappedInstance'
+ }, this.nextState));
+ };
+
+ return Connect;
+ })(Component);
+
+ Connect.displayName = 'Connect(' + getDisplayName(WrappedComponent) + ')';
+ Connect.WrappedComponent = WrappedComponent;
+ Connect.contextTypes = {
+ store: storeShape
+ };
+ Connect.propTypes = {
+ store: storeShape
+ };
+
+ if (true) {
+ Connect.prototype.componentWillUpdate = function componentWillUpdate() {
+ if (this.version === version) {
+ return;
+ }
+
+ // We are hot reloading!
+ this.version = version;
+
+ // Update the state and bindings.
+ this.trySubscribe();
+ this.updateStateProps();
+ this.updateDispatchProps();
+ this.updateState();
+ };
+ }
+
+ return _hoistNonReactStatics2['default'](Connect, WrappedComponent);
+ };
+ };
+ }
+
+ module.exports = exports['default'];
+
+/***/ },
+/* 4 */
+/***/ function(module, exports, __webpack_require__) {
+
+ 'use strict';
+
+ exports.__esModule = true;
+ exports['default'] = createProvider;
+
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
+
+ function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }
+
+ function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
+
+ var _utilsCreateStoreShape = __webpack_require__(1);
+
+ var _utilsCreateStoreShape2 = _interopRequireDefault(_utilsCreateStoreShape);
+
+ function isUsingOwnerContext(React) {
+ var version = React.version;
+
+ if (typeof version !== 'string') {
+ return true;
+ }
+
+ var sections = version.split('.');
+ var major = parseInt(sections[0], 10);
+ var minor = parseInt(sections[1], 10);
+
+ return major === 0 && minor === 13;
+ }
+
+ function createProvider(React) {
+ var Component = React.Component;
+ var PropTypes = React.PropTypes;
+ var Children = React.Children;
+
+ var storeShape = _utilsCreateStoreShape2['default'](PropTypes);
+ var requireFunctionChild = isUsingOwnerContext(React);
+
+ var didWarnAboutChild = false;
+ function warnAboutFunctionChild() {
+ if (didWarnAboutChild || requireFunctionChild) {
+ return;
+ }
+
+ didWarnAboutChild = true;
+ console.error( // eslint-disable-line no-console
+ 'With React 0.14 and later versions, you no longer need to ' + 'wrap <Provider> child into a function.');
+ }
+ function warnAboutElementChild() {
+ if (didWarnAboutChild || !requireFunctionChild) {
+ return;
+ }
+
+ didWarnAboutChild = true;
+ console.error( // eslint-disable-line no-console
+ 'With React 0.13, you need to ' + 'wrap <Provider> child into a function. ' + 'This restriction will be removed with React 0.14.');
+ }
+
+ var didWarnAboutReceivingStore = false;
+ function warnAboutReceivingStore() {
+ if (didWarnAboutReceivingStore) {
+ return;
+ }
+
+ didWarnAboutReceivingStore = true;
+ console.error( // eslint-disable-line no-console
+ '<Provider> does not support changing `store` on the fly. ' + 'It is most likely that you see this error because you updated to ' + 'Redux 2.x and React Redux 2.x which no longer hot reload reducers ' + 'automatically. See https://github.com/rackt/react-redux/releases/' + 'tag/v2.0.0 for the migration instructions.');
+ }
+
+ var Provider = (function (_Component) {
+ _inherits(Provider, _Component);
+
+ Provider.prototype.getChildContext = function getChildContext() {
+ return { store: this.store };
+ };
+
+ function Provider(props, context) {
+ _classCallCheck(this, Provider);
+
+ _Component.call(this, props, context);
+ this.store = props.store;
+ }
+
+ Provider.prototype.componentWillReceiveProps = function componentWillReceiveProps(nextProps) {
+ var store = this.store;
+ var nextStore = nextProps.store;
+
+ if (store !== nextStore) {
+ warnAboutReceivingStore();
+ }
+ };
+
+ Provider.prototype.render = function render() {
+ var children = this.props.children;
+
+ if (typeof children === 'function') {
+ warnAboutFunctionChild();
+ children = children();
+ } else {
+ warnAboutElementChild();
+ }
+
+ return Children.only(children);
+ };
+
+ return Provider;
+ })(Component);
+
+ Provider.childContextTypes = {
+ store: storeShape.isRequired
+ };
+ Provider.propTypes = {
+ store: storeShape.isRequired,
+ children: (requireFunctionChild ? PropTypes.func : PropTypes.element).isRequired
+ };
+
+ return Provider;
+ }
+
+ module.exports = exports['default'];
+
+/***/ },
+/* 5 */
+/***/ function(module, exports) {
+
+ 'use strict';
+
+ exports.__esModule = true;
+ exports['default'] = isPlainObject;
+ var fnToString = function fnToString(fn) {
+ return Function.prototype.toString.call(fn);
+ };
+
+ /**
+ * @param {any} obj The object to inspect.
+ * @returns {boolean} True if the argument appears to be a plain object.
+ */
+
+ function isPlainObject(obj) {
+ if (!obj || typeof obj !== 'object') {
+ return false;
+ }
+
+ var proto = typeof obj.constructor === 'function' ? Object.getPrototypeOf(obj) : Object.prototype;
+
+ if (proto === null) {
+ return true;
+ }
+
+ var constructor = proto.constructor;
+
+ return typeof constructor === 'function' && constructor instanceof constructor && fnToString(constructor) === fnToString(Object);
+ }
+
+ module.exports = exports['default'];
+
+/***/ },
+/* 6 */
+/***/ function(module, exports) {
+
+ "use strict";
+
+ exports.__esModule = true;
+ exports["default"] = shallowEqual;
+
+ function shallowEqual(objA, objB) {
+ if (objA === objB) {
+ return true;
+ }
+
+ var keysA = Object.keys(objA);
+ var keysB = Object.keys(objB);
+
+ if (keysA.length !== keysB.length) {
+ return false;
+ }
+
+ // Test for A's keys different from B.
+ var hasOwn = Object.prototype.hasOwnProperty;
+ for (var i = 0; i < keysA.length; i++) {
+ if (!hasOwn.call(objB, keysA[i]) || objA[keysA[i]] !== objB[keysA[i]]) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ module.exports = exports["default"];
+
+/***/ },
+/* 7 */
+/***/ function(module, exports, __webpack_require__) {
+
+ 'use strict';
+
+ exports.__esModule = true;
+ exports['default'] = wrapActionCreators;
+
+ var _redux = __webpack_require__(11);
+
+ function wrapActionCreators(actionCreators) {
+ return function (dispatch) {
+ return _redux.bindActionCreators(actionCreators, dispatch);
+ };
+ }
+
+ module.exports = exports['default'];
+
+/***/ },
+/* 8 */
+/***/ function(module, exports) {
+
+ /**
+ * Copyright 2015, Yahoo! Inc.
+ * Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
+ */
+ 'use strict';
+
+ var REACT_STATICS = {
+ childContextTypes: true,
+ contextTypes: true,
+ defaultProps: true,
+ displayName: true,
+ getDefaultProps: true,
+ mixins: true,
+ propTypes: true,
+ type: true
+ };
+
+ var KNOWN_STATICS = {
+ name: true,
+ length: true,
+ prototype: true,
+ caller: true,
+ arguments: true,
+ arity: true
+ };
+
+ module.exports = function hoistNonReactStatics(targetComponent, sourceComponent) {
+ var keys = Object.getOwnPropertyNames(sourceComponent);
+ for (var i=0; i<keys.length; ++i) {
+ if (!REACT_STATICS[keys[i]] && !KNOWN_STATICS[keys[i]]) {
+ targetComponent[keys[i]] = sourceComponent[keys[i]];
+ }
+ }
+
+ return targetComponent;
+ };
+
+
+/***/ },
+/* 9 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule invariant
+ */
+
+ 'use strict';
+
+ /**
+ * Use invariant() to assert state which your program assumes to be true.
+ *
+ * Provide sprintf-style format (only %s is supported) and arguments
+ * to provide information about what broke and what you were
+ * expecting.
+ *
+ * The invariant message will be stripped in production, but the invariant
+ * will remain to ensure logic does not differ in production.
+ */
+
+ var invariant = function(condition, format, a, b, c, d, e, f) {
+ if (true) {
+ if (format === undefined) {
+ throw new Error('invariant requires an error message argument');
+ }
+ }
+
+ if (!condition) {
+ var error;
+ if (format === undefined) {
+ error = new Error(
+ 'Minified exception occurred; use the non-minified dev environment ' +
+ 'for the full error message and additional helpful warnings.'
+ );
+ } else {
+ var args = [a, b, c, d, e, f];
+ var argIndex = 0;
+ error = new Error(
+ 'Invariant Violation: ' +
+ format.replace(/%s/g, function() { return args[argIndex++]; })
+ );
+ }
+
+ error.framesToPop = 1; // we don't care about invariant's own frame
+ throw error;
+ }
+ };
+
+ module.exports = invariant;
+
+
+/***/ },
+/* 10 */
+/***/ function(module, exports) {
+
+ module.exports = __WEBPACK_EXTERNAL_MODULE_10__;
+
+/***/ },
+/* 11 */
+/***/ function(module, exports) {
+
+ module.exports = __WEBPACK_EXTERNAL_MODULE_11__;
+
+/***/ }
+/******/ ])
+});
+;
diff --git a/devtools/client/shared/vendor/react-virtualized.js b/devtools/client/shared/vendor/react-virtualized.js
new file mode 100644
index 000000000..dab201ac6
--- /dev/null
+++ b/devtools/client/shared/vendor/react-virtualized.js
@@ -0,0 +1,4296 @@
+var REACT_PATH = "devtools/client/shared/vendor/react";
+var REACT_DOM_PATH = "devtools/client/shared/vendor/react-dom";
+var REACT_SHALLOW_COMPARE = "devtools/client/shared/vendor/react-addons-shallow-compare";
+
+!function(root, factory) {
+ let React = require(REACT_PATH);
+ let shallowCompare = require(REACT_SHALLOW_COMPARE);
+ let ReactDOM = require(REACT_DOM_PATH);
+ module.exports = factory(React, shallowCompare, ReactDOM);
+}(this, function(__WEBPACK_EXTERNAL_MODULE_89__, __WEBPACK_EXTERNAL_MODULE_90__, __WEBPACK_EXTERNAL_MODULE_96__) {
+ /******/
+ return function(modules) {
+ /******/
+ /******/
+ // The require function
+ /******/
+ function __webpack_require__(moduleId) {
+ /******/
+ /******/
+ // Check if module is in cache
+ /******/
+ if (installedModules[moduleId]) /******/
+ return installedModules[moduleId].exports;
+ /******/
+ /******/
+ // Create a new module (and put it into the cache)
+ /******/
+ var module = installedModules[moduleId] = {
+ /******/
+ exports: {},
+ /******/
+ id: moduleId,
+ /******/
+ loaded: !1
+ };
+ /******/
+ /******/
+ // Return the exports of the module
+ /******/
+ /******/
+ /******/
+ // Execute the module function
+ /******/
+ /******/
+ /******/
+ // Flag the module as loaded
+ /******/
+ return modules[moduleId].call(module.exports, module, module.exports, __webpack_require__),
+ module.loaded = !0, module.exports;
+ }
+ // webpackBootstrap
+ /******/
+ // The module cache
+ /******/
+ var installedModules = {};
+ /******/
+ /******/
+ // Load entry module and return exports
+ /******/
+ /******/
+ /******/
+ /******/
+ // expose the modules object (__webpack_modules__)
+ /******/
+ /******/
+ /******/
+ // expose the module cache
+ /******/
+ /******/
+ /******/
+ // __webpack_public_path__
+ /******/
+ return __webpack_require__.m = modules, __webpack_require__.c = installedModules,
+ __webpack_require__.p = "", __webpack_require__(0);
+ }([ /* 0 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ });
+ var _ArrowKeyStepper = __webpack_require__(1);
+ Object.defineProperty(exports, "ArrowKeyStepper", {
+ enumerable: !0,
+ get: function() {
+ return _ArrowKeyStepper.ArrowKeyStepper;
+ }
+ });
+ var _AutoSizer = __webpack_require__(91);
+ Object.defineProperty(exports, "AutoSizer", {
+ enumerable: !0,
+ get: function() {
+ return _AutoSizer.AutoSizer;
+ }
+ });
+ var _CellMeasurer = __webpack_require__(94);
+ Object.defineProperty(exports, "CellMeasurer", {
+ enumerable: !0,
+ get: function() {
+ return _CellMeasurer.CellMeasurer;
+ }
+ }), Object.defineProperty(exports, "defaultCellMeasurerCellSizeCache", {
+ enumerable: !0,
+ get: function() {
+ return _CellMeasurer.defaultCellSizeCache;
+ }
+ }), Object.defineProperty(exports, "uniformSizeCellMeasurerCellSizeCache", {
+ enumerable: !0,
+ get: function() {
+ return _CellMeasurer.defaultCellSizeCache;
+ }
+ });
+ var _Collection = __webpack_require__(98);
+ Object.defineProperty(exports, "Collection", {
+ enumerable: !0,
+ get: function() {
+ return _Collection.Collection;
+ }
+ });
+ var _ColumnSizer = __webpack_require__(118);
+ Object.defineProperty(exports, "ColumnSizer", {
+ enumerable: !0,
+ get: function() {
+ return _ColumnSizer.ColumnSizer;
+ }
+ });
+ var _Table = __webpack_require__(128);
+ Object.defineProperty(exports, "defaultTableCellDataGetter", {
+ enumerable: !0,
+ get: function() {
+ return _Table.defaultCellDataGetter;
+ }
+ }), Object.defineProperty(exports, "defaultTableCellRenderer", {
+ enumerable: !0,
+ get: function() {
+ return _Table.defaultCellRenderer;
+ }
+ }), Object.defineProperty(exports, "defaultTableHeaderRenderer", {
+ enumerable: !0,
+ get: function() {
+ return _Table.defaultHeaderRenderer;
+ }
+ }), Object.defineProperty(exports, "defaultTableRowRenderer", {
+ enumerable: !0,
+ get: function() {
+ return _Table.defaultRowRenderer;
+ }
+ }), Object.defineProperty(exports, "Table", {
+ enumerable: !0,
+ get: function() {
+ return _Table.Table;
+ }
+ }), Object.defineProperty(exports, "Column", {
+ enumerable: !0,
+ get: function() {
+ return _Table.Column;
+ }
+ }), Object.defineProperty(exports, "SortDirection", {
+ enumerable: !0,
+ get: function() {
+ return _Table.SortDirection;
+ }
+ }), Object.defineProperty(exports, "SortIndicator", {
+ enumerable: !0,
+ get: function() {
+ return _Table.SortIndicator;
+ }
+ });
+ var _Grid = __webpack_require__(120);
+ Object.defineProperty(exports, "defaultCellRangeRenderer", {
+ enumerable: !0,
+ get: function() {
+ return _Grid.defaultCellRangeRenderer;
+ }
+ }), Object.defineProperty(exports, "Grid", {
+ enumerable: !0,
+ get: function() {
+ return _Grid.Grid;
+ }
+ });
+ var _InfiniteLoader = __webpack_require__(137);
+ Object.defineProperty(exports, "InfiniteLoader", {
+ enumerable: !0,
+ get: function() {
+ return _InfiniteLoader.InfiniteLoader;
+ }
+ });
+ var _ScrollSync = __webpack_require__(139);
+ Object.defineProperty(exports, "ScrollSync", {
+ enumerable: !0,
+ get: function() {
+ return _ScrollSync.ScrollSync;
+ }
+ });
+ var _List = __webpack_require__(141);
+ Object.defineProperty(exports, "List", {
+ enumerable: !0,
+ get: function() {
+ return _List.List;
+ }
+ });
+ var _WindowScroller = __webpack_require__(143);
+ Object.defineProperty(exports, "WindowScroller", {
+ enumerable: !0,
+ get: function() {
+ return _WindowScroller.WindowScroller;
+ }
+ });
+ }, /* 1 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ }), exports.ArrowKeyStepper = exports.default = void 0;
+ var _ArrowKeyStepper2 = __webpack_require__(2), _ArrowKeyStepper3 = _interopRequireDefault(_ArrowKeyStepper2);
+ exports.default = _ArrowKeyStepper3.default, exports.ArrowKeyStepper = _ArrowKeyStepper3.default;
+ }, /* 2 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ });
+ var _getPrototypeOf = __webpack_require__(3), _getPrototypeOf2 = _interopRequireDefault(_getPrototypeOf), _classCallCheck2 = __webpack_require__(29), _classCallCheck3 = _interopRequireDefault(_classCallCheck2), _createClass2 = __webpack_require__(30), _createClass3 = _interopRequireDefault(_createClass2), _possibleConstructorReturn2 = __webpack_require__(34), _possibleConstructorReturn3 = _interopRequireDefault(_possibleConstructorReturn2), _inherits2 = __webpack_require__(81), _inherits3 = _interopRequireDefault(_inherits2), _react = __webpack_require__(89), _react2 = _interopRequireDefault(_react), _reactAddonsShallowCompare = __webpack_require__(90), _reactAddonsShallowCompare2 = _interopRequireDefault(_reactAddonsShallowCompare), ArrowKeyStepper = function(_Component) {
+ function ArrowKeyStepper(props, context) {
+ (0, _classCallCheck3.default)(this, ArrowKeyStepper);
+ var _this = (0, _possibleConstructorReturn3.default)(this, (ArrowKeyStepper.__proto__ || (0,
+ _getPrototypeOf2.default)(ArrowKeyStepper)).call(this, props, context));
+ return _this.state = {
+ scrollToColumn: 0,
+ scrollToRow: 0
+ }, _this._columnStartIndex = 0, _this._columnStopIndex = 0, _this._rowStartIndex = 0,
+ _this._rowStopIndex = 0, _this._onKeyDown = _this._onKeyDown.bind(_this), _this._onSectionRendered = _this._onSectionRendered.bind(_this),
+ _this;
+ }
+ return (0, _inherits3.default)(ArrowKeyStepper, _Component), (0, _createClass3.default)(ArrowKeyStepper, [ {
+ key: "render",
+ value: function() {
+ var _props = this.props, className = _props.className, children = _props.children, _state = this.state, scrollToColumn = _state.scrollToColumn, scrollToRow = _state.scrollToRow;
+ return _react2.default.createElement("div", {
+ className: className,
+ onKeyDown: this._onKeyDown
+ }, children({
+ onSectionRendered: this._onSectionRendered,
+ scrollToColumn: scrollToColumn,
+ scrollToRow: scrollToRow
+ }));
+ }
+ }, {
+ key: "shouldComponentUpdate",
+ value: function(nextProps, nextState) {
+ return (0, _reactAddonsShallowCompare2.default)(this, nextProps, nextState);
+ }
+ }, {
+ key: "_onKeyDown",
+ value: function(event) {
+ var _props2 = this.props, columnCount = _props2.columnCount, rowCount = _props2.rowCount;
+ switch (event.key) {
+ case "ArrowDown":
+ event.preventDefault(), this.setState({
+ scrollToRow: Math.min(this._rowStopIndex + 1, rowCount - 1)
+ });
+ break;
+
+ case "ArrowLeft":
+ event.preventDefault(), this.setState({
+ scrollToColumn: Math.max(this._columnStartIndex - 1, 0)
+ });
+ break;
+
+ case "ArrowRight":
+ event.preventDefault(), this.setState({
+ scrollToColumn: Math.min(this._columnStopIndex + 1, columnCount - 1)
+ });
+ break;
+
+ case "ArrowUp":
+ event.preventDefault(), this.setState({
+ scrollToRow: Math.max(this._rowStartIndex - 1, 0)
+ });
+ }
+ }
+ }, {
+ key: "_onSectionRendered",
+ value: function(_ref) {
+ var columnStartIndex = _ref.columnStartIndex, columnStopIndex = _ref.columnStopIndex, rowStartIndex = _ref.rowStartIndex, rowStopIndex = _ref.rowStopIndex;
+ this._columnStartIndex = columnStartIndex, this._columnStopIndex = columnStopIndex,
+ this._rowStartIndex = rowStartIndex, this._rowStopIndex = rowStopIndex;
+ }
+ } ]), ArrowKeyStepper;
+ }(_react.Component);
+ exports.default = ArrowKeyStepper;
+ }, /* 3 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ module.exports = {
+ default: __webpack_require__(4),
+ __esModule: !0
+ };
+ }, /* 4 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ __webpack_require__(5), module.exports = __webpack_require__(16).Object.getPrototypeOf;
+ }, /* 5 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ // 19.1.2.9 Object.getPrototypeOf(O)
+ var toObject = __webpack_require__(6), $getPrototypeOf = __webpack_require__(8);
+ __webpack_require__(14)("getPrototypeOf", function() {
+ return function(it) {
+ return $getPrototypeOf(toObject(it));
+ };
+ });
+ }, /* 6 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ // 7.1.13 ToObject(argument)
+ var defined = __webpack_require__(7);
+ module.exports = function(it) {
+ return Object(defined(it));
+ };
+ }, /* 7 */
+ /***/
+ function(module, exports) {
+ // 7.2.1 RequireObjectCoercible(argument)
+ module.exports = function(it) {
+ if (void 0 == it) throw TypeError("Can't call method on " + it);
+ return it;
+ };
+ }, /* 8 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ // 19.1.2.9 / 15.2.3.2 Object.getPrototypeOf(O)
+ var has = __webpack_require__(9), toObject = __webpack_require__(6), IE_PROTO = __webpack_require__(10)("IE_PROTO"), ObjectProto = Object.prototype;
+ module.exports = Object.getPrototypeOf || function(O) {
+ return O = toObject(O), has(O, IE_PROTO) ? O[IE_PROTO] : "function" == typeof O.constructor && O instanceof O.constructor ? O.constructor.prototype : O instanceof Object ? ObjectProto : null;
+ };
+ }, /* 9 */
+ /***/
+ function(module, exports) {
+ var hasOwnProperty = {}.hasOwnProperty;
+ module.exports = function(it, key) {
+ return hasOwnProperty.call(it, key);
+ };
+ }, /* 10 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ var shared = __webpack_require__(11)("keys"), uid = __webpack_require__(13);
+ module.exports = function(key) {
+ return shared[key] || (shared[key] = uid(key));
+ };
+ }, /* 11 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ var global = __webpack_require__(12), SHARED = "__core-js_shared__", store = global[SHARED] || (global[SHARED] = {});
+ module.exports = function(key) {
+ return store[key] || (store[key] = {});
+ };
+ }, /* 12 */
+ /***/
+ function(module, exports) {
+ // https://github.com/zloirock/core-js/issues/86#issuecomment-115759028
+ var global = module.exports = "undefined" != typeof window && window.Math == Math ? window : "undefined" != typeof self && self.Math == Math ? self : Function("return this")();
+ "number" == typeof __g && (__g = global);
+ }, /* 13 */
+ /***/
+ function(module, exports) {
+ var id = 0, px = Math.random();
+ module.exports = function(key) {
+ return "Symbol(".concat(void 0 === key ? "" : key, ")_", (++id + px).toString(36));
+ };
+ }, /* 14 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ // most Object methods by ES6 should accept primitives
+ var $export = __webpack_require__(15), core = __webpack_require__(16), fails = __webpack_require__(25);
+ module.exports = function(KEY, exec) {
+ var fn = (core.Object || {})[KEY] || Object[KEY], exp = {};
+ exp[KEY] = exec(fn), $export($export.S + $export.F * fails(function() {
+ fn(1);
+ }), "Object", exp);
+ };
+ }, /* 15 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ var global = __webpack_require__(12), core = __webpack_require__(16), ctx = __webpack_require__(17), hide = __webpack_require__(19), PROTOTYPE = "prototype", $export = function(type, name, source) {
+ var key, own, out, IS_FORCED = type & $export.F, IS_GLOBAL = type & $export.G, IS_STATIC = type & $export.S, IS_PROTO = type & $export.P, IS_BIND = type & $export.B, IS_WRAP = type & $export.W, exports = IS_GLOBAL ? core : core[name] || (core[name] = {}), expProto = exports[PROTOTYPE], target = IS_GLOBAL ? global : IS_STATIC ? global[name] : (global[name] || {})[PROTOTYPE];
+ IS_GLOBAL && (source = name);
+ for (key in source) // contains in native
+ own = !IS_FORCED && target && void 0 !== target[key], own && key in exports || (// export native or passed
+ out = own ? target[key] : source[key], // prevent global pollution for namespaces
+ exports[key] = IS_GLOBAL && "function" != typeof target[key] ? source[key] : IS_BIND && own ? ctx(out, global) : IS_WRAP && target[key] == out ? function(C) {
+ var F = function(a, b, c) {
+ if (this instanceof C) {
+ switch (arguments.length) {
+ case 0:
+ return new C();
+
+ case 1:
+ return new C(a);
+
+ case 2:
+ return new C(a, b);
+ }
+ return new C(a, b, c);
+ }
+ return C.apply(this, arguments);
+ };
+ return F[PROTOTYPE] = C[PROTOTYPE], F;
+ }(out) : IS_PROTO && "function" == typeof out ? ctx(Function.call, out) : out, // export proto methods to core.%CONSTRUCTOR%.methods.%NAME%
+ IS_PROTO && ((exports.virtual || (exports.virtual = {}))[key] = out, // export proto methods to core.%CONSTRUCTOR%.prototype.%NAME%
+ type & $export.R && expProto && !expProto[key] && hide(expProto, key, out)));
+ };
+ // type bitmap
+ $export.F = 1, // forced
+ $export.G = 2, // global
+ $export.S = 4, // static
+ $export.P = 8, // proto
+ $export.B = 16, // bind
+ $export.W = 32, // wrap
+ $export.U = 64, // safe
+ $export.R = 128, // real proto method for `library`
+ module.exports = $export;
+ }, /* 16 */
+ /***/
+ function(module, exports) {
+ var core = module.exports = {
+ version: "2.4.0"
+ };
+ "number" == typeof __e && (__e = core);
+ }, /* 17 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ // optional / simple context binding
+ var aFunction = __webpack_require__(18);
+ module.exports = function(fn, that, length) {
+ if (aFunction(fn), void 0 === that) return fn;
+ switch (length) {
+ case 1:
+ return function(a) {
+ return fn.call(that, a);
+ };
+
+ case 2:
+ return function(a, b) {
+ return fn.call(that, a, b);
+ };
+
+ case 3:
+ return function(a, b, c) {
+ return fn.call(that, a, b, c);
+ };
+ }
+ return function() {
+ return fn.apply(that, arguments);
+ };
+ };
+ }, /* 18 */
+ /***/
+ function(module, exports) {
+ module.exports = function(it) {
+ if ("function" != typeof it) throw TypeError(it + " is not a function!");
+ return it;
+ };
+ }, /* 19 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ var dP = __webpack_require__(20), createDesc = __webpack_require__(28);
+ module.exports = __webpack_require__(24) ? function(object, key, value) {
+ return dP.f(object, key, createDesc(1, value));
+ } : function(object, key, value) {
+ return object[key] = value, object;
+ };
+ }, /* 20 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ var anObject = __webpack_require__(21), IE8_DOM_DEFINE = __webpack_require__(23), toPrimitive = __webpack_require__(27), dP = Object.defineProperty;
+ exports.f = __webpack_require__(24) ? Object.defineProperty : function(O, P, Attributes) {
+ if (anObject(O), P = toPrimitive(P, !0), anObject(Attributes), IE8_DOM_DEFINE) try {
+ return dP(O, P, Attributes);
+ } catch (e) {}
+ if ("get" in Attributes || "set" in Attributes) throw TypeError("Accessors not supported!");
+ return "value" in Attributes && (O[P] = Attributes.value), O;
+ };
+ }, /* 21 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ var isObject = __webpack_require__(22);
+ module.exports = function(it) {
+ if (!isObject(it)) throw TypeError(it + " is not an object!");
+ return it;
+ };
+ }, /* 22 */
+ /***/
+ function(module, exports) {
+ module.exports = function(it) {
+ return "object" == typeof it ? null !== it : "function" == typeof it;
+ };
+ }, /* 23 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ module.exports = !__webpack_require__(24) && !__webpack_require__(25)(function() {
+ return 7 != Object.defineProperty(__webpack_require__(26)("div"), "a", {
+ get: function() {
+ return 7;
+ }
+ }).a;
+ });
+ }, /* 24 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ // Thank's IE8 for his funny defineProperty
+ module.exports = !__webpack_require__(25)(function() {
+ return 7 != Object.defineProperty({}, "a", {
+ get: function() {
+ return 7;
+ }
+ }).a;
+ });
+ }, /* 25 */
+ /***/
+ function(module, exports) {
+ module.exports = function(exec) {
+ try {
+ return !!exec();
+ } catch (e) {
+ return !0;
+ }
+ };
+ }, /* 26 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ var isObject = __webpack_require__(22), document = __webpack_require__(12).document, is = isObject(document) && isObject(document.createElement);
+ module.exports = function(it) {
+ return is ? document.createElementNS("http://www.w3.org/1999/xhtml",it) : {};
+ };
+ }, /* 27 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ // 7.1.1 ToPrimitive(input [, PreferredType])
+ var isObject = __webpack_require__(22);
+ // instead of the ES6 spec version, we didn't implement @@toPrimitive case
+ // and the second argument - flag - preferred type is a string
+ module.exports = function(it, S) {
+ if (!isObject(it)) return it;
+ var fn, val;
+ if (S && "function" == typeof (fn = it.toString) && !isObject(val = fn.call(it))) return val;
+ if ("function" == typeof (fn = it.valueOf) && !isObject(val = fn.call(it))) return val;
+ if (!S && "function" == typeof (fn = it.toString) && !isObject(val = fn.call(it))) return val;
+ throw TypeError("Can't convert object to primitive value");
+ };
+ }, /* 28 */
+ /***/
+ function(module, exports) {
+ module.exports = function(bitmap, value) {
+ return {
+ enumerable: !(1 & bitmap),
+ configurable: !(2 & bitmap),
+ writable: !(4 & bitmap),
+ value: value
+ };
+ };
+ }, /* 29 */
+ /***/
+ function(module, exports) {
+ "use strict";
+ exports.__esModule = !0, exports.default = function(instance, Constructor) {
+ if (!(instance instanceof Constructor)) throw new TypeError("Cannot call a class as a function");
+ };
+ }, /* 30 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ exports.__esModule = !0;
+ var _defineProperty = __webpack_require__(31), _defineProperty2 = _interopRequireDefault(_defineProperty);
+ exports.default = function() {
+ function defineProperties(target, props) {
+ for (var i = 0; i < props.length; i++) {
+ var descriptor = props[i];
+ descriptor.enumerable = descriptor.enumerable || !1, descriptor.configurable = !0,
+ "value" in descriptor && (descriptor.writable = !0), (0, _defineProperty2.default)(target, descriptor.key, descriptor);
+ }
+ }
+ return function(Constructor, protoProps, staticProps) {
+ return protoProps && defineProperties(Constructor.prototype, protoProps), staticProps && defineProperties(Constructor, staticProps),
+ Constructor;
+ };
+ }();
+ }, /* 31 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ module.exports = {
+ default: __webpack_require__(32),
+ __esModule: !0
+ };
+ }, /* 32 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ __webpack_require__(33);
+ var $Object = __webpack_require__(16).Object;
+ module.exports = function(it, key, desc) {
+ return $Object.defineProperty(it, key, desc);
+ };
+ }, /* 33 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ var $export = __webpack_require__(15);
+ // 19.1.2.4 / 15.2.3.6 Object.defineProperty(O, P, Attributes)
+ $export($export.S + $export.F * !__webpack_require__(24), "Object", {
+ defineProperty: __webpack_require__(20).f
+ });
+ }, /* 34 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ exports.__esModule = !0;
+ var _typeof2 = __webpack_require__(35), _typeof3 = _interopRequireDefault(_typeof2);
+ exports.default = function(self, call) {
+ if (!self) throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
+ return !call || "object" !== ("undefined" == typeof call ? "undefined" : (0, _typeof3.default)(call)) && "function" != typeof call ? self : call;
+ };
+ }, /* 35 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ exports.__esModule = !0;
+ var _iterator = __webpack_require__(36), _iterator2 = _interopRequireDefault(_iterator), _symbol = __webpack_require__(65), _symbol2 = _interopRequireDefault(_symbol), _typeof = "function" == typeof _symbol2.default && "symbol" == typeof _iterator2.default ? function(obj) {
+ return typeof obj;
+ } : function(obj) {
+ return obj && "function" == typeof _symbol2.default && obj.constructor === _symbol2.default && obj !== _symbol2.default.prototype ? "symbol" : typeof obj;
+ };
+ exports.default = "function" == typeof _symbol2.default && "symbol" === _typeof(_iterator2.default) ? function(obj) {
+ return "undefined" == typeof obj ? "undefined" : _typeof(obj);
+ } : function(obj) {
+ return obj && "function" == typeof _symbol2.default && obj.constructor === _symbol2.default && obj !== _symbol2.default.prototype ? "symbol" : "undefined" == typeof obj ? "undefined" : _typeof(obj);
+ };
+ }, /* 36 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ module.exports = {
+ default: __webpack_require__(37),
+ __esModule: !0
+ };
+ }, /* 37 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ __webpack_require__(38), __webpack_require__(60), module.exports = __webpack_require__(64).f("iterator");
+ }, /* 38 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ var $at = __webpack_require__(39)(!0);
+ // 21.1.3.27 String.prototype[@@iterator]()
+ __webpack_require__(41)(String, "String", function(iterated) {
+ this._t = String(iterated), // target
+ this._i = 0;
+ }, function() {
+ var point, O = this._t, index = this._i;
+ return index >= O.length ? {
+ value: void 0,
+ done: !0
+ } : (point = $at(O, index), this._i += point.length, {
+ value: point,
+ done: !1
+ });
+ });
+ }, /* 39 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ var toInteger = __webpack_require__(40), defined = __webpack_require__(7);
+ // true -> String#at
+ // false -> String#codePointAt
+ module.exports = function(TO_STRING) {
+ return function(that, pos) {
+ var a, b, s = String(defined(that)), i = toInteger(pos), l = s.length;
+ return i < 0 || i >= l ? TO_STRING ? "" : void 0 : (a = s.charCodeAt(i), a < 55296 || a > 56319 || i + 1 === l || (b = s.charCodeAt(i + 1)) < 56320 || b > 57343 ? TO_STRING ? s.charAt(i) : a : TO_STRING ? s.slice(i, i + 2) : (a - 55296 << 10) + (b - 56320) + 65536);
+ };
+ };
+ }, /* 40 */
+ /***/
+ function(module, exports) {
+ // 7.1.4 ToInteger
+ var ceil = Math.ceil, floor = Math.floor;
+ module.exports = function(it) {
+ return isNaN(it = +it) ? 0 : (it > 0 ? floor : ceil)(it);
+ };
+ }, /* 41 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ var LIBRARY = __webpack_require__(42), $export = __webpack_require__(15), redefine = __webpack_require__(43), hide = __webpack_require__(19), has = __webpack_require__(9), Iterators = __webpack_require__(44), $iterCreate = __webpack_require__(45), setToStringTag = __webpack_require__(58), getPrototypeOf = __webpack_require__(8), ITERATOR = __webpack_require__(59)("iterator"), BUGGY = !([].keys && "next" in [].keys()), FF_ITERATOR = "@@iterator", KEYS = "keys", VALUES = "values", returnThis = function() {
+ return this;
+ };
+ module.exports = function(Base, NAME, Constructor, next, DEFAULT, IS_SET, FORCED) {
+ $iterCreate(Constructor, NAME, next);
+ var methods, key, IteratorPrototype, getMethod = function(kind) {
+ if (!BUGGY && kind in proto) return proto[kind];
+ switch (kind) {
+ case KEYS:
+ return function() {
+ return new Constructor(this, kind);
+ };
+
+ case VALUES:
+ return function() {
+ return new Constructor(this, kind);
+ };
+ }
+ return function() {
+ return new Constructor(this, kind);
+ };
+ }, TAG = NAME + " Iterator", DEF_VALUES = DEFAULT == VALUES, VALUES_BUG = !1, proto = Base.prototype, $native = proto[ITERATOR] || proto[FF_ITERATOR] || DEFAULT && proto[DEFAULT], $default = $native || getMethod(DEFAULT), $entries = DEFAULT ? DEF_VALUES ? getMethod("entries") : $default : void 0, $anyNative = "Array" == NAME ? proto.entries || $native : $native;
+ if (// Fix native
+ $anyNative && (IteratorPrototype = getPrototypeOf($anyNative.call(new Base())),
+ IteratorPrototype !== Object.prototype && (// Set @@toStringTag to native iterators
+ setToStringTag(IteratorPrototype, TAG, !0), // fix for some old engines
+ LIBRARY || has(IteratorPrototype, ITERATOR) || hide(IteratorPrototype, ITERATOR, returnThis))),
+ // fix Array#{values, @@iterator}.name in V8 / FF
+ DEF_VALUES && $native && $native.name !== VALUES && (VALUES_BUG = !0, $default = function() {
+ return $native.call(this);
+ }), // Define iterator
+ LIBRARY && !FORCED || !BUGGY && !VALUES_BUG && proto[ITERATOR] || hide(proto, ITERATOR, $default),
+ // Plug for library
+ Iterators[NAME] = $default, Iterators[TAG] = returnThis, DEFAULT) if (methods = {
+ values: DEF_VALUES ? $default : getMethod(VALUES),
+ keys: IS_SET ? $default : getMethod(KEYS),
+ entries: $entries
+ }, FORCED) for (key in methods) key in proto || redefine(proto, key, methods[key]); else $export($export.P + $export.F * (BUGGY || VALUES_BUG), NAME, methods);
+ return methods;
+ };
+ }, /* 42 */
+ /***/
+ function(module, exports) {
+ module.exports = !0;
+ }, /* 43 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ module.exports = __webpack_require__(19);
+ }, /* 44 */
+ /***/
+ function(module, exports) {
+ module.exports = {};
+ }, /* 45 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ var create = __webpack_require__(46), descriptor = __webpack_require__(28), setToStringTag = __webpack_require__(58), IteratorPrototype = {};
+ // 25.1.2.1.1 %IteratorPrototype%[@@iterator]()
+ __webpack_require__(19)(IteratorPrototype, __webpack_require__(59)("iterator"), function() {
+ return this;
+ }), module.exports = function(Constructor, NAME, next) {
+ Constructor.prototype = create(IteratorPrototype, {
+ next: descriptor(1, next)
+ }), setToStringTag(Constructor, NAME + " Iterator");
+ };
+ }, /* 46 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ // 19.1.2.2 / 15.2.3.5 Object.create(O [, Properties])
+ var anObject = __webpack_require__(21), dPs = __webpack_require__(47), enumBugKeys = __webpack_require__(56), IE_PROTO = __webpack_require__(10)("IE_PROTO"), Empty = function() {}, PROTOTYPE = "prototype", createDict = function() {
+ // Thrash, waste and sodomy: IE GC bug
+ var iframeDocument, iframe = __webpack_require__(26)("iframe"), i = enumBugKeys.length, lt = "<", gt = ">";
+ for (iframe.style.display = "none", __webpack_require__(57).appendChild(iframe),
+ iframe.src = "javascript:", // eslint-disable-line no-script-url
+ // createDict = iframe.contentWindow.Object;
+ // html.removeChild(iframe);
+ iframeDocument = iframe.contentWindow.document, iframeDocument.open(), iframeDocument.write(lt + "script" + gt + "document.F=Object" + lt + "/script" + gt),
+ iframeDocument.close(), createDict = iframeDocument.F; i--; ) delete createDict[PROTOTYPE][enumBugKeys[i]];
+ return createDict();
+ };
+ module.exports = Object.create || function(O, Properties) {
+ var result;
+ // add "__proto__" for Object.getPrototypeOf polyfill
+ return null !== O ? (Empty[PROTOTYPE] = anObject(O), result = new Empty(), Empty[PROTOTYPE] = null,
+ result[IE_PROTO] = O) : result = createDict(), void 0 === Properties ? result : dPs(result, Properties);
+ };
+ }, /* 47 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ var dP = __webpack_require__(20), anObject = __webpack_require__(21), getKeys = __webpack_require__(48);
+ module.exports = __webpack_require__(24) ? Object.defineProperties : function(O, Properties) {
+ anObject(O);
+ for (var P, keys = getKeys(Properties), length = keys.length, i = 0; length > i; ) dP.f(O, P = keys[i++], Properties[P]);
+ return O;
+ };
+ }, /* 48 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ // 19.1.2.14 / 15.2.3.14 Object.keys(O)
+ var $keys = __webpack_require__(49), enumBugKeys = __webpack_require__(56);
+ module.exports = Object.keys || function(O) {
+ return $keys(O, enumBugKeys);
+ };
+ }, /* 49 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ var has = __webpack_require__(9), toIObject = __webpack_require__(50), arrayIndexOf = __webpack_require__(53)(!1), IE_PROTO = __webpack_require__(10)("IE_PROTO");
+ module.exports = function(object, names) {
+ var key, O = toIObject(object), i = 0, result = [];
+ for (key in O) key != IE_PROTO && has(O, key) && result.push(key);
+ // Don't enum bug & hidden keys
+ for (;names.length > i; ) has(O, key = names[i++]) && (~arrayIndexOf(result, key) || result.push(key));
+ return result;
+ };
+ }, /* 50 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ // to indexed object, toObject with fallback for non-array-like ES3 strings
+ var IObject = __webpack_require__(51), defined = __webpack_require__(7);
+ module.exports = function(it) {
+ return IObject(defined(it));
+ };
+ }, /* 51 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ // fallback for non-array-like ES3 and non-enumerable old V8 strings
+ var cof = __webpack_require__(52);
+ module.exports = Object("z").propertyIsEnumerable(0) ? Object : function(it) {
+ return "String" == cof(it) ? it.split("") : Object(it);
+ };
+ }, /* 52 */
+ /***/
+ function(module, exports) {
+ var toString = {}.toString;
+ module.exports = function(it) {
+ return toString.call(it).slice(8, -1);
+ };
+ }, /* 53 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ // false -> Array#indexOf
+ // true -> Array#includes
+ var toIObject = __webpack_require__(50), toLength = __webpack_require__(54), toIndex = __webpack_require__(55);
+ module.exports = function(IS_INCLUDES) {
+ return function($this, el, fromIndex) {
+ var value, O = toIObject($this), length = toLength(O.length), index = toIndex(fromIndex, length);
+ // Array#includes uses SameValueZero equality algorithm
+ if (IS_INCLUDES && el != el) {
+ for (;length > index; ) if (value = O[index++], value != value) return !0;
+ } else for (;length > index; index++) if ((IS_INCLUDES || index in O) && O[index] === el) return IS_INCLUDES || index || 0;
+ return !IS_INCLUDES && -1;
+ };
+ };
+ }, /* 54 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ // 7.1.15 ToLength
+ var toInteger = __webpack_require__(40), min = Math.min;
+ module.exports = function(it) {
+ return it > 0 ? min(toInteger(it), 9007199254740991) : 0;
+ };
+ }, /* 55 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ var toInteger = __webpack_require__(40), max = Math.max, min = Math.min;
+ module.exports = function(index, length) {
+ return index = toInteger(index), index < 0 ? max(index + length, 0) : min(index, length);
+ };
+ }, /* 56 */
+ /***/
+ function(module, exports) {
+ // IE 8- don't enum bug keys
+ module.exports = "constructor,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,toLocaleString,toString,valueOf".split(",");
+ }, /* 57 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ module.exports = __webpack_require__(12).document && document.documentElement;
+ }, /* 58 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ var def = __webpack_require__(20).f, has = __webpack_require__(9), TAG = __webpack_require__(59)("toStringTag");
+ module.exports = function(it, tag, stat) {
+ it && !has(it = stat ? it : it.prototype, TAG) && def(it, TAG, {
+ configurable: !0,
+ value: tag
+ });
+ };
+ }, /* 59 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ var store = __webpack_require__(11)("wks"), uid = __webpack_require__(13), Symbol = __webpack_require__(12).Symbol, USE_SYMBOL = "function" == typeof Symbol, $exports = module.exports = function(name) {
+ return store[name] || (store[name] = USE_SYMBOL && Symbol[name] || (USE_SYMBOL ? Symbol : uid)("Symbol." + name));
+ };
+ $exports.store = store;
+ }, /* 60 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ __webpack_require__(61);
+ for (var global = __webpack_require__(12), hide = __webpack_require__(19), Iterators = __webpack_require__(44), TO_STRING_TAG = __webpack_require__(59)("toStringTag"), collections = [ "NodeList", "DOMTokenList", "MediaList", "StyleSheetList", "CSSRuleList" ], i = 0; i < 5; i++) {
+ var NAME = collections[i], Collection = global[NAME], proto = Collection && Collection.prototype;
+ proto && !proto[TO_STRING_TAG] && hide(proto, TO_STRING_TAG, NAME), Iterators[NAME] = Iterators.Array;
+ }
+ }, /* 61 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ var addToUnscopables = __webpack_require__(62), step = __webpack_require__(63), Iterators = __webpack_require__(44), toIObject = __webpack_require__(50);
+ // 22.1.3.4 Array.prototype.entries()
+ // 22.1.3.13 Array.prototype.keys()
+ // 22.1.3.29 Array.prototype.values()
+ // 22.1.3.30 Array.prototype[@@iterator]()
+ module.exports = __webpack_require__(41)(Array, "Array", function(iterated, kind) {
+ this._t = toIObject(iterated), // target
+ this._i = 0, // next index
+ this._k = kind;
+ }, function() {
+ var O = this._t, kind = this._k, index = this._i++;
+ return !O || index >= O.length ? (this._t = void 0, step(1)) : "keys" == kind ? step(0, index) : "values" == kind ? step(0, O[index]) : step(0, [ index, O[index] ]);
+ }, "values"), // argumentsList[@@iterator] is %ArrayProto_values% (9.4.4.6, 9.4.4.7)
+ Iterators.Arguments = Iterators.Array, addToUnscopables("keys"), addToUnscopables("values"),
+ addToUnscopables("entries");
+ }, /* 62 */
+ /***/
+ function(module, exports) {
+ module.exports = function() {};
+ }, /* 63 */
+ /***/
+ function(module, exports) {
+ module.exports = function(done, value) {
+ return {
+ value: value,
+ done: !!done
+ };
+ };
+ }, /* 64 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ exports.f = __webpack_require__(59);
+ }, /* 65 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ module.exports = {
+ default: __webpack_require__(66),
+ __esModule: !0
+ };
+ }, /* 66 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ __webpack_require__(67), __webpack_require__(78), __webpack_require__(79), __webpack_require__(80),
+ module.exports = __webpack_require__(16).Symbol;
+ }, /* 67 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ // ECMAScript 6 symbols shim
+ var global = __webpack_require__(12), has = __webpack_require__(9), DESCRIPTORS = __webpack_require__(24), $export = __webpack_require__(15), redefine = __webpack_require__(43), META = __webpack_require__(68).KEY, $fails = __webpack_require__(25), shared = __webpack_require__(11), setToStringTag = __webpack_require__(58), uid = __webpack_require__(13), wks = __webpack_require__(59), wksExt = __webpack_require__(64), wksDefine = __webpack_require__(69), keyOf = __webpack_require__(70), enumKeys = __webpack_require__(71), isArray = __webpack_require__(74), anObject = __webpack_require__(21), toIObject = __webpack_require__(50), toPrimitive = __webpack_require__(27), createDesc = __webpack_require__(28), _create = __webpack_require__(46), gOPNExt = __webpack_require__(75), $GOPD = __webpack_require__(77), $DP = __webpack_require__(20), $keys = __webpack_require__(48), gOPD = $GOPD.f, dP = $DP.f, gOPN = gOPNExt.f, $Symbol = global.Symbol, $JSON = global.JSON, _stringify = $JSON && $JSON.stringify, PROTOTYPE = "prototype", HIDDEN = wks("_hidden"), TO_PRIMITIVE = wks("toPrimitive"), isEnum = {}.propertyIsEnumerable, SymbolRegistry = shared("symbol-registry"), AllSymbols = shared("symbols"), OPSymbols = shared("op-symbols"), ObjectProto = Object[PROTOTYPE], USE_NATIVE = "function" == typeof $Symbol, QObject = global.QObject, setter = !QObject || !QObject[PROTOTYPE] || !QObject[PROTOTYPE].findChild, setSymbolDesc = DESCRIPTORS && $fails(function() {
+ return 7 != _create(dP({}, "a", {
+ get: function() {
+ return dP(this, "a", {
+ value: 7
+ }).a;
+ }
+ })).a;
+ }) ? function(it, key, D) {
+ var protoDesc = gOPD(ObjectProto, key);
+ protoDesc && delete ObjectProto[key], dP(it, key, D), protoDesc && it !== ObjectProto && dP(ObjectProto, key, protoDesc);
+ } : dP, wrap = function(tag) {
+ var sym = AllSymbols[tag] = _create($Symbol[PROTOTYPE]);
+ return sym._k = tag, sym;
+ }, isSymbol = USE_NATIVE && "symbol" == typeof $Symbol.iterator ? function(it) {
+ return "symbol" == typeof it;
+ } : function(it) {
+ return it instanceof $Symbol;
+ }, $defineProperty = function(it, key, D) {
+ return it === ObjectProto && $defineProperty(OPSymbols, key, D), anObject(it), key = toPrimitive(key, !0),
+ anObject(D), has(AllSymbols, key) ? (D.enumerable ? (has(it, HIDDEN) && it[HIDDEN][key] && (it[HIDDEN][key] = !1),
+ D = _create(D, {
+ enumerable: createDesc(0, !1)
+ })) : (has(it, HIDDEN) || dP(it, HIDDEN, createDesc(1, {})), it[HIDDEN][key] = !0),
+ setSymbolDesc(it, key, D)) : dP(it, key, D);
+ }, $defineProperties = function(it, P) {
+ anObject(it);
+ for (var key, keys = enumKeys(P = toIObject(P)), i = 0, l = keys.length; l > i; ) $defineProperty(it, key = keys[i++], P[key]);
+ return it;
+ }, $create = function(it, P) {
+ return void 0 === P ? _create(it) : $defineProperties(_create(it), P);
+ }, $propertyIsEnumerable = function(key) {
+ var E = isEnum.call(this, key = toPrimitive(key, !0));
+ return !(this === ObjectProto && has(AllSymbols, key) && !has(OPSymbols, key)) && (!(E || !has(this, key) || !has(AllSymbols, key) || has(this, HIDDEN) && this[HIDDEN][key]) || E);
+ }, $getOwnPropertyDescriptor = function(it, key) {
+ if (it = toIObject(it), key = toPrimitive(key, !0), it !== ObjectProto || !has(AllSymbols, key) || has(OPSymbols, key)) {
+ var D = gOPD(it, key);
+ return !D || !has(AllSymbols, key) || has(it, HIDDEN) && it[HIDDEN][key] || (D.enumerable = !0),
+ D;
+ }
+ }, $getOwnPropertyNames = function(it) {
+ for (var key, names = gOPN(toIObject(it)), result = [], i = 0; names.length > i; ) has(AllSymbols, key = names[i++]) || key == HIDDEN || key == META || result.push(key);
+ return result;
+ }, $getOwnPropertySymbols = function(it) {
+ for (var key, IS_OP = it === ObjectProto, names = gOPN(IS_OP ? OPSymbols : toIObject(it)), result = [], i = 0; names.length > i; ) !has(AllSymbols, key = names[i++]) || IS_OP && !has(ObjectProto, key) || result.push(AllSymbols[key]);
+ return result;
+ };
+ // 19.4.1.1 Symbol([description])
+ USE_NATIVE || ($Symbol = function() {
+ if (this instanceof $Symbol) throw TypeError("Symbol is not a constructor!");
+ var tag = uid(arguments.length > 0 ? arguments[0] : void 0), $set = function(value) {
+ this === ObjectProto && $set.call(OPSymbols, value), has(this, HIDDEN) && has(this[HIDDEN], tag) && (this[HIDDEN][tag] = !1),
+ setSymbolDesc(this, tag, createDesc(1, value));
+ };
+ return DESCRIPTORS && setter && setSymbolDesc(ObjectProto, tag, {
+ configurable: !0,
+ set: $set
+ }), wrap(tag);
+ }, redefine($Symbol[PROTOTYPE], "toString", function() {
+ return this._k;
+ }), $GOPD.f = $getOwnPropertyDescriptor, $DP.f = $defineProperty, __webpack_require__(76).f = gOPNExt.f = $getOwnPropertyNames,
+ __webpack_require__(73).f = $propertyIsEnumerable, __webpack_require__(72).f = $getOwnPropertySymbols,
+ DESCRIPTORS && !__webpack_require__(42) && redefine(ObjectProto, "propertyIsEnumerable", $propertyIsEnumerable, !0),
+ wksExt.f = function(name) {
+ return wrap(wks(name));
+ }), $export($export.G + $export.W + $export.F * !USE_NATIVE, {
+ Symbol: $Symbol
+ });
+ for (var symbols = "hasInstance,isConcatSpreadable,iterator,match,replace,search,species,split,toPrimitive,toStringTag,unscopables".split(","), i = 0; symbols.length > i; ) wks(symbols[i++]);
+ for (var symbols = $keys(wks.store), i = 0; symbols.length > i; ) wksDefine(symbols[i++]);
+ $export($export.S + $export.F * !USE_NATIVE, "Symbol", {
+ // 19.4.2.1 Symbol.for(key)
+ for: function(key) {
+ return has(SymbolRegistry, key += "") ? SymbolRegistry[key] : SymbolRegistry[key] = $Symbol(key);
+ },
+ // 19.4.2.5 Symbol.keyFor(sym)
+ keyFor: function(key) {
+ if (isSymbol(key)) return keyOf(SymbolRegistry, key);
+ throw TypeError(key + " is not a symbol!");
+ },
+ useSetter: function() {
+ setter = !0;
+ },
+ useSimple: function() {
+ setter = !1;
+ }
+ }), $export($export.S + $export.F * !USE_NATIVE, "Object", {
+ // 19.1.2.2 Object.create(O [, Properties])
+ create: $create,
+ // 19.1.2.4 Object.defineProperty(O, P, Attributes)
+ defineProperty: $defineProperty,
+ // 19.1.2.3 Object.defineProperties(O, Properties)
+ defineProperties: $defineProperties,
+ // 19.1.2.6 Object.getOwnPropertyDescriptor(O, P)
+ getOwnPropertyDescriptor: $getOwnPropertyDescriptor,
+ // 19.1.2.7 Object.getOwnPropertyNames(O)
+ getOwnPropertyNames: $getOwnPropertyNames,
+ // 19.1.2.8 Object.getOwnPropertySymbols(O)
+ getOwnPropertySymbols: $getOwnPropertySymbols
+ }), // 24.3.2 JSON.stringify(value [, replacer [, space]])
+ $JSON && $export($export.S + $export.F * (!USE_NATIVE || $fails(function() {
+ var S = $Symbol();
+ // MS Edge converts symbol values to JSON as {}
+ // WebKit converts symbol values to JSON as null
+ // V8 throws on boxed symbols
+ return "[null]" != _stringify([ S ]) || "{}" != _stringify({
+ a: S
+ }) || "{}" != _stringify(Object(S));
+ })), "JSON", {
+ stringify: function(it) {
+ if (void 0 !== it && !isSymbol(it)) {
+ for (// IE8 returns string on undefined
+ var replacer, $replacer, args = [ it ], i = 1; arguments.length > i; ) args.push(arguments[i++]);
+ return replacer = args[1], "function" == typeof replacer && ($replacer = replacer),
+ !$replacer && isArray(replacer) || (replacer = function(key, value) {
+ if ($replacer && (value = $replacer.call(this, key, value)), !isSymbol(value)) return value;
+ }), args[1] = replacer, _stringify.apply($JSON, args);
+ }
+ }
+ }), // 19.4.3.4 Symbol.prototype[@@toPrimitive](hint)
+ $Symbol[PROTOTYPE][TO_PRIMITIVE] || __webpack_require__(19)($Symbol[PROTOTYPE], TO_PRIMITIVE, $Symbol[PROTOTYPE].valueOf),
+ // 19.4.3.5 Symbol.prototype[@@toStringTag]
+ setToStringTag($Symbol, "Symbol"), // 20.2.1.9 Math[@@toStringTag]
+ setToStringTag(Math, "Math", !0), // 24.3.3 JSON[@@toStringTag]
+ setToStringTag(global.JSON, "JSON", !0);
+ }, /* 68 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ var META = __webpack_require__(13)("meta"), isObject = __webpack_require__(22), has = __webpack_require__(9), setDesc = __webpack_require__(20).f, id = 0, isExtensible = Object.isExtensible || function() {
+ return !0;
+ }, FREEZE = !__webpack_require__(25)(function() {
+ return isExtensible(Object.preventExtensions({}));
+ }), setMeta = function(it) {
+ setDesc(it, META, {
+ value: {
+ i: "O" + ++id,
+ // object ID
+ w: {}
+ }
+ });
+ }, fastKey = function(it, create) {
+ // return primitive with prefix
+ if (!isObject(it)) return "symbol" == typeof it ? it : ("string" == typeof it ? "S" : "P") + it;
+ if (!has(it, META)) {
+ // can't set metadata to uncaught frozen object
+ if (!isExtensible(it)) return "F";
+ // not necessary to add metadata
+ if (!create) return "E";
+ // add missing metadata
+ setMeta(it);
+ }
+ return it[META].i;
+ }, getWeak = function(it, create) {
+ if (!has(it, META)) {
+ // can't set metadata to uncaught frozen object
+ if (!isExtensible(it)) return !0;
+ // not necessary to add metadata
+ if (!create) return !1;
+ // add missing metadata
+ setMeta(it);
+ }
+ return it[META].w;
+ }, onFreeze = function(it) {
+ return FREEZE && meta.NEED && isExtensible(it) && !has(it, META) && setMeta(it),
+ it;
+ }, meta = module.exports = {
+ KEY: META,
+ NEED: !1,
+ fastKey: fastKey,
+ getWeak: getWeak,
+ onFreeze: onFreeze
+ };
+ }, /* 69 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ var global = __webpack_require__(12), core = __webpack_require__(16), LIBRARY = __webpack_require__(42), wksExt = __webpack_require__(64), defineProperty = __webpack_require__(20).f;
+ module.exports = function(name) {
+ var $Symbol = core.Symbol || (core.Symbol = LIBRARY ? {} : global.Symbol || {});
+ "_" == name.charAt(0) || name in $Symbol || defineProperty($Symbol, name, {
+ value: wksExt.f(name)
+ });
+ };
+ }, /* 70 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ var getKeys = __webpack_require__(48), toIObject = __webpack_require__(50);
+ module.exports = function(object, el) {
+ for (var key, O = toIObject(object), keys = getKeys(O), length = keys.length, index = 0; length > index; ) if (O[key = keys[index++]] === el) return key;
+ };
+ }, /* 71 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ // all enumerable object keys, includes symbols
+ var getKeys = __webpack_require__(48), gOPS = __webpack_require__(72), pIE = __webpack_require__(73);
+ module.exports = function(it) {
+ var result = getKeys(it), getSymbols = gOPS.f;
+ if (getSymbols) for (var key, symbols = getSymbols(it), isEnum = pIE.f, i = 0; symbols.length > i; ) isEnum.call(it, key = symbols[i++]) && result.push(key);
+ return result;
+ };
+ }, /* 72 */
+ /***/
+ function(module, exports) {
+ exports.f = Object.getOwnPropertySymbols;
+ }, /* 73 */
+ /***/
+ function(module, exports) {
+ exports.f = {}.propertyIsEnumerable;
+ }, /* 74 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ // 7.2.2 IsArray(argument)
+ var cof = __webpack_require__(52);
+ module.exports = Array.isArray || function(arg) {
+ return "Array" == cof(arg);
+ };
+ }, /* 75 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ // fallback for IE11 buggy Object.getOwnPropertyNames with iframe and window
+ var toIObject = __webpack_require__(50), gOPN = __webpack_require__(76).f, toString = {}.toString, windowNames = "object" == typeof window && window && Object.getOwnPropertyNames ? Object.getOwnPropertyNames(window) : [], getWindowNames = function(it) {
+ try {
+ return gOPN(it);
+ } catch (e) {
+ return windowNames.slice();
+ }
+ };
+ module.exports.f = function(it) {
+ return windowNames && "[object Window]" == toString.call(it) ? getWindowNames(it) : gOPN(toIObject(it));
+ };
+ }, /* 76 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ // 19.1.2.7 / 15.2.3.4 Object.getOwnPropertyNames(O)
+ var $keys = __webpack_require__(49), hiddenKeys = __webpack_require__(56).concat("length", "prototype");
+ exports.f = Object.getOwnPropertyNames || function(O) {
+ return $keys(O, hiddenKeys);
+ };
+ }, /* 77 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ var pIE = __webpack_require__(73), createDesc = __webpack_require__(28), toIObject = __webpack_require__(50), toPrimitive = __webpack_require__(27), has = __webpack_require__(9), IE8_DOM_DEFINE = __webpack_require__(23), gOPD = Object.getOwnPropertyDescriptor;
+ exports.f = __webpack_require__(24) ? gOPD : function(O, P) {
+ if (O = toIObject(O), P = toPrimitive(P, !0), IE8_DOM_DEFINE) try {
+ return gOPD(O, P);
+ } catch (e) {}
+ if (has(O, P)) return createDesc(!pIE.f.call(O, P), O[P]);
+ };
+ }, /* 78 */
+ /***/
+ function(module, exports) {}, /* 79 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ __webpack_require__(69)("asyncIterator");
+ }, /* 80 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ __webpack_require__(69)("observable");
+ }, /* 81 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ exports.__esModule = !0;
+ var _setPrototypeOf = __webpack_require__(82), _setPrototypeOf2 = _interopRequireDefault(_setPrototypeOf), _create = __webpack_require__(86), _create2 = _interopRequireDefault(_create), _typeof2 = __webpack_require__(35), _typeof3 = _interopRequireDefault(_typeof2);
+ exports.default = function(subClass, superClass) {
+ if ("function" != typeof superClass && null !== superClass) throw new TypeError("Super expression must either be null or a function, not " + ("undefined" == typeof superClass ? "undefined" : (0,
+ _typeof3.default)(superClass)));
+ subClass.prototype = (0, _create2.default)(superClass && superClass.prototype, {
+ constructor: {
+ value: subClass,
+ enumerable: !1,
+ writable: !0,
+ configurable: !0
+ }
+ }), superClass && (_setPrototypeOf2.default ? (0, _setPrototypeOf2.default)(subClass, superClass) : subClass.__proto__ = superClass);
+ };
+ }, /* 82 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ module.exports = {
+ default: __webpack_require__(83),
+ __esModule: !0
+ };
+ }, /* 83 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ __webpack_require__(84), module.exports = __webpack_require__(16).Object.setPrototypeOf;
+ }, /* 84 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ // 19.1.3.19 Object.setPrototypeOf(O, proto)
+ var $export = __webpack_require__(15);
+ $export($export.S, "Object", {
+ setPrototypeOf: __webpack_require__(85).set
+ });
+ }, /* 85 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ // Works with __proto__ only. Old v8 can't work with null proto objects.
+ /* eslint-disable no-proto */
+ var isObject = __webpack_require__(22), anObject = __webpack_require__(21), check = function(O, proto) {
+ if (anObject(O), !isObject(proto) && null !== proto) throw TypeError(proto + ": can't set as prototype!");
+ };
+ module.exports = {
+ set: Object.setPrototypeOf || ("__proto__" in {} ? // eslint-disable-line
+ function(test, buggy, set) {
+ try {
+ set = __webpack_require__(17)(Function.call, __webpack_require__(77).f(Object.prototype, "__proto__").set, 2),
+ set(test, []), buggy = !(test instanceof Array);
+ } catch (e) {
+ buggy = !0;
+ }
+ return function(O, proto) {
+ return check(O, proto), buggy ? O.__proto__ = proto : set(O, proto), O;
+ };
+ }({}, !1) : void 0),
+ check: check
+ };
+ }, /* 86 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ module.exports = {
+ default: __webpack_require__(87),
+ __esModule: !0
+ };
+ }, /* 87 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ __webpack_require__(88);
+ var $Object = __webpack_require__(16).Object;
+ module.exports = function(P, D) {
+ return $Object.create(P, D);
+ };
+ }, /* 88 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ var $export = __webpack_require__(15);
+ // 19.1.2.2 / 15.2.3.5 Object.create(O [, Properties])
+ $export($export.S, "Object", {
+ create: __webpack_require__(46)
+ });
+ }, /* 89 */
+ /***/
+ function(module, exports) {
+ module.exports = __WEBPACK_EXTERNAL_MODULE_89__;
+ }, /* 90 */
+ /***/
+ function(module, exports) {
+ module.exports = __WEBPACK_EXTERNAL_MODULE_90__;
+ }, /* 91 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ }), exports.AutoSizer = exports.default = void 0;
+ var _AutoSizer2 = __webpack_require__(92), _AutoSizer3 = _interopRequireDefault(_AutoSizer2);
+ exports.default = _AutoSizer3.default, exports.AutoSizer = _AutoSizer3.default;
+ }, /* 92 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ });
+ var _getPrototypeOf = __webpack_require__(3), _getPrototypeOf2 = _interopRequireDefault(_getPrototypeOf), _classCallCheck2 = __webpack_require__(29), _classCallCheck3 = _interopRequireDefault(_classCallCheck2), _createClass2 = __webpack_require__(30), _createClass3 = _interopRequireDefault(_createClass2), _possibleConstructorReturn2 = __webpack_require__(34), _possibleConstructorReturn3 = _interopRequireDefault(_possibleConstructorReturn2), _inherits2 = __webpack_require__(81), _inherits3 = _interopRequireDefault(_inherits2), _react = __webpack_require__(89), _react2 = _interopRequireDefault(_react), _reactAddonsShallowCompare = __webpack_require__(90), _reactAddonsShallowCompare2 = _interopRequireDefault(_reactAddonsShallowCompare), _detectElementResize = __webpack_require__(93), _detectElementResize2 = _interopRequireDefault(_detectElementResize), AutoSizer = function(_Component) {
+ function AutoSizer(props) {
+ (0, _classCallCheck3.default)(this, AutoSizer);
+ var _this = (0, _possibleConstructorReturn3.default)(this, (AutoSizer.__proto__ || (0,
+ _getPrototypeOf2.default)(AutoSizer)).call(this, props));
+ return _this.state = {
+ height: 0,
+ width: 0
+ }, _this._onResize = _this._onResize.bind(_this), _this._setRef = _this._setRef.bind(_this),
+ _this;
+ }
+ return (0, _inherits3.default)(AutoSizer, _Component), (0, _createClass3.default)(AutoSizer, [ {
+ key: "componentDidMount",
+ value: function() {
+ this._parentNode = this._autoSizer.parentNode, this._detectElementResize = (0, _detectElementResize2.default)(),
+ this._detectElementResize.addResizeListener(this._parentNode, this._onResize), this._onResize();
+ }
+ }, {
+ key: "componentWillUnmount",
+ value: function() {
+ this._detectElementResize && this._detectElementResize.removeResizeListener(this._parentNode, this._onResize);
+ }
+ }, {
+ key: "render",
+ value: function() {
+ var _props = this.props, children = _props.children, disableHeight = _props.disableHeight, disableWidth = _props.disableWidth, _state = this.state, height = _state.height, width = _state.width, outerStyle = {
+ overflow: "visible"
+ };
+ return disableHeight || (outerStyle.height = 0), disableWidth || (outerStyle.width = 0),
+ _react2.default.createElement("div", {
+ ref: this._setRef,
+ style: outerStyle
+ }, children({
+ height: height,
+ width: width
+ }));
+ }
+ }, {
+ key: "shouldComponentUpdate",
+ value: function(nextProps, nextState) {
+ return (0, _reactAddonsShallowCompare2.default)(this, nextProps, nextState);
+ }
+ }, {
+ key: "_onResize",
+ value: function() {
+ var onResize = this.props.onResize, boundingRect = this._parentNode.getBoundingClientRect(), height = boundingRect.height || 0, width = boundingRect.width || 0, style = window.getComputedStyle(this._parentNode), paddingLeft = parseInt(style.paddingLeft, 10) || 0, paddingRight = parseInt(style.paddingRight, 10) || 0, paddingTop = parseInt(style.paddingTop, 10) || 0, paddingBottom = parseInt(style.paddingBottom, 10) || 0;
+ this.setState({
+ height: height - paddingTop - paddingBottom,
+ width: width - paddingLeft - paddingRight
+ }), onResize({
+ height: height,
+ width: width
+ });
+ }
+ }, {
+ key: "_setRef",
+ value: function(autoSizer) {
+ this._autoSizer = autoSizer;
+ }
+ } ]), AutoSizer;
+ }(_react.Component);
+ AutoSizer.defaultProps = {
+ onResize: function() {}
+ }, exports.default = AutoSizer;
+ }, /* 93 */
+ /***/
+ function(module, exports) {
+ "use strict";
+ function createDetectElementResize() {
+ var _window;
+ _window = "undefined" != typeof window ? window : "undefined" != typeof self ? self : this;
+ var attachEvent = "undefined" != typeof document && document.attachEvent, stylesCreated = !1;
+ if (!attachEvent) {
+ var requestFrame = function() {
+ var raf = _window.requestAnimationFrame || _window.mozRequestAnimationFrame || _window.webkitRequestAnimationFrame || function(fn) {
+ return _window.setTimeout(fn, 20);
+ };
+ return function(fn) {
+ return raf(fn);
+ };
+ }(), cancelFrame = function() {
+ var cancel = _window.cancelAnimationFrame || _window.mozCancelAnimationFrame || _window.webkitCancelAnimationFrame || _window.clearTimeout;
+ return function(id) {
+ return cancel(id);
+ };
+ }(), resetTriggers = function(element) {
+ var triggers = element.__resizeTriggers__, expand = triggers.firstElementChild, contract = triggers.lastElementChild, expandChild = expand.firstElementChild;
+ contract.scrollLeft = contract.scrollWidth, contract.scrollTop = contract.scrollHeight,
+ expandChild.style.width = expand.offsetWidth + 1 + "px", expandChild.style.height = expand.offsetHeight + 1 + "px",
+ expand.scrollLeft = expand.scrollWidth, expand.scrollTop = expand.scrollHeight;
+ }, checkTriggers = function(element) {
+ return element.offsetWidth != element.__resizeLast__.width || element.offsetHeight != element.__resizeLast__.height;
+ }, scrollListener = function(e) {
+ if (!(e.target.className.indexOf("contract-trigger") < 0 && e.target.className.indexOf("expand-trigger") < 0)) {
+ var element = this;
+ resetTriggers(this), this.__resizeRAF__ && cancelFrame(this.__resizeRAF__), this.__resizeRAF__ = requestFrame(function() {
+ checkTriggers(element) && (element.__resizeLast__.width = element.offsetWidth, element.__resizeLast__.height = element.offsetHeight,
+ element.__resizeListeners__.forEach(function(fn) {
+ fn.call(element, e);
+ }));
+ });
+ }
+ }, animation = !1, animationstring = "animation", keyframeprefix = "", animationstartevent = "animationstart", domPrefixes = "Webkit Moz O ms".split(" "), startEvents = "webkitAnimationStart animationstart oAnimationStart MSAnimationStart".split(" "), pfx = "", elm = document.createElementNS("http://www.w3.org/1999/xhtml","fakeelement");
+ if (void 0 !== elm.style.animationName && (animation = !0), animation === !1) for (var i = 0; i < domPrefixes.length; i++) if (void 0 !== elm.style[domPrefixes[i] + "AnimationName"]) {
+ pfx = domPrefixes[i], animationstring = pfx + "Animation", keyframeprefix = "-" + pfx.toLowerCase() + "-",
+ animationstartevent = startEvents[i], animation = !0;
+ break;
+ }
+ var animationName = "resizeanim", animationKeyframes = "@" + keyframeprefix + "keyframes " + animationName + " { from { opacity: 0; } to { opacity: 0; } } ", animationStyle = keyframeprefix + "animation: 1ms " + animationName + "; ";
+ }
+ var createStyles = function() {
+ if (!stylesCreated) {
+ var css = (animationKeyframes ? animationKeyframes : "") + ".resize-triggers { " + (animationStyle ? animationStyle : "") + 'visibility: hidden; opacity: 0; } .resize-triggers, .resize-triggers > div, .contract-trigger:before { content: " "; display: block; position: absolute; top: 0; left: 0; height: 100%; width: 100%; overflow: hidden; z-index: -1; } .resize-triggers > div { background: #eee; overflow: auto; } .contract-trigger:before { width: 200%; height: 200%; }', head = document.firstElementChild || document.getElementsByTagName("head")[0], style = document.createElementNS("http://www.w3.org/1999/xhtml","style");
+ style.type = "text/css", style.styleSheet ? style.styleSheet.cssText = css : style.appendChild(document.createTextNode(css)),
+ head.appendChild(style), stylesCreated = !0;
+ }
+ }, addResizeListener = function(element, fn) {
+ attachEvent ? element.attachEvent("onresize", fn) : (element.__resizeTriggers__ || ("static" == _window.getComputedStyle(element).position && (element.style.position = "relative"),
+ createStyles(), element.__resizeLast__ = {}, element.__resizeListeners__ = [], (element.__resizeTriggers__ = document.createElementNS("http://www.w3.org/1999/xhtml","div")).className = "resize-triggers",
+ element.__resizeTriggers__.innerHTML = '<div class="expand-trigger"><div></div></div><div class="contract-trigger"></div>',
+ element.appendChild(element.__resizeTriggers__), resetTriggers(element), element.addEventListener("scroll", scrollListener, !0),
+ animationstartevent && (element.__resizeTriggers__.__animationListener__ = function(e) {
+ e.animationName == animationName && resetTriggers(element);
+ }, element.__resizeTriggers__.addEventListener(animationstartevent, element.__resizeTriggers__.__animationListener__))),
+ element.__resizeListeners__.push(fn));
+ }, removeResizeListener = function(element, fn) {
+ attachEvent ? element.detachEvent("onresize", fn) : (element.__resizeListeners__.splice(element.__resizeListeners__.indexOf(fn), 1),
+ element.__resizeListeners__.length || (element.removeEventListener("scroll", scrollListener, !0),
+ element.__resizeTriggers__.__animationListener__ && (element.__resizeTriggers__.removeEventListener(animationstartevent, element.__resizeTriggers__.__animationListener__),
+ element.__resizeTriggers__.__animationListener__ = null), element.__resizeTriggers__ = !element.removeChild(element.__resizeTriggers__)));
+ };
+ return {
+ addResizeListener: addResizeListener,
+ removeResizeListener: removeResizeListener
+ };
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ }), exports.default = createDetectElementResize;
+ }, /* 94 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ }), exports.defaultCellSizeCache = exports.CellMeasurer = exports.default = void 0;
+ var _CellMeasurer2 = __webpack_require__(95), _CellMeasurer3 = _interopRequireDefault(_CellMeasurer2), _defaultCellSizeCache2 = __webpack_require__(97), _defaultCellSizeCache3 = _interopRequireDefault(_defaultCellSizeCache2);
+ exports.default = _CellMeasurer3.default, exports.CellMeasurer = _CellMeasurer3.default,
+ exports.defaultCellSizeCache = _defaultCellSizeCache3.default;
+ }, /* 95 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ });
+ var _getPrototypeOf = __webpack_require__(3), _getPrototypeOf2 = _interopRequireDefault(_getPrototypeOf), _classCallCheck2 = __webpack_require__(29), _classCallCheck3 = _interopRequireDefault(_classCallCheck2), _createClass2 = __webpack_require__(30), _createClass3 = _interopRequireDefault(_createClass2), _possibleConstructorReturn2 = __webpack_require__(34), _possibleConstructorReturn3 = _interopRequireDefault(_possibleConstructorReturn2), _inherits2 = __webpack_require__(81), _inherits3 = _interopRequireDefault(_inherits2), _react = __webpack_require__(89), _reactAddonsShallowCompare = (_interopRequireDefault(_react),
+ __webpack_require__(90)), _reactAddonsShallowCompare2 = _interopRequireDefault(_reactAddonsShallowCompare), _reactDom = __webpack_require__(96), _reactDom2 = _interopRequireDefault(_reactDom), _defaultCellSizeCache = __webpack_require__(97), _defaultCellSizeCache2 = _interopRequireDefault(_defaultCellSizeCache), CellMeasurer = function(_Component) {
+ function CellMeasurer(props, state) {
+ (0, _classCallCheck3.default)(this, CellMeasurer);
+ var _this = (0, _possibleConstructorReturn3.default)(this, (CellMeasurer.__proto__ || (0,
+ _getPrototypeOf2.default)(CellMeasurer)).call(this, props, state));
+ return _this._cellSizeCache = props.cellSizeCache || new _defaultCellSizeCache2.default(),
+ _this.getColumnWidth = _this.getColumnWidth.bind(_this), _this.getRowHeight = _this.getRowHeight.bind(_this),
+ _this.resetMeasurements = _this.resetMeasurements.bind(_this), _this.resetMeasurementForColumn = _this.resetMeasurementForColumn.bind(_this),
+ _this.resetMeasurementForRow = _this.resetMeasurementForRow.bind(_this), _this;
+ }
+ return (0, _inherits3.default)(CellMeasurer, _Component), (0, _createClass3.default)(CellMeasurer, [ {
+ key: "getColumnWidth",
+ value: function(_ref) {
+ var index = _ref.index;
+ if (this._cellSizeCache.hasColumnWidth(index)) return this._cellSizeCache.getColumnWidth(index);
+ for (var rowCount = this.props.rowCount, maxWidth = 0, rowIndex = 0; rowIndex < rowCount; rowIndex++) {
+ var _measureCell2 = this._measureCell({
+ clientWidth: !0,
+ columnIndex: index,
+ rowIndex: rowIndex
+ }), width = _measureCell2.width;
+ maxWidth = Math.max(maxWidth, width);
+ }
+ return this._cellSizeCache.setColumnWidth(index, maxWidth), maxWidth;
+ }
+ }, {
+ key: "getRowHeight",
+ value: function(_ref2) {
+ var index = _ref2.index;
+ if (this._cellSizeCache.hasRowHeight(index)) return this._cellSizeCache.getRowHeight(index);
+ for (var columnCount = this.props.columnCount, maxHeight = 0, columnIndex = 0; columnIndex < columnCount; columnIndex++) {
+ var _measureCell3 = this._measureCell({
+ clientHeight: !0,
+ columnIndex: columnIndex,
+ rowIndex: index
+ }), height = _measureCell3.height;
+ maxHeight = Math.max(maxHeight, height);
+ }
+ return this._cellSizeCache.setRowHeight(index, maxHeight), maxHeight;
+ }
+ }, {
+ key: "resetMeasurementForColumn",
+ value: function(columnIndex) {
+ this._cellSizeCache.clearColumnWidth(columnIndex);
+ }
+ }, {
+ key: "resetMeasurementForRow",
+ value: function(rowIndex) {
+ this._cellSizeCache.clearRowHeight(rowIndex);
+ }
+ }, {
+ key: "resetMeasurements",
+ value: function() {
+ this._cellSizeCache.clearAllColumnWidths(), this._cellSizeCache.clearAllRowHeights();
+ }
+ }, {
+ key: "componentDidMount",
+ value: function() {
+ this._renderAndMount();
+ }
+ }, {
+ key: "componentWillReceiveProps",
+ value: function(nextProps) {
+ var cellSizeCache = this.props.cellSizeCache;
+ cellSizeCache !== nextProps.cellSizeCache && (this._cellSizeCache = nextProps.cellSizeCache),
+ this._updateDivDimensions(nextProps);
+ }
+ }, {
+ key: "componentWillUnmount",
+ value: function() {
+ this._unmountContainer();
+ }
+ }, {
+ key: "render",
+ value: function() {
+ var children = this.props.children;
+ return children({
+ getColumnWidth: this.getColumnWidth,
+ getRowHeight: this.getRowHeight,
+ resetMeasurements: this.resetMeasurements,
+ resetMeasurementForColumn: this.resetMeasurementForColumn,
+ resetMeasurementForRow: this.resetMeasurementForRow
+ });
+ }
+ }, {
+ key: "shouldComponentUpdate",
+ value: function(nextProps, nextState) {
+ return (0, _reactAddonsShallowCompare2.default)(this, nextProps, nextState);
+ }
+ }, {
+ key: "_getContainerNode",
+ value: function(props) {
+ var container = props.container;
+ return container ? _reactDom2.default.findDOMNode("function" == typeof container ? container() : container) : document.firstElementChild;
+ }
+ }, {
+ key: "_measureCell",
+ value: function(_ref3) {
+ var _ref3$clientHeight = _ref3.clientHeight, clientHeight = void 0 !== _ref3$clientHeight && _ref3$clientHeight, _ref3$clientWidth = _ref3.clientWidth, clientWidth = void 0 === _ref3$clientWidth || _ref3$clientWidth, columnIndex = _ref3.columnIndex, rowIndex = _ref3.rowIndex, cellRenderer = this.props.cellRenderer, rendered = cellRenderer({
+ columnIndex: columnIndex,
+ rowIndex: rowIndex
+ });
+ this._renderAndMount(), _reactDom2.default.unstable_renderSubtreeIntoContainer(this, rendered, this._div);
+ var measurements = {
+ height: clientHeight && this._div.clientHeight,
+ width: clientWidth && this._div.clientWidth
+ };
+ return _reactDom2.default.unmountComponentAtNode(this._div), measurements;
+ }
+ }, {
+ key: "_renderAndMount",
+ value: function() {
+ this._div || (this._div = document.createElementNS("http://www.w3.org/1999/xhtml","div"), this._div.style.display = "inline-block",
+ this._div.style.position = "absolute", this._div.style.visibility = "hidden", this._div.style.zIndex = -1,
+ this._updateDivDimensions(this.props), this._containerNode = this._getContainerNode(this.props),
+ this._containerNode.appendChild(this._div));
+ }
+ }, {
+ key: "_unmountContainer",
+ value: function() {
+ this._div && (this._containerNode.removeChild(this._div), this._div = null), this._containerNode = null;
+ }
+ }, {
+ key: "_updateDivDimensions",
+ value: function(props) {
+ var height = props.height, width = props.width;
+ height && height !== this._divHeight && (this._divHeight = height, this._div.style.height = height + "px"),
+ width && width !== this._divWidth && (this._divWidth = width, this._div.style.width = width + "px");
+ }
+ } ]), CellMeasurer;
+ }(_react.Component);
+ exports.default = CellMeasurer;
+ }, /* 96 */
+ /***/
+ function(module, exports) {
+ module.exports = __WEBPACK_EXTERNAL_MODULE_96__;
+ }, /* 97 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ });
+ var _classCallCheck2 = __webpack_require__(29), _classCallCheck3 = _interopRequireDefault(_classCallCheck2), _createClass2 = __webpack_require__(30), _createClass3 = _interopRequireDefault(_createClass2), CellSizeCache = function() {
+ function CellSizeCache() {
+ var _ref = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {}, _ref$uniformRowHeight = _ref.uniformRowHeight, uniformRowHeight = void 0 !== _ref$uniformRowHeight && _ref$uniformRowHeight, _ref$uniformColumnWid = _ref.uniformColumnWidth, uniformColumnWidth = void 0 !== _ref$uniformColumnWid && _ref$uniformColumnWid;
+ (0, _classCallCheck3.default)(this, CellSizeCache), this._uniformRowHeight = uniformRowHeight,
+ this._uniformColumnWidth = uniformColumnWidth, this._cachedColumnWidths = {}, this._cachedRowHeights = {};
+ }
+ return (0, _createClass3.default)(CellSizeCache, [ {
+ key: "clearAllColumnWidths",
+ value: function() {
+ this._cachedColumnWidth = void 0, this._cachedColumnWidths = {};
+ }
+ }, {
+ key: "clearAllRowHeights",
+ value: function() {
+ this._cachedRowHeight = void 0, this._cachedRowHeights = {};
+ }
+ }, {
+ key: "clearColumnWidth",
+ value: function(index) {
+ this._cachedColumnWidth = void 0, delete this._cachedColumnWidths[index];
+ }
+ }, {
+ key: "clearRowHeight",
+ value: function(index) {
+ this._cachedRowHeight = void 0, delete this._cachedRowHeights[index];
+ }
+ }, {
+ key: "getColumnWidth",
+ value: function(index) {
+ return this._uniformColumnWidth ? this._cachedColumnWidth : this._cachedColumnWidths[index];
+ }
+ }, {
+ key: "getRowHeight",
+ value: function(index) {
+ return this._uniformRowHeight ? this._cachedRowHeight : this._cachedRowHeights[index];
+ }
+ }, {
+ key: "hasColumnWidth",
+ value: function(index) {
+ return this._uniformColumnWidth ? !!this._cachedColumnWidth : !!this._cachedColumnWidths[index];
+ }
+ }, {
+ key: "hasRowHeight",
+ value: function(index) {
+ return this._uniformRowHeight ? !!this._cachedRowHeight : !!this._cachedRowHeights[index];
+ }
+ }, {
+ key: "setColumnWidth",
+ value: function(index, width) {
+ this._cachedColumnWidth = width, this._cachedColumnWidths[index] = width;
+ }
+ }, {
+ key: "setRowHeight",
+ value: function(index, height) {
+ this._cachedRowHeight = height, this._cachedRowHeights[index] = height;
+ }
+ } ]), CellSizeCache;
+ }();
+ exports.default = CellSizeCache;
+ }, /* 98 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ }), exports.Collection = exports.default = void 0;
+ var _Collection2 = __webpack_require__(99), _Collection3 = _interopRequireDefault(_Collection2);
+ exports.default = _Collection3.default, exports.Collection = _Collection3.default;
+ }, /* 99 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ function defaultCellGroupRenderer(_ref5) {
+ var cellCache = _ref5.cellCache, cellRenderer = _ref5.cellRenderer, cellSizeAndPositionGetter = _ref5.cellSizeAndPositionGetter, indices = _ref5.indices, isScrolling = _ref5.isScrolling;
+ return indices.map(function(index) {
+ var cellMetadata = cellSizeAndPositionGetter({
+ index: index
+ }), cellRendererProps = {
+ index: index,
+ isScrolling: isScrolling,
+ key: index,
+ style: {
+ height: cellMetadata.height,
+ left: cellMetadata.x,
+ position: "absolute",
+ top: cellMetadata.y,
+ width: cellMetadata.width
+ }
+ };
+ return isScrolling ? (index in cellCache || (cellCache[index] = cellRenderer(cellRendererProps)),
+ cellCache[index]) : cellRenderer(cellRendererProps);
+ }).filter(function(renderedCell) {
+ return !!renderedCell;
+ });
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ });
+ var _extends2 = __webpack_require__(100), _extends3 = _interopRequireDefault(_extends2), _objectWithoutProperties2 = __webpack_require__(105), _objectWithoutProperties3 = _interopRequireDefault(_objectWithoutProperties2), _getPrototypeOf = __webpack_require__(3), _getPrototypeOf2 = _interopRequireDefault(_getPrototypeOf), _classCallCheck2 = __webpack_require__(29), _classCallCheck3 = _interopRequireDefault(_classCallCheck2), _createClass2 = __webpack_require__(30), _createClass3 = _interopRequireDefault(_createClass2), _possibleConstructorReturn2 = __webpack_require__(34), _possibleConstructorReturn3 = _interopRequireDefault(_possibleConstructorReturn2), _inherits2 = __webpack_require__(81), _inherits3 = _interopRequireDefault(_inherits2), _react = __webpack_require__(89), _react2 = _interopRequireDefault(_react), _CollectionView = __webpack_require__(106), _CollectionView2 = _interopRequireDefault(_CollectionView), _calculateSizeAndPositionData2 = __webpack_require__(114), _calculateSizeAndPositionData3 = _interopRequireDefault(_calculateSizeAndPositionData2), _getUpdatedOffsetForIndex = __webpack_require__(117), _getUpdatedOffsetForIndex2 = _interopRequireDefault(_getUpdatedOffsetForIndex), _reactAddonsShallowCompare = __webpack_require__(90), _reactAddonsShallowCompare2 = _interopRequireDefault(_reactAddonsShallowCompare), Collection = function(_Component) {
+ function Collection(props, context) {
+ (0, _classCallCheck3.default)(this, Collection);
+ var _this = (0, _possibleConstructorReturn3.default)(this, (Collection.__proto__ || (0,
+ _getPrototypeOf2.default)(Collection)).call(this, props, context));
+ return _this._cellMetadata = [], _this._lastRenderedCellIndices = [], _this._cellCache = [],
+ _this._isScrollingChange = _this._isScrollingChange.bind(_this), _this;
+ }
+ return (0, _inherits3.default)(Collection, _Component), (0, _createClass3.default)(Collection, [ {
+ key: "recomputeCellSizesAndPositions",
+ value: function() {
+ this._cellCache = [], this._collectionView.recomputeCellSizesAndPositions();
+ }
+ }, {
+ key: "render",
+ value: function() {
+ var _this2 = this, props = (0, _objectWithoutProperties3.default)(this.props, []);
+ return _react2.default.createElement(_CollectionView2.default, (0, _extends3.default)({
+ cellLayoutManager: this,
+ isScrollingChange: this._isScrollingChange,
+ ref: function(_ref) {
+ _this2._collectionView = _ref;
+ }
+ }, props));
+ }
+ }, {
+ key: "shouldComponentUpdate",
+ value: function(nextProps, nextState) {
+ return (0, _reactAddonsShallowCompare2.default)(this, nextProps, nextState);
+ }
+ }, {
+ key: "calculateSizeAndPositionData",
+ value: function() {
+ var _props = this.props, cellCount = _props.cellCount, cellSizeAndPositionGetter = _props.cellSizeAndPositionGetter, sectionSize = _props.sectionSize, data = (0,
+ _calculateSizeAndPositionData3.default)({
+ cellCount: cellCount,
+ cellSizeAndPositionGetter: cellSizeAndPositionGetter,
+ sectionSize: sectionSize
+ });
+ this._cellMetadata = data.cellMetadata, this._sectionManager = data.sectionManager,
+ this._height = data.height, this._width = data.width;
+ }
+ }, {
+ key: "getLastRenderedIndices",
+ value: function() {
+ return this._lastRenderedCellIndices;
+ }
+ }, {
+ key: "getScrollPositionForCell",
+ value: function(_ref2) {
+ var align = _ref2.align, cellIndex = _ref2.cellIndex, height = _ref2.height, scrollLeft = _ref2.scrollLeft, scrollTop = _ref2.scrollTop, width = _ref2.width, cellCount = this.props.cellCount;
+ if (cellIndex >= 0 && cellIndex < cellCount) {
+ var cellMetadata = this._cellMetadata[cellIndex];
+ scrollLeft = (0, _getUpdatedOffsetForIndex2.default)({
+ align: align,
+ cellOffset: cellMetadata.x,
+ cellSize: cellMetadata.width,
+ containerSize: width,
+ currentOffset: scrollLeft,
+ targetIndex: cellIndex
+ }), scrollTop = (0, _getUpdatedOffsetForIndex2.default)({
+ align: align,
+ cellOffset: cellMetadata.y,
+ cellSize: cellMetadata.height,
+ containerSize: height,
+ currentOffset: scrollTop,
+ targetIndex: cellIndex
+ });
+ }
+ return {
+ scrollLeft: scrollLeft,
+ scrollTop: scrollTop
+ };
+ }
+ }, {
+ key: "getTotalSize",
+ value: function() {
+ return {
+ height: this._height,
+ width: this._width
+ };
+ }
+ }, {
+ key: "cellRenderers",
+ value: function(_ref3) {
+ var _this3 = this, height = _ref3.height, isScrolling = _ref3.isScrolling, width = _ref3.width, x = _ref3.x, y = _ref3.y, _props2 = this.props, cellGroupRenderer = _props2.cellGroupRenderer, cellRenderer = _props2.cellRenderer;
+ return this._lastRenderedCellIndices = this._sectionManager.getCellIndices({
+ height: height,
+ width: width,
+ x: x,
+ y: y
+ }), cellGroupRenderer({
+ cellCache: this._cellCache,
+ cellRenderer: cellRenderer,
+ cellSizeAndPositionGetter: function(_ref4) {
+ var index = _ref4.index;
+ return _this3._sectionManager.getCellMetadata({
+ index: index
+ });
+ },
+ indices: this._lastRenderedCellIndices,
+ isScrolling: isScrolling
+ });
+ }
+ }, {
+ key: "_isScrollingChange",
+ value: function(isScrolling) {
+ isScrolling || (this._cellCache = []);
+ }
+ } ]), Collection;
+ }(_react.Component);
+ Collection.defaultProps = {
+ "aria-label": "grid",
+ cellGroupRenderer: defaultCellGroupRenderer
+ }, exports.default = Collection;
+ }, /* 100 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ exports.__esModule = !0;
+ var _assign = __webpack_require__(101), _assign2 = _interopRequireDefault(_assign);
+ exports.default = _assign2.default || function(target) {
+ for (var i = 1; i < arguments.length; i++) {
+ var source = arguments[i];
+ for (var key in source) Object.prototype.hasOwnProperty.call(source, key) && (target[key] = source[key]);
+ }
+ return target;
+ };
+ }, /* 101 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ module.exports = {
+ default: __webpack_require__(102),
+ __esModule: !0
+ };
+ }, /* 102 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ __webpack_require__(103), module.exports = __webpack_require__(16).Object.assign;
+ }, /* 103 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ // 19.1.3.1 Object.assign(target, source)
+ var $export = __webpack_require__(15);
+ $export($export.S + $export.F, "Object", {
+ assign: __webpack_require__(104)
+ });
+ }, /* 104 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ // 19.1.2.1 Object.assign(target, source, ...)
+ var getKeys = __webpack_require__(48), gOPS = __webpack_require__(72), pIE = __webpack_require__(73), toObject = __webpack_require__(6), IObject = __webpack_require__(51), $assign = Object.assign;
+ // should work with symbols and should have deterministic property order (V8 bug)
+ module.exports = !$assign || __webpack_require__(25)(function() {
+ var A = {}, B = {}, S = Symbol(), K = "abcdefghijklmnopqrst";
+ return A[S] = 7, K.split("").forEach(function(k) {
+ B[k] = k;
+ }), 7 != $assign({}, A)[S] || Object.keys($assign({}, B)).join("") != K;
+ }) ? function(target, source) {
+ for (// eslint-disable-line no-unused-vars
+ var T = toObject(target), aLen = arguments.length, index = 1, getSymbols = gOPS.f, isEnum = pIE.f; aLen > index; ) for (var key, S = IObject(arguments[index++]), keys = getSymbols ? getKeys(S).concat(getSymbols(S)) : getKeys(S), length = keys.length, j = 0; length > j; ) isEnum.call(S, key = keys[j++]) && (T[key] = S[key]);
+ return T;
+ } : $assign;
+ }, /* 105 */
+ /***/
+ function(module, exports) {
+ "use strict";
+ exports.__esModule = !0, exports.default = function(obj, keys) {
+ var target = {};
+ for (var i in obj) keys.indexOf(i) >= 0 || Object.prototype.hasOwnProperty.call(obj, i) && (target[i] = obj[i]);
+ return target;
+ };
+ }, /* 106 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ });
+ var _extends2 = __webpack_require__(100), _extends3 = _interopRequireDefault(_extends2), _getPrototypeOf = __webpack_require__(3), _getPrototypeOf2 = _interopRequireDefault(_getPrototypeOf), _classCallCheck2 = __webpack_require__(29), _classCallCheck3 = _interopRequireDefault(_classCallCheck2), _createClass2 = __webpack_require__(30), _createClass3 = _interopRequireDefault(_createClass2), _possibleConstructorReturn2 = __webpack_require__(34), _possibleConstructorReturn3 = _interopRequireDefault(_possibleConstructorReturn2), _inherits2 = __webpack_require__(81), _inherits3 = _interopRequireDefault(_inherits2), _react = __webpack_require__(89), _react2 = _interopRequireDefault(_react), _classnames = __webpack_require__(107), _classnames2 = _interopRequireDefault(_classnames), _createCallbackMemoizer = __webpack_require__(108), _createCallbackMemoizer2 = _interopRequireDefault(_createCallbackMemoizer), _scrollbarSize = __webpack_require__(112), _scrollbarSize2 = _interopRequireDefault(_scrollbarSize), _reactAddonsShallowCompare = __webpack_require__(90), _reactAddonsShallowCompare2 = _interopRequireDefault(_reactAddonsShallowCompare), IS_SCROLLING_TIMEOUT = 150, SCROLL_POSITION_CHANGE_REASONS = {
+ OBSERVED: "observed",
+ REQUESTED: "requested"
+ }, CollectionView = function(_Component) {
+ function CollectionView(props, context) {
+ (0, _classCallCheck3.default)(this, CollectionView);
+ var _this = (0, _possibleConstructorReturn3.default)(this, (CollectionView.__proto__ || (0,
+ _getPrototypeOf2.default)(CollectionView)).call(this, props, context));
+ return _this.state = {
+ calculateSizeAndPositionDataOnNextUpdate: !1,
+ isScrolling: !1,
+ scrollLeft: 0,
+ scrollTop: 0
+ }, _this._onSectionRenderedMemoizer = (0, _createCallbackMemoizer2.default)(), _this._onScrollMemoizer = (0,
+ _createCallbackMemoizer2.default)(!1), _this._invokeOnSectionRenderedHelper = _this._invokeOnSectionRenderedHelper.bind(_this),
+ _this._onScroll = _this._onScroll.bind(_this), _this._updateScrollPositionForScrollToCell = _this._updateScrollPositionForScrollToCell.bind(_this),
+ _this;
+ }
+ return (0, _inherits3.default)(CollectionView, _Component), (0, _createClass3.default)(CollectionView, [ {
+ key: "recomputeCellSizesAndPositions",
+ value: function() {
+ this.setState({
+ calculateSizeAndPositionDataOnNextUpdate: !0
+ });
+ }
+ }, {
+ key: "componentDidMount",
+ value: function() {
+ var _props = this.props, cellLayoutManager = _props.cellLayoutManager, scrollLeft = _props.scrollLeft, scrollToCell = _props.scrollToCell, scrollTop = _props.scrollTop;
+ this._scrollbarSizeMeasured || (this._scrollbarSize = (0, _scrollbarSize2.default)(),
+ this._scrollbarSizeMeasured = !0, this.setState({})), scrollToCell >= 0 ? this._updateScrollPositionForScrollToCell() : (scrollLeft >= 0 || scrollTop >= 0) && this._setScrollPosition({
+ scrollLeft: scrollLeft,
+ scrollTop: scrollTop
+ }), this._invokeOnSectionRenderedHelper();
+ var _cellLayoutManager$ge = cellLayoutManager.getTotalSize(), totalHeight = _cellLayoutManager$ge.height, totalWidth = _cellLayoutManager$ge.width;
+ this._invokeOnScrollMemoizer({
+ scrollLeft: scrollLeft || 0,
+ scrollTop: scrollTop || 0,
+ totalHeight: totalHeight,
+ totalWidth: totalWidth
+ });
+ }
+ }, {
+ key: "componentDidUpdate",
+ value: function(prevProps, prevState) {
+ var _props2 = this.props, height = _props2.height, scrollToAlignment = _props2.scrollToAlignment, scrollToCell = _props2.scrollToCell, width = _props2.width, _state = this.state, scrollLeft = _state.scrollLeft, scrollPositionChangeReason = _state.scrollPositionChangeReason, scrollTop = _state.scrollTop;
+ scrollPositionChangeReason === SCROLL_POSITION_CHANGE_REASONS.REQUESTED && (scrollLeft >= 0 && scrollLeft !== prevState.scrollLeft && scrollLeft !== this._scrollingContainer.scrollLeft && (this._scrollingContainer.scrollLeft = scrollLeft),
+ scrollTop >= 0 && scrollTop !== prevState.scrollTop && scrollTop !== this._scrollingContainer.scrollTop && (this._scrollingContainer.scrollTop = scrollTop)),
+ height === prevProps.height && scrollToAlignment === prevProps.scrollToAlignment && scrollToCell === prevProps.scrollToCell && width === prevProps.width || this._updateScrollPositionForScrollToCell(),
+ this._invokeOnSectionRenderedHelper();
+ }
+ }, {
+ key: "componentWillMount",
+ value: function() {
+ var cellLayoutManager = this.props.cellLayoutManager;
+ cellLayoutManager.calculateSizeAndPositionData(), this._scrollbarSize = (0, _scrollbarSize2.default)(),
+ void 0 === this._scrollbarSize ? (this._scrollbarSizeMeasured = !1, this._scrollbarSize = 0) : this._scrollbarSizeMeasured = !0;
+ }
+ }, {
+ key: "componentWillUnmount",
+ value: function() {
+ this._disablePointerEventsTimeoutId && clearTimeout(this._disablePointerEventsTimeoutId);
+ }
+ }, {
+ key: "componentWillUpdate",
+ value: function(nextProps, nextState) {
+ 0 !== nextProps.cellCount || 0 === nextState.scrollLeft && 0 === nextState.scrollTop ? nextProps.scrollLeft === this.props.scrollLeft && nextProps.scrollTop === this.props.scrollTop || this._setScrollPosition({
+ scrollLeft: nextProps.scrollLeft,
+ scrollTop: nextProps.scrollTop
+ }) : this._setScrollPosition({
+ scrollLeft: 0,
+ scrollTop: 0
+ }), (nextProps.cellCount !== this.props.cellCount || nextProps.cellLayoutManager !== this.props.cellLayoutManager || nextState.calculateSizeAndPositionDataOnNextUpdate) && nextProps.cellLayoutManager.calculateSizeAndPositionData(),
+ nextState.calculateSizeAndPositionDataOnNextUpdate && this.setState({
+ calculateSizeAndPositionDataOnNextUpdate: !1
+ });
+ }
+ }, {
+ key: "render",
+ value: function() {
+ var _this2 = this, _props3 = this.props, autoHeight = _props3.autoHeight, cellCount = _props3.cellCount, cellLayoutManager = _props3.cellLayoutManager, className = _props3.className, height = _props3.height, horizontalOverscanSize = _props3.horizontalOverscanSize, id = _props3.id, noContentRenderer = _props3.noContentRenderer, style = _props3.style, verticalOverscanSize = _props3.verticalOverscanSize, width = _props3.width, _state2 = this.state, isScrolling = _state2.isScrolling, scrollLeft = _state2.scrollLeft, scrollTop = _state2.scrollTop, _cellLayoutManager$ge2 = cellLayoutManager.getTotalSize(), totalHeight = _cellLayoutManager$ge2.height, totalWidth = _cellLayoutManager$ge2.width, left = Math.max(0, scrollLeft - horizontalOverscanSize), top = Math.max(0, scrollTop - verticalOverscanSize), right = Math.min(totalWidth, scrollLeft + width + horizontalOverscanSize), bottom = Math.min(totalHeight, scrollTop + height + verticalOverscanSize), childrenToDisplay = height > 0 && width > 0 ? cellLayoutManager.cellRenderers({
+ height: bottom - top,
+ isScrolling: isScrolling,
+ width: right - left,
+ x: left,
+ y: top
+ }) : [], collectionStyle = {
+ boxSizing: "border-box",
+ height: autoHeight ? "auto" : height,
+ overflow: "auto",
+ position: "relative",
+ WebkitOverflowScrolling: "touch",
+ width: width,
+ willChange: "transform"
+ }, verticalScrollBarSize = totalHeight > height ? this._scrollbarSize : 0, horizontalScrollBarSize = totalWidth > width ? this._scrollbarSize : 0;
+ return totalWidth + verticalScrollBarSize <= width && (collectionStyle.overflowX = "hidden"),
+ totalHeight + horizontalScrollBarSize <= height && (collectionStyle.overflowY = "hidden"),
+ _react2.default.createElement("div", {
+ ref: function(_ref) {
+ _this2._scrollingContainer = _ref;
+ },
+ "aria-label": this.props["aria-label"],
+ className: (0, _classnames2.default)("ReactVirtualized__Collection", className),
+ id: id,
+ onScroll: this._onScroll,
+ role: "grid",
+ style: (0, _extends3.default)({}, collectionStyle, style),
+ tabIndex: 0
+ }, cellCount > 0 && _react2.default.createElement("div", {
+ className: "ReactVirtualized__Collection__innerScrollContainer",
+ style: {
+ height: totalHeight,
+ maxHeight: totalHeight,
+ maxWidth: totalWidth,
+ overflow: "hidden",
+ pointerEvents: isScrolling ? "none" : "",
+ width: totalWidth
+ }
+ }, childrenToDisplay), 0 === cellCount && noContentRenderer());
+ }
+ }, {
+ key: "shouldComponentUpdate",
+ value: function(nextProps, nextState) {
+ return (0, _reactAddonsShallowCompare2.default)(this, nextProps, nextState);
+ }
+ }, {
+ key: "_enablePointerEventsAfterDelay",
+ value: function() {
+ var _this3 = this;
+ this._disablePointerEventsTimeoutId && clearTimeout(this._disablePointerEventsTimeoutId),
+ this._disablePointerEventsTimeoutId = setTimeout(function() {
+ var isScrollingChange = _this3.props.isScrollingChange;
+ isScrollingChange(!1), _this3._disablePointerEventsTimeoutId = null, _this3.setState({
+ isScrolling: !1
+ });
+ }, IS_SCROLLING_TIMEOUT);
+ }
+ }, {
+ key: "_invokeOnSectionRenderedHelper",
+ value: function() {
+ var _props4 = this.props, cellLayoutManager = _props4.cellLayoutManager, onSectionRendered = _props4.onSectionRendered;
+ this._onSectionRenderedMemoizer({
+ callback: onSectionRendered,
+ indices: {
+ indices: cellLayoutManager.getLastRenderedIndices()
+ }
+ });
+ }
+ }, {
+ key: "_invokeOnScrollMemoizer",
+ value: function(_ref2) {
+ var _this4 = this, scrollLeft = _ref2.scrollLeft, scrollTop = _ref2.scrollTop, totalHeight = _ref2.totalHeight, totalWidth = _ref2.totalWidth;
+ this._onScrollMemoizer({
+ callback: function(_ref3) {
+ var scrollLeft = _ref3.scrollLeft, scrollTop = _ref3.scrollTop, _props5 = _this4.props, height = _props5.height, onScroll = _props5.onScroll, width = _props5.width;
+ onScroll({
+ clientHeight: height,
+ clientWidth: width,
+ scrollHeight: totalHeight,
+ scrollLeft: scrollLeft,
+ scrollTop: scrollTop,
+ scrollWidth: totalWidth
+ });
+ },
+ indices: {
+ scrollLeft: scrollLeft,
+ scrollTop: scrollTop
+ }
+ });
+ }
+ }, {
+ key: "_setScrollPosition",
+ value: function(_ref4) {
+ var scrollLeft = _ref4.scrollLeft, scrollTop = _ref4.scrollTop, newState = {
+ scrollPositionChangeReason: SCROLL_POSITION_CHANGE_REASONS.REQUESTED
+ };
+ scrollLeft >= 0 && (newState.scrollLeft = scrollLeft), scrollTop >= 0 && (newState.scrollTop = scrollTop),
+ (scrollLeft >= 0 && scrollLeft !== this.state.scrollLeft || scrollTop >= 0 && scrollTop !== this.state.scrollTop) && this.setState(newState);
+ }
+ }, {
+ key: "_updateScrollPositionForScrollToCell",
+ value: function() {
+ var _props6 = this.props, cellLayoutManager = _props6.cellLayoutManager, height = _props6.height, scrollToAlignment = _props6.scrollToAlignment, scrollToCell = _props6.scrollToCell, width = _props6.width, _state3 = this.state, scrollLeft = _state3.scrollLeft, scrollTop = _state3.scrollTop;
+ if (scrollToCell >= 0) {
+ var scrollPosition = cellLayoutManager.getScrollPositionForCell({
+ align: scrollToAlignment,
+ cellIndex: scrollToCell,
+ height: height,
+ scrollLeft: scrollLeft,
+ scrollTop: scrollTop,
+ width: width
+ });
+ scrollPosition.scrollLeft === scrollLeft && scrollPosition.scrollTop === scrollTop || this._setScrollPosition(scrollPosition);
+ }
+ }
+ }, {
+ key: "_onScroll",
+ value: function(event) {
+ if (event.target === this._scrollingContainer) {
+ this._enablePointerEventsAfterDelay();
+ var _props7 = this.props, cellLayoutManager = _props7.cellLayoutManager, height = _props7.height, isScrollingChange = _props7.isScrollingChange, width = _props7.width, scrollbarSize = this._scrollbarSize, _cellLayoutManager$ge3 = cellLayoutManager.getTotalSize(), totalHeight = _cellLayoutManager$ge3.height, totalWidth = _cellLayoutManager$ge3.width, scrollLeft = Math.max(0, Math.min(totalWidth - width + scrollbarSize, event.target.scrollLeft)), scrollTop = Math.max(0, Math.min(totalHeight - height + scrollbarSize, event.target.scrollTop));
+ if (this.state.scrollLeft !== scrollLeft || this.state.scrollTop !== scrollTop) {
+ var scrollPositionChangeReason = event.cancelable ? SCROLL_POSITION_CHANGE_REASONS.OBSERVED : SCROLL_POSITION_CHANGE_REASONS.REQUESTED;
+ this.state.isScrolling || isScrollingChange(!0), this.setState({
+ isScrolling: !0,
+ scrollLeft: scrollLeft,
+ scrollPositionChangeReason: scrollPositionChangeReason,
+ scrollTop: scrollTop
+ });
+ }
+ this._invokeOnScrollMemoizer({
+ scrollLeft: scrollLeft,
+ scrollTop: scrollTop,
+ totalWidth: totalWidth,
+ totalHeight: totalHeight
+ });
+ }
+ }
+ } ]), CollectionView;
+ }(_react.Component);
+ CollectionView.defaultProps = {
+ "aria-label": "grid",
+ horizontalOverscanSize: 0,
+ noContentRenderer: function() {
+ return null;
+ },
+ onScroll: function() {
+ return null;
+ },
+ onSectionRendered: function() {
+ return null;
+ },
+ scrollToAlignment: "auto",
+ style: {},
+ verticalOverscanSize: 0
+ }, exports.default = CollectionView;
+ }, /* 107 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;
+ /*!
+ Copyright (c) 2016 Jed Watson.
+ Licensed under the MIT License (MIT), see
+ http://jedwatson.github.io/classnames
+ */
+ /* global define */
+ !function() {
+ "use strict";
+ function classNames() {
+ for (var classes = [], i = 0; i < arguments.length; i++) {
+ var arg = arguments[i];
+ if (arg) {
+ var argType = typeof arg;
+ if ("string" === argType || "number" === argType) classes.push(arg); else if (Array.isArray(arg)) classes.push(classNames.apply(null, arg)); else if ("object" === argType) for (var key in arg) hasOwn.call(arg, key) && arg[key] && classes.push(key);
+ }
+ }
+ return classes.join(" ");
+ }
+ var hasOwn = {}.hasOwnProperty;
+ "undefined" != typeof module && module.exports ? module.exports = classNames : (__WEBPACK_AMD_DEFINE_ARRAY__ = [],
+ __WEBPACK_AMD_DEFINE_RESULT__ = function() {
+ return classNames;
+ }.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__), // register as 'classnames', consistent with npm package name
+ !(void 0 !== __WEBPACK_AMD_DEFINE_RESULT__ && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)));
+ }();
+ }, /* 108 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ function createCallbackMemoizer() {
+ var requireAllKeys = !(arguments.length > 0 && void 0 !== arguments[0]) || arguments[0], cachedIndices = {};
+ return function(_ref) {
+ var callback = _ref.callback, indices = _ref.indices, keys = (0, _keys2.default)(indices), allInitialized = !requireAllKeys || keys.every(function(key) {
+ var value = indices[key];
+ return Array.isArray(value) ? value.length > 0 : value >= 0;
+ }), indexChanged = keys.length !== (0, _keys2.default)(cachedIndices).length || keys.some(function(key) {
+ var cachedValue = cachedIndices[key], value = indices[key];
+ return Array.isArray(value) ? cachedValue.join(",") !== value.join(",") : cachedValue !== value;
+ });
+ cachedIndices = indices, allInitialized && indexChanged && callback(indices);
+ };
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ });
+ var _keys = __webpack_require__(109), _keys2 = _interopRequireDefault(_keys);
+ exports.default = createCallbackMemoizer;
+ }, /* 109 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ module.exports = {
+ default: __webpack_require__(110),
+ __esModule: !0
+ };
+ }, /* 110 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ __webpack_require__(111), module.exports = __webpack_require__(16).Object.keys;
+ }, /* 111 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ // 19.1.2.14 Object.keys(O)
+ var toObject = __webpack_require__(6), $keys = __webpack_require__(48);
+ __webpack_require__(14)("keys", function() {
+ return function(it) {
+ return $keys(toObject(it));
+ };
+ });
+ }, /* 112 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ var size, canUseDOM = __webpack_require__(113);
+ module.exports = function(recalc) {
+ if ((!size || recalc) && canUseDOM) {
+ var scrollDiv = document.createElementNS("http://www.w3.org/1999/xhtml","div");
+ scrollDiv.style.position = "absolute", scrollDiv.style.top = "-9999px", scrollDiv.style.width = "50px",
+ scrollDiv.style.height = "50px", scrollDiv.style.overflow = "scroll", document.firstElementChild.appendChild(scrollDiv),
+ size = scrollDiv.offsetWidth - scrollDiv.clientWidth, document.firstElementChild.removeChild(scrollDiv);
+ }
+ return size;
+ };
+ }, /* 113 */
+ /***/
+ function(module, exports) {
+ "use strict";
+ module.exports = !("undefined" == typeof window || !window.document || !window.document.createElement);
+ }, /* 114 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ function calculateSizeAndPositionData(_ref) {
+ for (var cellCount = _ref.cellCount, cellSizeAndPositionGetter = _ref.cellSizeAndPositionGetter, sectionSize = _ref.sectionSize, cellMetadata = [], sectionManager = new _SectionManager2.default(sectionSize), height = 0, width = 0, index = 0; index < cellCount; index++) {
+ var cellMetadatum = cellSizeAndPositionGetter({
+ index: index
+ });
+ if (null == cellMetadatum.height || isNaN(cellMetadatum.height) || null == cellMetadatum.width || isNaN(cellMetadatum.width) || null == cellMetadatum.x || isNaN(cellMetadatum.x) || null == cellMetadatum.y || isNaN(cellMetadatum.y)) throw Error("Invalid metadata returned for cell " + index + ":\n x:" + cellMetadatum.x + ", y:" + cellMetadatum.y + ", width:" + cellMetadatum.width + ", height:" + cellMetadatum.height);
+ height = Math.max(height, cellMetadatum.y + cellMetadatum.height), width = Math.max(width, cellMetadatum.x + cellMetadatum.width),
+ cellMetadata[index] = cellMetadatum, sectionManager.registerCell({
+ cellMetadatum: cellMetadatum,
+ index: index
+ });
+ }
+ return {
+ cellMetadata: cellMetadata,
+ height: height,
+ sectionManager: sectionManager,
+ width: width
+ };
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ }), exports.default = calculateSizeAndPositionData;
+ var _SectionManager = __webpack_require__(115), _SectionManager2 = _interopRequireDefault(_SectionManager);
+ }, /* 115 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ });
+ var _keys = __webpack_require__(109), _keys2 = _interopRequireDefault(_keys), _classCallCheck2 = __webpack_require__(29), _classCallCheck3 = _interopRequireDefault(_classCallCheck2), _createClass2 = __webpack_require__(30), _createClass3 = _interopRequireDefault(_createClass2), _Section = __webpack_require__(116), _Section2 = _interopRequireDefault(_Section), SECTION_SIZE = 100, SectionManager = function() {
+ function SectionManager() {
+ var sectionSize = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : SECTION_SIZE;
+ (0, _classCallCheck3.default)(this, SectionManager), this._sectionSize = sectionSize,
+ this._cellMetadata = [], this._sections = {};
+ }
+ return (0, _createClass3.default)(SectionManager, [ {
+ key: "getCellIndices",
+ value: function(_ref) {
+ var height = _ref.height, width = _ref.width, x = _ref.x, y = _ref.y, indices = {};
+ return this.getSections({
+ height: height,
+ width: width,
+ x: x,
+ y: y
+ }).forEach(function(section) {
+ return section.getCellIndices().forEach(function(index) {
+ indices[index] = index;
+ });
+ }), (0, _keys2.default)(indices).map(function(index) {
+ return indices[index];
+ });
+ }
+ }, {
+ key: "getCellMetadata",
+ value: function(_ref2) {
+ var index = _ref2.index;
+ return this._cellMetadata[index];
+ }
+ }, {
+ key: "getSections",
+ value: function(_ref3) {
+ for (var height = _ref3.height, width = _ref3.width, x = _ref3.x, y = _ref3.y, sectionXStart = Math.floor(x / this._sectionSize), sectionXStop = Math.floor((x + width - 1) / this._sectionSize), sectionYStart = Math.floor(y / this._sectionSize), sectionYStop = Math.floor((y + height - 1) / this._sectionSize), sections = [], sectionX = sectionXStart; sectionX <= sectionXStop; sectionX++) for (var sectionY = sectionYStart; sectionY <= sectionYStop; sectionY++) {
+ var key = sectionX + "." + sectionY;
+ this._sections[key] || (this._sections[key] = new _Section2.default({
+ height: this._sectionSize,
+ width: this._sectionSize,
+ x: sectionX * this._sectionSize,
+ y: sectionY * this._sectionSize
+ })), sections.push(this._sections[key]);
+ }
+ return sections;
+ }
+ }, {
+ key: "getTotalSectionCount",
+ value: function() {
+ return (0, _keys2.default)(this._sections).length;
+ }
+ }, {
+ key: "toString",
+ value: function() {
+ var _this = this;
+ return (0, _keys2.default)(this._sections).map(function(index) {
+ return _this._sections[index].toString();
+ });
+ }
+ }, {
+ key: "registerCell",
+ value: function(_ref4) {
+ var cellMetadatum = _ref4.cellMetadatum, index = _ref4.index;
+ this._cellMetadata[index] = cellMetadatum, this.getSections(cellMetadatum).forEach(function(section) {
+ return section.addCellIndex({
+ index: index
+ });
+ });
+ }
+ } ]), SectionManager;
+ }();
+ exports.default = SectionManager;
+ }, /* 116 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ });
+ var _classCallCheck2 = __webpack_require__(29), _classCallCheck3 = _interopRequireDefault(_classCallCheck2), _createClass2 = __webpack_require__(30), _createClass3 = _interopRequireDefault(_createClass2), Section = function() {
+ function Section(_ref) {
+ var height = _ref.height, width = _ref.width, x = _ref.x, y = _ref.y;
+ (0, _classCallCheck3.default)(this, Section), this.height = height, this.width = width,
+ this.x = x, this.y = y, this._indexMap = {}, this._indices = [];
+ }
+ return (0, _createClass3.default)(Section, [ {
+ key: "addCellIndex",
+ value: function(_ref2) {
+ var index = _ref2.index;
+ this._indexMap[index] || (this._indexMap[index] = !0, this._indices.push(index));
+ }
+ }, {
+ key: "getCellIndices",
+ value: function() {
+ return this._indices;
+ }
+ }, {
+ key: "toString",
+ value: function() {
+ return this.x + "," + this.y + " " + this.width + "x" + this.height;
+ }
+ } ]), Section;
+ }();
+ exports.default = Section;
+ }, /* 117 */
+ /***/
+ function(module, exports) {
+ "use strict";
+ function getUpdatedOffsetForIndex(_ref) {
+ var _ref$align = _ref.align, align = void 0 === _ref$align ? "auto" : _ref$align, cellOffset = _ref.cellOffset, cellSize = _ref.cellSize, containerSize = _ref.containerSize, currentOffset = _ref.currentOffset, maxOffset = cellOffset, minOffset = maxOffset - containerSize + cellSize;
+ switch (align) {
+ case "start":
+ return maxOffset;
+
+ case "end":
+ return minOffset;
+
+ case "center":
+ return maxOffset - (containerSize - cellSize) / 2;
+
+ default:
+ return Math.max(minOffset, Math.min(maxOffset, currentOffset));
+ }
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ }), exports.default = getUpdatedOffsetForIndex;
+ }, /* 118 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ }), exports.ColumnSizer = exports.default = void 0;
+ var _ColumnSizer2 = __webpack_require__(119), _ColumnSizer3 = _interopRequireDefault(_ColumnSizer2);
+ exports.default = _ColumnSizer3.default, exports.ColumnSizer = _ColumnSizer3.default;
+ }, /* 119 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ });
+ var _getPrototypeOf = __webpack_require__(3), _getPrototypeOf2 = _interopRequireDefault(_getPrototypeOf), _classCallCheck2 = __webpack_require__(29), _classCallCheck3 = _interopRequireDefault(_classCallCheck2), _createClass2 = __webpack_require__(30), _createClass3 = _interopRequireDefault(_createClass2), _possibleConstructorReturn2 = __webpack_require__(34), _possibleConstructorReturn3 = _interopRequireDefault(_possibleConstructorReturn2), _inherits2 = __webpack_require__(81), _inherits3 = _interopRequireDefault(_inherits2), _react = __webpack_require__(89), _reactAddonsShallowCompare = __webpack_require__(90), _reactAddonsShallowCompare2 = _interopRequireDefault(_reactAddonsShallowCompare), _Grid = __webpack_require__(120), _Grid2 = _interopRequireDefault(_Grid), ColumnSizer = function(_Component) {
+ function ColumnSizer(props, context) {
+ (0, _classCallCheck3.default)(this, ColumnSizer);
+ var _this = (0, _possibleConstructorReturn3.default)(this, (ColumnSizer.__proto__ || (0,
+ _getPrototypeOf2.default)(ColumnSizer)).call(this, props, context));
+ return _this._registerChild = _this._registerChild.bind(_this), _this;
+ }
+ return (0, _inherits3.default)(ColumnSizer, _Component), (0, _createClass3.default)(ColumnSizer, [ {
+ key: "componentDidUpdate",
+ value: function(prevProps, prevState) {
+ var _props = this.props, columnMaxWidth = _props.columnMaxWidth, columnMinWidth = _props.columnMinWidth, columnCount = _props.columnCount, width = _props.width;
+ columnMaxWidth === prevProps.columnMaxWidth && columnMinWidth === prevProps.columnMinWidth && columnCount === prevProps.columnCount && width === prevProps.width || this._registeredChild && this._registeredChild.recomputeGridSize();
+ }
+ }, {
+ key: "render",
+ value: function() {
+ var _props2 = this.props, children = _props2.children, columnMaxWidth = _props2.columnMaxWidth, columnMinWidth = _props2.columnMinWidth, columnCount = _props2.columnCount, width = _props2.width, safeColumnMinWidth = columnMinWidth || 1, safeColumnMaxWidth = columnMaxWidth ? Math.min(columnMaxWidth, width) : width, columnWidth = width / columnCount;
+ columnWidth = Math.max(safeColumnMinWidth, columnWidth), columnWidth = Math.min(safeColumnMaxWidth, columnWidth),
+ columnWidth = Math.floor(columnWidth);
+ var adjustedWidth = Math.min(width, columnWidth * columnCount);
+ return children({
+ adjustedWidth: adjustedWidth,
+ getColumnWidth: function() {
+ return columnWidth;
+ },
+ registerChild: this._registerChild
+ });
+ }
+ }, {
+ key: "shouldComponentUpdate",
+ value: function(nextProps, nextState) {
+ return (0, _reactAddonsShallowCompare2.default)(this, nextProps, nextState);
+ }
+ }, {
+ key: "_registerChild",
+ value: function(child) {
+ if (null !== child && !(child instanceof _Grid2.default)) throw Error("Unexpected child type registered; only Grid children are supported.");
+ this._registeredChild = child, this._registeredChild && this._registeredChild.recomputeGridSize();
+ }
+ } ]), ColumnSizer;
+ }(_react.Component);
+ exports.default = ColumnSizer;
+ }, /* 120 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ }), exports.defaultCellRangeRenderer = exports.Grid = exports.default = void 0;
+ var _Grid2 = __webpack_require__(121), _Grid3 = _interopRequireDefault(_Grid2), _defaultCellRangeRenderer2 = __webpack_require__(127), _defaultCellRangeRenderer3 = _interopRequireDefault(_defaultCellRangeRenderer2);
+ exports.default = _Grid3.default, exports.Grid = _Grid3.default, exports.defaultCellRangeRenderer = _defaultCellRangeRenderer3.default;
+ }, /* 121 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ }), exports.DEFAULT_SCROLLING_RESET_TIME_INTERVAL = void 0;
+ var _extends2 = __webpack_require__(100), _extends3 = _interopRequireDefault(_extends2), _getPrototypeOf = __webpack_require__(3), _getPrototypeOf2 = _interopRequireDefault(_getPrototypeOf), _classCallCheck2 = __webpack_require__(29), _classCallCheck3 = _interopRequireDefault(_classCallCheck2), _createClass2 = __webpack_require__(30), _createClass3 = _interopRequireDefault(_createClass2), _possibleConstructorReturn2 = __webpack_require__(34), _possibleConstructorReturn3 = _interopRequireDefault(_possibleConstructorReturn2), _inherits2 = __webpack_require__(81), _inherits3 = _interopRequireDefault(_inherits2), _react = __webpack_require__(89), _react2 = _interopRequireDefault(_react), _classnames = __webpack_require__(107), _classnames2 = _interopRequireDefault(_classnames), _calculateSizeAndPositionDataAndUpdateScrollOffset = __webpack_require__(122), _calculateSizeAndPositionDataAndUpdateScrollOffset2 = _interopRequireDefault(_calculateSizeAndPositionDataAndUpdateScrollOffset), _ScalingCellSizeAndPositionManager = __webpack_require__(123), _ScalingCellSizeAndPositionManager2 = _interopRequireDefault(_ScalingCellSizeAndPositionManager), _createCallbackMemoizer = __webpack_require__(108), _createCallbackMemoizer2 = _interopRequireDefault(_createCallbackMemoizer), _getOverscanIndices = __webpack_require__(125), _getOverscanIndices2 = _interopRequireDefault(_getOverscanIndices), _scrollbarSize = __webpack_require__(112), _scrollbarSize2 = _interopRequireDefault(_scrollbarSize), _reactAddonsShallowCompare = __webpack_require__(90), _reactAddonsShallowCompare2 = _interopRequireDefault(_reactAddonsShallowCompare), _updateScrollIndexHelper = __webpack_require__(126), _updateScrollIndexHelper2 = _interopRequireDefault(_updateScrollIndexHelper), _defaultCellRangeRenderer = __webpack_require__(127), _defaultCellRangeRenderer2 = _interopRequireDefault(_defaultCellRangeRenderer), DEFAULT_SCROLLING_RESET_TIME_INTERVAL = exports.DEFAULT_SCROLLING_RESET_TIME_INTERVAL = 150, SCROLL_POSITION_CHANGE_REASONS = {
+ OBSERVED: "observed",
+ REQUESTED: "requested"
+ }, Grid = function(_Component) {
+ function Grid(props, context) {
+ (0, _classCallCheck3.default)(this, Grid);
+ var _this = (0, _possibleConstructorReturn3.default)(this, (Grid.__proto__ || (0,
+ _getPrototypeOf2.default)(Grid)).call(this, props, context));
+ return _this.state = {
+ isScrolling: !1,
+ scrollDirectionHorizontal: _getOverscanIndices.SCROLL_DIRECTION_FIXED,
+ scrollDirectionVertical: _getOverscanIndices.SCROLL_DIRECTION_FIXED,
+ scrollLeft: 0,
+ scrollTop: 0
+ }, _this._onGridRenderedMemoizer = (0, _createCallbackMemoizer2.default)(), _this._onScrollMemoizer = (0,
+ _createCallbackMemoizer2.default)(!1), _this._enablePointerEventsAfterDelayCallback = _this._enablePointerEventsAfterDelayCallback.bind(_this),
+ _this._invokeOnGridRenderedHelper = _this._invokeOnGridRenderedHelper.bind(_this),
+ _this._onScroll = _this._onScroll.bind(_this), _this._updateScrollLeftForScrollToColumn = _this._updateScrollLeftForScrollToColumn.bind(_this),
+ _this._updateScrollTopForScrollToRow = _this._updateScrollTopForScrollToRow.bind(_this),
+ _this._columnWidthGetter = _this._wrapSizeGetter(props.columnWidth), _this._rowHeightGetter = _this._wrapSizeGetter(props.rowHeight),
+ _this._columnSizeAndPositionManager = new _ScalingCellSizeAndPositionManager2.default({
+ cellCount: props.columnCount,
+ cellSizeGetter: function(index) {
+ return _this._columnWidthGetter(index);
+ },
+ estimatedCellSize: _this._getEstimatedColumnSize(props)
+ }), _this._rowSizeAndPositionManager = new _ScalingCellSizeAndPositionManager2.default({
+ cellCount: props.rowCount,
+ cellSizeGetter: function(index) {
+ return _this._rowHeightGetter(index);
+ },
+ estimatedCellSize: _this._getEstimatedRowSize(props)
+ }), _this._cellCache = {}, _this;
+ }
+ return (0, _inherits3.default)(Grid, _Component), (0, _createClass3.default)(Grid, [ {
+ key: "measureAllCells",
+ value: function() {
+ var _props = this.props, columnCount = _props.columnCount, rowCount = _props.rowCount;
+ this._columnSizeAndPositionManager.getSizeAndPositionOfCell(columnCount - 1), this._rowSizeAndPositionManager.getSizeAndPositionOfCell(rowCount - 1);
+ }
+ }, {
+ key: "recomputeGridSize",
+ value: function() {
+ var _ref = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {}, _ref$columnIndex = _ref.columnIndex, columnIndex = void 0 === _ref$columnIndex ? 0 : _ref$columnIndex, _ref$rowIndex = _ref.rowIndex, rowIndex = void 0 === _ref$rowIndex ? 0 : _ref$rowIndex;
+ this._columnSizeAndPositionManager.resetCell(columnIndex), this._rowSizeAndPositionManager.resetCell(rowIndex),
+ this._cellCache = {}, this.forceUpdate();
+ }
+ }, {
+ key: "componentDidMount",
+ value: function() {
+ var _props2 = this.props, scrollLeft = _props2.scrollLeft, scrollToColumn = _props2.scrollToColumn, scrollTop = _props2.scrollTop, scrollToRow = _props2.scrollToRow;
+ this._scrollbarSizeMeasured || (this._scrollbarSize = (0, _scrollbarSize2.default)(),
+ this._scrollbarSizeMeasured = !0, this.setState({})), (scrollLeft >= 0 || scrollTop >= 0) && this._setScrollPosition({
+ scrollLeft: scrollLeft,
+ scrollTop: scrollTop
+ }), (scrollToColumn >= 0 || scrollToRow >= 0) && (this._updateScrollLeftForScrollToColumn(),
+ this._updateScrollTopForScrollToRow()), this._invokeOnGridRenderedHelper(), this._invokeOnScrollMemoizer({
+ scrollLeft: scrollLeft || 0,
+ scrollTop: scrollTop || 0,
+ totalColumnsWidth: this._columnSizeAndPositionManager.getTotalSize(),
+ totalRowsHeight: this._rowSizeAndPositionManager.getTotalSize()
+ });
+ }
+ }, {
+ key: "componentDidUpdate",
+ value: function(prevProps, prevState) {
+ var _this2 = this, _props3 = this.props, autoHeight = _props3.autoHeight, columnCount = _props3.columnCount, height = _props3.height, rowCount = _props3.rowCount, scrollToAlignment = _props3.scrollToAlignment, scrollToColumn = _props3.scrollToColumn, scrollToRow = _props3.scrollToRow, width = _props3.width, _state = this.state, scrollLeft = _state.scrollLeft, scrollPositionChangeReason = _state.scrollPositionChangeReason, scrollTop = _state.scrollTop, columnOrRowCountJustIncreasedFromZero = columnCount > 0 && 0 === prevProps.columnCount || rowCount > 0 && 0 === prevProps.rowCount;
+ if (scrollPositionChangeReason === SCROLL_POSITION_CHANGE_REASONS.REQUESTED && (scrollLeft >= 0 && (scrollLeft !== prevState.scrollLeft && scrollLeft !== this._scrollingContainer.scrollLeft || columnOrRowCountJustIncreasedFromZero) && (this._scrollingContainer.scrollLeft = scrollLeft),
+ !autoHeight && scrollTop >= 0 && (scrollTop !== prevState.scrollTop && scrollTop !== this._scrollingContainer.scrollTop || columnOrRowCountJustIncreasedFromZero) && (this._scrollingContainer.scrollTop = scrollTop)),
+ (0, _updateScrollIndexHelper2.default)({
+ cellSizeAndPositionManager: this._columnSizeAndPositionManager,
+ previousCellsCount: prevProps.columnCount,
+ previousCellSize: prevProps.columnWidth,
+ previousScrollToAlignment: prevProps.scrollToAlignment,
+ previousScrollToIndex: prevProps.scrollToColumn,
+ previousSize: prevProps.width,
+ scrollOffset: scrollLeft,
+ scrollToAlignment: scrollToAlignment,
+ scrollToIndex: scrollToColumn,
+ size: width,
+ updateScrollIndexCallback: function(scrollToColumn) {
+ return _this2._updateScrollLeftForScrollToColumn((0, _extends3.default)({}, _this2.props, {
+ scrollToColumn: scrollToColumn
+ }));
+ }
+ }), (0, _updateScrollIndexHelper2.default)({
+ cellSizeAndPositionManager: this._rowSizeAndPositionManager,
+ previousCellsCount: prevProps.rowCount,
+ previousCellSize: prevProps.rowHeight,
+ previousScrollToAlignment: prevProps.scrollToAlignment,
+ previousScrollToIndex: prevProps.scrollToRow,
+ previousSize: prevProps.height,
+ scrollOffset: scrollTop,
+ scrollToAlignment: scrollToAlignment,
+ scrollToIndex: scrollToRow,
+ size: height,
+ updateScrollIndexCallback: function(scrollToRow) {
+ return _this2._updateScrollTopForScrollToRow((0, _extends3.default)({}, _this2.props, {
+ scrollToRow: scrollToRow
+ }));
+ }
+ }), this._invokeOnGridRenderedHelper(), scrollLeft !== prevState.scrollLeft || scrollTop !== prevState.scrollTop) {
+ var totalRowsHeight = this._rowSizeAndPositionManager.getTotalSize(), totalColumnsWidth = this._columnSizeAndPositionManager.getTotalSize();
+ this._invokeOnScrollMemoizer({
+ scrollLeft: scrollLeft,
+ scrollTop: scrollTop,
+ totalColumnsWidth: totalColumnsWidth,
+ totalRowsHeight: totalRowsHeight
+ });
+ }
+ }
+ }, {
+ key: "componentWillMount",
+ value: function() {
+ this._scrollbarSize = (0, _scrollbarSize2.default)(), void 0 === this._scrollbarSize ? (this._scrollbarSizeMeasured = !1,
+ this._scrollbarSize = 0) : this._scrollbarSizeMeasured = !0, this._calculateChildrenToRender();
+ }
+ }, {
+ key: "componentWillUnmount",
+ value: function() {
+ this._disablePointerEventsTimeoutId && clearTimeout(this._disablePointerEventsTimeoutId);
+ }
+ }, {
+ key: "componentWillUpdate",
+ value: function(nextProps, nextState) {
+ var _this3 = this;
+ 0 === nextProps.columnCount && 0 !== nextState.scrollLeft || 0 === nextProps.rowCount && 0 !== nextState.scrollTop ? this._setScrollPosition({
+ scrollLeft: 0,
+ scrollTop: 0
+ }) : nextProps.scrollLeft === this.props.scrollLeft && nextProps.scrollTop === this.props.scrollTop || this._setScrollPosition({
+ scrollLeft: nextProps.scrollLeft,
+ scrollTop: nextProps.scrollTop
+ }), this._columnWidthGetter = this._wrapSizeGetter(nextProps.columnWidth), this._rowHeightGetter = this._wrapSizeGetter(nextProps.rowHeight),
+ this._columnSizeAndPositionManager.configure({
+ cellCount: nextProps.columnCount,
+ estimatedCellSize: this._getEstimatedColumnSize(nextProps)
+ }), this._rowSizeAndPositionManager.configure({
+ cellCount: nextProps.rowCount,
+ estimatedCellSize: this._getEstimatedRowSize(nextProps)
+ }), (0, _calculateSizeAndPositionDataAndUpdateScrollOffset2.default)({
+ cellCount: this.props.columnCount,
+ cellSize: this.props.columnWidth,
+ computeMetadataCallback: function() {
+ return _this3._columnSizeAndPositionManager.resetCell(0);
+ },
+ computeMetadataCallbackProps: nextProps,
+ nextCellsCount: nextProps.columnCount,
+ nextCellSize: nextProps.columnWidth,
+ nextScrollToIndex: nextProps.scrollToColumn,
+ scrollToIndex: this.props.scrollToColumn,
+ updateScrollOffsetForScrollToIndex: function() {
+ return _this3._updateScrollLeftForScrollToColumn(nextProps, nextState);
+ }
+ }), (0, _calculateSizeAndPositionDataAndUpdateScrollOffset2.default)({
+ cellCount: this.props.rowCount,
+ cellSize: this.props.rowHeight,
+ computeMetadataCallback: function() {
+ return _this3._rowSizeAndPositionManager.resetCell(0);
+ },
+ computeMetadataCallbackProps: nextProps,
+ nextCellsCount: nextProps.rowCount,
+ nextCellSize: nextProps.rowHeight,
+ nextScrollToIndex: nextProps.scrollToRow,
+ scrollToIndex: this.props.scrollToRow,
+ updateScrollOffsetForScrollToIndex: function() {
+ return _this3._updateScrollTopForScrollToRow(nextProps, nextState);
+ }
+ }), this._calculateChildrenToRender(nextProps, nextState);
+ }
+ }, {
+ key: "render",
+ value: function() {
+ var _this4 = this, _props4 = this.props, autoContainerWidth = _props4.autoContainerWidth, autoHeight = _props4.autoHeight, className = _props4.className, containerStyle = _props4.containerStyle, height = _props4.height, id = _props4.id, noContentRenderer = _props4.noContentRenderer, style = _props4.style, tabIndex = _props4.tabIndex, width = _props4.width, isScrolling = this.state.isScrolling, gridStyle = {
+ boxSizing: "border-box",
+ height: autoHeight ? "auto" : height,
+ position: "relative",
+ width: width,
+ WebkitOverflowScrolling: "touch",
+ willChange: "transform"
+ }, totalColumnsWidth = this._columnSizeAndPositionManager.getTotalSize(), totalRowsHeight = this._rowSizeAndPositionManager.getTotalSize(), verticalScrollBarSize = totalRowsHeight > height ? this._scrollbarSize : 0, horizontalScrollBarSize = totalColumnsWidth > width ? this._scrollbarSize : 0;
+ gridStyle.overflowX = totalColumnsWidth + verticalScrollBarSize <= width ? "hidden" : "auto",
+ gridStyle.overflowY = totalRowsHeight + horizontalScrollBarSize <= height ? "hidden" : "auto";
+ var childrenToDisplay = this._childrenToDisplay, showNoContentRenderer = 0 === childrenToDisplay.length && height > 0 && width > 0;
+ return _react2.default.createElement("div", {
+ ref: function(_ref2) {
+ _this4._scrollingContainer = _ref2;
+ },
+ "aria-label": this.props["aria-label"],
+ className: (0, _classnames2.default)("ReactVirtualized__Grid", className),
+ id: id,
+ onScroll: this._onScroll,
+ role: "grid",
+ style: (0, _extends3.default)({}, gridStyle, style),
+ tabIndex: tabIndex
+ }, childrenToDisplay.length > 0 && _react2.default.createElement("div", {
+ className: "ReactVirtualized__Grid__innerScrollContainer",
+ style: (0, _extends3.default)({
+ width: autoContainerWidth ? "auto" : totalColumnsWidth,
+ height: totalRowsHeight,
+ maxWidth: totalColumnsWidth,
+ maxHeight: totalRowsHeight,
+ overflow: "hidden",
+ pointerEvents: isScrolling ? "none" : ""
+ }, containerStyle)
+ }, childrenToDisplay), showNoContentRenderer && noContentRenderer());
+ }
+ }, {
+ key: "shouldComponentUpdate",
+ value: function(nextProps, nextState) {
+ return (0, _reactAddonsShallowCompare2.default)(this, nextProps, nextState);
+ }
+ }, {
+ key: "_calculateChildrenToRender",
+ value: function() {
+ var props = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : this.props, state = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : this.state, cellRenderer = props.cellRenderer, cellRangeRenderer = props.cellRangeRenderer, columnCount = props.columnCount, height = props.height, overscanColumnCount = props.overscanColumnCount, overscanRowCount = props.overscanRowCount, rowCount = props.rowCount, width = props.width, isScrolling = state.isScrolling, scrollDirectionHorizontal = state.scrollDirectionHorizontal, scrollDirectionVertical = state.scrollDirectionVertical, scrollLeft = state.scrollLeft, scrollTop = state.scrollTop;
+ if (this._childrenToDisplay = [], height > 0 && width > 0) {
+ var visibleColumnIndices = this._columnSizeAndPositionManager.getVisibleCellRange({
+ containerSize: width,
+ offset: scrollLeft
+ }), visibleRowIndices = this._rowSizeAndPositionManager.getVisibleCellRange({
+ containerSize: height,
+ offset: scrollTop
+ }), horizontalOffsetAdjustment = this._columnSizeAndPositionManager.getOffsetAdjustment({
+ containerSize: width,
+ offset: scrollLeft
+ }), verticalOffsetAdjustment = this._rowSizeAndPositionManager.getOffsetAdjustment({
+ containerSize: height,
+ offset: scrollTop
+ });
+ this._renderedColumnStartIndex = visibleColumnIndices.start, this._renderedColumnStopIndex = visibleColumnIndices.stop,
+ this._renderedRowStartIndex = visibleRowIndices.start, this._renderedRowStopIndex = visibleRowIndices.stop;
+ var overscanColumnIndices = (0, _getOverscanIndices2.default)({
+ cellCount: columnCount,
+ overscanCellsCount: overscanColumnCount,
+ scrollDirection: scrollDirectionHorizontal,
+ startIndex: this._renderedColumnStartIndex,
+ stopIndex: this._renderedColumnStopIndex
+ }), overscanRowIndices = (0, _getOverscanIndices2.default)({
+ cellCount: rowCount,
+ overscanCellsCount: overscanRowCount,
+ scrollDirection: scrollDirectionVertical,
+ startIndex: this._renderedRowStartIndex,
+ stopIndex: this._renderedRowStopIndex
+ });
+ this._columnStartIndex = overscanColumnIndices.overscanStartIndex, this._columnStopIndex = overscanColumnIndices.overscanStopIndex,
+ this._rowStartIndex = overscanRowIndices.overscanStartIndex, this._rowStopIndex = overscanRowIndices.overscanStopIndex,
+ this._childrenToDisplay = cellRangeRenderer({
+ cellCache: this._cellCache,
+ cellRenderer: cellRenderer,
+ columnSizeAndPositionManager: this._columnSizeAndPositionManager,
+ columnStartIndex: this._columnStartIndex,
+ columnStopIndex: this._columnStopIndex,
+ horizontalOffsetAdjustment: horizontalOffsetAdjustment,
+ isScrolling: isScrolling,
+ rowSizeAndPositionManager: this._rowSizeAndPositionManager,
+ rowStartIndex: this._rowStartIndex,
+ rowStopIndex: this._rowStopIndex,
+ scrollLeft: scrollLeft,
+ scrollTop: scrollTop,
+ verticalOffsetAdjustment: verticalOffsetAdjustment,
+ visibleColumnIndices: visibleColumnIndices,
+ visibleRowIndices: visibleRowIndices
+ });
+ }
+ }
+ }, {
+ key: "_enablePointerEventsAfterDelay",
+ value: function() {
+ var scrollingResetTimeInterval = this.props.scrollingResetTimeInterval;
+ this._disablePointerEventsTimeoutId && clearTimeout(this._disablePointerEventsTimeoutId),
+ this._disablePointerEventsTimeoutId = setTimeout(this._enablePointerEventsAfterDelayCallback, scrollingResetTimeInterval);
+ }
+ }, {
+ key: "_enablePointerEventsAfterDelayCallback",
+ value: function() {
+ this._disablePointerEventsTimeoutId = null, this._cellCache = {}, this.setState({
+ isScrolling: !1,
+ scrollDirectionHorizontal: _getOverscanIndices.SCROLL_DIRECTION_FIXED,
+ scrollDirectionVertical: _getOverscanIndices.SCROLL_DIRECTION_FIXED
+ });
+ }
+ }, {
+ key: "_getEstimatedColumnSize",
+ value: function(props) {
+ return "number" == typeof props.columnWidth ? props.columnWidth : props.estimatedColumnSize;
+ }
+ }, {
+ key: "_getEstimatedRowSize",
+ value: function(props) {
+ return "number" == typeof props.rowHeight ? props.rowHeight : props.estimatedRowSize;
+ }
+ }, {
+ key: "_invokeOnGridRenderedHelper",
+ value: function() {
+ var onSectionRendered = this.props.onSectionRendered;
+ this._onGridRenderedMemoizer({
+ callback: onSectionRendered,
+ indices: {
+ columnOverscanStartIndex: this._columnStartIndex,
+ columnOverscanStopIndex: this._columnStopIndex,
+ columnStartIndex: this._renderedColumnStartIndex,
+ columnStopIndex: this._renderedColumnStopIndex,
+ rowOverscanStartIndex: this._rowStartIndex,
+ rowOverscanStopIndex: this._rowStopIndex,
+ rowStartIndex: this._renderedRowStartIndex,
+ rowStopIndex: this._renderedRowStopIndex
+ }
+ });
+ }
+ }, {
+ key: "_invokeOnScrollMemoizer",
+ value: function(_ref3) {
+ var _this5 = this, scrollLeft = _ref3.scrollLeft, scrollTop = _ref3.scrollTop, totalColumnsWidth = _ref3.totalColumnsWidth, totalRowsHeight = _ref3.totalRowsHeight;
+ this._onScrollMemoizer({
+ callback: function(_ref4) {
+ var scrollLeft = _ref4.scrollLeft, scrollTop = _ref4.scrollTop, _props5 = _this5.props, height = _props5.height, onScroll = _props5.onScroll, width = _props5.width;
+ onScroll({
+ clientHeight: height,
+ clientWidth: width,
+ scrollHeight: totalRowsHeight,
+ scrollLeft: scrollLeft,
+ scrollTop: scrollTop,
+ scrollWidth: totalColumnsWidth
+ });
+ },
+ indices: {
+ scrollLeft: scrollLeft,
+ scrollTop: scrollTop
+ }
+ });
+ }
+ }, {
+ key: "_setScrollPosition",
+ value: function(_ref5) {
+ var scrollLeft = _ref5.scrollLeft, scrollTop = _ref5.scrollTop, newState = {
+ scrollPositionChangeReason: SCROLL_POSITION_CHANGE_REASONS.REQUESTED
+ };
+ scrollLeft >= 0 && (newState.scrollLeft = scrollLeft), scrollTop >= 0 && (newState.scrollTop = scrollTop),
+ (scrollLeft >= 0 && scrollLeft !== this.state.scrollLeft || scrollTop >= 0 && scrollTop !== this.state.scrollTop) && this.setState(newState);
+ }
+ }, {
+ key: "_wrapPropertyGetter",
+ value: function(value) {
+ return value instanceof Function ? value : function() {
+ return value;
+ };
+ }
+ }, {
+ key: "_wrapSizeGetter",
+ value: function(size) {
+ return this._wrapPropertyGetter(size);
+ }
+ }, {
+ key: "_updateScrollLeftForScrollToColumn",
+ value: function() {
+ var props = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : this.props, state = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : this.state, columnCount = props.columnCount, scrollToAlignment = props.scrollToAlignment, scrollToColumn = props.scrollToColumn, width = props.width, scrollLeft = state.scrollLeft;
+ if (scrollToColumn >= 0 && columnCount > 0) {
+ var targetIndex = Math.max(0, Math.min(columnCount - 1, scrollToColumn)), calculatedScrollLeft = this._columnSizeAndPositionManager.getUpdatedOffsetForIndex({
+ align: scrollToAlignment,
+ containerSize: width,
+ currentOffset: scrollLeft,
+ targetIndex: targetIndex
+ });
+ scrollLeft !== calculatedScrollLeft && this._setScrollPosition({
+ scrollLeft: calculatedScrollLeft
+ });
+ }
+ }
+ }, {
+ key: "_updateScrollTopForScrollToRow",
+ value: function() {
+ var props = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : this.props, state = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : this.state, height = props.height, rowCount = props.rowCount, scrollToAlignment = props.scrollToAlignment, scrollToRow = props.scrollToRow, scrollTop = state.scrollTop;
+ if (scrollToRow >= 0 && rowCount > 0) {
+ var targetIndex = Math.max(0, Math.min(rowCount - 1, scrollToRow)), calculatedScrollTop = this._rowSizeAndPositionManager.getUpdatedOffsetForIndex({
+ align: scrollToAlignment,
+ containerSize: height,
+ currentOffset: scrollTop,
+ targetIndex: targetIndex
+ });
+ scrollTop !== calculatedScrollTop && this._setScrollPosition({
+ scrollTop: calculatedScrollTop
+ });
+ }
+ }
+ }, {
+ key: "_onScroll",
+ value: function(event) {
+ if (event.target === this._scrollingContainer) {
+ this._enablePointerEventsAfterDelay();
+ var _props6 = this.props, height = _props6.height, width = _props6.width, scrollbarSize = this._scrollbarSize, totalRowsHeight = this._rowSizeAndPositionManager.getTotalSize(), totalColumnsWidth = this._columnSizeAndPositionManager.getTotalSize(), scrollLeft = Math.min(Math.max(0, totalColumnsWidth - width + scrollbarSize), event.target.scrollLeft), scrollTop = Math.min(Math.max(0, totalRowsHeight - height + scrollbarSize), event.target.scrollTop);
+ if (this.state.scrollLeft !== scrollLeft || this.state.scrollTop !== scrollTop) {
+ var scrollDirectionVertical = scrollTop > this.state.scrollTop ? _getOverscanIndices.SCROLL_DIRECTION_FORWARD : _getOverscanIndices.SCROLL_DIRECTION_BACKWARD, scrollDirectionHorizontal = scrollLeft > this.state.scrollLeft ? _getOverscanIndices.SCROLL_DIRECTION_FORWARD : _getOverscanIndices.SCROLL_DIRECTION_BACKWARD;
+ this.setState({
+ isScrolling: !0,
+ scrollDirectionHorizontal: scrollDirectionHorizontal,
+ scrollDirectionVertical: scrollDirectionVertical,
+ scrollLeft: scrollLeft,
+ scrollPositionChangeReason: SCROLL_POSITION_CHANGE_REASONS.OBSERVED,
+ scrollTop: scrollTop
+ });
+ }
+ this._invokeOnScrollMemoizer({
+ scrollLeft: scrollLeft,
+ scrollTop: scrollTop,
+ totalColumnsWidth: totalColumnsWidth,
+ totalRowsHeight: totalRowsHeight
+ });
+ }
+ }
+ } ]), Grid;
+ }(_react.Component);
+ Grid.defaultProps = {
+ "aria-label": "grid",
+ cellRangeRenderer: _defaultCellRangeRenderer2.default,
+ estimatedColumnSize: 100,
+ estimatedRowSize: 30,
+ noContentRenderer: function() {
+ return null;
+ },
+ onScroll: function() {
+ return null;
+ },
+ onSectionRendered: function() {
+ return null;
+ },
+ overscanColumnCount: 0,
+ overscanRowCount: 10,
+ scrollingResetTimeInterval: DEFAULT_SCROLLING_RESET_TIME_INTERVAL,
+ scrollToAlignment: "auto",
+ style: {},
+ tabIndex: 0
+ }, exports.default = Grid;
+ }, /* 122 */
+ /***/
+ function(module, exports) {
+ "use strict";
+ function calculateSizeAndPositionDataAndUpdateScrollOffset(_ref) {
+ var cellCount = _ref.cellCount, cellSize = _ref.cellSize, computeMetadataCallback = _ref.computeMetadataCallback, computeMetadataCallbackProps = _ref.computeMetadataCallbackProps, nextCellsCount = _ref.nextCellsCount, nextCellSize = _ref.nextCellSize, nextScrollToIndex = _ref.nextScrollToIndex, scrollToIndex = _ref.scrollToIndex, updateScrollOffsetForScrollToIndex = _ref.updateScrollOffsetForScrollToIndex;
+ cellCount === nextCellsCount && ("number" != typeof cellSize && "number" != typeof nextCellSize || cellSize === nextCellSize) || (computeMetadataCallback(computeMetadataCallbackProps),
+ scrollToIndex >= 0 && scrollToIndex === nextScrollToIndex && updateScrollOffsetForScrollToIndex());
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ }), exports.default = calculateSizeAndPositionDataAndUpdateScrollOffset;
+ }, /* 123 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ }), exports.DEFAULT_MAX_SCROLL_SIZE = void 0;
+ var _objectWithoutProperties2 = __webpack_require__(105), _objectWithoutProperties3 = _interopRequireDefault(_objectWithoutProperties2), _classCallCheck2 = __webpack_require__(29), _classCallCheck3 = _interopRequireDefault(_classCallCheck2), _createClass2 = __webpack_require__(30), _createClass3 = _interopRequireDefault(_createClass2), _CellSizeAndPositionManager = __webpack_require__(124), _CellSizeAndPositionManager2 = _interopRequireDefault(_CellSizeAndPositionManager), DEFAULT_MAX_SCROLL_SIZE = exports.DEFAULT_MAX_SCROLL_SIZE = 15e5, ScalingCellSizeAndPositionManager = function() {
+ function ScalingCellSizeAndPositionManager(_ref) {
+ var _ref$maxScrollSize = _ref.maxScrollSize, maxScrollSize = void 0 === _ref$maxScrollSize ? DEFAULT_MAX_SCROLL_SIZE : _ref$maxScrollSize, params = (0,
+ _objectWithoutProperties3.default)(_ref, [ "maxScrollSize" ]);
+ (0, _classCallCheck3.default)(this, ScalingCellSizeAndPositionManager), this._cellSizeAndPositionManager = new _CellSizeAndPositionManager2.default(params),
+ this._maxScrollSize = maxScrollSize;
+ }
+ return (0, _createClass3.default)(ScalingCellSizeAndPositionManager, [ {
+ key: "configure",
+ value: function(params) {
+ this._cellSizeAndPositionManager.configure(params);
+ }
+ }, {
+ key: "getCellCount",
+ value: function() {
+ return this._cellSizeAndPositionManager.getCellCount();
+ }
+ }, {
+ key: "getEstimatedCellSize",
+ value: function() {
+ return this._cellSizeAndPositionManager.getEstimatedCellSize();
+ }
+ }, {
+ key: "getLastMeasuredIndex",
+ value: function() {
+ return this._cellSizeAndPositionManager.getLastMeasuredIndex();
+ }
+ }, {
+ key: "getOffsetAdjustment",
+ value: function(_ref2) {
+ var containerSize = _ref2.containerSize, offset = _ref2.offset, totalSize = this._cellSizeAndPositionManager.getTotalSize(), safeTotalSize = this.getTotalSize(), offsetPercentage = this._getOffsetPercentage({
+ containerSize: containerSize,
+ offset: offset,
+ totalSize: safeTotalSize
+ });
+ return Math.round(offsetPercentage * (safeTotalSize - totalSize));
+ }
+ }, {
+ key: "getSizeAndPositionOfCell",
+ value: function(index) {
+ return this._cellSizeAndPositionManager.getSizeAndPositionOfCell(index);
+ }
+ }, {
+ key: "getSizeAndPositionOfLastMeasuredCell",
+ value: function() {
+ return this._cellSizeAndPositionManager.getSizeAndPositionOfLastMeasuredCell();
+ }
+ }, {
+ key: "getTotalSize",
+ value: function() {
+ return Math.min(this._maxScrollSize, this._cellSizeAndPositionManager.getTotalSize());
+ }
+ }, {
+ key: "getUpdatedOffsetForIndex",
+ value: function(_ref3) {
+ var _ref3$align = _ref3.align, align = void 0 === _ref3$align ? "auto" : _ref3$align, containerSize = _ref3.containerSize, currentOffset = _ref3.currentOffset, targetIndex = _ref3.targetIndex, totalSize = _ref3.totalSize;
+ currentOffset = this._safeOffsetToOffset({
+ containerSize: containerSize,
+ offset: currentOffset
+ });
+ var offset = this._cellSizeAndPositionManager.getUpdatedOffsetForIndex({
+ align: align,
+ containerSize: containerSize,
+ currentOffset: currentOffset,
+ targetIndex: targetIndex,
+ totalSize: totalSize
+ });
+ return this._offsetToSafeOffset({
+ containerSize: containerSize,
+ offset: offset
+ });
+ }
+ }, {
+ key: "getVisibleCellRange",
+ value: function(_ref4) {
+ var containerSize = _ref4.containerSize, offset = _ref4.offset;
+ return offset = this._safeOffsetToOffset({
+ containerSize: containerSize,
+ offset: offset
+ }), this._cellSizeAndPositionManager.getVisibleCellRange({
+ containerSize: containerSize,
+ offset: offset
+ });
+ }
+ }, {
+ key: "resetCell",
+ value: function(index) {
+ this._cellSizeAndPositionManager.resetCell(index);
+ }
+ }, {
+ key: "_getOffsetPercentage",
+ value: function(_ref5) {
+ var containerSize = _ref5.containerSize, offset = _ref5.offset, totalSize = _ref5.totalSize;
+ return totalSize <= containerSize ? 0 : offset / (totalSize - containerSize);
+ }
+ }, {
+ key: "_offsetToSafeOffset",
+ value: function(_ref6) {
+ var containerSize = _ref6.containerSize, offset = _ref6.offset, totalSize = this._cellSizeAndPositionManager.getTotalSize(), safeTotalSize = this.getTotalSize();
+ if (totalSize === safeTotalSize) return offset;
+ var offsetPercentage = this._getOffsetPercentage({
+ containerSize: containerSize,
+ offset: offset,
+ totalSize: totalSize
+ });
+ return Math.round(offsetPercentage * (safeTotalSize - containerSize));
+ }
+ }, {
+ key: "_safeOffsetToOffset",
+ value: function(_ref7) {
+ var containerSize = _ref7.containerSize, offset = _ref7.offset, totalSize = this._cellSizeAndPositionManager.getTotalSize(), safeTotalSize = this.getTotalSize();
+ if (totalSize === safeTotalSize) return offset;
+ var offsetPercentage = this._getOffsetPercentage({
+ containerSize: containerSize,
+ offset: offset,
+ totalSize: safeTotalSize
+ });
+ return Math.round(offsetPercentage * (totalSize - containerSize));
+ }
+ } ]), ScalingCellSizeAndPositionManager;
+ }();
+ exports.default = ScalingCellSizeAndPositionManager;
+ }, /* 124 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ });
+ var _classCallCheck2 = __webpack_require__(29), _classCallCheck3 = _interopRequireDefault(_classCallCheck2), _createClass2 = __webpack_require__(30), _createClass3 = _interopRequireDefault(_createClass2), CellSizeAndPositionManager = function() {
+ function CellSizeAndPositionManager(_ref) {
+ var cellCount = _ref.cellCount, cellSizeGetter = _ref.cellSizeGetter, estimatedCellSize = _ref.estimatedCellSize;
+ (0, _classCallCheck3.default)(this, CellSizeAndPositionManager), this._cellSizeGetter = cellSizeGetter,
+ this._cellCount = cellCount, this._estimatedCellSize = estimatedCellSize, this._cellSizeAndPositionData = {},
+ this._lastMeasuredIndex = -1;
+ }
+ return (0, _createClass3.default)(CellSizeAndPositionManager, [ {
+ key: "configure",
+ value: function(_ref2) {
+ var cellCount = _ref2.cellCount, estimatedCellSize = _ref2.estimatedCellSize;
+ this._cellCount = cellCount, this._estimatedCellSize = estimatedCellSize;
+ }
+ }, {
+ key: "getCellCount",
+ value: function() {
+ return this._cellCount;
+ }
+ }, {
+ key: "getEstimatedCellSize",
+ value: function() {
+ return this._estimatedCellSize;
+ }
+ }, {
+ key: "getLastMeasuredIndex",
+ value: function() {
+ return this._lastMeasuredIndex;
+ }
+ }, {
+ key: "getSizeAndPositionOfCell",
+ value: function(index) {
+ if (index < 0 || index >= this._cellCount) throw Error("Requested index " + index + " is outside of range 0.." + this._cellCount);
+ if (index > this._lastMeasuredIndex) {
+ for (var lastMeasuredCellSizeAndPosition = this.getSizeAndPositionOfLastMeasuredCell(), _offset = lastMeasuredCellSizeAndPosition.offset + lastMeasuredCellSizeAndPosition.size, i = this._lastMeasuredIndex + 1; i <= index; i++) {
+ var _size = this._cellSizeGetter({
+ index: i
+ });
+ if (null == _size || isNaN(_size)) throw Error("Invalid size returned for cell " + i + " of value " + _size);
+ this._cellSizeAndPositionData[i] = {
+ offset: _offset,
+ size: _size
+ }, _offset += _size;
+ }
+ this._lastMeasuredIndex = index;
+ }
+ return this._cellSizeAndPositionData[index];
+ }
+ }, {
+ key: "getSizeAndPositionOfLastMeasuredCell",
+ value: function() {
+ return this._lastMeasuredIndex >= 0 ? this._cellSizeAndPositionData[this._lastMeasuredIndex] : {
+ offset: 0,
+ size: 0
+ };
+ }
+ }, {
+ key: "getTotalSize",
+ value: function() {
+ var lastMeasuredCellSizeAndPosition = this.getSizeAndPositionOfLastMeasuredCell();
+ return lastMeasuredCellSizeAndPosition.offset + lastMeasuredCellSizeAndPosition.size + (this._cellCount - this._lastMeasuredIndex - 1) * this._estimatedCellSize;
+ }
+ }, {
+ key: "getUpdatedOffsetForIndex",
+ value: function(_ref3) {
+ var _ref3$align = _ref3.align, align = void 0 === _ref3$align ? "auto" : _ref3$align, containerSize = _ref3.containerSize, currentOffset = _ref3.currentOffset, targetIndex = _ref3.targetIndex;
+ if (containerSize <= 0) return 0;
+ var datum = this.getSizeAndPositionOfCell(targetIndex), maxOffset = datum.offset, minOffset = maxOffset - containerSize + datum.size, idealOffset = void 0;
+ switch (align) {
+ case "start":
+ idealOffset = maxOffset;
+ break;
+
+ case "end":
+ idealOffset = minOffset;
+ break;
+
+ case "center":
+ idealOffset = maxOffset - (containerSize - datum.size) / 2;
+ break;
+
+ default:
+ idealOffset = Math.max(minOffset, Math.min(maxOffset, currentOffset));
+ }
+ var totalSize = this.getTotalSize();
+ return Math.max(0, Math.min(totalSize - containerSize, idealOffset));
+ }
+ }, {
+ key: "getVisibleCellRange",
+ value: function(_ref4) {
+ var containerSize = _ref4.containerSize, offset = _ref4.offset, totalSize = this.getTotalSize();
+ if (0 === totalSize) return {};
+ var maxOffset = offset + containerSize, start = this._findNearestCell(offset), datum = this.getSizeAndPositionOfCell(start);
+ offset = datum.offset + datum.size;
+ for (var stop = start; offset < maxOffset && stop < this._cellCount - 1; ) stop++,
+ offset += this.getSizeAndPositionOfCell(stop).size;
+ return {
+ start: start,
+ stop: stop
+ };
+ }
+ }, {
+ key: "resetCell",
+ value: function(index) {
+ this._lastMeasuredIndex = Math.min(this._lastMeasuredIndex, index - 1);
+ }
+ }, {
+ key: "_binarySearch",
+ value: function(_ref5) {
+ for (var high = _ref5.high, low = _ref5.low, offset = _ref5.offset, middle = void 0, currentOffset = void 0; low <= high; ) {
+ if (middle = low + Math.floor((high - low) / 2), currentOffset = this.getSizeAndPositionOfCell(middle).offset,
+ currentOffset === offset) return middle;
+ currentOffset < offset ? low = middle + 1 : currentOffset > offset && (high = middle - 1);
+ }
+ if (low > 0) return low - 1;
+ }
+ }, {
+ key: "_exponentialSearch",
+ value: function(_ref6) {
+ for (var index = _ref6.index, offset = _ref6.offset, interval = 1; index < this._cellCount && this.getSizeAndPositionOfCell(index).offset < offset; ) index += interval,
+ interval *= 2;
+ return this._binarySearch({
+ high: Math.min(index, this._cellCount - 1),
+ low: Math.floor(index / 2),
+ offset: offset
+ });
+ }
+ }, {
+ key: "_findNearestCell",
+ value: function(offset) {
+ if (isNaN(offset)) throw Error("Invalid offset " + offset + " specified");
+ offset = Math.max(0, offset);
+ var lastMeasuredCellSizeAndPosition = this.getSizeAndPositionOfLastMeasuredCell(), lastMeasuredIndex = Math.max(0, this._lastMeasuredIndex);
+ return lastMeasuredCellSizeAndPosition.offset >= offset ? this._binarySearch({
+ high: lastMeasuredIndex,
+ low: 0,
+ offset: offset
+ }) : this._exponentialSearch({
+ index: lastMeasuredIndex,
+ offset: offset
+ });
+ }
+ } ]), CellSizeAndPositionManager;
+ }();
+ exports.default = CellSizeAndPositionManager;
+ }, /* 125 */
+ /***/
+ function(module, exports) {
+ "use strict";
+ function getOverscanIndices(_ref) {
+ var cellCount = _ref.cellCount, overscanCellsCount = _ref.overscanCellsCount, scrollDirection = _ref.scrollDirection, startIndex = _ref.startIndex, stopIndex = _ref.stopIndex, overscanStartIndex = void 0, overscanStopIndex = void 0;
+ return scrollDirection === SCROLL_DIRECTION_FORWARD ? (overscanStartIndex = startIndex,
+ overscanStopIndex = stopIndex + 2 * overscanCellsCount) : scrollDirection === SCROLL_DIRECTION_BACKWARD ? (overscanStartIndex = startIndex - 2 * overscanCellsCount,
+ overscanStopIndex = stopIndex) : (overscanStartIndex = startIndex - overscanCellsCount,
+ overscanStopIndex = stopIndex + overscanCellsCount), {
+ overscanStartIndex: Math.max(0, overscanStartIndex),
+ overscanStopIndex: Math.min(cellCount - 1, overscanStopIndex)
+ };
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ }), exports.default = getOverscanIndices;
+ var SCROLL_DIRECTION_BACKWARD = exports.SCROLL_DIRECTION_BACKWARD = -1, SCROLL_DIRECTION_FORWARD = (exports.SCROLL_DIRECTION_FIXED = 0,
+ exports.SCROLL_DIRECTION_FORWARD = 1);
+ }, /* 126 */
+ /***/
+ function(module, exports) {
+ "use strict";
+ function updateScrollIndexHelper(_ref) {
+ var cellSize = _ref.cellSize, cellSizeAndPositionManager = _ref.cellSizeAndPositionManager, previousCellsCount = _ref.previousCellsCount, previousCellSize = _ref.previousCellSize, previousScrollToAlignment = _ref.previousScrollToAlignment, previousScrollToIndex = _ref.previousScrollToIndex, previousSize = _ref.previousSize, scrollOffset = _ref.scrollOffset, scrollToAlignment = _ref.scrollToAlignment, scrollToIndex = _ref.scrollToIndex, size = _ref.size, updateScrollIndexCallback = _ref.updateScrollIndexCallback, cellCount = cellSizeAndPositionManager.getCellCount(), hasScrollToIndex = scrollToIndex >= 0 && scrollToIndex < cellCount, sizeHasChanged = size !== previousSize || !previousCellSize || "number" == typeof cellSize && cellSize !== previousCellSize;
+ hasScrollToIndex && (sizeHasChanged || scrollToAlignment !== previousScrollToAlignment || scrollToIndex !== previousScrollToIndex) ? updateScrollIndexCallback(scrollToIndex) : !hasScrollToIndex && cellCount > 0 && (size < previousSize || cellCount < previousCellsCount) && scrollOffset > cellSizeAndPositionManager.getTotalSize() - size && updateScrollIndexCallback(cellCount - 1);
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ }), exports.default = updateScrollIndexHelper;
+ }, /* 127 */
+ /***/
+ function(module, exports) {
+ "use strict";
+ function defaultCellRangeRenderer(_ref) {
+ for (var cellCache = _ref.cellCache, cellRenderer = _ref.cellRenderer, columnSizeAndPositionManager = _ref.columnSizeAndPositionManager, columnStartIndex = _ref.columnStartIndex, columnStopIndex = _ref.columnStopIndex, horizontalOffsetAdjustment = _ref.horizontalOffsetAdjustment, isScrolling = _ref.isScrolling, rowSizeAndPositionManager = _ref.rowSizeAndPositionManager, rowStartIndex = _ref.rowStartIndex, rowStopIndex = _ref.rowStopIndex, verticalOffsetAdjustment = (_ref.scrollLeft,
+ _ref.scrollTop, _ref.verticalOffsetAdjustment), visibleColumnIndices = _ref.visibleColumnIndices, visibleRowIndices = _ref.visibleRowIndices, renderedCells = [], rowIndex = rowStartIndex; rowIndex <= rowStopIndex; rowIndex++) for (var rowDatum = rowSizeAndPositionManager.getSizeAndPositionOfCell(rowIndex), columnIndex = columnStartIndex; columnIndex <= columnStopIndex; columnIndex++) {
+ var columnDatum = columnSizeAndPositionManager.getSizeAndPositionOfCell(columnIndex), isVisible = columnIndex >= visibleColumnIndices.start && columnIndex <= visibleColumnIndices.stop && rowIndex >= visibleRowIndices.start && rowIndex <= visibleRowIndices.stop, key = rowIndex + "-" + columnIndex, style = {
+ height: rowDatum.size,
+ left: columnDatum.offset + horizontalOffsetAdjustment,
+ position: "absolute",
+ top: rowDatum.offset + verticalOffsetAdjustment,
+ width: columnDatum.size
+ }, cellRendererParams = {
+ columnIndex: columnIndex,
+ isScrolling: isScrolling,
+ isVisible: isVisible,
+ key: key,
+ rowIndex: rowIndex,
+ style: style
+ }, renderedCell = void 0;
+ !isScrolling || horizontalOffsetAdjustment || verticalOffsetAdjustment ? renderedCell = cellRenderer(cellRendererParams) : (cellCache[key] || (cellCache[key] = cellRenderer(cellRendererParams)),
+ renderedCell = cellCache[key]), null != renderedCell && renderedCell !== !1 && renderedCells.push(renderedCell);
+ }
+ return renderedCells;
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ }), exports.default = defaultCellRangeRenderer;
+ }, /* 128 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ }), exports.SortIndicator = exports.SortDirection = exports.Column = exports.Table = exports.defaultRowRenderer = exports.defaultHeaderRenderer = exports.defaultCellRenderer = exports.defaultCellDataGetter = exports.default = void 0;
+ var _Table2 = __webpack_require__(129), _Table3 = _interopRequireDefault(_Table2), _defaultCellDataGetter2 = __webpack_require__(135), _defaultCellDataGetter3 = _interopRequireDefault(_defaultCellDataGetter2), _defaultCellRenderer2 = __webpack_require__(134), _defaultCellRenderer3 = _interopRequireDefault(_defaultCellRenderer2), _defaultHeaderRenderer2 = __webpack_require__(131), _defaultHeaderRenderer3 = _interopRequireDefault(_defaultHeaderRenderer2), _defaultRowRenderer2 = __webpack_require__(136), _defaultRowRenderer3 = _interopRequireDefault(_defaultRowRenderer2), _Column2 = __webpack_require__(130), _Column3 = _interopRequireDefault(_Column2), _SortDirection2 = __webpack_require__(133), _SortDirection3 = _interopRequireDefault(_SortDirection2), _SortIndicator2 = __webpack_require__(132), _SortIndicator3 = _interopRequireDefault(_SortIndicator2);
+ exports.default = _Table3.default, exports.defaultCellDataGetter = _defaultCellDataGetter3.default,
+ exports.defaultCellRenderer = _defaultCellRenderer3.default, exports.defaultHeaderRenderer = _defaultHeaderRenderer3.default,
+ exports.defaultRowRenderer = _defaultRowRenderer3.default, exports.Table = _Table3.default,
+ exports.Column = _Column3.default, exports.SortDirection = _SortDirection3.default,
+ exports.SortIndicator = _SortIndicator3.default;
+ }, /* 129 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ });
+ var _extends2 = __webpack_require__(100), _extends3 = _interopRequireDefault(_extends2), _getPrototypeOf = __webpack_require__(3), _getPrototypeOf2 = _interopRequireDefault(_getPrototypeOf), _classCallCheck2 = __webpack_require__(29), _classCallCheck3 = _interopRequireDefault(_classCallCheck2), _createClass2 = __webpack_require__(30), _createClass3 = _interopRequireDefault(_createClass2), _possibleConstructorReturn2 = __webpack_require__(34), _possibleConstructorReturn3 = _interopRequireDefault(_possibleConstructorReturn2), _inherits2 = __webpack_require__(81), _inherits3 = _interopRequireDefault(_inherits2), _classnames = __webpack_require__(107), _classnames2 = _interopRequireDefault(_classnames), _Column = __webpack_require__(130), _react = (_interopRequireDefault(_Column),
+ __webpack_require__(89)), _react2 = _interopRequireDefault(_react), _reactDom = __webpack_require__(96), _reactAddonsShallowCompare = __webpack_require__(90), _reactAddonsShallowCompare2 = _interopRequireDefault(_reactAddonsShallowCompare), _Grid = __webpack_require__(120), _Grid2 = _interopRequireDefault(_Grid), _defaultRowRenderer = __webpack_require__(136), _defaultRowRenderer2 = _interopRequireDefault(_defaultRowRenderer), _SortDirection = __webpack_require__(133), _SortDirection2 = _interopRequireDefault(_SortDirection), Table = function(_Component) {
+ function Table(props) {
+ (0, _classCallCheck3.default)(this, Table);
+ var _this = (0, _possibleConstructorReturn3.default)(this, (Table.__proto__ || (0,
+ _getPrototypeOf2.default)(Table)).call(this, props));
+ return _this.state = {
+ scrollbarWidth: 0
+ }, _this._createColumn = _this._createColumn.bind(_this), _this._createRow = _this._createRow.bind(_this),
+ _this._onScroll = _this._onScroll.bind(_this), _this._onSectionRendered = _this._onSectionRendered.bind(_this),
+ _this;
+ }
+ return (0, _inherits3.default)(Table, _Component), (0, _createClass3.default)(Table, [ {
+ key: "forceUpdateGrid",
+ value: function() {
+ this.Grid.forceUpdate();
+ }
+ }, {
+ key: "measureAllRows",
+ value: function() {
+ this.Grid.measureAllCells();
+ }
+ }, {
+ key: "recomputeRowHeights",
+ value: function() {
+ var index = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : 0;
+ this.Grid.recomputeGridSize({
+ rowIndex: index
+ }), this.forceUpdateGrid();
+ }
+ }, {
+ key: "componentDidMount",
+ value: function() {
+ this._setScrollbarWidth();
+ }
+ }, {
+ key: "componentDidUpdate",
+ value: function() {
+ this._setScrollbarWidth();
+ }
+ }, {
+ key: "render",
+ value: function() {
+ var _this2 = this, _props = this.props, children = _props.children, className = _props.className, disableHeader = _props.disableHeader, gridClassName = _props.gridClassName, gridStyle = _props.gridStyle, headerHeight = _props.headerHeight, height = _props.height, id = _props.id, noRowsRenderer = _props.noRowsRenderer, rowClassName = _props.rowClassName, rowStyle = _props.rowStyle, scrollToIndex = _props.scrollToIndex, style = _props.style, width = _props.width, scrollbarWidth = this.state.scrollbarWidth, availableRowsHeight = height - headerHeight, rowClass = rowClassName instanceof Function ? rowClassName({
+ index: -1
+ }) : rowClassName, rowStyleObject = rowStyle instanceof Function ? rowStyle({
+ index: -1
+ }) : rowStyle;
+ return this._cachedColumnStyles = [], _react2.default.Children.toArray(children).forEach(function(column, index) {
+ var flexStyles = _this2._getFlexStyleForColumn(column, column.props.style);
+ _this2._cachedColumnStyles[index] = (0, _extends3.default)({}, flexStyles, {
+ overflow: "hidden"
+ });
+ }), _react2.default.createElement("div", {
+ className: (0, _classnames2.default)("ReactVirtualized__Table", className),
+ id: id,
+ style: style
+ }, !disableHeader && _react2.default.createElement("div", {
+ className: (0, _classnames2.default)("ReactVirtualized__Table__headerRow", rowClass),
+ style: (0, _extends3.default)({}, rowStyleObject, {
+ height: headerHeight,
+ overflow: "hidden",
+ paddingRight: scrollbarWidth,
+ width: width
+ })
+ }, this._getRenderedHeaderRow()), _react2.default.createElement(_Grid2.default, (0,
+ _extends3.default)({}, this.props, {
+ autoContainerWidth: !0,
+ className: (0, _classnames2.default)("ReactVirtualized__Table__Grid", gridClassName),
+ cellRenderer: this._createRow,
+ columnWidth: width,
+ columnCount: 1,
+ height: availableRowsHeight,
+ id: void 0,
+ noContentRenderer: noRowsRenderer,
+ onScroll: this._onScroll,
+ onSectionRendered: this._onSectionRendered,
+ ref: function(_ref) {
+ _this2.Grid = _ref;
+ },
+ scrollbarWidth: scrollbarWidth,
+ scrollToRow: scrollToIndex,
+ style: (0, _extends3.default)({}, gridStyle, {
+ overflowX: "hidden"
+ })
+ })));
+ }
+ }, {
+ key: "shouldComponentUpdate",
+ value: function(nextProps, nextState) {
+ return (0, _reactAddonsShallowCompare2.default)(this, nextProps, nextState);
+ }
+ }, {
+ key: "_createColumn",
+ value: function(_ref2) {
+ var column = _ref2.column, columnIndex = _ref2.columnIndex, isScrolling = _ref2.isScrolling, rowData = _ref2.rowData, rowIndex = _ref2.rowIndex, _column$props = column.props, cellDataGetter = _column$props.cellDataGetter, cellRenderer = _column$props.cellRenderer, className = _column$props.className, columnData = _column$props.columnData, dataKey = _column$props.dataKey, cellData = cellDataGetter({
+ columnData: columnData,
+ dataKey: dataKey,
+ rowData: rowData
+ }), renderedCell = cellRenderer({
+ cellData: cellData,
+ columnData: columnData,
+ dataKey: dataKey,
+ isScrolling: isScrolling,
+ rowData: rowData,
+ rowIndex: rowIndex
+ }), style = this._cachedColumnStyles[columnIndex], title = "string" == typeof renderedCell ? renderedCell : null;
+ return _react2.default.createElement("div", {
+ key: "Row" + rowIndex + "-Col" + columnIndex,
+ className: (0, _classnames2.default)("ReactVirtualized__Table__rowColumn", className),
+ style: style,
+ title: title
+ }, renderedCell);
+ }
+ }, {
+ key: "_createHeader",
+ value: function(_ref3) {
+ var column = _ref3.column, index = _ref3.index, _props2 = this.props, headerClassName = _props2.headerClassName, headerStyle = _props2.headerStyle, onHeaderClick = _props2.onHeaderClick, sort = _props2.sort, sortBy = _props2.sortBy, sortDirection = _props2.sortDirection, _column$props2 = column.props, dataKey = _column$props2.dataKey, disableSort = _column$props2.disableSort, headerRenderer = _column$props2.headerRenderer, label = _column$props2.label, columnData = _column$props2.columnData, sortEnabled = !disableSort && sort, classNames = (0,
+ _classnames2.default)("ReactVirtualized__Table__headerColumn", headerClassName, column.props.headerClassName, {
+ ReactVirtualized__Table__sortableHeaderColumn: sortEnabled
+ }), style = this._getFlexStyleForColumn(column, headerStyle), renderedHeader = headerRenderer({
+ columnData: columnData,
+ dataKey: dataKey,
+ disableSort: disableSort,
+ label: label,
+ sortBy: sortBy,
+ sortDirection: sortDirection
+ }), a11yProps = {};
+ return (sortEnabled || onHeaderClick) && !function() {
+ var newSortDirection = sortBy !== dataKey || sortDirection === _SortDirection2.default.DESC ? _SortDirection2.default.ASC : _SortDirection2.default.DESC, onClick = function() {
+ sortEnabled && sort({
+ sortBy: dataKey,
+ sortDirection: newSortDirection
+ }), onHeaderClick && onHeaderClick({
+ columnData: columnData,
+ dataKey: dataKey
+ });
+ }, onKeyDown = function(event) {
+ "Enter" !== event.key && " " !== event.key || onClick();
+ };
+ a11yProps["aria-label"] = column.props["aria-label"] || label || dataKey, a11yProps.role = "rowheader",
+ a11yProps.tabIndex = 0, a11yProps.onClick = onClick, a11yProps.onKeyDown = onKeyDown;
+ }(), _react2.default.createElement("div", (0, _extends3.default)({}, a11yProps, {
+ key: "Header-Col" + index,
+ className: classNames,
+ style: style
+ }), renderedHeader);
+ }
+ }, {
+ key: "_createRow",
+ value: function(_ref4) {
+ var _this3 = this, index = _ref4.rowIndex, isScrolling = _ref4.isScrolling, key = _ref4.key, style = _ref4.style, _props3 = this.props, children = _props3.children, onRowClick = _props3.onRowClick, onRowDoubleClick = _props3.onRowDoubleClick, onRowMouseOver = _props3.onRowMouseOver, onRowMouseOut = _props3.onRowMouseOut, rowClassName = _props3.rowClassName, rowGetter = _props3.rowGetter, rowRenderer = _props3.rowRenderer, rowStyle = _props3.rowStyle, scrollbarWidth = this.state.scrollbarWidth, rowClass = rowClassName instanceof Function ? rowClassName({
+ index: index
+ }) : rowClassName, rowStyleObject = rowStyle instanceof Function ? rowStyle({
+ index: index
+ }) : rowStyle, rowData = rowGetter({
+ index: index
+ }), columns = _react2.default.Children.toArray(children).map(function(column, columnIndex) {
+ return _this3._createColumn({
+ column: column,
+ columnIndex: columnIndex,
+ isScrolling: isScrolling,
+ rowData: rowData,
+ rowIndex: index,
+ scrollbarWidth: scrollbarWidth
+ });
+ }), className = (0, _classnames2.default)("ReactVirtualized__Table__row", rowClass), flattenedStyle = (0,
+ _extends3.default)({}, style, rowStyleObject, {
+ height: this._getRowHeight(index),
+ overflow: "hidden",
+ paddingRight: scrollbarWidth
+ });
+ return rowRenderer({
+ className: className,
+ columns: columns,
+ index: index,
+ isScrolling: isScrolling,
+ key: key,
+ onRowClick: onRowClick,
+ onRowDoubleClick: onRowDoubleClick,
+ onRowMouseOver: onRowMouseOver,
+ onRowMouseOut: onRowMouseOut,
+ rowData: rowData,
+ style: flattenedStyle
+ });
+ }
+ }, {
+ key: "_getFlexStyleForColumn",
+ value: function(column) {
+ var customStyle = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : {}, flexValue = column.props.flexGrow + " " + column.props.flexShrink + " " + column.props.width + "px", style = (0,
+ _extends3.default)({}, customStyle, {
+ flex: flexValue,
+ msFlex: flexValue,
+ WebkitFlex: flexValue
+ });
+ return column.props.maxWidth && (style.maxWidth = column.props.maxWidth), column.props.minWidth && (style.minWidth = column.props.minWidth),
+ style;
+ }
+ }, {
+ key: "_getRenderedHeaderRow",
+ value: function() {
+ var _this4 = this, _props4 = this.props, children = _props4.children, disableHeader = _props4.disableHeader, items = disableHeader ? [] : _react2.default.Children.toArray(children);
+ return items.map(function(column, index) {
+ return _this4._createHeader({
+ column: column,
+ index: index
+ });
+ });
+ }
+ }, {
+ key: "_getRowHeight",
+ value: function(rowIndex) {
+ var rowHeight = this.props.rowHeight;
+ return rowHeight instanceof Function ? rowHeight({
+ index: rowIndex
+ }) : rowHeight;
+ }
+ }, {
+ key: "_onScroll",
+ value: function(_ref5) {
+ var clientHeight = _ref5.clientHeight, scrollHeight = _ref5.scrollHeight, scrollTop = _ref5.scrollTop, onScroll = this.props.onScroll;
+ onScroll({
+ clientHeight: clientHeight,
+ scrollHeight: scrollHeight,
+ scrollTop: scrollTop
+ });
+ }
+ }, {
+ key: "_onSectionRendered",
+ value: function(_ref6) {
+ var rowOverscanStartIndex = _ref6.rowOverscanStartIndex, rowOverscanStopIndex = _ref6.rowOverscanStopIndex, rowStartIndex = _ref6.rowStartIndex, rowStopIndex = _ref6.rowStopIndex, onRowsRendered = this.props.onRowsRendered;
+ onRowsRendered({
+ overscanStartIndex: rowOverscanStartIndex,
+ overscanStopIndex: rowOverscanStopIndex,
+ startIndex: rowStartIndex,
+ stopIndex: rowStopIndex
+ });
+ }
+ }, {
+ key: "_setScrollbarWidth",
+ value: function() {
+ var Grid = (0, _reactDom.findDOMNode)(this.Grid), clientWidth = Grid.clientWidth || 0, offsetWidth = Grid.offsetWidth || 0, scrollbarWidth = offsetWidth - clientWidth;
+ this.setState({
+ scrollbarWidth: scrollbarWidth
+ });
+ }
+ } ]), Table;
+ }(_react.Component);
+ Table.defaultProps = {
+ disableHeader: !1,
+ estimatedRowSize: 30,
+ headerHeight: 0,
+ headerStyle: {},
+ noRowsRenderer: function() {
+ return null;
+ },
+ onRowsRendered: function() {
+ return null;
+ },
+ onScroll: function() {
+ return null;
+ },
+ overscanRowCount: 10,
+ rowRenderer: _defaultRowRenderer2.default,
+ rowStyle: {},
+ scrollToAlignment: "auto",
+ style: {}
+ }, exports.default = Table;
+ }, /* 130 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ });
+ var _getPrototypeOf = __webpack_require__(3), _getPrototypeOf2 = _interopRequireDefault(_getPrototypeOf), _classCallCheck2 = __webpack_require__(29), _classCallCheck3 = _interopRequireDefault(_classCallCheck2), _possibleConstructorReturn2 = __webpack_require__(34), _possibleConstructorReturn3 = _interopRequireDefault(_possibleConstructorReturn2), _inherits2 = __webpack_require__(81), _inherits3 = _interopRequireDefault(_inherits2), _react = __webpack_require__(89), _defaultHeaderRenderer = __webpack_require__(131), _defaultHeaderRenderer2 = _interopRequireDefault(_defaultHeaderRenderer), _defaultCellRenderer = __webpack_require__(134), _defaultCellRenderer2 = _interopRequireDefault(_defaultCellRenderer), _defaultCellDataGetter = __webpack_require__(135), _defaultCellDataGetter2 = _interopRequireDefault(_defaultCellDataGetter), Column = function(_Component) {
+ function Column() {
+ return (0, _classCallCheck3.default)(this, Column), (0, _possibleConstructorReturn3.default)(this, (Column.__proto__ || (0,
+ _getPrototypeOf2.default)(Column)).apply(this, arguments));
+ }
+ return (0, _inherits3.default)(Column, _Component), Column;
+ }(_react.Component);
+ Column.defaultProps = {
+ cellDataGetter: _defaultCellDataGetter2.default,
+ cellRenderer: _defaultCellRenderer2.default,
+ flexGrow: 0,
+ flexShrink: 1,
+ headerRenderer: _defaultHeaderRenderer2.default,
+ style: {}
+ }, exports.default = Column;
+ }, /* 131 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ function defaultHeaderRenderer(_ref) {
+ var dataKey = (_ref.columnData, _ref.dataKey), label = (_ref.disableSort, _ref.label), sortBy = _ref.sortBy, sortDirection = _ref.sortDirection, showSortIndicator = sortBy === dataKey, children = [ _react2.default.createElement("span", {
+ className: "ReactVirtualized__Table__headerTruncatedText",
+ key: "label",
+ title: label
+ }, label) ];
+ return showSortIndicator && children.push(_react2.default.createElement(_SortIndicator2.default, {
+ key: "SortIndicator",
+ sortDirection: sortDirection
+ })), children;
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ }), exports.default = defaultHeaderRenderer;
+ var _react = __webpack_require__(89), _react2 = _interopRequireDefault(_react), _SortIndicator = __webpack_require__(132), _SortIndicator2 = _interopRequireDefault(_SortIndicator);
+ }, /* 132 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ function SortIndicator(_ref) {
+ var sortDirection = _ref.sortDirection, classNames = (0, _classnames2.default)("ReactVirtualized__Table__sortableHeaderIcon", {
+ "ReactVirtualized__Table__sortableHeaderIcon--ASC": sortDirection === _SortDirection2.default.ASC,
+ "ReactVirtualized__Table__sortableHeaderIcon--DESC": sortDirection === _SortDirection2.default.DESC
+ });
+ return _react2.default.createElement("svg", {
+ className: classNames,
+ width: 18,
+ height: 18,
+ viewBox: "0 0 24 24"
+ }, sortDirection === _SortDirection2.default.ASC ? _react2.default.createElement("path", {
+ d: "M7 14l5-5 5 5z"
+ }) : _react2.default.createElement("path", {
+ d: "M7 10l5 5 5-5z"
+ }), _react2.default.createElement("path", {
+ d: "M0 0h24v24H0z",
+ fill: "none"
+ }));
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ }), exports.default = SortIndicator;
+ var _react = __webpack_require__(89), _react2 = _interopRequireDefault(_react), _classnames = __webpack_require__(107), _classnames2 = _interopRequireDefault(_classnames), _SortDirection = __webpack_require__(133), _SortDirection2 = _interopRequireDefault(_SortDirection);
+ }, /* 133 */
+ /***/
+ function(module, exports) {
+ "use strict";
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ });
+ var SortDirection = {
+ ASC: "ASC",
+ DESC: "DESC"
+ };
+ exports.default = SortDirection;
+ }, /* 134 */
+ /***/
+ function(module, exports) {
+ "use strict";
+ function defaultCellRenderer(_ref) {
+ var cellData = _ref.cellData;
+ _ref.cellDataKey, _ref.columnData, _ref.rowData, _ref.rowIndex;
+ return null == cellData ? "" : String(cellData);
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ }), exports.default = defaultCellRenderer;
+ }, /* 135 */
+ /***/
+ function(module, exports) {
+ "use strict";
+ function defaultCellDataGetter(_ref) {
+ var dataKey = (_ref.columnData, _ref.dataKey), rowData = _ref.rowData;
+ return rowData.get instanceof Function ? rowData.get(dataKey) : rowData[dataKey];
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ }), exports.default = defaultCellDataGetter;
+ }, /* 136 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ function defaultRowRenderer(_ref) {
+ var className = _ref.className, columns = _ref.columns, index = _ref.index, key = (_ref.isScrolling,
+ _ref.key), onRowClick = _ref.onRowClick, onRowDoubleClick = _ref.onRowDoubleClick, onRowMouseOver = _ref.onRowMouseOver, onRowMouseOut = _ref.onRowMouseOut, style = (_ref.rowData,
+ _ref.style), a11yProps = {};
+ return (onRowClick || onRowDoubleClick || onRowMouseOver || onRowMouseOut) && (a11yProps["aria-label"] = "row",
+ a11yProps.role = "row", a11yProps.tabIndex = 0, onRowClick && (a11yProps.onClick = function() {
+ return onRowClick({
+ index: index
+ });
+ }), onRowDoubleClick && (a11yProps.onDoubleClick = function() {
+ return onRowDoubleClick({
+ index: index
+ });
+ }), onRowMouseOut && (a11yProps.onMouseOut = function() {
+ return onRowMouseOut({
+ index: index
+ });
+ }), onRowMouseOver && (a11yProps.onMouseOver = function() {
+ return onRowMouseOver({
+ index: index
+ });
+ })), _react2.default.createElement("div", (0, _extends3.default)({}, a11yProps, {
+ className: className,
+ key: key,
+ style: style
+ }), columns);
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ });
+ var _extends2 = __webpack_require__(100), _extends3 = _interopRequireDefault(_extends2);
+ exports.default = defaultRowRenderer;
+ var _react = __webpack_require__(89), _react2 = _interopRequireDefault(_react);
+ }, /* 137 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ }), exports.InfiniteLoader = exports.default = void 0;
+ var _InfiniteLoader2 = __webpack_require__(138), _InfiniteLoader3 = _interopRequireDefault(_InfiniteLoader2);
+ exports.default = _InfiniteLoader3.default, exports.InfiniteLoader = _InfiniteLoader3.default;
+ }, /* 138 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ function isRangeVisible(_ref2) {
+ var lastRenderedStartIndex = _ref2.lastRenderedStartIndex, lastRenderedStopIndex = _ref2.lastRenderedStopIndex, startIndex = _ref2.startIndex, stopIndex = _ref2.stopIndex;
+ return !(startIndex > lastRenderedStopIndex || stopIndex < lastRenderedStartIndex);
+ }
+ function scanForUnloadedRanges(_ref3) {
+ for (var isRowLoaded = _ref3.isRowLoaded, minimumBatchSize = _ref3.minimumBatchSize, rowCount = _ref3.rowCount, startIndex = _ref3.startIndex, stopIndex = _ref3.stopIndex, unloadedRanges = [], rangeStartIndex = null, rangeStopIndex = null, index = startIndex; index <= stopIndex; index++) {
+ var loaded = isRowLoaded({
+ index: index
+ });
+ loaded ? null !== rangeStopIndex && (unloadedRanges.push({
+ startIndex: rangeStartIndex,
+ stopIndex: rangeStopIndex
+ }), rangeStartIndex = rangeStopIndex = null) : (rangeStopIndex = index, null === rangeStartIndex && (rangeStartIndex = index));
+ }
+ if (null !== rangeStopIndex) {
+ for (var potentialStopIndex = Math.min(Math.max(rangeStopIndex, rangeStartIndex + minimumBatchSize - 1), rowCount - 1), _index = rangeStopIndex + 1; _index <= potentialStopIndex && !isRowLoaded({
+ index: _index
+ }); _index++) rangeStopIndex = _index;
+ unloadedRanges.push({
+ startIndex: rangeStartIndex,
+ stopIndex: rangeStopIndex
+ });
+ }
+ if (unloadedRanges.length) for (var firstUnloadedRange = unloadedRanges[0]; firstUnloadedRange.stopIndex - firstUnloadedRange.startIndex + 1 < minimumBatchSize && firstUnloadedRange.startIndex > 0; ) {
+ var _index2 = firstUnloadedRange.startIndex - 1;
+ if (isRowLoaded({
+ index: _index2
+ })) break;
+ firstUnloadedRange.startIndex = _index2;
+ }
+ return unloadedRanges;
+ }
+ function forceUpdateReactVirtualizedComponent(component) {
+ "function" == typeof component.forceUpdateGrid ? component.forceUpdateGrid() : component.forceUpdate();
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ });
+ var _getPrototypeOf = __webpack_require__(3), _getPrototypeOf2 = _interopRequireDefault(_getPrototypeOf), _classCallCheck2 = __webpack_require__(29), _classCallCheck3 = _interopRequireDefault(_classCallCheck2), _createClass2 = __webpack_require__(30), _createClass3 = _interopRequireDefault(_createClass2), _possibleConstructorReturn2 = __webpack_require__(34), _possibleConstructorReturn3 = _interopRequireDefault(_possibleConstructorReturn2), _inherits2 = __webpack_require__(81), _inherits3 = _interopRequireDefault(_inherits2);
+ exports.isRangeVisible = isRangeVisible, exports.scanForUnloadedRanges = scanForUnloadedRanges,
+ exports.forceUpdateReactVirtualizedComponent = forceUpdateReactVirtualizedComponent;
+ var _react = __webpack_require__(89), _reactAddonsShallowCompare = __webpack_require__(90), _reactAddonsShallowCompare2 = _interopRequireDefault(_reactAddonsShallowCompare), _createCallbackMemoizer = __webpack_require__(108), _createCallbackMemoizer2 = _interopRequireDefault(_createCallbackMemoizer), InfiniteLoader = function(_Component) {
+ function InfiniteLoader(props, context) {
+ (0, _classCallCheck3.default)(this, InfiniteLoader);
+ var _this = (0, _possibleConstructorReturn3.default)(this, (InfiniteLoader.__proto__ || (0,
+ _getPrototypeOf2.default)(InfiniteLoader)).call(this, props, context));
+ return _this._loadMoreRowsMemoizer = (0, _createCallbackMemoizer2.default)(), _this._onRowsRendered = _this._onRowsRendered.bind(_this),
+ _this._registerChild = _this._registerChild.bind(_this), _this;
+ }
+ return (0, _inherits3.default)(InfiniteLoader, _Component), (0, _createClass3.default)(InfiniteLoader, [ {
+ key: "render",
+ value: function() {
+ var children = this.props.children;
+ return children({
+ onRowsRendered: this._onRowsRendered,
+ registerChild: this._registerChild
+ });
+ }
+ }, {
+ key: "shouldComponentUpdate",
+ value: function(nextProps, nextState) {
+ return (0, _reactAddonsShallowCompare2.default)(this, nextProps, nextState);
+ }
+ }, {
+ key: "_loadUnloadedRanges",
+ value: function(unloadedRanges) {
+ var _this2 = this, loadMoreRows = this.props.loadMoreRows;
+ unloadedRanges.forEach(function(unloadedRange) {
+ var promise = loadMoreRows(unloadedRange);
+ promise && promise.then(function() {
+ isRangeVisible({
+ lastRenderedStartIndex: _this2._lastRenderedStartIndex,
+ lastRenderedStopIndex: _this2._lastRenderedStopIndex,
+ startIndex: unloadedRange.startIndex,
+ stopIndex: unloadedRange.stopIndex
+ }) && _this2._registeredChild && forceUpdateReactVirtualizedComponent(_this2._registeredChild);
+ });
+ });
+ }
+ }, {
+ key: "_onRowsRendered",
+ value: function(_ref) {
+ var _this3 = this, startIndex = _ref.startIndex, stopIndex = _ref.stopIndex, _props = this.props, isRowLoaded = _props.isRowLoaded, minimumBatchSize = _props.minimumBatchSize, rowCount = _props.rowCount, threshold = _props.threshold;
+ this._lastRenderedStartIndex = startIndex, this._lastRenderedStopIndex = stopIndex;
+ var unloadedRanges = scanForUnloadedRanges({
+ isRowLoaded: isRowLoaded,
+ minimumBatchSize: minimumBatchSize,
+ rowCount: rowCount,
+ startIndex: Math.max(0, startIndex - threshold),
+ stopIndex: Math.min(rowCount - 1, stopIndex + threshold)
+ }), squashedUnloadedRanges = unloadedRanges.reduce(function(reduced, unloadedRange) {
+ return reduced.concat([ unloadedRange.startIndex, unloadedRange.stopIndex ]);
+ }, []);
+ this._loadMoreRowsMemoizer({
+ callback: function() {
+ _this3._loadUnloadedRanges(unloadedRanges);
+ },
+ indices: {
+ squashedUnloadedRanges: squashedUnloadedRanges
+ }
+ });
+ }
+ }, {
+ key: "_registerChild",
+ value: function(registeredChild) {
+ this._registeredChild = registeredChild;
+ }
+ } ]), InfiniteLoader;
+ }(_react.Component);
+ InfiniteLoader.defaultProps = {
+ minimumBatchSize: 10,
+ rowCount: 0,
+ threshold: 15
+ }, exports.default = InfiniteLoader;
+ }, /* 139 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ }), exports.ScrollSync = exports.default = void 0;
+ var _ScrollSync2 = __webpack_require__(140), _ScrollSync3 = _interopRequireDefault(_ScrollSync2);
+ exports.default = _ScrollSync3.default, exports.ScrollSync = _ScrollSync3.default;
+ }, /* 140 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ });
+ var _getPrototypeOf = __webpack_require__(3), _getPrototypeOf2 = _interopRequireDefault(_getPrototypeOf), _classCallCheck2 = __webpack_require__(29), _classCallCheck3 = _interopRequireDefault(_classCallCheck2), _createClass2 = __webpack_require__(30), _createClass3 = _interopRequireDefault(_createClass2), _possibleConstructorReturn2 = __webpack_require__(34), _possibleConstructorReturn3 = _interopRequireDefault(_possibleConstructorReturn2), _inherits2 = __webpack_require__(81), _inherits3 = _interopRequireDefault(_inherits2), _react = __webpack_require__(89), _reactAddonsShallowCompare = __webpack_require__(90), _reactAddonsShallowCompare2 = _interopRequireDefault(_reactAddonsShallowCompare), ScrollSync = function(_Component) {
+ function ScrollSync(props, context) {
+ (0, _classCallCheck3.default)(this, ScrollSync);
+ var _this = (0, _possibleConstructorReturn3.default)(this, (ScrollSync.__proto__ || (0,
+ _getPrototypeOf2.default)(ScrollSync)).call(this, props, context));
+ return _this.state = {
+ clientHeight: 0,
+ clientWidth: 0,
+ scrollHeight: 0,
+ scrollLeft: 0,
+ scrollTop: 0,
+ scrollWidth: 0
+ }, _this._onScroll = _this._onScroll.bind(_this), _this;
+ }
+ return (0, _inherits3.default)(ScrollSync, _Component), (0, _createClass3.default)(ScrollSync, [ {
+ key: "render",
+ value: function() {
+ var children = this.props.children, _state = this.state, clientHeight = _state.clientHeight, clientWidth = _state.clientWidth, scrollHeight = _state.scrollHeight, scrollLeft = _state.scrollLeft, scrollTop = _state.scrollTop, scrollWidth = _state.scrollWidth;
+ return children({
+ clientHeight: clientHeight,
+ clientWidth: clientWidth,
+ onScroll: this._onScroll,
+ scrollHeight: scrollHeight,
+ scrollLeft: scrollLeft,
+ scrollTop: scrollTop,
+ scrollWidth: scrollWidth
+ });
+ }
+ }, {
+ key: "shouldComponentUpdate",
+ value: function(nextProps, nextState) {
+ return (0, _reactAddonsShallowCompare2.default)(this, nextProps, nextState);
+ }
+ }, {
+ key: "_onScroll",
+ value: function(_ref) {
+ var clientHeight = _ref.clientHeight, clientWidth = _ref.clientWidth, scrollHeight = _ref.scrollHeight, scrollLeft = _ref.scrollLeft, scrollTop = _ref.scrollTop, scrollWidth = _ref.scrollWidth;
+ this.setState({
+ clientHeight: clientHeight,
+ clientWidth: clientWidth,
+ scrollHeight: scrollHeight,
+ scrollLeft: scrollLeft,
+ scrollTop: scrollTop,
+ scrollWidth: scrollWidth
+ });
+ }
+ } ]), ScrollSync;
+ }(_react.Component);
+ exports.default = ScrollSync;
+ }, /* 141 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ }), exports.List = exports.default = void 0;
+ var _List2 = __webpack_require__(142), _List3 = _interopRequireDefault(_List2);
+ exports.default = _List3.default, exports.List = _List3.default;
+ }, /* 142 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ });
+ var _objectWithoutProperties2 = __webpack_require__(105), _objectWithoutProperties3 = _interopRequireDefault(_objectWithoutProperties2), _extends2 = __webpack_require__(100), _extends3 = _interopRequireDefault(_extends2), _getPrototypeOf = __webpack_require__(3), _getPrototypeOf2 = _interopRequireDefault(_getPrototypeOf), _classCallCheck2 = __webpack_require__(29), _classCallCheck3 = _interopRequireDefault(_classCallCheck2), _createClass2 = __webpack_require__(30), _createClass3 = _interopRequireDefault(_createClass2), _possibleConstructorReturn2 = __webpack_require__(34), _possibleConstructorReturn3 = _interopRequireDefault(_possibleConstructorReturn2), _inherits2 = __webpack_require__(81), _inherits3 = _interopRequireDefault(_inherits2), _Grid = __webpack_require__(120), _Grid2 = _interopRequireDefault(_Grid), _react = __webpack_require__(89), _react2 = _interopRequireDefault(_react), _classnames = __webpack_require__(107), _classnames2 = _interopRequireDefault(_classnames), _reactAddonsShallowCompare = __webpack_require__(90), _reactAddonsShallowCompare2 = _interopRequireDefault(_reactAddonsShallowCompare), List = function(_Component) {
+ function List(props, context) {
+ (0, _classCallCheck3.default)(this, List);
+ var _this = (0, _possibleConstructorReturn3.default)(this, (List.__proto__ || (0,
+ _getPrototypeOf2.default)(List)).call(this, props, context));
+ return _this._cellRenderer = _this._cellRenderer.bind(_this), _this._onScroll = _this._onScroll.bind(_this),
+ _this._onSectionRendered = _this._onSectionRendered.bind(_this), _this;
+ }
+ return (0, _inherits3.default)(List, _Component), (0, _createClass3.default)(List, [ {
+ key: "forceUpdateGrid",
+ value: function() {
+ this.Grid.forceUpdate();
+ }
+ }, {
+ key: "measureAllRows",
+ value: function() {
+ this.Grid.measureAllCells();
+ }
+ }, {
+ key: "recomputeRowHeights",
+ value: function() {
+ var index = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : 0;
+ this.Grid.recomputeGridSize({
+ rowIndex: index
+ }), this.forceUpdateGrid();
+ }
+ }, {
+ key: "render",
+ value: function() {
+ var _this2 = this, _props = this.props, className = _props.className, noRowsRenderer = _props.noRowsRenderer, scrollToIndex = _props.scrollToIndex, width = _props.width, classNames = (0,
+ _classnames2.default)("ReactVirtualized__List", className);
+ return _react2.default.createElement(_Grid2.default, (0, _extends3.default)({}, this.props, {
+ autoContainerWidth: !0,
+ cellRenderer: this._cellRenderer,
+ className: classNames,
+ columnWidth: width,
+ columnCount: 1,
+ noContentRenderer: noRowsRenderer,
+ onScroll: this._onScroll,
+ onSectionRendered: this._onSectionRendered,
+ ref: function(_ref) {
+ _this2.Grid = _ref;
+ },
+ scrollToRow: scrollToIndex
+ }));
+ }
+ }, {
+ key: "shouldComponentUpdate",
+ value: function(nextProps, nextState) {
+ return (0, _reactAddonsShallowCompare2.default)(this, nextProps, nextState);
+ }
+ }, {
+ key: "_cellRenderer",
+ value: function(_ref2) {
+ var rowIndex = _ref2.rowIndex, style = _ref2.style, rest = (0, _objectWithoutProperties3.default)(_ref2, [ "rowIndex", "style" ]), rowRenderer = this.props.rowRenderer;
+ return style.width = "100%", rowRenderer((0, _extends3.default)({
+ index: rowIndex,
+ style: style
+ }, rest));
+ }
+ }, {
+ key: "_onScroll",
+ value: function(_ref3) {
+ var clientHeight = _ref3.clientHeight, scrollHeight = _ref3.scrollHeight, scrollTop = _ref3.scrollTop, onScroll = this.props.onScroll;
+ onScroll({
+ clientHeight: clientHeight,
+ scrollHeight: scrollHeight,
+ scrollTop: scrollTop
+ });
+ }
+ }, {
+ key: "_onSectionRendered",
+ value: function(_ref4) {
+ var rowOverscanStartIndex = _ref4.rowOverscanStartIndex, rowOverscanStopIndex = _ref4.rowOverscanStopIndex, rowStartIndex = _ref4.rowStartIndex, rowStopIndex = _ref4.rowStopIndex, onRowsRendered = this.props.onRowsRendered;
+ onRowsRendered({
+ overscanStartIndex: rowOverscanStartIndex,
+ overscanStopIndex: rowOverscanStopIndex,
+ startIndex: rowStartIndex,
+ stopIndex: rowStopIndex
+ });
+ }
+ } ]), List;
+ }(_react.Component);
+ List.defaultProps = {
+ estimatedRowSize: 30,
+ noRowsRenderer: function() {
+ return null;
+ },
+ onRowsRendered: function() {
+ return null;
+ },
+ onScroll: function() {
+ return null;
+ },
+ overscanRowCount: 10,
+ scrollToAlignment: "auto",
+ style: {}
+ }, exports.default = List;
+ }, /* 143 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ }), exports.IS_SCROLLING_TIMEOUT = exports.WindowScroller = exports.default = void 0;
+ var _onScroll = __webpack_require__(144);
+ Object.defineProperty(exports, "IS_SCROLLING_TIMEOUT", {
+ enumerable: !0,
+ get: function() {
+ return _onScroll.IS_SCROLLING_TIMEOUT;
+ }
+ });
+ var _WindowScroller2 = __webpack_require__(145), _WindowScroller3 = _interopRequireDefault(_WindowScroller2);
+ exports.default = _WindowScroller3.default, exports.WindowScroller = _WindowScroller3.default;
+ }, /* 144 */
+ /***/
+ function(module, exports) {
+ "use strict";
+ function enablePointerEventsIfDisabled() {
+ disablePointerEventsTimeoutId && (disablePointerEventsTimeoutId = null, document.firstElementChild.style.pointerEvents = originalBodyPointerEvents,
+ originalBodyPointerEvents = null);
+ }
+ function enablePointerEventsAfterDelayCallback() {
+ enablePointerEventsIfDisabled(), mountedInstances.forEach(function(component) {
+ return component._enablePointerEventsAfterDelayCallback();
+ });
+ }
+ function enablePointerEventsAfterDelay() {
+ disablePointerEventsTimeoutId && clearTimeout(disablePointerEventsTimeoutId), disablePointerEventsTimeoutId = setTimeout(enablePointerEventsAfterDelayCallback, IS_SCROLLING_TIMEOUT);
+ }
+ function onScrollWindow(event) {
+ null == originalBodyPointerEvents && (originalBodyPointerEvents = document.firstElementChild.style.pointerEvents,
+ document.firstElementChild.style.pointerEvents = "none", enablePointerEventsAfterDelay()), mountedInstances.forEach(function(component) {
+ return component._onScrollWindow(event);
+ });
+ }
+ function registerScrollListener(component) {
+ mountedInstances.length || window.addEventListener("scroll", onScrollWindow), mountedInstances.push(component);
+ }
+ function unregisterScrollListener(component) {
+ mountedInstances = mountedInstances.filter(function(c) {
+ return c !== component;
+ }), mountedInstances.length || (window.removeEventListener("scroll", onScrollWindow),
+ disablePointerEventsTimeoutId && (clearTimeout(disablePointerEventsTimeoutId), enablePointerEventsIfDisabled()));
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ }), exports.registerScrollListener = registerScrollListener, exports.unregisterScrollListener = unregisterScrollListener;
+ var mountedInstances = [], originalBodyPointerEvents = null, disablePointerEventsTimeoutId = null, IS_SCROLLING_TIMEOUT = exports.IS_SCROLLING_TIMEOUT = 150;
+ }, /* 145 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ });
+ var _getPrototypeOf = __webpack_require__(3), _getPrototypeOf2 = _interopRequireDefault(_getPrototypeOf), _classCallCheck2 = __webpack_require__(29), _classCallCheck3 = _interopRequireDefault(_classCallCheck2), _createClass2 = __webpack_require__(30), _createClass3 = _interopRequireDefault(_createClass2), _possibleConstructorReturn2 = __webpack_require__(34), _possibleConstructorReturn3 = _interopRequireDefault(_possibleConstructorReturn2), _inherits2 = __webpack_require__(81), _inherits3 = _interopRequireDefault(_inherits2), _react = __webpack_require__(89), _react2 = _interopRequireDefault(_react), _reactDom = __webpack_require__(96), _reactDom2 = _interopRequireDefault(_reactDom), _reactAddonsShallowCompare = __webpack_require__(90), _reactAddonsShallowCompare2 = _interopRequireDefault(_reactAddonsShallowCompare), _onScroll = __webpack_require__(144), WindowScroller = function(_Component) {
+ function WindowScroller(props) {
+ (0, _classCallCheck3.default)(this, WindowScroller);
+ var _this = (0, _possibleConstructorReturn3.default)(this, (WindowScroller.__proto__ || (0,
+ _getPrototypeOf2.default)(WindowScroller)).call(this, props)), height = "undefined" != typeof window ? window.innerHeight : 0;
+ return _this.state = {
+ isScrolling: !1,
+ height: height,
+ scrollTop: 0
+ }, _this._onScrollWindow = _this._onScrollWindow.bind(_this), _this._onResizeWindow = _this._onResizeWindow.bind(_this),
+ _this._enablePointerEventsAfterDelayCallback = _this._enablePointerEventsAfterDelayCallback.bind(_this),
+ _this;
+ }
+ return (0, _inherits3.default)(WindowScroller, _Component), (0, _createClass3.default)(WindowScroller, [ {
+ key: "componentDidMount",
+ value: function() {
+ var height = this.state.height;
+ this._positionFromTop = _reactDom2.default.findDOMNode(this).getBoundingClientRect().top - document.documentElement.getBoundingClientRect().top,
+ height !== window.innerHeight && this.setState({
+ height: window.innerHeight
+ }), (0, _onScroll.registerScrollListener)(this), window.addEventListener("resize", this._onResizeWindow, !1);
+ }
+ }, {
+ key: "componentWillUnmount",
+ value: function() {
+ (0, _onScroll.unregisterScrollListener)(this), window.removeEventListener("resize", this._onResizeWindow, !1);
+ }
+ }, {
+ key: "render",
+ value: function() {
+ var children = this.props.children, _state = this.state, isScrolling = _state.isScrolling, scrollTop = _state.scrollTop, height = _state.height;
+ return _react2.default.createElement("div", null, children({
+ height: height,
+ isScrolling: isScrolling,
+ scrollTop: scrollTop
+ }));
+ }
+ }, {
+ key: "shouldComponentUpdate",
+ value: function(nextProps, nextState) {
+ return (0, _reactAddonsShallowCompare2.default)(this, nextProps, nextState);
+ }
+ }, {
+ key: "_enablePointerEventsAfterDelayCallback",
+ value: function() {
+ this.setState({
+ isScrolling: !1
+ });
+ }
+ }, {
+ key: "_onResizeWindow",
+ value: function(event) {
+ var onResize = this.props.onResize, height = window.innerHeight || 0;
+ this.setState({
+ height: height
+ }), onResize({
+ height: height
+ });
+ }
+ }, {
+ key: "_onScrollWindow",
+ value: function(event) {
+ var onScroll = this.props.onScroll, scrollY = "scrollY" in window ? window.scrollY : document.documentElement.scrollTop, scrollTop = Math.max(0, scrollY - this._positionFromTop);
+ this.setState({
+ isScrolling: !0,
+ scrollTop: scrollTop
+ }), onScroll({
+ scrollTop: scrollTop
+ });
+ }
+ } ]), WindowScroller;
+ }(_react.Component);
+ WindowScroller.defaultProps = {
+ onResize: function() {},
+ onScroll: function() {}
+ }, exports.default = WindowScroller;
+ } ]);
+});
+//# sourceMappingURL=react-virtualized.js.map \ No newline at end of file
diff --git a/devtools/client/shared/vendor/react.js b/devtools/client/shared/vendor/react.js
new file mode 100644
index 000000000..828dc5f13
--- /dev/null
+++ b/devtools/client/shared/vendor/react.js
@@ -0,0 +1,20763 @@
+ /**
+ * React (with addons) v0.14.6
+ */
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.React = f()}})(function(){var define,module,exports;return (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<r.length;o++)s(r[o]);return s})({1:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactWithAddons
+ */
+
+/**
+ * This module exists purely in the open source project, and is meant as a way
+ * to create a separate standalone build of React. This build has "addons", or
+ * functionality we've built and think might be useful but doesn't have a good
+ * place to live inside React core.
+ */
+
+'use strict';
+
+var LinkedStateMixin = _dereq_(22);
+var React = _dereq_(26);
+var ReactComponentWithPureRenderMixin = _dereq_(37);
+var ReactCSSTransitionGroup = _dereq_(29);
+var ReactFragment = _dereq_(64);
+var ReactTransitionGroup = _dereq_(94);
+var ReactUpdates = _dereq_(96);
+
+var cloneWithProps = _dereq_(118);
+var shallowCompare = _dereq_(140);
+var update = _dereq_(143);
+var warning = _dereq_(173);
+
+var warnedAboutBatchedUpdates = false;
+
+React.addons = {
+ CSSTransitionGroup: ReactCSSTransitionGroup,
+ LinkedStateMixin: LinkedStateMixin,
+ PureRenderMixin: ReactComponentWithPureRenderMixin,
+ TransitionGroup: ReactTransitionGroup,
+
+ batchedUpdates: function () {
+ if ("production" !== 'production') {
+ "production" !== 'production' ? warning(warnedAboutBatchedUpdates, 'React.addons.batchedUpdates is deprecated. Use ' + 'ReactDOM.unstable_batchedUpdates instead.') : undefined;
+ warnedAboutBatchedUpdates = true;
+ }
+ return ReactUpdates.batchedUpdates.apply(this, arguments);
+ },
+ cloneWithProps: cloneWithProps,
+ createFragment: ReactFragment.create,
+ shallowCompare: shallowCompare,
+ update: update
+};
+
+React.addons.TestUtils = _dereq_(91);
+
+if ("production" !== 'production') {
+ React.addons.Perf = _dereq_(55);
+}
+
+module.exports = React;
+},{"118":118,"140":140,"143":143,"173":173,"22":22,"26":26,"29":29,"37":37,"55":55,"64":64,"91":91,"94":94,"96":96}],2:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule AutoFocusUtils
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var ReactMount = _dereq_(72);
+
+var findDOMNode = _dereq_(122);
+var focusNode = _dereq_(155);
+
+var Mixin = {
+ componentDidMount: function () {
+ if (this.props.autoFocus) {
+ focusNode(findDOMNode(this));
+ }
+ }
+};
+
+var AutoFocusUtils = {
+ Mixin: Mixin,
+
+ focusDOMComponent: function () {
+ focusNode(ReactMount.getNode(this._rootNodeID));
+ }
+};
+
+module.exports = AutoFocusUtils;
+},{"122":122,"155":155,"72":72}],3:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015 Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule BeforeInputEventPlugin
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var EventConstants = _dereq_(15);
+var EventPropagators = _dereq_(19);
+var ExecutionEnvironment = _dereq_(147);
+var FallbackCompositionState = _dereq_(20);
+var SyntheticCompositionEvent = _dereq_(103);
+var SyntheticInputEvent = _dereq_(107);
+
+var keyOf = _dereq_(166);
+
+var END_KEYCODES = [9, 13, 27, 32]; // Tab, Return, Esc, Space
+var START_KEYCODE = 229;
+
+var canUseCompositionEvent = ExecutionEnvironment.canUseDOM && 'CompositionEvent' in window;
+
+var documentMode = null;
+if (ExecutionEnvironment.canUseDOM && 'documentMode' in document) {
+ documentMode = document.documentMode;
+}
+
+// Webkit offers a very useful `textInput` event that can be used to
+// directly represent `beforeInput`. The IE `textinput` event is not as
+// useful, so we don't use it.
+var canUseTextInputEvent = ExecutionEnvironment.canUseDOM && 'TextEvent' in window && !documentMode && !isPresto();
+
+// In IE9+, we have access to composition events, but the data supplied
+// by the native compositionend event may be incorrect. Japanese ideographic
+// spaces, for instance (\u3000) are not recorded correctly.
+var useFallbackCompositionData = ExecutionEnvironment.canUseDOM && (!canUseCompositionEvent || documentMode && documentMode > 8 && documentMode <= 11);
+
+/**
+ * Opera <= 12 includes TextEvent in window, but does not fire
+ * text input events. Rely on keypress instead.
+ */
+function isPresto() {
+ var opera = window.opera;
+ return typeof opera === 'object' && typeof opera.version === 'function' && parseInt(opera.version(), 10) <= 12;
+}
+
+var SPACEBAR_CODE = 32;
+var SPACEBAR_CHAR = String.fromCharCode(SPACEBAR_CODE);
+
+var topLevelTypes = EventConstants.topLevelTypes;
+
+// Events and their corresponding property names.
+var eventTypes = {
+ beforeInput: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onBeforeInput: null }),
+ captured: keyOf({ onBeforeInputCapture: null })
+ },
+ dependencies: [topLevelTypes.topCompositionEnd, topLevelTypes.topKeyPress, topLevelTypes.topTextInput, topLevelTypes.topPaste]
+ },
+ compositionEnd: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onCompositionEnd: null }),
+ captured: keyOf({ onCompositionEndCapture: null })
+ },
+ dependencies: [topLevelTypes.topBlur, topLevelTypes.topCompositionEnd, topLevelTypes.topKeyDown, topLevelTypes.topKeyPress, topLevelTypes.topKeyUp, topLevelTypes.topMouseDown]
+ },
+ compositionStart: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onCompositionStart: null }),
+ captured: keyOf({ onCompositionStartCapture: null })
+ },
+ dependencies: [topLevelTypes.topBlur, topLevelTypes.topCompositionStart, topLevelTypes.topKeyDown, topLevelTypes.topKeyPress, topLevelTypes.topKeyUp, topLevelTypes.topMouseDown]
+ },
+ compositionUpdate: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onCompositionUpdate: null }),
+ captured: keyOf({ onCompositionUpdateCapture: null })
+ },
+ dependencies: [topLevelTypes.topBlur, topLevelTypes.topCompositionUpdate, topLevelTypes.topKeyDown, topLevelTypes.topKeyPress, topLevelTypes.topKeyUp, topLevelTypes.topMouseDown]
+ }
+};
+
+// Track whether we've ever handled a keypress on the space key.
+var hasSpaceKeypress = false;
+
+/**
+ * Return whether a native keypress event is assumed to be a command.
+ * This is required because Firefox fires `keypress` events for key commands
+ * (cut, copy, select-all, etc.) even though no character is inserted.
+ */
+function isKeypressCommand(nativeEvent) {
+ return (nativeEvent.ctrlKey || nativeEvent.altKey || nativeEvent.metaKey) &&
+ // ctrlKey && altKey is equivalent to AltGr, and is not a command.
+ !(nativeEvent.ctrlKey && nativeEvent.altKey);
+}
+
+/**
+ * Translate native top level events into event types.
+ *
+ * @param {string} topLevelType
+ * @return {object}
+ */
+function getCompositionEventType(topLevelType) {
+ switch (topLevelType) {
+ case topLevelTypes.topCompositionStart:
+ return eventTypes.compositionStart;
+ case topLevelTypes.topCompositionEnd:
+ return eventTypes.compositionEnd;
+ case topLevelTypes.topCompositionUpdate:
+ return eventTypes.compositionUpdate;
+ }
+}
+
+/**
+ * Does our fallback best-guess model think this event signifies that
+ * composition has begun?
+ *
+ * @param {string} topLevelType
+ * @param {object} nativeEvent
+ * @return {boolean}
+ */
+function isFallbackCompositionStart(topLevelType, nativeEvent) {
+ return topLevelType === topLevelTypes.topKeyDown && nativeEvent.keyCode === START_KEYCODE;
+}
+
+/**
+ * Does our fallback mode think that this event is the end of composition?
+ *
+ * @param {string} topLevelType
+ * @param {object} nativeEvent
+ * @return {boolean}
+ */
+function isFallbackCompositionEnd(topLevelType, nativeEvent) {
+ switch (topLevelType) {
+ case topLevelTypes.topKeyUp:
+ // Command keys insert or clear IME input.
+ return END_KEYCODES.indexOf(nativeEvent.keyCode) !== -1;
+ case topLevelTypes.topKeyDown:
+ // Expect IME keyCode on each keydown. If we get any other
+ // code we must have exited earlier.
+ return nativeEvent.keyCode !== START_KEYCODE;
+ case topLevelTypes.topKeyPress:
+ case topLevelTypes.topMouseDown:
+ case topLevelTypes.topBlur:
+ // Events are not possible without cancelling IME.
+ return true;
+ default:
+ return false;
+ }
+}
+
+/**
+ * Google Input Tools provides composition data via a CustomEvent,
+ * with the `data` property populated in the `detail` object. If this
+ * is available on the event object, use it. If not, this is a plain
+ * composition event and we have nothing special to extract.
+ *
+ * @param {object} nativeEvent
+ * @return {?string}
+ */
+function getDataFromCustomEvent(nativeEvent) {
+ var detail = nativeEvent.detail;
+ if (typeof detail === 'object' && 'data' in detail) {
+ return detail.data;
+ }
+ return null;
+}
+
+// Track the current IME composition fallback object, if any.
+var currentComposition = null;
+
+/**
+ * @param {string} topLevelType Record from `EventConstants`.
+ * @param {DOMEventTarget} topLevelTarget The listening component root node.
+ * @param {string} topLevelTargetID ID of `topLevelTarget`.
+ * @param {object} nativeEvent Native browser event.
+ * @return {?object} A SyntheticCompositionEvent.
+ */
+function extractCompositionEvent(topLevelType, topLevelTarget, topLevelTargetID, nativeEvent, nativeEventTarget) {
+ var eventType;
+ var fallbackData;
+
+ if (canUseCompositionEvent) {
+ eventType = getCompositionEventType(topLevelType);
+ } else if (!currentComposition) {
+ if (isFallbackCompositionStart(topLevelType, nativeEvent)) {
+ eventType = eventTypes.compositionStart;
+ }
+ } else if (isFallbackCompositionEnd(topLevelType, nativeEvent)) {
+ eventType = eventTypes.compositionEnd;
+ }
+
+ if (!eventType) {
+ return null;
+ }
+
+ if (useFallbackCompositionData) {
+ // The current composition is stored statically and must not be
+ // overwritten while composition continues.
+ if (!currentComposition && eventType === eventTypes.compositionStart) {
+ currentComposition = FallbackCompositionState.getPooled(topLevelTarget);
+ } else if (eventType === eventTypes.compositionEnd) {
+ if (currentComposition) {
+ fallbackData = currentComposition.getData();
+ }
+ }
+ }
+
+ var event = SyntheticCompositionEvent.getPooled(eventType, topLevelTargetID, nativeEvent, nativeEventTarget);
+
+ if (fallbackData) {
+ // Inject data generated from fallback path into the synthetic event.
+ // This matches the property of native CompositionEventInterface.
+ event.data = fallbackData;
+ } else {
+ var customData = getDataFromCustomEvent(nativeEvent);
+ if (customData !== null) {
+ event.data = customData;
+ }
+ }
+
+ EventPropagators.accumulateTwoPhaseDispatches(event);
+ return event;
+}
+
+/**
+ * @param {string} topLevelType Record from `EventConstants`.
+ * @param {object} nativeEvent Native browser event.
+ * @return {?string} The string corresponding to this `beforeInput` event.
+ */
+function getNativeBeforeInputChars(topLevelType, nativeEvent) {
+ switch (topLevelType) {
+ case topLevelTypes.topCompositionEnd:
+ return getDataFromCustomEvent(nativeEvent);
+ case topLevelTypes.topKeyPress:
+ /**
+ * If native `textInput` events are available, our goal is to make
+ * use of them. However, there is a special case: the spacebar key.
+ * In Webkit, preventing default on a spacebar `textInput` event
+ * cancels character insertion, but it *also* causes the browser
+ * to fall back to its default spacebar behavior of scrolling the
+ * page.
+ *
+ * Tracking at:
+ * https://code.google.com/p/chromium/issues/detail?id=355103
+ *
+ * To avoid this issue, use the keypress event as if no `textInput`
+ * event is available.
+ */
+ var which = nativeEvent.which;
+ if (which !== SPACEBAR_CODE) {
+ return null;
+ }
+
+ hasSpaceKeypress = true;
+ return SPACEBAR_CHAR;
+
+ case topLevelTypes.topTextInput:
+ // Record the characters to be added to the DOM.
+ var chars = nativeEvent.data;
+
+ // If it's a spacebar character, assume that we have already handled
+ // it at the keypress level and bail immediately. Android Chrome
+ // doesn't give us keycodes, so we need to blacklist it.
+ if (chars === SPACEBAR_CHAR && hasSpaceKeypress) {
+ return null;
+ }
+
+ return chars;
+
+ default:
+ // For other native event types, do nothing.
+ return null;
+ }
+}
+
+/**
+ * For browsers that do not provide the `textInput` event, extract the
+ * appropriate string to use for SyntheticInputEvent.
+ *
+ * @param {string} topLevelType Record from `EventConstants`.
+ * @param {object} nativeEvent Native browser event.
+ * @return {?string} The fallback string for this `beforeInput` event.
+ */
+function getFallbackBeforeInputChars(topLevelType, nativeEvent) {
+ // If we are currently composing (IME) and using a fallback to do so,
+ // try to extract the composed characters from the fallback object.
+ if (currentComposition) {
+ if (topLevelType === topLevelTypes.topCompositionEnd || isFallbackCompositionEnd(topLevelType, nativeEvent)) {
+ var chars = currentComposition.getData();
+ FallbackCompositionState.release(currentComposition);
+ currentComposition = null;
+ return chars;
+ }
+ return null;
+ }
+
+ switch (topLevelType) {
+ case topLevelTypes.topPaste:
+ // If a paste event occurs after a keypress, throw out the input
+ // chars. Paste events should not lead to BeforeInput events.
+ return null;
+ case topLevelTypes.topKeyPress:
+ /**
+ * As of v27, Firefox may fire keypress events even when no character
+ * will be inserted. A few possibilities:
+ *
+ * - `which` is `0`. Arrow keys, Esc key, etc.
+ *
+ * - `which` is the pressed key code, but no char is available.
+ * Ex: 'AltGr + d` in Polish. There is no modified character for
+ * this key combination and no character is inserted into the
+ * document, but FF fires the keypress for char code `100` anyway.
+ * No `input` event will occur.
+ *
+ * - `which` is the pressed key code, but a command combination is
+ * being used. Ex: `Cmd+C`. No character is inserted, and no
+ * `input` event will occur.
+ */
+ if (nativeEvent.which && !isKeypressCommand(nativeEvent)) {
+ return String.fromCharCode(nativeEvent.which);
+ }
+ return null;
+ case topLevelTypes.topCompositionEnd:
+ return useFallbackCompositionData ? null : nativeEvent.data;
+ default:
+ return null;
+ }
+}
+
+/**
+ * Extract a SyntheticInputEvent for `beforeInput`, based on either native
+ * `textInput` or fallback behavior.
+ *
+ * @param {string} topLevelType Record from `EventConstants`.
+ * @param {DOMEventTarget} topLevelTarget The listening component root node.
+ * @param {string} topLevelTargetID ID of `topLevelTarget`.
+ * @param {object} nativeEvent Native browser event.
+ * @return {?object} A SyntheticInputEvent.
+ */
+function extractBeforeInputEvent(topLevelType, topLevelTarget, topLevelTargetID, nativeEvent, nativeEventTarget) {
+ var chars;
+
+ if (canUseTextInputEvent) {
+ chars = getNativeBeforeInputChars(topLevelType, nativeEvent);
+ } else {
+ chars = getFallbackBeforeInputChars(topLevelType, nativeEvent);
+ }
+
+ // If no characters are being inserted, no BeforeInput event should
+ // be fired.
+ if (!chars) {
+ return null;
+ }
+
+ var event = SyntheticInputEvent.getPooled(eventTypes.beforeInput, topLevelTargetID, nativeEvent, nativeEventTarget);
+
+ event.data = chars;
+ EventPropagators.accumulateTwoPhaseDispatches(event);
+ return event;
+}
+
+/**
+ * Create an `onBeforeInput` event to match
+ * http://www.w3.org/TR/2013/WD-DOM-Level-3-Events-20131105/#events-inputevents.
+ *
+ * This event plugin is based on the native `textInput` event
+ * available in Chrome, Safari, Opera, and IE. This event fires after
+ * `onKeyPress` and `onCompositionEnd`, but before `onInput`.
+ *
+ * `beforeInput` is spec'd but not implemented in any browsers, and
+ * the `input` event does not provide any useful information about what has
+ * actually been added, contrary to the spec. Thus, `textInput` is the best
+ * available event to identify the characters that have actually been inserted
+ * into the target node.
+ *
+ * This plugin is also responsible for emitting `composition` events, thus
+ * allowing us to share composition fallback code for both `beforeInput` and
+ * `composition` event types.
+ */
+var BeforeInputEventPlugin = {
+
+ eventTypes: eventTypes,
+
+ /**
+ * @param {string} topLevelType Record from `EventConstants`.
+ * @param {DOMEventTarget} topLevelTarget The listening component root node.
+ * @param {string} topLevelTargetID ID of `topLevelTarget`.
+ * @param {object} nativeEvent Native browser event.
+ * @return {*} An accumulation of synthetic events.
+ * @see {EventPluginHub.extractEvents}
+ */
+ extractEvents: function (topLevelType, topLevelTarget, topLevelTargetID, nativeEvent, nativeEventTarget) {
+ return [extractCompositionEvent(topLevelType, topLevelTarget, topLevelTargetID, nativeEvent, nativeEventTarget), extractBeforeInputEvent(topLevelType, topLevelTarget, topLevelTargetID, nativeEvent, nativeEventTarget)];
+ }
+};
+
+module.exports = BeforeInputEventPlugin;
+},{"103":103,"107":107,"147":147,"15":15,"166":166,"19":19,"20":20}],4:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule CSSProperty
+ */
+
+'use strict';
+
+/**
+ * CSS properties which accept numbers but are not in units of "px".
+ */
+var isUnitlessNumber = {
+ animationIterationCount: true,
+ boxFlex: true,
+ boxFlexGroup: true,
+ boxOrdinalGroup: true,
+ columnCount: true,
+ flex: true,
+ flexGrow: true,
+ flexPositive: true,
+ flexShrink: true,
+ flexNegative: true,
+ flexOrder: true,
+ fontWeight: true,
+ lineClamp: true,
+ lineHeight: true,
+ opacity: true,
+ order: true,
+ orphans: true,
+ tabSize: true,
+ widows: true,
+ zIndex: true,
+ zoom: true,
+
+ // SVG-related properties
+ fillOpacity: true,
+ stopOpacity: true,
+ strokeDashoffset: true,
+ strokeOpacity: true,
+ strokeWidth: true
+};
+
+/**
+ * @param {string} prefix vendor-specific prefix, eg: Webkit
+ * @param {string} key style name, eg: transitionDuration
+ * @return {string} style name prefixed with `prefix`, properly camelCased, eg:
+ * WebkitTransitionDuration
+ */
+function prefixKey(prefix, key) {
+ return prefix + key.charAt(0).toUpperCase() + key.substring(1);
+}
+
+/**
+ * Support style names that may come passed in prefixed by adding permutations
+ * of vendor prefixes.
+ */
+var prefixes = ['Webkit', 'ms', 'Moz', 'O'];
+
+// Using Object.keys here, or else the vanilla for-in loop makes IE8 go into an
+// infinite loop, because it iterates over the newly added props too.
+Object.keys(isUnitlessNumber).forEach(function (prop) {
+ prefixes.forEach(function (prefix) {
+ isUnitlessNumber[prefixKey(prefix, prop)] = isUnitlessNumber[prop];
+ });
+});
+
+/**
+ * Most style properties can be unset by doing .style[prop] = '' but IE8
+ * doesn't like doing that with shorthand properties so for the properties that
+ * IE8 breaks on, which are listed here, we instead unset each of the
+ * individual properties. See http://bugs.jquery.com/ticket/12385.
+ * The 4-value 'clock' properties like margin, padding, border-width seem to
+ * behave without any problems. Curiously, list-style works too without any
+ * special prodding.
+ */
+var shorthandPropertyExpansions = {
+ background: {
+ backgroundAttachment: true,
+ backgroundColor: true,
+ backgroundImage: true,
+ backgroundPositionX: true,
+ backgroundPositionY: true,
+ backgroundRepeat: true
+ },
+ backgroundPosition: {
+ backgroundPositionX: true,
+ backgroundPositionY: true
+ },
+ border: {
+ borderWidth: true,
+ borderStyle: true,
+ borderColor: true
+ },
+ borderBottom: {
+ borderBottomWidth: true,
+ borderBottomStyle: true,
+ borderBottomColor: true
+ },
+ borderLeft: {
+ borderLeftWidth: true,
+ borderLeftStyle: true,
+ borderLeftColor: true
+ },
+ borderRight: {
+ borderRightWidth: true,
+ borderRightStyle: true,
+ borderRightColor: true
+ },
+ borderTop: {
+ borderTopWidth: true,
+ borderTopStyle: true,
+ borderTopColor: true
+ },
+ font: {
+ fontStyle: true,
+ fontVariant: true,
+ fontWeight: true,
+ fontSize: true,
+ lineHeight: true,
+ fontFamily: true
+ },
+ outline: {
+ outlineWidth: true,
+ outlineStyle: true,
+ outlineColor: true
+ }
+};
+
+var CSSProperty = {
+ isUnitlessNumber: isUnitlessNumber,
+ shorthandPropertyExpansions: shorthandPropertyExpansions
+};
+
+module.exports = CSSProperty;
+},{}],5:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule CSSPropertyOperations
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var CSSProperty = _dereq_(4);
+var ExecutionEnvironment = _dereq_(147);
+var ReactPerf = _dereq_(78);
+
+var camelizeStyleName = _dereq_(149);
+var dangerousStyleValue = _dereq_(119);
+var hyphenateStyleName = _dereq_(160);
+var memoizeStringOnly = _dereq_(168);
+var warning = _dereq_(173);
+
+var processStyleName = memoizeStringOnly(function (styleName) {
+ return hyphenateStyleName(styleName);
+});
+
+var hasShorthandPropertyBug = false;
+var styleFloatAccessor = 'cssFloat';
+if (ExecutionEnvironment.canUseDOM) {
+ var tempStyle = document.createElementNS('http://www.w3.org/1999/xhtml', 'div').style;
+ try {
+ // IE8 throws "Invalid argument." if resetting shorthand style properties.
+ tempStyle.font = '';
+ } catch (e) {
+ hasShorthandPropertyBug = true;
+ }
+ // IE8 only supports accessing cssFloat (standard) as styleFloat
+ if (document.documentElement.style.cssFloat === undefined) {
+ styleFloatAccessor = 'styleFloat';
+ }
+}
+
+if ("production" !== 'production') {
+ // 'msTransform' is correct, but the other prefixes should be capitalized
+ var badVendoredStyleNamePattern = /^(?:webkit|moz|o)[A-Z]/;
+
+ // style values shouldn't contain a semicolon
+ var badStyleValueWithSemicolonPattern = /;\s*$/;
+
+ var warnedStyleNames = {};
+ var warnedStyleValues = {};
+
+ var warnHyphenatedStyleName = function (name) {
+ if (warnedStyleNames.hasOwnProperty(name) && warnedStyleNames[name]) {
+ return;
+ }
+
+ warnedStyleNames[name] = true;
+ "production" !== 'production' ? warning(false, 'Unsupported style property %s. Did you mean %s?', name, camelizeStyleName(name)) : undefined;
+ };
+
+ var warnBadVendoredStyleName = function (name) {
+ if (warnedStyleNames.hasOwnProperty(name) && warnedStyleNames[name]) {
+ return;
+ }
+
+ warnedStyleNames[name] = true;
+ "production" !== 'production' ? warning(false, 'Unsupported vendor-prefixed style property %s. Did you mean %s?', name, name.charAt(0).toUpperCase() + name.slice(1)) : undefined;
+ };
+
+ var warnStyleValueWithSemicolon = function (name, value) {
+ if (warnedStyleValues.hasOwnProperty(value) && warnedStyleValues[value]) {
+ return;
+ }
+
+ warnedStyleValues[value] = true;
+ "production" !== 'production' ? warning(false, 'Style property values shouldn\'t contain a semicolon. ' + 'Try "%s: %s" instead.', name, value.replace(badStyleValueWithSemicolonPattern, '')) : undefined;
+ };
+
+ /**
+ * @param {string} name
+ * @param {*} value
+ */
+ var warnValidStyle = function (name, value) {
+ if (name.indexOf('-') > -1) {
+ warnHyphenatedStyleName(name);
+ } else if (badVendoredStyleNamePattern.test(name)) {
+ warnBadVendoredStyleName(name);
+ } else if (badStyleValueWithSemicolonPattern.test(value)) {
+ warnStyleValueWithSemicolon(name, value);
+ }
+ };
+}
+
+/**
+ * Operations for dealing with CSS properties.
+ */
+var CSSPropertyOperations = {
+
+ /**
+ * Serializes a mapping of style properties for use as inline styles:
+ *
+ * > createMarkupForStyles({width: '200px', height: 0})
+ * "width:200px;height:0;"
+ *
+ * Undefined values are ignored so that declarative programming is easier.
+ * The result should be HTML-escaped before insertion into the DOM.
+ *
+ * @param {object} styles
+ * @return {?string}
+ */
+ createMarkupForStyles: function (styles) {
+ var serialized = '';
+ for (var styleName in styles) {
+ if (!styles.hasOwnProperty(styleName)) {
+ continue;
+ }
+ var styleValue = styles[styleName];
+ if ("production" !== 'production') {
+ warnValidStyle(styleName, styleValue);
+ }
+ if (styleValue != null) {
+ serialized += processStyleName(styleName) + ':';
+ serialized += dangerousStyleValue(styleName, styleValue) + ';';
+ }
+ }
+ return serialized || null;
+ },
+
+ /**
+ * Sets the value for multiple styles on a node. If a value is specified as
+ * '' (empty string), the corresponding style property will be unset.
+ *
+ * @param {DOMElement} node
+ * @param {object} styles
+ */
+ setValueForStyles: function (node, styles) {
+ var style = node.style;
+ for (var styleName in styles) {
+ if (!styles.hasOwnProperty(styleName)) {
+ continue;
+ }
+ if ("production" !== 'production') {
+ warnValidStyle(styleName, styles[styleName]);
+ }
+ var styleValue = dangerousStyleValue(styleName, styles[styleName]);
+ if (styleName === 'float') {
+ styleName = styleFloatAccessor;
+ }
+ if (styleValue) {
+ style[styleName] = styleValue;
+ } else {
+ var expansion = hasShorthandPropertyBug && CSSProperty.shorthandPropertyExpansions[styleName];
+ if (expansion) {
+ // Shorthand property that IE8 won't like unsetting, so unset each
+ // component to placate it
+ for (var individualStyleName in expansion) {
+ style[individualStyleName] = '';
+ }
+ } else {
+ style[styleName] = '';
+ }
+ }
+ }
+ }
+
+};
+
+ReactPerf.measureMethods(CSSPropertyOperations, 'CSSPropertyOperations', {
+ setValueForStyles: 'setValueForStyles'
+});
+
+module.exports = CSSPropertyOperations;
+},{"119":119,"147":147,"149":149,"160":160,"168":168,"173":173,"4":4,"78":78}],6:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule CallbackQueue
+ */
+
+'use strict';
+
+var PooledClass = _dereq_(25);
+
+var assign = _dereq_(24);
+var invariant = _dereq_(161);
+
+/**
+ * A specialized pseudo-event module to help keep track of components waiting to
+ * be notified when their DOM representations are available for use.
+ *
+ * This implements `PooledClass`, so you should never need to instantiate this.
+ * Instead, use `CallbackQueue.getPooled()`.
+ *
+ * @class ReactMountReady
+ * @implements PooledClass
+ * @internal
+ */
+function CallbackQueue() {
+ this._callbacks = null;
+ this._contexts = null;
+}
+
+assign(CallbackQueue.prototype, {
+
+ /**
+ * Enqueues a callback to be invoked when `notifyAll` is invoked.
+ *
+ * @param {function} callback Invoked when `notifyAll` is invoked.
+ * @param {?object} context Context to call `callback` with.
+ * @internal
+ */
+ enqueue: function (callback, context) {
+ this._callbacks = this._callbacks || [];
+ this._contexts = this._contexts || [];
+ this._callbacks.push(callback);
+ this._contexts.push(context);
+ },
+
+ /**
+ * Invokes all enqueued callbacks and clears the queue. This is invoked after
+ * the DOM representation of a component has been created or updated.
+ *
+ * @internal
+ */
+ notifyAll: function () {
+ var callbacks = this._callbacks;
+ var contexts = this._contexts;
+ if (callbacks) {
+ !(callbacks.length === contexts.length) ? "production" !== 'production' ? invariant(false, 'Mismatched list of contexts in callback queue') : invariant(false) : undefined;
+ this._callbacks = null;
+ this._contexts = null;
+ for (var i = 0; i < callbacks.length; i++) {
+ callbacks[i].call(contexts[i]);
+ }
+ callbacks.length = 0;
+ contexts.length = 0;
+ }
+ },
+
+ /**
+ * Resets the internal queue.
+ *
+ * @internal
+ */
+ reset: function () {
+ this._callbacks = null;
+ this._contexts = null;
+ },
+
+ /**
+ * `PooledClass` looks for this.
+ */
+ destructor: function () {
+ this.reset();
+ }
+
+});
+
+PooledClass.addPoolingTo(CallbackQueue);
+
+module.exports = CallbackQueue;
+},{"161":161,"24":24,"25":25}],7:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ChangeEventPlugin
+ */
+
+'use strict';
+
+var EventConstants = _dereq_(15);
+var EventPluginHub = _dereq_(16);
+var EventPropagators = _dereq_(19);
+var ExecutionEnvironment = _dereq_(147);
+var ReactUpdates = _dereq_(96);
+var SyntheticEvent = _dereq_(105);
+
+var getEventTarget = _dereq_(128);
+var isEventSupported = _dereq_(133);
+var isTextInputElement = _dereq_(134);
+var keyOf = _dereq_(166);
+
+var topLevelTypes = EventConstants.topLevelTypes;
+
+var eventTypes = {
+ change: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onChange: null }),
+ captured: keyOf({ onChangeCapture: null })
+ },
+ dependencies: [topLevelTypes.topBlur, topLevelTypes.topChange, topLevelTypes.topClick, topLevelTypes.topFocus, topLevelTypes.topInput, topLevelTypes.topKeyDown, topLevelTypes.topKeyUp, topLevelTypes.topSelectionChange]
+ }
+};
+
+/**
+ * For IE shims
+ */
+var activeElement = null;
+var activeElementID = null;
+var activeElementValue = null;
+var activeElementValueProp = null;
+
+/**
+ * SECTION: handle `change` event
+ */
+function shouldUseChangeEvent(elem) {
+ var nodeName = elem.nodeName && elem.nodeName.toLowerCase();
+ return nodeName === 'select' || nodeName === 'input' && elem.type === 'file';
+}
+
+var doesChangeEventBubble = false;
+if (ExecutionEnvironment.canUseDOM) {
+ // See `handleChange` comment below
+ doesChangeEventBubble = isEventSupported('change') && (!('documentMode' in document) || document.documentMode > 8);
+}
+
+function manualDispatchChangeEvent(nativeEvent) {
+ var event = SyntheticEvent.getPooled(eventTypes.change, activeElementID, nativeEvent, getEventTarget(nativeEvent));
+ EventPropagators.accumulateTwoPhaseDispatches(event);
+
+ // If change and propertychange bubbled, we'd just bind to it like all the
+ // other events and have it go through ReactBrowserEventEmitter. Since it
+ // doesn't, we manually listen for the events and so we have to enqueue and
+ // process the abstract event manually.
+ //
+ // Batching is necessary here in order to ensure that all event handlers run
+ // before the next rerender (including event handlers attached to ancestor
+ // elements instead of directly on the input). Without this, controlled
+ // components don't work properly in conjunction with event bubbling because
+ // the component is rerendered and the value reverted before all the event
+ // handlers can run. See https://github.com/facebook/react/issues/708.
+ ReactUpdates.batchedUpdates(runEventInBatch, event);
+}
+
+function runEventInBatch(event) {
+ EventPluginHub.enqueueEvents(event);
+ EventPluginHub.processEventQueue(false);
+}
+
+function startWatchingForChangeEventIE8(target, targetID) {
+ activeElement = target;
+ activeElementID = targetID;
+ activeElement.attachEvent('onchange', manualDispatchChangeEvent);
+}
+
+function stopWatchingForChangeEventIE8() {
+ if (!activeElement) {
+ return;
+ }
+ activeElement.detachEvent('onchange', manualDispatchChangeEvent);
+ activeElement = null;
+ activeElementID = null;
+}
+
+function getTargetIDForChangeEvent(topLevelType, topLevelTarget, topLevelTargetID) {
+ if (topLevelType === topLevelTypes.topChange) {
+ return topLevelTargetID;
+ }
+}
+function handleEventsForChangeEventIE8(topLevelType, topLevelTarget, topLevelTargetID) {
+ if (topLevelType === topLevelTypes.topFocus) {
+ // stopWatching() should be a noop here but we call it just in case we
+ // missed a blur event somehow.
+ stopWatchingForChangeEventIE8();
+ startWatchingForChangeEventIE8(topLevelTarget, topLevelTargetID);
+ } else if (topLevelType === topLevelTypes.topBlur) {
+ stopWatchingForChangeEventIE8();
+ }
+}
+
+/**
+ * SECTION: handle `input` event
+ */
+var isInputEventSupported = false;
+if (ExecutionEnvironment.canUseDOM) {
+ // IE9 claims to support the input event but fails to trigger it when
+ // deleting text, so we ignore its input events
+ isInputEventSupported = isEventSupported('input') && (!('documentMode' in document) || document.documentMode > 9);
+}
+
+/**
+ * (For old IE.) Replacement getter/setter for the `value` property that gets
+ * set on the active element.
+ */
+var newValueProp = {
+ get: function () {
+ return activeElementValueProp.get.call(this);
+ },
+ set: function (val) {
+ // Cast to a string so we can do equality checks.
+ activeElementValue = '' + val;
+ activeElementValueProp.set.call(this, val);
+ }
+};
+
+/**
+ * (For old IE.) Starts tracking propertychange events on the passed-in element
+ * and override the value property so that we can distinguish user events from
+ * value changes in JS.
+ */
+function startWatchingForValueChange(target, targetID) {
+ activeElement = target;
+ activeElementID = targetID;
+ activeElementValue = target.value;
+ activeElementValueProp = Object.getOwnPropertyDescriptor(target.constructor.prototype, 'value');
+
+ // Not guarded in a canDefineProperty check: IE8 supports defineProperty only
+ // on DOM elements
+ Object.defineProperty(activeElement, 'value', newValueProp);
+ activeElement.attachEvent('onpropertychange', handlePropertyChange);
+}
+
+/**
+ * (For old IE.) Removes the event listeners from the currently-tracked element,
+ * if any exists.
+ */
+function stopWatchingForValueChange() {
+ if (!activeElement) {
+ return;
+ }
+
+ // delete restores the original property definition
+ delete activeElement.value;
+ activeElement.detachEvent('onpropertychange', handlePropertyChange);
+
+ activeElement = null;
+ activeElementID = null;
+ activeElementValue = null;
+ activeElementValueProp = null;
+}
+
+/**
+ * (For old IE.) Handles a propertychange event, sending a `change` event if
+ * the value of the active element has changed.
+ */
+function handlePropertyChange(nativeEvent) {
+ if (nativeEvent.propertyName !== 'value') {
+ return;
+ }
+ var value = nativeEvent.srcElement.value;
+ if (value === activeElementValue) {
+ return;
+ }
+ activeElementValue = value;
+
+ manualDispatchChangeEvent(nativeEvent);
+}
+
+/**
+ * If a `change` event should be fired, returns the target's ID.
+ */
+function getTargetIDForInputEvent(topLevelType, topLevelTarget, topLevelTargetID) {
+ if (topLevelType === topLevelTypes.topInput) {
+ // In modern browsers (i.e., not IE8 or IE9), the input event is exactly
+ // what we want so fall through here and trigger an abstract event
+ return topLevelTargetID;
+ }
+}
+
+// For IE8 and IE9.
+function handleEventsForInputEventIE(topLevelType, topLevelTarget, topLevelTargetID) {
+ if (topLevelType === topLevelTypes.topFocus) {
+ // In IE8, we can capture almost all .value changes by adding a
+ // propertychange handler and looking for events with propertyName
+ // equal to 'value'
+ // In IE9, propertychange fires for most input events but is buggy and
+ // doesn't fire when text is deleted, but conveniently, selectionchange
+ // appears to fire in all of the remaining cases so we catch those and
+ // forward the event if the value has changed
+ // In either case, we don't want to call the event handler if the value
+ // is changed from JS so we redefine a setter for `.value` that updates
+ // our activeElementValue variable, allowing us to ignore those changes
+ //
+ // stopWatching() should be a noop here but we call it just in case we
+ // missed a blur event somehow.
+ stopWatchingForValueChange();
+ startWatchingForValueChange(topLevelTarget, topLevelTargetID);
+ } else if (topLevelType === topLevelTypes.topBlur) {
+ stopWatchingForValueChange();
+ }
+}
+
+// For IE8 and IE9.
+function getTargetIDForInputEventIE(topLevelType, topLevelTarget, topLevelTargetID) {
+ if (topLevelType === topLevelTypes.topSelectionChange || topLevelType === topLevelTypes.topKeyUp || topLevelType === topLevelTypes.topKeyDown) {
+ // On the selectionchange event, the target is just document which isn't
+ // helpful for us so just check activeElement instead.
+ //
+ // 99% of the time, keydown and keyup aren't necessary. IE8 fails to fire
+ // propertychange on the first input event after setting `value` from a
+ // script and fires only keydown, keypress, keyup. Catching keyup usually
+ // gets it and catching keydown lets us fire an event for the first
+ // keystroke if user does a key repeat (it'll be a little delayed: right
+ // before the second keystroke). Other input methods (e.g., paste) seem to
+ // fire selectionchange normally.
+ if (activeElement && activeElement.value !== activeElementValue) {
+ activeElementValue = activeElement.value;
+ return activeElementID;
+ }
+ }
+}
+
+/**
+ * SECTION: handle `click` event
+ */
+function shouldUseClickEvent(elem) {
+ // Use the `click` event to detect changes to checkbox and radio inputs.
+ // This approach works across all browsers, whereas `change` does not fire
+ // until `blur` in IE8.
+ return elem.nodeName && elem.nodeName.toLowerCase() === 'input' && (elem.type === 'checkbox' || elem.type === 'radio');
+}
+
+function getTargetIDForClickEvent(topLevelType, topLevelTarget, topLevelTargetID) {
+ if (topLevelType === topLevelTypes.topClick) {
+ return topLevelTargetID;
+ }
+}
+
+/**
+ * This plugin creates an `onChange` event that normalizes change events
+ * across form elements. This event fires at a time when it's possible to
+ * change the element's value without seeing a flicker.
+ *
+ * Supported elements are:
+ * - input (see `isTextInputElement`)
+ * - textarea
+ * - select
+ */
+var ChangeEventPlugin = {
+
+ eventTypes: eventTypes,
+
+ /**
+ * @param {string} topLevelType Record from `EventConstants`.
+ * @param {DOMEventTarget} topLevelTarget The listening component root node.
+ * @param {string} topLevelTargetID ID of `topLevelTarget`.
+ * @param {object} nativeEvent Native browser event.
+ * @return {*} An accumulation of synthetic events.
+ * @see {EventPluginHub.extractEvents}
+ */
+ extractEvents: function (topLevelType, topLevelTarget, topLevelTargetID, nativeEvent, nativeEventTarget) {
+
+ var getTargetIDFunc, handleEventFunc;
+ if (shouldUseChangeEvent(topLevelTarget)) {
+ if (doesChangeEventBubble) {
+ getTargetIDFunc = getTargetIDForChangeEvent;
+ } else {
+ handleEventFunc = handleEventsForChangeEventIE8;
+ }
+ } else if (isTextInputElement(topLevelTarget)) {
+ if (isInputEventSupported) {
+ getTargetIDFunc = getTargetIDForInputEvent;
+ } else {
+ getTargetIDFunc = getTargetIDForInputEventIE;
+ handleEventFunc = handleEventsForInputEventIE;
+ }
+ } else if (shouldUseClickEvent(topLevelTarget)) {
+ getTargetIDFunc = getTargetIDForClickEvent;
+ }
+
+ if (getTargetIDFunc) {
+ var targetID = getTargetIDFunc(topLevelType, topLevelTarget, topLevelTargetID);
+ if (targetID) {
+ var event = SyntheticEvent.getPooled(eventTypes.change, targetID, nativeEvent, nativeEventTarget);
+ event.type = 'change';
+ EventPropagators.accumulateTwoPhaseDispatches(event);
+ return event;
+ }
+ }
+
+ if (handleEventFunc) {
+ handleEventFunc(topLevelType, topLevelTarget, topLevelTargetID);
+ }
+ }
+
+};
+
+module.exports = ChangeEventPlugin;
+},{"105":105,"128":128,"133":133,"134":134,"147":147,"15":15,"16":16,"166":166,"19":19,"96":96}],8:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ClientReactRootIndex
+ * @typechecks
+ */
+
+'use strict';
+
+var nextReactRootIndex = 0;
+
+var ClientReactRootIndex = {
+ createReactRootIndex: function () {
+ return nextReactRootIndex++;
+ }
+};
+
+module.exports = ClientReactRootIndex;
+},{}],9:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule DOMChildrenOperations
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var Danger = _dereq_(12);
+var ReactMultiChildUpdateTypes = _dereq_(74);
+var ReactPerf = _dereq_(78);
+
+var setInnerHTML = _dereq_(138);
+var setTextContent = _dereq_(139);
+var invariant = _dereq_(161);
+
+/**
+ * Inserts `childNode` as a child of `parentNode` at the `index`.
+ *
+ * @param {DOMElement} parentNode Parent node in which to insert.
+ * @param {DOMElement} childNode Child node to insert.
+ * @param {number} index Index at which to insert the child.
+ * @internal
+ */
+function insertChildAt(parentNode, childNode, index) {
+ // By exploiting arrays returning `undefined` for an undefined index, we can
+ // rely exclusively on `insertBefore(node, null)` instead of also using
+ // `appendChild(node)`. However, using `undefined` is not allowed by all
+ // browsers so we must replace it with `null`.
+
+ // fix render order error in safari
+ // IE8 will throw error when index out of list size.
+ var beforeChild = index >= parentNode.childNodes.length ? null : parentNode.childNodes.item(index);
+
+ parentNode.insertBefore(childNode, beforeChild);
+}
+
+/**
+ * Operations for updating with DOM children.
+ */
+var DOMChildrenOperations = {
+
+ dangerouslyReplaceNodeWithMarkup: Danger.dangerouslyReplaceNodeWithMarkup,
+
+ updateTextContent: setTextContent,
+
+ /**
+ * Updates a component's children by processing a series of updates. The
+ * update configurations are each expected to have a `parentNode` property.
+ *
+ * @param {array<object>} updates List of update configurations.
+ * @param {array<string>} markupList List of markup strings.
+ * @internal
+ */
+ processUpdates: function (updates, markupList) {
+ var update;
+ // Mapping from parent IDs to initial child orderings.
+ var initialChildren = null;
+ // List of children that will be moved or removed.
+ var updatedChildren = null;
+
+ for (var i = 0; i < updates.length; i++) {
+ update = updates[i];
+ if (update.type === ReactMultiChildUpdateTypes.MOVE_EXISTING || update.type === ReactMultiChildUpdateTypes.REMOVE_NODE) {
+ var updatedIndex = update.fromIndex;
+ var updatedChild = update.parentNode.childNodes[updatedIndex];
+ var parentID = update.parentID;
+
+ !updatedChild ? "production" !== 'production' ? invariant(false, 'processUpdates(): Unable to find child %s of element. This ' + 'probably means the DOM was unexpectedly mutated (e.g., by the ' + 'browser), usually due to forgetting a <tbody> when using tables, ' + 'nesting tags like <form>, <p>, or <a>, or using non-SVG elements ' + 'in an <svg> parent. Try inspecting the child nodes of the element ' + 'with React ID `%s`.', updatedIndex, parentID) : invariant(false) : undefined;
+
+ initialChildren = initialChildren || {};
+ initialChildren[parentID] = initialChildren[parentID] || [];
+ initialChildren[parentID][updatedIndex] = updatedChild;
+
+ updatedChildren = updatedChildren || [];
+ updatedChildren.push(updatedChild);
+ }
+ }
+
+ var renderedMarkup;
+ // markupList is either a list of markup or just a list of elements
+ if (markupList.length && typeof markupList[0] === 'string') {
+ renderedMarkup = Danger.dangerouslyRenderMarkup(markupList);
+ } else {
+ renderedMarkup = markupList;
+ }
+
+ // Remove updated children first so that `toIndex` is consistent.
+ if (updatedChildren) {
+ for (var j = 0; j < updatedChildren.length; j++) {
+ updatedChildren[j].parentNode.removeChild(updatedChildren[j]);
+ }
+ }
+
+ for (var k = 0; k < updates.length; k++) {
+ update = updates[k];
+ switch (update.type) {
+ case ReactMultiChildUpdateTypes.INSERT_MARKUP:
+ insertChildAt(update.parentNode, renderedMarkup[update.markupIndex], update.toIndex);
+ break;
+ case ReactMultiChildUpdateTypes.MOVE_EXISTING:
+ insertChildAt(update.parentNode, initialChildren[update.parentID][update.fromIndex], update.toIndex);
+ break;
+ case ReactMultiChildUpdateTypes.SET_MARKUP:
+ setInnerHTML(update.parentNode, update.content);
+ break;
+ case ReactMultiChildUpdateTypes.TEXT_CONTENT:
+ setTextContent(update.parentNode, update.content);
+ break;
+ case ReactMultiChildUpdateTypes.REMOVE_NODE:
+ // Already removed by the for-loop above.
+ break;
+ }
+ }
+ }
+
+};
+
+ReactPerf.measureMethods(DOMChildrenOperations, 'DOMChildrenOperations', {
+ updateTextContent: 'updateTextContent'
+});
+
+module.exports = DOMChildrenOperations;
+},{"12":12,"138":138,"139":139,"161":161,"74":74,"78":78}],10:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule DOMProperty
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var invariant = _dereq_(161);
+
+function checkMask(value, bitmask) {
+ return (value & bitmask) === bitmask;
+}
+
+var DOMPropertyInjection = {
+ /**
+ * Mapping from normalized, camelcased property names to a configuration that
+ * specifies how the associated DOM property should be accessed or rendered.
+ */
+ MUST_USE_ATTRIBUTE: 0x1,
+ MUST_USE_PROPERTY: 0x2,
+ HAS_SIDE_EFFECTS: 0x4,
+ HAS_BOOLEAN_VALUE: 0x8,
+ HAS_NUMERIC_VALUE: 0x10,
+ HAS_POSITIVE_NUMERIC_VALUE: 0x20 | 0x10,
+ HAS_OVERLOADED_BOOLEAN_VALUE: 0x40,
+
+ /**
+ * Inject some specialized knowledge about the DOM. This takes a config object
+ * with the following properties:
+ *
+ * isCustomAttribute: function that given an attribute name will return true
+ * if it can be inserted into the DOM verbatim. Useful for data-* or aria-*
+ * attributes where it's impossible to enumerate all of the possible
+ * attribute names,
+ *
+ * Properties: object mapping DOM property name to one of the
+ * DOMPropertyInjection constants or null. If your attribute isn't in here,
+ * it won't get written to the DOM.
+ *
+ * DOMAttributeNames: object mapping React attribute name to the DOM
+ * attribute name. Attribute names not specified use the **lowercase**
+ * normalized name.
+ *
+ * DOMAttributeNamespaces: object mapping React attribute name to the DOM
+ * attribute namespace URL. (Attribute names not specified use no namespace.)
+ *
+ * DOMPropertyNames: similar to DOMAttributeNames but for DOM properties.
+ * Property names not specified use the normalized name.
+ *
+ * DOMMutationMethods: Properties that require special mutation methods. If
+ * `value` is undefined, the mutation method should unset the property.
+ *
+ * @param {object} domPropertyConfig the config as described above.
+ */
+ injectDOMPropertyConfig: function (domPropertyConfig) {
+ var Injection = DOMPropertyInjection;
+ var Properties = domPropertyConfig.Properties || {};
+ var DOMAttributeNamespaces = domPropertyConfig.DOMAttributeNamespaces || {};
+ var DOMAttributeNames = domPropertyConfig.DOMAttributeNames || {};
+ var DOMPropertyNames = domPropertyConfig.DOMPropertyNames || {};
+ var DOMMutationMethods = domPropertyConfig.DOMMutationMethods || {};
+
+ if (domPropertyConfig.isCustomAttribute) {
+ DOMProperty._isCustomAttributeFunctions.push(domPropertyConfig.isCustomAttribute);
+ }
+
+ for (var propName in Properties) {
+ !!DOMProperty.properties.hasOwnProperty(propName) ? "production" !== 'production' ? invariant(false, 'injectDOMPropertyConfig(...): You\'re trying to inject DOM property ' + '\'%s\' which has already been injected. You may be accidentally ' + 'injecting the same DOM property config twice, or you may be ' + 'injecting two configs that have conflicting property names.', propName) : invariant(false) : undefined;
+
+ var lowerCased = propName.toLowerCase();
+ var propConfig = Properties[propName];
+
+ var propertyInfo = {
+ attributeName: lowerCased,
+ attributeNamespace: null,
+ propertyName: propName,
+ mutationMethod: null,
+
+ mustUseAttribute: checkMask(propConfig, Injection.MUST_USE_ATTRIBUTE),
+ mustUseProperty: checkMask(propConfig, Injection.MUST_USE_PROPERTY),
+ hasSideEffects: checkMask(propConfig, Injection.HAS_SIDE_EFFECTS),
+ hasBooleanValue: checkMask(propConfig, Injection.HAS_BOOLEAN_VALUE),
+ hasNumericValue: checkMask(propConfig, Injection.HAS_NUMERIC_VALUE),
+ hasPositiveNumericValue: checkMask(propConfig, Injection.HAS_POSITIVE_NUMERIC_VALUE),
+ hasOverloadedBooleanValue: checkMask(propConfig, Injection.HAS_OVERLOADED_BOOLEAN_VALUE)
+ };
+
+ !(!propertyInfo.mustUseAttribute || !propertyInfo.mustUseProperty) ? "production" !== 'production' ? invariant(false, 'DOMProperty: Cannot require using both attribute and property: %s', propName) : invariant(false) : undefined;
+ !(propertyInfo.mustUseProperty || !propertyInfo.hasSideEffects) ? "production" !== 'production' ? invariant(false, 'DOMProperty: Properties that have side effects must use property: %s', propName) : invariant(false) : undefined;
+ !(propertyInfo.hasBooleanValue + propertyInfo.hasNumericValue + propertyInfo.hasOverloadedBooleanValue <= 1) ? "production" !== 'production' ? invariant(false, 'DOMProperty: Value can be one of boolean, overloaded boolean, or ' + 'numeric value, but not a combination: %s', propName) : invariant(false) : undefined;
+
+ if ("production" !== 'production') {
+ DOMProperty.getPossibleStandardName[lowerCased] = propName;
+ }
+
+ if (DOMAttributeNames.hasOwnProperty(propName)) {
+ var attributeName = DOMAttributeNames[propName];
+ propertyInfo.attributeName = attributeName;
+ if ("production" !== 'production') {
+ DOMProperty.getPossibleStandardName[attributeName] = propName;
+ }
+ }
+
+ if (DOMAttributeNamespaces.hasOwnProperty(propName)) {
+ propertyInfo.attributeNamespace = DOMAttributeNamespaces[propName];
+ }
+
+ if (DOMPropertyNames.hasOwnProperty(propName)) {
+ propertyInfo.propertyName = DOMPropertyNames[propName];
+ }
+
+ if (DOMMutationMethods.hasOwnProperty(propName)) {
+ propertyInfo.mutationMethod = DOMMutationMethods[propName];
+ }
+
+ DOMProperty.properties[propName] = propertyInfo;
+ }
+ }
+};
+var defaultValueCache = {};
+
+/**
+ * DOMProperty exports lookup objects that can be used like functions:
+ *
+ * > DOMProperty.isValid['id']
+ * true
+ * > DOMProperty.isValid['foobar']
+ * undefined
+ *
+ * Although this may be confusing, it performs better in general.
+ *
+ * @see http://jsperf.com/key-exists
+ * @see http://jsperf.com/key-missing
+ */
+var DOMProperty = {
+
+ ID_ATTRIBUTE_NAME: 'data-reactid',
+
+ /**
+ * Map from property "standard name" to an object with info about how to set
+ * the property in the DOM. Each object contains:
+ *
+ * attributeName:
+ * Used when rendering markup or with `*Attribute()`.
+ * attributeNamespace
+ * propertyName:
+ * Used on DOM node instances. (This includes properties that mutate due to
+ * external factors.)
+ * mutationMethod:
+ * If non-null, used instead of the property or `setAttribute()` after
+ * initial render.
+ * mustUseAttribute:
+ * Whether the property must be accessed and mutated using `*Attribute()`.
+ * (This includes anything that fails `<propName> in <element>`.)
+ * mustUseProperty:
+ * Whether the property must be accessed and mutated as an object property.
+ * hasSideEffects:
+ * Whether or not setting a value causes side effects such as triggering
+ * resources to be loaded or text selection changes. If true, we read from
+ * the DOM before updating to ensure that the value is only set if it has
+ * changed.
+ * hasBooleanValue:
+ * Whether the property should be removed when set to a falsey value.
+ * hasNumericValue:
+ * Whether the property must be numeric or parse as a numeric and should be
+ * removed when set to a falsey value.
+ * hasPositiveNumericValue:
+ * Whether the property must be positive numeric or parse as a positive
+ * numeric and should be removed when set to a falsey value.
+ * hasOverloadedBooleanValue:
+ * Whether the property can be used as a flag as well as with a value.
+ * Removed when strictly equal to false; present without a value when
+ * strictly equal to true; present with a value otherwise.
+ */
+ properties: {},
+
+ /**
+ * Mapping from lowercase property names to the properly cased version, used
+ * to warn in the case of missing properties. Available only in __DEV__.
+ * @type {Object}
+ */
+ getPossibleStandardName: "production" !== 'production' ? {} : null,
+
+ /**
+ * All of the isCustomAttribute() functions that have been injected.
+ */
+ _isCustomAttributeFunctions: [],
+
+ /**
+ * Checks whether a property name is a custom attribute.
+ * @method
+ */
+ isCustomAttribute: function (attributeName) {
+ for (var i = 0; i < DOMProperty._isCustomAttributeFunctions.length; i++) {
+ var isCustomAttributeFn = DOMProperty._isCustomAttributeFunctions[i];
+ if (isCustomAttributeFn(attributeName)) {
+ return true;
+ }
+ }
+ return false;
+ },
+
+ /**
+ * Returns the default property value for a DOM property (i.e., not an
+ * attribute). Most default values are '' or false, but not all. Worse yet,
+ * some (in particular, `type`) vary depending on the type of element.
+ *
+ * TODO: Is it better to grab all the possible properties when creating an
+ * element to avoid having to create the same element twice?
+ */
+ getDefaultValueForProperty: function (nodeName, prop) {
+ var nodeDefaults = defaultValueCache[nodeName];
+ var testElement;
+ if (!nodeDefaults) {
+ defaultValueCache[nodeName] = nodeDefaults = {};
+ }
+ if (!(prop in nodeDefaults)) {
+ testElement = document.createElementNS('http://www.w3.org/1999/xhtml', nodeName);
+ nodeDefaults[prop] = testElement[prop];
+ }
+ return nodeDefaults[prop];
+ },
+
+ injection: DOMPropertyInjection
+};
+
+module.exports = DOMProperty;
+},{"161":161}],11:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule DOMPropertyOperations
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var DOMProperty = _dereq_(10);
+var ReactPerf = _dereq_(78);
+
+var quoteAttributeValueForBrowser = _dereq_(136);
+var warning = _dereq_(173);
+
+// Simplified subset
+var VALID_ATTRIBUTE_NAME_REGEX = /^[a-zA-Z_][\w\.\-]*$/;
+var illegalAttributeNameCache = {};
+var validatedAttributeNameCache = {};
+
+function isAttributeNameSafe(attributeName) {
+ if (validatedAttributeNameCache.hasOwnProperty(attributeName)) {
+ return true;
+ }
+ if (illegalAttributeNameCache.hasOwnProperty(attributeName)) {
+ return false;
+ }
+ if (VALID_ATTRIBUTE_NAME_REGEX.test(attributeName)) {
+ validatedAttributeNameCache[attributeName] = true;
+ return true;
+ }
+ illegalAttributeNameCache[attributeName] = true;
+ "production" !== 'production' ? warning(false, 'Invalid attribute name: `%s`', attributeName) : undefined;
+ return false;
+}
+
+function shouldIgnoreValue(propertyInfo, value) {
+ return value == null || propertyInfo.hasBooleanValue && !value || propertyInfo.hasNumericValue && isNaN(value) || propertyInfo.hasPositiveNumericValue && value < 1 || propertyInfo.hasOverloadedBooleanValue && value === false;
+}
+
+if ("production" !== 'production') {
+ var reactProps = {
+ children: true,
+ dangerouslySetInnerHTML: true,
+ key: true,
+ ref: true
+ };
+ var warnedProperties = {};
+
+ var warnUnknownProperty = function (name) {
+ if (reactProps.hasOwnProperty(name) && reactProps[name] || warnedProperties.hasOwnProperty(name) && warnedProperties[name]) {
+ return;
+ }
+
+ warnedProperties[name] = true;
+ var lowerCasedName = name.toLowerCase();
+
+ // data-* attributes should be lowercase; suggest the lowercase version
+ var standardName = DOMProperty.isCustomAttribute(lowerCasedName) ? lowerCasedName : DOMProperty.getPossibleStandardName.hasOwnProperty(lowerCasedName) ? DOMProperty.getPossibleStandardName[lowerCasedName] : null;
+
+ // For now, only warn when we have a suggested correction. This prevents
+ // logging too much when using transferPropsTo.
+ "production" !== 'production' ? warning(standardName == null, 'Unknown DOM property %s. Did you mean %s?', name, standardName) : undefined;
+ };
+}
+
+/**
+ * Operations for dealing with DOM properties.
+ */
+var DOMPropertyOperations = {
+
+ /**
+ * Creates markup for the ID property.
+ *
+ * @param {string} id Unescaped ID.
+ * @return {string} Markup string.
+ */
+ createMarkupForID: function (id) {
+ return DOMProperty.ID_ATTRIBUTE_NAME + '=' + quoteAttributeValueForBrowser(id);
+ },
+
+ setAttributeForID: function (node, id) {
+ node.setAttribute(DOMProperty.ID_ATTRIBUTE_NAME, id);
+ },
+
+ /**
+ * Creates markup for a property.
+ *
+ * @param {string} name
+ * @param {*} value
+ * @return {?string} Markup string, or null if the property was invalid.
+ */
+ createMarkupForProperty: function (name, value) {
+ var propertyInfo = DOMProperty.properties.hasOwnProperty(name) ? DOMProperty.properties[name] : null;
+ if (propertyInfo) {
+ if (shouldIgnoreValue(propertyInfo, value)) {
+ return '';
+ }
+ var attributeName = propertyInfo.attributeName;
+ if (propertyInfo.hasBooleanValue || propertyInfo.hasOverloadedBooleanValue && value === true) {
+ return attributeName + '=""';
+ }
+ return attributeName + '=' + quoteAttributeValueForBrowser(value);
+ } else if (DOMProperty.isCustomAttribute(name)) {
+ if (value == null) {
+ return '';
+ }
+ return name + '=' + quoteAttributeValueForBrowser(value);
+ } else if ("production" !== 'production') {
+ warnUnknownProperty(name);
+ }
+ return null;
+ },
+
+ /**
+ * Creates markup for a custom property.
+ *
+ * @param {string} name
+ * @param {*} value
+ * @return {string} Markup string, or empty string if the property was invalid.
+ */
+ createMarkupForCustomAttribute: function (name, value) {
+ if (!isAttributeNameSafe(name) || value == null) {
+ return '';
+ }
+ return name + '=' + quoteAttributeValueForBrowser(value);
+ },
+
+ /**
+ * Sets the value for a property on a node.
+ *
+ * @param {DOMElement} node
+ * @param {string} name
+ * @param {*} value
+ */
+ setValueForProperty: function (node, name, value) {
+ var propertyInfo = DOMProperty.properties.hasOwnProperty(name) ? DOMProperty.properties[name] : null;
+ if (propertyInfo) {
+ var mutationMethod = propertyInfo.mutationMethod;
+ if (mutationMethod) {
+ mutationMethod(node, value);
+ } else if (shouldIgnoreValue(propertyInfo, value)) {
+ this.deleteValueForProperty(node, name);
+ } else if (propertyInfo.mustUseAttribute) {
+ var attributeName = propertyInfo.attributeName;
+ var namespace = propertyInfo.attributeNamespace;
+ // `setAttribute` with objects becomes only `[object]` in IE8/9,
+ // ('' + value) makes it output the correct toString()-value.
+ if (namespace) {
+ node.setAttributeNS(namespace, attributeName, '' + value);
+ } else if (propertyInfo.hasBooleanValue || propertyInfo.hasOverloadedBooleanValue && value === true) {
+ node.setAttribute(attributeName, '');
+ } else {
+ node.setAttribute(attributeName, '' + value);
+ }
+ } else {
+ var propName = propertyInfo.propertyName;
+ // Must explicitly cast values for HAS_SIDE_EFFECTS-properties to the
+ // property type before comparing; only `value` does and is string.
+ if (!propertyInfo.hasSideEffects || '' + node[propName] !== '' + value) {
+ // Contrary to `setAttribute`, object properties are properly
+ // `toString`ed by IE8/9.
+ node[propName] = value;
+ }
+ }
+ } else if (DOMProperty.isCustomAttribute(name)) {
+ DOMPropertyOperations.setValueForAttribute(node, name, value);
+ } else if ("production" !== 'production') {
+ warnUnknownProperty(name);
+ }
+ },
+
+ setValueForAttribute: function (node, name, value) {
+ if (!isAttributeNameSafe(name)) {
+ return;
+ }
+ if (value == null) {
+ node.removeAttribute(name);
+ } else {
+ node.setAttribute(name, '' + value);
+ }
+ },
+
+ /**
+ * Deletes the value for a property on a node.
+ *
+ * @param {DOMElement} node
+ * @param {string} name
+ */
+ deleteValueForProperty: function (node, name) {
+ var propertyInfo = DOMProperty.properties.hasOwnProperty(name) ? DOMProperty.properties[name] : null;
+ if (propertyInfo) {
+ var mutationMethod = propertyInfo.mutationMethod;
+ if (mutationMethod) {
+ mutationMethod(node, undefined);
+ } else if (propertyInfo.mustUseAttribute) {
+ node.removeAttribute(propertyInfo.attributeName);
+ } else {
+ var propName = propertyInfo.propertyName;
+ var defaultValue = DOMProperty.getDefaultValueForProperty(node.nodeName, propName);
+ if (!propertyInfo.hasSideEffects || '' + node[propName] !== defaultValue) {
+ node[propName] = defaultValue;
+ }
+ }
+ } else if (DOMProperty.isCustomAttribute(name)) {
+ node.removeAttribute(name);
+ } else if ("production" !== 'production') {
+ warnUnknownProperty(name);
+ }
+ }
+
+};
+
+ReactPerf.measureMethods(DOMPropertyOperations, 'DOMPropertyOperations', {
+ setValueForProperty: 'setValueForProperty',
+ setValueForAttribute: 'setValueForAttribute',
+ deleteValueForProperty: 'deleteValueForProperty'
+});
+
+module.exports = DOMPropertyOperations;
+},{"10":10,"136":136,"173":173,"78":78}],12:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule Danger
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var ExecutionEnvironment = _dereq_(147);
+
+var createNodesFromMarkup = _dereq_(152);
+var emptyFunction = _dereq_(153);
+var getMarkupWrap = _dereq_(157);
+var invariant = _dereq_(161);
+
+var OPEN_TAG_NAME_EXP = /^(<[^ \/>]+)/;
+var RESULT_INDEX_ATTR = 'data-danger-index';
+
+/**
+ * Extracts the `nodeName` from a string of markup.
+ *
+ * NOTE: Extracting the `nodeName` does not require a regular expression match
+ * because we make assumptions about React-generated markup (i.e. there are no
+ * spaces surrounding the opening tag and there is at least one attribute).
+ *
+ * @param {string} markup String of markup.
+ * @return {string} Node name of the supplied markup.
+ * @see http://jsperf.com/extract-nodename
+ */
+function getNodeName(markup) {
+ return markup.substring(1, markup.indexOf(' '));
+}
+
+var Danger = {
+
+ /**
+ * Renders markup into an array of nodes. The markup is expected to render
+ * into a list of root nodes. Also, the length of `resultList` and
+ * `markupList` should be the same.
+ *
+ * @param {array<string>} markupList List of markup strings to render.
+ * @return {array<DOMElement>} List of rendered nodes.
+ * @internal
+ */
+ dangerouslyRenderMarkup: function (markupList) {
+ !ExecutionEnvironment.canUseDOM ? "production" !== 'production' ? invariant(false, 'dangerouslyRenderMarkup(...): Cannot render markup in a worker ' + 'thread. Make sure `window` and `document` are available globally ' + 'before requiring React when unit testing or use ' + 'ReactDOMServer.renderToString for server rendering.') : invariant(false) : undefined;
+ var nodeName;
+ var markupByNodeName = {};
+ // Group markup by `nodeName` if a wrap is necessary, else by '*'.
+ for (var i = 0; i < markupList.length; i++) {
+ !markupList[i] ? "production" !== 'production' ? invariant(false, 'dangerouslyRenderMarkup(...): Missing markup.') : invariant(false) : undefined;
+ nodeName = getNodeName(markupList[i]);
+ nodeName = getMarkupWrap(nodeName) ? nodeName : '*';
+ markupByNodeName[nodeName] = markupByNodeName[nodeName] || [];
+ markupByNodeName[nodeName][i] = markupList[i];
+ }
+ var resultList = [];
+ var resultListAssignmentCount = 0;
+ for (nodeName in markupByNodeName) {
+ if (!markupByNodeName.hasOwnProperty(nodeName)) {
+ continue;
+ }
+ var markupListByNodeName = markupByNodeName[nodeName];
+
+ // This for-in loop skips the holes of the sparse array. The order of
+ // iteration should follow the order of assignment, which happens to match
+ // numerical index order, but we don't rely on that.
+ var resultIndex;
+ for (resultIndex in markupListByNodeName) {
+ if (markupListByNodeName.hasOwnProperty(resultIndex)) {
+ var markup = markupListByNodeName[resultIndex];
+
+ // Push the requested markup with an additional RESULT_INDEX_ATTR
+ // attribute. If the markup does not start with a < character, it
+ // will be discarded below (with an appropriate console.error).
+ markupListByNodeName[resultIndex] = markup.replace(OPEN_TAG_NAME_EXP,
+ // This index will be parsed back out below.
+ '$1 ' + RESULT_INDEX_ATTR + '="' + resultIndex + '" ');
+ }
+ }
+
+ // Render each group of markup with similar wrapping `nodeName`.
+ var renderNodes = createNodesFromMarkup(markupListByNodeName.join(''), emptyFunction // Do nothing special with <script> tags.
+ );
+
+ for (var j = 0; j < renderNodes.length; ++j) {
+ var renderNode = renderNodes[j];
+ if (renderNode.hasAttribute && renderNode.hasAttribute(RESULT_INDEX_ATTR)) {
+
+ resultIndex = +renderNode.getAttribute(RESULT_INDEX_ATTR);
+ renderNode.removeAttribute(RESULT_INDEX_ATTR);
+
+ !!resultList.hasOwnProperty(resultIndex) ? "production" !== 'production' ? invariant(false, 'Danger: Assigning to an already-occupied result index.') : invariant(false) : undefined;
+
+ resultList[resultIndex] = renderNode;
+
+ // This should match resultList.length and markupList.length when
+ // we're done.
+ resultListAssignmentCount += 1;
+ } else if ("production" !== 'production') {
+ console.error('Danger: Discarding unexpected node:', renderNode);
+ }
+ }
+ }
+
+ // Although resultList was populated out of order, it should now be a dense
+ // array.
+ !(resultListAssignmentCount === resultList.length) ? "production" !== 'production' ? invariant(false, 'Danger: Did not assign to every index of resultList.') : invariant(false) : undefined;
+
+ !(resultList.length === markupList.length) ? "production" !== 'production' ? invariant(false, 'Danger: Expected markup to render %s nodes, but rendered %s.', markupList.length, resultList.length) : invariant(false) : undefined;
+
+ return resultList;
+ },
+
+ /**
+ * Replaces a node with a string of markup at its current position within its
+ * parent. The markup must render into a single root node.
+ *
+ * @param {DOMElement} oldChild Child node to replace.
+ * @param {string} markup Markup to render in place of the child node.
+ * @internal
+ */
+ dangerouslyReplaceNodeWithMarkup: function (oldChild, markup) {
+ !ExecutionEnvironment.canUseDOM ? "production" !== 'production' ? invariant(false, 'dangerouslyReplaceNodeWithMarkup(...): Cannot render markup in a ' + 'worker thread. Make sure `window` and `document` are available ' + 'globally before requiring React when unit testing or use ' + 'ReactDOMServer.renderToString() for server rendering.') : invariant(false) : undefined;
+ !markup ? "production" !== 'production' ? invariant(false, 'dangerouslyReplaceNodeWithMarkup(...): Missing markup.') : invariant(false) : undefined;
+ !(oldChild.tagName.toLowerCase() !== 'html') ? "production" !== 'production' ? invariant(false, 'dangerouslyReplaceNodeWithMarkup(...): Cannot replace markup of the ' + '<html> node. This is because browser quirks make this unreliable ' + 'and/or slow. If you want to render to the root you must use ' + 'server rendering. See ReactDOMServer.renderToString().') : invariant(false) : undefined;
+
+ var newChild;
+ if (typeof markup === 'string') {
+ newChild = createNodesFromMarkup(markup, emptyFunction)[0];
+ } else {
+ newChild = markup;
+ }
+ oldChild.parentNode.replaceChild(newChild, oldChild);
+ }
+
+};
+
+module.exports = Danger;
+},{"147":147,"152":152,"153":153,"157":157,"161":161}],13:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule DefaultEventPluginOrder
+ */
+
+'use strict';
+
+var keyOf = _dereq_(166);
+
+/**
+ * Module that is injectable into `EventPluginHub`, that specifies a
+ * deterministic ordering of `EventPlugin`s. A convenient way to reason about
+ * plugins, without having to package every one of them. This is better than
+ * having plugins be ordered in the same order that they are injected because
+ * that ordering would be influenced by the packaging order.
+ * `ResponderEventPlugin` must occur before `SimpleEventPlugin` so that
+ * preventing default on events is convenient in `SimpleEventPlugin` handlers.
+ */
+var DefaultEventPluginOrder = [keyOf({ ResponderEventPlugin: null }), keyOf({ SimpleEventPlugin: null }), keyOf({ TapEventPlugin: null }), keyOf({ EnterLeaveEventPlugin: null }), keyOf({ ChangeEventPlugin: null }), keyOf({ SelectEventPlugin: null }), keyOf({ BeforeInputEventPlugin: null })];
+
+module.exports = DefaultEventPluginOrder;
+},{"166":166}],14:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule EnterLeaveEventPlugin
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var EventConstants = _dereq_(15);
+var EventPropagators = _dereq_(19);
+var SyntheticMouseEvent = _dereq_(109);
+
+var ReactMount = _dereq_(72);
+var keyOf = _dereq_(166);
+
+var topLevelTypes = EventConstants.topLevelTypes;
+var getFirstReactDOM = ReactMount.getFirstReactDOM;
+
+var eventTypes = {
+ mouseEnter: {
+ registrationName: keyOf({ onMouseEnter: null }),
+ dependencies: [topLevelTypes.topMouseOut, topLevelTypes.topMouseOver]
+ },
+ mouseLeave: {
+ registrationName: keyOf({ onMouseLeave: null }),
+ dependencies: [topLevelTypes.topMouseOut, topLevelTypes.topMouseOver]
+ }
+};
+
+var extractedEvents = [null, null];
+
+var EnterLeaveEventPlugin = {
+
+ eventTypes: eventTypes,
+
+ /**
+ * For almost every interaction we care about, there will be both a top-level
+ * `mouseover` and `mouseout` event that occurs. Only use `mouseout` so that
+ * we do not extract duplicate events. However, moving the mouse into the
+ * browser from outside will not fire a `mouseout` event. In this case, we use
+ * the `mouseover` top-level event.
+ *
+ * @param {string} topLevelType Record from `EventConstants`.
+ * @param {DOMEventTarget} topLevelTarget The listening component root node.
+ * @param {string} topLevelTargetID ID of `topLevelTarget`.
+ * @param {object} nativeEvent Native browser event.
+ * @return {*} An accumulation of synthetic events.
+ * @see {EventPluginHub.extractEvents}
+ */
+ extractEvents: function (topLevelType, topLevelTarget, topLevelTargetID, nativeEvent, nativeEventTarget) {
+ if (topLevelType === topLevelTypes.topMouseOver && (nativeEvent.relatedTarget || nativeEvent.fromElement)) {
+ return null;
+ }
+ if (topLevelType !== topLevelTypes.topMouseOut && topLevelType !== topLevelTypes.topMouseOver) {
+ // Must not be a mouse in or mouse out - ignoring.
+ return null;
+ }
+
+ var win;
+ if (topLevelTarget.window === topLevelTarget) {
+ // `topLevelTarget` is probably a window object.
+ win = topLevelTarget;
+ } else {
+ // TODO: Figure out why `ownerDocument` is sometimes undefined in IE8.
+ var doc = topLevelTarget.ownerDocument;
+ if (doc) {
+ win = doc.defaultView || doc.parentWindow;
+ } else {
+ win = window;
+ }
+ }
+
+ var from;
+ var to;
+ var fromID = '';
+ var toID = '';
+ if (topLevelType === topLevelTypes.topMouseOut) {
+ from = topLevelTarget;
+ fromID = topLevelTargetID;
+ to = getFirstReactDOM(nativeEvent.relatedTarget || nativeEvent.toElement);
+ if (to) {
+ toID = ReactMount.getID(to);
+ } else {
+ to = win;
+ }
+ to = to || win;
+ } else {
+ from = win;
+ to = topLevelTarget;
+ toID = topLevelTargetID;
+ }
+
+ if (from === to) {
+ // Nothing pertains to our managed components.
+ return null;
+ }
+
+ var leave = SyntheticMouseEvent.getPooled(eventTypes.mouseLeave, fromID, nativeEvent, nativeEventTarget);
+ leave.type = 'mouseleave';
+ leave.target = from;
+ leave.relatedTarget = to;
+
+ var enter = SyntheticMouseEvent.getPooled(eventTypes.mouseEnter, toID, nativeEvent, nativeEventTarget);
+ enter.type = 'mouseenter';
+ enter.target = to;
+ enter.relatedTarget = from;
+
+ EventPropagators.accumulateEnterLeaveDispatches(leave, enter, fromID, toID);
+
+ extractedEvents[0] = leave;
+ extractedEvents[1] = enter;
+
+ return extractedEvents;
+ }
+
+};
+
+module.exports = EnterLeaveEventPlugin;
+},{"109":109,"15":15,"166":166,"19":19,"72":72}],15:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule EventConstants
+ */
+
+'use strict';
+
+var keyMirror = _dereq_(165);
+
+var PropagationPhases = keyMirror({ bubbled: null, captured: null });
+
+/**
+ * Types of raw signals from the browser caught at the top level.
+ */
+var topLevelTypes = keyMirror({
+ topAbort: null,
+ topBlur: null,
+ topCanPlay: null,
+ topCanPlayThrough: null,
+ topChange: null,
+ topClick: null,
+ topCompositionEnd: null,
+ topCompositionStart: null,
+ topCompositionUpdate: null,
+ topContextMenu: null,
+ topCopy: null,
+ topCut: null,
+ topDoubleClick: null,
+ topDrag: null,
+ topDragEnd: null,
+ topDragEnter: null,
+ topDragExit: null,
+ topDragLeave: null,
+ topDragOver: null,
+ topDragStart: null,
+ topDrop: null,
+ topDurationChange: null,
+ topEmptied: null,
+ topEncrypted: null,
+ topEnded: null,
+ topError: null,
+ topFocus: null,
+ topInput: null,
+ topKeyDown: null,
+ topKeyPress: null,
+ topKeyUp: null,
+ topLoad: null,
+ topLoadedData: null,
+ topLoadedMetadata: null,
+ topLoadStart: null,
+ topMouseDown: null,
+ topMouseMove: null,
+ topMouseOut: null,
+ topMouseOver: null,
+ topMouseUp: null,
+ topPaste: null,
+ topPause: null,
+ topPlay: null,
+ topPlaying: null,
+ topProgress: null,
+ topRateChange: null,
+ topReset: null,
+ topScroll: null,
+ topSeeked: null,
+ topSeeking: null,
+ topSelectionChange: null,
+ topStalled: null,
+ topSubmit: null,
+ topSuspend: null,
+ topTextInput: null,
+ topTimeUpdate: null,
+ topTouchCancel: null,
+ topTouchEnd: null,
+ topTouchMove: null,
+ topTouchStart: null,
+ topVolumeChange: null,
+ topWaiting: null,
+ topWheel: null
+});
+
+var EventConstants = {
+ topLevelTypes: topLevelTypes,
+ PropagationPhases: PropagationPhases
+};
+
+module.exports = EventConstants;
+},{"165":165}],16:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule EventPluginHub
+ */
+
+'use strict';
+
+var EventPluginRegistry = _dereq_(17);
+var EventPluginUtils = _dereq_(18);
+var ReactErrorUtils = _dereq_(61);
+
+var accumulateInto = _dereq_(115);
+var forEachAccumulated = _dereq_(124);
+var invariant = _dereq_(161);
+var warning = _dereq_(173);
+
+/**
+ * Internal store for event listeners
+ */
+var listenerBank = {};
+
+/**
+ * Internal queue of events that have accumulated their dispatches and are
+ * waiting to have their dispatches executed.
+ */
+var eventQueue = null;
+
+/**
+ * Dispatches an event and releases it back into the pool, unless persistent.
+ *
+ * @param {?object} event Synthetic event to be dispatched.
+ * @param {boolean} simulated If the event is simulated (changes exn behavior)
+ * @private
+ */
+var executeDispatchesAndRelease = function (event, simulated) {
+ if (event) {
+ EventPluginUtils.executeDispatchesInOrder(event, simulated);
+
+ if (!event.isPersistent()) {
+ event.constructor.release(event);
+ }
+ }
+};
+var executeDispatchesAndReleaseSimulated = function (e) {
+ return executeDispatchesAndRelease(e, true);
+};
+var executeDispatchesAndReleaseTopLevel = function (e) {
+ return executeDispatchesAndRelease(e, false);
+};
+
+/**
+ * - `InstanceHandle`: [required] Module that performs logical traversals of DOM
+ * hierarchy given ids of the logical DOM elements involved.
+ */
+var InstanceHandle = null;
+
+function validateInstanceHandle() {
+ var valid = InstanceHandle && InstanceHandle.traverseTwoPhase && InstanceHandle.traverseEnterLeave;
+ "production" !== 'production' ? warning(valid, 'InstanceHandle not injected before use!') : undefined;
+}
+
+/**
+ * This is a unified interface for event plugins to be installed and configured.
+ *
+ * Event plugins can implement the following properties:
+ *
+ * `extractEvents` {function(string, DOMEventTarget, string, object): *}
+ * Required. When a top-level event is fired, this method is expected to
+ * extract synthetic events that will in turn be queued and dispatched.
+ *
+ * `eventTypes` {object}
+ * Optional, plugins that fire events must publish a mapping of registration
+ * names that are used to register listeners. Values of this mapping must
+ * be objects that contain `registrationName` or `phasedRegistrationNames`.
+ *
+ * `executeDispatch` {function(object, function, string)}
+ * Optional, allows plugins to override how an event gets dispatched. By
+ * default, the listener is simply invoked.
+ *
+ * Each plugin that is injected into `EventsPluginHub` is immediately operable.
+ *
+ * @public
+ */
+var EventPluginHub = {
+
+ /**
+ * Methods for injecting dependencies.
+ */
+ injection: {
+
+ /**
+ * @param {object} InjectedMount
+ * @public
+ */
+ injectMount: EventPluginUtils.injection.injectMount,
+
+ /**
+ * @param {object} InjectedInstanceHandle
+ * @public
+ */
+ injectInstanceHandle: function (InjectedInstanceHandle) {
+ InstanceHandle = InjectedInstanceHandle;
+ if ("production" !== 'production') {
+ validateInstanceHandle();
+ }
+ },
+
+ getInstanceHandle: function () {
+ if ("production" !== 'production') {
+ validateInstanceHandle();
+ }
+ return InstanceHandle;
+ },
+
+ /**
+ * @param {array} InjectedEventPluginOrder
+ * @public
+ */
+ injectEventPluginOrder: EventPluginRegistry.injectEventPluginOrder,
+
+ /**
+ * @param {object} injectedNamesToPlugins Map from names to plugin modules.
+ */
+ injectEventPluginsByName: EventPluginRegistry.injectEventPluginsByName
+
+ },
+
+ eventNameDispatchConfigs: EventPluginRegistry.eventNameDispatchConfigs,
+
+ registrationNameModules: EventPluginRegistry.registrationNameModules,
+
+ /**
+ * Stores `listener` at `listenerBank[registrationName][id]`. Is idempotent.
+ *
+ * @param {string} id ID of the DOM element.
+ * @param {string} registrationName Name of listener (e.g. `onClick`).
+ * @param {?function} listener The callback to store.
+ */
+ putListener: function (id, registrationName, listener) {
+ !(typeof listener === 'function') ? "production" !== 'production' ? invariant(false, 'Expected %s listener to be a function, instead got type %s', registrationName, typeof listener) : invariant(false) : undefined;
+
+ var bankForRegistrationName = listenerBank[registrationName] || (listenerBank[registrationName] = {});
+ bankForRegistrationName[id] = listener;
+
+ var PluginModule = EventPluginRegistry.registrationNameModules[registrationName];
+ if (PluginModule && PluginModule.didPutListener) {
+ PluginModule.didPutListener(id, registrationName, listener);
+ }
+ },
+
+ /**
+ * @param {string} id ID of the DOM element.
+ * @param {string} registrationName Name of listener (e.g. `onClick`).
+ * @return {?function} The stored callback.
+ */
+ getListener: function (id, registrationName) {
+ var bankForRegistrationName = listenerBank[registrationName];
+ return bankForRegistrationName && bankForRegistrationName[id];
+ },
+
+ /**
+ * Deletes a listener from the registration bank.
+ *
+ * @param {string} id ID of the DOM element.
+ * @param {string} registrationName Name of listener (e.g. `onClick`).
+ */
+ deleteListener: function (id, registrationName) {
+ var PluginModule = EventPluginRegistry.registrationNameModules[registrationName];
+ if (PluginModule && PluginModule.willDeleteListener) {
+ PluginModule.willDeleteListener(id, registrationName);
+ }
+
+ var bankForRegistrationName = listenerBank[registrationName];
+ // TODO: This should never be null -- when is it?
+ if (bankForRegistrationName) {
+ delete bankForRegistrationName[id];
+ }
+ },
+
+ /**
+ * Deletes all listeners for the DOM element with the supplied ID.
+ *
+ * @param {string} id ID of the DOM element.
+ */
+ deleteAllListeners: function (id) {
+ for (var registrationName in listenerBank) {
+ if (!listenerBank[registrationName][id]) {
+ continue;
+ }
+
+ var PluginModule = EventPluginRegistry.registrationNameModules[registrationName];
+ if (PluginModule && PluginModule.willDeleteListener) {
+ PluginModule.willDeleteListener(id, registrationName);
+ }
+
+ delete listenerBank[registrationName][id];
+ }
+ },
+
+ /**
+ * Allows registered plugins an opportunity to extract events from top-level
+ * native browser events.
+ *
+ * @param {string} topLevelType Record from `EventConstants`.
+ * @param {DOMEventTarget} topLevelTarget The listening component root node.
+ * @param {string} topLevelTargetID ID of `topLevelTarget`.
+ * @param {object} nativeEvent Native browser event.
+ * @return {*} An accumulation of synthetic events.
+ * @internal
+ */
+ extractEvents: function (topLevelType, topLevelTarget, topLevelTargetID, nativeEvent, nativeEventTarget) {
+ var events;
+ var plugins = EventPluginRegistry.plugins;
+ for (var i = 0; i < plugins.length; i++) {
+ // Not every plugin in the ordering may be loaded at runtime.
+ var possiblePlugin = plugins[i];
+ if (possiblePlugin) {
+ var extractedEvents = possiblePlugin.extractEvents(topLevelType, topLevelTarget, topLevelTargetID, nativeEvent, nativeEventTarget);
+ if (extractedEvents) {
+ events = accumulateInto(events, extractedEvents);
+ }
+ }
+ }
+ return events;
+ },
+
+ /**
+ * Enqueues a synthetic event that should be dispatched when
+ * `processEventQueue` is invoked.
+ *
+ * @param {*} events An accumulation of synthetic events.
+ * @internal
+ */
+ enqueueEvents: function (events) {
+ if (events) {
+ eventQueue = accumulateInto(eventQueue, events);
+ }
+ },
+
+ /**
+ * Dispatches all synthetic events on the event queue.
+ *
+ * @internal
+ */
+ processEventQueue: function (simulated) {
+ // Set `eventQueue` to null before processing it so that we can tell if more
+ // events get enqueued while processing.
+ var processingEventQueue = eventQueue;
+ eventQueue = null;
+ if (simulated) {
+ forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseSimulated);
+ } else {
+ forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel);
+ }
+ !!eventQueue ? "production" !== 'production' ? invariant(false, 'processEventQueue(): Additional events were enqueued while processing ' + 'an event queue. Support for this has not yet been implemented.') : invariant(false) : undefined;
+ // This would be a good time to rethrow if any of the event handlers threw.
+ ReactErrorUtils.rethrowCaughtError();
+ },
+
+ /**
+ * These are needed for tests only. Do not use!
+ */
+ __purge: function () {
+ listenerBank = {};
+ },
+
+ __getListenerBank: function () {
+ return listenerBank;
+ }
+
+};
+
+module.exports = EventPluginHub;
+},{"115":115,"124":124,"161":161,"17":17,"173":173,"18":18,"61":61}],17:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule EventPluginRegistry
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var invariant = _dereq_(161);
+
+/**
+ * Injectable ordering of event plugins.
+ */
+var EventPluginOrder = null;
+
+/**
+ * Injectable mapping from names to event plugin modules.
+ */
+var namesToPlugins = {};
+
+/**
+ * Recomputes the plugin list using the injected plugins and plugin ordering.
+ *
+ * @private
+ */
+function recomputePluginOrdering() {
+ if (!EventPluginOrder) {
+ // Wait until an `EventPluginOrder` is injected.
+ return;
+ }
+ for (var pluginName in namesToPlugins) {
+ var PluginModule = namesToPlugins[pluginName];
+ var pluginIndex = EventPluginOrder.indexOf(pluginName);
+ !(pluginIndex > -1) ? "production" !== 'production' ? invariant(false, 'EventPluginRegistry: Cannot inject event plugins that do not exist in ' + 'the plugin ordering, `%s`.', pluginName) : invariant(false) : undefined;
+ if (EventPluginRegistry.plugins[pluginIndex]) {
+ continue;
+ }
+ !PluginModule.extractEvents ? "production" !== 'production' ? invariant(false, 'EventPluginRegistry: Event plugins must implement an `extractEvents` ' + 'method, but `%s` does not.', pluginName) : invariant(false) : undefined;
+ EventPluginRegistry.plugins[pluginIndex] = PluginModule;
+ var publishedEvents = PluginModule.eventTypes;
+ for (var eventName in publishedEvents) {
+ !publishEventForPlugin(publishedEvents[eventName], PluginModule, eventName) ? "production" !== 'production' ? invariant(false, 'EventPluginRegistry: Failed to publish event `%s` for plugin `%s`.', eventName, pluginName) : invariant(false) : undefined;
+ }
+ }
+}
+
+/**
+ * Publishes an event so that it can be dispatched by the supplied plugin.
+ *
+ * @param {object} dispatchConfig Dispatch configuration for the event.
+ * @param {object} PluginModule Plugin publishing the event.
+ * @return {boolean} True if the event was successfully published.
+ * @private
+ */
+function publishEventForPlugin(dispatchConfig, PluginModule, eventName) {
+ !!EventPluginRegistry.eventNameDispatchConfigs.hasOwnProperty(eventName) ? "production" !== 'production' ? invariant(false, 'EventPluginHub: More than one plugin attempted to publish the same ' + 'event name, `%s`.', eventName) : invariant(false) : undefined;
+ EventPluginRegistry.eventNameDispatchConfigs[eventName] = dispatchConfig;
+
+ var phasedRegistrationNames = dispatchConfig.phasedRegistrationNames;
+ if (phasedRegistrationNames) {
+ for (var phaseName in phasedRegistrationNames) {
+ if (phasedRegistrationNames.hasOwnProperty(phaseName)) {
+ var phasedRegistrationName = phasedRegistrationNames[phaseName];
+ publishRegistrationName(phasedRegistrationName, PluginModule, eventName);
+ }
+ }
+ return true;
+ } else if (dispatchConfig.registrationName) {
+ publishRegistrationName(dispatchConfig.registrationName, PluginModule, eventName);
+ return true;
+ }
+ return false;
+}
+
+/**
+ * Publishes a registration name that is used to identify dispatched events and
+ * can be used with `EventPluginHub.putListener` to register listeners.
+ *
+ * @param {string} registrationName Registration name to add.
+ * @param {object} PluginModule Plugin publishing the event.
+ * @private
+ */
+function publishRegistrationName(registrationName, PluginModule, eventName) {
+ !!EventPluginRegistry.registrationNameModules[registrationName] ? "production" !== 'production' ? invariant(false, 'EventPluginHub: More than one plugin attempted to publish the same ' + 'registration name, `%s`.', registrationName) : invariant(false) : undefined;
+ EventPluginRegistry.registrationNameModules[registrationName] = PluginModule;
+ EventPluginRegistry.registrationNameDependencies[registrationName] = PluginModule.eventTypes[eventName].dependencies;
+}
+
+/**
+ * Registers plugins so that they can extract and dispatch events.
+ *
+ * @see {EventPluginHub}
+ */
+var EventPluginRegistry = {
+
+ /**
+ * Ordered list of injected plugins.
+ */
+ plugins: [],
+
+ /**
+ * Mapping from event name to dispatch config
+ */
+ eventNameDispatchConfigs: {},
+
+ /**
+ * Mapping from registration name to plugin module
+ */
+ registrationNameModules: {},
+
+ /**
+ * Mapping from registration name to event name
+ */
+ registrationNameDependencies: {},
+
+ /**
+ * Injects an ordering of plugins (by plugin name). This allows the ordering
+ * to be decoupled from injection of the actual plugins so that ordering is
+ * always deterministic regardless of packaging, on-the-fly injection, etc.
+ *
+ * @param {array} InjectedEventPluginOrder
+ * @internal
+ * @see {EventPluginHub.injection.injectEventPluginOrder}
+ */
+ injectEventPluginOrder: function (InjectedEventPluginOrder) {
+ !!EventPluginOrder ? "production" !== 'production' ? invariant(false, 'EventPluginRegistry: Cannot inject event plugin ordering more than ' + 'once. You are likely trying to load more than one copy of React.') : invariant(false) : undefined;
+ // Clone the ordering so it cannot be dynamically mutated.
+ EventPluginOrder = Array.prototype.slice.call(InjectedEventPluginOrder);
+ recomputePluginOrdering();
+ },
+
+ /**
+ * Injects plugins to be used by `EventPluginHub`. The plugin names must be
+ * in the ordering injected by `injectEventPluginOrder`.
+ *
+ * Plugins can be injected as part of page initialization or on-the-fly.
+ *
+ * @param {object} injectedNamesToPlugins Map from names to plugin modules.
+ * @internal
+ * @see {EventPluginHub.injection.injectEventPluginsByName}
+ */
+ injectEventPluginsByName: function (injectedNamesToPlugins) {
+ var isOrderingDirty = false;
+ for (var pluginName in injectedNamesToPlugins) {
+ if (!injectedNamesToPlugins.hasOwnProperty(pluginName)) {
+ continue;
+ }
+ var PluginModule = injectedNamesToPlugins[pluginName];
+ if (!namesToPlugins.hasOwnProperty(pluginName) || namesToPlugins[pluginName] !== PluginModule) {
+ !!namesToPlugins[pluginName] ? "production" !== 'production' ? invariant(false, 'EventPluginRegistry: Cannot inject two different event plugins ' + 'using the same name, `%s`.', pluginName) : invariant(false) : undefined;
+ namesToPlugins[pluginName] = PluginModule;
+ isOrderingDirty = true;
+ }
+ }
+ if (isOrderingDirty) {
+ recomputePluginOrdering();
+ }
+ },
+
+ /**
+ * Looks up the plugin for the supplied event.
+ *
+ * @param {object} event A synthetic event.
+ * @return {?object} The plugin that created the supplied event.
+ * @internal
+ */
+ getPluginModuleForEvent: function (event) {
+ var dispatchConfig = event.dispatchConfig;
+ if (dispatchConfig.registrationName) {
+ return EventPluginRegistry.registrationNameModules[dispatchConfig.registrationName] || null;
+ }
+ for (var phase in dispatchConfig.phasedRegistrationNames) {
+ if (!dispatchConfig.phasedRegistrationNames.hasOwnProperty(phase)) {
+ continue;
+ }
+ var PluginModule = EventPluginRegistry.registrationNameModules[dispatchConfig.phasedRegistrationNames[phase]];
+ if (PluginModule) {
+ return PluginModule;
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Exposed for unit testing.
+ * @private
+ */
+ _resetEventPlugins: function () {
+ EventPluginOrder = null;
+ for (var pluginName in namesToPlugins) {
+ if (namesToPlugins.hasOwnProperty(pluginName)) {
+ delete namesToPlugins[pluginName];
+ }
+ }
+ EventPluginRegistry.plugins.length = 0;
+
+ var eventNameDispatchConfigs = EventPluginRegistry.eventNameDispatchConfigs;
+ for (var eventName in eventNameDispatchConfigs) {
+ if (eventNameDispatchConfigs.hasOwnProperty(eventName)) {
+ delete eventNameDispatchConfigs[eventName];
+ }
+ }
+
+ var registrationNameModules = EventPluginRegistry.registrationNameModules;
+ for (var registrationName in registrationNameModules) {
+ if (registrationNameModules.hasOwnProperty(registrationName)) {
+ delete registrationNameModules[registrationName];
+ }
+ }
+ }
+
+};
+
+module.exports = EventPluginRegistry;
+},{"161":161}],18:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule EventPluginUtils
+ */
+
+'use strict';
+
+var EventConstants = _dereq_(15);
+var ReactErrorUtils = _dereq_(61);
+
+var invariant = _dereq_(161);
+var warning = _dereq_(173);
+
+/**
+ * Injected dependencies:
+ */
+
+/**
+ * - `Mount`: [required] Module that can convert between React dom IDs and
+ * actual node references.
+ */
+var injection = {
+ Mount: null,
+ injectMount: function (InjectedMount) {
+ injection.Mount = InjectedMount;
+ if ("production" !== 'production') {
+ "production" !== 'production' ? warning(InjectedMount && InjectedMount.getNode && InjectedMount.getID, 'EventPluginUtils.injection.injectMount(...): Injected Mount ' + 'module is missing getNode or getID.') : undefined;
+ }
+ }
+};
+
+var topLevelTypes = EventConstants.topLevelTypes;
+
+function isEndish(topLevelType) {
+ return topLevelType === topLevelTypes.topMouseUp || topLevelType === topLevelTypes.topTouchEnd || topLevelType === topLevelTypes.topTouchCancel;
+}
+
+function isMoveish(topLevelType) {
+ return topLevelType === topLevelTypes.topMouseMove || topLevelType === topLevelTypes.topTouchMove;
+}
+function isStartish(topLevelType) {
+ return topLevelType === topLevelTypes.topMouseDown || topLevelType === topLevelTypes.topTouchStart;
+}
+
+var validateEventDispatches;
+if ("production" !== 'production') {
+ validateEventDispatches = function (event) {
+ var dispatchListeners = event._dispatchListeners;
+ var dispatchIDs = event._dispatchIDs;
+
+ var listenersIsArr = Array.isArray(dispatchListeners);
+ var idsIsArr = Array.isArray(dispatchIDs);
+ var IDsLen = idsIsArr ? dispatchIDs.length : dispatchIDs ? 1 : 0;
+ var listenersLen = listenersIsArr ? dispatchListeners.length : dispatchListeners ? 1 : 0;
+
+ "production" !== 'production' ? warning(idsIsArr === listenersIsArr && IDsLen === listenersLen, 'EventPluginUtils: Invalid `event`.') : undefined;
+ };
+}
+
+/**
+ * Dispatch the event to the listener.
+ * @param {SyntheticEvent} event SyntheticEvent to handle
+ * @param {boolean} simulated If the event is simulated (changes exn behavior)
+ * @param {function} listener Application-level callback
+ * @param {string} domID DOM id to pass to the callback.
+ */
+function executeDispatch(event, simulated, listener, domID) {
+ var type = event.type || 'unknown-event';
+ event.currentTarget = injection.Mount.getNode(domID);
+ if (simulated) {
+ ReactErrorUtils.invokeGuardedCallbackWithCatch(type, listener, event, domID);
+ } else {
+ ReactErrorUtils.invokeGuardedCallback(type, listener, event, domID);
+ }
+ event.currentTarget = null;
+}
+
+/**
+ * Standard/simple iteration through an event's collected dispatches.
+ */
+function executeDispatchesInOrder(event, simulated) {
+ var dispatchListeners = event._dispatchListeners;
+ var dispatchIDs = event._dispatchIDs;
+ if ("production" !== 'production') {
+ validateEventDispatches(event);
+ }
+ if (Array.isArray(dispatchListeners)) {
+ for (var i = 0; i < dispatchListeners.length; i++) {
+ if (event.isPropagationStopped()) {
+ break;
+ }
+ // Listeners and IDs are two parallel arrays that are always in sync.
+ executeDispatch(event, simulated, dispatchListeners[i], dispatchIDs[i]);
+ }
+ } else if (dispatchListeners) {
+ executeDispatch(event, simulated, dispatchListeners, dispatchIDs);
+ }
+ event._dispatchListeners = null;
+ event._dispatchIDs = null;
+}
+
+/**
+ * Standard/simple iteration through an event's collected dispatches, but stops
+ * at the first dispatch execution returning true, and returns that id.
+ *
+ * @return {?string} id of the first dispatch execution who's listener returns
+ * true, or null if no listener returned true.
+ */
+function executeDispatchesInOrderStopAtTrueImpl(event) {
+ var dispatchListeners = event._dispatchListeners;
+ var dispatchIDs = event._dispatchIDs;
+ if ("production" !== 'production') {
+ validateEventDispatches(event);
+ }
+ if (Array.isArray(dispatchListeners)) {
+ for (var i = 0; i < dispatchListeners.length; i++) {
+ if (event.isPropagationStopped()) {
+ break;
+ }
+ // Listeners and IDs are two parallel arrays that are always in sync.
+ if (dispatchListeners[i](event, dispatchIDs[i])) {
+ return dispatchIDs[i];
+ }
+ }
+ } else if (dispatchListeners) {
+ if (dispatchListeners(event, dispatchIDs)) {
+ return dispatchIDs;
+ }
+ }
+ return null;
+}
+
+/**
+ * @see executeDispatchesInOrderStopAtTrueImpl
+ */
+function executeDispatchesInOrderStopAtTrue(event) {
+ var ret = executeDispatchesInOrderStopAtTrueImpl(event);
+ event._dispatchIDs = null;
+ event._dispatchListeners = null;
+ return ret;
+}
+
+/**
+ * Execution of a "direct" dispatch - there must be at most one dispatch
+ * accumulated on the event or it is considered an error. It doesn't really make
+ * sense for an event with multiple dispatches (bubbled) to keep track of the
+ * return values at each dispatch execution, but it does tend to make sense when
+ * dealing with "direct" dispatches.
+ *
+ * @return {*} The return value of executing the single dispatch.
+ */
+function executeDirectDispatch(event) {
+ if ("production" !== 'production') {
+ validateEventDispatches(event);
+ }
+ var dispatchListener = event._dispatchListeners;
+ var dispatchID = event._dispatchIDs;
+ !!Array.isArray(dispatchListener) ? "production" !== 'production' ? invariant(false, 'executeDirectDispatch(...): Invalid `event`.') : invariant(false) : undefined;
+ var res = dispatchListener ? dispatchListener(event, dispatchID) : null;
+ event._dispatchListeners = null;
+ event._dispatchIDs = null;
+ return res;
+}
+
+/**
+ * @param {SyntheticEvent} event
+ * @return {boolean} True iff number of dispatches accumulated is greater than 0.
+ */
+function hasDispatches(event) {
+ return !!event._dispatchListeners;
+}
+
+/**
+ * General utilities that are useful in creating custom Event Plugins.
+ */
+var EventPluginUtils = {
+ isEndish: isEndish,
+ isMoveish: isMoveish,
+ isStartish: isStartish,
+
+ executeDirectDispatch: executeDirectDispatch,
+ executeDispatchesInOrder: executeDispatchesInOrder,
+ executeDispatchesInOrderStopAtTrue: executeDispatchesInOrderStopAtTrue,
+ hasDispatches: hasDispatches,
+
+ getNode: function (id) {
+ return injection.Mount.getNode(id);
+ },
+ getID: function (node) {
+ return injection.Mount.getID(node);
+ },
+
+ injection: injection
+};
+
+module.exports = EventPluginUtils;
+},{"15":15,"161":161,"173":173,"61":61}],19:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule EventPropagators
+ */
+
+'use strict';
+
+var EventConstants = _dereq_(15);
+var EventPluginHub = _dereq_(16);
+
+var warning = _dereq_(173);
+
+var accumulateInto = _dereq_(115);
+var forEachAccumulated = _dereq_(124);
+
+var PropagationPhases = EventConstants.PropagationPhases;
+var getListener = EventPluginHub.getListener;
+
+/**
+ * Some event types have a notion of different registration names for different
+ * "phases" of propagation. This finds listeners by a given phase.
+ */
+function listenerAtPhase(id, event, propagationPhase) {
+ var registrationName = event.dispatchConfig.phasedRegistrationNames[propagationPhase];
+ return getListener(id, registrationName);
+}
+
+/**
+ * Tags a `SyntheticEvent` with dispatched listeners. Creating this function
+ * here, allows us to not have to bind or create functions for each event.
+ * Mutating the event's members allows us to not have to create a wrapping
+ * "dispatch" object that pairs the event with the listener.
+ */
+function accumulateDirectionalDispatches(domID, upwards, event) {
+ if ("production" !== 'production') {
+ "production" !== 'production' ? warning(domID, 'Dispatching id must not be null') : undefined;
+ }
+ var phase = upwards ? PropagationPhases.bubbled : PropagationPhases.captured;
+ var listener = listenerAtPhase(domID, event, phase);
+ if (listener) {
+ event._dispatchListeners = accumulateInto(event._dispatchListeners, listener);
+ event._dispatchIDs = accumulateInto(event._dispatchIDs, domID);
+ }
+}
+
+/**
+ * Collect dispatches (must be entirely collected before dispatching - see unit
+ * tests). Lazily allocate the array to conserve memory. We must loop through
+ * each event and perform the traversal for each one. We cannot perform a
+ * single traversal for the entire collection of events because each event may
+ * have a different target.
+ */
+function accumulateTwoPhaseDispatchesSingle(event) {
+ if (event && event.dispatchConfig.phasedRegistrationNames) {
+ EventPluginHub.injection.getInstanceHandle().traverseTwoPhase(event.dispatchMarker, accumulateDirectionalDispatches, event);
+ }
+}
+
+/**
+ * Same as `accumulateTwoPhaseDispatchesSingle`, but skips over the targetID.
+ */
+function accumulateTwoPhaseDispatchesSingleSkipTarget(event) {
+ if (event && event.dispatchConfig.phasedRegistrationNames) {
+ EventPluginHub.injection.getInstanceHandle().traverseTwoPhaseSkipTarget(event.dispatchMarker, accumulateDirectionalDispatches, event);
+ }
+}
+
+/**
+ * Accumulates without regard to direction, does not look for phased
+ * registration names. Same as `accumulateDirectDispatchesSingle` but without
+ * requiring that the `dispatchMarker` be the same as the dispatched ID.
+ */
+function accumulateDispatches(id, ignoredDirection, event) {
+ if (event && event.dispatchConfig.registrationName) {
+ var registrationName = event.dispatchConfig.registrationName;
+ var listener = getListener(id, registrationName);
+ if (listener) {
+ event._dispatchListeners = accumulateInto(event._dispatchListeners, listener);
+ event._dispatchIDs = accumulateInto(event._dispatchIDs, id);
+ }
+ }
+}
+
+/**
+ * Accumulates dispatches on an `SyntheticEvent`, but only for the
+ * `dispatchMarker`.
+ * @param {SyntheticEvent} event
+ */
+function accumulateDirectDispatchesSingle(event) {
+ if (event && event.dispatchConfig.registrationName) {
+ accumulateDispatches(event.dispatchMarker, null, event);
+ }
+}
+
+function accumulateTwoPhaseDispatches(events) {
+ forEachAccumulated(events, accumulateTwoPhaseDispatchesSingle);
+}
+
+function accumulateTwoPhaseDispatchesSkipTarget(events) {
+ forEachAccumulated(events, accumulateTwoPhaseDispatchesSingleSkipTarget);
+}
+
+function accumulateEnterLeaveDispatches(leave, enter, fromID, toID) {
+ EventPluginHub.injection.getInstanceHandle().traverseEnterLeave(fromID, toID, accumulateDispatches, leave, enter);
+}
+
+function accumulateDirectDispatches(events) {
+ forEachAccumulated(events, accumulateDirectDispatchesSingle);
+}
+
+/**
+ * A small set of propagation patterns, each of which will accept a small amount
+ * of information, and generate a set of "dispatch ready event objects" - which
+ * are sets of events that have already been annotated with a set of dispatched
+ * listener functions/ids. The API is designed this way to discourage these
+ * propagation strategies from actually executing the dispatches, since we
+ * always want to collect the entire set of dispatches before executing event a
+ * single one.
+ *
+ * @constructor EventPropagators
+ */
+var EventPropagators = {
+ accumulateTwoPhaseDispatches: accumulateTwoPhaseDispatches,
+ accumulateTwoPhaseDispatchesSkipTarget: accumulateTwoPhaseDispatchesSkipTarget,
+ accumulateDirectDispatches: accumulateDirectDispatches,
+ accumulateEnterLeaveDispatches: accumulateEnterLeaveDispatches
+};
+
+module.exports = EventPropagators;
+},{"115":115,"124":124,"15":15,"16":16,"173":173}],20:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule FallbackCompositionState
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var PooledClass = _dereq_(25);
+
+var assign = _dereq_(24);
+var getTextContentAccessor = _dereq_(131);
+
+/**
+ * This helper class stores information about text content of a target node,
+ * allowing comparison of content before and after a given event.
+ *
+ * Identify the node where selection currently begins, then observe
+ * both its text content and its current position in the DOM. Since the
+ * browser may natively replace the target node during composition, we can
+ * use its position to find its replacement.
+ *
+ * @param {DOMEventTarget} root
+ */
+function FallbackCompositionState(root) {
+ this._root = root;
+ this._startText = this.getText();
+ this._fallbackText = null;
+}
+
+assign(FallbackCompositionState.prototype, {
+ destructor: function () {
+ this._root = null;
+ this._startText = null;
+ this._fallbackText = null;
+ },
+
+ /**
+ * Get current text of input.
+ *
+ * @return {string}
+ */
+ getText: function () {
+ if ('value' in this._root) {
+ return this._root.value;
+ }
+ return this._root[getTextContentAccessor()];
+ },
+
+ /**
+ * Determine the differing substring between the initially stored
+ * text content and the current content.
+ *
+ * @return {string}
+ */
+ getData: function () {
+ if (this._fallbackText) {
+ return this._fallbackText;
+ }
+
+ var start;
+ var startValue = this._startText;
+ var startLength = startValue.length;
+ var end;
+ var endValue = this.getText();
+ var endLength = endValue.length;
+
+ for (start = 0; start < startLength; start++) {
+ if (startValue[start] !== endValue[start]) {
+ break;
+ }
+ }
+
+ var minEnd = startLength - start;
+ for (end = 1; end <= minEnd; end++) {
+ if (startValue[startLength - end] !== endValue[endLength - end]) {
+ break;
+ }
+ }
+
+ var sliceTail = end > 1 ? 1 - end : undefined;
+ this._fallbackText = endValue.slice(start, sliceTail);
+ return this._fallbackText;
+ }
+});
+
+PooledClass.addPoolingTo(FallbackCompositionState);
+
+module.exports = FallbackCompositionState;
+},{"131":131,"24":24,"25":25}],21:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule HTMLDOMPropertyConfig
+ */
+
+'use strict';
+
+var DOMProperty = _dereq_(10);
+var ExecutionEnvironment = _dereq_(147);
+
+var MUST_USE_ATTRIBUTE = DOMProperty.injection.MUST_USE_ATTRIBUTE;
+var MUST_USE_PROPERTY = DOMProperty.injection.MUST_USE_PROPERTY;
+var HAS_BOOLEAN_VALUE = DOMProperty.injection.HAS_BOOLEAN_VALUE;
+var HAS_SIDE_EFFECTS = DOMProperty.injection.HAS_SIDE_EFFECTS;
+var HAS_NUMERIC_VALUE = DOMProperty.injection.HAS_NUMERIC_VALUE;
+var HAS_POSITIVE_NUMERIC_VALUE = DOMProperty.injection.HAS_POSITIVE_NUMERIC_VALUE;
+var HAS_OVERLOADED_BOOLEAN_VALUE = DOMProperty.injection.HAS_OVERLOADED_BOOLEAN_VALUE;
+
+var hasSVG;
+if (ExecutionEnvironment.canUseDOM) {
+ var implementation = document.implementation;
+ hasSVG = implementation && implementation.hasFeature && implementation.hasFeature('http://www.w3.org/TR/SVG11/feature#BasicStructure', '1.1');
+}
+
+var HTMLDOMPropertyConfig = {
+ isCustomAttribute: RegExp.prototype.test.bind(/^(data|aria)-[a-z_][a-z\d_.\-]*$/),
+ Properties: {
+ /**
+ * Standard Properties
+ */
+ accept: null,
+ acceptCharset: null,
+ accessKey: null,
+ action: null,
+ allowFullScreen: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE,
+ allowTransparency: MUST_USE_ATTRIBUTE,
+ alt: null,
+ async: HAS_BOOLEAN_VALUE,
+ autoComplete: null,
+ // autoFocus is polyfilled/normalized by AutoFocusUtils
+ // autoFocus: HAS_BOOLEAN_VALUE,
+ autoPlay: HAS_BOOLEAN_VALUE,
+ capture: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE,
+ cellPadding: null,
+ cellSpacing: null,
+ charSet: MUST_USE_ATTRIBUTE,
+ challenge: MUST_USE_ATTRIBUTE,
+ checked: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,
+ classID: MUST_USE_ATTRIBUTE,
+ // To set className on SVG elements, it's necessary to use .setAttribute;
+ // this works on HTML elements too in all browsers except IE8. Conveniently,
+ // IE8 doesn't support SVG and so we can simply use the attribute in
+ // browsers that support SVG and the property in browsers that don't,
+ // regardless of whether the element is HTML or SVG.
+ className: hasSVG ? MUST_USE_ATTRIBUTE : MUST_USE_PROPERTY,
+ cols: MUST_USE_ATTRIBUTE | HAS_POSITIVE_NUMERIC_VALUE,
+ colSpan: null,
+ content: null,
+ contentEditable: null,
+ contextMenu: MUST_USE_ATTRIBUTE,
+ controls: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,
+ coords: null,
+ crossOrigin: null,
+ data: null, // For `<object />` acts as `src`.
+ dateTime: MUST_USE_ATTRIBUTE,
+ 'default': HAS_BOOLEAN_VALUE,
+ defer: HAS_BOOLEAN_VALUE,
+ dir: null,
+ disabled: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE,
+ download: HAS_OVERLOADED_BOOLEAN_VALUE,
+ draggable: null,
+ encType: null,
+ form: MUST_USE_ATTRIBUTE,
+ formAction: MUST_USE_ATTRIBUTE,
+ formEncType: MUST_USE_ATTRIBUTE,
+ formMethod: MUST_USE_ATTRIBUTE,
+ formNoValidate: HAS_BOOLEAN_VALUE,
+ formTarget: MUST_USE_ATTRIBUTE,
+ frameBorder: MUST_USE_ATTRIBUTE,
+ headers: null,
+ height: MUST_USE_ATTRIBUTE,
+ hidden: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE,
+ high: null,
+ href: null,
+ hrefLang: null,
+ htmlFor: null,
+ httpEquiv: null,
+ icon: null,
+ id: MUST_USE_PROPERTY,
+ inputMode: MUST_USE_ATTRIBUTE,
+ integrity: null,
+ is: MUST_USE_ATTRIBUTE,
+ keyParams: MUST_USE_ATTRIBUTE,
+ keyType: MUST_USE_ATTRIBUTE,
+ kind: null,
+ label: null,
+ lang: null,
+ list: MUST_USE_ATTRIBUTE,
+ loop: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,
+ low: null,
+ manifest: MUST_USE_ATTRIBUTE,
+ marginHeight: null,
+ marginWidth: null,
+ max: null,
+ maxLength: MUST_USE_ATTRIBUTE,
+ media: MUST_USE_ATTRIBUTE,
+ mediaGroup: null,
+ method: null,
+ min: null,
+ minLength: MUST_USE_ATTRIBUTE,
+ multiple: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,
+ muted: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,
+ name: null,
+ nonce: MUST_USE_ATTRIBUTE,
+ noValidate: HAS_BOOLEAN_VALUE,
+ open: HAS_BOOLEAN_VALUE,
+ optimum: null,
+ pattern: null,
+ placeholder: null,
+ poster: null,
+ preload: null,
+ radioGroup: null,
+ readOnly: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,
+ rel: null,
+ required: HAS_BOOLEAN_VALUE,
+ reversed: HAS_BOOLEAN_VALUE,
+ role: MUST_USE_ATTRIBUTE,
+ rows: MUST_USE_ATTRIBUTE | HAS_POSITIVE_NUMERIC_VALUE,
+ rowSpan: null,
+ sandbox: null,
+ scope: null,
+ scoped: HAS_BOOLEAN_VALUE,
+ scrolling: null,
+ seamless: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE,
+ selected: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,
+ shape: null,
+ size: MUST_USE_ATTRIBUTE | HAS_POSITIVE_NUMERIC_VALUE,
+ sizes: MUST_USE_ATTRIBUTE,
+ span: HAS_POSITIVE_NUMERIC_VALUE,
+ spellCheck: null,
+ src: null,
+ srcDoc: MUST_USE_PROPERTY,
+ srcLang: null,
+ srcSet: MUST_USE_ATTRIBUTE,
+ start: HAS_NUMERIC_VALUE,
+ step: null,
+ style: null,
+ summary: null,
+ tabIndex: null,
+ target: null,
+ title: null,
+ type: null,
+ useMap: null,
+ value: MUST_USE_PROPERTY | HAS_SIDE_EFFECTS,
+ width: MUST_USE_ATTRIBUTE,
+ wmode: MUST_USE_ATTRIBUTE,
+ wrap: null,
+
+ /**
+ * RDFa Properties
+ */
+ about: MUST_USE_ATTRIBUTE,
+ datatype: MUST_USE_ATTRIBUTE,
+ inlist: MUST_USE_ATTRIBUTE,
+ prefix: MUST_USE_ATTRIBUTE,
+ // property is also supported for OpenGraph in meta tags.
+ property: MUST_USE_ATTRIBUTE,
+ resource: MUST_USE_ATTRIBUTE,
+ 'typeof': MUST_USE_ATTRIBUTE,
+ vocab: MUST_USE_ATTRIBUTE,
+
+ /**
+ * Non-standard Properties
+ */
+ // autoCapitalize and autoCorrect are supported in Mobile Safari for
+ // keyboard hints.
+ autoCapitalize: MUST_USE_ATTRIBUTE,
+ autoCorrect: MUST_USE_ATTRIBUTE,
+ // autoSave allows WebKit/Blink to persist values of input fields on page reloads
+ autoSave: null,
+ // color is for Safari mask-icon link
+ color: null,
+ // itemProp, itemScope, itemType are for
+ // Microdata support. See http://schema.org/docs/gs.html
+ itemProp: MUST_USE_ATTRIBUTE,
+ itemScope: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE,
+ itemType: MUST_USE_ATTRIBUTE,
+ // itemID and itemRef are for Microdata support as well but
+ // only specified in the the WHATWG spec document. See
+ // https://html.spec.whatwg.org/multipage/microdata.html#microdata-dom-api
+ itemID: MUST_USE_ATTRIBUTE,
+ itemRef: MUST_USE_ATTRIBUTE,
+ // results show looking glass icon and recent searches on input
+ // search fields in WebKit/Blink
+ results: null,
+ // IE-only attribute that specifies security restrictions on an iframe
+ // as an alternative to the sandbox attribute on IE<10
+ security: MUST_USE_ATTRIBUTE,
+ // IE-only attribute that controls focus behavior
+ unselectable: MUST_USE_ATTRIBUTE
+ },
+ DOMAttributeNames: {
+ acceptCharset: 'accept-charset',
+ className: 'class',
+ htmlFor: 'for',
+ httpEquiv: 'http-equiv'
+ },
+ DOMPropertyNames: {
+ autoComplete: 'autocomplete',
+ autoFocus: 'autofocus',
+ autoPlay: 'autoplay',
+ autoSave: 'autosave',
+ // `encoding` is equivalent to `enctype`, IE8 lacks an `enctype` setter.
+ // http://www.w3.org/TR/html5/forms.html#dom-fs-encoding
+ encType: 'encoding',
+ hrefLang: 'hreflang',
+ radioGroup: 'radiogroup',
+ spellCheck: 'spellcheck',
+ srcDoc: 'srcdoc',
+ srcSet: 'srcset'
+ }
+};
+
+module.exports = HTMLDOMPropertyConfig;
+},{"10":10,"147":147}],22:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule LinkedStateMixin
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var ReactLink = _dereq_(70);
+var ReactStateSetters = _dereq_(90);
+
+/**
+ * A simple mixin around ReactLink.forState().
+ */
+var LinkedStateMixin = {
+ /**
+ * Create a ReactLink that's linked to part of this component's state. The
+ * ReactLink will have the current value of this.state[key] and will call
+ * setState() when a change is requested.
+ *
+ * @param {string} key state key to update. Note: you may want to use keyOf()
+ * if you're using Google Closure Compiler advanced mode.
+ * @return {ReactLink} ReactLink instance linking to the state.
+ */
+ linkState: function (key) {
+ return new ReactLink(this.state[key], ReactStateSetters.createStateKeySetter(this, key));
+ }
+};
+
+module.exports = LinkedStateMixin;
+},{"70":70,"90":90}],23:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule LinkedValueUtils
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var ReactPropTypes = _dereq_(82);
+var ReactPropTypeLocations = _dereq_(81);
+
+var invariant = _dereq_(161);
+var warning = _dereq_(173);
+
+var hasReadOnlyValue = {
+ 'button': true,
+ 'checkbox': true,
+ 'image': true,
+ 'hidden': true,
+ 'radio': true,
+ 'reset': true,
+ 'submit': true
+};
+
+function _assertSingleLink(inputProps) {
+ !(inputProps.checkedLink == null || inputProps.valueLink == null) ? "production" !== 'production' ? invariant(false, 'Cannot provide a checkedLink and a valueLink. If you want to use ' + 'checkedLink, you probably don\'t want to use valueLink and vice versa.') : invariant(false) : undefined;
+}
+function _assertValueLink(inputProps) {
+ _assertSingleLink(inputProps);
+ !(inputProps.value == null && inputProps.onChange == null) ? "production" !== 'production' ? invariant(false, 'Cannot provide a valueLink and a value or onChange event. If you want ' + 'to use value or onChange, you probably don\'t want to use valueLink.') : invariant(false) : undefined;
+}
+
+function _assertCheckedLink(inputProps) {
+ _assertSingleLink(inputProps);
+ !(inputProps.checked == null && inputProps.onChange == null) ? "production" !== 'production' ? invariant(false, 'Cannot provide a checkedLink and a checked property or onChange event. ' + 'If you want to use checked or onChange, you probably don\'t want to ' + 'use checkedLink') : invariant(false) : undefined;
+}
+
+var propTypes = {
+ value: function (props, propName, componentName) {
+ if (!props[propName] || hasReadOnlyValue[props.type] || props.onChange || props.readOnly || props.disabled) {
+ return null;
+ }
+ return new Error('You provided a `value` prop to a form field without an ' + '`onChange` handler. This will render a read-only field. If ' + 'the field should be mutable use `defaultValue`. Otherwise, ' + 'set either `onChange` or `readOnly`.');
+ },
+ checked: function (props, propName, componentName) {
+ if (!props[propName] || props.onChange || props.readOnly || props.disabled) {
+ return null;
+ }
+ return new Error('You provided a `checked` prop to a form field without an ' + '`onChange` handler. This will render a read-only field. If ' + 'the field should be mutable use `defaultChecked`. Otherwise, ' + 'set either `onChange` or `readOnly`.');
+ },
+ onChange: ReactPropTypes.func
+};
+
+var loggedTypeFailures = {};
+function getDeclarationErrorAddendum(owner) {
+ if (owner) {
+ var name = owner.getName();
+ if (name) {
+ return ' Check the render method of `' + name + '`.';
+ }
+ }
+ return '';
+}
+
+/**
+ * Provide a linked `value` attribute for controlled forms. You should not use
+ * this outside of the ReactDOM controlled form components.
+ */
+var LinkedValueUtils = {
+ checkPropTypes: function (tagName, props, owner) {
+ for (var propName in propTypes) {
+ if (propTypes.hasOwnProperty(propName)) {
+ var error = propTypes[propName](props, propName, tagName, ReactPropTypeLocations.prop);
+ }
+ if (error instanceof Error && !(error.message in loggedTypeFailures)) {
+ // Only monitor this failure once because there tends to be a lot of the
+ // same error.
+ loggedTypeFailures[error.message] = true;
+
+ var addendum = getDeclarationErrorAddendum(owner);
+ "production" !== 'production' ? warning(false, 'Failed form propType: %s%s', error.message, addendum) : undefined;
+ }
+ }
+ },
+
+ /**
+ * @param {object} inputProps Props for form component
+ * @return {*} current value of the input either from value prop or link.
+ */
+ getValue: function (inputProps) {
+ if (inputProps.valueLink) {
+ _assertValueLink(inputProps);
+ return inputProps.valueLink.value;
+ }
+ return inputProps.value;
+ },
+
+ /**
+ * @param {object} inputProps Props for form component
+ * @return {*} current checked status of the input either from checked prop
+ * or link.
+ */
+ getChecked: function (inputProps) {
+ if (inputProps.checkedLink) {
+ _assertCheckedLink(inputProps);
+ return inputProps.checkedLink.value;
+ }
+ return inputProps.checked;
+ },
+
+ /**
+ * @param {object} inputProps Props for form component
+ * @param {SyntheticEvent} event change event to handle
+ */
+ executeOnChange: function (inputProps, event) {
+ if (inputProps.valueLink) {
+ _assertValueLink(inputProps);
+ return inputProps.valueLink.requestChange(event.target.value);
+ } else if (inputProps.checkedLink) {
+ _assertCheckedLink(inputProps);
+ return inputProps.checkedLink.requestChange(event.target.checked);
+ } else if (inputProps.onChange) {
+ return inputProps.onChange.call(undefined, event);
+ }
+ }
+};
+
+module.exports = LinkedValueUtils;
+},{"161":161,"173":173,"81":81,"82":82}],24:[function(_dereq_,module,exports){
+/**
+ * Copyright 2014-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule Object.assign
+ */
+
+// https://people.mozilla.org/~jorendorff/es6-draft.html#sec-object.assign
+
+'use strict';
+
+function assign(target, sources) {
+ if (target == null) {
+ throw new TypeError('Object.assign target cannot be null or undefined');
+ }
+
+ var to = Object(target);
+ var hasOwnProperty = Object.prototype.hasOwnProperty;
+
+ for (var nextIndex = 1; nextIndex < arguments.length; nextIndex++) {
+ var nextSource = arguments[nextIndex];
+ if (nextSource == null) {
+ continue;
+ }
+
+ var from = Object(nextSource);
+
+ // We don't currently support accessors nor proxies. Therefore this
+ // copy cannot throw. If we ever supported this then we must handle
+ // exceptions and side-effects. We don't support symbols so they won't
+ // be transferred.
+
+ for (var key in from) {
+ if (hasOwnProperty.call(from, key)) {
+ to[key] = from[key];
+ }
+ }
+ }
+
+ return to;
+}
+
+module.exports = assign;
+},{}],25:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule PooledClass
+ */
+
+'use strict';
+
+var invariant = _dereq_(161);
+
+/**
+ * Static poolers. Several custom versions for each potential number of
+ * arguments. A completely generic pooler is easy to implement, but would
+ * require accessing the `arguments` object. In each of these, `this` refers to
+ * the Class itself, not an instance. If any others are needed, simply add them
+ * here, or in their own files.
+ */
+var oneArgumentPooler = function (copyFieldsFrom) {
+ var Klass = this;
+ if (Klass.instancePool.length) {
+ var instance = Klass.instancePool.pop();
+ Klass.call(instance, copyFieldsFrom);
+ return instance;
+ } else {
+ return new Klass(copyFieldsFrom);
+ }
+};
+
+var twoArgumentPooler = function (a1, a2) {
+ var Klass = this;
+ if (Klass.instancePool.length) {
+ var instance = Klass.instancePool.pop();
+ Klass.call(instance, a1, a2);
+ return instance;
+ } else {
+ return new Klass(a1, a2);
+ }
+};
+
+var threeArgumentPooler = function (a1, a2, a3) {
+ var Klass = this;
+ if (Klass.instancePool.length) {
+ var instance = Klass.instancePool.pop();
+ Klass.call(instance, a1, a2, a3);
+ return instance;
+ } else {
+ return new Klass(a1, a2, a3);
+ }
+};
+
+var fourArgumentPooler = function (a1, a2, a3, a4) {
+ var Klass = this;
+ if (Klass.instancePool.length) {
+ var instance = Klass.instancePool.pop();
+ Klass.call(instance, a1, a2, a3, a4);
+ return instance;
+ } else {
+ return new Klass(a1, a2, a3, a4);
+ }
+};
+
+var fiveArgumentPooler = function (a1, a2, a3, a4, a5) {
+ var Klass = this;
+ if (Klass.instancePool.length) {
+ var instance = Klass.instancePool.pop();
+ Klass.call(instance, a1, a2, a3, a4, a5);
+ return instance;
+ } else {
+ return new Klass(a1, a2, a3, a4, a5);
+ }
+};
+
+var standardReleaser = function (instance) {
+ var Klass = this;
+ !(instance instanceof Klass) ? "production" !== 'production' ? invariant(false, 'Trying to release an instance into a pool of a different type.') : invariant(false) : undefined;
+ instance.destructor();
+ if (Klass.instancePool.length < Klass.poolSize) {
+ Klass.instancePool.push(instance);
+ }
+};
+
+var DEFAULT_POOL_SIZE = 10;
+var DEFAULT_POOLER = oneArgumentPooler;
+
+/**
+ * Augments `CopyConstructor` to be a poolable class, augmenting only the class
+ * itself (statically) not adding any prototypical fields. Any CopyConstructor
+ * you give this may have a `poolSize` property, and will look for a
+ * prototypical `destructor` on instances (optional).
+ *
+ * @param {Function} CopyConstructor Constructor that can be used to reset.
+ * @param {Function} pooler Customizable pooler.
+ */
+var addPoolingTo = function (CopyConstructor, pooler) {
+ var NewKlass = CopyConstructor;
+ NewKlass.instancePool = [];
+ NewKlass.getPooled = pooler || DEFAULT_POOLER;
+ if (!NewKlass.poolSize) {
+ NewKlass.poolSize = DEFAULT_POOL_SIZE;
+ }
+ NewKlass.release = standardReleaser;
+ return NewKlass;
+};
+
+var PooledClass = {
+ addPoolingTo: addPoolingTo,
+ oneArgumentPooler: oneArgumentPooler,
+ twoArgumentPooler: twoArgumentPooler,
+ threeArgumentPooler: threeArgumentPooler,
+ fourArgumentPooler: fourArgumentPooler,
+ fiveArgumentPooler: fiveArgumentPooler
+};
+
+module.exports = PooledClass;
+},{"161":161}],26:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule React
+ */
+
+'use strict';
+
+var ReactDOM = _dereq_(40);
+var ReactDOMServer = _dereq_(50);
+var ReactIsomorphic = _dereq_(69);
+
+var assign = _dereq_(24);
+var deprecated = _dereq_(120);
+
+// `version` will be added here by ReactIsomorphic.
+var React = {};
+
+assign(React, ReactIsomorphic);
+
+assign(React, {
+ // ReactDOM
+ findDOMNode: deprecated('findDOMNode', 'ReactDOM', 'react-dom', ReactDOM, ReactDOM.findDOMNode),
+ render: deprecated('render', 'ReactDOM', 'react-dom', ReactDOM, ReactDOM.render),
+ unmountComponentAtNode: deprecated('unmountComponentAtNode', 'ReactDOM', 'react-dom', ReactDOM, ReactDOM.unmountComponentAtNode),
+
+ // ReactDOMServer
+ renderToString: deprecated('renderToString', 'ReactDOMServer', 'react-dom/server', ReactDOMServer, ReactDOMServer.renderToString),
+ renderToStaticMarkup: deprecated('renderToStaticMarkup', 'ReactDOMServer', 'react-dom/server', ReactDOMServer, ReactDOMServer.renderToStaticMarkup)
+});
+
+React.__SECRET_DOM_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = ReactDOM;
+React.__SECRET_DOM_SERVER_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = ReactDOMServer;
+
+module.exports = React;
+},{"120":120,"24":24,"40":40,"50":50,"69":69}],27:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactBrowserComponentMixin
+ */
+
+'use strict';
+
+var ReactInstanceMap = _dereq_(68);
+
+var findDOMNode = _dereq_(122);
+var warning = _dereq_(173);
+
+var didWarnKey = '_getDOMNodeDidWarn';
+
+var ReactBrowserComponentMixin = {
+ /**
+ * Returns the DOM node rendered by this component.
+ *
+ * @return {DOMElement} The root node of this component.
+ * @final
+ * @protected
+ */
+ getDOMNode: function () {
+ "production" !== 'production' ? warning(this.constructor[didWarnKey], '%s.getDOMNode(...) is deprecated. Please use ' + 'ReactDOM.findDOMNode(instance) instead.', ReactInstanceMap.get(this).getName() || this.tagName || 'Unknown') : undefined;
+ this.constructor[didWarnKey] = true;
+ return findDOMNode(this);
+ }
+};
+
+module.exports = ReactBrowserComponentMixin;
+},{"122":122,"173":173,"68":68}],28:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactBrowserEventEmitter
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var EventConstants = _dereq_(15);
+var EventPluginHub = _dereq_(16);
+var EventPluginRegistry = _dereq_(17);
+var ReactEventEmitterMixin = _dereq_(62);
+var ReactPerf = _dereq_(78);
+var ViewportMetrics = _dereq_(114);
+
+var assign = _dereq_(24);
+var isEventSupported = _dereq_(133);
+
+/**
+ * Summary of `ReactBrowserEventEmitter` event handling:
+ *
+ * - Top-level delegation is used to trap most native browser events. This
+ * may only occur in the main thread and is the responsibility of
+ * ReactEventListener, which is injected and can therefore support pluggable
+ * event sources. This is the only work that occurs in the main thread.
+ *
+ * - We normalize and de-duplicate events to account for browser quirks. This
+ * may be done in the worker thread.
+ *
+ * - Forward these native events (with the associated top-level type used to
+ * trap it) to `EventPluginHub`, which in turn will ask plugins if they want
+ * to extract any synthetic events.
+ *
+ * - The `EventPluginHub` will then process each event by annotating them with
+ * "dispatches", a sequence of listeners and IDs that care about that event.
+ *
+ * - The `EventPluginHub` then dispatches the events.
+ *
+ * Overview of React and the event system:
+ *
+ * +------------+ .
+ * | DOM | .
+ * +------------+ .
+ * | .
+ * v .
+ * +------------+ .
+ * | ReactEvent | .
+ * | Listener | .
+ * +------------+ . +-----------+
+ * | . +--------+|SimpleEvent|
+ * | . | |Plugin |
+ * +-----|------+ . v +-----------+
+ * | | | . +--------------+ +------------+
+ * | +-----------.--->|EventPluginHub| | Event |
+ * | | . | | +-----------+ | Propagators|
+ * | ReactEvent | . | | |TapEvent | |------------|
+ * | Emitter | . | |<---+|Plugin | |other plugin|
+ * | | . | | +-----------+ | utilities |
+ * | +-----------.--->| | +------------+
+ * | | | . +--------------+
+ * +-----|------+ . ^ +-----------+
+ * | . | |Enter/Leave|
+ * + . +-------+|Plugin |
+ * +-------------+ . +-----------+
+ * | application | .
+ * |-------------| .
+ * | | .
+ * | | .
+ * +-------------+ .
+ * .
+ * React Core . General Purpose Event Plugin System
+ */
+
+var alreadyListeningTo = {};
+var isMonitoringScrollValue = false;
+var reactTopListenersCounter = 0;
+
+// For events like 'submit' which don't consistently bubble (which we trap at a
+// lower node than `document`), binding at `document` would cause duplicate
+// events so we don't include them here
+var topEventMapping = {
+ topAbort: 'abort',
+ topBlur: 'blur',
+ topCanPlay: 'canplay',
+ topCanPlayThrough: 'canplaythrough',
+ topChange: 'change',
+ topClick: 'click',
+ topCompositionEnd: 'compositionend',
+ topCompositionStart: 'compositionstart',
+ topCompositionUpdate: 'compositionupdate',
+ topContextMenu: 'contextmenu',
+ topCopy: 'copy',
+ topCut: 'cut',
+ topDoubleClick: 'dblclick',
+ topDrag: 'drag',
+ topDragEnd: 'dragend',
+ topDragEnter: 'dragenter',
+ topDragExit: 'dragexit',
+ topDragLeave: 'dragleave',
+ topDragOver: 'dragover',
+ topDragStart: 'dragstart',
+ topDrop: 'drop',
+ topDurationChange: 'durationchange',
+ topEmptied: 'emptied',
+ topEncrypted: 'encrypted',
+ topEnded: 'ended',
+ topError: 'error',
+ topFocus: 'focus',
+ topInput: 'input',
+ topKeyDown: 'keydown',
+ topKeyPress: 'keypress',
+ topKeyUp: 'keyup',
+ topLoadedData: 'loadeddata',
+ topLoadedMetadata: 'loadedmetadata',
+ topLoadStart: 'loadstart',
+ topMouseDown: 'mousedown',
+ topMouseMove: 'mousemove',
+ topMouseOut: 'mouseout',
+ topMouseOver: 'mouseover',
+ topMouseUp: 'mouseup',
+ topPaste: 'paste',
+ topPause: 'pause',
+ topPlay: 'play',
+ topPlaying: 'playing',
+ topProgress: 'progress',
+ topRateChange: 'ratechange',
+ topScroll: 'scroll',
+ topSeeked: 'seeked',
+ topSeeking: 'seeking',
+ topSelectionChange: 'selectionchange',
+ topStalled: 'stalled',
+ topSuspend: 'suspend',
+ topTextInput: 'textInput',
+ topTimeUpdate: 'timeupdate',
+ topTouchCancel: 'touchcancel',
+ topTouchEnd: 'touchend',
+ topTouchMove: 'touchmove',
+ topTouchStart: 'touchstart',
+ topVolumeChange: 'volumechange',
+ topWaiting: 'waiting',
+ topWheel: 'wheel'
+};
+
+/**
+ * To ensure no conflicts with other potential React instances on the page
+ */
+var topListenersIDKey = '_reactListenersID' + String(Math.random()).slice(2);
+
+function getListeningForDocument(mountAt) {
+ // In IE8, `mountAt` is a host object and doesn't have `hasOwnProperty`
+ // directly.
+ if (!Object.prototype.hasOwnProperty.call(mountAt, topListenersIDKey)) {
+ mountAt[topListenersIDKey] = reactTopListenersCounter++;
+ alreadyListeningTo[mountAt[topListenersIDKey]] = {};
+ }
+ return alreadyListeningTo[mountAt[topListenersIDKey]];
+}
+
+/**
+ * `ReactBrowserEventEmitter` is used to attach top-level event listeners. For
+ * example:
+ *
+ * ReactBrowserEventEmitter.putListener('myID', 'onClick', myFunction);
+ *
+ * This would allocate a "registration" of `('onClick', myFunction)` on 'myID'.
+ *
+ * @internal
+ */
+var ReactBrowserEventEmitter = assign({}, ReactEventEmitterMixin, {
+
+ /**
+ * Injectable event backend
+ */
+ ReactEventListener: null,
+
+ injection: {
+ /**
+ * @param {object} ReactEventListener
+ */
+ injectReactEventListener: function (ReactEventListener) {
+ ReactEventListener.setHandleTopLevel(ReactBrowserEventEmitter.handleTopLevel);
+ ReactBrowserEventEmitter.ReactEventListener = ReactEventListener;
+ }
+ },
+
+ /**
+ * Sets whether or not any created callbacks should be enabled.
+ *
+ * @param {boolean} enabled True if callbacks should be enabled.
+ */
+ setEnabled: function (enabled) {
+ if (ReactBrowserEventEmitter.ReactEventListener) {
+ ReactBrowserEventEmitter.ReactEventListener.setEnabled(enabled);
+ }
+ },
+
+ /**
+ * @return {boolean} True if callbacks are enabled.
+ */
+ isEnabled: function () {
+ return !!(ReactBrowserEventEmitter.ReactEventListener && ReactBrowserEventEmitter.ReactEventListener.isEnabled());
+ },
+
+ /**
+ * We listen for bubbled touch events on the document object.
+ *
+ * Firefox v8.01 (and possibly others) exhibited strange behavior when
+ * mounting `onmousemove` events at some node that was not the document
+ * element. The symptoms were that if your mouse is not moving over something
+ * contained within that mount point (for example on the background) the
+ * top-level listeners for `onmousemove` won't be called. However, if you
+ * register the `mousemove` on the document object, then it will of course
+ * catch all `mousemove`s. This along with iOS quirks, justifies restricting
+ * top-level listeners to the document object only, at least for these
+ * movement types of events and possibly all events.
+ *
+ * @see http://www.quirksmode.org/blog/archives/2010/09/click_event_del.html
+ *
+ * Also, `keyup`/`keypress`/`keydown` do not bubble to the window on IE, but
+ * they bubble to document.
+ *
+ * @param {string} registrationName Name of listener (e.g. `onClick`).
+ * @param {object} contentDocumentHandle Document which owns the container
+ */
+ listenTo: function (registrationName, contentDocumentHandle) {
+ var mountAt = contentDocumentHandle;
+ var isListening = getListeningForDocument(mountAt);
+ var dependencies = EventPluginRegistry.registrationNameDependencies[registrationName];
+
+ var topLevelTypes = EventConstants.topLevelTypes;
+ for (var i = 0; i < dependencies.length; i++) {
+ var dependency = dependencies[i];
+ if (!(isListening.hasOwnProperty(dependency) && isListening[dependency])) {
+ if (dependency === topLevelTypes.topWheel) {
+ if (isEventSupported('wheel')) {
+ ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(topLevelTypes.topWheel, 'wheel', mountAt);
+ } else if (isEventSupported('mousewheel')) {
+ ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(topLevelTypes.topWheel, 'mousewheel', mountAt);
+ } else {
+ // Firefox needs to capture a different mouse scroll event.
+ // @see http://www.quirksmode.org/dom/events/tests/scroll.html
+ ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(topLevelTypes.topWheel, 'DOMMouseScroll', mountAt);
+ }
+ } else if (dependency === topLevelTypes.topScroll) {
+
+ if (isEventSupported('scroll', true)) {
+ ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent(topLevelTypes.topScroll, 'scroll', mountAt);
+ } else {
+ ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(topLevelTypes.topScroll, 'scroll', ReactBrowserEventEmitter.ReactEventListener.WINDOW_HANDLE);
+ }
+ } else if (dependency === topLevelTypes.topFocus || dependency === topLevelTypes.topBlur) {
+
+ if (isEventSupported('focus', true)) {
+ ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent(topLevelTypes.topFocus, 'focus', mountAt);
+ ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent(topLevelTypes.topBlur, 'blur', mountAt);
+ } else if (isEventSupported('focusin')) {
+ // IE has `focusin` and `focusout` events which bubble.
+ // @see http://www.quirksmode.org/blog/archives/2008/04/delegating_the.html
+ ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(topLevelTypes.topFocus, 'focusin', mountAt);
+ ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(topLevelTypes.topBlur, 'focusout', mountAt);
+ }
+
+ // to make sure blur and focus event listeners are only attached once
+ isListening[topLevelTypes.topBlur] = true;
+ isListening[topLevelTypes.topFocus] = true;
+ } else if (topEventMapping.hasOwnProperty(dependency)) {
+ ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(dependency, topEventMapping[dependency], mountAt);
+ }
+
+ isListening[dependency] = true;
+ }
+ }
+ },
+
+ trapBubbledEvent: function (topLevelType, handlerBaseName, handle) {
+ return ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(topLevelType, handlerBaseName, handle);
+ },
+
+ trapCapturedEvent: function (topLevelType, handlerBaseName, handle) {
+ return ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent(topLevelType, handlerBaseName, handle);
+ },
+
+ /**
+ * Listens to window scroll and resize events. We cache scroll values so that
+ * application code can access them without triggering reflows.
+ *
+ * NOTE: Scroll events do not bubble.
+ *
+ * @see http://www.quirksmode.org/dom/events/scroll.html
+ */
+ ensureScrollValueMonitoring: function () {
+ if (!isMonitoringScrollValue) {
+ var refresh = ViewportMetrics.refreshScrollValues;
+ ReactBrowserEventEmitter.ReactEventListener.monitorScrollValue(refresh);
+ isMonitoringScrollValue = true;
+ }
+ },
+
+ eventNameDispatchConfigs: EventPluginHub.eventNameDispatchConfigs,
+
+ registrationNameModules: EventPluginHub.registrationNameModules,
+
+ putListener: EventPluginHub.putListener,
+
+ getListener: EventPluginHub.getListener,
+
+ deleteListener: EventPluginHub.deleteListener,
+
+ deleteAllListeners: EventPluginHub.deleteAllListeners
+
+});
+
+ReactPerf.measureMethods(ReactBrowserEventEmitter, 'ReactBrowserEventEmitter', {
+ putListener: 'putListener',
+ deleteListener: 'deleteListener'
+});
+
+module.exports = ReactBrowserEventEmitter;
+},{"114":114,"133":133,"15":15,"16":16,"17":17,"24":24,"62":62,"78":78}],29:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @typechecks
+ * @providesModule ReactCSSTransitionGroup
+ */
+
+'use strict';
+
+var React = _dereq_(26);
+
+var assign = _dereq_(24);
+
+var ReactTransitionGroup = _dereq_(94);
+var ReactCSSTransitionGroupChild = _dereq_(30);
+
+function createTransitionTimeoutPropValidator(transitionType) {
+ var timeoutPropName = 'transition' + transitionType + 'Timeout';
+ var enabledPropName = 'transition' + transitionType;
+
+ return function (props) {
+ // If the transition is enabled
+ if (props[enabledPropName]) {
+ // If no timeout duration is provided
+ if (props[timeoutPropName] == null) {
+ return new Error(timeoutPropName + ' wasn\'t supplied to ReactCSSTransitionGroup: ' + 'this can cause unreliable animations and won\'t be supported in ' + 'a future version of React. See ' + 'https://fb.me/react-animation-transition-group-timeout for more ' + 'information.');
+
+ // If the duration isn't a number
+ } else if (typeof props[timeoutPropName] !== 'number') {
+ return new Error(timeoutPropName + ' must be a number (in milliseconds)');
+ }
+ }
+ };
+}
+
+var ReactCSSTransitionGroup = React.createClass({
+ displayName: 'ReactCSSTransitionGroup',
+
+ propTypes: {
+ transitionName: ReactCSSTransitionGroupChild.propTypes.name,
+
+ transitionAppear: React.PropTypes.bool,
+ transitionEnter: React.PropTypes.bool,
+ transitionLeave: React.PropTypes.bool,
+ transitionAppearTimeout: createTransitionTimeoutPropValidator('Appear'),
+ transitionEnterTimeout: createTransitionTimeoutPropValidator('Enter'),
+ transitionLeaveTimeout: createTransitionTimeoutPropValidator('Leave')
+ },
+
+ getDefaultProps: function () {
+ return {
+ transitionAppear: false,
+ transitionEnter: true,
+ transitionLeave: true
+ };
+ },
+
+ _wrapChild: function (child) {
+ // We need to provide this childFactory so that
+ // ReactCSSTransitionGroupChild can receive updates to name, enter, and
+ // leave while it is leaving.
+ return React.createElement(ReactCSSTransitionGroupChild, {
+ name: this.props.transitionName,
+ appear: this.props.transitionAppear,
+ enter: this.props.transitionEnter,
+ leave: this.props.transitionLeave,
+ appearTimeout: this.props.transitionAppearTimeout,
+ enterTimeout: this.props.transitionEnterTimeout,
+ leaveTimeout: this.props.transitionLeaveTimeout
+ }, child);
+ },
+
+ render: function () {
+ return React.createElement(ReactTransitionGroup, assign({}, this.props, { childFactory: this._wrapChild }));
+ }
+});
+
+module.exports = ReactCSSTransitionGroup;
+},{"24":24,"26":26,"30":30,"94":94}],30:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @typechecks
+ * @providesModule ReactCSSTransitionGroupChild
+ */
+
+'use strict';
+
+var React = _dereq_(26);
+var ReactDOM = _dereq_(40);
+
+var CSSCore = _dereq_(145);
+var ReactTransitionEvents = _dereq_(93);
+
+var onlyChild = _dereq_(135);
+
+// We don't remove the element from the DOM until we receive an animationend or
+// transitionend event. If the user screws up and forgets to add an animation
+// their node will be stuck in the DOM forever, so we detect if an animation
+// does not start and if it doesn't, we just call the end listener immediately.
+var TICK = 17;
+
+var ReactCSSTransitionGroupChild = React.createClass({
+ displayName: 'ReactCSSTransitionGroupChild',
+
+ propTypes: {
+ name: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.shape({
+ enter: React.PropTypes.string,
+ leave: React.PropTypes.string,
+ active: React.PropTypes.string
+ }), React.PropTypes.shape({
+ enter: React.PropTypes.string,
+ enterActive: React.PropTypes.string,
+ leave: React.PropTypes.string,
+ leaveActive: React.PropTypes.string,
+ appear: React.PropTypes.string,
+ appearActive: React.PropTypes.string
+ })]).isRequired,
+
+ // Once we require timeouts to be specified, we can remove the
+ // boolean flags (appear etc.) and just accept a number
+ // or a bool for the timeout flags (appearTimeout etc.)
+ appear: React.PropTypes.bool,
+ enter: React.PropTypes.bool,
+ leave: React.PropTypes.bool,
+ appearTimeout: React.PropTypes.number,
+ enterTimeout: React.PropTypes.number,
+ leaveTimeout: React.PropTypes.number
+ },
+
+ transition: function (animationType, finishCallback, userSpecifiedDelay) {
+ var node = ReactDOM.findDOMNode(this);
+
+ if (!node) {
+ if (finishCallback) {
+ finishCallback();
+ }
+ return;
+ }
+
+ var className = this.props.name[animationType] || this.props.name + '-' + animationType;
+ var activeClassName = this.props.name[animationType + 'Active'] || className + '-active';
+ var timeout = null;
+
+ var endListener = function (e) {
+ if (e && e.target !== node) {
+ return;
+ }
+
+ clearTimeout(timeout);
+
+ CSSCore.removeClass(node, className);
+ CSSCore.removeClass(node, activeClassName);
+
+ ReactTransitionEvents.removeEndEventListener(node, endListener);
+
+ // Usually this optional callback is used for informing an owner of
+ // a leave animation and telling it to remove the child.
+ if (finishCallback) {
+ finishCallback();
+ }
+ };
+
+ CSSCore.addClass(node, className);
+
+ // Need to do this to actually trigger a transition.
+ this.queueClass(activeClassName);
+
+ // If the user specified a timeout delay.
+ if (userSpecifiedDelay) {
+ // Clean-up the animation after the specified delay
+ timeout = setTimeout(endListener, userSpecifiedDelay);
+ this.transitionTimeouts.push(timeout);
+ } else {
+ // DEPRECATED: this listener will be removed in a future version of react
+ ReactTransitionEvents.addEndEventListener(node, endListener);
+ }
+ },
+
+ queueClass: function (className) {
+ this.classNameQueue.push(className);
+
+ if (!this.timeout) {
+ this.timeout = setTimeout(this.flushClassNameQueue, TICK);
+ }
+ },
+
+ flushClassNameQueue: function () {
+ if (this.isMounted()) {
+ this.classNameQueue.forEach(CSSCore.addClass.bind(CSSCore, ReactDOM.findDOMNode(this)));
+ }
+ this.classNameQueue.length = 0;
+ this.timeout = null;
+ },
+
+ componentWillMount: function () {
+ this.classNameQueue = [];
+ this.transitionTimeouts = [];
+ },
+
+ componentWillUnmount: function () {
+ if (this.timeout) {
+ clearTimeout(this.timeout);
+ }
+ this.transitionTimeouts.forEach(function (timeout) {
+ clearTimeout(timeout);
+ });
+ },
+
+ componentWillAppear: function (done) {
+ if (this.props.appear) {
+ this.transition('appear', done, this.props.appearTimeout);
+ } else {
+ done();
+ }
+ },
+
+ componentWillEnter: function (done) {
+ if (this.props.enter) {
+ this.transition('enter', done, this.props.enterTimeout);
+ } else {
+ done();
+ }
+ },
+
+ componentWillLeave: function (done) {
+ if (this.props.leave) {
+ this.transition('leave', done, this.props.leaveTimeout);
+ } else {
+ done();
+ }
+ },
+
+ render: function () {
+ return onlyChild(this.props.children);
+ }
+});
+
+module.exports = ReactCSSTransitionGroupChild;
+},{"135":135,"145":145,"26":26,"40":40,"93":93}],31:[function(_dereq_,module,exports){
+/**
+ * Copyright 2014-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactChildReconciler
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var ReactReconciler = _dereq_(84);
+
+var instantiateReactComponent = _dereq_(132);
+var shouldUpdateReactComponent = _dereq_(141);
+var traverseAllChildren = _dereq_(142);
+var warning = _dereq_(173);
+
+function instantiateChild(childInstances, child, name) {
+ // We found a component instance.
+ var keyUnique = childInstances[name] === undefined;
+ if ("production" !== 'production') {
+ "production" !== 'production' ? warning(keyUnique, 'flattenChildren(...): Encountered two children with the same key, ' + '`%s`. Child keys must be unique; when two children share a key, only ' + 'the first child will be used.', name) : undefined;
+ }
+ if (child != null && keyUnique) {
+ childInstances[name] = instantiateReactComponent(child, null);
+ }
+}
+
+/**
+ * ReactChildReconciler provides helpers for initializing or updating a set of
+ * children. Its output is suitable for passing it onto ReactMultiChild which
+ * does diffed reordering and insertion.
+ */
+var ReactChildReconciler = {
+ /**
+ * Generates a "mount image" for each of the supplied children. In the case
+ * of `ReactDOMComponent`, a mount image is a string of markup.
+ *
+ * @param {?object} nestedChildNodes Nested child maps.
+ * @return {?object} A set of child instances.
+ * @internal
+ */
+ instantiateChildren: function (nestedChildNodes, transaction, context) {
+ if (nestedChildNodes == null) {
+ return null;
+ }
+ var childInstances = {};
+ traverseAllChildren(nestedChildNodes, instantiateChild, childInstances);
+ return childInstances;
+ },
+
+ /**
+ * Updates the rendered children and returns a new set of children.
+ *
+ * @param {?object} prevChildren Previously initialized set of children.
+ * @param {?object} nextChildren Flat child element maps.
+ * @param {ReactReconcileTransaction} transaction
+ * @param {object} context
+ * @return {?object} A new set of child instances.
+ * @internal
+ */
+ updateChildren: function (prevChildren, nextChildren, transaction, context) {
+ // We currently don't have a way to track moves here but if we use iterators
+ // instead of for..in we can zip the iterators and check if an item has
+ // moved.
+ // TODO: If nothing has changed, return the prevChildren object so that we
+ // can quickly bailout if nothing has changed.
+ if (!nextChildren && !prevChildren) {
+ return null;
+ }
+ var name;
+ for (name in nextChildren) {
+ if (!nextChildren.hasOwnProperty(name)) {
+ continue;
+ }
+ var prevChild = prevChildren && prevChildren[name];
+ var prevElement = prevChild && prevChild._currentElement;
+ var nextElement = nextChildren[name];
+ if (prevChild != null && shouldUpdateReactComponent(prevElement, nextElement)) {
+ ReactReconciler.receiveComponent(prevChild, nextElement, transaction, context);
+ nextChildren[name] = prevChild;
+ } else {
+ if (prevChild) {
+ ReactReconciler.unmountComponent(prevChild, name);
+ }
+ // The child must be instantiated before it's mounted.
+ var nextChildInstance = instantiateReactComponent(nextElement, null);
+ nextChildren[name] = nextChildInstance;
+ }
+ }
+ // Unmount children that are no longer present.
+ for (name in prevChildren) {
+ if (prevChildren.hasOwnProperty(name) && !(nextChildren && nextChildren.hasOwnProperty(name))) {
+ ReactReconciler.unmountComponent(prevChildren[name]);
+ }
+ }
+ return nextChildren;
+ },
+
+ /**
+ * Unmounts all rendered children. This should be used to clean up children
+ * when this component is unmounted.
+ *
+ * @param {?object} renderedChildren Previously initialized set of children.
+ * @internal
+ */
+ unmountChildren: function (renderedChildren) {
+ for (var name in renderedChildren) {
+ if (renderedChildren.hasOwnProperty(name)) {
+ var renderedChild = renderedChildren[name];
+ ReactReconciler.unmountComponent(renderedChild);
+ }
+ }
+ }
+
+};
+
+module.exports = ReactChildReconciler;
+},{"132":132,"141":141,"142":142,"173":173,"84":84}],32:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactChildren
+ */
+
+'use strict';
+
+var PooledClass = _dereq_(25);
+var ReactElement = _dereq_(57);
+
+var emptyFunction = _dereq_(153);
+var traverseAllChildren = _dereq_(142);
+
+var twoArgumentPooler = PooledClass.twoArgumentPooler;
+var fourArgumentPooler = PooledClass.fourArgumentPooler;
+
+var userProvidedKeyEscapeRegex = /\/(?!\/)/g;
+function escapeUserProvidedKey(text) {
+ return ('' + text).replace(userProvidedKeyEscapeRegex, '//');
+}
+
+/**
+ * PooledClass representing the bookkeeping associated with performing a child
+ * traversal. Allows avoiding binding callbacks.
+ *
+ * @constructor ForEachBookKeeping
+ * @param {!function} forEachFunction Function to perform traversal with.
+ * @param {?*} forEachContext Context to perform context with.
+ */
+function ForEachBookKeeping(forEachFunction, forEachContext) {
+ this.func = forEachFunction;
+ this.context = forEachContext;
+ this.count = 0;
+}
+ForEachBookKeeping.prototype.destructor = function () {
+ this.func = null;
+ this.context = null;
+ this.count = 0;
+};
+PooledClass.addPoolingTo(ForEachBookKeeping, twoArgumentPooler);
+
+function forEachSingleChild(bookKeeping, child, name) {
+ var func = bookKeeping.func;
+ var context = bookKeeping.context;
+
+ func.call(context, child, bookKeeping.count++);
+}
+
+/**
+ * Iterates through children that are typically specified as `props.children`.
+ *
+ * The provided forEachFunc(child, index) will be called for each
+ * leaf child.
+ *
+ * @param {?*} children Children tree container.
+ * @param {function(*, int)} forEachFunc
+ * @param {*} forEachContext Context for forEachContext.
+ */
+function forEachChildren(children, forEachFunc, forEachContext) {
+ if (children == null) {
+ return children;
+ }
+ var traverseContext = ForEachBookKeeping.getPooled(forEachFunc, forEachContext);
+ traverseAllChildren(children, forEachSingleChild, traverseContext);
+ ForEachBookKeeping.release(traverseContext);
+}
+
+/**
+ * PooledClass representing the bookkeeping associated with performing a child
+ * mapping. Allows avoiding binding callbacks.
+ *
+ * @constructor MapBookKeeping
+ * @param {!*} mapResult Object containing the ordered map of results.
+ * @param {!function} mapFunction Function to perform mapping with.
+ * @param {?*} mapContext Context to perform mapping with.
+ */
+function MapBookKeeping(mapResult, keyPrefix, mapFunction, mapContext) {
+ this.result = mapResult;
+ this.keyPrefix = keyPrefix;
+ this.func = mapFunction;
+ this.context = mapContext;
+ this.count = 0;
+}
+MapBookKeeping.prototype.destructor = function () {
+ this.result = null;
+ this.keyPrefix = null;
+ this.func = null;
+ this.context = null;
+ this.count = 0;
+};
+PooledClass.addPoolingTo(MapBookKeeping, fourArgumentPooler);
+
+function mapSingleChildIntoContext(bookKeeping, child, childKey) {
+ var result = bookKeeping.result;
+ var keyPrefix = bookKeeping.keyPrefix;
+ var func = bookKeeping.func;
+ var context = bookKeeping.context;
+
+ var mappedChild = func.call(context, child, bookKeeping.count++);
+ if (Array.isArray(mappedChild)) {
+ mapIntoWithKeyPrefixInternal(mappedChild, result, childKey, emptyFunction.thatReturnsArgument);
+ } else if (mappedChild != null) {
+ if (ReactElement.isValidElement(mappedChild)) {
+ mappedChild = ReactElement.cloneAndReplaceKey(mappedChild,
+ // Keep both the (mapped) and old keys if they differ, just as
+ // traverseAllChildren used to do for objects as children
+ keyPrefix + (mappedChild !== child ? escapeUserProvidedKey(mappedChild.key || '') + '/' : '') + childKey);
+ }
+ result.push(mappedChild);
+ }
+}
+
+function mapIntoWithKeyPrefixInternal(children, array, prefix, func, context) {
+ var escapedPrefix = '';
+ if (prefix != null) {
+ escapedPrefix = escapeUserProvidedKey(prefix) + '/';
+ }
+ var traverseContext = MapBookKeeping.getPooled(array, escapedPrefix, func, context);
+ traverseAllChildren(children, mapSingleChildIntoContext, traverseContext);
+ MapBookKeeping.release(traverseContext);
+}
+
+/**
+ * Maps children that are typically specified as `props.children`.
+ *
+ * The provided mapFunction(child, key, index) will be called for each
+ * leaf child.
+ *
+ * @param {?*} children Children tree container.
+ * @param {function(*, int)} func The map function.
+ * @param {*} context Context for mapFunction.
+ * @return {object} Object containing the ordered map of results.
+ */
+function mapChildren(children, func, context) {
+ if (children == null) {
+ return children;
+ }
+ var result = [];
+ mapIntoWithKeyPrefixInternal(children, result, null, func, context);
+ return result;
+}
+
+function forEachSingleChildDummy(traverseContext, child, name) {
+ return null;
+}
+
+/**
+ * Count the number of children that are typically specified as
+ * `props.children`.
+ *
+ * @param {?*} children Children tree container.
+ * @return {number} The number of children.
+ */
+function countChildren(children, context) {
+ return traverseAllChildren(children, forEachSingleChildDummy, null);
+}
+
+/**
+ * Flatten a children object (typically specified as `props.children`) and
+ * return an array with appropriately re-keyed children.
+ */
+function toArray(children) {
+ var result = [];
+ mapIntoWithKeyPrefixInternal(children, result, null, emptyFunction.thatReturnsArgument);
+ return result;
+}
+
+var ReactChildren = {
+ forEach: forEachChildren,
+ map: mapChildren,
+ mapIntoWithKeyPrefixInternal: mapIntoWithKeyPrefixInternal,
+ count: countChildren,
+ toArray: toArray
+};
+
+module.exports = ReactChildren;
+},{"142":142,"153":153,"25":25,"57":57}],33:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactClass
+ */
+
+'use strict';
+
+var ReactComponent = _dereq_(34);
+var ReactElement = _dereq_(57);
+var ReactPropTypeLocations = _dereq_(81);
+var ReactPropTypeLocationNames = _dereq_(80);
+var ReactNoopUpdateQueue = _dereq_(76);
+
+var assign = _dereq_(24);
+var emptyObject = _dereq_(154);
+var invariant = _dereq_(161);
+var keyMirror = _dereq_(165);
+var keyOf = _dereq_(166);
+var warning = _dereq_(173);
+
+var MIXINS_KEY = keyOf({ mixins: null });
+
+/**
+ * Policies that describe methods in `ReactClassInterface`.
+ */
+var SpecPolicy = keyMirror({
+ /**
+ * These methods may be defined only once by the class specification or mixin.
+ */
+ DEFINE_ONCE: null,
+ /**
+ * These methods may be defined by both the class specification and mixins.
+ * Subsequent definitions will be chained. These methods must return void.
+ */
+ DEFINE_MANY: null,
+ /**
+ * These methods are overriding the base class.
+ */
+ OVERRIDE_BASE: null,
+ /**
+ * These methods are similar to DEFINE_MANY, except we assume they return
+ * objects. We try to merge the keys of the return values of all the mixed in
+ * functions. If there is a key conflict we throw.
+ */
+ DEFINE_MANY_MERGED: null
+});
+
+var injectedMixins = [];
+
+var warnedSetProps = false;
+function warnSetProps() {
+ if (!warnedSetProps) {
+ warnedSetProps = true;
+ "production" !== 'production' ? warning(false, 'setProps(...) and replaceProps(...) are deprecated. ' + 'Instead, call render again at the top level.') : undefined;
+ }
+}
+
+/**
+ * Composite components are higher-level components that compose other composite
+ * or native components.
+ *
+ * To create a new type of `ReactClass`, pass a specification of
+ * your new class to `React.createClass`. The only requirement of your class
+ * specification is that you implement a `render` method.
+ *
+ * var MyComponent = React.createClass({
+ * render: function() {
+ * return <div>Hello World</div>;
+ * }
+ * });
+ *
+ * The class specification supports a specific protocol of methods that have
+ * special meaning (e.g. `render`). See `ReactClassInterface` for
+ * more the comprehensive protocol. Any other properties and methods in the
+ * class specification will be available on the prototype.
+ *
+ * @interface ReactClassInterface
+ * @internal
+ */
+var ReactClassInterface = {
+
+ /**
+ * An array of Mixin objects to include when defining your component.
+ *
+ * @type {array}
+ * @optional
+ */
+ mixins: SpecPolicy.DEFINE_MANY,
+
+ /**
+ * An object containing properties and methods that should be defined on
+ * the component's constructor instead of its prototype (static methods).
+ *
+ * @type {object}
+ * @optional
+ */
+ statics: SpecPolicy.DEFINE_MANY,
+
+ /**
+ * Definition of prop types for this component.
+ *
+ * @type {object}
+ * @optional
+ */
+ propTypes: SpecPolicy.DEFINE_MANY,
+
+ /**
+ * Definition of context types for this component.
+ *
+ * @type {object}
+ * @optional
+ */
+ contextTypes: SpecPolicy.DEFINE_MANY,
+
+ /**
+ * Definition of context types this component sets for its children.
+ *
+ * @type {object}
+ * @optional
+ */
+ childContextTypes: SpecPolicy.DEFINE_MANY,
+
+ // ==== Definition methods ====
+
+ /**
+ * Invoked when the component is mounted. Values in the mapping will be set on
+ * `this.props` if that prop is not specified (i.e. using an `in` check).
+ *
+ * This method is invoked before `getInitialState` and therefore cannot rely
+ * on `this.state` or use `this.setState`.
+ *
+ * @return {object}
+ * @optional
+ */
+ getDefaultProps: SpecPolicy.DEFINE_MANY_MERGED,
+
+ /**
+ * Invoked once before the component is mounted. The return value will be used
+ * as the initial value of `this.state`.
+ *
+ * getInitialState: function() {
+ * return {
+ * isOn: false,
+ * fooBaz: new BazFoo()
+ * }
+ * }
+ *
+ * @return {object}
+ * @optional
+ */
+ getInitialState: SpecPolicy.DEFINE_MANY_MERGED,
+
+ /**
+ * @return {object}
+ * @optional
+ */
+ getChildContext: SpecPolicy.DEFINE_MANY_MERGED,
+
+ /**
+ * Uses props from `this.props` and state from `this.state` to render the
+ * structure of the component.
+ *
+ * No guarantees are made about when or how often this method is invoked, so
+ * it must not have side effects.
+ *
+ * render: function() {
+ * var name = this.props.name;
+ * return <div>Hello, {name}!</div>;
+ * }
+ *
+ * @return {ReactComponent}
+ * @nosideeffects
+ * @required
+ */
+ render: SpecPolicy.DEFINE_ONCE,
+
+ // ==== Delegate methods ====
+
+ /**
+ * Invoked when the component is initially created and about to be mounted.
+ * This may have side effects, but any external subscriptions or data created
+ * by this method must be cleaned up in `componentWillUnmount`.
+ *
+ * @optional
+ */
+ componentWillMount: SpecPolicy.DEFINE_MANY,
+
+ /**
+ * Invoked when the component has been mounted and has a DOM representation.
+ * However, there is no guarantee that the DOM node is in the document.
+ *
+ * Use this as an opportunity to operate on the DOM when the component has
+ * been mounted (initialized and rendered) for the first time.
+ *
+ * @param {DOMElement} rootNode DOM element representing the component.
+ * @optional
+ */
+ componentDidMount: SpecPolicy.DEFINE_MANY,
+
+ /**
+ * Invoked before the component receives new props.
+ *
+ * Use this as an opportunity to react to a prop transition by updating the
+ * state using `this.setState`. Current props are accessed via `this.props`.
+ *
+ * componentWillReceiveProps: function(nextProps, nextContext) {
+ * this.setState({
+ * likesIncreasing: nextProps.likeCount > this.props.likeCount
+ * });
+ * }
+ *
+ * NOTE: There is no equivalent `componentWillReceiveState`. An incoming prop
+ * transition may cause a state change, but the opposite is not true. If you
+ * need it, you are probably looking for `componentWillUpdate`.
+ *
+ * @param {object} nextProps
+ * @optional
+ */
+ componentWillReceiveProps: SpecPolicy.DEFINE_MANY,
+
+ /**
+ * Invoked while deciding if the component should be updated as a result of
+ * receiving new props, state and/or context.
+ *
+ * Use this as an opportunity to `return false` when you're certain that the
+ * transition to the new props/state/context will not require a component
+ * update.
+ *
+ * shouldComponentUpdate: function(nextProps, nextState, nextContext) {
+ * return !equal(nextProps, this.props) ||
+ * !equal(nextState, this.state) ||
+ * !equal(nextContext, this.context);
+ * }
+ *
+ * @param {object} nextProps
+ * @param {?object} nextState
+ * @param {?object} nextContext
+ * @return {boolean} True if the component should update.
+ * @optional
+ */
+ shouldComponentUpdate: SpecPolicy.DEFINE_ONCE,
+
+ /**
+ * Invoked when the component is about to update due to a transition from
+ * `this.props`, `this.state` and `this.context` to `nextProps`, `nextState`
+ * and `nextContext`.
+ *
+ * Use this as an opportunity to perform preparation before an update occurs.
+ *
+ * NOTE: You **cannot** use `this.setState()` in this method.
+ *
+ * @param {object} nextProps
+ * @param {?object} nextState
+ * @param {?object} nextContext
+ * @param {ReactReconcileTransaction} transaction
+ * @optional
+ */
+ componentWillUpdate: SpecPolicy.DEFINE_MANY,
+
+ /**
+ * Invoked when the component's DOM representation has been updated.
+ *
+ * Use this as an opportunity to operate on the DOM when the component has
+ * been updated.
+ *
+ * @param {object} prevProps
+ * @param {?object} prevState
+ * @param {?object} prevContext
+ * @param {DOMElement} rootNode DOM element representing the component.
+ * @optional
+ */
+ componentDidUpdate: SpecPolicy.DEFINE_MANY,
+
+ /**
+ * Invoked when the component is about to be removed from its parent and have
+ * its DOM representation destroyed.
+ *
+ * Use this as an opportunity to deallocate any external resources.
+ *
+ * NOTE: There is no `componentDidUnmount` since your component will have been
+ * destroyed by that point.
+ *
+ * @optional
+ */
+ componentWillUnmount: SpecPolicy.DEFINE_MANY,
+
+ // ==== Advanced methods ====
+
+ /**
+ * Updates the component's currently mounted DOM representation.
+ *
+ * By default, this implements React's rendering and reconciliation algorithm.
+ * Sophisticated clients may wish to override this.
+ *
+ * @param {ReactReconcileTransaction} transaction
+ * @internal
+ * @overridable
+ */
+ updateComponent: SpecPolicy.OVERRIDE_BASE
+
+};
+
+/**
+ * Mapping from class specification keys to special processing functions.
+ *
+ * Although these are declared like instance properties in the specification
+ * when defining classes using `React.createClass`, they are actually static
+ * and are accessible on the constructor instead of the prototype. Despite
+ * being static, they must be defined outside of the "statics" key under
+ * which all other static methods are defined.
+ */
+var RESERVED_SPEC_KEYS = {
+ displayName: function (Constructor, displayName) {
+ Constructor.displayName = displayName;
+ },
+ mixins: function (Constructor, mixins) {
+ if (mixins) {
+ for (var i = 0; i < mixins.length; i++) {
+ mixSpecIntoComponent(Constructor, mixins[i]);
+ }
+ }
+ },
+ childContextTypes: function (Constructor, childContextTypes) {
+ if ("production" !== 'production') {
+ validateTypeDef(Constructor, childContextTypes, ReactPropTypeLocations.childContext);
+ }
+ Constructor.childContextTypes = assign({}, Constructor.childContextTypes, childContextTypes);
+ },
+ contextTypes: function (Constructor, contextTypes) {
+ if ("production" !== 'production') {
+ validateTypeDef(Constructor, contextTypes, ReactPropTypeLocations.context);
+ }
+ Constructor.contextTypes = assign({}, Constructor.contextTypes, contextTypes);
+ },
+ /**
+ * Special case getDefaultProps which should move into statics but requires
+ * automatic merging.
+ */
+ getDefaultProps: function (Constructor, getDefaultProps) {
+ if (Constructor.getDefaultProps) {
+ Constructor.getDefaultProps = createMergedResultFunction(Constructor.getDefaultProps, getDefaultProps);
+ } else {
+ Constructor.getDefaultProps = getDefaultProps;
+ }
+ },
+ propTypes: function (Constructor, propTypes) {
+ if ("production" !== 'production') {
+ validateTypeDef(Constructor, propTypes, ReactPropTypeLocations.prop);
+ }
+ Constructor.propTypes = assign({}, Constructor.propTypes, propTypes);
+ },
+ statics: function (Constructor, statics) {
+ mixStaticSpecIntoComponent(Constructor, statics);
+ },
+ autobind: function () {} };
+
+// noop
+function validateTypeDef(Constructor, typeDef, location) {
+ for (var propName in typeDef) {
+ if (typeDef.hasOwnProperty(propName)) {
+ // use a warning instead of an invariant so components
+ // don't show up in prod but not in __DEV__
+ "production" !== 'production' ? warning(typeof typeDef[propName] === 'function', '%s: %s type `%s` is invalid; it must be a function, usually from ' + 'React.PropTypes.', Constructor.displayName || 'ReactClass', ReactPropTypeLocationNames[location], propName) : undefined;
+ }
+ }
+}
+
+function validateMethodOverride(proto, name) {
+ var specPolicy = ReactClassInterface.hasOwnProperty(name) ? ReactClassInterface[name] : null;
+
+ // Disallow overriding of base class methods unless explicitly allowed.
+ if (ReactClassMixin.hasOwnProperty(name)) {
+ !(specPolicy === SpecPolicy.OVERRIDE_BASE) ? "production" !== 'production' ? invariant(false, 'ReactClassInterface: You are attempting to override ' + '`%s` from your class specification. Ensure that your method names ' + 'do not overlap with React methods.', name) : invariant(false) : undefined;
+ }
+
+ // Disallow defining methods more than once unless explicitly allowed.
+ if (proto.hasOwnProperty(name)) {
+ !(specPolicy === SpecPolicy.DEFINE_MANY || specPolicy === SpecPolicy.DEFINE_MANY_MERGED) ? "production" !== 'production' ? invariant(false, 'ReactClassInterface: You are attempting to define ' + '`%s` on your component more than once. This conflict may be due ' + 'to a mixin.', name) : invariant(false) : undefined;
+ }
+}
+
+/**
+ * Mixin helper which handles policy validation and reserved
+ * specification keys when building React classses.
+ */
+function mixSpecIntoComponent(Constructor, spec) {
+ if (!spec) {
+ return;
+ }
+
+ !(typeof spec !== 'function') ? "production" !== 'production' ? invariant(false, 'ReactClass: You\'re attempting to ' + 'use a component class as a mixin. Instead, just use a regular object.') : invariant(false) : undefined;
+ !!ReactElement.isValidElement(spec) ? "production" !== 'production' ? invariant(false, 'ReactClass: You\'re attempting to ' + 'use a component as a mixin. Instead, just use a regular object.') : invariant(false) : undefined;
+
+ var proto = Constructor.prototype;
+
+ // By handling mixins before any other properties, we ensure the same
+ // chaining order is applied to methods with DEFINE_MANY policy, whether
+ // mixins are listed before or after these methods in the spec.
+ if (spec.hasOwnProperty(MIXINS_KEY)) {
+ RESERVED_SPEC_KEYS.mixins(Constructor, spec.mixins);
+ }
+
+ for (var name in spec) {
+ if (!spec.hasOwnProperty(name)) {
+ continue;
+ }
+
+ if (name === MIXINS_KEY) {
+ // We have already handled mixins in a special case above.
+ continue;
+ }
+
+ var property = spec[name];
+ validateMethodOverride(proto, name);
+
+ if (RESERVED_SPEC_KEYS.hasOwnProperty(name)) {
+ RESERVED_SPEC_KEYS[name](Constructor, property);
+ } else {
+ // Setup methods on prototype:
+ // The following member methods should not be automatically bound:
+ // 1. Expected ReactClass methods (in the "interface").
+ // 2. Overridden methods (that were mixed in).
+ var isReactClassMethod = ReactClassInterface.hasOwnProperty(name);
+ var isAlreadyDefined = proto.hasOwnProperty(name);
+ var isFunction = typeof property === 'function';
+ var shouldAutoBind = isFunction && !isReactClassMethod && !isAlreadyDefined && spec.autobind !== false;
+
+ if (shouldAutoBind) {
+ if (!proto.__reactAutoBindMap) {
+ proto.__reactAutoBindMap = {};
+ }
+ proto.__reactAutoBindMap[name] = property;
+ proto[name] = property;
+ } else {
+ if (isAlreadyDefined) {
+ var specPolicy = ReactClassInterface[name];
+
+ // These cases should already be caught by validateMethodOverride.
+ !(isReactClassMethod && (specPolicy === SpecPolicy.DEFINE_MANY_MERGED || specPolicy === SpecPolicy.DEFINE_MANY)) ? "production" !== 'production' ? invariant(false, 'ReactClass: Unexpected spec policy %s for key %s ' + 'when mixing in component specs.', specPolicy, name) : invariant(false) : undefined;
+
+ // For methods which are defined more than once, call the existing
+ // methods before calling the new property, merging if appropriate.
+ if (specPolicy === SpecPolicy.DEFINE_MANY_MERGED) {
+ proto[name] = createMergedResultFunction(proto[name], property);
+ } else if (specPolicy === SpecPolicy.DEFINE_MANY) {
+ proto[name] = createChainedFunction(proto[name], property);
+ }
+ } else {
+ proto[name] = property;
+ if ("production" !== 'production') {
+ // Add verbose displayName to the function, which helps when looking
+ // at profiling tools.
+ if (typeof property === 'function' && spec.displayName) {
+ proto[name].displayName = spec.displayName + '_' + name;
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+function mixStaticSpecIntoComponent(Constructor, statics) {
+ if (!statics) {
+ return;
+ }
+ for (var name in statics) {
+ var property = statics[name];
+ if (!statics.hasOwnProperty(name)) {
+ continue;
+ }
+
+ var isReserved = (name in RESERVED_SPEC_KEYS);
+ !!isReserved ? "production" !== 'production' ? invariant(false, 'ReactClass: You are attempting to define a reserved ' + 'property, `%s`, that shouldn\'t be on the "statics" key. Define it ' + 'as an instance property instead; it will still be accessible on the ' + 'constructor.', name) : invariant(false) : undefined;
+
+ var isInherited = (name in Constructor);
+ !!isInherited ? "production" !== 'production' ? invariant(false, 'ReactClass: You are attempting to define ' + '`%s` on your component more than once. This conflict may be ' + 'due to a mixin.', name) : invariant(false) : undefined;
+ Constructor[name] = property;
+ }
+}
+
+/**
+ * Merge two objects, but throw if both contain the same key.
+ *
+ * @param {object} one The first object, which is mutated.
+ * @param {object} two The second object
+ * @return {object} one after it has been mutated to contain everything in two.
+ */
+function mergeIntoWithNoDuplicateKeys(one, two) {
+ !(one && two && typeof one === 'object' && typeof two === 'object') ? "production" !== 'production' ? invariant(false, 'mergeIntoWithNoDuplicateKeys(): Cannot merge non-objects.') : invariant(false) : undefined;
+
+ for (var key in two) {
+ if (two.hasOwnProperty(key)) {
+ !(one[key] === undefined) ? "production" !== 'production' ? invariant(false, 'mergeIntoWithNoDuplicateKeys(): ' + 'Tried to merge two objects with the same key: `%s`. This conflict ' + 'may be due to a mixin; in particular, this may be caused by two ' + 'getInitialState() or getDefaultProps() methods returning objects ' + 'with clashing keys.', key) : invariant(false) : undefined;
+ one[key] = two[key];
+ }
+ }
+ return one;
+}
+
+/**
+ * Creates a function that invokes two functions and merges their return values.
+ *
+ * @param {function} one Function to invoke first.
+ * @param {function} two Function to invoke second.
+ * @return {function} Function that invokes the two argument functions.
+ * @private
+ */
+function createMergedResultFunction(one, two) {
+ return function mergedResult() {
+ var a = one.apply(this, arguments);
+ var b = two.apply(this, arguments);
+ if (a == null) {
+ return b;
+ } else if (b == null) {
+ return a;
+ }
+ var c = {};
+ mergeIntoWithNoDuplicateKeys(c, a);
+ mergeIntoWithNoDuplicateKeys(c, b);
+ return c;
+ };
+}
+
+/**
+ * Creates a function that invokes two functions and ignores their return vales.
+ *
+ * @param {function} one Function to invoke first.
+ * @param {function} two Function to invoke second.
+ * @return {function} Function that invokes the two argument functions.
+ * @private
+ */
+function createChainedFunction(one, two) {
+ return function chainedFunction() {
+ one.apply(this, arguments);
+ two.apply(this, arguments);
+ };
+}
+
+/**
+ * Binds a method to the component.
+ *
+ * @param {object} component Component whose method is going to be bound.
+ * @param {function} method Method to be bound.
+ * @return {function} The bound method.
+ */
+function bindAutoBindMethod(component, method) {
+ var boundMethod = method.bind(component);
+ if ("production" !== 'production') {
+ boundMethod.__reactBoundContext = component;
+ boundMethod.__reactBoundMethod = method;
+ boundMethod.__reactBoundArguments = null;
+ var componentName = component.constructor.displayName;
+ var _bind = boundMethod.bind;
+ /* eslint-disable block-scoped-var, no-undef */
+ boundMethod.bind = function (newThis) {
+ for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
+ args[_key - 1] = arguments[_key];
+ }
+
+ // User is trying to bind() an autobound method; we effectively will
+ // ignore the value of "this" that the user is trying to use, so
+ // let's warn.
+ if (newThis !== component && newThis !== null) {
+ "production" !== 'production' ? warning(false, 'bind(): React component methods may only be bound to the ' + 'component instance. See %s', componentName) : undefined;
+ } else if (!args.length) {
+ "production" !== 'production' ? warning(false, 'bind(): You are binding a component method to the component. ' + 'React does this for you automatically in a high-performance ' + 'way, so you can safely remove this call. See %s', componentName) : undefined;
+ return boundMethod;
+ }
+ var reboundMethod = _bind.apply(boundMethod, arguments);
+ reboundMethod.__reactBoundContext = component;
+ reboundMethod.__reactBoundMethod = method;
+ reboundMethod.__reactBoundArguments = args;
+ return reboundMethod;
+ /* eslint-enable */
+ };
+ }
+ return boundMethod;
+}
+
+/**
+ * Binds all auto-bound methods in a component.
+ *
+ * @param {object} component Component whose method is going to be bound.
+ */
+function bindAutoBindMethods(component) {
+ for (var autoBindKey in component.__reactAutoBindMap) {
+ if (component.__reactAutoBindMap.hasOwnProperty(autoBindKey)) {
+ var method = component.__reactAutoBindMap[autoBindKey];
+ component[autoBindKey] = bindAutoBindMethod(component, method);
+ }
+ }
+}
+
+/**
+ * Add more to the ReactClass base class. These are all legacy features and
+ * therefore not already part of the modern ReactComponent.
+ */
+var ReactClassMixin = {
+
+ /**
+ * TODO: This will be deprecated because state should always keep a consistent
+ * type signature and the only use case for this, is to avoid that.
+ */
+ replaceState: function (newState, callback) {
+ this.updater.enqueueReplaceState(this, newState);
+ if (callback) {
+ this.updater.enqueueCallback(this, callback);
+ }
+ },
+
+ /**
+ * Checks whether or not this composite component is mounted.
+ * @return {boolean} True if mounted, false otherwise.
+ * @protected
+ * @final
+ */
+ isMounted: function () {
+ return this.updater.isMounted(this);
+ },
+
+ /**
+ * Sets a subset of the props.
+ *
+ * @param {object} partialProps Subset of the next props.
+ * @param {?function} callback Called after props are updated.
+ * @final
+ * @public
+ * @deprecated
+ */
+ setProps: function (partialProps, callback) {
+ if ("production" !== 'production') {
+ warnSetProps();
+ }
+ this.updater.enqueueSetProps(this, partialProps);
+ if (callback) {
+ this.updater.enqueueCallback(this, callback);
+ }
+ },
+
+ /**
+ * Replace all the props.
+ *
+ * @param {object} newProps Subset of the next props.
+ * @param {?function} callback Called after props are updated.
+ * @final
+ * @public
+ * @deprecated
+ */
+ replaceProps: function (newProps, callback) {
+ if ("production" !== 'production') {
+ warnSetProps();
+ }
+ this.updater.enqueueReplaceProps(this, newProps);
+ if (callback) {
+ this.updater.enqueueCallback(this, callback);
+ }
+ }
+};
+
+var ReactClassComponent = function () {};
+assign(ReactClassComponent.prototype, ReactComponent.prototype, ReactClassMixin);
+
+/**
+ * Module for creating composite components.
+ *
+ * @class ReactClass
+ */
+var ReactClass = {
+
+ /**
+ * Creates a composite component class given a class specification.
+ *
+ * @param {object} spec Class specification (which must define `render`).
+ * @return {function} Component constructor function.
+ * @public
+ */
+ createClass: function (spec) {
+ var Constructor = function (props, context, updater) {
+ // This constructor is overridden by mocks. The argument is used
+ // by mocks to assert on what gets mounted.
+
+ if ("production" !== 'production') {
+ "production" !== 'production' ? warning(this instanceof Constructor, 'Something is calling a React component directly. Use a factory or ' + 'JSX instead. See: https://fb.me/react-legacyfactory') : undefined;
+ }
+
+ // Wire up auto-binding
+ if (this.__reactAutoBindMap) {
+ bindAutoBindMethods(this);
+ }
+
+ this.props = props;
+ this.context = context;
+ this.refs = emptyObject;
+ this.updater = updater || ReactNoopUpdateQueue;
+
+ this.state = null;
+
+ // ReactClasses doesn't have constructors. Instead, they use the
+ // getInitialState and componentWillMount methods for initialization.
+
+ var initialState = this.getInitialState ? this.getInitialState() : null;
+ if ("production" !== 'production') {
+ // We allow auto-mocks to proceed as if they're returning null.
+ if (typeof initialState === 'undefined' && this.getInitialState._isMockFunction) {
+ // This is probably bad practice. Consider warning here and
+ // deprecating this convenience.
+ initialState = null;
+ }
+ }
+ !(typeof initialState === 'object' && !Array.isArray(initialState)) ? "production" !== 'production' ? invariant(false, '%s.getInitialState(): must return an object or null', Constructor.displayName || 'ReactCompositeComponent') : invariant(false) : undefined;
+
+ this.state = initialState;
+ };
+ Constructor.prototype = new ReactClassComponent();
+ Constructor.prototype.constructor = Constructor;
+
+ injectedMixins.forEach(mixSpecIntoComponent.bind(null, Constructor));
+
+ mixSpecIntoComponent(Constructor, spec);
+
+ // Initialize the defaultProps property after all mixins have been merged.
+ if (Constructor.getDefaultProps) {
+ Constructor.defaultProps = Constructor.getDefaultProps();
+ }
+
+ if ("production" !== 'production') {
+ // This is a tag to indicate that the use of these method names is ok,
+ // since it's used with createClass. If it's not, then it's likely a
+ // mistake so we'll warn you to use the static property, property
+ // initializer or constructor respectively.
+ if (Constructor.getDefaultProps) {
+ Constructor.getDefaultProps.isReactClassApproved = {};
+ }
+ if (Constructor.prototype.getInitialState) {
+ Constructor.prototype.getInitialState.isReactClassApproved = {};
+ }
+ }
+
+ !Constructor.prototype.render ? "production" !== 'production' ? invariant(false, 'createClass(...): Class specification must implement a `render` method.') : invariant(false) : undefined;
+
+ if ("production" !== 'production') {
+ "production" !== 'production' ? warning(!Constructor.prototype.componentShouldUpdate, '%s has a method called ' + 'componentShouldUpdate(). Did you mean shouldComponentUpdate()? ' + 'The name is phrased as a question because the function is ' + 'expected to return a value.', spec.displayName || 'A component') : undefined;
+ "production" !== 'production' ? warning(!Constructor.prototype.componentWillRecieveProps, '%s has a method called ' + 'componentWillRecieveProps(). Did you mean componentWillReceiveProps()?', spec.displayName || 'A component') : undefined;
+ }
+
+ // Reduce time spent doing lookups by setting these on the prototype.
+ for (var methodName in ReactClassInterface) {
+ if (!Constructor.prototype[methodName]) {
+ Constructor.prototype[methodName] = null;
+ }
+ }
+
+ return Constructor;
+ },
+
+ injection: {
+ injectMixin: function (mixin) {
+ injectedMixins.push(mixin);
+ }
+ }
+
+};
+
+module.exports = ReactClass;
+},{"154":154,"161":161,"165":165,"166":166,"173":173,"24":24,"34":34,"57":57,"76":76,"80":80,"81":81}],34:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactComponent
+ */
+
+'use strict';
+
+var ReactNoopUpdateQueue = _dereq_(76);
+
+var canDefineProperty = _dereq_(117);
+var emptyObject = _dereq_(154);
+var invariant = _dereq_(161);
+var warning = _dereq_(173);
+
+/**
+ * Base class helpers for the updating state of a component.
+ */
+function ReactComponent(props, context, updater) {
+ this.props = props;
+ this.context = context;
+ this.refs = emptyObject;
+ // We initialize the default updater but the real one gets injected by the
+ // renderer.
+ this.updater = updater || ReactNoopUpdateQueue;
+}
+
+ReactComponent.prototype.isReactComponent = {};
+
+/**
+ * Sets a subset of the state. Always use this to mutate
+ * state. You should treat `this.state` as immutable.
+ *
+ * There is no guarantee that `this.state` will be immediately updated, so
+ * accessing `this.state` after calling this method may return the old value.
+ *
+ * There is no guarantee that calls to `setState` will run synchronously,
+ * as they may eventually be batched together. You can provide an optional
+ * callback that will be executed when the call to setState is actually
+ * completed.
+ *
+ * When a function is provided to setState, it will be called at some point in
+ * the future (not synchronously). It will be called with the up to date
+ * component arguments (state, props, context). These values can be different
+ * from this.* because your function may be called after receiveProps but before
+ * shouldComponentUpdate, and this new state, props, and context will not yet be
+ * assigned to this.
+ *
+ * @param {object|function} partialState Next partial state or function to
+ * produce next partial state to be merged with current state.
+ * @param {?function} callback Called after state is updated.
+ * @final
+ * @protected
+ */
+ReactComponent.prototype.setState = function (partialState, callback) {
+ !(typeof partialState === 'object' || typeof partialState === 'function' || partialState == null) ? "production" !== 'production' ? invariant(false, 'setState(...): takes an object of state variables to update or a ' + 'function which returns an object of state variables.') : invariant(false) : undefined;
+ if ("production" !== 'production') {
+ "production" !== 'production' ? warning(partialState != null, 'setState(...): You passed an undefined or null state object; ' + 'instead, use forceUpdate().') : undefined;
+ }
+ this.updater.enqueueSetState(this, partialState);
+ if (callback) {
+ this.updater.enqueueCallback(this, callback);
+ }
+};
+
+/**
+ * Forces an update. This should only be invoked when it is known with
+ * certainty that we are **not** in a DOM transaction.
+ *
+ * You may want to call this when you know that some deeper aspect of the
+ * component's state has changed but `setState` was not called.
+ *
+ * This will not invoke `shouldComponentUpdate`, but it will invoke
+ * `componentWillUpdate` and `componentDidUpdate`.
+ *
+ * @param {?function} callback Called after update is complete.
+ * @final
+ * @protected
+ */
+ReactComponent.prototype.forceUpdate = function (callback) {
+ this.updater.enqueueForceUpdate(this);
+ if (callback) {
+ this.updater.enqueueCallback(this, callback);
+ }
+};
+
+/**
+ * Deprecated APIs. These APIs used to exist on classic React classes but since
+ * we would like to deprecate them, we're not going to move them over to this
+ * modern base class. Instead, we define a getter that warns if it's accessed.
+ */
+if ("production" !== 'production') {
+ var deprecatedAPIs = {
+ getDOMNode: ['getDOMNode', 'Use ReactDOM.findDOMNode(component) instead.'],
+ isMounted: ['isMounted', 'Instead, make sure to clean up subscriptions and pending requests in ' + 'componentWillUnmount to prevent memory leaks.'],
+ replaceProps: ['replaceProps', 'Instead, call render again at the top level.'],
+ replaceState: ['replaceState', 'Refactor your code to use setState instead (see ' + 'https://github.com/facebook/react/issues/3236).'],
+ setProps: ['setProps', 'Instead, call render again at the top level.']
+ };
+ var defineDeprecationWarning = function (methodName, info) {
+ if (canDefineProperty) {
+ Object.defineProperty(ReactComponent.prototype, methodName, {
+ get: function () {
+ "production" !== 'production' ? warning(false, '%s(...) is deprecated in plain JavaScript React classes. %s', info[0], info[1]) : undefined;
+ return undefined;
+ }
+ });
+ }
+ };
+ for (var fnName in deprecatedAPIs) {
+ if (deprecatedAPIs.hasOwnProperty(fnName)) {
+ defineDeprecationWarning(fnName, deprecatedAPIs[fnName]);
+ }
+ }
+}
+
+module.exports = ReactComponent;
+},{"117":117,"154":154,"161":161,"173":173,"76":76}],35:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactComponentBrowserEnvironment
+ */
+
+'use strict';
+
+var ReactDOMIDOperations = _dereq_(45);
+var ReactMount = _dereq_(72);
+
+/**
+ * Abstracts away all functionality of the reconciler that requires knowledge of
+ * the browser context. TODO: These callers should be refactored to avoid the
+ * need for this injection.
+ */
+var ReactComponentBrowserEnvironment = {
+
+ processChildrenUpdates: ReactDOMIDOperations.dangerouslyProcessChildrenUpdates,
+
+ replaceNodeWithMarkupByID: ReactDOMIDOperations.dangerouslyReplaceNodeWithMarkupByID,
+
+ /**
+ * If a particular environment requires that some resources be cleaned up,
+ * specify this in the injected Mixin. In the DOM, we would likely want to
+ * purge any cached node ID lookups.
+ *
+ * @private
+ */
+ unmountIDFromEnvironment: function (rootNodeID) {
+ ReactMount.purgeID(rootNodeID);
+ }
+
+};
+
+module.exports = ReactComponentBrowserEnvironment;
+},{"45":45,"72":72}],36:[function(_dereq_,module,exports){
+/**
+ * Copyright 2014-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactComponentEnvironment
+ */
+
+'use strict';
+
+var invariant = _dereq_(161);
+
+var injected = false;
+
+var ReactComponentEnvironment = {
+
+ /**
+ * Optionally injectable environment dependent cleanup hook. (server vs.
+ * browser etc). Example: A browser system caches DOM nodes based on component
+ * ID and must remove that cache entry when this instance is unmounted.
+ */
+ unmountIDFromEnvironment: null,
+
+ /**
+ * Optionally injectable hook for swapping out mount images in the middle of
+ * the tree.
+ */
+ replaceNodeWithMarkupByID: null,
+
+ /**
+ * Optionally injectable hook for processing a queue of child updates. Will
+ * later move into MultiChildComponents.
+ */
+ processChildrenUpdates: null,
+
+ injection: {
+ injectEnvironment: function (environment) {
+ !!injected ? "production" !== 'production' ? invariant(false, 'ReactCompositeComponent: injectEnvironment() can only be called once.') : invariant(false) : undefined;
+ ReactComponentEnvironment.unmountIDFromEnvironment = environment.unmountIDFromEnvironment;
+ ReactComponentEnvironment.replaceNodeWithMarkupByID = environment.replaceNodeWithMarkupByID;
+ ReactComponentEnvironment.processChildrenUpdates = environment.processChildrenUpdates;
+ injected = true;
+ }
+ }
+
+};
+
+module.exports = ReactComponentEnvironment;
+},{"161":161}],37:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactComponentWithPureRenderMixin
+ */
+
+'use strict';
+
+var shallowCompare = _dereq_(140);
+
+/**
+ * If your React component's render function is "pure", e.g. it will render the
+ * same result given the same props and state, provide this Mixin for a
+ * considerable performance boost.
+ *
+ * Most React components have pure render functions.
+ *
+ * Example:
+ *
+ * var ReactComponentWithPureRenderMixin =
+ * require('ReactComponentWithPureRenderMixin');
+ * React.createClass({
+ * mixins: [ReactComponentWithPureRenderMixin],
+ *
+ * render: function() {
+ * return <div className={this.props.className}>foo</div>;
+ * }
+ * });
+ *
+ * Note: This only checks shallow equality for props and state. If these contain
+ * complex data structures this mixin may have false-negatives for deeper
+ * differences. Only mixin to components which have simple props and state, or
+ * use `forceUpdate()` when you know deep data structures have changed.
+ */
+var ReactComponentWithPureRenderMixin = {
+ shouldComponentUpdate: function (nextProps, nextState) {
+ return shallowCompare(this, nextProps, nextState);
+ }
+};
+
+module.exports = ReactComponentWithPureRenderMixin;
+},{"140":140}],38:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactCompositeComponent
+ */
+
+'use strict';
+
+var ReactComponentEnvironment = _dereq_(36);
+var ReactCurrentOwner = _dereq_(39);
+var ReactElement = _dereq_(57);
+var ReactInstanceMap = _dereq_(68);
+var ReactPerf = _dereq_(78);
+var ReactPropTypeLocations = _dereq_(81);
+var ReactPropTypeLocationNames = _dereq_(80);
+var ReactReconciler = _dereq_(84);
+var ReactUpdateQueue = _dereq_(95);
+
+var assign = _dereq_(24);
+var emptyObject = _dereq_(154);
+var invariant = _dereq_(161);
+var shouldUpdateReactComponent = _dereq_(141);
+var warning = _dereq_(173);
+
+function getDeclarationErrorAddendum(component) {
+ var owner = component._currentElement._owner || null;
+ if (owner) {
+ var name = owner.getName();
+ if (name) {
+ return ' Check the render method of `' + name + '`.';
+ }
+ }
+ return '';
+}
+
+function StatelessComponent(Component) {}
+StatelessComponent.prototype.render = function () {
+ var Component = ReactInstanceMap.get(this)._currentElement.type;
+ return Component(this.props, this.context, this.updater);
+};
+
+/**
+ * ------------------ The Life-Cycle of a Composite Component ------------------
+ *
+ * - constructor: Initialization of state. The instance is now retained.
+ * - componentWillMount
+ * - render
+ * - [children's constructors]
+ * - [children's componentWillMount and render]
+ * - [children's componentDidMount]
+ * - componentDidMount
+ *
+ * Update Phases:
+ * - componentWillReceiveProps (only called if parent updated)
+ * - shouldComponentUpdate
+ * - componentWillUpdate
+ * - render
+ * - [children's constructors or receive props phases]
+ * - componentDidUpdate
+ *
+ * - componentWillUnmount
+ * - [children's componentWillUnmount]
+ * - [children destroyed]
+ * - (destroyed): The instance is now blank, released by React and ready for GC.
+ *
+ * -----------------------------------------------------------------------------
+ */
+
+/**
+ * An incrementing ID assigned to each component when it is mounted. This is
+ * used to enforce the order in which `ReactUpdates` updates dirty components.
+ *
+ * @private
+ */
+var nextMountID = 1;
+
+/**
+ * @lends {ReactCompositeComponent.prototype}
+ */
+var ReactCompositeComponentMixin = {
+
+ /**
+ * Base constructor for all composite component.
+ *
+ * @param {ReactElement} element
+ * @final
+ * @internal
+ */
+ construct: function (element) {
+ this._currentElement = element;
+ this._rootNodeID = null;
+ this._instance = null;
+
+ // See ReactUpdateQueue
+ this._pendingElement = null;
+ this._pendingStateQueue = null;
+ this._pendingReplaceState = false;
+ this._pendingForceUpdate = false;
+
+ this._renderedComponent = null;
+
+ this._context = null;
+ this._mountOrder = 0;
+ this._topLevelWrapper = null;
+
+ // See ReactUpdates and ReactUpdateQueue.
+ this._pendingCallbacks = null;
+ },
+
+ /**
+ * Initializes the component, renders markup, and registers event listeners.
+ *
+ * @param {string} rootID DOM ID of the root node.
+ * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
+ * @return {?string} Rendered markup to be inserted into the DOM.
+ * @final
+ * @internal
+ */
+ mountComponent: function (rootID, transaction, context) {
+ this._context = context;
+ this._mountOrder = nextMountID++;
+ this._rootNodeID = rootID;
+
+ var publicProps = this._processProps(this._currentElement.props);
+ var publicContext = this._processContext(context);
+
+ var Component = this._currentElement.type;
+
+ // Initialize the public class
+ var inst;
+ var renderedElement;
+
+ // This is a way to detect if Component is a stateless arrow function
+ // component, which is not newable. It might not be 100% reliable but is
+ // something we can do until we start detecting that Component extends
+ // React.Component. We already assume that typeof Component === 'function'.
+ var canInstantiate = ('prototype' in Component);
+
+ if (canInstantiate) {
+ if ("production" !== 'production') {
+ ReactCurrentOwner.current = this;
+ try {
+ inst = new Component(publicProps, publicContext, ReactUpdateQueue);
+ } finally {
+ ReactCurrentOwner.current = null;
+ }
+ } else {
+ inst = new Component(publicProps, publicContext, ReactUpdateQueue);
+ }
+ }
+
+ if (!canInstantiate || inst === null || inst === false || ReactElement.isValidElement(inst)) {
+ renderedElement = inst;
+ inst = new StatelessComponent(Component);
+ }
+
+ if ("production" !== 'production') {
+ // This will throw later in _renderValidatedComponent, but add an early
+ // warning now to help debugging
+ if (inst.render == null) {
+ "production" !== 'production' ? warning(false, '%s(...): No `render` method found on the returned component ' + 'instance: you may have forgotten to define `render`, returned ' + 'null/false from a stateless component, or tried to render an ' + 'element whose type is a function that isn\'t a React component.', Component.displayName || Component.name || 'Component') : undefined;
+ } else {
+ // We support ES6 inheriting from React.Component, the module pattern,
+ // and stateless components, but not ES6 classes that don't extend
+ "production" !== 'production' ? warning(Component.prototype && Component.prototype.isReactComponent || !canInstantiate || !(inst instanceof Component), '%s(...): React component classes must extend React.Component.', Component.displayName || Component.name || 'Component') : undefined;
+ }
+ }
+
+ // These should be set up in the constructor, but as a convenience for
+ // simpler class abstractions, we set them up after the fact.
+ inst.props = publicProps;
+ inst.context = publicContext;
+ inst.refs = emptyObject;
+ inst.updater = ReactUpdateQueue;
+
+ this._instance = inst;
+
+ // Store a reference from the instance back to the internal representation
+ ReactInstanceMap.set(inst, this);
+
+ if ("production" !== 'production') {
+ // Since plain JS classes are defined without any special initialization
+ // logic, we can not catch common errors early. Therefore, we have to
+ // catch them here, at initialization time, instead.
+ "production" !== 'production' ? warning(!inst.getInitialState || inst.getInitialState.isReactClassApproved, 'getInitialState was defined on %s, a plain JavaScript class. ' + 'This is only supported for classes created using React.createClass. ' + 'Did you mean to define a state property instead?', this.getName() || 'a component') : undefined;
+ "production" !== 'production' ? warning(!inst.getDefaultProps || inst.getDefaultProps.isReactClassApproved, 'getDefaultProps was defined on %s, a plain JavaScript class. ' + 'This is only supported for classes created using React.createClass. ' + 'Use a static property to define defaultProps instead.', this.getName() || 'a component') : undefined;
+ "production" !== 'production' ? warning(!inst.propTypes, 'propTypes was defined as an instance property on %s. Use a static ' + 'property to define propTypes instead.', this.getName() || 'a component') : undefined;
+ "production" !== 'production' ? warning(!inst.contextTypes, 'contextTypes was defined as an instance property on %s. Use a ' + 'static property to define contextTypes instead.', this.getName() || 'a component') : undefined;
+ "production" !== 'production' ? warning(typeof inst.componentShouldUpdate !== 'function', '%s has a method called ' + 'componentShouldUpdate(). Did you mean shouldComponentUpdate()? ' + 'The name is phrased as a question because the function is ' + 'expected to return a value.', this.getName() || 'A component') : undefined;
+ "production" !== 'production' ? warning(typeof inst.componentDidUnmount !== 'function', '%s has a method called ' + 'componentDidUnmount(). But there is no such lifecycle method. ' + 'Did you mean componentWillUnmount()?', this.getName() || 'A component') : undefined;
+ "production" !== 'production' ? warning(typeof inst.componentWillRecieveProps !== 'function', '%s has a method called ' + 'componentWillRecieveProps(). Did you mean componentWillReceiveProps()?', this.getName() || 'A component') : undefined;
+ }
+
+ var initialState = inst.state;
+ if (initialState === undefined) {
+ inst.state = initialState = null;
+ }
+ !(typeof initialState === 'object' && !Array.isArray(initialState)) ? "production" !== 'production' ? invariant(false, '%s.state: must be set to an object or null', this.getName() || 'ReactCompositeComponent') : invariant(false) : undefined;
+
+ this._pendingStateQueue = null;
+ this._pendingReplaceState = false;
+ this._pendingForceUpdate = false;
+
+ if (inst.componentWillMount) {
+ inst.componentWillMount();
+ // When mounting, calls to `setState` by `componentWillMount` will set
+ // `this._pendingStateQueue` without triggering a re-render.
+ if (this._pendingStateQueue) {
+ inst.state = this._processPendingState(inst.props, inst.context);
+ }
+ }
+
+ // If not a stateless component, we now render
+ if (renderedElement === undefined) {
+ renderedElement = this._renderValidatedComponent();
+ }
+
+ this._renderedComponent = this._instantiateReactComponent(renderedElement);
+
+ var markup = ReactReconciler.mountComponent(this._renderedComponent, rootID, transaction, this._processChildContext(context));
+ if (inst.componentDidMount) {
+ transaction.getReactMountReady().enqueue(inst.componentDidMount, inst);
+ }
+
+ return markup;
+ },
+
+ /**
+ * Releases any resources allocated by `mountComponent`.
+ *
+ * @final
+ * @internal
+ */
+ unmountComponent: function () {
+ var inst = this._instance;
+
+ if (inst.componentWillUnmount) {
+ inst.componentWillUnmount();
+ }
+
+ ReactReconciler.unmountComponent(this._renderedComponent);
+ this._renderedComponent = null;
+ this._instance = null;
+
+ // Reset pending fields
+ // Even if this component is scheduled for another update in ReactUpdates,
+ // it would still be ignored because these fields are reset.
+ this._pendingStateQueue = null;
+ this._pendingReplaceState = false;
+ this._pendingForceUpdate = false;
+ this._pendingCallbacks = null;
+ this._pendingElement = null;
+
+ // These fields do not really need to be reset since this object is no
+ // longer accessible.
+ this._context = null;
+ this._rootNodeID = null;
+ this._topLevelWrapper = null;
+
+ // Delete the reference from the instance to this internal representation
+ // which allow the internals to be properly cleaned up even if the user
+ // leaks a reference to the public instance.
+ ReactInstanceMap.remove(inst);
+
+ // Some existing components rely on inst.props even after they've been
+ // destroyed (in event handlers).
+ // TODO: inst.props = null;
+ // TODO: inst.state = null;
+ // TODO: inst.context = null;
+ },
+
+ /**
+ * Filters the context object to only contain keys specified in
+ * `contextTypes`
+ *
+ * @param {object} context
+ * @return {?object}
+ * @private
+ */
+ _maskContext: function (context) {
+ var maskedContext = null;
+ var Component = this._currentElement.type;
+ var contextTypes = Component.contextTypes;
+ if (!contextTypes) {
+ return emptyObject;
+ }
+ maskedContext = {};
+ for (var contextName in contextTypes) {
+ maskedContext[contextName] = context[contextName];
+ }
+ return maskedContext;
+ },
+
+ /**
+ * Filters the context object to only contain keys specified in
+ * `contextTypes`, and asserts that they are valid.
+ *
+ * @param {object} context
+ * @return {?object}
+ * @private
+ */
+ _processContext: function (context) {
+ var maskedContext = this._maskContext(context);
+ if ("production" !== 'production') {
+ var Component = this._currentElement.type;
+ if (Component.contextTypes) {
+ this._checkPropTypes(Component.contextTypes, maskedContext, ReactPropTypeLocations.context);
+ }
+ }
+ return maskedContext;
+ },
+
+ /**
+ * @param {object} currentContext
+ * @return {object}
+ * @private
+ */
+ _processChildContext: function (currentContext) {
+ var Component = this._currentElement.type;
+ var inst = this._instance;
+ var childContext = inst.getChildContext && inst.getChildContext();
+ if (childContext) {
+ !(typeof Component.childContextTypes === 'object') ? "production" !== 'production' ? invariant(false, '%s.getChildContext(): childContextTypes must be defined in order to ' + 'use getChildContext().', this.getName() || 'ReactCompositeComponent') : invariant(false) : undefined;
+ if ("production" !== 'production') {
+ this._checkPropTypes(Component.childContextTypes, childContext, ReactPropTypeLocations.childContext);
+ }
+ for (var name in childContext) {
+ !(name in Component.childContextTypes) ? "production" !== 'production' ? invariant(false, '%s.getChildContext(): key "%s" is not defined in childContextTypes.', this.getName() || 'ReactCompositeComponent', name) : invariant(false) : undefined;
+ }
+ return assign({}, currentContext, childContext);
+ }
+ return currentContext;
+ },
+
+ /**
+ * Processes props by setting default values for unspecified props and
+ * asserting that the props are valid. Does not mutate its argument; returns
+ * a new props object with defaults merged in.
+ *
+ * @param {object} newProps
+ * @return {object}
+ * @private
+ */
+ _processProps: function (newProps) {
+ if ("production" !== 'production') {
+ var Component = this._currentElement.type;
+ if (Component.propTypes) {
+ this._checkPropTypes(Component.propTypes, newProps, ReactPropTypeLocations.prop);
+ }
+ }
+ return newProps;
+ },
+
+ /**
+ * Assert that the props are valid
+ *
+ * @param {object} propTypes Map of prop name to a ReactPropType
+ * @param {object} props
+ * @param {string} location e.g. "prop", "context", "child context"
+ * @private
+ */
+ _checkPropTypes: function (propTypes, props, location) {
+ // TODO: Stop validating prop types here and only use the element
+ // validation.
+ var componentName = this.getName();
+ for (var propName in propTypes) {
+ if (propTypes.hasOwnProperty(propName)) {
+ var error;
+ try {
+ // This is intentionally an invariant that gets caught. It's the same
+ // behavior as without this statement except with a better message.
+ !(typeof propTypes[propName] === 'function') ? "production" !== 'production' ? invariant(false, '%s: %s type `%s` is invalid; it must be a function, usually ' + 'from React.PropTypes.', componentName || 'React class', ReactPropTypeLocationNames[location], propName) : invariant(false) : undefined;
+ error = propTypes[propName](props, propName, componentName, location);
+ } catch (ex) {
+ error = ex;
+ }
+ if (error instanceof Error) {
+ // We may want to extend this logic for similar errors in
+ // top-level render calls, so I'm abstracting it away into
+ // a function to minimize refactoring in the future
+ var addendum = getDeclarationErrorAddendum(this);
+
+ if (location === ReactPropTypeLocations.prop) {
+ // Preface gives us something to blacklist in warning module
+ "production" !== 'production' ? warning(false, 'Failed Composite propType: %s%s', error.message, addendum) : undefined;
+ } else {
+ "production" !== 'production' ? warning(false, 'Failed Context Types: %s%s', error.message, addendum) : undefined;
+ }
+ }
+ }
+ }
+ },
+
+ receiveComponent: function (nextElement, transaction, nextContext) {
+ var prevElement = this._currentElement;
+ var prevContext = this._context;
+
+ this._pendingElement = null;
+
+ this.updateComponent(transaction, prevElement, nextElement, prevContext, nextContext);
+ },
+
+ /**
+ * If any of `_pendingElement`, `_pendingStateQueue`, or `_pendingForceUpdate`
+ * is set, update the component.
+ *
+ * @param {ReactReconcileTransaction} transaction
+ * @internal
+ */
+ performUpdateIfNecessary: function (transaction) {
+ if (this._pendingElement != null) {
+ ReactReconciler.receiveComponent(this, this._pendingElement || this._currentElement, transaction, this._context);
+ }
+
+ if (this._pendingStateQueue !== null || this._pendingForceUpdate) {
+ this.updateComponent(transaction, this._currentElement, this._currentElement, this._context, this._context);
+ }
+ },
+
+ /**
+ * Perform an update to a mounted component. The componentWillReceiveProps and
+ * shouldComponentUpdate methods are called, then (assuming the update isn't
+ * skipped) the remaining update lifecycle methods are called and the DOM
+ * representation is updated.
+ *
+ * By default, this implements React's rendering and reconciliation algorithm.
+ * Sophisticated clients may wish to override this.
+ *
+ * @param {ReactReconcileTransaction} transaction
+ * @param {ReactElement} prevParentElement
+ * @param {ReactElement} nextParentElement
+ * @internal
+ * @overridable
+ */
+ updateComponent: function (transaction, prevParentElement, nextParentElement, prevUnmaskedContext, nextUnmaskedContext) {
+ var inst = this._instance;
+
+ var nextContext = this._context === nextUnmaskedContext ? inst.context : this._processContext(nextUnmaskedContext);
+ var nextProps;
+
+ // Distinguish between a props update versus a simple state update
+ if (prevParentElement === nextParentElement) {
+ // Skip checking prop types again -- we don't read inst.props to avoid
+ // warning for DOM component props in this upgrade
+ nextProps = nextParentElement.props;
+ } else {
+ nextProps = this._processProps(nextParentElement.props);
+ // An update here will schedule an update but immediately set
+ // _pendingStateQueue which will ensure that any state updates gets
+ // immediately reconciled instead of waiting for the next batch.
+
+ if (inst.componentWillReceiveProps) {
+ inst.componentWillReceiveProps(nextProps, nextContext);
+ }
+ }
+
+ var nextState = this._processPendingState(nextProps, nextContext);
+
+ var shouldUpdate = this._pendingForceUpdate || !inst.shouldComponentUpdate || inst.shouldComponentUpdate(nextProps, nextState, nextContext);
+
+ if ("production" !== 'production') {
+ "production" !== 'production' ? warning(typeof shouldUpdate !== 'undefined', '%s.shouldComponentUpdate(): Returned undefined instead of a ' + 'boolean value. Make sure to return true or false.', this.getName() || 'ReactCompositeComponent') : undefined;
+ }
+
+ if (shouldUpdate) {
+ this._pendingForceUpdate = false;
+ // Will set `this.props`, `this.state` and `this.context`.
+ this._performComponentUpdate(nextParentElement, nextProps, nextState, nextContext, transaction, nextUnmaskedContext);
+ } else {
+ // If it's determined that a component should not update, we still want
+ // to set props and state but we shortcut the rest of the update.
+ this._currentElement = nextParentElement;
+ this._context = nextUnmaskedContext;
+ inst.props = nextProps;
+ inst.state = nextState;
+ inst.context = nextContext;
+ }
+ },
+
+ _processPendingState: function (props, context) {
+ var inst = this._instance;
+ var queue = this._pendingStateQueue;
+ var replace = this._pendingReplaceState;
+ this._pendingReplaceState = false;
+ this._pendingStateQueue = null;
+
+ if (!queue) {
+ return inst.state;
+ }
+
+ if (replace && queue.length === 1) {
+ return queue[0];
+ }
+
+ var nextState = assign({}, replace ? queue[0] : inst.state);
+ for (var i = replace ? 1 : 0; i < queue.length; i++) {
+ var partial = queue[i];
+ assign(nextState, typeof partial === 'function' ? partial.call(inst, nextState, props, context) : partial);
+ }
+
+ return nextState;
+ },
+
+ /**
+ * Merges new props and state, notifies delegate methods of update and
+ * performs update.
+ *
+ * @param {ReactElement} nextElement Next element
+ * @param {object} nextProps Next public object to set as properties.
+ * @param {?object} nextState Next object to set as state.
+ * @param {?object} nextContext Next public object to set as context.
+ * @param {ReactReconcileTransaction} transaction
+ * @param {?object} unmaskedContext
+ * @private
+ */
+ _performComponentUpdate: function (nextElement, nextProps, nextState, nextContext, transaction, unmaskedContext) {
+ var inst = this._instance;
+
+ var hasComponentDidUpdate = Boolean(inst.componentDidUpdate);
+ var prevProps;
+ var prevState;
+ var prevContext;
+ if (hasComponentDidUpdate) {
+ prevProps = inst.props;
+ prevState = inst.state;
+ prevContext = inst.context;
+ }
+
+ if (inst.componentWillUpdate) {
+ inst.componentWillUpdate(nextProps, nextState, nextContext);
+ }
+
+ this._currentElement = nextElement;
+ this._context = unmaskedContext;
+ inst.props = nextProps;
+ inst.state = nextState;
+ inst.context = nextContext;
+
+ this._updateRenderedComponent(transaction, unmaskedContext);
+
+ if (hasComponentDidUpdate) {
+ transaction.getReactMountReady().enqueue(inst.componentDidUpdate.bind(inst, prevProps, prevState, prevContext), inst);
+ }
+ },
+
+ /**
+ * Call the component's `render` method and update the DOM accordingly.
+ *
+ * @param {ReactReconcileTransaction} transaction
+ * @internal
+ */
+ _updateRenderedComponent: function (transaction, context) {
+ var prevComponentInstance = this._renderedComponent;
+ var prevRenderedElement = prevComponentInstance._currentElement;
+ var nextRenderedElement = this._renderValidatedComponent();
+ if (shouldUpdateReactComponent(prevRenderedElement, nextRenderedElement)) {
+ ReactReconciler.receiveComponent(prevComponentInstance, nextRenderedElement, transaction, this._processChildContext(context));
+ } else {
+ // These two IDs are actually the same! But nothing should rely on that.
+ var thisID = this._rootNodeID;
+ var prevComponentID = prevComponentInstance._rootNodeID;
+ ReactReconciler.unmountComponent(prevComponentInstance);
+
+ this._renderedComponent = this._instantiateReactComponent(nextRenderedElement);
+ var nextMarkup = ReactReconciler.mountComponent(this._renderedComponent, thisID, transaction, this._processChildContext(context));
+ this._replaceNodeWithMarkupByID(prevComponentID, nextMarkup);
+ }
+ },
+
+ /**
+ * @protected
+ */
+ _replaceNodeWithMarkupByID: function (prevComponentID, nextMarkup) {
+ ReactComponentEnvironment.replaceNodeWithMarkupByID(prevComponentID, nextMarkup);
+ },
+
+ /**
+ * @protected
+ */
+ _renderValidatedComponentWithoutOwnerOrContext: function () {
+ var inst = this._instance;
+ var renderedComponent = inst.render();
+ if ("production" !== 'production') {
+ // We allow auto-mocks to proceed as if they're returning null.
+ if (typeof renderedComponent === 'undefined' && inst.render._isMockFunction) {
+ // This is probably bad practice. Consider warning here and
+ // deprecating this convenience.
+ renderedComponent = null;
+ }
+ }
+
+ return renderedComponent;
+ },
+
+ /**
+ * @private
+ */
+ _renderValidatedComponent: function () {
+ var renderedComponent;
+ ReactCurrentOwner.current = this;
+ try {
+ renderedComponent = this._renderValidatedComponentWithoutOwnerOrContext();
+ } finally {
+ ReactCurrentOwner.current = null;
+ }
+ !(
+ // TODO: An `isValidNode` function would probably be more appropriate
+ renderedComponent === null || renderedComponent === false || ReactElement.isValidElement(renderedComponent)) ? "production" !== 'production' ? invariant(false, '%s.render(): A valid ReactComponent must be returned. You may have ' + 'returned undefined, an array or some other invalid object.', this.getName() || 'ReactCompositeComponent') : invariant(false) : undefined;
+ return renderedComponent;
+ },
+
+ /**
+ * Lazily allocates the refs object and stores `component` as `ref`.
+ *
+ * @param {string} ref Reference name.
+ * @param {component} component Component to store as `ref`.
+ * @final
+ * @private
+ */
+ attachRef: function (ref, component) {
+ var inst = this.getPublicInstance();
+ !(inst != null) ? "production" !== 'production' ? invariant(false, 'Stateless function components cannot have refs.') : invariant(false) : undefined;
+ var publicComponentInstance = component.getPublicInstance();
+ if ("production" !== 'production') {
+ var componentName = component && component.getName ? component.getName() : 'a component';
+ "production" !== 'production' ? warning(publicComponentInstance != null, 'Stateless function components cannot be given refs ' + '(See ref "%s" in %s created by %s). ' + 'Attempts to access this ref will fail.', ref, componentName, this.getName()) : undefined;
+ }
+ var refs = inst.refs === emptyObject ? inst.refs = {} : inst.refs;
+ refs[ref] = publicComponentInstance;
+ },
+
+ /**
+ * Detaches a reference name.
+ *
+ * @param {string} ref Name to dereference.
+ * @final
+ * @private
+ */
+ detachRef: function (ref) {
+ var refs = this.getPublicInstance().refs;
+ delete refs[ref];
+ },
+
+ /**
+ * Get a text description of the component that can be used to identify it
+ * in error messages.
+ * @return {string} The name or null.
+ * @internal
+ */
+ getName: function () {
+ var type = this._currentElement.type;
+ var constructor = this._instance && this._instance.constructor;
+ return type.displayName || constructor && constructor.displayName || type.name || constructor && constructor.name || null;
+ },
+
+ /**
+ * Get the publicly accessible representation of this component - i.e. what
+ * is exposed by refs and returned by render. Can be null for stateless
+ * components.
+ *
+ * @return {ReactComponent} the public component instance.
+ * @internal
+ */
+ getPublicInstance: function () {
+ var inst = this._instance;
+ if (inst instanceof StatelessComponent) {
+ return null;
+ }
+ return inst;
+ },
+
+ // Stub
+ _instantiateReactComponent: null
+
+};
+
+ReactPerf.measureMethods(ReactCompositeComponentMixin, 'ReactCompositeComponent', {
+ mountComponent: 'mountComponent',
+ updateComponent: 'updateComponent',
+ _renderValidatedComponent: '_renderValidatedComponent'
+});
+
+var ReactCompositeComponent = {
+
+ Mixin: ReactCompositeComponentMixin
+
+};
+
+module.exports = ReactCompositeComponent;
+},{"141":141,"154":154,"161":161,"173":173,"24":24,"36":36,"39":39,"57":57,"68":68,"78":78,"80":80,"81":81,"84":84,"95":95}],39:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactCurrentOwner
+ */
+
+'use strict';
+
+/**
+ * Keeps track of the current owner.
+ *
+ * The current owner is the component who should own any components that are
+ * currently being constructed.
+ */
+var ReactCurrentOwner = {
+
+ /**
+ * @internal
+ * @type {ReactComponent}
+ */
+ current: null
+
+};
+
+module.exports = ReactCurrentOwner;
+},{}],40:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDOM
+ */
+
+/* globals __REACT_DEVTOOLS_GLOBAL_HOOK__*/
+
+'use strict';
+
+var ReactCurrentOwner = _dereq_(39);
+var ReactDOMTextComponent = _dereq_(51);
+var ReactDefaultInjection = _dereq_(54);
+var ReactInstanceHandles = _dereq_(67);
+var ReactMount = _dereq_(72);
+var ReactPerf = _dereq_(78);
+var ReactReconciler = _dereq_(84);
+var ReactUpdates = _dereq_(96);
+var ReactVersion = _dereq_(97);
+
+var findDOMNode = _dereq_(122);
+var renderSubtreeIntoContainer = _dereq_(137);
+var warning = _dereq_(173);
+
+ReactDefaultInjection.inject();
+
+var render = ReactPerf.measure('React', 'render', ReactMount.render);
+
+var React = {
+ findDOMNode: findDOMNode,
+ render: render,
+ unmountComponentAtNode: ReactMount.unmountComponentAtNode,
+ version: ReactVersion,
+
+ /* eslint-disable camelcase */
+ unstable_batchedUpdates: ReactUpdates.batchedUpdates,
+ unstable_renderSubtreeIntoContainer: renderSubtreeIntoContainer
+};
+
+// Inject the runtime into a devtools global hook regardless of browser.
+// Allows for debugging when the hook is injected on the page.
+/* eslint-enable camelcase */
+if (typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ !== 'undefined' && typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.inject === 'function') {
+ __REACT_DEVTOOLS_GLOBAL_HOOK__.inject({
+ CurrentOwner: ReactCurrentOwner,
+ InstanceHandles: ReactInstanceHandles,
+ Mount: ReactMount,
+ Reconciler: ReactReconciler,
+ TextComponent: ReactDOMTextComponent
+ });
+}
+
+if ("production" !== 'production') {
+ var ExecutionEnvironment = _dereq_(147);
+ if (ExecutionEnvironment.canUseDOM && window.top === window.self) {
+
+ // First check if devtools is not installed
+ if (typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ === 'undefined') {
+ // If we're in Chrome or Firefox, provide a download link if not installed.
+ if (navigator.userAgent.indexOf('Chrome') > -1 && navigator.userAgent.indexOf('Edge') === -1 || navigator.userAgent.indexOf('Firefox') > -1) {
+ console.debug('Download the React DevTools for a better development experience: ' + 'https://fb.me/react-devtools');
+ }
+ }
+
+ // If we're in IE8, check to see if we are in compatibility mode and provide
+ // information on preventing compatibility mode
+ var ieCompatibilityMode = document.documentMode && document.documentMode < 8;
+
+ "production" !== 'production' ? warning(!ieCompatibilityMode, 'Internet Explorer is running in compatibility mode; please add the ' + 'following tag to your HTML to prevent this from happening: ' + '<meta http-equiv="X-UA-Compatible" content="IE=edge" />') : undefined;
+
+ var expectedFeatures = [
+ // shims
+ Array.isArray, Array.prototype.every, Array.prototype.forEach, Array.prototype.indexOf, Array.prototype.map, Date.now, Function.prototype.bind, Object.keys, String.prototype.split, String.prototype.trim,
+
+ // shams
+ Object.create, Object.freeze];
+
+ for (var i = 0; i < expectedFeatures.length; i++) {
+ if (!expectedFeatures[i]) {
+ console.error('One or more ES5 shim/shams expected by React are not available: ' + 'https://fb.me/react-warning-polyfills');
+ break;
+ }
+ }
+ }
+}
+
+module.exports = React;
+},{"122":122,"137":137,"147":147,"173":173,"39":39,"51":51,"54":54,"67":67,"72":72,"78":78,"84":84,"96":96,"97":97}],41:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDOMButton
+ */
+
+'use strict';
+
+var mouseListenerNames = {
+ onClick: true,
+ onDoubleClick: true,
+ onMouseDown: true,
+ onMouseMove: true,
+ onMouseUp: true,
+
+ onClickCapture: true,
+ onDoubleClickCapture: true,
+ onMouseDownCapture: true,
+ onMouseMoveCapture: true,
+ onMouseUpCapture: true
+};
+
+/**
+ * Implements a <button> native component that does not receive mouse events
+ * when `disabled` is set.
+ */
+var ReactDOMButton = {
+ getNativeProps: function (inst, props, context) {
+ if (!props.disabled) {
+ return props;
+ }
+
+ // Copy the props, except the mouse listeners
+ var nativeProps = {};
+ for (var key in props) {
+ if (props.hasOwnProperty(key) && !mouseListenerNames[key]) {
+ nativeProps[key] = props[key];
+ }
+ }
+
+ return nativeProps;
+ }
+};
+
+module.exports = ReactDOMButton;
+},{}],42:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDOMComponent
+ * @typechecks static-only
+ */
+
+/* global hasOwnProperty:true */
+
+'use strict';
+
+var AutoFocusUtils = _dereq_(2);
+var CSSPropertyOperations = _dereq_(5);
+var DOMProperty = _dereq_(10);
+var DOMPropertyOperations = _dereq_(11);
+var EventConstants = _dereq_(15);
+var ReactBrowserEventEmitter = _dereq_(28);
+var ReactComponentBrowserEnvironment = _dereq_(35);
+var ReactDOMButton = _dereq_(41);
+var ReactDOMInput = _dereq_(46);
+var ReactDOMOption = _dereq_(47);
+var ReactDOMSelect = _dereq_(48);
+var ReactDOMTextarea = _dereq_(52);
+var ReactMount = _dereq_(72);
+var ReactMultiChild = _dereq_(73);
+var ReactPerf = _dereq_(78);
+var ReactUpdateQueue = _dereq_(95);
+
+var assign = _dereq_(24);
+var canDefineProperty = _dereq_(117);
+var escapeTextContentForBrowser = _dereq_(121);
+var invariant = _dereq_(161);
+var isEventSupported = _dereq_(133);
+var keyOf = _dereq_(166);
+var setInnerHTML = _dereq_(138);
+var setTextContent = _dereq_(139);
+var shallowEqual = _dereq_(171);
+var validateDOMNesting = _dereq_(144);
+var warning = _dereq_(173);
+
+var deleteListener = ReactBrowserEventEmitter.deleteListener;
+var listenTo = ReactBrowserEventEmitter.listenTo;
+var registrationNameModules = ReactBrowserEventEmitter.registrationNameModules;
+
+// For quickly matching children type, to test if can be treated as content.
+var CONTENT_TYPES = { 'string': true, 'number': true };
+
+var CHILDREN = keyOf({ children: null });
+var STYLE = keyOf({ style: null });
+var HTML = keyOf({ __html: null });
+
+var ELEMENT_NODE_TYPE = 1;
+
+function getDeclarationErrorAddendum(internalInstance) {
+ if (internalInstance) {
+ var owner = internalInstance._currentElement._owner || null;
+ if (owner) {
+ var name = owner.getName();
+ if (name) {
+ return ' This DOM node was rendered by `' + name + '`.';
+ }
+ }
+ }
+ return '';
+}
+
+var legacyPropsDescriptor;
+if ("production" !== 'production') {
+ legacyPropsDescriptor = {
+ props: {
+ enumerable: false,
+ get: function () {
+ var component = this._reactInternalComponent;
+ "production" !== 'production' ? warning(false, 'ReactDOMComponent: Do not access .props of a DOM node; instead, ' + 'recreate the props as `render` did originally or read the DOM ' + 'properties/attributes directly from this node (e.g., ' + 'this.refs.box.className).%s', getDeclarationErrorAddendum(component)) : undefined;
+ return component._currentElement.props;
+ }
+ }
+ };
+}
+
+function legacyGetDOMNode() {
+ if ("production" !== 'production') {
+ var component = this._reactInternalComponent;
+ "production" !== 'production' ? warning(false, 'ReactDOMComponent: Do not access .getDOMNode() of a DOM node; ' + 'instead, use the node directly.%s', getDeclarationErrorAddendum(component)) : undefined;
+ }
+ return this;
+}
+
+function legacyIsMounted() {
+ var component = this._reactInternalComponent;
+ if ("production" !== 'production') {
+ "production" !== 'production' ? warning(false, 'ReactDOMComponent: Do not access .isMounted() of a DOM node.%s', getDeclarationErrorAddendum(component)) : undefined;
+ }
+ return !!component;
+}
+
+function legacySetStateEtc() {
+ if ("production" !== 'production') {
+ var component = this._reactInternalComponent;
+ "production" !== 'production' ? warning(false, 'ReactDOMComponent: Do not access .setState(), .replaceState(), or ' + '.forceUpdate() of a DOM node. This is a no-op.%s', getDeclarationErrorAddendum(component)) : undefined;
+ }
+}
+
+function legacySetProps(partialProps, callback) {
+ var component = this._reactInternalComponent;
+ if ("production" !== 'production') {
+ "production" !== 'production' ? warning(false, 'ReactDOMComponent: Do not access .setProps() of a DOM node. ' + 'Instead, call ReactDOM.render again at the top level.%s', getDeclarationErrorAddendum(component)) : undefined;
+ }
+ if (!component) {
+ return;
+ }
+ ReactUpdateQueue.enqueueSetPropsInternal(component, partialProps);
+ if (callback) {
+ ReactUpdateQueue.enqueueCallbackInternal(component, callback);
+ }
+}
+
+function legacyReplaceProps(partialProps, callback) {
+ var component = this._reactInternalComponent;
+ if ("production" !== 'production') {
+ "production" !== 'production' ? warning(false, 'ReactDOMComponent: Do not access .replaceProps() of a DOM node. ' + 'Instead, call ReactDOM.render again at the top level.%s', getDeclarationErrorAddendum(component)) : undefined;
+ }
+ if (!component) {
+ return;
+ }
+ ReactUpdateQueue.enqueueReplacePropsInternal(component, partialProps);
+ if (callback) {
+ ReactUpdateQueue.enqueueCallbackInternal(component, callback);
+ }
+}
+
+function friendlyStringify(obj) {
+ if (typeof obj === 'object') {
+ if (Array.isArray(obj)) {
+ return '[' + obj.map(friendlyStringify).join(', ') + ']';
+ } else {
+ var pairs = [];
+ for (var key in obj) {
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
+ var keyEscaped = /^[a-z$_][\w$_]*$/i.test(key) ? key : JSON.stringify(key);
+ pairs.push(keyEscaped + ': ' + friendlyStringify(obj[key]));
+ }
+ }
+ return '{' + pairs.join(', ') + '}';
+ }
+ } else if (typeof obj === 'string') {
+ return JSON.stringify(obj);
+ } else if (typeof obj === 'function') {
+ return '[function object]';
+ }
+ // Differs from JSON.stringify in that undefined becauses undefined and that
+ // inf and nan don't become null
+ return String(obj);
+}
+
+var styleMutationWarning = {};
+
+function checkAndWarnForMutatedStyle(style1, style2, component) {
+ if (style1 == null || style2 == null) {
+ return;
+ }
+ if (shallowEqual(style1, style2)) {
+ return;
+ }
+
+ var componentName = component._tag;
+ var owner = component._currentElement._owner;
+ var ownerName;
+ if (owner) {
+ ownerName = owner.getName();
+ }
+
+ var hash = ownerName + '|' + componentName;
+
+ if (styleMutationWarning.hasOwnProperty(hash)) {
+ return;
+ }
+
+ styleMutationWarning[hash] = true;
+
+ "production" !== 'production' ? warning(false, '`%s` was passed a style object that has previously been mutated. ' + 'Mutating `style` is deprecated. Consider cloning it beforehand. Check ' + 'the `render` %s. Previous style: %s. Mutated style: %s.', componentName, owner ? 'of `' + ownerName + '`' : 'using <' + componentName + '>', friendlyStringify(style1), friendlyStringify(style2)) : undefined;
+}
+
+/**
+ * @param {object} component
+ * @param {?object} props
+ */
+function assertValidProps(component, props) {
+ if (!props) {
+ return;
+ }
+ // Note the use of `==` which checks for null or undefined.
+ if ("production" !== 'production') {
+ if (voidElementTags[component._tag]) {
+ "production" !== 'production' ? warning(props.children == null && props.dangerouslySetInnerHTML == null, '%s is a void element tag and must not have `children` or ' + 'use `props.dangerouslySetInnerHTML`.%s', component._tag, component._currentElement._owner ? ' Check the render method of ' + component._currentElement._owner.getName() + '.' : '') : undefined;
+ }
+ }
+ if (props.dangerouslySetInnerHTML != null) {
+ !(props.children == null) ? "production" !== 'production' ? invariant(false, 'Can only set one of `children` or `props.dangerouslySetInnerHTML`.') : invariant(false) : undefined;
+ !(typeof props.dangerouslySetInnerHTML === 'object' && HTML in props.dangerouslySetInnerHTML) ? "production" !== 'production' ? invariant(false, '`props.dangerouslySetInnerHTML` must be in the form `{__html: ...}`. ' + 'Please visit https://fb.me/react-invariant-dangerously-set-inner-html ' + 'for more information.') : invariant(false) : undefined;
+ }
+ if ("production" !== 'production') {
+ "production" !== 'production' ? warning(props.innerHTML == null, 'Directly setting property `innerHTML` is not permitted. ' + 'For more information, lookup documentation on `dangerouslySetInnerHTML`.') : undefined;
+ "production" !== 'production' ? warning(!props.contentEditable || props.children == null, 'A component is `contentEditable` and contains `children` managed by ' + 'React. It is now your responsibility to guarantee that none of ' + 'those nodes are unexpectedly modified or duplicated. This is ' + 'probably not intentional.') : undefined;
+ }
+ !(props.style == null || typeof props.style === 'object') ? "production" !== 'production' ? invariant(false, 'The `style` prop expects a mapping from style properties to values, ' + 'not a string. For example, style={{marginRight: spacing + \'em\'}} when ' + 'using JSX.%s', getDeclarationErrorAddendum(component)) : invariant(false) : undefined;
+}
+
+function enqueuePutListener(id, registrationName, listener, transaction) {
+ if ("production" !== 'production') {
+ // IE8 has no API for event capturing and the `onScroll` event doesn't
+ // bubble.
+ "production" !== 'production' ? warning(registrationName !== 'onScroll' || isEventSupported('scroll', true), 'This browser doesn\'t support the `onScroll` event') : undefined;
+ }
+ var container = ReactMount.findReactContainerForID(id);
+ if (container) {
+ var doc = container.nodeType === ELEMENT_NODE_TYPE ? container.ownerDocument : container;
+ listenTo(registrationName, doc);
+ }
+ transaction.getReactMountReady().enqueue(putListener, {
+ id: id,
+ registrationName: registrationName,
+ listener: listener
+ });
+}
+
+function putListener() {
+ var listenerToPut = this;
+ ReactBrowserEventEmitter.putListener(listenerToPut.id, listenerToPut.registrationName, listenerToPut.listener);
+}
+
+// There are so many media events, it makes sense to just
+// maintain a list rather than create a `trapBubbledEvent` for each
+var mediaEvents = {
+ topAbort: 'abort',
+ topCanPlay: 'canplay',
+ topCanPlayThrough: 'canplaythrough',
+ topDurationChange: 'durationchange',
+ topEmptied: 'emptied',
+ topEncrypted: 'encrypted',
+ topEnded: 'ended',
+ topError: 'error',
+ topLoadedData: 'loadeddata',
+ topLoadedMetadata: 'loadedmetadata',
+ topLoadStart: 'loadstart',
+ topPause: 'pause',
+ topPlay: 'play',
+ topPlaying: 'playing',
+ topProgress: 'progress',
+ topRateChange: 'ratechange',
+ topSeeked: 'seeked',
+ topSeeking: 'seeking',
+ topStalled: 'stalled',
+ topSuspend: 'suspend',
+ topTimeUpdate: 'timeupdate',
+ topVolumeChange: 'volumechange',
+ topWaiting: 'waiting'
+};
+
+function trapBubbledEventsLocal() {
+ var inst = this;
+ // If a component renders to null or if another component fatals and causes
+ // the state of the tree to be corrupted, `node` here can be null.
+ !inst._rootNodeID ? "production" !== 'production' ? invariant(false, 'Must be mounted to trap events') : invariant(false) : undefined;
+ var node = ReactMount.getNode(inst._rootNodeID);
+ !node ? "production" !== 'production' ? invariant(false, 'trapBubbledEvent(...): Requires node to be rendered.') : invariant(false) : undefined;
+
+ switch (inst._tag) {
+ case 'iframe':
+ inst._wrapperState.listeners = [ReactBrowserEventEmitter.trapBubbledEvent(EventConstants.topLevelTypes.topLoad, 'load', node)];
+ break;
+ case 'video':
+ case 'audio':
+
+ inst._wrapperState.listeners = [];
+ // create listener for each media event
+ for (var event in mediaEvents) {
+ if (mediaEvents.hasOwnProperty(event)) {
+ inst._wrapperState.listeners.push(ReactBrowserEventEmitter.trapBubbledEvent(EventConstants.topLevelTypes[event], mediaEvents[event], node));
+ }
+ }
+
+ break;
+ case 'img':
+ inst._wrapperState.listeners = [ReactBrowserEventEmitter.trapBubbledEvent(EventConstants.topLevelTypes.topError, 'error', node), ReactBrowserEventEmitter.trapBubbledEvent(EventConstants.topLevelTypes.topLoad, 'load', node)];
+ break;
+ case 'form':
+ inst._wrapperState.listeners = [ReactBrowserEventEmitter.trapBubbledEvent(EventConstants.topLevelTypes.topReset, 'reset', node), ReactBrowserEventEmitter.trapBubbledEvent(EventConstants.topLevelTypes.topSubmit, 'submit', node)];
+ break;
+ }
+}
+
+function mountReadyInputWrapper() {
+ ReactDOMInput.mountReadyWrapper(this);
+}
+
+function postUpdateSelectWrapper() {
+ ReactDOMSelect.postUpdateWrapper(this);
+}
+
+// For HTML, certain tags should omit their close tag. We keep a whitelist for
+// those special cased tags.
+
+var omittedCloseTags = {
+ 'area': true,
+ 'base': true,
+ 'br': true,
+ 'col': true,
+ 'embed': true,
+ 'hr': true,
+ 'img': true,
+ 'input': true,
+ 'keygen': true,
+ 'link': true,
+ 'meta': true,
+ 'param': true,
+ 'source': true,
+ 'track': true,
+ 'wbr': true
+};
+
+// NOTE: menuitem's close tag should be omitted, but that causes problems.
+var newlineEatingTags = {
+ 'listing': true,
+ 'pre': true,
+ 'textarea': true
+};
+
+// For HTML, certain tags cannot have children. This has the same purpose as
+// `omittedCloseTags` except that `menuitem` should still have its closing tag.
+
+var voidElementTags = assign({
+ 'menuitem': true
+}, omittedCloseTags);
+
+// We accept any tag to be rendered but since this gets injected into arbitrary
+// HTML, we want to make sure that it's a safe tag.
+// http://www.w3.org/TR/REC-xml/#NT-Name
+
+var VALID_TAG_REGEX = /^[a-zA-Z][a-zA-Z:_\.\-\d]*$/; // Simplified subset
+var validatedTagCache = {};
+var hasOwnProperty = ({}).hasOwnProperty;
+
+function validateDangerousTag(tag) {
+ if (!hasOwnProperty.call(validatedTagCache, tag)) {
+ !VALID_TAG_REGEX.test(tag) ? "production" !== 'production' ? invariant(false, 'Invalid tag: %s', tag) : invariant(false) : undefined;
+ validatedTagCache[tag] = true;
+ }
+}
+
+function processChildContextDev(context, inst) {
+ // Pass down our tag name to child components for validation purposes
+ context = assign({}, context);
+ var info = context[validateDOMNesting.ancestorInfoContextKey];
+ context[validateDOMNesting.ancestorInfoContextKey] = validateDOMNesting.updatedAncestorInfo(info, inst._tag, inst);
+ return context;
+}
+
+function isCustomComponent(tagName, props) {
+ return tagName.indexOf('-') >= 0 || props.is != null;
+}
+
+/**
+ * Creates a new React class that is idempotent and capable of containing other
+ * React components. It accepts event listeners and DOM properties that are
+ * valid according to `DOMProperty`.
+ *
+ * - Event listeners: `onClick`, `onMouseDown`, etc.
+ * - DOM properties: `className`, `name`, `title`, etc.
+ *
+ * The `style` property functions differently from the DOM API. It accepts an
+ * object mapping of style properties to values.
+ *
+ * @constructor ReactDOMComponent
+ * @extends ReactMultiChild
+ */
+function ReactDOMComponent(tag) {
+ validateDangerousTag(tag);
+ this._tag = tag.toLowerCase();
+ this._renderedChildren = null;
+ this._previousStyle = null;
+ this._previousStyleCopy = null;
+ this._rootNodeID = null;
+ this._wrapperState = null;
+ this._topLevelWrapper = null;
+ this._nodeWithLegacyProperties = null;
+ if ("production" !== 'production') {
+ this._unprocessedContextDev = null;
+ this._processedContextDev = null;
+ }
+}
+
+ReactDOMComponent.displayName = 'ReactDOMComponent';
+
+ReactDOMComponent.Mixin = {
+
+ construct: function (element) {
+ this._currentElement = element;
+ },
+
+ /**
+ * Generates root tag markup then recurses. This method has side effects and
+ * is not idempotent.
+ *
+ * @internal
+ * @param {string} rootID The root DOM ID for this node.
+ * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
+ * @param {object} context
+ * @return {string} The computed markup.
+ */
+ mountComponent: function (rootID, transaction, context) {
+ this._rootNodeID = rootID;
+
+ var props = this._currentElement.props;
+
+ switch (this._tag) {
+ case 'iframe':
+ case 'img':
+ case 'form':
+ case 'video':
+ case 'audio':
+ this._wrapperState = {
+ listeners: null
+ };
+ transaction.getReactMountReady().enqueue(trapBubbledEventsLocal, this);
+ break;
+ case 'button':
+ props = ReactDOMButton.getNativeProps(this, props, context);
+ break;
+ case 'input':
+ ReactDOMInput.mountWrapper(this, props, context);
+ props = ReactDOMInput.getNativeProps(this, props, context);
+ break;
+ case 'option':
+ ReactDOMOption.mountWrapper(this, props, context);
+ props = ReactDOMOption.getNativeProps(this, props, context);
+ break;
+ case 'select':
+ ReactDOMSelect.mountWrapper(this, props, context);
+ props = ReactDOMSelect.getNativeProps(this, props, context);
+ context = ReactDOMSelect.processChildContext(this, props, context);
+ break;
+ case 'textarea':
+ ReactDOMTextarea.mountWrapper(this, props, context);
+ props = ReactDOMTextarea.getNativeProps(this, props, context);
+ break;
+ }
+
+ assertValidProps(this, props);
+ if ("production" !== 'production') {
+ if (context[validateDOMNesting.ancestorInfoContextKey]) {
+ validateDOMNesting(this._tag, this, context[validateDOMNesting.ancestorInfoContextKey]);
+ }
+ }
+
+ if ("production" !== 'production') {
+ this._unprocessedContextDev = context;
+ this._processedContextDev = processChildContextDev(context, this);
+ context = this._processedContextDev;
+ }
+
+ var mountImage;
+ if (transaction.useCreateElement) {
+ var ownerDocument = context[ReactMount.ownerDocumentContextKey];
+ var el = ownerDocument.createElementNS('http://www.w3.org/1999/xhtml', this._currentElement.type);
+ DOMPropertyOperations.setAttributeForID(el, this._rootNodeID);
+ // Populate node cache
+ ReactMount.getID(el);
+ this._updateDOMProperties({}, props, transaction, el);
+ this._createInitialChildren(transaction, props, context, el);
+ mountImage = el;
+ } else {
+ var tagOpen = this._createOpenTagMarkupAndPutListeners(transaction, props);
+ var tagContent = this._createContentMarkup(transaction, props, context);
+ if (!tagContent && omittedCloseTags[this._tag]) {
+ mountImage = tagOpen + '/>';
+ } else {
+ mountImage = tagOpen + '>' + tagContent + '</' + this._currentElement.type + '>';
+ }
+ }
+
+ switch (this._tag) {
+ case 'input':
+ transaction.getReactMountReady().enqueue(mountReadyInputWrapper, this);
+ // falls through
+ case 'button':
+ case 'select':
+ case 'textarea':
+ if (props.autoFocus) {
+ transaction.getReactMountReady().enqueue(AutoFocusUtils.focusDOMComponent, this);
+ }
+ break;
+ }
+
+ return mountImage;
+ },
+
+ /**
+ * Creates markup for the open tag and all attributes.
+ *
+ * This method has side effects because events get registered.
+ *
+ * Iterating over object properties is faster than iterating over arrays.
+ * @see http://jsperf.com/obj-vs-arr-iteration
+ *
+ * @private
+ * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
+ * @param {object} props
+ * @return {string} Markup of opening tag.
+ */
+ _createOpenTagMarkupAndPutListeners: function (transaction, props) {
+ var ret = '<' + this._currentElement.type;
+
+ for (var propKey in props) {
+ if (!props.hasOwnProperty(propKey)) {
+ continue;
+ }
+ var propValue = props[propKey];
+ if (propValue == null) {
+ continue;
+ }
+ if (registrationNameModules.hasOwnProperty(propKey)) {
+ if (propValue) {
+ enqueuePutListener(this._rootNodeID, propKey, propValue, transaction);
+ }
+ } else {
+ if (propKey === STYLE) {
+ if (propValue) {
+ if ("production" !== 'production') {
+ // See `_updateDOMProperties`. style block
+ this._previousStyle = propValue;
+ }
+ propValue = this._previousStyleCopy = assign({}, props.style);
+ }
+ propValue = CSSPropertyOperations.createMarkupForStyles(propValue);
+ }
+ var markup = null;
+ if (this._tag != null && isCustomComponent(this._tag, props)) {
+ if (propKey !== CHILDREN) {
+ markup = DOMPropertyOperations.createMarkupForCustomAttribute(propKey, propValue);
+ }
+ } else {
+ markup = DOMPropertyOperations.createMarkupForProperty(propKey, propValue);
+ }
+ if (markup) {
+ ret += ' ' + markup;
+ }
+ }
+ }
+
+ // For static pages, no need to put React ID and checksum. Saves lots of
+ // bytes.
+ if (transaction.renderToStaticMarkup) {
+ return ret;
+ }
+
+ var markupForID = DOMPropertyOperations.createMarkupForID(this._rootNodeID);
+ return ret + ' ' + markupForID;
+ },
+
+ /**
+ * Creates markup for the content between the tags.
+ *
+ * @private
+ * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
+ * @param {object} props
+ * @param {object} context
+ * @return {string} Content markup.
+ */
+ _createContentMarkup: function (transaction, props, context) {
+ var ret = '';
+
+ // Intentional use of != to avoid catching zero/false.
+ var innerHTML = props.dangerouslySetInnerHTML;
+ if (innerHTML != null) {
+ if (innerHTML.__html != null) {
+ ret = innerHTML.__html;
+ }
+ } else {
+ var contentToUse = CONTENT_TYPES[typeof props.children] ? props.children : null;
+ var childrenToUse = contentToUse != null ? null : props.children;
+ if (contentToUse != null) {
+ // TODO: Validate that text is allowed as a child of this node
+ ret = escapeTextContentForBrowser(contentToUse);
+ } else if (childrenToUse != null) {
+ var mountImages = this.mountChildren(childrenToUse, transaction, context);
+ ret = mountImages.join('');
+ }
+ }
+ if (newlineEatingTags[this._tag] && ret.charAt(0) === '\n') {
+ // text/html ignores the first character in these tags if it's a newline
+ // Prefer to break application/xml over text/html (for now) by adding
+ // a newline specifically to get eaten by the parser. (Alternately for
+ // textareas, replacing "^\n" with "\r\n" doesn't get eaten, and the first
+ // \r is normalized out by HTMLTextAreaElement#value.)
+ // See: <http://www.w3.org/TR/html-polyglot/#newlines-in-textarea-and-pre>
+ // See: <http://www.w3.org/TR/html5/syntax.html#element-restrictions>
+ // See: <http://www.w3.org/TR/html5/syntax.html#newlines>
+ // See: Parsing of "textarea" "listing" and "pre" elements
+ // from <http://www.w3.org/TR/html5/syntax.html#parsing-main-inbody>
+ return '\n' + ret;
+ } else {
+ return ret;
+ }
+ },
+
+ _createInitialChildren: function (transaction, props, context, el) {
+ // Intentional use of != to avoid catching zero/false.
+ var innerHTML = props.dangerouslySetInnerHTML;
+ if (innerHTML != null) {
+ if (innerHTML.__html != null) {
+ setInnerHTML(el, innerHTML.__html);
+ }
+ } else {
+ var contentToUse = CONTENT_TYPES[typeof props.children] ? props.children : null;
+ var childrenToUse = contentToUse != null ? null : props.children;
+ if (contentToUse != null) {
+ // TODO: Validate that text is allowed as a child of this node
+ setTextContent(el, contentToUse);
+ } else if (childrenToUse != null) {
+ var mountImages = this.mountChildren(childrenToUse, transaction, context);
+ for (var i = 0; i < mountImages.length; i++) {
+ el.appendChild(mountImages[i]);
+ }
+ }
+ }
+ },
+
+ /**
+ * Receives a next element and updates the component.
+ *
+ * @internal
+ * @param {ReactElement} nextElement
+ * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
+ * @param {object} context
+ */
+ receiveComponent: function (nextElement, transaction, context) {
+ var prevElement = this._currentElement;
+ this._currentElement = nextElement;
+ this.updateComponent(transaction, prevElement, nextElement, context);
+ },
+
+ /**
+ * Updates a native DOM component after it has already been allocated and
+ * attached to the DOM. Reconciles the root DOM node, then recurses.
+ *
+ * @param {ReactReconcileTransaction} transaction
+ * @param {ReactElement} prevElement
+ * @param {ReactElement} nextElement
+ * @internal
+ * @overridable
+ */
+ updateComponent: function (transaction, prevElement, nextElement, context) {
+ var lastProps = prevElement.props;
+ var nextProps = this._currentElement.props;
+
+ switch (this._tag) {
+ case 'button':
+ lastProps = ReactDOMButton.getNativeProps(this, lastProps);
+ nextProps = ReactDOMButton.getNativeProps(this, nextProps);
+ break;
+ case 'input':
+ ReactDOMInput.updateWrapper(this);
+ lastProps = ReactDOMInput.getNativeProps(this, lastProps);
+ nextProps = ReactDOMInput.getNativeProps(this, nextProps);
+ break;
+ case 'option':
+ lastProps = ReactDOMOption.getNativeProps(this, lastProps);
+ nextProps = ReactDOMOption.getNativeProps(this, nextProps);
+ break;
+ case 'select':
+ lastProps = ReactDOMSelect.getNativeProps(this, lastProps);
+ nextProps = ReactDOMSelect.getNativeProps(this, nextProps);
+ break;
+ case 'textarea':
+ ReactDOMTextarea.updateWrapper(this);
+ lastProps = ReactDOMTextarea.getNativeProps(this, lastProps);
+ nextProps = ReactDOMTextarea.getNativeProps(this, nextProps);
+ break;
+ }
+
+ if ("production" !== 'production') {
+ // If the context is reference-equal to the old one, pass down the same
+ // processed object so the update bailout in ReactReconciler behaves
+ // correctly (and identically in dev and prod). See #5005.
+ if (this._unprocessedContextDev !== context) {
+ this._unprocessedContextDev = context;
+ this._processedContextDev = processChildContextDev(context, this);
+ }
+ context = this._processedContextDev;
+ }
+
+ assertValidProps(this, nextProps);
+ this._updateDOMProperties(lastProps, nextProps, transaction, null);
+ this._updateDOMChildren(lastProps, nextProps, transaction, context);
+
+ if (!canDefineProperty && this._nodeWithLegacyProperties) {
+ this._nodeWithLegacyProperties.props = nextProps;
+ }
+
+ if (this._tag === 'select') {
+ // <select> value update needs to occur after <option> children
+ // reconciliation
+ transaction.getReactMountReady().enqueue(postUpdateSelectWrapper, this);
+ }
+ },
+
+ /**
+ * Reconciles the properties by detecting differences in property values and
+ * updating the DOM as necessary. This function is probably the single most
+ * critical path for performance optimization.
+ *
+ * TODO: Benchmark whether checking for changed values in memory actually
+ * improves performance (especially statically positioned elements).
+ * TODO: Benchmark the effects of putting this at the top since 99% of props
+ * do not change for a given reconciliation.
+ * TODO: Benchmark areas that can be improved with caching.
+ *
+ * @private
+ * @param {object} lastProps
+ * @param {object} nextProps
+ * @param {ReactReconcileTransaction} transaction
+ * @param {?DOMElement} node
+ */
+ _updateDOMProperties: function (lastProps, nextProps, transaction, node) {
+ var propKey;
+ var styleName;
+ var styleUpdates;
+ for (propKey in lastProps) {
+ if (nextProps.hasOwnProperty(propKey) || !lastProps.hasOwnProperty(propKey)) {
+ continue;
+ }
+ if (propKey === STYLE) {
+ var lastStyle = this._previousStyleCopy;
+ for (styleName in lastStyle) {
+ if (lastStyle.hasOwnProperty(styleName)) {
+ styleUpdates = styleUpdates || {};
+ styleUpdates[styleName] = '';
+ }
+ }
+ this._previousStyleCopy = null;
+ } else if (registrationNameModules.hasOwnProperty(propKey)) {
+ if (lastProps[propKey]) {
+ // Only call deleteListener if there was a listener previously or
+ // else willDeleteListener gets called when there wasn't actually a
+ // listener (e.g., onClick={null})
+ deleteListener(this._rootNodeID, propKey);
+ }
+ } else if (DOMProperty.properties[propKey] || DOMProperty.isCustomAttribute(propKey)) {
+ if (!node) {
+ node = ReactMount.getNode(this._rootNodeID);
+ }
+ DOMPropertyOperations.deleteValueForProperty(node, propKey);
+ }
+ }
+ for (propKey in nextProps) {
+ var nextProp = nextProps[propKey];
+ var lastProp = propKey === STYLE ? this._previousStyleCopy : lastProps[propKey];
+ if (!nextProps.hasOwnProperty(propKey) || nextProp === lastProp) {
+ continue;
+ }
+ if (propKey === STYLE) {
+ if (nextProp) {
+ if ("production" !== 'production') {
+ checkAndWarnForMutatedStyle(this._previousStyleCopy, this._previousStyle, this);
+ this._previousStyle = nextProp;
+ }
+ nextProp = this._previousStyleCopy = assign({}, nextProp);
+ } else {
+ this._previousStyleCopy = null;
+ }
+ if (lastProp) {
+ // Unset styles on `lastProp` but not on `nextProp`.
+ for (styleName in lastProp) {
+ if (lastProp.hasOwnProperty(styleName) && (!nextProp || !nextProp.hasOwnProperty(styleName))) {
+ styleUpdates = styleUpdates || {};
+ styleUpdates[styleName] = '';
+ }
+ }
+ // Update styles that changed since `lastProp`.
+ for (styleName in nextProp) {
+ if (nextProp.hasOwnProperty(styleName) && lastProp[styleName] !== nextProp[styleName]) {
+ styleUpdates = styleUpdates || {};
+ styleUpdates[styleName] = nextProp[styleName];
+ }
+ }
+ } else {
+ // Relies on `updateStylesByID` not mutating `styleUpdates`.
+ styleUpdates = nextProp;
+ }
+ } else if (registrationNameModules.hasOwnProperty(propKey)) {
+ if (nextProp) {
+ enqueuePutListener(this._rootNodeID, propKey, nextProp, transaction);
+ } else if (lastProp) {
+ deleteListener(this._rootNodeID, propKey);
+ }
+ } else if (isCustomComponent(this._tag, nextProps)) {
+ if (!node) {
+ node = ReactMount.getNode(this._rootNodeID);
+ }
+ if (propKey === CHILDREN) {
+ nextProp = null;
+ }
+ DOMPropertyOperations.setValueForAttribute(node, propKey, nextProp);
+ } else if (DOMProperty.properties[propKey] || DOMProperty.isCustomAttribute(propKey)) {
+ if (!node) {
+ node = ReactMount.getNode(this._rootNodeID);
+ }
+ // If we're updating to null or undefined, we should remove the property
+ // from the DOM node instead of inadvertantly setting to a string. This
+ // brings us in line with the same behavior we have on initial render.
+ if (nextProp != null) {
+ DOMPropertyOperations.setValueForProperty(node, propKey, nextProp);
+ } else {
+ DOMPropertyOperations.deleteValueForProperty(node, propKey);
+ }
+ }
+ }
+ if (styleUpdates) {
+ if (!node) {
+ node = ReactMount.getNode(this._rootNodeID);
+ }
+ CSSPropertyOperations.setValueForStyles(node, styleUpdates);
+ }
+ },
+
+ /**
+ * Reconciles the children with the various properties that affect the
+ * children content.
+ *
+ * @param {object} lastProps
+ * @param {object} nextProps
+ * @param {ReactReconcileTransaction} transaction
+ * @param {object} context
+ */
+ _updateDOMChildren: function (lastProps, nextProps, transaction, context) {
+ var lastContent = CONTENT_TYPES[typeof lastProps.children] ? lastProps.children : null;
+ var nextContent = CONTENT_TYPES[typeof nextProps.children] ? nextProps.children : null;
+
+ var lastHtml = lastProps.dangerouslySetInnerHTML && lastProps.dangerouslySetInnerHTML.__html;
+ var nextHtml = nextProps.dangerouslySetInnerHTML && nextProps.dangerouslySetInnerHTML.__html;
+
+ // Note the use of `!=` which checks for null or undefined.
+ var lastChildren = lastContent != null ? null : lastProps.children;
+ var nextChildren = nextContent != null ? null : nextProps.children;
+
+ // If we're switching from children to content/html or vice versa, remove
+ // the old content
+ var lastHasContentOrHtml = lastContent != null || lastHtml != null;
+ var nextHasContentOrHtml = nextContent != null || nextHtml != null;
+ if (lastChildren != null && nextChildren == null) {
+ this.updateChildren(null, transaction, context);
+ } else if (lastHasContentOrHtml && !nextHasContentOrHtml) {
+ this.updateTextContent('');
+ }
+
+ if (nextContent != null) {
+ if (lastContent !== nextContent) {
+ this.updateTextContent('' + nextContent);
+ }
+ } else if (nextHtml != null) {
+ if (lastHtml !== nextHtml) {
+ this.updateMarkup('' + nextHtml);
+ }
+ } else if (nextChildren != null) {
+ this.updateChildren(nextChildren, transaction, context);
+ }
+ },
+
+ /**
+ * Destroys all event registrations for this instance. Does not remove from
+ * the DOM. That must be done by the parent.
+ *
+ * @internal
+ */
+ unmountComponent: function () {
+ switch (this._tag) {
+ case 'iframe':
+ case 'img':
+ case 'form':
+ case 'video':
+ case 'audio':
+ var listeners = this._wrapperState.listeners;
+ if (listeners) {
+ for (var i = 0; i < listeners.length; i++) {
+ listeners[i].remove();
+ }
+ }
+ break;
+ case 'input':
+ ReactDOMInput.unmountWrapper(this);
+ break;
+ case 'html':
+ case 'head':
+ case 'body':
+ /**
+ * Components like <html> <head> and <body> can't be removed or added
+ * easily in a cross-browser way, however it's valuable to be able to
+ * take advantage of React's reconciliation for styling and <title>
+ * management. So we just document it and throw in dangerous cases.
+ */
+ !false ? "production" !== 'production' ? invariant(false, '<%s> tried to unmount. Because of cross-browser quirks it is ' + 'impossible to unmount some top-level components (eg <html>, ' + '<head>, and <body>) reliably and efficiently. To fix this, have a ' + 'single top-level component that never unmounts render these ' + 'elements.', this._tag) : invariant(false) : undefined;
+ break;
+ }
+
+ this.unmountChildren();
+ ReactBrowserEventEmitter.deleteAllListeners(this._rootNodeID);
+ ReactComponentBrowserEnvironment.unmountIDFromEnvironment(this._rootNodeID);
+ this._rootNodeID = null;
+ this._wrapperState = null;
+ if (this._nodeWithLegacyProperties) {
+ var node = this._nodeWithLegacyProperties;
+ node._reactInternalComponent = null;
+ this._nodeWithLegacyProperties = null;
+ }
+ },
+
+ getPublicInstance: function () {
+ if (!this._nodeWithLegacyProperties) {
+ var node = ReactMount.getNode(this._rootNodeID);
+
+ node._reactInternalComponent = this;
+ node.getDOMNode = legacyGetDOMNode;
+ node.isMounted = legacyIsMounted;
+ node.setState = legacySetStateEtc;
+ node.replaceState = legacySetStateEtc;
+ node.forceUpdate = legacySetStateEtc;
+ node.setProps = legacySetProps;
+ node.replaceProps = legacyReplaceProps;
+
+ if ("production" !== 'production') {
+ if (canDefineProperty) {
+ Object.defineProperties(node, legacyPropsDescriptor);
+ } else {
+ // updateComponent will update this property on subsequent renders
+ node.props = this._currentElement.props;
+ }
+ } else {
+ // updateComponent will update this property on subsequent renders
+ node.props = this._currentElement.props;
+ }
+
+ this._nodeWithLegacyProperties = node;
+ }
+ return this._nodeWithLegacyProperties;
+ }
+
+};
+
+ReactPerf.measureMethods(ReactDOMComponent, 'ReactDOMComponent', {
+ mountComponent: 'mountComponent',
+ updateComponent: 'updateComponent'
+});
+
+assign(ReactDOMComponent.prototype, ReactDOMComponent.Mixin, ReactMultiChild.Mixin);
+
+module.exports = ReactDOMComponent;
+},{"10":10,"11":11,"117":117,"121":121,"133":133,"138":138,"139":139,"144":144,"15":15,"161":161,"166":166,"171":171,"173":173,"2":2,"24":24,"28":28,"35":35,"41":41,"46":46,"47":47,"48":48,"5":5,"52":52,"72":72,"73":73,"78":78,"95":95}],43:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDOMFactories
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var ReactElement = _dereq_(57);
+var ReactElementValidator = _dereq_(58);
+
+var mapObject = _dereq_(167);
+
+/**
+ * Create a factory that creates HTML tag elements.
+ *
+ * @param {string} tag Tag name (e.g. `div`).
+ * @private
+ */
+function createDOMFactory(tag) {
+ if ("production" !== 'production') {
+ return ReactElementValidator.createFactory(tag);
+ }
+ return ReactElement.createFactory(tag);
+}
+
+/**
+ * Creates a mapping from supported HTML tags to `ReactDOMComponent` classes.
+ * This is also accessible via `React.DOM`.
+ *
+ * @public
+ */
+var ReactDOMFactories = mapObject({
+ a: 'a',
+ abbr: 'abbr',
+ address: 'address',
+ area: 'area',
+ article: 'article',
+ aside: 'aside',
+ audio: 'audio',
+ b: 'b',
+ base: 'base',
+ bdi: 'bdi',
+ bdo: 'bdo',
+ big: 'big',
+ blockquote: 'blockquote',
+ body: 'body',
+ br: 'br',
+ button: 'button',
+ canvas: 'canvas',
+ caption: 'caption',
+ cite: 'cite',
+ code: 'code',
+ col: 'col',
+ colgroup: 'colgroup',
+ data: 'data',
+ datalist: 'datalist',
+ dd: 'dd',
+ del: 'del',
+ details: 'details',
+ dfn: 'dfn',
+ dialog: 'dialog',
+ div: 'div',
+ dl: 'dl',
+ dt: 'dt',
+ em: 'em',
+ embed: 'embed',
+ fieldset: 'fieldset',
+ figcaption: 'figcaption',
+ figure: 'figure',
+ footer: 'footer',
+ form: 'form',
+ h1: 'h1',
+ h2: 'h2',
+ h3: 'h3',
+ h4: 'h4',
+ h5: 'h5',
+ h6: 'h6',
+ head: 'head',
+ header: 'header',
+ hgroup: 'hgroup',
+ hr: 'hr',
+ html: 'html',
+ i: 'i',
+ iframe: 'iframe',
+ img: 'img',
+ input: 'input',
+ ins: 'ins',
+ kbd: 'kbd',
+ keygen: 'keygen',
+ label: 'label',
+ legend: 'legend',
+ li: 'li',
+ link: 'link',
+ main: 'main',
+ map: 'map',
+ mark: 'mark',
+ menu: 'menu',
+ menuitem: 'menuitem',
+ meta: 'meta',
+ meter: 'meter',
+ nav: 'nav',
+ noscript: 'noscript',
+ object: 'object',
+ ol: 'ol',
+ optgroup: 'optgroup',
+ option: 'option',
+ output: 'output',
+ p: 'p',
+ param: 'param',
+ picture: 'picture',
+ pre: 'pre',
+ progress: 'progress',
+ q: 'q',
+ rp: 'rp',
+ rt: 'rt',
+ ruby: 'ruby',
+ s: 's',
+ samp: 'samp',
+ script: 'script',
+ section: 'section',
+ select: 'select',
+ small: 'small',
+ source: 'source',
+ span: 'span',
+ strong: 'strong',
+ style: 'style',
+ sub: 'sub',
+ summary: 'summary',
+ sup: 'sup',
+ table: 'table',
+ tbody: 'tbody',
+ td: 'td',
+ textarea: 'textarea',
+ tfoot: 'tfoot',
+ th: 'th',
+ thead: 'thead',
+ time: 'time',
+ title: 'title',
+ tr: 'tr',
+ track: 'track',
+ u: 'u',
+ ul: 'ul',
+ 'var': 'var',
+ video: 'video',
+ wbr: 'wbr',
+
+ // SVG
+ circle: 'circle',
+ clipPath: 'clipPath',
+ defs: 'defs',
+ ellipse: 'ellipse',
+ g: 'g',
+ image: 'image',
+ line: 'line',
+ linearGradient: 'linearGradient',
+ mask: 'mask',
+ path: 'path',
+ pattern: 'pattern',
+ polygon: 'polygon',
+ polyline: 'polyline',
+ radialGradient: 'radialGradient',
+ rect: 'rect',
+ stop: 'stop',
+ svg: 'svg',
+ text: 'text',
+ tspan: 'tspan'
+
+}, createDOMFactory);
+
+module.exports = ReactDOMFactories;
+},{"167":167,"57":57,"58":58}],44:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDOMFeatureFlags
+ */
+
+'use strict';
+
+var ReactDOMFeatureFlags = {
+ useCreateElement: false
+};
+
+module.exports = ReactDOMFeatureFlags;
+},{}],45:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDOMIDOperations
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var DOMChildrenOperations = _dereq_(9);
+var DOMPropertyOperations = _dereq_(11);
+var ReactMount = _dereq_(72);
+var ReactPerf = _dereq_(78);
+
+var invariant = _dereq_(161);
+
+/**
+ * Errors for properties that should not be updated with `updatePropertyByID()`.
+ *
+ * @type {object}
+ * @private
+ */
+var INVALID_PROPERTY_ERRORS = {
+ dangerouslySetInnerHTML: '`dangerouslySetInnerHTML` must be set using `updateInnerHTMLByID()`.',
+ style: '`style` must be set using `updateStylesByID()`.'
+};
+
+/**
+ * Operations used to process updates to DOM nodes.
+ */
+var ReactDOMIDOperations = {
+
+ /**
+ * Updates a DOM node with new property values. This should only be used to
+ * update DOM properties in `DOMProperty`.
+ *
+ * @param {string} id ID of the node to update.
+ * @param {string} name A valid property name, see `DOMProperty`.
+ * @param {*} value New value of the property.
+ * @internal
+ */
+ updatePropertyByID: function (id, name, value) {
+ var node = ReactMount.getNode(id);
+ !!INVALID_PROPERTY_ERRORS.hasOwnProperty(name) ? "production" !== 'production' ? invariant(false, 'updatePropertyByID(...): %s', INVALID_PROPERTY_ERRORS[name]) : invariant(false) : undefined;
+
+ // If we're updating to null or undefined, we should remove the property
+ // from the DOM node instead of inadvertantly setting to a string. This
+ // brings us in line with the same behavior we have on initial render.
+ if (value != null) {
+ DOMPropertyOperations.setValueForProperty(node, name, value);
+ } else {
+ DOMPropertyOperations.deleteValueForProperty(node, name);
+ }
+ },
+
+ /**
+ * Replaces a DOM node that exists in the document with markup.
+ *
+ * @param {string} id ID of child to be replaced.
+ * @param {string} markup Dangerous markup to inject in place of child.
+ * @internal
+ * @see {Danger.dangerouslyReplaceNodeWithMarkup}
+ */
+ dangerouslyReplaceNodeWithMarkupByID: function (id, markup) {
+ var node = ReactMount.getNode(id);
+ DOMChildrenOperations.dangerouslyReplaceNodeWithMarkup(node, markup);
+ },
+
+ /**
+ * Updates a component's children by processing a series of updates.
+ *
+ * @param {array<object>} updates List of update configurations.
+ * @param {array<string>} markup List of markup strings.
+ * @internal
+ */
+ dangerouslyProcessChildrenUpdates: function (updates, markup) {
+ for (var i = 0; i < updates.length; i++) {
+ updates[i].parentNode = ReactMount.getNode(updates[i].parentID);
+ }
+ DOMChildrenOperations.processUpdates(updates, markup);
+ }
+};
+
+ReactPerf.measureMethods(ReactDOMIDOperations, 'ReactDOMIDOperations', {
+ dangerouslyReplaceNodeWithMarkupByID: 'dangerouslyReplaceNodeWithMarkupByID',
+ dangerouslyProcessChildrenUpdates: 'dangerouslyProcessChildrenUpdates'
+});
+
+module.exports = ReactDOMIDOperations;
+},{"11":11,"161":161,"72":72,"78":78,"9":9}],46:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDOMInput
+ */
+
+'use strict';
+
+var ReactDOMIDOperations = _dereq_(45);
+var LinkedValueUtils = _dereq_(23);
+var ReactMount = _dereq_(72);
+var ReactUpdates = _dereq_(96);
+
+var assign = _dereq_(24);
+var invariant = _dereq_(161);
+
+var instancesByReactID = {};
+
+function forceUpdateIfMounted() {
+ if (this._rootNodeID) {
+ // DOM component is still mounted; update
+ ReactDOMInput.updateWrapper(this);
+ }
+}
+
+/**
+ * Implements an <input> native component that allows setting these optional
+ * props: `checked`, `value`, `defaultChecked`, and `defaultValue`.
+ *
+ * If `checked` or `value` are not supplied (or null/undefined), user actions
+ * that affect the checked state or value will trigger updates to the element.
+ *
+ * If they are supplied (and not null/undefined), the rendered element will not
+ * trigger updates to the element. Instead, the props must change in order for
+ * the rendered element to be updated.
+ *
+ * The rendered element will be initialized as unchecked (or `defaultChecked`)
+ * with an empty value (or `defaultValue`).
+ *
+ * @see http://www.w3.org/TR/2012/WD-html5-20121025/the-input-element.html
+ */
+var ReactDOMInput = {
+ getNativeProps: function (inst, props, context) {
+ var value = LinkedValueUtils.getValue(props);
+ var checked = LinkedValueUtils.getChecked(props);
+
+ var nativeProps = assign({}, props, {
+ defaultChecked: undefined,
+ defaultValue: undefined,
+ value: value != null ? value : inst._wrapperState.initialValue,
+ checked: checked != null ? checked : inst._wrapperState.initialChecked,
+ onChange: inst._wrapperState.onChange
+ });
+
+ return nativeProps;
+ },
+
+ mountWrapper: function (inst, props) {
+ if ("production" !== 'production') {
+ LinkedValueUtils.checkPropTypes('input', props, inst._currentElement._owner);
+ }
+
+ var defaultValue = props.defaultValue;
+ inst._wrapperState = {
+ initialChecked: props.defaultChecked || false,
+ initialValue: defaultValue != null ? defaultValue : null,
+ onChange: _handleChange.bind(inst)
+ };
+ },
+
+ mountReadyWrapper: function (inst) {
+ // Can't be in mountWrapper or else server rendering leaks.
+ instancesByReactID[inst._rootNodeID] = inst;
+ },
+
+ unmountWrapper: function (inst) {
+ delete instancesByReactID[inst._rootNodeID];
+ },
+
+ updateWrapper: function (inst) {
+ var props = inst._currentElement.props;
+
+ // TODO: Shouldn't this be getChecked(props)?
+ var checked = props.checked;
+ if (checked != null) {
+ ReactDOMIDOperations.updatePropertyByID(inst._rootNodeID, 'checked', checked || false);
+ }
+
+ var value = LinkedValueUtils.getValue(props);
+ if (value != null) {
+ // Cast `value` to a string to ensure the value is set correctly. While
+ // browsers typically do this as necessary, jsdom doesn't.
+ ReactDOMIDOperations.updatePropertyByID(inst._rootNodeID, 'value', '' + value);
+ }
+ }
+};
+
+function _handleChange(event) {
+ var props = this._currentElement.props;
+
+ var returnValue = LinkedValueUtils.executeOnChange(props, event);
+
+ // Here we use asap to wait until all updates have propagated, which
+ // is important when using controlled components within layers:
+ // https://github.com/facebook/react/issues/1698
+ ReactUpdates.asap(forceUpdateIfMounted, this);
+
+ var name = props.name;
+ if (props.type === 'radio' && name != null) {
+ var rootNode = ReactMount.getNode(this._rootNodeID);
+ var queryRoot = rootNode;
+
+ while (queryRoot.parentNode) {
+ queryRoot = queryRoot.parentNode;
+ }
+
+ // If `rootNode.form` was non-null, then we could try `form.elements`,
+ // but that sometimes behaves strangely in IE8. We could also try using
+ // `form.getElementsByName`, but that will only return direct children
+ // and won't include inputs that use the HTML5 `form=` attribute. Since
+ // the input might not even be in a form, let's just use the global
+ // `querySelectorAll` to ensure we don't miss anything.
+ var group = queryRoot.querySelectorAll('input[name=' + JSON.stringify('' + name) + '][type="radio"]');
+
+ for (var i = 0; i < group.length; i++) {
+ var otherNode = group[i];
+ if (otherNode === rootNode || otherNode.form !== rootNode.form) {
+ continue;
+ }
+ // This will throw if radio buttons rendered by different copies of React
+ // and the same name are rendered into the same form (same as #1939).
+ // That's probably okay; we don't support it just as we don't support
+ // mixing React with non-React.
+ var otherID = ReactMount.getID(otherNode);
+ !otherID ? "production" !== 'production' ? invariant(false, 'ReactDOMInput: Mixing React and non-React radio inputs with the ' + 'same `name` is not supported.') : invariant(false) : undefined;
+ var otherInstance = instancesByReactID[otherID];
+ !otherInstance ? "production" !== 'production' ? invariant(false, 'ReactDOMInput: Unknown radio button ID %s.', otherID) : invariant(false) : undefined;
+ // If this is a controlled radio button group, forcing the input that
+ // was previously checked to update will cause it to be come re-checked
+ // as appropriate.
+ ReactUpdates.asap(forceUpdateIfMounted, otherInstance);
+ }
+ }
+
+ return returnValue;
+}
+
+module.exports = ReactDOMInput;
+},{"161":161,"23":23,"24":24,"45":45,"72":72,"96":96}],47:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDOMOption
+ */
+
+'use strict';
+
+var ReactChildren = _dereq_(32);
+var ReactDOMSelect = _dereq_(48);
+
+var assign = _dereq_(24);
+var warning = _dereq_(173);
+
+var valueContextKey = ReactDOMSelect.valueContextKey;
+
+/**
+ * Implements an <option> native component that warns when `selected` is set.
+ */
+var ReactDOMOption = {
+ mountWrapper: function (inst, props, context) {
+ // TODO (yungsters): Remove support for `selected` in <option>.
+ if ("production" !== 'production') {
+ "production" !== 'production' ? warning(props.selected == null, 'Use the `defaultValue` or `value` props on <select> instead of ' + 'setting `selected` on <option>.') : undefined;
+ }
+
+ // Look up whether this option is 'selected' via context
+ var selectValue = context[valueContextKey];
+
+ // If context key is null (e.g., no specified value or after initial mount)
+ // or missing (e.g., for <datalist>), we don't change props.selected
+ var selected = null;
+ if (selectValue != null) {
+ selected = false;
+ if (Array.isArray(selectValue)) {
+ // multiple
+ for (var i = 0; i < selectValue.length; i++) {
+ if ('' + selectValue[i] === '' + props.value) {
+ selected = true;
+ break;
+ }
+ }
+ } else {
+ selected = '' + selectValue === '' + props.value;
+ }
+ }
+
+ inst._wrapperState = { selected: selected };
+ },
+
+ getNativeProps: function (inst, props, context) {
+ var nativeProps = assign({ selected: undefined, children: undefined }, props);
+
+ // Read state only from initial mount because <select> updates value
+ // manually; we need the initial state only for server rendering
+ if (inst._wrapperState.selected != null) {
+ nativeProps.selected = inst._wrapperState.selected;
+ }
+
+ var content = '';
+
+ // Flatten children and warn if they aren't strings or numbers;
+ // invalid types are ignored.
+ ReactChildren.forEach(props.children, function (child) {
+ if (child == null) {
+ return;
+ }
+ if (typeof child === 'string' || typeof child === 'number') {
+ content += child;
+ } else {
+ "production" !== 'production' ? warning(false, 'Only strings and numbers are supported as <option> children.') : undefined;
+ }
+ });
+
+ nativeProps.children = content;
+ return nativeProps;
+ }
+
+};
+
+module.exports = ReactDOMOption;
+},{"173":173,"24":24,"32":32,"48":48}],48:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDOMSelect
+ */
+
+'use strict';
+
+var LinkedValueUtils = _dereq_(23);
+var ReactMount = _dereq_(72);
+var ReactUpdates = _dereq_(96);
+
+var assign = _dereq_(24);
+var warning = _dereq_(173);
+
+var valueContextKey = '__ReactDOMSelect_value$' + Math.random().toString(36).slice(2);
+
+function updateOptionsIfPendingUpdateAndMounted() {
+ if (this._rootNodeID && this._wrapperState.pendingUpdate) {
+ this._wrapperState.pendingUpdate = false;
+
+ var props = this._currentElement.props;
+ var value = LinkedValueUtils.getValue(props);
+
+ if (value != null) {
+ updateOptions(this, Boolean(props.multiple), value);
+ }
+ }
+}
+
+function getDeclarationErrorAddendum(owner) {
+ if (owner) {
+ var name = owner.getName();
+ if (name) {
+ return ' Check the render method of `' + name + '`.';
+ }
+ }
+ return '';
+}
+
+var valuePropNames = ['value', 'defaultValue'];
+
+/**
+ * Validation function for `value` and `defaultValue`.
+ * @private
+ */
+function checkSelectPropTypes(inst, props) {
+ var owner = inst._currentElement._owner;
+ LinkedValueUtils.checkPropTypes('select', props, owner);
+
+ for (var i = 0; i < valuePropNames.length; i++) {
+ var propName = valuePropNames[i];
+ if (props[propName] == null) {
+ continue;
+ }
+ if (props.multiple) {
+ "production" !== 'production' ? warning(Array.isArray(props[propName]), 'The `%s` prop supplied to <select> must be an array if ' + '`multiple` is true.%s', propName, getDeclarationErrorAddendum(owner)) : undefined;
+ } else {
+ "production" !== 'production' ? warning(!Array.isArray(props[propName]), 'The `%s` prop supplied to <select> must be a scalar ' + 'value if `multiple` is false.%s', propName, getDeclarationErrorAddendum(owner)) : undefined;
+ }
+ }
+}
+
+/**
+ * @param {ReactDOMComponent} inst
+ * @param {boolean} multiple
+ * @param {*} propValue A stringable (with `multiple`, a list of stringables).
+ * @private
+ */
+function updateOptions(inst, multiple, propValue) {
+ var selectedValue, i;
+ var options = ReactMount.getNode(inst._rootNodeID).options;
+
+ if (multiple) {
+ selectedValue = {};
+ for (i = 0; i < propValue.length; i++) {
+ selectedValue['' + propValue[i]] = true;
+ }
+ for (i = 0; i < options.length; i++) {
+ var selected = selectedValue.hasOwnProperty(options[i].value);
+ if (options[i].selected !== selected) {
+ options[i].selected = selected;
+ }
+ }
+ } else {
+ // Do not set `select.value` as exact behavior isn't consistent across all
+ // browsers for all cases.
+ selectedValue = '' + propValue;
+ for (i = 0; i < options.length; i++) {
+ if (options[i].value === selectedValue) {
+ options[i].selected = true;
+ return;
+ }
+ }
+ if (options.length) {
+ options[0].selected = true;
+ }
+ }
+}
+
+/**
+ * Implements a <select> native component that allows optionally setting the
+ * props `value` and `defaultValue`. If `multiple` is false, the prop must be a
+ * stringable. If `multiple` is true, the prop must be an array of stringables.
+ *
+ * If `value` is not supplied (or null/undefined), user actions that change the
+ * selected option will trigger updates to the rendered options.
+ *
+ * If it is supplied (and not null/undefined), the rendered options will not
+ * update in response to user actions. Instead, the `value` prop must change in
+ * order for the rendered options to update.
+ *
+ * If `defaultValue` is provided, any options with the supplied values will be
+ * selected.
+ */
+var ReactDOMSelect = {
+ valueContextKey: valueContextKey,
+
+ getNativeProps: function (inst, props, context) {
+ return assign({}, props, {
+ onChange: inst._wrapperState.onChange,
+ value: undefined
+ });
+ },
+
+ mountWrapper: function (inst, props) {
+ if ("production" !== 'production') {
+ checkSelectPropTypes(inst, props);
+ }
+
+ var value = LinkedValueUtils.getValue(props);
+ inst._wrapperState = {
+ pendingUpdate: false,
+ initialValue: value != null ? value : props.defaultValue,
+ onChange: _handleChange.bind(inst),
+ wasMultiple: Boolean(props.multiple)
+ };
+ },
+
+ processChildContext: function (inst, props, context) {
+ // Pass down initial value so initial generated markup has correct
+ // `selected` attributes
+ var childContext = assign({}, context);
+ childContext[valueContextKey] = inst._wrapperState.initialValue;
+ return childContext;
+ },
+
+ postUpdateWrapper: function (inst) {
+ var props = inst._currentElement.props;
+
+ // After the initial mount, we control selected-ness manually so don't pass
+ // the context value down
+ inst._wrapperState.initialValue = undefined;
+
+ var wasMultiple = inst._wrapperState.wasMultiple;
+ inst._wrapperState.wasMultiple = Boolean(props.multiple);
+
+ var value = LinkedValueUtils.getValue(props);
+ if (value != null) {
+ inst._wrapperState.pendingUpdate = false;
+ updateOptions(inst, Boolean(props.multiple), value);
+ } else if (wasMultiple !== Boolean(props.multiple)) {
+ // For simplicity, reapply `defaultValue` if `multiple` is toggled.
+ if (props.defaultValue != null) {
+ updateOptions(inst, Boolean(props.multiple), props.defaultValue);
+ } else {
+ // Revert the select back to its default unselected state.
+ updateOptions(inst, Boolean(props.multiple), props.multiple ? [] : '');
+ }
+ }
+ }
+};
+
+function _handleChange(event) {
+ var props = this._currentElement.props;
+ var returnValue = LinkedValueUtils.executeOnChange(props, event);
+
+ this._wrapperState.pendingUpdate = true;
+ ReactUpdates.asap(updateOptionsIfPendingUpdateAndMounted, this);
+ return returnValue;
+}
+
+module.exports = ReactDOMSelect;
+},{"173":173,"23":23,"24":24,"72":72,"96":96}],49:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDOMSelection
+ */
+
+'use strict';
+
+var ExecutionEnvironment = _dereq_(147);
+
+var getNodeForCharacterOffset = _dereq_(130);
+var getTextContentAccessor = _dereq_(131);
+
+/**
+ * While `isCollapsed` is available on the Selection object and `collapsed`
+ * is available on the Range object, IE11 sometimes gets them wrong.
+ * If the anchor/focus nodes and offsets are the same, the range is collapsed.
+ */
+function isCollapsed(anchorNode, anchorOffset, focusNode, focusOffset) {
+ return anchorNode === focusNode && anchorOffset === focusOffset;
+}
+
+/**
+ * Get the appropriate anchor and focus node/offset pairs for IE.
+ *
+ * The catch here is that IE's selection API doesn't provide information
+ * about whether the selection is forward or backward, so we have to
+ * behave as though it's always forward.
+ *
+ * IE text differs from modern selection in that it behaves as though
+ * block elements end with a new line. This means character offsets will
+ * differ between the two APIs.
+ *
+ * @param {DOMElement} node
+ * @return {object}
+ */
+function getIEOffsets(node) {
+ var selection = document.selection;
+ var selectedRange = selection.createRange();
+ var selectedLength = selectedRange.text.length;
+
+ // Duplicate selection so we can move range without breaking user selection.
+ var fromStart = selectedRange.duplicate();
+ fromStart.moveToElementText(node);
+ fromStart.setEndPoint('EndToStart', selectedRange);
+
+ var startOffset = fromStart.text.length;
+ var endOffset = startOffset + selectedLength;
+
+ return {
+ start: startOffset,
+ end: endOffset
+ };
+}
+
+/**
+ * @param {DOMElement} node
+ * @return {?object}
+ */
+function getModernOffsets(node) {
+ var selection = window.getSelection && window.getSelection();
+
+ if (!selection || selection.rangeCount === 0) {
+ return null;
+ }
+
+ var anchorNode = selection.anchorNode;
+ var anchorOffset = selection.anchorOffset;
+ var focusNode = selection.focusNode;
+ var focusOffset = selection.focusOffset;
+
+ var currentRange = selection.getRangeAt(0);
+
+ // In Firefox, range.startContainer and range.endContainer can be "anonymous
+ // divs", e.g. the up/down buttons on an <input type="number">. Anonymous
+ // divs do not seem to expose properties, triggering a "Permission denied
+ // error" if any of its properties are accessed. The only seemingly possible
+ // way to avoid erroring is to access a property that typically works for
+ // non-anonymous divs and catch any error that may otherwise arise. See
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=208427
+ try {
+ /* eslint-disable no-unused-expressions */
+ currentRange.startContainer.nodeType;
+ currentRange.endContainer.nodeType;
+ /* eslint-enable no-unused-expressions */
+ } catch (e) {
+ return null;
+ }
+
+ // If the node and offset values are the same, the selection is collapsed.
+ // `Selection.isCollapsed` is available natively, but IE sometimes gets
+ // this value wrong.
+ var isSelectionCollapsed = isCollapsed(selection.anchorNode, selection.anchorOffset, selection.focusNode, selection.focusOffset);
+
+ var rangeLength = isSelectionCollapsed ? 0 : currentRange.toString().length;
+
+ var tempRange = currentRange.cloneRange();
+ tempRange.selectNodeContents(node);
+ tempRange.setEnd(currentRange.startContainer, currentRange.startOffset);
+
+ var isTempRangeCollapsed = isCollapsed(tempRange.startContainer, tempRange.startOffset, tempRange.endContainer, tempRange.endOffset);
+
+ var start = isTempRangeCollapsed ? 0 : tempRange.toString().length;
+ var end = start + rangeLength;
+
+ // Detect whether the selection is backward.
+ var detectionRange = document.createRange();
+ detectionRange.setStart(anchorNode, anchorOffset);
+ detectionRange.setEnd(focusNode, focusOffset);
+ var isBackward = detectionRange.collapsed;
+
+ return {
+ start: isBackward ? end : start,
+ end: isBackward ? start : end
+ };
+}
+
+/**
+ * @param {DOMElement|DOMTextNode} node
+ * @param {object} offsets
+ */
+function setIEOffsets(node, offsets) {
+ var range = document.selection.createRange().duplicate();
+ var start, end;
+
+ if (typeof offsets.end === 'undefined') {
+ start = offsets.start;
+ end = start;
+ } else if (offsets.start > offsets.end) {
+ start = offsets.end;
+ end = offsets.start;
+ } else {
+ start = offsets.start;
+ end = offsets.end;
+ }
+
+ range.moveToElementText(node);
+ range.moveStart('character', start);
+ range.setEndPoint('EndToStart', range);
+ range.moveEnd('character', end - start);
+ range.select();
+}
+
+/**
+ * In modern non-IE browsers, we can support both forward and backward
+ * selections.
+ *
+ * Note: IE10+ supports the Selection object, but it does not support
+ * the `extend` method, which means that even in modern IE, it's not possible
+ * to programatically create a backward selection. Thus, for all IE
+ * versions, we use the old IE API to create our selections.
+ *
+ * @param {DOMElement|DOMTextNode} node
+ * @param {object} offsets
+ */
+function setModernOffsets(node, offsets) {
+ if (!window.getSelection) {
+ return;
+ }
+
+ var selection = window.getSelection();
+ var length = node[getTextContentAccessor()].length;
+ var start = Math.min(offsets.start, length);
+ var end = typeof offsets.end === 'undefined' ? start : Math.min(offsets.end, length);
+
+ // IE 11 uses modern selection, but doesn't support the extend method.
+ // Flip backward selections, so we can set with a single range.
+ if (!selection.extend && start > end) {
+ var temp = end;
+ end = start;
+ start = temp;
+ }
+
+ var startMarker = getNodeForCharacterOffset(node, start);
+ var endMarker = getNodeForCharacterOffset(node, end);
+
+ if (startMarker && endMarker) {
+ var range = document.createRange();
+ range.setStart(startMarker.node, startMarker.offset);
+ selection.removeAllRanges();
+
+ if (start > end) {
+ selection.addRange(range);
+ selection.extend(endMarker.node, endMarker.offset);
+ } else {
+ range.setEnd(endMarker.node, endMarker.offset);
+ selection.addRange(range);
+ }
+ }
+}
+
+var useIEOffsets = ExecutionEnvironment.canUseDOM && 'selection' in document && !('getSelection' in window);
+
+var ReactDOMSelection = {
+ /**
+ * @param {DOMElement} node
+ */
+ getOffsets: useIEOffsets ? getIEOffsets : getModernOffsets,
+
+ /**
+ * @param {DOMElement|DOMTextNode} node
+ * @param {object} offsets
+ */
+ setOffsets: useIEOffsets ? setIEOffsets : setModernOffsets
+};
+
+module.exports = ReactDOMSelection;
+},{"130":130,"131":131,"147":147}],50:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDOMServer
+ */
+
+'use strict';
+
+var ReactDefaultInjection = _dereq_(54);
+var ReactServerRendering = _dereq_(88);
+var ReactVersion = _dereq_(97);
+
+ReactDefaultInjection.inject();
+
+var ReactDOMServer = {
+ renderToString: ReactServerRendering.renderToString,
+ renderToStaticMarkup: ReactServerRendering.renderToStaticMarkup,
+ version: ReactVersion
+};
+
+module.exports = ReactDOMServer;
+},{"54":54,"88":88,"97":97}],51:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDOMTextComponent
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var DOMChildrenOperations = _dereq_(9);
+var DOMPropertyOperations = _dereq_(11);
+var ReactComponentBrowserEnvironment = _dereq_(35);
+var ReactMount = _dereq_(72);
+
+var assign = _dereq_(24);
+var escapeTextContentForBrowser = _dereq_(121);
+var setTextContent = _dereq_(139);
+var validateDOMNesting = _dereq_(144);
+
+/**
+ * Text nodes violate a couple assumptions that React makes about components:
+ *
+ * - When mounting text into the DOM, adjacent text nodes are merged.
+ * - Text nodes cannot be assigned a React root ID.
+ *
+ * This component is used to wrap strings in elements so that they can undergo
+ * the same reconciliation that is applied to elements.
+ *
+ * TODO: Investigate representing React components in the DOM with text nodes.
+ *
+ * @class ReactDOMTextComponent
+ * @extends ReactComponent
+ * @internal
+ */
+var ReactDOMTextComponent = function (props) {
+ // This constructor and its argument is currently used by mocks.
+};
+
+assign(ReactDOMTextComponent.prototype, {
+
+ /**
+ * @param {ReactText} text
+ * @internal
+ */
+ construct: function (text) {
+ // TODO: This is really a ReactText (ReactNode), not a ReactElement
+ this._currentElement = text;
+ this._stringText = '' + text;
+
+ // Properties
+ this._rootNodeID = null;
+ this._mountIndex = 0;
+ },
+
+ /**
+ * Creates the markup for this text node. This node is not intended to have
+ * any features besides containing text content.
+ *
+ * @param {string} rootID DOM ID of the root node.
+ * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
+ * @return {string} Markup for this text node.
+ * @internal
+ */
+ mountComponent: function (rootID, transaction, context) {
+ if ("production" !== 'production') {
+ if (context[validateDOMNesting.ancestorInfoContextKey]) {
+ validateDOMNesting('span', null, context[validateDOMNesting.ancestorInfoContextKey]);
+ }
+ }
+
+ this._rootNodeID = rootID;
+ if (transaction.useCreateElement) {
+ var ownerDocument = context[ReactMount.ownerDocumentContextKey];
+ var el = ownerDocument.createElementNS('http://www.w3.org/1999/xhtml', 'span');
+ DOMPropertyOperations.setAttributeForID(el, rootID);
+ // Populate node cache
+ ReactMount.getID(el);
+ setTextContent(el, this._stringText);
+ return el;
+ } else {
+ var escapedText = escapeTextContentForBrowser(this._stringText);
+
+ if (transaction.renderToStaticMarkup) {
+ // Normally we'd wrap this in a `span` for the reasons stated above, but
+ // since this is a situation where React won't take over (static pages),
+ // we can simply return the text as it is.
+ return escapedText;
+ }
+
+ return '<span ' + DOMPropertyOperations.createMarkupForID(rootID) + '>' + escapedText + '</span>';
+ }
+ },
+
+ /**
+ * Updates this component by updating the text content.
+ *
+ * @param {ReactText} nextText The next text content
+ * @param {ReactReconcileTransaction} transaction
+ * @internal
+ */
+ receiveComponent: function (nextText, transaction) {
+ if (nextText !== this._currentElement) {
+ this._currentElement = nextText;
+ var nextStringText = '' + nextText;
+ if (nextStringText !== this._stringText) {
+ // TODO: Save this as pending props and use performUpdateIfNecessary
+ // and/or updateComponent to do the actual update for consistency with
+ // other component types?
+ this._stringText = nextStringText;
+ var node = ReactMount.getNode(this._rootNodeID);
+ DOMChildrenOperations.updateTextContent(node, nextStringText);
+ }
+ }
+ },
+
+ unmountComponent: function () {
+ ReactComponentBrowserEnvironment.unmountIDFromEnvironment(this._rootNodeID);
+ }
+
+});
+
+module.exports = ReactDOMTextComponent;
+},{"11":11,"121":121,"139":139,"144":144,"24":24,"35":35,"72":72,"9":9}],52:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDOMTextarea
+ */
+
+'use strict';
+
+var LinkedValueUtils = _dereq_(23);
+var ReactDOMIDOperations = _dereq_(45);
+var ReactUpdates = _dereq_(96);
+
+var assign = _dereq_(24);
+var invariant = _dereq_(161);
+var warning = _dereq_(173);
+
+function forceUpdateIfMounted() {
+ if (this._rootNodeID) {
+ // DOM component is still mounted; update
+ ReactDOMTextarea.updateWrapper(this);
+ }
+}
+
+/**
+ * Implements a <textarea> native component that allows setting `value`, and
+ * `defaultValue`. This differs from the traditional DOM API because value is
+ * usually set as PCDATA children.
+ *
+ * If `value` is not supplied (or null/undefined), user actions that affect the
+ * value will trigger updates to the element.
+ *
+ * If `value` is supplied (and not null/undefined), the rendered element will
+ * not trigger updates to the element. Instead, the `value` prop must change in
+ * order for the rendered element to be updated.
+ *
+ * The rendered element will be initialized with an empty value, the prop
+ * `defaultValue` if specified, or the children content (deprecated).
+ */
+var ReactDOMTextarea = {
+ getNativeProps: function (inst, props, context) {
+ !(props.dangerouslySetInnerHTML == null) ? "production" !== 'production' ? invariant(false, '`dangerouslySetInnerHTML` does not make sense on <textarea>.') : invariant(false) : undefined;
+
+ // Always set children to the same thing. In IE9, the selection range will
+ // get reset if `textContent` is mutated.
+ var nativeProps = assign({}, props, {
+ defaultValue: undefined,
+ value: undefined,
+ children: inst._wrapperState.initialValue,
+ onChange: inst._wrapperState.onChange
+ });
+
+ return nativeProps;
+ },
+
+ mountWrapper: function (inst, props) {
+ if ("production" !== 'production') {
+ LinkedValueUtils.checkPropTypes('textarea', props, inst._currentElement._owner);
+ }
+
+ var defaultValue = props.defaultValue;
+ // TODO (yungsters): Remove support for children content in <textarea>.
+ var children = props.children;
+ if (children != null) {
+ if ("production" !== 'production') {
+ "production" !== 'production' ? warning(false, 'Use the `defaultValue` or `value` props instead of setting ' + 'children on <textarea>.') : undefined;
+ }
+ !(defaultValue == null) ? "production" !== 'production' ? invariant(false, 'If you supply `defaultValue` on a <textarea>, do not pass children.') : invariant(false) : undefined;
+ if (Array.isArray(children)) {
+ !(children.length <= 1) ? "production" !== 'production' ? invariant(false, '<textarea> can only have at most one child.') : invariant(false) : undefined;
+ children = children[0];
+ }
+
+ defaultValue = '' + children;
+ }
+ if (defaultValue == null) {
+ defaultValue = '';
+ }
+ var value = LinkedValueUtils.getValue(props);
+
+ inst._wrapperState = {
+ // We save the initial value so that `ReactDOMComponent` doesn't update
+ // `textContent` (unnecessary since we update value).
+ // The initial value can be a boolean or object so that's why it's
+ // forced to be a string.
+ initialValue: '' + (value != null ? value : defaultValue),
+ onChange: _handleChange.bind(inst)
+ };
+ },
+
+ updateWrapper: function (inst) {
+ var props = inst._currentElement.props;
+ var value = LinkedValueUtils.getValue(props);
+ if (value != null) {
+ // Cast `value` to a string to ensure the value is set correctly. While
+ // browsers typically do this as necessary, jsdom doesn't.
+ ReactDOMIDOperations.updatePropertyByID(inst._rootNodeID, 'value', '' + value);
+ }
+ }
+};
+
+function _handleChange(event) {
+ var props = this._currentElement.props;
+ var returnValue = LinkedValueUtils.executeOnChange(props, event);
+ ReactUpdates.asap(forceUpdateIfMounted, this);
+ return returnValue;
+}
+
+module.exports = ReactDOMTextarea;
+},{"161":161,"173":173,"23":23,"24":24,"45":45,"96":96}],53:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDefaultBatchingStrategy
+ */
+
+'use strict';
+
+var ReactUpdates = _dereq_(96);
+var Transaction = _dereq_(113);
+
+var assign = _dereq_(24);
+var emptyFunction = _dereq_(153);
+
+var RESET_BATCHED_UPDATES = {
+ initialize: emptyFunction,
+ close: function () {
+ ReactDefaultBatchingStrategy.isBatchingUpdates = false;
+ }
+};
+
+var FLUSH_BATCHED_UPDATES = {
+ initialize: emptyFunction,
+ close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)
+};
+
+var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];
+
+function ReactDefaultBatchingStrategyTransaction() {
+ this.reinitializeTransaction();
+}
+
+assign(ReactDefaultBatchingStrategyTransaction.prototype, Transaction.Mixin, {
+ getTransactionWrappers: function () {
+ return TRANSACTION_WRAPPERS;
+ }
+});
+
+var transaction = new ReactDefaultBatchingStrategyTransaction();
+
+var ReactDefaultBatchingStrategy = {
+ isBatchingUpdates: false,
+
+ /**
+ * Call the provided function in a context within which calls to `setState`
+ * and friends are batched such that components aren't updated unnecessarily.
+ */
+ batchedUpdates: function (callback, a, b, c, d, e) {
+ var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
+
+ ReactDefaultBatchingStrategy.isBatchingUpdates = true;
+
+ // The code is written this way to avoid extra allocations
+ if (alreadyBatchingUpdates) {
+ callback(a, b, c, d, e);
+ } else {
+ transaction.perform(callback, null, a, b, c, d, e);
+ }
+ }
+};
+
+module.exports = ReactDefaultBatchingStrategy;
+},{"113":113,"153":153,"24":24,"96":96}],54:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDefaultInjection
+ */
+
+'use strict';
+
+var BeforeInputEventPlugin = _dereq_(3);
+var ChangeEventPlugin = _dereq_(7);
+var ClientReactRootIndex = _dereq_(8);
+var DefaultEventPluginOrder = _dereq_(13);
+var EnterLeaveEventPlugin = _dereq_(14);
+var ExecutionEnvironment = _dereq_(147);
+var HTMLDOMPropertyConfig = _dereq_(21);
+var ReactBrowserComponentMixin = _dereq_(27);
+var ReactComponentBrowserEnvironment = _dereq_(35);
+var ReactDefaultBatchingStrategy = _dereq_(53);
+var ReactDOMComponent = _dereq_(42);
+var ReactDOMTextComponent = _dereq_(51);
+var ReactEventListener = _dereq_(63);
+var ReactInjection = _dereq_(65);
+var ReactInstanceHandles = _dereq_(67);
+var ReactMount = _dereq_(72);
+var ReactReconcileTransaction = _dereq_(83);
+var SelectEventPlugin = _dereq_(99);
+var ServerReactRootIndex = _dereq_(100);
+var SimpleEventPlugin = _dereq_(101);
+var SVGDOMPropertyConfig = _dereq_(98);
+
+var alreadyInjected = false;
+
+function inject() {
+ if (alreadyInjected) {
+ // TODO: This is currently true because these injections are shared between
+ // the client and the server package. They should be built independently
+ // and not share any injection state. Then this problem will be solved.
+ return;
+ }
+ alreadyInjected = true;
+
+ ReactInjection.EventEmitter.injectReactEventListener(ReactEventListener);
+
+ /**
+ * Inject modules for resolving DOM hierarchy and plugin ordering.
+ */
+ ReactInjection.EventPluginHub.injectEventPluginOrder(DefaultEventPluginOrder);
+ ReactInjection.EventPluginHub.injectInstanceHandle(ReactInstanceHandles);
+ ReactInjection.EventPluginHub.injectMount(ReactMount);
+
+ /**
+ * Some important event plugins included by default (without having to require
+ * them).
+ */
+ ReactInjection.EventPluginHub.injectEventPluginsByName({
+ SimpleEventPlugin: SimpleEventPlugin,
+ EnterLeaveEventPlugin: EnterLeaveEventPlugin,
+ ChangeEventPlugin: ChangeEventPlugin,
+ SelectEventPlugin: SelectEventPlugin,
+ BeforeInputEventPlugin: BeforeInputEventPlugin
+ });
+
+ ReactInjection.NativeComponent.injectGenericComponentClass(ReactDOMComponent);
+
+ ReactInjection.NativeComponent.injectTextComponentClass(ReactDOMTextComponent);
+
+ ReactInjection.Class.injectMixin(ReactBrowserComponentMixin);
+
+ ReactInjection.DOMProperty.injectDOMPropertyConfig(HTMLDOMPropertyConfig);
+ ReactInjection.DOMProperty.injectDOMPropertyConfig(SVGDOMPropertyConfig);
+
+ ReactInjection.EmptyComponent.injectEmptyComponent('noscript');
+
+ ReactInjection.Updates.injectReconcileTransaction(ReactReconcileTransaction);
+ ReactInjection.Updates.injectBatchingStrategy(ReactDefaultBatchingStrategy);
+
+ ReactInjection.RootIndex.injectCreateReactRootIndex(ExecutionEnvironment.canUseDOM ? ClientReactRootIndex.createReactRootIndex : ServerReactRootIndex.createReactRootIndex);
+
+ ReactInjection.Component.injectEnvironment(ReactComponentBrowserEnvironment);
+
+ if ("production" !== 'production') {
+ var url = ExecutionEnvironment.canUseDOM && window.location.href || '';
+ if (/[?&]react_perf\b/.test(url)) {
+ var ReactDefaultPerf = _dereq_(55);
+ ReactDefaultPerf.start();
+ }
+ }
+}
+
+module.exports = {
+ inject: inject
+};
+},{"100":100,"101":101,"13":13,"14":14,"147":147,"21":21,"27":27,"3":3,"35":35,"42":42,"51":51,"53":53,"55":55,"63":63,"65":65,"67":67,"7":7,"72":72,"8":8,"83":83,"98":98,"99":99}],55:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDefaultPerf
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var DOMProperty = _dereq_(10);
+var ReactDefaultPerfAnalysis = _dereq_(56);
+var ReactMount = _dereq_(72);
+var ReactPerf = _dereq_(78);
+
+var performanceNow = _dereq_(170);
+
+function roundFloat(val) {
+ return Math.floor(val * 100) / 100;
+}
+
+function addValue(obj, key, val) {
+ obj[key] = (obj[key] || 0) + val;
+}
+
+var ReactDefaultPerf = {
+ _allMeasurements: [], // last item in the list is the current one
+ _mountStack: [0],
+ _injected: false,
+
+ start: function () {
+ if (!ReactDefaultPerf._injected) {
+ ReactPerf.injection.injectMeasure(ReactDefaultPerf.measure);
+ }
+
+ ReactDefaultPerf._allMeasurements.length = 0;
+ ReactPerf.enableMeasure = true;
+ },
+
+ stop: function () {
+ ReactPerf.enableMeasure = false;
+ },
+
+ getLastMeasurements: function () {
+ return ReactDefaultPerf._allMeasurements;
+ },
+
+ printExclusive: function (measurements) {
+ measurements = measurements || ReactDefaultPerf._allMeasurements;
+ var summary = ReactDefaultPerfAnalysis.getExclusiveSummary(measurements);
+ console.table(summary.map(function (item) {
+ return {
+ 'Component class name': item.componentName,
+ 'Total inclusive time (ms)': roundFloat(item.inclusive),
+ 'Exclusive mount time (ms)': roundFloat(item.exclusive),
+ 'Exclusive render time (ms)': roundFloat(item.render),
+ 'Mount time per instance (ms)': roundFloat(item.exclusive / item.count),
+ 'Render time per instance (ms)': roundFloat(item.render / item.count),
+ 'Instances': item.count
+ };
+ }));
+ // TODO: ReactDefaultPerfAnalysis.getTotalTime() does not return the correct
+ // number.
+ },
+
+ printInclusive: function (measurements) {
+ measurements = measurements || ReactDefaultPerf._allMeasurements;
+ var summary = ReactDefaultPerfAnalysis.getInclusiveSummary(measurements);
+ console.table(summary.map(function (item) {
+ return {
+ 'Owner > component': item.componentName,
+ 'Inclusive time (ms)': roundFloat(item.time),
+ 'Instances': item.count
+ };
+ }));
+ console.log('Total time:', ReactDefaultPerfAnalysis.getTotalTime(measurements).toFixed(2) + ' ms');
+ },
+
+ getMeasurementsSummaryMap: function (measurements) {
+ var summary = ReactDefaultPerfAnalysis.getInclusiveSummary(measurements, true);
+ return summary.map(function (item) {
+ return {
+ 'Owner > component': item.componentName,
+ 'Wasted time (ms)': item.time,
+ 'Instances': item.count
+ };
+ });
+ },
+
+ printWasted: function (measurements) {
+ measurements = measurements || ReactDefaultPerf._allMeasurements;
+ console.table(ReactDefaultPerf.getMeasurementsSummaryMap(measurements));
+ console.log('Total time:', ReactDefaultPerfAnalysis.getTotalTime(measurements).toFixed(2) + ' ms');
+ },
+
+ printDOM: function (measurements) {
+ measurements = measurements || ReactDefaultPerf._allMeasurements;
+ var summary = ReactDefaultPerfAnalysis.getDOMSummary(measurements);
+ console.table(summary.map(function (item) {
+ var result = {};
+ result[DOMProperty.ID_ATTRIBUTE_NAME] = item.id;
+ result.type = item.type;
+ result.args = JSON.stringify(item.args);
+ return result;
+ }));
+ console.log('Total time:', ReactDefaultPerfAnalysis.getTotalTime(measurements).toFixed(2) + ' ms');
+ },
+
+ _recordWrite: function (id, fnName, totalTime, args) {
+ // TODO: totalTime isn't that useful since it doesn't count paints/reflows
+ var writes = ReactDefaultPerf._allMeasurements[ReactDefaultPerf._allMeasurements.length - 1].writes;
+ writes[id] = writes[id] || [];
+ writes[id].push({
+ type: fnName,
+ time: totalTime,
+ args: args
+ });
+ },
+
+ measure: function (moduleName, fnName, func) {
+ return function () {
+ for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
+ args[_key] = arguments[_key];
+ }
+
+ var totalTime;
+ var rv;
+ var start;
+
+ if (fnName === '_renderNewRootComponent' || fnName === 'flushBatchedUpdates') {
+ // A "measurement" is a set of metrics recorded for each flush. We want
+ // to group the metrics for a given flush together so we can look at the
+ // components that rendered and the DOM operations that actually
+ // happened to determine the amount of "wasted work" performed.
+ ReactDefaultPerf._allMeasurements.push({
+ exclusive: {},
+ inclusive: {},
+ render: {},
+ counts: {},
+ writes: {},
+ displayNames: {},
+ totalTime: 0,
+ created: {}
+ });
+ start = performanceNow();
+ rv = func.apply(this, args);
+ ReactDefaultPerf._allMeasurements[ReactDefaultPerf._allMeasurements.length - 1].totalTime = performanceNow() - start;
+ return rv;
+ } else if (fnName === '_mountImageIntoNode' || moduleName === 'ReactBrowserEventEmitter' || moduleName === 'ReactDOMIDOperations' || moduleName === 'CSSPropertyOperations' || moduleName === 'DOMChildrenOperations' || moduleName === 'DOMPropertyOperations') {
+ start = performanceNow();
+ rv = func.apply(this, args);
+ totalTime = performanceNow() - start;
+
+ if (fnName === '_mountImageIntoNode') {
+ var mountID = ReactMount.getID(args[1]);
+ ReactDefaultPerf._recordWrite(mountID, fnName, totalTime, args[0]);
+ } else if (fnName === 'dangerouslyProcessChildrenUpdates') {
+ // special format
+ args[0].forEach(function (update) {
+ var writeArgs = {};
+ if (update.fromIndex !== null) {
+ writeArgs.fromIndex = update.fromIndex;
+ }
+ if (update.toIndex !== null) {
+ writeArgs.toIndex = update.toIndex;
+ }
+ if (update.textContent !== null) {
+ writeArgs.textContent = update.textContent;
+ }
+ if (update.markupIndex !== null) {
+ writeArgs.markup = args[1][update.markupIndex];
+ }
+ ReactDefaultPerf._recordWrite(update.parentID, update.type, totalTime, writeArgs);
+ });
+ } else {
+ // basic format
+ var id = args[0];
+ if (typeof id === 'object') {
+ id = ReactMount.getID(args[0]);
+ }
+ ReactDefaultPerf._recordWrite(id, fnName, totalTime, Array.prototype.slice.call(args, 1));
+ }
+ return rv;
+ } else if (moduleName === 'ReactCompositeComponent' && (fnName === 'mountComponent' || fnName === 'updateComponent' || // TODO: receiveComponent()?
+ fnName === '_renderValidatedComponent')) {
+
+ if (this._currentElement.type === ReactMount.TopLevelWrapper) {
+ return func.apply(this, args);
+ }
+
+ var rootNodeID = fnName === 'mountComponent' ? args[0] : this._rootNodeID;
+ var isRender = fnName === '_renderValidatedComponent';
+ var isMount = fnName === 'mountComponent';
+
+ var mountStack = ReactDefaultPerf._mountStack;
+ var entry = ReactDefaultPerf._allMeasurements[ReactDefaultPerf._allMeasurements.length - 1];
+
+ if (isRender) {
+ addValue(entry.counts, rootNodeID, 1);
+ } else if (isMount) {
+ entry.created[rootNodeID] = true;
+ mountStack.push(0);
+ }
+
+ start = performanceNow();
+ rv = func.apply(this, args);
+ totalTime = performanceNow() - start;
+
+ if (isRender) {
+ addValue(entry.render, rootNodeID, totalTime);
+ } else if (isMount) {
+ var subMountTime = mountStack.pop();
+ mountStack[mountStack.length - 1] += totalTime;
+ addValue(entry.exclusive, rootNodeID, totalTime - subMountTime);
+ addValue(entry.inclusive, rootNodeID, totalTime);
+ } else {
+ addValue(entry.inclusive, rootNodeID, totalTime);
+ }
+
+ entry.displayNames[rootNodeID] = {
+ current: this.getName(),
+ owner: this._currentElement._owner ? this._currentElement._owner.getName() : '<root>'
+ };
+
+ return rv;
+ } else {
+ return func.apply(this, args);
+ }
+ };
+ }
+};
+
+module.exports = ReactDefaultPerf;
+},{"10":10,"170":170,"56":56,"72":72,"78":78}],56:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDefaultPerfAnalysis
+ */
+
+'use strict';
+
+var assign = _dereq_(24);
+
+// Don't try to save users less than 1.2ms (a number I made up)
+var DONT_CARE_THRESHOLD = 1.2;
+var DOM_OPERATION_TYPES = {
+ '_mountImageIntoNode': 'set innerHTML',
+ INSERT_MARKUP: 'set innerHTML',
+ MOVE_EXISTING: 'move',
+ REMOVE_NODE: 'remove',
+ SET_MARKUP: 'set innerHTML',
+ TEXT_CONTENT: 'set textContent',
+ 'setValueForProperty': 'update attribute',
+ 'setValueForAttribute': 'update attribute',
+ 'deleteValueForProperty': 'remove attribute',
+ 'setValueForStyles': 'update styles',
+ 'replaceNodeWithMarkup': 'replace',
+ 'updateTextContent': 'set textContent'
+};
+
+function getTotalTime(measurements) {
+ // TODO: return number of DOM ops? could be misleading.
+ // TODO: measure dropped frames after reconcile?
+ // TODO: log total time of each reconcile and the top-level component
+ // class that triggered it.
+ var totalTime = 0;
+ for (var i = 0; i < measurements.length; i++) {
+ var measurement = measurements[i];
+ totalTime += measurement.totalTime;
+ }
+ return totalTime;
+}
+
+function getDOMSummary(measurements) {
+ var items = [];
+ measurements.forEach(function (measurement) {
+ Object.keys(measurement.writes).forEach(function (id) {
+ measurement.writes[id].forEach(function (write) {
+ items.push({
+ id: id,
+ type: DOM_OPERATION_TYPES[write.type] || write.type,
+ args: write.args
+ });
+ });
+ });
+ });
+ return items;
+}
+
+function getExclusiveSummary(measurements) {
+ var candidates = {};
+ var displayName;
+
+ for (var i = 0; i < measurements.length; i++) {
+ var measurement = measurements[i];
+ var allIDs = assign({}, measurement.exclusive, measurement.inclusive);
+
+ for (var id in allIDs) {
+ displayName = measurement.displayNames[id].current;
+
+ candidates[displayName] = candidates[displayName] || {
+ componentName: displayName,
+ inclusive: 0,
+ exclusive: 0,
+ render: 0,
+ count: 0
+ };
+ if (measurement.render[id]) {
+ candidates[displayName].render += measurement.render[id];
+ }
+ if (measurement.exclusive[id]) {
+ candidates[displayName].exclusive += measurement.exclusive[id];
+ }
+ if (measurement.inclusive[id]) {
+ candidates[displayName].inclusive += measurement.inclusive[id];
+ }
+ if (measurement.counts[id]) {
+ candidates[displayName].count += measurement.counts[id];
+ }
+ }
+ }
+
+ // Now make a sorted array with the results.
+ var arr = [];
+ for (displayName in candidates) {
+ if (candidates[displayName].exclusive >= DONT_CARE_THRESHOLD) {
+ arr.push(candidates[displayName]);
+ }
+ }
+
+ arr.sort(function (a, b) {
+ return b.exclusive - a.exclusive;
+ });
+
+ return arr;
+}
+
+function getInclusiveSummary(measurements, onlyClean) {
+ var candidates = {};
+ var inclusiveKey;
+
+ for (var i = 0; i < measurements.length; i++) {
+ var measurement = measurements[i];
+ var allIDs = assign({}, measurement.exclusive, measurement.inclusive);
+ var cleanComponents;
+
+ if (onlyClean) {
+ cleanComponents = getUnchangedComponents(measurement);
+ }
+
+ for (var id in allIDs) {
+ if (onlyClean && !cleanComponents[id]) {
+ continue;
+ }
+
+ var displayName = measurement.displayNames[id];
+
+ // Inclusive time is not useful for many components without knowing where
+ // they are instantiated. So we aggregate inclusive time with both the
+ // owner and current displayName as the key.
+ inclusiveKey = displayName.owner + ' > ' + displayName.current;
+
+ candidates[inclusiveKey] = candidates[inclusiveKey] || {
+ componentName: inclusiveKey,
+ time: 0,
+ count: 0
+ };
+
+ if (measurement.inclusive[id]) {
+ candidates[inclusiveKey].time += measurement.inclusive[id];
+ }
+ if (measurement.counts[id]) {
+ candidates[inclusiveKey].count += measurement.counts[id];
+ }
+ }
+ }
+
+ // Now make a sorted array with the results.
+ var arr = [];
+ for (inclusiveKey in candidates) {
+ if (candidates[inclusiveKey].time >= DONT_CARE_THRESHOLD) {
+ arr.push(candidates[inclusiveKey]);
+ }
+ }
+
+ arr.sort(function (a, b) {
+ return b.time - a.time;
+ });
+
+ return arr;
+}
+
+function getUnchangedComponents(measurement) {
+ // For a given reconcile, look at which components did not actually
+ // render anything to the DOM and return a mapping of their ID to
+ // the amount of time it took to render the entire subtree.
+ var cleanComponents = {};
+ var dirtyLeafIDs = Object.keys(measurement.writes);
+ var allIDs = assign({}, measurement.exclusive, measurement.inclusive);
+
+ for (var id in allIDs) {
+ var isDirty = false;
+ // For each component that rendered, see if a component that triggered
+ // a DOM op is in its subtree.
+ for (var i = 0; i < dirtyLeafIDs.length; i++) {
+ if (dirtyLeafIDs[i].indexOf(id) === 0) {
+ isDirty = true;
+ break;
+ }
+ }
+ // check if component newly created
+ if (measurement.created[id]) {
+ isDirty = true;
+ }
+ if (!isDirty && measurement.counts[id] > 0) {
+ cleanComponents[id] = true;
+ }
+ }
+ return cleanComponents;
+}
+
+var ReactDefaultPerfAnalysis = {
+ getExclusiveSummary: getExclusiveSummary,
+ getInclusiveSummary: getInclusiveSummary,
+ getDOMSummary: getDOMSummary,
+ getTotalTime: getTotalTime
+};
+
+module.exports = ReactDefaultPerfAnalysis;
+},{"24":24}],57:[function(_dereq_,module,exports){
+/**
+ * Copyright 2014-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactElement
+ */
+
+'use strict';
+
+var ReactCurrentOwner = _dereq_(39);
+
+var assign = _dereq_(24);
+var canDefineProperty = _dereq_(117);
+
+// The Symbol used to tag the ReactElement type. If there is no native Symbol
+// nor polyfill, then a plain number is used for performance.
+var REACT_ELEMENT_TYPE = typeof Symbol === 'function' && Symbol['for'] && Symbol['for']('react.element') || 0xeac7;
+
+var RESERVED_PROPS = {
+ key: true,
+ ref: true,
+ __self: true,
+ __source: true
+};
+
+/**
+ * Base constructor for all React elements. This is only used to make this
+ * work with a dynamic instanceof check. Nothing should live on this prototype.
+ *
+ * @param {*} type
+ * @param {*} key
+ * @param {string|object} ref
+ * @param {*} self A *temporary* helper to detect places where `this` is
+ * different from the `owner` when React.createElement is called, so that we
+ * can warn. We want to get rid of owner and replace string `ref`s with arrow
+ * functions, and as long as `this` and owner are the same, there will be no
+ * change in behavior.
+ * @param {*} source An annotation object (added by a transpiler or otherwise)
+ * indicating filename, line number, and/or other information.
+ * @param {*} owner
+ * @param {*} props
+ * @internal
+ */
+var ReactElement = function (type, key, ref, self, source, owner, props) {
+ var element = {
+ // This tag allow us to uniquely identify this as a React Element
+ $$typeof: REACT_ELEMENT_TYPE,
+
+ // Built-in properties that belong on the element
+ type: type,
+ key: key,
+ ref: ref,
+ props: props,
+
+ // Record the component responsible for creating this element.
+ _owner: owner
+ };
+
+ if ("production" !== 'production') {
+ // The validation flag is currently mutative. We put it on
+ // an external backing store so that we can freeze the whole object.
+ // This can be replaced with a WeakMap once they are implemented in
+ // commonly used development environments.
+ element._store = {};
+
+ // To make comparing ReactElements easier for testing purposes, we make
+ // the validation flag non-enumerable (where possible, which should
+ // include every environment we run tests in), so the test framework
+ // ignores it.
+ if (canDefineProperty) {
+ Object.defineProperty(element._store, 'validated', {
+ configurable: false,
+ enumerable: false,
+ writable: true,
+ value: false
+ });
+ // self and source are DEV only properties.
+ Object.defineProperty(element, '_self', {
+ configurable: false,
+ enumerable: false,
+ writable: false,
+ value: self
+ });
+ // Two elements created in two different places should be considered
+ // equal for testing purposes and therefore we hide it from enumeration.
+ Object.defineProperty(element, '_source', {
+ configurable: false,
+ enumerable: false,
+ writable: false,
+ value: source
+ });
+ } else {
+ element._store.validated = false;
+ element._self = self;
+ element._source = source;
+ }
+ Object.freeze(element.props);
+ Object.freeze(element);
+ }
+
+ return element;
+};
+
+ReactElement.createElement = function (type, config, children) {
+ var propName;
+
+ // Reserved names are extracted
+ var props = {};
+
+ var key = null;
+ var ref = null;
+ var self = null;
+ var source = null;
+
+ if (config != null) {
+ ref = config.ref === undefined ? null : config.ref;
+ key = config.key === undefined ? null : '' + config.key;
+ self = config.__self === undefined ? null : config.__self;
+ source = config.__source === undefined ? null : config.__source;
+ // Remaining properties are added to a new props object
+ for (propName in config) {
+ if (config.hasOwnProperty(propName) && !RESERVED_PROPS.hasOwnProperty(propName)) {
+ props[propName] = config[propName];
+ }
+ }
+ }
+
+ // Children can be more than one argument, and those are transferred onto
+ // the newly allocated props object.
+ var childrenLength = arguments.length - 2;
+ if (childrenLength === 1) {
+ props.children = children;
+ } else if (childrenLength > 1) {
+ var childArray = Array(childrenLength);
+ for (var i = 0; i < childrenLength; i++) {
+ childArray[i] = arguments[i + 2];
+ }
+ props.children = childArray;
+ }
+
+ // Resolve default props
+ if (type && type.defaultProps) {
+ var defaultProps = type.defaultProps;
+ for (propName in defaultProps) {
+ if (typeof props[propName] === 'undefined') {
+ props[propName] = defaultProps[propName];
+ }
+ }
+ }
+
+ return ReactElement(type, key, ref, self, source, ReactCurrentOwner.current, props);
+};
+
+ReactElement.createFactory = function (type) {
+ var factory = ReactElement.createElement.bind(null, type);
+ // Expose the type on the factory and the prototype so that it can be
+ // easily accessed on elements. E.g. `<Foo />.type === Foo`.
+ // This should not be named `constructor` since this may not be the function
+ // that created the element, and it may not even be a constructor.
+ // Legacy hook TODO: Warn if this is accessed
+ factory.type = type;
+ return factory;
+};
+
+ReactElement.cloneAndReplaceKey = function (oldElement, newKey) {
+ var newElement = ReactElement(oldElement.type, newKey, oldElement.ref, oldElement._self, oldElement._source, oldElement._owner, oldElement.props);
+
+ return newElement;
+};
+
+ReactElement.cloneAndReplaceProps = function (oldElement, newProps) {
+ var newElement = ReactElement(oldElement.type, oldElement.key, oldElement.ref, oldElement._self, oldElement._source, oldElement._owner, newProps);
+
+ if ("production" !== 'production') {
+ // If the key on the original is valid, then the clone is valid
+ newElement._store.validated = oldElement._store.validated;
+ }
+
+ return newElement;
+};
+
+ReactElement.cloneElement = function (element, config, children) {
+ var propName;
+
+ // Original props are copied
+ var props = assign({}, element.props);
+
+ // Reserved names are extracted
+ var key = element.key;
+ var ref = element.ref;
+ // Self is preserved since the owner is preserved.
+ var self = element._self;
+ // Source is preserved since cloneElement is unlikely to be targeted by a
+ // transpiler, and the original source is probably a better indicator of the
+ // true owner.
+ var source = element._source;
+
+ // Owner will be preserved, unless ref is overridden
+ var owner = element._owner;
+
+ if (config != null) {
+ if (config.ref !== undefined) {
+ // Silently steal the ref from the parent.
+ ref = config.ref;
+ owner = ReactCurrentOwner.current;
+ }
+ if (config.key !== undefined) {
+ key = '' + config.key;
+ }
+ // Remaining properties override existing props
+ for (propName in config) {
+ if (config.hasOwnProperty(propName) && !RESERVED_PROPS.hasOwnProperty(propName)) {
+ props[propName] = config[propName];
+ }
+ }
+ }
+
+ // Children can be more than one argument, and those are transferred onto
+ // the newly allocated props object.
+ var childrenLength = arguments.length - 2;
+ if (childrenLength === 1) {
+ props.children = children;
+ } else if (childrenLength > 1) {
+ var childArray = Array(childrenLength);
+ for (var i = 0; i < childrenLength; i++) {
+ childArray[i] = arguments[i + 2];
+ }
+ props.children = childArray;
+ }
+
+ return ReactElement(element.type, key, ref, self, source, owner, props);
+};
+
+/**
+ * @param {?object} object
+ * @return {boolean} True if `object` is a valid component.
+ * @final
+ */
+ReactElement.isValidElement = function (object) {
+ return typeof object === 'object' && object !== null && object.$$typeof === REACT_ELEMENT_TYPE;
+};
+
+module.exports = ReactElement;
+},{"117":117,"24":24,"39":39}],58:[function(_dereq_,module,exports){
+/**
+ * Copyright 2014-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactElementValidator
+ */
+
+/**
+ * ReactElementValidator provides a wrapper around a element factory
+ * which validates the props passed to the element. This is intended to be
+ * used only in DEV and could be replaced by a static type checker for languages
+ * that support it.
+ */
+
+'use strict';
+
+var ReactElement = _dereq_(57);
+var ReactPropTypeLocations = _dereq_(81);
+var ReactPropTypeLocationNames = _dereq_(80);
+var ReactCurrentOwner = _dereq_(39);
+
+var canDefineProperty = _dereq_(117);
+var getIteratorFn = _dereq_(129);
+var invariant = _dereq_(161);
+var warning = _dereq_(173);
+
+function getDeclarationErrorAddendum() {
+ if (ReactCurrentOwner.current) {
+ var name = ReactCurrentOwner.current.getName();
+ if (name) {
+ return ' Check the render method of `' + name + '`.';
+ }
+ }
+ return '';
+}
+
+/**
+ * Warn if there's no key explicitly set on dynamic arrays of children or
+ * object keys are not valid. This allows us to keep track of children between
+ * updates.
+ */
+var ownerHasKeyUseWarning = {};
+
+var loggedTypeFailures = {};
+
+/**
+ * Warn if the element doesn't have an explicit key assigned to it.
+ * This element is in an array. The array could grow and shrink or be
+ * reordered. All children that haven't already been validated are required to
+ * have a "key" property assigned to it.
+ *
+ * @internal
+ * @param {ReactElement} element Element that requires a key.
+ * @param {*} parentType element's parent's type.
+ */
+function validateExplicitKey(element, parentType) {
+ if (!element._store || element._store.validated || element.key != null) {
+ return;
+ }
+ element._store.validated = true;
+
+ var addenda = getAddendaForKeyUse('uniqueKey', element, parentType);
+ if (addenda === null) {
+ // we already showed the warning
+ return;
+ }
+ "production" !== 'production' ? warning(false, 'Each child in an array or iterator should have a unique "key" prop.' + '%s%s%s', addenda.parentOrOwner || '', addenda.childOwner || '', addenda.url || '') : undefined;
+}
+
+/**
+ * Shared warning and monitoring code for the key warnings.
+ *
+ * @internal
+ * @param {string} messageType A key used for de-duping warnings.
+ * @param {ReactElement} element Component that requires a key.
+ * @param {*} parentType element's parent's type.
+ * @returns {?object} A set of addenda to use in the warning message, or null
+ * if the warning has already been shown before (and shouldn't be shown again).
+ */
+function getAddendaForKeyUse(messageType, element, parentType) {
+ var addendum = getDeclarationErrorAddendum();
+ if (!addendum) {
+ var parentName = typeof parentType === 'string' ? parentType : parentType.displayName || parentType.name;
+ if (parentName) {
+ addendum = ' Check the top-level render call using <' + parentName + '>.';
+ }
+ }
+
+ var memoizer = ownerHasKeyUseWarning[messageType] || (ownerHasKeyUseWarning[messageType] = {});
+ if (memoizer[addendum]) {
+ return null;
+ }
+ memoizer[addendum] = true;
+
+ var addenda = {
+ parentOrOwner: addendum,
+ url: ' See https://fb.me/react-warning-keys for more information.',
+ childOwner: null
+ };
+
+ // Usually the current owner is the offender, but if it accepts children as a
+ // property, it may be the creator of the child that's responsible for
+ // assigning it a key.
+ if (element && element._owner && element._owner !== ReactCurrentOwner.current) {
+ // Give the component that originally created this child.
+ addenda.childOwner = ' It was passed a child from ' + element._owner.getName() + '.';
+ }
+
+ return addenda;
+}
+
+/**
+ * Ensure that every element either is passed in a static location, in an
+ * array with an explicit keys property defined, or in an object literal
+ * with valid key property.
+ *
+ * @internal
+ * @param {ReactNode} node Statically passed child of any type.
+ * @param {*} parentType node's parent's type.
+ */
+function validateChildKeys(node, parentType) {
+ if (typeof node !== 'object') {
+ return;
+ }
+ if (Array.isArray(node)) {
+ for (var i = 0; i < node.length; i++) {
+ var child = node[i];
+ if (ReactElement.isValidElement(child)) {
+ validateExplicitKey(child, parentType);
+ }
+ }
+ } else if (ReactElement.isValidElement(node)) {
+ // This element was passed in a valid location.
+ if (node._store) {
+ node._store.validated = true;
+ }
+ } else if (node) {
+ var iteratorFn = getIteratorFn(node);
+ // Entry iterators provide implicit keys.
+ if (iteratorFn) {
+ if (iteratorFn !== node.entries) {
+ var iterator = iteratorFn.call(node);
+ var step;
+ while (!(step = iterator.next()).done) {
+ if (ReactElement.isValidElement(step.value)) {
+ validateExplicitKey(step.value, parentType);
+ }
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Assert that the props are valid
+ *
+ * @param {string} componentName Name of the component for error messages.
+ * @param {object} propTypes Map of prop name to a ReactPropType
+ * @param {object} props
+ * @param {string} location e.g. "prop", "context", "child context"
+ * @private
+ */
+function checkPropTypes(componentName, propTypes, props, location) {
+ for (var propName in propTypes) {
+ if (propTypes.hasOwnProperty(propName)) {
+ var error;
+ // Prop type validation may throw. In case they do, we don't want to
+ // fail the render phase where it didn't fail before. So we log it.
+ // After these have been cleaned up, we'll let them throw.
+ try {
+ // This is intentionally an invariant that gets caught. It's the same
+ // behavior as without this statement except with a better message.
+ !(typeof propTypes[propName] === 'function') ? "production" !== 'production' ? invariant(false, '%s: %s type `%s` is invalid; it must be a function, usually from ' + 'React.PropTypes.', componentName || 'React class', ReactPropTypeLocationNames[location], propName) : invariant(false) : undefined;
+ error = propTypes[propName](props, propName, componentName, location);
+ } catch (ex) {
+ error = ex;
+ }
+ "production" !== 'production' ? warning(!error || error instanceof Error, '%s: type specification of %s `%s` is invalid; the type checker ' + 'function must return `null` or an `Error` but returned a %s. ' + 'You may have forgotten to pass an argument to the type checker ' + 'creator (arrayOf, instanceOf, objectOf, oneOf, oneOfType, and ' + 'shape all require an argument).', componentName || 'React class', ReactPropTypeLocationNames[location], propName, typeof error) : undefined;
+ if (error instanceof Error && !(error.message in loggedTypeFailures)) {
+ // Only monitor this failure once because there tends to be a lot of the
+ // same error.
+ loggedTypeFailures[error.message] = true;
+
+ var addendum = getDeclarationErrorAddendum();
+ "production" !== 'production' ? warning(false, 'Failed propType: %s%s', error.message, addendum) : undefined;
+ }
+ }
+ }
+}
+
+/**
+ * Given an element, validate that its props follow the propTypes definition,
+ * provided by the type.
+ *
+ * @param {ReactElement} element
+ */
+function validatePropTypes(element) {
+ var componentClass = element.type;
+ if (typeof componentClass !== 'function') {
+ return;
+ }
+ var name = componentClass.displayName || componentClass.name;
+ if (componentClass.propTypes) {
+ checkPropTypes(name, componentClass.propTypes, element.props, ReactPropTypeLocations.prop);
+ }
+ if (typeof componentClass.getDefaultProps === 'function') {
+ "production" !== 'production' ? warning(componentClass.getDefaultProps.isReactClassApproved, 'getDefaultProps is only used on classic React.createClass ' + 'definitions. Use a static property named `defaultProps` instead.') : undefined;
+ }
+}
+
+var ReactElementValidator = {
+
+ createElement: function (type, props, children) {
+ var validType = typeof type === 'string' || typeof type === 'function';
+ // We warn in this case but don't throw. We expect the element creation to
+ // succeed and there will likely be errors in render.
+ "production" !== 'production' ? warning(validType, 'React.createElement: type should not be null, undefined, boolean, or ' + 'number. It should be a string (for DOM elements) or a ReactClass ' + '(for composite components).%s', getDeclarationErrorAddendum()) : undefined;
+
+ var element = ReactElement.createElement.apply(this, arguments);
+
+ // The result can be nullish if a mock or a custom function is used.
+ // TODO: Drop this when these are no longer allowed as the type argument.
+ if (element == null) {
+ return element;
+ }
+
+ // Skip key warning if the type isn't valid since our key validation logic
+ // doesn't expect a non-string/function type and can throw confusing errors.
+ // We don't want exception behavior to differ between dev and prod.
+ // (Rendering will throw with a helpful message and as soon as the type is
+ // fixed, the key warnings will appear.)
+ if (validType) {
+ for (var i = 2; i < arguments.length; i++) {
+ validateChildKeys(arguments[i], type);
+ }
+ }
+
+ validatePropTypes(element);
+
+ return element;
+ },
+
+ createFactory: function (type) {
+ var validatedFactory = ReactElementValidator.createElement.bind(null, type);
+ // Legacy hook TODO: Warn if this is accessed
+ validatedFactory.type = type;
+
+ if ("production" !== 'production') {
+ if (canDefineProperty) {
+ Object.defineProperty(validatedFactory, 'type', {
+ enumerable: false,
+ get: function () {
+ "production" !== 'production' ? warning(false, 'Factory.type is deprecated. Access the class directly ' + 'before passing it to createFactory.') : undefined;
+ Object.defineProperty(this, 'type', {
+ value: type
+ });
+ return type;
+ }
+ });
+ }
+ }
+
+ return validatedFactory;
+ },
+
+ cloneElement: function (element, props, children) {
+ var newElement = ReactElement.cloneElement.apply(this, arguments);
+ for (var i = 2; i < arguments.length; i++) {
+ validateChildKeys(arguments[i], newElement.type);
+ }
+ validatePropTypes(newElement);
+ return newElement;
+ }
+
+};
+
+module.exports = ReactElementValidator;
+},{"117":117,"129":129,"161":161,"173":173,"39":39,"57":57,"80":80,"81":81}],59:[function(_dereq_,module,exports){
+/**
+ * Copyright 2014-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactEmptyComponent
+ */
+
+'use strict';
+
+var ReactElement = _dereq_(57);
+var ReactEmptyComponentRegistry = _dereq_(60);
+var ReactReconciler = _dereq_(84);
+
+var assign = _dereq_(24);
+
+var placeholderElement;
+
+var ReactEmptyComponentInjection = {
+ injectEmptyComponent: function (component) {
+ placeholderElement = ReactElement.createElement(component);
+ }
+};
+
+var ReactEmptyComponent = function (instantiate) {
+ this._currentElement = null;
+ this._rootNodeID = null;
+ this._renderedComponent = instantiate(placeholderElement);
+};
+assign(ReactEmptyComponent.prototype, {
+ construct: function (element) {},
+ mountComponent: function (rootID, transaction, context) {
+ ReactEmptyComponentRegistry.registerNullComponentID(rootID);
+ this._rootNodeID = rootID;
+ return ReactReconciler.mountComponent(this._renderedComponent, rootID, transaction, context);
+ },
+ receiveComponent: function () {},
+ unmountComponent: function (rootID, transaction, context) {
+ ReactReconciler.unmountComponent(this._renderedComponent);
+ ReactEmptyComponentRegistry.deregisterNullComponentID(this._rootNodeID);
+ this._rootNodeID = null;
+ this._renderedComponent = null;
+ }
+});
+
+ReactEmptyComponent.injection = ReactEmptyComponentInjection;
+
+module.exports = ReactEmptyComponent;
+},{"24":24,"57":57,"60":60,"84":84}],60:[function(_dereq_,module,exports){
+/**
+ * Copyright 2014-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactEmptyComponentRegistry
+ */
+
+'use strict';
+
+// This registry keeps track of the React IDs of the components that rendered to
+// `null` (in reality a placeholder such as `noscript`)
+var nullComponentIDsRegistry = {};
+
+/**
+ * @param {string} id Component's `_rootNodeID`.
+ * @return {boolean} True if the component is rendered to null.
+ */
+function isNullComponentID(id) {
+ return !!nullComponentIDsRegistry[id];
+}
+
+/**
+ * Mark the component as having rendered to null.
+ * @param {string} id Component's `_rootNodeID`.
+ */
+function registerNullComponentID(id) {
+ nullComponentIDsRegistry[id] = true;
+}
+
+/**
+ * Unmark the component as having rendered to null: it renders to something now.
+ * @param {string} id Component's `_rootNodeID`.
+ */
+function deregisterNullComponentID(id) {
+ delete nullComponentIDsRegistry[id];
+}
+
+var ReactEmptyComponentRegistry = {
+ isNullComponentID: isNullComponentID,
+ registerNullComponentID: registerNullComponentID,
+ deregisterNullComponentID: deregisterNullComponentID
+};
+
+module.exports = ReactEmptyComponentRegistry;
+},{}],61:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactErrorUtils
+ * @typechecks
+ */
+
+'use strict';
+
+var caughtError = null;
+
+/**
+ * Call a function while guarding against errors that happens within it.
+ *
+ * @param {?String} name of the guard to use for logging or debugging
+ * @param {Function} func The function to invoke
+ * @param {*} a First argument
+ * @param {*} b Second argument
+ */
+function invokeGuardedCallback(name, func, a, b) {
+ try {
+ return func(a, b);
+ } catch (x) {
+ if (caughtError === null) {
+ caughtError = x;
+ }
+ return undefined;
+ }
+}
+
+var ReactErrorUtils = {
+ invokeGuardedCallback: invokeGuardedCallback,
+
+ /**
+ * Invoked by ReactTestUtils.Simulate so that any errors thrown by the event
+ * handler are sure to be rethrown by rethrowCaughtError.
+ */
+ invokeGuardedCallbackWithCatch: invokeGuardedCallback,
+
+ /**
+ * During execution of guarded functions we will capture the first error which
+ * we will rethrow to be handled by the top level error handler.
+ */
+ rethrowCaughtError: function () {
+ if (caughtError) {
+ var error = caughtError;
+ caughtError = null;
+ throw error;
+ }
+ }
+};
+
+if ("production" !== 'production') {
+ /**
+ * To help development we can get better devtools integration by simulating a
+ * real browser event.
+ */
+ if (typeof window !== 'undefined' && typeof window.dispatchEvent === 'function' && typeof document !== 'undefined' && typeof document.createEvent === 'function') {
+ var fakeNode = document.createElementNS('http://www.w3.org/1999/xhtml', 'react');
+ ReactErrorUtils.invokeGuardedCallback = function (name, func, a, b) {
+ var boundFunc = func.bind(null, a, b);
+ var evtType = 'react-' + name;
+ fakeNode.addEventListener(evtType, boundFunc, false);
+ var evt = document.createEvent('Event');
+ evt.initEvent(evtType, false, false);
+ fakeNode.dispatchEvent(evt);
+ fakeNode.removeEventListener(evtType, boundFunc, false);
+ };
+ }
+}
+
+module.exports = ReactErrorUtils;
+},{}],62:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactEventEmitterMixin
+ */
+
+'use strict';
+
+var EventPluginHub = _dereq_(16);
+
+function runEventQueueInBatch(events) {
+ EventPluginHub.enqueueEvents(events);
+ EventPluginHub.processEventQueue(false);
+}
+
+var ReactEventEmitterMixin = {
+
+ /**
+ * Streams a fired top-level event to `EventPluginHub` where plugins have the
+ * opportunity to create `ReactEvent`s to be dispatched.
+ *
+ * @param {string} topLevelType Record from `EventConstants`.
+ * @param {object} topLevelTarget The listening component root node.
+ * @param {string} topLevelTargetID ID of `topLevelTarget`.
+ * @param {object} nativeEvent Native environment event.
+ */
+ handleTopLevel: function (topLevelType, topLevelTarget, topLevelTargetID, nativeEvent, nativeEventTarget) {
+ var events = EventPluginHub.extractEvents(topLevelType, topLevelTarget, topLevelTargetID, nativeEvent, nativeEventTarget);
+ runEventQueueInBatch(events);
+ }
+};
+
+module.exports = ReactEventEmitterMixin;
+},{"16":16}],63:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactEventListener
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var EventListener = _dereq_(146);
+var ExecutionEnvironment = _dereq_(147);
+var PooledClass = _dereq_(25);
+var ReactInstanceHandles = _dereq_(67);
+var ReactMount = _dereq_(72);
+var ReactUpdates = _dereq_(96);
+
+var assign = _dereq_(24);
+var getEventTarget = _dereq_(128);
+var getUnboundedScrollPosition = _dereq_(158);
+
+var DOCUMENT_FRAGMENT_NODE_TYPE = 11;
+
+/**
+ * Finds the parent React component of `node`.
+ *
+ * @param {*} node
+ * @return {?DOMEventTarget} Parent container, or `null` if the specified node
+ * is not nested.
+ */
+function findParent(node) {
+ // TODO: It may be a good idea to cache this to prevent unnecessary DOM
+ // traversal, but caching is difficult to do correctly without using a
+ // mutation observer to listen for all DOM changes.
+ var nodeID = ReactMount.getID(node);
+ var rootID = ReactInstanceHandles.getReactRootIDFromNodeID(nodeID);
+ var container = ReactMount.findReactContainerForID(rootID);
+ var parent = ReactMount.getFirstReactDOM(container);
+ return parent;
+}
+
+// Used to store ancestor hierarchy in top level callback
+function TopLevelCallbackBookKeeping(topLevelType, nativeEvent) {
+ this.topLevelType = topLevelType;
+ this.nativeEvent = nativeEvent;
+ this.ancestors = [];
+}
+assign(TopLevelCallbackBookKeeping.prototype, {
+ destructor: function () {
+ this.topLevelType = null;
+ this.nativeEvent = null;
+ this.ancestors.length = 0;
+ }
+});
+PooledClass.addPoolingTo(TopLevelCallbackBookKeeping, PooledClass.twoArgumentPooler);
+
+function handleTopLevelImpl(bookKeeping) {
+ // TODO: Re-enable event.path handling
+ //
+ // if (bookKeeping.nativeEvent.path && bookKeeping.nativeEvent.path.length > 1) {
+ // // New browsers have a path attribute on native events
+ // handleTopLevelWithPath(bookKeeping);
+ // } else {
+ // // Legacy browsers don't have a path attribute on native events
+ // handleTopLevelWithoutPath(bookKeeping);
+ // }
+
+ void handleTopLevelWithPath; // temporarily unused
+ handleTopLevelWithoutPath(bookKeeping);
+}
+
+// Legacy browsers don't have a path attribute on native events
+function handleTopLevelWithoutPath(bookKeeping) {
+ var topLevelTarget = ReactMount.getFirstReactDOM(getEventTarget(bookKeeping.nativeEvent)) || window;
+
+ // Loop through the hierarchy, in case there's any nested components.
+ // It's important that we build the array of ancestors before calling any
+ // event handlers, because event handlers can modify the DOM, leading to
+ // inconsistencies with ReactMount's node cache. See #1105.
+ var ancestor = topLevelTarget;
+ while (ancestor) {
+ bookKeeping.ancestors.push(ancestor);
+ ancestor = findParent(ancestor);
+ }
+
+ for (var i = 0; i < bookKeeping.ancestors.length; i++) {
+ topLevelTarget = bookKeeping.ancestors[i];
+ var topLevelTargetID = ReactMount.getID(topLevelTarget) || '';
+ ReactEventListener._handleTopLevel(bookKeeping.topLevelType, topLevelTarget, topLevelTargetID, bookKeeping.nativeEvent, getEventTarget(bookKeeping.nativeEvent));
+ }
+}
+
+// New browsers have a path attribute on native events
+function handleTopLevelWithPath(bookKeeping) {
+ var path = bookKeeping.nativeEvent.path;
+ var currentNativeTarget = path[0];
+ var eventsFired = 0;
+ for (var i = 0; i < path.length; i++) {
+ var currentPathElement = path[i];
+ if (currentPathElement.nodeType === DOCUMENT_FRAGMENT_NODE_TYPE) {
+ currentNativeTarget = path[i + 1];
+ }
+ // TODO: slow
+ var reactParent = ReactMount.getFirstReactDOM(currentPathElement);
+ if (reactParent === currentPathElement) {
+ var currentPathElementID = ReactMount.getID(currentPathElement);
+ var newRootID = ReactInstanceHandles.getReactRootIDFromNodeID(currentPathElementID);
+ bookKeeping.ancestors.push(currentPathElement);
+
+ var topLevelTargetID = ReactMount.getID(currentPathElement) || '';
+ eventsFired++;
+ ReactEventListener._handleTopLevel(bookKeeping.topLevelType, currentPathElement, topLevelTargetID, bookKeeping.nativeEvent, currentNativeTarget);
+
+ // Jump to the root of this React render tree
+ while (currentPathElementID !== newRootID) {
+ i++;
+ currentPathElement = path[i];
+ currentPathElementID = ReactMount.getID(currentPathElement);
+ }
+ }
+ }
+ if (eventsFired === 0) {
+ ReactEventListener._handleTopLevel(bookKeeping.topLevelType, window, '', bookKeeping.nativeEvent, getEventTarget(bookKeeping.nativeEvent));
+ }
+}
+
+function scrollValueMonitor(cb) {
+ var scrollPosition = getUnboundedScrollPosition(window);
+ cb(scrollPosition);
+}
+
+var ReactEventListener = {
+ _enabled: true,
+ _handleTopLevel: null,
+
+ WINDOW_HANDLE: ExecutionEnvironment.canUseDOM ? window : null,
+
+ setHandleTopLevel: function (handleTopLevel) {
+ ReactEventListener._handleTopLevel = handleTopLevel;
+ },
+
+ setEnabled: function (enabled) {
+ ReactEventListener._enabled = !!enabled;
+ },
+
+ isEnabled: function () {
+ return ReactEventListener._enabled;
+ },
+
+ /**
+ * Traps top-level events by using event bubbling.
+ *
+ * @param {string} topLevelType Record from `EventConstants`.
+ * @param {string} handlerBaseName Event name (e.g. "click").
+ * @param {object} handle Element on which to attach listener.
+ * @return {?object} An object with a remove function which will forcefully
+ * remove the listener.
+ * @internal
+ */
+ trapBubbledEvent: function (topLevelType, handlerBaseName, handle) {
+ var element = handle;
+ if (!element) {
+ return null;
+ }
+ return EventListener.listen(element, handlerBaseName, ReactEventListener.dispatchEvent.bind(null, topLevelType));
+ },
+
+ /**
+ * Traps a top-level event by using event capturing.
+ *
+ * @param {string} topLevelType Record from `EventConstants`.
+ * @param {string} handlerBaseName Event name (e.g. "click").
+ * @param {object} handle Element on which to attach listener.
+ * @return {?object} An object with a remove function which will forcefully
+ * remove the listener.
+ * @internal
+ */
+ trapCapturedEvent: function (topLevelType, handlerBaseName, handle) {
+ var element = handle;
+ if (!element) {
+ return null;
+ }
+ return EventListener.capture(element, handlerBaseName, ReactEventListener.dispatchEvent.bind(null, topLevelType));
+ },
+
+ monitorScrollValue: function (refresh) {
+ var callback = scrollValueMonitor.bind(null, refresh);
+ EventListener.listen(window, 'scroll', callback);
+ },
+
+ dispatchEvent: function (topLevelType, nativeEvent) {
+ if (!ReactEventListener._enabled) {
+ return;
+ }
+
+ var bookKeeping = TopLevelCallbackBookKeeping.getPooled(topLevelType, nativeEvent);
+ try {
+ // Event queue being processed in the same cycle allows
+ // `preventDefault`.
+ ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping);
+ } finally {
+ TopLevelCallbackBookKeeping.release(bookKeeping);
+ }
+ }
+};
+
+module.exports = ReactEventListener;
+},{"128":128,"146":146,"147":147,"158":158,"24":24,"25":25,"67":67,"72":72,"96":96}],64:[function(_dereq_,module,exports){
+/**
+ * Copyright 2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactFragment
+ */
+
+'use strict';
+
+var ReactChildren = _dereq_(32);
+var ReactElement = _dereq_(57);
+
+var emptyFunction = _dereq_(153);
+var invariant = _dereq_(161);
+var warning = _dereq_(173);
+
+/**
+ * We used to allow keyed objects to serve as a collection of ReactElements,
+ * or nested sets. This allowed us a way to explicitly key a set a fragment of
+ * components. This is now being replaced with an opaque data structure.
+ * The upgrade path is to call React.addons.createFragment({ key: value }) to
+ * create a keyed fragment. The resulting data structure is an array.
+ */
+
+var numericPropertyRegex = /^\d+$/;
+
+var warnedAboutNumeric = false;
+
+var ReactFragment = {
+ // Wrap a keyed object in an opaque proxy that warns you if you access any
+ // of its properties.
+ create: function (object) {
+ if (typeof object !== 'object' || !object || Array.isArray(object)) {
+ "production" !== 'production' ? warning(false, 'React.addons.createFragment only accepts a single object. Got: %s', object) : undefined;
+ return object;
+ }
+ if (ReactElement.isValidElement(object)) {
+ "production" !== 'production' ? warning(false, 'React.addons.createFragment does not accept a ReactElement ' + 'without a wrapper object.') : undefined;
+ return object;
+ }
+
+ !(object.nodeType !== 1) ? "production" !== 'production' ? invariant(false, 'React.addons.createFragment(...): Encountered an invalid child; DOM ' + 'elements are not valid children of React components.') : invariant(false) : undefined;
+
+ var result = [];
+
+ for (var key in object) {
+ if ("production" !== 'production') {
+ if (!warnedAboutNumeric && numericPropertyRegex.test(key)) {
+ "production" !== 'production' ? warning(false, 'React.addons.createFragment(...): Child objects should have ' + 'non-numeric keys so ordering is preserved.') : undefined;
+ warnedAboutNumeric = true;
+ }
+ }
+ ReactChildren.mapIntoWithKeyPrefixInternal(object[key], result, key, emptyFunction.thatReturnsArgument);
+ }
+
+ return result;
+ }
+};
+
+module.exports = ReactFragment;
+},{"153":153,"161":161,"173":173,"32":32,"57":57}],65:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactInjection
+ */
+
+'use strict';
+
+var DOMProperty = _dereq_(10);
+var EventPluginHub = _dereq_(16);
+var ReactComponentEnvironment = _dereq_(36);
+var ReactClass = _dereq_(33);
+var ReactEmptyComponent = _dereq_(59);
+var ReactBrowserEventEmitter = _dereq_(28);
+var ReactNativeComponent = _dereq_(75);
+var ReactPerf = _dereq_(78);
+var ReactRootIndex = _dereq_(86);
+var ReactUpdates = _dereq_(96);
+
+var ReactInjection = {
+ Component: ReactComponentEnvironment.injection,
+ Class: ReactClass.injection,
+ DOMProperty: DOMProperty.injection,
+ EmptyComponent: ReactEmptyComponent.injection,
+ EventPluginHub: EventPluginHub.injection,
+ EventEmitter: ReactBrowserEventEmitter.injection,
+ NativeComponent: ReactNativeComponent.injection,
+ Perf: ReactPerf.injection,
+ RootIndex: ReactRootIndex.injection,
+ Updates: ReactUpdates.injection
+};
+
+module.exports = ReactInjection;
+},{"10":10,"16":16,"28":28,"33":33,"36":36,"59":59,"75":75,"78":78,"86":86,"96":96}],66:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactInputSelection
+ */
+
+'use strict';
+
+var ReactDOMSelection = _dereq_(49);
+
+var containsNode = _dereq_(150);
+var focusNode = _dereq_(155);
+var getActiveElement = _dereq_(156);
+
+function isInDocument(node) {
+ return containsNode(document.documentElement, node);
+}
+
+/**
+ * @ReactInputSelection: React input selection module. Based on Selection.js,
+ * but modified to be suitable for react and has a couple of bug fixes (doesn't
+ * assume buttons have range selections allowed).
+ * Input selection module for React.
+ */
+var ReactInputSelection = {
+
+ hasSelectionCapabilities: function (elem) {
+ var nodeName = elem && elem.nodeName && elem.nodeName.toLowerCase();
+ return nodeName && (nodeName === 'input' && elem.type === 'text' || nodeName === 'textarea' || elem.contentEditable === 'true');
+ },
+
+ getSelectionInformation: function () {
+ var focusedElem = getActiveElement();
+ return {
+ focusedElem: focusedElem,
+ selectionRange: ReactInputSelection.hasSelectionCapabilities(focusedElem) ? ReactInputSelection.getSelection(focusedElem) : null
+ };
+ },
+
+ /**
+ * @restoreSelection: If any selection information was potentially lost,
+ * restore it. This is useful when performing operations that could remove dom
+ * nodes and place them back in, resulting in focus being lost.
+ */
+ restoreSelection: function (priorSelectionInformation) {
+ var curFocusedElem = getActiveElement();
+ var priorFocusedElem = priorSelectionInformation.focusedElem;
+ var priorSelectionRange = priorSelectionInformation.selectionRange;
+ if (curFocusedElem !== priorFocusedElem && isInDocument(priorFocusedElem)) {
+ if (ReactInputSelection.hasSelectionCapabilities(priorFocusedElem)) {
+ ReactInputSelection.setSelection(priorFocusedElem, priorSelectionRange);
+ }
+ focusNode(priorFocusedElem);
+ }
+ },
+
+ /**
+ * @getSelection: Gets the selection bounds of a focused textarea, input or
+ * contentEditable node.
+ * -@input: Look up selection bounds of this input
+ * -@return {start: selectionStart, end: selectionEnd}
+ */
+ getSelection: function (input) {
+ var selection;
+
+ if ('selectionStart' in input) {
+ // Modern browser with input or textarea.
+ selection = {
+ start: input.selectionStart,
+ end: input.selectionEnd
+ };
+ } else if (document.selection && (input.nodeName && input.nodeName.toLowerCase() === 'input')) {
+ // IE8 input.
+ var range = document.selection.createRange();
+ // There can only be one selection per document in IE, so it must
+ // be in our element.
+ if (range.parentElement() === input) {
+ selection = {
+ start: -range.moveStart('character', -input.value.length),
+ end: -range.moveEnd('character', -input.value.length)
+ };
+ }
+ } else {
+ // Content editable or old IE textarea.
+ selection = ReactDOMSelection.getOffsets(input);
+ }
+
+ return selection || { start: 0, end: 0 };
+ },
+
+ /**
+ * @setSelection: Sets the selection bounds of a textarea or input and focuses
+ * the input.
+ * -@input Set selection bounds of this input or textarea
+ * -@offsets Object of same form that is returned from get*
+ */
+ setSelection: function (input, offsets) {
+ var start = offsets.start;
+ var end = offsets.end;
+ if (typeof end === 'undefined') {
+ end = start;
+ }
+
+ if ('selectionStart' in input) {
+ input.selectionStart = start;
+ input.selectionEnd = Math.min(end, input.value.length);
+ } else if (document.selection && (input.nodeName && input.nodeName.toLowerCase() === 'input')) {
+ var range = input.createTextRange();
+ range.collapse(true);
+ range.moveStart('character', start);
+ range.moveEnd('character', end - start);
+ range.select();
+ } else {
+ ReactDOMSelection.setOffsets(input, offsets);
+ }
+ }
+};
+
+module.exports = ReactInputSelection;
+},{"150":150,"155":155,"156":156,"49":49}],67:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactInstanceHandles
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var ReactRootIndex = _dereq_(86);
+
+var invariant = _dereq_(161);
+
+var SEPARATOR = '.';
+var SEPARATOR_LENGTH = SEPARATOR.length;
+
+/**
+ * Maximum depth of traversals before we consider the possibility of a bad ID.
+ */
+var MAX_TREE_DEPTH = 10000;
+
+/**
+ * Creates a DOM ID prefix to use when mounting React components.
+ *
+ * @param {number} index A unique integer
+ * @return {string} React root ID.
+ * @internal
+ */
+function getReactRootIDString(index) {
+ return SEPARATOR + index.toString(36);
+}
+
+/**
+ * Checks if a character in the supplied ID is a separator or the end.
+ *
+ * @param {string} id A React DOM ID.
+ * @param {number} index Index of the character to check.
+ * @return {boolean} True if the character is a separator or end of the ID.
+ * @private
+ */
+function isBoundary(id, index) {
+ return id.charAt(index) === SEPARATOR || index === id.length;
+}
+
+/**
+ * Checks if the supplied string is a valid React DOM ID.
+ *
+ * @param {string} id A React DOM ID, maybe.
+ * @return {boolean} True if the string is a valid React DOM ID.
+ * @private
+ */
+function isValidID(id) {
+ return id === '' || id.charAt(0) === SEPARATOR && id.charAt(id.length - 1) !== SEPARATOR;
+}
+
+/**
+ * Checks if the first ID is an ancestor of or equal to the second ID.
+ *
+ * @param {string} ancestorID
+ * @param {string} descendantID
+ * @return {boolean} True if `ancestorID` is an ancestor of `descendantID`.
+ * @internal
+ */
+function isAncestorIDOf(ancestorID, descendantID) {
+ return descendantID.indexOf(ancestorID) === 0 && isBoundary(descendantID, ancestorID.length);
+}
+
+/**
+ * Gets the parent ID of the supplied React DOM ID, `id`.
+ *
+ * @param {string} id ID of a component.
+ * @return {string} ID of the parent, or an empty string.
+ * @private
+ */
+function getParentID(id) {
+ return id ? id.substr(0, id.lastIndexOf(SEPARATOR)) : '';
+}
+
+/**
+ * Gets the next DOM ID on the tree path from the supplied `ancestorID` to the
+ * supplied `destinationID`. If they are equal, the ID is returned.
+ *
+ * @param {string} ancestorID ID of an ancestor node of `destinationID`.
+ * @param {string} destinationID ID of the destination node.
+ * @return {string} Next ID on the path from `ancestorID` to `destinationID`.
+ * @private
+ */
+function getNextDescendantID(ancestorID, destinationID) {
+ !(isValidID(ancestorID) && isValidID(destinationID)) ? "production" !== 'production' ? invariant(false, 'getNextDescendantID(%s, %s): Received an invalid React DOM ID.', ancestorID, destinationID) : invariant(false) : undefined;
+ !isAncestorIDOf(ancestorID, destinationID) ? "production" !== 'production' ? invariant(false, 'getNextDescendantID(...): React has made an invalid assumption about ' + 'the DOM hierarchy. Expected `%s` to be an ancestor of `%s`.', ancestorID, destinationID) : invariant(false) : undefined;
+ if (ancestorID === destinationID) {
+ return ancestorID;
+ }
+ // Skip over the ancestor and the immediate separator. Traverse until we hit
+ // another separator or we reach the end of `destinationID`.
+ var start = ancestorID.length + SEPARATOR_LENGTH;
+ var i;
+ for (i = start; i < destinationID.length; i++) {
+ if (isBoundary(destinationID, i)) {
+ break;
+ }
+ }
+ return destinationID.substr(0, i);
+}
+
+/**
+ * Gets the nearest common ancestor ID of two IDs.
+ *
+ * Using this ID scheme, the nearest common ancestor ID is the longest common
+ * prefix of the two IDs that immediately preceded a "marker" in both strings.
+ *
+ * @param {string} oneID
+ * @param {string} twoID
+ * @return {string} Nearest common ancestor ID, or the empty string if none.
+ * @private
+ */
+function getFirstCommonAncestorID(oneID, twoID) {
+ var minLength = Math.min(oneID.length, twoID.length);
+ if (minLength === 0) {
+ return '';
+ }
+ var lastCommonMarkerIndex = 0;
+ // Use `<=` to traverse until the "EOL" of the shorter string.
+ for (var i = 0; i <= minLength; i++) {
+ if (isBoundary(oneID, i) && isBoundary(twoID, i)) {
+ lastCommonMarkerIndex = i;
+ } else if (oneID.charAt(i) !== twoID.charAt(i)) {
+ break;
+ }
+ }
+ var longestCommonID = oneID.substr(0, lastCommonMarkerIndex);
+ !isValidID(longestCommonID) ? "production" !== 'production' ? invariant(false, 'getFirstCommonAncestorID(%s, %s): Expected a valid React DOM ID: %s', oneID, twoID, longestCommonID) : invariant(false) : undefined;
+ return longestCommonID;
+}
+
+/**
+ * Traverses the parent path between two IDs (either up or down). The IDs must
+ * not be the same, and there must exist a parent path between them. If the
+ * callback returns `false`, traversal is stopped.
+ *
+ * @param {?string} start ID at which to start traversal.
+ * @param {?string} stop ID at which to end traversal.
+ * @param {function} cb Callback to invoke each ID with.
+ * @param {*} arg Argument to invoke the callback with.
+ * @param {?boolean} skipFirst Whether or not to skip the first node.
+ * @param {?boolean} skipLast Whether or not to skip the last node.
+ * @private
+ */
+function traverseParentPath(start, stop, cb, arg, skipFirst, skipLast) {
+ start = start || '';
+ stop = stop || '';
+ !(start !== stop) ? "production" !== 'production' ? invariant(false, 'traverseParentPath(...): Cannot traverse from and to the same ID, `%s`.', start) : invariant(false) : undefined;
+ var traverseUp = isAncestorIDOf(stop, start);
+ !(traverseUp || isAncestorIDOf(start, stop)) ? "production" !== 'production' ? invariant(false, 'traverseParentPath(%s, %s, ...): Cannot traverse from two IDs that do ' + 'not have a parent path.', start, stop) : invariant(false) : undefined;
+ // Traverse from `start` to `stop` one depth at a time.
+ var depth = 0;
+ var traverse = traverseUp ? getParentID : getNextDescendantID;
+ for (var id = start;; /* until break */id = traverse(id, stop)) {
+ var ret;
+ if ((!skipFirst || id !== start) && (!skipLast || id !== stop)) {
+ ret = cb(id, traverseUp, arg);
+ }
+ if (ret === false || id === stop) {
+ // Only break //after// visiting `stop`.
+ break;
+ }
+ !(depth++ < MAX_TREE_DEPTH) ? "production" !== 'production' ? invariant(false, 'traverseParentPath(%s, %s, ...): Detected an infinite loop while ' + 'traversing the React DOM ID tree. This may be due to malformed IDs: %s', start, stop, id) : invariant(false) : undefined;
+ }
+}
+
+/**
+ * Manages the IDs assigned to DOM representations of React components. This
+ * uses a specific scheme in order to traverse the DOM efficiently (e.g. in
+ * order to simulate events).
+ *
+ * @internal
+ */
+var ReactInstanceHandles = {
+
+ /**
+ * Constructs a React root ID
+ * @return {string} A React root ID.
+ */
+ createReactRootID: function () {
+ return getReactRootIDString(ReactRootIndex.createReactRootIndex());
+ },
+
+ /**
+ * Constructs a React ID by joining a root ID with a name.
+ *
+ * @param {string} rootID Root ID of a parent component.
+ * @param {string} name A component's name (as flattened children).
+ * @return {string} A React ID.
+ * @internal
+ */
+ createReactID: function (rootID, name) {
+ return rootID + name;
+ },
+
+ /**
+ * Gets the DOM ID of the React component that is the root of the tree that
+ * contains the React component with the supplied DOM ID.
+ *
+ * @param {string} id DOM ID of a React component.
+ * @return {?string} DOM ID of the React component that is the root.
+ * @internal
+ */
+ getReactRootIDFromNodeID: function (id) {
+ if (id && id.charAt(0) === SEPARATOR && id.length > 1) {
+ var index = id.indexOf(SEPARATOR, 1);
+ return index > -1 ? id.substr(0, index) : id;
+ }
+ return null;
+ },
+
+ /**
+ * Traverses the ID hierarchy and invokes the supplied `cb` on any IDs that
+ * should would receive a `mouseEnter` or `mouseLeave` event.
+ *
+ * NOTE: Does not invoke the callback on the nearest common ancestor because
+ * nothing "entered" or "left" that element.
+ *
+ * @param {string} leaveID ID being left.
+ * @param {string} enterID ID being entered.
+ * @param {function} cb Callback to invoke on each entered/left ID.
+ * @param {*} upArg Argument to invoke the callback with on left IDs.
+ * @param {*} downArg Argument to invoke the callback with on entered IDs.
+ * @internal
+ */
+ traverseEnterLeave: function (leaveID, enterID, cb, upArg, downArg) {
+ var ancestorID = getFirstCommonAncestorID(leaveID, enterID);
+ if (ancestorID !== leaveID) {
+ traverseParentPath(leaveID, ancestorID, cb, upArg, false, true);
+ }
+ if (ancestorID !== enterID) {
+ traverseParentPath(ancestorID, enterID, cb, downArg, true, false);
+ }
+ },
+
+ /**
+ * Simulates the traversal of a two-phase, capture/bubble event dispatch.
+ *
+ * NOTE: This traversal happens on IDs without touching the DOM.
+ *
+ * @param {string} targetID ID of the target node.
+ * @param {function} cb Callback to invoke.
+ * @param {*} arg Argument to invoke the callback with.
+ * @internal
+ */
+ traverseTwoPhase: function (targetID, cb, arg) {
+ if (targetID) {
+ traverseParentPath('', targetID, cb, arg, true, false);
+ traverseParentPath(targetID, '', cb, arg, false, true);
+ }
+ },
+
+ /**
+ * Same as `traverseTwoPhase` but skips the `targetID`.
+ */
+ traverseTwoPhaseSkipTarget: function (targetID, cb, arg) {
+ if (targetID) {
+ traverseParentPath('', targetID, cb, arg, true, true);
+ traverseParentPath(targetID, '', cb, arg, true, true);
+ }
+ },
+
+ /**
+ * Traverse a node ID, calling the supplied `cb` for each ancestor ID. For
+ * example, passing `.0.$row-0.1` would result in `cb` getting called
+ * with `.0`, `.0.$row-0`, and `.0.$row-0.1`.
+ *
+ * NOTE: This traversal happens on IDs without touching the DOM.
+ *
+ * @param {string} targetID ID of the target node.
+ * @param {function} cb Callback to invoke.
+ * @param {*} arg Argument to invoke the callback with.
+ * @internal
+ */
+ traverseAncestors: function (targetID, cb, arg) {
+ traverseParentPath('', targetID, cb, arg, true, false);
+ },
+
+ getFirstCommonAncestorID: getFirstCommonAncestorID,
+
+ /**
+ * Exposed for unit testing.
+ * @private
+ */
+ _getNextDescendantID: getNextDescendantID,
+
+ isAncestorIDOf: isAncestorIDOf,
+
+ SEPARATOR: SEPARATOR
+
+};
+
+module.exports = ReactInstanceHandles;
+},{"161":161,"86":86}],68:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactInstanceMap
+ */
+
+'use strict';
+
+/**
+ * `ReactInstanceMap` maintains a mapping from a public facing stateful
+ * instance (key) and the internal representation (value). This allows public
+ * methods to accept the user facing instance as an argument and map them back
+ * to internal methods.
+ */
+
+// TODO: Replace this with ES6: var ReactInstanceMap = new Map();
+var ReactInstanceMap = {
+
+ /**
+ * This API should be called `delete` but we'd have to make sure to always
+ * transform these to strings for IE support. When this transform is fully
+ * supported we can rename it.
+ */
+ remove: function (key) {
+ key._reactInternalInstance = undefined;
+ },
+
+ get: function (key) {
+ return key._reactInternalInstance;
+ },
+
+ has: function (key) {
+ return key._reactInternalInstance !== undefined;
+ },
+
+ set: function (key, value) {
+ key._reactInternalInstance = value;
+ }
+
+};
+
+module.exports = ReactInstanceMap;
+},{}],69:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactIsomorphic
+ */
+
+'use strict';
+
+var ReactChildren = _dereq_(32);
+var ReactComponent = _dereq_(34);
+var ReactClass = _dereq_(33);
+var ReactDOMFactories = _dereq_(43);
+var ReactElement = _dereq_(57);
+var ReactElementValidator = _dereq_(58);
+var ReactPropTypes = _dereq_(82);
+var ReactVersion = _dereq_(97);
+
+var assign = _dereq_(24);
+var onlyChild = _dereq_(135);
+
+var createElement = ReactElement.createElement;
+var createFactory = ReactElement.createFactory;
+var cloneElement = ReactElement.cloneElement;
+
+if ("production" !== 'production') {
+ createElement = ReactElementValidator.createElement;
+ createFactory = ReactElementValidator.createFactory;
+ cloneElement = ReactElementValidator.cloneElement;
+}
+
+var React = {
+
+ // Modern
+
+ Children: {
+ map: ReactChildren.map,
+ forEach: ReactChildren.forEach,
+ count: ReactChildren.count,
+ toArray: ReactChildren.toArray,
+ only: onlyChild
+ },
+
+ Component: ReactComponent,
+
+ createElement: createElement,
+ cloneElement: cloneElement,
+ isValidElement: ReactElement.isValidElement,
+
+ // Classic
+
+ PropTypes: ReactPropTypes,
+ createClass: ReactClass.createClass,
+ createFactory: createFactory,
+ createMixin: function (mixin) {
+ // Currently a noop. Will be used to validate and trace mixins.
+ return mixin;
+ },
+
+ // This looks DOM specific but these are actually isomorphic helpers
+ // since they are just generating DOM strings.
+ DOM: ReactDOMFactories,
+
+ version: ReactVersion,
+
+ // Hook for JSX spread, don't use this for anything else.
+ __spread: assign
+};
+
+module.exports = React;
+},{"135":135,"24":24,"32":32,"33":33,"34":34,"43":43,"57":57,"58":58,"82":82,"97":97}],70:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactLink
+ * @typechecks static-only
+ */
+
+'use strict';
+
+/**
+ * ReactLink encapsulates a common pattern in which a component wants to modify
+ * a prop received from its parent. ReactLink allows the parent to pass down a
+ * value coupled with a callback that, when invoked, expresses an intent to
+ * modify that value. For example:
+ *
+ * React.createClass({
+ * getInitialState: function() {
+ * return {value: ''};
+ * },
+ * render: function() {
+ * var valueLink = new ReactLink(this.state.value, this._handleValueChange);
+ * return <input valueLink={valueLink} />;
+ * },
+ * _handleValueChange: function(newValue) {
+ * this.setState({value: newValue});
+ * }
+ * });
+ *
+ * We have provided some sugary mixins to make the creation and
+ * consumption of ReactLink easier; see LinkedValueUtils and LinkedStateMixin.
+ */
+
+var React = _dereq_(26);
+
+/**
+ * @param {*} value current value of the link
+ * @param {function} requestChange callback to request a change
+ */
+function ReactLink(value, requestChange) {
+ this.value = value;
+ this.requestChange = requestChange;
+}
+
+/**
+ * Creates a PropType that enforces the ReactLink API and optionally checks the
+ * type of the value being passed inside the link. Example:
+ *
+ * MyComponent.propTypes = {
+ * tabIndexLink: ReactLink.PropTypes.link(React.PropTypes.number)
+ * }
+ */
+function createLinkTypeChecker(linkType) {
+ var shapes = {
+ value: typeof linkType === 'undefined' ? React.PropTypes.any.isRequired : linkType.isRequired,
+ requestChange: React.PropTypes.func.isRequired
+ };
+ return React.PropTypes.shape(shapes);
+}
+
+ReactLink.PropTypes = {
+ link: createLinkTypeChecker
+};
+
+module.exports = ReactLink;
+},{"26":26}],71:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactMarkupChecksum
+ */
+
+'use strict';
+
+var adler32 = _dereq_(116);
+
+var TAG_END = /\/?>/;
+
+var ReactMarkupChecksum = {
+ CHECKSUM_ATTR_NAME: 'data-react-checksum',
+
+ /**
+ * @param {string} markup Markup string
+ * @return {string} Markup string with checksum attribute attached
+ */
+ addChecksumToMarkup: function (markup) {
+ var checksum = adler32(markup);
+
+ // Add checksum (handle both parent tags and self-closing tags)
+ return markup.replace(TAG_END, ' ' + ReactMarkupChecksum.CHECKSUM_ATTR_NAME + '="' + checksum + '"$&');
+ },
+
+ /**
+ * @param {string} markup to use
+ * @param {DOMElement} element root React element
+ * @returns {boolean} whether or not the markup is the same
+ */
+ canReuseMarkup: function (markup, element) {
+ var existingChecksum = element.getAttribute(ReactMarkupChecksum.CHECKSUM_ATTR_NAME);
+ existingChecksum = existingChecksum && parseInt(existingChecksum, 10);
+ var markupChecksum = adler32(markup);
+ return markupChecksum === existingChecksum;
+ }
+};
+
+module.exports = ReactMarkupChecksum;
+},{"116":116}],72:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactMount
+ */
+
+'use strict';
+
+var DOMProperty = _dereq_(10);
+var ReactBrowserEventEmitter = _dereq_(28);
+var ReactCurrentOwner = _dereq_(39);
+var ReactDOMFeatureFlags = _dereq_(44);
+var ReactElement = _dereq_(57);
+var ReactEmptyComponentRegistry = _dereq_(60);
+var ReactInstanceHandles = _dereq_(67);
+var ReactInstanceMap = _dereq_(68);
+var ReactMarkupChecksum = _dereq_(71);
+var ReactPerf = _dereq_(78);
+var ReactReconciler = _dereq_(84);
+var ReactUpdateQueue = _dereq_(95);
+var ReactUpdates = _dereq_(96);
+
+var assign = _dereq_(24);
+var emptyObject = _dereq_(154);
+var containsNode = _dereq_(150);
+var instantiateReactComponent = _dereq_(132);
+var invariant = _dereq_(161);
+var setInnerHTML = _dereq_(138);
+var shouldUpdateReactComponent = _dereq_(141);
+var validateDOMNesting = _dereq_(144);
+var warning = _dereq_(173);
+
+var ATTR_NAME = DOMProperty.ID_ATTRIBUTE_NAME;
+var nodeCache = {};
+
+var ELEMENT_NODE_TYPE = 1;
+var DOC_NODE_TYPE = 9;
+var DOCUMENT_FRAGMENT_NODE_TYPE = 11;
+
+var ownerDocumentContextKey = '__ReactMount_ownerDocument$' + Math.random().toString(36).slice(2);
+
+/** Mapping from reactRootID to React component instance. */
+var instancesByReactRootID = {};
+
+/** Mapping from reactRootID to `container` nodes. */
+var containersByReactRootID = {};
+
+if ("production" !== 'production') {
+ /** __DEV__-only mapping from reactRootID to root elements. */
+ var rootElementsByReactRootID = {};
+}
+
+// Used to store breadth-first search state in findComponentRoot.
+var findComponentRootReusableArray = [];
+
+/**
+ * Finds the index of the first character
+ * that's not common between the two given strings.
+ *
+ * @return {number} the index of the character where the strings diverge
+ */
+function firstDifferenceIndex(string1, string2) {
+ var minLen = Math.min(string1.length, string2.length);
+ for (var i = 0; i < minLen; i++) {
+ if (string1.charAt(i) !== string2.charAt(i)) {
+ return i;
+ }
+ }
+ return string1.length === string2.length ? -1 : minLen;
+}
+
+/**
+ * @param {DOMElement|DOMDocument} container DOM element that may contain
+ * a React component
+ * @return {?*} DOM element that may have the reactRoot ID, or null.
+ */
+function getReactRootElementInContainer(container) {
+ if (!container) {
+ return null;
+ }
+
+ if (container.nodeType === DOC_NODE_TYPE) {
+ return container.documentElement;
+ } else {
+ return container.firstChild;
+ }
+}
+
+/**
+ * @param {DOMElement} container DOM element that may contain a React component.
+ * @return {?string} A "reactRoot" ID, if a React component is rendered.
+ */
+function getReactRootID(container) {
+ var rootElement = getReactRootElementInContainer(container);
+ return rootElement && ReactMount.getID(rootElement);
+}
+
+/**
+ * Accessing node[ATTR_NAME] or calling getAttribute(ATTR_NAME) on a form
+ * element can return its control whose name or ID equals ATTR_NAME. All
+ * DOM nodes support `getAttributeNode` but this can also get called on
+ * other objects so just return '' if we're given something other than a
+ * DOM node (such as window).
+ *
+ * @param {?DOMElement|DOMWindow|DOMDocument|DOMTextNode} node DOM node.
+ * @return {string} ID of the supplied `domNode`.
+ */
+function getID(node) {
+ var id = internalGetID(node);
+ if (id) {
+ if (nodeCache.hasOwnProperty(id)) {
+ var cached = nodeCache[id];
+ if (cached !== node) {
+ !!isValid(cached, id) ? "production" !== 'production' ? invariant(false, 'ReactMount: Two valid but unequal nodes with the same `%s`: %s', ATTR_NAME, id) : invariant(false) : undefined;
+
+ nodeCache[id] = node;
+ }
+ } else {
+ nodeCache[id] = node;
+ }
+ }
+
+ return id;
+}
+
+function internalGetID(node) {
+ // If node is something like a window, document, or text node, none of
+ // which support attributes or a .getAttribute method, gracefully return
+ // the empty string, as if the attribute were missing.
+ return node && node.getAttribute && node.getAttribute(ATTR_NAME) || '';
+}
+
+/**
+ * Sets the React-specific ID of the given node.
+ *
+ * @param {DOMElement} node The DOM node whose ID will be set.
+ * @param {string} id The value of the ID attribute.
+ */
+function setID(node, id) {
+ var oldID = internalGetID(node);
+ if (oldID !== id) {
+ delete nodeCache[oldID];
+ }
+ node.setAttribute(ATTR_NAME, id);
+ nodeCache[id] = node;
+}
+
+/**
+ * Finds the node with the supplied React-generated DOM ID.
+ *
+ * @param {string} id A React-generated DOM ID.
+ * @return {DOMElement} DOM node with the suppled `id`.
+ * @internal
+ */
+function getNode(id) {
+ if (!nodeCache.hasOwnProperty(id) || !isValid(nodeCache[id], id)) {
+ nodeCache[id] = ReactMount.findReactNodeByID(id);
+ }
+ return nodeCache[id];
+}
+
+/**
+ * Finds the node with the supplied public React instance.
+ *
+ * @param {*} instance A public React instance.
+ * @return {?DOMElement} DOM node with the suppled `id`.
+ * @internal
+ */
+function getNodeFromInstance(instance) {
+ var id = ReactInstanceMap.get(instance)._rootNodeID;
+ if (ReactEmptyComponentRegistry.isNullComponentID(id)) {
+ return null;
+ }
+ if (!nodeCache.hasOwnProperty(id) || !isValid(nodeCache[id], id)) {
+ nodeCache[id] = ReactMount.findReactNodeByID(id);
+ }
+ return nodeCache[id];
+}
+
+/**
+ * A node is "valid" if it is contained by a currently mounted container.
+ *
+ * This means that the node does not have to be contained by a document in
+ * order to be considered valid.
+ *
+ * @param {?DOMElement} node The candidate DOM node.
+ * @param {string} id The expected ID of the node.
+ * @return {boolean} Whether the node is contained by a mounted container.
+ */
+function isValid(node, id) {
+ if (node) {
+ !(internalGetID(node) === id) ? "production" !== 'production' ? invariant(false, 'ReactMount: Unexpected modification of `%s`', ATTR_NAME) : invariant(false) : undefined;
+
+ var container = ReactMount.findReactContainerForID(id);
+ if (container && containsNode(container, node)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/**
+ * Causes the cache to forget about one React-specific ID.
+ *
+ * @param {string} id The ID to forget.
+ */
+function purgeID(id) {
+ delete nodeCache[id];
+}
+
+var deepestNodeSoFar = null;
+function findDeepestCachedAncestorImpl(ancestorID) {
+ var ancestor = nodeCache[ancestorID];
+ if (ancestor && isValid(ancestor, ancestorID)) {
+ deepestNodeSoFar = ancestor;
+ } else {
+ // This node isn't populated in the cache, so presumably none of its
+ // descendants are. Break out of the loop.
+ return false;
+ }
+}
+
+/**
+ * Return the deepest cached node whose ID is a prefix of `targetID`.
+ */
+function findDeepestCachedAncestor(targetID) {
+ deepestNodeSoFar = null;
+ ReactInstanceHandles.traverseAncestors(targetID, findDeepestCachedAncestorImpl);
+
+ var foundNode = deepestNodeSoFar;
+ deepestNodeSoFar = null;
+ return foundNode;
+}
+
+/**
+ * Mounts this component and inserts it into the DOM.
+ *
+ * @param {ReactComponent} componentInstance The instance to mount.
+ * @param {string} rootID DOM ID of the root node.
+ * @param {DOMElement} container DOM element to mount into.
+ * @param {ReactReconcileTransaction} transaction
+ * @param {boolean} shouldReuseMarkup If true, do not insert markup
+ */
+function mountComponentIntoNode(componentInstance, rootID, container, transaction, shouldReuseMarkup, context) {
+ if (ReactDOMFeatureFlags.useCreateElement) {
+ context = assign({}, context);
+ if (container.nodeType === DOC_NODE_TYPE) {
+ context[ownerDocumentContextKey] = container;
+ } else {
+ context[ownerDocumentContextKey] = container.ownerDocument;
+ }
+ }
+ if ("production" !== 'production') {
+ if (context === emptyObject) {
+ context = {};
+ }
+ var tag = container.nodeName.toLowerCase();
+ context[validateDOMNesting.ancestorInfoContextKey] = validateDOMNesting.updatedAncestorInfo(null, tag, null);
+ }
+ var markup = ReactReconciler.mountComponent(componentInstance, rootID, transaction, context);
+ componentInstance._renderedComponent._topLevelWrapper = componentInstance;
+ ReactMount._mountImageIntoNode(markup, container, shouldReuseMarkup, transaction);
+}
+
+/**
+ * Batched mount.
+ *
+ * @param {ReactComponent} componentInstance The instance to mount.
+ * @param {string} rootID DOM ID of the root node.
+ * @param {DOMElement} container DOM element to mount into.
+ * @param {boolean} shouldReuseMarkup If true, do not insert markup
+ */
+function batchedMountComponentIntoNode(componentInstance, rootID, container, shouldReuseMarkup, context) {
+ var transaction = ReactUpdates.ReactReconcileTransaction.getPooled(
+ /* forceHTML */shouldReuseMarkup);
+ transaction.perform(mountComponentIntoNode, null, componentInstance, rootID, container, transaction, shouldReuseMarkup, context);
+ ReactUpdates.ReactReconcileTransaction.release(transaction);
+}
+
+/**
+ * Unmounts a component and removes it from the DOM.
+ *
+ * @param {ReactComponent} instance React component instance.
+ * @param {DOMElement} container DOM element to unmount from.
+ * @final
+ * @internal
+ * @see {ReactMount.unmountComponentAtNode}
+ */
+function unmountComponentFromNode(instance, container) {
+ ReactReconciler.unmountComponent(instance);
+
+ if (container.nodeType === DOC_NODE_TYPE) {
+ container = container.documentElement;
+ }
+
+ // http://jsperf.com/emptying-a-node
+ while (container.lastChild) {
+ container.removeChild(container.lastChild);
+ }
+}
+
+/**
+ * True if the supplied DOM node has a direct React-rendered child that is
+ * not a React root element. Useful for warning in `render`,
+ * `unmountComponentAtNode`, etc.
+ *
+ * @param {?DOMElement} node The candidate DOM node.
+ * @return {boolean} True if the DOM element contains a direct child that was
+ * rendered by React but is not a root element.
+ * @internal
+ */
+function hasNonRootReactChild(node) {
+ var reactRootID = getReactRootID(node);
+ return reactRootID ? reactRootID !== ReactInstanceHandles.getReactRootIDFromNodeID(reactRootID) : false;
+}
+
+/**
+ * Returns the first (deepest) ancestor of a node which is rendered by this copy
+ * of React.
+ */
+function findFirstReactDOMImpl(node) {
+ // This node might be from another React instance, so we make sure not to
+ // examine the node cache here
+ for (; node && node.parentNode !== node; node = node.parentNode) {
+ if (node.nodeType !== 1) {
+ // Not a DOMElement, therefore not a React component
+ continue;
+ }
+ var nodeID = internalGetID(node);
+ if (!nodeID) {
+ continue;
+ }
+ var reactRootID = ReactInstanceHandles.getReactRootIDFromNodeID(nodeID);
+
+ // If containersByReactRootID contains the container we find by crawling up
+ // the tree, we know that this instance of React rendered the node.
+ // nb. isValid's strategy (with containsNode) does not work because render
+ // trees may be nested and we don't want a false positive in that case.
+ var current = node;
+ var lastID;
+ do {
+ lastID = internalGetID(current);
+ current = current.parentNode;
+ if (current == null) {
+ // The passed-in node has been detached from the container it was
+ // originally rendered into.
+ return null;
+ }
+ } while (lastID !== reactRootID);
+
+ if (current === containersByReactRootID[reactRootID]) {
+ return node;
+ }
+ }
+ return null;
+}
+
+/**
+ * Temporary (?) hack so that we can store all top-level pending updates on
+ * composites instead of having to worry about different types of components
+ * here.
+ */
+var TopLevelWrapper = function () {};
+TopLevelWrapper.prototype.isReactComponent = {};
+if ("production" !== 'production') {
+ TopLevelWrapper.displayName = 'TopLevelWrapper';
+}
+TopLevelWrapper.prototype.render = function () {
+ // this.props is actually a ReactElement
+ return this.props;
+};
+
+/**
+ * Mounting is the process of initializing a React component by creating its
+ * representative DOM elements and inserting them into a supplied `container`.
+ * Any prior content inside `container` is destroyed in the process.
+ *
+ * ReactMount.render(
+ * component,
+ * document.getElementById('container')
+ * );
+ *
+ * <div id="container"> <-- Supplied `container`.
+ * <div data-reactid=".3"> <-- Rendered reactRoot of React
+ * // ... component.
+ * </div>
+ * </div>
+ *
+ * Inside of `container`, the first element rendered is the "reactRoot".
+ */
+var ReactMount = {
+
+ TopLevelWrapper: TopLevelWrapper,
+
+ /** Exposed for debugging purposes **/
+ _instancesByReactRootID: instancesByReactRootID,
+
+ /**
+ * This is a hook provided to support rendering React components while
+ * ensuring that the apparent scroll position of its `container` does not
+ * change.
+ *
+ * @param {DOMElement} container The `container` being rendered into.
+ * @param {function} renderCallback This must be called once to do the render.
+ */
+ scrollMonitor: function (container, renderCallback) {
+ renderCallback();
+ },
+
+ /**
+ * Take a component that's already mounted into the DOM and replace its props
+ * @param {ReactComponent} prevComponent component instance already in the DOM
+ * @param {ReactElement} nextElement component instance to render
+ * @param {DOMElement} container container to render into
+ * @param {?function} callback function triggered on completion
+ */
+ _updateRootComponent: function (prevComponent, nextElement, container, callback) {
+ ReactMount.scrollMonitor(container, function () {
+ ReactUpdateQueue.enqueueElementInternal(prevComponent, nextElement);
+ if (callback) {
+ ReactUpdateQueue.enqueueCallbackInternal(prevComponent, callback);
+ }
+ });
+
+ if ("production" !== 'production') {
+ // Record the root element in case it later gets transplanted.
+ rootElementsByReactRootID[getReactRootID(container)] = getReactRootElementInContainer(container);
+ }
+
+ return prevComponent;
+ },
+
+ /**
+ * Register a component into the instance map and starts scroll value
+ * monitoring
+ * @param {ReactComponent} nextComponent component instance to render
+ * @param {DOMElement} container container to render into
+ * @return {string} reactRoot ID prefix
+ */
+ _registerComponent: function (nextComponent, container) {
+ !(container && (container.nodeType === ELEMENT_NODE_TYPE || container.nodeType === DOC_NODE_TYPE || container.nodeType === DOCUMENT_FRAGMENT_NODE_TYPE)) ? "production" !== 'production' ? invariant(false, '_registerComponent(...): Target container is not a DOM element.') : invariant(false) : undefined;
+
+ ReactBrowserEventEmitter.ensureScrollValueMonitoring();
+
+ var reactRootID = ReactMount.registerContainer(container);
+ instancesByReactRootID[reactRootID] = nextComponent;
+ return reactRootID;
+ },
+
+ /**
+ * Render a new component into the DOM.
+ * @param {ReactElement} nextElement element to render
+ * @param {DOMElement} container container to render into
+ * @param {boolean} shouldReuseMarkup if we should skip the markup insertion
+ * @return {ReactComponent} nextComponent
+ */
+ _renderNewRootComponent: function (nextElement, container, shouldReuseMarkup, context) {
+ // Various parts of our code (such as ReactCompositeComponent's
+ // _renderValidatedComponent) assume that calls to render aren't nested;
+ // verify that that's the case.
+ "production" !== 'production' ? warning(ReactCurrentOwner.current == null, '_renderNewRootComponent(): Render methods should be a pure function ' + 'of props and state; triggering nested component updates from ' + 'render is not allowed. If necessary, trigger nested updates in ' + 'componentDidUpdate. Check the render method of %s.', ReactCurrentOwner.current && ReactCurrentOwner.current.getName() || 'ReactCompositeComponent') : undefined;
+
+ var componentInstance = instantiateReactComponent(nextElement, null);
+ var reactRootID = ReactMount._registerComponent(componentInstance, container);
+
+ // The initial render is synchronous but any updates that happen during
+ // rendering, in componentWillMount or componentDidMount, will be batched
+ // according to the current batching strategy.
+
+ ReactUpdates.batchedUpdates(batchedMountComponentIntoNode, componentInstance, reactRootID, container, shouldReuseMarkup, context);
+
+ if ("production" !== 'production') {
+ // Record the root element in case it later gets transplanted.
+ rootElementsByReactRootID[reactRootID] = getReactRootElementInContainer(container);
+ }
+
+ return componentInstance;
+ },
+
+ /**
+ * Renders a React component into the DOM in the supplied `container`.
+ *
+ * If the React component was previously rendered into `container`, this will
+ * perform an update on it and only mutate the DOM as necessary to reflect the
+ * latest React component.
+ *
+ * @param {ReactComponent} parentComponent The conceptual parent of this render tree.
+ * @param {ReactElement} nextElement Component element to render.
+ * @param {DOMElement} container DOM element to render into.
+ * @param {?function} callback function triggered on completion
+ * @return {ReactComponent} Component instance rendered in `container`.
+ */
+ renderSubtreeIntoContainer: function (parentComponent, nextElement, container, callback) {
+ !(parentComponent != null && parentComponent._reactInternalInstance != null) ? "production" !== 'production' ? invariant(false, 'parentComponent must be a valid React Component') : invariant(false) : undefined;
+ return ReactMount._renderSubtreeIntoContainer(parentComponent, nextElement, container, callback);
+ },
+
+ _renderSubtreeIntoContainer: function (parentComponent, nextElement, container, callback) {
+ !ReactElement.isValidElement(nextElement) ? "production" !== 'production' ? invariant(false, 'ReactDOM.render(): Invalid component element.%s', typeof nextElement === 'string' ? ' Instead of passing an element string, make sure to instantiate ' + 'it by passing it to React.createElement.' : typeof nextElement === 'function' ? ' Instead of passing a component class, make sure to instantiate ' + 'it by passing it to React.createElement.' :
+ // Check if it quacks like an element
+ nextElement != null && nextElement.props !== undefined ? ' This may be caused by unintentionally loading two independent ' + 'copies of React.' : '') : invariant(false) : undefined;
+
+ "production" !== 'production' ? warning(!container || !container.tagName || container.tagName.toUpperCase() !== 'BODY', 'render(): Rendering components directly into document.body is ' + 'discouraged, since its children are often manipulated by third-party ' + 'scripts and browser extensions. This may lead to subtle ' + 'reconciliation issues. Try rendering into a container element created ' + 'for your app.') : undefined;
+
+ var nextWrappedElement = new ReactElement(TopLevelWrapper, null, null, null, null, null, nextElement);
+
+ var prevComponent = instancesByReactRootID[getReactRootID(container)];
+
+ if (prevComponent) {
+ var prevWrappedElement = prevComponent._currentElement;
+ var prevElement = prevWrappedElement.props;
+ if (shouldUpdateReactComponent(prevElement, nextElement)) {
+ var publicInst = prevComponent._renderedComponent.getPublicInstance();
+ var updatedCallback = callback && function () {
+ callback.call(publicInst);
+ };
+ ReactMount._updateRootComponent(prevComponent, nextWrappedElement, container, updatedCallback);
+ return publicInst;
+ } else {
+ ReactMount.unmountComponentAtNode(container);
+ }
+ }
+
+ var reactRootElement = getReactRootElementInContainer(container);
+ var containerHasReactMarkup = reactRootElement && !!internalGetID(reactRootElement);
+ var containerHasNonRootReactChild = hasNonRootReactChild(container);
+
+ if ("production" !== 'production') {
+ "production" !== 'production' ? warning(!containerHasNonRootReactChild, 'render(...): Replacing React-rendered children with a new root ' + 'component. If you intended to update the children of this node, ' + 'you should instead have the existing children update their state ' + 'and render the new components instead of calling ReactDOM.render.') : undefined;
+
+ if (!containerHasReactMarkup || reactRootElement.nextSibling) {
+ var rootElementSibling = reactRootElement;
+ while (rootElementSibling) {
+ if (internalGetID(rootElementSibling)) {
+ "production" !== 'production' ? warning(false, 'render(): Target node has markup rendered by React, but there ' + 'are unrelated nodes as well. This is most commonly caused by ' + 'white-space inserted around server-rendered markup.') : undefined;
+ break;
+ }
+ rootElementSibling = rootElementSibling.nextSibling;
+ }
+ }
+ }
+
+ var shouldReuseMarkup = containerHasReactMarkup && !prevComponent && !containerHasNonRootReactChild;
+ var component = ReactMount._renderNewRootComponent(nextWrappedElement, container, shouldReuseMarkup, parentComponent != null ? parentComponent._reactInternalInstance._processChildContext(parentComponent._reactInternalInstance._context) : emptyObject)._renderedComponent.getPublicInstance();
+ if (callback) {
+ callback.call(component);
+ }
+ return component;
+ },
+
+ /**
+ * Renders a React component into the DOM in the supplied `container`.
+ *
+ * If the React component was previously rendered into `container`, this will
+ * perform an update on it and only mutate the DOM as necessary to reflect the
+ * latest React component.
+ *
+ * @param {ReactElement} nextElement Component element to render.
+ * @param {DOMElement} container DOM element to render into.
+ * @param {?function} callback function triggered on completion
+ * @return {ReactComponent} Component instance rendered in `container`.
+ */
+ render: function (nextElement, container, callback) {
+ return ReactMount._renderSubtreeIntoContainer(null, nextElement, container, callback);
+ },
+
+ /**
+ * Registers a container node into which React components will be rendered.
+ * This also creates the "reactRoot" ID that will be assigned to the element
+ * rendered within.
+ *
+ * @param {DOMElement} container DOM element to register as a container.
+ * @return {string} The "reactRoot" ID of elements rendered within.
+ */
+ registerContainer: function (container) {
+ var reactRootID = getReactRootID(container);
+ if (reactRootID) {
+ // If one exists, make sure it is a valid "reactRoot" ID.
+ reactRootID = ReactInstanceHandles.getReactRootIDFromNodeID(reactRootID);
+ }
+ if (!reactRootID) {
+ // No valid "reactRoot" ID found, create one.
+ reactRootID = ReactInstanceHandles.createReactRootID();
+ }
+ containersByReactRootID[reactRootID] = container;
+ return reactRootID;
+ },
+
+ /**
+ * Unmounts and destroys the React component rendered in the `container`.
+ *
+ * @param {DOMElement} container DOM element containing a React component.
+ * @return {boolean} True if a component was found in and unmounted from
+ * `container`
+ */
+ unmountComponentAtNode: function (container) {
+ // Various parts of our code (such as ReactCompositeComponent's
+ // _renderValidatedComponent) assume that calls to render aren't nested;
+ // verify that that's the case. (Strictly speaking, unmounting won't cause a
+ // render but we still don't expect to be in a render call here.)
+ "production" !== 'production' ? warning(ReactCurrentOwner.current == null, 'unmountComponentAtNode(): Render methods should be a pure function ' + 'of props and state; triggering nested component updates from render ' + 'is not allowed. If necessary, trigger nested updates in ' + 'componentDidUpdate. Check the render method of %s.', ReactCurrentOwner.current && ReactCurrentOwner.current.getName() || 'ReactCompositeComponent') : undefined;
+
+ !(container && (container.nodeType === ELEMENT_NODE_TYPE || container.nodeType === DOC_NODE_TYPE || container.nodeType === DOCUMENT_FRAGMENT_NODE_TYPE)) ? "production" !== 'production' ? invariant(false, 'unmountComponentAtNode(...): Target container is not a DOM element.') : invariant(false) : undefined;
+
+ var reactRootID = getReactRootID(container);
+ var component = instancesByReactRootID[reactRootID];
+ if (!component) {
+ // Check if the node being unmounted was rendered by React, but isn't a
+ // root node.
+ var containerHasNonRootReactChild = hasNonRootReactChild(container);
+
+ // Check if the container itself is a React root node.
+ var containerID = internalGetID(container);
+ var isContainerReactRoot = containerID && containerID === ReactInstanceHandles.getReactRootIDFromNodeID(containerID);
+
+ if ("production" !== 'production') {
+ "production" !== 'production' ? warning(!containerHasNonRootReactChild, 'unmountComponentAtNode(): The node you\'re attempting to unmount ' + 'was rendered by React and is not a top-level container. %s', isContainerReactRoot ? 'You may have accidentally passed in a React root node instead ' + 'of its container.' : 'Instead, have the parent component update its state and ' + 'rerender in order to remove this component.') : undefined;
+ }
+
+ return false;
+ }
+ ReactUpdates.batchedUpdates(unmountComponentFromNode, component, container);
+ delete instancesByReactRootID[reactRootID];
+ delete containersByReactRootID[reactRootID];
+ if ("production" !== 'production') {
+ delete rootElementsByReactRootID[reactRootID];
+ }
+ return true;
+ },
+
+ /**
+ * Finds the container DOM element that contains React component to which the
+ * supplied DOM `id` belongs.
+ *
+ * @param {string} id The ID of an element rendered by a React component.
+ * @return {?DOMElement} DOM element that contains the `id`.
+ */
+ findReactContainerForID: function (id) {
+ var reactRootID = ReactInstanceHandles.getReactRootIDFromNodeID(id);
+ var container = containersByReactRootID[reactRootID];
+
+ if ("production" !== 'production') {
+ var rootElement = rootElementsByReactRootID[reactRootID];
+ if (rootElement && rootElement.parentNode !== container) {
+ "production" !== 'production' ? warning(
+ // Call internalGetID here because getID calls isValid which calls
+ // findReactContainerForID (this function).
+ internalGetID(rootElement) === reactRootID, 'ReactMount: Root element ID differed from reactRootID.') : undefined;
+ var containerChild = container.firstChild;
+ if (containerChild && reactRootID === internalGetID(containerChild)) {
+ // If the container has a new child with the same ID as the old
+ // root element, then rootElementsByReactRootID[reactRootID] is
+ // just stale and needs to be updated. The case that deserves a
+ // warning is when the container is empty.
+ rootElementsByReactRootID[reactRootID] = containerChild;
+ } else {
+ "production" !== 'production' ? warning(false, 'ReactMount: Root element has been removed from its original ' + 'container. New container: %s', rootElement.parentNode) : undefined;
+ }
+ }
+ }
+
+ return container;
+ },
+
+ /**
+ * Finds an element rendered by React with the supplied ID.
+ *
+ * @param {string} id ID of a DOM node in the React component.
+ * @return {DOMElement} Root DOM node of the React component.
+ */
+ findReactNodeByID: function (id) {
+ var reactRoot = ReactMount.findReactContainerForID(id);
+ return ReactMount.findComponentRoot(reactRoot, id);
+ },
+
+ /**
+ * Traverses up the ancestors of the supplied node to find a node that is a
+ * DOM representation of a React component rendered by this copy of React.
+ *
+ * @param {*} node
+ * @return {?DOMEventTarget}
+ * @internal
+ */
+ getFirstReactDOM: function (node) {
+ return findFirstReactDOMImpl(node);
+ },
+
+ /**
+ * Finds a node with the supplied `targetID` inside of the supplied
+ * `ancestorNode`. Exploits the ID naming scheme to perform the search
+ * quickly.
+ *
+ * @param {DOMEventTarget} ancestorNode Search from this root.
+ * @pararm {string} targetID ID of the DOM representation of the component.
+ * @return {DOMEventTarget} DOM node with the supplied `targetID`.
+ * @internal
+ */
+ findComponentRoot: function (ancestorNode, targetID) {
+ var firstChildren = findComponentRootReusableArray;
+ var childIndex = 0;
+
+ var deepestAncestor = findDeepestCachedAncestor(targetID) || ancestorNode;
+
+ if ("production" !== 'production') {
+ // This will throw on the next line; give an early warning
+ "production" !== 'production' ? warning(deepestAncestor != null, 'React can\'t find the root component node for data-reactid value ' + '`%s`. If you\'re seeing this message, it probably means that ' + 'you\'ve loaded two copies of React on the page. At this time, only ' + 'a single copy of React can be loaded at a time.', targetID) : undefined;
+ }
+
+ firstChildren[0] = deepestAncestor.firstChild;
+ firstChildren.length = 1;
+
+ while (childIndex < firstChildren.length) {
+ var child = firstChildren[childIndex++];
+ var targetChild;
+
+ while (child) {
+ var childID = ReactMount.getID(child);
+ if (childID) {
+ // Even if we find the node we're looking for, we finish looping
+ // through its siblings to ensure they're cached so that we don't have
+ // to revisit this node again. Otherwise, we make n^2 calls to getID
+ // when visiting the many children of a single node in order.
+
+ if (targetID === childID) {
+ targetChild = child;
+ } else if (ReactInstanceHandles.isAncestorIDOf(childID, targetID)) {
+ // If we find a child whose ID is an ancestor of the given ID,
+ // then we can be sure that we only want to search the subtree
+ // rooted at this child, so we can throw out the rest of the
+ // search state.
+ firstChildren.length = childIndex = 0;
+ firstChildren.push(child.firstChild);
+ }
+ } else {
+ // If this child had no ID, then there's a chance that it was
+ // injected automatically by the browser, as when a `<table>`
+ // element sprouts an extra `<tbody>` child as a side effect of
+ // `.innerHTML` parsing. Optimistically continue down this
+ // branch, but not before examining the other siblings.
+ firstChildren.push(child.firstChild);
+ }
+
+ child = child.nextSibling;
+ }
+
+ if (targetChild) {
+ // Emptying firstChildren/findComponentRootReusableArray is
+ // not necessary for correctness, but it helps the GC reclaim
+ // any nodes that were left at the end of the search.
+ firstChildren.length = 0;
+
+ return targetChild;
+ }
+ }
+
+ firstChildren.length = 0;
+
+ !false ? "production" !== 'production' ? invariant(false, 'findComponentRoot(..., %s): Unable to find element. This probably ' + 'means the DOM was unexpectedly mutated (e.g., by the browser), ' + 'usually due to forgetting a <tbody> when using tables, nesting tags ' + 'like <form>, <p>, or <a>, or using non-SVG elements in an <svg> ' + 'parent. ' + 'Try inspecting the child nodes of the element with React ID `%s`.', targetID, ReactMount.getID(ancestorNode)) : invariant(false) : undefined;
+ },
+
+ _mountImageIntoNode: function (markup, container, shouldReuseMarkup, transaction) {
+ !(container && (container.nodeType === ELEMENT_NODE_TYPE || container.nodeType === DOC_NODE_TYPE || container.nodeType === DOCUMENT_FRAGMENT_NODE_TYPE)) ? "production" !== 'production' ? invariant(false, 'mountComponentIntoNode(...): Target container is not valid.') : invariant(false) : undefined;
+
+ if (shouldReuseMarkup) {
+ var rootElement = getReactRootElementInContainer(container);
+ if (ReactMarkupChecksum.canReuseMarkup(markup, rootElement)) {
+ return;
+ } else {
+ var checksum = rootElement.getAttribute(ReactMarkupChecksum.CHECKSUM_ATTR_NAME);
+ rootElement.removeAttribute(ReactMarkupChecksum.CHECKSUM_ATTR_NAME);
+
+ var rootMarkup = rootElement.outerHTML;
+ rootElement.setAttribute(ReactMarkupChecksum.CHECKSUM_ATTR_NAME, checksum);
+
+ var normalizedMarkup = markup;
+ if ("production" !== 'production') {
+ // because rootMarkup is retrieved from the DOM, various normalizations
+ // will have occurred which will not be present in `markup`. Here,
+ // insert markup into a <div> or <iframe> depending on the container
+ // type to perform the same normalizations before comparing.
+ var normalizer;
+ if (container.nodeType === ELEMENT_NODE_TYPE) {
+ normalizer = document.createElementNS('http://www.w3.org/1999/xhtml', 'div');
+ normalizer.innerHTML = markup;
+ normalizedMarkup = normalizer.innerHTML;
+ } else {
+ normalizer = document.createElementNS('http://www.w3.org/1999/xhtml', 'iframe');
+ document.body.appendChild(normalizer);
+ normalizer.contentDocument.write(markup);
+ normalizedMarkup = normalizer.contentDocument.documentElement.outerHTML;
+ document.body.removeChild(normalizer);
+ }
+ }
+
+ var diffIndex = firstDifferenceIndex(normalizedMarkup, rootMarkup);
+ var difference = ' (client) ' + normalizedMarkup.substring(diffIndex - 20, diffIndex + 20) + '\n (server) ' + rootMarkup.substring(diffIndex - 20, diffIndex + 20);
+
+ !(container.nodeType !== DOC_NODE_TYPE) ? "production" !== 'production' ? invariant(false, 'You\'re trying to render a component to the document using ' + 'server rendering but the checksum was invalid. This usually ' + 'means you rendered a different component type or props on ' + 'the client from the one on the server, or your render() ' + 'methods are impure. React cannot handle this case due to ' + 'cross-browser quirks by rendering at the document root. You ' + 'should look for environment dependent code in your components ' + 'and ensure the props are the same client and server side:\n%s', difference) : invariant(false) : undefined;
+
+ if ("production" !== 'production') {
+ "production" !== 'production' ? warning(false, 'React attempted to reuse markup in a container but the ' + 'checksum was invalid. This generally means that you are ' + 'using server rendering and the markup generated on the ' + 'server was not what the client was expecting. React injected ' + 'new markup to compensate which works but you have lost many ' + 'of the benefits of server rendering. Instead, figure out ' + 'why the markup being generated is different on the client ' + 'or server:\n%s', difference) : undefined;
+ }
+ }
+ }
+
+ !(container.nodeType !== DOC_NODE_TYPE) ? "production" !== 'production' ? invariant(false, 'You\'re trying to render a component to the document but ' + 'you didn\'t use server rendering. We can\'t do this ' + 'without using server rendering due to cross-browser quirks. ' + 'See ReactDOMServer.renderToString() for server rendering.') : invariant(false) : undefined;
+
+ if (transaction.useCreateElement) {
+ while (container.lastChild) {
+ container.removeChild(container.lastChild);
+ }
+ container.appendChild(markup);
+ } else {
+ setInnerHTML(container, markup);
+ }
+ },
+
+ ownerDocumentContextKey: ownerDocumentContextKey,
+
+ /**
+ * React ID utilities.
+ */
+
+ getReactRootID: getReactRootID,
+
+ getID: getID,
+
+ setID: setID,
+
+ getNode: getNode,
+
+ getNodeFromInstance: getNodeFromInstance,
+
+ isValid: isValid,
+
+ purgeID: purgeID
+};
+
+ReactPerf.measureMethods(ReactMount, 'ReactMount', {
+ _renderNewRootComponent: '_renderNewRootComponent',
+ _mountImageIntoNode: '_mountImageIntoNode'
+});
+
+module.exports = ReactMount;
+},{"10":10,"132":132,"138":138,"141":141,"144":144,"150":150,"154":154,"161":161,"173":173,"24":24,"28":28,"39":39,"44":44,"57":57,"60":60,"67":67,"68":68,"71":71,"78":78,"84":84,"95":95,"96":96}],73:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactMultiChild
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var ReactComponentEnvironment = _dereq_(36);
+var ReactMultiChildUpdateTypes = _dereq_(74);
+
+var ReactCurrentOwner = _dereq_(39);
+var ReactReconciler = _dereq_(84);
+var ReactChildReconciler = _dereq_(31);
+
+var flattenChildren = _dereq_(123);
+
+/**
+ * Updating children of a component may trigger recursive updates. The depth is
+ * used to batch recursive updates to render markup more efficiently.
+ *
+ * @type {number}
+ * @private
+ */
+var updateDepth = 0;
+
+/**
+ * Queue of update configuration objects.
+ *
+ * Each object has a `type` property that is in `ReactMultiChildUpdateTypes`.
+ *
+ * @type {array<object>}
+ * @private
+ */
+var updateQueue = [];
+
+/**
+ * Queue of markup to be rendered.
+ *
+ * @type {array<string>}
+ * @private
+ */
+var markupQueue = [];
+
+/**
+ * Enqueues markup to be rendered and inserted at a supplied index.
+ *
+ * @param {string} parentID ID of the parent component.
+ * @param {string} markup Markup that renders into an element.
+ * @param {number} toIndex Destination index.
+ * @private
+ */
+function enqueueInsertMarkup(parentID, markup, toIndex) {
+ // NOTE: Null values reduce hidden classes.
+ updateQueue.push({
+ parentID: parentID,
+ parentNode: null,
+ type: ReactMultiChildUpdateTypes.INSERT_MARKUP,
+ markupIndex: markupQueue.push(markup) - 1,
+ content: null,
+ fromIndex: null,
+ toIndex: toIndex
+ });
+}
+
+/**
+ * Enqueues moving an existing element to another index.
+ *
+ * @param {string} parentID ID of the parent component.
+ * @param {number} fromIndex Source index of the existing element.
+ * @param {number} toIndex Destination index of the element.
+ * @private
+ */
+function enqueueMove(parentID, fromIndex, toIndex) {
+ // NOTE: Null values reduce hidden classes.
+ updateQueue.push({
+ parentID: parentID,
+ parentNode: null,
+ type: ReactMultiChildUpdateTypes.MOVE_EXISTING,
+ markupIndex: null,
+ content: null,
+ fromIndex: fromIndex,
+ toIndex: toIndex
+ });
+}
+
+/**
+ * Enqueues removing an element at an index.
+ *
+ * @param {string} parentID ID of the parent component.
+ * @param {number} fromIndex Index of the element to remove.
+ * @private
+ */
+function enqueueRemove(parentID, fromIndex) {
+ // NOTE: Null values reduce hidden classes.
+ updateQueue.push({
+ parentID: parentID,
+ parentNode: null,
+ type: ReactMultiChildUpdateTypes.REMOVE_NODE,
+ markupIndex: null,
+ content: null,
+ fromIndex: fromIndex,
+ toIndex: null
+ });
+}
+
+/**
+ * Enqueues setting the markup of a node.
+ *
+ * @param {string} parentID ID of the parent component.
+ * @param {string} markup Markup that renders into an element.
+ * @private
+ */
+function enqueueSetMarkup(parentID, markup) {
+ // NOTE: Null values reduce hidden classes.
+ updateQueue.push({
+ parentID: parentID,
+ parentNode: null,
+ type: ReactMultiChildUpdateTypes.SET_MARKUP,
+ markupIndex: null,
+ content: markup,
+ fromIndex: null,
+ toIndex: null
+ });
+}
+
+/**
+ * Enqueues setting the text content.
+ *
+ * @param {string} parentID ID of the parent component.
+ * @param {string} textContent Text content to set.
+ * @private
+ */
+function enqueueTextContent(parentID, textContent) {
+ // NOTE: Null values reduce hidden classes.
+ updateQueue.push({
+ parentID: parentID,
+ parentNode: null,
+ type: ReactMultiChildUpdateTypes.TEXT_CONTENT,
+ markupIndex: null,
+ content: textContent,
+ fromIndex: null,
+ toIndex: null
+ });
+}
+
+/**
+ * Processes any enqueued updates.
+ *
+ * @private
+ */
+function processQueue() {
+ if (updateQueue.length) {
+ ReactComponentEnvironment.processChildrenUpdates(updateQueue, markupQueue);
+ clearQueue();
+ }
+}
+
+/**
+ * Clears any enqueued updates.
+ *
+ * @private
+ */
+function clearQueue() {
+ updateQueue.length = 0;
+ markupQueue.length = 0;
+}
+
+/**
+ * ReactMultiChild are capable of reconciling multiple children.
+ *
+ * @class ReactMultiChild
+ * @internal
+ */
+var ReactMultiChild = {
+
+ /**
+ * Provides common functionality for components that must reconcile multiple
+ * children. This is used by `ReactDOMComponent` to mount, update, and
+ * unmount child components.
+ *
+ * @lends {ReactMultiChild.prototype}
+ */
+ Mixin: {
+
+ _reconcilerInstantiateChildren: function (nestedChildren, transaction, context) {
+ if ("production" !== 'production') {
+ if (this._currentElement) {
+ try {
+ ReactCurrentOwner.current = this._currentElement._owner;
+ return ReactChildReconciler.instantiateChildren(nestedChildren, transaction, context);
+ } finally {
+ ReactCurrentOwner.current = null;
+ }
+ }
+ }
+ return ReactChildReconciler.instantiateChildren(nestedChildren, transaction, context);
+ },
+
+ _reconcilerUpdateChildren: function (prevChildren, nextNestedChildrenElements, transaction, context) {
+ var nextChildren;
+ if ("production" !== 'production') {
+ if (this._currentElement) {
+ try {
+ ReactCurrentOwner.current = this._currentElement._owner;
+ nextChildren = flattenChildren(nextNestedChildrenElements);
+ } finally {
+ ReactCurrentOwner.current = null;
+ }
+ return ReactChildReconciler.updateChildren(prevChildren, nextChildren, transaction, context);
+ }
+ }
+ nextChildren = flattenChildren(nextNestedChildrenElements);
+ return ReactChildReconciler.updateChildren(prevChildren, nextChildren, transaction, context);
+ },
+
+ /**
+ * Generates a "mount image" for each of the supplied children. In the case
+ * of `ReactDOMComponent`, a mount image is a string of markup.
+ *
+ * @param {?object} nestedChildren Nested child maps.
+ * @return {array} An array of mounted representations.
+ * @internal
+ */
+ mountChildren: function (nestedChildren, transaction, context) {
+ var children = this._reconcilerInstantiateChildren(nestedChildren, transaction, context);
+ this._renderedChildren = children;
+ var mountImages = [];
+ var index = 0;
+ for (var name in children) {
+ if (children.hasOwnProperty(name)) {
+ var child = children[name];
+ // Inlined for performance, see `ReactInstanceHandles.createReactID`.
+ var rootID = this._rootNodeID + name;
+ var mountImage = ReactReconciler.mountComponent(child, rootID, transaction, context);
+ child._mountIndex = index++;
+ mountImages.push(mountImage);
+ }
+ }
+ return mountImages;
+ },
+
+ /**
+ * Replaces any rendered children with a text content string.
+ *
+ * @param {string} nextContent String of content.
+ * @internal
+ */
+ updateTextContent: function (nextContent) {
+ updateDepth++;
+ var errorThrown = true;
+ try {
+ var prevChildren = this._renderedChildren;
+ // Remove any rendered children.
+ ReactChildReconciler.unmountChildren(prevChildren);
+ // TODO: The setTextContent operation should be enough
+ for (var name in prevChildren) {
+ if (prevChildren.hasOwnProperty(name)) {
+ this._unmountChild(prevChildren[name]);
+ }
+ }
+ // Set new text content.
+ this.setTextContent(nextContent);
+ errorThrown = false;
+ } finally {
+ updateDepth--;
+ if (!updateDepth) {
+ if (errorThrown) {
+ clearQueue();
+ } else {
+ processQueue();
+ }
+ }
+ }
+ },
+
+ /**
+ * Replaces any rendered children with a markup string.
+ *
+ * @param {string} nextMarkup String of markup.
+ * @internal
+ */
+ updateMarkup: function (nextMarkup) {
+ updateDepth++;
+ var errorThrown = true;
+ try {
+ var prevChildren = this._renderedChildren;
+ // Remove any rendered children.
+ ReactChildReconciler.unmountChildren(prevChildren);
+ for (var name in prevChildren) {
+ if (prevChildren.hasOwnProperty(name)) {
+ this._unmountChildByName(prevChildren[name], name);
+ }
+ }
+ this.setMarkup(nextMarkup);
+ errorThrown = false;
+ } finally {
+ updateDepth--;
+ if (!updateDepth) {
+ if (errorThrown) {
+ clearQueue();
+ } else {
+ processQueue();
+ }
+ }
+ }
+ },
+
+ /**
+ * Updates the rendered children with new children.
+ *
+ * @param {?object} nextNestedChildrenElements Nested child element maps.
+ * @param {ReactReconcileTransaction} transaction
+ * @internal
+ */
+ updateChildren: function (nextNestedChildrenElements, transaction, context) {
+ updateDepth++;
+ var errorThrown = true;
+ try {
+ this._updateChildren(nextNestedChildrenElements, transaction, context);
+ errorThrown = false;
+ } finally {
+ updateDepth--;
+ if (!updateDepth) {
+ if (errorThrown) {
+ clearQueue();
+ } else {
+ processQueue();
+ }
+ }
+ }
+ },
+
+ /**
+ * Improve performance by isolating this hot code path from the try/catch
+ * block in `updateChildren`.
+ *
+ * @param {?object} nextNestedChildrenElements Nested child element maps.
+ * @param {ReactReconcileTransaction} transaction
+ * @final
+ * @protected
+ */
+ _updateChildren: function (nextNestedChildrenElements, transaction, context) {
+ var prevChildren = this._renderedChildren;
+ var nextChildren = this._reconcilerUpdateChildren(prevChildren, nextNestedChildrenElements, transaction, context);
+ this._renderedChildren = nextChildren;
+ if (!nextChildren && !prevChildren) {
+ return;
+ }
+ var name;
+ // `nextIndex` will increment for each child in `nextChildren`, but
+ // `lastIndex` will be the last index visited in `prevChildren`.
+ var lastIndex = 0;
+ var nextIndex = 0;
+ for (name in nextChildren) {
+ if (!nextChildren.hasOwnProperty(name)) {
+ continue;
+ }
+ var prevChild = prevChildren && prevChildren[name];
+ var nextChild = nextChildren[name];
+ if (prevChild === nextChild) {
+ this.moveChild(prevChild, nextIndex, lastIndex);
+ lastIndex = Math.max(prevChild._mountIndex, lastIndex);
+ prevChild._mountIndex = nextIndex;
+ } else {
+ if (prevChild) {
+ // Update `lastIndex` before `_mountIndex` gets unset by unmounting.
+ lastIndex = Math.max(prevChild._mountIndex, lastIndex);
+ this._unmountChild(prevChild);
+ }
+ // The child must be instantiated before it's mounted.
+ this._mountChildByNameAtIndex(nextChild, name, nextIndex, transaction, context);
+ }
+ nextIndex++;
+ }
+ // Remove children that are no longer present.
+ for (name in prevChildren) {
+ if (prevChildren.hasOwnProperty(name) && !(nextChildren && nextChildren.hasOwnProperty(name))) {
+ this._unmountChild(prevChildren[name]);
+ }
+ }
+ },
+
+ /**
+ * Unmounts all rendered children. This should be used to clean up children
+ * when this component is unmounted.
+ *
+ * @internal
+ */
+ unmountChildren: function () {
+ var renderedChildren = this._renderedChildren;
+ ReactChildReconciler.unmountChildren(renderedChildren);
+ this._renderedChildren = null;
+ },
+
+ /**
+ * Moves a child component to the supplied index.
+ *
+ * @param {ReactComponent} child Component to move.
+ * @param {number} toIndex Destination index of the element.
+ * @param {number} lastIndex Last index visited of the siblings of `child`.
+ * @protected
+ */
+ moveChild: function (child, toIndex, lastIndex) {
+ // If the index of `child` is less than `lastIndex`, then it needs to
+ // be moved. Otherwise, we do not need to move it because a child will be
+ // inserted or moved before `child`.
+ if (child._mountIndex < lastIndex) {
+ enqueueMove(this._rootNodeID, child._mountIndex, toIndex);
+ }
+ },
+
+ /**
+ * Creates a child component.
+ *
+ * @param {ReactComponent} child Component to create.
+ * @param {string} mountImage Markup to insert.
+ * @protected
+ */
+ createChild: function (child, mountImage) {
+ enqueueInsertMarkup(this._rootNodeID, mountImage, child._mountIndex);
+ },
+
+ /**
+ * Removes a child component.
+ *
+ * @param {ReactComponent} child Child to remove.
+ * @protected
+ */
+ removeChild: function (child) {
+ enqueueRemove(this._rootNodeID, child._mountIndex);
+ },
+
+ /**
+ * Sets this text content string.
+ *
+ * @param {string} textContent Text content to set.
+ * @protected
+ */
+ setTextContent: function (textContent) {
+ enqueueTextContent(this._rootNodeID, textContent);
+ },
+
+ /**
+ * Sets this markup string.
+ *
+ * @param {string} markup Markup to set.
+ * @protected
+ */
+ setMarkup: function (markup) {
+ enqueueSetMarkup(this._rootNodeID, markup);
+ },
+
+ /**
+ * Mounts a child with the supplied name.
+ *
+ * NOTE: This is part of `updateChildren` and is here for readability.
+ *
+ * @param {ReactComponent} child Component to mount.
+ * @param {string} name Name of the child.
+ * @param {number} index Index at which to insert the child.
+ * @param {ReactReconcileTransaction} transaction
+ * @private
+ */
+ _mountChildByNameAtIndex: function (child, name, index, transaction, context) {
+ // Inlined for performance, see `ReactInstanceHandles.createReactID`.
+ var rootID = this._rootNodeID + name;
+ var mountImage = ReactReconciler.mountComponent(child, rootID, transaction, context);
+ child._mountIndex = index;
+ this.createChild(child, mountImage);
+ },
+
+ /**
+ * Unmounts a rendered child.
+ *
+ * NOTE: This is part of `updateChildren` and is here for readability.
+ *
+ * @param {ReactComponent} child Component to unmount.
+ * @private
+ */
+ _unmountChild: function (child) {
+ this.removeChild(child);
+ child._mountIndex = null;
+ }
+
+ }
+
+};
+
+module.exports = ReactMultiChild;
+},{"123":123,"31":31,"36":36,"39":39,"74":74,"84":84}],74:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactMultiChildUpdateTypes
+ */
+
+'use strict';
+
+var keyMirror = _dereq_(165);
+
+/**
+ * When a component's children are updated, a series of update configuration
+ * objects are created in order to batch and serialize the required changes.
+ *
+ * Enumerates all the possible types of update configurations.
+ *
+ * @internal
+ */
+var ReactMultiChildUpdateTypes = keyMirror({
+ INSERT_MARKUP: null,
+ MOVE_EXISTING: null,
+ REMOVE_NODE: null,
+ SET_MARKUP: null,
+ TEXT_CONTENT: null
+});
+
+module.exports = ReactMultiChildUpdateTypes;
+},{"165":165}],75:[function(_dereq_,module,exports){
+/**
+ * Copyright 2014-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactNativeComponent
+ */
+
+'use strict';
+
+var assign = _dereq_(24);
+var invariant = _dereq_(161);
+
+var autoGenerateWrapperClass = null;
+var genericComponentClass = null;
+// This registry keeps track of wrapper classes around native tags.
+var tagToComponentClass = {};
+var textComponentClass = null;
+
+var ReactNativeComponentInjection = {
+ // This accepts a class that receives the tag string. This is a catch all
+ // that can render any kind of tag.
+ injectGenericComponentClass: function (componentClass) {
+ genericComponentClass = componentClass;
+ },
+ // This accepts a text component class that takes the text string to be
+ // rendered as props.
+ injectTextComponentClass: function (componentClass) {
+ textComponentClass = componentClass;
+ },
+ // This accepts a keyed object with classes as values. Each key represents a
+ // tag. That particular tag will use this class instead of the generic one.
+ injectComponentClasses: function (componentClasses) {
+ assign(tagToComponentClass, componentClasses);
+ }
+};
+
+/**
+ * Get a composite component wrapper class for a specific tag.
+ *
+ * @param {ReactElement} element The tag for which to get the class.
+ * @return {function} The React class constructor function.
+ */
+function getComponentClassForElement(element) {
+ if (typeof element.type === 'function') {
+ return element.type;
+ }
+ var tag = element.type;
+ var componentClass = tagToComponentClass[tag];
+ if (componentClass == null) {
+ tagToComponentClass[tag] = componentClass = autoGenerateWrapperClass(tag);
+ }
+ return componentClass;
+}
+
+/**
+ * Get a native internal component class for a specific tag.
+ *
+ * @param {ReactElement} element The element to create.
+ * @return {function} The internal class constructor function.
+ */
+function createInternalComponent(element) {
+ !genericComponentClass ? "production" !== 'production' ? invariant(false, 'There is no registered component for the tag %s', element.type) : invariant(false) : undefined;
+ return new genericComponentClass(element.type, element.props);
+}
+
+/**
+ * @param {ReactText} text
+ * @return {ReactComponent}
+ */
+function createInstanceForText(text) {
+ return new textComponentClass(text);
+}
+
+/**
+ * @param {ReactComponent} component
+ * @return {boolean}
+ */
+function isTextComponent(component) {
+ return component instanceof textComponentClass;
+}
+
+var ReactNativeComponent = {
+ getComponentClassForElement: getComponentClassForElement,
+ createInternalComponent: createInternalComponent,
+ createInstanceForText: createInstanceForText,
+ isTextComponent: isTextComponent,
+ injection: ReactNativeComponentInjection
+};
+
+module.exports = ReactNativeComponent;
+},{"161":161,"24":24}],76:[function(_dereq_,module,exports){
+/**
+ * Copyright 2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactNoopUpdateQueue
+ */
+
+'use strict';
+
+var warning = _dereq_(173);
+
+function warnTDZ(publicInstance, callerName) {
+ if ("production" !== 'production') {
+ "production" !== 'production' ? warning(false, '%s(...): Can only update a mounted or mounting component. ' + 'This usually means you called %s() on an unmounted component. ' + 'This is a no-op. Please check the code for the %s component.', callerName, callerName, publicInstance.constructor && publicInstance.constructor.displayName || '') : undefined;
+ }
+}
+
+/**
+ * This is the abstract API for an update queue.
+ */
+var ReactNoopUpdateQueue = {
+
+ /**
+ * Checks whether or not this composite component is mounted.
+ * @param {ReactClass} publicInstance The instance we want to test.
+ * @return {boolean} True if mounted, false otherwise.
+ * @protected
+ * @final
+ */
+ isMounted: function (publicInstance) {
+ return false;
+ },
+
+ /**
+ * Enqueue a callback that will be executed after all the pending updates
+ * have processed.
+ *
+ * @param {ReactClass} publicInstance The instance to use as `this` context.
+ * @param {?function} callback Called after state is updated.
+ * @internal
+ */
+ enqueueCallback: function (publicInstance, callback) {},
+
+ /**
+ * Forces an update. This should only be invoked when it is known with
+ * certainty that we are **not** in a DOM transaction.
+ *
+ * You may want to call this when you know that some deeper aspect of the
+ * component's state has changed but `setState` was not called.
+ *
+ * This will not invoke `shouldComponentUpdate`, but it will invoke
+ * `componentWillUpdate` and `componentDidUpdate`.
+ *
+ * @param {ReactClass} publicInstance The instance that should rerender.
+ * @internal
+ */
+ enqueueForceUpdate: function (publicInstance) {
+ warnTDZ(publicInstance, 'forceUpdate');
+ },
+
+ /**
+ * Replaces all of the state. Always use this or `setState` to mutate state.
+ * You should treat `this.state` as immutable.
+ *
+ * There is no guarantee that `this.state` will be immediately updated, so
+ * accessing `this.state` after calling this method may return the old value.
+ *
+ * @param {ReactClass} publicInstance The instance that should rerender.
+ * @param {object} completeState Next state.
+ * @internal
+ */
+ enqueueReplaceState: function (publicInstance, completeState) {
+ warnTDZ(publicInstance, 'replaceState');
+ },
+
+ /**
+ * Sets a subset of the state. This only exists because _pendingState is
+ * internal. This provides a merging strategy that is not available to deep
+ * properties which is confusing. TODO: Expose pendingState or don't use it
+ * during the merge.
+ *
+ * @param {ReactClass} publicInstance The instance that should rerender.
+ * @param {object} partialState Next partial state to be merged with state.
+ * @internal
+ */
+ enqueueSetState: function (publicInstance, partialState) {
+ warnTDZ(publicInstance, 'setState');
+ },
+
+ /**
+ * Sets a subset of the props.
+ *
+ * @param {ReactClass} publicInstance The instance that should rerender.
+ * @param {object} partialProps Subset of the next props.
+ * @internal
+ */
+ enqueueSetProps: function (publicInstance, partialProps) {
+ warnTDZ(publicInstance, 'setProps');
+ },
+
+ /**
+ * Replaces all of the props.
+ *
+ * @param {ReactClass} publicInstance The instance that should rerender.
+ * @param {object} props New props.
+ * @internal
+ */
+ enqueueReplaceProps: function (publicInstance, props) {
+ warnTDZ(publicInstance, 'replaceProps');
+ }
+
+};
+
+module.exports = ReactNoopUpdateQueue;
+},{"173":173}],77:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactOwner
+ */
+
+'use strict';
+
+var invariant = _dereq_(161);
+
+/**
+ * ReactOwners are capable of storing references to owned components.
+ *
+ * All components are capable of //being// referenced by owner components, but
+ * only ReactOwner components are capable of //referencing// owned components.
+ * The named reference is known as a "ref".
+ *
+ * Refs are available when mounted and updated during reconciliation.
+ *
+ * var MyComponent = React.createClass({
+ * render: function() {
+ * return (
+ * <div onClick={this.handleClick}>
+ * <CustomComponent ref="custom" />
+ * </div>
+ * );
+ * },
+ * handleClick: function() {
+ * this.refs.custom.handleClick();
+ * },
+ * componentDidMount: function() {
+ * this.refs.custom.initialize();
+ * }
+ * });
+ *
+ * Refs should rarely be used. When refs are used, they should only be done to
+ * control data that is not handled by React's data flow.
+ *
+ * @class ReactOwner
+ */
+var ReactOwner = {
+
+ /**
+ * @param {?object} object
+ * @return {boolean} True if `object` is a valid owner.
+ * @final
+ */
+ isValidOwner: function (object) {
+ return !!(object && typeof object.attachRef === 'function' && typeof object.detachRef === 'function');
+ },
+
+ /**
+ * Adds a component by ref to an owner component.
+ *
+ * @param {ReactComponent} component Component to reference.
+ * @param {string} ref Name by which to refer to the component.
+ * @param {ReactOwner} owner Component on which to record the ref.
+ * @final
+ * @internal
+ */
+ addComponentAsRefTo: function (component, ref, owner) {
+ !ReactOwner.isValidOwner(owner) ? "production" !== 'production' ? invariant(false, 'addComponentAsRefTo(...): Only a ReactOwner can have refs. You might ' + 'be adding a ref to a component that was not created inside a component\'s ' + '`render` method, or you have multiple copies of React loaded ' + '(details: https://fb.me/react-refs-must-have-owner).') : invariant(false) : undefined;
+ owner.attachRef(ref, component);
+ },
+
+ /**
+ * Removes a component by ref from an owner component.
+ *
+ * @param {ReactComponent} component Component to dereference.
+ * @param {string} ref Name of the ref to remove.
+ * @param {ReactOwner} owner Component on which the ref is recorded.
+ * @final
+ * @internal
+ */
+ removeComponentAsRefFrom: function (component, ref, owner) {
+ !ReactOwner.isValidOwner(owner) ? "production" !== 'production' ? invariant(false, 'removeComponentAsRefFrom(...): Only a ReactOwner can have refs. You might ' + 'be removing a ref to a component that was not created inside a component\'s ' + '`render` method, or you have multiple copies of React loaded ' + '(details: https://fb.me/react-refs-must-have-owner).') : invariant(false) : undefined;
+ // Check that `component` is still the current ref because we do not want to
+ // detach the ref if another component stole it.
+ if (owner.getPublicInstance().refs[ref] === component.getPublicInstance()) {
+ owner.detachRef(ref);
+ }
+ }
+
+};
+
+module.exports = ReactOwner;
+},{"161":161}],78:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactPerf
+ * @typechecks static-only
+ */
+
+'use strict';
+
+/**
+ * ReactPerf is a general AOP system designed to measure performance. This
+ * module only has the hooks: see ReactDefaultPerf for the analysis tool.
+ */
+var ReactPerf = {
+ /**
+ * Boolean to enable/disable measurement. Set to false by default to prevent
+ * accidental logging and perf loss.
+ */
+ enableMeasure: false,
+
+ /**
+ * Holds onto the measure function in use. By default, don't measure
+ * anything, but we'll override this if we inject a measure function.
+ */
+ storedMeasure: _noMeasure,
+
+ /**
+ * @param {object} object
+ * @param {string} objectName
+ * @param {object<string>} methodNames
+ */
+ measureMethods: function (object, objectName, methodNames) {
+ if ("production" !== 'production') {
+ for (var key in methodNames) {
+ if (!methodNames.hasOwnProperty(key)) {
+ continue;
+ }
+ object[key] = ReactPerf.measure(objectName, methodNames[key], object[key]);
+ }
+ }
+ },
+
+ /**
+ * Use this to wrap methods you want to measure. Zero overhead in production.
+ *
+ * @param {string} objName
+ * @param {string} fnName
+ * @param {function} func
+ * @return {function}
+ */
+ measure: function (objName, fnName, func) {
+ if ("production" !== 'production') {
+ var measuredFunc = null;
+ var wrapper = function () {
+ if (ReactPerf.enableMeasure) {
+ if (!measuredFunc) {
+ measuredFunc = ReactPerf.storedMeasure(objName, fnName, func);
+ }
+ return measuredFunc.apply(this, arguments);
+ }
+ return func.apply(this, arguments);
+ };
+ wrapper.displayName = objName + '_' + fnName;
+ return wrapper;
+ }
+ return func;
+ },
+
+ injection: {
+ /**
+ * @param {function} measure
+ */
+ injectMeasure: function (measure) {
+ ReactPerf.storedMeasure = measure;
+ }
+ }
+};
+
+/**
+ * Simply passes through the measured function, without measuring it.
+ *
+ * @param {string} objName
+ * @param {string} fnName
+ * @param {function} func
+ * @return {function}
+ */
+function _noMeasure(objName, fnName, func) {
+ return func;
+}
+
+module.exports = ReactPerf;
+},{}],79:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactPropTransferer
+ */
+
+'use strict';
+
+var assign = _dereq_(24);
+var emptyFunction = _dereq_(153);
+var joinClasses = _dereq_(164);
+
+/**
+ * Creates a transfer strategy that will merge prop values using the supplied
+ * `mergeStrategy`. If a prop was previously unset, this just sets it.
+ *
+ * @param {function} mergeStrategy
+ * @return {function}
+ */
+function createTransferStrategy(mergeStrategy) {
+ return function (props, key, value) {
+ if (!props.hasOwnProperty(key)) {
+ props[key] = value;
+ } else {
+ props[key] = mergeStrategy(props[key], value);
+ }
+ };
+}
+
+var transferStrategyMerge = createTransferStrategy(function (a, b) {
+ // `merge` overrides the first object's (`props[key]` above) keys using the
+ // second object's (`value`) keys. An object's style's existing `propA` would
+ // get overridden. Flip the order here.
+ return assign({}, b, a);
+});
+
+/**
+ * Transfer strategies dictate how props are transferred by `transferPropsTo`.
+ * NOTE: if you add any more exceptions to this list you should be sure to
+ * update `cloneWithProps()` accordingly.
+ */
+var TransferStrategies = {
+ /**
+ * Never transfer `children`.
+ */
+ children: emptyFunction,
+ /**
+ * Transfer the `className` prop by merging them.
+ */
+ className: createTransferStrategy(joinClasses),
+ /**
+ * Transfer the `style` prop (which is an object) by merging them.
+ */
+ style: transferStrategyMerge
+};
+
+/**
+ * Mutates the first argument by transferring the properties from the second
+ * argument.
+ *
+ * @param {object} props
+ * @param {object} newProps
+ * @return {object}
+ */
+function transferInto(props, newProps) {
+ for (var thisKey in newProps) {
+ if (!newProps.hasOwnProperty(thisKey)) {
+ continue;
+ }
+
+ var transferStrategy = TransferStrategies[thisKey];
+
+ if (transferStrategy && TransferStrategies.hasOwnProperty(thisKey)) {
+ transferStrategy(props, thisKey, newProps[thisKey]);
+ } else if (!props.hasOwnProperty(thisKey)) {
+ props[thisKey] = newProps[thisKey];
+ }
+ }
+ return props;
+}
+
+/**
+ * ReactPropTransferer are capable of transferring props to another component
+ * using a `transferPropsTo` method.
+ *
+ * @class ReactPropTransferer
+ */
+var ReactPropTransferer = {
+
+ /**
+ * Merge two props objects using TransferStrategies.
+ *
+ * @param {object} oldProps original props (they take precedence)
+ * @param {object} newProps new props to merge in
+ * @return {object} a new object containing both sets of props merged.
+ */
+ mergeProps: function (oldProps, newProps) {
+ return transferInto(assign({}, oldProps), newProps);
+ }
+
+};
+
+module.exports = ReactPropTransferer;
+},{"153":153,"164":164,"24":24}],80:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactPropTypeLocationNames
+ */
+
+'use strict';
+
+var ReactPropTypeLocationNames = {};
+
+if ("production" !== 'production') {
+ ReactPropTypeLocationNames = {
+ prop: 'prop',
+ context: 'context',
+ childContext: 'child context'
+ };
+}
+
+module.exports = ReactPropTypeLocationNames;
+},{}],81:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactPropTypeLocations
+ */
+
+'use strict';
+
+var keyMirror = _dereq_(165);
+
+var ReactPropTypeLocations = keyMirror({
+ prop: null,
+ context: null,
+ childContext: null
+});
+
+module.exports = ReactPropTypeLocations;
+},{"165":165}],82:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactPropTypes
+ */
+
+'use strict';
+
+var ReactElement = _dereq_(57);
+var ReactPropTypeLocationNames = _dereq_(80);
+
+var emptyFunction = _dereq_(153);
+var getIteratorFn = _dereq_(129);
+
+/**
+ * Collection of methods that allow declaration and validation of props that are
+ * supplied to React components. Example usage:
+ *
+ * var Props = require('ReactPropTypes');
+ * var MyArticle = React.createClass({
+ * propTypes: {
+ * // An optional string prop named "description".
+ * description: Props.string,
+ *
+ * // A required enum prop named "category".
+ * category: Props.oneOf(['News','Photos']).isRequired,
+ *
+ * // A prop named "dialog" that requires an instance of Dialog.
+ * dialog: Props.instanceOf(Dialog).isRequired
+ * },
+ * render: function() { ... }
+ * });
+ *
+ * A more formal specification of how these methods are used:
+ *
+ * type := array|bool|func|object|number|string|oneOf([...])|instanceOf(...)
+ * decl := ReactPropTypes.{type}(.isRequired)?
+ *
+ * Each and every declaration produces a function with the same signature. This
+ * allows the creation of custom validation functions. For example:
+ *
+ * var MyLink = React.createClass({
+ * propTypes: {
+ * // An optional string or URI prop named "href".
+ * href: function(props, propName, componentName) {
+ * var propValue = props[propName];
+ * if (propValue != null && typeof propValue !== 'string' &&
+ * !(propValue instanceof URI)) {
+ * return new Error(
+ * 'Expected a string or an URI for ' + propName + ' in ' +
+ * componentName
+ * );
+ * }
+ * }
+ * },
+ * render: function() {...}
+ * });
+ *
+ * @internal
+ */
+
+var ANONYMOUS = '<<anonymous>>';
+
+var ReactPropTypes = {
+ array: createPrimitiveTypeChecker('array'),
+ bool: createPrimitiveTypeChecker('boolean'),
+ func: createPrimitiveTypeChecker('function'),
+ number: createPrimitiveTypeChecker('number'),
+ object: createPrimitiveTypeChecker('object'),
+ string: createPrimitiveTypeChecker('string'),
+
+ any: createAnyTypeChecker(),
+ arrayOf: createArrayOfTypeChecker,
+ element: createElementTypeChecker(),
+ instanceOf: createInstanceTypeChecker,
+ node: createNodeChecker(),
+ objectOf: createObjectOfTypeChecker,
+ oneOf: createEnumTypeChecker,
+ oneOfType: createUnionTypeChecker,
+ shape: createShapeTypeChecker
+};
+
+function createChainableTypeChecker(validate) {
+ function checkType(isRequired, props, propName, componentName, location, propFullName) {
+ componentName = componentName || ANONYMOUS;
+ propFullName = propFullName || propName;
+ if (props[propName] == null) {
+ var locationName = ReactPropTypeLocationNames[location];
+ if (isRequired) {
+ return new Error('Required ' + locationName + ' `' + propFullName + '` was not specified in ' + ('`' + componentName + '`.'));
+ }
+ return null;
+ } else {
+ return validate(props, propName, componentName, location, propFullName);
+ }
+ }
+
+ var chainedCheckType = checkType.bind(null, false);
+ chainedCheckType.isRequired = checkType.bind(null, true);
+
+ return chainedCheckType;
+}
+
+function createPrimitiveTypeChecker(expectedType) {
+ function validate(props, propName, componentName, location, propFullName) {
+ var propValue = props[propName];
+ var propType = getPropType(propValue);
+ if (propType !== expectedType) {
+ var locationName = ReactPropTypeLocationNames[location];
+ // `propValue` being instance of, say, date/regexp, pass the 'object'
+ // check, but we can offer a more precise error message here rather than
+ // 'of type `object`'.
+ var preciseType = getPreciseType(propValue);
+
+ return new Error('Invalid ' + locationName + ' `' + propFullName + '` of type ' + ('`' + preciseType + '` supplied to `' + componentName + '`, expected ') + ('`' + expectedType + '`.'));
+ }
+ return null;
+ }
+ return createChainableTypeChecker(validate);
+}
+
+function createAnyTypeChecker() {
+ return createChainableTypeChecker(emptyFunction.thatReturns(null));
+}
+
+function createArrayOfTypeChecker(typeChecker) {
+ function validate(props, propName, componentName, location, propFullName) {
+ var propValue = props[propName];
+ if (!Array.isArray(propValue)) {
+ var locationName = ReactPropTypeLocationNames[location];
+ var propType = getPropType(propValue);
+ return new Error('Invalid ' + locationName + ' `' + propFullName + '` of type ' + ('`' + propType + '` supplied to `' + componentName + '`, expected an array.'));
+ }
+ for (var i = 0; i < propValue.length; i++) {
+ var error = typeChecker(propValue, i, componentName, location, propFullName + '[' + i + ']');
+ if (error instanceof Error) {
+ return error;
+ }
+ }
+ return null;
+ }
+ return createChainableTypeChecker(validate);
+}
+
+function createElementTypeChecker() {
+ function validate(props, propName, componentName, location, propFullName) {
+ if (!ReactElement.isValidElement(props[propName])) {
+ var locationName = ReactPropTypeLocationNames[location];
+ return new Error('Invalid ' + locationName + ' `' + propFullName + '` supplied to ' + ('`' + componentName + '`, expected a single ReactElement.'));
+ }
+ return null;
+ }
+ return createChainableTypeChecker(validate);
+}
+
+function createInstanceTypeChecker(expectedClass) {
+ function validate(props, propName, componentName, location, propFullName) {
+ if (!(props[propName] instanceof expectedClass)) {
+ var locationName = ReactPropTypeLocationNames[location];
+ var expectedClassName = expectedClass.name || ANONYMOUS;
+ var actualClassName = getClassName(props[propName]);
+ return new Error('Invalid ' + locationName + ' `' + propFullName + '` of type ' + ('`' + actualClassName + '` supplied to `' + componentName + '`, expected ') + ('instance of `' + expectedClassName + '`.'));
+ }
+ return null;
+ }
+ return createChainableTypeChecker(validate);
+}
+
+function createEnumTypeChecker(expectedValues) {
+ if (!Array.isArray(expectedValues)) {
+ return createChainableTypeChecker(function () {
+ return new Error('Invalid argument supplied to oneOf, expected an instance of array.');
+ });
+ }
+
+ function validate(props, propName, componentName, location, propFullName) {
+ var propValue = props[propName];
+ for (var i = 0; i < expectedValues.length; i++) {
+ if (propValue === expectedValues[i]) {
+ return null;
+ }
+ }
+
+ var locationName = ReactPropTypeLocationNames[location];
+ var valuesString = JSON.stringify(expectedValues);
+ return new Error('Invalid ' + locationName + ' `' + propFullName + '` of value `' + propValue + '` ' + ('supplied to `' + componentName + '`, expected one of ' + valuesString + '.'));
+ }
+ return createChainableTypeChecker(validate);
+}
+
+function createObjectOfTypeChecker(typeChecker) {
+ function validate(props, propName, componentName, location, propFullName) {
+ var propValue = props[propName];
+ var propType = getPropType(propValue);
+ if (propType !== 'object') {
+ var locationName = ReactPropTypeLocationNames[location];
+ return new Error('Invalid ' + locationName + ' `' + propFullName + '` of type ' + ('`' + propType + '` supplied to `' + componentName + '`, expected an object.'));
+ }
+ for (var key in propValue) {
+ if (propValue.hasOwnProperty(key)) {
+ var error = typeChecker(propValue, key, componentName, location, propFullName + '.' + key);
+ if (error instanceof Error) {
+ return error;
+ }
+ }
+ }
+ return null;
+ }
+ return createChainableTypeChecker(validate);
+}
+
+function createUnionTypeChecker(arrayOfTypeCheckers) {
+ if (!Array.isArray(arrayOfTypeCheckers)) {
+ return createChainableTypeChecker(function () {
+ return new Error('Invalid argument supplied to oneOfType, expected an instance of array.');
+ });
+ }
+
+ function validate(props, propName, componentName, location, propFullName) {
+ for (var i = 0; i < arrayOfTypeCheckers.length; i++) {
+ var checker = arrayOfTypeCheckers[i];
+ if (checker(props, propName, componentName, location, propFullName) == null) {
+ return null;
+ }
+ }
+
+ var locationName = ReactPropTypeLocationNames[location];
+ return new Error('Invalid ' + locationName + ' `' + propFullName + '` supplied to ' + ('`' + componentName + '`.'));
+ }
+ return createChainableTypeChecker(validate);
+}
+
+function createNodeChecker() {
+ function validate(props, propName, componentName, location, propFullName) {
+ if (!isNode(props[propName])) {
+ var locationName = ReactPropTypeLocationNames[location];
+ return new Error('Invalid ' + locationName + ' `' + propFullName + '` supplied to ' + ('`' + componentName + '`, expected a ReactNode.'));
+ }
+ return null;
+ }
+ return createChainableTypeChecker(validate);
+}
+
+function createShapeTypeChecker(shapeTypes) {
+ function validate(props, propName, componentName, location, propFullName) {
+ var propValue = props[propName];
+ var propType = getPropType(propValue);
+ if (propType !== 'object') {
+ var locationName = ReactPropTypeLocationNames[location];
+ return new Error('Invalid ' + locationName + ' `' + propFullName + '` of type `' + propType + '` ' + ('supplied to `' + componentName + '`, expected `object`.'));
+ }
+ for (var key in shapeTypes) {
+ var checker = shapeTypes[key];
+ if (!checker) {
+ continue;
+ }
+ var error = checker(propValue, key, componentName, location, propFullName + '.' + key);
+ if (error) {
+ return error;
+ }
+ }
+ return null;
+ }
+ return createChainableTypeChecker(validate);
+}
+
+function isNode(propValue) {
+ switch (typeof propValue) {
+ case 'number':
+ case 'string':
+ case 'undefined':
+ return true;
+ case 'boolean':
+ return !propValue;
+ case 'object':
+ if (Array.isArray(propValue)) {
+ return propValue.every(isNode);
+ }
+ if (propValue === null || ReactElement.isValidElement(propValue)) {
+ return true;
+ }
+
+ var iteratorFn = getIteratorFn(propValue);
+ if (iteratorFn) {
+ var iterator = iteratorFn.call(propValue);
+ var step;
+ if (iteratorFn !== propValue.entries) {
+ while (!(step = iterator.next()).done) {
+ if (!isNode(step.value)) {
+ return false;
+ }
+ }
+ } else {
+ // Iterator will provide entry [k,v] tuples rather than values.
+ while (!(step = iterator.next()).done) {
+ var entry = step.value;
+ if (entry) {
+ if (!isNode(entry[1])) {
+ return false;
+ }
+ }
+ }
+ }
+ } else {
+ return false;
+ }
+
+ return true;
+ default:
+ return false;
+ }
+}
+
+// Equivalent of `typeof` but with special handling for array and regexp.
+function getPropType(propValue) {
+ var propType = typeof propValue;
+ if (Array.isArray(propValue)) {
+ return 'array';
+ }
+ if (propValue instanceof RegExp) {
+ // Old webkits (at least until Android 4.0) return 'function' rather than
+ // 'object' for typeof a RegExp. We'll normalize this here so that /bla/
+ // passes PropTypes.object.
+ return 'object';
+ }
+ return propType;
+}
+
+// This handles more types than `getPropType`. Only used for error messages.
+// See `createPrimitiveTypeChecker`.
+function getPreciseType(propValue) {
+ var propType = getPropType(propValue);
+ if (propType === 'object') {
+ if (propValue instanceof Date) {
+ return 'date';
+ } else if (propValue instanceof RegExp) {
+ return 'regexp';
+ }
+ }
+ return propType;
+}
+
+// Returns class name of the object, if any.
+function getClassName(propValue) {
+ if (!propValue.constructor || !propValue.constructor.name) {
+ return '<<anonymous>>';
+ }
+ return propValue.constructor.name;
+}
+
+module.exports = ReactPropTypes;
+},{"129":129,"153":153,"57":57,"80":80}],83:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactReconcileTransaction
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var CallbackQueue = _dereq_(6);
+var PooledClass = _dereq_(25);
+var ReactBrowserEventEmitter = _dereq_(28);
+var ReactDOMFeatureFlags = _dereq_(44);
+var ReactInputSelection = _dereq_(66);
+var Transaction = _dereq_(113);
+
+var assign = _dereq_(24);
+
+/**
+ * Ensures that, when possible, the selection range (currently selected text
+ * input) is not disturbed by performing the transaction.
+ */
+var SELECTION_RESTORATION = {
+ /**
+ * @return {Selection} Selection information.
+ */
+ initialize: ReactInputSelection.getSelectionInformation,
+ /**
+ * @param {Selection} sel Selection information returned from `initialize`.
+ */
+ close: ReactInputSelection.restoreSelection
+};
+
+/**
+ * Suppresses events (blur/focus) that could be inadvertently dispatched due to
+ * high level DOM manipulations (like temporarily removing a text input from the
+ * DOM).
+ */
+var EVENT_SUPPRESSION = {
+ /**
+ * @return {boolean} The enabled status of `ReactBrowserEventEmitter` before
+ * the reconciliation.
+ */
+ initialize: function () {
+ var currentlyEnabled = ReactBrowserEventEmitter.isEnabled();
+ ReactBrowserEventEmitter.setEnabled(false);
+ return currentlyEnabled;
+ },
+
+ /**
+ * @param {boolean} previouslyEnabled Enabled status of
+ * `ReactBrowserEventEmitter` before the reconciliation occurred. `close`
+ * restores the previous value.
+ */
+ close: function (previouslyEnabled) {
+ ReactBrowserEventEmitter.setEnabled(previouslyEnabled);
+ }
+};
+
+/**
+ * Provides a queue for collecting `componentDidMount` and
+ * `componentDidUpdate` callbacks during the the transaction.
+ */
+var ON_DOM_READY_QUEUEING = {
+ /**
+ * Initializes the internal `onDOMReady` queue.
+ */
+ initialize: function () {
+ this.reactMountReady.reset();
+ },
+
+ /**
+ * After DOM is flushed, invoke all registered `onDOMReady` callbacks.
+ */
+ close: function () {
+ this.reactMountReady.notifyAll();
+ }
+};
+
+/**
+ * Executed within the scope of the `Transaction` instance. Consider these as
+ * being member methods, but with an implied ordering while being isolated from
+ * each other.
+ */
+var TRANSACTION_WRAPPERS = [SELECTION_RESTORATION, EVENT_SUPPRESSION, ON_DOM_READY_QUEUEING];
+
+/**
+ * Currently:
+ * - The order that these are listed in the transaction is critical:
+ * - Suppresses events.
+ * - Restores selection range.
+ *
+ * Future:
+ * - Restore document/overflow scroll positions that were unintentionally
+ * modified via DOM insertions above the top viewport boundary.
+ * - Implement/integrate with customized constraint based layout system and keep
+ * track of which dimensions must be remeasured.
+ *
+ * @class ReactReconcileTransaction
+ */
+function ReactReconcileTransaction(forceHTML) {
+ this.reinitializeTransaction();
+ // Only server-side rendering really needs this option (see
+ // `ReactServerRendering`), but server-side uses
+ // `ReactServerRenderingTransaction` instead. This option is here so that it's
+ // accessible and defaults to false when `ReactDOMComponent` and
+ // `ReactTextComponent` checks it in `mountComponent`.`
+ this.renderToStaticMarkup = false;
+ this.reactMountReady = CallbackQueue.getPooled(null);
+ this.useCreateElement = !forceHTML && ReactDOMFeatureFlags.useCreateElement;
+}
+
+var Mixin = {
+ /**
+ * @see Transaction
+ * @abstract
+ * @final
+ * @return {array<object>} List of operation wrap procedures.
+ * TODO: convert to array<TransactionWrapper>
+ */
+ getTransactionWrappers: function () {
+ return TRANSACTION_WRAPPERS;
+ },
+
+ /**
+ * @return {object} The queue to collect `onDOMReady` callbacks with.
+ */
+ getReactMountReady: function () {
+ return this.reactMountReady;
+ },
+
+ /**
+ * `PooledClass` looks for this, and will invoke this before allowing this
+ * instance to be reused.
+ */
+ destructor: function () {
+ CallbackQueue.release(this.reactMountReady);
+ this.reactMountReady = null;
+ }
+};
+
+assign(ReactReconcileTransaction.prototype, Transaction.Mixin, Mixin);
+
+PooledClass.addPoolingTo(ReactReconcileTransaction);
+
+module.exports = ReactReconcileTransaction;
+},{"113":113,"24":24,"25":25,"28":28,"44":44,"6":6,"66":66}],84:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactReconciler
+ */
+
+'use strict';
+
+var ReactRef = _dereq_(85);
+
+/**
+ * Helper to call ReactRef.attachRefs with this composite component, split out
+ * to avoid allocations in the transaction mount-ready queue.
+ */
+function attachRefs() {
+ ReactRef.attachRefs(this, this._currentElement);
+}
+
+var ReactReconciler = {
+
+ /**
+ * Initializes the component, renders markup, and registers event listeners.
+ *
+ * @param {ReactComponent} internalInstance
+ * @param {string} rootID DOM ID of the root node.
+ * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
+ * @return {?string} Rendered markup to be inserted into the DOM.
+ * @final
+ * @internal
+ */
+ mountComponent: function (internalInstance, rootID, transaction, context) {
+ var markup = internalInstance.mountComponent(rootID, transaction, context);
+ if (internalInstance._currentElement && internalInstance._currentElement.ref != null) {
+ transaction.getReactMountReady().enqueue(attachRefs, internalInstance);
+ }
+ return markup;
+ },
+
+ /**
+ * Releases any resources allocated by `mountComponent`.
+ *
+ * @final
+ * @internal
+ */
+ unmountComponent: function (internalInstance) {
+ ReactRef.detachRefs(internalInstance, internalInstance._currentElement);
+ internalInstance.unmountComponent();
+ },
+
+ /**
+ * Update a component using a new element.
+ *
+ * @param {ReactComponent} internalInstance
+ * @param {ReactElement} nextElement
+ * @param {ReactReconcileTransaction} transaction
+ * @param {object} context
+ * @internal
+ */
+ receiveComponent: function (internalInstance, nextElement, transaction, context) {
+ var prevElement = internalInstance._currentElement;
+
+ if (nextElement === prevElement && context === internalInstance._context) {
+ // Since elements are immutable after the owner is rendered,
+ // we can do a cheap identity compare here to determine if this is a
+ // superfluous reconcile. It's possible for state to be mutable but such
+ // change should trigger an update of the owner which would recreate
+ // the element. We explicitly check for the existence of an owner since
+ // it's possible for an element created outside a composite to be
+ // deeply mutated and reused.
+
+ // TODO: Bailing out early is just a perf optimization right?
+ // TODO: Removing the return statement should affect correctness?
+ return;
+ }
+
+ var refsChanged = ReactRef.shouldUpdateRefs(prevElement, nextElement);
+
+ if (refsChanged) {
+ ReactRef.detachRefs(internalInstance, prevElement);
+ }
+
+ internalInstance.receiveComponent(nextElement, transaction, context);
+
+ if (refsChanged && internalInstance._currentElement && internalInstance._currentElement.ref != null) {
+ transaction.getReactMountReady().enqueue(attachRefs, internalInstance);
+ }
+ },
+
+ /**
+ * Flush any dirty changes in a component.
+ *
+ * @param {ReactComponent} internalInstance
+ * @param {ReactReconcileTransaction} transaction
+ * @internal
+ */
+ performUpdateIfNecessary: function (internalInstance, transaction) {
+ internalInstance.performUpdateIfNecessary(transaction);
+ }
+
+};
+
+module.exports = ReactReconciler;
+},{"85":85}],85:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactRef
+ */
+
+'use strict';
+
+var ReactOwner = _dereq_(77);
+
+var ReactRef = {};
+
+function attachRef(ref, component, owner) {
+ if (typeof ref === 'function') {
+ ref(component.getPublicInstance());
+ } else {
+ // Legacy ref
+ ReactOwner.addComponentAsRefTo(component, ref, owner);
+ }
+}
+
+function detachRef(ref, component, owner) {
+ if (typeof ref === 'function') {
+ ref(null);
+ } else {
+ // Legacy ref
+ ReactOwner.removeComponentAsRefFrom(component, ref, owner);
+ }
+}
+
+ReactRef.attachRefs = function (instance, element) {
+ if (element === null || element === false) {
+ return;
+ }
+ var ref = element.ref;
+ if (ref != null) {
+ attachRef(ref, instance, element._owner);
+ }
+};
+
+ReactRef.shouldUpdateRefs = function (prevElement, nextElement) {
+ // If either the owner or a `ref` has changed, make sure the newest owner
+ // has stored a reference to `this`, and the previous owner (if different)
+ // has forgotten the reference to `this`. We use the element instead
+ // of the public this.props because the post processing cannot determine
+ // a ref. The ref conceptually lives on the element.
+
+ // TODO: Should this even be possible? The owner cannot change because
+ // it's forbidden by shouldUpdateReactComponent. The ref can change
+ // if you swap the keys of but not the refs. Reconsider where this check
+ // is made. It probably belongs where the key checking and
+ // instantiateReactComponent is done.
+
+ var prevEmpty = prevElement === null || prevElement === false;
+ var nextEmpty = nextElement === null || nextElement === false;
+
+ return(
+ // This has a few false positives w/r/t empty components.
+ prevEmpty || nextEmpty || nextElement._owner !== prevElement._owner || nextElement.ref !== prevElement.ref
+ );
+};
+
+ReactRef.detachRefs = function (instance, element) {
+ if (element === null || element === false) {
+ return;
+ }
+ var ref = element.ref;
+ if (ref != null) {
+ detachRef(ref, instance, element._owner);
+ }
+};
+
+module.exports = ReactRef;
+},{"77":77}],86:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactRootIndex
+ * @typechecks
+ */
+
+'use strict';
+
+var ReactRootIndexInjection = {
+ /**
+ * @param {function} _createReactRootIndex
+ */
+ injectCreateReactRootIndex: function (_createReactRootIndex) {
+ ReactRootIndex.createReactRootIndex = _createReactRootIndex;
+ }
+};
+
+var ReactRootIndex = {
+ createReactRootIndex: null,
+ injection: ReactRootIndexInjection
+};
+
+module.exports = ReactRootIndex;
+},{}],87:[function(_dereq_,module,exports){
+/**
+ * Copyright 2014-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactServerBatchingStrategy
+ * @typechecks
+ */
+
+'use strict';
+
+var ReactServerBatchingStrategy = {
+ isBatchingUpdates: false,
+ batchedUpdates: function (callback) {
+ // Don't do anything here. During the server rendering we don't want to
+ // schedule any updates. We will simply ignore them.
+ }
+};
+
+module.exports = ReactServerBatchingStrategy;
+},{}],88:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @typechecks static-only
+ * @providesModule ReactServerRendering
+ */
+'use strict';
+
+var ReactDefaultBatchingStrategy = _dereq_(53);
+var ReactElement = _dereq_(57);
+var ReactInstanceHandles = _dereq_(67);
+var ReactMarkupChecksum = _dereq_(71);
+var ReactServerBatchingStrategy = _dereq_(87);
+var ReactServerRenderingTransaction = _dereq_(89);
+var ReactUpdates = _dereq_(96);
+
+var emptyObject = _dereq_(154);
+var instantiateReactComponent = _dereq_(132);
+var invariant = _dereq_(161);
+
+/**
+ * @param {ReactElement} element
+ * @return {string} the HTML markup
+ */
+function renderToString(element) {
+ !ReactElement.isValidElement(element) ? "production" !== 'production' ? invariant(false, 'renderToString(): You must pass a valid ReactElement.') : invariant(false) : undefined;
+
+ var transaction;
+ try {
+ ReactUpdates.injection.injectBatchingStrategy(ReactServerBatchingStrategy);
+
+ var id = ReactInstanceHandles.createReactRootID();
+ transaction = ReactServerRenderingTransaction.getPooled(false);
+
+ return transaction.perform(function () {
+ var componentInstance = instantiateReactComponent(element, null);
+ var markup = componentInstance.mountComponent(id, transaction, emptyObject);
+ return ReactMarkupChecksum.addChecksumToMarkup(markup);
+ }, null);
+ } finally {
+ ReactServerRenderingTransaction.release(transaction);
+ // Revert to the DOM batching strategy since these two renderers
+ // currently share these stateful modules.
+ ReactUpdates.injection.injectBatchingStrategy(ReactDefaultBatchingStrategy);
+ }
+}
+
+/**
+ * @param {ReactElement} element
+ * @return {string} the HTML markup, without the extra React ID and checksum
+ * (for generating static pages)
+ */
+function renderToStaticMarkup(element) {
+ !ReactElement.isValidElement(element) ? "production" !== 'production' ? invariant(false, 'renderToStaticMarkup(): You must pass a valid ReactElement.') : invariant(false) : undefined;
+
+ var transaction;
+ try {
+ ReactUpdates.injection.injectBatchingStrategy(ReactServerBatchingStrategy);
+
+ var id = ReactInstanceHandles.createReactRootID();
+ transaction = ReactServerRenderingTransaction.getPooled(true);
+
+ return transaction.perform(function () {
+ var componentInstance = instantiateReactComponent(element, null);
+ return componentInstance.mountComponent(id, transaction, emptyObject);
+ }, null);
+ } finally {
+ ReactServerRenderingTransaction.release(transaction);
+ // Revert to the DOM batching strategy since these two renderers
+ // currently share these stateful modules.
+ ReactUpdates.injection.injectBatchingStrategy(ReactDefaultBatchingStrategy);
+ }
+}
+
+module.exports = {
+ renderToString: renderToString,
+ renderToStaticMarkup: renderToStaticMarkup
+};
+},{"132":132,"154":154,"161":161,"53":53,"57":57,"67":67,"71":71,"87":87,"89":89,"96":96}],89:[function(_dereq_,module,exports){
+/**
+ * Copyright 2014-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactServerRenderingTransaction
+ * @typechecks
+ */
+
+'use strict';
+
+var PooledClass = _dereq_(25);
+var CallbackQueue = _dereq_(6);
+var Transaction = _dereq_(113);
+
+var assign = _dereq_(24);
+var emptyFunction = _dereq_(153);
+
+/**
+ * Provides a `CallbackQueue` queue for collecting `onDOMReady` callbacks
+ * during the performing of the transaction.
+ */
+var ON_DOM_READY_QUEUEING = {
+ /**
+ * Initializes the internal `onDOMReady` queue.
+ */
+ initialize: function () {
+ this.reactMountReady.reset();
+ },
+
+ close: emptyFunction
+};
+
+/**
+ * Executed within the scope of the `Transaction` instance. Consider these as
+ * being member methods, but with an implied ordering while being isolated from
+ * each other.
+ */
+var TRANSACTION_WRAPPERS = [ON_DOM_READY_QUEUEING];
+
+/**
+ * @class ReactServerRenderingTransaction
+ * @param {boolean} renderToStaticMarkup
+ */
+function ReactServerRenderingTransaction(renderToStaticMarkup) {
+ this.reinitializeTransaction();
+ this.renderToStaticMarkup = renderToStaticMarkup;
+ this.reactMountReady = CallbackQueue.getPooled(null);
+ this.useCreateElement = false;
+}
+
+var Mixin = {
+ /**
+ * @see Transaction
+ * @abstract
+ * @final
+ * @return {array} Empty list of operation wrap procedures.
+ */
+ getTransactionWrappers: function () {
+ return TRANSACTION_WRAPPERS;
+ },
+
+ /**
+ * @return {object} The queue to collect `onDOMReady` callbacks with.
+ */
+ getReactMountReady: function () {
+ return this.reactMountReady;
+ },
+
+ /**
+ * `PooledClass` looks for this, and will invoke this before allowing this
+ * instance to be reused.
+ */
+ destructor: function () {
+ CallbackQueue.release(this.reactMountReady);
+ this.reactMountReady = null;
+ }
+};
+
+assign(ReactServerRenderingTransaction.prototype, Transaction.Mixin, Mixin);
+
+PooledClass.addPoolingTo(ReactServerRenderingTransaction);
+
+module.exports = ReactServerRenderingTransaction;
+},{"113":113,"153":153,"24":24,"25":25,"6":6}],90:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactStateSetters
+ */
+
+'use strict';
+
+var ReactStateSetters = {
+ /**
+ * Returns a function that calls the provided function, and uses the result
+ * of that to set the component's state.
+ *
+ * @param {ReactCompositeComponent} component
+ * @param {function} funcReturningState Returned callback uses this to
+ * determine how to update state.
+ * @return {function} callback that when invoked uses funcReturningState to
+ * determined the object literal to setState.
+ */
+ createStateSetter: function (component, funcReturningState) {
+ return function (a, b, c, d, e, f) {
+ var partialState = funcReturningState.call(component, a, b, c, d, e, f);
+ if (partialState) {
+ component.setState(partialState);
+ }
+ };
+ },
+
+ /**
+ * Returns a single-argument callback that can be used to update a single
+ * key in the component's state.
+ *
+ * Note: this is memoized function, which makes it inexpensive to call.
+ *
+ * @param {ReactCompositeComponent} component
+ * @param {string} key The key in the state that you should update.
+ * @return {function} callback of 1 argument which calls setState() with
+ * the provided keyName and callback argument.
+ */
+ createStateKeySetter: function (component, key) {
+ // Memoize the setters.
+ var cache = component.__keySetters || (component.__keySetters = {});
+ return cache[key] || (cache[key] = createStateKeySetter(component, key));
+ }
+};
+
+function createStateKeySetter(component, key) {
+ // Partial state is allocated outside of the function closure so it can be
+ // reused with every call, avoiding memory allocation when this function
+ // is called.
+ var partialState = {};
+ return function stateKeySetter(value) {
+ partialState[key] = value;
+ component.setState(partialState);
+ };
+}
+
+ReactStateSetters.Mixin = {
+ /**
+ * Returns a function that calls the provided function, and uses the result
+ * of that to set the component's state.
+ *
+ * For example, these statements are equivalent:
+ *
+ * this.setState({x: 1});
+ * this.createStateSetter(function(xValue) {
+ * return {x: xValue};
+ * })(1);
+ *
+ * @param {function} funcReturningState Returned callback uses this to
+ * determine how to update state.
+ * @return {function} callback that when invoked uses funcReturningState to
+ * determined the object literal to setState.
+ */
+ createStateSetter: function (funcReturningState) {
+ return ReactStateSetters.createStateSetter(this, funcReturningState);
+ },
+
+ /**
+ * Returns a single-argument callback that can be used to update a single
+ * key in the component's state.
+ *
+ * For example, these statements are equivalent:
+ *
+ * this.setState({x: 1});
+ * this.createStateKeySetter('x')(1);
+ *
+ * Note: this is memoized function, which makes it inexpensive to call.
+ *
+ * @param {string} key The key in the state that you should update.
+ * @return {function} callback of 1 argument which calls setState() with
+ * the provided keyName and callback argument.
+ */
+ createStateKeySetter: function (key) {
+ return ReactStateSetters.createStateKeySetter(this, key);
+ }
+};
+
+module.exports = ReactStateSetters;
+},{}],91:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactTestUtils
+ */
+
+'use strict';
+
+var EventConstants = _dereq_(15);
+var EventPluginHub = _dereq_(16);
+var EventPropagators = _dereq_(19);
+var React = _dereq_(26);
+var ReactDOM = _dereq_(40);
+var ReactElement = _dereq_(57);
+var ReactBrowserEventEmitter = _dereq_(28);
+var ReactCompositeComponent = _dereq_(38);
+var ReactInstanceHandles = _dereq_(67);
+var ReactInstanceMap = _dereq_(68);
+var ReactMount = _dereq_(72);
+var ReactUpdates = _dereq_(96);
+var SyntheticEvent = _dereq_(105);
+
+var assign = _dereq_(24);
+var emptyObject = _dereq_(154);
+var findDOMNode = _dereq_(122);
+var invariant = _dereq_(161);
+
+var topLevelTypes = EventConstants.topLevelTypes;
+
+function Event(suffix) {}
+
+/**
+ * @class ReactTestUtils
+ */
+
+function findAllInRenderedTreeInternal(inst, test) {
+ if (!inst || !inst.getPublicInstance) {
+ return [];
+ }
+ var publicInst = inst.getPublicInstance();
+ var ret = test(publicInst) ? [publicInst] : [];
+ var currentElement = inst._currentElement;
+ if (ReactTestUtils.isDOMComponent(publicInst)) {
+ var renderedChildren = inst._renderedChildren;
+ var key;
+ for (key in renderedChildren) {
+ if (!renderedChildren.hasOwnProperty(key)) {
+ continue;
+ }
+ ret = ret.concat(findAllInRenderedTreeInternal(renderedChildren[key], test));
+ }
+ } else if (ReactElement.isValidElement(currentElement) && typeof currentElement.type === 'function') {
+ ret = ret.concat(findAllInRenderedTreeInternal(inst._renderedComponent, test));
+ }
+ return ret;
+}
+
+/**
+ * Todo: Support the entire DOM.scry query syntax. For now, these simple
+ * utilities will suffice for testing purposes.
+ * @lends ReactTestUtils
+ */
+var ReactTestUtils = {
+ renderIntoDocument: function (instance) {
+ var div = document.createElementNS('http://www.w3.org/1999/xhtml', 'div');
+ // None of our tests actually require attaching the container to the
+ // DOM, and doing so creates a mess that we rely on test isolation to
+ // clean up, so we're going to stop honoring the name of this method
+ // (and probably rename it eventually) if no problems arise.
+ // document.documentElement.appendChild(div);
+ return ReactDOM.render(instance, div);
+ },
+
+ isElement: function (element) {
+ return ReactElement.isValidElement(element);
+ },
+
+ isElementOfType: function (inst, convenienceConstructor) {
+ return ReactElement.isValidElement(inst) && inst.type === convenienceConstructor;
+ },
+
+ isDOMComponent: function (inst) {
+ return !!(inst && inst.nodeType === 1 && inst.tagName);
+ },
+
+ isDOMComponentElement: function (inst) {
+ return !!(inst && ReactElement.isValidElement(inst) && !!inst.tagName);
+ },
+
+ isCompositeComponent: function (inst) {
+ if (ReactTestUtils.isDOMComponent(inst)) {
+ // Accessing inst.setState warns; just return false as that'll be what
+ // this returns when we have DOM nodes as refs directly
+ return false;
+ }
+ return inst != null && typeof inst.render === 'function' && typeof inst.setState === 'function';
+ },
+
+ isCompositeComponentWithType: function (inst, type) {
+ if (!ReactTestUtils.isCompositeComponent(inst)) {
+ return false;
+ }
+ var internalInstance = ReactInstanceMap.get(inst);
+ var constructor = internalInstance._currentElement.type;
+
+ return constructor === type;
+ },
+
+ isCompositeComponentElement: function (inst) {
+ if (!ReactElement.isValidElement(inst)) {
+ return false;
+ }
+ // We check the prototype of the type that will get mounted, not the
+ // instance itself. This is a future proof way of duck typing.
+ var prototype = inst.type.prototype;
+ return typeof prototype.render === 'function' && typeof prototype.setState === 'function';
+ },
+
+ isCompositeComponentElementWithType: function (inst, type) {
+ var internalInstance = ReactInstanceMap.get(inst);
+ var constructor = internalInstance._currentElement.type;
+
+ return !!(ReactTestUtils.isCompositeComponentElement(inst) && constructor === type);
+ },
+
+ getRenderedChildOfCompositeComponent: function (inst) {
+ if (!ReactTestUtils.isCompositeComponent(inst)) {
+ return null;
+ }
+ var internalInstance = ReactInstanceMap.get(inst);
+ return internalInstance._renderedComponent.getPublicInstance();
+ },
+
+ findAllInRenderedTree: function (inst, test) {
+ if (!inst) {
+ return [];
+ }
+ !ReactTestUtils.isCompositeComponent(inst) ? "production" !== 'production' ? invariant(false, 'findAllInRenderedTree(...): instance must be a composite component') : invariant(false) : undefined;
+ return findAllInRenderedTreeInternal(ReactInstanceMap.get(inst), test);
+ },
+
+ /**
+ * Finds all instance of components in the rendered tree that are DOM
+ * components with the class name matching `className`.
+ * @return {array} an array of all the matches.
+ */
+ scryRenderedDOMComponentsWithClass: function (root, classNames) {
+ if (!Array.isArray(classNames)) {
+ classNames = classNames.split(/\s+/);
+ }
+ return ReactTestUtils.findAllInRenderedTree(root, function (inst) {
+ if (ReactTestUtils.isDOMComponent(inst)) {
+ var className = inst.className;
+ if (typeof className !== 'string') {
+ // SVG, probably.
+ className = inst.getAttribute('class') || '';
+ }
+ var classList = className.split(/\s+/);
+ return classNames.every(function (name) {
+ return classList.indexOf(name) !== -1;
+ });
+ }
+ return false;
+ });
+ },
+
+ /**
+ * Like scryRenderedDOMComponentsWithClass but expects there to be one result,
+ * and returns that one result, or throws exception if there is any other
+ * number of matches besides one.
+ * @return {!ReactDOMComponent} The one match.
+ */
+ findRenderedDOMComponentWithClass: function (root, className) {
+ var all = ReactTestUtils.scryRenderedDOMComponentsWithClass(root, className);
+ if (all.length !== 1) {
+ throw new Error('Did not find exactly one match ' + '(found: ' + all.length + ') for class:' + className);
+ }
+ return all[0];
+ },
+
+ /**
+ * Finds all instance of components in the rendered tree that are DOM
+ * components with the tag name matching `tagName`.
+ * @return {array} an array of all the matches.
+ */
+ scryRenderedDOMComponentsWithTag: function (root, tagName) {
+ return ReactTestUtils.findAllInRenderedTree(root, function (inst) {
+ return ReactTestUtils.isDOMComponent(inst) && inst.tagName.toUpperCase() === tagName.toUpperCase();
+ });
+ },
+
+ /**
+ * Like scryRenderedDOMComponentsWithTag but expects there to be one result,
+ * and returns that one result, or throws exception if there is any other
+ * number of matches besides one.
+ * @return {!ReactDOMComponent} The one match.
+ */
+ findRenderedDOMComponentWithTag: function (root, tagName) {
+ var all = ReactTestUtils.scryRenderedDOMComponentsWithTag(root, tagName);
+ if (all.length !== 1) {
+ throw new Error('Did not find exactly one match for tag:' + tagName);
+ }
+ return all[0];
+ },
+
+ /**
+ * Finds all instances of components with type equal to `componentType`.
+ * @return {array} an array of all the matches.
+ */
+ scryRenderedComponentsWithType: function (root, componentType) {
+ return ReactTestUtils.findAllInRenderedTree(root, function (inst) {
+ return ReactTestUtils.isCompositeComponentWithType(inst, componentType);
+ });
+ },
+
+ /**
+ * Same as `scryRenderedComponentsWithType` but expects there to be one result
+ * and returns that one result, or throws exception if there is any other
+ * number of matches besides one.
+ * @return {!ReactComponent} The one match.
+ */
+ findRenderedComponentWithType: function (root, componentType) {
+ var all = ReactTestUtils.scryRenderedComponentsWithType(root, componentType);
+ if (all.length !== 1) {
+ throw new Error('Did not find exactly one match for componentType:' + componentType + ' (found ' + all.length + ')');
+ }
+ return all[0];
+ },
+
+ /**
+ * Pass a mocked component module to this method to augment it with
+ * useful methods that allow it to be used as a dummy React component.
+ * Instead of rendering as usual, the component will become a simple
+ * <div> containing any provided children.
+ *
+ * @param {object} module the mock function object exported from a
+ * module that defines the component to be mocked
+ * @param {?string} mockTagName optional dummy root tag name to return
+ * from render method (overrides
+ * module.mockTagName if provided)
+ * @return {object} the ReactTestUtils object (for chaining)
+ */
+ mockComponent: function (module, mockTagName) {
+ mockTagName = mockTagName || module.mockTagName || 'div';
+
+ module.prototype.render.mockImplementation(function () {
+ return React.createElement(mockTagName, null, this.props.children);
+ });
+
+ return this;
+ },
+
+ /**
+ * Simulates a top level event being dispatched from a raw event that occurred
+ * on an `Element` node.
+ * @param {Object} topLevelType A type from `EventConstants.topLevelTypes`
+ * @param {!Element} node The dom to simulate an event occurring on.
+ * @param {?Event} fakeNativeEvent Fake native event to use in SyntheticEvent.
+ */
+ simulateNativeEventOnNode: function (topLevelType, node, fakeNativeEvent) {
+ fakeNativeEvent.target = node;
+ ReactBrowserEventEmitter.ReactEventListener.dispatchEvent(topLevelType, fakeNativeEvent);
+ },
+
+ /**
+ * Simulates a top level event being dispatched from a raw event that occurred
+ * on the `ReactDOMComponent` `comp`.
+ * @param {Object} topLevelType A type from `EventConstants.topLevelTypes`.
+ * @param {!ReactDOMComponent} comp
+ * @param {?Event} fakeNativeEvent Fake native event to use in SyntheticEvent.
+ */
+ simulateNativeEventOnDOMComponent: function (topLevelType, comp, fakeNativeEvent) {
+ ReactTestUtils.simulateNativeEventOnNode(topLevelType, findDOMNode(comp), fakeNativeEvent);
+ },
+
+ nativeTouchData: function (x, y) {
+ return {
+ touches: [{ pageX: x, pageY: y }]
+ };
+ },
+
+ createRenderer: function () {
+ return new ReactShallowRenderer();
+ },
+
+ Simulate: null,
+ SimulateNative: {}
+};
+
+/**
+ * @class ReactShallowRenderer
+ */
+var ReactShallowRenderer = function () {
+ this._instance = null;
+};
+
+ReactShallowRenderer.prototype.getRenderOutput = function () {
+ return this._instance && this._instance._renderedComponent && this._instance._renderedComponent._renderedOutput || null;
+};
+
+var NoopInternalComponent = function (element) {
+ this._renderedOutput = element;
+ this._currentElement = element;
+};
+
+NoopInternalComponent.prototype = {
+
+ mountComponent: function () {},
+
+ receiveComponent: function (element) {
+ this._renderedOutput = element;
+ this._currentElement = element;
+ },
+
+ unmountComponent: function () {},
+
+ getPublicInstance: function () {
+ return null;
+ }
+};
+
+var ShallowComponentWrapper = function () {};
+assign(ShallowComponentWrapper.prototype, ReactCompositeComponent.Mixin, {
+ _instantiateReactComponent: function (element) {
+ return new NoopInternalComponent(element);
+ },
+ _replaceNodeWithMarkupByID: function () {},
+ _renderValidatedComponent: ReactCompositeComponent.Mixin._renderValidatedComponentWithoutOwnerOrContext
+});
+
+ReactShallowRenderer.prototype.render = function (element, context) {
+ !ReactElement.isValidElement(element) ? "production" !== 'production' ? invariant(false, 'ReactShallowRenderer render(): Invalid component element.%s', typeof element === 'function' ? ' Instead of passing a component class, make sure to instantiate ' + 'it by passing it to React.createElement.' : '') : invariant(false) : undefined;
+ !(typeof element.type !== 'string') ? "production" !== 'production' ? invariant(false, 'ReactShallowRenderer render(): Shallow rendering works only with custom ' + 'components, not primitives (%s). Instead of calling `.render(el)` and ' + 'inspecting the rendered output, look at `el.props` directly instead.', element.type) : invariant(false) : undefined;
+
+ if (!context) {
+ context = emptyObject;
+ }
+ var transaction = ReactUpdates.ReactReconcileTransaction.getPooled(false);
+ this._render(element, transaction, context);
+ ReactUpdates.ReactReconcileTransaction.release(transaction);
+};
+
+ReactShallowRenderer.prototype.unmount = function () {
+ if (this._instance) {
+ this._instance.unmountComponent();
+ }
+};
+
+ReactShallowRenderer.prototype._render = function (element, transaction, context) {
+ if (this._instance) {
+ this._instance.receiveComponent(element, transaction, context);
+ } else {
+ var rootID = ReactInstanceHandles.createReactRootID();
+ var instance = new ShallowComponentWrapper(element.type);
+ instance.construct(element);
+
+ instance.mountComponent(rootID, transaction, context);
+
+ this._instance = instance;
+ }
+};
+
+/**
+ * Exports:
+ *
+ * - `ReactTestUtils.Simulate.click(Element/ReactDOMComponent)`
+ * - `ReactTestUtils.Simulate.mouseMove(Element/ReactDOMComponent)`
+ * - `ReactTestUtils.Simulate.change(Element/ReactDOMComponent)`
+ * - ... (All keys from event plugin `eventTypes` objects)
+ */
+function makeSimulator(eventType) {
+ return function (domComponentOrNode, eventData) {
+ var node;
+ if (ReactTestUtils.isDOMComponent(domComponentOrNode)) {
+ node = findDOMNode(domComponentOrNode);
+ } else if (domComponentOrNode.tagName) {
+ node = domComponentOrNode;
+ }
+
+ var dispatchConfig = ReactBrowserEventEmitter.eventNameDispatchConfigs[eventType];
+
+ var fakeNativeEvent = new Event();
+ fakeNativeEvent.target = node;
+ // We don't use SyntheticEvent.getPooled in order to not have to worry about
+ // properly destroying any properties assigned from `eventData` upon release
+ var event = new SyntheticEvent(dispatchConfig, ReactMount.getID(node), fakeNativeEvent, node);
+ assign(event, eventData);
+
+ if (dispatchConfig.phasedRegistrationNames) {
+ EventPropagators.accumulateTwoPhaseDispatches(event);
+ } else {
+ EventPropagators.accumulateDirectDispatches(event);
+ }
+
+ ReactUpdates.batchedUpdates(function () {
+ EventPluginHub.enqueueEvents(event);
+ EventPluginHub.processEventQueue(true);
+ });
+ };
+}
+
+function buildSimulators() {
+ ReactTestUtils.Simulate = {};
+
+ var eventType;
+ for (eventType in ReactBrowserEventEmitter.eventNameDispatchConfigs) {
+ /**
+ * @param {!Element|ReactDOMComponent} domComponentOrNode
+ * @param {?object} eventData Fake event data to use in SyntheticEvent.
+ */
+ ReactTestUtils.Simulate[eventType] = makeSimulator(eventType);
+ }
+}
+
+// Rebuild ReactTestUtils.Simulate whenever event plugins are injected
+var oldInjectEventPluginOrder = EventPluginHub.injection.injectEventPluginOrder;
+EventPluginHub.injection.injectEventPluginOrder = function () {
+ oldInjectEventPluginOrder.apply(this, arguments);
+ buildSimulators();
+};
+var oldInjectEventPlugins = EventPluginHub.injection.injectEventPluginsByName;
+EventPluginHub.injection.injectEventPluginsByName = function () {
+ oldInjectEventPlugins.apply(this, arguments);
+ buildSimulators();
+};
+
+buildSimulators();
+
+/**
+ * Exports:
+ *
+ * - `ReactTestUtils.SimulateNative.click(Element/ReactDOMComponent)`
+ * - `ReactTestUtils.SimulateNative.mouseMove(Element/ReactDOMComponent)`
+ * - `ReactTestUtils.SimulateNative.mouseIn/ReactDOMComponent)`
+ * - `ReactTestUtils.SimulateNative.mouseOut(Element/ReactDOMComponent)`
+ * - ... (All keys from `EventConstants.topLevelTypes`)
+ *
+ * Note: Top level event types are a subset of the entire set of handler types
+ * (which include a broader set of "synthetic" events). For example, onDragDone
+ * is a synthetic event. Except when testing an event plugin or React's event
+ * handling code specifically, you probably want to use ReactTestUtils.Simulate
+ * to dispatch synthetic events.
+ */
+
+function makeNativeSimulator(eventType) {
+ return function (domComponentOrNode, nativeEventData) {
+ var fakeNativeEvent = new Event(eventType);
+ assign(fakeNativeEvent, nativeEventData);
+ if (ReactTestUtils.isDOMComponent(domComponentOrNode)) {
+ ReactTestUtils.simulateNativeEventOnDOMComponent(eventType, domComponentOrNode, fakeNativeEvent);
+ } else if (domComponentOrNode.tagName) {
+ // Will allow on actual dom nodes.
+ ReactTestUtils.simulateNativeEventOnNode(eventType, domComponentOrNode, fakeNativeEvent);
+ }
+ };
+}
+
+Object.keys(topLevelTypes).forEach(function (eventType) {
+ // Event type is stored as 'topClick' - we transform that to 'click'
+ var convenienceName = eventType.indexOf('top') === 0 ? eventType.charAt(3).toLowerCase() + eventType.substr(4) : eventType;
+ /**
+ * @param {!Element|ReactDOMComponent} domComponentOrNode
+ * @param {?Event} nativeEventData Fake native event to use in SyntheticEvent.
+ */
+ ReactTestUtils.SimulateNative[convenienceName] = makeNativeSimulator(eventType);
+});
+
+module.exports = ReactTestUtils;
+},{"105":105,"122":122,"15":15,"154":154,"16":16,"161":161,"19":19,"24":24,"26":26,"28":28,"38":38,"40":40,"57":57,"67":67,"68":68,"72":72,"96":96}],92:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @typechecks static-only
+ * @providesModule ReactTransitionChildMapping
+ */
+
+'use strict';
+
+var flattenChildren = _dereq_(123);
+
+var ReactTransitionChildMapping = {
+ /**
+ * Given `this.props.children`, return an object mapping key to child. Just
+ * simple syntactic sugar around flattenChildren().
+ *
+ * @param {*} children `this.props.children`
+ * @return {object} Mapping of key to child
+ */
+ getChildMapping: function (children) {
+ if (!children) {
+ return children;
+ }
+ return flattenChildren(children);
+ },
+
+ /**
+ * When you're adding or removing children some may be added or removed in the
+ * same render pass. We want to show *both* since we want to simultaneously
+ * animate elements in and out. This function takes a previous set of keys
+ * and a new set of keys and merges them with its best guess of the correct
+ * ordering. In the future we may expose some of the utilities in
+ * ReactMultiChild to make this easy, but for now React itself does not
+ * directly have this concept of the union of prevChildren and nextChildren
+ * so we implement it here.
+ *
+ * @param {object} prev prev children as returned from
+ * `ReactTransitionChildMapping.getChildMapping()`.
+ * @param {object} next next children as returned from
+ * `ReactTransitionChildMapping.getChildMapping()`.
+ * @return {object} a key set that contains all keys in `prev` and all keys
+ * in `next` in a reasonable order.
+ */
+ mergeChildMappings: function (prev, next) {
+ prev = prev || {};
+ next = next || {};
+
+ function getValueForKey(key) {
+ if (next.hasOwnProperty(key)) {
+ return next[key];
+ } else {
+ return prev[key];
+ }
+ }
+
+ // For each key of `next`, the list of keys to insert before that key in
+ // the combined list
+ var nextKeysPending = {};
+
+ var pendingKeys = [];
+ for (var prevKey in prev) {
+ if (next.hasOwnProperty(prevKey)) {
+ if (pendingKeys.length) {
+ nextKeysPending[prevKey] = pendingKeys;
+ pendingKeys = [];
+ }
+ } else {
+ pendingKeys.push(prevKey);
+ }
+ }
+
+ var i;
+ var childMapping = {};
+ for (var nextKey in next) {
+ if (nextKeysPending.hasOwnProperty(nextKey)) {
+ for (i = 0; i < nextKeysPending[nextKey].length; i++) {
+ var pendingNextKey = nextKeysPending[nextKey][i];
+ childMapping[nextKeysPending[nextKey][i]] = getValueForKey(pendingNextKey);
+ }
+ }
+ childMapping[nextKey] = getValueForKey(nextKey);
+ }
+
+ // Finally, add the keys which didn't appear before any key in `next`
+ for (i = 0; i < pendingKeys.length; i++) {
+ childMapping[pendingKeys[i]] = getValueForKey(pendingKeys[i]);
+ }
+
+ return childMapping;
+ }
+};
+
+module.exports = ReactTransitionChildMapping;
+},{"123":123}],93:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactTransitionEvents
+ */
+
+'use strict';
+
+var ExecutionEnvironment = _dereq_(147);
+
+/**
+ * EVENT_NAME_MAP is used to determine which event fired when a
+ * transition/animation ends, based on the style property used to
+ * define that event.
+ */
+var EVENT_NAME_MAP = {
+ transitionend: {
+ 'transition': 'transitionend',
+ 'WebkitTransition': 'webkitTransitionEnd',
+ 'MozTransition': 'mozTransitionEnd',
+ 'OTransition': 'oTransitionEnd',
+ 'msTransition': 'MSTransitionEnd'
+ },
+
+ animationend: {
+ 'animation': 'animationend',
+ 'WebkitAnimation': 'webkitAnimationEnd',
+ 'MozAnimation': 'mozAnimationEnd',
+ 'OAnimation': 'oAnimationEnd',
+ 'msAnimation': 'MSAnimationEnd'
+ }
+};
+
+var endEvents = [];
+
+function detectEvents() {
+ var testEl = document.createElementNS('http://www.w3.org/1999/xhtml', 'div');
+ var style = testEl.style;
+
+ // On some platforms, in particular some releases of Android 4.x,
+ // the un-prefixed "animation" and "transition" properties are defined on the
+ // style object but the events that fire will still be prefixed, so we need
+ // to check if the un-prefixed events are useable, and if not remove them
+ // from the map
+ if (!('AnimationEvent' in window)) {
+ delete EVENT_NAME_MAP.animationend.animation;
+ }
+
+ if (!('TransitionEvent' in window)) {
+ delete EVENT_NAME_MAP.transitionend.transition;
+ }
+
+ for (var baseEventName in EVENT_NAME_MAP) {
+ var baseEvents = EVENT_NAME_MAP[baseEventName];
+ for (var styleName in baseEvents) {
+ if (styleName in style) {
+ endEvents.push(baseEvents[styleName]);
+ break;
+ }
+ }
+ }
+}
+
+if (ExecutionEnvironment.canUseDOM) {
+ detectEvents();
+}
+
+// We use the raw {add|remove}EventListener() call because EventListener
+// does not know how to remove event listeners and we really should
+// clean up. Also, these events are not triggered in older browsers
+// so we should be A-OK here.
+
+function addEventListener(node, eventName, eventListener) {
+ node.addEventListener(eventName, eventListener, false);
+}
+
+function removeEventListener(node, eventName, eventListener) {
+ node.removeEventListener(eventName, eventListener, false);
+}
+
+var ReactTransitionEvents = {
+ addEndEventListener: function (node, eventListener) {
+ if (endEvents.length === 0) {
+ // If CSS transitions are not supported, trigger an "end animation"
+ // event immediately.
+ window.setTimeout(eventListener, 0);
+ return;
+ }
+ endEvents.forEach(function (endEvent) {
+ addEventListener(node, endEvent, eventListener);
+ });
+ },
+
+ removeEndEventListener: function (node, eventListener) {
+ if (endEvents.length === 0) {
+ return;
+ }
+ endEvents.forEach(function (endEvent) {
+ removeEventListener(node, endEvent, eventListener);
+ });
+ }
+};
+
+module.exports = ReactTransitionEvents;
+},{"147":147}],94:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactTransitionGroup
+ */
+
+'use strict';
+
+var React = _dereq_(26);
+var ReactTransitionChildMapping = _dereq_(92);
+
+var assign = _dereq_(24);
+var emptyFunction = _dereq_(153);
+
+var ReactTransitionGroup = React.createClass({
+ displayName: 'ReactTransitionGroup',
+
+ propTypes: {
+ component: React.PropTypes.any,
+ childFactory: React.PropTypes.func
+ },
+
+ getDefaultProps: function () {
+ return {
+ component: 'span',
+ childFactory: emptyFunction.thatReturnsArgument
+ };
+ },
+
+ getInitialState: function () {
+ return {
+ children: ReactTransitionChildMapping.getChildMapping(this.props.children)
+ };
+ },
+
+ componentWillMount: function () {
+ this.currentlyTransitioningKeys = {};
+ this.keysToEnter = [];
+ this.keysToLeave = [];
+ },
+
+ componentDidMount: function () {
+ var initialChildMapping = this.state.children;
+ for (var key in initialChildMapping) {
+ if (initialChildMapping[key]) {
+ this.performAppear(key);
+ }
+ }
+ },
+
+ componentWillReceiveProps: function (nextProps) {
+ var nextChildMapping = ReactTransitionChildMapping.getChildMapping(nextProps.children);
+ var prevChildMapping = this.state.children;
+
+ this.setState({
+ children: ReactTransitionChildMapping.mergeChildMappings(prevChildMapping, nextChildMapping)
+ });
+
+ var key;
+
+ for (key in nextChildMapping) {
+ var hasPrev = prevChildMapping && prevChildMapping.hasOwnProperty(key);
+ if (nextChildMapping[key] && !hasPrev && !this.currentlyTransitioningKeys[key]) {
+ this.keysToEnter.push(key);
+ }
+ }
+
+ for (key in prevChildMapping) {
+ var hasNext = nextChildMapping && nextChildMapping.hasOwnProperty(key);
+ if (prevChildMapping[key] && !hasNext && !this.currentlyTransitioningKeys[key]) {
+ this.keysToLeave.push(key);
+ }
+ }
+
+ // If we want to someday check for reordering, we could do it here.
+ },
+
+ componentDidUpdate: function () {
+ var keysToEnter = this.keysToEnter;
+ this.keysToEnter = [];
+ keysToEnter.forEach(this.performEnter);
+
+ var keysToLeave = this.keysToLeave;
+ this.keysToLeave = [];
+ keysToLeave.forEach(this.performLeave);
+ },
+
+ performAppear: function (key) {
+ this.currentlyTransitioningKeys[key] = true;
+
+ var component = this.refs[key];
+
+ if (component.componentWillAppear) {
+ component.componentWillAppear(this._handleDoneAppearing.bind(this, key));
+ } else {
+ this._handleDoneAppearing(key);
+ }
+ },
+
+ _handleDoneAppearing: function (key) {
+ var component = this.refs[key];
+ if (component.componentDidAppear) {
+ component.componentDidAppear();
+ }
+
+ delete this.currentlyTransitioningKeys[key];
+
+ var currentChildMapping = ReactTransitionChildMapping.getChildMapping(this.props.children);
+
+ if (!currentChildMapping || !currentChildMapping.hasOwnProperty(key)) {
+ // This was removed before it had fully appeared. Remove it.
+ this.performLeave(key);
+ }
+ },
+
+ performEnter: function (key) {
+ this.currentlyTransitioningKeys[key] = true;
+
+ var component = this.refs[key];
+
+ if (component.componentWillEnter) {
+ component.componentWillEnter(this._handleDoneEntering.bind(this, key));
+ } else {
+ this._handleDoneEntering(key);
+ }
+ },
+
+ _handleDoneEntering: function (key) {
+ var component = this.refs[key];
+ if (component.componentDidEnter) {
+ component.componentDidEnter();
+ }
+
+ delete this.currentlyTransitioningKeys[key];
+
+ var currentChildMapping = ReactTransitionChildMapping.getChildMapping(this.props.children);
+
+ if (!currentChildMapping || !currentChildMapping.hasOwnProperty(key)) {
+ // This was removed before it had fully entered. Remove it.
+ this.performLeave(key);
+ }
+ },
+
+ performLeave: function (key) {
+ this.currentlyTransitioningKeys[key] = true;
+
+ var component = this.refs[key];
+ if (component.componentWillLeave) {
+ component.componentWillLeave(this._handleDoneLeaving.bind(this, key));
+ } else {
+ // Note that this is somewhat dangerous b/c it calls setState()
+ // again, effectively mutating the component before all the work
+ // is done.
+ this._handleDoneLeaving(key);
+ }
+ },
+
+ _handleDoneLeaving: function (key) {
+ var component = this.refs[key];
+
+ if (component.componentDidLeave) {
+ component.componentDidLeave();
+ }
+
+ delete this.currentlyTransitioningKeys[key];
+
+ var currentChildMapping = ReactTransitionChildMapping.getChildMapping(this.props.children);
+
+ if (currentChildMapping && currentChildMapping.hasOwnProperty(key)) {
+ // This entered again before it fully left. Add it again.
+ this.performEnter(key);
+ } else {
+ this.setState(function (state) {
+ var newChildren = assign({}, state.children);
+ delete newChildren[key];
+ return { children: newChildren };
+ });
+ }
+ },
+
+ render: function () {
+ // TODO: we could get rid of the need for the wrapper node
+ // by cloning a single child
+ var childrenToRender = [];
+ for (var key in this.state.children) {
+ var child = this.state.children[key];
+ if (child) {
+ // You may need to apply reactive updates to a child as it is leaving.
+ // The normal React way to do it won't work since the child will have
+ // already been removed. In case you need this behavior you can provide
+ // a childFactory function to wrap every child, even the ones that are
+ // leaving.
+ childrenToRender.push(React.cloneElement(this.props.childFactory(child), { ref: key, key: key }));
+ }
+ }
+ return React.createElement(this.props.component, this.props, childrenToRender);
+ }
+});
+
+module.exports = ReactTransitionGroup;
+},{"153":153,"24":24,"26":26,"92":92}],95:[function(_dereq_,module,exports){
+/**
+ * Copyright 2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactUpdateQueue
+ */
+
+'use strict';
+
+var ReactCurrentOwner = _dereq_(39);
+var ReactElement = _dereq_(57);
+var ReactInstanceMap = _dereq_(68);
+var ReactUpdates = _dereq_(96);
+
+var assign = _dereq_(24);
+var invariant = _dereq_(161);
+var warning = _dereq_(173);
+
+function enqueueUpdate(internalInstance) {
+ ReactUpdates.enqueueUpdate(internalInstance);
+}
+
+function getInternalInstanceReadyForUpdate(publicInstance, callerName) {
+ var internalInstance = ReactInstanceMap.get(publicInstance);
+ if (!internalInstance) {
+ if ("production" !== 'production') {
+ // Only warn when we have a callerName. Otherwise we should be silent.
+ // We're probably calling from enqueueCallback. We don't want to warn
+ // there because we already warned for the corresponding lifecycle method.
+ "production" !== 'production' ? warning(!callerName, '%s(...): Can only update a mounted or mounting component. ' + 'This usually means you called %s() on an unmounted component. ' + 'This is a no-op. Please check the code for the %s component.', callerName, callerName, publicInstance.constructor.displayName) : undefined;
+ }
+ return null;
+ }
+
+ if ("production" !== 'production') {
+ "production" !== 'production' ? warning(ReactCurrentOwner.current == null, '%s(...): Cannot update during an existing state transition ' + '(such as within `render`). Render methods should be a pure function ' + 'of props and state.', callerName) : undefined;
+ }
+
+ return internalInstance;
+}
+
+/**
+ * ReactUpdateQueue allows for state updates to be scheduled into a later
+ * reconciliation step.
+ */
+var ReactUpdateQueue = {
+
+ /**
+ * Checks whether or not this composite component is mounted.
+ * @param {ReactClass} publicInstance The instance we want to test.
+ * @return {boolean} True if mounted, false otherwise.
+ * @protected
+ * @final
+ */
+ isMounted: function (publicInstance) {
+ if ("production" !== 'production') {
+ var owner = ReactCurrentOwner.current;
+ if (owner !== null) {
+ "production" !== 'production' ? warning(owner._warnedAboutRefsInRender, '%s is accessing isMounted inside its render() function. ' + 'render() should be a pure function of props and state. It should ' + 'never access something that requires stale data from the previous ' + 'render, such as refs. Move this logic to componentDidMount and ' + 'componentDidUpdate instead.', owner.getName() || 'A component') : undefined;
+ owner._warnedAboutRefsInRender = true;
+ }
+ }
+ var internalInstance = ReactInstanceMap.get(publicInstance);
+ if (internalInstance) {
+ // During componentWillMount and render this will still be null but after
+ // that will always render to something. At least for now. So we can use
+ // this hack.
+ return !!internalInstance._renderedComponent;
+ } else {
+ return false;
+ }
+ },
+
+ /**
+ * Enqueue a callback that will be executed after all the pending updates
+ * have processed.
+ *
+ * @param {ReactClass} publicInstance The instance to use as `this` context.
+ * @param {?function} callback Called after state is updated.
+ * @internal
+ */
+ enqueueCallback: function (publicInstance, callback) {
+ !(typeof callback === 'function') ? "production" !== 'production' ? invariant(false, 'enqueueCallback(...): You called `setProps`, `replaceProps`, ' + '`setState`, `replaceState`, or `forceUpdate` with a callback that ' + 'isn\'t callable.') : invariant(false) : undefined;
+ var internalInstance = getInternalInstanceReadyForUpdate(publicInstance);
+
+ // Previously we would throw an error if we didn't have an internal
+ // instance. Since we want to make it a no-op instead, we mirror the same
+ // behavior we have in other enqueue* methods.
+ // We also need to ignore callbacks in componentWillMount. See
+ // enqueueUpdates.
+ if (!internalInstance) {
+ return null;
+ }
+
+ if (internalInstance._pendingCallbacks) {
+ internalInstance._pendingCallbacks.push(callback);
+ } else {
+ internalInstance._pendingCallbacks = [callback];
+ }
+ // TODO: The callback here is ignored when setState is called from
+ // componentWillMount. Either fix it or disallow doing so completely in
+ // favor of getInitialState. Alternatively, we can disallow
+ // componentWillMount during server-side rendering.
+ enqueueUpdate(internalInstance);
+ },
+
+ enqueueCallbackInternal: function (internalInstance, callback) {
+ !(typeof callback === 'function') ? "production" !== 'production' ? invariant(false, 'enqueueCallback(...): You called `setProps`, `replaceProps`, ' + '`setState`, `replaceState`, or `forceUpdate` with a callback that ' + 'isn\'t callable.') : invariant(false) : undefined;
+ if (internalInstance._pendingCallbacks) {
+ internalInstance._pendingCallbacks.push(callback);
+ } else {
+ internalInstance._pendingCallbacks = [callback];
+ }
+ enqueueUpdate(internalInstance);
+ },
+
+ /**
+ * Forces an update. This should only be invoked when it is known with
+ * certainty that we are **not** in a DOM transaction.
+ *
+ * You may want to call this when you know that some deeper aspect of the
+ * component's state has changed but `setState` was not called.
+ *
+ * This will not invoke `shouldComponentUpdate`, but it will invoke
+ * `componentWillUpdate` and `componentDidUpdate`.
+ *
+ * @param {ReactClass} publicInstance The instance that should rerender.
+ * @internal
+ */
+ enqueueForceUpdate: function (publicInstance) {
+ var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'forceUpdate');
+
+ if (!internalInstance) {
+ return;
+ }
+
+ internalInstance._pendingForceUpdate = true;
+
+ enqueueUpdate(internalInstance);
+ },
+
+ /**
+ * Replaces all of the state. Always use this or `setState` to mutate state.
+ * You should treat `this.state` as immutable.
+ *
+ * There is no guarantee that `this.state` will be immediately updated, so
+ * accessing `this.state` after calling this method may return the old value.
+ *
+ * @param {ReactClass} publicInstance The instance that should rerender.
+ * @param {object} completeState Next state.
+ * @internal
+ */
+ enqueueReplaceState: function (publicInstance, completeState) {
+ var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'replaceState');
+
+ if (!internalInstance) {
+ return;
+ }
+
+ internalInstance._pendingStateQueue = [completeState];
+ internalInstance._pendingReplaceState = true;
+
+ enqueueUpdate(internalInstance);
+ },
+
+ /**
+ * Sets a subset of the state. This only exists because _pendingState is
+ * internal. This provides a merging strategy that is not available to deep
+ * properties which is confusing. TODO: Expose pendingState or don't use it
+ * during the merge.
+ *
+ * @param {ReactClass} publicInstance The instance that should rerender.
+ * @param {object} partialState Next partial state to be merged with state.
+ * @internal
+ */
+ enqueueSetState: function (publicInstance, partialState) {
+ var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');
+
+ if (!internalInstance) {
+ return;
+ }
+
+ var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
+ queue.push(partialState);
+
+ enqueueUpdate(internalInstance);
+ },
+
+ /**
+ * Sets a subset of the props.
+ *
+ * @param {ReactClass} publicInstance The instance that should rerender.
+ * @param {object} partialProps Subset of the next props.
+ * @internal
+ */
+ enqueueSetProps: function (publicInstance, partialProps) {
+ var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setProps');
+ if (!internalInstance) {
+ return;
+ }
+ ReactUpdateQueue.enqueueSetPropsInternal(internalInstance, partialProps);
+ },
+
+ enqueueSetPropsInternal: function (internalInstance, partialProps) {
+ var topLevelWrapper = internalInstance._topLevelWrapper;
+ !topLevelWrapper ? "production" !== 'production' ? invariant(false, 'setProps(...): You called `setProps` on a ' + 'component with a parent. This is an anti-pattern since props will ' + 'get reactively updated when rendered. Instead, change the owner\'s ' + '`render` method to pass the correct value as props to the component ' + 'where it is created.') : invariant(false) : undefined;
+
+ // Merge with the pending element if it exists, otherwise with existing
+ // element props.
+ var wrapElement = topLevelWrapper._pendingElement || topLevelWrapper._currentElement;
+ var element = wrapElement.props;
+ var props = assign({}, element.props, partialProps);
+ topLevelWrapper._pendingElement = ReactElement.cloneAndReplaceProps(wrapElement, ReactElement.cloneAndReplaceProps(element, props));
+
+ enqueueUpdate(topLevelWrapper);
+ },
+
+ /**
+ * Replaces all of the props.
+ *
+ * @param {ReactClass} publicInstance The instance that should rerender.
+ * @param {object} props New props.
+ * @internal
+ */
+ enqueueReplaceProps: function (publicInstance, props) {
+ var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'replaceProps');
+ if (!internalInstance) {
+ return;
+ }
+ ReactUpdateQueue.enqueueReplacePropsInternal(internalInstance, props);
+ },
+
+ enqueueReplacePropsInternal: function (internalInstance, props) {
+ var topLevelWrapper = internalInstance._topLevelWrapper;
+ !topLevelWrapper ? "production" !== 'production' ? invariant(false, 'replaceProps(...): You called `replaceProps` on a ' + 'component with a parent. This is an anti-pattern since props will ' + 'get reactively updated when rendered. Instead, change the owner\'s ' + '`render` method to pass the correct value as props to the component ' + 'where it is created.') : invariant(false) : undefined;
+
+ // Merge with the pending element if it exists, otherwise with existing
+ // element props.
+ var wrapElement = topLevelWrapper._pendingElement || topLevelWrapper._currentElement;
+ var element = wrapElement.props;
+ topLevelWrapper._pendingElement = ReactElement.cloneAndReplaceProps(wrapElement, ReactElement.cloneAndReplaceProps(element, props));
+
+ enqueueUpdate(topLevelWrapper);
+ },
+
+ enqueueElementInternal: function (internalInstance, newElement) {
+ internalInstance._pendingElement = newElement;
+ enqueueUpdate(internalInstance);
+ }
+
+};
+
+module.exports = ReactUpdateQueue;
+},{"161":161,"173":173,"24":24,"39":39,"57":57,"68":68,"96":96}],96:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactUpdates
+ */
+
+'use strict';
+
+var CallbackQueue = _dereq_(6);
+var PooledClass = _dereq_(25);
+var ReactPerf = _dereq_(78);
+var ReactReconciler = _dereq_(84);
+var Transaction = _dereq_(113);
+
+var assign = _dereq_(24);
+var invariant = _dereq_(161);
+
+var dirtyComponents = [];
+var asapCallbackQueue = CallbackQueue.getPooled();
+var asapEnqueued = false;
+
+var batchingStrategy = null;
+
+function ensureInjected() {
+ !(ReactUpdates.ReactReconcileTransaction && batchingStrategy) ? "production" !== 'production' ? invariant(false, 'ReactUpdates: must inject a reconcile transaction class and batching ' + 'strategy') : invariant(false) : undefined;
+}
+
+var NESTED_UPDATES = {
+ initialize: function () {
+ this.dirtyComponentsLength = dirtyComponents.length;
+ },
+ close: function () {
+ if (this.dirtyComponentsLength !== dirtyComponents.length) {
+ // Additional updates were enqueued by componentDidUpdate handlers or
+ // similar; before our own UPDATE_QUEUEING wrapper closes, we want to run
+ // these new updates so that if A's componentDidUpdate calls setState on
+ // B, B will update before the callback A's updater provided when calling
+ // setState.
+ dirtyComponents.splice(0, this.dirtyComponentsLength);
+ flushBatchedUpdates();
+ } else {
+ dirtyComponents.length = 0;
+ }
+ }
+};
+
+var UPDATE_QUEUEING = {
+ initialize: function () {
+ this.callbackQueue.reset();
+ },
+ close: function () {
+ this.callbackQueue.notifyAll();
+ }
+};
+
+var TRANSACTION_WRAPPERS = [NESTED_UPDATES, UPDATE_QUEUEING];
+
+function ReactUpdatesFlushTransaction() {
+ this.reinitializeTransaction();
+ this.dirtyComponentsLength = null;
+ this.callbackQueue = CallbackQueue.getPooled();
+ this.reconcileTransaction = ReactUpdates.ReactReconcileTransaction.getPooled( /* forceHTML */false);
+}
+
+assign(ReactUpdatesFlushTransaction.prototype, Transaction.Mixin, {
+ getTransactionWrappers: function () {
+ return TRANSACTION_WRAPPERS;
+ },
+
+ destructor: function () {
+ this.dirtyComponentsLength = null;
+ CallbackQueue.release(this.callbackQueue);
+ this.callbackQueue = null;
+ ReactUpdates.ReactReconcileTransaction.release(this.reconcileTransaction);
+ this.reconcileTransaction = null;
+ },
+
+ perform: function (method, scope, a) {
+ // Essentially calls `this.reconcileTransaction.perform(method, scope, a)`
+ // with this transaction's wrappers around it.
+ return Transaction.Mixin.perform.call(this, this.reconcileTransaction.perform, this.reconcileTransaction, method, scope, a);
+ }
+});
+
+PooledClass.addPoolingTo(ReactUpdatesFlushTransaction);
+
+function batchedUpdates(callback, a, b, c, d, e) {
+ ensureInjected();
+ batchingStrategy.batchedUpdates(callback, a, b, c, d, e);
+}
+
+/**
+ * Array comparator for ReactComponents by mount ordering.
+ *
+ * @param {ReactComponent} c1 first component you're comparing
+ * @param {ReactComponent} c2 second component you're comparing
+ * @return {number} Return value usable by Array.prototype.sort().
+ */
+function mountOrderComparator(c1, c2) {
+ return c1._mountOrder - c2._mountOrder;
+}
+
+function runBatchedUpdates(transaction) {
+ var len = transaction.dirtyComponentsLength;
+ !(len === dirtyComponents.length) ? "production" !== 'production' ? invariant(false, 'Expected flush transaction\'s stored dirty-components length (%s) to ' + 'match dirty-components array length (%s).', len, dirtyComponents.length) : invariant(false) : undefined;
+
+ // Since reconciling a component higher in the owner hierarchy usually (not
+ // always -- see shouldComponentUpdate()) will reconcile children, reconcile
+ // them before their children by sorting the array.
+ dirtyComponents.sort(mountOrderComparator);
+
+ for (var i = 0; i < len; i++) {
+ // If a component is unmounted before pending changes apply, it will still
+ // be here, but we assume that it has cleared its _pendingCallbacks and
+ // that performUpdateIfNecessary is a noop.
+ var component = dirtyComponents[i];
+
+ // If performUpdateIfNecessary happens to enqueue any new updates, we
+ // shouldn't execute the callbacks until the next render happens, so
+ // stash the callbacks first
+ var callbacks = component._pendingCallbacks;
+ component._pendingCallbacks = null;
+
+ ReactReconciler.performUpdateIfNecessary(component, transaction.reconcileTransaction);
+
+ if (callbacks) {
+ for (var j = 0; j < callbacks.length; j++) {
+ transaction.callbackQueue.enqueue(callbacks[j], component.getPublicInstance());
+ }
+ }
+ }
+}
+
+var flushBatchedUpdates = function () {
+ // ReactUpdatesFlushTransaction's wrappers will clear the dirtyComponents
+ // array and perform any updates enqueued by mount-ready handlers (i.e.,
+ // componentDidUpdate) but we need to check here too in order to catch
+ // updates enqueued by setState callbacks and asap calls.
+ while (dirtyComponents.length || asapEnqueued) {
+ if (dirtyComponents.length) {
+ var transaction = ReactUpdatesFlushTransaction.getPooled();
+ transaction.perform(runBatchedUpdates, null, transaction);
+ ReactUpdatesFlushTransaction.release(transaction);
+ }
+
+ if (asapEnqueued) {
+ asapEnqueued = false;
+ var queue = asapCallbackQueue;
+ asapCallbackQueue = CallbackQueue.getPooled();
+ queue.notifyAll();
+ CallbackQueue.release(queue);
+ }
+ }
+};
+flushBatchedUpdates = ReactPerf.measure('ReactUpdates', 'flushBatchedUpdates', flushBatchedUpdates);
+
+/**
+ * Mark a component as needing a rerender, adding an optional callback to a
+ * list of functions which will be executed once the rerender occurs.
+ */
+function enqueueUpdate(component) {
+ ensureInjected();
+
+ // Various parts of our code (such as ReactCompositeComponent's
+ // _renderValidatedComponent) assume that calls to render aren't nested;
+ // verify that that's the case. (This is called by each top-level update
+ // function, like setProps, setState, forceUpdate, etc.; creation and
+ // destruction of top-level components is guarded in ReactMount.)
+
+ if (!batchingStrategy.isBatchingUpdates) {
+ batchingStrategy.batchedUpdates(enqueueUpdate, component);
+ return;
+ }
+
+ dirtyComponents.push(component);
+}
+
+/**
+ * Enqueue a callback to be run at the end of the current batching cycle. Throws
+ * if no updates are currently being performed.
+ */
+function asap(callback, context) {
+ !batchingStrategy.isBatchingUpdates ? "production" !== 'production' ? invariant(false, 'ReactUpdates.asap: Can\'t enqueue an asap callback in a context where' + 'updates are not being batched.') : invariant(false) : undefined;
+ asapCallbackQueue.enqueue(callback, context);
+ asapEnqueued = true;
+}
+
+var ReactUpdatesInjection = {
+ injectReconcileTransaction: function (ReconcileTransaction) {
+ !ReconcileTransaction ? "production" !== 'production' ? invariant(false, 'ReactUpdates: must provide a reconcile transaction class') : invariant(false) : undefined;
+ ReactUpdates.ReactReconcileTransaction = ReconcileTransaction;
+ },
+
+ injectBatchingStrategy: function (_batchingStrategy) {
+ !_batchingStrategy ? "production" !== 'production' ? invariant(false, 'ReactUpdates: must provide a batching strategy') : invariant(false) : undefined;
+ !(typeof _batchingStrategy.batchedUpdates === 'function') ? "production" !== 'production' ? invariant(false, 'ReactUpdates: must provide a batchedUpdates() function') : invariant(false) : undefined;
+ !(typeof _batchingStrategy.isBatchingUpdates === 'boolean') ? "production" !== 'production' ? invariant(false, 'ReactUpdates: must provide an isBatchingUpdates boolean attribute') : invariant(false) : undefined;
+ batchingStrategy = _batchingStrategy;
+ }
+};
+
+var ReactUpdates = {
+ /**
+ * React references `ReactReconcileTransaction` using this property in order
+ * to allow dependency injection.
+ *
+ * @internal
+ */
+ ReactReconcileTransaction: null,
+
+ batchedUpdates: batchedUpdates,
+ enqueueUpdate: enqueueUpdate,
+ flushBatchedUpdates: flushBatchedUpdates,
+ injection: ReactUpdatesInjection,
+ asap: asap
+};
+
+module.exports = ReactUpdates;
+},{"113":113,"161":161,"24":24,"25":25,"6":6,"78":78,"84":84}],97:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactVersion
+ */
+
+'use strict';
+
+module.exports = '0.14.6';
+},{}],98:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule SVGDOMPropertyConfig
+ */
+
+'use strict';
+
+var DOMProperty = _dereq_(10);
+
+var MUST_USE_ATTRIBUTE = DOMProperty.injection.MUST_USE_ATTRIBUTE;
+
+var NS = {
+ xlink: 'http://www.w3.org/1999/xlink',
+ xml: 'http://www.w3.org/XML/1998/namespace'
+};
+
+var SVGDOMPropertyConfig = {
+ Properties: {
+ clipPath: MUST_USE_ATTRIBUTE,
+ cx: MUST_USE_ATTRIBUTE,
+ cy: MUST_USE_ATTRIBUTE,
+ d: MUST_USE_ATTRIBUTE,
+ dx: MUST_USE_ATTRIBUTE,
+ dy: MUST_USE_ATTRIBUTE,
+ fill: MUST_USE_ATTRIBUTE,
+ fillOpacity: MUST_USE_ATTRIBUTE,
+ fontFamily: MUST_USE_ATTRIBUTE,
+ fontSize: MUST_USE_ATTRIBUTE,
+ fx: MUST_USE_ATTRIBUTE,
+ fy: MUST_USE_ATTRIBUTE,
+ gradientTransform: MUST_USE_ATTRIBUTE,
+ gradientUnits: MUST_USE_ATTRIBUTE,
+ markerEnd: MUST_USE_ATTRIBUTE,
+ markerMid: MUST_USE_ATTRIBUTE,
+ markerStart: MUST_USE_ATTRIBUTE,
+ offset: MUST_USE_ATTRIBUTE,
+ opacity: MUST_USE_ATTRIBUTE,
+ patternContentUnits: MUST_USE_ATTRIBUTE,
+ patternUnits: MUST_USE_ATTRIBUTE,
+ points: MUST_USE_ATTRIBUTE,
+ preserveAspectRatio: MUST_USE_ATTRIBUTE,
+ r: MUST_USE_ATTRIBUTE,
+ rx: MUST_USE_ATTRIBUTE,
+ ry: MUST_USE_ATTRIBUTE,
+ spreadMethod: MUST_USE_ATTRIBUTE,
+ stopColor: MUST_USE_ATTRIBUTE,
+ stopOpacity: MUST_USE_ATTRIBUTE,
+ stroke: MUST_USE_ATTRIBUTE,
+ strokeDasharray: MUST_USE_ATTRIBUTE,
+ strokeLinecap: MUST_USE_ATTRIBUTE,
+ strokeOpacity: MUST_USE_ATTRIBUTE,
+ strokeWidth: MUST_USE_ATTRIBUTE,
+ textAnchor: MUST_USE_ATTRIBUTE,
+ transform: MUST_USE_ATTRIBUTE,
+ version: MUST_USE_ATTRIBUTE,
+ viewBox: MUST_USE_ATTRIBUTE,
+ x1: MUST_USE_ATTRIBUTE,
+ x2: MUST_USE_ATTRIBUTE,
+ x: MUST_USE_ATTRIBUTE,
+ xlinkActuate: MUST_USE_ATTRIBUTE,
+ xlinkArcrole: MUST_USE_ATTRIBUTE,
+ xlinkHref: MUST_USE_ATTRIBUTE,
+ xlinkRole: MUST_USE_ATTRIBUTE,
+ xlinkShow: MUST_USE_ATTRIBUTE,
+ xlinkTitle: MUST_USE_ATTRIBUTE,
+ xlinkType: MUST_USE_ATTRIBUTE,
+ xmlBase: MUST_USE_ATTRIBUTE,
+ xmlLang: MUST_USE_ATTRIBUTE,
+ xmlSpace: MUST_USE_ATTRIBUTE,
+ y1: MUST_USE_ATTRIBUTE,
+ y2: MUST_USE_ATTRIBUTE,
+ y: MUST_USE_ATTRIBUTE
+ },
+ DOMAttributeNamespaces: {
+ xlinkActuate: NS.xlink,
+ xlinkArcrole: NS.xlink,
+ xlinkHref: NS.xlink,
+ xlinkRole: NS.xlink,
+ xlinkShow: NS.xlink,
+ xlinkTitle: NS.xlink,
+ xlinkType: NS.xlink,
+ xmlBase: NS.xml,
+ xmlLang: NS.xml,
+ xmlSpace: NS.xml
+ },
+ DOMAttributeNames: {
+ clipPath: 'clip-path',
+ fillOpacity: 'fill-opacity',
+ fontFamily: 'font-family',
+ fontSize: 'font-size',
+ gradientTransform: 'gradientTransform',
+ gradientUnits: 'gradientUnits',
+ markerEnd: 'marker-end',
+ markerMid: 'marker-mid',
+ markerStart: 'marker-start',
+ patternContentUnits: 'patternContentUnits',
+ patternUnits: 'patternUnits',
+ preserveAspectRatio: 'preserveAspectRatio',
+ spreadMethod: 'spreadMethod',
+ stopColor: 'stop-color',
+ stopOpacity: 'stop-opacity',
+ strokeDasharray: 'stroke-dasharray',
+ strokeLinecap: 'stroke-linecap',
+ strokeOpacity: 'stroke-opacity',
+ strokeWidth: 'stroke-width',
+ textAnchor: 'text-anchor',
+ viewBox: 'viewBox',
+ xlinkActuate: 'xlink:actuate',
+ xlinkArcrole: 'xlink:arcrole',
+ xlinkHref: 'xlink:href',
+ xlinkRole: 'xlink:role',
+ xlinkShow: 'xlink:show',
+ xlinkTitle: 'xlink:title',
+ xlinkType: 'xlink:type',
+ xmlBase: 'xml:base',
+ xmlLang: 'xml:lang',
+ xmlSpace: 'xml:space'
+ }
+};
+
+module.exports = SVGDOMPropertyConfig;
+},{"10":10}],99:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule SelectEventPlugin
+ */
+
+'use strict';
+
+var EventConstants = _dereq_(15);
+var EventPropagators = _dereq_(19);
+var ExecutionEnvironment = _dereq_(147);
+var ReactInputSelection = _dereq_(66);
+var SyntheticEvent = _dereq_(105);
+
+var getActiveElement = _dereq_(156);
+var isTextInputElement = _dereq_(134);
+var keyOf = _dereq_(166);
+var shallowEqual = _dereq_(171);
+
+var topLevelTypes = EventConstants.topLevelTypes;
+
+var skipSelectionChangeEvent = ExecutionEnvironment.canUseDOM && 'documentMode' in document && document.documentMode <= 11;
+
+var eventTypes = {
+ select: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onSelect: null }),
+ captured: keyOf({ onSelectCapture: null })
+ },
+ dependencies: [topLevelTypes.topBlur, topLevelTypes.topContextMenu, topLevelTypes.topFocus, topLevelTypes.topKeyDown, topLevelTypes.topMouseDown, topLevelTypes.topMouseUp, topLevelTypes.topSelectionChange]
+ }
+};
+
+var activeElement = null;
+var activeElementID = null;
+var lastSelection = null;
+var mouseDown = false;
+
+// Track whether a listener exists for this plugin. If none exist, we do
+// not extract events.
+var hasListener = false;
+var ON_SELECT_KEY = keyOf({ onSelect: null });
+
+/**
+ * Get an object which is a unique representation of the current selection.
+ *
+ * The return value will not be consistent across nodes or browsers, but
+ * two identical selections on the same node will return identical objects.
+ *
+ * @param {DOMElement} node
+ * @return {object}
+ */
+function getSelection(node) {
+ if ('selectionStart' in node && ReactInputSelection.hasSelectionCapabilities(node)) {
+ return {
+ start: node.selectionStart,
+ end: node.selectionEnd
+ };
+ } else if (window.getSelection) {
+ var selection = window.getSelection();
+ return {
+ anchorNode: selection.anchorNode,
+ anchorOffset: selection.anchorOffset,
+ focusNode: selection.focusNode,
+ focusOffset: selection.focusOffset
+ };
+ } else if (document.selection) {
+ var range = document.selection.createRange();
+ return {
+ parentElement: range.parentElement(),
+ text: range.text,
+ top: range.boundingTop,
+ left: range.boundingLeft
+ };
+ }
+}
+
+/**
+ * Poll selection to see whether it's changed.
+ *
+ * @param {object} nativeEvent
+ * @return {?SyntheticEvent}
+ */
+function constructSelectEvent(nativeEvent, nativeEventTarget) {
+ // Ensure we have the right element, and that the user is not dragging a
+ // selection (this matches native `select` event behavior). In HTML5, select
+ // fires only on input and textarea thus if there's no focused element we
+ // won't dispatch.
+ if (mouseDown || activeElement == null || activeElement !== getActiveElement()) {
+ return null;
+ }
+
+ // Only fire when selection has actually changed.
+ var currentSelection = getSelection(activeElement);
+ if (!lastSelection || !shallowEqual(lastSelection, currentSelection)) {
+ lastSelection = currentSelection;
+
+ var syntheticEvent = SyntheticEvent.getPooled(eventTypes.select, activeElementID, nativeEvent, nativeEventTarget);
+
+ syntheticEvent.type = 'select';
+ syntheticEvent.target = activeElement;
+
+ EventPropagators.accumulateTwoPhaseDispatches(syntheticEvent);
+
+ return syntheticEvent;
+ }
+
+ return null;
+}
+
+/**
+ * This plugin creates an `onSelect` event that normalizes select events
+ * across form elements.
+ *
+ * Supported elements are:
+ * - input (see `isTextInputElement`)
+ * - textarea
+ * - contentEditable
+ *
+ * This differs from native browser implementations in the following ways:
+ * - Fires on contentEditable fields as well as inputs.
+ * - Fires for collapsed selection.
+ * - Fires after user input.
+ */
+var SelectEventPlugin = {
+
+ eventTypes: eventTypes,
+
+ /**
+ * @param {string} topLevelType Record from `EventConstants`.
+ * @param {DOMEventTarget} topLevelTarget The listening component root node.
+ * @param {string} topLevelTargetID ID of `topLevelTarget`.
+ * @param {object} nativeEvent Native browser event.
+ * @return {*} An accumulation of synthetic events.
+ * @see {EventPluginHub.extractEvents}
+ */
+ extractEvents: function (topLevelType, topLevelTarget, topLevelTargetID, nativeEvent, nativeEventTarget) {
+ if (!hasListener) {
+ return null;
+ }
+
+ switch (topLevelType) {
+ // Track the input node that has focus.
+ case topLevelTypes.topFocus:
+ if (isTextInputElement(topLevelTarget) || topLevelTarget.contentEditable === 'true') {
+ activeElement = topLevelTarget;
+ activeElementID = topLevelTargetID;
+ lastSelection = null;
+ }
+ break;
+ case topLevelTypes.topBlur:
+ activeElement = null;
+ activeElementID = null;
+ lastSelection = null;
+ break;
+
+ // Don't fire the event while the user is dragging. This matches the
+ // semantics of the native select event.
+ case topLevelTypes.topMouseDown:
+ mouseDown = true;
+ break;
+ case topLevelTypes.topContextMenu:
+ case topLevelTypes.topMouseUp:
+ mouseDown = false;
+ return constructSelectEvent(nativeEvent, nativeEventTarget);
+
+ // Chrome and IE fire non-standard event when selection is changed (and
+ // sometimes when it hasn't). IE's event fires out of order with respect
+ // to key and input events on deletion, so we discard it.
+ //
+ // Firefox doesn't support selectionchange, so check selection status
+ // after each key entry. The selection changes after keydown and before
+ // keyup, but we check on keydown as well in the case of holding down a
+ // key, when multiple keydown events are fired but only one keyup is.
+ // This is also our approach for IE handling, for the reason above.
+ case topLevelTypes.topSelectionChange:
+ if (skipSelectionChangeEvent) {
+ break;
+ }
+ // falls through
+ case topLevelTypes.topKeyDown:
+ case topLevelTypes.topKeyUp:
+ return constructSelectEvent(nativeEvent, nativeEventTarget);
+ }
+
+ return null;
+ },
+
+ didPutListener: function (id, registrationName, listener) {
+ if (registrationName === ON_SELECT_KEY) {
+ hasListener = true;
+ }
+ }
+};
+
+module.exports = SelectEventPlugin;
+},{"105":105,"134":134,"147":147,"15":15,"156":156,"166":166,"171":171,"19":19,"66":66}],100:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ServerReactRootIndex
+ * @typechecks
+ */
+
+'use strict';
+
+/**
+ * Size of the reactRoot ID space. We generate random numbers for React root
+ * IDs and if there's a collision the events and DOM update system will
+ * get confused. In the future we need a way to generate GUIDs but for
+ * now this will work on a smaller scale.
+ */
+var GLOBAL_MOUNT_POINT_MAX = Math.pow(2, 53);
+
+var ServerReactRootIndex = {
+ createReactRootIndex: function () {
+ return Math.ceil(Math.random() * GLOBAL_MOUNT_POINT_MAX);
+ }
+};
+
+module.exports = ServerReactRootIndex;
+},{}],101:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule SimpleEventPlugin
+ */
+
+'use strict';
+
+var EventConstants = _dereq_(15);
+var EventListener = _dereq_(146);
+var EventPropagators = _dereq_(19);
+var ReactMount = _dereq_(72);
+var SyntheticClipboardEvent = _dereq_(102);
+var SyntheticEvent = _dereq_(105);
+var SyntheticFocusEvent = _dereq_(106);
+var SyntheticKeyboardEvent = _dereq_(108);
+var SyntheticMouseEvent = _dereq_(109);
+var SyntheticDragEvent = _dereq_(104);
+var SyntheticTouchEvent = _dereq_(110);
+var SyntheticUIEvent = _dereq_(111);
+var SyntheticWheelEvent = _dereq_(112);
+
+var emptyFunction = _dereq_(153);
+var getEventCharCode = _dereq_(125);
+var invariant = _dereq_(161);
+var keyOf = _dereq_(166);
+
+var topLevelTypes = EventConstants.topLevelTypes;
+
+var eventTypes = {
+ abort: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onAbort: true }),
+ captured: keyOf({ onAbortCapture: true })
+ }
+ },
+ blur: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onBlur: true }),
+ captured: keyOf({ onBlurCapture: true })
+ }
+ },
+ canPlay: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onCanPlay: true }),
+ captured: keyOf({ onCanPlayCapture: true })
+ }
+ },
+ canPlayThrough: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onCanPlayThrough: true }),
+ captured: keyOf({ onCanPlayThroughCapture: true })
+ }
+ },
+ click: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onClick: true }),
+ captured: keyOf({ onClickCapture: true })
+ }
+ },
+ contextMenu: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onContextMenu: true }),
+ captured: keyOf({ onContextMenuCapture: true })
+ }
+ },
+ copy: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onCopy: true }),
+ captured: keyOf({ onCopyCapture: true })
+ }
+ },
+ cut: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onCut: true }),
+ captured: keyOf({ onCutCapture: true })
+ }
+ },
+ doubleClick: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onDoubleClick: true }),
+ captured: keyOf({ onDoubleClickCapture: true })
+ }
+ },
+ drag: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onDrag: true }),
+ captured: keyOf({ onDragCapture: true })
+ }
+ },
+ dragEnd: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onDragEnd: true }),
+ captured: keyOf({ onDragEndCapture: true })
+ }
+ },
+ dragEnter: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onDragEnter: true }),
+ captured: keyOf({ onDragEnterCapture: true })
+ }
+ },
+ dragExit: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onDragExit: true }),
+ captured: keyOf({ onDragExitCapture: true })
+ }
+ },
+ dragLeave: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onDragLeave: true }),
+ captured: keyOf({ onDragLeaveCapture: true })
+ }
+ },
+ dragOver: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onDragOver: true }),
+ captured: keyOf({ onDragOverCapture: true })
+ }
+ },
+ dragStart: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onDragStart: true }),
+ captured: keyOf({ onDragStartCapture: true })
+ }
+ },
+ drop: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onDrop: true }),
+ captured: keyOf({ onDropCapture: true })
+ }
+ },
+ durationChange: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onDurationChange: true }),
+ captured: keyOf({ onDurationChangeCapture: true })
+ }
+ },
+ emptied: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onEmptied: true }),
+ captured: keyOf({ onEmptiedCapture: true })
+ }
+ },
+ encrypted: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onEncrypted: true }),
+ captured: keyOf({ onEncryptedCapture: true })
+ }
+ },
+ ended: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onEnded: true }),
+ captured: keyOf({ onEndedCapture: true })
+ }
+ },
+ error: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onError: true }),
+ captured: keyOf({ onErrorCapture: true })
+ }
+ },
+ focus: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onFocus: true }),
+ captured: keyOf({ onFocusCapture: true })
+ }
+ },
+ input: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onInput: true }),
+ captured: keyOf({ onInputCapture: true })
+ }
+ },
+ keyDown: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onKeyDown: true }),
+ captured: keyOf({ onKeyDownCapture: true })
+ }
+ },
+ keyPress: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onKeyPress: true }),
+ captured: keyOf({ onKeyPressCapture: true })
+ }
+ },
+ keyUp: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onKeyUp: true }),
+ captured: keyOf({ onKeyUpCapture: true })
+ }
+ },
+ load: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onLoad: true }),
+ captured: keyOf({ onLoadCapture: true })
+ }
+ },
+ loadedData: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onLoadedData: true }),
+ captured: keyOf({ onLoadedDataCapture: true })
+ }
+ },
+ loadedMetadata: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onLoadedMetadata: true }),
+ captured: keyOf({ onLoadedMetadataCapture: true })
+ }
+ },
+ loadStart: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onLoadStart: true }),
+ captured: keyOf({ onLoadStartCapture: true })
+ }
+ },
+ // Note: We do not allow listening to mouseOver events. Instead, use the
+ // onMouseEnter/onMouseLeave created by `EnterLeaveEventPlugin`.
+ mouseDown: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onMouseDown: true }),
+ captured: keyOf({ onMouseDownCapture: true })
+ }
+ },
+ mouseMove: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onMouseMove: true }),
+ captured: keyOf({ onMouseMoveCapture: true })
+ }
+ },
+ mouseOut: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onMouseOut: true }),
+ captured: keyOf({ onMouseOutCapture: true })
+ }
+ },
+ mouseOver: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onMouseOver: true }),
+ captured: keyOf({ onMouseOverCapture: true })
+ }
+ },
+ mouseUp: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onMouseUp: true }),
+ captured: keyOf({ onMouseUpCapture: true })
+ }
+ },
+ paste: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onPaste: true }),
+ captured: keyOf({ onPasteCapture: true })
+ }
+ },
+ pause: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onPause: true }),
+ captured: keyOf({ onPauseCapture: true })
+ }
+ },
+ play: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onPlay: true }),
+ captured: keyOf({ onPlayCapture: true })
+ }
+ },
+ playing: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onPlaying: true }),
+ captured: keyOf({ onPlayingCapture: true })
+ }
+ },
+ progress: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onProgress: true }),
+ captured: keyOf({ onProgressCapture: true })
+ }
+ },
+ rateChange: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onRateChange: true }),
+ captured: keyOf({ onRateChangeCapture: true })
+ }
+ },
+ reset: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onReset: true }),
+ captured: keyOf({ onResetCapture: true })
+ }
+ },
+ scroll: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onScroll: true }),
+ captured: keyOf({ onScrollCapture: true })
+ }
+ },
+ seeked: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onSeeked: true }),
+ captured: keyOf({ onSeekedCapture: true })
+ }
+ },
+ seeking: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onSeeking: true }),
+ captured: keyOf({ onSeekingCapture: true })
+ }
+ },
+ stalled: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onStalled: true }),
+ captured: keyOf({ onStalledCapture: true })
+ }
+ },
+ submit: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onSubmit: true }),
+ captured: keyOf({ onSubmitCapture: true })
+ }
+ },
+ suspend: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onSuspend: true }),
+ captured: keyOf({ onSuspendCapture: true })
+ }
+ },
+ timeUpdate: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onTimeUpdate: true }),
+ captured: keyOf({ onTimeUpdateCapture: true })
+ }
+ },
+ touchCancel: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onTouchCancel: true }),
+ captured: keyOf({ onTouchCancelCapture: true })
+ }
+ },
+ touchEnd: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onTouchEnd: true }),
+ captured: keyOf({ onTouchEndCapture: true })
+ }
+ },
+ touchMove: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onTouchMove: true }),
+ captured: keyOf({ onTouchMoveCapture: true })
+ }
+ },
+ touchStart: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onTouchStart: true }),
+ captured: keyOf({ onTouchStartCapture: true })
+ }
+ },
+ volumeChange: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onVolumeChange: true }),
+ captured: keyOf({ onVolumeChangeCapture: true })
+ }
+ },
+ waiting: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onWaiting: true }),
+ captured: keyOf({ onWaitingCapture: true })
+ }
+ },
+ wheel: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onWheel: true }),
+ captured: keyOf({ onWheelCapture: true })
+ }
+ }
+};
+
+var topLevelEventsToDispatchConfig = {
+ topAbort: eventTypes.abort,
+ topBlur: eventTypes.blur,
+ topCanPlay: eventTypes.canPlay,
+ topCanPlayThrough: eventTypes.canPlayThrough,
+ topClick: eventTypes.click,
+ topContextMenu: eventTypes.contextMenu,
+ topCopy: eventTypes.copy,
+ topCut: eventTypes.cut,
+ topDoubleClick: eventTypes.doubleClick,
+ topDrag: eventTypes.drag,
+ topDragEnd: eventTypes.dragEnd,
+ topDragEnter: eventTypes.dragEnter,
+ topDragExit: eventTypes.dragExit,
+ topDragLeave: eventTypes.dragLeave,
+ topDragOver: eventTypes.dragOver,
+ topDragStart: eventTypes.dragStart,
+ topDrop: eventTypes.drop,
+ topDurationChange: eventTypes.durationChange,
+ topEmptied: eventTypes.emptied,
+ topEncrypted: eventTypes.encrypted,
+ topEnded: eventTypes.ended,
+ topError: eventTypes.error,
+ topFocus: eventTypes.focus,
+ topInput: eventTypes.input,
+ topKeyDown: eventTypes.keyDown,
+ topKeyPress: eventTypes.keyPress,
+ topKeyUp: eventTypes.keyUp,
+ topLoad: eventTypes.load,
+ topLoadedData: eventTypes.loadedData,
+ topLoadedMetadata: eventTypes.loadedMetadata,
+ topLoadStart: eventTypes.loadStart,
+ topMouseDown: eventTypes.mouseDown,
+ topMouseMove: eventTypes.mouseMove,
+ topMouseOut: eventTypes.mouseOut,
+ topMouseOver: eventTypes.mouseOver,
+ topMouseUp: eventTypes.mouseUp,
+ topPaste: eventTypes.paste,
+ topPause: eventTypes.pause,
+ topPlay: eventTypes.play,
+ topPlaying: eventTypes.playing,
+ topProgress: eventTypes.progress,
+ topRateChange: eventTypes.rateChange,
+ topReset: eventTypes.reset,
+ topScroll: eventTypes.scroll,
+ topSeeked: eventTypes.seeked,
+ topSeeking: eventTypes.seeking,
+ topStalled: eventTypes.stalled,
+ topSubmit: eventTypes.submit,
+ topSuspend: eventTypes.suspend,
+ topTimeUpdate: eventTypes.timeUpdate,
+ topTouchCancel: eventTypes.touchCancel,
+ topTouchEnd: eventTypes.touchEnd,
+ topTouchMove: eventTypes.touchMove,
+ topTouchStart: eventTypes.touchStart,
+ topVolumeChange: eventTypes.volumeChange,
+ topWaiting: eventTypes.waiting,
+ topWheel: eventTypes.wheel
+};
+
+for (var type in topLevelEventsToDispatchConfig) {
+ topLevelEventsToDispatchConfig[type].dependencies = [type];
+}
+
+var ON_CLICK_KEY = keyOf({ onClick: null });
+var onClickListeners = {};
+
+var SimpleEventPlugin = {
+
+ eventTypes: eventTypes,
+
+ /**
+ * @param {string} topLevelType Record from `EventConstants`.
+ * @param {DOMEventTarget} topLevelTarget The listening component root node.
+ * @param {string} topLevelTargetID ID of `topLevelTarget`.
+ * @param {object} nativeEvent Native browser event.
+ * @return {*} An accumulation of synthetic events.
+ * @see {EventPluginHub.extractEvents}
+ */
+ extractEvents: function (topLevelType, topLevelTarget, topLevelTargetID, nativeEvent, nativeEventTarget) {
+ var dispatchConfig = topLevelEventsToDispatchConfig[topLevelType];
+ if (!dispatchConfig) {
+ return null;
+ }
+ var EventConstructor;
+ switch (topLevelType) {
+ case topLevelTypes.topAbort:
+ case topLevelTypes.topCanPlay:
+ case topLevelTypes.topCanPlayThrough:
+ case topLevelTypes.topDurationChange:
+ case topLevelTypes.topEmptied:
+ case topLevelTypes.topEncrypted:
+ case topLevelTypes.topEnded:
+ case topLevelTypes.topError:
+ case topLevelTypes.topInput:
+ case topLevelTypes.topLoad:
+ case topLevelTypes.topLoadedData:
+ case topLevelTypes.topLoadedMetadata:
+ case topLevelTypes.topLoadStart:
+ case topLevelTypes.topPause:
+ case topLevelTypes.topPlay:
+ case topLevelTypes.topPlaying:
+ case topLevelTypes.topProgress:
+ case topLevelTypes.topRateChange:
+ case topLevelTypes.topReset:
+ case topLevelTypes.topSeeked:
+ case topLevelTypes.topSeeking:
+ case topLevelTypes.topStalled:
+ case topLevelTypes.topSubmit:
+ case topLevelTypes.topSuspend:
+ case topLevelTypes.topTimeUpdate:
+ case topLevelTypes.topVolumeChange:
+ case topLevelTypes.topWaiting:
+ // HTML Events
+ // @see http://www.w3.org/TR/html5/index.html#events-0
+ EventConstructor = SyntheticEvent;
+ break;
+ case topLevelTypes.topKeyPress:
+ // FireFox creates a keypress event for function keys too. This removes
+ // the unwanted keypress events. Enter is however both printable and
+ // non-printable. One would expect Tab to be as well (but it isn't).
+ if (getEventCharCode(nativeEvent) === 0) {
+ return null;
+ }
+ /* falls through */
+ case topLevelTypes.topKeyDown:
+ case topLevelTypes.topKeyUp:
+ EventConstructor = SyntheticKeyboardEvent;
+ break;
+ case topLevelTypes.topBlur:
+ case topLevelTypes.topFocus:
+ EventConstructor = SyntheticFocusEvent;
+ break;
+ case topLevelTypes.topClick:
+ // Firefox creates a click event on right mouse clicks. This removes the
+ // unwanted click events.
+ if (nativeEvent.button === 2) {
+ return null;
+ }
+ /* falls through */
+ case topLevelTypes.topContextMenu:
+ case topLevelTypes.topDoubleClick:
+ case topLevelTypes.topMouseDown:
+ case topLevelTypes.topMouseMove:
+ case topLevelTypes.topMouseOut:
+ case topLevelTypes.topMouseOver:
+ case topLevelTypes.topMouseUp:
+ EventConstructor = SyntheticMouseEvent;
+ break;
+ case topLevelTypes.topDrag:
+ case topLevelTypes.topDragEnd:
+ case topLevelTypes.topDragEnter:
+ case topLevelTypes.topDragExit:
+ case topLevelTypes.topDragLeave:
+ case topLevelTypes.topDragOver:
+ case topLevelTypes.topDragStart:
+ case topLevelTypes.topDrop:
+ EventConstructor = SyntheticDragEvent;
+ break;
+ case topLevelTypes.topTouchCancel:
+ case topLevelTypes.topTouchEnd:
+ case topLevelTypes.topTouchMove:
+ case topLevelTypes.topTouchStart:
+ EventConstructor = SyntheticTouchEvent;
+ break;
+ case topLevelTypes.topScroll:
+ EventConstructor = SyntheticUIEvent;
+ break;
+ case topLevelTypes.topWheel:
+ EventConstructor = SyntheticWheelEvent;
+ break;
+ case topLevelTypes.topCopy:
+ case topLevelTypes.topCut:
+ case topLevelTypes.topPaste:
+ EventConstructor = SyntheticClipboardEvent;
+ break;
+ }
+ !EventConstructor ? "production" !== 'production' ? invariant(false, 'SimpleEventPlugin: Unhandled event type, `%s`.', topLevelType) : invariant(false) : undefined;
+ var event = EventConstructor.getPooled(dispatchConfig, topLevelTargetID, nativeEvent, nativeEventTarget);
+ EventPropagators.accumulateTwoPhaseDispatches(event);
+ return event;
+ },
+
+ didPutListener: function (id, registrationName, listener) {
+ // Mobile Safari does not fire properly bubble click events on
+ // non-interactive elements, which means delegated click listeners do not
+ // fire. The workaround for this bug involves attaching an empty click
+ // listener on the target node.
+ if (registrationName === ON_CLICK_KEY) {
+ var node = ReactMount.getNode(id);
+ if (!onClickListeners[id]) {
+ onClickListeners[id] = EventListener.listen(node, 'click', emptyFunction);
+ }
+ }
+ },
+
+ willDeleteListener: function (id, registrationName) {
+ if (registrationName === ON_CLICK_KEY) {
+ onClickListeners[id].remove();
+ delete onClickListeners[id];
+ }
+ }
+
+};
+
+module.exports = SimpleEventPlugin;
+},{"102":102,"104":104,"105":105,"106":106,"108":108,"109":109,"110":110,"111":111,"112":112,"125":125,"146":146,"15":15,"153":153,"161":161,"166":166,"19":19,"72":72}],102:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule SyntheticClipboardEvent
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var SyntheticEvent = _dereq_(105);
+
+/**
+ * @interface Event
+ * @see http://www.w3.org/TR/clipboard-apis/
+ */
+var ClipboardEventInterface = {
+ clipboardData: function (event) {
+ return 'clipboardData' in event ? event.clipboardData : window.clipboardData;
+ }
+};
+
+/**
+ * @param {object} dispatchConfig Configuration used to dispatch this event.
+ * @param {string} dispatchMarker Marker identifying the event target.
+ * @param {object} nativeEvent Native browser event.
+ * @extends {SyntheticUIEvent}
+ */
+function SyntheticClipboardEvent(dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget) {
+ SyntheticEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget);
+}
+
+SyntheticEvent.augmentClass(SyntheticClipboardEvent, ClipboardEventInterface);
+
+module.exports = SyntheticClipboardEvent;
+},{"105":105}],103:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule SyntheticCompositionEvent
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var SyntheticEvent = _dereq_(105);
+
+/**
+ * @interface Event
+ * @see http://www.w3.org/TR/DOM-Level-3-Events/#events-compositionevents
+ */
+var CompositionEventInterface = {
+ data: null
+};
+
+/**
+ * @param {object} dispatchConfig Configuration used to dispatch this event.
+ * @param {string} dispatchMarker Marker identifying the event target.
+ * @param {object} nativeEvent Native browser event.
+ * @extends {SyntheticUIEvent}
+ */
+function SyntheticCompositionEvent(dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget) {
+ SyntheticEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget);
+}
+
+SyntheticEvent.augmentClass(SyntheticCompositionEvent, CompositionEventInterface);
+
+module.exports = SyntheticCompositionEvent;
+},{"105":105}],104:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule SyntheticDragEvent
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var SyntheticMouseEvent = _dereq_(109);
+
+/**
+ * @interface DragEvent
+ * @see http://www.w3.org/TR/DOM-Level-3-Events/
+ */
+var DragEventInterface = {
+ dataTransfer: null
+};
+
+/**
+ * @param {object} dispatchConfig Configuration used to dispatch this event.
+ * @param {string} dispatchMarker Marker identifying the event target.
+ * @param {object} nativeEvent Native browser event.
+ * @extends {SyntheticUIEvent}
+ */
+function SyntheticDragEvent(dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget) {
+ SyntheticMouseEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget);
+}
+
+SyntheticMouseEvent.augmentClass(SyntheticDragEvent, DragEventInterface);
+
+module.exports = SyntheticDragEvent;
+},{"109":109}],105:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule SyntheticEvent
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var PooledClass = _dereq_(25);
+
+var assign = _dereq_(24);
+var emptyFunction = _dereq_(153);
+var warning = _dereq_(173);
+
+/**
+ * @interface Event
+ * @see http://www.w3.org/TR/DOM-Level-3-Events/
+ */
+var EventInterface = {
+ type: null,
+ // currentTarget is set when dispatching; no use in copying it here
+ currentTarget: emptyFunction.thatReturnsNull,
+ eventPhase: null,
+ bubbles: null,
+ cancelable: null,
+ timeStamp: function (event) {
+ return event.timeStamp || Date.now();
+ },
+ defaultPrevented: null,
+ isTrusted: null
+};
+
+/**
+ * Synthetic events are dispatched by event plugins, typically in response to a
+ * top-level event delegation handler.
+ *
+ * These systems should generally use pooling to reduce the frequency of garbage
+ * collection. The system should check `isPersistent` to determine whether the
+ * event should be released into the pool after being dispatched. Users that
+ * need a persisted event should invoke `persist`.
+ *
+ * Synthetic events (and subclasses) implement the DOM Level 3 Events API by
+ * normalizing browser quirks. Subclasses do not necessarily have to implement a
+ * DOM interface; custom application-specific events can also subclass this.
+ *
+ * @param {object} dispatchConfig Configuration used to dispatch this event.
+ * @param {string} dispatchMarker Marker identifying the event target.
+ * @param {object} nativeEvent Native browser event.
+ */
+function SyntheticEvent(dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget) {
+ this.dispatchConfig = dispatchConfig;
+ this.dispatchMarker = dispatchMarker;
+ this.nativeEvent = nativeEvent;
+ this.target = nativeEventTarget;
+ this.currentTarget = nativeEventTarget;
+
+ var Interface = this.constructor.Interface;
+ for (var propName in Interface) {
+ if (!Interface.hasOwnProperty(propName)) {
+ continue;
+ }
+ var normalize = Interface[propName];
+ if (normalize) {
+ this[propName] = normalize(nativeEvent);
+ } else {
+ this[propName] = nativeEvent[propName];
+ }
+ }
+
+ var defaultPrevented = nativeEvent.defaultPrevented != null ? nativeEvent.defaultPrevented : nativeEvent.returnValue === false;
+ if (defaultPrevented) {
+ this.isDefaultPrevented = emptyFunction.thatReturnsTrue;
+ } else {
+ this.isDefaultPrevented = emptyFunction.thatReturnsFalse;
+ }
+ this.isPropagationStopped = emptyFunction.thatReturnsFalse;
+}
+
+assign(SyntheticEvent.prototype, {
+
+ preventDefault: function () {
+ this.defaultPrevented = true;
+ var event = this.nativeEvent;
+ if ("production" !== 'production') {
+ "production" !== 'production' ? warning(event, 'This synthetic event is reused for performance reasons. If you\'re ' + 'seeing this, you\'re calling `preventDefault` on a ' + 'released/nullified synthetic event. This is a no-op. See ' + 'https://fb.me/react-event-pooling for more information.') : undefined;
+ }
+ if (!event) {
+ return;
+ }
+
+ if (event.preventDefault) {
+ event.preventDefault();
+ } else {
+ event.returnValue = false;
+ }
+ this.isDefaultPrevented = emptyFunction.thatReturnsTrue;
+ },
+
+ stopPropagation: function () {
+ var event = this.nativeEvent;
+ if ("production" !== 'production') {
+ "production" !== 'production' ? warning(event, 'This synthetic event is reused for performance reasons. If you\'re ' + 'seeing this, you\'re calling `stopPropagation` on a ' + 'released/nullified synthetic event. This is a no-op. See ' + 'https://fb.me/react-event-pooling for more information.') : undefined;
+ }
+ if (!event) {
+ return;
+ }
+
+ if (event.stopPropagation) {
+ event.stopPropagation();
+ } else {
+ event.cancelBubble = true;
+ }
+ this.isPropagationStopped = emptyFunction.thatReturnsTrue;
+ },
+
+ /**
+ * We release all dispatched `SyntheticEvent`s after each event loop, adding
+ * them back into the pool. This allows a way to hold onto a reference that
+ * won't be added back into the pool.
+ */
+ persist: function () {
+ this.isPersistent = emptyFunction.thatReturnsTrue;
+ },
+
+ /**
+ * Checks if this event should be released back into the pool.
+ *
+ * @return {boolean} True if this should not be released, false otherwise.
+ */
+ isPersistent: emptyFunction.thatReturnsFalse,
+
+ /**
+ * `PooledClass` looks for `destructor` on each instance it releases.
+ */
+ destructor: function () {
+ var Interface = this.constructor.Interface;
+ for (var propName in Interface) {
+ this[propName] = null;
+ }
+ this.dispatchConfig = null;
+ this.dispatchMarker = null;
+ this.nativeEvent = null;
+ }
+
+});
+
+SyntheticEvent.Interface = EventInterface;
+
+/**
+ * Helper to reduce boilerplate when creating subclasses.
+ *
+ * @param {function} Class
+ * @param {?object} Interface
+ */
+SyntheticEvent.augmentClass = function (Class, Interface) {
+ var Super = this;
+
+ var prototype = Object.create(Super.prototype);
+ assign(prototype, Class.prototype);
+ Class.prototype = prototype;
+ Class.prototype.constructor = Class;
+
+ Class.Interface = assign({}, Super.Interface, Interface);
+ Class.augmentClass = Super.augmentClass;
+
+ PooledClass.addPoolingTo(Class, PooledClass.fourArgumentPooler);
+};
+
+PooledClass.addPoolingTo(SyntheticEvent, PooledClass.fourArgumentPooler);
+
+module.exports = SyntheticEvent;
+},{"153":153,"173":173,"24":24,"25":25}],106:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule SyntheticFocusEvent
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var SyntheticUIEvent = _dereq_(111);
+
+/**
+ * @interface FocusEvent
+ * @see http://www.w3.org/TR/DOM-Level-3-Events/
+ */
+var FocusEventInterface = {
+ relatedTarget: null
+};
+
+/**
+ * @param {object} dispatchConfig Configuration used to dispatch this event.
+ * @param {string} dispatchMarker Marker identifying the event target.
+ * @param {object} nativeEvent Native browser event.
+ * @extends {SyntheticUIEvent}
+ */
+function SyntheticFocusEvent(dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget) {
+ SyntheticUIEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget);
+}
+
+SyntheticUIEvent.augmentClass(SyntheticFocusEvent, FocusEventInterface);
+
+module.exports = SyntheticFocusEvent;
+},{"111":111}],107:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule SyntheticInputEvent
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var SyntheticEvent = _dereq_(105);
+
+/**
+ * @interface Event
+ * @see http://www.w3.org/TR/2013/WD-DOM-Level-3-Events-20131105
+ * /#events-inputevents
+ */
+var InputEventInterface = {
+ data: null
+};
+
+/**
+ * @param {object} dispatchConfig Configuration used to dispatch this event.
+ * @param {string} dispatchMarker Marker identifying the event target.
+ * @param {object} nativeEvent Native browser event.
+ * @extends {SyntheticUIEvent}
+ */
+function SyntheticInputEvent(dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget) {
+ SyntheticEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget);
+}
+
+SyntheticEvent.augmentClass(SyntheticInputEvent, InputEventInterface);
+
+module.exports = SyntheticInputEvent;
+},{"105":105}],108:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule SyntheticKeyboardEvent
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var SyntheticUIEvent = _dereq_(111);
+
+var getEventCharCode = _dereq_(125);
+var getEventKey = _dereq_(126);
+var getEventModifierState = _dereq_(127);
+
+/**
+ * @interface KeyboardEvent
+ * @see http://www.w3.org/TR/DOM-Level-3-Events/
+ */
+var KeyboardEventInterface = {
+ key: getEventKey,
+ location: null,
+ ctrlKey: null,
+ shiftKey: null,
+ altKey: null,
+ metaKey: null,
+ repeat: null,
+ locale: null,
+ getModifierState: getEventModifierState,
+ // Legacy Interface
+ charCode: function (event) {
+ // `charCode` is the result of a KeyPress event and represents the value of
+ // the actual printable character.
+
+ // KeyPress is deprecated, but its replacement is not yet final and not
+ // implemented in any major browser. Only KeyPress has charCode.
+ if (event.type === 'keypress') {
+ return getEventCharCode(event);
+ }
+ return 0;
+ },
+ keyCode: function (event) {
+ // `keyCode` is the result of a KeyDown/Up event and represents the value of
+ // physical keyboard key.
+
+ // The actual meaning of the value depends on the users' keyboard layout
+ // which cannot be detected. Assuming that it is a US keyboard layout
+ // provides a surprisingly accurate mapping for US and European users.
+ // Due to this, it is left to the user to implement at this time.
+ if (event.type === 'keydown' || event.type === 'keyup') {
+ return event.keyCode;
+ }
+ return 0;
+ },
+ which: function (event) {
+ // `which` is an alias for either `keyCode` or `charCode` depending on the
+ // type of the event.
+ if (event.type === 'keypress') {
+ return getEventCharCode(event);
+ }
+ if (event.type === 'keydown' || event.type === 'keyup') {
+ return event.keyCode;
+ }
+ return 0;
+ }
+};
+
+/**
+ * @param {object} dispatchConfig Configuration used to dispatch this event.
+ * @param {string} dispatchMarker Marker identifying the event target.
+ * @param {object} nativeEvent Native browser event.
+ * @extends {SyntheticUIEvent}
+ */
+function SyntheticKeyboardEvent(dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget) {
+ SyntheticUIEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget);
+}
+
+SyntheticUIEvent.augmentClass(SyntheticKeyboardEvent, KeyboardEventInterface);
+
+module.exports = SyntheticKeyboardEvent;
+},{"111":111,"125":125,"126":126,"127":127}],109:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule SyntheticMouseEvent
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var SyntheticUIEvent = _dereq_(111);
+var ViewportMetrics = _dereq_(114);
+
+var getEventModifierState = _dereq_(127);
+
+/**
+ * @interface MouseEvent
+ * @see http://www.w3.org/TR/DOM-Level-3-Events/
+ */
+var MouseEventInterface = {
+ screenX: null,
+ screenY: null,
+ clientX: null,
+ clientY: null,
+ ctrlKey: null,
+ shiftKey: null,
+ altKey: null,
+ metaKey: null,
+ getModifierState: getEventModifierState,
+ button: function (event) {
+ // Webkit, Firefox, IE9+
+ // which: 1 2 3
+ // button: 0 1 2 (standard)
+ var button = event.button;
+ if ('which' in event) {
+ return button;
+ }
+ // IE<9
+ // which: undefined
+ // button: 0 0 0
+ // button: 1 4 2 (onmouseup)
+ return button === 2 ? 2 : button === 4 ? 1 : 0;
+ },
+ buttons: null,
+ relatedTarget: function (event) {
+ return event.relatedTarget || (event.fromElement === event.srcElement ? event.toElement : event.fromElement);
+ },
+ // "Proprietary" Interface.
+ pageX: function (event) {
+ return 'pageX' in event ? event.pageX : event.clientX + ViewportMetrics.currentScrollLeft;
+ },
+ pageY: function (event) {
+ return 'pageY' in event ? event.pageY : event.clientY + ViewportMetrics.currentScrollTop;
+ }
+};
+
+/**
+ * @param {object} dispatchConfig Configuration used to dispatch this event.
+ * @param {string} dispatchMarker Marker identifying the event target.
+ * @param {object} nativeEvent Native browser event.
+ * @extends {SyntheticUIEvent}
+ */
+function SyntheticMouseEvent(dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget) {
+ SyntheticUIEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget);
+}
+
+SyntheticUIEvent.augmentClass(SyntheticMouseEvent, MouseEventInterface);
+
+module.exports = SyntheticMouseEvent;
+},{"111":111,"114":114,"127":127}],110:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule SyntheticTouchEvent
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var SyntheticUIEvent = _dereq_(111);
+
+var getEventModifierState = _dereq_(127);
+
+/**
+ * @interface TouchEvent
+ * @see http://www.w3.org/TR/touch-events/
+ */
+var TouchEventInterface = {
+ touches: null,
+ targetTouches: null,
+ changedTouches: null,
+ altKey: null,
+ metaKey: null,
+ ctrlKey: null,
+ shiftKey: null,
+ getModifierState: getEventModifierState
+};
+
+/**
+ * @param {object} dispatchConfig Configuration used to dispatch this event.
+ * @param {string} dispatchMarker Marker identifying the event target.
+ * @param {object} nativeEvent Native browser event.
+ * @extends {SyntheticUIEvent}
+ */
+function SyntheticTouchEvent(dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget) {
+ SyntheticUIEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget);
+}
+
+SyntheticUIEvent.augmentClass(SyntheticTouchEvent, TouchEventInterface);
+
+module.exports = SyntheticTouchEvent;
+},{"111":111,"127":127}],111:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule SyntheticUIEvent
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var SyntheticEvent = _dereq_(105);
+
+var getEventTarget = _dereq_(128);
+
+/**
+ * @interface UIEvent
+ * @see http://www.w3.org/TR/DOM-Level-3-Events/
+ */
+var UIEventInterface = {
+ view: function (event) {
+ if (event.view) {
+ return event.view;
+ }
+
+ var target = getEventTarget(event);
+ if (target != null && target.window === target) {
+ // target is a window object
+ return target;
+ }
+
+ var doc = target.ownerDocument;
+ // TODO: Figure out why `ownerDocument` is sometimes undefined in IE8.
+ if (doc) {
+ return doc.defaultView || doc.parentWindow;
+ } else {
+ return window;
+ }
+ },
+ detail: function (event) {
+ return event.detail || 0;
+ }
+};
+
+/**
+ * @param {object} dispatchConfig Configuration used to dispatch this event.
+ * @param {string} dispatchMarker Marker identifying the event target.
+ * @param {object} nativeEvent Native browser event.
+ * @extends {SyntheticEvent}
+ */
+function SyntheticUIEvent(dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget) {
+ SyntheticEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget);
+}
+
+SyntheticEvent.augmentClass(SyntheticUIEvent, UIEventInterface);
+
+module.exports = SyntheticUIEvent;
+},{"105":105,"128":128}],112:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule SyntheticWheelEvent
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var SyntheticMouseEvent = _dereq_(109);
+
+/**
+ * @interface WheelEvent
+ * @see http://www.w3.org/TR/DOM-Level-3-Events/
+ */
+var WheelEventInterface = {
+ deltaX: function (event) {
+ return 'deltaX' in event ? event.deltaX :
+ // Fallback to `wheelDeltaX` for Webkit and normalize (right is positive).
+ 'wheelDeltaX' in event ? -event.wheelDeltaX : 0;
+ },
+ deltaY: function (event) {
+ return 'deltaY' in event ? event.deltaY :
+ // Fallback to `wheelDeltaY` for Webkit and normalize (down is positive).
+ 'wheelDeltaY' in event ? -event.wheelDeltaY :
+ // Fallback to `wheelDelta` for IE<9 and normalize (down is positive).
+ 'wheelDelta' in event ? -event.wheelDelta : 0;
+ },
+ deltaZ: null,
+
+ // Browsers without "deltaMode" is reporting in raw wheel delta where one
+ // notch on the scroll is always +/- 120, roughly equivalent to pixels.
+ // A good approximation of DOM_DELTA_LINE (1) is 5% of viewport size or
+ // ~40 pixels, for DOM_DELTA_SCREEN (2) it is 87.5% of viewport size.
+ deltaMode: null
+};
+
+/**
+ * @param {object} dispatchConfig Configuration used to dispatch this event.
+ * @param {string} dispatchMarker Marker identifying the event target.
+ * @param {object} nativeEvent Native browser event.
+ * @extends {SyntheticMouseEvent}
+ */
+function SyntheticWheelEvent(dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget) {
+ SyntheticMouseEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget);
+}
+
+SyntheticMouseEvent.augmentClass(SyntheticWheelEvent, WheelEventInterface);
+
+module.exports = SyntheticWheelEvent;
+},{"109":109}],113:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule Transaction
+ */
+
+'use strict';
+
+var invariant = _dereq_(161);
+
+/**
+ * `Transaction` creates a black box that is able to wrap any method such that
+ * certain invariants are maintained before and after the method is invoked
+ * (Even if an exception is thrown while invoking the wrapped method). Whoever
+ * instantiates a transaction can provide enforcers of the invariants at
+ * creation time. The `Transaction` class itself will supply one additional
+ * automatic invariant for you - the invariant that any transaction instance
+ * should not be run while it is already being run. You would typically create a
+ * single instance of a `Transaction` for reuse multiple times, that potentially
+ * is used to wrap several different methods. Wrappers are extremely simple -
+ * they only require implementing two methods.
+ *
+ * <pre>
+ * wrappers (injected at creation time)
+ * + +
+ * | |
+ * +-----------------|--------|--------------+
+ * | v | |
+ * | +---------------+ | |
+ * | +--| wrapper1 |---|----+ |
+ * | | +---------------+ v | |
+ * | | +-------------+ | |
+ * | | +----| wrapper2 |--------+ |
+ * | | | +-------------+ | | |
+ * | | | | | |
+ * | v v v v | wrapper
+ * | +---+ +---+ +---------+ +---+ +---+ | invariants
+ * perform(anyMethod) | | | | | | | | | | | | maintained
+ * +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|-------->
+ * | | | | | | | | | | | |
+ * | | | | | | | | | | | |
+ * | | | | | | | | | | | |
+ * | +---+ +---+ +---------+ +---+ +---+ |
+ * | initialize close |
+ * +-----------------------------------------+
+ * </pre>
+ *
+ * Use cases:
+ * - Preserving the input selection ranges before/after reconciliation.
+ * Restoring selection even in the event of an unexpected error.
+ * - Deactivating events while rearranging the DOM, preventing blurs/focuses,
+ * while guaranteeing that afterwards, the event system is reactivated.
+ * - Flushing a queue of collected DOM mutations to the main UI thread after a
+ * reconciliation takes place in a worker thread.
+ * - Invoking any collected `componentDidUpdate` callbacks after rendering new
+ * content.
+ * - (Future use case): Wrapping particular flushes of the `ReactWorker` queue
+ * to preserve the `scrollTop` (an automatic scroll aware DOM).
+ * - (Future use case): Layout calculations before and after DOM updates.
+ *
+ * Transactional plugin API:
+ * - A module that has an `initialize` method that returns any precomputation.
+ * - and a `close` method that accepts the precomputation. `close` is invoked
+ * when the wrapped process is completed, or has failed.
+ *
+ * @param {Array<TransactionalWrapper>} transactionWrapper Wrapper modules
+ * that implement `initialize` and `close`.
+ * @return {Transaction} Single transaction for reuse in thread.
+ *
+ * @class Transaction
+ */
+var Mixin = {
+ /**
+ * Sets up this instance so that it is prepared for collecting metrics. Does
+ * so such that this setup method may be used on an instance that is already
+ * initialized, in a way that does not consume additional memory upon reuse.
+ * That can be useful if you decide to make your subclass of this mixin a
+ * "PooledClass".
+ */
+ reinitializeTransaction: function () {
+ this.transactionWrappers = this.getTransactionWrappers();
+ if (this.wrapperInitData) {
+ this.wrapperInitData.length = 0;
+ } else {
+ this.wrapperInitData = [];
+ }
+ this._isInTransaction = false;
+ },
+
+ _isInTransaction: false,
+
+ /**
+ * @abstract
+ * @return {Array<TransactionWrapper>} Array of transaction wrappers.
+ */
+ getTransactionWrappers: null,
+
+ isInTransaction: function () {
+ return !!this._isInTransaction;
+ },
+
+ /**
+ * Executes the function within a safety window. Use this for the top level
+ * methods that result in large amounts of computation/mutations that would
+ * need to be safety checked. The optional arguments helps prevent the need
+ * to bind in many cases.
+ *
+ * @param {function} method Member of scope to call.
+ * @param {Object} scope Scope to invoke from.
+ * @param {Object?=} a Argument to pass to the method.
+ * @param {Object?=} b Argument to pass to the method.
+ * @param {Object?=} c Argument to pass to the method.
+ * @param {Object?=} d Argument to pass to the method.
+ * @param {Object?=} e Argument to pass to the method.
+ * @param {Object?=} f Argument to pass to the method.
+ *
+ * @return {*} Return value from `method`.
+ */
+ perform: function (method, scope, a, b, c, d, e, f) {
+ !!this.isInTransaction() ? "production" !== 'production' ? invariant(false, 'Transaction.perform(...): Cannot initialize a transaction when there ' + 'is already an outstanding transaction.') : invariant(false) : undefined;
+ var errorThrown;
+ var ret;
+ try {
+ this._isInTransaction = true;
+ // Catching errors makes debugging more difficult, so we start with
+ // errorThrown set to true before setting it to false after calling
+ // close -- if it's still set to true in the finally block, it means
+ // one of these calls threw.
+ errorThrown = true;
+ this.initializeAll(0);
+ ret = method.call(scope, a, b, c, d, e, f);
+ errorThrown = false;
+ } finally {
+ try {
+ if (errorThrown) {
+ // If `method` throws, prefer to show that stack trace over any thrown
+ // by invoking `closeAll`.
+ try {
+ this.closeAll(0);
+ } catch (err) {}
+ } else {
+ // Since `method` didn't throw, we don't want to silence the exception
+ // here.
+ this.closeAll(0);
+ }
+ } finally {
+ this._isInTransaction = false;
+ }
+ }
+ return ret;
+ },
+
+ initializeAll: function (startIndex) {
+ var transactionWrappers = this.transactionWrappers;
+ for (var i = startIndex; i < transactionWrappers.length; i++) {
+ var wrapper = transactionWrappers[i];
+ try {
+ // Catching errors makes debugging more difficult, so we start with the
+ // OBSERVED_ERROR state before overwriting it with the real return value
+ // of initialize -- if it's still set to OBSERVED_ERROR in the finally
+ // block, it means wrapper.initialize threw.
+ this.wrapperInitData[i] = Transaction.OBSERVED_ERROR;
+ this.wrapperInitData[i] = wrapper.initialize ? wrapper.initialize.call(this) : null;
+ } finally {
+ if (this.wrapperInitData[i] === Transaction.OBSERVED_ERROR) {
+ // The initializer for wrapper i threw an error; initialize the
+ // remaining wrappers but silence any exceptions from them to ensure
+ // that the first error is the one to bubble up.
+ try {
+ this.initializeAll(i + 1);
+ } catch (err) {}
+ }
+ }
+ }
+ },
+
+ /**
+ * Invokes each of `this.transactionWrappers.close[i]` functions, passing into
+ * them the respective return values of `this.transactionWrappers.init[i]`
+ * (`close`rs that correspond to initializers that failed will not be
+ * invoked).
+ */
+ closeAll: function (startIndex) {
+ !this.isInTransaction() ? "production" !== 'production' ? invariant(false, 'Transaction.closeAll(): Cannot close transaction when none are open.') : invariant(false) : undefined;
+ var transactionWrappers = this.transactionWrappers;
+ for (var i = startIndex; i < transactionWrappers.length; i++) {
+ var wrapper = transactionWrappers[i];
+ var initData = this.wrapperInitData[i];
+ var errorThrown;
+ try {
+ // Catching errors makes debugging more difficult, so we start with
+ // errorThrown set to true before setting it to false after calling
+ // close -- if it's still set to true in the finally block, it means
+ // wrapper.close threw.
+ errorThrown = true;
+ if (initData !== Transaction.OBSERVED_ERROR && wrapper.close) {
+ wrapper.close.call(this, initData);
+ }
+ errorThrown = false;
+ } finally {
+ if (errorThrown) {
+ // The closer for wrapper i threw an error; close the remaining
+ // wrappers but silence any exceptions from them to ensure that the
+ // first error is the one to bubble up.
+ try {
+ this.closeAll(i + 1);
+ } catch (e) {}
+ }
+ }
+ }
+ this.wrapperInitData.length = 0;
+ }
+};
+
+var Transaction = {
+
+ Mixin: Mixin,
+
+ /**
+ * Token to look for to determine if an error occurred.
+ */
+ OBSERVED_ERROR: {}
+
+};
+
+module.exports = Transaction;
+},{"161":161}],114:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ViewportMetrics
+ */
+
+'use strict';
+
+var ViewportMetrics = {
+
+ currentScrollLeft: 0,
+
+ currentScrollTop: 0,
+
+ refreshScrollValues: function (scrollPosition) {
+ ViewportMetrics.currentScrollLeft = scrollPosition.x;
+ ViewportMetrics.currentScrollTop = scrollPosition.y;
+ }
+
+};
+
+module.exports = ViewportMetrics;
+},{}],115:[function(_dereq_,module,exports){
+/**
+ * Copyright 2014-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule accumulateInto
+ */
+
+'use strict';
+
+var invariant = _dereq_(161);
+
+/**
+ *
+ * Accumulates items that must not be null or undefined into the first one. This
+ * is used to conserve memory by avoiding array allocations, and thus sacrifices
+ * API cleanness. Since `current` can be null before being passed in and not
+ * null after this function, make sure to assign it back to `current`:
+ *
+ * `a = accumulateInto(a, b);`
+ *
+ * This API should be sparingly used. Try `accumulate` for something cleaner.
+ *
+ * @return {*|array<*>} An accumulation of items.
+ */
+
+function accumulateInto(current, next) {
+ !(next != null) ? "production" !== 'production' ? invariant(false, 'accumulateInto(...): Accumulated items must not be null or undefined.') : invariant(false) : undefined;
+ if (current == null) {
+ return next;
+ }
+
+ // Both are not empty. Warning: Never call x.concat(y) when you are not
+ // certain that x is an Array (x could be a string with concat method).
+ var currentIsArray = Array.isArray(current);
+ var nextIsArray = Array.isArray(next);
+
+ if (currentIsArray && nextIsArray) {
+ current.push.apply(current, next);
+ return current;
+ }
+
+ if (currentIsArray) {
+ current.push(next);
+ return current;
+ }
+
+ if (nextIsArray) {
+ // A bit too dangerous to mutate `next`.
+ return [current].concat(next);
+ }
+
+ return [current, next];
+}
+
+module.exports = accumulateInto;
+},{"161":161}],116:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule adler32
+ */
+
+'use strict';
+
+var MOD = 65521;
+
+// adler32 is not cryptographically strong, and is only used to sanity check that
+// markup generated on the server matches the markup generated on the client.
+// This implementation (a modified version of the SheetJS version) has been optimized
+// for our use case, at the expense of conforming to the adler32 specification
+// for non-ascii inputs.
+function adler32(data) {
+ var a = 1;
+ var b = 0;
+ var i = 0;
+ var l = data.length;
+ var m = l & ~0x3;
+ while (i < m) {
+ for (; i < Math.min(i + 4096, m); i += 4) {
+ b += (a += data.charCodeAt(i)) + (a += data.charCodeAt(i + 1)) + (a += data.charCodeAt(i + 2)) + (a += data.charCodeAt(i + 3));
+ }
+ a %= MOD;
+ b %= MOD;
+ }
+ for (; i < l; i++) {
+ b += a += data.charCodeAt(i);
+ }
+ a %= MOD;
+ b %= MOD;
+ return a | b << 16;
+}
+
+module.exports = adler32;
+},{}],117:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule canDefineProperty
+ */
+
+'use strict';
+
+var canDefineProperty = false;
+if ("production" !== 'production') {
+ try {
+ Object.defineProperty({}, 'x', { get: function () {} });
+ canDefineProperty = true;
+ } catch (x) {
+ // IE will fail on defineProperty
+ }
+}
+
+module.exports = canDefineProperty;
+},{}],118:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @typechecks static-only
+ * @providesModule cloneWithProps
+ */
+
+'use strict';
+
+var ReactElement = _dereq_(57);
+var ReactPropTransferer = _dereq_(79);
+
+var keyOf = _dereq_(166);
+var warning = _dereq_(173);
+
+var CHILDREN_PROP = keyOf({ children: null });
+
+var didDeprecatedWarn = false;
+
+/**
+ * Sometimes you want to change the props of a child passed to you. Usually
+ * this is to add a CSS class.
+ *
+ * @param {ReactElement} child child element you'd like to clone
+ * @param {object} props props you'd like to modify. className and style will be
+ * merged automatically.
+ * @return {ReactElement} a clone of child with props merged in.
+ * @deprecated
+ */
+function cloneWithProps(child, props) {
+ if ("production" !== 'production') {
+ "production" !== 'production' ? warning(didDeprecatedWarn, 'cloneWithProps(...) is deprecated. ' + 'Please use React.cloneElement instead.') : undefined;
+ didDeprecatedWarn = true;
+ "production" !== 'production' ? warning(!child.ref, 'You are calling cloneWithProps() on a child with a ref. This is ' + 'dangerous because you\'re creating a new child which will not be ' + 'added as a ref to its parent.') : undefined;
+ }
+
+ var newProps = ReactPropTransferer.mergeProps(props, child.props);
+
+ // Use `child.props.children` if it is provided.
+ if (!newProps.hasOwnProperty(CHILDREN_PROP) && child.props.hasOwnProperty(CHILDREN_PROP)) {
+ newProps.children = child.props.children;
+ }
+
+ // The current API doesn't retain _owner, which is why this
+ // doesn't use ReactElement.cloneAndReplaceProps.
+ return ReactElement.createElement(child.type, newProps);
+}
+
+module.exports = cloneWithProps;
+},{"166":166,"173":173,"57":57,"79":79}],119:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule dangerousStyleValue
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var CSSProperty = _dereq_(4);
+
+var isUnitlessNumber = CSSProperty.isUnitlessNumber;
+
+/**
+ * Convert a value into the proper css writable value. The style name `name`
+ * should be logical (no hyphens), as specified
+ * in `CSSProperty.isUnitlessNumber`.
+ *
+ * @param {string} name CSS property name such as `topMargin`.
+ * @param {*} value CSS property value such as `10px`.
+ * @return {string} Normalized style value with dimensions applied.
+ */
+function dangerousStyleValue(name, value) {
+ // Note that we've removed escapeTextForBrowser() calls here since the
+ // whole string will be escaped when the attribute is injected into
+ // the markup. If you provide unsafe user data here they can inject
+ // arbitrary CSS which may be problematic (I couldn't repro this):
+ // https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet
+ // http://www.thespanner.co.uk/2007/11/26/ultimate-xss-css-injection/
+ // This is not an XSS hole but instead a potential CSS injection issue
+ // which has lead to a greater discussion about how we're going to
+ // trust URLs moving forward. See #2115901
+
+ var isEmpty = value == null || typeof value === 'boolean' || value === '';
+ if (isEmpty) {
+ return '';
+ }
+
+ var isNonNumeric = isNaN(value);
+ if (isNonNumeric || value === 0 || isUnitlessNumber.hasOwnProperty(name) && isUnitlessNumber[name]) {
+ return '' + value; // cast to string
+ }
+
+ if (typeof value === 'string') {
+ value = value.trim();
+ }
+ return value + 'px';
+}
+
+module.exports = dangerousStyleValue;
+},{"4":4}],120:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule deprecated
+ */
+
+'use strict';
+
+var assign = _dereq_(24);
+var warning = _dereq_(173);
+
+/**
+ * This will log a single deprecation notice per function and forward the call
+ * on to the new API.
+ *
+ * @param {string} fnName The name of the function
+ * @param {string} newModule The module that fn will exist in
+ * @param {string} newPackage The module that fn will exist in
+ * @param {*} ctx The context this forwarded call should run in
+ * @param {function} fn The function to forward on to
+ * @return {function} The function that will warn once and then call fn
+ */
+function deprecated(fnName, newModule, newPackage, ctx, fn) {
+ var warned = false;
+ if ("production" !== 'production') {
+ var newFn = function () {
+ "production" !== 'production' ? warning(warned,
+ // Require examples in this string must be split to prevent React's
+ // build tools from mistaking them for real requires.
+ // Otherwise the build tools will attempt to build a '%s' module.
+ 'React.%s is deprecated. Please use %s.%s from require' + '(\'%s\') ' + 'instead.', fnName, newModule, fnName, newPackage) : undefined;
+ warned = true;
+ return fn.apply(ctx, arguments);
+ };
+ // We need to make sure all properties of the original fn are copied over.
+ // In particular, this is needed to support PropTypes
+ return assign(newFn, fn);
+ }
+
+ return fn;
+}
+
+module.exports = deprecated;
+},{"173":173,"24":24}],121:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule escapeTextContentForBrowser
+ */
+
+'use strict';
+
+var ESCAPE_LOOKUP = {
+ '&': '&amp;',
+ '>': '&gt;',
+ '<': '&lt;',
+ '"': '&quot;',
+ '\'': '&#x27;'
+};
+
+var ESCAPE_REGEX = /[&><"']/g;
+
+function escaper(match) {
+ return ESCAPE_LOOKUP[match];
+}
+
+/**
+ * Escapes text to prevent scripting attacks.
+ *
+ * @param {*} text Text value to escape.
+ * @return {string} An escaped string.
+ */
+function escapeTextContentForBrowser(text) {
+ return ('' + text).replace(ESCAPE_REGEX, escaper);
+}
+
+module.exports = escapeTextContentForBrowser;
+},{}],122:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule findDOMNode
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var ReactCurrentOwner = _dereq_(39);
+var ReactInstanceMap = _dereq_(68);
+var ReactMount = _dereq_(72);
+
+var invariant = _dereq_(161);
+var warning = _dereq_(173);
+
+/**
+ * Returns the DOM node rendered by this element.
+ *
+ * @param {ReactComponent|DOMElement} componentOrElement
+ * @return {?DOMElement} The root node of this element.
+ */
+function findDOMNode(componentOrElement) {
+ if ("production" !== 'production') {
+ var owner = ReactCurrentOwner.current;
+ if (owner !== null) {
+ "production" !== 'production' ? warning(owner._warnedAboutRefsInRender, '%s is accessing getDOMNode or findDOMNode inside its render(). ' + 'render() should be a pure function of props and state. It should ' + 'never access something that requires stale data from the previous ' + 'render, such as refs. Move this logic to componentDidMount and ' + 'componentDidUpdate instead.', owner.getName() || 'A component') : undefined;
+ owner._warnedAboutRefsInRender = true;
+ }
+ }
+ if (componentOrElement == null) {
+ return null;
+ }
+ if (componentOrElement.nodeType === 1) {
+ return componentOrElement;
+ }
+ if (ReactInstanceMap.has(componentOrElement)) {
+ return ReactMount.getNodeFromInstance(componentOrElement);
+ }
+ !(componentOrElement.render == null || typeof componentOrElement.render !== 'function') ? "production" !== 'production' ? invariant(false, 'findDOMNode was called on an unmounted component.') : invariant(false) : undefined;
+ !false ? "production" !== 'production' ? invariant(false, 'Element appears to be neither ReactComponent nor DOMNode (keys: %s)', Object.keys(componentOrElement)) : invariant(false) : undefined;
+}
+
+module.exports = findDOMNode;
+},{"161":161,"173":173,"39":39,"68":68,"72":72}],123:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule flattenChildren
+ */
+
+'use strict';
+
+var traverseAllChildren = _dereq_(142);
+var warning = _dereq_(173);
+
+/**
+ * @param {function} traverseContext Context passed through traversal.
+ * @param {?ReactComponent} child React child component.
+ * @param {!string} name String name of key path to child.
+ */
+function flattenSingleChildIntoContext(traverseContext, child, name) {
+ // We found a component instance.
+ var result = traverseContext;
+ var keyUnique = result[name] === undefined;
+ if ("production" !== 'production') {
+ "production" !== 'production' ? warning(keyUnique, 'flattenChildren(...): Encountered two children with the same key, ' + '`%s`. Child keys must be unique; when two children share a key, only ' + 'the first child will be used.', name) : undefined;
+ }
+ if (keyUnique && child != null) {
+ result[name] = child;
+ }
+}
+
+/**
+ * Flattens children that are typically specified as `props.children`. Any null
+ * children will not be included in the resulting object.
+ * @return {!object} flattened children keyed by name.
+ */
+function flattenChildren(children) {
+ if (children == null) {
+ return children;
+ }
+ var result = {};
+ traverseAllChildren(children, flattenSingleChildIntoContext, result);
+ return result;
+}
+
+module.exports = flattenChildren;
+},{"142":142,"173":173}],124:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule forEachAccumulated
+ */
+
+'use strict';
+
+/**
+ * @param {array} arr an "accumulation" of items which is either an Array or
+ * a single item. Useful when paired with the `accumulate` module. This is a
+ * simple utility that allows us to reason about a collection of items, but
+ * handling the case when there is exactly one item (and we do not need to
+ * allocate an array).
+ */
+var forEachAccumulated = function (arr, cb, scope) {
+ if (Array.isArray(arr)) {
+ arr.forEach(cb, scope);
+ } else if (arr) {
+ cb.call(scope, arr);
+ }
+};
+
+module.exports = forEachAccumulated;
+},{}],125:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule getEventCharCode
+ * @typechecks static-only
+ */
+
+'use strict';
+
+/**
+ * `charCode` represents the actual "character code" and is safe to use with
+ * `String.fromCharCode`. As such, only keys that correspond to printable
+ * characters produce a valid `charCode`, the only exception to this is Enter.
+ * The Tab-key is considered non-printable and does not have a `charCode`,
+ * presumably because it does not produce a tab-character in browsers.
+ *
+ * @param {object} nativeEvent Native browser event.
+ * @return {number} Normalized `charCode` property.
+ */
+function getEventCharCode(nativeEvent) {
+ var charCode;
+ var keyCode = nativeEvent.keyCode;
+
+ if ('charCode' in nativeEvent) {
+ charCode = nativeEvent.charCode;
+
+ // FF does not set `charCode` for the Enter-key, check against `keyCode`.
+ if (charCode === 0 && keyCode === 13) {
+ charCode = 13;
+ }
+ } else {
+ // IE8 does not implement `charCode`, but `keyCode` has the correct value.
+ charCode = keyCode;
+ }
+
+ // Some non-printable keys are reported in `charCode`/`keyCode`, discard them.
+ // Must not discard the (non-)printable Enter-key.
+ if (charCode >= 32 || charCode === 13) {
+ return charCode;
+ }
+
+ return 0;
+}
+
+module.exports = getEventCharCode;
+},{}],126:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule getEventKey
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var getEventCharCode = _dereq_(125);
+
+/**
+ * Normalization of deprecated HTML5 `key` values
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent#Key_names
+ */
+var normalizeKey = {
+ 'Esc': 'Escape',
+ 'Spacebar': ' ',
+ 'Left': 'ArrowLeft',
+ 'Up': 'ArrowUp',
+ 'Right': 'ArrowRight',
+ 'Down': 'ArrowDown',
+ 'Del': 'Delete',
+ 'Win': 'OS',
+ 'Menu': 'ContextMenu',
+ 'Apps': 'ContextMenu',
+ 'Scroll': 'ScrollLock',
+ 'MozPrintableKey': 'Unidentified'
+};
+
+/**
+ * Translation from legacy `keyCode` to HTML5 `key`
+ * Only special keys supported, all others depend on keyboard layout or browser
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent#Key_names
+ */
+var translateToKey = {
+ 8: 'Backspace',
+ 9: 'Tab',
+ 12: 'Clear',
+ 13: 'Enter',
+ 16: 'Shift',
+ 17: 'Control',
+ 18: 'Alt',
+ 19: 'Pause',
+ 20: 'CapsLock',
+ 27: 'Escape',
+ 32: ' ',
+ 33: 'PageUp',
+ 34: 'PageDown',
+ 35: 'End',
+ 36: 'Home',
+ 37: 'ArrowLeft',
+ 38: 'ArrowUp',
+ 39: 'ArrowRight',
+ 40: 'ArrowDown',
+ 45: 'Insert',
+ 46: 'Delete',
+ 112: 'F1', 113: 'F2', 114: 'F3', 115: 'F4', 116: 'F5', 117: 'F6',
+ 118: 'F7', 119: 'F8', 120: 'F9', 121: 'F10', 122: 'F11', 123: 'F12',
+ 144: 'NumLock',
+ 145: 'ScrollLock',
+ 224: 'Meta'
+};
+
+/**
+ * @param {object} nativeEvent Native browser event.
+ * @return {string} Normalized `key` property.
+ */
+function getEventKey(nativeEvent) {
+ if (nativeEvent.key) {
+ // Normalize inconsistent values reported by browsers due to
+ // implementations of a working draft specification.
+
+ // FireFox implements `key` but returns `MozPrintableKey` for all
+ // printable characters (normalized to `Unidentified`), ignore it.
+ var key = normalizeKey[nativeEvent.key] || nativeEvent.key;
+ if (key !== 'Unidentified') {
+ return key;
+ }
+ }
+
+ // Browser does not implement `key`, polyfill as much of it as we can.
+ if (nativeEvent.type === 'keypress') {
+ var charCode = getEventCharCode(nativeEvent);
+
+ // The enter-key is technically both printable and non-printable and can
+ // thus be captured by `keypress`, no other non-printable key should.
+ return charCode === 13 ? 'Enter' : String.fromCharCode(charCode);
+ }
+ if (nativeEvent.type === 'keydown' || nativeEvent.type === 'keyup') {
+ // While user keyboard layout determines the actual meaning of each
+ // `keyCode` value, almost all function keys have a universal value.
+ return translateToKey[nativeEvent.keyCode] || 'Unidentified';
+ }
+ return '';
+}
+
+module.exports = getEventKey;
+},{"125":125}],127:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule getEventModifierState
+ * @typechecks static-only
+ */
+
+'use strict';
+
+/**
+ * Translation from modifier key to the associated property in the event.
+ * @see http://www.w3.org/TR/DOM-Level-3-Events/#keys-Modifiers
+ */
+
+var modifierKeyToProp = {
+ 'Alt': 'altKey',
+ 'Control': 'ctrlKey',
+ 'Meta': 'metaKey',
+ 'Shift': 'shiftKey'
+};
+
+// IE8 does not implement getModifierState so we simply map it to the only
+// modifier keys exposed by the event itself, does not support Lock-keys.
+// Currently, all major browsers except Chrome seems to support Lock-keys.
+function modifierStateGetter(keyArg) {
+ var syntheticEvent = this;
+ var nativeEvent = syntheticEvent.nativeEvent;
+ if (nativeEvent.getModifierState) {
+ return nativeEvent.getModifierState(keyArg);
+ }
+ var keyProp = modifierKeyToProp[keyArg];
+ return keyProp ? !!nativeEvent[keyProp] : false;
+}
+
+function getEventModifierState(nativeEvent) {
+ return modifierStateGetter;
+}
+
+module.exports = getEventModifierState;
+},{}],128:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule getEventTarget
+ * @typechecks static-only
+ */
+
+'use strict';
+
+/**
+ * Gets the target node from a native browser event by accounting for
+ * inconsistencies in browser DOM APIs.
+ *
+ * @param {object} nativeEvent Native browser event.
+ * @return {DOMEventTarget} Target node.
+ */
+function getEventTarget(nativeEvent) {
+ var target = nativeEvent.target || nativeEvent.srcElement || window;
+ // Safari may fire events on text nodes (Node.TEXT_NODE is 3).
+ // @see http://www.quirksmode.org/js/events_properties.html
+ return target.nodeType === 3 ? target.parentNode : target;
+}
+
+module.exports = getEventTarget;
+},{}],129:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule getIteratorFn
+ * @typechecks static-only
+ */
+
+'use strict';
+
+/* global Symbol */
+var ITERATOR_SYMBOL = typeof Symbol === 'function' && Symbol.iterator;
+var FAUX_ITERATOR_SYMBOL = '@@iterator'; // Before Symbol spec.
+
+/**
+ * Returns the iterator method function contained on the iterable object.
+ *
+ * Be sure to invoke the function with the iterable as context:
+ *
+ * var iteratorFn = getIteratorFn(myIterable);
+ * if (iteratorFn) {
+ * var iterator = iteratorFn.call(myIterable);
+ * ...
+ * }
+ *
+ * @param {?object} maybeIterable
+ * @return {?function}
+ */
+function getIteratorFn(maybeIterable) {
+ var iteratorFn = maybeIterable && (ITERATOR_SYMBOL && maybeIterable[ITERATOR_SYMBOL] || maybeIterable[FAUX_ITERATOR_SYMBOL]);
+ if (typeof iteratorFn === 'function') {
+ return iteratorFn;
+ }
+}
+
+module.exports = getIteratorFn;
+},{}],130:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule getNodeForCharacterOffset
+ */
+
+'use strict';
+
+/**
+ * Given any node return the first leaf node without children.
+ *
+ * @param {DOMElement|DOMTextNode} node
+ * @return {DOMElement|DOMTextNode}
+ */
+function getLeafNode(node) {
+ while (node && node.firstChild) {
+ node = node.firstChild;
+ }
+ return node;
+}
+
+/**
+ * Get the next sibling within a container. This will walk up the
+ * DOM if a node's siblings have been exhausted.
+ *
+ * @param {DOMElement|DOMTextNode} node
+ * @return {?DOMElement|DOMTextNode}
+ */
+function getSiblingNode(node) {
+ while (node) {
+ if (node.nextSibling) {
+ return node.nextSibling;
+ }
+ node = node.parentNode;
+ }
+}
+
+/**
+ * Get object describing the nodes which contain characters at offset.
+ *
+ * @param {DOMElement|DOMTextNode} root
+ * @param {number} offset
+ * @return {?object}
+ */
+function getNodeForCharacterOffset(root, offset) {
+ var node = getLeafNode(root);
+ var nodeStart = 0;
+ var nodeEnd = 0;
+
+ while (node) {
+ if (node.nodeType === 3) {
+ nodeEnd = nodeStart + node.textContent.length;
+
+ if (nodeStart <= offset && nodeEnd >= offset) {
+ return {
+ node: node,
+ offset: offset - nodeStart
+ };
+ }
+
+ nodeStart = nodeEnd;
+ }
+
+ node = getLeafNode(getSiblingNode(node));
+ }
+}
+
+module.exports = getNodeForCharacterOffset;
+},{}],131:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule getTextContentAccessor
+ */
+
+'use strict';
+
+var ExecutionEnvironment = _dereq_(147);
+
+var contentKey = null;
+
+/**
+ * Gets the key used to access text content on a DOM node.
+ *
+ * @return {?string} Key used to access text content.
+ * @internal
+ */
+function getTextContentAccessor() {
+ if (!contentKey && ExecutionEnvironment.canUseDOM) {
+ // Prefer textContent to innerText because many browsers support both but
+ // SVG <text> elements don't support innerText even when <div> does.
+ contentKey = 'textContent' in document.documentElement ? 'textContent' : 'innerText';
+ }
+ return contentKey;
+}
+
+module.exports = getTextContentAccessor;
+},{"147":147}],132:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule instantiateReactComponent
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var ReactCompositeComponent = _dereq_(38);
+var ReactEmptyComponent = _dereq_(59);
+var ReactNativeComponent = _dereq_(75);
+
+var assign = _dereq_(24);
+var invariant = _dereq_(161);
+var warning = _dereq_(173);
+
+// To avoid a cyclic dependency, we create the final class in this module
+var ReactCompositeComponentWrapper = function () {};
+assign(ReactCompositeComponentWrapper.prototype, ReactCompositeComponent.Mixin, {
+ _instantiateReactComponent: instantiateReactComponent
+});
+
+function getDeclarationErrorAddendum(owner) {
+ if (owner) {
+ var name = owner.getName();
+ if (name) {
+ return ' Check the render method of `' + name + '`.';
+ }
+ }
+ return '';
+}
+
+/**
+ * Check if the type reference is a known internal type. I.e. not a user
+ * provided composite type.
+ *
+ * @param {function} type
+ * @return {boolean} Returns true if this is a valid internal type.
+ */
+function isInternalComponentType(type) {
+ return typeof type === 'function' && typeof type.prototype !== 'undefined' && typeof type.prototype.mountComponent === 'function' && typeof type.prototype.receiveComponent === 'function';
+}
+
+/**
+ * Given a ReactNode, create an instance that will actually be mounted.
+ *
+ * @param {ReactNode} node
+ * @return {object} A new instance of the element's constructor.
+ * @protected
+ */
+function instantiateReactComponent(node) {
+ var instance;
+
+ if (node === null || node === false) {
+ instance = new ReactEmptyComponent(instantiateReactComponent);
+ } else if (typeof node === 'object') {
+ var element = node;
+ !(element && (typeof element.type === 'function' || typeof element.type === 'string')) ? "production" !== 'production' ? invariant(false, 'Element type is invalid: expected a string (for built-in components) ' + 'or a class/function (for composite components) but got: %s.%s', element.type == null ? element.type : typeof element.type, getDeclarationErrorAddendum(element._owner)) : invariant(false) : undefined;
+
+ // Special case string values
+ if (typeof element.type === 'string') {
+ instance = ReactNativeComponent.createInternalComponent(element);
+ } else if (isInternalComponentType(element.type)) {
+ // This is temporarily available for custom components that are not string
+ // representations. I.e. ART. Once those are updated to use the string
+ // representation, we can drop this code path.
+ instance = new element.type(element);
+ } else {
+ instance = new ReactCompositeComponentWrapper();
+ }
+ } else if (typeof node === 'string' || typeof node === 'number') {
+ instance = ReactNativeComponent.createInstanceForText(node);
+ } else {
+ !false ? "production" !== 'production' ? invariant(false, 'Encountered invalid React node of type %s', typeof node) : invariant(false) : undefined;
+ }
+
+ if ("production" !== 'production') {
+ "production" !== 'production' ? warning(typeof instance.construct === 'function' && typeof instance.mountComponent === 'function' && typeof instance.receiveComponent === 'function' && typeof instance.unmountComponent === 'function', 'Only React Components can be mounted.') : undefined;
+ }
+
+ // Sets up the instance. This can probably just move into the constructor now.
+ instance.construct(node);
+
+ // These two fields are used by the DOM and ART diffing algorithms
+ // respectively. Instead of using expandos on components, we should be
+ // storing the state needed by the diffing algorithms elsewhere.
+ instance._mountIndex = 0;
+ instance._mountImage = null;
+
+ if ("production" !== 'production') {
+ instance._isOwnerNecessary = false;
+ instance._warnedAboutRefsInRender = false;
+ }
+
+ // Internal instances should fully constructed at this point, so they should
+ // not get any new fields added to them at this point.
+ if ("production" !== 'production') {
+ if (Object.preventExtensions) {
+ Object.preventExtensions(instance);
+ }
+ }
+
+ return instance;
+}
+
+module.exports = instantiateReactComponent;
+},{"161":161,"173":173,"24":24,"38":38,"59":59,"75":75}],133:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule isEventSupported
+ */
+
+'use strict';
+
+var ExecutionEnvironment = _dereq_(147);
+
+var useHasFeature;
+if (ExecutionEnvironment.canUseDOM) {
+ useHasFeature = document.implementation && document.implementation.hasFeature &&
+ // always returns true in newer browsers as per the standard.
+ // @see http://dom.spec.whatwg.org/#dom-domimplementation-hasfeature
+ document.implementation.hasFeature('', '') !== true;
+}
+
+/**
+ * Checks if an event is supported in the current execution environment.
+ *
+ * NOTE: This will not work correctly for non-generic events such as `change`,
+ * `reset`, `load`, `error`, and `select`.
+ *
+ * Borrows from Modernizr.
+ *
+ * @param {string} eventNameSuffix Event name, e.g. "click".
+ * @param {?boolean} capture Check if the capture phase is supported.
+ * @return {boolean} True if the event is supported.
+ * @internal
+ * @license Modernizr 3.0.0pre (Custom Build) | MIT
+ */
+function isEventSupported(eventNameSuffix, capture) {
+ if (!ExecutionEnvironment.canUseDOM || capture && !('addEventListener' in document)) {
+ return false;
+ }
+
+ var eventName = 'on' + eventNameSuffix;
+ var isSupported = (eventName in document);
+
+ if (!isSupported) {
+ var element = document.createElementNS('http://www.w3.org/1999/xhtml', 'div');
+ element.setAttribute(eventName, 'return;');
+ isSupported = typeof element[eventName] === 'function';
+ }
+
+ if (!isSupported && useHasFeature && eventNameSuffix === 'wheel') {
+ // This is the only way to test support for the `wheel` event in IE9+.
+ isSupported = document.implementation.hasFeature('Events.wheel', '3.0');
+ }
+
+ return isSupported;
+}
+
+module.exports = isEventSupported;
+},{"147":147}],134:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule isTextInputElement
+ */
+
+'use strict';
+
+/**
+ * @see http://www.whatwg.org/specs/web-apps/current-work/multipage/the-input-element.html#input-type-attr-summary
+ */
+var supportedInputTypes = {
+ 'color': true,
+ 'date': true,
+ 'datetime': true,
+ 'datetime-local': true,
+ 'email': true,
+ 'month': true,
+ 'number': true,
+ 'password': true,
+ 'range': true,
+ 'search': true,
+ 'tel': true,
+ 'text': true,
+ 'time': true,
+ 'url': true,
+ 'week': true
+};
+
+function isTextInputElement(elem) {
+ var nodeName = elem && elem.nodeName && elem.nodeName.toLowerCase();
+ return nodeName && (nodeName === 'input' && supportedInputTypes[elem.type] || nodeName === 'textarea');
+}
+
+module.exports = isTextInputElement;
+},{}],135:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule onlyChild
+ */
+'use strict';
+
+var ReactElement = _dereq_(57);
+
+var invariant = _dereq_(161);
+
+/**
+ * Returns the first child in a collection of children and verifies that there
+ * is only one child in the collection. The current implementation of this
+ * function assumes that a single child gets passed without a wrapper, but the
+ * purpose of this helper function is to abstract away the particular structure
+ * of children.
+ *
+ * @param {?object} children Child collection structure.
+ * @return {ReactComponent} The first and only `ReactComponent` contained in the
+ * structure.
+ */
+function onlyChild(children) {
+ !ReactElement.isValidElement(children) ? "production" !== 'production' ? invariant(false, 'onlyChild must be passed a children with exactly one child.') : invariant(false) : undefined;
+ return children;
+}
+
+module.exports = onlyChild;
+},{"161":161,"57":57}],136:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule quoteAttributeValueForBrowser
+ */
+
+'use strict';
+
+var escapeTextContentForBrowser = _dereq_(121);
+
+/**
+ * Escapes attribute value to prevent scripting attacks.
+ *
+ * @param {*} value Value to escape.
+ * @return {string} An escaped string.
+ */
+function quoteAttributeValueForBrowser(value) {
+ return '"' + escapeTextContentForBrowser(value) + '"';
+}
+
+module.exports = quoteAttributeValueForBrowser;
+},{"121":121}],137:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+* @providesModule renderSubtreeIntoContainer
+*/
+
+'use strict';
+
+var ReactMount = _dereq_(72);
+
+module.exports = ReactMount.renderSubtreeIntoContainer;
+},{"72":72}],138:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule setInnerHTML
+ */
+
+/* globals MSApp */
+
+'use strict';
+
+var ExecutionEnvironment = _dereq_(147);
+
+var WHITESPACE_TEST = /^[ \r\n\t\f]/;
+var NONVISIBLE_TEST = /<(!--|link|noscript|meta|script|style)[ \r\n\t\f\/>]/;
+
+/**
+ * Set the innerHTML property of a node, ensuring that whitespace is preserved
+ * even in IE8.
+ *
+ * @param {DOMElement} node
+ * @param {string} html
+ * @internal
+ */
+var setInnerHTML = function (node, html) {
+ node.innerHTML = html;
+};
+
+// Win8 apps: Allow all html to be inserted
+if (typeof MSApp !== 'undefined' && MSApp.execUnsafeLocalFunction) {
+ setInnerHTML = function (node, html) {
+ MSApp.execUnsafeLocalFunction(function () {
+ node.innerHTML = html;
+ });
+ };
+}
+
+if (ExecutionEnvironment.canUseDOM) {
+ // IE8: When updating a just created node with innerHTML only leading
+ // whitespace is removed. When updating an existing node with innerHTML
+ // whitespace in root TextNodes is also collapsed.
+ // @see quirksmode.org/bugreports/archives/2004/11/innerhtml_and_t.html
+
+ // Feature detection; only IE8 is known to behave improperly like this.
+ var testElement = document.createElementNS('http://www.w3.org/1999/xhtml', 'div');
+ testElement.innerHTML = ' ';
+ if (testElement.innerHTML === '') {
+ setInnerHTML = function (node, html) {
+ // Magic theory: IE8 supposedly differentiates between added and updated
+ // nodes when processing innerHTML, innerHTML on updated nodes suffers
+ // from worse whitespace behavior. Re-adding a node like this triggers
+ // the initial and more favorable whitespace behavior.
+ // TODO: What to do on a detached node?
+ if (node.parentNode) {
+ node.parentNode.replaceChild(node, node);
+ }
+
+ // We also implement a workaround for non-visible tags disappearing into
+ // thin air on IE8, this only happens if there is no visible text
+ // in-front of the non-visible tags. Piggyback on the whitespace fix
+ // and simply check if any non-visible tags appear in the source.
+ if (WHITESPACE_TEST.test(html) || html[0] === '<' && NONVISIBLE_TEST.test(html)) {
+ // Recover leading whitespace by temporarily prepending any character.
+ // \uFEFF has the potential advantage of being zero-width/invisible.
+ // UglifyJS drops U+FEFF chars when parsing, so use String.fromCharCode
+ // in hopes that this is preserved even if "\uFEFF" is transformed to
+ // the actual Unicode character (by Babel, for example).
+ // https://github.com/mishoo/UglifyJS2/blob/v2.4.20/lib/parse.js#L216
+ node.innerHTML = String.fromCharCode(0xFEFF) + html;
+
+ // deleteData leaves an empty `TextNode` which offsets the index of all
+ // children. Definitely want to avoid this.
+ var textNode = node.firstChild;
+ if (textNode.data.length === 1) {
+ node.removeChild(textNode);
+ } else {
+ textNode.deleteData(0, 1);
+ }
+ } else {
+ node.innerHTML = html;
+ }
+ };
+ }
+}
+
+module.exports = setInnerHTML;
+},{"147":147}],139:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule setTextContent
+ */
+
+'use strict';
+
+var ExecutionEnvironment = _dereq_(147);
+var escapeTextContentForBrowser = _dereq_(121);
+var setInnerHTML = _dereq_(138);
+
+/**
+ * Set the textContent property of a node, ensuring that whitespace is preserved
+ * even in IE8. innerText is a poor substitute for textContent and, among many
+ * issues, inserts <br> instead of the literal newline chars. innerHTML behaves
+ * as it should.
+ *
+ * @param {DOMElement} node
+ * @param {string} text
+ * @internal
+ */
+var setTextContent = function (node, text) {
+ node.textContent = text;
+};
+
+if (ExecutionEnvironment.canUseDOM) {
+ if (!('textContent' in document.documentElement)) {
+ setTextContent = function (node, text) {
+ setInnerHTML(node, escapeTextContentForBrowser(text));
+ };
+ }
+}
+
+module.exports = setTextContent;
+},{"121":121,"138":138,"147":147}],140:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+* @providesModule shallowCompare
+*/
+
+'use strict';
+
+var shallowEqual = _dereq_(171);
+
+/**
+ * Does a shallow comparison for props and state.
+ * See ReactComponentWithPureRenderMixin
+ */
+function shallowCompare(instance, nextProps, nextState) {
+ return !shallowEqual(instance.props, nextProps) || !shallowEqual(instance.state, nextState);
+}
+
+module.exports = shallowCompare;
+},{"171":171}],141:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule shouldUpdateReactComponent
+ * @typechecks static-only
+ */
+
+'use strict';
+
+/**
+ * Given a `prevElement` and `nextElement`, determines if the existing
+ * instance should be updated as opposed to being destroyed or replaced by a new
+ * instance. Both arguments are elements. This ensures that this logic can
+ * operate on stateless trees without any backing instance.
+ *
+ * @param {?object} prevElement
+ * @param {?object} nextElement
+ * @return {boolean} True if the existing instance should be updated.
+ * @protected
+ */
+function shouldUpdateReactComponent(prevElement, nextElement) {
+ var prevEmpty = prevElement === null || prevElement === false;
+ var nextEmpty = nextElement === null || nextElement === false;
+ if (prevEmpty || nextEmpty) {
+ return prevEmpty === nextEmpty;
+ }
+
+ var prevType = typeof prevElement;
+ var nextType = typeof nextElement;
+ if (prevType === 'string' || prevType === 'number') {
+ return nextType === 'string' || nextType === 'number';
+ } else {
+ return nextType === 'object' && prevElement.type === nextElement.type && prevElement.key === nextElement.key;
+ }
+ return false;
+}
+
+module.exports = shouldUpdateReactComponent;
+},{}],142:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule traverseAllChildren
+ */
+
+'use strict';
+
+var ReactCurrentOwner = _dereq_(39);
+var ReactElement = _dereq_(57);
+var ReactInstanceHandles = _dereq_(67);
+
+var getIteratorFn = _dereq_(129);
+var invariant = _dereq_(161);
+var warning = _dereq_(173);
+
+var SEPARATOR = ReactInstanceHandles.SEPARATOR;
+var SUBSEPARATOR = ':';
+
+/**
+ * TODO: Test that a single child and an array with one item have the same key
+ * pattern.
+ */
+
+var userProvidedKeyEscaperLookup = {
+ '=': '=0',
+ '.': '=1',
+ ':': '=2'
+};
+
+var userProvidedKeyEscapeRegex = /[=.:]/g;
+
+var didWarnAboutMaps = false;
+
+function userProvidedKeyEscaper(match) {
+ return userProvidedKeyEscaperLookup[match];
+}
+
+/**
+ * Generate a key string that identifies a component within a set.
+ *
+ * @param {*} component A component that could contain a manual key.
+ * @param {number} index Index that is used if a manual key is not provided.
+ * @return {string}
+ */
+function getComponentKey(component, index) {
+ if (component && component.key != null) {
+ // Explicit key
+ return wrapUserProvidedKey(component.key);
+ }
+ // Implicit key determined by the index in the set
+ return index.toString(36);
+}
+
+/**
+ * Escape a component key so that it is safe to use in a reactid.
+ *
+ * @param {*} text Component key to be escaped.
+ * @return {string} An escaped string.
+ */
+function escapeUserProvidedKey(text) {
+ return ('' + text).replace(userProvidedKeyEscapeRegex, userProvidedKeyEscaper);
+}
+
+/**
+ * Wrap a `key` value explicitly provided by the user to distinguish it from
+ * implicitly-generated keys generated by a component's index in its parent.
+ *
+ * @param {string} key Value of a user-provided `key` attribute
+ * @return {string}
+ */
+function wrapUserProvidedKey(key) {
+ return '$' + escapeUserProvidedKey(key);
+}
+
+/**
+ * @param {?*} children Children tree container.
+ * @param {!string} nameSoFar Name of the key path so far.
+ * @param {!function} callback Callback to invoke with each child found.
+ * @param {?*} traverseContext Used to pass information throughout the traversal
+ * process.
+ * @return {!number} The number of children in this subtree.
+ */
+function traverseAllChildrenImpl(children, nameSoFar, callback, traverseContext) {
+ var type = typeof children;
+
+ if (type === 'undefined' || type === 'boolean') {
+ // All of the above are perceived as null.
+ children = null;
+ }
+
+ if (children === null || type === 'string' || type === 'number' || ReactElement.isValidElement(children)) {
+ callback(traverseContext, children,
+ // If it's the only child, treat the name as if it was wrapped in an array
+ // so that it's consistent if the number of children grows.
+ nameSoFar === '' ? SEPARATOR + getComponentKey(children, 0) : nameSoFar);
+ return 1;
+ }
+
+ var child;
+ var nextName;
+ var subtreeCount = 0; // Count of children found in the current subtree.
+ var nextNamePrefix = nameSoFar === '' ? SEPARATOR : nameSoFar + SUBSEPARATOR;
+
+ if (Array.isArray(children)) {
+ for (var i = 0; i < children.length; i++) {
+ child = children[i];
+ nextName = nextNamePrefix + getComponentKey(child, i);
+ subtreeCount += traverseAllChildrenImpl(child, nextName, callback, traverseContext);
+ }
+ } else {
+ var iteratorFn = getIteratorFn(children);
+ if (iteratorFn) {
+ var iterator = iteratorFn.call(children);
+ var step;
+ if (iteratorFn !== children.entries) {
+ var ii = 0;
+ while (!(step = iterator.next()).done) {
+ child = step.value;
+ nextName = nextNamePrefix + getComponentKey(child, ii++);
+ subtreeCount += traverseAllChildrenImpl(child, nextName, callback, traverseContext);
+ }
+ } else {
+ if ("production" !== 'production') {
+ "production" !== 'production' ? warning(didWarnAboutMaps, 'Using Maps as children is not yet fully supported. It is an ' + 'experimental feature that might be removed. Convert it to a ' + 'sequence / iterable of keyed ReactElements instead.') : undefined;
+ didWarnAboutMaps = true;
+ }
+ // Iterator will provide entry [k,v] tuples rather than values.
+ while (!(step = iterator.next()).done) {
+ var entry = step.value;
+ if (entry) {
+ child = entry[1];
+ nextName = nextNamePrefix + wrapUserProvidedKey(entry[0]) + SUBSEPARATOR + getComponentKey(child, 0);
+ subtreeCount += traverseAllChildrenImpl(child, nextName, callback, traverseContext);
+ }
+ }
+ }
+ } else if (type === 'object') {
+ var addendum = '';
+ if ("production" !== 'production') {
+ addendum = ' If you meant to render a collection of children, use an array ' + 'instead or wrap the object using createFragment(object) from the ' + 'React add-ons.';
+ if (children._isReactElement) {
+ addendum = ' It looks like you\'re using an element created by a different ' + 'version of React. Make sure to use only one copy of React.';
+ }
+ if (ReactCurrentOwner.current) {
+ var name = ReactCurrentOwner.current.getName();
+ if (name) {
+ addendum += ' Check the render method of `' + name + '`.';
+ }
+ }
+ }
+ var childrenString = String(children);
+ !false ? "production" !== 'production' ? invariant(false, 'Objects are not valid as a React child (found: %s).%s', childrenString === '[object Object]' ? 'object with keys {' + Object.keys(children).join(', ') + '}' : childrenString, addendum) : invariant(false) : undefined;
+ }
+ }
+
+ return subtreeCount;
+}
+
+/**
+ * Traverses children that are typically specified as `props.children`, but
+ * might also be specified through attributes:
+ *
+ * - `traverseAllChildren(this.props.children, ...)`
+ * - `traverseAllChildren(this.props.leftPanelChildren, ...)`
+ *
+ * The `traverseContext` is an optional argument that is passed through the
+ * entire traversal. It can be used to store accumulations or anything else that
+ * the callback might find relevant.
+ *
+ * @param {?*} children Children tree object.
+ * @param {!function} callback To invoke upon traversing each child.
+ * @param {?*} traverseContext Context for traversal.
+ * @return {!number} The number of children in this subtree.
+ */
+function traverseAllChildren(children, callback, traverseContext) {
+ if (children == null) {
+ return 0;
+ }
+
+ return traverseAllChildrenImpl(children, '', callback, traverseContext);
+}
+
+module.exports = traverseAllChildren;
+},{"129":129,"161":161,"173":173,"39":39,"57":57,"67":67}],143:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule update
+ */
+
+/* global hasOwnProperty:true */
+
+'use strict';
+
+var assign = _dereq_(24);
+var keyOf = _dereq_(166);
+var invariant = _dereq_(161);
+var hasOwnProperty = ({}).hasOwnProperty;
+
+function shallowCopy(x) {
+ if (Array.isArray(x)) {
+ return x.concat();
+ } else if (x && typeof x === 'object') {
+ return assign(new x.constructor(), x);
+ } else {
+ return x;
+ }
+}
+
+var COMMAND_PUSH = keyOf({ $push: null });
+var COMMAND_UNSHIFT = keyOf({ $unshift: null });
+var COMMAND_SPLICE = keyOf({ $splice: null });
+var COMMAND_SET = keyOf({ $set: null });
+var COMMAND_MERGE = keyOf({ $merge: null });
+var COMMAND_APPLY = keyOf({ $apply: null });
+
+var ALL_COMMANDS_LIST = [COMMAND_PUSH, COMMAND_UNSHIFT, COMMAND_SPLICE, COMMAND_SET, COMMAND_MERGE, COMMAND_APPLY];
+
+var ALL_COMMANDS_SET = {};
+
+ALL_COMMANDS_LIST.forEach(function (command) {
+ ALL_COMMANDS_SET[command] = true;
+});
+
+function invariantArrayCase(value, spec, command) {
+ !Array.isArray(value) ? "production" !== 'production' ? invariant(false, 'update(): expected target of %s to be an array; got %s.', command, value) : invariant(false) : undefined;
+ var specValue = spec[command];
+ !Array.isArray(specValue) ? "production" !== 'production' ? invariant(false, 'update(): expected spec of %s to be an array; got %s. ' + 'Did you forget to wrap your parameter in an array?', command, specValue) : invariant(false) : undefined;
+}
+
+function update(value, spec) {
+ !(typeof spec === 'object') ? "production" !== 'production' ? invariant(false, 'update(): You provided a key path to update() that did not contain one ' + 'of %s. Did you forget to include {%s: ...}?', ALL_COMMANDS_LIST.join(', '), COMMAND_SET) : invariant(false) : undefined;
+
+ if (hasOwnProperty.call(spec, COMMAND_SET)) {
+ !(Object.keys(spec).length === 1) ? "production" !== 'production' ? invariant(false, 'Cannot have more than one key in an object with %s', COMMAND_SET) : invariant(false) : undefined;
+
+ return spec[COMMAND_SET];
+ }
+
+ var nextValue = shallowCopy(value);
+
+ if (hasOwnProperty.call(spec, COMMAND_MERGE)) {
+ var mergeObj = spec[COMMAND_MERGE];
+ !(mergeObj && typeof mergeObj === 'object') ? "production" !== 'production' ? invariant(false, 'update(): %s expects a spec of type \'object\'; got %s', COMMAND_MERGE, mergeObj) : invariant(false) : undefined;
+ !(nextValue && typeof nextValue === 'object') ? "production" !== 'production' ? invariant(false, 'update(): %s expects a target of type \'object\'; got %s', COMMAND_MERGE, nextValue) : invariant(false) : undefined;
+ assign(nextValue, spec[COMMAND_MERGE]);
+ }
+
+ if (hasOwnProperty.call(spec, COMMAND_PUSH)) {
+ invariantArrayCase(value, spec, COMMAND_PUSH);
+ spec[COMMAND_PUSH].forEach(function (item) {
+ nextValue.push(item);
+ });
+ }
+
+ if (hasOwnProperty.call(spec, COMMAND_UNSHIFT)) {
+ invariantArrayCase(value, spec, COMMAND_UNSHIFT);
+ spec[COMMAND_UNSHIFT].forEach(function (item) {
+ nextValue.unshift(item);
+ });
+ }
+
+ if (hasOwnProperty.call(spec, COMMAND_SPLICE)) {
+ !Array.isArray(value) ? "production" !== 'production' ? invariant(false, 'Expected %s target to be an array; got %s', COMMAND_SPLICE, value) : invariant(false) : undefined;
+ !Array.isArray(spec[COMMAND_SPLICE]) ? "production" !== 'production' ? invariant(false, 'update(): expected spec of %s to be an array of arrays; got %s. ' + 'Did you forget to wrap your parameters in an array?', COMMAND_SPLICE, spec[COMMAND_SPLICE]) : invariant(false) : undefined;
+ spec[COMMAND_SPLICE].forEach(function (args) {
+ !Array.isArray(args) ? "production" !== 'production' ? invariant(false, 'update(): expected spec of %s to be an array of arrays; got %s. ' + 'Did you forget to wrap your parameters in an array?', COMMAND_SPLICE, spec[COMMAND_SPLICE]) : invariant(false) : undefined;
+ nextValue.splice.apply(nextValue, args);
+ });
+ }
+
+ if (hasOwnProperty.call(spec, COMMAND_APPLY)) {
+ !(typeof spec[COMMAND_APPLY] === 'function') ? "production" !== 'production' ? invariant(false, 'update(): expected spec of %s to be a function; got %s.', COMMAND_APPLY, spec[COMMAND_APPLY]) : invariant(false) : undefined;
+ nextValue = spec[COMMAND_APPLY](nextValue);
+ }
+
+ for (var k in spec) {
+ if (!(ALL_COMMANDS_SET.hasOwnProperty(k) && ALL_COMMANDS_SET[k])) {
+ nextValue[k] = update(value[k], spec[k]);
+ }
+ }
+
+ return nextValue;
+}
+
+module.exports = update;
+},{"161":161,"166":166,"24":24}],144:[function(_dereq_,module,exports){
+/**
+ * Copyright 2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule validateDOMNesting
+ */
+
+'use strict';
+
+var assign = _dereq_(24);
+var emptyFunction = _dereq_(153);
+var warning = _dereq_(173);
+
+var validateDOMNesting = emptyFunction;
+
+if ("production" !== 'production') {
+ // This validation code was written based on the HTML5 parsing spec:
+ // https://html.spec.whatwg.org/multipage/syntax.html#has-an-element-in-scope
+ //
+ // Note: this does not catch all invalid nesting, nor does it try to (as it's
+ // not clear what practical benefit doing so provides); instead, we warn only
+ // for cases where the parser will give a parse tree differing from what React
+ // intended. For example, <b><div></div></b> is invalid but we don't warn
+ // because it still parses correctly; we do warn for other cases like nested
+ // <p> tags where the beginning of the second element implicitly closes the
+ // first, causing a confusing mess.
+
+ // https://html.spec.whatwg.org/multipage/syntax.html#special
+ var specialTags = ['address', 'applet', 'area', 'article', 'aside', 'base', 'basefont', 'bgsound', 'blockquote', 'body', 'br', 'button', 'caption', 'center', 'col', 'colgroup', 'dd', 'details', 'dir', 'div', 'dl', 'dt', 'embed', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'frame', 'frameset', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'iframe', 'img', 'input', 'isindex', 'li', 'link', 'listing', 'main', 'marquee', 'menu', 'menuitem', 'meta', 'nav', 'noembed', 'noframes', 'noscript', 'object', 'ol', 'p', 'param', 'plaintext', 'pre', 'script', 'section', 'select', 'source', 'style', 'summary', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'title', 'tr', 'track', 'ul', 'wbr', 'xmp'];
+
+ // https://html.spec.whatwg.org/multipage/syntax.html#has-an-element-in-scope
+ var inScopeTags = ['applet', 'caption', 'html', 'table', 'td', 'th', 'marquee', 'object', 'template',
+
+ // https://html.spec.whatwg.org/multipage/syntax.html#html-integration-point
+ // TODO: Distinguish by namespace here -- for <title>, including it here
+ // errs on the side of fewer warnings
+ 'foreignObject', 'desc', 'title'];
+
+ // https://html.spec.whatwg.org/multipage/syntax.html#has-an-element-in-button-scope
+ var buttonScopeTags = inScopeTags.concat(['button']);
+
+ // https://html.spec.whatwg.org/multipage/syntax.html#generate-implied-end-tags
+ var impliedEndTags = ['dd', 'dt', 'li', 'option', 'optgroup', 'p', 'rp', 'rt'];
+
+ var emptyAncestorInfo = {
+ parentTag: null,
+
+ formTag: null,
+ aTagInScope: null,
+ buttonTagInScope: null,
+ nobrTagInScope: null,
+ pTagInButtonScope: null,
+
+ listItemTagAutoclosing: null,
+ dlItemTagAutoclosing: null
+ };
+
+ var updatedAncestorInfo = function (oldInfo, tag, instance) {
+ var ancestorInfo = assign({}, oldInfo || emptyAncestorInfo);
+ var info = { tag: tag, instance: instance };
+
+ if (inScopeTags.indexOf(tag) !== -1) {
+ ancestorInfo.aTagInScope = null;
+ ancestorInfo.buttonTagInScope = null;
+ ancestorInfo.nobrTagInScope = null;
+ }
+ if (buttonScopeTags.indexOf(tag) !== -1) {
+ ancestorInfo.pTagInButtonScope = null;
+ }
+
+ // See rules for 'li', 'dd', 'dt' start tags in
+ // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inbody
+ if (specialTags.indexOf(tag) !== -1 && tag !== 'address' && tag !== 'div' && tag !== 'p') {
+ ancestorInfo.listItemTagAutoclosing = null;
+ ancestorInfo.dlItemTagAutoclosing = null;
+ }
+
+ ancestorInfo.parentTag = info;
+
+ if (tag === 'form') {
+ ancestorInfo.formTag = info;
+ }
+ if (tag === 'a') {
+ ancestorInfo.aTagInScope = info;
+ }
+ if (tag === 'button') {
+ ancestorInfo.buttonTagInScope = info;
+ }
+ if (tag === 'nobr') {
+ ancestorInfo.nobrTagInScope = info;
+ }
+ if (tag === 'p') {
+ ancestorInfo.pTagInButtonScope = info;
+ }
+ if (tag === 'li') {
+ ancestorInfo.listItemTagAutoclosing = info;
+ }
+ if (tag === 'dd' || tag === 'dt') {
+ ancestorInfo.dlItemTagAutoclosing = info;
+ }
+
+ return ancestorInfo;
+ };
+
+ /**
+ * Returns whether
+ */
+ var isTagValidWithParent = function (tag, parentTag) {
+ // First, let's check if we're in an unusual parsing mode...
+ switch (parentTag) {
+ // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inselect
+ case 'select':
+ return tag === 'option' || tag === 'optgroup' || tag === '#text';
+ case 'optgroup':
+ return tag === 'option' || tag === '#text';
+ // Strictly speaking, seeing an <option> doesn't mean we're in a <select>
+ // but
+ case 'option':
+ return tag === '#text';
+
+ // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intd
+ // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-incaption
+ // No special behavior since these rules fall back to "in body" mode for
+ // all except special table nodes which cause bad parsing behavior anyway.
+
+ // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intr
+ case 'tr':
+ return tag === 'th' || tag === 'td' || tag === 'style' || tag === 'script' || tag === 'template';
+
+ // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intbody
+ case 'tbody':
+ case 'thead':
+ case 'tfoot':
+ return tag === 'tr' || tag === 'style' || tag === 'script' || tag === 'template';
+
+ // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-incolgroup
+ case 'colgroup':
+ return tag === 'col' || tag === 'template';
+
+ // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intable
+ case 'table':
+ return tag === 'caption' || tag === 'colgroup' || tag === 'tbody' || tag === 'tfoot' || tag === 'thead' || tag === 'style' || tag === 'script' || tag === 'template';
+
+ // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inhead
+ case 'head':
+ return tag === 'base' || tag === 'basefont' || tag === 'bgsound' || tag === 'link' || tag === 'meta' || tag === 'title' || tag === 'noscript' || tag === 'noframes' || tag === 'style' || tag === 'script' || tag === 'template';
+
+ // https://html.spec.whatwg.org/multipage/semantics.html#the-html-element
+ case 'html':
+ return tag === 'head' || tag === 'body';
+ }
+
+ // Probably in the "in body" parsing mode, so we outlaw only tag combos
+ // where the parsing rules cause implicit opens or closes to be added.
+ // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inbody
+ switch (tag) {
+ case 'h1':
+ case 'h2':
+ case 'h3':
+ case 'h4':
+ case 'h5':
+ case 'h6':
+ return parentTag !== 'h1' && parentTag !== 'h2' && parentTag !== 'h3' && parentTag !== 'h4' && parentTag !== 'h5' && parentTag !== 'h6';
+
+ case 'rp':
+ case 'rt':
+ return impliedEndTags.indexOf(parentTag) === -1;
+
+ case 'caption':
+ case 'col':
+ case 'colgroup':
+ case 'frame':
+ case 'head':
+ case 'tbody':
+ case 'td':
+ case 'tfoot':
+ case 'th':
+ case 'thead':
+ case 'tr':
+ // These tags are only valid with a few parents that have special child
+ // parsing rules -- if we're down here, then none of those matched and
+ // so we allow it only if we don't know what the parent is, as all other
+ // cases are invalid.
+ return parentTag == null;
+ }
+
+ return true;
+ };
+
+ /**
+ * Returns whether
+ */
+ var findInvalidAncestorForTag = function (tag, ancestorInfo) {
+ switch (tag) {
+ case 'address':
+ case 'article':
+ case 'aside':
+ case 'blockquote':
+ case 'center':
+ case 'details':
+ case 'dialog':
+ case 'dir':
+ case 'div':
+ case 'dl':
+ case 'fieldset':
+ case 'figcaption':
+ case 'figure':
+ case 'footer':
+ case 'header':
+ case 'hgroup':
+ case 'main':
+ case 'menu':
+ case 'nav':
+ case 'ol':
+ case 'p':
+ case 'section':
+ case 'summary':
+ case 'ul':
+
+ case 'pre':
+ case 'listing':
+
+ case 'table':
+
+ case 'hr':
+
+ case 'xmp':
+
+ case 'h1':
+ case 'h2':
+ case 'h3':
+ case 'h4':
+ case 'h5':
+ case 'h6':
+ return ancestorInfo.pTagInButtonScope;
+
+ case 'form':
+ return ancestorInfo.formTag || ancestorInfo.pTagInButtonScope;
+
+ case 'li':
+ return ancestorInfo.listItemTagAutoclosing;
+
+ case 'dd':
+ case 'dt':
+ return ancestorInfo.dlItemTagAutoclosing;
+
+ case 'button':
+ return ancestorInfo.buttonTagInScope;
+
+ case 'a':
+ // Spec says something about storing a list of markers, but it sounds
+ // equivalent to this check.
+ return ancestorInfo.aTagInScope;
+
+ case 'nobr':
+ return ancestorInfo.nobrTagInScope;
+ }
+
+ return null;
+ };
+
+ /**
+ * Given a ReactCompositeComponent instance, return a list of its recursive
+ * owners, starting at the root and ending with the instance itself.
+ */
+ var findOwnerStack = function (instance) {
+ if (!instance) {
+ return [];
+ }
+
+ var stack = [];
+ /*eslint-disable space-after-keywords */
+ do {
+ /*eslint-enable space-after-keywords */
+ stack.push(instance);
+ } while (instance = instance._currentElement._owner);
+ stack.reverse();
+ return stack;
+ };
+
+ var didWarn = {};
+
+ validateDOMNesting = function (childTag, childInstance, ancestorInfo) {
+ ancestorInfo = ancestorInfo || emptyAncestorInfo;
+ var parentInfo = ancestorInfo.parentTag;
+ var parentTag = parentInfo && parentInfo.tag;
+
+ var invalidParent = isTagValidWithParent(childTag, parentTag) ? null : parentInfo;
+ var invalidAncestor = invalidParent ? null : findInvalidAncestorForTag(childTag, ancestorInfo);
+ var problematic = invalidParent || invalidAncestor;
+
+ if (problematic) {
+ var ancestorTag = problematic.tag;
+ var ancestorInstance = problematic.instance;
+
+ var childOwner = childInstance && childInstance._currentElement._owner;
+ var ancestorOwner = ancestorInstance && ancestorInstance._currentElement._owner;
+
+ var childOwners = findOwnerStack(childOwner);
+ var ancestorOwners = findOwnerStack(ancestorOwner);
+
+ var minStackLen = Math.min(childOwners.length, ancestorOwners.length);
+ var i;
+
+ var deepestCommon = -1;
+ for (i = 0; i < minStackLen; i++) {
+ if (childOwners[i] === ancestorOwners[i]) {
+ deepestCommon = i;
+ } else {
+ break;
+ }
+ }
+
+ var UNKNOWN = '(unknown)';
+ var childOwnerNames = childOwners.slice(deepestCommon + 1).map(function (inst) {
+ return inst.getName() || UNKNOWN;
+ });
+ var ancestorOwnerNames = ancestorOwners.slice(deepestCommon + 1).map(function (inst) {
+ return inst.getName() || UNKNOWN;
+ });
+ var ownerInfo = [].concat(
+ // If the parent and child instances have a common owner ancestor, start
+ // with that -- otherwise we just start with the parent's owners.
+ deepestCommon !== -1 ? childOwners[deepestCommon].getName() || UNKNOWN : [], ancestorOwnerNames, ancestorTag,
+ // If we're warning about an invalid (non-parent) ancestry, add '...'
+ invalidAncestor ? ['...'] : [], childOwnerNames, childTag).join(' > ');
+
+ var warnKey = !!invalidParent + '|' + childTag + '|' + ancestorTag + '|' + ownerInfo;
+ if (didWarn[warnKey]) {
+ return;
+ }
+ didWarn[warnKey] = true;
+
+ if (invalidParent) {
+ var info = '';
+ if (ancestorTag === 'table' && childTag === 'tr') {
+ info += ' Add a <tbody> to your code to match the DOM tree generated by ' + 'the browser.';
+ }
+ "production" !== 'production' ? warning(false, 'validateDOMNesting(...): <%s> cannot appear as a child of <%s>. ' + 'See %s.%s', childTag, ancestorTag, ownerInfo, info) : undefined;
+ } else {
+ "production" !== 'production' ? warning(false, 'validateDOMNesting(...): <%s> cannot appear as a descendant of ' + '<%s>. See %s.', childTag, ancestorTag, ownerInfo) : undefined;
+ }
+ }
+ };
+
+ validateDOMNesting.ancestorInfoContextKey = '__validateDOMNesting_ancestorInfo$' + Math.random().toString(36).slice(2);
+
+ validateDOMNesting.updatedAncestorInfo = updatedAncestorInfo;
+
+ // For testing
+ validateDOMNesting.isTagValidInContext = function (tag, ancestorInfo) {
+ ancestorInfo = ancestorInfo || emptyAncestorInfo;
+ var parentInfo = ancestorInfo.parentTag;
+ var parentTag = parentInfo && parentInfo.tag;
+ return isTagValidWithParent(tag, parentTag) && !findInvalidAncestorForTag(tag, ancestorInfo);
+ };
+}
+
+module.exports = validateDOMNesting;
+},{"153":153,"173":173,"24":24}],145:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule CSSCore
+ * @typechecks
+ */
+
+'use strict';
+
+var invariant = _dereq_(161);
+
+/**
+ * The CSSCore module specifies the API (and implements most of the methods)
+ * that should be used when dealing with the display of elements (via their
+ * CSS classes and visibility on screen. It is an API focused on mutating the
+ * display and not reading it as no logical state should be encoded in the
+ * display of elements.
+ */
+
+var CSSCore = {
+
+ /**
+ * Adds the class passed in to the element if it doesn't already have it.
+ *
+ * @param {DOMElement} element the element to set the class on
+ * @param {string} className the CSS className
+ * @return {DOMElement} the element passed in
+ */
+ addClass: function (element, className) {
+ !!/\s/.test(className) ? "production" !== 'production' ? invariant(false, 'CSSCore.addClass takes only a single class name. "%s" contains ' + 'multiple classes.', className) : invariant(false) : undefined;
+
+ if (className) {
+ if (element.classList) {
+ element.classList.add(className);
+ } else if (!CSSCore.hasClass(element, className)) {
+ element.className = element.className + ' ' + className;
+ }
+ }
+ return element;
+ },
+
+ /**
+ * Removes the class passed in from the element
+ *
+ * @param {DOMElement} element the element to set the class on
+ * @param {string} className the CSS className
+ * @return {DOMElement} the element passed in
+ */
+ removeClass: function (element, className) {
+ !!/\s/.test(className) ? "production" !== 'production' ? invariant(false, 'CSSCore.removeClass takes only a single class name. "%s" contains ' + 'multiple classes.', className) : invariant(false) : undefined;
+
+ if (className) {
+ if (element.classList) {
+ element.classList.remove(className);
+ } else if (CSSCore.hasClass(element, className)) {
+ element.className = element.className.replace(new RegExp('(^|\\s)' + className + '(?:\\s|$)', 'g'), '$1').replace(/\s+/g, ' ') // multiple spaces to one
+ .replace(/^\s*|\s*$/g, ''); // trim the ends
+ }
+ }
+ return element;
+ },
+
+ /**
+ * Helper to add or remove a class from an element based on a condition.
+ *
+ * @param {DOMElement} element the element to set the class on
+ * @param {string} className the CSS className
+ * @param {*} bool condition to whether to add or remove the class
+ * @return {DOMElement} the element passed in
+ */
+ conditionClass: function (element, className, bool) {
+ return (bool ? CSSCore.addClass : CSSCore.removeClass)(element, className);
+ },
+
+ /**
+ * Tests whether the element has the class specified.
+ *
+ * @param {DOMNode|DOMWindow} element the element to set the class on
+ * @param {string} className the CSS className
+ * @return {boolean} true if the element has the class, false if not
+ */
+ hasClass: function (element, className) {
+ !!/\s/.test(className) ? "production" !== 'production' ? invariant(false, 'CSS.hasClass takes only a single class name.') : invariant(false) : undefined;
+ if (element.classList) {
+ return !!className && element.classList.contains(className);
+ }
+ return (' ' + element.className + ' ').indexOf(' ' + className + ' ') > -1;
+ }
+
+};
+
+module.exports = CSSCore;
+},{"161":161}],146:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * @providesModule EventListener
+ * @typechecks
+ */
+
+'use strict';
+
+var emptyFunction = _dereq_(153);
+
+/**
+ * Upstream version of event listener. Does not take into account specific
+ * nature of platform.
+ */
+var EventListener = {
+ /**
+ * Listen to DOM events during the bubble phase.
+ *
+ * @param {DOMEventTarget} target DOM element to register listener on.
+ * @param {string} eventType Event type, e.g. 'click' or 'mouseover'.
+ * @param {function} callback Callback function.
+ * @return {object} Object with a `remove` method.
+ */
+ listen: function (target, eventType, callback) {
+ if (target.addEventListener) {
+ target.addEventListener(eventType, callback, false);
+ return {
+ remove: function () {
+ target.removeEventListener(eventType, callback, false);
+ }
+ };
+ } else if (target.attachEvent) {
+ target.attachEvent('on' + eventType, callback);
+ return {
+ remove: function () {
+ target.detachEvent('on' + eventType, callback);
+ }
+ };
+ }
+ },
+
+ /**
+ * Listen to DOM events during the capture phase.
+ *
+ * @param {DOMEventTarget} target DOM element to register listener on.
+ * @param {string} eventType Event type, e.g. 'click' or 'mouseover'.
+ * @param {function} callback Callback function.
+ * @return {object} Object with a `remove` method.
+ */
+ capture: function (target, eventType, callback) {
+ if (target.addEventListener) {
+ target.addEventListener(eventType, callback, true);
+ return {
+ remove: function () {
+ target.removeEventListener(eventType, callback, true);
+ }
+ };
+ } else {
+ if ("production" !== 'production') {
+ console.error('Attempted to listen to events during the capture phase on a ' + 'browser that does not support the capture phase. Your application ' + 'will not receive some events.');
+ }
+ return {
+ remove: emptyFunction
+ };
+ }
+ },
+
+ registerDefault: function () {}
+};
+
+module.exports = EventListener;
+},{"153":153}],147:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ExecutionEnvironment
+ */
+
+'use strict';
+
+var canUseDOM = !!(typeof window !== 'undefined' && window.document && window.document.createElement);
+
+/**
+ * Simple, lightweight module assisting with the detection and context of
+ * Worker. Helps avoid circular dependencies and allows code to reason about
+ * whether or not they are in a Worker, even if they never include the main
+ * `ReactWorker` dependency.
+ */
+var ExecutionEnvironment = {
+
+ canUseDOM: canUseDOM,
+
+ canUseWorkers: typeof Worker !== 'undefined',
+
+ canUseEventListeners: canUseDOM && !!(window.addEventListener || window.attachEvent),
+
+ canUseViewport: canUseDOM && !!window.screen,
+
+ isInWorker: !canUseDOM // For now, this is true - might change in the future.
+
+};
+
+module.exports = ExecutionEnvironment;
+},{}],148:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule camelize
+ * @typechecks
+ */
+
+"use strict";
+
+var _hyphenPattern = /-(.)/g;
+
+/**
+ * Camelcases a hyphenated string, for example:
+ *
+ * > camelize('background-color')
+ * < "backgroundColor"
+ *
+ * @param {string} string
+ * @return {string}
+ */
+function camelize(string) {
+ return string.replace(_hyphenPattern, function (_, character) {
+ return character.toUpperCase();
+ });
+}
+
+module.exports = camelize;
+},{}],149:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule camelizeStyleName
+ * @typechecks
+ */
+
+'use strict';
+
+var camelize = _dereq_(148);
+
+var msPattern = /^-ms-/;
+
+/**
+ * Camelcases a hyphenated CSS property name, for example:
+ *
+ * > camelizeStyleName('background-color')
+ * < "backgroundColor"
+ * > camelizeStyleName('-moz-transition')
+ * < "MozTransition"
+ * > camelizeStyleName('-ms-transition')
+ * < "msTransition"
+ *
+ * As Andi Smith suggests
+ * (http://www.andismith.com/blog/2012/02/modernizr-prefixed/), an `-ms` prefix
+ * is converted to lowercase `ms`.
+ *
+ * @param {string} string
+ * @return {string}
+ */
+function camelizeStyleName(string) {
+ return camelize(string.replace(msPattern, 'ms-'));
+}
+
+module.exports = camelizeStyleName;
+},{"148":148}],150:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule containsNode
+ * @typechecks
+ */
+
+'use strict';
+
+var isTextNode = _dereq_(163);
+
+/*eslint-disable no-bitwise */
+
+/**
+ * Checks if a given DOM node contains or is another DOM node.
+ *
+ * @param {?DOMNode} outerNode Outer DOM node.
+ * @param {?DOMNode} innerNode Inner DOM node.
+ * @return {boolean} True if `outerNode` contains or is `innerNode`.
+ */
+function containsNode(_x, _x2) {
+ var _again = true;
+
+ _function: while (_again) {
+ var outerNode = _x,
+ innerNode = _x2;
+ _again = false;
+
+ if (!outerNode || !innerNode) {
+ return false;
+ } else if (outerNode === innerNode) {
+ return true;
+ } else if (isTextNode(outerNode)) {
+ return false;
+ } else if (isTextNode(innerNode)) {
+ _x = outerNode;
+ _x2 = innerNode.parentNode;
+ _again = true;
+ continue _function;
+ } else if (outerNode.contains) {
+ return outerNode.contains(innerNode);
+ } else if (outerNode.compareDocumentPosition) {
+ return !!(outerNode.compareDocumentPosition(innerNode) & 16);
+ } else {
+ return false;
+ }
+ }
+}
+
+module.exports = containsNode;
+},{"163":163}],151:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule createArrayFromMixed
+ * @typechecks
+ */
+
+'use strict';
+
+var toArray = _dereq_(172);
+
+/**
+ * Perform a heuristic test to determine if an object is "array-like".
+ *
+ * A monk asked Joshu, a Zen master, "Has a dog Buddha nature?"
+ * Joshu replied: "Mu."
+ *
+ * This function determines if its argument has "array nature": it returns
+ * true if the argument is an actual array, an `arguments' object, or an
+ * HTMLCollection (e.g. node.childNodes or node.getElementsByTagName()).
+ *
+ * It will return false for other array-like objects like Filelist.
+ *
+ * @param {*} obj
+ * @return {boolean}
+ */
+function hasArrayNature(obj) {
+ return(
+ // not null/false
+ !!obj && (
+ // arrays are objects, NodeLists are functions in Safari
+ typeof obj == 'object' || typeof obj == 'function') &&
+ // quacks like an array
+ 'length' in obj &&
+ // not window
+ !('setInterval' in obj) &&
+ // no DOM node should be considered an array-like
+ // a 'select' element has 'length' and 'item' properties on IE8
+ typeof obj.nodeType != 'number' && (
+ // a real array
+ Array.isArray(obj) ||
+ // arguments
+ 'callee' in obj ||
+ // HTMLCollection/NodeList
+ 'item' in obj)
+ );
+}
+
+/**
+ * Ensure that the argument is an array by wrapping it in an array if it is not.
+ * Creates a copy of the argument if it is already an array.
+ *
+ * This is mostly useful idiomatically:
+ *
+ * var createArrayFromMixed = require('createArrayFromMixed');
+ *
+ * function takesOneOrMoreThings(things) {
+ * things = createArrayFromMixed(things);
+ * ...
+ * }
+ *
+ * This allows you to treat `things' as an array, but accept scalars in the API.
+ *
+ * If you need to convert an array-like object, like `arguments`, into an array
+ * use toArray instead.
+ *
+ * @param {*} obj
+ * @return {array}
+ */
+function createArrayFromMixed(obj) {
+ if (!hasArrayNature(obj)) {
+ return [obj];
+ } else if (Array.isArray(obj)) {
+ return obj.slice();
+ } else {
+ return toArray(obj);
+ }
+}
+
+module.exports = createArrayFromMixed;
+},{"172":172}],152:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule createNodesFromMarkup
+ * @typechecks
+ */
+
+/*eslint-disable fb-www/unsafe-html*/
+
+'use strict';
+
+var ExecutionEnvironment = _dereq_(147);
+
+var createArrayFromMixed = _dereq_(151);
+var getMarkupWrap = _dereq_(157);
+var invariant = _dereq_(161);
+
+/**
+ * Dummy container used to render all markup.
+ */
+var dummyNode = ExecutionEnvironment.canUseDOM ? document.createElementNS('http://www.w3.org/1999/xhtml', 'div') : null;
+
+/**
+ * Pattern used by `getNodeName`.
+ */
+var nodeNamePattern = /^\s*<(\w+)/;
+
+/**
+ * Extracts the `nodeName` of the first element in a string of markup.
+ *
+ * @param {string} markup String of markup.
+ * @return {?string} Node name of the supplied markup.
+ */
+function getNodeName(markup) {
+ var nodeNameMatch = markup.match(nodeNamePattern);
+ return nodeNameMatch && nodeNameMatch[1].toLowerCase();
+}
+
+/**
+ * Creates an array containing the nodes rendered from the supplied markup. The
+ * optionally supplied `handleScript` function will be invoked once for each
+ * <script> element that is rendered. If no `handleScript` function is supplied,
+ * an exception is thrown if any <script> elements are rendered.
+ *
+ * @param {string} markup A string of valid HTML markup.
+ * @param {?function} handleScript Invoked once for each rendered <script>.
+ * @return {array<DOMElement|DOMTextNode>} An array of rendered nodes.
+ */
+function createNodesFromMarkup(markup, handleScript) {
+ var node = dummyNode;
+ !!!dummyNode ? "production" !== 'production' ? invariant(false, 'createNodesFromMarkup dummy not initialized') : invariant(false) : undefined;
+ var nodeName = getNodeName(markup);
+
+ var wrap = nodeName && getMarkupWrap(nodeName);
+ if (wrap) {
+ node.innerHTML = wrap[1] + markup + wrap[2];
+
+ var wrapDepth = wrap[0];
+ while (wrapDepth--) {
+ node = node.lastChild;
+ }
+ } else {
+ node.innerHTML = markup;
+ }
+
+ var scripts = node.getElementsByTagName('script');
+ if (scripts.length) {
+ !handleScript ? "production" !== 'production' ? invariant(false, 'createNodesFromMarkup(...): Unexpected <script> element rendered.') : invariant(false) : undefined;
+ createArrayFromMixed(scripts).forEach(handleScript);
+ }
+
+ var nodes = createArrayFromMixed(node.childNodes);
+ while (node.lastChild) {
+ node.removeChild(node.lastChild);
+ }
+ return nodes;
+}
+
+module.exports = createNodesFromMarkup;
+
+},{"147":147,"151":151,"157":157,"161":161}],153:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule emptyFunction
+ */
+
+"use strict";
+
+function makeEmptyFunction(arg) {
+ return function () {
+ return arg;
+ };
+}
+
+/**
+ * This function accepts and discards inputs; it has no side effects. This is
+ * primarily useful idiomatically for overridable function endpoints which
+ * always need to be callable, since JS lacks a null-call idiom ala Cocoa.
+ */
+function emptyFunction() {}
+
+emptyFunction.thatReturns = makeEmptyFunction;
+emptyFunction.thatReturnsFalse = makeEmptyFunction(false);
+emptyFunction.thatReturnsTrue = makeEmptyFunction(true);
+emptyFunction.thatReturnsNull = makeEmptyFunction(null);
+emptyFunction.thatReturnsThis = function () {
+ return this;
+};
+emptyFunction.thatReturnsArgument = function (arg) {
+ return arg;
+};
+
+module.exports = emptyFunction;
+},{}],154:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule emptyObject
+ */
+
+'use strict';
+
+var emptyObject = {};
+
+if ("production" !== 'production') {
+ Object.freeze(emptyObject);
+}
+
+module.exports = emptyObject;
+},{}],155:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule focusNode
+ */
+
+'use strict';
+
+/**
+ * @param {DOMElement} node input/textarea to focus
+ */
+function focusNode(node) {
+ // IE8 can throw "Can't move focus to the control because it is invisible,
+ // not enabled, or of a type that does not accept the focus." for all kinds of
+ // reasons that are too expensive and fragile to test.
+ try {
+ node.focus();
+ } catch (e) {}
+}
+
+module.exports = focusNode;
+},{}],156:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule getActiveElement
+ * @typechecks
+ */
+
+/* eslint-disable fb-www/typeof-undefined */
+
+/**
+ * Same as document.activeElement but wraps in a try-catch block. In IE it is
+ * not safe to call document.activeElement if there is nothing focused.
+ *
+ * The activeElement will be null only if the document or document body is not
+ * yet defined.
+ */
+'use strict';
+
+function getActiveElement() /*?DOMElement*/{
+ if (typeof document === 'undefined') {
+ return null;
+ }
+ try {
+ return document.activeElement || document.body;
+ } catch (e) {
+ return document.body;
+ }
+}
+
+module.exports = getActiveElement;
+},{}],157:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule getMarkupWrap
+ */
+
+/*eslint-disable fb-www/unsafe-html */
+
+'use strict';
+
+var ExecutionEnvironment = _dereq_(147);
+
+var invariant = _dereq_(161);
+
+/**
+ * Dummy container used to detect which wraps are necessary.
+ */
+var dummyNode = ExecutionEnvironment.canUseDOM ? document.createElementNS('http://www.w3.org/1999/xhtml', 'div') : null;
+
+/**
+ * Some browsers cannot use `innerHTML` to render certain elements standalone,
+ * so we wrap them, render the wrapped nodes, then extract the desired node.
+ *
+ * In IE8, certain elements cannot render alone, so wrap all elements ('*').
+ */
+
+var shouldWrap = {};
+
+var selectWrap = [1, '<select multiple="true">', '</select>'];
+var tableWrap = [1, '<table>', '</table>'];
+var trWrap = [3, '<table><tbody><tr>', '</tr></tbody></table>'];
+
+var svgWrap = [1, '<svg xmlns="http://www.w3.org/2000/svg">', '</svg>'];
+
+var markupWrap = {
+ '*': [1, '?<div>', '</div>'],
+
+ 'area': [1, '<map>', '</map>'],
+ 'col': [2, '<table><tbody></tbody><colgroup>', '</colgroup></table>'],
+ 'legend': [1, '<fieldset>', '</fieldset>'],
+ 'param': [1, '<object>', '</object>'],
+ 'tr': [2, '<table><tbody>', '</tbody></table>'],
+
+ 'optgroup': selectWrap,
+ 'option': selectWrap,
+
+ 'caption': tableWrap,
+ 'colgroup': tableWrap,
+ 'tbody': tableWrap,
+ 'tfoot': tableWrap,
+ 'thead': tableWrap,
+
+ 'td': trWrap,
+ 'th': trWrap
+};
+
+// Initialize the SVG elements since we know they'll always need to be wrapped
+// consistently. If they are created inside a <div> they will be initialized in
+// the wrong namespace (and will not display).
+var svgElements = ['circle', 'clipPath', 'defs', 'ellipse', 'g', 'image', 'line', 'linearGradient', 'mask', 'path', 'pattern', 'polygon', 'polyline', 'radialGradient', 'rect', 'stop', 'text', 'tspan'];
+svgElements.forEach(function (nodeName) {
+ markupWrap[nodeName] = svgWrap;
+ shouldWrap[nodeName] = true;
+});
+
+/**
+ * Gets the markup wrap configuration for the supplied `nodeName`.
+ *
+ * NOTE: This lazily detects which wraps are necessary for the current browser.
+ *
+ * @param {string} nodeName Lowercase `nodeName`.
+ * @return {?array} Markup wrap configuration, if applicable.
+ */
+function getMarkupWrap(nodeName) {
+ !!!dummyNode ? "production" !== 'production' ? invariant(false, 'Markup wrapping node not initialized') : invariant(false) : undefined;
+ if (!markupWrap.hasOwnProperty(nodeName)) {
+ nodeName = '*';
+ }
+ if (!shouldWrap.hasOwnProperty(nodeName)) {
+ if (nodeName === '*') {
+ dummyNode.innerHTML = '<link />';
+ } else {
+ dummyNode.innerHTML = '<' + nodeName + '></' + nodeName + '>';
+ }
+ shouldWrap[nodeName] = !dummyNode.firstChild;
+ }
+ return shouldWrap[nodeName] ? markupWrap[nodeName] : null;
+}
+
+module.exports = getMarkupWrap;
+},{"147":147,"161":161}],158:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule getUnboundedScrollPosition
+ * @typechecks
+ */
+
+'use strict';
+
+/**
+ * Gets the scroll position of the supplied element or window.
+ *
+ * The return values are unbounded, unlike `getScrollPosition`. This means they
+ * may be negative or exceed the element boundaries (which is possible using
+ * inertial scrolling).
+ *
+ * @param {DOMWindow|DOMElement} scrollable
+ * @return {object} Map with `x` and `y` keys.
+ */
+function getUnboundedScrollPosition(scrollable) {
+ if (scrollable === window) {
+ return {
+ x: window.pageXOffset || document.documentElement.scrollLeft,
+ y: window.pageYOffset || document.documentElement.scrollTop
+ };
+ }
+ return {
+ x: scrollable.scrollLeft,
+ y: scrollable.scrollTop
+ };
+}
+
+module.exports = getUnboundedScrollPosition;
+},{}],159:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule hyphenate
+ * @typechecks
+ */
+
+'use strict';
+
+var _uppercasePattern = /([A-Z])/g;
+
+/**
+ * Hyphenates a camelcased string, for example:
+ *
+ * > hyphenate('backgroundColor')
+ * < "background-color"
+ *
+ * For CSS style names, use `hyphenateStyleName` instead which works properly
+ * with all vendor prefixes, including `ms`.
+ *
+ * @param {string} string
+ * @return {string}
+ */
+function hyphenate(string) {
+ return string.replace(_uppercasePattern, '-$1').toLowerCase();
+}
+
+module.exports = hyphenate;
+},{}],160:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule hyphenateStyleName
+ * @typechecks
+ */
+
+'use strict';
+
+var hyphenate = _dereq_(159);
+
+var msPattern = /^ms-/;
+
+/**
+ * Hyphenates a camelcased CSS property name, for example:
+ *
+ * > hyphenateStyleName('backgroundColor')
+ * < "background-color"
+ * > hyphenateStyleName('MozTransition')
+ * < "-moz-transition"
+ * > hyphenateStyleName('msTransition')
+ * < "-ms-transition"
+ *
+ * As Modernizr suggests (http://modernizr.com/docs/#prefixed), an `ms` prefix
+ * is converted to `-ms-`.
+ *
+ * @param {string} string
+ * @return {string}
+ */
+function hyphenateStyleName(string) {
+ return hyphenate(string).replace(msPattern, '-ms-');
+}
+
+module.exports = hyphenateStyleName;
+},{"159":159}],161:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule invariant
+ */
+
+'use strict';
+
+/**
+ * Use invariant() to assert state which your program assumes to be true.
+ *
+ * Provide sprintf-style format (only %s is supported) and arguments
+ * to provide information about what broke and what you were
+ * expecting.
+ *
+ * The invariant message will be stripped in production, but the invariant
+ * will remain to ensure logic does not differ in production.
+ */
+
+function invariant(condition, format, a, b, c, d, e, f) {
+ if ("production" !== 'production') {
+ if (format === undefined) {
+ throw new Error('invariant requires an error message argument');
+ }
+ }
+
+ if (!condition) {
+ var error;
+ if (format === undefined) {
+ error = new Error('Minified exception occurred; use the non-minified dev environment ' + 'for the full error message and additional helpful warnings.');
+ } else {
+ var args = [a, b, c, d, e, f];
+ var argIndex = 0;
+ error = new Error(format.replace(/%s/g, function () {
+ return args[argIndex++];
+ }));
+ error.name = 'Invariant Violation';
+ }
+
+ error.framesToPop = 1; // we don't care about invariant's own frame
+ throw error;
+ }
+}
+
+module.exports = invariant;
+},{}],162:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule isNode
+ * @typechecks
+ */
+
+/**
+ * @param {*} object The object to check.
+ * @return {boolean} Whether or not the object is a DOM node.
+ */
+'use strict';
+
+function isNode(object) {
+ return !!(object && (typeof Node === 'function' ? object instanceof Node : typeof object === 'object' && typeof object.nodeType === 'number' && typeof object.nodeName === 'string'));
+}
+
+module.exports = isNode;
+},{}],163:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule isTextNode
+ * @typechecks
+ */
+
+'use strict';
+
+var isNode = _dereq_(162);
+
+/**
+ * @param {*} object The object to check.
+ * @return {boolean} Whether or not the object is a DOM text node.
+ */
+function isTextNode(object) {
+ return isNode(object) && object.nodeType == 3;
+}
+
+module.exports = isTextNode;
+},{"162":162}],164:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule joinClasses
+ * @typechecks static-only
+ */
+
+'use strict';
+
+/**
+ * Combines multiple className strings into one.
+ * http://jsperf.com/joinclasses-args-vs-array
+ *
+ * @param {...?string} className
+ * @return {string}
+ */
+function joinClasses(className /*, ... */) {
+ if (!className) {
+ className = '';
+ }
+ var nextClass;
+ var argLength = arguments.length;
+ if (argLength > 1) {
+ for (var ii = 1; ii < argLength; ii++) {
+ nextClass = arguments[ii];
+ if (nextClass) {
+ className = (className ? className + ' ' : '') + nextClass;
+ }
+ }
+ }
+ return className;
+}
+
+module.exports = joinClasses;
+},{}],165:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule keyMirror
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var invariant = _dereq_(161);
+
+/**
+ * Constructs an enumeration with keys equal to their value.
+ *
+ * For example:
+ *
+ * var COLORS = keyMirror({blue: null, red: null});
+ * var myColor = COLORS.blue;
+ * var isColorValid = !!COLORS[myColor];
+ *
+ * The last line could not be performed if the values of the generated enum were
+ * not equal to their keys.
+ *
+ * Input: {key1: val1, key2: val2}
+ * Output: {key1: key1, key2: key2}
+ *
+ * @param {object} obj
+ * @return {object}
+ */
+var keyMirror = function (obj) {
+ var ret = {};
+ var key;
+ !(obj instanceof Object && !Array.isArray(obj)) ? "production" !== 'production' ? invariant(false, 'keyMirror(...): Argument must be an object.') : invariant(false) : undefined;
+ for (key in obj) {
+ if (!obj.hasOwnProperty(key)) {
+ continue;
+ }
+ ret[key] = key;
+ }
+ return ret;
+};
+
+module.exports = keyMirror;
+},{"161":161}],166:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule keyOf
+ */
+
+/**
+ * Allows extraction of a minified key. Let's the build system minify keys
+ * without losing the ability to dynamically use key strings as values
+ * themselves. Pass in an object with a single key/val pair and it will return
+ * you the string key of that single record. Suppose you want to grab the
+ * value for a key 'className' inside of an object. Key/val minification may
+ * have aliased that key to be 'xa12'. keyOf({className: null}) will return
+ * 'xa12' in that case. Resolve keys you want to use once at startup time, then
+ * reuse those resolutions.
+ */
+"use strict";
+
+var keyOf = function (oneKeyObj) {
+ var key;
+ for (key in oneKeyObj) {
+ if (!oneKeyObj.hasOwnProperty(key)) {
+ continue;
+ }
+ return key;
+ }
+ return null;
+};
+
+module.exports = keyOf;
+},{}],167:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule mapObject
+ */
+
+'use strict';
+
+var hasOwnProperty = Object.prototype.hasOwnProperty;
+
+/**
+ * Executes the provided `callback` once for each enumerable own property in the
+ * object and constructs a new object from the results. The `callback` is
+ * invoked with three arguments:
+ *
+ * - the property value
+ * - the property name
+ * - the object being traversed
+ *
+ * Properties that are added after the call to `mapObject` will not be visited
+ * by `callback`. If the values of existing properties are changed, the value
+ * passed to `callback` will be the value at the time `mapObject` visits them.
+ * Properties that are deleted before being visited are not visited.
+ *
+ * @grep function objectMap()
+ * @grep function objMap()
+ *
+ * @param {?object} object
+ * @param {function} callback
+ * @param {*} context
+ * @return {?object}
+ */
+function mapObject(object, callback, context) {
+ if (!object) {
+ return null;
+ }
+ var result = {};
+ for (var name in object) {
+ if (hasOwnProperty.call(object, name)) {
+ result[name] = callback.call(context, object[name], name, object);
+ }
+ }
+ return result;
+}
+
+module.exports = mapObject;
+},{}],168:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule memoizeStringOnly
+ * @typechecks static-only
+ */
+
+'use strict';
+
+/**
+ * Memoizes the return value of a function that accepts one string argument.
+ *
+ * @param {function} callback
+ * @return {function}
+ */
+function memoizeStringOnly(callback) {
+ var cache = {};
+ return function (string) {
+ if (!cache.hasOwnProperty(string)) {
+ cache[string] = callback.call(this, string);
+ }
+ return cache[string];
+ };
+}
+
+module.exports = memoizeStringOnly;
+},{}],169:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule performance
+ * @typechecks
+ */
+
+'use strict';
+
+var ExecutionEnvironment = _dereq_(147);
+
+var performance;
+
+if (ExecutionEnvironment.canUseDOM) {
+ performance = window.performance || window.msPerformance || window.webkitPerformance;
+}
+
+module.exports = performance || {};
+},{"147":147}],170:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule performanceNow
+ * @typechecks
+ */
+
+'use strict';
+
+var performance = _dereq_(169);
+
+var performanceNow;
+
+/**
+ * Detect if we can use `window.performance.now()` and gracefully fallback to
+ * `Date.now()` if it doesn't exist. We need to support Firefox < 15 for now
+ * because of Facebook's testing infrastructure.
+ */
+if (performance.now) {
+ performanceNow = function () {
+ return performance.now();
+ };
+} else {
+ performanceNow = function () {
+ return Date.now();
+ };
+}
+
+module.exports = performanceNow;
+},{"169":169}],171:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule shallowEqual
+ * @typechecks
+ *
+ */
+
+'use strict';
+
+var hasOwnProperty = Object.prototype.hasOwnProperty;
+
+/**
+ * Performs equality by iterating through keys on an object and returning false
+ * when any key has values which are not strictly equal between the arguments.
+ * Returns true when the values of all keys are strictly equal.
+ */
+function shallowEqual(objA, objB) {
+ if (objA === objB) {
+ return true;
+ }
+
+ if (typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null) {
+ return false;
+ }
+
+ var keysA = Object.keys(objA);
+ var keysB = Object.keys(objB);
+
+ if (keysA.length !== keysB.length) {
+ return false;
+ }
+
+ // Test for A's keys different from B.
+ var bHasOwnProperty = hasOwnProperty.bind(objB);
+ for (var i = 0; i < keysA.length; i++) {
+ if (!bHasOwnProperty(keysA[i]) || objA[keysA[i]] !== objB[keysA[i]]) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+module.exports = shallowEqual;
+},{}],172:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule toArray
+ * @typechecks
+ */
+
+'use strict';
+
+var invariant = _dereq_(161);
+
+/**
+ * Convert array-like objects to arrays.
+ *
+ * This API assumes the caller knows the contents of the data type. For less
+ * well defined inputs use createArrayFromMixed.
+ *
+ * @param {object|function|filelist} obj
+ * @return {array}
+ */
+function toArray(obj) {
+ var length = obj.length;
+
+ // Some browse builtin objects can report typeof 'function' (e.g. NodeList in
+ // old versions of Safari).
+ !(!Array.isArray(obj) && (typeof obj === 'object' || typeof obj === 'function')) ? "production" !== 'production' ? invariant(false, 'toArray: Array-like object expected') : invariant(false) : undefined;
+
+ !(typeof length === 'number') ? "production" !== 'production' ? invariant(false, 'toArray: Object needs a length property') : invariant(false) : undefined;
+
+ !(length === 0 || length - 1 in obj) ? "production" !== 'production' ? invariant(false, 'toArray: Object should have keys for indices') : invariant(false) : undefined;
+
+ // Old IE doesn't give collections access to hasOwnProperty. Assume inputs
+ // without method will throw during the slice call and skip straight to the
+ // fallback.
+ if (obj.hasOwnProperty) {
+ try {
+ return Array.prototype.slice.call(obj);
+ } catch (e) {
+ // IE < 9 does not support Array#slice on collections objects
+ }
+ }
+
+ // Fall back to copying key by key. This assumes all keys have a value,
+ // so will not preserve sparsely populated inputs.
+ var ret = Array(length);
+ for (var ii = 0; ii < length; ii++) {
+ ret[ii] = obj[ii];
+ }
+ return ret;
+}
+
+module.exports = toArray;
+},{"161":161}],173:[function(_dereq_,module,exports){
+/**
+ * Copyright 2014-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule warning
+ */
+
+'use strict';
+
+var emptyFunction = _dereq_(153);
+
+/**
+ * Similar to invariant but only logs a warning if the condition is not met.
+ * This can be used to log issues in development environments in critical
+ * paths. Removing the logging code for production environments will keep the
+ * same logic and follow the same code paths.
+ */
+
+var warning = emptyFunction;
+
+if ("production" !== 'production') {
+ warning = function (condition, format) {
+ for (var _len = arguments.length, args = Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) {
+ args[_key - 2] = arguments[_key];
+ }
+
+ if (format === undefined) {
+ throw new Error('`warning(condition, format, ...args)` requires a warning ' + 'message argument');
+ }
+
+ if (format.indexOf('Failed Composite propType: ') === 0) {
+ return; // Ignore CompositeComponent proptype check.
+ }
+
+ if (!condition) {
+ var argIndex = 0;
+ var message = 'Warning: ' + format.replace(/%s/g, function () {
+ return args[argIndex++];
+ });
+ if (typeof console !== 'undefined') {
+ console.error(message);
+ }
+ try {
+ // --- Welcome to debugging React ---
+ // This error was thrown as a convenience so that you can use this stack
+ // to find the callsite that caused this warning to fire.
+ throw new Error(message);
+ } catch (x) {}
+ }
+ };
+}
+
+module.exports = warning;
+},{"153":153}]},{},[1])(1)
+});
diff --git a/devtools/client/shared/vendor/redux.js b/devtools/client/shared/vendor/redux.js
new file mode 100644
index 000000000..fe12ae878
--- /dev/null
+++ b/devtools/client/shared/vendor/redux.js
@@ -0,0 +1,775 @@
+(function webpackUniversalModuleDefinition(root, factory) {
+ if(typeof exports === 'object' && typeof module === 'object')
+ module.exports = factory();
+ else if(typeof define === 'function' && define.amd)
+ define([], factory);
+ else if(typeof exports === 'object')
+ exports["Redux"] = factory();
+ else
+ root["Redux"] = factory();
+})(this, function() {
+return /******/ (function(modules) { // webpackBootstrap
+/******/ // The module cache
+/******/ var installedModules = {};
+
+/******/ // The require function
+/******/ function __webpack_require__(moduleId) {
+
+/******/ // Check if module is in cache
+/******/ if(installedModules[moduleId])
+/******/ return installedModules[moduleId].exports;
+
+/******/ // Create a new module (and put it into the cache)
+/******/ var module = installedModules[moduleId] = {
+/******/ exports: {},
+/******/ id: moduleId,
+/******/ loaded: false
+/******/ };
+
+/******/ // Execute the module function
+/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
+
+/******/ // Flag the module as loaded
+/******/ module.loaded = true;
+
+/******/ // Return the exports of the module
+/******/ return module.exports;
+/******/ }
+
+
+/******/ // expose the modules object (__webpack_modules__)
+/******/ __webpack_require__.m = modules;
+
+/******/ // expose the module cache
+/******/ __webpack_require__.c = installedModules;
+
+/******/ // __webpack_public_path__
+/******/ __webpack_require__.p = "";
+
+/******/ // Load entry module and return exports
+/******/ return __webpack_require__(0);
+/******/ })
+/************************************************************************/
+/******/ ([
+/* 0 */
+/***/ function(module, exports, __webpack_require__) {
+
+ 'use strict';
+
+ exports.__esModule = true;
+ exports.compose = exports.applyMiddleware = exports.bindActionCreators = exports.combineReducers = exports.createStore = undefined;
+
+ var _createStore = __webpack_require__(2);
+
+ var _createStore2 = _interopRequireDefault(_createStore);
+
+ var _combineReducers = __webpack_require__(7);
+
+ var _combineReducers2 = _interopRequireDefault(_combineReducers);
+
+ var _bindActionCreators = __webpack_require__(6);
+
+ var _bindActionCreators2 = _interopRequireDefault(_bindActionCreators);
+
+ var _applyMiddleware = __webpack_require__(5);
+
+ var _applyMiddleware2 = _interopRequireDefault(_applyMiddleware);
+
+ var _compose = __webpack_require__(1);
+
+ var _compose2 = _interopRequireDefault(_compose);
+
+ var _warning = __webpack_require__(3);
+
+ var _warning2 = _interopRequireDefault(_warning);
+
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
+
+ /*
+ * This is a dummy function to check if the function name has been altered by minification.
+ * If the function has been minified and NODE_ENV !== 'production', warn the user.
+ */
+ function isCrushed() {}
+
+ if (("development") !== 'production' && typeof isCrushed.name === 'string' && isCrushed.name !== 'isCrushed') {
+ (0, _warning2["default"])('You are currently using minified code outside of NODE_ENV === \'production\'. ' + 'This means that you are running a slower development build of Redux. ' + 'You can use loose-envify (https://github.com/zertosh/loose-envify) for browserify ' + 'or DefinePlugin for webpack (http://stackoverflow.com/questions/30030031) ' + 'to ensure you have the correct code for your production build.');
+ }
+
+ exports.createStore = _createStore2["default"];
+ exports.combineReducers = _combineReducers2["default"];
+ exports.bindActionCreators = _bindActionCreators2["default"];
+ exports.applyMiddleware = _applyMiddleware2["default"];
+ exports.compose = _compose2["default"];
+
+/***/ },
+/* 1 */
+/***/ function(module, exports) {
+
+ "use strict";
+
+ exports.__esModule = true;
+ exports["default"] = compose;
+ /**
+ * Composes single-argument functions from right to left.
+ *
+ * @param {...Function} funcs The functions to compose.
+ * @returns {Function} A function obtained by composing functions from right to
+ * left. For example, compose(f, g, h) is identical to arg => f(g(h(arg))).
+ */
+ function compose() {
+ for (var _len = arguments.length, funcs = Array(_len), _key = 0; _key < _len; _key++) {
+ funcs[_key] = arguments[_key];
+ }
+
+ return function () {
+ if (funcs.length === 0) {
+ return arguments.length <= 0 ? undefined : arguments[0];
+ }
+
+ var last = funcs[funcs.length - 1];
+ var rest = funcs.slice(0, -1);
+
+ return rest.reduceRight(function (composed, f) {
+ return f(composed);
+ }, last.apply(undefined, arguments));
+ };
+ }
+
+/***/ },
+/* 2 */
+/***/ function(module, exports, __webpack_require__) {
+
+ 'use strict';
+
+ exports.__esModule = true;
+ exports.ActionTypes = undefined;
+ exports["default"] = createStore;
+
+ var _isPlainObject = __webpack_require__(4);
+
+ var _isPlainObject2 = _interopRequireDefault(_isPlainObject);
+
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
+
+ /**
+ * These are private action types reserved by Redux.
+ * For any unknown actions, you must return the current state.
+ * If the current state is undefined, you must return the initial state.
+ * Do not reference these action types directly in your code.
+ */
+ var ActionTypes = exports.ActionTypes = {
+ INIT: '@@redux/INIT'
+ };
+
+ /**
+ * Creates a Redux store that holds the state tree.
+ * The only way to change the data in the store is to call `dispatch()` on it.
+ *
+ * There should only be a single store in your app. To specify how different
+ * parts of the state tree respond to actions, you may combine several reducers
+ * into a single reducer function by using `combineReducers`.
+ *
+ * @param {Function} reducer A function that returns the next state tree, given
+ * the current state tree and the action to handle.
+ *
+ * @param {any} [initialState] The initial state. You may optionally specify it
+ * to hydrate the state from the server in universal apps, or to restore a
+ * previously serialized user session.
+ * If you use `combineReducers` to produce the root reducer function, this must be
+ * an object with the same shape as `combineReducers` keys.
+ *
+ * @param {Function} enhancer The store enhancer. You may optionally specify it
+ * to enhance the store with third-party capabilities such as middleware,
+ * time travel, persistence, etc. The only store enhancer that ships with Redux
+ * is `applyMiddleware()`.
+ *
+ * @returns {Store} A Redux store that lets you read the state, dispatch actions
+ * and subscribe to changes.
+ */
+ function createStore(reducer, initialState, enhancer) {
+ if (typeof initialState === 'function' && typeof enhancer === 'undefined') {
+ enhancer = initialState;
+ initialState = undefined;
+ }
+
+ if (typeof enhancer !== 'undefined') {
+ if (typeof enhancer !== 'function') {
+ throw new Error('Expected the enhancer to be a function.');
+ }
+
+ return enhancer(createStore)(reducer, initialState);
+ }
+
+ if (typeof reducer !== 'function') {
+ throw new Error('Expected the reducer to be a function.');
+ }
+
+ var currentReducer = reducer;
+ var currentState = initialState;
+ var currentListeners = [];
+ var nextListeners = currentListeners;
+ var isDispatching = false;
+
+ function ensureCanMutateNextListeners() {
+ if (nextListeners === currentListeners) {
+ nextListeners = currentListeners.slice();
+ }
+ }
+
+ /**
+ * Reads the state tree managed by the store.
+ *
+ * @returns {any} The current state tree of your application.
+ */
+ function getState() {
+ return currentState;
+ }
+
+ /**
+ * Adds a change listener. It will be called any time an action is dispatched,
+ * and some part of the state tree may potentially have changed. You may then
+ * call `getState()` to read the current state tree inside the callback.
+ *
+ * You may call `dispatch()` from a change listener, with the following
+ * caveats:
+ *
+ * 1. The subscriptions are snapshotted just before every `dispatch()` call.
+ * If you subscribe or unsubscribe while the listeners are being invoked, this
+ * will not have any effect on the `dispatch()` that is currently in progress.
+ * However, the next `dispatch()` call, whether nested or not, will use a more
+ * recent snapshot of the subscription list.
+ *
+ * 2. The listener should not expect to see all states changes, as the state
+ * might have been updated multiple times during a nested `dispatch()` before
+ * the listener is called. It is, however, guaranteed that all subscribers
+ * registered before the `dispatch()` started will be called with the latest
+ * state by the time it exits.
+ *
+ * @param {Function} listener A callback to be invoked on every dispatch.
+ * @returns {Function} A function to remove this change listener.
+ */
+ function subscribe(listener) {
+ if (typeof listener !== 'function') {
+ throw new Error('Expected listener to be a function.');
+ }
+
+ var isSubscribed = true;
+
+ ensureCanMutateNextListeners();
+ nextListeners.push(listener);
+
+ return function unsubscribe() {
+ if (!isSubscribed) {
+ return;
+ }
+
+ isSubscribed = false;
+
+ ensureCanMutateNextListeners();
+ var index = nextListeners.indexOf(listener);
+ nextListeners.splice(index, 1);
+ };
+ }
+
+ /**
+ * Dispatches an action. It is the only way to trigger a state change.
+ *
+ * The `reducer` function, used to create the store, will be called with the
+ * current state tree and the given `action`. Its return value will
+ * be considered the **next** state of the tree, and the change listeners
+ * will be notified.
+ *
+ * The base implementation only supports plain object actions. If you want to
+ * dispatch a Promise, an Observable, a thunk, or something else, you need to
+ * wrap your store creating function into the corresponding middleware. For
+ * example, see the documentation for the `redux-thunk` package. Even the
+ * middleware will eventually dispatch plain object actions using this method.
+ *
+ * @param {Object} action A plain object representing “what changedâ€. It is
+ * a good idea to keep actions serializable so you can record and replay user
+ * sessions, or use the time travelling `redux-devtools`. An action must have
+ * a `type` property which may not be `undefined`. It is a good idea to use
+ * string constants for action types.
+ *
+ * @returns {Object} For convenience, the same action object you dispatched.
+ *
+ * Note that, if you use a custom middleware, it may wrap `dispatch()` to
+ * return something else (for example, a Promise you can await).
+ */
+ function dispatch(action) {
+ if (!(0, _isPlainObject2["default"])(action)) {
+ throw new Error('Actions must be plain objects. ' + 'Use custom middleware for async actions.');
+ }
+
+ if (typeof action.type === 'undefined') {
+ throw new Error('Actions may not have an undefined "type" property. ' + 'Have you misspelled a constant?');
+ }
+
+ if (isDispatching) {
+ throw new Error('Reducers may not dispatch actions.');
+ }
+
+ try {
+ isDispatching = true;
+ currentState = currentReducer(currentState, action);
+ } finally {
+ isDispatching = false;
+ }
+
+ var listeners = currentListeners = nextListeners;
+ for (var i = 0; i < listeners.length; i++) {
+ listeners[i]();
+ }
+
+ return action;
+ }
+
+ /**
+ * Replaces the reducer currently used by the store to calculate the state.
+ *
+ * You might need this if your app implements code splitting and you want to
+ * load some of the reducers dynamically. You might also need this if you
+ * implement a hot reloading mechanism for Redux.
+ *
+ * @param {Function} nextReducer The reducer for the store to use instead.
+ * @returns {void}
+ */
+ function replaceReducer(nextReducer) {
+ if (typeof nextReducer !== 'function') {
+ throw new Error('Expected the nextReducer to be a function.');
+ }
+
+ currentReducer = nextReducer;
+ dispatch({ type: ActionTypes.INIT });
+ }
+
+ // When a store is created, an "INIT" action is dispatched so that every
+ // reducer returns their initial state. This effectively populates
+ // the initial state tree.
+ dispatch({ type: ActionTypes.INIT });
+
+ return {
+ dispatch: dispatch,
+ subscribe: subscribe,
+ getState: getState,
+ replaceReducer: replaceReducer
+ };
+ }
+
+/***/ },
+/* 3 */
+/***/ function(module, exports) {
+
+ 'use strict';
+
+ exports.__esModule = true;
+ exports["default"] = warning;
+ /**
+ * Prints a warning in the console if it exists.
+ *
+ * @param {String} message The warning message.
+ * @returns {void}
+ */
+ function warning(message) {
+ /* eslint-disable no-console */
+ if (typeof console !== 'undefined' && typeof console.error === 'function') {
+ console.error(message);
+ }
+ /* eslint-enable no-console */
+ try {
+ // This error was thrown as a convenience so that you can use this stack
+ // to find the callsite that caused this warning to fire.
+ throw new Error(message);
+ /* eslint-disable no-empty */
+ } catch (e) {}
+ /* eslint-enable no-empty */
+ }
+
+/***/ },
+/* 4 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var isHostObject = __webpack_require__(8),
+ isObjectLike = __webpack_require__(9);
+
+ /** `Object#toString` result references. */
+ var objectTag = '[object Object]';
+
+ /** Used for built-in method references. */
+ var objectProto = Object.prototype;
+
+ /** Used to resolve the decompiled source of functions. */
+ var funcToString = Function.prototype.toString;
+
+ /** Used to infer the `Object` constructor. */
+ var objectCtorString = funcToString.call(Object);
+
+ /**
+ * Used to resolve the [`toStringTag`](http://ecma-international.org/ecma-262/6.0/#sec-object.prototype.tostring)
+ * of values.
+ */
+ var objectToString = objectProto.toString;
+
+ /** Built-in value references. */
+ var getPrototypeOf = Object.getPrototypeOf;
+
+ /**
+ * Checks if `value` is a plain object, that is, an object created by the
+ * `Object` constructor or one with a `[[Prototype]]` of `null`.
+ *
+ * @static
+ * @memberOf _
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is a plain object, else `false`.
+ * @example
+ *
+ * function Foo() {
+ * this.a = 1;
+ * }
+ *
+ * _.isPlainObject(new Foo);
+ * // => false
+ *
+ * _.isPlainObject([1, 2, 3]);
+ * // => false
+ *
+ * _.isPlainObject({ 'x': 0, 'y': 0 });
+ * // => true
+ *
+ * _.isPlainObject(Object.create(null));
+ * // => true
+ */
+ function isPlainObject(value) {
+ if (!isObjectLike(value) || objectToString.call(value) != objectTag || isHostObject(value)) {
+ return false;
+ }
+ var proto = objectProto;
+ if (typeof value.constructor == 'function') {
+ proto = getPrototypeOf(value);
+ }
+ if (proto === null) {
+ return true;
+ }
+ var Ctor = proto.constructor;
+ return (typeof Ctor == 'function' &&
+ Ctor instanceof Ctor && funcToString.call(Ctor) == objectCtorString);
+ }
+
+ module.exports = isPlainObject;
+
+
+/***/ },
+/* 5 */
+/***/ function(module, exports, __webpack_require__) {
+
+ 'use strict';
+
+ var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
+
+ exports.__esModule = true;
+ exports["default"] = applyMiddleware;
+
+ var _compose = __webpack_require__(1);
+
+ var _compose2 = _interopRequireDefault(_compose);
+
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
+
+ /**
+ * Creates a store enhancer that applies middleware to the dispatch method
+ * of the Redux store. This is handy for a variety of tasks, such as expressing
+ * asynchronous actions in a concise manner, or logging every action payload.
+ *
+ * See `redux-thunk` package as an example of the Redux middleware.
+ *
+ * Because middleware is potentially asynchronous, this should be the first
+ * store enhancer in the composition chain.
+ *
+ * Note that each middleware will be given the `dispatch` and `getState` functions
+ * as named arguments.
+ *
+ * @param {...Function} middlewares The middleware chain to be applied.
+ * @returns {Function} A store enhancer applying the middleware.
+ */
+ function applyMiddleware() {
+ for (var _len = arguments.length, middlewares = Array(_len), _key = 0; _key < _len; _key++) {
+ middlewares[_key] = arguments[_key];
+ }
+
+ return function (createStore) {
+ return function (reducer, initialState, enhancer) {
+ var store = createStore(reducer, initialState, enhancer);
+ var _dispatch = store.dispatch;
+ var chain = [];
+
+ var middlewareAPI = {
+ getState: store.getState,
+ dispatch: function dispatch(action) {
+ return _dispatch(action);
+ }
+ };
+ chain = middlewares.map(function (middleware) {
+ return middleware(middlewareAPI);
+ });
+ _dispatch = _compose2["default"].apply(undefined, chain)(store.dispatch);
+
+ return _extends({}, store, {
+ dispatch: _dispatch
+ });
+ };
+ };
+ }
+
+/***/ },
+/* 6 */
+/***/ function(module, exports) {
+
+ 'use strict';
+
+ exports.__esModule = true;
+ exports["default"] = bindActionCreators;
+ function bindActionCreator(actionCreator, dispatch) {
+ return function () {
+ return dispatch(actionCreator.apply(undefined, arguments));
+ };
+ }
+
+ /**
+ * Turns an object whose values are action creators, into an object with the
+ * same keys, but with every function wrapped into a `dispatch` call so they
+ * may be invoked directly. This is just a convenience method, as you can call
+ * `store.dispatch(MyActionCreators.doSomething())` yourself just fine.
+ *
+ * For convenience, you can also pass a single function as the first argument,
+ * and get a function in return.
+ *
+ * @param {Function|Object} actionCreators An object whose values are action
+ * creator functions. One handy way to obtain it is to use ES6 `import * as`
+ * syntax. You may also pass a single function.
+ *
+ * @param {Function} dispatch The `dispatch` function available on your Redux
+ * store.
+ *
+ * @returns {Function|Object} The object mimicking the original object, but with
+ * every action creator wrapped into the `dispatch` call. If you passed a
+ * function as `actionCreators`, the return value will also be a single
+ * function.
+ */
+ function bindActionCreators(actionCreators, dispatch) {
+ if (typeof actionCreators === 'function') {
+ return bindActionCreator(actionCreators, dispatch);
+ }
+
+ if (typeof actionCreators !== 'object' || actionCreators === null) {
+ throw new Error('bindActionCreators expected an object or a function, instead received ' + (actionCreators === null ? 'null' : typeof actionCreators) + '. ' + 'Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?');
+ }
+
+ var keys = Object.keys(actionCreators);
+ var boundActionCreators = {};
+ for (var i = 0; i < keys.length; i++) {
+ var key = keys[i];
+ var actionCreator = actionCreators[key];
+ if (typeof actionCreator === 'function') {
+ boundActionCreators[key] = bindActionCreator(actionCreator, dispatch);
+ }
+ }
+ return boundActionCreators;
+ }
+
+/***/ },
+/* 7 */
+/***/ function(module, exports, __webpack_require__) {
+
+ 'use strict';
+
+ exports.__esModule = true;
+ exports["default"] = combineReducers;
+
+ var _createStore = __webpack_require__(2);
+
+ var _isPlainObject = __webpack_require__(4);
+
+ var _isPlainObject2 = _interopRequireDefault(_isPlainObject);
+
+ var _warning = __webpack_require__(3);
+
+ var _warning2 = _interopRequireDefault(_warning);
+
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
+
+ function getUndefinedStateErrorMessage(key, action) {
+ var actionType = action && action.type;
+ var actionName = actionType && '"' + actionType.toString() + '"' || 'an action';
+
+ return 'Reducer "' + key + '" returned undefined handling ' + actionName + '. ' + 'To ignore an action, you must explicitly return the previous state.';
+ }
+
+ function getUnexpectedStateShapeWarningMessage(inputState, reducers, action) {
+ var reducerKeys = Object.keys(reducers);
+ var argumentName = action && action.type === _createStore.ActionTypes.INIT ? 'initialState argument passed to createStore' : 'previous state received by the reducer';
+
+ if (reducerKeys.length === 0) {
+ return 'Store does not have a valid reducer. Make sure the argument passed ' + 'to combineReducers is an object whose values are reducers.';
+ }
+
+ if (!(0, _isPlainObject2["default"])(inputState)) {
+ return 'The ' + argumentName + ' has unexpected type of "' + {}.toString.call(inputState).match(/\s([a-z|A-Z]+)/)[1] + '". Expected argument to be an object with the following ' + ('keys: "' + reducerKeys.join('", "') + '"');
+ }
+
+ var unexpectedKeys = Object.keys(inputState).filter(function (key) {
+ return !reducers.hasOwnProperty(key);
+ });
+
+ if (unexpectedKeys.length > 0) {
+ return 'Unexpected ' + (unexpectedKeys.length > 1 ? 'keys' : 'key') + ' ' + ('"' + unexpectedKeys.join('", "') + '" found in ' + argumentName + '. ') + 'Expected to find one of the known reducer keys instead: ' + ('"' + reducerKeys.join('", "') + '". Unexpected keys will be ignored.');
+ }
+ }
+
+ function assertReducerSanity(reducers) {
+ Object.keys(reducers).forEach(function (key) {
+ var reducer = reducers[key];
+ var initialState = reducer(undefined, { type: _createStore.ActionTypes.INIT });
+
+ if (typeof initialState === 'undefined') {
+ throw new Error('Reducer "' + key + '" returned undefined during initialization. ' + 'If the state passed to the reducer is undefined, you must ' + 'explicitly return the initial state. The initial state may ' + 'not be undefined.');
+ }
+
+ var type = '@@redux/PROBE_UNKNOWN_ACTION_' + Math.random().toString(36).substring(7).split('').join('.');
+ if (typeof reducer(undefined, { type: type }) === 'undefined') {
+ throw new Error('Reducer "' + key + '" returned undefined when probed with a random type. ' + ('Don\'t try to handle ' + _createStore.ActionTypes.INIT + ' or other actions in "redux/*" ') + 'namespace. They are considered private. Instead, you must return the ' + 'current state for any unknown actions, unless it is undefined, ' + 'in which case you must return the initial state, regardless of the ' + 'action type. The initial state may not be undefined.');
+ }
+ });
+ }
+
+ /**
+ * Turns an object whose values are different reducer functions, into a single
+ * reducer function. It will call every child reducer, and gather their results
+ * into a single state object, whose keys correspond to the keys of the passed
+ * reducer functions.
+ *
+ * @param {Object} reducers An object whose values correspond to different
+ * reducer functions that need to be combined into one. One handy way to obtain
+ * it is to use ES6 `import * as reducers` syntax. The reducers may never return
+ * undefined for any action. Instead, they should return their initial state
+ * if the state passed to them was undefined, and the current state for any
+ * unrecognized action.
+ *
+ * @returns {Function} A reducer function that invokes every reducer inside the
+ * passed object, and builds a state object with the same shape.
+ */
+ function combineReducers(reducers) {
+ var reducerKeys = Object.keys(reducers);
+ var finalReducers = {};
+ for (var i = 0; i < reducerKeys.length; i++) {
+ var key = reducerKeys[i];
+ if (typeof reducers[key] === 'function') {
+ finalReducers[key] = reducers[key];
+ }
+ }
+ var finalReducerKeys = Object.keys(finalReducers);
+
+ var sanityError;
+ try {
+ assertReducerSanity(finalReducers);
+ } catch (e) {
+ sanityError = e;
+ }
+
+ return function combination() {
+ var state = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0];
+ var action = arguments[1];
+
+ if (sanityError) {
+ throw sanityError;
+ }
+
+ if (true) {
+ var warningMessage = getUnexpectedStateShapeWarningMessage(state, finalReducers, action);
+ if (warningMessage) {
+ (0, _warning2["default"])(warningMessage);
+ }
+ }
+
+ var hasChanged = false;
+ var nextState = {};
+ for (var i = 0; i < finalReducerKeys.length; i++) {
+ var key = finalReducerKeys[i];
+ var reducer = finalReducers[key];
+ var previousStateForKey = state[key];
+ var nextStateForKey = reducer(previousStateForKey, action);
+ if (typeof nextStateForKey === 'undefined') {
+ var errorMessage = getUndefinedStateErrorMessage(key, action);
+ throw new Error(errorMessage);
+ }
+ nextState[key] = nextStateForKey;
+ hasChanged = hasChanged || nextStateForKey !== previousStateForKey;
+ }
+ return hasChanged ? nextState : state;
+ };
+ }
+
+/***/ },
+/* 8 */
+/***/ function(module, exports) {
+
+ /**
+ * Checks if `value` is a host object in IE < 9.
+ *
+ * @private
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is a host object, else `false`.
+ */
+ function isHostObject(value) {
+ // Many host objects are `Object` objects that can coerce to strings
+ // despite having improperly defined `toString` methods.
+ var result = false;
+ if (value != null && typeof value.toString != 'function') {
+ try {
+ result = !!(value + '');
+ } catch (e) {}
+ }
+ return result;
+ }
+
+ module.exports = isHostObject;
+
+
+/***/ },
+/* 9 */
+/***/ function(module, exports) {
+
+ /**
+ * Checks if `value` is object-like. A value is object-like if it's not `null`
+ * and has a `typeof` result of "object".
+ *
+ * @static
+ * @memberOf _
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is object-like, else `false`.
+ * @example
+ *
+ * _.isObjectLike({});
+ * // => true
+ *
+ * _.isObjectLike([1, 2, 3]);
+ * // => true
+ *
+ * _.isObjectLike(_.noop);
+ * // => false
+ *
+ * _.isObjectLike(null);
+ * // => false
+ */
+ function isObjectLike(value) {
+ return !!value && typeof value == 'object';
+ }
+
+ module.exports = isObjectLike;
+
+
+/***/ }
+/******/ ])
+});
+; \ No newline at end of file
diff --git a/devtools/client/shared/vendor/reselect.js b/devtools/client/shared/vendor/reselect.js
new file mode 100644
index 000000000..4d51d342c
--- /dev/null
+++ b/devtools/client/shared/vendor/reselect.js
@@ -0,0 +1,136 @@
+(function (global, factory) {
+ if (typeof define === "function" && define.amd) {
+ define('Reselect', ['exports'], factory);
+ } else if (typeof exports !== "undefined") {
+ factory(exports);
+ } else {
+ var mod = {
+ exports: {}
+ };
+ factory(mod.exports);
+ global.Reselect = mod.exports;
+ }
+})(this, function (exports) {
+ 'use strict';
+
+ exports.__esModule = true;
+ exports.defaultMemoize = defaultMemoize;
+ exports.createSelectorCreator = createSelectorCreator;
+ exports.createStructuredSelector = createStructuredSelector;
+
+ function _toConsumableArray(arr) {
+ if (Array.isArray(arr)) {
+ for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) {
+ arr2[i] = arr[i];
+ }
+
+ return arr2;
+ } else {
+ return Array.from(arr);
+ }
+ }
+
+ function defaultEqualityCheck(a, b) {
+ return a === b;
+ }
+
+ function defaultMemoize(func) {
+ var equalityCheck = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : defaultEqualityCheck;
+
+ var lastArgs = null;
+ var lastResult = null;
+ var isEqualToLastArg = function isEqualToLastArg(value, index) {
+ return equalityCheck(value, lastArgs[index]);
+ };
+ return function () {
+ for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
+ args[_key] = arguments[_key];
+ }
+
+ if (lastArgs === null || lastArgs.length !== args.length || !args.every(isEqualToLastArg)) {
+ lastResult = func.apply(undefined, args);
+ }
+ lastArgs = args;
+ return lastResult;
+ };
+ }
+
+ function getDependencies(funcs) {
+ var dependencies = Array.isArray(funcs[0]) ? funcs[0] : funcs;
+
+ if (!dependencies.every(function (dep) {
+ return typeof dep === 'function';
+ })) {
+ var dependencyTypes = dependencies.map(function (dep) {
+ return typeof dep;
+ }).join(', ');
+ throw new Error('Selector creators expect all input-selectors to be functions, ' + ('instead received the following types: [' + dependencyTypes + ']'));
+ }
+
+ return dependencies;
+ }
+
+ function createSelectorCreator(memoize) {
+ for (var _len2 = arguments.length, memoizeOptions = Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
+ memoizeOptions[_key2 - 1] = arguments[_key2];
+ }
+
+ return function () {
+ for (var _len3 = arguments.length, funcs = Array(_len3), _key3 = 0; _key3 < _len3; _key3++) {
+ funcs[_key3] = arguments[_key3];
+ }
+
+ var recomputations = 0;
+ var resultFunc = funcs.pop();
+ var dependencies = getDependencies(funcs);
+
+ var memoizedResultFunc = memoize.apply(undefined, [function () {
+ recomputations++;
+ return resultFunc.apply(undefined, arguments);
+ }].concat(memoizeOptions));
+
+ var selector = function selector(state, props) {
+ for (var _len4 = arguments.length, args = Array(_len4 > 2 ? _len4 - 2 : 0), _key4 = 2; _key4 < _len4; _key4++) {
+ args[_key4 - 2] = arguments[_key4];
+ }
+
+ var params = dependencies.map(function (dependency) {
+ return dependency.apply(undefined, [state, props].concat(args));
+ });
+ return memoizedResultFunc.apply(undefined, _toConsumableArray(params));
+ };
+
+ selector.resultFunc = resultFunc;
+ selector.recomputations = function () {
+ return recomputations;
+ };
+ selector.resetRecomputations = function () {
+ return recomputations = 0;
+ };
+ return selector;
+ };
+ }
+
+ var createSelector = exports.createSelector = createSelectorCreator(defaultMemoize);
+
+ function createStructuredSelector(selectors) {
+ var selectorCreator = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : createSelector;
+
+ if (typeof selectors !== 'object') {
+ throw new Error('createStructuredSelector expects first argument to be an object ' + ('where each property is a selector, instead received a ' + typeof selectors));
+ }
+ var objectKeys = Object.keys(selectors);
+ return selectorCreator(objectKeys.map(function (key) {
+ return selectors[key];
+ }), function () {
+ for (var _len5 = arguments.length, values = Array(_len5), _key5 = 0; _key5 < _len5; _key5++) {
+ values[_key5] = arguments[_key5];
+ }
+
+ return values.reduce(function (composition, value, index) {
+ composition[objectKeys[index]] = value;
+ return composition;
+ }, {});
+ });
+ }
+});
diff --git a/devtools/client/shared/vendor/seamless-immutable.js b/devtools/client/shared/vendor/seamless-immutable.js
new file mode 100644
index 000000000..ef893f384
--- /dev/null
+++ b/devtools/client/shared/vendor/seamless-immutable.js
@@ -0,0 +1,392 @@
+(function(){
+ "use strict";
+
+ function addPropertyTo(target, methodName, value) {
+ Object.defineProperty(target, methodName, {
+ enumerable: false,
+ configurable: false,
+ writable: false,
+ value: value
+ });
+ }
+
+ function banProperty(target, methodName) {
+ addPropertyTo(target, methodName, function() {
+ throw new ImmutableError("The " + methodName +
+ " method cannot be invoked on an Immutable data structure.");
+ });
+ }
+
+ var immutabilityTag = "__immutable_invariants_hold";
+
+ function addImmutabilityTag(target) {
+ addPropertyTo(target, immutabilityTag, true);
+ }
+
+ function isImmutable(target) {
+ if (typeof target === "object") {
+ return target === null || target.hasOwnProperty(immutabilityTag);
+ } else {
+ // In JavaScript, only objects are even potentially mutable.
+ // strings, numbers, null, and undefined are all naturally immutable.
+ return true;
+ }
+ }
+
+ function isMergableObject(target) {
+ return target !== null && typeof target === "object" && !(target instanceof Array) && !(target instanceof Date);
+ }
+
+ var mutatingObjectMethods = [
+ "setPrototypeOf"
+ ];
+
+ var nonMutatingObjectMethods = [
+ "keys"
+ ];
+
+ var mutatingArrayMethods = mutatingObjectMethods.concat([
+ "push", "pop", "sort", "splice", "shift", "unshift", "reverse"
+ ]);
+
+ var nonMutatingArrayMethods = nonMutatingObjectMethods.concat([
+ "map", "filter", "slice", "concat", "reduce", "reduceRight"
+ ]);
+
+ function ImmutableError(message) {
+ var err = new Error(message);
+ err.__proto__ = ImmutableError;
+
+ return err;
+ }
+ ImmutableError.prototype = Error.prototype;
+
+ function makeImmutable(obj, bannedMethods) {
+ // Tag it so we can quickly tell it's immutable later.
+ addImmutabilityTag(obj);
+
+ if ("development" === "development") {
+ // Make all mutating methods throw exceptions.
+ for (var index in bannedMethods) {
+ if (bannedMethods.hasOwnProperty(index)) {
+ banProperty(obj, bannedMethods[index]);
+ }
+ }
+
+ // Freeze it and return it.
+ Object.freeze(obj);
+ }
+
+ return obj;
+ }
+
+ function makeMethodReturnImmutable(obj, methodName) {
+ var currentMethod = obj[methodName];
+
+ addPropertyTo(obj, methodName, function() {
+ return Immutable(currentMethod.apply(obj, arguments));
+ });
+ }
+
+ function makeImmutableArray(array) {
+ // Don't change their implementations, but wrap these functions to make sure
+ // they always return an immutable value.
+ for (var index in nonMutatingArrayMethods) {
+ if (nonMutatingArrayMethods.hasOwnProperty(index)) {
+ var methodName = nonMutatingArrayMethods[index];
+ makeMethodReturnImmutable(array, methodName);
+ }
+ }
+
+ addPropertyTo(array, "flatMap", flatMap);
+ addPropertyTo(array, "asObject", asObject);
+ addPropertyTo(array, "asMutable", asMutableArray);
+
+ for(var i = 0, length = array.length; i < length; i++) {
+ array[i] = Immutable(array[i]);
+ }
+
+ return makeImmutable(array, mutatingArrayMethods);
+ }
+
+ /**
+ * Effectively performs a map() over the elements in the array, using the
+ * provided iterator, except that whenever the iterator returns an array, that
+ * array's elements are added to the final result instead of the array itself.
+ *
+ * @param {function} iterator - The iterator function that will be invoked on each element in the array. It will receive three arguments: the current value, the current index, and the current object.
+ */
+ function flatMap(iterator) {
+ // Calling .flatMap() with no arguments is a no-op. Don't bother cloning.
+ if (arguments.length === 0) {
+ return this;
+ }
+
+ var result = [],
+ length = this.length,
+ index;
+
+ for (index = 0; index < length; index++) {
+ var iteratorResult = iterator(this[index], index, this);
+
+ if (iteratorResult instanceof Array) {
+ // Concatenate Array results into the return value we're building up.
+ result.push.apply(result, iteratorResult);
+ } else {
+ // Handle non-Array results the same way map() does.
+ result.push(iteratorResult);
+ }
+ }
+
+ return makeImmutableArray(result);
+ }
+
+ /**
+ * Returns an Immutable copy of the object without the given keys included.
+ *
+ * @param {array} keysToRemove - A list of strings representing the keys to exclude in the return value. Instead of providing a single array, this method can also be called by passing multiple strings as separate arguments.
+ */
+ function without(keysToRemove) {
+ // Calling .without() with no arguments is a no-op. Don't bother cloning.
+ if (arguments.length === 0) {
+ return this;
+ }
+
+ // If we weren't given an array, use the arguments list.
+ if (!(keysToRemove instanceof Array)) {
+ keysToRemove = Array.prototype.slice.call(arguments);
+ }
+
+ var result = this.instantiateEmptyObject();
+
+ for (var key in this) {
+ if (this.hasOwnProperty(key) && (keysToRemove.indexOf(key) === -1)) {
+ result[key] = this[key];
+ }
+ }
+
+ return makeImmutableObject(result,
+ {instantiateEmptyObject: this.instantiateEmptyObject});
+ }
+
+ function asMutableArray(opts) {
+ var result = [], i, length;
+
+ if(opts && opts.deep) {
+ for(i = 0, length = this.length; i < length; i++) {
+ result.push( asDeepMutable(this[i]) );
+ }
+ } else {
+ for(i = 0, length = this.length; i < length; i++) {
+ result.push(this[i]);
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Effectively performs a [map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map) over the elements in the array, expecting that the iterator function
+ * will return an array of two elements - the first representing a key, the other
+ * a value. Then returns an Immutable Object constructed of those keys and values.
+ *
+ * @param {function} iterator - A function which should return an array of two elements - the first representing the desired key, the other the desired value.
+ */
+ function asObject(iterator) {
+ // If no iterator was provided, assume the identity function
+ // (suggesting this array is already a list of key/value pairs.)
+ if (typeof iterator !== "function") {
+ iterator = function(value) { return value; };
+ }
+
+ var result = {},
+ length = this.length,
+ index;
+
+ for (index = 0; index < length; index++) {
+ var pair = iterator(this[index], index, this),
+ key = pair[0],
+ value = pair[1];
+
+ result[key] = value;
+ }
+
+ return makeImmutableObject(result);
+ }
+
+ function asDeepMutable(obj) {
+ if(!obj || !obj.hasOwnProperty(immutabilityTag) || obj instanceof Date) { return obj; }
+ return obj.asMutable({deep: true});
+ }
+
+ function quickCopy(src, dest) {
+ for (var key in src) {
+ if (src.hasOwnProperty(key)) {
+ dest[key] = src[key];
+ }
+ }
+
+ return dest;
+ }
+
+ /**
+ * Returns an Immutable Object containing the properties and values of both
+ * this object and the provided object, prioritizing the provided object's
+ * values whenever the same key is present in both objects.
+ *
+ * @param {object} other - The other object to merge. Multiple objects can be passed as an array. In such a case, the later an object appears in that list, the higher its priority.
+ * @param {object} config - Optional config object that contains settings. Supported settings are: {deep: true} for deep merge and {merger: mergerFunc} where mergerFunc is a function
+ * that takes a property from both objects. If anything is returned it overrides the normal merge behaviour.
+ */
+ function merge(other, config) {
+ // Calling .merge() with no arguments is a no-op. Don't bother cloning.
+ if (arguments.length === 0) {
+ return this;
+ }
+
+ if (other === null || (typeof other !== "object")) {
+ throw new TypeError("Immutable#merge can only be invoked with objects or arrays, not " + JSON.stringify(other));
+ }
+
+ var anyChanges = false,
+ result = quickCopy(this, this.instantiateEmptyObject()), // A shallow clone of this object.
+ receivedArray = (other instanceof Array),
+ deep = config && config.deep,
+ merger = config && config.merger,
+ key;
+
+ // Use the given key to extract a value from the given object, then place
+ // that value in the result object under the same key. If that resulted
+ // in a change from this object's value at that key, set anyChanges = true.
+ function addToResult(currentObj, otherObj, key) {
+ var immutableValue = Immutable(otherObj[key]);
+ var mergerResult = merger && merger(currentObj[key], immutableValue, config);
+ if (merger && mergerResult && mergerResult === currentObj[key]) return;
+
+ anyChanges = anyChanges ||
+ mergerResult !== undefined ||
+ (!currentObj.hasOwnProperty(key) ||
+ ((immutableValue !== currentObj[key]) &&
+ // Avoid false positives due to (NaN !== NaN) evaluating to true
+ (immutableValue === immutableValue)));
+
+ if (mergerResult) {
+ result[key] = mergerResult;
+ } else if (deep && isMergableObject(currentObj[key]) && isMergableObject(immutableValue)) {
+ result[key] = currentObj[key].merge(immutableValue, config);
+ } else {
+ result[key] = immutableValue;
+ }
+ }
+
+ // Achieve prioritization by overriding previous values that get in the way.
+ if (!receivedArray) {
+ // The most common use case: just merge one object into the existing one.
+ for (key in other) {
+ if (other.hasOwnProperty(key)) {
+ addToResult(this, other, key);
+ }
+ }
+ } else {
+ // We also accept an Array
+ for (var index=0; index < other.length; index++) {
+ var otherFromArray = other[index];
+
+ for (key in otherFromArray) {
+ if (otherFromArray.hasOwnProperty(key)) {
+ addToResult(this, otherFromArray, key);
+ }
+ }
+ }
+ }
+
+ if (anyChanges) {
+ return makeImmutableObject(result,
+ {instantiateEmptyObject: this.instantiateEmptyObject});
+ } else {
+ return this;
+ }
+ }
+
+ function asMutableObject(opts) {
+ var result = this.instantiateEmptyObject(), key;
+
+ if(opts && opts.deep) {
+ for (key in this) {
+ if (this.hasOwnProperty(key)) {
+ result[key] = asDeepMutable(this[key]);
+ }
+ }
+ } else {
+ for (key in this) {
+ if (this.hasOwnProperty(key)) {
+ result[key] = this[key];
+ }
+ }
+ }
+
+ return result;
+ }
+
+ // Creates plain object to be used for cloning
+ function instantiatePlainObject() {
+ return {};
+ }
+
+ // Finalizes an object with immutable methods, freezes it, and returns it.
+ function makeImmutableObject(obj, options) {
+ var instantiateEmptyObject =
+ (options && options.instantiateEmptyObject) ?
+ options.instantiateEmptyObject : instantiatePlainObject;
+
+ addPropertyTo(obj, "merge", merge);
+ addPropertyTo(obj, "without", without);
+ addPropertyTo(obj, "asMutable", asMutableObject);
+ addPropertyTo(obj, "instantiateEmptyObject", instantiateEmptyObject);
+
+ return makeImmutable(obj, mutatingObjectMethods);
+ }
+
+ function Immutable(obj, options) {
+ if (isImmutable(obj)) {
+ return obj;
+ } else if (obj instanceof Array) {
+ return makeImmutableArray(obj.slice());
+ } else if (obj instanceof Date) {
+ return makeImmutable(new Date(obj.getTime()));
+ } else {
+ // Don't freeze the object we were given; make a clone and use that.
+ var prototype = options && options.prototype;
+ var instantiateEmptyObject =
+ (!prototype || prototype === Object.prototype) ?
+ instantiatePlainObject : (function() { return Object.create(prototype); });
+ var clone = instantiateEmptyObject();
+
+ for (var key in obj) {
+ if (obj.hasOwnProperty(key)) {
+ clone[key] = Immutable(obj[key]);
+ }
+ }
+
+ return makeImmutableObject(clone,
+ {instantiateEmptyObject: instantiateEmptyObject});
+ }
+ }
+
+ // Export the library
+ Immutable.isImmutable = isImmutable;
+ Immutable.ImmutableError = ImmutableError;
+
+ Object.freeze(Immutable);
+
+ /* istanbul ignore if */
+ if (typeof module === "object") {
+ module.exports = Immutable;
+ } else if (typeof exports === "object") {
+ exports.Immutable = Immutable;
+ } else if (typeof window === "object") {
+ window.Immutable = Immutable;
+ } else if (typeof global === "object") {
+ global.Immutable = Immutable;
+ }
+})();
diff --git a/devtools/client/shared/view-source.js b/devtools/client/shared/view-source.js
new file mode 100644
index 000000000..6e2623ab4
--- /dev/null
+++ b/devtools/client/shared/view-source.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";
+
+var { Task } = require("devtools/shared/task");
+
+var Services = require("Services");
+var { gDevTools } = require("devtools/client/framework/devtools");
+var { getSourceText } = require("devtools/client/debugger/content/queries");
+
+/**
+ * Tries to open a Stylesheet file in the Style Editor. If the file is not
+ * found, it is opened in source view instead.
+ * Returns a promise resolving to a boolean indicating whether or not
+ * the source was able to be displayed in the StyleEditor, as the built-in
+ * Firefox View Source is the fallback.
+ *
+ * @param {Toolbox} toolbox
+ * @param {string} sourceURL
+ * @param {number} sourceLine
+ *
+ * @return {Promise<boolean>}
+ */
+exports.viewSourceInStyleEditor = Task.async(function* (toolbox, sourceURL,
+ sourceLine) {
+ let panel = yield toolbox.loadTool("styleeditor");
+
+ try {
+ yield panel.selectStyleSheet(sourceURL, sourceLine);
+ yield toolbox.selectTool("styleeditor");
+ return true;
+ } catch (e) {
+ exports.viewSource(toolbox, sourceURL, sourceLine);
+ return false;
+ }
+});
+
+/**
+ * Tries to open a JavaScript file in the Debugger. If the file is not found,
+ * it is opened in source view instead.
+ * Returns a promise resolving to a boolean indicating whether or not
+ * the source was able to be displayed in the Debugger, as the built-in Firefox
+ * View Source is the fallback.
+ *
+ * @param {Toolbox} toolbox
+ * @param {string} sourceURL
+ * @param {number} sourceLine
+ *
+ * @return {Promise<boolean>}
+ */
+exports.viewSourceInDebugger = Task.async(function* (toolbox, sourceURL, sourceLine) {
+ // If the Debugger was already open, switch to it and try to show the
+ // source immediately. Otherwise, initialize it and wait for the sources
+ // to be added first.
+ let debuggerAlreadyOpen = toolbox.getPanel("jsdebugger");
+ let dbg = yield toolbox.loadTool("jsdebugger");
+
+ // New debugger frontend
+ if (Services.prefs.getBoolPref("devtools.debugger.new-debugger-frontend")) {
+ yield toolbox.selectTool("jsdebugger");
+ const source = dbg._selectors().getSourceByURL(dbg._getState(), sourceURL);
+ if (source) {
+ dbg._actions().selectSourceURL(sourceURL, { line: sourceLine });
+ return true;
+ }
+
+ exports.viewSource(toolbox, sourceURL, sourceLine);
+ return false;
+ }
+
+ const win = dbg.panelWin;
+
+ // Old debugger frontend
+ if (!debuggerAlreadyOpen) {
+ yield win.DebuggerController.waitForSourcesLoaded();
+ }
+
+ let { DebuggerView } = win;
+ let { Sources } = DebuggerView;
+
+ let item = Sources.getItemForAttachment(a => a.source.url === sourceURL);
+ if (item) {
+ yield toolbox.selectTool("jsdebugger");
+
+ // Determine if the source has already finished loading. There's two cases
+ // in which we need to wait for the source to be shown:
+ // 1) The requested source is not yet selected and will be shown once it is
+ // selected and loaded
+ // 2) The requested source is selected BUT the source text is still loading.
+ const { actor } = item.attachment.source;
+ const state = win.DebuggerController.getState();
+
+ // (1) Is the source selected?
+ const selected = state.sources.selectedSource;
+ const isSelected = selected === actor;
+
+ // (2) Has the source text finished loading?
+ let isLoading = false;
+
+ // Only check if the source is loading when the source is already selected.
+ // If the source is not selected, we will select it below and the already
+ // pending load will be cancelled and this check is useless.
+ if (isSelected) {
+ const sourceTextInfo = getSourceText(state, selected);
+ isLoading = sourceTextInfo && sourceTextInfo.loading;
+ }
+
+ // Select the requested source
+ DebuggerView.setEditorLocation(actor, sourceLine, { noDebug: true });
+
+ // Wait for it to load
+ if (!isSelected || isLoading) {
+ yield win.DebuggerController.waitForSourceShown(sourceURL);
+ }
+ return true;
+ }
+
+ // If not found, still attempt to open in View Source
+ exports.viewSource(toolbox, sourceURL, sourceLine);
+ return false;
+});
+
+/**
+ * Tries to open a JavaScript file in the corresponding Scratchpad.
+ *
+ * @param {string} sourceURL
+ * @param {number} sourceLine
+ *
+ * @return {Promise}
+ */
+exports.viewSourceInScratchpad = Task.async(function* (sourceURL, sourceLine) {
+ // Check for matching top level scratchpad window.
+ let wins = Services.wm.getEnumerator("devtools:scratchpad");
+
+ while (wins.hasMoreElements()) {
+ let win = wins.getNext();
+
+ if (!win.closed && win.Scratchpad.uniqueName === sourceURL) {
+ win.focus();
+ win.Scratchpad.editor.setCursor({ line: sourceLine, ch: 0 });
+ return;
+ }
+ }
+
+ // For scratchpads within toolbox
+ for (let [, toolbox] of gDevTools) {
+ let scratchpadPanel = toolbox.getPanel("scratchpad");
+ if (scratchpadPanel) {
+ let { scratchpad } = scratchpadPanel;
+ if (scratchpad.uniqueName === sourceURL) {
+ toolbox.selectTool("scratchpad");
+ toolbox.raise();
+ scratchpad.editor.focus();
+ scratchpad.editor.setCursor({ line: sourceLine, ch: 0 });
+ return;
+ }
+ }
+ }
+});
+
+/**
+ * Open a link in Firefox's View Source.
+ *
+ * @param {Toolbox} toolbox
+ * @param {string} sourceURL
+ * @param {number} sourceLine
+ *
+ * @return {Promise}
+ */
+exports.viewSource = Task.async(function* (toolbox, sourceURL, sourceLine) {
+ // Attempt to access view source via a browser first, which may display it in
+ // a tab, if enabled.
+ let browserWin = Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
+ if (browserWin && browserWin.BrowserViewSourceOfDocument) {
+ return browserWin.BrowserViewSourceOfDocument({
+ URL: sourceURL,
+ lineNumber: sourceLine
+ });
+ }
+ let utils = toolbox.gViewSourceUtils;
+ utils.viewSource(sourceURL, null, toolbox.doc, sourceLine || 0);
+ return null;
+});
diff --git a/devtools/client/shared/webgl-utils.js b/devtools/client/shared/webgl-utils.js
new file mode 100644
index 000000000..f7618c397
--- /dev/null
+++ b/devtools/client/shared/webgl-utils.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/. */
+"use strict";
+
+const { Cc, Ci } = require("chrome");
+const Services = require("Services");
+
+const WEBGL_CONTEXT_NAME = "experimental-webgl";
+
+function isWebGLForceEnabled() {
+ return Services.prefs.getBoolPref("webgl.force-enabled");
+}
+
+function isWebGLSupportedByGFX() {
+ let supported = false;
+
+ try {
+ let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
+ let angle = gfxInfo.FEATURE_WEBGL_ANGLE;
+ let opengl = gfxInfo.FEATURE_WEBGL_OPENGL;
+
+ // if either the Angle or OpenGL renderers are available, WebGL should work
+ supported = gfxInfo.getFeatureStatus(angle) === gfxInfo.FEATURE_STATUS_OK ||
+ gfxInfo.getFeatureStatus(opengl) === gfxInfo.FEATURE_STATUS_OK;
+ } catch (e) {
+ return false;
+ }
+ return supported;
+}
+
+function create3DContext(canvas) {
+ // try to get a valid context from an existing canvas
+ let context = null;
+ try {
+ context = canvas.getContext(WEBGL_CONTEXT_NAME, aFlags);
+ } catch (e) {
+ return null;
+ }
+ return context;
+}
+
+function createCanvas(doc) {
+ return doc.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
+}
+
+function isWebGLSupported(doc) {
+ let supported =
+ !isWebGLForceEnabled() &&
+ isWebGLSupportedByGFX() &&
+ create3DContext(createCanvas(doc));
+
+ return supported;
+}
+exports.isWebGLSupported = isWebGLSupported;
diff --git a/devtools/client/shared/widgets/AbstractTreeItem.jsm b/devtools/client/shared/widgets/AbstractTreeItem.jsm
new file mode 100644
index 000000000..541ab6777
--- /dev/null
+++ b/devtools/client/shared/widgets/AbstractTreeItem.jsm
@@ -0,0 +1,661 @@
+/* -*- 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 { interfaces: Ci, utils: Cu } = Components;
+
+const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
+const { ViewHelpers } = require("devtools/client/shared/widgets/view-helpers");
+const { KeyCodes } = require("devtools/client/shared/keycodes");
+
+XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
+ "resource://devtools/shared/event-emitter.js");
+
+XPCOMUtils.defineLazyModuleGetter(this, "console",
+ "resource://gre/modules/Console.jsm");
+
+this.EXPORTED_SYMBOLS = ["AbstractTreeItem"];
+
+/**
+ * A very generic and low-level tree view implementation. It is not intended
+ * to be used alone, but as a base class that you can extend to build your
+ * own custom implementation.
+ *
+ * Language:
+ * - An "item" is an instance of an AbstractTreeItem.
+ * - An "element" or "node" is an nsIDOMNode.
+ *
+ * The following events are emitted by this tree, always from the root item,
+ * with the first argument pointing to the affected child item:
+ * - "expand": when an item is expanded in the tree
+ * - "collapse": when an item is collapsed in the tree
+ * - "focus": when an item is selected in the tree
+ *
+ * For example, you can extend this abstract class like this:
+ *
+ * function MyCustomTreeItem(dataSrc, properties) {
+ * AbstractTreeItem.call(this, properties);
+ * this.itemDataSrc = dataSrc;
+ * }
+ *
+ * MyCustomTreeItem.prototype = Heritage.extend(AbstractTreeItem.prototype, {
+ * _displaySelf: function(document, arrowNode) {
+ * let node = document.createElement("hbox");
+ * ...
+ * // Append the provided arrow node wherever you want.
+ * node.appendChild(arrowNode);
+ * ...
+ * // Use `this.itemDataSrc` to customize the tree item and
+ * // `this.level` to calculate the indentation.
+ * node.style.marginInlineStart = (this.level * 10) + "px";
+ * node.appendChild(document.createTextNode(this.itemDataSrc.label));
+ * ...
+ * return node;
+ * },
+ * _populateSelf: function(children) {
+ * ...
+ * // Use `this.itemDataSrc` to get the data source for the child items.
+ * let someChildDataSrc = this.itemDataSrc.children[0];
+ * ...
+ * children.push(new MyCustomTreeItem(someChildDataSrc, {
+ * parent: this,
+ * level: this.level + 1
+ * }));
+ * ...
+ * }
+ * });
+ *
+ * And then you could use it like this:
+ *
+ * let dataSrc = {
+ * label: "root",
+ * children: [{
+ * label: "foo",
+ * children: []
+ * }, {
+ * label: "bar",
+ * children: [{
+ * label: "baz",
+ * children: []
+ * }]
+ * }]
+ * };
+ * let root = new MyCustomTreeItem(dataSrc, { parent: null });
+ * root.attachTo(nsIDOMNode);
+ * root.expand();
+ *
+ * The following tree view will be generated (after expanding all nodes):
+ * â–¼ root
+ * â–¶ foo
+ * â–¼ bar
+ * â–¶ baz
+ *
+ * The way the data source is implemented is completely up to you. There's
+ * no assumptions made and you can use it however you like inside the
+ * `_displaySelf` and `populateSelf` methods. If you need to add children to a
+ * node at a later date, you just need to modify the data source:
+ *
+ * dataSrc[...path-to-foo...].children.push({
+ * label: "lazily-added-node"
+ * children: []
+ * });
+ *
+ * The existing tree view will be modified like so (after expanding `foo`):
+ * â–¼ root
+ * â–¼ foo
+ * â–¶ lazily-added-node
+ * â–¼ bar
+ * â–¶ baz
+ *
+ * Everything else is taken care of automagically!
+ *
+ * @param AbstractTreeItem parent
+ * The parent tree item. Should be null for root items.
+ * @param number level
+ * The indentation level in the tree. The root item is at level 0.
+ */
+function AbstractTreeItem({ parent, level }) {
+ this._rootItem = parent ? parent._rootItem : this;
+ this._parentItem = parent;
+ this._level = level || 0;
+ this._childTreeItems = [];
+
+ // Events are always propagated through the root item. Decorating every
+ // tree item as an event emitter is a very costly operation.
+ if (this == this._rootItem) {
+ EventEmitter.decorate(this);
+ }
+}
+this.AbstractTreeItem = AbstractTreeItem;
+
+AbstractTreeItem.prototype = {
+ _containerNode: null,
+ _targetNode: null,
+ _arrowNode: null,
+ _constructed: false,
+ _populated: false,
+ _expanded: false,
+
+ /**
+ * Optionally, trees may be allowed to automatically expand a few levels deep
+ * to avoid initially displaying a completely collapsed tree.
+ */
+ autoExpandDepth: 0,
+
+ /**
+ * Creates the view for this tree item. Implement this method in the
+ * inheriting classes to create the child node displayed in the tree.
+ * Use `this.level` and the provided `arrowNode` as you see fit.
+ *
+ * @param nsIDOMNode document
+ * @param nsIDOMNode arrowNode
+ * @return nsIDOMNode
+ */
+ _displaySelf: function (document, arrowNode) {
+ throw new Error(
+ "The `_displaySelf` method needs to be implemented by inheriting classes.");
+ },
+
+ /**
+ * Populates this tree item with child items, whenever it's expanded.
+ * Implement this method in the inheriting classes to fill the provided
+ * `children` array with AbstractTreeItem instances, which will then be
+ * magically handled by this tree item.
+ *
+ * @param array:AbstractTreeItem children
+ */
+ _populateSelf: function (children) {
+ throw new Error(
+ "The `_populateSelf` method needs to be implemented by inheriting classes.");
+ },
+
+ /**
+ * Gets the this tree's owner document.
+ * @return Document
+ */
+ get document() {
+ return this._containerNode.ownerDocument;
+ },
+
+ /**
+ * Gets the root item of this tree.
+ * @return AbstractTreeItem
+ */
+ get root() {
+ return this._rootItem;
+ },
+
+ /**
+ * Gets the parent of this tree item.
+ * @return AbstractTreeItem
+ */
+ get parent() {
+ return this._parentItem;
+ },
+
+ /**
+ * Gets the indentation level of this tree item.
+ */
+ get level() {
+ return this._level;
+ },
+
+ /**
+ * Gets the element displaying this tree item.
+ */
+ get target() {
+ return this._targetNode;
+ },
+
+ /**
+ * Gets the element containing all tree items.
+ * @return nsIDOMNode
+ */
+ get container() {
+ return this._containerNode;
+ },
+
+ /**
+ * Returns whether or not this item is populated in the tree.
+ * Collapsed items can still be populated.
+ * @return boolean
+ */
+ get populated() {
+ return this._populated;
+ },
+
+ /**
+ * Returns whether or not this item is expanded in the tree.
+ * Expanded items with no children aren't consudered `populated`.
+ * @return boolean
+ */
+ get expanded() {
+ return this._expanded;
+ },
+
+ /**
+ * Gets the bounds for this tree's container without flushing.
+ * @return object
+ */
+ get bounds() {
+ let win = this.document.defaultView;
+ let utils = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+ return utils.getBoundsWithoutFlushing(this._containerNode);
+ },
+
+ /**
+ * Creates and appends this tree item to the specified parent element.
+ *
+ * @param nsIDOMNode containerNode
+ * The parent element for this tree item (and every other tree item).
+ * @param nsIDOMNode fragmentNode [optional]
+ * An optional document fragment temporarily holding this tree item in
+ * the current batch. Defaults to the `containerNode`.
+ * @param nsIDOMNode beforeNode [optional]
+ * An optional child element which should succeed this tree item.
+ */
+ attachTo: function (containerNode, fragmentNode = containerNode, beforeNode = null) {
+ this._containerNode = containerNode;
+ this._constructTargetNode();
+
+ if (beforeNode) {
+ fragmentNode.insertBefore(this._targetNode, beforeNode);
+ } else {
+ fragmentNode.appendChild(this._targetNode);
+ }
+
+ if (this._level < this.autoExpandDepth) {
+ this.expand();
+ }
+ },
+
+ /**
+ * Permanently removes this tree item (and all subsequent children) from the
+ * parent container.
+ */
+ remove: function () {
+ this._targetNode.remove();
+ this._hideChildren();
+ this._childTreeItems.length = 0;
+ },
+
+ /**
+ * Focuses this item in the tree.
+ */
+ focus: function () {
+ this._targetNode.focus();
+ },
+
+ /**
+ * Expands this item in the tree.
+ */
+ expand: function () {
+ if (this._expanded) {
+ return;
+ }
+ this._expanded = true;
+ this._arrowNode.setAttribute("open", "");
+ this._targetNode.setAttribute("expanded", "");
+ this._toggleChildren(true);
+ this._rootItem.emit("expand", this);
+ },
+
+ /**
+ * Collapses this item in the tree.
+ */
+ collapse: function () {
+ if (!this._expanded) {
+ return;
+ }
+ this._expanded = false;
+ this._arrowNode.removeAttribute("open");
+ this._targetNode.removeAttribute("expanded", "");
+ this._toggleChildren(false);
+ this._rootItem.emit("collapse", this);
+ },
+
+ /**
+ * Returns the child item at the specified index.
+ *
+ * @param number index
+ * @return AbstractTreeItem
+ */
+ getChild: function (index = 0) {
+ return this._childTreeItems[index];
+ },
+
+ /**
+ * Calls the provided function on all the descendants of this item.
+ * If this item was never expanded, then no descendents exist yet.
+ * @param function cb
+ */
+ traverse: function (cb) {
+ for (let child of this._childTreeItems) {
+ cb(child);
+ child.bfs();
+ }
+ },
+
+ /**
+ * Calls the provided function on all descendants of this item until
+ * a truthy value is returned by the predicate.
+ * @param function predicate
+ * @return AbstractTreeItem
+ */
+ find: function (predicate) {
+ for (let child of this._childTreeItems) {
+ if (predicate(child) || child.find(predicate)) {
+ return child;
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Shows or hides all the children of this item in the tree. If neessary,
+ * populates this item with children.
+ *
+ * @param boolean visible
+ * True if the children should be visible, false otherwise.
+ */
+ _toggleChildren: function (visible) {
+ if (visible) {
+ if (!this._populated) {
+ this._populateSelf(this._childTreeItems);
+ this._populated = this._childTreeItems.length > 0;
+ }
+ this._showChildren();
+ } else {
+ this._hideChildren();
+ }
+ },
+
+ /**
+ * Shows all children of this item in the tree.
+ */
+ _showChildren: function () {
+ // If this is the root item and we're not expanding any child nodes,
+ // it is safe to append everything at once.
+ if (this == this._rootItem && this.autoExpandDepth == 0) {
+ this._appendChildrenBatch();
+ }
+ // Otherwise, append the child items and their descendants successively;
+ // if not, the tree will become garbled and nodes will intertwine,
+ // since all the tree items are sharing a single container node.
+ else {
+ this._appendChildrenSuccessive();
+ }
+ },
+
+ /**
+ * Hides all children of this item in the tree.
+ */
+ _hideChildren: function () {
+ for (let item of this._childTreeItems) {
+ item._targetNode.remove();
+ item._hideChildren();
+ }
+ },
+
+ /**
+ * Appends all children in a single batch.
+ * This only works properly for root nodes when no child nodes will expand.
+ */
+ _appendChildrenBatch: function () {
+ if (this._fragment === undefined) {
+ this._fragment = this.document.createDocumentFragment();
+ }
+
+ let childTreeItems = this._childTreeItems;
+
+ for (let i = 0, len = childTreeItems.length; i < len; i++) {
+ childTreeItems[i].attachTo(this._containerNode, this._fragment);
+ }
+
+ this._containerNode.appendChild(this._fragment);
+ },
+
+ /**
+ * Appends all children successively.
+ */
+ _appendChildrenSuccessive: function () {
+ let childTreeItems = this._childTreeItems;
+ let expandedChildTreeItems = childTreeItems.filter(e => e._expanded);
+ let nextNode = this._getSiblingAtDelta(1);
+
+ for (let i = 0, len = childTreeItems.length; i < len; i++) {
+ childTreeItems[i].attachTo(this._containerNode, undefined, nextNode);
+ }
+ for (let i = 0, len = expandedChildTreeItems.length; i < len; i++) {
+ expandedChildTreeItems[i]._showChildren();
+ }
+ },
+
+ /**
+ * Constructs and stores the target node displaying this tree item.
+ */
+ _constructTargetNode: function () {
+ if (this._constructed) {
+ return;
+ }
+ this._onArrowClick = this._onArrowClick.bind(this);
+ this._onClick = this._onClick.bind(this);
+ this._onDoubleClick = this._onDoubleClick.bind(this);
+ this._onKeyPress = this._onKeyPress.bind(this);
+ this._onFocus = this._onFocus.bind(this);
+ this._onBlur = this._onBlur.bind(this);
+
+ let document = this.document;
+
+ let arrowNode = this._arrowNode = document.createElement("hbox");
+ arrowNode.className = "arrow theme-twisty";
+ arrowNode.addEventListener("mousedown", this._onArrowClick);
+
+ let targetNode = this._targetNode = this._displaySelf(document, arrowNode);
+ targetNode.style.MozUserFocus = "normal";
+
+ targetNode.addEventListener("mousedown", this._onClick);
+ targetNode.addEventListener("dblclick", this._onDoubleClick);
+ targetNode.addEventListener("keypress", this._onKeyPress);
+ targetNode.addEventListener("focus", this._onFocus);
+ targetNode.addEventListener("blur", this._onBlur);
+
+ this._constructed = true;
+ },
+
+ /**
+ * Gets the element displaying an item in the tree at the specified offset
+ * relative to this item.
+ *
+ * @param number delta
+ * The offset from this item to the target item.
+ * @return nsIDOMNode
+ * The element displaying the target item at the specified offset.
+ */
+ _getSiblingAtDelta: function (delta) {
+ let childNodes = this._containerNode.childNodes;
+ let indexOfSelf = Array.indexOf(childNodes, this._targetNode);
+ if (indexOfSelf + delta >= 0) {
+ return childNodes[indexOfSelf + delta];
+ }
+ return undefined;
+ },
+
+ _getNodesPerPageSize: function() {
+ let childNodes = this._containerNode.childNodes;
+ let nodeHeight = this._getHeight(childNodes[childNodes.length - 1]);
+ let containerHeight = this.bounds.height;
+ return Math.ceil(containerHeight / nodeHeight);
+ },
+
+ _getHeight: function(elem) {
+ let win = this.document.defaultView;
+ let utils = win.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ return utils.getBoundsWithoutFlushing(elem).height;
+ },
+
+ /**
+ * Focuses the first item in this tree.
+ */
+ _focusFirstNode: function () {
+ let childNodes = this._containerNode.childNodes;
+ // The root node of the tree may be hidden in practice, so uses for-loop
+ // here to find the next visible node.
+ for (let i = 0; i < childNodes.length; i++) {
+ // The height will be 0 if an element is invisible.
+ if (this._getHeight(childNodes[i])) {
+ childNodes[i].focus();
+ return;
+ }
+ }
+ },
+
+ /**
+ * Focuses the last item in this tree.
+ */
+ _focusLastNode: function () {
+ let childNodes = this._containerNode.childNodes;
+ childNodes[childNodes.length - 1].focus();
+ },
+
+ /**
+ * Focuses the next item in this tree.
+ */
+ _focusNextNode: function () {
+ let nextElement = this._getSiblingAtDelta(1);
+ if (nextElement) nextElement.focus(); // nsIDOMNode
+ },
+
+ /**
+ * Focuses the previous item in this tree.
+ */
+ _focusPrevNode: function () {
+ let prevElement = this._getSiblingAtDelta(-1);
+ if (prevElement) prevElement.focus(); // nsIDOMNode
+ },
+
+ /**
+ * Focuses the parent item in this tree.
+ *
+ * The parent item is not always the previous item, because any tree item
+ * may have multiple children.
+ */
+ _focusParentNode: function () {
+ let parentItem = this._parentItem;
+ if (parentItem) parentItem.focus(); // AbstractTreeItem
+ },
+
+ /**
+ * Handler for the "click" event on the arrow node of this tree item.
+ */
+ _onArrowClick: function (e) {
+ if (!this._expanded) {
+ this.expand();
+ } else {
+ this.collapse();
+ }
+ },
+
+ /**
+ * Handler for the "click" event on the element displaying this tree item.
+ */
+ _onClick: function (e) {
+ e.stopPropagation();
+ this.focus();
+ },
+
+ /**
+ * Handler for the "dblclick" event on the element displaying this tree item.
+ */
+ _onDoubleClick: function (e) {
+ // Ignore dblclick on the arrow as it has already recived and handled two
+ // click events.
+ if (!e.target.classList.contains("arrow")) {
+ this._onArrowClick(e);
+ }
+ this.focus();
+ },
+
+ /**
+ * Handler for the "keypress" event on the element displaying this tree item.
+ */
+ _onKeyPress: function (e) {
+ // Prevent scrolling when pressing navigation keys.
+ ViewHelpers.preventScrolling(e);
+
+ switch (e.keyCode) {
+ case KeyCodes.DOM_VK_UP:
+ this._focusPrevNode();
+ return;
+
+ case KeyCodes.DOM_VK_DOWN:
+ this._focusNextNode();
+ return;
+
+ case KeyCodes.DOM_VK_LEFT:
+ if (this._expanded && this._populated) {
+ this.collapse();
+ } else {
+ this._focusParentNode();
+ }
+ return;
+
+ case KeyCodes.DOM_VK_RIGHT:
+ if (!this._expanded) {
+ this.expand();
+ } else {
+ this._focusNextNode();
+ }
+ return;
+
+ case KeyCodes.DOM_VK_PAGE_UP:
+ let pageUpElement =
+ this._getSiblingAtDelta(-this._getNodesPerPageSize());
+ // There's a chance that the root node is hidden. In this case, its
+ // height will be 0.
+ if (pageUpElement && this._getHeight(pageUpElement)) {
+ pageUpElement.focus();
+ } else {
+ this._focusFirstNode();
+ }
+ return;
+
+ case KeyCodes.DOM_VK_PAGE_DOWN:
+ let pageDownElement =
+ this._getSiblingAtDelta(this._getNodesPerPageSize());
+ if (pageDownElement) {
+ pageDownElement.focus();
+ } else {
+ this._focusLastNode();
+ }
+ return;
+
+ case KeyCodes.DOM_VK_HOME:
+ this._focusFirstNode();
+ return;
+
+ case KeyCodes.DOM_VK_END:
+ this._focusLastNode();
+ return;
+ }
+ },
+
+ /**
+ * Handler for the "focus" event on the element displaying this tree item.
+ */
+ _onFocus: function (e) {
+ this._rootItem.emit("focus", this);
+ },
+
+ /**
+ * Handler for the "blur" event on the element displaying this tree item.
+ */
+ _onBlur: function (e) {
+ this._rootItem.emit("blur", this);
+ }
+};
diff --git a/devtools/client/shared/widgets/BarGraphWidget.js b/devtools/client/shared/widgets/BarGraphWidget.js
new file mode 100644
index 000000000..b11c6c021
--- /dev/null
+++ b/devtools/client/shared/widgets/BarGraphWidget.js
@@ -0,0 +1,498 @@
+"use strict";
+
+const { Heritage, setNamedTimeout, clearNamedTimeout } = require("devtools/client/shared/widgets/view-helpers");
+const { AbstractCanvasGraph, CanvasGraphUtils } = require("devtools/client/shared/widgets/Graphs");
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+
+// Bar graph constants.
+
+const GRAPH_DAMPEN_VALUES_FACTOR = 0.75;
+
+// The following are in pixels
+const GRAPH_BARS_MARGIN_TOP = 1;
+const GRAPH_BARS_MARGIN_END = 1;
+const GRAPH_MIN_BARS_WIDTH = 5;
+const GRAPH_MIN_BLOCKS_HEIGHT = 1;
+
+const GRAPH_BACKGROUND_GRADIENT_START = "rgba(0,136,204,0.0)";
+const GRAPH_BACKGROUND_GRADIENT_END = "rgba(255,255,255,0.25)";
+
+const GRAPH_CLIPHEAD_LINE_COLOR = "#666";
+const GRAPH_SELECTION_LINE_COLOR = "#555";
+const GRAPH_SELECTION_BACKGROUND_COLOR = "rgba(0,136,204,0.25)";
+const GRAPH_SELECTION_STRIPES_COLOR = "rgba(255,255,255,0.1)";
+const GRAPH_REGION_BACKGROUND_COLOR = "transparent";
+const GRAPH_REGION_STRIPES_COLOR = "rgba(237,38,85,0.2)";
+
+const GRAPH_HIGHLIGHTS_MASK_BACKGROUND = "rgba(255,255,255,0.75)";
+const GRAPH_HIGHLIGHTS_MASK_STRIPES = "rgba(255,255,255,0.5)";
+
+// in ms
+const GRAPH_LEGEND_MOUSEOVER_DEBOUNCE = 50;
+
+/**
+ * A bar graph, plotting tuples of values as rectangles.
+ *
+ * @see AbstractCanvasGraph for emitted events and other options.
+ *
+ * Example usage:
+ * let graph = new BarGraphWidget(node);
+ * graph.format = ...;
+ * graph.once("ready", () => {
+ * graph.setData(src);
+ * });
+ *
+ * The `graph.format` traits are mandatory and will determine how the values
+ * are styled as "blocks" in every "bar":
+ * [
+ * { color: "#f00", label: "Foo" },
+ * { color: "#0f0", label: "Bar" },
+ * ...
+ * { color: "#00f", label: "Baz" }
+ * ]
+ *
+ * Data source format:
+ * [
+ * { delta: x1, values: [y11, y12, ... y1n] },
+ * { delta: x2, values: [y21, y22, ... y2n] },
+ * ...
+ * { delta: xm, values: [ym1, ym2, ... ymn] }
+ * ]
+ * where each item in the array represents a "bar", for which every value
+ * represents a "block" inside that "bar", plotted at the "delta" position.
+ *
+ * @param nsIDOMNode parent
+ * The parent node holding the graph.
+ */
+this.BarGraphWidget = function (parent, ...args) {
+ AbstractCanvasGraph.apply(this, [parent, "bar-graph", ...args]);
+
+ this.once("ready", () => {
+ this._onLegendMouseOver = this._onLegendMouseOver.bind(this);
+ this._onLegendMouseOut = this._onLegendMouseOut.bind(this);
+ this._onLegendMouseDown = this._onLegendMouseDown.bind(this);
+ this._onLegendMouseUp = this._onLegendMouseUp.bind(this);
+ this._createLegend();
+ });
+};
+
+BarGraphWidget.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
+ clipheadLineColor: GRAPH_CLIPHEAD_LINE_COLOR,
+ selectionLineColor: GRAPH_SELECTION_LINE_COLOR,
+ selectionBackgroundColor: GRAPH_SELECTION_BACKGROUND_COLOR,
+ selectionStripesColor: GRAPH_SELECTION_STRIPES_COLOR,
+ regionBackgroundColor: GRAPH_REGION_BACKGROUND_COLOR,
+ regionStripesColor: GRAPH_REGION_STRIPES_COLOR,
+
+ /**
+ * List of colors used to fill each block inside every bar, also
+ * corresponding to labels displayed in this graph's legend.
+ * @see constructor
+ */
+ format: null,
+
+ /**
+ * Optionally offsets the `delta` in the data source by this scalar.
+ */
+ dataOffsetX: 0,
+
+ /**
+ * Optionally uses this value instead of the last tick in the data source
+ * to compute the horizontal scaling.
+ */
+ dataDuration: 0,
+
+ /**
+ * The scalar used to multiply the graph values to leave some headroom
+ * on the top.
+ */
+ dampenValuesFactor: GRAPH_DAMPEN_VALUES_FACTOR,
+
+ /**
+ * Bars that are too close too each other in the graph will be combined.
+ * This scalar specifies the required minimum width of each bar.
+ */
+ minBarsWidth: GRAPH_MIN_BARS_WIDTH,
+
+ /**
+ * Blocks in a bar that are too thin inside the bar will not be rendered.
+ * This scalar specifies the required minimum height of each block.
+ */
+ minBlocksHeight: GRAPH_MIN_BLOCKS_HEIGHT,
+
+ /**
+ * Renders the graph's background.
+ * @see AbstractCanvasGraph.prototype.buildBackgroundImage
+ */
+ buildBackgroundImage: function () {
+ let { canvas, ctx } = this._getNamedCanvas("bar-graph-background");
+ let width = this._width;
+ let height = this._height;
+
+ let gradient = ctx.createLinearGradient(0, 0, 0, height);
+ gradient.addColorStop(0, GRAPH_BACKGROUND_GRADIENT_START);
+ gradient.addColorStop(1, GRAPH_BACKGROUND_GRADIENT_END);
+ ctx.fillStyle = gradient;
+ ctx.fillRect(0, 0, width, height);
+
+ return canvas;
+ },
+
+ /**
+ * Renders the graph's data source.
+ * @see AbstractCanvasGraph.prototype.buildGraphImage
+ */
+ buildGraphImage: function () {
+ if (!this.format || !this.format.length) {
+ throw new Error("The graph format traits are mandatory to style " +
+ "the data source.");
+ }
+ let { canvas, ctx } = this._getNamedCanvas("bar-graph-data");
+ let width = this._width;
+ let height = this._height;
+
+ let totalTypes = this.format.length;
+ let totalTicks = this._data.length;
+ let lastTick = this._data[totalTicks - 1].delta;
+
+ let minBarsWidth = this.minBarsWidth * this._pixelRatio;
+ let minBlocksHeight = this.minBlocksHeight * this._pixelRatio;
+
+ let duration = this.dataDuration || lastTick;
+ let dataScaleX = this.dataScaleX = width / (duration - this.dataOffsetX);
+ let dataScaleY = this.dataScaleY = height / this._calcMaxHeight({
+ data: this._data,
+ dataScaleX: dataScaleX,
+ minBarsWidth: minBarsWidth
+ }) * this.dampenValuesFactor;
+
+ // Draw the graph.
+
+ // Iterate over the blocks, then the bars, to draw all rectangles of
+ // the same color in a single pass. See the @constructor for more
+ // information about the data source, and how a "bar" contains "blocks".
+
+ this._blocksBoundingRects = [];
+ let prevHeight = [];
+ let scaledMarginEnd = GRAPH_BARS_MARGIN_END * this._pixelRatio;
+ let scaledMarginTop = GRAPH_BARS_MARGIN_TOP * this._pixelRatio;
+
+ for (let type = 0; type < totalTypes; type++) {
+ ctx.fillStyle = this.format[type].color || "#000";
+ ctx.beginPath();
+
+ let prevRight = 0;
+ let skippedCount = 0;
+ let skippedHeight = 0;
+
+ for (let tick = 0; tick < totalTicks; tick++) {
+ let delta = this._data[tick].delta;
+ let value = this._data[tick].values[type] || 0;
+ let blockRight = (delta - this.dataOffsetX) * dataScaleX;
+ let blockHeight = value * dataScaleY;
+
+ let blockWidth = blockRight - prevRight;
+ if (blockWidth < minBarsWidth) {
+ skippedCount++;
+ skippedHeight += blockHeight;
+ continue;
+ }
+
+ let averageHeight = (blockHeight + skippedHeight) / (skippedCount + 1);
+ if (averageHeight >= minBlocksHeight) {
+ let bottom = height - ~~prevHeight[tick];
+ ctx.moveTo(prevRight, bottom);
+ ctx.lineTo(prevRight, bottom - averageHeight);
+ ctx.lineTo(blockRight, bottom - averageHeight);
+ ctx.lineTo(blockRight, bottom);
+
+ // Remember this block's type and location.
+ this._blocksBoundingRects.push({
+ type: type,
+ start: prevRight,
+ end: blockRight,
+ top: bottom - averageHeight,
+ bottom: bottom
+ });
+
+ if (prevHeight[tick] === undefined) {
+ prevHeight[tick] = averageHeight + scaledMarginTop;
+ } else {
+ prevHeight[tick] += averageHeight + scaledMarginTop;
+ }
+ }
+
+ prevRight += blockWidth + scaledMarginEnd;
+ skippedHeight = 0;
+ skippedCount = 0;
+ }
+
+ ctx.fill();
+ }
+
+ // The blocks bounding rects isn't guaranteed to be sorted ascending by
+ // block location on the X axis. This should be the case, for better
+ // cache cohesion and a faster `buildMaskImage`.
+ this._blocksBoundingRects.sort((a, b) => a.start > b.start ? 1 : -1);
+
+ // Update the legend.
+
+ while (this._legendNode.hasChildNodes()) {
+ this._legendNode.firstChild.remove();
+ }
+ for (let { color, label } of this.format) {
+ this._createLegendItem(color, label);
+ }
+
+ return canvas;
+ },
+
+ /**
+ * Renders the graph's mask.
+ * Fades in only the parts of the graph that are inside the specified areas.
+ *
+ * @param array highlights
+ * A list of { start, end } values. Optionally, each object
+ * in the list may also specify { top, bottom } pixel values if the
+ * highlighting shouldn't span across the full height of the graph.
+ * @param boolean inPixels
+ * Set this to true if the { start, end } values in the highlights
+ * list are pixel values, and not values from the data source.
+ * @param function unpack [optional]
+ * @see AbstractCanvasGraph.prototype.getMappedSelection
+ */
+ buildMaskImage: function (highlights, inPixels = false,
+ unpack = e => e.delta) {
+ // A null `highlights` array is used to clear the mask. An empty array
+ // will mask the entire graph.
+ if (!highlights) {
+ return null;
+ }
+
+ // Get a render target for the highlights. It will be overlaid on top of
+ // the existing graph, masking the areas that aren't highlighted.
+
+ let { canvas, ctx } = this._getNamedCanvas("graph-highlights");
+ let width = this._width;
+ let height = this._height;
+
+ // Draw the background mask.
+
+ let pattern = AbstractCanvasGraph.getStripePattern({
+ ownerDocument: this._document,
+ backgroundColor: GRAPH_HIGHLIGHTS_MASK_BACKGROUND,
+ stripesColor: GRAPH_HIGHLIGHTS_MASK_STRIPES
+ });
+ ctx.fillStyle = pattern;
+ ctx.fillRect(0, 0, width, height);
+
+ // Clear highlighted areas.
+
+ let totalTicks = this._data.length;
+ let firstTick = unpack(this._data[0]);
+ let lastTick = unpack(this._data[totalTicks - 1]);
+
+ for (let { start, end, top, bottom } of highlights) {
+ if (!inPixels) {
+ start = CanvasGraphUtils.map(start, firstTick, lastTick, 0, width);
+ end = CanvasGraphUtils.map(end, firstTick, lastTick, 0, width);
+ }
+ let firstSnap = findFirst(this._blocksBoundingRects,
+ e => e.start >= start);
+ let lastSnap = findLast(this._blocksBoundingRects,
+ e => e.start >= start && e.end <= end);
+
+ let x1 = firstSnap ? firstSnap.start : start;
+ let x2;
+ if (lastSnap) {
+ x2 = lastSnap.end;
+ } else {
+ x2 = firstSnap ? firstSnap.end : end;
+ }
+
+ let y1 = top || 0;
+ let y2 = bottom || height;
+ ctx.clearRect(x1, y1, x2 - x1, y2 - y1);
+ }
+
+ return canvas;
+ },
+
+ /**
+ * A list storing the bounding rectangle for each drawn block in the graph.
+ * Created whenever `buildGraphImage` is invoked.
+ */
+ _blocksBoundingRects: null,
+
+ /**
+ * Calculates the height of the tallest bar that would eventially be rendered
+ * in this graph.
+ *
+ * Bars that are too close too each other in the graph will be combined.
+ * @see `minBarsWidth`
+ *
+ * @return number
+ * The tallest bar height in this graph.
+ */
+ _calcMaxHeight: function ({ data, dataScaleX, minBarsWidth }) {
+ let maxHeight = 0;
+ let prevRight = 0;
+ let skippedCount = 0;
+ let skippedHeight = 0;
+ let scaledMarginEnd = GRAPH_BARS_MARGIN_END * this._pixelRatio;
+
+ for (let { delta, values } of data) {
+ let barRight = (delta - this.dataOffsetX) * dataScaleX;
+ let barHeight = values.reduce((a, b) => a + b, 0);
+
+ let barWidth = barRight - prevRight;
+ if (barWidth < minBarsWidth) {
+ skippedCount++;
+ skippedHeight += barHeight;
+ continue;
+ }
+
+ let averageHeight = (barHeight + skippedHeight) / (skippedCount + 1);
+ maxHeight = Math.max(averageHeight, maxHeight);
+
+ prevRight += barWidth + scaledMarginEnd;
+ skippedHeight = 0;
+ skippedCount = 0;
+ }
+
+ return maxHeight;
+ },
+
+ /**
+ * Creates the legend container when constructing this graph.
+ */
+ _createLegend: function () {
+ let legendNode = this._legendNode = this._document.createElementNS(HTML_NS,
+ "div");
+ legendNode.className = "bar-graph-widget-legend";
+ this._container.appendChild(legendNode);
+ },
+
+ /**
+ * Creates a legend item when constructing this graph.
+ */
+ _createLegendItem: function (color, label) {
+ let itemNode = this._document.createElementNS(HTML_NS, "div");
+ itemNode.className = "bar-graph-widget-legend-item";
+
+ let colorNode = this._document.createElementNS(HTML_NS, "span");
+ colorNode.setAttribute("view", "color");
+ colorNode.setAttribute("data-index", this._legendNode.childNodes.length);
+ colorNode.style.backgroundColor = color;
+ colorNode.addEventListener("mouseover", this._onLegendMouseOver);
+ colorNode.addEventListener("mouseout", this._onLegendMouseOut);
+ colorNode.addEventListener("mousedown", this._onLegendMouseDown);
+ colorNode.addEventListener("mouseup", this._onLegendMouseUp);
+
+ let labelNode = this._document.createElementNS(HTML_NS, "span");
+ labelNode.setAttribute("view", "label");
+ labelNode.textContent = label;
+
+ itemNode.appendChild(colorNode);
+ itemNode.appendChild(labelNode);
+ this._legendNode.appendChild(itemNode);
+ },
+
+ /**
+ * Invoked whenever a color node in the legend is hovered.
+ */
+ _onLegendMouseOver: function (ev) {
+ setNamedTimeout(
+ "bar-graph-debounce",
+ GRAPH_LEGEND_MOUSEOVER_DEBOUNCE,
+ () => {
+ let type = ev.target.dataset.index;
+ let rects = this._blocksBoundingRects.filter(e => e.type == type);
+
+ this._originalHighlights = this._mask;
+ this._hasCustomHighlights = true;
+ this.setMask(rects, true);
+
+ this.emit("legend-hover", [type, rects]);
+ }
+ );
+ },
+
+ /**
+ * Invoked whenever a color node in the legend is unhovered.
+ */
+ _onLegendMouseOut: function () {
+ clearNamedTimeout("bar-graph-debounce");
+
+ if (this._hasCustomHighlights) {
+ this.setMask(this._originalHighlights);
+ this._hasCustomHighlights = false;
+ this._originalHighlights = null;
+ }
+
+ this.emit("legend-unhover");
+ },
+
+ /**
+ * Invoked whenever a color node in the legend is pressed.
+ */
+ _onLegendMouseDown: function (ev) {
+ ev.preventDefault();
+ ev.stopPropagation();
+
+ let type = ev.target.dataset.index;
+ let rects = this._blocksBoundingRects.filter(e => e.type == type);
+ let leftmost = rects[0];
+ let rightmost = rects[rects.length - 1];
+ if (!leftmost || !rightmost) {
+ this.dropSelection();
+ } else {
+ this.setSelection({ start: leftmost.start, end: rightmost.end });
+ }
+
+ this.emit("legend-selection", [leftmost, rightmost]);
+ },
+
+ /**
+ * Invoked whenever a color node in the legend is released.
+ */
+ _onLegendMouseUp: function (e) {
+ e.preventDefault();
+ e.stopPropagation();
+ }
+});
+
+/**
+ * Finds the first element in an array that validates a predicate.
+ * @param array
+ * @param function predicate
+ * @return number
+ */
+function findFirst(array, predicate) {
+ for (let i = 0, len = array.length; i < len; i++) {
+ let element = array[i];
+ if (predicate(element)) {
+ return element;
+ }
+ }
+ return null;
+}
+
+/**
+ * Finds the last element in an array that validates a predicate.
+ * @param array
+ * @param function predicate
+ * @return number
+ */
+function findLast(array, predicate) {
+ for (let i = array.length - 1; i >= 0; i--) {
+ let element = array[i];
+ if (predicate(element)) {
+ return element;
+ }
+ }
+ return null;
+}
+
+module.exports = BarGraphWidget;
diff --git a/devtools/client/shared/widgets/BreadcrumbsWidget.jsm b/devtools/client/shared/widgets/BreadcrumbsWidget.jsm
new file mode 100644
index 000000000..900a125b0
--- /dev/null
+++ b/devtools/client/shared/widgets/BreadcrumbsWidget.jsm
@@ -0,0 +1,250 @@
+/* -*- 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 Cu = Components.utils;
+
+const ENSURE_SELECTION_VISIBLE_DELAY = 50; // ms
+
+const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const { ViewHelpers, setNamedTimeout } = require("devtools/client/shared/widgets/view-helpers");
+const EventEmitter = require("devtools/shared/event-emitter");
+
+this.EXPORTED_SYMBOLS = ["BreadcrumbsWidget"];
+
+/**
+ * A breadcrumb-like list of items.
+ *
+ * Note: this widget should be used in tandem with the WidgetMethods in
+ * view-helpers.js.
+ *
+ * @param nsIDOMNode aNode
+ * The element associated with the widget.
+ * @param Object aOptions
+ * - smoothScroll: specifies if smooth scrolling on selection is enabled.
+ */
+this.BreadcrumbsWidget = function BreadcrumbsWidget(aNode, aOptions = {}) {
+ this.document = aNode.ownerDocument;
+ this.window = this.document.defaultView;
+ this._parent = aNode;
+
+ // Create an internal arrowscrollbox container.
+ this._list = this.document.createElement("arrowscrollbox");
+ this._list.className = "breadcrumbs-widget-container";
+ this._list.setAttribute("flex", "1");
+ this._list.setAttribute("orient", "horizontal");
+ this._list.setAttribute("clicktoscroll", "true");
+ this._list.setAttribute("smoothscroll", !!aOptions.smoothScroll);
+ this._list.addEventListener("keypress", e => this.emit("keyPress", e), false);
+ this._list.addEventListener("mousedown", e => this.emit("mousePress", e), false);
+ this._parent.appendChild(this._list);
+
+ // By default, hide the arrows. We let the arrowscrollbox show them
+ // in case of overflow.
+ this._list._scrollButtonUp.collapsed = true;
+ this._list._scrollButtonDown.collapsed = true;
+ this._list.addEventListener("underflow", this._onUnderflow.bind(this), false);
+ this._list.addEventListener("overflow", this._onOverflow.bind(this), false);
+
+ // This widget emits events that can be handled in a MenuContainer.
+ EventEmitter.decorate(this);
+
+ // Delegate some of the associated node's methods to satisfy the interface
+ // required by MenuContainer instances.
+ ViewHelpers.delegateWidgetAttributeMethods(this, aNode);
+ ViewHelpers.delegateWidgetEventMethods(this, aNode);
+};
+
+BreadcrumbsWidget.prototype = {
+ /**
+ * Inserts an item in this container at the specified index.
+ *
+ * @param number aIndex
+ * The position in the container intended for this item.
+ * @param nsIDOMNode aContents
+ * The node displayed in the container.
+ * @return nsIDOMNode
+ * The element associated with the displayed item.
+ */
+ insertItemAt: function (aIndex, aContents) {
+ let list = this._list;
+ let breadcrumb = new Breadcrumb(this, aContents);
+ return list.insertBefore(breadcrumb._target, list.childNodes[aIndex]);
+ },
+
+ /**
+ * Returns the child node in this container situated at the specified index.
+ *
+ * @param number aIndex
+ * The position in the container intended for this item.
+ * @return nsIDOMNode
+ * The element associated with the displayed item.
+ */
+ getItemAtIndex: function (aIndex) {
+ return this._list.childNodes[aIndex];
+ },
+
+ /**
+ * Removes the specified child node from this container.
+ *
+ * @param nsIDOMNode aChild
+ * The element associated with the displayed item.
+ */
+ removeChild: function (aChild) {
+ this._list.removeChild(aChild);
+
+ if (this._selectedItem == aChild) {
+ this._selectedItem = null;
+ }
+ },
+
+ /**
+ * Removes all of the child nodes from this container.
+ */
+ removeAllItems: function () {
+ let list = this._list;
+
+ while (list.hasChildNodes()) {
+ list.firstChild.remove();
+ }
+
+ this._selectedItem = null;
+ },
+
+ /**
+ * Gets the currently selected child node in this container.
+ * @return nsIDOMNode
+ */
+ get selectedItem() {
+ return this._selectedItem;
+ },
+
+ /**
+ * Sets the currently selected child node in this container.
+ * @param nsIDOMNode aChild
+ */
+ set selectedItem(aChild) {
+ let childNodes = this._list.childNodes;
+
+ if (!aChild) {
+ this._selectedItem = null;
+ }
+ for (let node of childNodes) {
+ if (node == aChild) {
+ node.setAttribute("checked", "");
+ this._selectedItem = node;
+ } else {
+ node.removeAttribute("checked");
+ }
+ }
+ },
+
+ /**
+ * Returns the value of the named attribute on this container.
+ *
+ * @param string aName
+ * The name of the attribute.
+ * @return string
+ * The current attribute value.
+ */
+ getAttribute: function (aName) {
+ if (aName == "scrollPosition") return this._list.scrollPosition;
+ if (aName == "scrollWidth") return this._list.scrollWidth;
+ return this._parent.getAttribute(aName);
+ },
+
+ /**
+ * Ensures the specified element is visible.
+ *
+ * @param nsIDOMNode aElement
+ * The element to make visible.
+ */
+ ensureElementIsVisible: function (aElement) {
+ if (!aElement) {
+ return;
+ }
+
+ // Repeated calls to ensureElementIsVisible would interfere with each other
+ // and may sometimes result in incorrect scroll positions.
+ setNamedTimeout("breadcrumb-select", ENSURE_SELECTION_VISIBLE_DELAY, () => {
+ if (this._list.ensureElementIsVisible) {
+ this._list.ensureElementIsVisible(aElement);
+ }
+ });
+ },
+
+ /**
+ * The underflow and overflow listener for the arrowscrollbox container.
+ */
+ _onUnderflow: function ({ target }) {
+ if (target != this._list) {
+ return;
+ }
+ target._scrollButtonUp.collapsed = true;
+ target._scrollButtonDown.collapsed = true;
+ target.removeAttribute("overflows");
+ },
+
+ /**
+ * The underflow and overflow listener for the arrowscrollbox container.
+ */
+ _onOverflow: function ({ target }) {
+ if (target != this._list) {
+ return;
+ }
+ target._scrollButtonUp.collapsed = false;
+ target._scrollButtonDown.collapsed = false;
+ target.setAttribute("overflows", "");
+ },
+
+ window: null,
+ document: null,
+ _parent: null,
+ _list: null,
+ _selectedItem: null
+};
+
+/**
+ * A Breadcrumb constructor for the BreadcrumbsWidget.
+ *
+ * @param BreadcrumbsWidget aWidget
+ * The widget to contain this breadcrumb.
+ * @param nsIDOMNode aContents
+ * The node displayed in the container.
+ */
+function Breadcrumb(aWidget, aContents) {
+ this.document = aWidget.document;
+ this.window = aWidget.window;
+ this.ownerView = aWidget;
+
+ this._target = this.document.createElement("hbox");
+ this._target.className = "breadcrumbs-widget-item";
+ this._target.setAttribute("align", "center");
+ this.contents = aContents;
+}
+
+Breadcrumb.prototype = {
+ /**
+ * Sets the contents displayed in this item's view.
+ *
+ * @param string | nsIDOMNode aContents
+ * The string or node displayed in the container.
+ */
+ set contents(aContents) {
+ // If there are already some contents displayed, replace them.
+ if (this._target.hasChildNodes()) {
+ this._target.replaceChild(aContents, this._target.firstChild);
+ return;
+ }
+ // These are the first contents ever displayed.
+ this._target.appendChild(aContents);
+ },
+
+ window: null,
+ document: null,
+ ownerView: null,
+ _target: null
+};
diff --git a/devtools/client/shared/widgets/Chart.jsm b/devtools/client/shared/widgets/Chart.jsm
new file mode 100644
index 000000000..0894a62ca
--- /dev/null
+++ b/devtools/client/shared/widgets/Chart.jsm
@@ -0,0 +1,449 @@
+/* -*- 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 Cu = Components.utils;
+
+const NET_STRINGS_URI = "devtools/client/locales/netmonitor.properties";
+const SVG_NS = "http://www.w3.org/2000/svg";
+const PI = Math.PI;
+const TAU = PI * 2;
+const EPSILON = 0.0000001;
+const NAMED_SLICE_MIN_ANGLE = TAU / 8;
+const NAMED_SLICE_TEXT_DISTANCE_RATIO = 1.9;
+const HOVERED_SLICE_TRANSLATE_DISTANCE_RATIO = 20;
+
+const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
+const EventEmitter = require("devtools/shared/event-emitter");
+const { LocalizationHelper } = require("devtools/shared/l10n");
+
+this.EXPORTED_SYMBOLS = ["Chart"];
+
+/**
+ * Localization convenience methods.
+ */
+var L10N = new LocalizationHelper(NET_STRINGS_URI);
+
+/**
+ * A factory for creating charts.
+ * Example usage: let myChart = Chart.Pie(document, { ... });
+ */
+var Chart = {
+ Pie: createPieChart,
+ Table: createTableChart,
+ PieTable: createPieTableChart
+};
+
+/**
+ * A simple pie chart proxy for the underlying view.
+ * Each item in the `slices` property represents a [data, node] pair containing
+ * the data used to create the slice and the nsIDOMNode displaying it.
+ *
+ * @param nsIDOMNode node
+ * The node representing the view for this chart.
+ */
+function PieChart(node) {
+ this.node = node;
+ this.slices = new WeakMap();
+ EventEmitter.decorate(this);
+}
+
+/**
+ * A simple table chart proxy for the underlying view.
+ * Each item in the `rows` property represents a [data, node] pair containing
+ * the data used to create the row and the nsIDOMNode displaying it.
+ *
+ * @param nsIDOMNode node
+ * The node representing the view for this chart.
+ */
+function TableChart(node) {
+ this.node = node;
+ this.rows = new WeakMap();
+ EventEmitter.decorate(this);
+}
+
+/**
+ * A simple pie+table chart proxy for the underlying view.
+ *
+ * @param nsIDOMNode node
+ * The node representing the view for this chart.
+ * @param PieChart pie
+ * The pie chart proxy.
+ * @param TableChart table
+ * The table chart proxy.
+ */
+function PieTableChart(node, pie, table) {
+ this.node = node;
+ this.pie = pie;
+ this.table = table;
+ EventEmitter.decorate(this);
+}
+
+/**
+ * Creates the DOM for a pie+table chart.
+ *
+ * @param nsIDocument document
+ * The document responsible with creating the DOM.
+ * @param object
+ * An object containing all or some of the following properties:
+ * - title: a string displayed as the table chart's (description)/local
+ * - diameter: the diameter of the pie chart, in pixels
+ * - data: an array of items used to display each slice in the pie
+ * and each row in the table;
+ * @see `createPieChart` and `createTableChart` for details.
+ * - strings: @see `createTableChart` for details.
+ * - totals: @see `createTableChart` for details.
+ * - sorted: a flag specifying if the `data` should be sorted
+ * ascending by `size`.
+ * @return PieTableChart
+ * A pie+table chart proxy instance, which emits the following events:
+ * - "mouseover", when the mouse enters a slice or a row
+ * - "mouseout", when the mouse leaves a slice or a row
+ * - "click", when the mouse enters a slice or a row
+ */
+function createPieTableChart(document, { title, diameter, data, strings, totals, sorted }) {
+ if (data && sorted) {
+ data = data.slice().sort((a, b) => +(a.size < b.size));
+ }
+
+ let pie = Chart.Pie(document, {
+ width: diameter,
+ data: data
+ });
+
+ let table = Chart.Table(document, {
+ title: title,
+ data: data,
+ strings: strings,
+ totals: totals
+ });
+
+ let container = document.createElement("hbox");
+ container.className = "pie-table-chart-container";
+ container.appendChild(pie.node);
+ container.appendChild(table.node);
+
+ let proxy = new PieTableChart(container, pie, table);
+
+ pie.on("click", (event, item) => {
+ proxy.emit(event, item);
+ });
+
+ table.on("click", (event, item) => {
+ proxy.emit(event, item);
+ });
+
+ pie.on("mouseover", (event, item) => {
+ proxy.emit(event, item);
+ if (table.rows.has(item)) {
+ table.rows.get(item).setAttribute("focused", "");
+ }
+ });
+
+ pie.on("mouseout", (event, item) => {
+ proxy.emit(event, item);
+ if (table.rows.has(item)) {
+ table.rows.get(item).removeAttribute("focused");
+ }
+ });
+
+ table.on("mouseover", (event, item) => {
+ proxy.emit(event, item);
+ if (pie.slices.has(item)) {
+ pie.slices.get(item).setAttribute("focused", "");
+ }
+ });
+
+ table.on("mouseout", (event, item) => {
+ proxy.emit(event, item);
+ if (pie.slices.has(item)) {
+ pie.slices.get(item).removeAttribute("focused");
+ }
+ });
+
+ return proxy;
+}
+
+/**
+ * Creates the DOM for a pie chart based on the specified properties.
+ *
+ * @param nsIDocument document
+ * The document responsible with creating the DOM.
+ * @param object
+ * An object containing all or some of the following properties:
+ * - data: an array of items used to display each slice; all the items
+ * should be objects containing a `size` and a `label` property.
+ * e.g: [{
+ * size: 1,
+ * label: "foo"
+ * }, {
+ * size: 2,
+ * label: "bar"
+ * }];
+ * - width: the width of the chart, in pixels
+ * - height: optional, the height of the chart, in pixels.
+ * - centerX: optional, the X-axis center of the chart, in pixels.
+ * - centerY: optional, the Y-axis center of the chart, in pixels.
+ * - radius: optional, the radius of the chart, in pixels.
+ * @return PieChart
+ * A pie chart proxy instance, which emits the following events:
+ * - "mouseover", when the mouse enters a slice
+ * - "mouseout", when the mouse leaves a slice
+ * - "click", when the mouse clicks a slice
+ */
+function createPieChart(document, { data, width, height, centerX, centerY, radius }) {
+ height = height || width;
+ centerX = centerX || width / 2;
+ centerY = centerY || height / 2;
+ radius = radius || (width + height) / 4;
+ let isPlaceholder = false;
+
+ // Filter out very small sizes, as they'll just render invisible slices.
+ data = data ? data.filter(e => e.size > EPSILON) : null;
+
+ // If there's no data available, display an empty placeholder.
+ if (!data) {
+ data = loadingPieChartData;
+ isPlaceholder = true;
+ }
+ if (!data.length) {
+ data = emptyPieChartData;
+ isPlaceholder = true;
+ }
+
+ let container = document.createElementNS(SVG_NS, "svg");
+ container.setAttribute("class", "generic-chart-container pie-chart-container");
+ container.setAttribute("pack", "center");
+ container.setAttribute("flex", "1");
+ container.setAttribute("width", width);
+ container.setAttribute("height", height);
+ container.setAttribute("viewBox", "0 0 " + width + " " + height);
+ container.setAttribute("slices", data.length);
+ container.setAttribute("placeholder", isPlaceholder);
+
+ let proxy = new PieChart(container);
+
+ let total = data.reduce((acc, e) => acc + e.size, 0);
+ let angles = data.map(e => e.size / total * (TAU - EPSILON));
+ let largest = data.reduce((a, b) => a.size > b.size ? a : b);
+ let smallest = data.reduce((a, b) => a.size < b.size ? a : b);
+
+ let textDistance = radius / NAMED_SLICE_TEXT_DISTANCE_RATIO;
+ let translateDistance = radius / HOVERED_SLICE_TRANSLATE_DISTANCE_RATIO;
+ let startAngle = TAU;
+ let endAngle = 0;
+ let midAngle = 0;
+ radius -= translateDistance;
+
+ for (let i = data.length - 1; i >= 0; i--) {
+ let sliceInfo = data[i];
+ let sliceAngle = angles[i];
+ if (!sliceInfo.size || sliceAngle < EPSILON) {
+ continue;
+ }
+
+ endAngle = startAngle - sliceAngle;
+ midAngle = (startAngle + endAngle) / 2;
+
+ let x1 = centerX + radius * Math.sin(startAngle);
+ let y1 = centerY - radius * Math.cos(startAngle);
+ let x2 = centerX + radius * Math.sin(endAngle);
+ let y2 = centerY - radius * Math.cos(endAngle);
+ let largeArcFlag = Math.abs(startAngle - endAngle) > PI ? 1 : 0;
+
+ let pathNode = document.createElementNS(SVG_NS, "path");
+ pathNode.setAttribute("class", "pie-chart-slice chart-colored-blob");
+ pathNode.setAttribute("name", sliceInfo.label);
+ pathNode.setAttribute("d",
+ " M " + centerX + "," + centerY +
+ " L " + x2 + "," + y2 +
+ " A " + radius + "," + radius +
+ " 0 " + largeArcFlag +
+ " 1 " + x1 + "," + y1 +
+ " Z");
+
+ if (sliceInfo == largest) {
+ pathNode.setAttribute("largest", "");
+ }
+ if (sliceInfo == smallest) {
+ pathNode.setAttribute("smallest", "");
+ }
+
+ let hoverX = translateDistance * Math.sin(midAngle);
+ let hoverY = -translateDistance * Math.cos(midAngle);
+ let hoverTransform = "transform: translate(" + hoverX + "px, " + hoverY + "px)";
+ pathNode.setAttribute("style", data.length > 1 ? hoverTransform : "");
+
+ proxy.slices.set(sliceInfo, pathNode);
+ delegate(proxy, ["click", "mouseover", "mouseout"], pathNode, sliceInfo);
+ container.appendChild(pathNode);
+
+ if (sliceInfo.label && sliceAngle > NAMED_SLICE_MIN_ANGLE) {
+ let textX = centerX + textDistance * Math.sin(midAngle);
+ let textY = centerY - textDistance * Math.cos(midAngle);
+ let label = document.createElementNS(SVG_NS, "text");
+ label.appendChild(document.createTextNode(sliceInfo.label));
+ label.setAttribute("class", "pie-chart-label");
+ label.setAttribute("style", data.length > 1 ? hoverTransform : "");
+ label.setAttribute("x", data.length > 1 ? textX : centerX);
+ label.setAttribute("y", data.length > 1 ? textY : centerY);
+ container.appendChild(label);
+ }
+
+ startAngle = endAngle;
+ }
+
+ return proxy;
+}
+
+/**
+ * Creates the DOM for a table chart based on the specified properties.
+ *
+ * @param nsIDocument document
+ * The document responsible with creating the DOM.
+ * @param object
+ * An object containing all or some of the following properties:
+ * - title: a string displayed as the chart's (description)/local
+ * - data: an array of items used to display each row; all the items
+ * should be objects representing columns, for which the
+ * properties' values will be displayed in each cell of a row.
+ * e.g: [{
+ * label1: 1,
+ * label2: 3,
+ * label3: "foo"
+ * }, {
+ * label1: 4,
+ * label2: 6,
+ * label3: "bar
+ * }];
+ * - strings: an object specifying for which rows in the `data` array
+ * their cell values should be stringified and localized
+ * based on a predicate function;
+ * e.g: {
+ * label1: value => l10n.getFormatStr("...", value)
+ * }
+ * - totals: an object specifying for which rows in the `data` array
+ * the sum of their cells is to be displayed in the chart;
+ * e.g: {
+ * label1: total => l10n.getFormatStr("...", total), // 5
+ * label2: total => l10n.getFormatStr("...", total), // 9
+ * }
+ * @return TableChart
+ * A table chart proxy instance, which emits the following events:
+ * - "mouseover", when the mouse enters a row
+ * - "mouseout", when the mouse leaves a row
+ * - "click", when the mouse clicks a row
+ */
+function createTableChart(document, { title, data, strings, totals }) {
+ strings = strings || {};
+ totals = totals || {};
+ let isPlaceholder = false;
+
+ // If there's no data available, display an empty placeholder.
+ if (!data) {
+ data = loadingTableChartData;
+ isPlaceholder = true;
+ }
+ if (!data.length) {
+ data = emptyTableChartData;
+ isPlaceholder = true;
+ }
+
+ let container = document.createElement("vbox");
+ container.className = "generic-chart-container table-chart-container";
+ container.setAttribute("pack", "center");
+ container.setAttribute("flex", "1");
+ container.setAttribute("rows", data.length);
+ container.setAttribute("placeholder", isPlaceholder);
+
+ let proxy = new TableChart(container);
+
+ let titleNode = document.createElement("label");
+ titleNode.className = "plain table-chart-title";
+ titleNode.setAttribute("value", title);
+ container.appendChild(titleNode);
+
+ let tableNode = document.createElement("vbox");
+ tableNode.className = "plain table-chart-grid";
+ container.appendChild(tableNode);
+
+ for (let rowInfo of data) {
+ let rowNode = document.createElement("hbox");
+ rowNode.className = "table-chart-row";
+ rowNode.setAttribute("align", "center");
+
+ let boxNode = document.createElement("hbox");
+ boxNode.className = "table-chart-row-box chart-colored-blob";
+ boxNode.setAttribute("name", rowInfo.label);
+ rowNode.appendChild(boxNode);
+
+ for (let [key, value] of Object.entries(rowInfo)) {
+ let index = data.indexOf(rowInfo);
+ let stringified = strings[key] ? strings[key](value, index) : value;
+ let labelNode = document.createElement("label");
+ labelNode.className = "plain table-chart-row-label";
+ labelNode.setAttribute("name", key);
+ labelNode.setAttribute("value", stringified);
+ rowNode.appendChild(labelNode);
+ }
+
+ proxy.rows.set(rowInfo, rowNode);
+ delegate(proxy, ["click", "mouseover", "mouseout"], rowNode, rowInfo);
+ tableNode.appendChild(rowNode);
+ }
+
+ let totalsNode = document.createElement("vbox");
+ totalsNode.className = "table-chart-totals";
+
+ for (let [key, value] of Object.entries(totals)) {
+ let total = data.reduce((acc, e) => acc + e[key], 0);
+ let stringified = totals[key] ? totals[key](total || 0) : total;
+ let labelNode = document.createElement("label");
+ labelNode.className = "plain table-chart-summary-label";
+ labelNode.setAttribute("name", key);
+ labelNode.setAttribute("value", stringified);
+ totalsNode.appendChild(labelNode);
+ }
+
+ container.appendChild(totalsNode);
+
+ return proxy;
+}
+
+XPCOMUtils.defineLazyGetter(this, "loadingPieChartData", () => {
+ return [{ size: 1, label: L10N.getStr("pieChart.loading") }];
+});
+
+XPCOMUtils.defineLazyGetter(this, "emptyPieChartData", () => {
+ return [{ size: 1, label: L10N.getStr("pieChart.unavailable") }];
+});
+
+XPCOMUtils.defineLazyGetter(this, "loadingTableChartData", () => {
+ return [{ size: "", label: L10N.getStr("tableChart.loading") }];
+});
+
+XPCOMUtils.defineLazyGetter(this, "emptyTableChartData", () => {
+ return [{ size: "", label: L10N.getStr("tableChart.unavailable") }];
+});
+
+/**
+ * Delegates DOM events emitted by an nsIDOMNode to an EventEmitter proxy.
+ *
+ * @param EventEmitter emitter
+ * The event emitter proxy instance.
+ * @param array events
+ * An array of events, e.g. ["mouseover", "mouseout"].
+ * @param nsIDOMNode node
+ * The element firing the DOM events.
+ * @param any args
+ * The arguments passed when emitting events through the proxy.
+ */
+function delegate(emitter, events, node, args) {
+ for (let event of events) {
+ node.addEventListener(event, emitter.emit.bind(emitter, event, args));
+ }
+}
diff --git a/devtools/client/shared/widgets/CubicBezierPresets.js b/devtools/client/shared/widgets/CubicBezierPresets.js
new file mode 100644
index 000000000..d2a77a85c
--- /dev/null
+++ b/devtools/client/shared/widgets/CubicBezierPresets.js
@@ -0,0 +1,64 @@
+/**
+ * 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/.
+ */
+
+// Set of preset definitions for use with CubicBezierWidget
+// Credit: http://easings.net
+
+"use strict";
+
+const PREDEFINED = {
+ "ease": [0.25, 0.1, 0.25, 1],
+ "linear": [0, 0, 1, 1],
+ "ease-in": [0.42, 0, 1, 1],
+ "ease-out": [0, 0, 0.58, 1],
+ "ease-in-out": [0.42, 0, 0.58, 1]
+};
+
+const PRESETS = {
+ "ease-in": {
+ "ease-in-linear": [0, 0, 1, 1],
+ "ease-in-ease-in": [0.42, 0, 1, 1],
+ "ease-in-sine": [0.47, 0, 0.74, 0.71],
+ "ease-in-quadratic": [0.55, 0.09, 0.68, 0.53],
+ "ease-in-cubic": [0.55, 0.06, 0.68, 0.19],
+ "ease-in-quartic": [0.9, 0.03, 0.69, 0.22],
+ "ease-in-quintic": [0.76, 0.05, 0.86, 0.06],
+ "ease-in-exponential": [0.95, 0.05, 0.8, 0.04],
+ "ease-in-circular": [0.6, 0.04, 0.98, 0.34],
+ "ease-in-backward": [0.6, -0.28, 0.74, 0.05]
+ },
+ "ease-out": {
+ "ease-out-linear": [0, 0, 1, 1],
+ "ease-out-ease-out": [0, 0, 0.58, 1],
+ "ease-out-sine": [0.39, 0.58, 0.57, 1],
+ "ease-out-quadratic": [0.25, 0.46, 0.45, 0.94],
+ "ease-out-cubic": [0.22, 0.61, 0.36, 1],
+ "ease-out-quartic": [0.17, 0.84, 0.44, 1],
+ "ease-out-quintic": [0.23, 1, 0.32, 1],
+ "ease-out-exponential": [0.19, 1, 0.22, 1],
+ "ease-out-circular": [0.08, 0.82, 0.17, 1],
+ "ease-out-backward": [0.18, 0.89, 0.32, 1.28]
+ },
+ "ease-in-out": {
+ "ease-in-out-linear": [0, 0, 1, 1],
+ "ease-in-out-ease": [0.25, 0.1, 0.25, 1],
+ "ease-in-out-ease-in-out": [0.42, 0, 0.58, 1],
+ "ease-in-out-sine": [0.45, 0.05, 0.55, 0.95],
+ "ease-in-out-quadratic": [0.46, 0.03, 0.52, 0.96],
+ "ease-in-out-cubic": [0.65, 0.05, 0.36, 1],
+ "ease-in-out-quartic": [0.77, 0, 0.18, 1],
+ "ease-in-out-quintic": [0.86, 0, 0.07, 1],
+ "ease-in-out-exponential": [1, 0, 0, 1],
+ "ease-in-out-circular": [0.79, 0.14, 0.15, 0.86],
+ "ease-in-out-backward": [0.68, -0.55, 0.27, 1.55]
+ }
+};
+
+const DEFAULT_PRESET_CATEGORY = Object.keys(PRESETS)[0];
+
+exports.PRESETS = PRESETS;
+exports.PREDEFINED = PREDEFINED;
+exports.DEFAULT_PRESET_CATEGORY = DEFAULT_PRESET_CATEGORY;
diff --git a/devtools/client/shared/widgets/CubicBezierWidget.js b/devtools/client/shared/widgets/CubicBezierWidget.js
new file mode 100644
index 000000000..337282d46
--- /dev/null
+++ b/devtools/client/shared/widgets/CubicBezierWidget.js
@@ -0,0 +1,897 @@
+/**
+ * Copyright (c) 2013 Lea Verou. All rights reserved.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+// Based on www.cubic-bezier.com by Lea Verou
+// See https://github.com/LeaVerou/cubic-bezier
+
+"use strict";
+
+const EventEmitter = require("devtools/shared/event-emitter");
+const {
+ PREDEFINED,
+ PRESETS,
+ DEFAULT_PRESET_CATEGORY
+} = require("devtools/client/shared/widgets/CubicBezierPresets");
+const {getCSSLexer} = require("devtools/shared/css/lexer");
+const XHTML_NS = "http://www.w3.org/1999/xhtml";
+
+/**
+ * CubicBezier data structure helper
+ * Accepts an array of coordinates and exposes a few useful getters
+ * @param {Array} coordinates i.e. [.42, 0, .58, 1]
+ */
+function CubicBezier(coordinates) {
+ if (!coordinates) {
+ throw new Error("No offsets were defined");
+ }
+
+ this.coordinates = coordinates.map(n => +n);
+
+ for (let i = 4; i--;) {
+ let xy = this.coordinates[i];
+ if (isNaN(xy) || (!(i % 2) && (xy < 0 || xy > 1))) {
+ throw new Error(`Wrong coordinate at ${i}(${xy})`);
+ }
+ }
+
+ this.coordinates.toString = function () {
+ return this.map(n => {
+ return (Math.round(n * 100) / 100 + "").replace(/^0\./, ".");
+ }) + "";
+ };
+}
+
+exports.CubicBezier = CubicBezier;
+
+CubicBezier.prototype = {
+ get P1() {
+ return this.coordinates.slice(0, 2);
+ },
+
+ get P2() {
+ return this.coordinates.slice(2);
+ },
+
+ toString: function () {
+ // Check first if current coords are one of css predefined functions
+ let predefName = Object.keys(PREDEFINED)
+ .find(key => coordsAreEqual(PREDEFINED[key],
+ this.coordinates));
+
+ return predefName || "cubic-bezier(" + this.coordinates + ")";
+ }
+};
+
+/**
+ * Bezier curve canvas plotting class
+ * @param {DOMNode} canvas
+ * @param {CubicBezier} bezier
+ * @param {Array} padding Amount of horizontal,vertical padding around the graph
+ */
+function BezierCanvas(canvas, bezier, padding) {
+ this.canvas = canvas;
+ this.bezier = bezier;
+ this.padding = getPadding(padding);
+
+ // Convert to a cartesian coordinate system with axes from 0 to 1
+ this.ctx = this.canvas.getContext("2d");
+ let p = this.padding;
+
+ this.ctx.scale(canvas.width * (1 - p[1] - p[3]),
+ -canvas.height * (1 - p[0] - p[2]));
+ this.ctx.translate(p[3] / (1 - p[1] - p[3]),
+ -1 - p[0] / (1 - p[0] - p[2]));
+}
+
+exports.BezierCanvas = BezierCanvas;
+
+BezierCanvas.prototype = {
+ /**
+ * Get P1 and P2 current top/left offsets so they can be positioned
+ * @return {Array} Returns an array of 2 {top:String,left:String} objects
+ */
+ get offsets() {
+ let p = this.padding, w = this.canvas.width, h = this.canvas.height;
+
+ return [{
+ left: w * (this.bezier.coordinates[0] * (1 - p[3] - p[1]) - p[3]) + "px",
+ top: h * (1 - this.bezier.coordinates[1] * (1 - p[0] - p[2]) - p[0])
+ + "px"
+ }, {
+ left: w * (this.bezier.coordinates[2] * (1 - p[3] - p[1]) - p[3]) + "px",
+ top: h * (1 - this.bezier.coordinates[3] * (1 - p[0] - p[2]) - p[0])
+ + "px"
+ }];
+ },
+
+ /**
+ * Convert an element's left/top offsets into coordinates
+ */
+ offsetsToCoordinates: function (element) {
+ let p = this.padding, w = this.canvas.width, h = this.canvas.height;
+
+ // Convert padding percentage to actual padding
+ p = p.map((a, i) => a * (i % 2 ? w : h));
+
+ return [
+ (parseFloat(element.style.left) - p[3]) / (w + p[1] + p[3]),
+ (h - parseFloat(element.style.top) - p[2]) / (h - p[0] - p[2])
+ ];
+ },
+
+ /**
+ * Draw the cubic bezier curve for the current coordinates
+ */
+ plot: function (settings = {}) {
+ let xy = this.bezier.coordinates;
+
+ let defaultSettings = {
+ handleColor: "#666",
+ handleThickness: .008,
+ bezierColor: "#4C9ED9",
+ bezierThickness: .015,
+ drawHandles: true
+ };
+
+ for (let setting in settings) {
+ defaultSettings[setting] = settings[setting];
+ }
+
+ // Clear the canvas –making sure to clear the
+ // whole area by resetting the transform first.
+ this.ctx.save();
+ this.ctx.setTransform(1, 0, 0, 1, 0, 0);
+ this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
+ this.ctx.restore();
+
+ if (defaultSettings.drawHandles) {
+ // Draw control handles
+ this.ctx.beginPath();
+ this.ctx.fillStyle = defaultSettings.handleColor;
+ this.ctx.lineWidth = defaultSettings.handleThickness;
+ this.ctx.strokeStyle = defaultSettings.handleColor;
+
+ this.ctx.moveTo(0, 0);
+ this.ctx.lineTo(xy[0], xy[1]);
+ this.ctx.moveTo(1, 1);
+ this.ctx.lineTo(xy[2], xy[3]);
+
+ this.ctx.stroke();
+ this.ctx.closePath();
+
+ let circle = (ctx, cx, cy, r) => {
+ ctx.beginPath();
+ ctx.arc(cx, cy, r, 0, 2 * Math.PI, !1);
+ ctx.closePath();
+ };
+
+ circle(this.ctx, xy[0], xy[1], 1.5 * defaultSettings.handleThickness);
+ this.ctx.fill();
+ circle(this.ctx, xy[2], xy[3], 1.5 * defaultSettings.handleThickness);
+ this.ctx.fill();
+ }
+
+ // Draw bezier curve
+ this.ctx.beginPath();
+ this.ctx.lineWidth = defaultSettings.bezierThickness;
+ this.ctx.strokeStyle = defaultSettings.bezierColor;
+ this.ctx.moveTo(0, 0);
+ this.ctx.bezierCurveTo(xy[0], xy[1], xy[2], xy[3], 1, 1);
+ this.ctx.stroke();
+ this.ctx.closePath();
+ }
+};
+
+/**
+ * Cubic-bezier widget. Uses the BezierCanvas class to draw the curve and
+ * adds the control points and user interaction
+ * @param {DOMNode} parent The container where the graph should be created
+ * @param {Array} coordinates Coordinates of the curve to be drawn
+ *
+ * Emits "updated" events whenever the curve is changed. Along with the event is
+ * sent a CubicBezier object
+ */
+function CubicBezierWidget(parent,
+ coordinates = PRESETS["ease-in"]["ease-in-sine"]) {
+ EventEmitter.decorate(this);
+
+ this.parent = parent;
+ let {curve, p1, p2} = this._initMarkup();
+
+ this.curveBoundingBox = curve.getBoundingClientRect();
+ this.curve = curve;
+ this.p1 = p1;
+ this.p2 = p2;
+
+ // Create and plot the bezier curve
+ this.bezierCanvas = new BezierCanvas(this.curve,
+ new CubicBezier(coordinates), [0.30, 0]);
+ this.bezierCanvas.plot();
+
+ // Place the control points
+ let offsets = this.bezierCanvas.offsets;
+ this.p1.style.left = offsets[0].left;
+ this.p1.style.top = offsets[0].top;
+ this.p2.style.left = offsets[1].left;
+ this.p2.style.top = offsets[1].top;
+
+ this._onPointMouseDown = this._onPointMouseDown.bind(this);
+ this._onPointKeyDown = this._onPointKeyDown.bind(this);
+ this._onCurveClick = this._onCurveClick.bind(this);
+ this._onNewCoordinates = this._onNewCoordinates.bind(this);
+
+ // Add preset preview menu
+ this.presets = new CubicBezierPresetWidget(parent);
+
+ // Add the timing function previewer
+ this.timingPreview = new TimingFunctionPreviewWidget(parent);
+
+ this._initEvents();
+}
+
+exports.CubicBezierWidget = CubicBezierWidget;
+
+CubicBezierWidget.prototype = {
+ _initMarkup: function () {
+ let doc = this.parent.ownerDocument;
+
+ let wrap = doc.createElementNS(XHTML_NS, "div");
+ wrap.className = "display-wrap";
+
+ let plane = doc.createElementNS(XHTML_NS, "div");
+ plane.className = "coordinate-plane";
+
+ let p1 = doc.createElementNS(XHTML_NS, "button");
+ p1.className = "control-point";
+ plane.appendChild(p1);
+
+ let p2 = doc.createElementNS(XHTML_NS, "button");
+ p2.className = "control-point";
+ plane.appendChild(p2);
+
+ let curve = doc.createElementNS(XHTML_NS, "canvas");
+ curve.setAttribute("width", 150);
+ curve.setAttribute("height", 370);
+ curve.className = "curve";
+
+ plane.appendChild(curve);
+ wrap.appendChild(plane);
+
+ this.parent.appendChild(wrap);
+
+ return {
+ p1,
+ p2,
+ curve
+ };
+ },
+
+ _removeMarkup: function () {
+ this.parent.querySelector(".display-wrap").remove();
+ },
+
+ _initEvents: function () {
+ this.p1.addEventListener("mousedown", this._onPointMouseDown);
+ this.p2.addEventListener("mousedown", this._onPointMouseDown);
+
+ this.p1.addEventListener("keydown", this._onPointKeyDown);
+ this.p2.addEventListener("keydown", this._onPointKeyDown);
+
+ this.curve.addEventListener("click", this._onCurveClick);
+
+ this.presets.on("new-coordinates", this._onNewCoordinates);
+ },
+
+ _removeEvents: function () {
+ this.p1.removeEventListener("mousedown", this._onPointMouseDown);
+ this.p2.removeEventListener("mousedown", this._onPointMouseDown);
+
+ this.p1.removeEventListener("keydown", this._onPointKeyDown);
+ this.p2.removeEventListener("keydown", this._onPointKeyDown);
+
+ this.curve.removeEventListener("click", this._onCurveClick);
+
+ this.presets.off("new-coordinates", this._onNewCoordinates);
+ },
+
+ _onPointMouseDown: function (event) {
+ // Updating the boundingbox in case it has changed
+ this.curveBoundingBox = this.curve.getBoundingClientRect();
+
+ let point = event.target;
+ let doc = point.ownerDocument;
+ let self = this;
+
+ doc.onmousemove = function drag(e) {
+ let x = e.pageX;
+ let y = e.pageY;
+ let left = self.curveBoundingBox.left;
+ let top = self.curveBoundingBox.top;
+
+ if (x === 0 && y == 0) {
+ return;
+ }
+
+ // Constrain x
+ x = Math.min(Math.max(left, x), left + self.curveBoundingBox.width);
+
+ point.style.left = x - left + "px";
+ point.style.top = y - top + "px";
+
+ self._updateFromPoints();
+ };
+
+ doc.onmouseup = function () {
+ point.focus();
+ doc.onmousemove = doc.onmouseup = null;
+ };
+ },
+
+ _onPointKeyDown: function (event) {
+ let point = event.target;
+ let code = event.keyCode;
+
+ if (code >= 37 && code <= 40) {
+ event.preventDefault();
+
+ // Arrow keys pressed
+ let left = parseInt(point.style.left, 10);
+ let top = parseInt(point.style.top, 10);
+ let offset = 3 * (event.shiftKey ? 10 : 1);
+
+ switch (code) {
+ case 37: point.style.left = left - offset + "px"; break;
+ case 38: point.style.top = top - offset + "px"; break;
+ case 39: point.style.left = left + offset + "px"; break;
+ case 40: point.style.top = top + offset + "px"; break;
+ }
+
+ this._updateFromPoints();
+ }
+ },
+
+ _onCurveClick: function (event) {
+ this.curveBoundingBox = this.curve.getBoundingClientRect();
+
+ let left = this.curveBoundingBox.left;
+ let top = this.curveBoundingBox.top;
+ let x = event.pageX - left;
+ let y = event.pageY - top;
+
+ // Find which point is closer
+ let distP1 = distance(x, y,
+ parseInt(this.p1.style.left, 10), parseInt(this.p1.style.top, 10));
+ let distP2 = distance(x, y,
+ parseInt(this.p2.style.left, 10), parseInt(this.p2.style.top, 10));
+
+ let point = distP1 < distP2 ? this.p1 : this.p2;
+ point.style.left = x + "px";
+ point.style.top = y + "px";
+
+ this._updateFromPoints();
+ },
+
+ _onNewCoordinates: function (event, coordinates) {
+ this.coordinates = coordinates;
+ },
+
+ /**
+ * Get the current point coordinates and redraw the curve to match
+ */
+ _updateFromPoints: function () {
+ // Get the new coordinates from the point's offsets
+ let coordinates = this.bezierCanvas.offsetsToCoordinates(this.p1);
+ coordinates = coordinates.concat(
+ this.bezierCanvas.offsetsToCoordinates(this.p2)
+ );
+
+ this.presets.refreshMenu(coordinates);
+ this._redraw(coordinates);
+ },
+
+ /**
+ * Redraw the curve
+ * @param {Array} coordinates The array of control point coordinates
+ */
+ _redraw: function (coordinates) {
+ // Provide a new CubicBezier to the canvas and plot the curve
+ this.bezierCanvas.bezier = new CubicBezier(coordinates);
+ this.bezierCanvas.plot();
+ this.emit("updated", this.bezierCanvas.bezier);
+
+ this.timingPreview.preview(this.bezierCanvas.bezier + "");
+ },
+
+ /**
+ * Set new coordinates for the control points and redraw the curve
+ * @param {Array} coordinates
+ */
+ set coordinates(coordinates) {
+ this._redraw(coordinates);
+
+ // Move the points
+ let offsets = this.bezierCanvas.offsets;
+ this.p1.style.left = offsets[0].left;
+ this.p1.style.top = offsets[0].top;
+ this.p2.style.left = offsets[1].left;
+ this.p2.style.top = offsets[1].top;
+ },
+
+ /**
+ * Set new coordinates for the control point and redraw the curve
+ * @param {String} value A string value. E.g. "linear",
+ * "cubic-bezier(0,0,1,1)"
+ */
+ set cssCubicBezierValue(value) {
+ if (!value) {
+ return;
+ }
+
+ value = value.trim();
+
+ // Try with one of the predefined values
+ let coordinates = parseTimingFunction(value);
+
+ this.presets.refreshMenu(coordinates);
+ this.coordinates = coordinates;
+ },
+
+ destroy: function () {
+ this._removeEvents();
+ this._removeMarkup();
+
+ this.timingPreview.destroy();
+ this.presets.destroy();
+
+ this.curve = this.p1 = this.p2 = null;
+ }
+};
+
+/**
+ * CubicBezierPreset widget.
+ * Builds a menu of presets from CubicBezierPresets
+ * @param {DOMNode} parent The container where the preset panel should be
+ * created
+ *
+ * Emits "new-coordinate" event along with the coordinates
+ * whenever a preset is selected.
+ */
+function CubicBezierPresetWidget(parent) {
+ this.parent = parent;
+
+ let {presetPane, presets, categories} = this._initMarkup();
+ this.presetPane = presetPane;
+ this.presets = presets;
+ this.categories = categories;
+
+ this._activeCategory = null;
+ this._activePresetList = null;
+ this._activePreset = null;
+
+ this._onCategoryClick = this._onCategoryClick.bind(this);
+ this._onPresetClick = this._onPresetClick.bind(this);
+
+ EventEmitter.decorate(this);
+ this._initEvents();
+}
+
+exports.CubicBezierPresetWidget = CubicBezierPresetWidget;
+
+CubicBezierPresetWidget.prototype = {
+ /*
+ * Constructs a list of all preset categories and a list
+ * of presets for each category.
+ *
+ * High level markup:
+ * div .preset-pane
+ * div .preset-categories
+ * div .category
+ * div .category
+ * ...
+ * div .preset-container
+ * div .presetList
+ * div .preset
+ * ...
+ * div .presetList
+ * div .preset
+ * ...
+ */
+ _initMarkup: function () {
+ let doc = this.parent.ownerDocument;
+
+ let presetPane = doc.createElementNS(XHTML_NS, "div");
+ presetPane.className = "preset-pane";
+
+ let categoryList = doc.createElementNS(XHTML_NS, "div");
+ categoryList.id = "preset-categories";
+
+ let presetContainer = doc.createElementNS(XHTML_NS, "div");
+ presetContainer.id = "preset-container";
+
+ Object.keys(PRESETS).forEach(categoryLabel => {
+ let category = this._createCategory(categoryLabel);
+ categoryList.appendChild(category);
+
+ let presetList = this._createPresetList(categoryLabel);
+ presetContainer.appendChild(presetList);
+ });
+
+ presetPane.appendChild(categoryList);
+ presetPane.appendChild(presetContainer);
+
+ this.parent.appendChild(presetPane);
+
+ let allCategories = presetPane.querySelectorAll(".category");
+ let allPresets = presetPane.querySelectorAll(".preset");
+
+ return {
+ presetPane: presetPane,
+ presets: allPresets,
+ categories: allCategories
+ };
+ },
+
+ _createCategory: function (categoryLabel) {
+ let doc = this.parent.ownerDocument;
+
+ let category = doc.createElementNS(XHTML_NS, "div");
+ category.id = categoryLabel;
+ category.classList.add("category");
+
+ let categoryDisplayLabel = this._normalizeCategoryLabel(categoryLabel);
+ category.textContent = categoryDisplayLabel;
+ category.setAttribute("title", categoryDisplayLabel);
+
+ return category;
+ },
+
+ _normalizeCategoryLabel: function (categoryLabel) {
+ return categoryLabel.replace("/-/g", " ");
+ },
+
+ _createPresetList: function (categoryLabel) {
+ let doc = this.parent.ownerDocument;
+
+ let presetList = doc.createElementNS(XHTML_NS, "div");
+ presetList.id = "preset-category-" + categoryLabel;
+ presetList.classList.add("preset-list");
+
+ Object.keys(PRESETS[categoryLabel]).forEach(presetLabel => {
+ let preset = this._createPreset(categoryLabel, presetLabel);
+ presetList.appendChild(preset);
+ });
+
+ return presetList;
+ },
+
+ _createPreset: function (categoryLabel, presetLabel) {
+ let doc = this.parent.ownerDocument;
+
+ let preset = doc.createElementNS(XHTML_NS, "div");
+ preset.classList.add("preset");
+ preset.id = presetLabel;
+ preset.coordinates = PRESETS[categoryLabel][presetLabel];
+ // Create preset preview
+ let curve = doc.createElementNS(XHTML_NS, "canvas");
+ let bezier = new CubicBezier(preset.coordinates);
+ curve.setAttribute("height", 50);
+ curve.setAttribute("width", 50);
+ preset.bezierCanvas = new BezierCanvas(curve, bezier, [0.15, 0]);
+ preset.bezierCanvas.plot({
+ drawHandles: false,
+ bezierThickness: 0.025
+ });
+ preset.appendChild(curve);
+
+ // Create preset label
+ let presetLabelElem = doc.createElementNS(XHTML_NS, "p");
+ let presetDisplayLabel = this._normalizePresetLabel(categoryLabel,
+ presetLabel);
+ presetLabelElem.textContent = presetDisplayLabel;
+ preset.appendChild(presetLabelElem);
+ preset.setAttribute("title", presetDisplayLabel);
+
+ return preset;
+ },
+
+ _normalizePresetLabel: function (categoryLabel, presetLabel) {
+ return presetLabel.replace(categoryLabel + "-", "").replace("/-/g", " ");
+ },
+
+ _initEvents: function () {
+ for (let category of this.categories) {
+ category.addEventListener("click", this._onCategoryClick);
+ }
+
+ for (let preset of this.presets) {
+ preset.addEventListener("click", this._onPresetClick);
+ }
+ },
+
+ _removeEvents: function () {
+ for (let category of this.categories) {
+ category.removeEventListener("click", this._onCategoryClick);
+ }
+
+ for (let preset of this.presets) {
+ preset.removeEventListener("click", this._onPresetClick);
+ }
+ },
+
+ _onPresetClick: function (event) {
+ this.emit("new-coordinates", event.currentTarget.coordinates);
+ this.activePreset = event.currentTarget;
+ },
+
+ _onCategoryClick: function (event) {
+ this.activeCategory = event.target;
+ },
+
+ _setActivePresetList: function (presetListId) {
+ let presetList = this.presetPane.querySelector("#" + presetListId);
+ swapClassName("active-preset-list", this._activePresetList, presetList);
+ this._activePresetList = presetList;
+ },
+
+ set activeCategory(category) {
+ swapClassName("active-category", this._activeCategory, category);
+ this._activeCategory = category;
+ this._setActivePresetList("preset-category-" + category.id);
+ },
+
+ get activeCategory() {
+ return this._activeCategory;
+ },
+
+ set activePreset(preset) {
+ swapClassName("active-preset", this._activePreset, preset);
+ this._activePreset = preset;
+ },
+
+ get activePreset() {
+ return this._activePreset;
+ },
+
+ /**
+ * Called by CubicBezierWidget onload and when
+ * the curve is modified via the canvas.
+ * Attempts to match the new user setting with an
+ * existing preset.
+ * @param {Array} coordinates new coords [i, j, k, l]
+ */
+ refreshMenu: function (coordinates) {
+ // If we cannot find a matching preset, keep
+ // menu on last known preset category.
+ let category = this._activeCategory;
+
+ // If we cannot find a matching preset
+ // deselect any selected preset.
+ let preset = null;
+
+ // If a category has never been viewed before
+ // show the default category.
+ if (!category) {
+ category = this.parent.querySelector("#" + DEFAULT_PRESET_CATEGORY);
+ }
+
+ // If the new coordinates do match a preset,
+ // set its category and preset button as active.
+ Object.keys(PRESETS).forEach(categoryLabel => {
+ Object.keys(PRESETS[categoryLabel]).forEach(presetLabel => {
+ if (coordsAreEqual(PRESETS[categoryLabel][presetLabel], coordinates)) {
+ category = this.parent.querySelector("#" + categoryLabel);
+ preset = this.parent.querySelector("#" + presetLabel);
+ }
+ });
+ });
+
+ this.activeCategory = category;
+ this.activePreset = preset;
+ },
+
+ destroy: function () {
+ this._removeEvents();
+ this.parent.querySelector(".preset-pane").remove();
+ }
+};
+
+/**
+ * The TimingFunctionPreviewWidget animates a dot on a scale with a given
+ * timing-function
+ * @param {DOMNode} parent The container where this widget should go
+ */
+function TimingFunctionPreviewWidget(parent) {
+ this.previousValue = null;
+ this.autoRestartAnimation = null;
+
+ this.parent = parent;
+ this._initMarkup();
+}
+
+TimingFunctionPreviewWidget.prototype = {
+ PREVIEW_DURATION: 1000,
+
+ _initMarkup: function () {
+ let doc = this.parent.ownerDocument;
+
+ let container = doc.createElementNS(XHTML_NS, "div");
+ container.className = "timing-function-preview";
+
+ this.dot = doc.createElementNS(XHTML_NS, "div");
+ this.dot.className = "dot";
+ container.appendChild(this.dot);
+
+ let scale = doc.createElementNS(XHTML_NS, "div");
+ scale.className = "scale";
+ container.appendChild(scale);
+
+ this.parent.appendChild(container);
+ },
+
+ destroy: function () {
+ clearTimeout(this.autoRestartAnimation);
+ this.parent.querySelector(".timing-function-preview").remove();
+ this.parent = this.dot = null;
+ },
+
+ /**
+ * Preview a new timing function. The current preview will only be stopped if
+ * the supplied function value is different from the previous one. If the
+ * supplied function is invalid, the preview will stop.
+ * @param {String} value
+ */
+ preview: function (value) {
+ // Don't restart the preview animation if the value is the same
+ if (value === this.previousValue) {
+ return;
+ }
+
+ clearTimeout(this.autoRestartAnimation);
+
+ if (parseTimingFunction(value)) {
+ this.dot.style.animationTimingFunction = value;
+ this.restartAnimation();
+ }
+
+ this.previousValue = value;
+ },
+
+ /**
+ * Re-start the preview animation from the beginning
+ */
+ restartAnimation: function () {
+ // Just toggling the class won't do it unless there's a sync reflow
+ this.dot.animate([
+ { left: "-7px", offset: 0 },
+ { left: "143px", offset: 0.25 },
+ { left: "143px", offset: 0.5 },
+ { left: "-7px", offset: 0.75 },
+ { left: "-7px", offset: 1 }
+ ], {
+ duration: (this.PREVIEW_DURATION * 2),
+ fill: "forwards"
+ });
+
+ // Restart it again after a while
+ this.autoRestartAnimation = setTimeout(this.restartAnimation.bind(this),
+ this.PREVIEW_DURATION * 2);
+ }
+};
+
+// Helpers
+
+function getPadding(padding) {
+ let p = typeof padding === "number" ? [padding] : padding;
+
+ if (p.length === 1) {
+ p[1] = p[0];
+ }
+
+ if (p.length === 2) {
+ p[2] = p[0];
+ }
+
+ if (p.length === 3) {
+ p[3] = p[1];
+ }
+
+ return p;
+}
+
+function distance(x1, y1, x2, y2) {
+ return Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2));
+}
+
+/**
+ * Parse a string to see whether it is a valid timing function.
+ * If it is, return the coordinates as an array.
+ * Otherwise, return undefined.
+ * @param {String} value
+ * @return {Array} of coordinates, or undefined
+ */
+function parseTimingFunction(value) {
+ if (value in PREDEFINED) {
+ return PREDEFINED[value];
+ }
+
+ let tokenStream = getCSSLexer(value);
+ let getNextToken = () => {
+ while (true) {
+ let token = tokenStream.nextToken();
+ if (!token || (token.tokenType !== "whitespace" &&
+ token.tokenType !== "comment")) {
+ return token;
+ }
+ }
+ };
+
+ let token = getNextToken();
+ if (token.tokenType !== "function" || token.text !== "cubic-bezier") {
+ return undefined;
+ }
+
+ let result = [];
+ for (let i = 0; i < 4; ++i) {
+ token = getNextToken();
+ if (!token || token.tokenType !== "number") {
+ return undefined;
+ }
+ result.push(token.number);
+
+ token = getNextToken();
+ if (!token || token.tokenType !== "symbol" ||
+ token.text !== (i == 3 ? ")" : ",")) {
+ return undefined;
+ }
+ }
+
+ return result;
+}
+
+// This is exported for testing.
+exports._parseTimingFunction = parseTimingFunction;
+
+/**
+ * Removes a class from a node and adds it to another.
+ * @param {String} className the class to swap
+ * @param {DOMNode} from the node to remove the class from
+ * @param {DOMNode} to the node to add the class to
+ */
+function swapClassName(className, from, to) {
+ if (from !== null) {
+ from.classList.remove(className);
+ }
+
+ if (to !== null) {
+ to.classList.add(className);
+ }
+}
+
+/**
+ * Compares two arrays of coordinates [i, j, k, l]
+ * @param {Array} c1 first coordinate array to compare
+ * @param {Array} c2 second coordinate array to compare
+ * @return {Boolean}
+ */
+function coordsAreEqual(c1, c2) {
+ return c1.reduce((prev, curr, index) => prev && (curr === c2[index]), true);
+}
diff --git a/devtools/client/shared/widgets/FastListWidget.js b/devtools/client/shared/widgets/FastListWidget.js
new file mode 100644
index 000000000..d005ead51
--- /dev/null
+++ b/devtools/client/shared/widgets/FastListWidget.js
@@ -0,0 +1,249 @@
+/* -*- 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 EventEmitter = require("devtools/shared/event-emitter");
+const { ViewHelpers } = require("devtools/client/shared/widgets/view-helpers");
+
+/**
+ * A list menu widget that attempts to be very fast.
+ *
+ * Note: this widget should be used in tandem with the WidgetMethods in
+ * view-helpers.js.
+ *
+ * @param nsIDOMNode aNode
+ * The element associated with the widget.
+ */
+const FastListWidget = module.exports = function FastListWidget(node) {
+ this.document = node.ownerDocument;
+ this.window = this.document.defaultView;
+ this._parent = node;
+ this._fragment = this.document.createDocumentFragment();
+
+ // This is a prototype element that each item added to the list clones.
+ this._templateElement = this.document.createElement("hbox");
+
+ // Create an internal scrollbox container.
+ this._list = this.document.createElement("scrollbox");
+ this._list.className = "fast-list-widget-container theme-body";
+ this._list.setAttribute("flex", "1");
+ this._list.setAttribute("orient", "vertical");
+ this._list.setAttribute("tabindex", "0");
+ this._list.addEventListener("keypress", e => this.emit("keyPress", e), false);
+ this._list.addEventListener("mousedown", e => this.emit("mousePress", e),
+ false);
+ this._parent.appendChild(this._list);
+
+ this._orderedMenuElementsArray = [];
+ this._itemsByElement = new Map();
+
+ // This widget emits events that can be handled in a MenuContainer.
+ EventEmitter.decorate(this);
+
+ // Delegate some of the associated node's methods to satisfy the interface
+ // required by MenuContainer instances.
+ ViewHelpers.delegateWidgetAttributeMethods(this, node);
+ ViewHelpers.delegateWidgetEventMethods(this, node);
+};
+
+FastListWidget.prototype = {
+ /**
+ * Inserts an item in this container at the specified index, optionally
+ * grouping by name.
+ *
+ * @param number aIndex
+ * The position in the container intended for this item.
+ * @param nsIDOMNode aContents
+ * The node to be displayed in the container.
+ * @param Object aAttachment [optional]
+ * Extra data for the user.
+ * @return nsIDOMNode
+ * The element associated with the displayed item.
+ */
+ insertItemAt: function (index, contents, attachment = {}) {
+ let element = this._templateElement.cloneNode();
+ element.appendChild(contents);
+
+ if (index >= 0) {
+ throw new Error("FastListWidget only supports appending items.");
+ }
+
+ this._fragment.appendChild(element);
+ this._orderedMenuElementsArray.push(element);
+ this._itemsByElement.set(element, this);
+
+ return element;
+ },
+
+ /**
+ * This is a non-standard widget implementation method. When appending items,
+ * they are queued in a document fragment. This method appends the document
+ * fragment to the dom.
+ */
+ flush: function () {
+ this._list.appendChild(this._fragment);
+ },
+
+ /**
+ * Removes all of the child nodes from this container.
+ */
+ removeAllItems: function () {
+ let list = this._list;
+
+ while (list.hasChildNodes()) {
+ list.firstChild.remove();
+ }
+
+ this._selectedItem = null;
+
+ this._orderedMenuElementsArray.length = 0;
+ this._itemsByElement.clear();
+ },
+
+ /**
+ * Remove the given item.
+ */
+ removeChild: function (child) {
+ throw new Error("Not yet implemented");
+ },
+
+ /**
+ * Gets the currently selected child node in this container.
+ * @return nsIDOMNode
+ */
+ get selectedItem() {
+ return this._selectedItem;
+ },
+
+ /**
+ * Sets the currently selected child node in this container.
+ * @param nsIDOMNode child
+ */
+ set selectedItem(child) {
+ let menuArray = this._orderedMenuElementsArray;
+
+ if (!child) {
+ this._selectedItem = null;
+ }
+ for (let node of menuArray) {
+ if (node == child) {
+ node.classList.add("selected");
+ this._selectedItem = node;
+ } else {
+ node.classList.remove("selected");
+ }
+ }
+
+ this.ensureElementIsVisible(this.selectedItem);
+ },
+
+ /**
+ * Returns the child node in this container situated at the specified index.
+ *
+ * @param number index
+ * The position in the container intended for this item.
+ * @return nsIDOMNode
+ * The element associated with the displayed item.
+ */
+ getItemAtIndex: function (index) {
+ return this._orderedMenuElementsArray[index];
+ },
+
+ /**
+ * Adds a new attribute or changes an existing attribute on this container.
+ *
+ * @param string name
+ * The name of the attribute.
+ * @param string value
+ * The desired attribute value.
+ */
+ setAttribute: function (name, value) {
+ this._parent.setAttribute(name, value);
+
+ if (name == "emptyText") {
+ this._textWhenEmpty = value;
+ }
+ },
+
+ /**
+ * Removes an attribute on this container.
+ *
+ * @param string name
+ * The name of the attribute.
+ */
+ removeAttribute: function (name) {
+ this._parent.removeAttribute(name);
+
+ if (name == "emptyText") {
+ this._removeEmptyText();
+ }
+ },
+
+ /**
+ * Ensures the specified element is visible.
+ *
+ * @param nsIDOMNode element
+ * The element to make visible.
+ */
+ ensureElementIsVisible: function (element) {
+ if (!element) {
+ return;
+ }
+
+ // Ensure the element is visible but not scrolled horizontally.
+ let boxObject = this._list.boxObject;
+ boxObject.ensureElementIsVisible(element);
+ boxObject.scrollBy(-this._list.clientWidth, 0);
+ },
+
+ /**
+ * Sets the text displayed in this container when empty.
+ * @param string aValue
+ */
+ set _textWhenEmpty(value) {
+ if (this._emptyTextNode) {
+ this._emptyTextNode.setAttribute("value", value);
+ }
+ this._emptyTextValue = value;
+ this._showEmptyText();
+ },
+
+ /**
+ * Creates and appends a label signaling that this container is empty.
+ */
+ _showEmptyText: function () {
+ if (this._emptyTextNode || !this._emptyTextValue) {
+ return;
+ }
+ let label = this.document.createElement("label");
+ label.className = "plain fast-list-widget-empty-text";
+ label.setAttribute("value", this._emptyTextValue);
+
+ this._parent.insertBefore(label, this._list);
+ this._emptyTextNode = label;
+ },
+
+ /**
+ * Removes the label signaling that this container is empty.
+ */
+ _removeEmptyText: function () {
+ if (!this._emptyTextNode) {
+ return;
+ }
+ this._parent.removeChild(this._emptyTextNode);
+ this._emptyTextNode = null;
+ },
+
+ window: null,
+ document: null,
+ _parent: null,
+ _list: null,
+ _selectedItem: null,
+ _orderedMenuElementsArray: null,
+ _itemsByElement: null,
+ _emptyTextNode: null,
+ _emptyTextValue: ""
+};
diff --git a/devtools/client/shared/widgets/FilterWidget.js b/devtools/client/shared/widgets/FilterWidget.js
new file mode 100644
index 000000000..9cdb27a5a
--- /dev/null
+++ b/devtools/client/shared/widgets/FilterWidget.js
@@ -0,0 +1,1073 @@
+/* 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";
+
+/**
+ * This is a CSS Filter Editor widget used
+ * for Rule View's filter swatches
+ */
+
+const EventEmitter = require("devtools/shared/event-emitter");
+const { Cc, Ci } = require("chrome");
+const XHTML_NS = "http://www.w3.org/1999/xhtml";
+
+const { LocalizationHelper } = require("devtools/shared/l10n");
+const STRINGS_URI = "devtools/client/locales/filterwidget.properties";
+const L10N = new LocalizationHelper(STRINGS_URI);
+
+const {cssTokenizer} = require("devtools/shared/css/parsing-utils");
+
+const asyncStorage = require("devtools/shared/async-storage");
+
+loader.lazyGetter(this, "DOMUtils", () => {
+ return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
+});
+
+const DEFAULT_FILTER_TYPE = "length";
+const UNIT_MAPPING = {
+ percentage: "%",
+ length: "px",
+ angle: "deg",
+ string: ""
+};
+
+const FAST_VALUE_MULTIPLIER = 10;
+const SLOW_VALUE_MULTIPLIER = 0.1;
+const DEFAULT_VALUE_MULTIPLIER = 1;
+
+const LIST_PADDING = 7;
+const LIST_ITEM_HEIGHT = 32;
+
+const filterList = [
+ {
+ "name": "blur",
+ "range": [0, Infinity],
+ "type": "length"
+ },
+ {
+ "name": "brightness",
+ "range": [0, Infinity],
+ "type": "percentage"
+ },
+ {
+ "name": "contrast",
+ "range": [0, Infinity],
+ "type": "percentage"
+ },
+ {
+ "name": "drop-shadow",
+ "placeholder": L10N.getStr("dropShadowPlaceholder"),
+ "type": "string"
+ },
+ {
+ "name": "grayscale",
+ "range": [0, 100],
+ "type": "percentage"
+ },
+ {
+ "name": "hue-rotate",
+ "range": [0, Infinity],
+ "type": "angle"
+ },
+ {
+ "name": "invert",
+ "range": [0, 100],
+ "type": "percentage"
+ },
+ {
+ "name": "opacity",
+ "range": [0, 100],
+ "type": "percentage"
+ },
+ {
+ "name": "saturate",
+ "range": [0, Infinity],
+ "type": "percentage"
+ },
+ {
+ "name": "sepia",
+ "range": [0, 100],
+ "type": "percentage"
+ },
+ {
+ "name": "url",
+ "placeholder": "example.svg#c1",
+ "type": "string"
+ }
+];
+
+// Valid values that shouldn't be parsed for filters.
+const SPECIAL_VALUES = new Set(["none", "unset", "initial", "inherit"]);
+
+/**
+ * A CSS Filter editor widget used to add/remove/modify
+ * filters.
+ *
+ * Normally, it takes a CSS filter value as input, parses it
+ * and creates the required elements / bindings.
+ *
+ * You can, however, use add/remove/update methods manually.
+ * See each method's comments for more details
+ *
+ * @param {nsIDOMNode} el
+ * The widget container.
+ * @param {String} value
+ * CSS filter value
+ * @param {Function} cssIsValid
+ * Test whether css name / value is valid.
+ */
+function CSSFilterEditorWidget(el, value = "", cssIsValid) {
+ this.doc = el.ownerDocument;
+ this.win = this.doc.defaultView;
+ this.el = el;
+ this._cssIsValid = cssIsValid;
+
+ this._addButtonClick = this._addButtonClick.bind(this);
+ this._removeButtonClick = this._removeButtonClick.bind(this);
+ this._mouseMove = this._mouseMove.bind(this);
+ this._mouseUp = this._mouseUp.bind(this);
+ this._mouseDown = this._mouseDown.bind(this);
+ this._keyDown = this._keyDown.bind(this);
+ this._input = this._input.bind(this);
+ this._presetClick = this._presetClick.bind(this);
+ this._savePreset = this._savePreset.bind(this);
+ this._togglePresets = this._togglePresets.bind(this);
+ this._resetFocus = this._resetFocus.bind(this);
+
+ // Passed to asyncStorage, requires binding
+ this.renderPresets = this.renderPresets.bind(this);
+
+ this._initMarkup();
+ this._buildFilterItemMarkup();
+ this._buildPresetItemMarkup();
+ this._addEventListeners();
+
+ EventEmitter.decorate(this);
+
+ this.filters = [];
+ this.setCssValue(value);
+ this.renderPresets();
+}
+
+exports.CSSFilterEditorWidget = CSSFilterEditorWidget;
+
+CSSFilterEditorWidget.prototype = {
+ _initMarkup: function () {
+ let filterListSelectPlaceholder =
+ L10N.getStr("filterListSelectPlaceholder");
+ let addNewFilterButton = L10N.getStr("addNewFilterButton");
+ let presetsToggleButton = L10N.getStr("presetsToggleButton");
+ let newPresetPlaceholder = L10N.getStr("newPresetPlaceholder");
+ let savePresetButton = L10N.getStr("savePresetButton");
+
+ this.el.innerHTML = `
+ <div class="filters-list">
+ <div id="filters"></div>
+ <div class="footer">
+ <select value="">
+ <option value="">${filterListSelectPlaceholder}</option>
+ </select>
+ <button id="add-filter" class="add">${addNewFilterButton}</button>
+ <button id="toggle-presets">${presetsToggleButton}</button>
+ </div>
+ </div>
+
+ <div class="presets-list">
+ <div id="presets"></div>
+ <div class="footer">
+ <input value="" class="devtools-textinput"
+ placeholder="${newPresetPlaceholder}"></input>
+ <button class="add">${savePresetButton}</button>
+ </div>
+ </div>
+ `;
+ this.filtersList = this.el.querySelector("#filters");
+ this.presetsList = this.el.querySelector("#presets");
+ this.togglePresets = this.el.querySelector("#toggle-presets");
+ this.filterSelect = this.el.querySelector("select");
+ this.addPresetButton = this.el.querySelector(".presets-list .add");
+ this.addPresetInput = this.el.querySelector(".presets-list .footer input");
+
+ this.el.querySelector(".presets-list input").value = "";
+
+ this._populateFilterSelect();
+ },
+
+ _destroyMarkup: function () {
+ this._filterItemMarkup.remove();
+ this.el.remove();
+ this.el = this.filtersList = this._filterItemMarkup = null;
+ this.presetsList = this.togglePresets = this.filterSelect = null;
+ this.addPresetButton = null;
+ },
+
+ destroy: function () {
+ this._removeEventListeners();
+ this._destroyMarkup();
+ },
+
+ /**
+ * Creates <option> elements for each filter definition
+ * in filterList
+ */
+ _populateFilterSelect: function () {
+ let select = this.filterSelect;
+ filterList.forEach(filter => {
+ let option = this.doc.createElementNS(XHTML_NS, "option");
+ option.innerHTML = option.value = filter.name;
+ select.appendChild(option);
+ });
+ },
+
+ /**
+ * Creates a template for filter elements which is cloned and used in render
+ */
+ _buildFilterItemMarkup: function () {
+ let base = this.doc.createElementNS(XHTML_NS, "div");
+ base.className = "filter";
+
+ let name = this.doc.createElementNS(XHTML_NS, "div");
+ name.className = "filter-name";
+
+ let value = this.doc.createElementNS(XHTML_NS, "div");
+ value.className = "filter-value";
+
+ let drag = this.doc.createElementNS(XHTML_NS, "i");
+ drag.title = L10N.getStr("dragHandleTooltipText");
+
+ let label = this.doc.createElementNS(XHTML_NS, "label");
+
+ name.appendChild(drag);
+ name.appendChild(label);
+
+ let unitPreview = this.doc.createElementNS(XHTML_NS, "span");
+ let input = this.doc.createElementNS(XHTML_NS, "input");
+ input.classList.add("devtools-textinput");
+
+ value.appendChild(input);
+ value.appendChild(unitPreview);
+
+ let removeButton = this.doc.createElementNS(XHTML_NS, "button");
+ removeButton.className = "remove-button";
+
+ base.appendChild(name);
+ base.appendChild(value);
+ base.appendChild(removeButton);
+
+ this._filterItemMarkup = base;
+ },
+
+ _buildPresetItemMarkup: function () {
+ let base = this.doc.createElementNS(XHTML_NS, "div");
+ base.classList.add("preset");
+
+ let name = this.doc.createElementNS(XHTML_NS, "label");
+ base.appendChild(name);
+
+ let value = this.doc.createElementNS(XHTML_NS, "span");
+ base.appendChild(value);
+
+ let removeButton = this.doc.createElementNS(XHTML_NS, "button");
+ removeButton.classList.add("remove-button");
+
+ base.appendChild(removeButton);
+
+ this._presetItemMarkup = base;
+ },
+
+ _addEventListeners: function () {
+ this.addButton = this.el.querySelector("#add-filter");
+ this.addButton.addEventListener("click", this._addButtonClick);
+ this.filtersList.addEventListener("click", this._removeButtonClick);
+ this.filtersList.addEventListener("mousedown", this._mouseDown);
+ this.filtersList.addEventListener("keydown", this._keyDown);
+ this.el.addEventListener("mousedown", this._resetFocus);
+
+ this.presetsList.addEventListener("click", this._presetClick);
+ this.togglePresets.addEventListener("click", this._togglePresets);
+ this.addPresetButton.addEventListener("click", this._savePreset);
+
+ // These events are event delegators for
+ // drag-drop re-ordering and label-dragging
+ this.win.addEventListener("mousemove", this._mouseMove);
+ this.win.addEventListener("mouseup", this._mouseUp);
+
+ // Used to workaround float-precision problems
+ this.filtersList.addEventListener("input", this._input);
+ },
+
+ _removeEventListeners: function () {
+ this.addButton.removeEventListener("click", this._addButtonClick);
+ this.filtersList.removeEventListener("click", this._removeButtonClick);
+ this.filtersList.removeEventListener("mousedown", this._mouseDown);
+ this.filtersList.removeEventListener("keydown", this._keyDown);
+ this.el.removeEventListener("mousedown", this._resetFocus);
+
+ this.presetsList.removeEventListener("click", this._presetClick);
+ this.togglePresets.removeEventListener("click", this._togglePresets);
+ this.addPresetButton.removeEventListener("click", this._savePreset);
+
+ // These events are used for drag drop re-ordering
+ this.win.removeEventListener("mousemove", this._mouseMove);
+ this.win.removeEventListener("mouseup", this._mouseUp);
+
+ // Used to workaround float-precision problems
+ this.filtersList.removeEventListener("input", this._input);
+ },
+
+ _getFilterElementIndex: function (el) {
+ return [...this.filtersList.children].indexOf(el);
+ },
+
+ _keyDown: function (e) {
+ if (e.target.tagName.toLowerCase() !== "input" ||
+ (e.keyCode !== 40 && e.keyCode !== 38)) {
+ return;
+ }
+ let input = e.target;
+
+ const direction = e.keyCode === 40 ? -1 : 1;
+
+ let multiplier = DEFAULT_VALUE_MULTIPLIER;
+ if (e.altKey) {
+ multiplier = SLOW_VALUE_MULTIPLIER;
+ } else if (e.shiftKey) {
+ multiplier = FAST_VALUE_MULTIPLIER;
+ }
+
+ const filterEl = e.target.closest(".filter");
+ const index = this._getFilterElementIndex(filterEl);
+ const filter = this.filters[index];
+
+ // Filters that have units are number-type filters. For them,
+ // the value can be incremented/decremented simply.
+ // For other types of filters (e.g. drop-shadow) we need to check
+ // if the keypress happened close to a number first.
+ if (filter.unit) {
+ let startValue = parseFloat(e.target.value);
+ let value = startValue + direction * multiplier;
+
+ const [min, max] = this._definition(filter.name).range;
+ if (value < min) {
+ value = min;
+ } else if (value > max) {
+ value = max;
+ }
+
+ input.value = fixFloat(value);
+
+ this.updateValueAt(index, value);
+ } else {
+ let selectionStart = input.selectionStart;
+ let num = getNeighbourNumber(input.value, selectionStart);
+ if (!num) {
+ return;
+ }
+
+ let {start, end, value} = num;
+
+ let split = input.value.split("");
+ let computed = fixFloat(value + direction * multiplier);
+ let dotIndex = computed.indexOf(".0");
+ if (dotIndex > -1) {
+ computed = computed.slice(0, -2);
+
+ selectionStart = selectionStart > start + dotIndex ?
+ start + dotIndex :
+ selectionStart;
+ }
+ split.splice(start, end - start, computed);
+
+ value = split.join("");
+ input.value = value;
+ this.updateValueAt(index, value);
+ input.setSelectionRange(selectionStart, selectionStart);
+ }
+ e.preventDefault();
+ },
+
+ _input: function (e) {
+ let filterEl = e.target.closest(".filter");
+ let index = this._getFilterElementIndex(filterEl);
+ let filter = this.filters[index];
+ let def = this._definition(filter.name);
+
+ if (def.type !== "string") {
+ e.target.value = fixFloat(e.target.value);
+ }
+ this.updateValueAt(index, e.target.value);
+ },
+
+ _mouseDown: function (e) {
+ let filterEl = e.target.closest(".filter");
+
+ // re-ordering drag handle
+ if (e.target.tagName.toLowerCase() === "i") {
+ this.isReorderingFilter = true;
+ filterEl.startingY = e.pageY;
+ filterEl.classList.add("dragging");
+
+ this.el.classList.add("dragging");
+ // label-dragging
+ } else if (e.target.classList.contains("devtools-draglabel")) {
+ let label = e.target;
+ let input = filterEl.querySelector("input");
+ let index = this._getFilterElementIndex(filterEl);
+
+ this._dragging = {
+ index, label, input,
+ startX: e.pageX
+ };
+
+ this.isDraggingLabel = true;
+ }
+ },
+
+ _addButtonClick: function () {
+ const select = this.filterSelect;
+ if (!select.value) {
+ return;
+ }
+
+ const key = select.value;
+ this.add(key, null);
+
+ this.render();
+ },
+
+ _removeButtonClick: function (e) {
+ const isRemoveButton = e.target.classList.contains("remove-button");
+ if (!isRemoveButton) {
+ return;
+ }
+
+ let filterEl = e.target.closest(".filter");
+ let index = this._getFilterElementIndex(filterEl);
+ this.removeAt(index);
+ },
+
+ _mouseMove: function (e) {
+ if (this.isReorderingFilter) {
+ this._dragFilterElement(e);
+ } else if (this.isDraggingLabel) {
+ this._dragLabel(e);
+ }
+ },
+
+ _dragFilterElement: function (e) {
+ const rect = this.filtersList.getBoundingClientRect();
+ let top = e.pageY - LIST_PADDING;
+ let bottom = e.pageY + LIST_PADDING;
+ // don't allow dragging over top/bottom of list
+ if (top < rect.top || bottom > rect.bottom) {
+ return;
+ }
+
+ const filterEl = this.filtersList.querySelector(".dragging");
+
+ const delta = e.pageY - filterEl.startingY;
+ filterEl.style.top = delta + "px";
+
+ // change is the number of _steps_ taken from initial position
+ // i.e. how many elements we have passed
+ let change = delta / LIST_ITEM_HEIGHT;
+ if (change > 0) {
+ change = Math.floor(change);
+ } else if (change < 0) {
+ change = Math.ceil(change);
+ }
+
+ const children = this.filtersList.children;
+ const index = [...children].indexOf(filterEl);
+ const destination = index + change;
+
+ // If we're moving out, or there's no change at all, stop and return
+ if (destination >= children.length || destination < 0 || change === 0) {
+ return;
+ }
+
+ // Re-order filter objects
+ swapArrayIndices(this.filters, index, destination);
+
+ // Re-order the dragging element in markup
+ const target = change > 0 ? children[destination + 1]
+ : children[destination];
+ if (target) {
+ this.filtersList.insertBefore(filterEl, target);
+ } else {
+ this.filtersList.appendChild(filterEl);
+ }
+
+ filterEl.removeAttribute("style");
+
+ const currentPosition = change * LIST_ITEM_HEIGHT;
+ filterEl.startingY = e.pageY + currentPosition - delta;
+ },
+
+ _dragLabel: function (e) {
+ let dragging = this._dragging;
+
+ let input = dragging.input;
+
+ let multiplier = DEFAULT_VALUE_MULTIPLIER;
+
+ if (e.altKey) {
+ multiplier = SLOW_VALUE_MULTIPLIER;
+ } else if (e.shiftKey) {
+ multiplier = FAST_VALUE_MULTIPLIER;
+ }
+
+ dragging.lastX = e.pageX;
+ const delta = e.pageX - dragging.startX;
+ const startValue = parseFloat(input.value);
+ let value = startValue + delta * multiplier;
+
+ const filter = this.filters[dragging.index];
+ const [min, max] = this._definition(filter.name).range;
+ if (value < min) {
+ value = min;
+ } else if (value > max) {
+ value = max;
+ }
+
+ input.value = fixFloat(value);
+
+ dragging.startX = e.pageX;
+
+ this.updateValueAt(dragging.index, value);
+ },
+
+ _mouseUp: function () {
+ // Label-dragging is disabled on mouseup
+ this._dragging = null;
+ this.isDraggingLabel = false;
+
+ // Filter drag/drop needs more cleaning
+ if (!this.isReorderingFilter) {
+ return;
+ }
+ let filterEl = this.filtersList.querySelector(".dragging");
+
+ this.isReorderingFilter = false;
+ filterEl.classList.remove("dragging");
+ this.el.classList.remove("dragging");
+ filterEl.removeAttribute("style");
+
+ this.emit("updated", this.getCssValue());
+ this.render();
+ },
+
+ _presetClick: function (e) {
+ let el = e.target;
+ let preset = el.closest(".preset");
+ if (!preset) {
+ return;
+ }
+
+ let id = +preset.dataset.id;
+
+ this.getPresets().then(presets => {
+ if (el.classList.contains("remove-button")) {
+ // If the click happened on the remove button.
+ presets.splice(id, 1);
+ this.setPresets(presets).then(this.renderPresets,
+ ex => console.error(ex));
+ } else {
+ // Or if the click happened on a preset.
+ let p = presets[id];
+
+ this.setCssValue(p.value);
+ this.addPresetInput.value = p.name;
+ }
+ }, ex => console.error(ex));
+ },
+
+ _togglePresets: function () {
+ this.el.classList.toggle("show-presets");
+ this.emit("render");
+ },
+
+ _savePreset: function (e) {
+ e.preventDefault();
+
+ let name = this.addPresetInput.value;
+ let value = this.getCssValue();
+
+ if (!name || !value || SPECIAL_VALUES.has(value)) {
+ this.emit("preset-save-error");
+ return;
+ }
+
+ this.getPresets().then(presets => {
+ let index = presets.findIndex(preset => preset.name === name);
+
+ if (index > -1) {
+ presets[index].value = value;
+ } else {
+ presets.push({name, value});
+ }
+
+ this.setPresets(presets).then(this.renderPresets,
+ ex => console.error(ex));
+ }, ex => console.error(ex));
+ },
+
+ /**
+ * Workaround needed to reset the focus when using a HTML select inside a XUL panel.
+ * See Bug 1294366.
+ */
+ _resetFocus: function () {
+ this.filterSelect.ownerDocument.defaultView.focus();
+ },
+
+ /**
+ * Clears the list and renders filters, binding required events.
+ * There are some delegated events bound in _addEventListeners method
+ */
+ render: function () {
+ if (!this.filters.length) {
+ this.filtersList.innerHTML = `<p> ${L10N.getStr("emptyFilterList")} <br />
+ ${L10N.getStr("addUsingList")} </p>`;
+ this.emit("render");
+ return;
+ }
+
+ this.filtersList.innerHTML = "";
+
+ let base = this._filterItemMarkup;
+
+ for (let filter of this.filters) {
+ const def = this._definition(filter.name);
+
+ let el = base.cloneNode(true);
+
+ let [name, value] = el.children;
+ let label = name.children[1];
+ let [input, unitPreview] = value.children;
+
+ let min, max;
+ if (def.range) {
+ [min, max] = def.range;
+ }
+
+ label.textContent = filter.name;
+ input.value = filter.value;
+
+ switch (def.type) {
+ case "percentage":
+ case "angle":
+ case "length":
+ input.type = "number";
+ input.min = min;
+ if (max !== Infinity) {
+ input.max = max;
+ }
+ input.step = "0.1";
+ break;
+ case "string":
+ input.type = "text";
+ input.placeholder = def.placeholder;
+ break;
+ }
+
+ // use photoshop-style label-dragging
+ // and show filters' unit next to their <input>
+ if (def.type !== "string") {
+ unitPreview.textContent = filter.unit;
+
+ label.classList.add("devtools-draglabel");
+ label.title = L10N.getStr("labelDragTooltipText");
+ } else {
+ // string-type filters have no unit
+ unitPreview.remove();
+ }
+
+ this.filtersList.appendChild(el);
+ }
+
+ let lastInput =
+ this.filtersList.querySelector(".filter:last-of-type input");
+ if (lastInput) {
+ lastInput.focus();
+ if (lastInput.type === "text") {
+ // move cursor to end of input
+ const end = lastInput.value.length;
+ lastInput.setSelectionRange(end, end);
+ }
+ }
+
+ this.emit("render");
+ },
+
+ renderPresets: function () {
+ this.getPresets().then(presets => {
+ // getPresets is async and the widget may be destroyed in between.
+ if (!this.presetsList) {
+ return;
+ }
+
+ if (!presets || !presets.length) {
+ this.presetsList.innerHTML = `<p>${L10N.getStr("emptyPresetList")}</p>`;
+ this.emit("render");
+ return;
+ }
+ let base = this._presetItemMarkup;
+
+ this.presetsList.innerHTML = "";
+
+ for (let [index, preset] of presets.entries()) {
+ let el = base.cloneNode(true);
+
+ let [label, span] = el.children;
+
+ el.dataset.id = index;
+
+ label.textContent = preset.name;
+ span.textContent = preset.value;
+
+ this.presetsList.appendChild(el);
+ }
+
+ this.emit("render");
+ });
+ },
+
+ /**
+ * returns definition of a filter as defined in filterList
+ *
+ * @param {String} name
+ * filter name (e.g. blur)
+ * @return {Object}
+ * filter's definition
+ */
+ _definition: function (name) {
+ name = name.toLowerCase();
+ return filterList.find(a => a.name === name);
+ },
+
+ /**
+ * Parses the CSS value specified, updating widget's filters
+ *
+ * @param {String} cssValue
+ * css value to be parsed
+ */
+ setCssValue: function (cssValue) {
+ if (!cssValue) {
+ throw new Error("Missing CSS filter value in setCssValue");
+ }
+
+ this.filters = [];
+
+ if (SPECIAL_VALUES.has(cssValue)) {
+ this._specialValue = cssValue;
+ this.emit("updated", this.getCssValue());
+ this.render();
+ return;
+ }
+
+ for (let {name, value, quote} of tokenizeFilterValue(cssValue)) {
+ // If the specified value is invalid, replace it with the
+ // default.
+ if (name !== "url") {
+ if (!this._cssIsValid("filter", name + "(" + value + ")")) {
+ value = null;
+ }
+ }
+
+ this.add(name, value, quote, true);
+ }
+
+ this.emit("updated", this.getCssValue());
+ this.render();
+ },
+
+ /**
+ * Creates a new [name] filter record with value
+ *
+ * @param {String} name
+ * filter name (e.g. blur)
+ * @param {String} value
+ * value of the filter (e.g. 30px, 20%)
+ * If this is |null|, then a default value may be supplied.
+ * @param {String} quote
+ * For a url filter, the quoting style. This can be a
+ * single quote, a double quote, or empty.
+ * @return {Number}
+ * The index of the new filter in the current list of filters
+ * @param {Boolean}
+ * By default, adding a new filter emits an "updated" event, but if
+ * you're calling add in a loop and wait to emit a single event after
+ * the loop yourself, set this parameter to true.
+ */
+ add: function (name, value, quote, noEvent) {
+ const def = this._definition(name);
+ if (!def) {
+ return false;
+ }
+
+ if (value === null) {
+ // UNIT_MAPPING[string] is an empty string (falsy), so
+ // using || doesn't work here
+ const unitLabel = typeof UNIT_MAPPING[def.type] === "undefined" ?
+ UNIT_MAPPING[DEFAULT_FILTER_TYPE] :
+ UNIT_MAPPING[def.type];
+
+ // string-type filters have no default value but a placeholder instead
+ if (!unitLabel) {
+ value = "";
+ } else {
+ value = def.range[0] + unitLabel;
+ }
+
+ if (name === "url") {
+ // Default quote.
+ quote = "\"";
+ }
+ }
+
+ let unit = def.type === "string"
+ ? ""
+ : (/[a-zA-Z%]+/.exec(value) || [])[0];
+
+ if (def.type !== "string") {
+ value = parseFloat(value);
+
+ // You can omit percentage values' and use a value between 0..1
+ if (def.type === "percentage" && !unit) {
+ value = value * 100;
+ unit = "%";
+ }
+
+ const [min, max] = def.range;
+ if (value < min) {
+ value = min;
+ } else if (value > max) {
+ value = max;
+ }
+ }
+
+ const index = this.filters.push({value, unit, name, quote}) - 1;
+ if (!noEvent) {
+ this.emit("updated", this.getCssValue());
+ }
+
+ return index;
+ },
+
+ /**
+ * returns value + unit of the specified filter
+ *
+ * @param {Number} index
+ * filter index
+ * @return {String}
+ * css value of filter
+ */
+ getValueAt: function (index) {
+ let filter = this.filters[index];
+ if (!filter) {
+ return null;
+ }
+
+ // Just return the value+unit for non-url functions.
+ if (filter.name !== "url") {
+ return filter.value + filter.unit;
+ }
+
+ // url values need to be quoted and escaped.
+ if (filter.quote === "'") {
+ return "'" + filter.value.replace(/\'/g, "\\'") + "'";
+ } else if (filter.quote === "\"") {
+ return "\"" + filter.value.replace(/\"/g, "\\\"") + "\"";
+ }
+
+ // Unquoted. This approach might change the original input -- for
+ // example the original might be over-quoted. But, this is
+ // correct and probably good enough.
+ return filter.value.replace(/[\\ \t()"']/g, "\\$&");
+ },
+
+ removeAt: function (index) {
+ if (!this.filters[index]) {
+ return;
+ }
+
+ this.filters.splice(index, 1);
+ this.emit("updated", this.getCssValue());
+ this.render();
+ },
+
+ /**
+ * Generates CSS filter value for filters of the widget
+ *
+ * @return {String}
+ * css value of filters
+ */
+ getCssValue: function () {
+ return this.filters.map((filter, i) => {
+ return `${filter.name}(${this.getValueAt(i)})`;
+ }).join(" ") || this._specialValue || "none";
+ },
+
+ /**
+ * Updates specified filter's value
+ *
+ * @param {Number} index
+ * The index of the filter in the current list of filters
+ * @param {number/string} value
+ * value to set, string for string-typed filters
+ * number for the rest (unit automatically determined)
+ */
+ updateValueAt: function (index, value) {
+ let filter = this.filters[index];
+ if (!filter) {
+ return;
+ }
+
+ const def = this._definition(filter.name);
+
+ if (def.type !== "string") {
+ const [min, max] = def.range;
+ if (value < min) {
+ value = min;
+ } else if (value > max) {
+ value = max;
+ }
+ }
+
+ filter.value = filter.unit ? fixFloat(value, true) : value;
+
+ this.emit("updated", this.getCssValue());
+ },
+
+ getPresets: function () {
+ return asyncStorage.getItem("cssFilterPresets").then(presets => {
+ if (!presets) {
+ return [];
+ }
+
+ return presets;
+ }, e => console.error(e));
+ },
+
+ setPresets: function (presets) {
+ return asyncStorage.setItem("cssFilterPresets", presets)
+ .catch(e => console.error(e));
+ }
+};
+
+// Fixes JavaScript's float precision
+function fixFloat(a, number) {
+ let fixed = parseFloat(a).toFixed(1);
+ return number ? parseFloat(fixed) : fixed;
+}
+
+/**
+ * Used to swap two filters' indexes
+ * after drag/drop re-ordering
+ *
+ * @param {Array} array
+ * the array to swap elements of
+ * @param {Number} a
+ * index of first element
+ * @param {Number} b
+ * index of second element
+ */
+function swapArrayIndices(array, a, b) {
+ array[a] = array.splice(b, 1, array[a])[0];
+}
+
+/**
+ * Tokenizes a CSS Filter value and returns an array of {name, value} pairs.
+ *
+ * @param {String} css CSS Filter value to be parsed
+ * @return {Array} An array of {name, value} pairs
+ */
+function tokenizeFilterValue(css) {
+ let filters = [];
+ let depth = 0;
+
+ if (SPECIAL_VALUES.has(css)) {
+ return filters;
+ }
+
+ let state = "initial";
+ let name;
+ let contents;
+ for (let token of cssTokenizer(css)) {
+ switch (state) {
+ case "initial":
+ if (token.tokenType === "function") {
+ name = token.text;
+ contents = "";
+ state = "function";
+ depth = 1;
+ } else if (token.tokenType === "url" || token.tokenType === "bad_url") {
+ // Extract the quoting style from the url.
+ let originalText = css.substring(token.startOffset, token.endOffset);
+ let [, quote] = /^url\([ \t\r\n\f]*(["']?)/i.exec(originalText);
+
+ filters.push({name: "url", value: token.text.trim(), quote: quote});
+ // Leave state as "initial" because the URL token includes
+ // the trailing close paren.
+ }
+ break;
+
+ case "function":
+ if (token.tokenType === "symbol" && token.text === ")") {
+ --depth;
+ if (depth === 0) {
+ filters.push({name: name, value: contents.trim()});
+ state = "initial";
+ break;
+ }
+ }
+ contents += css.substring(token.startOffset, token.endOffset);
+ if (token.tokenType === "function" ||
+ (token.tokenType === "symbol" && token.text === "(")) {
+ ++depth;
+ }
+ break;
+ }
+ }
+
+ return filters;
+}
+
+/**
+ * Finds neighbour number characters of an index in a string
+ * the numbers may be floats (containing dots)
+ * It's assumed that the value given to this function is a valid number
+ *
+ * @param {String} string
+ * The string containing numbers
+ * @param {Number} index
+ * The index to look for neighbours for
+ * @return {Object}
+ * returns null if no number is found
+ * value: The number found
+ * start: The number's starting index
+ * end: The number's ending index
+ */
+function getNeighbourNumber(string, index) {
+ if (!/\d/.test(string)) {
+ return null;
+ }
+
+ let left = /-?[0-9.]*$/.exec(string.slice(0, index));
+ let right = /-?[0-9.]*/.exec(string.slice(index));
+
+ left = left ? left[0] : "";
+ right = right ? right[0] : "";
+
+ if (!right && !left) {
+ return null;
+ }
+
+ return {
+ value: fixFloat(left + right, true),
+ start: index - left.length,
+ end: index + right.length
+ };
+}
diff --git a/devtools/client/shared/widgets/FlameGraph.js b/devtools/client/shared/widgets/FlameGraph.js
new file mode 100644
index 000000000..e9d25b345
--- /dev/null
+++ b/devtools/client/shared/widgets/FlameGraph.js
@@ -0,0 +1,1462 @@
+/* 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 { Task } = require("devtools/shared/task");
+const { ViewHelpers, setNamedTimeout } = require("devtools/client/shared/widgets/view-helpers");
+const { ELLIPSIS } = require("devtools/shared/l10n");
+
+loader.lazyRequireGetter(this, "defer", "devtools/shared/defer");
+loader.lazyRequireGetter(this, "EventEmitter",
+ "devtools/shared/event-emitter");
+
+loader.lazyRequireGetter(this, "getColor",
+ "devtools/client/shared/theme", true);
+
+loader.lazyRequireGetter(this, "CATEGORY_MAPPINGS",
+ "devtools/client/performance/modules/categories", true);
+loader.lazyRequireGetter(this, "FrameUtils",
+ "devtools/client/performance/modules/logic/frame-utils");
+loader.lazyRequireGetter(this, "demangle",
+ "devtools/client/shared/demangle");
+
+loader.lazyRequireGetter(this, "AbstractCanvasGraph",
+ "devtools/client/shared/widgets/Graphs", true);
+loader.lazyRequireGetter(this, "GraphArea",
+ "devtools/client/shared/widgets/Graphs", true);
+loader.lazyRequireGetter(this, "GraphAreaDragger",
+ "devtools/client/shared/widgets/Graphs", true);
+
+const GRAPH_SRC = "chrome://devtools/content/shared/widgets/graphs-frame.xhtml";
+
+// ms
+const GRAPH_RESIZE_EVENTS_DRAIN = 100;
+
+const GRAPH_WHEEL_ZOOM_SENSITIVITY = 0.00035;
+const GRAPH_WHEEL_SCROLL_SENSITIVITY = 0.5;
+const GRAPH_KEYBOARD_ZOOM_SENSITIVITY = 20;
+const GRAPH_KEYBOARD_PAN_SENSITIVITY = 20;
+const GRAPH_KEYBOARD_ACCELERATION = 1.05;
+const GRAPH_KEYBOARD_TRANSLATION_MAX = 150;
+
+// ms
+const GRAPH_MIN_SELECTION_WIDTH = 0.001;
+
+// px
+const GRAPH_HORIZONTAL_PAN_THRESHOLD = 10;
+const GRAPH_VERTICAL_PAN_THRESHOLD = 30;
+
+const FIND_OPTIMAL_TICK_INTERVAL_MAX_ITERS = 100;
+
+// ms
+const TIMELINE_TICKS_MULTIPLE = 5;
+// px
+const TIMELINE_TICKS_SPACING_MIN = 75;
+
+// px
+const OVERVIEW_HEADER_HEIGHT = 16;
+const OVERVIEW_HEADER_TEXT_FONT_SIZE = 9;
+const OVERVIEW_HEADER_TEXT_FONT_FAMILY = "sans-serif";
+// px
+const OVERVIEW_HEADER_TEXT_PADDING_LEFT = 6;
+const OVERVIEW_HEADER_TEXT_PADDING_TOP = 5;
+const OVERVIEW_HEADER_TIMELINE_STROKE_COLOR = "rgba(128, 128, 128, 0.5)";
+
+// px
+const FLAME_GRAPH_BLOCK_HEIGHT = 15;
+const FLAME_GRAPH_BLOCK_BORDER = 1;
+const FLAME_GRAPH_BLOCK_TEXT_FONT_SIZE = 10;
+const FLAME_GRAPH_BLOCK_TEXT_FONT_FAMILY = "message-box, Helvetica Neue," +
+ "Helvetica, sans-serif";
+// px
+const FLAME_GRAPH_BLOCK_TEXT_PADDING_TOP = 0;
+const FLAME_GRAPH_BLOCK_TEXT_PADDING_LEFT = 3;
+const FLAME_GRAPH_BLOCK_TEXT_PADDING_RIGHT = 3;
+
+// Large enough number for a diverse pallette.
+const PALLETTE_SIZE = 20;
+const PALLETTE_HUE_OFFSET = Math.random() * 90;
+const PALLETTE_HUE_RANGE = 270;
+const PALLETTE_SATURATION = 100;
+const PALLETTE_BRIGHTNESS = 55;
+const PALLETTE_OPACITY = 0.35;
+
+const COLOR_PALLETTE = Array.from(Array(PALLETTE_SIZE)).map((_, i) => "hsla" +
+ "(" +
+ ((PALLETTE_HUE_OFFSET + (i / PALLETTE_SIZE * PALLETTE_HUE_RANGE)) | 0 % 360) +
+ "," + PALLETTE_SATURATION + "%" +
+ "," + PALLETTE_BRIGHTNESS + "%" +
+ "," + PALLETTE_OPACITY +
+ ")"
+);
+
+/**
+ * A flamegraph visualization. This implementation is responsable only with
+ * drawing the graph, using a data source consisting of rectangles and
+ * their corresponding widths.
+ *
+ * Example usage:
+ * let graph = new FlameGraph(node);
+ * graph.once("ready", () => {
+ * let data = FlameGraphUtils.createFlameGraphDataFromThread(thread);
+ * let bounds = { startTime, endTime };
+ * graph.setData({ data, bounds });
+ * });
+ *
+ * Data source format:
+ * [
+ * {
+ * color: "string",
+ * blocks: [
+ * {
+ * x: number,
+ * y: number,
+ * width: number,
+ * height: number,
+ * text: "string"
+ * },
+ * ...
+ * ]
+ * },
+ * {
+ * color: "string",
+ * blocks: [...]
+ * },
+ * ...
+ * {
+ * color: "string",
+ * blocks: [...]
+ * }
+ * ]
+ *
+ * Use `FlameGraphUtils` to convert profiler data (or any other data source)
+ * into a drawable format.
+ *
+ * @param nsIDOMNode parent
+ * The parent node holding the graph.
+ * @param number sharpness [optional]
+ * Defaults to the current device pixel ratio.
+ */
+function FlameGraph(parent, sharpness) {
+ EventEmitter.decorate(this);
+
+ this._parent = parent;
+ this._ready = defer();
+
+ this.setTheme();
+
+ AbstractCanvasGraph.createIframe(GRAPH_SRC, parent, iframe => {
+ this._iframe = iframe;
+ this._window = iframe.contentWindow;
+ this._document = iframe.contentDocument;
+ this._pixelRatio = sharpness || this._window.devicePixelRatio;
+
+ let container =
+ this._container = this._document.getElementById("graph-container");
+ container.className = "flame-graph-widget-container graph-widget-container";
+
+ let canvas = this._canvas = this._document.getElementById("graph-canvas");
+ canvas.className = "flame-graph-widget-canvas graph-widget-canvas";
+
+ let bounds = parent.getBoundingClientRect();
+ bounds.width = this.fixedWidth || bounds.width;
+ bounds.height = this.fixedHeight || bounds.height;
+ iframe.setAttribute("width", bounds.width);
+ iframe.setAttribute("height", bounds.height);
+
+ this._width = canvas.width = bounds.width * this._pixelRatio;
+ this._height = canvas.height = bounds.height * this._pixelRatio;
+ this._ctx = canvas.getContext("2d");
+
+ this._bounds = new GraphArea();
+ this._selection = new GraphArea();
+ this._selectionDragger = new GraphAreaDragger();
+ this._verticalOffset = 0;
+ this._verticalOffsetDragger = new GraphAreaDragger(0);
+ this._keyboardZoomAccelerationFactor = 1;
+ this._keyboardPanAccelerationFactor = 1;
+
+ this._userInputStack = 0;
+ this._keysPressed = [];
+
+ // Calculating text widths is necessary to trim the text inside the blocks
+ // while the scaling changes (e.g. via scrolling). This is very expensive,
+ // so maintain a cache of string contents to text widths.
+ this._textWidthsCache = {};
+
+ let fontSize = FLAME_GRAPH_BLOCK_TEXT_FONT_SIZE * this._pixelRatio;
+ let fontFamily = FLAME_GRAPH_BLOCK_TEXT_FONT_FAMILY;
+ this._ctx.font = fontSize + "px " + fontFamily;
+ this._averageCharWidth = this._calcAverageCharWidth();
+ this._overflowCharWidth = this._getTextWidth(this.overflowChar);
+
+ this._onAnimationFrame = this._onAnimationFrame.bind(this);
+ this._onKeyDown = this._onKeyDown.bind(this);
+ this._onKeyUp = this._onKeyUp.bind(this);
+ this._onKeyPress = this._onKeyPress.bind(this);
+ this._onMouseMove = this._onMouseMove.bind(this);
+ this._onMouseDown = this._onMouseDown.bind(this);
+ this._onMouseUp = this._onMouseUp.bind(this);
+ this._onMouseWheel = this._onMouseWheel.bind(this);
+ this._onResize = this._onResize.bind(this);
+ this.refresh = this.refresh.bind(this);
+
+ this._window.addEventListener("keydown", this._onKeyDown);
+ this._window.addEventListener("keyup", this._onKeyUp);
+ this._window.addEventListener("keypress", this._onKeyPress);
+ this._window.addEventListener("mousemove", this._onMouseMove);
+ this._window.addEventListener("mousedown", this._onMouseDown);
+ this._window.addEventListener("mouseup", this._onMouseUp);
+ this._window.addEventListener("MozMousePixelScroll", this._onMouseWheel);
+
+ let ownerWindow = this._parent.ownerDocument.defaultView;
+ ownerWindow.addEventListener("resize", this._onResize);
+
+ this._animationId =
+ this._window.requestAnimationFrame(this._onAnimationFrame);
+
+ this._ready.resolve(this);
+ this.emit("ready", this);
+ });
+}
+
+FlameGraph.prototype = {
+ /**
+ * Read-only width and height of the canvas.
+ * @return number
+ */
+ get width() {
+ return this._width;
+ },
+ get height() {
+ return this._height;
+ },
+
+ /**
+ * Returns a promise resolved once this graph is ready to receive data.
+ */
+ ready: function () {
+ return this._ready.promise;
+ },
+
+ /**
+ * Destroys this graph.
+ */
+ destroy: Task.async(function* () {
+ yield this.ready();
+
+ this._window.removeEventListener("keydown", this._onKeyDown);
+ this._window.removeEventListener("keyup", this._onKeyUp);
+ this._window.removeEventListener("keypress", this._onKeyPress);
+ this._window.removeEventListener("mousemove", this._onMouseMove);
+ this._window.removeEventListener("mousedown", this._onMouseDown);
+ this._window.removeEventListener("mouseup", this._onMouseUp);
+ this._window.removeEventListener("MozMousePixelScroll", this._onMouseWheel);
+
+ let ownerWindow = this._parent.ownerDocument.defaultView;
+ if (ownerWindow) {
+ ownerWindow.removeEventListener("resize", this._onResize);
+ }
+
+ this._window.cancelAnimationFrame(this._animationId);
+ this._iframe.remove();
+
+ this._bounds = null;
+ this._selection = null;
+ this._selectionDragger = null;
+ this._verticalOffset = null;
+ this._verticalOffsetDragger = null;
+ this._keyboardZoomAccelerationFactor = null;
+ this._keyboardPanAccelerationFactor = null;
+ this._textWidthsCache = null;
+
+ this._data = null;
+
+ this.emit("destroyed");
+ }),
+
+ /**
+ * Makes sure the canvas graph is of the specified width or height, and
+ * doesn't flex to fit all the available space.
+ */
+ fixedWidth: null,
+ fixedHeight: null,
+
+ /**
+ * How much preliminar drag is necessary to determine the panning direction.
+ */
+ horizontalPanThreshold: GRAPH_HORIZONTAL_PAN_THRESHOLD,
+ verticalPanThreshold: GRAPH_VERTICAL_PAN_THRESHOLD,
+
+ /**
+ * The units used in the overhead ticks. Could be "ms", for example.
+ * Overwrite this with your own localized format.
+ */
+ timelineTickUnits: "",
+
+ /**
+ * Character used when a block's text is overflowing.
+ * Defaults to an ellipsis.
+ */
+ overflowChar: ELLIPSIS,
+
+ /**
+ * Sets the data source for this graph.
+ *
+ * @param object data
+ * An object containing the following properties:
+ * - data: the data source; see the constructor for more info
+ * - bounds: the minimum/maximum { start, end }, in ms or px
+ * - visible: optional, the shown { start, end }, in ms or px
+ */
+ setData: function ({ data, bounds, visible }) {
+ this._data = data;
+ this.setOuterBounds(bounds);
+ this.setViewRange(visible || bounds);
+ },
+
+ /**
+ * Same as `setData`, but waits for this graph to finish initializing first.
+ *
+ * @param object data
+ * The data source. See the constructor for more information.
+ * @return promise
+ * A promise resolved once the data is set.
+ */
+ setDataWhenReady: Task.async(function* (data) {
+ yield this.ready();
+ this.setData(data);
+ }),
+
+ /**
+ * Gets whether or not this graph has a data source.
+ * @return boolean
+ */
+ hasData: function () {
+ return !!this._data;
+ },
+
+ /**
+ * Sets the maximum selection (i.e. the 'graph bounds').
+ * @param object { start, end }
+ */
+ setOuterBounds: function ({ startTime, endTime }) {
+ this._bounds.start = startTime * this._pixelRatio;
+ this._bounds.end = endTime * this._pixelRatio;
+ this._shouldRedraw = true;
+ },
+
+ /**
+ * Sets the selection and vertical offset (i.e. the 'view range').
+ * @return number
+ */
+ setViewRange: function ({ startTime, endTime }, verticalOffset = 0) {
+ this._selection.start = startTime * this._pixelRatio;
+ this._selection.end = endTime * this._pixelRatio;
+ this._verticalOffset = verticalOffset * this._pixelRatio;
+ this._shouldRedraw = true;
+ },
+
+ /**
+ * Gets the maximum selection (i.e. the 'graph bounds').
+ * @return number
+ */
+ getOuterBounds: function () {
+ return {
+ startTime: this._bounds.start / this._pixelRatio,
+ endTime: this._bounds.end / this._pixelRatio
+ };
+ },
+
+ /**
+ * Gets the current selection and vertical offset (i.e. the 'view range').
+ * @return number
+ */
+ getViewRange: function () {
+ return {
+ startTime: this._selection.start / this._pixelRatio,
+ endTime: this._selection.end / this._pixelRatio,
+ verticalOffset: this._verticalOffset / this._pixelRatio
+ };
+ },
+
+ /**
+ * Focuses this graph's iframe window.
+ */
+ focus: function () {
+ this._window.focus();
+ },
+
+ /**
+ * Updates this graph to reflect the new dimensions of the parent node.
+ *
+ * @param boolean options.force
+ * Force redraw everything.
+ */
+ refresh: function (options = {}) {
+ let bounds = this._parent.getBoundingClientRect();
+ let newWidth = this.fixedWidth || bounds.width;
+ let newHeight = this.fixedHeight || bounds.height;
+
+ // Prevent redrawing everything if the graph's width & height won't change,
+ // except if force=true.
+ if (!options.force &&
+ this._width == newWidth * this._pixelRatio &&
+ this._height == newHeight * this._pixelRatio) {
+ this.emit("refresh-cancelled");
+ return;
+ }
+
+ bounds.width = newWidth;
+ bounds.height = newHeight;
+ this._iframe.setAttribute("width", bounds.width);
+ this._iframe.setAttribute("height", bounds.height);
+ this._width = this._canvas.width = bounds.width * this._pixelRatio;
+ this._height = this._canvas.height = bounds.height * this._pixelRatio;
+
+ this._shouldRedraw = true;
+ this.emit("refresh");
+ },
+
+ /**
+ * Sets the theme via `theme` to either "light" or "dark",
+ * and updates the internal styling to match. Requires a redraw
+ * to see the effects.
+ */
+ setTheme: function (theme) {
+ theme = theme || "light";
+ this.overviewHeaderBackgroundColor = getColor("body-background", theme);
+ this.overviewHeaderTextColor = getColor("body-color", theme);
+ // Hard to get a color that is readable across both themes for the text
+ // on the flames
+ this.blockTextColor = getColor(theme === "dark" ? "selection-color"
+ : "body-color", theme);
+ },
+
+ /**
+ * The contents of this graph are redrawn only when something changed,
+ * like the data source, or the selection bounds etc. This flag tracks
+ * if the rendering is "dirty" and needs to be refreshed.
+ */
+ _shouldRedraw: false,
+
+ /**
+ * Animation frame callback, invoked on each tick of the refresh driver.
+ */
+ _onAnimationFrame: function () {
+ this._animationId =
+ this._window.requestAnimationFrame(this._onAnimationFrame);
+ this._drawWidget();
+ },
+
+ /**
+ * Redraws the widget when necessary. The actual graph is not refreshed
+ * every time this function is called, only the cliphead, selection etc.
+ */
+ _drawWidget: function () {
+ if (!this._shouldRedraw) {
+ return;
+ }
+
+ // Unlike mouse events which are updated as needed in their own respective
+ // handlers, keyboard events are granular and non-continuous (not even
+ // "keydown", which is fired with a low frequency). Therefore, to maintain
+ // animation smoothness, update anything that's controllable via the
+ // keyboard here, in the animation loop, before any actual drawing.
+ this._keyboardUpdateLoop();
+
+ let ctx = this._ctx;
+ let canvasWidth = this._width;
+ let canvasHeight = this._height;
+ ctx.clearRect(0, 0, canvasWidth, canvasHeight);
+
+ let selection = this._selection;
+ let selectionWidth = selection.end - selection.start;
+ let selectionScale = canvasWidth / selectionWidth;
+ this._drawTicks(selection.start, selectionScale);
+ this._drawPyramid(this._data, this._verticalOffset,
+ selection.start, selectionScale);
+ this._drawHeader(selection.start, selectionScale);
+
+ // If the user isn't doing anything anymore, it's safe to stop drawing.
+ // XXX: This doesn't handle cases where we should still be drawing even
+ // if any input stops (e.g. smooth panning transitions after the user
+ // finishes input). We don't care about that right now.
+ if (this._userInputStack == 0) {
+ this._shouldRedraw = false;
+ return;
+ }
+ if (this._userInputStack < 0) {
+ throw new Error("The user went back in time from a pyramid.");
+ }
+ },
+
+ /**
+ * Performs any necessary changes to the graph's state based on the
+ * user's input on a keyboard.
+ */
+ _keyboardUpdateLoop: function () {
+ const KEY_CODE_UP = 38;
+ const KEY_CODE_DOWN = 40;
+ const KEY_CODE_LEFT = 37;
+ const KEY_CODE_RIGHT = 39;
+ const KEY_CODE_W = 87;
+ const KEY_CODE_A = 65;
+ const KEY_CODE_S = 83;
+ const KEY_CODE_D = 68;
+
+ let canvasWidth = this._width;
+ let pressed = this._keysPressed;
+
+ let selection = this._selection;
+ let selectionWidth = selection.end - selection.start;
+ let selectionScale = canvasWidth / selectionWidth;
+
+ let translation = [0, 0];
+ let isZooming = false;
+ let isPanning = false;
+
+ if (pressed[KEY_CODE_UP] || pressed[KEY_CODE_W]) {
+ translation[0] += GRAPH_KEYBOARD_ZOOM_SENSITIVITY / selectionScale;
+ translation[1] -= GRAPH_KEYBOARD_ZOOM_SENSITIVITY / selectionScale;
+ isZooming = true;
+ }
+ if (pressed[KEY_CODE_DOWN] || pressed[KEY_CODE_S]) {
+ translation[0] -= GRAPH_KEYBOARD_ZOOM_SENSITIVITY / selectionScale;
+ translation[1] += GRAPH_KEYBOARD_ZOOM_SENSITIVITY / selectionScale;
+ isZooming = true;
+ }
+ if (pressed[KEY_CODE_LEFT] || pressed[KEY_CODE_A]) {
+ translation[0] -= GRAPH_KEYBOARD_PAN_SENSITIVITY / selectionScale;
+ translation[1] -= GRAPH_KEYBOARD_PAN_SENSITIVITY / selectionScale;
+ isPanning = true;
+ }
+ if (pressed[KEY_CODE_RIGHT] || pressed[KEY_CODE_D]) {
+ translation[0] += GRAPH_KEYBOARD_PAN_SENSITIVITY / selectionScale;
+ translation[1] += GRAPH_KEYBOARD_PAN_SENSITIVITY / selectionScale;
+ isPanning = true;
+ }
+
+ if (isPanning) {
+ // Accelerate the left/right selection panning continuously
+ // while the pan keys are pressed.
+ this._keyboardPanAccelerationFactor *= GRAPH_KEYBOARD_ACCELERATION;
+ translation[0] *= this._keyboardPanAccelerationFactor;
+ translation[1] *= this._keyboardPanAccelerationFactor;
+ } else {
+ this._keyboardPanAccelerationFactor = 1;
+ }
+
+ if (isZooming) {
+ // Accelerate the in/out selection zooming continuously
+ // while the zoom keys are pressed.
+ this._keyboardZoomAccelerationFactor *= GRAPH_KEYBOARD_ACCELERATION;
+ translation[0] *= this._keyboardZoomAccelerationFactor;
+ translation[1] *= this._keyboardZoomAccelerationFactor;
+ } else {
+ this._keyboardZoomAccelerationFactor = 1;
+ }
+
+ if (translation[0] != 0 || translation[1] != 0) {
+ // Make sure the panning translation speed doesn't end up
+ // being too high.
+ let maxTranslation = GRAPH_KEYBOARD_TRANSLATION_MAX / selectionScale;
+ if (Math.abs(translation[0]) > maxTranslation) {
+ translation[0] = Math.sign(translation[0]) * maxTranslation;
+ }
+ if (Math.abs(translation[1]) > maxTranslation) {
+ translation[1] = Math.sign(translation[1]) * maxTranslation;
+ }
+ this._selection.start += translation[0];
+ this._selection.end += translation[1];
+ this._normalizeSelectionBounds();
+ this.emit("selecting");
+ }
+ },
+
+ /**
+ * Draws the overhead header, with time markers and ticks in this graph.
+ *
+ * @param number dataOffset, dataScale
+ * Offsets and scales the data source by the specified amount.
+ * This is used for scrolling the visualization.
+ */
+ _drawHeader: function (dataOffset, dataScale) {
+ let ctx = this._ctx;
+ let canvasWidth = this._width;
+ let headerHeight = OVERVIEW_HEADER_HEIGHT * this._pixelRatio;
+
+ ctx.fillStyle = this.overviewHeaderBackgroundColor;
+ ctx.fillRect(0, 0, canvasWidth, headerHeight);
+
+ this._drawTicks(dataOffset, dataScale, {
+ from: 0,
+ to: headerHeight,
+ renderText: true
+ });
+ },
+
+ /**
+ * Draws the overhead ticks in this graph in the flame graph area.
+ *
+ * @param number dataOffset, dataScale, from, to, renderText
+ * Offsets and scales the data source by the specified amount.
+ * from and to determine the Y position of how far the stroke
+ * should be drawn.
+ * This is used when scrolling the visualization.
+ */
+ _drawTicks: function (dataOffset, dataScale, options) {
+ let { from, to, renderText } = options || {};
+ let ctx = this._ctx;
+ let canvasWidth = this._width;
+ let canvasHeight = this._height;
+ let scaledOffset = dataOffset * dataScale;
+
+ let fontSize = OVERVIEW_HEADER_TEXT_FONT_SIZE * this._pixelRatio;
+ let fontFamily = OVERVIEW_HEADER_TEXT_FONT_FAMILY;
+ let textPaddingLeft = OVERVIEW_HEADER_TEXT_PADDING_LEFT * this._pixelRatio;
+ let textPaddingTop = OVERVIEW_HEADER_TEXT_PADDING_TOP * this._pixelRatio;
+ let tickInterval = this._findOptimalTickInterval(dataScale);
+
+ ctx.textBaseline = "top";
+ ctx.font = fontSize + "px " + fontFamily;
+ ctx.fillStyle = this.overviewHeaderTextColor;
+ ctx.strokeStyle = OVERVIEW_HEADER_TIMELINE_STROKE_COLOR;
+ ctx.beginPath();
+
+ for (let x = -scaledOffset % tickInterval; x < canvasWidth;
+ x += tickInterval) {
+ let lineLeft = x;
+ let textLeft = lineLeft + textPaddingLeft;
+ let time = Math.round((x / dataScale + dataOffset) / this._pixelRatio);
+ let label = time + " " + this.timelineTickUnits;
+ if (renderText) {
+ ctx.fillText(label, textLeft, textPaddingTop);
+ }
+ ctx.moveTo(lineLeft, from || 0);
+ ctx.lineTo(lineLeft, to || canvasHeight);
+ }
+
+ ctx.stroke();
+ },
+
+ /**
+ * Draws the blocks and text in this graph.
+ *
+ * @param object dataSource
+ * The data source. See the constructor for more information.
+ * @param number verticalOffset
+ * Offsets the drawing vertically by the specified amount.
+ * @param number dataOffset, dataScale
+ * Offsets and scales the data source by the specified amount.
+ * This is used for scrolling the visualization.
+ */
+ _drawPyramid: function (dataSource, verticalOffset, dataOffset, dataScale) {
+ let ctx = this._ctx;
+
+ let fontSize = FLAME_GRAPH_BLOCK_TEXT_FONT_SIZE * this._pixelRatio;
+ let fontFamily = FLAME_GRAPH_BLOCK_TEXT_FONT_FAMILY;
+ let visibleBlocksInfo = this._drawPyramidFill(dataSource, verticalOffset,
+ dataOffset, dataScale);
+
+ ctx.textBaseline = "middle";
+ ctx.font = fontSize + "px " + fontFamily;
+ ctx.fillStyle = this.blockTextColor;
+
+ this._drawPyramidText(visibleBlocksInfo, verticalOffset,
+ dataOffset, dataScale);
+ },
+
+ /**
+ * Fills all block inside this graph's pyramid.
+ * @see FlameGraph.prototype._drawPyramid
+ */
+ _drawPyramidFill: function (dataSource, verticalOffset, dataOffset,
+ dataScale) {
+ let visibleBlocksInfoStore = [];
+ let minVisibleBlockWidth = this._overflowCharWidth;
+
+ for (let { color, blocks } of dataSource) {
+ this._drawBlocksFill(
+ color, blocks, verticalOffset, dataOffset, dataScale,
+ visibleBlocksInfoStore, minVisibleBlockWidth);
+ }
+
+ return visibleBlocksInfoStore;
+ },
+
+ /**
+ * Adds the text for all block inside this graph's pyramid.
+ * @see FlameGraph.prototype._drawPyramid
+ */
+ _drawPyramidText: function (blocksInfo, verticalOffset, dataOffset,
+ dataScale) {
+ for (let { block, rect } of blocksInfo) {
+ this._drawBlockText(block, rect, verticalOffset, dataOffset, dataScale);
+ }
+ },
+
+ /**
+ * Fills a group of blocks sharing the same style.
+ *
+ * @param string color
+ * The color used as the block's background.
+ * @param array blocks
+ * A list of { x, y, width, height } objects visually representing
+ * all the blocks sharing this particular style.
+ * @param number verticalOffset
+ * Offsets the drawing vertically by the specified amount.
+ * @param number dataOffset, dataScale
+ * Offsets and scales the data source by the specified amount.
+ * This is used for scrolling the visualization.
+ * @param array visibleBlocksInfoStore
+ * An array to store all the visible blocks into, along with the
+ * final baked coordinates and dimensions, after drawing them.
+ * The provided array will be populated.
+ * @param number minVisibleBlockWidth
+ * The minimum width of the blocks that will be added into
+ * the `visibleBlocksInfoStore`.
+ */
+ _drawBlocksFill: function (
+ color, blocks, verticalOffset, dataOffset, dataScale,
+ visibleBlocksInfoStore, minVisibleBlockWidth) {
+ let ctx = this._ctx;
+ let canvasWidth = this._width;
+ let canvasHeight = this._height;
+ let scaledOffset = dataOffset * dataScale;
+
+ ctx.fillStyle = color;
+ ctx.beginPath();
+
+ for (let block of blocks) {
+ let { x, y, width, height } = block;
+ let rectLeft = x * this._pixelRatio * dataScale - scaledOffset;
+ let rectTop = (y - verticalOffset + OVERVIEW_HEADER_HEIGHT)
+ * this._pixelRatio;
+ let rectWidth = width * this._pixelRatio * dataScale;
+ let rectHeight = height * this._pixelRatio;
+
+ // Too far respectively right/left/bottom/top
+ if (rectLeft > canvasWidth ||
+ rectLeft < -rectWidth ||
+ rectTop > canvasHeight ||
+ rectTop < -rectHeight) {
+ continue;
+ }
+
+ // Clamp the blocks position to start at 0. Avoid negative X coords,
+ // to properly place the text inside the blocks.
+ if (rectLeft < 0) {
+ rectWidth += rectLeft;
+ rectLeft = 0;
+ }
+
+ // Avoid drawing blocks that are too narrow.
+ if (rectWidth <= FLAME_GRAPH_BLOCK_BORDER ||
+ rectHeight <= FLAME_GRAPH_BLOCK_BORDER) {
+ continue;
+ }
+
+ ctx.rect(
+ rectLeft, rectTop,
+ rectWidth - FLAME_GRAPH_BLOCK_BORDER,
+ rectHeight - FLAME_GRAPH_BLOCK_BORDER);
+
+ // Populate the visible blocks store with this block if the width
+ // is longer than a given threshold.
+ if (rectWidth > minVisibleBlockWidth) {
+ visibleBlocksInfoStore.push({
+ block: block,
+ rect: { rectLeft, rectTop, rectWidth, rectHeight }
+ });
+ }
+ }
+
+ ctx.fill();
+ },
+
+ /**
+ * Adds text for a single block.
+ *
+ * @param object block
+ * A single { x, y, width, height, text } object visually representing
+ * the block containing the text.
+ * @param object rect
+ * A single { rectLeft, rectTop, rectWidth, rectHeight } object
+ * representing the final baked coordinates of the drawn rectangle.
+ * Think of them as screen-space values, vs. object-space values. These
+ * differ from the scalars in `block` when the graph is scaled/panned.
+ * @param number verticalOffset
+ * Offsets the drawing vertically by the specified amount.
+ * @param number dataOffset, dataScale
+ * Offsets and scales the data source by the specified amount.
+ * This is used for scrolling the visualization.
+ */
+ _drawBlockText: function (block, rect, verticalOffset, dataOffset,
+ dataScale) {
+ let ctx = this._ctx;
+
+ let { text } = block;
+ let { rectLeft, rectTop, rectWidth, rectHeight } = rect;
+
+ let paddingTop = FLAME_GRAPH_BLOCK_TEXT_PADDING_TOP * this._pixelRatio;
+ let paddingLeft = FLAME_GRAPH_BLOCK_TEXT_PADDING_LEFT * this._pixelRatio;
+ let paddingRight = FLAME_GRAPH_BLOCK_TEXT_PADDING_RIGHT * this._pixelRatio;
+ let totalHorizontalPadding = paddingLeft + paddingRight;
+
+ // Clamp the blocks position to start at 0. Avoid negative X coords,
+ // to properly place the text inside the blocks.
+ if (rectLeft < 0) {
+ rectWidth += rectLeft;
+ rectLeft = 0;
+ }
+
+ let textLeft = rectLeft + paddingLeft;
+ let textTop = rectTop + rectHeight / 2 + paddingTop;
+ let textAvailableWidth = rectWidth - totalHorizontalPadding;
+
+ // Massage the text to fit inside a given width. This clamps the string
+ // at the end to avoid overflowing.
+ let fittedText = this._getFittedText(text, textAvailableWidth);
+ if (fittedText.length < 1) {
+ return;
+ }
+
+ ctx.fillText(fittedText, textLeft, textTop);
+ },
+
+ /**
+ * Calculating text widths is necessary to trim the text inside the blocks
+ * while the scaling changes (e.g. via scrolling). This is very expensive,
+ * so maintain a cache of string contents to text widths.
+ */
+ _textWidthsCache: null,
+ _overflowCharWidth: null,
+ _averageCharWidth: null,
+
+ /**
+ * Gets the width of the specified text, for the current context state
+ * (font size, family etc.).
+ *
+ * @param string text
+ * The text to analyze.
+ * @return number
+ * The text width.
+ */
+ _getTextWidth: function (text) {
+ let cachedWidth = this._textWidthsCache[text];
+ if (cachedWidth) {
+ return cachedWidth;
+ }
+ let metrics = this._ctx.measureText(text);
+ return (this._textWidthsCache[text] = metrics.width);
+ },
+
+ /**
+ * Gets an approximate width of the specified text. This is much faster
+ * than `_getTextWidth`, but inexact.
+ *
+ * @param string text
+ * The text to analyze.
+ * @return number
+ * The approximate text width.
+ */
+ _getTextWidthApprox: function (text) {
+ return text.length * this._averageCharWidth;
+ },
+
+ /**
+ * Gets the average letter width in the English alphabet, for the current
+ * context state (font size, family etc.). This provides a close enough
+ * value to use in `_getTextWidthApprox`.
+ *
+ * @return number
+ * The average letter width.
+ */
+ _calcAverageCharWidth: function () {
+ let letterWidthsSum = 0;
+ // space
+ let start = 32;
+ // "z"
+ let end = 123;
+
+ for (let i = start; i < end; i++) {
+ let char = String.fromCharCode(i);
+ letterWidthsSum += this._getTextWidth(char);
+ }
+
+ return letterWidthsSum / (end - start);
+ },
+
+ /**
+ * Massage a text to fit inside a given width. This clamps the string
+ * at the end to avoid overflowing.
+ *
+ * @param string text
+ * The text to fit inside the given width.
+ * @param number maxWidth
+ * The available width for the given text.
+ * @return string
+ * The fitted text.
+ */
+ _getFittedText: function (text, maxWidth) {
+ let textWidth = this._getTextWidth(text);
+ if (textWidth < maxWidth) {
+ return text;
+ }
+ if (this._overflowCharWidth > maxWidth) {
+ return "";
+ }
+ for (let i = 1, len = text.length; i <= len; i++) {
+ let trimmedText = text.substring(0, len - i);
+ let trimmedWidth = this._getTextWidthApprox(trimmedText)
+ + this._overflowCharWidth;
+ if (trimmedWidth < maxWidth) {
+ return trimmedText + this.overflowChar;
+ }
+ }
+ return "";
+ },
+
+ /**
+ * Listener for the "keydown" event on the graph's container.
+ */
+ _onKeyDown: function (e) {
+ ViewHelpers.preventScrolling(e);
+
+ const hasModifier = e.ctrlKey || e.shiftKey || e.altKey || e.metaKey;
+
+ if (!hasModifier && !this._keysPressed[e.keyCode]) {
+ this._keysPressed[e.keyCode] = true;
+ this._userInputStack++;
+ this._shouldRedraw = true;
+ }
+ },
+
+ /**
+ * Listener for the "keyup" event on the graph's container.
+ */
+ _onKeyUp: function (e) {
+ ViewHelpers.preventScrolling(e);
+
+ if (this._keysPressed[e.keyCode]) {
+ this._keysPressed[e.keyCode] = false;
+ this._userInputStack--;
+ this._shouldRedraw = true;
+ }
+ },
+
+ /**
+ * Listener for the "keypress" event on the graph's container.
+ */
+ _onKeyPress: function (e) {
+ ViewHelpers.preventScrolling(e);
+ },
+
+ /**
+ * Listener for the "mousemove" event on the graph's container.
+ */
+ _onMouseMove: function (e) {
+ let {mouseX, mouseY} = this._getRelativeEventCoordinates(e);
+
+ let canvasWidth = this._width;
+
+ let selection = this._selection;
+ let selectionWidth = selection.end - selection.start;
+ let selectionScale = canvasWidth / selectionWidth;
+
+ let horizDrag = this._selectionDragger;
+ let vertDrag = this._verticalOffsetDragger;
+
+ // Avoid dragging both horizontally and vertically at the same time,
+ // as this doesn't feel natural. Based on a minimum distance, enable either
+ // one, and remember the drag direction to offset the mouse coords later.
+ if (!this._horizontalDragEnabled && !this._verticalDragEnabled) {
+ let horizDiff = Math.abs(horizDrag.origin - mouseX);
+ if (horizDiff > this.horizontalPanThreshold) {
+ this._horizontalDragDirection = Math.sign(horizDrag.origin - mouseX);
+ this._horizontalDragEnabled = true;
+ }
+ let vertDiff = Math.abs(vertDrag.origin - mouseY);
+ if (vertDiff > this.verticalPanThreshold) {
+ this._verticalDragDirection = Math.sign(vertDrag.origin - mouseY);
+ this._verticalDragEnabled = true;
+ }
+ }
+
+ if (horizDrag.origin != null && this._horizontalDragEnabled) {
+ let relativeX = mouseX + this._horizontalDragDirection *
+ this.horizontalPanThreshold;
+ selection.start = horizDrag.anchor.start +
+ (horizDrag.origin - relativeX) / selectionScale;
+ selection.end = horizDrag.anchor.end +
+ (horizDrag.origin - relativeX) / selectionScale;
+ this._normalizeSelectionBounds();
+ this._shouldRedraw = true;
+ this.emit("selecting");
+ }
+
+ if (vertDrag.origin != null && this._verticalDragEnabled) {
+ let relativeY = mouseY +
+ this._verticalDragDirection * this.verticalPanThreshold;
+ this._verticalOffset = vertDrag.anchor +
+ (vertDrag.origin - relativeY) / this._pixelRatio;
+ this._normalizeVerticalOffset();
+ this._shouldRedraw = true;
+ this.emit("panning-vertically");
+ }
+ },
+
+ /**
+ * Listener for the "mousedown" event on the graph's container.
+ */
+ _onMouseDown: function (e) {
+ let {mouseX, mouseY} = this._getRelativeEventCoordinates(e);
+
+ this._selectionDragger.origin = mouseX;
+ this._selectionDragger.anchor.start = this._selection.start;
+ this._selectionDragger.anchor.end = this._selection.end;
+
+ this._verticalOffsetDragger.origin = mouseY;
+ this._verticalOffsetDragger.anchor = this._verticalOffset;
+
+ this._horizontalDragEnabled = false;
+ this._verticalDragEnabled = false;
+
+ this._canvas.setAttribute("input", "adjusting-view-area");
+ },
+
+ /**
+ * Listener for the "mouseup" event on the graph's container.
+ */
+ _onMouseUp: function () {
+ this._selectionDragger.origin = null;
+ this._verticalOffsetDragger.origin = null;
+ this._horizontalDragEnabled = false;
+ this._horizontalDragDirection = 0;
+ this._verticalDragEnabled = false;
+ this._verticalDragDirection = 0;
+ this._canvas.removeAttribute("input");
+ },
+
+ /**
+ * Listener for the "wheel" event on the graph's container.
+ */
+ _onMouseWheel: function (e) {
+ let {mouseX} = this._getRelativeEventCoordinates(e);
+
+ let canvasWidth = this._width;
+
+ let selection = this._selection;
+ let selectionWidth = selection.end - selection.start;
+ let selectionScale = canvasWidth / selectionWidth;
+
+ switch (e.axis) {
+ case e.VERTICAL_AXIS: {
+ let distFromStart = mouseX;
+ let distFromEnd = canvasWidth - mouseX;
+ let vector = e.detail * GRAPH_WHEEL_ZOOM_SENSITIVITY / selectionScale;
+ selection.start -= distFromStart * vector;
+ selection.end += distFromEnd * vector;
+ break;
+ }
+ case e.HORIZONTAL_AXIS: {
+ let vector = e.detail * GRAPH_WHEEL_SCROLL_SENSITIVITY / selectionScale;
+ selection.start += vector;
+ selection.end += vector;
+ break;
+ }
+ }
+
+ this._normalizeSelectionBounds();
+ this._shouldRedraw = true;
+ this.emit("selecting");
+ },
+
+ /**
+ * Makes sure the start and end points of the current selection
+ * are withing the graph's visible bounds, and that they form a selection
+ * wider than the allowed minimum width.
+ */
+ _normalizeSelectionBounds: function () {
+ let boundsStart = this._bounds.start;
+ let boundsEnd = this._bounds.end;
+ let selectionStart = this._selection.start;
+ let selectionEnd = this._selection.end;
+
+ if (selectionStart < boundsStart) {
+ selectionStart = boundsStart;
+ }
+ if (selectionEnd < boundsStart) {
+ selectionStart = boundsStart;
+ selectionEnd = GRAPH_MIN_SELECTION_WIDTH;
+ }
+ if (selectionEnd > boundsEnd) {
+ selectionEnd = boundsEnd;
+ }
+ if (selectionStart > boundsEnd) {
+ selectionEnd = boundsEnd;
+ selectionStart = boundsEnd - GRAPH_MIN_SELECTION_WIDTH;
+ }
+ if (selectionEnd - selectionStart < GRAPH_MIN_SELECTION_WIDTH) {
+ let midPoint = (selectionStart + selectionEnd) / 2;
+ selectionStart = midPoint - GRAPH_MIN_SELECTION_WIDTH / 2;
+ selectionEnd = midPoint + GRAPH_MIN_SELECTION_WIDTH / 2;
+ }
+
+ this._selection.start = selectionStart;
+ this._selection.end = selectionEnd;
+ },
+
+ /**
+ * Makes sure that the current vertical offset is within the allowed
+ * panning range.
+ */
+ _normalizeVerticalOffset: function () {
+ this._verticalOffset = Math.max(this._verticalOffset, 0);
+ },
+
+ /**
+ *
+ * Finds the optimal tick interval between time markers in this graph.
+ *
+ * @param number dataScale
+ * @return number
+ */
+ _findOptimalTickInterval: function (dataScale) {
+ let timingStep = TIMELINE_TICKS_MULTIPLE;
+ let spacingMin = TIMELINE_TICKS_SPACING_MIN * this._pixelRatio;
+ let maxIters = FIND_OPTIMAL_TICK_INTERVAL_MAX_ITERS;
+ let numIters = 0;
+
+ if (dataScale > spacingMin) {
+ return dataScale;
+ }
+
+ while (true) {
+ let scaledStep = dataScale * timingStep;
+ if (++numIters > maxIters) {
+ return scaledStep;
+ }
+ if (scaledStep < spacingMin) {
+ timingStep <<= 1;
+ continue;
+ }
+ return scaledStep;
+ }
+ },
+
+ /**
+ * Gets the offset of this graph's container relative to the owner window.
+ *
+ * @return object
+ * The { left, top } offset.
+ */
+ _getContainerOffset: function () {
+ let node = this._canvas;
+ let x = 0;
+ let y = 0;
+
+ while ((node = node.offsetParent)) {
+ x += node.offsetLeft;
+ y += node.offsetTop;
+ }
+
+ return { left: x, top: y };
+ },
+
+ /**
+ * Given a MouseEvent, make it relative to this._canvas.
+ * @return object {mouseX,mouseY}
+ */
+ _getRelativeEventCoordinates: function (e) {
+ // For ease of testing, testX and testY can be passed in as the event
+ // object.
+ if ("testX" in e && "testY" in e) {
+ return {
+ mouseX: e.testX * this._pixelRatio,
+ mouseY: e.testY * this._pixelRatio
+ };
+ }
+
+ let offset = this._getContainerOffset();
+ let mouseX = (e.clientX - offset.left) * this._pixelRatio;
+ let mouseY = (e.clientY - offset.top) * this._pixelRatio;
+
+ return {mouseX, mouseY};
+ },
+
+ /**
+ * Listener for the "resize" event on the graph's parent node.
+ */
+ _onResize: function () {
+ if (this.hasData()) {
+ setNamedTimeout(this._uid, GRAPH_RESIZE_EVENTS_DRAIN, this.refresh);
+ }
+ }
+};
+
+/**
+ * A collection of utility functions converting various data sources
+ * into a format drawable by the FlameGraph.
+ */
+var FlameGraphUtils = {
+ _cache: new WeakMap(),
+
+ /**
+ * Create data suitable for use with FlameGraph from a profile's samples.
+ * Iterate the profile's samples and keep a moving window of stack traces.
+ *
+ * @param object thread
+ * The raw thread object received from the backend.
+ * @param object options
+ * Additional supported options,
+ * - boolean contentOnly [optional]
+ * - boolean invertTree [optional]
+ * - boolean flattenRecursion [optional]
+ * - string showIdleBlocks [optional]
+ * @return object
+ * Data source usable by FlameGraph.
+ */
+ createFlameGraphDataFromThread: function (thread, options = {}, out = []) {
+ let cached = this._cache.get(thread);
+ if (cached) {
+ return cached;
+ }
+
+ // 1. Create a map of colors to arrays, representing buckets of
+ // blocks inside the flame graph pyramid sharing the same style.
+
+ let buckets = Array.from({ length: PALLETTE_SIZE }, () => []);
+
+ // 2. Populate the buckets by iterating over every frame in every sample.
+
+ let { samples, stackTable, frameTable, stringTable } = thread;
+
+ const SAMPLE_STACK_SLOT = samples.schema.stack;
+ const SAMPLE_TIME_SLOT = samples.schema.time;
+
+ const STACK_PREFIX_SLOT = stackTable.schema.prefix;
+ const STACK_FRAME_SLOT = stackTable.schema.frame;
+
+ const getOrAddInflatedFrame = FrameUtils.getOrAddInflatedFrame;
+
+ let inflatedFrameCache = FrameUtils.getInflatedFrameCache(frameTable);
+ let labelCache = Object.create(null);
+
+ let samplesData = samples.data;
+ let stacksData = stackTable.data;
+
+ let flattenRecursion = options.flattenRecursion;
+
+ // Reused objects.
+ let mutableFrameKeyOptions = {
+ contentOnly: options.contentOnly,
+ isRoot: false,
+ isLeaf: false,
+ isMetaCategoryOut: false
+ };
+
+ // Take the timestamp of the first sample as prevTime. 0 is incorrect due
+ // to circular buffer wraparound. If wraparound happens, then the first
+ // sample will have an incorrect, large duration.
+ let prevTime = samplesData.length > 0 ? samplesData[0][SAMPLE_TIME_SLOT]
+ : 0;
+ let prevFrames = [];
+ let sampleFrames = [];
+ let sampleFrameKeys = [];
+
+ for (let i = 1; i < samplesData.length; i++) {
+ let sample = samplesData[i];
+ let time = sample[SAMPLE_TIME_SLOT];
+
+ let stackIndex = sample[SAMPLE_STACK_SLOT];
+ let prevFrameKey;
+
+ let stackDepth = 0;
+
+ // Inflate the stack and keep a moving window of call stacks.
+ //
+ // For reference, see the similar block comment in
+ // ThreadNode.prototype._buildInverted.
+ //
+ // In a similar fashion to _buildInverted, frames are inflated on the
+ // fly while stackwalking the stackTable trie. The exact same frame key
+ // is computed in both _buildInverted and here.
+ //
+ // Unlike _buildInverted, which builds a call tree directly, the flame
+ // graph inflates the stack into an array, as it maintains a moving
+ // window of stacks over time.
+ //
+ // Like _buildInverted, the various filtering functions are also inlined
+ // into stack inflation loop.
+ while (stackIndex !== null) {
+ let stackEntry = stacksData[stackIndex];
+ let frameIndex = stackEntry[STACK_FRAME_SLOT];
+
+ // Fetch the stack prefix (i.e. older frames) index.
+ stackIndex = stackEntry[STACK_PREFIX_SLOT];
+
+ // Inflate the frame.
+ let inflatedFrame = getOrAddInflatedFrame(inflatedFrameCache,
+ frameIndex, frameTable,
+ stringTable);
+
+ mutableFrameKeyOptions.isRoot = stackIndex === null;
+ mutableFrameKeyOptions.isLeaf = stackDepth === 0;
+ let frameKey = inflatedFrame.getFrameKey(mutableFrameKeyOptions);
+
+ // If not skipping the frame, add it to the current level. The (root)
+ // node isn't useful for flame graphs.
+ if (frameKey !== "" && frameKey !== "(root)") {
+ // If the frame is a meta category, use the category label.
+ if (mutableFrameKeyOptions.isMetaCategoryOut) {
+ frameKey = CATEGORY_MAPPINGS[frameKey].label;
+ }
+
+ sampleFrames[stackDepth] = inflatedFrame;
+ sampleFrameKeys[stackDepth] = frameKey;
+
+ // If we shouldn't flatten the current frame into the previous one,
+ // increment the stack depth.
+ if (!flattenRecursion || frameKey !== prevFrameKey) {
+ stackDepth++;
+ }
+
+ prevFrameKey = frameKey;
+ }
+ }
+
+ // Uninvert frames in place if needed.
+ if (!options.invertTree) {
+ sampleFrames.length = stackDepth;
+ sampleFrames.reverse();
+ sampleFrameKeys.length = stackDepth;
+ sampleFrameKeys.reverse();
+ }
+
+ // If no frames are available, add a pseudo "idle" block in between.
+ let isIdleFrame = false;
+ if (options.showIdleBlocks && stackDepth === 0) {
+ sampleFrames[0] = null;
+ sampleFrameKeys[0] = options.showIdleBlocks;
+ stackDepth = 1;
+ isIdleFrame = true;
+ }
+
+ // Put each frame in a bucket.
+ for (let frameIndex = 0; frameIndex < stackDepth; frameIndex++) {
+ let key = sampleFrameKeys[frameIndex];
+ let prevFrame = prevFrames[frameIndex];
+
+ // Frames at the same location and the same depth will be reused.
+ // If there is a block already created, change its width.
+ if (prevFrame && prevFrame.frameKey === key) {
+ prevFrame.width = (time - prevFrame.startTime);
+ } else {
+ // Otherwise, create a new block for this frame at this depth,
+ // using a simple location based salt for picking a color.
+ let hash = this._getStringHash(key);
+ let bucket = buckets[hash % PALLETTE_SIZE];
+
+ let label;
+ if (isIdleFrame) {
+ label = key;
+ } else {
+ label = labelCache[key];
+ if (!label) {
+ label = labelCache[key] =
+ this._formatLabel(key, sampleFrames[frameIndex]);
+ }
+ }
+
+ bucket.push(prevFrames[frameIndex] = {
+ startTime: prevTime,
+ frameKey: key,
+ x: prevTime,
+ y: frameIndex * FLAME_GRAPH_BLOCK_HEIGHT,
+ width: time - prevTime,
+ height: FLAME_GRAPH_BLOCK_HEIGHT,
+ text: label
+ });
+ }
+ }
+
+ // Previous frames at stack depths greater than the current sample's
+ // maximum need to be nullified. It's nonsensical to reuse them.
+ prevFrames.length = stackDepth;
+ prevTime = time;
+ }
+
+ // 3. Convert the buckets into a data source usable by the FlameGraph.
+ // This is a simple conversion from a Map to an Array.
+
+ for (let i = 0; i < buckets.length; i++) {
+ out.push({ color: COLOR_PALLETTE[i], blocks: buckets[i] });
+ }
+
+ this._cache.set(thread, out);
+ return out;
+ },
+
+ /**
+ * Clears the cached flame graph data created for the given source.
+ * @param any source
+ */
+ removeFromCache: function (source) {
+ this._cache.delete(source);
+ },
+
+ /**
+ * Very dumb hashing of a string. Used to pick colors from a pallette.
+ *
+ * @param string input
+ * @return number
+ */
+ _getStringHash: function (input) {
+ const STRING_HASH_PRIME1 = 7;
+ const STRING_HASH_PRIME2 = 31;
+
+ let hash = STRING_HASH_PRIME1;
+
+ for (let i = 0, len = input.length; i < len; i++) {
+ hash *= STRING_HASH_PRIME2;
+ hash += input.charCodeAt(i);
+
+ if (hash > Number.MAX_SAFE_INTEGER / STRING_HASH_PRIME2) {
+ return hash;
+ }
+ }
+
+ return hash;
+ },
+
+ /**
+ * Takes a frame key and a frame, and returns a string that should be
+ * displayed in its flame block.
+ *
+ * @param string key
+ * @param object frame
+ * @return string
+ */
+ _formatLabel: function (key, frame) {
+ let { functionName, fileName, line } =
+ FrameUtils.parseLocation(key, frame.line);
+ let label = FrameUtils.shouldDemangle(functionName) ? demangle(functionName)
+ : functionName;
+
+ if (fileName) {
+ label += ` (${fileName}${line != null ? (":" + line) : ""})`;
+ }
+
+ return label;
+ }
+};
+
+exports.FlameGraph = FlameGraph;
+exports.FlameGraphUtils = FlameGraphUtils;
+exports.PALLETTE_SIZE = PALLETTE_SIZE;
+exports.FLAME_GRAPH_BLOCK_HEIGHT = FLAME_GRAPH_BLOCK_HEIGHT;
+exports.FLAME_GRAPH_BLOCK_TEXT_FONT_SIZE = FLAME_GRAPH_BLOCK_TEXT_FONT_SIZE;
+exports.FLAME_GRAPH_BLOCK_TEXT_FONT_FAMILY = FLAME_GRAPH_BLOCK_TEXT_FONT_FAMILY;
diff --git a/devtools/client/shared/widgets/Graphs.js b/devtools/client/shared/widgets/Graphs.js
new file mode 100644
index 000000000..485da2b1b
--- /dev/null
+++ b/devtools/client/shared/widgets/Graphs.js
@@ -0,0 +1,1424 @@
+/* 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 { Task } = require("devtools/shared/task");
+const { setNamedTimeout } = require("devtools/client/shared/widgets/view-helpers");
+const { getCurrentZoom } = require("devtools/shared/layout/utils");
+
+loader.lazyRequireGetter(this, "defer", "devtools/shared/defer");
+loader.lazyRequireGetter(this, "EventEmitter",
+ "devtools/shared/event-emitter");
+
+loader.lazyImporter(this, "DevToolsWorker",
+ "resource://devtools/shared/worker/worker.js");
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const GRAPH_SRC = "chrome://devtools/content/shared/widgets/graphs-frame.xhtml";
+const WORKER_URL =
+ "resource://devtools/client/shared/widgets/GraphsWorker.js";
+
+// Generic constants.
+
+// ms
+const GRAPH_RESIZE_EVENTS_DRAIN = 100;
+const GRAPH_WHEEL_ZOOM_SENSITIVITY = 0.00075;
+const GRAPH_WHEEL_SCROLL_SENSITIVITY = 0.1;
+// px
+const GRAPH_WHEEL_MIN_SELECTION_WIDTH = 10;
+
+// px
+const GRAPH_SELECTION_BOUNDARY_HOVER_LINE_WIDTH = 4;
+const GRAPH_SELECTION_BOUNDARY_HOVER_THRESHOLD = 10;
+const GRAPH_MAX_SELECTION_LEFT_PADDING = 1;
+const GRAPH_MAX_SELECTION_RIGHT_PADDING = 1;
+
+// px
+const GRAPH_REGION_LINE_WIDTH = 1;
+const GRAPH_REGION_LINE_COLOR = "rgba(237,38,85,0.8)";
+
+// px
+const GRAPH_STRIPE_PATTERN_WIDTH = 16;
+const GRAPH_STRIPE_PATTERN_HEIGHT = 16;
+const GRAPH_STRIPE_PATTERN_LINE_WIDTH = 2;
+const GRAPH_STRIPE_PATTERN_LINE_SPACING = 4;
+
+/**
+ * Small data primitives for all graphs.
+ */
+this.GraphCursor = function () {
+ this.x = null;
+ this.y = null;
+};
+
+this.GraphArea = function () {
+ this.start = null;
+ this.end = null;
+};
+
+this.GraphAreaDragger = function (anchor = new GraphArea()) {
+ this.origin = null;
+ this.anchor = anchor;
+};
+
+this.GraphAreaResizer = function () {
+ this.margin = null;
+};
+
+/**
+ * Base class for all graphs using a canvas to render the data source. Handles
+ * frame creation, data source, selection bounds, cursor position, etc.
+ *
+ * Language:
+ * - The "data" represents the values used when building the graph.
+ * Its specific format is defined by the inheriting classes.
+ *
+ * - A "cursor" is the cliphead position across the X axis of the graph.
+ *
+ * - A "selection" is defined by a "start" and an "end" value and
+ * represents the selected bounds in the graph.
+ *
+ * - A "region" is a highlighted area in the graph, also defined by a
+ * "start" and an "end" value, but distinct from the "selection". It is
+ * simply used to highlight important regions in the data.
+ *
+ * Instances of this class are EventEmitters with the following events:
+ * - "ready": when the container iframe and canvas are created.
+ * - "selecting": when the selection is set or changed.
+ * - "deselecting": when the selection is dropped.
+ *
+ * @param nsIDOMNode parent
+ * The parent node holding the graph.
+ * @param string name
+ * The graph type, used for setting the correct class names.
+ * Currently supported: "line-graph" only.
+ * @param number sharpness [optional]
+ * Defaults to the current device pixel ratio.
+ */
+this.AbstractCanvasGraph = function (parent, name, sharpness) {
+ EventEmitter.decorate(this);
+
+ this._parent = parent;
+ this._ready = defer();
+
+ this._uid = "canvas-graph-" + Date.now();
+ this._renderTargets = new Map();
+
+ AbstractCanvasGraph.createIframe(GRAPH_SRC, parent, iframe => {
+ this._iframe = iframe;
+ this._window = iframe.contentWindow;
+ this._topWindow = this._window.top;
+ this._document = iframe.contentDocument;
+ this._pixelRatio = sharpness || this._window.devicePixelRatio;
+
+ let container =
+ this._container = this._document.getElementById("graph-container");
+ container.className = name + "-widget-container graph-widget-container";
+
+ let canvas = this._canvas = this._document.getElementById("graph-canvas");
+ canvas.className = name + "-widget-canvas graph-widget-canvas";
+
+ let bounds = parent.getBoundingClientRect();
+ bounds.width = this.fixedWidth || bounds.width;
+ bounds.height = this.fixedHeight || bounds.height;
+ iframe.setAttribute("width", bounds.width);
+ iframe.setAttribute("height", bounds.height);
+
+ this._width = canvas.width = bounds.width * this._pixelRatio;
+ this._height = canvas.height = bounds.height * this._pixelRatio;
+ this._ctx = canvas.getContext("2d");
+ this._ctx.imageSmoothingEnabled = false;
+
+ this._cursor = new GraphCursor();
+ this._selection = new GraphArea();
+ this._selectionDragger = new GraphAreaDragger();
+ this._selectionResizer = new GraphAreaResizer();
+ this._isMouseActive = false;
+
+ this._onAnimationFrame = this._onAnimationFrame.bind(this);
+ this._onMouseMove = this._onMouseMove.bind(this);
+ this._onMouseDown = this._onMouseDown.bind(this);
+ this._onMouseUp = this._onMouseUp.bind(this);
+ this._onMouseWheel = this._onMouseWheel.bind(this);
+ this._onMouseOut = this._onMouseOut.bind(this);
+ this._onResize = this._onResize.bind(this);
+ this.refresh = this.refresh.bind(this);
+
+ this._window.addEventListener("mousemove", this._onMouseMove);
+ this._window.addEventListener("mousedown", this._onMouseDown);
+ this._window.addEventListener("MozMousePixelScroll", this._onMouseWheel);
+ this._window.addEventListener("mouseout", this._onMouseOut);
+
+ let ownerWindow = this._parent.ownerDocument.defaultView;
+ ownerWindow.addEventListener("resize", this._onResize);
+
+ this._animationId =
+ this._window.requestAnimationFrame(this._onAnimationFrame);
+
+ this._ready.resolve(this);
+ this.emit("ready", this);
+ });
+};
+
+AbstractCanvasGraph.prototype = {
+ /**
+ * Read-only width and height of the canvas.
+ * @return number
+ */
+ get width() {
+ return this._width;
+ },
+ get height() {
+ return this._height;
+ },
+
+ /**
+ * Return true if the mouse is actively messing with the selection, false
+ * otherwise.
+ */
+ get isMouseActive() {
+ return this._isMouseActive;
+ },
+
+ /**
+ * Returns a promise resolved once this graph is ready to receive data.
+ */
+ ready: function () {
+ return this._ready.promise;
+ },
+
+ /**
+ * Destroys this graph.
+ */
+ destroy: Task.async(function* () {
+ yield this.ready();
+
+ this._topWindow.removeEventListener("mousemove", this._onMouseMove);
+ this._topWindow.removeEventListener("mouseup", this._onMouseUp);
+ this._window.removeEventListener("mousemove", this._onMouseMove);
+ this._window.removeEventListener("mousedown", this._onMouseDown);
+ this._window.removeEventListener("MozMousePixelScroll", this._onMouseWheel);
+ this._window.removeEventListener("mouseout", this._onMouseOut);
+
+ let ownerWindow = this._parent.ownerDocument.defaultView;
+ if (ownerWindow) {
+ ownerWindow.removeEventListener("resize", this._onResize);
+ }
+
+ this._window.cancelAnimationFrame(this._animationId);
+ this._iframe.remove();
+
+ this._cursor = null;
+ this._selection = null;
+ this._selectionDragger = null;
+ this._selectionResizer = null;
+
+ this._data = null;
+ this._mask = null;
+ this._maskArgs = null;
+ this._regions = null;
+
+ this._cachedBackgroundImage = null;
+ this._cachedGraphImage = null;
+ this._cachedMaskImage = null;
+ this._renderTargets.clear();
+ gCachedStripePattern.clear();
+
+ this.emit("destroyed");
+ }),
+
+ /**
+ * Rendering options. Subclasses should override these.
+ */
+ clipheadLineWidth: 1,
+ clipheadLineColor: "transparent",
+ selectionLineWidth: 1,
+ selectionLineColor: "transparent",
+ selectionBackgroundColor: "transparent",
+ selectionStripesColor: "transparent",
+ regionBackgroundColor: "transparent",
+ regionStripesColor: "transparent",
+
+ /**
+ * Makes sure the canvas graph is of the specified width or height, and
+ * doesn't flex to fit all the available space.
+ */
+ fixedWidth: null,
+ fixedHeight: null,
+
+ /**
+ * Optionally builds and caches a background image for this graph.
+ * Inheriting classes may override this method.
+ */
+ buildBackgroundImage: function () {
+ return null;
+ },
+
+ /**
+ * Builds and caches a graph image, based on the data source supplied
+ * in `setData`. The graph image is not rebuilt on each frame, but
+ * only when the data source changes.
+ */
+ buildGraphImage: function () {
+ let error = "This method needs to be implemented by inheriting classes.";
+ throw new Error(error);
+ },
+
+ /**
+ * Optionally builds and caches a mask image for this graph, composited
+ * over the data image created via `buildGraphImage`. Inheriting classes
+ * may override this method.
+ */
+ buildMaskImage: function () {
+ return null;
+ },
+
+ /**
+ * When setting the data source, the coordinates and values may be
+ * stretched or squeezed on the X/Y axis, to fit into the available space.
+ */
+ dataScaleX: 1,
+ dataScaleY: 1,
+
+ /**
+ * Sets the data source for this graph.
+ *
+ * @param object data
+ * The data source. The actual format is specified by subclasses.
+ */
+ setData: function (data) {
+ this._data = data;
+ this._cachedBackgroundImage = this.buildBackgroundImage();
+ this._cachedGraphImage = this.buildGraphImage();
+ this._shouldRedraw = true;
+ },
+
+ /**
+ * Same as `setData`, but waits for this graph to finish initializing first.
+ *
+ * @param object data
+ * The data source. The actual format is specified by subclasses.
+ * @return promise
+ * A promise resolved once the data is set.
+ */
+ setDataWhenReady: Task.async(function* (data) {
+ yield this.ready();
+ this.setData(data);
+ }),
+
+ /**
+ * Adds a mask to this graph.
+ *
+ * @param any mask, options
+ * See `buildMaskImage` in inheriting classes for the required args.
+ */
+ setMask: function (mask, ...options) {
+ this._mask = mask;
+ this._maskArgs = [mask, ...options];
+ this._cachedMaskImage = this.buildMaskImage.apply(this, this._maskArgs);
+ this._shouldRedraw = true;
+ },
+
+ /**
+ * Adds regions to this graph.
+ *
+ * See the "Language" section in the constructor documentation
+ * for details about what "regions" represent.
+ *
+ * @param array regions
+ * A list of { start, end } values.
+ */
+ setRegions: function (regions) {
+ if (!this._cachedGraphImage) {
+ throw new Error("Can't highlight regions on a graph with " +
+ "no data displayed.");
+ }
+ if (this._regions) {
+ throw new Error("Regions were already highlighted on the graph.");
+ }
+ this._regions = regions.map(e => ({
+ start: e.start * this.dataScaleX,
+ end: e.end * this.dataScaleX
+ }));
+ this._bakeRegions(this._regions, this._cachedGraphImage);
+ this._shouldRedraw = true;
+ },
+
+ /**
+ * Gets whether or not this graph has a data source.
+ * @return boolean
+ */
+ hasData: function () {
+ return !!this._data;
+ },
+
+ /**
+ * Gets whether or not this graph has any mask applied.
+ * @return boolean
+ */
+ hasMask: function () {
+ return !!this._mask;
+ },
+
+ /**
+ * Gets whether or not this graph has any regions.
+ * @return boolean
+ */
+ hasRegions: function () {
+ return !!this._regions;
+ },
+
+ /**
+ * Sets the selection bounds.
+ * Use `dropSelection` to remove the selection.
+ *
+ * If the bounds aren't different, no "selection" event is emitted.
+ *
+ * See the "Language" section in the constructor documentation
+ * for details about what a "selection" represents.
+ *
+ * @param object selection
+ * The selection's { start, end } values.
+ */
+ setSelection: function (selection) {
+ if (!selection || selection.start == null || selection.end == null) {
+ throw new Error("Invalid selection coordinates");
+ }
+ if (!this.isSelectionDifferent(selection)) {
+ return;
+ }
+ this._selection.start = selection.start;
+ this._selection.end = selection.end;
+ this._shouldRedraw = true;
+ this.emit("selecting");
+ },
+
+ /**
+ * Gets the selection bounds.
+ * If there's no selection, the bounds have null values.
+ *
+ * @return object
+ * The selection's { start, end } values.
+ */
+ getSelection: function () {
+ if (this.hasSelection()) {
+ return { start: this._selection.start, end: this._selection.end };
+ }
+ if (this.hasSelectionInProgress()) {
+ return { start: this._selection.start, end: this._cursor.x };
+ }
+ return { start: null, end: null };
+ },
+
+ /**
+ * Sets the selection bounds, scaled to correlate with the data source ranges,
+ * such that a [0, max width] selection maps to [first value, last value].
+ *
+ * @param object selection
+ * The selection's { start, end } values.
+ * @param object { mapStart, mapEnd } mapping [optional]
+ * Invoked when retrieving the numbers in the data source representing
+ * the first and last values, on the X axis.
+ */
+ setMappedSelection: function (selection, mapping = {}) {
+ if (!this.hasData()) {
+ throw new Error("A data source is necessary for retrieving " +
+ "a mapped selection.");
+ }
+ if (!selection || selection.start == null || selection.end == null) {
+ throw new Error("Invalid selection coordinates");
+ }
+
+ let { mapStart, mapEnd } = mapping;
+ let startTime = (mapStart || (e => e.delta))(this._data[0]);
+ let endTime = (mapEnd || (e => e.delta))(this._data[this._data.length - 1]);
+
+ // The selection's start and end values are not guaranteed to be ascending.
+ // Also make sure that the selection bounds fit inside the data bounds.
+ let min = Math.max(Math.min(selection.start, selection.end), startTime);
+ let max = Math.min(Math.max(selection.start, selection.end), endTime);
+ min = map(min, startTime, endTime, 0, this._width);
+ max = map(max, startTime, endTime, 0, this._width);
+
+ this.setSelection({ start: min, end: max });
+ },
+
+ /**
+ * Gets the selection bounds, scaled to correlate with the data source ranges,
+ * such that a [0, max width] selection maps to [first value, last value].
+ *
+ * @param object { mapStart, mapEnd } mapping [optional]
+ * Invoked when retrieving the numbers in the data source representing
+ * the first and last values, on the X axis.
+ * @return object
+ * The mapped selection's { min, max } values.
+ */
+ getMappedSelection: function (mapping = {}) {
+ if (!this.hasData()) {
+ throw new Error("A data source is necessary for retrieving a " +
+ "mapped selection.");
+ }
+ if (!this.hasSelection() && !this.hasSelectionInProgress()) {
+ return { min: null, max: null };
+ }
+
+ let { mapStart, mapEnd } = mapping;
+ let startTime = (mapStart || (e => e.delta))(this._data[0]);
+ let endTime = (mapEnd || (e => e.delta))(this._data[this._data.length - 1]);
+
+ // The selection's start and end values are not guaranteed to be ascending.
+ // This can happen, for example, when click & dragging from right to left.
+ // Also make sure that the selection bounds fit inside the canvas bounds.
+ let selection = this.getSelection();
+ let min = Math.max(Math.min(selection.start, selection.end), 0);
+ let max = Math.min(Math.max(selection.start, selection.end), this._width);
+ min = map(min, 0, this._width, startTime, endTime);
+ max = map(max, 0, this._width, startTime, endTime);
+
+ return { min: min, max: max };
+ },
+
+ /**
+ * Removes the selection.
+ */
+ dropSelection: function () {
+ if (!this.hasSelection() && !this.hasSelectionInProgress()) {
+ return;
+ }
+ this._selection.start = null;
+ this._selection.end = null;
+ this._shouldRedraw = true;
+ this.emit("deselecting");
+ },
+
+ /**
+ * Gets whether or not this graph has a selection.
+ * @return boolean
+ */
+ hasSelection: function () {
+ return this._selection &&
+ this._selection.start != null && this._selection.end != null;
+ },
+
+ /**
+ * Gets whether or not a selection is currently being made, for example
+ * via a click+drag operation.
+ * @return boolean
+ */
+ hasSelectionInProgress: function () {
+ return this._selection &&
+ this._selection.start != null && this._selection.end == null;
+ },
+
+ /**
+ * Specifies whether or not mouse selection is allowed.
+ * @type boolean
+ */
+ selectionEnabled: true,
+
+ /**
+ * Sets the selection bounds.
+ * Use `dropCursor` to hide the cursor.
+ *
+ * @param object cursor
+ * The cursor's { x, y } position.
+ */
+ setCursor: function (cursor) {
+ if (!cursor || cursor.x == null || cursor.y == null) {
+ throw new Error("Invalid cursor coordinates");
+ }
+ if (!this.isCursorDifferent(cursor)) {
+ return;
+ }
+ this._cursor.x = cursor.x;
+ this._cursor.y = cursor.y;
+ this._shouldRedraw = true;
+ },
+
+ /**
+ * Gets the cursor position.
+ * If there's no cursor, the position has null values.
+ *
+ * @return object
+ * The cursor's { x, y } values.
+ */
+ getCursor: function () {
+ return { x: this._cursor.x, y: this._cursor.y };
+ },
+
+ /**
+ * Hides the cursor.
+ */
+ dropCursor: function () {
+ if (!this.hasCursor()) {
+ return;
+ }
+ this._cursor.x = null;
+ this._cursor.y = null;
+ this._shouldRedraw = true;
+ },
+
+ /**
+ * Gets whether or not this graph has a visible cursor.
+ * @return boolean
+ */
+ hasCursor: function () {
+ return this._cursor && this._cursor.x != null;
+ },
+
+ /**
+ * Specifies if this graph's selection is different from another one.
+ *
+ * @param object other
+ * The other graph's selection, as { start, end } values.
+ */
+ isSelectionDifferent: function (other) {
+ if (!other) {
+ return true;
+ }
+ let current = this.getSelection();
+ return current.start != other.start || current.end != other.end;
+ },
+
+ /**
+ * Specifies if this graph's cursor is different from another one.
+ *
+ * @param object other
+ * The other graph's position, as { x, y } values.
+ */
+ isCursorDifferent: function (other) {
+ if (!other) {
+ return true;
+ }
+ let current = this.getCursor();
+ return current.x != other.x || current.y != other.y;
+ },
+
+ /**
+ * Gets the width of the current selection.
+ * If no selection is available, 0 is returned.
+ *
+ * @return number
+ * The selection width.
+ */
+ getSelectionWidth: function () {
+ let selection = this.getSelection();
+ return Math.abs(selection.start - selection.end);
+ },
+
+ /**
+ * Gets the currently hovered region, if any.
+ * If no region is currently hovered, null is returned.
+ *
+ * @return object
+ * The hovered region, as { start, end } values.
+ */
+ getHoveredRegion: function () {
+ if (!this.hasRegions() || !this.hasCursor()) {
+ return null;
+ }
+ let { x } = this._cursor;
+ return this._regions.find(({ start, end }) =>
+ (start < end && start < x && end > x) ||
+ (start > end && end < x && start > x));
+ },
+
+ /**
+ * Updates this graph to reflect the new dimensions of the parent node.
+ *
+ * @param boolean options.force
+ * Force redrawing everything
+ */
+ refresh: function (options = {}) {
+ let bounds = this._parent.getBoundingClientRect();
+ let newWidth = this.fixedWidth || bounds.width;
+ let newHeight = this.fixedHeight || bounds.height;
+
+ // Prevent redrawing everything if the graph's width & height won't change,
+ // except if force=true.
+ if (!options.force &&
+ this._width == newWidth * this._pixelRatio &&
+ this._height == newHeight * this._pixelRatio) {
+ this.emit("refresh-cancelled");
+ return;
+ }
+
+ // Handle a changed size by mapping the old selection to the new width
+ if (this._width && newWidth && this.hasSelection()) {
+ let ratio = this._width / (newWidth * this._pixelRatio);
+ this._selection.start = Math.round(this._selection.start / ratio);
+ this._selection.end = Math.round(this._selection.end / ratio);
+ }
+
+ bounds.width = newWidth;
+ bounds.height = newHeight;
+ this._iframe.setAttribute("width", bounds.width);
+ this._iframe.setAttribute("height", bounds.height);
+ this._width = this._canvas.width = bounds.width * this._pixelRatio;
+ this._height = this._canvas.height = bounds.height * this._pixelRatio;
+
+ if (this.hasData()) {
+ this._cachedBackgroundImage = this.buildBackgroundImage();
+ this._cachedGraphImage = this.buildGraphImage();
+ }
+ if (this.hasMask()) {
+ this._cachedMaskImage = this.buildMaskImage.apply(this, this._maskArgs);
+ }
+ if (this.hasRegions()) {
+ this._bakeRegions(this._regions, this._cachedGraphImage);
+ }
+
+ this._shouldRedraw = true;
+ this.emit("refresh");
+ },
+
+ /**
+ * Gets a canvas with the specified name, for this graph.
+ *
+ * If it doesn't exist yet, it will be created, otherwise the cached instance
+ * will be cleared and returned.
+ *
+ * @param string name
+ * The canvas name.
+ * @param number width, height [optional]
+ * A custom width and height for the canvas. Defaults to this graph's
+ * container canvas width and height.
+ */
+ _getNamedCanvas: function (name, width = this._width, height = this._height) {
+ let cachedRenderTarget = this._renderTargets.get(name);
+ if (cachedRenderTarget) {
+ let { canvas, ctx } = cachedRenderTarget;
+ canvas.width = width;
+ canvas.height = height;
+ ctx.clearRect(0, 0, width, height);
+ return cachedRenderTarget;
+ }
+
+ let canvas = this._document.createElementNS(HTML_NS, "canvas");
+ let ctx = canvas.getContext("2d");
+ canvas.width = width;
+ canvas.height = height;
+
+ let renderTarget = { canvas: canvas, ctx: ctx };
+ this._renderTargets.set(name, renderTarget);
+ return renderTarget;
+ },
+
+ /**
+ * The contents of this graph are redrawn only when something changed,
+ * like the data source, or the selection bounds etc. This flag tracks
+ * if the rendering is "dirty" and needs to be refreshed.
+ */
+ _shouldRedraw: false,
+
+ /**
+ * Animation frame callback, invoked on each tick of the refresh driver.
+ */
+ _onAnimationFrame: function () {
+ this._animationId =
+ this._window.requestAnimationFrame(this._onAnimationFrame);
+ this._drawWidget();
+ },
+
+ /**
+ * Redraws the widget when necessary. The actual graph is not refreshed
+ * every time this function is called, only the cliphead, selection etc.
+ */
+ _drawWidget: function () {
+ if (!this._shouldRedraw) {
+ return;
+ }
+ let ctx = this._ctx;
+ ctx.clearRect(0, 0, this._width, this._height);
+
+ if (this._cachedGraphImage) {
+ ctx.drawImage(this._cachedGraphImage, 0, 0, this._width, this._height);
+ }
+ if (this._cachedMaskImage) {
+ ctx.globalCompositeOperation = "destination-out";
+ ctx.drawImage(this._cachedMaskImage, 0, 0, this._width, this._height);
+ }
+ if (this._cachedBackgroundImage) {
+ ctx.globalCompositeOperation = "destination-over";
+ ctx.drawImage(this._cachedBackgroundImage, 0, 0,
+ this._width, this._height);
+ }
+
+ // Revert to the original global composition operation.
+ if (this._cachedMaskImage || this._cachedBackgroundImage) {
+ ctx.globalCompositeOperation = "source-over";
+ }
+
+ if (this.hasCursor()) {
+ this._drawCliphead();
+ }
+ if (this.hasSelection() || this.hasSelectionInProgress()) {
+ this._drawSelection();
+ }
+
+ this._shouldRedraw = false;
+ },
+
+ /**
+ * Draws the cliphead, if available and necessary.
+ */
+ _drawCliphead: function () {
+ if (this._isHoveringSelectionContentsOrBoundaries() ||
+ this._isHoveringRegion()) {
+ return;
+ }
+
+ let ctx = this._ctx;
+ ctx.lineWidth = this.clipheadLineWidth;
+ ctx.strokeStyle = this.clipheadLineColor;
+ ctx.beginPath();
+ ctx.moveTo(this._cursor.x, 0);
+ ctx.lineTo(this._cursor.x, this._height);
+ ctx.stroke();
+ },
+
+ /**
+ * Draws the selection, if available and necessary.
+ */
+ _drawSelection: function () {
+ let { start, end } = this.getSelection();
+ let input = this._canvas.getAttribute("input");
+
+ let ctx = this._ctx;
+ ctx.strokeStyle = this.selectionLineColor;
+
+ // Fill selection.
+
+ let pattern = AbstractCanvasGraph.getStripePattern({
+ ownerDocument: this._document,
+ backgroundColor: this.selectionBackgroundColor,
+ stripesColor: this.selectionStripesColor
+ });
+ ctx.fillStyle = pattern;
+ let rectStart = Math.min(this._width, Math.max(0, start));
+ let rectEnd = Math.min(this._width, Math.max(0, end));
+ ctx.fillRect(rectStart, 0, rectEnd - rectStart, this._height);
+
+ // Draw left boundary.
+
+ if (input == "hovering-selection-start-boundary") {
+ ctx.lineWidth = GRAPH_SELECTION_BOUNDARY_HOVER_LINE_WIDTH;
+ } else {
+ ctx.lineWidth = this.clipheadLineWidth;
+ }
+ ctx.beginPath();
+ ctx.moveTo(start, 0);
+ ctx.lineTo(start, this._height);
+ ctx.stroke();
+
+ // Draw right boundary.
+
+ if (input == "hovering-selection-end-boundary") {
+ ctx.lineWidth = GRAPH_SELECTION_BOUNDARY_HOVER_LINE_WIDTH;
+ } else {
+ ctx.lineWidth = this.clipheadLineWidth;
+ }
+ ctx.beginPath();
+ ctx.moveTo(end, this._height);
+ ctx.lineTo(end, 0);
+ ctx.stroke();
+ },
+
+ /**
+ * Draws regions into the cached graph image, created via `buildGraphImage`.
+ * Called when new regions are set.
+ */
+ _bakeRegions: function (regions, destination) {
+ let ctx = destination.getContext("2d");
+
+ let pattern = AbstractCanvasGraph.getStripePattern({
+ ownerDocument: this._document,
+ backgroundColor: this.regionBackgroundColor,
+ stripesColor: this.regionStripesColor
+ });
+ ctx.fillStyle = pattern;
+ ctx.strokeStyle = GRAPH_REGION_LINE_COLOR;
+ ctx.lineWidth = GRAPH_REGION_LINE_WIDTH;
+
+ let y = -GRAPH_REGION_LINE_WIDTH;
+ let height = this._height + GRAPH_REGION_LINE_WIDTH;
+
+ for (let { start, end } of regions) {
+ let x = start;
+ let width = end - start;
+ ctx.fillRect(x, y, width, height);
+ ctx.strokeRect(x, y, width, height);
+ }
+ },
+
+ /**
+ * Checks whether the start handle of the selection is hovered.
+ * @return boolean
+ */
+ _isHoveringStartBoundary: function () {
+ if (!this.hasSelection() || !this.hasCursor()) {
+ return false;
+ }
+ let { x } = this._cursor;
+ let { start } = this._selection;
+ let threshold = GRAPH_SELECTION_BOUNDARY_HOVER_THRESHOLD * this._pixelRatio;
+ return Math.abs(start - x) < threshold;
+ },
+
+ /**
+ * Checks whether the end handle of the selection is hovered.
+ * @return boolean
+ */
+ _isHoveringEndBoundary: function () {
+ if (!this.hasSelection() || !this.hasCursor()) {
+ return false;
+ }
+ let { x } = this._cursor;
+ let { end } = this._selection;
+ let threshold = GRAPH_SELECTION_BOUNDARY_HOVER_THRESHOLD * this._pixelRatio;
+ return Math.abs(end - x) < threshold;
+ },
+
+ /**
+ * Checks whether the selection is hovered.
+ * @return boolean
+ */
+ _isHoveringSelectionContents: function () {
+ if (!this.hasSelection() || !this.hasCursor()) {
+ return false;
+ }
+ let { x } = this._cursor;
+ let { start, end } = this._selection;
+ return (start < end && start < x && end > x) ||
+ (start > end && end < x && start > x);
+ },
+
+ /**
+ * Checks whether the selection or its handles are hovered.
+ * @return boolean
+ */
+ _isHoveringSelectionContentsOrBoundaries: function () {
+ return this._isHoveringSelectionContents() ||
+ this._isHoveringStartBoundary() ||
+ this._isHoveringEndBoundary();
+ },
+
+ /**
+ * Checks whether a region is hovered.
+ * @return boolean
+ */
+ _isHoveringRegion: function () {
+ return !!this.getHoveredRegion();
+ },
+
+ /**
+ * Given a MouseEvent, make it relative to this._canvas.
+ * @return object {mouseX,mouseY}
+ */
+ _getRelativeEventCoordinates: function (e) {
+ // For ease of testing, testX and testY can be passed in as the event
+ // object. If so, just return this.
+ if ("testX" in e && "testY" in e) {
+ return {
+ mouseX: e.testX * this._pixelRatio,
+ mouseY: e.testY * this._pixelRatio
+ };
+ }
+
+ // This method is concerned with converting mouse event coordinates from
+ // "screen space" to "local space" (in other words, relative to this
+ // canvas's position, thus (0,0) would correspond to the upper left corner).
+ // We can't simply use `clientX` and `clientY` because the given MouseEvent
+ // object may be generated from events coming from other DOM nodes.
+ // Therefore, we need to get a bounding box relative to the top document and
+ // do some simple math to convert screen coords into local coords.
+ // However, `getBoxQuads` may be a very costly operation depending on the
+ // complexity of the "outside world" DOM, so cache the results until we
+ // suspect they might change (e.g. on a resize).
+ // It'd sure be nice if we could use `getBoundsWithoutFlushing`, but it's
+ // not taking the document zoom factor into consideration consistently.
+ if (!this._boundingBox || this._maybeDirtyBoundingBox) {
+ let topDocument = this._topWindow.document;
+ let boxQuad = this._canvas.getBoxQuads({ relativeTo: topDocument })[0];
+ this._boundingBox = boxQuad;
+ this._maybeDirtyBoundingBox = false;
+ }
+
+ let bb = this._boundingBox;
+ let x = (e.screenX - this._topWindow.screenX) - bb.p1.x;
+ let y = (e.screenY - this._topWindow.screenY) - bb.p1.y;
+
+ // Don't allow the event coordinates to be bigger than the canvas
+ // or less than 0.
+ let maxX = bb.p2.x - bb.p1.x;
+ let maxY = bb.p3.y - bb.p1.y;
+ let mouseX = Math.max(0, Math.min(x, maxX)) * this._pixelRatio;
+ let mouseY = Math.max(0, Math.min(y, maxY)) * this._pixelRatio;
+
+ // The coordinates need to be modified with the current zoom level
+ // to prevent them from being wrong.
+ let zoom = getCurrentZoom(this._canvas);
+ mouseX /= zoom;
+ mouseY /= zoom;
+
+ return {mouseX, mouseY};
+ },
+
+ /**
+ * Listener for the "mousemove" event on the graph's container.
+ */
+ _onMouseMove: function (e) {
+ let resizer = this._selectionResizer;
+ let dragger = this._selectionDragger;
+
+ // Need to stop propagation here, since this function can be bound
+ // to both this._window and this._topWindow. It's only attached to
+ // this._topWindow during a drag event. Null check here since tests
+ // don't pass this method into the event object.
+ if (e.stopPropagation && this._isMouseActive) {
+ e.stopPropagation();
+ }
+
+ // If a mouseup happened outside the window and the current operation
+ // is causing the selection to change, then end it.
+ if (e.buttons == 0 && (this.hasSelectionInProgress() ||
+ resizer.margin != null ||
+ dragger.origin != null)) {
+ this._onMouseUp();
+ return;
+ }
+
+ let {mouseX, mouseY} = this._getRelativeEventCoordinates(e);
+ this._cursor.x = mouseX;
+ this._cursor.y = mouseY;
+
+ if (resizer.margin != null) {
+ this._selection[resizer.margin] = mouseX;
+ this._shouldRedraw = true;
+ this.emit("selecting");
+ return;
+ }
+
+ if (dragger.origin != null) {
+ this._selection.start = dragger.anchor.start - dragger.origin + mouseX;
+ this._selection.end = dragger.anchor.end - dragger.origin + mouseX;
+ this._shouldRedraw = true;
+ this.emit("selecting");
+ return;
+ }
+
+ if (this.hasSelectionInProgress()) {
+ this._shouldRedraw = true;
+ this.emit("selecting");
+ return;
+ }
+
+ if (this.hasSelection()) {
+ if (this._isHoveringStartBoundary()) {
+ this._canvas.setAttribute("input", "hovering-selection-start-boundary");
+ this._shouldRedraw = true;
+ return;
+ }
+ if (this._isHoveringEndBoundary()) {
+ this._canvas.setAttribute("input", "hovering-selection-end-boundary");
+ this._shouldRedraw = true;
+ return;
+ }
+ if (this._isHoveringSelectionContents()) {
+ this._canvas.setAttribute("input", "hovering-selection-contents");
+ this._shouldRedraw = true;
+ return;
+ }
+ }
+
+ let region = this.getHoveredRegion();
+ if (region) {
+ this._canvas.setAttribute("input", "hovering-region");
+ } else {
+ this._canvas.setAttribute("input", "hovering-background");
+ }
+
+ this._shouldRedraw = true;
+ },
+
+ /**
+ * Listener for the "mousedown" event on the graph's container.
+ */
+ _onMouseDown: function (e) {
+ this._isMouseActive = true;
+ let {mouseX} = this._getRelativeEventCoordinates(e);
+
+ switch (this._canvas.getAttribute("input")) {
+ case "hovering-background":
+ case "hovering-region":
+ if (!this.selectionEnabled) {
+ break;
+ }
+ this._selection.start = mouseX;
+ this._selection.end = null;
+ this.emit("selecting");
+ break;
+
+ case "hovering-selection-start-boundary":
+ this._selectionResizer.margin = "start";
+ break;
+
+ case "hovering-selection-end-boundary":
+ this._selectionResizer.margin = "end";
+ break;
+
+ case "hovering-selection-contents":
+ this._selectionDragger.origin = mouseX;
+ this._selectionDragger.anchor.start = this._selection.start;
+ this._selectionDragger.anchor.end = this._selection.end;
+ this._canvas.setAttribute("input", "dragging-selection-contents");
+ break;
+ }
+
+ // During a drag, bind to the top level window so that mouse movement
+ // outside of this frame will still work.
+ this._topWindow.addEventListener("mousemove", this._onMouseMove);
+ this._topWindow.addEventListener("mouseup", this._onMouseUp);
+
+ this._shouldRedraw = true;
+ this.emit("mousedown");
+ },
+
+ /**
+ * Listener for the "mouseup" event on the graph's container.
+ */
+ _onMouseUp: function () {
+ this._isMouseActive = false;
+ switch (this._canvas.getAttribute("input")) {
+ case "hovering-background":
+ case "hovering-region":
+ if (!this.selectionEnabled) {
+ break;
+ }
+ if (this.getSelectionWidth() < 1) {
+ let region = this.getHoveredRegion();
+ if (region) {
+ this._selection.start = region.start;
+ this._selection.end = region.end;
+ this.emit("selecting");
+ } else {
+ this._selection.start = null;
+ this._selection.end = null;
+ this.emit("deselecting");
+ }
+ } else {
+ this._selection.end = this._cursor.x;
+ this.emit("selecting");
+ }
+ break;
+
+ case "hovering-selection-start-boundary":
+ case "hovering-selection-end-boundary":
+ this._selectionResizer.margin = null;
+ break;
+
+ case "dragging-selection-contents":
+ this._selectionDragger.origin = null;
+ this._canvas.setAttribute("input", "hovering-selection-contents");
+ break;
+ }
+
+ // No longer dragging, no need to bind to the top level window.
+ this._topWindow.removeEventListener("mousemove", this._onMouseMove);
+ this._topWindow.removeEventListener("mouseup", this._onMouseUp);
+
+ this._shouldRedraw = true;
+ this.emit("mouseup");
+ },
+
+ /**
+ * Listener for the "wheel" event on the graph's container.
+ */
+ _onMouseWheel: function (e) {
+ if (!this.hasSelection()) {
+ return;
+ }
+
+ let {mouseX} = this._getRelativeEventCoordinates(e);
+ let focusX = mouseX;
+
+ let selection = this._selection;
+ let vector = 0;
+
+ // If the selection is hovered, "zoom" towards or away the cursor,
+ // by shrinking or growing the selection.
+ if (this._isHoveringSelectionContentsOrBoundaries()) {
+ let distStart = selection.start - focusX;
+ let distEnd = selection.end - focusX;
+ vector = e.detail * GRAPH_WHEEL_ZOOM_SENSITIVITY;
+ selection.start = selection.start + distStart * vector;
+ selection.end = selection.end + distEnd * vector;
+ } else {
+ // Otherwise, simply pan the selection towards the left or right.
+ let direction = 0;
+ if (focusX > selection.end) {
+ direction = Math.sign(focusX - selection.end);
+ } else if (focusX < selection.start) {
+ direction = Math.sign(focusX - selection.start);
+ }
+ vector = direction * e.detail * GRAPH_WHEEL_SCROLL_SENSITIVITY;
+ selection.start -= vector;
+ selection.end -= vector;
+ }
+
+ // Make sure the selection bounds are still comfortably inside the
+ // graph's bounds when zooming out, to keep the margin handles accessible.
+
+ let minStart = GRAPH_MAX_SELECTION_LEFT_PADDING;
+ let maxEnd = this._width - GRAPH_MAX_SELECTION_RIGHT_PADDING;
+ if (selection.start < minStart) {
+ selection.start = minStart;
+ }
+ if (selection.start > maxEnd) {
+ selection.start = maxEnd;
+ }
+ if (selection.end < minStart) {
+ selection.end = minStart;
+ }
+ if (selection.end > maxEnd) {
+ selection.end = maxEnd;
+ }
+
+ // Make sure the selection doesn't get too narrow when zooming in.
+
+ let thickness = Math.abs(selection.start - selection.end);
+ if (thickness < GRAPH_WHEEL_MIN_SELECTION_WIDTH) {
+ let midPoint = (selection.start + selection.end) / 2;
+ selection.start = midPoint - GRAPH_WHEEL_MIN_SELECTION_WIDTH / 2;
+ selection.end = midPoint + GRAPH_WHEEL_MIN_SELECTION_WIDTH / 2;
+ }
+
+ this._shouldRedraw = true;
+ this.emit("selecting");
+ this.emit("scroll");
+ },
+
+ /**
+ * Listener for the "mouseout" event on the graph's container.
+ * Clear any active cursors if a drag isn't happening.
+ */
+ _onMouseOut: function (e) {
+ if (!this._isMouseActive) {
+ this._cursor.x = null;
+ this._cursor.y = null;
+ this._canvas.removeAttribute("input");
+ this._shouldRedraw = true;
+ }
+ },
+
+ /**
+ * Listener for the "resize" event on the graph's parent node.
+ */
+ _onResize: function () {
+ if (this.hasData()) {
+ // The assumption is that resize events may change the outside world
+ // layout in a way that affects this graph's bounding box location
+ // relative to the top window's document. Graphs aren't currently
+ // (or ever) expected to move around on their own.
+ this._maybeDirtyBoundingBox = true;
+ setNamedTimeout(this._uid, GRAPH_RESIZE_EVENTS_DRAIN, this.refresh);
+ }
+ }
+};
+
+// Helper functions.
+
+/**
+ * Creates an iframe element with the provided source URL, appends it to
+ * the specified node and invokes the callback once the content is loaded.
+ *
+ * @param string url
+ * The desired source URL for the iframe.
+ * @param nsIDOMNode parent
+ * The desired parent node for the iframe.
+ * @param function callback
+ * Invoked once the content is loaded, with the iframe as an argument.
+ */
+AbstractCanvasGraph.createIframe = function (url, parent, callback) {
+ let iframe = parent.ownerDocument.createElementNS(HTML_NS, "iframe");
+
+ iframe.addEventListener("DOMContentLoaded", function onLoad() {
+ iframe.removeEventListener("DOMContentLoaded", onLoad);
+ callback(iframe);
+ });
+
+ // Setting 100% width on the frame and flex on the parent allows the graph
+ // to properly shrink when the window is resized to be smaller.
+ iframe.setAttribute("frameborder", "0");
+ iframe.style.width = "100%";
+ iframe.style.minWidth = "50px";
+ iframe.src = url;
+
+ parent.style.display = "flex";
+ parent.appendChild(iframe);
+};
+
+/**
+ * Gets a striped pattern used as a background in selections and regions.
+ *
+ * @param object data
+ * The following properties are required:
+ * - ownerDocument: the nsIDocumentElement owning the canvas
+ * - backgroundColor: a string representing the fill style
+ * - stripesColor: a string representing the stroke style
+ * @return nsIDOMCanvasPattern
+ * The custom striped pattern.
+ */
+AbstractCanvasGraph.getStripePattern = function (data) {
+ let { ownerDocument, backgroundColor, stripesColor } = data;
+ let id = [backgroundColor, stripesColor].join(",");
+
+ if (gCachedStripePattern.has(id)) {
+ return gCachedStripePattern.get(id);
+ }
+
+ let canvas = ownerDocument.createElementNS(HTML_NS, "canvas");
+ let ctx = canvas.getContext("2d");
+ let width = canvas.width = GRAPH_STRIPE_PATTERN_WIDTH;
+ let height = canvas.height = GRAPH_STRIPE_PATTERN_HEIGHT;
+
+ ctx.fillStyle = backgroundColor;
+ ctx.fillRect(0, 0, width, height);
+
+ let pixelRatio = ownerDocument.defaultView.devicePixelRatio;
+ let scaledLineWidth = GRAPH_STRIPE_PATTERN_LINE_WIDTH * pixelRatio;
+ let scaledLineSpacing = GRAPH_STRIPE_PATTERN_LINE_SPACING * pixelRatio;
+
+ ctx.strokeStyle = stripesColor;
+ ctx.lineWidth = scaledLineWidth;
+ ctx.lineCap = "square";
+ ctx.beginPath();
+
+ for (let i = -height; i <= height; i += scaledLineSpacing) {
+ ctx.moveTo(width, i);
+ ctx.lineTo(0, i + height);
+ }
+
+ ctx.stroke();
+
+ let pattern = ctx.createPattern(canvas, "repeat");
+ gCachedStripePattern.set(id, pattern);
+ return pattern;
+};
+
+/**
+ * Cache used by `AbstractCanvasGraph.getStripePattern`.
+ */
+const gCachedStripePattern = new Map();
+
+/**
+ * Utility functions for graph canvases.
+ */
+this.CanvasGraphUtils = {
+ _graphUtilsWorker: null,
+ _graphUtilsTaskId: 0,
+
+ /**
+ * Merges the animation loop of two graphs.
+ */
+ linkAnimation: Task.async(function* (graph1, graph2) {
+ if (!graph1 || !graph2) {
+ return;
+ }
+ yield graph1.ready();
+ yield graph2.ready();
+
+ let window = graph1._window;
+ window.cancelAnimationFrame(graph1._animationId);
+ window.cancelAnimationFrame(graph2._animationId);
+
+ let loop = () => {
+ window.requestAnimationFrame(loop);
+ graph1._drawWidget();
+ graph2._drawWidget();
+ };
+
+ window.requestAnimationFrame(loop);
+ }),
+
+ /**
+ * Makes sure selections in one graph are reflected in another.
+ */
+ linkSelection: function (graph1, graph2) {
+ if (!graph1 || !graph2) {
+ return;
+ }
+
+ if (graph1.hasSelection()) {
+ graph2.setSelection(graph1.getSelection());
+ } else {
+ graph2.dropSelection();
+ }
+
+ graph1.on("selecting", () => {
+ graph2.setSelection(graph1.getSelection());
+ });
+ graph2.on("selecting", () => {
+ graph1.setSelection(graph2.getSelection());
+ });
+ graph1.on("deselecting", () => {
+ graph2.dropSelection();
+ });
+ graph2.on("deselecting", () => {
+ graph1.dropSelection();
+ });
+ },
+
+ /**
+ * Performs the given task in a chrome worker, assuming it exists.
+ *
+ * @param string task
+ * The task name. Currently supported: "plotTimestampsGraph".
+ * @param any data
+ * Extra arguments to pass to the worker.
+ * @return object
+ * A promise that is resolved once the worker finishes the task.
+ */
+ _performTaskInWorker: function (task, data) {
+ let worker = this._graphUtilsWorker || new DevToolsWorker(WORKER_URL);
+ return worker.performTask(task, data);
+ }
+};
+
+/**
+ * Maps a value from one range to another.
+ * @param number value, istart, istop, ostart, ostop
+ * @return number
+ */
+function map(value, istart, istop, ostart, ostop) {
+ let ratio = istop - istart;
+ if (ratio == 0) {
+ return value;
+ }
+ return ostart + (ostop - ostart) * ((value - istart) / ratio);
+}
+
+/**
+ * Constrains a value to a range.
+ * @param number value, min, max
+ * @return number
+ */
+function clamp(value, min, max) {
+ if (value < min) {
+ return min;
+ }
+ if (value > max) {
+ return max;
+ }
+ return value;
+}
+
+exports.GraphCursor = GraphCursor;
+exports.GraphArea = GraphArea;
+exports.GraphAreaDragger = GraphAreaDragger;
+exports.GraphAreaResizer = GraphAreaResizer;
+exports.AbstractCanvasGraph = AbstractCanvasGraph;
+exports.CanvasGraphUtils = CanvasGraphUtils;
+exports.CanvasGraphUtils.map = map;
+exports.CanvasGraphUtils.clamp = clamp;
diff --git a/devtools/client/shared/widgets/GraphsWorker.js b/devtools/client/shared/widgets/GraphsWorker.js
new file mode 100644
index 000000000..1e12f1d11
--- /dev/null
+++ b/devtools/client/shared/widgets/GraphsWorker.js
@@ -0,0 +1,103 @@
+/* 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";
+
+/* eslint-env worker */
+
+/**
+ * Import `createTask` to communicate with `devtools/shared/worker`.
+ */
+importScripts("resource://gre/modules/workers/require.js");
+const { createTask } = require("resource://devtools/shared/worker/helper.js");
+
+/**
+ * @see LineGraphWidget.prototype.setDataFromTimestamps in Graphs.js
+ * @param number id
+ * @param array timestamps
+ * @param number interval
+ * @param number duration
+ */
+createTask(self, "plotTimestampsGraph", function ({ timestamps,
+ interval, duration }) {
+ let plottedData = plotTimestamps(timestamps, interval);
+ let plottedMinMaxSum = getMinMaxAvg(plottedData, timestamps, duration);
+
+ return { plottedData, plottedMinMaxSum };
+});
+
+/**
+ * Gets the min, max and average of the values in an array.
+ * @param array source
+ * @param array timestamps
+ * @param number duration
+ * @return object
+ */
+function getMinMaxAvg(source, timestamps, duration) {
+ let totalFrames = timestamps.length;
+ let maxValue = Number.MIN_SAFE_INTEGER;
+ let minValue = Number.MAX_SAFE_INTEGER;
+ // Calculate the average by counting how many frames occurred
+ // in the duration of the recording, rather than average the frame points
+ // we have, as that weights higher FPS, as there'll be more timestamps for
+ // those values
+ let avgValue = totalFrames / (duration / 1000);
+
+ for (let { value } of source) {
+ maxValue = Math.max(value, maxValue);
+ minValue = Math.min(value, minValue);
+ }
+
+ return { minValue, maxValue, avgValue };
+}
+
+/**
+ * Takes a list of numbers and plots them on a line graph representing
+ * the rate of occurences in a specified interval.
+ *
+ * @param array timestamps
+ * A list of numbers representing time, ordered ascending. For example,
+ * this can be the raw data received from the framerate actor, which
+ * represents the elapsed time on each refresh driver tick.
+ * @param number interval
+ * The maximum amount of time to wait between calculations.
+ * @param number clamp
+ * The maximum allowed value.
+ * @return array
+ * A collection of { delta, value } objects representing the
+ * plotted value at every delta time.
+ */
+function plotTimestamps(timestamps, interval = 100, clamp = 60) {
+ let timeline = [];
+ let totalTicks = timestamps.length;
+
+ // If the refresh driver didn't get a chance to tick before the
+ // recording was stopped, assume rate was 0.
+ if (totalTicks == 0) {
+ timeline.push({ delta: 0, value: 0 });
+ timeline.push({ delta: interval, value: 0 });
+ return timeline;
+ }
+
+ let frameCount = 0;
+ let prevTime = +timestamps[0];
+
+ for (let i = 1; i < totalTicks; i++) {
+ let currTime = +timestamps[i];
+ frameCount++;
+
+ let elapsedTime = currTime - prevTime;
+ if (elapsedTime < interval) {
+ continue;
+ }
+
+ let rate = Math.min(1000 / (elapsedTime / frameCount), clamp);
+ timeline.push({ delta: prevTime, value: rate });
+ timeline.push({ delta: currTime, value: rate });
+
+ frameCount = 0;
+ prevTime = currTime;
+ }
+
+ return timeline;
+}
diff --git a/devtools/client/shared/widgets/LineGraphWidget.js b/devtools/client/shared/widgets/LineGraphWidget.js
new file mode 100644
index 000000000..12ca425ad
--- /dev/null
+++ b/devtools/client/shared/widgets/LineGraphWidget.js
@@ -0,0 +1,402 @@
+"use strict";
+
+const { Task } = require("devtools/shared/task");
+const { Heritage } = require("devtools/client/shared/widgets/view-helpers");
+const { AbstractCanvasGraph, CanvasGraphUtils } = require("devtools/client/shared/widgets/Graphs");
+const { LocalizationHelper } = require("devtools/shared/l10n");
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const L10N = new LocalizationHelper("devtools/client/locales/graphs.properties");
+
+// Line graph constants.
+
+const GRAPH_DAMPEN_VALUES_FACTOR = 0.85;
+// px
+const GRAPH_TOOLTIP_SAFE_BOUNDS = 8;
+const GRAPH_MIN_MAX_TOOLTIP_DISTANCE = 14;
+
+const GRAPH_BACKGROUND_COLOR = "#0088cc";
+// px
+const GRAPH_STROKE_WIDTH = 1;
+const GRAPH_STROKE_COLOR = "rgba(255,255,255,0.9)";
+// px
+const GRAPH_HELPER_LINES_DASH = [5];
+const GRAPH_HELPER_LINES_WIDTH = 1;
+const GRAPH_MAXIMUM_LINE_COLOR = "rgba(255,255,255,0.4)";
+const GRAPH_AVERAGE_LINE_COLOR = "rgba(255,255,255,0.7)";
+const GRAPH_MINIMUM_LINE_COLOR = "rgba(255,255,255,0.9)";
+const GRAPH_BACKGROUND_GRADIENT_START = "rgba(255,255,255,0.25)";
+const GRAPH_BACKGROUND_GRADIENT_END = "rgba(255,255,255,0.0)";
+
+const GRAPH_CLIPHEAD_LINE_COLOR = "#fff";
+const GRAPH_SELECTION_LINE_COLOR = "#fff";
+const GRAPH_SELECTION_BACKGROUND_COLOR = "rgba(44,187,15,0.25)";
+const GRAPH_SELECTION_STRIPES_COLOR = "rgba(255,255,255,0.1)";
+const GRAPH_REGION_BACKGROUND_COLOR = "transparent";
+const GRAPH_REGION_STRIPES_COLOR = "rgba(237,38,85,0.2)";
+
+/**
+ * A basic line graph, plotting values on a curve and adding helper lines
+ * and tooltips for maximum, average and minimum values.
+ *
+ * @see AbstractCanvasGraph for emitted events and other options.
+ *
+ * Example usage:
+ * let graph = new LineGraphWidget(node, "units");
+ * graph.once("ready", () => {
+ * graph.setData(src);
+ * });
+ *
+ * Data source format:
+ * [
+ * { delta: x1, value: y1 },
+ * { delta: x2, value: y2 },
+ * ...
+ * { delta: xn, value: yn }
+ * ]
+ * where each item in the array represents a point in the graph.
+ *
+ * @param nsIDOMNode parent
+ * The parent node holding the graph.
+ * @param object options [optional]
+ * `metric`: The metric displayed in the graph, e.g. "fps" or "bananas".
+ * `min`: Boolean whether to show the min tooltip/gutter/line (default: true)
+ * `max`: Boolean whether to show the max tooltip/gutter/line (default: true)
+ * `avg`: Boolean whether to show the avg tooltip/gutter/line (default: true)
+ */
+this.LineGraphWidget = function (parent, options = {}, ...args) {
+ let { metric, min, max, avg } = options;
+
+ this._showMin = min !== false;
+ this._showMax = max !== false;
+ this._showAvg = avg !== false;
+
+ AbstractCanvasGraph.apply(this, [parent, "line-graph", ...args]);
+
+ this.once("ready", () => {
+ // Create all gutters and tooltips incase the showing of min/max/avg
+ // are changed later
+ this._gutter = this._createGutter();
+ this._maxGutterLine = this._createGutterLine("maximum");
+ this._maxTooltip = this._createTooltip(
+ "maximum", "start", L10N.getStr("graphs.label.maximum"), metric
+ );
+ this._minGutterLine = this._createGutterLine("minimum");
+ this._minTooltip = this._createTooltip(
+ "minimum", "start", L10N.getStr("graphs.label.minimum"), metric
+ );
+ this._avgGutterLine = this._createGutterLine("average");
+ this._avgTooltip = this._createTooltip(
+ "average", "end", L10N.getStr("graphs.label.average"), metric
+ );
+ });
+};
+
+LineGraphWidget.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
+ backgroundColor: GRAPH_BACKGROUND_COLOR,
+ backgroundGradientStart: GRAPH_BACKGROUND_GRADIENT_START,
+ backgroundGradientEnd: GRAPH_BACKGROUND_GRADIENT_END,
+ strokeColor: GRAPH_STROKE_COLOR,
+ strokeWidth: GRAPH_STROKE_WIDTH,
+ maximumLineColor: GRAPH_MAXIMUM_LINE_COLOR,
+ averageLineColor: GRAPH_AVERAGE_LINE_COLOR,
+ minimumLineColor: GRAPH_MINIMUM_LINE_COLOR,
+ clipheadLineColor: GRAPH_CLIPHEAD_LINE_COLOR,
+ selectionLineColor: GRAPH_SELECTION_LINE_COLOR,
+ selectionBackgroundColor: GRAPH_SELECTION_BACKGROUND_COLOR,
+ selectionStripesColor: GRAPH_SELECTION_STRIPES_COLOR,
+ regionBackgroundColor: GRAPH_REGION_BACKGROUND_COLOR,
+ regionStripesColor: GRAPH_REGION_STRIPES_COLOR,
+
+ /**
+ * Optionally offsets the `delta` in the data source by this scalar.
+ */
+ dataOffsetX: 0,
+
+ /**
+ * Optionally uses this value instead of the last tick in the data source
+ * to compute the horizontal scaling.
+ */
+ dataDuration: 0,
+
+ /**
+ * The scalar used to multiply the graph values to leave some headroom.
+ */
+ dampenValuesFactor: GRAPH_DAMPEN_VALUES_FACTOR,
+
+ /**
+ * Specifies if min/max/avg tooltips have arrow handlers on their sides.
+ */
+ withTooltipArrows: true,
+
+ /**
+ * Specifies if min/max/avg tooltips are positioned based on the actual
+ * values, or just placed next to the graph corners.
+ */
+ withFixedTooltipPositions: false,
+
+ /**
+ * Takes a list of numbers and plots them on a line graph representing
+ * the rate of occurences in a specified interval. Useful for drawing
+ * framerate, for example, from a sequence of timestamps.
+ *
+ * @param array timestamps
+ * A list of numbers representing time, ordered ascending. For example,
+ * this can be the raw data received from the framerate actor, which
+ * represents the elapsed time on each refresh driver tick.
+ * @param number interval
+ * The maximum amount of time to wait between calculations.
+ * @param number duration
+ * The duration of the recording in milliseconds.
+ */
+ setDataFromTimestamps: Task.async(function* (timestamps, interval, duration) {
+ let {
+ plottedData,
+ plottedMinMaxSum
+ } = yield CanvasGraphUtils._performTaskInWorker("plotTimestampsGraph", {
+ timestamps, interval, duration
+ });
+
+ this._tempMinMaxSum = plottedMinMaxSum;
+ this.setData(plottedData);
+ }),
+
+ /**
+ * Renders the graph's data source.
+ * @see AbstractCanvasGraph.prototype.buildGraphImage
+ */
+ buildGraphImage: function () {
+ let { canvas, ctx } = this._getNamedCanvas("line-graph-data");
+ let width = this._width;
+ let height = this._height;
+
+ let totalTicks = this._data.length;
+ let firstTick = totalTicks ? this._data[0].delta : 0;
+ let lastTick = totalTicks ? this._data[totalTicks - 1].delta : 0;
+ let maxValue = Number.MIN_SAFE_INTEGER;
+ let minValue = Number.MAX_SAFE_INTEGER;
+ let avgValue = 0;
+
+ if (this._tempMinMaxSum) {
+ maxValue = this._tempMinMaxSum.maxValue;
+ minValue = this._tempMinMaxSum.minValue;
+ avgValue = this._tempMinMaxSum.avgValue;
+ } else {
+ let sumValues = 0;
+ for (let { value } of this._data) {
+ maxValue = Math.max(value, maxValue);
+ minValue = Math.min(value, minValue);
+ sumValues += value;
+ }
+ avgValue = sumValues / totalTicks;
+ }
+
+ let duration = this.dataDuration || lastTick;
+ let dataScaleX = this.dataScaleX = width / (duration - this.dataOffsetX);
+ let dataScaleY =
+ this.dataScaleY = height / maxValue * this.dampenValuesFactor;
+
+ // Draw the background.
+
+ ctx.fillStyle = this.backgroundColor;
+ ctx.fillRect(0, 0, width, height);
+
+ // Draw the graph.
+
+ let gradient = ctx.createLinearGradient(0, height / 2, 0, height);
+ gradient.addColorStop(0, this.backgroundGradientStart);
+ gradient.addColorStop(1, this.backgroundGradientEnd);
+ ctx.fillStyle = gradient;
+ ctx.strokeStyle = this.strokeColor;
+ ctx.lineWidth = this.strokeWidth * this._pixelRatio;
+ ctx.beginPath();
+
+ for (let { delta, value } of this._data) {
+ let currX = (delta - this.dataOffsetX) * dataScaleX;
+ let currY = height - value * dataScaleY;
+
+ if (delta == firstTick) {
+ ctx.moveTo(-GRAPH_STROKE_WIDTH, height);
+ ctx.lineTo(-GRAPH_STROKE_WIDTH, currY);
+ }
+
+ ctx.lineTo(currX, currY);
+
+ if (delta == lastTick) {
+ ctx.lineTo(width + GRAPH_STROKE_WIDTH, currY);
+ ctx.lineTo(width + GRAPH_STROKE_WIDTH, height);
+ }
+ }
+
+ ctx.fill();
+ ctx.stroke();
+
+ this._drawOverlays(ctx, minValue, maxValue, avgValue, dataScaleY);
+
+ return canvas;
+ },
+
+ /**
+ * Draws the min, max and average horizontal lines, along with their
+ * repsective tooltips.
+ *
+ * @param CanvasRenderingContext2D ctx
+ * @param number minValue
+ * @param number maxValue
+ * @param number avgValue
+ * @param number dataScaleY
+ */
+ _drawOverlays: function (ctx, minValue, maxValue, avgValue, dataScaleY) {
+ let width = this._width;
+ let height = this._height;
+ let totalTicks = this._data.length;
+
+ // Draw the maximum value horizontal line.
+ if (this._showMax) {
+ ctx.strokeStyle = this.maximumLineColor;
+ ctx.lineWidth = GRAPH_HELPER_LINES_WIDTH;
+ ctx.setLineDash(GRAPH_HELPER_LINES_DASH);
+ ctx.beginPath();
+ let maximumY = height - maxValue * dataScaleY;
+ ctx.moveTo(0, maximumY);
+ ctx.lineTo(width, maximumY);
+ ctx.stroke();
+ }
+
+ // Draw the average value horizontal line.
+ if (this._showAvg) {
+ ctx.strokeStyle = this.averageLineColor;
+ ctx.lineWidth = GRAPH_HELPER_LINES_WIDTH;
+ ctx.setLineDash(GRAPH_HELPER_LINES_DASH);
+ ctx.beginPath();
+ let averageY = height - avgValue * dataScaleY;
+ ctx.moveTo(0, averageY);
+ ctx.lineTo(width, averageY);
+ ctx.stroke();
+ }
+
+ // Draw the minimum value horizontal line.
+ if (this._showMin) {
+ ctx.strokeStyle = this.minimumLineColor;
+ ctx.lineWidth = GRAPH_HELPER_LINES_WIDTH;
+ ctx.setLineDash(GRAPH_HELPER_LINES_DASH);
+ ctx.beginPath();
+ let minimumY = height - minValue * dataScaleY;
+ ctx.moveTo(0, minimumY);
+ ctx.lineTo(width, minimumY);
+ ctx.stroke();
+ }
+
+ // Update the tooltips text and gutter lines.
+
+ this._maxTooltip.querySelector("[text=value]").textContent =
+ L10N.numberWithDecimals(maxValue, 2);
+ this._avgTooltip.querySelector("[text=value]").textContent =
+ L10N.numberWithDecimals(avgValue, 2);
+ this._minTooltip.querySelector("[text=value]").textContent =
+ L10N.numberWithDecimals(minValue, 2);
+
+ let bottom = height / this._pixelRatio;
+ let maxPosY = CanvasGraphUtils.map(maxValue * this.dampenValuesFactor, 0,
+ maxValue, bottom, 0);
+ let avgPosY = CanvasGraphUtils.map(avgValue * this.dampenValuesFactor, 0,
+ maxValue, bottom, 0);
+ let minPosY = CanvasGraphUtils.map(minValue * this.dampenValuesFactor, 0,
+ maxValue, bottom, 0);
+
+ let safeTop = GRAPH_TOOLTIP_SAFE_BOUNDS;
+ let safeBottom = bottom - GRAPH_TOOLTIP_SAFE_BOUNDS;
+
+ let maxTooltipTop = (this.withFixedTooltipPositions
+ ? safeTop : CanvasGraphUtils.clamp(maxPosY, safeTop, safeBottom));
+ let avgTooltipTop = (this.withFixedTooltipPositions
+ ? safeTop : CanvasGraphUtils.clamp(avgPosY, safeTop, safeBottom));
+ let minTooltipTop = (this.withFixedTooltipPositions
+ ? safeBottom : CanvasGraphUtils.clamp(minPosY, safeTop, safeBottom));
+
+ this._maxTooltip.style.top = maxTooltipTop + "px";
+ this._avgTooltip.style.top = avgTooltipTop + "px";
+ this._minTooltip.style.top = minTooltipTop + "px";
+
+ this._maxGutterLine.style.top = maxPosY + "px";
+ this._avgGutterLine.style.top = avgPosY + "px";
+ this._minGutterLine.style.top = minPosY + "px";
+
+ this._maxTooltip.setAttribute("with-arrows", this.withTooltipArrows);
+ this._avgTooltip.setAttribute("with-arrows", this.withTooltipArrows);
+ this._minTooltip.setAttribute("with-arrows", this.withTooltipArrows);
+
+ let distanceMinMax = Math.abs(maxTooltipTop - minTooltipTop);
+ this._maxTooltip.hidden = this._showMax === false
+ || !totalTicks
+ || distanceMinMax < GRAPH_MIN_MAX_TOOLTIP_DISTANCE;
+ this._avgTooltip.hidden = this._showAvg === false || !totalTicks;
+ this._minTooltip.hidden = this._showMin === false || !totalTicks;
+ this._gutter.hidden = (this._showMin === false &&
+ this._showAvg === false &&
+ this._showMax === false) || !totalTicks;
+
+ this._maxGutterLine.hidden = this._showMax === false;
+ this._avgGutterLine.hidden = this._showAvg === false;
+ this._minGutterLine.hidden = this._showMin === false;
+ },
+
+ /**
+ * Creates the gutter node when constructing this graph.
+ * @return nsIDOMNode
+ */
+ _createGutter: function () {
+ let gutter = this._document.createElementNS(HTML_NS, "div");
+ gutter.className = "line-graph-widget-gutter";
+ gutter.setAttribute("hidden", true);
+ this._container.appendChild(gutter);
+
+ return gutter;
+ },
+
+ /**
+ * Creates the gutter line nodes when constructing this graph.
+ * @return nsIDOMNode
+ */
+ _createGutterLine: function (type) {
+ let line = this._document.createElementNS(HTML_NS, "div");
+ line.className = "line-graph-widget-gutter-line";
+ line.setAttribute("type", type);
+ this._gutter.appendChild(line);
+
+ return line;
+ },
+
+ /**
+ * Creates the tooltip nodes when constructing this graph.
+ * @return nsIDOMNode
+ */
+ _createTooltip: function (type, arrow, info, metric) {
+ let tooltip = this._document.createElementNS(HTML_NS, "div");
+ tooltip.className = "line-graph-widget-tooltip";
+ tooltip.setAttribute("type", type);
+ tooltip.setAttribute("arrow", arrow);
+ tooltip.setAttribute("hidden", true);
+
+ let infoNode = this._document.createElementNS(HTML_NS, "span");
+ infoNode.textContent = info;
+ infoNode.setAttribute("text", "info");
+
+ let valueNode = this._document.createElementNS(HTML_NS, "span");
+ valueNode.textContent = 0;
+ valueNode.setAttribute("text", "value");
+
+ let metricNode = this._document.createElementNS(HTML_NS, "span");
+ metricNode.textContent = metric;
+ metricNode.setAttribute("text", "metric");
+
+ tooltip.appendChild(infoNode);
+ tooltip.appendChild(valueNode);
+ tooltip.appendChild(metricNode);
+ this._container.appendChild(tooltip);
+
+ return tooltip;
+ }
+});
+
+module.exports = LineGraphWidget;
diff --git a/devtools/client/shared/widgets/MdnDocsWidget.js b/devtools/client/shared/widgets/MdnDocsWidget.js
new file mode 100644
index 000000000..6a26b05c8
--- /dev/null
+++ b/devtools/client/shared/widgets/MdnDocsWidget.js
@@ -0,0 +1,510 @@
+/* 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/. */
+
+/**
+ * This file contains functions to retrieve docs content from
+ * MDN (developer.mozilla.org) for particular items, and to display
+ * the content in a tooltip.
+ *
+ * At the moment it only supports fetching content for CSS properties,
+ * but it might support other types of content in the future
+ * (Web APIs, for example).
+ *
+ * It's split into two parts:
+ *
+ * - functions like getCssDocs that just fetch content from MDN,
+ * without any constraints on what to do with the content. If you
+ * want to embed the content in some custom way, use this.
+ *
+ * - the MdnDocsWidget class, that manages and updates a tooltip
+ * document whose content is taken from MDN. If you want to embed
+ * the content in a tooltip, use this in conjunction with Tooltip.js.
+ */
+
+"use strict";
+
+const Services = require("Services");
+const defer = require("devtools/shared/defer");
+const {getCSSLexer} = require("devtools/shared/css/lexer");
+const EventEmitter = require("devtools/shared/event-emitter");
+const {gDevTools} = require("devtools/client/framework/devtools");
+
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper("devtools/client/locales/inspector.properties");
+
+const XHTML_NS = "http://www.w3.org/1999/xhtml";
+
+// Parameters for the XHR request
+// see https://developer.mozilla.org/en-US/docs/MDN/Kuma/API#Document_parameters
+const XHR_PARAMS = "?raw&macros";
+// URL for the XHR request
+var XHR_CSS_URL = "https://developer.mozilla.org/en-US/docs/Web/CSS/";
+
+// Parameters for the link to MDN in the tooltip, so
+// so we know which MDN visits come from this feature
+const PAGE_LINK_PARAMS =
+ "?utm_source=mozilla&utm_medium=firefox-inspector&utm_campaign=default";
+// URL for the page link omits locale, so a locale-specific page will be loaded
+var PAGE_LINK_URL = "https://developer.mozilla.org/docs/Web/CSS/";
+exports.PAGE_LINK_URL = PAGE_LINK_URL;
+
+const PROPERTY_NAME_COLOR = "theme-fg-color5";
+const PROPERTY_VALUE_COLOR = "theme-fg-color1";
+const COMMENT_COLOR = "theme-comment";
+
+/**
+ * Turns a string containing a series of CSS declarations into
+ * a series of DOM nodes, with classes applied to provide syntax
+ * highlighting.
+ *
+ * It uses the CSS tokenizer to generate a stream of CSS tokens.
+ * https://dxr.mozilla.org/mozilla-central/source/dom/webidl/CSSLexer.webidl
+ * lists all the token types.
+ *
+ * - "whitespace", "comment", and "symbol" tokens are appended as TEXT nodes,
+ * and will inherit the default style for text.
+ *
+ * - "ident" tokens that we think are property names are considered to be
+ * a property name, and are appended as SPAN nodes with a distinct color class.
+ *
+ * - "ident" nodes which we do not think are property names, and nodes
+ * of all other types ("number", "url", "percentage", ...) are considered
+ * to be part of a property value, and are appended as SPAN nodes with
+ * a different color class.
+ *
+ * @param {Document} doc
+ * Used to create nodes.
+ *
+ * @param {String} syntaxText
+ * The CSS input. This is assumed to consist of a series of
+ * CSS declarations, with trailing semicolons.
+ *
+ * @param {DOM node} syntaxSection
+ * This is the parent for the output nodes. Generated nodes
+ * are appended to this as children.
+ */
+function appendSyntaxHighlightedCSS(cssText, parentElement) {
+ let doc = parentElement.ownerDocument;
+ let identClass = PROPERTY_NAME_COLOR;
+ let lexer = getCSSLexer(cssText);
+
+ /**
+ * Create a SPAN node with the given text content and class.
+ */
+ function createStyledNode(textContent, className) {
+ let newNode = doc.createElementNS(XHTML_NS, "span");
+ newNode.classList.add(className);
+ newNode.textContent = textContent;
+ return newNode;
+ }
+
+ /**
+ * If the symbol is ":", we will expect the next
+ * "ident" token to be part of a property value.
+ *
+ * If the symbol is ";", we will expect the next
+ * "ident" token to be a property name.
+ */
+ function updateIdentClass(tokenText) {
+ if (tokenText === ":") {
+ identClass = PROPERTY_VALUE_COLOR;
+ } else if (tokenText === ";") {
+ identClass = PROPERTY_NAME_COLOR;
+ }
+ }
+
+ /**
+ * Create the appropriate node for this token type.
+ *
+ * If this token is a symbol, also update our expectations
+ * for what the next "ident" token represents.
+ */
+ function tokenToNode(token, tokenText) {
+ switch (token.tokenType) {
+ case "ident":
+ return createStyledNode(tokenText, identClass);
+ case "symbol":
+ updateIdentClass(tokenText);
+ return doc.createTextNode(tokenText);
+ case "whitespace":
+ return doc.createTextNode(tokenText);
+ case "comment":
+ return createStyledNode(tokenText, COMMENT_COLOR);
+ default:
+ return createStyledNode(tokenText, PROPERTY_VALUE_COLOR);
+ }
+ }
+
+ let token = lexer.nextToken();
+ while (token) {
+ let tokenText = cssText.slice(token.startOffset, token.endOffset);
+ let newNode = tokenToNode(token, tokenText);
+ parentElement.appendChild(newNode);
+ token = lexer.nextToken();
+ }
+}
+
+exports.appendSyntaxHighlightedCSS = appendSyntaxHighlightedCSS;
+
+/**
+ * Fetch an MDN page.
+ *
+ * @param {string} pageUrl
+ * URL of the page to fetch.
+ *
+ * @return {promise}
+ * The promise is resolved with the page as an XML document.
+ *
+ * The promise is rejected with an error message if
+ * we could not load the page.
+ */
+function getMdnPage(pageUrl) {
+ let deferred = defer();
+
+ let xhr = new XMLHttpRequest();
+
+ xhr.addEventListener("load", onLoaded, false);
+ xhr.addEventListener("error", onError, false);
+
+ xhr.open("GET", pageUrl);
+ xhr.responseType = "document";
+ xhr.send();
+
+ function onLoaded(e) {
+ if (xhr.status != 200) {
+ deferred.reject({page: pageUrl, status: xhr.status});
+ } else {
+ deferred.resolve(xhr.responseXML);
+ }
+ }
+
+ function onError(e) {
+ deferred.reject({page: pageUrl, status: xhr.status});
+ }
+
+ return deferred.promise;
+}
+
+/**
+ * Gets some docs for the given CSS property.
+ * Loads an MDN page for the property and gets some
+ * information about the property.
+ *
+ * @param {string} cssProperty
+ * The property for which we want docs.
+ *
+ * @return {promise}
+ * The promise is resolved with an object containing:
+ * - summary: a short summary of the property
+ * - syntax: some example syntax
+ *
+ * The promise is rejected with an error message if
+ * we could not load the page.
+ */
+function getCssDocs(cssProperty) {
+ let deferred = defer();
+ let pageUrl = XHR_CSS_URL + cssProperty + XHR_PARAMS;
+
+ getMdnPage(pageUrl).then(parseDocsFromResponse, handleRejection);
+
+ function parseDocsFromResponse(responseDocument) {
+ let theDocs = {};
+ theDocs.summary = getSummary(responseDocument);
+ theDocs.syntax = getSyntax(responseDocument);
+ if (theDocs.summary || theDocs.syntax) {
+ deferred.resolve(theDocs);
+ } else {
+ deferred.reject("Couldn't find the docs in the page.");
+ }
+ }
+
+ function handleRejection(e) {
+ deferred.reject(e.status);
+ }
+
+ return deferred.promise;
+}
+
+exports.getCssDocs = getCssDocs;
+
+/**
+ * The MdnDocsWidget is used by tooltip code that needs to display docs
+ * from MDN in a tooltip.
+ *
+ * In the constructor, the widget does some general setup that's not
+ * dependent on the particular item we need docs for.
+ *
+ * After that, when the tooltip code needs to display docs for an item, it
+ * asks the widget to retrieve the docs and update the document with them.
+ *
+ * @param {Element} tooltipContainer
+ * A DOM element where the MdnDocs widget markup should be created.
+ */
+function MdnDocsWidget(tooltipContainer) {
+ EventEmitter.decorate(this);
+
+ tooltipContainer.innerHTML =
+ `<header>
+ <h1 class="mdn-property-name theme-fg-color5"></h1>
+ </header>
+ <div class="mdn-property-info">
+ <div class="mdn-summary"></div>
+ <pre class="mdn-syntax devtools-monospace"></pre>
+ </div>
+ <footer>
+ <a class="mdn-visit-page theme-link" href="#">Visit MDN (placeholder)</a>
+ </footer>`;
+
+ // fetch all the bits of the document that we will manipulate later
+ this.elements = {
+ heading: tooltipContainer.querySelector(".mdn-property-name"),
+ summary: tooltipContainer.querySelector(".mdn-summary"),
+ syntax: tooltipContainer.querySelector(".mdn-syntax"),
+ info: tooltipContainer.querySelector(".mdn-property-info"),
+ linkToMdn: tooltipContainer.querySelector(".mdn-visit-page")
+ };
+
+ // get the localized string for the link text
+ this.elements.linkToMdn.textContent = L10N.getStr("docsTooltip.visitMDN");
+
+ // listen for clicks and open in the browser window instead
+ let mainWindow = Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
+ this.elements.linkToMdn.addEventListener("click", (e) => {
+ e.stopPropagation();
+ e.preventDefault();
+ mainWindow.openUILinkIn(e.target.href, "tab");
+ this.emit("visitlink");
+ });
+}
+
+exports.MdnDocsWidget = MdnDocsWidget;
+
+MdnDocsWidget.prototype = {
+ /**
+ * This is called just before the tooltip is displayed, and is
+ * passed the CSS property for which we want to display help.
+ *
+ * Its job is to make sure the document contains the docs
+ * content for that CSS property.
+ *
+ * First, it initializes the document, setting the things it can
+ * set synchronously, resetting the things it needs to get
+ * asynchronously, and making sure the throbber is throbbing.
+ *
+ * Then it tries to get the content asynchronously, updating
+ * the document with the content or with an error message.
+ *
+ * It returns immediately, so the caller can display the tooltip
+ * without waiting for the asynch operation to complete.
+ *
+ * @param {string} propertyName
+ * The name of the CSS property for which we need to display help.
+ */
+ loadCssDocs: function (propertyName) {
+ /**
+ * Do all the setup we can do synchronously, and get the document in
+ * a state where it can be displayed while we are waiting for the
+ * MDN docs content to be retrieved.
+ */
+ function initializeDocument(propName) {
+ // set property name heading
+ elements.heading.textContent = propName;
+
+ // set link target
+ elements.linkToMdn.setAttribute("href",
+ PAGE_LINK_URL + propName + PAGE_LINK_PARAMS);
+
+ // clear docs summary and syntax
+ elements.summary.textContent = "";
+ while (elements.syntax.firstChild) {
+ elements.syntax.firstChild.remove();
+ }
+
+ // reset the scroll position
+ elements.info.scrollTop = 0;
+ elements.info.scrollLeft = 0;
+
+ // show the throbber
+ elements.info.classList.add("devtools-throbber");
+ }
+
+ /**
+ * This is called if we successfully got the docs content.
+ * Finishes setting up the tooltip content, and disables the throbber.
+ */
+ function finalizeDocument({summary, syntax}) {
+ // set docs summary and syntax
+ elements.summary.textContent = summary;
+ appendSyntaxHighlightedCSS(syntax, elements.syntax);
+
+ // hide the throbber
+ elements.info.classList.remove("devtools-throbber");
+
+ deferred.resolve(this);
+ }
+
+ /**
+ * This is called if we failed to get the docs content.
+ * Sets the content to contain an error message, and disables the throbber.
+ */
+ function gotError(error) {
+ // show error message
+ elements.summary.textContent = L10N.getStr("docsTooltip.loadDocsError");
+
+ // hide the throbber
+ elements.info.classList.remove("devtools-throbber");
+
+ // although gotError is called when there's an error, we have handled
+ // the error, so call resolve not reject.
+ deferred.resolve(this);
+ }
+
+ let deferred = defer();
+ let elements = this.elements;
+
+ initializeDocument(propertyName);
+ getCssDocs(propertyName).then(finalizeDocument, gotError);
+
+ return deferred.promise;
+ },
+
+ destroy: function () {
+ this.elements = null;
+ }
+};
+
+/**
+ * Test whether a node is all whitespace.
+ *
+ * @return {boolean}
+ * True if the node all whitespace, otherwise false.
+ */
+function isAllWhitespace(node) {
+ return !(/[^\t\n\r ]/.test(node.textContent));
+}
+
+/**
+ * Test whether a node is a comment or whitespace node.
+ *
+ * @return {boolean}
+ * True if the node is a comment node or is all whitespace, otherwise false.
+ */
+function isIgnorable(node) {
+ // Comment nodes (8), text nodes (3) or whitespace
+ return (node.nodeType == 8) ||
+ ((node.nodeType == 3) && isAllWhitespace(node));
+}
+
+/**
+ * Get the next node, skipping comments and whitespace.
+ *
+ * @return {node}
+ * The next sibling node that is not a comment or whitespace, or null if
+ * there isn't one.
+ */
+function nodeAfter(sib) {
+ while ((sib = sib.nextSibling)) {
+ if (!isIgnorable(sib)) {
+ return sib;
+ }
+ }
+ return null;
+}
+
+/**
+ * Test whether the argument `node` is a node whose tag is `tagName`.
+ *
+ * @param {node} node
+ * The code to test. May be null.
+ *
+ * @param {string} tagName
+ * The tag name to test against.
+ *
+ * @return {boolean}
+ * True if the node is not null and has the tag name `tagName`,
+ * otherwise false.
+ */
+function hasTagName(node, tagName) {
+ return node && node.tagName &&
+ node.tagName.toLowerCase() == tagName.toLowerCase();
+}
+
+/**
+ * Given an MDN page, get the "summary" portion.
+ *
+ * This is the textContent of the first non-whitespace
+ * element in the #Summary section of the document.
+ *
+ * It's expected to be a <P> element.
+ *
+ * @param {Document} mdnDocument
+ * The document in which to look for the "summary" section.
+ *
+ * @return {string}
+ * The summary section as a string, or null if it could not be found.
+ */
+function getSummary(mdnDocument) {
+ let summary = mdnDocument.getElementById("Summary");
+ if (!hasTagName(summary, "H2")) {
+ return null;
+ }
+
+ let firstParagraph = nodeAfter(summary);
+ if (!hasTagName(firstParagraph, "P")) {
+ return null;
+ }
+
+ return firstParagraph.textContent;
+}
+
+/**
+ * Given an MDN page, get the "syntax" portion.
+ *
+ * First we get the #Syntax section of the document. The syntax
+ * section we want is somewhere inside there.
+ *
+ * If the page is in the old structure, then the *first two*
+ * non-whitespace elements in the #Syntax section will be <PRE>
+ * nodes, and the second of these will be the syntax section.
+ *
+ * If the page is in the new structure, then the only the *first*
+ * non-whitespace element in the #Syntax section will be a <PRE>
+ * node, and it will be the syntax section.
+ *
+ * @param {Document} mdnDocument
+ * The document in which to look for the "syntax" section.
+ *
+ * @return {string}
+ * The syntax section as a string, or null if it could not be found.
+ */
+function getSyntax(mdnDocument) {
+ let syntax = mdnDocument.getElementById("Syntax");
+ if (!hasTagName(syntax, "H2")) {
+ return null;
+ }
+
+ let firstParagraph = nodeAfter(syntax);
+ if (!hasTagName(firstParagraph, "PRE")) {
+ return null;
+ }
+
+ let secondParagraph = nodeAfter(firstParagraph);
+ if (hasTagName(secondParagraph, "PRE")) {
+ return secondParagraph.textContent;
+ }
+ return firstParagraph.textContent;
+}
+
+/**
+ * Use a different URL for CSS docs pages. Used only for testing.
+ *
+ * @param {string} baseUrl
+ * The baseURL to use.
+ */
+function setBaseCssDocsUrl(baseUrl) {
+ PAGE_LINK_URL = baseUrl;
+ XHR_CSS_URL = baseUrl;
+}
+
+exports.setBaseCssDocsUrl = setBaseCssDocsUrl;
diff --git a/devtools/client/shared/widgets/MountainGraphWidget.js b/devtools/client/shared/widgets/MountainGraphWidget.js
new file mode 100644
index 000000000..394ac4584
--- /dev/null
+++ b/devtools/client/shared/widgets/MountainGraphWidget.js
@@ -0,0 +1,195 @@
+"use strict";
+
+const { Heritage } = require("devtools/client/shared/widgets/view-helpers");
+const { AbstractCanvasGraph } = require("devtools/client/shared/widgets/Graphs");
+
+// Bar graph constants.
+
+const GRAPH_DAMPEN_VALUES_FACTOR = 0.9;
+
+const GRAPH_BACKGROUND_COLOR = "#ddd";
+// px
+const GRAPH_STROKE_WIDTH = 1;
+const GRAPH_STROKE_COLOR = "rgba(255,255,255,0.9)";
+// px
+const GRAPH_HELPER_LINES_DASH = [5];
+const GRAPH_HELPER_LINES_WIDTH = 1;
+
+const GRAPH_CLIPHEAD_LINE_COLOR = "#fff";
+const GRAPH_SELECTION_LINE_COLOR = "#fff";
+const GRAPH_SELECTION_BACKGROUND_COLOR = "rgba(44,187,15,0.25)";
+const GRAPH_SELECTION_STRIPES_COLOR = "rgba(255,255,255,0.1)";
+const GRAPH_REGION_BACKGROUND_COLOR = "transparent";
+const GRAPH_REGION_STRIPES_COLOR = "rgba(237,38,85,0.2)";
+
+/**
+ * A mountain graph, plotting sets of values as line graphs.
+ *
+ * @see AbstractCanvasGraph for emitted events and other options.
+ *
+ * Example usage:
+ * let graph = new MountainGraphWidget(node);
+ * graph.format = ...;
+ * graph.once("ready", () => {
+ * graph.setData(src);
+ * });
+ *
+ * The `graph.format` traits are mandatory and will determine how each
+ * section of the moutain will be styled:
+ * [
+ * { color: "#f00", ... },
+ * { color: "#0f0", ... },
+ * ...
+ * { color: "#00f", ... }
+ * ]
+ *
+ * Data source format:
+ * [
+ * { delta: x1, values: [y11, y12, ... y1n] },
+ * { delta: x2, values: [y21, y22, ... y2n] },
+ * ...
+ * { delta: xm, values: [ym1, ym2, ... ymn] }
+ * ]
+ * where the [ymn] values is assumed to aready be normalized from [0..1].
+ *
+ * @param nsIDOMNode parent
+ * The parent node holding the graph.
+ */
+this.MountainGraphWidget = function (parent, ...args) {
+ AbstractCanvasGraph.apply(this, [parent, "mountain-graph", ...args]);
+};
+
+MountainGraphWidget.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
+ backgroundColor: GRAPH_BACKGROUND_COLOR,
+ strokeColor: GRAPH_STROKE_COLOR,
+ strokeWidth: GRAPH_STROKE_WIDTH,
+ clipheadLineColor: GRAPH_CLIPHEAD_LINE_COLOR,
+ selectionLineColor: GRAPH_SELECTION_LINE_COLOR,
+ selectionBackgroundColor: GRAPH_SELECTION_BACKGROUND_COLOR,
+ selectionStripesColor: GRAPH_SELECTION_STRIPES_COLOR,
+ regionBackgroundColor: GRAPH_REGION_BACKGROUND_COLOR,
+ regionStripesColor: GRAPH_REGION_STRIPES_COLOR,
+
+ /**
+ * List of rules used to style each section of the mountain.
+ * @see constructor
+ * @type array
+ */
+ format: null,
+
+ /**
+ * Optionally offsets the `delta` in the data source by this scalar.
+ */
+ dataOffsetX: 0,
+
+ /**
+ * Optionally uses this value instead of the last tick in the data source
+ * to compute the horizontal scaling.
+ */
+ dataDuration: 0,
+
+ /**
+ * The scalar used to multiply the graph values to leave some headroom
+ * on the top.
+ */
+ dampenValuesFactor: GRAPH_DAMPEN_VALUES_FACTOR,
+
+ /**
+ * Renders the graph's background.
+ * @see AbstractCanvasGraph.prototype.buildBackgroundImage
+ */
+ buildBackgroundImage: function () {
+ let { canvas, ctx } = this._getNamedCanvas("mountain-graph-background");
+ let width = this._width;
+ let height = this._height;
+
+ ctx.fillStyle = this.backgroundColor;
+ ctx.fillRect(0, 0, width, height);
+
+ return canvas;
+ },
+
+ /**
+ * Renders the graph's data source.
+ * @see AbstractCanvasGraph.prototype.buildGraphImage
+ */
+ buildGraphImage: function () {
+ if (!this.format || !this.format.length) {
+ throw new Error("The graph format traits are mandatory to style " +
+ "the data source.");
+ }
+ let { canvas, ctx } = this._getNamedCanvas("mountain-graph-data");
+ let width = this._width;
+ let height = this._height;
+
+ let totalSections = this.format.length;
+ let totalTicks = this._data.length;
+ let firstTick = totalTicks ? this._data[0].delta : 0;
+ let lastTick = totalTicks ? this._data[totalTicks - 1].delta : 0;
+
+ let duration = this.dataDuration || lastTick;
+ let dataScaleX = this.dataScaleX = width / (duration - this.dataOffsetX);
+ let dataScaleY = this.dataScaleY = height * this.dampenValuesFactor;
+
+ // Draw the graph.
+
+ let prevHeights = Array.from({ length: totalTicks }).fill(0);
+
+ ctx.globalCompositeOperation = "destination-over";
+ ctx.strokeStyle = this.strokeColor;
+ ctx.lineWidth = this.strokeWidth * this._pixelRatio;
+
+ for (let section = 0; section < totalSections; section++) {
+ ctx.fillStyle = this.format[section].color || "#000";
+ ctx.beginPath();
+
+ for (let tick = 0; tick < totalTicks; tick++) {
+ let { delta, values } = this._data[tick];
+ let currX = (delta - this.dataOffsetX) * dataScaleX;
+ let currY = values[section] * dataScaleY;
+ let prevY = prevHeights[tick];
+
+ if (delta == firstTick) {
+ ctx.moveTo(-GRAPH_STROKE_WIDTH, height);
+ ctx.lineTo(-GRAPH_STROKE_WIDTH, height - currY - prevY);
+ }
+
+ ctx.lineTo(currX, height - currY - prevY);
+
+ if (delta == lastTick) {
+ ctx.lineTo(width + GRAPH_STROKE_WIDTH, height - currY - prevY);
+ ctx.lineTo(width + GRAPH_STROKE_WIDTH, height);
+ }
+
+ prevHeights[tick] += currY;
+ }
+
+ ctx.fill();
+ ctx.stroke();
+ }
+
+ ctx.globalCompositeOperation = "source-over";
+ ctx.lineWidth = GRAPH_HELPER_LINES_WIDTH;
+ ctx.setLineDash(GRAPH_HELPER_LINES_DASH);
+
+ // Draw the maximum value horizontal line.
+
+ ctx.beginPath();
+ let maximumY = height * this.dampenValuesFactor;
+ ctx.moveTo(0, maximumY);
+ ctx.lineTo(width, maximumY);
+ ctx.stroke();
+
+ // Draw the average value horizontal line.
+
+ ctx.beginPath();
+ let averageY = height / 2 * this.dampenValuesFactor;
+ ctx.moveTo(0, averageY);
+ ctx.lineTo(width, averageY);
+ ctx.stroke();
+
+ return canvas;
+ }
+});
+
+module.exports = MountainGraphWidget;
diff --git a/devtools/client/shared/widgets/SideMenuWidget.jsm b/devtools/client/shared/widgets/SideMenuWidget.jsm
new file mode 100644
index 000000000..0c132f232
--- /dev/null
+++ b/devtools/client/shared/widgets/SideMenuWidget.jsm
@@ -0,0 +1,725 @@
+/* -*- 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 Ci = Components.interfaces;
+const Cu = Components.utils;
+
+const SHARED_STRINGS_URI = "devtools/client/locales/shared.properties";
+
+const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const EventEmitter = require("devtools/shared/event-emitter");
+const { LocalizationHelper } = require("devtools/shared/l10n");
+const { ViewHelpers } = require("devtools/client/shared/widgets/view-helpers");
+
+this.EXPORTED_SYMBOLS = ["SideMenuWidget"];
+
+/**
+ * Localization convenience methods.
+ */
+var L10N = new LocalizationHelper(SHARED_STRINGS_URI);
+
+/**
+ * A simple side menu, with the ability of grouping menu items.
+ *
+ * Note: this widget should be used in tandem with the WidgetMethods in
+ * view-helpers.js.
+ *
+ * @param nsIDOMNode aNode
+ * The element associated with the widget.
+ * @param Object aOptions
+ * - contextMenu: optional element or element ID that serves as a context menu.
+ * - showArrows: specifies if items should display horizontal arrows.
+ * - showItemCheckboxes: specifies if items should display checkboxes.
+ * - showGroupCheckboxes: specifies if groups should display checkboxes.
+ */
+this.SideMenuWidget = function SideMenuWidget(aNode, aOptions = {}) {
+ this.document = aNode.ownerDocument;
+ this.window = this.document.defaultView;
+ this._parent = aNode;
+
+ let { contextMenu, showArrows, showItemCheckboxes, showGroupCheckboxes } = aOptions;
+ this._contextMenu = contextMenu || null;
+ this._showArrows = showArrows || false;
+ this._showItemCheckboxes = showItemCheckboxes || false;
+ this._showGroupCheckboxes = showGroupCheckboxes || false;
+
+ // Create an internal scrollbox container.
+ this._list = this.document.createElement("scrollbox");
+ this._list.className = "side-menu-widget-container theme-sidebar";
+ this._list.setAttribute("flex", "1");
+ this._list.setAttribute("orient", "vertical");
+ this._list.setAttribute("with-arrows", this._showArrows);
+ this._list.setAttribute("with-item-checkboxes", this._showItemCheckboxes);
+ this._list.setAttribute("with-group-checkboxes", this._showGroupCheckboxes);
+ this._list.setAttribute("tabindex", "0");
+ this._list.addEventListener("contextmenu", e => this._showContextMenu(e), false);
+ this._list.addEventListener("keypress", e => this.emit("keyPress", e), false);
+ this._list.addEventListener("mousedown", e => this.emit("mousePress", e), false);
+ this._parent.appendChild(this._list);
+
+ // Menu items can optionally be grouped.
+ this._groupsByName = new Map(); // Can't use a WeakMap because keys are strings.
+ this._orderedGroupElementsArray = [];
+ this._orderedMenuElementsArray = [];
+ this._itemsByElement = new Map();
+
+ // This widget emits events that can be handled in a MenuContainer.
+ EventEmitter.decorate(this);
+
+ // Delegate some of the associated node's methods to satisfy the interface
+ // required by MenuContainer instances.
+ ViewHelpers.delegateWidgetAttributeMethods(this, aNode);
+ ViewHelpers.delegateWidgetEventMethods(this, aNode);
+};
+
+SideMenuWidget.prototype = {
+ /**
+ * Specifies if groups in this container should be sorted.
+ */
+ sortedGroups: true,
+
+ /**
+ * The comparator used to sort groups.
+ */
+ groupSortPredicate: (a, b) => a.localeCompare(b),
+
+ /**
+ * Inserts an item in this container at the specified index, optionally
+ * grouping by name.
+ *
+ * @param number aIndex
+ * The position in the container intended for this item.
+ * @param nsIDOMNode aContents
+ * The node displayed in the container.
+ * @param object aAttachment [optional]
+ * Some attached primitive/object. Custom options supported:
+ * - group: a string specifying the group to place this item into
+ * - checkboxState: the checked state of the checkbox, if shown
+ * - checkboxTooltip: the tooltip text for the checkbox, if shown
+ * @return nsIDOMNode
+ * The element associated with the displayed item.
+ */
+ insertItemAt: function (aIndex, aContents, aAttachment = {}) {
+ let group = this._getMenuGroupForName(aAttachment.group);
+ let item = this._getMenuItemForGroup(group, aContents, aAttachment);
+ let element = item.insertSelfAt(aIndex);
+
+ return element;
+ },
+
+ /**
+ * Checks to see if the list is scrolled all the way to the bottom.
+ * Uses getBoundsWithoutFlushing to limit the performance impact
+ * of this function.
+ *
+ * @return bool
+ */
+ isScrolledToBottom: function () {
+ if (this._list.lastElementChild) {
+ let utils = this.window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ let childRect = utils.getBoundsWithoutFlushing(this._list.lastElementChild);
+ let listRect = utils.getBoundsWithoutFlushing(this._list);
+
+ // Cheap way to check if it's scrolled all the way to the bottom.
+ return (childRect.height + childRect.top) <= listRect.bottom;
+ }
+
+ return false;
+ },
+
+ /**
+ * Scroll the list to the bottom after a timeout.
+ * If the user scrolls in the meantime, cancel this operation.
+ */
+ scrollToBottom: function () {
+ this._list.scrollTop = this._list.scrollHeight;
+ this.emit("scroll-to-bottom");
+ },
+
+ /**
+ * Returns the child node in this container situated at the specified index.
+ *
+ * @param number aIndex
+ * The position in the container intended for this item.
+ * @return nsIDOMNode
+ * The element associated with the displayed item.
+ */
+ getItemAtIndex: function (aIndex) {
+ return this._orderedMenuElementsArray[aIndex];
+ },
+
+ /**
+ * Removes the specified child node from this container.
+ *
+ * @param nsIDOMNode aChild
+ * The element associated with the displayed item.
+ */
+ removeChild: function (aChild) {
+ this._getNodeForContents(aChild).remove();
+
+ this._orderedMenuElementsArray.splice(
+ this._orderedMenuElementsArray.indexOf(aChild), 1);
+
+ this._itemsByElement.delete(aChild);
+
+ if (this._selectedItem == aChild) {
+ this._selectedItem = null;
+ }
+ },
+
+ /**
+ * Removes all of the child nodes from this container.
+ */
+ removeAllItems: function () {
+ let parent = this._parent;
+ let list = this._list;
+
+ while (list.hasChildNodes()) {
+ list.firstChild.remove();
+ }
+
+ this._selectedItem = null;
+
+ this._groupsByName.clear();
+ this._orderedGroupElementsArray.length = 0;
+ this._orderedMenuElementsArray.length = 0;
+ this._itemsByElement.clear();
+ },
+
+ /**
+ * Gets the currently selected child node in this container.
+ * @return nsIDOMNode
+ */
+ get selectedItem() {
+ return this._selectedItem;
+ },
+
+ /**
+ * Sets the currently selected child node in this container.
+ * @param nsIDOMNode aChild
+ */
+ set selectedItem(aChild) {
+ let menuArray = this._orderedMenuElementsArray;
+
+ if (!aChild) {
+ this._selectedItem = null;
+ }
+ for (let node of menuArray) {
+ if (node == aChild) {
+ this._getNodeForContents(node).classList.add("selected");
+ this._selectedItem = node;
+ } else {
+ this._getNodeForContents(node).classList.remove("selected");
+ }
+ }
+ },
+
+ /**
+ * Ensures the specified element is visible.
+ *
+ * @param nsIDOMNode aElement
+ * The element to make visible.
+ */
+ ensureElementIsVisible: function (aElement) {
+ if (!aElement) {
+ return;
+ }
+
+ // Ensure the element is visible but not scrolled horizontally.
+ let boxObject = this._list.boxObject;
+ boxObject.ensureElementIsVisible(aElement);
+ boxObject.scrollBy(-this._list.clientWidth, 0);
+ },
+
+ /**
+ * Shows all the groups, even the ones with no visible children.
+ */
+ showEmptyGroups: function () {
+ for (let group of this._orderedGroupElementsArray) {
+ group.hidden = false;
+ }
+ },
+
+ /**
+ * Hides all the groups which have no visible children.
+ */
+ hideEmptyGroups: function () {
+ let visibleChildNodes = ".side-menu-widget-item-contents:not([hidden=true])";
+
+ for (let group of this._orderedGroupElementsArray) {
+ group.hidden = group.querySelectorAll(visibleChildNodes).length == 0;
+ }
+ for (let menuItem of this._orderedMenuElementsArray) {
+ menuItem.parentNode.hidden = menuItem.hidden;
+ }
+ },
+
+ /**
+ * Adds a new attribute or changes an existing attribute on this container.
+ *
+ * @param string aName
+ * The name of the attribute.
+ * @param string aValue
+ * The desired attribute value.
+ */
+ setAttribute: function (aName, aValue) {
+ this._parent.setAttribute(aName, aValue);
+
+ if (aName == "emptyText") {
+ this._textWhenEmpty = aValue;
+ }
+ },
+
+ /**
+ * Removes an attribute on this container.
+ *
+ * @param string aName
+ * The name of the attribute.
+ */
+ removeAttribute: function (aName) {
+ this._parent.removeAttribute(aName);
+
+ if (aName == "emptyText") {
+ this._removeEmptyText();
+ }
+ },
+
+ /**
+ * Set the checkbox state for the item associated with the given node.
+ *
+ * @param nsIDOMNode aNode
+ * The dom node for an item we want to check.
+ * @param boolean aCheckState
+ * True to check, false to uncheck.
+ */
+ checkItem: function (aNode, aCheckState) {
+ const widgetItem = this._itemsByElement.get(aNode);
+ if (!widgetItem) {
+ throw new Error("No item for " + aNode);
+ }
+ widgetItem.check(aCheckState);
+ },
+
+ /**
+ * Sets the text displayed in this container when empty.
+ * @param string aValue
+ */
+ set _textWhenEmpty(aValue) {
+ if (this._emptyTextNode) {
+ this._emptyTextNode.setAttribute("value", aValue);
+ }
+ this._emptyTextValue = aValue;
+ this._showEmptyText();
+ },
+
+ /**
+ * Creates and appends a label signaling that this container is empty.
+ */
+ _showEmptyText: function () {
+ if (this._emptyTextNode || !this._emptyTextValue) {
+ return;
+ }
+ let label = this.document.createElement("label");
+ label.className = "plain side-menu-widget-empty-text";
+ label.setAttribute("value", this._emptyTextValue);
+
+ this._parent.insertBefore(label, this._list);
+ this._emptyTextNode = label;
+ },
+
+ /**
+ * Removes the label representing a notice in this container.
+ */
+ _removeEmptyText: function () {
+ if (!this._emptyTextNode) {
+ return;
+ }
+
+ this._parent.removeChild(this._emptyTextNode);
+ this._emptyTextNode = null;
+ },
+
+ /**
+ * Gets a container representing a group for menu items. If the container
+ * is not available yet, it is immediately created.
+ *
+ * @param string aName
+ * The required group name.
+ * @return SideMenuGroup
+ * The newly created group.
+ */
+ _getMenuGroupForName: function (aName) {
+ let cachedGroup = this._groupsByName.get(aName);
+ if (cachedGroup) {
+ return cachedGroup;
+ }
+
+ let group = new SideMenuGroup(this, aName, {
+ showCheckbox: this._showGroupCheckboxes
+ });
+
+ this._groupsByName.set(aName, group);
+ group.insertSelfAt(this.sortedGroups ? group.findExpectedIndexForSelf(this.groupSortPredicate) : -1);
+
+ return group;
+ },
+
+ /**
+ * Gets a menu item to be displayed inside a group.
+ * @see SideMenuWidget.prototype._getMenuGroupForName
+ *
+ * @param SideMenuGroup aGroup
+ * The group to contain the menu item.
+ * @param nsIDOMNode aContents
+ * The node displayed in the container.
+ * @param object aAttachment [optional]
+ * Some attached primitive/object.
+ */
+ _getMenuItemForGroup: function (aGroup, aContents, aAttachment) {
+ return new SideMenuItem(aGroup, aContents, aAttachment, {
+ showArrow: this._showArrows,
+ showCheckbox: this._showItemCheckboxes
+ });
+ },
+
+ /**
+ * Returns the .side-menu-widget-item node corresponding to a SideMenuItem.
+ * To optimize the markup, some redundant elemenst are skipped when creating
+ * these child items, in which case we need to be careful on which nodes
+ * .selected class names are added, or which nodes are removed.
+ *
+ * @param nsIDOMNode aChild
+ * An element which is the target node of a SideMenuItem.
+ * @return nsIDOMNode
+ * The wrapper node if there is one, or the same child otherwise.
+ */
+ _getNodeForContents: function (aChild) {
+ if (aChild.hasAttribute("merged-item-contents")) {
+ return aChild;
+ } else {
+ return aChild.parentNode;
+ }
+ },
+
+ /**
+ * Shows the contextMenu element.
+ */
+ _showContextMenu: function (e) {
+ if (!this._contextMenu) {
+ return;
+ }
+
+ // Don't show the menu if a descendant node is going to be visible also.
+ let node = e.originalTarget;
+ while (node && node !== this._list) {
+ if (node.hasAttribute("contextmenu")) {
+ return;
+ }
+ node = node.parentNode;
+ }
+
+ this._contextMenu.openPopupAtScreen(e.screenX, e.screenY, true);
+ },
+
+ window: null,
+ document: null,
+ _showArrows: false,
+ _showItemCheckboxes: false,
+ _showGroupCheckboxes: false,
+ _parent: null,
+ _list: null,
+ _selectedItem: null,
+ _groupsByName: null,
+ _orderedGroupElementsArray: null,
+ _orderedMenuElementsArray: null,
+ _itemsByElement: null,
+ _emptyTextNode: null,
+ _emptyTextValue: ""
+};
+
+/**
+ * A SideMenuGroup constructor for the BreadcrumbsWidget.
+ * Represents a group which should contain SideMenuItems.
+ *
+ * @param SideMenuWidget aWidget
+ * The widget to contain this menu item.
+ * @param string aName
+ * The string displayed in the container.
+ * @param object aOptions [optional]
+ * An object containing the following properties:
+ * - showCheckbox: specifies if a checkbox should be displayed.
+ */
+function SideMenuGroup(aWidget, aName, aOptions = {}) {
+ this.document = aWidget.document;
+ this.window = aWidget.window;
+ this.ownerView = aWidget;
+ this.identifier = aName;
+
+ // Create an internal title and list container.
+ if (aName) {
+ let target = this._target = this.document.createElement("vbox");
+ target.className = "side-menu-widget-group";
+ target.setAttribute("name", aName);
+
+ let list = this._list = this.document.createElement("vbox");
+ list.className = "side-menu-widget-group-list";
+
+ let title = this._title = this.document.createElement("hbox");
+ title.className = "side-menu-widget-group-title";
+
+ let name = this._name = this.document.createElement("label");
+ name.className = "plain name";
+ name.setAttribute("value", aName);
+ name.setAttribute("crop", "end");
+ name.setAttribute("flex", "1");
+
+ // Show a checkbox before the content.
+ if (aOptions.showCheckbox) {
+ let checkbox = this._checkbox = makeCheckbox(title, {
+ description: aName,
+ checkboxTooltip: L10N.getStr("sideMenu.groupCheckbox.tooltip")
+ });
+ checkbox.className = "side-menu-widget-group-checkbox";
+ }
+
+ title.appendChild(name);
+ target.appendChild(title);
+ target.appendChild(list);
+ }
+ // Skip a few redundant nodes when no title is shown.
+ else {
+ let target = this._target = this._list = this.document.createElement("vbox");
+ target.className = "side-menu-widget-group side-menu-widget-group-list";
+ target.setAttribute("merged-group-contents", "");
+ }
+}
+
+SideMenuGroup.prototype = {
+ get _orderedGroupElementsArray() {
+ return this.ownerView._orderedGroupElementsArray;
+ },
+ get _orderedMenuElementsArray() {
+ return this.ownerView._orderedMenuElementsArray;
+ },
+ get _itemsByElement() { return this.ownerView._itemsByElement; },
+
+ /**
+ * Inserts this group in the parent container at the specified index.
+ *
+ * @param number aIndex
+ * The position in the container intended for this group.
+ */
+ insertSelfAt: function (aIndex) {
+ let ownerList = this.ownerView._list;
+ let groupsArray = this._orderedGroupElementsArray;
+
+ if (aIndex >= 0) {
+ ownerList.insertBefore(this._target, groupsArray[aIndex]);
+ groupsArray.splice(aIndex, 0, this._target);
+ } else {
+ ownerList.appendChild(this._target);
+ groupsArray.push(this._target);
+ }
+ },
+
+ /**
+ * Finds the expected index of this group based on its name.
+ *
+ * @return number
+ * The expected index.
+ */
+ findExpectedIndexForSelf: function (sortPredicate) {
+ let identifier = this.identifier;
+ let groupsArray = this._orderedGroupElementsArray;
+
+ for (let group of groupsArray) {
+ let name = group.getAttribute("name");
+ if (sortPredicate(name, identifier) > 0 && // Insertion sort at its best :)
+ !name.includes(identifier)) { // Least significant group should be last.
+ return groupsArray.indexOf(group);
+ }
+ }
+ return -1;
+ },
+
+ window: null,
+ document: null,
+ ownerView: null,
+ identifier: "",
+ _target: null,
+ _checkbox: null,
+ _title: null,
+ _name: null,
+ _list: null
+};
+
+/**
+ * A SideMenuItem constructor for the BreadcrumbsWidget.
+ *
+ * @param SideMenuGroup aGroup
+ * The group to contain this menu item.
+ * @param nsIDOMNode aContents
+ * The node displayed in the container.
+ * @param object aAttachment [optional]
+ * The attachment object.
+ * @param object aOptions [optional]
+ * An object containing the following properties:
+ * - showArrow: specifies if a horizontal arrow should be displayed.
+ * - showCheckbox: specifies if a checkbox should be displayed.
+ */
+function SideMenuItem(aGroup, aContents, aAttachment = {}, aOptions = {}) {
+ this.document = aGroup.document;
+ this.window = aGroup.window;
+ this.ownerView = aGroup;
+
+ if (aOptions.showArrow || aOptions.showCheckbox) {
+ let container = this._container = this.document.createElement("hbox");
+ container.className = "side-menu-widget-item";
+
+ let target = this._target = this.document.createElement("vbox");
+ target.className = "side-menu-widget-item-contents";
+
+ // Show a checkbox before the content.
+ if (aOptions.showCheckbox) {
+ let checkbox = this._checkbox = makeCheckbox(container, aAttachment);
+ checkbox.className = "side-menu-widget-item-checkbox";
+ }
+
+ container.appendChild(target);
+
+ // Show a horizontal arrow towards the content.
+ if (aOptions.showArrow) {
+ let arrow = this._arrow = this.document.createElement("hbox");
+ arrow.className = "side-menu-widget-item-arrow";
+ container.appendChild(arrow);
+ }
+ }
+ // Skip a few redundant nodes when no horizontal arrow or checkbox is shown.
+ else {
+ let target = this._target = this._container = this.document.createElement("hbox");
+ target.className = "side-menu-widget-item side-menu-widget-item-contents";
+ target.setAttribute("merged-item-contents", "");
+ }
+
+ this._target.setAttribute("flex", "1");
+ this.contents = aContents;
+}
+
+SideMenuItem.prototype = {
+ get _orderedGroupElementsArray() {
+ return this.ownerView._orderedGroupElementsArray;
+ },
+ get _orderedMenuElementsArray() {
+ return this.ownerView._orderedMenuElementsArray;
+ },
+ get _itemsByElement() { return this.ownerView._itemsByElement; },
+
+ /**
+ * Inserts this item in the parent group at the specified index.
+ *
+ * @param number aIndex
+ * The position in the container intended for this item.
+ * @return nsIDOMNode
+ * The element associated with the displayed item.
+ */
+ insertSelfAt: function (aIndex) {
+ let ownerList = this.ownerView._list;
+ let menuArray = this._orderedMenuElementsArray;
+
+ if (aIndex >= 0) {
+ ownerList.insertBefore(this._container, ownerList.childNodes[aIndex]);
+ menuArray.splice(aIndex, 0, this._target);
+ } else {
+ ownerList.appendChild(this._container);
+ menuArray.push(this._target);
+ }
+ this._itemsByElement.set(this._target, this);
+
+ return this._target;
+ },
+
+ /**
+ * Check or uncheck the checkbox associated with this item.
+ *
+ * @param boolean aCheckState
+ * True to check, false to uncheck.
+ */
+ check: function (aCheckState) {
+ if (!this._checkbox) {
+ throw new Error("Cannot check items that do not have checkboxes.");
+ }
+ // Don't set or remove the "checked" attribute, assign the property instead.
+ // Otherwise, the "CheckboxStateChange" event will not be fired. XUL!!
+ this._checkbox.checked = !!aCheckState;
+ },
+
+ /**
+ * Sets the contents displayed in this item's view.
+ *
+ * @param string | nsIDOMNode aContents
+ * The string or node displayed in the container.
+ */
+ set contents(aContents) {
+ // If there are already some contents displayed, replace them.
+ if (this._target.hasChildNodes()) {
+ this._target.replaceChild(aContents, this._target.firstChild);
+ return;
+ }
+ // These are the first contents ever displayed.
+ this._target.appendChild(aContents);
+ },
+
+ window: null,
+ document: null,
+ ownerView: null,
+ _target: null,
+ _container: null,
+ _checkbox: null,
+ _arrow: null
+};
+
+/**
+ * Creates a checkbox to a specified parent node. Emits a "check" event
+ * whenever the checkbox is checked or unchecked by the user.
+ *
+ * @param nsIDOMNode aParentNode
+ * The parent node to contain this checkbox.
+ * @param object aOptions
+ * An object containing some or all of the following properties:
+ * - description: defaults to "item" if unspecified
+ * - checkboxState: true for checked, false for unchecked
+ * - checkboxTooltip: the tooltip text of the checkbox
+ */
+function makeCheckbox(aParentNode, aOptions) {
+ let checkbox = aParentNode.ownerDocument.createElement("checkbox");
+
+ checkbox.setAttribute("tooltiptext", aOptions.checkboxTooltip || "");
+
+ if (aOptions.checkboxState) {
+ checkbox.setAttribute("checked", true);
+ } else {
+ checkbox.removeAttribute("checked");
+ }
+
+ // Stop the toggling of the checkbox from selecting the list item.
+ checkbox.addEventListener("mousedown", e => {
+ e.stopPropagation();
+ }, false);
+
+ // Emit an event from the checkbox when it is toggled. Don't listen for the
+ // "command" event! It won't fire for programmatic changes. XUL!!
+ checkbox.addEventListener("CheckboxStateChange", e => {
+ ViewHelpers.dispatchEvent(checkbox, "check", {
+ description: aOptions.description || "item",
+ checked: checkbox.checked
+ });
+ }, false);
+
+ aParentNode.appendChild(checkbox);
+ return checkbox;
+}
diff --git a/devtools/client/shared/widgets/SimpleListWidget.jsm b/devtools/client/shared/widgets/SimpleListWidget.jsm
new file mode 100644
index 000000000..ec47ab0da
--- /dev/null
+++ b/devtools/client/shared/widgets/SimpleListWidget.jsm
@@ -0,0 +1,255 @@
+/* -*- 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 Ci = Components.interfaces;
+const Cu = Components.utils;
+
+const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const { ViewHelpers } = require("devtools/client/shared/widgets/view-helpers");
+
+this.EXPORTED_SYMBOLS = ["SimpleListWidget"];
+
+/**
+ * A very simple vertical list view.
+ *
+ * Note: this widget should be used in tandem with the WidgetMethods in
+ * view-helpers.js.
+ *
+ * @param nsIDOMNode aNode
+ * The element associated with the widget.
+ */
+function SimpleListWidget(aNode) {
+ this.document = aNode.ownerDocument;
+ this.window = this.document.defaultView;
+ this._parent = aNode;
+
+ // Create an internal list container.
+ this._list = this.document.createElement("scrollbox");
+ this._list.className = "simple-list-widget-container theme-body";
+ this._list.setAttribute("flex", "1");
+ this._list.setAttribute("orient", "vertical");
+ this._parent.appendChild(this._list);
+
+ // Delegate some of the associated node's methods to satisfy the interface
+ // required by WidgetMethods instances.
+ ViewHelpers.delegateWidgetAttributeMethods(this, aNode);
+ ViewHelpers.delegateWidgetEventMethods(this, aNode);
+}
+this.SimpleListWidget = SimpleListWidget;
+
+SimpleListWidget.prototype = {
+ /**
+ * Inserts an item in this container at the specified index.
+ *
+ * @param number aIndex
+ * The position in the container intended for this item.
+ * @param nsIDOMNode aContents
+ * The node displayed in the container.
+ * @return nsIDOMNode
+ * The element associated with the displayed item.
+ */
+ insertItemAt: function (aIndex, aContents) {
+ aContents.classList.add("simple-list-widget-item");
+
+ let list = this._list;
+ return list.insertBefore(aContents, list.childNodes[aIndex]);
+ },
+
+ /**
+ * Returns the child node in this container situated at the specified index.
+ *
+ * @param number aIndex
+ * The position in the container intended for this item.
+ * @return nsIDOMNode
+ * The element associated with the displayed item.
+ */
+ getItemAtIndex: function (aIndex) {
+ return this._list.childNodes[aIndex];
+ },
+
+ /**
+ * Immediately removes the specified child node from this container.
+ *
+ * @param nsIDOMNode aChild
+ * The element associated with the displayed item.
+ */
+ removeChild: function (aChild) {
+ this._list.removeChild(aChild);
+
+ if (this._selectedItem == aChild) {
+ this._selectedItem = null;
+ }
+ },
+
+ /**
+ * Removes all of the child nodes from this container.
+ */
+ removeAllItems: function () {
+ let list = this._list;
+ let parent = this._parent;
+
+ while (list.hasChildNodes()) {
+ list.firstChild.remove();
+ }
+
+ parent.scrollTop = 0;
+ parent.scrollLeft = 0;
+ this._selectedItem = null;
+ },
+
+ /**
+ * Gets the currently selected child node in this container.
+ * @return nsIDOMNode
+ */
+ get selectedItem() {
+ return this._selectedItem;
+ },
+
+ /**
+ * Sets the currently selected child node in this container.
+ * @param nsIDOMNode aChild
+ */
+ set selectedItem(aChild) {
+ let childNodes = this._list.childNodes;
+
+ if (!aChild) {
+ this._selectedItem = null;
+ }
+ for (let node of childNodes) {
+ if (node == aChild) {
+ node.classList.add("selected");
+ this._selectedItem = node;
+ } else {
+ node.classList.remove("selected");
+ }
+ }
+ },
+
+ /**
+ * Adds a new attribute or changes an existing attribute on this container.
+ *
+ * @param string aName
+ * The name of the attribute.
+ * @param string aValue
+ * The desired attribute value.
+ */
+ setAttribute: function (aName, aValue) {
+ this._parent.setAttribute(aName, aValue);
+
+ if (aName == "emptyText") {
+ this._textWhenEmpty = aValue;
+ } else if (aName == "headerText") {
+ this._textAsHeader = aValue;
+ }
+ },
+
+ /**
+ * Removes an attribute on this container.
+ *
+ * @param string aName
+ * The name of the attribute.
+ */
+ removeAttribute: function (aName) {
+ this._parent.removeAttribute(aName);
+
+ if (aName == "emptyText") {
+ this._removeEmptyText();
+ }
+ },
+
+ /**
+ * Ensures the specified element is visible.
+ *
+ * @param nsIDOMNode aElement
+ * The element to make visible.
+ */
+ ensureElementIsVisible: function (aElement) {
+ if (!aElement) {
+ return;
+ }
+
+ // Ensure the element is visible but not scrolled horizontally.
+ let boxObject = this._list.boxObject;
+ boxObject.ensureElementIsVisible(aElement);
+ boxObject.scrollBy(-this._list.clientWidth, 0);
+ },
+
+ /**
+ * Sets the text displayed permanently in this container as a header.
+ * @param string aValue
+ */
+ set _textAsHeader(aValue) {
+ if (this._headerTextNode) {
+ this._headerTextNode.setAttribute("value", aValue);
+ }
+ this._headerTextValue = aValue;
+ this._showHeaderText();
+ },
+
+ /**
+ * Sets the text displayed in this container when empty.
+ * @param string aValue
+ */
+ set _textWhenEmpty(aValue) {
+ if (this._emptyTextNode) {
+ this._emptyTextNode.setAttribute("value", aValue);
+ }
+ this._emptyTextValue = aValue;
+ this._showEmptyText();
+ },
+
+ /**
+ * Creates and appends a label displayed as this container's header.
+ */
+ _showHeaderText: function () {
+ if (this._headerTextNode || !this._headerTextValue) {
+ return;
+ }
+ let label = this.document.createElement("label");
+ label.className = "plain simple-list-widget-perma-text";
+ label.setAttribute("value", this._headerTextValue);
+
+ this._parent.insertBefore(label, this._list);
+ this._headerTextNode = label;
+ },
+
+ /**
+ * Creates and appends a label signaling that this container is empty.
+ */
+ _showEmptyText: function () {
+ if (this._emptyTextNode || !this._emptyTextValue) {
+ return;
+ }
+ let label = this.document.createElement("label");
+ label.className = "plain simple-list-widget-empty-text";
+ label.setAttribute("value", this._emptyTextValue);
+
+ this._parent.appendChild(label);
+ this._emptyTextNode = label;
+ },
+
+ /**
+ * Removes the label signaling that this container is empty.
+ */
+ _removeEmptyText: function () {
+ if (!this._emptyTextNode) {
+ return;
+ }
+ this._parent.removeChild(this._emptyTextNode);
+ this._emptyTextNode = null;
+ },
+
+ window: null,
+ document: null,
+ _parent: null,
+ _list: null,
+ _selectedItem: null,
+ _headerTextNode: null,
+ _headerTextValue: "",
+ _emptyTextNode: null,
+ _emptyTextValue: ""
+};
diff --git a/devtools/client/shared/widgets/Spectrum.js b/devtools/client/shared/widgets/Spectrum.js
new file mode 100644
index 000000000..00110f13e
--- /dev/null
+++ b/devtools/client/shared/widgets/Spectrum.js
@@ -0,0 +1,336 @@
+/* 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 EventEmitter = require("devtools/shared/event-emitter");
+const XHTML_NS = "http://www.w3.org/1999/xhtml";
+
+/**
+ * Spectrum creates a color picker widget in any container you give it.
+ *
+ * Simple usage example:
+ *
+ * const {Spectrum} = require("devtools/client/shared/widgets/Spectrum");
+ * let s = new Spectrum(containerElement, [255, 126, 255, 1]);
+ * s.on("changed", (event, rgba, color) => {
+ * console.log("rgba(" + rgba[0] + ", " + rgba[1] + ", " + rgba[2] + ", " +
+ * rgba[3] + ")");
+ * });
+ * s.show();
+ * s.destroy();
+ *
+ * Note that the color picker is hidden by default and you need to call show to
+ * make it appear. This 2 stages initialization helps in cases you are creating
+ * the color picker in a parent element that hasn't been appended anywhere yet
+ * or that is hidden. Calling show() when the parent element is appended and
+ * visible will allow spectrum to correctly initialize its various parts.
+ *
+ * Fires the following events:
+ * - changed : When the user changes the current color
+ */
+function Spectrum(parentEl, rgb) {
+ EventEmitter.decorate(this);
+
+ this.element = parentEl.ownerDocument.createElementNS(XHTML_NS, "div");
+ this.parentEl = parentEl;
+
+ this.element.className = "spectrum-container";
+ this.element.innerHTML = `
+ <div class="spectrum-top">
+ <div class="spectrum-fill"></div>
+ <div class="spectrum-top-inner">
+ <div class="spectrum-color spectrum-box">
+ <div class="spectrum-sat">
+ <div class="spectrum-val">
+ <div class="spectrum-dragger"></div>
+ </div>
+ </div>
+ </div>
+ <div class="spectrum-hue spectrum-box">
+ <div class="spectrum-slider spectrum-slider-control"></div>
+ </div>
+ </div>
+ </div>
+ <div class="spectrum-alpha spectrum-checker spectrum-box">
+ <div class="spectrum-alpha-inner">
+ <div class="spectrum-alpha-handle spectrum-slider-control"></div>
+ </div>
+ </div>
+ `;
+
+ this.onElementClick = this.onElementClick.bind(this);
+ this.element.addEventListener("click", this.onElementClick, false);
+
+ this.parentEl.appendChild(this.element);
+
+ this.slider = this.element.querySelector(".spectrum-hue");
+ this.slideHelper = this.element.querySelector(".spectrum-slider");
+ Spectrum.draggable(this.slider, this.onSliderMove.bind(this));
+
+ this.dragger = this.element.querySelector(".spectrum-color");
+ this.dragHelper = this.element.querySelector(".spectrum-dragger");
+ Spectrum.draggable(this.dragger, this.onDraggerMove.bind(this));
+
+ this.alphaSlider = this.element.querySelector(".spectrum-alpha");
+ this.alphaSliderInner = this.element.querySelector(".spectrum-alpha-inner");
+ this.alphaSliderHelper = this.element.querySelector(".spectrum-alpha-handle");
+ Spectrum.draggable(this.alphaSliderInner, this.onAlphaSliderMove.bind(this));
+
+ if (rgb) {
+ this.rgb = rgb;
+ this.updateUI();
+ }
+}
+
+module.exports.Spectrum = Spectrum;
+
+Spectrum.hsvToRgb = function (h, s, v, a) {
+ let r, g, b;
+
+ let i = Math.floor(h * 6);
+ let f = h * 6 - i;
+ let p = v * (1 - s);
+ let q = v * (1 - f * s);
+ let t = v * (1 - (1 - f) * s);
+
+ switch (i % 6) {
+ case 0: r = v; g = t; b = p; break;
+ case 1: r = q; g = v; b = p; break;
+ case 2: r = p; g = v; b = t; break;
+ case 3: r = p; g = q; b = v; break;
+ case 4: r = t; g = p; b = v; break;
+ case 5: r = v; g = p; b = q; break;
+ }
+
+ return [r * 255, g * 255, b * 255, a];
+};
+
+Spectrum.rgbToHsv = function (r, g, b, a) {
+ r = r / 255;
+ g = g / 255;
+ b = b / 255;
+
+ let max = Math.max(r, g, b), min = Math.min(r, g, b);
+ let h, s, v = max;
+
+ let d = max - min;
+ s = max == 0 ? 0 : d / max;
+
+ if (max == min) {
+ // achromatic
+ h = 0;
+ } else {
+ switch (max) {
+ case r: h = (g - b) / d + (g < b ? 6 : 0); break;
+ case g: h = (b - r) / d + 2; break;
+ case b: h = (r - g) / d + 4; break;
+ }
+ h /= 6;
+ }
+ return [h, s, v, a];
+};
+
+Spectrum.draggable = function (element, onmove, onstart, onstop) {
+ onmove = onmove || function () {};
+ onstart = onstart || function () {};
+ onstop = onstop || function () {};
+
+ let doc = element.ownerDocument;
+ let dragging = false;
+ let offset = {};
+ let maxHeight = 0;
+ let maxWidth = 0;
+
+ function prevent(e) {
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
+ function move(e) {
+ if (dragging) {
+ if (e.buttons === 0) {
+ // The button is no longer pressed but we did not get a mouseup event.
+ stop();
+ return;
+ }
+ let pageX = e.pageX;
+ let pageY = e.pageY;
+
+ let dragX = Math.max(0, Math.min(pageX - offset.left, maxWidth));
+ let dragY = Math.max(0, Math.min(pageY - offset.top, maxHeight));
+
+ onmove.apply(element, [dragX, dragY]);
+ }
+ }
+
+ function start(e) {
+ let rightclick = e.which === 3;
+
+ if (!rightclick && !dragging) {
+ if (onstart.apply(element, arguments) !== false) {
+ dragging = true;
+ maxHeight = element.offsetHeight;
+ maxWidth = element.offsetWidth;
+
+ offset = element.getBoundingClientRect();
+
+ move(e);
+
+ doc.addEventListener("selectstart", prevent, false);
+ doc.addEventListener("dragstart", prevent, false);
+ doc.addEventListener("mousemove", move, false);
+ doc.addEventListener("mouseup", stop, false);
+
+ prevent(e);
+ }
+ }
+ }
+
+ function stop() {
+ if (dragging) {
+ doc.removeEventListener("selectstart", prevent, false);
+ doc.removeEventListener("dragstart", prevent, false);
+ doc.removeEventListener("mousemove", move, false);
+ doc.removeEventListener("mouseup", stop, false);
+ onstop.apply(element, arguments);
+ }
+ dragging = false;
+ }
+
+ element.addEventListener("mousedown", start, false);
+};
+
+Spectrum.prototype = {
+ set rgb(color) {
+ this.hsv = Spectrum.rgbToHsv(color[0], color[1], color[2], color[3]);
+ },
+
+ get rgb() {
+ let rgb = Spectrum.hsvToRgb(this.hsv[0], this.hsv[1], this.hsv[2],
+ this.hsv[3]);
+ return [Math.round(rgb[0]), Math.round(rgb[1]), Math.round(rgb[2]),
+ Math.round(rgb[3] * 100) / 100];
+ },
+
+ get rgbNoSatVal() {
+ let rgb = Spectrum.hsvToRgb(this.hsv[0], 1, 1);
+ return [Math.round(rgb[0]), Math.round(rgb[1]), Math.round(rgb[2]), rgb[3]];
+ },
+
+ get rgbCssString() {
+ let rgb = this.rgb;
+ return "rgba(" + rgb[0] + ", " + rgb[1] + ", " + rgb[2] + ", " +
+ rgb[3] + ")";
+ },
+
+ show: function () {
+ this.element.classList.add("spectrum-show");
+
+ this.slideHeight = this.slider.offsetHeight;
+ this.dragWidth = this.dragger.offsetWidth;
+ this.dragHeight = this.dragger.offsetHeight;
+ this.dragHelperHeight = this.dragHelper.offsetHeight;
+ this.slideHelperHeight = this.slideHelper.offsetHeight;
+ this.alphaSliderWidth = this.alphaSliderInner.offsetWidth;
+ this.alphaSliderHelperWidth = this.alphaSliderHelper.offsetWidth;
+
+ this.updateUI();
+ },
+
+ onElementClick: function (e) {
+ e.stopPropagation();
+ },
+
+ onSliderMove: function (dragX, dragY) {
+ this.hsv[0] = (dragY / this.slideHeight);
+ this.updateUI();
+ this.onChange();
+ },
+
+ onDraggerMove: function (dragX, dragY) {
+ this.hsv[1] = dragX / this.dragWidth;
+ this.hsv[2] = (this.dragHeight - dragY) / this.dragHeight;
+ this.updateUI();
+ this.onChange();
+ },
+
+ onAlphaSliderMove: function (dragX, dragY) {
+ this.hsv[3] = dragX / this.alphaSliderWidth;
+ this.updateUI();
+ this.onChange();
+ },
+
+ onChange: function () {
+ this.emit("changed", this.rgb, this.rgbCssString);
+ },
+
+ updateHelperLocations: function () {
+ // If the UI hasn't been shown yet then none of the dimensions will be
+ // correct
+ if (!this.element.classList.contains("spectrum-show")) {
+ return;
+ }
+
+ let h = this.hsv[0];
+ let s = this.hsv[1];
+ let v = this.hsv[2];
+
+ // Placing the color dragger
+ let dragX = s * this.dragWidth;
+ let dragY = this.dragHeight - (v * this.dragHeight);
+ let helperDim = this.dragHelperHeight / 2;
+
+ dragX = Math.max(
+ -helperDim,
+ Math.min(this.dragWidth - helperDim, dragX - helperDim)
+ );
+ dragY = Math.max(
+ -helperDim,
+ Math.min(this.dragHeight - helperDim, dragY - helperDim)
+ );
+
+ this.dragHelper.style.top = dragY + "px";
+ this.dragHelper.style.left = dragX + "px";
+
+ // Placing the hue slider
+ let slideY = (h * this.slideHeight) - this.slideHelperHeight / 2;
+ this.slideHelper.style.top = slideY + "px";
+
+ // Placing the alpha slider
+ let alphaSliderX = (this.hsv[3] * this.alphaSliderWidth) -
+ (this.alphaSliderHelperWidth / 2);
+ this.alphaSliderHelper.style.left = alphaSliderX + "px";
+ },
+
+ updateUI: function () {
+ this.updateHelperLocations();
+
+ let rgb = this.rgb;
+ let rgbNoSatVal = this.rgbNoSatVal;
+
+ let flatColor = "rgb(" + rgbNoSatVal[0] + ", " + rgbNoSatVal[1] + ", " +
+ rgbNoSatVal[2] + ")";
+
+ this.dragger.style.backgroundColor = flatColor;
+
+ let rgbNoAlpha = "rgb(" + rgb[0] + "," + rgb[1] + "," + rgb[2] + ")";
+ let rgbAlpha0 = "rgba(" + rgb[0] + "," + rgb[1] + "," + rgb[2] + ", 0)";
+ let alphaGradient = "linear-gradient(to right, " + rgbAlpha0 + ", " +
+ rgbNoAlpha + ")";
+ this.alphaSliderInner.style.background = alphaGradient;
+ },
+
+ destroy: function () {
+ this.element.removeEventListener("click", this.onElementClick, false);
+
+ this.parentEl.removeChild(this.element);
+
+ this.slider = null;
+ this.dragger = null;
+ this.alphaSlider = this.alphaSliderInner = this.alphaSliderHelper = null;
+ this.parentEl = null;
+ this.element = null;
+ }
+};
diff --git a/devtools/client/shared/widgets/TableWidget.js b/devtools/client/shared/widgets/TableWidget.js
new file mode 100644
index 000000000..5dacd1b67
--- /dev/null
+++ b/devtools/client/shared/widgets/TableWidget.js
@@ -0,0 +1,1817 @@
+/* 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 EventEmitter = require("devtools/shared/event-emitter");
+loader.lazyRequireGetter(this, "setNamedTimeout",
+ "devtools/client/shared/widgets/view-helpers", true);
+loader.lazyRequireGetter(this, "clearNamedTimeout",
+ "devtools/client/shared/widgets/view-helpers", true);
+const {KeyCodes} = require("devtools/client/shared/keycodes");
+
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const AFTER_SCROLL_DELAY = 100;
+
+// Different types of events emitted by the Various components of the
+// TableWidget.
+const EVENTS = {
+ CELL_EDIT: "cell-edit",
+ COLUMN_SORTED: "column-sorted",
+ COLUMN_TOGGLED: "column-toggled",
+ FIELDS_EDITABLE: "fields-editable",
+ HEADER_CONTEXT_MENU: "header-context-menu",
+ ROW_EDIT: "row-edit",
+ ROW_CONTEXT_MENU: "row-context-menu",
+ ROW_REMOVED: "row-removed",
+ ROW_SELECTED: "row-selected",
+ ROW_UPDATED: "row-updated",
+ TABLE_CLEARED: "table-cleared",
+ TABLE_FILTERED: "table-filtered",
+ SCROLL_END: "scroll-end"
+};
+Object.defineProperty(this, "EVENTS", {
+ value: EVENTS,
+ enumerable: true,
+ writable: false
+});
+
+/**
+ * A table widget with various features like resizble/toggleable columns,
+ * sorting, keyboard navigation etc.
+ *
+ * @param {nsIDOMNode} node
+ * The container element for the table widget.
+ * @param {object} options
+ * - initialColumns: map of key vs display name for initial columns of
+ * the table. See @setupColumns for more info.
+ * - uniqueId: the column which will be the unique identifier of each
+ * entry in the table. Default: name.
+ * - wrapTextInElements: Don't ever use 'value' attribute on labels.
+ * Default: false.
+ * - emptyText: text to display when no entries in the table to display.
+ * - highlightUpdated: true to highlight the changed/added row.
+ * - removableColumns: Whether columns are removeable. If set to false,
+ * the context menu in the headers will not appear.
+ * - firstColumn: key of the first column that should appear.
+ * - cellContextMenuId: ID of a <menupopup> element to be set as a
+ * context menu of every cell.
+ */
+function TableWidget(node, options = {}) {
+ EventEmitter.decorate(this);
+
+ this.document = node.ownerDocument;
+ this.window = this.document.defaultView;
+ this._parent = node;
+
+ let {initialColumns, emptyText, uniqueId, highlightUpdated, removableColumns,
+ firstColumn, wrapTextInElements, cellContextMenuId} = options;
+ this.emptyText = emptyText || "";
+ this.uniqueId = uniqueId || "name";
+ this.wrapTextInElements = wrapTextInElements || false;
+ this.firstColumn = firstColumn || "";
+ this.highlightUpdated = highlightUpdated || false;
+ this.removableColumns = removableColumns !== false;
+ this.cellContextMenuId = cellContextMenuId;
+
+ this.tbody = this.document.createElementNS(XUL_NS, "hbox");
+ this.tbody.className = "table-widget-body theme-body";
+ this.tbody.setAttribute("flex", "1");
+ this.tbody.setAttribute("tabindex", "0");
+ this._parent.appendChild(this.tbody);
+ this.afterScroll = this.afterScroll.bind(this);
+ this.tbody.addEventListener("scroll", this.onScroll.bind(this));
+
+ this.placeholder = this.document.createElementNS(XUL_NS, "label");
+ this.placeholder.className = "plain table-widget-empty-text";
+ this.placeholder.setAttribute("flex", "1");
+ this._parent.appendChild(this.placeholder);
+
+ this.items = new Map();
+ this.columns = new Map();
+
+ // Setup the column headers context menu to allow users to hide columns at
+ // will.
+ if (this.removableColumns) {
+ this.onPopupCommand = this.onPopupCommand.bind(this);
+ this.setupHeadersContextMenu();
+ }
+
+ if (initialColumns) {
+ this.setColumns(initialColumns, uniqueId);
+ } else if (this.emptyText) {
+ this.setPlaceholderText(this.emptyText);
+ }
+
+ this.bindSelectedRow = (event, id) => {
+ this.selectedRow = id;
+ };
+ this.on(EVENTS.ROW_SELECTED, this.bindSelectedRow);
+
+ this.onChange = this.onChange.bind(this);
+ this.onEditorDestroyed = this.onEditorDestroyed.bind(this);
+ this.onEditorTab = this.onEditorTab.bind(this);
+ this.onKeydown = this.onKeydown.bind(this);
+ this.onMousedown = this.onMousedown.bind(this);
+ this.onRowRemoved = this.onRowRemoved.bind(this);
+
+ this.document.addEventListener("keydown", this.onKeydown, false);
+ this.document.addEventListener("mousedown", this.onMousedown, false);
+}
+
+TableWidget.prototype = {
+
+ items: null,
+
+ /**
+ * Getter for the headers context menu popup id.
+ */
+ get headersContextMenu() {
+ if (this.menupopup) {
+ return this.menupopup.id;
+ }
+ return null;
+ },
+
+ /**
+ * Select the row corresponding to the json object `id`
+ */
+ set selectedRow(id) {
+ for (let column of this.columns.values()) {
+ column.selectRow(id[this.uniqueId] || id);
+ }
+ },
+
+ /**
+ * Returns the json object corresponding to the selected row.
+ */
+ get selectedRow() {
+ return this.items.get(this.columns.get(this.uniqueId).selectedRow);
+ },
+
+ /**
+ * Selects the row at index `index`.
+ */
+ set selectedIndex(index) {
+ for (let column of this.columns.values()) {
+ column.selectRowAt(index);
+ }
+ },
+
+ /**
+ * Returns the index of the selected row.
+ */
+ get selectedIndex() {
+ return this.columns.get(this.uniqueId).selectedIndex;
+ },
+
+ /**
+ * Returns the index of the selected row disregarding hidden rows.
+ */
+ get visibleSelectedIndex() {
+ let cells = this.columns.get(this.uniqueId).visibleCellNodes;
+
+ for (let i = 0; i < cells.length; i++) {
+ if (cells[i].classList.contains("theme-selected")) {
+ return i;
+ }
+ }
+
+ return -1;
+ },
+
+ /**
+ * returns all editable columns.
+ */
+ get editableColumns() {
+ let filter = columns => {
+ columns = [...columns].filter(col => {
+ if (col.clientWidth === 0) {
+ return false;
+ }
+
+ let cell = col.querySelector(".table-widget-cell");
+
+ for (let selector of this._editableFieldsEngine.selectors) {
+ if (cell.matches(selector)) {
+ return true;
+ }
+ }
+
+ return false;
+ });
+
+ return columns;
+ };
+
+ let columns = this._parent.querySelectorAll(".table-widget-column");
+ return filter(columns);
+ },
+
+ /**
+ * Emit all cell edit events.
+ */
+ onChange: function (type, data) {
+ let changedField = data.change.field;
+ let colName = changedField.parentNode.id;
+ let column = this.columns.get(colName);
+ let uniqueId = column.table.uniqueId;
+ let itemIndex = column.cellNodes.indexOf(changedField);
+ let items = {};
+
+ for (let [name, col] of this.columns) {
+ items[name] = col.cellNodes[itemIndex].value;
+ }
+
+ let change = {
+ host: this.host,
+ key: uniqueId,
+ field: colName,
+ oldValue: data.change.oldValue,
+ newValue: data.change.newValue,
+ items: items
+ };
+
+ // A rows position in the table can change as the result of an edit. In
+ // order to ensure that the correct row is highlighted after an edit we
+ // save the uniqueId in editBookmark.
+ this.editBookmark = colName === uniqueId ? change.newValue
+ : items[uniqueId];
+ this.emit(EVENTS.CELL_EDIT, change);
+ },
+
+ onEditorDestroyed: function () {
+ this._editableFieldsEngine = null;
+ },
+
+ /**
+ * Called by the inplace editor when Tab / Shift-Tab is pressed in edit-mode.
+ * Because tables are live any row, column, cell or table can be added,
+ * deleted or moved by deleting and adding e.g. a row again.
+ *
+ * This presents various challenges when navigating via the keyboard so please
+ * keep this in mind whenever editing this method.
+ *
+ * @param {Event} event
+ * Keydown event
+ */
+ onEditorTab: function (event) {
+ let textbox = event.target;
+ let editor = this._editableFieldsEngine;
+
+ if (textbox.id !== editor.INPUT_ID) {
+ return;
+ }
+
+ let column = textbox.parentNode;
+
+ // Changing any value can change the position of the row depending on which
+ // column it is currently sorted on. In addition to this, the table cell may
+ // have been edited and had to be recreated when the user has pressed tab or
+ // shift+tab. Both of these situations require us to recover our target,
+ // select the appropriate row and move the textbox on to the next cell.
+ if (editor.changePending) {
+ // We need to apply a change, which can mean that the position of cells
+ // within the table can change. Because of this we need to wait for
+ // EVENTS.ROW_EDIT and then move the textbox.
+ this.once(EVENTS.ROW_EDIT, (e, uniqueId) => {
+ let cell;
+ let cells;
+ let columnObj;
+ let cols = this.editableColumns;
+ let rowIndex = this.visibleSelectedIndex;
+ let colIndex = cols.indexOf(column);
+ let newIndex;
+
+ // If the row has been deleted we should bail out.
+ if (!uniqueId) {
+ return;
+ }
+
+ // Find the column we need to move to.
+ if (event.shiftKey) {
+ // Navigate backwards on shift tab.
+ if (colIndex === 0) {
+ if (rowIndex === 0) {
+ return;
+ }
+ newIndex = cols.length - 1;
+ } else {
+ newIndex = colIndex - 1;
+ }
+ } else if (colIndex === cols.length - 1) {
+ let id = cols[0].id;
+ columnObj = this.columns.get(id);
+ let maxRowIndex = columnObj.visibleCellNodes.length - 1;
+ if (rowIndex === maxRowIndex) {
+ return;
+ }
+ newIndex = 0;
+ } else {
+ newIndex = colIndex + 1;
+ }
+
+ let newcol = cols[newIndex];
+ columnObj = this.columns.get(newcol.id);
+
+ // Select the correct row even if it has moved due to sorting.
+ let dataId = editor.currentTarget.getAttribute("data-id");
+ if (this.items.get(dataId)) {
+ this.emit(EVENTS.ROW_SELECTED, dataId);
+ } else {
+ this.emit(EVENTS.ROW_SELECTED, uniqueId);
+ }
+
+ // EVENTS.ROW_SELECTED may have changed the selected row so let's save
+ // the result in rowIndex.
+ rowIndex = this.visibleSelectedIndex;
+
+ // Edit the appropriate cell.
+ cells = columnObj.visibleCellNodes;
+ cell = cells[rowIndex];
+ editor.edit(cell);
+
+ // Remove flash-out class... it won't have been auto-removed because the
+ // cell was hidden for editing.
+ cell.classList.remove("flash-out");
+ });
+ }
+
+ // Begin cell edit. We always do this so that we can begin editing even in
+ // the case that the previous edit will cause the row to move.
+ let cell = this.getEditedCellOnTab(event, column);
+ editor.edit(cell);
+ },
+
+ /**
+ * Get the cell that will be edited next on tab / shift tab and highlight the
+ * appropriate row. Edits etc. are not taken into account.
+ *
+ * This is used to tab from one field to another without editing and makes the
+ * editor much more responsive.
+ *
+ * @param {Event} event
+ * Keydown event
+ */
+ getEditedCellOnTab: function (event, column) {
+ let cell = null;
+ let cols = this.editableColumns;
+ let rowIndex = this.visibleSelectedIndex;
+ let colIndex = cols.indexOf(column);
+ let maxCol = cols.length - 1;
+ let maxRow = this.columns.get(column.id).visibleCellNodes.length - 1;
+
+ if (event.shiftKey) {
+ // Navigate backwards on shift tab.
+ if (colIndex === 0) {
+ if (rowIndex === 0) {
+ this._editableFieldsEngine.completeEdit();
+ return null;
+ }
+
+ column = cols[cols.length - 1];
+ let cells = this.columns.get(column.id).visibleCellNodes;
+ cell = cells[rowIndex - 1];
+
+ let rowId = cell.getAttribute("data-id");
+ this.emit(EVENTS.ROW_SELECTED, rowId);
+ } else {
+ column = cols[colIndex - 1];
+ let cells = this.columns.get(column.id).visibleCellNodes;
+ cell = cells[rowIndex];
+ }
+ } else if (colIndex === maxCol) {
+ // If in the rightmost column on the last row stop editing.
+ if (rowIndex === maxRow) {
+ this._editableFieldsEngine.completeEdit();
+ return null;
+ }
+
+ // If in the rightmost column of a row then move to the first column of
+ // the next row.
+ column = cols[0];
+ let cells = this.columns.get(column.id).visibleCellNodes;
+ cell = cells[rowIndex + 1];
+
+ let rowId = cell.getAttribute("data-id");
+ this.emit(EVENTS.ROW_SELECTED, rowId);
+ } else {
+ // Navigate forwards on tab.
+ column = cols[colIndex + 1];
+ let cells = this.columns.get(column.id).visibleCellNodes;
+ cell = cells[rowIndex];
+ }
+
+ return cell;
+ },
+
+ /**
+ * Reset the editable fields engine if the currently edited row is removed.
+ *
+ * @param {String} event
+ * The event name "event-removed."
+ * @param {Object} row
+ * The values from the removed row.
+ */
+ onRowRemoved: function (event, row) {
+ if (!this._editableFieldsEngine || !this._editableFieldsEngine.isEditing) {
+ return;
+ }
+
+ let removedKey = row[this.uniqueId];
+ let column = this.columns.get(this.uniqueId);
+
+ if (removedKey in column.items) {
+ return;
+ }
+
+ // The target is lost so we need to hide the remove the textbox from the DOM
+ // and reset the target nodes.
+ this.onEditorTargetLost();
+ },
+
+ /**
+ * Cancel an edit because the edit target has been lost.
+ */
+ onEditorTargetLost: function () {
+ let editor = this._editableFieldsEngine;
+
+ if (!editor || !editor.isEditing) {
+ return;
+ }
+
+ editor.cancelEdit();
+ },
+
+ /**
+ * Keydown event handler for the table. Used for keyboard navigation amongst
+ * rows.
+ */
+ onKeydown: function (event) {
+ // If we are in edit mode bail out.
+ if (this._editableFieldsEngine && this._editableFieldsEngine.isEditing) {
+ return;
+ }
+
+ let selectedCell = this.tbody.querySelector(".theme-selected");
+ if (!selectedCell) {
+ return;
+ }
+
+ let colName;
+ let column;
+ let visibleCells;
+ let index;
+ let cell;
+
+ switch (event.keyCode) {
+ case KeyCodes.DOM_VK_UP:
+ event.preventDefault();
+
+ colName = selectedCell.parentNode.id;
+ column = this.columns.get(colName);
+ visibleCells = column.visibleCellNodes;
+ index = visibleCells.indexOf(selectedCell);
+
+ if (index > 0) {
+ index--;
+ } else {
+ index = visibleCells.length - 1;
+ }
+
+ cell = visibleCells[index];
+
+ this.emit(EVENTS.ROW_SELECTED, cell.getAttribute("data-id"));
+ break;
+ case KeyCodes.DOM_VK_DOWN:
+ event.preventDefault();
+
+ colName = selectedCell.parentNode.id;
+ column = this.columns.get(colName);
+ visibleCells = column.visibleCellNodes;
+ index = visibleCells.indexOf(selectedCell);
+
+ if (index === visibleCells.length - 1) {
+ index = 0;
+ } else {
+ index++;
+ }
+
+ cell = visibleCells[index];
+
+ this.emit(EVENTS.ROW_SELECTED, cell.getAttribute("data-id"));
+ break;
+ }
+ },
+
+ /**
+ * Close any editors if the area "outside the table" is clicked. In reality,
+ * the table covers the whole area but there are labels filling the top few
+ * rows. This method clears any inline editors if an area outside a textbox or
+ * label is clicked.
+ */
+ onMousedown: function ({target}) {
+ let nodeName = target.nodeName;
+
+ if (nodeName === "textbox" || !this._editableFieldsEngine) {
+ return;
+ }
+
+ // Force any editor fields to hide due to XUL focus quirks.
+ this._editableFieldsEngine.blur();
+ },
+
+ /**
+ * Make table fields editable.
+ *
+ * @param {String|Array} editableColumns
+ * An array or comma separated list of editable column names.
+ */
+ makeFieldsEditable: function (editableColumns) {
+ let selectors = [];
+
+ if (typeof editableColumns === "string") {
+ editableColumns = [editableColumns];
+ }
+
+ for (let id of editableColumns) {
+ selectors.push("#" + id + " .table-widget-cell");
+ }
+
+ for (let [name, column] of this.columns) {
+ if (!editableColumns.includes(name)) {
+ column.column.setAttribute("readonly", "");
+ }
+ }
+
+ if (this._editableFieldsEngine) {
+ this._editableFieldsEngine.selectors = selectors;
+ } else {
+ this._editableFieldsEngine = new EditableFieldsEngine({
+ root: this.tbody,
+ onTab: this.onEditorTab,
+ onTriggerEvent: "dblclick",
+ selectors: selectors
+ });
+
+ this._editableFieldsEngine.on("change", this.onChange);
+ this._editableFieldsEngine.on("destroyed", this.onEditorDestroyed);
+
+ this.on(EVENTS.ROW_REMOVED, this.onRowRemoved);
+ this.on(EVENTS.TABLE_CLEARED, this._editableFieldsEngine.cancelEdit);
+
+ this.emit(EVENTS.FIELDS_EDITABLE, this._editableFieldsEngine);
+ }
+ },
+
+ destroy: function () {
+ this.off(EVENTS.ROW_SELECTED, this.bindSelectedRow);
+ this.off(EVENTS.ROW_REMOVED, this.onRowRemoved);
+
+ this.document.removeEventListener("keydown", this.onKeydown, false);
+ this.document.removeEventListener("mousedown", this.onMousedown, false);
+
+ if (this._editableFieldsEngine) {
+ this.off(EVENTS.TABLE_CLEARED, this._editableFieldsEngine.cancelEdit);
+ this._editableFieldsEngine.off("change", this.onChange);
+ this._editableFieldsEngine.off("destroyed", this.onEditorDestroyed);
+ this._editableFieldsEngine.destroy();
+ this._editableFieldsEngine = null;
+ }
+
+ if (this.menupopup) {
+ this.menupopup.removeEventListener("command", this.onPopupCommand);
+ this.menupopup.remove();
+ }
+ },
+
+ /**
+ * Sets the text to be shown when the table is empty.
+ */
+ setPlaceholderText: function (text) {
+ this.placeholder.setAttribute("value", text);
+ },
+
+ /**
+ * Prepares the context menu for the headers of the table columns. This
+ * context menu allows users to toggle various columns, only with an exception
+ * of the unique columns and when only two columns are visible in the table.
+ */
+ setupHeadersContextMenu: function () {
+ let popupset = this.document.getElementsByTagName("popupset")[0];
+ if (!popupset) {
+ popupset = this.document.createElementNS(XUL_NS, "popupset");
+ this.document.documentElement.appendChild(popupset);
+ }
+
+ this.menupopup = this.document.createElementNS(XUL_NS, "menupopup");
+ this.menupopup.id = "table-widget-column-select";
+ this.menupopup.addEventListener("command", this.onPopupCommand);
+ popupset.appendChild(this.menupopup);
+ this.populateMenuPopup();
+ },
+
+ /**
+ * Populates the header context menu with the names of the columns along with
+ * displaying which columns are hidden or visible.
+ */
+ populateMenuPopup: function () {
+ if (!this.menupopup) {
+ return;
+ }
+
+ while (this.menupopup.firstChild) {
+ this.menupopup.firstChild.remove();
+ }
+
+ for (let column of this.columns.values()) {
+ let menuitem = this.document.createElementNS(XUL_NS, "menuitem");
+ menuitem.setAttribute("label", column.header.getAttribute("value"));
+ menuitem.setAttribute("data-id", column.id);
+ menuitem.setAttribute("type", "checkbox");
+ menuitem.setAttribute("checked", !column.wrapper.getAttribute("hidden"));
+ if (column.id == this.uniqueId) {
+ menuitem.setAttribute("disabled", "true");
+ }
+ this.menupopup.appendChild(menuitem);
+ }
+ let checked = this.menupopup.querySelectorAll("menuitem[checked]");
+ if (checked.length == 2) {
+ checked[checked.length - 1].setAttribute("disabled", "true");
+ }
+ },
+
+ /**
+ * Event handler for the `command` event on the column headers context menu
+ */
+ onPopupCommand: function (event) {
+ let item = event.originalTarget;
+ let checked = !!item.getAttribute("checked");
+ let id = item.getAttribute("data-id");
+ this.emit(EVENTS.HEADER_CONTEXT_MENU, id, checked);
+ checked = this.menupopup.querySelectorAll("menuitem[checked]");
+ let disabled = this.menupopup.querySelectorAll("menuitem[disabled]");
+ if (checked.length == 2) {
+ checked[checked.length - 1].setAttribute("disabled", "true");
+ } else if (disabled.length > 1) {
+ disabled[disabled.length - 1].removeAttribute("disabled");
+ }
+ },
+
+ /**
+ * Creates the columns in the table. Without calling this method, data cannot
+ * be inserted into the table unless `initialColumns` was supplied.
+ *
+ * @param {object} columns
+ * A key value pair representing the columns of the table. Where the
+ * key represents the id of the column and the value is the displayed
+ * label in the header of the column.
+ * @param {string} sortOn
+ * The id of the column on which the table will be initially sorted on.
+ * @param {array} hiddenColumns
+ * Ids of all the columns that are hidden by default.
+ */
+ setColumns: function (columns, sortOn = this.sortedOn, hiddenColumns = []) {
+ for (let column of this.columns.values()) {
+ column.destroy();
+ }
+
+ this.columns.clear();
+
+ if (!(sortOn in columns)) {
+ sortOn = null;
+ }
+
+ if (!(this.firstColumn in columns)) {
+ this.firstColumn = null;
+ }
+
+ if (this.firstColumn) {
+ this.columns.set(this.firstColumn,
+ new Column(this, this.firstColumn, columns[this.firstColumn]));
+ }
+
+ for (let id in columns) {
+ if (!sortOn) {
+ sortOn = id;
+ }
+
+ if (this.firstColumn && id == this.firstColumn) {
+ continue;
+ }
+
+ this.columns.set(id, new Column(this, id, columns[id]));
+ if (hiddenColumns.indexOf(id) > -1) {
+ this.columns.get(id).toggleColumn();
+ }
+ }
+ this.sortedOn = sortOn;
+ this.sortBy(this.sortedOn);
+ this.populateMenuPopup();
+ },
+
+ /**
+ * Returns true if the passed string or the row json object corresponds to the
+ * selected item in the table.
+ */
+ isSelected: function (item) {
+ if (typeof item == "object") {
+ item = item[this.uniqueId];
+ }
+
+ return this.selectedRow && item == this.selectedRow[this.uniqueId];
+ },
+
+ /**
+ * Selects the row corresponding to the `id` json.
+ */
+ selectRow: function (id) {
+ this.selectedRow = id;
+ },
+
+ /**
+ * Selects the next row. Cycles over to the first row if last row is selected
+ */
+ selectNextRow: function () {
+ for (let column of this.columns.values()) {
+ column.selectNextRow();
+ }
+ },
+
+ /**
+ * Selects the previous row. Cycles over to the last row if first row is
+ * selected.
+ */
+ selectPreviousRow: function () {
+ for (let column of this.columns.values()) {
+ column.selectPreviousRow();
+ }
+ },
+
+ /**
+ * Clears any selected row.
+ */
+ clearSelection: function () {
+ this.selectedIndex = -1;
+ },
+
+ /**
+ * Adds a row into the table.
+ *
+ * @param {object} item
+ * The object from which the key-value pairs will be taken and added
+ * into the row. This object can have any arbitarary key value pairs,
+ * but only those will be used whose keys match to the ids of the
+ * columns.
+ * @param {boolean} suppressFlash
+ * true to not flash the row while inserting the row.
+ */
+ push: function (item, suppressFlash) {
+ if (!this.sortedOn || !this.columns) {
+ console.error("Can't insert item without defining columns first");
+ return;
+ }
+
+ if (this.items.has(item[this.uniqueId])) {
+ this.update(item);
+ return;
+ }
+
+ let index = this.columns.get(this.sortedOn).push(item);
+ for (let [key, column] of this.columns) {
+ if (key != this.sortedOn) {
+ column.insertAt(item, index);
+ }
+ column.updateZebra();
+ }
+ this.items.set(item[this.uniqueId], item);
+ this.tbody.removeAttribute("empty");
+
+ if (!suppressFlash) {
+ this.emit(EVENTS.ROW_UPDATED, item[this.uniqueId]);
+ }
+
+ this.emit(EVENTS.ROW_EDIT, item[this.uniqueId]);
+ },
+
+ /**
+ * Removes the row associated with the `item` object.
+ */
+ remove: function (item) {
+ if (typeof item != "object") {
+ item = this.items.get(item);
+ }
+ if (!item) {
+ return;
+ }
+ let removed = this.items.delete(item[this.uniqueId]);
+
+ if (!removed) {
+ return;
+ }
+ for (let column of this.columns.values()) {
+ column.remove(item);
+ column.updateZebra();
+ }
+ if (this.items.size == 0) {
+ this.tbody.setAttribute("empty", "empty");
+ }
+
+ this.emit(EVENTS.ROW_REMOVED, item);
+ },
+
+ /**
+ * Updates the items in the row corresponding to the `item` object previously
+ * used to insert the row using `push` method. The linking is done via the
+ * `uniqueId` key's value.
+ */
+ update: function (item) {
+ let oldItem = this.items.get(item[this.uniqueId]);
+ if (!oldItem) {
+ return;
+ }
+ this.items.set(item[this.uniqueId], item);
+
+ let changed = false;
+ for (let column of this.columns.values()) {
+ if (item[column.id] != oldItem[column.id]) {
+ column.update(item);
+ changed = true;
+ }
+ }
+ if (changed) {
+ this.emit(EVENTS.ROW_UPDATED, item[this.uniqueId]);
+ this.emit(EVENTS.ROW_EDIT, item[this.uniqueId]);
+ }
+ },
+
+ /**
+ * Removes all of the rows from the table.
+ */
+ clear: function () {
+ this.items.clear();
+ for (let column of this.columns.values()) {
+ column.clear();
+ }
+ this.tbody.setAttribute("empty", "empty");
+ this.setPlaceholderText(this.emptyText);
+
+ this.emit(EVENTS.TABLE_CLEARED, this);
+ },
+
+ /**
+ * Sorts the table by a given column.
+ *
+ * @param {string} column
+ * The id of the column on which the table should be sorted.
+ */
+ sortBy: function (column) {
+ this.emit(EVENTS.COLUMN_SORTED, column);
+ this.sortedOn = column;
+
+ if (!this.items.size) {
+ return;
+ }
+
+ let sortedItems = this.columns.get(column).sort([...this.items.values()]);
+ for (let [id, col] of this.columns) {
+ if (id != col) {
+ col.sort(sortedItems);
+ }
+ }
+ },
+
+ /**
+ * Filters the table based on a specific value
+ *
+ * @param {String} value: The filter value
+ * @param {Array} ignoreProps: Props to ignore while filtering
+ */
+ filterItems(value, ignoreProps = []) {
+ if (this.filteredValue == value) {
+ return;
+ }
+ if (this._editableFieldsEngine) {
+ this._editableFieldsEngine.completeEdit();
+ }
+
+ this.filteredValue = value;
+ if (!value) {
+ this.emit(EVENTS.TABLE_FILTERED, []);
+ return;
+ }
+ // Shouldn't be case-sensitive
+ value = value.toLowerCase();
+
+ let itemsToHide = [...this.items.keys()];
+ // Loop through all items and hide unmatched items
+ for (let [id, val] of this.items) {
+ for (let prop in val) {
+ if (ignoreProps.includes(prop)) {
+ continue;
+ }
+ let propValue = val[prop].toString().toLowerCase();
+ if (propValue.includes(value)) {
+ itemsToHide.splice(itemsToHide.indexOf(id), 1);
+ break;
+ }
+ }
+ }
+ this.emit(EVENTS.TABLE_FILTERED, itemsToHide);
+ },
+
+ /**
+ * Calls the afterScroll function when the user has stopped scrolling
+ */
+ onScroll: function () {
+ clearNamedTimeout("table-scroll");
+ setNamedTimeout("table-scroll", AFTER_SCROLL_DELAY, this.afterScroll);
+ },
+
+ /**
+ * Emits the "scroll-end" event when the whole table is scrolled
+ */
+ afterScroll: function () {
+ let scrollHeight = this.tbody.getBoundingClientRect().height -
+ this.tbody.querySelector(".table-widget-column-header").clientHeight;
+
+ // Emit scroll-end event when 9/10 of the table is scrolled
+ if (this.tbody.scrollTop >= 0.9 * scrollHeight) {
+ this.emit("scroll-end");
+ }
+ }
+};
+
+TableWidget.EVENTS = EVENTS;
+
+module.exports.TableWidget = TableWidget;
+
+/**
+ * A single column object in the table.
+ *
+ * @param {TableWidget} table
+ * The table object to which the column belongs.
+ * @param {string} id
+ * Id of the column.
+ * @param {String} header
+ * The displayed string on the column's header.
+ */
+function Column(table, id, header) {
+ this.tbody = table.tbody;
+ this.document = table.document;
+ this.window = table.window;
+ this.id = id;
+ this.uniqueId = table.uniqueId;
+ this.wrapTextInElements = table.wrapTextInElements;
+ this.table = table;
+ this.cells = [];
+ this.items = {};
+
+ this.highlightUpdated = table.highlightUpdated;
+
+ // This wrapping element is required solely so that position:sticky works on
+ // the headers of the columns.
+ this.wrapper = this.document.createElementNS(XUL_NS, "vbox");
+ this.wrapper.className = "table-widget-wrapper";
+ this.wrapper.setAttribute("flex", "1");
+ this.wrapper.setAttribute("tabindex", "0");
+ this.tbody.appendChild(this.wrapper);
+
+ this.splitter = this.document.createElementNS(XUL_NS, "splitter");
+ this.splitter.className = "devtools-side-splitter";
+ this.tbody.appendChild(this.splitter);
+
+ this.column = this.document.createElementNS(HTML_NS, "div");
+ this.column.id = id;
+ this.column.className = "table-widget-column";
+ this.wrapper.appendChild(this.column);
+
+ this.header = this.document.createElementNS(XUL_NS, "label");
+ this.header.className = "devtools-toolbar table-widget-column-header";
+ this.header.setAttribute("value", header);
+ this.column.appendChild(this.header);
+ if (table.headersContextMenu) {
+ this.header.setAttribute("context", table.headersContextMenu);
+ }
+ this.toggleColumn = this.toggleColumn.bind(this);
+ this.table.on(EVENTS.HEADER_CONTEXT_MENU, this.toggleColumn);
+
+ this.onColumnSorted = this.onColumnSorted.bind(this);
+ this.table.on(EVENTS.COLUMN_SORTED, this.onColumnSorted);
+
+ this.onRowUpdated = this.onRowUpdated.bind(this);
+ this.table.on(EVENTS.ROW_UPDATED, this.onRowUpdated);
+
+ this.onTableFiltered = this.onTableFiltered.bind(this);
+ this.table.on(EVENTS.TABLE_FILTERED, this.onTableFiltered);
+
+ this.onClick = this.onClick.bind(this);
+ this.onMousedown = this.onMousedown.bind(this);
+ this.column.addEventListener("click", this.onClick);
+ this.column.addEventListener("mousedown", this.onMousedown);
+}
+
+Column.prototype = {
+
+ // items is a cell-id to cell-index map. It is basically a reverse map of the
+ // this.cells object and is used to quickly reverse lookup a cell by its id
+ // instead of looping through the cells array. This reverse map is not kept
+ // upto date in sync with the cells array as updating it is in itself a loop
+ // through all the cells of the columns. Thus update it on demand when it goes
+ // out of sync with this.cells.
+ items: null,
+
+ // _itemsDirty is a flag which becomes true when this.items goes out of sync
+ // with this.cells
+ _itemsDirty: null,
+
+ selectedRow: null,
+
+ cells: null,
+
+ /**
+ * Gets whether the table is sorted on this column or not.
+ * 0 - not sorted.
+ * 1 - ascending order
+ * 2 - descending order
+ */
+ get sorted() {
+ return this._sortState || 0;
+ },
+
+ /**
+ * Sets the sorted value
+ */
+ set sorted(value) {
+ if (!value) {
+ this.header.removeAttribute("sorted");
+ } else {
+ this.header.setAttribute("sorted",
+ value == 1 ? "ascending" : "descending");
+ }
+ this._sortState = value;
+ },
+
+ /**
+ * Gets the selected row in the column.
+ */
+ get selectedIndex() {
+ if (!this.selectedRow) {
+ return -1;
+ }
+ return this.items[this.selectedRow];
+ },
+
+ get cellNodes() {
+ return [...this.column.querySelectorAll(".table-widget-cell")];
+ },
+
+ get visibleCellNodes() {
+ let editor = this.table._editableFieldsEngine;
+ let nodes = this.cellNodes.filter(node => {
+ // If the cell is currently being edited we should class it as visible.
+ if (editor && editor.currentTarget === node) {
+ return true;
+ }
+ return node.clientWidth !== 0;
+ });
+
+ return nodes;
+ },
+
+ /**
+ * Called when the column is sorted by.
+ *
+ * @param {string} event
+ * The event name of the event. i.e. EVENTS.COLUMN_SORTED
+ * @param {string} column
+ * The id of the column being sorted by.
+ */
+ onColumnSorted: function (event, column) {
+ if (column != this.id) {
+ this.sorted = 0;
+ return;
+ } else if (this.sorted == 0 || this.sorted == 2) {
+ this.sorted = 1;
+ } else {
+ this.sorted = 2;
+ }
+ this.updateZebra();
+ },
+
+ onTableFiltered: function (event, itemsToHide) {
+ this._updateItems();
+ if (!this.cells) {
+ return;
+ }
+ for (let cell of this.cells) {
+ cell.hidden = false;
+ }
+ for (let id of itemsToHide) {
+ this.cells[this.items[id]].hidden = true;
+ }
+ this.updateZebra();
+ },
+
+ /**
+ * Called when a row is updated.
+ *
+ * @param {string} event
+ * The event name of the event. i.e. EVENTS.ROW_UPDATED
+ * @param {string} id
+ * The unique id of the object associated with the row.
+ */
+ onRowUpdated: function (event, id) {
+ this._updateItems();
+ if (this.highlightUpdated && this.items[id] != null) {
+ if (this.table.editBookmark) {
+ // A rows position in the table can change as the result of an edit. In
+ // order to ensure that the correct row is highlighted after an edit we
+ // save the uniqueId in editBookmark. Here we send the signal that the
+ // row has been edited and that the row needs to be selected again.
+ this.table.emit(EVENTS.ROW_SELECTED, this.table.editBookmark);
+ this.table.editBookmark = null;
+ }
+
+ this.cells[this.items[id]].flash();
+ }
+ this.updateZebra();
+ },
+
+ destroy: function () {
+ this.table.off(EVENTS.COLUMN_SORTED, this.onColumnSorted);
+ this.table.off(EVENTS.HEADER_CONTEXT_MENU, this.toggleColumn);
+ this.table.off(EVENTS.ROW_UPDATED, this.onRowUpdated);
+ this.table.off(EVENTS.TABLE_FILTERED, this.onTableFiltered);
+
+ this.column.removeEventListener("click", this.onClick);
+ this.column.removeEventListener("mousedown", this.onMousedown);
+
+ this.splitter.remove();
+ this.column.parentNode.remove();
+ this.cells = null;
+ this.items = null;
+ this.selectedRow = null;
+ },
+
+ /**
+ * Selects the row at the `index` index
+ */
+ selectRowAt: function (index) {
+ if (this.selectedRow != null) {
+ this.cells[this.items[this.selectedRow]].toggleClass("theme-selected");
+ }
+ if (index < 0) {
+ this.selectedRow = null;
+ return;
+ }
+ let cell = this.cells[index];
+ cell.toggleClass("theme-selected");
+ this.selectedRow = cell.id;
+ },
+
+ /**
+ * Selects the row with the object having the `uniqueId` value as `id`
+ */
+ selectRow: function (id) {
+ this._updateItems();
+ this.selectRowAt(this.items[id]);
+ },
+
+ /**
+ * Selects the next row. Cycles to first if last row is selected.
+ */
+ selectNextRow: function () {
+ this._updateItems();
+ let index = this.items[this.selectedRow] + 1;
+ if (index == this.cells.length) {
+ index = 0;
+ }
+ this.selectRowAt(index);
+ },
+
+ /**
+ * Selects the previous row. Cycles to last if first row is selected.
+ */
+ selectPreviousRow: function () {
+ this._updateItems();
+ let index = this.items[this.selectedRow] - 1;
+ if (index == -1) {
+ index = this.cells.length - 1;
+ }
+ this.selectRowAt(index);
+ },
+
+ /**
+ * Pushes the `item` object into the column. If this column is sorted on,
+ * then inserts the object at the right position based on the column's id
+ * key's value.
+ *
+ * @returns {number}
+ * The index of the currently pushed item.
+ */
+ push: function (item) {
+ let value = item[this.id];
+
+ if (this.sorted) {
+ let index;
+ if (this.sorted == 1) {
+ index = this.cells.findIndex(element => {
+ return value < element.value;
+ });
+ } else {
+ index = this.cells.findIndex(element => {
+ return value > element.value;
+ });
+ }
+ index = index >= 0 ? index : this.cells.length;
+ if (index < this.cells.length) {
+ this._itemsDirty = true;
+ }
+ this.items[item[this.uniqueId]] = index;
+ this.cells.splice(index, 0, new Cell(this, item, this.cells[index]));
+ return index;
+ }
+
+ this.items[item[this.uniqueId]] = this.cells.length;
+ return this.cells.push(new Cell(this, item)) - 1;
+ },
+
+ /**
+ * Inserts the `item` object at the given `index` index in the table.
+ */
+ insertAt: function (item, index) {
+ if (index < this.cells.length) {
+ this._itemsDirty = true;
+ }
+ this.items[item[this.uniqueId]] = index;
+ this.cells.splice(index, 0, new Cell(this, item, this.cells[index]));
+ this.updateZebra();
+ },
+
+ /**
+ * Event handler for the command event coming from the header context menu.
+ * Toggles the column if it was requested by the user.
+ * When called explicitly without parameters, it toggles the corresponding
+ * column.
+ *
+ * @param {string} event
+ * The name of the event. i.e. EVENTS.HEADER_CONTEXT_MENU
+ * @param {string} id
+ * Id of the column to be toggled
+ * @param {string} checked
+ * true if the column is visible
+ */
+ toggleColumn: function (event, id, checked) {
+ if (arguments.length == 0) {
+ // Act like a toggling method when called with no params
+ id = this.id;
+ checked = this.wrapper.hasAttribute("hidden");
+ }
+ if (id != this.id) {
+ return;
+ }
+ if (checked) {
+ this.wrapper.removeAttribute("hidden");
+ } else {
+ this.wrapper.setAttribute("hidden", "true");
+ }
+ },
+
+ /**
+ * Removes the corresponding item from the column.
+ */
+ remove: function (item) {
+ this._updateItems();
+ let index = this.items[item[this.uniqueId]];
+ if (index == null) {
+ return;
+ }
+
+ if (index < this.cells.length) {
+ this._itemsDirty = true;
+ }
+ this.cells[index].destroy();
+ this.cells.splice(index, 1);
+ delete this.items[item[this.uniqueId]];
+ },
+
+ /**
+ * Updates the corresponding item from the column.
+ */
+ update: function (item) {
+ this._updateItems();
+
+ let index = this.items[item[this.uniqueId]];
+ if (index == null) {
+ return;
+ }
+
+ this.cells[index].value = item[this.id];
+ },
+
+ /**
+ * Updates the `this.items` cell-id vs cell-index map to be in sync with
+ * `this.cells`.
+ */
+ _updateItems: function () {
+ if (!this._itemsDirty) {
+ return;
+ }
+ for (let i = 0; i < this.cells.length; i++) {
+ this.items[this.cells[i].id] = i;
+ }
+ this._itemsDirty = false;
+ },
+
+ /**
+ * Clears the current column
+ */
+ clear: function () {
+ this.cells = [];
+ this.items = {};
+ this._itemsDirty = false;
+ this.selectedRow = null;
+ while (this.header.nextSibling) {
+ this.header.nextSibling.remove();
+ }
+ },
+
+ /**
+ * Sorts the given items and returns the sorted list if the table was sorted
+ * by this column.
+ */
+ sort: function (items) {
+ // Only sort the array if we are sorting based on this column
+ if (this.sorted == 1) {
+ items.sort((a, b) => {
+ let val1 = (a[this.id] instanceof Node) ?
+ a[this.id].textContent : a[this.id];
+ let val2 = (b[this.id] instanceof Node) ?
+ b[this.id].textContent : b[this.id];
+ return val1 > val2;
+ });
+ } else if (this.sorted > 1) {
+ items.sort((a, b) => {
+ let val1 = (a[this.id] instanceof Node) ?
+ a[this.id].textContent : a[this.id];
+ let val2 = (b[this.id] instanceof Node) ?
+ b[this.id].textContent : b[this.id];
+ return val2 > val1;
+ });
+ }
+
+ if (this.selectedRow) {
+ this.cells[this.items[this.selectedRow]].toggleClass("theme-selected");
+ }
+ this.items = {};
+ // Otherwise, just use the sorted array passed to update the cells value.
+ items.forEach((item, i) => {
+ this.items[item[this.uniqueId]] = i;
+ this.cells[i].value = item[this.id];
+ this.cells[i].id = item[this.uniqueId];
+ });
+ if (this.selectedRow) {
+ this.cells[this.items[this.selectedRow]].toggleClass("theme-selected");
+ }
+ this._itemsDirty = false;
+ this.updateZebra();
+ return items;
+ },
+
+ updateZebra() {
+ this._updateItems();
+ let i = 0;
+ for (let cell of this.cells) {
+ if (!cell.hidden) {
+ i++;
+ }
+ cell.toggleClass("even", !(i % 2));
+ }
+ },
+
+ /**
+ * Click event handler for the column. Used to detect click on header for
+ * for sorting.
+ */
+ onClick: function (event) {
+ let target = event.originalTarget;
+
+ if (target.nodeType !== target.ELEMENT_NODE || target == this.column) {
+ return;
+ }
+
+ if (event.button == 0 && target == this.header) {
+ this.table.sortBy(this.id);
+ }
+ },
+
+ /**
+ * Mousedown event handler for the column. Used to select rows.
+ */
+ onMousedown: function (event) {
+ let target = event.originalTarget;
+
+ if (target.nodeType !== target.ELEMENT_NODE ||
+ target == this.column ||
+ target == this.header) {
+ return;
+ }
+ if (event.button == 0) {
+ let closest = target.closest("[data-id]");
+ if (!closest) {
+ return;
+ }
+
+ let dataid = closest.getAttribute("data-id");
+ this.table.emit(EVENTS.ROW_SELECTED, dataid);
+ }
+ },
+};
+
+/**
+ * A single cell in a column
+ *
+ * @param {Column} column
+ * The column object to which the cell belongs.
+ * @param {object} item
+ * The object representing the row. It contains a key value pair
+ * representing the column id and its associated value. The value
+ * can be a DOMNode that is appended or a string value.
+ * @param {Cell} nextCell
+ * The cell object which is next to this cell. null if this cell is last
+ * cell of the column
+ */
+function Cell(column, item, nextCell) {
+ let document = column.document;
+
+ this.wrapTextInElements = column.wrapTextInElements;
+ this.label = document.createElementNS(XUL_NS, "label");
+ this.label.setAttribute("crop", "end");
+ this.label.className = "plain table-widget-cell";
+
+ if (nextCell) {
+ column.column.insertBefore(this.label, nextCell.label);
+ } else {
+ column.column.appendChild(this.label);
+ }
+
+ if (column.table.cellContextMenuId) {
+ this.label.setAttribute("context", column.table.cellContextMenuId);
+ this.label.addEventListener("contextmenu", (event) => {
+ // Make the ID of the clicked cell available as a property on the table.
+ // It's then available for the popupshowing or command handler.
+ column.table.contextMenuRowId = this.id;
+ }, false);
+ }
+
+ this.value = item[column.id];
+ this.id = item[column.uniqueId];
+}
+
+Cell.prototype = {
+
+ set id(value) {
+ this._id = value;
+ this.label.setAttribute("data-id", value);
+ },
+
+ get id() {
+ return this._id;
+ },
+
+ get hidden() {
+ return this.label.hasAttribute("hidden");
+ },
+
+ set hidden(value) {
+ if (value) {
+ this.label.setAttribute("hidden", "hidden");
+ } else {
+ this.label.removeAttribute("hidden");
+ }
+ },
+
+ set value(value) {
+ this._value = value;
+ if (value == null) {
+ this.label.setAttribute("value", "");
+ return;
+ }
+
+ if (this.wrapTextInElements && !(value instanceof Node)) {
+ let span = this.label.ownerDocument.createElementNS(HTML_NS, "span");
+ span.textContent = value;
+ value = span;
+ }
+
+ if (value instanceof Node) {
+ this.label.removeAttribute("value");
+
+ while (this.label.firstChild) {
+ this.label.removeChild(this.label.firstChild);
+ }
+
+ this.label.appendChild(value);
+ } else {
+ this.label.setAttribute("value", value + "");
+ }
+ },
+
+ get value() {
+ return this._value;
+ },
+
+ toggleClass: function (className, condition) {
+ this.label.classList.toggle(className, condition);
+ },
+
+ /**
+ * Flashes the cell for a brief time. This when done for with cells in all
+ * columns, makes it look like the row is being highlighted/flashed.
+ */
+ flash: function () {
+ if (!this.label.parentNode) {
+ return;
+ }
+ this.label.classList.remove("flash-out");
+ // Cause a reflow so that the animation retriggers on adding back the class
+ let a = this.label.parentNode.offsetWidth; // eslint-disable-line
+ let onAnimEnd = () => {
+ this.label.classList.remove("flash-out");
+ this.label.removeEventListener("animationend", onAnimEnd);
+ };
+ this.label.addEventListener("animationend", onAnimEnd);
+ this.label.classList.add("flash-out");
+ },
+
+ focus: function () {
+ this.label.focus();
+ },
+
+ destroy: function () {
+ this.label.remove();
+ this.label = null;
+ }
+};
+
+/**
+ * Simple widget to make nodes matching a CSS selector editable.
+ *
+ * @param {Object} options
+ * An object with the following format:
+ * {
+ * // The node that will act as a container for the editor e.g. a
+ * // div or table.
+ * root: someNode,
+ *
+ * // The onTab event to be handled by the caller.
+ * onTab: function(event) { ... }
+ *
+ * // Optional event used to trigger the editor. By default this is
+ * // dblclick.
+ * onTriggerEvent: "dblclick",
+ *
+ * // Array or comma separated string of CSS Selectors matching
+ * // elements that are to be made editable.
+ * selectors: [
+ * "#name .table-widget-cell",
+ * "#value .table-widget-cell"
+ * ]
+ * }
+ */
+function EditableFieldsEngine(options) {
+ EventEmitter.decorate(this);
+
+ if (!Array.isArray(options.selectors)) {
+ options.selectors = [options.selectors];
+ }
+
+ this.root = options.root;
+ this.selectors = options.selectors;
+ this.onTab = options.onTab;
+ this.onTriggerEvent = options.onTriggerEvent || "dblclick";
+
+ this.edit = this.edit.bind(this);
+ this.cancelEdit = this.cancelEdit.bind(this);
+ this.destroy = this.destroy.bind(this);
+
+ this.onTrigger = this.onTrigger.bind(this);
+ this.root.addEventListener(this.onTriggerEvent, this.onTrigger);
+}
+
+EditableFieldsEngine.prototype = {
+ INPUT_ID: "inlineEditor",
+
+ get changePending() {
+ return this.isEditing && (this.textbox.value !== this.currentValue);
+ },
+
+ get isEditing() {
+ return this.root && !this.textbox.hidden;
+ },
+
+ get textbox() {
+ if (!this._textbox) {
+ let doc = this.root.ownerDocument;
+ this._textbox = doc.createElementNS(XUL_NS, "textbox");
+ this._textbox.id = this.INPUT_ID;
+
+ this._textbox.setAttribute("flex", "1");
+
+ this.onKeydown = this.onKeydown.bind(this);
+ this._textbox.addEventListener("keydown", this.onKeydown);
+
+ this.completeEdit = this.completeEdit.bind(this);
+ doc.addEventListener("blur", this.completeEdit);
+ }
+
+ return this._textbox;
+ },
+
+ /**
+ * Called when a trigger event is detected (default is dblclick).
+ *
+ * @param {EventTarget} target
+ * Calling event's target.
+ */
+ onTrigger: function ({target}) {
+ this.edit(target);
+ },
+
+ /**
+ * Handle keypresses when in edit mode:
+ * - <escape> revert the value and close the textbox.
+ * - <return> apply the value and close the textbox.
+ * - <tab> Handled by the consumer's `onTab` callback.
+ * - <shift><tab> Handled by the consumer's `onTab` callback.
+ *
+ * @param {Event} event
+ * The calling event.
+ */
+ onKeydown: function (event) {
+ if (!this.textbox) {
+ return;
+ }
+
+ switch (event.keyCode) {
+ case KeyCodes.DOM_VK_ESCAPE:
+ this.cancelEdit();
+ event.preventDefault();
+ break;
+ case KeyCodes.DOM_VK_RETURN:
+ this.completeEdit();
+ break;
+ case KeyCodes.DOM_VK_TAB:
+ if (this.onTab) {
+ this.onTab(event);
+ }
+ break;
+ }
+ },
+
+ /**
+ * Overlay the target node with an edit field.
+ *
+ * @param {Node} target
+ * Dom node to be edited.
+ */
+ edit: function (target) {
+ if (!target) {
+ return;
+ }
+
+ target.scrollIntoView(false);
+ target.focus();
+
+ if (!target.matches(this.selectors.join(","))) {
+ return;
+ }
+
+ // If we are actively editing something complete the edit first.
+ if (this.isEditing) {
+ this.completeEdit();
+ }
+
+ this.copyStyles(target, this.textbox);
+
+ target.parentNode.insertBefore(this.textbox, target);
+ this.currentTarget = target;
+ this.textbox.value = this.currentValue = target.value;
+ target.hidden = true;
+ this.textbox.hidden = false;
+
+ this.textbox.focus();
+ this.textbox.select();
+ },
+
+ completeEdit: function () {
+ if (!this.isEditing) {
+ return;
+ }
+
+ let oldValue = this.currentValue;
+ let newValue = this.textbox.value;
+ let changed = oldValue !== newValue;
+
+ this.textbox.hidden = true;
+
+ if (!this.currentTarget) {
+ return;
+ }
+
+ this.currentTarget.hidden = false;
+ if (changed) {
+ this.currentTarget.value = newValue;
+
+ let data = {
+ change: {
+ field: this.currentTarget,
+ oldValue: oldValue,
+ newValue: newValue
+ }
+ };
+
+ this.emit("change", data);
+ }
+ },
+
+ /**
+ * Cancel an edit.
+ */
+ cancelEdit: function () {
+ if (!this.isEditing) {
+ return;
+ }
+ if (this.currentTarget) {
+ this.currentTarget.hidden = false;
+ }
+
+ this.textbox.hidden = true;
+ },
+
+ /**
+ * Stop edit mode and apply changes.
+ */
+ blur: function () {
+ if (this.isEditing) {
+ this.completeEdit();
+ }
+ },
+
+ /**
+ * Copies various styles from one node to another.
+ *
+ * @param {Node} source
+ * The node to copy styles from.
+ * @param {Node} destination [description]
+ * The node to copy styles to.
+ */
+ copyStyles: function (source, destination) {
+ let style = source.ownerDocument.defaultView.getComputedStyle(source);
+ let props = [
+ "borderTopWidth",
+ "borderRightWidth",
+ "borderBottomWidth",
+ "borderLeftWidth",
+ "fontFamily",
+ "fontSize",
+ "fontWeight",
+ "height",
+ "marginTop",
+ "marginRight",
+ "marginBottom",
+ "marginLeft",
+ "marginInlineStart",
+ "marginInlineEnd"
+ ];
+
+ for (let prop of props) {
+ destination.style[prop] = style[prop];
+ }
+
+ // We need to set the label width to 100% to work around a XUL flex bug.
+ destination.style.width = "100%";
+ },
+
+ /**
+ * Destroys all editors in the current document.
+ */
+ destroy: function () {
+ if (this.textbox) {
+ this.textbox.removeEventListener("keydown", this.onKeydown);
+ this.textbox.remove();
+ }
+
+ if (this.root) {
+ this.root.removeEventListener(this.onTriggerEvent, this.onTrigger);
+ this.root.ownerDocument.removeEventListener("blur", this.completeEdit);
+ }
+
+ this._textbox = this.root = this.selectors = this.onTab = null;
+ this.currentTarget = this.currentValue = null;
+
+ this.emit("destroyed");
+ },
+};
diff --git a/devtools/client/shared/widgets/TreeWidget.js b/devtools/client/shared/widgets/TreeWidget.js
new file mode 100644
index 000000000..1f766cc6b
--- /dev/null
+++ b/devtools/client/shared/widgets/TreeWidget.js
@@ -0,0 +1,605 @@
+/* -*- 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 HTML_NS = "http://www.w3.org/1999/xhtml";
+
+const EventEmitter = require("devtools/shared/event-emitter");
+const {KeyCodes} = require("devtools/client/shared/keycodes");
+
+/**
+ * A tree widget with keyboard navigation and collapsable structure.
+ *
+ * @param {nsIDOMNode} node
+ * The container element for the tree widget.
+ * @param {Object} options
+ * - emptyText {string}: text to display when no entries in the table.
+ * - defaultType {string}: The default type of the tree items. For ex.
+ * 'js'
+ * - sorted {boolean}: Defaults to true. If true, tree items are kept in
+ * lexical order. If false, items will be kept in insertion order.
+ * - contextMenuId {string}: ID of context menu to be displayed on
+ * tree items.
+ */
+function TreeWidget(node, options = {}) {
+ EventEmitter.decorate(this);
+
+ this.document = node.ownerDocument;
+ this.window = this.document.defaultView;
+ this._parent = node;
+
+ this.emptyText = options.emptyText || "";
+ this.defaultType = options.defaultType;
+ this.sorted = options.sorted !== false;
+ this.contextMenuId = options.contextMenuId;
+
+ this.setupRoot();
+
+ this.placeholder = this.document.createElementNS(HTML_NS, "label");
+ this.placeholder.className = "tree-widget-empty-text";
+ this._parent.appendChild(this.placeholder);
+
+ if (this.emptyText) {
+ this.setPlaceholderText(this.emptyText);
+ }
+ // A map to hold all the passed attachment to each leaf in the tree.
+ this.attachments = new Map();
+}
+
+TreeWidget.prototype = {
+
+ _selectedLabel: null,
+ _selectedItem: null,
+
+ /**
+ * Select any node in the tree.
+ *
+ * @param {array} ids
+ * An array of ids leading upto the selected item
+ */
+ set selectedItem(ids) {
+ if (this._selectedLabel) {
+ this._selectedLabel.classList.remove("theme-selected");
+ }
+ let currentSelected = this._selectedLabel;
+ if (ids == -1) {
+ this._selectedLabel = this._selectedItem = null;
+ return;
+ }
+ if (!Array.isArray(ids)) {
+ return;
+ }
+ this._selectedLabel = this.root.setSelectedItem(ids);
+ if (!this._selectedLabel) {
+ this._selectedItem = null;
+ } else {
+ if (currentSelected != this._selectedLabel) {
+ this.ensureSelectedVisible();
+ }
+ this._selectedItem = ids;
+ this.emit("select", this._selectedItem,
+ this.attachments.get(JSON.stringify(ids)));
+ }
+ },
+
+ /**
+ * Gets the selected item in the tree.
+ *
+ * @return {array}
+ * An array of ids leading upto the selected item
+ */
+ get selectedItem() {
+ return this._selectedItem;
+ },
+
+ /**
+ * Returns if the passed array corresponds to the selected item in the tree.
+ *
+ * @return {array}
+ * An array of ids leading upto the requested item
+ */
+ isSelected: function (item) {
+ if (!this._selectedItem || this._selectedItem.length != item.length) {
+ return false;
+ }
+
+ for (let i = 0; i < this._selectedItem.length; i++) {
+ if (this._selectedItem[i] != item[i]) {
+ return false;
+ }
+ }
+
+ return true;
+ },
+
+ destroy: function () {
+ this.root.remove();
+ this.root = null;
+ },
+
+ /**
+ * Sets up the root container of the TreeWidget.
+ */
+ setupRoot: function () {
+ this.root = new TreeItem(this.document);
+ if (this.contextMenuId) {
+ this.root.children.addEventListener("contextmenu", (event) => {
+ let menu = this.document.getElementById(this.contextMenuId);
+ menu.openPopupAtScreen(event.screenX, event.screenY, true);
+ });
+ }
+
+ this._parent.appendChild(this.root.children);
+
+ this.root.children.addEventListener("mousedown", e => this.onClick(e));
+ this.root.children.addEventListener("keypress", e => this.onKeypress(e));
+ },
+
+ /**
+ * Sets the text to be shown when no node is present in the tree
+ */
+ setPlaceholderText: function (text) {
+ this.placeholder.textContent = text;
+ },
+
+ /**
+ * Select any node in the tree.
+ *
+ * @param {array} id
+ * An array of ids leading upto the selected item
+ */
+ selectItem: function (id) {
+ this.selectedItem = id;
+ },
+
+ /**
+ * Selects the next visible item in the tree.
+ */
+ selectNextItem: function () {
+ let next = this.getNextVisibleItem();
+ if (next) {
+ this.selectedItem = next;
+ }
+ },
+
+ /**
+ * Selects the previos visible item in the tree
+ */
+ selectPreviousItem: function () {
+ let prev = this.getPreviousVisibleItem();
+ if (prev) {
+ this.selectedItem = prev;
+ }
+ },
+
+ /**
+ * Returns the next visible item in the tree
+ */
+ getNextVisibleItem: function () {
+ let node = this._selectedLabel;
+ if (node.hasAttribute("expanded") && node.nextSibling.firstChild) {
+ return JSON.parse(node.nextSibling.firstChild.getAttribute("data-id"));
+ }
+ node = node.parentNode;
+ if (node.nextSibling) {
+ return JSON.parse(node.nextSibling.getAttribute("data-id"));
+ }
+ node = node.parentNode;
+ while (node.parentNode && node != this.root.children) {
+ if (node.parentNode && node.parentNode.nextSibling) {
+ return JSON.parse(node.parentNode.nextSibling.getAttribute("data-id"));
+ }
+ node = node.parentNode;
+ }
+ return null;
+ },
+
+ /**
+ * Returns the previous visible item in the tree
+ */
+ getPreviousVisibleItem: function () {
+ let node = this._selectedLabel.parentNode;
+ if (node.previousSibling) {
+ node = node.previousSibling.firstChild;
+ while (node.hasAttribute("expanded") && !node.hasAttribute("empty")) {
+ if (!node.nextSibling.lastChild) {
+ break;
+ }
+ node = node.nextSibling.lastChild.firstChild;
+ }
+ return JSON.parse(node.parentNode.getAttribute("data-id"));
+ }
+ node = node.parentNode;
+ if (node.parentNode && node != this.root.children) {
+ node = node.parentNode;
+ while (node.hasAttribute("expanded") && !node.hasAttribute("empty")) {
+ if (!node.nextSibling.firstChild) {
+ break;
+ }
+ node = node.nextSibling.firstChild.firstChild;
+ }
+ return JSON.parse(node.getAttribute("data-id"));
+ }
+ return null;
+ },
+
+ clearSelection: function () {
+ this.selectedItem = -1;
+ },
+
+ /**
+ * Adds an item in the tree. The item can be added as a child to any node in
+ * the tree. The method will also create any subnode not present in the
+ * process.
+ *
+ * @param {[string|object]} items
+ * An array of either string or objects where each increasing index
+ * represents an item corresponding to an equivalent depth in the tree.
+ * Each array element can be either just a string with the value as the
+ * id of of that item as well as the display value, or it can be an
+ * object with the following propeties:
+ * - id {string} The id of the item
+ * - label {string} The display value of the item
+ * - node {DOMNode} The dom node if you want to insert some custom
+ * element as the item. The label property is not used in this
+ * case
+ * - attachment {object} Any object to be associated with this item.
+ * - type {string} The type of this particular item. If this is null,
+ * then defaultType will be used.
+ * For example, if items = ["foo", "bar", { id: "id1", label: "baz" }]
+ * and the tree is empty, then the following hierarchy will be created
+ * in the tree:
+ * foo
+ * â”” bar
+ * â”” baz
+ * Passing the string id instead of the complete object helps when you
+ * are simply adding children to an already existing node and you know
+ * its id.
+ */
+ add: function (items) {
+ this.root.add(items, this.defaultType, this.sorted);
+ for (let i = 0; i < items.length; i++) {
+ if (items[i].attachment) {
+ this.attachments.set(JSON.stringify(
+ items.slice(0, i + 1).map(item => item.id || item)
+ ), items[i].attachment);
+ }
+ }
+ // Empty the empty-tree-text
+ this.setPlaceholderText("");
+ },
+
+ /**
+ * Removes the specified item and all of its child items from the tree.
+ *
+ * @param {array} item
+ * The array of ids leading up to the item.
+ */
+ remove: function (item) {
+ this.root.remove(item);
+ this.attachments.delete(JSON.stringify(item));
+ // Display the empty tree text
+ if (this.root.items.size == 0 && this.emptyText) {
+ this.setPlaceholderText(this.emptyText);
+ }
+ },
+
+ /**
+ * Removes all of the child nodes from this tree.
+ */
+ clear: function () {
+ this.root.remove();
+ this.setupRoot();
+ this.attachments.clear();
+ if (this.emptyText) {
+ this.setPlaceholderText(this.emptyText);
+ }
+ },
+
+ /**
+ * Expands the tree completely
+ */
+ expandAll: function () {
+ this.root.expandAll();
+ },
+
+ /**
+ * Collapses the tree completely
+ */
+ collapseAll: function () {
+ this.root.collapseAll();
+ },
+
+ /**
+ * Click handler for the tree. Used to select, open and close the tree nodes.
+ */
+ onClick: function (event) {
+ let target = event.originalTarget;
+ while (target && !target.classList.contains("tree-widget-item")) {
+ if (target == this.root.children) {
+ return;
+ }
+ target = target.parentNode;
+ }
+ if (!target) {
+ return;
+ }
+
+ if (target.hasAttribute("expanded")) {
+ target.removeAttribute("expanded");
+ } else {
+ target.setAttribute("expanded", "true");
+ }
+
+ if (this._selectedLabel != target) {
+ let ids = target.parentNode.getAttribute("data-id");
+ this.selectedItem = JSON.parse(ids);
+ }
+ },
+
+ /**
+ * Keypress handler for this tree. Used to select next and previous visible
+ * items, as well as collapsing and expanding any item.
+ */
+ onKeypress: function (event) {
+ switch (event.keyCode) {
+ case KeyCodes.DOM_VK_UP:
+ this.selectPreviousItem();
+ break;
+
+ case KeyCodes.DOM_VK_DOWN:
+ this.selectNextItem();
+ break;
+
+ case KeyCodes.DOM_VK_RIGHT:
+ if (this._selectedLabel.hasAttribute("expanded")) {
+ this.selectNextItem();
+ } else {
+ this._selectedLabel.setAttribute("expanded", "true");
+ }
+ break;
+
+ case KeyCodes.DOM_VK_LEFT:
+ if (this._selectedLabel.hasAttribute("expanded") &&
+ !this._selectedLabel.hasAttribute("empty")) {
+ this._selectedLabel.removeAttribute("expanded");
+ } else {
+ this.selectPreviousItem();
+ }
+ break;
+
+ default: return;
+ }
+ event.preventDefault();
+ },
+
+ /**
+ * Scrolls the viewport of the tree so that the selected item is always
+ * visible.
+ */
+ ensureSelectedVisible: function () {
+ let {top, bottom} = this._selectedLabel.getBoundingClientRect();
+ let height = this.root.children.parentNode.clientHeight;
+ if (top < 0) {
+ this._selectedLabel.scrollIntoView();
+ } else if (bottom > height) {
+ this._selectedLabel.scrollIntoView(false);
+ }
+ }
+};
+
+module.exports.TreeWidget = TreeWidget;
+
+/**
+ * Any item in the tree. This can be an empty leaf node also.
+ *
+ * @param {HTMLDocument} document
+ * The document element used for creating new nodes.
+ * @param {TreeItem} parent
+ * The parent item for this item.
+ * @param {string|DOMElement} label
+ * Either the dom node to be used as the item, or the string to be
+ * displayed for this node in the tree
+ * @param {string} type
+ * The type of the current node. For ex. "js"
+ */
+function TreeItem(document, parent, label, type) {
+ this.document = document;
+ this.node = this.document.createElementNS(HTML_NS, "li");
+ this.node.setAttribute("tabindex", "0");
+ this.isRoot = !parent;
+ this.parent = parent;
+ if (this.parent) {
+ this.level = this.parent.level + 1;
+ }
+ if (label) {
+ this.label = this.document.createElementNS(HTML_NS, "div");
+ this.label.setAttribute("empty", "true");
+ this.label.setAttribute("level", this.level);
+ this.label.className = "tree-widget-item";
+ if (type) {
+ this.label.setAttribute("type", type);
+ }
+ if (typeof label == "string") {
+ this.label.textContent = label;
+ } else {
+ this.label.appendChild(label);
+ }
+ this.node.appendChild(this.label);
+ }
+ this.children = this.document.createElementNS(HTML_NS, "ul");
+ if (this.isRoot) {
+ this.children.className = "tree-widget-container";
+ } else {
+ this.children.className = "tree-widget-children";
+ }
+ this.node.appendChild(this.children);
+ this.items = new Map();
+}
+
+TreeItem.prototype = {
+
+ items: null,
+
+ isSelected: false,
+
+ expanded: false,
+
+ isRoot: false,
+
+ parent: null,
+
+ children: null,
+
+ level: 0,
+
+ /**
+ * Adds the item to the sub tree contained by this node. The item to be
+ * inserted can be a direct child of this node, or further down the tree.
+ *
+ * @param {array} items
+ * Same as TreeWidget.add method's argument
+ * @param {string} defaultType
+ * The default type of the item to be used when items[i].type is null
+ * @param {boolean} sorted
+ * true if the tree items are inserted in a lexically sorted manner.
+ * Otherwise, false if the item are to be appended to their parent.
+ */
+ add: function (items, defaultType, sorted) {
+ if (items.length == this.level) {
+ // This is the exit condition of recursive TreeItem.add calls
+ return;
+ }
+ // Get the id and label corresponding to this level inside the tree.
+ let id = items[this.level].id || items[this.level];
+ if (this.items.has(id)) {
+ // An item with same id already exists, thus calling the add method of
+ // that child to add the passed node at correct position.
+ this.items.get(id).add(items, defaultType, sorted);
+ return;
+ }
+ // No item with the id `id` exists, so we create one and call the add
+ // method of that item.
+ // The display string of the item can be the label, the id, or the item
+ // itself if its a plain string.
+ let label = items[this.level].label ||
+ items[this.level].id ||
+ items[this.level];
+ let node = items[this.level].node;
+ if (node) {
+ // The item is supposed to be a DOMNode, so we fetch the textContent in
+ // order to find the correct sorted location of this new item.
+ label = node.textContent;
+ }
+ let treeItem = new TreeItem(this.document, this, node || label,
+ items[this.level].type || defaultType);
+
+ treeItem.add(items, defaultType, sorted);
+ treeItem.node.setAttribute("data-id", JSON.stringify(
+ items.slice(0, this.level + 1).map(item => item.id || item)
+ ));
+
+ if (sorted) {
+ // Inserting this newly created item at correct position
+ let nextSibling = [...this.items.values()].find(child => {
+ return child.label.textContent >= label;
+ });
+
+ if (nextSibling) {
+ this.children.insertBefore(treeItem.node, nextSibling.node);
+ } else {
+ this.children.appendChild(treeItem.node);
+ }
+ } else {
+ this.children.appendChild(treeItem.node);
+ }
+
+ if (this.label) {
+ this.label.removeAttribute("empty");
+ }
+ this.items.set(id, treeItem);
+ },
+
+ /**
+ * If this item is to be removed, then removes this item and thus all of its
+ * subtree. Otherwise, call the remove method of appropriate child. This
+ * recursive method goes on till we have reached the end of the branch or the
+ * current item is to be removed.
+ *
+ * @param {array} items
+ * Ids of items leading up to the item to be removed.
+ */
+ remove: function (items = []) {
+ let id = items.shift();
+ if (id && this.items.has(id)) {
+ let deleted = this.items.get(id);
+ if (!items.length) {
+ this.items.delete(id);
+ }
+ if (this.items.size == 0) {
+ this.label.setAttribute("empty", "true");
+ }
+ deleted.remove(items);
+ } else if (!id) {
+ this.destroy();
+ }
+ },
+
+ /**
+ * If this item is to be selected, then selected and expands the item.
+ * Otherwise, if a child item is to be selected, just expands this item.
+ *
+ * @param {array} items
+ * Ids of items leading up to the item to be selected.
+ */
+ setSelectedItem: function (items) {
+ if (!items[this.level]) {
+ this.label.classList.add("theme-selected");
+ this.label.setAttribute("expanded", "true");
+ return this.label;
+ }
+ if (this.items.has(items[this.level])) {
+ let label = this.items.get(items[this.level]).setSelectedItem(items);
+ if (label && this.label) {
+ this.label.setAttribute("expanded", true);
+ }
+ return label;
+ }
+ return null;
+ },
+
+ /**
+ * Collapses this item and all of its sub tree items
+ */
+ collapseAll: function () {
+ if (this.label) {
+ this.label.removeAttribute("expanded");
+ }
+ for (let child of this.items.values()) {
+ child.collapseAll();
+ }
+ },
+
+ /**
+ * Expands this item and all of its sub tree items
+ */
+ expandAll: function () {
+ if (this.label) {
+ this.label.setAttribute("expanded", "true");
+ }
+ for (let child of this.items.values()) {
+ child.expandAll();
+ }
+ },
+
+ destroy: function () {
+ this.children.remove();
+ this.node.remove();
+ this.label = null;
+ this.items = null;
+ this.children = null;
+ }
+};
diff --git a/devtools/client/shared/widgets/VariablesView.jsm b/devtools/client/shared/widgets/VariablesView.jsm
new file mode 100644
index 000000000..c291066ba
--- /dev/null
+++ b/devtools/client/shared/widgets/VariablesView.jsm
@@ -0,0 +1,4182 @@
+/* -*- 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 Ci = Components.interfaces;
+const Cu = Components.utils;
+
+const DBG_STRINGS_URI = "devtools/client/locales/debugger.properties";
+const LAZY_EMPTY_DELAY = 150; // ms
+const SCROLL_PAGE_SIZE_DEFAULT = 0;
+const PAGE_SIZE_SCROLL_HEIGHT_RATIO = 100;
+const PAGE_SIZE_MAX_JUMPS = 30;
+const SEARCH_ACTION_MAX_DELAY = 300; // ms
+const ITEM_FLASH_DURATION = 300; // ms
+
+const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const {XPCOMUtils} = require("resource://gre/modules/XPCOMUtils.jsm");
+const EventEmitter = require("devtools/shared/event-emitter");
+const DevToolsUtils = require("devtools/shared/DevToolsUtils");
+const Services = require("Services");
+const { getSourceNames } = require("devtools/client/shared/source-utils");
+const promise = require("promise");
+const defer = require("devtools/shared/defer");
+const { Heritage, ViewHelpers, setNamedTimeout } =
+ require("devtools/client/shared/widgets/view-helpers");
+const { Task } = require("devtools/shared/task");
+const nodeConstants = require("devtools/shared/dom-node-constants");
+const {KeyCodes} = require("devtools/client/shared/keycodes");
+const {PluralForm} = require("devtools/shared/plural-form");
+const {LocalizationHelper, ELLIPSIS} = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper(DBG_STRINGS_URI);
+
+XPCOMUtils.defineLazyServiceGetter(this, "clipboardHelper",
+ "@mozilla.org/widget/clipboardhelper;1",
+ "nsIClipboardHelper");
+
+Object.defineProperty(this, "WebConsoleUtils", {
+ get: function () {
+ return require("devtools/client/webconsole/utils").Utils;
+ },
+ configurable: true,
+ enumerable: true
+});
+
+Object.defineProperty(this, "NetworkHelper", {
+ get: function () {
+ return require("devtools/shared/webconsole/network-helper");
+ },
+ configurable: true,
+ enumerable: true
+});
+
+this.EXPORTED_SYMBOLS = ["VariablesView", "escapeHTML"];
+
+/**
+ * A tree view for inspecting scopes, objects and properties.
+ * Iterable via "for (let [id, scope] of instance) { }".
+ * Requires the devtools common.css and debugger.css skin stylesheets.
+ *
+ * To allow replacing variable or property values in this view, provide an
+ * "eval" function property. To allow replacing variable or property names,
+ * provide a "switch" function. To handle deleting variables or properties,
+ * provide a "delete" function.
+ *
+ * @param nsIDOMNode aParentNode
+ * The parent node to hold this view.
+ * @param object aFlags [optional]
+ * An object contaning initialization options for this view.
+ * e.g. { lazyEmpty: true, searchEnabled: true ... }
+ */
+this.VariablesView = function VariablesView(aParentNode, aFlags = {}) {
+ this._store = []; // Can't use a Map because Scope names needn't be unique.
+ this._itemsByElement = new WeakMap();
+ this._prevHierarchy = new Map();
+ this._currHierarchy = new Map();
+
+ this._parent = aParentNode;
+ this._parent.classList.add("variables-view-container");
+ this._parent.classList.add("theme-body");
+ this._appendEmptyNotice();
+
+ this._onSearchboxInput = this._onSearchboxInput.bind(this);
+ this._onSearchboxKeyPress = this._onSearchboxKeyPress.bind(this);
+ this._onViewKeyPress = this._onViewKeyPress.bind(this);
+ this._onViewKeyDown = this._onViewKeyDown.bind(this);
+
+ // Create an internal scrollbox container.
+ this._list = this.document.createElement("scrollbox");
+ this._list.setAttribute("orient", "vertical");
+ this._list.addEventListener("keypress", this._onViewKeyPress, false);
+ this._list.addEventListener("keydown", this._onViewKeyDown, false);
+ this._parent.appendChild(this._list);
+
+ for (let name in aFlags) {
+ this[name] = aFlags[name];
+ }
+
+ EventEmitter.decorate(this);
+};
+
+VariablesView.prototype = {
+ /**
+ * Helper setter for populating this container with a raw object.
+ *
+ * @param object aObject
+ * The raw object to display. You can only provide this object
+ * if you want the variables view to work in sync mode.
+ */
+ set rawObject(aObject) {
+ this.empty();
+ this.addScope()
+ .addItem(undefined, { enumerable: true })
+ .populate(aObject, { sorted: true });
+ },
+
+ /**
+ * Adds a scope to contain any inspected variables.
+ *
+ * This new scope will be considered the parent of any other scope
+ * added afterwards.
+ *
+ * @param string aName
+ * The scope's name (e.g. "Local", "Global" etc.).
+ * @param string aCustomClass
+ * An additional class name for the containing element.
+ * @return Scope
+ * The newly created Scope instance.
+ */
+ addScope: function (aName = "", aCustomClass = "") {
+ this._removeEmptyNotice();
+ this._toggleSearchVisibility(true);
+
+ let scope = new Scope(this, aName, { customClass: aCustomClass });
+ this._store.push(scope);
+ this._itemsByElement.set(scope._target, scope);
+ this._currHierarchy.set(aName, scope);
+ scope.header = !!aName;
+
+ return scope;
+ },
+
+ /**
+ * Removes all items from this container.
+ *
+ * @param number aTimeout [optional]
+ * The number of milliseconds to delay the operation if
+ * lazy emptying of this container is enabled.
+ */
+ empty: function (aTimeout = this.lazyEmptyDelay) {
+ // If there are no items in this container, emptying is useless.
+ if (!this._store.length) {
+ return;
+ }
+
+ this._store.length = 0;
+ this._itemsByElement = new WeakMap();
+ this._prevHierarchy = this._currHierarchy;
+ this._currHierarchy = new Map(); // Don't clear, this is just simple swapping.
+
+ // Check if this empty operation may be executed lazily.
+ if (this.lazyEmpty && aTimeout > 0) {
+ this._emptySoon(aTimeout);
+ return;
+ }
+
+ while (this._list.hasChildNodes()) {
+ this._list.firstChild.remove();
+ }
+
+ this._appendEmptyNotice();
+ this._toggleSearchVisibility(false);
+ },
+
+ /**
+ * Emptying this container and rebuilding it immediately afterwards would
+ * result in a brief redraw flicker, because the previously expanded nodes
+ * may get asynchronously re-expanded, after fetching the prototype and
+ * properties from a server.
+ *
+ * To avoid such behaviour, a normal container list is rebuild, but not
+ * immediately attached to the parent container. The old container list
+ * is kept around for a short period of time, hopefully accounting for the
+ * data fetching delay. In the meantime, any operations can be executed
+ * normally.
+ *
+ * @see VariablesView.empty
+ * @see VariablesView.commitHierarchy
+ */
+ _emptySoon: function (aTimeout) {
+ let prevList = this._list;
+ let currList = this._list = this.document.createElement("scrollbox");
+
+ this.window.setTimeout(() => {
+ prevList.removeEventListener("keypress", this._onViewKeyPress, false);
+ prevList.removeEventListener("keydown", this._onViewKeyDown, false);
+ currList.addEventListener("keypress", this._onViewKeyPress, false);
+ currList.addEventListener("keydown", this._onViewKeyDown, false);
+ currList.setAttribute("orient", "vertical");
+
+ this._parent.removeChild(prevList);
+ this._parent.appendChild(currList);
+
+ if (!this._store.length) {
+ this._appendEmptyNotice();
+ this._toggleSearchVisibility(false);
+ }
+ }, aTimeout);
+ },
+
+ /**
+ * Optional DevTools toolbox containing this VariablesView. Used to
+ * communicate with the inspector and highlighter.
+ */
+ toolbox: null,
+
+ /**
+ * The controller for this VariablesView, if it has one.
+ */
+ controller: null,
+
+ /**
+ * The amount of time (in milliseconds) it takes to empty this view lazily.
+ */
+ lazyEmptyDelay: LAZY_EMPTY_DELAY,
+
+ /**
+ * Specifies if this view may be emptied lazily.
+ * @see VariablesView.prototype.empty
+ */
+ lazyEmpty: false,
+
+ /**
+ * Specifies if nodes in this view may be searched lazily.
+ */
+ lazySearch: true,
+
+ /**
+ * The number of elements in this container to jump when Page Up or Page Down
+ * keys are pressed. If falsy, then the page size will be based on the
+ * container height.
+ */
+ scrollPageSize: SCROLL_PAGE_SIZE_DEFAULT,
+
+ /**
+ * Function called each time a variable or property's value is changed via
+ * user interaction. If null, then value changes are disabled.
+ *
+ * This property is applied recursively onto each scope in this view and
+ * affects only the child nodes when they're created.
+ */
+ eval: null,
+
+ /**
+ * Function called each time a variable or property's name is changed via
+ * user interaction. If null, then name changes are disabled.
+ *
+ * This property is applied recursively onto each scope in this view and
+ * affects only the child nodes when they're created.
+ */
+ switch: null,
+
+ /**
+ * Function called each time a variable or property is deleted via
+ * user interaction. If null, then deletions are disabled.
+ *
+ * This property is applied recursively onto each scope in this view and
+ * affects only the child nodes when they're created.
+ */
+ delete: null,
+
+ /**
+ * Function called each time a property is added via user interaction. If
+ * null, then property additions are disabled.
+ *
+ * This property is applied recursively onto each scope in this view and
+ * affects only the child nodes when they're created.
+ */
+ new: null,
+
+ /**
+ * Specifies if after an eval or switch operation, the variable or property
+ * which has been edited should be disabled.
+ */
+ preventDisableOnChange: false,
+
+ /**
+ * Specifies if, whenever a variable or property descriptor is available,
+ * configurable, enumerable, writable, frozen, sealed and extensible
+ * attributes should not affect presentation.
+ *
+ * This flag is applied recursively onto each scope in this view and
+ * affects only the child nodes when they're created.
+ */
+ preventDescriptorModifiers: false,
+
+ /**
+ * The tooltip text shown on a variable or property's value if an |eval|
+ * function is provided, in order to change the variable or property's value.
+ *
+ * This flag is applied recursively onto each scope in this view and
+ * affects only the child nodes when they're created.
+ */
+ editableValueTooltip: L10N.getStr("variablesEditableValueTooltip"),
+
+ /**
+ * The tooltip text shown on a variable or property's name if a |switch|
+ * function is provided, in order to change the variable or property's name.
+ *
+ * This flag is applied recursively onto each scope in this view and
+ * affects only the child nodes when they're created.
+ */
+ editableNameTooltip: L10N.getStr("variablesEditableNameTooltip"),
+
+ /**
+ * The tooltip text shown on a variable or property's edit button if an
+ * |eval| function is provided and a getter/setter descriptor is present,
+ * in order to change the variable or property to a plain value.
+ *
+ * This flag is applied recursively onto each scope in this view and
+ * affects only the child nodes when they're created.
+ */
+ editButtonTooltip: L10N.getStr("variablesEditButtonTooltip"),
+
+ /**
+ * The tooltip text shown on a variable or property's value if that value is
+ * a DOMNode that can be highlighted and selected in the inspector.
+ *
+ * This flag is applied recursively onto each scope in this view and
+ * affects only the child nodes when they're created.
+ */
+ domNodeValueTooltip: L10N.getStr("variablesDomNodeValueTooltip"),
+
+ /**
+ * The tooltip text shown on a variable or property's delete button if a
+ * |delete| function is provided, in order to delete the variable or property.
+ *
+ * This flag is applied recursively onto each scope in this view and
+ * affects only the child nodes when they're created.
+ */
+ deleteButtonTooltip: L10N.getStr("variablesCloseButtonTooltip"),
+
+ /**
+ * Specifies the context menu attribute set on variables and properties.
+ *
+ * This flag is applied recursively onto each scope in this view and
+ * affects only the child nodes when they're created.
+ */
+ contextMenuId: "",
+
+ /**
+ * The separator label between the variables or properties name and value.
+ *
+ * This flag is applied recursively onto each scope in this view and
+ * affects only the child nodes when they're created.
+ */
+ separatorStr: L10N.getStr("variablesSeparatorLabel"),
+
+ /**
+ * Specifies if enumerable properties and variables should be displayed.
+ * These variables and properties are visible by default.
+ * @param boolean aFlag
+ */
+ set enumVisible(aFlag) {
+ this._enumVisible = aFlag;
+
+ for (let scope of this._store) {
+ scope._enumVisible = aFlag;
+ }
+ },
+
+ /**
+ * Specifies if non-enumerable properties and variables should be displayed.
+ * These variables and properties are visible by default.
+ * @param boolean aFlag
+ */
+ set nonEnumVisible(aFlag) {
+ this._nonEnumVisible = aFlag;
+
+ for (let scope of this._store) {
+ scope._nonEnumVisible = aFlag;
+ }
+ },
+
+ /**
+ * Specifies if only enumerable properties and variables should be displayed.
+ * Both types of these variables and properties are visible by default.
+ * @param boolean aFlag
+ */
+ set onlyEnumVisible(aFlag) {
+ if (aFlag) {
+ this.enumVisible = true;
+ this.nonEnumVisible = false;
+ } else {
+ this.enumVisible = true;
+ this.nonEnumVisible = true;
+ }
+ },
+
+ /**
+ * Sets if the variable and property searching is enabled.
+ * @param boolean aFlag
+ */
+ set searchEnabled(aFlag) {
+ aFlag ? this._enableSearch() : this._disableSearch();
+ },
+
+ /**
+ * Gets if the variable and property searching is enabled.
+ * @return boolean
+ */
+ get searchEnabled() {
+ return !!this._searchboxContainer;
+ },
+
+ /**
+ * Sets the text displayed for the searchbox in this container.
+ * @param string aValue
+ */
+ set searchPlaceholder(aValue) {
+ if (this._searchboxNode) {
+ this._searchboxNode.setAttribute("placeholder", aValue);
+ }
+ this._searchboxPlaceholder = aValue;
+ },
+
+ /**
+ * Gets the text displayed for the searchbox in this container.
+ * @return string
+ */
+ get searchPlaceholder() {
+ return this._searchboxPlaceholder;
+ },
+
+ /**
+ * Enables variable and property searching in this view.
+ * Use the "searchEnabled" setter to enable searching.
+ */
+ _enableSearch: function () {
+ // If searching was already enabled, no need to re-enable it again.
+ if (this._searchboxContainer) {
+ return;
+ }
+ let document = this.document;
+ let ownerNode = this._parent.parentNode;
+
+ let container = this._searchboxContainer = document.createElement("hbox");
+ container.className = "devtools-toolbar";
+
+ // Hide the variables searchbox container if there are no variables or
+ // properties to display.
+ container.hidden = !this._store.length;
+
+ let searchbox = this._searchboxNode = document.createElement("textbox");
+ searchbox.className = "variables-view-searchinput devtools-filterinput";
+ searchbox.setAttribute("placeholder", this._searchboxPlaceholder);
+ searchbox.setAttribute("type", "search");
+ searchbox.setAttribute("flex", "1");
+ searchbox.addEventListener("command", this._onSearchboxInput, false);
+ searchbox.addEventListener("keypress", this._onSearchboxKeyPress, false);
+
+ container.appendChild(searchbox);
+ ownerNode.insertBefore(container, this._parent);
+ },
+
+ /**
+ * Disables variable and property searching in this view.
+ * Use the "searchEnabled" setter to disable searching.
+ */
+ _disableSearch: function () {
+ // If searching was already disabled, no need to re-disable it again.
+ if (!this._searchboxContainer) {
+ return;
+ }
+ this._searchboxContainer.remove();
+ this._searchboxNode.removeEventListener("command", this._onSearchboxInput, false);
+ this._searchboxNode.removeEventListener("keypress", this._onSearchboxKeyPress, false);
+
+ this._searchboxContainer = null;
+ this._searchboxNode = null;
+ },
+
+ /**
+ * Sets the variables searchbox container hidden or visible.
+ * It's hidden by default.
+ *
+ * @param boolean aVisibleFlag
+ * Specifies the intended visibility.
+ */
+ _toggleSearchVisibility: function (aVisibleFlag) {
+ // If searching was already disabled, there's no need to hide it.
+ if (!this._searchboxContainer) {
+ return;
+ }
+ this._searchboxContainer.hidden = !aVisibleFlag;
+ },
+
+ /**
+ * Listener handling the searchbox input event.
+ */
+ _onSearchboxInput: function () {
+ this.scheduleSearch(this._searchboxNode.value);
+ },
+
+ /**
+ * Listener handling the searchbox key press event.
+ */
+ _onSearchboxKeyPress: function (e) {
+ switch (e.keyCode) {
+ case KeyCodes.DOM_VK_RETURN:
+ this._onSearchboxInput();
+ return;
+ case KeyCodes.DOM_VK_ESCAPE:
+ this._searchboxNode.value = "";
+ this._onSearchboxInput();
+ return;
+ }
+ },
+
+ /**
+ * Schedules searching for variables or properties matching the query.
+ *
+ * @param string aToken
+ * The variable or property to search for.
+ * @param number aWait
+ * The amount of milliseconds to wait until draining.
+ */
+ scheduleSearch: function (aToken, aWait) {
+ // Check if this search operation may not be executed lazily.
+ if (!this.lazySearch) {
+ this._doSearch(aToken);
+ return;
+ }
+
+ // The amount of time to wait for the requests to settle.
+ let maxDelay = SEARCH_ACTION_MAX_DELAY;
+ let delay = aWait === undefined ? maxDelay / aToken.length : aWait;
+
+ // Allow requests to settle down first.
+ setNamedTimeout("vview-search", delay, () => this._doSearch(aToken));
+ },
+
+ /**
+ * Performs a case insensitive search for variables or properties matching
+ * the query, and hides non-matched items.
+ *
+ * If aToken is falsy, then all the scopes are unhidden and expanded,
+ * while the available variables and properties inside those scopes are
+ * just unhidden.
+ *
+ * @param string aToken
+ * The variable or property to search for.
+ */
+ _doSearch: function (aToken) {
+ if (this.controller && this.controller.supportsSearch()) {
+ // Retrieve the main Scope in which we add attributes
+ let scope = this._store[0]._store.get(undefined);
+ if (!aToken) {
+ // Prune the view from old previous content
+ // so that we delete the intermediate search results
+ // we created in previous searches
+ for (let property of scope._store.values()) {
+ property.remove();
+ }
+ }
+ // Retrieve new attributes eventually hidden in splits
+ this.controller.performSearch(scope, aToken);
+ // Filter already displayed attributes
+ if (aToken) {
+ scope._performSearch(aToken.toLowerCase());
+ }
+ return;
+ }
+ for (let scope of this._store) {
+ switch (aToken) {
+ case "":
+ case null:
+ case undefined:
+ scope.expand();
+ scope._performSearch("");
+ break;
+ default:
+ scope._performSearch(aToken.toLowerCase());
+ break;
+ }
+ }
+ },
+
+ /**
+ * Find the first item in the tree of visible items in this container that
+ * matches the predicate. Searches in visual order (the order seen by the
+ * user). Descends into each scope to check the scope and its children.
+ *
+ * @param function aPredicate
+ * A function that returns true when a match is found.
+ * @return Scope | Variable | Property
+ * The first visible scope, variable or property, or null if nothing
+ * is found.
+ */
+ _findInVisibleItems: function (aPredicate) {
+ for (let scope of this._store) {
+ let result = scope._findInVisibleItems(aPredicate);
+ if (result) {
+ return result;
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Find the last item in the tree of visible items in this container that
+ * matches the predicate. Searches in reverse visual order (opposite of the
+ * order seen by the user). Descends into each scope to check the scope and
+ * its children.
+ *
+ * @param function aPredicate
+ * A function that returns true when a match is found.
+ * @return Scope | Variable | Property
+ * The last visible scope, variable or property, or null if nothing
+ * is found.
+ */
+ _findInVisibleItemsReverse: function (aPredicate) {
+ for (let i = this._store.length - 1; i >= 0; i--) {
+ let scope = this._store[i];
+ let result = scope._findInVisibleItemsReverse(aPredicate);
+ if (result) {
+ return result;
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Gets the scope at the specified index.
+ *
+ * @param number aIndex
+ * The scope's index.
+ * @return Scope
+ * The scope if found, undefined if not.
+ */
+ getScopeAtIndex: function (aIndex) {
+ return this._store[aIndex];
+ },
+
+ /**
+ * Recursively searches this container for the scope, variable or property
+ * displayed by the specified node.
+ *
+ * @param nsIDOMNode aNode
+ * The node to search for.
+ * @return Scope | Variable | Property
+ * The matched scope, variable or property, or null if nothing is found.
+ */
+ getItemForNode: function (aNode) {
+ return this._itemsByElement.get(aNode);
+ },
+
+ /**
+ * Gets the scope owning a Variable or Property.
+ *
+ * @param Variable | Property
+ * The variable or property to retrieven the owner scope for.
+ * @return Scope
+ * The owner scope.
+ */
+ getOwnerScopeForVariableOrProperty: function (aItem) {
+ if (!aItem) {
+ return null;
+ }
+ // If this is a Scope, return it.
+ if (!(aItem instanceof Variable)) {
+ return aItem;
+ }
+ // If this is a Variable or Property, find its owner scope.
+ if (aItem instanceof Variable && aItem.ownerView) {
+ return this.getOwnerScopeForVariableOrProperty(aItem.ownerView);
+ }
+ return null;
+ },
+
+ /**
+ * Gets the parent scopes for a specified Variable or Property.
+ * The returned list will not include the owner scope.
+ *
+ * @param Variable | Property
+ * The variable or property for which to find the parent scopes.
+ * @return array
+ * A list of parent Scopes.
+ */
+ getParentScopesForVariableOrProperty: function (aItem) {
+ let scope = this.getOwnerScopeForVariableOrProperty(aItem);
+ return this._store.slice(0, Math.max(this._store.indexOf(scope), 0));
+ },
+
+ /**
+ * Gets the currently focused scope, variable or property in this view.
+ *
+ * @return Scope | Variable | Property
+ * The focused scope, variable or property, or null if nothing is found.
+ */
+ getFocusedItem: function () {
+ let focused = this.document.commandDispatcher.focusedElement;
+ return this.getItemForNode(focused);
+ },
+
+ /**
+ * Focuses the first visible scope, variable, or property in this container.
+ */
+ focusFirstVisibleItem: function () {
+ let focusableItem = this._findInVisibleItems(item => item.focusable);
+ if (focusableItem) {
+ this._focusItem(focusableItem);
+ }
+ this._parent.scrollTop = 0;
+ this._parent.scrollLeft = 0;
+ },
+
+ /**
+ * Focuses the last visible scope, variable, or property in this container.
+ */
+ focusLastVisibleItem: function () {
+ let focusableItem = this._findInVisibleItemsReverse(item => item.focusable);
+ if (focusableItem) {
+ this._focusItem(focusableItem);
+ }
+ this._parent.scrollTop = this._parent.scrollHeight;
+ this._parent.scrollLeft = 0;
+ },
+
+ /**
+ * Focuses the next scope, variable or property in this view.
+ */
+ focusNextItem: function () {
+ this.focusItemAtDelta(+1);
+ },
+
+ /**
+ * Focuses the previous scope, variable or property in this view.
+ */
+ focusPrevItem: function () {
+ this.focusItemAtDelta(-1);
+ },
+
+ /**
+ * Focuses another scope, variable or property in this view, based on
+ * the index distance from the currently focused item.
+ *
+ * @param number aDelta
+ * A scalar specifying by how many items should the selection change.
+ */
+ focusItemAtDelta: function (aDelta) {
+ let direction = aDelta > 0 ? "advanceFocus" : "rewindFocus";
+ let distance = Math.abs(Math[aDelta > 0 ? "ceil" : "floor"](aDelta));
+ while (distance--) {
+ if (!this._focusChange(direction)) {
+ break; // Out of bounds.
+ }
+ }
+ },
+
+ /**
+ * Focuses the next or previous scope, variable or property in this view.
+ *
+ * @param string aDirection
+ * Either "advanceFocus" or "rewindFocus".
+ * @return boolean
+ * False if the focus went out of bounds and the first or last element
+ * in this view was focused instead.
+ */
+ _focusChange: function (aDirection) {
+ let commandDispatcher = this.document.commandDispatcher;
+ let prevFocusedElement = commandDispatcher.focusedElement;
+ let currFocusedItem = null;
+
+ do {
+ commandDispatcher.suppressFocusScroll = true;
+ commandDispatcher[aDirection]();
+
+ // Make sure the newly focused item is a part of this view.
+ // If the focus goes out of bounds, revert the previously focused item.
+ if (!(currFocusedItem = this.getFocusedItem())) {
+ prevFocusedElement.focus();
+ return false;
+ }
+ } while (!currFocusedItem.focusable);
+
+ // Focus remained within bounds.
+ return true;
+ },
+
+ /**
+ * Focuses a scope, variable or property and makes sure it's visible.
+ *
+ * @param aItem Scope | Variable | Property
+ * The item to focus.
+ * @param boolean aCollapseFlag
+ * True if the focused item should also be collapsed.
+ * @return boolean
+ * True if the item was successfully focused.
+ */
+ _focusItem: function (aItem, aCollapseFlag) {
+ if (!aItem.focusable) {
+ return false;
+ }
+ if (aCollapseFlag) {
+ aItem.collapse();
+ }
+ aItem._target.focus();
+ this.boxObject.ensureElementIsVisible(aItem._arrow);
+ return true;
+ },
+
+ /**
+ * Listener handling a key press event on the view.
+ */
+ _onViewKeyPress: function (e) {
+ let item = this.getFocusedItem();
+
+ // Prevent scrolling when pressing navigation keys.
+ ViewHelpers.preventScrolling(e);
+
+ switch (e.keyCode) {
+ case KeyCodes.DOM_VK_UP:
+ // Always rewind focus.
+ this.focusPrevItem(true);
+ return;
+
+ case KeyCodes.DOM_VK_DOWN:
+ // Always advance focus.
+ this.focusNextItem(true);
+ return;
+
+ case KeyCodes.DOM_VK_LEFT:
+ // Collapse scopes, variables and properties before rewinding focus.
+ if (item._isExpanded && item._isArrowVisible) {
+ item.collapse();
+ } else {
+ this._focusItem(item.ownerView);
+ }
+ return;
+
+ case KeyCodes.DOM_VK_RIGHT:
+ // Nothing to do here if this item never expands.
+ if (!item._isArrowVisible) {
+ return;
+ }
+ // Expand scopes, variables and properties before advancing focus.
+ if (!item._isExpanded) {
+ item.expand();
+ } else {
+ this.focusNextItem(true);
+ }
+ return;
+
+ case KeyCodes.DOM_VK_PAGE_UP:
+ // Rewind a certain number of elements based on the container height.
+ this.focusItemAtDelta(-(this.scrollPageSize || Math.min(Math.floor(this._list.scrollHeight /
+ PAGE_SIZE_SCROLL_HEIGHT_RATIO),
+ PAGE_SIZE_MAX_JUMPS)));
+ return;
+
+ case KeyCodes.DOM_VK_PAGE_DOWN:
+ // Advance a certain number of elements based on the container height.
+ this.focusItemAtDelta(+(this.scrollPageSize || Math.min(Math.floor(this._list.scrollHeight /
+ PAGE_SIZE_SCROLL_HEIGHT_RATIO),
+ PAGE_SIZE_MAX_JUMPS)));
+ return;
+
+ case KeyCodes.DOM_VK_HOME:
+ this.focusFirstVisibleItem();
+ return;
+
+ case KeyCodes.DOM_VK_END:
+ this.focusLastVisibleItem();
+ return;
+
+ case KeyCodes.DOM_VK_RETURN:
+ // Start editing the value or name of the Variable or Property.
+ if (item instanceof Variable) {
+ if (e.metaKey || e.altKey || e.shiftKey) {
+ item._activateNameInput();
+ } else {
+ item._activateValueInput();
+ }
+ }
+ return;
+
+ case KeyCodes.DOM_VK_DELETE:
+ case KeyCodes.DOM_VK_BACK_SPACE:
+ // Delete the Variable or Property if allowed.
+ if (item instanceof Variable) {
+ item._onDelete(e);
+ }
+ return;
+
+ case KeyCodes.DOM_VK_INSERT:
+ item._onAddProperty(e);
+ return;
+ }
+ },
+
+ /**
+ * Listener handling a key down event on the view.
+ */
+ _onViewKeyDown: function (e) {
+ if (e.keyCode == KeyCodes.DOM_VK_C) {
+ // Copy current selection to clipboard.
+ if (e.ctrlKey || e.metaKey) {
+ let item = this.getFocusedItem();
+ clipboardHelper.copyString(
+ item._nameString + item.separatorStr + item._valueString
+ );
+ }
+ }
+ },
+
+ /**
+ * Sets the text displayed in this container when there are no available items.
+ * @param string aValue
+ */
+ set emptyText(aValue) {
+ if (this._emptyTextNode) {
+ this._emptyTextNode.setAttribute("value", aValue);
+ }
+ this._emptyTextValue = aValue;
+ this._appendEmptyNotice();
+ },
+
+ /**
+ * Creates and appends a label signaling that this container is empty.
+ */
+ _appendEmptyNotice: function () {
+ if (this._emptyTextNode || !this._emptyTextValue) {
+ return;
+ }
+
+ let label = this.document.createElement("label");
+ label.className = "variables-view-empty-notice";
+ label.setAttribute("value", this._emptyTextValue);
+
+ this._parent.appendChild(label);
+ this._emptyTextNode = label;
+ },
+
+ /**
+ * Removes the label signaling that this container is empty.
+ */
+ _removeEmptyNotice: function () {
+ if (!this._emptyTextNode) {
+ return;
+ }
+
+ this._parent.removeChild(this._emptyTextNode);
+ this._emptyTextNode = null;
+ },
+
+ /**
+ * Gets if all values should be aligned together.
+ * @return boolean
+ */
+ get alignedValues() {
+ return this._alignedValues;
+ },
+
+ /**
+ * Sets if all values should be aligned together.
+ * @param boolean aFlag
+ */
+ set alignedValues(aFlag) {
+ this._alignedValues = aFlag;
+ if (aFlag) {
+ this._parent.setAttribute("aligned-values", "");
+ } else {
+ this._parent.removeAttribute("aligned-values");
+ }
+ },
+
+ /**
+ * Gets if action buttons (like delete) should be placed at the beginning or
+ * end of a line.
+ * @return boolean
+ */
+ get actionsFirst() {
+ return this._actionsFirst;
+ },
+
+ /**
+ * Sets if action buttons (like delete) should be placed at the beginning or
+ * end of a line.
+ * @param boolean aFlag
+ */
+ set actionsFirst(aFlag) {
+ this._actionsFirst = aFlag;
+ if (aFlag) {
+ this._parent.setAttribute("actions-first", "");
+ } else {
+ this._parent.removeAttribute("actions-first");
+ }
+ },
+
+ /**
+ * Gets the parent node holding this view.
+ * @return nsIDOMNode
+ */
+ get boxObject() {
+ return this._list.boxObject;
+ },
+
+ /**
+ * Gets the parent node holding this view.
+ * @return nsIDOMNode
+ */
+ get parentNode() {
+ return this._parent;
+ },
+
+ /**
+ * Gets the owner document holding this view.
+ * @return nsIHTMLDocument
+ */
+ get document() {
+ return this._document || (this._document = this._parent.ownerDocument);
+ },
+
+ /**
+ * Gets the default window holding this view.
+ * @return nsIDOMWindow
+ */
+ get window() {
+ return this._window || (this._window = this.document.defaultView);
+ },
+
+ _document: null,
+ _window: null,
+
+ _store: null,
+ _itemsByElement: null,
+ _prevHierarchy: null,
+ _currHierarchy: null,
+
+ _enumVisible: true,
+ _nonEnumVisible: true,
+ _alignedValues: false,
+ _actionsFirst: false,
+
+ _parent: null,
+ _list: null,
+ _searchboxNode: null,
+ _searchboxContainer: null,
+ _searchboxPlaceholder: "",
+ _emptyTextNode: null,
+ _emptyTextValue: ""
+};
+
+VariablesView.NON_SORTABLE_CLASSES = [
+ "Array",
+ "Int8Array",
+ "Uint8Array",
+ "Uint8ClampedArray",
+ "Int16Array",
+ "Uint16Array",
+ "Int32Array",
+ "Uint32Array",
+ "Float32Array",
+ "Float64Array",
+ "NodeList"
+];
+
+/**
+ * Determine whether an object's properties should be sorted based on its class.
+ *
+ * @param string aClassName
+ * The class of the object.
+ */
+VariablesView.isSortable = function (aClassName) {
+ return VariablesView.NON_SORTABLE_CLASSES.indexOf(aClassName) == -1;
+};
+
+/**
+ * Generates the string evaluated when performing simple value changes.
+ *
+ * @param Variable | Property aItem
+ * The current variable or property.
+ * @param string aCurrentString
+ * The trimmed user inputted string.
+ * @param string aPrefix [optional]
+ * Prefix for the symbolic name.
+ * @return string
+ * The string to be evaluated.
+ */
+VariablesView.simpleValueEvalMacro = function (aItem, aCurrentString, aPrefix = "") {
+ return aPrefix + aItem.symbolicName + "=" + aCurrentString;
+};
+
+/**
+ * Generates the string evaluated when overriding getters and setters with
+ * plain values.
+ *
+ * @param Property aItem
+ * The current getter or setter property.
+ * @param string aCurrentString
+ * The trimmed user inputted string.
+ * @param string aPrefix [optional]
+ * Prefix for the symbolic name.
+ * @return string
+ * The string to be evaluated.
+ */
+VariablesView.overrideValueEvalMacro = function (aItem, aCurrentString, aPrefix = "") {
+ let property = escapeString(aItem._nameString);
+ let parent = aPrefix + aItem.ownerView.symbolicName || "this";
+
+ return "Object.defineProperty(" + parent + "," + property + "," +
+ "{ value: " + aCurrentString +
+ ", enumerable: " + parent + ".propertyIsEnumerable(" + property + ")" +
+ ", configurable: true" +
+ ", writable: true" +
+ "})";
+};
+
+/**
+ * Generates the string evaluated when performing getters and setters changes.
+ *
+ * @param Property aItem
+ * The current getter or setter property.
+ * @param string aCurrentString
+ * The trimmed user inputted string.
+ * @param string aPrefix [optional]
+ * Prefix for the symbolic name.
+ * @return string
+ * The string to be evaluated.
+ */
+VariablesView.getterOrSetterEvalMacro = function (aItem, aCurrentString, aPrefix = "") {
+ let type = aItem._nameString;
+ let propertyObject = aItem.ownerView;
+ let parentObject = propertyObject.ownerView;
+ let property = escapeString(propertyObject._nameString);
+ let parent = aPrefix + parentObject.symbolicName || "this";
+
+ switch (aCurrentString) {
+ case "":
+ case "null":
+ case "undefined":
+ let mirrorType = type == "get" ? "set" : "get";
+ let mirrorLookup = type == "get" ? "__lookupSetter__" : "__lookupGetter__";
+
+ // If the parent object will end up without any getter or setter,
+ // morph it into a plain value.
+ if ((type == "set" && propertyObject.getter.type == "undefined") ||
+ (type == "get" && propertyObject.setter.type == "undefined")) {
+ // Make sure the right getter/setter to value override macro is applied
+ // to the target object.
+ return propertyObject.evaluationMacro(propertyObject, "undefined", aPrefix);
+ }
+
+ // Construct and return the getter/setter removal evaluation string.
+ // e.g: Object.defineProperty(foo, "bar", {
+ // get: foo.__lookupGetter__("bar"),
+ // set: undefined,
+ // enumerable: true,
+ // configurable: true
+ // })
+ return "Object.defineProperty(" + parent + "," + property + "," +
+ "{" + mirrorType + ":" + parent + "." + mirrorLookup + "(" + property + ")" +
+ "," + type + ":" + undefined +
+ ", enumerable: " + parent + ".propertyIsEnumerable(" + property + ")" +
+ ", configurable: true" +
+ "})";
+
+ default:
+ // Wrap statements inside a function declaration if not already wrapped.
+ if (!aCurrentString.startsWith("function")) {
+ let header = "function(" + (type == "set" ? "value" : "") + ")";
+ let body = "";
+ // If there's a return statement explicitly written, always use the
+ // standard function definition syntax
+ if (aCurrentString.includes("return ")) {
+ body = "{" + aCurrentString + "}";
+ }
+ // If block syntax is used, use the whole string as the function body.
+ else if (aCurrentString.startsWith("{")) {
+ body = aCurrentString;
+ }
+ // Prefer an expression closure.
+ else {
+ body = "(" + aCurrentString + ")";
+ }
+ aCurrentString = header + body;
+ }
+
+ // Determine if a new getter or setter should be defined.
+ let defineType = type == "get" ? "__defineGetter__" : "__defineSetter__";
+
+ // Make sure all quotes are escaped in the expression's syntax,
+ let defineFunc = "eval(\"(" + aCurrentString.replace(/"/g, "\\$&") + ")\")";
+
+ // Construct and return the getter/setter evaluation string.
+ // e.g: foo.__defineGetter__("bar", eval("(function() { return 42; })"))
+ return parent + "." + defineType + "(" + property + "," + defineFunc + ")";
+ }
+};
+
+/**
+ * Function invoked when a getter or setter is deleted.
+ *
+ * @param Property aItem
+ * The current getter or setter property.
+ */
+VariablesView.getterOrSetterDeleteCallback = function (aItem) {
+ aItem._disable();
+
+ // Make sure the right getter/setter to value override macro is applied
+ // to the target object.
+ aItem.ownerView.eval(aItem, "");
+
+ return true; // Don't hide the element.
+};
+
+
+/**
+ * A Scope is an object holding Variable instances.
+ * Iterable via "for (let [name, variable] of instance) { }".
+ *
+ * @param VariablesView aView
+ * The view to contain this scope.
+ * @param string aName
+ * The scope's name.
+ * @param object aFlags [optional]
+ * Additional options or flags for this scope.
+ */
+function Scope(aView, aName, aFlags = {}) {
+ this.ownerView = aView;
+
+ this._onClick = this._onClick.bind(this);
+ this._openEnum = this._openEnum.bind(this);
+ this._openNonEnum = this._openNonEnum.bind(this);
+
+ // Inherit properties and flags from the parent view. You can override
+ // each of these directly onto any scope, variable or property instance.
+ this.scrollPageSize = aView.scrollPageSize;
+ this.eval = aView.eval;
+ this.switch = aView.switch;
+ this.delete = aView.delete;
+ this.new = aView.new;
+ this.preventDisableOnChange = aView.preventDisableOnChange;
+ this.preventDescriptorModifiers = aView.preventDescriptorModifiers;
+ this.editableNameTooltip = aView.editableNameTooltip;
+ this.editableValueTooltip = aView.editableValueTooltip;
+ this.editButtonTooltip = aView.editButtonTooltip;
+ this.deleteButtonTooltip = aView.deleteButtonTooltip;
+ this.domNodeValueTooltip = aView.domNodeValueTooltip;
+ this.contextMenuId = aView.contextMenuId;
+ this.separatorStr = aView.separatorStr;
+
+ this._init(aName, aFlags);
+}
+
+Scope.prototype = {
+ /**
+ * Whether this Scope should be prefetched when it is remoted.
+ */
+ shouldPrefetch: true,
+
+ /**
+ * Whether this Scope should paginate its contents.
+ */
+ allowPaginate: false,
+
+ /**
+ * The class name applied to this scope's target element.
+ */
+ targetClassName: "variables-view-scope",
+
+ /**
+ * Create a new Variable that is a child of this Scope.
+ *
+ * @param string aName
+ * The name of the new Property.
+ * @param object aDescriptor
+ * The variable's descriptor.
+ * @param object aOptions
+ * Options of the form accepted by addItem.
+ * @return Variable
+ * The newly created child Variable.
+ */
+ _createChild: function (aName, aDescriptor, aOptions) {
+ return new Variable(this, aName, aDescriptor, aOptions);
+ },
+
+ /**
+ * Adds a child to contain any inspected properties.
+ *
+ * @param string aName
+ * The child's name.
+ * @param object aDescriptor
+ * Specifies the value and/or type & class of the child,
+ * or 'get' & 'set' accessor properties. If the type is implicit,
+ * it will be inferred from the value. If this parameter is omitted,
+ * a property without a value will be added (useful for branch nodes).
+ * e.g. - { value: 42 }
+ * - { value: true }
+ * - { value: "nasu" }
+ * - { value: { type: "undefined" } }
+ * - { value: { type: "null" } }
+ * - { value: { type: "object", class: "Object" } }
+ * - { get: { type: "object", class: "Function" },
+ * set: { type: "undefined" } }
+ * @param object aOptions
+ * Specifies some options affecting the new variable.
+ * Recognized properties are
+ * * boolean relaxed true if name duplicates should be allowed.
+ * You probably shouldn't do it. Use this
+ * with caution.
+ * * boolean internalItem true if the item is internally generated.
+ * This is used for special variables
+ * like <return> or <exception> and distinguishes
+ * them from ordinary properties that happen
+ * to have the same name
+ * @return Variable
+ * The newly created Variable instance, null if it already exists.
+ */
+ addItem: function (aName, aDescriptor = {}, aOptions = {}) {
+ let {relaxed} = aOptions;
+ if (this._store.has(aName) && !relaxed) {
+ return this._store.get(aName);
+ }
+
+ let child = this._createChild(aName, aDescriptor, aOptions);
+ this._store.set(aName, child);
+ this._variablesView._itemsByElement.set(child._target, child);
+ this._variablesView._currHierarchy.set(child.absoluteName, child);
+ child.header = aName !== undefined;
+
+ return child;
+ },
+
+ /**
+ * Adds items for this variable.
+ *
+ * @param object aItems
+ * An object containing some { name: descriptor } data properties,
+ * specifying the value and/or type & class of the variable,
+ * or 'get' & 'set' accessor properties. If the type is implicit,
+ * it will be inferred from the value.
+ * e.g. - { someProp0: { value: 42 },
+ * someProp1: { value: true },
+ * someProp2: { value: "nasu" },
+ * someProp3: { value: { type: "undefined" } },
+ * someProp4: { value: { type: "null" } },
+ * someProp5: { value: { type: "object", class: "Object" } },
+ * someProp6: { get: { type: "object", class: "Function" },
+ * set: { type: "undefined" } } }
+ * @param object aOptions [optional]
+ * Additional options for adding the properties. Supported options:
+ * - sorted: true to sort all the properties before adding them
+ * - callback: function invoked after each item is added
+ */
+ addItems: function (aItems, aOptions = {}) {
+ let names = Object.keys(aItems);
+
+ // Sort all of the properties before adding them, if preferred.
+ if (aOptions.sorted) {
+ names.sort(this._naturalSort);
+ }
+
+ // Add the properties to the current scope.
+ for (let name of names) {
+ let descriptor = aItems[name];
+ let item = this.addItem(name, descriptor);
+
+ if (aOptions.callback) {
+ aOptions.callback(item, descriptor && descriptor.value);
+ }
+ }
+ },
+
+ /**
+ * Remove this Scope from its parent and remove all children recursively.
+ */
+ remove: function () {
+ let view = this._variablesView;
+ view._store.splice(view._store.indexOf(this), 1);
+ view._itemsByElement.delete(this._target);
+ view._currHierarchy.delete(this._nameString);
+
+ this._target.remove();
+
+ for (let variable of this._store.values()) {
+ variable.remove();
+ }
+ },
+
+ /**
+ * Gets the variable in this container having the specified name.
+ *
+ * @param string aName
+ * The name of the variable to get.
+ * @return Variable
+ * The matched variable, or null if nothing is found.
+ */
+ get: function (aName) {
+ return this._store.get(aName);
+ },
+
+ /**
+ * Recursively searches for the variable or property in this container
+ * displayed by the specified node.
+ *
+ * @param nsIDOMNode aNode
+ * The node to search for.
+ * @return Variable | Property
+ * The matched variable or property, or null if nothing is found.
+ */
+ find: function (aNode) {
+ for (let [, variable] of this._store) {
+ let match;
+ if (variable._target == aNode) {
+ match = variable;
+ } else {
+ match = variable.find(aNode);
+ }
+ if (match) {
+ return match;
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Determines if this scope is a direct child of a parent variables view,
+ * scope, variable or property.
+ *
+ * @param VariablesView | Scope | Variable | Property
+ * The parent to check.
+ * @return boolean
+ * True if the specified item is a direct child, false otherwise.
+ */
+ isChildOf: function (aParent) {
+ return this.ownerView == aParent;
+ },
+
+ /**
+ * Determines if this scope is a descendant of a parent variables view,
+ * scope, variable or property.
+ *
+ * @param VariablesView | Scope | Variable | Property
+ * The parent to check.
+ * @return boolean
+ * True if the specified item is a descendant, false otherwise.
+ */
+ isDescendantOf: function (aParent) {
+ if (this.isChildOf(aParent)) {
+ return true;
+ }
+
+ // Recurse to parent if it is a Scope, Variable, or Property.
+ if (this.ownerView instanceof Scope) {
+ return this.ownerView.isDescendantOf(aParent);
+ }
+
+ return false;
+ },
+
+ /**
+ * Shows the scope.
+ */
+ show: function () {
+ this._target.hidden = false;
+ this._isContentVisible = true;
+
+ if (this.onshow) {
+ this.onshow(this);
+ }
+ },
+
+ /**
+ * Hides the scope.
+ */
+ hide: function () {
+ this._target.hidden = true;
+ this._isContentVisible = false;
+
+ if (this.onhide) {
+ this.onhide(this);
+ }
+ },
+
+ /**
+ * Expands the scope, showing all the added details.
+ */
+ expand: function () {
+ if (this._isExpanded || this._isLocked) {
+ return;
+ }
+ if (this._variablesView._enumVisible) {
+ this._openEnum();
+ }
+ if (this._variablesView._nonEnumVisible) {
+ Services.tm.currentThread.dispatch({ run: this._openNonEnum }, 0);
+ }
+ this._isExpanded = true;
+
+ if (this.onexpand) {
+ // We return onexpand as it sometimes returns a promise
+ // (up to the user of VariableView to do it)
+ // that can indicate when the view is done expanding
+ // and attributes are available. (Mostly used for tests)
+ return this.onexpand(this);
+ }
+ },
+
+ /**
+ * Collapses the scope, hiding all the added details.
+ */
+ collapse: function () {
+ if (!this._isExpanded || this._isLocked) {
+ return;
+ }
+ this._arrow.removeAttribute("open");
+ this._enum.removeAttribute("open");
+ this._nonenum.removeAttribute("open");
+ this._isExpanded = false;
+
+ if (this.oncollapse) {
+ this.oncollapse(this);
+ }
+ },
+
+ /**
+ * Toggles between the scope's collapsed and expanded state.
+ */
+ toggle: function (e) {
+ if (e && e.button != 0) {
+ // Only allow left-click to trigger this event.
+ return;
+ }
+ this.expanded ^= 1;
+
+ // Make sure the scope and its contents are visibile.
+ for (let [, variable] of this._store) {
+ variable.header = true;
+ variable._matched = true;
+ }
+ if (this.ontoggle) {
+ this.ontoggle(this);
+ }
+ },
+
+ /**
+ * Shows the scope's title header.
+ */
+ showHeader: function () {
+ if (this._isHeaderVisible || !this._nameString) {
+ return;
+ }
+ this._target.removeAttribute("untitled");
+ this._isHeaderVisible = true;
+ },
+
+ /**
+ * Hides the scope's title header.
+ * This action will automatically expand the scope.
+ */
+ hideHeader: function () {
+ if (!this._isHeaderVisible) {
+ return;
+ }
+ this.expand();
+ this._target.setAttribute("untitled", "");
+ this._isHeaderVisible = false;
+ },
+
+ /**
+ * Sort in ascending order
+ * This only needs to compare non-numbers since it is dealing with an array
+ * which numeric-based indices are placed in order.
+ *
+ * @param string a
+ * @param string b
+ * @return number
+ * -1 if a is less than b, 0 if no change in order, +1 if a is greater than 0
+ */
+ _naturalSort: function (a, b) {
+ if (isNaN(parseFloat(a)) && isNaN(parseFloat(b))) {
+ return a < b ? -1 : 1;
+ }
+ },
+
+ /**
+ * Shows the scope's expand/collapse arrow.
+ */
+ showArrow: function () {
+ if (this._isArrowVisible) {
+ return;
+ }
+ this._arrow.removeAttribute("invisible");
+ this._isArrowVisible = true;
+ },
+
+ /**
+ * Hides the scope's expand/collapse arrow.
+ */
+ hideArrow: function () {
+ if (!this._isArrowVisible) {
+ return;
+ }
+ this._arrow.setAttribute("invisible", "");
+ this._isArrowVisible = false;
+ },
+
+ /**
+ * Gets the visibility state.
+ * @return boolean
+ */
+ get visible() {
+ return this._isContentVisible;
+ },
+
+ /**
+ * Gets the expanded state.
+ * @return boolean
+ */
+ get expanded() {
+ return this._isExpanded;
+ },
+
+ /**
+ * Gets the header visibility state.
+ * @return boolean
+ */
+ get header() {
+ return this._isHeaderVisible;
+ },
+
+ /**
+ * Gets the twisty visibility state.
+ * @return boolean
+ */
+ get twisty() {
+ return this._isArrowVisible;
+ },
+
+ /**
+ * Gets the expand lock state.
+ * @return boolean
+ */
+ get locked() {
+ return this._isLocked;
+ },
+
+ /**
+ * Sets the visibility state.
+ * @param boolean aFlag
+ */
+ set visible(aFlag) {
+ aFlag ? this.show() : this.hide();
+ },
+
+ /**
+ * Sets the expanded state.
+ * @param boolean aFlag
+ */
+ set expanded(aFlag) {
+ aFlag ? this.expand() : this.collapse();
+ },
+
+ /**
+ * Sets the header visibility state.
+ * @param boolean aFlag
+ */
+ set header(aFlag) {
+ aFlag ? this.showHeader() : this.hideHeader();
+ },
+
+ /**
+ * Sets the twisty visibility state.
+ * @param boolean aFlag
+ */
+ set twisty(aFlag) {
+ aFlag ? this.showArrow() : this.hideArrow();
+ },
+
+ /**
+ * Sets the expand lock state.
+ * @param boolean aFlag
+ */
+ set locked(aFlag) {
+ this._isLocked = aFlag;
+ },
+
+ /**
+ * Specifies if this target node may be focused.
+ * @return boolean
+ */
+ get focusable() {
+ // Check if this target node is actually visibile.
+ if (!this._nameString ||
+ !this._isContentVisible ||
+ !this._isHeaderVisible ||
+ !this._isMatch) {
+ return false;
+ }
+ // Check if all parent objects are expanded.
+ let item = this;
+
+ // Recurse while parent is a Scope, Variable, or Property
+ while ((item = item.ownerView) && item instanceof Scope) {
+ if (!item._isExpanded) {
+ return false;
+ }
+ }
+ return true;
+ },
+
+ /**
+ * Focus this scope.
+ */
+ focus: function () {
+ this._variablesView._focusItem(this);
+ },
+
+ /**
+ * Adds an event listener for a certain event on this scope's title.
+ * @param string aName
+ * @param function aCallback
+ * @param boolean aCapture
+ */
+ addEventListener: function (aName, aCallback, aCapture) {
+ this._title.addEventListener(aName, aCallback, aCapture);
+ },
+
+ /**
+ * Removes an event listener for a certain event on this scope's title.
+ * @param string aName
+ * @param function aCallback
+ * @param boolean aCapture
+ */
+ removeEventListener: function (aName, aCallback, aCapture) {
+ this._title.removeEventListener(aName, aCallback, aCapture);
+ },
+
+ /**
+ * Gets the id associated with this item.
+ * @return string
+ */
+ get id() {
+ return this._idString;
+ },
+
+ /**
+ * Gets the name associated with this item.
+ * @return string
+ */
+ get name() {
+ return this._nameString;
+ },
+
+ /**
+ * Gets the displayed value for this item.
+ * @return string
+ */
+ get displayValue() {
+ return this._valueString;
+ },
+
+ /**
+ * Gets the class names used for the displayed value.
+ * @return string
+ */
+ get displayValueClassName() {
+ return this._valueClassName;
+ },
+
+ /**
+ * Gets the element associated with this item.
+ * @return nsIDOMNode
+ */
+ get target() {
+ return this._target;
+ },
+
+ /**
+ * Initializes this scope's id, view and binds event listeners.
+ *
+ * @param string aName
+ * The scope's name.
+ * @param object aFlags [optional]
+ * Additional options or flags for this scope.
+ */
+ _init: function (aName, aFlags) {
+ this._idString = generateId(this._nameString = aName);
+ this._displayScope(aName, `${this.targetClassName} ${aFlags.customClass}`,
+ "devtools-toolbar");
+ this._addEventListeners();
+ this.parentNode.appendChild(this._target);
+ },
+
+ /**
+ * Creates the necessary nodes for this scope.
+ *
+ * @param string aName
+ * The scope's name.
+ * @param string aTargetClassName
+ * A custom class name for this scope's target element.
+ * @param string aTitleClassName [optional]
+ * A custom class name for this scope's title element.
+ */
+ _displayScope: function (aName = "", aTargetClassName, aTitleClassName = "") {
+ let document = this.document;
+
+ let element = this._target = document.createElement("vbox");
+ element.id = this._idString;
+ element.className = aTargetClassName;
+
+ let arrow = this._arrow = document.createElement("hbox");
+ arrow.className = "arrow theme-twisty";
+
+ let name = this._name = document.createElement("label");
+ name.className = "plain name";
+ name.setAttribute("value", aName.trim());
+ name.setAttribute("crop", "end");
+
+ let title = this._title = document.createElement("hbox");
+ title.className = "title " + aTitleClassName;
+ title.setAttribute("align", "center");
+
+ let enumerable = this._enum = document.createElement("vbox");
+ let nonenum = this._nonenum = document.createElement("vbox");
+ enumerable.className = "variables-view-element-details enum";
+ nonenum.className = "variables-view-element-details nonenum";
+
+ title.appendChild(arrow);
+ title.appendChild(name);
+
+ element.appendChild(title);
+ element.appendChild(enumerable);
+ element.appendChild(nonenum);
+ },
+
+ /**
+ * Adds the necessary event listeners for this scope.
+ */
+ _addEventListeners: function () {
+ this._title.addEventListener("mousedown", this._onClick, false);
+ },
+
+ /**
+ * The click listener for this scope's title.
+ */
+ _onClick: function (e) {
+ if (this.editing ||
+ e.button != 0 ||
+ e.target == this._editNode ||
+ e.target == this._deleteNode ||
+ e.target == this._addPropertyNode) {
+ return;
+ }
+ this.toggle();
+ this.focus();
+ },
+
+ /**
+ * Opens the enumerable items container.
+ */
+ _openEnum: function () {
+ this._arrow.setAttribute("open", "");
+ this._enum.setAttribute("open", "");
+ },
+
+ /**
+ * Opens the non-enumerable items container.
+ */
+ _openNonEnum: function () {
+ this._nonenum.setAttribute("open", "");
+ },
+
+ /**
+ * Specifies if enumerable properties and variables should be displayed.
+ * @param boolean aFlag
+ */
+ set _enumVisible(aFlag) {
+ for (let [, variable] of this._store) {
+ variable._enumVisible = aFlag;
+
+ if (!this._isExpanded) {
+ continue;
+ }
+ if (aFlag) {
+ this._enum.setAttribute("open", "");
+ } else {
+ this._enum.removeAttribute("open");
+ }
+ }
+ },
+
+ /**
+ * Specifies if non-enumerable properties and variables should be displayed.
+ * @param boolean aFlag
+ */
+ set _nonEnumVisible(aFlag) {
+ for (let [, variable] of this._store) {
+ variable._nonEnumVisible = aFlag;
+
+ if (!this._isExpanded) {
+ continue;
+ }
+ if (aFlag) {
+ this._nonenum.setAttribute("open", "");
+ } else {
+ this._nonenum.removeAttribute("open");
+ }
+ }
+ },
+
+ /**
+ * Performs a case insensitive search for variables or properties matching
+ * the query, and hides non-matched items.
+ *
+ * @param string aLowerCaseQuery
+ * The lowercased name of the variable or property to search for.
+ */
+ _performSearch: function (aLowerCaseQuery) {
+ for (let [, variable] of this._store) {
+ let currentObject = variable;
+ let lowerCaseName = variable._nameString.toLowerCase();
+ let lowerCaseValue = variable._valueString.toLowerCase();
+
+ // Non-matched variables or properties require a corresponding attribute.
+ if (!lowerCaseName.includes(aLowerCaseQuery) &&
+ !lowerCaseValue.includes(aLowerCaseQuery)) {
+ variable._matched = false;
+ }
+ // Variable or property is matched.
+ else {
+ variable._matched = true;
+
+ // If the variable was ever expanded, there's a possibility it may
+ // contain some matched properties, so make sure they're visible
+ // ("expand downwards").
+ if (variable._store.size) {
+ variable.expand();
+ }
+
+ // If the variable is contained in another Scope, Variable, or Property,
+ // the parent may not be a match, thus hidden. It should be visible
+ // ("expand upwards").
+ while ((variable = variable.ownerView) && variable instanceof Scope) {
+ variable._matched = true;
+ variable.expand();
+ }
+ }
+
+ // Proceed with the search recursively inside this variable or property.
+ if (currentObject._store.size || currentObject.getter || currentObject.setter) {
+ currentObject._performSearch(aLowerCaseQuery);
+ }
+ }
+ },
+
+ /**
+ * Sets if this object instance is a matched or non-matched item.
+ * @param boolean aStatus
+ */
+ set _matched(aStatus) {
+ if (this._isMatch == aStatus) {
+ return;
+ }
+ if (aStatus) {
+ this._isMatch = true;
+ this.target.removeAttribute("unmatched");
+ } else {
+ this._isMatch = false;
+ this.target.setAttribute("unmatched", "");
+ }
+ },
+
+ /**
+ * Find the first item in the tree of visible items in this item that matches
+ * the predicate. Searches in visual order (the order seen by the user).
+ * Tests itself, then descends into first the enumerable children and then
+ * the non-enumerable children (since they are presented in separate groups).
+ *
+ * @param function aPredicate
+ * A function that returns true when a match is found.
+ * @return Scope | Variable | Property
+ * The first visible scope, variable or property, or null if nothing
+ * is found.
+ */
+ _findInVisibleItems: function (aPredicate) {
+ if (aPredicate(this)) {
+ return this;
+ }
+
+ if (this._isExpanded) {
+ if (this._variablesView._enumVisible) {
+ for (let item of this._enumItems) {
+ let result = item._findInVisibleItems(aPredicate);
+ if (result) {
+ return result;
+ }
+ }
+ }
+
+ if (this._variablesView._nonEnumVisible) {
+ for (let item of this._nonEnumItems) {
+ let result = item._findInVisibleItems(aPredicate);
+ if (result) {
+ return result;
+ }
+ }
+ }
+ }
+
+ return null;
+ },
+
+ /**
+ * Find the last item in the tree of visible items in this item that matches
+ * the predicate. Searches in reverse visual order (opposite of the order
+ * seen by the user). Descends into first the non-enumerable children, then
+ * the enumerable children (since they are presented in separate groups), and
+ * finally tests itself.
+ *
+ * @param function aPredicate
+ * A function that returns true when a match is found.
+ * @return Scope | Variable | Property
+ * The last visible scope, variable or property, or null if nothing
+ * is found.
+ */
+ _findInVisibleItemsReverse: function (aPredicate) {
+ if (this._isExpanded) {
+ if (this._variablesView._nonEnumVisible) {
+ for (let i = this._nonEnumItems.length - 1; i >= 0; i--) {
+ let item = this._nonEnumItems[i];
+ let result = item._findInVisibleItemsReverse(aPredicate);
+ if (result) {
+ return result;
+ }
+ }
+ }
+
+ if (this._variablesView._enumVisible) {
+ for (let i = this._enumItems.length - 1; i >= 0; i--) {
+ let item = this._enumItems[i];
+ let result = item._findInVisibleItemsReverse(aPredicate);
+ if (result) {
+ return result;
+ }
+ }
+ }
+ }
+
+ if (aPredicate(this)) {
+ return this;
+ }
+
+ return null;
+ },
+
+ /**
+ * Gets top level variables view instance.
+ * @return VariablesView
+ */
+ get _variablesView() {
+ return this._topView || (this._topView = (() => {
+ let parentView = this.ownerView;
+ let topView;
+
+ while ((topView = parentView.ownerView)) {
+ parentView = topView;
+ }
+ return parentView;
+ })());
+ },
+
+ /**
+ * Gets the parent node holding this scope.
+ * @return nsIDOMNode
+ */
+ get parentNode() {
+ return this.ownerView._list;
+ },
+
+ /**
+ * Gets the owner document holding this scope.
+ * @return nsIHTMLDocument
+ */
+ get document() {
+ return this._document || (this._document = this.ownerView.document);
+ },
+
+ /**
+ * Gets the default window holding this scope.
+ * @return nsIDOMWindow
+ */
+ get window() {
+ return this._window || (this._window = this.ownerView.window);
+ },
+
+ _topView: null,
+ _document: null,
+ _window: null,
+
+ ownerView: null,
+ eval: null,
+ switch: null,
+ delete: null,
+ new: null,
+ preventDisableOnChange: false,
+ preventDescriptorModifiers: false,
+ editing: false,
+ editableNameTooltip: "",
+ editableValueTooltip: "",
+ editButtonTooltip: "",
+ deleteButtonTooltip: "",
+ domNodeValueTooltip: "",
+ contextMenuId: "",
+ separatorStr: "",
+
+ _store: null,
+ _enumItems: null,
+ _nonEnumItems: null,
+ _fetched: false,
+ _committed: false,
+ _isLocked: false,
+ _isExpanded: false,
+ _isContentVisible: true,
+ _isHeaderVisible: true,
+ _isArrowVisible: true,
+ _isMatch: true,
+ _idString: "",
+ _nameString: "",
+ _target: null,
+ _arrow: null,
+ _name: null,
+ _title: null,
+ _enum: null,
+ _nonenum: null,
+};
+
+// Creating maps and arrays thousands of times for variables or properties
+// with a large number of children fills up a lot of memory. Make sure
+// these are instantiated only if needed.
+DevToolsUtils.defineLazyPrototypeGetter(Scope.prototype, "_store", () => new Map());
+DevToolsUtils.defineLazyPrototypeGetter(Scope.prototype, "_enumItems", Array);
+DevToolsUtils.defineLazyPrototypeGetter(Scope.prototype, "_nonEnumItems", Array);
+
+/**
+ * A Variable is a Scope holding Property instances.
+ * Iterable via "for (let [name, property] of instance) { }".
+ *
+ * @param Scope aScope
+ * The scope to contain this variable.
+ * @param string aName
+ * The variable's name.
+ * @param object aDescriptor
+ * The variable's descriptor.
+ * @param object aOptions
+ * Options of the form accepted by Scope.addItem
+ */
+function Variable(aScope, aName, aDescriptor, aOptions) {
+ this._setTooltips = this._setTooltips.bind(this);
+ this._activateNameInput = this._activateNameInput.bind(this);
+ this._activateValueInput = this._activateValueInput.bind(this);
+ this.openNodeInInspector = this.openNodeInInspector.bind(this);
+ this.highlightDomNode = this.highlightDomNode.bind(this);
+ this.unhighlightDomNode = this.unhighlightDomNode.bind(this);
+ this._internalItem = aOptions.internalItem;
+
+ // Treat safe getter descriptors as descriptors with a value.
+ if ("getterValue" in aDescriptor) {
+ aDescriptor.value = aDescriptor.getterValue;
+ delete aDescriptor.get;
+ delete aDescriptor.set;
+ }
+
+ Scope.call(this, aScope, aName, this._initialDescriptor = aDescriptor);
+ this.setGrip(aDescriptor.value);
+}
+
+Variable.prototype = Heritage.extend(Scope.prototype, {
+ /**
+ * Whether this Variable should be prefetched when it is remoted.
+ */
+ get shouldPrefetch() {
+ return this.name == "window" || this.name == "this";
+ },
+
+ /**
+ * Whether this Variable should paginate its contents.
+ */
+ get allowPaginate() {
+ return this.name != "window" && this.name != "this";
+ },
+
+ /**
+ * The class name applied to this variable's target element.
+ */
+ targetClassName: "variables-view-variable variable-or-property",
+
+ /**
+ * Create a new Property that is a child of Variable.
+ *
+ * @param string aName
+ * The name of the new Property.
+ * @param object aDescriptor
+ * The property's descriptor.
+ * @param object aOptions
+ * Options of the form accepted by Scope.addItem
+ * @return Property
+ * The newly created child Property.
+ */
+ _createChild: function (aName, aDescriptor, aOptions) {
+ return new Property(this, aName, aDescriptor, aOptions);
+ },
+
+ /**
+ * Remove this Variable from its parent and remove all children recursively.
+ */
+ remove: function () {
+ if (this._linkedToInspector) {
+ this.unhighlightDomNode();
+ this._valueLabel.removeEventListener("mouseover", this.highlightDomNode, false);
+ this._valueLabel.removeEventListener("mouseout", this.unhighlightDomNode, false);
+ this._openInspectorNode.removeEventListener("mousedown", this.openNodeInInspector, false);
+ }
+
+ this.ownerView._store.delete(this._nameString);
+ this._variablesView._itemsByElement.delete(this._target);
+ this._variablesView._currHierarchy.delete(this.absoluteName);
+
+ this._target.remove();
+
+ for (let property of this._store.values()) {
+ property.remove();
+ }
+ },
+
+ /**
+ * Populates this variable to contain all the properties of an object.
+ *
+ * @param object aObject
+ * The raw object you want to display.
+ * @param object aOptions [optional]
+ * Additional options for adding the properties. Supported options:
+ * - sorted: true to sort all the properties before adding them
+ * - expanded: true to expand all the properties after adding them
+ */
+ populate: function (aObject, aOptions = {}) {
+ // Retrieve the properties only once.
+ if (this._fetched) {
+ return;
+ }
+ this._fetched = true;
+
+ let propertyNames = Object.getOwnPropertyNames(aObject);
+ let prototype = Object.getPrototypeOf(aObject);
+
+ // Sort all of the properties before adding them, if preferred.
+ if (aOptions.sorted) {
+ propertyNames.sort(this._naturalSort);
+ }
+
+ // Add all the variable properties.
+ for (let name of propertyNames) {
+ let descriptor = Object.getOwnPropertyDescriptor(aObject, name);
+ if (descriptor.get || descriptor.set) {
+ let prop = this._addRawNonValueProperty(name, descriptor);
+ if (aOptions.expanded) {
+ prop.expanded = true;
+ }
+ } else {
+ let prop = this._addRawValueProperty(name, descriptor, aObject[name]);
+ if (aOptions.expanded) {
+ prop.expanded = true;
+ }
+ }
+ }
+ // Add the variable's __proto__.
+ if (prototype) {
+ this._addRawValueProperty("__proto__", {}, prototype);
+ }
+ },
+
+ /**
+ * Populates a specific variable or property instance to contain all the
+ * properties of an object
+ *
+ * @param Variable | Property aVar
+ * The target variable to populate.
+ * @param object aObject [optional]
+ * The raw object you want to display. If unspecified, the object is
+ * assumed to be defined in a _sourceValue property on the target.
+ */
+ _populateTarget: function (aVar, aObject = aVar._sourceValue) {
+ aVar.populate(aObject);
+ },
+
+ /**
+ * Adds a property for this variable based on a raw value descriptor.
+ *
+ * @param string aName
+ * The property's name.
+ * @param object aDescriptor
+ * Specifies the exact property descriptor as returned by a call to
+ * Object.getOwnPropertyDescriptor.
+ * @param object aValue
+ * The raw property value you want to display.
+ * @return Property
+ * The newly added property instance.
+ */
+ _addRawValueProperty: function (aName, aDescriptor, aValue) {
+ let descriptor = Object.create(aDescriptor);
+ descriptor.value = VariablesView.getGrip(aValue);
+
+ let propertyItem = this.addItem(aName, descriptor);
+ propertyItem._sourceValue = aValue;
+
+ // Add an 'onexpand' callback for the property, lazily handling
+ // the addition of new child properties.
+ if (!VariablesView.isPrimitive(descriptor)) {
+ propertyItem.onexpand = this._populateTarget;
+ }
+ return propertyItem;
+ },
+
+ /**
+ * Adds a property for this variable based on a getter/setter descriptor.
+ *
+ * @param string aName
+ * The property's name.
+ * @param object aDescriptor
+ * Specifies the exact property descriptor as returned by a call to
+ * Object.getOwnPropertyDescriptor.
+ * @return Property
+ * The newly added property instance.
+ */
+ _addRawNonValueProperty: function (aName, aDescriptor) {
+ let descriptor = Object.create(aDescriptor);
+ descriptor.get = VariablesView.getGrip(aDescriptor.get);
+ descriptor.set = VariablesView.getGrip(aDescriptor.set);
+
+ return this.addItem(aName, descriptor);
+ },
+
+ /**
+ * Gets this variable's path to the topmost scope in the form of a string
+ * meant for use via eval() or a similar approach.
+ * For example, a symbolic name may look like "arguments['0']['foo']['bar']".
+ * @return string
+ */
+ get symbolicName() {
+ return this._nameString || "";
+ },
+
+ /**
+ * Gets full path to this variable, including name of the scope.
+ * @return string
+ */
+ get absoluteName() {
+ if (this._absoluteName) {
+ return this._absoluteName;
+ }
+
+ this._absoluteName = this.ownerView._nameString + "[" + escapeString(this._nameString) + "]";
+ return this._absoluteName;
+ },
+
+ /**
+ * Gets this variable's symbolic path to the topmost scope.
+ * @return array
+ * @see Variable._buildSymbolicPath
+ */
+ get symbolicPath() {
+ if (this._symbolicPath) {
+ return this._symbolicPath;
+ }
+ this._symbolicPath = this._buildSymbolicPath();
+ return this._symbolicPath;
+ },
+
+ /**
+ * Build this variable's path to the topmost scope in form of an array of
+ * strings, one for each segment of the path.
+ * For example, a symbolic path may look like ["0", "foo", "bar"].
+ * @return array
+ */
+ _buildSymbolicPath: function (path = []) {
+ if (this.name) {
+ path.unshift(this.name);
+ if (this.ownerView instanceof Variable) {
+ return this.ownerView._buildSymbolicPath(path);
+ }
+ }
+ return path;
+ },
+
+ /**
+ * Returns this variable's value from the descriptor if available.
+ * @return any
+ */
+ get value() {
+ return this._initialDescriptor.value;
+ },
+
+ /**
+ * Returns this variable's getter from the descriptor if available.
+ * @return object
+ */
+ get getter() {
+ return this._initialDescriptor.get;
+ },
+
+ /**
+ * Returns this variable's getter from the descriptor if available.
+ * @return object
+ */
+ get setter() {
+ return this._initialDescriptor.set;
+ },
+
+ /**
+ * Sets the specific grip for this variable (applies the text content and
+ * class name to the value label).
+ *
+ * The grip should contain the value or the type & class, as defined in the
+ * remote debugger protocol. For convenience, undefined and null are
+ * both considered types.
+ *
+ * @param any aGrip
+ * Specifies the value and/or type & class of the variable.
+ * e.g. - 42
+ * - true
+ * - "nasu"
+ * - { type: "undefined" }
+ * - { type: "null" }
+ * - { type: "object", class: "Object" }
+ */
+ setGrip: function (aGrip) {
+ // Don't allow displaying grip information if there's no name available
+ // or the grip is malformed.
+ if (this._nameString === undefined || aGrip === undefined || aGrip === null) {
+ return;
+ }
+ // Getters and setters should display grip information in sub-properties.
+ if (this.getter || this.setter) {
+ return;
+ }
+
+ let prevGrip = this._valueGrip;
+ if (prevGrip) {
+ this._valueLabel.classList.remove(VariablesView.getClass(prevGrip));
+ }
+ this._valueGrip = aGrip;
+
+ if (aGrip && (aGrip.optimizedOut || aGrip.uninitialized || aGrip.missingArguments)) {
+ if (aGrip.optimizedOut) {
+ this._valueString = L10N.getStr("variablesViewOptimizedOut");
+ }
+ else if (aGrip.uninitialized) {
+ this._valueString = L10N.getStr("variablesViewUninitialized");
+ }
+ else if (aGrip.missingArguments) {
+ this._valueString = L10N.getStr("variablesViewMissingArgs");
+ }
+ this.eval = null;
+ }
+ else {
+ this._valueString = VariablesView.getString(aGrip, {
+ concise: true,
+ noEllipsis: true,
+ });
+ this.eval = this.ownerView.eval;
+ }
+
+ this._valueClassName = VariablesView.getClass(aGrip);
+
+ this._valueLabel.classList.add(this._valueClassName);
+ this._valueLabel.setAttribute("value", this._valueString);
+ this._separatorLabel.hidden = false;
+
+ // DOMNodes get special treatment since they can be linked to the inspector
+ if (this._valueGrip.preview && this._valueGrip.preview.kind === "DOMNode") {
+ this._linkToInspector();
+ }
+ },
+
+ /**
+ * Marks this variable as overridden.
+ *
+ * @param boolean aFlag
+ * Whether this variable is overridden or not.
+ */
+ setOverridden: function (aFlag) {
+ if (aFlag) {
+ this._target.setAttribute("overridden", "");
+ } else {
+ this._target.removeAttribute("overridden");
+ }
+ },
+
+ /**
+ * Briefly flashes this variable.
+ *
+ * @param number aDuration [optional]
+ * An optional flash animation duration.
+ */
+ flash: function (aDuration = ITEM_FLASH_DURATION) {
+ let fadeInDelay = this._variablesView.lazyEmptyDelay + 1;
+ let fadeOutDelay = fadeInDelay + aDuration;
+
+ setNamedTimeout("vview-flash-in" + this.absoluteName,
+ fadeInDelay, () => this._target.setAttribute("changed", ""));
+
+ setNamedTimeout("vview-flash-out" + this.absoluteName,
+ fadeOutDelay, () => this._target.removeAttribute("changed"));
+ },
+
+ /**
+ * Initializes this variable's id, view and binds event listeners.
+ *
+ * @param string aName
+ * The variable's name.
+ * @param object aDescriptor
+ * The variable's descriptor.
+ */
+ _init: function (aName, aDescriptor) {
+ this._idString = generateId(this._nameString = aName);
+ this._displayScope(aName, this.targetClassName);
+ this._displayVariable();
+ this._customizeVariable();
+ this._prepareTooltips();
+ this._setAttributes();
+ this._addEventListeners();
+
+ if (this._initialDescriptor.enumerable ||
+ this._nameString == "this" ||
+ this._internalItem) {
+ this.ownerView._enum.appendChild(this._target);
+ this.ownerView._enumItems.push(this);
+ } else {
+ this.ownerView._nonenum.appendChild(this._target);
+ this.ownerView._nonEnumItems.push(this);
+ }
+ },
+
+ /**
+ * Creates the necessary nodes for this variable.
+ */
+ _displayVariable: function () {
+ let document = this.document;
+ let descriptor = this._initialDescriptor;
+
+ let separatorLabel = this._separatorLabel = document.createElement("label");
+ separatorLabel.className = "plain separator";
+ separatorLabel.setAttribute("value", this.separatorStr + " ");
+
+ let valueLabel = this._valueLabel = document.createElement("label");
+ valueLabel.className = "plain value";
+ valueLabel.setAttribute("flex", "1");
+ valueLabel.setAttribute("crop", "center");
+
+ this._title.appendChild(separatorLabel);
+ this._title.appendChild(valueLabel);
+
+ if (VariablesView.isPrimitive(descriptor)) {
+ this.hideArrow();
+ }
+
+ // If no value will be displayed, we don't need the separator.
+ if (!descriptor.get && !descriptor.set && !("value" in descriptor)) {
+ separatorLabel.hidden = true;
+ }
+
+ // If this is a getter/setter property, create two child pseudo-properties
+ // called "get" and "set" that display the corresponding functions.
+ if (descriptor.get || descriptor.set) {
+ separatorLabel.hidden = true;
+ valueLabel.hidden = true;
+
+ // Changing getter/setter names is never allowed.
+ this.switch = null;
+
+ // Getter/setter properties require special handling when it comes to
+ // evaluation and deletion.
+ if (this.ownerView.eval) {
+ this.delete = VariablesView.getterOrSetterDeleteCallback;
+ this.evaluationMacro = VariablesView.overrideValueEvalMacro;
+ }
+ // Deleting getters and setters individually is not allowed if no
+ // evaluation method is provided.
+ else {
+ this.delete = null;
+ this.evaluationMacro = null;
+ }
+
+ let getter = this.addItem("get", { value: descriptor.get });
+ let setter = this.addItem("set", { value: descriptor.set });
+ getter.evaluationMacro = VariablesView.getterOrSetterEvalMacro;
+ setter.evaluationMacro = VariablesView.getterOrSetterEvalMacro;
+
+ getter.hideArrow();
+ setter.hideArrow();
+ this.expand();
+ }
+ },
+
+ /**
+ * Adds specific nodes for this variable based on custom flags.
+ */
+ _customizeVariable: function () {
+ let ownerView = this.ownerView;
+ let descriptor = this._initialDescriptor;
+
+ if (ownerView.eval && this.getter || this.setter) {
+ let editNode = this._editNode = this.document.createElement("toolbarbutton");
+ editNode.className = "plain variables-view-edit";
+ editNode.addEventListener("mousedown", this._onEdit.bind(this), false);
+ this._title.insertBefore(editNode, this._spacer);
+ }
+
+ if (ownerView.delete) {
+ let deleteNode = this._deleteNode = this.document.createElement("toolbarbutton");
+ deleteNode.className = "plain variables-view-delete";
+ deleteNode.addEventListener("click", this._onDelete.bind(this), false);
+ this._title.appendChild(deleteNode);
+ }
+
+ if (ownerView.new) {
+ let addPropertyNode = this._addPropertyNode = this.document.createElement("toolbarbutton");
+ addPropertyNode.className = "plain variables-view-add-property";
+ addPropertyNode.addEventListener("mousedown", this._onAddProperty.bind(this), false);
+ this._title.appendChild(addPropertyNode);
+
+ // Can't add properties to primitive values, hide the node in those cases.
+ if (VariablesView.isPrimitive(descriptor)) {
+ addPropertyNode.setAttribute("invisible", "");
+ }
+ }
+
+ if (ownerView.contextMenuId) {
+ this._title.setAttribute("context", ownerView.contextMenuId);
+ }
+
+ if (ownerView.preventDescriptorModifiers) {
+ return;
+ }
+
+ if (!descriptor.writable && !ownerView.getter && !ownerView.setter) {
+ let nonWritableIcon = this.document.createElement("hbox");
+ nonWritableIcon.className = "plain variable-or-property-non-writable-icon";
+ nonWritableIcon.setAttribute("optional-visibility", "");
+ this._title.appendChild(nonWritableIcon);
+ }
+ if (descriptor.value && typeof descriptor.value == "object") {
+ if (descriptor.value.frozen) {
+ let frozenLabel = this.document.createElement("label");
+ frozenLabel.className = "plain variable-or-property-frozen-label";
+ frozenLabel.setAttribute("optional-visibility", "");
+ frozenLabel.setAttribute("value", "F");
+ this._title.appendChild(frozenLabel);
+ }
+ if (descriptor.value.sealed) {
+ let sealedLabel = this.document.createElement("label");
+ sealedLabel.className = "plain variable-or-property-sealed-label";
+ sealedLabel.setAttribute("optional-visibility", "");
+ sealedLabel.setAttribute("value", "S");
+ this._title.appendChild(sealedLabel);
+ }
+ if (!descriptor.value.extensible) {
+ let nonExtensibleLabel = this.document.createElement("label");
+ nonExtensibleLabel.className = "plain variable-or-property-non-extensible-label";
+ nonExtensibleLabel.setAttribute("optional-visibility", "");
+ nonExtensibleLabel.setAttribute("value", "N");
+ this._title.appendChild(nonExtensibleLabel);
+ }
+ }
+ },
+
+ /**
+ * Prepares all tooltips for this variable.
+ */
+ _prepareTooltips: function () {
+ this._target.addEventListener("mouseover", this._setTooltips, false);
+ },
+
+ /**
+ * Sets all tooltips for this variable.
+ */
+ _setTooltips: function () {
+ this._target.removeEventListener("mouseover", this._setTooltips, false);
+
+ let ownerView = this.ownerView;
+ if (ownerView.preventDescriptorModifiers) {
+ return;
+ }
+
+ let tooltip = this.document.createElement("tooltip");
+ tooltip.id = "tooltip-" + this._idString;
+ tooltip.setAttribute("orient", "horizontal");
+
+ let labels = [
+ "configurable", "enumerable", "writable",
+ "frozen", "sealed", "extensible", "overridden", "WebIDL"];
+
+ for (let type of labels) {
+ let labelElement = this.document.createElement("label");
+ labelElement.className = type;
+ labelElement.setAttribute("value", L10N.getStr(type + "Tooltip"));
+ tooltip.appendChild(labelElement);
+ }
+
+ this._target.appendChild(tooltip);
+ this._target.setAttribute("tooltip", tooltip.id);
+
+ if (this._editNode && ownerView.eval) {
+ this._editNode.setAttribute("tooltiptext", ownerView.editButtonTooltip);
+ }
+ if (this._openInspectorNode && this._linkedToInspector) {
+ this._openInspectorNode.setAttribute("tooltiptext", this.ownerView.domNodeValueTooltip);
+ }
+ if (this._valueLabel && ownerView.eval) {
+ this._valueLabel.setAttribute("tooltiptext", ownerView.editableValueTooltip);
+ }
+ if (this._name && ownerView.switch) {
+ this._name.setAttribute("tooltiptext", ownerView.editableNameTooltip);
+ }
+ if (this._deleteNode && ownerView.delete) {
+ this._deleteNode.setAttribute("tooltiptext", ownerView.deleteButtonTooltip);
+ }
+ },
+
+ /**
+ * Get the parent variablesview toolbox, if any.
+ */
+ get toolbox() {
+ return this._variablesView.toolbox;
+ },
+
+ /**
+ * Checks if this variable is a DOMNode and is part of a variablesview that
+ * has been linked to the toolbox, so that highlighting and jumping to the
+ * inspector can be done.
+ */
+ _isLinkableToInspector: function () {
+ let isDomNode = this._valueGrip && this._valueGrip.preview.kind === "DOMNode";
+ let hasBeenLinked = this._linkedToInspector;
+ let hasToolbox = !!this.toolbox;
+
+ return isDomNode && !hasBeenLinked && hasToolbox;
+ },
+
+ /**
+ * If the variable is a DOMNode, and if a toolbox is set, then link it to the
+ * inspector (highlight on hover, and jump to markup-view on click)
+ */
+ _linkToInspector: function () {
+ if (!this._isLinkableToInspector()) {
+ return;
+ }
+
+ // Listen to value mouseover/click events to highlight and jump
+ this._valueLabel.addEventListener("mouseover", this.highlightDomNode, false);
+ this._valueLabel.addEventListener("mouseout", this.unhighlightDomNode, false);
+
+ // Add a button to open the node in the inspector
+ this._openInspectorNode = this.document.createElement("toolbarbutton");
+ this._openInspectorNode.className = "plain variables-view-open-inspector";
+ this._openInspectorNode.addEventListener("mousedown", this.openNodeInInspector, false);
+ this._title.appendChild(this._openInspectorNode);
+
+ this._linkedToInspector = true;
+ },
+
+ /**
+ * In case this variable is a DOMNode and part of a variablesview that has been
+ * linked to the toolbox's inspector, then select the corresponding node in
+ * the inspector, and switch the inspector tool in the toolbox
+ * @return a promise that resolves when the node is selected and the inspector
+ * has been switched to and is ready
+ */
+ openNodeInInspector: function (event) {
+ if (!this.toolbox) {
+ return promise.reject(new Error("Toolbox not available"));
+ }
+
+ event && event.stopPropagation();
+
+ return Task.spawn(function* () {
+ yield this.toolbox.initInspector();
+
+ let nodeFront = this._nodeFront;
+ if (!nodeFront) {
+ nodeFront = yield this.toolbox.walker.getNodeActorFromObjectActor(this._valueGrip.actor);
+ }
+
+ if (nodeFront) {
+ yield this.toolbox.selectTool("inspector");
+
+ let inspectorReady = defer();
+ this.toolbox.getPanel("inspector").once("inspector-updated", inspectorReady.resolve);
+ yield this.toolbox.selection.setNodeFront(nodeFront, "variables-view");
+ yield inspectorReady.promise;
+ }
+ }.bind(this));
+ },
+
+ /**
+ * In case this variable is a DOMNode and part of a variablesview that has been
+ * linked to the toolbox's inspector, then highlight the corresponding node
+ */
+ highlightDomNode: function () {
+ if (this.toolbox) {
+ if (this._nodeFront) {
+ // If the nodeFront has been retrieved before, no need to ask the server
+ // again for it
+ this.toolbox.highlighterUtils.highlightNodeFront(this._nodeFront);
+ return;
+ }
+
+ this.toolbox.highlighterUtils.highlightDomValueGrip(this._valueGrip).then(front => {
+ this._nodeFront = front;
+ });
+ }
+ },
+
+ /**
+ * Unhighlight a previously highlit node
+ * @see highlightDomNode
+ */
+ unhighlightDomNode: function () {
+ if (this.toolbox) {
+ this.toolbox.highlighterUtils.unhighlight();
+ }
+ },
+
+ /**
+ * Sets a variable's configurable, enumerable and writable attributes,
+ * and specifies if it's a 'this', '<exception>', '<return>' or '__proto__'
+ * reference.
+ */
+ _setAttributes: function () {
+ let ownerView = this.ownerView;
+ if (ownerView.preventDescriptorModifiers) {
+ return;
+ }
+
+ let descriptor = this._initialDescriptor;
+ let target = this._target;
+ let name = this._nameString;
+
+ if (ownerView.eval) {
+ target.setAttribute("editable", "");
+ }
+
+ if (!descriptor.configurable) {
+ target.setAttribute("non-configurable", "");
+ }
+ if (!descriptor.enumerable) {
+ target.setAttribute("non-enumerable", "");
+ }
+ if (!descriptor.writable && !ownerView.getter && !ownerView.setter) {
+ target.setAttribute("non-writable", "");
+ }
+
+ if (descriptor.value && typeof descriptor.value == "object") {
+ if (descriptor.value.frozen) {
+ target.setAttribute("frozen", "");
+ }
+ if (descriptor.value.sealed) {
+ target.setAttribute("sealed", "");
+ }
+ if (!descriptor.value.extensible) {
+ target.setAttribute("non-extensible", "");
+ }
+ }
+
+ if (descriptor && "getterValue" in descriptor) {
+ target.setAttribute("safe-getter", "");
+ }
+
+ if (name == "this") {
+ target.setAttribute("self", "");
+ }
+ else if (this._internalItem && name == "<exception>") {
+ target.setAttribute("exception", "");
+ target.setAttribute("pseudo-item", "");
+ }
+ else if (this._internalItem && name == "<return>") {
+ target.setAttribute("return", "");
+ target.setAttribute("pseudo-item", "");
+ }
+ else if (name == "__proto__") {
+ target.setAttribute("proto", "");
+ target.setAttribute("pseudo-item", "");
+ }
+
+ if (Object.keys(descriptor).length == 0) {
+ target.setAttribute("pseudo-item", "");
+ }
+ },
+
+ /**
+ * Adds the necessary event listeners for this variable.
+ */
+ _addEventListeners: function () {
+ this._name.addEventListener("dblclick", this._activateNameInput, false);
+ this._valueLabel.addEventListener("mousedown", this._activateValueInput, false);
+ this._title.addEventListener("mousedown", this._onClick, false);
+ },
+
+ /**
+ * Makes this variable's name editable.
+ */
+ _activateNameInput: function (e) {
+ if (!this._variablesView.alignedValues) {
+ this._separatorLabel.hidden = true;
+ this._valueLabel.hidden = true;
+ }
+
+ EditableName.create(this, {
+ onSave: aKey => {
+ if (!this._variablesView.preventDisableOnChange) {
+ this._disable();
+ }
+ this.ownerView.switch(this, aKey);
+ },
+ onCleanup: () => {
+ if (!this._variablesView.alignedValues) {
+ this._separatorLabel.hidden = false;
+ this._valueLabel.hidden = false;
+ }
+ }
+ }, e);
+ },
+
+ /**
+ * Makes this variable's value editable.
+ */
+ _activateValueInput: function (e) {
+ EditableValue.create(this, {
+ onSave: aString => {
+ if (this._linkedToInspector) {
+ this.unhighlightDomNode();
+ }
+ if (!this._variablesView.preventDisableOnChange) {
+ this._disable();
+ }
+ this.ownerView.eval(this, aString);
+ }
+ }, e);
+ },
+
+ /**
+ * Disables this variable prior to a new name switch or value evaluation.
+ */
+ _disable: function () {
+ // Prevent the variable from being collapsed or expanded.
+ this.hideArrow();
+
+ // Hide any nodes that may offer information about the variable.
+ for (let node of this._title.childNodes) {
+ node.hidden = node != this._arrow && node != this._name;
+ }
+ this._enum.hidden = true;
+ this._nonenum.hidden = true;
+ },
+
+ /**
+ * The current macro used to generate the string evaluated when performing
+ * a variable or property value change.
+ */
+ evaluationMacro: VariablesView.simpleValueEvalMacro,
+
+ /**
+ * The click listener for the edit button.
+ */
+ _onEdit: function (e) {
+ if (e.button != 0) {
+ return;
+ }
+
+ e.preventDefault();
+ e.stopPropagation();
+ this._activateValueInput();
+ },
+
+ /**
+ * The click listener for the delete button.
+ */
+ _onDelete: function (e) {
+ if ("button" in e && e.button != 0) {
+ return;
+ }
+
+ e.preventDefault();
+ e.stopPropagation();
+
+ if (this.ownerView.delete) {
+ if (!this.ownerView.delete(this)) {
+ this.hide();
+ }
+ }
+ },
+
+ /**
+ * The click listener for the add property button.
+ */
+ _onAddProperty: function (e) {
+ if ("button" in e && e.button != 0) {
+ return;
+ }
+
+ e.preventDefault();
+ e.stopPropagation();
+
+ this.expanded = true;
+
+ let item = this.addItem(" ", {
+ value: undefined,
+ configurable: true,
+ enumerable: true,
+ writable: true
+ }, {relaxed: true});
+
+ // Force showing the separator.
+ item._separatorLabel.hidden = false;
+
+ EditableNameAndValue.create(item, {
+ onSave: ([aKey, aValue]) => {
+ if (!this._variablesView.preventDisableOnChange) {
+ this._disable();
+ }
+ this.ownerView.new(this, aKey, aValue);
+ }
+ }, e);
+ },
+
+ _symbolicName: null,
+ _symbolicPath: null,
+ _absoluteName: null,
+ _initialDescriptor: null,
+ _separatorLabel: null,
+ _valueLabel: null,
+ _spacer: null,
+ _editNode: null,
+ _deleteNode: null,
+ _addPropertyNode: null,
+ _tooltip: null,
+ _valueGrip: null,
+ _valueString: "",
+ _valueClassName: "",
+ _prevExpandable: false,
+ _prevExpanded: false
+});
+
+/**
+ * A Property is a Variable holding additional child Property instances.
+ * Iterable via "for (let [name, property] of instance) { }".
+ *
+ * @param Variable aVar
+ * The variable to contain this property.
+ * @param string aName
+ * The property's name.
+ * @param object aDescriptor
+ * The property's descriptor.
+ * @param object aOptions
+ * Options of the form accepted by Scope.addItem
+ */
+function Property(aVar, aName, aDescriptor, aOptions) {
+ Variable.call(this, aVar, aName, aDescriptor, aOptions);
+}
+
+Property.prototype = Heritage.extend(Variable.prototype, {
+ /**
+ * The class name applied to this property's target element.
+ */
+ targetClassName: "variables-view-property variable-or-property",
+
+ /**
+ * @see Variable.symbolicName
+ * @return string
+ */
+ get symbolicName() {
+ if (this._symbolicName) {
+ return this._symbolicName;
+ }
+
+ this._symbolicName = this.ownerView.symbolicName + "[" + escapeString(this._nameString) + "]";
+ return this._symbolicName;
+ },
+
+ /**
+ * @see Variable.absoluteName
+ * @return string
+ */
+ get absoluteName() {
+ if (this._absoluteName) {
+ return this._absoluteName;
+ }
+
+ this._absoluteName = this.ownerView.absoluteName + "[" + escapeString(this._nameString) + "]";
+ return this._absoluteName;
+ }
+});
+
+/**
+ * A generator-iterator over the VariablesView, Scopes, Variables and Properties.
+ */
+VariablesView.prototype[Symbol.iterator] =
+Scope.prototype[Symbol.iterator] =
+Variable.prototype[Symbol.iterator] =
+Property.prototype[Symbol.iterator] = function* () {
+ yield* this._store;
+};
+
+/**
+ * Forget everything recorded about added scopes, variables or properties.
+ * @see VariablesView.commitHierarchy
+ */
+VariablesView.prototype.clearHierarchy = function () {
+ this._prevHierarchy.clear();
+ this._currHierarchy.clear();
+};
+
+/**
+ * Perform operations on all the VariablesView Scopes, Variables and Properties
+ * after you've added all the items you wanted.
+ *
+ * Calling this method is optional, and does the following:
+ * - styles the items overridden by other items in parent scopes
+ * - reopens the items which were previously expanded
+ * - flashes the items whose values changed
+ */
+VariablesView.prototype.commitHierarchy = function () {
+ for (let [, currItem] of this._currHierarchy) {
+ // Avoid performing expensive operations.
+ if (this.commitHierarchyIgnoredItems[currItem._nameString]) {
+ continue;
+ }
+ let overridden = this.isOverridden(currItem);
+ if (overridden) {
+ currItem.setOverridden(true);
+ }
+ let expanded = !currItem._committed && this.wasExpanded(currItem);
+ if (expanded) {
+ currItem.expand();
+ }
+ let changed = !currItem._committed && this.hasChanged(currItem);
+ if (changed) {
+ currItem.flash();
+ }
+ currItem._committed = true;
+ }
+ if (this.oncommit) {
+ this.oncommit(this);
+ }
+};
+
+// Some variables are likely to contain a very large number of properties.
+// It would be a bad idea to re-expand them or perform expensive operations.
+VariablesView.prototype.commitHierarchyIgnoredItems = Heritage.extend(null, {
+ "window": true,
+ "this": true
+});
+
+/**
+ * Checks if the an item was previously expanded, if it existed in a
+ * previous hierarchy.
+ *
+ * @param Scope | Variable | Property aItem
+ * The item to verify.
+ * @return boolean
+ * Whether the item was expanded.
+ */
+VariablesView.prototype.wasExpanded = function (aItem) {
+ if (!(aItem instanceof Scope)) {
+ return false;
+ }
+ let prevItem = this._prevHierarchy.get(aItem.absoluteName || aItem._nameString);
+ return prevItem ? prevItem._isExpanded : false;
+};
+
+/**
+ * Checks if the an item's displayed value (a representation of the grip)
+ * has changed, if it existed in a previous hierarchy.
+ *
+ * @param Variable | Property aItem
+ * The item to verify.
+ * @return boolean
+ * Whether the item has changed.
+ */
+VariablesView.prototype.hasChanged = function (aItem) {
+ // Only analyze Variables and Properties for displayed value changes.
+ // Scopes are just collections of Variables and Properties and
+ // don't have a "value", so they can't change.
+ if (!(aItem instanceof Variable)) {
+ return false;
+ }
+ let prevItem = this._prevHierarchy.get(aItem.absoluteName);
+ return prevItem ? prevItem._valueString != aItem._valueString : false;
+};
+
+/**
+ * Checks if the an item was previously expanded, if it existed in a
+ * previous hierarchy.
+ *
+ * @param Scope | Variable | Property aItem
+ * The item to verify.
+ * @return boolean
+ * Whether the item was expanded.
+ */
+VariablesView.prototype.isOverridden = function (aItem) {
+ // Only analyze Variables for being overridden in different Scopes.
+ if (!(aItem instanceof Variable) || aItem instanceof Property) {
+ return false;
+ }
+ let currVariableName = aItem._nameString;
+ let parentScopes = this.getParentScopesForVariableOrProperty(aItem);
+
+ for (let otherScope of parentScopes) {
+ for (let [otherVariableName] of otherScope) {
+ if (otherVariableName == currVariableName) {
+ return true;
+ }
+ }
+ }
+ return false;
+};
+
+/**
+ * Returns true if the descriptor represents an undefined, null or
+ * primitive value.
+ *
+ * @param object aDescriptor
+ * The variable's descriptor.
+ */
+VariablesView.isPrimitive = function (aDescriptor) {
+ // For accessor property descriptors, the getter and setter need to be
+ // contained in 'get' and 'set' properties.
+ let getter = aDescriptor.get;
+ let setter = aDescriptor.set;
+ if (getter || setter) {
+ return false;
+ }
+
+ // As described in the remote debugger protocol, the value grip
+ // must be contained in a 'value' property.
+ let grip = aDescriptor.value;
+ if (typeof grip != "object") {
+ return true;
+ }
+
+ // For convenience, undefined, null, Infinity, -Infinity, NaN, -0, and long
+ // strings are considered types.
+ let type = grip.type;
+ if (type == "undefined" ||
+ type == "null" ||
+ type == "Infinity" ||
+ type == "-Infinity" ||
+ type == "NaN" ||
+ type == "-0" ||
+ type == "symbol" ||
+ type == "longString") {
+ return true;
+ }
+
+ return false;
+};
+
+/**
+ * Returns true if the descriptor represents an undefined value.
+ *
+ * @param object aDescriptor
+ * The variable's descriptor.
+ */
+VariablesView.isUndefined = function (aDescriptor) {
+ // For accessor property descriptors, the getter and setter need to be
+ // contained in 'get' and 'set' properties.
+ let getter = aDescriptor.get;
+ let setter = aDescriptor.set;
+ if (typeof getter == "object" && getter.type == "undefined" &&
+ typeof setter == "object" && setter.type == "undefined") {
+ return true;
+ }
+
+ // As described in the remote debugger protocol, the value grip
+ // must be contained in a 'value' property.
+ let grip = aDescriptor.value;
+ if (typeof grip == "object" && grip.type == "undefined") {
+ return true;
+ }
+
+ return false;
+};
+
+/**
+ * Returns true if the descriptor represents a falsy value.
+ *
+ * @param object aDescriptor
+ * The variable's descriptor.
+ */
+VariablesView.isFalsy = function (aDescriptor) {
+ // As described in the remote debugger protocol, the value grip
+ // must be contained in a 'value' property.
+ let grip = aDescriptor.value;
+ if (typeof grip != "object") {
+ return !grip;
+ }
+
+ // For convenience, undefined, null, NaN, and -0 are all considered types.
+ let type = grip.type;
+ if (type == "undefined" ||
+ type == "null" ||
+ type == "NaN" ||
+ type == "-0") {
+ return true;
+ }
+
+ return false;
+};
+
+/**
+ * Returns true if the value is an instance of Variable or Property.
+ *
+ * @param any aValue
+ * The value to test.
+ */
+VariablesView.isVariable = function (aValue) {
+ return aValue instanceof Variable;
+};
+
+/**
+ * Returns a standard grip for a value.
+ *
+ * @param any aValue
+ * The raw value to get a grip for.
+ * @return any
+ * The value's grip.
+ */
+VariablesView.getGrip = function (aValue) {
+ switch (typeof aValue) {
+ case "boolean":
+ case "string":
+ return aValue;
+ case "number":
+ if (aValue === Infinity) {
+ return { type: "Infinity" };
+ } else if (aValue === -Infinity) {
+ return { type: "-Infinity" };
+ } else if (Number.isNaN(aValue)) {
+ return { type: "NaN" };
+ } else if (1 / aValue === -Infinity) {
+ return { type: "-0" };
+ }
+ return aValue;
+ case "undefined":
+ // document.all is also "undefined"
+ if (aValue === undefined) {
+ return { type: "undefined" };
+ }
+ case "object":
+ if (aValue === null) {
+ return { type: "null" };
+ }
+ case "function":
+ return { type: "object",
+ class: WebConsoleUtils.getObjectClassName(aValue) };
+ default:
+ console.error("Failed to provide a grip for value of " + typeof value +
+ ": " + aValue);
+ return null;
+ }
+};
+
+/**
+ * Returns a custom formatted property string for a grip.
+ *
+ * @param any aGrip
+ * @see Variable.setGrip
+ * @param object aOptions
+ * Options:
+ * - concise: boolean that tells you want a concisely formatted string.
+ * - noStringQuotes: boolean that tells to not quote strings.
+ * - noEllipsis: boolean that tells to not add an ellipsis after the
+ * initial text of a longString.
+ * @return string
+ * The formatted property string.
+ */
+VariablesView.getString = function (aGrip, aOptions = {}) {
+ if (aGrip && typeof aGrip == "object") {
+ switch (aGrip.type) {
+ case "undefined":
+ case "null":
+ case "NaN":
+ case "Infinity":
+ case "-Infinity":
+ case "-0":
+ return aGrip.type;
+ default:
+ let stringifier = VariablesView.stringifiers.byType[aGrip.type];
+ if (stringifier) {
+ let result = stringifier(aGrip, aOptions);
+ if (result != null) {
+ return result;
+ }
+ }
+
+ if (aGrip.displayString) {
+ return VariablesView.getString(aGrip.displayString, aOptions);
+ }
+
+ if (aGrip.type == "object" && aOptions.concise) {
+ return aGrip.class;
+ }
+
+ return "[" + aGrip.type + " " + aGrip.class + "]";
+ }
+ }
+
+ switch (typeof aGrip) {
+ case "string":
+ return VariablesView.stringifiers.byType.string(aGrip, aOptions);
+ case "boolean":
+ return aGrip ? "true" : "false";
+ case "number":
+ if (!aGrip && 1 / aGrip === -Infinity) {
+ return "-0";
+ }
+ default:
+ return aGrip + "";
+ }
+};
+
+/**
+ * The VariablesView stringifiers are used by VariablesView.getString(). These
+ * are organized by object type, object class and by object actor preview kind.
+ * Some objects share identical ways for previews, for example Arrays, Sets and
+ * NodeLists.
+ *
+ * Any stringifier function must return a string. If null is returned, * then
+ * the default stringifier will be used. When invoked, the stringifier is
+ * given the same two arguments as those given to VariablesView.getString().
+ */
+VariablesView.stringifiers = {};
+
+VariablesView.stringifiers.byType = {
+ string: function (aGrip, {noStringQuotes}) {
+ if (noStringQuotes) {
+ return aGrip;
+ }
+ return '"' + aGrip + '"';
+ },
+
+ longString: function ({initial}, {noStringQuotes, noEllipsis}) {
+ let ellipsis = noEllipsis ? "" : ELLIPSIS;
+ if (noStringQuotes) {
+ return initial + ellipsis;
+ }
+ let result = '"' + initial + '"';
+ if (!ellipsis) {
+ return result;
+ }
+ return result.substr(0, result.length - 1) + ellipsis + '"';
+ },
+
+ object: function (aGrip, aOptions) {
+ let {preview} = aGrip;
+ let stringifier;
+ if (aGrip.class) {
+ stringifier = VariablesView.stringifiers.byObjectClass[aGrip.class];
+ }
+ if (!stringifier && preview && preview.kind) {
+ stringifier = VariablesView.stringifiers.byObjectKind[preview.kind];
+ }
+ if (stringifier) {
+ return stringifier(aGrip, aOptions);
+ }
+ return null;
+ },
+
+ symbol: function (aGrip, aOptions) {
+ const name = aGrip.name || "";
+ return "Symbol(" + name + ")";
+ },
+
+ mapEntry: function (aGrip, {concise}) {
+ let { preview: { key, value }} = aGrip;
+
+ let keyString = VariablesView.getString(key, {
+ concise: true,
+ noStringQuotes: true,
+ });
+ let valueString = VariablesView.getString(value, { concise: true });
+
+ return keyString + " \u2192 " + valueString;
+ },
+
+}; // VariablesView.stringifiers.byType
+
+VariablesView.stringifiers.byObjectClass = {
+ Function: function (aGrip, {concise}) {
+ // TODO: Bug 948484 - support arrow functions and ES6 generators
+
+ let name = aGrip.userDisplayName || aGrip.displayName || aGrip.name || "";
+ name = VariablesView.getString(name, { noStringQuotes: true });
+
+ // TODO: Bug 948489 - Support functions with destructured parameters and
+ // rest parameters
+ let params = aGrip.parameterNames || "";
+ if (!concise) {
+ return "function " + name + "(" + params + ")";
+ }
+ return (name || "function ") + "(" + params + ")";
+ },
+
+ RegExp: function ({displayString}) {
+ return VariablesView.getString(displayString, { noStringQuotes: true });
+ },
+
+ Date: function ({preview}) {
+ if (!preview || !("timestamp" in preview)) {
+ return null;
+ }
+
+ if (typeof preview.timestamp != "number") {
+ return new Date(preview.timestamp).toString(); // invalid date
+ }
+
+ return "Date " + new Date(preview.timestamp).toISOString();
+ },
+
+ Number: function (aGrip) {
+ let {preview} = aGrip;
+ if (preview === undefined) {
+ return null;
+ }
+ return aGrip.class + " { " + VariablesView.getString(preview.wrappedValue) +
+ " }";
+ },
+}; // VariablesView.stringifiers.byObjectClass
+
+VariablesView.stringifiers.byObjectClass.Boolean =
+ VariablesView.stringifiers.byObjectClass.Number;
+
+VariablesView.stringifiers.byObjectKind = {
+ ArrayLike: function (aGrip, {concise}) {
+ let {preview} = aGrip;
+ if (concise) {
+ return aGrip.class + "[" + preview.length + "]";
+ }
+
+ if (!preview.items) {
+ return null;
+ }
+
+ let shown = 0, result = [], lastHole = null;
+ for (let item of preview.items) {
+ if (item === null) {
+ if (lastHole !== null) {
+ result[lastHole] += ",";
+ } else {
+ result.push("");
+ }
+ lastHole = result.length - 1;
+ } else {
+ lastHole = null;
+ result.push(VariablesView.getString(item, { concise: true }));
+ }
+ shown++;
+ }
+
+ if (shown < preview.length) {
+ let n = preview.length - shown;
+ result.push(VariablesView.stringifiers._getNMoreString(n));
+ } else if (lastHole !== null) {
+ // make sure we have the right number of commas...
+ result[lastHole] += ",";
+ }
+
+ let prefix = aGrip.class == "Array" ? "" : aGrip.class + " ";
+ return prefix + "[" + result.join(", ") + "]";
+ },
+
+ MapLike: function (aGrip, {concise}) {
+ let {preview} = aGrip;
+ if (concise || !preview.entries) {
+ let size = typeof preview.size == "number" ?
+ "[" + preview.size + "]" : "";
+ return aGrip.class + size;
+ }
+
+ let entries = [];
+ for (let [key, value] of preview.entries) {
+ let keyString = VariablesView.getString(key, {
+ concise: true,
+ noStringQuotes: true,
+ });
+ let valueString = VariablesView.getString(value, { concise: true });
+ entries.push(keyString + ": " + valueString);
+ }
+
+ if (typeof preview.size == "number" && preview.size > entries.length) {
+ let n = preview.size - entries.length;
+ entries.push(VariablesView.stringifiers._getNMoreString(n));
+ }
+
+ return aGrip.class + " {" + entries.join(", ") + "}";
+ },
+
+ ObjectWithText: function (aGrip, {concise}) {
+ if (concise) {
+ return aGrip.class;
+ }
+
+ return aGrip.class + " " + VariablesView.getString(aGrip.preview.text);
+ },
+
+ ObjectWithURL: function (aGrip, {concise}) {
+ let result = aGrip.class;
+ let url = aGrip.preview.url;
+ if (!VariablesView.isFalsy({ value: url })) {
+ result += ` \u2192 ${getSourceNames(url)[concise ? "short" : "long"]}`;
+ }
+ return result;
+ },
+
+ // Stringifier for any kind of object.
+ Object: function (aGrip, {concise}) {
+ if (concise) {
+ return aGrip.class;
+ }
+
+ let {preview} = aGrip;
+ let props = [];
+
+ if (aGrip.class == "Promise" && aGrip.promiseState) {
+ let { state, value, reason } = aGrip.promiseState;
+ props.push("<state>: " + VariablesView.getString(state));
+ if (state == "fulfilled") {
+ props.push("<value>: " + VariablesView.getString(value, { concise: true }));
+ } else if (state == "rejected") {
+ props.push("<reason>: " + VariablesView.getString(reason, { concise: true }));
+ }
+ }
+
+ for (let key of Object.keys(preview.ownProperties || {})) {
+ let value = preview.ownProperties[key];
+ let valueString = "";
+ if (value.get) {
+ valueString = "Getter";
+ } else if (value.set) {
+ valueString = "Setter";
+ } else {
+ valueString = VariablesView.getString(value.value, { concise: true });
+ }
+ props.push(key + ": " + valueString);
+ }
+
+ for (let key of Object.keys(preview.safeGetterValues || {})) {
+ let value = preview.safeGetterValues[key];
+ let valueString = VariablesView.getString(value.getterValue,
+ { concise: true });
+ props.push(key + ": " + valueString);
+ }
+
+ if (!props.length) {
+ return null;
+ }
+
+ if (preview.ownPropertiesLength) {
+ let previewLength = Object.keys(preview.ownProperties).length;
+ let diff = preview.ownPropertiesLength - previewLength;
+ if (diff > 0) {
+ props.push(VariablesView.stringifiers._getNMoreString(diff));
+ }
+ }
+
+ let prefix = aGrip.class != "Object" ? aGrip.class + " " : "";
+ return prefix + "{" + props.join(", ") + "}";
+ }, // Object
+
+ Error: function (aGrip, {concise}) {
+ let {preview} = aGrip;
+ let name = VariablesView.getString(preview.name, { noStringQuotes: true });
+ if (concise) {
+ return name || aGrip.class;
+ }
+
+ let msg = name + ": " +
+ VariablesView.getString(preview.message, { noStringQuotes: true });
+
+ if (!VariablesView.isFalsy({ value: preview.stack })) {
+ msg += "\n" + L10N.getStr("variablesViewErrorStacktrace") +
+ "\n" + preview.stack;
+ }
+
+ return msg;
+ },
+
+ DOMException: function (aGrip, {concise}) {
+ let {preview} = aGrip;
+ if (concise) {
+ return preview.name || aGrip.class;
+ }
+
+ let msg = aGrip.class + " [" + preview.name + ": " +
+ VariablesView.getString(preview.message) + "\n" +
+ "code: " + preview.code + "\n" +
+ "nsresult: 0x" + (+preview.result).toString(16);
+
+ if (preview.filename) {
+ msg += "\nlocation: " + preview.filename;
+ if (preview.lineNumber) {
+ msg += ":" + preview.lineNumber;
+ }
+ }
+
+ return msg + "]";
+ },
+
+ DOMEvent: function (aGrip, {concise}) {
+ let {preview} = aGrip;
+ if (!preview.type) {
+ return null;
+ }
+
+ if (concise) {
+ return aGrip.class + " " + preview.type;
+ }
+
+ let result = preview.type;
+
+ if (preview.eventKind == "key" && preview.modifiers &&
+ preview.modifiers.length) {
+ result += " " + preview.modifiers.join("-");
+ }
+
+ let props = [];
+ if (preview.target) {
+ let target = VariablesView.getString(preview.target, { concise: true });
+ props.push("target: " + target);
+ }
+
+ for (let prop in preview.properties) {
+ let value = preview.properties[prop];
+ props.push(prop + ": " + VariablesView.getString(value, { concise: true }));
+ }
+
+ return result + " {" + props.join(", ") + "}";
+ }, // DOMEvent
+
+ DOMNode: function (aGrip, {concise}) {
+ let {preview} = aGrip;
+
+ switch (preview.nodeType) {
+ case nodeConstants.DOCUMENT_NODE: {
+ let result = aGrip.class;
+ if (preview.location) {
+ result += ` \u2192 ${getSourceNames(preview.location)[concise ? "short" : "long"]}`;
+ }
+
+ return result;
+ }
+
+ case nodeConstants.ATTRIBUTE_NODE: {
+ let value = VariablesView.getString(preview.value, { noStringQuotes: true });
+ return preview.nodeName + '="' + escapeHTML(value) + '"';
+ }
+
+ case nodeConstants.TEXT_NODE:
+ return preview.nodeName + " " +
+ VariablesView.getString(preview.textContent);
+
+ case nodeConstants.COMMENT_NODE: {
+ let comment = VariablesView.getString(preview.textContent,
+ { noStringQuotes: true });
+ return "<!--" + comment + "-->";
+ }
+
+ case nodeConstants.DOCUMENT_FRAGMENT_NODE: {
+ if (concise || !preview.childNodes) {
+ return aGrip.class + "[" + preview.childNodesLength + "]";
+ }
+ let nodes = [];
+ for (let node of preview.childNodes) {
+ nodes.push(VariablesView.getString(node));
+ }
+ if (nodes.length < preview.childNodesLength) {
+ let n = preview.childNodesLength - nodes.length;
+ nodes.push(VariablesView.stringifiers._getNMoreString(n));
+ }
+ return aGrip.class + " [" + nodes.join(", ") + "]";
+ }
+
+ case nodeConstants.ELEMENT_NODE: {
+ let attrs = preview.attributes;
+ if (!concise) {
+ let n = 0, result = "<" + preview.nodeName;
+ for (let name in attrs) {
+ let value = VariablesView.getString(attrs[name],
+ { noStringQuotes: true });
+ result += " " + name + '="' + escapeHTML(value) + '"';
+ n++;
+ }
+ if (preview.attributesLength > n) {
+ result += " " + ELLIPSIS;
+ }
+ return result + ">";
+ }
+
+ let result = "<" + preview.nodeName;
+ if (attrs.id) {
+ result += "#" + attrs.id;
+ }
+
+ if (attrs.class) {
+ result += "." + attrs.class.trim().replace(/\s+/, ".");
+ }
+ return result + ">";
+ }
+
+ default:
+ return null;
+ }
+ }, // DOMNode
+}; // VariablesView.stringifiers.byObjectKind
+
+
+/**
+ * Get the "N more…" formatted string, given an N. This is used for displaying
+ * how many elements are not displayed in an object preview (eg. an array).
+ *
+ * @private
+ * @param number aNumber
+ * @return string
+ */
+VariablesView.stringifiers._getNMoreString = function (aNumber) {
+ let str = L10N.getStr("variablesViewMoreObjects");
+ return PluralForm.get(aNumber, str).replace("#1", aNumber);
+};
+
+/**
+ * Returns a custom class style for a grip.
+ *
+ * @param any aGrip
+ * @see Variable.setGrip
+ * @return string
+ * The custom class style.
+ */
+VariablesView.getClass = function (aGrip) {
+ if (aGrip && typeof aGrip == "object") {
+ if (aGrip.preview) {
+ switch (aGrip.preview.kind) {
+ case "DOMNode":
+ return "token-domnode";
+ }
+ }
+
+ switch (aGrip.type) {
+ case "undefined":
+ return "token-undefined";
+ case "null":
+ return "token-null";
+ case "Infinity":
+ case "-Infinity":
+ case "NaN":
+ case "-0":
+ return "token-number";
+ case "longString":
+ return "token-string";
+ }
+ }
+ switch (typeof aGrip) {
+ case "string":
+ return "token-string";
+ case "boolean":
+ return "token-boolean";
+ case "number":
+ return "token-number";
+ default:
+ return "token-other";
+ }
+};
+
+/**
+ * A monotonically-increasing counter, that guarantees the uniqueness of scope,
+ * variables and properties ids.
+ *
+ * @param string aName
+ * An optional string to prefix the id with.
+ * @return number
+ * A unique id.
+ */
+var generateId = (function () {
+ let count = 0;
+ return function (aName = "") {
+ return aName.toLowerCase().trim().replace(/\s+/g, "-") + (++count);
+ };
+})();
+
+/**
+ * Serialize a string to JSON. The result can be inserted in a string evaluated by `eval`.
+ *
+ * @param string aString
+ * The string to be escaped. If undefined, the function returns the empty string.
+ * @return string
+ */
+function escapeString(aString) {
+ return JSON.stringify(aString) || "";
+}
+
+/**
+ * Escape some HTML special characters. We do not need full HTML serialization
+ * here, we just want to make strings safe to display in HTML attributes, for
+ * the stringifiers.
+ *
+ * @param string aString
+ * @return string
+ */
+function escapeHTML(aString) {
+ return aString.replace(/&/g, "&amp;")
+ .replace(/"/g, "&quot;")
+ .replace(/</g, "&lt;")
+ .replace(/>/g, "&gt;");
+}
+
+
+/**
+ * An Editable encapsulates the UI of an edit box that overlays a label,
+ * allowing the user to edit the value.
+ *
+ * @param Variable aVariable
+ * The Variable or Property to make editable.
+ * @param object aOptions
+ * - onSave
+ * The callback to call with the value when editing is complete.
+ * - onCleanup
+ * The callback to call when the editable is removed for any reason.
+ */
+function Editable(aVariable, aOptions) {
+ this._variable = aVariable;
+ this._onSave = aOptions.onSave;
+ this._onCleanup = aOptions.onCleanup;
+}
+
+Editable.create = function (aVariable, aOptions, aEvent) {
+ let editable = new this(aVariable, aOptions);
+ editable.activate(aEvent);
+ return editable;
+};
+
+Editable.prototype = {
+ /**
+ * The class name for targeting this Editable type's label element. Overridden
+ * by inheriting classes.
+ */
+ className: null,
+
+ /**
+ * Boolean indicating whether this Editable should activate. Overridden by
+ * inheriting classes.
+ */
+ shouldActivate: null,
+
+ /**
+ * The label element for this Editable. Overridden by inheriting classes.
+ */
+ label: null,
+
+ /**
+ * Activate this editable by replacing the input box it overlays and
+ * initialize the handlers.
+ *
+ * @param Event e [optional]
+ * Optionally, the Event object that was used to activate the Editable.
+ */
+ activate: function (e) {
+ if (!this.shouldActivate) {
+ this._onCleanup && this._onCleanup();
+ return;
+ }
+
+ let { label } = this;
+ let initialString = label.getAttribute("value");
+
+ if (e) {
+ e.preventDefault();
+ e.stopPropagation();
+ }
+
+ // Create a texbox input element which will be shown in the current
+ // element's specified label location.
+ let input = this._input = this._variable.document.createElement("textbox");
+ input.className = "plain " + this.className;
+ input.setAttribute("value", initialString);
+ input.setAttribute("flex", "1");
+
+ // Replace the specified label with a textbox input element.
+ label.parentNode.replaceChild(input, label);
+ this._variable._variablesView.boxObject.ensureElementIsVisible(input);
+ input.select();
+
+ // When the value is a string (displayed as "value"), then we probably want
+ // to change it to another string in the textbox, so to avoid typing the ""
+ // again, tackle with the selection bounds just a bit.
+ if (initialString.match(/^".+"$/)) {
+ input.selectionEnd--;
+ input.selectionStart++;
+ }
+
+ this._onKeypress = this._onKeypress.bind(this);
+ this._onBlur = this._onBlur.bind(this);
+ input.addEventListener("keypress", this._onKeypress);
+ input.addEventListener("blur", this._onBlur);
+
+ this._prevExpandable = this._variable.twisty;
+ this._prevExpanded = this._variable.expanded;
+ this._variable.collapse();
+ this._variable.hideArrow();
+ this._variable.locked = true;
+ this._variable.editing = true;
+ },
+
+ /**
+ * Remove the input box and restore the Variable or Property to its previous
+ * state.
+ */
+ deactivate: function () {
+ this._input.removeEventListener("keypress", this._onKeypress);
+ this._input.removeEventListener("blur", this.deactivate);
+ this._input.parentNode.replaceChild(this.label, this._input);
+ this._input = null;
+
+ let { boxObject } = this._variable._variablesView;
+ boxObject.scrollBy(-this._variable._target, 0);
+ this._variable.locked = false;
+ this._variable.twisty = this._prevExpandable;
+ this._variable.expanded = this._prevExpanded;
+ this._variable.editing = false;
+ this._onCleanup && this._onCleanup();
+ },
+
+ /**
+ * Save the current value and deactivate the Editable.
+ */
+ _save: function () {
+ let initial = this.label.getAttribute("value");
+ let current = this._input.value.trim();
+ this.deactivate();
+ if (initial != current) {
+ this._onSave(current);
+ }
+ },
+
+ /**
+ * Called when tab is pressed, allowing subclasses to link different
+ * behavior to tabbing if desired.
+ */
+ _next: function () {
+ this._save();
+ },
+
+ /**
+ * Called when escape is pressed, indicating a cancelling of editing without
+ * saving.
+ */
+ _reset: function () {
+ this.deactivate();
+ this._variable.focus();
+ },
+
+ /**
+ * Event handler for when the input loses focus.
+ */
+ _onBlur: function () {
+ this.deactivate();
+ },
+
+ /**
+ * Event handler for when the input receives a key press.
+ */
+ _onKeypress: function (e) {
+ e.stopPropagation();
+
+ switch (e.keyCode) {
+ case KeyCodes.DOM_VK_TAB:
+ this._next();
+ break;
+ case KeyCodes.DOM_VK_RETURN:
+ this._save();
+ break;
+ case KeyCodes.DOM_VK_ESCAPE:
+ this._reset();
+ break;
+ }
+ },
+};
+
+
+/**
+ * An Editable specific to editing the name of a Variable or Property.
+ */
+function EditableName(aVariable, aOptions) {
+ Editable.call(this, aVariable, aOptions);
+}
+
+EditableName.create = Editable.create;
+
+EditableName.prototype = Heritage.extend(Editable.prototype, {
+ className: "element-name-input",
+
+ get label() {
+ return this._variable._name;
+ },
+
+ get shouldActivate() {
+ return !!this._variable.ownerView.switch;
+ },
+});
+
+
+/**
+ * An Editable specific to editing the value of a Variable or Property.
+ */
+function EditableValue(aVariable, aOptions) {
+ Editable.call(this, aVariable, aOptions);
+}
+
+EditableValue.create = Editable.create;
+
+EditableValue.prototype = Heritage.extend(Editable.prototype, {
+ className: "element-value-input",
+
+ get label() {
+ return this._variable._valueLabel;
+ },
+
+ get shouldActivate() {
+ return !!this._variable.ownerView.eval;
+ },
+});
+
+
+/**
+ * An Editable specific to editing the key and value of a new property.
+ */
+function EditableNameAndValue(aVariable, aOptions) {
+ EditableName.call(this, aVariable, aOptions);
+}
+
+EditableNameAndValue.create = Editable.create;
+
+EditableNameAndValue.prototype = Heritage.extend(EditableName.prototype, {
+ _reset: function (e) {
+ // Hide the Variable or Property if the user presses escape.
+ this._variable.remove();
+ this.deactivate();
+ },
+
+ _next: function (e) {
+ // Override _next so as to set both key and value at the same time.
+ let key = this._input.value;
+ this.label.setAttribute("value", key);
+
+ let valueEditable = EditableValue.create(this._variable, {
+ onSave: aValue => {
+ this._onSave([key, aValue]);
+ }
+ });
+ valueEditable._reset = () => {
+ this._variable.remove();
+ valueEditable.deactivate();
+ };
+ },
+
+ _save: function (e) {
+ // Both _save and _next activate the value edit box.
+ this._next(e);
+ }
+});
diff --git a/devtools/client/shared/widgets/VariablesView.xul b/devtools/client/shared/widgets/VariablesView.xul
new file mode 100644
index 000000000..fe8bb13ec
--- /dev/null
+++ b/devtools/client/shared/widgets/VariablesView.xul
@@ -0,0 +1,18 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+<?xml-stylesheet href="chrome://global/skin/global.css"?>
+<?xml-stylesheet href="chrome://devtools/content/shared/widgets/widgets.css" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/skin/widgets.css" type="text/css"?>
+<!DOCTYPE window [
+ <!ENTITY % viewDTD SYSTEM "chrome://devtools/locale/VariablesView.dtd">
+ %viewDTD;
+]>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="&PropertiesViewWindowTitle;">
+
+ <script type="application/javascript;version=1.8"
+ src="chrome://devtools/content/shared/theme-switching.js"/>
+ <vbox id="variables" flex="1"/>
+</window>
diff --git a/devtools/client/shared/widgets/VariablesViewController.jsm b/devtools/client/shared/widgets/VariablesViewController.jsm
new file mode 100644
index 000000000..5413ce1bf
--- /dev/null
+++ b/devtools/client/shared/widgets/VariablesViewController.jsm
@@ -0,0 +1,858 @@
+/* -*- 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 { utils: Cu } = Components;
+
+var {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+var {XPCOMUtils} = require("resource://gre/modules/XPCOMUtils.jsm");
+var {VariablesView} = require("resource://devtools/client/shared/widgets/VariablesView.jsm");
+var Services = require("Services");
+var promise = require("promise");
+var defer = require("devtools/shared/defer");
+var {LocalizationHelper, ELLIPSIS} = require("devtools/shared/l10n");
+
+Object.defineProperty(this, "WebConsoleUtils", {
+ get: function () {
+ return require("devtools/client/webconsole/utils").Utils;
+ },
+ configurable: true,
+ enumerable: true
+});
+
+XPCOMUtils.defineLazyGetter(this, "VARIABLES_SORTING_ENABLED", () =>
+ Services.prefs.getBoolPref("devtools.debugger.ui.variables-sorting-enabled")
+);
+
+XPCOMUtils.defineLazyModuleGetter(this, "console",
+ "resource://gre/modules/Console.jsm");
+
+const MAX_LONG_STRING_LENGTH = 200000;
+const MAX_PROPERTY_ITEMS = 2000;
+const DBG_STRINGS_URI = "devtools/client/locales/debugger.properties";
+
+this.EXPORTED_SYMBOLS = ["VariablesViewController", "StackFrameUtils"];
+
+/**
+ * Localization convenience methods.
+ */
+var L10N = new LocalizationHelper(DBG_STRINGS_URI);
+
+/**
+ * Controller for a VariablesView that handles interfacing with the debugger
+ * protocol. Is able to populate scopes and variables via the protocol as well
+ * as manage actor lifespans.
+ *
+ * @param VariablesView aView
+ * The view to attach to.
+ * @param object aOptions [optional]
+ * Options for configuring the controller. Supported options:
+ * - getObjectClient: @see this._setClientGetters
+ * - getLongStringClient: @see this._setClientGetters
+ * - getEnvironmentClient: @see this._setClientGetters
+ * - releaseActor: @see this._setClientGetters
+ * - overrideValueEvalMacro: @see _setEvaluationMacros
+ * - getterOrSetterEvalMacro: @see _setEvaluationMacros
+ * - simpleValueEvalMacro: @see _setEvaluationMacros
+ */
+function VariablesViewController(aView, aOptions = {}) {
+ this.addExpander = this.addExpander.bind(this);
+
+ this._setClientGetters(aOptions);
+ this._setEvaluationMacros(aOptions);
+
+ this._actors = new Set();
+ this.view = aView;
+ this.view.controller = this;
+}
+this.VariablesViewController = VariablesViewController;
+
+VariablesViewController.prototype = {
+ /**
+ * The default getter/setter evaluation macro.
+ */
+ _getterOrSetterEvalMacro: VariablesView.getterOrSetterEvalMacro,
+
+ /**
+ * The default override value evaluation macro.
+ */
+ _overrideValueEvalMacro: VariablesView.overrideValueEvalMacro,
+
+ /**
+ * The default simple value evaluation macro.
+ */
+ _simpleValueEvalMacro: VariablesView.simpleValueEvalMacro,
+
+ /**
+ * Set the functions used to retrieve debugger client grips.
+ *
+ * @param object aOptions
+ * Options for getting the client grips. Supported options:
+ * - getObjectClient: callback for creating an object grip client
+ * - getLongStringClient: callback for creating a long string grip client
+ * - getEnvironmentClient: callback for creating an environment client
+ * - releaseActor: callback for releasing an actor when it's no longer needed
+ */
+ _setClientGetters: function (aOptions) {
+ if (aOptions.getObjectClient) {
+ this._getObjectClient = aOptions.getObjectClient;
+ }
+ if (aOptions.getLongStringClient) {
+ this._getLongStringClient = aOptions.getLongStringClient;
+ }
+ if (aOptions.getEnvironmentClient) {
+ this._getEnvironmentClient = aOptions.getEnvironmentClient;
+ }
+ if (aOptions.releaseActor) {
+ this._releaseActor = aOptions.releaseActor;
+ }
+ },
+
+ /**
+ * Sets the functions used when evaluating strings in the variables view.
+ *
+ * @param object aOptions
+ * Options for configuring the macros. Supported options:
+ * - overrideValueEvalMacro: callback for creating an overriding eval macro
+ * - getterOrSetterEvalMacro: callback for creating a getter/setter eval macro
+ * - simpleValueEvalMacro: callback for creating a simple value eval macro
+ */
+ _setEvaluationMacros: function (aOptions) {
+ if (aOptions.overrideValueEvalMacro) {
+ this._overrideValueEvalMacro = aOptions.overrideValueEvalMacro;
+ }
+ if (aOptions.getterOrSetterEvalMacro) {
+ this._getterOrSetterEvalMacro = aOptions.getterOrSetterEvalMacro;
+ }
+ if (aOptions.simpleValueEvalMacro) {
+ this._simpleValueEvalMacro = aOptions.simpleValueEvalMacro;
+ }
+ },
+
+ /**
+ * Populate a long string into a target using a grip.
+ *
+ * @param Variable aTarget
+ * The target Variable/Property to put the retrieved string into.
+ * @param LongStringActor aGrip
+ * The long string grip that use to retrieve the full string.
+ * @return Promise
+ * The promise that will be resolved when the string is retrieved.
+ */
+ _populateFromLongString: function (aTarget, aGrip) {
+ let deferred = defer();
+
+ let from = aGrip.initial.length;
+ let to = Math.min(aGrip.length, MAX_LONG_STRING_LENGTH);
+
+ this._getLongStringClient(aGrip).substring(from, to, aResponse => {
+ // Stop tracking the actor because it's no longer needed.
+ this.releaseActor(aGrip);
+
+ // Replace the preview with the full string and make it non-expandable.
+ aTarget.onexpand = null;
+ aTarget.setGrip(aGrip.initial + aResponse.substring);
+ aTarget.hideArrow();
+
+ deferred.resolve();
+ });
+
+ return deferred.promise;
+ },
+
+ /**
+ * Adds pseudo items in case there is too many properties to display.
+ * Each item can expand into property slices.
+ *
+ * @param Scope aTarget
+ * The Scope where the properties will be placed into.
+ * @param object aGrip
+ * The property iterator grip.
+ */
+ _populatePropertySlices: function (aTarget, aGrip) {
+ if (aGrip.count < MAX_PROPERTY_ITEMS) {
+ return this._populateFromPropertyIterator(aTarget, aGrip);
+ }
+
+ // Divide the keys into quarters.
+ let items = Math.ceil(aGrip.count / 4);
+ let iterator = aGrip.propertyIterator;
+ let promises = [];
+ for (let i = 0; i < 4; i++) {
+ let start = aGrip.start + i * items;
+ let count = i != 3 ? items : aGrip.count - i * items;
+
+ // Create a new kind of grip, with additional fields to define the slice
+ let sliceGrip = {
+ type: "property-iterator",
+ propertyIterator: iterator,
+ start: start,
+ count: count
+ };
+
+ // Query the name of the first and last items for this slice
+ let deferred = defer();
+ iterator.names([start, start + count - 1], ({ names }) => {
+ let label = "[" + names[0] + ELLIPSIS + names[1] + "]";
+ let item = aTarget.addItem(label, {}, { internalItem: true });
+ item.showArrow();
+ this.addExpander(item, sliceGrip);
+ deferred.resolve();
+ });
+ promises.push(deferred.promise);
+ }
+
+ return promise.all(promises);
+ },
+
+ /**
+ * Adds a property slice for a Variable in the view using the already
+ * property iterator
+ *
+ * @param Scope aTarget
+ * The Scope where the properties will be placed into.
+ * @param object aGrip
+ * The property iterator grip.
+ */
+ _populateFromPropertyIterator: function (aTarget, aGrip) {
+ if (aGrip.count >= MAX_PROPERTY_ITEMS) {
+ // We already started to split, but there is still too many properties, split again.
+ return this._populatePropertySlices(aTarget, aGrip);
+ }
+ // We started slicing properties, and the slice is now small enough to be displayed
+ let deferred = defer();
+ aGrip.propertyIterator.slice(aGrip.start, aGrip.count,
+ ({ ownProperties }) => {
+ // Add all the variable properties.
+ if (Object.keys(ownProperties).length > 0) {
+ aTarget.addItems(ownProperties, {
+ sorted: true,
+ // Expansion handlers must be set after the properties are added.
+ callback: this.addExpander
+ });
+ }
+ deferred.resolve();
+ });
+ return deferred.promise;
+ },
+
+ /**
+ * Adds the properties for a Variable in the view using a new feature in FF40+
+ * that allows iteration over properties in slices.
+ *
+ * @param Scope aTarget
+ * The Scope where the properties will be placed into.
+ * @param object aGrip
+ * The grip to use to populate the target.
+ * @param string aQuery [optional]
+ * The query string used to fetch only a subset of properties
+ */
+ _populateFromObjectWithIterator: function (aTarget, aGrip, aQuery) {
+ // FF40+ starts exposing `ownPropertyLength` on ObjectActor's grip,
+ // as well as `enumProperties` request.
+ let deferred = defer();
+ let objectClient = this._getObjectClient(aGrip);
+ let isArray = aGrip.preview && aGrip.preview.kind === "ArrayLike";
+ if (isArray) {
+ // First enumerate array items, e.g. properties from `0` to `array.length`.
+ let options = {
+ ignoreNonIndexedProperties: true,
+ query: aQuery
+ };
+ objectClient.enumProperties(options, ({ iterator }) => {
+ let sliceGrip = {
+ type: "property-iterator",
+ propertyIterator: iterator,
+ start: 0,
+ count: iterator.count
+ };
+ this._populatePropertySlices(aTarget, sliceGrip)
+ .then(() => {
+ // Then enumerate the rest of the properties, like length, buffer, etc.
+ let options = {
+ ignoreIndexedProperties: true,
+ sort: true,
+ query: aQuery
+ };
+ objectClient.enumProperties(options, ({ iterator }) => {
+ let sliceGrip = {
+ type: "property-iterator",
+ propertyIterator: iterator,
+ start: 0,
+ count: iterator.count
+ };
+ deferred.resolve(this._populatePropertySlices(aTarget, sliceGrip));
+ });
+ });
+ });
+ } else {
+ // For objects, we just enumerate all the properties sorted by name.
+ objectClient.enumProperties({ sort: true, query: aQuery }, ({ iterator }) => {
+ let sliceGrip = {
+ type: "property-iterator",
+ propertyIterator: iterator,
+ start: 0,
+ count: iterator.count
+ };
+ deferred.resolve(this._populatePropertySlices(aTarget, sliceGrip));
+ });
+
+ }
+ return deferred.promise;
+ },
+
+ /**
+ * Adds the given prototype in the view.
+ *
+ * @param Scope aTarget
+ * The Scope where the properties will be placed into.
+ * @param object aProtype
+ * The prototype grip.
+ */
+ _populateObjectPrototype: function (aTarget, aPrototype) {
+ // Add the variable's __proto__.
+ if (aPrototype && aPrototype.type != "null") {
+ let proto = aTarget.addItem("__proto__", { value: aPrototype });
+ this.addExpander(proto, aPrototype);
+ }
+ },
+
+ /**
+ * Adds properties to a Scope, Variable, or Property in the view. Triggered
+ * when a scope is expanded or certain variables are hovered.
+ *
+ * @param Scope aTarget
+ * The Scope where the properties will be placed into.
+ * @param object aGrip
+ * The grip to use to populate the target.
+ */
+ _populateFromObject: function (aTarget, aGrip) {
+ if (aGrip.class === "Proxy") {
+ this.addExpander(
+ aTarget.addItem("<target>", { value: aGrip.proxyTarget }, { internalItem: true }),
+ aGrip.proxyTarget);
+ this.addExpander(
+ aTarget.addItem("<handler>", { value: aGrip.proxyHandler }, { internalItem: true }),
+ aGrip.proxyHandler);
+
+ // Refuse to play the proxy's stupid game and return immediately
+ let deferred = defer();
+ deferred.resolve();
+ return deferred.promise;
+ }
+
+ if (aGrip.class === "Promise" && aGrip.promiseState) {
+ const { state, value, reason } = aGrip.promiseState;
+ aTarget.addItem("<state>", { value: state }, { internalItem: true });
+ if (state === "fulfilled") {
+ this.addExpander(
+ aTarget.addItem("<value>", { value }, { internalItem: true }),
+ value);
+ } else if (state === "rejected") {
+ this.addExpander(
+ aTarget.addItem("<reason>", { value: reason }, { internalItem: true }),
+ reason);
+ }
+ } else if (["Map", "WeakMap", "Set", "WeakSet"].includes(aGrip.class)) {
+ let entriesList = aTarget.addItem("<entries>", {}, { internalItem: true });
+ entriesList.showArrow();
+ this.addExpander(entriesList, {
+ type: "entries-list",
+ obj: aGrip
+ });
+ }
+
+ // Fetch properties by slices if there is too many in order to prevent UI freeze.
+ if ("ownPropertyLength" in aGrip && aGrip.ownPropertyLength >= MAX_PROPERTY_ITEMS) {
+ return this._populateFromObjectWithIterator(aTarget, aGrip)
+ .then(() => {
+ let deferred = defer();
+ let objectClient = this._getObjectClient(aGrip);
+ objectClient.getPrototype(({ prototype }) => {
+ this._populateObjectPrototype(aTarget, prototype);
+ deferred.resolve();
+ });
+ return deferred.promise;
+ });
+ }
+
+ return this._populateProperties(aTarget, aGrip);
+ },
+
+ _populateProperties: function (aTarget, aGrip, aOptions) {
+ let deferred = defer();
+
+ let objectClient = this._getObjectClient(aGrip);
+ objectClient.getPrototypeAndProperties(aResponse => {
+ let ownProperties = aResponse.ownProperties || {};
+ let prototype = aResponse.prototype || null;
+ // 'safeGetterValues' is new and isn't necessary defined on old actors.
+ let safeGetterValues = aResponse.safeGetterValues || {};
+ let sortable = VariablesView.isSortable(aGrip.class);
+
+ // Merge the safe getter values into one object such that we can use it
+ // in VariablesView.
+ for (let name of Object.keys(safeGetterValues)) {
+ if (name in ownProperties) {
+ let { getterValue, getterPrototypeLevel } = safeGetterValues[name];
+ ownProperties[name].getterValue = getterValue;
+ ownProperties[name].getterPrototypeLevel = getterPrototypeLevel;
+ } else {
+ ownProperties[name] = safeGetterValues[name];
+ }
+ }
+
+ // Add all the variable properties.
+ aTarget.addItems(ownProperties, {
+ // Not all variables need to force sorted properties.
+ sorted: sortable,
+ // Expansion handlers must be set after the properties are added.
+ callback: this.addExpander
+ });
+
+ // Add the variable's __proto__.
+ this._populateObjectPrototype(aTarget, prototype);
+
+ // If the object is a function we need to fetch its scope chain
+ // to show them as closures for the respective function.
+ if (aGrip.class == "Function") {
+ objectClient.getScope(aResponse => {
+ if (aResponse.error) {
+ // This function is bound to a built-in object or it's not present
+ // in the current scope chain. Not necessarily an actual error,
+ // it just means that there's no closure for the function.
+ console.warn(aResponse.error + ": " + aResponse.message);
+ return void deferred.resolve();
+ }
+ this._populateWithClosure(aTarget, aResponse.scope).then(deferred.resolve);
+ });
+ } else {
+ deferred.resolve();
+ }
+ });
+
+ return deferred.promise;
+ },
+
+ /**
+ * Adds the scope chain elements (closures) of a function variable.
+ *
+ * @param Variable aTarget
+ * The variable where the properties will be placed into.
+ * @param Scope aScope
+ * The lexical environment form as specified in the protocol.
+ */
+ _populateWithClosure: function (aTarget, aScope) {
+ let objectScopes = [];
+ let environment = aScope;
+ let funcScope = aTarget.addItem("<Closure>");
+ funcScope.target.setAttribute("scope", "");
+ funcScope.showArrow();
+
+ do {
+ // Create a scope to contain all the inspected variables.
+ let label = StackFrameUtils.getScopeLabel(environment);
+
+ // Block scopes may have the same label, so make addItem allow duplicates.
+ let closure = funcScope.addItem(label, undefined, {relaxed: true});
+ closure.target.setAttribute("scope", "");
+ closure.showArrow();
+
+ // Add nodes for every argument and every other variable in scope.
+ if (environment.bindings) {
+ this._populateWithEnvironmentBindings(closure, environment.bindings);
+ } else {
+ let deferred = defer();
+ objectScopes.push(deferred.promise);
+ this._getEnvironmentClient(environment).getBindings(response => {
+ this._populateWithEnvironmentBindings(closure, response.bindings);
+ deferred.resolve();
+ });
+ }
+ } while ((environment = environment.parent));
+
+ return promise.all(objectScopes).then(() => {
+ // Signal that scopes have been fetched.
+ this.view.emit("fetched", "scopes", funcScope);
+ });
+ },
+
+ /**
+ * Adds nodes for every specified binding to the closure node.
+ *
+ * @param Variable aTarget
+ * The variable where the bindings will be placed into.
+ * @param object aBindings
+ * The bindings form as specified in the protocol.
+ */
+ _populateWithEnvironmentBindings: function (aTarget, aBindings) {
+ // Add nodes for every argument in the scope.
+ aTarget.addItems(aBindings.arguments.reduce((accumulator, arg) => {
+ let name = Object.getOwnPropertyNames(arg)[0];
+ let descriptor = arg[name];
+ accumulator[name] = descriptor;
+ return accumulator;
+ }, {}), {
+ // Arguments aren't sorted.
+ sorted: false,
+ // Expansion handlers must be set after the properties are added.
+ callback: this.addExpander
+ });
+
+ // Add nodes for every other variable in the scope.
+ aTarget.addItems(aBindings.variables, {
+ // Not all variables need to force sorted properties.
+ sorted: VARIABLES_SORTING_ENABLED,
+ // Expansion handlers must be set after the properties are added.
+ callback: this.addExpander
+ });
+ },
+
+ _populateFromEntries: function (target, grip) {
+ let objGrip = grip.obj;
+ let objectClient = this._getObjectClient(objGrip);
+
+ return new promise((resolve, reject) => {
+ objectClient.enumEntries((response) => {
+ if (response.error) {
+ // Older server might not support the enumEntries method
+ console.warn(response.error + ": " + response.message);
+ resolve();
+ } else {
+ let sliceGrip = {
+ type: "property-iterator",
+ propertyIterator: response.iterator,
+ start: 0,
+ count: response.iterator.count
+ };
+
+ resolve(this._populatePropertySlices(target, sliceGrip));
+ }
+ });
+ });
+ },
+
+ /**
+ * Adds an 'onexpand' callback for a variable, lazily handling
+ * the addition of new properties.
+ *
+ * @param Variable aTarget
+ * The variable where the properties will be placed into.
+ * @param any aSource
+ * The source to use to populate the target.
+ */
+ addExpander: function (aTarget, aSource) {
+ // Attach evaluation macros as necessary.
+ if (aTarget.getter || aTarget.setter) {
+ aTarget.evaluationMacro = this._overrideValueEvalMacro;
+ let getter = aTarget.get("get");
+ if (getter) {
+ getter.evaluationMacro = this._getterOrSetterEvalMacro;
+ }
+ let setter = aTarget.get("set");
+ if (setter) {
+ setter.evaluationMacro = this._getterOrSetterEvalMacro;
+ }
+ } else {
+ aTarget.evaluationMacro = this._simpleValueEvalMacro;
+ }
+
+ // If the source is primitive then an expander is not needed.
+ if (VariablesView.isPrimitive({ value: aSource })) {
+ return;
+ }
+
+ // If the source is a long string then show the arrow.
+ if (WebConsoleUtils.isActorGrip(aSource) && aSource.type == "longString") {
+ aTarget.showArrow();
+ }
+
+ // Make sure that properties are always available on expansion.
+ aTarget.onexpand = () => this.populate(aTarget, aSource);
+
+ // Some variables are likely to contain a very large number of properties.
+ // It's a good idea to be prepared in case of an expansion.
+ if (aTarget.shouldPrefetch) {
+ aTarget.addEventListener("mouseover", aTarget.onexpand, false);
+ }
+
+ // Register all the actors that this controller now depends on.
+ for (let grip of [aTarget.value, aTarget.getter, aTarget.setter]) {
+ if (WebConsoleUtils.isActorGrip(grip)) {
+ this._actors.add(grip.actor);
+ }
+ }
+ },
+
+ /**
+ * Adds properties to a Scope, Variable, or Property in the view. Triggered
+ * when a scope is expanded or certain variables are hovered.
+ *
+ * This does not expand the target, it only populates it.
+ *
+ * @param Scope aTarget
+ * The Scope to be expanded.
+ * @param object aSource
+ * The source to use to populate the target.
+ * @return Promise
+ * The promise that is resolved once the target has been expanded.
+ */
+ populate: function (aTarget, aSource) {
+ // Fetch the variables only once.
+ if (aTarget._fetched) {
+ return aTarget._fetched;
+ }
+ // Make sure the source grip is available.
+ if (!aSource) {
+ return promise.reject(new Error("No actor grip was given for the variable."));
+ }
+
+ let deferred = defer();
+ aTarget._fetched = deferred.promise;
+
+ if (aSource.type === "property-iterator") {
+ return this._populateFromPropertyIterator(aTarget, aSource);
+ }
+
+ if (aSource.type === "entries-list") {
+ return this._populateFromEntries(aTarget, aSource);
+ }
+
+ if (aSource.type === "mapEntry") {
+ aTarget.addItems({
+ key: { value: aSource.preview.key },
+ value: { value: aSource.preview.value }
+ }, {
+ callback: this.addExpander
+ });
+
+ return promise.resolve();
+ }
+
+ // If the target is a Variable or Property then we're fetching properties.
+ if (VariablesView.isVariable(aTarget)) {
+ this._populateFromObject(aTarget, aSource).then(() => {
+ // Signal that properties have been fetched.
+ this.view.emit("fetched", "properties", aTarget);
+ // Commit the hierarchy because new items were added.
+ this.view.commitHierarchy();
+ deferred.resolve();
+ });
+ return deferred.promise;
+ }
+
+ switch (aSource.type) {
+ case "longString":
+ this._populateFromLongString(aTarget, aSource).then(() => {
+ // Signal that a long string has been fetched.
+ this.view.emit("fetched", "longString", aTarget);
+ deferred.resolve();
+ });
+ break;
+ case "with":
+ case "object":
+ this._populateFromObject(aTarget, aSource.object).then(() => {
+ // Signal that variables have been fetched.
+ this.view.emit("fetched", "variables", aTarget);
+ // Commit the hierarchy because new items were added.
+ this.view.commitHierarchy();
+ deferred.resolve();
+ });
+ break;
+ case "block":
+ case "function":
+ this._populateWithEnvironmentBindings(aTarget, aSource.bindings);
+ // No need to signal that variables have been fetched, since
+ // the scope arguments and variables are already attached to the
+ // environment bindings, so pausing the active thread is unnecessary.
+ // Commit the hierarchy because new items were added.
+ this.view.commitHierarchy();
+ deferred.resolve();
+ break;
+ default:
+ let error = "Unknown Debugger.Environment type: " + aSource.type;
+ console.error(error);
+ deferred.reject(error);
+ }
+
+ return deferred.promise;
+ },
+
+ /**
+ * Indicates to the view if the targeted actor supports properties search
+ *
+ * @return boolean True, if the actor supports enumProperty request
+ */
+ supportsSearch: function () {
+ // FF40+ starts exposing ownPropertyLength on object actor's grip
+ // as well as enumProperty which allows to query a subset of properties.
+ return this.objectActor && ("ownPropertyLength" in this.objectActor);
+ },
+
+ /**
+ * Try to use the actor to perform an attribute search.
+ *
+ * @param Scope aScope
+ * The Scope instance to populate with properties
+ * @param string aToken
+ * The query string
+ */
+ performSearch: function (aScope, aToken) {
+ this._populateFromObjectWithIterator(aScope, this.objectActor, aToken)
+ .then(() => {
+ this.view.emit("fetched", "search", aScope);
+ });
+ },
+
+ /**
+ * Release an actor from the controller.
+ *
+ * @param object aActor
+ * The actor to release.
+ */
+ releaseActor: function (aActor) {
+ if (this._releaseActor) {
+ this._releaseActor(aActor);
+ }
+ this._actors.delete(aActor);
+ },
+
+ /**
+ * Release all the actors referenced by the controller, optionally filtered.
+ *
+ * @param function aFilter [optional]
+ * Callback to filter which actors are released.
+ */
+ releaseActors: function (aFilter) {
+ for (let actor of this._actors) {
+ if (!aFilter || aFilter(actor)) {
+ this.releaseActor(actor);
+ }
+ }
+ },
+
+ /**
+ * Helper function for setting up a single Scope with a single Variable
+ * contained within it.
+ *
+ * This function will empty the variables view.
+ *
+ * @param object options
+ * Options for the contents of the view:
+ * - objectActor: the grip of the new ObjectActor to show.
+ * - rawObject: the raw object to show.
+ * - label: the label for the inspected object.
+ * @param object configuration
+ * Additional options for the controller:
+ * - overrideValueEvalMacro: @see _setEvaluationMacros
+ * - getterOrSetterEvalMacro: @see _setEvaluationMacros
+ * - simpleValueEvalMacro: @see _setEvaluationMacros
+ * @return Object
+ * - variable: the created Variable.
+ * - expanded: the Promise that resolves when the variable expands.
+ */
+ setSingleVariable: function (options, configuration = {}) {
+ this._setEvaluationMacros(configuration);
+ this.view.empty();
+
+ let scope = this.view.addScope(options.label);
+ scope.expanded = true; // Expand the scope by default.
+ scope.locked = true; // Prevent collapsing the scope.
+
+ let variable = scope.addItem(undefined, { enumerable: true });
+ let populated;
+
+ if (options.objectActor) {
+ // Save objectActor for properties filtering
+ this.objectActor = options.objectActor;
+ if (VariablesView.isPrimitive({ value: this.objectActor })) {
+ populated = promise.resolve();
+ } else {
+ populated = this.populate(variable, options.objectActor);
+ variable.expand();
+ }
+ } else if (options.rawObject) {
+ variable.populate(options.rawObject, { expanded: true });
+ populated = promise.resolve();
+ }
+
+ return { variable: variable, expanded: populated };
+ },
+};
+
+
+/**
+ * Attaches a VariablesViewController to a VariablesView if it doesn't already
+ * have one.
+ *
+ * @param VariablesView aView
+ * The view to attach to.
+ * @param object aOptions
+ * The options to use in creating the controller.
+ * @return VariablesViewController
+ */
+VariablesViewController.attach = function (aView, aOptions) {
+ if (aView.controller) {
+ return aView.controller;
+ }
+ return new VariablesViewController(aView, aOptions);
+};
+
+/**
+ * Utility functions for handling stackframes.
+ */
+var StackFrameUtils = this.StackFrameUtils = {
+ /**
+ * Create a textual representation for the specified stack frame
+ * to display in the stackframes container.
+ *
+ * @param object aFrame
+ * The stack frame to label.
+ */
+ getFrameTitle: function (aFrame) {
+ if (aFrame.type == "call") {
+ let c = aFrame.callee;
+ return (c.name || c.userDisplayName || c.displayName || "(anonymous)");
+ }
+ return "(" + aFrame.type + ")";
+ },
+
+ /**
+ * Constructs a scope label based on its environment.
+ *
+ * @param object aEnv
+ * The scope's environment.
+ * @return string
+ * The scope's label.
+ */
+ getScopeLabel: function (aEnv) {
+ let name = "";
+
+ // Name the outermost scope Global.
+ if (!aEnv.parent) {
+ name = L10N.getStr("globalScopeLabel");
+ }
+ // Otherwise construct the scope name.
+ else {
+ name = aEnv.type.charAt(0).toUpperCase() + aEnv.type.slice(1);
+ }
+
+ let label = L10N.getFormatStr("scopeLabel", name);
+ switch (aEnv.type) {
+ case "with":
+ case "object":
+ label += " [" + aEnv.object.class + "]";
+ break;
+ case "function":
+ let f = aEnv.function;
+ label += " [" +
+ (f.name || f.userDisplayName || f.displayName || "(anonymous)") +
+ "]";
+ break;
+ }
+ return label;
+ }
+};
diff --git a/devtools/client/shared/widgets/cubic-bezier.css b/devtools/client/shared/widgets/cubic-bezier.css
new file mode 100644
index 000000000..203fe336a
--- /dev/null
+++ b/devtools/client/shared/widgets/cubic-bezier.css
@@ -0,0 +1,216 @@
+/* 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/. */
+
+/* Based on Lea Verou www.cubic-bezier.com
+ See https://github.com/LeaVerou/cubic-bezier */
+
+.cubic-bezier-container {
+ display: flex;
+ width: 510px;
+ height: 370px;
+ flex-direction: row-reverse;
+ overflow: hidden;
+ padding: 5px;
+ box-sizing: border-box;
+}
+
+.display-wrap {
+ width: 50%;
+ height: 100%;
+ text-align: center;
+ overflow: hidden;
+}
+
+/* Coordinate Plane */
+
+.coordinate-plane {
+ width: 150px;
+ height: 370px;
+ margin: 0 auto;
+ position: relative;
+}
+
+.control-point {
+ position: absolute;
+ z-index: 1;
+ height: 10px;
+ width: 10px;
+ border: 0;
+ background: #666;
+ display: block;
+ margin: -5px 0 0 -5px;
+ outline: none;
+ border-radius: 5px;
+ padding: 0;
+ cursor: pointer;
+}
+
+.display-wrap {
+ background:
+ repeating-linear-gradient(0deg,
+ transparent,
+ var(--bezier-grid-color) 0,
+ var(--bezier-grid-color) 1px,
+ transparent 1px,
+ transparent 15px) no-repeat,
+ repeating-linear-gradient(90deg,
+ transparent,
+ var(--bezier-grid-color) 0,
+ var(--bezier-grid-color) 1px,
+ transparent 1px,
+ transparent 15px) no-repeat;
+ background-size: 100% 100%, 100% 100%;
+ background-position: -2px 5px, -2px 5px;
+
+ -moz-user-select: none;
+}
+
+canvas.curve {
+ background:
+ linear-gradient(-45deg,
+ transparent 49.7%,
+ var(--bezier-diagonal-color) 49.7%,
+ var(--bezier-diagonal-color) 50.3%,
+ transparent 50.3%) center no-repeat;
+ background-size: 100% 100%;
+ background-position: 0 0;
+}
+
+/* Timing Function Preview Widget */
+
+.timing-function-preview {
+ position: absolute;
+ bottom: 20px;
+ right: 45px;
+ width: 150px;
+}
+
+.timing-function-preview .scale {
+ position: absolute;
+ top: 6px;
+ left: 0;
+ z-index: 1;
+
+ width: 150px;
+ height: 1px;
+
+ background: #ccc;
+}
+
+.timing-function-preview .dot {
+ position: absolute;
+ top: 0;
+ left: -7px;
+ z-index: 2;
+
+ width: 10px;
+ height: 10px;
+
+ border-radius: 50%;
+ border: 2px solid white;
+ background: #4C9ED9;
+}
+
+/* Preset Widget */
+
+.preset-pane {
+ width: 50%;
+ height: 100%;
+ border-right: 1px solid var(--theme-splitter-color);
+ padding-right: 4px; /* Visual balance for the panel-arrowcontent border on the left */
+}
+
+#preset-categories {
+ display: flex;
+ width: 95%;
+ border: 1px solid var(--theme-splitter-color);
+ border-radius: 2px;
+ background-color: var(--theme-toolbar-background);
+ margin: 3px auto 0 auto;
+}
+
+#preset-categories .category:last-child {
+ border-right: none;
+}
+
+.category {
+ padding: 5px 0px;
+ width: 33.33%;
+ text-align: center;
+ text-transform: capitalize;
+ border-right: 1px solid var(--theme-splitter-color);
+ cursor: default;
+ color: var(--theme-body-color);
+ text-overflow: ellipsis;
+ overflow: hidden;
+}
+
+.category:hover {
+ background-color: var(--theme-tab-toolbar-background);
+}
+
+.active-category {
+ background-color: var(--theme-selection-background);
+ color: var(--theme-selection-color);
+}
+
+.active-category:hover {
+ background-color: var(--theme-selection-background);
+}
+
+#preset-container {
+ padding: 0px;
+ width: 100%;
+ height: 331px;
+ overflow-y: auto;
+}
+
+.preset-list {
+ display: none;
+ padding-top: 6px;
+}
+
+.active-preset-list {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: flex-start;
+}
+
+.preset {
+ cursor: pointer;
+ width: 33.33%;
+ margin: 5px 0px;
+ text-align: center;
+}
+
+.preset canvas {
+ display: block;
+ border: 1px solid var(--theme-splitter-color);
+ border-radius: 3px;
+ background-color: var(--theme-body-background);
+ margin: 0 auto;
+}
+
+.preset p {
+ font-size: 80%;
+ margin: 2px auto 0px auto;
+ color: var(--theme-body-color-alt);
+ text-transform: capitalize;
+ text-overflow: ellipsis;
+ overflow: hidden;
+}
+
+.active-preset p, .active-preset:hover p {
+ color: var(--theme-body-color);
+}
+
+.preset:hover canvas {
+ border-color: var(--theme-selection-background);
+}
+
+.active-preset canvas,
+.active-preset:hover canvas {
+ background-color: var(--theme-selection-background-semitransparent);
+ border-color: var(--theme-selection-background);
+}
diff --git a/devtools/client/shared/widgets/filter-widget.css b/devtools/client/shared/widgets/filter-widget.css
new file mode 100644
index 000000000..d015cb5b1
--- /dev/null
+++ b/devtools/client/shared/widgets/filter-widget.css
@@ -0,0 +1,238 @@
+/* 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/. */
+
+/* Main container: Displays the filters and presets in 2 columns */
+
+#filter-container {
+ width: 510px;
+ height: 200px;
+ display: flex;
+ position: relative;
+ padding: 5px;
+ box-sizing: border-box;
+ /* when opened in a xul:panel, a gray color is applied to text */
+ color: var(--theme-body-color);
+}
+
+#filter-container.dragging {
+ -moz-user-select: none;
+}
+
+.filters-list,
+.presets-list {
+ display: flex;
+ flex-direction: column;
+ box-sizing: border-box;
+}
+
+.filters-list {
+ /* Allow the filters list to take the full width when the presets list is
+ hidden */
+ flex-grow: 1;
+ padding: 0 6px;
+}
+
+.presets-list {
+ /* Make sure that when the presets list is shown, it has a fixed width */
+ width: 200px;
+ padding-left: 6px;
+ transition: width .1s;
+ flex-shrink: 0;
+ border-left: 1px solid var(--theme-splitter-color);
+}
+
+#filter-container:not(.show-presets) .presets-list {
+ width: 0;
+ border-left: none;
+ padding-left: 0;
+}
+
+#filter-container.show-presets .filters-list {
+ width: 300px;
+}
+
+/* The list of filters and list of presets should push their footers to the
+ bottom, so they can take as much space as there is */
+
+#filters,
+#presets {
+ flex-grow: 1;
+ /* Avoid pushing below the tooltip's area */
+ overflow-y: auto;
+}
+
+/* The filters and presets list both have footers displayed at the bottom.
+ These footers have some input (taking up as much space as possible) and an
+ add button next */
+
+.footer {
+ display: flex;
+ margin: 10px 3px;
+ align-items: center;
+}
+
+.footer :not(button) {
+ flex-grow: 1;
+ margin-right: 3px;
+}
+
+/* Styles for 1 filter function item */
+
+.filter,
+.filter-name,
+.filter-value {
+ display: flex;
+ align-items: center;
+}
+
+.filter {
+ margin: 5px 0;
+}
+
+.filter-name {
+ width: 120px;
+ margin-right: 10px;
+}
+
+.filter-name label {
+ -moz-user-select: none;
+ flex-grow: 1;
+}
+
+.filter-name label.devtools-draglabel {
+ cursor: ew-resize;
+}
+
+/* drag/drop handle */
+
+.filter-name i {
+ width: 10px;
+ height: 10px;
+ margin-right: 10px;
+ cursor: grab;
+ background: linear-gradient(to bottom,
+ currentColor 0,
+ currentcolor 1px,
+ transparent 1px,
+ transparent 2px);
+ background-repeat: repeat-y;
+ background-size: auto 4px;
+ background-position: 0 1px;
+}
+
+.filter-value {
+ min-width: 150px;
+ margin-right: 10px;
+ flex: 1;
+}
+
+.filter-value input {
+ flex-grow: 1;
+}
+
+/* Fix the size of inputs */
+/* Especially needed on Linux where input are bigger */
+input {
+ width: 8em;
+}
+
+.preset {
+ display: flex;
+ margin-bottom: 10px;
+ cursor: pointer;
+ padding: 3px 5px;
+
+ flex-direction: row;
+ flex-wrap: wrap;
+}
+
+.preset label,
+.preset span {
+ display: flex;
+ align-items: center;
+}
+
+.preset label {
+ flex: 1 0;
+ cursor: pointer;
+ color: var(--theme-body-color);
+}
+
+.preset:hover {
+ background: var(--theme-selection-background);
+}
+
+.preset:hover label, .preset:hover span {
+ color: var(--theme-selection-color);
+}
+
+.preset .remove-button {
+ order: 2;
+}
+
+.preset span {
+ flex: 2 100%;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: block;
+ order: 3;
+ color: var(--theme-body-color-alt);
+}
+
+.remove-button {
+ width: 16px;
+ height: 16px;
+ background: url(chrome://devtools/skin/images/close.svg);
+ background-size: cover;
+ font-size: 0;
+ border: none;
+ cursor: pointer;
+}
+
+.hidden {
+ display: none !important;
+}
+
+#filter-container .dragging {
+ position: relative;
+ z-index: 10;
+ cursor: grab;
+}
+
+/* message shown when there's no filter specified */
+#filter-container p {
+ text-align: center;
+ line-height: 20px;
+}
+
+.add,
+#toggle-presets {
+ background-size: cover;
+ border: none;
+ width: 16px;
+ height: 16px;
+ font-size: 0;
+ vertical-align: middle;
+ cursor: pointer;
+ margin: 0 5px;
+}
+
+.add {
+ background: url(chrome://devtools/skin/images/add.svg);
+}
+
+#toggle-presets {
+ background: url(chrome://devtools/skin/images/pseudo-class.svg);
+}
+
+.add,
+.remove-button,
+#toggle-presets {
+ filter: var(--icon-filter);
+}
+
+.show-presets #toggle-presets {
+ filter: url(chrome://devtools/skin/images/filters.svg#checked-icon-state);
+}
diff --git a/devtools/client/shared/widgets/graphs-frame.xhtml b/devtools/client/shared/widgets/graphs-frame.xhtml
new file mode 100644
index 000000000..8c6f45e03
--- /dev/null
+++ b/devtools/client/shared/widgets/graphs-frame.xhtml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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/. -->
+<!DOCTYPE html>
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <link rel="stylesheet" href="chrome://devtools/skin/widgets.css" ype="text/css"/>
+ <script type="application/javascript;version=1.8" src="chrome://devtools/content/shared/theme-switching.js"/>
+ <style>
+ body {
+ overflow: hidden;
+ margin: 0;
+ padding: 0;
+ font-size: 0;
+ }
+ </style>
+</head>
+<body role="application">
+ <div id="graph-container">
+ <canvas id="graph-canvas"></canvas>
+ </div>
+</body>
+</html>
diff --git a/devtools/client/shared/widgets/mdn-docs.css b/devtools/client/shared/widgets/mdn-docs.css
new file mode 100644
index 000000000..e3547489f
--- /dev/null
+++ b/devtools/client/shared/widgets/mdn-docs.css
@@ -0,0 +1,39 @@
+/* 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/. */
+
+.mdn-container {
+ height: 300px;
+ margin: 4px;
+ overflow: auto;
+ box-sizing: border-box;
+ display: flex;
+ flex-direction: column;
+}
+
+.mdn-container header,
+.mdn-container footer {
+ flex: 1;
+ padding: 0 1em;
+}
+
+.mdn-property-info {
+ flex: 10;
+ padding: 0 1em;
+ overflow: auto;
+ transition: opacity 400ms ease-in;
+}
+
+.mdn-syntax {
+ margin-top: 1em;
+}
+
+.devtools-throbber {
+ align-self: center;
+ opacity: 0;
+}
+
+.mdn-visit-page {
+ display: inline-block;
+ padding: 1em 0;
+}
diff --git a/devtools/client/shared/widgets/moz.build b/devtools/client/shared/widgets/moz.build
new file mode 100644
index 000000000..5a28d21ca
--- /dev/null
+++ b/devtools/client/shared/widgets/moz.build
@@ -0,0 +1,34 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DIRS += [
+ 'tooltip',
+]
+
+DevToolsModules(
+ 'AbstractTreeItem.jsm',
+ 'BarGraphWidget.js',
+ 'BreadcrumbsWidget.jsm',
+ 'Chart.jsm',
+ 'CubicBezierPresets.js',
+ 'CubicBezierWidget.js',
+ 'FastListWidget.js',
+ 'FilterWidget.js',
+ 'FlameGraph.js',
+ 'Graphs.js',
+ 'GraphsWorker.js',
+ 'LineGraphWidget.js',
+ 'MdnDocsWidget.js',
+ 'MountainGraphWidget.js',
+ 'SideMenuWidget.jsm',
+ 'SimpleListWidget.jsm',
+ 'Spectrum.js',
+ 'TableWidget.js',
+ 'TreeWidget.js',
+ 'VariablesView.jsm',
+ 'VariablesViewController.jsm',
+ 'view-helpers.js',
+)
diff --git a/devtools/client/shared/widgets/spectrum.css b/devtools/client/shared/widgets/spectrum.css
new file mode 100644
index 000000000..46826f2e1
--- /dev/null
+++ b/devtools/client/shared/widgets/spectrum.css
@@ -0,0 +1,155 @@
+/* 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/. */
+
+#eyedropper-button {
+ margin-inline-start: 5px;
+ display: block;
+}
+
+#eyedropper-button::before {
+ background-image: url(chrome://devtools/skin/images/command-eyedropper.svg);
+}
+
+/* Mix-in classes */
+
+.spectrum-checker {
+ background-color: #eee;
+ background-image: linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%, #ccc),
+ linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%, #ccc);
+ background-size: 12px 12px;
+ background-position: 0 0, 6px 6px;
+}
+
+.spectrum-slider-control {
+ cursor: pointer;
+ box-shadow: 0 0 2px rgba(0,0,0,.6);
+ background: #fff;
+ border-radius: 10px;
+ opacity: .8;
+}
+
+.spectrum-box {
+ border: 1px solid rgba(0,0,0,0.2);
+ border-radius: 2px;
+ background-clip: content-box;
+}
+
+/* Elements */
+
+#spectrum-tooltip {
+ padding: 4px;
+}
+
+.spectrum-container {
+ position: relative;
+ display: none;
+ top: 0;
+ left: 0;
+ border-radius: 0;
+ width: 200px;
+ padding: 5px;
+}
+
+.spectrum-show {
+ display: inline-block;
+}
+
+/* Keep aspect ratio:
+http://www.briangrinstead.com/blog/keep-aspect-ratio-with-html-and-css */
+.spectrum-top {
+ position: relative;
+ width: 100%;
+ display: inline-block;
+}
+
+.spectrum-top-inner {
+ position: absolute;
+ top:0;
+ left:0;
+ bottom:0;
+ right:0;
+}
+
+.spectrum-color {
+ position: absolute;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ right: 20%;
+}
+
+.spectrum-hue {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 83%;
+}
+
+.spectrum-fill {
+ /* Same as spectrum-color width */
+ margin-top: 85%;
+}
+
+.spectrum-sat, .spectrum-val {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+}
+
+.spectrum-dragger, .spectrum-slider {
+ -moz-user-select: none;
+}
+
+.spectrum-alpha {
+ position: relative;
+ height: 8px;
+ margin-top: 3px;
+}
+
+.spectrum-alpha-inner {
+ height: 100%;
+}
+
+.spectrum-alpha-handle {
+ position: absolute;
+ top: -3px;
+ bottom: -3px;
+ width: 5px;
+ left: 50%;
+}
+
+.spectrum-sat {
+ background-image: linear-gradient(to right, #FFF, rgba(204, 154, 129, 0));
+}
+
+.spectrum-val {
+ background-image: linear-gradient(to top, #000000, rgba(204, 154, 129, 0));
+}
+
+.spectrum-hue {
+ background: linear-gradient(to bottom, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%);
+}
+
+.spectrum-dragger {
+ position: absolute;
+ top: 0px;
+ left: 0px;
+ cursor: pointer;
+ border-radius: 50%;
+ height: 8px;
+ width: 8px;
+ border: 1px solid white;
+ box-shadow: 0 0 2px rgba(0,0,0,.6);
+}
+
+.spectrum-slider {
+ position: absolute;
+ top: 0;
+ height: 5px;
+ left: -3px;
+ right: -3px;
+}
diff --git a/devtools/client/shared/widgets/tooltip/CssDocsTooltip.js b/devtools/client/shared/widgets/tooltip/CssDocsTooltip.js
new file mode 100644
index 000000000..880c34de3
--- /dev/null
+++ b/devtools/client/shared/widgets/tooltip/CssDocsTooltip.js
@@ -0,0 +1,93 @@
+/* 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 {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+const {MdnDocsWidget} = require("devtools/client/shared/widgets/MdnDocsWidget");
+const {KeyShortcuts} = require("devtools/client/shared/key-shortcuts");
+const XHTML_NS = "http://www.w3.org/1999/xhtml";
+
+const TOOLTIP_WIDTH = 418;
+const TOOLTIP_HEIGHT = 308;
+
+/**
+ * Tooltip for displaying docs for CSS properties from MDN.
+ *
+ * @param {Document} toolboxDoc
+ * The toolbox document to attach the CSS docs tooltip.
+ */
+function CssDocsTooltip(toolboxDoc) {
+ this.tooltip = new HTMLTooltip(toolboxDoc, {
+ type: "arrow",
+ consumeOutsideClicks: true,
+ autofocus: true,
+ useXulWrapper: true,
+ stylesheet: "chrome://devtools/content/shared/widgets/mdn-docs.css",
+ });
+ this.widget = this.setMdnDocsContent();
+ this._onVisitLink = this._onVisitLink.bind(this);
+ this.widget.on("visitlink", this._onVisitLink);
+
+ // Initialize keyboard shortcuts
+ this.shortcuts = new KeyShortcuts({ window: this.tooltip.topWindow });
+ this._onShortcut = this._onShortcut.bind(this);
+
+ this.shortcuts.on("Escape", this._onShortcut);
+}
+
+CssDocsTooltip.prototype = {
+ /**
+ * Load CSS docs for the given property,
+ * then display the tooltip.
+ */
+ show: function (anchor, propertyName) {
+ this.tooltip.once("shown", () => {
+ this.widget.loadCssDocs(propertyName);
+ });
+ this.tooltip.show(anchor);
+ },
+
+ hide: function () {
+ this.tooltip.hide();
+ },
+
+ _onShortcut: function (shortcut, event) {
+ if (!this.tooltip.isVisible()) {
+ return;
+ }
+ event.stopPropagation();
+ event.preventDefault();
+ this.hide();
+ },
+
+ _onVisitLink: function () {
+ this.hide();
+ },
+
+ /**
+ * Set the content of this tooltip to the MDN docs widget. This is called when the
+ * tooltip is first constructed.
+ * The caller can use the MdnDocsWidget to update the tooltip's UI with new content
+ * each time the tooltip is shown.
+ *
+ * @return {MdnDocsWidget} the created MdnDocsWidget instance.
+ */
+ setMdnDocsContent: function () {
+ let container = this.tooltip.doc.createElementNS(XHTML_NS, "div");
+ container.setAttribute("class", "mdn-container theme-body");
+ this.tooltip.setContent(container, {width: TOOLTIP_WIDTH, height: TOOLTIP_HEIGHT});
+ return new MdnDocsWidget(container);
+ },
+
+ destroy: function () {
+ this.widget.off("visitlink", this._onVisitLink);
+ this.widget.destroy();
+
+ this.shortcuts.destroy();
+ this.tooltip.destroy();
+ }
+};
+
+module.exports = CssDocsTooltip;
diff --git a/devtools/client/shared/widgets/tooltip/EventTooltipHelper.js b/devtools/client/shared/widgets/tooltip/EventTooltipHelper.js
new file mode 100644
index 000000000..63507bc5e
--- /dev/null
+++ b/devtools/client/shared/widgets/tooltip/EventTooltipHelper.js
@@ -0,0 +1,313 @@
+/* -*- 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 {LocalizationHelper} = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper("devtools/client/locales/inspector.properties");
+
+const Editor = require("devtools/client/sourceeditor/editor");
+const beautify = require("devtools/shared/jsbeautify/beautify");
+
+const XHTML_NS = "http://www.w3.org/1999/xhtml";
+const CONTAINER_WIDTH = 500;
+
+/**
+ * Set the content of a provided HTMLTooltip instance to display a list of event
+ * listeners, with their event type, capturing argument and a link to the code
+ * of the event handler.
+ *
+ * @param {HTMLTooltip} tooltip
+ * The tooltip instance on which the event details content should be set
+ * @param {Array} eventListenerInfos
+ * A list of event listeners
+ * @param {Toolbox} toolbox
+ * Toolbox used to select debugger panel
+ */
+function setEventTooltip(tooltip, eventListenerInfos, toolbox) {
+ let eventTooltip = new EventTooltip(tooltip, eventListenerInfos, toolbox);
+ eventTooltip.init();
+}
+
+function EventTooltip(tooltip, eventListenerInfos, toolbox) {
+ this._tooltip = tooltip;
+ this._eventListenerInfos = eventListenerInfos;
+ this._toolbox = toolbox;
+ this._eventEditors = new WeakMap();
+
+ // Used in tests: add a reference to the EventTooltip instance on the HTMLTooltip.
+ this._tooltip.eventTooltip = this;
+
+ this._headerClicked = this._headerClicked.bind(this);
+ this._debugClicked = this._debugClicked.bind(this);
+ this.destroy = this.destroy.bind(this);
+}
+
+EventTooltip.prototype = {
+ init: function () {
+ let config = {
+ mode: Editor.modes.js,
+ lineNumbers: false,
+ lineWrapping: true,
+ readOnly: true,
+ styleActiveLine: true,
+ extraKeys: {},
+ theme: "mozilla markup-view"
+ };
+
+ let doc = this._tooltip.doc;
+ this.container = doc.createElementNS(XHTML_NS, "div");
+ this.container.className = "devtools-tooltip-events-container";
+
+ for (let listener of this._eventListenerInfos) {
+ let phase = listener.capturing ? "Capturing" : "Bubbling";
+ let level = listener.DOM0 ? "DOM0" : "DOM2";
+
+ // Header
+ let header = doc.createElementNS(XHTML_NS, "div");
+ header.className = "event-header devtools-toolbar";
+ this.container.appendChild(header);
+
+ if (!listener.hide.debugger) {
+ let debuggerIcon = doc.createElementNS(XHTML_NS, "img");
+ debuggerIcon.className = "event-tooltip-debugger-icon";
+ debuggerIcon.setAttribute("src",
+ "chrome://devtools/skin/images/tool-debugger.svg");
+ let openInDebugger = L10N.getStr("eventsTooltip.openInDebugger");
+ debuggerIcon.setAttribute("title", openInDebugger);
+ header.appendChild(debuggerIcon);
+ }
+
+ if (!listener.hide.type) {
+ let eventTypeLabel = doc.createElementNS(XHTML_NS, "span");
+ eventTypeLabel.className = "event-tooltip-event-type";
+ eventTypeLabel.textContent = listener.type;
+ eventTypeLabel.setAttribute("title", listener.type);
+ header.appendChild(eventTypeLabel);
+ }
+
+ if (!listener.hide.filename) {
+ let filename = doc.createElementNS(XHTML_NS, "span");
+ filename.className = "event-tooltip-filename devtools-monospace";
+ filename.textContent = listener.origin;
+ filename.setAttribute("title", listener.origin);
+ header.appendChild(filename);
+ }
+
+ let attributesContainer = doc.createElementNS(XHTML_NS, "div");
+ attributesContainer.className = "event-tooltip-attributes-container";
+ header.appendChild(attributesContainer);
+
+ if (!listener.hide.capturing) {
+ let attributesBox = doc.createElementNS(XHTML_NS, "div");
+ attributesBox.className = "event-tooltip-attributes-box";
+ attributesContainer.appendChild(attributesBox);
+
+ let capturing = doc.createElementNS(XHTML_NS, "span");
+ capturing.className = "event-tooltip-attributes";
+ capturing.textContent = phase;
+ capturing.setAttribute("title", phase);
+ attributesBox.appendChild(capturing);
+ }
+
+ if (listener.tags) {
+ for (let tag of listener.tags.split(",")) {
+ let attributesBox = doc.createElementNS(XHTML_NS, "div");
+ attributesBox.className = "event-tooltip-attributes-box";
+ attributesContainer.appendChild(attributesBox);
+
+ let tagBox = doc.createElementNS(XHTML_NS, "span");
+ tagBox.className = "event-tooltip-attributes";
+ tagBox.textContent = tag;
+ tagBox.setAttribute("title", tag);
+ attributesBox.appendChild(tagBox);
+ }
+ }
+
+ if (!listener.hide.dom0) {
+ let attributesBox = doc.createElementNS(XHTML_NS, "div");
+ attributesBox.className = "event-tooltip-attributes-box";
+ attributesContainer.appendChild(attributesBox);
+
+ let dom0 = doc.createElementNS(XHTML_NS, "span");
+ dom0.className = "event-tooltip-attributes";
+ dom0.textContent = level;
+ dom0.setAttribute("title", level);
+ attributesBox.appendChild(dom0);
+ }
+
+ // Content
+ let content = doc.createElementNS(XHTML_NS, "div");
+ let editor = new Editor(config);
+ this._eventEditors.set(content, {
+ editor: editor,
+ handler: listener.handler,
+ searchString: listener.searchString,
+ uri: listener.origin,
+ dom0: listener.DOM0,
+ appended: false
+ });
+
+ content.className = "event-tooltip-content-box";
+ this.container.appendChild(content);
+
+ this._addContentListeners(header);
+ }
+
+ this._tooltip.setContent(this.container, {width: CONTAINER_WIDTH});
+ this._tooltip.on("hidden", this.destroy);
+ },
+
+ _addContentListeners: function (header) {
+ header.addEventListener("click", this._headerClicked);
+ },
+
+ _headerClicked: function (event) {
+ if (event.target.classList.contains("event-tooltip-debugger-icon")) {
+ this._debugClicked(event);
+ event.stopPropagation();
+ return;
+ }
+
+ let doc = this._tooltip.doc;
+ let header = event.currentTarget;
+ let content = header.nextElementSibling;
+
+ if (content.hasAttribute("open")) {
+ content.removeAttribute("open");
+ } else {
+ let contentNodes = doc.querySelectorAll(".event-tooltip-content-box");
+
+ for (let node of contentNodes) {
+ if (node !== content) {
+ node.removeAttribute("open");
+ }
+ }
+
+ content.setAttribute("open", "");
+
+ let eventEditor = this._eventEditors.get(content);
+
+ if (eventEditor.appended) {
+ return;
+ }
+
+ let {editor, handler} = eventEditor;
+
+ let iframe = doc.createElementNS(XHTML_NS, "iframe");
+ iframe.setAttribute("style", "width: 100%; height: 100%; border-style: none;");
+
+ editor.appendTo(content, iframe).then(() => {
+ let tidied = beautify.js(handler, { "indent_size": 2 });
+ editor.setText(tidied);
+
+ eventEditor.appended = true;
+
+ let container = header.parentElement.getBoundingClientRect();
+ if (header.getBoundingClientRect().top < container.top) {
+ header.scrollIntoView(true);
+ } else if (content.getBoundingClientRect().bottom > container.bottom) {
+ content.scrollIntoView(false);
+ }
+
+ this._tooltip.emit("event-tooltip-ready");
+ });
+ }
+ },
+
+ _debugClicked: function (event) {
+ let header = event.currentTarget;
+ let content = header.nextElementSibling;
+
+ let {uri, searchString, dom0} = this._eventEditors.get(content);
+
+ if (uri && uri !== "?") {
+ // Save a copy of toolbox as it will be set to null when we hide the tooltip.
+ let toolbox = this._toolbox;
+
+ this._tooltip.hide();
+
+ uri = uri.replace(/"/g, "");
+
+ let showSource = ({ DebuggerView }) => {
+ let matches = uri.match(/(.*):(\d+$)/);
+ let line = 1;
+
+ if (matches) {
+ uri = matches[1];
+ line = matches[2];
+ }
+
+ let item = DebuggerView.Sources.getItemForAttachment(a => a.source.url === uri);
+ if (item) {
+ let actor = item.attachment.source.actor;
+ DebuggerView.setEditorLocation(
+ actor, line, {noDebug: true}
+ ).then(() => {
+ if (dom0) {
+ let text = DebuggerView.editor.getText();
+ let index = text.indexOf(searchString);
+ let lastIndex = text.lastIndexOf(searchString);
+
+ // To avoid confusion we only search for DOM0 event handlers when
+ // there is only one possible match in the file.
+ if (index !== -1 && index === lastIndex) {
+ text = text.substr(0, index);
+ let newlineMatches = text.match(/\n/g);
+
+ if (newlineMatches) {
+ DebuggerView.editor.setCursor({
+ line: newlineMatches.length
+ });
+ }
+ }
+ }
+ });
+ }
+ };
+
+ let debuggerAlreadyOpen = toolbox.getPanel("jsdebugger");
+ toolbox.selectTool("jsdebugger").then(({ panelWin: dbg }) => {
+ if (debuggerAlreadyOpen) {
+ showSource(dbg);
+ } else {
+ dbg.once(dbg.EVENTS.SOURCES_ADDED, () => showSource(dbg));
+ }
+ });
+ }
+ },
+
+ destroy: function () {
+ if (this._tooltip) {
+ this._tooltip.off("hidden", this.destroy);
+
+ let boxes = this.container.querySelectorAll(".event-tooltip-content-box");
+
+ for (let box of boxes) {
+ let {editor} = this._eventEditors.get(box);
+ editor.destroy();
+ }
+
+ this._eventEditors = null;
+ this._tooltip.eventTooltip = null;
+ }
+
+ let headerNodes = this.container.querySelectorAll(".event-header");
+
+ for (let node of headerNodes) {
+ node.removeEventListener("click", this._headerClicked);
+ }
+
+ let sourceNodes = this.container.querySelectorAll(".event-tooltip-debugger-icon");
+ for (let node of sourceNodes) {
+ node.removeEventListener("click", this._debugClicked);
+ }
+
+ this._eventListenerInfos = this._toolbox = this._tooltip = null;
+ }
+};
+
+module.exports.setEventTooltip = setEventTooltip;
diff --git a/devtools/client/shared/widgets/tooltip/HTMLTooltip.js b/devtools/client/shared/widgets/tooltip/HTMLTooltip.js
new file mode 100644
index 000000000..749878220
--- /dev/null
+++ b/devtools/client/shared/widgets/tooltip/HTMLTooltip.js
@@ -0,0 +1,638 @@
+/* -*- 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 EventEmitter = require("devtools/shared/event-emitter");
+const {TooltipToggle} = require("devtools/client/shared/widgets/tooltip/TooltipToggle");
+const {listenOnce} = require("devtools/shared/async-utils");
+const {Task} = require("devtools/shared/task");
+
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+const XHTML_NS = "http://www.w3.org/1999/xhtml";
+
+const POSITION = {
+ TOP: "top",
+ BOTTOM: "bottom",
+};
+
+module.exports.POSITION = POSITION;
+
+const TYPE = {
+ NORMAL: "normal",
+ ARROW: "arrow",
+};
+
+module.exports.TYPE = TYPE;
+
+const ARROW_WIDTH = 32;
+
+// Default offset between the tooltip's left edge and the tooltip arrow.
+const ARROW_OFFSET = 20;
+
+const EXTRA_HEIGHT = {
+ "normal": 0,
+ // The arrow is 16px tall, but merges on 3px with the panel border
+ "arrow": 13,
+};
+
+const EXTRA_BORDER = {
+ "normal": 0,
+ "arrow": 3,
+};
+
+/**
+ * Calculate the vertical position & offsets to use for the tooltip. Will attempt to
+ * respect the provided height and position preferences, unless the available height
+ * prevents this.
+ *
+ * @param {DOMRect} anchorRect
+ * Bounding rectangle for the anchor, relative to the tooltip document.
+ * @param {DOMRect} viewportRect
+ * Bounding rectangle for the viewport. top/left can be different from 0 if some
+ * space should not be used by tooltips (for instance OS toolbars, taskbars etc.).
+ * @param {Number} height
+ * Preferred height for the tooltip.
+ * @param {String} pos
+ * Preferred position for the tooltip. Possible values: "top" or "bottom".
+ * @return {Object}
+ * - {Number} top: the top offset for the tooltip.
+ * - {Number} height: the height to use for the tooltip container.
+ * - {String} computedPosition: Can differ from the preferred position depending
+ * on the available height). "top" or "bottom"
+ */
+const calculateVerticalPosition =
+function (anchorRect, viewportRect, height, pos, offset) {
+ let {TOP, BOTTOM} = POSITION;
+
+ let {top: anchorTop, height: anchorHeight} = anchorRect;
+
+ // Translate to the available viewport space before calculating dimensions and position.
+ anchorTop -= viewportRect.top;
+
+ // Calculate available space for the tooltip.
+ let availableTop = anchorTop;
+ let availableBottom = viewportRect.height - (anchorTop + anchorHeight);
+
+ // Find POSITION
+ let keepPosition = false;
+ if (pos === TOP) {
+ keepPosition = availableTop >= height + offset;
+ } else if (pos === BOTTOM) {
+ keepPosition = availableBottom >= height + offset;
+ }
+ if (!keepPosition) {
+ pos = availableTop > availableBottom ? TOP : BOTTOM;
+ }
+
+ // Calculate HEIGHT.
+ let availableHeight = pos === TOP ? availableTop : availableBottom;
+ height = Math.min(height, availableHeight - offset);
+ height = Math.floor(height);
+
+ // Calculate TOP.
+ let top = pos === TOP ? anchorTop - height - offset : anchorTop + anchorHeight + offset;
+
+ // Translate back to absolute coordinates by re-including viewport top margin.
+ top += viewportRect.top;
+
+ return {top, height, computedPosition: pos};
+};
+
+/**
+ * Calculate the vertical position & offsets to use for the tooltip. Will attempt to
+ * respect the provided height and position preferences, unless the available height
+ * prevents this.
+ *
+ * @param {DOMRect} anchorRect
+ * Bounding rectangle for the anchor, relative to the tooltip document.
+ * @param {DOMRect} viewportRect
+ * Bounding rectangle for the viewport. top/left can be different from 0 if some
+ * space should not be used by tooltips (for instance OS toolbars, taskbars etc.).
+ * @param {Number} width
+ * Preferred width for the tooltip.
+ * @param {String} type
+ * The tooltip type (e.g. "arrow").
+ * @param {Number} offset
+ * Horizontal offset in pixels.
+ * @param {Boolean} isRtl
+ * If the anchor is in RTL, the tooltip should be aligned to the right.
+ * @return {Object}
+ * - {Number} left: the left offset for the tooltip.
+ * - {Number} width: the width to use for the tooltip container.
+ * - {Number} arrowLeft: the left offset to use for the arrow element.
+ */
+const calculateHorizontalPosition =
+function (anchorRect, viewportRect, width, type, offset, isRtl) {
+ let anchorWidth = anchorRect.width;
+ let anchorStart = isRtl ? anchorRect.right : anchorRect.left;
+
+ // Translate to the available viewport space before calculating dimensions and position.
+ anchorStart -= viewportRect.left;
+
+ // Calculate WIDTH.
+ width = Math.min(width, viewportRect.width);
+
+ // Calculate LEFT.
+ // By default the tooltip is aligned with the anchor left edge. Unless this
+ // makes it overflow the viewport, in which case is shifts to the left.
+ let left = anchorStart + offset - (isRtl ? width : 0);
+ left = Math.min(left, viewportRect.width - width);
+ left = Math.max(0, left);
+
+ // Calculate ARROW LEFT (tooltip's LEFT might be updated)
+ let arrowLeft;
+ // Arrow style tooltips may need to be shifted to the left
+ if (type === TYPE.ARROW) {
+ let arrowCenter = left + ARROW_OFFSET + ARROW_WIDTH / 2;
+ let anchorCenter = anchorStart + anchorWidth / 2;
+ // If the anchor is too narrow, align the arrow and the anchor center.
+ if (arrowCenter > anchorCenter) {
+ left = Math.max(0, left - (arrowCenter - anchorCenter));
+ }
+ // Arrow's left offset relative to the anchor.
+ arrowLeft = Math.min(ARROW_OFFSET, (anchorWidth - ARROW_WIDTH) / 2) | 0;
+ // Translate the coordinate to tooltip container
+ arrowLeft += anchorStart - left;
+ // Make sure the arrow remains in the tooltip container.
+ arrowLeft = Math.min(arrowLeft, width - ARROW_WIDTH);
+ arrowLeft = Math.max(arrowLeft, 0);
+ }
+
+ // Translate back to absolute coordinates by re-including viewport left margin.
+ left += viewportRect.left;
+
+ return {left, width, arrowLeft};
+};
+
+/**
+ * Get the bounding client rectangle for a given node, relative to a custom
+ * reference element (instead of the default for getBoundingClientRect which
+ * is always the element's ownerDocument).
+ */
+const getRelativeRect = function (node, relativeTo) {
+ // Width and Height can be taken from the rect.
+ let {width, height} = node.getBoundingClientRect();
+
+ let quads = node.getBoxQuads({relativeTo});
+ let top = quads[0].bounds.top;
+ let left = quads[0].bounds.left;
+
+ // Compute right and bottom coordinates using the rest of the data.
+ let right = left + width;
+ let bottom = top + height;
+
+ return {top, right, bottom, left, width, height};
+};
+
+/**
+ * The HTMLTooltip can display HTML content in a tooltip popup.
+ *
+ * @param {Document} toolboxDoc
+ * The toolbox document to attach the HTMLTooltip popup.
+ * @param {Object}
+ * - {String} type
+ * Display type of the tooltip. Possible values: "normal", "arrow"
+ * - {Boolean} autofocus
+ * Defaults to false. Should the tooltip be focused when opening it.
+ * - {Boolean} consumeOutsideClicks
+ * Defaults to true. The tooltip is closed when clicking outside.
+ * Should this event be stopped and consumed or not.
+ * - {Boolean} useXulWrapper
+ * Defaults to false. If the tooltip is hosted in a XUL document, use a XUL panel
+ * in order to use all the screen viewport available.
+ * - {String} stylesheet
+ * Style sheet URL to apply to the tooltip content.
+ */
+function HTMLTooltip(toolboxDoc, {
+ type = "normal",
+ autofocus = false,
+ consumeOutsideClicks = true,
+ useXulWrapper = false,
+ stylesheet = "",
+ } = {}) {
+ EventEmitter.decorate(this);
+
+ this.doc = toolboxDoc;
+ this.type = type;
+ this.autofocus = autofocus;
+ this.consumeOutsideClicks = consumeOutsideClicks;
+ this.useXulWrapper = this._isXUL() && useXulWrapper;
+
+ // The top window is used to attach click event listeners to close the tooltip if the
+ // user clicks on the content page.
+ this.topWindow = this._getTopWindow();
+
+ this._position = null;
+
+ this._onClick = this._onClick.bind(this);
+ this._onXulPanelHidden = this._onXulPanelHidden.bind(this);
+
+ this._toggle = new TooltipToggle(this);
+ this.startTogglingOnHover = this._toggle.start.bind(this._toggle);
+ this.stopTogglingOnHover = this._toggle.stop.bind(this._toggle);
+
+ this.container = this._createContainer();
+
+ if (stylesheet) {
+ this._applyStylesheet(stylesheet);
+ }
+ if (this.useXulWrapper) {
+ // When using a XUL panel as the wrapper, the actual markup for the tooltip is as
+ // follows :
+ // <panel> <!-- XUL panel used to position the tooltip anywhere on screen -->
+ // <div> <!-- div wrapper used to isolate the tooltip container -->
+ // <div> <! the actual tooltip.container element -->
+ this.xulPanelWrapper = this._createXulPanelWrapper();
+ let inner = this.doc.createElementNS(XHTML_NS, "div");
+ inner.classList.add("tooltip-xul-wrapper-inner");
+
+ this.doc.documentElement.appendChild(this.xulPanelWrapper);
+ this.xulPanelWrapper.appendChild(inner);
+ inner.appendChild(this.container);
+ } else if (this._isXUL()) {
+ this.doc.documentElement.appendChild(this.container);
+ } else {
+ // In non-XUL context the container is ready to use as is.
+ this.doc.body.appendChild(this.container);
+ }
+}
+
+module.exports.HTMLTooltip = HTMLTooltip;
+
+HTMLTooltip.prototype = {
+ /**
+ * The tooltip panel is the parentNode of the tooltip content provided in
+ * setContent().
+ */
+ get panel() {
+ return this.container.querySelector(".tooltip-panel");
+ },
+
+ /**
+ * The arrow element. Might be null depending on the tooltip type.
+ */
+ get arrow() {
+ return this.container.querySelector(".tooltip-arrow");
+ },
+
+ /**
+ * Retrieve the displayed position used for the tooltip. Null if the tooltip is hidden.
+ */
+ get position() {
+ return this.isVisible() ? this._position : null;
+ },
+
+ /**
+ * Set the tooltip content element. The preferred width/height should also be
+ * specified here.
+ *
+ * @param {Element} content
+ * The tooltip content, should be a HTML element.
+ * @param {Object}
+ * - {Number} width: preferred width for the tooltip container. If not specified
+ * the tooltip container will be measured before being displayed, and the
+ * measured width will be used as preferred width.
+ * - {Number} height: optional, preferred height for the tooltip container. If
+ * not specified, the tooltip will be able to use all the height available.
+ */
+ setContent: function (content, {width = "auto", height = Infinity} = {}) {
+ this.preferredWidth = width;
+ this.preferredHeight = height;
+
+ this.panel.innerHTML = "";
+ this.panel.appendChild(content);
+ },
+
+ /**
+ * Show the tooltip next to the provided anchor element. A preferred position
+ * can be set. The event "shown" will be fired after the tooltip is displayed.
+ *
+ * @param {Element} anchor
+ * The reference element with which the tooltip should be aligned
+ * @param {Object}
+ * - {String} position: optional, possible values: top|bottom
+ * If layout permits, the tooltip will be displayed on top/bottom
+ * of the anchor. If ommitted, the tooltip will be displayed where
+ * more space is available.
+ * - {Number} x: optional, horizontal offset between the anchor and the tooltip
+ * - {Number} y: optional, vertical offset between the anchor and the tooltip
+ */
+ show: Task.async(function* (anchor, {position, x = 0, y = 0} = {}) {
+ // Get anchor geometry
+ let anchorRect = getRelativeRect(anchor, this.doc);
+ if (this.useXulWrapper) {
+ anchorRect = this._convertToScreenRect(anchorRect);
+ }
+
+ // Get viewport size
+ let viewportRect = this._getViewportRect();
+
+ let themeHeight = EXTRA_HEIGHT[this.type] + 2 * EXTRA_BORDER[this.type];
+ let preferredHeight = this.preferredHeight + themeHeight;
+
+ let {top, height, computedPosition} =
+ calculateVerticalPosition(anchorRect, viewportRect, preferredHeight, position, y);
+
+ this._position = computedPosition;
+ // Apply height before measuring the content width (if width="auto").
+ let isTop = computedPosition === POSITION.TOP;
+ this.container.classList.toggle("tooltip-top", isTop);
+ this.container.classList.toggle("tooltip-bottom", !isTop);
+
+ // If the preferred height is set to Infinity, the tooltip container should grow based
+ // on its content's height and use as much height as possible.
+ this.container.classList.toggle("tooltip-flexible-height",
+ this.preferredHeight === Infinity);
+
+ this.container.style.height = height + "px";
+
+ let preferredWidth;
+ if (this.preferredWidth === "auto") {
+ preferredWidth = this._measureContainerWidth();
+ } else {
+ let themeWidth = 2 * EXTRA_BORDER[this.type];
+ preferredWidth = this.preferredWidth + themeWidth;
+ }
+
+ let anchorWin = anchor.ownerDocument.defaultView;
+ let isRtl = anchorWin.getComputedStyle(anchor).direction === "rtl";
+ let {left, width, arrowLeft} = calculateHorizontalPosition(
+ anchorRect, viewportRect, preferredWidth, this.type, x, isRtl);
+
+ this.container.style.width = width + "px";
+
+ if (this.type === TYPE.ARROW) {
+ this.arrow.style.left = arrowLeft + "px";
+ }
+
+ if (this.useXulWrapper) {
+ yield this._showXulWrapperAt(left, top);
+ } else {
+ this.container.style.left = left + "px";
+ this.container.style.top = top + "px";
+ }
+
+ this.container.classList.add("tooltip-visible");
+
+ // Keep a pointer on the focused element to refocus it when hiding the tooltip.
+ this._focusedElement = this.doc.activeElement;
+
+ this.doc.defaultView.clearTimeout(this.attachEventsTimer);
+ this.attachEventsTimer = this.doc.defaultView.setTimeout(() => {
+ this._maybeFocusTooltip();
+ // Updated the top window reference each time in case the host changes.
+ this.topWindow = this._getTopWindow();
+ this.topWindow.addEventListener("click", this._onClick, true);
+ this.emit("shown");
+ }, 0);
+ }),
+
+ /**
+ * Calculate the rect of the viewport that limits the tooltip dimensions. When using a
+ * XUL panel wrapper, the viewport will be able to use the whole screen (excluding space
+ * reserved by the OS for toolbars etc.). Otherwise, the viewport is limited to the
+ * tooltip's document.
+ *
+ * @return {Object} DOMRect-like object with the Number properties: top, right, bottom,
+ * left, width, height
+ */
+ _getViewportRect: function () {
+ if (this.useXulWrapper) {
+ // availLeft/Top are the coordinates first pixel available on the screen for
+ // applications (excluding space dedicated for OS toolbars, menus etc...)
+ // availWidth/Height are the dimensions available to applications excluding all
+ // the OS reserved space
+ let {availLeft, availTop, availHeight, availWidth} = this.doc.defaultView.screen;
+ return {
+ top: availTop,
+ right: availLeft + availWidth,
+ bottom: availTop + availHeight,
+ left: availLeft,
+ width: availWidth,
+ height: availHeight,
+ };
+ }
+
+ return this.doc.documentElement.getBoundingClientRect();
+ },
+
+ _measureContainerWidth: function () {
+ let xulParent = this.container.parentNode;
+ if (this.useXulWrapper && !this.isVisible()) {
+ // Move the container out of the XUL Panel to measure it.
+ this.doc.documentElement.appendChild(this.container);
+ }
+
+ this.container.classList.add("tooltip-hidden");
+ this.container.style.width = "auto";
+ let width = this.container.getBoundingClientRect().width;
+ this.container.classList.remove("tooltip-hidden");
+
+ if (this.useXulWrapper && !this.isVisible()) {
+ xulParent.appendChild(this.container);
+ }
+
+ return width;
+ },
+
+ /**
+ * Hide the current tooltip. The event "hidden" will be fired when the tooltip
+ * is hidden.
+ */
+ hide: Task.async(function* () {
+ this.doc.defaultView.clearTimeout(this.attachEventsTimer);
+ if (!this.isVisible()) {
+ this.emit("hidden");
+ return;
+ }
+
+ this.topWindow.removeEventListener("click", this._onClick, true);
+ this.container.classList.remove("tooltip-visible");
+ if (this.useXulWrapper) {
+ yield this._hideXulWrapper();
+ }
+
+ this.emit("hidden");
+
+ let tooltipHasFocus = this.container.contains(this.doc.activeElement);
+ if (tooltipHasFocus && this._focusedElement) {
+ this._focusedElement.focus();
+ this._focusedElement = null;
+ }
+ }),
+
+ /**
+ * Check if the tooltip is currently displayed.
+ * @return {Boolean} true if the tooltip is visible
+ */
+ isVisible: function () {
+ return this.container.classList.contains("tooltip-visible");
+ },
+
+ /**
+ * Destroy the tooltip instance. Hide the tooltip if displayed, remove the
+ * tooltip container from the document.
+ */
+ destroy: function () {
+ this.hide();
+ this.container.remove();
+ if (this.xulPanelWrapper) {
+ this.xulPanelWrapper.remove();
+ }
+ },
+
+ _createContainer: function () {
+ let container = this.doc.createElementNS(XHTML_NS, "div");
+ container.setAttribute("type", this.type);
+ container.classList.add("tooltip-container");
+
+ let html = '<div class="tooltip-filler"></div>';
+ html += '<div class="tooltip-panel"></div>';
+
+ if (this.type === TYPE.ARROW) {
+ html += '<div class="tooltip-arrow"></div>';
+ }
+ container.innerHTML = html;
+ return container;
+ },
+
+ _onClick: function (e) {
+ if (this._isInTooltipContainer(e.target)) {
+ return;
+ }
+
+ this.hide();
+ if (this.consumeOutsideClicks && e.button === 0) {
+ // Consume only left click events (button === 0).
+ e.preventDefault();
+ e.stopPropagation();
+ }
+ },
+
+ _isInTooltipContainer: function (node) {
+ // Check if the target is the tooltip arrow.
+ if (this.arrow && this.arrow === node) {
+ return true;
+ }
+
+ let tooltipWindow = this.panel.ownerDocument.defaultView;
+ let win = node.ownerDocument.defaultView;
+
+ // Check if the tooltip panel contains the node if they live in the same document.
+ if (win === tooltipWindow) {
+ return this.panel.contains(node);
+ }
+
+ // Check if the node window is in the tooltip container.
+ while (win.parent && win.parent !== win) {
+ if (win.parent === tooltipWindow) {
+ // If the parent window is the tooltip window, check if the tooltip contains
+ // the current frame element.
+ return this.panel.contains(win.frameElement);
+ }
+ win = win.parent;
+ }
+
+ return false;
+ },
+
+ _onXulPanelHidden: function () {
+ if (this.isVisible()) {
+ this.hide();
+ }
+ },
+
+ /**
+ * If the tootlip is configured to autofocus and a focusable element can be found,
+ * focus it.
+ */
+ _maybeFocusTooltip: function () {
+ // Simplied selector targetting elements that can receive the focus, full version at
+ // http://stackoverflow.com/questions/1599660/which-html-elements-can-receive-focus .
+ let focusableSelector = "a, button, iframe, input, select, textarea";
+ let focusableElement = this.panel.querySelector(focusableSelector);
+ if (this.autofocus && focusableElement) {
+ focusableElement.focus();
+ }
+ },
+
+ _getTopWindow: function () {
+ return this.doc.defaultView.top;
+ },
+
+ /**
+ * Check if the tooltip's owner document is a XUL document.
+ */
+ _isXUL: function () {
+ return this.doc.documentElement.namespaceURI === XUL_NS;
+ },
+
+ _createXulPanelWrapper: function () {
+ let panel = this.doc.createElementNS(XUL_NS, "panel");
+
+ // XUL panel is only a way to display DOM elements outside of the document viewport,
+ // so disable all features that impact the behavior.
+ panel.setAttribute("animate", false);
+ panel.setAttribute("consumeoutsideclicks", false);
+ panel.setAttribute("noautofocus", true);
+ panel.setAttribute("ignorekeys", true);
+ panel.setAttribute("tooltip", "aHTMLTooltip");
+
+ // Use type="arrow" to prevent side effects (see Bug 1285206)
+ panel.setAttribute("type", "arrow");
+
+ panel.setAttribute("level", "top");
+ panel.setAttribute("class", "tooltip-xul-wrapper");
+
+ return panel;
+ },
+
+ _showXulWrapperAt: function (left, top) {
+ this.xulPanelWrapper.addEventListener("popuphidden", this._onXulPanelHidden);
+ let onPanelShown = listenOnce(this.xulPanelWrapper, "popupshown");
+ this.xulPanelWrapper.openPopupAtScreen(left, top, false);
+ return onPanelShown;
+ },
+
+ _hideXulWrapper: function () {
+ this.xulPanelWrapper.removeEventListener("popuphidden", this._onXulPanelHidden);
+
+ if (this.xulPanelWrapper.state === "closed") {
+ // XUL panel is already closed, resolve immediately.
+ return Promise.resolve();
+ }
+
+ let onPanelHidden = listenOnce(this.xulPanelWrapper, "popuphidden");
+ this.xulPanelWrapper.hidePopup();
+ return onPanelHidden;
+ },
+
+ /**
+ * Convert from coordinates relative to the tooltip's document, to coordinates relative
+ * to the "available" screen. By "available" we mean the screen, excluding the OS bars
+ * display on screen edges.
+ */
+ _convertToScreenRect: function ({left, top, width, height}) {
+ // mozInnerScreenX/Y are the coordinates of the top left corner of the window's
+ // viewport, excluding chrome UI.
+ left += this.doc.defaultView.mozInnerScreenX;
+ top += this.doc.defaultView.mozInnerScreenY;
+ return {top, right: left + width, bottom: top + height, left, width, height};
+ },
+
+ /**
+ * Apply a scoped stylesheet to the container so that this css file only
+ * applies to it.
+ */
+ _applyStylesheet: function (url) {
+ let style = this.doc.createElementNS(XHTML_NS, "style");
+ style.setAttribute("scoped", "true");
+ url = url.replace(/"/g, "\\\"");
+ style.textContent = `@import url("${url}");`;
+ this.container.appendChild(style);
+ }
+};
diff --git a/devtools/client/shared/widgets/tooltip/ImageTooltipHelper.js b/devtools/client/shared/widgets/tooltip/ImageTooltipHelper.js
new file mode 100644
index 000000000..04c932005
--- /dev/null
+++ b/devtools/client/shared/widgets/tooltip/ImageTooltipHelper.js
@@ -0,0 +1,131 @@
+/* -*- 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 {LocalizationHelper} = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper("devtools/client/locales/inspector.properties");
+
+const XHTML_NS = "http://www.w3.org/1999/xhtml";
+
+// Default image tooltip max dimension
+const MAX_DIMENSION = 200;
+const CONTAINER_MIN_WIDTH = 100;
+const LABEL_HEIGHT = 20;
+const IMAGE_PADDING = 4;
+
+/**
+ * Image preview tooltips should be provided with the naturalHeight and
+ * naturalWidth value for the image to display. This helper loads the provided
+ * image URL in an image object in order to retrieve the image dimensions after
+ * the load.
+ *
+ * @param {Document} doc the document element to use to create the image object
+ * @param {String} imageUrl the url of the image to measure
+ * @return {Promise} returns a promise that will resolve after the iamge load:
+ * - {Number} naturalWidth natural width of the loaded image
+ * - {Number} naturalHeight natural height of the loaded image
+ */
+function getImageDimensions(doc, imageUrl) {
+ return new Promise(resolve => {
+ let imgObj = new doc.defaultView.Image();
+ imgObj.onload = () => {
+ imgObj.onload = null;
+ let { naturalWidth, naturalHeight } = imgObj;
+ resolve({ naturalWidth, naturalHeight });
+ };
+ imgObj.src = imageUrl;
+ });
+}
+
+/**
+ * Set the tooltip content of a provided HTMLTooltip instance to display an
+ * image preview matching the provided imageUrl.
+ *
+ * @param {HTMLTooltip} tooltip
+ * The tooltip instance on which the image preview content should be set
+ * @param {Document} doc
+ * A document element to create the HTML elements needed for the tooltip
+ * @param {String} imageUrl
+ * Absolute URL of the image to display in the tooltip
+ * @param {Object} options
+ * - {Number} naturalWidth mandatory, width of the image to display
+ * - {Number} naturalHeight mandatory, height of the image to display
+ * - {Number} maxDim optional, max width/height of the preview
+ * - {Boolean} hideDimensionLabel optional, pass true to hide the label
+ */
+function setImageTooltip(tooltip, doc, imageUrl, options) {
+ let {naturalWidth, naturalHeight, hideDimensionLabel, maxDim} = options;
+ maxDim = maxDim || MAX_DIMENSION;
+
+ let imgHeight = naturalHeight;
+ let imgWidth = naturalWidth;
+ if (imgHeight > maxDim || imgWidth > maxDim) {
+ let scale = maxDim / Math.max(imgHeight, imgWidth);
+ // Only allow integer values to avoid rounding errors.
+ imgHeight = Math.floor(scale * naturalHeight);
+ imgWidth = Math.ceil(scale * naturalWidth);
+ }
+
+ // Create tooltip content
+ let div = doc.createElementNS(XHTML_NS, "div");
+ div.style.cssText = `
+ height: 100%;
+ min-width: 100px;
+ display: flex;
+ flex-direction: column;
+ text-align: center;`;
+ let html = `
+ <div style="flex: 1;
+ display: flex;
+ padding: ${IMAGE_PADDING}px;
+ align-items: center;
+ justify-content: center;
+ min-height: 1px;">
+ <img style="height: ${imgHeight}px; max-height: 100%;"
+ src="${encodeURI(imageUrl)}"/>
+ </div>`;
+
+ if (!hideDimensionLabel) {
+ let label = naturalWidth + " \u00D7 " + naturalHeight;
+ html += `
+ <div style="height: ${LABEL_HEIGHT}px;
+ text-align: center;">
+ <span class="theme-comment devtools-tooltip-caption">${label}</span>
+ </div>`;
+ }
+ div.innerHTML = html;
+
+ // Calculate tooltip dimensions
+ let height = imgHeight + 2 * IMAGE_PADDING;
+ if (!hideDimensionLabel) {
+ height += LABEL_HEIGHT;
+ }
+ let width = Math.max(CONTAINER_MIN_WIDTH, imgWidth + 2 * IMAGE_PADDING);
+
+ tooltip.setContent(div, {width, height});
+}
+
+/*
+ * Set the tooltip content of a provided HTMLTooltip instance to display a
+ * fallback error message when an image preview tooltip can not be displayed.
+ *
+ * @param {HTMLTooltip} tooltip
+ * The tooltip instance on which the image preview content should be set
+ * @param {Document} doc
+ * A document element to create the HTML elements needed for the tooltip
+ */
+function setBrokenImageTooltip(tooltip, doc) {
+ let div = doc.createElementNS(XHTML_NS, "div");
+ div.className = "theme-comment devtools-tooltip-image-broken";
+ let message = L10N.getStr("previewTooltip.image.brokenImage");
+ div.textContent = message;
+ tooltip.setContent(div, {width: 150, height: 30});
+}
+
+module.exports.getImageDimensions = getImageDimensions;
+module.exports.setImageTooltip = setImageTooltip;
+module.exports.setBrokenImageTooltip = setBrokenImageTooltip;
diff --git a/devtools/client/shared/widgets/tooltip/SwatchBasedEditorTooltip.js b/devtools/client/shared/widgets/tooltip/SwatchBasedEditorTooltip.js
new file mode 100644
index 000000000..52bf565e2
--- /dev/null
+++ b/devtools/client/shared/widgets/tooltip/SwatchBasedEditorTooltip.js
@@ -0,0 +1,209 @@
+/* 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 EventEmitter = require("devtools/shared/event-emitter");
+const {KeyShortcuts} = require("devtools/client/shared/key-shortcuts");
+const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+
+/**
+ * Base class for all (color, gradient, ...)-swatch based value editors inside
+ * tooltips
+ *
+ * @param {Document} document
+ * The document to attach the SwatchBasedEditorTooltip. This is either the toolbox
+ * document if the tooltip is a popup tooltip or the panel's document if it is an
+ * inline editor.
+ */
+function SwatchBasedEditorTooltip(document, stylesheet) {
+ EventEmitter.decorate(this);
+ // Creating a tooltip instance
+ // This one will consume outside clicks as it makes more sense to let the user
+ // close the tooltip by clicking out
+ // It will also close on <escape> and <enter>
+ this.tooltip = new HTMLTooltip(document, {
+ type: "arrow",
+ consumeOutsideClicks: true,
+ useXulWrapper: true,
+ stylesheet
+ });
+
+ // By default, swatch-based editor tooltips revert value change on <esc> and
+ // commit value change on <enter>
+ this.shortcuts = new KeyShortcuts({
+ window: this.tooltip.topWindow
+ });
+ this.shortcuts.on("Escape", (name, event) => {
+ if (!this.tooltip.isVisible()) {
+ return;
+ }
+ this.revert();
+ this.hide();
+ event.stopPropagation();
+ event.preventDefault();
+ });
+ this.shortcuts.on("Return", (name, event) => {
+ if (!this.tooltip.isVisible()) {
+ return;
+ }
+ this.commit();
+ this.hide();
+ event.stopPropagation();
+ event.preventDefault();
+ });
+
+ // All target swatches are kept in a map, indexed by swatch DOM elements
+ this.swatches = new Map();
+
+ // When a swatch is clicked, and for as long as the tooltip is shown, the
+ // activeSwatch property will hold the reference to the swatch DOM element
+ // that was clicked
+ this.activeSwatch = null;
+
+ this._onSwatchClick = this._onSwatchClick.bind(this);
+}
+
+SwatchBasedEditorTooltip.prototype = {
+ /**
+ * Show the editor tooltip for the currently active swatch.
+ *
+ * @return {Promise} a promise that resolves once the editor tooltip is displayed, or
+ * immediately if there is no currently active swatch.
+ */
+ show: function () {
+ if (this.activeSwatch) {
+ let onShown = this.tooltip.once("shown");
+ this.tooltip.show(this.activeSwatch, "topcenter bottomleft");
+
+ // When the tooltip is closed by clicking outside the panel we want to
+ // commit any changes.
+ this.tooltip.once("hidden", () => {
+ if (!this._reverted && !this.eyedropperOpen) {
+ this.commit();
+ }
+ this._reverted = false;
+
+ // Once the tooltip is hidden we need to clean up any remaining objects.
+ if (!this.eyedropperOpen) {
+ this.activeSwatch = null;
+ }
+ });
+
+ return onShown;
+ }
+
+ return Promise.resolve();
+ },
+
+ hide: function () {
+ this.tooltip.hide();
+ },
+
+ /**
+ * Add a new swatch DOM element to the list of swatch elements this editor
+ * tooltip knows about. That means from now on, clicking on that swatch will
+ * toggle the editor.
+ *
+ * @param {node} swatchEl
+ * The element to add
+ * @param {object} callbacks
+ * Callbacks that will be executed when the editor wants to preview a
+ * value change, or revert a change, or commit a change.
+ * - onShow: will be called when one of the swatch tooltip is shown
+ * - onPreview: will be called when one of the sub-classes calls
+ * preview
+ * - onRevert: will be called when the user ESCapes out of the tooltip
+ * - onCommit: will be called when the user presses ENTER or clicks
+ * outside the tooltip.
+ */
+ addSwatch: function (swatchEl, callbacks = {}) {
+ if (!callbacks.onShow) {
+ callbacks.onShow = function () {};
+ }
+ if (!callbacks.onPreview) {
+ callbacks.onPreview = function () {};
+ }
+ if (!callbacks.onRevert) {
+ callbacks.onRevert = function () {};
+ }
+ if (!callbacks.onCommit) {
+ callbacks.onCommit = function () {};
+ }
+
+ this.swatches.set(swatchEl, {
+ callbacks: callbacks
+ });
+ swatchEl.addEventListener("click", this._onSwatchClick, false);
+ },
+
+ removeSwatch: function (swatchEl) {
+ if (this.swatches.has(swatchEl)) {
+ if (this.activeSwatch === swatchEl) {
+ this.hide();
+ this.activeSwatch = null;
+ }
+ swatchEl.removeEventListener("click", this._onSwatchClick, false);
+ this.swatches.delete(swatchEl);
+ }
+ },
+
+ _onSwatchClick: function (event) {
+ let swatch = this.swatches.get(event.target);
+
+ if (event.shiftKey) {
+ event.stopPropagation();
+ return;
+ }
+ if (swatch) {
+ this.activeSwatch = event.target;
+ this.show();
+ swatch.callbacks.onShow();
+ event.stopPropagation();
+ }
+ },
+
+ /**
+ * Not called by this parent class, needs to be taken care of by sub-classes
+ */
+ preview: function (value) {
+ if (this.activeSwatch) {
+ let swatch = this.swatches.get(this.activeSwatch);
+ swatch.callbacks.onPreview(value);
+ }
+ },
+
+ /**
+ * This parent class only calls this on <esc> keypress
+ */
+ revert: function () {
+ if (this.activeSwatch) {
+ this._reverted = true;
+ let swatch = this.swatches.get(this.activeSwatch);
+ this.tooltip.once("hidden", () => {
+ swatch.callbacks.onRevert();
+ });
+ }
+ },
+
+ /**
+ * This parent class only calls this on <enter> keypress
+ */
+ commit: function () {
+ if (this.activeSwatch) {
+ let swatch = this.swatches.get(this.activeSwatch);
+ swatch.callbacks.onCommit();
+ }
+ },
+
+ destroy: function () {
+ this.swatches.clear();
+ this.activeSwatch = null;
+ this.tooltip.off("keypress", this._onTooltipKeypress);
+ this.tooltip.destroy();
+ this.shortcuts.destroy();
+ }
+};
+
+module.exports = SwatchBasedEditorTooltip;
diff --git a/devtools/client/shared/widgets/tooltip/SwatchColorPickerTooltip.js b/devtools/client/shared/widgets/tooltip/SwatchColorPickerTooltip.js
new file mode 100644
index 000000000..bf211b8b9
--- /dev/null
+++ b/devtools/client/shared/widgets/tooltip/SwatchColorPickerTooltip.js
@@ -0,0 +1,182 @@
+/* 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 {Task} = require("devtools/shared/task");
+const {colorUtils} = require("devtools/shared/css/color");
+const {Spectrum} = require("devtools/client/shared/widgets/Spectrum");
+const SwatchBasedEditorTooltip = require("devtools/client/shared/widgets/tooltip/SwatchBasedEditorTooltip");
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper("devtools/client/locales/inspector.properties");
+
+const Heritage = require("sdk/core/heritage");
+
+const XHTML_NS = "http://www.w3.org/1999/xhtml";
+
+/**
+ * The swatch color picker tooltip class is a specific class meant to be used
+ * along with output-parser's generated color swatches.
+ * It extends the parent SwatchBasedEditorTooltip class.
+ * It just wraps a standard Tooltip and sets its content with an instance of a
+ * color picker.
+ *
+ * @param {Document} document
+ * The document to attach the SwatchColorPickerTooltip. This is either the toolbox
+ * document if the tooltip is a popup tooltip or the panel's document if it is an
+ * inline editor.
+ * @param {InspectorPanel} inspector
+ * The inspector panel, needed for the eyedropper.
+ */
+function SwatchColorPickerTooltip(document, inspector) {
+ let stylesheet = "chrome://devtools/content/shared/widgets/spectrum.css";
+ SwatchBasedEditorTooltip.call(this, document, stylesheet);
+
+ this.inspector = inspector;
+
+ // Creating a spectrum instance. this.spectrum will always be a promise that
+ // resolves to the spectrum instance
+ this.spectrum = this.setColorPickerContent([0, 0, 0, 1]);
+ this._onSpectrumColorChange = this._onSpectrumColorChange.bind(this);
+ this._openEyeDropper = this._openEyeDropper.bind(this);
+}
+
+SwatchColorPickerTooltip.prototype = Heritage.extend(SwatchBasedEditorTooltip.prototype, {
+ /**
+ * Fill the tooltip with a new instance of the spectrum color picker widget
+ * initialized with the given color, and return the instance of spectrum
+ */
+ setColorPickerContent: function (color) {
+ let { doc } = this.tooltip;
+
+ let container = doc.createElementNS(XHTML_NS, "div");
+ container.id = "spectrum-tooltip";
+ let spectrumNode = doc.createElementNS(XHTML_NS, "div");
+ spectrumNode.id = "spectrum";
+ container.appendChild(spectrumNode);
+ let eyedropper = doc.createElementNS(XHTML_NS, "button");
+ eyedropper.id = "eyedropper-button";
+ eyedropper.className = "devtools-button";
+ /* pointerEvents for eyedropper has to be set auto to display tooltip when
+ * eyedropper is disabled in non-HTML documents.
+ */
+ eyedropper.style.pointerEvents = "auto";
+ container.appendChild(eyedropper);
+
+ this.tooltip.setContent(container, { width: 218, height: 224 });
+
+ let spectrum = new Spectrum(spectrumNode, color);
+
+ // Wait for the tooltip to be shown before calling spectrum.show
+ // as it expect to be visible in order to compute DOM element sizes.
+ this.tooltip.once("shown", () => {
+ spectrum.show();
+ });
+
+ return spectrum;
+ },
+
+ /**
+ * Overriding the SwatchBasedEditorTooltip.show function to set spectrum's
+ * color.
+ */
+ show: Task.async(function* () {
+ // Call then parent class' show function
+ yield SwatchBasedEditorTooltip.prototype.show.call(this);
+ // Then set spectrum's color and listen to color changes to preview them
+ if (this.activeSwatch) {
+ this.currentSwatchColor = this.activeSwatch.nextSibling;
+ this._originalColor = this.currentSwatchColor.textContent;
+ let color = this.activeSwatch.style.backgroundColor;
+ this.spectrum.off("changed", this._onSpectrumColorChange);
+ this.spectrum.rgb = this._colorToRgba(color);
+ this.spectrum.on("changed", this._onSpectrumColorChange);
+ this.spectrum.updateUI();
+ }
+
+ let {target} = this.inspector;
+ target.actorHasMethod("inspector", "pickColorFromPage").then(value => {
+ let tooltipDoc = this.tooltip.doc;
+ let eyeButton = tooltipDoc.querySelector("#eyedropper-button");
+ if (value && this.inspector.selection.nodeFront.isInHTMLDocument) {
+ eyeButton.disabled = false;
+ eyeButton.removeAttribute("title");
+ eyeButton.addEventListener("click", this._openEyeDropper);
+ } else {
+ eyeButton.disabled = true;
+ eyeButton.title = L10N.getStr("eyedropper.disabled.title");
+ }
+ this.emit("ready");
+ }, e => console.error(e));
+ }),
+
+ _onSpectrumColorChange: function (event, rgba, cssColor) {
+ this._selectColor(cssColor);
+ },
+
+ _selectColor: function (color) {
+ if (this.activeSwatch) {
+ this.activeSwatch.style.backgroundColor = color;
+ this.activeSwatch.parentNode.dataset.color = color;
+
+ color = this._toDefaultType(color);
+ this.currentSwatchColor.textContent = color;
+ this.preview(color);
+
+ if (this.eyedropperOpen) {
+ this.commit();
+ }
+ }
+ },
+
+ _openEyeDropper: function () {
+ let {inspector, toolbox, telemetry} = this.inspector;
+ telemetry.toolOpened("pickereyedropper");
+ inspector.pickColorFromPage(toolbox, {copyOnSelect: false}).then(() => {
+ this.eyedropperOpen = true;
+
+ // close the colorpicker tooltip so that only the eyedropper is open.
+ this.hide();
+
+ this.tooltip.emit("eyedropper-opened");
+ }, e => console.error(e));
+
+ inspector.once("color-picked", color => {
+ toolbox.win.focus();
+ this._selectColor(color);
+ this._onEyeDropperDone();
+ });
+
+ inspector.once("color-pick-canceled", () => {
+ this._onEyeDropperDone();
+ });
+ },
+
+ _onEyeDropperDone: function () {
+ this.eyedropperOpen = false;
+ this.activeSwatch = null;
+ },
+
+ _colorToRgba: function (color) {
+ color = new colorUtils.CssColor(color);
+ let rgba = color._getRGBATuple();
+ return [rgba.r, rgba.g, rgba.b, rgba.a];
+ },
+
+ _toDefaultType: function (color) {
+ let colorObj = new colorUtils.CssColor(color);
+ colorObj.setAuthoredUnitFromColor(this._originalColor);
+ return colorObj.toString();
+ },
+
+ destroy: function () {
+ SwatchBasedEditorTooltip.prototype.destroy.call(this);
+ this.inspector = null;
+ this.currentSwatchColor = null;
+ this.spectrum.off("changed", this._onSpectrumColorChange);
+ this.spectrum.destroy();
+ }
+});
+
+module.exports = SwatchColorPickerTooltip;
diff --git a/devtools/client/shared/widgets/tooltip/SwatchCubicBezierTooltip.js b/devtools/client/shared/widgets/tooltip/SwatchCubicBezierTooltip.js
new file mode 100644
index 000000000..02f6fbea4
--- /dev/null
+++ b/devtools/client/shared/widgets/tooltip/SwatchCubicBezierTooltip.js
@@ -0,0 +1,102 @@
+/* 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 defer = require("devtools/shared/defer");
+const {Task} = require("devtools/shared/task");
+const {CubicBezierWidget} = require("devtools/client/shared/widgets/CubicBezierWidget");
+const SwatchBasedEditorTooltip = require("devtools/client/shared/widgets/tooltip/SwatchBasedEditorTooltip");
+
+const Heritage = require("sdk/core/heritage");
+
+const XHTML_NS = "http://www.w3.org/1999/xhtml";
+
+/**
+ * The swatch cubic-bezier tooltip class is a specific class meant to be used
+ * along with rule-view's generated cubic-bezier swatches.
+ * It extends the parent SwatchBasedEditorTooltip class.
+ * It just wraps a standard Tooltip and sets its content with an instance of a
+ * CubicBezierWidget.
+ *
+ * @param {Document} document
+ * The document to attach the SwatchCubicBezierTooltip. This is either the toolbox
+ * document if the tooltip is a popup tooltip or the panel's document if it is an
+ * inline editor.
+ */
+function SwatchCubicBezierTooltip(document) {
+ let stylesheet = "chrome://devtools/content/shared/widgets/cubic-bezier.css";
+ SwatchBasedEditorTooltip.call(this, document, stylesheet);
+
+ // Creating a cubic-bezier instance.
+ // this.widget will always be a promise that resolves to the widget instance
+ this.widget = this.setCubicBezierContent([0, 0, 1, 1]);
+ this._onUpdate = this._onUpdate.bind(this);
+}
+
+SwatchCubicBezierTooltip.prototype = Heritage.extend(SwatchBasedEditorTooltip.prototype, {
+ /**
+ * Fill the tooltip with a new instance of the cubic-bezier widget
+ * initialized with the given value, and return a promise that resolves to
+ * the instance of the widget
+ */
+ setCubicBezierContent: function (bezier) {
+ let { doc } = this.tooltip;
+
+ let container = doc.createElementNS(XHTML_NS, "div");
+ container.className = "cubic-bezier-container";
+
+ this.tooltip.setContent(container, { width: 510, height: 370 });
+
+ let def = defer();
+
+ // Wait for the tooltip to be shown before calling instanciating the widget
+ // as it expect its DOM elements to be visible.
+ this.tooltip.once("shown", () => {
+ let widget = new CubicBezierWidget(container, bezier);
+ def.resolve(widget);
+ });
+
+ return def.promise;
+ },
+
+ /**
+ * Overriding the SwatchBasedEditorTooltip.show function to set the cubic
+ * bezier curve in the widget
+ */
+ show: Task.async(function* () {
+ // Call the parent class' show function
+ yield SwatchBasedEditorTooltip.prototype.show.call(this);
+ // Then set the curve and listen to changes to preview them
+ if (this.activeSwatch) {
+ this.currentBezierValue = this.activeSwatch.nextSibling;
+ this.widget.then(widget => {
+ widget.off("updated", this._onUpdate);
+ widget.cssCubicBezierValue = this.currentBezierValue.textContent;
+ widget.on("updated", this._onUpdate);
+ this.emit("ready");
+ });
+ }
+ }),
+
+ _onUpdate: function (event, bezier) {
+ if (!this.activeSwatch) {
+ return;
+ }
+
+ this.currentBezierValue.textContent = bezier + "";
+ this.preview(bezier + "");
+ },
+
+ destroy: function () {
+ SwatchBasedEditorTooltip.prototype.destroy.call(this);
+ this.currentBezierValue = null;
+ this.widget.then(widget => {
+ widget.off("updated", this._onUpdate);
+ widget.destroy();
+ });
+ }
+});
+
+module.exports = SwatchCubicBezierTooltip;
diff --git a/devtools/client/shared/widgets/tooltip/SwatchFilterTooltip.js b/devtools/client/shared/widgets/tooltip/SwatchFilterTooltip.js
new file mode 100644
index 000000000..bc69c3b70
--- /dev/null
+++ b/devtools/client/shared/widgets/tooltip/SwatchFilterTooltip.js
@@ -0,0 +1,116 @@
+/* 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 {Task} = require("devtools/shared/task");
+const {CSSFilterEditorWidget} = require("devtools/client/shared/widgets/FilterWidget");
+const SwatchBasedEditorTooltip = require("devtools/client/shared/widgets/tooltip/SwatchBasedEditorTooltip");
+
+const Heritage = require("sdk/core/heritage");
+
+const XHTML_NS = "http://www.w3.org/1999/xhtml";
+
+/**
+ * The swatch-based css filter tooltip class is a specific class meant to be
+ * used along with rule-view's generated css filter swatches.
+ * It extends the parent SwatchBasedEditorTooltip class.
+ * It just wraps a standard Tooltip and sets its content with an instance of a
+ * CSSFilterEditorWidget.
+ *
+ * @param {Document} document
+ * The document to attach the SwatchFilterTooltip. This is either the toolbox
+ * document if the tooltip is a popup tooltip or the panel's document if it is an
+ * inline editor.
+ * @param {function} cssIsValid
+ * A function to check that css declaration's name and values are valid together.
+ * This can be obtained from "shared/fronts/css-properties.js".
+ */
+function SwatchFilterTooltip(document, cssIsValid) {
+ let stylesheet = "chrome://devtools/content/shared/widgets/filter-widget.css";
+ SwatchBasedEditorTooltip.call(this, document, stylesheet);
+ this._cssIsValid = cssIsValid;
+
+ // Creating a filter editor instance.
+ this.widget = this.setFilterContent("none");
+ this._onUpdate = this._onUpdate.bind(this);
+}
+
+SwatchFilterTooltip.prototype = Heritage.extend(SwatchBasedEditorTooltip.prototype, {
+ /**
+ * Fill the tooltip with a new instance of the CSSFilterEditorWidget
+ * widget initialized with the given filter value, and return a promise
+ * that resolves to the instance of the widget when ready.
+ */
+ setFilterContent: function (filter) {
+ let { doc } = this.tooltip;
+
+ let container = doc.createElementNS(XHTML_NS, "div");
+ container.id = "filter-container";
+
+ this.tooltip.setContent(container, { width: 510, height: 200 });
+
+ return new CSSFilterEditorWidget(container, filter, this._cssIsValid);
+ },
+
+ show: Task.async(function* () {
+ // Call the parent class' show function
+ yield SwatchBasedEditorTooltip.prototype.show.call(this);
+ // Then set the filter value and listen to changes to preview them
+ if (this.activeSwatch) {
+ this.currentFilterValue = this.activeSwatch.nextSibling;
+ this.widget.off("updated", this._onUpdate);
+ this.widget.on("updated", this._onUpdate);
+ this.widget.setCssValue(this.currentFilterValue.textContent);
+ this.widget.render();
+ this.emit("ready");
+ }
+ }),
+
+ _onUpdate: function (event, filters) {
+ if (!this.activeSwatch) {
+ return;
+ }
+
+ // Remove the old children and reparse the property value to
+ // recompute them.
+ while (this.currentFilterValue.firstChild) {
+ this.currentFilterValue.firstChild.remove();
+ }
+ let node = this._parser.parseCssProperty("filter", filters, this._options);
+ this.currentFilterValue.appendChild(node);
+
+ this.preview();
+ },
+
+ destroy: function () {
+ SwatchBasedEditorTooltip.prototype.destroy.call(this);
+ this.currentFilterValue = null;
+ this.widget.off("updated", this._onUpdate);
+ this.widget.destroy();
+ },
+
+ /**
+ * Like SwatchBasedEditorTooltip.addSwatch, but accepts a parser object
+ * to use when previewing the updated property value.
+ *
+ * @param {node} swatchEl
+ * @see SwatchBasedEditorTooltip.addSwatch
+ * @param {object} callbacks
+ * @see SwatchBasedEditorTooltip.addSwatch
+ * @param {object} parser
+ * A parser object; @see OutputParser object
+ * @param {object} options
+ * options to pass to the output parser, with
+ * the option |filterSwatch| set.
+ */
+ addSwatch: function (swatchEl, callbacks, parser, options) {
+ SwatchBasedEditorTooltip.prototype.addSwatch.call(this, swatchEl,
+ callbacks);
+ this._parser = parser;
+ this._options = options;
+ }
+});
+
+module.exports = SwatchFilterTooltip;
diff --git a/devtools/client/shared/widgets/tooltip/Tooltip.js b/devtools/client/shared/widgets/tooltip/Tooltip.js
new file mode 100644
index 000000000..c3c365152
--- /dev/null
+++ b/devtools/client/shared/widgets/tooltip/Tooltip.js
@@ -0,0 +1,410 @@
+/* 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 defer = require("devtools/shared/defer");
+const EventEmitter = require("devtools/shared/event-emitter");
+const {KeyCodes} = require("devtools/client/shared/keycodes");
+const {TooltipToggle} = require("devtools/client/shared/widgets/tooltip/TooltipToggle");
+
+const XHTML_NS = "http://www.w3.org/1999/xhtml";
+const ESCAPE_KEYCODE = KeyCodes.DOM_VK_ESCAPE;
+const POPUP_EVENTS = ["shown", "hidden", "showing", "hiding"];
+
+/**
+ * Tooltip widget.
+ *
+ * This widget is intended at any tool that may need to show rich content in the
+ * form of floating panels.
+ * A common use case is image previewing in the CSS rule view, but more complex
+ * use cases may include color pickers, object inspection, etc...
+ *
+ * Tooltips are based on XUL (namely XUL arrow-type <panel>s), and therefore
+ * need a XUL Document to live in.
+ * This is pretty much the only requirement they have on their environment.
+ *
+ * The way to use a tooltip is simply by instantiating a tooltip yourself and
+ * attaching some content in it, or using one of the ready-made content types.
+ *
+ * A convenient `startTogglingOnHover` method may avoid having to register event
+ * handlers yourself if the tooltip has to be shown when hovering over a
+ * specific element or group of elements (which is usually the most common case)
+ */
+
+/**
+ * Tooltip class.
+ *
+ * Basic usage:
+ * let t = new Tooltip(xulDoc);
+ * t.content = someXulContent;
+ * t.show();
+ * t.hide();
+ * t.destroy();
+ *
+ * Better usage:
+ * let t = new Tooltip(xulDoc);
+ * t.startTogglingOnHover(container, target => {
+ * if (<condition based on target>) {
+ * t.content = el;
+ * return true;
+ * }
+ * });
+ * t.destroy();
+ *
+ * @param {XULDocument} doc
+ * The XUL document hosting this tooltip
+ * @param {Object} options
+ * Optional options that give options to consumers:
+ * - consumeOutsideClick {Boolean} Wether the first click outside of the
+ * tooltip should close the tooltip and be consumed or not.
+ * Defaults to false.
+ * - closeOnKeys {Array} An array of key codes that should close the
+ * tooltip. Defaults to [27] (escape key).
+ * - closeOnEvents [{emitter: {Object}, event: {String},
+ * useCapture: {Boolean}}]
+ * Provide an optional list of emitter objects and event names here to
+ * trigger the closing of the tooltip when these events are fired by the
+ * emitters. The emitter objects should either implement
+ * on/off(event, cb) or addEventListener/removeEventListener(event, cb).
+ * Defaults to [].
+ * For instance, the following would close the tooltip whenever the
+ * toolbox selects a new tool and when a DOM node gets scrolled:
+ * new Tooltip(doc, {
+ * closeOnEvents: [
+ * {emitter: toolbox, event: "select"},
+ * {emitter: myContainer, event: "scroll", useCapture: true}
+ * ]
+ * });
+ * - noAutoFocus {Boolean} Should the focus automatically go to the panel
+ * when it opens. Defaults to true.
+ *
+ * Fires these events:
+ * - showing : just before the tooltip shows
+ * - shown : when the tooltip is shown
+ * - hiding : just before the tooltip closes
+ * - hidden : when the tooltip gets hidden
+ * - keypress : when any key gets pressed, with keyCode
+ */
+function Tooltip(doc, {
+ consumeOutsideClick = false,
+ closeOnKeys = [ESCAPE_KEYCODE],
+ noAutoFocus = true,
+ closeOnEvents = [],
+ } = {}) {
+ EventEmitter.decorate(this);
+
+ this.doc = doc;
+ this.consumeOutsideClick = consumeOutsideClick;
+ this.closeOnKeys = closeOnKeys;
+ this.noAutoFocus = noAutoFocus;
+ this.closeOnEvents = closeOnEvents;
+
+ this.panel = this._createPanel();
+
+ // Create tooltip toggle helper and decorate the Tooltip instance with
+ // shortcut methods.
+ this._toggle = new TooltipToggle(this);
+ this.startTogglingOnHover = this._toggle.start.bind(this._toggle);
+ this.stopTogglingOnHover = this._toggle.stop.bind(this._toggle);
+
+ // Emit show/hide events when the panel does.
+ for (let eventName of POPUP_EVENTS) {
+ this["_onPopup" + eventName] = (name => {
+ return e => {
+ if (e.target === this.panel) {
+ this.emit(name);
+ }
+ };
+ })(eventName);
+ this.panel.addEventListener("popup" + eventName,
+ this["_onPopup" + eventName], false);
+ }
+
+ // Listen to keypress events to close the tooltip if configured to do so
+ let win = this.doc.querySelector("window");
+ this._onKeyPress = event => {
+ if (this.panel.hidden) {
+ return;
+ }
+
+ this.emit("keypress", event.keyCode);
+ if (this.closeOnKeys.indexOf(event.keyCode) !== -1 &&
+ this.isShown()) {
+ event.stopPropagation();
+ this.hide();
+ }
+ };
+ win.addEventListener("keypress", this._onKeyPress, false);
+
+ // Listen to custom emitters' events to close the tooltip
+ this.hide = this.hide.bind(this);
+ for (let {emitter, event, useCapture} of this.closeOnEvents) {
+ for (let add of ["addEventListener", "on"]) {
+ if (add in emitter) {
+ emitter[add](event, this.hide, useCapture);
+ break;
+ }
+ }
+ }
+}
+
+Tooltip.prototype = {
+ defaultPosition: "before_start",
+ // px
+ defaultOffsetX: 0,
+ // px
+ defaultOffsetY: 0,
+ // px
+
+ /**
+ * Show the tooltip. It might be wise to append some content first if you
+ * don't want the tooltip to be empty. You may access the content of the
+ * tooltip by setting a XUL node to t.content.
+ * @param {node} anchor
+ * Which node should the tooltip be shown on
+ * @param {string} position [optional]
+ * Optional tooltip position. Defaults to before_start
+ * https://developer.mozilla.org/en-US/docs/XUL/PopupGuide/Positioning
+ * @param {number} x, y [optional]
+ * The left and top offset coordinates, in pixels.
+ */
+ show: function (anchor,
+ position = this.defaultPosition,
+ x = this.defaultOffsetX,
+ y = this.defaultOffsetY) {
+ this.panel.hidden = false;
+ this.panel.openPopup(anchor, position, x, y);
+ },
+
+ /**
+ * Hide the tooltip
+ */
+ hide: function () {
+ this.panel.hidden = true;
+ this.panel.hidePopup();
+ },
+
+ isShown: function () {
+ return this.panel &&
+ this.panel.state !== "closed" &&
+ this.panel.state !== "hiding";
+ },
+
+ setSize: function (width, height) {
+ this.panel.sizeTo(width, height);
+ },
+
+ /**
+ * Empty the tooltip's content
+ */
+ empty: function () {
+ while (this.panel.hasChildNodes()) {
+ this.panel.removeChild(this.panel.firstChild);
+ }
+ },
+
+ /**
+ * Gets this panel's visibility state.
+ * @return boolean
+ */
+ isHidden: function () {
+ return this.panel.state == "closed" || this.panel.state == "hiding";
+ },
+
+ /**
+ * Gets if this panel has any child nodes.
+ * @return boolean
+ */
+ isEmpty: function () {
+ return !this.panel.hasChildNodes();
+ },
+
+ /**
+ * Get rid of references and event listeners
+ */
+ destroy: function () {
+ this.hide();
+
+ for (let eventName of POPUP_EVENTS) {
+ this.panel.removeEventListener("popup" + eventName,
+ this["_onPopup" + eventName], false);
+ }
+
+ let win = this.doc.querySelector("window");
+ win.removeEventListener("keypress", this._onKeyPress, false);
+
+ for (let {emitter, event, useCapture} of this.closeOnEvents) {
+ for (let remove of ["removeEventListener", "off"]) {
+ if (remove in emitter) {
+ emitter[remove](event, this.hide, useCapture);
+ break;
+ }
+ }
+ }
+
+ this.content = null;
+
+ this._toggle.destroy();
+
+ this.doc = null;
+
+ this.panel.remove();
+ this.panel = null;
+ },
+
+ /**
+ * Returns the outer container node (that includes the arrow etc.). Happens
+ * to be identical to this.panel here, can be different element in other
+ * Tooltip implementations.
+ */
+ get container() {
+ return this.panel;
+ },
+
+ /**
+ * Set the content of this tooltip. Will first empty the tooltip and then
+ * append the new content element.
+ * Consider using one of the set<type>Content() functions instead.
+ * @param {node} content
+ * A node that can be appended in the tooltip XUL element
+ */
+ set content(content) {
+ if (this.content == content) {
+ return;
+ }
+
+ this.empty();
+ this.panel.removeAttribute("clamped-dimensions");
+ this.panel.removeAttribute("clamped-dimensions-no-min-height");
+ this.panel.removeAttribute("clamped-dimensions-no-max-or-min-height");
+ this.panel.removeAttribute("wide");
+
+ if (content) {
+ this.panel.appendChild(content);
+ }
+ },
+
+ get content() {
+ return this.panel.firstChild;
+ },
+
+ /**
+ * Sets some text as the content of this tooltip.
+ *
+ * @param {array} messages
+ * A list of text messages.
+ * @param {string} messagesClass [optional]
+ * A style class for the text messages.
+ * @param {string} containerClass [optional]
+ * A style class for the text messages container.
+ */
+ setTextContent: function (
+ {
+ messages,
+ messagesClass,
+ containerClass
+ },
+ extraButtons = []) {
+ messagesClass = messagesClass || "default-tooltip-simple-text-colors";
+ containerClass = containerClass || "default-tooltip-simple-text-colors";
+
+ let vbox = this.doc.createElement("vbox");
+ vbox.className = "devtools-tooltip-simple-text-container " + containerClass;
+ vbox.setAttribute("flex", "1");
+
+ for (let text of messages) {
+ let description = this.doc.createElement("description");
+ description.setAttribute("flex", "1");
+ description.className = "devtools-tooltip-simple-text " + messagesClass;
+ description.textContent = text;
+ vbox.appendChild(description);
+ }
+
+ for (let { label, className, command } of extraButtons) {
+ let button = this.doc.createElement("button");
+ button.className = className;
+ button.setAttribute("label", label);
+ button.addEventListener("command", command);
+ vbox.appendChild(button);
+ }
+
+ this.content = vbox;
+ },
+
+ /**
+ * Load a document into an iframe, and set the iframe
+ * to be the tooltip's content.
+ *
+ * Used by tooltips that want to load their interface
+ * into an iframe from a URL.
+ *
+ * @param {string} width
+ * Width of the iframe.
+ * @param {string} height
+ * Height of the iframe.
+ * @param {string} url
+ * URL of the document to load into the iframe.
+ *
+ * @return {promise} A promise which is resolved with
+ * the iframe.
+ *
+ * This function creates an iframe, loads the specified document
+ * into it, sets the tooltip's content to the iframe, and returns
+ * a promise.
+ *
+ * When the document is loaded, the function gets the content window
+ * and resolves the promise with the content window.
+ */
+ setIFrameContent: function ({width, height}, url) {
+ let def = defer();
+
+ // Create an iframe
+ let iframe = this.doc.createElementNS(XHTML_NS, "iframe");
+ iframe.setAttribute("transparent", true);
+ iframe.setAttribute("width", width);
+ iframe.setAttribute("height", height);
+ iframe.setAttribute("flex", "1");
+ iframe.setAttribute("tooltip", "aHTMLTooltip");
+ iframe.setAttribute("class", "devtools-tooltip-iframe");
+
+ // Wait for the load to initialize the widget
+ function onLoad() {
+ iframe.removeEventListener("load", onLoad, true);
+ def.resolve(iframe);
+ }
+ iframe.addEventListener("load", onLoad, true);
+
+ // load the document from url into the iframe
+ iframe.setAttribute("src", url);
+
+ // Put the iframe in the tooltip
+ this.content = iframe;
+
+ return def.promise;
+ },
+
+ /**
+ * Create the tooltip panel
+ */
+ _createPanel() {
+ let panel = this.doc.createElement("panel");
+ panel.setAttribute("hidden", true);
+ panel.setAttribute("ignorekeys", true);
+ panel.setAttribute("animate", false);
+
+ panel.setAttribute("consumeoutsideclicks",
+ this.consumeOutsideClick);
+ panel.setAttribute("noautofocus", this.noAutoFocus);
+ panel.setAttribute("type", "arrow");
+ panel.setAttribute("level", "top");
+
+ panel.setAttribute("class", "devtools-tooltip theme-tooltip-panel");
+ this.doc.querySelector("window").appendChild(panel);
+
+ return panel;
+ }
+};
+
+module.exports = Tooltip;
diff --git a/devtools/client/shared/widgets/tooltip/TooltipToggle.js b/devtools/client/shared/widgets/tooltip/TooltipToggle.js
new file mode 100644
index 000000000..b53664f34
--- /dev/null
+++ b/devtools/client/shared/widgets/tooltip/TooltipToggle.js
@@ -0,0 +1,182 @@
+/* -*- 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 {Task} = require("devtools/shared/task");
+
+const DEFAULT_TOGGLE_DELAY = 50;
+
+/**
+ * Tooltip helper designed to show/hide the tooltip when the mouse hovers over
+ * particular nodes.
+ *
+ * This works by tracking mouse movements on a base container node (baseNode)
+ * and showing the tooltip when the mouse stops moving. A callback can be
+ * provided to the start() method to know whether or not the node being
+ * hovered over should indeed receive the tooltip.
+ */
+function TooltipToggle(tooltip) {
+ this.tooltip = tooltip;
+ this.win = tooltip.doc.defaultView;
+
+ this._onMouseMove = this._onMouseMove.bind(this);
+ this._onMouseOut = this._onMouseOut.bind(this);
+
+ this._onTooltipMouseOver = this._onTooltipMouseOver.bind(this);
+ this._onTooltipMouseOut = this._onTooltipMouseOut.bind(this);
+}
+
+module.exports.TooltipToggle = TooltipToggle;
+
+TooltipToggle.prototype = {
+ /**
+ * Start tracking mouse movements on the provided baseNode to show the
+ * tooltip.
+ *
+ * 2 Ways to make this work:
+ * - Provide a single node to attach the tooltip to, as the baseNode, and
+ * omit the second targetNodeCb argument
+ * - Provide a baseNode that is the container of possibly numerous children
+ * elements that may receive a tooltip. In this case, provide the second
+ * targetNodeCb argument to decide wether or not a child should receive
+ * a tooltip.
+ *
+ * Note that if you call this function a second time, it will itself call
+ * stop() before adding mouse tracking listeners again.
+ *
+ * @param {node} baseNode
+ * The container for all target nodes
+ * @param {Function} targetNodeCb
+ * A function that accepts a node argument and that checks if a tooltip
+ * should be displayed. Possible return values are:
+ * - false (or a falsy value) if the tooltip should not be displayed
+ * - true if the tooltip should be displayed
+ * - a DOM node to display the tooltip on the returned anchor
+ * The function can also return a promise that will resolve to one of
+ * the values listed above.
+ * If omitted, the tooltip will be shown everytime.
+ * @param {Object} options
+ Set of optional arguments:
+ * - {Number} toggleDelay
+ * An optional delay (in ms) that will be observed before showing
+ * and before hiding the tooltip. Defaults to DEFAULT_TOGGLE_DELAY.
+ * - {Boolean} interactive
+ * If enabled, the tooltip is not hidden when mouse leaves the
+ * target element and enters the tooltip. Allows the tooltip
+ * content to be interactive.
+ */
+ start: function (baseNode, targetNodeCb,
+ {toggleDelay = DEFAULT_TOGGLE_DELAY, interactive = false} = {}) {
+ this.stop();
+
+ if (!baseNode) {
+ // Calling tool is in the process of being destroyed.
+ return;
+ }
+
+ this._baseNode = baseNode;
+ this._targetNodeCb = targetNodeCb || (() => true);
+ this._toggleDelay = toggleDelay;
+ this._interactive = interactive;
+
+ baseNode.addEventListener("mousemove", this._onMouseMove);
+ baseNode.addEventListener("mouseout", this._onMouseOut);
+
+ if (this._interactive) {
+ this.tooltip.container.addEventListener("mouseover", this._onTooltipMouseOver);
+ this.tooltip.container.addEventListener("mouseout", this._onTooltipMouseOut);
+ }
+ },
+
+ /**
+ * If the start() function has been used previously, and you want to get rid
+ * of this behavior, then call this function to remove the mouse movement
+ * tracking
+ */
+ stop: function () {
+ this.win.clearTimeout(this.toggleTimer);
+
+ if (!this._baseNode) {
+ return;
+ }
+
+ this._baseNode.removeEventListener("mousemove", this._onMouseMove);
+ this._baseNode.removeEventListener("mouseout", this._onMouseOut);
+
+ if (this._interactive) {
+ this.tooltip.container.removeEventListener("mouseover", this._onTooltipMouseOver);
+ this.tooltip.container.removeEventListener("mouseout", this._onTooltipMouseOut);
+ }
+
+ this._baseNode = null;
+ this._targetNodeCb = null;
+ this._lastHovered = null;
+ },
+
+ _onMouseMove: function (event) {
+ if (event.target !== this._lastHovered) {
+ this._lastHovered = event.target;
+
+ this.win.clearTimeout(this.toggleTimer);
+ this.toggleTimer = this.win.setTimeout(() => {
+ this.tooltip.hide();
+ this.isValidHoverTarget(event.target).then(target => {
+ if (target === null) {
+ return;
+ }
+ this.tooltip.show(target);
+ }, reason => {
+ console.error("isValidHoverTarget rejected with unexpected reason:");
+ console.error(reason);
+ });
+ }, this._toggleDelay);
+ }
+ },
+
+ /**
+ * Is the given target DOMNode a valid node for toggling the tooltip on hover.
+ * This delegates to the user-defined _targetNodeCb callback.
+ * @return {Promise} a promise that will resolve the anchor to use for the
+ * tooltip or null if no valid target was found.
+ */
+ isValidHoverTarget: Task.async(function* (target) {
+ let res = yield this._targetNodeCb(target, this.tooltip);
+ if (res) {
+ return res.nodeName ? res : target;
+ }
+
+ return null;
+ }),
+
+ _onMouseOut: function (event) {
+ // Only hide the tooltip if the mouse leaves baseNode.
+ if (event && this._baseNode && !this._baseNode.contains(event.relatedTarget)) {
+ return;
+ }
+
+ this._lastHovered = null;
+ this.win.clearTimeout(this.toggleTimer);
+ this.toggleTimer = this.win.setTimeout(() => {
+ this.tooltip.hide();
+ }, this._toggleDelay);
+ },
+
+ _onTooltipMouseOver() {
+ this.win.clearTimeout(this.toggleTimer);
+ },
+
+ _onTooltipMouseOut() {
+ this.win.clearTimeout(this.toggleTimer);
+ this.toggleTimer = this.win.setTimeout(() => {
+ this.tooltip.hide();
+ }, this._toggleDelay);
+ },
+
+ destroy: function () {
+ this.stop();
+ }
+};
diff --git a/devtools/client/shared/widgets/tooltip/VariableContentHelper.js b/devtools/client/shared/widgets/tooltip/VariableContentHelper.js
new file mode 100644
index 000000000..4dc02da9b
--- /dev/null
+++ b/devtools/client/shared/widgets/tooltip/VariableContentHelper.js
@@ -0,0 +1,89 @@
+/* -*- 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 {XPCOMUtils} = require("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "VariablesView",
+ "resource://devtools/client/shared/widgets/VariablesView.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "VariablesViewController",
+ "resource://devtools/client/shared/widgets/VariablesViewController.jsm");
+
+/**
+ * Fill the tooltip with a variables view, inspecting an object via its
+ * corresponding object actor, as specified in the remote debugging protocol.
+ *
+ * @param {Tooltip} tooltip
+ * The tooltip to use
+ * @param {object} objectActor
+ * The value grip for the object actor.
+ * @param {object} viewOptions [optional]
+ * Options for the variables view visualization.
+ * @param {object} controllerOptions [optional]
+ * Options for the variables view controller.
+ * @param {object} relayEvents [optional]
+ * A collection of events to listen on the variables view widget.
+ * For example, { fetched: () => ... }
+ * @param {array} extraButtons [optional]
+ * An array of extra buttons to add. Each element of the array
+ * should be of the form {label, className, command}.
+ * @param {Toolbox} toolbox [optional]
+ * Pass the instance of the current toolbox if you want the variables
+ * view widget to allow highlighting and selection of DOM nodes
+ */
+
+function setTooltipVariableContent(tooltip, objectActor,
+ viewOptions = {}, controllerOptions = {},
+ relayEvents = {}, extraButtons = [],
+ toolbox = null) {
+ let doc = tooltip.doc;
+ let vbox = doc.createElement("vbox");
+ vbox.className = "devtools-tooltip-variables-view-box";
+ vbox.setAttribute("flex", "1");
+
+ let innerbox = doc.createElement("vbox");
+ innerbox.className = "devtools-tooltip-variables-view-innerbox";
+ innerbox.setAttribute("flex", "1");
+ vbox.appendChild(innerbox);
+
+ for (let { label, className, command } of extraButtons) {
+ let button = doc.createElement("button");
+ button.className = className;
+ button.setAttribute("label", label);
+ button.addEventListener("command", command);
+ vbox.appendChild(button);
+ }
+
+ let widget = new VariablesView(innerbox, viewOptions);
+
+ // If a toolbox was provided, link it to the vview
+ if (toolbox) {
+ widget.toolbox = toolbox;
+ }
+
+ // Analyzing state history isn't useful with transient object inspectors.
+ widget.commitHierarchy = () => {};
+
+ for (let e in relayEvents) {
+ widget.on(e, relayEvents[e]);
+ }
+ VariablesViewController.attach(widget, controllerOptions);
+
+ // Some of the view options are allowed to change between uses.
+ widget.searchPlaceholder = viewOptions.searchPlaceholder;
+ widget.searchEnabled = viewOptions.searchEnabled;
+
+ // Use the object actor's grip to display it as a variable in the widget.
+ // The controller options are allowed to change between uses.
+ widget.controller.setSingleVariable(
+ { objectActor: objectActor }, controllerOptions);
+
+ tooltip.content = vbox;
+ tooltip.panel.setAttribute("clamped-dimensions", "");
+}
+
+exports.setTooltipVariableContent = setTooltipVariableContent;
diff --git a/devtools/client/shared/widgets/tooltip/moz.build b/devtools/client/shared/widgets/tooltip/moz.build
new file mode 100644
index 000000000..93172227a
--- /dev/null
+++ b/devtools/client/shared/widgets/tooltip/moz.build
@@ -0,0 +1,19 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'CssDocsTooltip.js',
+ 'EventTooltipHelper.js',
+ 'HTMLTooltip.js',
+ 'ImageTooltipHelper.js',
+ 'SwatchBasedEditorTooltip.js',
+ 'SwatchColorPickerTooltip.js',
+ 'SwatchCubicBezierTooltip.js',
+ 'SwatchFilterTooltip.js',
+ 'Tooltip.js',
+ 'TooltipToggle.js',
+ 'VariableContentHelper.js',
+)
diff --git a/devtools/client/shared/widgets/view-helpers.js b/devtools/client/shared/widgets/view-helpers.js
new file mode 100644
index 000000000..4686d4e1c
--- /dev/null
+++ b/devtools/client/shared/widgets/view-helpers.js
@@ -0,0 +1,1625 @@
+/* -*- 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 {KeyCodes} = require("devtools/client/shared/keycodes");
+
+const PANE_APPEARANCE_DELAY = 50;
+const PAGE_SIZE_ITEM_COUNT_RATIO = 5;
+const WIDGET_FOCUSABLE_NODES = new Set(["vbox", "hbox"]);
+
+var namedTimeoutsStore = new Map();
+
+/**
+ * Inheritance helpers from the addon SDK's core/heritage.
+ * Remove these when all devtools are loadered.
+ */
+exports.Heritage = {
+ /**
+ * @see extend in sdk/core/heritage.
+ */
+ extend: function (prototype, properties = {}) {
+ return Object.create(prototype, this.getOwnPropertyDescriptors(properties));
+ },
+
+ /**
+ * @see getOwnPropertyDescriptors in sdk/core/heritage.
+ */
+ getOwnPropertyDescriptors: function (object) {
+ return Object.getOwnPropertyNames(object).reduce((descriptor, name) => {
+ descriptor[name] = Object.getOwnPropertyDescriptor(object, name);
+ return descriptor;
+ }, {});
+ }
+};
+
+/**
+ * Helper for draining a rapid succession of events and invoking a callback
+ * once everything settles down.
+ *
+ * @param string id
+ * A string identifier for the named timeout.
+ * @param number wait
+ * The amount of milliseconds to wait after no more events are fired.
+ * @param function callback
+ * Invoked when no more events are fired after the specified time.
+ */
+const setNamedTimeout = function setNamedTimeout(id, wait, callback) {
+ clearNamedTimeout(id);
+
+ namedTimeoutsStore.set(id, setTimeout(() =>
+ namedTimeoutsStore.delete(id) && callback(), wait));
+};
+exports.setNamedTimeout = setNamedTimeout;
+
+/**
+ * Clears a named timeout.
+ * @see setNamedTimeout
+ *
+ * @param string id
+ * A string identifier for the named timeout.
+ */
+const clearNamedTimeout = function clearNamedTimeout(id) {
+ if (!namedTimeoutsStore) {
+ return;
+ }
+ clearTimeout(namedTimeoutsStore.get(id));
+ namedTimeoutsStore.delete(id);
+};
+exports.clearNamedTimeout = clearNamedTimeout;
+
+/**
+ * Same as `setNamedTimeout`, but invokes the callback only if the provided
+ * predicate function returns true. Otherwise, the timeout is re-triggered.
+ *
+ * @param string id
+ * A string identifier for the conditional timeout.
+ * @param number wait
+ * The amount of milliseconds to wait after no more events are fired.
+ * @param function predicate
+ * The predicate function used to determine whether the timeout restarts.
+ * @param function callback
+ * Invoked when no more events are fired after the specified time, and
+ * the provided predicate function returns true.
+ */
+const setConditionalTimeout = function setConditionalTimeout(id, wait,
+ predicate,
+ callback) {
+ setNamedTimeout(id, wait, function maybeCallback() {
+ if (predicate()) {
+ callback();
+ return;
+ }
+ setConditionalTimeout(id, wait, predicate, callback);
+ });
+};
+exports.setConditionalTimeout = setConditionalTimeout;
+
+/**
+ * Clears a conditional timeout.
+ * @see setConditionalTimeout
+ *
+ * @param string id
+ * A string identifier for the conditional timeout.
+ */
+const clearConditionalTimeout = function clearConditionalTimeout(id) {
+ clearNamedTimeout(id);
+};
+exports.clearConditionalTimeout = clearConditionalTimeout;
+
+/**
+ * Helpers for creating and messaging between UI components.
+ */
+const ViewHelpers = exports.ViewHelpers = {
+ /**
+ * Convenience method, dispatching a custom event.
+ *
+ * @param nsIDOMNode target
+ * A custom target element to dispatch the event from.
+ * @param string type
+ * The name of the event.
+ * @param any detail
+ * The data passed when initializing the event.
+ * @return boolean
+ * True if the event was cancelled or a registered handler
+ * called preventDefault.
+ */
+ dispatchEvent: function (target, type, detail) {
+ if (!(target instanceof Node)) {
+ // Event cancelled.
+ return true;
+ }
+ let document = target.ownerDocument || target;
+ let dispatcher = target.ownerDocument ? target : document.documentElement;
+
+ let event = document.createEvent("CustomEvent");
+ event.initCustomEvent(type, true, true, detail);
+ return dispatcher.dispatchEvent(event);
+ },
+
+ /**
+ * Helper delegating some of the DOM attribute methods of a node to a widget.
+ *
+ * @param object widget
+ * The widget to assign the methods to.
+ * @param nsIDOMNode node
+ * A node to delegate the methods to.
+ */
+ delegateWidgetAttributeMethods: function (widget, node) {
+ widget.getAttribute =
+ widget.getAttribute || node.getAttribute.bind(node);
+ widget.setAttribute =
+ widget.setAttribute || node.setAttribute.bind(node);
+ widget.removeAttribute =
+ widget.removeAttribute || node.removeAttribute.bind(node);
+ },
+
+ /**
+ * Helper delegating some of the DOM event methods of a node to a widget.
+ *
+ * @param object widget
+ * The widget to assign the methods to.
+ * @param nsIDOMNode node
+ * A node to delegate the methods to.
+ */
+ delegateWidgetEventMethods: function (widget, node) {
+ widget.addEventListener =
+ widget.addEventListener || node.addEventListener.bind(node);
+ widget.removeEventListener =
+ widget.removeEventListener || node.removeEventListener.bind(node);
+ },
+
+ /**
+ * Checks if the specified object looks like it's been decorated by an
+ * event emitter.
+ *
+ * @return boolean
+ * True if it looks, walks and quacks like an event emitter.
+ */
+ isEventEmitter: function (object) {
+ return object && object.on && object.off && object.once && object.emit;
+ },
+
+ /**
+ * Checks if the specified object is an instance of a DOM node.
+ *
+ * @return boolean
+ * True if it's a node, false otherwise.
+ */
+ isNode: function (object) {
+ return object instanceof Node ||
+ object instanceof Element ||
+ object instanceof DocumentFragment;
+ },
+
+ /**
+ * Prevents event propagation when navigation keys are pressed.
+ *
+ * @param Event e
+ * The event to be prevented.
+ */
+ preventScrolling: function (e) {
+ switch (e.keyCode) {
+ case KeyCodes.DOM_VK_UP:
+ case KeyCodes.DOM_VK_DOWN:
+ case KeyCodes.DOM_VK_LEFT:
+ case KeyCodes.DOM_VK_RIGHT:
+ case KeyCodes.DOM_VK_PAGE_UP:
+ case KeyCodes.DOM_VK_PAGE_DOWN:
+ case KeyCodes.DOM_VK_HOME:
+ case KeyCodes.DOM_VK_END:
+ e.preventDefault();
+ e.stopPropagation();
+ }
+ },
+
+ /**
+ * Check if the enter key or space was pressed
+ *
+ * @param event event
+ * The event triggered by a keypress on an element
+ */
+ isSpaceOrReturn: function (event) {
+ return event.keyCode === KeyCodes.DOM_VK_SPACE ||
+ event.keyCode === KeyCodes.DOM_VK_RETURN;
+ },
+
+ /**
+ * Sets a toggled pane hidden or visible. The pane can either be displayed on
+ * the side (right or left depending on the locale) or at the bottom.
+ *
+ * @param object flags
+ * An object containing some of the following properties:
+ * - visible: true if the pane should be shown, false to hide
+ * - animated: true to display an animation on toggle
+ * - delayed: true to wait a few cycles before toggle
+ * - callback: a function to invoke when the toggle finishes
+ * @param nsIDOMNode pane
+ * The element representing the pane to toggle.
+ */
+ togglePane: function (flags, pane) {
+ // Make sure a pane is actually available first.
+ if (!pane) {
+ return;
+ }
+
+ // Hiding is always handled via margins, not the hidden attribute.
+ pane.removeAttribute("hidden");
+
+ // Add a class to the pane to handle min-widths, margins and animations.
+ pane.classList.add("generic-toggled-pane");
+
+ // Avoid toggles in the middle of animation.
+ if (pane.hasAttribute("animated")) {
+ return;
+ }
+
+ // Avoid useless toggles.
+ if (flags.visible == !pane.classList.contains("pane-collapsed")) {
+ if (flags.callback) {
+ flags.callback();
+ }
+ return;
+ }
+
+ // The "animated" attributes enables animated toggles (slide in-out).
+ if (flags.animated) {
+ pane.setAttribute("animated", "");
+ } else {
+ pane.removeAttribute("animated");
+ }
+
+ // Computes and sets the pane margins in order to hide or show it.
+ let doToggle = () => {
+ // Negative margins are applied to "right" and "left" to support RTL and
+ // LTR directions, as well as to "bottom" to support vertical layouts.
+ // Unnecessary negative margins are forced to 0 via CSS in widgets.css.
+ if (flags.visible) {
+ pane.style.marginLeft = "0";
+ pane.style.marginRight = "0";
+ pane.style.marginBottom = "0";
+ pane.classList.remove("pane-collapsed");
+ } else {
+ let width = Math.floor(pane.getAttribute("width")) + 1;
+ let height = Math.floor(pane.getAttribute("height")) + 1;
+ pane.style.marginLeft = -width + "px";
+ pane.style.marginRight = -width + "px";
+ pane.style.marginBottom = -height + "px";
+ }
+
+ // Wait for the animation to end before calling afterToggle()
+ if (flags.animated) {
+ let options = {
+ useCapture: false,
+ once: true
+ };
+
+ pane.addEventListener("transitionend", () => {
+ // Prevent unwanted transitions: if the panel is hidden and the layout
+ // changes margins will be updated and the panel will pop out.
+ pane.removeAttribute("animated");
+
+ if (!flags.visible) {
+ pane.classList.add("pane-collapsed");
+ }
+ if (flags.callback) {
+ flags.callback();
+ }
+ }, options);
+ } else {
+ if (!flags.visible) {
+ pane.classList.add("pane-collapsed");
+ }
+
+ // Invoke the callback immediately since there's no transition.
+ if (flags.callback) {
+ flags.callback();
+ }
+ }
+ };
+
+ // Sometimes it's useful delaying the toggle a few ticks to ensure
+ // a smoother slide in-out animation.
+ if (flags.delayed) {
+ pane.ownerDocument.defaultView.setTimeout(doToggle,
+ PANE_APPEARANCE_DELAY);
+ } else {
+ doToggle();
+ }
+ }
+};
+
+/**
+ * A generic Item is used to describe children present in a Widget.
+ *
+ * This is basically a very thin wrapper around an nsIDOMNode, with a few
+ * characteristics, like a `value` and an `attachment`.
+ *
+ * The characteristics are optional, and their meaning is entirely up to you.
+ * - The `value` should be a string, passed as an argument.
+ * - The `attachment` is any kind of primitive or object, passed as an argument.
+ *
+ * Iterable via "for (let childItem of parentItem) { }".
+ *
+ * @param object ownerView
+ * The owner view creating this item.
+ * @param nsIDOMNode element
+ * A prebuilt node to be wrapped.
+ * @param string value
+ * A string identifying the node.
+ * @param any attachment
+ * Some attached primitive/object.
+ */
+function Item(ownerView, element, value, attachment) {
+ this.ownerView = ownerView;
+ this.attachment = attachment;
+ this._value = value + "";
+ this._prebuiltNode = element;
+ this._itemsByElement = new Map();
+}
+
+Item.prototype = {
+ get value() {
+ return this._value;
+ },
+ get target() {
+ return this._target;
+ },
+ get prebuiltNode() {
+ return this._prebuiltNode;
+ },
+
+ /**
+ * Immediately appends a child item to this item.
+ *
+ * @param nsIDOMNode element
+ * An nsIDOMNode representing the child element to append.
+ * @param object options [optional]
+ * Additional options or flags supported by this operation:
+ * - attachment: some attached primitive/object for the item
+ * - attributes: a batch of attributes set to the displayed element
+ * - finalize: function invoked when the child item is removed
+ * @return Item
+ * The item associated with the displayed element.
+ */
+ append: function (element, options = {}) {
+ let item = new Item(this, element, "", options.attachment);
+
+ // Entangle the item with the newly inserted child node.
+ // Make sure this is done with the value returned by appendChild(),
+ // to avoid storing a potential DocumentFragment.
+ this._entangleItem(item, this._target.appendChild(element));
+
+ // Handle any additional options after entangling the item.
+ if (options.attributes) {
+ options.attributes.forEach(e => item._target.setAttribute(e[0], e[1]));
+ }
+ if (options.finalize) {
+ item.finalize = options.finalize;
+ }
+
+ // Return the item associated with the displayed element.
+ return item;
+ },
+
+ /**
+ * Immediately removes the specified child item from this item.
+ *
+ * @param Item item
+ * The item associated with the element to remove.
+ */
+ remove: function (item) {
+ if (!item) {
+ return;
+ }
+ this._target.removeChild(item._target);
+ this._untangleItem(item);
+ },
+
+ /**
+ * Entangles an item (model) with a displayed node element (view).
+ *
+ * @param Item item
+ * The item describing a target element.
+ * @param nsIDOMNode element
+ * The element displaying the item.
+ */
+ _entangleItem: function (item, element) {
+ this._itemsByElement.set(element, item);
+ item._target = element;
+ },
+
+ /**
+ * Untangles an item (model) from a displayed node element (view).
+ *
+ * @param Item item
+ * The item describing a target element.
+ */
+ _untangleItem: function (item) {
+ if (item.finalize) {
+ item.finalize(item);
+ }
+ for (let childItem of item) {
+ item.remove(childItem);
+ }
+
+ this._unlinkItem(item);
+ item._target = null;
+ },
+
+ /**
+ * Deletes an item from the its parent's storage maps.
+ *
+ * @param Item item
+ * The item describing a target element.
+ */
+ _unlinkItem: function (item) {
+ this._itemsByElement.delete(item._target);
+ },
+
+ /**
+ * Returns a string representing the object.
+ * Avoid using `toString` to avoid accidental JSONification.
+ * @return string
+ */
+ stringify: function () {
+ return JSON.stringify({
+ value: this._value,
+ target: this._target + "",
+ prebuiltNode: this._prebuiltNode + "",
+ attachment: this.attachment
+ }, null, 2);
+ },
+
+ _value: "",
+ _target: null,
+ _prebuiltNode: null,
+ finalize: null,
+ attachment: null
+};
+
+/**
+ * Some generic Widget methods handling Item instances.
+ * Iterable via "for (let childItem of wrappedView) { }".
+ *
+ * Usage:
+ * function MyView() {
+ * this.widget = new MyWidget(document.querySelector(".my-node"));
+ * }
+ *
+ * MyView.prototype = Heritage.extend(WidgetMethods, {
+ * myMethod: function() {},
+ * ...
+ * });
+ *
+ * See https://gist.github.com/victorporof/5749386 for more details.
+ * The devtools/shared/widgets/SimpleListWidget.jsm is an implementation
+ * example.
+ *
+ * Language:
+ * - An "item" is an instance of an Item.
+ * - An "element" or "node" is a nsIDOMNode.
+ *
+ * The supplied widget can be any object implementing the following
+ * methods:
+ * - function:nsIDOMNode insertItemAt(aIndex:number, aNode:nsIDOMNode,
+ * aValue:string)
+ * - function:nsIDOMNode getItemAtIndex(aIndex:number)
+ * - function removeChild(aChild:nsIDOMNode)
+ * - function removeAllItems()
+ * - get:nsIDOMNode selectedItem()
+ * - set selectedItem(aChild:nsIDOMNode)
+ * - function getAttribute(aName:string)
+ * - function setAttribute(aName:string, aValue:string)
+ * - function removeAttribute(aName:string)
+ * - function addEventListener(aName:string, aCallback:function,
+ * aBubbleFlag:boolean)
+ * - function removeEventListener(aName:string, aCallback:function,
+ * aBubbleFlag:boolean)
+ *
+ * Optional methods that can be implemented by the widget:
+ * - function ensureElementIsVisible(aChild:nsIDOMNode)
+ *
+ * Optional attributes that may be handled (when calling
+ * get/set/removeAttribute):
+ * - "emptyText": label temporarily added when there are no items present
+ * - "headerText": label permanently added as a header
+ *
+ * For automagical keyboard and mouse accessibility, the widget should be an
+ * event emitter with the following events:
+ * - "keyPress" -> (aName:string, aEvent:KeyboardEvent)
+ * - "mousePress" -> (aName:string, aEvent:MouseEvent)
+ */
+const WidgetMethods = exports.WidgetMethods = {
+ /**
+ * Sets the element node or widget associated with this container.
+ * @param nsIDOMNode | object widget
+ */
+ set widget(widget) {
+ this._widget = widget;
+
+ // Can't use a WeakMap for _itemsByValue because keys are strings, and
+ // can't use one for _itemsByElement either, since it needs to be iterable.
+ this._itemsByValue = new Map();
+ this._itemsByElement = new Map();
+ this._stagedItems = [];
+
+ // Handle internal events emitted by the widget if necessary.
+ if (ViewHelpers.isEventEmitter(widget)) {
+ widget.on("keyPress", this._onWidgetKeyPress.bind(this));
+ widget.on("mousePress", this._onWidgetMousePress.bind(this));
+ }
+ },
+
+ /**
+ * Gets the element node or widget associated with this container.
+ * @return nsIDOMNode | object
+ */
+ get widget() {
+ return this._widget;
+ },
+
+ /**
+ * Prepares an item to be added to this container. This allows, for example,
+ * for a large number of items to be batched up before being sorted & added.
+ *
+ * If the "staged" flag is *not* set to true, the item will be immediately
+ * inserted at the correct position in this container, so that all the items
+ * still remain sorted. This can (possibly) be much slower than batching up
+ * multiple items.
+ *
+ * By default, this container assumes that all the items should be displayed
+ * sorted by their value. This can be overridden with the "index" flag,
+ * specifying on which position should an item be appended. The "staged" and
+ * "index" flags are mutually exclusive, meaning that all staged items
+ * will always be appended.
+ *
+ * @param nsIDOMNode element
+ * A prebuilt node to be wrapped.
+ * @param string value
+ * A string identifying the node.
+ * @param object options [optional]
+ * Additional options or flags supported by this operation:
+ * - attachment: some attached primitive/object for the item
+ * - staged: true to stage the item to be appended later
+ * - index: specifies on which position should the item be appended
+ * - attributes: a batch of attributes set to the displayed element
+ * - finalize: function invoked when the item is removed
+ * @return Item
+ * The item associated with the displayed element if an unstaged push,
+ * undefined if the item was staged for a later commit.
+ */
+ push: function ([element, value], options = {}) {
+ let item = new Item(this, element, value, options.attachment);
+
+ // Batch the item to be added later.
+ if (options.staged) {
+ // An ulterior commit operation will ignore any specified index, so
+ // no reason to keep it around.
+ options.index = undefined;
+ return void this._stagedItems.push({ item: item, options: options });
+ }
+ // Find the target position in this container and insert the item there.
+ if (!("index" in options)) {
+ return this._insertItemAt(this._findExpectedIndexFor(item), item,
+ options);
+ }
+ // Insert the item at the specified index. If negative or out of bounds,
+ // the item will be simply appended.
+ return this._insertItemAt(options.index, item, options);
+ },
+
+ /**
+ * Flushes all the prepared items into this container.
+ * Any specified index on the items will be ignored. Everything is appended.
+ *
+ * @param object options [optional]
+ * Additional options or flags supported by this operation:
+ * - sorted: true to sort all the items before adding them
+ */
+ commit: function (options = {}) {
+ let stagedItems = this._stagedItems;
+
+ // Sort the items before adding them to this container, if preferred.
+ if (options.sorted) {
+ stagedItems.sort((a, b) => this._currentSortPredicate(a.item, b.item));
+ }
+ // Append the prepared items to this container.
+ for (let { item, opt } of stagedItems) {
+ this._insertItemAt(-1, item, opt);
+ }
+ // Recreate the temporary items list for ulterior pushes.
+ this._stagedItems.length = 0;
+ },
+
+ /**
+ * Immediately removes the specified item from this container.
+ *
+ * @param Item item
+ * The item associated with the element to remove.
+ */
+ remove: function (item) {
+ if (!item) {
+ return;
+ }
+ this._widget.removeChild(item._target);
+ this._untangleItem(item);
+
+ if (!this._itemsByElement.size) {
+ this._preferredValue = this.selectedValue;
+ this._widget.selectedItem = null;
+ this._widget.setAttribute("emptyText", this._emptyText);
+ }
+ },
+
+ /**
+ * Removes the item at the specified index from this container.
+ *
+ * @param number index
+ * The index of the item to remove.
+ */
+ removeAt: function (index) {
+ this.remove(this.getItemAtIndex(index));
+ },
+
+ /**
+ * Removes the items in this container based on a predicate.
+ */
+ removeForPredicate: function (predicate) {
+ let item;
+ while ((item = this.getItemForPredicate(predicate))) {
+ this.remove(item);
+ }
+ },
+
+ /**
+ * Removes all items from this container.
+ */
+ empty: function () {
+ this._preferredValue = this.selectedValue;
+ this._widget.selectedItem = null;
+ this._widget.removeAllItems();
+ this._widget.setAttribute("emptyText", this._emptyText);
+
+ for (let [, item] of this._itemsByElement) {
+ this._untangleItem(item);
+ }
+
+ this._itemsByValue.clear();
+ this._itemsByElement.clear();
+ this._stagedItems.length = 0;
+ },
+
+ /**
+ * Ensures the specified item is visible in this container.
+ *
+ * @param Item item
+ * The item to bring into view.
+ */
+ ensureItemIsVisible: function (item) {
+ this._widget.ensureElementIsVisible(item._target);
+ },
+
+ /**
+ * Ensures the item at the specified index is visible in this container.
+ *
+ * @param number index
+ * The index of the item to bring into view.
+ */
+ ensureIndexIsVisible: function (index) {
+ this.ensureItemIsVisible(this.getItemAtIndex(index));
+ },
+
+ /**
+ * Sugar for ensuring the selected item is visible in this container.
+ */
+ ensureSelectedItemIsVisible: function () {
+ this.ensureItemIsVisible(this.selectedItem);
+ },
+
+ /**
+ * If supported by the widget, the label string temporarily added to this
+ * container when there are no child items present.
+ */
+ set emptyText(value) {
+ this._emptyText = value;
+
+ // Apply the emptyText attribute right now if there are no child items.
+ if (!this._itemsByElement.size) {
+ this._widget.setAttribute("emptyText", value);
+ }
+ },
+
+ /**
+ * If supported by the widget, the label string permanently added to this
+ * container as a header.
+ * @param string value
+ */
+ set headerText(value) {
+ this._headerText = value;
+ this._widget.setAttribute("headerText", value);
+ },
+
+ /**
+ * Toggles all the items in this container hidden or visible.
+ *
+ * This does not change the default filtering predicate, so newly inserted
+ * items will always be visible. Use WidgetMethods.filterContents if you care.
+ *
+ * @param boolean visibleFlag
+ * Specifies the intended visibility.
+ */
+ toggleContents: function (visibleFlag) {
+ for (let [element] of this._itemsByElement) {
+ element.hidden = !visibleFlag;
+ }
+ },
+
+ /**
+ * Toggles all items in this container hidden or visible based on a predicate.
+ *
+ * @param function predicate [optional]
+ * Items are toggled according to the return value of this function,
+ * which will become the new default filtering predicate in this
+ * container.
+ * If unspecified, all items will be toggled visible.
+ */
+ filterContents: function (predicate = this._currentFilterPredicate) {
+ this._currentFilterPredicate = predicate;
+
+ for (let [element, item] of this._itemsByElement) {
+ element.hidden = !predicate(item);
+ }
+ },
+
+ /**
+ * Sorts all the items in this container based on a predicate.
+ *
+ * @param function predicate [optional]
+ * Items are sorted according to the return value of the function,
+ * which will become the new default sorting predicate in this
+ * container. If unspecified, all items will be sorted by their value.
+ */
+ sortContents: function (predicate = this._currentSortPredicate) {
+ let sortedItems = this.items.sort(this._currentSortPredicate = predicate);
+
+ for (let i = 0, len = sortedItems.length; i < len; i++) {
+ this.swapItems(this.getItemAtIndex(i), sortedItems[i]);
+ }
+ },
+
+ /**
+ * Visually swaps two items in this container.
+ *
+ * @param Item first
+ * The first item to be swapped.
+ * @param Item second
+ * The second item to be swapped.
+ */
+ swapItems: function (first, second) {
+ if (first == second) {
+ // We're just dandy, thank you.
+ return;
+ }
+ let { _prebuiltNode: firstPrebuiltTarget, _target: firstTarget } = first;
+ let { _prebuiltNode: secondPrebuiltTarget, _target: secondTarget } = second;
+
+ // If the two items were constructed with prebuilt nodes as
+ // DocumentFragments, then those DocumentFragments are now
+ // empty and need to be reassembled.
+ if (firstPrebuiltTarget instanceof DocumentFragment) {
+ for (let node of firstTarget.childNodes) {
+ firstPrebuiltTarget.appendChild(node.cloneNode(true));
+ }
+ }
+ if (secondPrebuiltTarget instanceof DocumentFragment) {
+ for (let node of secondTarget.childNodes) {
+ secondPrebuiltTarget.appendChild(node.cloneNode(true));
+ }
+ }
+
+ // 1. Get the indices of the two items to swap.
+ let i = this._indexOfElement(firstTarget);
+ let j = this._indexOfElement(secondTarget);
+
+ // 2. Remeber the selection index, to reselect an item, if necessary.
+ let selectedTarget = this._widget.selectedItem;
+ let selectedIndex = -1;
+ if (selectedTarget == firstTarget) {
+ selectedIndex = i;
+ } else if (selectedTarget == secondTarget) {
+ selectedIndex = j;
+ }
+
+ // 3. Silently nuke both items, nobody needs to know about this.
+ this._widget.removeChild(firstTarget);
+ this._widget.removeChild(secondTarget);
+ this._unlinkItem(first);
+ this._unlinkItem(second);
+
+ // 4. Add the items again, but reversing their indices.
+ this._insertItemAt.apply(this, i < j ? [i, second] : [j, first]);
+ this._insertItemAt.apply(this, i < j ? [j, first] : [i, second]);
+
+ // 5. Restore the previous selection, if necessary.
+ if (selectedIndex == i) {
+ this._widget.selectedItem = first._target;
+ } else if (selectedIndex == j) {
+ this._widget.selectedItem = second._target;
+ }
+
+ // 6. Let the outside world know that these two items were swapped.
+ ViewHelpers.dispatchEvent(first.target, "swap", [second, first]);
+ },
+
+ /**
+ * Visually swaps two items in this container at specific indices.
+ *
+ * @param number first
+ * The index of the first item to be swapped.
+ * @param number second
+ * The index of the second item to be swapped.
+ */
+ swapItemsAtIndices: function (first, second) {
+ this.swapItems(this.getItemAtIndex(first), this.getItemAtIndex(second));
+ },
+
+ /**
+ * Checks whether an item with the specified value is among the elements
+ * shown in this container.
+ *
+ * @param string value
+ * The item's value.
+ * @return boolean
+ * True if the value is known, false otherwise.
+ */
+ containsValue: function (value) {
+ return this._itemsByValue.has(value) ||
+ this._stagedItems.some(({ item }) => item._value == value);
+ },
+
+ /**
+ * Gets the "preferred value". This is the latest selected item's value,
+ * remembered just before emptying this container.
+ * @return string
+ */
+ get preferredValue() {
+ return this._preferredValue;
+ },
+
+ /**
+ * Retrieves the item associated with the selected element.
+ * @return Item | null
+ */
+ get selectedItem() {
+ let selectedElement = this._widget.selectedItem;
+ if (selectedElement) {
+ return this._itemsByElement.get(selectedElement);
+ }
+ return null;
+ },
+
+ /**
+ * Retrieves the selected element's index in this container.
+ * @return number
+ */
+ get selectedIndex() {
+ let selectedElement = this._widget.selectedItem;
+ if (selectedElement) {
+ return this._indexOfElement(selectedElement);
+ }
+ return -1;
+ },
+
+ /**
+ * Retrieves the value of the selected element.
+ * @return string
+ */
+ get selectedValue() {
+ let selectedElement = this._widget.selectedItem;
+ if (selectedElement) {
+ return this._itemsByElement.get(selectedElement)._value;
+ }
+ return "";
+ },
+
+ /**
+ * Retrieves the attachment of the selected element.
+ * @return object | null
+ */
+ get selectedAttachment() {
+ let selectedElement = this._widget.selectedItem;
+ if (selectedElement) {
+ return this._itemsByElement.get(selectedElement).attachment;
+ }
+ return null;
+ },
+
+ _selectItem: function (item) {
+ // A falsy item is allowed to invalidate the current selection.
+ let targetElement = item ? item._target : null;
+ let prevElement = this._widget.selectedItem;
+
+ // Make sure the selected item's target element is focused and visible.
+ if (this.autoFocusOnSelection && targetElement) {
+ targetElement.focus();
+ }
+
+ if (targetElement != prevElement) {
+ this._widget.selectedItem = targetElement;
+ }
+ },
+
+ /**
+ * Selects the element with the entangled item in this container.
+ * @param Item | function item
+ */
+ set selectedItem(item) {
+ // A predicate is allowed to select a specific item.
+ // If no item is matched, then the current selection is removed.
+ if (typeof item == "function") {
+ item = this.getItemForPredicate(item);
+ }
+
+ let targetElement = item ? item._target : null;
+ let prevElement = this._widget.selectedItem;
+
+ if (this.maintainSelectionVisible && targetElement) {
+ // Some methods are optional. See the WidgetMethods object documentation
+ // for a comprehensive list.
+ if ("ensureElementIsVisible" in this._widget) {
+ this._widget.ensureElementIsVisible(targetElement);
+ }
+ }
+
+ this._selectItem(item);
+
+ // Prevent selecting the same item again and avoid dispatching
+ // a redundant selection event, so return early.
+ if (targetElement != prevElement) {
+ let dispTarget = targetElement || prevElement;
+ let dispName = this.suppressSelectionEvents ? "suppressed-select"
+ : "select";
+ ViewHelpers.dispatchEvent(dispTarget, dispName, item);
+ }
+ },
+
+ /**
+ * Selects the element at the specified index in this container.
+ * @param number index
+ */
+ set selectedIndex(index) {
+ let targetElement = this._widget.getItemAtIndex(index);
+ if (targetElement) {
+ this.selectedItem = this._itemsByElement.get(targetElement);
+ return;
+ }
+ this.selectedItem = null;
+ },
+
+ /**
+ * Selects the element with the specified value in this container.
+ * @param string value
+ */
+ set selectedValue(value) {
+ this.selectedItem = this._itemsByValue.get(value);
+ },
+
+ /**
+ * Deselects and re-selects an item in this container.
+ *
+ * Useful when you want a "select" event to be emitted, even though
+ * the specified item was already selected.
+ *
+ * @param Item | function item
+ * @see `set selectedItem`
+ */
+ forceSelect: function (item) {
+ this.selectedItem = null;
+ this.selectedItem = item;
+ },
+
+ /**
+ * Specifies if this container should try to keep the selected item visible.
+ * (For example, when new items are added the selection is brought into view).
+ */
+ maintainSelectionVisible: true,
+
+ /**
+ * Specifies if "select" events dispatched from the elements in this container
+ * when their respective items are selected should be suppressed or not.
+ *
+ * If this flag is set to true, then consumers of this container won't
+ * be normally notified when items are selected.
+ */
+ suppressSelectionEvents: false,
+
+ /**
+ * Focus this container the first time an element is inserted?
+ *
+ * If this flag is set to true, then when the first item is inserted in
+ * this container (and thus it's the only item available), its corresponding
+ * target element is focused as well.
+ */
+ autoFocusOnFirstItem: true,
+
+ /**
+ * Focus on selection?
+ *
+ * If this flag is set to true, then whenever an item is selected in
+ * this container (e.g. via the selectedIndex or selectedItem setters),
+ * its corresponding target element is focused as well.
+ *
+ * You can disable this flag, for example, to maintain a certain node
+ * focused but visually indicate a different selection in this container.
+ */
+ autoFocusOnSelection: true,
+
+ /**
+ * Focus on input (e.g. mouse click)?
+ *
+ * If this flag is set to true, then whenever an item receives user input in
+ * this container, its corresponding target element is focused as well.
+ */
+ autoFocusOnInput: true,
+
+ /**
+ * When focusing on input, allow right clicks?
+ * @see WidgetMethods.autoFocusOnInput
+ */
+ allowFocusOnRightClick: false,
+
+ /**
+ * The number of elements in this container to jump when Page Up or Page Down
+ * keys are pressed. If falsy, then the page size will be based on the
+ * number of visible items in the container.
+ */
+ pageSize: 0,
+
+ /**
+ * Focuses the first visible item in this container.
+ */
+ focusFirstVisibleItem: function () {
+ this.focusItemAtDelta(-this.itemCount);
+ },
+
+ /**
+ * Focuses the last visible item in this container.
+ */
+ focusLastVisibleItem: function () {
+ this.focusItemAtDelta(+this.itemCount);
+ },
+
+ /**
+ * Focuses the next item in this container.
+ */
+ focusNextItem: function () {
+ this.focusItemAtDelta(+1);
+ },
+
+ /**
+ * Focuses the previous item in this container.
+ */
+ focusPrevItem: function () {
+ this.focusItemAtDelta(-1);
+ },
+
+ /**
+ * Focuses another item in this container based on the index distance
+ * from the currently focused item.
+ *
+ * @param number delta
+ * A scalar specifying by how many items should the selection change.
+ */
+ focusItemAtDelta: function (delta) {
+ // Make sure the currently selected item is also focused, so that the
+ // command dispatcher mechanism has a relative node to work with.
+ // If there's no selection, just select an item at a corresponding index
+ // (e.g. the first item in this container if delta <= 1).
+ let selectedElement = this._widget.selectedItem;
+ if (selectedElement) {
+ selectedElement.focus();
+ } else {
+ this.selectedIndex = Math.max(0, delta - 1);
+ return;
+ }
+
+ let direction = delta > 0 ? "advanceFocus" : "rewindFocus";
+ let distance = Math.abs(Math[delta > 0 ? "ceil" : "floor"](delta));
+ while (distance--) {
+ if (!this._focusChange(direction)) {
+ // Out of bounds.
+ break;
+ }
+ }
+
+ // Synchronize the selected item as being the currently focused element.
+ this.selectedItem = this.getItemForElement(this._focusedElement);
+ },
+
+ /**
+ * Focuses the next or previous item in this container.
+ *
+ * @param string direction
+ * Either "advanceFocus" or "rewindFocus".
+ * @return boolean
+ * False if the focus went out of bounds and the first or last item
+ * in this container was focused instead.
+ */
+ _focusChange: function (direction) {
+ let commandDispatcher = this._commandDispatcher;
+ let prevFocusedElement = commandDispatcher.focusedElement;
+ let currFocusedElement;
+
+ do {
+ commandDispatcher.suppressFocusScroll = true;
+ commandDispatcher[direction]();
+ currFocusedElement = commandDispatcher.focusedElement;
+
+ // Make sure the newly focused item is a part of this container. If the
+ // focus goes out of bounds, revert the previously focused item.
+ if (!this.getItemForElement(currFocusedElement)) {
+ prevFocusedElement.focus();
+ return false;
+ }
+ } while (!WIDGET_FOCUSABLE_NODES.has(currFocusedElement.tagName));
+
+ // Focus remained within bounds.
+ return true;
+ },
+
+ /**
+ * Gets the command dispatcher instance associated with this container's DOM.
+ * If there are no items displayed in this container, null is returned.
+ * @return nsIDOMXULCommandDispatcher | null
+ */
+ get _commandDispatcher() {
+ if (this._cachedCommandDispatcher) {
+ return this._cachedCommandDispatcher;
+ }
+ let someElement = this._widget.getItemAtIndex(0);
+ if (someElement) {
+ let commandDispatcher = someElement.ownerDocument.commandDispatcher;
+ this._cachedCommandDispatcher = commandDispatcher;
+ return commandDispatcher;
+ }
+ return null;
+ },
+
+ /**
+ * Gets the currently focused element in this container.
+ *
+ * @return nsIDOMNode
+ * The focused element, or null if nothing is found.
+ */
+ get _focusedElement() {
+ let commandDispatcher = this._commandDispatcher;
+ if (commandDispatcher) {
+ return commandDispatcher.focusedElement;
+ }
+ return null;
+ },
+
+ /**
+ * Gets the item in the container having the specified index.
+ *
+ * @param number index
+ * The index used to identify the element.
+ * @return Item
+ * The matched item, or null if nothing is found.
+ */
+ getItemAtIndex: function (index) {
+ return this.getItemForElement(this._widget.getItemAtIndex(index));
+ },
+
+ /**
+ * Gets the item in the container having the specified value.
+ *
+ * @param string value
+ * The value used to identify the element.
+ * @return Item
+ * The matched item, or null if nothing is found.
+ */
+ getItemByValue: function (value) {
+ return this._itemsByValue.get(value);
+ },
+
+ /**
+ * Gets the item in the container associated with the specified element.
+ *
+ * @param nsIDOMNode element
+ * The element used to identify the item.
+ * @param object flags [optional]
+ * Additional options for showing the source. Supported options:
+ * - noSiblings: if siblings shouldn't be taken into consideration
+ * when searching for the associated item.
+ * @return Item
+ * The matched item, or null if nothing is found.
+ */
+ getItemForElement: function (element, flags = {}) {
+ while (element) {
+ let item = this._itemsByElement.get(element);
+
+ // Also search the siblings if allowed.
+ if (!flags.noSiblings) {
+ item = item ||
+ this._itemsByElement.get(element.nextElementSibling) ||
+ this._itemsByElement.get(element.previousElementSibling);
+ }
+ if (item) {
+ return item;
+ }
+ element = element.parentNode;
+ }
+ return null;
+ },
+
+ /**
+ * Gets a visible item in this container validating a specified predicate.
+ *
+ * @param function predicate
+ * The first item which validates this predicate is returned
+ * @return Item
+ * The matched item, or null if nothing is found.
+ */
+ getItemForPredicate: function (predicate, owner = this) {
+ // Recursively check the items in this widget for a predicate match.
+ for (let [element, item] of owner._itemsByElement) {
+ let match;
+ if (predicate(item) && !element.hidden) {
+ match = item;
+ } else {
+ match = this.getItemForPredicate(predicate, item);
+ }
+ if (match) {
+ return match;
+ }
+ }
+ // Also check the staged items. No need to do this recursively since
+ // they're not even appended to the view yet.
+ for (let { item } of this._stagedItems) {
+ if (predicate(item)) {
+ return item;
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Shortcut function for getItemForPredicate which works on item attachments.
+ * @see getItemForPredicate
+ */
+ getItemForAttachment: function (predicate, owner = this) {
+ return this.getItemForPredicate(e => predicate(e.attachment));
+ },
+
+ /**
+ * Finds the index of an item in the container.
+ *
+ * @param Item item
+ * The item get the index for.
+ * @return number
+ * The index of the matched item, or -1 if nothing is found.
+ */
+ indexOfItem: function (item) {
+ return this._indexOfElement(item._target);
+ },
+
+ /**
+ * Finds the index of an element in the container.
+ *
+ * @param nsIDOMNode element
+ * The element get the index for.
+ * @return number
+ * The index of the matched element, or -1 if nothing is found.
+ */
+ _indexOfElement: function (element) {
+ for (let i = 0; i < this._itemsByElement.size; i++) {
+ if (this._widget.getItemAtIndex(i) == element) {
+ return i;
+ }
+ }
+ return -1;
+ },
+
+ /**
+ * Gets the total number of items in this container.
+ * @return number
+ */
+ get itemCount() {
+ return this._itemsByElement.size;
+ },
+
+ /**
+ * Returns a list of items in this container, in the displayed order.
+ * @return array
+ */
+ get items() {
+ let store = [];
+ let itemCount = this.itemCount;
+ for (let i = 0; i < itemCount; i++) {
+ store.push(this.getItemAtIndex(i));
+ }
+ return store;
+ },
+
+ /**
+ * Returns a list of values in this container, in the displayed order.
+ * @return array
+ */
+ get values() {
+ return this.items.map(e => e._value);
+ },
+
+ /**
+ * Returns a list of attachments in this container, in the displayed order.
+ * @return array
+ */
+ get attachments() {
+ return this.items.map(e => e.attachment);
+ },
+
+ /**
+ * Returns a list of all the visible (non-hidden) items in this container,
+ * in the displayed order
+ * @return array
+ */
+ get visibleItems() {
+ return this.items.filter(e => !e._target.hidden);
+ },
+
+ /**
+ * Checks if an item is unique in this container. If an item's value is an
+ * empty string, "undefined" or "null", it is considered unique.
+ *
+ * @param Item item
+ * The item for which to verify uniqueness.
+ * @return boolean
+ * True if the item is unique, false otherwise.
+ */
+ isUnique: function (item) {
+ let value = item._value;
+ if (value == "" || value == "undefined" || value == "null") {
+ return true;
+ }
+ return !this._itemsByValue.has(value);
+ },
+
+ /**
+ * Checks if an item is eligible for this container. By default, this checks
+ * whether an item is unique and has a prebuilt target node.
+ *
+ * @param Item item
+ * The item for which to verify eligibility.
+ * @return boolean
+ * True if the item is eligible, false otherwise.
+ */
+ isEligible: function (item) {
+ return this.isUnique(item) && item._prebuiltNode;
+ },
+
+ /**
+ * Finds the expected item index in this container based on the default
+ * sort predicate.
+ *
+ * @param Item item
+ * The item for which to get the expected index.
+ * @return number
+ * The expected item index.
+ */
+ _findExpectedIndexFor: function (item) {
+ let itemCount = this.itemCount;
+ for (let i = 0; i < itemCount; i++) {
+ if (this._currentSortPredicate(this.getItemAtIndex(i), item) > 0) {
+ return i;
+ }
+ }
+ return itemCount;
+ },
+
+ /**
+ * Immediately inserts an item in this container at the specified index.
+ *
+ * @param number index
+ * The position in the container intended for this item.
+ * @param Item item
+ * The item describing a target element.
+ * @param object options [optional]
+ * Additional options or flags supported by this operation:
+ * - attributes: a batch of attributes set to the displayed element
+ * - finalize: function when the item is untangled (removed)
+ * @return Item
+ * The item associated with the displayed element, null if rejected.
+ */
+ _insertItemAt: function (index, item, options = {}) {
+ if (!this.isEligible(item)) {
+ return null;
+ }
+
+ // Entangle the item with the newly inserted node.
+ // Make sure this is done with the value returned by insertItemAt(),
+ // to avoid storing a potential DocumentFragment.
+ let node = item._prebuiltNode;
+ let attachment = item.attachment;
+ this._entangleItem(item,
+ this._widget.insertItemAt(index, node, attachment));
+
+ // Handle any additional options after entangling the item.
+ if (!this._currentFilterPredicate(item)) {
+ item._target.hidden = true;
+ }
+ if (this.autoFocusOnFirstItem && this._itemsByElement.size == 1) {
+ item._target.focus();
+ }
+ if (options.attributes) {
+ options.attributes.forEach(e => item._target.setAttribute(e[0], e[1]));
+ }
+ if (options.finalize) {
+ item.finalize = options.finalize;
+ }
+
+ // Hide the empty text if the selection wasn't lost.
+ this._widget.removeAttribute("emptyText");
+
+ // Return the item associated with the displayed element.
+ return item;
+ },
+
+ /**
+ * Entangles an item (model) with a displayed node element (view).
+ *
+ * @param Item item
+ * The item describing a target element.
+ * @param nsIDOMNode element
+ * The element displaying the item.
+ */
+ _entangleItem: function (item, element) {
+ this._itemsByValue.set(item._value, item);
+ this._itemsByElement.set(element, item);
+ item._target = element;
+ },
+
+ /**
+ * Untangles an item (model) from a displayed node element (view).
+ *
+ * @param Item item
+ * The item describing a target element.
+ */
+ _untangleItem: function (item) {
+ if (item.finalize) {
+ item.finalize(item);
+ }
+ for (let childItem of item) {
+ item.remove(childItem);
+ }
+
+ this._unlinkItem(item);
+ item._target = null;
+ },
+
+ /**
+ * Deletes an item from the its parent's storage maps.
+ *
+ * @param Item item
+ * The item describing a target element.
+ */
+ _unlinkItem: function (item) {
+ this._itemsByValue.delete(item._value);
+ this._itemsByElement.delete(item._target);
+ },
+
+ /**
+ * The keyPress event listener for this container.
+ * @param string name
+ * @param KeyboardEvent event
+ */
+ _onWidgetKeyPress: function (name, event) {
+ // Prevent scrolling when pressing navigation keys.
+ ViewHelpers.preventScrolling(event);
+
+ switch (event.keyCode) {
+ case KeyCodes.DOM_VK_UP:
+ case KeyCodes.DOM_VK_LEFT:
+ this.focusPrevItem();
+ return;
+ case KeyCodes.DOM_VK_DOWN:
+ case KeyCodes.DOM_VK_RIGHT:
+ this.focusNextItem();
+ return;
+ case KeyCodes.DOM_VK_PAGE_UP:
+ this.focusItemAtDelta(-(this.pageSize ||
+ (this.itemCount / PAGE_SIZE_ITEM_COUNT_RATIO)));
+ return;
+ case KeyCodes.DOM_VK_PAGE_DOWN:
+ this.focusItemAtDelta(+(this.pageSize ||
+ (this.itemCount / PAGE_SIZE_ITEM_COUNT_RATIO)));
+ return;
+ case KeyCodes.DOM_VK_HOME:
+ this.focusFirstVisibleItem();
+ return;
+ case KeyCodes.DOM_VK_END:
+ this.focusLastVisibleItem();
+ return;
+ }
+ },
+
+ /**
+ * The mousePress event listener for this container.
+ * @param string name
+ * @param MouseEvent event
+ */
+ _onWidgetMousePress: function (name, event) {
+ if (event.button != 0 && !this.allowFocusOnRightClick) {
+ // Only allow left-click to trigger this event.
+ return;
+ }
+
+ let item = this.getItemForElement(event.target);
+ if (item) {
+ // The container is not empty and we clicked on an actual item.
+ this.selectedItem = item;
+ // Make sure the current event's target element is also focused.
+ this.autoFocusOnInput && item._target.focus();
+ }
+ },
+
+ /**
+ * The predicate used when filtering items. By default, all items in this
+ * view are visible.
+ *
+ * @param Item item
+ * The item passing through the filter.
+ * @return boolean
+ * True if the item should be visible, false otherwise.
+ */
+ _currentFilterPredicate: function (item) {
+ return true;
+ },
+
+ /**
+ * The predicate used when sorting items. By default, items in this view
+ * are sorted by their label.
+ *
+ * @param Item first
+ * The first item used in the comparison.
+ * @param Item second
+ * The second item used in the comparison.
+ * @return number
+ * -1 to sort first to a lower index than second
+ * 0 to leave first and second unchanged with respect to each other
+ * 1 to sort second to a lower index than first
+ */
+ _currentSortPredicate: function (first, second) {
+ return +(first._value.toLowerCase() > second._value.toLowerCase());
+ },
+
+ /**
+ * Call a method on this widget named `methodName`. Any further arguments are
+ * passed on to the method. Returns the result of the method call.
+ *
+ * @param String methodName
+ * The name of the method you want to call.
+ * @param args
+ * Optional. Any arguments you want to pass through to the method.
+ */
+ callMethod: function (methodName, ...args) {
+ return this._widget[methodName].apply(this._widget, args);
+ },
+
+ _widget: null,
+ _emptyText: "",
+ _headerText: "",
+ _preferredValue: "",
+ _cachedCommandDispatcher: null
+};
+
+/**
+ * A generator-iterator over all the items in this container.
+ */
+Item.prototype[Symbol.iterator] =
+WidgetMethods[Symbol.iterator] = function* () {
+ yield* this._itemsByElement.values();
+};
diff --git a/devtools/client/shared/widgets/widgets.css b/devtools/client/shared/widgets/widgets.css
new file mode 100644
index 000000000..b979cf266
--- /dev/null
+++ b/devtools/client/shared/widgets/widgets.css
@@ -0,0 +1,109 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+/* BreacrumbsWidget */
+
+.breadcrumbs-widget-item {
+ direction: ltr;
+}
+
+.breadcrumbs-widget-item {
+ -moz-user-focus: normal;
+}
+
+/* SimpleListWidget */
+
+.simple-list-widget-container {
+ overflow-x: hidden;
+ overflow-y: auto;
+}
+
+/* FastListWidget */
+
+.fast-list-widget-container {
+ overflow: auto;
+}
+
+/* SideMenuWidget */
+
+.side-menu-widget-container {
+ overflow-x: hidden;
+ overflow-y: auto;
+}
+
+.side-menu-widget-item-contents {
+ -moz-user-focus: normal;
+}
+
+.side-menu-widget-group-checkbox .checkbox-label-box,
+.side-menu-widget-item-checkbox .checkbox-label-box {
+ display: none; /* See bug 669507 */
+}
+
+/* VariablesView */
+
+.variables-view-container {
+ overflow-x: hidden;
+ overflow-y: auto;
+}
+
+.variables-view-element-details:not([open]) {
+ display: none;
+}
+
+.variables-view-scope,
+.variable-or-property {
+ -moz-user-focus: normal;
+}
+
+.variables-view-scope > .title,
+.variable-or-property > .title {
+ overflow: hidden;
+}
+
+.variables-view-scope[untitled] > .title,
+.variable-or-property[untitled] > .title,
+.variable-or-property[unmatched] > .title {
+ display: none;
+}
+
+.variable-or-property:not([safe-getter]) > tooltip > label.WebIDL,
+.variable-or-property:not([overridden]) > tooltip > label.overridden,
+.variable-or-property:not([non-extensible]) > tooltip > label.extensible,
+.variable-or-property:not([frozen]) > tooltip > label.frozen,
+.variable-or-property:not([sealed]) > tooltip > label.sealed {
+ display: none;
+}
+
+.variable-or-property[pseudo-item] > tooltip,
+.variable-or-property[pseudo-item] > .title > .variables-view-edit,
+.variable-or-property[pseudo-item] > .title > .variables-view-delete,
+.variable-or-property[pseudo-item] > .title > .variables-view-add-property,
+.variable-or-property[pseudo-item] > .title > .variables-view-open-inspector,
+.variable-or-property[pseudo-item] > .title > .variable-or-property-frozen-label,
+.variable-or-property[pseudo-item] > .title > .variable-or-property-sealed-label,
+.variable-or-property[pseudo-item] > .title > .variable-or-property-non-extensible-label,
+.variable-or-property[pseudo-item] > .title > .variable-or-property-non-writable-icon {
+ display: none;
+}
+
+.variable-or-property > .title .toolbarbutton-text {
+ display: none;
+}
+
+*:not(:hover) .variables-view-delete,
+*:not(:hover) .variables-view-add-property,
+*:not(:hover) .variables-view-open-inspector {
+ visibility: hidden;
+}
+
+.variables-view-container[aligned-values] [optional-visibility] {
+ display: none;
+}
+
+/* Table Widget */
+.table-widget-body > .devtools-side-splitter:last-child {
+ display: none;
+}
diff --git a/devtools/client/shared/zoom-keys.js b/devtools/client/shared/zoom-keys.js
new file mode 100644
index 000000000..80f4386fb
--- /dev/null
+++ b/devtools/client/shared/zoom-keys.js
@@ -0,0 +1,85 @@
+/* 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 { KeyShortcuts } = require("devtools/client/shared/key-shortcuts");
+
+const ZOOM_PREF = "devtools.toolbox.zoomValue";
+const MIN_ZOOM = 0.5;
+const MAX_ZOOM = 2;
+
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper("devtools/client/locales/toolbox.properties");
+
+/**
+ * Register generic keys to control zoom level of the given document.
+ * Used by both the toolboxes and the browser console.
+ *
+ * @param {DOMWindow} The window on which we should listent to key strokes and
+ * modify the zoom factor.
+ */
+exports.register = function (window) {
+ let shortcuts = new KeyShortcuts({
+ window
+ });
+ let docShell = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShell);
+ let contViewer = docShell.contentViewer;
+ let zoomValue = parseFloat(Services.prefs.getCharPref(ZOOM_PREF));
+ let zoomIn = function (name, event) {
+ setZoom(zoomValue + 0.1);
+ event.preventDefault();
+ };
+
+ let zoomOut = function (name, event) {
+ setZoom(zoomValue - 0.1);
+ event.preventDefault();
+ };
+
+ let zoomReset = function (name, event) {
+ setZoom(1);
+ event.preventDefault();
+ };
+
+ let setZoom = function (newValue) {
+ // cap zoom value
+ zoomValue = Math.max(newValue, MIN_ZOOM);
+ zoomValue = Math.min(zoomValue, MAX_ZOOM);
+
+ contViewer.fullZoom = zoomValue;
+
+ Services.prefs.setCharPref(ZOOM_PREF, zoomValue);
+ };
+
+ // Set zoom to whatever the last setting was.
+ setZoom(zoomValue);
+
+ shortcuts.on(L10N.getStr("toolbox.zoomIn.key"), zoomIn);
+ let zoomIn2 = L10N.getStr("toolbox.zoomIn2.key");
+ if (zoomIn2) {
+ shortcuts.on(zoomIn2, zoomIn);
+ }
+ let zoomIn3 = L10N.getStr("toolbox.zoomIn2.key");
+ if (zoomIn3) {
+ shortcuts.on(zoomIn3, zoomIn);
+ }
+
+ shortcuts.on(L10N.getStr("toolbox.zoomOut.key"),
+ zoomOut);
+ let zoomOut2 = L10N.getStr("toolbox.zoomOut2.key");
+ if (zoomOut2) {
+ shortcuts.on(zoomOut2, zoomOut);
+ }
+
+ shortcuts.on(L10N.getStr("toolbox.zoomReset.key"),
+ zoomReset);
+ let zoomReset2 = L10N.getStr("toolbox.zoomReset2.key");
+ if (zoomReset2) {
+ shortcuts.on(zoomReset2, zoomReset);
+ }
+};
diff --git a/devtools/client/shims/gDevTools.jsm b/devtools/client/shims/gDevTools.jsm
new file mode 100644
index 000000000..0bdf45633
--- /dev/null
+++ b/devtools/client/shims/gDevTools.jsm
@@ -0,0 +1,35 @@
+/* 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";
+
+/**
+ * This file only exists to support add-ons which import this module at a
+ * specific path.
+ */
+
+const Cu = Components.utils;
+
+const { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
+
+const WARNING_PREF = "devtools.migration.warnings";
+if (Services.prefs.getBoolPref(WARNING_PREF)) {
+ const { Deprecated } = Cu.import("resource://gre/modules/Deprecated.jsm", {});
+ Deprecated.warning("This path to gDevTools.jsm is deprecated. Please use " +
+ "Cu.import(\"resource://devtools/client/" +
+ "framework/gDevTools.jsm\") to load this module.",
+ "https://bugzil.la/912121");
+}
+
+this.EXPORTED_SYMBOLS = [
+ "gDevTools",
+ "gDevToolsBrowser"
+];
+
+const module =
+ Cu.import("resource://devtools/client/framework/gDevTools.jsm", {});
+
+for (let symbol of this.EXPORTED_SYMBOLS) {
+ this[symbol] = module[symbol];
+}
diff --git a/devtools/client/shims/moz.build b/devtools/client/shims/moz.build
new file mode 100644
index 000000000..21b689af8
--- /dev/null
+++ b/devtools/client/shims/moz.build
@@ -0,0 +1,18 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+# Unlike most DevTools build files, this file does not use DevToolsModules
+# because these files are here for add-on compatibility, and so they must be
+# installed to previously defined locations.
+
+EXTRA_JS_MODULES.devtools += [
+ 'gDevTools.jsm',
+]
+
+# Extra compatibility layer for transitional URLs used for part of 44 cycle
+EXTRA_JS_MODULES.devtools.client.framework += [
+ 'gDevTools.jsm',
+]
diff --git a/devtools/client/sourceeditor/.eslintrc.js b/devtools/client/sourceeditor/.eslintrc.js
new file mode 100644
index 000000000..e705169c7
--- /dev/null
+++ b/devtools/client/sourceeditor/.eslintrc.js
@@ -0,0 +1,15 @@
+"use strict";
+
+module.exports = {
+ // Extend from the devtools eslintrc.
+ "extends": "../../.eslintrc.js",
+
+ "rules": {
+ // The inspector is being migrated to HTML and cleaned of
+ // chrome-privileged code, so this rule disallows requiring chrome
+ // code. Some files here disable this rule still. The
+ // goal is to enable the rule globally on all files.
+ /* eslint-disable max-len */
+ "mozilla/reject-some-requires": ["error", "^(chrome|chrome:.*|resource:.*|devtools/server/.*|.*\\.jsm)$"],
+ },
+};
diff --git a/devtools/client/sourceeditor/autocomplete.js b/devtools/client/sourceeditor/autocomplete.js
new file mode 100644
index 000000000..357f25ed1
--- /dev/null
+++ b/devtools/client/sourceeditor/autocomplete.js
@@ -0,0 +1,405 @@
+/* vim:set ts=2 sw=2 sts=2 et 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 CSSCompleter = require("devtools/client/sourceeditor/css-autocompleter");
+const { AutocompletePopup } = require("devtools/client/shared/autocomplete-popup");
+const {KeyCodes} = require("devtools/client/shared/keycodes");
+
+const CM_TERN_SCRIPTS = [
+ "chrome://devtools/content/sourceeditor/codemirror/addon/tern/tern.js",
+ "chrome://devtools/content/sourceeditor/codemirror/addon/hint/show-hint.js"
+];
+
+const autocompleteMap = new WeakMap();
+
+/**
+ * Prepares an editor instance for autocompletion.
+ */
+function initializeAutoCompletion(ctx, options = {}) {
+ let { cm, ed, Editor } = ctx;
+ if (autocompleteMap.has(ed)) {
+ return;
+ }
+
+ let win = ed.container.contentWindow.wrappedJSObject;
+ let { CodeMirror, document } = win;
+
+ let completer = null;
+ let autocompleteKey = "Ctrl-" +
+ Editor.keyFor("autocompletion", { noaccel: true });
+ if (ed.config.mode == Editor.modes.js) {
+ let defs = [
+ require("./tern/browser"),
+ require("./tern/ecma5"),
+ ];
+
+ CM_TERN_SCRIPTS.forEach(ed.loadScript, ed);
+ win.tern = require("./tern/tern");
+ cm.tern = new CodeMirror.TernServer({
+ defs: defs,
+ typeTip: function (data) {
+ let tip = document.createElement("span");
+ tip.className = "CodeMirror-Tern-information";
+ let tipType = document.createElement("strong");
+ let tipText = document.createTextNode(data.type ||
+ cm.l10n("autocompletion.notFound"));
+ tipType.appendChild(tipText);
+ tip.appendChild(tipType);
+
+ if (data.doc) {
+ tip.appendChild(document.createTextNode(" — " + data.doc));
+ }
+
+ if (data.url) {
+ tip.appendChild(document.createTextNode(" "));
+ let docLink = document.createElement("a");
+ docLink.textContent = "[" + cm.l10n("autocompletion.docsLink") + "]";
+ docLink.href = data.url;
+ docLink.className = "theme-link";
+ docLink.setAttribute("target", "_blank");
+ tip.appendChild(docLink);
+ }
+
+ return tip;
+ }
+ });
+
+ let keyMap = {};
+ let updateArgHintsCallback = cm.tern.updateArgHints.bind(cm.tern, cm);
+ cm.on("cursorActivity", updateArgHintsCallback);
+
+ keyMap[autocompleteKey] = cmArg => {
+ cmArg.tern.getHint(cmArg, data => {
+ CodeMirror.on(data, "shown", () => ed.emit("before-suggest"));
+ CodeMirror.on(data, "close", () => ed.emit("after-suggest"));
+ CodeMirror.on(data, "select", () => ed.emit("suggestion-entered"));
+ CodeMirror.showHint(cmArg, (cmIgnore, cb) => cb(data), { async: true });
+ });
+ };
+
+ keyMap[Editor.keyFor("showInformation2", { noaccel: true })] = cmArg => {
+ cmArg.tern.showType(cmArg, null, () => {
+ ed.emit("show-information");
+ });
+ };
+ cm.addKeyMap(keyMap);
+
+ let destroyTern = function () {
+ ed.off("destroy", destroyTern);
+ cm.off("cursorActivity", updateArgHintsCallback);
+ cm.removeKeyMap(keyMap);
+ win.tern = cm.tern = null;
+ autocompleteMap.delete(ed);
+ };
+
+ ed.on("destroy", destroyTern);
+
+ autocompleteMap.set(ed, {
+ destroy: destroyTern
+ });
+
+ // TODO: Integrate tern autocompletion with this autocomplete API.
+ return;
+ } else if (ed.config.mode == Editor.modes.css) {
+ completer = new CSSCompleter({walker: options.walker,
+ cssProperties: options.cssProperties});
+ }
+
+ function insertSelectedPopupItem() {
+ let autocompleteState = autocompleteMap.get(ed);
+ if (!popup || !popup.isOpen || !autocompleteState) {
+ return false;
+ }
+
+ if (!autocompleteState.suggestionInsertedOnce && popup.selectedItem) {
+ autocompleteMap.get(ed).insertingSuggestion = true;
+ insertPopupItem(ed, popup.selectedItem);
+ }
+
+ popup.once("popup-closed", () => {
+ // This event is used in tests.
+ ed.emit("popup-hidden");
+ });
+ popup.hidePopup();
+ return true;
+ }
+
+ // Give each popup a new name to avoid sharing the elements.
+
+ let popup = new AutocompletePopup(win.parent.document, {
+ position: "bottom",
+ theme: "auto",
+ autoSelect: true,
+ onClick: insertSelectedPopupItem
+ });
+
+ let cycle = reverse => {
+ if (popup && popup.isOpen) {
+ cycleSuggestions(ed, reverse == true);
+ return null;
+ }
+
+ return CodeMirror.Pass;
+ };
+
+ let keyMap = {
+ "Tab": cycle,
+ "Down": cycle,
+ "Shift-Tab": cycle.bind(null, true),
+ "Up": cycle.bind(null, true),
+ "Enter": () => {
+ let wasHandled = insertSelectedPopupItem();
+ return wasHandled ? true : CodeMirror.Pass;
+ }
+ };
+
+ let autoCompleteCallback = autoComplete.bind(null, ctx);
+ let keypressCallback = onEditorKeypress.bind(null, ctx);
+ keyMap[autocompleteKey] = autoCompleteCallback;
+ cm.addKeyMap(keyMap);
+
+ cm.on("keydown", keypressCallback);
+ ed.on("change", autoCompleteCallback);
+ ed.on("destroy", destroy);
+
+ function destroy() {
+ ed.off("destroy", destroy);
+ cm.off("keydown", keypressCallback);
+ ed.off("change", autoCompleteCallback);
+ cm.removeKeyMap(keyMap);
+ popup.destroy();
+ keyMap = popup = completer = null;
+ autocompleteMap.delete(ed);
+ }
+
+ autocompleteMap.set(ed, {
+ popup: popup,
+ completer: completer,
+ keyMap: keyMap,
+ destroy: destroy,
+ insertingSuggestion: false,
+ suggestionInsertedOnce: false
+ });
+}
+
+/**
+ * Destroy autocompletion on an editor instance.
+ */
+function destroyAutoCompletion(ctx) {
+ let { ed } = ctx;
+ if (!autocompleteMap.has(ed)) {
+ return;
+ }
+
+ let {destroy} = autocompleteMap.get(ed);
+ destroy();
+}
+
+/**
+ * Provides suggestions to autocomplete the current token/word being typed.
+ */
+function autoComplete({ ed, cm }) {
+ let autocompleteOpts = autocompleteMap.get(ed);
+ let { completer, popup } = autocompleteOpts;
+ if (!completer || autocompleteOpts.insertingSuggestion ||
+ autocompleteOpts.doNotAutocomplete) {
+ autocompleteOpts.insertingSuggestion = false;
+ return;
+ }
+ let cur = ed.getCursor();
+ completer.complete(cm.getRange({line: 0, ch: 0}, cur), cur).then(suggestions => {
+ if (!suggestions || !suggestions.length || suggestions[0].preLabel == null) {
+ autocompleteOpts.suggestionInsertedOnce = false;
+ popup.once("popup-closed", () => {
+ // This event is used in tests.
+ ed.emit("after-suggest");
+ });
+ popup.hidePopup();
+ return;
+ }
+ // The cursor is at the end of the currently entered part of the token,
+ // like "backgr|" but we need to open the popup at the beginning of the
+ // character "b". Thus we need to calculate the width of the entered part
+ // of the token ("backgr" here). 4 comes from the popup's left padding.
+
+ let cursorElement = cm.display.cursorDiv.querySelector(".CodeMirror-cursor");
+ let left = suggestions[0].preLabel.length * cm.defaultCharWidth() + 4;
+ popup.hidePopup();
+ popup.setItems(suggestions);
+
+ popup.once("popup-opened", () => {
+ // This event is used in tests.
+ ed.emit("after-suggest");
+ });
+ popup.openPopup(cursorElement, -1 * left, 0);
+ autocompleteOpts.suggestionInsertedOnce = false;
+ }).then(null, e => console.error(e));
+}
+
+/**
+ * Inserts a popup item into the current cursor location
+ * in the editor.
+ */
+function insertPopupItem(ed, popupItem) {
+ let {preLabel, text} = popupItem;
+ let cur = ed.getCursor();
+ let textBeforeCursor = ed.getText(cur.line).substring(0, cur.ch);
+ let backwardsTextBeforeCursor = textBeforeCursor.split("").reverse().join("");
+ let backwardsPreLabel = preLabel.split("").reverse().join("");
+
+ // If there is additional text in the preLabel vs the line, then
+ // just insert the entire autocomplete text. An example:
+ // if you type 'a' and select '#about' from the autocomplete menu,
+ // then the final text needs to the end up as '#about'.
+ if (backwardsPreLabel.indexOf(backwardsTextBeforeCursor) === 0) {
+ ed.replaceText(text, {line: cur.line, ch: 0}, cur);
+ } else {
+ ed.replaceText(text.slice(preLabel.length), cur, cur);
+ }
+}
+
+/**
+ * Cycles through provided suggestions by the popup in a top to bottom manner
+ * when `reverse` is not true. Opposite otherwise.
+ */
+function cycleSuggestions(ed, reverse) {
+ let autocompleteOpts = autocompleteMap.get(ed);
+ let { popup } = autocompleteOpts;
+ let cur = ed.getCursor();
+ autocompleteOpts.insertingSuggestion = true;
+ if (!autocompleteOpts.suggestionInsertedOnce) {
+ autocompleteOpts.suggestionInsertedOnce = true;
+ let firstItem;
+ if (reverse) {
+ firstItem = popup.getItemAtIndex(popup.itemCount - 1);
+ popup.selectPreviousItem();
+ } else {
+ firstItem = popup.getItemAtIndex(0);
+ if (firstItem.label == firstItem.preLabel && popup.itemCount > 1) {
+ firstItem = popup.getItemAtIndex(1);
+ popup.selectNextItem();
+ }
+ }
+ if (popup.itemCount == 1) {
+ popup.hidePopup();
+ }
+ insertPopupItem(ed, firstItem);
+ } else {
+ let fromCur = {
+ line: cur.line,
+ ch: cur.ch - popup.selectedItem.text.length
+ };
+ if (reverse) {
+ popup.selectPreviousItem();
+ } else {
+ popup.selectNextItem();
+ }
+ ed.replaceText(popup.selectedItem.text, fromCur, cur);
+ }
+ // This event is used in tests.
+ ed.emit("suggestion-entered");
+}
+
+/**
+ * onkeydown handler for the editor instance to prevent autocompleting on some
+ * keypresses.
+ */
+function onEditorKeypress({ ed, Editor }, cm, event) {
+ let autocompleteOpts = autocompleteMap.get(ed);
+
+ // Do not try to autocomplete with multiple selections.
+ if (ed.hasMultipleSelections()) {
+ autocompleteOpts.doNotAutocomplete = true;
+ autocompleteOpts.popup.hidePopup();
+ return;
+ }
+
+ if ((event.ctrlKey || event.metaKey) && event.keyCode == KeyCodes.DOM_VK_SPACE) {
+ // When Ctrl/Cmd + Space is pressed, two simultaneous keypresses are emitted
+ // first one for just the Ctrl/Cmd and second one for combo. The first one
+ // leave the autocompleteOpts.doNotAutocomplete as true, so we have to make
+ // it false
+ autocompleteOpts.doNotAutocomplete = false;
+ return;
+ }
+
+ if (event.ctrlKey || event.metaKey || event.altKey) {
+ autocompleteOpts.doNotAutocomplete = true;
+ autocompleteOpts.popup.hidePopup();
+ return;
+ }
+
+ switch (event.keyCode) {
+ case KeyCodes.DOM_VK_RETURN:
+ autocompleteOpts.doNotAutocomplete = true;
+ break;
+ case KeyCodes.DOM_VK_ESCAPE:
+ if (autocompleteOpts.popup.isOpen) {
+ event.preventDefault();
+ }
+ break;
+ case KeyCodes.DOM_VK_LEFT:
+ case KeyCodes.DOM_VK_RIGHT:
+ case KeyCodes.DOM_VK_HOME:
+ case KeyCodes.DOM_VK_END:
+ autocompleteOpts.doNotAutocomplete = true;
+ autocompleteOpts.popup.hidePopup();
+ break;
+ case KeyCodes.DOM_VK_BACK_SPACE:
+ case KeyCodes.DOM_VK_DELETE:
+ if (ed.config.mode == Editor.modes.css) {
+ autocompleteOpts.completer.invalidateCache(ed.getCursor().line);
+ }
+ autocompleteOpts.doNotAutocomplete = true;
+ autocompleteOpts.popup.hidePopup();
+ break;
+ default:
+ autocompleteOpts.doNotAutocomplete = false;
+ }
+}
+
+/**
+ * Returns the private popup. This method is used by tests to test the feature.
+ */
+function getPopup({ ed }) {
+ if (autocompleteMap.has(ed)) {
+ return autocompleteMap.get(ed).popup;
+ }
+
+ return null;
+}
+
+/**
+ * Returns contextual information about the token covered by the caret if the
+ * implementation of completer supports it.
+ */
+function getInfoAt({ ed }, caret) {
+ if (autocompleteMap.has(ed)) {
+ let completer = autocompleteMap.get(ed).completer;
+ if (completer && completer.getInfoAt) {
+ return completer.getInfoAt(ed.getText(), caret);
+ }
+ }
+
+ return null;
+}
+
+/**
+ * Returns whether autocompletion is enabled for this editor.
+ * Used for testing
+ */
+function isAutocompletionEnabled({ ed }) {
+ return autocompleteMap.has(ed);
+}
+
+// Export functions
+
+module.exports.initializeAutoCompletion = initializeAutoCompletion;
+module.exports.destroyAutoCompletion = destroyAutoCompletion;
+module.exports.getAutocompletionPopup = getPopup;
+module.exports.getInfoAt = getInfoAt;
+module.exports.isAutocompletionEnabled = isAutocompletionEnabled;
diff --git a/devtools/client/sourceeditor/codemirror/LICENSE b/devtools/client/sourceeditor/codemirror/LICENSE
new file mode 100644
index 000000000..4db615dd2
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/LICENSE
@@ -0,0 +1,23 @@
+Copyright (C) 2015 by Marijn Haverbeke <marijnh@gmail.com> and others
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+Please note that some subdirectories of the CodeMirror distribution
+include their own LICENSE files, and are released under different
+licences.
diff --git a/devtools/client/sourceeditor/codemirror/README b/devtools/client/sourceeditor/codemirror/README
new file mode 100644
index 000000000..38ab10e9c
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/README
@@ -0,0 +1,114 @@
+This is the CodeMirror editor packaged for the Mozilla Project. CodeMirror
+is a JavaScript component that provides a code editor in the browser. When
+a mode is available for the language you are coding in, it will color your
+code, and optionally help with indentation.
+
+# Upgrade
+
+Currently used version is 5.16.0. To upgrade: download a new version of
+CodeMirror from the project's page [1] and replace all JavaScript and
+CSS files inside the codemirror directory [2].
+
+Then to recreate codemirror.bundle.js:
+ > cd devtools/client
+ > npm install
+ > webpack
+
+To confirm the functionality run mochitests for the following components:
+
+ * sourceeditor
+ * scratchpad
+ * debugger
+ * styleditor
+ * netmonitor
+
+The sourceeditor component contains imported CodeMirror tests [3].
+
+ * Some tests were commented out because we don't use that functionality
+ within Firefox (for example Ruby editing mode). Be careful when updating
+ files test/codemirror.html and test/vimemacs.html; they were modified to
+ co-exist with Mozilla's testing infrastructure. Basically, vimemacs.html
+ is a copy of codemirror.html but only with VIM and Emacs mode tests
+ enabled.
+ * In cm_comment_test.js comment out fallbackToBlock and fallbackToLine
+ tests.
+ * The search addon (search.js) was slightly modified to make search
+ UI localizable (see patch below).
+
+Other than that, we don't have any Mozilla-specific patches applied to
+CodeMirror itself.
+
+# Addons
+
+To install a new CodeMirror addon add it to the codemirror directory,
+jar.mn [4] file and editor.js [5]. Also, add it to the License section
+below.
+
+# License
+
+The following files in this directory and devtools/client/sourceeditor/test/codemirror/
+are licensed according to the contents in the LICENSE file.
+
+# Localization patches
+
+diff --git a/devtools/client/sourceeditor/codemirror/addon/search/search.js b/devtools/client/sourceeditor/codemirror/addon/search/search.js
+--- a/devtools/client/sourceeditor/codemirror/addon/search/search.js
++++ b/devtools/client/sourceeditor/codemirror/addon/search/search.js
+@@ -92,32 +92,47 @@
+ } else {
+ query = parseString(query)
+ }
+ if (typeof query == "string" ? query == "" : query.test(""))
+ query = /x^/;
+ return query;
+ }
+
+- var queryDialog =
+- 'Search: <input type="text" style="width: 10em" class="CodeMirror-search-field"/> <span style="color: #888" class="CodeMirror-search-hint">(Use /re/ syntax for regexp search)</span>';
++ var queryDialog;
+
+ function startSearch(cm, state, query) {
+ state.queryText = query;
+ state.query = parseQuery(query);
+ cm.removeOverlay(state.overlay, queryCaseInsensitive(state.query));
+ state.overlay = searchOverlay(state.query, queryCaseInsensitive(state.query));
+ cm.addOverlay(state.overlay);
+ if (cm.showMatchesOnScrollbar) {
+ if (state.annotate) { state.annotate.clear(); state.annotate = null; }
+ state.annotate = cm.showMatchesOnScrollbar(state.query, queryCaseInsensitive(state.query));
+ }
+ }
+
+ function doSearch(cm, rev, persistent) {
++ if (!queryDialog) {
++ let doc = cm.getWrapperElement().ownerDocument;
++ let inp = doc.createElement("input");
++
++ inp.type = "search";
++ inp.placeholder = cm.l10n("findCmd.promptMessage");
++ inp.style.marginInlineStart = "1em";
++ inp.style.marginInlineEnd = "1em";
++ inp.style.flexGrow = "1";
++ inp.addEventListener("focus", () => inp.select());
++
++ queryDialog = doc.createElement("div");
++ queryDialog.appendChild(inp);
++ queryDialog.style.display = "flex";
++ }
++
+ var state = getSearchState(cm);
+ if (state.query) return findNext(cm, rev);
+ var q = cm.getSelection() || state.lastQuery;
+ if (persistent && cm.openDialog) {
+ var hiding = null
+ persistentDialog(cm, queryDialog, q, function(query, event) {
+ CodeMirror.e_stop(event);
+ if (!query) return;
+
+# Footnotes
+
+[1] http://codemirror.net
+[2] devtools/client/sourceeditor/codemirror
+[3] devtools/client/sourceeditor/test/browser_codemirror.js
+[4] devtools/client/jar.mn
+[5] devtools/client/sourceeditor/editor.js
diff --git a/devtools/client/sourceeditor/codemirror/addon/comment/comment.js b/devtools/client/sourceeditor/codemirror/addon/comment/comment.js
new file mode 100644
index 000000000..2c4f975d0
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/addon/comment/comment.js
@@ -0,0 +1,203 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../../lib/codemirror"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function(CodeMirror) {
+ "use strict";
+
+ var noOptions = {};
+ var nonWS = /[^\s\u00a0]/;
+ var Pos = CodeMirror.Pos;
+
+ function firstNonWS(str) {
+ var found = str.search(nonWS);
+ return found == -1 ? 0 : found;
+ }
+
+ CodeMirror.commands.toggleComment = function(cm) {
+ cm.toggleComment();
+ };
+
+ CodeMirror.defineExtension("toggleComment", function(options) {
+ if (!options) options = noOptions;
+ var cm = this;
+ var minLine = Infinity, ranges = this.listSelections(), mode = null;
+ for (var i = ranges.length - 1; i >= 0; i--) {
+ var from = ranges[i].from(), to = ranges[i].to();
+ if (from.line >= minLine) continue;
+ if (to.line >= minLine) to = Pos(minLine, 0);
+ minLine = from.line;
+ if (mode == null) {
+ if (cm.uncomment(from, to, options)) mode = "un";
+ else { cm.lineComment(from, to, options); mode = "line"; }
+ } else if (mode == "un") {
+ cm.uncomment(from, to, options);
+ } else {
+ cm.lineComment(from, to, options);
+ }
+ }
+ });
+
+ // Rough heuristic to try and detect lines that are part of multi-line string
+ function probablyInsideString(cm, pos, line) {
+ return /\bstring\b/.test(cm.getTokenTypeAt(Pos(pos.line, 0))) && !/^[\'\"`]/.test(line)
+ }
+
+ CodeMirror.defineExtension("lineComment", function(from, to, options) {
+ if (!options) options = noOptions;
+ var self = this, mode = self.getModeAt(from);
+ var firstLine = self.getLine(from.line);
+ if (firstLine == null || probablyInsideString(self, from, firstLine)) return;
+
+ var commentString = options.lineComment || mode.lineComment;
+ if (!commentString) {
+ if (options.blockCommentStart || mode.blockCommentStart) {
+ options.fullLines = true;
+ self.blockComment(from, to, options);
+ }
+ return;
+ }
+
+ var end = Math.min(to.ch != 0 || to.line == from.line ? to.line + 1 : to.line, self.lastLine() + 1);
+ var pad = options.padding == null ? " " : options.padding;
+ var blankLines = options.commentBlankLines || from.line == to.line;
+
+ self.operation(function() {
+ if (options.indent) {
+ var baseString = null;
+ for (var i = from.line; i < end; ++i) {
+ var line = self.getLine(i);
+ var whitespace = line.slice(0, firstNonWS(line));
+ if (baseString == null || baseString.length > whitespace.length) {
+ baseString = whitespace;
+ }
+ }
+ for (var i = from.line; i < end; ++i) {
+ var line = self.getLine(i), cut = baseString.length;
+ if (!blankLines && !nonWS.test(line)) continue;
+ if (line.slice(0, cut) != baseString) cut = firstNonWS(line);
+ self.replaceRange(baseString + commentString + pad, Pos(i, 0), Pos(i, cut));
+ }
+ } else {
+ for (var i = from.line; i < end; ++i) {
+ if (blankLines || nonWS.test(self.getLine(i)))
+ self.replaceRange(commentString + pad, Pos(i, 0));
+ }
+ }
+ });
+ });
+
+ CodeMirror.defineExtension("blockComment", function(from, to, options) {
+ if (!options) options = noOptions;
+ var self = this, mode = self.getModeAt(from);
+ var startString = options.blockCommentStart || mode.blockCommentStart;
+ var endString = options.blockCommentEnd || mode.blockCommentEnd;
+ if (!startString || !endString) {
+ if ((options.lineComment || mode.lineComment) && options.fullLines != false)
+ self.lineComment(from, to, options);
+ return;
+ }
+
+ var end = Math.min(to.line, self.lastLine());
+ if (end != from.line && to.ch == 0 && nonWS.test(self.getLine(end))) --end;
+
+ var pad = options.padding == null ? " " : options.padding;
+ if (from.line > end) return;
+
+ self.operation(function() {
+ if (options.fullLines != false) {
+ var lastLineHasText = nonWS.test(self.getLine(end));
+ self.replaceRange(pad + endString, Pos(end));
+ self.replaceRange(startString + pad, Pos(from.line, 0));
+ var lead = options.blockCommentLead || mode.blockCommentLead;
+ if (lead != null) for (var i = from.line + 1; i <= end; ++i)
+ if (i != end || lastLineHasText)
+ self.replaceRange(lead + pad, Pos(i, 0));
+ } else {
+ self.replaceRange(endString, to);
+ self.replaceRange(startString, from);
+ }
+ });
+ });
+
+ CodeMirror.defineExtension("uncomment", function(from, to, options) {
+ if (!options) options = noOptions;
+ var self = this, mode = self.getModeAt(from);
+ var end = Math.min(to.ch != 0 || to.line == from.line ? to.line : to.line - 1, self.lastLine()), start = Math.min(from.line, end);
+
+ // Try finding line comments
+ var lineString = options.lineComment || mode.lineComment, lines = [];
+ var pad = options.padding == null ? " " : options.padding, didSomething;
+ lineComment: {
+ if (!lineString) break lineComment;
+ for (var i = start; i <= end; ++i) {
+ var line = self.getLine(i);
+ var found = line.indexOf(lineString);
+ if (found > -1 && !/comment/.test(self.getTokenTypeAt(Pos(i, found + 1)))) found = -1;
+ if (found == -1 && (i != end || i == start) && nonWS.test(line)) break lineComment;
+ if (found > -1 && nonWS.test(line.slice(0, found))) break lineComment;
+ lines.push(line);
+ }
+ self.operation(function() {
+ for (var i = start; i <= end; ++i) {
+ var line = lines[i - start];
+ var pos = line.indexOf(lineString), endPos = pos + lineString.length;
+ if (pos < 0) continue;
+ if (line.slice(endPos, endPos + pad.length) == pad) endPos += pad.length;
+ didSomething = true;
+ self.replaceRange("", Pos(i, pos), Pos(i, endPos));
+ }
+ });
+ if (didSomething) return true;
+ }
+
+ // Try block comments
+ var startString = options.blockCommentStart || mode.blockCommentStart;
+ var endString = options.blockCommentEnd || mode.blockCommentEnd;
+ if (!startString || !endString) return false;
+ var lead = options.blockCommentLead || mode.blockCommentLead;
+ var startLine = self.getLine(start), endLine = end == start ? startLine : self.getLine(end);
+ var open = startLine.indexOf(startString), close = endLine.lastIndexOf(endString);
+ if (close == -1 && start != end) {
+ endLine = self.getLine(--end);
+ close = endLine.lastIndexOf(endString);
+ }
+ if (open == -1 || close == -1 ||
+ !/comment/.test(self.getTokenTypeAt(Pos(start, open + 1))) ||
+ !/comment/.test(self.getTokenTypeAt(Pos(end, close + 1))))
+ return false;
+
+ // Avoid killing block comments completely outside the selection.
+ // Positions of the last startString before the start of the selection, and the first endString after it.
+ var lastStart = startLine.lastIndexOf(startString, from.ch);
+ var firstEnd = lastStart == -1 ? -1 : startLine.slice(0, from.ch).indexOf(endString, lastStart + startString.length);
+ if (lastStart != -1 && firstEnd != -1 && firstEnd + endString.length != from.ch) return false;
+ // Positions of the first endString after the end of the selection, and the last startString before it.
+ firstEnd = endLine.indexOf(endString, to.ch);
+ var almostLastStart = endLine.slice(to.ch).lastIndexOf(startString, firstEnd - to.ch);
+ lastStart = (firstEnd == -1 || almostLastStart == -1) ? -1 : to.ch + almostLastStart;
+ if (firstEnd != -1 && lastStart != -1 && lastStart != to.ch) return false;
+
+ self.operation(function() {
+ self.replaceRange("", Pos(end, close - (pad && endLine.slice(close - pad.length, close) == pad ? pad.length : 0)),
+ Pos(end, close + endString.length));
+ var openEnd = open + startString.length;
+ if (pad && startLine.slice(openEnd, openEnd + pad.length) == pad) openEnd += pad.length;
+ self.replaceRange("", Pos(start, open), Pos(start, openEnd));
+ if (lead) for (var i = start + 1; i <= end; ++i) {
+ var line = self.getLine(i), found = line.indexOf(lead);
+ if (found == -1 || nonWS.test(line.slice(0, found))) continue;
+ var foundEnd = found + lead.length;
+ if (pad && line.slice(foundEnd, foundEnd + pad.length) == pad) foundEnd += pad.length;
+ self.replaceRange("", Pos(i, found), Pos(i, foundEnd));
+ }
+ });
+ return true;
+ });
+});
diff --git a/devtools/client/sourceeditor/codemirror/addon/comment/continuecomment.js b/devtools/client/sourceeditor/codemirror/addon/comment/continuecomment.js
new file mode 100644
index 000000000..b11d51e6c
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/addon/comment/continuecomment.js
@@ -0,0 +1,85 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../../lib/codemirror"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function(CodeMirror) {
+ var modes = ["clike", "css", "javascript"];
+
+ for (var i = 0; i < modes.length; ++i)
+ CodeMirror.extendMode(modes[i], {blockCommentContinue: " * "});
+
+ function continueComment(cm) {
+ if (cm.getOption("disableInput")) return CodeMirror.Pass;
+ var ranges = cm.listSelections(), mode, inserts = [];
+ for (var i = 0; i < ranges.length; i++) {
+ var pos = ranges[i].head, token = cm.getTokenAt(pos);
+ if (token.type != "comment") return CodeMirror.Pass;
+ var modeHere = CodeMirror.innerMode(cm.getMode(), token.state).mode;
+ if (!mode) mode = modeHere;
+ else if (mode != modeHere) return CodeMirror.Pass;
+
+ var insert = null;
+ if (mode.blockCommentStart && mode.blockCommentContinue) {
+ var end = token.string.indexOf(mode.blockCommentEnd);
+ var full = cm.getRange(CodeMirror.Pos(pos.line, 0), CodeMirror.Pos(pos.line, token.end)), found;
+ if (end != -1 && end == token.string.length - mode.blockCommentEnd.length && pos.ch >= end) {
+ // Comment ended, don't continue it
+ } else if (token.string.indexOf(mode.blockCommentStart) == 0) {
+ insert = full.slice(0, token.start);
+ if (!/^\s*$/.test(insert)) {
+ insert = "";
+ for (var j = 0; j < token.start; ++j) insert += " ";
+ }
+ } else if ((found = full.indexOf(mode.blockCommentContinue)) != -1 &&
+ found + mode.blockCommentContinue.length > token.start &&
+ /^\s*$/.test(full.slice(0, found))) {
+ insert = full.slice(0, found);
+ }
+ if (insert != null) insert += mode.blockCommentContinue;
+ }
+ if (insert == null && mode.lineComment && continueLineCommentEnabled(cm)) {
+ var line = cm.getLine(pos.line), found = line.indexOf(mode.lineComment);
+ if (found > -1) {
+ insert = line.slice(0, found);
+ if (/\S/.test(insert)) insert = null;
+ else insert += mode.lineComment + line.slice(found + mode.lineComment.length).match(/^\s*/)[0];
+ }
+ }
+ if (insert == null) return CodeMirror.Pass;
+ inserts[i] = "\n" + insert;
+ }
+
+ cm.operation(function() {
+ for (var i = ranges.length - 1; i >= 0; i--)
+ cm.replaceRange(inserts[i], ranges[i].from(), ranges[i].to(), "+insert");
+ });
+ }
+
+ function continueLineCommentEnabled(cm) {
+ var opt = cm.getOption("continueComments");
+ if (opt && typeof opt == "object")
+ return opt.continueLineComment !== false;
+ return true;
+ }
+
+ CodeMirror.defineOption("continueComments", null, function(cm, val, prev) {
+ if (prev && prev != CodeMirror.Init)
+ cm.removeKeyMap("continueComment");
+ if (val) {
+ var key = "Enter";
+ if (typeof val == "string")
+ key = val;
+ else if (typeof val == "object" && val.key)
+ key = val.key;
+ var map = {name: "continueComment"};
+ map[key] = continueComment;
+ cm.addKeyMap(map);
+ }
+ });
+});
diff --git a/devtools/client/sourceeditor/codemirror/addon/dialog/dialog.css b/devtools/client/sourceeditor/codemirror/addon/dialog/dialog.css
new file mode 100644
index 000000000..677c07838
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/addon/dialog/dialog.css
@@ -0,0 +1,32 @@
+.CodeMirror-dialog {
+ position: absolute;
+ left: 0; right: 0;
+ background: inherit;
+ z-index: 15;
+ padding: .1em .8em;
+ overflow: hidden;
+ color: inherit;
+}
+
+.CodeMirror-dialog-top {
+ border-bottom: 1px solid #eee;
+ top: 0;
+}
+
+.CodeMirror-dialog-bottom {
+ border-top: 1px solid #eee;
+ bottom: 0;
+}
+
+.CodeMirror-dialog input {
+ border: none;
+ outline: none;
+ background: transparent;
+ width: 20em;
+ color: inherit;
+ font-family: monospace;
+}
+
+.CodeMirror-dialog button {
+ font-size: 70%;
+}
diff --git a/devtools/client/sourceeditor/codemirror/addon/dialog/dialog.js b/devtools/client/sourceeditor/codemirror/addon/dialog/dialog.js
new file mode 100644
index 000000000..f10bb5bf1
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/addon/dialog/dialog.js
@@ -0,0 +1,157 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+// Open simple dialogs on top of an editor. Relies on dialog.css.
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../../lib/codemirror"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function(CodeMirror) {
+ function dialogDiv(cm, template, bottom) {
+ var wrap = cm.getWrapperElement();
+ var dialog;
+ dialog = wrap.appendChild(document.createElement("div"));
+ if (bottom)
+ dialog.className = "CodeMirror-dialog CodeMirror-dialog-bottom";
+ else
+ dialog.className = "CodeMirror-dialog CodeMirror-dialog-top";
+
+ if (typeof template == "string") {
+ dialog.innerHTML = template;
+ } else { // Assuming it's a detached DOM element.
+ dialog.appendChild(template);
+ }
+ return dialog;
+ }
+
+ function closeNotification(cm, newVal) {
+ if (cm.state.currentNotificationClose)
+ cm.state.currentNotificationClose();
+ cm.state.currentNotificationClose = newVal;
+ }
+
+ CodeMirror.defineExtension("openDialog", function(template, callback, options) {
+ if (!options) options = {};
+
+ closeNotification(this, null);
+
+ var dialog = dialogDiv(this, template, options.bottom);
+ var closed = false, me = this;
+ function close(newVal) {
+ if (typeof newVal == 'string') {
+ inp.value = newVal;
+ } else {
+ if (closed) return;
+ closed = true;
+ dialog.parentNode.removeChild(dialog);
+ me.focus();
+
+ if (options.onClose) options.onClose(dialog);
+ }
+ }
+
+ var inp = dialog.getElementsByTagName("input")[0], button;
+ if (inp) {
+ inp.focus();
+
+ if (options.value) {
+ inp.value = options.value;
+ if (options.selectValueOnOpen !== false) {
+ inp.select();
+ }
+ }
+
+ if (options.onInput)
+ CodeMirror.on(inp, "input", function(e) { options.onInput(e, inp.value, close);});
+ if (options.onKeyUp)
+ CodeMirror.on(inp, "keyup", function(e) {options.onKeyUp(e, inp.value, close);});
+
+ CodeMirror.on(inp, "keydown", function(e) {
+ if (options && options.onKeyDown && options.onKeyDown(e, inp.value, close)) { return; }
+ if (e.keyCode == 27 || (options.closeOnEnter !== false && e.keyCode == 13)) {
+ inp.blur();
+ CodeMirror.e_stop(e);
+ close();
+ }
+ if (e.keyCode == 13) callback(inp.value, e);
+ });
+
+ if (options.closeOnBlur !== false) CodeMirror.on(inp, "blur", close);
+ } else if (button = dialog.getElementsByTagName("button")[0]) {
+ CodeMirror.on(button, "click", function() {
+ close();
+ me.focus();
+ });
+
+ if (options.closeOnBlur !== false) CodeMirror.on(button, "blur", close);
+
+ button.focus();
+ }
+ return close;
+ });
+
+ CodeMirror.defineExtension("openConfirm", function(template, callbacks, options) {
+ closeNotification(this, null);
+ var dialog = dialogDiv(this, template, options && options.bottom);
+ var buttons = dialog.getElementsByTagName("button");
+ var closed = false, me = this, blurring = 1;
+ function close() {
+ if (closed) return;
+ closed = true;
+ dialog.parentNode.removeChild(dialog);
+ me.focus();
+ }
+ buttons[0].focus();
+ for (var i = 0; i < buttons.length; ++i) {
+ var b = buttons[i];
+ (function(callback) {
+ CodeMirror.on(b, "click", function(e) {
+ CodeMirror.e_preventDefault(e);
+ close();
+ if (callback) callback(me);
+ });
+ })(callbacks[i]);
+ CodeMirror.on(b, "blur", function() {
+ --blurring;
+ setTimeout(function() { if (blurring <= 0) close(); }, 200);
+ });
+ CodeMirror.on(b, "focus", function() { ++blurring; });
+ }
+ });
+
+ /*
+ * openNotification
+ * Opens a notification, that can be closed with an optional timer
+ * (default 5000ms timer) and always closes on click.
+ *
+ * If a notification is opened while another is opened, it will close the
+ * currently opened one and open the new one immediately.
+ */
+ CodeMirror.defineExtension("openNotification", function(template, options) {
+ closeNotification(this, close);
+ var dialog = dialogDiv(this, template, options && options.bottom);
+ var closed = false, doneTimer;
+ var duration = options && typeof options.duration !== "undefined" ? options.duration : 5000;
+
+ function close() {
+ if (closed) return;
+ closed = true;
+ clearTimeout(doneTimer);
+ dialog.parentNode.removeChild(dialog);
+ }
+
+ CodeMirror.on(dialog, 'click', function(e) {
+ CodeMirror.e_preventDefault(e);
+ close();
+ });
+
+ if (duration)
+ doneTimer = setTimeout(close, duration);
+
+ return close;
+ });
+});
diff --git a/devtools/client/sourceeditor/codemirror/addon/edit/closebrackets.js b/devtools/client/sourceeditor/codemirror/addon/edit/closebrackets.js
new file mode 100644
index 000000000..af7fce2a8
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/addon/edit/closebrackets.js
@@ -0,0 +1,195 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../../lib/codemirror"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function(CodeMirror) {
+ var defaults = {
+ pairs: "()[]{}''\"\"",
+ triples: "",
+ explode: "[]{}"
+ };
+
+ var Pos = CodeMirror.Pos;
+
+ CodeMirror.defineOption("autoCloseBrackets", false, function(cm, val, old) {
+ if (old && old != CodeMirror.Init) {
+ cm.removeKeyMap(keyMap);
+ cm.state.closeBrackets = null;
+ }
+ if (val) {
+ cm.state.closeBrackets = val;
+ cm.addKeyMap(keyMap);
+ }
+ });
+
+ function getOption(conf, name) {
+ if (name == "pairs" && typeof conf == "string") return conf;
+ if (typeof conf == "object" && conf[name] != null) return conf[name];
+ return defaults[name];
+ }
+
+ var bind = defaults.pairs + "`";
+ var keyMap = {Backspace: handleBackspace, Enter: handleEnter};
+ for (var i = 0; i < bind.length; i++)
+ keyMap["'" + bind.charAt(i) + "'"] = handler(bind.charAt(i));
+
+ function handler(ch) {
+ return function(cm) { return handleChar(cm, ch); };
+ }
+
+ function getConfig(cm) {
+ var deflt = cm.state.closeBrackets;
+ if (!deflt) return null;
+ var mode = cm.getModeAt(cm.getCursor());
+ return mode.closeBrackets || deflt;
+ }
+
+ function handleBackspace(cm) {
+ var conf = getConfig(cm);
+ if (!conf || cm.getOption("disableInput")) return CodeMirror.Pass;
+
+ var pairs = getOption(conf, "pairs");
+ var ranges = cm.listSelections();
+ for (var i = 0; i < ranges.length; i++) {
+ if (!ranges[i].empty()) return CodeMirror.Pass;
+ var around = charsAround(cm, ranges[i].head);
+ if (!around || pairs.indexOf(around) % 2 != 0) return CodeMirror.Pass;
+ }
+ for (var i = ranges.length - 1; i >= 0; i--) {
+ var cur = ranges[i].head;
+ cm.replaceRange("", Pos(cur.line, cur.ch - 1), Pos(cur.line, cur.ch + 1), "+delete");
+ }
+ }
+
+ function handleEnter(cm) {
+ var conf = getConfig(cm);
+ var explode = conf && getOption(conf, "explode");
+ if (!explode || cm.getOption("disableInput")) return CodeMirror.Pass;
+
+ var ranges = cm.listSelections();
+ for (var i = 0; i < ranges.length; i++) {
+ if (!ranges[i].empty()) return CodeMirror.Pass;
+ var around = charsAround(cm, ranges[i].head);
+ if (!around || explode.indexOf(around) % 2 != 0) return CodeMirror.Pass;
+ }
+ cm.operation(function() {
+ cm.replaceSelection("\n\n", null);
+ cm.execCommand("goCharLeft");
+ ranges = cm.listSelections();
+ for (var i = 0; i < ranges.length; i++) {
+ var line = ranges[i].head.line;
+ cm.indentLine(line, null, true);
+ cm.indentLine(line + 1, null, true);
+ }
+ });
+ }
+
+ function contractSelection(sel) {
+ var inverted = CodeMirror.cmpPos(sel.anchor, sel.head) > 0;
+ return {anchor: new Pos(sel.anchor.line, sel.anchor.ch + (inverted ? -1 : 1)),
+ head: new Pos(sel.head.line, sel.head.ch + (inverted ? 1 : -1))};
+ }
+
+ function handleChar(cm, ch) {
+ var conf = getConfig(cm);
+ if (!conf || cm.getOption("disableInput")) return CodeMirror.Pass;
+
+ var pairs = getOption(conf, "pairs");
+ var pos = pairs.indexOf(ch);
+ if (pos == -1) return CodeMirror.Pass;
+ var triples = getOption(conf, "triples");
+
+ var identical = pairs.charAt(pos + 1) == ch;
+ var ranges = cm.listSelections();
+ var opening = pos % 2 == 0;
+
+ var type;
+ for (var i = 0; i < ranges.length; i++) {
+ var range = ranges[i], cur = range.head, curType;
+ var next = cm.getRange(cur, Pos(cur.line, cur.ch + 1));
+ if (opening && !range.empty()) {
+ curType = "surround";
+ } else if ((identical || !opening) && next == ch) {
+ if (triples.indexOf(ch) >= 0 && cm.getRange(cur, Pos(cur.line, cur.ch + 3)) == ch + ch + ch)
+ curType = "skipThree";
+ else
+ curType = "skip";
+ } else if (identical && cur.ch > 1 && triples.indexOf(ch) >= 0 &&
+ cm.getRange(Pos(cur.line, cur.ch - 2), cur) == ch + ch &&
+ (cur.ch <= 2 || cm.getRange(Pos(cur.line, cur.ch - 3), Pos(cur.line, cur.ch - 2)) != ch)) {
+ curType = "addFour";
+ } else if (identical) {
+ if (!CodeMirror.isWordChar(next) && enteringString(cm, cur, ch)) curType = "both";
+ else return CodeMirror.Pass;
+ } else if (opening && (cm.getLine(cur.line).length == cur.ch ||
+ isClosingBracket(next, pairs) ||
+ /\s/.test(next))) {
+ curType = "both";
+ } else {
+ return CodeMirror.Pass;
+ }
+ if (!type) type = curType;
+ else if (type != curType) return CodeMirror.Pass;
+ }
+
+ var left = pos % 2 ? pairs.charAt(pos - 1) : ch;
+ var right = pos % 2 ? ch : pairs.charAt(pos + 1);
+ cm.operation(function() {
+ if (type == "skip") {
+ cm.execCommand("goCharRight");
+ } else if (type == "skipThree") {
+ for (var i = 0; i < 3; i++)
+ cm.execCommand("goCharRight");
+ } else if (type == "surround") {
+ var sels = cm.getSelections();
+ for (var i = 0; i < sels.length; i++)
+ sels[i] = left + sels[i] + right;
+ cm.replaceSelections(sels, "around");
+ sels = cm.listSelections().slice();
+ for (var i = 0; i < sels.length; i++)
+ sels[i] = contractSelection(sels[i]);
+ cm.setSelections(sels);
+ } else if (type == "both") {
+ cm.replaceSelection(left + right, null);
+ cm.triggerElectric(left + right);
+ cm.execCommand("goCharLeft");
+ } else if (type == "addFour") {
+ cm.replaceSelection(left + left + left + left, "before");
+ cm.execCommand("goCharRight");
+ }
+ });
+ }
+
+ function isClosingBracket(ch, pairs) {
+ var pos = pairs.lastIndexOf(ch);
+ return pos > -1 && pos % 2 == 1;
+ }
+
+ function charsAround(cm, pos) {
+ var str = cm.getRange(Pos(pos.line, pos.ch - 1),
+ Pos(pos.line, pos.ch + 1));
+ return str.length == 2 ? str : null;
+ }
+
+ // Project the token type that will exists after the given char is
+ // typed, and use it to determine whether it would cause the start
+ // of a string token.
+ function enteringString(cm, pos, ch) {
+ var line = cm.getLine(pos.line);
+ var token = cm.getTokenAt(pos);
+ if (/\bstring2?\b/.test(token.type)) return false;
+ var stream = new CodeMirror.StringStream(line.slice(0, pos.ch) + ch + line.slice(pos.ch), 4);
+ stream.pos = stream.start = token.start;
+ for (;;) {
+ var type1 = cm.getMode().token(stream, token.state);
+ if (stream.pos >= pos.ch + 1) return /\bstring2?\b/.test(type1);
+ stream.start = stream.pos;
+ }
+ }
+});
diff --git a/devtools/client/sourceeditor/codemirror/addon/edit/closetag.js b/devtools/client/sourceeditor/codemirror/addon/edit/closetag.js
new file mode 100644
index 000000000..a518da3ec
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/addon/edit/closetag.js
@@ -0,0 +1,169 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+/**
+ * Tag-closer extension for CodeMirror.
+ *
+ * This extension adds an "autoCloseTags" option that can be set to
+ * either true to get the default behavior, or an object to further
+ * configure its behavior.
+ *
+ * These are supported options:
+ *
+ * `whenClosing` (default true)
+ * Whether to autoclose when the '/' of a closing tag is typed.
+ * `whenOpening` (default true)
+ * Whether to autoclose the tag when the final '>' of an opening
+ * tag is typed.
+ * `dontCloseTags` (default is empty tags for HTML, none for XML)
+ * An array of tag names that should not be autoclosed.
+ * `indentTags` (default is block tags for HTML, none for XML)
+ * An array of tag names that should, when opened, cause a
+ * blank line to be added inside the tag, and the blank line and
+ * closing line to be indented.
+ *
+ * See demos/closetag.html for a usage example.
+ */
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../../lib/codemirror"), require("../fold/xml-fold"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror", "../fold/xml-fold"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function(CodeMirror) {
+ CodeMirror.defineOption("autoCloseTags", false, function(cm, val, old) {
+ if (old != CodeMirror.Init && old)
+ cm.removeKeyMap("autoCloseTags");
+ if (!val) return;
+ var map = {name: "autoCloseTags"};
+ if (typeof val != "object" || val.whenClosing)
+ map["'/'"] = function(cm) { return autoCloseSlash(cm); };
+ if (typeof val != "object" || val.whenOpening)
+ map["'>'"] = function(cm) { return autoCloseGT(cm); };
+ cm.addKeyMap(map);
+ });
+
+ var htmlDontClose = ["area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "meta", "param",
+ "source", "track", "wbr"];
+ var htmlIndent = ["applet", "blockquote", "body", "button", "div", "dl", "fieldset", "form", "frameset", "h1", "h2", "h3", "h4",
+ "h5", "h6", "head", "html", "iframe", "layer", "legend", "object", "ol", "p", "select", "table", "ul"];
+
+ function autoCloseGT(cm) {
+ if (cm.getOption("disableInput")) return CodeMirror.Pass;
+ var ranges = cm.listSelections(), replacements = [];
+ for (var i = 0; i < ranges.length; i++) {
+ if (!ranges[i].empty()) return CodeMirror.Pass;
+ var pos = ranges[i].head, tok = cm.getTokenAt(pos);
+ var inner = CodeMirror.innerMode(cm.getMode(), tok.state), state = inner.state;
+ if (inner.mode.name != "xml" || !state.tagName) return CodeMirror.Pass;
+
+ var opt = cm.getOption("autoCloseTags"), html = inner.mode.configuration == "html";
+ var dontCloseTags = (typeof opt == "object" && opt.dontCloseTags) || (html && htmlDontClose);
+ var indentTags = (typeof opt == "object" && opt.indentTags) || (html && htmlIndent);
+
+ var tagName = state.tagName;
+ if (tok.end > pos.ch) tagName = tagName.slice(0, tagName.length - tok.end + pos.ch);
+ var lowerTagName = tagName.toLowerCase();
+ // Don't process the '>' at the end of an end-tag or self-closing tag
+ if (!tagName ||
+ tok.type == "string" && (tok.end != pos.ch || !/[\"\']/.test(tok.string.charAt(tok.string.length - 1)) || tok.string.length == 1) ||
+ tok.type == "tag" && state.type == "closeTag" ||
+ tok.string.indexOf("/") == (tok.string.length - 1) || // match something like <someTagName />
+ dontCloseTags && indexOf(dontCloseTags, lowerTagName) > -1 ||
+ closingTagExists(cm, tagName, pos, state, true))
+ return CodeMirror.Pass;
+
+ var indent = indentTags && indexOf(indentTags, lowerTagName) > -1;
+ replacements[i] = {indent: indent,
+ text: ">" + (indent ? "\n\n" : "") + "</" + tagName + ">",
+ newPos: indent ? CodeMirror.Pos(pos.line + 1, 0) : CodeMirror.Pos(pos.line, pos.ch + 1)};
+ }
+
+ for (var i = ranges.length - 1; i >= 0; i--) {
+ var info = replacements[i];
+ cm.replaceRange(info.text, ranges[i].head, ranges[i].anchor, "+insert");
+ var sel = cm.listSelections().slice(0);
+ sel[i] = {head: info.newPos, anchor: info.newPos};
+ cm.setSelections(sel);
+ if (info.indent) {
+ cm.indentLine(info.newPos.line, null, true);
+ cm.indentLine(info.newPos.line + 1, null, true);
+ }
+ }
+ }
+
+ function autoCloseCurrent(cm, typingSlash) {
+ var ranges = cm.listSelections(), replacements = [];
+ var head = typingSlash ? "/" : "</";
+ for (var i = 0; i < ranges.length; i++) {
+ if (!ranges[i].empty()) return CodeMirror.Pass;
+ var pos = ranges[i].head, tok = cm.getTokenAt(pos);
+ var inner = CodeMirror.innerMode(cm.getMode(), tok.state), state = inner.state;
+ if (typingSlash && (tok.type == "string" || tok.string.charAt(0) != "<" ||
+ tok.start != pos.ch - 1))
+ return CodeMirror.Pass;
+ // Kludge to get around the fact that we are not in XML mode
+ // when completing in JS/CSS snippet in htmlmixed mode. Does not
+ // work for other XML embedded languages (there is no general
+ // way to go from a mixed mode to its current XML state).
+ var replacement;
+ if (inner.mode.name != "xml") {
+ if (cm.getMode().name == "htmlmixed" && inner.mode.name == "javascript")
+ replacement = head + "script";
+ else if (cm.getMode().name == "htmlmixed" && inner.mode.name == "css")
+ replacement = head + "style";
+ else
+ return CodeMirror.Pass;
+ } else {
+ if (!state.context || !state.context.tagName ||
+ closingTagExists(cm, state.context.tagName, pos, state))
+ return CodeMirror.Pass;
+ replacement = head + state.context.tagName;
+ }
+ if (cm.getLine(pos.line).charAt(tok.end) != ">") replacement += ">";
+ replacements[i] = replacement;
+ }
+ cm.replaceSelections(replacements);
+ ranges = cm.listSelections();
+ for (var i = 0; i < ranges.length; i++)
+ if (i == ranges.length - 1 || ranges[i].head.line < ranges[i + 1].head.line)
+ cm.indentLine(ranges[i].head.line);
+ }
+
+ function autoCloseSlash(cm) {
+ if (cm.getOption("disableInput")) return CodeMirror.Pass;
+ return autoCloseCurrent(cm, true);
+ }
+
+ CodeMirror.commands.closeTag = function(cm) { return autoCloseCurrent(cm); };
+
+ function indexOf(collection, elt) {
+ if (collection.indexOf) return collection.indexOf(elt);
+ for (var i = 0, e = collection.length; i < e; ++i)
+ if (collection[i] == elt) return i;
+ return -1;
+ }
+
+ // If xml-fold is loaded, we use its functionality to try and verify
+ // whether a given tag is actually unclosed.
+ function closingTagExists(cm, tagName, pos, state, newTag) {
+ if (!CodeMirror.scanForClosingTag) return false;
+ var end = Math.min(cm.lastLine() + 1, pos.line + 500);
+ var nextClose = CodeMirror.scanForClosingTag(cm, pos, null, end);
+ if (!nextClose || nextClose.tag != tagName) return false;
+ var cx = state.context;
+ // If the immediate wrapping context contains onCx instances of
+ // the same tag, a closing tag only exists if there are at least
+ // that many closing tags of that type following.
+ for (var onCx = newTag ? 1 : 0; cx && cx.tagName == tagName; cx = cx.prev) ++onCx;
+ pos = nextClose.to;
+ for (var i = 1; i < onCx; i++) {
+ var next = CodeMirror.scanForClosingTag(cm, pos, null, end);
+ if (!next || next.tag != tagName) return false;
+ pos = next.to;
+ }
+ return true;
+ }
+});
diff --git a/devtools/client/sourceeditor/codemirror/addon/edit/continuelist.js b/devtools/client/sourceeditor/codemirror/addon/edit/continuelist.js
new file mode 100644
index 000000000..df5179fe4
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/addon/edit/continuelist.js
@@ -0,0 +1,51 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../../lib/codemirror"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function(CodeMirror) {
+ "use strict";
+
+ var listRE = /^(\s*)(>[> ]*|[*+-]\s|(\d+)([.)]))(\s*)/,
+ emptyListRE = /^(\s*)(>[> ]*|[*+-]|(\d+)[.)])(\s*)$/,
+ unorderedListRE = /[*+-]\s/;
+
+ CodeMirror.commands.newlineAndIndentContinueMarkdownList = function(cm) {
+ if (cm.getOption("disableInput")) return CodeMirror.Pass;
+ var ranges = cm.listSelections(), replacements = [];
+ for (var i = 0; i < ranges.length; i++) {
+ var pos = ranges[i].head;
+ var eolState = cm.getStateAfter(pos.line);
+ var inList = eolState.list !== false;
+ var inQuote = eolState.quote !== 0;
+
+ var line = cm.getLine(pos.line), match = listRE.exec(line);
+ if (!ranges[i].empty() || (!inList && !inQuote) || !match) {
+ cm.execCommand("newlineAndIndent");
+ return;
+ }
+ if (emptyListRE.test(line)) {
+ cm.replaceRange("", {
+ line: pos.line, ch: 0
+ }, {
+ line: pos.line, ch: pos.ch + 1
+ });
+ replacements[i] = "\n";
+ } else {
+ var indent = match[1], after = match[5];
+ var bullet = unorderedListRE.test(match[2]) || match[2].indexOf(">") >= 0
+ ? match[2]
+ : (parseInt(match[3], 10) + 1) + match[4];
+
+ replacements[i] = "\n" + indent + bullet + after;
+ }
+ }
+
+ cm.replaceSelections(replacements);
+ };
+});
diff --git a/devtools/client/sourceeditor/codemirror/addon/edit/matchbrackets.js b/devtools/client/sourceeditor/codemirror/addon/edit/matchbrackets.js
new file mode 100644
index 000000000..70e1ae18c
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/addon/edit/matchbrackets.js
@@ -0,0 +1,120 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../../lib/codemirror"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function(CodeMirror) {
+ var ie_lt8 = /MSIE \d/.test(navigator.userAgent) &&
+ (document.documentMode == null || document.documentMode < 8);
+
+ var Pos = CodeMirror.Pos;
+
+ var matching = {"(": ")>", ")": "(<", "[": "]>", "]": "[<", "{": "}>", "}": "{<"};
+
+ function findMatchingBracket(cm, where, strict, config) {
+ var line = cm.getLineHandle(where.line), pos = where.ch - 1;
+ var match = (pos >= 0 && matching[line.text.charAt(pos)]) || matching[line.text.charAt(++pos)];
+ if (!match) return null;
+ var dir = match.charAt(1) == ">" ? 1 : -1;
+ if (strict && (dir > 0) != (pos == where.ch)) return null;
+ var style = cm.getTokenTypeAt(Pos(where.line, pos + 1));
+
+ var found = scanForBracket(cm, Pos(where.line, pos + (dir > 0 ? 1 : 0)), dir, style || null, config);
+ if (found == null) return null;
+ return {from: Pos(where.line, pos), to: found && found.pos,
+ match: found && found.ch == match.charAt(0), forward: dir > 0};
+ }
+
+ // bracketRegex is used to specify which type of bracket to scan
+ // should be a regexp, e.g. /[[\]]/
+ //
+ // Note: If "where" is on an open bracket, then this bracket is ignored.
+ //
+ // Returns false when no bracket was found, null when it reached
+ // maxScanLines and gave up
+ function scanForBracket(cm, where, dir, style, config) {
+ var maxScanLen = (config && config.maxScanLineLength) || 10000;
+ var maxScanLines = (config && config.maxScanLines) || 1000;
+
+ var stack = [];
+ var re = config && config.bracketRegex ? config.bracketRegex : /[(){}[\]]/;
+ var lineEnd = dir > 0 ? Math.min(where.line + maxScanLines, cm.lastLine() + 1)
+ : Math.max(cm.firstLine() - 1, where.line - maxScanLines);
+ for (var lineNo = where.line; lineNo != lineEnd; lineNo += dir) {
+ var line = cm.getLine(lineNo);
+ if (!line) continue;
+ var pos = dir > 0 ? 0 : line.length - 1, end = dir > 0 ? line.length : -1;
+ if (line.length > maxScanLen) continue;
+ if (lineNo == where.line) pos = where.ch - (dir < 0 ? 1 : 0);
+ for (; pos != end; pos += dir) {
+ var ch = line.charAt(pos);
+ if (re.test(ch) && (style === undefined || cm.getTokenTypeAt(Pos(lineNo, pos + 1)) == style)) {
+ var match = matching[ch];
+ if ((match.charAt(1) == ">") == (dir > 0)) stack.push(ch);
+ else if (!stack.length) return {pos: Pos(lineNo, pos), ch: ch};
+ else stack.pop();
+ }
+ }
+ }
+ return lineNo - dir == (dir > 0 ? cm.lastLine() : cm.firstLine()) ? false : null;
+ }
+
+ function matchBrackets(cm, autoclear, config) {
+ // Disable brace matching in long lines, since it'll cause hugely slow updates
+ var maxHighlightLen = cm.state.matchBrackets.maxHighlightLineLength || 1000;
+ var marks = [], ranges = cm.listSelections();
+ for (var i = 0; i < ranges.length; i++) {
+ var match = ranges[i].empty() && findMatchingBracket(cm, ranges[i].head, false, config);
+ if (match && cm.getLine(match.from.line).length <= maxHighlightLen) {
+ var style = match.match ? "CodeMirror-matchingbracket" : "CodeMirror-nonmatchingbracket";
+ marks.push(cm.markText(match.from, Pos(match.from.line, match.from.ch + 1), {className: style}));
+ if (match.to && cm.getLine(match.to.line).length <= maxHighlightLen)
+ marks.push(cm.markText(match.to, Pos(match.to.line, match.to.ch + 1), {className: style}));
+ }
+ }
+
+ if (marks.length) {
+ // Kludge to work around the IE bug from issue #1193, where text
+ // input stops going to the textare whever this fires.
+ if (ie_lt8 && cm.state.focused) cm.focus();
+
+ var clear = function() {
+ cm.operation(function() {
+ for (var i = 0; i < marks.length; i++) marks[i].clear();
+ });
+ };
+ if (autoclear) setTimeout(clear, 800);
+ else return clear;
+ }
+ }
+
+ var currentlyHighlighted = null;
+ function doMatchBrackets(cm) {
+ cm.operation(function() {
+ if (currentlyHighlighted) {currentlyHighlighted(); currentlyHighlighted = null;}
+ currentlyHighlighted = matchBrackets(cm, false, cm.state.matchBrackets);
+ });
+ }
+
+ CodeMirror.defineOption("matchBrackets", false, function(cm, val, old) {
+ if (old && old != CodeMirror.Init)
+ cm.off("cursorActivity", doMatchBrackets);
+ if (val) {
+ cm.state.matchBrackets = typeof val == "object" ? val : {};
+ cm.on("cursorActivity", doMatchBrackets);
+ }
+ });
+
+ CodeMirror.defineExtension("matchBrackets", function() {matchBrackets(this, true);});
+ CodeMirror.defineExtension("findMatchingBracket", function(pos, strict, config){
+ return findMatchingBracket(this, pos, strict, config);
+ });
+ CodeMirror.defineExtension("scanForBracket", function(pos, dir, style, config){
+ return scanForBracket(this, pos, dir, style, config);
+ });
+});
diff --git a/devtools/client/sourceeditor/codemirror/addon/edit/matchtags.js b/devtools/client/sourceeditor/codemirror/addon/edit/matchtags.js
new file mode 100644
index 000000000..fb1911a8d
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/addon/edit/matchtags.js
@@ -0,0 +1,66 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../../lib/codemirror"), require("../fold/xml-fold"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror", "../fold/xml-fold"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function(CodeMirror) {
+ "use strict";
+
+ CodeMirror.defineOption("matchTags", false, function(cm, val, old) {
+ if (old && old != CodeMirror.Init) {
+ cm.off("cursorActivity", doMatchTags);
+ cm.off("viewportChange", maybeUpdateMatch);
+ clear(cm);
+ }
+ if (val) {
+ cm.state.matchBothTags = typeof val == "object" && val.bothTags;
+ cm.on("cursorActivity", doMatchTags);
+ cm.on("viewportChange", maybeUpdateMatch);
+ doMatchTags(cm);
+ }
+ });
+
+ function clear(cm) {
+ if (cm.state.tagHit) cm.state.tagHit.clear();
+ if (cm.state.tagOther) cm.state.tagOther.clear();
+ cm.state.tagHit = cm.state.tagOther = null;
+ }
+
+ function doMatchTags(cm) {
+ cm.state.failedTagMatch = false;
+ cm.operation(function() {
+ clear(cm);
+ if (cm.somethingSelected()) return;
+ var cur = cm.getCursor(), range = cm.getViewport();
+ range.from = Math.min(range.from, cur.line); range.to = Math.max(cur.line + 1, range.to);
+ var match = CodeMirror.findMatchingTag(cm, cur, range);
+ if (!match) return;
+ if (cm.state.matchBothTags) {
+ var hit = match.at == "open" ? match.open : match.close;
+ if (hit) cm.state.tagHit = cm.markText(hit.from, hit.to, {className: "CodeMirror-matchingtag"});
+ }
+ var other = match.at == "close" ? match.open : match.close;
+ if (other)
+ cm.state.tagOther = cm.markText(other.from, other.to, {className: "CodeMirror-matchingtag"});
+ else
+ cm.state.failedTagMatch = true;
+ });
+ }
+
+ function maybeUpdateMatch(cm) {
+ if (cm.state.failedTagMatch) doMatchTags(cm);
+ }
+
+ CodeMirror.commands.toMatchingTag = function(cm) {
+ var found = CodeMirror.findMatchingTag(cm, cm.getCursor());
+ if (found) {
+ var other = found.at == "close" ? found.open : found.close;
+ if (other) cm.extendSelection(other.to, other.from);
+ }
+ };
+});
diff --git a/devtools/client/sourceeditor/codemirror/addon/edit/trailingspace.js b/devtools/client/sourceeditor/codemirror/addon/edit/trailingspace.js
new file mode 100644
index 000000000..fa7b56be5
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/addon/edit/trailingspace.js
@@ -0,0 +1,27 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../../lib/codemirror"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function(CodeMirror) {
+ CodeMirror.defineOption("showTrailingSpace", false, function(cm, val, prev) {
+ if (prev == CodeMirror.Init) prev = false;
+ if (prev && !val)
+ cm.removeOverlay("trailingspace");
+ else if (!prev && val)
+ cm.addOverlay({
+ token: function(stream) {
+ for (var l = stream.string.length, i = l; i && /\s/.test(stream.string.charAt(i - 1)); --i) {}
+ if (i > stream.pos) { stream.pos = i; return null; }
+ stream.pos = l;
+ return "trailingspace";
+ },
+ name: "trailingspace"
+ });
+ });
+});
diff --git a/devtools/client/sourceeditor/codemirror/addon/fold/brace-fold.js b/devtools/client/sourceeditor/codemirror/addon/fold/brace-fold.js
new file mode 100644
index 000000000..13c0f0cd8
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/addon/fold/brace-fold.js
@@ -0,0 +1,105 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../../lib/codemirror"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function(CodeMirror) {
+"use strict";
+
+CodeMirror.registerHelper("fold", "brace", function(cm, start) {
+ var line = start.line, lineText = cm.getLine(line);
+ var tokenType;
+
+ function findOpening(openCh) {
+ for (var at = start.ch, pass = 0;;) {
+ var found = at <= 0 ? -1 : lineText.lastIndexOf(openCh, at - 1);
+ if (found == -1) {
+ if (pass == 1) break;
+ pass = 1;
+ at = lineText.length;
+ continue;
+ }
+ if (pass == 1 && found < start.ch) break;
+ tokenType = cm.getTokenTypeAt(CodeMirror.Pos(line, found + 1));
+ if (!/^(comment|string)/.test(tokenType)) return found + 1;
+ at = found - 1;
+ }
+ }
+
+ var startToken = "{", endToken = "}", startCh = findOpening("{");
+ if (startCh == null) {
+ startToken = "[", endToken = "]";
+ startCh = findOpening("[");
+ }
+
+ if (startCh == null) return;
+ var count = 1, lastLine = cm.lastLine(), end, endCh;
+ outer: for (var i = line; i <= lastLine; ++i) {
+ var text = cm.getLine(i), pos = i == line ? startCh : 0;
+ for (;;) {
+ var nextOpen = text.indexOf(startToken, pos), nextClose = text.indexOf(endToken, pos);
+ if (nextOpen < 0) nextOpen = text.length;
+ if (nextClose < 0) nextClose = text.length;
+ pos = Math.min(nextOpen, nextClose);
+ if (pos == text.length) break;
+ if (cm.getTokenTypeAt(CodeMirror.Pos(i, pos + 1)) == tokenType) {
+ if (pos == nextOpen) ++count;
+ else if (!--count) { end = i; endCh = pos; break outer; }
+ }
+ ++pos;
+ }
+ }
+ if (end == null || line == end && endCh == startCh) return;
+ return {from: CodeMirror.Pos(line, startCh),
+ to: CodeMirror.Pos(end, endCh)};
+});
+
+CodeMirror.registerHelper("fold", "import", function(cm, start) {
+ function hasImport(line) {
+ if (line < cm.firstLine() || line > cm.lastLine()) return null;
+ var start = cm.getTokenAt(CodeMirror.Pos(line, 1));
+ if (!/\S/.test(start.string)) start = cm.getTokenAt(CodeMirror.Pos(line, start.end + 1));
+ if (start.type != "keyword" || start.string != "import") return null;
+ // Now find closing semicolon, return its position
+ for (var i = line, e = Math.min(cm.lastLine(), line + 10); i <= e; ++i) {
+ var text = cm.getLine(i), semi = text.indexOf(";");
+ if (semi != -1) return {startCh: start.end, end: CodeMirror.Pos(i, semi)};
+ }
+ }
+
+ var startLine = start.line, has = hasImport(startLine), prev;
+ if (!has || hasImport(startLine - 1) || ((prev = hasImport(startLine - 2)) && prev.end.line == startLine - 1))
+ return null;
+ for (var end = has.end;;) {
+ var next = hasImport(end.line + 1);
+ if (next == null) break;
+ end = next.end;
+ }
+ return {from: cm.clipPos(CodeMirror.Pos(startLine, has.startCh + 1)), to: end};
+});
+
+CodeMirror.registerHelper("fold", "include", function(cm, start) {
+ function hasInclude(line) {
+ if (line < cm.firstLine() || line > cm.lastLine()) return null;
+ var start = cm.getTokenAt(CodeMirror.Pos(line, 1));
+ if (!/\S/.test(start.string)) start = cm.getTokenAt(CodeMirror.Pos(line, start.end + 1));
+ if (start.type == "meta" && start.string.slice(0, 8) == "#include") return start.start + 8;
+ }
+
+ var startLine = start.line, has = hasInclude(startLine);
+ if (has == null || hasInclude(startLine - 1) != null) return null;
+ for (var end = startLine;;) {
+ var next = hasInclude(end + 1);
+ if (next == null) break;
+ ++end;
+ }
+ return {from: CodeMirror.Pos(startLine, has + 1),
+ to: cm.clipPos(CodeMirror.Pos(end))};
+});
+
+});
diff --git a/devtools/client/sourceeditor/codemirror/addon/fold/comment-fold.js b/devtools/client/sourceeditor/codemirror/addon/fold/comment-fold.js
new file mode 100644
index 000000000..e8d800eb5
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/addon/fold/comment-fold.js
@@ -0,0 +1,59 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../../lib/codemirror"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function(CodeMirror) {
+"use strict";
+
+CodeMirror.registerGlobalHelper("fold", "comment", function(mode) {
+ return mode.blockCommentStart && mode.blockCommentEnd;
+}, function(cm, start) {
+ var mode = cm.getModeAt(start), startToken = mode.blockCommentStart, endToken = mode.blockCommentEnd;
+ if (!startToken || !endToken) return;
+ var line = start.line, lineText = cm.getLine(line);
+
+ var startCh;
+ for (var at = start.ch, pass = 0;;) {
+ var found = at <= 0 ? -1 : lineText.lastIndexOf(startToken, at - 1);
+ if (found == -1) {
+ if (pass == 1) return;
+ pass = 1;
+ at = lineText.length;
+ continue;
+ }
+ if (pass == 1 && found < start.ch) return;
+ if (/comment/.test(cm.getTokenTypeAt(CodeMirror.Pos(line, found + 1))) &&
+ (found == 0 || lineText.slice(found - endToken.length, found) == endToken ||
+ !/comment/.test(cm.getTokenTypeAt(CodeMirror.Pos(line, found))))) {
+ startCh = found + startToken.length;
+ break;
+ }
+ at = found - 1;
+ }
+
+ var depth = 1, lastLine = cm.lastLine(), end, endCh;
+ outer: for (var i = line; i <= lastLine; ++i) {
+ var text = cm.getLine(i), pos = i == line ? startCh : 0;
+ for (;;) {
+ var nextOpen = text.indexOf(startToken, pos), nextClose = text.indexOf(endToken, pos);
+ if (nextOpen < 0) nextOpen = text.length;
+ if (nextClose < 0) nextClose = text.length;
+ pos = Math.min(nextOpen, nextClose);
+ if (pos == text.length) break;
+ if (pos == nextOpen) ++depth;
+ else if (!--depth) { end = i; endCh = pos; break outer; }
+ ++pos;
+ }
+ }
+ if (end == null || line == end && endCh == startCh) return;
+ return {from: CodeMirror.Pos(line, startCh),
+ to: CodeMirror.Pos(end, endCh)};
+});
+
+});
diff --git a/devtools/client/sourceeditor/codemirror/addon/fold/foldcode.js b/devtools/client/sourceeditor/codemirror/addon/fold/foldcode.js
new file mode 100644
index 000000000..78b36c464
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/addon/fold/foldcode.js
@@ -0,0 +1,150 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../../lib/codemirror"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function(CodeMirror) {
+ "use strict";
+
+ function doFold(cm, pos, options, force) {
+ if (options && options.call) {
+ var finder = options;
+ options = null;
+ } else {
+ var finder = getOption(cm, options, "rangeFinder");
+ }
+ if (typeof pos == "number") pos = CodeMirror.Pos(pos, 0);
+ var minSize = getOption(cm, options, "minFoldSize");
+
+ function getRange(allowFolded) {
+ var range = finder(cm, pos);
+ if (!range || range.to.line - range.from.line < minSize) return null;
+ var marks = cm.findMarksAt(range.from);
+ for (var i = 0; i < marks.length; ++i) {
+ if (marks[i].__isFold && force !== "fold") {
+ if (!allowFolded) return null;
+ range.cleared = true;
+ marks[i].clear();
+ }
+ }
+ return range;
+ }
+
+ var range = getRange(true);
+ if (getOption(cm, options, "scanUp")) while (!range && pos.line > cm.firstLine()) {
+ pos = CodeMirror.Pos(pos.line - 1, 0);
+ range = getRange(false);
+ }
+ if (!range || range.cleared || force === "unfold") return;
+
+ var myWidget = makeWidget(cm, options);
+ CodeMirror.on(myWidget, "mousedown", function(e) {
+ myRange.clear();
+ CodeMirror.e_preventDefault(e);
+ });
+ var myRange = cm.markText(range.from, range.to, {
+ replacedWith: myWidget,
+ clearOnEnter: getOption(cm, options, "clearOnEnter"),
+ __isFold: true
+ });
+ myRange.on("clear", function(from, to) {
+ CodeMirror.signal(cm, "unfold", cm, from, to);
+ });
+ CodeMirror.signal(cm, "fold", cm, range.from, range.to);
+ }
+
+ function makeWidget(cm, options) {
+ var widget = getOption(cm, options, "widget");
+ if (typeof widget == "string") {
+ var text = document.createTextNode(widget);
+ widget = document.createElement("span");
+ widget.appendChild(text);
+ widget.className = "CodeMirror-foldmarker";
+ }
+ return widget;
+ }
+
+ // Clumsy backwards-compatible interface
+ CodeMirror.newFoldFunction = function(rangeFinder, widget) {
+ return function(cm, pos) { doFold(cm, pos, {rangeFinder: rangeFinder, widget: widget}); };
+ };
+
+ // New-style interface
+ CodeMirror.defineExtension("foldCode", function(pos, options, force) {
+ doFold(this, pos, options, force);
+ });
+
+ CodeMirror.defineExtension("isFolded", function(pos) {
+ var marks = this.findMarksAt(pos);
+ for (var i = 0; i < marks.length; ++i)
+ if (marks[i].__isFold) return true;
+ });
+
+ CodeMirror.commands.toggleFold = function(cm) {
+ cm.foldCode(cm.getCursor());
+ };
+ CodeMirror.commands.fold = function(cm) {
+ cm.foldCode(cm.getCursor(), null, "fold");
+ };
+ CodeMirror.commands.unfold = function(cm) {
+ cm.foldCode(cm.getCursor(), null, "unfold");
+ };
+ CodeMirror.commands.foldAll = function(cm) {
+ cm.operation(function() {
+ for (var i = cm.firstLine(), e = cm.lastLine(); i <= e; i++)
+ cm.foldCode(CodeMirror.Pos(i, 0), null, "fold");
+ });
+ };
+ CodeMirror.commands.unfoldAll = function(cm) {
+ cm.operation(function() {
+ for (var i = cm.firstLine(), e = cm.lastLine(); i <= e; i++)
+ cm.foldCode(CodeMirror.Pos(i, 0), null, "unfold");
+ });
+ };
+
+ CodeMirror.registerHelper("fold", "combine", function() {
+ var funcs = Array.prototype.slice.call(arguments, 0);
+ return function(cm, start) {
+ for (var i = 0; i < funcs.length; ++i) {
+ var found = funcs[i](cm, start);
+ if (found) return found;
+ }
+ };
+ });
+
+ CodeMirror.registerHelper("fold", "auto", function(cm, start) {
+ var helpers = cm.getHelpers(start, "fold");
+ for (var i = 0; i < helpers.length; i++) {
+ var cur = helpers[i](cm, start);
+ if (cur) return cur;
+ }
+ });
+
+ var defaultOptions = {
+ rangeFinder: CodeMirror.fold.auto,
+ widget: "\u2194",
+ minFoldSize: 0,
+ scanUp: false,
+ clearOnEnter: true
+ };
+
+ CodeMirror.defineOption("foldOptions", null);
+
+ function getOption(cm, options, name) {
+ if (options && options[name] !== undefined)
+ return options[name];
+ var editorOptions = cm.options.foldOptions;
+ if (editorOptions && editorOptions[name] !== undefined)
+ return editorOptions[name];
+ return defaultOptions[name];
+ }
+
+ CodeMirror.defineExtension("foldOption", function(options, name) {
+ return getOption(this, options, name);
+ });
+});
diff --git a/devtools/client/sourceeditor/codemirror/addon/fold/foldgutter.css b/devtools/client/sourceeditor/codemirror/addon/fold/foldgutter.css
new file mode 100644
index 000000000..ad19ae2d3
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/addon/fold/foldgutter.css
@@ -0,0 +1,20 @@
+.CodeMirror-foldmarker {
+ color: blue;
+ text-shadow: #b9f 1px 1px 2px, #b9f -1px -1px 2px, #b9f 1px -1px 2px, #b9f -1px 1px 2px;
+ font-family: arial;
+ line-height: .3;
+ cursor: pointer;
+}
+.CodeMirror-foldgutter {
+ width: .7em;
+}
+.CodeMirror-foldgutter-open,
+.CodeMirror-foldgutter-folded {
+ cursor: pointer;
+}
+.CodeMirror-foldgutter-open:after {
+ content: "\25BE";
+}
+.CodeMirror-foldgutter-folded:after {
+ content: "\25B8";
+}
diff --git a/devtools/client/sourceeditor/codemirror/addon/fold/foldgutter.js b/devtools/client/sourceeditor/codemirror/addon/fold/foldgutter.js
new file mode 100644
index 000000000..9d3232656
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/addon/fold/foldgutter.js
@@ -0,0 +1,146 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../../lib/codemirror"), require("./foldcode"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror", "./foldcode"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function(CodeMirror) {
+ "use strict";
+
+ CodeMirror.defineOption("foldGutter", false, function(cm, val, old) {
+ if (old && old != CodeMirror.Init) {
+ cm.clearGutter(cm.state.foldGutter.options.gutter);
+ cm.state.foldGutter = null;
+ cm.off("gutterClick", onGutterClick);
+ cm.off("change", onChange);
+ cm.off("viewportChange", onViewportChange);
+ cm.off("fold", onFold);
+ cm.off("unfold", onFold);
+ cm.off("swapDoc", onChange);
+ }
+ if (val) {
+ cm.state.foldGutter = new State(parseOptions(val));
+ updateInViewport(cm);
+ cm.on("gutterClick", onGutterClick);
+ cm.on("change", onChange);
+ cm.on("viewportChange", onViewportChange);
+ cm.on("fold", onFold);
+ cm.on("unfold", onFold);
+ cm.on("swapDoc", onChange);
+ }
+ });
+
+ var Pos = CodeMirror.Pos;
+
+ function State(options) {
+ this.options = options;
+ this.from = this.to = 0;
+ }
+
+ function parseOptions(opts) {
+ if (opts === true) opts = {};
+ if (opts.gutter == null) opts.gutter = "CodeMirror-foldgutter";
+ if (opts.indicatorOpen == null) opts.indicatorOpen = "CodeMirror-foldgutter-open";
+ if (opts.indicatorFolded == null) opts.indicatorFolded = "CodeMirror-foldgutter-folded";
+ return opts;
+ }
+
+ function isFolded(cm, line) {
+ var marks = cm.findMarks(Pos(line, 0), Pos(line + 1, 0));
+ for (var i = 0; i < marks.length; ++i)
+ if (marks[i].__isFold && marks[i].find().from.line == line) return marks[i];
+ }
+
+ function marker(spec) {
+ if (typeof spec == "string") {
+ var elt = document.createElement("div");
+ elt.className = spec + " CodeMirror-guttermarker-subtle";
+ return elt;
+ } else {
+ return spec.cloneNode(true);
+ }
+ }
+
+ function updateFoldInfo(cm, from, to) {
+ var opts = cm.state.foldGutter.options, cur = from;
+ var minSize = cm.foldOption(opts, "minFoldSize");
+ var func = cm.foldOption(opts, "rangeFinder");
+ cm.eachLine(from, to, function(line) {
+ var mark = null;
+ if (isFolded(cm, cur)) {
+ mark = marker(opts.indicatorFolded);
+ } else {
+ var pos = Pos(cur, 0);
+ var range = func && func(cm, pos);
+ if (range && range.to.line - range.from.line >= minSize)
+ mark = marker(opts.indicatorOpen);
+ }
+ cm.setGutterMarker(line, opts.gutter, mark);
+ ++cur;
+ });
+ }
+
+ function updateInViewport(cm) {
+ var vp = cm.getViewport(), state = cm.state.foldGutter;
+ if (!state) return;
+ cm.operation(function() {
+ updateFoldInfo(cm, vp.from, vp.to);
+ });
+ state.from = vp.from; state.to = vp.to;
+ }
+
+ function onGutterClick(cm, line, gutter) {
+ var state = cm.state.foldGutter;
+ if (!state) return;
+ var opts = state.options;
+ if (gutter != opts.gutter) return;
+ var folded = isFolded(cm, line);
+ if (folded) folded.clear();
+ else cm.foldCode(Pos(line, 0), opts.rangeFinder);
+ }
+
+ function onChange(cm) {
+ var state = cm.state.foldGutter;
+ if (!state) return;
+ var opts = state.options;
+ state.from = state.to = 0;
+ clearTimeout(state.changeUpdate);
+ state.changeUpdate = setTimeout(function() { updateInViewport(cm); }, opts.foldOnChangeTimeSpan || 600);
+ }
+
+ function onViewportChange(cm) {
+ var state = cm.state.foldGutter;
+ if (!state) return;
+ var opts = state.options;
+ clearTimeout(state.changeUpdate);
+ state.changeUpdate = setTimeout(function() {
+ var vp = cm.getViewport();
+ if (state.from == state.to || vp.from - state.to > 20 || state.from - vp.to > 20) {
+ updateInViewport(cm);
+ } else {
+ cm.operation(function() {
+ if (vp.from < state.from) {
+ updateFoldInfo(cm, vp.from, state.from);
+ state.from = vp.from;
+ }
+ if (vp.to > state.to) {
+ updateFoldInfo(cm, state.to, vp.to);
+ state.to = vp.to;
+ }
+ });
+ }
+ }, opts.updateViewportTimeSpan || 400);
+ }
+
+ function onFold(cm, from) {
+ var state = cm.state.foldGutter;
+ if (!state) return;
+ var line = from.line;
+ if (line >= state.from && line < state.to)
+ updateFoldInfo(cm, line, line + 1);
+ }
+});
diff --git a/devtools/client/sourceeditor/codemirror/addon/fold/indent-fold.js b/devtools/client/sourceeditor/codemirror/addon/fold/indent-fold.js
new file mode 100644
index 000000000..e29f15e9d
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/addon/fold/indent-fold.js
@@ -0,0 +1,44 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../../lib/codemirror"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function(CodeMirror) {
+"use strict";
+
+CodeMirror.registerHelper("fold", "indent", function(cm, start) {
+ var tabSize = cm.getOption("tabSize"), firstLine = cm.getLine(start.line);
+ if (!/\S/.test(firstLine)) return;
+ var getIndent = function(line) {
+ return CodeMirror.countColumn(line, null, tabSize);
+ };
+ var myIndent = getIndent(firstLine);
+ var lastLineInFold = null;
+ // Go through lines until we find a line that definitely doesn't belong in
+ // the block we're folding, or to the end.
+ for (var i = start.line + 1, end = cm.lastLine(); i <= end; ++i) {
+ var curLine = cm.getLine(i);
+ var curIndent = getIndent(curLine);
+ if (curIndent > myIndent) {
+ // Lines with a greater indent are considered part of the block.
+ lastLineInFold = i;
+ } else if (!/\S/.test(curLine)) {
+ // Empty lines might be breaks within the block we're trying to fold.
+ } else {
+ // A non-empty line at an indent equal to or less than ours marks the
+ // start of another block.
+ break;
+ }
+ }
+ if (lastLineInFold) return {
+ from: CodeMirror.Pos(start.line, firstLine.length),
+ to: CodeMirror.Pos(lastLineInFold, cm.getLine(lastLineInFold).length)
+ };
+});
+
+});
diff --git a/devtools/client/sourceeditor/codemirror/addon/fold/markdown-fold.js b/devtools/client/sourceeditor/codemirror/addon/fold/markdown-fold.js
new file mode 100644
index 000000000..ce84c946c
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/addon/fold/markdown-fold.js
@@ -0,0 +1,49 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../../lib/codemirror"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function(CodeMirror) {
+"use strict";
+
+CodeMirror.registerHelper("fold", "markdown", function(cm, start) {
+ var maxDepth = 100;
+
+ function isHeader(lineNo) {
+ var tokentype = cm.getTokenTypeAt(CodeMirror.Pos(lineNo, 0));
+ return tokentype && /\bheader\b/.test(tokentype);
+ }
+
+ function headerLevel(lineNo, line, nextLine) {
+ var match = line && line.match(/^#+/);
+ if (match && isHeader(lineNo)) return match[0].length;
+ match = nextLine && nextLine.match(/^[=\-]+\s*$/);
+ if (match && isHeader(lineNo + 1)) return nextLine[0] == "=" ? 1 : 2;
+ return maxDepth;
+ }
+
+ var firstLine = cm.getLine(start.line), nextLine = cm.getLine(start.line + 1);
+ var level = headerLevel(start.line, firstLine, nextLine);
+ if (level === maxDepth) return undefined;
+
+ var lastLineNo = cm.lastLine();
+ var end = start.line, nextNextLine = cm.getLine(end + 2);
+ while (end < lastLineNo) {
+ if (headerLevel(end + 1, nextLine, nextNextLine) <= level) break;
+ ++end;
+ nextLine = nextNextLine;
+ nextNextLine = cm.getLine(end + 2);
+ }
+
+ return {
+ from: CodeMirror.Pos(start.line, firstLine.length),
+ to: CodeMirror.Pos(end, cm.getLine(end).length)
+ };
+});
+
+});
diff --git a/devtools/client/sourceeditor/codemirror/addon/fold/xml-fold.js b/devtools/client/sourceeditor/codemirror/addon/fold/xml-fold.js
new file mode 100644
index 000000000..f8c67b897
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/addon/fold/xml-fold.js
@@ -0,0 +1,182 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../../lib/codemirror"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function(CodeMirror) {
+ "use strict";
+
+ var Pos = CodeMirror.Pos;
+ function cmp(a, b) { return a.line - b.line || a.ch - b.ch; }
+
+ var nameStartChar = "A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD";
+ var nameChar = nameStartChar + "\-\:\.0-9\\u00B7\\u0300-\\u036F\\u203F-\\u2040";
+ var xmlTagStart = new RegExp("<(/?)([" + nameStartChar + "][" + nameChar + "]*)", "g");
+
+ function Iter(cm, line, ch, range) {
+ this.line = line; this.ch = ch;
+ this.cm = cm; this.text = cm.getLine(line);
+ this.min = range ? range.from : cm.firstLine();
+ this.max = range ? range.to - 1 : cm.lastLine();
+ }
+
+ function tagAt(iter, ch) {
+ var type = iter.cm.getTokenTypeAt(Pos(iter.line, ch));
+ return type && /\btag\b/.test(type);
+ }
+
+ function nextLine(iter) {
+ if (iter.line >= iter.max) return;
+ iter.ch = 0;
+ iter.text = iter.cm.getLine(++iter.line);
+ return true;
+ }
+ function prevLine(iter) {
+ if (iter.line <= iter.min) return;
+ iter.text = iter.cm.getLine(--iter.line);
+ iter.ch = iter.text.length;
+ return true;
+ }
+
+ function toTagEnd(iter) {
+ for (;;) {
+ var gt = iter.text.indexOf(">", iter.ch);
+ if (gt == -1) { if (nextLine(iter)) continue; else return; }
+ if (!tagAt(iter, gt + 1)) { iter.ch = gt + 1; continue; }
+ var lastSlash = iter.text.lastIndexOf("/", gt);
+ var selfClose = lastSlash > -1 && !/\S/.test(iter.text.slice(lastSlash + 1, gt));
+ iter.ch = gt + 1;
+ return selfClose ? "selfClose" : "regular";
+ }
+ }
+ function toTagStart(iter) {
+ for (;;) {
+ var lt = iter.ch ? iter.text.lastIndexOf("<", iter.ch - 1) : -1;
+ if (lt == -1) { if (prevLine(iter)) continue; else return; }
+ if (!tagAt(iter, lt + 1)) { iter.ch = lt; continue; }
+ xmlTagStart.lastIndex = lt;
+ iter.ch = lt;
+ var match = xmlTagStart.exec(iter.text);
+ if (match && match.index == lt) return match;
+ }
+ }
+
+ function toNextTag(iter) {
+ for (;;) {
+ xmlTagStart.lastIndex = iter.ch;
+ var found = xmlTagStart.exec(iter.text);
+ if (!found) { if (nextLine(iter)) continue; else return; }
+ if (!tagAt(iter, found.index + 1)) { iter.ch = found.index + 1; continue; }
+ iter.ch = found.index + found[0].length;
+ return found;
+ }
+ }
+ function toPrevTag(iter) {
+ for (;;) {
+ var gt = iter.ch ? iter.text.lastIndexOf(">", iter.ch - 1) : -1;
+ if (gt == -1) { if (prevLine(iter)) continue; else return; }
+ if (!tagAt(iter, gt + 1)) { iter.ch = gt; continue; }
+ var lastSlash = iter.text.lastIndexOf("/", gt);
+ var selfClose = lastSlash > -1 && !/\S/.test(iter.text.slice(lastSlash + 1, gt));
+ iter.ch = gt + 1;
+ return selfClose ? "selfClose" : "regular";
+ }
+ }
+
+ function findMatchingClose(iter, tag) {
+ var stack = [];
+ for (;;) {
+ var next = toNextTag(iter), end, startLine = iter.line, startCh = iter.ch - (next ? next[0].length : 0);
+ if (!next || !(end = toTagEnd(iter))) return;
+ if (end == "selfClose") continue;
+ if (next[1]) { // closing tag
+ for (var i = stack.length - 1; i >= 0; --i) if (stack[i] == next[2]) {
+ stack.length = i;
+ break;
+ }
+ if (i < 0 && (!tag || tag == next[2])) return {
+ tag: next[2],
+ from: Pos(startLine, startCh),
+ to: Pos(iter.line, iter.ch)
+ };
+ } else { // opening tag
+ stack.push(next[2]);
+ }
+ }
+ }
+ function findMatchingOpen(iter, tag) {
+ var stack = [];
+ for (;;) {
+ var prev = toPrevTag(iter);
+ if (!prev) return;
+ if (prev == "selfClose") { toTagStart(iter); continue; }
+ var endLine = iter.line, endCh = iter.ch;
+ var start = toTagStart(iter);
+ if (!start) return;
+ if (start[1]) { // closing tag
+ stack.push(start[2]);
+ } else { // opening tag
+ for (var i = stack.length - 1; i >= 0; --i) if (stack[i] == start[2]) {
+ stack.length = i;
+ break;
+ }
+ if (i < 0 && (!tag || tag == start[2])) return {
+ tag: start[2],
+ from: Pos(iter.line, iter.ch),
+ to: Pos(endLine, endCh)
+ };
+ }
+ }
+ }
+
+ CodeMirror.registerHelper("fold", "xml", function(cm, start) {
+ var iter = new Iter(cm, start.line, 0);
+ for (;;) {
+ var openTag = toNextTag(iter), end;
+ if (!openTag || iter.line != start.line || !(end = toTagEnd(iter))) return;
+ if (!openTag[1] && end != "selfClose") {
+ var startPos = Pos(iter.line, iter.ch);
+ var endPos = findMatchingClose(iter, openTag[2]);
+ return endPos && {from: startPos, to: endPos.from};
+ }
+ }
+ });
+ CodeMirror.findMatchingTag = function(cm, pos, range) {
+ var iter = new Iter(cm, pos.line, pos.ch, range);
+ if (iter.text.indexOf(">") == -1 && iter.text.indexOf("<") == -1) return;
+ var end = toTagEnd(iter), to = end && Pos(iter.line, iter.ch);
+ var start = end && toTagStart(iter);
+ if (!end || !start || cmp(iter, pos) > 0) return;
+ var here = {from: Pos(iter.line, iter.ch), to: to, tag: start[2]};
+ if (end == "selfClose") return {open: here, close: null, at: "open"};
+
+ if (start[1]) { // closing tag
+ return {open: findMatchingOpen(iter, start[2]), close: here, at: "close"};
+ } else { // opening tag
+ iter = new Iter(cm, to.line, to.ch, range);
+ return {open: here, close: findMatchingClose(iter, start[2]), at: "open"};
+ }
+ };
+
+ CodeMirror.findEnclosingTag = function(cm, pos, range) {
+ var iter = new Iter(cm, pos.line, pos.ch, range);
+ for (;;) {
+ var open = findMatchingOpen(iter);
+ if (!open) break;
+ var forward = new Iter(cm, pos.line, pos.ch, range);
+ var close = findMatchingClose(forward, open.tag);
+ if (close) return {open: open, close: close};
+ }
+ };
+
+ // Used by addon/edit/closetag.js
+ CodeMirror.scanForClosingTag = function(cm, pos, name, end) {
+ var iter = new Iter(cm, pos.line, pos.ch, end ? {from: 0, to: end} : null);
+ return findMatchingClose(iter, name);
+ };
+});
diff --git a/devtools/client/sourceeditor/codemirror/addon/hint/show-hint.js b/devtools/client/sourceeditor/codemirror/addon/hint/show-hint.js
new file mode 100644
index 000000000..64ec9289c
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/addon/hint/show-hint.js
@@ -0,0 +1,437 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../../lib/codemirror"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function(CodeMirror) {
+ "use strict";
+
+ var HINT_ELEMENT_CLASS = "CodeMirror-hint";
+ var ACTIVE_HINT_ELEMENT_CLASS = "CodeMirror-hint-active";
+
+ // This is the old interface, kept around for now to stay
+ // backwards-compatible.
+ CodeMirror.showHint = function(cm, getHints, options) {
+ if (!getHints) return cm.showHint(options);
+ if (options && options.async) getHints.async = true;
+ var newOpts = {hint: getHints};
+ if (options) for (var prop in options) newOpts[prop] = options[prop];
+ return cm.showHint(newOpts);
+ };
+
+ CodeMirror.defineExtension("showHint", function(options) {
+ options = parseOptions(this, this.getCursor("start"), options);
+ var selections = this.listSelections()
+ if (selections.length > 1) return;
+ // By default, don't allow completion when something is selected.
+ // A hint function can have a `supportsSelection` property to
+ // indicate that it can handle selections.
+ if (this.somethingSelected()) {
+ if (!options.hint.supportsSelection) return;
+ // Don't try with cross-line selections
+ for (var i = 0; i < selections.length; i++)
+ if (selections[i].head.line != selections[i].anchor.line) return;
+ }
+
+ if (this.state.completionActive) this.state.completionActive.close();
+ var completion = this.state.completionActive = new Completion(this, options);
+ if (!completion.options.hint) return;
+
+ CodeMirror.signal(this, "startCompletion", this);
+ completion.update(true);
+ });
+
+ function Completion(cm, options) {
+ this.cm = cm;
+ this.options = options;
+ this.widget = null;
+ this.debounce = 0;
+ this.tick = 0;
+ this.startPos = this.cm.getCursor("start");
+ this.startLen = this.cm.getLine(this.startPos.line).length - this.cm.getSelection().length;
+
+ var self = this;
+ cm.on("cursorActivity", this.activityFunc = function() { self.cursorActivity(); });
+ }
+
+ var requestAnimationFrame = window.requestAnimationFrame || function(fn) {
+ return setTimeout(fn, 1000/60);
+ };
+ var cancelAnimationFrame = window.cancelAnimationFrame || clearTimeout;
+
+ Completion.prototype = {
+ close: function() {
+ if (!this.active()) return;
+ this.cm.state.completionActive = null;
+ this.tick = null;
+ this.cm.off("cursorActivity", this.activityFunc);
+
+ if (this.widget && this.data) CodeMirror.signal(this.data, "close");
+ if (this.widget) this.widget.close();
+ CodeMirror.signal(this.cm, "endCompletion", this.cm);
+ },
+
+ active: function() {
+ return this.cm.state.completionActive == this;
+ },
+
+ pick: function(data, i) {
+ var completion = data.list[i];
+ if (completion.hint) completion.hint(this.cm, data, completion);
+ else this.cm.replaceRange(getText(completion), completion.from || data.from,
+ completion.to || data.to, "complete");
+ CodeMirror.signal(data, "pick", completion);
+ this.close();
+ },
+
+ cursorActivity: function() {
+ if (this.debounce) {
+ cancelAnimationFrame(this.debounce);
+ this.debounce = 0;
+ }
+
+ var pos = this.cm.getCursor(), line = this.cm.getLine(pos.line);
+ if (pos.line != this.startPos.line || line.length - pos.ch != this.startLen - this.startPos.ch ||
+ pos.ch < this.startPos.ch || this.cm.somethingSelected() ||
+ (pos.ch && this.options.closeCharacters.test(line.charAt(pos.ch - 1)))) {
+ this.close();
+ } else {
+ var self = this;
+ this.debounce = requestAnimationFrame(function() {self.update();});
+ if (this.widget) this.widget.disable();
+ }
+ },
+
+ update: function(first) {
+ if (this.tick == null) return
+ var self = this, myTick = ++this.tick
+ fetchHints(this.options.hint, this.cm, this.options, function(data) {
+ if (self.tick == myTick) self.finishUpdate(data, first)
+ })
+ },
+
+ finishUpdate: function(data, first) {
+ if (this.data) CodeMirror.signal(this.data, "update");
+
+ var picked = (this.widget && this.widget.picked) || (first && this.options.completeSingle);
+ if (this.widget) this.widget.close();
+
+ if (data && this.data && isNewCompletion(this.data, data)) return;
+ this.data = data;
+
+ if (data && data.list.length) {
+ if (picked && data.list.length == 1) {
+ this.pick(data, 0);
+ } else {
+ this.widget = new Widget(this, data);
+ CodeMirror.signal(data, "shown");
+ }
+ }
+ }
+ };
+
+ function isNewCompletion(old, nw) {
+ var moved = CodeMirror.cmpPos(nw.from, old.from)
+ return moved > 0 && old.to.ch - old.from.ch != nw.to.ch - nw.from.ch
+ }
+
+ function parseOptions(cm, pos, options) {
+ var editor = cm.options.hintOptions;
+ var out = {};
+ for (var prop in defaultOptions) out[prop] = defaultOptions[prop];
+ if (editor) for (var prop in editor)
+ if (editor[prop] !== undefined) out[prop] = editor[prop];
+ if (options) for (var prop in options)
+ if (options[prop] !== undefined) out[prop] = options[prop];
+ if (out.hint.resolve) out.hint = out.hint.resolve(cm, pos)
+ return out;
+ }
+
+ function getText(completion) {
+ if (typeof completion == "string") return completion;
+ else return completion.text;
+ }
+
+ function buildKeyMap(completion, handle) {
+ var baseMap = {
+ Up: function() {handle.moveFocus(-1);},
+ Down: function() {handle.moveFocus(1);},
+ PageUp: function() {handle.moveFocus(-handle.menuSize() + 1, true);},
+ PageDown: function() {handle.moveFocus(handle.menuSize() - 1, true);},
+ Home: function() {handle.setFocus(0);},
+ End: function() {handle.setFocus(handle.length - 1);},
+ Enter: handle.pick,
+ Tab: handle.pick,
+ Esc: handle.close
+ };
+ var custom = completion.options.customKeys;
+ var ourMap = custom ? {} : baseMap;
+ function addBinding(key, val) {
+ var bound;
+ if (typeof val != "string")
+ bound = function(cm) { return val(cm, handle); };
+ // This mechanism is deprecated
+ else if (baseMap.hasOwnProperty(val))
+ bound = baseMap[val];
+ else
+ bound = val;
+ ourMap[key] = bound;
+ }
+ if (custom)
+ for (var key in custom) if (custom.hasOwnProperty(key))
+ addBinding(key, custom[key]);
+ var extra = completion.options.extraKeys;
+ if (extra)
+ for (var key in extra) if (extra.hasOwnProperty(key))
+ addBinding(key, extra[key]);
+ return ourMap;
+ }
+
+ function getHintElement(hintsElement, el) {
+ while (el && el != hintsElement) {
+ if (el.nodeName.toUpperCase() === "LI" && el.parentNode == hintsElement) return el;
+ el = el.parentNode;
+ }
+ }
+
+ function Widget(completion, data) {
+ this.completion = completion;
+ this.data = data;
+ this.picked = false;
+ var widget = this, cm = completion.cm;
+
+ var hints = this.hints = document.createElement("ul");
+ hints.className = "CodeMirror-hints";
+ this.selectedHint = data.selectedHint || 0;
+
+ var completions = data.list;
+ for (var i = 0; i < completions.length; ++i) {
+ var elt = hints.appendChild(document.createElement("li")), cur = completions[i];
+ var className = HINT_ELEMENT_CLASS + (i != this.selectedHint ? "" : " " + ACTIVE_HINT_ELEMENT_CLASS);
+ if (cur.className != null) className = cur.className + " " + className;
+ elt.className = className;
+ if (cur.render) cur.render(elt, data, cur);
+ else elt.appendChild(document.createTextNode(cur.displayText || getText(cur)));
+ elt.hintId = i;
+ }
+
+ var pos = cm.cursorCoords(completion.options.alignWithWord ? data.from : null);
+ var left = pos.left, top = pos.bottom, below = true;
+ hints.style.left = left + "px";
+ hints.style.top = top + "px";
+ // If we're at the edge of the screen, then we want the menu to appear on the left of the cursor.
+ var winW = window.innerWidth || Math.max(document.body.offsetWidth, document.documentElement.offsetWidth);
+ var winH = window.innerHeight || Math.max(document.body.offsetHeight, document.documentElement.offsetHeight);
+ (completion.options.container || document.body).appendChild(hints);
+ var box = hints.getBoundingClientRect(), overlapY = box.bottom - winH;
+ var scrolls = hints.scrollHeight > hints.clientHeight + 1
+ if (overlapY > 0) {
+ var height = box.bottom - box.top, curTop = pos.top - (pos.bottom - box.top);
+ if (curTop - height > 0) { // Fits above cursor
+ hints.style.top = (top = pos.top - height) + "px";
+ below = false;
+ } else if (height > winH) {
+ hints.style.height = (winH - 5) + "px";
+ hints.style.top = (top = pos.bottom - box.top) + "px";
+ var cursor = cm.getCursor();
+ if (data.from.ch != cursor.ch) {
+ pos = cm.cursorCoords(cursor);
+ hints.style.left = (left = pos.left) + "px";
+ box = hints.getBoundingClientRect();
+ }
+ }
+ }
+ var overlapX = box.right - winW;
+ if (overlapX > 0) {
+ if (box.right - box.left > winW) {
+ hints.style.width = (winW - 5) + "px";
+ overlapX -= (box.right - box.left) - winW;
+ }
+ hints.style.left = (left = pos.left - overlapX) + "px";
+ }
+ if (scrolls) for (var node = hints.firstChild; node; node = node.nextSibling)
+ node.style.paddingRight = cm.display.nativeBarWidth + "px"
+
+ cm.addKeyMap(this.keyMap = buildKeyMap(completion, {
+ moveFocus: function(n, avoidWrap) { widget.changeActive(widget.selectedHint + n, avoidWrap); },
+ setFocus: function(n) { widget.changeActive(n); },
+ menuSize: function() { return widget.screenAmount(); },
+ length: completions.length,
+ close: function() { completion.close(); },
+ pick: function() { widget.pick(); },
+ data: data
+ }));
+
+ if (completion.options.closeOnUnfocus) {
+ var closingOnBlur;
+ cm.on("blur", this.onBlur = function() { closingOnBlur = setTimeout(function() { completion.close(); }, 100); });
+ cm.on("focus", this.onFocus = function() { clearTimeout(closingOnBlur); });
+ }
+
+ var startScroll = cm.getScrollInfo();
+ cm.on("scroll", this.onScroll = function() {
+ var curScroll = cm.getScrollInfo(), editor = cm.getWrapperElement().getBoundingClientRect();
+ var newTop = top + startScroll.top - curScroll.top;
+ var point = newTop - (window.pageYOffset || (document.documentElement || document.body).scrollTop);
+ if (!below) point += hints.offsetHeight;
+ if (point <= editor.top || point >= editor.bottom) return completion.close();
+ hints.style.top = newTop + "px";
+ hints.style.left = (left + startScroll.left - curScroll.left) + "px";
+ });
+
+ CodeMirror.on(hints, "dblclick", function(e) {
+ var t = getHintElement(hints, e.target || e.srcElement);
+ if (t && t.hintId != null) {widget.changeActive(t.hintId); widget.pick();}
+ });
+
+ CodeMirror.on(hints, "click", function(e) {
+ var t = getHintElement(hints, e.target || e.srcElement);
+ if (t && t.hintId != null) {
+ widget.changeActive(t.hintId);
+ if (completion.options.completeOnSingleClick) widget.pick();
+ }
+ });
+
+ CodeMirror.on(hints, "mousedown", function() {
+ setTimeout(function(){cm.focus();}, 20);
+ });
+
+ CodeMirror.signal(data, "select", completions[0], hints.firstChild);
+ return true;
+ }
+
+ Widget.prototype = {
+ close: function() {
+ if (this.completion.widget != this) return;
+ this.completion.widget = null;
+ this.hints.parentNode.removeChild(this.hints);
+ this.completion.cm.removeKeyMap(this.keyMap);
+
+ var cm = this.completion.cm;
+ if (this.completion.options.closeOnUnfocus) {
+ cm.off("blur", this.onBlur);
+ cm.off("focus", this.onFocus);
+ }
+ cm.off("scroll", this.onScroll);
+ },
+
+ disable: function() {
+ this.completion.cm.removeKeyMap(this.keyMap);
+ var widget = this;
+ this.keyMap = {Enter: function() { widget.picked = true; }};
+ this.completion.cm.addKeyMap(this.keyMap);
+ },
+
+ pick: function() {
+ this.completion.pick(this.data, this.selectedHint);
+ },
+
+ changeActive: function(i, avoidWrap) {
+ if (i >= this.data.list.length)
+ i = avoidWrap ? this.data.list.length - 1 : 0;
+ else if (i < 0)
+ i = avoidWrap ? 0 : this.data.list.length - 1;
+ if (this.selectedHint == i) return;
+ var node = this.hints.childNodes[this.selectedHint];
+ node.className = node.className.replace(" " + ACTIVE_HINT_ELEMENT_CLASS, "");
+ node = this.hints.childNodes[this.selectedHint = i];
+ node.className += " " + ACTIVE_HINT_ELEMENT_CLASS;
+ if (node.offsetTop < this.hints.scrollTop)
+ this.hints.scrollTop = node.offsetTop - 3;
+ else if (node.offsetTop + node.offsetHeight > this.hints.scrollTop + this.hints.clientHeight)
+ this.hints.scrollTop = node.offsetTop + node.offsetHeight - this.hints.clientHeight + 3;
+ CodeMirror.signal(this.data, "select", this.data.list[this.selectedHint], node);
+ },
+
+ screenAmount: function() {
+ return Math.floor(this.hints.clientHeight / this.hints.firstChild.offsetHeight) || 1;
+ }
+ };
+
+ function applicableHelpers(cm, helpers) {
+ if (!cm.somethingSelected()) return helpers
+ var result = []
+ for (var i = 0; i < helpers.length; i++)
+ if (helpers[i].supportsSelection) result.push(helpers[i])
+ return result
+ }
+
+ function fetchHints(hint, cm, options, callback) {
+ if (hint.async) {
+ hint(cm, callback, options)
+ } else {
+ var result = hint(cm, options)
+ if (result && result.then) result.then(callback)
+ else callback(result)
+ }
+ }
+
+ function resolveAutoHints(cm, pos) {
+ var helpers = cm.getHelpers(pos, "hint"), words
+ if (helpers.length) {
+ var resolved = function(cm, callback, options) {
+ var app = applicableHelpers(cm, helpers);
+ function run(i) {
+ if (i == app.length) return callback(null)
+ fetchHints(app[i], cm, options, function(result) {
+ if (result && result.list.length > 0) callback(result)
+ else run(i + 1)
+ })
+ }
+ run(0)
+ }
+ resolved.async = true
+ resolved.supportsSelection = true
+ return resolved
+ } else if (words = cm.getHelper(cm.getCursor(), "hintWords")) {
+ return function(cm) { return CodeMirror.hint.fromList(cm, {words: words}) }
+ } else if (CodeMirror.hint.anyword) {
+ return function(cm, options) { return CodeMirror.hint.anyword(cm, options) }
+ } else {
+ return function() {}
+ }
+ }
+
+ CodeMirror.registerHelper("hint", "auto", {
+ resolve: resolveAutoHints
+ });
+
+ CodeMirror.registerHelper("hint", "fromList", function(cm, options) {
+ var cur = cm.getCursor(), token = cm.getTokenAt(cur);
+ var to = CodeMirror.Pos(cur.line, token.end);
+ if (token.string && /\w/.test(token.string[token.string.length - 1])) {
+ var term = token.string, from = CodeMirror.Pos(cur.line, token.start);
+ } else {
+ var term = "", from = to;
+ }
+ var found = [];
+ for (var i = 0; i < options.words.length; i++) {
+ var word = options.words[i];
+ if (word.slice(0, term.length) == term)
+ found.push(word);
+ }
+
+ if (found.length) return {list: found, from: from, to: to};
+ });
+
+ CodeMirror.commands.autocomplete = CodeMirror.showHint;
+
+ var defaultOptions = {
+ hint: CodeMirror.hint.auto,
+ completeSingle: true,
+ alignWithWord: true,
+ closeCharacters: /[\s()\[\]{};:>,]/,
+ closeOnUnfocus: true,
+ completeOnSingleClick: true,
+ container: null,
+ customKeys: null,
+ extraKeys: null
+ };
+
+ CodeMirror.defineOption("hintOptions", null);
+});
diff --git a/devtools/client/sourceeditor/codemirror/addon/search/match-highlighter.js b/devtools/client/sourceeditor/codemirror/addon/search/match-highlighter.js
new file mode 100644
index 000000000..2c2914a9d
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/addon/search/match-highlighter.js
@@ -0,0 +1,146 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+// Highlighting text that matches the selection
+//
+// Defines an option highlightSelectionMatches, which, when enabled,
+// will style strings that match the selection throughout the
+// document.
+//
+// The option can be set to true to simply enable it, or to a
+// {minChars, style, wordsOnly, showToken, delay} object to explicitly
+// configure it. minChars is the minimum amount of characters that should be
+// selected for the behavior to occur, and style is the token style to
+// apply to the matches. This will be prefixed by "cm-" to create an
+// actual CSS class name. If wordsOnly is enabled, the matches will be
+// highlighted only if the selected text is a word. showToken, when enabled,
+// will cause the current token to be highlighted when nothing is selected.
+// delay is used to specify how much time to wait, in milliseconds, before
+// highlighting the matches. If annotateScrollbar is enabled, the occurences
+// will be highlighted on the scrollbar via the matchesonscrollbar addon.
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../../lib/codemirror"), require("./matchesonscrollbar"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror", "./matchesonscrollbar"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function(CodeMirror) {
+ "use strict";
+
+ var defaults = {
+ style: "matchhighlight",
+ minChars: 2,
+ delay: 100,
+ wordsOnly: false,
+ annotateScrollbar: false,
+ showToken: false,
+ trim: true
+ }
+
+ function State(options) {
+ this.options = {}
+ for (var name in defaults)
+ this.options[name] = (options && options.hasOwnProperty(name) ? options : defaults)[name]
+ this.overlay = this.timeout = null;
+ this.matchesonscroll = null;
+ }
+
+ CodeMirror.defineOption("highlightSelectionMatches", false, function(cm, val, old) {
+ if (old && old != CodeMirror.Init) {
+ removeOverlay(cm);
+ clearTimeout(cm.state.matchHighlighter.timeout);
+ cm.state.matchHighlighter = null;
+ cm.off("cursorActivity", cursorActivity);
+ }
+ if (val) {
+ cm.state.matchHighlighter = new State(val);
+ highlightMatches(cm);
+ cm.on("cursorActivity", cursorActivity);
+ }
+ });
+
+ function cursorActivity(cm) {
+ var state = cm.state.matchHighlighter;
+ clearTimeout(state.timeout);
+ state.timeout = setTimeout(function() {highlightMatches(cm);}, state.options.delay);
+ }
+
+ function addOverlay(cm, query, hasBoundary, style) {
+ var state = cm.state.matchHighlighter;
+ cm.addOverlay(state.overlay = makeOverlay(query, hasBoundary, style));
+ if (state.options.annotateScrollbar && cm.showMatchesOnScrollbar) {
+ var searchFor = hasBoundary ? new RegExp("\\b" + query + "\\b") : query;
+ state.matchesonscroll = cm.showMatchesOnScrollbar(searchFor, true,
+ {className: "CodeMirror-selection-highlight-scrollbar"});
+ }
+ }
+
+ function removeOverlay(cm) {
+ var state = cm.state.matchHighlighter;
+ if (state.overlay) {
+ cm.removeOverlay(state.overlay);
+ state.overlay = null;
+ if (state.matchesonscroll) {
+ state.matchesonscroll.clear();
+ state.matchesonscroll = null;
+ }
+ }
+ }
+
+ function highlightMatches(cm) {
+ cm.operation(function() {
+ var state = cm.state.matchHighlighter;
+ removeOverlay(cm);
+ if (!cm.somethingSelected() && state.options.showToken) {
+ var re = state.options.showToken === true ? /[\w$]/ : state.options.showToken;
+ var cur = cm.getCursor(), line = cm.getLine(cur.line), start = cur.ch, end = start;
+ while (start && re.test(line.charAt(start - 1))) --start;
+ while (end < line.length && re.test(line.charAt(end))) ++end;
+ if (start < end)
+ addOverlay(cm, line.slice(start, end), re, state.options.style);
+ return;
+ }
+ var from = cm.getCursor("from"), to = cm.getCursor("to");
+ if (from.line != to.line) return;
+ if (state.options.wordsOnly && !isWord(cm, from, to)) return;
+ var selection = cm.getRange(from, to)
+ if (state.options.trim) selection = selection.replace(/^\s+|\s+$/g, "")
+ if (selection.length >= state.options.minChars)
+ addOverlay(cm, selection, false, state.options.style);
+ });
+ }
+
+ function isWord(cm, from, to) {
+ var str = cm.getRange(from, to);
+ if (str.match(/^\w+$/) !== null) {
+ if (from.ch > 0) {
+ var pos = {line: from.line, ch: from.ch - 1};
+ var chr = cm.getRange(pos, from);
+ if (chr.match(/\W/) === null) return false;
+ }
+ if (to.ch < cm.getLine(from.line).length) {
+ var pos = {line: to.line, ch: to.ch + 1};
+ var chr = cm.getRange(to, pos);
+ if (chr.match(/\W/) === null) return false;
+ }
+ return true;
+ } else return false;
+ }
+
+ function boundariesAround(stream, re) {
+ return (!stream.start || !re.test(stream.string.charAt(stream.start - 1))) &&
+ (stream.pos == stream.string.length || !re.test(stream.string.charAt(stream.pos)));
+ }
+
+ function makeOverlay(query, hasBoundary, style) {
+ return {token: function(stream) {
+ if (stream.match(query) &&
+ (!hasBoundary || boundariesAround(stream, hasBoundary)))
+ return style;
+ stream.next();
+ stream.skipTo(query.charAt(0)) || stream.skipToEnd();
+ }};
+ }
+});
diff --git a/devtools/client/sourceeditor/codemirror/addon/search/search.js b/devtools/client/sourceeditor/codemirror/addon/search/search.js
new file mode 100644
index 000000000..198531821
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/addon/search/search.js
@@ -0,0 +1,246 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+// Define search commands. Depends on dialog.js or another
+// implementation of the openDialog method.
+
+// Replace works a little oddly -- it will do the replace on the next
+// Ctrl-G (or whatever is bound to findNext) press. You prevent a
+// replace by making sure the match is no longer selected when hitting
+// Ctrl-G.
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../../lib/codemirror"), require("./searchcursor"), require("../dialog/dialog"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror", "./searchcursor", "../dialog/dialog"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function(CodeMirror) {
+ "use strict";
+
+ function searchOverlay(query, caseInsensitive) {
+ if (typeof query == "string")
+ query = new RegExp(query.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"), caseInsensitive ? "gi" : "g");
+ else if (!query.global)
+ query = new RegExp(query.source, query.ignoreCase ? "gi" : "g");
+
+ return {token: function(stream) {
+ query.lastIndex = stream.pos;
+ var match = query.exec(stream.string);
+ if (match && match.index == stream.pos) {
+ stream.pos += match[0].length || 1;
+ return "searching";
+ } else if (match) {
+ stream.pos = match.index;
+ } else {
+ stream.skipToEnd();
+ }
+ }};
+ }
+
+ function SearchState() {
+ this.posFrom = this.posTo = this.lastQuery = this.query = null;
+ this.overlay = null;
+ }
+
+ function getSearchState(cm) {
+ return cm.state.search || (cm.state.search = new SearchState());
+ }
+
+ function queryCaseInsensitive(query) {
+ return typeof query == "string" && query == query.toLowerCase();
+ }
+
+ function getSearchCursor(cm, query, pos) {
+ // Heuristic: if the query string is all lowercase, do a case insensitive search.
+ return cm.getSearchCursor(query, pos, queryCaseInsensitive(query));
+ }
+
+ function persistentDialog(cm, text, deflt, f) {
+ cm.openDialog(text, f, {
+ value: deflt,
+ selectValueOnOpen: true,
+ closeOnEnter: false,
+ onClose: function() { clearSearch(cm); }
+ });
+ }
+
+ function dialog(cm, text, shortText, deflt, f) {
+ if (cm.openDialog) cm.openDialog(text, f, {value: deflt, selectValueOnOpen: true});
+ else f(prompt(shortText, deflt));
+ }
+
+ function confirmDialog(cm, text, shortText, fs) {
+ if (cm.openConfirm) cm.openConfirm(text, fs);
+ else if (confirm(shortText)) fs[0]();
+ }
+
+ function parseString(string) {
+ return string.replace(/\\(.)/g, function(_, ch) {
+ if (ch == "n") return "\n"
+ if (ch == "r") return "\r"
+ return ch
+ })
+ }
+
+ function parseQuery(query) {
+ var isRE = query.match(/^\/(.*)\/([a-z]*)$/);
+ if (isRE) {
+ try { query = new RegExp(isRE[1], isRE[2].indexOf("i") == -1 ? "" : "i"); }
+ catch(e) {} // Not a regular expression after all, do a string search
+ } else {
+ query = parseString(query)
+ }
+ if (typeof query == "string" ? query == "" : query.test(""))
+ query = /x^/;
+ return query;
+ }
+
+ var queryDialog;
+
+ function startSearch(cm, state, query) {
+ state.queryText = query;
+ state.query = parseQuery(query);
+ cm.removeOverlay(state.overlay, queryCaseInsensitive(state.query));
+ state.overlay = searchOverlay(state.query, queryCaseInsensitive(state.query));
+ cm.addOverlay(state.overlay);
+ if (cm.showMatchesOnScrollbar) {
+ if (state.annotate) { state.annotate.clear(); state.annotate = null; }
+ state.annotate = cm.showMatchesOnScrollbar(state.query, queryCaseInsensitive(state.query));
+ }
+ }
+
+ function doSearch(cm, rev, persistent) {
+ if (!queryDialog) {
+ let doc = cm.getWrapperElement().ownerDocument;
+ let inp = doc.createElement("input");
+
+ inp.type = "search";
+ inp.placeholder = cm.l10n("findCmd.promptMessage");
+ inp.style.marginInlineStart = "1em";
+ inp.style.marginInlineEnd = "1em";
+ inp.style.flexGrow = "1";
+ inp.addEventListener("focus", () => inp.select());
+
+ queryDialog = doc.createElement("div");
+ queryDialog.appendChild(inp);
+ queryDialog.style.display = "flex";
+ }
+
+ var state = getSearchState(cm);
+ if (state.query) return findNext(cm, rev);
+ var q = cm.getSelection() || state.lastQuery;
+ if (persistent && cm.openDialog) {
+ var hiding = null
+ persistentDialog(cm, queryDialog, q, function(query, event) {
+ CodeMirror.e_stop(event);
+ if (!query) return;
+ if (query != state.queryText) {
+ startSearch(cm, state, query);
+ state.posFrom = state.posTo = cm.getCursor();
+ }
+ if (hiding) hiding.style.opacity = 1
+ findNext(cm, event.shiftKey, function(_, to) {
+ var dialog
+ if (to.line < 3 && document.querySelector &&
+ (dialog = cm.display.wrapper.querySelector(".CodeMirror-dialog")) &&
+ dialog.getBoundingClientRect().bottom - 4 > cm.cursorCoords(to, "window").top)
+ (hiding = dialog).style.opacity = .4
+ })
+ });
+ } else {
+ dialog(cm, queryDialog, "Search for:", q, function(query) {
+ if (query && !state.query) cm.operation(function() {
+ startSearch(cm, state, query);
+ state.posFrom = state.posTo = cm.getCursor();
+ findNext(cm, rev);
+ });
+ });
+ }
+ }
+
+ function findNext(cm, rev, callback) {cm.operation(function() {
+ var state = getSearchState(cm);
+ var cursor = getSearchCursor(cm, state.query, rev ? state.posFrom : state.posTo);
+ if (!cursor.find(rev)) {
+ cursor = getSearchCursor(cm, state.query, rev ? CodeMirror.Pos(cm.lastLine()) : CodeMirror.Pos(cm.firstLine(), 0));
+ if (!cursor.find(rev)) return;
+ }
+ cm.setSelection(cursor.from(), cursor.to());
+ cm.scrollIntoView({from: cursor.from(), to: cursor.to()}, 20);
+ state.posFrom = cursor.from(); state.posTo = cursor.to();
+ if (callback) callback(cursor.from(), cursor.to())
+ });}
+
+ function clearSearch(cm) {cm.operation(function() {
+ var state = getSearchState(cm);
+ state.lastQuery = state.query;
+ if (!state.query) return;
+ state.query = state.queryText = null;
+ cm.removeOverlay(state.overlay);
+ if (state.annotate) { state.annotate.clear(); state.annotate = null; }
+ });}
+
+ var replaceQueryDialog =
+ ' <input type="text" style="width: 10em" class="CodeMirror-search-field"/> <span style="color: #888" class="CodeMirror-search-hint">(Use /re/ syntax for regexp search)</span>';
+ var replacementQueryDialog = 'With: <input type="text" style="width: 10em" class="CodeMirror-search-field"/>';
+ var doReplaceConfirm = "Replace? <button>Yes</button> <button>No</button> <button>All</button> <button>Stop</button>";
+
+ function replaceAll(cm, query, text) {
+ cm.operation(function() {
+ for (var cursor = getSearchCursor(cm, query); cursor.findNext();) {
+ if (typeof query != "string") {
+ var match = cm.getRange(cursor.from(), cursor.to()).match(query);
+ cursor.replace(text.replace(/\$(\d)/g, function(_, i) {return match[i];}));
+ } else cursor.replace(text);
+ }
+ });
+ }
+
+ function replace(cm, all) {
+ if (cm.getOption("readOnly")) return;
+ var query = cm.getSelection() || getSearchState(cm).lastQuery;
+ var dialogText = all ? "Replace all:" : "Replace:"
+ dialog(cm, dialogText + replaceQueryDialog, dialogText, query, function(query) {
+ if (!query) return;
+ query = parseQuery(query);
+ dialog(cm, replacementQueryDialog, "Replace with:", "", function(text) {
+ text = parseString(text)
+ if (all) {
+ replaceAll(cm, query, text)
+ } else {
+ clearSearch(cm);
+ var cursor = getSearchCursor(cm, query, cm.getCursor("from"));
+ var advance = function() {
+ var start = cursor.from(), match;
+ if (!(match = cursor.findNext())) {
+ cursor = getSearchCursor(cm, query);
+ if (!(match = cursor.findNext()) ||
+ (start && cursor.from().line == start.line && cursor.from().ch == start.ch)) return;
+ }
+ cm.setSelection(cursor.from(), cursor.to());
+ cm.scrollIntoView({from: cursor.from(), to: cursor.to()});
+ confirmDialog(cm, doReplaceConfirm, "Replace?",
+ [function() {doReplace(match);}, advance,
+ function() {replaceAll(cm, query, text)}]);
+ };
+ var doReplace = function(match) {
+ cursor.replace(typeof query == "string" ? text :
+ text.replace(/\$(\d)/g, function(_, i) {return match[i];}));
+ advance();
+ };
+ advance();
+ }
+ });
+ });
+ }
+
+ CodeMirror.commands.find = function(cm) {clearSearch(cm); doSearch(cm);};
+ CodeMirror.commands.findPersistent = function(cm) {clearSearch(cm); doSearch(cm, false, true);};
+ CodeMirror.commands.findNext = doSearch;
+ CodeMirror.commands.findPrev = function(cm) {doSearch(cm, true);};
+ CodeMirror.commands.clearSearch = clearSearch;
+ CodeMirror.commands.replace = replace;
+ CodeMirror.commands.replaceAll = function(cm) {replace(cm, true);};
+});
diff --git a/devtools/client/sourceeditor/codemirror/addon/search/searchcursor.js b/devtools/client/sourceeditor/codemirror/addon/search/searchcursor.js
new file mode 100644
index 000000000..b70242ee4
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/addon/search/searchcursor.js
@@ -0,0 +1,189 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../../lib/codemirror"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function(CodeMirror) {
+ "use strict";
+ var Pos = CodeMirror.Pos;
+
+ function SearchCursor(doc, query, pos, caseFold) {
+ this.atOccurrence = false; this.doc = doc;
+ if (caseFold == null && typeof query == "string") caseFold = false;
+
+ pos = pos ? doc.clipPos(pos) : Pos(0, 0);
+ this.pos = {from: pos, to: pos};
+
+ // The matches method is filled in based on the type of query.
+ // It takes a position and a direction, and returns an object
+ // describing the next occurrence of the query, or null if no
+ // more matches were found.
+ if (typeof query != "string") { // Regexp match
+ if (!query.global) query = new RegExp(query.source, query.ignoreCase ? "ig" : "g");
+ this.matches = function(reverse, pos) {
+ if (reverse) {
+ query.lastIndex = 0;
+ var line = doc.getLine(pos.line).slice(0, pos.ch), cutOff = 0, match, start;
+ for (;;) {
+ query.lastIndex = cutOff;
+ var newMatch = query.exec(line);
+ if (!newMatch) break;
+ match = newMatch;
+ start = match.index;
+ cutOff = match.index + (match[0].length || 1);
+ if (cutOff == line.length) break;
+ }
+ var matchLen = (match && match[0].length) || 0;
+ if (!matchLen) {
+ if (start == 0 && line.length == 0) {match = undefined;}
+ else if (start != doc.getLine(pos.line).length) {
+ matchLen++;
+ }
+ }
+ } else {
+ query.lastIndex = pos.ch;
+ var line = doc.getLine(pos.line), match = query.exec(line);
+ var matchLen = (match && match[0].length) || 0;
+ var start = match && match.index;
+ if (start + matchLen != line.length && !matchLen) matchLen = 1;
+ }
+ if (match && matchLen)
+ return {from: Pos(pos.line, start),
+ to: Pos(pos.line, start + matchLen),
+ match: match};
+ };
+ } else { // String query
+ var origQuery = query;
+ if (caseFold) query = query.toLowerCase();
+ var fold = caseFold ? function(str){return str.toLowerCase();} : function(str){return str;};
+ var target = query.split("\n");
+ // Different methods for single-line and multi-line queries
+ if (target.length == 1) {
+ if (!query.length) {
+ // Empty string would match anything and never progress, so
+ // we define it to match nothing instead.
+ this.matches = function() {};
+ } else {
+ this.matches = function(reverse, pos) {
+ if (reverse) {
+ var orig = doc.getLine(pos.line).slice(0, pos.ch), line = fold(orig);
+ var match = line.lastIndexOf(query);
+ if (match > -1) {
+ match = adjustPos(orig, line, match);
+ return {from: Pos(pos.line, match), to: Pos(pos.line, match + origQuery.length)};
+ }
+ } else {
+ var orig = doc.getLine(pos.line).slice(pos.ch), line = fold(orig);
+ var match = line.indexOf(query);
+ if (match > -1) {
+ match = adjustPos(orig, line, match) + pos.ch;
+ return {from: Pos(pos.line, match), to: Pos(pos.line, match + origQuery.length)};
+ }
+ }
+ };
+ }
+ } else {
+ var origTarget = origQuery.split("\n");
+ this.matches = function(reverse, pos) {
+ var last = target.length - 1;
+ if (reverse) {
+ if (pos.line - (target.length - 1) < doc.firstLine()) return;
+ if (fold(doc.getLine(pos.line).slice(0, origTarget[last].length)) != target[target.length - 1]) return;
+ var to = Pos(pos.line, origTarget[last].length);
+ for (var ln = pos.line - 1, i = last - 1; i >= 1; --i, --ln)
+ if (target[i] != fold(doc.getLine(ln))) return;
+ var line = doc.getLine(ln), cut = line.length - origTarget[0].length;
+ if (fold(line.slice(cut)) != target[0]) return;
+ return {from: Pos(ln, cut), to: to};
+ } else {
+ if (pos.line + (target.length - 1) > doc.lastLine()) return;
+ var line = doc.getLine(pos.line), cut = line.length - origTarget[0].length;
+ if (fold(line.slice(cut)) != target[0]) return;
+ var from = Pos(pos.line, cut);
+ for (var ln = pos.line + 1, i = 1; i < last; ++i, ++ln)
+ if (target[i] != fold(doc.getLine(ln))) return;
+ if (fold(doc.getLine(ln).slice(0, origTarget[last].length)) != target[last]) return;
+ return {from: from, to: Pos(ln, origTarget[last].length)};
+ }
+ };
+ }
+ }
+ }
+
+ SearchCursor.prototype = {
+ findNext: function() {return this.find(false);},
+ findPrevious: function() {return this.find(true);},
+
+ find: function(reverse) {
+ var self = this, pos = this.doc.clipPos(reverse ? this.pos.from : this.pos.to);
+ function savePosAndFail(line) {
+ var pos = Pos(line, 0);
+ self.pos = {from: pos, to: pos};
+ self.atOccurrence = false;
+ return false;
+ }
+
+ for (;;) {
+ if (this.pos = this.matches(reverse, pos)) {
+ this.atOccurrence = true;
+ return this.pos.match || true;
+ }
+ if (reverse) {
+ if (!pos.line) return savePosAndFail(0);
+ pos = Pos(pos.line-1, this.doc.getLine(pos.line-1).length);
+ }
+ else {
+ var maxLine = this.doc.lineCount();
+ if (pos.line == maxLine - 1) return savePosAndFail(maxLine);
+ pos = Pos(pos.line + 1, 0);
+ }
+ }
+ },
+
+ from: function() {if (this.atOccurrence) return this.pos.from;},
+ to: function() {if (this.atOccurrence) return this.pos.to;},
+
+ replace: function(newText, origin) {
+ if (!this.atOccurrence) return;
+ var lines = CodeMirror.splitLines(newText);
+ this.doc.replaceRange(lines, this.pos.from, this.pos.to, origin);
+ this.pos.to = Pos(this.pos.from.line + lines.length - 1,
+ lines[lines.length - 1].length + (lines.length == 1 ? this.pos.from.ch : 0));
+ }
+ };
+
+ // Maps a position in a case-folded line back to a position in the original line
+ // (compensating for codepoints increasing in number during folding)
+ function adjustPos(orig, folded, pos) {
+ if (orig.length == folded.length) return pos;
+ for (var pos1 = Math.min(pos, orig.length);;) {
+ var len1 = orig.slice(0, pos1).toLowerCase().length;
+ if (len1 < pos) ++pos1;
+ else if (len1 > pos) --pos1;
+ else return pos1;
+ }
+ }
+
+ CodeMirror.defineExtension("getSearchCursor", function(query, pos, caseFold) {
+ return new SearchCursor(this.doc, query, pos, caseFold);
+ });
+ CodeMirror.defineDocExtension("getSearchCursor", function(query, pos, caseFold) {
+ return new SearchCursor(this, query, pos, caseFold);
+ });
+
+ CodeMirror.defineExtension("selectMatches", function(query, caseFold) {
+ var ranges = [];
+ var cur = this.getSearchCursor(query, this.getCursor("from"), caseFold);
+ while (cur.findNext()) {
+ if (CodeMirror.cmpPos(cur.to(), this.getCursor("to")) > 0) break;
+ ranges.push({anchor: cur.from(), head: cur.to()});
+ }
+ if (ranges.length)
+ this.setSelections(ranges, 0);
+ });
+});
diff --git a/devtools/client/sourceeditor/codemirror/addon/selection/active-line.js b/devtools/client/sourceeditor/codemirror/addon/selection/active-line.js
new file mode 100644
index 000000000..b0b3f61af
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/addon/selection/active-line.js
@@ -0,0 +1,74 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+// Because sometimes you need to style the cursor's line.
+//
+// Adds an option 'styleActiveLine' which, when enabled, gives the
+// active line's wrapping <div> the CSS class "CodeMirror-activeline",
+// and gives its background <div> the class "CodeMirror-activeline-background".
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../../lib/codemirror"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function(CodeMirror) {
+ "use strict";
+ var WRAP_CLASS = "CodeMirror-activeline";
+ var BACK_CLASS = "CodeMirror-activeline-background";
+ var GUTT_CLASS = "CodeMirror-activeline-gutter";
+
+ CodeMirror.defineOption("styleActiveLine", false, function(cm, val, old) {
+ var prev = old && old != CodeMirror.Init;
+ if (val && !prev) {
+ cm.state.activeLines = [];
+ updateActiveLines(cm, cm.listSelections());
+ cm.on("beforeSelectionChange", selectionChange);
+ } else if (!val && prev) {
+ cm.off("beforeSelectionChange", selectionChange);
+ clearActiveLines(cm);
+ delete cm.state.activeLines;
+ }
+ });
+
+ function clearActiveLines(cm) {
+ for (var i = 0; i < cm.state.activeLines.length; i++) {
+ cm.removeLineClass(cm.state.activeLines[i], "wrap", WRAP_CLASS);
+ cm.removeLineClass(cm.state.activeLines[i], "background", BACK_CLASS);
+ cm.removeLineClass(cm.state.activeLines[i], "gutter", GUTT_CLASS);
+ }
+ }
+
+ function sameArray(a, b) {
+ if (a.length != b.length) return false;
+ for (var i = 0; i < a.length; i++)
+ if (a[i] != b[i]) return false;
+ return true;
+ }
+
+ function updateActiveLines(cm, ranges) {
+ var active = [];
+ for (var i = 0; i < ranges.length; i++) {
+ var range = ranges[i];
+ if (!range.empty()) continue;
+ var line = cm.getLineHandleVisualStart(range.head.line);
+ if (active[active.length - 1] != line) active.push(line);
+ }
+ if (sameArray(cm.state.activeLines, active)) return;
+ cm.operation(function() {
+ clearActiveLines(cm);
+ for (var i = 0; i < active.length; i++) {
+ cm.addLineClass(active[i], "wrap", WRAP_CLASS);
+ cm.addLineClass(active[i], "background", BACK_CLASS);
+ cm.addLineClass(active[i], "gutter", GUTT_CLASS);
+ }
+ cm.state.activeLines = active;
+ });
+ }
+
+ function selectionChange(cm, sel) {
+ updateActiveLines(cm, sel.ranges);
+ }
+});
diff --git a/devtools/client/sourceeditor/codemirror/addon/selection/mark-selection.js b/devtools/client/sourceeditor/codemirror/addon/selection/mark-selection.js
new file mode 100644
index 000000000..5c42d21eb
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/addon/selection/mark-selection.js
@@ -0,0 +1,118 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+// Because sometimes you need to mark the selected *text*.
+//
+// Adds an option 'styleSelectedText' which, when enabled, gives
+// selected text the CSS class given as option value, or
+// "CodeMirror-selectedtext" when the value is not a string.
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../../lib/codemirror"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function(CodeMirror) {
+ "use strict";
+
+ CodeMirror.defineOption("styleSelectedText", false, function(cm, val, old) {
+ var prev = old && old != CodeMirror.Init;
+ if (val && !prev) {
+ cm.state.markedSelection = [];
+ cm.state.markedSelectionStyle = typeof val == "string" ? val : "CodeMirror-selectedtext";
+ reset(cm);
+ cm.on("cursorActivity", onCursorActivity);
+ cm.on("change", onChange);
+ } else if (!val && prev) {
+ cm.off("cursorActivity", onCursorActivity);
+ cm.off("change", onChange);
+ clear(cm);
+ cm.state.markedSelection = cm.state.markedSelectionStyle = null;
+ }
+ });
+
+ function onCursorActivity(cm) {
+ cm.operation(function() { update(cm); });
+ }
+
+ function onChange(cm) {
+ if (cm.state.markedSelection.length)
+ cm.operation(function() { clear(cm); });
+ }
+
+ var CHUNK_SIZE = 8;
+ var Pos = CodeMirror.Pos;
+ var cmp = CodeMirror.cmpPos;
+
+ function coverRange(cm, from, to, addAt) {
+ if (cmp(from, to) == 0) return;
+ var array = cm.state.markedSelection;
+ var cls = cm.state.markedSelectionStyle;
+ for (var line = from.line;;) {
+ var start = line == from.line ? from : Pos(line, 0);
+ var endLine = line + CHUNK_SIZE, atEnd = endLine >= to.line;
+ var end = atEnd ? to : Pos(endLine, 0);
+ var mark = cm.markText(start, end, {className: cls});
+ if (addAt == null) array.push(mark);
+ else array.splice(addAt++, 0, mark);
+ if (atEnd) break;
+ line = endLine;
+ }
+ }
+
+ function clear(cm) {
+ var array = cm.state.markedSelection;
+ for (var i = 0; i < array.length; ++i) array[i].clear();
+ array.length = 0;
+ }
+
+ function reset(cm) {
+ clear(cm);
+ var ranges = cm.listSelections();
+ for (var i = 0; i < ranges.length; i++)
+ coverRange(cm, ranges[i].from(), ranges[i].to());
+ }
+
+ function update(cm) {
+ if (!cm.somethingSelected()) return clear(cm);
+ if (cm.listSelections().length > 1) return reset(cm);
+
+ var from = cm.getCursor("start"), to = cm.getCursor("end");
+
+ var array = cm.state.markedSelection;
+ if (!array.length) return coverRange(cm, from, to);
+
+ var coverStart = array[0].find(), coverEnd = array[array.length - 1].find();
+ if (!coverStart || !coverEnd || to.line - from.line < CHUNK_SIZE ||
+ cmp(from, coverEnd.to) >= 0 || cmp(to, coverStart.from) <= 0)
+ return reset(cm);
+
+ while (cmp(from, coverStart.from) > 0) {
+ array.shift().clear();
+ coverStart = array[0].find();
+ }
+ if (cmp(from, coverStart.from) < 0) {
+ if (coverStart.to.line - from.line < CHUNK_SIZE) {
+ array.shift().clear();
+ coverRange(cm, from, coverStart.to, 0);
+ } else {
+ coverRange(cm, from, coverStart.from, 0);
+ }
+ }
+
+ while (cmp(to, coverEnd.to) < 0) {
+ array.pop().clear();
+ coverEnd = array[array.length - 1].find();
+ }
+ if (cmp(to, coverEnd.to) > 0) {
+ if (to.line - coverEnd.from.line < CHUNK_SIZE) {
+ array.pop().clear();
+ coverRange(cm, coverEnd.from, to);
+ } else {
+ coverRange(cm, coverEnd.to, to);
+ }
+ }
+ }
+});
diff --git a/devtools/client/sourceeditor/codemirror/addon/tern/tern.css b/devtools/client/sourceeditor/codemirror/addon/tern/tern.css
new file mode 100644
index 000000000..c4b8a2f77
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/addon/tern/tern.css
@@ -0,0 +1,87 @@
+.CodeMirror-Tern-completion {
+ padding-left: 22px;
+ position: relative;
+ line-height: 1.5;
+}
+.CodeMirror-Tern-completion:before {
+ position: absolute;
+ left: 2px;
+ bottom: 2px;
+ border-radius: 50%;
+ font-size: 12px;
+ font-weight: bold;
+ height: 15px;
+ width: 15px;
+ line-height: 16px;
+ text-align: center;
+ color: white;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+.CodeMirror-Tern-completion-unknown:before {
+ content: "?";
+ background: #4bb;
+}
+.CodeMirror-Tern-completion-object:before {
+ content: "O";
+ background: #77c;
+}
+.CodeMirror-Tern-completion-fn:before {
+ content: "F";
+ background: #7c7;
+}
+.CodeMirror-Tern-completion-array:before {
+ content: "A";
+ background: #c66;
+}
+.CodeMirror-Tern-completion-number:before {
+ content: "1";
+ background: #999;
+}
+.CodeMirror-Tern-completion-string:before {
+ content: "S";
+ background: #999;
+}
+.CodeMirror-Tern-completion-bool:before {
+ content: "B";
+ background: #999;
+}
+
+.CodeMirror-Tern-completion-guess {
+ color: #999;
+}
+
+.CodeMirror-Tern-tooltip {
+ border: 1px solid silver;
+ border-radius: 3px;
+ color: #444;
+ padding: 2px 5px;
+ font-size: 90%;
+ font-family: monospace;
+ background-color: white;
+ white-space: pre-wrap;
+
+ max-width: 40em;
+ position: absolute;
+ z-index: 10;
+ -webkit-box-shadow: 2px 3px 5px rgba(0,0,0,.2);
+ -moz-box-shadow: 2px 3px 5px rgba(0,0,0,.2);
+ box-shadow: 2px 3px 5px rgba(0,0,0,.2);
+
+ transition: opacity 1s;
+ -moz-transition: opacity 1s;
+ -webkit-transition: opacity 1s;
+ -o-transition: opacity 1s;
+ -ms-transition: opacity 1s;
+}
+
+.CodeMirror-Tern-hint-doc {
+ max-width: 25em;
+ margin-top: -3px;
+}
+
+.CodeMirror-Tern-fname { color: black; }
+.CodeMirror-Tern-farg { color: #70a; }
+.CodeMirror-Tern-farg-current { text-decoration: underline; }
+.CodeMirror-Tern-type { color: #07c; }
+.CodeMirror-Tern-fhint-guess { opacity: .7; }
diff --git a/devtools/client/sourceeditor/codemirror/addon/tern/tern.js b/devtools/client/sourceeditor/codemirror/addon/tern/tern.js
new file mode 100644
index 000000000..efdf2ed62
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/addon/tern/tern.js
@@ -0,0 +1,701 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+// Glue code between CodeMirror and Tern.
+//
+// Create a CodeMirror.TernServer to wrap an actual Tern server,
+// register open documents (CodeMirror.Doc instances) with it, and
+// call its methods to activate the assisting functions that Tern
+// provides.
+//
+// Options supported (all optional):
+// * defs: An array of JSON definition data structures.
+// * plugins: An object mapping plugin names to configuration
+// options.
+// * getFile: A function(name, c) that can be used to access files in
+// the project that haven't been loaded yet. Simply do c(null) to
+// indicate that a file is not available.
+// * fileFilter: A function(value, docName, doc) that will be applied
+// to documents before passing them on to Tern.
+// * switchToDoc: A function(name, doc) that should, when providing a
+// multi-file view, switch the view or focus to the named file.
+// * showError: A function(editor, message) that can be used to
+// override the way errors are displayed.
+// * completionTip: Customize the content in tooltips for completions.
+// Is passed a single argument—the completion's data as returned by
+// Tern—and may return a string, DOM node, or null to indicate that
+// no tip should be shown. By default the docstring is shown.
+// * typeTip: Like completionTip, but for the tooltips shown for type
+// queries.
+// * responseFilter: A function(doc, query, request, error, data) that
+// will be applied to the Tern responses before treating them
+//
+//
+// It is possible to run the Tern server in a web worker by specifying
+// these additional options:
+// * useWorker: Set to true to enable web worker mode. You'll probably
+// want to feature detect the actual value you use here, for example
+// !!window.Worker.
+// * workerScript: The main script of the worker. Point this to
+// wherever you are hosting worker.js from this directory.
+// * workerDeps: An array of paths pointing (relative to workerScript)
+// to the Acorn and Tern libraries and any Tern plugins you want to
+// load. Or, if you minified those into a single script and included
+// them in the workerScript, simply leave this undefined.
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../../lib/codemirror"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function(CodeMirror) {
+ "use strict";
+ // declare global: tern
+
+ CodeMirror.TernServer = function(options) {
+ var self = this;
+ this.options = options || {};
+ var plugins = this.options.plugins || (this.options.plugins = {});
+ if (!plugins.doc_comment) plugins.doc_comment = true;
+ this.docs = Object.create(null);
+ if (this.options.useWorker) {
+ this.server = new WorkerServer(this);
+ } else {
+ this.server = new tern.Server({
+ getFile: function(name, c) { return getFile(self, name, c); },
+ async: true,
+ defs: this.options.defs || [],
+ plugins: plugins
+ });
+ }
+ this.trackChange = function(doc, change) { trackChange(self, doc, change); };
+
+ this.cachedArgHints = null;
+ this.activeArgHints = null;
+ this.jumpStack = [];
+
+ this.getHint = function(cm, c) { return hint(self, cm, c); };
+ this.getHint.async = true;
+ };
+
+ CodeMirror.TernServer.prototype = {
+ addDoc: function(name, doc) {
+ var data = {doc: doc, name: name, changed: null};
+ this.server.addFile(name, docValue(this, data));
+ CodeMirror.on(doc, "change", this.trackChange);
+ return this.docs[name] = data;
+ },
+
+ delDoc: function(id) {
+ var found = resolveDoc(this, id);
+ if (!found) return;
+ CodeMirror.off(found.doc, "change", this.trackChange);
+ delete this.docs[found.name];
+ this.server.delFile(found.name);
+ },
+
+ hideDoc: function(id) {
+ closeArgHints(this);
+ var found = resolveDoc(this, id);
+ if (found && found.changed) sendDoc(this, found);
+ },
+
+ complete: function(cm) {
+ cm.showHint({hint: this.getHint});
+ },
+
+ showType: function(cm, pos, c) { showContextInfo(this, cm, pos, "type", c); },
+
+ showDocs: function(cm, pos, c) { showContextInfo(this, cm, pos, "documentation", c); },
+
+ updateArgHints: function(cm) { updateArgHints(this, cm); },
+
+ jumpToDef: function(cm) { jumpToDef(this, cm); },
+
+ jumpBack: function(cm) { jumpBack(this, cm); },
+
+ rename: function(cm) { rename(this, cm); },
+
+ selectName: function(cm) { selectName(this, cm); },
+
+ request: function (cm, query, c, pos) {
+ var self = this;
+ var doc = findDoc(this, cm.getDoc());
+ var request = buildRequest(this, doc, query, pos);
+ var extraOptions = request.query && this.options.queryOptions && this.options.queryOptions[request.query.type]
+ if (extraOptions) for (var prop in extraOptions) request.query[prop] = extraOptions[prop];
+
+ this.server.request(request, function (error, data) {
+ if (!error && self.options.responseFilter)
+ data = self.options.responseFilter(doc, query, request, error, data);
+ c(error, data);
+ });
+ },
+
+ destroy: function () {
+ closeArgHints(this)
+ if (this.worker) {
+ this.worker.terminate();
+ this.worker = null;
+ }
+ }
+ };
+
+ var Pos = CodeMirror.Pos;
+ var cls = "CodeMirror-Tern-";
+ var bigDoc = 250;
+
+ function getFile(ts, name, c) {
+ var buf = ts.docs[name];
+ if (buf)
+ c(docValue(ts, buf));
+ else if (ts.options.getFile)
+ ts.options.getFile(name, c);
+ else
+ c(null);
+ }
+
+ function findDoc(ts, doc, name) {
+ for (var n in ts.docs) {
+ var cur = ts.docs[n];
+ if (cur.doc == doc) return cur;
+ }
+ if (!name) for (var i = 0;; ++i) {
+ n = "[doc" + (i || "") + "]";
+ if (!ts.docs[n]) { name = n; break; }
+ }
+ return ts.addDoc(name, doc);
+ }
+
+ function resolveDoc(ts, id) {
+ if (typeof id == "string") return ts.docs[id];
+ if (id instanceof CodeMirror) id = id.getDoc();
+ if (id instanceof CodeMirror.Doc) return findDoc(ts, id);
+ }
+
+ function trackChange(ts, doc, change) {
+ var data = findDoc(ts, doc);
+
+ var argHints = ts.cachedArgHints;
+ if (argHints && argHints.doc == doc && cmpPos(argHints.start, change.to) >= 0)
+ ts.cachedArgHints = null;
+
+ var changed = data.changed;
+ if (changed == null)
+ data.changed = changed = {from: change.from.line, to: change.from.line};
+ var end = change.from.line + (change.text.length - 1);
+ if (change.from.line < changed.to) changed.to = changed.to - (change.to.line - end);
+ if (end >= changed.to) changed.to = end + 1;
+ if (changed.from > change.from.line) changed.from = change.from.line;
+
+ if (doc.lineCount() > bigDoc && change.to - changed.from > 100) setTimeout(function() {
+ if (data.changed && data.changed.to - data.changed.from > 100) sendDoc(ts, data);
+ }, 200);
+ }
+
+ function sendDoc(ts, doc) {
+ ts.server.request({files: [{type: "full", name: doc.name, text: docValue(ts, doc)}]}, function(error) {
+ if (error) window.console.error(error);
+ else doc.changed = null;
+ });
+ }
+
+ // Completion
+
+ function hint(ts, cm, c) {
+ ts.request(cm, {type: "completions", types: true, docs: true, urls: true}, function(error, data) {
+ if (error) return showError(ts, cm, error);
+ var completions = [], after = "";
+ var from = data.start, to = data.end;
+ if (cm.getRange(Pos(from.line, from.ch - 2), from) == "[\"" &&
+ cm.getRange(to, Pos(to.line, to.ch + 2)) != "\"]")
+ after = "\"]";
+
+ for (var i = 0; i < data.completions.length; ++i) {
+ var completion = data.completions[i], className = typeToIcon(completion.type);
+ if (data.guess) className += " " + cls + "guess";
+ completions.push({text: completion.name + after,
+ displayText: completion.displayName || completion.name,
+ className: className,
+ data: completion});
+ }
+
+ var obj = {from: from, to: to, list: completions};
+ var tooltip = null;
+ CodeMirror.on(obj, "close", function() { remove(tooltip); });
+ CodeMirror.on(obj, "update", function() { remove(tooltip); });
+ CodeMirror.on(obj, "select", function(cur, node) {
+ remove(tooltip);
+ var content = ts.options.completionTip ? ts.options.completionTip(cur.data) : cur.data.doc;
+ if (content) {
+ tooltip = makeTooltip(node.parentNode.getBoundingClientRect().right + window.pageXOffset,
+ node.getBoundingClientRect().top + window.pageYOffset, content);
+ tooltip.className += " " + cls + "hint-doc";
+ }
+ });
+ c(obj);
+ });
+ }
+
+ function typeToIcon(type) {
+ var suffix;
+ if (type == "?") suffix = "unknown";
+ else if (type == "number" || type == "string" || type == "bool") suffix = type;
+ else if (/^fn\(/.test(type)) suffix = "fn";
+ else if (/^\[/.test(type)) suffix = "array";
+ else suffix = "object";
+ return cls + "completion " + cls + "completion-" + suffix;
+ }
+
+ // Type queries
+
+ function showContextInfo(ts, cm, pos, queryName, c) {
+ ts.request(cm, queryName, function(error, data) {
+ if (error) return showError(ts, cm, error);
+ if (ts.options.typeTip) {
+ var tip = ts.options.typeTip(data);
+ } else {
+ var tip = elt("span", null, elt("strong", null, data.type || "not found"));
+ if (data.doc)
+ tip.appendChild(document.createTextNode(" — " + data.doc));
+ if (data.url) {
+ tip.appendChild(document.createTextNode(" "));
+ var child = tip.appendChild(elt("a", null, "[docs]"));
+ child.href = data.url;
+ child.target = "_blank";
+ }
+ }
+ tempTooltip(cm, tip, ts);
+ if (c) c();
+ }, pos);
+ }
+
+ // Maintaining argument hints
+
+ function updateArgHints(ts, cm) {
+ closeArgHints(ts);
+
+ if (cm.somethingSelected()) return;
+ var state = cm.getTokenAt(cm.getCursor()).state;
+ var inner = CodeMirror.innerMode(cm.getMode(), state);
+ if (inner.mode.name != "javascript") return;
+ var lex = inner.state.lexical;
+ if (lex.info != "call") return;
+
+ var ch, argPos = lex.pos || 0, tabSize = cm.getOption("tabSize");
+ for (var line = cm.getCursor().line, e = Math.max(0, line - 9), found = false; line >= e; --line) {
+ var str = cm.getLine(line), extra = 0;
+ for (var pos = 0;;) {
+ var tab = str.indexOf("\t", pos);
+ if (tab == -1) break;
+ extra += tabSize - (tab + extra) % tabSize - 1;
+ pos = tab + 1;
+ }
+ ch = lex.column - extra;
+ if (str.charAt(ch) == "(") {found = true; break;}
+ }
+ if (!found) return;
+
+ var start = Pos(line, ch);
+ var cache = ts.cachedArgHints;
+ if (cache && cache.doc == cm.getDoc() && cmpPos(start, cache.start) == 0)
+ return showArgHints(ts, cm, argPos);
+
+ ts.request(cm, {type: "type", preferFunction: true, end: start}, function(error, data) {
+ if (error || !data.type || !(/^fn\(/).test(data.type)) return;
+ ts.cachedArgHints = {
+ start: start,
+ type: parseFnType(data.type),
+ name: data.exprName || data.name || "fn",
+ guess: data.guess,
+ doc: cm.getDoc()
+ };
+ showArgHints(ts, cm, argPos);
+ });
+ }
+
+ function showArgHints(ts, cm, pos) {
+ closeArgHints(ts);
+
+ var cache = ts.cachedArgHints, tp = cache.type;
+ var tip = elt("span", cache.guess ? cls + "fhint-guess" : null,
+ elt("span", cls + "fname", cache.name), "(");
+ for (var i = 0; i < tp.args.length; ++i) {
+ if (i) tip.appendChild(document.createTextNode(", "));
+ var arg = tp.args[i];
+ tip.appendChild(elt("span", cls + "farg" + (i == pos ? " " + cls + "farg-current" : ""), arg.name || "?"));
+ if (arg.type != "?") {
+ tip.appendChild(document.createTextNode(":\u00a0"));
+ tip.appendChild(elt("span", cls + "type", arg.type));
+ }
+ }
+ tip.appendChild(document.createTextNode(tp.rettype ? ") ->\u00a0" : ")"));
+ if (tp.rettype) tip.appendChild(elt("span", cls + "type", tp.rettype));
+ var place = cm.cursorCoords(null, "page");
+ ts.activeArgHints = makeTooltip(place.right + 1, place.bottom, tip);
+ }
+
+ function parseFnType(text) {
+ var args = [], pos = 3;
+
+ function skipMatching(upto) {
+ var depth = 0, start = pos;
+ for (;;) {
+ var next = text.charAt(pos);
+ if (upto.test(next) && !depth) return text.slice(start, pos);
+ if (/[{\[\(]/.test(next)) ++depth;
+ else if (/[}\]\)]/.test(next)) --depth;
+ ++pos;
+ }
+ }
+
+ // Parse arguments
+ if (text.charAt(pos) != ")") for (;;) {
+ var name = text.slice(pos).match(/^([^, \(\[\{]+): /);
+ if (name) {
+ pos += name[0].length;
+ name = name[1];
+ }
+ args.push({name: name, type: skipMatching(/[\),]/)});
+ if (text.charAt(pos) == ")") break;
+ pos += 2;
+ }
+
+ var rettype = text.slice(pos).match(/^\) -> (.*)$/);
+
+ return {args: args, rettype: rettype && rettype[1]};
+ }
+
+ // Moving to the definition of something
+
+ function jumpToDef(ts, cm) {
+ function inner(varName) {
+ var req = {type: "definition", variable: varName || null};
+ var doc = findDoc(ts, cm.getDoc());
+ ts.server.request(buildRequest(ts, doc, req), function(error, data) {
+ if (error) return showError(ts, cm, error);
+ if (!data.file && data.url) { window.open(data.url); return; }
+
+ if (data.file) {
+ var localDoc = ts.docs[data.file], found;
+ if (localDoc && (found = findContext(localDoc.doc, data))) {
+ ts.jumpStack.push({file: doc.name,
+ start: cm.getCursor("from"),
+ end: cm.getCursor("to")});
+ moveTo(ts, doc, localDoc, found.start, found.end);
+ return;
+ }
+ }
+ showError(ts, cm, "Could not find a definition.");
+ });
+ }
+
+ if (!atInterestingExpression(cm))
+ dialog(cm, "Jump to variable", function(name) { if (name) inner(name); });
+ else
+ inner();
+ }
+
+ function jumpBack(ts, cm) {
+ var pos = ts.jumpStack.pop(), doc = pos && ts.docs[pos.file];
+ if (!doc) return;
+ moveTo(ts, findDoc(ts, cm.getDoc()), doc, pos.start, pos.end);
+ }
+
+ function moveTo(ts, curDoc, doc, start, end) {
+ doc.doc.setSelection(start, end);
+ if (curDoc != doc && ts.options.switchToDoc) {
+ closeArgHints(ts);
+ ts.options.switchToDoc(doc.name, doc.doc);
+ }
+ }
+
+ // The {line,ch} representation of positions makes this rather awkward.
+ function findContext(doc, data) {
+ var before = data.context.slice(0, data.contextOffset).split("\n");
+ var startLine = data.start.line - (before.length - 1);
+ var start = Pos(startLine, (before.length == 1 ? data.start.ch : doc.getLine(startLine).length) - before[0].length);
+
+ var text = doc.getLine(startLine).slice(start.ch);
+ for (var cur = startLine + 1; cur < doc.lineCount() && text.length < data.context.length; ++cur)
+ text += "\n" + doc.getLine(cur);
+ if (text.slice(0, data.context.length) == data.context) return data;
+
+ var cursor = doc.getSearchCursor(data.context, 0, false);
+ var nearest, nearestDist = Infinity;
+ while (cursor.findNext()) {
+ var from = cursor.from(), dist = Math.abs(from.line - start.line) * 10000;
+ if (!dist) dist = Math.abs(from.ch - start.ch);
+ if (dist < nearestDist) { nearest = from; nearestDist = dist; }
+ }
+ if (!nearest) return null;
+
+ if (before.length == 1)
+ nearest.ch += before[0].length;
+ else
+ nearest = Pos(nearest.line + (before.length - 1), before[before.length - 1].length);
+ if (data.start.line == data.end.line)
+ var end = Pos(nearest.line, nearest.ch + (data.end.ch - data.start.ch));
+ else
+ var end = Pos(nearest.line + (data.end.line - data.start.line), data.end.ch);
+ return {start: nearest, end: end};
+ }
+
+ function atInterestingExpression(cm) {
+ var pos = cm.getCursor("end"), tok = cm.getTokenAt(pos);
+ if (tok.start < pos.ch && tok.type == "comment") return false;
+ return /[\w)\]]/.test(cm.getLine(pos.line).slice(Math.max(pos.ch - 1, 0), pos.ch + 1));
+ }
+
+ // Variable renaming
+
+ function rename(ts, cm) {
+ var token = cm.getTokenAt(cm.getCursor());
+ if (!/\w/.test(token.string)) return showError(ts, cm, "Not at a variable");
+ dialog(cm, "New name for " + token.string, function(newName) {
+ ts.request(cm, {type: "rename", newName: newName, fullDocs: true}, function(error, data) {
+ if (error) return showError(ts, cm, error);
+ applyChanges(ts, data.changes);
+ });
+ });
+ }
+
+ function selectName(ts, cm) {
+ var name = findDoc(ts, cm.doc).name;
+ ts.request(cm, {type: "refs"}, function(error, data) {
+ if (error) return showError(ts, cm, error);
+ var ranges = [], cur = 0;
+ var curPos = cm.getCursor();
+ for (var i = 0; i < data.refs.length; i++) {
+ var ref = data.refs[i];
+ if (ref.file == name) {
+ ranges.push({anchor: ref.start, head: ref.end});
+ if (cmpPos(curPos, ref.start) >= 0 && cmpPos(curPos, ref.end) <= 0)
+ cur = ranges.length - 1;
+ }
+ }
+ cm.setSelections(ranges, cur);
+ });
+ }
+
+ var nextChangeOrig = 0;
+ function applyChanges(ts, changes) {
+ var perFile = Object.create(null);
+ for (var i = 0; i < changes.length; ++i) {
+ var ch = changes[i];
+ (perFile[ch.file] || (perFile[ch.file] = [])).push(ch);
+ }
+ for (var file in perFile) {
+ var known = ts.docs[file], chs = perFile[file];;
+ if (!known) continue;
+ chs.sort(function(a, b) { return cmpPos(b.start, a.start); });
+ var origin = "*rename" + (++nextChangeOrig);
+ for (var i = 0; i < chs.length; ++i) {
+ var ch = chs[i];
+ known.doc.replaceRange(ch.text, ch.start, ch.end, origin);
+ }
+ }
+ }
+
+ // Generic request-building helper
+
+ function buildRequest(ts, doc, query, pos) {
+ var files = [], offsetLines = 0, allowFragments = !query.fullDocs;
+ if (!allowFragments) delete query.fullDocs;
+ if (typeof query == "string") query = {type: query};
+ query.lineCharPositions = true;
+ if (query.end == null) {
+ query.end = pos || doc.doc.getCursor("end");
+ if (doc.doc.somethingSelected())
+ query.start = doc.doc.getCursor("start");
+ }
+ var startPos = query.start || query.end;
+
+ if (doc.changed) {
+ if (doc.doc.lineCount() > bigDoc && allowFragments !== false &&
+ doc.changed.to - doc.changed.from < 100 &&
+ doc.changed.from <= startPos.line && doc.changed.to > query.end.line) {
+ files.push(getFragmentAround(doc, startPos, query.end));
+ query.file = "#0";
+ var offsetLines = files[0].offsetLines;
+ if (query.start != null) query.start = Pos(query.start.line - -offsetLines, query.start.ch);
+ query.end = Pos(query.end.line - offsetLines, query.end.ch);
+ } else {
+ files.push({type: "full",
+ name: doc.name,
+ text: docValue(ts, doc)});
+ query.file = doc.name;
+ doc.changed = null;
+ }
+ } else {
+ query.file = doc.name;
+ }
+ for (var name in ts.docs) {
+ var cur = ts.docs[name];
+ if (cur.changed && cur != doc) {
+ files.push({type: "full", name: cur.name, text: docValue(ts, cur)});
+ cur.changed = null;
+ }
+ }
+
+ return {query: query, files: files};
+ }
+
+ function getFragmentAround(data, start, end) {
+ var doc = data.doc;
+ var minIndent = null, minLine = null, endLine, tabSize = 4;
+ for (var p = start.line - 1, min = Math.max(0, p - 50); p >= min; --p) {
+ var line = doc.getLine(p), fn = line.search(/\bfunction\b/);
+ if (fn < 0) continue;
+ var indent = CodeMirror.countColumn(line, null, tabSize);
+ if (minIndent != null && minIndent <= indent) continue;
+ minIndent = indent;
+ minLine = p;
+ }
+ if (minLine == null) minLine = min;
+ var max = Math.min(doc.lastLine(), end.line + 20);
+ if (minIndent == null || minIndent == CodeMirror.countColumn(doc.getLine(start.line), null, tabSize))
+ endLine = max;
+ else for (endLine = end.line + 1; endLine < max; ++endLine) {
+ var indent = CodeMirror.countColumn(doc.getLine(endLine), null, tabSize);
+ if (indent <= minIndent) break;
+ }
+ var from = Pos(minLine, 0);
+
+ return {type: "part",
+ name: data.name,
+ offsetLines: from.line,
+ text: doc.getRange(from, Pos(endLine, 0))};
+ }
+
+ // Generic utilities
+
+ var cmpPos = CodeMirror.cmpPos;
+
+ function elt(tagname, cls /*, ... elts*/) {
+ var e = document.createElement(tagname);
+ if (cls) e.className = cls;
+ for (var i = 2; i < arguments.length; ++i) {
+ var elt = arguments[i];
+ if (typeof elt == "string") elt = document.createTextNode(elt);
+ e.appendChild(elt);
+ }
+ return e;
+ }
+
+ function dialog(cm, text, f) {
+ if (cm.openDialog)
+ cm.openDialog(text + ": <input type=text>", f);
+ else
+ f(prompt(text, ""));
+ }
+
+ // Tooltips
+
+ function tempTooltip(cm, content, ts) {
+ if (cm.state.ternTooltip) remove(cm.state.ternTooltip);
+ var where = cm.cursorCoords();
+ var tip = cm.state.ternTooltip = makeTooltip(where.right + 1, where.bottom, content);
+ function maybeClear() {
+ old = true;
+ if (!mouseOnTip) clear();
+ }
+ function clear() {
+ cm.state.ternTooltip = null;
+ if (!tip.parentNode) return;
+ cm.off("cursorActivity", clear);
+ cm.off('blur', clear);
+ cm.off('scroll', clear);
+ fadeOut(tip);
+ }
+ var mouseOnTip = false, old = false;
+ CodeMirror.on(tip, "mousemove", function() { mouseOnTip = true; });
+ CodeMirror.on(tip, "mouseout", function(e) {
+ if (!CodeMirror.contains(tip, e.relatedTarget || e.toElement)) {
+ if (old) clear();
+ else mouseOnTip = false;
+ }
+ });
+ setTimeout(maybeClear, ts.options.hintDelay ? ts.options.hintDelay : 1700);
+ cm.on("cursorActivity", clear);
+ cm.on('blur', clear);
+ cm.on('scroll', clear);
+ }
+
+ function makeTooltip(x, y, content) {
+ var node = elt("div", cls + "tooltip", content);
+ node.style.left = x + "px";
+ node.style.top = y + "px";
+ document.body.appendChild(node);
+ return node;
+ }
+
+ function remove(node) {
+ var p = node && node.parentNode;
+ if (p) p.removeChild(node);
+ }
+
+ function fadeOut(tooltip) {
+ tooltip.style.opacity = "0";
+ setTimeout(function() { remove(tooltip); }, 1100);
+ }
+
+ function showError(ts, cm, msg) {
+ if (ts.options.showError)
+ ts.options.showError(cm, msg);
+ else
+ tempTooltip(cm, String(msg), ts);
+ }
+
+ function closeArgHints(ts) {
+ if (ts.activeArgHints) { remove(ts.activeArgHints); ts.activeArgHints = null; }
+ }
+
+ function docValue(ts, doc) {
+ var val = doc.doc.getValue();
+ if (ts.options.fileFilter) val = ts.options.fileFilter(val, doc.name, doc.doc);
+ return val;
+ }
+
+ // Worker wrapper
+
+ function WorkerServer(ts) {
+ var worker = ts.worker = new Worker(ts.options.workerScript);
+ worker.postMessage({type: "init",
+ defs: ts.options.defs,
+ plugins: ts.options.plugins,
+ scripts: ts.options.workerDeps});
+ var msgId = 0, pending = {};
+
+ function send(data, c) {
+ if (c) {
+ data.id = ++msgId;
+ pending[msgId] = c;
+ }
+ worker.postMessage(data);
+ }
+ worker.onmessage = function(e) {
+ var data = e.data;
+ if (data.type == "getFile") {
+ getFile(ts, data.name, function(err, text) {
+ send({type: "getFile", err: String(err), text: text, id: data.id});
+ });
+ } else if (data.type == "debug") {
+ window.console.log(data.message);
+ } else if (data.id && pending[data.id]) {
+ pending[data.id](data.err, data.body);
+ delete pending[data.id];
+ }
+ };
+ worker.onerror = function(e) {
+ for (var id in pending) pending[id](e);
+ pending = {};
+ };
+
+ this.addFile = function(name, text) { send({type: "add", name: name, text: text}); };
+ this.delFile = function(name) { send({type: "del", name: name}); };
+ this.request = function(body, c) { send({type: "req", body: body}, c); };
+ }
+});
diff --git a/devtools/client/sourceeditor/codemirror/codemirror.bundle.js b/devtools/client/sourceeditor/codemirror/codemirror.bundle.js
new file mode 100644
index 000000000..e01abc925
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/codemirror.bundle.js
@@ -0,0 +1,20152 @@
+var CodeMirror =
+/******/ (function(modules) { // webpackBootstrap
+/******/ // The module cache
+/******/ var installedModules = {};
+
+/******/ // The require function
+/******/ function __webpack_require__(moduleId) {
+
+/******/ // Check if module is in cache
+/******/ if(installedModules[moduleId])
+/******/ return installedModules[moduleId].exports;
+
+/******/ // Create a new module (and put it into the cache)
+/******/ var module = installedModules[moduleId] = {
+/******/ exports: {},
+/******/ id: moduleId,
+/******/ loaded: false
+/******/ };
+
+/******/ // Execute the module function
+/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
+
+/******/ // Flag the module as loaded
+/******/ module.loaded = true;
+
+/******/ // Return the exports of the module
+/******/ return module.exports;
+/******/ }
+
+
+/******/ // expose the modules object (__webpack_modules__)
+/******/ __webpack_require__.m = modules;
+
+/******/ // expose the module cache
+/******/ __webpack_require__.c = installedModules;
+
+/******/ // __webpack_public_path__
+/******/ __webpack_require__.p = "";
+
+/******/ // Load entry module and return exports
+/******/ return __webpack_require__(0);
+/******/ })
+/************************************************************************/
+/******/ ([
+/* 0 */
+/***/ function(module, exports, __webpack_require__) {
+
+ __webpack_require__(1);
+ __webpack_require__(3);
+ __webpack_require__(4);
+ __webpack_require__(5);
+ __webpack_require__(6);
+ __webpack_require__(7);
+ __webpack_require__(8);
+ __webpack_require__(9);
+ __webpack_require__(10);
+ __webpack_require__(11);
+ __webpack_require__(12);
+ __webpack_require__(13);
+ __webpack_require__(14);
+ __webpack_require__(15);
+ __webpack_require__(16);
+ __webpack_require__(17);
+ __webpack_require__(18);
+ __webpack_require__(19);
+ __webpack_require__(20);
+ __webpack_require__(21);
+ __webpack_require__(22);
+ __webpack_require__(23);
+ module.exports = __webpack_require__(2);
+
+
+/***/ },
+/* 1 */
+/***/ function(module, exports, __webpack_require__) {
+
+ // CodeMirror, copyright (c) by Marijn Haverbeke and others
+ // Distributed under an MIT license: http://codemirror.net/LICENSE
+
+ // Open simple dialogs on top of an editor. Relies on dialog.css.
+
+ (function(mod) {
+ if (true) // CommonJS
+ mod(__webpack_require__(2));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+ })(function(CodeMirror) {
+ function dialogDiv(cm, template, bottom) {
+ var wrap = cm.getWrapperElement();
+ var dialog;
+ dialog = wrap.appendChild(document.createElement("div"));
+ if (bottom)
+ dialog.className = "CodeMirror-dialog CodeMirror-dialog-bottom";
+ else
+ dialog.className = "CodeMirror-dialog CodeMirror-dialog-top";
+
+ if (typeof template == "string") {
+ dialog.innerHTML = template;
+ } else { // Assuming it's a detached DOM element.
+ dialog.appendChild(template);
+ }
+ return dialog;
+ }
+
+ function closeNotification(cm, newVal) {
+ if (cm.state.currentNotificationClose)
+ cm.state.currentNotificationClose();
+ cm.state.currentNotificationClose = newVal;
+ }
+
+ CodeMirror.defineExtension("openDialog", function(template, callback, options) {
+ if (!options) options = {};
+
+ closeNotification(this, null);
+
+ var dialog = dialogDiv(this, template, options.bottom);
+ var closed = false, me = this;
+ function close(newVal) {
+ if (typeof newVal == 'string') {
+ inp.value = newVal;
+ } else {
+ if (closed) return;
+ closed = true;
+ dialog.parentNode.removeChild(dialog);
+ me.focus();
+
+ if (options.onClose) options.onClose(dialog);
+ }
+ }
+
+ var inp = dialog.getElementsByTagName("input")[0], button;
+ if (inp) {
+ inp.focus();
+
+ if (options.value) {
+ inp.value = options.value;
+ if (options.selectValueOnOpen !== false) {
+ inp.select();
+ }
+ }
+
+ if (options.onInput)
+ CodeMirror.on(inp, "input", function(e) { options.onInput(e, inp.value, close);});
+ if (options.onKeyUp)
+ CodeMirror.on(inp, "keyup", function(e) {options.onKeyUp(e, inp.value, close);});
+
+ CodeMirror.on(inp, "keydown", function(e) {
+ if (options && options.onKeyDown && options.onKeyDown(e, inp.value, close)) { return; }
+ if (e.keyCode == 27 || (options.closeOnEnter !== false && e.keyCode == 13)) {
+ inp.blur();
+ CodeMirror.e_stop(e);
+ close();
+ }
+ if (e.keyCode == 13) callback(inp.value, e);
+ });
+
+ if (options.closeOnBlur !== false) CodeMirror.on(inp, "blur", close);
+ } else if (button = dialog.getElementsByTagName("button")[0]) {
+ CodeMirror.on(button, "click", function() {
+ close();
+ me.focus();
+ });
+
+ if (options.closeOnBlur !== false) CodeMirror.on(button, "blur", close);
+
+ button.focus();
+ }
+ return close;
+ });
+
+ CodeMirror.defineExtension("openConfirm", function(template, callbacks, options) {
+ closeNotification(this, null);
+ var dialog = dialogDiv(this, template, options && options.bottom);
+ var buttons = dialog.getElementsByTagName("button");
+ var closed = false, me = this, blurring = 1;
+ function close() {
+ if (closed) return;
+ closed = true;
+ dialog.parentNode.removeChild(dialog);
+ me.focus();
+ }
+ buttons[0].focus();
+ for (var i = 0; i < buttons.length; ++i) {
+ var b = buttons[i];
+ (function(callback) {
+ CodeMirror.on(b, "click", function(e) {
+ CodeMirror.e_preventDefault(e);
+ close();
+ if (callback) callback(me);
+ });
+ })(callbacks[i]);
+ CodeMirror.on(b, "blur", function() {
+ --blurring;
+ setTimeout(function() { if (blurring <= 0) close(); }, 200);
+ });
+ CodeMirror.on(b, "focus", function() { ++blurring; });
+ }
+ });
+
+ /*
+ * openNotification
+ * Opens a notification, that can be closed with an optional timer
+ * (default 5000ms timer) and always closes on click.
+ *
+ * If a notification is opened while another is opened, it will close the
+ * currently opened one and open the new one immediately.
+ */
+ CodeMirror.defineExtension("openNotification", function(template, options) {
+ closeNotification(this, close);
+ var dialog = dialogDiv(this, template, options && options.bottom);
+ var closed = false, doneTimer;
+ var duration = options && typeof options.duration !== "undefined" ? options.duration : 5000;
+
+ function close() {
+ if (closed) return;
+ closed = true;
+ clearTimeout(doneTimer);
+ dialog.parentNode.removeChild(dialog);
+ }
+
+ CodeMirror.on(dialog, 'click', function(e) {
+ CodeMirror.e_preventDefault(e);
+ close();
+ });
+
+ if (duration)
+ doneTimer = setTimeout(close, duration);
+
+ return close;
+ });
+ });
+
+
+/***/ },
+/* 2 */
+/***/ function(module, exports, __webpack_require__) {
+
+ // CodeMirror, copyright (c) by Marijn Haverbeke and others
+ // Distributed under an MIT license: http://codemirror.net/LICENSE
+
+ // This is CodeMirror (http://codemirror.net), a code editor
+ // implemented in JavaScript on top of the browser's DOM.
+ //
+ // You can find some technical background for some of the code below
+ // at http://marijnhaverbeke.nl/blog/#cm-internals .
+
+ (function(mod) {
+ if (true) // CommonJS
+ module.exports = mod();
+ else if (typeof define == "function" && define.amd) // AMD
+ return define([], mod);
+ else // Plain browser env
+ (this || window).CodeMirror = mod();
+ })(function() {
+ "use strict";
+
+ // BROWSER SNIFFING
+
+ // Kludges for bugs and behavior differences that can't be feature
+ // detected are enabled based on userAgent etc sniffing.
+ var userAgent = navigator.userAgent;
+ var platform = navigator.platform;
+
+ var gecko = /gecko\/\d/i.test(userAgent);
+ var ie_upto10 = /MSIE \d/.test(userAgent);
+ var ie_11up = /Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(userAgent);
+ var ie = ie_upto10 || ie_11up;
+ var ie_version = ie && (ie_upto10 ? document.documentMode || 6 : ie_11up[1]);
+ var webkit = /WebKit\//.test(userAgent);
+ var qtwebkit = webkit && /Qt\/\d+\.\d+/.test(userAgent);
+ var chrome = /Chrome\//.test(userAgent);
+ var presto = /Opera\//.test(userAgent);
+ var safari = /Apple Computer/.test(navigator.vendor);
+ var mac_geMountainLion = /Mac OS X 1\d\D([8-9]|\d\d)\D/.test(userAgent);
+ var phantom = /PhantomJS/.test(userAgent);
+
+ var ios = /AppleWebKit/.test(userAgent) && /Mobile\/\w+/.test(userAgent);
+ // This is woefully incomplete. Suggestions for alternative methods welcome.
+ var mobile = ios || /Android|webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(userAgent);
+ var mac = ios || /Mac/.test(platform);
+ var chromeOS = /\bCrOS\b/.test(userAgent);
+ var windows = /win/i.test(platform);
+
+ var presto_version = presto && userAgent.match(/Version\/(\d*\.\d*)/);
+ if (presto_version) presto_version = Number(presto_version[1]);
+ if (presto_version && presto_version >= 15) { presto = false; webkit = true; }
+ // Some browsers use the wrong event properties to signal cmd/ctrl on OS X
+ var flipCtrlCmd = mac && (qtwebkit || presto && (presto_version == null || presto_version < 12.11));
+ var captureRightClick = gecko || (ie && ie_version >= 9);
+
+ // Optimize some code when these features are not used.
+ var sawReadOnlySpans = false, sawCollapsedSpans = false;
+
+ // EDITOR CONSTRUCTOR
+
+ // A CodeMirror instance represents an editor. This is the object
+ // that user code is usually dealing with.
+
+ function CodeMirror(place, options) {
+ if (!(this instanceof CodeMirror)) return new CodeMirror(place, options);
+
+ this.options = options = options ? copyObj(options) : {};
+ // Determine effective options based on given values and defaults.
+ copyObj(defaults, options, false);
+ setGuttersForLineNumbers(options);
+
+ var doc = options.value;
+ if (typeof doc == "string") doc = new Doc(doc, options.mode, null, options.lineSeparator);
+ this.doc = doc;
+
+ var input = new CodeMirror.inputStyles[options.inputStyle](this);
+ var display = this.display = new Display(place, doc, input);
+ display.wrapper.CodeMirror = this;
+ updateGutters(this);
+ themeChanged(this);
+ if (options.lineWrapping)
+ this.display.wrapper.className += " CodeMirror-wrap";
+ if (options.autofocus && !mobile) display.input.focus();
+ initScrollbars(this);
+
+ this.state = {
+ keyMaps: [], // stores maps added by addKeyMap
+ overlays: [], // highlighting overlays, as added by addOverlay
+ modeGen: 0, // bumped when mode/overlay changes, used to invalidate highlighting info
+ overwrite: false,
+ delayingBlurEvent: false,
+ focused: false,
+ suppressEdits: false, // used to disable editing during key handlers when in readOnly mode
+ pasteIncoming: false, cutIncoming: false, // help recognize paste/cut edits in input.poll
+ selectingText: false,
+ draggingText: false,
+ highlight: new Delayed(), // stores highlight worker timeout
+ keySeq: null, // Unfinished key sequence
+ specialChars: null
+ };
+
+ var cm = this;
+
+ // Override magic textarea content restore that IE sometimes does
+ // on our hidden textarea on reload
+ if (ie && ie_version < 11) setTimeout(function() { cm.display.input.reset(true); }, 20);
+
+ registerEventHandlers(this);
+ ensureGlobalHandlers();
+
+ startOperation(this);
+ this.curOp.forceUpdate = true;
+ attachDoc(this, doc);
+
+ if ((options.autofocus && !mobile) || cm.hasFocus())
+ setTimeout(bind(onFocus, this), 20);
+ else
+ onBlur(this);
+
+ for (var opt in optionHandlers) if (optionHandlers.hasOwnProperty(opt))
+ optionHandlers[opt](this, options[opt], Init);
+ maybeUpdateLineNumberWidth(this);
+ if (options.finishInit) options.finishInit(this);
+ for (var i = 0; i < initHooks.length; ++i) initHooks[i](this);
+ endOperation(this);
+ // Suppress optimizelegibility in Webkit, since it breaks text
+ // measuring on line wrapping boundaries.
+ if (webkit && options.lineWrapping &&
+ getComputedStyle(display.lineDiv).textRendering == "optimizelegibility")
+ display.lineDiv.style.textRendering = "auto";
+ }
+
+ // DISPLAY CONSTRUCTOR
+
+ // The display handles the DOM integration, both for input reading
+ // and content drawing. It holds references to DOM nodes and
+ // display-related state.
+
+ function Display(place, doc, input) {
+ var d = this;
+ this.input = input;
+
+ // Covers bottom-right square when both scrollbars are present.
+ d.scrollbarFiller = elt("div", null, "CodeMirror-scrollbar-filler");
+ d.scrollbarFiller.setAttribute("cm-not-content", "true");
+ // Covers bottom of gutter when coverGutterNextToScrollbar is on
+ // and h scrollbar is present.
+ d.gutterFiller = elt("div", null, "CodeMirror-gutter-filler");
+ d.gutterFiller.setAttribute("cm-not-content", "true");
+ // Will contain the actual code, positioned to cover the viewport.
+ d.lineDiv = elt("div", null, "CodeMirror-code");
+ // Elements are added to these to represent selection and cursors.
+ d.selectionDiv = elt("div", null, null, "position: relative; z-index: 1");
+ d.cursorDiv = elt("div", null, "CodeMirror-cursors");
+ // A visibility: hidden element used to find the size of things.
+ d.measure = elt("div", null, "CodeMirror-measure");
+ // When lines outside of the viewport are measured, they are drawn in this.
+ d.lineMeasure = elt("div", null, "CodeMirror-measure");
+ // Wraps everything that needs to exist inside the vertically-padded coordinate system
+ d.lineSpace = elt("div", [d.measure, d.lineMeasure, d.selectionDiv, d.cursorDiv, d.lineDiv],
+ null, "position: relative; outline: none");
+ // Moved around its parent to cover visible view.
+ d.mover = elt("div", [elt("div", [d.lineSpace], "CodeMirror-lines")], null, "position: relative");
+ // Set to the height of the document, allowing scrolling.
+ d.sizer = elt("div", [d.mover], "CodeMirror-sizer");
+ d.sizerWidth = null;
+ // Behavior of elts with overflow: auto and padding is
+ // inconsistent across browsers. This is used to ensure the
+ // scrollable area is big enough.
+ d.heightForcer = elt("div", null, null, "position: absolute; height: " + scrollerGap + "px; width: 1px;");
+ // Will contain the gutters, if any.
+ d.gutters = elt("div", null, "CodeMirror-gutters");
+ d.lineGutter = null;
+ // Actual scrollable element.
+ d.scroller = elt("div", [d.sizer, d.heightForcer, d.gutters], "CodeMirror-scroll");
+ d.scroller.setAttribute("tabIndex", "-1");
+ // The element in which the editor lives.
+ d.wrapper = elt("div", [d.scrollbarFiller, d.gutterFiller, d.scroller], "CodeMirror");
+
+ // Work around IE7 z-index bug (not perfect, hence IE7 not really being supported)
+ if (ie && ie_version < 8) { d.gutters.style.zIndex = -1; d.scroller.style.paddingRight = 0; }
+ if (!webkit && !(gecko && mobile)) d.scroller.draggable = true;
+
+ if (place) {
+ if (place.appendChild) place.appendChild(d.wrapper);
+ else place(d.wrapper);
+ }
+
+ // Current rendered range (may be bigger than the view window).
+ d.viewFrom = d.viewTo = doc.first;
+ d.reportedViewFrom = d.reportedViewTo = doc.first;
+ // Information about the rendered lines.
+ d.view = [];
+ d.renderedView = null;
+ // Holds info about a single rendered line when it was rendered
+ // for measurement, while not in view.
+ d.externalMeasured = null;
+ // Empty space (in pixels) above the view
+ d.viewOffset = 0;
+ d.lastWrapHeight = d.lastWrapWidth = 0;
+ d.updateLineNumbers = null;
+
+ d.nativeBarWidth = d.barHeight = d.barWidth = 0;
+ d.scrollbarsClipped = false;
+
+ // Used to only resize the line number gutter when necessary (when
+ // the amount of lines crosses a boundary that makes its width change)
+ d.lineNumWidth = d.lineNumInnerWidth = d.lineNumChars = null;
+ // Set to true when a non-horizontal-scrolling line widget is
+ // added. As an optimization, line widget aligning is skipped when
+ // this is false.
+ d.alignWidgets = false;
+
+ d.cachedCharWidth = d.cachedTextHeight = d.cachedPaddingH = null;
+
+ // Tracks the maximum line length so that the horizontal scrollbar
+ // can be kept static when scrolling.
+ d.maxLine = null;
+ d.maxLineLength = 0;
+ d.maxLineChanged = false;
+
+ // Used for measuring wheel scrolling granularity
+ d.wheelDX = d.wheelDY = d.wheelStartX = d.wheelStartY = null;
+
+ // True when shift is held down.
+ d.shift = false;
+
+ // Used to track whether anything happened since the context menu
+ // was opened.
+ d.selForContextMenu = null;
+
+ d.activeTouch = null;
+
+ input.init(d);
+ }
+
+ // STATE UPDATES
+
+ // Used to get the editor into a consistent state again when options change.
+
+ function loadMode(cm) {
+ cm.doc.mode = CodeMirror.getMode(cm.options, cm.doc.modeOption);
+ resetModeState(cm);
+ }
+
+ function resetModeState(cm) {
+ cm.doc.iter(function(line) {
+ if (line.stateAfter) line.stateAfter = null;
+ if (line.styles) line.styles = null;
+ });
+ cm.doc.frontier = cm.doc.first;
+ startWorker(cm, 100);
+ cm.state.modeGen++;
+ if (cm.curOp) regChange(cm);
+ }
+
+ function wrappingChanged(cm) {
+ if (cm.options.lineWrapping) {
+ addClass(cm.display.wrapper, "CodeMirror-wrap");
+ cm.display.sizer.style.minWidth = "";
+ cm.display.sizerWidth = null;
+ } else {
+ rmClass(cm.display.wrapper, "CodeMirror-wrap");
+ findMaxLine(cm);
+ }
+ estimateLineHeights(cm);
+ regChange(cm);
+ clearCaches(cm);
+ setTimeout(function(){updateScrollbars(cm);}, 100);
+ }
+
+ // Returns a function that estimates the height of a line, to use as
+ // first approximation until the line becomes visible (and is thus
+ // properly measurable).
+ function estimateHeight(cm) {
+ var th = textHeight(cm.display), wrapping = cm.options.lineWrapping;
+ var perLine = wrapping && Math.max(5, cm.display.scroller.clientWidth / charWidth(cm.display) - 3);
+ return function(line) {
+ if (lineIsHidden(cm.doc, line)) return 0;
+
+ var widgetsHeight = 0;
+ if (line.widgets) for (var i = 0; i < line.widgets.length; i++) {
+ if (line.widgets[i].height) widgetsHeight += line.widgets[i].height;
+ }
+
+ if (wrapping)
+ return widgetsHeight + (Math.ceil(line.text.length / perLine) || 1) * th;
+ else
+ return widgetsHeight + th;
+ };
+ }
+
+ function estimateLineHeights(cm) {
+ var doc = cm.doc, est = estimateHeight(cm);
+ doc.iter(function(line) {
+ var estHeight = est(line);
+ if (estHeight != line.height) updateLineHeight(line, estHeight);
+ });
+ }
+
+ function themeChanged(cm) {
+ cm.display.wrapper.className = cm.display.wrapper.className.replace(/\s*cm-s-\S+/g, "") +
+ cm.options.theme.replace(/(^|\s)\s*/g, " cm-s-");
+ clearCaches(cm);
+ }
+
+ function guttersChanged(cm) {
+ updateGutters(cm);
+ regChange(cm);
+ setTimeout(function(){alignHorizontally(cm);}, 20);
+ }
+
+ // Rebuild the gutter elements, ensure the margin to the left of the
+ // code matches their width.
+ function updateGutters(cm) {
+ var gutters = cm.display.gutters, specs = cm.options.gutters;
+ removeChildren(gutters);
+ for (var i = 0; i < specs.length; ++i) {
+ var gutterClass = specs[i];
+ var gElt = gutters.appendChild(elt("div", null, "CodeMirror-gutter " + gutterClass));
+ if (gutterClass == "CodeMirror-linenumbers") {
+ cm.display.lineGutter = gElt;
+ gElt.style.width = (cm.display.lineNumWidth || 1) + "px";
+ }
+ }
+ gutters.style.display = i ? "" : "none";
+ updateGutterSpace(cm);
+ }
+
+ function updateGutterSpace(cm) {
+ var width = cm.display.gutters.offsetWidth;
+ cm.display.sizer.style.marginLeft = width + "px";
+ }
+
+ // Compute the character length of a line, taking into account
+ // collapsed ranges (see markText) that might hide parts, and join
+ // other lines onto it.
+ function lineLength(line) {
+ if (line.height == 0) return 0;
+ var len = line.text.length, merged, cur = line;
+ while (merged = collapsedSpanAtStart(cur)) {
+ var found = merged.find(0, true);
+ cur = found.from.line;
+ len += found.from.ch - found.to.ch;
+ }
+ cur = line;
+ while (merged = collapsedSpanAtEnd(cur)) {
+ var found = merged.find(0, true);
+ len -= cur.text.length - found.from.ch;
+ cur = found.to.line;
+ len += cur.text.length - found.to.ch;
+ }
+ return len;
+ }
+
+ // Find the longest line in the document.
+ function findMaxLine(cm) {
+ var d = cm.display, doc = cm.doc;
+ d.maxLine = getLine(doc, doc.first);
+ d.maxLineLength = lineLength(d.maxLine);
+ d.maxLineChanged = true;
+ doc.iter(function(line) {
+ var len = lineLength(line);
+ if (len > d.maxLineLength) {
+ d.maxLineLength = len;
+ d.maxLine = line;
+ }
+ });
+ }
+
+ // Make sure the gutters options contains the element
+ // "CodeMirror-linenumbers" when the lineNumbers option is true.
+ function setGuttersForLineNumbers(options) {
+ var found = indexOf(options.gutters, "CodeMirror-linenumbers");
+ if (found == -1 && options.lineNumbers) {
+ options.gutters = options.gutters.concat(["CodeMirror-linenumbers"]);
+ } else if (found > -1 && !options.lineNumbers) {
+ options.gutters = options.gutters.slice(0);
+ options.gutters.splice(found, 1);
+ }
+ }
+
+ // SCROLLBARS
+
+ // Prepare DOM reads needed to update the scrollbars. Done in one
+ // shot to minimize update/measure roundtrips.
+ function measureForScrollbars(cm) {
+ var d = cm.display, gutterW = d.gutters.offsetWidth;
+ var docH = Math.round(cm.doc.height + paddingVert(cm.display));
+ return {
+ clientHeight: d.scroller.clientHeight,
+ viewHeight: d.wrapper.clientHeight,
+ scrollWidth: d.scroller.scrollWidth, clientWidth: d.scroller.clientWidth,
+ viewWidth: d.wrapper.clientWidth,
+ barLeft: cm.options.fixedGutter ? gutterW : 0,
+ docHeight: docH,
+ scrollHeight: docH + scrollGap(cm) + d.barHeight,
+ nativeBarWidth: d.nativeBarWidth,
+ gutterWidth: gutterW
+ };
+ }
+
+ function NativeScrollbars(place, scroll, cm) {
+ this.cm = cm;
+ var vert = this.vert = elt("div", [elt("div", null, null, "min-width: 1px")], "CodeMirror-vscrollbar");
+ var horiz = this.horiz = elt("div", [elt("div", null, null, "height: 100%; min-height: 1px")], "CodeMirror-hscrollbar");
+ place(vert); place(horiz);
+
+ on(vert, "scroll", function() {
+ if (vert.clientHeight) scroll(vert.scrollTop, "vertical");
+ });
+ on(horiz, "scroll", function() {
+ if (horiz.clientWidth) scroll(horiz.scrollLeft, "horizontal");
+ });
+
+ this.checkedZeroWidth = false;
+ // Need to set a minimum width to see the scrollbar on IE7 (but must not set it on IE8).
+ if (ie && ie_version < 8) this.horiz.style.minHeight = this.vert.style.minWidth = "18px";
+ }
+
+ NativeScrollbars.prototype = copyObj({
+ update: function(measure) {
+ var needsH = measure.scrollWidth > measure.clientWidth + 1;
+ var needsV = measure.scrollHeight > measure.clientHeight + 1;
+ var sWidth = measure.nativeBarWidth;
+
+ if (needsV) {
+ this.vert.style.display = "block";
+ this.vert.style.bottom = needsH ? sWidth + "px" : "0";
+ var totalHeight = measure.viewHeight - (needsH ? sWidth : 0);
+ // A bug in IE8 can cause this value to be negative, so guard it.
+ this.vert.firstChild.style.height =
+ Math.max(0, measure.scrollHeight - measure.clientHeight + totalHeight) + "px";
+ } else {
+ this.vert.style.display = "";
+ this.vert.firstChild.style.height = "0";
+ }
+
+ if (needsH) {
+ this.horiz.style.display = "block";
+ this.horiz.style.right = needsV ? sWidth + "px" : "0";
+ this.horiz.style.left = measure.barLeft + "px";
+ var totalWidth = measure.viewWidth - measure.barLeft - (needsV ? sWidth : 0);
+ this.horiz.firstChild.style.width =
+ (measure.scrollWidth - measure.clientWidth + totalWidth) + "px";
+ } else {
+ this.horiz.style.display = "";
+ this.horiz.firstChild.style.width = "0";
+ }
+
+ if (!this.checkedZeroWidth && measure.clientHeight > 0) {
+ if (sWidth == 0) this.zeroWidthHack();
+ this.checkedZeroWidth = true;
+ }
+
+ return {right: needsV ? sWidth : 0, bottom: needsH ? sWidth : 0};
+ },
+ setScrollLeft: function(pos) {
+ if (this.horiz.scrollLeft != pos) this.horiz.scrollLeft = pos;
+ if (this.disableHoriz) this.enableZeroWidthBar(this.horiz, this.disableHoriz);
+ },
+ setScrollTop: function(pos) {
+ if (this.vert.scrollTop != pos) this.vert.scrollTop = pos;
+ if (this.disableVert) this.enableZeroWidthBar(this.vert, this.disableVert);
+ },
+ zeroWidthHack: function() {
+ var w = mac && !mac_geMountainLion ? "12px" : "18px";
+ this.horiz.style.height = this.vert.style.width = w;
+ this.horiz.style.pointerEvents = this.vert.style.pointerEvents = "none";
+ this.disableHoriz = new Delayed;
+ this.disableVert = new Delayed;
+ },
+ enableZeroWidthBar: function(bar, delay) {
+ bar.style.pointerEvents = "auto";
+ function maybeDisable() {
+ // To find out whether the scrollbar is still visible, we
+ // check whether the element under the pixel in the bottom
+ // left corner of the scrollbar box is the scrollbar box
+ // itself (when the bar is still visible) or its filler child
+ // (when the bar is hidden). If it is still visible, we keep
+ // it enabled, if it's hidden, we disable pointer events.
+ var box = bar.getBoundingClientRect();
+ var elt = document.elementFromPoint(box.left + 1, box.bottom - 1);
+ if (elt != bar) bar.style.pointerEvents = "none";
+ else delay.set(1000, maybeDisable);
+ }
+ delay.set(1000, maybeDisable);
+ },
+ clear: function() {
+ var parent = this.horiz.parentNode;
+ parent.removeChild(this.horiz);
+ parent.removeChild(this.vert);
+ }
+ }, NativeScrollbars.prototype);
+
+ function NullScrollbars() {}
+
+ NullScrollbars.prototype = copyObj({
+ update: function() { return {bottom: 0, right: 0}; },
+ setScrollLeft: function() {},
+ setScrollTop: function() {},
+ clear: function() {}
+ }, NullScrollbars.prototype);
+
+ CodeMirror.scrollbarModel = {"native": NativeScrollbars, "null": NullScrollbars};
+
+ function initScrollbars(cm) {
+ if (cm.display.scrollbars) {
+ cm.display.scrollbars.clear();
+ if (cm.display.scrollbars.addClass)
+ rmClass(cm.display.wrapper, cm.display.scrollbars.addClass);
+ }
+
+ cm.display.scrollbars = new CodeMirror.scrollbarModel[cm.options.scrollbarStyle](function(node) {
+ cm.display.wrapper.insertBefore(node, cm.display.scrollbarFiller);
+ // Prevent clicks in the scrollbars from killing focus
+ on(node, "mousedown", function() {
+ if (cm.state.focused) setTimeout(function() { cm.display.input.focus(); }, 0);
+ });
+ node.setAttribute("cm-not-content", "true");
+ }, function(pos, axis) {
+ if (axis == "horizontal") setScrollLeft(cm, pos);
+ else setScrollTop(cm, pos);
+ }, cm);
+ if (cm.display.scrollbars.addClass)
+ addClass(cm.display.wrapper, cm.display.scrollbars.addClass);
+ }
+
+ function updateScrollbars(cm, measure) {
+ if (!measure) measure = measureForScrollbars(cm);
+ var startWidth = cm.display.barWidth, startHeight = cm.display.barHeight;
+ updateScrollbarsInner(cm, measure);
+ for (var i = 0; i < 4 && startWidth != cm.display.barWidth || startHeight != cm.display.barHeight; i++) {
+ if (startWidth != cm.display.barWidth && cm.options.lineWrapping)
+ updateHeightsInViewport(cm);
+ updateScrollbarsInner(cm, measureForScrollbars(cm));
+ startWidth = cm.display.barWidth; startHeight = cm.display.barHeight;
+ }
+ }
+
+ // Re-synchronize the fake scrollbars with the actual size of the
+ // content.
+ function updateScrollbarsInner(cm, measure) {
+ var d = cm.display;
+ var sizes = d.scrollbars.update(measure);
+
+ d.sizer.style.paddingRight = (d.barWidth = sizes.right) + "px";
+ d.sizer.style.paddingBottom = (d.barHeight = sizes.bottom) + "px";
+ d.heightForcer.style.borderBottom = sizes.bottom + "px solid transparent"
+
+ if (sizes.right && sizes.bottom) {
+ d.scrollbarFiller.style.display = "block";
+ d.scrollbarFiller.style.height = sizes.bottom + "px";
+ d.scrollbarFiller.style.width = sizes.right + "px";
+ } else d.scrollbarFiller.style.display = "";
+ if (sizes.bottom && cm.options.coverGutterNextToScrollbar && cm.options.fixedGutter) {
+ d.gutterFiller.style.display = "block";
+ d.gutterFiller.style.height = sizes.bottom + "px";
+ d.gutterFiller.style.width = measure.gutterWidth + "px";
+ } else d.gutterFiller.style.display = "";
+ }
+
+ // Compute the lines that are visible in a given viewport (defaults
+ // the the current scroll position). viewport may contain top,
+ // height, and ensure (see op.scrollToPos) properties.
+ function visibleLines(display, doc, viewport) {
+ var top = viewport && viewport.top != null ? Math.max(0, viewport.top) : display.scroller.scrollTop;
+ top = Math.floor(top - paddingTop(display));
+ var bottom = viewport && viewport.bottom != null ? viewport.bottom : top + display.wrapper.clientHeight;
+
+ var from = lineAtHeight(doc, top), to = lineAtHeight(doc, bottom);
+ // Ensure is a {from: {line, ch}, to: {line, ch}} object, and
+ // forces those lines into the viewport (if possible).
+ if (viewport && viewport.ensure) {
+ var ensureFrom = viewport.ensure.from.line, ensureTo = viewport.ensure.to.line;
+ if (ensureFrom < from) {
+ from = ensureFrom;
+ to = lineAtHeight(doc, heightAtLine(getLine(doc, ensureFrom)) + display.wrapper.clientHeight);
+ } else if (Math.min(ensureTo, doc.lastLine()) >= to) {
+ from = lineAtHeight(doc, heightAtLine(getLine(doc, ensureTo)) - display.wrapper.clientHeight);
+ to = ensureTo;
+ }
+ }
+ return {from: from, to: Math.max(to, from + 1)};
+ }
+
+ // LINE NUMBERS
+
+ // Re-align line numbers and gutter marks to compensate for
+ // horizontal scrolling.
+ function alignHorizontally(cm) {
+ var display = cm.display, view = display.view;
+ if (!display.alignWidgets && (!display.gutters.firstChild || !cm.options.fixedGutter)) return;
+ var comp = compensateForHScroll(display) - display.scroller.scrollLeft + cm.doc.scrollLeft;
+ var gutterW = display.gutters.offsetWidth, left = comp + "px";
+ for (var i = 0; i < view.length; i++) if (!view[i].hidden) {
+ if (cm.options.fixedGutter && view[i].gutter)
+ view[i].gutter.style.left = left;
+ var align = view[i].alignable;
+ if (align) for (var j = 0; j < align.length; j++)
+ align[j].style.left = left;
+ }
+ if (cm.options.fixedGutter)
+ display.gutters.style.left = (comp + gutterW) + "px";
+ }
+
+ // Used to ensure that the line number gutter is still the right
+ // size for the current document size. Returns true when an update
+ // is needed.
+ function maybeUpdateLineNumberWidth(cm) {
+ if (!cm.options.lineNumbers) return false;
+ var doc = cm.doc, last = lineNumberFor(cm.options, doc.first + doc.size - 1), display = cm.display;
+ if (last.length != display.lineNumChars) {
+ var test = display.measure.appendChild(elt("div", [elt("div", last)],
+ "CodeMirror-linenumber CodeMirror-gutter-elt"));
+ var innerW = test.firstChild.offsetWidth, padding = test.offsetWidth - innerW;
+ display.lineGutter.style.width = "";
+ display.lineNumInnerWidth = Math.max(innerW, display.lineGutter.offsetWidth - padding) + 1;
+ display.lineNumWidth = display.lineNumInnerWidth + padding;
+ display.lineNumChars = display.lineNumInnerWidth ? last.length : -1;
+ display.lineGutter.style.width = display.lineNumWidth + "px";
+ updateGutterSpace(cm);
+ return true;
+ }
+ return false;
+ }
+
+ function lineNumberFor(options, i) {
+ return String(options.lineNumberFormatter(i + options.firstLineNumber));
+ }
+
+ // Computes display.scroller.scrollLeft + display.gutters.offsetWidth,
+ // but using getBoundingClientRect to get a sub-pixel-accurate
+ // result.
+ function compensateForHScroll(display) {
+ return display.scroller.getBoundingClientRect().left - display.sizer.getBoundingClientRect().left;
+ }
+
+ // DISPLAY DRAWING
+
+ function DisplayUpdate(cm, viewport, force) {
+ var display = cm.display;
+
+ this.viewport = viewport;
+ // Store some values that we'll need later (but don't want to force a relayout for)
+ this.visible = visibleLines(display, cm.doc, viewport);
+ this.editorIsHidden = !display.wrapper.offsetWidth;
+ this.wrapperHeight = display.wrapper.clientHeight;
+ this.wrapperWidth = display.wrapper.clientWidth;
+ this.oldDisplayWidth = displayWidth(cm);
+ this.force = force;
+ this.dims = getDimensions(cm);
+ this.events = [];
+ }
+
+ DisplayUpdate.prototype.signal = function(emitter, type) {
+ if (hasHandler(emitter, type))
+ this.events.push(arguments);
+ };
+ DisplayUpdate.prototype.finish = function() {
+ for (var i = 0; i < this.events.length; i++)
+ signal.apply(null, this.events[i]);
+ };
+
+ function maybeClipScrollbars(cm) {
+ var display = cm.display;
+ if (!display.scrollbarsClipped && display.scroller.offsetWidth) {
+ display.nativeBarWidth = display.scroller.offsetWidth - display.scroller.clientWidth;
+ display.heightForcer.style.height = scrollGap(cm) + "px";
+ display.sizer.style.marginBottom = -display.nativeBarWidth + "px";
+ display.sizer.style.borderRightWidth = scrollGap(cm) + "px";
+ display.scrollbarsClipped = true;
+ }
+ }
+
+ // Does the actual updating of the line display. Bails out
+ // (returning false) when there is nothing to be done and forced is
+ // false.
+ function updateDisplayIfNeeded(cm, update) {
+ var display = cm.display, doc = cm.doc;
+
+ if (update.editorIsHidden) {
+ resetView(cm);
+ return false;
+ }
+
+ // Bail out if the visible area is already rendered and nothing changed.
+ if (!update.force &&
+ update.visible.from >= display.viewFrom && update.visible.to <= display.viewTo &&
+ (display.updateLineNumbers == null || display.updateLineNumbers >= display.viewTo) &&
+ display.renderedView == display.view && countDirtyView(cm) == 0)
+ return false;
+
+ if (maybeUpdateLineNumberWidth(cm)) {
+ resetView(cm);
+ update.dims = getDimensions(cm);
+ }
+
+ // Compute a suitable new viewport (from & to)
+ var end = doc.first + doc.size;
+ var from = Math.max(update.visible.from - cm.options.viewportMargin, doc.first);
+ var to = Math.min(end, update.visible.to + cm.options.viewportMargin);
+ if (display.viewFrom < from && from - display.viewFrom < 20) from = Math.max(doc.first, display.viewFrom);
+ if (display.viewTo > to && display.viewTo - to < 20) to = Math.min(end, display.viewTo);
+ if (sawCollapsedSpans) {
+ from = visualLineNo(cm.doc, from);
+ to = visualLineEndNo(cm.doc, to);
+ }
+
+ var different = from != display.viewFrom || to != display.viewTo ||
+ display.lastWrapHeight != update.wrapperHeight || display.lastWrapWidth != update.wrapperWidth;
+ adjustView(cm, from, to);
+
+ display.viewOffset = heightAtLine(getLine(cm.doc, display.viewFrom));
+ // Position the mover div to align with the current scroll position
+ cm.display.mover.style.top = display.viewOffset + "px";
+
+ var toUpdate = countDirtyView(cm);
+ if (!different && toUpdate == 0 && !update.force && display.renderedView == display.view &&
+ (display.updateLineNumbers == null || display.updateLineNumbers >= display.viewTo))
+ return false;
+
+ // For big changes, we hide the enclosing element during the
+ // update, since that speeds up the operations on most browsers.
+ var focused = activeElt();
+ if (toUpdate > 4) display.lineDiv.style.display = "none";
+ patchDisplay(cm, display.updateLineNumbers, update.dims);
+ if (toUpdate > 4) display.lineDiv.style.display = "";
+ display.renderedView = display.view;
+ // There might have been a widget with a focused element that got
+ // hidden or updated, if so re-focus it.
+ if (focused && activeElt() != focused && focused.offsetHeight) focused.focus();
+
+ // Prevent selection and cursors from interfering with the scroll
+ // width and height.
+ removeChildren(display.cursorDiv);
+ removeChildren(display.selectionDiv);
+ display.gutters.style.height = display.sizer.style.minHeight = 0;
+
+ if (different) {
+ display.lastWrapHeight = update.wrapperHeight;
+ display.lastWrapWidth = update.wrapperWidth;
+ startWorker(cm, 400);
+ }
+
+ display.updateLineNumbers = null;
+
+ return true;
+ }
+
+ function postUpdateDisplay(cm, update) {
+ var viewport = update.viewport;
+
+ for (var first = true;; first = false) {
+ if (!first || !cm.options.lineWrapping || update.oldDisplayWidth == displayWidth(cm)) {
+ // Clip forced viewport to actual scrollable area.
+ if (viewport && viewport.top != null)
+ viewport = {top: Math.min(cm.doc.height + paddingVert(cm.display) - displayHeight(cm), viewport.top)};
+ // Updated line heights might result in the drawn area not
+ // actually covering the viewport. Keep looping until it does.
+ update.visible = visibleLines(cm.display, cm.doc, viewport);
+ if (update.visible.from >= cm.display.viewFrom && update.visible.to <= cm.display.viewTo)
+ break;
+ }
+ if (!updateDisplayIfNeeded(cm, update)) break;
+ updateHeightsInViewport(cm);
+ var barMeasure = measureForScrollbars(cm);
+ updateSelection(cm);
+ updateScrollbars(cm, barMeasure);
+ setDocumentHeight(cm, barMeasure);
+ }
+
+ update.signal(cm, "update", cm);
+ if (cm.display.viewFrom != cm.display.reportedViewFrom || cm.display.viewTo != cm.display.reportedViewTo) {
+ update.signal(cm, "viewportChange", cm, cm.display.viewFrom, cm.display.viewTo);
+ cm.display.reportedViewFrom = cm.display.viewFrom; cm.display.reportedViewTo = cm.display.viewTo;
+ }
+ }
+
+ function updateDisplaySimple(cm, viewport) {
+ var update = new DisplayUpdate(cm, viewport);
+ if (updateDisplayIfNeeded(cm, update)) {
+ updateHeightsInViewport(cm);
+ postUpdateDisplay(cm, update);
+ var barMeasure = measureForScrollbars(cm);
+ updateSelection(cm);
+ updateScrollbars(cm, barMeasure);
+ setDocumentHeight(cm, barMeasure);
+ update.finish();
+ }
+ }
+
+ function setDocumentHeight(cm, measure) {
+ cm.display.sizer.style.minHeight = measure.docHeight + "px";
+ cm.display.heightForcer.style.top = measure.docHeight + "px";
+ cm.display.gutters.style.height = (measure.docHeight + cm.display.barHeight + scrollGap(cm)) + "px";
+ }
+
+ // Read the actual heights of the rendered lines, and update their
+ // stored heights to match.
+ function updateHeightsInViewport(cm) {
+ var display = cm.display;
+ var prevBottom = display.lineDiv.offsetTop;
+ for (var i = 0; i < display.view.length; i++) {
+ var cur = display.view[i], height;
+ if (cur.hidden) continue;
+ if (ie && ie_version < 8) {
+ var bot = cur.node.offsetTop + cur.node.offsetHeight;
+ height = bot - prevBottom;
+ prevBottom = bot;
+ } else {
+ var box = cur.node.getBoundingClientRect();
+ height = box.bottom - box.top;
+ }
+ var diff = cur.line.height - height;
+ if (height < 2) height = textHeight(display);
+ if (diff > .001 || diff < -.001) {
+ updateLineHeight(cur.line, height);
+ updateWidgetHeight(cur.line);
+ if (cur.rest) for (var j = 0; j < cur.rest.length; j++)
+ updateWidgetHeight(cur.rest[j]);
+ }
+ }
+ }
+
+ // Read and store the height of line widgets associated with the
+ // given line.
+ function updateWidgetHeight(line) {
+ if (line.widgets) for (var i = 0; i < line.widgets.length; ++i)
+ line.widgets[i].height = line.widgets[i].node.parentNode.offsetHeight;
+ }
+
+ // Do a bulk-read of the DOM positions and sizes needed to draw the
+ // view, so that we don't interleave reading and writing to the DOM.
+ function getDimensions(cm) {
+ var d = cm.display, left = {}, width = {};
+ var gutterLeft = d.gutters.clientLeft;
+ for (var n = d.gutters.firstChild, i = 0; n; n = n.nextSibling, ++i) {
+ left[cm.options.gutters[i]] = n.offsetLeft + n.clientLeft + gutterLeft;
+ width[cm.options.gutters[i]] = n.clientWidth;
+ }
+ return {fixedPos: compensateForHScroll(d),
+ gutterTotalWidth: d.gutters.offsetWidth,
+ gutterLeft: left,
+ gutterWidth: width,
+ wrapperWidth: d.wrapper.clientWidth};
+ }
+
+ // Sync the actual display DOM structure with display.view, removing
+ // nodes for lines that are no longer in view, and creating the ones
+ // that are not there yet, and updating the ones that are out of
+ // date.
+ function patchDisplay(cm, updateNumbersFrom, dims) {
+ var display = cm.display, lineNumbers = cm.options.lineNumbers;
+ var container = display.lineDiv, cur = container.firstChild;
+
+ function rm(node) {
+ var next = node.nextSibling;
+ // Works around a throw-scroll bug in OS X Webkit
+ if (webkit && mac && cm.display.currentWheelTarget == node)
+ node.style.display = "none";
+ else
+ node.parentNode.removeChild(node);
+ return next;
+ }
+
+ var view = display.view, lineN = display.viewFrom;
+ // Loop over the elements in the view, syncing cur (the DOM nodes
+ // in display.lineDiv) with the view as we go.
+ for (var i = 0; i < view.length; i++) {
+ var lineView = view[i];
+ if (lineView.hidden) {
+ } else if (!lineView.node || lineView.node.parentNode != container) { // Not drawn yet
+ var node = buildLineElement(cm, lineView, lineN, dims);
+ container.insertBefore(node, cur);
+ } else { // Already drawn
+ while (cur != lineView.node) cur = rm(cur);
+ var updateNumber = lineNumbers && updateNumbersFrom != null &&
+ updateNumbersFrom <= lineN && lineView.lineNumber;
+ if (lineView.changes) {
+ if (indexOf(lineView.changes, "gutter") > -1) updateNumber = false;
+ updateLineForChanges(cm, lineView, lineN, dims);
+ }
+ if (updateNumber) {
+ removeChildren(lineView.lineNumber);
+ lineView.lineNumber.appendChild(document.createTextNode(lineNumberFor(cm.options, lineN)));
+ }
+ cur = lineView.node.nextSibling;
+ }
+ lineN += lineView.size;
+ }
+ while (cur) cur = rm(cur);
+ }
+
+ // When an aspect of a line changes, a string is added to
+ // lineView.changes. This updates the relevant part of the line's
+ // DOM structure.
+ function updateLineForChanges(cm, lineView, lineN, dims) {
+ for (var j = 0; j < lineView.changes.length; j++) {
+ var type = lineView.changes[j];
+ if (type == "text") updateLineText(cm, lineView);
+ else if (type == "gutter") updateLineGutter(cm, lineView, lineN, dims);
+ else if (type == "class") updateLineClasses(lineView);
+ else if (type == "widget") updateLineWidgets(cm, lineView, dims);
+ }
+ lineView.changes = null;
+ }
+
+ // Lines with gutter elements, widgets or a background class need to
+ // be wrapped, and have the extra elements added to the wrapper div
+ function ensureLineWrapped(lineView) {
+ if (lineView.node == lineView.text) {
+ lineView.node = elt("div", null, null, "position: relative");
+ if (lineView.text.parentNode)
+ lineView.text.parentNode.replaceChild(lineView.node, lineView.text);
+ lineView.node.appendChild(lineView.text);
+ if (ie && ie_version < 8) lineView.node.style.zIndex = 2;
+ }
+ return lineView.node;
+ }
+
+ function updateLineBackground(lineView) {
+ var cls = lineView.bgClass ? lineView.bgClass + " " + (lineView.line.bgClass || "") : lineView.line.bgClass;
+ if (cls) cls += " CodeMirror-linebackground";
+ if (lineView.background) {
+ if (cls) lineView.background.className = cls;
+ else { lineView.background.parentNode.removeChild(lineView.background); lineView.background = null; }
+ } else if (cls) {
+ var wrap = ensureLineWrapped(lineView);
+ lineView.background = wrap.insertBefore(elt("div", null, cls), wrap.firstChild);
+ }
+ }
+
+ // Wrapper around buildLineContent which will reuse the structure
+ // in display.externalMeasured when possible.
+ function getLineContent(cm, lineView) {
+ var ext = cm.display.externalMeasured;
+ if (ext && ext.line == lineView.line) {
+ cm.display.externalMeasured = null;
+ lineView.measure = ext.measure;
+ return ext.built;
+ }
+ return buildLineContent(cm, lineView);
+ }
+
+ // Redraw the line's text. Interacts with the background and text
+ // classes because the mode may output tokens that influence these
+ // classes.
+ function updateLineText(cm, lineView) {
+ var cls = lineView.text.className;
+ var built = getLineContent(cm, lineView);
+ if (lineView.text == lineView.node) lineView.node = built.pre;
+ lineView.text.parentNode.replaceChild(built.pre, lineView.text);
+ lineView.text = built.pre;
+ if (built.bgClass != lineView.bgClass || built.textClass != lineView.textClass) {
+ lineView.bgClass = built.bgClass;
+ lineView.textClass = built.textClass;
+ updateLineClasses(lineView);
+ } else if (cls) {
+ lineView.text.className = cls;
+ }
+ }
+
+ function updateLineClasses(lineView) {
+ updateLineBackground(lineView);
+ if (lineView.line.wrapClass)
+ ensureLineWrapped(lineView).className = lineView.line.wrapClass;
+ else if (lineView.node != lineView.text)
+ lineView.node.className = "";
+ var textClass = lineView.textClass ? lineView.textClass + " " + (lineView.line.textClass || "") : lineView.line.textClass;
+ lineView.text.className = textClass || "";
+ }
+
+ function updateLineGutter(cm, lineView, lineN, dims) {
+ if (lineView.gutter) {
+ lineView.node.removeChild(lineView.gutter);
+ lineView.gutter = null;
+ }
+ if (lineView.gutterBackground) {
+ lineView.node.removeChild(lineView.gutterBackground);
+ lineView.gutterBackground = null;
+ }
+ if (lineView.line.gutterClass) {
+ var wrap = ensureLineWrapped(lineView);
+ lineView.gutterBackground = elt("div", null, "CodeMirror-gutter-background " + lineView.line.gutterClass,
+ "left: " + (cm.options.fixedGutter ? dims.fixedPos : -dims.gutterTotalWidth) +
+ "px; width: " + dims.gutterTotalWidth + "px");
+ wrap.insertBefore(lineView.gutterBackground, lineView.text);
+ }
+ var markers = lineView.line.gutterMarkers;
+ if (cm.options.lineNumbers || markers) {
+ var wrap = ensureLineWrapped(lineView);
+ var gutterWrap = lineView.gutter = elt("div", null, "CodeMirror-gutter-wrapper", "left: " +
+ (cm.options.fixedGutter ? dims.fixedPos : -dims.gutterTotalWidth) + "px");
+ cm.display.input.setUneditable(gutterWrap);
+ wrap.insertBefore(gutterWrap, lineView.text);
+ if (lineView.line.gutterClass)
+ gutterWrap.className += " " + lineView.line.gutterClass;
+ if (cm.options.lineNumbers && (!markers || !markers["CodeMirror-linenumbers"]))
+ lineView.lineNumber = gutterWrap.appendChild(
+ elt("div", lineNumberFor(cm.options, lineN),
+ "CodeMirror-linenumber CodeMirror-gutter-elt",
+ "left: " + dims.gutterLeft["CodeMirror-linenumbers"] + "px; width: "
+ + cm.display.lineNumInnerWidth + "px"));
+ if (markers) for (var k = 0; k < cm.options.gutters.length; ++k) {
+ var id = cm.options.gutters[k], found = markers.hasOwnProperty(id) && markers[id];
+ if (found)
+ gutterWrap.appendChild(elt("div", [found], "CodeMirror-gutter-elt", "left: " +
+ dims.gutterLeft[id] + "px; width: " + dims.gutterWidth[id] + "px"));
+ }
+ }
+ }
+
+ function updateLineWidgets(cm, lineView, dims) {
+ if (lineView.alignable) lineView.alignable = null;
+ for (var node = lineView.node.firstChild, next; node; node = next) {
+ var next = node.nextSibling;
+ if (node.className == "CodeMirror-linewidget")
+ lineView.node.removeChild(node);
+ }
+ insertLineWidgets(cm, lineView, dims);
+ }
+
+ // Build a line's DOM representation from scratch
+ function buildLineElement(cm, lineView, lineN, dims) {
+ var built = getLineContent(cm, lineView);
+ lineView.text = lineView.node = built.pre;
+ if (built.bgClass) lineView.bgClass = built.bgClass;
+ if (built.textClass) lineView.textClass = built.textClass;
+
+ updateLineClasses(lineView);
+ updateLineGutter(cm, lineView, lineN, dims);
+ insertLineWidgets(cm, lineView, dims);
+ return lineView.node;
+ }
+
+ // A lineView may contain multiple logical lines (when merged by
+ // collapsed spans). The widgets for all of them need to be drawn.
+ function insertLineWidgets(cm, lineView, dims) {
+ insertLineWidgetsFor(cm, lineView.line, lineView, dims, true);
+ if (lineView.rest) for (var i = 0; i < lineView.rest.length; i++)
+ insertLineWidgetsFor(cm, lineView.rest[i], lineView, dims, false);
+ }
+
+ function insertLineWidgetsFor(cm, line, lineView, dims, allowAbove) {
+ if (!line.widgets) return;
+ var wrap = ensureLineWrapped(lineView);
+ for (var i = 0, ws = line.widgets; i < ws.length; ++i) {
+ var widget = ws[i], node = elt("div", [widget.node], "CodeMirror-linewidget");
+ if (!widget.handleMouseEvents) node.setAttribute("cm-ignore-events", "true");
+ positionLineWidget(widget, node, lineView, dims);
+ cm.display.input.setUneditable(node);
+ if (allowAbove && widget.above)
+ wrap.insertBefore(node, lineView.gutter || lineView.text);
+ else
+ wrap.appendChild(node);
+ signalLater(widget, "redraw");
+ }
+ }
+
+ function positionLineWidget(widget, node, lineView, dims) {
+ if (widget.noHScroll) {
+ (lineView.alignable || (lineView.alignable = [])).push(node);
+ var width = dims.wrapperWidth;
+ node.style.left = dims.fixedPos + "px";
+ if (!widget.coverGutter) {
+ width -= dims.gutterTotalWidth;
+ node.style.paddingLeft = dims.gutterTotalWidth + "px";
+ }
+ node.style.width = width + "px";
+ }
+ if (widget.coverGutter) {
+ node.style.zIndex = 5;
+ node.style.position = "relative";
+ if (!widget.noHScroll) node.style.marginLeft = -dims.gutterTotalWidth + "px";
+ }
+ }
+
+ // POSITION OBJECT
+
+ // A Pos instance represents a position within the text.
+ var Pos = CodeMirror.Pos = function(line, ch) {
+ if (!(this instanceof Pos)) return new Pos(line, ch);
+ this.line = line; this.ch = ch;
+ };
+
+ // Compare two positions, return 0 if they are the same, a negative
+ // number when a is less, and a positive number otherwise.
+ var cmp = CodeMirror.cmpPos = function(a, b) { return a.line - b.line || a.ch - b.ch; };
+
+ function copyPos(x) {return Pos(x.line, x.ch);}
+ function maxPos(a, b) { return cmp(a, b) < 0 ? b : a; }
+ function minPos(a, b) { return cmp(a, b) < 0 ? a : b; }
+
+ // INPUT HANDLING
+
+ function ensureFocus(cm) {
+ if (!cm.state.focused) { cm.display.input.focus(); onFocus(cm); }
+ }
+
+ // This will be set to a {lineWise: bool, text: [string]} object, so
+ // that, when pasting, we know what kind of selections the copied
+ // text was made out of.
+ var lastCopied = null;
+
+ function applyTextInput(cm, inserted, deleted, sel, origin) {
+ var doc = cm.doc;
+ cm.display.shift = false;
+ if (!sel) sel = doc.sel;
+
+ var paste = cm.state.pasteIncoming || origin == "paste";
+ var textLines = doc.splitLines(inserted), multiPaste = null
+ // When pasing N lines into N selections, insert one line per selection
+ if (paste && sel.ranges.length > 1) {
+ if (lastCopied && lastCopied.text.join("\n") == inserted) {
+ if (sel.ranges.length % lastCopied.text.length == 0) {
+ multiPaste = [];
+ for (var i = 0; i < lastCopied.text.length; i++)
+ multiPaste.push(doc.splitLines(lastCopied.text[i]));
+ }
+ } else if (textLines.length == sel.ranges.length) {
+ multiPaste = map(textLines, function(l) { return [l]; });
+ }
+ }
+
+ // Normal behavior is to insert the new text into every selection
+ for (var i = sel.ranges.length - 1; i >= 0; i--) {
+ var range = sel.ranges[i];
+ var from = range.from(), to = range.to();
+ if (range.empty()) {
+ if (deleted && deleted > 0) // Handle deletion
+ from = Pos(from.line, from.ch - deleted);
+ else if (cm.state.overwrite && !paste) // Handle overwrite
+ to = Pos(to.line, Math.min(getLine(doc, to.line).text.length, to.ch + lst(textLines).length));
+ else if (lastCopied && lastCopied.lineWise && lastCopied.text.join("\n") == inserted)
+ from = to = Pos(from.line, 0)
+ }
+ var updateInput = cm.curOp.updateInput;
+ var changeEvent = {from: from, to: to, text: multiPaste ? multiPaste[i % multiPaste.length] : textLines,
+ origin: origin || (paste ? "paste" : cm.state.cutIncoming ? "cut" : "+input")};
+ makeChange(cm.doc, changeEvent);
+ signalLater(cm, "inputRead", cm, changeEvent);
+ }
+ if (inserted && !paste)
+ triggerElectric(cm, inserted);
+
+ ensureCursorVisible(cm);
+ cm.curOp.updateInput = updateInput;
+ cm.curOp.typing = true;
+ cm.state.pasteIncoming = cm.state.cutIncoming = false;
+ }
+
+ function handlePaste(e, cm) {
+ var pasted = e.clipboardData && e.clipboardData.getData("text/plain");
+ if (pasted) {
+ e.preventDefault();
+ if (!cm.isReadOnly() && !cm.options.disableInput)
+ runInOp(cm, function() { applyTextInput(cm, pasted, 0, null, "paste"); });
+ return true;
+ }
+ }
+
+ function triggerElectric(cm, inserted) {
+ // When an 'electric' character is inserted, immediately trigger a reindent
+ if (!cm.options.electricChars || !cm.options.smartIndent) return;
+ var sel = cm.doc.sel;
+
+ for (var i = sel.ranges.length - 1; i >= 0; i--) {
+ var range = sel.ranges[i];
+ if (range.head.ch > 100 || (i && sel.ranges[i - 1].head.line == range.head.line)) continue;
+ var mode = cm.getModeAt(range.head);
+ var indented = false;
+ if (mode.electricChars) {
+ for (var j = 0; j < mode.electricChars.length; j++)
+ if (inserted.indexOf(mode.electricChars.charAt(j)) > -1) {
+ indented = indentLine(cm, range.head.line, "smart");
+ break;
+ }
+ } else if (mode.electricInput) {
+ if (mode.electricInput.test(getLine(cm.doc, range.head.line).text.slice(0, range.head.ch)))
+ indented = indentLine(cm, range.head.line, "smart");
+ }
+ if (indented) signalLater(cm, "electricInput", cm, range.head.line);
+ }
+ }
+
+ function copyableRanges(cm) {
+ var text = [], ranges = [];
+ for (var i = 0; i < cm.doc.sel.ranges.length; i++) {
+ var line = cm.doc.sel.ranges[i].head.line;
+ var lineRange = {anchor: Pos(line, 0), head: Pos(line + 1, 0)};
+ ranges.push(lineRange);
+ text.push(cm.getRange(lineRange.anchor, lineRange.head));
+ }
+ return {text: text, ranges: ranges};
+ }
+
+ function disableBrowserMagic(field) {
+ field.setAttribute("autocorrect", "off");
+ field.setAttribute("autocapitalize", "off");
+ field.setAttribute("spellcheck", "false");
+ }
+
+ // TEXTAREA INPUT STYLE
+
+ function TextareaInput(cm) {
+ this.cm = cm;
+ // See input.poll and input.reset
+ this.prevInput = "";
+
+ // Flag that indicates whether we expect input to appear real soon
+ // now (after some event like 'keypress' or 'input') and are
+ // polling intensively.
+ this.pollingFast = false;
+ // Self-resetting timeout for the poller
+ this.polling = new Delayed();
+ // Tracks when input.reset has punted to just putting a short
+ // string into the textarea instead of the full selection.
+ this.inaccurateSelection = false;
+ // Used to work around IE issue with selection being forgotten when focus moves away from textarea
+ this.hasSelection = false;
+ this.composing = null;
+ };
+
+ function hiddenTextarea() {
+ var te = elt("textarea", null, null, "position: absolute; padding: 0; width: 1px; height: 1em; outline: none");
+ var div = elt("div", [te], null, "overflow: hidden; position: relative; width: 3px; height: 0px;");
+ // The textarea is kept positioned near the cursor to prevent the
+ // fact that it'll be scrolled into view on input from scrolling
+ // our fake cursor out of view. On webkit, when wrap=off, paste is
+ // very slow. So make the area wide instead.
+ if (webkit) te.style.width = "1000px";
+ else te.setAttribute("wrap", "off");
+ // If border: 0; -- iOS fails to open keyboard (issue #1287)
+ if (ios) te.style.border = "1px solid black";
+ disableBrowserMagic(te);
+ return div;
+ }
+
+ TextareaInput.prototype = copyObj({
+ init: function(display) {
+ var input = this, cm = this.cm;
+
+ // Wraps and hides input textarea
+ var div = this.wrapper = hiddenTextarea();
+ // The semihidden textarea that is focused when the editor is
+ // focused, and receives input.
+ var te = this.textarea = div.firstChild;
+ display.wrapper.insertBefore(div, display.wrapper.firstChild);
+
+ // Needed to hide big blue blinking cursor on Mobile Safari (doesn't seem to work in iOS 8 anymore)
+ if (ios) te.style.width = "0px";
+
+ on(te, "input", function() {
+ if (ie && ie_version >= 9 && input.hasSelection) input.hasSelection = null;
+ input.poll();
+ });
+
+ on(te, "paste", function(e) {
+ if (signalDOMEvent(cm, e) || handlePaste(e, cm)) return
+
+ cm.state.pasteIncoming = true;
+ input.fastPoll();
+ });
+
+ function prepareCopyCut(e) {
+ if (signalDOMEvent(cm, e)) return
+ if (cm.somethingSelected()) {
+ lastCopied = {lineWise: false, text: cm.getSelections()};
+ if (input.inaccurateSelection) {
+ input.prevInput = "";
+ input.inaccurateSelection = false;
+ te.value = lastCopied.text.join("\n");
+ selectInput(te);
+ }
+ } else if (!cm.options.lineWiseCopyCut) {
+ return;
+ } else {
+ var ranges = copyableRanges(cm);
+ lastCopied = {lineWise: true, text: ranges.text};
+ if (e.type == "cut") {
+ cm.setSelections(ranges.ranges, null, sel_dontScroll);
+ } else {
+ input.prevInput = "";
+ te.value = ranges.text.join("\n");
+ selectInput(te);
+ }
+ }
+ if (e.type == "cut") cm.state.cutIncoming = true;
+ }
+ on(te, "cut", prepareCopyCut);
+ on(te, "copy", prepareCopyCut);
+
+ on(display.scroller, "paste", function(e) {
+ if (eventInWidget(display, e) || signalDOMEvent(cm, e)) return;
+ cm.state.pasteIncoming = true;
+ input.focus();
+ });
+
+ // Prevent normal selection in the editor (we handle our own)
+ on(display.lineSpace, "selectstart", function(e) {
+ if (!eventInWidget(display, e)) e_preventDefault(e);
+ });
+
+ on(te, "compositionstart", function() {
+ var start = cm.getCursor("from");
+ if (input.composing) input.composing.range.clear()
+ input.composing = {
+ start: start,
+ range: cm.markText(start, cm.getCursor("to"), {className: "CodeMirror-composing"})
+ };
+ });
+ on(te, "compositionend", function() {
+ if (input.composing) {
+ input.poll();
+ input.composing.range.clear();
+ input.composing = null;
+ }
+ });
+ },
+
+ prepareSelection: function() {
+ // Redraw the selection and/or cursor
+ var cm = this.cm, display = cm.display, doc = cm.doc;
+ var result = prepareSelection(cm);
+
+ // Move the hidden textarea near the cursor to prevent scrolling artifacts
+ if (cm.options.moveInputWithCursor) {
+ var headPos = cursorCoords(cm, doc.sel.primary().head, "div");
+ var wrapOff = display.wrapper.getBoundingClientRect(), lineOff = display.lineDiv.getBoundingClientRect();
+ result.teTop = Math.max(0, Math.min(display.wrapper.clientHeight - 10,
+ headPos.top + lineOff.top - wrapOff.top));
+ result.teLeft = Math.max(0, Math.min(display.wrapper.clientWidth - 10,
+ headPos.left + lineOff.left - wrapOff.left));
+ }
+
+ return result;
+ },
+
+ showSelection: function(drawn) {
+ var cm = this.cm, display = cm.display;
+ removeChildrenAndAdd(display.cursorDiv, drawn.cursors);
+ removeChildrenAndAdd(display.selectionDiv, drawn.selection);
+ if (drawn.teTop != null) {
+ this.wrapper.style.top = drawn.teTop + "px";
+ this.wrapper.style.left = drawn.teLeft + "px";
+ }
+ },
+
+ // Reset the input to correspond to the selection (or to be empty,
+ // when not typing and nothing is selected)
+ reset: function(typing) {
+ if (this.contextMenuPending) return;
+ var minimal, selected, cm = this.cm, doc = cm.doc;
+ if (cm.somethingSelected()) {
+ this.prevInput = "";
+ var range = doc.sel.primary();
+ minimal = hasCopyEvent &&
+ (range.to().line - range.from().line > 100 || (selected = cm.getSelection()).length > 1000);
+ var content = minimal ? "-" : selected || cm.getSelection();
+ this.textarea.value = content;
+ if (cm.state.focused) selectInput(this.textarea);
+ if (ie && ie_version >= 9) this.hasSelection = content;
+ } else if (!typing) {
+ this.prevInput = this.textarea.value = "";
+ if (ie && ie_version >= 9) this.hasSelection = null;
+ }
+ this.inaccurateSelection = minimal;
+ },
+
+ getField: function() { return this.textarea; },
+
+ supportsTouch: function() { return false; },
+
+ focus: function() {
+ if (this.cm.options.readOnly != "nocursor" && (!mobile || activeElt() != this.textarea)) {
+ try { this.textarea.focus(); }
+ catch (e) {} // IE8 will throw if the textarea is display: none or not in DOM
+ }
+ },
+
+ blur: function() { this.textarea.blur(); },
+
+ resetPosition: function() {
+ this.wrapper.style.top = this.wrapper.style.left = 0;
+ },
+
+ receivedFocus: function() { this.slowPoll(); },
+
+ // Poll for input changes, using the normal rate of polling. This
+ // runs as long as the editor is focused.
+ slowPoll: function() {
+ var input = this;
+ if (input.pollingFast) return;
+ input.polling.set(this.cm.options.pollInterval, function() {
+ input.poll();
+ if (input.cm.state.focused) input.slowPoll();
+ });
+ },
+
+ // When an event has just come in that is likely to add or change
+ // something in the input textarea, we poll faster, to ensure that
+ // the change appears on the screen quickly.
+ fastPoll: function() {
+ var missed = false, input = this;
+ input.pollingFast = true;
+ function p() {
+ var changed = input.poll();
+ if (!changed && !missed) {missed = true; input.polling.set(60, p);}
+ else {input.pollingFast = false; input.slowPoll();}
+ }
+ input.polling.set(20, p);
+ },
+
+ // Read input from the textarea, and update the document to match.
+ // When something is selected, it is present in the textarea, and
+ // selected (unless it is huge, in which case a placeholder is
+ // used). When nothing is selected, the cursor sits after previously
+ // seen text (can be empty), which is stored in prevInput (we must
+ // not reset the textarea when typing, because that breaks IME).
+ poll: function() {
+ var cm = this.cm, input = this.textarea, prevInput = this.prevInput;
+ // Since this is called a *lot*, try to bail out as cheaply as
+ // possible when it is clear that nothing happened. hasSelection
+ // will be the case when there is a lot of text in the textarea,
+ // in which case reading its value would be expensive.
+ if (this.contextMenuPending || !cm.state.focused ||
+ (hasSelection(input) && !prevInput && !this.composing) ||
+ cm.isReadOnly() || cm.options.disableInput || cm.state.keySeq)
+ return false;
+
+ var text = input.value;
+ // If nothing changed, bail.
+ if (text == prevInput && !cm.somethingSelected()) return false;
+ // Work around nonsensical selection resetting in IE9/10, and
+ // inexplicable appearance of private area unicode characters on
+ // some key combos in Mac (#2689).
+ if (ie && ie_version >= 9 && this.hasSelection === text ||
+ mac && /[\uf700-\uf7ff]/.test(text)) {
+ cm.display.input.reset();
+ return false;
+ }
+
+ if (cm.doc.sel == cm.display.selForContextMenu) {
+ var first = text.charCodeAt(0);
+ if (first == 0x200b && !prevInput) prevInput = "\u200b";
+ if (first == 0x21da) { this.reset(); return this.cm.execCommand("undo"); }
+ }
+ // Find the part of the input that is actually new
+ var same = 0, l = Math.min(prevInput.length, text.length);
+ while (same < l && prevInput.charCodeAt(same) == text.charCodeAt(same)) ++same;
+
+ var self = this;
+ runInOp(cm, function() {
+ applyTextInput(cm, text.slice(same), prevInput.length - same,
+ null, self.composing ? "*compose" : null);
+
+ // Don't leave long text in the textarea, since it makes further polling slow
+ if (text.length > 1000 || text.indexOf("\n") > -1) input.value = self.prevInput = "";
+ else self.prevInput = text;
+
+ if (self.composing) {
+ self.composing.range.clear();
+ self.composing.range = cm.markText(self.composing.start, cm.getCursor("to"),
+ {className: "CodeMirror-composing"});
+ }
+ });
+ return true;
+ },
+
+ ensurePolled: function() {
+ if (this.pollingFast && this.poll()) this.pollingFast = false;
+ },
+
+ onKeyPress: function() {
+ if (ie && ie_version >= 9) this.hasSelection = null;
+ this.fastPoll();
+ },
+
+ onContextMenu: function(e) {
+ var input = this, cm = input.cm, display = cm.display, te = input.textarea;
+ var pos = posFromMouse(cm, e), scrollPos = display.scroller.scrollTop;
+ if (!pos || presto) return; // Opera is difficult.
+
+ // Reset the current text selection only if the click is done outside of the selection
+ // and 'resetSelectionOnContextMenu' option is true.
+ var reset = cm.options.resetSelectionOnContextMenu;
+ if (reset && cm.doc.sel.contains(pos) == -1)
+ operation(cm, setSelection)(cm.doc, simpleSelection(pos), sel_dontScroll);
+
+ var oldCSS = te.style.cssText, oldWrapperCSS = input.wrapper.style.cssText;
+ input.wrapper.style.cssText = "position: absolute"
+ var wrapperBox = input.wrapper.getBoundingClientRect()
+ te.style.cssText = "position: absolute; width: 30px; height: 30px; top: " + (e.clientY - wrapperBox.top - 5) +
+ "px; left: " + (e.clientX - wrapperBox.left - 5) + "px; z-index: 1000; background: " +
+ (ie ? "rgba(255, 255, 255, .05)" : "transparent") +
+ "; outline: none; border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity=5);";
+ if (webkit) var oldScrollY = window.scrollY; // Work around Chrome issue (#2712)
+ display.input.focus();
+ if (webkit) window.scrollTo(null, oldScrollY);
+ display.input.reset();
+ // Adds "Select all" to context menu in FF
+ if (!cm.somethingSelected()) te.value = input.prevInput = " ";
+ input.contextMenuPending = true;
+ display.selForContextMenu = cm.doc.sel;
+ clearTimeout(display.detectingSelectAll);
+
+ // Select-all will be greyed out if there's nothing to select, so
+ // this adds a zero-width space so that we can later check whether
+ // it got selected.
+ function prepareSelectAllHack() {
+ if (te.selectionStart != null) {
+ var selected = cm.somethingSelected();
+ var extval = "\u200b" + (selected ? te.value : "");
+ te.value = "\u21da"; // Used to catch context-menu undo
+ te.value = extval;
+ input.prevInput = selected ? "" : "\u200b";
+ te.selectionStart = 1; te.selectionEnd = extval.length;
+ // Re-set this, in case some other handler touched the
+ // selection in the meantime.
+ display.selForContextMenu = cm.doc.sel;
+ }
+ }
+ function rehide() {
+ input.contextMenuPending = false;
+ input.wrapper.style.cssText = oldWrapperCSS
+ te.style.cssText = oldCSS;
+ if (ie && ie_version < 9) display.scrollbars.setScrollTop(display.scroller.scrollTop = scrollPos);
+
+ // Try to detect the user choosing select-all
+ if (te.selectionStart != null) {
+ if (!ie || (ie && ie_version < 9)) prepareSelectAllHack();
+ var i = 0, poll = function() {
+ if (display.selForContextMenu == cm.doc.sel && te.selectionStart == 0 &&
+ te.selectionEnd > 0 && input.prevInput == "\u200b")
+ operation(cm, commands.selectAll)(cm);
+ else if (i++ < 10) display.detectingSelectAll = setTimeout(poll, 500);
+ else display.input.reset();
+ };
+ display.detectingSelectAll = setTimeout(poll, 200);
+ }
+ }
+
+ if (ie && ie_version >= 9) prepareSelectAllHack();
+ if (captureRightClick) {
+ e_stop(e);
+ var mouseup = function() {
+ off(window, "mouseup", mouseup);
+ setTimeout(rehide, 20);
+ };
+ on(window, "mouseup", mouseup);
+ } else {
+ setTimeout(rehide, 50);
+ }
+ },
+
+ readOnlyChanged: function(val) {
+ if (!val) this.reset();
+ },
+
+ setUneditable: nothing,
+
+ needsContentAttribute: false
+ }, TextareaInput.prototype);
+
+ // CONTENTEDITABLE INPUT STYLE
+
+ function ContentEditableInput(cm) {
+ this.cm = cm;
+ this.lastAnchorNode = this.lastAnchorOffset = this.lastFocusNode = this.lastFocusOffset = null;
+ this.polling = new Delayed();
+ this.gracePeriod = false;
+ }
+
+ ContentEditableInput.prototype = copyObj({
+ init: function(display) {
+ var input = this, cm = input.cm;
+ var div = input.div = display.lineDiv;
+ disableBrowserMagic(div);
+
+ on(div, "paste", function(e) {
+ if (!signalDOMEvent(cm, e)) handlePaste(e, cm);
+ })
+
+ on(div, "compositionstart", function(e) {
+ var data = e.data;
+ input.composing = {sel: cm.doc.sel, data: data, startData: data};
+ if (!data) return;
+ var prim = cm.doc.sel.primary();
+ var line = cm.getLine(prim.head.line);
+ var found = line.indexOf(data, Math.max(0, prim.head.ch - data.length));
+ if (found > -1 && found <= prim.head.ch)
+ input.composing.sel = simpleSelection(Pos(prim.head.line, found),
+ Pos(prim.head.line, found + data.length));
+ });
+ on(div, "compositionupdate", function(e) {
+ input.composing.data = e.data;
+ });
+ on(div, "compositionend", function(e) {
+ var ours = input.composing;
+ if (!ours) return;
+ if (e.data != ours.startData && !/\u200b/.test(e.data))
+ ours.data = e.data;
+ // Need a small delay to prevent other code (input event,
+ // selection polling) from doing damage when fired right after
+ // compositionend.
+ setTimeout(function() {
+ if (!ours.handled)
+ input.applyComposition(ours);
+ if (input.composing == ours)
+ input.composing = null;
+ }, 50);
+ });
+
+ on(div, "touchstart", function() {
+ input.forceCompositionEnd();
+ });
+
+ on(div, "input", function() {
+ if (input.composing) return;
+ if (cm.isReadOnly() || !input.pollContent())
+ runInOp(input.cm, function() {regChange(cm);});
+ });
+
+ function onCopyCut(e) {
+ if (signalDOMEvent(cm, e)) return
+ if (cm.somethingSelected()) {
+ lastCopied = {lineWise: false, text: cm.getSelections()};
+ if (e.type == "cut") cm.replaceSelection("", null, "cut");
+ } else if (!cm.options.lineWiseCopyCut) {
+ return;
+ } else {
+ var ranges = copyableRanges(cm);
+ lastCopied = {lineWise: true, text: ranges.text};
+ if (e.type == "cut") {
+ cm.operation(function() {
+ cm.setSelections(ranges.ranges, 0, sel_dontScroll);
+ cm.replaceSelection("", null, "cut");
+ });
+ }
+ }
+ // iOS exposes the clipboard API, but seems to discard content inserted into it
+ if (e.clipboardData && !ios) {
+ e.preventDefault();
+ e.clipboardData.clearData();
+ e.clipboardData.setData("text/plain", lastCopied.text.join("\n"));
+ } else {
+ // Old-fashioned briefly-focus-a-textarea hack
+ var kludge = hiddenTextarea(), te = kludge.firstChild;
+ cm.display.lineSpace.insertBefore(kludge, cm.display.lineSpace.firstChild);
+ te.value = lastCopied.text.join("\n");
+ var hadFocus = document.activeElement;
+ selectInput(te);
+ setTimeout(function() {
+ cm.display.lineSpace.removeChild(kludge);
+ hadFocus.focus();
+ }, 50);
+ }
+ }
+ on(div, "copy", onCopyCut);
+ on(div, "cut", onCopyCut);
+ },
+
+ prepareSelection: function() {
+ var result = prepareSelection(this.cm, false);
+ result.focus = this.cm.state.focused;
+ return result;
+ },
+
+ showSelection: function(info, takeFocus) {
+ if (!info || !this.cm.display.view.length) return;
+ if (info.focus || takeFocus) this.showPrimarySelection();
+ this.showMultipleSelections(info);
+ },
+
+ showPrimarySelection: function() {
+ var sel = window.getSelection(), prim = this.cm.doc.sel.primary();
+ var curAnchor = domToPos(this.cm, sel.anchorNode, sel.anchorOffset);
+ var curFocus = domToPos(this.cm, sel.focusNode, sel.focusOffset);
+ if (curAnchor && !curAnchor.bad && curFocus && !curFocus.bad &&
+ cmp(minPos(curAnchor, curFocus), prim.from()) == 0 &&
+ cmp(maxPos(curAnchor, curFocus), prim.to()) == 0)
+ return;
+
+ var start = posToDOM(this.cm, prim.from());
+ var end = posToDOM(this.cm, prim.to());
+ if (!start && !end) return;
+
+ var view = this.cm.display.view;
+ var old = sel.rangeCount && sel.getRangeAt(0);
+ if (!start) {
+ start = {node: view[0].measure.map[2], offset: 0};
+ } else if (!end) { // FIXME dangerously hacky
+ var measure = view[view.length - 1].measure;
+ var map = measure.maps ? measure.maps[measure.maps.length - 1] : measure.map;
+ end = {node: map[map.length - 1], offset: map[map.length - 2] - map[map.length - 3]};
+ }
+
+ try { var rng = range(start.node, start.offset, end.offset, end.node); }
+ catch(e) {} // Our model of the DOM might be outdated, in which case the range we try to set can be impossible
+ if (rng) {
+ if (!gecko && this.cm.state.focused) {
+ sel.collapse(start.node, start.offset);
+ if (!rng.collapsed) sel.addRange(rng);
+ } else {
+ sel.removeAllRanges();
+ sel.addRange(rng);
+ }
+ if (old && sel.anchorNode == null) sel.addRange(old);
+ else if (gecko) this.startGracePeriod();
+ }
+ this.rememberSelection();
+ },
+
+ startGracePeriod: function() {
+ var input = this;
+ clearTimeout(this.gracePeriod);
+ this.gracePeriod = setTimeout(function() {
+ input.gracePeriod = false;
+ if (input.selectionChanged())
+ input.cm.operation(function() { input.cm.curOp.selectionChanged = true; });
+ }, 20);
+ },
+
+ showMultipleSelections: function(info) {
+ removeChildrenAndAdd(this.cm.display.cursorDiv, info.cursors);
+ removeChildrenAndAdd(this.cm.display.selectionDiv, info.selection);
+ },
+
+ rememberSelection: function() {
+ var sel = window.getSelection();
+ this.lastAnchorNode = sel.anchorNode; this.lastAnchorOffset = sel.anchorOffset;
+ this.lastFocusNode = sel.focusNode; this.lastFocusOffset = sel.focusOffset;
+ },
+
+ selectionInEditor: function() {
+ var sel = window.getSelection();
+ if (!sel.rangeCount) return false;
+ var node = sel.getRangeAt(0).commonAncestorContainer;
+ return contains(this.div, node);
+ },
+
+ focus: function() {
+ if (this.cm.options.readOnly != "nocursor") this.div.focus();
+ },
+ blur: function() { this.div.blur(); },
+ getField: function() { return this.div; },
+
+ supportsTouch: function() { return true; },
+
+ receivedFocus: function() {
+ var input = this;
+ if (this.selectionInEditor())
+ this.pollSelection();
+ else
+ runInOp(this.cm, function() { input.cm.curOp.selectionChanged = true; });
+
+ function poll() {
+ if (input.cm.state.focused) {
+ input.pollSelection();
+ input.polling.set(input.cm.options.pollInterval, poll);
+ }
+ }
+ this.polling.set(this.cm.options.pollInterval, poll);
+ },
+
+ selectionChanged: function() {
+ var sel = window.getSelection();
+ return sel.anchorNode != this.lastAnchorNode || sel.anchorOffset != this.lastAnchorOffset ||
+ sel.focusNode != this.lastFocusNode || sel.focusOffset != this.lastFocusOffset;
+ },
+
+ pollSelection: function() {
+ if (!this.composing && !this.gracePeriod && this.selectionChanged()) {
+ var sel = window.getSelection(), cm = this.cm;
+ this.rememberSelection();
+ var anchor = domToPos(cm, sel.anchorNode, sel.anchorOffset);
+ var head = domToPos(cm, sel.focusNode, sel.focusOffset);
+ if (anchor && head) runInOp(cm, function() {
+ setSelection(cm.doc, simpleSelection(anchor, head), sel_dontScroll);
+ if (anchor.bad || head.bad) cm.curOp.selectionChanged = true;
+ });
+ }
+ },
+
+ pollContent: function() {
+ var cm = this.cm, display = cm.display, sel = cm.doc.sel.primary();
+ var from = sel.from(), to = sel.to();
+ if (from.line < display.viewFrom || to.line > display.viewTo - 1) return false;
+
+ var fromIndex;
+ if (from.line == display.viewFrom || (fromIndex = findViewIndex(cm, from.line)) == 0) {
+ var fromLine = lineNo(display.view[0].line);
+ var fromNode = display.view[0].node;
+ } else {
+ var fromLine = lineNo(display.view[fromIndex].line);
+ var fromNode = display.view[fromIndex - 1].node.nextSibling;
+ }
+ var toIndex = findViewIndex(cm, to.line);
+ if (toIndex == display.view.length - 1) {
+ var toLine = display.viewTo - 1;
+ var toNode = display.lineDiv.lastChild;
+ } else {
+ var toLine = lineNo(display.view[toIndex + 1].line) - 1;
+ var toNode = display.view[toIndex + 1].node.previousSibling;
+ }
+
+ var newText = cm.doc.splitLines(domTextBetween(cm, fromNode, toNode, fromLine, toLine));
+ var oldText = getBetween(cm.doc, Pos(fromLine, 0), Pos(toLine, getLine(cm.doc, toLine).text.length));
+ while (newText.length > 1 && oldText.length > 1) {
+ if (lst(newText) == lst(oldText)) { newText.pop(); oldText.pop(); toLine--; }
+ else if (newText[0] == oldText[0]) { newText.shift(); oldText.shift(); fromLine++; }
+ else break;
+ }
+
+ var cutFront = 0, cutEnd = 0;
+ var newTop = newText[0], oldTop = oldText[0], maxCutFront = Math.min(newTop.length, oldTop.length);
+ while (cutFront < maxCutFront && newTop.charCodeAt(cutFront) == oldTop.charCodeAt(cutFront))
+ ++cutFront;
+ var newBot = lst(newText), oldBot = lst(oldText);
+ var maxCutEnd = Math.min(newBot.length - (newText.length == 1 ? cutFront : 0),
+ oldBot.length - (oldText.length == 1 ? cutFront : 0));
+ while (cutEnd < maxCutEnd &&
+ newBot.charCodeAt(newBot.length - cutEnd - 1) == oldBot.charCodeAt(oldBot.length - cutEnd - 1))
+ ++cutEnd;
+
+ newText[newText.length - 1] = newBot.slice(0, newBot.length - cutEnd);
+ newText[0] = newText[0].slice(cutFront);
+
+ var chFrom = Pos(fromLine, cutFront);
+ var chTo = Pos(toLine, oldText.length ? lst(oldText).length - cutEnd : 0);
+ if (newText.length > 1 || newText[0] || cmp(chFrom, chTo)) {
+ replaceRange(cm.doc, newText, chFrom, chTo, "+input");
+ return true;
+ }
+ },
+
+ ensurePolled: function() {
+ this.forceCompositionEnd();
+ },
+ reset: function() {
+ this.forceCompositionEnd();
+ },
+ forceCompositionEnd: function() {
+ if (!this.composing || this.composing.handled) return;
+ this.applyComposition(this.composing);
+ this.composing.handled = true;
+ this.div.blur();
+ this.div.focus();
+ },
+ applyComposition: function(composing) {
+ if (this.cm.isReadOnly())
+ operation(this.cm, regChange)(this.cm)
+ else if (composing.data && composing.data != composing.startData)
+ operation(this.cm, applyTextInput)(this.cm, composing.data, 0, composing.sel);
+ },
+
+ setUneditable: function(node) {
+ node.contentEditable = "false"
+ },
+
+ onKeyPress: function(e) {
+ e.preventDefault();
+ if (!this.cm.isReadOnly())
+ operation(this.cm, applyTextInput)(this.cm, String.fromCharCode(e.charCode == null ? e.keyCode : e.charCode), 0);
+ },
+
+ readOnlyChanged: function(val) {
+ this.div.contentEditable = String(val != "nocursor")
+ },
+
+ onContextMenu: nothing,
+ resetPosition: nothing,
+
+ needsContentAttribute: true
+ }, ContentEditableInput.prototype);
+
+ function posToDOM(cm, pos) {
+ var view = findViewForLine(cm, pos.line);
+ if (!view || view.hidden) return null;
+ var line = getLine(cm.doc, pos.line);
+ var info = mapFromLineView(view, line, pos.line);
+
+ var order = getOrder(line), side = "left";
+ if (order) {
+ var partPos = getBidiPartAt(order, pos.ch);
+ side = partPos % 2 ? "right" : "left";
+ }
+ var result = nodeAndOffsetInLineMap(info.map, pos.ch, side);
+ result.offset = result.collapse == "right" ? result.end : result.start;
+ return result;
+ }
+
+ function badPos(pos, bad) { if (bad) pos.bad = true; return pos; }
+
+ function domToPos(cm, node, offset) {
+ var lineNode;
+ if (node == cm.display.lineDiv) {
+ lineNode = cm.display.lineDiv.childNodes[offset];
+ if (!lineNode) return badPos(cm.clipPos(Pos(cm.display.viewTo - 1)), true);
+ node = null; offset = 0;
+ } else {
+ for (lineNode = node;; lineNode = lineNode.parentNode) {
+ if (!lineNode || lineNode == cm.display.lineDiv) return null;
+ if (lineNode.parentNode && lineNode.parentNode == cm.display.lineDiv) break;
+ }
+ }
+ for (var i = 0; i < cm.display.view.length; i++) {
+ var lineView = cm.display.view[i];
+ if (lineView.node == lineNode)
+ return locateNodeInLineView(lineView, node, offset);
+ }
+ }
+
+ function locateNodeInLineView(lineView, node, offset) {
+ var wrapper = lineView.text.firstChild, bad = false;
+ if (!node || !contains(wrapper, node)) return badPos(Pos(lineNo(lineView.line), 0), true);
+ if (node == wrapper) {
+ bad = true;
+ node = wrapper.childNodes[offset];
+ offset = 0;
+ if (!node) {
+ var line = lineView.rest ? lst(lineView.rest) : lineView.line;
+ return badPos(Pos(lineNo(line), line.text.length), bad);
+ }
+ }
+
+ var textNode = node.nodeType == 3 ? node : null, topNode = node;
+ if (!textNode && node.childNodes.length == 1 && node.firstChild.nodeType == 3) {
+ textNode = node.firstChild;
+ if (offset) offset = textNode.nodeValue.length;
+ }
+ while (topNode.parentNode != wrapper) topNode = topNode.parentNode;
+ var measure = lineView.measure, maps = measure.maps;
+
+ function find(textNode, topNode, offset) {
+ for (var i = -1; i < (maps ? maps.length : 0); i++) {
+ var map = i < 0 ? measure.map : maps[i];
+ for (var j = 0; j < map.length; j += 3) {
+ var curNode = map[j + 2];
+ if (curNode == textNode || curNode == topNode) {
+ var line = lineNo(i < 0 ? lineView.line : lineView.rest[i]);
+ var ch = map[j] + offset;
+ if (offset < 0 || curNode != textNode) ch = map[j + (offset ? 1 : 0)];
+ return Pos(line, ch);
+ }
+ }
+ }
+ }
+ var found = find(textNode, topNode, offset);
+ if (found) return badPos(found, bad);
+
+ // FIXME this is all really shaky. might handle the few cases it needs to handle, but likely to cause problems
+ for (var after = topNode.nextSibling, dist = textNode ? textNode.nodeValue.length - offset : 0; after; after = after.nextSibling) {
+ found = find(after, after.firstChild, 0);
+ if (found)
+ return badPos(Pos(found.line, found.ch - dist), bad);
+ else
+ dist += after.textContent.length;
+ }
+ for (var before = topNode.previousSibling, dist = offset; before; before = before.previousSibling) {
+ found = find(before, before.firstChild, -1);
+ if (found)
+ return badPos(Pos(found.line, found.ch + dist), bad);
+ else
+ dist += after.textContent.length;
+ }
+ }
+
+ function domTextBetween(cm, from, to, fromLine, toLine) {
+ var text = "", closing = false, lineSep = cm.doc.lineSeparator();
+ function recognizeMarker(id) { return function(marker) { return marker.id == id; }; }
+ function walk(node) {
+ if (node.nodeType == 1) {
+ var cmText = node.getAttribute("cm-text");
+ if (cmText != null) {
+ if (cmText == "") cmText = node.textContent.replace(/\u200b/g, "");
+ text += cmText;
+ return;
+ }
+ var markerID = node.getAttribute("cm-marker"), range;
+ if (markerID) {
+ var found = cm.findMarks(Pos(fromLine, 0), Pos(toLine + 1, 0), recognizeMarker(+markerID));
+ if (found.length && (range = found[0].find()))
+ text += getBetween(cm.doc, range.from, range.to).join(lineSep);
+ return;
+ }
+ if (node.getAttribute("contenteditable") == "false") return;
+ for (var i = 0; i < node.childNodes.length; i++)
+ walk(node.childNodes[i]);
+ if (/^(pre|div|p)$/i.test(node.nodeName))
+ closing = true;
+ } else if (node.nodeType == 3) {
+ var val = node.nodeValue;
+ if (!val) return;
+ if (closing) {
+ text += lineSep;
+ closing = false;
+ }
+ text += val;
+ }
+ }
+ for (;;) {
+ walk(from);
+ if (from == to) break;
+ from = from.nextSibling;
+ }
+ return text;
+ }
+
+ CodeMirror.inputStyles = {"textarea": TextareaInput, "contenteditable": ContentEditableInput};
+
+ // SELECTION / CURSOR
+
+ // Selection objects are immutable. A new one is created every time
+ // the selection changes. A selection is one or more non-overlapping
+ // (and non-touching) ranges, sorted, and an integer that indicates
+ // which one is the primary selection (the one that's scrolled into
+ // view, that getCursor returns, etc).
+ function Selection(ranges, primIndex) {
+ this.ranges = ranges;
+ this.primIndex = primIndex;
+ }
+
+ Selection.prototype = {
+ primary: function() { return this.ranges[this.primIndex]; },
+ equals: function(other) {
+ if (other == this) return true;
+ if (other.primIndex != this.primIndex || other.ranges.length != this.ranges.length) return false;
+ for (var i = 0; i < this.ranges.length; i++) {
+ var here = this.ranges[i], there = other.ranges[i];
+ if (cmp(here.anchor, there.anchor) != 0 || cmp(here.head, there.head) != 0) return false;
+ }
+ return true;
+ },
+ deepCopy: function() {
+ for (var out = [], i = 0; i < this.ranges.length; i++)
+ out[i] = new Range(copyPos(this.ranges[i].anchor), copyPos(this.ranges[i].head));
+ return new Selection(out, this.primIndex);
+ },
+ somethingSelected: function() {
+ for (var i = 0; i < this.ranges.length; i++)
+ if (!this.ranges[i].empty()) return true;
+ return false;
+ },
+ contains: function(pos, end) {
+ if (!end) end = pos;
+ for (var i = 0; i < this.ranges.length; i++) {
+ var range = this.ranges[i];
+ if (cmp(end, range.from()) >= 0 && cmp(pos, range.to()) <= 0)
+ return i;
+ }
+ return -1;
+ }
+ };
+
+ function Range(anchor, head) {
+ this.anchor = anchor; this.head = head;
+ }
+
+ Range.prototype = {
+ from: function() { return minPos(this.anchor, this.head); },
+ to: function() { return maxPos(this.anchor, this.head); },
+ empty: function() {
+ return this.head.line == this.anchor.line && this.head.ch == this.anchor.ch;
+ }
+ };
+
+ // Take an unsorted, potentially overlapping set of ranges, and
+ // build a selection out of it. 'Consumes' ranges array (modifying
+ // it).
+ function normalizeSelection(ranges, primIndex) {
+ var prim = ranges[primIndex];
+ ranges.sort(function(a, b) { return cmp(a.from(), b.from()); });
+ primIndex = indexOf(ranges, prim);
+ for (var i = 1; i < ranges.length; i++) {
+ var cur = ranges[i], prev = ranges[i - 1];
+ if (cmp(prev.to(), cur.from()) >= 0) {
+ var from = minPos(prev.from(), cur.from()), to = maxPos(prev.to(), cur.to());
+ var inv = prev.empty() ? cur.from() == cur.head : prev.from() == prev.head;
+ if (i <= primIndex) --primIndex;
+ ranges.splice(--i, 2, new Range(inv ? to : from, inv ? from : to));
+ }
+ }
+ return new Selection(ranges, primIndex);
+ }
+
+ function simpleSelection(anchor, head) {
+ return new Selection([new Range(anchor, head || anchor)], 0);
+ }
+
+ // Most of the external API clips given positions to make sure they
+ // actually exist within the document.
+ function clipLine(doc, n) {return Math.max(doc.first, Math.min(n, doc.first + doc.size - 1));}
+ function clipPos(doc, pos) {
+ if (pos.line < doc.first) return Pos(doc.first, 0);
+ var last = doc.first + doc.size - 1;
+ if (pos.line > last) return Pos(last, getLine(doc, last).text.length);
+ return clipToLen(pos, getLine(doc, pos.line).text.length);
+ }
+ function clipToLen(pos, linelen) {
+ var ch = pos.ch;
+ if (ch == null || ch > linelen) return Pos(pos.line, linelen);
+ else if (ch < 0) return Pos(pos.line, 0);
+ else return pos;
+ }
+ function isLine(doc, l) {return l >= doc.first && l < doc.first + doc.size;}
+ function clipPosArray(doc, array) {
+ for (var out = [], i = 0; i < array.length; i++) out[i] = clipPos(doc, array[i]);
+ return out;
+ }
+
+ // SELECTION UPDATES
+
+ // The 'scroll' parameter given to many of these indicated whether
+ // the new cursor position should be scrolled into view after
+ // modifying the selection.
+
+ // If shift is held or the extend flag is set, extends a range to
+ // include a given position (and optionally a second position).
+ // Otherwise, simply returns the range between the given positions.
+ // Used for cursor motion and such.
+ function extendRange(doc, range, head, other) {
+ if (doc.cm && doc.cm.display.shift || doc.extend) {
+ var anchor = range.anchor;
+ if (other) {
+ var posBefore = cmp(head, anchor) < 0;
+ if (posBefore != (cmp(other, anchor) < 0)) {
+ anchor = head;
+ head = other;
+ } else if (posBefore != (cmp(head, other) < 0)) {
+ head = other;
+ }
+ }
+ return new Range(anchor, head);
+ } else {
+ return new Range(other || head, head);
+ }
+ }
+
+ // Extend the primary selection range, discard the rest.
+ function extendSelection(doc, head, other, options) {
+ setSelection(doc, new Selection([extendRange(doc, doc.sel.primary(), head, other)], 0), options);
+ }
+
+ // Extend all selections (pos is an array of selections with length
+ // equal the number of selections)
+ function extendSelections(doc, heads, options) {
+ for (var out = [], i = 0; i < doc.sel.ranges.length; i++)
+ out[i] = extendRange(doc, doc.sel.ranges[i], heads[i], null);
+ var newSel = normalizeSelection(out, doc.sel.primIndex);
+ setSelection(doc, newSel, options);
+ }
+
+ // Updates a single range in the selection.
+ function replaceOneSelection(doc, i, range, options) {
+ var ranges = doc.sel.ranges.slice(0);
+ ranges[i] = range;
+ setSelection(doc, normalizeSelection(ranges, doc.sel.primIndex), options);
+ }
+
+ // Reset the selection to a single range.
+ function setSimpleSelection(doc, anchor, head, options) {
+ setSelection(doc, simpleSelection(anchor, head), options);
+ }
+
+ // Give beforeSelectionChange handlers a change to influence a
+ // selection update.
+ function filterSelectionChange(doc, sel, options) {
+ var obj = {
+ ranges: sel.ranges,
+ update: function(ranges) {
+ this.ranges = [];
+ for (var i = 0; i < ranges.length; i++)
+ this.ranges[i] = new Range(clipPos(doc, ranges[i].anchor),
+ clipPos(doc, ranges[i].head));
+ },
+ origin: options && options.origin
+ };
+ signal(doc, "beforeSelectionChange", doc, obj);
+ if (doc.cm) signal(doc.cm, "beforeSelectionChange", doc.cm, obj);
+ if (obj.ranges != sel.ranges) return normalizeSelection(obj.ranges, obj.ranges.length - 1);
+ else return sel;
+ }
+
+ function setSelectionReplaceHistory(doc, sel, options) {
+ var done = doc.history.done, last = lst(done);
+ if (last && last.ranges) {
+ done[done.length - 1] = sel;
+ setSelectionNoUndo(doc, sel, options);
+ } else {
+ setSelection(doc, sel, options);
+ }
+ }
+
+ // Set a new selection.
+ function setSelection(doc, sel, options) {
+ setSelectionNoUndo(doc, sel, options);
+ addSelectionToHistory(doc, doc.sel, doc.cm ? doc.cm.curOp.id : NaN, options);
+ }
+
+ function setSelectionNoUndo(doc, sel, options) {
+ if (hasHandler(doc, "beforeSelectionChange") || doc.cm && hasHandler(doc.cm, "beforeSelectionChange"))
+ sel = filterSelectionChange(doc, sel, options);
+
+ var bias = options && options.bias ||
+ (cmp(sel.primary().head, doc.sel.primary().head) < 0 ? -1 : 1);
+ setSelectionInner(doc, skipAtomicInSelection(doc, sel, bias, true));
+
+ if (!(options && options.scroll === false) && doc.cm)
+ ensureCursorVisible(doc.cm);
+ }
+
+ function setSelectionInner(doc, sel) {
+ if (sel.equals(doc.sel)) return;
+
+ doc.sel = sel;
+
+ if (doc.cm) {
+ doc.cm.curOp.updateInput = doc.cm.curOp.selectionChanged = true;
+ signalCursorActivity(doc.cm);
+ }
+ signalLater(doc, "cursorActivity", doc);
+ }
+
+ // Verify that the selection does not partially select any atomic
+ // marked ranges.
+ function reCheckSelection(doc) {
+ setSelectionInner(doc, skipAtomicInSelection(doc, doc.sel, null, false), sel_dontScroll);
+ }
+
+ // Return a selection that does not partially select any atomic
+ // ranges.
+ function skipAtomicInSelection(doc, sel, bias, mayClear) {
+ var out;
+ for (var i = 0; i < sel.ranges.length; i++) {
+ var range = sel.ranges[i];
+ var old = sel.ranges.length == doc.sel.ranges.length && doc.sel.ranges[i];
+ var newAnchor = skipAtomic(doc, range.anchor, old && old.anchor, bias, mayClear);
+ var newHead = skipAtomic(doc, range.head, old && old.head, bias, mayClear);
+ if (out || newAnchor != range.anchor || newHead != range.head) {
+ if (!out) out = sel.ranges.slice(0, i);
+ out[i] = new Range(newAnchor, newHead);
+ }
+ }
+ return out ? normalizeSelection(out, sel.primIndex) : sel;
+ }
+
+ function skipAtomicInner(doc, pos, oldPos, dir, mayClear) {
+ var line = getLine(doc, pos.line);
+ if (line.markedSpans) for (var i = 0; i < line.markedSpans.length; ++i) {
+ var sp = line.markedSpans[i], m = sp.marker;
+ if ((sp.from == null || (m.inclusiveLeft ? sp.from <= pos.ch : sp.from < pos.ch)) &&
+ (sp.to == null || (m.inclusiveRight ? sp.to >= pos.ch : sp.to > pos.ch))) {
+ if (mayClear) {
+ signal(m, "beforeCursorEnter");
+ if (m.explicitlyCleared) {
+ if (!line.markedSpans) break;
+ else {--i; continue;}
+ }
+ }
+ if (!m.atomic) continue;
+
+ if (oldPos) {
+ var near = m.find(dir < 0 ? 1 : -1), diff;
+ if (dir < 0 ? m.inclusiveRight : m.inclusiveLeft)
+ near = movePos(doc, near, -dir, near && near.line == pos.line ? line : null);
+ if (near && near.line == pos.line && (diff = cmp(near, oldPos)) && (dir < 0 ? diff < 0 : diff > 0))
+ return skipAtomicInner(doc, near, pos, dir, mayClear);
+ }
+
+ var far = m.find(dir < 0 ? -1 : 1);
+ if (dir < 0 ? m.inclusiveLeft : m.inclusiveRight)
+ far = movePos(doc, far, dir, far.line == pos.line ? line : null);
+ return far ? skipAtomicInner(doc, far, pos, dir, mayClear) : null;
+ }
+ }
+ return pos;
+ }
+
+ // Ensure a given position is not inside an atomic range.
+ function skipAtomic(doc, pos, oldPos, bias, mayClear) {
+ var dir = bias || 1;
+ var found = skipAtomicInner(doc, pos, oldPos, dir, mayClear) ||
+ (!mayClear && skipAtomicInner(doc, pos, oldPos, dir, true)) ||
+ skipAtomicInner(doc, pos, oldPos, -dir, mayClear) ||
+ (!mayClear && skipAtomicInner(doc, pos, oldPos, -dir, true));
+ if (!found) {
+ doc.cantEdit = true;
+ return Pos(doc.first, 0);
+ }
+ return found;
+ }
+
+ function movePos(doc, pos, dir, line) {
+ if (dir < 0 && pos.ch == 0) {
+ if (pos.line > doc.first) return clipPos(doc, Pos(pos.line - 1));
+ else return null;
+ } else if (dir > 0 && pos.ch == (line || getLine(doc, pos.line)).text.length) {
+ if (pos.line < doc.first + doc.size - 1) return Pos(pos.line + 1, 0);
+ else return null;
+ } else {
+ return new Pos(pos.line, pos.ch + dir);
+ }
+ }
+
+ // SELECTION DRAWING
+
+ function updateSelection(cm) {
+ cm.display.input.showSelection(cm.display.input.prepareSelection());
+ }
+
+ function prepareSelection(cm, primary) {
+ var doc = cm.doc, result = {};
+ var curFragment = result.cursors = document.createDocumentFragment();
+ var selFragment = result.selection = document.createDocumentFragment();
+
+ for (var i = 0; i < doc.sel.ranges.length; i++) {
+ if (primary === false && i == doc.sel.primIndex) continue;
+ var range = doc.sel.ranges[i];
+ if (range.from().line >= cm.display.viewTo || range.to().line < cm.display.viewFrom) continue;
+ var collapsed = range.empty();
+ if (collapsed || cm.options.showCursorWhenSelecting)
+ drawSelectionCursor(cm, range.head, curFragment);
+ if (!collapsed)
+ drawSelectionRange(cm, range, selFragment);
+ }
+ return result;
+ }
+
+ // Draws a cursor for the given range
+ function drawSelectionCursor(cm, head, output) {
+ var pos = cursorCoords(cm, head, "div", null, null, !cm.options.singleCursorHeightPerLine);
+
+ var cursor = output.appendChild(elt("div", "\u00a0", "CodeMirror-cursor"));
+ cursor.style.left = pos.left + "px";
+ cursor.style.top = pos.top + "px";
+ cursor.style.height = Math.max(0, pos.bottom - pos.top) * cm.options.cursorHeight + "px";
+
+ if (pos.other) {
+ // Secondary cursor, shown when on a 'jump' in bi-directional text
+ var otherCursor = output.appendChild(elt("div", "\u00a0", "CodeMirror-cursor CodeMirror-secondarycursor"));
+ otherCursor.style.display = "";
+ otherCursor.style.left = pos.other.left + "px";
+ otherCursor.style.top = pos.other.top + "px";
+ otherCursor.style.height = (pos.other.bottom - pos.other.top) * .85 + "px";
+ }
+ }
+
+ // Draws the given range as a highlighted selection
+ function drawSelectionRange(cm, range, output) {
+ var display = cm.display, doc = cm.doc;
+ var fragment = document.createDocumentFragment();
+ var padding = paddingH(cm.display), leftSide = padding.left;
+ var rightSide = Math.max(display.sizerWidth, displayWidth(cm) - display.sizer.offsetLeft) - padding.right;
+
+ function add(left, top, width, bottom) {
+ if (top < 0) top = 0;
+ top = Math.round(top);
+ bottom = Math.round(bottom);
+ fragment.appendChild(elt("div", null, "CodeMirror-selected", "position: absolute; left: " + left +
+ "px; top: " + top + "px; width: " + (width == null ? rightSide - left : width) +
+ "px; height: " + (bottom - top) + "px"));
+ }
+
+ function drawForLine(line, fromArg, toArg) {
+ var lineObj = getLine(doc, line);
+ var lineLen = lineObj.text.length;
+ var start, end;
+ function coords(ch, bias) {
+ return charCoords(cm, Pos(line, ch), "div", lineObj, bias);
+ }
+
+ iterateBidiSections(getOrder(lineObj), fromArg || 0, toArg == null ? lineLen : toArg, function(from, to, dir) {
+ var leftPos = coords(from, "left"), rightPos, left, right;
+ if (from == to) {
+ rightPos = leftPos;
+ left = right = leftPos.left;
+ } else {
+ rightPos = coords(to - 1, "right");
+ if (dir == "rtl") { var tmp = leftPos; leftPos = rightPos; rightPos = tmp; }
+ left = leftPos.left;
+ right = rightPos.right;
+ }
+ if (fromArg == null && from == 0) left = leftSide;
+ if (rightPos.top - leftPos.top > 3) { // Different lines, draw top part
+ add(left, leftPos.top, null, leftPos.bottom);
+ left = leftSide;
+ if (leftPos.bottom < rightPos.top) add(left, leftPos.bottom, null, rightPos.top);
+ }
+ if (toArg == null && to == lineLen) right = rightSide;
+ if (!start || leftPos.top < start.top || leftPos.top == start.top && leftPos.left < start.left)
+ start = leftPos;
+ if (!end || rightPos.bottom > end.bottom || rightPos.bottom == end.bottom && rightPos.right > end.right)
+ end = rightPos;
+ if (left < leftSide + 1) left = leftSide;
+ add(left, rightPos.top, right - left, rightPos.bottom);
+ });
+ return {start: start, end: end};
+ }
+
+ var sFrom = range.from(), sTo = range.to();
+ if (sFrom.line == sTo.line) {
+ drawForLine(sFrom.line, sFrom.ch, sTo.ch);
+ } else {
+ var fromLine = getLine(doc, sFrom.line), toLine = getLine(doc, sTo.line);
+ var singleVLine = visualLine(fromLine) == visualLine(toLine);
+ var leftEnd = drawForLine(sFrom.line, sFrom.ch, singleVLine ? fromLine.text.length + 1 : null).end;
+ var rightStart = drawForLine(sTo.line, singleVLine ? 0 : null, sTo.ch).start;
+ if (singleVLine) {
+ if (leftEnd.top < rightStart.top - 2) {
+ add(leftEnd.right, leftEnd.top, null, leftEnd.bottom);
+ add(leftSide, rightStart.top, rightStart.left, rightStart.bottom);
+ } else {
+ add(leftEnd.right, leftEnd.top, rightStart.left - leftEnd.right, leftEnd.bottom);
+ }
+ }
+ if (leftEnd.bottom < rightStart.top)
+ add(leftSide, leftEnd.bottom, null, rightStart.top);
+ }
+
+ output.appendChild(fragment);
+ }
+
+ // Cursor-blinking
+ function restartBlink(cm) {
+ if (!cm.state.focused) return;
+ var display = cm.display;
+ clearInterval(display.blinker);
+ var on = true;
+ display.cursorDiv.style.visibility = "";
+ if (cm.options.cursorBlinkRate > 0)
+ display.blinker = setInterval(function() {
+ display.cursorDiv.style.visibility = (on = !on) ? "" : "hidden";
+ }, cm.options.cursorBlinkRate);
+ else if (cm.options.cursorBlinkRate < 0)
+ display.cursorDiv.style.visibility = "hidden";
+ }
+
+ // HIGHLIGHT WORKER
+
+ function startWorker(cm, time) {
+ if (cm.doc.mode.startState && cm.doc.frontier < cm.display.viewTo)
+ cm.state.highlight.set(time, bind(highlightWorker, cm));
+ }
+
+ function highlightWorker(cm) {
+ var doc = cm.doc;
+ if (doc.frontier < doc.first) doc.frontier = doc.first;
+ if (doc.frontier >= cm.display.viewTo) return;
+ var end = +new Date + cm.options.workTime;
+ var state = copyState(doc.mode, getStateBefore(cm, doc.frontier));
+ var changedLines = [];
+
+ doc.iter(doc.frontier, Math.min(doc.first + doc.size, cm.display.viewTo + 500), function(line) {
+ if (doc.frontier >= cm.display.viewFrom) { // Visible
+ var oldStyles = line.styles, tooLong = line.text.length > cm.options.maxHighlightLength;
+ var highlighted = highlightLine(cm, line, tooLong ? copyState(doc.mode, state) : state, true);
+ line.styles = highlighted.styles;
+ var oldCls = line.styleClasses, newCls = highlighted.classes;
+ if (newCls) line.styleClasses = newCls;
+ else if (oldCls) line.styleClasses = null;
+ var ischange = !oldStyles || oldStyles.length != line.styles.length ||
+ oldCls != newCls && (!oldCls || !newCls || oldCls.bgClass != newCls.bgClass || oldCls.textClass != newCls.textClass);
+ for (var i = 0; !ischange && i < oldStyles.length; ++i) ischange = oldStyles[i] != line.styles[i];
+ if (ischange) changedLines.push(doc.frontier);
+ line.stateAfter = tooLong ? state : copyState(doc.mode, state);
+ } else {
+ if (line.text.length <= cm.options.maxHighlightLength)
+ processLine(cm, line.text, state);
+ line.stateAfter = doc.frontier % 5 == 0 ? copyState(doc.mode, state) : null;
+ }
+ ++doc.frontier;
+ if (+new Date > end) {
+ startWorker(cm, cm.options.workDelay);
+ return true;
+ }
+ });
+ if (changedLines.length) runInOp(cm, function() {
+ for (var i = 0; i < changedLines.length; i++)
+ regLineChange(cm, changedLines[i], "text");
+ });
+ }
+
+ // Finds the line to start with when starting a parse. Tries to
+ // find a line with a stateAfter, so that it can start with a
+ // valid state. If that fails, it returns the line with the
+ // smallest indentation, which tends to need the least context to
+ // parse correctly.
+ function findStartLine(cm, n, precise) {
+ var minindent, minline, doc = cm.doc;
+ var lim = precise ? -1 : n - (cm.doc.mode.innerMode ? 1000 : 100);
+ for (var search = n; search > lim; --search) {
+ if (search <= doc.first) return doc.first;
+ var line = getLine(doc, search - 1);
+ if (line.stateAfter && (!precise || search <= doc.frontier)) return search;
+ var indented = countColumn(line.text, null, cm.options.tabSize);
+ if (minline == null || minindent > indented) {
+ minline = search - 1;
+ minindent = indented;
+ }
+ }
+ return minline;
+ }
+
+ function getStateBefore(cm, n, precise) {
+ var doc = cm.doc, display = cm.display;
+ if (!doc.mode.startState) return true;
+ var pos = findStartLine(cm, n, precise), state = pos > doc.first && getLine(doc, pos-1).stateAfter;
+ if (!state) state = startState(doc.mode);
+ else state = copyState(doc.mode, state);
+ doc.iter(pos, n, function(line) {
+ processLine(cm, line.text, state);
+ var save = pos == n - 1 || pos % 5 == 0 || pos >= display.viewFrom && pos < display.viewTo;
+ line.stateAfter = save ? copyState(doc.mode, state) : null;
+ ++pos;
+ });
+ if (precise) doc.frontier = pos;
+ return state;
+ }
+
+ // POSITION MEASUREMENT
+
+ function paddingTop(display) {return display.lineSpace.offsetTop;}
+ function paddingVert(display) {return display.mover.offsetHeight - display.lineSpace.offsetHeight;}
+ function paddingH(display) {
+ if (display.cachedPaddingH) return display.cachedPaddingH;
+ var e = removeChildrenAndAdd(display.measure, elt("pre", "x"));
+ var style = window.getComputedStyle ? window.getComputedStyle(e) : e.currentStyle;
+ var data = {left: parseInt(style.paddingLeft), right: parseInt(style.paddingRight)};
+ if (!isNaN(data.left) && !isNaN(data.right)) display.cachedPaddingH = data;
+ return data;
+ }
+
+ function scrollGap(cm) { return scrollerGap - cm.display.nativeBarWidth; }
+ function displayWidth(cm) {
+ return cm.display.scroller.clientWidth - scrollGap(cm) - cm.display.barWidth;
+ }
+ function displayHeight(cm) {
+ return cm.display.scroller.clientHeight - scrollGap(cm) - cm.display.barHeight;
+ }
+
+ // Ensure the lineView.wrapping.heights array is populated. This is
+ // an array of bottom offsets for the lines that make up a drawn
+ // line. When lineWrapping is on, there might be more than one
+ // height.
+ function ensureLineHeights(cm, lineView, rect) {
+ var wrapping = cm.options.lineWrapping;
+ var curWidth = wrapping && displayWidth(cm);
+ if (!lineView.measure.heights || wrapping && lineView.measure.width != curWidth) {
+ var heights = lineView.measure.heights = [];
+ if (wrapping) {
+ lineView.measure.width = curWidth;
+ var rects = lineView.text.firstChild.getClientRects();
+ for (var i = 0; i < rects.length - 1; i++) {
+ var cur = rects[i], next = rects[i + 1];
+ if (Math.abs(cur.bottom - next.bottom) > 2)
+ heights.push((cur.bottom + next.top) / 2 - rect.top);
+ }
+ }
+ heights.push(rect.bottom - rect.top);
+ }
+ }
+
+ // Find a line map (mapping character offsets to text nodes) and a
+ // measurement cache for the given line number. (A line view might
+ // contain multiple lines when collapsed ranges are present.)
+ function mapFromLineView(lineView, line, lineN) {
+ if (lineView.line == line)
+ return {map: lineView.measure.map, cache: lineView.measure.cache};
+ for (var i = 0; i < lineView.rest.length; i++)
+ if (lineView.rest[i] == line)
+ return {map: lineView.measure.maps[i], cache: lineView.measure.caches[i]};
+ for (var i = 0; i < lineView.rest.length; i++)
+ if (lineNo(lineView.rest[i]) > lineN)
+ return {map: lineView.measure.maps[i], cache: lineView.measure.caches[i], before: true};
+ }
+
+ // Render a line into the hidden node display.externalMeasured. Used
+ // when measurement is needed for a line that's not in the viewport.
+ function updateExternalMeasurement(cm, line) {
+ line = visualLine(line);
+ var lineN = lineNo(line);
+ var view = cm.display.externalMeasured = new LineView(cm.doc, line, lineN);
+ view.lineN = lineN;
+ var built = view.built = buildLineContent(cm, view);
+ view.text = built.pre;
+ removeChildrenAndAdd(cm.display.lineMeasure, built.pre);
+ return view;
+ }
+
+ // Get a {top, bottom, left, right} box (in line-local coordinates)
+ // for a given character.
+ function measureChar(cm, line, ch, bias) {
+ return measureCharPrepared(cm, prepareMeasureForLine(cm, line), ch, bias);
+ }
+
+ // Find a line view that corresponds to the given line number.
+ function findViewForLine(cm, lineN) {
+ if (lineN >= cm.display.viewFrom && lineN < cm.display.viewTo)
+ return cm.display.view[findViewIndex(cm, lineN)];
+ var ext = cm.display.externalMeasured;
+ if (ext && lineN >= ext.lineN && lineN < ext.lineN + ext.size)
+ return ext;
+ }
+
+ // Measurement can be split in two steps, the set-up work that
+ // applies to the whole line, and the measurement of the actual
+ // character. Functions like coordsChar, that need to do a lot of
+ // measurements in a row, can thus ensure that the set-up work is
+ // only done once.
+ function prepareMeasureForLine(cm, line) {
+ var lineN = lineNo(line);
+ var view = findViewForLine(cm, lineN);
+ if (view && !view.text) {
+ view = null;
+ } else if (view && view.changes) {
+ updateLineForChanges(cm, view, lineN, getDimensions(cm));
+ cm.curOp.forceUpdate = true;
+ }
+ if (!view)
+ view = updateExternalMeasurement(cm, line);
+
+ var info = mapFromLineView(view, line, lineN);
+ return {
+ line: line, view: view, rect: null,
+ map: info.map, cache: info.cache, before: info.before,
+ hasHeights: false
+ };
+ }
+
+ // Given a prepared measurement object, measures the position of an
+ // actual character (or fetches it from the cache).
+ function measureCharPrepared(cm, prepared, ch, bias, varHeight) {
+ if (prepared.before) ch = -1;
+ var key = ch + (bias || ""), found;
+ if (prepared.cache.hasOwnProperty(key)) {
+ found = prepared.cache[key];
+ } else {
+ if (!prepared.rect)
+ prepared.rect = prepared.view.text.getBoundingClientRect();
+ if (!prepared.hasHeights) {
+ ensureLineHeights(cm, prepared.view, prepared.rect);
+ prepared.hasHeights = true;
+ }
+ found = measureCharInner(cm, prepared, ch, bias);
+ if (!found.bogus) prepared.cache[key] = found;
+ }
+ return {left: found.left, right: found.right,
+ top: varHeight ? found.rtop : found.top,
+ bottom: varHeight ? found.rbottom : found.bottom};
+ }
+
+ var nullRect = {left: 0, right: 0, top: 0, bottom: 0};
+
+ function nodeAndOffsetInLineMap(map, ch, bias) {
+ var node, start, end, collapse;
+ // First, search the line map for the text node corresponding to,
+ // or closest to, the target character.
+ for (var i = 0; i < map.length; i += 3) {
+ var mStart = map[i], mEnd = map[i + 1];
+ if (ch < mStart) {
+ start = 0; end = 1;
+ collapse = "left";
+ } else if (ch < mEnd) {
+ start = ch - mStart;
+ end = start + 1;
+ } else if (i == map.length - 3 || ch == mEnd && map[i + 3] > ch) {
+ end = mEnd - mStart;
+ start = end - 1;
+ if (ch >= mEnd) collapse = "right";
+ }
+ if (start != null) {
+ node = map[i + 2];
+ if (mStart == mEnd && bias == (node.insertLeft ? "left" : "right"))
+ collapse = bias;
+ if (bias == "left" && start == 0)
+ while (i && map[i - 2] == map[i - 3] && map[i - 1].insertLeft) {
+ node = map[(i -= 3) + 2];
+ collapse = "left";
+ }
+ if (bias == "right" && start == mEnd - mStart)
+ while (i < map.length - 3 && map[i + 3] == map[i + 4] && !map[i + 5].insertLeft) {
+ node = map[(i += 3) + 2];
+ collapse = "right";
+ }
+ break;
+ }
+ }
+ return {node: node, start: start, end: end, collapse: collapse, coverStart: mStart, coverEnd: mEnd};
+ }
+
+ function measureCharInner(cm, prepared, ch, bias) {
+ var place = nodeAndOffsetInLineMap(prepared.map, ch, bias);
+ var node = place.node, start = place.start, end = place.end, collapse = place.collapse;
+
+ var rect;
+ if (node.nodeType == 3) { // If it is a text node, use a range to retrieve the coordinates.
+ for (var i = 0; i < 4; i++) { // Retry a maximum of 4 times when nonsense rectangles are returned
+ while (start && isExtendingChar(prepared.line.text.charAt(place.coverStart + start))) --start;
+ while (place.coverStart + end < place.coverEnd && isExtendingChar(prepared.line.text.charAt(place.coverStart + end))) ++end;
+ if (ie && ie_version < 9 && start == 0 && end == place.coverEnd - place.coverStart) {
+ rect = node.parentNode.getBoundingClientRect();
+ } else if (ie && cm.options.lineWrapping) {
+ var rects = range(node, start, end).getClientRects();
+ if (rects.length)
+ rect = rects[bias == "right" ? rects.length - 1 : 0];
+ else
+ rect = nullRect;
+ } else {
+ rect = range(node, start, end).getBoundingClientRect() || nullRect;
+ }
+ if (rect.left || rect.right || start == 0) break;
+ end = start;
+ start = start - 1;
+ collapse = "right";
+ }
+ if (ie && ie_version < 11) rect = maybeUpdateRectForZooming(cm.display.measure, rect);
+ } else { // If it is a widget, simply get the box for the whole widget.
+ if (start > 0) collapse = bias = "right";
+ var rects;
+ if (cm.options.lineWrapping && (rects = node.getClientRects()).length > 1)
+ rect = rects[bias == "right" ? rects.length - 1 : 0];
+ else
+ rect = node.getBoundingClientRect();
+ }
+ if (ie && ie_version < 9 && !start && (!rect || !rect.left && !rect.right)) {
+ var rSpan = node.parentNode.getClientRects()[0];
+ if (rSpan)
+ rect = {left: rSpan.left, right: rSpan.left + charWidth(cm.display), top: rSpan.top, bottom: rSpan.bottom};
+ else
+ rect = nullRect;
+ }
+
+ var rtop = rect.top - prepared.rect.top, rbot = rect.bottom - prepared.rect.top;
+ var mid = (rtop + rbot) / 2;
+ var heights = prepared.view.measure.heights;
+ for (var i = 0; i < heights.length - 1; i++)
+ if (mid < heights[i]) break;
+ var top = i ? heights[i - 1] : 0, bot = heights[i];
+ var result = {left: (collapse == "right" ? rect.right : rect.left) - prepared.rect.left,
+ right: (collapse == "left" ? rect.left : rect.right) - prepared.rect.left,
+ top: top, bottom: bot};
+ if (!rect.left && !rect.right) result.bogus = true;
+ if (!cm.options.singleCursorHeightPerLine) { result.rtop = rtop; result.rbottom = rbot; }
+
+ return result;
+ }
+
+ // Work around problem with bounding client rects on ranges being
+ // returned incorrectly when zoomed on IE10 and below.
+ function maybeUpdateRectForZooming(measure, rect) {
+ if (!window.screen || screen.logicalXDPI == null ||
+ screen.logicalXDPI == screen.deviceXDPI || !hasBadZoomedRects(measure))
+ return rect;
+ var scaleX = screen.logicalXDPI / screen.deviceXDPI;
+ var scaleY = screen.logicalYDPI / screen.deviceYDPI;
+ return {left: rect.left * scaleX, right: rect.right * scaleX,
+ top: rect.top * scaleY, bottom: rect.bottom * scaleY};
+ }
+
+ function clearLineMeasurementCacheFor(lineView) {
+ if (lineView.measure) {
+ lineView.measure.cache = {};
+ lineView.measure.heights = null;
+ if (lineView.rest) for (var i = 0; i < lineView.rest.length; i++)
+ lineView.measure.caches[i] = {};
+ }
+ }
+
+ function clearLineMeasurementCache(cm) {
+ cm.display.externalMeasure = null;
+ removeChildren(cm.display.lineMeasure);
+ for (var i = 0; i < cm.display.view.length; i++)
+ clearLineMeasurementCacheFor(cm.display.view[i]);
+ }
+
+ function clearCaches(cm) {
+ clearLineMeasurementCache(cm);
+ cm.display.cachedCharWidth = cm.display.cachedTextHeight = cm.display.cachedPaddingH = null;
+ if (!cm.options.lineWrapping) cm.display.maxLineChanged = true;
+ cm.display.lineNumChars = null;
+ }
+
+ function pageScrollX() { return window.pageXOffset || (document.documentElement || document.body).scrollLeft; }
+ function pageScrollY() { return window.pageYOffset || (document.documentElement || document.body).scrollTop; }
+
+ // Converts a {top, bottom, left, right} box from line-local
+ // coordinates into another coordinate system. Context may be one of
+ // "line", "div" (display.lineDiv), "local"/null (editor), "window",
+ // or "page".
+ function intoCoordSystem(cm, lineObj, rect, context) {
+ if (lineObj.widgets) for (var i = 0; i < lineObj.widgets.length; ++i) if (lineObj.widgets[i].above) {
+ var size = widgetHeight(lineObj.widgets[i]);
+ rect.top += size; rect.bottom += size;
+ }
+ if (context == "line") return rect;
+ if (!context) context = "local";
+ var yOff = heightAtLine(lineObj);
+ if (context == "local") yOff += paddingTop(cm.display);
+ else yOff -= cm.display.viewOffset;
+ if (context == "page" || context == "window") {
+ var lOff = cm.display.lineSpace.getBoundingClientRect();
+ yOff += lOff.top + (context == "window" ? 0 : pageScrollY());
+ var xOff = lOff.left + (context == "window" ? 0 : pageScrollX());
+ rect.left += xOff; rect.right += xOff;
+ }
+ rect.top += yOff; rect.bottom += yOff;
+ return rect;
+ }
+
+ // Coverts a box from "div" coords to another coordinate system.
+ // Context may be "window", "page", "div", or "local"/null.
+ function fromCoordSystem(cm, coords, context) {
+ if (context == "div") return coords;
+ var left = coords.left, top = coords.top;
+ // First move into "page" coordinate system
+ if (context == "page") {
+ left -= pageScrollX();
+ top -= pageScrollY();
+ } else if (context == "local" || !context) {
+ var localBox = cm.display.sizer.getBoundingClientRect();
+ left += localBox.left;
+ top += localBox.top;
+ }
+
+ var lineSpaceBox = cm.display.lineSpace.getBoundingClientRect();
+ return {left: left - lineSpaceBox.left, top: top - lineSpaceBox.top};
+ }
+
+ function charCoords(cm, pos, context, lineObj, bias) {
+ if (!lineObj) lineObj = getLine(cm.doc, pos.line);
+ return intoCoordSystem(cm, lineObj, measureChar(cm, lineObj, pos.ch, bias), context);
+ }
+
+ // Returns a box for a given cursor position, which may have an
+ // 'other' property containing the position of the secondary cursor
+ // on a bidi boundary.
+ function cursorCoords(cm, pos, context, lineObj, preparedMeasure, varHeight) {
+ lineObj = lineObj || getLine(cm.doc, pos.line);
+ if (!preparedMeasure) preparedMeasure = prepareMeasureForLine(cm, lineObj);
+ function get(ch, right) {
+ var m = measureCharPrepared(cm, preparedMeasure, ch, right ? "right" : "left", varHeight);
+ if (right) m.left = m.right; else m.right = m.left;
+ return intoCoordSystem(cm, lineObj, m, context);
+ }
+ function getBidi(ch, partPos) {
+ var part = order[partPos], right = part.level % 2;
+ if (ch == bidiLeft(part) && partPos && part.level < order[partPos - 1].level) {
+ part = order[--partPos];
+ ch = bidiRight(part) - (part.level % 2 ? 0 : 1);
+ right = true;
+ } else if (ch == bidiRight(part) && partPos < order.length - 1 && part.level < order[partPos + 1].level) {
+ part = order[++partPos];
+ ch = bidiLeft(part) - part.level % 2;
+ right = false;
+ }
+ if (right && ch == part.to && ch > part.from) return get(ch - 1);
+ return get(ch, right);
+ }
+ var order = getOrder(lineObj), ch = pos.ch;
+ if (!order) return get(ch);
+ var partPos = getBidiPartAt(order, ch);
+ var val = getBidi(ch, partPos);
+ if (bidiOther != null) val.other = getBidi(ch, bidiOther);
+ return val;
+ }
+
+ // Used to cheaply estimate the coordinates for a position. Used for
+ // intermediate scroll updates.
+ function estimateCoords(cm, pos) {
+ var left = 0, pos = clipPos(cm.doc, pos);
+ if (!cm.options.lineWrapping) left = charWidth(cm.display) * pos.ch;
+ var lineObj = getLine(cm.doc, pos.line);
+ var top = heightAtLine(lineObj) + paddingTop(cm.display);
+ return {left: left, right: left, top: top, bottom: top + lineObj.height};
+ }
+
+ // Positions returned by coordsChar contain some extra information.
+ // xRel is the relative x position of the input coordinates compared
+ // to the found position (so xRel > 0 means the coordinates are to
+ // the right of the character position, for example). When outside
+ // is true, that means the coordinates lie outside the line's
+ // vertical range.
+ function PosWithInfo(line, ch, outside, xRel) {
+ var pos = Pos(line, ch);
+ pos.xRel = xRel;
+ if (outside) pos.outside = true;
+ return pos;
+ }
+
+ // Compute the character position closest to the given coordinates.
+ // Input must be lineSpace-local ("div" coordinate system).
+ function coordsChar(cm, x, y) {
+ var doc = cm.doc;
+ y += cm.display.viewOffset;
+ if (y < 0) return PosWithInfo(doc.first, 0, true, -1);
+ var lineN = lineAtHeight(doc, y), last = doc.first + doc.size - 1;
+ if (lineN > last)
+ return PosWithInfo(doc.first + doc.size - 1, getLine(doc, last).text.length, true, 1);
+ if (x < 0) x = 0;
+
+ var lineObj = getLine(doc, lineN);
+ for (;;) {
+ var found = coordsCharInner(cm, lineObj, lineN, x, y);
+ var merged = collapsedSpanAtEnd(lineObj);
+ var mergedPos = merged && merged.find(0, true);
+ if (merged && (found.ch > mergedPos.from.ch || found.ch == mergedPos.from.ch && found.xRel > 0))
+ lineN = lineNo(lineObj = mergedPos.to.line);
+ else
+ return found;
+ }
+ }
+
+ function coordsCharInner(cm, lineObj, lineNo, x, y) {
+ var innerOff = y - heightAtLine(lineObj);
+ var wrongLine = false, adjust = 2 * cm.display.wrapper.clientWidth;
+ var preparedMeasure = prepareMeasureForLine(cm, lineObj);
+
+ function getX(ch) {
+ var sp = cursorCoords(cm, Pos(lineNo, ch), "line", lineObj, preparedMeasure);
+ wrongLine = true;
+ if (innerOff > sp.bottom) return sp.left - adjust;
+ else if (innerOff < sp.top) return sp.left + adjust;
+ else wrongLine = false;
+ return sp.left;
+ }
+
+ var bidi = getOrder(lineObj), dist = lineObj.text.length;
+ var from = lineLeft(lineObj), to = lineRight(lineObj);
+ var fromX = getX(from), fromOutside = wrongLine, toX = getX(to), toOutside = wrongLine;
+
+ if (x > toX) return PosWithInfo(lineNo, to, toOutside, 1);
+ // Do a binary search between these bounds.
+ for (;;) {
+ if (bidi ? to == from || to == moveVisually(lineObj, from, 1) : to - from <= 1) {
+ var ch = x < fromX || x - fromX <= toX - x ? from : to;
+ var outside = ch == from ? fromOutside : toOutside
+ var xDiff = x - (ch == from ? fromX : toX);
+ // This is a kludge to handle the case where the coordinates
+ // are after a line-wrapped line. We should replace it with a
+ // more general handling of cursor positions around line
+ // breaks. (Issue #4078)
+ if (toOutside && !bidi && !/\s/.test(lineObj.text.charAt(ch)) && xDiff > 0 &&
+ ch < lineObj.text.length && preparedMeasure.view.measure.heights.length > 1) {
+ var charSize = measureCharPrepared(cm, preparedMeasure, ch, "right");
+ if (innerOff <= charSize.bottom && innerOff >= charSize.top && Math.abs(x - charSize.right) < xDiff) {
+ outside = false
+ ch++
+ xDiff = x - charSize.right
+ }
+ }
+ while (isExtendingChar(lineObj.text.charAt(ch))) ++ch;
+ var pos = PosWithInfo(lineNo, ch, outside, xDiff < -1 ? -1 : xDiff > 1 ? 1 : 0);
+ return pos;
+ }
+ var step = Math.ceil(dist / 2), middle = from + step;
+ if (bidi) {
+ middle = from;
+ for (var i = 0; i < step; ++i) middle = moveVisually(lineObj, middle, 1);
+ }
+ var middleX = getX(middle);
+ if (middleX > x) {to = middle; toX = middleX; if (toOutside = wrongLine) toX += 1000; dist = step;}
+ else {from = middle; fromX = middleX; fromOutside = wrongLine; dist -= step;}
+ }
+ }
+
+ var measureText;
+ // Compute the default text height.
+ function textHeight(display) {
+ if (display.cachedTextHeight != null) return display.cachedTextHeight;
+ if (measureText == null) {
+ measureText = elt("pre");
+ // Measure a bunch of lines, for browsers that compute
+ // fractional heights.
+ for (var i = 0; i < 49; ++i) {
+ measureText.appendChild(document.createTextNode("x"));
+ measureText.appendChild(elt("br"));
+ }
+ measureText.appendChild(document.createTextNode("x"));
+ }
+ removeChildrenAndAdd(display.measure, measureText);
+ var height = measureText.offsetHeight / 50;
+ if (height > 3) display.cachedTextHeight = height;
+ removeChildren(display.measure);
+ return height || 1;
+ }
+
+ // Compute the default character width.
+ function charWidth(display) {
+ if (display.cachedCharWidth != null) return display.cachedCharWidth;
+ var anchor = elt("span", "xxxxxxxxxx");
+ var pre = elt("pre", [anchor]);
+ removeChildrenAndAdd(display.measure, pre);
+ var rect = anchor.getBoundingClientRect(), width = (rect.right - rect.left) / 10;
+ if (width > 2) display.cachedCharWidth = width;
+ return width || 10;
+ }
+
+ // OPERATIONS
+
+ // Operations are used to wrap a series of changes to the editor
+ // state in such a way that each change won't have to update the
+ // cursor and display (which would be awkward, slow, and
+ // error-prone). Instead, display updates are batched and then all
+ // combined and executed at once.
+
+ var operationGroup = null;
+
+ var nextOpId = 0;
+ // Start a new operation.
+ function startOperation(cm) {
+ cm.curOp = {
+ cm: cm,
+ viewChanged: false, // Flag that indicates that lines might need to be redrawn
+ startHeight: cm.doc.height, // Used to detect need to update scrollbar
+ forceUpdate: false, // Used to force a redraw
+ updateInput: null, // Whether to reset the input textarea
+ typing: false, // Whether this reset should be careful to leave existing text (for compositing)
+ changeObjs: null, // Accumulated changes, for firing change events
+ cursorActivityHandlers: null, // Set of handlers to fire cursorActivity on
+ cursorActivityCalled: 0, // Tracks which cursorActivity handlers have been called already
+ selectionChanged: false, // Whether the selection needs to be redrawn
+ updateMaxLine: false, // Set when the widest line needs to be determined anew
+ scrollLeft: null, scrollTop: null, // Intermediate scroll position, not pushed to DOM yet
+ scrollToPos: null, // Used to scroll to a specific position
+ focus: false,
+ id: ++nextOpId // Unique ID
+ };
+ if (operationGroup) {
+ operationGroup.ops.push(cm.curOp);
+ } else {
+ cm.curOp.ownsGroup = operationGroup = {
+ ops: [cm.curOp],
+ delayedCallbacks: []
+ };
+ }
+ }
+
+ function fireCallbacksForOps(group) {
+ // Calls delayed callbacks and cursorActivity handlers until no
+ // new ones appear
+ var callbacks = group.delayedCallbacks, i = 0;
+ do {
+ for (; i < callbacks.length; i++)
+ callbacks[i].call(null);
+ for (var j = 0; j < group.ops.length; j++) {
+ var op = group.ops[j];
+ if (op.cursorActivityHandlers)
+ while (op.cursorActivityCalled < op.cursorActivityHandlers.length)
+ op.cursorActivityHandlers[op.cursorActivityCalled++].call(null, op.cm);
+ }
+ } while (i < callbacks.length);
+ }
+
+ // Finish an operation, updating the display and signalling delayed events
+ function endOperation(cm) {
+ var op = cm.curOp, group = op.ownsGroup;
+ if (!group) return;
+
+ try { fireCallbacksForOps(group); }
+ finally {
+ operationGroup = null;
+ for (var i = 0; i < group.ops.length; i++)
+ group.ops[i].cm.curOp = null;
+ endOperations(group);
+ }
+ }
+
+ // The DOM updates done when an operation finishes are batched so
+ // that the minimum number of relayouts are required.
+ function endOperations(group) {
+ var ops = group.ops;
+ for (var i = 0; i < ops.length; i++) // Read DOM
+ endOperation_R1(ops[i]);
+ for (var i = 0; i < ops.length; i++) // Write DOM (maybe)
+ endOperation_W1(ops[i]);
+ for (var i = 0; i < ops.length; i++) // Read DOM
+ endOperation_R2(ops[i]);
+ for (var i = 0; i < ops.length; i++) // Write DOM (maybe)
+ endOperation_W2(ops[i]);
+ for (var i = 0; i < ops.length; i++) // Read DOM
+ endOperation_finish(ops[i]);
+ }
+
+ function endOperation_R1(op) {
+ var cm = op.cm, display = cm.display;
+ maybeClipScrollbars(cm);
+ if (op.updateMaxLine) findMaxLine(cm);
+
+ op.mustUpdate = op.viewChanged || op.forceUpdate || op.scrollTop != null ||
+ op.scrollToPos && (op.scrollToPos.from.line < display.viewFrom ||
+ op.scrollToPos.to.line >= display.viewTo) ||
+ display.maxLineChanged && cm.options.lineWrapping;
+ op.update = op.mustUpdate &&
+ new DisplayUpdate(cm, op.mustUpdate && {top: op.scrollTop, ensure: op.scrollToPos}, op.forceUpdate);
+ }
+
+ function endOperation_W1(op) {
+ op.updatedDisplay = op.mustUpdate && updateDisplayIfNeeded(op.cm, op.update);
+ }
+
+ function endOperation_R2(op) {
+ var cm = op.cm, display = cm.display;
+ if (op.updatedDisplay) updateHeightsInViewport(cm);
+
+ op.barMeasure = measureForScrollbars(cm);
+
+ // If the max line changed since it was last measured, measure it,
+ // and ensure the document's width matches it.
+ // updateDisplay_W2 will use these properties to do the actual resizing
+ if (display.maxLineChanged && !cm.options.lineWrapping) {
+ op.adjustWidthTo = measureChar(cm, display.maxLine, display.maxLine.text.length).left + 3;
+ cm.display.sizerWidth = op.adjustWidthTo;
+ op.barMeasure.scrollWidth =
+ Math.max(display.scroller.clientWidth, display.sizer.offsetLeft + op.adjustWidthTo + scrollGap(cm) + cm.display.barWidth);
+ op.maxScrollLeft = Math.max(0, display.sizer.offsetLeft + op.adjustWidthTo - displayWidth(cm));
+ }
+
+ if (op.updatedDisplay || op.selectionChanged)
+ op.preparedSelection = display.input.prepareSelection(op.focus);
+ }
+
+ function endOperation_W2(op) {
+ var cm = op.cm;
+
+ if (op.adjustWidthTo != null) {
+ cm.display.sizer.style.minWidth = op.adjustWidthTo + "px";
+ if (op.maxScrollLeft < cm.doc.scrollLeft)
+ setScrollLeft(cm, Math.min(cm.display.scroller.scrollLeft, op.maxScrollLeft), true);
+ cm.display.maxLineChanged = false;
+ }
+
+ var takeFocus = op.focus && op.focus == activeElt() && (!document.hasFocus || document.hasFocus())
+ if (op.preparedSelection)
+ cm.display.input.showSelection(op.preparedSelection, takeFocus);
+ if (op.updatedDisplay || op.startHeight != cm.doc.height)
+ updateScrollbars(cm, op.barMeasure);
+ if (op.updatedDisplay)
+ setDocumentHeight(cm, op.barMeasure);
+
+ if (op.selectionChanged) restartBlink(cm);
+
+ if (cm.state.focused && op.updateInput)
+ cm.display.input.reset(op.typing);
+ if (takeFocus) ensureFocus(op.cm);
+ }
+
+ function endOperation_finish(op) {
+ var cm = op.cm, display = cm.display, doc = cm.doc;
+
+ if (op.updatedDisplay) postUpdateDisplay(cm, op.update);
+
+ // Abort mouse wheel delta measurement, when scrolling explicitly
+ if (display.wheelStartX != null && (op.scrollTop != null || op.scrollLeft != null || op.scrollToPos))
+ display.wheelStartX = display.wheelStartY = null;
+
+ // Propagate the scroll position to the actual DOM scroller
+ if (op.scrollTop != null && (display.scroller.scrollTop != op.scrollTop || op.forceScroll)) {
+ doc.scrollTop = Math.max(0, Math.min(display.scroller.scrollHeight - display.scroller.clientHeight, op.scrollTop));
+ display.scrollbars.setScrollTop(doc.scrollTop);
+ display.scroller.scrollTop = doc.scrollTop;
+ }
+ if (op.scrollLeft != null && (display.scroller.scrollLeft != op.scrollLeft || op.forceScroll)) {
+ doc.scrollLeft = Math.max(0, Math.min(display.scroller.scrollWidth - display.scroller.clientWidth, op.scrollLeft));
+ display.scrollbars.setScrollLeft(doc.scrollLeft);
+ display.scroller.scrollLeft = doc.scrollLeft;
+ alignHorizontally(cm);
+ }
+ // If we need to scroll a specific position into view, do so.
+ if (op.scrollToPos) {
+ var coords = scrollPosIntoView(cm, clipPos(doc, op.scrollToPos.from),
+ clipPos(doc, op.scrollToPos.to), op.scrollToPos.margin);
+ if (op.scrollToPos.isCursor && cm.state.focused) maybeScrollWindow(cm, coords);
+ }
+
+ // Fire events for markers that are hidden/unidden by editing or
+ // undoing
+ var hidden = op.maybeHiddenMarkers, unhidden = op.maybeUnhiddenMarkers;
+ if (hidden) for (var i = 0; i < hidden.length; ++i)
+ if (!hidden[i].lines.length) signal(hidden[i], "hide");
+ if (unhidden) for (var i = 0; i < unhidden.length; ++i)
+ if (unhidden[i].lines.length) signal(unhidden[i], "unhide");
+
+ if (display.wrapper.offsetHeight)
+ doc.scrollTop = cm.display.scroller.scrollTop;
+
+ // Fire change events, and delayed event handlers
+ if (op.changeObjs)
+ signal(cm, "changes", cm, op.changeObjs);
+ if (op.update)
+ op.update.finish();
+ }
+
+ // Run the given function in an operation
+ function runInOp(cm, f) {
+ if (cm.curOp) return f();
+ startOperation(cm);
+ try { return f(); }
+ finally { endOperation(cm); }
+ }
+ // Wraps a function in an operation. Returns the wrapped function.
+ function operation(cm, f) {
+ return function() {
+ if (cm.curOp) return f.apply(cm, arguments);
+ startOperation(cm);
+ try { return f.apply(cm, arguments); }
+ finally { endOperation(cm); }
+ };
+ }
+ // Used to add methods to editor and doc instances, wrapping them in
+ // operations.
+ function methodOp(f) {
+ return function() {
+ if (this.curOp) return f.apply(this, arguments);
+ startOperation(this);
+ try { return f.apply(this, arguments); }
+ finally { endOperation(this); }
+ };
+ }
+ function docMethodOp(f) {
+ return function() {
+ var cm = this.cm;
+ if (!cm || cm.curOp) return f.apply(this, arguments);
+ startOperation(cm);
+ try { return f.apply(this, arguments); }
+ finally { endOperation(cm); }
+ };
+ }
+
+ // VIEW TRACKING
+
+ // These objects are used to represent the visible (currently drawn)
+ // part of the document. A LineView may correspond to multiple
+ // logical lines, if those are connected by collapsed ranges.
+ function LineView(doc, line, lineN) {
+ // The starting line
+ this.line = line;
+ // Continuing lines, if any
+ this.rest = visualLineContinued(line);
+ // Number of logical lines in this visual line
+ this.size = this.rest ? lineNo(lst(this.rest)) - lineN + 1 : 1;
+ this.node = this.text = null;
+ this.hidden = lineIsHidden(doc, line);
+ }
+
+ // Create a range of LineView objects for the given lines.
+ function buildViewArray(cm, from, to) {
+ var array = [], nextPos;
+ for (var pos = from; pos < to; pos = nextPos) {
+ var view = new LineView(cm.doc, getLine(cm.doc, pos), pos);
+ nextPos = pos + view.size;
+ array.push(view);
+ }
+ return array;
+ }
+
+ // Updates the display.view data structure for a given change to the
+ // document. From and to are in pre-change coordinates. Lendiff is
+ // the amount of lines added or subtracted by the change. This is
+ // used for changes that span multiple lines, or change the way
+ // lines are divided into visual lines. regLineChange (below)
+ // registers single-line changes.
+ function regChange(cm, from, to, lendiff) {
+ if (from == null) from = cm.doc.first;
+ if (to == null) to = cm.doc.first + cm.doc.size;
+ if (!lendiff) lendiff = 0;
+
+ var display = cm.display;
+ if (lendiff && to < display.viewTo &&
+ (display.updateLineNumbers == null || display.updateLineNumbers > from))
+ display.updateLineNumbers = from;
+
+ cm.curOp.viewChanged = true;
+
+ if (from >= display.viewTo) { // Change after
+ if (sawCollapsedSpans && visualLineNo(cm.doc, from) < display.viewTo)
+ resetView(cm);
+ } else if (to <= display.viewFrom) { // Change before
+ if (sawCollapsedSpans && visualLineEndNo(cm.doc, to + lendiff) > display.viewFrom) {
+ resetView(cm);
+ } else {
+ display.viewFrom += lendiff;
+ display.viewTo += lendiff;
+ }
+ } else if (from <= display.viewFrom && to >= display.viewTo) { // Full overlap
+ resetView(cm);
+ } else if (from <= display.viewFrom) { // Top overlap
+ var cut = viewCuttingPoint(cm, to, to + lendiff, 1);
+ if (cut) {
+ display.view = display.view.slice(cut.index);
+ display.viewFrom = cut.lineN;
+ display.viewTo += lendiff;
+ } else {
+ resetView(cm);
+ }
+ } else if (to >= display.viewTo) { // Bottom overlap
+ var cut = viewCuttingPoint(cm, from, from, -1);
+ if (cut) {
+ display.view = display.view.slice(0, cut.index);
+ display.viewTo = cut.lineN;
+ } else {
+ resetView(cm);
+ }
+ } else { // Gap in the middle
+ var cutTop = viewCuttingPoint(cm, from, from, -1);
+ var cutBot = viewCuttingPoint(cm, to, to + lendiff, 1);
+ if (cutTop && cutBot) {
+ display.view = display.view.slice(0, cutTop.index)
+ .concat(buildViewArray(cm, cutTop.lineN, cutBot.lineN))
+ .concat(display.view.slice(cutBot.index));
+ display.viewTo += lendiff;
+ } else {
+ resetView(cm);
+ }
+ }
+
+ var ext = display.externalMeasured;
+ if (ext) {
+ if (to < ext.lineN)
+ ext.lineN += lendiff;
+ else if (from < ext.lineN + ext.size)
+ display.externalMeasured = null;
+ }
+ }
+
+ // Register a change to a single line. Type must be one of "text",
+ // "gutter", "class", "widget"
+ function regLineChange(cm, line, type) {
+ cm.curOp.viewChanged = true;
+ var display = cm.display, ext = cm.display.externalMeasured;
+ if (ext && line >= ext.lineN && line < ext.lineN + ext.size)
+ display.externalMeasured = null;
+
+ if (line < display.viewFrom || line >= display.viewTo) return;
+ var lineView = display.view[findViewIndex(cm, line)];
+ if (lineView.node == null) return;
+ var arr = lineView.changes || (lineView.changes = []);
+ if (indexOf(arr, type) == -1) arr.push(type);
+ }
+
+ // Clear the view.
+ function resetView(cm) {
+ cm.display.viewFrom = cm.display.viewTo = cm.doc.first;
+ cm.display.view = [];
+ cm.display.viewOffset = 0;
+ }
+
+ // Find the view element corresponding to a given line. Return null
+ // when the line isn't visible.
+ function findViewIndex(cm, n) {
+ if (n >= cm.display.viewTo) return null;
+ n -= cm.display.viewFrom;
+ if (n < 0) return null;
+ var view = cm.display.view;
+ for (var i = 0; i < view.length; i++) {
+ n -= view[i].size;
+ if (n < 0) return i;
+ }
+ }
+
+ function viewCuttingPoint(cm, oldN, newN, dir) {
+ var index = findViewIndex(cm, oldN), diff, view = cm.display.view;
+ if (!sawCollapsedSpans || newN == cm.doc.first + cm.doc.size)
+ return {index: index, lineN: newN};
+ for (var i = 0, n = cm.display.viewFrom; i < index; i++)
+ n += view[i].size;
+ if (n != oldN) {
+ if (dir > 0) {
+ if (index == view.length - 1) return null;
+ diff = (n + view[index].size) - oldN;
+ index++;
+ } else {
+ diff = n - oldN;
+ }
+ oldN += diff; newN += diff;
+ }
+ while (visualLineNo(cm.doc, newN) != newN) {
+ if (index == (dir < 0 ? 0 : view.length - 1)) return null;
+ newN += dir * view[index - (dir < 0 ? 1 : 0)].size;
+ index += dir;
+ }
+ return {index: index, lineN: newN};
+ }
+
+ // Force the view to cover a given range, adding empty view element
+ // or clipping off existing ones as needed.
+ function adjustView(cm, from, to) {
+ var display = cm.display, view = display.view;
+ if (view.length == 0 || from >= display.viewTo || to <= display.viewFrom) {
+ display.view = buildViewArray(cm, from, to);
+ display.viewFrom = from;
+ } else {
+ if (display.viewFrom > from)
+ display.view = buildViewArray(cm, from, display.viewFrom).concat(display.view);
+ else if (display.viewFrom < from)
+ display.view = display.view.slice(findViewIndex(cm, from));
+ display.viewFrom = from;
+ if (display.viewTo < to)
+ display.view = display.view.concat(buildViewArray(cm, display.viewTo, to));
+ else if (display.viewTo > to)
+ display.view = display.view.slice(0, findViewIndex(cm, to));
+ }
+ display.viewTo = to;
+ }
+
+ // Count the number of lines in the view whose DOM representation is
+ // out of date (or nonexistent).
+ function countDirtyView(cm) {
+ var view = cm.display.view, dirty = 0;
+ for (var i = 0; i < view.length; i++) {
+ var lineView = view[i];
+ if (!lineView.hidden && (!lineView.node || lineView.changes)) ++dirty;
+ }
+ return dirty;
+ }
+
+ // EVENT HANDLERS
+
+ // Attach the necessary event handlers when initializing the editor
+ function registerEventHandlers(cm) {
+ var d = cm.display;
+ on(d.scroller, "mousedown", operation(cm, onMouseDown));
+ // Older IE's will not fire a second mousedown for a double click
+ if (ie && ie_version < 11)
+ on(d.scroller, "dblclick", operation(cm, function(e) {
+ if (signalDOMEvent(cm, e)) return;
+ var pos = posFromMouse(cm, e);
+ if (!pos || clickInGutter(cm, e) || eventInWidget(cm.display, e)) return;
+ e_preventDefault(e);
+ var word = cm.findWordAt(pos);
+ extendSelection(cm.doc, word.anchor, word.head);
+ }));
+ else
+ on(d.scroller, "dblclick", function(e) { signalDOMEvent(cm, e) || e_preventDefault(e); });
+ // Some browsers fire contextmenu *after* opening the menu, at
+ // which point we can't mess with it anymore. Context menu is
+ // handled in onMouseDown for these browsers.
+ if (!captureRightClick) on(d.scroller, "contextmenu", function(e) {onContextMenu(cm, e);});
+
+ // Used to suppress mouse event handling when a touch happens
+ var touchFinished, prevTouch = {end: 0};
+ function finishTouch() {
+ if (d.activeTouch) {
+ touchFinished = setTimeout(function() {d.activeTouch = null;}, 1000);
+ prevTouch = d.activeTouch;
+ prevTouch.end = +new Date;
+ }
+ };
+ function isMouseLikeTouchEvent(e) {
+ if (e.touches.length != 1) return false;
+ var touch = e.touches[0];
+ return touch.radiusX <= 1 && touch.radiusY <= 1;
+ }
+ function farAway(touch, other) {
+ if (other.left == null) return true;
+ var dx = other.left - touch.left, dy = other.top - touch.top;
+ return dx * dx + dy * dy > 20 * 20;
+ }
+ on(d.scroller, "touchstart", function(e) {
+ if (!signalDOMEvent(cm, e) && !isMouseLikeTouchEvent(e)) {
+ clearTimeout(touchFinished);
+ var now = +new Date;
+ d.activeTouch = {start: now, moved: false,
+ prev: now - prevTouch.end <= 300 ? prevTouch : null};
+ if (e.touches.length == 1) {
+ d.activeTouch.left = e.touches[0].pageX;
+ d.activeTouch.top = e.touches[0].pageY;
+ }
+ }
+ });
+ on(d.scroller, "touchmove", function() {
+ if (d.activeTouch) d.activeTouch.moved = true;
+ });
+ on(d.scroller, "touchend", function(e) {
+ var touch = d.activeTouch;
+ if (touch && !eventInWidget(d, e) && touch.left != null &&
+ !touch.moved && new Date - touch.start < 300) {
+ var pos = cm.coordsChar(d.activeTouch, "page"), range;
+ if (!touch.prev || farAway(touch, touch.prev)) // Single tap
+ range = new Range(pos, pos);
+ else if (!touch.prev.prev || farAway(touch, touch.prev.prev)) // Double tap
+ range = cm.findWordAt(pos);
+ else // Triple tap
+ range = new Range(Pos(pos.line, 0), clipPos(cm.doc, Pos(pos.line + 1, 0)));
+ cm.setSelection(range.anchor, range.head);
+ cm.focus();
+ e_preventDefault(e);
+ }
+ finishTouch();
+ });
+ on(d.scroller, "touchcancel", finishTouch);
+
+ // Sync scrolling between fake scrollbars and real scrollable
+ // area, ensure viewport is updated when scrolling.
+ on(d.scroller, "scroll", function() {
+ if (d.scroller.clientHeight) {
+ setScrollTop(cm, d.scroller.scrollTop);
+ setScrollLeft(cm, d.scroller.scrollLeft, true);
+ signal(cm, "scroll", cm);
+ }
+ });
+
+ // Listen to wheel events in order to try and update the viewport on time.
+ on(d.scroller, "mousewheel", function(e){onScrollWheel(cm, e);});
+ on(d.scroller, "DOMMouseScroll", function(e){onScrollWheel(cm, e);});
+
+ // Prevent wrapper from ever scrolling
+ on(d.wrapper, "scroll", function() { d.wrapper.scrollTop = d.wrapper.scrollLeft = 0; });
+
+ d.dragFunctions = {
+ enter: function(e) {if (!signalDOMEvent(cm, e)) e_stop(e);},
+ over: function(e) {if (!signalDOMEvent(cm, e)) { onDragOver(cm, e); e_stop(e); }},
+ start: function(e){onDragStart(cm, e);},
+ drop: operation(cm, onDrop),
+ leave: function(e) {if (!signalDOMEvent(cm, e)) { clearDragCursor(cm); }}
+ };
+
+ var inp = d.input.getField();
+ on(inp, "keyup", function(e) { onKeyUp.call(cm, e); });
+ on(inp, "keydown", operation(cm, onKeyDown));
+ on(inp, "keypress", operation(cm, onKeyPress));
+ on(inp, "focus", bind(onFocus, cm));
+ on(inp, "blur", bind(onBlur, cm));
+ }
+
+ function dragDropChanged(cm, value, old) {
+ var wasOn = old && old != CodeMirror.Init;
+ if (!value != !wasOn) {
+ var funcs = cm.display.dragFunctions;
+ var toggle = value ? on : off;
+ toggle(cm.display.scroller, "dragstart", funcs.start);
+ toggle(cm.display.scroller, "dragenter", funcs.enter);
+ toggle(cm.display.scroller, "dragover", funcs.over);
+ toggle(cm.display.scroller, "dragleave", funcs.leave);
+ toggle(cm.display.scroller, "drop", funcs.drop);
+ }
+ }
+
+ // Called when the window resizes
+ function onResize(cm) {
+ var d = cm.display;
+ if (d.lastWrapHeight == d.wrapper.clientHeight && d.lastWrapWidth == d.wrapper.clientWidth)
+ return;
+ // Might be a text scaling operation, clear size caches.
+ d.cachedCharWidth = d.cachedTextHeight = d.cachedPaddingH = null;
+ d.scrollbarsClipped = false;
+ cm.setSize();
+ }
+
+ // MOUSE EVENTS
+
+ // Return true when the given mouse event happened in a widget
+ function eventInWidget(display, e) {
+ for (var n = e_target(e); n != display.wrapper; n = n.parentNode) {
+ if (!n || (n.nodeType == 1 && n.getAttribute("cm-ignore-events") == "true") ||
+ (n.parentNode == display.sizer && n != display.mover))
+ return true;
+ }
+ }
+
+ // Given a mouse event, find the corresponding position. If liberal
+ // is false, it checks whether a gutter or scrollbar was clicked,
+ // and returns null if it was. forRect is used by rectangular
+ // selections, and tries to estimate a character position even for
+ // coordinates beyond the right of the text.
+ function posFromMouse(cm, e, liberal, forRect) {
+ var display = cm.display;
+ if (!liberal && e_target(e).getAttribute("cm-not-content") == "true") return null;
+
+ var x, y, space = display.lineSpace.getBoundingClientRect();
+ // Fails unpredictably on IE[67] when mouse is dragged around quickly.
+ try { x = e.clientX - space.left; y = e.clientY - space.top; }
+ catch (e) { return null; }
+ var coords = coordsChar(cm, x, y), line;
+ if (forRect && coords.xRel == 1 && (line = getLine(cm.doc, coords.line).text).length == coords.ch) {
+ var colDiff = countColumn(line, line.length, cm.options.tabSize) - line.length;
+ coords = Pos(coords.line, Math.max(0, Math.round((x - paddingH(cm.display).left) / charWidth(cm.display)) - colDiff));
+ }
+ return coords;
+ }
+
+ // A mouse down can be a single click, double click, triple click,
+ // start of selection drag, start of text drag, new cursor
+ // (ctrl-click), rectangle drag (alt-drag), or xwin
+ // middle-click-paste. Or it might be a click on something we should
+ // not interfere with, such as a scrollbar or widget.
+ function onMouseDown(e) {
+ var cm = this, display = cm.display;
+ if (signalDOMEvent(cm, e) || display.activeTouch && display.input.supportsTouch()) return;
+ display.shift = e.shiftKey;
+
+ if (eventInWidget(display, e)) {
+ if (!webkit) {
+ // Briefly turn off draggability, to allow widgets to do
+ // normal dragging things.
+ display.scroller.draggable = false;
+ setTimeout(function(){display.scroller.draggable = true;}, 100);
+ }
+ return;
+ }
+ if (clickInGutter(cm, e)) return;
+ var start = posFromMouse(cm, e);
+ window.focus();
+
+ switch (e_button(e)) {
+ case 1:
+ // #3261: make sure, that we're not starting a second selection
+ if (cm.state.selectingText)
+ cm.state.selectingText(e);
+ else if (start)
+ leftButtonDown(cm, e, start);
+ else if (e_target(e) == display.scroller)
+ e_preventDefault(e);
+ break;
+ case 2:
+ if (webkit) cm.state.lastMiddleDown = +new Date;
+ if (start) extendSelection(cm.doc, start);
+ setTimeout(function() {display.input.focus();}, 20);
+ e_preventDefault(e);
+ break;
+ case 3:
+ if (captureRightClick) onContextMenu(cm, e);
+ else delayBlurEvent(cm);
+ break;
+ }
+ }
+
+ var lastClick, lastDoubleClick;
+ function leftButtonDown(cm, e, start) {
+ if (ie) setTimeout(bind(ensureFocus, cm), 0);
+ else cm.curOp.focus = activeElt();
+
+ var now = +new Date, type;
+ if (lastDoubleClick && lastDoubleClick.time > now - 400 && cmp(lastDoubleClick.pos, start) == 0) {
+ type = "triple";
+ } else if (lastClick && lastClick.time > now - 400 && cmp(lastClick.pos, start) == 0) {
+ type = "double";
+ lastDoubleClick = {time: now, pos: start};
+ } else {
+ type = "single";
+ lastClick = {time: now, pos: start};
+ }
+
+ var sel = cm.doc.sel, modifier = mac ? e.metaKey : e.ctrlKey, contained;
+ if (cm.options.dragDrop && dragAndDrop && !cm.isReadOnly() &&
+ type == "single" && (contained = sel.contains(start)) > -1 &&
+ (cmp((contained = sel.ranges[contained]).from(), start) < 0 || start.xRel > 0) &&
+ (cmp(contained.to(), start) > 0 || start.xRel < 0))
+ leftButtonStartDrag(cm, e, start, modifier);
+ else
+ leftButtonSelect(cm, e, start, type, modifier);
+ }
+
+ // Start a text drag. When it ends, see if any dragging actually
+ // happen, and treat as a click if it didn't.
+ function leftButtonStartDrag(cm, e, start, modifier) {
+ var display = cm.display, startTime = +new Date;
+ var dragEnd = operation(cm, function(e2) {
+ if (webkit) display.scroller.draggable = false;
+ cm.state.draggingText = false;
+ off(document, "mouseup", dragEnd);
+ off(display.scroller, "drop", dragEnd);
+ if (Math.abs(e.clientX - e2.clientX) + Math.abs(e.clientY - e2.clientY) < 10) {
+ e_preventDefault(e2);
+ if (!modifier && +new Date - 200 < startTime)
+ extendSelection(cm.doc, start);
+ // Work around unexplainable focus problem in IE9 (#2127) and Chrome (#3081)
+ if (webkit || ie && ie_version == 9)
+ setTimeout(function() {document.body.focus(); display.input.focus();}, 20);
+ else
+ display.input.focus();
+ }
+ });
+ // Let the drag handler handle this.
+ if (webkit) display.scroller.draggable = true;
+ cm.state.draggingText = dragEnd;
+ dragEnd.copy = mac ? e.altKey : e.ctrlKey
+ // IE's approach to draggable
+ if (display.scroller.dragDrop) display.scroller.dragDrop();
+ on(document, "mouseup", dragEnd);
+ on(display.scroller, "drop", dragEnd);
+ }
+
+ // Normal selection, as opposed to text dragging.
+ function leftButtonSelect(cm, e, start, type, addNew) {
+ var display = cm.display, doc = cm.doc;
+ e_preventDefault(e);
+
+ var ourRange, ourIndex, startSel = doc.sel, ranges = startSel.ranges;
+ if (addNew && !e.shiftKey) {
+ ourIndex = doc.sel.contains(start);
+ if (ourIndex > -1)
+ ourRange = ranges[ourIndex];
+ else
+ ourRange = new Range(start, start);
+ } else {
+ ourRange = doc.sel.primary();
+ ourIndex = doc.sel.primIndex;
+ }
+
+ if (chromeOS ? e.shiftKey && e.metaKey : e.altKey) {
+ type = "rect";
+ if (!addNew) ourRange = new Range(start, start);
+ start = posFromMouse(cm, e, true, true);
+ ourIndex = -1;
+ } else if (type == "double") {
+ var word = cm.findWordAt(start);
+ if (cm.display.shift || doc.extend)
+ ourRange = extendRange(doc, ourRange, word.anchor, word.head);
+ else
+ ourRange = word;
+ } else if (type == "triple") {
+ var line = new Range(Pos(start.line, 0), clipPos(doc, Pos(start.line + 1, 0)));
+ if (cm.display.shift || doc.extend)
+ ourRange = extendRange(doc, ourRange, line.anchor, line.head);
+ else
+ ourRange = line;
+ } else {
+ ourRange = extendRange(doc, ourRange, start);
+ }
+
+ if (!addNew) {
+ ourIndex = 0;
+ setSelection(doc, new Selection([ourRange], 0), sel_mouse);
+ startSel = doc.sel;
+ } else if (ourIndex == -1) {
+ ourIndex = ranges.length;
+ setSelection(doc, normalizeSelection(ranges.concat([ourRange]), ourIndex),
+ {scroll: false, origin: "*mouse"});
+ } else if (ranges.length > 1 && ranges[ourIndex].empty() && type == "single" && !e.shiftKey) {
+ setSelection(doc, normalizeSelection(ranges.slice(0, ourIndex).concat(ranges.slice(ourIndex + 1)), 0),
+ {scroll: false, origin: "*mouse"});
+ startSel = doc.sel;
+ } else {
+ replaceOneSelection(doc, ourIndex, ourRange, sel_mouse);
+ }
+
+ var lastPos = start;
+ function extendTo(pos) {
+ if (cmp(lastPos, pos) == 0) return;
+ lastPos = pos;
+
+ if (type == "rect") {
+ var ranges = [], tabSize = cm.options.tabSize;
+ var startCol = countColumn(getLine(doc, start.line).text, start.ch, tabSize);
+ var posCol = countColumn(getLine(doc, pos.line).text, pos.ch, tabSize);
+ var left = Math.min(startCol, posCol), right = Math.max(startCol, posCol);
+ for (var line = Math.min(start.line, pos.line), end = Math.min(cm.lastLine(), Math.max(start.line, pos.line));
+ line <= end; line++) {
+ var text = getLine(doc, line).text, leftPos = findColumn(text, left, tabSize);
+ if (left == right)
+ ranges.push(new Range(Pos(line, leftPos), Pos(line, leftPos)));
+ else if (text.length > leftPos)
+ ranges.push(new Range(Pos(line, leftPos), Pos(line, findColumn(text, right, tabSize))));
+ }
+ if (!ranges.length) ranges.push(new Range(start, start));
+ setSelection(doc, normalizeSelection(startSel.ranges.slice(0, ourIndex).concat(ranges), ourIndex),
+ {origin: "*mouse", scroll: false});
+ cm.scrollIntoView(pos);
+ } else {
+ var oldRange = ourRange;
+ var anchor = oldRange.anchor, head = pos;
+ if (type != "single") {
+ if (type == "double")
+ var range = cm.findWordAt(pos);
+ else
+ var range = new Range(Pos(pos.line, 0), clipPos(doc, Pos(pos.line + 1, 0)));
+ if (cmp(range.anchor, anchor) > 0) {
+ head = range.head;
+ anchor = minPos(oldRange.from(), range.anchor);
+ } else {
+ head = range.anchor;
+ anchor = maxPos(oldRange.to(), range.head);
+ }
+ }
+ var ranges = startSel.ranges.slice(0);
+ ranges[ourIndex] = new Range(clipPos(doc, anchor), head);
+ setSelection(doc, normalizeSelection(ranges, ourIndex), sel_mouse);
+ }
+ }
+
+ var editorSize = display.wrapper.getBoundingClientRect();
+ // Used to ensure timeout re-tries don't fire when another extend
+ // happened in the meantime (clearTimeout isn't reliable -- at
+ // least on Chrome, the timeouts still happen even when cleared,
+ // if the clear happens after their scheduled firing time).
+ var counter = 0;
+
+ function extend(e) {
+ var curCount = ++counter;
+ var cur = posFromMouse(cm, e, true, type == "rect");
+ if (!cur) return;
+ if (cmp(cur, lastPos) != 0) {
+ cm.curOp.focus = activeElt();
+ extendTo(cur);
+ var visible = visibleLines(display, doc);
+ if (cur.line >= visible.to || cur.line < visible.from)
+ setTimeout(operation(cm, function(){if (counter == curCount) extend(e);}), 150);
+ } else {
+ var outside = e.clientY < editorSize.top ? -20 : e.clientY > editorSize.bottom ? 20 : 0;
+ if (outside) setTimeout(operation(cm, function() {
+ if (counter != curCount) return;
+ display.scroller.scrollTop += outside;
+ extend(e);
+ }), 50);
+ }
+ }
+
+ function done(e) {
+ cm.state.selectingText = false;
+ counter = Infinity;
+ e_preventDefault(e);
+ display.input.focus();
+ off(document, "mousemove", move);
+ off(document, "mouseup", up);
+ doc.history.lastSelOrigin = null;
+ }
+
+ var move = operation(cm, function(e) {
+ if (!e_button(e)) done(e);
+ else extend(e);
+ });
+ var up = operation(cm, done);
+ cm.state.selectingText = up;
+ on(document, "mousemove", move);
+ on(document, "mouseup", up);
+ }
+
+ // Determines whether an event happened in the gutter, and fires the
+ // handlers for the corresponding event.
+ function gutterEvent(cm, e, type, prevent) {
+ try { var mX = e.clientX, mY = e.clientY; }
+ catch(e) { return false; }
+ if (mX >= Math.floor(cm.display.gutters.getBoundingClientRect().right)) return false;
+ if (prevent) e_preventDefault(e);
+
+ var display = cm.display;
+ var lineBox = display.lineDiv.getBoundingClientRect();
+
+ if (mY > lineBox.bottom || !hasHandler(cm, type)) return e_defaultPrevented(e);
+ mY -= lineBox.top - display.viewOffset;
+
+ for (var i = 0; i < cm.options.gutters.length; ++i) {
+ var g = display.gutters.childNodes[i];
+ if (g && g.getBoundingClientRect().right >= mX) {
+ var line = lineAtHeight(cm.doc, mY);
+ var gutter = cm.options.gutters[i];
+ signal(cm, type, cm, line, gutter, e);
+ return e_defaultPrevented(e);
+ }
+ }
+ }
+
+ function clickInGutter(cm, e) {
+ return gutterEvent(cm, e, "gutterClick", true);
+ }
+
+ // Kludge to work around strange IE behavior where it'll sometimes
+ // re-fire a series of drag-related events right after the drop (#1551)
+ var lastDrop = 0;
+
+ function onDrop(e) {
+ var cm = this;
+ clearDragCursor(cm);
+ if (signalDOMEvent(cm, e) || eventInWidget(cm.display, e))
+ return;
+ e_preventDefault(e);
+ if (ie) lastDrop = +new Date;
+ var pos = posFromMouse(cm, e, true), files = e.dataTransfer.files;
+ if (!pos || cm.isReadOnly()) return;
+ // Might be a file drop, in which case we simply extract the text
+ // and insert it.
+ if (files && files.length && window.FileReader && window.File) {
+ var n = files.length, text = Array(n), read = 0;
+ var loadFile = function(file, i) {
+ if (cm.options.allowDropFileTypes &&
+ indexOf(cm.options.allowDropFileTypes, file.type) == -1)
+ return;
+
+ var reader = new FileReader;
+ reader.onload = operation(cm, function() {
+ var content = reader.result;
+ if (/[\x00-\x08\x0e-\x1f]{2}/.test(content)) content = "";
+ text[i] = content;
+ if (++read == n) {
+ pos = clipPos(cm.doc, pos);
+ var change = {from: pos, to: pos,
+ text: cm.doc.splitLines(text.join(cm.doc.lineSeparator())),
+ origin: "paste"};
+ makeChange(cm.doc, change);
+ setSelectionReplaceHistory(cm.doc, simpleSelection(pos, changeEnd(change)));
+ }
+ });
+ reader.readAsText(file);
+ };
+ for (var i = 0; i < n; ++i) loadFile(files[i], i);
+ } else { // Normal drop
+ // Don't do a replace if the drop happened inside of the selected text.
+ if (cm.state.draggingText && cm.doc.sel.contains(pos) > -1) {
+ cm.state.draggingText(e);
+ // Ensure the editor is re-focused
+ setTimeout(function() {cm.display.input.focus();}, 20);
+ return;
+ }
+ try {
+ var text = e.dataTransfer.getData("Text");
+ if (text) {
+ if (cm.state.draggingText && !cm.state.draggingText.copy)
+ var selected = cm.listSelections();
+ setSelectionNoUndo(cm.doc, simpleSelection(pos, pos));
+ if (selected) for (var i = 0; i < selected.length; ++i)
+ replaceRange(cm.doc, "", selected[i].anchor, selected[i].head, "drag");
+ cm.replaceSelection(text, "around", "paste");
+ cm.display.input.focus();
+ }
+ }
+ catch(e){}
+ }
+ }
+
+ function onDragStart(cm, e) {
+ if (ie && (!cm.state.draggingText || +new Date - lastDrop < 100)) { e_stop(e); return; }
+ if (signalDOMEvent(cm, e) || eventInWidget(cm.display, e)) return;
+
+ e.dataTransfer.setData("Text", cm.getSelection());
+ e.dataTransfer.effectAllowed = "copyMove"
+
+ // Use dummy image instead of default browsers image.
+ // Recent Safari (~6.0.2) have a tendency to segfault when this happens, so we don't do it there.
+ if (e.dataTransfer.setDragImage && !safari) {
+ var img = elt("img", null, null, "position: fixed; left: 0; top: 0;");
+ img.src = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==";
+ if (presto) {
+ img.width = img.height = 1;
+ cm.display.wrapper.appendChild(img);
+ // Force a relayout, or Opera won't use our image for some obscure reason
+ img._top = img.offsetTop;
+ }
+ e.dataTransfer.setDragImage(img, 0, 0);
+ if (presto) img.parentNode.removeChild(img);
+ }
+ }
+
+ function onDragOver(cm, e) {
+ var pos = posFromMouse(cm, e);
+ if (!pos) return;
+ var frag = document.createDocumentFragment();
+ drawSelectionCursor(cm, pos, frag);
+ if (!cm.display.dragCursor) {
+ cm.display.dragCursor = elt("div", null, "CodeMirror-cursors CodeMirror-dragcursors");
+ cm.display.lineSpace.insertBefore(cm.display.dragCursor, cm.display.cursorDiv);
+ }
+ removeChildrenAndAdd(cm.display.dragCursor, frag);
+ }
+
+ function clearDragCursor(cm) {
+ if (cm.display.dragCursor) {
+ cm.display.lineSpace.removeChild(cm.display.dragCursor);
+ cm.display.dragCursor = null;
+ }
+ }
+
+ // SCROLL EVENTS
+
+ // Sync the scrollable area and scrollbars, ensure the viewport
+ // covers the visible area.
+ function setScrollTop(cm, val) {
+ if (Math.abs(cm.doc.scrollTop - val) < 2) return;
+ cm.doc.scrollTop = val;
+ if (!gecko) updateDisplaySimple(cm, {top: val});
+ if (cm.display.scroller.scrollTop != val) cm.display.scroller.scrollTop = val;
+ cm.display.scrollbars.setScrollTop(val);
+ if (gecko) updateDisplaySimple(cm);
+ startWorker(cm, 100);
+ }
+ // Sync scroller and scrollbar, ensure the gutter elements are
+ // aligned.
+ function setScrollLeft(cm, val, isScroller) {
+ if (isScroller ? val == cm.doc.scrollLeft : Math.abs(cm.doc.scrollLeft - val) < 2) return;
+ val = Math.min(val, cm.display.scroller.scrollWidth - cm.display.scroller.clientWidth);
+ cm.doc.scrollLeft = val;
+ alignHorizontally(cm);
+ if (cm.display.scroller.scrollLeft != val) cm.display.scroller.scrollLeft = val;
+ cm.display.scrollbars.setScrollLeft(val);
+ }
+
+ // Since the delta values reported on mouse wheel events are
+ // unstandardized between browsers and even browser versions, and
+ // generally horribly unpredictable, this code starts by measuring
+ // the scroll effect that the first few mouse wheel events have,
+ // and, from that, detects the way it can convert deltas to pixel
+ // offsets afterwards.
+ //
+ // The reason we want to know the amount a wheel event will scroll
+ // is that it gives us a chance to update the display before the
+ // actual scrolling happens, reducing flickering.
+
+ var wheelSamples = 0, wheelPixelsPerUnit = null;
+ // Fill in a browser-detected starting value on browsers where we
+ // know one. These don't have to be accurate -- the result of them
+ // being wrong would just be a slight flicker on the first wheel
+ // scroll (if it is large enough).
+ if (ie) wheelPixelsPerUnit = -.53;
+ else if (gecko) wheelPixelsPerUnit = 15;
+ else if (chrome) wheelPixelsPerUnit = -.7;
+ else if (safari) wheelPixelsPerUnit = -1/3;
+
+ var wheelEventDelta = function(e) {
+ var dx = e.wheelDeltaX, dy = e.wheelDeltaY;
+ if (dx == null && e.detail && e.axis == e.HORIZONTAL_AXIS) dx = e.detail;
+ if (dy == null && e.detail && e.axis == e.VERTICAL_AXIS) dy = e.detail;
+ else if (dy == null) dy = e.wheelDelta;
+ return {x: dx, y: dy};
+ };
+ CodeMirror.wheelEventPixels = function(e) {
+ var delta = wheelEventDelta(e);
+ delta.x *= wheelPixelsPerUnit;
+ delta.y *= wheelPixelsPerUnit;
+ return delta;
+ };
+
+ function onScrollWheel(cm, e) {
+ var delta = wheelEventDelta(e), dx = delta.x, dy = delta.y;
+
+ var display = cm.display, scroll = display.scroller;
+ // Quit if there's nothing to scroll here
+ var canScrollX = scroll.scrollWidth > scroll.clientWidth;
+ var canScrollY = scroll.scrollHeight > scroll.clientHeight;
+ if (!(dx && canScrollX || dy && canScrollY)) return;
+
+ // Webkit browsers on OS X abort momentum scrolls when the target
+ // of the scroll event is removed from the scrollable element.
+ // This hack (see related code in patchDisplay) makes sure the
+ // element is kept around.
+ if (dy && mac && webkit) {
+ outer: for (var cur = e.target, view = display.view; cur != scroll; cur = cur.parentNode) {
+ for (var i = 0; i < view.length; i++) {
+ if (view[i].node == cur) {
+ cm.display.currentWheelTarget = cur;
+ break outer;
+ }
+ }
+ }
+ }
+
+ // On some browsers, horizontal scrolling will cause redraws to
+ // happen before the gutter has been realigned, causing it to
+ // wriggle around in a most unseemly way. When we have an
+ // estimated pixels/delta value, we just handle horizontal
+ // scrolling entirely here. It'll be slightly off from native, but
+ // better than glitching out.
+ if (dx && !gecko && !presto && wheelPixelsPerUnit != null) {
+ if (dy && canScrollY)
+ setScrollTop(cm, Math.max(0, Math.min(scroll.scrollTop + dy * wheelPixelsPerUnit, scroll.scrollHeight - scroll.clientHeight)));
+ setScrollLeft(cm, Math.max(0, Math.min(scroll.scrollLeft + dx * wheelPixelsPerUnit, scroll.scrollWidth - scroll.clientWidth)));
+ // Only prevent default scrolling if vertical scrolling is
+ // actually possible. Otherwise, it causes vertical scroll
+ // jitter on OSX trackpads when deltaX is small and deltaY
+ // is large (issue #3579)
+ if (!dy || (dy && canScrollY))
+ e_preventDefault(e);
+ display.wheelStartX = null; // Abort measurement, if in progress
+ return;
+ }
+
+ // 'Project' the visible viewport to cover the area that is being
+ // scrolled into view (if we know enough to estimate it).
+ if (dy && wheelPixelsPerUnit != null) {
+ var pixels = dy * wheelPixelsPerUnit;
+ var top = cm.doc.scrollTop, bot = top + display.wrapper.clientHeight;
+ if (pixels < 0) top = Math.max(0, top + pixels - 50);
+ else bot = Math.min(cm.doc.height, bot + pixels + 50);
+ updateDisplaySimple(cm, {top: top, bottom: bot});
+ }
+
+ if (wheelSamples < 20) {
+ if (display.wheelStartX == null) {
+ display.wheelStartX = scroll.scrollLeft; display.wheelStartY = scroll.scrollTop;
+ display.wheelDX = dx; display.wheelDY = dy;
+ setTimeout(function() {
+ if (display.wheelStartX == null) return;
+ var movedX = scroll.scrollLeft - display.wheelStartX;
+ var movedY = scroll.scrollTop - display.wheelStartY;
+ var sample = (movedY && display.wheelDY && movedY / display.wheelDY) ||
+ (movedX && display.wheelDX && movedX / display.wheelDX);
+ display.wheelStartX = display.wheelStartY = null;
+ if (!sample) return;
+ wheelPixelsPerUnit = (wheelPixelsPerUnit * wheelSamples + sample) / (wheelSamples + 1);
+ ++wheelSamples;
+ }, 200);
+ } else {
+ display.wheelDX += dx; display.wheelDY += dy;
+ }
+ }
+ }
+
+ // KEY EVENTS
+
+ // Run a handler that was bound to a key.
+ function doHandleBinding(cm, bound, dropShift) {
+ if (typeof bound == "string") {
+ bound = commands[bound];
+ if (!bound) return false;
+ }
+ // Ensure previous input has been read, so that the handler sees a
+ // consistent view of the document
+ cm.display.input.ensurePolled();
+ var prevShift = cm.display.shift, done = false;
+ try {
+ if (cm.isReadOnly()) cm.state.suppressEdits = true;
+ if (dropShift) cm.display.shift = false;
+ done = bound(cm) != Pass;
+ } finally {
+ cm.display.shift = prevShift;
+ cm.state.suppressEdits = false;
+ }
+ return done;
+ }
+
+ function lookupKeyForEditor(cm, name, handle) {
+ for (var i = 0; i < cm.state.keyMaps.length; i++) {
+ var result = lookupKey(name, cm.state.keyMaps[i], handle, cm);
+ if (result) return result;
+ }
+ return (cm.options.extraKeys && lookupKey(name, cm.options.extraKeys, handle, cm))
+ || lookupKey(name, cm.options.keyMap, handle, cm);
+ }
+
+ var stopSeq = new Delayed;
+ function dispatchKey(cm, name, e, handle) {
+ var seq = cm.state.keySeq;
+ if (seq) {
+ if (isModifierKey(name)) return "handled";
+ stopSeq.set(50, function() {
+ if (cm.state.keySeq == seq) {
+ cm.state.keySeq = null;
+ cm.display.input.reset();
+ }
+ });
+ name = seq + " " + name;
+ }
+ var result = lookupKeyForEditor(cm, name, handle);
+
+ if (result == "multi")
+ cm.state.keySeq = name;
+ if (result == "handled")
+ signalLater(cm, "keyHandled", cm, name, e);
+
+ if (result == "handled" || result == "multi") {
+ e_preventDefault(e);
+ restartBlink(cm);
+ }
+
+ if (seq && !result && /\'$/.test(name)) {
+ e_preventDefault(e);
+ return true;
+ }
+ return !!result;
+ }
+
+ // Handle a key from the keydown event.
+ function handleKeyBinding(cm, e) {
+ var name = keyName(e, true);
+ if (!name) return false;
+
+ if (e.shiftKey && !cm.state.keySeq) {
+ // First try to resolve full name (including 'Shift-'). Failing
+ // that, see if there is a cursor-motion command (starting with
+ // 'go') bound to the keyname without 'Shift-'.
+ return dispatchKey(cm, "Shift-" + name, e, function(b) {return doHandleBinding(cm, b, true);})
+ || dispatchKey(cm, name, e, function(b) {
+ if (typeof b == "string" ? /^go[A-Z]/.test(b) : b.motion)
+ return doHandleBinding(cm, b);
+ });
+ } else {
+ return dispatchKey(cm, name, e, function(b) { return doHandleBinding(cm, b); });
+ }
+ }
+
+ // Handle a key from the keypress event
+ function handleCharBinding(cm, e, ch) {
+ return dispatchKey(cm, "'" + ch + "'", e,
+ function(b) { return doHandleBinding(cm, b, true); });
+ }
+
+ var lastStoppedKey = null;
+ function onKeyDown(e) {
+ var cm = this;
+ cm.curOp.focus = activeElt();
+ if (signalDOMEvent(cm, e)) return;
+ // IE does strange things with escape.
+ if (ie && ie_version < 11 && e.keyCode == 27) e.returnValue = false;
+ var code = e.keyCode;
+ cm.display.shift = code == 16 || e.shiftKey;
+ var handled = handleKeyBinding(cm, e);
+ if (presto) {
+ lastStoppedKey = handled ? code : null;
+ // Opera has no cut event... we try to at least catch the key combo
+ if (!handled && code == 88 && !hasCopyEvent && (mac ? e.metaKey : e.ctrlKey))
+ cm.replaceSelection("", null, "cut");
+ }
+
+ // Turn mouse into crosshair when Alt is held on Mac.
+ if (code == 18 && !/\bCodeMirror-crosshair\b/.test(cm.display.lineDiv.className))
+ showCrossHair(cm);
+ }
+
+ function showCrossHair(cm) {
+ var lineDiv = cm.display.lineDiv;
+ addClass(lineDiv, "CodeMirror-crosshair");
+
+ function up(e) {
+ if (e.keyCode == 18 || !e.altKey) {
+ rmClass(lineDiv, "CodeMirror-crosshair");
+ off(document, "keyup", up);
+ off(document, "mouseover", up);
+ }
+ }
+ on(document, "keyup", up);
+ on(document, "mouseover", up);
+ }
+
+ function onKeyUp(e) {
+ if (e.keyCode == 16) this.doc.sel.shift = false;
+ signalDOMEvent(this, e);
+ }
+
+ function onKeyPress(e) {
+ var cm = this;
+ if (eventInWidget(cm.display, e) || signalDOMEvent(cm, e) || e.ctrlKey && !e.altKey || mac && e.metaKey) return;
+ var keyCode = e.keyCode, charCode = e.charCode;
+ if (presto && keyCode == lastStoppedKey) {lastStoppedKey = null; e_preventDefault(e); return;}
+ if ((presto && (!e.which || e.which < 10)) && handleKeyBinding(cm, e)) return;
+ var ch = String.fromCharCode(charCode == null ? keyCode : charCode);
+ if (handleCharBinding(cm, e, ch)) return;
+ cm.display.input.onKeyPress(e);
+ }
+
+ // FOCUS/BLUR EVENTS
+
+ function delayBlurEvent(cm) {
+ cm.state.delayingBlurEvent = true;
+ setTimeout(function() {
+ if (cm.state.delayingBlurEvent) {
+ cm.state.delayingBlurEvent = false;
+ onBlur(cm);
+ }
+ }, 100);
+ }
+
+ function onFocus(cm) {
+ if (cm.state.delayingBlurEvent) cm.state.delayingBlurEvent = false;
+
+ if (cm.options.readOnly == "nocursor") return;
+ if (!cm.state.focused) {
+ signal(cm, "focus", cm);
+ cm.state.focused = true;
+ addClass(cm.display.wrapper, "CodeMirror-focused");
+ // This test prevents this from firing when a context
+ // menu is closed (since the input reset would kill the
+ // select-all detection hack)
+ if (!cm.curOp && cm.display.selForContextMenu != cm.doc.sel) {
+ cm.display.input.reset();
+ if (webkit) setTimeout(function() { cm.display.input.reset(true); }, 20); // Issue #1730
+ }
+ cm.display.input.receivedFocus();
+ }
+ restartBlink(cm);
+ }
+ function onBlur(cm) {
+ if (cm.state.delayingBlurEvent) return;
+
+ if (cm.state.focused) {
+ signal(cm, "blur", cm);
+ cm.state.focused = false;
+ rmClass(cm.display.wrapper, "CodeMirror-focused");
+ }
+ clearInterval(cm.display.blinker);
+ setTimeout(function() {if (!cm.state.focused) cm.display.shift = false;}, 150);
+ }
+
+ // CONTEXT MENU HANDLING
+
+ // To make the context menu work, we need to briefly unhide the
+ // textarea (making it as unobtrusive as possible) to let the
+ // right-click take effect on it.
+ function onContextMenu(cm, e) {
+ if (eventInWidget(cm.display, e) || contextMenuInGutter(cm, e)) return;
+ if (signalDOMEvent(cm, e, "contextmenu")) return;
+ cm.display.input.onContextMenu(e);
+ }
+
+ function contextMenuInGutter(cm, e) {
+ if (!hasHandler(cm, "gutterContextMenu")) return false;
+ return gutterEvent(cm, e, "gutterContextMenu", false);
+ }
+
+ // UPDATING
+
+ // Compute the position of the end of a change (its 'to' property
+ // refers to the pre-change end).
+ var changeEnd = CodeMirror.changeEnd = function(change) {
+ if (!change.text) return change.to;
+ return Pos(change.from.line + change.text.length - 1,
+ lst(change.text).length + (change.text.length == 1 ? change.from.ch : 0));
+ };
+
+ // Adjust a position to refer to the post-change position of the
+ // same text, or the end of the change if the change covers it.
+ function adjustForChange(pos, change) {
+ if (cmp(pos, change.from) < 0) return pos;
+ if (cmp(pos, change.to) <= 0) return changeEnd(change);
+
+ var line = pos.line + change.text.length - (change.to.line - change.from.line) - 1, ch = pos.ch;
+ if (pos.line == change.to.line) ch += changeEnd(change).ch - change.to.ch;
+ return Pos(line, ch);
+ }
+
+ function computeSelAfterChange(doc, change) {
+ var out = [];
+ for (var i = 0; i < doc.sel.ranges.length; i++) {
+ var range = doc.sel.ranges[i];
+ out.push(new Range(adjustForChange(range.anchor, change),
+ adjustForChange(range.head, change)));
+ }
+ return normalizeSelection(out, doc.sel.primIndex);
+ }
+
+ function offsetPos(pos, old, nw) {
+ if (pos.line == old.line)
+ return Pos(nw.line, pos.ch - old.ch + nw.ch);
+ else
+ return Pos(nw.line + (pos.line - old.line), pos.ch);
+ }
+
+ // Used by replaceSelections to allow moving the selection to the
+ // start or around the replaced test. Hint may be "start" or "around".
+ function computeReplacedSel(doc, changes, hint) {
+ var out = [];
+ var oldPrev = Pos(doc.first, 0), newPrev = oldPrev;
+ for (var i = 0; i < changes.length; i++) {
+ var change = changes[i];
+ var from = offsetPos(change.from, oldPrev, newPrev);
+ var to = offsetPos(changeEnd(change), oldPrev, newPrev);
+ oldPrev = change.to;
+ newPrev = to;
+ if (hint == "around") {
+ var range = doc.sel.ranges[i], inv = cmp(range.head, range.anchor) < 0;
+ out[i] = new Range(inv ? to : from, inv ? from : to);
+ } else {
+ out[i] = new Range(from, from);
+ }
+ }
+ return new Selection(out, doc.sel.primIndex);
+ }
+
+ // Allow "beforeChange" event handlers to influence a change
+ function filterChange(doc, change, update) {
+ var obj = {
+ canceled: false,
+ from: change.from,
+ to: change.to,
+ text: change.text,
+ origin: change.origin,
+ cancel: function() { this.canceled = true; }
+ };
+ if (update) obj.update = function(from, to, text, origin) {
+ if (from) this.from = clipPos(doc, from);
+ if (to) this.to = clipPos(doc, to);
+ if (text) this.text = text;
+ if (origin !== undefined) this.origin = origin;
+ };
+ signal(doc, "beforeChange", doc, obj);
+ if (doc.cm) signal(doc.cm, "beforeChange", doc.cm, obj);
+
+ if (obj.canceled) return null;
+ return {from: obj.from, to: obj.to, text: obj.text, origin: obj.origin};
+ }
+
+ // Apply a change to a document, and add it to the document's
+ // history, and propagating it to all linked documents.
+ function makeChange(doc, change, ignoreReadOnly) {
+ if (doc.cm) {
+ if (!doc.cm.curOp) return operation(doc.cm, makeChange)(doc, change, ignoreReadOnly);
+ if (doc.cm.state.suppressEdits) return;
+ }
+
+ if (hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange")) {
+ change = filterChange(doc, change, true);
+ if (!change) return;
+ }
+
+ // Possibly split or suppress the update based on the presence
+ // of read-only spans in its range.
+ var split = sawReadOnlySpans && !ignoreReadOnly && removeReadOnlyRanges(doc, change.from, change.to);
+ if (split) {
+ for (var i = split.length - 1; i >= 0; --i)
+ makeChangeInner(doc, {from: split[i].from, to: split[i].to, text: i ? [""] : change.text});
+ } else {
+ makeChangeInner(doc, change);
+ }
+ }
+
+ function makeChangeInner(doc, change) {
+ if (change.text.length == 1 && change.text[0] == "" && cmp(change.from, change.to) == 0) return;
+ var selAfter = computeSelAfterChange(doc, change);
+ addChangeToHistory(doc, change, selAfter, doc.cm ? doc.cm.curOp.id : NaN);
+
+ makeChangeSingleDoc(doc, change, selAfter, stretchSpansOverChange(doc, change));
+ var rebased = [];
+
+ linkedDocs(doc, function(doc, sharedHist) {
+ if (!sharedHist && indexOf(rebased, doc.history) == -1) {
+ rebaseHist(doc.history, change);
+ rebased.push(doc.history);
+ }
+ makeChangeSingleDoc(doc, change, null, stretchSpansOverChange(doc, change));
+ });
+ }
+
+ // Revert a change stored in a document's history.
+ function makeChangeFromHistory(doc, type, allowSelectionOnly) {
+ if (doc.cm && doc.cm.state.suppressEdits) return;
+
+ var hist = doc.history, event, selAfter = doc.sel;
+ var source = type == "undo" ? hist.done : hist.undone, dest = type == "undo" ? hist.undone : hist.done;
+
+ // Verify that there is a useable event (so that ctrl-z won't
+ // needlessly clear selection events)
+ for (var i = 0; i < source.length; i++) {
+ event = source[i];
+ if (allowSelectionOnly ? event.ranges && !event.equals(doc.sel) : !event.ranges)
+ break;
+ }
+ if (i == source.length) return;
+ hist.lastOrigin = hist.lastSelOrigin = null;
+
+ for (;;) {
+ event = source.pop();
+ if (event.ranges) {
+ pushSelectionToHistory(event, dest);
+ if (allowSelectionOnly && !event.equals(doc.sel)) {
+ setSelection(doc, event, {clearRedo: false});
+ return;
+ }
+ selAfter = event;
+ }
+ else break;
+ }
+
+ // Build up a reverse change object to add to the opposite history
+ // stack (redo when undoing, and vice versa).
+ var antiChanges = [];
+ pushSelectionToHistory(selAfter, dest);
+ dest.push({changes: antiChanges, generation: hist.generation});
+ hist.generation = event.generation || ++hist.maxGeneration;
+
+ var filter = hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange");
+
+ for (var i = event.changes.length - 1; i >= 0; --i) {
+ var change = event.changes[i];
+ change.origin = type;
+ if (filter && !filterChange(doc, change, false)) {
+ source.length = 0;
+ return;
+ }
+
+ antiChanges.push(historyChangeFromChange(doc, change));
+
+ var after = i ? computeSelAfterChange(doc, change) : lst(source);
+ makeChangeSingleDoc(doc, change, after, mergeOldSpans(doc, change));
+ if (!i && doc.cm) doc.cm.scrollIntoView({from: change.from, to: changeEnd(change)});
+ var rebased = [];
+
+ // Propagate to the linked documents
+ linkedDocs(doc, function(doc, sharedHist) {
+ if (!sharedHist && indexOf(rebased, doc.history) == -1) {
+ rebaseHist(doc.history, change);
+ rebased.push(doc.history);
+ }
+ makeChangeSingleDoc(doc, change, null, mergeOldSpans(doc, change));
+ });
+ }
+ }
+
+ // Sub-views need their line numbers shifted when text is added
+ // above or below them in the parent document.
+ function shiftDoc(doc, distance) {
+ if (distance == 0) return;
+ doc.first += distance;
+ doc.sel = new Selection(map(doc.sel.ranges, function(range) {
+ return new Range(Pos(range.anchor.line + distance, range.anchor.ch),
+ Pos(range.head.line + distance, range.head.ch));
+ }), doc.sel.primIndex);
+ if (doc.cm) {
+ regChange(doc.cm, doc.first, doc.first - distance, distance);
+ for (var d = doc.cm.display, l = d.viewFrom; l < d.viewTo; l++)
+ regLineChange(doc.cm, l, "gutter");
+ }
+ }
+
+ // More lower-level change function, handling only a single document
+ // (not linked ones).
+ function makeChangeSingleDoc(doc, change, selAfter, spans) {
+ if (doc.cm && !doc.cm.curOp)
+ return operation(doc.cm, makeChangeSingleDoc)(doc, change, selAfter, spans);
+
+ if (change.to.line < doc.first) {
+ shiftDoc(doc, change.text.length - 1 - (change.to.line - change.from.line));
+ return;
+ }
+ if (change.from.line > doc.lastLine()) return;
+
+ // Clip the change to the size of this doc
+ if (change.from.line < doc.first) {
+ var shift = change.text.length - 1 - (doc.first - change.from.line);
+ shiftDoc(doc, shift);
+ change = {from: Pos(doc.first, 0), to: Pos(change.to.line + shift, change.to.ch),
+ text: [lst(change.text)], origin: change.origin};
+ }
+ var last = doc.lastLine();
+ if (change.to.line > last) {
+ change = {from: change.from, to: Pos(last, getLine(doc, last).text.length),
+ text: [change.text[0]], origin: change.origin};
+ }
+
+ change.removed = getBetween(doc, change.from, change.to);
+
+ if (!selAfter) selAfter = computeSelAfterChange(doc, change);
+ if (doc.cm) makeChangeSingleDocInEditor(doc.cm, change, spans);
+ else updateDoc(doc, change, spans);
+ setSelectionNoUndo(doc, selAfter, sel_dontScroll);
+ }
+
+ // Handle the interaction of a change to a document with the editor
+ // that this document is part of.
+ function makeChangeSingleDocInEditor(cm, change, spans) {
+ var doc = cm.doc, display = cm.display, from = change.from, to = change.to;
+
+ var recomputeMaxLength = false, checkWidthStart = from.line;
+ if (!cm.options.lineWrapping) {
+ checkWidthStart = lineNo(visualLine(getLine(doc, from.line)));
+ doc.iter(checkWidthStart, to.line + 1, function(line) {
+ if (line == display.maxLine) {
+ recomputeMaxLength = true;
+ return true;
+ }
+ });
+ }
+
+ if (doc.sel.contains(change.from, change.to) > -1)
+ signalCursorActivity(cm);
+
+ updateDoc(doc, change, spans, estimateHeight(cm));
+
+ if (!cm.options.lineWrapping) {
+ doc.iter(checkWidthStart, from.line + change.text.length, function(line) {
+ var len = lineLength(line);
+ if (len > display.maxLineLength) {
+ display.maxLine = line;
+ display.maxLineLength = len;
+ display.maxLineChanged = true;
+ recomputeMaxLength = false;
+ }
+ });
+ if (recomputeMaxLength) cm.curOp.updateMaxLine = true;
+ }
+
+ // Adjust frontier, schedule worker
+ doc.frontier = Math.min(doc.frontier, from.line);
+ startWorker(cm, 400);
+
+ var lendiff = change.text.length - (to.line - from.line) - 1;
+ // Remember that these lines changed, for updating the display
+ if (change.full)
+ regChange(cm);
+ else if (from.line == to.line && change.text.length == 1 && !isWholeLineUpdate(cm.doc, change))
+ regLineChange(cm, from.line, "text");
+ else
+ regChange(cm, from.line, to.line + 1, lendiff);
+
+ var changesHandler = hasHandler(cm, "changes"), changeHandler = hasHandler(cm, "change");
+ if (changeHandler || changesHandler) {
+ var obj = {
+ from: from, to: to,
+ text: change.text,
+ removed: change.removed,
+ origin: change.origin
+ };
+ if (changeHandler) signalLater(cm, "change", cm, obj);
+ if (changesHandler) (cm.curOp.changeObjs || (cm.curOp.changeObjs = [])).push(obj);
+ }
+ cm.display.selForContextMenu = null;
+ }
+
+ function replaceRange(doc, code, from, to, origin) {
+ if (!to) to = from;
+ if (cmp(to, from) < 0) { var tmp = to; to = from; from = tmp; }
+ if (typeof code == "string") code = doc.splitLines(code);
+ makeChange(doc, {from: from, to: to, text: code, origin: origin});
+ }
+
+ // SCROLLING THINGS INTO VIEW
+
+ // If an editor sits on the top or bottom of the window, partially
+ // scrolled out of view, this ensures that the cursor is visible.
+ function maybeScrollWindow(cm, coords) {
+ if (signalDOMEvent(cm, "scrollCursorIntoView")) return;
+
+ var display = cm.display, box = display.sizer.getBoundingClientRect(), doScroll = null;
+ if (coords.top + box.top < 0) doScroll = true;
+ else if (coords.bottom + box.top > (window.innerHeight || document.documentElement.clientHeight)) doScroll = false;
+ if (doScroll != null && !phantom) {
+ var scrollNode = elt("div", "\u200b", null, "position: absolute; top: " +
+ (coords.top - display.viewOffset - paddingTop(cm.display)) + "px; height: " +
+ (coords.bottom - coords.top + scrollGap(cm) + display.barHeight) + "px; left: " +
+ coords.left + "px; width: 2px;");
+ cm.display.lineSpace.appendChild(scrollNode);
+ scrollNode.scrollIntoView(doScroll);
+ cm.display.lineSpace.removeChild(scrollNode);
+ }
+ }
+
+ // Scroll a given position into view (immediately), verifying that
+ // it actually became visible (as line heights are accurately
+ // measured, the position of something may 'drift' during drawing).
+ function scrollPosIntoView(cm, pos, end, margin) {
+ if (margin == null) margin = 0;
+ for (var limit = 0; limit < 5; limit++) {
+ var changed = false, coords = cursorCoords(cm, pos);
+ var endCoords = !end || end == pos ? coords : cursorCoords(cm, end);
+ var scrollPos = calculateScrollPos(cm, Math.min(coords.left, endCoords.left),
+ Math.min(coords.top, endCoords.top) - margin,
+ Math.max(coords.left, endCoords.left),
+ Math.max(coords.bottom, endCoords.bottom) + margin);
+ var startTop = cm.doc.scrollTop, startLeft = cm.doc.scrollLeft;
+ if (scrollPos.scrollTop != null) {
+ setScrollTop(cm, scrollPos.scrollTop);
+ if (Math.abs(cm.doc.scrollTop - startTop) > 1) changed = true;
+ }
+ if (scrollPos.scrollLeft != null) {
+ setScrollLeft(cm, scrollPos.scrollLeft);
+ if (Math.abs(cm.doc.scrollLeft - startLeft) > 1) changed = true;
+ }
+ if (!changed) break;
+ }
+ return coords;
+ }
+
+ // Scroll a given set of coordinates into view (immediately).
+ function scrollIntoView(cm, x1, y1, x2, y2) {
+ var scrollPos = calculateScrollPos(cm, x1, y1, x2, y2);
+ if (scrollPos.scrollTop != null) setScrollTop(cm, scrollPos.scrollTop);
+ if (scrollPos.scrollLeft != null) setScrollLeft(cm, scrollPos.scrollLeft);
+ }
+
+ // Calculate a new scroll position needed to scroll the given
+ // rectangle into view. Returns an object with scrollTop and
+ // scrollLeft properties. When these are undefined, the
+ // vertical/horizontal position does not need to be adjusted.
+ function calculateScrollPos(cm, x1, y1, x2, y2) {
+ var display = cm.display, snapMargin = textHeight(cm.display);
+ if (y1 < 0) y1 = 0;
+ var screentop = cm.curOp && cm.curOp.scrollTop != null ? cm.curOp.scrollTop : display.scroller.scrollTop;
+ var screen = displayHeight(cm), result = {};
+ if (y2 - y1 > screen) y2 = y1 + screen;
+ var docBottom = cm.doc.height + paddingVert(display);
+ var atTop = y1 < snapMargin, atBottom = y2 > docBottom - snapMargin;
+ if (y1 < screentop) {
+ result.scrollTop = atTop ? 0 : y1;
+ } else if (y2 > screentop + screen) {
+ var newTop = Math.min(y1, (atBottom ? docBottom : y2) - screen);
+ if (newTop != screentop) result.scrollTop = newTop;
+ }
+
+ var screenleft = cm.curOp && cm.curOp.scrollLeft != null ? cm.curOp.scrollLeft : display.scroller.scrollLeft;
+ var screenw = displayWidth(cm) - (cm.options.fixedGutter ? display.gutters.offsetWidth : 0);
+ var tooWide = x2 - x1 > screenw;
+ if (tooWide) x2 = x1 + screenw;
+ if (x1 < 10)
+ result.scrollLeft = 0;
+ else if (x1 < screenleft)
+ result.scrollLeft = Math.max(0, x1 - (tooWide ? 0 : 10));
+ else if (x2 > screenw + screenleft - 3)
+ result.scrollLeft = x2 + (tooWide ? 0 : 10) - screenw;
+ return result;
+ }
+
+ // Store a relative adjustment to the scroll position in the current
+ // operation (to be applied when the operation finishes).
+ function addToScrollPos(cm, left, top) {
+ if (left != null || top != null) resolveScrollToPos(cm);
+ if (left != null)
+ cm.curOp.scrollLeft = (cm.curOp.scrollLeft == null ? cm.doc.scrollLeft : cm.curOp.scrollLeft) + left;
+ if (top != null)
+ cm.curOp.scrollTop = (cm.curOp.scrollTop == null ? cm.doc.scrollTop : cm.curOp.scrollTop) + top;
+ }
+
+ // Make sure that at the end of the operation the current cursor is
+ // shown.
+ function ensureCursorVisible(cm) {
+ resolveScrollToPos(cm);
+ var cur = cm.getCursor(), from = cur, to = cur;
+ if (!cm.options.lineWrapping) {
+ from = cur.ch ? Pos(cur.line, cur.ch - 1) : cur;
+ to = Pos(cur.line, cur.ch + 1);
+ }
+ cm.curOp.scrollToPos = {from: from, to: to, margin: cm.options.cursorScrollMargin, isCursor: true};
+ }
+
+ // When an operation has its scrollToPos property set, and another
+ // scroll action is applied before the end of the operation, this
+ // 'simulates' scrolling that position into view in a cheap way, so
+ // that the effect of intermediate scroll commands is not ignored.
+ function resolveScrollToPos(cm) {
+ var range = cm.curOp.scrollToPos;
+ if (range) {
+ cm.curOp.scrollToPos = null;
+ var from = estimateCoords(cm, range.from), to = estimateCoords(cm, range.to);
+ var sPos = calculateScrollPos(cm, Math.min(from.left, to.left),
+ Math.min(from.top, to.top) - range.margin,
+ Math.max(from.right, to.right),
+ Math.max(from.bottom, to.bottom) + range.margin);
+ cm.scrollTo(sPos.scrollLeft, sPos.scrollTop);
+ }
+ }
+
+ // API UTILITIES
+
+ // Indent the given line. The how parameter can be "smart",
+ // "add"/null, "subtract", or "prev". When aggressive is false
+ // (typically set to true for forced single-line indents), empty
+ // lines are not indented, and places where the mode returns Pass
+ // are left alone.
+ function indentLine(cm, n, how, aggressive) {
+ var doc = cm.doc, state;
+ if (how == null) how = "add";
+ if (how == "smart") {
+ // Fall back to "prev" when the mode doesn't have an indentation
+ // method.
+ if (!doc.mode.indent) how = "prev";
+ else state = getStateBefore(cm, n);
+ }
+
+ var tabSize = cm.options.tabSize;
+ var line = getLine(doc, n), curSpace = countColumn(line.text, null, tabSize);
+ if (line.stateAfter) line.stateAfter = null;
+ var curSpaceString = line.text.match(/^\s*/)[0], indentation;
+ if (!aggressive && !/\S/.test(line.text)) {
+ indentation = 0;
+ how = "not";
+ } else if (how == "smart") {
+ indentation = doc.mode.indent(state, line.text.slice(curSpaceString.length), line.text);
+ if (indentation == Pass || indentation > 150) {
+ if (!aggressive) return;
+ how = "prev";
+ }
+ }
+ if (how == "prev") {
+ if (n > doc.first) indentation = countColumn(getLine(doc, n-1).text, null, tabSize);
+ else indentation = 0;
+ } else if (how == "add") {
+ indentation = curSpace + cm.options.indentUnit;
+ } else if (how == "subtract") {
+ indentation = curSpace - cm.options.indentUnit;
+ } else if (typeof how == "number") {
+ indentation = curSpace + how;
+ }
+ indentation = Math.max(0, indentation);
+
+ var indentString = "", pos = 0;
+ if (cm.options.indentWithTabs)
+ for (var i = Math.floor(indentation / tabSize); i; --i) {pos += tabSize; indentString += "\t";}
+ if (pos < indentation) indentString += spaceStr(indentation - pos);
+
+ if (indentString != curSpaceString) {
+ replaceRange(doc, indentString, Pos(n, 0), Pos(n, curSpaceString.length), "+input");
+ line.stateAfter = null;
+ return true;
+ } else {
+ // Ensure that, if the cursor was in the whitespace at the start
+ // of the line, it is moved to the end of that space.
+ for (var i = 0; i < doc.sel.ranges.length; i++) {
+ var range = doc.sel.ranges[i];
+ if (range.head.line == n && range.head.ch < curSpaceString.length) {
+ var pos = Pos(n, curSpaceString.length);
+ replaceOneSelection(doc, i, new Range(pos, pos));
+ break;
+ }
+ }
+ }
+ }
+
+ // Utility for applying a change to a line by handle or number,
+ // returning the number and optionally registering the line as
+ // changed.
+ function changeLine(doc, handle, changeType, op) {
+ var no = handle, line = handle;
+ if (typeof handle == "number") line = getLine(doc, clipLine(doc, handle));
+ else no = lineNo(handle);
+ if (no == null) return null;
+ if (op(line, no) && doc.cm) regLineChange(doc.cm, no, changeType);
+ return line;
+ }
+
+ // Helper for deleting text near the selection(s), used to implement
+ // backspace, delete, and similar functionality.
+ function deleteNearSelection(cm, compute) {
+ var ranges = cm.doc.sel.ranges, kill = [];
+ // Build up a set of ranges to kill first, merging overlapping
+ // ranges.
+ for (var i = 0; i < ranges.length; i++) {
+ var toKill = compute(ranges[i]);
+ while (kill.length && cmp(toKill.from, lst(kill).to) <= 0) {
+ var replaced = kill.pop();
+ if (cmp(replaced.from, toKill.from) < 0) {
+ toKill.from = replaced.from;
+ break;
+ }
+ }
+ kill.push(toKill);
+ }
+ // Next, remove those actual ranges.
+ runInOp(cm, function() {
+ for (var i = kill.length - 1; i >= 0; i--)
+ replaceRange(cm.doc, "", kill[i].from, kill[i].to, "+delete");
+ ensureCursorVisible(cm);
+ });
+ }
+
+ // Used for horizontal relative motion. Dir is -1 or 1 (left or
+ // right), unit can be "char", "column" (like char, but doesn't
+ // cross line boundaries), "word" (across next word), or "group" (to
+ // the start of next group of word or non-word-non-whitespace
+ // chars). The visually param controls whether, in right-to-left
+ // text, direction 1 means to move towards the next index in the
+ // string, or towards the character to the right of the current
+ // position. The resulting position will have a hitSide=true
+ // property if it reached the end of the document.
+ function findPosH(doc, pos, dir, unit, visually) {
+ var line = pos.line, ch = pos.ch, origDir = dir;
+ var lineObj = getLine(doc, line);
+ function findNextLine() {
+ var l = line + dir;
+ if (l < doc.first || l >= doc.first + doc.size) return false
+ line = l;
+ return lineObj = getLine(doc, l);
+ }
+ function moveOnce(boundToLine) {
+ var next = (visually ? moveVisually : moveLogically)(lineObj, ch, dir, true);
+ if (next == null) {
+ if (!boundToLine && findNextLine()) {
+ if (visually) ch = (dir < 0 ? lineRight : lineLeft)(lineObj);
+ else ch = dir < 0 ? lineObj.text.length : 0;
+ } else return false
+ } else ch = next;
+ return true;
+ }
+
+ if (unit == "char") {
+ moveOnce()
+ } else if (unit == "column") {
+ moveOnce(true)
+ } else if (unit == "word" || unit == "group") {
+ var sawType = null, group = unit == "group";
+ var helper = doc.cm && doc.cm.getHelper(pos, "wordChars");
+ for (var first = true;; first = false) {
+ if (dir < 0 && !moveOnce(!first)) break;
+ var cur = lineObj.text.charAt(ch) || "\n";
+ var type = isWordChar(cur, helper) ? "w"
+ : group && cur == "\n" ? "n"
+ : !group || /\s/.test(cur) ? null
+ : "p";
+ if (group && !first && !type) type = "s";
+ if (sawType && sawType != type) {
+ if (dir < 0) {dir = 1; moveOnce();}
+ break;
+ }
+
+ if (type) sawType = type;
+ if (dir > 0 && !moveOnce(!first)) break;
+ }
+ }
+ var result = skipAtomic(doc, Pos(line, ch), pos, origDir, true);
+ if (!cmp(pos, result)) result.hitSide = true;
+ return result;
+ }
+
+ // For relative vertical movement. Dir may be -1 or 1. Unit can be
+ // "page" or "line". The resulting position will have a hitSide=true
+ // property if it reached the end of the document.
+ function findPosV(cm, pos, dir, unit) {
+ var doc = cm.doc, x = pos.left, y;
+ if (unit == "page") {
+ var pageSize = Math.min(cm.display.wrapper.clientHeight, window.innerHeight || document.documentElement.clientHeight);
+ y = pos.top + dir * (pageSize - (dir < 0 ? 1.5 : .5) * textHeight(cm.display));
+ } else if (unit == "line") {
+ y = dir > 0 ? pos.bottom + 3 : pos.top - 3;
+ }
+ for (;;) {
+ var target = coordsChar(cm, x, y);
+ if (!target.outside) break;
+ if (dir < 0 ? y <= 0 : y >= doc.height) { target.hitSide = true; break; }
+ y += dir * 5;
+ }
+ return target;
+ }
+
+ // EDITOR METHODS
+
+ // The publicly visible API. Note that methodOp(f) means
+ // 'wrap f in an operation, performed on its `this` parameter'.
+
+ // This is not the complete set of editor methods. Most of the
+ // methods defined on the Doc type are also injected into
+ // CodeMirror.prototype, for backwards compatibility and
+ // convenience.
+
+ CodeMirror.prototype = {
+ constructor: CodeMirror,
+ focus: function(){window.focus(); this.display.input.focus();},
+
+ setOption: function(option, value) {
+ var options = this.options, old = options[option];
+ if (options[option] == value && option != "mode") return;
+ options[option] = value;
+ if (optionHandlers.hasOwnProperty(option))
+ operation(this, optionHandlers[option])(this, value, old);
+ },
+
+ getOption: function(option) {return this.options[option];},
+ getDoc: function() {return this.doc;},
+
+ addKeyMap: function(map, bottom) {
+ this.state.keyMaps[bottom ? "push" : "unshift"](getKeyMap(map));
+ },
+ removeKeyMap: function(map) {
+ var maps = this.state.keyMaps;
+ for (var i = 0; i < maps.length; ++i)
+ if (maps[i] == map || maps[i].name == map) {
+ maps.splice(i, 1);
+ return true;
+ }
+ },
+
+ addOverlay: methodOp(function(spec, options) {
+ var mode = spec.token ? spec : CodeMirror.getMode(this.options, spec);
+ if (mode.startState) throw new Error("Overlays may not be stateful.");
+ this.state.overlays.push({mode: mode, modeSpec: spec, opaque: options && options.opaque});
+ this.state.modeGen++;
+ regChange(this);
+ }),
+ removeOverlay: methodOp(function(spec) {
+ var overlays = this.state.overlays;
+ for (var i = 0; i < overlays.length; ++i) {
+ var cur = overlays[i].modeSpec;
+ if (cur == spec || typeof spec == "string" && cur.name == spec) {
+ overlays.splice(i, 1);
+ this.state.modeGen++;
+ regChange(this);
+ return;
+ }
+ }
+ }),
+
+ indentLine: methodOp(function(n, dir, aggressive) {
+ if (typeof dir != "string" && typeof dir != "number") {
+ if (dir == null) dir = this.options.smartIndent ? "smart" : "prev";
+ else dir = dir ? "add" : "subtract";
+ }
+ if (isLine(this.doc, n)) indentLine(this, n, dir, aggressive);
+ }),
+ indentSelection: methodOp(function(how) {
+ var ranges = this.doc.sel.ranges, end = -1;
+ for (var i = 0; i < ranges.length; i++) {
+ var range = ranges[i];
+ if (!range.empty()) {
+ var from = range.from(), to = range.to();
+ var start = Math.max(end, from.line);
+ end = Math.min(this.lastLine(), to.line - (to.ch ? 0 : 1)) + 1;
+ for (var j = start; j < end; ++j)
+ indentLine(this, j, how);
+ var newRanges = this.doc.sel.ranges;
+ if (from.ch == 0 && ranges.length == newRanges.length && newRanges[i].from().ch > 0)
+ replaceOneSelection(this.doc, i, new Range(from, newRanges[i].to()), sel_dontScroll);
+ } else if (range.head.line > end) {
+ indentLine(this, range.head.line, how, true);
+ end = range.head.line;
+ if (i == this.doc.sel.primIndex) ensureCursorVisible(this);
+ }
+ }
+ }),
+
+ // Fetch the parser token for a given character. Useful for hacks
+ // that want to inspect the mode state (say, for completion).
+ getTokenAt: function(pos, precise) {
+ return takeToken(this, pos, precise);
+ },
+
+ getLineTokens: function(line, precise) {
+ return takeToken(this, Pos(line), precise, true);
+ },
+
+ getTokenTypeAt: function(pos) {
+ pos = clipPos(this.doc, pos);
+ var styles = getLineStyles(this, getLine(this.doc, pos.line));
+ var before = 0, after = (styles.length - 1) / 2, ch = pos.ch;
+ var type;
+ if (ch == 0) type = styles[2];
+ else for (;;) {
+ var mid = (before + after) >> 1;
+ if ((mid ? styles[mid * 2 - 1] : 0) >= ch) after = mid;
+ else if (styles[mid * 2 + 1] < ch) before = mid + 1;
+ else { type = styles[mid * 2 + 2]; break; }
+ }
+ var cut = type ? type.indexOf("cm-overlay ") : -1;
+ return cut < 0 ? type : cut == 0 ? null : type.slice(0, cut - 1);
+ },
+
+ getModeAt: function(pos) {
+ var mode = this.doc.mode;
+ if (!mode.innerMode) return mode;
+ return CodeMirror.innerMode(mode, this.getTokenAt(pos).state).mode;
+ },
+
+ getHelper: function(pos, type) {
+ return this.getHelpers(pos, type)[0];
+ },
+
+ getHelpers: function(pos, type) {
+ var found = [];
+ if (!helpers.hasOwnProperty(type)) return found;
+ var help = helpers[type], mode = this.getModeAt(pos);
+ if (typeof mode[type] == "string") {
+ if (help[mode[type]]) found.push(help[mode[type]]);
+ } else if (mode[type]) {
+ for (var i = 0; i < mode[type].length; i++) {
+ var val = help[mode[type][i]];
+ if (val) found.push(val);
+ }
+ } else if (mode.helperType && help[mode.helperType]) {
+ found.push(help[mode.helperType]);
+ } else if (help[mode.name]) {
+ found.push(help[mode.name]);
+ }
+ for (var i = 0; i < help._global.length; i++) {
+ var cur = help._global[i];
+ if (cur.pred(mode, this) && indexOf(found, cur.val) == -1)
+ found.push(cur.val);
+ }
+ return found;
+ },
+
+ getStateAfter: function(line, precise) {
+ var doc = this.doc;
+ line = clipLine(doc, line == null ? doc.first + doc.size - 1: line);
+ return getStateBefore(this, line + 1, precise);
+ },
+
+ cursorCoords: function(start, mode) {
+ var pos, range = this.doc.sel.primary();
+ if (start == null) pos = range.head;
+ else if (typeof start == "object") pos = clipPos(this.doc, start);
+ else pos = start ? range.from() : range.to();
+ return cursorCoords(this, pos, mode || "page");
+ },
+
+ charCoords: function(pos, mode) {
+ return charCoords(this, clipPos(this.doc, pos), mode || "page");
+ },
+
+ coordsChar: function(coords, mode) {
+ coords = fromCoordSystem(this, coords, mode || "page");
+ return coordsChar(this, coords.left, coords.top);
+ },
+
+ lineAtHeight: function(height, mode) {
+ height = fromCoordSystem(this, {top: height, left: 0}, mode || "page").top;
+ return lineAtHeight(this.doc, height + this.display.viewOffset);
+ },
+ heightAtLine: function(line, mode) {
+ var end = false, lineObj;
+ if (typeof line == "number") {
+ var last = this.doc.first + this.doc.size - 1;
+ if (line < this.doc.first) line = this.doc.first;
+ else if (line > last) { line = last; end = true; }
+ lineObj = getLine(this.doc, line);
+ } else {
+ lineObj = line;
+ }
+ return intoCoordSystem(this, lineObj, {top: 0, left: 0}, mode || "page").top +
+ (end ? this.doc.height - heightAtLine(lineObj) : 0);
+ },
+
+ defaultTextHeight: function() { return textHeight(this.display); },
+ defaultCharWidth: function() { return charWidth(this.display); },
+
+ setGutterMarker: methodOp(function(line, gutterID, value) {
+ return changeLine(this.doc, line, "gutter", function(line) {
+ var markers = line.gutterMarkers || (line.gutterMarkers = {});
+ markers[gutterID] = value;
+ if (!value && isEmpty(markers)) line.gutterMarkers = null;
+ return true;
+ });
+ }),
+
+ clearGutter: methodOp(function(gutterID) {
+ var cm = this, doc = cm.doc, i = doc.first;
+ doc.iter(function(line) {
+ if (line.gutterMarkers && line.gutterMarkers[gutterID]) {
+ line.gutterMarkers[gutterID] = null;
+ regLineChange(cm, i, "gutter");
+ if (isEmpty(line.gutterMarkers)) line.gutterMarkers = null;
+ }
+ ++i;
+ });
+ }),
+
+ lineInfo: function(line) {
+ if (typeof line == "number") {
+ if (!isLine(this.doc, line)) return null;
+ var n = line;
+ line = getLine(this.doc, line);
+ if (!line) return null;
+ } else {
+ var n = lineNo(line);
+ if (n == null) return null;
+ }
+ return {line: n, handle: line, text: line.text, gutterMarkers: line.gutterMarkers,
+ textClass: line.textClass, bgClass: line.bgClass, wrapClass: line.wrapClass,
+ widgets: line.widgets};
+ },
+
+ getViewport: function() { return {from: this.display.viewFrom, to: this.display.viewTo};},
+
+ addWidget: function(pos, node, scroll, vert, horiz) {
+ var display = this.display;
+ pos = cursorCoords(this, clipPos(this.doc, pos));
+ var top = pos.bottom, left = pos.left;
+ node.style.position = "absolute";
+ node.setAttribute("cm-ignore-events", "true");
+ this.display.input.setUneditable(node);
+ display.sizer.appendChild(node);
+ if (vert == "over") {
+ top = pos.top;
+ } else if (vert == "above" || vert == "near") {
+ var vspace = Math.max(display.wrapper.clientHeight, this.doc.height),
+ hspace = Math.max(display.sizer.clientWidth, display.lineSpace.clientWidth);
+ // Default to positioning above (if specified and possible); otherwise default to positioning below
+ if ((vert == 'above' || pos.bottom + node.offsetHeight > vspace) && pos.top > node.offsetHeight)
+ top = pos.top - node.offsetHeight;
+ else if (pos.bottom + node.offsetHeight <= vspace)
+ top = pos.bottom;
+ if (left + node.offsetWidth > hspace)
+ left = hspace - node.offsetWidth;
+ }
+ node.style.top = top + "px";
+ node.style.left = node.style.right = "";
+ if (horiz == "right") {
+ left = display.sizer.clientWidth - node.offsetWidth;
+ node.style.right = "0px";
+ } else {
+ if (horiz == "left") left = 0;
+ else if (horiz == "middle") left = (display.sizer.clientWidth - node.offsetWidth) / 2;
+ node.style.left = left + "px";
+ }
+ if (scroll)
+ scrollIntoView(this, left, top, left + node.offsetWidth, top + node.offsetHeight);
+ },
+
+ triggerOnKeyDown: methodOp(onKeyDown),
+ triggerOnKeyPress: methodOp(onKeyPress),
+ triggerOnKeyUp: onKeyUp,
+
+ execCommand: function(cmd) {
+ if (commands.hasOwnProperty(cmd))
+ return commands[cmd].call(null, this);
+ },
+
+ triggerElectric: methodOp(function(text) { triggerElectric(this, text); }),
+
+ findPosH: function(from, amount, unit, visually) {
+ var dir = 1;
+ if (amount < 0) { dir = -1; amount = -amount; }
+ for (var i = 0, cur = clipPos(this.doc, from); i < amount; ++i) {
+ cur = findPosH(this.doc, cur, dir, unit, visually);
+ if (cur.hitSide) break;
+ }
+ return cur;
+ },
+
+ moveH: methodOp(function(dir, unit) {
+ var cm = this;
+ cm.extendSelectionsBy(function(range) {
+ if (cm.display.shift || cm.doc.extend || range.empty())
+ return findPosH(cm.doc, range.head, dir, unit, cm.options.rtlMoveVisually);
+ else
+ return dir < 0 ? range.from() : range.to();
+ }, sel_move);
+ }),
+
+ deleteH: methodOp(function(dir, unit) {
+ var sel = this.doc.sel, doc = this.doc;
+ if (sel.somethingSelected())
+ doc.replaceSelection("", null, "+delete");
+ else
+ deleteNearSelection(this, function(range) {
+ var other = findPosH(doc, range.head, dir, unit, false);
+ return dir < 0 ? {from: other, to: range.head} : {from: range.head, to: other};
+ });
+ }),
+
+ findPosV: function(from, amount, unit, goalColumn) {
+ var dir = 1, x = goalColumn;
+ if (amount < 0) { dir = -1; amount = -amount; }
+ for (var i = 0, cur = clipPos(this.doc, from); i < amount; ++i) {
+ var coords = cursorCoords(this, cur, "div");
+ if (x == null) x = coords.left;
+ else coords.left = x;
+ cur = findPosV(this, coords, dir, unit);
+ if (cur.hitSide) break;
+ }
+ return cur;
+ },
+
+ moveV: methodOp(function(dir, unit) {
+ var cm = this, doc = this.doc, goals = [];
+ var collapse = !cm.display.shift && !doc.extend && doc.sel.somethingSelected();
+ doc.extendSelectionsBy(function(range) {
+ if (collapse)
+ return dir < 0 ? range.from() : range.to();
+ var headPos = cursorCoords(cm, range.head, "div");
+ if (range.goalColumn != null) headPos.left = range.goalColumn;
+ goals.push(headPos.left);
+ var pos = findPosV(cm, headPos, dir, unit);
+ if (unit == "page" && range == doc.sel.primary())
+ addToScrollPos(cm, null, charCoords(cm, pos, "div").top - headPos.top);
+ return pos;
+ }, sel_move);
+ if (goals.length) for (var i = 0; i < doc.sel.ranges.length; i++)
+ doc.sel.ranges[i].goalColumn = goals[i];
+ }),
+
+ // Find the word at the given position (as returned by coordsChar).
+ findWordAt: function(pos) {
+ var doc = this.doc, line = getLine(doc, pos.line).text;
+ var start = pos.ch, end = pos.ch;
+ if (line) {
+ var helper = this.getHelper(pos, "wordChars");
+ if ((pos.xRel < 0 || end == line.length) && start) --start; else ++end;
+ var startChar = line.charAt(start);
+ var check = isWordChar(startChar, helper)
+ ? function(ch) { return isWordChar(ch, helper); }
+ : /\s/.test(startChar) ? function(ch) {return /\s/.test(ch);}
+ : function(ch) {return !/\s/.test(ch) && !isWordChar(ch);};
+ while (start > 0 && check(line.charAt(start - 1))) --start;
+ while (end < line.length && check(line.charAt(end))) ++end;
+ }
+ return new Range(Pos(pos.line, start), Pos(pos.line, end));
+ },
+
+ toggleOverwrite: function(value) {
+ if (value != null && value == this.state.overwrite) return;
+ if (this.state.overwrite = !this.state.overwrite)
+ addClass(this.display.cursorDiv, "CodeMirror-overwrite");
+ else
+ rmClass(this.display.cursorDiv, "CodeMirror-overwrite");
+
+ signal(this, "overwriteToggle", this, this.state.overwrite);
+ },
+ hasFocus: function() { return this.display.input.getField() == activeElt(); },
+ isReadOnly: function() { return !!(this.options.readOnly || this.doc.cantEdit); },
+
+ scrollTo: methodOp(function(x, y) {
+ if (x != null || y != null) resolveScrollToPos(this);
+ if (x != null) this.curOp.scrollLeft = x;
+ if (y != null) this.curOp.scrollTop = y;
+ }),
+ getScrollInfo: function() {
+ var scroller = this.display.scroller;
+ return {left: scroller.scrollLeft, top: scroller.scrollTop,
+ height: scroller.scrollHeight - scrollGap(this) - this.display.barHeight,
+ width: scroller.scrollWidth - scrollGap(this) - this.display.barWidth,
+ clientHeight: displayHeight(this), clientWidth: displayWidth(this)};
+ },
+
+ scrollIntoView: methodOp(function(range, margin) {
+ if (range == null) {
+ range = {from: this.doc.sel.primary().head, to: null};
+ if (margin == null) margin = this.options.cursorScrollMargin;
+ } else if (typeof range == "number") {
+ range = {from: Pos(range, 0), to: null};
+ } else if (range.from == null) {
+ range = {from: range, to: null};
+ }
+ if (!range.to) range.to = range.from;
+ range.margin = margin || 0;
+
+ if (range.from.line != null) {
+ resolveScrollToPos(this);
+ this.curOp.scrollToPos = range;
+ } else {
+ var sPos = calculateScrollPos(this, Math.min(range.from.left, range.to.left),
+ Math.min(range.from.top, range.to.top) - range.margin,
+ Math.max(range.from.right, range.to.right),
+ Math.max(range.from.bottom, range.to.bottom) + range.margin);
+ this.scrollTo(sPos.scrollLeft, sPos.scrollTop);
+ }
+ }),
+
+ setSize: methodOp(function(width, height) {
+ var cm = this;
+ function interpret(val) {
+ return typeof val == "number" || /^\d+$/.test(String(val)) ? val + "px" : val;
+ }
+ if (width != null) cm.display.wrapper.style.width = interpret(width);
+ if (height != null) cm.display.wrapper.style.height = interpret(height);
+ if (cm.options.lineWrapping) clearLineMeasurementCache(this);
+ var lineNo = cm.display.viewFrom;
+ cm.doc.iter(lineNo, cm.display.viewTo, function(line) {
+ if (line.widgets) for (var i = 0; i < line.widgets.length; i++)
+ if (line.widgets[i].noHScroll) { regLineChange(cm, lineNo, "widget"); break; }
+ ++lineNo;
+ });
+ cm.curOp.forceUpdate = true;
+ signal(cm, "refresh", this);
+ }),
+
+ operation: function(f){return runInOp(this, f);},
+
+ refresh: methodOp(function() {
+ var oldHeight = this.display.cachedTextHeight;
+ regChange(this);
+ this.curOp.forceUpdate = true;
+ clearCaches(this);
+ this.scrollTo(this.doc.scrollLeft, this.doc.scrollTop);
+ updateGutterSpace(this);
+ if (oldHeight == null || Math.abs(oldHeight - textHeight(this.display)) > .5)
+ estimateLineHeights(this);
+ signal(this, "refresh", this);
+ }),
+
+ swapDoc: methodOp(function(doc) {
+ var old = this.doc;
+ old.cm = null;
+ attachDoc(this, doc);
+ clearCaches(this);
+ this.display.input.reset();
+ this.scrollTo(doc.scrollLeft, doc.scrollTop);
+ this.curOp.forceScroll = true;
+ signalLater(this, "swapDoc", this, old);
+ return old;
+ }),
+
+ getInputField: function(){return this.display.input.getField();},
+ getWrapperElement: function(){return this.display.wrapper;},
+ getScrollerElement: function(){return this.display.scroller;},
+ getGutterElement: function(){return this.display.gutters;}
+ };
+ eventMixin(CodeMirror);
+
+ // OPTION DEFAULTS
+
+ // The default configuration options.
+ var defaults = CodeMirror.defaults = {};
+ // Functions to run when options are changed.
+ var optionHandlers = CodeMirror.optionHandlers = {};
+
+ function option(name, deflt, handle, notOnInit) {
+ CodeMirror.defaults[name] = deflt;
+ if (handle) optionHandlers[name] =
+ notOnInit ? function(cm, val, old) {if (old != Init) handle(cm, val, old);} : handle;
+ }
+
+ // Passed to option handlers when there is no old value.
+ var Init = CodeMirror.Init = {toString: function(){return "CodeMirror.Init";}};
+
+ // These two are, on init, called from the constructor because they
+ // have to be initialized before the editor can start at all.
+ option("value", "", function(cm, val) {
+ cm.setValue(val);
+ }, true);
+ option("mode", null, function(cm, val) {
+ cm.doc.modeOption = val;
+ loadMode(cm);
+ }, true);
+
+ option("indentUnit", 2, loadMode, true);
+ option("indentWithTabs", false);
+ option("smartIndent", true);
+ option("tabSize", 4, function(cm) {
+ resetModeState(cm);
+ clearCaches(cm);
+ regChange(cm);
+ }, true);
+ option("lineSeparator", null, function(cm, val) {
+ cm.doc.lineSep = val;
+ if (!val) return;
+ var newBreaks = [], lineNo = cm.doc.first;
+ cm.doc.iter(function(line) {
+ for (var pos = 0;;) {
+ var found = line.text.indexOf(val, pos);
+ if (found == -1) break;
+ pos = found + val.length;
+ newBreaks.push(Pos(lineNo, found));
+ }
+ lineNo++;
+ });
+ for (var i = newBreaks.length - 1; i >= 0; i--)
+ replaceRange(cm.doc, val, newBreaks[i], Pos(newBreaks[i].line, newBreaks[i].ch + val.length))
+ });
+ option("specialChars", /[\u0000-\u001f\u007f\u00ad\u200b-\u200f\u2028\u2029\ufeff]/g, function(cm, val, old) {
+ cm.state.specialChars = new RegExp(val.source + (val.test("\t") ? "" : "|\t"), "g");
+ if (old != CodeMirror.Init) cm.refresh();
+ });
+ option("specialCharPlaceholder", defaultSpecialCharPlaceholder, function(cm) {cm.refresh();}, true);
+ option("electricChars", true);
+ option("inputStyle", mobile ? "contenteditable" : "textarea", function() {
+ throw new Error("inputStyle can not (yet) be changed in a running editor"); // FIXME
+ }, true);
+ option("rtlMoveVisually", !windows);
+ option("wholeLineUpdateBefore", true);
+
+ option("theme", "default", function(cm) {
+ themeChanged(cm);
+ guttersChanged(cm);
+ }, true);
+ option("keyMap", "default", function(cm, val, old) {
+ var next = getKeyMap(val);
+ var prev = old != CodeMirror.Init && getKeyMap(old);
+ if (prev && prev.detach) prev.detach(cm, next);
+ if (next.attach) next.attach(cm, prev || null);
+ });
+ option("extraKeys", null);
+
+ option("lineWrapping", false, wrappingChanged, true);
+ option("gutters", [], function(cm) {
+ setGuttersForLineNumbers(cm.options);
+ guttersChanged(cm);
+ }, true);
+ option("fixedGutter", true, function(cm, val) {
+ cm.display.gutters.style.left = val ? compensateForHScroll(cm.display) + "px" : "0";
+ cm.refresh();
+ }, true);
+ option("coverGutterNextToScrollbar", false, function(cm) {updateScrollbars(cm);}, true);
+ option("scrollbarStyle", "native", function(cm) {
+ initScrollbars(cm);
+ updateScrollbars(cm);
+ cm.display.scrollbars.setScrollTop(cm.doc.scrollTop);
+ cm.display.scrollbars.setScrollLeft(cm.doc.scrollLeft);
+ }, true);
+ option("lineNumbers", false, function(cm) {
+ setGuttersForLineNumbers(cm.options);
+ guttersChanged(cm);
+ }, true);
+ option("firstLineNumber", 1, guttersChanged, true);
+ option("lineNumberFormatter", function(integer) {return integer;}, guttersChanged, true);
+ option("showCursorWhenSelecting", false, updateSelection, true);
+
+ option("resetSelectionOnContextMenu", true);
+ option("lineWiseCopyCut", true);
+
+ option("readOnly", false, function(cm, val) {
+ if (val == "nocursor") {
+ onBlur(cm);
+ cm.display.input.blur();
+ cm.display.disabled = true;
+ } else {
+ cm.display.disabled = false;
+ }
+ cm.display.input.readOnlyChanged(val)
+ });
+ option("disableInput", false, function(cm, val) {if (!val) cm.display.input.reset();}, true);
+ option("dragDrop", true, dragDropChanged);
+ option("allowDropFileTypes", null);
+
+ option("cursorBlinkRate", 530);
+ option("cursorScrollMargin", 0);
+ option("cursorHeight", 1, updateSelection, true);
+ option("singleCursorHeightPerLine", true, updateSelection, true);
+ option("workTime", 100);
+ option("workDelay", 100);
+ option("flattenSpans", true, resetModeState, true);
+ option("addModeClass", false, resetModeState, true);
+ option("pollInterval", 100);
+ option("undoDepth", 200, function(cm, val){cm.doc.history.undoDepth = val;});
+ option("historyEventDelay", 1250);
+ option("viewportMargin", 10, function(cm){cm.refresh();}, true);
+ option("maxHighlightLength", 10000, resetModeState, true);
+ option("moveInputWithCursor", true, function(cm, val) {
+ if (!val) cm.display.input.resetPosition();
+ });
+
+ option("tabindex", null, function(cm, val) {
+ cm.display.input.getField().tabIndex = val || "";
+ });
+ option("autofocus", null);
+
+ // MODE DEFINITION AND QUERYING
+
+ // Known modes, by name and by MIME
+ var modes = CodeMirror.modes = {}, mimeModes = CodeMirror.mimeModes = {};
+
+ // Extra arguments are stored as the mode's dependencies, which is
+ // used by (legacy) mechanisms like loadmode.js to automatically
+ // load a mode. (Preferred mechanism is the require/define calls.)
+ CodeMirror.defineMode = function(name, mode) {
+ if (!CodeMirror.defaults.mode && name != "null") CodeMirror.defaults.mode = name;
+ if (arguments.length > 2)
+ mode.dependencies = Array.prototype.slice.call(arguments, 2);
+ modes[name] = mode;
+ };
+
+ CodeMirror.defineMIME = function(mime, spec) {
+ mimeModes[mime] = spec;
+ };
+
+ // Given a MIME type, a {name, ...options} config object, or a name
+ // string, return a mode config object.
+ CodeMirror.resolveMode = function(spec) {
+ if (typeof spec == "string" && mimeModes.hasOwnProperty(spec)) {
+ spec = mimeModes[spec];
+ } else if (spec && typeof spec.name == "string" && mimeModes.hasOwnProperty(spec.name)) {
+ var found = mimeModes[spec.name];
+ if (typeof found == "string") found = {name: found};
+ spec = createObj(found, spec);
+ spec.name = found.name;
+ } else if (typeof spec == "string" && /^[\w\-]+\/[\w\-]+\+xml$/.test(spec)) {
+ return CodeMirror.resolveMode("application/xml");
+ }
+ if (typeof spec == "string") return {name: spec};
+ else return spec || {name: "null"};
+ };
+
+ // Given a mode spec (anything that resolveMode accepts), find and
+ // initialize an actual mode object.
+ CodeMirror.getMode = function(options, spec) {
+ var spec = CodeMirror.resolveMode(spec);
+ var mfactory = modes[spec.name];
+ if (!mfactory) return CodeMirror.getMode(options, "text/plain");
+ var modeObj = mfactory(options, spec);
+ if (modeExtensions.hasOwnProperty(spec.name)) {
+ var exts = modeExtensions[spec.name];
+ for (var prop in exts) {
+ if (!exts.hasOwnProperty(prop)) continue;
+ if (modeObj.hasOwnProperty(prop)) modeObj["_" + prop] = modeObj[prop];
+ modeObj[prop] = exts[prop];
+ }
+ }
+ modeObj.name = spec.name;
+ if (spec.helperType) modeObj.helperType = spec.helperType;
+ if (spec.modeProps) for (var prop in spec.modeProps)
+ modeObj[prop] = spec.modeProps[prop];
+
+ return modeObj;
+ };
+
+ // Minimal default mode.
+ CodeMirror.defineMode("null", function() {
+ return {token: function(stream) {stream.skipToEnd();}};
+ });
+ CodeMirror.defineMIME("text/plain", "null");
+
+ // This can be used to attach properties to mode objects from
+ // outside the actual mode definition.
+ var modeExtensions = CodeMirror.modeExtensions = {};
+ CodeMirror.extendMode = function(mode, properties) {
+ var exts = modeExtensions.hasOwnProperty(mode) ? modeExtensions[mode] : (modeExtensions[mode] = {});
+ copyObj(properties, exts);
+ };
+
+ // EXTENSIONS
+
+ CodeMirror.defineExtension = function(name, func) {
+ CodeMirror.prototype[name] = func;
+ };
+ CodeMirror.defineDocExtension = function(name, func) {
+ Doc.prototype[name] = func;
+ };
+ CodeMirror.defineOption = option;
+
+ var initHooks = [];
+ CodeMirror.defineInitHook = function(f) {initHooks.push(f);};
+
+ var helpers = CodeMirror.helpers = {};
+ CodeMirror.registerHelper = function(type, name, value) {
+ if (!helpers.hasOwnProperty(type)) helpers[type] = CodeMirror[type] = {_global: []};
+ helpers[type][name] = value;
+ };
+ CodeMirror.registerGlobalHelper = function(type, name, predicate, value) {
+ CodeMirror.registerHelper(type, name, value);
+ helpers[type]._global.push({pred: predicate, val: value});
+ };
+
+ // MODE STATE HANDLING
+
+ // Utility functions for working with state. Exported because nested
+ // modes need to do this for their inner modes.
+
+ var copyState = CodeMirror.copyState = function(mode, state) {
+ if (state === true) return state;
+ if (mode.copyState) return mode.copyState(state);
+ var nstate = {};
+ for (var n in state) {
+ var val = state[n];
+ if (val instanceof Array) val = val.concat([]);
+ nstate[n] = val;
+ }
+ return nstate;
+ };
+
+ var startState = CodeMirror.startState = function(mode, a1, a2) {
+ return mode.startState ? mode.startState(a1, a2) : true;
+ };
+
+ // Given a mode and a state (for that mode), find the inner mode and
+ // state at the position that the state refers to.
+ CodeMirror.innerMode = function(mode, state) {
+ while (mode.innerMode) {
+ var info = mode.innerMode(state);
+ if (!info || info.mode == mode) break;
+ state = info.state;
+ mode = info.mode;
+ }
+ return info || {mode: mode, state: state};
+ };
+
+ // STANDARD COMMANDS
+
+ // Commands are parameter-less actions that can be performed on an
+ // editor, mostly used for keybindings.
+ var commands = CodeMirror.commands = {
+ selectAll: function(cm) {cm.setSelection(Pos(cm.firstLine(), 0), Pos(cm.lastLine()), sel_dontScroll);},
+ singleSelection: function(cm) {
+ cm.setSelection(cm.getCursor("anchor"), cm.getCursor("head"), sel_dontScroll);
+ },
+ killLine: function(cm) {
+ deleteNearSelection(cm, function(range) {
+ if (range.empty()) {
+ var len = getLine(cm.doc, range.head.line).text.length;
+ if (range.head.ch == len && range.head.line < cm.lastLine())
+ return {from: range.head, to: Pos(range.head.line + 1, 0)};
+ else
+ return {from: range.head, to: Pos(range.head.line, len)};
+ } else {
+ return {from: range.from(), to: range.to()};
+ }
+ });
+ },
+ deleteLine: function(cm) {
+ deleteNearSelection(cm, function(range) {
+ return {from: Pos(range.from().line, 0),
+ to: clipPos(cm.doc, Pos(range.to().line + 1, 0))};
+ });
+ },
+ delLineLeft: function(cm) {
+ deleteNearSelection(cm, function(range) {
+ return {from: Pos(range.from().line, 0), to: range.from()};
+ });
+ },
+ delWrappedLineLeft: function(cm) {
+ deleteNearSelection(cm, function(range) {
+ var top = cm.charCoords(range.head, "div").top + 5;
+ var leftPos = cm.coordsChar({left: 0, top: top}, "div");
+ return {from: leftPos, to: range.from()};
+ });
+ },
+ delWrappedLineRight: function(cm) {
+ deleteNearSelection(cm, function(range) {
+ var top = cm.charCoords(range.head, "div").top + 5;
+ var rightPos = cm.coordsChar({left: cm.display.lineDiv.offsetWidth + 100, top: top}, "div");
+ return {from: range.from(), to: rightPos };
+ });
+ },
+ undo: function(cm) {cm.undo();},
+ redo: function(cm) {cm.redo();},
+ undoSelection: function(cm) {cm.undoSelection();},
+ redoSelection: function(cm) {cm.redoSelection();},
+ goDocStart: function(cm) {cm.extendSelection(Pos(cm.firstLine(), 0));},
+ goDocEnd: function(cm) {cm.extendSelection(Pos(cm.lastLine()));},
+ goLineStart: function(cm) {
+ cm.extendSelectionsBy(function(range) { return lineStart(cm, range.head.line); },
+ {origin: "+move", bias: 1});
+ },
+ goLineStartSmart: function(cm) {
+ cm.extendSelectionsBy(function(range) {
+ return lineStartSmart(cm, range.head);
+ }, {origin: "+move", bias: 1});
+ },
+ goLineEnd: function(cm) {
+ cm.extendSelectionsBy(function(range) { return lineEnd(cm, range.head.line); },
+ {origin: "+move", bias: -1});
+ },
+ goLineRight: function(cm) {
+ cm.extendSelectionsBy(function(range) {
+ var top = cm.charCoords(range.head, "div").top + 5;
+ return cm.coordsChar({left: cm.display.lineDiv.offsetWidth + 100, top: top}, "div");
+ }, sel_move);
+ },
+ goLineLeft: function(cm) {
+ cm.extendSelectionsBy(function(range) {
+ var top = cm.charCoords(range.head, "div").top + 5;
+ return cm.coordsChar({left: 0, top: top}, "div");
+ }, sel_move);
+ },
+ goLineLeftSmart: function(cm) {
+ cm.extendSelectionsBy(function(range) {
+ var top = cm.charCoords(range.head, "div").top + 5;
+ var pos = cm.coordsChar({left: 0, top: top}, "div");
+ if (pos.ch < cm.getLine(pos.line).search(/\S/)) return lineStartSmart(cm, range.head);
+ return pos;
+ }, sel_move);
+ },
+ goLineUp: function(cm) {cm.moveV(-1, "line");},
+ goLineDown: function(cm) {cm.moveV(1, "line");},
+ goPageUp: function(cm) {cm.moveV(-1, "page");},
+ goPageDown: function(cm) {cm.moveV(1, "page");},
+ goCharLeft: function(cm) {cm.moveH(-1, "char");},
+ goCharRight: function(cm) {cm.moveH(1, "char");},
+ goColumnLeft: function(cm) {cm.moveH(-1, "column");},
+ goColumnRight: function(cm) {cm.moveH(1, "column");},
+ goWordLeft: function(cm) {cm.moveH(-1, "word");},
+ goGroupRight: function(cm) {cm.moveH(1, "group");},
+ goGroupLeft: function(cm) {cm.moveH(-1, "group");},
+ goWordRight: function(cm) {cm.moveH(1, "word");},
+ delCharBefore: function(cm) {cm.deleteH(-1, "char");},
+ delCharAfter: function(cm) {cm.deleteH(1, "char");},
+ delWordBefore: function(cm) {cm.deleteH(-1, "word");},
+ delWordAfter: function(cm) {cm.deleteH(1, "word");},
+ delGroupBefore: function(cm) {cm.deleteH(-1, "group");},
+ delGroupAfter: function(cm) {cm.deleteH(1, "group");},
+ indentAuto: function(cm) {cm.indentSelection("smart");},
+ indentMore: function(cm) {cm.indentSelection("add");},
+ indentLess: function(cm) {cm.indentSelection("subtract");},
+ insertTab: function(cm) {cm.replaceSelection("\t");},
+ insertSoftTab: function(cm) {
+ var spaces = [], ranges = cm.listSelections(), tabSize = cm.options.tabSize;
+ for (var i = 0; i < ranges.length; i++) {
+ var pos = ranges[i].from();
+ var col = countColumn(cm.getLine(pos.line), pos.ch, tabSize);
+ spaces.push(spaceStr(tabSize - col % tabSize));
+ }
+ cm.replaceSelections(spaces);
+ },
+ defaultTab: function(cm) {
+ if (cm.somethingSelected()) cm.indentSelection("add");
+ else cm.execCommand("insertTab");
+ },
+ transposeChars: function(cm) {
+ runInOp(cm, function() {
+ var ranges = cm.listSelections(), newSel = [];
+ for (var i = 0; i < ranges.length; i++) {
+ var cur = ranges[i].head, line = getLine(cm.doc, cur.line).text;
+ if (line) {
+ if (cur.ch == line.length) cur = new Pos(cur.line, cur.ch - 1);
+ if (cur.ch > 0) {
+ cur = new Pos(cur.line, cur.ch + 1);
+ cm.replaceRange(line.charAt(cur.ch - 1) + line.charAt(cur.ch - 2),
+ Pos(cur.line, cur.ch - 2), cur, "+transpose");
+ } else if (cur.line > cm.doc.first) {
+ var prev = getLine(cm.doc, cur.line - 1).text;
+ if (prev)
+ cm.replaceRange(line.charAt(0) + cm.doc.lineSeparator() +
+ prev.charAt(prev.length - 1),
+ Pos(cur.line - 1, prev.length - 1), Pos(cur.line, 1), "+transpose");
+ }
+ }
+ newSel.push(new Range(cur, cur));
+ }
+ cm.setSelections(newSel);
+ });
+ },
+ newlineAndIndent: function(cm) {
+ runInOp(cm, function() {
+ var len = cm.listSelections().length;
+ for (var i = 0; i < len; i++) {
+ var range = cm.listSelections()[i];
+ cm.replaceRange(cm.doc.lineSeparator(), range.anchor, range.head, "+input");
+ cm.indentLine(range.from().line + 1, null, true);
+ }
+ ensureCursorVisible(cm);
+ });
+ },
+ openLine: function(cm) {cm.replaceSelection("\n", "start")},
+ toggleOverwrite: function(cm) {cm.toggleOverwrite();}
+ };
+
+
+ // STANDARD KEYMAPS
+
+ var keyMap = CodeMirror.keyMap = {};
+
+ keyMap.basic = {
+ "Left": "goCharLeft", "Right": "goCharRight", "Up": "goLineUp", "Down": "goLineDown",
+ "End": "goLineEnd", "Home": "goLineStartSmart", "PageUp": "goPageUp", "PageDown": "goPageDown",
+ "Delete": "delCharAfter", "Backspace": "delCharBefore", "Shift-Backspace": "delCharBefore",
+ "Tab": "defaultTab", "Shift-Tab": "indentAuto",
+ "Enter": "newlineAndIndent", "Insert": "toggleOverwrite",
+ "Esc": "singleSelection"
+ };
+ // Note that the save and find-related commands aren't defined by
+ // default. User code or addons can define them. Unknown commands
+ // are simply ignored.
+ keyMap.pcDefault = {
+ "Ctrl-A": "selectAll", "Ctrl-D": "deleteLine", "Ctrl-Z": "undo", "Shift-Ctrl-Z": "redo", "Ctrl-Y": "redo",
+ "Ctrl-Home": "goDocStart", "Ctrl-End": "goDocEnd", "Ctrl-Up": "goLineUp", "Ctrl-Down": "goLineDown",
+ "Ctrl-Left": "goGroupLeft", "Ctrl-Right": "goGroupRight", "Alt-Left": "goLineStart", "Alt-Right": "goLineEnd",
+ "Ctrl-Backspace": "delGroupBefore", "Ctrl-Delete": "delGroupAfter", "Ctrl-S": "save", "Ctrl-F": "find",
+ "Ctrl-G": "findNext", "Shift-Ctrl-G": "findPrev", "Shift-Ctrl-F": "replace", "Shift-Ctrl-R": "replaceAll",
+ "Ctrl-[": "indentLess", "Ctrl-]": "indentMore",
+ "Ctrl-U": "undoSelection", "Shift-Ctrl-U": "redoSelection", "Alt-U": "redoSelection",
+ fallthrough: "basic"
+ };
+ // Very basic readline/emacs-style bindings, which are standard on Mac.
+ keyMap.emacsy = {
+ "Ctrl-F": "goCharRight", "Ctrl-B": "goCharLeft", "Ctrl-P": "goLineUp", "Ctrl-N": "goLineDown",
+ "Alt-F": "goWordRight", "Alt-B": "goWordLeft", "Ctrl-A": "goLineStart", "Ctrl-E": "goLineEnd",
+ "Ctrl-V": "goPageDown", "Shift-Ctrl-V": "goPageUp", "Ctrl-D": "delCharAfter", "Ctrl-H": "delCharBefore",
+ "Alt-D": "delWordAfter", "Alt-Backspace": "delWordBefore", "Ctrl-K": "killLine", "Ctrl-T": "transposeChars",
+ "Ctrl-O": "openLine"
+ };
+ keyMap.macDefault = {
+ "Cmd-A": "selectAll", "Cmd-D": "deleteLine", "Cmd-Z": "undo", "Shift-Cmd-Z": "redo", "Cmd-Y": "redo",
+ "Cmd-Home": "goDocStart", "Cmd-Up": "goDocStart", "Cmd-End": "goDocEnd", "Cmd-Down": "goDocEnd", "Alt-Left": "goGroupLeft",
+ "Alt-Right": "goGroupRight", "Cmd-Left": "goLineLeft", "Cmd-Right": "goLineRight", "Alt-Backspace": "delGroupBefore",
+ "Ctrl-Alt-Backspace": "delGroupAfter", "Alt-Delete": "delGroupAfter", "Cmd-S": "save", "Cmd-F": "find",
+ "Cmd-G": "findNext", "Shift-Cmd-G": "findPrev", "Cmd-Alt-F": "replace", "Shift-Cmd-Alt-F": "replaceAll",
+ "Cmd-[": "indentLess", "Cmd-]": "indentMore", "Cmd-Backspace": "delWrappedLineLeft", "Cmd-Delete": "delWrappedLineRight",
+ "Cmd-U": "undoSelection", "Shift-Cmd-U": "redoSelection", "Ctrl-Up": "goDocStart", "Ctrl-Down": "goDocEnd",
+ fallthrough: ["basic", "emacsy"]
+ };
+ keyMap["default"] = mac ? keyMap.macDefault : keyMap.pcDefault;
+
+ // KEYMAP DISPATCH
+
+ function normalizeKeyName(name) {
+ var parts = name.split(/-(?!$)/), name = parts[parts.length - 1];
+ var alt, ctrl, shift, cmd;
+ for (var i = 0; i < parts.length - 1; i++) {
+ var mod = parts[i];
+ if (/^(cmd|meta|m)$/i.test(mod)) cmd = true;
+ else if (/^a(lt)?$/i.test(mod)) alt = true;
+ else if (/^(c|ctrl|control)$/i.test(mod)) ctrl = true;
+ else if (/^s(hift)$/i.test(mod)) shift = true;
+ else throw new Error("Unrecognized modifier name: " + mod);
+ }
+ if (alt) name = "Alt-" + name;
+ if (ctrl) name = "Ctrl-" + name;
+ if (cmd) name = "Cmd-" + name;
+ if (shift) name = "Shift-" + name;
+ return name;
+ }
+
+ // This is a kludge to keep keymaps mostly working as raw objects
+ // (backwards compatibility) while at the same time support features
+ // like normalization and multi-stroke key bindings. It compiles a
+ // new normalized keymap, and then updates the old object to reflect
+ // this.
+ CodeMirror.normalizeKeyMap = function(keymap) {
+ var copy = {};
+ for (var keyname in keymap) if (keymap.hasOwnProperty(keyname)) {
+ var value = keymap[keyname];
+ if (/^(name|fallthrough|(de|at)tach)$/.test(keyname)) continue;
+ if (value == "...") { delete keymap[keyname]; continue; }
+
+ var keys = map(keyname.split(" "), normalizeKeyName);
+ for (var i = 0; i < keys.length; i++) {
+ var val, name;
+ if (i == keys.length - 1) {
+ name = keys.join(" ");
+ val = value;
+ } else {
+ name = keys.slice(0, i + 1).join(" ");
+ val = "...";
+ }
+ var prev = copy[name];
+ if (!prev) copy[name] = val;
+ else if (prev != val) throw new Error("Inconsistent bindings for " + name);
+ }
+ delete keymap[keyname];
+ }
+ for (var prop in copy) keymap[prop] = copy[prop];
+ return keymap;
+ };
+
+ var lookupKey = CodeMirror.lookupKey = function(key, map, handle, context) {
+ map = getKeyMap(map);
+ var found = map.call ? map.call(key, context) : map[key];
+ if (found === false) return "nothing";
+ if (found === "...") return "multi";
+ if (found != null && handle(found)) return "handled";
+
+ if (map.fallthrough) {
+ if (Object.prototype.toString.call(map.fallthrough) != "[object Array]")
+ return lookupKey(key, map.fallthrough, handle, context);
+ for (var i = 0; i < map.fallthrough.length; i++) {
+ var result = lookupKey(key, map.fallthrough[i], handle, context);
+ if (result) return result;
+ }
+ }
+ };
+
+ // Modifier key presses don't count as 'real' key presses for the
+ // purpose of keymap fallthrough.
+ var isModifierKey = CodeMirror.isModifierKey = function(value) {
+ var name = typeof value == "string" ? value : keyNames[value.keyCode];
+ return name == "Ctrl" || name == "Alt" || name == "Shift" || name == "Mod";
+ };
+
+ // Look up the name of a key as indicated by an event object.
+ var keyName = CodeMirror.keyName = function(event, noShift) {
+ if (presto && event.keyCode == 34 && event["char"]) return false;
+ var base = keyNames[event.keyCode], name = base;
+ if (name == null || event.altGraphKey) return false;
+ if (event.altKey && base != "Alt") name = "Alt-" + name;
+ if ((flipCtrlCmd ? event.metaKey : event.ctrlKey) && base != "Ctrl") name = "Ctrl-" + name;
+ if ((flipCtrlCmd ? event.ctrlKey : event.metaKey) && base != "Cmd") name = "Cmd-" + name;
+ if (!noShift && event.shiftKey && base != "Shift") name = "Shift-" + name;
+ return name;
+ };
+
+ function getKeyMap(val) {
+ return typeof val == "string" ? keyMap[val] : val;
+ }
+
+ // FROMTEXTAREA
+
+ CodeMirror.fromTextArea = function(textarea, options) {
+ options = options ? copyObj(options) : {};
+ options.value = textarea.value;
+ if (!options.tabindex && textarea.tabIndex)
+ options.tabindex = textarea.tabIndex;
+ if (!options.placeholder && textarea.placeholder)
+ options.placeholder = textarea.placeholder;
+ // Set autofocus to true if this textarea is focused, or if it has
+ // autofocus and no other element is focused.
+ if (options.autofocus == null) {
+ var hasFocus = activeElt();
+ options.autofocus = hasFocus == textarea ||
+ textarea.getAttribute("autofocus") != null && hasFocus == document.body;
+ }
+
+ function save() {textarea.value = cm.getValue();}
+ if (textarea.form) {
+ on(textarea.form, "submit", save);
+ // Deplorable hack to make the submit method do the right thing.
+ if (!options.leaveSubmitMethodAlone) {
+ var form = textarea.form, realSubmit = form.submit;
+ try {
+ var wrappedSubmit = form.submit = function() {
+ save();
+ form.submit = realSubmit;
+ form.submit();
+ form.submit = wrappedSubmit;
+ };
+ } catch(e) {}
+ }
+ }
+
+ options.finishInit = function(cm) {
+ cm.save = save;
+ cm.getTextArea = function() { return textarea; };
+ cm.toTextArea = function() {
+ cm.toTextArea = isNaN; // Prevent this from being ran twice
+ save();
+ textarea.parentNode.removeChild(cm.getWrapperElement());
+ textarea.style.display = "";
+ if (textarea.form) {
+ off(textarea.form, "submit", save);
+ if (typeof textarea.form.submit == "function")
+ textarea.form.submit = realSubmit;
+ }
+ };
+ };
+
+ textarea.style.display = "none";
+ var cm = CodeMirror(function(node) {
+ textarea.parentNode.insertBefore(node, textarea.nextSibling);
+ }, options);
+ return cm;
+ };
+
+ // STRING STREAM
+
+ // Fed to the mode parsers, provides helper functions to make
+ // parsers more succinct.
+
+ var StringStream = CodeMirror.StringStream = function(string, tabSize) {
+ this.pos = this.start = 0;
+ this.string = string;
+ this.tabSize = tabSize || 8;
+ this.lastColumnPos = this.lastColumnValue = 0;
+ this.lineStart = 0;
+ };
+
+ StringStream.prototype = {
+ eol: function() {return this.pos >= this.string.length;},
+ sol: function() {return this.pos == this.lineStart;},
+ peek: function() {return this.string.charAt(this.pos) || undefined;},
+ next: function() {
+ if (this.pos < this.string.length)
+ return this.string.charAt(this.pos++);
+ },
+ eat: function(match) {
+ var ch = this.string.charAt(this.pos);
+ if (typeof match == "string") var ok = ch == match;
+ else var ok = ch && (match.test ? match.test(ch) : match(ch));
+ if (ok) {++this.pos; return ch;}
+ },
+ eatWhile: function(match) {
+ var start = this.pos;
+ while (this.eat(match)){}
+ return this.pos > start;
+ },
+ eatSpace: function() {
+ var start = this.pos;
+ while (/[\s\u00a0]/.test(this.string.charAt(this.pos))) ++this.pos;
+ return this.pos > start;
+ },
+ skipToEnd: function() {this.pos = this.string.length;},
+ skipTo: function(ch) {
+ var found = this.string.indexOf(ch, this.pos);
+ if (found > -1) {this.pos = found; return true;}
+ },
+ backUp: function(n) {this.pos -= n;},
+ column: function() {
+ if (this.lastColumnPos < this.start) {
+ this.lastColumnValue = countColumn(this.string, this.start, this.tabSize, this.lastColumnPos, this.lastColumnValue);
+ this.lastColumnPos = this.start;
+ }
+ return this.lastColumnValue - (this.lineStart ? countColumn(this.string, this.lineStart, this.tabSize) : 0);
+ },
+ indentation: function() {
+ return countColumn(this.string, null, this.tabSize) -
+ (this.lineStart ? countColumn(this.string, this.lineStart, this.tabSize) : 0);
+ },
+ match: function(pattern, consume, caseInsensitive) {
+ if (typeof pattern == "string") {
+ var cased = function(str) {return caseInsensitive ? str.toLowerCase() : str;};
+ var substr = this.string.substr(this.pos, pattern.length);
+ if (cased(substr) == cased(pattern)) {
+ if (consume !== false) this.pos += pattern.length;
+ return true;
+ }
+ } else {
+ var match = this.string.slice(this.pos).match(pattern);
+ if (match && match.index > 0) return null;
+ if (match && consume !== false) this.pos += match[0].length;
+ return match;
+ }
+ },
+ current: function(){return this.string.slice(this.start, this.pos);},
+ hideFirstChars: function(n, inner) {
+ this.lineStart += n;
+ try { return inner(); }
+ finally { this.lineStart -= n; }
+ }
+ };
+
+ // TEXTMARKERS
+
+ // Created with markText and setBookmark methods. A TextMarker is a
+ // handle that can be used to clear or find a marked position in the
+ // document. Line objects hold arrays (markedSpans) containing
+ // {from, to, marker} object pointing to such marker objects, and
+ // indicating that such a marker is present on that line. Multiple
+ // lines may point to the same marker when it spans across lines.
+ // The spans will have null for their from/to properties when the
+ // marker continues beyond the start/end of the line. Markers have
+ // links back to the lines they currently touch.
+
+ var nextMarkerId = 0;
+
+ var TextMarker = CodeMirror.TextMarker = function(doc, type) {
+ this.lines = [];
+ this.type = type;
+ this.doc = doc;
+ this.id = ++nextMarkerId;
+ };
+ eventMixin(TextMarker);
+
+ // Clear the marker.
+ TextMarker.prototype.clear = function() {
+ if (this.explicitlyCleared) return;
+ var cm = this.doc.cm, withOp = cm && !cm.curOp;
+ if (withOp) startOperation(cm);
+ if (hasHandler(this, "clear")) {
+ var found = this.find();
+ if (found) signalLater(this, "clear", found.from, found.to);
+ }
+ var min = null, max = null;
+ for (var i = 0; i < this.lines.length; ++i) {
+ var line = this.lines[i];
+ var span = getMarkedSpanFor(line.markedSpans, this);
+ if (cm && !this.collapsed) regLineChange(cm, lineNo(line), "text");
+ else if (cm) {
+ if (span.to != null) max = lineNo(line);
+ if (span.from != null) min = lineNo(line);
+ }
+ line.markedSpans = removeMarkedSpan(line.markedSpans, span);
+ if (span.from == null && this.collapsed && !lineIsHidden(this.doc, line) && cm)
+ updateLineHeight(line, textHeight(cm.display));
+ }
+ if (cm && this.collapsed && !cm.options.lineWrapping) for (var i = 0; i < this.lines.length; ++i) {
+ var visual = visualLine(this.lines[i]), len = lineLength(visual);
+ if (len > cm.display.maxLineLength) {
+ cm.display.maxLine = visual;
+ cm.display.maxLineLength = len;
+ cm.display.maxLineChanged = true;
+ }
+ }
+
+ if (min != null && cm && this.collapsed) regChange(cm, min, max + 1);
+ this.lines.length = 0;
+ this.explicitlyCleared = true;
+ if (this.atomic && this.doc.cantEdit) {
+ this.doc.cantEdit = false;
+ if (cm) reCheckSelection(cm.doc);
+ }
+ if (cm) signalLater(cm, "markerCleared", cm, this);
+ if (withOp) endOperation(cm);
+ if (this.parent) this.parent.clear();
+ };
+
+ // Find the position of the marker in the document. Returns a {from,
+ // to} object by default. Side can be passed to get a specific side
+ // -- 0 (both), -1 (left), or 1 (right). When lineObj is true, the
+ // Pos objects returned contain a line object, rather than a line
+ // number (used to prevent looking up the same line twice).
+ TextMarker.prototype.find = function(side, lineObj) {
+ if (side == null && this.type == "bookmark") side = 1;
+ var from, to;
+ for (var i = 0; i < this.lines.length; ++i) {
+ var line = this.lines[i];
+ var span = getMarkedSpanFor(line.markedSpans, this);
+ if (span.from != null) {
+ from = Pos(lineObj ? line : lineNo(line), span.from);
+ if (side == -1) return from;
+ }
+ if (span.to != null) {
+ to = Pos(lineObj ? line : lineNo(line), span.to);
+ if (side == 1) return to;
+ }
+ }
+ return from && {from: from, to: to};
+ };
+
+ // Signals that the marker's widget changed, and surrounding layout
+ // should be recomputed.
+ TextMarker.prototype.changed = function() {
+ var pos = this.find(-1, true), widget = this, cm = this.doc.cm;
+ if (!pos || !cm) return;
+ runInOp(cm, function() {
+ var line = pos.line, lineN = lineNo(pos.line);
+ var view = findViewForLine(cm, lineN);
+ if (view) {
+ clearLineMeasurementCacheFor(view);
+ cm.curOp.selectionChanged = cm.curOp.forceUpdate = true;
+ }
+ cm.curOp.updateMaxLine = true;
+ if (!lineIsHidden(widget.doc, line) && widget.height != null) {
+ var oldHeight = widget.height;
+ widget.height = null;
+ var dHeight = widgetHeight(widget) - oldHeight;
+ if (dHeight)
+ updateLineHeight(line, line.height + dHeight);
+ }
+ });
+ };
+
+ TextMarker.prototype.attachLine = function(line) {
+ if (!this.lines.length && this.doc.cm) {
+ var op = this.doc.cm.curOp;
+ if (!op.maybeHiddenMarkers || indexOf(op.maybeHiddenMarkers, this) == -1)
+ (op.maybeUnhiddenMarkers || (op.maybeUnhiddenMarkers = [])).push(this);
+ }
+ this.lines.push(line);
+ };
+ TextMarker.prototype.detachLine = function(line) {
+ this.lines.splice(indexOf(this.lines, line), 1);
+ if (!this.lines.length && this.doc.cm) {
+ var op = this.doc.cm.curOp;
+ (op.maybeHiddenMarkers || (op.maybeHiddenMarkers = [])).push(this);
+ }
+ };
+
+ // Collapsed markers have unique ids, in order to be able to order
+ // them, which is needed for uniquely determining an outer marker
+ // when they overlap (they may nest, but not partially overlap).
+ var nextMarkerId = 0;
+
+ // Create a marker, wire it up to the right lines, and
+ function markText(doc, from, to, options, type) {
+ // Shared markers (across linked documents) are handled separately
+ // (markTextShared will call out to this again, once per
+ // document).
+ if (options && options.shared) return markTextShared(doc, from, to, options, type);
+ // Ensure we are in an operation.
+ if (doc.cm && !doc.cm.curOp) return operation(doc.cm, markText)(doc, from, to, options, type);
+
+ var marker = new TextMarker(doc, type), diff = cmp(from, to);
+ if (options) copyObj(options, marker, false);
+ // Don't connect empty markers unless clearWhenEmpty is false
+ if (diff > 0 || diff == 0 && marker.clearWhenEmpty !== false)
+ return marker;
+ if (marker.replacedWith) {
+ // Showing up as a widget implies collapsed (widget replaces text)
+ marker.collapsed = true;
+ marker.widgetNode = elt("span", [marker.replacedWith], "CodeMirror-widget");
+ if (!options.handleMouseEvents) marker.widgetNode.setAttribute("cm-ignore-events", "true");
+ if (options.insertLeft) marker.widgetNode.insertLeft = true;
+ }
+ if (marker.collapsed) {
+ if (conflictingCollapsedRange(doc, from.line, from, to, marker) ||
+ from.line != to.line && conflictingCollapsedRange(doc, to.line, from, to, marker))
+ throw new Error("Inserting collapsed marker partially overlapping an existing one");
+ sawCollapsedSpans = true;
+ }
+
+ if (marker.addToHistory)
+ addChangeToHistory(doc, {from: from, to: to, origin: "markText"}, doc.sel, NaN);
+
+ var curLine = from.line, cm = doc.cm, updateMaxLine;
+ doc.iter(curLine, to.line + 1, function(line) {
+ if (cm && marker.collapsed && !cm.options.lineWrapping && visualLine(line) == cm.display.maxLine)
+ updateMaxLine = true;
+ if (marker.collapsed && curLine != from.line) updateLineHeight(line, 0);
+ addMarkedSpan(line, new MarkedSpan(marker,
+ curLine == from.line ? from.ch : null,
+ curLine == to.line ? to.ch : null));
+ ++curLine;
+ });
+ // lineIsHidden depends on the presence of the spans, so needs a second pass
+ if (marker.collapsed) doc.iter(from.line, to.line + 1, function(line) {
+ if (lineIsHidden(doc, line)) updateLineHeight(line, 0);
+ });
+
+ if (marker.clearOnEnter) on(marker, "beforeCursorEnter", function() { marker.clear(); });
+
+ if (marker.readOnly) {
+ sawReadOnlySpans = true;
+ if (doc.history.done.length || doc.history.undone.length)
+ doc.clearHistory();
+ }
+ if (marker.collapsed) {
+ marker.id = ++nextMarkerId;
+ marker.atomic = true;
+ }
+ if (cm) {
+ // Sync editor state
+ if (updateMaxLine) cm.curOp.updateMaxLine = true;
+ if (marker.collapsed)
+ regChange(cm, from.line, to.line + 1);
+ else if (marker.className || marker.title || marker.startStyle || marker.endStyle || marker.css)
+ for (var i = from.line; i <= to.line; i++) regLineChange(cm, i, "text");
+ if (marker.atomic) reCheckSelection(cm.doc);
+ signalLater(cm, "markerAdded", cm, marker);
+ }
+ return marker;
+ }
+
+ // SHARED TEXTMARKERS
+
+ // A shared marker spans multiple linked documents. It is
+ // implemented as a meta-marker-object controlling multiple normal
+ // markers.
+ var SharedTextMarker = CodeMirror.SharedTextMarker = function(markers, primary) {
+ this.markers = markers;
+ this.primary = primary;
+ for (var i = 0; i < markers.length; ++i)
+ markers[i].parent = this;
+ };
+ eventMixin(SharedTextMarker);
+
+ SharedTextMarker.prototype.clear = function() {
+ if (this.explicitlyCleared) return;
+ this.explicitlyCleared = true;
+ for (var i = 0; i < this.markers.length; ++i)
+ this.markers[i].clear();
+ signalLater(this, "clear");
+ };
+ SharedTextMarker.prototype.find = function(side, lineObj) {
+ return this.primary.find(side, lineObj);
+ };
+
+ function markTextShared(doc, from, to, options, type) {
+ options = copyObj(options);
+ options.shared = false;
+ var markers = [markText(doc, from, to, options, type)], primary = markers[0];
+ var widget = options.widgetNode;
+ linkedDocs(doc, function(doc) {
+ if (widget) options.widgetNode = widget.cloneNode(true);
+ markers.push(markText(doc, clipPos(doc, from), clipPos(doc, to), options, type));
+ for (var i = 0; i < doc.linked.length; ++i)
+ if (doc.linked[i].isParent) return;
+ primary = lst(markers);
+ });
+ return new SharedTextMarker(markers, primary);
+ }
+
+ function findSharedMarkers(doc) {
+ return doc.findMarks(Pos(doc.first, 0), doc.clipPos(Pos(doc.lastLine())),
+ function(m) { return m.parent; });
+ }
+
+ function copySharedMarkers(doc, markers) {
+ for (var i = 0; i < markers.length; i++) {
+ var marker = markers[i], pos = marker.find();
+ var mFrom = doc.clipPos(pos.from), mTo = doc.clipPos(pos.to);
+ if (cmp(mFrom, mTo)) {
+ var subMark = markText(doc, mFrom, mTo, marker.primary, marker.primary.type);
+ marker.markers.push(subMark);
+ subMark.parent = marker;
+ }
+ }
+ }
+
+ function detachSharedMarkers(markers) {
+ for (var i = 0; i < markers.length; i++) {
+ var marker = markers[i], linked = [marker.primary.doc];;
+ linkedDocs(marker.primary.doc, function(d) { linked.push(d); });
+ for (var j = 0; j < marker.markers.length; j++) {
+ var subMarker = marker.markers[j];
+ if (indexOf(linked, subMarker.doc) == -1) {
+ subMarker.parent = null;
+ marker.markers.splice(j--, 1);
+ }
+ }
+ }
+ }
+
+ // TEXTMARKER SPANS
+
+ function MarkedSpan(marker, from, to) {
+ this.marker = marker;
+ this.from = from; this.to = to;
+ }
+
+ // Search an array of spans for a span matching the given marker.
+ function getMarkedSpanFor(spans, marker) {
+ if (spans) for (var i = 0; i < spans.length; ++i) {
+ var span = spans[i];
+ if (span.marker == marker) return span;
+ }
+ }
+ // Remove a span from an array, returning undefined if no spans are
+ // left (we don't store arrays for lines without spans).
+ function removeMarkedSpan(spans, span) {
+ for (var r, i = 0; i < spans.length; ++i)
+ if (spans[i] != span) (r || (r = [])).push(spans[i]);
+ return r;
+ }
+ // Add a span to a line.
+ function addMarkedSpan(line, span) {
+ line.markedSpans = line.markedSpans ? line.markedSpans.concat([span]) : [span];
+ span.marker.attachLine(line);
+ }
+
+ // Used for the algorithm that adjusts markers for a change in the
+ // document. These functions cut an array of spans at a given
+ // character position, returning an array of remaining chunks (or
+ // undefined if nothing remains).
+ function markedSpansBefore(old, startCh, isInsert) {
+ if (old) for (var i = 0, nw; i < old.length; ++i) {
+ var span = old[i], marker = span.marker;
+ var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= startCh : span.from < startCh);
+ if (startsBefore || span.from == startCh && marker.type == "bookmark" && (!isInsert || !span.marker.insertLeft)) {
+ var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= startCh : span.to > startCh);
+ (nw || (nw = [])).push(new MarkedSpan(marker, span.from, endsAfter ? null : span.to));
+ }
+ }
+ return nw;
+ }
+ function markedSpansAfter(old, endCh, isInsert) {
+ if (old) for (var i = 0, nw; i < old.length; ++i) {
+ var span = old[i], marker = span.marker;
+ var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= endCh : span.to > endCh);
+ if (endsAfter || span.from == endCh && marker.type == "bookmark" && (!isInsert || span.marker.insertLeft)) {
+ var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= endCh : span.from < endCh);
+ (nw || (nw = [])).push(new MarkedSpan(marker, startsBefore ? null : span.from - endCh,
+ span.to == null ? null : span.to - endCh));
+ }
+ }
+ return nw;
+ }
+
+ // Given a change object, compute the new set of marker spans that
+ // cover the line in which the change took place. Removes spans
+ // entirely within the change, reconnects spans belonging to the
+ // same marker that appear on both sides of the change, and cuts off
+ // spans partially within the change. Returns an array of span
+ // arrays with one element for each line in (after) the change.
+ function stretchSpansOverChange(doc, change) {
+ if (change.full) return null;
+ var oldFirst = isLine(doc, change.from.line) && getLine(doc, change.from.line).markedSpans;
+ var oldLast = isLine(doc, change.to.line) && getLine(doc, change.to.line).markedSpans;
+ if (!oldFirst && !oldLast) return null;
+
+ var startCh = change.from.ch, endCh = change.to.ch, isInsert = cmp(change.from, change.to) == 0;
+ // Get the spans that 'stick out' on both sides
+ var first = markedSpansBefore(oldFirst, startCh, isInsert);
+ var last = markedSpansAfter(oldLast, endCh, isInsert);
+
+ // Next, merge those two ends
+ var sameLine = change.text.length == 1, offset = lst(change.text).length + (sameLine ? startCh : 0);
+ if (first) {
+ // Fix up .to properties of first
+ for (var i = 0; i < first.length; ++i) {
+ var span = first[i];
+ if (span.to == null) {
+ var found = getMarkedSpanFor(last, span.marker);
+ if (!found) span.to = startCh;
+ else if (sameLine) span.to = found.to == null ? null : found.to + offset;
+ }
+ }
+ }
+ if (last) {
+ // Fix up .from in last (or move them into first in case of sameLine)
+ for (var i = 0; i < last.length; ++i) {
+ var span = last[i];
+ if (span.to != null) span.to += offset;
+ if (span.from == null) {
+ var found = getMarkedSpanFor(first, span.marker);
+ if (!found) {
+ span.from = offset;
+ if (sameLine) (first || (first = [])).push(span);
+ }
+ } else {
+ span.from += offset;
+ if (sameLine) (first || (first = [])).push(span);
+ }
+ }
+ }
+ // Make sure we didn't create any zero-length spans
+ if (first) first = clearEmptySpans(first);
+ if (last && last != first) last = clearEmptySpans(last);
+
+ var newMarkers = [first];
+ if (!sameLine) {
+ // Fill gap with whole-line-spans
+ var gap = change.text.length - 2, gapMarkers;
+ if (gap > 0 && first)
+ for (var i = 0; i < first.length; ++i)
+ if (first[i].to == null)
+ (gapMarkers || (gapMarkers = [])).push(new MarkedSpan(first[i].marker, null, null));
+ for (var i = 0; i < gap; ++i)
+ newMarkers.push(gapMarkers);
+ newMarkers.push(last);
+ }
+ return newMarkers;
+ }
+
+ // Remove spans that are empty and don't have a clearWhenEmpty
+ // option of false.
+ function clearEmptySpans(spans) {
+ for (var i = 0; i < spans.length; ++i) {
+ var span = spans[i];
+ if (span.from != null && span.from == span.to && span.marker.clearWhenEmpty !== false)
+ spans.splice(i--, 1);
+ }
+ if (!spans.length) return null;
+ return spans;
+ }
+
+ // Used for un/re-doing changes from the history. Combines the
+ // result of computing the existing spans with the set of spans that
+ // existed in the history (so that deleting around a span and then
+ // undoing brings back the span).
+ function mergeOldSpans(doc, change) {
+ var old = getOldSpans(doc, change);
+ var stretched = stretchSpansOverChange(doc, change);
+ if (!old) return stretched;
+ if (!stretched) return old;
+
+ for (var i = 0; i < old.length; ++i) {
+ var oldCur = old[i], stretchCur = stretched[i];
+ if (oldCur && stretchCur) {
+ spans: for (var j = 0; j < stretchCur.length; ++j) {
+ var span = stretchCur[j];
+ for (var k = 0; k < oldCur.length; ++k)
+ if (oldCur[k].marker == span.marker) continue spans;
+ oldCur.push(span);
+ }
+ } else if (stretchCur) {
+ old[i] = stretchCur;
+ }
+ }
+ return old;
+ }
+
+ // Used to 'clip' out readOnly ranges when making a change.
+ function removeReadOnlyRanges(doc, from, to) {
+ var markers = null;
+ doc.iter(from.line, to.line + 1, function(line) {
+ if (line.markedSpans) for (var i = 0; i < line.markedSpans.length; ++i) {
+ var mark = line.markedSpans[i].marker;
+ if (mark.readOnly && (!markers || indexOf(markers, mark) == -1))
+ (markers || (markers = [])).push(mark);
+ }
+ });
+ if (!markers) return null;
+ var parts = [{from: from, to: to}];
+ for (var i = 0; i < markers.length; ++i) {
+ var mk = markers[i], m = mk.find(0);
+ for (var j = 0; j < parts.length; ++j) {
+ var p = parts[j];
+ if (cmp(p.to, m.from) < 0 || cmp(p.from, m.to) > 0) continue;
+ var newParts = [j, 1], dfrom = cmp(p.from, m.from), dto = cmp(p.to, m.to);
+ if (dfrom < 0 || !mk.inclusiveLeft && !dfrom)
+ newParts.push({from: p.from, to: m.from});
+ if (dto > 0 || !mk.inclusiveRight && !dto)
+ newParts.push({from: m.to, to: p.to});
+ parts.splice.apply(parts, newParts);
+ j += newParts.length - 1;
+ }
+ }
+ return parts;
+ }
+
+ // Connect or disconnect spans from a line.
+ function detachMarkedSpans(line) {
+ var spans = line.markedSpans;
+ if (!spans) return;
+ for (var i = 0; i < spans.length; ++i)
+ spans[i].marker.detachLine(line);
+ line.markedSpans = null;
+ }
+ function attachMarkedSpans(line, spans) {
+ if (!spans) return;
+ for (var i = 0; i < spans.length; ++i)
+ spans[i].marker.attachLine(line);
+ line.markedSpans = spans;
+ }
+
+ // Helpers used when computing which overlapping collapsed span
+ // counts as the larger one.
+ function extraLeft(marker) { return marker.inclusiveLeft ? -1 : 0; }
+ function extraRight(marker) { return marker.inclusiveRight ? 1 : 0; }
+
+ // Returns a number indicating which of two overlapping collapsed
+ // spans is larger (and thus includes the other). Falls back to
+ // comparing ids when the spans cover exactly the same range.
+ function compareCollapsedMarkers(a, b) {
+ var lenDiff = a.lines.length - b.lines.length;
+ if (lenDiff != 0) return lenDiff;
+ var aPos = a.find(), bPos = b.find();
+ var fromCmp = cmp(aPos.from, bPos.from) || extraLeft(a) - extraLeft(b);
+ if (fromCmp) return -fromCmp;
+ var toCmp = cmp(aPos.to, bPos.to) || extraRight(a) - extraRight(b);
+ if (toCmp) return toCmp;
+ return b.id - a.id;
+ }
+
+ // Find out whether a line ends or starts in a collapsed span. If
+ // so, return the marker for that span.
+ function collapsedSpanAtSide(line, start) {
+ var sps = sawCollapsedSpans && line.markedSpans, found;
+ if (sps) for (var sp, i = 0; i < sps.length; ++i) {
+ sp = sps[i];
+ if (sp.marker.collapsed && (start ? sp.from : sp.to) == null &&
+ (!found || compareCollapsedMarkers(found, sp.marker) < 0))
+ found = sp.marker;
+ }
+ return found;
+ }
+ function collapsedSpanAtStart(line) { return collapsedSpanAtSide(line, true); }
+ function collapsedSpanAtEnd(line) { return collapsedSpanAtSide(line, false); }
+
+ // Test whether there exists a collapsed span that partially
+ // overlaps (covers the start or end, but not both) of a new span.
+ // Such overlap is not allowed.
+ function conflictingCollapsedRange(doc, lineNo, from, to, marker) {
+ var line = getLine(doc, lineNo);
+ var sps = sawCollapsedSpans && line.markedSpans;
+ if (sps) for (var i = 0; i < sps.length; ++i) {
+ var sp = sps[i];
+ if (!sp.marker.collapsed) continue;
+ var found = sp.marker.find(0);
+ var fromCmp = cmp(found.from, from) || extraLeft(sp.marker) - extraLeft(marker);
+ var toCmp = cmp(found.to, to) || extraRight(sp.marker) - extraRight(marker);
+ if (fromCmp >= 0 && toCmp <= 0 || fromCmp <= 0 && toCmp >= 0) continue;
+ if (fromCmp <= 0 && (sp.marker.inclusiveRight && marker.inclusiveLeft ? cmp(found.to, from) >= 0 : cmp(found.to, from) > 0) ||
+ fromCmp >= 0 && (sp.marker.inclusiveRight && marker.inclusiveLeft ? cmp(found.from, to) <= 0 : cmp(found.from, to) < 0))
+ return true;
+ }
+ }
+
+ // A visual line is a line as drawn on the screen. Folding, for
+ // example, can cause multiple logical lines to appear on the same
+ // visual line. This finds the start of the visual line that the
+ // given line is part of (usually that is the line itself).
+ function visualLine(line) {
+ var merged;
+ while (merged = collapsedSpanAtStart(line))
+ line = merged.find(-1, true).line;
+ return line;
+ }
+
+ // Returns an array of logical lines that continue the visual line
+ // started by the argument, or undefined if there are no such lines.
+ function visualLineContinued(line) {
+ var merged, lines;
+ while (merged = collapsedSpanAtEnd(line)) {
+ line = merged.find(1, true).line;
+ (lines || (lines = [])).push(line);
+ }
+ return lines;
+ }
+
+ // Get the line number of the start of the visual line that the
+ // given line number is part of.
+ function visualLineNo(doc, lineN) {
+ var line = getLine(doc, lineN), vis = visualLine(line);
+ if (line == vis) return lineN;
+ return lineNo(vis);
+ }
+ // Get the line number of the start of the next visual line after
+ // the given line.
+ function visualLineEndNo(doc, lineN) {
+ if (lineN > doc.lastLine()) return lineN;
+ var line = getLine(doc, lineN), merged;
+ if (!lineIsHidden(doc, line)) return lineN;
+ while (merged = collapsedSpanAtEnd(line))
+ line = merged.find(1, true).line;
+ return lineNo(line) + 1;
+ }
+
+ // Compute whether a line is hidden. Lines count as hidden when they
+ // are part of a visual line that starts with another line, or when
+ // they are entirely covered by collapsed, non-widget span.
+ function lineIsHidden(doc, line) {
+ var sps = sawCollapsedSpans && line.markedSpans;
+ if (sps) for (var sp, i = 0; i < sps.length; ++i) {
+ sp = sps[i];
+ if (!sp.marker.collapsed) continue;
+ if (sp.from == null) return true;
+ if (sp.marker.widgetNode) continue;
+ if (sp.from == 0 && sp.marker.inclusiveLeft && lineIsHiddenInner(doc, line, sp))
+ return true;
+ }
+ }
+ function lineIsHiddenInner(doc, line, span) {
+ if (span.to == null) {
+ var end = span.marker.find(1, true);
+ return lineIsHiddenInner(doc, end.line, getMarkedSpanFor(end.line.markedSpans, span.marker));
+ }
+ if (span.marker.inclusiveRight && span.to == line.text.length)
+ return true;
+ for (var sp, i = 0; i < line.markedSpans.length; ++i) {
+ sp = line.markedSpans[i];
+ if (sp.marker.collapsed && !sp.marker.widgetNode && sp.from == span.to &&
+ (sp.to == null || sp.to != span.from) &&
+ (sp.marker.inclusiveLeft || span.marker.inclusiveRight) &&
+ lineIsHiddenInner(doc, line, sp)) return true;
+ }
+ }
+
+ // LINE WIDGETS
+
+ // Line widgets are block elements displayed above or below a line.
+
+ var LineWidget = CodeMirror.LineWidget = function(doc, node, options) {
+ if (options) for (var opt in options) if (options.hasOwnProperty(opt))
+ this[opt] = options[opt];
+ this.doc = doc;
+ this.node = node;
+ };
+ eventMixin(LineWidget);
+
+ function adjustScrollWhenAboveVisible(cm, line, diff) {
+ if (heightAtLine(line) < ((cm.curOp && cm.curOp.scrollTop) || cm.doc.scrollTop))
+ addToScrollPos(cm, null, diff);
+ }
+
+ LineWidget.prototype.clear = function() {
+ var cm = this.doc.cm, ws = this.line.widgets, line = this.line, no = lineNo(line);
+ if (no == null || !ws) return;
+ for (var i = 0; i < ws.length; ++i) if (ws[i] == this) ws.splice(i--, 1);
+ if (!ws.length) line.widgets = null;
+ var height = widgetHeight(this);
+ updateLineHeight(line, Math.max(0, line.height - height));
+ if (cm) runInOp(cm, function() {
+ adjustScrollWhenAboveVisible(cm, line, -height);
+ regLineChange(cm, no, "widget");
+ });
+ };
+ LineWidget.prototype.changed = function() {
+ var oldH = this.height, cm = this.doc.cm, line = this.line;
+ this.height = null;
+ var diff = widgetHeight(this) - oldH;
+ if (!diff) return;
+ updateLineHeight(line, line.height + diff);
+ if (cm) runInOp(cm, function() {
+ cm.curOp.forceUpdate = true;
+ adjustScrollWhenAboveVisible(cm, line, diff);
+ });
+ };
+
+ function widgetHeight(widget) {
+ if (widget.height != null) return widget.height;
+ var cm = widget.doc.cm;
+ if (!cm) return 0;
+ if (!contains(document.body, widget.node)) {
+ var parentStyle = "position: relative;";
+ if (widget.coverGutter)
+ parentStyle += "margin-left: -" + cm.display.gutters.offsetWidth + "px;";
+ if (widget.noHScroll)
+ parentStyle += "width: " + cm.display.wrapper.clientWidth + "px;";
+ removeChildrenAndAdd(cm.display.measure, elt("div", [widget.node], null, parentStyle));
+ }
+ return widget.height = widget.node.parentNode.offsetHeight;
+ }
+
+ function addLineWidget(doc, handle, node, options) {
+ var widget = new LineWidget(doc, node, options);
+ var cm = doc.cm;
+ if (cm && widget.noHScroll) cm.display.alignWidgets = true;
+ changeLine(doc, handle, "widget", function(line) {
+ var widgets = line.widgets || (line.widgets = []);
+ if (widget.insertAt == null) widgets.push(widget);
+ else widgets.splice(Math.min(widgets.length - 1, Math.max(0, widget.insertAt)), 0, widget);
+ widget.line = line;
+ if (cm && !lineIsHidden(doc, line)) {
+ var aboveVisible = heightAtLine(line) < doc.scrollTop;
+ updateLineHeight(line, line.height + widgetHeight(widget));
+ if (aboveVisible) addToScrollPos(cm, null, widget.height);
+ cm.curOp.forceUpdate = true;
+ }
+ return true;
+ });
+ return widget;
+ }
+
+ // LINE DATA STRUCTURE
+
+ // Line objects. These hold state related to a line, including
+ // highlighting info (the styles array).
+ var Line = CodeMirror.Line = function(text, markedSpans, estimateHeight) {
+ this.text = text;
+ attachMarkedSpans(this, markedSpans);
+ this.height = estimateHeight ? estimateHeight(this) : 1;
+ };
+ eventMixin(Line);
+ Line.prototype.lineNo = function() { return lineNo(this); };
+
+ // Change the content (text, markers) of a line. Automatically
+ // invalidates cached information and tries to re-estimate the
+ // line's height.
+ function updateLine(line, text, markedSpans, estimateHeight) {
+ line.text = text;
+ if (line.stateAfter) line.stateAfter = null;
+ if (line.styles) line.styles = null;
+ if (line.order != null) line.order = null;
+ detachMarkedSpans(line);
+ attachMarkedSpans(line, markedSpans);
+ var estHeight = estimateHeight ? estimateHeight(line) : 1;
+ if (estHeight != line.height) updateLineHeight(line, estHeight);
+ }
+
+ // Detach a line from the document tree and its markers.
+ function cleanUpLine(line) {
+ line.parent = null;
+ detachMarkedSpans(line);
+ }
+
+ function extractLineClasses(type, output) {
+ if (type) for (;;) {
+ var lineClass = type.match(/(?:^|\s+)line-(background-)?(\S+)/);
+ if (!lineClass) break;
+ type = type.slice(0, lineClass.index) + type.slice(lineClass.index + lineClass[0].length);
+ var prop = lineClass[1] ? "bgClass" : "textClass";
+ if (output[prop] == null)
+ output[prop] = lineClass[2];
+ else if (!(new RegExp("(?:^|\s)" + lineClass[2] + "(?:$|\s)")).test(output[prop]))
+ output[prop] += " " + lineClass[2];
+ }
+ return type;
+ }
+
+ function callBlankLine(mode, state) {
+ if (mode.blankLine) return mode.blankLine(state);
+ if (!mode.innerMode) return;
+ var inner = CodeMirror.innerMode(mode, state);
+ if (inner.mode.blankLine) return inner.mode.blankLine(inner.state);
+ }
+
+ function readToken(mode, stream, state, inner) {
+ for (var i = 0; i < 10; i++) {
+ if (inner) inner[0] = CodeMirror.innerMode(mode, state).mode;
+ var style = mode.token(stream, state);
+ if (stream.pos > stream.start) return style;
+ }
+ throw new Error("Mode " + mode.name + " failed to advance stream.");
+ }
+
+ // Utility for getTokenAt and getLineTokens
+ function takeToken(cm, pos, precise, asArray) {
+ function getObj(copy) {
+ return {start: stream.start, end: stream.pos,
+ string: stream.current(),
+ type: style || null,
+ state: copy ? copyState(doc.mode, state) : state};
+ }
+
+ var doc = cm.doc, mode = doc.mode, style;
+ pos = clipPos(doc, pos);
+ var line = getLine(doc, pos.line), state = getStateBefore(cm, pos.line, precise);
+ var stream = new StringStream(line.text, cm.options.tabSize), tokens;
+ if (asArray) tokens = [];
+ while ((asArray || stream.pos < pos.ch) && !stream.eol()) {
+ stream.start = stream.pos;
+ style = readToken(mode, stream, state);
+ if (asArray) tokens.push(getObj(true));
+ }
+ return asArray ? tokens : getObj();
+ }
+
+ // Run the given mode's parser over a line, calling f for each token.
+ function runMode(cm, text, mode, state, f, lineClasses, forceToEnd) {
+ var flattenSpans = mode.flattenSpans;
+ if (flattenSpans == null) flattenSpans = cm.options.flattenSpans;
+ var curStart = 0, curStyle = null;
+ var stream = new StringStream(text, cm.options.tabSize), style;
+ var inner = cm.options.addModeClass && [null];
+ if (text == "") extractLineClasses(callBlankLine(mode, state), lineClasses);
+ while (!stream.eol()) {
+ if (stream.pos > cm.options.maxHighlightLength) {
+ flattenSpans = false;
+ if (forceToEnd) processLine(cm, text, state, stream.pos);
+ stream.pos = text.length;
+ style = null;
+ } else {
+ style = extractLineClasses(readToken(mode, stream, state, inner), lineClasses);
+ }
+ if (inner) {
+ var mName = inner[0].name;
+ if (mName) style = "m-" + (style ? mName + " " + style : mName);
+ }
+ if (!flattenSpans || curStyle != style) {
+ while (curStart < stream.start) {
+ curStart = Math.min(stream.start, curStart + 50000);
+ f(curStart, curStyle);
+ }
+ curStyle = style;
+ }
+ stream.start = stream.pos;
+ }
+ while (curStart < stream.pos) {
+ // Webkit seems to refuse to render text nodes longer than 57444 characters
+ var pos = Math.min(stream.pos, curStart + 50000);
+ f(pos, curStyle);
+ curStart = pos;
+ }
+ }
+
+ // Compute a style array (an array starting with a mode generation
+ // -- for invalidation -- followed by pairs of end positions and
+ // style strings), which is used to highlight the tokens on the
+ // line.
+ function highlightLine(cm, line, state, forceToEnd) {
+ // A styles array always starts with a number identifying the
+ // mode/overlays that it is based on (for easy invalidation).
+ var st = [cm.state.modeGen], lineClasses = {};
+ // Compute the base array of styles
+ runMode(cm, line.text, cm.doc.mode, state, function(end, style) {
+ st.push(end, style);
+ }, lineClasses, forceToEnd);
+
+ // Run overlays, adjust style array.
+ for (var o = 0; o < cm.state.overlays.length; ++o) {
+ var overlay = cm.state.overlays[o], i = 1, at = 0;
+ runMode(cm, line.text, overlay.mode, true, function(end, style) {
+ var start = i;
+ // Ensure there's a token end at the current position, and that i points at it
+ while (at < end) {
+ var i_end = st[i];
+ if (i_end > end)
+ st.splice(i, 1, end, st[i+1], i_end);
+ i += 2;
+ at = Math.min(end, i_end);
+ }
+ if (!style) return;
+ if (overlay.opaque) {
+ st.splice(start, i - start, end, "cm-overlay " + style);
+ i = start + 2;
+ } else {
+ for (; start < i; start += 2) {
+ var cur = st[start+1];
+ st[start+1] = (cur ? cur + " " : "") + "cm-overlay " + style;
+ }
+ }
+ }, lineClasses);
+ }
+
+ return {styles: st, classes: lineClasses.bgClass || lineClasses.textClass ? lineClasses : null};
+ }
+
+ function getLineStyles(cm, line, updateFrontier) {
+ if (!line.styles || line.styles[0] != cm.state.modeGen) {
+ var state = getStateBefore(cm, lineNo(line));
+ var result = highlightLine(cm, line, line.text.length > cm.options.maxHighlightLength ? copyState(cm.doc.mode, state) : state);
+ line.stateAfter = state;
+ line.styles = result.styles;
+ if (result.classes) line.styleClasses = result.classes;
+ else if (line.styleClasses) line.styleClasses = null;
+ if (updateFrontier === cm.doc.frontier) cm.doc.frontier++;
+ }
+ return line.styles;
+ }
+
+ // Lightweight form of highlight -- proceed over this line and
+ // update state, but don't save a style array. Used for lines that
+ // aren't currently visible.
+ function processLine(cm, text, state, startAt) {
+ var mode = cm.doc.mode;
+ var stream = new StringStream(text, cm.options.tabSize);
+ stream.start = stream.pos = startAt || 0;
+ if (text == "") callBlankLine(mode, state);
+ while (!stream.eol()) {
+ readToken(mode, stream, state);
+ stream.start = stream.pos;
+ }
+ }
+
+ // Convert a style as returned by a mode (either null, or a string
+ // containing one or more styles) to a CSS style. This is cached,
+ // and also looks for line-wide styles.
+ var styleToClassCache = {}, styleToClassCacheWithMode = {};
+ function interpretTokenStyle(style, options) {
+ if (!style || /^\s*$/.test(style)) return null;
+ var cache = options.addModeClass ? styleToClassCacheWithMode : styleToClassCache;
+ return cache[style] ||
+ (cache[style] = style.replace(/\S+/g, "cm-$&"));
+ }
+
+ // Render the DOM representation of the text of a line. Also builds
+ // up a 'line map', which points at the DOM nodes that represent
+ // specific stretches of text, and is used by the measuring code.
+ // The returned object contains the DOM node, this map, and
+ // information about line-wide styles that were set by the mode.
+ function buildLineContent(cm, lineView) {
+ // The padding-right forces the element to have a 'border', which
+ // is needed on Webkit to be able to get line-level bounding
+ // rectangles for it (in measureChar).
+ var content = elt("span", null, null, webkit ? "padding-right: .1px" : null);
+ var builder = {pre: elt("pre", [content], "CodeMirror-line"), content: content,
+ col: 0, pos: 0, cm: cm,
+ splitSpaces: (ie || webkit) && cm.getOption("lineWrapping")};
+ lineView.measure = {};
+
+ // Iterate over the logical lines that make up this visual line.
+ for (var i = 0; i <= (lineView.rest ? lineView.rest.length : 0); i++) {
+ var line = i ? lineView.rest[i - 1] : lineView.line, order;
+ builder.pos = 0;
+ builder.addToken = buildToken;
+ // Optionally wire in some hacks into the token-rendering
+ // algorithm, to deal with browser quirks.
+ if (hasBadBidiRects(cm.display.measure) && (order = getOrder(line)))
+ builder.addToken = buildTokenBadBidi(builder.addToken, order);
+ builder.map = [];
+ var allowFrontierUpdate = lineView != cm.display.externalMeasured && lineNo(line);
+ insertLineContent(line, builder, getLineStyles(cm, line, allowFrontierUpdate));
+ if (line.styleClasses) {
+ if (line.styleClasses.bgClass)
+ builder.bgClass = joinClasses(line.styleClasses.bgClass, builder.bgClass || "");
+ if (line.styleClasses.textClass)
+ builder.textClass = joinClasses(line.styleClasses.textClass, builder.textClass || "");
+ }
+
+ // Ensure at least a single node is present, for measuring.
+ if (builder.map.length == 0)
+ builder.map.push(0, 0, builder.content.appendChild(zeroWidthElement(cm.display.measure)));
+
+ // Store the map and a cache object for the current logical line
+ if (i == 0) {
+ lineView.measure.map = builder.map;
+ lineView.measure.cache = {};
+ } else {
+ (lineView.measure.maps || (lineView.measure.maps = [])).push(builder.map);
+ (lineView.measure.caches || (lineView.measure.caches = [])).push({});
+ }
+ }
+
+ // See issue #2901
+ if (webkit) {
+ var last = builder.content.lastChild
+ if (/\bcm-tab\b/.test(last.className) || (last.querySelector && last.querySelector(".cm-tab")))
+ builder.content.className = "cm-tab-wrap-hack";
+ }
+
+ signal(cm, "renderLine", cm, lineView.line, builder.pre);
+ if (builder.pre.className)
+ builder.textClass = joinClasses(builder.pre.className, builder.textClass || "");
+
+ return builder;
+ }
+
+ function defaultSpecialCharPlaceholder(ch) {
+ var token = elt("span", "\u2022", "cm-invalidchar");
+ token.title = "\\u" + ch.charCodeAt(0).toString(16);
+ token.setAttribute("aria-label", token.title);
+ return token;
+ }
+
+ // Build up the DOM representation for a single token, and add it to
+ // the line map. Takes care to render special characters separately.
+ function buildToken(builder, text, style, startStyle, endStyle, title, css) {
+ if (!text) return;
+ var displayText = builder.splitSpaces ? text.replace(/ {3,}/g, splitSpaces) : text;
+ var special = builder.cm.state.specialChars, mustWrap = false;
+ if (!special.test(text)) {
+ builder.col += text.length;
+ var content = document.createTextNode(displayText);
+ builder.map.push(builder.pos, builder.pos + text.length, content);
+ if (ie && ie_version < 9) mustWrap = true;
+ builder.pos += text.length;
+ } else {
+ var content = document.createDocumentFragment(), pos = 0;
+ while (true) {
+ special.lastIndex = pos;
+ var m = special.exec(text);
+ var skipped = m ? m.index - pos : text.length - pos;
+ if (skipped) {
+ var txt = document.createTextNode(displayText.slice(pos, pos + skipped));
+ if (ie && ie_version < 9) content.appendChild(elt("span", [txt]));
+ else content.appendChild(txt);
+ builder.map.push(builder.pos, builder.pos + skipped, txt);
+ builder.col += skipped;
+ builder.pos += skipped;
+ }
+ if (!m) break;
+ pos += skipped + 1;
+ if (m[0] == "\t") {
+ var tabSize = builder.cm.options.tabSize, tabWidth = tabSize - builder.col % tabSize;
+ var txt = content.appendChild(elt("span", spaceStr(tabWidth), "cm-tab"));
+ txt.setAttribute("role", "presentation");
+ txt.setAttribute("cm-text", "\t");
+ builder.col += tabWidth;
+ } else if (m[0] == "\r" || m[0] == "\n") {
+ var txt = content.appendChild(elt("span", m[0] == "\r" ? "\u240d" : "\u2424", "cm-invalidchar"));
+ txt.setAttribute("cm-text", m[0]);
+ builder.col += 1;
+ } else {
+ var txt = builder.cm.options.specialCharPlaceholder(m[0]);
+ txt.setAttribute("cm-text", m[0]);
+ if (ie && ie_version < 9) content.appendChild(elt("span", [txt]));
+ else content.appendChild(txt);
+ builder.col += 1;
+ }
+ builder.map.push(builder.pos, builder.pos + 1, txt);
+ builder.pos++;
+ }
+ }
+ if (style || startStyle || endStyle || mustWrap || css) {
+ var fullStyle = style || "";
+ if (startStyle) fullStyle += startStyle;
+ if (endStyle) fullStyle += endStyle;
+ var token = elt("span", [content], fullStyle, css);
+ if (title) token.title = title;
+ return builder.content.appendChild(token);
+ }
+ builder.content.appendChild(content);
+ }
+
+ function splitSpaces(old) {
+ var out = " ";
+ for (var i = 0; i < old.length - 2; ++i) out += i % 2 ? " " : "\u00a0";
+ out += " ";
+ return out;
+ }
+
+ // Work around nonsense dimensions being reported for stretches of
+ // right-to-left text.
+ function buildTokenBadBidi(inner, order) {
+ return function(builder, text, style, startStyle, endStyle, title, css) {
+ style = style ? style + " cm-force-border" : "cm-force-border";
+ var start = builder.pos, end = start + text.length;
+ for (;;) {
+ // Find the part that overlaps with the start of this text
+ for (var i = 0; i < order.length; i++) {
+ var part = order[i];
+ if (part.to > start && part.from <= start) break;
+ }
+ if (part.to >= end) return inner(builder, text, style, startStyle, endStyle, title, css);
+ inner(builder, text.slice(0, part.to - start), style, startStyle, null, title, css);
+ startStyle = null;
+ text = text.slice(part.to - start);
+ start = part.to;
+ }
+ };
+ }
+
+ function buildCollapsedSpan(builder, size, marker, ignoreWidget) {
+ var widget = !ignoreWidget && marker.widgetNode;
+ if (widget) builder.map.push(builder.pos, builder.pos + size, widget);
+ if (!ignoreWidget && builder.cm.display.input.needsContentAttribute) {
+ if (!widget)
+ widget = builder.content.appendChild(document.createElement("span"));
+ widget.setAttribute("cm-marker", marker.id);
+ }
+ if (widget) {
+ builder.cm.display.input.setUneditable(widget);
+ builder.content.appendChild(widget);
+ }
+ builder.pos += size;
+ }
+
+ // Outputs a number of spans to make up a line, taking highlighting
+ // and marked text into account.
+ function insertLineContent(line, builder, styles) {
+ var spans = line.markedSpans, allText = line.text, at = 0;
+ if (!spans) {
+ for (var i = 1; i < styles.length; i+=2)
+ builder.addToken(builder, allText.slice(at, at = styles[i]), interpretTokenStyle(styles[i+1], builder.cm.options));
+ return;
+ }
+
+ var len = allText.length, pos = 0, i = 1, text = "", style, css;
+ var nextChange = 0, spanStyle, spanEndStyle, spanStartStyle, title, collapsed;
+ for (;;) {
+ if (nextChange == pos) { // Update current marker set
+ spanStyle = spanEndStyle = spanStartStyle = title = css = "";
+ collapsed = null; nextChange = Infinity;
+ var foundBookmarks = [], endStyles
+ for (var j = 0; j < spans.length; ++j) {
+ var sp = spans[j], m = sp.marker;
+ if (m.type == "bookmark" && sp.from == pos && m.widgetNode) {
+ foundBookmarks.push(m);
+ } else if (sp.from <= pos && (sp.to == null || sp.to > pos || m.collapsed && sp.to == pos && sp.from == pos)) {
+ if (sp.to != null && sp.to != pos && nextChange > sp.to) {
+ nextChange = sp.to;
+ spanEndStyle = "";
+ }
+ if (m.className) spanStyle += " " + m.className;
+ if (m.css) css = (css ? css + ";" : "") + m.css;
+ if (m.startStyle && sp.from == pos) spanStartStyle += " " + m.startStyle;
+ if (m.endStyle && sp.to == nextChange) (endStyles || (endStyles = [])).push(m.endStyle, sp.to)
+ if (m.title && !title) title = m.title;
+ if (m.collapsed && (!collapsed || compareCollapsedMarkers(collapsed.marker, m) < 0))
+ collapsed = sp;
+ } else if (sp.from > pos && nextChange > sp.from) {
+ nextChange = sp.from;
+ }
+ }
+ if (endStyles) for (var j = 0; j < endStyles.length; j += 2)
+ if (endStyles[j + 1] == nextChange) spanEndStyle += " " + endStyles[j]
+
+ if (!collapsed || collapsed.from == pos) for (var j = 0; j < foundBookmarks.length; ++j)
+ buildCollapsedSpan(builder, 0, foundBookmarks[j]);
+ if (collapsed && (collapsed.from || 0) == pos) {
+ buildCollapsedSpan(builder, (collapsed.to == null ? len + 1 : collapsed.to) - pos,
+ collapsed.marker, collapsed.from == null);
+ if (collapsed.to == null) return;
+ if (collapsed.to == pos) collapsed = false;
+ }
+ }
+ if (pos >= len) break;
+
+ var upto = Math.min(len, nextChange);
+ while (true) {
+ if (text) {
+ var end = pos + text.length;
+ if (!collapsed) {
+ var tokenText = end > upto ? text.slice(0, upto - pos) : text;
+ builder.addToken(builder, tokenText, style ? style + spanStyle : spanStyle,
+ spanStartStyle, pos + tokenText.length == nextChange ? spanEndStyle : "", title, css);
+ }
+ if (end >= upto) {text = text.slice(upto - pos); pos = upto; break;}
+ pos = end;
+ spanStartStyle = "";
+ }
+ text = allText.slice(at, at = styles[i++]);
+ style = interpretTokenStyle(styles[i++], builder.cm.options);
+ }
+ }
+ }
+
+ // DOCUMENT DATA STRUCTURE
+
+ // By default, updates that start and end at the beginning of a line
+ // are treated specially, in order to make the association of line
+ // widgets and marker elements with the text behave more intuitive.
+ function isWholeLineUpdate(doc, change) {
+ return change.from.ch == 0 && change.to.ch == 0 && lst(change.text) == "" &&
+ (!doc.cm || doc.cm.options.wholeLineUpdateBefore);
+ }
+
+ // Perform a change on the document data structure.
+ function updateDoc(doc, change, markedSpans, estimateHeight) {
+ function spansFor(n) {return markedSpans ? markedSpans[n] : null;}
+ function update(line, text, spans) {
+ updateLine(line, text, spans, estimateHeight);
+ signalLater(line, "change", line, change);
+ }
+ function linesFor(start, end) {
+ for (var i = start, result = []; i < end; ++i)
+ result.push(new Line(text[i], spansFor(i), estimateHeight));
+ return result;
+ }
+
+ var from = change.from, to = change.to, text = change.text;
+ var firstLine = getLine(doc, from.line), lastLine = getLine(doc, to.line);
+ var lastText = lst(text), lastSpans = spansFor(text.length - 1), nlines = to.line - from.line;
+
+ // Adjust the line structure
+ if (change.full) {
+ doc.insert(0, linesFor(0, text.length));
+ doc.remove(text.length, doc.size - text.length);
+ } else if (isWholeLineUpdate(doc, change)) {
+ // This is a whole-line replace. Treated specially to make
+ // sure line objects move the way they are supposed to.
+ var added = linesFor(0, text.length - 1);
+ update(lastLine, lastLine.text, lastSpans);
+ if (nlines) doc.remove(from.line, nlines);
+ if (added.length) doc.insert(from.line, added);
+ } else if (firstLine == lastLine) {
+ if (text.length == 1) {
+ update(firstLine, firstLine.text.slice(0, from.ch) + lastText + firstLine.text.slice(to.ch), lastSpans);
+ } else {
+ var added = linesFor(1, text.length - 1);
+ added.push(new Line(lastText + firstLine.text.slice(to.ch), lastSpans, estimateHeight));
+ update(firstLine, firstLine.text.slice(0, from.ch) + text[0], spansFor(0));
+ doc.insert(from.line + 1, added);
+ }
+ } else if (text.length == 1) {
+ update(firstLine, firstLine.text.slice(0, from.ch) + text[0] + lastLine.text.slice(to.ch), spansFor(0));
+ doc.remove(from.line + 1, nlines);
+ } else {
+ update(firstLine, firstLine.text.slice(0, from.ch) + text[0], spansFor(0));
+ update(lastLine, lastText + lastLine.text.slice(to.ch), lastSpans);
+ var added = linesFor(1, text.length - 1);
+ if (nlines > 1) doc.remove(from.line + 1, nlines - 1);
+ doc.insert(from.line + 1, added);
+ }
+
+ signalLater(doc, "change", doc, change);
+ }
+
+ // The document is represented as a BTree consisting of leaves, with
+ // chunk of lines in them, and branches, with up to ten leaves or
+ // other branch nodes below them. The top node is always a branch
+ // node, and is the document object itself (meaning it has
+ // additional methods and properties).
+ //
+ // All nodes have parent links. The tree is used both to go from
+ // line numbers to line objects, and to go from objects to numbers.
+ // It also indexes by height, and is used to convert between height
+ // and line object, and to find the total height of the document.
+ //
+ // See also http://marijnhaverbeke.nl/blog/codemirror-line-tree.html
+
+ function LeafChunk(lines) {
+ this.lines = lines;
+ this.parent = null;
+ for (var i = 0, height = 0; i < lines.length; ++i) {
+ lines[i].parent = this;
+ height += lines[i].height;
+ }
+ this.height = height;
+ }
+
+ LeafChunk.prototype = {
+ chunkSize: function() { return this.lines.length; },
+ // Remove the n lines at offset 'at'.
+ removeInner: function(at, n) {
+ for (var i = at, e = at + n; i < e; ++i) {
+ var line = this.lines[i];
+ this.height -= line.height;
+ cleanUpLine(line);
+ signalLater(line, "delete");
+ }
+ this.lines.splice(at, n);
+ },
+ // Helper used to collapse a small branch into a single leaf.
+ collapse: function(lines) {
+ lines.push.apply(lines, this.lines);
+ },
+ // Insert the given array of lines at offset 'at', count them as
+ // having the given height.
+ insertInner: function(at, lines, height) {
+ this.height += height;
+ this.lines = this.lines.slice(0, at).concat(lines).concat(this.lines.slice(at));
+ for (var i = 0; i < lines.length; ++i) lines[i].parent = this;
+ },
+ // Used to iterate over a part of the tree.
+ iterN: function(at, n, op) {
+ for (var e = at + n; at < e; ++at)
+ if (op(this.lines[at])) return true;
+ }
+ };
+
+ function BranchChunk(children) {
+ this.children = children;
+ var size = 0, height = 0;
+ for (var i = 0; i < children.length; ++i) {
+ var ch = children[i];
+ size += ch.chunkSize(); height += ch.height;
+ ch.parent = this;
+ }
+ this.size = size;
+ this.height = height;
+ this.parent = null;
+ }
+
+ BranchChunk.prototype = {
+ chunkSize: function() { return this.size; },
+ removeInner: function(at, n) {
+ this.size -= n;
+ for (var i = 0; i < this.children.length; ++i) {
+ var child = this.children[i], sz = child.chunkSize();
+ if (at < sz) {
+ var rm = Math.min(n, sz - at), oldHeight = child.height;
+ child.removeInner(at, rm);
+ this.height -= oldHeight - child.height;
+ if (sz == rm) { this.children.splice(i--, 1); child.parent = null; }
+ if ((n -= rm) == 0) break;
+ at = 0;
+ } else at -= sz;
+ }
+ // If the result is smaller than 25 lines, ensure that it is a
+ // single leaf node.
+ if (this.size - n < 25 &&
+ (this.children.length > 1 || !(this.children[0] instanceof LeafChunk))) {
+ var lines = [];
+ this.collapse(lines);
+ this.children = [new LeafChunk(lines)];
+ this.children[0].parent = this;
+ }
+ },
+ collapse: function(lines) {
+ for (var i = 0; i < this.children.length; ++i) this.children[i].collapse(lines);
+ },
+ insertInner: function(at, lines, height) {
+ this.size += lines.length;
+ this.height += height;
+ for (var i = 0; i < this.children.length; ++i) {
+ var child = this.children[i], sz = child.chunkSize();
+ if (at <= sz) {
+ child.insertInner(at, lines, height);
+ if (child.lines && child.lines.length > 50) {
+ // To avoid memory thrashing when child.lines is huge (e.g. first view of a large file), it's never spliced.
+ // Instead, small slices are taken. They're taken in order because sequential memory accesses are fastest.
+ var remaining = child.lines.length % 25 + 25
+ for (var pos = remaining; pos < child.lines.length;) {
+ var leaf = new LeafChunk(child.lines.slice(pos, pos += 25));
+ child.height -= leaf.height;
+ this.children.splice(++i, 0, leaf);
+ leaf.parent = this;
+ }
+ child.lines = child.lines.slice(0, remaining);
+ this.maybeSpill();
+ }
+ break;
+ }
+ at -= sz;
+ }
+ },
+ // When a node has grown, check whether it should be split.
+ maybeSpill: function() {
+ if (this.children.length <= 10) return;
+ var me = this;
+ do {
+ var spilled = me.children.splice(me.children.length - 5, 5);
+ var sibling = new BranchChunk(spilled);
+ if (!me.parent) { // Become the parent node
+ var copy = new BranchChunk(me.children);
+ copy.parent = me;
+ me.children = [copy, sibling];
+ me = copy;
+ } else {
+ me.size -= sibling.size;
+ me.height -= sibling.height;
+ var myIndex = indexOf(me.parent.children, me);
+ me.parent.children.splice(myIndex + 1, 0, sibling);
+ }
+ sibling.parent = me.parent;
+ } while (me.children.length > 10);
+ me.parent.maybeSpill();
+ },
+ iterN: function(at, n, op) {
+ for (var i = 0; i < this.children.length; ++i) {
+ var child = this.children[i], sz = child.chunkSize();
+ if (at < sz) {
+ var used = Math.min(n, sz - at);
+ if (child.iterN(at, used, op)) return true;
+ if ((n -= used) == 0) break;
+ at = 0;
+ } else at -= sz;
+ }
+ }
+ };
+
+ var nextDocId = 0;
+ var Doc = CodeMirror.Doc = function(text, mode, firstLine, lineSep) {
+ if (!(this instanceof Doc)) return new Doc(text, mode, firstLine, lineSep);
+ if (firstLine == null) firstLine = 0;
+
+ BranchChunk.call(this, [new LeafChunk([new Line("", null)])]);
+ this.first = firstLine;
+ this.scrollTop = this.scrollLeft = 0;
+ this.cantEdit = false;
+ this.cleanGeneration = 1;
+ this.frontier = firstLine;
+ var start = Pos(firstLine, 0);
+ this.sel = simpleSelection(start);
+ this.history = new History(null);
+ this.id = ++nextDocId;
+ this.modeOption = mode;
+ this.lineSep = lineSep;
+ this.extend = false;
+
+ if (typeof text == "string") text = this.splitLines(text);
+ updateDoc(this, {from: start, to: start, text: text});
+ setSelection(this, simpleSelection(start), sel_dontScroll);
+ };
+
+ Doc.prototype = createObj(BranchChunk.prototype, {
+ constructor: Doc,
+ // Iterate over the document. Supports two forms -- with only one
+ // argument, it calls that for each line in the document. With
+ // three, it iterates over the range given by the first two (with
+ // the second being non-inclusive).
+ iter: function(from, to, op) {
+ if (op) this.iterN(from - this.first, to - from, op);
+ else this.iterN(this.first, this.first + this.size, from);
+ },
+
+ // Non-public interface for adding and removing lines.
+ insert: function(at, lines) {
+ var height = 0;
+ for (var i = 0; i < lines.length; ++i) height += lines[i].height;
+ this.insertInner(at - this.first, lines, height);
+ },
+ remove: function(at, n) { this.removeInner(at - this.first, n); },
+
+ // From here, the methods are part of the public interface. Most
+ // are also available from CodeMirror (editor) instances.
+
+ getValue: function(lineSep) {
+ var lines = getLines(this, this.first, this.first + this.size);
+ if (lineSep === false) return lines;
+ return lines.join(lineSep || this.lineSeparator());
+ },
+ setValue: docMethodOp(function(code) {
+ var top = Pos(this.first, 0), last = this.first + this.size - 1;
+ makeChange(this, {from: top, to: Pos(last, getLine(this, last).text.length),
+ text: this.splitLines(code), origin: "setValue", full: true}, true);
+ setSelection(this, simpleSelection(top));
+ }),
+ replaceRange: function(code, from, to, origin) {
+ from = clipPos(this, from);
+ to = to ? clipPos(this, to) : from;
+ replaceRange(this, code, from, to, origin);
+ },
+ getRange: function(from, to, lineSep) {
+ var lines = getBetween(this, clipPos(this, from), clipPos(this, to));
+ if (lineSep === false) return lines;
+ return lines.join(lineSep || this.lineSeparator());
+ },
+
+ getLine: function(line) {var l = this.getLineHandle(line); return l && l.text;},
+
+ getLineHandle: function(line) {if (isLine(this, line)) return getLine(this, line);},
+ getLineNumber: function(line) {return lineNo(line);},
+
+ getLineHandleVisualStart: function(line) {
+ if (typeof line == "number") line = getLine(this, line);
+ return visualLine(line);
+ },
+
+ lineCount: function() {return this.size;},
+ firstLine: function() {return this.first;},
+ lastLine: function() {return this.first + this.size - 1;},
+
+ clipPos: function(pos) {return clipPos(this, pos);},
+
+ getCursor: function(start) {
+ var range = this.sel.primary(), pos;
+ if (start == null || start == "head") pos = range.head;
+ else if (start == "anchor") pos = range.anchor;
+ else if (start == "end" || start == "to" || start === false) pos = range.to();
+ else pos = range.from();
+ return pos;
+ },
+ listSelections: function() { return this.sel.ranges; },
+ somethingSelected: function() {return this.sel.somethingSelected();},
+
+ setCursor: docMethodOp(function(line, ch, options) {
+ setSimpleSelection(this, clipPos(this, typeof line == "number" ? Pos(line, ch || 0) : line), null, options);
+ }),
+ setSelection: docMethodOp(function(anchor, head, options) {
+ setSimpleSelection(this, clipPos(this, anchor), clipPos(this, head || anchor), options);
+ }),
+ extendSelection: docMethodOp(function(head, other, options) {
+ extendSelection(this, clipPos(this, head), other && clipPos(this, other), options);
+ }),
+ extendSelections: docMethodOp(function(heads, options) {
+ extendSelections(this, clipPosArray(this, heads), options);
+ }),
+ extendSelectionsBy: docMethodOp(function(f, options) {
+ var heads = map(this.sel.ranges, f);
+ extendSelections(this, clipPosArray(this, heads), options);
+ }),
+ setSelections: docMethodOp(function(ranges, primary, options) {
+ if (!ranges.length) return;
+ for (var i = 0, out = []; i < ranges.length; i++)
+ out[i] = new Range(clipPos(this, ranges[i].anchor),
+ clipPos(this, ranges[i].head));
+ if (primary == null) primary = Math.min(ranges.length - 1, this.sel.primIndex);
+ setSelection(this, normalizeSelection(out, primary), options);
+ }),
+ addSelection: docMethodOp(function(anchor, head, options) {
+ var ranges = this.sel.ranges.slice(0);
+ ranges.push(new Range(clipPos(this, anchor), clipPos(this, head || anchor)));
+ setSelection(this, normalizeSelection(ranges, ranges.length - 1), options);
+ }),
+
+ getSelection: function(lineSep) {
+ var ranges = this.sel.ranges, lines;
+ for (var i = 0; i < ranges.length; i++) {
+ var sel = getBetween(this, ranges[i].from(), ranges[i].to());
+ lines = lines ? lines.concat(sel) : sel;
+ }
+ if (lineSep === false) return lines;
+ else return lines.join(lineSep || this.lineSeparator());
+ },
+ getSelections: function(lineSep) {
+ var parts = [], ranges = this.sel.ranges;
+ for (var i = 0; i < ranges.length; i++) {
+ var sel = getBetween(this, ranges[i].from(), ranges[i].to());
+ if (lineSep !== false) sel = sel.join(lineSep || this.lineSeparator());
+ parts[i] = sel;
+ }
+ return parts;
+ },
+ replaceSelection: function(code, collapse, origin) {
+ var dup = [];
+ for (var i = 0; i < this.sel.ranges.length; i++)
+ dup[i] = code;
+ this.replaceSelections(dup, collapse, origin || "+input");
+ },
+ replaceSelections: docMethodOp(function(code, collapse, origin) {
+ var changes = [], sel = this.sel;
+ for (var i = 0; i < sel.ranges.length; i++) {
+ var range = sel.ranges[i];
+ changes[i] = {from: range.from(), to: range.to(), text: this.splitLines(code[i]), origin: origin};
+ }
+ var newSel = collapse && collapse != "end" && computeReplacedSel(this, changes, collapse);
+ for (var i = changes.length - 1; i >= 0; i--)
+ makeChange(this, changes[i]);
+ if (newSel) setSelectionReplaceHistory(this, newSel);
+ else if (this.cm) ensureCursorVisible(this.cm);
+ }),
+ undo: docMethodOp(function() {makeChangeFromHistory(this, "undo");}),
+ redo: docMethodOp(function() {makeChangeFromHistory(this, "redo");}),
+ undoSelection: docMethodOp(function() {makeChangeFromHistory(this, "undo", true);}),
+ redoSelection: docMethodOp(function() {makeChangeFromHistory(this, "redo", true);}),
+
+ setExtending: function(val) {this.extend = val;},
+ getExtending: function() {return this.extend;},
+
+ historySize: function() {
+ var hist = this.history, done = 0, undone = 0;
+ for (var i = 0; i < hist.done.length; i++) if (!hist.done[i].ranges) ++done;
+ for (var i = 0; i < hist.undone.length; i++) if (!hist.undone[i].ranges) ++undone;
+ return {undo: done, redo: undone};
+ },
+ clearHistory: function() {this.history = new History(this.history.maxGeneration);},
+
+ markClean: function() {
+ this.cleanGeneration = this.changeGeneration(true);
+ },
+ changeGeneration: function(forceSplit) {
+ if (forceSplit)
+ this.history.lastOp = this.history.lastSelOp = this.history.lastOrigin = null;
+ return this.history.generation;
+ },
+ isClean: function (gen) {
+ return this.history.generation == (gen || this.cleanGeneration);
+ },
+
+ getHistory: function() {
+ return {done: copyHistoryArray(this.history.done),
+ undone: copyHistoryArray(this.history.undone)};
+ },
+ setHistory: function(histData) {
+ var hist = this.history = new History(this.history.maxGeneration);
+ hist.done = copyHistoryArray(histData.done.slice(0), null, true);
+ hist.undone = copyHistoryArray(histData.undone.slice(0), null, true);
+ },
+
+ addLineClass: docMethodOp(function(handle, where, cls) {
+ return changeLine(this, handle, where == "gutter" ? "gutter" : "class", function(line) {
+ var prop = where == "text" ? "textClass"
+ : where == "background" ? "bgClass"
+ : where == "gutter" ? "gutterClass" : "wrapClass";
+ if (!line[prop]) line[prop] = cls;
+ else if (classTest(cls).test(line[prop])) return false;
+ else line[prop] += " " + cls;
+ return true;
+ });
+ }),
+ removeLineClass: docMethodOp(function(handle, where, cls) {
+ return changeLine(this, handle, where == "gutter" ? "gutter" : "class", function(line) {
+ var prop = where == "text" ? "textClass"
+ : where == "background" ? "bgClass"
+ : where == "gutter" ? "gutterClass" : "wrapClass";
+ var cur = line[prop];
+ if (!cur) return false;
+ else if (cls == null) line[prop] = null;
+ else {
+ var found = cur.match(classTest(cls));
+ if (!found) return false;
+ var end = found.index + found[0].length;
+ line[prop] = cur.slice(0, found.index) + (!found.index || end == cur.length ? "" : " ") + cur.slice(end) || null;
+ }
+ return true;
+ });
+ }),
+
+ addLineWidget: docMethodOp(function(handle, node, options) {
+ return addLineWidget(this, handle, node, options);
+ }),
+ removeLineWidget: function(widget) { widget.clear(); },
+
+ markText: function(from, to, options) {
+ return markText(this, clipPos(this, from), clipPos(this, to), options, options && options.type || "range");
+ },
+ setBookmark: function(pos, options) {
+ var realOpts = {replacedWith: options && (options.nodeType == null ? options.widget : options),
+ insertLeft: options && options.insertLeft,
+ clearWhenEmpty: false, shared: options && options.shared,
+ handleMouseEvents: options && options.handleMouseEvents};
+ pos = clipPos(this, pos);
+ return markText(this, pos, pos, realOpts, "bookmark");
+ },
+ findMarksAt: function(pos) {
+ pos = clipPos(this, pos);
+ var markers = [], spans = getLine(this, pos.line).markedSpans;
+ if (spans) for (var i = 0; i < spans.length; ++i) {
+ var span = spans[i];
+ if ((span.from == null || span.from <= pos.ch) &&
+ (span.to == null || span.to >= pos.ch))
+ markers.push(span.marker.parent || span.marker);
+ }
+ return markers;
+ },
+ findMarks: function(from, to, filter) {
+ from = clipPos(this, from); to = clipPos(this, to);
+ var found = [], lineNo = from.line;
+ this.iter(from.line, to.line + 1, function(line) {
+ var spans = line.markedSpans;
+ if (spans) for (var i = 0; i < spans.length; i++) {
+ var span = spans[i];
+ if (!(span.to != null && lineNo == from.line && from.ch >= span.to ||
+ span.from == null && lineNo != from.line ||
+ span.from != null && lineNo == to.line && span.from >= to.ch) &&
+ (!filter || filter(span.marker)))
+ found.push(span.marker.parent || span.marker);
+ }
+ ++lineNo;
+ });
+ return found;
+ },
+ getAllMarks: function() {
+ var markers = [];
+ this.iter(function(line) {
+ var sps = line.markedSpans;
+ if (sps) for (var i = 0; i < sps.length; ++i)
+ if (sps[i].from != null) markers.push(sps[i].marker);
+ });
+ return markers;
+ },
+
+ posFromIndex: function(off) {
+ var ch, lineNo = this.first, sepSize = this.lineSeparator().length;
+ this.iter(function(line) {
+ var sz = line.text.length + sepSize;
+ if (sz > off) { ch = off; return true; }
+ off -= sz;
+ ++lineNo;
+ });
+ return clipPos(this, Pos(lineNo, ch));
+ },
+ indexFromPos: function (coords) {
+ coords = clipPos(this, coords);
+ var index = coords.ch;
+ if (coords.line < this.first || coords.ch < 0) return 0;
+ var sepSize = this.lineSeparator().length;
+ this.iter(this.first, coords.line, function (line) {
+ index += line.text.length + sepSize;
+ });
+ return index;
+ },
+
+ copy: function(copyHistory) {
+ var doc = new Doc(getLines(this, this.first, this.first + this.size),
+ this.modeOption, this.first, this.lineSep);
+ doc.scrollTop = this.scrollTop; doc.scrollLeft = this.scrollLeft;
+ doc.sel = this.sel;
+ doc.extend = false;
+ if (copyHistory) {
+ doc.history.undoDepth = this.history.undoDepth;
+ doc.setHistory(this.getHistory());
+ }
+ return doc;
+ },
+
+ linkedDoc: function(options) {
+ if (!options) options = {};
+ var from = this.first, to = this.first + this.size;
+ if (options.from != null && options.from > from) from = options.from;
+ if (options.to != null && options.to < to) to = options.to;
+ var copy = new Doc(getLines(this, from, to), options.mode || this.modeOption, from, this.lineSep);
+ if (options.sharedHist) copy.history = this.history;
+ (this.linked || (this.linked = [])).push({doc: copy, sharedHist: options.sharedHist});
+ copy.linked = [{doc: this, isParent: true, sharedHist: options.sharedHist}];
+ copySharedMarkers(copy, findSharedMarkers(this));
+ return copy;
+ },
+ unlinkDoc: function(other) {
+ if (other instanceof CodeMirror) other = other.doc;
+ if (this.linked) for (var i = 0; i < this.linked.length; ++i) {
+ var link = this.linked[i];
+ if (link.doc != other) continue;
+ this.linked.splice(i, 1);
+ other.unlinkDoc(this);
+ detachSharedMarkers(findSharedMarkers(this));
+ break;
+ }
+ // If the histories were shared, split them again
+ if (other.history == this.history) {
+ var splitIds = [other.id];
+ linkedDocs(other, function(doc) {splitIds.push(doc.id);}, true);
+ other.history = new History(null);
+ other.history.done = copyHistoryArray(this.history.done, splitIds);
+ other.history.undone = copyHistoryArray(this.history.undone, splitIds);
+ }
+ },
+ iterLinkedDocs: function(f) {linkedDocs(this, f);},
+
+ getMode: function() {return this.mode;},
+ getEditor: function() {return this.cm;},
+
+ splitLines: function(str) {
+ if (this.lineSep) return str.split(this.lineSep);
+ return splitLinesAuto(str);
+ },
+ lineSeparator: function() { return this.lineSep || "\n"; }
+ });
+
+ // Public alias.
+ Doc.prototype.eachLine = Doc.prototype.iter;
+
+ // Set up methods on CodeMirror's prototype to redirect to the editor's document.
+ var dontDelegate = "iter insert remove copy getEditor constructor".split(" ");
+ for (var prop in Doc.prototype) if (Doc.prototype.hasOwnProperty(prop) && indexOf(dontDelegate, prop) < 0)
+ CodeMirror.prototype[prop] = (function(method) {
+ return function() {return method.apply(this.doc, arguments);};
+ })(Doc.prototype[prop]);
+
+ eventMixin(Doc);
+
+ // Call f for all linked documents.
+ function linkedDocs(doc, f, sharedHistOnly) {
+ function propagate(doc, skip, sharedHist) {
+ if (doc.linked) for (var i = 0; i < doc.linked.length; ++i) {
+ var rel = doc.linked[i];
+ if (rel.doc == skip) continue;
+ var shared = sharedHist && rel.sharedHist;
+ if (sharedHistOnly && !shared) continue;
+ f(rel.doc, shared);
+ propagate(rel.doc, doc, shared);
+ }
+ }
+ propagate(doc, null, true);
+ }
+
+ // Attach a document to an editor.
+ function attachDoc(cm, doc) {
+ if (doc.cm) throw new Error("This document is already in use.");
+ cm.doc = doc;
+ doc.cm = cm;
+ estimateLineHeights(cm);
+ loadMode(cm);
+ if (!cm.options.lineWrapping) findMaxLine(cm);
+ cm.options.mode = doc.modeOption;
+ regChange(cm);
+ }
+
+ // LINE UTILITIES
+
+ // Find the line object corresponding to the given line number.
+ function getLine(doc, n) {
+ n -= doc.first;
+ if (n < 0 || n >= doc.size) throw new Error("There is no line " + (n + doc.first) + " in the document.");
+ for (var chunk = doc; !chunk.lines;) {
+ for (var i = 0;; ++i) {
+ var child = chunk.children[i], sz = child.chunkSize();
+ if (n < sz) { chunk = child; break; }
+ n -= sz;
+ }
+ }
+ return chunk.lines[n];
+ }
+
+ // Get the part of a document between two positions, as an array of
+ // strings.
+ function getBetween(doc, start, end) {
+ var out = [], n = start.line;
+ doc.iter(start.line, end.line + 1, function(line) {
+ var text = line.text;
+ if (n == end.line) text = text.slice(0, end.ch);
+ if (n == start.line) text = text.slice(start.ch);
+ out.push(text);
+ ++n;
+ });
+ return out;
+ }
+ // Get the lines between from and to, as array of strings.
+ function getLines(doc, from, to) {
+ var out = [];
+ doc.iter(from, to, function(line) { out.push(line.text); });
+ return out;
+ }
+
+ // Update the height of a line, propagating the height change
+ // upwards to parent nodes.
+ function updateLineHeight(line, height) {
+ var diff = height - line.height;
+ if (diff) for (var n = line; n; n = n.parent) n.height += diff;
+ }
+
+ // Given a line object, find its line number by walking up through
+ // its parent links.
+ function lineNo(line) {
+ if (line.parent == null) return null;
+ var cur = line.parent, no = indexOf(cur.lines, line);
+ for (var chunk = cur.parent; chunk; cur = chunk, chunk = chunk.parent) {
+ for (var i = 0;; ++i) {
+ if (chunk.children[i] == cur) break;
+ no += chunk.children[i].chunkSize();
+ }
+ }
+ return no + cur.first;
+ }
+
+ // Find the line at the given vertical position, using the height
+ // information in the document tree.
+ function lineAtHeight(chunk, h) {
+ var n = chunk.first;
+ outer: do {
+ for (var i = 0; i < chunk.children.length; ++i) {
+ var child = chunk.children[i], ch = child.height;
+ if (h < ch) { chunk = child; continue outer; }
+ h -= ch;
+ n += child.chunkSize();
+ }
+ return n;
+ } while (!chunk.lines);
+ for (var i = 0; i < chunk.lines.length; ++i) {
+ var line = chunk.lines[i], lh = line.height;
+ if (h < lh) break;
+ h -= lh;
+ }
+ return n + i;
+ }
+
+
+ // Find the height above the given line.
+ function heightAtLine(lineObj) {
+ lineObj = visualLine(lineObj);
+
+ var h = 0, chunk = lineObj.parent;
+ for (var i = 0; i < chunk.lines.length; ++i) {
+ var line = chunk.lines[i];
+ if (line == lineObj) break;
+ else h += line.height;
+ }
+ for (var p = chunk.parent; p; chunk = p, p = chunk.parent) {
+ for (var i = 0; i < p.children.length; ++i) {
+ var cur = p.children[i];
+ if (cur == chunk) break;
+ else h += cur.height;
+ }
+ }
+ return h;
+ }
+
+ // Get the bidi ordering for the given line (and cache it). Returns
+ // false for lines that are fully left-to-right, and an array of
+ // BidiSpan objects otherwise.
+ function getOrder(line) {
+ var order = line.order;
+ if (order == null) order = line.order = bidiOrdering(line.text);
+ return order;
+ }
+
+ // HISTORY
+
+ function History(startGen) {
+ // Arrays of change events and selections. Doing something adds an
+ // event to done and clears undo. Undoing moves events from done
+ // to undone, redoing moves them in the other direction.
+ this.done = []; this.undone = [];
+ this.undoDepth = Infinity;
+ // Used to track when changes can be merged into a single undo
+ // event
+ this.lastModTime = this.lastSelTime = 0;
+ this.lastOp = this.lastSelOp = null;
+ this.lastOrigin = this.lastSelOrigin = null;
+ // Used by the isClean() method
+ this.generation = this.maxGeneration = startGen || 1;
+ }
+
+ // Create a history change event from an updateDoc-style change
+ // object.
+ function historyChangeFromChange(doc, change) {
+ var histChange = {from: copyPos(change.from), to: changeEnd(change), text: getBetween(doc, change.from, change.to)};
+ attachLocalSpans(doc, histChange, change.from.line, change.to.line + 1);
+ linkedDocs(doc, function(doc) {attachLocalSpans(doc, histChange, change.from.line, change.to.line + 1);}, true);
+ return histChange;
+ }
+
+ // Pop all selection events off the end of a history array. Stop at
+ // a change event.
+ function clearSelectionEvents(array) {
+ while (array.length) {
+ var last = lst(array);
+ if (last.ranges) array.pop();
+ else break;
+ }
+ }
+
+ // Find the top change event in the history. Pop off selection
+ // events that are in the way.
+ function lastChangeEvent(hist, force) {
+ if (force) {
+ clearSelectionEvents(hist.done);
+ return lst(hist.done);
+ } else if (hist.done.length && !lst(hist.done).ranges) {
+ return lst(hist.done);
+ } else if (hist.done.length > 1 && !hist.done[hist.done.length - 2].ranges) {
+ hist.done.pop();
+ return lst(hist.done);
+ }
+ }
+
+ // Register a change in the history. Merges changes that are within
+ // a single operation, ore are close together with an origin that
+ // allows merging (starting with "+") into a single event.
+ function addChangeToHistory(doc, change, selAfter, opId) {
+ var hist = doc.history;
+ hist.undone.length = 0;
+ var time = +new Date, cur;
+
+ if ((hist.lastOp == opId ||
+ hist.lastOrigin == change.origin && change.origin &&
+ ((change.origin.charAt(0) == "+" && doc.cm && hist.lastModTime > time - doc.cm.options.historyEventDelay) ||
+ change.origin.charAt(0) == "*")) &&
+ (cur = lastChangeEvent(hist, hist.lastOp == opId))) {
+ // Merge this change into the last event
+ var last = lst(cur.changes);
+ if (cmp(change.from, change.to) == 0 && cmp(change.from, last.to) == 0) {
+ // Optimized case for simple insertion -- don't want to add
+ // new changesets for every character typed
+ last.to = changeEnd(change);
+ } else {
+ // Add new sub-event
+ cur.changes.push(historyChangeFromChange(doc, change));
+ }
+ } else {
+ // Can not be merged, start a new event.
+ var before = lst(hist.done);
+ if (!before || !before.ranges)
+ pushSelectionToHistory(doc.sel, hist.done);
+ cur = {changes: [historyChangeFromChange(doc, change)],
+ generation: hist.generation};
+ hist.done.push(cur);
+ while (hist.done.length > hist.undoDepth) {
+ hist.done.shift();
+ if (!hist.done[0].ranges) hist.done.shift();
+ }
+ }
+ hist.done.push(selAfter);
+ hist.generation = ++hist.maxGeneration;
+ hist.lastModTime = hist.lastSelTime = time;
+ hist.lastOp = hist.lastSelOp = opId;
+ hist.lastOrigin = hist.lastSelOrigin = change.origin;
+
+ if (!last) signal(doc, "historyAdded");
+ }
+
+ function selectionEventCanBeMerged(doc, origin, prev, sel) {
+ var ch = origin.charAt(0);
+ return ch == "*" ||
+ ch == "+" &&
+ prev.ranges.length == sel.ranges.length &&
+ prev.somethingSelected() == sel.somethingSelected() &&
+ new Date - doc.history.lastSelTime <= (doc.cm ? doc.cm.options.historyEventDelay : 500);
+ }
+
+ // Called whenever the selection changes, sets the new selection as
+ // the pending selection in the history, and pushes the old pending
+ // selection into the 'done' array when it was significantly
+ // different (in number of selected ranges, emptiness, or time).
+ function addSelectionToHistory(doc, sel, opId, options) {
+ var hist = doc.history, origin = options && options.origin;
+
+ // A new event is started when the previous origin does not match
+ // the current, or the origins don't allow matching. Origins
+ // starting with * are always merged, those starting with + are
+ // merged when similar and close together in time.
+ if (opId == hist.lastSelOp ||
+ (origin && hist.lastSelOrigin == origin &&
+ (hist.lastModTime == hist.lastSelTime && hist.lastOrigin == origin ||
+ selectionEventCanBeMerged(doc, origin, lst(hist.done), sel))))
+ hist.done[hist.done.length - 1] = sel;
+ else
+ pushSelectionToHistory(sel, hist.done);
+
+ hist.lastSelTime = +new Date;
+ hist.lastSelOrigin = origin;
+ hist.lastSelOp = opId;
+ if (options && options.clearRedo !== false)
+ clearSelectionEvents(hist.undone);
+ }
+
+ function pushSelectionToHistory(sel, dest) {
+ var top = lst(dest);
+ if (!(top && top.ranges && top.equals(sel)))
+ dest.push(sel);
+ }
+
+ // Used to store marked span information in the history.
+ function attachLocalSpans(doc, change, from, to) {
+ var existing = change["spans_" + doc.id], n = 0;
+ doc.iter(Math.max(doc.first, from), Math.min(doc.first + doc.size, to), function(line) {
+ if (line.markedSpans)
+ (existing || (existing = change["spans_" + doc.id] = {}))[n] = line.markedSpans;
+ ++n;
+ });
+ }
+
+ // When un/re-doing restores text containing marked spans, those
+ // that have been explicitly cleared should not be restored.
+ function removeClearedSpans(spans) {
+ if (!spans) return null;
+ for (var i = 0, out; i < spans.length; ++i) {
+ if (spans[i].marker.explicitlyCleared) { if (!out) out = spans.slice(0, i); }
+ else if (out) out.push(spans[i]);
+ }
+ return !out ? spans : out.length ? out : null;
+ }
+
+ // Retrieve and filter the old marked spans stored in a change event.
+ function getOldSpans(doc, change) {
+ var found = change["spans_" + doc.id];
+ if (!found) return null;
+ for (var i = 0, nw = []; i < change.text.length; ++i)
+ nw.push(removeClearedSpans(found[i]));
+ return nw;
+ }
+
+ // Used both to provide a JSON-safe object in .getHistory, and, when
+ // detaching a document, to split the history in two
+ function copyHistoryArray(events, newGroup, instantiateSel) {
+ for (var i = 0, copy = []; i < events.length; ++i) {
+ var event = events[i];
+ if (event.ranges) {
+ copy.push(instantiateSel ? Selection.prototype.deepCopy.call(event) : event);
+ continue;
+ }
+ var changes = event.changes, newChanges = [];
+ copy.push({changes: newChanges});
+ for (var j = 0; j < changes.length; ++j) {
+ var change = changes[j], m;
+ newChanges.push({from: change.from, to: change.to, text: change.text});
+ if (newGroup) for (var prop in change) if (m = prop.match(/^spans_(\d+)$/)) {
+ if (indexOf(newGroup, Number(m[1])) > -1) {
+ lst(newChanges)[prop] = change[prop];
+ delete change[prop];
+ }
+ }
+ }
+ }
+ return copy;
+ }
+
+ // Rebasing/resetting history to deal with externally-sourced changes
+
+ function rebaseHistSelSingle(pos, from, to, diff) {
+ if (to < pos.line) {
+ pos.line += diff;
+ } else if (from < pos.line) {
+ pos.line = from;
+ pos.ch = 0;
+ }
+ }
+
+ // Tries to rebase an array of history events given a change in the
+ // document. If the change touches the same lines as the event, the
+ // event, and everything 'behind' it, is discarded. If the change is
+ // before the event, the event's positions are updated. Uses a
+ // copy-on-write scheme for the positions, to avoid having to
+ // reallocate them all on every rebase, but also avoid problems with
+ // shared position objects being unsafely updated.
+ function rebaseHistArray(array, from, to, diff) {
+ for (var i = 0; i < array.length; ++i) {
+ var sub = array[i], ok = true;
+ if (sub.ranges) {
+ if (!sub.copied) { sub = array[i] = sub.deepCopy(); sub.copied = true; }
+ for (var j = 0; j < sub.ranges.length; j++) {
+ rebaseHistSelSingle(sub.ranges[j].anchor, from, to, diff);
+ rebaseHistSelSingle(sub.ranges[j].head, from, to, diff);
+ }
+ continue;
+ }
+ for (var j = 0; j < sub.changes.length; ++j) {
+ var cur = sub.changes[j];
+ if (to < cur.from.line) {
+ cur.from = Pos(cur.from.line + diff, cur.from.ch);
+ cur.to = Pos(cur.to.line + diff, cur.to.ch);
+ } else if (from <= cur.to.line) {
+ ok = false;
+ break;
+ }
+ }
+ if (!ok) {
+ array.splice(0, i + 1);
+ i = 0;
+ }
+ }
+ }
+
+ function rebaseHist(hist, change) {
+ var from = change.from.line, to = change.to.line, diff = change.text.length - (to - from) - 1;
+ rebaseHistArray(hist.done, from, to, diff);
+ rebaseHistArray(hist.undone, from, to, diff);
+ }
+
+ // EVENT UTILITIES
+
+ // Due to the fact that we still support jurassic IE versions, some
+ // compatibility wrappers are needed.
+
+ var e_preventDefault = CodeMirror.e_preventDefault = function(e) {
+ if (e.preventDefault) e.preventDefault();
+ else e.returnValue = false;
+ };
+ var e_stopPropagation = CodeMirror.e_stopPropagation = function(e) {
+ if (e.stopPropagation) e.stopPropagation();
+ else e.cancelBubble = true;
+ };
+ function e_defaultPrevented(e) {
+ return e.defaultPrevented != null ? e.defaultPrevented : e.returnValue == false;
+ }
+ var e_stop = CodeMirror.e_stop = function(e) {e_preventDefault(e); e_stopPropagation(e);};
+
+ function e_target(e) {return e.target || e.srcElement;}
+ function e_button(e) {
+ var b = e.which;
+ if (b == null) {
+ if (e.button & 1) b = 1;
+ else if (e.button & 2) b = 3;
+ else if (e.button & 4) b = 2;
+ }
+ if (mac && e.ctrlKey && b == 1) b = 3;
+ return b;
+ }
+
+ // EVENT HANDLING
+
+ // Lightweight event framework. on/off also work on DOM nodes,
+ // registering native DOM handlers.
+
+ var on = CodeMirror.on = function(emitter, type, f) {
+ if (emitter.addEventListener)
+ emitter.addEventListener(type, f, false);
+ else if (emitter.attachEvent)
+ emitter.attachEvent("on" + type, f);
+ else {
+ var map = emitter._handlers || (emitter._handlers = {});
+ var arr = map[type] || (map[type] = []);
+ arr.push(f);
+ }
+ };
+
+ var noHandlers = []
+ function getHandlers(emitter, type, copy) {
+ var arr = emitter._handlers && emitter._handlers[type]
+ if (copy) return arr && arr.length > 0 ? arr.slice() : noHandlers
+ else return arr || noHandlers
+ }
+
+ var off = CodeMirror.off = function(emitter, type, f) {
+ if (emitter.removeEventListener)
+ emitter.removeEventListener(type, f, false);
+ else if (emitter.detachEvent)
+ emitter.detachEvent("on" + type, f);
+ else {
+ var handlers = getHandlers(emitter, type, false)
+ for (var i = 0; i < handlers.length; ++i)
+ if (handlers[i] == f) { handlers.splice(i, 1); break; }
+ }
+ };
+
+ var signal = CodeMirror.signal = function(emitter, type /*, values...*/) {
+ var handlers = getHandlers(emitter, type, true)
+ if (!handlers.length) return;
+ var args = Array.prototype.slice.call(arguments, 2);
+ for (var i = 0; i < handlers.length; ++i) handlers[i].apply(null, args);
+ };
+
+ var orphanDelayedCallbacks = null;
+
+ // Often, we want to signal events at a point where we are in the
+ // middle of some work, but don't want the handler to start calling
+ // other methods on the editor, which might be in an inconsistent
+ // state or simply not expect any other events to happen.
+ // signalLater looks whether there are any handlers, and schedules
+ // them to be executed when the last operation ends, or, if no
+ // operation is active, when a timeout fires.
+ function signalLater(emitter, type /*, values...*/) {
+ var arr = getHandlers(emitter, type, false)
+ if (!arr.length) return;
+ var args = Array.prototype.slice.call(arguments, 2), list;
+ if (operationGroup) {
+ list = operationGroup.delayedCallbacks;
+ } else if (orphanDelayedCallbacks) {
+ list = orphanDelayedCallbacks;
+ } else {
+ list = orphanDelayedCallbacks = [];
+ setTimeout(fireOrphanDelayed, 0);
+ }
+ function bnd(f) {return function(){f.apply(null, args);};};
+ for (var i = 0; i < arr.length; ++i)
+ list.push(bnd(arr[i]));
+ }
+
+ function fireOrphanDelayed() {
+ var delayed = orphanDelayedCallbacks;
+ orphanDelayedCallbacks = null;
+ for (var i = 0; i < delayed.length; ++i) delayed[i]();
+ }
+
+ // The DOM events that CodeMirror handles can be overridden by
+ // registering a (non-DOM) handler on the editor for the event name,
+ // and preventDefault-ing the event in that handler.
+ function signalDOMEvent(cm, e, override) {
+ if (typeof e == "string")
+ e = {type: e, preventDefault: function() { this.defaultPrevented = true; }};
+ signal(cm, override || e.type, cm, e);
+ return e_defaultPrevented(e) || e.codemirrorIgnore;
+ }
+
+ function signalCursorActivity(cm) {
+ var arr = cm._handlers && cm._handlers.cursorActivity;
+ if (!arr) return;
+ var set = cm.curOp.cursorActivityHandlers || (cm.curOp.cursorActivityHandlers = []);
+ for (var i = 0; i < arr.length; ++i) if (indexOf(set, arr[i]) == -1)
+ set.push(arr[i]);
+ }
+
+ function hasHandler(emitter, type) {
+ return getHandlers(emitter, type).length > 0
+ }
+
+ // Add on and off methods to a constructor's prototype, to make
+ // registering events on such objects more convenient.
+ function eventMixin(ctor) {
+ ctor.prototype.on = function(type, f) {on(this, type, f);};
+ ctor.prototype.off = function(type, f) {off(this, type, f);};
+ }
+
+ // MISC UTILITIES
+
+ // Number of pixels added to scroller and sizer to hide scrollbar
+ var scrollerGap = 30;
+
+ // Returned or thrown by various protocols to signal 'I'm not
+ // handling this'.
+ var Pass = CodeMirror.Pass = {toString: function(){return "CodeMirror.Pass";}};
+
+ // Reused option objects for setSelection & friends
+ var sel_dontScroll = {scroll: false}, sel_mouse = {origin: "*mouse"}, sel_move = {origin: "+move"};
+
+ function Delayed() {this.id = null;}
+ Delayed.prototype.set = function(ms, f) {
+ clearTimeout(this.id);
+ this.id = setTimeout(f, ms);
+ };
+
+ // Counts the column offset in a string, taking tabs into account.
+ // Used mostly to find indentation.
+ var countColumn = CodeMirror.countColumn = function(string, end, tabSize, startIndex, startValue) {
+ if (end == null) {
+ end = string.search(/[^\s\u00a0]/);
+ if (end == -1) end = string.length;
+ }
+ for (var i = startIndex || 0, n = startValue || 0;;) {
+ var nextTab = string.indexOf("\t", i);
+ if (nextTab < 0 || nextTab >= end)
+ return n + (end - i);
+ n += nextTab - i;
+ n += tabSize - (n % tabSize);
+ i = nextTab + 1;
+ }
+ };
+
+ // The inverse of countColumn -- find the offset that corresponds to
+ // a particular column.
+ var findColumn = CodeMirror.findColumn = function(string, goal, tabSize) {
+ for (var pos = 0, col = 0;;) {
+ var nextTab = string.indexOf("\t", pos);
+ if (nextTab == -1) nextTab = string.length;
+ var skipped = nextTab - pos;
+ if (nextTab == string.length || col + skipped >= goal)
+ return pos + Math.min(skipped, goal - col);
+ col += nextTab - pos;
+ col += tabSize - (col % tabSize);
+ pos = nextTab + 1;
+ if (col >= goal) return pos;
+ }
+ }
+
+ var spaceStrs = [""];
+ function spaceStr(n) {
+ while (spaceStrs.length <= n)
+ spaceStrs.push(lst(spaceStrs) + " ");
+ return spaceStrs[n];
+ }
+
+ function lst(arr) { return arr[arr.length-1]; }
+
+ var selectInput = function(node) { node.select(); };
+ if (ios) // Mobile Safari apparently has a bug where select() is broken.
+ selectInput = function(node) { node.selectionStart = 0; node.selectionEnd = node.value.length; };
+ else if (ie) // Suppress mysterious IE10 errors
+ selectInput = function(node) { try { node.select(); } catch(_e) {} };
+
+ function indexOf(array, elt) {
+ for (var i = 0; i < array.length; ++i)
+ if (array[i] == elt) return i;
+ return -1;
+ }
+ function map(array, f) {
+ var out = [];
+ for (var i = 0; i < array.length; i++) out[i] = f(array[i], i);
+ return out;
+ }
+
+ function nothing() {}
+
+ function createObj(base, props) {
+ var inst;
+ if (Object.create) {
+ inst = Object.create(base);
+ } else {
+ nothing.prototype = base;
+ inst = new nothing();
+ }
+ if (props) copyObj(props, inst);
+ return inst;
+ };
+
+ function copyObj(obj, target, overwrite) {
+ if (!target) target = {};
+ for (var prop in obj)
+ if (obj.hasOwnProperty(prop) && (overwrite !== false || !target.hasOwnProperty(prop)))
+ target[prop] = obj[prop];
+ return target;
+ }
+
+ function bind(f) {
+ var args = Array.prototype.slice.call(arguments, 1);
+ return function(){return f.apply(null, args);};
+ }
+
+ var nonASCIISingleCaseWordChar = /[\u00df\u0587\u0590-\u05f4\u0600-\u06ff\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc\uac00-\ud7af]/;
+ var isWordCharBasic = CodeMirror.isWordChar = function(ch) {
+ return /\w/.test(ch) || ch > "\x80" &&
+ (ch.toUpperCase() != ch.toLowerCase() || nonASCIISingleCaseWordChar.test(ch));
+ };
+ function isWordChar(ch, helper) {
+ if (!helper) return isWordCharBasic(ch);
+ if (helper.source.indexOf("\\w") > -1 && isWordCharBasic(ch)) return true;
+ return helper.test(ch);
+ }
+
+ function isEmpty(obj) {
+ for (var n in obj) if (obj.hasOwnProperty(n) && obj[n]) return false;
+ return true;
+ }
+
+ // Extending unicode characters. A series of a non-extending char +
+ // any number of extending chars is treated as a single unit as far
+ // as editing and measuring is concerned. This is not fully correct,
+ // since some scripts/fonts/browsers also treat other configurations
+ // of code points as a group.
+ var extendingChars = /[\u0300-\u036f\u0483-\u0489\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u064b-\u065e\u0670\u06d6-\u06dc\u06de-\u06e4\u06e7\u06e8\u06ea-\u06ed\u0711\u0730-\u074a\u07a6-\u07b0\u07eb-\u07f3\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0900-\u0902\u093c\u0941-\u0948\u094d\u0951-\u0955\u0962\u0963\u0981\u09bc\u09be\u09c1-\u09c4\u09cd\u09d7\u09e2\u09e3\u0a01\u0a02\u0a3c\u0a41\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a70\u0a71\u0a75\u0a81\u0a82\u0abc\u0ac1-\u0ac5\u0ac7\u0ac8\u0acd\u0ae2\u0ae3\u0b01\u0b3c\u0b3e\u0b3f\u0b41-\u0b44\u0b4d\u0b56\u0b57\u0b62\u0b63\u0b82\u0bbe\u0bc0\u0bcd\u0bd7\u0c3e-\u0c40\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62\u0c63\u0cbc\u0cbf\u0cc2\u0cc6\u0ccc\u0ccd\u0cd5\u0cd6\u0ce2\u0ce3\u0d3e\u0d41-\u0d44\u0d4d\u0d57\u0d62\u0d63\u0dca\u0dcf\u0dd2-\u0dd4\u0dd6\u0ddf\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0eb1\u0eb4-\u0eb9\u0ebb\u0ebc\u0ec8-\u0ecd\u0f18\u0f19\u0f35\u0f37\u0f39\u0f71-\u0f7e\u0f80-\u0f84\u0f86\u0f87\u0f90-\u0f97\u0f99-\u0fbc\u0fc6\u102d-\u1030\u1032-\u1037\u1039\u103a\u103d\u103e\u1058\u1059\u105e-\u1060\u1071-\u1074\u1082\u1085\u1086\u108d\u109d\u135f\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17b7-\u17bd\u17c6\u17c9-\u17d3\u17dd\u180b-\u180d\u18a9\u1920-\u1922\u1927\u1928\u1932\u1939-\u193b\u1a17\u1a18\u1a56\u1a58-\u1a5e\u1a60\u1a62\u1a65-\u1a6c\u1a73-\u1a7c\u1a7f\u1b00-\u1b03\u1b34\u1b36-\u1b3a\u1b3c\u1b42\u1b6b-\u1b73\u1b80\u1b81\u1ba2-\u1ba5\u1ba8\u1ba9\u1c2c-\u1c33\u1c36\u1c37\u1cd0-\u1cd2\u1cd4-\u1ce0\u1ce2-\u1ce8\u1ced\u1dc0-\u1de6\u1dfd-\u1dff\u200c\u200d\u20d0-\u20f0\u2cef-\u2cf1\u2de0-\u2dff\u302a-\u302f\u3099\u309a\ua66f-\ua672\ua67c\ua67d\ua6f0\ua6f1\ua802\ua806\ua80b\ua825\ua826\ua8c4\ua8e0-\ua8f1\ua926-\ua92d\ua947-\ua951\ua980-\ua982\ua9b3\ua9b6-\ua9b9\ua9bc\uaa29-\uaa2e\uaa31\uaa32\uaa35\uaa36\uaa43\uaa4c\uaab0\uaab2-\uaab4\uaab7\uaab8\uaabe\uaabf\uaac1\uabe5\uabe8\uabed\udc00-\udfff\ufb1e\ufe00-\ufe0f\ufe20-\ufe26\uff9e\uff9f]/;
+ function isExtendingChar(ch) { return ch.charCodeAt(0) >= 768 && extendingChars.test(ch); }
+
+ // DOM UTILITIES
+
+ function elt(tag, content, className, style) {
+ var e = document.createElement(tag);
+ if (className) e.className = className;
+ if (style) e.style.cssText = style;
+ if (typeof content == "string") e.appendChild(document.createTextNode(content));
+ else if (content) for (var i = 0; i < content.length; ++i) e.appendChild(content[i]);
+ return e;
+ }
+
+ var range;
+ if (document.createRange) range = function(node, start, end, endNode) {
+ var r = document.createRange();
+ r.setEnd(endNode || node, end);
+ r.setStart(node, start);
+ return r;
+ };
+ else range = function(node, start, end) {
+ var r = document.body.createTextRange();
+ try { r.moveToElementText(node.parentNode); }
+ catch(e) { return r; }
+ r.collapse(true);
+ r.moveEnd("character", end);
+ r.moveStart("character", start);
+ return r;
+ };
+
+ function removeChildren(e) {
+ for (var count = e.childNodes.length; count > 0; --count)
+ e.removeChild(e.firstChild);
+ return e;
+ }
+
+ function removeChildrenAndAdd(parent, e) {
+ return removeChildren(parent).appendChild(e);
+ }
+
+ var contains = CodeMirror.contains = function(parent, child) {
+ if (child.nodeType == 3) // Android browser always returns false when child is a textnode
+ child = child.parentNode;
+ if (parent.contains)
+ return parent.contains(child);
+ do {
+ if (child.nodeType == 11) child = child.host;
+ if (child == parent) return true;
+ } while (child = child.parentNode);
+ };
+
+ function activeElt() {
+ var activeElement = document.activeElement;
+ while (activeElement && activeElement.root && activeElement.root.activeElement)
+ activeElement = activeElement.root.activeElement;
+ return activeElement;
+ }
+ // Older versions of IE throws unspecified error when touching
+ // document.activeElement in some cases (during loading, in iframe)
+ if (ie && ie_version < 11) activeElt = function() {
+ try { return document.activeElement; }
+ catch(e) { return document.body; }
+ };
+
+ function classTest(cls) { return new RegExp("(^|\\s)" + cls + "(?:$|\\s)\\s*"); }
+ var rmClass = CodeMirror.rmClass = function(node, cls) {
+ var current = node.className;
+ var match = classTest(cls).exec(current);
+ if (match) {
+ var after = current.slice(match.index + match[0].length);
+ node.className = current.slice(0, match.index) + (after ? match[1] + after : "");
+ }
+ };
+ var addClass = CodeMirror.addClass = function(node, cls) {
+ var current = node.className;
+ if (!classTest(cls).test(current)) node.className += (current ? " " : "") + cls;
+ };
+ function joinClasses(a, b) {
+ var as = a.split(" ");
+ for (var i = 0; i < as.length; i++)
+ if (as[i] && !classTest(as[i]).test(b)) b += " " + as[i];
+ return b;
+ }
+
+ // WINDOW-WIDE EVENTS
+
+ // These must be handled carefully, because naively registering a
+ // handler for each editor will cause the editors to never be
+ // garbage collected.
+
+ function forEachCodeMirror(f) {
+ if (!document.body.getElementsByClassName) return;
+ var byClass = document.body.getElementsByClassName("CodeMirror");
+ for (var i = 0; i < byClass.length; i++) {
+ var cm = byClass[i].CodeMirror;
+ if (cm) f(cm);
+ }
+ }
+
+ var globalsRegistered = false;
+ function ensureGlobalHandlers() {
+ if (globalsRegistered) return;
+ registerGlobalHandlers();
+ globalsRegistered = true;
+ }
+ function registerGlobalHandlers() {
+ // When the window resizes, we need to refresh active editors.
+ var resizeTimer;
+ on(window, "resize", function() {
+ if (resizeTimer == null) resizeTimer = setTimeout(function() {
+ resizeTimer = null;
+ forEachCodeMirror(onResize);
+ }, 100);
+ });
+ // When the window loses focus, we want to show the editor as blurred
+ on(window, "blur", function() {
+ forEachCodeMirror(onBlur);
+ });
+ }
+
+ // FEATURE DETECTION
+
+ // Detect drag-and-drop
+ var dragAndDrop = function() {
+ // There is *some* kind of drag-and-drop support in IE6-8, but I
+ // couldn't get it to work yet.
+ if (ie && ie_version < 9) return false;
+ var div = elt('div');
+ return "draggable" in div || "dragDrop" in div;
+ }();
+
+ var zwspSupported;
+ function zeroWidthElement(measure) {
+ if (zwspSupported == null) {
+ var test = elt("span", "\u200b");
+ removeChildrenAndAdd(measure, elt("span", [test, document.createTextNode("x")]));
+ if (measure.firstChild.offsetHeight != 0)
+ zwspSupported = test.offsetWidth <= 1 && test.offsetHeight > 2 && !(ie && ie_version < 8);
+ }
+ var node = zwspSupported ? elt("span", "\u200b") :
+ elt("span", "\u00a0", null, "display: inline-block; width: 1px; margin-right: -1px");
+ node.setAttribute("cm-text", "");
+ return node;
+ }
+
+ // Feature-detect IE's crummy client rect reporting for bidi text
+ var badBidiRects;
+ function hasBadBidiRects(measure) {
+ if (badBidiRects != null) return badBidiRects;
+ var txt = removeChildrenAndAdd(measure, document.createTextNode("A\u062eA"));
+ var r0 = range(txt, 0, 1).getBoundingClientRect();
+ if (!r0 || r0.left == r0.right) return false; // Safari returns null in some cases (#2780)
+ var r1 = range(txt, 1, 2).getBoundingClientRect();
+ return badBidiRects = (r1.right - r0.right < 3);
+ }
+
+ // See if "".split is the broken IE version, if so, provide an
+ // alternative way to split lines.
+ var splitLinesAuto = CodeMirror.splitLines = "\n\nb".split(/\n/).length != 3 ? function(string) {
+ var pos = 0, result = [], l = string.length;
+ while (pos <= l) {
+ var nl = string.indexOf("\n", pos);
+ if (nl == -1) nl = string.length;
+ var line = string.slice(pos, string.charAt(nl - 1) == "\r" ? nl - 1 : nl);
+ var rt = line.indexOf("\r");
+ if (rt != -1) {
+ result.push(line.slice(0, rt));
+ pos += rt + 1;
+ } else {
+ result.push(line);
+ pos = nl + 1;
+ }
+ }
+ return result;
+ } : function(string){return string.split(/\r\n?|\n/);};
+
+ var hasSelection = window.getSelection ? function(te) {
+ try { return te.selectionStart != te.selectionEnd; }
+ catch(e) { return false; }
+ } : function(te) {
+ try {var range = te.ownerDocument.selection.createRange();}
+ catch(e) {}
+ if (!range || range.parentElement() != te) return false;
+ return range.compareEndPoints("StartToEnd", range) != 0;
+ };
+
+ var hasCopyEvent = (function() {
+ var e = elt("div");
+ if ("oncopy" in e) return true;
+ e.setAttribute("oncopy", "return;");
+ return typeof e.oncopy == "function";
+ })();
+
+ var badZoomedRects = null;
+ function hasBadZoomedRects(measure) {
+ if (badZoomedRects != null) return badZoomedRects;
+ var node = removeChildrenAndAdd(measure, elt("span", "x"));
+ var normal = node.getBoundingClientRect();
+ var fromRange = range(node, 0, 1).getBoundingClientRect();
+ return badZoomedRects = Math.abs(normal.left - fromRange.left) > 1;
+ }
+
+ // KEY NAMES
+
+ var keyNames = CodeMirror.keyNames = {
+ 3: "Enter", 8: "Backspace", 9: "Tab", 13: "Enter", 16: "Shift", 17: "Ctrl", 18: "Alt",
+ 19: "Pause", 20: "CapsLock", 27: "Esc", 32: "Space", 33: "PageUp", 34: "PageDown", 35: "End",
+ 36: "Home", 37: "Left", 38: "Up", 39: "Right", 40: "Down", 44: "PrintScrn", 45: "Insert",
+ 46: "Delete", 59: ";", 61: "=", 91: "Mod", 92: "Mod", 93: "Mod",
+ 106: "*", 107: "=", 109: "-", 110: ".", 111: "/", 127: "Delete",
+ 173: "-", 186: ";", 187: "=", 188: ",", 189: "-", 190: ".", 191: "/", 192: "`", 219: "[", 220: "\\",
+ 221: "]", 222: "'", 63232: "Up", 63233: "Down", 63234: "Left", 63235: "Right", 63272: "Delete",
+ 63273: "Home", 63275: "End", 63276: "PageUp", 63277: "PageDown", 63302: "Insert"
+ };
+ (function() {
+ // Number keys
+ for (var i = 0; i < 10; i++) keyNames[i + 48] = keyNames[i + 96] = String(i);
+ // Alphabetic keys
+ for (var i = 65; i <= 90; i++) keyNames[i] = String.fromCharCode(i);
+ // Function keys
+ for (var i = 1; i <= 12; i++) keyNames[i + 111] = keyNames[i + 63235] = "F" + i;
+ })();
+
+ // BIDI HELPERS
+
+ function iterateBidiSections(order, from, to, f) {
+ if (!order) return f(from, to, "ltr");
+ var found = false;
+ for (var i = 0; i < order.length; ++i) {
+ var part = order[i];
+ if (part.from < to && part.to > from || from == to && part.to == from) {
+ f(Math.max(part.from, from), Math.min(part.to, to), part.level == 1 ? "rtl" : "ltr");
+ found = true;
+ }
+ }
+ if (!found) f(from, to, "ltr");
+ }
+
+ function bidiLeft(part) { return part.level % 2 ? part.to : part.from; }
+ function bidiRight(part) { return part.level % 2 ? part.from : part.to; }
+
+ function lineLeft(line) { var order = getOrder(line); return order ? bidiLeft(order[0]) : 0; }
+ function lineRight(line) {
+ var order = getOrder(line);
+ if (!order) return line.text.length;
+ return bidiRight(lst(order));
+ }
+
+ function lineStart(cm, lineN) {
+ var line = getLine(cm.doc, lineN);
+ var visual = visualLine(line);
+ if (visual != line) lineN = lineNo(visual);
+ var order = getOrder(visual);
+ var ch = !order ? 0 : order[0].level % 2 ? lineRight(visual) : lineLeft(visual);
+ return Pos(lineN, ch);
+ }
+ function lineEnd(cm, lineN) {
+ var merged, line = getLine(cm.doc, lineN);
+ while (merged = collapsedSpanAtEnd(line)) {
+ line = merged.find(1, true).line;
+ lineN = null;
+ }
+ var order = getOrder(line);
+ var ch = !order ? line.text.length : order[0].level % 2 ? lineLeft(line) : lineRight(line);
+ return Pos(lineN == null ? lineNo(line) : lineN, ch);
+ }
+ function lineStartSmart(cm, pos) {
+ var start = lineStart(cm, pos.line);
+ var line = getLine(cm.doc, start.line);
+ var order = getOrder(line);
+ if (!order || order[0].level == 0) {
+ var firstNonWS = Math.max(0, line.text.search(/\S/));
+ var inWS = pos.line == start.line && pos.ch <= firstNonWS && pos.ch;
+ return Pos(start.line, inWS ? 0 : firstNonWS);
+ }
+ return start;
+ }
+
+ function compareBidiLevel(order, a, b) {
+ var linedir = order[0].level;
+ if (a == linedir) return true;
+ if (b == linedir) return false;
+ return a < b;
+ }
+ var bidiOther;
+ function getBidiPartAt(order, pos) {
+ bidiOther = null;
+ for (var i = 0, found; i < order.length; ++i) {
+ var cur = order[i];
+ if (cur.from < pos && cur.to > pos) return i;
+ if ((cur.from == pos || cur.to == pos)) {
+ if (found == null) {
+ found = i;
+ } else if (compareBidiLevel(order, cur.level, order[found].level)) {
+ if (cur.from != cur.to) bidiOther = found;
+ return i;
+ } else {
+ if (cur.from != cur.to) bidiOther = i;
+ return found;
+ }
+ }
+ }
+ return found;
+ }
+
+ function moveInLine(line, pos, dir, byUnit) {
+ if (!byUnit) return pos + dir;
+ do pos += dir;
+ while (pos > 0 && isExtendingChar(line.text.charAt(pos)));
+ return pos;
+ }
+
+ // This is needed in order to move 'visually' through bi-directional
+ // text -- i.e., pressing left should make the cursor go left, even
+ // when in RTL text. The tricky part is the 'jumps', where RTL and
+ // LTR text touch each other. This often requires the cursor offset
+ // to move more than one unit, in order to visually move one unit.
+ function moveVisually(line, start, dir, byUnit) {
+ var bidi = getOrder(line);
+ if (!bidi) return moveLogically(line, start, dir, byUnit);
+ var pos = getBidiPartAt(bidi, start), part = bidi[pos];
+ var target = moveInLine(line, start, part.level % 2 ? -dir : dir, byUnit);
+
+ for (;;) {
+ if (target > part.from && target < part.to) return target;
+ if (target == part.from || target == part.to) {
+ if (getBidiPartAt(bidi, target) == pos) return target;
+ part = bidi[pos += dir];
+ return (dir > 0) == part.level % 2 ? part.to : part.from;
+ } else {
+ part = bidi[pos += dir];
+ if (!part) return null;
+ if ((dir > 0) == part.level % 2)
+ target = moveInLine(line, part.to, -1, byUnit);
+ else
+ target = moveInLine(line, part.from, 1, byUnit);
+ }
+ }
+ }
+
+ function moveLogically(line, start, dir, byUnit) {
+ var target = start + dir;
+ if (byUnit) while (target > 0 && isExtendingChar(line.text.charAt(target))) target += dir;
+ return target < 0 || target > line.text.length ? null : target;
+ }
+
+ // Bidirectional ordering algorithm
+ // See http://unicode.org/reports/tr9/tr9-13.html for the algorithm
+ // that this (partially) implements.
+
+ // One-char codes used for character types:
+ // L (L): Left-to-Right
+ // R (R): Right-to-Left
+ // r (AL): Right-to-Left Arabic
+ // 1 (EN): European Number
+ // + (ES): European Number Separator
+ // % (ET): European Number Terminator
+ // n (AN): Arabic Number
+ // , (CS): Common Number Separator
+ // m (NSM): Non-Spacing Mark
+ // b (BN): Boundary Neutral
+ // s (B): Paragraph Separator
+ // t (S): Segment Separator
+ // w (WS): Whitespace
+ // N (ON): Other Neutrals
+
+ // Returns null if characters are ordered as they appear
+ // (left-to-right), or an array of sections ({from, to, level}
+ // objects) in the order in which they occur visually.
+ var bidiOrdering = (function() {
+ // Character types for codepoints 0 to 0xff
+ var lowTypes = "bbbbbbbbbtstwsbbbbbbbbbbbbbbssstwNN%%%NNNNNN,N,N1111111111NNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNbbbbbbsbbbbbbbbbbbbbbbbbbbbbbbbbb,N%%%%NNNNLNNNNN%%11NLNNN1LNNNNNLLLLLLLLLLLLLLLLLLLLLLLNLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLN";
+ // Character types for codepoints 0x600 to 0x6ff
+ var arabicTypes = "rrrrrrrrrrrr,rNNmmmmmmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmrrrrrrrnnnnnnnnnn%nnrrrmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmmmmmmNmmmm";
+ function charType(code) {
+ if (code <= 0xf7) return lowTypes.charAt(code);
+ else if (0x590 <= code && code <= 0x5f4) return "R";
+ else if (0x600 <= code && code <= 0x6ed) return arabicTypes.charAt(code - 0x600);
+ else if (0x6ee <= code && code <= 0x8ac) return "r";
+ else if (0x2000 <= code && code <= 0x200b) return "w";
+ else if (code == 0x200c) return "b";
+ else return "L";
+ }
+
+ var bidiRE = /[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/;
+ var isNeutral = /[stwN]/, isStrong = /[LRr]/, countsAsLeft = /[Lb1n]/, countsAsNum = /[1n]/;
+ // Browsers seem to always treat the boundaries of block elements as being L.
+ var outerType = "L";
+
+ function BidiSpan(level, from, to) {
+ this.level = level;
+ this.from = from; this.to = to;
+ }
+
+ return function(str) {
+ if (!bidiRE.test(str)) return false;
+ var len = str.length, types = [];
+ for (var i = 0, type; i < len; ++i)
+ types.push(type = charType(str.charCodeAt(i)));
+
+ // W1. Examine each non-spacing mark (NSM) in the level run, and
+ // change the type of the NSM to the type of the previous
+ // character. If the NSM is at the start of the level run, it will
+ // get the type of sor.
+ for (var i = 0, prev = outerType; i < len; ++i) {
+ var type = types[i];
+ if (type == "m") types[i] = prev;
+ else prev = type;
+ }
+
+ // W2. Search backwards from each instance of a European number
+ // until the first strong type (R, L, AL, or sor) is found. If an
+ // AL is found, change the type of the European number to Arabic
+ // number.
+ // W3. Change all ALs to R.
+ for (var i = 0, cur = outerType; i < len; ++i) {
+ var type = types[i];
+ if (type == "1" && cur == "r") types[i] = "n";
+ else if (isStrong.test(type)) { cur = type; if (type == "r") types[i] = "R"; }
+ }
+
+ // W4. A single European separator between two European numbers
+ // changes to a European number. A single common separator between
+ // two numbers of the same type changes to that type.
+ for (var i = 1, prev = types[0]; i < len - 1; ++i) {
+ var type = types[i];
+ if (type == "+" && prev == "1" && types[i+1] == "1") types[i] = "1";
+ else if (type == "," && prev == types[i+1] &&
+ (prev == "1" || prev == "n")) types[i] = prev;
+ prev = type;
+ }
+
+ // W5. A sequence of European terminators adjacent to European
+ // numbers changes to all European numbers.
+ // W6. Otherwise, separators and terminators change to Other
+ // Neutral.
+ for (var i = 0; i < len; ++i) {
+ var type = types[i];
+ if (type == ",") types[i] = "N";
+ else if (type == "%") {
+ for (var end = i + 1; end < len && types[end] == "%"; ++end) {}
+ var replace = (i && types[i-1] == "!") || (end < len && types[end] == "1") ? "1" : "N";
+ for (var j = i; j < end; ++j) types[j] = replace;
+ i = end - 1;
+ }
+ }
+
+ // W7. Search backwards from each instance of a European number
+ // until the first strong type (R, L, or sor) is found. If an L is
+ // found, then change the type of the European number to L.
+ for (var i = 0, cur = outerType; i < len; ++i) {
+ var type = types[i];
+ if (cur == "L" && type == "1") types[i] = "L";
+ else if (isStrong.test(type)) cur = type;
+ }
+
+ // N1. A sequence of neutrals takes the direction of the
+ // surrounding strong text if the text on both sides has the same
+ // direction. European and Arabic numbers act as if they were R in
+ // terms of their influence on neutrals. Start-of-level-run (sor)
+ // and end-of-level-run (eor) are used at level run boundaries.
+ // N2. Any remaining neutrals take the embedding direction.
+ for (var i = 0; i < len; ++i) {
+ if (isNeutral.test(types[i])) {
+ for (var end = i + 1; end < len && isNeutral.test(types[end]); ++end) {}
+ var before = (i ? types[i-1] : outerType) == "L";
+ var after = (end < len ? types[end] : outerType) == "L";
+ var replace = before || after ? "L" : "R";
+ for (var j = i; j < end; ++j) types[j] = replace;
+ i = end - 1;
+ }
+ }
+
+ // Here we depart from the documented algorithm, in order to avoid
+ // building up an actual levels array. Since there are only three
+ // levels (0, 1, 2) in an implementation that doesn't take
+ // explicit embedding into account, we can build up the order on
+ // the fly, without following the level-based algorithm.
+ var order = [], m;
+ for (var i = 0; i < len;) {
+ if (countsAsLeft.test(types[i])) {
+ var start = i;
+ for (++i; i < len && countsAsLeft.test(types[i]); ++i) {}
+ order.push(new BidiSpan(0, start, i));
+ } else {
+ var pos = i, at = order.length;
+ for (++i; i < len && types[i] != "L"; ++i) {}
+ for (var j = pos; j < i;) {
+ if (countsAsNum.test(types[j])) {
+ if (pos < j) order.splice(at, 0, new BidiSpan(1, pos, j));
+ var nstart = j;
+ for (++j; j < i && countsAsNum.test(types[j]); ++j) {}
+ order.splice(at, 0, new BidiSpan(2, nstart, j));
+ pos = j;
+ } else ++j;
+ }
+ if (pos < i) order.splice(at, 0, new BidiSpan(1, pos, i));
+ }
+ }
+ if (order[0].level == 1 && (m = str.match(/^\s+/))) {
+ order[0].from = m[0].length;
+ order.unshift(new BidiSpan(0, 0, m[0].length));
+ }
+ if (lst(order).level == 1 && (m = str.match(/\s+$/))) {
+ lst(order).to -= m[0].length;
+ order.push(new BidiSpan(0, len - m[0].length, len));
+ }
+ if (order[0].level == 2)
+ order.unshift(new BidiSpan(1, order[0].to, order[0].to));
+ if (order[0].level != lst(order).level)
+ order.push(new BidiSpan(order[0].level, len, len));
+
+ return order;
+ };
+ })();
+
+ // THE END
+
+ CodeMirror.version = "5.16.0";
+
+ return CodeMirror;
+ });
+
+
+/***/ },
+/* 3 */
+/***/ function(module, exports, __webpack_require__) {
+
+ // CodeMirror, copyright (c) by Marijn Haverbeke and others
+ // Distributed under an MIT license: http://codemirror.net/LICENSE
+
+ (function(mod) {
+ if (true) // CommonJS
+ mod(__webpack_require__(2));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+ })(function(CodeMirror) {
+ "use strict";
+ var Pos = CodeMirror.Pos;
+
+ function SearchCursor(doc, query, pos, caseFold) {
+ this.atOccurrence = false; this.doc = doc;
+ if (caseFold == null && typeof query == "string") caseFold = false;
+
+ pos = pos ? doc.clipPos(pos) : Pos(0, 0);
+ this.pos = {from: pos, to: pos};
+
+ // The matches method is filled in based on the type of query.
+ // It takes a position and a direction, and returns an object
+ // describing the next occurrence of the query, or null if no
+ // more matches were found.
+ if (typeof query != "string") { // Regexp match
+ if (!query.global) query = new RegExp(query.source, query.ignoreCase ? "ig" : "g");
+ this.matches = function(reverse, pos) {
+ if (reverse) {
+ query.lastIndex = 0;
+ var line = doc.getLine(pos.line).slice(0, pos.ch), cutOff = 0, match, start;
+ for (;;) {
+ query.lastIndex = cutOff;
+ var newMatch = query.exec(line);
+ if (!newMatch) break;
+ match = newMatch;
+ start = match.index;
+ cutOff = match.index + (match[0].length || 1);
+ if (cutOff == line.length) break;
+ }
+ var matchLen = (match && match[0].length) || 0;
+ if (!matchLen) {
+ if (start == 0 && line.length == 0) {match = undefined;}
+ else if (start != doc.getLine(pos.line).length) {
+ matchLen++;
+ }
+ }
+ } else {
+ query.lastIndex = pos.ch;
+ var line = doc.getLine(pos.line), match = query.exec(line);
+ var matchLen = (match && match[0].length) || 0;
+ var start = match && match.index;
+ if (start + matchLen != line.length && !matchLen) matchLen = 1;
+ }
+ if (match && matchLen)
+ return {from: Pos(pos.line, start),
+ to: Pos(pos.line, start + matchLen),
+ match: match};
+ };
+ } else { // String query
+ var origQuery = query;
+ if (caseFold) query = query.toLowerCase();
+ var fold = caseFold ? function(str){return str.toLowerCase();} : function(str){return str;};
+ var target = query.split("\n");
+ // Different methods for single-line and multi-line queries
+ if (target.length == 1) {
+ if (!query.length) {
+ // Empty string would match anything and never progress, so
+ // we define it to match nothing instead.
+ this.matches = function() {};
+ } else {
+ this.matches = function(reverse, pos) {
+ if (reverse) {
+ var orig = doc.getLine(pos.line).slice(0, pos.ch), line = fold(orig);
+ var match = line.lastIndexOf(query);
+ if (match > -1) {
+ match = adjustPos(orig, line, match);
+ return {from: Pos(pos.line, match), to: Pos(pos.line, match + origQuery.length)};
+ }
+ } else {
+ var orig = doc.getLine(pos.line).slice(pos.ch), line = fold(orig);
+ var match = line.indexOf(query);
+ if (match > -1) {
+ match = adjustPos(orig, line, match) + pos.ch;
+ return {from: Pos(pos.line, match), to: Pos(pos.line, match + origQuery.length)};
+ }
+ }
+ };
+ }
+ } else {
+ var origTarget = origQuery.split("\n");
+ this.matches = function(reverse, pos) {
+ var last = target.length - 1;
+ if (reverse) {
+ if (pos.line - (target.length - 1) < doc.firstLine()) return;
+ if (fold(doc.getLine(pos.line).slice(0, origTarget[last].length)) != target[target.length - 1]) return;
+ var to = Pos(pos.line, origTarget[last].length);
+ for (var ln = pos.line - 1, i = last - 1; i >= 1; --i, --ln)
+ if (target[i] != fold(doc.getLine(ln))) return;
+ var line = doc.getLine(ln), cut = line.length - origTarget[0].length;
+ if (fold(line.slice(cut)) != target[0]) return;
+ return {from: Pos(ln, cut), to: to};
+ } else {
+ if (pos.line + (target.length - 1) > doc.lastLine()) return;
+ var line = doc.getLine(pos.line), cut = line.length - origTarget[0].length;
+ if (fold(line.slice(cut)) != target[0]) return;
+ var from = Pos(pos.line, cut);
+ for (var ln = pos.line + 1, i = 1; i < last; ++i, ++ln)
+ if (target[i] != fold(doc.getLine(ln))) return;
+ if (fold(doc.getLine(ln).slice(0, origTarget[last].length)) != target[last]) return;
+ return {from: from, to: Pos(ln, origTarget[last].length)};
+ }
+ };
+ }
+ }
+ }
+
+ SearchCursor.prototype = {
+ findNext: function() {return this.find(false);},
+ findPrevious: function() {return this.find(true);},
+
+ find: function(reverse) {
+ var self = this, pos = this.doc.clipPos(reverse ? this.pos.from : this.pos.to);
+ function savePosAndFail(line) {
+ var pos = Pos(line, 0);
+ self.pos = {from: pos, to: pos};
+ self.atOccurrence = false;
+ return false;
+ }
+
+ for (;;) {
+ if (this.pos = this.matches(reverse, pos)) {
+ this.atOccurrence = true;
+ return this.pos.match || true;
+ }
+ if (reverse) {
+ if (!pos.line) return savePosAndFail(0);
+ pos = Pos(pos.line-1, this.doc.getLine(pos.line-1).length);
+ }
+ else {
+ var maxLine = this.doc.lineCount();
+ if (pos.line == maxLine - 1) return savePosAndFail(maxLine);
+ pos = Pos(pos.line + 1, 0);
+ }
+ }
+ },
+
+ from: function() {if (this.atOccurrence) return this.pos.from;},
+ to: function() {if (this.atOccurrence) return this.pos.to;},
+
+ replace: function(newText, origin) {
+ if (!this.atOccurrence) return;
+ var lines = CodeMirror.splitLines(newText);
+ this.doc.replaceRange(lines, this.pos.from, this.pos.to, origin);
+ this.pos.to = Pos(this.pos.from.line + lines.length - 1,
+ lines[lines.length - 1].length + (lines.length == 1 ? this.pos.from.ch : 0));
+ }
+ };
+
+ // Maps a position in a case-folded line back to a position in the original line
+ // (compensating for codepoints increasing in number during folding)
+ function adjustPos(orig, folded, pos) {
+ if (orig.length == folded.length) return pos;
+ for (var pos1 = Math.min(pos, orig.length);;) {
+ var len1 = orig.slice(0, pos1).toLowerCase().length;
+ if (len1 < pos) ++pos1;
+ else if (len1 > pos) --pos1;
+ else return pos1;
+ }
+ }
+
+ CodeMirror.defineExtension("getSearchCursor", function(query, pos, caseFold) {
+ return new SearchCursor(this.doc, query, pos, caseFold);
+ });
+ CodeMirror.defineDocExtension("getSearchCursor", function(query, pos, caseFold) {
+ return new SearchCursor(this, query, pos, caseFold);
+ });
+
+ CodeMirror.defineExtension("selectMatches", function(query, caseFold) {
+ var ranges = [];
+ var cur = this.getSearchCursor(query, this.getCursor("from"), caseFold);
+ while (cur.findNext()) {
+ if (CodeMirror.cmpPos(cur.to(), this.getCursor("to")) > 0) break;
+ ranges.push({anchor: cur.from(), head: cur.to()});
+ }
+ if (ranges.length)
+ this.setSelections(ranges, 0);
+ });
+ });
+
+
+/***/ },
+/* 4 */
+/***/ function(module, exports, __webpack_require__) {
+
+ // CodeMirror, copyright (c) by Marijn Haverbeke and others
+ // Distributed under an MIT license: http://codemirror.net/LICENSE
+
+ // Define search commands. Depends on dialog.js or another
+ // implementation of the openDialog method.
+
+ // Replace works a little oddly -- it will do the replace on the next
+ // Ctrl-G (or whatever is bound to findNext) press. You prevent a
+ // replace by making sure the match is no longer selected when hitting
+ // Ctrl-G.
+
+ (function(mod) {
+ if (true) // CommonJS
+ mod(__webpack_require__(2), __webpack_require__(3), __webpack_require__(1));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror", "./searchcursor", "../dialog/dialog"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+ })(function(CodeMirror) {
+ "use strict";
+
+ function searchOverlay(query, caseInsensitive) {
+ if (typeof query == "string")
+ query = new RegExp(query.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"), caseInsensitive ? "gi" : "g");
+ else if (!query.global)
+ query = new RegExp(query.source, query.ignoreCase ? "gi" : "g");
+
+ return {token: function(stream) {
+ query.lastIndex = stream.pos;
+ var match = query.exec(stream.string);
+ if (match && match.index == stream.pos) {
+ stream.pos += match[0].length || 1;
+ return "searching";
+ } else if (match) {
+ stream.pos = match.index;
+ } else {
+ stream.skipToEnd();
+ }
+ }};
+ }
+
+ function SearchState() {
+ this.posFrom = this.posTo = this.lastQuery = this.query = null;
+ this.overlay = null;
+ }
+
+ function getSearchState(cm) {
+ return cm.state.search || (cm.state.search = new SearchState());
+ }
+
+ function queryCaseInsensitive(query) {
+ return typeof query == "string" && query == query.toLowerCase();
+ }
+
+ function getSearchCursor(cm, query, pos) {
+ // Heuristic: if the query string is all lowercase, do a case insensitive search.
+ return cm.getSearchCursor(query, pos, queryCaseInsensitive(query));
+ }
+
+ function persistentDialog(cm, text, deflt, f) {
+ cm.openDialog(text, f, {
+ value: deflt,
+ selectValueOnOpen: true,
+ closeOnEnter: false,
+ onClose: function() { clearSearch(cm); }
+ });
+ }
+
+ function dialog(cm, text, shortText, deflt, f) {
+ if (cm.openDialog) cm.openDialog(text, f, {value: deflt, selectValueOnOpen: true});
+ else f(prompt(shortText, deflt));
+ }
+
+ function confirmDialog(cm, text, shortText, fs) {
+ if (cm.openConfirm) cm.openConfirm(text, fs);
+ else if (confirm(shortText)) fs[0]();
+ }
+
+ function parseString(string) {
+ return string.replace(/\\(.)/g, function(_, ch) {
+ if (ch == "n") return "\n"
+ if (ch == "r") return "\r"
+ return ch
+ })
+ }
+
+ function parseQuery(query) {
+ var isRE = query.match(/^\/(.*)\/([a-z]*)$/);
+ if (isRE) {
+ try { query = new RegExp(isRE[1], isRE[2].indexOf("i") == -1 ? "" : "i"); }
+ catch(e) {} // Not a regular expression after all, do a string search
+ } else {
+ query = parseString(query)
+ }
+ if (typeof query == "string" ? query == "" : query.test(""))
+ query = /x^/;
+ return query;
+ }
+
+ var queryDialog;
+
+ function startSearch(cm, state, query) {
+ state.queryText = query;
+ state.query = parseQuery(query);
+ cm.removeOverlay(state.overlay, queryCaseInsensitive(state.query));
+ state.overlay = searchOverlay(state.query, queryCaseInsensitive(state.query));
+ cm.addOverlay(state.overlay);
+ if (cm.showMatchesOnScrollbar) {
+ if (state.annotate) { state.annotate.clear(); state.annotate = null; }
+ state.annotate = cm.showMatchesOnScrollbar(state.query, queryCaseInsensitive(state.query));
+ }
+ }
+
+ function doSearch(cm, rev, persistent) {
+ if (!queryDialog) {
+ let doc = cm.getWrapperElement().ownerDocument;
+ let inp = doc.createElement("input");
+
+ inp.type = "search";
+ inp.placeholder = cm.l10n("findCmd.promptMessage");
+ inp.style.marginInlineStart = "1em";
+ inp.style.marginInlineEnd = "1em";
+ inp.style.flexGrow = "1";
+ inp.addEventListener("focus", () => inp.select());
+
+ queryDialog = doc.createElement("div");
+ queryDialog.appendChild(inp);
+ queryDialog.style.display = "flex";
+ }
+
+ var state = getSearchState(cm);
+ if (state.query) return findNext(cm, rev);
+ var q = cm.getSelection() || state.lastQuery;
+ if (persistent && cm.openDialog) {
+ var hiding = null
+ persistentDialog(cm, queryDialog, q, function(query, event) {
+ CodeMirror.e_stop(event);
+ if (!query) return;
+ if (query != state.queryText) {
+ startSearch(cm, state, query);
+ state.posFrom = state.posTo = cm.getCursor();
+ }
+ if (hiding) hiding.style.opacity = 1
+ findNext(cm, event.shiftKey, function(_, to) {
+ var dialog
+ if (to.line < 3 && document.querySelector &&
+ (dialog = cm.display.wrapper.querySelector(".CodeMirror-dialog")) &&
+ dialog.getBoundingClientRect().bottom - 4 > cm.cursorCoords(to, "window").top)
+ (hiding = dialog).style.opacity = .4
+ })
+ });
+ } else {
+ dialog(cm, queryDialog, "Search for:", q, function(query) {
+ if (query && !state.query) cm.operation(function() {
+ startSearch(cm, state, query);
+ state.posFrom = state.posTo = cm.getCursor();
+ findNext(cm, rev);
+ });
+ });
+ }
+ }
+
+ function findNext(cm, rev, callback) {cm.operation(function() {
+ var state = getSearchState(cm);
+ var cursor = getSearchCursor(cm, state.query, rev ? state.posFrom : state.posTo);
+ if (!cursor.find(rev)) {
+ cursor = getSearchCursor(cm, state.query, rev ? CodeMirror.Pos(cm.lastLine()) : CodeMirror.Pos(cm.firstLine(), 0));
+ if (!cursor.find(rev)) return;
+ }
+ cm.setSelection(cursor.from(), cursor.to());
+ cm.scrollIntoView({from: cursor.from(), to: cursor.to()}, 20);
+ state.posFrom = cursor.from(); state.posTo = cursor.to();
+ if (callback) callback(cursor.from(), cursor.to())
+ });}
+
+ function clearSearch(cm) {cm.operation(function() {
+ var state = getSearchState(cm);
+ state.lastQuery = state.query;
+ if (!state.query) return;
+ state.query = state.queryText = null;
+ cm.removeOverlay(state.overlay);
+ if (state.annotate) { state.annotate.clear(); state.annotate = null; }
+ });}
+
+ var replaceQueryDialog =
+ ' <input type="text" style="width: 10em" class="CodeMirror-search-field"/> <span style="color: #888" class="CodeMirror-search-hint">(Use /re/ syntax for regexp search)</span>';
+ var replacementQueryDialog = 'With: <input type="text" style="width: 10em" class="CodeMirror-search-field"/>';
+ var doReplaceConfirm = "Replace? <button>Yes</button> <button>No</button> <button>All</button> <button>Stop</button>";
+
+ function replaceAll(cm, query, text) {
+ cm.operation(function() {
+ for (var cursor = getSearchCursor(cm, query); cursor.findNext();) {
+ if (typeof query != "string") {
+ var match = cm.getRange(cursor.from(), cursor.to()).match(query);
+ cursor.replace(text.replace(/\$(\d)/g, function(_, i) {return match[i];}));
+ } else cursor.replace(text);
+ }
+ });
+ }
+
+ function replace(cm, all) {
+ if (cm.getOption("readOnly")) return;
+ var query = cm.getSelection() || getSearchState(cm).lastQuery;
+ var dialogText = all ? "Replace all:" : "Replace:"
+ dialog(cm, dialogText + replaceQueryDialog, dialogText, query, function(query) {
+ if (!query) return;
+ query = parseQuery(query);
+ dialog(cm, replacementQueryDialog, "Replace with:", "", function(text) {
+ text = parseString(text)
+ if (all) {
+ replaceAll(cm, query, text)
+ } else {
+ clearSearch(cm);
+ var cursor = getSearchCursor(cm, query, cm.getCursor("from"));
+ var advance = function() {
+ var start = cursor.from(), match;
+ if (!(match = cursor.findNext())) {
+ cursor = getSearchCursor(cm, query);
+ if (!(match = cursor.findNext()) ||
+ (start && cursor.from().line == start.line && cursor.from().ch == start.ch)) return;
+ }
+ cm.setSelection(cursor.from(), cursor.to());
+ cm.scrollIntoView({from: cursor.from(), to: cursor.to()});
+ confirmDialog(cm, doReplaceConfirm, "Replace?",
+ [function() {doReplace(match);}, advance,
+ function() {replaceAll(cm, query, text)}]);
+ };
+ var doReplace = function(match) {
+ cursor.replace(typeof query == "string" ? text :
+ text.replace(/\$(\d)/g, function(_, i) {return match[i];}));
+ advance();
+ };
+ advance();
+ }
+ });
+ });
+ }
+
+ CodeMirror.commands.find = function(cm) {clearSearch(cm); doSearch(cm);};
+ CodeMirror.commands.findPersistent = function(cm) {clearSearch(cm); doSearch(cm, false, true);};
+ CodeMirror.commands.findNext = doSearch;
+ CodeMirror.commands.findPrev = function(cm) {doSearch(cm, true);};
+ CodeMirror.commands.clearSearch = clearSearch;
+ CodeMirror.commands.replace = replace;
+ CodeMirror.commands.replaceAll = function(cm) {replace(cm, true);};
+ });
+
+
+/***/ },
+/* 5 */
+/***/ function(module, exports, __webpack_require__) {
+
+ // CodeMirror, copyright (c) by Marijn Haverbeke and others
+ // Distributed under an MIT license: http://codemirror.net/LICENSE
+
+ (function(mod) {
+ if (true) // CommonJS
+ mod(__webpack_require__(2));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+ })(function(CodeMirror) {
+ var ie_lt8 = /MSIE \d/.test(navigator.userAgent) &&
+ (document.documentMode == null || document.documentMode < 8);
+
+ var Pos = CodeMirror.Pos;
+
+ var matching = {"(": ")>", ")": "(<", "[": "]>", "]": "[<", "{": "}>", "}": "{<"};
+
+ function findMatchingBracket(cm, where, strict, config) {
+ var line = cm.getLineHandle(where.line), pos = where.ch - 1;
+ var match = (pos >= 0 && matching[line.text.charAt(pos)]) || matching[line.text.charAt(++pos)];
+ if (!match) return null;
+ var dir = match.charAt(1) == ">" ? 1 : -1;
+ if (strict && (dir > 0) != (pos == where.ch)) return null;
+ var style = cm.getTokenTypeAt(Pos(where.line, pos + 1));
+
+ var found = scanForBracket(cm, Pos(where.line, pos + (dir > 0 ? 1 : 0)), dir, style || null, config);
+ if (found == null) return null;
+ return {from: Pos(where.line, pos), to: found && found.pos,
+ match: found && found.ch == match.charAt(0), forward: dir > 0};
+ }
+
+ // bracketRegex is used to specify which type of bracket to scan
+ // should be a regexp, e.g. /[[\]]/
+ //
+ // Note: If "where" is on an open bracket, then this bracket is ignored.
+ //
+ // Returns false when no bracket was found, null when it reached
+ // maxScanLines and gave up
+ function scanForBracket(cm, where, dir, style, config) {
+ var maxScanLen = (config && config.maxScanLineLength) || 10000;
+ var maxScanLines = (config && config.maxScanLines) || 1000;
+
+ var stack = [];
+ var re = config && config.bracketRegex ? config.bracketRegex : /[(){}[\]]/;
+ var lineEnd = dir > 0 ? Math.min(where.line + maxScanLines, cm.lastLine() + 1)
+ : Math.max(cm.firstLine() - 1, where.line - maxScanLines);
+ for (var lineNo = where.line; lineNo != lineEnd; lineNo += dir) {
+ var line = cm.getLine(lineNo);
+ if (!line) continue;
+ var pos = dir > 0 ? 0 : line.length - 1, end = dir > 0 ? line.length : -1;
+ if (line.length > maxScanLen) continue;
+ if (lineNo == where.line) pos = where.ch - (dir < 0 ? 1 : 0);
+ for (; pos != end; pos += dir) {
+ var ch = line.charAt(pos);
+ if (re.test(ch) && (style === undefined || cm.getTokenTypeAt(Pos(lineNo, pos + 1)) == style)) {
+ var match = matching[ch];
+ if ((match.charAt(1) == ">") == (dir > 0)) stack.push(ch);
+ else if (!stack.length) return {pos: Pos(lineNo, pos), ch: ch};
+ else stack.pop();
+ }
+ }
+ }
+ return lineNo - dir == (dir > 0 ? cm.lastLine() : cm.firstLine()) ? false : null;
+ }
+
+ function matchBrackets(cm, autoclear, config) {
+ // Disable brace matching in long lines, since it'll cause hugely slow updates
+ var maxHighlightLen = cm.state.matchBrackets.maxHighlightLineLength || 1000;
+ var marks = [], ranges = cm.listSelections();
+ for (var i = 0; i < ranges.length; i++) {
+ var match = ranges[i].empty() && findMatchingBracket(cm, ranges[i].head, false, config);
+ if (match && cm.getLine(match.from.line).length <= maxHighlightLen) {
+ var style = match.match ? "CodeMirror-matchingbracket" : "CodeMirror-nonmatchingbracket";
+ marks.push(cm.markText(match.from, Pos(match.from.line, match.from.ch + 1), {className: style}));
+ if (match.to && cm.getLine(match.to.line).length <= maxHighlightLen)
+ marks.push(cm.markText(match.to, Pos(match.to.line, match.to.ch + 1), {className: style}));
+ }
+ }
+
+ if (marks.length) {
+ // Kludge to work around the IE bug from issue #1193, where text
+ // input stops going to the textare whever this fires.
+ if (ie_lt8 && cm.state.focused) cm.focus();
+
+ var clear = function() {
+ cm.operation(function() {
+ for (var i = 0; i < marks.length; i++) marks[i].clear();
+ });
+ };
+ if (autoclear) setTimeout(clear, 800);
+ else return clear;
+ }
+ }
+
+ var currentlyHighlighted = null;
+ function doMatchBrackets(cm) {
+ cm.operation(function() {
+ if (currentlyHighlighted) {currentlyHighlighted(); currentlyHighlighted = null;}
+ currentlyHighlighted = matchBrackets(cm, false, cm.state.matchBrackets);
+ });
+ }
+
+ CodeMirror.defineOption("matchBrackets", false, function(cm, val, old) {
+ if (old && old != CodeMirror.Init)
+ cm.off("cursorActivity", doMatchBrackets);
+ if (val) {
+ cm.state.matchBrackets = typeof val == "object" ? val : {};
+ cm.on("cursorActivity", doMatchBrackets);
+ }
+ });
+
+ CodeMirror.defineExtension("matchBrackets", function() {matchBrackets(this, true);});
+ CodeMirror.defineExtension("findMatchingBracket", function(pos, strict, config){
+ return findMatchingBracket(this, pos, strict, config);
+ });
+ CodeMirror.defineExtension("scanForBracket", function(pos, dir, style, config){
+ return scanForBracket(this, pos, dir, style, config);
+ });
+ });
+
+
+/***/ },
+/* 6 */
+/***/ function(module, exports, __webpack_require__) {
+
+ // CodeMirror, copyright (c) by Marijn Haverbeke and others
+ // Distributed under an MIT license: http://codemirror.net/LICENSE
+
+ (function(mod) {
+ if (true) // CommonJS
+ mod(__webpack_require__(2));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+ })(function(CodeMirror) {
+ var defaults = {
+ pairs: "()[]{}''\"\"",
+ triples: "",
+ explode: "[]{}"
+ };
+
+ var Pos = CodeMirror.Pos;
+
+ CodeMirror.defineOption("autoCloseBrackets", false, function(cm, val, old) {
+ if (old && old != CodeMirror.Init) {
+ cm.removeKeyMap(keyMap);
+ cm.state.closeBrackets = null;
+ }
+ if (val) {
+ cm.state.closeBrackets = val;
+ cm.addKeyMap(keyMap);
+ }
+ });
+
+ function getOption(conf, name) {
+ if (name == "pairs" && typeof conf == "string") return conf;
+ if (typeof conf == "object" && conf[name] != null) return conf[name];
+ return defaults[name];
+ }
+
+ var bind = defaults.pairs + "`";
+ var keyMap = {Backspace: handleBackspace, Enter: handleEnter};
+ for (var i = 0; i < bind.length; i++)
+ keyMap["'" + bind.charAt(i) + "'"] = handler(bind.charAt(i));
+
+ function handler(ch) {
+ return function(cm) { return handleChar(cm, ch); };
+ }
+
+ function getConfig(cm) {
+ var deflt = cm.state.closeBrackets;
+ if (!deflt) return null;
+ var mode = cm.getModeAt(cm.getCursor());
+ return mode.closeBrackets || deflt;
+ }
+
+ function handleBackspace(cm) {
+ var conf = getConfig(cm);
+ if (!conf || cm.getOption("disableInput")) return CodeMirror.Pass;
+
+ var pairs = getOption(conf, "pairs");
+ var ranges = cm.listSelections();
+ for (var i = 0; i < ranges.length; i++) {
+ if (!ranges[i].empty()) return CodeMirror.Pass;
+ var around = charsAround(cm, ranges[i].head);
+ if (!around || pairs.indexOf(around) % 2 != 0) return CodeMirror.Pass;
+ }
+ for (var i = ranges.length - 1; i >= 0; i--) {
+ var cur = ranges[i].head;
+ cm.replaceRange("", Pos(cur.line, cur.ch - 1), Pos(cur.line, cur.ch + 1), "+delete");
+ }
+ }
+
+ function handleEnter(cm) {
+ var conf = getConfig(cm);
+ var explode = conf && getOption(conf, "explode");
+ if (!explode || cm.getOption("disableInput")) return CodeMirror.Pass;
+
+ var ranges = cm.listSelections();
+ for (var i = 0; i < ranges.length; i++) {
+ if (!ranges[i].empty()) return CodeMirror.Pass;
+ var around = charsAround(cm, ranges[i].head);
+ if (!around || explode.indexOf(around) % 2 != 0) return CodeMirror.Pass;
+ }
+ cm.operation(function() {
+ cm.replaceSelection("\n\n", null);
+ cm.execCommand("goCharLeft");
+ ranges = cm.listSelections();
+ for (var i = 0; i < ranges.length; i++) {
+ var line = ranges[i].head.line;
+ cm.indentLine(line, null, true);
+ cm.indentLine(line + 1, null, true);
+ }
+ });
+ }
+
+ function contractSelection(sel) {
+ var inverted = CodeMirror.cmpPos(sel.anchor, sel.head) > 0;
+ return {anchor: new Pos(sel.anchor.line, sel.anchor.ch + (inverted ? -1 : 1)),
+ head: new Pos(sel.head.line, sel.head.ch + (inverted ? 1 : -1))};
+ }
+
+ function handleChar(cm, ch) {
+ var conf = getConfig(cm);
+ if (!conf || cm.getOption("disableInput")) return CodeMirror.Pass;
+
+ var pairs = getOption(conf, "pairs");
+ var pos = pairs.indexOf(ch);
+ if (pos == -1) return CodeMirror.Pass;
+ var triples = getOption(conf, "triples");
+
+ var identical = pairs.charAt(pos + 1) == ch;
+ var ranges = cm.listSelections();
+ var opening = pos % 2 == 0;
+
+ var type;
+ for (var i = 0; i < ranges.length; i++) {
+ var range = ranges[i], cur = range.head, curType;
+ var next = cm.getRange(cur, Pos(cur.line, cur.ch + 1));
+ if (opening && !range.empty()) {
+ curType = "surround";
+ } else if ((identical || !opening) && next == ch) {
+ if (triples.indexOf(ch) >= 0 && cm.getRange(cur, Pos(cur.line, cur.ch + 3)) == ch + ch + ch)
+ curType = "skipThree";
+ else
+ curType = "skip";
+ } else if (identical && cur.ch > 1 && triples.indexOf(ch) >= 0 &&
+ cm.getRange(Pos(cur.line, cur.ch - 2), cur) == ch + ch &&
+ (cur.ch <= 2 || cm.getRange(Pos(cur.line, cur.ch - 3), Pos(cur.line, cur.ch - 2)) != ch)) {
+ curType = "addFour";
+ } else if (identical) {
+ if (!CodeMirror.isWordChar(next) && enteringString(cm, cur, ch)) curType = "both";
+ else return CodeMirror.Pass;
+ } else if (opening && (cm.getLine(cur.line).length == cur.ch ||
+ isClosingBracket(next, pairs) ||
+ /\s/.test(next))) {
+ curType = "both";
+ } else {
+ return CodeMirror.Pass;
+ }
+ if (!type) type = curType;
+ else if (type != curType) return CodeMirror.Pass;
+ }
+
+ var left = pos % 2 ? pairs.charAt(pos - 1) : ch;
+ var right = pos % 2 ? ch : pairs.charAt(pos + 1);
+ cm.operation(function() {
+ if (type == "skip") {
+ cm.execCommand("goCharRight");
+ } else if (type == "skipThree") {
+ for (var i = 0; i < 3; i++)
+ cm.execCommand("goCharRight");
+ } else if (type == "surround") {
+ var sels = cm.getSelections();
+ for (var i = 0; i < sels.length; i++)
+ sels[i] = left + sels[i] + right;
+ cm.replaceSelections(sels, "around");
+ sels = cm.listSelections().slice();
+ for (var i = 0; i < sels.length; i++)
+ sels[i] = contractSelection(sels[i]);
+ cm.setSelections(sels);
+ } else if (type == "both") {
+ cm.replaceSelection(left + right, null);
+ cm.triggerElectric(left + right);
+ cm.execCommand("goCharLeft");
+ } else if (type == "addFour") {
+ cm.replaceSelection(left + left + left + left, "before");
+ cm.execCommand("goCharRight");
+ }
+ });
+ }
+
+ function isClosingBracket(ch, pairs) {
+ var pos = pairs.lastIndexOf(ch);
+ return pos > -1 && pos % 2 == 1;
+ }
+
+ function charsAround(cm, pos) {
+ var str = cm.getRange(Pos(pos.line, pos.ch - 1),
+ Pos(pos.line, pos.ch + 1));
+ return str.length == 2 ? str : null;
+ }
+
+ // Project the token type that will exists after the given char is
+ // typed, and use it to determine whether it would cause the start
+ // of a string token.
+ function enteringString(cm, pos, ch) {
+ var line = cm.getLine(pos.line);
+ var token = cm.getTokenAt(pos);
+ if (/\bstring2?\b/.test(token.type)) return false;
+ var stream = new CodeMirror.StringStream(line.slice(0, pos.ch) + ch + line.slice(pos.ch), 4);
+ stream.pos = stream.start = token.start;
+ for (;;) {
+ var type1 = cm.getMode().token(stream, token.state);
+ if (stream.pos >= pos.ch + 1) return /\bstring2?\b/.test(type1);
+ stream.start = stream.pos;
+ }
+ }
+ });
+
+
+/***/ },
+/* 7 */
+/***/ function(module, exports, __webpack_require__) {
+
+ // CodeMirror, copyright (c) by Marijn Haverbeke and others
+ // Distributed under an MIT license: http://codemirror.net/LICENSE
+
+ (function(mod) {
+ if (true) // CommonJS
+ mod(__webpack_require__(2));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+ })(function(CodeMirror) {
+ "use strict";
+
+ var noOptions = {};
+ var nonWS = /[^\s\u00a0]/;
+ var Pos = CodeMirror.Pos;
+
+ function firstNonWS(str) {
+ var found = str.search(nonWS);
+ return found == -1 ? 0 : found;
+ }
+
+ CodeMirror.commands.toggleComment = function(cm) {
+ cm.toggleComment();
+ };
+
+ CodeMirror.defineExtension("toggleComment", function(options) {
+ if (!options) options = noOptions;
+ var cm = this;
+ var minLine = Infinity, ranges = this.listSelections(), mode = null;
+ for (var i = ranges.length - 1; i >= 0; i--) {
+ var from = ranges[i].from(), to = ranges[i].to();
+ if (from.line >= minLine) continue;
+ if (to.line >= minLine) to = Pos(minLine, 0);
+ minLine = from.line;
+ if (mode == null) {
+ if (cm.uncomment(from, to, options)) mode = "un";
+ else { cm.lineComment(from, to, options); mode = "line"; }
+ } else if (mode == "un") {
+ cm.uncomment(from, to, options);
+ } else {
+ cm.lineComment(from, to, options);
+ }
+ }
+ });
+
+ // Rough heuristic to try and detect lines that are part of multi-line string
+ function probablyInsideString(cm, pos, line) {
+ return /\bstring\b/.test(cm.getTokenTypeAt(Pos(pos.line, 0))) && !/^[\'\"`]/.test(line)
+ }
+
+ CodeMirror.defineExtension("lineComment", function(from, to, options) {
+ if (!options) options = noOptions;
+ var self = this, mode = self.getModeAt(from);
+ var firstLine = self.getLine(from.line);
+ if (firstLine == null || probablyInsideString(self, from, firstLine)) return;
+
+ var commentString = options.lineComment || mode.lineComment;
+ if (!commentString) {
+ if (options.blockCommentStart || mode.blockCommentStart) {
+ options.fullLines = true;
+ self.blockComment(from, to, options);
+ }
+ return;
+ }
+
+ var end = Math.min(to.ch != 0 || to.line == from.line ? to.line + 1 : to.line, self.lastLine() + 1);
+ var pad = options.padding == null ? " " : options.padding;
+ var blankLines = options.commentBlankLines || from.line == to.line;
+
+ self.operation(function() {
+ if (options.indent) {
+ var baseString = null;
+ for (var i = from.line; i < end; ++i) {
+ var line = self.getLine(i);
+ var whitespace = line.slice(0, firstNonWS(line));
+ if (baseString == null || baseString.length > whitespace.length) {
+ baseString = whitespace;
+ }
+ }
+ for (var i = from.line; i < end; ++i) {
+ var line = self.getLine(i), cut = baseString.length;
+ if (!blankLines && !nonWS.test(line)) continue;
+ if (line.slice(0, cut) != baseString) cut = firstNonWS(line);
+ self.replaceRange(baseString + commentString + pad, Pos(i, 0), Pos(i, cut));
+ }
+ } else {
+ for (var i = from.line; i < end; ++i) {
+ if (blankLines || nonWS.test(self.getLine(i)))
+ self.replaceRange(commentString + pad, Pos(i, 0));
+ }
+ }
+ });
+ });
+
+ CodeMirror.defineExtension("blockComment", function(from, to, options) {
+ if (!options) options = noOptions;
+ var self = this, mode = self.getModeAt(from);
+ var startString = options.blockCommentStart || mode.blockCommentStart;
+ var endString = options.blockCommentEnd || mode.blockCommentEnd;
+ if (!startString || !endString) {
+ if ((options.lineComment || mode.lineComment) && options.fullLines != false)
+ self.lineComment(from, to, options);
+ return;
+ }
+
+ var end = Math.min(to.line, self.lastLine());
+ if (end != from.line && to.ch == 0 && nonWS.test(self.getLine(end))) --end;
+
+ var pad = options.padding == null ? " " : options.padding;
+ if (from.line > end) return;
+
+ self.operation(function() {
+ if (options.fullLines != false) {
+ var lastLineHasText = nonWS.test(self.getLine(end));
+ self.replaceRange(pad + endString, Pos(end));
+ self.replaceRange(startString + pad, Pos(from.line, 0));
+ var lead = options.blockCommentLead || mode.blockCommentLead;
+ if (lead != null) for (var i = from.line + 1; i <= end; ++i)
+ if (i != end || lastLineHasText)
+ self.replaceRange(lead + pad, Pos(i, 0));
+ } else {
+ self.replaceRange(endString, to);
+ self.replaceRange(startString, from);
+ }
+ });
+ });
+
+ CodeMirror.defineExtension("uncomment", function(from, to, options) {
+ if (!options) options = noOptions;
+ var self = this, mode = self.getModeAt(from);
+ var end = Math.min(to.ch != 0 || to.line == from.line ? to.line : to.line - 1, self.lastLine()), start = Math.min(from.line, end);
+
+ // Try finding line comments
+ var lineString = options.lineComment || mode.lineComment, lines = [];
+ var pad = options.padding == null ? " " : options.padding, didSomething;
+ lineComment: {
+ if (!lineString) break lineComment;
+ for (var i = start; i <= end; ++i) {
+ var line = self.getLine(i);
+ var found = line.indexOf(lineString);
+ if (found > -1 && !/comment/.test(self.getTokenTypeAt(Pos(i, found + 1)))) found = -1;
+ if (found == -1 && (i != end || i == start) && nonWS.test(line)) break lineComment;
+ if (found > -1 && nonWS.test(line.slice(0, found))) break lineComment;
+ lines.push(line);
+ }
+ self.operation(function() {
+ for (var i = start; i <= end; ++i) {
+ var line = lines[i - start];
+ var pos = line.indexOf(lineString), endPos = pos + lineString.length;
+ if (pos < 0) continue;
+ if (line.slice(endPos, endPos + pad.length) == pad) endPos += pad.length;
+ didSomething = true;
+ self.replaceRange("", Pos(i, pos), Pos(i, endPos));
+ }
+ });
+ if (didSomething) return true;
+ }
+
+ // Try block comments
+ var startString = options.blockCommentStart || mode.blockCommentStart;
+ var endString = options.blockCommentEnd || mode.blockCommentEnd;
+ if (!startString || !endString) return false;
+ var lead = options.blockCommentLead || mode.blockCommentLead;
+ var startLine = self.getLine(start), endLine = end == start ? startLine : self.getLine(end);
+ var open = startLine.indexOf(startString), close = endLine.lastIndexOf(endString);
+ if (close == -1 && start != end) {
+ endLine = self.getLine(--end);
+ close = endLine.lastIndexOf(endString);
+ }
+ if (open == -1 || close == -1 ||
+ !/comment/.test(self.getTokenTypeAt(Pos(start, open + 1))) ||
+ !/comment/.test(self.getTokenTypeAt(Pos(end, close + 1))))
+ return false;
+
+ // Avoid killing block comments completely outside the selection.
+ // Positions of the last startString before the start of the selection, and the first endString after it.
+ var lastStart = startLine.lastIndexOf(startString, from.ch);
+ var firstEnd = lastStart == -1 ? -1 : startLine.slice(0, from.ch).indexOf(endString, lastStart + startString.length);
+ if (lastStart != -1 && firstEnd != -1 && firstEnd + endString.length != from.ch) return false;
+ // Positions of the first endString after the end of the selection, and the last startString before it.
+ firstEnd = endLine.indexOf(endString, to.ch);
+ var almostLastStart = endLine.slice(to.ch).lastIndexOf(startString, firstEnd - to.ch);
+ lastStart = (firstEnd == -1 || almostLastStart == -1) ? -1 : to.ch + almostLastStart;
+ if (firstEnd != -1 && lastStart != -1 && lastStart != to.ch) return false;
+
+ self.operation(function() {
+ self.replaceRange("", Pos(end, close - (pad && endLine.slice(close - pad.length, close) == pad ? pad.length : 0)),
+ Pos(end, close + endString.length));
+ var openEnd = open + startString.length;
+ if (pad && startLine.slice(openEnd, openEnd + pad.length) == pad) openEnd += pad.length;
+ self.replaceRange("", Pos(start, open), Pos(start, openEnd));
+ if (lead) for (var i = start + 1; i <= end; ++i) {
+ var line = self.getLine(i), found = line.indexOf(lead);
+ if (found == -1 || nonWS.test(line.slice(0, found))) continue;
+ var foundEnd = found + lead.length;
+ if (pad && line.slice(foundEnd, foundEnd + pad.length) == pad) foundEnd += pad.length;
+ self.replaceRange("", Pos(i, found), Pos(i, foundEnd));
+ }
+ });
+ return true;
+ });
+ });
+
+
+/***/ },
+/* 8 */
+/***/ function(module, exports, __webpack_require__) {
+
+ // CodeMirror, copyright (c) by Marijn Haverbeke and others
+ // Distributed under an MIT license: http://codemirror.net/LICENSE
+
+ // TODO actually recognize syntax of TypeScript constructs
+
+ (function(mod) {
+ if (true) // CommonJS
+ mod(__webpack_require__(2));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+ })(function(CodeMirror) {
+ "use strict";
+
+ function expressionAllowed(stream, state, backUp) {
+ return /^(?:operator|sof|keyword c|case|new|[\[{}\(,;:]|=>)$/.test(state.lastType) ||
+ (state.lastType == "quasi" && /\{\s*$/.test(stream.string.slice(0, stream.pos - (backUp || 0))))
+ }
+
+ CodeMirror.defineMode("javascript", function(config, parserConfig) {
+ var indentUnit = config.indentUnit;
+ var statementIndent = parserConfig.statementIndent;
+ var jsonldMode = parserConfig.jsonld;
+ var jsonMode = parserConfig.json || jsonldMode;
+ var isTS = parserConfig.typescript;
+ var wordRE = parserConfig.wordCharacters || /[\w$\xa1-\uffff]/;
+
+ // Tokenizer
+
+ var keywords = function(){
+ function kw(type) {return {type: type, style: "keyword"};}
+ var A = kw("keyword a"), B = kw("keyword b"), C = kw("keyword c");
+ var operator = kw("operator"), atom = {type: "atom", style: "atom"};
+
+ var jsKeywords = {
+ "if": kw("if"), "while": A, "with": A, "else": B, "do": B, "try": B, "finally": B,
+ "return": C, "break": C, "continue": C, "new": kw("new"), "delete": C, "throw": C, "debugger": C,
+ "var": kw("var"), "const": kw("var"), "let": kw("var"),
+ "function": kw("function"), "catch": kw("catch"),
+ "for": kw("for"), "switch": kw("switch"), "case": kw("case"), "default": kw("default"),
+ "in": operator, "typeof": operator, "instanceof": operator,
+ "true": atom, "false": atom, "null": atom, "undefined": atom, "NaN": atom, "Infinity": atom,
+ "this": kw("this"), "class": kw("class"), "super": kw("atom"),
+ "yield": C, "export": kw("export"), "import": kw("import"), "extends": C,
+ "await": C, "async": kw("async")
+ };
+
+ // Extend the 'normal' keywords with the TypeScript language extensions
+ if (isTS) {
+ var type = {type: "variable", style: "variable-3"};
+ var tsKeywords = {
+ // object-like things
+ "interface": kw("class"),
+ "implements": C,
+ "namespace": C,
+ "module": kw("module"),
+ "enum": kw("module"),
+
+ // scope modifiers
+ "public": kw("modifier"),
+ "private": kw("modifier"),
+ "protected": kw("modifier"),
+ "abstract": kw("modifier"),
+
+ // operators
+ "as": operator,
+
+ // types
+ "string": type, "number": type, "boolean": type, "any": type
+ };
+
+ for (var attr in tsKeywords) {
+ jsKeywords[attr] = tsKeywords[attr];
+ }
+ }
+
+ return jsKeywords;
+ }();
+
+ var isOperatorChar = /[+\-*&%=<>!?|~^]/;
+ var isJsonldKeyword = /^@(context|id|value|language|type|container|list|set|reverse|index|base|vocab|graph)"/;
+
+ function readRegexp(stream) {
+ var escaped = false, next, inSet = false;
+ while ((next = stream.next()) != null) {
+ if (!escaped) {
+ if (next == "/" && !inSet) return;
+ if (next == "[") inSet = true;
+ else if (inSet && next == "]") inSet = false;
+ }
+ escaped = !escaped && next == "\\";
+ }
+ }
+
+ // Used as scratch variables to communicate multiple values without
+ // consing up tons of objects.
+ var type, content;
+ function ret(tp, style, cont) {
+ type = tp; content = cont;
+ return style;
+ }
+ function tokenBase(stream, state) {
+ var ch = stream.next();
+ if (ch == '"' || ch == "'") {
+ state.tokenize = tokenString(ch);
+ return state.tokenize(stream, state);
+ } else if (ch == "." && stream.match(/^\d+(?:[eE][+\-]?\d+)?/)) {
+ return ret("number", "number");
+ } else if (ch == "." && stream.match("..")) {
+ return ret("spread", "meta");
+ } else if (/[\[\]{}\(\),;\:\.]/.test(ch)) {
+ return ret(ch);
+ } else if (ch == "=" && stream.eat(">")) {
+ return ret("=>", "operator");
+ } else if (ch == "0" && stream.eat(/x/i)) {
+ stream.eatWhile(/[\da-f]/i);
+ return ret("number", "number");
+ } else if (ch == "0" && stream.eat(/o/i)) {
+ stream.eatWhile(/[0-7]/i);
+ return ret("number", "number");
+ } else if (ch == "0" && stream.eat(/b/i)) {
+ stream.eatWhile(/[01]/i);
+ return ret("number", "number");
+ } else if (/\d/.test(ch)) {
+ stream.match(/^\d*(?:\.\d*)?(?:[eE][+\-]?\d+)?/);
+ return ret("number", "number");
+ } else if (ch == "/") {
+ if (stream.eat("*")) {
+ state.tokenize = tokenComment;
+ return tokenComment(stream, state);
+ } else if (stream.eat("/")) {
+ stream.skipToEnd();
+ return ret("comment", "comment");
+ } else if (expressionAllowed(stream, state, 1)) {
+ readRegexp(stream);
+ stream.match(/^\b(([gimyu])(?![gimyu]*\2))+\b/);
+ return ret("regexp", "string-2");
+ } else {
+ stream.eatWhile(isOperatorChar);
+ return ret("operator", "operator", stream.current());
+ }
+ } else if (ch == "`") {
+ state.tokenize = tokenQuasi;
+ return tokenQuasi(stream, state);
+ } else if (ch == "#") {
+ stream.skipToEnd();
+ return ret("error", "error");
+ } else if (isOperatorChar.test(ch)) {
+ stream.eatWhile(isOperatorChar);
+ return ret("operator", "operator", stream.current());
+ } else if (wordRE.test(ch)) {
+ stream.eatWhile(wordRE);
+ var word = stream.current(), known = keywords.propertyIsEnumerable(word) && keywords[word];
+ return (known && state.lastType != ".") ? ret(known.type, known.style, word) :
+ ret("variable", "variable", word);
+ }
+ }
+
+ function tokenString(quote) {
+ return function(stream, state) {
+ var escaped = false, next;
+ if (jsonldMode && stream.peek() == "@" && stream.match(isJsonldKeyword)){
+ state.tokenize = tokenBase;
+ return ret("jsonld-keyword", "meta");
+ }
+ while ((next = stream.next()) != null) {
+ if (next == quote && !escaped) break;
+ escaped = !escaped && next == "\\";
+ }
+ if (!escaped) state.tokenize = tokenBase;
+ return ret("string", "string");
+ };
+ }
+
+ function tokenComment(stream, state) {
+ var maybeEnd = false, ch;
+ while (ch = stream.next()) {
+ if (ch == "/" && maybeEnd) {
+ state.tokenize = tokenBase;
+ break;
+ }
+ maybeEnd = (ch == "*");
+ }
+ return ret("comment", "comment");
+ }
+
+ function tokenQuasi(stream, state) {
+ var escaped = false, next;
+ while ((next = stream.next()) != null) {
+ if (!escaped && (next == "`" || next == "$" && stream.eat("{"))) {
+ state.tokenize = tokenBase;
+ break;
+ }
+ escaped = !escaped && next == "\\";
+ }
+ return ret("quasi", "string-2", stream.current());
+ }
+
+ var brackets = "([{}])";
+ // This is a crude lookahead trick to try and notice that we're
+ // parsing the argument patterns for a fat-arrow function before we
+ // actually hit the arrow token. It only works if the arrow is on
+ // the same line as the arguments and there's no strange noise
+ // (comments) in between. Fallback is to only notice when we hit the
+ // arrow, and not declare the arguments as locals for the arrow
+ // body.
+ function findFatArrow(stream, state) {
+ if (state.fatArrowAt) state.fatArrowAt = null;
+ var arrow = stream.string.indexOf("=>", stream.start);
+ if (arrow < 0) return;
+
+ var depth = 0, sawSomething = false;
+ for (var pos = arrow - 1; pos >= 0; --pos) {
+ var ch = stream.string.charAt(pos);
+ var bracket = brackets.indexOf(ch);
+ if (bracket >= 0 && bracket < 3) {
+ if (!depth) { ++pos; break; }
+ if (--depth == 0) break;
+ } else if (bracket >= 3 && bracket < 6) {
+ ++depth;
+ } else if (wordRE.test(ch)) {
+ sawSomething = true;
+ } else if (/["'\/]/.test(ch)) {
+ return;
+ } else if (sawSomething && !depth) {
+ ++pos;
+ break;
+ }
+ }
+ if (sawSomething && !depth) state.fatArrowAt = pos;
+ }
+
+ // Parser
+
+ var atomicTypes = {"atom": true, "number": true, "variable": true, "string": true, "regexp": true, "this": true, "jsonld-keyword": true};
+
+ function JSLexical(indented, column, type, align, prev, info) {
+ this.indented = indented;
+ this.column = column;
+ this.type = type;
+ this.prev = prev;
+ this.info = info;
+ if (align != null) this.align = align;
+ }
+
+ function inScope(state, varname) {
+ for (var v = state.localVars; v; v = v.next)
+ if (v.name == varname) return true;
+ for (var cx = state.context; cx; cx = cx.prev) {
+ for (var v = cx.vars; v; v = v.next)
+ if (v.name == varname) return true;
+ }
+ }
+
+ function parseJS(state, style, type, content, stream) {
+ var cc = state.cc;
+ // Communicate our context to the combinators.
+ // (Less wasteful than consing up a hundred closures on every call.)
+ cx.state = state; cx.stream = stream; cx.marked = null, cx.cc = cc; cx.style = style;
+
+ if (!state.lexical.hasOwnProperty("align"))
+ state.lexical.align = true;
+
+ while(true) {
+ var combinator = cc.length ? cc.pop() : jsonMode ? expression : statement;
+ if (combinator(type, content)) {
+ while(cc.length && cc[cc.length - 1].lex)
+ cc.pop()();
+ if (cx.marked) return cx.marked;
+ if (type == "variable" && inScope(state, content)) return "variable-2";
+ return style;
+ }
+ }
+ }
+
+ // Combinator utils
+
+ var cx = {state: null, column: null, marked: null, cc: null};
+ function pass() {
+ for (var i = arguments.length - 1; i >= 0; i--) cx.cc.push(arguments[i]);
+ }
+ function cont() {
+ pass.apply(null, arguments);
+ return true;
+ }
+ function register(varname) {
+ function inList(list) {
+ for (var v = list; v; v = v.next)
+ if (v.name == varname) return true;
+ return false;
+ }
+ var state = cx.state;
+ cx.marked = "def";
+ if (state.context) {
+ if (inList(state.localVars)) return;
+ state.localVars = {name: varname, next: state.localVars};
+ } else {
+ if (inList(state.globalVars)) return;
+ if (parserConfig.globalVars)
+ state.globalVars = {name: varname, next: state.globalVars};
+ }
+ }
+
+ // Combinators
+
+ var defaultVars = {name: "this", next: {name: "arguments"}};
+ function pushcontext() {
+ cx.state.context = {prev: cx.state.context, vars: cx.state.localVars};
+ cx.state.localVars = defaultVars;
+ }
+ function popcontext() {
+ cx.state.localVars = cx.state.context.vars;
+ cx.state.context = cx.state.context.prev;
+ }
+ function pushlex(type, info) {
+ var result = function() {
+ var state = cx.state, indent = state.indented;
+ if (state.lexical.type == "stat") indent = state.lexical.indented;
+ else for (var outer = state.lexical; outer && outer.type == ")" && outer.align; outer = outer.prev)
+ indent = outer.indented;
+ state.lexical = new JSLexical(indent, cx.stream.column(), type, null, state.lexical, info);
+ };
+ result.lex = true;
+ return result;
+ }
+ function poplex() {
+ var state = cx.state;
+ if (state.lexical.prev) {
+ if (state.lexical.type == ")")
+ state.indented = state.lexical.indented;
+ state.lexical = state.lexical.prev;
+ }
+ }
+ poplex.lex = true;
+
+ function expect(wanted) {
+ function exp(type) {
+ if (type == wanted) return cont();
+ else if (wanted == ";") return pass();
+ else return cont(exp);
+ };
+ return exp;
+ }
+
+ function statement(type, value) {
+ if (type == "var") return cont(pushlex("vardef", value.length), vardef, expect(";"), poplex);
+ if (type == "keyword a") return cont(pushlex("form"), expression, statement, poplex);
+ if (type == "keyword b") return cont(pushlex("form"), statement, poplex);
+ if (type == "{") return cont(pushlex("}"), block, poplex);
+ if (type == ";") return cont();
+ if (type == "if") {
+ if (cx.state.lexical.info == "else" && cx.state.cc[cx.state.cc.length - 1] == poplex)
+ cx.state.cc.pop()();
+ return cont(pushlex("form"), expression, statement, poplex, maybeelse);
+ }
+ if (type == "function") return cont(functiondef);
+ if (type == "for") return cont(pushlex("form"), forspec, statement, poplex);
+ if (type == "variable") return cont(pushlex("stat"), maybelabel);
+ if (type == "switch") return cont(pushlex("form"), expression, pushlex("}", "switch"), expect("{"),
+ block, poplex, poplex);
+ if (type == "case") return cont(expression, expect(":"));
+ if (type == "default") return cont(expect(":"));
+ if (type == "catch") return cont(pushlex("form"), pushcontext, expect("("), funarg, expect(")"),
+ statement, poplex, popcontext);
+ if (type == "class") return cont(pushlex("form"), className, poplex);
+ if (type == "export") return cont(pushlex("stat"), afterExport, poplex);
+ if (type == "import") return cont(pushlex("stat"), afterImport, poplex);
+ if (type == "module") return cont(pushlex("form"), pattern, pushlex("}"), expect("{"), block, poplex, poplex)
+ if (type == "async") return cont(statement)
+ return pass(pushlex("stat"), expression, expect(";"), poplex);
+ }
+ function expression(type) {
+ return expressionInner(type, false);
+ }
+ function expressionNoComma(type) {
+ return expressionInner(type, true);
+ }
+ function expressionInner(type, noComma) {
+ if (cx.state.fatArrowAt == cx.stream.start) {
+ var body = noComma ? arrowBodyNoComma : arrowBody;
+ if (type == "(") return cont(pushcontext, pushlex(")"), commasep(pattern, ")"), poplex, expect("=>"), body, popcontext);
+ else if (type == "variable") return pass(pushcontext, pattern, expect("=>"), body, popcontext);
+ }
+
+ var maybeop = noComma ? maybeoperatorNoComma : maybeoperatorComma;
+ if (atomicTypes.hasOwnProperty(type)) return cont(maybeop);
+ if (type == "function") return cont(functiondef, maybeop);
+ if (type == "keyword c") return cont(noComma ? maybeexpressionNoComma : maybeexpression);
+ if (type == "(") return cont(pushlex(")"), maybeexpression, comprehension, expect(")"), poplex, maybeop);
+ if (type == "operator" || type == "spread") return cont(noComma ? expressionNoComma : expression);
+ if (type == "[") return cont(pushlex("]"), arrayLiteral, poplex, maybeop);
+ if (type == "{") return contCommasep(objprop, "}", null, maybeop);
+ if (type == "quasi") return pass(quasi, maybeop);
+ if (type == "new") return cont(maybeTarget(noComma));
+ return cont();
+ }
+ function maybeexpression(type) {
+ if (type.match(/[;\}\)\],]/)) return pass();
+ return pass(expression);
+ }
+ function maybeexpressionNoComma(type) {
+ if (type.match(/[;\}\)\],]/)) return pass();
+ return pass(expressionNoComma);
+ }
+
+ function maybeoperatorComma(type, value) {
+ if (type == ",") return cont(expression);
+ return maybeoperatorNoComma(type, value, false);
+ }
+ function maybeoperatorNoComma(type, value, noComma) {
+ var me = noComma == false ? maybeoperatorComma : maybeoperatorNoComma;
+ var expr = noComma == false ? expression : expressionNoComma;
+ if (type == "=>") return cont(pushcontext, noComma ? arrowBodyNoComma : arrowBody, popcontext);
+ if (type == "operator") {
+ if (/\+\+|--/.test(value)) return cont(me);
+ if (value == "?") return cont(expression, expect(":"), expr);
+ return cont(expr);
+ }
+ if (type == "quasi") { return pass(quasi, me); }
+ if (type == ";") return;
+ if (type == "(") return contCommasep(expressionNoComma, ")", "call", me);
+ if (type == ".") return cont(property, me);
+ if (type == "[") return cont(pushlex("]"), maybeexpression, expect("]"), poplex, me);
+ }
+ function quasi(type, value) {
+ if (type != "quasi") return pass();
+ if (value.slice(value.length - 2) != "${") return cont(quasi);
+ return cont(expression, continueQuasi);
+ }
+ function continueQuasi(type) {
+ if (type == "}") {
+ cx.marked = "string-2";
+ cx.state.tokenize = tokenQuasi;
+ return cont(quasi);
+ }
+ }
+ function arrowBody(type) {
+ findFatArrow(cx.stream, cx.state);
+ return pass(type == "{" ? statement : expression);
+ }
+ function arrowBodyNoComma(type) {
+ findFatArrow(cx.stream, cx.state);
+ return pass(type == "{" ? statement : expressionNoComma);
+ }
+ function maybeTarget(noComma) {
+ return function(type) {
+ if (type == ".") return cont(noComma ? targetNoComma : target);
+ else return pass(noComma ? expressionNoComma : expression);
+ };
+ }
+ function target(_, value) {
+ if (value == "target") { cx.marked = "keyword"; return cont(maybeoperatorComma); }
+ }
+ function targetNoComma(_, value) {
+ if (value == "target") { cx.marked = "keyword"; return cont(maybeoperatorNoComma); }
+ }
+ function maybelabel(type) {
+ if (type == ":") return cont(poplex, statement);
+ return pass(maybeoperatorComma, expect(";"), poplex);
+ }
+ function property(type) {
+ if (type == "variable") {cx.marked = "property"; return cont();}
+ }
+ function objprop(type, value) {
+ if (type == "variable" || cx.style == "keyword") {
+ cx.marked = "property";
+ if (value == "get" || value == "set") return cont(getterSetter);
+ return cont(afterprop);
+ } else if (type == "number" || type == "string") {
+ cx.marked = jsonldMode ? "property" : (cx.style + " property");
+ return cont(afterprop);
+ } else if (type == "jsonld-keyword") {
+ return cont(afterprop);
+ } else if (type == "modifier") {
+ return cont(objprop)
+ } else if (type == "[") {
+ return cont(expression, expect("]"), afterprop);
+ } else if (type == "spread") {
+ return cont(expression);
+ }
+ }
+ function getterSetter(type) {
+ if (type != "variable") return pass(afterprop);
+ cx.marked = "property";
+ return cont(functiondef);
+ }
+ function afterprop(type) {
+ if (type == ":") return cont(expressionNoComma);
+ if (type == "(") return pass(functiondef);
+ }
+ function commasep(what, end) {
+ function proceed(type, value) {
+ if (type == ",") {
+ var lex = cx.state.lexical;
+ if (lex.info == "call") lex.pos = (lex.pos || 0) + 1;
+ return cont(what, proceed);
+ }
+ if (type == end || value == end) return cont();
+ return cont(expect(end));
+ }
+ return function(type, value) {
+ if (type == end || value == end) return cont();
+ return pass(what, proceed);
+ };
+ }
+ function contCommasep(what, end, info) {
+ for (var i = 3; i < arguments.length; i++)
+ cx.cc.push(arguments[i]);
+ return cont(pushlex(end, info), commasep(what, end), poplex);
+ }
+ function block(type) {
+ if (type == "}") return cont();
+ return pass(statement, block);
+ }
+ function maybetype(type) {
+ if (isTS && type == ":") return cont(typeexpr);
+ }
+ function maybedefault(_, value) {
+ if (value == "=") return cont(expressionNoComma);
+ }
+ function typeexpr(type) {
+ if (type == "variable") {cx.marked = "variable-3"; return cont(afterType);}
+ }
+ function afterType(type, value) {
+ if (value == "<") return cont(commasep(typeexpr, ">"), afterType)
+ if (type == "[") return cont(expect("]"), afterType)
+ }
+ function vardef() {
+ return pass(pattern, maybetype, maybeAssign, vardefCont);
+ }
+ function pattern(type, value) {
+ if (type == "modifier") return cont(pattern)
+ if (type == "variable") { register(value); return cont(); }
+ if (type == "spread") return cont(pattern);
+ if (type == "[") return contCommasep(pattern, "]");
+ if (type == "{") return contCommasep(proppattern, "}");
+ }
+ function proppattern(type, value) {
+ if (type == "variable" && !cx.stream.match(/^\s*:/, false)) {
+ register(value);
+ return cont(maybeAssign);
+ }
+ if (type == "variable") cx.marked = "property";
+ if (type == "spread") return cont(pattern);
+ if (type == "}") return pass();
+ return cont(expect(":"), pattern, maybeAssign);
+ }
+ function maybeAssign(_type, value) {
+ if (value == "=") return cont(expressionNoComma);
+ }
+ function vardefCont(type) {
+ if (type == ",") return cont(vardef);
+ }
+ function maybeelse(type, value) {
+ if (type == "keyword b" && value == "else") return cont(pushlex("form", "else"), statement, poplex);
+ }
+ function forspec(type) {
+ if (type == "(") return cont(pushlex(")"), forspec1, expect(")"), poplex);
+ }
+ function forspec1(type) {
+ if (type == "var") return cont(vardef, expect(";"), forspec2);
+ if (type == ";") return cont(forspec2);
+ if (type == "variable") return cont(formaybeinof);
+ return pass(expression, expect(";"), forspec2);
+ }
+ function formaybeinof(_type, value) {
+ if (value == "in" || value == "of") { cx.marked = "keyword"; return cont(expression); }
+ return cont(maybeoperatorComma, forspec2);
+ }
+ function forspec2(type, value) {
+ if (type == ";") return cont(forspec3);
+ if (value == "in" || value == "of") { cx.marked = "keyword"; return cont(expression); }
+ return pass(expression, expect(";"), forspec3);
+ }
+ function forspec3(type) {
+ if (type != ")") cont(expression);
+ }
+ function functiondef(type, value) {
+ if (value == "*") {cx.marked = "keyword"; return cont(functiondef);}
+ if (type == "variable") {register(value); return cont(functiondef);}
+ if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, maybetype, statement, popcontext);
+ }
+ function funarg(type) {
+ if (type == "spread") return cont(funarg);
+ return pass(pattern, maybetype, maybedefault);
+ }
+ function className(type, value) {
+ if (type == "variable") {register(value); return cont(classNameAfter);}
+ }
+ function classNameAfter(type, value) {
+ if (value == "extends") return cont(expression, classNameAfter);
+ if (type == "{") return cont(pushlex("}"), classBody, poplex);
+ }
+ function classBody(type, value) {
+ if (type == "variable" || cx.style == "keyword") {
+ if (value == "static") {
+ cx.marked = "keyword";
+ return cont(classBody);
+ }
+ cx.marked = "property";
+ if (value == "get" || value == "set") return cont(classGetterSetter, functiondef, classBody);
+ return cont(functiondef, classBody);
+ }
+ if (value == "*") {
+ cx.marked = "keyword";
+ return cont(classBody);
+ }
+ if (type == ";") return cont(classBody);
+ if (type == "}") return cont();
+ }
+ function classGetterSetter(type) {
+ if (type != "variable") return pass();
+ cx.marked = "property";
+ return cont();
+ }
+ function afterExport(_type, value) {
+ if (value == "*") { cx.marked = "keyword"; return cont(maybeFrom, expect(";")); }
+ if (value == "default") { cx.marked = "keyword"; return cont(expression, expect(";")); }
+ return pass(statement);
+ }
+ function afterImport(type) {
+ if (type == "string") return cont();
+ return pass(importSpec, maybeFrom);
+ }
+ function importSpec(type, value) {
+ if (type == "{") return contCommasep(importSpec, "}");
+ if (type == "variable") register(value);
+ if (value == "*") cx.marked = "keyword";
+ return cont(maybeAs);
+ }
+ function maybeAs(_type, value) {
+ if (value == "as") { cx.marked = "keyword"; return cont(importSpec); }
+ }
+ function maybeFrom(_type, value) {
+ if (value == "from") { cx.marked = "keyword"; return cont(expression); }
+ }
+ function arrayLiteral(type) {
+ if (type == "]") return cont();
+ return pass(expressionNoComma, maybeArrayComprehension);
+ }
+ function maybeArrayComprehension(type) {
+ if (type == "for") return pass(comprehension, expect("]"));
+ if (type == ",") return cont(commasep(maybeexpressionNoComma, "]"));
+ return pass(commasep(expressionNoComma, "]"));
+ }
+ function comprehension(type) {
+ if (type == "for") return cont(forspec, comprehension);
+ if (type == "if") return cont(expression, comprehension);
+ }
+
+ function isContinuedStatement(state, textAfter) {
+ return state.lastType == "operator" || state.lastType == "," ||
+ isOperatorChar.test(textAfter.charAt(0)) ||
+ /[,.]/.test(textAfter.charAt(0));
+ }
+
+ // Interface
+
+ return {
+ startState: function(basecolumn) {
+ var state = {
+ tokenize: tokenBase,
+ lastType: "sof",
+ cc: [],
+ lexical: new JSLexical((basecolumn || 0) - indentUnit, 0, "block", false),
+ localVars: parserConfig.localVars,
+ context: parserConfig.localVars && {vars: parserConfig.localVars},
+ indented: basecolumn || 0
+ };
+ if (parserConfig.globalVars && typeof parserConfig.globalVars == "object")
+ state.globalVars = parserConfig.globalVars;
+ return state;
+ },
+
+ token: function(stream, state) {
+ if (stream.sol()) {
+ if (!state.lexical.hasOwnProperty("align"))
+ state.lexical.align = false;
+ state.indented = stream.indentation();
+ findFatArrow(stream, state);
+ }
+ if (state.tokenize != tokenComment && stream.eatSpace()) return null;
+ var style = state.tokenize(stream, state);
+ if (type == "comment") return style;
+ state.lastType = type == "operator" && (content == "++" || content == "--") ? "incdec" : type;
+ return parseJS(state, style, type, content, stream);
+ },
+
+ indent: function(state, textAfter) {
+ if (state.tokenize == tokenComment) return CodeMirror.Pass;
+ if (state.tokenize != tokenBase) return 0;
+ var firstChar = textAfter && textAfter.charAt(0), lexical = state.lexical;
+ // Kludge to prevent 'maybelse' from blocking lexical scope pops
+ if (!/^\s*else\b/.test(textAfter)) for (var i = state.cc.length - 1; i >= 0; --i) {
+ var c = state.cc[i];
+ if (c == poplex) lexical = lexical.prev;
+ else if (c != maybeelse) break;
+ }
+ if (lexical.type == "stat" && firstChar == "}") lexical = lexical.prev;
+ if (statementIndent && lexical.type == ")" && lexical.prev.type == "stat")
+ lexical = lexical.prev;
+ var type = lexical.type, closing = firstChar == type;
+
+ if (type == "vardef") return lexical.indented + (state.lastType == "operator" || state.lastType == "," ? lexical.info + 1 : 0);
+ else if (type == "form" && firstChar == "{") return lexical.indented;
+ else if (type == "form") return lexical.indented + indentUnit;
+ else if (type == "stat")
+ return lexical.indented + (isContinuedStatement(state, textAfter) ? statementIndent || indentUnit : 0);
+ else if (lexical.info == "switch" && !closing && parserConfig.doubleIndentSwitch != false)
+ return lexical.indented + (/^(?:case|default)\b/.test(textAfter) ? indentUnit : 2 * indentUnit);
+ else if (lexical.align) return lexical.column + (closing ? 0 : 1);
+ else return lexical.indented + (closing ? 0 : indentUnit);
+ },
+
+ electricInput: /^\s*(?:case .*?:|default:|\{|\})$/,
+ blockCommentStart: jsonMode ? null : "/*",
+ blockCommentEnd: jsonMode ? null : "*/",
+ lineComment: jsonMode ? null : "//",
+ fold: "brace",
+ closeBrackets: "()[]{}''\"\"``",
+
+ helperType: jsonMode ? "json" : "javascript",
+ jsonldMode: jsonldMode,
+ jsonMode: jsonMode,
+
+ expressionAllowed: expressionAllowed,
+ skipExpression: function(state) {
+ var top = state.cc[state.cc.length - 1]
+ if (top == expression || top == expressionNoComma) state.cc.pop()
+ }
+ };
+ });
+
+ CodeMirror.registerHelper("wordChars", "javascript", /[\w$]/);
+
+ CodeMirror.defineMIME("text/javascript", "javascript");
+ CodeMirror.defineMIME("text/ecmascript", "javascript");
+ CodeMirror.defineMIME("application/javascript", "javascript");
+ CodeMirror.defineMIME("application/x-javascript", "javascript");
+ CodeMirror.defineMIME("application/ecmascript", "javascript");
+ CodeMirror.defineMIME("application/json", {name: "javascript", json: true});
+ CodeMirror.defineMIME("application/x-json", {name: "javascript", json: true});
+ CodeMirror.defineMIME("application/ld+json", {name: "javascript", jsonld: true});
+ CodeMirror.defineMIME("text/typescript", { name: "javascript", typescript: true });
+ CodeMirror.defineMIME("application/typescript", { name: "javascript", typescript: true });
+
+ });
+
+
+/***/ },
+/* 9 */
+/***/ function(module, exports, __webpack_require__) {
+
+ // CodeMirror, copyright (c) by Marijn Haverbeke and others
+ // Distributed under an MIT license: http://codemirror.net/LICENSE
+
+ (function(mod) {
+ if (true) // CommonJS
+ mod(__webpack_require__(2));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+ })(function(CodeMirror) {
+ "use strict";
+
+ var htmlConfig = {
+ autoSelfClosers: {'area': true, 'base': true, 'br': true, 'col': true, 'command': true,
+ 'embed': true, 'frame': true, 'hr': true, 'img': true, 'input': true,
+ 'keygen': true, 'link': true, 'meta': true, 'param': true, 'source': true,
+ 'track': true, 'wbr': true, 'menuitem': true},
+ implicitlyClosed: {'dd': true, 'li': true, 'optgroup': true, 'option': true, 'p': true,
+ 'rp': true, 'rt': true, 'tbody': true, 'td': true, 'tfoot': true,
+ 'th': true, 'tr': true},
+ contextGrabbers: {
+ 'dd': {'dd': true, 'dt': true},
+ 'dt': {'dd': true, 'dt': true},
+ 'li': {'li': true},
+ 'option': {'option': true, 'optgroup': true},
+ 'optgroup': {'optgroup': true},
+ 'p': {'address': true, 'article': true, 'aside': true, 'blockquote': true, 'dir': true,
+ 'div': true, 'dl': true, 'fieldset': true, 'footer': true, 'form': true,
+ 'h1': true, 'h2': true, 'h3': true, 'h4': true, 'h5': true, 'h6': true,
+ 'header': true, 'hgroup': true, 'hr': true, 'menu': true, 'nav': true, 'ol': true,
+ 'p': true, 'pre': true, 'section': true, 'table': true, 'ul': true},
+ 'rp': {'rp': true, 'rt': true},
+ 'rt': {'rp': true, 'rt': true},
+ 'tbody': {'tbody': true, 'tfoot': true},
+ 'td': {'td': true, 'th': true},
+ 'tfoot': {'tbody': true},
+ 'th': {'td': true, 'th': true},
+ 'thead': {'tbody': true, 'tfoot': true},
+ 'tr': {'tr': true}
+ },
+ doNotIndent: {"pre": true},
+ allowUnquoted: true,
+ allowMissing: true,
+ caseFold: true
+ }
+
+ var xmlConfig = {
+ autoSelfClosers: {},
+ implicitlyClosed: {},
+ contextGrabbers: {},
+ doNotIndent: {},
+ allowUnquoted: false,
+ allowMissing: false,
+ caseFold: false
+ }
+
+ CodeMirror.defineMode("xml", function(editorConf, config_) {
+ var indentUnit = editorConf.indentUnit
+ var config = {}
+ var defaults = config_.htmlMode ? htmlConfig : xmlConfig
+ for (var prop in defaults) config[prop] = defaults[prop]
+ for (var prop in config_) config[prop] = config_[prop]
+
+ // Return variables for tokenizers
+ var type, setStyle;
+
+ function inText(stream, state) {
+ function chain(parser) {
+ state.tokenize = parser;
+ return parser(stream, state);
+ }
+
+ var ch = stream.next();
+ if (ch == "<") {
+ if (stream.eat("!")) {
+ if (stream.eat("[")) {
+ if (stream.match("CDATA[")) return chain(inBlock("atom", "]]>"));
+ else return null;
+ } else if (stream.match("--")) {
+ return chain(inBlock("comment", "-->"));
+ } else if (stream.match("DOCTYPE", true, true)) {
+ stream.eatWhile(/[\w\._\-]/);
+ return chain(doctype(1));
+ } else {
+ return null;
+ }
+ } else if (stream.eat("?")) {
+ stream.eatWhile(/[\w\._\-]/);
+ state.tokenize = inBlock("meta", "?>");
+ return "meta";
+ } else {
+ type = stream.eat("/") ? "closeTag" : "openTag";
+ state.tokenize = inTag;
+ return "tag bracket";
+ }
+ } else if (ch == "&") {
+ var ok;
+ if (stream.eat("#")) {
+ if (stream.eat("x")) {
+ ok = stream.eatWhile(/[a-fA-F\d]/) && stream.eat(";");
+ } else {
+ ok = stream.eatWhile(/[\d]/) && stream.eat(";");
+ }
+ } else {
+ ok = stream.eatWhile(/[\w\.\-:]/) && stream.eat(";");
+ }
+ return ok ? "atom" : "error";
+ } else {
+ stream.eatWhile(/[^&<]/);
+ return null;
+ }
+ }
+ inText.isInText = true;
+
+ function inTag(stream, state) {
+ var ch = stream.next();
+ if (ch == ">" || (ch == "/" && stream.eat(">"))) {
+ state.tokenize = inText;
+ type = ch == ">" ? "endTag" : "selfcloseTag";
+ return "tag bracket";
+ } else if (ch == "=") {
+ type = "equals";
+ return null;
+ } else if (ch == "<") {
+ state.tokenize = inText;
+ state.state = baseState;
+ state.tagName = state.tagStart = null;
+ var next = state.tokenize(stream, state);
+ return next ? next + " tag error" : "tag error";
+ } else if (/[\'\"]/.test(ch)) {
+ state.tokenize = inAttribute(ch);
+ state.stringStartCol = stream.column();
+ return state.tokenize(stream, state);
+ } else {
+ stream.match(/^[^\s\u00a0=<>\"\']*[^\s\u00a0=<>\"\'\/]/);
+ return "word";
+ }
+ }
+
+ function inAttribute(quote) {
+ var closure = function(stream, state) {
+ while (!stream.eol()) {
+ if (stream.next() == quote) {
+ state.tokenize = inTag;
+ break;
+ }
+ }
+ return "string";
+ };
+ closure.isInAttribute = true;
+ return closure;
+ }
+
+ function inBlock(style, terminator) {
+ return function(stream, state) {
+ while (!stream.eol()) {
+ if (stream.match(terminator)) {
+ state.tokenize = inText;
+ break;
+ }
+ stream.next();
+ }
+ return style;
+ };
+ }
+ function doctype(depth) {
+ return function(stream, state) {
+ var ch;
+ while ((ch = stream.next()) != null) {
+ if (ch == "<") {
+ state.tokenize = doctype(depth + 1);
+ return state.tokenize(stream, state);
+ } else if (ch == ">") {
+ if (depth == 1) {
+ state.tokenize = inText;
+ break;
+ } else {
+ state.tokenize = doctype(depth - 1);
+ return state.tokenize(stream, state);
+ }
+ }
+ }
+ return "meta";
+ };
+ }
+
+ function Context(state, tagName, startOfLine) {
+ this.prev = state.context;
+ this.tagName = tagName;
+ this.indent = state.indented;
+ this.startOfLine = startOfLine;
+ if (config.doNotIndent.hasOwnProperty(tagName) || (state.context && state.context.noIndent))
+ this.noIndent = true;
+ }
+ function popContext(state) {
+ if (state.context) state.context = state.context.prev;
+ }
+ function maybePopContext(state, nextTagName) {
+ var parentTagName;
+ while (true) {
+ if (!state.context) {
+ return;
+ }
+ parentTagName = state.context.tagName;
+ if (!config.contextGrabbers.hasOwnProperty(parentTagName) ||
+ !config.contextGrabbers[parentTagName].hasOwnProperty(nextTagName)) {
+ return;
+ }
+ popContext(state);
+ }
+ }
+
+ function baseState(type, stream, state) {
+ if (type == "openTag") {
+ state.tagStart = stream.column();
+ return tagNameState;
+ } else if (type == "closeTag") {
+ return closeTagNameState;
+ } else {
+ return baseState;
+ }
+ }
+ function tagNameState(type, stream, state) {
+ if (type == "word") {
+ state.tagName = stream.current();
+ setStyle = "tag";
+ return attrState;
+ } else {
+ setStyle = "error";
+ return tagNameState;
+ }
+ }
+ function closeTagNameState(type, stream, state) {
+ if (type == "word") {
+ var tagName = stream.current();
+ if (state.context && state.context.tagName != tagName &&
+ config.implicitlyClosed.hasOwnProperty(state.context.tagName))
+ popContext(state);
+ if ((state.context && state.context.tagName == tagName) || config.matchClosing === false) {
+ setStyle = "tag";
+ return closeState;
+ } else {
+ setStyle = "tag error";
+ return closeStateErr;
+ }
+ } else {
+ setStyle = "error";
+ return closeStateErr;
+ }
+ }
+
+ function closeState(type, _stream, state) {
+ if (type != "endTag") {
+ setStyle = "error";
+ return closeState;
+ }
+ popContext(state);
+ return baseState;
+ }
+ function closeStateErr(type, stream, state) {
+ setStyle = "error";
+ return closeState(type, stream, state);
+ }
+
+ function attrState(type, _stream, state) {
+ if (type == "word") {
+ setStyle = "attribute";
+ return attrEqState;
+ } else if (type == "endTag" || type == "selfcloseTag") {
+ var tagName = state.tagName, tagStart = state.tagStart;
+ state.tagName = state.tagStart = null;
+ if (type == "selfcloseTag" ||
+ config.autoSelfClosers.hasOwnProperty(tagName)) {
+ maybePopContext(state, tagName);
+ } else {
+ maybePopContext(state, tagName);
+ state.context = new Context(state, tagName, tagStart == state.indented);
+ }
+ return baseState;
+ }
+ setStyle = "error";
+ return attrState;
+ }
+ function attrEqState(type, stream, state) {
+ if (type == "equals") return attrValueState;
+ if (!config.allowMissing) setStyle = "error";
+ return attrState(type, stream, state);
+ }
+ function attrValueState(type, stream, state) {
+ if (type == "string") return attrContinuedState;
+ if (type == "word" && config.allowUnquoted) {setStyle = "string"; return attrState;}
+ setStyle = "error";
+ return attrState(type, stream, state);
+ }
+ function attrContinuedState(type, stream, state) {
+ if (type == "string") return attrContinuedState;
+ return attrState(type, stream, state);
+ }
+
+ return {
+ startState: function(baseIndent) {
+ var state = {tokenize: inText,
+ state: baseState,
+ indented: baseIndent || 0,
+ tagName: null, tagStart: null,
+ context: null}
+ if (baseIndent != null) state.baseIndent = baseIndent
+ return state
+ },
+
+ token: function(stream, state) {
+ if (!state.tagName && stream.sol())
+ state.indented = stream.indentation();
+
+ if (stream.eatSpace()) return null;
+ type = null;
+ var style = state.tokenize(stream, state);
+ if ((style || type) && style != "comment") {
+ setStyle = null;
+ state.state = state.state(type || style, stream, state);
+ if (setStyle)
+ style = setStyle == "error" ? style + " error" : setStyle;
+ }
+ return style;
+ },
+
+ indent: function(state, textAfter, fullLine) {
+ var context = state.context;
+ // Indent multi-line strings (e.g. css).
+ if (state.tokenize.isInAttribute) {
+ if (state.tagStart == state.indented)
+ return state.stringStartCol + 1;
+ else
+ return state.indented + indentUnit;
+ }
+ if (context && context.noIndent) return CodeMirror.Pass;
+ if (state.tokenize != inTag && state.tokenize != inText)
+ return fullLine ? fullLine.match(/^(\s*)/)[0].length : 0;
+ // Indent the starts of attribute names.
+ if (state.tagName) {
+ if (config.multilineTagIndentPastTag !== false)
+ return state.tagStart + state.tagName.length + 2;
+ else
+ return state.tagStart + indentUnit * (config.multilineTagIndentFactor || 1);
+ }
+ if (config.alignCDATA && /<!\[CDATA\[/.test(textAfter)) return 0;
+ var tagAfter = textAfter && /^<(\/)?([\w_:\.-]*)/.exec(textAfter);
+ if (tagAfter && tagAfter[1]) { // Closing tag spotted
+ while (context) {
+ if (context.tagName == tagAfter[2]) {
+ context = context.prev;
+ break;
+ } else if (config.implicitlyClosed.hasOwnProperty(context.tagName)) {
+ context = context.prev;
+ } else {
+ break;
+ }
+ }
+ } else if (tagAfter) { // Opening tag spotted
+ while (context) {
+ var grabbers = config.contextGrabbers[context.tagName];
+ if (grabbers && grabbers.hasOwnProperty(tagAfter[2]))
+ context = context.prev;
+ else
+ break;
+ }
+ }
+ while (context && context.prev && !context.startOfLine)
+ context = context.prev;
+ if (context) return context.indent + indentUnit;
+ else return state.baseIndent || 0;
+ },
+
+ electricInput: /<\/[\s\w:]+>$/,
+ blockCommentStart: "<!--",
+ blockCommentEnd: "-->",
+
+ configuration: config.htmlMode ? "html" : "xml",
+ helperType: config.htmlMode ? "html" : "xml",
+
+ skipAttribute: function(state) {
+ if (state.state == attrValueState)
+ state.state = attrState
+ }
+ };
+ });
+
+ CodeMirror.defineMIME("text/xml", "xml");
+ CodeMirror.defineMIME("application/xml", "xml");
+ if (!CodeMirror.mimeModes.hasOwnProperty("text/html"))
+ CodeMirror.defineMIME("text/html", {name: "xml", htmlMode: true});
+
+ });
+
+
+/***/ },
+/* 10 */
+/***/ function(module, exports, __webpack_require__) {
+
+ // CodeMirror, copyright (c) by Marijn Haverbeke and others
+ // Distributed under an MIT license: http://codemirror.net/LICENSE
+
+ (function(mod) {
+ if (true) // CommonJS
+ mod(__webpack_require__(2));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+ })(function(CodeMirror) {
+ "use strict";
+
+ CodeMirror.defineMode("css", function(config, parserConfig) {
+ var inline = parserConfig.inline
+ if (!parserConfig.propertyKeywords) parserConfig = CodeMirror.resolveMode("text/css");
+
+ var indentUnit = config.indentUnit,
+ tokenHooks = parserConfig.tokenHooks,
+ documentTypes = parserConfig.documentTypes || {},
+ mediaTypes = parserConfig.mediaTypes || {},
+ mediaFeatures = parserConfig.mediaFeatures || {},
+ mediaValueKeywords = parserConfig.mediaValueKeywords || {},
+ propertyKeywords = parserConfig.propertyKeywords || {},
+ nonStandardPropertyKeywords = parserConfig.nonStandardPropertyKeywords || {},
+ fontProperties = parserConfig.fontProperties || {},
+ counterDescriptors = parserConfig.counterDescriptors || {},
+ colorKeywords = parserConfig.colorKeywords || {},
+ valueKeywords = parserConfig.valueKeywords || {},
+ allowNested = parserConfig.allowNested,
+ supportsAtComponent = parserConfig.supportsAtComponent === true;
+
+ var type, override;
+ function ret(style, tp) { type = tp; return style; }
+
+ // Tokenizers
+
+ function tokenBase(stream, state) {
+ var ch = stream.next();
+ if (tokenHooks[ch]) {
+ var result = tokenHooks[ch](stream, state);
+ if (result !== false) return result;
+ }
+ if (ch == "@") {
+ stream.eatWhile(/[\w\\\-]/);
+ return ret("def", stream.current());
+ } else if (ch == "=" || (ch == "~" || ch == "|") && stream.eat("=")) {
+ return ret(null, "compare");
+ } else if (ch == "\"" || ch == "'") {
+ state.tokenize = tokenString(ch);
+ return state.tokenize(stream, state);
+ } else if (ch == "#") {
+ stream.eatWhile(/[\w\\\-]/);
+ return ret("atom", "hash");
+ } else if (ch == "!") {
+ stream.match(/^\s*\w*/);
+ return ret("keyword", "important");
+ } else if (/\d/.test(ch) || ch == "." && stream.eat(/\d/)) {
+ stream.eatWhile(/[\w.%]/);
+ return ret("number", "unit");
+ } else if (ch === "-") {
+ if (/[\d.]/.test(stream.peek())) {
+ stream.eatWhile(/[\w.%]/);
+ return ret("number", "unit");
+ } else if (stream.match(/^-[\w\\\-]+/)) {
+ stream.eatWhile(/[\w\\\-]/);
+ if (stream.match(/^\s*:/, false))
+ return ret("variable-2", "variable-definition");
+ return ret("variable-2", "variable");
+ } else if (stream.match(/^\w+-/)) {
+ return ret("meta", "meta");
+ }
+ } else if (/[,+>*\/]/.test(ch)) {
+ return ret(null, "select-op");
+ } else if (ch == "." && stream.match(/^-?[_a-z][_a-z0-9-]*/i)) {
+ return ret("qualifier", "qualifier");
+ } else if (/[:;{}\[\]\(\)]/.test(ch)) {
+ return ret(null, ch);
+ } else if ((ch == "u" && stream.match(/rl(-prefix)?\(/)) ||
+ (ch == "d" && stream.match("omain(")) ||
+ (ch == "r" && stream.match("egexp("))) {
+ stream.backUp(1);
+ state.tokenize = tokenParenthesized;
+ return ret("property", "word");
+ } else if (/[\w\\\-]/.test(ch)) {
+ stream.eatWhile(/[\w\\\-]/);
+ return ret("property", "word");
+ } else {
+ return ret(null, null);
+ }
+ }
+
+ function tokenString(quote) {
+ return function(stream, state) {
+ var escaped = false, ch;
+ while ((ch = stream.next()) != null) {
+ if (ch == quote && !escaped) {
+ if (quote == ")") stream.backUp(1);
+ break;
+ }
+ escaped = !escaped && ch == "\\";
+ }
+ if (ch == quote || !escaped && quote != ")") state.tokenize = null;
+ return ret("string", "string");
+ };
+ }
+
+ function tokenParenthesized(stream, state) {
+ stream.next(); // Must be '('
+ if (!stream.match(/\s*[\"\')]/, false))
+ state.tokenize = tokenString(")");
+ else
+ state.tokenize = null;
+ return ret(null, "(");
+ }
+
+ // Context management
+
+ function Context(type, indent, prev) {
+ this.type = type;
+ this.indent = indent;
+ this.prev = prev;
+ }
+
+ function pushContext(state, stream, type, indent) {
+ state.context = new Context(type, stream.indentation() + (indent === false ? 0 : indentUnit), state.context);
+ return type;
+ }
+
+ function popContext(state) {
+ if (state.context.prev)
+ state.context = state.context.prev;
+ return state.context.type;
+ }
+
+ function pass(type, stream, state) {
+ return states[state.context.type](type, stream, state);
+ }
+ function popAndPass(type, stream, state, n) {
+ for (var i = n || 1; i > 0; i--)
+ state.context = state.context.prev;
+ return pass(type, stream, state);
+ }
+
+ // Parser
+
+ function wordAsValue(stream) {
+ var word = stream.current().toLowerCase();
+ if (valueKeywords.hasOwnProperty(word))
+ override = "atom";
+ else if (colorKeywords.hasOwnProperty(word))
+ override = "keyword";
+ else
+ override = "variable";
+ }
+
+ var states = {};
+
+ states.top = function(type, stream, state) {
+ if (type == "{") {
+ return pushContext(state, stream, "block");
+ } else if (type == "}" && state.context.prev) {
+ return popContext(state);
+ } else if (supportsAtComponent && /@component/.test(type)) {
+ return pushContext(state, stream, "atComponentBlock");
+ } else if (/^@(-moz-)?document$/.test(type)) {
+ return pushContext(state, stream, "documentTypes");
+ } else if (/^@(media|supports|(-moz-)?document|import)$/.test(type)) {
+ return pushContext(state, stream, "atBlock");
+ } else if (/^@(font-face|counter-style)/.test(type)) {
+ state.stateArg = type;
+ return "restricted_atBlock_before";
+ } else if (/^@(-(moz|ms|o|webkit)-)?keyframes$/.test(type)) {
+ return "keyframes";
+ } else if (type && type.charAt(0) == "@") {
+ return pushContext(state, stream, "at");
+ } else if (type == "hash") {
+ override = "builtin";
+ } else if (type == "word") {
+ override = "tag";
+ } else if (type == "variable-definition") {
+ return "maybeprop";
+ } else if (type == "interpolation") {
+ return pushContext(state, stream, "interpolation");
+ } else if (type == ":") {
+ return "pseudo";
+ } else if (allowNested && type == "(") {
+ return pushContext(state, stream, "parens");
+ }
+ return state.context.type;
+ };
+
+ states.block = function(type, stream, state) {
+ if (type == "word") {
+ var word = stream.current().toLowerCase();
+ if (propertyKeywords.hasOwnProperty(word)) {
+ override = "property";
+ return "maybeprop";
+ } else if (nonStandardPropertyKeywords.hasOwnProperty(word)) {
+ override = "string-2";
+ return "maybeprop";
+ } else if (allowNested) {
+ override = stream.match(/^\s*:(?:\s|$)/, false) ? "property" : "tag";
+ return "block";
+ } else {
+ override += " error";
+ return "maybeprop";
+ }
+ } else if (type == "meta") {
+ return "block";
+ } else if (!allowNested && (type == "hash" || type == "qualifier")) {
+ override = "error";
+ return "block";
+ } else {
+ return states.top(type, stream, state);
+ }
+ };
+
+ states.maybeprop = function(type, stream, state) {
+ if (type == ":") return pushContext(state, stream, "prop");
+ return pass(type, stream, state);
+ };
+
+ states.prop = function(type, stream, state) {
+ if (type == ";") return popContext(state);
+ if (type == "{" && allowNested) return pushContext(state, stream, "propBlock");
+ if (type == "}" || type == "{") return popAndPass(type, stream, state);
+ if (type == "(") return pushContext(state, stream, "parens");
+
+ if (type == "hash" && !/^#([0-9a-fA-f]{3,4}|[0-9a-fA-f]{6}|[0-9a-fA-f]{8})$/.test(stream.current())) {
+ override += " error";
+ } else if (type == "word") {
+ wordAsValue(stream);
+ } else if (type == "interpolation") {
+ return pushContext(state, stream, "interpolation");
+ }
+ return "prop";
+ };
+
+ states.propBlock = function(type, _stream, state) {
+ if (type == "}") return popContext(state);
+ if (type == "word") { override = "property"; return "maybeprop"; }
+ return state.context.type;
+ };
+
+ states.parens = function(type, stream, state) {
+ if (type == "{" || type == "}") return popAndPass(type, stream, state);
+ if (type == ")") return popContext(state);
+ if (type == "(") return pushContext(state, stream, "parens");
+ if (type == "interpolation") return pushContext(state, stream, "interpolation");
+ if (type == "word") wordAsValue(stream);
+ return "parens";
+ };
+
+ states.pseudo = function(type, stream, state) {
+ if (type == "word") {
+ override = "variable-3";
+ return state.context.type;
+ }
+ return pass(type, stream, state);
+ };
+
+ states.documentTypes = function(type, stream, state) {
+ if (type == "word" && documentTypes.hasOwnProperty(stream.current())) {
+ override = "tag";
+ return state.context.type;
+ } else {
+ return states.atBlock(type, stream, state);
+ }
+ };
+
+ states.atBlock = function(type, stream, state) {
+ if (type == "(") return pushContext(state, stream, "atBlock_parens");
+ if (type == "}" || type == ";") return popAndPass(type, stream, state);
+ if (type == "{") return popContext(state) && pushContext(state, stream, allowNested ? "block" : "top");
+
+ if (type == "interpolation") return pushContext(state, stream, "interpolation");
+
+ if (type == "word") {
+ var word = stream.current().toLowerCase();
+ if (word == "only" || word == "not" || word == "and" || word == "or")
+ override = "keyword";
+ else if (mediaTypes.hasOwnProperty(word))
+ override = "attribute";
+ else if (mediaFeatures.hasOwnProperty(word))
+ override = "property";
+ else if (mediaValueKeywords.hasOwnProperty(word))
+ override = "keyword";
+ else if (propertyKeywords.hasOwnProperty(word))
+ override = "property";
+ else if (nonStandardPropertyKeywords.hasOwnProperty(word))
+ override = "string-2";
+ else if (valueKeywords.hasOwnProperty(word))
+ override = "atom";
+ else if (colorKeywords.hasOwnProperty(word))
+ override = "keyword";
+ else
+ override = "error";
+ }
+ return state.context.type;
+ };
+
+ states.atComponentBlock = function(type, stream, state) {
+ if (type == "}")
+ return popAndPass(type, stream, state);
+ if (type == "{")
+ return popContext(state) && pushContext(state, stream, allowNested ? "block" : "top", false);
+ if (type == "word")
+ override = "error";
+ return state.context.type;
+ };
+
+ states.atBlock_parens = function(type, stream, state) {
+ if (type == ")") return popContext(state);
+ if (type == "{" || type == "}") return popAndPass(type, stream, state, 2);
+ return states.atBlock(type, stream, state);
+ };
+
+ states.restricted_atBlock_before = function(type, stream, state) {
+ if (type == "{")
+ return pushContext(state, stream, "restricted_atBlock");
+ if (type == "word" && state.stateArg == "@counter-style") {
+ override = "variable";
+ return "restricted_atBlock_before";
+ }
+ return pass(type, stream, state);
+ };
+
+ states.restricted_atBlock = function(type, stream, state) {
+ if (type == "}") {
+ state.stateArg = null;
+ return popContext(state);
+ }
+ if (type == "word") {
+ if ((state.stateArg == "@font-face" && !fontProperties.hasOwnProperty(stream.current().toLowerCase())) ||
+ (state.stateArg == "@counter-style" && !counterDescriptors.hasOwnProperty(stream.current().toLowerCase())))
+ override = "error";
+ else
+ override = "property";
+ return "maybeprop";
+ }
+ return "restricted_atBlock";
+ };
+
+ states.keyframes = function(type, stream, state) {
+ if (type == "word") { override = "variable"; return "keyframes"; }
+ if (type == "{") return pushContext(state, stream, "top");
+ return pass(type, stream, state);
+ };
+
+ states.at = function(type, stream, state) {
+ if (type == ";") return popContext(state);
+ if (type == "{" || type == "}") return popAndPass(type, stream, state);
+ if (type == "word") override = "tag";
+ else if (type == "hash") override = "builtin";
+ return "at";
+ };
+
+ states.interpolation = function(type, stream, state) {
+ if (type == "}") return popContext(state);
+ if (type == "{" || type == ";") return popAndPass(type, stream, state);
+ if (type == "word") override = "variable";
+ else if (type != "variable" && type != "(" && type != ")") override = "error";
+ return "interpolation";
+ };
+
+ return {
+ startState: function(base) {
+ return {tokenize: null,
+ state: inline ? "block" : "top",
+ stateArg: null,
+ context: new Context(inline ? "block" : "top", base || 0, null)};
+ },
+
+ token: function(stream, state) {
+ if (!state.tokenize && stream.eatSpace()) return null;
+ var style = (state.tokenize || tokenBase)(stream, state);
+ if (style && typeof style == "object") {
+ type = style[1];
+ style = style[0];
+ }
+ override = style;
+ state.state = states[state.state](type, stream, state);
+ return override;
+ },
+
+ indent: function(state, textAfter) {
+ var cx = state.context, ch = textAfter && textAfter.charAt(0);
+ var indent = cx.indent;
+ if (cx.type == "prop" && (ch == "}" || ch == ")")) cx = cx.prev;
+ if (cx.prev) {
+ if (ch == "}" && (cx.type == "block" || cx.type == "top" ||
+ cx.type == "interpolation" || cx.type == "restricted_atBlock")) {
+ // Resume indentation from parent context.
+ cx = cx.prev;
+ indent = cx.indent;
+ } else if (ch == ")" && (cx.type == "parens" || cx.type == "atBlock_parens") ||
+ ch == "{" && (cx.type == "at" || cx.type == "atBlock")) {
+ // Dedent relative to current context.
+ indent = Math.max(0, cx.indent - indentUnit);
+ cx = cx.prev;
+ }
+ }
+ return indent;
+ },
+
+ electricChars: "}",
+ blockCommentStart: "/*",
+ blockCommentEnd: "*/",
+ fold: "brace"
+ };
+ });
+
+ function keySet(array) {
+ var keys = {};
+ for (var i = 0; i < array.length; ++i) {
+ keys[array[i]] = true;
+ }
+ return keys;
+ }
+
+ var documentTypes_ = [
+ "domain", "regexp", "url", "url-prefix"
+ ], documentTypes = keySet(documentTypes_);
+
+ var mediaTypes_ = [
+ "all", "aural", "braille", "handheld", "print", "projection", "screen",
+ "tty", "tv", "embossed"
+ ], mediaTypes = keySet(mediaTypes_);
+
+ var mediaFeatures_ = [
+ "width", "min-width", "max-width", "height", "min-height", "max-height",
+ "device-width", "min-device-width", "max-device-width", "device-height",
+ "min-device-height", "max-device-height", "aspect-ratio",
+ "min-aspect-ratio", "max-aspect-ratio", "device-aspect-ratio",
+ "min-device-aspect-ratio", "max-device-aspect-ratio", "color", "min-color",
+ "max-color", "color-index", "min-color-index", "max-color-index",
+ "monochrome", "min-monochrome", "max-monochrome", "resolution",
+ "min-resolution", "max-resolution", "scan", "grid", "orientation",
+ "device-pixel-ratio", "min-device-pixel-ratio", "max-device-pixel-ratio",
+ "pointer", "any-pointer", "hover", "any-hover"
+ ], mediaFeatures = keySet(mediaFeatures_);
+
+ var mediaValueKeywords_ = [
+ "landscape", "portrait", "none", "coarse", "fine", "on-demand", "hover",
+ "interlace", "progressive"
+ ], mediaValueKeywords = keySet(mediaValueKeywords_);
+
+ var propertyKeywords_ = [
+ "align-content", "align-items", "align-self", "alignment-adjust",
+ "alignment-baseline", "anchor-point", "animation", "animation-delay",
+ "animation-direction", "animation-duration", "animation-fill-mode",
+ "animation-iteration-count", "animation-name", "animation-play-state",
+ "animation-timing-function", "appearance", "azimuth", "backface-visibility",
+ "background", "background-attachment", "background-blend-mode", "background-clip",
+ "background-color", "background-image", "background-origin", "background-position",
+ "background-repeat", "background-size", "baseline-shift", "binding",
+ "bleed", "bookmark-label", "bookmark-level", "bookmark-state",
+ "bookmark-target", "border", "border-bottom", "border-bottom-color",
+ "border-bottom-left-radius", "border-bottom-right-radius",
+ "border-bottom-style", "border-bottom-width", "border-collapse",
+ "border-color", "border-image", "border-image-outset",
+ "border-image-repeat", "border-image-slice", "border-image-source",
+ "border-image-width", "border-left", "border-left-color",
+ "border-left-style", "border-left-width", "border-radius", "border-right",
+ "border-right-color", "border-right-style", "border-right-width",
+ "border-spacing", "border-style", "border-top", "border-top-color",
+ "border-top-left-radius", "border-top-right-radius", "border-top-style",
+ "border-top-width", "border-width", "bottom", "box-decoration-break",
+ "box-shadow", "box-sizing", "break-after", "break-before", "break-inside",
+ "caption-side", "clear", "clip", "color", "color-profile", "column-count",
+ "column-fill", "column-gap", "column-rule", "column-rule-color",
+ "column-rule-style", "column-rule-width", "column-span", "column-width",
+ "columns", "content", "counter-increment", "counter-reset", "crop", "cue",
+ "cue-after", "cue-before", "cursor", "direction", "display",
+ "dominant-baseline", "drop-initial-after-adjust",
+ "drop-initial-after-align", "drop-initial-before-adjust",
+ "drop-initial-before-align", "drop-initial-size", "drop-initial-value",
+ "elevation", "empty-cells", "fit", "fit-position", "flex", "flex-basis",
+ "flex-direction", "flex-flow", "flex-grow", "flex-shrink", "flex-wrap",
+ "float", "float-offset", "flow-from", "flow-into", "font", "font-feature-settings",
+ "font-family", "font-kerning", "font-language-override", "font-size", "font-size-adjust",
+ "font-stretch", "font-style", "font-synthesis", "font-variant",
+ "font-variant-alternates", "font-variant-caps", "font-variant-east-asian",
+ "font-variant-ligatures", "font-variant-numeric", "font-variant-position",
+ "font-weight", "grid", "grid-area", "grid-auto-columns", "grid-auto-flow",
+ "grid-auto-rows", "grid-column", "grid-column-end", "grid-column-gap",
+ "grid-column-start", "grid-gap", "grid-row", "grid-row-end", "grid-row-gap",
+ "grid-row-start", "grid-template", "grid-template-areas", "grid-template-columns",
+ "grid-template-rows", "hanging-punctuation", "height", "hyphens",
+ "icon", "image-orientation", "image-rendering", "image-resolution",
+ "inline-box-align", "justify-content", "left", "letter-spacing",
+ "line-break", "line-height", "line-stacking", "line-stacking-ruby",
+ "line-stacking-shift", "line-stacking-strategy", "list-style",
+ "list-style-image", "list-style-position", "list-style-type", "margin",
+ "margin-bottom", "margin-left", "margin-right", "margin-top",
+ "marks", "marquee-direction", "marquee-loop",
+ "marquee-play-count", "marquee-speed", "marquee-style", "max-height",
+ "max-width", "min-height", "min-width", "move-to", "nav-down", "nav-index",
+ "nav-left", "nav-right", "nav-up", "object-fit", "object-position",
+ "opacity", "order", "orphans", "outline",
+ "outline-color", "outline-offset", "outline-style", "outline-width",
+ "overflow", "overflow-style", "overflow-wrap", "overflow-x", "overflow-y",
+ "padding", "padding-bottom", "padding-left", "padding-right", "padding-top",
+ "page", "page-break-after", "page-break-before", "page-break-inside",
+ "page-policy", "pause", "pause-after", "pause-before", "perspective",
+ "perspective-origin", "pitch", "pitch-range", "play-during", "position",
+ "presentation-level", "punctuation-trim", "quotes", "region-break-after",
+ "region-break-before", "region-break-inside", "region-fragment",
+ "rendering-intent", "resize", "rest", "rest-after", "rest-before", "richness",
+ "right", "rotation", "rotation-point", "ruby-align", "ruby-overhang",
+ "ruby-position", "ruby-span", "shape-image-threshold", "shape-inside", "shape-margin",
+ "shape-outside", "size", "speak", "speak-as", "speak-header",
+ "speak-numeral", "speak-punctuation", "speech-rate", "stress", "string-set",
+ "tab-size", "table-layout", "target", "target-name", "target-new",
+ "target-position", "text-align", "text-align-last", "text-decoration",
+ "text-decoration-color", "text-decoration-line", "text-decoration-skip",
+ "text-decoration-style", "text-emphasis", "text-emphasis-color",
+ "text-emphasis-position", "text-emphasis-style", "text-height",
+ "text-indent", "text-justify", "text-outline", "text-overflow", "text-shadow",
+ "text-size-adjust", "text-space-collapse", "text-transform", "text-underline-position",
+ "text-wrap", "top", "transform", "transform-origin", "transform-style",
+ "transition", "transition-delay", "transition-duration",
+ "transition-property", "transition-timing-function", "unicode-bidi",
+ "vertical-align", "visibility", "voice-balance", "voice-duration",
+ "voice-family", "voice-pitch", "voice-range", "voice-rate", "voice-stress",
+ "voice-volume", "volume", "white-space", "widows", "width", "word-break",
+ "word-spacing", "word-wrap", "z-index",
+ // SVG-specific
+ "clip-path", "clip-rule", "mask", "enable-background", "filter", "flood-color",
+ "flood-opacity", "lighting-color", "stop-color", "stop-opacity", "pointer-events",
+ "color-interpolation", "color-interpolation-filters",
+ "color-rendering", "fill", "fill-opacity", "fill-rule", "image-rendering",
+ "marker", "marker-end", "marker-mid", "marker-start", "shape-rendering", "stroke",
+ "stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-linejoin",
+ "stroke-miterlimit", "stroke-opacity", "stroke-width", "text-rendering",
+ "baseline-shift", "dominant-baseline", "glyph-orientation-horizontal",
+ "glyph-orientation-vertical", "text-anchor", "writing-mode"
+ ], propertyKeywords = keySet(propertyKeywords_);
+
+ var nonStandardPropertyKeywords_ = [
+ "scrollbar-arrow-color", "scrollbar-base-color", "scrollbar-dark-shadow-color",
+ "scrollbar-face-color", "scrollbar-highlight-color", "scrollbar-shadow-color",
+ "scrollbar-3d-light-color", "scrollbar-track-color", "shape-inside",
+ "searchfield-cancel-button", "searchfield-decoration", "searchfield-results-button",
+ "searchfield-results-decoration", "zoom"
+ ], nonStandardPropertyKeywords = keySet(nonStandardPropertyKeywords_);
+
+ var fontProperties_ = [
+ "font-family", "src", "unicode-range", "font-variant", "font-feature-settings",
+ "font-stretch", "font-weight", "font-style"
+ ], fontProperties = keySet(fontProperties_);
+
+ var counterDescriptors_ = [
+ "additive-symbols", "fallback", "negative", "pad", "prefix", "range",
+ "speak-as", "suffix", "symbols", "system"
+ ], counterDescriptors = keySet(counterDescriptors_);
+
+ var colorKeywords_ = [
+ "aliceblue", "antiquewhite", "aqua", "aquamarine", "azure", "beige",
+ "bisque", "black", "blanchedalmond", "blue", "blueviolet", "brown",
+ "burlywood", "cadetblue", "chartreuse", "chocolate", "coral", "cornflowerblue",
+ "cornsilk", "crimson", "cyan", "darkblue", "darkcyan", "darkgoldenrod",
+ "darkgray", "darkgreen", "darkkhaki", "darkmagenta", "darkolivegreen",
+ "darkorange", "darkorchid", "darkred", "darksalmon", "darkseagreen",
+ "darkslateblue", "darkslategray", "darkturquoise", "darkviolet",
+ "deeppink", "deepskyblue", "dimgray", "dodgerblue", "firebrick",
+ "floralwhite", "forestgreen", "fuchsia", "gainsboro", "ghostwhite",
+ "gold", "goldenrod", "gray", "grey", "green", "greenyellow", "honeydew",
+ "hotpink", "indianred", "indigo", "ivory", "khaki", "lavender",
+ "lavenderblush", "lawngreen", "lemonchiffon", "lightblue", "lightcoral",
+ "lightcyan", "lightgoldenrodyellow", "lightgray", "lightgreen", "lightpink",
+ "lightsalmon", "lightseagreen", "lightskyblue", "lightslategray",
+ "lightsteelblue", "lightyellow", "lime", "limegreen", "linen", "magenta",
+ "maroon", "mediumaquamarine", "mediumblue", "mediumorchid", "mediumpurple",
+ "mediumseagreen", "mediumslateblue", "mediumspringgreen", "mediumturquoise",
+ "mediumvioletred", "midnightblue", "mintcream", "mistyrose", "moccasin",
+ "navajowhite", "navy", "oldlace", "olive", "olivedrab", "orange", "orangered",
+ "orchid", "palegoldenrod", "palegreen", "paleturquoise", "palevioletred",
+ "papayawhip", "peachpuff", "peru", "pink", "plum", "powderblue",
+ "purple", "rebeccapurple", "red", "rosybrown", "royalblue", "saddlebrown",
+ "salmon", "sandybrown", "seagreen", "seashell", "sienna", "silver", "skyblue",
+ "slateblue", "slategray", "snow", "springgreen", "steelblue", "tan",
+ "teal", "thistle", "tomato", "turquoise", "violet", "wheat", "white",
+ "whitesmoke", "yellow", "yellowgreen"
+ ], colorKeywords = keySet(colorKeywords_);
+
+ var valueKeywords_ = [
+ "above", "absolute", "activeborder", "additive", "activecaption", "afar",
+ "after-white-space", "ahead", "alias", "all", "all-scroll", "alphabetic", "alternate",
+ "always", "amharic", "amharic-abegede", "antialiased", "appworkspace",
+ "arabic-indic", "armenian", "asterisks", "attr", "auto", "avoid", "avoid-column", "avoid-page",
+ "avoid-region", "background", "backwards", "baseline", "below", "bidi-override", "binary",
+ "bengali", "blink", "block", "block-axis", "bold", "bolder", "border", "border-box",
+ "both", "bottom", "break", "break-all", "break-word", "bullets", "button", "button-bevel",
+ "buttonface", "buttonhighlight", "buttonshadow", "buttontext", "calc", "cambodian",
+ "capitalize", "caps-lock-indicator", "caption", "captiontext", "caret",
+ "cell", "center", "checkbox", "circle", "cjk-decimal", "cjk-earthly-branch",
+ "cjk-heavenly-stem", "cjk-ideographic", "clear", "clip", "close-quote",
+ "col-resize", "collapse", "color", "color-burn", "color-dodge", "column", "column-reverse",
+ "compact", "condensed", "contain", "content",
+ "content-box", "context-menu", "continuous", "copy", "counter", "counters", "cover", "crop",
+ "cross", "crosshair", "currentcolor", "cursive", "cyclic", "darken", "dashed", "decimal",
+ "decimal-leading-zero", "default", "default-button", "dense", "destination-atop",
+ "destination-in", "destination-out", "destination-over", "devanagari", "difference",
+ "disc", "discard", "disclosure-closed", "disclosure-open", "document",
+ "dot-dash", "dot-dot-dash",
+ "dotted", "double", "down", "e-resize", "ease", "ease-in", "ease-in-out", "ease-out",
+ "element", "ellipse", "ellipsis", "embed", "end", "ethiopic", "ethiopic-abegede",
+ "ethiopic-abegede-am-et", "ethiopic-abegede-gez", "ethiopic-abegede-ti-er",
+ "ethiopic-abegede-ti-et", "ethiopic-halehame-aa-er",
+ "ethiopic-halehame-aa-et", "ethiopic-halehame-am-et",
+ "ethiopic-halehame-gez", "ethiopic-halehame-om-et",
+ "ethiopic-halehame-sid-et", "ethiopic-halehame-so-et",
+ "ethiopic-halehame-ti-er", "ethiopic-halehame-ti-et", "ethiopic-halehame-tig",
+ "ethiopic-numeric", "ew-resize", "exclusion", "expanded", "extends", "extra-condensed",
+ "extra-expanded", "fantasy", "fast", "fill", "fixed", "flat", "flex", "flex-end", "flex-start", "footnotes",
+ "forwards", "from", "geometricPrecision", "georgian", "graytext", "grid", "groove",
+ "gujarati", "gurmukhi", "hand", "hangul", "hangul-consonant", "hard-light", "hebrew",
+ "help", "hidden", "hide", "higher", "highlight", "highlighttext",
+ "hiragana", "hiragana-iroha", "horizontal", "hsl", "hsla", "hue", "icon", "ignore",
+ "inactiveborder", "inactivecaption", "inactivecaptiontext", "infinite",
+ "infobackground", "infotext", "inherit", "initial", "inline", "inline-axis",
+ "inline-block", "inline-flex", "inline-grid", "inline-table", "inset", "inside", "intrinsic", "invert",
+ "italic", "japanese-formal", "japanese-informal", "justify", "kannada",
+ "katakana", "katakana-iroha", "keep-all", "khmer",
+ "korean-hangul-formal", "korean-hanja-formal", "korean-hanja-informal",
+ "landscape", "lao", "large", "larger", "left", "level", "lighter", "lighten",
+ "line-through", "linear", "linear-gradient", "lines", "list-item", "listbox", "listitem",
+ "local", "logical", "loud", "lower", "lower-alpha", "lower-armenian",
+ "lower-greek", "lower-hexadecimal", "lower-latin", "lower-norwegian",
+ "lower-roman", "lowercase", "ltr", "luminosity", "malayalam", "match", "matrix", "matrix3d",
+ "media-controls-background", "media-current-time-display",
+ "media-fullscreen-button", "media-mute-button", "media-play-button",
+ "media-return-to-realtime-button", "media-rewind-button",
+ "media-seek-back-button", "media-seek-forward-button", "media-slider",
+ "media-sliderthumb", "media-time-remaining-display", "media-volume-slider",
+ "media-volume-slider-container", "media-volume-sliderthumb", "medium",
+ "menu", "menulist", "menulist-button", "menulist-text",
+ "menulist-textfield", "menutext", "message-box", "middle", "min-intrinsic",
+ "mix", "mongolian", "monospace", "move", "multiple", "multiply", "myanmar", "n-resize",
+ "narrower", "ne-resize", "nesw-resize", "no-close-quote", "no-drop",
+ "no-open-quote", "no-repeat", "none", "normal", "not-allowed", "nowrap",
+ "ns-resize", "numbers", "numeric", "nw-resize", "nwse-resize", "oblique", "octal", "open-quote",
+ "optimizeLegibility", "optimizeSpeed", "oriya", "oromo", "outset",
+ "outside", "outside-shape", "overlay", "overline", "padding", "padding-box",
+ "painted", "page", "paused", "persian", "perspective", "plus-darker", "plus-lighter",
+ "pointer", "polygon", "portrait", "pre", "pre-line", "pre-wrap", "preserve-3d",
+ "progress", "push-button", "radial-gradient", "radio", "read-only",
+ "read-write", "read-write-plaintext-only", "rectangle", "region",
+ "relative", "repeat", "repeating-linear-gradient",
+ "repeating-radial-gradient", "repeat-x", "repeat-y", "reset", "reverse",
+ "rgb", "rgba", "ridge", "right", "rotate", "rotate3d", "rotateX", "rotateY",
+ "rotateZ", "round", "row", "row-resize", "row-reverse", "rtl", "run-in", "running",
+ "s-resize", "sans-serif", "saturation", "scale", "scale3d", "scaleX", "scaleY", "scaleZ", "screen",
+ "scroll", "scrollbar", "se-resize", "searchfield",
+ "searchfield-cancel-button", "searchfield-decoration",
+ "searchfield-results-button", "searchfield-results-decoration",
+ "semi-condensed", "semi-expanded", "separate", "serif", "show", "sidama",
+ "simp-chinese-formal", "simp-chinese-informal", "single",
+ "skew", "skewX", "skewY", "skip-white-space", "slide", "slider-horizontal",
+ "slider-vertical", "sliderthumb-horizontal", "sliderthumb-vertical", "slow",
+ "small", "small-caps", "small-caption", "smaller", "soft-light", "solid", "somali",
+ "source-atop", "source-in", "source-out", "source-over", "space", "space-around", "space-between", "spell-out", "square",
+ "square-button", "start", "static", "status-bar", "stretch", "stroke", "sub",
+ "subpixel-antialiased", "super", "sw-resize", "symbolic", "symbols", "table",
+ "table-caption", "table-cell", "table-column", "table-column-group",
+ "table-footer-group", "table-header-group", "table-row", "table-row-group",
+ "tamil",
+ "telugu", "text", "text-bottom", "text-top", "textarea", "textfield", "thai",
+ "thick", "thin", "threeddarkshadow", "threedface", "threedhighlight",
+ "threedlightshadow", "threedshadow", "tibetan", "tigre", "tigrinya-er",
+ "tigrinya-er-abegede", "tigrinya-et", "tigrinya-et-abegede", "to", "top",
+ "trad-chinese-formal", "trad-chinese-informal",
+ "translate", "translate3d", "translateX", "translateY", "translateZ",
+ "transparent", "ultra-condensed", "ultra-expanded", "underline", "up",
+ "upper-alpha", "upper-armenian", "upper-greek", "upper-hexadecimal",
+ "upper-latin", "upper-norwegian", "upper-roman", "uppercase", "urdu", "url",
+ "var", "vertical", "vertical-text", "visible", "visibleFill", "visiblePainted",
+ "visibleStroke", "visual", "w-resize", "wait", "wave", "wider",
+ "window", "windowframe", "windowtext", "words", "wrap", "wrap-reverse", "x-large", "x-small", "xor",
+ "xx-large", "xx-small"
+ ], valueKeywords = keySet(valueKeywords_);
+
+ var allWords = documentTypes_.concat(mediaTypes_).concat(mediaFeatures_).concat(mediaValueKeywords_)
+ .concat(propertyKeywords_).concat(nonStandardPropertyKeywords_).concat(colorKeywords_)
+ .concat(valueKeywords_);
+ CodeMirror.registerHelper("hintWords", "css", allWords);
+
+ function tokenCComment(stream, state) {
+ var maybeEnd = false, ch;
+ while ((ch = stream.next()) != null) {
+ if (maybeEnd && ch == "/") {
+ state.tokenize = null;
+ break;
+ }
+ maybeEnd = (ch == "*");
+ }
+ return ["comment", "comment"];
+ }
+
+ CodeMirror.defineMIME("text/css", {
+ documentTypes: documentTypes,
+ mediaTypes: mediaTypes,
+ mediaFeatures: mediaFeatures,
+ mediaValueKeywords: mediaValueKeywords,
+ propertyKeywords: propertyKeywords,
+ nonStandardPropertyKeywords: nonStandardPropertyKeywords,
+ fontProperties: fontProperties,
+ counterDescriptors: counterDescriptors,
+ colorKeywords: colorKeywords,
+ valueKeywords: valueKeywords,
+ tokenHooks: {
+ "/": function(stream, state) {
+ if (!stream.eat("*")) return false;
+ state.tokenize = tokenCComment;
+ return tokenCComment(stream, state);
+ }
+ },
+ name: "css"
+ });
+
+ CodeMirror.defineMIME("text/x-scss", {
+ mediaTypes: mediaTypes,
+ mediaFeatures: mediaFeatures,
+ mediaValueKeywords: mediaValueKeywords,
+ propertyKeywords: propertyKeywords,
+ nonStandardPropertyKeywords: nonStandardPropertyKeywords,
+ colorKeywords: colorKeywords,
+ valueKeywords: valueKeywords,
+ fontProperties: fontProperties,
+ allowNested: true,
+ tokenHooks: {
+ "/": function(stream, state) {
+ if (stream.eat("/")) {
+ stream.skipToEnd();
+ return ["comment", "comment"];
+ } else if (stream.eat("*")) {
+ state.tokenize = tokenCComment;
+ return tokenCComment(stream, state);
+ } else {
+ return ["operator", "operator"];
+ }
+ },
+ ":": function(stream) {
+ if (stream.match(/\s*\{/))
+ return [null, "{"];
+ return false;
+ },
+ "$": function(stream) {
+ stream.match(/^[\w-]+/);
+ if (stream.match(/^\s*:/, false))
+ return ["variable-2", "variable-definition"];
+ return ["variable-2", "variable"];
+ },
+ "#": function(stream) {
+ if (!stream.eat("{")) return false;
+ return [null, "interpolation"];
+ }
+ },
+ name: "css",
+ helperType: "scss"
+ });
+
+ CodeMirror.defineMIME("text/x-less", {
+ mediaTypes: mediaTypes,
+ mediaFeatures: mediaFeatures,
+ mediaValueKeywords: mediaValueKeywords,
+ propertyKeywords: propertyKeywords,
+ nonStandardPropertyKeywords: nonStandardPropertyKeywords,
+ colorKeywords: colorKeywords,
+ valueKeywords: valueKeywords,
+ fontProperties: fontProperties,
+ allowNested: true,
+ tokenHooks: {
+ "/": function(stream, state) {
+ if (stream.eat("/")) {
+ stream.skipToEnd();
+ return ["comment", "comment"];
+ } else if (stream.eat("*")) {
+ state.tokenize = tokenCComment;
+ return tokenCComment(stream, state);
+ } else {
+ return ["operator", "operator"];
+ }
+ },
+ "@": function(stream) {
+ if (stream.eat("{")) return [null, "interpolation"];
+ if (stream.match(/^(charset|document|font-face|import|(-(moz|ms|o|webkit)-)?keyframes|media|namespace|page|supports)\b/, false)) return false;
+ stream.eatWhile(/[\w\\\-]/);
+ if (stream.match(/^\s*:/, false))
+ return ["variable-2", "variable-definition"];
+ return ["variable-2", "variable"];
+ },
+ "&": function() {
+ return ["atom", "atom"];
+ }
+ },
+ name: "css",
+ helperType: "less"
+ });
+
+ CodeMirror.defineMIME("text/x-gss", {
+ documentTypes: documentTypes,
+ mediaTypes: mediaTypes,
+ mediaFeatures: mediaFeatures,
+ propertyKeywords: propertyKeywords,
+ nonStandardPropertyKeywords: nonStandardPropertyKeywords,
+ fontProperties: fontProperties,
+ counterDescriptors: counterDescriptors,
+ colorKeywords: colorKeywords,
+ valueKeywords: valueKeywords,
+ supportsAtComponent: true,
+ tokenHooks: {
+ "/": function(stream, state) {
+ if (!stream.eat("*")) return false;
+ state.tokenize = tokenCComment;
+ return tokenCComment(stream, state);
+ }
+ },
+ name: "css",
+ helperType: "gss"
+ });
+
+ });
+
+
+/***/ },
+/* 11 */
+/***/ function(module, exports, __webpack_require__) {
+
+ // CodeMirror, copyright (c) by Marijn Haverbeke and others
+ // Distributed under an MIT license: http://codemirror.net/LICENSE
+
+ (function(mod) {
+ if (true) // CommonJS
+ mod(__webpack_require__(2), __webpack_require__(9), __webpack_require__(8), __webpack_require__(10));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror", "../xml/xml", "../javascript/javascript", "../css/css"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+ })(function(CodeMirror) {
+ "use strict";
+
+ var defaultTags = {
+ script: [
+ ["lang", /(javascript|babel)/i, "javascript"],
+ ["type", /^(?:text|application)\/(?:x-)?(?:java|ecma)script$|^$/i, "javascript"],
+ ["type", /./, "text/plain"],
+ [null, null, "javascript"]
+ ],
+ style: [
+ ["lang", /^css$/i, "css"],
+ ["type", /^(text\/)?(x-)?(stylesheet|css)$/i, "css"],
+ ["type", /./, "text/plain"],
+ [null, null, "css"]
+ ]
+ };
+
+ function maybeBackup(stream, pat, style) {
+ var cur = stream.current(), close = cur.search(pat);
+ if (close > -1) {
+ stream.backUp(cur.length - close);
+ } else if (cur.match(/<\/?$/)) {
+ stream.backUp(cur.length);
+ if (!stream.match(pat, false)) stream.match(cur);
+ }
+ return style;
+ }
+
+ var attrRegexpCache = {};
+ function getAttrRegexp(attr) {
+ var regexp = attrRegexpCache[attr];
+ if (regexp) return regexp;
+ return attrRegexpCache[attr] = new RegExp("\\s+" + attr + "\\s*=\\s*('|\")?([^'\"]+)('|\")?\\s*");
+ }
+
+ function getAttrValue(text, attr) {
+ var match = text.match(getAttrRegexp(attr))
+ return match ? match[2] : ""
+ }
+
+ function getTagRegexp(tagName, anchored) {
+ return new RegExp((anchored ? "^" : "") + "<\/\s*" + tagName + "\s*>", "i");
+ }
+
+ function addTags(from, to) {
+ for (var tag in from) {
+ var dest = to[tag] || (to[tag] = []);
+ var source = from[tag];
+ for (var i = source.length - 1; i >= 0; i--)
+ dest.unshift(source[i])
+ }
+ }
+
+ function findMatchingMode(tagInfo, tagText) {
+ for (var i = 0; i < tagInfo.length; i++) {
+ var spec = tagInfo[i];
+ if (!spec[0] || spec[1].test(getAttrValue(tagText, spec[0]))) return spec[2];
+ }
+ }
+
+ CodeMirror.defineMode("htmlmixed", function (config, parserConfig) {
+ var htmlMode = CodeMirror.getMode(config, {
+ name: "xml",
+ htmlMode: true,
+ multilineTagIndentFactor: parserConfig.multilineTagIndentFactor,
+ multilineTagIndentPastTag: parserConfig.multilineTagIndentPastTag
+ });
+
+ var tags = {};
+ var configTags = parserConfig && parserConfig.tags, configScript = parserConfig && parserConfig.scriptTypes;
+ addTags(defaultTags, tags);
+ if (configTags) addTags(configTags, tags);
+ if (configScript) for (var i = configScript.length - 1; i >= 0; i--)
+ tags.script.unshift(["type", configScript[i].matches, configScript[i].mode])
+
+ function html(stream, state) {
+ var style = htmlMode.token(stream, state.htmlState), tag = /\btag\b/.test(style), tagName
+ if (tag && !/[<>\s\/]/.test(stream.current()) &&
+ (tagName = state.htmlState.tagName && state.htmlState.tagName.toLowerCase()) &&
+ tags.hasOwnProperty(tagName)) {
+ state.inTag = tagName + " "
+ } else if (state.inTag && tag && />$/.test(stream.current())) {
+ var inTag = /^([\S]+) (.*)/.exec(state.inTag)
+ state.inTag = null
+ var modeSpec = stream.current() == ">" && findMatchingMode(tags[inTag[1]], inTag[2])
+ var mode = CodeMirror.getMode(config, modeSpec)
+ var endTagA = getTagRegexp(inTag[1], true), endTag = getTagRegexp(inTag[1], false);
+ state.token = function (stream, state) {
+ if (stream.match(endTagA, false)) {
+ state.token = html;
+ state.localState = state.localMode = null;
+ return null;
+ }
+ return maybeBackup(stream, endTag, state.localMode.token(stream, state.localState));
+ };
+ state.localMode = mode;
+ state.localState = CodeMirror.startState(mode, htmlMode.indent(state.htmlState, ""));
+ } else if (state.inTag) {
+ state.inTag += stream.current()
+ if (stream.eol()) state.inTag += " "
+ }
+ return style;
+ };
+
+ return {
+ startState: function () {
+ var state = CodeMirror.startState(htmlMode);
+ return {token: html, inTag: null, localMode: null, localState: null, htmlState: state};
+ },
+
+ copyState: function (state) {
+ var local;
+ if (state.localState) {
+ local = CodeMirror.copyState(state.localMode, state.localState);
+ }
+ return {token: state.token, inTag: state.inTag,
+ localMode: state.localMode, localState: local,
+ htmlState: CodeMirror.copyState(htmlMode, state.htmlState)};
+ },
+
+ token: function (stream, state) {
+ return state.token(stream, state);
+ },
+
+ indent: function (state, textAfter) {
+ if (!state.localMode || /^\s*<\//.test(textAfter))
+ return htmlMode.indent(state.htmlState, textAfter);
+ else if (state.localMode.indent)
+ return state.localMode.indent(state.localState, textAfter);
+ else
+ return CodeMirror.Pass;
+ },
+
+ innerMode: function (state) {
+ return {state: state.localState || state.htmlState, mode: state.localMode || htmlMode};
+ }
+ };
+ }, "xml", "javascript", "css");
+
+ CodeMirror.defineMIME("text/html", "htmlmixed");
+ });
+
+
+/***/ },
+/* 12 */
+/***/ function(module, exports, __webpack_require__) {
+
+ // CodeMirror, copyright (c) by Marijn Haverbeke and others
+ // Distributed under an MIT license: http://codemirror.net/LICENSE
+
+ (function(mod) {
+ if (true) // CommonJS
+ mod(__webpack_require__(2));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+ })(function(CodeMirror) {
+ "use strict";
+
+ function Context(indented, column, type, info, align, prev) {
+ this.indented = indented;
+ this.column = column;
+ this.type = type;
+ this.info = info;
+ this.align = align;
+ this.prev = prev;
+ }
+ function pushContext(state, col, type, info) {
+ var indent = state.indented;
+ if (state.context && state.context.type != "statement" && type != "statement")
+ indent = state.context.indented;
+ return state.context = new Context(indent, col, type, info, null, state.context);
+ }
+ function popContext(state) {
+ var t = state.context.type;
+ if (t == ")" || t == "]" || t == "}")
+ state.indented = state.context.indented;
+ return state.context = state.context.prev;
+ }
+
+ function typeBefore(stream, state, pos) {
+ if (state.prevToken == "variable" || state.prevToken == "variable-3") return true;
+ if (/\S(?:[^- ]>|[*\]])\s*$|\*$/.test(stream.string.slice(0, pos))) return true;
+ if (state.typeAtEndOfLine && stream.column() == stream.indentation()) return true;
+ }
+
+ function isTopScope(context) {
+ for (;;) {
+ if (!context || context.type == "top") return true;
+ if (context.type == "}" && context.prev.info != "namespace") return false;
+ context = context.prev;
+ }
+ }
+
+ CodeMirror.defineMode("clike", function(config, parserConfig) {
+ var indentUnit = config.indentUnit,
+ statementIndentUnit = parserConfig.statementIndentUnit || indentUnit,
+ dontAlignCalls = parserConfig.dontAlignCalls,
+ keywords = parserConfig.keywords || {},
+ types = parserConfig.types || {},
+ builtin = parserConfig.builtin || {},
+ blockKeywords = parserConfig.blockKeywords || {},
+ defKeywords = parserConfig.defKeywords || {},
+ atoms = parserConfig.atoms || {},
+ hooks = parserConfig.hooks || {},
+ multiLineStrings = parserConfig.multiLineStrings,
+ indentStatements = parserConfig.indentStatements !== false,
+ indentSwitch = parserConfig.indentSwitch !== false,
+ namespaceSeparator = parserConfig.namespaceSeparator,
+ isPunctuationChar = parserConfig.isPunctuationChar || /[\[\]{}\(\),;\:\.]/,
+ numberStart = parserConfig.numberStart || /[\d\.]/,
+ number = parserConfig.number || /^(?:0x[a-f\d]+|0b[01]+|(?:\d+\.?\d*|\.\d+)(?:e[-+]?\d+)?)(u|ll?|l|f)?/i,
+ isOperatorChar = parserConfig.isOperatorChar || /[+\-*&%=<>!?|\/]/,
+ endStatement = parserConfig.endStatement || /^[;:,]$/;
+
+ var curPunc, isDefKeyword;
+
+ function tokenBase(stream, state) {
+ var ch = stream.next();
+ if (hooks[ch]) {
+ var result = hooks[ch](stream, state);
+ if (result !== false) return result;
+ }
+ if (ch == '"' || ch == "'") {
+ state.tokenize = tokenString(ch);
+ return state.tokenize(stream, state);
+ }
+ if (isPunctuationChar.test(ch)) {
+ curPunc = ch;
+ return null;
+ }
+ if (numberStart.test(ch)) {
+ stream.backUp(1)
+ if (stream.match(number)) return "number"
+ stream.next()
+ }
+ if (ch == "/") {
+ if (stream.eat("*")) {
+ state.tokenize = tokenComment;
+ return tokenComment(stream, state);
+ }
+ if (stream.eat("/")) {
+ stream.skipToEnd();
+ return "comment";
+ }
+ }
+ if (isOperatorChar.test(ch)) {
+ while (!stream.match(/^\/[\/*]/, false) && stream.eat(isOperatorChar)) {}
+ return "operator";
+ }
+ stream.eatWhile(/[\w\$_\xa1-\uffff]/);
+ if (namespaceSeparator) while (stream.match(namespaceSeparator))
+ stream.eatWhile(/[\w\$_\xa1-\uffff]/);
+
+ var cur = stream.current();
+ if (contains(keywords, cur)) {
+ if (contains(blockKeywords, cur)) curPunc = "newstatement";
+ if (contains(defKeywords, cur)) isDefKeyword = true;
+ return "keyword";
+ }
+ if (contains(types, cur)) return "variable-3";
+ if (contains(builtin, cur)) {
+ if (contains(blockKeywords, cur)) curPunc = "newstatement";
+ return "builtin";
+ }
+ if (contains(atoms, cur)) return "atom";
+ return "variable";
+ }
+
+ function tokenString(quote) {
+ return function(stream, state) {
+ var escaped = false, next, end = false;
+ while ((next = stream.next()) != null) {
+ if (next == quote && !escaped) {end = true; break;}
+ escaped = !escaped && next == "\\";
+ }
+ if (end || !(escaped || multiLineStrings))
+ state.tokenize = null;
+ return "string";
+ };
+ }
+
+ function tokenComment(stream, state) {
+ var maybeEnd = false, ch;
+ while (ch = stream.next()) {
+ if (ch == "/" && maybeEnd) {
+ state.tokenize = null;
+ break;
+ }
+ maybeEnd = (ch == "*");
+ }
+ return "comment";
+ }
+
+ function maybeEOL(stream, state) {
+ if (parserConfig.typeFirstDefinitions && stream.eol() && isTopScope(state.context))
+ state.typeAtEndOfLine = typeBefore(stream, state, stream.pos)
+ }
+
+ // Interface
+
+ return {
+ startState: function(basecolumn) {
+ return {
+ tokenize: null,
+ context: new Context((basecolumn || 0) - indentUnit, 0, "top", null, false),
+ indented: 0,
+ startOfLine: true,
+ prevToken: null
+ };
+ },
+
+ token: function(stream, state) {
+ var ctx = state.context;
+ if (stream.sol()) {
+ if (ctx.align == null) ctx.align = false;
+ state.indented = stream.indentation();
+ state.startOfLine = true;
+ }
+ if (stream.eatSpace()) { maybeEOL(stream, state); return null; }
+ curPunc = isDefKeyword = null;
+ var style = (state.tokenize || tokenBase)(stream, state);
+ if (style == "comment" || style == "meta") return style;
+ if (ctx.align == null) ctx.align = true;
+
+ if (endStatement.test(curPunc)) while (state.context.type == "statement") popContext(state);
+ else if (curPunc == "{") pushContext(state, stream.column(), "}");
+ else if (curPunc == "[") pushContext(state, stream.column(), "]");
+ else if (curPunc == "(") pushContext(state, stream.column(), ")");
+ else if (curPunc == "}") {
+ while (ctx.type == "statement") ctx = popContext(state);
+ if (ctx.type == "}") ctx = popContext(state);
+ while (ctx.type == "statement") ctx = popContext(state);
+ }
+ else if (curPunc == ctx.type) popContext(state);
+ else if (indentStatements &&
+ (((ctx.type == "}" || ctx.type == "top") && curPunc != ";") ||
+ (ctx.type == "statement" && curPunc == "newstatement"))) {
+ pushContext(state, stream.column(), "statement", stream.current());
+ }
+
+ if (style == "variable" &&
+ ((state.prevToken == "def" ||
+ (parserConfig.typeFirstDefinitions && typeBefore(stream, state, stream.start) &&
+ isTopScope(state.context) && stream.match(/^\s*\(/, false)))))
+ style = "def";
+
+ if (hooks.token) {
+ var result = hooks.token(stream, state, style);
+ if (result !== undefined) style = result;
+ }
+
+ if (style == "def" && parserConfig.styleDefs === false) style = "variable";
+
+ state.startOfLine = false;
+ state.prevToken = isDefKeyword ? "def" : style || curPunc;
+ maybeEOL(stream, state);
+ return style;
+ },
+
+ indent: function(state, textAfter) {
+ if (state.tokenize != tokenBase && state.tokenize != null || state.typeAtEndOfLine) return CodeMirror.Pass;
+ var ctx = state.context, firstChar = textAfter && textAfter.charAt(0);
+ if (ctx.type == "statement" && firstChar == "}") ctx = ctx.prev;
+ if (parserConfig.dontIndentStatements)
+ while (ctx.type == "statement" && parserConfig.dontIndentStatements.test(ctx.info))
+ ctx = ctx.prev
+ if (hooks.indent) {
+ var hook = hooks.indent(state, ctx, textAfter);
+ if (typeof hook == "number") return hook
+ }
+ var closing = firstChar == ctx.type;
+ var switchBlock = ctx.prev && ctx.prev.info == "switch";
+ if (parserConfig.allmanIndentation && /[{(]/.test(firstChar)) {
+ while (ctx.type != "top" && ctx.type != "}") ctx = ctx.prev
+ return ctx.indented
+ }
+ if (ctx.type == "statement")
+ return ctx.indented + (firstChar == "{" ? 0 : statementIndentUnit);
+ if (ctx.align && (!dontAlignCalls || ctx.type != ")"))
+ return ctx.column + (closing ? 0 : 1);
+ if (ctx.type == ")" && !closing)
+ return ctx.indented + statementIndentUnit;
+
+ return ctx.indented + (closing ? 0 : indentUnit) +
+ (!closing && switchBlock && !/^(?:case|default)\b/.test(textAfter) ? indentUnit : 0);
+ },
+
+ electricInput: indentSwitch ? /^\s*(?:case .*?:|default:|\{\}?|\})$/ : /^\s*[{}]$/,
+ blockCommentStart: "/*",
+ blockCommentEnd: "*/",
+ lineComment: "//",
+ fold: "brace"
+ };
+ });
+
+ function words(str) {
+ var obj = {}, words = str.split(" ");
+ for (var i = 0; i < words.length; ++i) obj[words[i]] = true;
+ return obj;
+ }
+ function contains(words, word) {
+ if (typeof words === "function") {
+ return words(word);
+ } else {
+ return words.propertyIsEnumerable(word);
+ }
+ }
+ var cKeywords = "auto if break case register continue return default do sizeof " +
+ "static else struct switch extern typedef union for goto while enum const volatile";
+ var cTypes = "int long char short double float unsigned signed void size_t ptrdiff_t";
+
+ function cppHook(stream, state) {
+ if (!state.startOfLine) return false
+ for (var ch, next = null; ch = stream.peek();) {
+ if (ch == "\\" && stream.match(/^.$/)) {
+ next = cppHook
+ break
+ } else if (ch == "/" && stream.match(/^\/[\/\*]/, false)) {
+ break
+ }
+ stream.next()
+ }
+ state.tokenize = next
+ return "meta"
+ }
+
+ function pointerHook(_stream, state) {
+ if (state.prevToken == "variable-3") return "variable-3";
+ return false;
+ }
+
+ function cpp14Literal(stream) {
+ stream.eatWhile(/[\w\.']/);
+ return "number";
+ }
+
+ function cpp11StringHook(stream, state) {
+ stream.backUp(1);
+ // Raw strings.
+ if (stream.match(/(R|u8R|uR|UR|LR)/)) {
+ var match = stream.match(/"([^\s\\()]{0,16})\(/);
+ if (!match) {
+ return false;
+ }
+ state.cpp11RawStringDelim = match[1];
+ state.tokenize = tokenRawString;
+ return tokenRawString(stream, state);
+ }
+ // Unicode strings/chars.
+ if (stream.match(/(u8|u|U|L)/)) {
+ if (stream.match(/["']/, /* eat */ false)) {
+ return "string";
+ }
+ return false;
+ }
+ // Ignore this hook.
+ stream.next();
+ return false;
+ }
+
+ function cppLooksLikeConstructor(word) {
+ var lastTwo = /(\w+)::(\w+)$/.exec(word);
+ return lastTwo && lastTwo[1] == lastTwo[2];
+ }
+
+ // C#-style strings where "" escapes a quote.
+ function tokenAtString(stream, state) {
+ var next;
+ while ((next = stream.next()) != null) {
+ if (next == '"' && !stream.eat('"')) {
+ state.tokenize = null;
+ break;
+ }
+ }
+ return "string";
+ }
+
+ // C++11 raw string literal is <prefix>"<delim>( anything )<delim>", where
+ // <delim> can be a string up to 16 characters long.
+ function tokenRawString(stream, state) {
+ // Escape characters that have special regex meanings.
+ var delim = state.cpp11RawStringDelim.replace(/[^\w\s]/g, '\\$&');
+ var match = stream.match(new RegExp(".*?\\)" + delim + '"'));
+ if (match)
+ state.tokenize = null;
+ else
+ stream.skipToEnd();
+ return "string";
+ }
+
+ function def(mimes, mode) {
+ if (typeof mimes == "string") mimes = [mimes];
+ var words = [];
+ function add(obj) {
+ if (obj) for (var prop in obj) if (obj.hasOwnProperty(prop))
+ words.push(prop);
+ }
+ add(mode.keywords);
+ add(mode.types);
+ add(mode.builtin);
+ add(mode.atoms);
+ if (words.length) {
+ mode.helperType = mimes[0];
+ CodeMirror.registerHelper("hintWords", mimes[0], words);
+ }
+
+ for (var i = 0; i < mimes.length; ++i)
+ CodeMirror.defineMIME(mimes[i], mode);
+ }
+
+ def(["text/x-csrc", "text/x-c", "text/x-chdr"], {
+ name: "clike",
+ keywords: words(cKeywords),
+ types: words(cTypes + " bool _Complex _Bool float_t double_t intptr_t intmax_t " +
+ "int8_t int16_t int32_t int64_t uintptr_t uintmax_t uint8_t uint16_t " +
+ "uint32_t uint64_t"),
+ blockKeywords: words("case do else for if switch while struct"),
+ defKeywords: words("struct"),
+ typeFirstDefinitions: true,
+ atoms: words("null true false"),
+ hooks: {"#": cppHook, "*": pointerHook},
+ modeProps: {fold: ["brace", "include"]}
+ });
+
+ def(["text/x-c++src", "text/x-c++hdr"], {
+ name: "clike",
+ keywords: words(cKeywords + " asm dynamic_cast namespace reinterpret_cast try explicit new " +
+ "static_cast typeid catch operator template typename class friend private " +
+ "this using const_cast inline public throw virtual delete mutable protected " +
+ "alignas alignof constexpr decltype nullptr noexcept thread_local final " +
+ "static_assert override"),
+ types: words(cTypes + " bool wchar_t"),
+ blockKeywords: words("catch class do else finally for if struct switch try while"),
+ defKeywords: words("class namespace struct enum union"),
+ typeFirstDefinitions: true,
+ atoms: words("true false null"),
+ dontIndentStatements: /^template$/,
+ hooks: {
+ "#": cppHook,
+ "*": pointerHook,
+ "u": cpp11StringHook,
+ "U": cpp11StringHook,
+ "L": cpp11StringHook,
+ "R": cpp11StringHook,
+ "0": cpp14Literal,
+ "1": cpp14Literal,
+ "2": cpp14Literal,
+ "3": cpp14Literal,
+ "4": cpp14Literal,
+ "5": cpp14Literal,
+ "6": cpp14Literal,
+ "7": cpp14Literal,
+ "8": cpp14Literal,
+ "9": cpp14Literal,
+ token: function(stream, state, style) {
+ if (style == "variable" && stream.peek() == "(" &&
+ (state.prevToken == ";" || state.prevToken == null ||
+ state.prevToken == "}") &&
+ cppLooksLikeConstructor(stream.current()))
+ return "def";
+ }
+ },
+ namespaceSeparator: "::",
+ modeProps: {fold: ["brace", "include"]}
+ });
+
+ def("text/x-java", {
+ name: "clike",
+ keywords: words("abstract assert break case catch class const continue default " +
+ "do else enum extends final finally float for goto if implements import " +
+ "instanceof interface native new package private protected public " +
+ "return static strictfp super switch synchronized this throw throws transient " +
+ "try volatile while"),
+ types: words("byte short int long float double boolean char void Boolean Byte Character Double Float " +
+ "Integer Long Number Object Short String StringBuffer StringBuilder Void"),
+ blockKeywords: words("catch class do else finally for if switch try while"),
+ defKeywords: words("class interface package enum"),
+ typeFirstDefinitions: true,
+ atoms: words("true false null"),
+ endStatement: /^[;:]$/,
+ number: /^(?:0x[a-f\d_]+|0b[01_]+|(?:[\d_]+\.?\d*|\.\d+)(?:e[-+]?[\d_]+)?)(u|ll?|l|f)?/i,
+ hooks: {
+ "@": function(stream) {
+ stream.eatWhile(/[\w\$_]/);
+ return "meta";
+ }
+ },
+ modeProps: {fold: ["brace", "import"]}
+ });
+
+ def("text/x-csharp", {
+ name: "clike",
+ keywords: words("abstract as async await base break case catch checked class const continue" +
+ " default delegate do else enum event explicit extern finally fixed for" +
+ " foreach goto if implicit in interface internal is lock namespace new" +
+ " operator out override params private protected public readonly ref return sealed" +
+ " sizeof stackalloc static struct switch this throw try typeof unchecked" +
+ " unsafe using virtual void volatile while add alias ascending descending dynamic from get" +
+ " global group into join let orderby partial remove select set value var yield"),
+ types: words("Action Boolean Byte Char DateTime DateTimeOffset Decimal Double Func" +
+ " Guid Int16 Int32 Int64 Object SByte Single String Task TimeSpan UInt16 UInt32" +
+ " UInt64 bool byte char decimal double short int long object" +
+ " sbyte float string ushort uint ulong"),
+ blockKeywords: words("catch class do else finally for foreach if struct switch try while"),
+ defKeywords: words("class interface namespace struct var"),
+ typeFirstDefinitions: true,
+ atoms: words("true false null"),
+ hooks: {
+ "@": function(stream, state) {
+ if (stream.eat('"')) {
+ state.tokenize = tokenAtString;
+ return tokenAtString(stream, state);
+ }
+ stream.eatWhile(/[\w\$_]/);
+ return "meta";
+ }
+ }
+ });
+
+ function tokenTripleString(stream, state) {
+ var escaped = false;
+ while (!stream.eol()) {
+ if (!escaped && stream.match('"""')) {
+ state.tokenize = null;
+ break;
+ }
+ escaped = stream.next() == "\\" && !escaped;
+ }
+ return "string";
+ }
+
+ def("text/x-scala", {
+ name: "clike",
+ keywords: words(
+
+ /* scala */
+ "abstract case catch class def do else extends final finally for forSome if " +
+ "implicit import lazy match new null object override package private protected return " +
+ "sealed super this throw trait try type val var while with yield _ : = => <- <: " +
+ "<% >: # @ " +
+
+ /* package scala */
+ "assert assume require print println printf readLine readBoolean readByte readShort " +
+ "readChar readInt readLong readFloat readDouble " +
+
+ ":: #:: "
+ ),
+ types: words(
+ "AnyVal App Application Array BufferedIterator BigDecimal BigInt Char Console Either " +
+ "Enumeration Equiv Error Exception Fractional Function IndexedSeq Int Integral Iterable " +
+ "Iterator List Map Numeric Nil NotNull Option Ordered Ordering PartialFunction PartialOrdering " +
+ "Product Proxy Range Responder Seq Serializable Set Specializable Stream StringBuilder " +
+ "StringContext Symbol Throwable Traversable TraversableOnce Tuple Unit Vector " +
+
+ /* package java.lang */
+ "Boolean Byte Character CharSequence Class ClassLoader Cloneable Comparable " +
+ "Compiler Double Exception Float Integer Long Math Number Object Package Pair Process " +
+ "Runtime Runnable SecurityManager Short StackTraceElement StrictMath String " +
+ "StringBuffer System Thread ThreadGroup ThreadLocal Throwable Triple Void"
+ ),
+ multiLineStrings: true,
+ blockKeywords: words("catch class do else finally for forSome if match switch try while"),
+ defKeywords: words("class def object package trait type val var"),
+ atoms: words("true false null"),
+ indentStatements: false,
+ indentSwitch: false,
+ hooks: {
+ "@": function(stream) {
+ stream.eatWhile(/[\w\$_]/);
+ return "meta";
+ },
+ '"': function(stream, state) {
+ if (!stream.match('""')) return false;
+ state.tokenize = tokenTripleString;
+ return state.tokenize(stream, state);
+ },
+ "'": function(stream) {
+ stream.eatWhile(/[\w\$_\xa1-\uffff]/);
+ return "atom";
+ },
+ "=": function(stream, state) {
+ var cx = state.context
+ if (cx.type == "}" && cx.align && stream.eat(">")) {
+ state.context = new Context(cx.indented, cx.column, cx.type, cx.info, null, cx.prev)
+ return "operator"
+ } else {
+ return false
+ }
+ }
+ },
+ modeProps: {closeBrackets: {triples: '"'}}
+ });
+
+ function tokenKotlinString(tripleString){
+ return function (stream, state) {
+ var escaped = false, next, end = false;
+ while (!stream.eol()) {
+ if (!tripleString && !escaped && stream.match('"') ) {end = true; break;}
+ if (tripleString && stream.match('"""')) {end = true; break;}
+ next = stream.next();
+ if(!escaped && next == "$" && stream.match('{'))
+ stream.skipTo("}");
+ escaped = !escaped && next == "\\" && !tripleString;
+ }
+ if (end || !tripleString)
+ state.tokenize = null;
+ return "string";
+ }
+ }
+
+ def("text/x-kotlin", {
+ name: "clike",
+ keywords: words(
+ /*keywords*/
+ "package as typealias class interface this super val " +
+ "var fun for is in This throw return " +
+ "break continue object if else while do try when !in !is as? " +
+
+ /*soft keywords*/
+ "file import where by get set abstract enum open inner override private public internal " +
+ "protected catch finally out final vararg reified dynamic companion constructor init " +
+ "sealed field property receiver param sparam lateinit data inline noinline tailrec " +
+ "external annotation crossinline const operator infix"
+ ),
+ types: words(
+ /* package java.lang */
+ "Boolean Byte Character CharSequence Class ClassLoader Cloneable Comparable " +
+ "Compiler Double Exception Float Integer Long Math Number Object Package Pair Process " +
+ "Runtime Runnable SecurityManager Short StackTraceElement StrictMath String " +
+ "StringBuffer System Thread ThreadGroup ThreadLocal Throwable Triple Void"
+ ),
+ intendSwitch: false,
+ indentStatements: false,
+ multiLineStrings: true,
+ blockKeywords: words("catch class do else finally for if where try while enum"),
+ defKeywords: words("class val var object package interface fun"),
+ atoms: words("true false null this"),
+ hooks: {
+ '"': function(stream, state) {
+ state.tokenize = tokenKotlinString(stream.match('""'));
+ return state.tokenize(stream, state);
+ }
+ },
+ modeProps: {closeBrackets: {triples: '"'}}
+ });
+
+ def(["x-shader/x-vertex", "x-shader/x-fragment"], {
+ name: "clike",
+ keywords: words("sampler1D sampler2D sampler3D samplerCube " +
+ "sampler1DShadow sampler2DShadow " +
+ "const attribute uniform varying " +
+ "break continue discard return " +
+ "for while do if else struct " +
+ "in out inout"),
+ types: words("float int bool void " +
+ "vec2 vec3 vec4 ivec2 ivec3 ivec4 bvec2 bvec3 bvec4 " +
+ "mat2 mat3 mat4"),
+ blockKeywords: words("for while do if else struct"),
+ builtin: words("radians degrees sin cos tan asin acos atan " +
+ "pow exp log exp2 sqrt inversesqrt " +
+ "abs sign floor ceil fract mod min max clamp mix step smoothstep " +
+ "length distance dot cross normalize ftransform faceforward " +
+ "reflect refract matrixCompMult " +
+ "lessThan lessThanEqual greaterThan greaterThanEqual " +
+ "equal notEqual any all not " +
+ "texture1D texture1DProj texture1DLod texture1DProjLod " +
+ "texture2D texture2DProj texture2DLod texture2DProjLod " +
+ "texture3D texture3DProj texture3DLod texture3DProjLod " +
+ "textureCube textureCubeLod " +
+ "shadow1D shadow2D shadow1DProj shadow2DProj " +
+ "shadow1DLod shadow2DLod shadow1DProjLod shadow2DProjLod " +
+ "dFdx dFdy fwidth " +
+ "noise1 noise2 noise3 noise4"),
+ atoms: words("true false " +
+ "gl_FragColor gl_SecondaryColor gl_Normal gl_Vertex " +
+ "gl_MultiTexCoord0 gl_MultiTexCoord1 gl_MultiTexCoord2 gl_MultiTexCoord3 " +
+ "gl_MultiTexCoord4 gl_MultiTexCoord5 gl_MultiTexCoord6 gl_MultiTexCoord7 " +
+ "gl_FogCoord gl_PointCoord " +
+ "gl_Position gl_PointSize gl_ClipVertex " +
+ "gl_FrontColor gl_BackColor gl_FrontSecondaryColor gl_BackSecondaryColor " +
+ "gl_TexCoord gl_FogFragCoord " +
+ "gl_FragCoord gl_FrontFacing " +
+ "gl_FragData gl_FragDepth " +
+ "gl_ModelViewMatrix gl_ProjectionMatrix gl_ModelViewProjectionMatrix " +
+ "gl_TextureMatrix gl_NormalMatrix gl_ModelViewMatrixInverse " +
+ "gl_ProjectionMatrixInverse gl_ModelViewProjectionMatrixInverse " +
+ "gl_TexureMatrixTranspose gl_ModelViewMatrixInverseTranspose " +
+ "gl_ProjectionMatrixInverseTranspose " +
+ "gl_ModelViewProjectionMatrixInverseTranspose " +
+ "gl_TextureMatrixInverseTranspose " +
+ "gl_NormalScale gl_DepthRange gl_ClipPlane " +
+ "gl_Point gl_FrontMaterial gl_BackMaterial gl_LightSource gl_LightModel " +
+ "gl_FrontLightModelProduct gl_BackLightModelProduct " +
+ "gl_TextureColor gl_EyePlaneS gl_EyePlaneT gl_EyePlaneR gl_EyePlaneQ " +
+ "gl_FogParameters " +
+ "gl_MaxLights gl_MaxClipPlanes gl_MaxTextureUnits gl_MaxTextureCoords " +
+ "gl_MaxVertexAttribs gl_MaxVertexUniformComponents gl_MaxVaryingFloats " +
+ "gl_MaxVertexTextureImageUnits gl_MaxTextureImageUnits " +
+ "gl_MaxFragmentUniformComponents gl_MaxCombineTextureImageUnits " +
+ "gl_MaxDrawBuffers"),
+ indentSwitch: false,
+ hooks: {"#": cppHook},
+ modeProps: {fold: ["brace", "include"]}
+ });
+
+ def("text/x-nesc", {
+ name: "clike",
+ keywords: words(cKeywords + "as atomic async call command component components configuration event generic " +
+ "implementation includes interface module new norace nx_struct nx_union post provides " +
+ "signal task uses abstract extends"),
+ types: words(cTypes),
+ blockKeywords: words("case do else for if switch while struct"),
+ atoms: words("null true false"),
+ hooks: {"#": cppHook},
+ modeProps: {fold: ["brace", "include"]}
+ });
+
+ def("text/x-objectivec", {
+ name: "clike",
+ keywords: words(cKeywords + "inline restrict _Bool _Complex _Imaginary BOOL Class bycopy byref id IMP in " +
+ "inout nil oneway out Protocol SEL self super atomic nonatomic retain copy readwrite readonly"),
+ types: words(cTypes),
+ atoms: words("YES NO NULL NILL ON OFF true false"),
+ hooks: {
+ "@": function(stream) {
+ stream.eatWhile(/[\w\$]/);
+ return "keyword";
+ },
+ "#": cppHook,
+ indent: function(_state, ctx, textAfter) {
+ if (ctx.type == "statement" && /^@\w/.test(textAfter)) return ctx.indented
+ }
+ },
+ modeProps: {fold: "brace"}
+ });
+
+ def("text/x-squirrel", {
+ name: "clike",
+ keywords: words("base break clone continue const default delete enum extends function in class" +
+ " foreach local resume return this throw typeof yield constructor instanceof static"),
+ types: words(cTypes),
+ blockKeywords: words("case catch class else for foreach if switch try while"),
+ defKeywords: words("function local class"),
+ typeFirstDefinitions: true,
+ atoms: words("true false null"),
+ hooks: {"#": cppHook},
+ modeProps: {fold: ["brace", "include"]}
+ });
+
+ // Ceylon Strings need to deal with interpolation
+ var stringTokenizer = null;
+ function tokenCeylonString(type) {
+ return function(stream, state) {
+ var escaped = false, next, end = false;
+ while (!stream.eol()) {
+ if (!escaped && stream.match('"') &&
+ (type == "single" || stream.match('""'))) {
+ end = true;
+ break;
+ }
+ if (!escaped && stream.match('``')) {
+ stringTokenizer = tokenCeylonString(type);
+ end = true;
+ break;
+ }
+ next = stream.next();
+ escaped = type == "single" && !escaped && next == "\\";
+ }
+ if (end)
+ state.tokenize = null;
+ return "string";
+ }
+ }
+
+ def("text/x-ceylon", {
+ name: "clike",
+ keywords: words("abstracts alias assembly assert assign break case catch class continue dynamic else" +
+ " exists extends finally for function given if import in interface is let module new" +
+ " nonempty object of out outer package return satisfies super switch then this throw" +
+ " try value void while"),
+ types: function(word) {
+ // In Ceylon all identifiers that start with an uppercase are types
+ var first = word.charAt(0);
+ return (first === first.toUpperCase() && first !== first.toLowerCase());
+ },
+ blockKeywords: words("case catch class dynamic else finally for function if interface module new object switch try while"),
+ defKeywords: words("class dynamic function interface module object package value"),
+ builtin: words("abstract actual aliased annotation by default deprecated doc final formal late license" +
+ " native optional sealed see serializable shared suppressWarnings tagged throws variable"),
+ isPunctuationChar: /[\[\]{}\(\),;\:\.`]/,
+ isOperatorChar: /[+\-*&%=<>!?|^~:\/]/,
+ numberStart: /[\d#$]/,
+ number: /^(?:#[\da-fA-F_]+|\$[01_]+|[\d_]+[kMGTPmunpf]?|[\d_]+\.[\d_]+(?:[eE][-+]?\d+|[kMGTPmunpf]|)|)/i,
+ multiLineStrings: true,
+ typeFirstDefinitions: true,
+ atoms: words("true false null larger smaller equal empty finished"),
+ indentSwitch: false,
+ styleDefs: false,
+ hooks: {
+ "@": function(stream) {
+ stream.eatWhile(/[\w\$_]/);
+ return "meta";
+ },
+ '"': function(stream, state) {
+ state.tokenize = tokenCeylonString(stream.match('""') ? "triple" : "single");
+ return state.tokenize(stream, state);
+ },
+ '`': function(stream, state) {
+ if (!stringTokenizer || !stream.match('`')) return false;
+ state.tokenize = stringTokenizer;
+ stringTokenizer = null;
+ return state.tokenize(stream, state);
+ },
+ "'": function(stream) {
+ stream.eatWhile(/[\w\$_\xa1-\uffff]/);
+ return "atom";
+ },
+ token: function(_stream, state, style) {
+ if ((style == "variable" || style == "variable-3") &&
+ state.prevToken == ".") {
+ return "variable-2";
+ }
+ }
+ },
+ modeProps: {
+ fold: ["brace", "import"],
+ closeBrackets: {triples: '"'}
+ }
+ });
+
+ });
+
+
+/***/ },
+/* 13 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/* 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/. */
+
+ // WebAssembly experimental syntax highlight add-on for CodeMirror.
+
+ (function (root, factory) {
+ if (true) {
+ !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(2)], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+ } else if (typeof exports !== 'undefined') {
+ factory(require("../../lib/codemirror"));
+ } else {
+ factory(root.CodeMirror);
+ }
+ }(this, function (CodeMirror) {
+ "use strict";
+
+ var isWordChar = /[\w\$_\.\\\/@]/;
+
+ function createLookupTable(list) {
+ var obj = Object.create(null);
+ list.forEach(function (key) {
+ obj[key] = true;
+ });
+ return obj;
+ }
+
+ CodeMirror.defineMode("wasm", function() {
+ var keywords = createLookupTable([
+ "function", "import", "export", "table", "memory", "segment", "as", "type",
+ "of", "from", "typeof", "br", "br_if", "loop", "br_table", "if", "else",
+ "call", "call_import", "call_indirect", "nop", "unreachable", "var",
+ "align", "select", "return"]);
+ var builtins = createLookupTable([
+ "i32:8", "i32:8u", "i32:8s", "i32:16", "i32:16u", "i32:16s",
+ "i64:8", "i64:8u", "i64:8s", "i64:16", "i64:16u", "i64:16s",
+ "i64:32", "i64:32u", "i64:32s",
+ "i32.add", "i32.sub", "i32.mul", "i32.div_s", "i32.div_u",
+ "i32.rem_s", "i32.rem_u", "i32.and", "i32.or", "i32.xor",
+ "i32.shl", "i32.shr_u", "i32.shr_s", "i32.rotr", "i32.rotl",
+ "i32.eq", "i32.ne", "i32.lt_s", "i32.le_s", "i32.lt_u",
+ "i32.le_u", "i32.gt_s", "i32.ge_s", "i32.gt_u", "i32.ge_u",
+ "i32.clz", "i32.ctz", "i32.popcnt", "i32.eqz", "i64.add",
+ "i64.sub", "i64.mul", "i64.div_s", "i64.div_u", "i64.rem_s",
+ "i64.rem_u", "i64.and", "i64.or", "i64.xor", "i64.shl",
+ "i64.shr_u", "i64.shr_s", "i64.rotr", "i64.rotl", "i64.eq",
+ "i64.ne", "i64.lt_s", "i64.le_s", "i64.lt_u", "i64.le_u",
+ "i64.gt_s", "i64.ge_s", "i64.gt_u", "i64.ge_u", "i64.clz",
+ "i64.ctz", "i64.popcnt", "i64.eqz", "f32.add", "f32.sub",
+ "f32.mul", "f32.div", "f32.min", "f32.max", "f32.abs",
+ "f32.neg", "f32.copysign", "f32.ceil", "f32.floor", "f32.trunc",
+ "f32.nearest", "f32.sqrt", "f32.eq", "f32.ne", "f32.lt",
+ "f32.le", "f32.gt", "f32.ge", "f64.add", "f64.sub", "f64.mul",
+ "f64.div", "f64.min", "f64.max", "f64.abs", "f64.neg",
+ "f64.copysign", "f64.ceil", "f64.floor", "f64.trunc", "f64.nearest",
+ "f64.sqrt", "f64.eq", "f64.ne", "f64.lt", "f64.le", "f64.gt",
+ "f64.ge", "i32.trunc_s/f32", "i32.trunc_s/f64", "i32.trunc_u/f32",
+ "i32.trunc_u/f64", "i32.wrap/i64", "i64.trunc_s/f32",
+ "i64.trunc_s/f64", "i64.trunc_u/f32", "i64.trunc_u/f64",
+ "i64.extend_s/i32", "i64.extend_u/i32", "f32.convert_s/i32",
+ "f32.convert_u/i32", "f32.convert_s/i64", "f32.convert_u/i64",
+ "f32.demote/f64", "f32.reinterpret/i32", "f64.convert_s/i32",
+ "f64.convert_u/i32", "f64.convert_s/i64", "f64.convert_u/i64",
+ "f64.promote/f32", "f64.reinterpret/i64", "i32.reinterpret/f32",
+ "i64.reinterpret/f64"]);
+ var dataTypes = createLookupTable(["i32", "i64", "f32", "f64"]);
+ var isUnaryOperator = /[\-!]/;
+ var operators = createLookupTable([
+ "+", "-", "*", "/", "/s", "/u", "%", "%s", "%u",
+ "<<", ">>u", ">>s", ">=", "<=", "==", "!=",
+ "<s", "<u", "<=s", "<=u", ">=s", ">=u", ">s", ">u",
+ "<", ">", "=", "&", "|", "^", "!"]);
+
+ function tokenBase(stream, state) {
+ var ch = stream.next();
+ if (ch === "$") {
+ stream.eatWhile(isWordChar);
+ return "variable";
+ }
+ if (ch === "@") {
+ stream.eatWhile(isWordChar);
+ return "meta";
+ }
+ if (ch === '"') {
+ state.tokenize = tokenString(ch);
+ return state.tokenize(stream, state);
+ }
+ if (ch == "/") {
+ if (stream.eat("*")) {
+ state.tokenize = tokenComment;
+ return tokenComment(stream, state);
+ } else if (stream.eat("/")) {
+ stream.skipToEnd();
+ return "comment";
+ }
+ }
+ if (/\d/.test(ch) ||
+ ((ch === "-" || ch === "+") && /\d/.test(stream.peek()))) {
+ stream.eatWhile(/[\w\._\-+]/);
+ return "number";
+ }
+ if (/[\[\]\(\)\{\},:]/.test(ch)) {
+ return null;
+ }
+ if (isUnaryOperator.test(ch)) {
+ return "operator";
+ }
+ stream.eatWhile(isWordChar);
+ var word = stream.current();
+
+ if (word in operators) {
+ return "operator";
+ }
+ if (word in keywords){
+ return "keyword";
+ }
+ if (word in dataTypes) {
+ if (!stream.eat(":")) {
+ return "builtin";
+ }
+ stream.eatWhile(isWordChar);
+ word = stream.current();
+ // fall thru for "builtin" check
+ }
+ if (word in builtins) {
+ return "builtin";
+ }
+
+ if (word === "Temporary") {
+ // Nightly has header with some text graphics -- skipping it.
+ state.tokenize = tokenTemporary;
+ return state.tokenize(stream, state);
+ }
+ return null;
+ }
+
+ function tokenComment(stream, state) {
+ state.commentDepth = 1;
+ var next;
+ while ((next = stream.next()) != null) {
+ if (next === "*" && stream.eat("/")) {
+ if (--state.commentDepth === 0) {
+ state.tokenize = null;
+ return "comment";
+ }
+ }
+ if (next === "/" && stream.eat("*")) {
+ // Nested comment
+ state.commentDepth++;
+ }
+ }
+ return "comment";
+ }
+
+ function tokenTemporary(stream, state) {
+ var next, endState = state.commentState;
+ // Skipping until "text support (Work In Progress):" is found.
+ while ((next = stream.next()) != null) {
+ if (endState === 0 && next === "t") {
+ endState = 1;
+ } else if (endState === 1 && next === ":") {
+ state.tokenize = null;
+ state.commentState = 0;
+ endState = 2;
+ return "comment";
+ }
+ }
+ state.commentState = endState;
+ return "comment";
+ }
+
+ function tokenString(quote) {
+ return function(stream, state) {
+ var escaped = false, next, end = false;
+ while ((next = stream.next()) != null) {
+ if (next == quote && !escaped) {
+ state.tokenize = null;
+ return "string";
+ }
+ escaped = !escaped && next === "\\";
+ }
+ return "string";
+ };
+ }
+
+ return {
+ startState: function() {
+ return {tokenize: null, commentState: 0, commentDepth: 0};
+ },
+
+ token: function(stream, state) {
+ if (stream.eatSpace()) return null;
+ var style = (state.tokenize || tokenBase)(stream, state);
+ return style;
+ }
+ };
+ });
+
+ CodeMirror.registerHelper("wordChars", "wasm", isWordChar);
+
+ CodeMirror.defineMIME("text/wasm", "wasm");
+
+ }));
+
+
+/***/ },
+/* 14 */
+/***/ function(module, exports, __webpack_require__) {
+
+ // CodeMirror, copyright (c) by Marijn Haverbeke and others
+ // Distributed under an MIT license: http://codemirror.net/LICENSE
+
+ // Because sometimes you need to style the cursor's line.
+ //
+ // Adds an option 'styleActiveLine' which, when enabled, gives the
+ // active line's wrapping <div> the CSS class "CodeMirror-activeline",
+ // and gives its background <div> the class "CodeMirror-activeline-background".
+
+ (function(mod) {
+ if (true) // CommonJS
+ mod(__webpack_require__(2));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+ })(function(CodeMirror) {
+ "use strict";
+ var WRAP_CLASS = "CodeMirror-activeline";
+ var BACK_CLASS = "CodeMirror-activeline-background";
+ var GUTT_CLASS = "CodeMirror-activeline-gutter";
+
+ CodeMirror.defineOption("styleActiveLine", false, function(cm, val, old) {
+ var prev = old && old != CodeMirror.Init;
+ if (val && !prev) {
+ cm.state.activeLines = [];
+ updateActiveLines(cm, cm.listSelections());
+ cm.on("beforeSelectionChange", selectionChange);
+ } else if (!val && prev) {
+ cm.off("beforeSelectionChange", selectionChange);
+ clearActiveLines(cm);
+ delete cm.state.activeLines;
+ }
+ });
+
+ function clearActiveLines(cm) {
+ for (var i = 0; i < cm.state.activeLines.length; i++) {
+ cm.removeLineClass(cm.state.activeLines[i], "wrap", WRAP_CLASS);
+ cm.removeLineClass(cm.state.activeLines[i], "background", BACK_CLASS);
+ cm.removeLineClass(cm.state.activeLines[i], "gutter", GUTT_CLASS);
+ }
+ }
+
+ function sameArray(a, b) {
+ if (a.length != b.length) return false;
+ for (var i = 0; i < a.length; i++)
+ if (a[i] != b[i]) return false;
+ return true;
+ }
+
+ function updateActiveLines(cm, ranges) {
+ var active = [];
+ for (var i = 0; i < ranges.length; i++) {
+ var range = ranges[i];
+ if (!range.empty()) continue;
+ var line = cm.getLineHandleVisualStart(range.head.line);
+ if (active[active.length - 1] != line) active.push(line);
+ }
+ if (sameArray(cm.state.activeLines, active)) return;
+ cm.operation(function() {
+ clearActiveLines(cm);
+ for (var i = 0; i < active.length; i++) {
+ cm.addLineClass(active[i], "wrap", WRAP_CLASS);
+ cm.addLineClass(active[i], "background", BACK_CLASS);
+ cm.addLineClass(active[i], "gutter", GUTT_CLASS);
+ }
+ cm.state.activeLines = active;
+ });
+ }
+
+ function selectionChange(cm, sel) {
+ updateActiveLines(cm, sel.ranges);
+ }
+ });
+
+
+/***/ },
+/* 15 */
+/***/ function(module, exports, __webpack_require__) {
+
+ // CodeMirror, copyright (c) by Marijn Haverbeke and others
+ // Distributed under an MIT license: http://codemirror.net/LICENSE
+
+ (function(mod) {
+ if (true) // CommonJS
+ mod(__webpack_require__(2));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+ })(function(CodeMirror) {
+ CodeMirror.defineOption("showTrailingSpace", false, function(cm, val, prev) {
+ if (prev == CodeMirror.Init) prev = false;
+ if (prev && !val)
+ cm.removeOverlay("trailingspace");
+ else if (!prev && val)
+ cm.addOverlay({
+ token: function(stream) {
+ for (var l = stream.string.length, i = l; i && /\s/.test(stream.string.charAt(i - 1)); --i) {}
+ if (i > stream.pos) { stream.pos = i; return null; }
+ stream.pos = l;
+ return "trailingspace";
+ },
+ name: "trailingspace"
+ });
+ });
+ });
+
+
+/***/ },
+/* 16 */
+/***/ function(module, exports, __webpack_require__) {
+
+ // CodeMirror, copyright (c) by Marijn Haverbeke and others
+ // Distributed under an MIT license: http://codemirror.net/LICENSE
+
+ (function(mod) {
+ if (true) // CommonJS
+ mod(__webpack_require__(2));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+ })(function(CodeMirror) {
+ "use strict";
+
+ var Pos = CodeMirror.Pos;
+ function posEq(a, b) { return a.line == b.line && a.ch == b.ch; }
+
+ // Kill 'ring'
+
+ var killRing = [];
+ function addToRing(str) {
+ killRing.push(str);
+ if (killRing.length > 50) killRing.shift();
+ }
+ function growRingTop(str) {
+ if (!killRing.length) return addToRing(str);
+ killRing[killRing.length - 1] += str;
+ }
+ function getFromRing(n) { return killRing[killRing.length - (n ? Math.min(n, 1) : 1)] || ""; }
+ function popFromRing() { if (killRing.length > 1) killRing.pop(); return getFromRing(); }
+
+ var lastKill = null;
+
+ function kill(cm, from, to, mayGrow, text) {
+ if (text == null) text = cm.getRange(from, to);
+
+ if (mayGrow && lastKill && lastKill.cm == cm && posEq(from, lastKill.pos) && cm.isClean(lastKill.gen))
+ growRingTop(text);
+ else
+ addToRing(text);
+ cm.replaceRange("", from, to, "+delete");
+
+ if (mayGrow) lastKill = {cm: cm, pos: from, gen: cm.changeGeneration()};
+ else lastKill = null;
+ }
+
+ // Boundaries of various units
+
+ function byChar(cm, pos, dir) {
+ return cm.findPosH(pos, dir, "char", true);
+ }
+
+ function byWord(cm, pos, dir) {
+ return cm.findPosH(pos, dir, "word", true);
+ }
+
+ function byLine(cm, pos, dir) {
+ return cm.findPosV(pos, dir, "line", cm.doc.sel.goalColumn);
+ }
+
+ function byPage(cm, pos, dir) {
+ return cm.findPosV(pos, dir, "page", cm.doc.sel.goalColumn);
+ }
+
+ function byParagraph(cm, pos, dir) {
+ var no = pos.line, line = cm.getLine(no);
+ var sawText = /\S/.test(dir < 0 ? line.slice(0, pos.ch) : line.slice(pos.ch));
+ var fst = cm.firstLine(), lst = cm.lastLine();
+ for (;;) {
+ no += dir;
+ if (no < fst || no > lst)
+ return cm.clipPos(Pos(no - dir, dir < 0 ? 0 : null));
+ line = cm.getLine(no);
+ var hasText = /\S/.test(line);
+ if (hasText) sawText = true;
+ else if (sawText) return Pos(no, 0);
+ }
+ }
+
+ function bySentence(cm, pos, dir) {
+ var line = pos.line, ch = pos.ch;
+ var text = cm.getLine(pos.line), sawWord = false;
+ for (;;) {
+ var next = text.charAt(ch + (dir < 0 ? -1 : 0));
+ if (!next) { // End/beginning of line reached
+ if (line == (dir < 0 ? cm.firstLine() : cm.lastLine())) return Pos(line, ch);
+ text = cm.getLine(line + dir);
+ if (!/\S/.test(text)) return Pos(line, ch);
+ line += dir;
+ ch = dir < 0 ? text.length : 0;
+ continue;
+ }
+ if (sawWord && /[!?.]/.test(next)) return Pos(line, ch + (dir > 0 ? 1 : 0));
+ if (!sawWord) sawWord = /\w/.test(next);
+ ch += dir;
+ }
+ }
+
+ function byExpr(cm, pos, dir) {
+ var wrap;
+ if (cm.findMatchingBracket && (wrap = cm.findMatchingBracket(pos, true))
+ && wrap.match && (wrap.forward ? 1 : -1) == dir)
+ return dir > 0 ? Pos(wrap.to.line, wrap.to.ch + 1) : wrap.to;
+
+ for (var first = true;; first = false) {
+ var token = cm.getTokenAt(pos);
+ var after = Pos(pos.line, dir < 0 ? token.start : token.end);
+ if (first && dir > 0 && token.end == pos.ch || !/\w/.test(token.string)) {
+ var newPos = cm.findPosH(after, dir, "char");
+ if (posEq(after, newPos)) return pos;
+ else pos = newPos;
+ } else {
+ return after;
+ }
+ }
+ }
+
+ // Prefixes (only crudely supported)
+
+ function getPrefix(cm, precise) {
+ var digits = cm.state.emacsPrefix;
+ if (!digits) return precise ? null : 1;
+ clearPrefix(cm);
+ return digits == "-" ? -1 : Number(digits);
+ }
+
+ function repeated(cmd) {
+ var f = typeof cmd == "string" ? function(cm) { cm.execCommand(cmd); } : cmd;
+ return function(cm) {
+ var prefix = getPrefix(cm);
+ f(cm);
+ for (var i = 1; i < prefix; ++i) f(cm);
+ };
+ }
+
+ function findEnd(cm, pos, by, dir) {
+ var prefix = getPrefix(cm);
+ if (prefix < 0) { dir = -dir; prefix = -prefix; }
+ for (var i = 0; i < prefix; ++i) {
+ var newPos = by(cm, pos, dir);
+ if (posEq(newPos, pos)) break;
+ pos = newPos;
+ }
+ return pos;
+ }
+
+ function move(by, dir) {
+ var f = function(cm) {
+ cm.extendSelection(findEnd(cm, cm.getCursor(), by, dir));
+ };
+ f.motion = true;
+ return f;
+ }
+
+ function killTo(cm, by, dir) {
+ var selections = cm.listSelections(), cursor;
+ var i = selections.length;
+ while (i--) {
+ cursor = selections[i].head;
+ kill(cm, cursor, findEnd(cm, cursor, by, dir), true);
+ }
+ }
+
+ function killRegion(cm) {
+ if (cm.somethingSelected()) {
+ var selections = cm.listSelections(), selection;
+ var i = selections.length;
+ while (i--) {
+ selection = selections[i];
+ kill(cm, selection.anchor, selection.head);
+ }
+ return true;
+ }
+ }
+
+ function addPrefix(cm, digit) {
+ if (cm.state.emacsPrefix) {
+ if (digit != "-") cm.state.emacsPrefix += digit;
+ return;
+ }
+ // Not active yet
+ cm.state.emacsPrefix = digit;
+ cm.on("keyHandled", maybeClearPrefix);
+ cm.on("inputRead", maybeDuplicateInput);
+ }
+
+ var prefixPreservingKeys = {"Alt-G": true, "Ctrl-X": true, "Ctrl-Q": true, "Ctrl-U": true};
+
+ function maybeClearPrefix(cm, arg) {
+ if (!cm.state.emacsPrefixMap && !prefixPreservingKeys.hasOwnProperty(arg))
+ clearPrefix(cm);
+ }
+
+ function clearPrefix(cm) {
+ cm.state.emacsPrefix = null;
+ cm.off("keyHandled", maybeClearPrefix);
+ cm.off("inputRead", maybeDuplicateInput);
+ }
+
+ function maybeDuplicateInput(cm, event) {
+ var dup = getPrefix(cm);
+ if (dup > 1 && event.origin == "+input") {
+ var one = event.text.join("\n"), txt = "";
+ for (var i = 1; i < dup; ++i) txt += one;
+ cm.replaceSelection(txt);
+ }
+ }
+
+ function addPrefixMap(cm) {
+ cm.state.emacsPrefixMap = true;
+ cm.addKeyMap(prefixMap);
+ cm.on("keyHandled", maybeRemovePrefixMap);
+ cm.on("inputRead", maybeRemovePrefixMap);
+ }
+
+ function maybeRemovePrefixMap(cm, arg) {
+ if (typeof arg == "string" && (/^\d$/.test(arg) || arg == "Ctrl-U")) return;
+ cm.removeKeyMap(prefixMap);
+ cm.state.emacsPrefixMap = false;
+ cm.off("keyHandled", maybeRemovePrefixMap);
+ cm.off("inputRead", maybeRemovePrefixMap);
+ }
+
+ // Utilities
+
+ function setMark(cm) {
+ cm.setCursor(cm.getCursor());
+ cm.setExtending(!cm.getExtending());
+ cm.on("change", function() { cm.setExtending(false); });
+ }
+
+ function clearMark(cm) {
+ cm.setExtending(false);
+ cm.setCursor(cm.getCursor());
+ }
+
+ function getInput(cm, msg, f) {
+ if (cm.openDialog)
+ cm.openDialog(msg + ": <input type=\"text\" style=\"width: 10em\"/>", f, {bottom: true});
+ else
+ f(prompt(msg, ""));
+ }
+
+ function operateOnWord(cm, op) {
+ var start = cm.getCursor(), end = cm.findPosH(start, 1, "word");
+ cm.replaceRange(op(cm.getRange(start, end)), start, end);
+ cm.setCursor(end);
+ }
+
+ function toEnclosingExpr(cm) {
+ var pos = cm.getCursor(), line = pos.line, ch = pos.ch;
+ var stack = [];
+ while (line >= cm.firstLine()) {
+ var text = cm.getLine(line);
+ for (var i = ch == null ? text.length : ch; i > 0;) {
+ var ch = text.charAt(--i);
+ if (ch == ")")
+ stack.push("(");
+ else if (ch == "]")
+ stack.push("[");
+ else if (ch == "}")
+ stack.push("{");
+ else if (/[\(\{\[]/.test(ch) && (!stack.length || stack.pop() != ch))
+ return cm.extendSelection(Pos(line, i));
+ }
+ --line; ch = null;
+ }
+ }
+
+ function quit(cm) {
+ cm.execCommand("clearSearch");
+ clearMark(cm);
+ }
+
+ // Actual keymap
+
+ var keyMap = CodeMirror.keyMap.emacs = CodeMirror.normalizeKeyMap({
+ "Ctrl-W": function(cm) {kill(cm, cm.getCursor("start"), cm.getCursor("end"));},
+ "Ctrl-K": repeated(function(cm) {
+ var start = cm.getCursor(), end = cm.clipPos(Pos(start.line));
+ var text = cm.getRange(start, end);
+ if (!/\S/.test(text)) {
+ text += "\n";
+ end = Pos(start.line + 1, 0);
+ }
+ kill(cm, start, end, true, text);
+ }),
+ "Alt-W": function(cm) {
+ addToRing(cm.getSelection());
+ clearMark(cm);
+ },
+ "Ctrl-Y": function(cm) {
+ var start = cm.getCursor();
+ cm.replaceRange(getFromRing(getPrefix(cm)), start, start, "paste");
+ cm.setSelection(start, cm.getCursor());
+ },
+ "Alt-Y": function(cm) {cm.replaceSelection(popFromRing(), "around", "paste");},
+
+ "Ctrl-Space": setMark, "Ctrl-Shift-2": setMark,
+
+ "Ctrl-F": move(byChar, 1), "Ctrl-B": move(byChar, -1),
+ "Right": move(byChar, 1), "Left": move(byChar, -1),
+ "Ctrl-D": function(cm) { killTo(cm, byChar, 1); },
+ "Delete": function(cm) { killRegion(cm) || killTo(cm, byChar, 1); },
+ "Ctrl-H": function(cm) { killTo(cm, byChar, -1); },
+ "Backspace": function(cm) { killRegion(cm) || killTo(cm, byChar, -1); },
+
+ "Alt-F": move(byWord, 1), "Alt-B": move(byWord, -1),
+ "Alt-D": function(cm) { killTo(cm, byWord, 1); },
+ "Alt-Backspace": function(cm) { killTo(cm, byWord, -1); },
+
+ "Ctrl-N": move(byLine, 1), "Ctrl-P": move(byLine, -1),
+ "Down": move(byLine, 1), "Up": move(byLine, -1),
+ "Ctrl-A": "goLineStart", "Ctrl-E": "goLineEnd",
+ "End": "goLineEnd", "Home": "goLineStart",
+
+ "Alt-V": move(byPage, -1), "Ctrl-V": move(byPage, 1),
+ "PageUp": move(byPage, -1), "PageDown": move(byPage, 1),
+
+ "Ctrl-Up": move(byParagraph, -1), "Ctrl-Down": move(byParagraph, 1),
+
+ "Alt-A": move(bySentence, -1), "Alt-E": move(bySentence, 1),
+ "Alt-K": function(cm) { killTo(cm, bySentence, 1); },
+
+ "Ctrl-Alt-K": function(cm) { killTo(cm, byExpr, 1); },
+ "Ctrl-Alt-Backspace": function(cm) { killTo(cm, byExpr, -1); },
+ "Ctrl-Alt-F": move(byExpr, 1), "Ctrl-Alt-B": move(byExpr, -1),
+
+ "Shift-Ctrl-Alt-2": function(cm) {
+ var cursor = cm.getCursor();
+ cm.setSelection(findEnd(cm, cursor, byExpr, 1), cursor);
+ },
+ "Ctrl-Alt-T": function(cm) {
+ var leftStart = byExpr(cm, cm.getCursor(), -1), leftEnd = byExpr(cm, leftStart, 1);
+ var rightEnd = byExpr(cm, leftEnd, 1), rightStart = byExpr(cm, rightEnd, -1);
+ cm.replaceRange(cm.getRange(rightStart, rightEnd) + cm.getRange(leftEnd, rightStart) +
+ cm.getRange(leftStart, leftEnd), leftStart, rightEnd);
+ },
+ "Ctrl-Alt-U": repeated(toEnclosingExpr),
+
+ "Alt-Space": function(cm) {
+ var pos = cm.getCursor(), from = pos.ch, to = pos.ch, text = cm.getLine(pos.line);
+ while (from && /\s/.test(text.charAt(from - 1))) --from;
+ while (to < text.length && /\s/.test(text.charAt(to))) ++to;
+ cm.replaceRange(" ", Pos(pos.line, from), Pos(pos.line, to));
+ },
+ "Ctrl-O": repeated(function(cm) { cm.replaceSelection("\n", "start"); }),
+ "Ctrl-T": repeated(function(cm) {
+ cm.execCommand("transposeChars");
+ }),
+
+ "Alt-C": repeated(function(cm) {
+ operateOnWord(cm, function(w) {
+ var letter = w.search(/\w/);
+ if (letter == -1) return w;
+ return w.slice(0, letter) + w.charAt(letter).toUpperCase() + w.slice(letter + 1).toLowerCase();
+ });
+ }),
+ "Alt-U": repeated(function(cm) {
+ operateOnWord(cm, function(w) { return w.toUpperCase(); });
+ }),
+ "Alt-L": repeated(function(cm) {
+ operateOnWord(cm, function(w) { return w.toLowerCase(); });
+ }),
+
+ "Alt-;": "toggleComment",
+
+ "Ctrl-/": repeated("undo"), "Shift-Ctrl--": repeated("undo"),
+ "Ctrl-Z": repeated("undo"), "Cmd-Z": repeated("undo"),
+ "Shift-Alt-,": "goDocStart", "Shift-Alt-.": "goDocEnd",
+ "Ctrl-S": "findNext", "Ctrl-R": "findPrev", "Ctrl-G": quit, "Shift-Alt-5": "replace",
+ "Alt-/": "autocomplete",
+ "Ctrl-J": "newlineAndIndent", "Enter": false, "Tab": "indentAuto",
+
+ "Alt-G G": function(cm) {
+ var prefix = getPrefix(cm, true);
+ if (prefix != null && prefix > 0) return cm.setCursor(prefix - 1);
+
+ getInput(cm, "Goto line", function(str) {
+ var num;
+ if (str && !isNaN(num = Number(str)) && num == (num|0) && num > 0)
+ cm.setCursor(num - 1);
+ });
+ },
+
+ "Ctrl-X Tab": function(cm) {
+ cm.indentSelection(getPrefix(cm, true) || cm.getOption("indentUnit"));
+ },
+ "Ctrl-X Ctrl-X": function(cm) {
+ cm.setSelection(cm.getCursor("head"), cm.getCursor("anchor"));
+ },
+ "Ctrl-X Ctrl-S": "save",
+ "Ctrl-X Ctrl-W": "save",
+ "Ctrl-X S": "saveAll",
+ "Ctrl-X F": "open",
+ "Ctrl-X U": repeated("undo"),
+ "Ctrl-X K": "close",
+ "Ctrl-X Delete": function(cm) { kill(cm, cm.getCursor(), bySentence(cm, cm.getCursor(), 1), true); },
+ "Ctrl-X H": "selectAll",
+
+ "Ctrl-Q Tab": repeated("insertTab"),
+ "Ctrl-U": addPrefixMap
+ });
+
+ var prefixMap = {"Ctrl-G": clearPrefix};
+ function regPrefix(d) {
+ prefixMap[d] = function(cm) { addPrefix(cm, d); };
+ keyMap["Ctrl-" + d] = function(cm) { addPrefix(cm, d); };
+ prefixPreservingKeys["Ctrl-" + d] = true;
+ }
+ for (var i = 0; i < 10; ++i) regPrefix(String(i));
+ regPrefix("-");
+ });
+
+
+/***/ },
+/* 17 */
+/***/ function(module, exports, __webpack_require__) {
+
+ // CodeMirror, copyright (c) by Marijn Haverbeke and others
+ // Distributed under an MIT license: http://codemirror.net/LICENSE
+
+ /**
+ * Supported keybindings:
+ * Too many to list. Refer to defaultKeyMap below.
+ *
+ * Supported Ex commands:
+ * Refer to defaultExCommandMap below.
+ *
+ * Registers: unnamed, -, a-z, A-Z, 0-9
+ * (Does not respect the special case for number registers when delete
+ * operator is made with these commands: %, (, ), , /, ?, n, N, {, } )
+ * TODO: Implement the remaining registers.
+ *
+ * Marks: a-z, A-Z, and 0-9
+ * TODO: Implement the remaining special marks. They have more complex
+ * behavior.
+ *
+ * Events:
+ * 'vim-mode-change' - raised on the editor anytime the current mode changes,
+ * Event object: {mode: "visual", subMode: "linewise"}
+ *
+ * Code structure:
+ * 1. Default keymap
+ * 2. Variable declarations and short basic helpers
+ * 3. Instance (External API) implementation
+ * 4. Internal state tracking objects (input state, counter) implementation
+ * and instantiation
+ * 5. Key handler (the main command dispatcher) implementation
+ * 6. Motion, operator, and action implementations
+ * 7. Helper functions for the key handler, motions, operators, and actions
+ * 8. Set up Vim to work as a keymap for CodeMirror.
+ * 9. Ex command implementations.
+ */
+
+ (function(mod) {
+ if (true) // CommonJS
+ mod(__webpack_require__(2), __webpack_require__(3), __webpack_require__(1), __webpack_require__(5));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../lib/codemirror", "../addon/search/searchcursor", "../addon/dialog/dialog", "../addon/edit/matchbrackets"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+ })(function(CodeMirror) {
+ 'use strict';
+
+ var defaultKeymap = [
+ // Key to key mapping. This goes first to make it possible to override
+ // existing mappings.
+ { keys: '<Left>', type: 'keyToKey', toKeys: 'h' },
+ { keys: '<Right>', type: 'keyToKey', toKeys: 'l' },
+ { keys: '<Up>', type: 'keyToKey', toKeys: 'k' },
+ { keys: '<Down>', type: 'keyToKey', toKeys: 'j' },
+ { keys: '<Space>', type: 'keyToKey', toKeys: 'l' },
+ { keys: '<BS>', type: 'keyToKey', toKeys: 'h', context: 'normal'},
+ { keys: '<C-Space>', type: 'keyToKey', toKeys: 'W' },
+ { keys: '<C-BS>', type: 'keyToKey', toKeys: 'B', context: 'normal' },
+ { keys: '<S-Space>', type: 'keyToKey', toKeys: 'w' },
+ { keys: '<S-BS>', type: 'keyToKey', toKeys: 'b', context: 'normal' },
+ { keys: '<C-n>', type: 'keyToKey', toKeys: 'j' },
+ { keys: '<C-p>', type: 'keyToKey', toKeys: 'k' },
+ { keys: '<C-[>', type: 'keyToKey', toKeys: '<Esc>' },
+ { keys: '<C-c>', type: 'keyToKey', toKeys: '<Esc>' },
+ { keys: '<C-[>', type: 'keyToKey', toKeys: '<Esc>', context: 'insert' },
+ { keys: '<C-c>', type: 'keyToKey', toKeys: '<Esc>', context: 'insert' },
+ { keys: 's', type: 'keyToKey', toKeys: 'cl', context: 'normal' },
+ { keys: 's', type: 'keyToKey', toKeys: 'c', context: 'visual'},
+ { keys: 'S', type: 'keyToKey', toKeys: 'cc', context: 'normal' },
+ { keys: 'S', type: 'keyToKey', toKeys: 'VdO', context: 'visual' },
+ { keys: '<Home>', type: 'keyToKey', toKeys: '0' },
+ { keys: '<End>', type: 'keyToKey', toKeys: '$' },
+ { keys: '<PageUp>', type: 'keyToKey', toKeys: '<C-b>' },
+ { keys: '<PageDown>', type: 'keyToKey', toKeys: '<C-f>' },
+ { keys: '<CR>', type: 'keyToKey', toKeys: 'j^', context: 'normal' },
+ // Motions
+ { keys: 'H', type: 'motion', motion: 'moveToTopLine', motionArgs: { linewise: true, toJumplist: true }},
+ { keys: 'M', type: 'motion', motion: 'moveToMiddleLine', motionArgs: { linewise: true, toJumplist: true }},
+ { keys: 'L', type: 'motion', motion: 'moveToBottomLine', motionArgs: { linewise: true, toJumplist: true }},
+ { keys: 'h', type: 'motion', motion: 'moveByCharacters', motionArgs: { forward: false }},
+ { keys: 'l', type: 'motion', motion: 'moveByCharacters', motionArgs: { forward: true }},
+ { keys: 'j', type: 'motion', motion: 'moveByLines', motionArgs: { forward: true, linewise: true }},
+ { keys: 'k', type: 'motion', motion: 'moveByLines', motionArgs: { forward: false, linewise: true }},
+ { keys: 'gj', type: 'motion', motion: 'moveByDisplayLines', motionArgs: { forward: true }},
+ { keys: 'gk', type: 'motion', motion: 'moveByDisplayLines', motionArgs: { forward: false }},
+ { keys: 'w', type: 'motion', motion: 'moveByWords', motionArgs: { forward: true, wordEnd: false }},
+ { keys: 'W', type: 'motion', motion: 'moveByWords', motionArgs: { forward: true, wordEnd: false, bigWord: true }},
+ { keys: 'e', type: 'motion', motion: 'moveByWords', motionArgs: { forward: true, wordEnd: true, inclusive: true }},
+ { keys: 'E', type: 'motion', motion: 'moveByWords', motionArgs: { forward: true, wordEnd: true, bigWord: true, inclusive: true }},
+ { keys: 'b', type: 'motion', motion: 'moveByWords', motionArgs: { forward: false, wordEnd: false }},
+ { keys: 'B', type: 'motion', motion: 'moveByWords', motionArgs: { forward: false, wordEnd: false, bigWord: true }},
+ { keys: 'ge', type: 'motion', motion: 'moveByWords', motionArgs: { forward: false, wordEnd: true, inclusive: true }},
+ { keys: 'gE', type: 'motion', motion: 'moveByWords', motionArgs: { forward: false, wordEnd: true, bigWord: true, inclusive: true }},
+ { keys: '{', type: 'motion', motion: 'moveByParagraph', motionArgs: { forward: false, toJumplist: true }},
+ { keys: '}', type: 'motion', motion: 'moveByParagraph', motionArgs: { forward: true, toJumplist: true }},
+ { keys: '<C-f>', type: 'motion', motion: 'moveByPage', motionArgs: { forward: true }},
+ { keys: '<C-b>', type: 'motion', motion: 'moveByPage', motionArgs: { forward: false }},
+ { keys: '<C-d>', type: 'motion', motion: 'moveByScroll', motionArgs: { forward: true, explicitRepeat: true }},
+ { keys: '<C-u>', type: 'motion', motion: 'moveByScroll', motionArgs: { forward: false, explicitRepeat: true }},
+ { keys: 'gg', type: 'motion', motion: 'moveToLineOrEdgeOfDocument', motionArgs: { forward: false, explicitRepeat: true, linewise: true, toJumplist: true }},
+ { keys: 'G', type: 'motion', motion: 'moveToLineOrEdgeOfDocument', motionArgs: { forward: true, explicitRepeat: true, linewise: true, toJumplist: true }},
+ { keys: '0', type: 'motion', motion: 'moveToStartOfLine' },
+ { keys: '^', type: 'motion', motion: 'moveToFirstNonWhiteSpaceCharacter' },
+ { keys: '+', type: 'motion', motion: 'moveByLines', motionArgs: { forward: true, toFirstChar:true }},
+ { keys: '-', type: 'motion', motion: 'moveByLines', motionArgs: { forward: false, toFirstChar:true }},
+ { keys: '_', type: 'motion', motion: 'moveByLines', motionArgs: { forward: true, toFirstChar:true, repeatOffset:-1 }},
+ { keys: '$', type: 'motion', motion: 'moveToEol', motionArgs: { inclusive: true }},
+ { keys: '%', type: 'motion', motion: 'moveToMatchedSymbol', motionArgs: { inclusive: true, toJumplist: true }},
+ { keys: 'f<character>', type: 'motion', motion: 'moveToCharacter', motionArgs: { forward: true , inclusive: true }},
+ { keys: 'F<character>', type: 'motion', motion: 'moveToCharacter', motionArgs: { forward: false }},
+ { keys: 't<character>', type: 'motion', motion: 'moveTillCharacter', motionArgs: { forward: true, inclusive: true }},
+ { keys: 'T<character>', type: 'motion', motion: 'moveTillCharacter', motionArgs: { forward: false }},
+ { keys: ';', type: 'motion', motion: 'repeatLastCharacterSearch', motionArgs: { forward: true }},
+ { keys: ',', type: 'motion', motion: 'repeatLastCharacterSearch', motionArgs: { forward: false }},
+ { keys: '\'<character>', type: 'motion', motion: 'goToMark', motionArgs: {toJumplist: true, linewise: true}},
+ { keys: '`<character>', type: 'motion', motion: 'goToMark', motionArgs: {toJumplist: true}},
+ { keys: ']`', type: 'motion', motion: 'jumpToMark', motionArgs: { forward: true } },
+ { keys: '[`', type: 'motion', motion: 'jumpToMark', motionArgs: { forward: false } },
+ { keys: ']\'', type: 'motion', motion: 'jumpToMark', motionArgs: { forward: true, linewise: true } },
+ { keys: '[\'', type: 'motion', motion: 'jumpToMark', motionArgs: { forward: false, linewise: true } },
+ // the next two aren't motions but must come before more general motion declarations
+ { keys: ']p', type: 'action', action: 'paste', isEdit: true, actionArgs: { after: true, isEdit: true, matchIndent: true}},
+ { keys: '[p', type: 'action', action: 'paste', isEdit: true, actionArgs: { after: false, isEdit: true, matchIndent: true}},
+ { keys: ']<character>', type: 'motion', motion: 'moveToSymbol', motionArgs: { forward: true, toJumplist: true}},
+ { keys: '[<character>', type: 'motion', motion: 'moveToSymbol', motionArgs: { forward: false, toJumplist: true}},
+ { keys: '|', type: 'motion', motion: 'moveToColumn'},
+ { keys: 'o', type: 'motion', motion: 'moveToOtherHighlightedEnd', context:'visual'},
+ { keys: 'O', type: 'motion', motion: 'moveToOtherHighlightedEnd', motionArgs: {sameLine: true}, context:'visual'},
+ // Operators
+ { keys: 'd', type: 'operator', operator: 'delete' },
+ { keys: 'y', type: 'operator', operator: 'yank' },
+ { keys: 'c', type: 'operator', operator: 'change' },
+ { keys: '>', type: 'operator', operator: 'indent', operatorArgs: { indentRight: true }},
+ { keys: '<', type: 'operator', operator: 'indent', operatorArgs: { indentRight: false }},
+ { keys: 'g~', type: 'operator', operator: 'changeCase' },
+ { keys: 'gu', type: 'operator', operator: 'changeCase', operatorArgs: {toLower: true}, isEdit: true },
+ { keys: 'gU', type: 'operator', operator: 'changeCase', operatorArgs: {toLower: false}, isEdit: true },
+ { keys: 'n', type: 'motion', motion: 'findNext', motionArgs: { forward: true, toJumplist: true }},
+ { keys: 'N', type: 'motion', motion: 'findNext', motionArgs: { forward: false, toJumplist: true }},
+ // Operator-Motion dual commands
+ { keys: 'x', type: 'operatorMotion', operator: 'delete', motion: 'moveByCharacters', motionArgs: { forward: true }, operatorMotionArgs: { visualLine: false }},
+ { keys: 'X', type: 'operatorMotion', operator: 'delete', motion: 'moveByCharacters', motionArgs: { forward: false }, operatorMotionArgs: { visualLine: true }},
+ { keys: 'D', type: 'operatorMotion', operator: 'delete', motion: 'moveToEol', motionArgs: { inclusive: true }, context: 'normal'},
+ { keys: 'D', type: 'operator', operator: 'delete', operatorArgs: { linewise: true }, context: 'visual'},
+ { keys: 'Y', type: 'operatorMotion', operator: 'yank', motion: 'moveToEol', motionArgs: { inclusive: true }, context: 'normal'},
+ { keys: 'Y', type: 'operator', operator: 'yank', operatorArgs: { linewise: true }, context: 'visual'},
+ { keys: 'C', type: 'operatorMotion', operator: 'change', motion: 'moveToEol', motionArgs: { inclusive: true }, context: 'normal'},
+ { keys: 'C', type: 'operator', operator: 'change', operatorArgs: { linewise: true }, context: 'visual'},
+ { keys: '~', type: 'operatorMotion', operator: 'changeCase', motion: 'moveByCharacters', motionArgs: { forward: true }, operatorArgs: { shouldMoveCursor: true }, context: 'normal'},
+ { keys: '~', type: 'operator', operator: 'changeCase', context: 'visual'},
+ { keys: '<C-w>', type: 'operatorMotion', operator: 'delete', motion: 'moveByWords', motionArgs: { forward: false, wordEnd: false }, context: 'insert' },
+ // Actions
+ { keys: '<C-i>', type: 'action', action: 'jumpListWalk', actionArgs: { forward: true }},
+ { keys: '<C-o>', type: 'action', action: 'jumpListWalk', actionArgs: { forward: false }},
+ { keys: '<C-e>', type: 'action', action: 'scroll', actionArgs: { forward: true, linewise: true }},
+ { keys: '<C-y>', type: 'action', action: 'scroll', actionArgs: { forward: false, linewise: true }},
+ { keys: 'a', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'charAfter' }, context: 'normal' },
+ { keys: 'A', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'eol' }, context: 'normal' },
+ { keys: 'A', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'endOfSelectedArea' }, context: 'visual' },
+ { keys: 'i', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'inplace' }, context: 'normal' },
+ { keys: 'I', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'firstNonBlank'}, context: 'normal' },
+ { keys: 'I', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'startOfSelectedArea' }, context: 'visual' },
+ { keys: 'o', type: 'action', action: 'newLineAndEnterInsertMode', isEdit: true, interlaceInsertRepeat: true, actionArgs: { after: true }, context: 'normal' },
+ { keys: 'O', type: 'action', action: 'newLineAndEnterInsertMode', isEdit: true, interlaceInsertRepeat: true, actionArgs: { after: false }, context: 'normal' },
+ { keys: 'v', type: 'action', action: 'toggleVisualMode' },
+ { keys: 'V', type: 'action', action: 'toggleVisualMode', actionArgs: { linewise: true }},
+ { keys: '<C-v>', type: 'action', action: 'toggleVisualMode', actionArgs: { blockwise: true }},
+ { keys: '<C-q>', type: 'action', action: 'toggleVisualMode', actionArgs: { blockwise: true }},
+ { keys: 'gv', type: 'action', action: 'reselectLastSelection' },
+ { keys: 'J', type: 'action', action: 'joinLines', isEdit: true },
+ { keys: 'p', type: 'action', action: 'paste', isEdit: true, actionArgs: { after: true, isEdit: true }},
+ { keys: 'P', type: 'action', action: 'paste', isEdit: true, actionArgs: { after: false, isEdit: true }},
+ { keys: 'r<character>', type: 'action', action: 'replace', isEdit: true },
+ { keys: '@<character>', type: 'action', action: 'replayMacro' },
+ { keys: 'q<character>', type: 'action', action: 'enterMacroRecordMode' },
+ // Handle Replace-mode as a special case of insert mode.
+ { keys: 'R', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { replace: true }},
+ { keys: 'u', type: 'action', action: 'undo', context: 'normal' },
+ { keys: 'u', type: 'operator', operator: 'changeCase', operatorArgs: {toLower: true}, context: 'visual', isEdit: true },
+ { keys: 'U', type: 'operator', operator: 'changeCase', operatorArgs: {toLower: false}, context: 'visual', isEdit: true },
+ { keys: '<C-r>', type: 'action', action: 'redo' },
+ { keys: 'm<character>', type: 'action', action: 'setMark' },
+ { keys: '"<character>', type: 'action', action: 'setRegister' },
+ { keys: 'zz', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'center' }},
+ { keys: 'z.', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'center' }, motion: 'moveToFirstNonWhiteSpaceCharacter' },
+ { keys: 'zt', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'top' }},
+ { keys: 'z<CR>', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'top' }, motion: 'moveToFirstNonWhiteSpaceCharacter' },
+ { keys: 'z-', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'bottom' }},
+ { keys: 'zb', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'bottom' }, motion: 'moveToFirstNonWhiteSpaceCharacter' },
+ { keys: '.', type: 'action', action: 'repeatLastEdit' },
+ { keys: '<C-a>', type: 'action', action: 'incrementNumberToken', isEdit: true, actionArgs: {increase: true, backtrack: false}},
+ { keys: '<C-x>', type: 'action', action: 'incrementNumberToken', isEdit: true, actionArgs: {increase: false, backtrack: false}},
+ // Text object motions
+ { keys: 'a<character>', type: 'motion', motion: 'textObjectManipulation' },
+ { keys: 'i<character>', type: 'motion', motion: 'textObjectManipulation', motionArgs: { textObjectInner: true }},
+ // Search
+ { keys: '/', type: 'search', searchArgs: { forward: true, querySrc: 'prompt', toJumplist: true }},
+ { keys: '?', type: 'search', searchArgs: { forward: false, querySrc: 'prompt', toJumplist: true }},
+ { keys: '*', type: 'search', searchArgs: { forward: true, querySrc: 'wordUnderCursor', wholeWordOnly: true, toJumplist: true }},
+ { keys: '#', type: 'search', searchArgs: { forward: false, querySrc: 'wordUnderCursor', wholeWordOnly: true, toJumplist: true }},
+ { keys: 'g*', type: 'search', searchArgs: { forward: true, querySrc: 'wordUnderCursor', toJumplist: true }},
+ { keys: 'g#', type: 'search', searchArgs: { forward: false, querySrc: 'wordUnderCursor', toJumplist: true }},
+ // Ex command
+ { keys: ':', type: 'ex' }
+ ];
+
+ /**
+ * Ex commands
+ * Care must be taken when adding to the default Ex command map. For any
+ * pair of commands that have a shared prefix, at least one of their
+ * shortNames must not match the prefix of the other command.
+ */
+ var defaultExCommandMap = [
+ { name: 'colorscheme', shortName: 'colo' },
+ { name: 'map' },
+ { name: 'imap', shortName: 'im' },
+ { name: 'nmap', shortName: 'nm' },
+ { name: 'vmap', shortName: 'vm' },
+ { name: 'unmap' },
+ { name: 'write', shortName: 'w' },
+ { name: 'undo', shortName: 'u' },
+ { name: 'redo', shortName: 'red' },
+ { name: 'set', shortName: 'se' },
+ { name: 'set', shortName: 'se' },
+ { name: 'setlocal', shortName: 'setl' },
+ { name: 'setglobal', shortName: 'setg' },
+ { name: 'sort', shortName: 'sor' },
+ { name: 'substitute', shortName: 's', possiblyAsync: true },
+ { name: 'nohlsearch', shortName: 'noh' },
+ { name: 'yank', shortName: 'y' },
+ { name: 'delmarks', shortName: 'delm' },
+ { name: 'registers', shortName: 'reg', excludeFromCommandHistory: true },
+ { name: 'global', shortName: 'g' }
+ ];
+
+ var Pos = CodeMirror.Pos;
+
+ var Vim = function() {
+ function enterVimMode(cm) {
+ cm.setOption('disableInput', true);
+ cm.setOption('showCursorWhenSelecting', false);
+ CodeMirror.signal(cm, "vim-mode-change", {mode: "normal"});
+ cm.on('cursorActivity', onCursorActivity);
+ maybeInitVimState(cm);
+ CodeMirror.on(cm.getInputField(), 'paste', getOnPasteFn(cm));
+ }
+
+ function leaveVimMode(cm) {
+ cm.setOption('disableInput', false);
+ cm.off('cursorActivity', onCursorActivity);
+ CodeMirror.off(cm.getInputField(), 'paste', getOnPasteFn(cm));
+ cm.state.vim = null;
+ }
+
+ function detachVimMap(cm, next) {
+ if (this == CodeMirror.keyMap.vim)
+ CodeMirror.rmClass(cm.getWrapperElement(), "cm-fat-cursor");
+
+ if (!next || next.attach != attachVimMap)
+ leaveVimMode(cm, false);
+ }
+ function attachVimMap(cm, prev) {
+ if (this == CodeMirror.keyMap.vim)
+ CodeMirror.addClass(cm.getWrapperElement(), "cm-fat-cursor");
+
+ if (!prev || prev.attach != attachVimMap)
+ enterVimMode(cm);
+ }
+
+ // Deprecated, simply setting the keymap works again.
+ CodeMirror.defineOption('vimMode', false, function(cm, val, prev) {
+ if (val && cm.getOption("keyMap") != "vim")
+ cm.setOption("keyMap", "vim");
+ else if (!val && prev != CodeMirror.Init && /^vim/.test(cm.getOption("keyMap")))
+ cm.setOption("keyMap", "default");
+ });
+
+ function cmKey(key, cm) {
+ if (!cm) { return undefined; }
+ var vimKey = cmKeyToVimKey(key);
+ if (!vimKey) {
+ return false;
+ }
+ var cmd = CodeMirror.Vim.findKey(cm, vimKey);
+ if (typeof cmd == 'function') {
+ CodeMirror.signal(cm, 'vim-keypress', vimKey);
+ }
+ return cmd;
+ }
+
+ var modifiers = {'Shift': 'S', 'Ctrl': 'C', 'Alt': 'A', 'Cmd': 'D', 'Mod': 'A'};
+ var specialKeys = {Enter:'CR',Backspace:'BS',Delete:'Del'};
+ function cmKeyToVimKey(key) {
+ if (key.charAt(0) == '\'') {
+ // Keypress character binding of format "'a'"
+ return key.charAt(1);
+ }
+ var pieces = key.split(/-(?!$)/);
+ var lastPiece = pieces[pieces.length - 1];
+ if (pieces.length == 1 && pieces[0].length == 1) {
+ // No-modifier bindings use literal character bindings above. Skip.
+ return false;
+ } else if (pieces.length == 2 && pieces[0] == 'Shift' && lastPiece.length == 1) {
+ // Ignore Shift+char bindings as they should be handled by literal character.
+ return false;
+ }
+ var hasCharacter = false;
+ for (var i = 0; i < pieces.length; i++) {
+ var piece = pieces[i];
+ if (piece in modifiers) { pieces[i] = modifiers[piece]; }
+ else { hasCharacter = true; }
+ if (piece in specialKeys) { pieces[i] = specialKeys[piece]; }
+ }
+ if (!hasCharacter) {
+ // Vim does not support modifier only keys.
+ return false;
+ }
+ // TODO: Current bindings expect the character to be lower case, but
+ // it looks like vim key notation uses upper case.
+ if (isUpperCase(lastPiece)) {
+ pieces[pieces.length - 1] = lastPiece.toLowerCase();
+ }
+ return '<' + pieces.join('-') + '>';
+ }
+
+ function getOnPasteFn(cm) {
+ var vim = cm.state.vim;
+ if (!vim.onPasteFn) {
+ vim.onPasteFn = function() {
+ if (!vim.insertMode) {
+ cm.setCursor(offsetCursor(cm.getCursor(), 0, 1));
+ actions.enterInsertMode(cm, {}, vim);
+ }
+ };
+ }
+ return vim.onPasteFn;
+ }
+
+ var numberRegex = /[\d]/;
+ var wordCharTest = [CodeMirror.isWordChar, function(ch) {
+ return ch && !CodeMirror.isWordChar(ch) && !/\s/.test(ch);
+ }], bigWordCharTest = [function(ch) {
+ return /\S/.test(ch);
+ }];
+ function makeKeyRange(start, size) {
+ var keys = [];
+ for (var i = start; i < start + size; i++) {
+ keys.push(String.fromCharCode(i));
+ }
+ return keys;
+ }
+ var upperCaseAlphabet = makeKeyRange(65, 26);
+ var lowerCaseAlphabet = makeKeyRange(97, 26);
+ var numbers = makeKeyRange(48, 10);
+ var validMarks = [].concat(upperCaseAlphabet, lowerCaseAlphabet, numbers, ['<', '>']);
+ var validRegisters = [].concat(upperCaseAlphabet, lowerCaseAlphabet, numbers, ['-', '"', '.', ':', '/']);
+
+ function isLine(cm, line) {
+ return line >= cm.firstLine() && line <= cm.lastLine();
+ }
+ function isLowerCase(k) {
+ return (/^[a-z]$/).test(k);
+ }
+ function isMatchableSymbol(k) {
+ return '()[]{}'.indexOf(k) != -1;
+ }
+ function isNumber(k) {
+ return numberRegex.test(k);
+ }
+ function isUpperCase(k) {
+ return (/^[A-Z]$/).test(k);
+ }
+ function isWhiteSpaceString(k) {
+ return (/^\s*$/).test(k);
+ }
+ function inArray(val, arr) {
+ for (var i = 0; i < arr.length; i++) {
+ if (arr[i] == val) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ var options = {};
+ function defineOption(name, defaultValue, type, aliases, callback) {
+ if (defaultValue === undefined && !callback) {
+ throw Error('defaultValue is required unless callback is provided');
+ }
+ if (!type) { type = 'string'; }
+ options[name] = {
+ type: type,
+ defaultValue: defaultValue,
+ callback: callback
+ };
+ if (aliases) {
+ for (var i = 0; i < aliases.length; i++) {
+ options[aliases[i]] = options[name];
+ }
+ }
+ if (defaultValue) {
+ setOption(name, defaultValue);
+ }
+ }
+
+ function setOption(name, value, cm, cfg) {
+ var option = options[name];
+ cfg = cfg || {};
+ var scope = cfg.scope;
+ if (!option) {
+ throw Error('Unknown option: ' + name);
+ }
+ if (option.type == 'boolean') {
+ if (value && value !== true) {
+ throw Error('Invalid argument: ' + name + '=' + value);
+ } else if (value !== false) {
+ // Boolean options are set to true if value is not defined.
+ value = true;
+ }
+ }
+ if (option.callback) {
+ if (scope !== 'local') {
+ option.callback(value, undefined);
+ }
+ if (scope !== 'global' && cm) {
+ option.callback(value, cm);
+ }
+ } else {
+ if (scope !== 'local') {
+ option.value = option.type == 'boolean' ? !!value : value;
+ }
+ if (scope !== 'global' && cm) {
+ cm.state.vim.options[name] = {value: value};
+ }
+ }
+ }
+
+ function getOption(name, cm, cfg) {
+ var option = options[name];
+ cfg = cfg || {};
+ var scope = cfg.scope;
+ if (!option) {
+ throw Error('Unknown option: ' + name);
+ }
+ if (option.callback) {
+ var local = cm && option.callback(undefined, cm);
+ if (scope !== 'global' && local !== undefined) {
+ return local;
+ }
+ if (scope !== 'local') {
+ return option.callback();
+ }
+ return;
+ } else {
+ var local = (scope !== 'global') && (cm && cm.state.vim.options[name]);
+ return (local || (scope !== 'local') && option || {}).value;
+ }
+ }
+
+ defineOption('filetype', undefined, 'string', ['ft'], function(name, cm) {
+ // Option is local. Do nothing for global.
+ if (cm === undefined) {
+ return;
+ }
+ // The 'filetype' option proxies to the CodeMirror 'mode' option.
+ if (name === undefined) {
+ var mode = cm.getOption('mode');
+ return mode == 'null' ? '' : mode;
+ } else {
+ var mode = name == '' ? 'null' : name;
+ cm.setOption('mode', mode);
+ }
+ });
+
+ var createCircularJumpList = function() {
+ var size = 100;
+ var pointer = -1;
+ var head = 0;
+ var tail = 0;
+ var buffer = new Array(size);
+ function add(cm, oldCur, newCur) {
+ var current = pointer % size;
+ var curMark = buffer[current];
+ function useNextSlot(cursor) {
+ var next = ++pointer % size;
+ var trashMark = buffer[next];
+ if (trashMark) {
+ trashMark.clear();
+ }
+ buffer[next] = cm.setBookmark(cursor);
+ }
+ if (curMark) {
+ var markPos = curMark.find();
+ // avoid recording redundant cursor position
+ if (markPos && !cursorEqual(markPos, oldCur)) {
+ useNextSlot(oldCur);
+ }
+ } else {
+ useNextSlot(oldCur);
+ }
+ useNextSlot(newCur);
+ head = pointer;
+ tail = pointer - size + 1;
+ if (tail < 0) {
+ tail = 0;
+ }
+ }
+ function move(cm, offset) {
+ pointer += offset;
+ if (pointer > head) {
+ pointer = head;
+ } else if (pointer < tail) {
+ pointer = tail;
+ }
+ var mark = buffer[(size + pointer) % size];
+ // skip marks that are temporarily removed from text buffer
+ if (mark && !mark.find()) {
+ var inc = offset > 0 ? 1 : -1;
+ var newCur;
+ var oldCur = cm.getCursor();
+ do {
+ pointer += inc;
+ mark = buffer[(size + pointer) % size];
+ // skip marks that are the same as current position
+ if (mark &&
+ (newCur = mark.find()) &&
+ !cursorEqual(oldCur, newCur)) {
+ break;
+ }
+ } while (pointer < head && pointer > tail);
+ }
+ return mark;
+ }
+ return {
+ cachedCursor: undefined, //used for # and * jumps
+ add: add,
+ move: move
+ };
+ };
+
+ // Returns an object to track the changes associated insert mode. It
+ // clones the object that is passed in, or creates an empty object one if
+ // none is provided.
+ var createInsertModeChanges = function(c) {
+ if (c) {
+ // Copy construction
+ return {
+ changes: c.changes,
+ expectCursorActivityForChange: c.expectCursorActivityForChange
+ };
+ }
+ return {
+ // Change list
+ changes: [],
+ // Set to true on change, false on cursorActivity.
+ expectCursorActivityForChange: false
+ };
+ };
+
+ function MacroModeState() {
+ this.latestRegister = undefined;
+ this.isPlaying = false;
+ this.isRecording = false;
+ this.replaySearchQueries = [];
+ this.onRecordingDone = undefined;
+ this.lastInsertModeChanges = createInsertModeChanges();
+ }
+ MacroModeState.prototype = {
+ exitMacroRecordMode: function() {
+ var macroModeState = vimGlobalState.macroModeState;
+ if (macroModeState.onRecordingDone) {
+ macroModeState.onRecordingDone(); // close dialog
+ }
+ macroModeState.onRecordingDone = undefined;
+ macroModeState.isRecording = false;
+ },
+ enterMacroRecordMode: function(cm, registerName) {
+ var register =
+ vimGlobalState.registerController.getRegister(registerName);
+ if (register) {
+ register.clear();
+ this.latestRegister = registerName;
+ if (cm.openDialog) {
+ this.onRecordingDone = cm.openDialog(
+ '(recording)['+registerName+']', null, {bottom:true});
+ }
+ this.isRecording = true;
+ }
+ }
+ };
+
+ function maybeInitVimState(cm) {
+ if (!cm.state.vim) {
+ // Store instance state in the CodeMirror object.
+ cm.state.vim = {
+ inputState: new InputState(),
+ // Vim's input state that triggered the last edit, used to repeat
+ // motions and operators with '.'.
+ lastEditInputState: undefined,
+ // Vim's action command before the last edit, used to repeat actions
+ // with '.' and insert mode repeat.
+ lastEditActionCommand: undefined,
+ // When using jk for navigation, if you move from a longer line to a
+ // shorter line, the cursor may clip to the end of the shorter line.
+ // If j is pressed again and cursor goes to the next line, the
+ // cursor should go back to its horizontal position on the longer
+ // line if it can. This is to keep track of the horizontal position.
+ lastHPos: -1,
+ // Doing the same with screen-position for gj/gk
+ lastHSPos: -1,
+ // The last motion command run. Cleared if a non-motion command gets
+ // executed in between.
+ lastMotion: null,
+ marks: {},
+ // Mark for rendering fake cursor for visual mode.
+ fakeCursor: null,
+ insertMode: false,
+ // Repeat count for changes made in insert mode, triggered by key
+ // sequences like 3,i. Only exists when insertMode is true.
+ insertModeRepeat: undefined,
+ visualMode: false,
+ // If we are in visual line mode. No effect if visualMode is false.
+ visualLine: false,
+ visualBlock: false,
+ lastSelection: null,
+ lastPastedText: null,
+ sel: {},
+ // Buffer-local/window-local values of vim options.
+ options: {}
+ };
+ }
+ return cm.state.vim;
+ }
+ var vimGlobalState;
+ function resetVimGlobalState() {
+ vimGlobalState = {
+ // The current search query.
+ searchQuery: null,
+ // Whether we are searching backwards.
+ searchIsReversed: false,
+ // Replace part of the last substituted pattern
+ lastSubstituteReplacePart: undefined,
+ jumpList: createCircularJumpList(),
+ macroModeState: new MacroModeState,
+ // Recording latest f, t, F or T motion command.
+ lastCharacterSearch: {increment:0, forward:true, selectedCharacter:''},
+ registerController: new RegisterController({}),
+ // search history buffer
+ searchHistoryController: new HistoryController({}),
+ // ex Command history buffer
+ exCommandHistoryController : new HistoryController({})
+ };
+ for (var optionName in options) {
+ var option = options[optionName];
+ option.value = option.defaultValue;
+ }
+ }
+
+ var lastInsertModeKeyTimer;
+ var vimApi= {
+ buildKeyMap: function() {
+ // TODO: Convert keymap into dictionary format for fast lookup.
+ },
+ // Testing hook, though it might be useful to expose the register
+ // controller anyways.
+ getRegisterController: function() {
+ return vimGlobalState.registerController;
+ },
+ // Testing hook.
+ resetVimGlobalState_: resetVimGlobalState,
+
+ // Testing hook.
+ getVimGlobalState_: function() {
+ return vimGlobalState;
+ },
+
+ // Testing hook.
+ maybeInitVimState_: maybeInitVimState,
+
+ suppressErrorLogging: false,
+
+ InsertModeKey: InsertModeKey,
+ map: function(lhs, rhs, ctx) {
+ // Add user defined key bindings.
+ exCommandDispatcher.map(lhs, rhs, ctx);
+ },
+ unmap: function(lhs, ctx) {
+ exCommandDispatcher.unmap(lhs, ctx);
+ },
+ // TODO: Expose setOption and getOption as instance methods. Need to decide how to namespace
+ // them, or somehow make them work with the existing CodeMirror setOption/getOption API.
+ setOption: setOption,
+ getOption: getOption,
+ defineOption: defineOption,
+ defineEx: function(name, prefix, func){
+ if (!prefix) {
+ prefix = name;
+ } else if (name.indexOf(prefix) !== 0) {
+ throw new Error('(Vim.defineEx) "'+prefix+'" is not a prefix of "'+name+'", command not registered');
+ }
+ exCommands[name]=func;
+ exCommandDispatcher.commandMap_[prefix]={name:name, shortName:prefix, type:'api'};
+ },
+ handleKey: function (cm, key, origin) {
+ var command = this.findKey(cm, key, origin);
+ if (typeof command === 'function') {
+ return command();
+ }
+ },
+ /**
+ * This is the outermost function called by CodeMirror, after keys have
+ * been mapped to their Vim equivalents.
+ *
+ * Finds a command based on the key (and cached keys if there is a
+ * multi-key sequence). Returns `undefined` if no key is matched, a noop
+ * function if a partial match is found (multi-key), and a function to
+ * execute the bound command if a a key is matched. The function always
+ * returns true.
+ */
+ findKey: function(cm, key, origin) {
+ var vim = maybeInitVimState(cm);
+ function handleMacroRecording() {
+ var macroModeState = vimGlobalState.macroModeState;
+ if (macroModeState.isRecording) {
+ if (key == 'q') {
+ macroModeState.exitMacroRecordMode();
+ clearInputState(cm);
+ return true;
+ }
+ if (origin != 'mapping') {
+ logKey(macroModeState, key);
+ }
+ }
+ }
+ function handleEsc() {
+ if (key == '<Esc>') {
+ // Clear input state and get back to normal mode.
+ clearInputState(cm);
+ if (vim.visualMode) {
+ exitVisualMode(cm);
+ } else if (vim.insertMode) {
+ exitInsertMode(cm);
+ }
+ return true;
+ }
+ }
+ function doKeyToKey(keys) {
+ // TODO: prevent infinite recursion.
+ var match;
+ while (keys) {
+ // Pull off one command key, which is either a single character
+ // or a special sequence wrapped in '<' and '>', e.g. '<Space>'.
+ match = (/<\w+-.+?>|<\w+>|./).exec(keys);
+ key = match[0];
+ keys = keys.substring(match.index + key.length);
+ CodeMirror.Vim.handleKey(cm, key, 'mapping');
+ }
+ }
+
+ function handleKeyInsertMode() {
+ if (handleEsc()) { return true; }
+ var keys = vim.inputState.keyBuffer = vim.inputState.keyBuffer + key;
+ var keysAreChars = key.length == 1;
+ var match = commandDispatcher.matchCommand(keys, defaultKeymap, vim.inputState, 'insert');
+ // Need to check all key substrings in insert mode.
+ while (keys.length > 1 && match.type != 'full') {
+ var keys = vim.inputState.keyBuffer = keys.slice(1);
+ var thisMatch = commandDispatcher.matchCommand(keys, defaultKeymap, vim.inputState, 'insert');
+ if (thisMatch.type != 'none') { match = thisMatch; }
+ }
+ if (match.type == 'none') { clearInputState(cm); return false; }
+ else if (match.type == 'partial') {
+ if (lastInsertModeKeyTimer) { window.clearTimeout(lastInsertModeKeyTimer); }
+ lastInsertModeKeyTimer = window.setTimeout(
+ function() { if (vim.insertMode && vim.inputState.keyBuffer) { clearInputState(cm); } },
+ getOption('insertModeEscKeysTimeout'));
+ return !keysAreChars;
+ }
+
+ if (lastInsertModeKeyTimer) { window.clearTimeout(lastInsertModeKeyTimer); }
+ if (keysAreChars) {
+ var here = cm.getCursor();
+ cm.replaceRange('', offsetCursor(here, 0, -(keys.length - 1)), here, '+input');
+ }
+ clearInputState(cm);
+ return match.command;
+ }
+
+ function handleKeyNonInsertMode() {
+ if (handleMacroRecording() || handleEsc()) { return true; };
+
+ var keys = vim.inputState.keyBuffer = vim.inputState.keyBuffer + key;
+ if (/^[1-9]\d*$/.test(keys)) { return true; }
+
+ var keysMatcher = /^(\d*)(.*)$/.exec(keys);
+ if (!keysMatcher) { clearInputState(cm); return false; }
+ var context = vim.visualMode ? 'visual' :
+ 'normal';
+ var match = commandDispatcher.matchCommand(keysMatcher[2] || keysMatcher[1], defaultKeymap, vim.inputState, context);
+ if (match.type == 'none') { clearInputState(cm); return false; }
+ else if (match.type == 'partial') { return true; }
+
+ vim.inputState.keyBuffer = '';
+ var keysMatcher = /^(\d*)(.*)$/.exec(keys);
+ if (keysMatcher[1] && keysMatcher[1] != '0') {
+ vim.inputState.pushRepeatDigit(keysMatcher[1]);
+ }
+ return match.command;
+ }
+
+ var command;
+ if (vim.insertMode) { command = handleKeyInsertMode(); }
+ else { command = handleKeyNonInsertMode(); }
+ if (command === false) {
+ return undefined;
+ } else if (command === true) {
+ // TODO: Look into using CodeMirror's multi-key handling.
+ // Return no-op since we are caching the key. Counts as handled, but
+ // don't want act on it just yet.
+ return function() {};
+ } else {
+ return function() {
+ return cm.operation(function() {
+ cm.curOp.isVimOp = true;
+ try {
+ if (command.type == 'keyToKey') {
+ doKeyToKey(command.toKeys);
+ } else {
+ commandDispatcher.processCommand(cm, vim, command);
+ }
+ } catch (e) {
+ // clear VIM state in case it's in a bad state.
+ cm.state.vim = undefined;
+ maybeInitVimState(cm);
+ if (!CodeMirror.Vim.suppressErrorLogging) {
+ console['log'](e);
+ }
+ throw e;
+ }
+ return true;
+ });
+ };
+ }
+ },
+ handleEx: function(cm, input) {
+ exCommandDispatcher.processCommand(cm, input);
+ },
+
+ defineMotion: defineMotion,
+ defineAction: defineAction,
+ defineOperator: defineOperator,
+ mapCommand: mapCommand,
+ _mapCommand: _mapCommand,
+
+ defineRegister: defineRegister,
+
+ exitVisualMode: exitVisualMode,
+ exitInsertMode: exitInsertMode
+ };
+
+ // Represents the current input state.
+ function InputState() {
+ this.prefixRepeat = [];
+ this.motionRepeat = [];
+
+ this.operator = null;
+ this.operatorArgs = null;
+ this.motion = null;
+ this.motionArgs = null;
+ this.keyBuffer = []; // For matching multi-key commands.
+ this.registerName = null; // Defaults to the unnamed register.
+ }
+ InputState.prototype.pushRepeatDigit = function(n) {
+ if (!this.operator) {
+ this.prefixRepeat = this.prefixRepeat.concat(n);
+ } else {
+ this.motionRepeat = this.motionRepeat.concat(n);
+ }
+ };
+ InputState.prototype.getRepeat = function() {
+ var repeat = 0;
+ if (this.prefixRepeat.length > 0 || this.motionRepeat.length > 0) {
+ repeat = 1;
+ if (this.prefixRepeat.length > 0) {
+ repeat *= parseInt(this.prefixRepeat.join(''), 10);
+ }
+ if (this.motionRepeat.length > 0) {
+ repeat *= parseInt(this.motionRepeat.join(''), 10);
+ }
+ }
+ return repeat;
+ };
+
+ function clearInputState(cm, reason) {
+ cm.state.vim.inputState = new InputState();
+ CodeMirror.signal(cm, 'vim-command-done', reason);
+ }
+
+ /*
+ * Register stores information about copy and paste registers. Besides
+ * text, a register must store whether it is linewise (i.e., when it is
+ * pasted, should it insert itself into a new line, or should the text be
+ * inserted at the cursor position.)
+ */
+ function Register(text, linewise, blockwise) {
+ this.clear();
+ this.keyBuffer = [text || ''];
+ this.insertModeChanges = [];
+ this.searchQueries = [];
+ this.linewise = !!linewise;
+ this.blockwise = !!blockwise;
+ }
+ Register.prototype = {
+ setText: function(text, linewise, blockwise) {
+ this.keyBuffer = [text || ''];
+ this.linewise = !!linewise;
+ this.blockwise = !!blockwise;
+ },
+ pushText: function(text, linewise) {
+ // if this register has ever been set to linewise, use linewise.
+ if (linewise) {
+ if (!this.linewise) {
+ this.keyBuffer.push('\n');
+ }
+ this.linewise = true;
+ }
+ this.keyBuffer.push(text);
+ },
+ pushInsertModeChanges: function(changes) {
+ this.insertModeChanges.push(createInsertModeChanges(changes));
+ },
+ pushSearchQuery: function(query) {
+ this.searchQueries.push(query);
+ },
+ clear: function() {
+ this.keyBuffer = [];
+ this.insertModeChanges = [];
+ this.searchQueries = [];
+ this.linewise = false;
+ },
+ toString: function() {
+ return this.keyBuffer.join('');
+ }
+ };
+
+ /**
+ * Defines an external register.
+ *
+ * The name should be a single character that will be used to reference the register.
+ * The register should support setText, pushText, clear, and toString(). See Register
+ * for a reference implementation.
+ */
+ function defineRegister(name, register) {
+ var registers = vimGlobalState.registerController.registers[name];
+ if (!name || name.length != 1) {
+ throw Error('Register name must be 1 character');
+ }
+ if (registers[name]) {
+ throw Error('Register already defined ' + name);
+ }
+ registers[name] = register;
+ validRegisters.push(name);
+ }
+
+ /*
+ * vim registers allow you to keep many independent copy and paste buffers.
+ * See http://usevim.com/2012/04/13/registers/ for an introduction.
+ *
+ * RegisterController keeps the state of all the registers. An initial
+ * state may be passed in. The unnamed register '"' will always be
+ * overridden.
+ */
+ function RegisterController(registers) {
+ this.registers = registers;
+ this.unnamedRegister = registers['"'] = new Register();
+ registers['.'] = new Register();
+ registers[':'] = new Register();
+ registers['/'] = new Register();
+ }
+ RegisterController.prototype = {
+ pushText: function(registerName, operator, text, linewise, blockwise) {
+ if (linewise && text.charAt(0) == '\n') {
+ text = text.slice(1) + '\n';
+ }
+ if (linewise && text.charAt(text.length - 1) !== '\n'){
+ text += '\n';
+ }
+ // Lowercase and uppercase registers refer to the same register.
+ // Uppercase just means append.
+ var register = this.isValidRegister(registerName) ?
+ this.getRegister(registerName) : null;
+ // if no register/an invalid register was specified, things go to the
+ // default registers
+ if (!register) {
+ switch (operator) {
+ case 'yank':
+ // The 0 register contains the text from the most recent yank.
+ this.registers['0'] = new Register(text, linewise, blockwise);
+ break;
+ case 'delete':
+ case 'change':
+ if (text.indexOf('\n') == -1) {
+ // Delete less than 1 line. Update the small delete register.
+ this.registers['-'] = new Register(text, linewise);
+ } else {
+ // Shift down the contents of the numbered registers and put the
+ // deleted text into register 1.
+ this.shiftNumericRegisters_();
+ this.registers['1'] = new Register(text, linewise);
+ }
+ break;
+ }
+ // Make sure the unnamed register is set to what just happened
+ this.unnamedRegister.setText(text, linewise, blockwise);
+ return;
+ }
+
+ // If we've gotten to this point, we've actually specified a register
+ var append = isUpperCase(registerName);
+ if (append) {
+ register.pushText(text, linewise);
+ } else {
+ register.setText(text, linewise, blockwise);
+ }
+ // The unnamed register always has the same value as the last used
+ // register.
+ this.unnamedRegister.setText(register.toString(), linewise);
+ },
+ // Gets the register named @name. If one of @name doesn't already exist,
+ // create it. If @name is invalid, return the unnamedRegister.
+ getRegister: function(name) {
+ if (!this.isValidRegister(name)) {
+ return this.unnamedRegister;
+ }
+ name = name.toLowerCase();
+ if (!this.registers[name]) {
+ this.registers[name] = new Register();
+ }
+ return this.registers[name];
+ },
+ isValidRegister: function(name) {
+ return name && inArray(name, validRegisters);
+ },
+ shiftNumericRegisters_: function() {
+ for (var i = 9; i >= 2; i--) {
+ this.registers[i] = this.getRegister('' + (i - 1));
+ }
+ }
+ };
+ function HistoryController() {
+ this.historyBuffer = [];
+ this.iterator = 0;
+ this.initialPrefix = null;
+ }
+ HistoryController.prototype = {
+ // the input argument here acts a user entered prefix for a small time
+ // until we start autocompletion in which case it is the autocompleted.
+ nextMatch: function (input, up) {
+ var historyBuffer = this.historyBuffer;
+ var dir = up ? -1 : 1;
+ if (this.initialPrefix === null) this.initialPrefix = input;
+ for (var i = this.iterator + dir; up ? i >= 0 : i < historyBuffer.length; i+= dir) {
+ var element = historyBuffer[i];
+ for (var j = 0; j <= element.length; j++) {
+ if (this.initialPrefix == element.substring(0, j)) {
+ this.iterator = i;
+ return element;
+ }
+ }
+ }
+ // should return the user input in case we reach the end of buffer.
+ if (i >= historyBuffer.length) {
+ this.iterator = historyBuffer.length;
+ return this.initialPrefix;
+ }
+ // return the last autocompleted query or exCommand as it is.
+ if (i < 0 ) return input;
+ },
+ pushInput: function(input) {
+ var index = this.historyBuffer.indexOf(input);
+ if (index > -1) this.historyBuffer.splice(index, 1);
+ if (input.length) this.historyBuffer.push(input);
+ },
+ reset: function() {
+ this.initialPrefix = null;
+ this.iterator = this.historyBuffer.length;
+ }
+ };
+ var commandDispatcher = {
+ matchCommand: function(keys, keyMap, inputState, context) {
+ var matches = commandMatches(keys, keyMap, context, inputState);
+ if (!matches.full && !matches.partial) {
+ return {type: 'none'};
+ } else if (!matches.full && matches.partial) {
+ return {type: 'partial'};
+ }
+
+ var bestMatch;
+ for (var i = 0; i < matches.full.length; i++) {
+ var match = matches.full[i];
+ if (!bestMatch) {
+ bestMatch = match;
+ }
+ }
+ if (bestMatch.keys.slice(-11) == '<character>') {
+ inputState.selectedCharacter = lastChar(keys);
+ }
+ return {type: 'full', command: bestMatch};
+ },
+ processCommand: function(cm, vim, command) {
+ vim.inputState.repeatOverride = command.repeatOverride;
+ switch (command.type) {
+ case 'motion':
+ this.processMotion(cm, vim, command);
+ break;
+ case 'operator':
+ this.processOperator(cm, vim, command);
+ break;
+ case 'operatorMotion':
+ this.processOperatorMotion(cm, vim, command);
+ break;
+ case 'action':
+ this.processAction(cm, vim, command);
+ break;
+ case 'search':
+ this.processSearch(cm, vim, command);
+ break;
+ case 'ex':
+ case 'keyToEx':
+ this.processEx(cm, vim, command);
+ break;
+ default:
+ break;
+ }
+ },
+ processMotion: function(cm, vim, command) {
+ vim.inputState.motion = command.motion;
+ vim.inputState.motionArgs = copyArgs(command.motionArgs);
+ this.evalInput(cm, vim);
+ },
+ processOperator: function(cm, vim, command) {
+ var inputState = vim.inputState;
+ if (inputState.operator) {
+ if (inputState.operator == command.operator) {
+ // Typing an operator twice like 'dd' makes the operator operate
+ // linewise
+ inputState.motion = 'expandToLine';
+ inputState.motionArgs = { linewise: true };
+ this.evalInput(cm, vim);
+ return;
+ } else {
+ // 2 different operators in a row doesn't make sense.
+ clearInputState(cm);
+ }
+ }
+ inputState.operator = command.operator;
+ inputState.operatorArgs = copyArgs(command.operatorArgs);
+ if (vim.visualMode) {
+ // Operating on a selection in visual mode. We don't need a motion.
+ this.evalInput(cm, vim);
+ }
+ },
+ processOperatorMotion: function(cm, vim, command) {
+ var visualMode = vim.visualMode;
+ var operatorMotionArgs = copyArgs(command.operatorMotionArgs);
+ if (operatorMotionArgs) {
+ // Operator motions may have special behavior in visual mode.
+ if (visualMode && operatorMotionArgs.visualLine) {
+ vim.visualLine = true;
+ }
+ }
+ this.processOperator(cm, vim, command);
+ if (!visualMode) {
+ this.processMotion(cm, vim, command);
+ }
+ },
+ processAction: function(cm, vim, command) {
+ var inputState = vim.inputState;
+ var repeat = inputState.getRepeat();
+ var repeatIsExplicit = !!repeat;
+ var actionArgs = copyArgs(command.actionArgs) || {};
+ if (inputState.selectedCharacter) {
+ actionArgs.selectedCharacter = inputState.selectedCharacter;
+ }
+ // Actions may or may not have motions and operators. Do these first.
+ if (command.operator) {
+ this.processOperator(cm, vim, command);
+ }
+ if (command.motion) {
+ this.processMotion(cm, vim, command);
+ }
+ if (command.motion || command.operator) {
+ this.evalInput(cm, vim);
+ }
+ actionArgs.repeat = repeat || 1;
+ actionArgs.repeatIsExplicit = repeatIsExplicit;
+ actionArgs.registerName = inputState.registerName;
+ clearInputState(cm);
+ vim.lastMotion = null;
+ if (command.isEdit) {
+ this.recordLastEdit(vim, inputState, command);
+ }
+ actions[command.action](cm, actionArgs, vim);
+ },
+ processSearch: function(cm, vim, command) {
+ if (!cm.getSearchCursor) {
+ // Search depends on SearchCursor.
+ return;
+ }
+ var forward = command.searchArgs.forward;
+ var wholeWordOnly = command.searchArgs.wholeWordOnly;
+ getSearchState(cm).setReversed(!forward);
+ var promptPrefix = (forward) ? '/' : '?';
+ var originalQuery = getSearchState(cm).getQuery();
+ var originalScrollPos = cm.getScrollInfo();
+ function handleQuery(query, ignoreCase, smartCase) {
+ vimGlobalState.searchHistoryController.pushInput(query);
+ vimGlobalState.searchHistoryController.reset();
+ try {
+ updateSearchQuery(cm, query, ignoreCase, smartCase);
+ } catch (e) {
+ showConfirm(cm, 'Invalid regex: ' + query);
+ clearInputState(cm);
+ return;
+ }
+ commandDispatcher.processMotion(cm, vim, {
+ type: 'motion',
+ motion: 'findNext',
+ motionArgs: { forward: true, toJumplist: command.searchArgs.toJumplist }
+ });
+ }
+ function onPromptClose(query) {
+ cm.scrollTo(originalScrollPos.left, originalScrollPos.top);
+ handleQuery(query, true /** ignoreCase */, true /** smartCase */);
+ var macroModeState = vimGlobalState.macroModeState;
+ if (macroModeState.isRecording) {
+ logSearchQuery(macroModeState, query);
+ }
+ }
+ function onPromptKeyUp(e, query, close) {
+ var keyName = CodeMirror.keyName(e), up;
+ if (keyName == 'Up' || keyName == 'Down') {
+ up = keyName == 'Up' ? true : false;
+ query = vimGlobalState.searchHistoryController.nextMatch(query, up) || '';
+ close(query);
+ } else {
+ if ( keyName != 'Left' && keyName != 'Right' && keyName != 'Ctrl' && keyName != 'Alt' && keyName != 'Shift')
+ vimGlobalState.searchHistoryController.reset();
+ }
+ var parsedQuery;
+ try {
+ parsedQuery = updateSearchQuery(cm, query,
+ true /** ignoreCase */, true /** smartCase */);
+ } catch (e) {
+ // Swallow bad regexes for incremental search.
+ }
+ if (parsedQuery) {
+ cm.scrollIntoView(findNext(cm, !forward, parsedQuery), 30);
+ } else {
+ clearSearchHighlight(cm);
+ cm.scrollTo(originalScrollPos.left, originalScrollPos.top);
+ }
+ }
+ function onPromptKeyDown(e, query, close) {
+ var keyName = CodeMirror.keyName(e);
+ if (keyName == 'Esc' || keyName == 'Ctrl-C' || keyName == 'Ctrl-[' ||
+ (keyName == 'Backspace' && query == '')) {
+ vimGlobalState.searchHistoryController.pushInput(query);
+ vimGlobalState.searchHistoryController.reset();
+ updateSearchQuery(cm, originalQuery);
+ clearSearchHighlight(cm);
+ cm.scrollTo(originalScrollPos.left, originalScrollPos.top);
+ CodeMirror.e_stop(e);
+ clearInputState(cm);
+ close();
+ cm.focus();
+ } else if (keyName == 'Ctrl-U') {
+ // Ctrl-U clears input.
+ CodeMirror.e_stop(e);
+ close('');
+ }
+ }
+ switch (command.searchArgs.querySrc) {
+ case 'prompt':
+ var macroModeState = vimGlobalState.macroModeState;
+ if (macroModeState.isPlaying) {
+ var query = macroModeState.replaySearchQueries.shift();
+ handleQuery(query, true /** ignoreCase */, false /** smartCase */);
+ } else {
+ showPrompt(cm, {
+ onClose: onPromptClose,
+ prefix: promptPrefix,
+ desc: searchPromptDesc,
+ onKeyUp: onPromptKeyUp,
+ onKeyDown: onPromptKeyDown
+ });
+ }
+ break;
+ case 'wordUnderCursor':
+ var word = expandWordUnderCursor(cm, false /** inclusive */,
+ true /** forward */, false /** bigWord */,
+ true /** noSymbol */);
+ var isKeyword = true;
+ if (!word) {
+ word = expandWordUnderCursor(cm, false /** inclusive */,
+ true /** forward */, false /** bigWord */,
+ false /** noSymbol */);
+ isKeyword = false;
+ }
+ if (!word) {
+ return;
+ }
+ var query = cm.getLine(word.start.line).substring(word.start.ch,
+ word.end.ch);
+ if (isKeyword && wholeWordOnly) {
+ query = '\\b' + query + '\\b';
+ } else {
+ query = escapeRegex(query);
+ }
+
+ // cachedCursor is used to save the old position of the cursor
+ // when * or # causes vim to seek for the nearest word and shift
+ // the cursor before entering the motion.
+ vimGlobalState.jumpList.cachedCursor = cm.getCursor();
+ cm.setCursor(word.start);
+
+ handleQuery(query, true /** ignoreCase */, false /** smartCase */);
+ break;
+ }
+ },
+ processEx: function(cm, vim, command) {
+ function onPromptClose(input) {
+ // Give the prompt some time to close so that if processCommand shows
+ // an error, the elements don't overlap.
+ vimGlobalState.exCommandHistoryController.pushInput(input);
+ vimGlobalState.exCommandHistoryController.reset();
+ exCommandDispatcher.processCommand(cm, input);
+ }
+ function onPromptKeyDown(e, input, close) {
+ var keyName = CodeMirror.keyName(e), up;
+ if (keyName == 'Esc' || keyName == 'Ctrl-C' || keyName == 'Ctrl-[' ||
+ (keyName == 'Backspace' && input == '')) {
+ vimGlobalState.exCommandHistoryController.pushInput(input);
+ vimGlobalState.exCommandHistoryController.reset();
+ CodeMirror.e_stop(e);
+ clearInputState(cm);
+ close();
+ cm.focus();
+ }
+ if (keyName == 'Up' || keyName == 'Down') {
+ up = keyName == 'Up' ? true : false;
+ input = vimGlobalState.exCommandHistoryController.nextMatch(input, up) || '';
+ close(input);
+ } else if (keyName == 'Ctrl-U') {
+ // Ctrl-U clears input.
+ CodeMirror.e_stop(e);
+ close('');
+ } else {
+ if ( keyName != 'Left' && keyName != 'Right' && keyName != 'Ctrl' && keyName != 'Alt' && keyName != 'Shift')
+ vimGlobalState.exCommandHistoryController.reset();
+ }
+ }
+ if (command.type == 'keyToEx') {
+ // Handle user defined Ex to Ex mappings
+ exCommandDispatcher.processCommand(cm, command.exArgs.input);
+ } else {
+ if (vim.visualMode) {
+ showPrompt(cm, { onClose: onPromptClose, prefix: ':', value: '\'<,\'>',
+ onKeyDown: onPromptKeyDown});
+ } else {
+ showPrompt(cm, { onClose: onPromptClose, prefix: ':',
+ onKeyDown: onPromptKeyDown});
+ }
+ }
+ },
+ evalInput: function(cm, vim) {
+ // If the motion command is set, execute both the operator and motion.
+ // Otherwise return.
+ var inputState = vim.inputState;
+ var motion = inputState.motion;
+ var motionArgs = inputState.motionArgs || {};
+ var operator = inputState.operator;
+ var operatorArgs = inputState.operatorArgs || {};
+ var registerName = inputState.registerName;
+ var sel = vim.sel;
+ // TODO: Make sure cm and vim selections are identical outside visual mode.
+ var origHead = copyCursor(vim.visualMode ? clipCursorToContent(cm, sel.head): cm.getCursor('head'));
+ var origAnchor = copyCursor(vim.visualMode ? clipCursorToContent(cm, sel.anchor) : cm.getCursor('anchor'));
+ var oldHead = copyCursor(origHead);
+ var oldAnchor = copyCursor(origAnchor);
+ var newHead, newAnchor;
+ var repeat;
+ if (operator) {
+ this.recordLastEdit(vim, inputState);
+ }
+ if (inputState.repeatOverride !== undefined) {
+ // If repeatOverride is specified, that takes precedence over the
+ // input state's repeat. Used by Ex mode and can be user defined.
+ repeat = inputState.repeatOverride;
+ } else {
+ repeat = inputState.getRepeat();
+ }
+ if (repeat > 0 && motionArgs.explicitRepeat) {
+ motionArgs.repeatIsExplicit = true;
+ } else if (motionArgs.noRepeat ||
+ (!motionArgs.explicitRepeat && repeat === 0)) {
+ repeat = 1;
+ motionArgs.repeatIsExplicit = false;
+ }
+ if (inputState.selectedCharacter) {
+ // If there is a character input, stick it in all of the arg arrays.
+ motionArgs.selectedCharacter = operatorArgs.selectedCharacter =
+ inputState.selectedCharacter;
+ }
+ motionArgs.repeat = repeat;
+ clearInputState(cm);
+ if (motion) {
+ var motionResult = motions[motion](cm, origHead, motionArgs, vim);
+ vim.lastMotion = motions[motion];
+ if (!motionResult) {
+ return;
+ }
+ if (motionArgs.toJumplist) {
+ var jumpList = vimGlobalState.jumpList;
+ // if the current motion is # or *, use cachedCursor
+ var cachedCursor = jumpList.cachedCursor;
+ if (cachedCursor) {
+ recordJumpPosition(cm, cachedCursor, motionResult);
+ delete jumpList.cachedCursor;
+ } else {
+ recordJumpPosition(cm, origHead, motionResult);
+ }
+ }
+ if (motionResult instanceof Array) {
+ newAnchor = motionResult[0];
+ newHead = motionResult[1];
+ } else {
+ newHead = motionResult;
+ }
+ // TODO: Handle null returns from motion commands better.
+ if (!newHead) {
+ newHead = copyCursor(origHead);
+ }
+ if (vim.visualMode) {
+ if (!(vim.visualBlock && newHead.ch === Infinity)) {
+ newHead = clipCursorToContent(cm, newHead, vim.visualBlock);
+ }
+ if (newAnchor) {
+ newAnchor = clipCursorToContent(cm, newAnchor, true);
+ }
+ newAnchor = newAnchor || oldAnchor;
+ sel.anchor = newAnchor;
+ sel.head = newHead;
+ updateCmSelection(cm);
+ updateMark(cm, vim, '<',
+ cursorIsBefore(newAnchor, newHead) ? newAnchor
+ : newHead);
+ updateMark(cm, vim, '>',
+ cursorIsBefore(newAnchor, newHead) ? newHead
+ : newAnchor);
+ } else if (!operator) {
+ newHead = clipCursorToContent(cm, newHead);
+ cm.setCursor(newHead.line, newHead.ch);
+ }
+ }
+ if (operator) {
+ if (operatorArgs.lastSel) {
+ // Replaying a visual mode operation
+ newAnchor = oldAnchor;
+ var lastSel = operatorArgs.lastSel;
+ var lineOffset = Math.abs(lastSel.head.line - lastSel.anchor.line);
+ var chOffset = Math.abs(lastSel.head.ch - lastSel.anchor.ch);
+ if (lastSel.visualLine) {
+ // Linewise Visual mode: The same number of lines.
+ newHead = Pos(oldAnchor.line + lineOffset, oldAnchor.ch);
+ } else if (lastSel.visualBlock) {
+ // Blockwise Visual mode: The same number of lines and columns.
+ newHead = Pos(oldAnchor.line + lineOffset, oldAnchor.ch + chOffset);
+ } else if (lastSel.head.line == lastSel.anchor.line) {
+ // Normal Visual mode within one line: The same number of characters.
+ newHead = Pos(oldAnchor.line, oldAnchor.ch + chOffset);
+ } else {
+ // Normal Visual mode with several lines: The same number of lines, in the
+ // last line the same number of characters as in the last line the last time.
+ newHead = Pos(oldAnchor.line + lineOffset, oldAnchor.ch);
+ }
+ vim.visualMode = true;
+ vim.visualLine = lastSel.visualLine;
+ vim.visualBlock = lastSel.visualBlock;
+ sel = vim.sel = {
+ anchor: newAnchor,
+ head: newHead
+ };
+ updateCmSelection(cm);
+ } else if (vim.visualMode) {
+ operatorArgs.lastSel = {
+ anchor: copyCursor(sel.anchor),
+ head: copyCursor(sel.head),
+ visualBlock: vim.visualBlock,
+ visualLine: vim.visualLine
+ };
+ }
+ var curStart, curEnd, linewise, mode;
+ var cmSel;
+ if (vim.visualMode) {
+ // Init visual op
+ curStart = cursorMin(sel.head, sel.anchor);
+ curEnd = cursorMax(sel.head, sel.anchor);
+ linewise = vim.visualLine || operatorArgs.linewise;
+ mode = vim.visualBlock ? 'block' :
+ linewise ? 'line' :
+ 'char';
+ cmSel = makeCmSelection(cm, {
+ anchor: curStart,
+ head: curEnd
+ }, mode);
+ if (linewise) {
+ var ranges = cmSel.ranges;
+ if (mode == 'block') {
+ // Linewise operators in visual block mode extend to end of line
+ for (var i = 0; i < ranges.length; i++) {
+ ranges[i].head.ch = lineLength(cm, ranges[i].head.line);
+ }
+ } else if (mode == 'line') {
+ ranges[0].head = Pos(ranges[0].head.line + 1, 0);
+ }
+ }
+ } else {
+ // Init motion op
+ curStart = copyCursor(newAnchor || oldAnchor);
+ curEnd = copyCursor(newHead || oldHead);
+ if (cursorIsBefore(curEnd, curStart)) {
+ var tmp = curStart;
+ curStart = curEnd;
+ curEnd = tmp;
+ }
+ linewise = motionArgs.linewise || operatorArgs.linewise;
+ if (linewise) {
+ // Expand selection to entire line.
+ expandSelectionToLine(cm, curStart, curEnd);
+ } else if (motionArgs.forward) {
+ // Clip to trailing newlines only if the motion goes forward.
+ clipToLine(cm, curStart, curEnd);
+ }
+ mode = 'char';
+ var exclusive = !motionArgs.inclusive || linewise;
+ cmSel = makeCmSelection(cm, {
+ anchor: curStart,
+ head: curEnd
+ }, mode, exclusive);
+ }
+ cm.setSelections(cmSel.ranges, cmSel.primary);
+ vim.lastMotion = null;
+ operatorArgs.repeat = repeat; // For indent in visual mode.
+ operatorArgs.registerName = registerName;
+ // Keep track of linewise as it affects how paste and change behave.
+ operatorArgs.linewise = linewise;
+ var operatorMoveTo = operators[operator](
+ cm, operatorArgs, cmSel.ranges, oldAnchor, newHead);
+ if (vim.visualMode) {
+ exitVisualMode(cm, operatorMoveTo != null);
+ }
+ if (operatorMoveTo) {
+ cm.setCursor(operatorMoveTo);
+ }
+ }
+ },
+ recordLastEdit: function(vim, inputState, actionCommand) {
+ var macroModeState = vimGlobalState.macroModeState;
+ if (macroModeState.isPlaying) { return; }
+ vim.lastEditInputState = inputState;
+ vim.lastEditActionCommand = actionCommand;
+ macroModeState.lastInsertModeChanges.changes = [];
+ macroModeState.lastInsertModeChanges.expectCursorActivityForChange = false;
+ }
+ };
+
+ /**
+ * typedef {Object{line:number,ch:number}} Cursor An object containing the
+ * position of the cursor.
+ */
+ // All of the functions below return Cursor objects.
+ var motions = {
+ moveToTopLine: function(cm, _head, motionArgs) {
+ var line = getUserVisibleLines(cm).top + motionArgs.repeat -1;
+ return Pos(line, findFirstNonWhiteSpaceCharacter(cm.getLine(line)));
+ },
+ moveToMiddleLine: function(cm) {
+ var range = getUserVisibleLines(cm);
+ var line = Math.floor((range.top + range.bottom) * 0.5);
+ return Pos(line, findFirstNonWhiteSpaceCharacter(cm.getLine(line)));
+ },
+ moveToBottomLine: function(cm, _head, motionArgs) {
+ var line = getUserVisibleLines(cm).bottom - motionArgs.repeat +1;
+ return Pos(line, findFirstNonWhiteSpaceCharacter(cm.getLine(line)));
+ },
+ expandToLine: function(_cm, head, motionArgs) {
+ // Expands forward to end of line, and then to next line if repeat is
+ // >1. Does not handle backward motion!
+ var cur = head;
+ return Pos(cur.line + motionArgs.repeat - 1, Infinity);
+ },
+ findNext: function(cm, _head, motionArgs) {
+ var state = getSearchState(cm);
+ var query = state.getQuery();
+ if (!query) {
+ return;
+ }
+ var prev = !motionArgs.forward;
+ // If search is initiated with ? instead of /, negate direction.
+ prev = (state.isReversed()) ? !prev : prev;
+ highlightSearchMatches(cm, query);
+ return findNext(cm, prev/** prev */, query, motionArgs.repeat);
+ },
+ goToMark: function(cm, _head, motionArgs, vim) {
+ var mark = vim.marks[motionArgs.selectedCharacter];
+ if (mark) {
+ var pos = mark.find();
+ return motionArgs.linewise ? { line: pos.line, ch: findFirstNonWhiteSpaceCharacter(cm.getLine(pos.line)) } : pos;
+ }
+ return null;
+ },
+ moveToOtherHighlightedEnd: function(cm, _head, motionArgs, vim) {
+ if (vim.visualBlock && motionArgs.sameLine) {
+ var sel = vim.sel;
+ return [
+ clipCursorToContent(cm, Pos(sel.anchor.line, sel.head.ch)),
+ clipCursorToContent(cm, Pos(sel.head.line, sel.anchor.ch))
+ ];
+ } else {
+ return ([vim.sel.head, vim.sel.anchor]);
+ }
+ },
+ jumpToMark: function(cm, head, motionArgs, vim) {
+ var best = head;
+ for (var i = 0; i < motionArgs.repeat; i++) {
+ var cursor = best;
+ for (var key in vim.marks) {
+ if (!isLowerCase(key)) {
+ continue;
+ }
+ var mark = vim.marks[key].find();
+ var isWrongDirection = (motionArgs.forward) ?
+ cursorIsBefore(mark, cursor) : cursorIsBefore(cursor, mark);
+
+ if (isWrongDirection) {
+ continue;
+ }
+ if (motionArgs.linewise && (mark.line == cursor.line)) {
+ continue;
+ }
+
+ var equal = cursorEqual(cursor, best);
+ var between = (motionArgs.forward) ?
+ cursorIsBetween(cursor, mark, best) :
+ cursorIsBetween(best, mark, cursor);
+
+ if (equal || between) {
+ best = mark;
+ }
+ }
+ }
+
+ if (motionArgs.linewise) {
+ // Vim places the cursor on the first non-whitespace character of
+ // the line if there is one, else it places the cursor at the end
+ // of the line, regardless of whether a mark was found.
+ best = Pos(best.line, findFirstNonWhiteSpaceCharacter(cm.getLine(best.line)));
+ }
+ return best;
+ },
+ moveByCharacters: function(_cm, head, motionArgs) {
+ var cur = head;
+ var repeat = motionArgs.repeat;
+ var ch = motionArgs.forward ? cur.ch + repeat : cur.ch - repeat;
+ return Pos(cur.line, ch);
+ },
+ moveByLines: function(cm, head, motionArgs, vim) {
+ var cur = head;
+ var endCh = cur.ch;
+ // Depending what our last motion was, we may want to do different
+ // things. If our last motion was moving vertically, we want to
+ // preserve the HPos from our last horizontal move. If our last motion
+ // was going to the end of a line, moving vertically we should go to
+ // the end of the line, etc.
+ switch (vim.lastMotion) {
+ case this.moveByLines:
+ case this.moveByDisplayLines:
+ case this.moveByScroll:
+ case this.moveToColumn:
+ case this.moveToEol:
+ endCh = vim.lastHPos;
+ break;
+ default:
+ vim.lastHPos = endCh;
+ }
+ var repeat = motionArgs.repeat+(motionArgs.repeatOffset||0);
+ var line = motionArgs.forward ? cur.line + repeat : cur.line - repeat;
+ var first = cm.firstLine();
+ var last = cm.lastLine();
+ // Vim go to line begin or line end when cursor at first/last line and
+ // move to previous/next line is triggered.
+ if (line < first && cur.line == first){
+ return this.moveToStartOfLine(cm, head, motionArgs, vim);
+ }else if (line > last && cur.line == last){
+ return this.moveToEol(cm, head, motionArgs, vim);
+ }
+ if (motionArgs.toFirstChar){
+ endCh=findFirstNonWhiteSpaceCharacter(cm.getLine(line));
+ vim.lastHPos = endCh;
+ }
+ vim.lastHSPos = cm.charCoords(Pos(line, endCh),'div').left;
+ return Pos(line, endCh);
+ },
+ moveByDisplayLines: function(cm, head, motionArgs, vim) {
+ var cur = head;
+ switch (vim.lastMotion) {
+ case this.moveByDisplayLines:
+ case this.moveByScroll:
+ case this.moveByLines:
+ case this.moveToColumn:
+ case this.moveToEol:
+ break;
+ default:
+ vim.lastHSPos = cm.charCoords(cur,'div').left;
+ }
+ var repeat = motionArgs.repeat;
+ var res=cm.findPosV(cur,(motionArgs.forward ? repeat : -repeat),'line',vim.lastHSPos);
+ if (res.hitSide) {
+ if (motionArgs.forward) {
+ var lastCharCoords = cm.charCoords(res, 'div');
+ var goalCoords = { top: lastCharCoords.top + 8, left: vim.lastHSPos };
+ var res = cm.coordsChar(goalCoords, 'div');
+ } else {
+ var resCoords = cm.charCoords(Pos(cm.firstLine(), 0), 'div');
+ resCoords.left = vim.lastHSPos;
+ res = cm.coordsChar(resCoords, 'div');
+ }
+ }
+ vim.lastHPos = res.ch;
+ return res;
+ },
+ moveByPage: function(cm, head, motionArgs) {
+ // CodeMirror only exposes functions that move the cursor page down, so
+ // doing this bad hack to move the cursor and move it back. evalInput
+ // will move the cursor to where it should be in the end.
+ var curStart = head;
+ var repeat = motionArgs.repeat;
+ return cm.findPosV(curStart, (motionArgs.forward ? repeat : -repeat), 'page');
+ },
+ moveByParagraph: function(cm, head, motionArgs) {
+ var dir = motionArgs.forward ? 1 : -1;
+ return findParagraph(cm, head, motionArgs.repeat, dir);
+ },
+ moveByScroll: function(cm, head, motionArgs, vim) {
+ var scrollbox = cm.getScrollInfo();
+ var curEnd = null;
+ var repeat = motionArgs.repeat;
+ if (!repeat) {
+ repeat = scrollbox.clientHeight / (2 * cm.defaultTextHeight());
+ }
+ var orig = cm.charCoords(head, 'local');
+ motionArgs.repeat = repeat;
+ var curEnd = motions.moveByDisplayLines(cm, head, motionArgs, vim);
+ if (!curEnd) {
+ return null;
+ }
+ var dest = cm.charCoords(curEnd, 'local');
+ cm.scrollTo(null, scrollbox.top + dest.top - orig.top);
+ return curEnd;
+ },
+ moveByWords: function(cm, head, motionArgs) {
+ return moveToWord(cm, head, motionArgs.repeat, !!motionArgs.forward,
+ !!motionArgs.wordEnd, !!motionArgs.bigWord);
+ },
+ moveTillCharacter: function(cm, _head, motionArgs) {
+ var repeat = motionArgs.repeat;
+ var curEnd = moveToCharacter(cm, repeat, motionArgs.forward,
+ motionArgs.selectedCharacter);
+ var increment = motionArgs.forward ? -1 : 1;
+ recordLastCharacterSearch(increment, motionArgs);
+ if (!curEnd) return null;
+ curEnd.ch += increment;
+ return curEnd;
+ },
+ moveToCharacter: function(cm, head, motionArgs) {
+ var repeat = motionArgs.repeat;
+ recordLastCharacterSearch(0, motionArgs);
+ return moveToCharacter(cm, repeat, motionArgs.forward,
+ motionArgs.selectedCharacter) || head;
+ },
+ moveToSymbol: function(cm, head, motionArgs) {
+ var repeat = motionArgs.repeat;
+ return findSymbol(cm, repeat, motionArgs.forward,
+ motionArgs.selectedCharacter) || head;
+ },
+ moveToColumn: function(cm, head, motionArgs, vim) {
+ var repeat = motionArgs.repeat;
+ // repeat is equivalent to which column we want to move to!
+ vim.lastHPos = repeat - 1;
+ vim.lastHSPos = cm.charCoords(head,'div').left;
+ return moveToColumn(cm, repeat);
+ },
+ moveToEol: function(cm, head, motionArgs, vim) {
+ var cur = head;
+ vim.lastHPos = Infinity;
+ var retval= Pos(cur.line + motionArgs.repeat - 1, Infinity);
+ var end=cm.clipPos(retval);
+ end.ch--;
+ vim.lastHSPos = cm.charCoords(end,'div').left;
+ return retval;
+ },
+ moveToFirstNonWhiteSpaceCharacter: function(cm, head) {
+ // Go to the start of the line where the text begins, or the end for
+ // whitespace-only lines
+ var cursor = head;
+ return Pos(cursor.line,
+ findFirstNonWhiteSpaceCharacter(cm.getLine(cursor.line)));
+ },
+ moveToMatchedSymbol: function(cm, head) {
+ var cursor = head;
+ var line = cursor.line;
+ var ch = cursor.ch;
+ var lineText = cm.getLine(line);
+ var symbol;
+ do {
+ symbol = lineText.charAt(ch++);
+ if (symbol && isMatchableSymbol(symbol)) {
+ var style = cm.getTokenTypeAt(Pos(line, ch));
+ if (style !== "string" && style !== "comment") {
+ break;
+ }
+ }
+ } while (symbol);
+ if (symbol) {
+ var matched = cm.findMatchingBracket(Pos(line, ch));
+ return matched.to;
+ } else {
+ return cursor;
+ }
+ },
+ moveToStartOfLine: function(_cm, head) {
+ return Pos(head.line, 0);
+ },
+ moveToLineOrEdgeOfDocument: function(cm, _head, motionArgs) {
+ var lineNum = motionArgs.forward ? cm.lastLine() : cm.firstLine();
+ if (motionArgs.repeatIsExplicit) {
+ lineNum = motionArgs.repeat - cm.getOption('firstLineNumber');
+ }
+ return Pos(lineNum,
+ findFirstNonWhiteSpaceCharacter(cm.getLine(lineNum)));
+ },
+ textObjectManipulation: function(cm, head, motionArgs, vim) {
+ // TODO: lots of possible exceptions that can be thrown here. Try da(
+ // outside of a () block.
+
+ // TODO: adding <> >< to this map doesn't work, presumably because
+ // they're operators
+ var mirroredPairs = {'(': ')', ')': '(',
+ '{': '}', '}': '{',
+ '[': ']', ']': '['};
+ var selfPaired = {'\'': true, '"': true};
+
+ var character = motionArgs.selectedCharacter;
+ // 'b' refers to '()' block.
+ // 'B' refers to '{}' block.
+ if (character == 'b') {
+ character = '(';
+ } else if (character == 'B') {
+ character = '{';
+ }
+
+ // Inclusive is the difference between a and i
+ // TODO: Instead of using the additional text object map to perform text
+ // object operations, merge the map into the defaultKeyMap and use
+ // motionArgs to define behavior. Define separate entries for 'aw',
+ // 'iw', 'a[', 'i[', etc.
+ var inclusive = !motionArgs.textObjectInner;
+
+ var tmp;
+ if (mirroredPairs[character]) {
+ tmp = selectCompanionObject(cm, head, character, inclusive);
+ } else if (selfPaired[character]) {
+ tmp = findBeginningAndEnd(cm, head, character, inclusive);
+ } else if (character === 'W') {
+ tmp = expandWordUnderCursor(cm, inclusive, true /** forward */,
+ true /** bigWord */);
+ } else if (character === 'w') {
+ tmp = expandWordUnderCursor(cm, inclusive, true /** forward */,
+ false /** bigWord */);
+ } else if (character === 'p') {
+ tmp = findParagraph(cm, head, motionArgs.repeat, 0, inclusive);
+ motionArgs.linewise = true;
+ if (vim.visualMode) {
+ if (!vim.visualLine) { vim.visualLine = true; }
+ } else {
+ var operatorArgs = vim.inputState.operatorArgs;
+ if (operatorArgs) { operatorArgs.linewise = true; }
+ tmp.end.line--;
+ }
+ } else {
+ // No text object defined for this, don't move.
+ return null;
+ }
+
+ if (!cm.state.vim.visualMode) {
+ return [tmp.start, tmp.end];
+ } else {
+ return expandSelection(cm, tmp.start, tmp.end);
+ }
+ },
+
+ repeatLastCharacterSearch: function(cm, head, motionArgs) {
+ var lastSearch = vimGlobalState.lastCharacterSearch;
+ var repeat = motionArgs.repeat;
+ var forward = motionArgs.forward === lastSearch.forward;
+ var increment = (lastSearch.increment ? 1 : 0) * (forward ? -1 : 1);
+ cm.moveH(-increment, 'char');
+ motionArgs.inclusive = forward ? true : false;
+ var curEnd = moveToCharacter(cm, repeat, forward, lastSearch.selectedCharacter);
+ if (!curEnd) {
+ cm.moveH(increment, 'char');
+ return head;
+ }
+ curEnd.ch += increment;
+ return curEnd;
+ }
+ };
+
+ function defineMotion(name, fn) {
+ motions[name] = fn;
+ }
+
+ function fillArray(val, times) {
+ var arr = [];
+ for (var i = 0; i < times; i++) {
+ arr.push(val);
+ }
+ return arr;
+ }
+ /**
+ * An operator acts on a text selection. It receives the list of selections
+ * as input. The corresponding CodeMirror selection is guaranteed to
+ * match the input selection.
+ */
+ var operators = {
+ change: function(cm, args, ranges) {
+ var finalHead, text;
+ var vim = cm.state.vim;
+ vimGlobalState.macroModeState.lastInsertModeChanges.inVisualBlock = vim.visualBlock;
+ if (!vim.visualMode) {
+ var anchor = ranges[0].anchor,
+ head = ranges[0].head;
+ text = cm.getRange(anchor, head);
+ var lastState = vim.lastEditInputState || {};
+ if (lastState.motion == "moveByWords" && !isWhiteSpaceString(text)) {
+ // Exclude trailing whitespace if the range is not all whitespace.
+ var match = (/\s+$/).exec(text);
+ if (match && lastState.motionArgs && lastState.motionArgs.forward) {
+ head = offsetCursor(head, 0, - match[0].length);
+ text = text.slice(0, - match[0].length);
+ }
+ }
+ var prevLineEnd = new Pos(anchor.line - 1, Number.MAX_VALUE);
+ var wasLastLine = cm.firstLine() == cm.lastLine();
+ if (head.line > cm.lastLine() && args.linewise && !wasLastLine) {
+ cm.replaceRange('', prevLineEnd, head);
+ } else {
+ cm.replaceRange('', anchor, head);
+ }
+ if (args.linewise) {
+ // Push the next line back down, if there is a next line.
+ if (!wasLastLine) {
+ cm.setCursor(prevLineEnd);
+ CodeMirror.commands.newlineAndIndent(cm);
+ }
+ // make sure cursor ends up at the end of the line.
+ anchor.ch = Number.MAX_VALUE;
+ }
+ finalHead = anchor;
+ } else {
+ text = cm.getSelection();
+ var replacement = fillArray('', ranges.length);
+ cm.replaceSelections(replacement);
+ finalHead = cursorMin(ranges[0].head, ranges[0].anchor);
+ }
+ vimGlobalState.registerController.pushText(
+ args.registerName, 'change', text,
+ args.linewise, ranges.length > 1);
+ actions.enterInsertMode(cm, {head: finalHead}, cm.state.vim);
+ },
+ // delete is a javascript keyword.
+ 'delete': function(cm, args, ranges) {
+ var finalHead, text;
+ var vim = cm.state.vim;
+ if (!vim.visualBlock) {
+ var anchor = ranges[0].anchor,
+ head = ranges[0].head;
+ if (args.linewise &&
+ head.line != cm.firstLine() &&
+ anchor.line == cm.lastLine() &&
+ anchor.line == head.line - 1) {
+ // Special case for dd on last line (and first line).
+ if (anchor.line == cm.firstLine()) {
+ anchor.ch = 0;
+ } else {
+ anchor = Pos(anchor.line - 1, lineLength(cm, anchor.line - 1));
+ }
+ }
+ text = cm.getRange(anchor, head);
+ cm.replaceRange('', anchor, head);
+ finalHead = anchor;
+ if (args.linewise) {
+ finalHead = motions.moveToFirstNonWhiteSpaceCharacter(cm, anchor);
+ }
+ } else {
+ text = cm.getSelection();
+ var replacement = fillArray('', ranges.length);
+ cm.replaceSelections(replacement);
+ finalHead = ranges[0].anchor;
+ }
+ vimGlobalState.registerController.pushText(
+ args.registerName, 'delete', text,
+ args.linewise, vim.visualBlock);
+ return clipCursorToContent(cm, finalHead);
+ },
+ indent: function(cm, args, ranges) {
+ var vim = cm.state.vim;
+ var startLine = ranges[0].anchor.line;
+ var endLine = vim.visualBlock ?
+ ranges[ranges.length - 1].anchor.line :
+ ranges[0].head.line;
+ // In visual mode, n> shifts the selection right n times, instead of
+ // shifting n lines right once.
+ var repeat = (vim.visualMode) ? args.repeat : 1;
+ if (args.linewise) {
+ // The only way to delete a newline is to delete until the start of
+ // the next line, so in linewise mode evalInput will include the next
+ // line. We don't want this in indent, so we go back a line.
+ endLine--;
+ }
+ for (var i = startLine; i <= endLine; i++) {
+ for (var j = 0; j < repeat; j++) {
+ cm.indentLine(i, args.indentRight);
+ }
+ }
+ return motions.moveToFirstNonWhiteSpaceCharacter(cm, ranges[0].anchor);
+ },
+ changeCase: function(cm, args, ranges, oldAnchor, newHead) {
+ var selections = cm.getSelections();
+ var swapped = [];
+ var toLower = args.toLower;
+ for (var j = 0; j < selections.length; j++) {
+ var toSwap = selections[j];
+ var text = '';
+ if (toLower === true) {
+ text = toSwap.toLowerCase();
+ } else if (toLower === false) {
+ text = toSwap.toUpperCase();
+ } else {
+ for (var i = 0; i < toSwap.length; i++) {
+ var character = toSwap.charAt(i);
+ text += isUpperCase(character) ? character.toLowerCase() :
+ character.toUpperCase();
+ }
+ }
+ swapped.push(text);
+ }
+ cm.replaceSelections(swapped);
+ if (args.shouldMoveCursor){
+ return newHead;
+ } else if (!cm.state.vim.visualMode && args.linewise && ranges[0].anchor.line + 1 == ranges[0].head.line) {
+ return motions.moveToFirstNonWhiteSpaceCharacter(cm, oldAnchor);
+ } else if (args.linewise){
+ return oldAnchor;
+ } else {
+ return cursorMin(ranges[0].anchor, ranges[0].head);
+ }
+ },
+ yank: function(cm, args, ranges, oldAnchor) {
+ var vim = cm.state.vim;
+ var text = cm.getSelection();
+ var endPos = vim.visualMode
+ ? cursorMin(vim.sel.anchor, vim.sel.head, ranges[0].head, ranges[0].anchor)
+ : oldAnchor;
+ vimGlobalState.registerController.pushText(
+ args.registerName, 'yank',
+ text, args.linewise, vim.visualBlock);
+ return endPos;
+ }
+ };
+
+ function defineOperator(name, fn) {
+ operators[name] = fn;
+ }
+
+ var actions = {
+ jumpListWalk: function(cm, actionArgs, vim) {
+ if (vim.visualMode) {
+ return;
+ }
+ var repeat = actionArgs.repeat;
+ var forward = actionArgs.forward;
+ var jumpList = vimGlobalState.jumpList;
+
+ var mark = jumpList.move(cm, forward ? repeat : -repeat);
+ var markPos = mark ? mark.find() : undefined;
+ markPos = markPos ? markPos : cm.getCursor();
+ cm.setCursor(markPos);
+ },
+ scroll: function(cm, actionArgs, vim) {
+ if (vim.visualMode) {
+ return;
+ }
+ var repeat = actionArgs.repeat || 1;
+ var lineHeight = cm.defaultTextHeight();
+ var top = cm.getScrollInfo().top;
+ var delta = lineHeight * repeat;
+ var newPos = actionArgs.forward ? top + delta : top - delta;
+ var cursor = copyCursor(cm.getCursor());
+ var cursorCoords = cm.charCoords(cursor, 'local');
+ if (actionArgs.forward) {
+ if (newPos > cursorCoords.top) {
+ cursor.line += (newPos - cursorCoords.top) / lineHeight;
+ cursor.line = Math.ceil(cursor.line);
+ cm.setCursor(cursor);
+ cursorCoords = cm.charCoords(cursor, 'local');
+ cm.scrollTo(null, cursorCoords.top);
+ } else {
+ // Cursor stays within bounds. Just reposition the scroll window.
+ cm.scrollTo(null, newPos);
+ }
+ } else {
+ var newBottom = newPos + cm.getScrollInfo().clientHeight;
+ if (newBottom < cursorCoords.bottom) {
+ cursor.line -= (cursorCoords.bottom - newBottom) / lineHeight;
+ cursor.line = Math.floor(cursor.line);
+ cm.setCursor(cursor);
+ cursorCoords = cm.charCoords(cursor, 'local');
+ cm.scrollTo(
+ null, cursorCoords.bottom - cm.getScrollInfo().clientHeight);
+ } else {
+ // Cursor stays within bounds. Just reposition the scroll window.
+ cm.scrollTo(null, newPos);
+ }
+ }
+ },
+ scrollToCursor: function(cm, actionArgs) {
+ var lineNum = cm.getCursor().line;
+ var charCoords = cm.charCoords(Pos(lineNum, 0), 'local');
+ var height = cm.getScrollInfo().clientHeight;
+ var y = charCoords.top;
+ var lineHeight = charCoords.bottom - y;
+ switch (actionArgs.position) {
+ case 'center': y = y - (height / 2) + lineHeight;
+ break;
+ case 'bottom': y = y - height + lineHeight;
+ break;
+ }
+ cm.scrollTo(null, y);
+ },
+ replayMacro: function(cm, actionArgs, vim) {
+ var registerName = actionArgs.selectedCharacter;
+ var repeat = actionArgs.repeat;
+ var macroModeState = vimGlobalState.macroModeState;
+ if (registerName == '@') {
+ registerName = macroModeState.latestRegister;
+ }
+ while(repeat--){
+ executeMacroRegister(cm, vim, macroModeState, registerName);
+ }
+ },
+ enterMacroRecordMode: function(cm, actionArgs) {
+ var macroModeState = vimGlobalState.macroModeState;
+ var registerName = actionArgs.selectedCharacter;
+ macroModeState.enterMacroRecordMode(cm, registerName);
+ },
+ enterInsertMode: function(cm, actionArgs, vim) {
+ if (cm.getOption('readOnly')) { return; }
+ vim.insertMode = true;
+ vim.insertModeRepeat = actionArgs && actionArgs.repeat || 1;
+ var insertAt = (actionArgs) ? actionArgs.insertAt : null;
+ var sel = vim.sel;
+ var head = actionArgs.head || cm.getCursor('head');
+ var height = cm.listSelections().length;
+ if (insertAt == 'eol') {
+ head = Pos(head.line, lineLength(cm, head.line));
+ } else if (insertAt == 'charAfter') {
+ head = offsetCursor(head, 0, 1);
+ } else if (insertAt == 'firstNonBlank') {
+ head = motions.moveToFirstNonWhiteSpaceCharacter(cm, head);
+ } else if (insertAt == 'startOfSelectedArea') {
+ if (!vim.visualBlock) {
+ if (sel.head.line < sel.anchor.line) {
+ head = sel.head;
+ } else {
+ head = Pos(sel.anchor.line, 0);
+ }
+ } else {
+ head = Pos(
+ Math.min(sel.head.line, sel.anchor.line),
+ Math.min(sel.head.ch, sel.anchor.ch));
+ height = Math.abs(sel.head.line - sel.anchor.line) + 1;
+ }
+ } else if (insertAt == 'endOfSelectedArea') {
+ if (!vim.visualBlock) {
+ if (sel.head.line >= sel.anchor.line) {
+ head = offsetCursor(sel.head, 0, 1);
+ } else {
+ head = Pos(sel.anchor.line, 0);
+ }
+ } else {
+ head = Pos(
+ Math.min(sel.head.line, sel.anchor.line),
+ Math.max(sel.head.ch + 1, sel.anchor.ch));
+ height = Math.abs(sel.head.line - sel.anchor.line) + 1;
+ }
+ } else if (insertAt == 'inplace') {
+ if (vim.visualMode){
+ return;
+ }
+ }
+ cm.setOption('keyMap', 'vim-insert');
+ cm.setOption('disableInput', false);
+ if (actionArgs && actionArgs.replace) {
+ // Handle Replace-mode as a special case of insert mode.
+ cm.toggleOverwrite(true);
+ cm.setOption('keyMap', 'vim-replace');
+ CodeMirror.signal(cm, "vim-mode-change", {mode: "replace"});
+ } else {
+ cm.setOption('keyMap', 'vim-insert');
+ CodeMirror.signal(cm, "vim-mode-change", {mode: "insert"});
+ }
+ if (!vimGlobalState.macroModeState.isPlaying) {
+ // Only record if not replaying.
+ cm.on('change', onChange);
+ CodeMirror.on(cm.getInputField(), 'keydown', onKeyEventTargetKeyDown);
+ }
+ if (vim.visualMode) {
+ exitVisualMode(cm);
+ }
+ selectForInsert(cm, head, height);
+ },
+ toggleVisualMode: function(cm, actionArgs, vim) {
+ var repeat = actionArgs.repeat;
+ var anchor = cm.getCursor();
+ var head;
+ // TODO: The repeat should actually select number of characters/lines
+ // equal to the repeat times the size of the previous visual
+ // operation.
+ if (!vim.visualMode) {
+ // Entering visual mode
+ vim.visualMode = true;
+ vim.visualLine = !!actionArgs.linewise;
+ vim.visualBlock = !!actionArgs.blockwise;
+ head = clipCursorToContent(
+ cm, Pos(anchor.line, anchor.ch + repeat - 1),
+ true /** includeLineBreak */);
+ vim.sel = {
+ anchor: anchor,
+ head: head
+ };
+ CodeMirror.signal(cm, "vim-mode-change", {mode: "visual", subMode: vim.visualLine ? "linewise" : vim.visualBlock ? "blockwise" : ""});
+ updateCmSelection(cm);
+ updateMark(cm, vim, '<', cursorMin(anchor, head));
+ updateMark(cm, vim, '>', cursorMax(anchor, head));
+ } else if (vim.visualLine ^ actionArgs.linewise ||
+ vim.visualBlock ^ actionArgs.blockwise) {
+ // Toggling between modes
+ vim.visualLine = !!actionArgs.linewise;
+ vim.visualBlock = !!actionArgs.blockwise;
+ CodeMirror.signal(cm, "vim-mode-change", {mode: "visual", subMode: vim.visualLine ? "linewise" : vim.visualBlock ? "blockwise" : ""});
+ updateCmSelection(cm);
+ } else {
+ exitVisualMode(cm);
+ }
+ },
+ reselectLastSelection: function(cm, _actionArgs, vim) {
+ var lastSelection = vim.lastSelection;
+ if (vim.visualMode) {
+ updateLastSelection(cm, vim);
+ }
+ if (lastSelection) {
+ var anchor = lastSelection.anchorMark.find();
+ var head = lastSelection.headMark.find();
+ if (!anchor || !head) {
+ // If the marks have been destroyed due to edits, do nothing.
+ return;
+ }
+ vim.sel = {
+ anchor: anchor,
+ head: head
+ };
+ vim.visualMode = true;
+ vim.visualLine = lastSelection.visualLine;
+ vim.visualBlock = lastSelection.visualBlock;
+ updateCmSelection(cm);
+ updateMark(cm, vim, '<', cursorMin(anchor, head));
+ updateMark(cm, vim, '>', cursorMax(anchor, head));
+ CodeMirror.signal(cm, 'vim-mode-change', {
+ mode: 'visual',
+ subMode: vim.visualLine ? 'linewise' :
+ vim.visualBlock ? 'blockwise' : ''});
+ }
+ },
+ joinLines: function(cm, actionArgs, vim) {
+ var curStart, curEnd;
+ if (vim.visualMode) {
+ curStart = cm.getCursor('anchor');
+ curEnd = cm.getCursor('head');
+ if (cursorIsBefore(curEnd, curStart)) {
+ var tmp = curEnd;
+ curEnd = curStart;
+ curStart = tmp;
+ }
+ curEnd.ch = lineLength(cm, curEnd.line) - 1;
+ } else {
+ // Repeat is the number of lines to join. Minimum 2 lines.
+ var repeat = Math.max(actionArgs.repeat, 2);
+ curStart = cm.getCursor();
+ curEnd = clipCursorToContent(cm, Pos(curStart.line + repeat - 1,
+ Infinity));
+ }
+ var finalCh = 0;
+ for (var i = curStart.line; i < curEnd.line; i++) {
+ finalCh = lineLength(cm, curStart.line);
+ var tmp = Pos(curStart.line + 1,
+ lineLength(cm, curStart.line + 1));
+ var text = cm.getRange(curStart, tmp);
+ text = text.replace(/\n\s*/g, ' ');
+ cm.replaceRange(text, curStart, tmp);
+ }
+ var curFinalPos = Pos(curStart.line, finalCh);
+ if (vim.visualMode) {
+ exitVisualMode(cm, false);
+ }
+ cm.setCursor(curFinalPos);
+ },
+ newLineAndEnterInsertMode: function(cm, actionArgs, vim) {
+ vim.insertMode = true;
+ var insertAt = copyCursor(cm.getCursor());
+ if (insertAt.line === cm.firstLine() && !actionArgs.after) {
+ // Special case for inserting newline before start of document.
+ cm.replaceRange('\n', Pos(cm.firstLine(), 0));
+ cm.setCursor(cm.firstLine(), 0);
+ } else {
+ insertAt.line = (actionArgs.after) ? insertAt.line :
+ insertAt.line - 1;
+ insertAt.ch = lineLength(cm, insertAt.line);
+ cm.setCursor(insertAt);
+ var newlineFn = CodeMirror.commands.newlineAndIndentContinueComment ||
+ CodeMirror.commands.newlineAndIndent;
+ newlineFn(cm);
+ }
+ this.enterInsertMode(cm, { repeat: actionArgs.repeat }, vim);
+ },
+ paste: function(cm, actionArgs, vim) {
+ var cur = copyCursor(cm.getCursor());
+ var register = vimGlobalState.registerController.getRegister(
+ actionArgs.registerName);
+ var text = register.toString();
+ if (!text) {
+ return;
+ }
+ if (actionArgs.matchIndent) {
+ var tabSize = cm.getOption("tabSize");
+ // length that considers tabs and tabSize
+ var whitespaceLength = function(str) {
+ var tabs = (str.split("\t").length - 1);
+ var spaces = (str.split(" ").length - 1);
+ return tabs * tabSize + spaces * 1;
+ };
+ var currentLine = cm.getLine(cm.getCursor().line);
+ var indent = whitespaceLength(currentLine.match(/^\s*/)[0]);
+ // chomp last newline b/c don't want it to match /^\s*/gm
+ var chompedText = text.replace(/\n$/, '');
+ var wasChomped = text !== chompedText;
+ var firstIndent = whitespaceLength(text.match(/^\s*/)[0]);
+ var text = chompedText.replace(/^\s*/gm, function(wspace) {
+ var newIndent = indent + (whitespaceLength(wspace) - firstIndent);
+ if (newIndent < 0) {
+ return "";
+ }
+ else if (cm.getOption("indentWithTabs")) {
+ var quotient = Math.floor(newIndent / tabSize);
+ return Array(quotient + 1).join('\t');
+ }
+ else {
+ return Array(newIndent + 1).join(' ');
+ }
+ });
+ text += wasChomped ? "\n" : "";
+ }
+ if (actionArgs.repeat > 1) {
+ var text = Array(actionArgs.repeat + 1).join(text);
+ }
+ var linewise = register.linewise;
+ var blockwise = register.blockwise;
+ if (linewise) {
+ if(vim.visualMode) {
+ text = vim.visualLine ? text.slice(0, -1) : '\n' + text.slice(0, text.length - 1) + '\n';
+ } else if (actionArgs.after) {
+ // Move the newline at the end to the start instead, and paste just
+ // before the newline character of the line we are on right now.
+ text = '\n' + text.slice(0, text.length - 1);
+ cur.ch = lineLength(cm, cur.line);
+ } else {
+ cur.ch = 0;
+ }
+ } else {
+ if (blockwise) {
+ text = text.split('\n');
+ for (var i = 0; i < text.length; i++) {
+ text[i] = (text[i] == '') ? ' ' : text[i];
+ }
+ }
+ cur.ch += actionArgs.after ? 1 : 0;
+ }
+ var curPosFinal;
+ var idx;
+ if (vim.visualMode) {
+ // save the pasted text for reselection if the need arises
+ vim.lastPastedText = text;
+ var lastSelectionCurEnd;
+ var selectedArea = getSelectedAreaRange(cm, vim);
+ var selectionStart = selectedArea[0];
+ var selectionEnd = selectedArea[1];
+ var selectedText = cm.getSelection();
+ var selections = cm.listSelections();
+ var emptyStrings = new Array(selections.length).join('1').split('1');
+ // save the curEnd marker before it get cleared due to cm.replaceRange.
+ if (vim.lastSelection) {
+ lastSelectionCurEnd = vim.lastSelection.headMark.find();
+ }
+ // push the previously selected text to unnamed register
+ vimGlobalState.registerController.unnamedRegister.setText(selectedText);
+ if (blockwise) {
+ // first delete the selected text
+ cm.replaceSelections(emptyStrings);
+ // Set new selections as per the block length of the yanked text
+ selectionEnd = Pos(selectionStart.line + text.length-1, selectionStart.ch);
+ cm.setCursor(selectionStart);
+ selectBlock(cm, selectionEnd);
+ cm.replaceSelections(text);
+ curPosFinal = selectionStart;
+ } else if (vim.visualBlock) {
+ cm.replaceSelections(emptyStrings);
+ cm.setCursor(selectionStart);
+ cm.replaceRange(text, selectionStart, selectionStart);
+ curPosFinal = selectionStart;
+ } else {
+ cm.replaceRange(text, selectionStart, selectionEnd);
+ curPosFinal = cm.posFromIndex(cm.indexFromPos(selectionStart) + text.length - 1);
+ }
+ // restore the the curEnd marker
+ if(lastSelectionCurEnd) {
+ vim.lastSelection.headMark = cm.setBookmark(lastSelectionCurEnd);
+ }
+ if (linewise) {
+ curPosFinal.ch=0;
+ }
+ } else {
+ if (blockwise) {
+ cm.setCursor(cur);
+ for (var i = 0; i < text.length; i++) {
+ var line = cur.line+i;
+ if (line > cm.lastLine()) {
+ cm.replaceRange('\n', Pos(line, 0));
+ }
+ var lastCh = lineLength(cm, line);
+ if (lastCh < cur.ch) {
+ extendLineToColumn(cm, line, cur.ch);
+ }
+ }
+ cm.setCursor(cur);
+ selectBlock(cm, Pos(cur.line + text.length-1, cur.ch));
+ cm.replaceSelections(text);
+ curPosFinal = cur;
+ } else {
+ cm.replaceRange(text, cur);
+ // Now fine tune the cursor to where we want it.
+ if (linewise && actionArgs.after) {
+ curPosFinal = Pos(
+ cur.line + 1,
+ findFirstNonWhiteSpaceCharacter(cm.getLine(cur.line + 1)));
+ } else if (linewise && !actionArgs.after) {
+ curPosFinal = Pos(
+ cur.line,
+ findFirstNonWhiteSpaceCharacter(cm.getLine(cur.line)));
+ } else if (!linewise && actionArgs.after) {
+ idx = cm.indexFromPos(cur);
+ curPosFinal = cm.posFromIndex(idx + text.length - 1);
+ } else {
+ idx = cm.indexFromPos(cur);
+ curPosFinal = cm.posFromIndex(idx + text.length);
+ }
+ }
+ }
+ if (vim.visualMode) {
+ exitVisualMode(cm, false);
+ }
+ cm.setCursor(curPosFinal);
+ },
+ undo: function(cm, actionArgs) {
+ cm.operation(function() {
+ repeatFn(cm, CodeMirror.commands.undo, actionArgs.repeat)();
+ cm.setCursor(cm.getCursor('anchor'));
+ });
+ },
+ redo: function(cm, actionArgs) {
+ repeatFn(cm, CodeMirror.commands.redo, actionArgs.repeat)();
+ },
+ setRegister: function(_cm, actionArgs, vim) {
+ vim.inputState.registerName = actionArgs.selectedCharacter;
+ },
+ setMark: function(cm, actionArgs, vim) {
+ var markName = actionArgs.selectedCharacter;
+ updateMark(cm, vim, markName, cm.getCursor());
+ },
+ replace: function(cm, actionArgs, vim) {
+ var replaceWith = actionArgs.selectedCharacter;
+ var curStart = cm.getCursor();
+ var replaceTo;
+ var curEnd;
+ var selections = cm.listSelections();
+ if (vim.visualMode) {
+ curStart = cm.getCursor('start');
+ curEnd = cm.getCursor('end');
+ } else {
+ var line = cm.getLine(curStart.line);
+ replaceTo = curStart.ch + actionArgs.repeat;
+ if (replaceTo > line.length) {
+ replaceTo=line.length;
+ }
+ curEnd = Pos(curStart.line, replaceTo);
+ }
+ if (replaceWith=='\n') {
+ if (!vim.visualMode) cm.replaceRange('', curStart, curEnd);
+ // special case, where vim help says to replace by just one line-break
+ (CodeMirror.commands.newlineAndIndentContinueComment || CodeMirror.commands.newlineAndIndent)(cm);
+ } else {
+ var replaceWithStr = cm.getRange(curStart, curEnd);
+ //replace all characters in range by selected, but keep linebreaks
+ replaceWithStr = replaceWithStr.replace(/[^\n]/g, replaceWith);
+ if (vim.visualBlock) {
+ // Tabs are split in visua block before replacing
+ var spaces = new Array(cm.getOption("tabSize")+1).join(' ');
+ replaceWithStr = cm.getSelection();
+ replaceWithStr = replaceWithStr.replace(/\t/g, spaces).replace(/[^\n]/g, replaceWith).split('\n');
+ cm.replaceSelections(replaceWithStr);
+ } else {
+ cm.replaceRange(replaceWithStr, curStart, curEnd);
+ }
+ if (vim.visualMode) {
+ curStart = cursorIsBefore(selections[0].anchor, selections[0].head) ?
+ selections[0].anchor : selections[0].head;
+ cm.setCursor(curStart);
+ exitVisualMode(cm, false);
+ } else {
+ cm.setCursor(offsetCursor(curEnd, 0, -1));
+ }
+ }
+ },
+ incrementNumberToken: function(cm, actionArgs) {
+ var cur = cm.getCursor();
+ var lineStr = cm.getLine(cur.line);
+ var re = /-?\d+/g;
+ var match;
+ var start;
+ var end;
+ var numberStr;
+ var token;
+ while ((match = re.exec(lineStr)) !== null) {
+ token = match[0];
+ start = match.index;
+ end = start + token.length;
+ if (cur.ch < end)break;
+ }
+ if (!actionArgs.backtrack && (end <= cur.ch))return;
+ if (token) {
+ var increment = actionArgs.increase ? 1 : -1;
+ var number = parseInt(token) + (increment * actionArgs.repeat);
+ var from = Pos(cur.line, start);
+ var to = Pos(cur.line, end);
+ numberStr = number.toString();
+ cm.replaceRange(numberStr, from, to);
+ } else {
+ return;
+ }
+ cm.setCursor(Pos(cur.line, start + numberStr.length - 1));
+ },
+ repeatLastEdit: function(cm, actionArgs, vim) {
+ var lastEditInputState = vim.lastEditInputState;
+ if (!lastEditInputState) { return; }
+ var repeat = actionArgs.repeat;
+ if (repeat && actionArgs.repeatIsExplicit) {
+ vim.lastEditInputState.repeatOverride = repeat;
+ } else {
+ repeat = vim.lastEditInputState.repeatOverride || repeat;
+ }
+ repeatLastEdit(cm, vim, repeat, false /** repeatForInsert */);
+ },
+ exitInsertMode: exitInsertMode
+ };
+
+ function defineAction(name, fn) {
+ actions[name] = fn;
+ }
+
+ /*
+ * Below are miscellaneous utility functions used by vim.js
+ */
+
+ /**
+ * Clips cursor to ensure that line is within the buffer's range
+ * If includeLineBreak is true, then allow cur.ch == lineLength.
+ */
+ function clipCursorToContent(cm, cur, includeLineBreak) {
+ var line = Math.min(Math.max(cm.firstLine(), cur.line), cm.lastLine() );
+ var maxCh = lineLength(cm, line) - 1;
+ maxCh = (includeLineBreak) ? maxCh + 1 : maxCh;
+ var ch = Math.min(Math.max(0, cur.ch), maxCh);
+ return Pos(line, ch);
+ }
+ function copyArgs(args) {
+ var ret = {};
+ for (var prop in args) {
+ if (args.hasOwnProperty(prop)) {
+ ret[prop] = args[prop];
+ }
+ }
+ return ret;
+ }
+ function offsetCursor(cur, offsetLine, offsetCh) {
+ if (typeof offsetLine === 'object') {
+ offsetCh = offsetLine.ch;
+ offsetLine = offsetLine.line;
+ }
+ return Pos(cur.line + offsetLine, cur.ch + offsetCh);
+ }
+ function getOffset(anchor, head) {
+ return {
+ line: head.line - anchor.line,
+ ch: head.line - anchor.line
+ };
+ }
+ function commandMatches(keys, keyMap, context, inputState) {
+ // Partial matches are not applied. They inform the key handler
+ // that the current key sequence is a subsequence of a valid key
+ // sequence, so that the key buffer is not cleared.
+ var match, partial = [], full = [];
+ for (var i = 0; i < keyMap.length; i++) {
+ var command = keyMap[i];
+ if (context == 'insert' && command.context != 'insert' ||
+ command.context && command.context != context ||
+ inputState.operator && command.type == 'action' ||
+ !(match = commandMatch(keys, command.keys))) { continue; }
+ if (match == 'partial') { partial.push(command); }
+ if (match == 'full') { full.push(command); }
+ }
+ return {
+ partial: partial.length && partial,
+ full: full.length && full
+ };
+ }
+ function commandMatch(pressed, mapped) {
+ if (mapped.slice(-11) == '<character>') {
+ // Last character matches anything.
+ var prefixLen = mapped.length - 11;
+ var pressedPrefix = pressed.slice(0, prefixLen);
+ var mappedPrefix = mapped.slice(0, prefixLen);
+ return pressedPrefix == mappedPrefix && pressed.length > prefixLen ? 'full' :
+ mappedPrefix.indexOf(pressedPrefix) == 0 ? 'partial' : false;
+ } else {
+ return pressed == mapped ? 'full' :
+ mapped.indexOf(pressed) == 0 ? 'partial' : false;
+ }
+ }
+ function lastChar(keys) {
+ var match = /^.*(<[\w\-]+>)$/.exec(keys);
+ var selectedCharacter = match ? match[1] : keys.slice(-1);
+ if (selectedCharacter.length > 1){
+ switch(selectedCharacter){
+ case '<CR>':
+ selectedCharacter='\n';
+ break;
+ case '<Space>':
+ selectedCharacter=' ';
+ break;
+ default:
+ break;
+ }
+ }
+ return selectedCharacter;
+ }
+ function repeatFn(cm, fn, repeat) {
+ return function() {
+ for (var i = 0; i < repeat; i++) {
+ fn(cm);
+ }
+ };
+ }
+ function copyCursor(cur) {
+ return Pos(cur.line, cur.ch);
+ }
+ function cursorEqual(cur1, cur2) {
+ return cur1.ch == cur2.ch && cur1.line == cur2.line;
+ }
+ function cursorIsBefore(cur1, cur2) {
+ if (cur1.line < cur2.line) {
+ return true;
+ }
+ if (cur1.line == cur2.line && cur1.ch < cur2.ch) {
+ return true;
+ }
+ return false;
+ }
+ function cursorMin(cur1, cur2) {
+ if (arguments.length > 2) {
+ cur2 = cursorMin.apply(undefined, Array.prototype.slice.call(arguments, 1));
+ }
+ return cursorIsBefore(cur1, cur2) ? cur1 : cur2;
+ }
+ function cursorMax(cur1, cur2) {
+ if (arguments.length > 2) {
+ cur2 = cursorMax.apply(undefined, Array.prototype.slice.call(arguments, 1));
+ }
+ return cursorIsBefore(cur1, cur2) ? cur2 : cur1;
+ }
+ function cursorIsBetween(cur1, cur2, cur3) {
+ // returns true if cur2 is between cur1 and cur3.
+ var cur1before2 = cursorIsBefore(cur1, cur2);
+ var cur2before3 = cursorIsBefore(cur2, cur3);
+ return cur1before2 && cur2before3;
+ }
+ function lineLength(cm, lineNum) {
+ return cm.getLine(lineNum).length;
+ }
+ function trim(s) {
+ if (s.trim) {
+ return s.trim();
+ }
+ return s.replace(/^\s+|\s+$/g, '');
+ }
+ function escapeRegex(s) {
+ return s.replace(/([.?*+$\[\]\/\\(){}|\-])/g, '\\$1');
+ }
+ function extendLineToColumn(cm, lineNum, column) {
+ var endCh = lineLength(cm, lineNum);
+ var spaces = new Array(column-endCh+1).join(' ');
+ cm.setCursor(Pos(lineNum, endCh));
+ cm.replaceRange(spaces, cm.getCursor());
+ }
+ // This functions selects a rectangular block
+ // of text with selectionEnd as any of its corner
+ // Height of block:
+ // Difference in selectionEnd.line and first/last selection.line
+ // Width of the block:
+ // Distance between selectionEnd.ch and any(first considered here) selection.ch
+ function selectBlock(cm, selectionEnd) {
+ var selections = [], ranges = cm.listSelections();
+ var head = copyCursor(cm.clipPos(selectionEnd));
+ var isClipped = !cursorEqual(selectionEnd, head);
+ var curHead = cm.getCursor('head');
+ var primIndex = getIndex(ranges, curHead);
+ var wasClipped = cursorEqual(ranges[primIndex].head, ranges[primIndex].anchor);
+ var max = ranges.length - 1;
+ var index = max - primIndex > primIndex ? max : 0;
+ var base = ranges[index].anchor;
+
+ var firstLine = Math.min(base.line, head.line);
+ var lastLine = Math.max(base.line, head.line);
+ var baseCh = base.ch, headCh = head.ch;
+
+ var dir = ranges[index].head.ch - baseCh;
+ var newDir = headCh - baseCh;
+ if (dir > 0 && newDir <= 0) {
+ baseCh++;
+ if (!isClipped) { headCh--; }
+ } else if (dir < 0 && newDir >= 0) {
+ baseCh--;
+ if (!wasClipped) { headCh++; }
+ } else if (dir < 0 && newDir == -1) {
+ baseCh--;
+ headCh++;
+ }
+ for (var line = firstLine; line <= lastLine; line++) {
+ var range = {anchor: new Pos(line, baseCh), head: new Pos(line, headCh)};
+ selections.push(range);
+ }
+ primIndex = head.line == lastLine ? selections.length - 1 : 0;
+ cm.setSelections(selections);
+ selectionEnd.ch = headCh;
+ base.ch = baseCh;
+ return base;
+ }
+ function selectForInsert(cm, head, height) {
+ var sel = [];
+ for (var i = 0; i < height; i++) {
+ var lineHead = offsetCursor(head, i, 0);
+ sel.push({anchor: lineHead, head: lineHead});
+ }
+ cm.setSelections(sel, 0);
+ }
+ // getIndex returns the index of the cursor in the selections.
+ function getIndex(ranges, cursor, end) {
+ for (var i = 0; i < ranges.length; i++) {
+ var atAnchor = end != 'head' && cursorEqual(ranges[i].anchor, cursor);
+ var atHead = end != 'anchor' && cursorEqual(ranges[i].head, cursor);
+ if (atAnchor || atHead) {
+ return i;
+ }
+ }
+ return -1;
+ }
+ function getSelectedAreaRange(cm, vim) {
+ var lastSelection = vim.lastSelection;
+ var getCurrentSelectedAreaRange = function() {
+ var selections = cm.listSelections();
+ var start = selections[0];
+ var end = selections[selections.length-1];
+ var selectionStart = cursorIsBefore(start.anchor, start.head) ? start.anchor : start.head;
+ var selectionEnd = cursorIsBefore(end.anchor, end.head) ? end.head : end.anchor;
+ return [selectionStart, selectionEnd];
+ };
+ var getLastSelectedAreaRange = function() {
+ var selectionStart = cm.getCursor();
+ var selectionEnd = cm.getCursor();
+ var block = lastSelection.visualBlock;
+ if (block) {
+ var width = block.width;
+ var height = block.height;
+ selectionEnd = Pos(selectionStart.line + height, selectionStart.ch + width);
+ var selections = [];
+ // selectBlock creates a 'proper' rectangular block.
+ // We do not want that in all cases, so we manually set selections.
+ for (var i = selectionStart.line; i < selectionEnd.line; i++) {
+ var anchor = Pos(i, selectionStart.ch);
+ var head = Pos(i, selectionEnd.ch);
+ var range = {anchor: anchor, head: head};
+ selections.push(range);
+ }
+ cm.setSelections(selections);
+ } else {
+ var start = lastSelection.anchorMark.find();
+ var end = lastSelection.headMark.find();
+ var line = end.line - start.line;
+ var ch = end.ch - start.ch;
+ selectionEnd = {line: selectionEnd.line + line, ch: line ? selectionEnd.ch : ch + selectionEnd.ch};
+ if (lastSelection.visualLine) {
+ selectionStart = Pos(selectionStart.line, 0);
+ selectionEnd = Pos(selectionEnd.line, lineLength(cm, selectionEnd.line));
+ }
+ cm.setSelection(selectionStart, selectionEnd);
+ }
+ return [selectionStart, selectionEnd];
+ };
+ if (!vim.visualMode) {
+ // In case of replaying the action.
+ return getLastSelectedAreaRange();
+ } else {
+ return getCurrentSelectedAreaRange();
+ }
+ }
+ // Updates the previous selection with the current selection's values. This
+ // should only be called in visual mode.
+ function updateLastSelection(cm, vim) {
+ var anchor = vim.sel.anchor;
+ var head = vim.sel.head;
+ // To accommodate the effect of lastPastedText in the last selection
+ if (vim.lastPastedText) {
+ head = cm.posFromIndex(cm.indexFromPos(anchor) + vim.lastPastedText.length);
+ vim.lastPastedText = null;
+ }
+ vim.lastSelection = {'anchorMark': cm.setBookmark(anchor),
+ 'headMark': cm.setBookmark(head),
+ 'anchor': copyCursor(anchor),
+ 'head': copyCursor(head),
+ 'visualMode': vim.visualMode,
+ 'visualLine': vim.visualLine,
+ 'visualBlock': vim.visualBlock};
+ }
+ function expandSelection(cm, start, end) {
+ var sel = cm.state.vim.sel;
+ var head = sel.head;
+ var anchor = sel.anchor;
+ var tmp;
+ if (cursorIsBefore(end, start)) {
+ tmp = end;
+ end = start;
+ start = tmp;
+ }
+ if (cursorIsBefore(head, anchor)) {
+ head = cursorMin(start, head);
+ anchor = cursorMax(anchor, end);
+ } else {
+ anchor = cursorMin(start, anchor);
+ head = cursorMax(head, end);
+ head = offsetCursor(head, 0, -1);
+ if (head.ch == -1 && head.line != cm.firstLine()) {
+ head = Pos(head.line - 1, lineLength(cm, head.line - 1));
+ }
+ }
+ return [anchor, head];
+ }
+ /**
+ * Updates the CodeMirror selection to match the provided vim selection.
+ * If no arguments are given, it uses the current vim selection state.
+ */
+ function updateCmSelection(cm, sel, mode) {
+ var vim = cm.state.vim;
+ sel = sel || vim.sel;
+ var mode = mode ||
+ vim.visualLine ? 'line' : vim.visualBlock ? 'block' : 'char';
+ var cmSel = makeCmSelection(cm, sel, mode);
+ cm.setSelections(cmSel.ranges, cmSel.primary);
+ updateFakeCursor(cm);
+ }
+ function makeCmSelection(cm, sel, mode, exclusive) {
+ var head = copyCursor(sel.head);
+ var anchor = copyCursor(sel.anchor);
+ if (mode == 'char') {
+ var headOffset = !exclusive && !cursorIsBefore(sel.head, sel.anchor) ? 1 : 0;
+ var anchorOffset = cursorIsBefore(sel.head, sel.anchor) ? 1 : 0;
+ head = offsetCursor(sel.head, 0, headOffset);
+ anchor = offsetCursor(sel.anchor, 0, anchorOffset);
+ return {
+ ranges: [{anchor: anchor, head: head}],
+ primary: 0
+ };
+ } else if (mode == 'line') {
+ if (!cursorIsBefore(sel.head, sel.anchor)) {
+ anchor.ch = 0;
+
+ var lastLine = cm.lastLine();
+ if (head.line > lastLine) {
+ head.line = lastLine;
+ }
+ head.ch = lineLength(cm, head.line);
+ } else {
+ head.ch = 0;
+ anchor.ch = lineLength(cm, anchor.line);
+ }
+ return {
+ ranges: [{anchor: anchor, head: head}],
+ primary: 0
+ };
+ } else if (mode == 'block') {
+ var top = Math.min(anchor.line, head.line),
+ left = Math.min(anchor.ch, head.ch),
+ bottom = Math.max(anchor.line, head.line),
+ right = Math.max(anchor.ch, head.ch) + 1;
+ var height = bottom - top + 1;
+ var primary = head.line == top ? 0 : height - 1;
+ var ranges = [];
+ for (var i = 0; i < height; i++) {
+ ranges.push({
+ anchor: Pos(top + i, left),
+ head: Pos(top + i, right)
+ });
+ }
+ return {
+ ranges: ranges,
+ primary: primary
+ };
+ }
+ }
+ function getHead(cm) {
+ var cur = cm.getCursor('head');
+ if (cm.getSelection().length == 1) {
+ // Small corner case when only 1 character is selected. The "real"
+ // head is the left of head and anchor.
+ cur = cursorMin(cur, cm.getCursor('anchor'));
+ }
+ return cur;
+ }
+
+ /**
+ * If moveHead is set to false, the CodeMirror selection will not be
+ * touched. The caller assumes the responsibility of putting the cursor
+ * in the right place.
+ */
+ function exitVisualMode(cm, moveHead) {
+ var vim = cm.state.vim;
+ if (moveHead !== false) {
+ cm.setCursor(clipCursorToContent(cm, vim.sel.head));
+ }
+ updateLastSelection(cm, vim);
+ vim.visualMode = false;
+ vim.visualLine = false;
+ vim.visualBlock = false;
+ CodeMirror.signal(cm, "vim-mode-change", {mode: "normal"});
+ if (vim.fakeCursor) {
+ vim.fakeCursor.clear();
+ }
+ }
+
+ // Remove any trailing newlines from the selection. For
+ // example, with the caret at the start of the last word on the line,
+ // 'dw' should word, but not the newline, while 'w' should advance the
+ // caret to the first character of the next line.
+ function clipToLine(cm, curStart, curEnd) {
+ var selection = cm.getRange(curStart, curEnd);
+ // Only clip if the selection ends with trailing newline + whitespace
+ if (/\n\s*$/.test(selection)) {
+ var lines = selection.split('\n');
+ // We know this is all whitespace.
+ lines.pop();
+
+ // Cases:
+ // 1. Last word is an empty line - do not clip the trailing '\n'
+ // 2. Last word is not an empty line - clip the trailing '\n'
+ var line;
+ // Find the line containing the last word, and clip all whitespace up
+ // to it.
+ for (var line = lines.pop(); lines.length > 0 && line && isWhiteSpaceString(line); line = lines.pop()) {
+ curEnd.line--;
+ curEnd.ch = 0;
+ }
+ // If the last word is not an empty line, clip an additional newline
+ if (line) {
+ curEnd.line--;
+ curEnd.ch = lineLength(cm, curEnd.line);
+ } else {
+ curEnd.ch = 0;
+ }
+ }
+ }
+
+ // Expand the selection to line ends.
+ function expandSelectionToLine(_cm, curStart, curEnd) {
+ curStart.ch = 0;
+ curEnd.ch = 0;
+ curEnd.line++;
+ }
+
+ function findFirstNonWhiteSpaceCharacter(text) {
+ if (!text) {
+ return 0;
+ }
+ var firstNonWS = text.search(/\S/);
+ return firstNonWS == -1 ? text.length : firstNonWS;
+ }
+
+ function expandWordUnderCursor(cm, inclusive, _forward, bigWord, noSymbol) {
+ var cur = getHead(cm);
+ var line = cm.getLine(cur.line);
+ var idx = cur.ch;
+
+ // Seek to first word or non-whitespace character, depending on if
+ // noSymbol is true.
+ var test = noSymbol ? wordCharTest[0] : bigWordCharTest [0];
+ while (!test(line.charAt(idx))) {
+ idx++;
+ if (idx >= line.length) { return null; }
+ }
+
+ if (bigWord) {
+ test = bigWordCharTest[0];
+ } else {
+ test = wordCharTest[0];
+ if (!test(line.charAt(idx))) {
+ test = wordCharTest[1];
+ }
+ }
+
+ var end = idx, start = idx;
+ while (test(line.charAt(end)) && end < line.length) { end++; }
+ while (test(line.charAt(start)) && start >= 0) { start--; }
+ start++;
+
+ if (inclusive) {
+ // If present, include all whitespace after word.
+ // Otherwise, include all whitespace before word, except indentation.
+ var wordEnd = end;
+ while (/\s/.test(line.charAt(end)) && end < line.length) { end++; }
+ if (wordEnd == end) {
+ var wordStart = start;
+ while (/\s/.test(line.charAt(start - 1)) && start > 0) { start--; }
+ if (!start) { start = wordStart; }
+ }
+ }
+ return { start: Pos(cur.line, start), end: Pos(cur.line, end) };
+ }
+
+ function recordJumpPosition(cm, oldCur, newCur) {
+ if (!cursorEqual(oldCur, newCur)) {
+ vimGlobalState.jumpList.add(cm, oldCur, newCur);
+ }
+ }
+
+ function recordLastCharacterSearch(increment, args) {
+ vimGlobalState.lastCharacterSearch.increment = increment;
+ vimGlobalState.lastCharacterSearch.forward = args.forward;
+ vimGlobalState.lastCharacterSearch.selectedCharacter = args.selectedCharacter;
+ }
+
+ var symbolToMode = {
+ '(': 'bracket', ')': 'bracket', '{': 'bracket', '}': 'bracket',
+ '[': 'section', ']': 'section',
+ '*': 'comment', '/': 'comment',
+ 'm': 'method', 'M': 'method',
+ '#': 'preprocess'
+ };
+ var findSymbolModes = {
+ bracket: {
+ isComplete: function(state) {
+ if (state.nextCh === state.symb) {
+ state.depth++;
+ if (state.depth >= 1)return true;
+ } else if (state.nextCh === state.reverseSymb) {
+ state.depth--;
+ }
+ return false;
+ }
+ },
+ section: {
+ init: function(state) {
+ state.curMoveThrough = true;
+ state.symb = (state.forward ? ']' : '[') === state.symb ? '{' : '}';
+ },
+ isComplete: function(state) {
+ return state.index === 0 && state.nextCh === state.symb;
+ }
+ },
+ comment: {
+ isComplete: function(state) {
+ var found = state.lastCh === '*' && state.nextCh === '/';
+ state.lastCh = state.nextCh;
+ return found;
+ }
+ },
+ // TODO: The original Vim implementation only operates on level 1 and 2.
+ // The current implementation doesn't check for code block level and
+ // therefore it operates on any levels.
+ method: {
+ init: function(state) {
+ state.symb = (state.symb === 'm' ? '{' : '}');
+ state.reverseSymb = state.symb === '{' ? '}' : '{';
+ },
+ isComplete: function(state) {
+ if (state.nextCh === state.symb)return true;
+ return false;
+ }
+ },
+ preprocess: {
+ init: function(state) {
+ state.index = 0;
+ },
+ isComplete: function(state) {
+ if (state.nextCh === '#') {
+ var token = state.lineText.match(/#(\w+)/)[1];
+ if (token === 'endif') {
+ if (state.forward && state.depth === 0) {
+ return true;
+ }
+ state.depth++;
+ } else if (token === 'if') {
+ if (!state.forward && state.depth === 0) {
+ return true;
+ }
+ state.depth--;
+ }
+ if (token === 'else' && state.depth === 0)return true;
+ }
+ return false;
+ }
+ }
+ };
+ function findSymbol(cm, repeat, forward, symb) {
+ var cur = copyCursor(cm.getCursor());
+ var increment = forward ? 1 : -1;
+ var endLine = forward ? cm.lineCount() : -1;
+ var curCh = cur.ch;
+ var line = cur.line;
+ var lineText = cm.getLine(line);
+ var state = {
+ lineText: lineText,
+ nextCh: lineText.charAt(curCh),
+ lastCh: null,
+ index: curCh,
+ symb: symb,
+ reverseSymb: (forward ? { ')': '(', '}': '{' } : { '(': ')', '{': '}' })[symb],
+ forward: forward,
+ depth: 0,
+ curMoveThrough: false
+ };
+ var mode = symbolToMode[symb];
+ if (!mode)return cur;
+ var init = findSymbolModes[mode].init;
+ var isComplete = findSymbolModes[mode].isComplete;
+ if (init) { init(state); }
+ while (line !== endLine && repeat) {
+ state.index += increment;
+ state.nextCh = state.lineText.charAt(state.index);
+ if (!state.nextCh) {
+ line += increment;
+ state.lineText = cm.getLine(line) || '';
+ if (increment > 0) {
+ state.index = 0;
+ } else {
+ var lineLen = state.lineText.length;
+ state.index = (lineLen > 0) ? (lineLen-1) : 0;
+ }
+ state.nextCh = state.lineText.charAt(state.index);
+ }
+ if (isComplete(state)) {
+ cur.line = line;
+ cur.ch = state.index;
+ repeat--;
+ }
+ }
+ if (state.nextCh || state.curMoveThrough) {
+ return Pos(line, state.index);
+ }
+ return cur;
+ }
+
+ /**
+ * Returns the boundaries of the next word. If the cursor in the middle of
+ * the word, then returns the boundaries of the current word, starting at
+ * the cursor. If the cursor is at the start/end of a word, and we are going
+ * forward/backward, respectively, find the boundaries of the next word.
+ *
+ * @param {CodeMirror} cm CodeMirror object.
+ * @param {Cursor} cur The cursor position.
+ * @param {boolean} forward True to search forward. False to search
+ * backward.
+ * @param {boolean} bigWord True if punctuation count as part of the word.
+ * False if only [a-zA-Z0-9] characters count as part of the word.
+ * @param {boolean} emptyLineIsWord True if empty lines should be treated
+ * as words.
+ * @return {Object{from:number, to:number, line: number}} The boundaries of
+ * the word, or null if there are no more words.
+ */
+ function findWord(cm, cur, forward, bigWord, emptyLineIsWord) {
+ var lineNum = cur.line;
+ var pos = cur.ch;
+ var line = cm.getLine(lineNum);
+ var dir = forward ? 1 : -1;
+ var charTests = bigWord ? bigWordCharTest: wordCharTest;
+
+ if (emptyLineIsWord && line == '') {
+ lineNum += dir;
+ line = cm.getLine(lineNum);
+ if (!isLine(cm, lineNum)) {
+ return null;
+ }
+ pos = (forward) ? 0 : line.length;
+ }
+
+ while (true) {
+ if (emptyLineIsWord && line == '') {
+ return { from: 0, to: 0, line: lineNum };
+ }
+ var stop = (dir > 0) ? line.length : -1;
+ var wordStart = stop, wordEnd = stop;
+ // Find bounds of next word.
+ while (pos != stop) {
+ var foundWord = false;
+ for (var i = 0; i < charTests.length && !foundWord; ++i) {
+ if (charTests[i](line.charAt(pos))) {
+ wordStart = pos;
+ // Advance to end of word.
+ while (pos != stop && charTests[i](line.charAt(pos))) {
+ pos += dir;
+ }
+ wordEnd = pos;
+ foundWord = wordStart != wordEnd;
+ if (wordStart == cur.ch && lineNum == cur.line &&
+ wordEnd == wordStart + dir) {
+ // We started at the end of a word. Find the next one.
+ continue;
+ } else {
+ return {
+ from: Math.min(wordStart, wordEnd + 1),
+ to: Math.max(wordStart, wordEnd),
+ line: lineNum };
+ }
+ }
+ }
+ if (!foundWord) {
+ pos += dir;
+ }
+ }
+ // Advance to next/prev line.
+ lineNum += dir;
+ if (!isLine(cm, lineNum)) {
+ return null;
+ }
+ line = cm.getLine(lineNum);
+ pos = (dir > 0) ? 0 : line.length;
+ }
+ }
+
+ /**
+ * @param {CodeMirror} cm CodeMirror object.
+ * @param {Pos} cur The position to start from.
+ * @param {int} repeat Number of words to move past.
+ * @param {boolean} forward True to search forward. False to search
+ * backward.
+ * @param {boolean} wordEnd True to move to end of word. False to move to
+ * beginning of word.
+ * @param {boolean} bigWord True if punctuation count as part of the word.
+ * False if only alphabet characters count as part of the word.
+ * @return {Cursor} The position the cursor should move to.
+ */
+ function moveToWord(cm, cur, repeat, forward, wordEnd, bigWord) {
+ var curStart = copyCursor(cur);
+ var words = [];
+ if (forward && !wordEnd || !forward && wordEnd) {
+ repeat++;
+ }
+ // For 'e', empty lines are not considered words, go figure.
+ var emptyLineIsWord = !(forward && wordEnd);
+ for (var i = 0; i < repeat; i++) {
+ var word = findWord(cm, cur, forward, bigWord, emptyLineIsWord);
+ if (!word) {
+ var eodCh = lineLength(cm, cm.lastLine());
+ words.push(forward
+ ? {line: cm.lastLine(), from: eodCh, to: eodCh}
+ : {line: 0, from: 0, to: 0});
+ break;
+ }
+ words.push(word);
+ cur = Pos(word.line, forward ? (word.to - 1) : word.from);
+ }
+ var shortCircuit = words.length != repeat;
+ var firstWord = words[0];
+ var lastWord = words.pop();
+ if (forward && !wordEnd) {
+ // w
+ if (!shortCircuit && (firstWord.from != curStart.ch || firstWord.line != curStart.line)) {
+ // We did not start in the middle of a word. Discard the extra word at the end.
+ lastWord = words.pop();
+ }
+ return Pos(lastWord.line, lastWord.from);
+ } else if (forward && wordEnd) {
+ return Pos(lastWord.line, lastWord.to - 1);
+ } else if (!forward && wordEnd) {
+ // ge
+ if (!shortCircuit && (firstWord.to != curStart.ch || firstWord.line != curStart.line)) {
+ // We did not start in the middle of a word. Discard the extra word at the end.
+ lastWord = words.pop();
+ }
+ return Pos(lastWord.line, lastWord.to);
+ } else {
+ // b
+ return Pos(lastWord.line, lastWord.from);
+ }
+ }
+
+ function moveToCharacter(cm, repeat, forward, character) {
+ var cur = cm.getCursor();
+ var start = cur.ch;
+ var idx;
+ for (var i = 0; i < repeat; i ++) {
+ var line = cm.getLine(cur.line);
+ idx = charIdxInLine(start, line, character, forward, true);
+ if (idx == -1) {
+ return null;
+ }
+ start = idx;
+ }
+ return Pos(cm.getCursor().line, idx);
+ }
+
+ function moveToColumn(cm, repeat) {
+ // repeat is always >= 1, so repeat - 1 always corresponds
+ // to the column we want to go to.
+ var line = cm.getCursor().line;
+ return clipCursorToContent(cm, Pos(line, repeat - 1));
+ }
+
+ function updateMark(cm, vim, markName, pos) {
+ if (!inArray(markName, validMarks)) {
+ return;
+ }
+ if (vim.marks[markName]) {
+ vim.marks[markName].clear();
+ }
+ vim.marks[markName] = cm.setBookmark(pos);
+ }
+
+ function charIdxInLine(start, line, character, forward, includeChar) {
+ // Search for char in line.
+ // motion_options: {forward, includeChar}
+ // If includeChar = true, include it too.
+ // If forward = true, search forward, else search backwards.
+ // If char is not found on this line, do nothing
+ var idx;
+ if (forward) {
+ idx = line.indexOf(character, start + 1);
+ if (idx != -1 && !includeChar) {
+ idx -= 1;
+ }
+ } else {
+ idx = line.lastIndexOf(character, start - 1);
+ if (idx != -1 && !includeChar) {
+ idx += 1;
+ }
+ }
+ return idx;
+ }
+
+ function findParagraph(cm, head, repeat, dir, inclusive) {
+ var line = head.line;
+ var min = cm.firstLine();
+ var max = cm.lastLine();
+ var start, end, i = line;
+ function isEmpty(i) { return !cm.getLine(i); }
+ function isBoundary(i, dir, any) {
+ if (any) { return isEmpty(i) != isEmpty(i + dir); }
+ return !isEmpty(i) && isEmpty(i + dir);
+ }
+ if (dir) {
+ while (min <= i && i <= max && repeat > 0) {
+ if (isBoundary(i, dir)) { repeat--; }
+ i += dir;
+ }
+ return new Pos(i, 0);
+ }
+
+ var vim = cm.state.vim;
+ if (vim.visualLine && isBoundary(line, 1, true)) {
+ var anchor = vim.sel.anchor;
+ if (isBoundary(anchor.line, -1, true)) {
+ if (!inclusive || anchor.line != line) {
+ line += 1;
+ }
+ }
+ }
+ var startState = isEmpty(line);
+ for (i = line; i <= max && repeat; i++) {
+ if (isBoundary(i, 1, true)) {
+ if (!inclusive || isEmpty(i) != startState) {
+ repeat--;
+ }
+ }
+ }
+ end = new Pos(i, 0);
+ // select boundary before paragraph for the last one
+ if (i > max && !startState) { startState = true; }
+ else { inclusive = false; }
+ for (i = line; i > min; i--) {
+ if (!inclusive || isEmpty(i) == startState || i == line) {
+ if (isBoundary(i, -1, true)) { break; }
+ }
+ }
+ start = new Pos(i, 0);
+ return { start: start, end: end };
+ }
+
+ // TODO: perhaps this finagling of start and end positions belonds
+ // in codemirror/replaceRange?
+ function selectCompanionObject(cm, head, symb, inclusive) {
+ var cur = head, start, end;
+
+ var bracketRegexp = ({
+ '(': /[()]/, ')': /[()]/,
+ '[': /[[\]]/, ']': /[[\]]/,
+ '{': /[{}]/, '}': /[{}]/})[symb];
+ var openSym = ({
+ '(': '(', ')': '(',
+ '[': '[', ']': '[',
+ '{': '{', '}': '{'})[symb];
+ var curChar = cm.getLine(cur.line).charAt(cur.ch);
+ // Due to the behavior of scanForBracket, we need to add an offset if the
+ // cursor is on a matching open bracket.
+ var offset = curChar === openSym ? 1 : 0;
+
+ start = cm.scanForBracket(Pos(cur.line, cur.ch + offset), -1, null, {'bracketRegex': bracketRegexp});
+ end = cm.scanForBracket(Pos(cur.line, cur.ch + offset), 1, null, {'bracketRegex': bracketRegexp});
+
+ if (!start || !end) {
+ return { start: cur, end: cur };
+ }
+
+ start = start.pos;
+ end = end.pos;
+
+ if ((start.line == end.line && start.ch > end.ch)
+ || (start.line > end.line)) {
+ var tmp = start;
+ start = end;
+ end = tmp;
+ }
+
+ if (inclusive) {
+ end.ch += 1;
+ } else {
+ start.ch += 1;
+ }
+
+ return { start: start, end: end };
+ }
+
+ // Takes in a symbol and a cursor and tries to simulate text objects that
+ // have identical opening and closing symbols
+ // TODO support across multiple lines
+ function findBeginningAndEnd(cm, head, symb, inclusive) {
+ var cur = copyCursor(head);
+ var line = cm.getLine(cur.line);
+ var chars = line.split('');
+ var start, end, i, len;
+ var firstIndex = chars.indexOf(symb);
+
+ // the decision tree is to always look backwards for the beginning first,
+ // but if the cursor is in front of the first instance of the symb,
+ // then move the cursor forward
+ if (cur.ch < firstIndex) {
+ cur.ch = firstIndex;
+ // Why is this line even here???
+ // cm.setCursor(cur.line, firstIndex+1);
+ }
+ // otherwise if the cursor is currently on the closing symbol
+ else if (firstIndex < cur.ch && chars[cur.ch] == symb) {
+ end = cur.ch; // assign end to the current cursor
+ --cur.ch; // make sure to look backwards
+ }
+
+ // if we're currently on the symbol, we've got a start
+ if (chars[cur.ch] == symb && !end) {
+ start = cur.ch + 1; // assign start to ahead of the cursor
+ } else {
+ // go backwards to find the start
+ for (i = cur.ch; i > -1 && !start; i--) {
+ if (chars[i] == symb) {
+ start = i + 1;
+ }
+ }
+ }
+
+ // look forwards for the end symbol
+ if (start && !end) {
+ for (i = start, len = chars.length; i < len && !end; i++) {
+ if (chars[i] == symb) {
+ end = i;
+ }
+ }
+ }
+
+ // nothing found
+ if (!start || !end) {
+ return { start: cur, end: cur };
+ }
+
+ // include the symbols
+ if (inclusive) {
+ --start; ++end;
+ }
+
+ return {
+ start: Pos(cur.line, start),
+ end: Pos(cur.line, end)
+ };
+ }
+
+ // Search functions
+ defineOption('pcre', true, 'boolean');
+ function SearchState() {}
+ SearchState.prototype = {
+ getQuery: function() {
+ return vimGlobalState.query;
+ },
+ setQuery: function(query) {
+ vimGlobalState.query = query;
+ },
+ getOverlay: function() {
+ return this.searchOverlay;
+ },
+ setOverlay: function(overlay) {
+ this.searchOverlay = overlay;
+ },
+ isReversed: function() {
+ return vimGlobalState.isReversed;
+ },
+ setReversed: function(reversed) {
+ vimGlobalState.isReversed = reversed;
+ },
+ getScrollbarAnnotate: function() {
+ return this.annotate;
+ },
+ setScrollbarAnnotate: function(annotate) {
+ this.annotate = annotate;
+ }
+ };
+ function getSearchState(cm) {
+ var vim = cm.state.vim;
+ return vim.searchState_ || (vim.searchState_ = new SearchState());
+ }
+ function dialog(cm, template, shortText, onClose, options) {
+ if (cm.openDialog) {
+ cm.openDialog(template, onClose, { bottom: true, value: options.value,
+ onKeyDown: options.onKeyDown, onKeyUp: options.onKeyUp,
+ selectValueOnOpen: false});
+ }
+ else {
+ onClose(prompt(shortText, ''));
+ }
+ }
+ function splitBySlash(argString) {
+ var slashes = findUnescapedSlashes(argString) || [];
+ if (!slashes.length) return [];
+ var tokens = [];
+ // in case of strings like foo/bar
+ if (slashes[0] !== 0) return;
+ for (var i = 0; i < slashes.length; i++) {
+ if (typeof slashes[i] == 'number')
+ tokens.push(argString.substring(slashes[i] + 1, slashes[i+1]));
+ }
+ return tokens;
+ }
+
+ function findUnescapedSlashes(str) {
+ var escapeNextChar = false;
+ var slashes = [];
+ for (var i = 0; i < str.length; i++) {
+ var c = str.charAt(i);
+ if (!escapeNextChar && c == '/') {
+ slashes.push(i);
+ }
+ escapeNextChar = !escapeNextChar && (c == '\\');
+ }
+ return slashes;
+ }
+
+ // Translates a search string from ex (vim) syntax into javascript form.
+ function translateRegex(str) {
+ // When these match, add a '\' if unescaped or remove one if escaped.
+ var specials = '|(){';
+ // Remove, but never add, a '\' for these.
+ var unescape = '}';
+ var escapeNextChar = false;
+ var out = [];
+ for (var i = -1; i < str.length; i++) {
+ var c = str.charAt(i) || '';
+ var n = str.charAt(i+1) || '';
+ var specialComesNext = (n && specials.indexOf(n) != -1);
+ if (escapeNextChar) {
+ if (c !== '\\' || !specialComesNext) {
+ out.push(c);
+ }
+ escapeNextChar = false;
+ } else {
+ if (c === '\\') {
+ escapeNextChar = true;
+ // Treat the unescape list as special for removing, but not adding '\'.
+ if (n && unescape.indexOf(n) != -1) {
+ specialComesNext = true;
+ }
+ // Not passing this test means removing a '\'.
+ if (!specialComesNext || n === '\\') {
+ out.push(c);
+ }
+ } else {
+ out.push(c);
+ if (specialComesNext && n !== '\\') {
+ out.push('\\');
+ }
+ }
+ }
+ }
+ return out.join('');
+ }
+
+ // Translates the replace part of a search and replace from ex (vim) syntax into
+ // javascript form. Similar to translateRegex, but additionally fixes back references
+ // (translates '\[0..9]' to '$[0..9]') and follows different rules for escaping '$'.
+ var charUnescapes = {'\\n': '\n', '\\r': '\r', '\\t': '\t'};
+ function translateRegexReplace(str) {
+ var escapeNextChar = false;
+ var out = [];
+ for (var i = -1; i < str.length; i++) {
+ var c = str.charAt(i) || '';
+ var n = str.charAt(i+1) || '';
+ if (charUnescapes[c + n]) {
+ out.push(charUnescapes[c+n]);
+ i++;
+ } else if (escapeNextChar) {
+ // At any point in the loop, escapeNextChar is true if the previous
+ // character was a '\' and was not escaped.
+ out.push(c);
+ escapeNextChar = false;
+ } else {
+ if (c === '\\') {
+ escapeNextChar = true;
+ if ((isNumber(n) || n === '$')) {
+ out.push('$');
+ } else if (n !== '/' && n !== '\\') {
+ out.push('\\');
+ }
+ } else {
+ if (c === '$') {
+ out.push('$');
+ }
+ out.push(c);
+ if (n === '/') {
+ out.push('\\');
+ }
+ }
+ }
+ }
+ return out.join('');
+ }
+
+ // Unescape \ and / in the replace part, for PCRE mode.
+ var unescapes = {'\\/': '/', '\\\\': '\\', '\\n': '\n', '\\r': '\r', '\\t': '\t'};
+ function unescapeRegexReplace(str) {
+ var stream = new CodeMirror.StringStream(str);
+ var output = [];
+ while (!stream.eol()) {
+ // Search for \.
+ while (stream.peek() && stream.peek() != '\\') {
+ output.push(stream.next());
+ }
+ var matched = false;
+ for (var matcher in unescapes) {
+ if (stream.match(matcher, true)) {
+ matched = true;
+ output.push(unescapes[matcher]);
+ break;
+ }
+ }
+ if (!matched) {
+ // Don't change anything
+ output.push(stream.next());
+ }
+ }
+ return output.join('');
+ }
+
+ /**
+ * Extract the regular expression from the query and return a Regexp object.
+ * Returns null if the query is blank.
+ * If ignoreCase is passed in, the Regexp object will have the 'i' flag set.
+ * If smartCase is passed in, and the query contains upper case letters,
+ * then ignoreCase is overridden, and the 'i' flag will not be set.
+ * If the query contains the /i in the flag part of the regular expression,
+ * then both ignoreCase and smartCase are ignored, and 'i' will be passed
+ * through to the Regex object.
+ */
+ function parseQuery(query, ignoreCase, smartCase) {
+ // First update the last search register
+ var lastSearchRegister = vimGlobalState.registerController.getRegister('/');
+ lastSearchRegister.setText(query);
+ // Check if the query is already a regex.
+ if (query instanceof RegExp) { return query; }
+ // First try to extract regex + flags from the input. If no flags found,
+ // extract just the regex. IE does not accept flags directly defined in
+ // the regex string in the form /regex/flags
+ var slashes = findUnescapedSlashes(query);
+ var regexPart;
+ var forceIgnoreCase;
+ if (!slashes.length) {
+ // Query looks like 'regexp'
+ regexPart = query;
+ } else {
+ // Query looks like 'regexp/...'
+ regexPart = query.substring(0, slashes[0]);
+ var flagsPart = query.substring(slashes[0]);
+ forceIgnoreCase = (flagsPart.indexOf('i') != -1);
+ }
+ if (!regexPart) {
+ return null;
+ }
+ if (!getOption('pcre')) {
+ regexPart = translateRegex(regexPart);
+ }
+ if (smartCase) {
+ ignoreCase = (/^[^A-Z]*$/).test(regexPart);
+ }
+ var regexp = new RegExp(regexPart,
+ (ignoreCase || forceIgnoreCase) ? 'i' : undefined);
+ return regexp;
+ }
+ function showConfirm(cm, text) {
+ if (cm.openNotification) {
+ cm.openNotification('<span style="color: red">' + text + '</span>',
+ {bottom: true, duration: 5000});
+ } else {
+ alert(text);
+ }
+ }
+ function makePrompt(prefix, desc) {
+ var raw = '<span style="font-family: monospace; white-space: pre">' +
+ (prefix || "") + '<input type="text"></span>';
+ if (desc)
+ raw += ' <span style="color: #888">' + desc + '</span>';
+ return raw;
+ }
+ var searchPromptDesc = '(Javascript regexp)';
+ function showPrompt(cm, options) {
+ var shortText = (options.prefix || '') + ' ' + (options.desc || '');
+ var prompt = makePrompt(options.prefix, options.desc);
+ dialog(cm, prompt, shortText, options.onClose, options);
+ }
+ function regexEqual(r1, r2) {
+ if (r1 instanceof RegExp && r2 instanceof RegExp) {
+ var props = ['global', 'multiline', 'ignoreCase', 'source'];
+ for (var i = 0; i < props.length; i++) {
+ var prop = props[i];
+ if (r1[prop] !== r2[prop]) {
+ return false;
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+ // Returns true if the query is valid.
+ function updateSearchQuery(cm, rawQuery, ignoreCase, smartCase) {
+ if (!rawQuery) {
+ return;
+ }
+ var state = getSearchState(cm);
+ var query = parseQuery(rawQuery, !!ignoreCase, !!smartCase);
+ if (!query) {
+ return;
+ }
+ highlightSearchMatches(cm, query);
+ if (regexEqual(query, state.getQuery())) {
+ return query;
+ }
+ state.setQuery(query);
+ return query;
+ }
+ function searchOverlay(query) {
+ if (query.source.charAt(0) == '^') {
+ var matchSol = true;
+ }
+ return {
+ token: function(stream) {
+ if (matchSol && !stream.sol()) {
+ stream.skipToEnd();
+ return;
+ }
+ var match = stream.match(query, false);
+ if (match) {
+ if (match[0].length == 0) {
+ // Matched empty string, skip to next.
+ stream.next();
+ return 'searching';
+ }
+ if (!stream.sol()) {
+ // Backtrack 1 to match \b
+ stream.backUp(1);
+ if (!query.exec(stream.next() + match[0])) {
+ stream.next();
+ return null;
+ }
+ }
+ stream.match(query);
+ return 'searching';
+ }
+ while (!stream.eol()) {
+ stream.next();
+ if (stream.match(query, false)) break;
+ }
+ },
+ query: query
+ };
+ }
+ function highlightSearchMatches(cm, query) {
+ var searchState = getSearchState(cm);
+ var overlay = searchState.getOverlay();
+ if (!overlay || query != overlay.query) {
+ if (overlay) {
+ cm.removeOverlay(overlay);
+ }
+ overlay = searchOverlay(query);
+ cm.addOverlay(overlay);
+ if (cm.showMatchesOnScrollbar) {
+ if (searchState.getScrollbarAnnotate()) {
+ searchState.getScrollbarAnnotate().clear();
+ }
+ searchState.setScrollbarAnnotate(cm.showMatchesOnScrollbar(query));
+ }
+ searchState.setOverlay(overlay);
+ }
+ }
+ function findNext(cm, prev, query, repeat) {
+ if (repeat === undefined) { repeat = 1; }
+ return cm.operation(function() {
+ var pos = cm.getCursor();
+ var cursor = cm.getSearchCursor(query, pos);
+ for (var i = 0; i < repeat; i++) {
+ var found = cursor.find(prev);
+ if (i == 0 && found && cursorEqual(cursor.from(), pos)) { found = cursor.find(prev); }
+ if (!found) {
+ // SearchCursor may have returned null because it hit EOF, wrap
+ // around and try again.
+ cursor = cm.getSearchCursor(query,
+ (prev) ? Pos(cm.lastLine()) : Pos(cm.firstLine(), 0) );
+ if (!cursor.find(prev)) {
+ return;
+ }
+ }
+ }
+ return cursor.from();
+ });
+ }
+ function clearSearchHighlight(cm) {
+ var state = getSearchState(cm);
+ cm.removeOverlay(getSearchState(cm).getOverlay());
+ state.setOverlay(null);
+ if (state.getScrollbarAnnotate()) {
+ state.getScrollbarAnnotate().clear();
+ state.setScrollbarAnnotate(null);
+ }
+ }
+ /**
+ * Check if pos is in the specified range, INCLUSIVE.
+ * Range can be specified with 1 or 2 arguments.
+ * If the first range argument is an array, treat it as an array of line
+ * numbers. Match pos against any of the lines.
+ * If the first range argument is a number,
+ * if there is only 1 range argument, check if pos has the same line
+ * number
+ * if there are 2 range arguments, then check if pos is in between the two
+ * range arguments.
+ */
+ function isInRange(pos, start, end) {
+ if (typeof pos != 'number') {
+ // Assume it is a cursor position. Get the line number.
+ pos = pos.line;
+ }
+ if (start instanceof Array) {
+ return inArray(pos, start);
+ } else {
+ if (end) {
+ return (pos >= start && pos <= end);
+ } else {
+ return pos == start;
+ }
+ }
+ }
+ function getUserVisibleLines(cm) {
+ var scrollInfo = cm.getScrollInfo();
+ var occludeToleranceTop = 6;
+ var occludeToleranceBottom = 10;
+ var from = cm.coordsChar({left:0, top: occludeToleranceTop + scrollInfo.top}, 'local');
+ var bottomY = scrollInfo.clientHeight - occludeToleranceBottom + scrollInfo.top;
+ var to = cm.coordsChar({left:0, top: bottomY}, 'local');
+ return {top: from.line, bottom: to.line};
+ }
+
+ var ExCommandDispatcher = function() {
+ this.buildCommandMap_();
+ };
+ ExCommandDispatcher.prototype = {
+ processCommand: function(cm, input, opt_params) {
+ var that = this;
+ cm.operation(function () {
+ cm.curOp.isVimOp = true;
+ that._processCommand(cm, input, opt_params);
+ });
+ },
+ _processCommand: function(cm, input, opt_params) {
+ var vim = cm.state.vim;
+ var commandHistoryRegister = vimGlobalState.registerController.getRegister(':');
+ var previousCommand = commandHistoryRegister.toString();
+ if (vim.visualMode) {
+ exitVisualMode(cm);
+ }
+ var inputStream = new CodeMirror.StringStream(input);
+ // update ": with the latest command whether valid or invalid
+ commandHistoryRegister.setText(input);
+ var params = opt_params || {};
+ params.input = input;
+ try {
+ this.parseInput_(cm, inputStream, params);
+ } catch(e) {
+ showConfirm(cm, e);
+ throw e;
+ }
+ var command;
+ var commandName;
+ if (!params.commandName) {
+ // If only a line range is defined, move to the line.
+ if (params.line !== undefined) {
+ commandName = 'move';
+ }
+ } else {
+ command = this.matchCommand_(params.commandName);
+ if (command) {
+ commandName = command.name;
+ if (command.excludeFromCommandHistory) {
+ commandHistoryRegister.setText(previousCommand);
+ }
+ this.parseCommandArgs_(inputStream, params, command);
+ if (command.type == 'exToKey') {
+ // Handle Ex to Key mapping.
+ for (var i = 0; i < command.toKeys.length; i++) {
+ CodeMirror.Vim.handleKey(cm, command.toKeys[i], 'mapping');
+ }
+ return;
+ } else if (command.type == 'exToEx') {
+ // Handle Ex to Ex mapping.
+ this.processCommand(cm, command.toInput);
+ return;
+ }
+ }
+ }
+ if (!commandName) {
+ showConfirm(cm, 'Not an editor command ":' + input + '"');
+ return;
+ }
+ try {
+ exCommands[commandName](cm, params);
+ // Possibly asynchronous commands (e.g. substitute, which might have a
+ // user confirmation), are responsible for calling the callback when
+ // done. All others have it taken care of for them here.
+ if ((!command || !command.possiblyAsync) && params.callback) {
+ params.callback();
+ }
+ } catch(e) {
+ showConfirm(cm, e);
+ throw e;
+ }
+ },
+ parseInput_: function(cm, inputStream, result) {
+ inputStream.eatWhile(':');
+ // Parse range.
+ if (inputStream.eat('%')) {
+ result.line = cm.firstLine();
+ result.lineEnd = cm.lastLine();
+ } else {
+ result.line = this.parseLineSpec_(cm, inputStream);
+ if (result.line !== undefined && inputStream.eat(',')) {
+ result.lineEnd = this.parseLineSpec_(cm, inputStream);
+ }
+ }
+
+ // Parse command name.
+ var commandMatch = inputStream.match(/^(\w+)/);
+ if (commandMatch) {
+ result.commandName = commandMatch[1];
+ } else {
+ result.commandName = inputStream.match(/.*/)[0];
+ }
+
+ return result;
+ },
+ parseLineSpec_: function(cm, inputStream) {
+ var numberMatch = inputStream.match(/^(\d+)/);
+ if (numberMatch) {
+ return parseInt(numberMatch[1], 10) - 1;
+ }
+ switch (inputStream.next()) {
+ case '.':
+ return cm.getCursor().line;
+ case '$':
+ return cm.lastLine();
+ case '\'':
+ var mark = cm.state.vim.marks[inputStream.next()];
+ if (mark && mark.find()) {
+ return mark.find().line;
+ }
+ throw new Error('Mark not set');
+ default:
+ inputStream.backUp(1);
+ return undefined;
+ }
+ },
+ parseCommandArgs_: function(inputStream, params, command) {
+ if (inputStream.eol()) {
+ return;
+ }
+ params.argString = inputStream.match(/.*/)[0];
+ // Parse command-line arguments
+ var delim = command.argDelimiter || /\s+/;
+ var args = trim(params.argString).split(delim);
+ if (args.length && args[0]) {
+ params.args = args;
+ }
+ },
+ matchCommand_: function(commandName) {
+ // Return the command in the command map that matches the shortest
+ // prefix of the passed in command name. The match is guaranteed to be
+ // unambiguous if the defaultExCommandMap's shortNames are set up
+ // correctly. (see @code{defaultExCommandMap}).
+ for (var i = commandName.length; i > 0; i--) {
+ var prefix = commandName.substring(0, i);
+ if (this.commandMap_[prefix]) {
+ var command = this.commandMap_[prefix];
+ if (command.name.indexOf(commandName) === 0) {
+ return command;
+ }
+ }
+ }
+ return null;
+ },
+ buildCommandMap_: function() {
+ this.commandMap_ = {};
+ for (var i = 0; i < defaultExCommandMap.length; i++) {
+ var command = defaultExCommandMap[i];
+ var key = command.shortName || command.name;
+ this.commandMap_[key] = command;
+ }
+ },
+ map: function(lhs, rhs, ctx) {
+ if (lhs != ':' && lhs.charAt(0) == ':') {
+ if (ctx) { throw Error('Mode not supported for ex mappings'); }
+ var commandName = lhs.substring(1);
+ if (rhs != ':' && rhs.charAt(0) == ':') {
+ // Ex to Ex mapping
+ this.commandMap_[commandName] = {
+ name: commandName,
+ type: 'exToEx',
+ toInput: rhs.substring(1),
+ user: true
+ };
+ } else {
+ // Ex to key mapping
+ this.commandMap_[commandName] = {
+ name: commandName,
+ type: 'exToKey',
+ toKeys: rhs,
+ user: true
+ };
+ }
+ } else {
+ if (rhs != ':' && rhs.charAt(0) == ':') {
+ // Key to Ex mapping.
+ var mapping = {
+ keys: lhs,
+ type: 'keyToEx',
+ exArgs: { input: rhs.substring(1) },
+ user: true};
+ if (ctx) { mapping.context = ctx; }
+ defaultKeymap.unshift(mapping);
+ } else {
+ // Key to key mapping
+ var mapping = {
+ keys: lhs,
+ type: 'keyToKey',
+ toKeys: rhs,
+ user: true
+ };
+ if (ctx) { mapping.context = ctx; }
+ defaultKeymap.unshift(mapping);
+ }
+ }
+ },
+ unmap: function(lhs, ctx) {
+ if (lhs != ':' && lhs.charAt(0) == ':') {
+ // Ex to Ex or Ex to key mapping
+ if (ctx) { throw Error('Mode not supported for ex mappings'); }
+ var commandName = lhs.substring(1);
+ if (this.commandMap_[commandName] && this.commandMap_[commandName].user) {
+ delete this.commandMap_[commandName];
+ return;
+ }
+ } else {
+ // Key to Ex or key to key mapping
+ var keys = lhs;
+ for (var i = 0; i < defaultKeymap.length; i++) {
+ if (keys == defaultKeymap[i].keys
+ && defaultKeymap[i].context === ctx
+ && defaultKeymap[i].user) {
+ defaultKeymap.splice(i, 1);
+ return;
+ }
+ }
+ }
+ throw Error('No such mapping.');
+ }
+ };
+
+ var exCommands = {
+ colorscheme: function(cm, params) {
+ if (!params.args || params.args.length < 1) {
+ showConfirm(cm, cm.getOption('theme'));
+ return;
+ }
+ cm.setOption('theme', params.args[0]);
+ },
+ map: function(cm, params, ctx) {
+ var mapArgs = params.args;
+ if (!mapArgs || mapArgs.length < 2) {
+ if (cm) {
+ showConfirm(cm, 'Invalid mapping: ' + params.input);
+ }
+ return;
+ }
+ exCommandDispatcher.map(mapArgs[0], mapArgs[1], ctx);
+ },
+ imap: function(cm, params) { this.map(cm, params, 'insert'); },
+ nmap: function(cm, params) { this.map(cm, params, 'normal'); },
+ vmap: function(cm, params) { this.map(cm, params, 'visual'); },
+ unmap: function(cm, params, ctx) {
+ var mapArgs = params.args;
+ if (!mapArgs || mapArgs.length < 1) {
+ if (cm) {
+ showConfirm(cm, 'No such mapping: ' + params.input);
+ }
+ return;
+ }
+ exCommandDispatcher.unmap(mapArgs[0], ctx);
+ },
+ move: function(cm, params) {
+ commandDispatcher.processCommand(cm, cm.state.vim, {
+ type: 'motion',
+ motion: 'moveToLineOrEdgeOfDocument',
+ motionArgs: { forward: false, explicitRepeat: true,
+ linewise: true },
+ repeatOverride: params.line+1});
+ },
+ set: function(cm, params) {
+ var setArgs = params.args;
+ // Options passed through to the setOption/getOption calls. May be passed in by the
+ // local/global versions of the set command
+ var setCfg = params.setCfg || {};
+ if (!setArgs || setArgs.length < 1) {
+ if (cm) {
+ showConfirm(cm, 'Invalid mapping: ' + params.input);
+ }
+ return;
+ }
+ var expr = setArgs[0].split('=');
+ var optionName = expr[0];
+ var value = expr[1];
+ var forceGet = false;
+
+ if (optionName.charAt(optionName.length - 1) == '?') {
+ // If post-fixed with ?, then the set is actually a get.
+ if (value) { throw Error('Trailing characters: ' + params.argString); }
+ optionName = optionName.substring(0, optionName.length - 1);
+ forceGet = true;
+ }
+ if (value === undefined && optionName.substring(0, 2) == 'no') {
+ // To set boolean options to false, the option name is prefixed with
+ // 'no'.
+ optionName = optionName.substring(2);
+ value = false;
+ }
+
+ var optionIsBoolean = options[optionName] && options[optionName].type == 'boolean';
+ if (optionIsBoolean && value == undefined) {
+ // Calling set with a boolean option sets it to true.
+ value = true;
+ }
+ // If no value is provided, then we assume this is a get.
+ if (!optionIsBoolean && value === undefined || forceGet) {
+ var oldValue = getOption(optionName, cm, setCfg);
+ if (oldValue === true || oldValue === false) {
+ showConfirm(cm, ' ' + (oldValue ? '' : 'no') + optionName);
+ } else {
+ showConfirm(cm, ' ' + optionName + '=' + oldValue);
+ }
+ } else {
+ setOption(optionName, value, cm, setCfg);
+ }
+ },
+ setlocal: function (cm, params) {
+ // setCfg is passed through to setOption
+ params.setCfg = {scope: 'local'};
+ this.set(cm, params);
+ },
+ setglobal: function (cm, params) {
+ // setCfg is passed through to setOption
+ params.setCfg = {scope: 'global'};
+ this.set(cm, params);
+ },
+ registers: function(cm, params) {
+ var regArgs = params.args;
+ var registers = vimGlobalState.registerController.registers;
+ var regInfo = '----------Registers----------<br><br>';
+ if (!regArgs) {
+ for (var registerName in registers) {
+ var text = registers[registerName].toString();
+ if (text.length) {
+ regInfo += '"' + registerName + ' ' + text + '<br>';
+ }
+ }
+ } else {
+ var registerName;
+ regArgs = regArgs.join('');
+ for (var i = 0; i < regArgs.length; i++) {
+ registerName = regArgs.charAt(i);
+ if (!vimGlobalState.registerController.isValidRegister(registerName)) {
+ continue;
+ }
+ var register = registers[registerName] || new Register();
+ regInfo += '"' + registerName + ' ' + register.toString() + '<br>';
+ }
+ }
+ showConfirm(cm, regInfo);
+ },
+ sort: function(cm, params) {
+ var reverse, ignoreCase, unique, number;
+ function parseArgs() {
+ if (params.argString) {
+ var args = new CodeMirror.StringStream(params.argString);
+ if (args.eat('!')) { reverse = true; }
+ if (args.eol()) { return; }
+ if (!args.eatSpace()) { return 'Invalid arguments'; }
+ var opts = args.match(/[a-z]+/);
+ if (opts) {
+ opts = opts[0];
+ ignoreCase = opts.indexOf('i') != -1;
+ unique = opts.indexOf('u') != -1;
+ var decimal = opts.indexOf('d') != -1 && 1;
+ var hex = opts.indexOf('x') != -1 && 1;
+ var octal = opts.indexOf('o') != -1 && 1;
+ if (decimal + hex + octal > 1) { return 'Invalid arguments'; }
+ number = decimal && 'decimal' || hex && 'hex' || octal && 'octal';
+ }
+ if (args.match(/\/.*\//)) { return 'patterns not supported'; }
+ }
+ }
+ var err = parseArgs();
+ if (err) {
+ showConfirm(cm, err + ': ' + params.argString);
+ return;
+ }
+ var lineStart = params.line || cm.firstLine();
+ var lineEnd = params.lineEnd || params.line || cm.lastLine();
+ if (lineStart == lineEnd) { return; }
+ var curStart = Pos(lineStart, 0);
+ var curEnd = Pos(lineEnd, lineLength(cm, lineEnd));
+ var text = cm.getRange(curStart, curEnd).split('\n');
+ var numberRegex = (number == 'decimal') ? /(-?)([\d]+)/ :
+ (number == 'hex') ? /(-?)(?:0x)?([0-9a-f]+)/i :
+ (number == 'octal') ? /([0-7]+)/ : null;
+ var radix = (number == 'decimal') ? 10 : (number == 'hex') ? 16 : (number == 'octal') ? 8 : null;
+ var numPart = [], textPart = [];
+ if (number) {
+ for (var i = 0; i < text.length; i++) {
+ if (numberRegex.exec(text[i])) {
+ numPart.push(text[i]);
+ } else {
+ textPart.push(text[i]);
+ }
+ }
+ } else {
+ textPart = text;
+ }
+ function compareFn(a, b) {
+ if (reverse) { var tmp; tmp = a; a = b; b = tmp; }
+ if (ignoreCase) { a = a.toLowerCase(); b = b.toLowerCase(); }
+ var anum = number && numberRegex.exec(a);
+ var bnum = number && numberRegex.exec(b);
+ if (!anum) { return a < b ? -1 : 1; }
+ anum = parseInt((anum[1] + anum[2]).toLowerCase(), radix);
+ bnum = parseInt((bnum[1] + bnum[2]).toLowerCase(), radix);
+ return anum - bnum;
+ }
+ numPart.sort(compareFn);
+ textPart.sort(compareFn);
+ text = (!reverse) ? textPart.concat(numPart) : numPart.concat(textPart);
+ if (unique) { // Remove duplicate lines
+ var textOld = text;
+ var lastLine;
+ text = [];
+ for (var i = 0; i < textOld.length; i++) {
+ if (textOld[i] != lastLine) {
+ text.push(textOld[i]);
+ }
+ lastLine = textOld[i];
+ }
+ }
+ cm.replaceRange(text.join('\n'), curStart, curEnd);
+ },
+ global: function(cm, params) {
+ // a global command is of the form
+ // :[range]g/pattern/[cmd]
+ // argString holds the string /pattern/[cmd]
+ var argString = params.argString;
+ if (!argString) {
+ showConfirm(cm, 'Regular Expression missing from global');
+ return;
+ }
+ // range is specified here
+ var lineStart = (params.line !== undefined) ? params.line : cm.firstLine();
+ var lineEnd = params.lineEnd || params.line || cm.lastLine();
+ // get the tokens from argString
+ var tokens = splitBySlash(argString);
+ var regexPart = argString, cmd;
+ if (tokens.length) {
+ regexPart = tokens[0];
+ cmd = tokens.slice(1, tokens.length).join('/');
+ }
+ if (regexPart) {
+ // If regex part is empty, then use the previous query. Otherwise
+ // use the regex part as the new query.
+ try {
+ updateSearchQuery(cm, regexPart, true /** ignoreCase */,
+ true /** smartCase */);
+ } catch (e) {
+ showConfirm(cm, 'Invalid regex: ' + regexPart);
+ return;
+ }
+ }
+ // now that we have the regexPart, search for regex matches in the
+ // specified range of lines
+ var query = getSearchState(cm).getQuery();
+ var matchedLines = [], content = '';
+ for (var i = lineStart; i <= lineEnd; i++) {
+ var matched = query.test(cm.getLine(i));
+ if (matched) {
+ matchedLines.push(i+1);
+ content+= cm.getLine(i) + '<br>';
+ }
+ }
+ // if there is no [cmd], just display the list of matched lines
+ if (!cmd) {
+ showConfirm(cm, content);
+ return;
+ }
+ var index = 0;
+ var nextCommand = function() {
+ if (index < matchedLines.length) {
+ var command = matchedLines[index] + cmd;
+ exCommandDispatcher.processCommand(cm, command, {
+ callback: nextCommand
+ });
+ }
+ index++;
+ };
+ nextCommand();
+ },
+ substitute: function(cm, params) {
+ if (!cm.getSearchCursor) {
+ throw new Error('Search feature not available. Requires searchcursor.js or ' +
+ 'any other getSearchCursor implementation.');
+ }
+ var argString = params.argString;
+ var tokens = argString ? splitBySlash(argString) : [];
+ var regexPart, replacePart = '', trailing, flagsPart, count;
+ var confirm = false; // Whether to confirm each replace.
+ var global = false; // True to replace all instances on a line, false to replace only 1.
+ if (tokens.length) {
+ regexPart = tokens[0];
+ replacePart = tokens[1];
+ if (replacePart !== undefined) {
+ if (getOption('pcre')) {
+ replacePart = unescapeRegexReplace(replacePart);
+ } else {
+ replacePart = translateRegexReplace(replacePart);
+ }
+ vimGlobalState.lastSubstituteReplacePart = replacePart;
+ }
+ trailing = tokens[2] ? tokens[2].split(' ') : [];
+ } else {
+ // either the argString is empty or its of the form ' hello/world'
+ // actually splitBySlash returns a list of tokens
+ // only if the string starts with a '/'
+ if (argString && argString.length) {
+ showConfirm(cm, 'Substitutions should be of the form ' +
+ ':s/pattern/replace/');
+ return;
+ }
+ }
+ // After the 3rd slash, we can have flags followed by a space followed
+ // by count.
+ if (trailing) {
+ flagsPart = trailing[0];
+ count = parseInt(trailing[1]);
+ if (flagsPart) {
+ if (flagsPart.indexOf('c') != -1) {
+ confirm = true;
+ flagsPart.replace('c', '');
+ }
+ if (flagsPart.indexOf('g') != -1) {
+ global = true;
+ flagsPart.replace('g', '');
+ }
+ regexPart = regexPart + '/' + flagsPart;
+ }
+ }
+ if (regexPart) {
+ // If regex part is empty, then use the previous query. Otherwise use
+ // the regex part as the new query.
+ try {
+ updateSearchQuery(cm, regexPart, true /** ignoreCase */,
+ true /** smartCase */);
+ } catch (e) {
+ showConfirm(cm, 'Invalid regex: ' + regexPart);
+ return;
+ }
+ }
+ replacePart = replacePart || vimGlobalState.lastSubstituteReplacePart;
+ if (replacePart === undefined) {
+ showConfirm(cm, 'No previous substitute regular expression');
+ return;
+ }
+ var state = getSearchState(cm);
+ var query = state.getQuery();
+ var lineStart = (params.line !== undefined) ? params.line : cm.getCursor().line;
+ var lineEnd = params.lineEnd || lineStart;
+ if (lineStart == cm.firstLine() && lineEnd == cm.lastLine()) {
+ lineEnd = Infinity;
+ }
+ if (count) {
+ lineStart = lineEnd;
+ lineEnd = lineStart + count - 1;
+ }
+ var startPos = clipCursorToContent(cm, Pos(lineStart, 0));
+ var cursor = cm.getSearchCursor(query, startPos);
+ doReplace(cm, confirm, global, lineStart, lineEnd, cursor, query, replacePart, params.callback);
+ },
+ redo: CodeMirror.commands.redo,
+ undo: CodeMirror.commands.undo,
+ write: function(cm) {
+ if (CodeMirror.commands.save) {
+ // If a save command is defined, call it.
+ CodeMirror.commands.save(cm);
+ } else if (cm.save) {
+ // Saves to text area if no save command is defined and cm.save() is available.
+ cm.save();
+ }
+ },
+ nohlsearch: function(cm) {
+ clearSearchHighlight(cm);
+ },
+ yank: function (cm) {
+ var cur = copyCursor(cm.getCursor());
+ var line = cur.line;
+ var lineText = cm.getLine(line);
+ vimGlobalState.registerController.pushText(
+ '0', 'yank', lineText, true, true);
+ },
+ delmarks: function(cm, params) {
+ if (!params.argString || !trim(params.argString)) {
+ showConfirm(cm, 'Argument required');
+ return;
+ }
+
+ var state = cm.state.vim;
+ var stream = new CodeMirror.StringStream(trim(params.argString));
+ while (!stream.eol()) {
+ stream.eatSpace();
+
+ // Record the streams position at the beginning of the loop for use
+ // in error messages.
+ var count = stream.pos;
+
+ if (!stream.match(/[a-zA-Z]/, false)) {
+ showConfirm(cm, 'Invalid argument: ' + params.argString.substring(count));
+ return;
+ }
+
+ var sym = stream.next();
+ // Check if this symbol is part of a range
+ if (stream.match('-', true)) {
+ // This symbol is part of a range.
+
+ // The range must terminate at an alphabetic character.
+ if (!stream.match(/[a-zA-Z]/, false)) {
+ showConfirm(cm, 'Invalid argument: ' + params.argString.substring(count));
+ return;
+ }
+
+ var startMark = sym;
+ var finishMark = stream.next();
+ // The range must terminate at an alphabetic character which
+ // shares the same case as the start of the range.
+ if (isLowerCase(startMark) && isLowerCase(finishMark) ||
+ isUpperCase(startMark) && isUpperCase(finishMark)) {
+ var start = startMark.charCodeAt(0);
+ var finish = finishMark.charCodeAt(0);
+ if (start >= finish) {
+ showConfirm(cm, 'Invalid argument: ' + params.argString.substring(count));
+ return;
+ }
+
+ // Because marks are always ASCII values, and we have
+ // determined that they are the same case, we can use
+ // their char codes to iterate through the defined range.
+ for (var j = 0; j <= finish - start; j++) {
+ var mark = String.fromCharCode(start + j);
+ delete state.marks[mark];
+ }
+ } else {
+ showConfirm(cm, 'Invalid argument: ' + startMark + '-');
+ return;
+ }
+ } else {
+ // This symbol is a valid mark, and is not part of a range.
+ delete state.marks[sym];
+ }
+ }
+ }
+ };
+
+ var exCommandDispatcher = new ExCommandDispatcher();
+
+ /**
+ * @param {CodeMirror} cm CodeMirror instance we are in.
+ * @param {boolean} confirm Whether to confirm each replace.
+ * @param {Cursor} lineStart Line to start replacing from.
+ * @param {Cursor} lineEnd Line to stop replacing at.
+ * @param {RegExp} query Query for performing matches with.
+ * @param {string} replaceWith Text to replace matches with. May contain $1,
+ * $2, etc for replacing captured groups using Javascript replace.
+ * @param {function()} callback A callback for when the replace is done.
+ */
+ function doReplace(cm, confirm, global, lineStart, lineEnd, searchCursor, query,
+ replaceWith, callback) {
+ // Set up all the functions.
+ cm.state.vim.exMode = true;
+ var done = false;
+ var lastPos = searchCursor.from();
+ function replaceAll() {
+ cm.operation(function() {
+ while (!done) {
+ replace();
+ next();
+ }
+ stop();
+ });
+ }
+ function replace() {
+ var text = cm.getRange(searchCursor.from(), searchCursor.to());
+ var newText = text.replace(query, replaceWith);
+ searchCursor.replace(newText);
+ }
+ function next() {
+ // The below only loops to skip over multiple occurrences on the same
+ // line when 'global' is not true.
+ while(searchCursor.findNext() &&
+ isInRange(searchCursor.from(), lineStart, lineEnd)) {
+ if (!global && lastPos && searchCursor.from().line == lastPos.line) {
+ continue;
+ }
+ cm.scrollIntoView(searchCursor.from(), 30);
+ cm.setSelection(searchCursor.from(), searchCursor.to());
+ lastPos = searchCursor.from();
+ done = false;
+ return;
+ }
+ done = true;
+ }
+ function stop(close) {
+ if (close) { close(); }
+ cm.focus();
+ if (lastPos) {
+ cm.setCursor(lastPos);
+ var vim = cm.state.vim;
+ vim.exMode = false;
+ vim.lastHPos = vim.lastHSPos = lastPos.ch;
+ }
+ if (callback) { callback(); }
+ }
+ function onPromptKeyDown(e, _value, close) {
+ // Swallow all keys.
+ CodeMirror.e_stop(e);
+ var keyName = CodeMirror.keyName(e);
+ switch (keyName) {
+ case 'Y':
+ replace(); next(); break;
+ case 'N':
+ next(); break;
+ case 'A':
+ // replaceAll contains a call to close of its own. We don't want it
+ // to fire too early or multiple times.
+ var savedCallback = callback;
+ callback = undefined;
+ cm.operation(replaceAll);
+ callback = savedCallback;
+ break;
+ case 'L':
+ replace();
+ // fall through and exit.
+ case 'Q':
+ case 'Esc':
+ case 'Ctrl-C':
+ case 'Ctrl-[':
+ stop(close);
+ break;
+ }
+ if (done) { stop(close); }
+ return true;
+ }
+
+ // Actually do replace.
+ next();
+ if (done) {
+ showConfirm(cm, 'No matches for ' + query.source);
+ return;
+ }
+ if (!confirm) {
+ replaceAll();
+ if (callback) { callback(); };
+ return;
+ }
+ showPrompt(cm, {
+ prefix: 'replace with <strong>' + replaceWith + '</strong> (y/n/a/q/l)',
+ onKeyDown: onPromptKeyDown
+ });
+ }
+
+ CodeMirror.keyMap.vim = {
+ attach: attachVimMap,
+ detach: detachVimMap,
+ call: cmKey
+ };
+
+ function exitInsertMode(cm) {
+ var vim = cm.state.vim;
+ var macroModeState = vimGlobalState.macroModeState;
+ var insertModeChangeRegister = vimGlobalState.registerController.getRegister('.');
+ var isPlaying = macroModeState.isPlaying;
+ var lastChange = macroModeState.lastInsertModeChanges;
+ // In case of visual block, the insertModeChanges are not saved as a
+ // single word, so we convert them to a single word
+ // so as to update the ". register as expected in real vim.
+ var text = [];
+ if (!isPlaying) {
+ var selLength = lastChange.inVisualBlock ? vim.lastSelection.visualBlock.height : 1;
+ var changes = lastChange.changes;
+ var text = [];
+ var i = 0;
+ // In case of multiple selections in blockwise visual,
+ // the inserted text, for example: 'f<Backspace>oo', is stored as
+ // 'f', 'f', InsertModeKey 'o', 'o', 'o', 'o'. (if you have a block with 2 lines).
+ // We push the contents of the changes array as per the following:
+ // 1. In case of InsertModeKey, just increment by 1.
+ // 2. In case of a character, jump by selLength (2 in the example).
+ while (i < changes.length) {
+ // This loop will convert 'ff<bs>oooo' to 'f<bs>oo'.
+ text.push(changes[i]);
+ if (changes[i] instanceof InsertModeKey) {
+ i++;
+ } else {
+ i+= selLength;
+ }
+ }
+ lastChange.changes = text;
+ cm.off('change', onChange);
+ CodeMirror.off(cm.getInputField(), 'keydown', onKeyEventTargetKeyDown);
+ }
+ if (!isPlaying && vim.insertModeRepeat > 1) {
+ // Perform insert mode repeat for commands like 3,a and 3,o.
+ repeatLastEdit(cm, vim, vim.insertModeRepeat - 1,
+ true /** repeatForInsert */);
+ vim.lastEditInputState.repeatOverride = vim.insertModeRepeat;
+ }
+ delete vim.insertModeRepeat;
+ vim.insertMode = false;
+ cm.setCursor(cm.getCursor().line, cm.getCursor().ch-1);
+ cm.setOption('keyMap', 'vim');
+ cm.setOption('disableInput', true);
+ cm.toggleOverwrite(false); // exit replace mode if we were in it.
+ // update the ". register before exiting insert mode
+ insertModeChangeRegister.setText(lastChange.changes.join(''));
+ CodeMirror.signal(cm, "vim-mode-change", {mode: "normal"});
+ if (macroModeState.isRecording) {
+ logInsertModeChange(macroModeState);
+ }
+ }
+
+ function _mapCommand(command) {
+ defaultKeymap.unshift(command);
+ }
+
+ function mapCommand(keys, type, name, args, extra) {
+ var command = {keys: keys, type: type};
+ command[type] = name;
+ command[type + "Args"] = args;
+ for (var key in extra)
+ command[key] = extra[key];
+ _mapCommand(command);
+ }
+
+ // The timeout in milliseconds for the two-character ESC keymap should be
+ // adjusted according to your typing speed to prevent false positives.
+ defineOption('insertModeEscKeysTimeout', 200, 'number');
+
+ CodeMirror.keyMap['vim-insert'] = {
+ // TODO: override navigation keys so that Esc will cancel automatic
+ // indentation from o, O, i_<CR>
+ 'Ctrl-N': 'autocomplete',
+ 'Ctrl-P': 'autocomplete',
+ 'Enter': function(cm) {
+ var fn = CodeMirror.commands.newlineAndIndentContinueComment ||
+ CodeMirror.commands.newlineAndIndent;
+ fn(cm);
+ },
+ fallthrough: ['default'],
+ attach: attachVimMap,
+ detach: detachVimMap,
+ call: cmKey
+ };
+
+ CodeMirror.keyMap['vim-replace'] = {
+ 'Backspace': 'goCharLeft',
+ fallthrough: ['vim-insert'],
+ attach: attachVimMap,
+ detach: detachVimMap,
+ call: cmKey
+ };
+
+ function executeMacroRegister(cm, vim, macroModeState, registerName) {
+ var register = vimGlobalState.registerController.getRegister(registerName);
+ if (registerName == ':') {
+ // Read-only register containing last Ex command.
+ if (register.keyBuffer[0]) {
+ exCommandDispatcher.processCommand(cm, register.keyBuffer[0]);
+ }
+ macroModeState.isPlaying = false;
+ return;
+ }
+ var keyBuffer = register.keyBuffer;
+ var imc = 0;
+ macroModeState.isPlaying = true;
+ macroModeState.replaySearchQueries = register.searchQueries.slice(0);
+ for (var i = 0; i < keyBuffer.length; i++) {
+ var text = keyBuffer[i];
+ var match, key;
+ while (text) {
+ // Pull off one command key, which is either a single character
+ // or a special sequence wrapped in '<' and '>', e.g. '<Space>'.
+ match = (/<\w+-.+?>|<\w+>|./).exec(text);
+ key = match[0];
+ text = text.substring(match.index + key.length);
+ CodeMirror.Vim.handleKey(cm, key, 'macro');
+ if (vim.insertMode) {
+ var changes = register.insertModeChanges[imc++].changes;
+ vimGlobalState.macroModeState.lastInsertModeChanges.changes =
+ changes;
+ repeatInsertModeChanges(cm, changes, 1);
+ exitInsertMode(cm);
+ }
+ }
+ };
+ macroModeState.isPlaying = false;
+ }
+
+ function logKey(macroModeState, key) {
+ if (macroModeState.isPlaying) { return; }
+ var registerName = macroModeState.latestRegister;
+ var register = vimGlobalState.registerController.getRegister(registerName);
+ if (register) {
+ register.pushText(key);
+ }
+ }
+
+ function logInsertModeChange(macroModeState) {
+ if (macroModeState.isPlaying) { return; }
+ var registerName = macroModeState.latestRegister;
+ var register = vimGlobalState.registerController.getRegister(registerName);
+ if (register && register.pushInsertModeChanges) {
+ register.pushInsertModeChanges(macroModeState.lastInsertModeChanges);
+ }
+ }
+
+ function logSearchQuery(macroModeState, query) {
+ if (macroModeState.isPlaying) { return; }
+ var registerName = macroModeState.latestRegister;
+ var register = vimGlobalState.registerController.getRegister(registerName);
+ if (register && register.pushSearchQuery) {
+ register.pushSearchQuery(query);
+ }
+ }
+
+ /**
+ * Listens for changes made in insert mode.
+ * Should only be active in insert mode.
+ */
+ function onChange(_cm, changeObj) {
+ var macroModeState = vimGlobalState.macroModeState;
+ var lastChange = macroModeState.lastInsertModeChanges;
+ if (!macroModeState.isPlaying) {
+ while(changeObj) {
+ lastChange.expectCursorActivityForChange = true;
+ if (changeObj.origin == '+input' || changeObj.origin == 'paste'
+ || changeObj.origin === undefined /* only in testing */) {
+ var text = changeObj.text.join('\n');
+ lastChange.changes.push(text);
+ }
+ // Change objects may be chained with next.
+ changeObj = changeObj.next;
+ }
+ }
+ }
+
+ /**
+ * Listens for any kind of cursor activity on CodeMirror.
+ */
+ function onCursorActivity(cm) {
+ var vim = cm.state.vim;
+ if (vim.insertMode) {
+ // Tracking cursor activity in insert mode (for macro support).
+ var macroModeState = vimGlobalState.macroModeState;
+ if (macroModeState.isPlaying) { return; }
+ var lastChange = macroModeState.lastInsertModeChanges;
+ if (lastChange.expectCursorActivityForChange) {
+ lastChange.expectCursorActivityForChange = false;
+ } else {
+ // Cursor moved outside the context of an edit. Reset the change.
+ lastChange.changes = [];
+ }
+ } else if (!cm.curOp.isVimOp) {
+ handleExternalSelection(cm, vim);
+ }
+ if (vim.visualMode) {
+ updateFakeCursor(cm);
+ }
+ }
+ function updateFakeCursor(cm) {
+ var vim = cm.state.vim;
+ var from = clipCursorToContent(cm, copyCursor(vim.sel.head));
+ var to = offsetCursor(from, 0, 1);
+ if (vim.fakeCursor) {
+ vim.fakeCursor.clear();
+ }
+ vim.fakeCursor = cm.markText(from, to, {className: 'cm-animate-fat-cursor'});
+ }
+ function handleExternalSelection(cm, vim) {
+ var anchor = cm.getCursor('anchor');
+ var head = cm.getCursor('head');
+ // Enter or exit visual mode to match mouse selection.
+ if (vim.visualMode && !cm.somethingSelected()) {
+ exitVisualMode(cm, false);
+ } else if (!vim.visualMode && !vim.insertMode && cm.somethingSelected()) {
+ vim.visualMode = true;
+ vim.visualLine = false;
+ CodeMirror.signal(cm, "vim-mode-change", {mode: "visual"});
+ }
+ if (vim.visualMode) {
+ // Bind CodeMirror selection model to vim selection model.
+ // Mouse selections are considered visual characterwise.
+ var headOffset = !cursorIsBefore(head, anchor) ? -1 : 0;
+ var anchorOffset = cursorIsBefore(head, anchor) ? -1 : 0;
+ head = offsetCursor(head, 0, headOffset);
+ anchor = offsetCursor(anchor, 0, anchorOffset);
+ vim.sel = {
+ anchor: anchor,
+ head: head
+ };
+ updateMark(cm, vim, '<', cursorMin(head, anchor));
+ updateMark(cm, vim, '>', cursorMax(head, anchor));
+ } else if (!vim.insertMode) {
+ // Reset lastHPos if selection was modified by something outside of vim mode e.g. by mouse.
+ vim.lastHPos = cm.getCursor().ch;
+ }
+ }
+
+ /** Wrapper for special keys pressed in insert mode */
+ function InsertModeKey(keyName) {
+ this.keyName = keyName;
+ }
+
+ /**
+ * Handles raw key down events from the text area.
+ * - Should only be active in insert mode.
+ * - For recording deletes in insert mode.
+ */
+ function onKeyEventTargetKeyDown(e) {
+ var macroModeState = vimGlobalState.macroModeState;
+ var lastChange = macroModeState.lastInsertModeChanges;
+ var keyName = CodeMirror.keyName(e);
+ if (!keyName) { return; }
+ function onKeyFound() {
+ lastChange.changes.push(new InsertModeKey(keyName));
+ return true;
+ }
+ if (keyName.indexOf('Delete') != -1 || keyName.indexOf('Backspace') != -1) {
+ CodeMirror.lookupKey(keyName, 'vim-insert', onKeyFound);
+ }
+ }
+
+ /**
+ * Repeats the last edit, which includes exactly 1 command and at most 1
+ * insert. Operator and motion commands are read from lastEditInputState,
+ * while action commands are read from lastEditActionCommand.
+ *
+ * If repeatForInsert is true, then the function was called by
+ * exitInsertMode to repeat the insert mode changes the user just made. The
+ * corresponding enterInsertMode call was made with a count.
+ */
+ function repeatLastEdit(cm, vim, repeat, repeatForInsert) {
+ var macroModeState = vimGlobalState.macroModeState;
+ macroModeState.isPlaying = true;
+ var isAction = !!vim.lastEditActionCommand;
+ var cachedInputState = vim.inputState;
+ function repeatCommand() {
+ if (isAction) {
+ commandDispatcher.processAction(cm, vim, vim.lastEditActionCommand);
+ } else {
+ commandDispatcher.evalInput(cm, vim);
+ }
+ }
+ function repeatInsert(repeat) {
+ if (macroModeState.lastInsertModeChanges.changes.length > 0) {
+ // For some reason, repeat cw in desktop VIM does not repeat
+ // insert mode changes. Will conform to that behavior.
+ repeat = !vim.lastEditActionCommand ? 1 : repeat;
+ var changeObject = macroModeState.lastInsertModeChanges;
+ repeatInsertModeChanges(cm, changeObject.changes, repeat);
+ }
+ }
+ vim.inputState = vim.lastEditInputState;
+ if (isAction && vim.lastEditActionCommand.interlaceInsertRepeat) {
+ // o and O repeat have to be interlaced with insert repeats so that the
+ // insertions appear on separate lines instead of the last line.
+ for (var i = 0; i < repeat; i++) {
+ repeatCommand();
+ repeatInsert(1);
+ }
+ } else {
+ if (!repeatForInsert) {
+ // Hack to get the cursor to end up at the right place. If I is
+ // repeated in insert mode repeat, cursor will be 1 insert
+ // change set left of where it should be.
+ repeatCommand();
+ }
+ repeatInsert(repeat);
+ }
+ vim.inputState = cachedInputState;
+ if (vim.insertMode && !repeatForInsert) {
+ // Don't exit insert mode twice. If repeatForInsert is set, then we
+ // were called by an exitInsertMode call lower on the stack.
+ exitInsertMode(cm);
+ }
+ macroModeState.isPlaying = false;
+ };
+
+ function repeatInsertModeChanges(cm, changes, repeat) {
+ function keyHandler(binding) {
+ if (typeof binding == 'string') {
+ CodeMirror.commands[binding](cm);
+ } else {
+ binding(cm);
+ }
+ return true;
+ }
+ var head = cm.getCursor('head');
+ var inVisualBlock = vimGlobalState.macroModeState.lastInsertModeChanges.inVisualBlock;
+ if (inVisualBlock) {
+ // Set up block selection again for repeating the changes.
+ var vim = cm.state.vim;
+ var lastSel = vim.lastSelection;
+ var offset = getOffset(lastSel.anchor, lastSel.head);
+ selectForInsert(cm, head, offset.line + 1);
+ repeat = cm.listSelections().length;
+ cm.setCursor(head);
+ }
+ for (var i = 0; i < repeat; i++) {
+ if (inVisualBlock) {
+ cm.setCursor(offsetCursor(head, i, 0));
+ }
+ for (var j = 0; j < changes.length; j++) {
+ var change = changes[j];
+ if (change instanceof InsertModeKey) {
+ CodeMirror.lookupKey(change.keyName, 'vim-insert', keyHandler);
+ } else {
+ var cur = cm.getCursor();
+ cm.replaceRange(change, cur, cur);
+ }
+ }
+ }
+ if (inVisualBlock) {
+ cm.setCursor(offsetCursor(head, 0, 1));
+ }
+ }
+
+ resetVimGlobalState();
+ return vimApi;
+ };
+ // Initialize Vim and make it available as an API.
+ CodeMirror.Vim = Vim();
+ });
+
+
+/***/ },
+/* 18 */
+/***/ function(module, exports, __webpack_require__) {
+
+ // CodeMirror, copyright (c) by Marijn Haverbeke and others
+ // Distributed under an MIT license: http://codemirror.net/LICENSE
+
+ // A rough approximation of Sublime Text's keybindings
+ // Depends on addon/search/searchcursor.js and optionally addon/dialog/dialogs.js
+
+ (function(mod) {
+ if (true) // CommonJS
+ mod(__webpack_require__(2), __webpack_require__(3), __webpack_require__(5));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../lib/codemirror", "../addon/search/searchcursor", "../addon/edit/matchbrackets"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+ })(function(CodeMirror) {
+ "use strict";
+
+ var map = CodeMirror.keyMap.sublime = {fallthrough: "default"};
+ var cmds = CodeMirror.commands;
+ var Pos = CodeMirror.Pos;
+ var mac = CodeMirror.keyMap["default"] == CodeMirror.keyMap.macDefault;
+ var ctrl = mac ? "Cmd-" : "Ctrl-";
+
+ // This is not exactly Sublime's algorithm. I couldn't make heads or tails of that.
+ function findPosSubword(doc, start, dir) {
+ if (dir < 0 && start.ch == 0) return doc.clipPos(Pos(start.line - 1));
+ var line = doc.getLine(start.line);
+ if (dir > 0 && start.ch >= line.length) return doc.clipPos(Pos(start.line + 1, 0));
+ var state = "start", type;
+ for (var pos = start.ch, e = dir < 0 ? 0 : line.length, i = 0; pos != e; pos += dir, i++) {
+ var next = line.charAt(dir < 0 ? pos - 1 : pos);
+ var cat = next != "_" && CodeMirror.isWordChar(next) ? "w" : "o";
+ if (cat == "w" && next.toUpperCase() == next) cat = "W";
+ if (state == "start") {
+ if (cat != "o") { state = "in"; type = cat; }
+ } else if (state == "in") {
+ if (type != cat) {
+ if (type == "w" && cat == "W" && dir < 0) pos--;
+ if (type == "W" && cat == "w" && dir > 0) { type = "w"; continue; }
+ break;
+ }
+ }
+ }
+ return Pos(start.line, pos);
+ }
+
+ function moveSubword(cm, dir) {
+ cm.extendSelectionsBy(function(range) {
+ if (cm.display.shift || cm.doc.extend || range.empty())
+ return findPosSubword(cm.doc, range.head, dir);
+ else
+ return dir < 0 ? range.from() : range.to();
+ });
+ }
+
+ cmds[map["Alt-Left"] = "goSubwordLeft"] = function(cm) { moveSubword(cm, -1); };
+ cmds[map["Alt-Right"] = "goSubwordRight"] = function(cm) { moveSubword(cm, 1); };
+
+ if (mac) map["Cmd-Left"] = "goLineStartSmart";
+
+ var scrollLineCombo = mac ? "Ctrl-Alt-" : "Ctrl-";
+
+ cmds[map[scrollLineCombo + "Up"] = "scrollLineUp"] = function(cm) {
+ var info = cm.getScrollInfo();
+ if (!cm.somethingSelected()) {
+ var visibleBottomLine = cm.lineAtHeight(info.top + info.clientHeight, "local");
+ if (cm.getCursor().line >= visibleBottomLine)
+ cm.execCommand("goLineUp");
+ }
+ cm.scrollTo(null, info.top - cm.defaultTextHeight());
+ };
+ cmds[map[scrollLineCombo + "Down"] = "scrollLineDown"] = function(cm) {
+ var info = cm.getScrollInfo();
+ if (!cm.somethingSelected()) {
+ var visibleTopLine = cm.lineAtHeight(info.top, "local")+1;
+ if (cm.getCursor().line <= visibleTopLine)
+ cm.execCommand("goLineDown");
+ }
+ cm.scrollTo(null, info.top + cm.defaultTextHeight());
+ };
+
+ cmds[map["Shift-" + ctrl + "L"] = "splitSelectionByLine"] = function(cm) {
+ var ranges = cm.listSelections(), lineRanges = [];
+ for (var i = 0; i < ranges.length; i++) {
+ var from = ranges[i].from(), to = ranges[i].to();
+ for (var line = from.line; line <= to.line; ++line)
+ if (!(to.line > from.line && line == to.line && to.ch == 0))
+ lineRanges.push({anchor: line == from.line ? from : Pos(line, 0),
+ head: line == to.line ? to : Pos(line)});
+ }
+ cm.setSelections(lineRanges, 0);
+ };
+
+ map["Shift-Tab"] = "indentLess";
+
+ cmds[map["Esc"] = "singleSelectionTop"] = function(cm) {
+ var range = cm.listSelections()[0];
+ cm.setSelection(range.anchor, range.head, {scroll: false});
+ };
+
+ cmds[map[ctrl + "L"] = "selectLine"] = function(cm) {
+ var ranges = cm.listSelections(), extended = [];
+ for (var i = 0; i < ranges.length; i++) {
+ var range = ranges[i];
+ extended.push({anchor: Pos(range.from().line, 0),
+ head: Pos(range.to().line + 1, 0)});
+ }
+ cm.setSelections(extended);
+ };
+
+ map["Shift-Ctrl-K"] = "deleteLine";
+
+ function insertLine(cm, above) {
+ if (cm.isReadOnly()) return CodeMirror.Pass
+ cm.operation(function() {
+ var len = cm.listSelections().length, newSelection = [], last = -1;
+ for (var i = 0; i < len; i++) {
+ var head = cm.listSelections()[i].head;
+ if (head.line <= last) continue;
+ var at = Pos(head.line + (above ? 0 : 1), 0);
+ cm.replaceRange("\n", at, null, "+insertLine");
+ cm.indentLine(at.line, null, true);
+ newSelection.push({head: at, anchor: at});
+ last = head.line + 1;
+ }
+ cm.setSelections(newSelection);
+ });
+ cm.execCommand("indentAuto");
+ }
+
+ cmds[map[ctrl + "Enter"] = "insertLineAfter"] = function(cm) { return insertLine(cm, false); };
+
+ cmds[map["Shift-" + ctrl + "Enter"] = "insertLineBefore"] = function(cm) { return insertLine(cm, true); };
+
+ function wordAt(cm, pos) {
+ var start = pos.ch, end = start, line = cm.getLine(pos.line);
+ while (start && CodeMirror.isWordChar(line.charAt(start - 1))) --start;
+ while (end < line.length && CodeMirror.isWordChar(line.charAt(end))) ++end;
+ return {from: Pos(pos.line, start), to: Pos(pos.line, end), word: line.slice(start, end)};
+ }
+
+ cmds[map[ctrl + "D"] = "selectNextOccurrence"] = function(cm) {
+ var from = cm.getCursor("from"), to = cm.getCursor("to");
+ var fullWord = cm.state.sublimeFindFullWord == cm.doc.sel;
+ if (CodeMirror.cmpPos(from, to) == 0) {
+ var word = wordAt(cm, from);
+ if (!word.word) return;
+ cm.setSelection(word.from, word.to);
+ fullWord = true;
+ } else {
+ var text = cm.getRange(from, to);
+ var query = fullWord ? new RegExp("\\b" + text + "\\b") : text;
+ var cur = cm.getSearchCursor(query, to);
+ if (cur.findNext()) {
+ cm.addSelection(cur.from(), cur.to());
+ } else {
+ cur = cm.getSearchCursor(query, Pos(cm.firstLine(), 0));
+ if (cur.findNext())
+ cm.addSelection(cur.from(), cur.to());
+ }
+ }
+ if (fullWord)
+ cm.state.sublimeFindFullWord = cm.doc.sel;
+ };
+
+ var mirror = "(){}[]";
+ function selectBetweenBrackets(cm) {
+ var pos = cm.getCursor(), opening = cm.scanForBracket(pos, -1);
+ if (!opening) return;
+ for (;;) {
+ var closing = cm.scanForBracket(pos, 1);
+ if (!closing) return;
+ if (closing.ch == mirror.charAt(mirror.indexOf(opening.ch) + 1)) {
+ cm.setSelection(Pos(opening.pos.line, opening.pos.ch + 1), closing.pos, false);
+ return true;
+ }
+ pos = Pos(closing.pos.line, closing.pos.ch + 1);
+ }
+ }
+
+ cmds[map["Shift-" + ctrl + "Space"] = "selectScope"] = function(cm) {
+ selectBetweenBrackets(cm) || cm.execCommand("selectAll");
+ };
+ cmds[map["Shift-" + ctrl + "M"] = "selectBetweenBrackets"] = function(cm) {
+ if (!selectBetweenBrackets(cm)) return CodeMirror.Pass;
+ };
+
+ cmds[map[ctrl + "M"] = "goToBracket"] = function(cm) {
+ cm.extendSelectionsBy(function(range) {
+ var next = cm.scanForBracket(range.head, 1);
+ if (next && CodeMirror.cmpPos(next.pos, range.head) != 0) return next.pos;
+ var prev = cm.scanForBracket(range.head, -1);
+ return prev && Pos(prev.pos.line, prev.pos.ch + 1) || range.head;
+ });
+ };
+
+ var swapLineCombo = mac ? "Cmd-Ctrl-" : "Shift-Ctrl-";
+
+ cmds[map[swapLineCombo + "Up"] = "swapLineUp"] = function(cm) {
+ if (cm.isReadOnly()) return CodeMirror.Pass
+ var ranges = cm.listSelections(), linesToMove = [], at = cm.firstLine() - 1, newSels = [];
+ for (var i = 0; i < ranges.length; i++) {
+ var range = ranges[i], from = range.from().line - 1, to = range.to().line;
+ newSels.push({anchor: Pos(range.anchor.line - 1, range.anchor.ch),
+ head: Pos(range.head.line - 1, range.head.ch)});
+ if (range.to().ch == 0 && !range.empty()) --to;
+ if (from > at) linesToMove.push(from, to);
+ else if (linesToMove.length) linesToMove[linesToMove.length - 1] = to;
+ at = to;
+ }
+ cm.operation(function() {
+ for (var i = 0; i < linesToMove.length; i += 2) {
+ var from = linesToMove[i], to = linesToMove[i + 1];
+ var line = cm.getLine(from);
+ cm.replaceRange("", Pos(from, 0), Pos(from + 1, 0), "+swapLine");
+ if (to > cm.lastLine())
+ cm.replaceRange("\n" + line, Pos(cm.lastLine()), null, "+swapLine");
+ else
+ cm.replaceRange(line + "\n", Pos(to, 0), null, "+swapLine");
+ }
+ cm.setSelections(newSels);
+ cm.scrollIntoView();
+ });
+ };
+
+ cmds[map[swapLineCombo + "Down"] = "swapLineDown"] = function(cm) {
+ if (cm.isReadOnly()) return CodeMirror.Pass
+ var ranges = cm.listSelections(), linesToMove = [], at = cm.lastLine() + 1;
+ for (var i = ranges.length - 1; i >= 0; i--) {
+ var range = ranges[i], from = range.to().line + 1, to = range.from().line;
+ if (range.to().ch == 0 && !range.empty()) from--;
+ if (from < at) linesToMove.push(from, to);
+ else if (linesToMove.length) linesToMove[linesToMove.length - 1] = to;
+ at = to;
+ }
+ cm.operation(function() {
+ for (var i = linesToMove.length - 2; i >= 0; i -= 2) {
+ var from = linesToMove[i], to = linesToMove[i + 1];
+ var line = cm.getLine(from);
+ if (from == cm.lastLine())
+ cm.replaceRange("", Pos(from - 1), Pos(from), "+swapLine");
+ else
+ cm.replaceRange("", Pos(from, 0), Pos(from + 1, 0), "+swapLine");
+ cm.replaceRange(line + "\n", Pos(to, 0), null, "+swapLine");
+ }
+ cm.scrollIntoView();
+ });
+ };
+
+ cmds[map[ctrl + "/"] = "toggleCommentIndented"] = function(cm) {
+ cm.toggleComment({ indent: true });
+ }
+
+ cmds[map[ctrl + "J"] = "joinLines"] = function(cm) {
+ var ranges = cm.listSelections(), joined = [];
+ for (var i = 0; i < ranges.length; i++) {
+ var range = ranges[i], from = range.from();
+ var start = from.line, end = range.to().line;
+ while (i < ranges.length - 1 && ranges[i + 1].from().line == end)
+ end = ranges[++i].to().line;
+ joined.push({start: start, end: end, anchor: !range.empty() && from});
+ }
+ cm.operation(function() {
+ var offset = 0, ranges = [];
+ for (var i = 0; i < joined.length; i++) {
+ var obj = joined[i];
+ var anchor = obj.anchor && Pos(obj.anchor.line - offset, obj.anchor.ch), head;
+ for (var line = obj.start; line <= obj.end; line++) {
+ var actual = line - offset;
+ if (line == obj.end) head = Pos(actual, cm.getLine(actual).length + 1);
+ if (actual < cm.lastLine()) {
+ cm.replaceRange(" ", Pos(actual), Pos(actual + 1, /^\s*/.exec(cm.getLine(actual + 1))[0].length));
+ ++offset;
+ }
+ }
+ ranges.push({anchor: anchor || head, head: head});
+ }
+ cm.setSelections(ranges, 0);
+ });
+ };
+
+ cmds[map["Shift-" + ctrl + "D"] = "duplicateLine"] = function(cm) {
+ cm.operation(function() {
+ var rangeCount = cm.listSelections().length;
+ for (var i = 0; i < rangeCount; i++) {
+ var range = cm.listSelections()[i];
+ if (range.empty())
+ cm.replaceRange(cm.getLine(range.head.line) + "\n", Pos(range.head.line, 0));
+ else
+ cm.replaceRange(cm.getRange(range.from(), range.to()), range.from());
+ }
+ cm.scrollIntoView();
+ });
+ };
+
+ map[ctrl + "T"] = "transposeChars";
+
+ function sortLines(cm, caseSensitive) {
+ if (cm.isReadOnly()) return CodeMirror.Pass
+ var ranges = cm.listSelections(), toSort = [], selected;
+ for (var i = 0; i < ranges.length; i++) {
+ var range = ranges[i];
+ if (range.empty()) continue;
+ var from = range.from().line, to = range.to().line;
+ while (i < ranges.length - 1 && ranges[i + 1].from().line == to)
+ to = range[++i].to().line;
+ toSort.push(from, to);
+ }
+ if (toSort.length) selected = true;
+ else toSort.push(cm.firstLine(), cm.lastLine());
+
+ cm.operation(function() {
+ var ranges = [];
+ for (var i = 0; i < toSort.length; i += 2) {
+ var from = toSort[i], to = toSort[i + 1];
+ var start = Pos(from, 0), end = Pos(to);
+ var lines = cm.getRange(start, end, false);
+ if (caseSensitive)
+ lines.sort();
+ else
+ lines.sort(function(a, b) {
+ var au = a.toUpperCase(), bu = b.toUpperCase();
+ if (au != bu) { a = au; b = bu; }
+ return a < b ? -1 : a == b ? 0 : 1;
+ });
+ cm.replaceRange(lines, start, end);
+ if (selected) ranges.push({anchor: start, head: end});
+ }
+ if (selected) cm.setSelections(ranges, 0);
+ });
+ }
+
+ cmds[map["F9"] = "sortLines"] = function(cm) { sortLines(cm, true); };
+ cmds[map[ctrl + "F9"] = "sortLinesInsensitive"] = function(cm) { sortLines(cm, false); };
+
+ cmds[map["F2"] = "nextBookmark"] = function(cm) {
+ var marks = cm.state.sublimeBookmarks;
+ if (marks) while (marks.length) {
+ var current = marks.shift();
+ var found = current.find();
+ if (found) {
+ marks.push(current);
+ return cm.setSelection(found.from, found.to);
+ }
+ }
+ };
+
+ cmds[map["Shift-F2"] = "prevBookmark"] = function(cm) {
+ var marks = cm.state.sublimeBookmarks;
+ if (marks) while (marks.length) {
+ marks.unshift(marks.pop());
+ var found = marks[marks.length - 1].find();
+ if (!found)
+ marks.pop();
+ else
+ return cm.setSelection(found.from, found.to);
+ }
+ };
+
+ cmds[map[ctrl + "F2"] = "toggleBookmark"] = function(cm) {
+ var ranges = cm.listSelections();
+ var marks = cm.state.sublimeBookmarks || (cm.state.sublimeBookmarks = []);
+ for (var i = 0; i < ranges.length; i++) {
+ var from = ranges[i].from(), to = ranges[i].to();
+ var found = cm.findMarks(from, to);
+ for (var j = 0; j < found.length; j++) {
+ if (found[j].sublimeBookmark) {
+ found[j].clear();
+ for (var k = 0; k < marks.length; k++)
+ if (marks[k] == found[j])
+ marks.splice(k--, 1);
+ break;
+ }
+ }
+ if (j == found.length)
+ marks.push(cm.markText(from, to, {sublimeBookmark: true, clearWhenEmpty: false}));
+ }
+ };
+
+ cmds[map["Shift-" + ctrl + "F2"] = "clearBookmarks"] = function(cm) {
+ var marks = cm.state.sublimeBookmarks;
+ if (marks) for (var i = 0; i < marks.length; i++) marks[i].clear();
+ marks.length = 0;
+ };
+
+ cmds[map["Alt-F2"] = "selectBookmarks"] = function(cm) {
+ var marks = cm.state.sublimeBookmarks, ranges = [];
+ if (marks) for (var i = 0; i < marks.length; i++) {
+ var found = marks[i].find();
+ if (!found)
+ marks.splice(i--, 0);
+ else
+ ranges.push({anchor: found.from, head: found.to});
+ }
+ if (ranges.length)
+ cm.setSelections(ranges, 0);
+ };
+
+ map["Alt-Q"] = "wrapLines";
+
+ var cK = ctrl + "K ";
+
+ function modifyWordOrSelection(cm, mod) {
+ cm.operation(function() {
+ var ranges = cm.listSelections(), indices = [], replacements = [];
+ for (var i = 0; i < ranges.length; i++) {
+ var range = ranges[i];
+ if (range.empty()) { indices.push(i); replacements.push(""); }
+ else replacements.push(mod(cm.getRange(range.from(), range.to())));
+ }
+ cm.replaceSelections(replacements, "around", "case");
+ for (var i = indices.length - 1, at; i >= 0; i--) {
+ var range = ranges[indices[i]];
+ if (at && CodeMirror.cmpPos(range.head, at) > 0) continue;
+ var word = wordAt(cm, range.head);
+ at = word.from;
+ cm.replaceRange(mod(word.word), word.from, word.to);
+ }
+ });
+ }
+
+ map[cK + ctrl + "Backspace"] = "delLineLeft";
+
+ cmds[map["Backspace"] = "smartBackspace"] = function(cm) {
+ if (cm.somethingSelected()) return CodeMirror.Pass;
+
+ cm.operation(function() {
+ var cursors = cm.listSelections();
+ var indentUnit = cm.getOption("indentUnit");
+
+ for (var i = cursors.length - 1; i >= 0; i--) {
+ var cursor = cursors[i].head;
+ var toStartOfLine = cm.getRange({line: cursor.line, ch: 0}, cursor);
+ var column = CodeMirror.countColumn(toStartOfLine, null, cm.getOption("tabSize"));
+
+ // Delete by one character by default
+ var deletePos = cm.findPosH(cursor, -1, "char", false);
+
+ if (toStartOfLine && !/\S/.test(toStartOfLine) && column % indentUnit == 0) {
+ var prevIndent = new Pos(cursor.line,
+ CodeMirror.findColumn(toStartOfLine, column - indentUnit, indentUnit));
+
+ // Smart delete only if we found a valid prevIndent location
+ if (prevIndent.ch != cursor.ch) deletePos = prevIndent;
+ }
+
+ cm.replaceRange("", deletePos, cursor, "+delete");
+ }
+ });
+ };
+
+ cmds[map[cK + ctrl + "K"] = "delLineRight"] = function(cm) {
+ cm.operation(function() {
+ var ranges = cm.listSelections();
+ for (var i = ranges.length - 1; i >= 0; i--)
+ cm.replaceRange("", ranges[i].anchor, Pos(ranges[i].to().line), "+delete");
+ cm.scrollIntoView();
+ });
+ };
+
+ cmds[map[cK + ctrl + "U"] = "upcaseAtCursor"] = function(cm) {
+ modifyWordOrSelection(cm, function(str) { return str.toUpperCase(); });
+ };
+ cmds[map[cK + ctrl + "L"] = "downcaseAtCursor"] = function(cm) {
+ modifyWordOrSelection(cm, function(str) { return str.toLowerCase(); });
+ };
+
+ cmds[map[cK + ctrl + "Space"] = "setSublimeMark"] = function(cm) {
+ if (cm.state.sublimeMark) cm.state.sublimeMark.clear();
+ cm.state.sublimeMark = cm.setBookmark(cm.getCursor());
+ };
+ cmds[map[cK + ctrl + "A"] = "selectToSublimeMark"] = function(cm) {
+ var found = cm.state.sublimeMark && cm.state.sublimeMark.find();
+ if (found) cm.setSelection(cm.getCursor(), found);
+ };
+ cmds[map[cK + ctrl + "W"] = "deleteToSublimeMark"] = function(cm) {
+ var found = cm.state.sublimeMark && cm.state.sublimeMark.find();
+ if (found) {
+ var from = cm.getCursor(), to = found;
+ if (CodeMirror.cmpPos(from, to) > 0) { var tmp = to; to = from; from = tmp; }
+ cm.state.sublimeKilled = cm.getRange(from, to);
+ cm.replaceRange("", from, to);
+ }
+ };
+ cmds[map[cK + ctrl + "X"] = "swapWithSublimeMark"] = function(cm) {
+ var found = cm.state.sublimeMark && cm.state.sublimeMark.find();
+ if (found) {
+ cm.state.sublimeMark.clear();
+ cm.state.sublimeMark = cm.setBookmark(cm.getCursor());
+ cm.setCursor(found);
+ }
+ };
+ cmds[map[cK + ctrl + "Y"] = "sublimeYank"] = function(cm) {
+ if (cm.state.sublimeKilled != null)
+ cm.replaceSelection(cm.state.sublimeKilled, null, "paste");
+ };
+
+ map[cK + ctrl + "G"] = "clearBookmarks";
+ cmds[map[cK + ctrl + "C"] = "showInCenter"] = function(cm) {
+ var pos = cm.cursorCoords(null, "local");
+ cm.scrollTo(null, (pos.top + pos.bottom) / 2 - cm.getScrollInfo().clientHeight / 2);
+ };
+
+ var selectLinesCombo = mac ? "Ctrl-Shift-" : "Ctrl-Alt-";
+ cmds[map[selectLinesCombo + "Up"] = "selectLinesUpward"] = function(cm) {
+ cm.operation(function() {
+ var ranges = cm.listSelections();
+ for (var i = 0; i < ranges.length; i++) {
+ var range = ranges[i];
+ if (range.head.line > cm.firstLine())
+ cm.addSelection(Pos(range.head.line - 1, range.head.ch));
+ }
+ });
+ };
+ cmds[map[selectLinesCombo + "Down"] = "selectLinesDownward"] = function(cm) {
+ cm.operation(function() {
+ var ranges = cm.listSelections();
+ for (var i = 0; i < ranges.length; i++) {
+ var range = ranges[i];
+ if (range.head.line < cm.lastLine())
+ cm.addSelection(Pos(range.head.line + 1, range.head.ch));
+ }
+ });
+ };
+
+ function getTarget(cm) {
+ var from = cm.getCursor("from"), to = cm.getCursor("to");
+ if (CodeMirror.cmpPos(from, to) == 0) {
+ var word = wordAt(cm, from);
+ if (!word.word) return;
+ from = word.from;
+ to = word.to;
+ }
+ return {from: from, to: to, query: cm.getRange(from, to), word: word};
+ }
+
+ function findAndGoTo(cm, forward) {
+ var target = getTarget(cm);
+ if (!target) return;
+ var query = target.query;
+ var cur = cm.getSearchCursor(query, forward ? target.to : target.from);
+
+ if (forward ? cur.findNext() : cur.findPrevious()) {
+ cm.setSelection(cur.from(), cur.to());
+ } else {
+ cur = cm.getSearchCursor(query, forward ? Pos(cm.firstLine(), 0)
+ : cm.clipPos(Pos(cm.lastLine())));
+ if (forward ? cur.findNext() : cur.findPrevious())
+ cm.setSelection(cur.from(), cur.to());
+ else if (target.word)
+ cm.setSelection(target.from, target.to);
+ }
+ };
+ cmds[map[ctrl + "F3"] = "findUnder"] = function(cm) { findAndGoTo(cm, true); };
+ cmds[map["Shift-" + ctrl + "F3"] = "findUnderPrevious"] = function(cm) { findAndGoTo(cm,false); };
+ cmds[map["Alt-F3"] = "findAllUnder"] = function(cm) {
+ var target = getTarget(cm);
+ if (!target) return;
+ var cur = cm.getSearchCursor(target.query);
+ var matches = [];
+ var primaryIndex = -1;
+ while (cur.findNext()) {
+ matches.push({anchor: cur.from(), head: cur.to()});
+ if (cur.from().line <= target.from.line && cur.from().ch <= target.from.ch)
+ primaryIndex++;
+ }
+ cm.setSelections(matches, primaryIndex);
+ };
+
+ map["Shift-" + ctrl + "["] = "fold";
+ map["Shift-" + ctrl + "]"] = "unfold";
+ map[cK + ctrl + "0"] = map[cK + ctrl + "j"] = "unfoldAll";
+
+ map[ctrl + "I"] = "findIncremental";
+ map["Shift-" + ctrl + "I"] = "findIncrementalReverse";
+ map[ctrl + "H"] = "replace";
+ map["F3"] = "findNext";
+ map["Shift-F3"] = "findPrev";
+
+ CodeMirror.normalizeKeyMap(map);
+ });
+
+
+/***/ },
+/* 19 */
+/***/ function(module, exports, __webpack_require__) {
+
+ // CodeMirror, copyright (c) by Marijn Haverbeke and others
+ // Distributed under an MIT license: http://codemirror.net/LICENSE
+
+ (function(mod) {
+ if (true) // CommonJS
+ mod(__webpack_require__(2));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+ })(function(CodeMirror) {
+ "use strict";
+
+ function doFold(cm, pos, options, force) {
+ if (options && options.call) {
+ var finder = options;
+ options = null;
+ } else {
+ var finder = getOption(cm, options, "rangeFinder");
+ }
+ if (typeof pos == "number") pos = CodeMirror.Pos(pos, 0);
+ var minSize = getOption(cm, options, "minFoldSize");
+
+ function getRange(allowFolded) {
+ var range = finder(cm, pos);
+ if (!range || range.to.line - range.from.line < minSize) return null;
+ var marks = cm.findMarksAt(range.from);
+ for (var i = 0; i < marks.length; ++i) {
+ if (marks[i].__isFold && force !== "fold") {
+ if (!allowFolded) return null;
+ range.cleared = true;
+ marks[i].clear();
+ }
+ }
+ return range;
+ }
+
+ var range = getRange(true);
+ if (getOption(cm, options, "scanUp")) while (!range && pos.line > cm.firstLine()) {
+ pos = CodeMirror.Pos(pos.line - 1, 0);
+ range = getRange(false);
+ }
+ if (!range || range.cleared || force === "unfold") return;
+
+ var myWidget = makeWidget(cm, options);
+ CodeMirror.on(myWidget, "mousedown", function(e) {
+ myRange.clear();
+ CodeMirror.e_preventDefault(e);
+ });
+ var myRange = cm.markText(range.from, range.to, {
+ replacedWith: myWidget,
+ clearOnEnter: getOption(cm, options, "clearOnEnter"),
+ __isFold: true
+ });
+ myRange.on("clear", function(from, to) {
+ CodeMirror.signal(cm, "unfold", cm, from, to);
+ });
+ CodeMirror.signal(cm, "fold", cm, range.from, range.to);
+ }
+
+ function makeWidget(cm, options) {
+ var widget = getOption(cm, options, "widget");
+ if (typeof widget == "string") {
+ var text = document.createTextNode(widget);
+ widget = document.createElement("span");
+ widget.appendChild(text);
+ widget.className = "CodeMirror-foldmarker";
+ }
+ return widget;
+ }
+
+ // Clumsy backwards-compatible interface
+ CodeMirror.newFoldFunction = function(rangeFinder, widget) {
+ return function(cm, pos) { doFold(cm, pos, {rangeFinder: rangeFinder, widget: widget}); };
+ };
+
+ // New-style interface
+ CodeMirror.defineExtension("foldCode", function(pos, options, force) {
+ doFold(this, pos, options, force);
+ });
+
+ CodeMirror.defineExtension("isFolded", function(pos) {
+ var marks = this.findMarksAt(pos);
+ for (var i = 0; i < marks.length; ++i)
+ if (marks[i].__isFold) return true;
+ });
+
+ CodeMirror.commands.toggleFold = function(cm) {
+ cm.foldCode(cm.getCursor());
+ };
+ CodeMirror.commands.fold = function(cm) {
+ cm.foldCode(cm.getCursor(), null, "fold");
+ };
+ CodeMirror.commands.unfold = function(cm) {
+ cm.foldCode(cm.getCursor(), null, "unfold");
+ };
+ CodeMirror.commands.foldAll = function(cm) {
+ cm.operation(function() {
+ for (var i = cm.firstLine(), e = cm.lastLine(); i <= e; i++)
+ cm.foldCode(CodeMirror.Pos(i, 0), null, "fold");
+ });
+ };
+ CodeMirror.commands.unfoldAll = function(cm) {
+ cm.operation(function() {
+ for (var i = cm.firstLine(), e = cm.lastLine(); i <= e; i++)
+ cm.foldCode(CodeMirror.Pos(i, 0), null, "unfold");
+ });
+ };
+
+ CodeMirror.registerHelper("fold", "combine", function() {
+ var funcs = Array.prototype.slice.call(arguments, 0);
+ return function(cm, start) {
+ for (var i = 0; i < funcs.length; ++i) {
+ var found = funcs[i](cm, start);
+ if (found) return found;
+ }
+ };
+ });
+
+ CodeMirror.registerHelper("fold", "auto", function(cm, start) {
+ var helpers = cm.getHelpers(start, "fold");
+ for (var i = 0; i < helpers.length; i++) {
+ var cur = helpers[i](cm, start);
+ if (cur) return cur;
+ }
+ });
+
+ var defaultOptions = {
+ rangeFinder: CodeMirror.fold.auto,
+ widget: "\u2194",
+ minFoldSize: 0,
+ scanUp: false,
+ clearOnEnter: true
+ };
+
+ CodeMirror.defineOption("foldOptions", null);
+
+ function getOption(cm, options, name) {
+ if (options && options[name] !== undefined)
+ return options[name];
+ var editorOptions = cm.options.foldOptions;
+ if (editorOptions && editorOptions[name] !== undefined)
+ return editorOptions[name];
+ return defaultOptions[name];
+ }
+
+ CodeMirror.defineExtension("foldOption", function(options, name) {
+ return getOption(this, options, name);
+ });
+ });
+
+
+/***/ },
+/* 20 */
+/***/ function(module, exports, __webpack_require__) {
+
+ // CodeMirror, copyright (c) by Marijn Haverbeke and others
+ // Distributed under an MIT license: http://codemirror.net/LICENSE
+
+ (function(mod) {
+ if (true) // CommonJS
+ mod(__webpack_require__(2));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+ })(function(CodeMirror) {
+ "use strict";
+
+ CodeMirror.registerHelper("fold", "brace", function(cm, start) {
+ var line = start.line, lineText = cm.getLine(line);
+ var tokenType;
+
+ function findOpening(openCh) {
+ for (var at = start.ch, pass = 0;;) {
+ var found = at <= 0 ? -1 : lineText.lastIndexOf(openCh, at - 1);
+ if (found == -1) {
+ if (pass == 1) break;
+ pass = 1;
+ at = lineText.length;
+ continue;
+ }
+ if (pass == 1 && found < start.ch) break;
+ tokenType = cm.getTokenTypeAt(CodeMirror.Pos(line, found + 1));
+ if (!/^(comment|string)/.test(tokenType)) return found + 1;
+ at = found - 1;
+ }
+ }
+
+ var startToken = "{", endToken = "}", startCh = findOpening("{");
+ if (startCh == null) {
+ startToken = "[", endToken = "]";
+ startCh = findOpening("[");
+ }
+
+ if (startCh == null) return;
+ var count = 1, lastLine = cm.lastLine(), end, endCh;
+ outer: for (var i = line; i <= lastLine; ++i) {
+ var text = cm.getLine(i), pos = i == line ? startCh : 0;
+ for (;;) {
+ var nextOpen = text.indexOf(startToken, pos), nextClose = text.indexOf(endToken, pos);
+ if (nextOpen < 0) nextOpen = text.length;
+ if (nextClose < 0) nextClose = text.length;
+ pos = Math.min(nextOpen, nextClose);
+ if (pos == text.length) break;
+ if (cm.getTokenTypeAt(CodeMirror.Pos(i, pos + 1)) == tokenType) {
+ if (pos == nextOpen) ++count;
+ else if (!--count) { end = i; endCh = pos; break outer; }
+ }
+ ++pos;
+ }
+ }
+ if (end == null || line == end && endCh == startCh) return;
+ return {from: CodeMirror.Pos(line, startCh),
+ to: CodeMirror.Pos(end, endCh)};
+ });
+
+ CodeMirror.registerHelper("fold", "import", function(cm, start) {
+ function hasImport(line) {
+ if (line < cm.firstLine() || line > cm.lastLine()) return null;
+ var start = cm.getTokenAt(CodeMirror.Pos(line, 1));
+ if (!/\S/.test(start.string)) start = cm.getTokenAt(CodeMirror.Pos(line, start.end + 1));
+ if (start.type != "keyword" || start.string != "import") return null;
+ // Now find closing semicolon, return its position
+ for (var i = line, e = Math.min(cm.lastLine(), line + 10); i <= e; ++i) {
+ var text = cm.getLine(i), semi = text.indexOf(";");
+ if (semi != -1) return {startCh: start.end, end: CodeMirror.Pos(i, semi)};
+ }
+ }
+
+ var startLine = start.line, has = hasImport(startLine), prev;
+ if (!has || hasImport(startLine - 1) || ((prev = hasImport(startLine - 2)) && prev.end.line == startLine - 1))
+ return null;
+ for (var end = has.end;;) {
+ var next = hasImport(end.line + 1);
+ if (next == null) break;
+ end = next.end;
+ }
+ return {from: cm.clipPos(CodeMirror.Pos(startLine, has.startCh + 1)), to: end};
+ });
+
+ CodeMirror.registerHelper("fold", "include", function(cm, start) {
+ function hasInclude(line) {
+ if (line < cm.firstLine() || line > cm.lastLine()) return null;
+ var start = cm.getTokenAt(CodeMirror.Pos(line, 1));
+ if (!/\S/.test(start.string)) start = cm.getTokenAt(CodeMirror.Pos(line, start.end + 1));
+ if (start.type == "meta" && start.string.slice(0, 8) == "#include") return start.start + 8;
+ }
+
+ var startLine = start.line, has = hasInclude(startLine);
+ if (has == null || hasInclude(startLine - 1) != null) return null;
+ for (var end = startLine;;) {
+ var next = hasInclude(end + 1);
+ if (next == null) break;
+ ++end;
+ }
+ return {from: CodeMirror.Pos(startLine, has + 1),
+ to: cm.clipPos(CodeMirror.Pos(end))};
+ });
+
+ });
+
+
+/***/ },
+/* 21 */
+/***/ function(module, exports, __webpack_require__) {
+
+ // CodeMirror, copyright (c) by Marijn Haverbeke and others
+ // Distributed under an MIT license: http://codemirror.net/LICENSE
+
+ (function(mod) {
+ if (true) // CommonJS
+ mod(__webpack_require__(2));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+ })(function(CodeMirror) {
+ "use strict";
+
+ CodeMirror.registerGlobalHelper("fold", "comment", function(mode) {
+ return mode.blockCommentStart && mode.blockCommentEnd;
+ }, function(cm, start) {
+ var mode = cm.getModeAt(start), startToken = mode.blockCommentStart, endToken = mode.blockCommentEnd;
+ if (!startToken || !endToken) return;
+ var line = start.line, lineText = cm.getLine(line);
+
+ var startCh;
+ for (var at = start.ch, pass = 0;;) {
+ var found = at <= 0 ? -1 : lineText.lastIndexOf(startToken, at - 1);
+ if (found == -1) {
+ if (pass == 1) return;
+ pass = 1;
+ at = lineText.length;
+ continue;
+ }
+ if (pass == 1 && found < start.ch) return;
+ if (/comment/.test(cm.getTokenTypeAt(CodeMirror.Pos(line, found + 1))) &&
+ (found == 0 || lineText.slice(found - endToken.length, found) == endToken ||
+ !/comment/.test(cm.getTokenTypeAt(CodeMirror.Pos(line, found))))) {
+ startCh = found + startToken.length;
+ break;
+ }
+ at = found - 1;
+ }
+
+ var depth = 1, lastLine = cm.lastLine(), end, endCh;
+ outer: for (var i = line; i <= lastLine; ++i) {
+ var text = cm.getLine(i), pos = i == line ? startCh : 0;
+ for (;;) {
+ var nextOpen = text.indexOf(startToken, pos), nextClose = text.indexOf(endToken, pos);
+ if (nextOpen < 0) nextOpen = text.length;
+ if (nextClose < 0) nextClose = text.length;
+ pos = Math.min(nextOpen, nextClose);
+ if (pos == text.length) break;
+ if (pos == nextOpen) ++depth;
+ else if (!--depth) { end = i; endCh = pos; break outer; }
+ ++pos;
+ }
+ }
+ if (end == null || line == end && endCh == startCh) return;
+ return {from: CodeMirror.Pos(line, startCh),
+ to: CodeMirror.Pos(end, endCh)};
+ });
+
+ });
+
+
+/***/ },
+/* 22 */
+/***/ function(module, exports, __webpack_require__) {
+
+ // CodeMirror, copyright (c) by Marijn Haverbeke and others
+ // Distributed under an MIT license: http://codemirror.net/LICENSE
+
+ (function(mod) {
+ if (true) // CommonJS
+ mod(__webpack_require__(2));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+ })(function(CodeMirror) {
+ "use strict";
+
+ var Pos = CodeMirror.Pos;
+ function cmp(a, b) { return a.line - b.line || a.ch - b.ch; }
+
+ var nameStartChar = "A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD";
+ var nameChar = nameStartChar + "\-\:\.0-9\\u00B7\\u0300-\\u036F\\u203F-\\u2040";
+ var xmlTagStart = new RegExp("<(/?)([" + nameStartChar + "][" + nameChar + "]*)", "g");
+
+ function Iter(cm, line, ch, range) {
+ this.line = line; this.ch = ch;
+ this.cm = cm; this.text = cm.getLine(line);
+ this.min = range ? range.from : cm.firstLine();
+ this.max = range ? range.to - 1 : cm.lastLine();
+ }
+
+ function tagAt(iter, ch) {
+ var type = iter.cm.getTokenTypeAt(Pos(iter.line, ch));
+ return type && /\btag\b/.test(type);
+ }
+
+ function nextLine(iter) {
+ if (iter.line >= iter.max) return;
+ iter.ch = 0;
+ iter.text = iter.cm.getLine(++iter.line);
+ return true;
+ }
+ function prevLine(iter) {
+ if (iter.line <= iter.min) return;
+ iter.text = iter.cm.getLine(--iter.line);
+ iter.ch = iter.text.length;
+ return true;
+ }
+
+ function toTagEnd(iter) {
+ for (;;) {
+ var gt = iter.text.indexOf(">", iter.ch);
+ if (gt == -1) { if (nextLine(iter)) continue; else return; }
+ if (!tagAt(iter, gt + 1)) { iter.ch = gt + 1; continue; }
+ var lastSlash = iter.text.lastIndexOf("/", gt);
+ var selfClose = lastSlash > -1 && !/\S/.test(iter.text.slice(lastSlash + 1, gt));
+ iter.ch = gt + 1;
+ return selfClose ? "selfClose" : "regular";
+ }
+ }
+ function toTagStart(iter) {
+ for (;;) {
+ var lt = iter.ch ? iter.text.lastIndexOf("<", iter.ch - 1) : -1;
+ if (lt == -1) { if (prevLine(iter)) continue; else return; }
+ if (!tagAt(iter, lt + 1)) { iter.ch = lt; continue; }
+ xmlTagStart.lastIndex = lt;
+ iter.ch = lt;
+ var match = xmlTagStart.exec(iter.text);
+ if (match && match.index == lt) return match;
+ }
+ }
+
+ function toNextTag(iter) {
+ for (;;) {
+ xmlTagStart.lastIndex = iter.ch;
+ var found = xmlTagStart.exec(iter.text);
+ if (!found) { if (nextLine(iter)) continue; else return; }
+ if (!tagAt(iter, found.index + 1)) { iter.ch = found.index + 1; continue; }
+ iter.ch = found.index + found[0].length;
+ return found;
+ }
+ }
+ function toPrevTag(iter) {
+ for (;;) {
+ var gt = iter.ch ? iter.text.lastIndexOf(">", iter.ch - 1) : -1;
+ if (gt == -1) { if (prevLine(iter)) continue; else return; }
+ if (!tagAt(iter, gt + 1)) { iter.ch = gt; continue; }
+ var lastSlash = iter.text.lastIndexOf("/", gt);
+ var selfClose = lastSlash > -1 && !/\S/.test(iter.text.slice(lastSlash + 1, gt));
+ iter.ch = gt + 1;
+ return selfClose ? "selfClose" : "regular";
+ }
+ }
+
+ function findMatchingClose(iter, tag) {
+ var stack = [];
+ for (;;) {
+ var next = toNextTag(iter), end, startLine = iter.line, startCh = iter.ch - (next ? next[0].length : 0);
+ if (!next || !(end = toTagEnd(iter))) return;
+ if (end == "selfClose") continue;
+ if (next[1]) { // closing tag
+ for (var i = stack.length - 1; i >= 0; --i) if (stack[i] == next[2]) {
+ stack.length = i;
+ break;
+ }
+ if (i < 0 && (!tag || tag == next[2])) return {
+ tag: next[2],
+ from: Pos(startLine, startCh),
+ to: Pos(iter.line, iter.ch)
+ };
+ } else { // opening tag
+ stack.push(next[2]);
+ }
+ }
+ }
+ function findMatchingOpen(iter, tag) {
+ var stack = [];
+ for (;;) {
+ var prev = toPrevTag(iter);
+ if (!prev) return;
+ if (prev == "selfClose") { toTagStart(iter); continue; }
+ var endLine = iter.line, endCh = iter.ch;
+ var start = toTagStart(iter);
+ if (!start) return;
+ if (start[1]) { // closing tag
+ stack.push(start[2]);
+ } else { // opening tag
+ for (var i = stack.length - 1; i >= 0; --i) if (stack[i] == start[2]) {
+ stack.length = i;
+ break;
+ }
+ if (i < 0 && (!tag || tag == start[2])) return {
+ tag: start[2],
+ from: Pos(iter.line, iter.ch),
+ to: Pos(endLine, endCh)
+ };
+ }
+ }
+ }
+
+ CodeMirror.registerHelper("fold", "xml", function(cm, start) {
+ var iter = new Iter(cm, start.line, 0);
+ for (;;) {
+ var openTag = toNextTag(iter), end;
+ if (!openTag || iter.line != start.line || !(end = toTagEnd(iter))) return;
+ if (!openTag[1] && end != "selfClose") {
+ var startPos = Pos(iter.line, iter.ch);
+ var endPos = findMatchingClose(iter, openTag[2]);
+ return endPos && {from: startPos, to: endPos.from};
+ }
+ }
+ });
+ CodeMirror.findMatchingTag = function(cm, pos, range) {
+ var iter = new Iter(cm, pos.line, pos.ch, range);
+ if (iter.text.indexOf(">") == -1 && iter.text.indexOf("<") == -1) return;
+ var end = toTagEnd(iter), to = end && Pos(iter.line, iter.ch);
+ var start = end && toTagStart(iter);
+ if (!end || !start || cmp(iter, pos) > 0) return;
+ var here = {from: Pos(iter.line, iter.ch), to: to, tag: start[2]};
+ if (end == "selfClose") return {open: here, close: null, at: "open"};
+
+ if (start[1]) { // closing tag
+ return {open: findMatchingOpen(iter, start[2]), close: here, at: "close"};
+ } else { // opening tag
+ iter = new Iter(cm, to.line, to.ch, range);
+ return {open: here, close: findMatchingClose(iter, start[2]), at: "open"};
+ }
+ };
+
+ CodeMirror.findEnclosingTag = function(cm, pos, range) {
+ var iter = new Iter(cm, pos.line, pos.ch, range);
+ for (;;) {
+ var open = findMatchingOpen(iter);
+ if (!open) break;
+ var forward = new Iter(cm, pos.line, pos.ch, range);
+ var close = findMatchingClose(forward, open.tag);
+ if (close) return {open: open, close: close};
+ }
+ };
+
+ // Used by addon/edit/closetag.js
+ CodeMirror.scanForClosingTag = function(cm, pos, name, end) {
+ var iter = new Iter(cm, pos.line, pos.ch, end ? {from: 0, to: end} : null);
+ return findMatchingClose(iter, name);
+ };
+ });
+
+
+/***/ },
+/* 23 */
+/***/ function(module, exports, __webpack_require__) {
+
+ // CodeMirror, copyright (c) by Marijn Haverbeke and others
+ // Distributed under an MIT license: http://codemirror.net/LICENSE
+
+ (function(mod) {
+ if (true) // CommonJS
+ mod(__webpack_require__(2), __webpack_require__(19));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror", "./foldcode"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+ })(function(CodeMirror) {
+ "use strict";
+
+ CodeMirror.defineOption("foldGutter", false, function(cm, val, old) {
+ if (old && old != CodeMirror.Init) {
+ cm.clearGutter(cm.state.foldGutter.options.gutter);
+ cm.state.foldGutter = null;
+ cm.off("gutterClick", onGutterClick);
+ cm.off("change", onChange);
+ cm.off("viewportChange", onViewportChange);
+ cm.off("fold", onFold);
+ cm.off("unfold", onFold);
+ cm.off("swapDoc", onChange);
+ }
+ if (val) {
+ cm.state.foldGutter = new State(parseOptions(val));
+ updateInViewport(cm);
+ cm.on("gutterClick", onGutterClick);
+ cm.on("change", onChange);
+ cm.on("viewportChange", onViewportChange);
+ cm.on("fold", onFold);
+ cm.on("unfold", onFold);
+ cm.on("swapDoc", onChange);
+ }
+ });
+
+ var Pos = CodeMirror.Pos;
+
+ function State(options) {
+ this.options = options;
+ this.from = this.to = 0;
+ }
+
+ function parseOptions(opts) {
+ if (opts === true) opts = {};
+ if (opts.gutter == null) opts.gutter = "CodeMirror-foldgutter";
+ if (opts.indicatorOpen == null) opts.indicatorOpen = "CodeMirror-foldgutter-open";
+ if (opts.indicatorFolded == null) opts.indicatorFolded = "CodeMirror-foldgutter-folded";
+ return opts;
+ }
+
+ function isFolded(cm, line) {
+ var marks = cm.findMarks(Pos(line, 0), Pos(line + 1, 0));
+ for (var i = 0; i < marks.length; ++i)
+ if (marks[i].__isFold && marks[i].find().from.line == line) return marks[i];
+ }
+
+ function marker(spec) {
+ if (typeof spec == "string") {
+ var elt = document.createElement("div");
+ elt.className = spec + " CodeMirror-guttermarker-subtle";
+ return elt;
+ } else {
+ return spec.cloneNode(true);
+ }
+ }
+
+ function updateFoldInfo(cm, from, to) {
+ var opts = cm.state.foldGutter.options, cur = from;
+ var minSize = cm.foldOption(opts, "minFoldSize");
+ var func = cm.foldOption(opts, "rangeFinder");
+ cm.eachLine(from, to, function(line) {
+ var mark = null;
+ if (isFolded(cm, cur)) {
+ mark = marker(opts.indicatorFolded);
+ } else {
+ var pos = Pos(cur, 0);
+ var range = func && func(cm, pos);
+ if (range && range.to.line - range.from.line >= minSize)
+ mark = marker(opts.indicatorOpen);
+ }
+ cm.setGutterMarker(line, opts.gutter, mark);
+ ++cur;
+ });
+ }
+
+ function updateInViewport(cm) {
+ var vp = cm.getViewport(), state = cm.state.foldGutter;
+ if (!state) return;
+ cm.operation(function() {
+ updateFoldInfo(cm, vp.from, vp.to);
+ });
+ state.from = vp.from; state.to = vp.to;
+ }
+
+ function onGutterClick(cm, line, gutter) {
+ var state = cm.state.foldGutter;
+ if (!state) return;
+ var opts = state.options;
+ if (gutter != opts.gutter) return;
+ var folded = isFolded(cm, line);
+ if (folded) folded.clear();
+ else cm.foldCode(Pos(line, 0), opts.rangeFinder);
+ }
+
+ function onChange(cm) {
+ var state = cm.state.foldGutter;
+ if (!state) return;
+ var opts = state.options;
+ state.from = state.to = 0;
+ clearTimeout(state.changeUpdate);
+ state.changeUpdate = setTimeout(function() { updateInViewport(cm); }, opts.foldOnChangeTimeSpan || 600);
+ }
+
+ function onViewportChange(cm) {
+ var state = cm.state.foldGutter;
+ if (!state) return;
+ var opts = state.options;
+ clearTimeout(state.changeUpdate);
+ state.changeUpdate = setTimeout(function() {
+ var vp = cm.getViewport();
+ if (state.from == state.to || vp.from - state.to > 20 || state.from - vp.to > 20) {
+ updateInViewport(cm);
+ } else {
+ cm.operation(function() {
+ if (vp.from < state.from) {
+ updateFoldInfo(cm, vp.from, state.from);
+ state.from = vp.from;
+ }
+ if (vp.to > state.to) {
+ updateFoldInfo(cm, state.to, vp.to);
+ state.to = vp.to;
+ }
+ });
+ }
+ }, opts.updateViewportTimeSpan || 400);
+ }
+
+ function onFold(cm, from) {
+ var state = cm.state.foldGutter;
+ if (!state) return;
+ var line = from.line;
+ if (line >= state.from && line < state.to)
+ updateFoldInfo(cm, line, line + 1);
+ }
+ });
+
+
+/***/ }
+/******/ ]); \ No newline at end of file
diff --git a/devtools/client/sourceeditor/codemirror/keymap/emacs.js b/devtools/client/sourceeditor/codemirror/keymap/emacs.js
new file mode 100644
index 000000000..3eec1e576
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/keymap/emacs.js
@@ -0,0 +1,412 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../lib/codemirror"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function(CodeMirror) {
+ "use strict";
+
+ var Pos = CodeMirror.Pos;
+ function posEq(a, b) { return a.line == b.line && a.ch == b.ch; }
+
+ // Kill 'ring'
+
+ var killRing = [];
+ function addToRing(str) {
+ killRing.push(str);
+ if (killRing.length > 50) killRing.shift();
+ }
+ function growRingTop(str) {
+ if (!killRing.length) return addToRing(str);
+ killRing[killRing.length - 1] += str;
+ }
+ function getFromRing(n) { return killRing[killRing.length - (n ? Math.min(n, 1) : 1)] || ""; }
+ function popFromRing() { if (killRing.length > 1) killRing.pop(); return getFromRing(); }
+
+ var lastKill = null;
+
+ function kill(cm, from, to, mayGrow, text) {
+ if (text == null) text = cm.getRange(from, to);
+
+ if (mayGrow && lastKill && lastKill.cm == cm && posEq(from, lastKill.pos) && cm.isClean(lastKill.gen))
+ growRingTop(text);
+ else
+ addToRing(text);
+ cm.replaceRange("", from, to, "+delete");
+
+ if (mayGrow) lastKill = {cm: cm, pos: from, gen: cm.changeGeneration()};
+ else lastKill = null;
+ }
+
+ // Boundaries of various units
+
+ function byChar(cm, pos, dir) {
+ return cm.findPosH(pos, dir, "char", true);
+ }
+
+ function byWord(cm, pos, dir) {
+ return cm.findPosH(pos, dir, "word", true);
+ }
+
+ function byLine(cm, pos, dir) {
+ return cm.findPosV(pos, dir, "line", cm.doc.sel.goalColumn);
+ }
+
+ function byPage(cm, pos, dir) {
+ return cm.findPosV(pos, dir, "page", cm.doc.sel.goalColumn);
+ }
+
+ function byParagraph(cm, pos, dir) {
+ var no = pos.line, line = cm.getLine(no);
+ var sawText = /\S/.test(dir < 0 ? line.slice(0, pos.ch) : line.slice(pos.ch));
+ var fst = cm.firstLine(), lst = cm.lastLine();
+ for (;;) {
+ no += dir;
+ if (no < fst || no > lst)
+ return cm.clipPos(Pos(no - dir, dir < 0 ? 0 : null));
+ line = cm.getLine(no);
+ var hasText = /\S/.test(line);
+ if (hasText) sawText = true;
+ else if (sawText) return Pos(no, 0);
+ }
+ }
+
+ function bySentence(cm, pos, dir) {
+ var line = pos.line, ch = pos.ch;
+ var text = cm.getLine(pos.line), sawWord = false;
+ for (;;) {
+ var next = text.charAt(ch + (dir < 0 ? -1 : 0));
+ if (!next) { // End/beginning of line reached
+ if (line == (dir < 0 ? cm.firstLine() : cm.lastLine())) return Pos(line, ch);
+ text = cm.getLine(line + dir);
+ if (!/\S/.test(text)) return Pos(line, ch);
+ line += dir;
+ ch = dir < 0 ? text.length : 0;
+ continue;
+ }
+ if (sawWord && /[!?.]/.test(next)) return Pos(line, ch + (dir > 0 ? 1 : 0));
+ if (!sawWord) sawWord = /\w/.test(next);
+ ch += dir;
+ }
+ }
+
+ function byExpr(cm, pos, dir) {
+ var wrap;
+ if (cm.findMatchingBracket && (wrap = cm.findMatchingBracket(pos, true))
+ && wrap.match && (wrap.forward ? 1 : -1) == dir)
+ return dir > 0 ? Pos(wrap.to.line, wrap.to.ch + 1) : wrap.to;
+
+ for (var first = true;; first = false) {
+ var token = cm.getTokenAt(pos);
+ var after = Pos(pos.line, dir < 0 ? token.start : token.end);
+ if (first && dir > 0 && token.end == pos.ch || !/\w/.test(token.string)) {
+ var newPos = cm.findPosH(after, dir, "char");
+ if (posEq(after, newPos)) return pos;
+ else pos = newPos;
+ } else {
+ return after;
+ }
+ }
+ }
+
+ // Prefixes (only crudely supported)
+
+ function getPrefix(cm, precise) {
+ var digits = cm.state.emacsPrefix;
+ if (!digits) return precise ? null : 1;
+ clearPrefix(cm);
+ return digits == "-" ? -1 : Number(digits);
+ }
+
+ function repeated(cmd) {
+ var f = typeof cmd == "string" ? function(cm) { cm.execCommand(cmd); } : cmd;
+ return function(cm) {
+ var prefix = getPrefix(cm);
+ f(cm);
+ for (var i = 1; i < prefix; ++i) f(cm);
+ };
+ }
+
+ function findEnd(cm, pos, by, dir) {
+ var prefix = getPrefix(cm);
+ if (prefix < 0) { dir = -dir; prefix = -prefix; }
+ for (var i = 0; i < prefix; ++i) {
+ var newPos = by(cm, pos, dir);
+ if (posEq(newPos, pos)) break;
+ pos = newPos;
+ }
+ return pos;
+ }
+
+ function move(by, dir) {
+ var f = function(cm) {
+ cm.extendSelection(findEnd(cm, cm.getCursor(), by, dir));
+ };
+ f.motion = true;
+ return f;
+ }
+
+ function killTo(cm, by, dir) {
+ var selections = cm.listSelections(), cursor;
+ var i = selections.length;
+ while (i--) {
+ cursor = selections[i].head;
+ kill(cm, cursor, findEnd(cm, cursor, by, dir), true);
+ }
+ }
+
+ function killRegion(cm) {
+ if (cm.somethingSelected()) {
+ var selections = cm.listSelections(), selection;
+ var i = selections.length;
+ while (i--) {
+ selection = selections[i];
+ kill(cm, selection.anchor, selection.head);
+ }
+ return true;
+ }
+ }
+
+ function addPrefix(cm, digit) {
+ if (cm.state.emacsPrefix) {
+ if (digit != "-") cm.state.emacsPrefix += digit;
+ return;
+ }
+ // Not active yet
+ cm.state.emacsPrefix = digit;
+ cm.on("keyHandled", maybeClearPrefix);
+ cm.on("inputRead", maybeDuplicateInput);
+ }
+
+ var prefixPreservingKeys = {"Alt-G": true, "Ctrl-X": true, "Ctrl-Q": true, "Ctrl-U": true};
+
+ function maybeClearPrefix(cm, arg) {
+ if (!cm.state.emacsPrefixMap && !prefixPreservingKeys.hasOwnProperty(arg))
+ clearPrefix(cm);
+ }
+
+ function clearPrefix(cm) {
+ cm.state.emacsPrefix = null;
+ cm.off("keyHandled", maybeClearPrefix);
+ cm.off("inputRead", maybeDuplicateInput);
+ }
+
+ function maybeDuplicateInput(cm, event) {
+ var dup = getPrefix(cm);
+ if (dup > 1 && event.origin == "+input") {
+ var one = event.text.join("\n"), txt = "";
+ for (var i = 1; i < dup; ++i) txt += one;
+ cm.replaceSelection(txt);
+ }
+ }
+
+ function addPrefixMap(cm) {
+ cm.state.emacsPrefixMap = true;
+ cm.addKeyMap(prefixMap);
+ cm.on("keyHandled", maybeRemovePrefixMap);
+ cm.on("inputRead", maybeRemovePrefixMap);
+ }
+
+ function maybeRemovePrefixMap(cm, arg) {
+ if (typeof arg == "string" && (/^\d$/.test(arg) || arg == "Ctrl-U")) return;
+ cm.removeKeyMap(prefixMap);
+ cm.state.emacsPrefixMap = false;
+ cm.off("keyHandled", maybeRemovePrefixMap);
+ cm.off("inputRead", maybeRemovePrefixMap);
+ }
+
+ // Utilities
+
+ function setMark(cm) {
+ cm.setCursor(cm.getCursor());
+ cm.setExtending(!cm.getExtending());
+ cm.on("change", function() { cm.setExtending(false); });
+ }
+
+ function clearMark(cm) {
+ cm.setExtending(false);
+ cm.setCursor(cm.getCursor());
+ }
+
+ function getInput(cm, msg, f) {
+ if (cm.openDialog)
+ cm.openDialog(msg + ": <input type=\"text\" style=\"width: 10em\"/>", f, {bottom: true});
+ else
+ f(prompt(msg, ""));
+ }
+
+ function operateOnWord(cm, op) {
+ var start = cm.getCursor(), end = cm.findPosH(start, 1, "word");
+ cm.replaceRange(op(cm.getRange(start, end)), start, end);
+ cm.setCursor(end);
+ }
+
+ function toEnclosingExpr(cm) {
+ var pos = cm.getCursor(), line = pos.line, ch = pos.ch;
+ var stack = [];
+ while (line >= cm.firstLine()) {
+ var text = cm.getLine(line);
+ for (var i = ch == null ? text.length : ch; i > 0;) {
+ var ch = text.charAt(--i);
+ if (ch == ")")
+ stack.push("(");
+ else if (ch == "]")
+ stack.push("[");
+ else if (ch == "}")
+ stack.push("{");
+ else if (/[\(\{\[]/.test(ch) && (!stack.length || stack.pop() != ch))
+ return cm.extendSelection(Pos(line, i));
+ }
+ --line; ch = null;
+ }
+ }
+
+ function quit(cm) {
+ cm.execCommand("clearSearch");
+ clearMark(cm);
+ }
+
+ // Actual keymap
+
+ var keyMap = CodeMirror.keyMap.emacs = CodeMirror.normalizeKeyMap({
+ "Ctrl-W": function(cm) {kill(cm, cm.getCursor("start"), cm.getCursor("end"));},
+ "Ctrl-K": repeated(function(cm) {
+ var start = cm.getCursor(), end = cm.clipPos(Pos(start.line));
+ var text = cm.getRange(start, end);
+ if (!/\S/.test(text)) {
+ text += "\n";
+ end = Pos(start.line + 1, 0);
+ }
+ kill(cm, start, end, true, text);
+ }),
+ "Alt-W": function(cm) {
+ addToRing(cm.getSelection());
+ clearMark(cm);
+ },
+ "Ctrl-Y": function(cm) {
+ var start = cm.getCursor();
+ cm.replaceRange(getFromRing(getPrefix(cm)), start, start, "paste");
+ cm.setSelection(start, cm.getCursor());
+ },
+ "Alt-Y": function(cm) {cm.replaceSelection(popFromRing(), "around", "paste");},
+
+ "Ctrl-Space": setMark, "Ctrl-Shift-2": setMark,
+
+ "Ctrl-F": move(byChar, 1), "Ctrl-B": move(byChar, -1),
+ "Right": move(byChar, 1), "Left": move(byChar, -1),
+ "Ctrl-D": function(cm) { killTo(cm, byChar, 1); },
+ "Delete": function(cm) { killRegion(cm) || killTo(cm, byChar, 1); },
+ "Ctrl-H": function(cm) { killTo(cm, byChar, -1); },
+ "Backspace": function(cm) { killRegion(cm) || killTo(cm, byChar, -1); },
+
+ "Alt-F": move(byWord, 1), "Alt-B": move(byWord, -1),
+ "Alt-D": function(cm) { killTo(cm, byWord, 1); },
+ "Alt-Backspace": function(cm) { killTo(cm, byWord, -1); },
+
+ "Ctrl-N": move(byLine, 1), "Ctrl-P": move(byLine, -1),
+ "Down": move(byLine, 1), "Up": move(byLine, -1),
+ "Ctrl-A": "goLineStart", "Ctrl-E": "goLineEnd",
+ "End": "goLineEnd", "Home": "goLineStart",
+
+ "Alt-V": move(byPage, -1), "Ctrl-V": move(byPage, 1),
+ "PageUp": move(byPage, -1), "PageDown": move(byPage, 1),
+
+ "Ctrl-Up": move(byParagraph, -1), "Ctrl-Down": move(byParagraph, 1),
+
+ "Alt-A": move(bySentence, -1), "Alt-E": move(bySentence, 1),
+ "Alt-K": function(cm) { killTo(cm, bySentence, 1); },
+
+ "Ctrl-Alt-K": function(cm) { killTo(cm, byExpr, 1); },
+ "Ctrl-Alt-Backspace": function(cm) { killTo(cm, byExpr, -1); },
+ "Ctrl-Alt-F": move(byExpr, 1), "Ctrl-Alt-B": move(byExpr, -1),
+
+ "Shift-Ctrl-Alt-2": function(cm) {
+ var cursor = cm.getCursor();
+ cm.setSelection(findEnd(cm, cursor, byExpr, 1), cursor);
+ },
+ "Ctrl-Alt-T": function(cm) {
+ var leftStart = byExpr(cm, cm.getCursor(), -1), leftEnd = byExpr(cm, leftStart, 1);
+ var rightEnd = byExpr(cm, leftEnd, 1), rightStart = byExpr(cm, rightEnd, -1);
+ cm.replaceRange(cm.getRange(rightStart, rightEnd) + cm.getRange(leftEnd, rightStart) +
+ cm.getRange(leftStart, leftEnd), leftStart, rightEnd);
+ },
+ "Ctrl-Alt-U": repeated(toEnclosingExpr),
+
+ "Alt-Space": function(cm) {
+ var pos = cm.getCursor(), from = pos.ch, to = pos.ch, text = cm.getLine(pos.line);
+ while (from && /\s/.test(text.charAt(from - 1))) --from;
+ while (to < text.length && /\s/.test(text.charAt(to))) ++to;
+ cm.replaceRange(" ", Pos(pos.line, from), Pos(pos.line, to));
+ },
+ "Ctrl-O": repeated(function(cm) { cm.replaceSelection("\n", "start"); }),
+ "Ctrl-T": repeated(function(cm) {
+ cm.execCommand("transposeChars");
+ }),
+
+ "Alt-C": repeated(function(cm) {
+ operateOnWord(cm, function(w) {
+ var letter = w.search(/\w/);
+ if (letter == -1) return w;
+ return w.slice(0, letter) + w.charAt(letter).toUpperCase() + w.slice(letter + 1).toLowerCase();
+ });
+ }),
+ "Alt-U": repeated(function(cm) {
+ operateOnWord(cm, function(w) { return w.toUpperCase(); });
+ }),
+ "Alt-L": repeated(function(cm) {
+ operateOnWord(cm, function(w) { return w.toLowerCase(); });
+ }),
+
+ "Alt-;": "toggleComment",
+
+ "Ctrl-/": repeated("undo"), "Shift-Ctrl--": repeated("undo"),
+ "Ctrl-Z": repeated("undo"), "Cmd-Z": repeated("undo"),
+ "Shift-Alt-,": "goDocStart", "Shift-Alt-.": "goDocEnd",
+ "Ctrl-S": "findNext", "Ctrl-R": "findPrev", "Ctrl-G": quit, "Shift-Alt-5": "replace",
+ "Alt-/": "autocomplete",
+ "Ctrl-J": "newlineAndIndent", "Enter": false, "Tab": "indentAuto",
+
+ "Alt-G G": function(cm) {
+ var prefix = getPrefix(cm, true);
+ if (prefix != null && prefix > 0) return cm.setCursor(prefix - 1);
+
+ getInput(cm, "Goto line", function(str) {
+ var num;
+ if (str && !isNaN(num = Number(str)) && num == (num|0) && num > 0)
+ cm.setCursor(num - 1);
+ });
+ },
+
+ "Ctrl-X Tab": function(cm) {
+ cm.indentSelection(getPrefix(cm, true) || cm.getOption("indentUnit"));
+ },
+ "Ctrl-X Ctrl-X": function(cm) {
+ cm.setSelection(cm.getCursor("head"), cm.getCursor("anchor"));
+ },
+ "Ctrl-X Ctrl-S": "save",
+ "Ctrl-X Ctrl-W": "save",
+ "Ctrl-X S": "saveAll",
+ "Ctrl-X F": "open",
+ "Ctrl-X U": repeated("undo"),
+ "Ctrl-X K": "close",
+ "Ctrl-X Delete": function(cm) { kill(cm, cm.getCursor(), bySentence(cm, cm.getCursor(), 1), true); },
+ "Ctrl-X H": "selectAll",
+
+ "Ctrl-Q Tab": repeated("insertTab"),
+ "Ctrl-U": addPrefixMap
+ });
+
+ var prefixMap = {"Ctrl-G": clearPrefix};
+ function regPrefix(d) {
+ prefixMap[d] = function(cm) { addPrefix(cm, d); };
+ keyMap["Ctrl-" + d] = function(cm) { addPrefix(cm, d); };
+ prefixPreservingKeys["Ctrl-" + d] = true;
+ }
+ for (var i = 0; i < 10; ++i) regPrefix(String(i));
+ regPrefix("-");
+});
diff --git a/devtools/client/sourceeditor/codemirror/keymap/sublime.js b/devtools/client/sourceeditor/codemirror/keymap/sublime.js
new file mode 100644
index 000000000..ed6b84742
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/keymap/sublime.js
@@ -0,0 +1,580 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+// A rough approximation of Sublime Text's keybindings
+// Depends on addon/search/searchcursor.js and optionally addon/dialog/dialogs.js
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../lib/codemirror"), require("../addon/search/searchcursor"), require("../addon/edit/matchbrackets"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../lib/codemirror", "../addon/search/searchcursor", "../addon/edit/matchbrackets"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function(CodeMirror) {
+ "use strict";
+
+ var map = CodeMirror.keyMap.sublime = {fallthrough: "default"};
+ var cmds = CodeMirror.commands;
+ var Pos = CodeMirror.Pos;
+ var mac = CodeMirror.keyMap["default"] == CodeMirror.keyMap.macDefault;
+ var ctrl = mac ? "Cmd-" : "Ctrl-";
+
+ // This is not exactly Sublime's algorithm. I couldn't make heads or tails of that.
+ function findPosSubword(doc, start, dir) {
+ if (dir < 0 && start.ch == 0) return doc.clipPos(Pos(start.line - 1));
+ var line = doc.getLine(start.line);
+ if (dir > 0 && start.ch >= line.length) return doc.clipPos(Pos(start.line + 1, 0));
+ var state = "start", type;
+ for (var pos = start.ch, e = dir < 0 ? 0 : line.length, i = 0; pos != e; pos += dir, i++) {
+ var next = line.charAt(dir < 0 ? pos - 1 : pos);
+ var cat = next != "_" && CodeMirror.isWordChar(next) ? "w" : "o";
+ if (cat == "w" && next.toUpperCase() == next) cat = "W";
+ if (state == "start") {
+ if (cat != "o") { state = "in"; type = cat; }
+ } else if (state == "in") {
+ if (type != cat) {
+ if (type == "w" && cat == "W" && dir < 0) pos--;
+ if (type == "W" && cat == "w" && dir > 0) { type = "w"; continue; }
+ break;
+ }
+ }
+ }
+ return Pos(start.line, pos);
+ }
+
+ function moveSubword(cm, dir) {
+ cm.extendSelectionsBy(function(range) {
+ if (cm.display.shift || cm.doc.extend || range.empty())
+ return findPosSubword(cm.doc, range.head, dir);
+ else
+ return dir < 0 ? range.from() : range.to();
+ });
+ }
+
+ cmds[map["Alt-Left"] = "goSubwordLeft"] = function(cm) { moveSubword(cm, -1); };
+ cmds[map["Alt-Right"] = "goSubwordRight"] = function(cm) { moveSubword(cm, 1); };
+
+ if (mac) map["Cmd-Left"] = "goLineStartSmart";
+
+ var scrollLineCombo = mac ? "Ctrl-Alt-" : "Ctrl-";
+
+ cmds[map[scrollLineCombo + "Up"] = "scrollLineUp"] = function(cm) {
+ var info = cm.getScrollInfo();
+ if (!cm.somethingSelected()) {
+ var visibleBottomLine = cm.lineAtHeight(info.top + info.clientHeight, "local");
+ if (cm.getCursor().line >= visibleBottomLine)
+ cm.execCommand("goLineUp");
+ }
+ cm.scrollTo(null, info.top - cm.defaultTextHeight());
+ };
+ cmds[map[scrollLineCombo + "Down"] = "scrollLineDown"] = function(cm) {
+ var info = cm.getScrollInfo();
+ if (!cm.somethingSelected()) {
+ var visibleTopLine = cm.lineAtHeight(info.top, "local")+1;
+ if (cm.getCursor().line <= visibleTopLine)
+ cm.execCommand("goLineDown");
+ }
+ cm.scrollTo(null, info.top + cm.defaultTextHeight());
+ };
+
+ cmds[map["Shift-" + ctrl + "L"] = "splitSelectionByLine"] = function(cm) {
+ var ranges = cm.listSelections(), lineRanges = [];
+ for (var i = 0; i < ranges.length; i++) {
+ var from = ranges[i].from(), to = ranges[i].to();
+ for (var line = from.line; line <= to.line; ++line)
+ if (!(to.line > from.line && line == to.line && to.ch == 0))
+ lineRanges.push({anchor: line == from.line ? from : Pos(line, 0),
+ head: line == to.line ? to : Pos(line)});
+ }
+ cm.setSelections(lineRanges, 0);
+ };
+
+ map["Shift-Tab"] = "indentLess";
+
+ cmds[map["Esc"] = "singleSelectionTop"] = function(cm) {
+ var range = cm.listSelections()[0];
+ cm.setSelection(range.anchor, range.head, {scroll: false});
+ };
+
+ cmds[map[ctrl + "L"] = "selectLine"] = function(cm) {
+ var ranges = cm.listSelections(), extended = [];
+ for (var i = 0; i < ranges.length; i++) {
+ var range = ranges[i];
+ extended.push({anchor: Pos(range.from().line, 0),
+ head: Pos(range.to().line + 1, 0)});
+ }
+ cm.setSelections(extended);
+ };
+
+ map["Shift-Ctrl-K"] = "deleteLine";
+
+ function insertLine(cm, above) {
+ if (cm.isReadOnly()) return CodeMirror.Pass
+ cm.operation(function() {
+ var len = cm.listSelections().length, newSelection = [], last = -1;
+ for (var i = 0; i < len; i++) {
+ var head = cm.listSelections()[i].head;
+ if (head.line <= last) continue;
+ var at = Pos(head.line + (above ? 0 : 1), 0);
+ cm.replaceRange("\n", at, null, "+insertLine");
+ cm.indentLine(at.line, null, true);
+ newSelection.push({head: at, anchor: at});
+ last = head.line + 1;
+ }
+ cm.setSelections(newSelection);
+ });
+ cm.execCommand("indentAuto");
+ }
+
+ cmds[map[ctrl + "Enter"] = "insertLineAfter"] = function(cm) { return insertLine(cm, false); };
+
+ cmds[map["Shift-" + ctrl + "Enter"] = "insertLineBefore"] = function(cm) { return insertLine(cm, true); };
+
+ function wordAt(cm, pos) {
+ var start = pos.ch, end = start, line = cm.getLine(pos.line);
+ while (start && CodeMirror.isWordChar(line.charAt(start - 1))) --start;
+ while (end < line.length && CodeMirror.isWordChar(line.charAt(end))) ++end;
+ return {from: Pos(pos.line, start), to: Pos(pos.line, end), word: line.slice(start, end)};
+ }
+
+ cmds[map[ctrl + "D"] = "selectNextOccurrence"] = function(cm) {
+ var from = cm.getCursor("from"), to = cm.getCursor("to");
+ var fullWord = cm.state.sublimeFindFullWord == cm.doc.sel;
+ if (CodeMirror.cmpPos(from, to) == 0) {
+ var word = wordAt(cm, from);
+ if (!word.word) return;
+ cm.setSelection(word.from, word.to);
+ fullWord = true;
+ } else {
+ var text = cm.getRange(from, to);
+ var query = fullWord ? new RegExp("\\b" + text + "\\b") : text;
+ var cur = cm.getSearchCursor(query, to);
+ if (cur.findNext()) {
+ cm.addSelection(cur.from(), cur.to());
+ } else {
+ cur = cm.getSearchCursor(query, Pos(cm.firstLine(), 0));
+ if (cur.findNext())
+ cm.addSelection(cur.from(), cur.to());
+ }
+ }
+ if (fullWord)
+ cm.state.sublimeFindFullWord = cm.doc.sel;
+ };
+
+ var mirror = "(){}[]";
+ function selectBetweenBrackets(cm) {
+ var pos = cm.getCursor(), opening = cm.scanForBracket(pos, -1);
+ if (!opening) return;
+ for (;;) {
+ var closing = cm.scanForBracket(pos, 1);
+ if (!closing) return;
+ if (closing.ch == mirror.charAt(mirror.indexOf(opening.ch) + 1)) {
+ cm.setSelection(Pos(opening.pos.line, opening.pos.ch + 1), closing.pos, false);
+ return true;
+ }
+ pos = Pos(closing.pos.line, closing.pos.ch + 1);
+ }
+ }
+
+ cmds[map["Shift-" + ctrl + "Space"] = "selectScope"] = function(cm) {
+ selectBetweenBrackets(cm) || cm.execCommand("selectAll");
+ };
+ cmds[map["Shift-" + ctrl + "M"] = "selectBetweenBrackets"] = function(cm) {
+ if (!selectBetweenBrackets(cm)) return CodeMirror.Pass;
+ };
+
+ cmds[map[ctrl + "M"] = "goToBracket"] = function(cm) {
+ cm.extendSelectionsBy(function(range) {
+ var next = cm.scanForBracket(range.head, 1);
+ if (next && CodeMirror.cmpPos(next.pos, range.head) != 0) return next.pos;
+ var prev = cm.scanForBracket(range.head, -1);
+ return prev && Pos(prev.pos.line, prev.pos.ch + 1) || range.head;
+ });
+ };
+
+ var swapLineCombo = mac ? "Cmd-Ctrl-" : "Shift-Ctrl-";
+
+ cmds[map[swapLineCombo + "Up"] = "swapLineUp"] = function(cm) {
+ if (cm.isReadOnly()) return CodeMirror.Pass
+ var ranges = cm.listSelections(), linesToMove = [], at = cm.firstLine() - 1, newSels = [];
+ for (var i = 0; i < ranges.length; i++) {
+ var range = ranges[i], from = range.from().line - 1, to = range.to().line;
+ newSels.push({anchor: Pos(range.anchor.line - 1, range.anchor.ch),
+ head: Pos(range.head.line - 1, range.head.ch)});
+ if (range.to().ch == 0 && !range.empty()) --to;
+ if (from > at) linesToMove.push(from, to);
+ else if (linesToMove.length) linesToMove[linesToMove.length - 1] = to;
+ at = to;
+ }
+ cm.operation(function() {
+ for (var i = 0; i < linesToMove.length; i += 2) {
+ var from = linesToMove[i], to = linesToMove[i + 1];
+ var line = cm.getLine(from);
+ cm.replaceRange("", Pos(from, 0), Pos(from + 1, 0), "+swapLine");
+ if (to > cm.lastLine())
+ cm.replaceRange("\n" + line, Pos(cm.lastLine()), null, "+swapLine");
+ else
+ cm.replaceRange(line + "\n", Pos(to, 0), null, "+swapLine");
+ }
+ cm.setSelections(newSels);
+ cm.scrollIntoView();
+ });
+ };
+
+ cmds[map[swapLineCombo + "Down"] = "swapLineDown"] = function(cm) {
+ if (cm.isReadOnly()) return CodeMirror.Pass
+ var ranges = cm.listSelections(), linesToMove = [], at = cm.lastLine() + 1;
+ for (var i = ranges.length - 1; i >= 0; i--) {
+ var range = ranges[i], from = range.to().line + 1, to = range.from().line;
+ if (range.to().ch == 0 && !range.empty()) from--;
+ if (from < at) linesToMove.push(from, to);
+ else if (linesToMove.length) linesToMove[linesToMove.length - 1] = to;
+ at = to;
+ }
+ cm.operation(function() {
+ for (var i = linesToMove.length - 2; i >= 0; i -= 2) {
+ var from = linesToMove[i], to = linesToMove[i + 1];
+ var line = cm.getLine(from);
+ if (from == cm.lastLine())
+ cm.replaceRange("", Pos(from - 1), Pos(from), "+swapLine");
+ else
+ cm.replaceRange("", Pos(from, 0), Pos(from + 1, 0), "+swapLine");
+ cm.replaceRange(line + "\n", Pos(to, 0), null, "+swapLine");
+ }
+ cm.scrollIntoView();
+ });
+ };
+
+ cmds[map[ctrl + "/"] = "toggleCommentIndented"] = function(cm) {
+ cm.toggleComment({ indent: true });
+ }
+
+ cmds[map[ctrl + "J"] = "joinLines"] = function(cm) {
+ var ranges = cm.listSelections(), joined = [];
+ for (var i = 0; i < ranges.length; i++) {
+ var range = ranges[i], from = range.from();
+ var start = from.line, end = range.to().line;
+ while (i < ranges.length - 1 && ranges[i + 1].from().line == end)
+ end = ranges[++i].to().line;
+ joined.push({start: start, end: end, anchor: !range.empty() && from});
+ }
+ cm.operation(function() {
+ var offset = 0, ranges = [];
+ for (var i = 0; i < joined.length; i++) {
+ var obj = joined[i];
+ var anchor = obj.anchor && Pos(obj.anchor.line - offset, obj.anchor.ch), head;
+ for (var line = obj.start; line <= obj.end; line++) {
+ var actual = line - offset;
+ if (line == obj.end) head = Pos(actual, cm.getLine(actual).length + 1);
+ if (actual < cm.lastLine()) {
+ cm.replaceRange(" ", Pos(actual), Pos(actual + 1, /^\s*/.exec(cm.getLine(actual + 1))[0].length));
+ ++offset;
+ }
+ }
+ ranges.push({anchor: anchor || head, head: head});
+ }
+ cm.setSelections(ranges, 0);
+ });
+ };
+
+ cmds[map["Shift-" + ctrl + "D"] = "duplicateLine"] = function(cm) {
+ cm.operation(function() {
+ var rangeCount = cm.listSelections().length;
+ for (var i = 0; i < rangeCount; i++) {
+ var range = cm.listSelections()[i];
+ if (range.empty())
+ cm.replaceRange(cm.getLine(range.head.line) + "\n", Pos(range.head.line, 0));
+ else
+ cm.replaceRange(cm.getRange(range.from(), range.to()), range.from());
+ }
+ cm.scrollIntoView();
+ });
+ };
+
+ map[ctrl + "T"] = "transposeChars";
+
+ function sortLines(cm, caseSensitive) {
+ if (cm.isReadOnly()) return CodeMirror.Pass
+ var ranges = cm.listSelections(), toSort = [], selected;
+ for (var i = 0; i < ranges.length; i++) {
+ var range = ranges[i];
+ if (range.empty()) continue;
+ var from = range.from().line, to = range.to().line;
+ while (i < ranges.length - 1 && ranges[i + 1].from().line == to)
+ to = range[++i].to().line;
+ toSort.push(from, to);
+ }
+ if (toSort.length) selected = true;
+ else toSort.push(cm.firstLine(), cm.lastLine());
+
+ cm.operation(function() {
+ var ranges = [];
+ for (var i = 0; i < toSort.length; i += 2) {
+ var from = toSort[i], to = toSort[i + 1];
+ var start = Pos(from, 0), end = Pos(to);
+ var lines = cm.getRange(start, end, false);
+ if (caseSensitive)
+ lines.sort();
+ else
+ lines.sort(function(a, b) {
+ var au = a.toUpperCase(), bu = b.toUpperCase();
+ if (au != bu) { a = au; b = bu; }
+ return a < b ? -1 : a == b ? 0 : 1;
+ });
+ cm.replaceRange(lines, start, end);
+ if (selected) ranges.push({anchor: start, head: end});
+ }
+ if (selected) cm.setSelections(ranges, 0);
+ });
+ }
+
+ cmds[map["F9"] = "sortLines"] = function(cm) { sortLines(cm, true); };
+ cmds[map[ctrl + "F9"] = "sortLinesInsensitive"] = function(cm) { sortLines(cm, false); };
+
+ cmds[map["F2"] = "nextBookmark"] = function(cm) {
+ var marks = cm.state.sublimeBookmarks;
+ if (marks) while (marks.length) {
+ var current = marks.shift();
+ var found = current.find();
+ if (found) {
+ marks.push(current);
+ return cm.setSelection(found.from, found.to);
+ }
+ }
+ };
+
+ cmds[map["Shift-F2"] = "prevBookmark"] = function(cm) {
+ var marks = cm.state.sublimeBookmarks;
+ if (marks) while (marks.length) {
+ marks.unshift(marks.pop());
+ var found = marks[marks.length - 1].find();
+ if (!found)
+ marks.pop();
+ else
+ return cm.setSelection(found.from, found.to);
+ }
+ };
+
+ cmds[map[ctrl + "F2"] = "toggleBookmark"] = function(cm) {
+ var ranges = cm.listSelections();
+ var marks = cm.state.sublimeBookmarks || (cm.state.sublimeBookmarks = []);
+ for (var i = 0; i < ranges.length; i++) {
+ var from = ranges[i].from(), to = ranges[i].to();
+ var found = cm.findMarks(from, to);
+ for (var j = 0; j < found.length; j++) {
+ if (found[j].sublimeBookmark) {
+ found[j].clear();
+ for (var k = 0; k < marks.length; k++)
+ if (marks[k] == found[j])
+ marks.splice(k--, 1);
+ break;
+ }
+ }
+ if (j == found.length)
+ marks.push(cm.markText(from, to, {sublimeBookmark: true, clearWhenEmpty: false}));
+ }
+ };
+
+ cmds[map["Shift-" + ctrl + "F2"] = "clearBookmarks"] = function(cm) {
+ var marks = cm.state.sublimeBookmarks;
+ if (marks) for (var i = 0; i < marks.length; i++) marks[i].clear();
+ marks.length = 0;
+ };
+
+ cmds[map["Alt-F2"] = "selectBookmarks"] = function(cm) {
+ var marks = cm.state.sublimeBookmarks, ranges = [];
+ if (marks) for (var i = 0; i < marks.length; i++) {
+ var found = marks[i].find();
+ if (!found)
+ marks.splice(i--, 0);
+ else
+ ranges.push({anchor: found.from, head: found.to});
+ }
+ if (ranges.length)
+ cm.setSelections(ranges, 0);
+ };
+
+ map["Alt-Q"] = "wrapLines";
+
+ var cK = ctrl + "K ";
+
+ function modifyWordOrSelection(cm, mod) {
+ cm.operation(function() {
+ var ranges = cm.listSelections(), indices = [], replacements = [];
+ for (var i = 0; i < ranges.length; i++) {
+ var range = ranges[i];
+ if (range.empty()) { indices.push(i); replacements.push(""); }
+ else replacements.push(mod(cm.getRange(range.from(), range.to())));
+ }
+ cm.replaceSelections(replacements, "around", "case");
+ for (var i = indices.length - 1, at; i >= 0; i--) {
+ var range = ranges[indices[i]];
+ if (at && CodeMirror.cmpPos(range.head, at) > 0) continue;
+ var word = wordAt(cm, range.head);
+ at = word.from;
+ cm.replaceRange(mod(word.word), word.from, word.to);
+ }
+ });
+ }
+
+ map[cK + ctrl + "Backspace"] = "delLineLeft";
+
+ cmds[map["Backspace"] = "smartBackspace"] = function(cm) {
+ if (cm.somethingSelected()) return CodeMirror.Pass;
+
+ cm.operation(function() {
+ var cursors = cm.listSelections();
+ var indentUnit = cm.getOption("indentUnit");
+
+ for (var i = cursors.length - 1; i >= 0; i--) {
+ var cursor = cursors[i].head;
+ var toStartOfLine = cm.getRange({line: cursor.line, ch: 0}, cursor);
+ var column = CodeMirror.countColumn(toStartOfLine, null, cm.getOption("tabSize"));
+
+ // Delete by one character by default
+ var deletePos = cm.findPosH(cursor, -1, "char", false);
+
+ if (toStartOfLine && !/\S/.test(toStartOfLine) && column % indentUnit == 0) {
+ var prevIndent = new Pos(cursor.line,
+ CodeMirror.findColumn(toStartOfLine, column - indentUnit, indentUnit));
+
+ // Smart delete only if we found a valid prevIndent location
+ if (prevIndent.ch != cursor.ch) deletePos = prevIndent;
+ }
+
+ cm.replaceRange("", deletePos, cursor, "+delete");
+ }
+ });
+ };
+
+ cmds[map[cK + ctrl + "K"] = "delLineRight"] = function(cm) {
+ cm.operation(function() {
+ var ranges = cm.listSelections();
+ for (var i = ranges.length - 1; i >= 0; i--)
+ cm.replaceRange("", ranges[i].anchor, Pos(ranges[i].to().line), "+delete");
+ cm.scrollIntoView();
+ });
+ };
+
+ cmds[map[cK + ctrl + "U"] = "upcaseAtCursor"] = function(cm) {
+ modifyWordOrSelection(cm, function(str) { return str.toUpperCase(); });
+ };
+ cmds[map[cK + ctrl + "L"] = "downcaseAtCursor"] = function(cm) {
+ modifyWordOrSelection(cm, function(str) { return str.toLowerCase(); });
+ };
+
+ cmds[map[cK + ctrl + "Space"] = "setSublimeMark"] = function(cm) {
+ if (cm.state.sublimeMark) cm.state.sublimeMark.clear();
+ cm.state.sublimeMark = cm.setBookmark(cm.getCursor());
+ };
+ cmds[map[cK + ctrl + "A"] = "selectToSublimeMark"] = function(cm) {
+ var found = cm.state.sublimeMark && cm.state.sublimeMark.find();
+ if (found) cm.setSelection(cm.getCursor(), found);
+ };
+ cmds[map[cK + ctrl + "W"] = "deleteToSublimeMark"] = function(cm) {
+ var found = cm.state.sublimeMark && cm.state.sublimeMark.find();
+ if (found) {
+ var from = cm.getCursor(), to = found;
+ if (CodeMirror.cmpPos(from, to) > 0) { var tmp = to; to = from; from = tmp; }
+ cm.state.sublimeKilled = cm.getRange(from, to);
+ cm.replaceRange("", from, to);
+ }
+ };
+ cmds[map[cK + ctrl + "X"] = "swapWithSublimeMark"] = function(cm) {
+ var found = cm.state.sublimeMark && cm.state.sublimeMark.find();
+ if (found) {
+ cm.state.sublimeMark.clear();
+ cm.state.sublimeMark = cm.setBookmark(cm.getCursor());
+ cm.setCursor(found);
+ }
+ };
+ cmds[map[cK + ctrl + "Y"] = "sublimeYank"] = function(cm) {
+ if (cm.state.sublimeKilled != null)
+ cm.replaceSelection(cm.state.sublimeKilled, null, "paste");
+ };
+
+ map[cK + ctrl + "G"] = "clearBookmarks";
+ cmds[map[cK + ctrl + "C"] = "showInCenter"] = function(cm) {
+ var pos = cm.cursorCoords(null, "local");
+ cm.scrollTo(null, (pos.top + pos.bottom) / 2 - cm.getScrollInfo().clientHeight / 2);
+ };
+
+ var selectLinesCombo = mac ? "Ctrl-Shift-" : "Ctrl-Alt-";
+ cmds[map[selectLinesCombo + "Up"] = "selectLinesUpward"] = function(cm) {
+ cm.operation(function() {
+ var ranges = cm.listSelections();
+ for (var i = 0; i < ranges.length; i++) {
+ var range = ranges[i];
+ if (range.head.line > cm.firstLine())
+ cm.addSelection(Pos(range.head.line - 1, range.head.ch));
+ }
+ });
+ };
+ cmds[map[selectLinesCombo + "Down"] = "selectLinesDownward"] = function(cm) {
+ cm.operation(function() {
+ var ranges = cm.listSelections();
+ for (var i = 0; i < ranges.length; i++) {
+ var range = ranges[i];
+ if (range.head.line < cm.lastLine())
+ cm.addSelection(Pos(range.head.line + 1, range.head.ch));
+ }
+ });
+ };
+
+ function getTarget(cm) {
+ var from = cm.getCursor("from"), to = cm.getCursor("to");
+ if (CodeMirror.cmpPos(from, to) == 0) {
+ var word = wordAt(cm, from);
+ if (!word.word) return;
+ from = word.from;
+ to = word.to;
+ }
+ return {from: from, to: to, query: cm.getRange(from, to), word: word};
+ }
+
+ function findAndGoTo(cm, forward) {
+ var target = getTarget(cm);
+ if (!target) return;
+ var query = target.query;
+ var cur = cm.getSearchCursor(query, forward ? target.to : target.from);
+
+ if (forward ? cur.findNext() : cur.findPrevious()) {
+ cm.setSelection(cur.from(), cur.to());
+ } else {
+ cur = cm.getSearchCursor(query, forward ? Pos(cm.firstLine(), 0)
+ : cm.clipPos(Pos(cm.lastLine())));
+ if (forward ? cur.findNext() : cur.findPrevious())
+ cm.setSelection(cur.from(), cur.to());
+ else if (target.word)
+ cm.setSelection(target.from, target.to);
+ }
+ };
+ cmds[map[ctrl + "F3"] = "findUnder"] = function(cm) { findAndGoTo(cm, true); };
+ cmds[map["Shift-" + ctrl + "F3"] = "findUnderPrevious"] = function(cm) { findAndGoTo(cm,false); };
+ cmds[map["Alt-F3"] = "findAllUnder"] = function(cm) {
+ var target = getTarget(cm);
+ if (!target) return;
+ var cur = cm.getSearchCursor(target.query);
+ var matches = [];
+ var primaryIndex = -1;
+ while (cur.findNext()) {
+ matches.push({anchor: cur.from(), head: cur.to()});
+ if (cur.from().line <= target.from.line && cur.from().ch <= target.from.ch)
+ primaryIndex++;
+ }
+ cm.setSelections(matches, primaryIndex);
+ };
+
+ map["Shift-" + ctrl + "["] = "fold";
+ map["Shift-" + ctrl + "]"] = "unfold";
+ map[cK + ctrl + "0"] = map[cK + ctrl + "j"] = "unfoldAll";
+
+ map[ctrl + "I"] = "findIncremental";
+ map["Shift-" + ctrl + "I"] = "findIncrementalReverse";
+ map[ctrl + "H"] = "replace";
+ map["F3"] = "findNext";
+ map["Shift-F3"] = "findPrev";
+
+ CodeMirror.normalizeKeyMap(map);
+});
diff --git a/devtools/client/sourceeditor/codemirror/keymap/vim.js b/devtools/client/sourceeditor/codemirror/keymap/vim.js
new file mode 100644
index 000000000..4278ee979
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/keymap/vim.js
@@ -0,0 +1,5065 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+/**
+ * Supported keybindings:
+ * Too many to list. Refer to defaultKeyMap below.
+ *
+ * Supported Ex commands:
+ * Refer to defaultExCommandMap below.
+ *
+ * Registers: unnamed, -, a-z, A-Z, 0-9
+ * (Does not respect the special case for number registers when delete
+ * operator is made with these commands: %, (, ), , /, ?, n, N, {, } )
+ * TODO: Implement the remaining registers.
+ *
+ * Marks: a-z, A-Z, and 0-9
+ * TODO: Implement the remaining special marks. They have more complex
+ * behavior.
+ *
+ * Events:
+ * 'vim-mode-change' - raised on the editor anytime the current mode changes,
+ * Event object: {mode: "visual", subMode: "linewise"}
+ *
+ * Code structure:
+ * 1. Default keymap
+ * 2. Variable declarations and short basic helpers
+ * 3. Instance (External API) implementation
+ * 4. Internal state tracking objects (input state, counter) implementation
+ * and instantiation
+ * 5. Key handler (the main command dispatcher) implementation
+ * 6. Motion, operator, and action implementations
+ * 7. Helper functions for the key handler, motions, operators, and actions
+ * 8. Set up Vim to work as a keymap for CodeMirror.
+ * 9. Ex command implementations.
+ */
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../lib/codemirror"), require("../addon/search/searchcursor"), require("../addon/dialog/dialog"), require("../addon/edit/matchbrackets.js"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../lib/codemirror", "../addon/search/searchcursor", "../addon/dialog/dialog", "../addon/edit/matchbrackets"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function(CodeMirror) {
+ 'use strict';
+
+ var defaultKeymap = [
+ // Key to key mapping. This goes first to make it possible to override
+ // existing mappings.
+ { keys: '<Left>', type: 'keyToKey', toKeys: 'h' },
+ { keys: '<Right>', type: 'keyToKey', toKeys: 'l' },
+ { keys: '<Up>', type: 'keyToKey', toKeys: 'k' },
+ { keys: '<Down>', type: 'keyToKey', toKeys: 'j' },
+ { keys: '<Space>', type: 'keyToKey', toKeys: 'l' },
+ { keys: '<BS>', type: 'keyToKey', toKeys: 'h', context: 'normal'},
+ { keys: '<C-Space>', type: 'keyToKey', toKeys: 'W' },
+ { keys: '<C-BS>', type: 'keyToKey', toKeys: 'B', context: 'normal' },
+ { keys: '<S-Space>', type: 'keyToKey', toKeys: 'w' },
+ { keys: '<S-BS>', type: 'keyToKey', toKeys: 'b', context: 'normal' },
+ { keys: '<C-n>', type: 'keyToKey', toKeys: 'j' },
+ { keys: '<C-p>', type: 'keyToKey', toKeys: 'k' },
+ { keys: '<C-[>', type: 'keyToKey', toKeys: '<Esc>' },
+ { keys: '<C-c>', type: 'keyToKey', toKeys: '<Esc>' },
+ { keys: '<C-[>', type: 'keyToKey', toKeys: '<Esc>', context: 'insert' },
+ { keys: '<C-c>', type: 'keyToKey', toKeys: '<Esc>', context: 'insert' },
+ { keys: 's', type: 'keyToKey', toKeys: 'cl', context: 'normal' },
+ { keys: 's', type: 'keyToKey', toKeys: 'c', context: 'visual'},
+ { keys: 'S', type: 'keyToKey', toKeys: 'cc', context: 'normal' },
+ { keys: 'S', type: 'keyToKey', toKeys: 'VdO', context: 'visual' },
+ { keys: '<Home>', type: 'keyToKey', toKeys: '0' },
+ { keys: '<End>', type: 'keyToKey', toKeys: '$' },
+ { keys: '<PageUp>', type: 'keyToKey', toKeys: '<C-b>' },
+ { keys: '<PageDown>', type: 'keyToKey', toKeys: '<C-f>' },
+ { keys: '<CR>', type: 'keyToKey', toKeys: 'j^', context: 'normal' },
+ // Motions
+ { keys: 'H', type: 'motion', motion: 'moveToTopLine', motionArgs: { linewise: true, toJumplist: true }},
+ { keys: 'M', type: 'motion', motion: 'moveToMiddleLine', motionArgs: { linewise: true, toJumplist: true }},
+ { keys: 'L', type: 'motion', motion: 'moveToBottomLine', motionArgs: { linewise: true, toJumplist: true }},
+ { keys: 'h', type: 'motion', motion: 'moveByCharacters', motionArgs: { forward: false }},
+ { keys: 'l', type: 'motion', motion: 'moveByCharacters', motionArgs: { forward: true }},
+ { keys: 'j', type: 'motion', motion: 'moveByLines', motionArgs: { forward: true, linewise: true }},
+ { keys: 'k', type: 'motion', motion: 'moveByLines', motionArgs: { forward: false, linewise: true }},
+ { keys: 'gj', type: 'motion', motion: 'moveByDisplayLines', motionArgs: { forward: true }},
+ { keys: 'gk', type: 'motion', motion: 'moveByDisplayLines', motionArgs: { forward: false }},
+ { keys: 'w', type: 'motion', motion: 'moveByWords', motionArgs: { forward: true, wordEnd: false }},
+ { keys: 'W', type: 'motion', motion: 'moveByWords', motionArgs: { forward: true, wordEnd: false, bigWord: true }},
+ { keys: 'e', type: 'motion', motion: 'moveByWords', motionArgs: { forward: true, wordEnd: true, inclusive: true }},
+ { keys: 'E', type: 'motion', motion: 'moveByWords', motionArgs: { forward: true, wordEnd: true, bigWord: true, inclusive: true }},
+ { keys: 'b', type: 'motion', motion: 'moveByWords', motionArgs: { forward: false, wordEnd: false }},
+ { keys: 'B', type: 'motion', motion: 'moveByWords', motionArgs: { forward: false, wordEnd: false, bigWord: true }},
+ { keys: 'ge', type: 'motion', motion: 'moveByWords', motionArgs: { forward: false, wordEnd: true, inclusive: true }},
+ { keys: 'gE', type: 'motion', motion: 'moveByWords', motionArgs: { forward: false, wordEnd: true, bigWord: true, inclusive: true }},
+ { keys: '{', type: 'motion', motion: 'moveByParagraph', motionArgs: { forward: false, toJumplist: true }},
+ { keys: '}', type: 'motion', motion: 'moveByParagraph', motionArgs: { forward: true, toJumplist: true }},
+ { keys: '<C-f>', type: 'motion', motion: 'moveByPage', motionArgs: { forward: true }},
+ { keys: '<C-b>', type: 'motion', motion: 'moveByPage', motionArgs: { forward: false }},
+ { keys: '<C-d>', type: 'motion', motion: 'moveByScroll', motionArgs: { forward: true, explicitRepeat: true }},
+ { keys: '<C-u>', type: 'motion', motion: 'moveByScroll', motionArgs: { forward: false, explicitRepeat: true }},
+ { keys: 'gg', type: 'motion', motion: 'moveToLineOrEdgeOfDocument', motionArgs: { forward: false, explicitRepeat: true, linewise: true, toJumplist: true }},
+ { keys: 'G', type: 'motion', motion: 'moveToLineOrEdgeOfDocument', motionArgs: { forward: true, explicitRepeat: true, linewise: true, toJumplist: true }},
+ { keys: '0', type: 'motion', motion: 'moveToStartOfLine' },
+ { keys: '^', type: 'motion', motion: 'moveToFirstNonWhiteSpaceCharacter' },
+ { keys: '+', type: 'motion', motion: 'moveByLines', motionArgs: { forward: true, toFirstChar:true }},
+ { keys: '-', type: 'motion', motion: 'moveByLines', motionArgs: { forward: false, toFirstChar:true }},
+ { keys: '_', type: 'motion', motion: 'moveByLines', motionArgs: { forward: true, toFirstChar:true, repeatOffset:-1 }},
+ { keys: '$', type: 'motion', motion: 'moveToEol', motionArgs: { inclusive: true }},
+ { keys: '%', type: 'motion', motion: 'moveToMatchedSymbol', motionArgs: { inclusive: true, toJumplist: true }},
+ { keys: 'f<character>', type: 'motion', motion: 'moveToCharacter', motionArgs: { forward: true , inclusive: true }},
+ { keys: 'F<character>', type: 'motion', motion: 'moveToCharacter', motionArgs: { forward: false }},
+ { keys: 't<character>', type: 'motion', motion: 'moveTillCharacter', motionArgs: { forward: true, inclusive: true }},
+ { keys: 'T<character>', type: 'motion', motion: 'moveTillCharacter', motionArgs: { forward: false }},
+ { keys: ';', type: 'motion', motion: 'repeatLastCharacterSearch', motionArgs: { forward: true }},
+ { keys: ',', type: 'motion', motion: 'repeatLastCharacterSearch', motionArgs: { forward: false }},
+ { keys: '\'<character>', type: 'motion', motion: 'goToMark', motionArgs: {toJumplist: true, linewise: true}},
+ { keys: '`<character>', type: 'motion', motion: 'goToMark', motionArgs: {toJumplist: true}},
+ { keys: ']`', type: 'motion', motion: 'jumpToMark', motionArgs: { forward: true } },
+ { keys: '[`', type: 'motion', motion: 'jumpToMark', motionArgs: { forward: false } },
+ { keys: ']\'', type: 'motion', motion: 'jumpToMark', motionArgs: { forward: true, linewise: true } },
+ { keys: '[\'', type: 'motion', motion: 'jumpToMark', motionArgs: { forward: false, linewise: true } },
+ // the next two aren't motions but must come before more general motion declarations
+ { keys: ']p', type: 'action', action: 'paste', isEdit: true, actionArgs: { after: true, isEdit: true, matchIndent: true}},
+ { keys: '[p', type: 'action', action: 'paste', isEdit: true, actionArgs: { after: false, isEdit: true, matchIndent: true}},
+ { keys: ']<character>', type: 'motion', motion: 'moveToSymbol', motionArgs: { forward: true, toJumplist: true}},
+ { keys: '[<character>', type: 'motion', motion: 'moveToSymbol', motionArgs: { forward: false, toJumplist: true}},
+ { keys: '|', type: 'motion', motion: 'moveToColumn'},
+ { keys: 'o', type: 'motion', motion: 'moveToOtherHighlightedEnd', context:'visual'},
+ { keys: 'O', type: 'motion', motion: 'moveToOtherHighlightedEnd', motionArgs: {sameLine: true}, context:'visual'},
+ // Operators
+ { keys: 'd', type: 'operator', operator: 'delete' },
+ { keys: 'y', type: 'operator', operator: 'yank' },
+ { keys: 'c', type: 'operator', operator: 'change' },
+ { keys: '>', type: 'operator', operator: 'indent', operatorArgs: { indentRight: true }},
+ { keys: '<', type: 'operator', operator: 'indent', operatorArgs: { indentRight: false }},
+ { keys: 'g~', type: 'operator', operator: 'changeCase' },
+ { keys: 'gu', type: 'operator', operator: 'changeCase', operatorArgs: {toLower: true}, isEdit: true },
+ { keys: 'gU', type: 'operator', operator: 'changeCase', operatorArgs: {toLower: false}, isEdit: true },
+ { keys: 'n', type: 'motion', motion: 'findNext', motionArgs: { forward: true, toJumplist: true }},
+ { keys: 'N', type: 'motion', motion: 'findNext', motionArgs: { forward: false, toJumplist: true }},
+ // Operator-Motion dual commands
+ { keys: 'x', type: 'operatorMotion', operator: 'delete', motion: 'moveByCharacters', motionArgs: { forward: true }, operatorMotionArgs: { visualLine: false }},
+ { keys: 'X', type: 'operatorMotion', operator: 'delete', motion: 'moveByCharacters', motionArgs: { forward: false }, operatorMotionArgs: { visualLine: true }},
+ { keys: 'D', type: 'operatorMotion', operator: 'delete', motion: 'moveToEol', motionArgs: { inclusive: true }, context: 'normal'},
+ { keys: 'D', type: 'operator', operator: 'delete', operatorArgs: { linewise: true }, context: 'visual'},
+ { keys: 'Y', type: 'operatorMotion', operator: 'yank', motion: 'moveToEol', motionArgs: { inclusive: true }, context: 'normal'},
+ { keys: 'Y', type: 'operator', operator: 'yank', operatorArgs: { linewise: true }, context: 'visual'},
+ { keys: 'C', type: 'operatorMotion', operator: 'change', motion: 'moveToEol', motionArgs: { inclusive: true }, context: 'normal'},
+ { keys: 'C', type: 'operator', operator: 'change', operatorArgs: { linewise: true }, context: 'visual'},
+ { keys: '~', type: 'operatorMotion', operator: 'changeCase', motion: 'moveByCharacters', motionArgs: { forward: true }, operatorArgs: { shouldMoveCursor: true }, context: 'normal'},
+ { keys: '~', type: 'operator', operator: 'changeCase', context: 'visual'},
+ { keys: '<C-w>', type: 'operatorMotion', operator: 'delete', motion: 'moveByWords', motionArgs: { forward: false, wordEnd: false }, context: 'insert' },
+ // Actions
+ { keys: '<C-i>', type: 'action', action: 'jumpListWalk', actionArgs: { forward: true }},
+ { keys: '<C-o>', type: 'action', action: 'jumpListWalk', actionArgs: { forward: false }},
+ { keys: '<C-e>', type: 'action', action: 'scroll', actionArgs: { forward: true, linewise: true }},
+ { keys: '<C-y>', type: 'action', action: 'scroll', actionArgs: { forward: false, linewise: true }},
+ { keys: 'a', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'charAfter' }, context: 'normal' },
+ { keys: 'A', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'eol' }, context: 'normal' },
+ { keys: 'A', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'endOfSelectedArea' }, context: 'visual' },
+ { keys: 'i', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'inplace' }, context: 'normal' },
+ { keys: 'I', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'firstNonBlank'}, context: 'normal' },
+ { keys: 'I', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'startOfSelectedArea' }, context: 'visual' },
+ { keys: 'o', type: 'action', action: 'newLineAndEnterInsertMode', isEdit: true, interlaceInsertRepeat: true, actionArgs: { after: true }, context: 'normal' },
+ { keys: 'O', type: 'action', action: 'newLineAndEnterInsertMode', isEdit: true, interlaceInsertRepeat: true, actionArgs: { after: false }, context: 'normal' },
+ { keys: 'v', type: 'action', action: 'toggleVisualMode' },
+ { keys: 'V', type: 'action', action: 'toggleVisualMode', actionArgs: { linewise: true }},
+ { keys: '<C-v>', type: 'action', action: 'toggleVisualMode', actionArgs: { blockwise: true }},
+ { keys: '<C-q>', type: 'action', action: 'toggleVisualMode', actionArgs: { blockwise: true }},
+ { keys: 'gv', type: 'action', action: 'reselectLastSelection' },
+ { keys: 'J', type: 'action', action: 'joinLines', isEdit: true },
+ { keys: 'p', type: 'action', action: 'paste', isEdit: true, actionArgs: { after: true, isEdit: true }},
+ { keys: 'P', type: 'action', action: 'paste', isEdit: true, actionArgs: { after: false, isEdit: true }},
+ { keys: 'r<character>', type: 'action', action: 'replace', isEdit: true },
+ { keys: '@<character>', type: 'action', action: 'replayMacro' },
+ { keys: 'q<character>', type: 'action', action: 'enterMacroRecordMode' },
+ // Handle Replace-mode as a special case of insert mode.
+ { keys: 'R', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { replace: true }},
+ { keys: 'u', type: 'action', action: 'undo', context: 'normal' },
+ { keys: 'u', type: 'operator', operator: 'changeCase', operatorArgs: {toLower: true}, context: 'visual', isEdit: true },
+ { keys: 'U', type: 'operator', operator: 'changeCase', operatorArgs: {toLower: false}, context: 'visual', isEdit: true },
+ { keys: '<C-r>', type: 'action', action: 'redo' },
+ { keys: 'm<character>', type: 'action', action: 'setMark' },
+ { keys: '"<character>', type: 'action', action: 'setRegister' },
+ { keys: 'zz', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'center' }},
+ { keys: 'z.', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'center' }, motion: 'moveToFirstNonWhiteSpaceCharacter' },
+ { keys: 'zt', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'top' }},
+ { keys: 'z<CR>', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'top' }, motion: 'moveToFirstNonWhiteSpaceCharacter' },
+ { keys: 'z-', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'bottom' }},
+ { keys: 'zb', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'bottom' }, motion: 'moveToFirstNonWhiteSpaceCharacter' },
+ { keys: '.', type: 'action', action: 'repeatLastEdit' },
+ { keys: '<C-a>', type: 'action', action: 'incrementNumberToken', isEdit: true, actionArgs: {increase: true, backtrack: false}},
+ { keys: '<C-x>', type: 'action', action: 'incrementNumberToken', isEdit: true, actionArgs: {increase: false, backtrack: false}},
+ // Text object motions
+ { keys: 'a<character>', type: 'motion', motion: 'textObjectManipulation' },
+ { keys: 'i<character>', type: 'motion', motion: 'textObjectManipulation', motionArgs: { textObjectInner: true }},
+ // Search
+ { keys: '/', type: 'search', searchArgs: { forward: true, querySrc: 'prompt', toJumplist: true }},
+ { keys: '?', type: 'search', searchArgs: { forward: false, querySrc: 'prompt', toJumplist: true }},
+ { keys: '*', type: 'search', searchArgs: { forward: true, querySrc: 'wordUnderCursor', wholeWordOnly: true, toJumplist: true }},
+ { keys: '#', type: 'search', searchArgs: { forward: false, querySrc: 'wordUnderCursor', wholeWordOnly: true, toJumplist: true }},
+ { keys: 'g*', type: 'search', searchArgs: { forward: true, querySrc: 'wordUnderCursor', toJumplist: true }},
+ { keys: 'g#', type: 'search', searchArgs: { forward: false, querySrc: 'wordUnderCursor', toJumplist: true }},
+ // Ex command
+ { keys: ':', type: 'ex' }
+ ];
+
+ /**
+ * Ex commands
+ * Care must be taken when adding to the default Ex command map. For any
+ * pair of commands that have a shared prefix, at least one of their
+ * shortNames must not match the prefix of the other command.
+ */
+ var defaultExCommandMap = [
+ { name: 'colorscheme', shortName: 'colo' },
+ { name: 'map' },
+ { name: 'imap', shortName: 'im' },
+ { name: 'nmap', shortName: 'nm' },
+ { name: 'vmap', shortName: 'vm' },
+ { name: 'unmap' },
+ { name: 'write', shortName: 'w' },
+ { name: 'undo', shortName: 'u' },
+ { name: 'redo', shortName: 'red' },
+ { name: 'set', shortName: 'se' },
+ { name: 'set', shortName: 'se' },
+ { name: 'setlocal', shortName: 'setl' },
+ { name: 'setglobal', shortName: 'setg' },
+ { name: 'sort', shortName: 'sor' },
+ { name: 'substitute', shortName: 's', possiblyAsync: true },
+ { name: 'nohlsearch', shortName: 'noh' },
+ { name: 'yank', shortName: 'y' },
+ { name: 'delmarks', shortName: 'delm' },
+ { name: 'registers', shortName: 'reg', excludeFromCommandHistory: true },
+ { name: 'global', shortName: 'g' }
+ ];
+
+ var Pos = CodeMirror.Pos;
+
+ var Vim = function() {
+ function enterVimMode(cm) {
+ cm.setOption('disableInput', true);
+ cm.setOption('showCursorWhenSelecting', false);
+ CodeMirror.signal(cm, "vim-mode-change", {mode: "normal"});
+ cm.on('cursorActivity', onCursorActivity);
+ maybeInitVimState(cm);
+ CodeMirror.on(cm.getInputField(), 'paste', getOnPasteFn(cm));
+ }
+
+ function leaveVimMode(cm) {
+ cm.setOption('disableInput', false);
+ cm.off('cursorActivity', onCursorActivity);
+ CodeMirror.off(cm.getInputField(), 'paste', getOnPasteFn(cm));
+ cm.state.vim = null;
+ }
+
+ function detachVimMap(cm, next) {
+ if (this == CodeMirror.keyMap.vim)
+ CodeMirror.rmClass(cm.getWrapperElement(), "cm-fat-cursor");
+
+ if (!next || next.attach != attachVimMap)
+ leaveVimMode(cm, false);
+ }
+ function attachVimMap(cm, prev) {
+ if (this == CodeMirror.keyMap.vim)
+ CodeMirror.addClass(cm.getWrapperElement(), "cm-fat-cursor");
+
+ if (!prev || prev.attach != attachVimMap)
+ enterVimMode(cm);
+ }
+
+ // Deprecated, simply setting the keymap works again.
+ CodeMirror.defineOption('vimMode', false, function(cm, val, prev) {
+ if (val && cm.getOption("keyMap") != "vim")
+ cm.setOption("keyMap", "vim");
+ else if (!val && prev != CodeMirror.Init && /^vim/.test(cm.getOption("keyMap")))
+ cm.setOption("keyMap", "default");
+ });
+
+ function cmKey(key, cm) {
+ if (!cm) { return undefined; }
+ var vimKey = cmKeyToVimKey(key);
+ if (!vimKey) {
+ return false;
+ }
+ var cmd = CodeMirror.Vim.findKey(cm, vimKey);
+ if (typeof cmd == 'function') {
+ CodeMirror.signal(cm, 'vim-keypress', vimKey);
+ }
+ return cmd;
+ }
+
+ var modifiers = {'Shift': 'S', 'Ctrl': 'C', 'Alt': 'A', 'Cmd': 'D', 'Mod': 'A'};
+ var specialKeys = {Enter:'CR',Backspace:'BS',Delete:'Del'};
+ function cmKeyToVimKey(key) {
+ if (key.charAt(0) == '\'') {
+ // Keypress character binding of format "'a'"
+ return key.charAt(1);
+ }
+ var pieces = key.split(/-(?!$)/);
+ var lastPiece = pieces[pieces.length - 1];
+ if (pieces.length == 1 && pieces[0].length == 1) {
+ // No-modifier bindings use literal character bindings above. Skip.
+ return false;
+ } else if (pieces.length == 2 && pieces[0] == 'Shift' && lastPiece.length == 1) {
+ // Ignore Shift+char bindings as they should be handled by literal character.
+ return false;
+ }
+ var hasCharacter = false;
+ for (var i = 0; i < pieces.length; i++) {
+ var piece = pieces[i];
+ if (piece in modifiers) { pieces[i] = modifiers[piece]; }
+ else { hasCharacter = true; }
+ if (piece in specialKeys) { pieces[i] = specialKeys[piece]; }
+ }
+ if (!hasCharacter) {
+ // Vim does not support modifier only keys.
+ return false;
+ }
+ // TODO: Current bindings expect the character to be lower case, but
+ // it looks like vim key notation uses upper case.
+ if (isUpperCase(lastPiece)) {
+ pieces[pieces.length - 1] = lastPiece.toLowerCase();
+ }
+ return '<' + pieces.join('-') + '>';
+ }
+
+ function getOnPasteFn(cm) {
+ var vim = cm.state.vim;
+ if (!vim.onPasteFn) {
+ vim.onPasteFn = function() {
+ if (!vim.insertMode) {
+ cm.setCursor(offsetCursor(cm.getCursor(), 0, 1));
+ actions.enterInsertMode(cm, {}, vim);
+ }
+ };
+ }
+ return vim.onPasteFn;
+ }
+
+ var numberRegex = /[\d]/;
+ var wordCharTest = [CodeMirror.isWordChar, function(ch) {
+ return ch && !CodeMirror.isWordChar(ch) && !/\s/.test(ch);
+ }], bigWordCharTest = [function(ch) {
+ return /\S/.test(ch);
+ }];
+ function makeKeyRange(start, size) {
+ var keys = [];
+ for (var i = start; i < start + size; i++) {
+ keys.push(String.fromCharCode(i));
+ }
+ return keys;
+ }
+ var upperCaseAlphabet = makeKeyRange(65, 26);
+ var lowerCaseAlphabet = makeKeyRange(97, 26);
+ var numbers = makeKeyRange(48, 10);
+ var validMarks = [].concat(upperCaseAlphabet, lowerCaseAlphabet, numbers, ['<', '>']);
+ var validRegisters = [].concat(upperCaseAlphabet, lowerCaseAlphabet, numbers, ['-', '"', '.', ':', '/']);
+
+ function isLine(cm, line) {
+ return line >= cm.firstLine() && line <= cm.lastLine();
+ }
+ function isLowerCase(k) {
+ return (/^[a-z]$/).test(k);
+ }
+ function isMatchableSymbol(k) {
+ return '()[]{}'.indexOf(k) != -1;
+ }
+ function isNumber(k) {
+ return numberRegex.test(k);
+ }
+ function isUpperCase(k) {
+ return (/^[A-Z]$/).test(k);
+ }
+ function isWhiteSpaceString(k) {
+ return (/^\s*$/).test(k);
+ }
+ function inArray(val, arr) {
+ for (var i = 0; i < arr.length; i++) {
+ if (arr[i] == val) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ var options = {};
+ function defineOption(name, defaultValue, type, aliases, callback) {
+ if (defaultValue === undefined && !callback) {
+ throw Error('defaultValue is required unless callback is provided');
+ }
+ if (!type) { type = 'string'; }
+ options[name] = {
+ type: type,
+ defaultValue: defaultValue,
+ callback: callback
+ };
+ if (aliases) {
+ for (var i = 0; i < aliases.length; i++) {
+ options[aliases[i]] = options[name];
+ }
+ }
+ if (defaultValue) {
+ setOption(name, defaultValue);
+ }
+ }
+
+ function setOption(name, value, cm, cfg) {
+ var option = options[name];
+ cfg = cfg || {};
+ var scope = cfg.scope;
+ if (!option) {
+ throw Error('Unknown option: ' + name);
+ }
+ if (option.type == 'boolean') {
+ if (value && value !== true) {
+ throw Error('Invalid argument: ' + name + '=' + value);
+ } else if (value !== false) {
+ // Boolean options are set to true if value is not defined.
+ value = true;
+ }
+ }
+ if (option.callback) {
+ if (scope !== 'local') {
+ option.callback(value, undefined);
+ }
+ if (scope !== 'global' && cm) {
+ option.callback(value, cm);
+ }
+ } else {
+ if (scope !== 'local') {
+ option.value = option.type == 'boolean' ? !!value : value;
+ }
+ if (scope !== 'global' && cm) {
+ cm.state.vim.options[name] = {value: value};
+ }
+ }
+ }
+
+ function getOption(name, cm, cfg) {
+ var option = options[name];
+ cfg = cfg || {};
+ var scope = cfg.scope;
+ if (!option) {
+ throw Error('Unknown option: ' + name);
+ }
+ if (option.callback) {
+ var local = cm && option.callback(undefined, cm);
+ if (scope !== 'global' && local !== undefined) {
+ return local;
+ }
+ if (scope !== 'local') {
+ return option.callback();
+ }
+ return;
+ } else {
+ var local = (scope !== 'global') && (cm && cm.state.vim.options[name]);
+ return (local || (scope !== 'local') && option || {}).value;
+ }
+ }
+
+ defineOption('filetype', undefined, 'string', ['ft'], function(name, cm) {
+ // Option is local. Do nothing for global.
+ if (cm === undefined) {
+ return;
+ }
+ // The 'filetype' option proxies to the CodeMirror 'mode' option.
+ if (name === undefined) {
+ var mode = cm.getOption('mode');
+ return mode == 'null' ? '' : mode;
+ } else {
+ var mode = name == '' ? 'null' : name;
+ cm.setOption('mode', mode);
+ }
+ });
+
+ var createCircularJumpList = function() {
+ var size = 100;
+ var pointer = -1;
+ var head = 0;
+ var tail = 0;
+ var buffer = new Array(size);
+ function add(cm, oldCur, newCur) {
+ var current = pointer % size;
+ var curMark = buffer[current];
+ function useNextSlot(cursor) {
+ var next = ++pointer % size;
+ var trashMark = buffer[next];
+ if (trashMark) {
+ trashMark.clear();
+ }
+ buffer[next] = cm.setBookmark(cursor);
+ }
+ if (curMark) {
+ var markPos = curMark.find();
+ // avoid recording redundant cursor position
+ if (markPos && !cursorEqual(markPos, oldCur)) {
+ useNextSlot(oldCur);
+ }
+ } else {
+ useNextSlot(oldCur);
+ }
+ useNextSlot(newCur);
+ head = pointer;
+ tail = pointer - size + 1;
+ if (tail < 0) {
+ tail = 0;
+ }
+ }
+ function move(cm, offset) {
+ pointer += offset;
+ if (pointer > head) {
+ pointer = head;
+ } else if (pointer < tail) {
+ pointer = tail;
+ }
+ var mark = buffer[(size + pointer) % size];
+ // skip marks that are temporarily removed from text buffer
+ if (mark && !mark.find()) {
+ var inc = offset > 0 ? 1 : -1;
+ var newCur;
+ var oldCur = cm.getCursor();
+ do {
+ pointer += inc;
+ mark = buffer[(size + pointer) % size];
+ // skip marks that are the same as current position
+ if (mark &&
+ (newCur = mark.find()) &&
+ !cursorEqual(oldCur, newCur)) {
+ break;
+ }
+ } while (pointer < head && pointer > tail);
+ }
+ return mark;
+ }
+ return {
+ cachedCursor: undefined, //used for # and * jumps
+ add: add,
+ move: move
+ };
+ };
+
+ // Returns an object to track the changes associated insert mode. It
+ // clones the object that is passed in, or creates an empty object one if
+ // none is provided.
+ var createInsertModeChanges = function(c) {
+ if (c) {
+ // Copy construction
+ return {
+ changes: c.changes,
+ expectCursorActivityForChange: c.expectCursorActivityForChange
+ };
+ }
+ return {
+ // Change list
+ changes: [],
+ // Set to true on change, false on cursorActivity.
+ expectCursorActivityForChange: false
+ };
+ };
+
+ function MacroModeState() {
+ this.latestRegister = undefined;
+ this.isPlaying = false;
+ this.isRecording = false;
+ this.replaySearchQueries = [];
+ this.onRecordingDone = undefined;
+ this.lastInsertModeChanges = createInsertModeChanges();
+ }
+ MacroModeState.prototype = {
+ exitMacroRecordMode: function() {
+ var macroModeState = vimGlobalState.macroModeState;
+ if (macroModeState.onRecordingDone) {
+ macroModeState.onRecordingDone(); // close dialog
+ }
+ macroModeState.onRecordingDone = undefined;
+ macroModeState.isRecording = false;
+ },
+ enterMacroRecordMode: function(cm, registerName) {
+ var register =
+ vimGlobalState.registerController.getRegister(registerName);
+ if (register) {
+ register.clear();
+ this.latestRegister = registerName;
+ if (cm.openDialog) {
+ this.onRecordingDone = cm.openDialog(
+ '(recording)['+registerName+']', null, {bottom:true});
+ }
+ this.isRecording = true;
+ }
+ }
+ };
+
+ function maybeInitVimState(cm) {
+ if (!cm.state.vim) {
+ // Store instance state in the CodeMirror object.
+ cm.state.vim = {
+ inputState: new InputState(),
+ // Vim's input state that triggered the last edit, used to repeat
+ // motions and operators with '.'.
+ lastEditInputState: undefined,
+ // Vim's action command before the last edit, used to repeat actions
+ // with '.' and insert mode repeat.
+ lastEditActionCommand: undefined,
+ // When using jk for navigation, if you move from a longer line to a
+ // shorter line, the cursor may clip to the end of the shorter line.
+ // If j is pressed again and cursor goes to the next line, the
+ // cursor should go back to its horizontal position on the longer
+ // line if it can. This is to keep track of the horizontal position.
+ lastHPos: -1,
+ // Doing the same with screen-position for gj/gk
+ lastHSPos: -1,
+ // The last motion command run. Cleared if a non-motion command gets
+ // executed in between.
+ lastMotion: null,
+ marks: {},
+ // Mark for rendering fake cursor for visual mode.
+ fakeCursor: null,
+ insertMode: false,
+ // Repeat count for changes made in insert mode, triggered by key
+ // sequences like 3,i. Only exists when insertMode is true.
+ insertModeRepeat: undefined,
+ visualMode: false,
+ // If we are in visual line mode. No effect if visualMode is false.
+ visualLine: false,
+ visualBlock: false,
+ lastSelection: null,
+ lastPastedText: null,
+ sel: {},
+ // Buffer-local/window-local values of vim options.
+ options: {}
+ };
+ }
+ return cm.state.vim;
+ }
+ var vimGlobalState;
+ function resetVimGlobalState() {
+ vimGlobalState = {
+ // The current search query.
+ searchQuery: null,
+ // Whether we are searching backwards.
+ searchIsReversed: false,
+ // Replace part of the last substituted pattern
+ lastSubstituteReplacePart: undefined,
+ jumpList: createCircularJumpList(),
+ macroModeState: new MacroModeState,
+ // Recording latest f, t, F or T motion command.
+ lastCharacterSearch: {increment:0, forward:true, selectedCharacter:''},
+ registerController: new RegisterController({}),
+ // search history buffer
+ searchHistoryController: new HistoryController({}),
+ // ex Command history buffer
+ exCommandHistoryController : new HistoryController({})
+ };
+ for (var optionName in options) {
+ var option = options[optionName];
+ option.value = option.defaultValue;
+ }
+ }
+
+ var lastInsertModeKeyTimer;
+ var vimApi= {
+ buildKeyMap: function() {
+ // TODO: Convert keymap into dictionary format for fast lookup.
+ },
+ // Testing hook, though it might be useful to expose the register
+ // controller anyways.
+ getRegisterController: function() {
+ return vimGlobalState.registerController;
+ },
+ // Testing hook.
+ resetVimGlobalState_: resetVimGlobalState,
+
+ // Testing hook.
+ getVimGlobalState_: function() {
+ return vimGlobalState;
+ },
+
+ // Testing hook.
+ maybeInitVimState_: maybeInitVimState,
+
+ suppressErrorLogging: false,
+
+ InsertModeKey: InsertModeKey,
+ map: function(lhs, rhs, ctx) {
+ // Add user defined key bindings.
+ exCommandDispatcher.map(lhs, rhs, ctx);
+ },
+ unmap: function(lhs, ctx) {
+ exCommandDispatcher.unmap(lhs, ctx);
+ },
+ // TODO: Expose setOption and getOption as instance methods. Need to decide how to namespace
+ // them, or somehow make them work with the existing CodeMirror setOption/getOption API.
+ setOption: setOption,
+ getOption: getOption,
+ defineOption: defineOption,
+ defineEx: function(name, prefix, func){
+ if (!prefix) {
+ prefix = name;
+ } else if (name.indexOf(prefix) !== 0) {
+ throw new Error('(Vim.defineEx) "'+prefix+'" is not a prefix of "'+name+'", command not registered');
+ }
+ exCommands[name]=func;
+ exCommandDispatcher.commandMap_[prefix]={name:name, shortName:prefix, type:'api'};
+ },
+ handleKey: function (cm, key, origin) {
+ var command = this.findKey(cm, key, origin);
+ if (typeof command === 'function') {
+ return command();
+ }
+ },
+ /**
+ * This is the outermost function called by CodeMirror, after keys have
+ * been mapped to their Vim equivalents.
+ *
+ * Finds a command based on the key (and cached keys if there is a
+ * multi-key sequence). Returns `undefined` if no key is matched, a noop
+ * function if a partial match is found (multi-key), and a function to
+ * execute the bound command if a a key is matched. The function always
+ * returns true.
+ */
+ findKey: function(cm, key, origin) {
+ var vim = maybeInitVimState(cm);
+ function handleMacroRecording() {
+ var macroModeState = vimGlobalState.macroModeState;
+ if (macroModeState.isRecording) {
+ if (key == 'q') {
+ macroModeState.exitMacroRecordMode();
+ clearInputState(cm);
+ return true;
+ }
+ if (origin != 'mapping') {
+ logKey(macroModeState, key);
+ }
+ }
+ }
+ function handleEsc() {
+ if (key == '<Esc>') {
+ // Clear input state and get back to normal mode.
+ clearInputState(cm);
+ if (vim.visualMode) {
+ exitVisualMode(cm);
+ } else if (vim.insertMode) {
+ exitInsertMode(cm);
+ }
+ return true;
+ }
+ }
+ function doKeyToKey(keys) {
+ // TODO: prevent infinite recursion.
+ var match;
+ while (keys) {
+ // Pull off one command key, which is either a single character
+ // or a special sequence wrapped in '<' and '>', e.g. '<Space>'.
+ match = (/<\w+-.+?>|<\w+>|./).exec(keys);
+ key = match[0];
+ keys = keys.substring(match.index + key.length);
+ CodeMirror.Vim.handleKey(cm, key, 'mapping');
+ }
+ }
+
+ function handleKeyInsertMode() {
+ if (handleEsc()) { return true; }
+ var keys = vim.inputState.keyBuffer = vim.inputState.keyBuffer + key;
+ var keysAreChars = key.length == 1;
+ var match = commandDispatcher.matchCommand(keys, defaultKeymap, vim.inputState, 'insert');
+ // Need to check all key substrings in insert mode.
+ while (keys.length > 1 && match.type != 'full') {
+ var keys = vim.inputState.keyBuffer = keys.slice(1);
+ var thisMatch = commandDispatcher.matchCommand(keys, defaultKeymap, vim.inputState, 'insert');
+ if (thisMatch.type != 'none') { match = thisMatch; }
+ }
+ if (match.type == 'none') { clearInputState(cm); return false; }
+ else if (match.type == 'partial') {
+ if (lastInsertModeKeyTimer) { window.clearTimeout(lastInsertModeKeyTimer); }
+ lastInsertModeKeyTimer = window.setTimeout(
+ function() { if (vim.insertMode && vim.inputState.keyBuffer) { clearInputState(cm); } },
+ getOption('insertModeEscKeysTimeout'));
+ return !keysAreChars;
+ }
+
+ if (lastInsertModeKeyTimer) { window.clearTimeout(lastInsertModeKeyTimer); }
+ if (keysAreChars) {
+ var here = cm.getCursor();
+ cm.replaceRange('', offsetCursor(here, 0, -(keys.length - 1)), here, '+input');
+ }
+ clearInputState(cm);
+ return match.command;
+ }
+
+ function handleKeyNonInsertMode() {
+ if (handleMacroRecording() || handleEsc()) { return true; };
+
+ var keys = vim.inputState.keyBuffer = vim.inputState.keyBuffer + key;
+ if (/^[1-9]\d*$/.test(keys)) { return true; }
+
+ var keysMatcher = /^(\d*)(.*)$/.exec(keys);
+ if (!keysMatcher) { clearInputState(cm); return false; }
+ var context = vim.visualMode ? 'visual' :
+ 'normal';
+ var match = commandDispatcher.matchCommand(keysMatcher[2] || keysMatcher[1], defaultKeymap, vim.inputState, context);
+ if (match.type == 'none') { clearInputState(cm); return false; }
+ else if (match.type == 'partial') { return true; }
+
+ vim.inputState.keyBuffer = '';
+ var keysMatcher = /^(\d*)(.*)$/.exec(keys);
+ if (keysMatcher[1] && keysMatcher[1] != '0') {
+ vim.inputState.pushRepeatDigit(keysMatcher[1]);
+ }
+ return match.command;
+ }
+
+ var command;
+ if (vim.insertMode) { command = handleKeyInsertMode(); }
+ else { command = handleKeyNonInsertMode(); }
+ if (command === false) {
+ return undefined;
+ } else if (command === true) {
+ // TODO: Look into using CodeMirror's multi-key handling.
+ // Return no-op since we are caching the key. Counts as handled, but
+ // don't want act on it just yet.
+ return function() {};
+ } else {
+ return function() {
+ return cm.operation(function() {
+ cm.curOp.isVimOp = true;
+ try {
+ if (command.type == 'keyToKey') {
+ doKeyToKey(command.toKeys);
+ } else {
+ commandDispatcher.processCommand(cm, vim, command);
+ }
+ } catch (e) {
+ // clear VIM state in case it's in a bad state.
+ cm.state.vim = undefined;
+ maybeInitVimState(cm);
+ if (!CodeMirror.Vim.suppressErrorLogging) {
+ console['log'](e);
+ }
+ throw e;
+ }
+ return true;
+ });
+ };
+ }
+ },
+ handleEx: function(cm, input) {
+ exCommandDispatcher.processCommand(cm, input);
+ },
+
+ defineMotion: defineMotion,
+ defineAction: defineAction,
+ defineOperator: defineOperator,
+ mapCommand: mapCommand,
+ _mapCommand: _mapCommand,
+
+ defineRegister: defineRegister,
+
+ exitVisualMode: exitVisualMode,
+ exitInsertMode: exitInsertMode
+ };
+
+ // Represents the current input state.
+ function InputState() {
+ this.prefixRepeat = [];
+ this.motionRepeat = [];
+
+ this.operator = null;
+ this.operatorArgs = null;
+ this.motion = null;
+ this.motionArgs = null;
+ this.keyBuffer = []; // For matching multi-key commands.
+ this.registerName = null; // Defaults to the unnamed register.
+ }
+ InputState.prototype.pushRepeatDigit = function(n) {
+ if (!this.operator) {
+ this.prefixRepeat = this.prefixRepeat.concat(n);
+ } else {
+ this.motionRepeat = this.motionRepeat.concat(n);
+ }
+ };
+ InputState.prototype.getRepeat = function() {
+ var repeat = 0;
+ if (this.prefixRepeat.length > 0 || this.motionRepeat.length > 0) {
+ repeat = 1;
+ if (this.prefixRepeat.length > 0) {
+ repeat *= parseInt(this.prefixRepeat.join(''), 10);
+ }
+ if (this.motionRepeat.length > 0) {
+ repeat *= parseInt(this.motionRepeat.join(''), 10);
+ }
+ }
+ return repeat;
+ };
+
+ function clearInputState(cm, reason) {
+ cm.state.vim.inputState = new InputState();
+ CodeMirror.signal(cm, 'vim-command-done', reason);
+ }
+
+ /*
+ * Register stores information about copy and paste registers. Besides
+ * text, a register must store whether it is linewise (i.e., when it is
+ * pasted, should it insert itself into a new line, or should the text be
+ * inserted at the cursor position.)
+ */
+ function Register(text, linewise, blockwise) {
+ this.clear();
+ this.keyBuffer = [text || ''];
+ this.insertModeChanges = [];
+ this.searchQueries = [];
+ this.linewise = !!linewise;
+ this.blockwise = !!blockwise;
+ }
+ Register.prototype = {
+ setText: function(text, linewise, blockwise) {
+ this.keyBuffer = [text || ''];
+ this.linewise = !!linewise;
+ this.blockwise = !!blockwise;
+ },
+ pushText: function(text, linewise) {
+ // if this register has ever been set to linewise, use linewise.
+ if (linewise) {
+ if (!this.linewise) {
+ this.keyBuffer.push('\n');
+ }
+ this.linewise = true;
+ }
+ this.keyBuffer.push(text);
+ },
+ pushInsertModeChanges: function(changes) {
+ this.insertModeChanges.push(createInsertModeChanges(changes));
+ },
+ pushSearchQuery: function(query) {
+ this.searchQueries.push(query);
+ },
+ clear: function() {
+ this.keyBuffer = [];
+ this.insertModeChanges = [];
+ this.searchQueries = [];
+ this.linewise = false;
+ },
+ toString: function() {
+ return this.keyBuffer.join('');
+ }
+ };
+
+ /**
+ * Defines an external register.
+ *
+ * The name should be a single character that will be used to reference the register.
+ * The register should support setText, pushText, clear, and toString(). See Register
+ * for a reference implementation.
+ */
+ function defineRegister(name, register) {
+ var registers = vimGlobalState.registerController.registers[name];
+ if (!name || name.length != 1) {
+ throw Error('Register name must be 1 character');
+ }
+ if (registers[name]) {
+ throw Error('Register already defined ' + name);
+ }
+ registers[name] = register;
+ validRegisters.push(name);
+ }
+
+ /*
+ * vim registers allow you to keep many independent copy and paste buffers.
+ * See http://usevim.com/2012/04/13/registers/ for an introduction.
+ *
+ * RegisterController keeps the state of all the registers. An initial
+ * state may be passed in. The unnamed register '"' will always be
+ * overridden.
+ */
+ function RegisterController(registers) {
+ this.registers = registers;
+ this.unnamedRegister = registers['"'] = new Register();
+ registers['.'] = new Register();
+ registers[':'] = new Register();
+ registers['/'] = new Register();
+ }
+ RegisterController.prototype = {
+ pushText: function(registerName, operator, text, linewise, blockwise) {
+ if (linewise && text.charAt(0) == '\n') {
+ text = text.slice(1) + '\n';
+ }
+ if (linewise && text.charAt(text.length - 1) !== '\n'){
+ text += '\n';
+ }
+ // Lowercase and uppercase registers refer to the same register.
+ // Uppercase just means append.
+ var register = this.isValidRegister(registerName) ?
+ this.getRegister(registerName) : null;
+ // if no register/an invalid register was specified, things go to the
+ // default registers
+ if (!register) {
+ switch (operator) {
+ case 'yank':
+ // The 0 register contains the text from the most recent yank.
+ this.registers['0'] = new Register(text, linewise, blockwise);
+ break;
+ case 'delete':
+ case 'change':
+ if (text.indexOf('\n') == -1) {
+ // Delete less than 1 line. Update the small delete register.
+ this.registers['-'] = new Register(text, linewise);
+ } else {
+ // Shift down the contents of the numbered registers and put the
+ // deleted text into register 1.
+ this.shiftNumericRegisters_();
+ this.registers['1'] = new Register(text, linewise);
+ }
+ break;
+ }
+ // Make sure the unnamed register is set to what just happened
+ this.unnamedRegister.setText(text, linewise, blockwise);
+ return;
+ }
+
+ // If we've gotten to this point, we've actually specified a register
+ var append = isUpperCase(registerName);
+ if (append) {
+ register.pushText(text, linewise);
+ } else {
+ register.setText(text, linewise, blockwise);
+ }
+ // The unnamed register always has the same value as the last used
+ // register.
+ this.unnamedRegister.setText(register.toString(), linewise);
+ },
+ // Gets the register named @name. If one of @name doesn't already exist,
+ // create it. If @name is invalid, return the unnamedRegister.
+ getRegister: function(name) {
+ if (!this.isValidRegister(name)) {
+ return this.unnamedRegister;
+ }
+ name = name.toLowerCase();
+ if (!this.registers[name]) {
+ this.registers[name] = new Register();
+ }
+ return this.registers[name];
+ },
+ isValidRegister: function(name) {
+ return name && inArray(name, validRegisters);
+ },
+ shiftNumericRegisters_: function() {
+ for (var i = 9; i >= 2; i--) {
+ this.registers[i] = this.getRegister('' + (i - 1));
+ }
+ }
+ };
+ function HistoryController() {
+ this.historyBuffer = [];
+ this.iterator = 0;
+ this.initialPrefix = null;
+ }
+ HistoryController.prototype = {
+ // the input argument here acts a user entered prefix for a small time
+ // until we start autocompletion in which case it is the autocompleted.
+ nextMatch: function (input, up) {
+ var historyBuffer = this.historyBuffer;
+ var dir = up ? -1 : 1;
+ if (this.initialPrefix === null) this.initialPrefix = input;
+ for (var i = this.iterator + dir; up ? i >= 0 : i < historyBuffer.length; i+= dir) {
+ var element = historyBuffer[i];
+ for (var j = 0; j <= element.length; j++) {
+ if (this.initialPrefix == element.substring(0, j)) {
+ this.iterator = i;
+ return element;
+ }
+ }
+ }
+ // should return the user input in case we reach the end of buffer.
+ if (i >= historyBuffer.length) {
+ this.iterator = historyBuffer.length;
+ return this.initialPrefix;
+ }
+ // return the last autocompleted query or exCommand as it is.
+ if (i < 0 ) return input;
+ },
+ pushInput: function(input) {
+ var index = this.historyBuffer.indexOf(input);
+ if (index > -1) this.historyBuffer.splice(index, 1);
+ if (input.length) this.historyBuffer.push(input);
+ },
+ reset: function() {
+ this.initialPrefix = null;
+ this.iterator = this.historyBuffer.length;
+ }
+ };
+ var commandDispatcher = {
+ matchCommand: function(keys, keyMap, inputState, context) {
+ var matches = commandMatches(keys, keyMap, context, inputState);
+ if (!matches.full && !matches.partial) {
+ return {type: 'none'};
+ } else if (!matches.full && matches.partial) {
+ return {type: 'partial'};
+ }
+
+ var bestMatch;
+ for (var i = 0; i < matches.full.length; i++) {
+ var match = matches.full[i];
+ if (!bestMatch) {
+ bestMatch = match;
+ }
+ }
+ if (bestMatch.keys.slice(-11) == '<character>') {
+ inputState.selectedCharacter = lastChar(keys);
+ }
+ return {type: 'full', command: bestMatch};
+ },
+ processCommand: function(cm, vim, command) {
+ vim.inputState.repeatOverride = command.repeatOverride;
+ switch (command.type) {
+ case 'motion':
+ this.processMotion(cm, vim, command);
+ break;
+ case 'operator':
+ this.processOperator(cm, vim, command);
+ break;
+ case 'operatorMotion':
+ this.processOperatorMotion(cm, vim, command);
+ break;
+ case 'action':
+ this.processAction(cm, vim, command);
+ break;
+ case 'search':
+ this.processSearch(cm, vim, command);
+ break;
+ case 'ex':
+ case 'keyToEx':
+ this.processEx(cm, vim, command);
+ break;
+ default:
+ break;
+ }
+ },
+ processMotion: function(cm, vim, command) {
+ vim.inputState.motion = command.motion;
+ vim.inputState.motionArgs = copyArgs(command.motionArgs);
+ this.evalInput(cm, vim);
+ },
+ processOperator: function(cm, vim, command) {
+ var inputState = vim.inputState;
+ if (inputState.operator) {
+ if (inputState.operator == command.operator) {
+ // Typing an operator twice like 'dd' makes the operator operate
+ // linewise
+ inputState.motion = 'expandToLine';
+ inputState.motionArgs = { linewise: true };
+ this.evalInput(cm, vim);
+ return;
+ } else {
+ // 2 different operators in a row doesn't make sense.
+ clearInputState(cm);
+ }
+ }
+ inputState.operator = command.operator;
+ inputState.operatorArgs = copyArgs(command.operatorArgs);
+ if (vim.visualMode) {
+ // Operating on a selection in visual mode. We don't need a motion.
+ this.evalInput(cm, vim);
+ }
+ },
+ processOperatorMotion: function(cm, vim, command) {
+ var visualMode = vim.visualMode;
+ var operatorMotionArgs = copyArgs(command.operatorMotionArgs);
+ if (operatorMotionArgs) {
+ // Operator motions may have special behavior in visual mode.
+ if (visualMode && operatorMotionArgs.visualLine) {
+ vim.visualLine = true;
+ }
+ }
+ this.processOperator(cm, vim, command);
+ if (!visualMode) {
+ this.processMotion(cm, vim, command);
+ }
+ },
+ processAction: function(cm, vim, command) {
+ var inputState = vim.inputState;
+ var repeat = inputState.getRepeat();
+ var repeatIsExplicit = !!repeat;
+ var actionArgs = copyArgs(command.actionArgs) || {};
+ if (inputState.selectedCharacter) {
+ actionArgs.selectedCharacter = inputState.selectedCharacter;
+ }
+ // Actions may or may not have motions and operators. Do these first.
+ if (command.operator) {
+ this.processOperator(cm, vim, command);
+ }
+ if (command.motion) {
+ this.processMotion(cm, vim, command);
+ }
+ if (command.motion || command.operator) {
+ this.evalInput(cm, vim);
+ }
+ actionArgs.repeat = repeat || 1;
+ actionArgs.repeatIsExplicit = repeatIsExplicit;
+ actionArgs.registerName = inputState.registerName;
+ clearInputState(cm);
+ vim.lastMotion = null;
+ if (command.isEdit) {
+ this.recordLastEdit(vim, inputState, command);
+ }
+ actions[command.action](cm, actionArgs, vim);
+ },
+ processSearch: function(cm, vim, command) {
+ if (!cm.getSearchCursor) {
+ // Search depends on SearchCursor.
+ return;
+ }
+ var forward = command.searchArgs.forward;
+ var wholeWordOnly = command.searchArgs.wholeWordOnly;
+ getSearchState(cm).setReversed(!forward);
+ var promptPrefix = (forward) ? '/' : '?';
+ var originalQuery = getSearchState(cm).getQuery();
+ var originalScrollPos = cm.getScrollInfo();
+ function handleQuery(query, ignoreCase, smartCase) {
+ vimGlobalState.searchHistoryController.pushInput(query);
+ vimGlobalState.searchHistoryController.reset();
+ try {
+ updateSearchQuery(cm, query, ignoreCase, smartCase);
+ } catch (e) {
+ showConfirm(cm, 'Invalid regex: ' + query);
+ clearInputState(cm);
+ return;
+ }
+ commandDispatcher.processMotion(cm, vim, {
+ type: 'motion',
+ motion: 'findNext',
+ motionArgs: { forward: true, toJumplist: command.searchArgs.toJumplist }
+ });
+ }
+ function onPromptClose(query) {
+ cm.scrollTo(originalScrollPos.left, originalScrollPos.top);
+ handleQuery(query, true /** ignoreCase */, true /** smartCase */);
+ var macroModeState = vimGlobalState.macroModeState;
+ if (macroModeState.isRecording) {
+ logSearchQuery(macroModeState, query);
+ }
+ }
+ function onPromptKeyUp(e, query, close) {
+ var keyName = CodeMirror.keyName(e), up;
+ if (keyName == 'Up' || keyName == 'Down') {
+ up = keyName == 'Up' ? true : false;
+ query = vimGlobalState.searchHistoryController.nextMatch(query, up) || '';
+ close(query);
+ } else {
+ if ( keyName != 'Left' && keyName != 'Right' && keyName != 'Ctrl' && keyName != 'Alt' && keyName != 'Shift')
+ vimGlobalState.searchHistoryController.reset();
+ }
+ var parsedQuery;
+ try {
+ parsedQuery = updateSearchQuery(cm, query,
+ true /** ignoreCase */, true /** smartCase */);
+ } catch (e) {
+ // Swallow bad regexes for incremental search.
+ }
+ if (parsedQuery) {
+ cm.scrollIntoView(findNext(cm, !forward, parsedQuery), 30);
+ } else {
+ clearSearchHighlight(cm);
+ cm.scrollTo(originalScrollPos.left, originalScrollPos.top);
+ }
+ }
+ function onPromptKeyDown(e, query, close) {
+ var keyName = CodeMirror.keyName(e);
+ if (keyName == 'Esc' || keyName == 'Ctrl-C' || keyName == 'Ctrl-[' ||
+ (keyName == 'Backspace' && query == '')) {
+ vimGlobalState.searchHistoryController.pushInput(query);
+ vimGlobalState.searchHistoryController.reset();
+ updateSearchQuery(cm, originalQuery);
+ clearSearchHighlight(cm);
+ cm.scrollTo(originalScrollPos.left, originalScrollPos.top);
+ CodeMirror.e_stop(e);
+ clearInputState(cm);
+ close();
+ cm.focus();
+ } else if (keyName == 'Ctrl-U') {
+ // Ctrl-U clears input.
+ CodeMirror.e_stop(e);
+ close('');
+ }
+ }
+ switch (command.searchArgs.querySrc) {
+ case 'prompt':
+ var macroModeState = vimGlobalState.macroModeState;
+ if (macroModeState.isPlaying) {
+ var query = macroModeState.replaySearchQueries.shift();
+ handleQuery(query, true /** ignoreCase */, false /** smartCase */);
+ } else {
+ showPrompt(cm, {
+ onClose: onPromptClose,
+ prefix: promptPrefix,
+ desc: searchPromptDesc,
+ onKeyUp: onPromptKeyUp,
+ onKeyDown: onPromptKeyDown
+ });
+ }
+ break;
+ case 'wordUnderCursor':
+ var word = expandWordUnderCursor(cm, false /** inclusive */,
+ true /** forward */, false /** bigWord */,
+ true /** noSymbol */);
+ var isKeyword = true;
+ if (!word) {
+ word = expandWordUnderCursor(cm, false /** inclusive */,
+ true /** forward */, false /** bigWord */,
+ false /** noSymbol */);
+ isKeyword = false;
+ }
+ if (!word) {
+ return;
+ }
+ var query = cm.getLine(word.start.line).substring(word.start.ch,
+ word.end.ch);
+ if (isKeyword && wholeWordOnly) {
+ query = '\\b' + query + '\\b';
+ } else {
+ query = escapeRegex(query);
+ }
+
+ // cachedCursor is used to save the old position of the cursor
+ // when * or # causes vim to seek for the nearest word and shift
+ // the cursor before entering the motion.
+ vimGlobalState.jumpList.cachedCursor = cm.getCursor();
+ cm.setCursor(word.start);
+
+ handleQuery(query, true /** ignoreCase */, false /** smartCase */);
+ break;
+ }
+ },
+ processEx: function(cm, vim, command) {
+ function onPromptClose(input) {
+ // Give the prompt some time to close so that if processCommand shows
+ // an error, the elements don't overlap.
+ vimGlobalState.exCommandHistoryController.pushInput(input);
+ vimGlobalState.exCommandHistoryController.reset();
+ exCommandDispatcher.processCommand(cm, input);
+ }
+ function onPromptKeyDown(e, input, close) {
+ var keyName = CodeMirror.keyName(e), up;
+ if (keyName == 'Esc' || keyName == 'Ctrl-C' || keyName == 'Ctrl-[' ||
+ (keyName == 'Backspace' && input == '')) {
+ vimGlobalState.exCommandHistoryController.pushInput(input);
+ vimGlobalState.exCommandHistoryController.reset();
+ CodeMirror.e_stop(e);
+ clearInputState(cm);
+ close();
+ cm.focus();
+ }
+ if (keyName == 'Up' || keyName == 'Down') {
+ up = keyName == 'Up' ? true : false;
+ input = vimGlobalState.exCommandHistoryController.nextMatch(input, up) || '';
+ close(input);
+ } else if (keyName == 'Ctrl-U') {
+ // Ctrl-U clears input.
+ CodeMirror.e_stop(e);
+ close('');
+ } else {
+ if ( keyName != 'Left' && keyName != 'Right' && keyName != 'Ctrl' && keyName != 'Alt' && keyName != 'Shift')
+ vimGlobalState.exCommandHistoryController.reset();
+ }
+ }
+ if (command.type == 'keyToEx') {
+ // Handle user defined Ex to Ex mappings
+ exCommandDispatcher.processCommand(cm, command.exArgs.input);
+ } else {
+ if (vim.visualMode) {
+ showPrompt(cm, { onClose: onPromptClose, prefix: ':', value: '\'<,\'>',
+ onKeyDown: onPromptKeyDown});
+ } else {
+ showPrompt(cm, { onClose: onPromptClose, prefix: ':',
+ onKeyDown: onPromptKeyDown});
+ }
+ }
+ },
+ evalInput: function(cm, vim) {
+ // If the motion command is set, execute both the operator and motion.
+ // Otherwise return.
+ var inputState = vim.inputState;
+ var motion = inputState.motion;
+ var motionArgs = inputState.motionArgs || {};
+ var operator = inputState.operator;
+ var operatorArgs = inputState.operatorArgs || {};
+ var registerName = inputState.registerName;
+ var sel = vim.sel;
+ // TODO: Make sure cm and vim selections are identical outside visual mode.
+ var origHead = copyCursor(vim.visualMode ? clipCursorToContent(cm, sel.head): cm.getCursor('head'));
+ var origAnchor = copyCursor(vim.visualMode ? clipCursorToContent(cm, sel.anchor) : cm.getCursor('anchor'));
+ var oldHead = copyCursor(origHead);
+ var oldAnchor = copyCursor(origAnchor);
+ var newHead, newAnchor;
+ var repeat;
+ if (operator) {
+ this.recordLastEdit(vim, inputState);
+ }
+ if (inputState.repeatOverride !== undefined) {
+ // If repeatOverride is specified, that takes precedence over the
+ // input state's repeat. Used by Ex mode and can be user defined.
+ repeat = inputState.repeatOverride;
+ } else {
+ repeat = inputState.getRepeat();
+ }
+ if (repeat > 0 && motionArgs.explicitRepeat) {
+ motionArgs.repeatIsExplicit = true;
+ } else if (motionArgs.noRepeat ||
+ (!motionArgs.explicitRepeat && repeat === 0)) {
+ repeat = 1;
+ motionArgs.repeatIsExplicit = false;
+ }
+ if (inputState.selectedCharacter) {
+ // If there is a character input, stick it in all of the arg arrays.
+ motionArgs.selectedCharacter = operatorArgs.selectedCharacter =
+ inputState.selectedCharacter;
+ }
+ motionArgs.repeat = repeat;
+ clearInputState(cm);
+ if (motion) {
+ var motionResult = motions[motion](cm, origHead, motionArgs, vim);
+ vim.lastMotion = motions[motion];
+ if (!motionResult) {
+ return;
+ }
+ if (motionArgs.toJumplist) {
+ var jumpList = vimGlobalState.jumpList;
+ // if the current motion is # or *, use cachedCursor
+ var cachedCursor = jumpList.cachedCursor;
+ if (cachedCursor) {
+ recordJumpPosition(cm, cachedCursor, motionResult);
+ delete jumpList.cachedCursor;
+ } else {
+ recordJumpPosition(cm, origHead, motionResult);
+ }
+ }
+ if (motionResult instanceof Array) {
+ newAnchor = motionResult[0];
+ newHead = motionResult[1];
+ } else {
+ newHead = motionResult;
+ }
+ // TODO: Handle null returns from motion commands better.
+ if (!newHead) {
+ newHead = copyCursor(origHead);
+ }
+ if (vim.visualMode) {
+ if (!(vim.visualBlock && newHead.ch === Infinity)) {
+ newHead = clipCursorToContent(cm, newHead, vim.visualBlock);
+ }
+ if (newAnchor) {
+ newAnchor = clipCursorToContent(cm, newAnchor, true);
+ }
+ newAnchor = newAnchor || oldAnchor;
+ sel.anchor = newAnchor;
+ sel.head = newHead;
+ updateCmSelection(cm);
+ updateMark(cm, vim, '<',
+ cursorIsBefore(newAnchor, newHead) ? newAnchor
+ : newHead);
+ updateMark(cm, vim, '>',
+ cursorIsBefore(newAnchor, newHead) ? newHead
+ : newAnchor);
+ } else if (!operator) {
+ newHead = clipCursorToContent(cm, newHead);
+ cm.setCursor(newHead.line, newHead.ch);
+ }
+ }
+ if (operator) {
+ if (operatorArgs.lastSel) {
+ // Replaying a visual mode operation
+ newAnchor = oldAnchor;
+ var lastSel = operatorArgs.lastSel;
+ var lineOffset = Math.abs(lastSel.head.line - lastSel.anchor.line);
+ var chOffset = Math.abs(lastSel.head.ch - lastSel.anchor.ch);
+ if (lastSel.visualLine) {
+ // Linewise Visual mode: The same number of lines.
+ newHead = Pos(oldAnchor.line + lineOffset, oldAnchor.ch);
+ } else if (lastSel.visualBlock) {
+ // Blockwise Visual mode: The same number of lines and columns.
+ newHead = Pos(oldAnchor.line + lineOffset, oldAnchor.ch + chOffset);
+ } else if (lastSel.head.line == lastSel.anchor.line) {
+ // Normal Visual mode within one line: The same number of characters.
+ newHead = Pos(oldAnchor.line, oldAnchor.ch + chOffset);
+ } else {
+ // Normal Visual mode with several lines: The same number of lines, in the
+ // last line the same number of characters as in the last line the last time.
+ newHead = Pos(oldAnchor.line + lineOffset, oldAnchor.ch);
+ }
+ vim.visualMode = true;
+ vim.visualLine = lastSel.visualLine;
+ vim.visualBlock = lastSel.visualBlock;
+ sel = vim.sel = {
+ anchor: newAnchor,
+ head: newHead
+ };
+ updateCmSelection(cm);
+ } else if (vim.visualMode) {
+ operatorArgs.lastSel = {
+ anchor: copyCursor(sel.anchor),
+ head: copyCursor(sel.head),
+ visualBlock: vim.visualBlock,
+ visualLine: vim.visualLine
+ };
+ }
+ var curStart, curEnd, linewise, mode;
+ var cmSel;
+ if (vim.visualMode) {
+ // Init visual op
+ curStart = cursorMin(sel.head, sel.anchor);
+ curEnd = cursorMax(sel.head, sel.anchor);
+ linewise = vim.visualLine || operatorArgs.linewise;
+ mode = vim.visualBlock ? 'block' :
+ linewise ? 'line' :
+ 'char';
+ cmSel = makeCmSelection(cm, {
+ anchor: curStart,
+ head: curEnd
+ }, mode);
+ if (linewise) {
+ var ranges = cmSel.ranges;
+ if (mode == 'block') {
+ // Linewise operators in visual block mode extend to end of line
+ for (var i = 0; i < ranges.length; i++) {
+ ranges[i].head.ch = lineLength(cm, ranges[i].head.line);
+ }
+ } else if (mode == 'line') {
+ ranges[0].head = Pos(ranges[0].head.line + 1, 0);
+ }
+ }
+ } else {
+ // Init motion op
+ curStart = copyCursor(newAnchor || oldAnchor);
+ curEnd = copyCursor(newHead || oldHead);
+ if (cursorIsBefore(curEnd, curStart)) {
+ var tmp = curStart;
+ curStart = curEnd;
+ curEnd = tmp;
+ }
+ linewise = motionArgs.linewise || operatorArgs.linewise;
+ if (linewise) {
+ // Expand selection to entire line.
+ expandSelectionToLine(cm, curStart, curEnd);
+ } else if (motionArgs.forward) {
+ // Clip to trailing newlines only if the motion goes forward.
+ clipToLine(cm, curStart, curEnd);
+ }
+ mode = 'char';
+ var exclusive = !motionArgs.inclusive || linewise;
+ cmSel = makeCmSelection(cm, {
+ anchor: curStart,
+ head: curEnd
+ }, mode, exclusive);
+ }
+ cm.setSelections(cmSel.ranges, cmSel.primary);
+ vim.lastMotion = null;
+ operatorArgs.repeat = repeat; // For indent in visual mode.
+ operatorArgs.registerName = registerName;
+ // Keep track of linewise as it affects how paste and change behave.
+ operatorArgs.linewise = linewise;
+ var operatorMoveTo = operators[operator](
+ cm, operatorArgs, cmSel.ranges, oldAnchor, newHead);
+ if (vim.visualMode) {
+ exitVisualMode(cm, operatorMoveTo != null);
+ }
+ if (operatorMoveTo) {
+ cm.setCursor(operatorMoveTo);
+ }
+ }
+ },
+ recordLastEdit: function(vim, inputState, actionCommand) {
+ var macroModeState = vimGlobalState.macroModeState;
+ if (macroModeState.isPlaying) { return; }
+ vim.lastEditInputState = inputState;
+ vim.lastEditActionCommand = actionCommand;
+ macroModeState.lastInsertModeChanges.changes = [];
+ macroModeState.lastInsertModeChanges.expectCursorActivityForChange = false;
+ }
+ };
+
+ /**
+ * typedef {Object{line:number,ch:number}} Cursor An object containing the
+ * position of the cursor.
+ */
+ // All of the functions below return Cursor objects.
+ var motions = {
+ moveToTopLine: function(cm, _head, motionArgs) {
+ var line = getUserVisibleLines(cm).top + motionArgs.repeat -1;
+ return Pos(line, findFirstNonWhiteSpaceCharacter(cm.getLine(line)));
+ },
+ moveToMiddleLine: function(cm) {
+ var range = getUserVisibleLines(cm);
+ var line = Math.floor((range.top + range.bottom) * 0.5);
+ return Pos(line, findFirstNonWhiteSpaceCharacter(cm.getLine(line)));
+ },
+ moveToBottomLine: function(cm, _head, motionArgs) {
+ var line = getUserVisibleLines(cm).bottom - motionArgs.repeat +1;
+ return Pos(line, findFirstNonWhiteSpaceCharacter(cm.getLine(line)));
+ },
+ expandToLine: function(_cm, head, motionArgs) {
+ // Expands forward to end of line, and then to next line if repeat is
+ // >1. Does not handle backward motion!
+ var cur = head;
+ return Pos(cur.line + motionArgs.repeat - 1, Infinity);
+ },
+ findNext: function(cm, _head, motionArgs) {
+ var state = getSearchState(cm);
+ var query = state.getQuery();
+ if (!query) {
+ return;
+ }
+ var prev = !motionArgs.forward;
+ // If search is initiated with ? instead of /, negate direction.
+ prev = (state.isReversed()) ? !prev : prev;
+ highlightSearchMatches(cm, query);
+ return findNext(cm, prev/** prev */, query, motionArgs.repeat);
+ },
+ goToMark: function(cm, _head, motionArgs, vim) {
+ var mark = vim.marks[motionArgs.selectedCharacter];
+ if (mark) {
+ var pos = mark.find();
+ return motionArgs.linewise ? { line: pos.line, ch: findFirstNonWhiteSpaceCharacter(cm.getLine(pos.line)) } : pos;
+ }
+ return null;
+ },
+ moveToOtherHighlightedEnd: function(cm, _head, motionArgs, vim) {
+ if (vim.visualBlock && motionArgs.sameLine) {
+ var sel = vim.sel;
+ return [
+ clipCursorToContent(cm, Pos(sel.anchor.line, sel.head.ch)),
+ clipCursorToContent(cm, Pos(sel.head.line, sel.anchor.ch))
+ ];
+ } else {
+ return ([vim.sel.head, vim.sel.anchor]);
+ }
+ },
+ jumpToMark: function(cm, head, motionArgs, vim) {
+ var best = head;
+ for (var i = 0; i < motionArgs.repeat; i++) {
+ var cursor = best;
+ for (var key in vim.marks) {
+ if (!isLowerCase(key)) {
+ continue;
+ }
+ var mark = vim.marks[key].find();
+ var isWrongDirection = (motionArgs.forward) ?
+ cursorIsBefore(mark, cursor) : cursorIsBefore(cursor, mark);
+
+ if (isWrongDirection) {
+ continue;
+ }
+ if (motionArgs.linewise && (mark.line == cursor.line)) {
+ continue;
+ }
+
+ var equal = cursorEqual(cursor, best);
+ var between = (motionArgs.forward) ?
+ cursorIsBetween(cursor, mark, best) :
+ cursorIsBetween(best, mark, cursor);
+
+ if (equal || between) {
+ best = mark;
+ }
+ }
+ }
+
+ if (motionArgs.linewise) {
+ // Vim places the cursor on the first non-whitespace character of
+ // the line if there is one, else it places the cursor at the end
+ // of the line, regardless of whether a mark was found.
+ best = Pos(best.line, findFirstNonWhiteSpaceCharacter(cm.getLine(best.line)));
+ }
+ return best;
+ },
+ moveByCharacters: function(_cm, head, motionArgs) {
+ var cur = head;
+ var repeat = motionArgs.repeat;
+ var ch = motionArgs.forward ? cur.ch + repeat : cur.ch - repeat;
+ return Pos(cur.line, ch);
+ },
+ moveByLines: function(cm, head, motionArgs, vim) {
+ var cur = head;
+ var endCh = cur.ch;
+ // Depending what our last motion was, we may want to do different
+ // things. If our last motion was moving vertically, we want to
+ // preserve the HPos from our last horizontal move. If our last motion
+ // was going to the end of a line, moving vertically we should go to
+ // the end of the line, etc.
+ switch (vim.lastMotion) {
+ case this.moveByLines:
+ case this.moveByDisplayLines:
+ case this.moveByScroll:
+ case this.moveToColumn:
+ case this.moveToEol:
+ endCh = vim.lastHPos;
+ break;
+ default:
+ vim.lastHPos = endCh;
+ }
+ var repeat = motionArgs.repeat+(motionArgs.repeatOffset||0);
+ var line = motionArgs.forward ? cur.line + repeat : cur.line - repeat;
+ var first = cm.firstLine();
+ var last = cm.lastLine();
+ // Vim go to line begin or line end when cursor at first/last line and
+ // move to previous/next line is triggered.
+ if (line < first && cur.line == first){
+ return this.moveToStartOfLine(cm, head, motionArgs, vim);
+ }else if (line > last && cur.line == last){
+ return this.moveToEol(cm, head, motionArgs, vim);
+ }
+ if (motionArgs.toFirstChar){
+ endCh=findFirstNonWhiteSpaceCharacter(cm.getLine(line));
+ vim.lastHPos = endCh;
+ }
+ vim.lastHSPos = cm.charCoords(Pos(line, endCh),'div').left;
+ return Pos(line, endCh);
+ },
+ moveByDisplayLines: function(cm, head, motionArgs, vim) {
+ var cur = head;
+ switch (vim.lastMotion) {
+ case this.moveByDisplayLines:
+ case this.moveByScroll:
+ case this.moveByLines:
+ case this.moveToColumn:
+ case this.moveToEol:
+ break;
+ default:
+ vim.lastHSPos = cm.charCoords(cur,'div').left;
+ }
+ var repeat = motionArgs.repeat;
+ var res=cm.findPosV(cur,(motionArgs.forward ? repeat : -repeat),'line',vim.lastHSPos);
+ if (res.hitSide) {
+ if (motionArgs.forward) {
+ var lastCharCoords = cm.charCoords(res, 'div');
+ var goalCoords = { top: lastCharCoords.top + 8, left: vim.lastHSPos };
+ var res = cm.coordsChar(goalCoords, 'div');
+ } else {
+ var resCoords = cm.charCoords(Pos(cm.firstLine(), 0), 'div');
+ resCoords.left = vim.lastHSPos;
+ res = cm.coordsChar(resCoords, 'div');
+ }
+ }
+ vim.lastHPos = res.ch;
+ return res;
+ },
+ moveByPage: function(cm, head, motionArgs) {
+ // CodeMirror only exposes functions that move the cursor page down, so
+ // doing this bad hack to move the cursor and move it back. evalInput
+ // will move the cursor to where it should be in the end.
+ var curStart = head;
+ var repeat = motionArgs.repeat;
+ return cm.findPosV(curStart, (motionArgs.forward ? repeat : -repeat), 'page');
+ },
+ moveByParagraph: function(cm, head, motionArgs) {
+ var dir = motionArgs.forward ? 1 : -1;
+ return findParagraph(cm, head, motionArgs.repeat, dir);
+ },
+ moveByScroll: function(cm, head, motionArgs, vim) {
+ var scrollbox = cm.getScrollInfo();
+ var curEnd = null;
+ var repeat = motionArgs.repeat;
+ if (!repeat) {
+ repeat = scrollbox.clientHeight / (2 * cm.defaultTextHeight());
+ }
+ var orig = cm.charCoords(head, 'local');
+ motionArgs.repeat = repeat;
+ var curEnd = motions.moveByDisplayLines(cm, head, motionArgs, vim);
+ if (!curEnd) {
+ return null;
+ }
+ var dest = cm.charCoords(curEnd, 'local');
+ cm.scrollTo(null, scrollbox.top + dest.top - orig.top);
+ return curEnd;
+ },
+ moveByWords: function(cm, head, motionArgs) {
+ return moveToWord(cm, head, motionArgs.repeat, !!motionArgs.forward,
+ !!motionArgs.wordEnd, !!motionArgs.bigWord);
+ },
+ moveTillCharacter: function(cm, _head, motionArgs) {
+ var repeat = motionArgs.repeat;
+ var curEnd = moveToCharacter(cm, repeat, motionArgs.forward,
+ motionArgs.selectedCharacter);
+ var increment = motionArgs.forward ? -1 : 1;
+ recordLastCharacterSearch(increment, motionArgs);
+ if (!curEnd) return null;
+ curEnd.ch += increment;
+ return curEnd;
+ },
+ moveToCharacter: function(cm, head, motionArgs) {
+ var repeat = motionArgs.repeat;
+ recordLastCharacterSearch(0, motionArgs);
+ return moveToCharacter(cm, repeat, motionArgs.forward,
+ motionArgs.selectedCharacter) || head;
+ },
+ moveToSymbol: function(cm, head, motionArgs) {
+ var repeat = motionArgs.repeat;
+ return findSymbol(cm, repeat, motionArgs.forward,
+ motionArgs.selectedCharacter) || head;
+ },
+ moveToColumn: function(cm, head, motionArgs, vim) {
+ var repeat = motionArgs.repeat;
+ // repeat is equivalent to which column we want to move to!
+ vim.lastHPos = repeat - 1;
+ vim.lastHSPos = cm.charCoords(head,'div').left;
+ return moveToColumn(cm, repeat);
+ },
+ moveToEol: function(cm, head, motionArgs, vim) {
+ var cur = head;
+ vim.lastHPos = Infinity;
+ var retval= Pos(cur.line + motionArgs.repeat - 1, Infinity);
+ var end=cm.clipPos(retval);
+ end.ch--;
+ vim.lastHSPos = cm.charCoords(end,'div').left;
+ return retval;
+ },
+ moveToFirstNonWhiteSpaceCharacter: function(cm, head) {
+ // Go to the start of the line where the text begins, or the end for
+ // whitespace-only lines
+ var cursor = head;
+ return Pos(cursor.line,
+ findFirstNonWhiteSpaceCharacter(cm.getLine(cursor.line)));
+ },
+ moveToMatchedSymbol: function(cm, head) {
+ var cursor = head;
+ var line = cursor.line;
+ var ch = cursor.ch;
+ var lineText = cm.getLine(line);
+ var symbol;
+ do {
+ symbol = lineText.charAt(ch++);
+ if (symbol && isMatchableSymbol(symbol)) {
+ var style = cm.getTokenTypeAt(Pos(line, ch));
+ if (style !== "string" && style !== "comment") {
+ break;
+ }
+ }
+ } while (symbol);
+ if (symbol) {
+ var matched = cm.findMatchingBracket(Pos(line, ch));
+ return matched.to;
+ } else {
+ return cursor;
+ }
+ },
+ moveToStartOfLine: function(_cm, head) {
+ return Pos(head.line, 0);
+ },
+ moveToLineOrEdgeOfDocument: function(cm, _head, motionArgs) {
+ var lineNum = motionArgs.forward ? cm.lastLine() : cm.firstLine();
+ if (motionArgs.repeatIsExplicit) {
+ lineNum = motionArgs.repeat - cm.getOption('firstLineNumber');
+ }
+ return Pos(lineNum,
+ findFirstNonWhiteSpaceCharacter(cm.getLine(lineNum)));
+ },
+ textObjectManipulation: function(cm, head, motionArgs, vim) {
+ // TODO: lots of possible exceptions that can be thrown here. Try da(
+ // outside of a () block.
+
+ // TODO: adding <> >< to this map doesn't work, presumably because
+ // they're operators
+ var mirroredPairs = {'(': ')', ')': '(',
+ '{': '}', '}': '{',
+ '[': ']', ']': '['};
+ var selfPaired = {'\'': true, '"': true};
+
+ var character = motionArgs.selectedCharacter;
+ // 'b' refers to '()' block.
+ // 'B' refers to '{}' block.
+ if (character == 'b') {
+ character = '(';
+ } else if (character == 'B') {
+ character = '{';
+ }
+
+ // Inclusive is the difference between a and i
+ // TODO: Instead of using the additional text object map to perform text
+ // object operations, merge the map into the defaultKeyMap and use
+ // motionArgs to define behavior. Define separate entries for 'aw',
+ // 'iw', 'a[', 'i[', etc.
+ var inclusive = !motionArgs.textObjectInner;
+
+ var tmp;
+ if (mirroredPairs[character]) {
+ tmp = selectCompanionObject(cm, head, character, inclusive);
+ } else if (selfPaired[character]) {
+ tmp = findBeginningAndEnd(cm, head, character, inclusive);
+ } else if (character === 'W') {
+ tmp = expandWordUnderCursor(cm, inclusive, true /** forward */,
+ true /** bigWord */);
+ } else if (character === 'w') {
+ tmp = expandWordUnderCursor(cm, inclusive, true /** forward */,
+ false /** bigWord */);
+ } else if (character === 'p') {
+ tmp = findParagraph(cm, head, motionArgs.repeat, 0, inclusive);
+ motionArgs.linewise = true;
+ if (vim.visualMode) {
+ if (!vim.visualLine) { vim.visualLine = true; }
+ } else {
+ var operatorArgs = vim.inputState.operatorArgs;
+ if (operatorArgs) { operatorArgs.linewise = true; }
+ tmp.end.line--;
+ }
+ } else {
+ // No text object defined for this, don't move.
+ return null;
+ }
+
+ if (!cm.state.vim.visualMode) {
+ return [tmp.start, tmp.end];
+ } else {
+ return expandSelection(cm, tmp.start, tmp.end);
+ }
+ },
+
+ repeatLastCharacterSearch: function(cm, head, motionArgs) {
+ var lastSearch = vimGlobalState.lastCharacterSearch;
+ var repeat = motionArgs.repeat;
+ var forward = motionArgs.forward === lastSearch.forward;
+ var increment = (lastSearch.increment ? 1 : 0) * (forward ? -1 : 1);
+ cm.moveH(-increment, 'char');
+ motionArgs.inclusive = forward ? true : false;
+ var curEnd = moveToCharacter(cm, repeat, forward, lastSearch.selectedCharacter);
+ if (!curEnd) {
+ cm.moveH(increment, 'char');
+ return head;
+ }
+ curEnd.ch += increment;
+ return curEnd;
+ }
+ };
+
+ function defineMotion(name, fn) {
+ motions[name] = fn;
+ }
+
+ function fillArray(val, times) {
+ var arr = [];
+ for (var i = 0; i < times; i++) {
+ arr.push(val);
+ }
+ return arr;
+ }
+ /**
+ * An operator acts on a text selection. It receives the list of selections
+ * as input. The corresponding CodeMirror selection is guaranteed to
+ * match the input selection.
+ */
+ var operators = {
+ change: function(cm, args, ranges) {
+ var finalHead, text;
+ var vim = cm.state.vim;
+ vimGlobalState.macroModeState.lastInsertModeChanges.inVisualBlock = vim.visualBlock;
+ if (!vim.visualMode) {
+ var anchor = ranges[0].anchor,
+ head = ranges[0].head;
+ text = cm.getRange(anchor, head);
+ var lastState = vim.lastEditInputState || {};
+ if (lastState.motion == "moveByWords" && !isWhiteSpaceString(text)) {
+ // Exclude trailing whitespace if the range is not all whitespace.
+ var match = (/\s+$/).exec(text);
+ if (match && lastState.motionArgs && lastState.motionArgs.forward) {
+ head = offsetCursor(head, 0, - match[0].length);
+ text = text.slice(0, - match[0].length);
+ }
+ }
+ var prevLineEnd = new Pos(anchor.line - 1, Number.MAX_VALUE);
+ var wasLastLine = cm.firstLine() == cm.lastLine();
+ if (head.line > cm.lastLine() && args.linewise && !wasLastLine) {
+ cm.replaceRange('', prevLineEnd, head);
+ } else {
+ cm.replaceRange('', anchor, head);
+ }
+ if (args.linewise) {
+ // Push the next line back down, if there is a next line.
+ if (!wasLastLine) {
+ cm.setCursor(prevLineEnd);
+ CodeMirror.commands.newlineAndIndent(cm);
+ }
+ // make sure cursor ends up at the end of the line.
+ anchor.ch = Number.MAX_VALUE;
+ }
+ finalHead = anchor;
+ } else {
+ text = cm.getSelection();
+ var replacement = fillArray('', ranges.length);
+ cm.replaceSelections(replacement);
+ finalHead = cursorMin(ranges[0].head, ranges[0].anchor);
+ }
+ vimGlobalState.registerController.pushText(
+ args.registerName, 'change', text,
+ args.linewise, ranges.length > 1);
+ actions.enterInsertMode(cm, {head: finalHead}, cm.state.vim);
+ },
+ // delete is a javascript keyword.
+ 'delete': function(cm, args, ranges) {
+ var finalHead, text;
+ var vim = cm.state.vim;
+ if (!vim.visualBlock) {
+ var anchor = ranges[0].anchor,
+ head = ranges[0].head;
+ if (args.linewise &&
+ head.line != cm.firstLine() &&
+ anchor.line == cm.lastLine() &&
+ anchor.line == head.line - 1) {
+ // Special case for dd on last line (and first line).
+ if (anchor.line == cm.firstLine()) {
+ anchor.ch = 0;
+ } else {
+ anchor = Pos(anchor.line - 1, lineLength(cm, anchor.line - 1));
+ }
+ }
+ text = cm.getRange(anchor, head);
+ cm.replaceRange('', anchor, head);
+ finalHead = anchor;
+ if (args.linewise) {
+ finalHead = motions.moveToFirstNonWhiteSpaceCharacter(cm, anchor);
+ }
+ } else {
+ text = cm.getSelection();
+ var replacement = fillArray('', ranges.length);
+ cm.replaceSelections(replacement);
+ finalHead = ranges[0].anchor;
+ }
+ vimGlobalState.registerController.pushText(
+ args.registerName, 'delete', text,
+ args.linewise, vim.visualBlock);
+ return clipCursorToContent(cm, finalHead);
+ },
+ indent: function(cm, args, ranges) {
+ var vim = cm.state.vim;
+ var startLine = ranges[0].anchor.line;
+ var endLine = vim.visualBlock ?
+ ranges[ranges.length - 1].anchor.line :
+ ranges[0].head.line;
+ // In visual mode, n> shifts the selection right n times, instead of
+ // shifting n lines right once.
+ var repeat = (vim.visualMode) ? args.repeat : 1;
+ if (args.linewise) {
+ // The only way to delete a newline is to delete until the start of
+ // the next line, so in linewise mode evalInput will include the next
+ // line. We don't want this in indent, so we go back a line.
+ endLine--;
+ }
+ for (var i = startLine; i <= endLine; i++) {
+ for (var j = 0; j < repeat; j++) {
+ cm.indentLine(i, args.indentRight);
+ }
+ }
+ return motions.moveToFirstNonWhiteSpaceCharacter(cm, ranges[0].anchor);
+ },
+ changeCase: function(cm, args, ranges, oldAnchor, newHead) {
+ var selections = cm.getSelections();
+ var swapped = [];
+ var toLower = args.toLower;
+ for (var j = 0; j < selections.length; j++) {
+ var toSwap = selections[j];
+ var text = '';
+ if (toLower === true) {
+ text = toSwap.toLowerCase();
+ } else if (toLower === false) {
+ text = toSwap.toUpperCase();
+ } else {
+ for (var i = 0; i < toSwap.length; i++) {
+ var character = toSwap.charAt(i);
+ text += isUpperCase(character) ? character.toLowerCase() :
+ character.toUpperCase();
+ }
+ }
+ swapped.push(text);
+ }
+ cm.replaceSelections(swapped);
+ if (args.shouldMoveCursor){
+ return newHead;
+ } else if (!cm.state.vim.visualMode && args.linewise && ranges[0].anchor.line + 1 == ranges[0].head.line) {
+ return motions.moveToFirstNonWhiteSpaceCharacter(cm, oldAnchor);
+ } else if (args.linewise){
+ return oldAnchor;
+ } else {
+ return cursorMin(ranges[0].anchor, ranges[0].head);
+ }
+ },
+ yank: function(cm, args, ranges, oldAnchor) {
+ var vim = cm.state.vim;
+ var text = cm.getSelection();
+ var endPos = vim.visualMode
+ ? cursorMin(vim.sel.anchor, vim.sel.head, ranges[0].head, ranges[0].anchor)
+ : oldAnchor;
+ vimGlobalState.registerController.pushText(
+ args.registerName, 'yank',
+ text, args.linewise, vim.visualBlock);
+ return endPos;
+ }
+ };
+
+ function defineOperator(name, fn) {
+ operators[name] = fn;
+ }
+
+ var actions = {
+ jumpListWalk: function(cm, actionArgs, vim) {
+ if (vim.visualMode) {
+ return;
+ }
+ var repeat = actionArgs.repeat;
+ var forward = actionArgs.forward;
+ var jumpList = vimGlobalState.jumpList;
+
+ var mark = jumpList.move(cm, forward ? repeat : -repeat);
+ var markPos = mark ? mark.find() : undefined;
+ markPos = markPos ? markPos : cm.getCursor();
+ cm.setCursor(markPos);
+ },
+ scroll: function(cm, actionArgs, vim) {
+ if (vim.visualMode) {
+ return;
+ }
+ var repeat = actionArgs.repeat || 1;
+ var lineHeight = cm.defaultTextHeight();
+ var top = cm.getScrollInfo().top;
+ var delta = lineHeight * repeat;
+ var newPos = actionArgs.forward ? top + delta : top - delta;
+ var cursor = copyCursor(cm.getCursor());
+ var cursorCoords = cm.charCoords(cursor, 'local');
+ if (actionArgs.forward) {
+ if (newPos > cursorCoords.top) {
+ cursor.line += (newPos - cursorCoords.top) / lineHeight;
+ cursor.line = Math.ceil(cursor.line);
+ cm.setCursor(cursor);
+ cursorCoords = cm.charCoords(cursor, 'local');
+ cm.scrollTo(null, cursorCoords.top);
+ } else {
+ // Cursor stays within bounds. Just reposition the scroll window.
+ cm.scrollTo(null, newPos);
+ }
+ } else {
+ var newBottom = newPos + cm.getScrollInfo().clientHeight;
+ if (newBottom < cursorCoords.bottom) {
+ cursor.line -= (cursorCoords.bottom - newBottom) / lineHeight;
+ cursor.line = Math.floor(cursor.line);
+ cm.setCursor(cursor);
+ cursorCoords = cm.charCoords(cursor, 'local');
+ cm.scrollTo(
+ null, cursorCoords.bottom - cm.getScrollInfo().clientHeight);
+ } else {
+ // Cursor stays within bounds. Just reposition the scroll window.
+ cm.scrollTo(null, newPos);
+ }
+ }
+ },
+ scrollToCursor: function(cm, actionArgs) {
+ var lineNum = cm.getCursor().line;
+ var charCoords = cm.charCoords(Pos(lineNum, 0), 'local');
+ var height = cm.getScrollInfo().clientHeight;
+ var y = charCoords.top;
+ var lineHeight = charCoords.bottom - y;
+ switch (actionArgs.position) {
+ case 'center': y = y - (height / 2) + lineHeight;
+ break;
+ case 'bottom': y = y - height + lineHeight;
+ break;
+ }
+ cm.scrollTo(null, y);
+ },
+ replayMacro: function(cm, actionArgs, vim) {
+ var registerName = actionArgs.selectedCharacter;
+ var repeat = actionArgs.repeat;
+ var macroModeState = vimGlobalState.macroModeState;
+ if (registerName == '@') {
+ registerName = macroModeState.latestRegister;
+ }
+ while(repeat--){
+ executeMacroRegister(cm, vim, macroModeState, registerName);
+ }
+ },
+ enterMacroRecordMode: function(cm, actionArgs) {
+ var macroModeState = vimGlobalState.macroModeState;
+ var registerName = actionArgs.selectedCharacter;
+ macroModeState.enterMacroRecordMode(cm, registerName);
+ },
+ enterInsertMode: function(cm, actionArgs, vim) {
+ if (cm.getOption('readOnly')) { return; }
+ vim.insertMode = true;
+ vim.insertModeRepeat = actionArgs && actionArgs.repeat || 1;
+ var insertAt = (actionArgs) ? actionArgs.insertAt : null;
+ var sel = vim.sel;
+ var head = actionArgs.head || cm.getCursor('head');
+ var height = cm.listSelections().length;
+ if (insertAt == 'eol') {
+ head = Pos(head.line, lineLength(cm, head.line));
+ } else if (insertAt == 'charAfter') {
+ head = offsetCursor(head, 0, 1);
+ } else if (insertAt == 'firstNonBlank') {
+ head = motions.moveToFirstNonWhiteSpaceCharacter(cm, head);
+ } else if (insertAt == 'startOfSelectedArea') {
+ if (!vim.visualBlock) {
+ if (sel.head.line < sel.anchor.line) {
+ head = sel.head;
+ } else {
+ head = Pos(sel.anchor.line, 0);
+ }
+ } else {
+ head = Pos(
+ Math.min(sel.head.line, sel.anchor.line),
+ Math.min(sel.head.ch, sel.anchor.ch));
+ height = Math.abs(sel.head.line - sel.anchor.line) + 1;
+ }
+ } else if (insertAt == 'endOfSelectedArea') {
+ if (!vim.visualBlock) {
+ if (sel.head.line >= sel.anchor.line) {
+ head = offsetCursor(sel.head, 0, 1);
+ } else {
+ head = Pos(sel.anchor.line, 0);
+ }
+ } else {
+ head = Pos(
+ Math.min(sel.head.line, sel.anchor.line),
+ Math.max(sel.head.ch + 1, sel.anchor.ch));
+ height = Math.abs(sel.head.line - sel.anchor.line) + 1;
+ }
+ } else if (insertAt == 'inplace') {
+ if (vim.visualMode){
+ return;
+ }
+ }
+ cm.setOption('keyMap', 'vim-insert');
+ cm.setOption('disableInput', false);
+ if (actionArgs && actionArgs.replace) {
+ // Handle Replace-mode as a special case of insert mode.
+ cm.toggleOverwrite(true);
+ cm.setOption('keyMap', 'vim-replace');
+ CodeMirror.signal(cm, "vim-mode-change", {mode: "replace"});
+ } else {
+ cm.setOption('keyMap', 'vim-insert');
+ CodeMirror.signal(cm, "vim-mode-change", {mode: "insert"});
+ }
+ if (!vimGlobalState.macroModeState.isPlaying) {
+ // Only record if not replaying.
+ cm.on('change', onChange);
+ CodeMirror.on(cm.getInputField(), 'keydown', onKeyEventTargetKeyDown);
+ }
+ if (vim.visualMode) {
+ exitVisualMode(cm);
+ }
+ selectForInsert(cm, head, height);
+ },
+ toggleVisualMode: function(cm, actionArgs, vim) {
+ var repeat = actionArgs.repeat;
+ var anchor = cm.getCursor();
+ var head;
+ // TODO: The repeat should actually select number of characters/lines
+ // equal to the repeat times the size of the previous visual
+ // operation.
+ if (!vim.visualMode) {
+ // Entering visual mode
+ vim.visualMode = true;
+ vim.visualLine = !!actionArgs.linewise;
+ vim.visualBlock = !!actionArgs.blockwise;
+ head = clipCursorToContent(
+ cm, Pos(anchor.line, anchor.ch + repeat - 1),
+ true /** includeLineBreak */);
+ vim.sel = {
+ anchor: anchor,
+ head: head
+ };
+ CodeMirror.signal(cm, "vim-mode-change", {mode: "visual", subMode: vim.visualLine ? "linewise" : vim.visualBlock ? "blockwise" : ""});
+ updateCmSelection(cm);
+ updateMark(cm, vim, '<', cursorMin(anchor, head));
+ updateMark(cm, vim, '>', cursorMax(anchor, head));
+ } else if (vim.visualLine ^ actionArgs.linewise ||
+ vim.visualBlock ^ actionArgs.blockwise) {
+ // Toggling between modes
+ vim.visualLine = !!actionArgs.linewise;
+ vim.visualBlock = !!actionArgs.blockwise;
+ CodeMirror.signal(cm, "vim-mode-change", {mode: "visual", subMode: vim.visualLine ? "linewise" : vim.visualBlock ? "blockwise" : ""});
+ updateCmSelection(cm);
+ } else {
+ exitVisualMode(cm);
+ }
+ },
+ reselectLastSelection: function(cm, _actionArgs, vim) {
+ var lastSelection = vim.lastSelection;
+ if (vim.visualMode) {
+ updateLastSelection(cm, vim);
+ }
+ if (lastSelection) {
+ var anchor = lastSelection.anchorMark.find();
+ var head = lastSelection.headMark.find();
+ if (!anchor || !head) {
+ // If the marks have been destroyed due to edits, do nothing.
+ return;
+ }
+ vim.sel = {
+ anchor: anchor,
+ head: head
+ };
+ vim.visualMode = true;
+ vim.visualLine = lastSelection.visualLine;
+ vim.visualBlock = lastSelection.visualBlock;
+ updateCmSelection(cm);
+ updateMark(cm, vim, '<', cursorMin(anchor, head));
+ updateMark(cm, vim, '>', cursorMax(anchor, head));
+ CodeMirror.signal(cm, 'vim-mode-change', {
+ mode: 'visual',
+ subMode: vim.visualLine ? 'linewise' :
+ vim.visualBlock ? 'blockwise' : ''});
+ }
+ },
+ joinLines: function(cm, actionArgs, vim) {
+ var curStart, curEnd;
+ if (vim.visualMode) {
+ curStart = cm.getCursor('anchor');
+ curEnd = cm.getCursor('head');
+ if (cursorIsBefore(curEnd, curStart)) {
+ var tmp = curEnd;
+ curEnd = curStart;
+ curStart = tmp;
+ }
+ curEnd.ch = lineLength(cm, curEnd.line) - 1;
+ } else {
+ // Repeat is the number of lines to join. Minimum 2 lines.
+ var repeat = Math.max(actionArgs.repeat, 2);
+ curStart = cm.getCursor();
+ curEnd = clipCursorToContent(cm, Pos(curStart.line + repeat - 1,
+ Infinity));
+ }
+ var finalCh = 0;
+ for (var i = curStart.line; i < curEnd.line; i++) {
+ finalCh = lineLength(cm, curStart.line);
+ var tmp = Pos(curStart.line + 1,
+ lineLength(cm, curStart.line + 1));
+ var text = cm.getRange(curStart, tmp);
+ text = text.replace(/\n\s*/g, ' ');
+ cm.replaceRange(text, curStart, tmp);
+ }
+ var curFinalPos = Pos(curStart.line, finalCh);
+ if (vim.visualMode) {
+ exitVisualMode(cm, false);
+ }
+ cm.setCursor(curFinalPos);
+ },
+ newLineAndEnterInsertMode: function(cm, actionArgs, vim) {
+ vim.insertMode = true;
+ var insertAt = copyCursor(cm.getCursor());
+ if (insertAt.line === cm.firstLine() && !actionArgs.after) {
+ // Special case for inserting newline before start of document.
+ cm.replaceRange('\n', Pos(cm.firstLine(), 0));
+ cm.setCursor(cm.firstLine(), 0);
+ } else {
+ insertAt.line = (actionArgs.after) ? insertAt.line :
+ insertAt.line - 1;
+ insertAt.ch = lineLength(cm, insertAt.line);
+ cm.setCursor(insertAt);
+ var newlineFn = CodeMirror.commands.newlineAndIndentContinueComment ||
+ CodeMirror.commands.newlineAndIndent;
+ newlineFn(cm);
+ }
+ this.enterInsertMode(cm, { repeat: actionArgs.repeat }, vim);
+ },
+ paste: function(cm, actionArgs, vim) {
+ var cur = copyCursor(cm.getCursor());
+ var register = vimGlobalState.registerController.getRegister(
+ actionArgs.registerName);
+ var text = register.toString();
+ if (!text) {
+ return;
+ }
+ if (actionArgs.matchIndent) {
+ var tabSize = cm.getOption("tabSize");
+ // length that considers tabs and tabSize
+ var whitespaceLength = function(str) {
+ var tabs = (str.split("\t").length - 1);
+ var spaces = (str.split(" ").length - 1);
+ return tabs * tabSize + spaces * 1;
+ };
+ var currentLine = cm.getLine(cm.getCursor().line);
+ var indent = whitespaceLength(currentLine.match(/^\s*/)[0]);
+ // chomp last newline b/c don't want it to match /^\s*/gm
+ var chompedText = text.replace(/\n$/, '');
+ var wasChomped = text !== chompedText;
+ var firstIndent = whitespaceLength(text.match(/^\s*/)[0]);
+ var text = chompedText.replace(/^\s*/gm, function(wspace) {
+ var newIndent = indent + (whitespaceLength(wspace) - firstIndent);
+ if (newIndent < 0) {
+ return "";
+ }
+ else if (cm.getOption("indentWithTabs")) {
+ var quotient = Math.floor(newIndent / tabSize);
+ return Array(quotient + 1).join('\t');
+ }
+ else {
+ return Array(newIndent + 1).join(' ');
+ }
+ });
+ text += wasChomped ? "\n" : "";
+ }
+ if (actionArgs.repeat > 1) {
+ var text = Array(actionArgs.repeat + 1).join(text);
+ }
+ var linewise = register.linewise;
+ var blockwise = register.blockwise;
+ if (linewise) {
+ if(vim.visualMode) {
+ text = vim.visualLine ? text.slice(0, -1) : '\n' + text.slice(0, text.length - 1) + '\n';
+ } else if (actionArgs.after) {
+ // Move the newline at the end to the start instead, and paste just
+ // before the newline character of the line we are on right now.
+ text = '\n' + text.slice(0, text.length - 1);
+ cur.ch = lineLength(cm, cur.line);
+ } else {
+ cur.ch = 0;
+ }
+ } else {
+ if (blockwise) {
+ text = text.split('\n');
+ for (var i = 0; i < text.length; i++) {
+ text[i] = (text[i] == '') ? ' ' : text[i];
+ }
+ }
+ cur.ch += actionArgs.after ? 1 : 0;
+ }
+ var curPosFinal;
+ var idx;
+ if (vim.visualMode) {
+ // save the pasted text for reselection if the need arises
+ vim.lastPastedText = text;
+ var lastSelectionCurEnd;
+ var selectedArea = getSelectedAreaRange(cm, vim);
+ var selectionStart = selectedArea[0];
+ var selectionEnd = selectedArea[1];
+ var selectedText = cm.getSelection();
+ var selections = cm.listSelections();
+ var emptyStrings = new Array(selections.length).join('1').split('1');
+ // save the curEnd marker before it get cleared due to cm.replaceRange.
+ if (vim.lastSelection) {
+ lastSelectionCurEnd = vim.lastSelection.headMark.find();
+ }
+ // push the previously selected text to unnamed register
+ vimGlobalState.registerController.unnamedRegister.setText(selectedText);
+ if (blockwise) {
+ // first delete the selected text
+ cm.replaceSelections(emptyStrings);
+ // Set new selections as per the block length of the yanked text
+ selectionEnd = Pos(selectionStart.line + text.length-1, selectionStart.ch);
+ cm.setCursor(selectionStart);
+ selectBlock(cm, selectionEnd);
+ cm.replaceSelections(text);
+ curPosFinal = selectionStart;
+ } else if (vim.visualBlock) {
+ cm.replaceSelections(emptyStrings);
+ cm.setCursor(selectionStart);
+ cm.replaceRange(text, selectionStart, selectionStart);
+ curPosFinal = selectionStart;
+ } else {
+ cm.replaceRange(text, selectionStart, selectionEnd);
+ curPosFinal = cm.posFromIndex(cm.indexFromPos(selectionStart) + text.length - 1);
+ }
+ // restore the the curEnd marker
+ if(lastSelectionCurEnd) {
+ vim.lastSelection.headMark = cm.setBookmark(lastSelectionCurEnd);
+ }
+ if (linewise) {
+ curPosFinal.ch=0;
+ }
+ } else {
+ if (blockwise) {
+ cm.setCursor(cur);
+ for (var i = 0; i < text.length; i++) {
+ var line = cur.line+i;
+ if (line > cm.lastLine()) {
+ cm.replaceRange('\n', Pos(line, 0));
+ }
+ var lastCh = lineLength(cm, line);
+ if (lastCh < cur.ch) {
+ extendLineToColumn(cm, line, cur.ch);
+ }
+ }
+ cm.setCursor(cur);
+ selectBlock(cm, Pos(cur.line + text.length-1, cur.ch));
+ cm.replaceSelections(text);
+ curPosFinal = cur;
+ } else {
+ cm.replaceRange(text, cur);
+ // Now fine tune the cursor to where we want it.
+ if (linewise && actionArgs.after) {
+ curPosFinal = Pos(
+ cur.line + 1,
+ findFirstNonWhiteSpaceCharacter(cm.getLine(cur.line + 1)));
+ } else if (linewise && !actionArgs.after) {
+ curPosFinal = Pos(
+ cur.line,
+ findFirstNonWhiteSpaceCharacter(cm.getLine(cur.line)));
+ } else if (!linewise && actionArgs.after) {
+ idx = cm.indexFromPos(cur);
+ curPosFinal = cm.posFromIndex(idx + text.length - 1);
+ } else {
+ idx = cm.indexFromPos(cur);
+ curPosFinal = cm.posFromIndex(idx + text.length);
+ }
+ }
+ }
+ if (vim.visualMode) {
+ exitVisualMode(cm, false);
+ }
+ cm.setCursor(curPosFinal);
+ },
+ undo: function(cm, actionArgs) {
+ cm.operation(function() {
+ repeatFn(cm, CodeMirror.commands.undo, actionArgs.repeat)();
+ cm.setCursor(cm.getCursor('anchor'));
+ });
+ },
+ redo: function(cm, actionArgs) {
+ repeatFn(cm, CodeMirror.commands.redo, actionArgs.repeat)();
+ },
+ setRegister: function(_cm, actionArgs, vim) {
+ vim.inputState.registerName = actionArgs.selectedCharacter;
+ },
+ setMark: function(cm, actionArgs, vim) {
+ var markName = actionArgs.selectedCharacter;
+ updateMark(cm, vim, markName, cm.getCursor());
+ },
+ replace: function(cm, actionArgs, vim) {
+ var replaceWith = actionArgs.selectedCharacter;
+ var curStart = cm.getCursor();
+ var replaceTo;
+ var curEnd;
+ var selections = cm.listSelections();
+ if (vim.visualMode) {
+ curStart = cm.getCursor('start');
+ curEnd = cm.getCursor('end');
+ } else {
+ var line = cm.getLine(curStart.line);
+ replaceTo = curStart.ch + actionArgs.repeat;
+ if (replaceTo > line.length) {
+ replaceTo=line.length;
+ }
+ curEnd = Pos(curStart.line, replaceTo);
+ }
+ if (replaceWith=='\n') {
+ if (!vim.visualMode) cm.replaceRange('', curStart, curEnd);
+ // special case, where vim help says to replace by just one line-break
+ (CodeMirror.commands.newlineAndIndentContinueComment || CodeMirror.commands.newlineAndIndent)(cm);
+ } else {
+ var replaceWithStr = cm.getRange(curStart, curEnd);
+ //replace all characters in range by selected, but keep linebreaks
+ replaceWithStr = replaceWithStr.replace(/[^\n]/g, replaceWith);
+ if (vim.visualBlock) {
+ // Tabs are split in visua block before replacing
+ var spaces = new Array(cm.getOption("tabSize")+1).join(' ');
+ replaceWithStr = cm.getSelection();
+ replaceWithStr = replaceWithStr.replace(/\t/g, spaces).replace(/[^\n]/g, replaceWith).split('\n');
+ cm.replaceSelections(replaceWithStr);
+ } else {
+ cm.replaceRange(replaceWithStr, curStart, curEnd);
+ }
+ if (vim.visualMode) {
+ curStart = cursorIsBefore(selections[0].anchor, selections[0].head) ?
+ selections[0].anchor : selections[0].head;
+ cm.setCursor(curStart);
+ exitVisualMode(cm, false);
+ } else {
+ cm.setCursor(offsetCursor(curEnd, 0, -1));
+ }
+ }
+ },
+ incrementNumberToken: function(cm, actionArgs) {
+ var cur = cm.getCursor();
+ var lineStr = cm.getLine(cur.line);
+ var re = /-?\d+/g;
+ var match;
+ var start;
+ var end;
+ var numberStr;
+ var token;
+ while ((match = re.exec(lineStr)) !== null) {
+ token = match[0];
+ start = match.index;
+ end = start + token.length;
+ if (cur.ch < end)break;
+ }
+ if (!actionArgs.backtrack && (end <= cur.ch))return;
+ if (token) {
+ var increment = actionArgs.increase ? 1 : -1;
+ var number = parseInt(token) + (increment * actionArgs.repeat);
+ var from = Pos(cur.line, start);
+ var to = Pos(cur.line, end);
+ numberStr = number.toString();
+ cm.replaceRange(numberStr, from, to);
+ } else {
+ return;
+ }
+ cm.setCursor(Pos(cur.line, start + numberStr.length - 1));
+ },
+ repeatLastEdit: function(cm, actionArgs, vim) {
+ var lastEditInputState = vim.lastEditInputState;
+ if (!lastEditInputState) { return; }
+ var repeat = actionArgs.repeat;
+ if (repeat && actionArgs.repeatIsExplicit) {
+ vim.lastEditInputState.repeatOverride = repeat;
+ } else {
+ repeat = vim.lastEditInputState.repeatOverride || repeat;
+ }
+ repeatLastEdit(cm, vim, repeat, false /** repeatForInsert */);
+ },
+ exitInsertMode: exitInsertMode
+ };
+
+ function defineAction(name, fn) {
+ actions[name] = fn;
+ }
+
+ /*
+ * Below are miscellaneous utility functions used by vim.js
+ */
+
+ /**
+ * Clips cursor to ensure that line is within the buffer's range
+ * If includeLineBreak is true, then allow cur.ch == lineLength.
+ */
+ function clipCursorToContent(cm, cur, includeLineBreak) {
+ var line = Math.min(Math.max(cm.firstLine(), cur.line), cm.lastLine() );
+ var maxCh = lineLength(cm, line) - 1;
+ maxCh = (includeLineBreak) ? maxCh + 1 : maxCh;
+ var ch = Math.min(Math.max(0, cur.ch), maxCh);
+ return Pos(line, ch);
+ }
+ function copyArgs(args) {
+ var ret = {};
+ for (var prop in args) {
+ if (args.hasOwnProperty(prop)) {
+ ret[prop] = args[prop];
+ }
+ }
+ return ret;
+ }
+ function offsetCursor(cur, offsetLine, offsetCh) {
+ if (typeof offsetLine === 'object') {
+ offsetCh = offsetLine.ch;
+ offsetLine = offsetLine.line;
+ }
+ return Pos(cur.line + offsetLine, cur.ch + offsetCh);
+ }
+ function getOffset(anchor, head) {
+ return {
+ line: head.line - anchor.line,
+ ch: head.line - anchor.line
+ };
+ }
+ function commandMatches(keys, keyMap, context, inputState) {
+ // Partial matches are not applied. They inform the key handler
+ // that the current key sequence is a subsequence of a valid key
+ // sequence, so that the key buffer is not cleared.
+ var match, partial = [], full = [];
+ for (var i = 0; i < keyMap.length; i++) {
+ var command = keyMap[i];
+ if (context == 'insert' && command.context != 'insert' ||
+ command.context && command.context != context ||
+ inputState.operator && command.type == 'action' ||
+ !(match = commandMatch(keys, command.keys))) { continue; }
+ if (match == 'partial') { partial.push(command); }
+ if (match == 'full') { full.push(command); }
+ }
+ return {
+ partial: partial.length && partial,
+ full: full.length && full
+ };
+ }
+ function commandMatch(pressed, mapped) {
+ if (mapped.slice(-11) == '<character>') {
+ // Last character matches anything.
+ var prefixLen = mapped.length - 11;
+ var pressedPrefix = pressed.slice(0, prefixLen);
+ var mappedPrefix = mapped.slice(0, prefixLen);
+ return pressedPrefix == mappedPrefix && pressed.length > prefixLen ? 'full' :
+ mappedPrefix.indexOf(pressedPrefix) == 0 ? 'partial' : false;
+ } else {
+ return pressed == mapped ? 'full' :
+ mapped.indexOf(pressed) == 0 ? 'partial' : false;
+ }
+ }
+ function lastChar(keys) {
+ var match = /^.*(<[\w\-]+>)$/.exec(keys);
+ var selectedCharacter = match ? match[1] : keys.slice(-1);
+ if (selectedCharacter.length > 1){
+ switch(selectedCharacter){
+ case '<CR>':
+ selectedCharacter='\n';
+ break;
+ case '<Space>':
+ selectedCharacter=' ';
+ break;
+ default:
+ break;
+ }
+ }
+ return selectedCharacter;
+ }
+ function repeatFn(cm, fn, repeat) {
+ return function() {
+ for (var i = 0; i < repeat; i++) {
+ fn(cm);
+ }
+ };
+ }
+ function copyCursor(cur) {
+ return Pos(cur.line, cur.ch);
+ }
+ function cursorEqual(cur1, cur2) {
+ return cur1.ch == cur2.ch && cur1.line == cur2.line;
+ }
+ function cursorIsBefore(cur1, cur2) {
+ if (cur1.line < cur2.line) {
+ return true;
+ }
+ if (cur1.line == cur2.line && cur1.ch < cur2.ch) {
+ return true;
+ }
+ return false;
+ }
+ function cursorMin(cur1, cur2) {
+ if (arguments.length > 2) {
+ cur2 = cursorMin.apply(undefined, Array.prototype.slice.call(arguments, 1));
+ }
+ return cursorIsBefore(cur1, cur2) ? cur1 : cur2;
+ }
+ function cursorMax(cur1, cur2) {
+ if (arguments.length > 2) {
+ cur2 = cursorMax.apply(undefined, Array.prototype.slice.call(arguments, 1));
+ }
+ return cursorIsBefore(cur1, cur2) ? cur2 : cur1;
+ }
+ function cursorIsBetween(cur1, cur2, cur3) {
+ // returns true if cur2 is between cur1 and cur3.
+ var cur1before2 = cursorIsBefore(cur1, cur2);
+ var cur2before3 = cursorIsBefore(cur2, cur3);
+ return cur1before2 && cur2before3;
+ }
+ function lineLength(cm, lineNum) {
+ return cm.getLine(lineNum).length;
+ }
+ function trim(s) {
+ if (s.trim) {
+ return s.trim();
+ }
+ return s.replace(/^\s+|\s+$/g, '');
+ }
+ function escapeRegex(s) {
+ return s.replace(/([.?*+$\[\]\/\\(){}|\-])/g, '\\$1');
+ }
+ function extendLineToColumn(cm, lineNum, column) {
+ var endCh = lineLength(cm, lineNum);
+ var spaces = new Array(column-endCh+1).join(' ');
+ cm.setCursor(Pos(lineNum, endCh));
+ cm.replaceRange(spaces, cm.getCursor());
+ }
+ // This functions selects a rectangular block
+ // of text with selectionEnd as any of its corner
+ // Height of block:
+ // Difference in selectionEnd.line and first/last selection.line
+ // Width of the block:
+ // Distance between selectionEnd.ch and any(first considered here) selection.ch
+ function selectBlock(cm, selectionEnd) {
+ var selections = [], ranges = cm.listSelections();
+ var head = copyCursor(cm.clipPos(selectionEnd));
+ var isClipped = !cursorEqual(selectionEnd, head);
+ var curHead = cm.getCursor('head');
+ var primIndex = getIndex(ranges, curHead);
+ var wasClipped = cursorEqual(ranges[primIndex].head, ranges[primIndex].anchor);
+ var max = ranges.length - 1;
+ var index = max - primIndex > primIndex ? max : 0;
+ var base = ranges[index].anchor;
+
+ var firstLine = Math.min(base.line, head.line);
+ var lastLine = Math.max(base.line, head.line);
+ var baseCh = base.ch, headCh = head.ch;
+
+ var dir = ranges[index].head.ch - baseCh;
+ var newDir = headCh - baseCh;
+ if (dir > 0 && newDir <= 0) {
+ baseCh++;
+ if (!isClipped) { headCh--; }
+ } else if (dir < 0 && newDir >= 0) {
+ baseCh--;
+ if (!wasClipped) { headCh++; }
+ } else if (dir < 0 && newDir == -1) {
+ baseCh--;
+ headCh++;
+ }
+ for (var line = firstLine; line <= lastLine; line++) {
+ var range = {anchor: new Pos(line, baseCh), head: new Pos(line, headCh)};
+ selections.push(range);
+ }
+ primIndex = head.line == lastLine ? selections.length - 1 : 0;
+ cm.setSelections(selections);
+ selectionEnd.ch = headCh;
+ base.ch = baseCh;
+ return base;
+ }
+ function selectForInsert(cm, head, height) {
+ var sel = [];
+ for (var i = 0; i < height; i++) {
+ var lineHead = offsetCursor(head, i, 0);
+ sel.push({anchor: lineHead, head: lineHead});
+ }
+ cm.setSelections(sel, 0);
+ }
+ // getIndex returns the index of the cursor in the selections.
+ function getIndex(ranges, cursor, end) {
+ for (var i = 0; i < ranges.length; i++) {
+ var atAnchor = end != 'head' && cursorEqual(ranges[i].anchor, cursor);
+ var atHead = end != 'anchor' && cursorEqual(ranges[i].head, cursor);
+ if (atAnchor || atHead) {
+ return i;
+ }
+ }
+ return -1;
+ }
+ function getSelectedAreaRange(cm, vim) {
+ var lastSelection = vim.lastSelection;
+ var getCurrentSelectedAreaRange = function() {
+ var selections = cm.listSelections();
+ var start = selections[0];
+ var end = selections[selections.length-1];
+ var selectionStart = cursorIsBefore(start.anchor, start.head) ? start.anchor : start.head;
+ var selectionEnd = cursorIsBefore(end.anchor, end.head) ? end.head : end.anchor;
+ return [selectionStart, selectionEnd];
+ };
+ var getLastSelectedAreaRange = function() {
+ var selectionStart = cm.getCursor();
+ var selectionEnd = cm.getCursor();
+ var block = lastSelection.visualBlock;
+ if (block) {
+ var width = block.width;
+ var height = block.height;
+ selectionEnd = Pos(selectionStart.line + height, selectionStart.ch + width);
+ var selections = [];
+ // selectBlock creates a 'proper' rectangular block.
+ // We do not want that in all cases, so we manually set selections.
+ for (var i = selectionStart.line; i < selectionEnd.line; i++) {
+ var anchor = Pos(i, selectionStart.ch);
+ var head = Pos(i, selectionEnd.ch);
+ var range = {anchor: anchor, head: head};
+ selections.push(range);
+ }
+ cm.setSelections(selections);
+ } else {
+ var start = lastSelection.anchorMark.find();
+ var end = lastSelection.headMark.find();
+ var line = end.line - start.line;
+ var ch = end.ch - start.ch;
+ selectionEnd = {line: selectionEnd.line + line, ch: line ? selectionEnd.ch : ch + selectionEnd.ch};
+ if (lastSelection.visualLine) {
+ selectionStart = Pos(selectionStart.line, 0);
+ selectionEnd = Pos(selectionEnd.line, lineLength(cm, selectionEnd.line));
+ }
+ cm.setSelection(selectionStart, selectionEnd);
+ }
+ return [selectionStart, selectionEnd];
+ };
+ if (!vim.visualMode) {
+ // In case of replaying the action.
+ return getLastSelectedAreaRange();
+ } else {
+ return getCurrentSelectedAreaRange();
+ }
+ }
+ // Updates the previous selection with the current selection's values. This
+ // should only be called in visual mode.
+ function updateLastSelection(cm, vim) {
+ var anchor = vim.sel.anchor;
+ var head = vim.sel.head;
+ // To accommodate the effect of lastPastedText in the last selection
+ if (vim.lastPastedText) {
+ head = cm.posFromIndex(cm.indexFromPos(anchor) + vim.lastPastedText.length);
+ vim.lastPastedText = null;
+ }
+ vim.lastSelection = {'anchorMark': cm.setBookmark(anchor),
+ 'headMark': cm.setBookmark(head),
+ 'anchor': copyCursor(anchor),
+ 'head': copyCursor(head),
+ 'visualMode': vim.visualMode,
+ 'visualLine': vim.visualLine,
+ 'visualBlock': vim.visualBlock};
+ }
+ function expandSelection(cm, start, end) {
+ var sel = cm.state.vim.sel;
+ var head = sel.head;
+ var anchor = sel.anchor;
+ var tmp;
+ if (cursorIsBefore(end, start)) {
+ tmp = end;
+ end = start;
+ start = tmp;
+ }
+ if (cursorIsBefore(head, anchor)) {
+ head = cursorMin(start, head);
+ anchor = cursorMax(anchor, end);
+ } else {
+ anchor = cursorMin(start, anchor);
+ head = cursorMax(head, end);
+ head = offsetCursor(head, 0, -1);
+ if (head.ch == -1 && head.line != cm.firstLine()) {
+ head = Pos(head.line - 1, lineLength(cm, head.line - 1));
+ }
+ }
+ return [anchor, head];
+ }
+ /**
+ * Updates the CodeMirror selection to match the provided vim selection.
+ * If no arguments are given, it uses the current vim selection state.
+ */
+ function updateCmSelection(cm, sel, mode) {
+ var vim = cm.state.vim;
+ sel = sel || vim.sel;
+ var mode = mode ||
+ vim.visualLine ? 'line' : vim.visualBlock ? 'block' : 'char';
+ var cmSel = makeCmSelection(cm, sel, mode);
+ cm.setSelections(cmSel.ranges, cmSel.primary);
+ updateFakeCursor(cm);
+ }
+ function makeCmSelection(cm, sel, mode, exclusive) {
+ var head = copyCursor(sel.head);
+ var anchor = copyCursor(sel.anchor);
+ if (mode == 'char') {
+ var headOffset = !exclusive && !cursorIsBefore(sel.head, sel.anchor) ? 1 : 0;
+ var anchorOffset = cursorIsBefore(sel.head, sel.anchor) ? 1 : 0;
+ head = offsetCursor(sel.head, 0, headOffset);
+ anchor = offsetCursor(sel.anchor, 0, anchorOffset);
+ return {
+ ranges: [{anchor: anchor, head: head}],
+ primary: 0
+ };
+ } else if (mode == 'line') {
+ if (!cursorIsBefore(sel.head, sel.anchor)) {
+ anchor.ch = 0;
+
+ var lastLine = cm.lastLine();
+ if (head.line > lastLine) {
+ head.line = lastLine;
+ }
+ head.ch = lineLength(cm, head.line);
+ } else {
+ head.ch = 0;
+ anchor.ch = lineLength(cm, anchor.line);
+ }
+ return {
+ ranges: [{anchor: anchor, head: head}],
+ primary: 0
+ };
+ } else if (mode == 'block') {
+ var top = Math.min(anchor.line, head.line),
+ left = Math.min(anchor.ch, head.ch),
+ bottom = Math.max(anchor.line, head.line),
+ right = Math.max(anchor.ch, head.ch) + 1;
+ var height = bottom - top + 1;
+ var primary = head.line == top ? 0 : height - 1;
+ var ranges = [];
+ for (var i = 0; i < height; i++) {
+ ranges.push({
+ anchor: Pos(top + i, left),
+ head: Pos(top + i, right)
+ });
+ }
+ return {
+ ranges: ranges,
+ primary: primary
+ };
+ }
+ }
+ function getHead(cm) {
+ var cur = cm.getCursor('head');
+ if (cm.getSelection().length == 1) {
+ // Small corner case when only 1 character is selected. The "real"
+ // head is the left of head and anchor.
+ cur = cursorMin(cur, cm.getCursor('anchor'));
+ }
+ return cur;
+ }
+
+ /**
+ * If moveHead is set to false, the CodeMirror selection will not be
+ * touched. The caller assumes the responsibility of putting the cursor
+ * in the right place.
+ */
+ function exitVisualMode(cm, moveHead) {
+ var vim = cm.state.vim;
+ if (moveHead !== false) {
+ cm.setCursor(clipCursorToContent(cm, vim.sel.head));
+ }
+ updateLastSelection(cm, vim);
+ vim.visualMode = false;
+ vim.visualLine = false;
+ vim.visualBlock = false;
+ CodeMirror.signal(cm, "vim-mode-change", {mode: "normal"});
+ if (vim.fakeCursor) {
+ vim.fakeCursor.clear();
+ }
+ }
+
+ // Remove any trailing newlines from the selection. For
+ // example, with the caret at the start of the last word on the line,
+ // 'dw' should word, but not the newline, while 'w' should advance the
+ // caret to the first character of the next line.
+ function clipToLine(cm, curStart, curEnd) {
+ var selection = cm.getRange(curStart, curEnd);
+ // Only clip if the selection ends with trailing newline + whitespace
+ if (/\n\s*$/.test(selection)) {
+ var lines = selection.split('\n');
+ // We know this is all whitespace.
+ lines.pop();
+
+ // Cases:
+ // 1. Last word is an empty line - do not clip the trailing '\n'
+ // 2. Last word is not an empty line - clip the trailing '\n'
+ var line;
+ // Find the line containing the last word, and clip all whitespace up
+ // to it.
+ for (var line = lines.pop(); lines.length > 0 && line && isWhiteSpaceString(line); line = lines.pop()) {
+ curEnd.line--;
+ curEnd.ch = 0;
+ }
+ // If the last word is not an empty line, clip an additional newline
+ if (line) {
+ curEnd.line--;
+ curEnd.ch = lineLength(cm, curEnd.line);
+ } else {
+ curEnd.ch = 0;
+ }
+ }
+ }
+
+ // Expand the selection to line ends.
+ function expandSelectionToLine(_cm, curStart, curEnd) {
+ curStart.ch = 0;
+ curEnd.ch = 0;
+ curEnd.line++;
+ }
+
+ function findFirstNonWhiteSpaceCharacter(text) {
+ if (!text) {
+ return 0;
+ }
+ var firstNonWS = text.search(/\S/);
+ return firstNonWS == -1 ? text.length : firstNonWS;
+ }
+
+ function expandWordUnderCursor(cm, inclusive, _forward, bigWord, noSymbol) {
+ var cur = getHead(cm);
+ var line = cm.getLine(cur.line);
+ var idx = cur.ch;
+
+ // Seek to first word or non-whitespace character, depending on if
+ // noSymbol is true.
+ var test = noSymbol ? wordCharTest[0] : bigWordCharTest [0];
+ while (!test(line.charAt(idx))) {
+ idx++;
+ if (idx >= line.length) { return null; }
+ }
+
+ if (bigWord) {
+ test = bigWordCharTest[0];
+ } else {
+ test = wordCharTest[0];
+ if (!test(line.charAt(idx))) {
+ test = wordCharTest[1];
+ }
+ }
+
+ var end = idx, start = idx;
+ while (test(line.charAt(end)) && end < line.length) { end++; }
+ while (test(line.charAt(start)) && start >= 0) { start--; }
+ start++;
+
+ if (inclusive) {
+ // If present, include all whitespace after word.
+ // Otherwise, include all whitespace before word, except indentation.
+ var wordEnd = end;
+ while (/\s/.test(line.charAt(end)) && end < line.length) { end++; }
+ if (wordEnd == end) {
+ var wordStart = start;
+ while (/\s/.test(line.charAt(start - 1)) && start > 0) { start--; }
+ if (!start) { start = wordStart; }
+ }
+ }
+ return { start: Pos(cur.line, start), end: Pos(cur.line, end) };
+ }
+
+ function recordJumpPosition(cm, oldCur, newCur) {
+ if (!cursorEqual(oldCur, newCur)) {
+ vimGlobalState.jumpList.add(cm, oldCur, newCur);
+ }
+ }
+
+ function recordLastCharacterSearch(increment, args) {
+ vimGlobalState.lastCharacterSearch.increment = increment;
+ vimGlobalState.lastCharacterSearch.forward = args.forward;
+ vimGlobalState.lastCharacterSearch.selectedCharacter = args.selectedCharacter;
+ }
+
+ var symbolToMode = {
+ '(': 'bracket', ')': 'bracket', '{': 'bracket', '}': 'bracket',
+ '[': 'section', ']': 'section',
+ '*': 'comment', '/': 'comment',
+ 'm': 'method', 'M': 'method',
+ '#': 'preprocess'
+ };
+ var findSymbolModes = {
+ bracket: {
+ isComplete: function(state) {
+ if (state.nextCh === state.symb) {
+ state.depth++;
+ if (state.depth >= 1)return true;
+ } else if (state.nextCh === state.reverseSymb) {
+ state.depth--;
+ }
+ return false;
+ }
+ },
+ section: {
+ init: function(state) {
+ state.curMoveThrough = true;
+ state.symb = (state.forward ? ']' : '[') === state.symb ? '{' : '}';
+ },
+ isComplete: function(state) {
+ return state.index === 0 && state.nextCh === state.symb;
+ }
+ },
+ comment: {
+ isComplete: function(state) {
+ var found = state.lastCh === '*' && state.nextCh === '/';
+ state.lastCh = state.nextCh;
+ return found;
+ }
+ },
+ // TODO: The original Vim implementation only operates on level 1 and 2.
+ // The current implementation doesn't check for code block level and
+ // therefore it operates on any levels.
+ method: {
+ init: function(state) {
+ state.symb = (state.symb === 'm' ? '{' : '}');
+ state.reverseSymb = state.symb === '{' ? '}' : '{';
+ },
+ isComplete: function(state) {
+ if (state.nextCh === state.symb)return true;
+ return false;
+ }
+ },
+ preprocess: {
+ init: function(state) {
+ state.index = 0;
+ },
+ isComplete: function(state) {
+ if (state.nextCh === '#') {
+ var token = state.lineText.match(/#(\w+)/)[1];
+ if (token === 'endif') {
+ if (state.forward && state.depth === 0) {
+ return true;
+ }
+ state.depth++;
+ } else if (token === 'if') {
+ if (!state.forward && state.depth === 0) {
+ return true;
+ }
+ state.depth--;
+ }
+ if (token === 'else' && state.depth === 0)return true;
+ }
+ return false;
+ }
+ }
+ };
+ function findSymbol(cm, repeat, forward, symb) {
+ var cur = copyCursor(cm.getCursor());
+ var increment = forward ? 1 : -1;
+ var endLine = forward ? cm.lineCount() : -1;
+ var curCh = cur.ch;
+ var line = cur.line;
+ var lineText = cm.getLine(line);
+ var state = {
+ lineText: lineText,
+ nextCh: lineText.charAt(curCh),
+ lastCh: null,
+ index: curCh,
+ symb: symb,
+ reverseSymb: (forward ? { ')': '(', '}': '{' } : { '(': ')', '{': '}' })[symb],
+ forward: forward,
+ depth: 0,
+ curMoveThrough: false
+ };
+ var mode = symbolToMode[symb];
+ if (!mode)return cur;
+ var init = findSymbolModes[mode].init;
+ var isComplete = findSymbolModes[mode].isComplete;
+ if (init) { init(state); }
+ while (line !== endLine && repeat) {
+ state.index += increment;
+ state.nextCh = state.lineText.charAt(state.index);
+ if (!state.nextCh) {
+ line += increment;
+ state.lineText = cm.getLine(line) || '';
+ if (increment > 0) {
+ state.index = 0;
+ } else {
+ var lineLen = state.lineText.length;
+ state.index = (lineLen > 0) ? (lineLen-1) : 0;
+ }
+ state.nextCh = state.lineText.charAt(state.index);
+ }
+ if (isComplete(state)) {
+ cur.line = line;
+ cur.ch = state.index;
+ repeat--;
+ }
+ }
+ if (state.nextCh || state.curMoveThrough) {
+ return Pos(line, state.index);
+ }
+ return cur;
+ }
+
+ /**
+ * Returns the boundaries of the next word. If the cursor in the middle of
+ * the word, then returns the boundaries of the current word, starting at
+ * the cursor. If the cursor is at the start/end of a word, and we are going
+ * forward/backward, respectively, find the boundaries of the next word.
+ *
+ * @param {CodeMirror} cm CodeMirror object.
+ * @param {Cursor} cur The cursor position.
+ * @param {boolean} forward True to search forward. False to search
+ * backward.
+ * @param {boolean} bigWord True if punctuation count as part of the word.
+ * False if only [a-zA-Z0-9] characters count as part of the word.
+ * @param {boolean} emptyLineIsWord True if empty lines should be treated
+ * as words.
+ * @return {Object{from:number, to:number, line: number}} The boundaries of
+ * the word, or null if there are no more words.
+ */
+ function findWord(cm, cur, forward, bigWord, emptyLineIsWord) {
+ var lineNum = cur.line;
+ var pos = cur.ch;
+ var line = cm.getLine(lineNum);
+ var dir = forward ? 1 : -1;
+ var charTests = bigWord ? bigWordCharTest: wordCharTest;
+
+ if (emptyLineIsWord && line == '') {
+ lineNum += dir;
+ line = cm.getLine(lineNum);
+ if (!isLine(cm, lineNum)) {
+ return null;
+ }
+ pos = (forward) ? 0 : line.length;
+ }
+
+ while (true) {
+ if (emptyLineIsWord && line == '') {
+ return { from: 0, to: 0, line: lineNum };
+ }
+ var stop = (dir > 0) ? line.length : -1;
+ var wordStart = stop, wordEnd = stop;
+ // Find bounds of next word.
+ while (pos != stop) {
+ var foundWord = false;
+ for (var i = 0; i < charTests.length && !foundWord; ++i) {
+ if (charTests[i](line.charAt(pos))) {
+ wordStart = pos;
+ // Advance to end of word.
+ while (pos != stop && charTests[i](line.charAt(pos))) {
+ pos += dir;
+ }
+ wordEnd = pos;
+ foundWord = wordStart != wordEnd;
+ if (wordStart == cur.ch && lineNum == cur.line &&
+ wordEnd == wordStart + dir) {
+ // We started at the end of a word. Find the next one.
+ continue;
+ } else {
+ return {
+ from: Math.min(wordStart, wordEnd + 1),
+ to: Math.max(wordStart, wordEnd),
+ line: lineNum };
+ }
+ }
+ }
+ if (!foundWord) {
+ pos += dir;
+ }
+ }
+ // Advance to next/prev line.
+ lineNum += dir;
+ if (!isLine(cm, lineNum)) {
+ return null;
+ }
+ line = cm.getLine(lineNum);
+ pos = (dir > 0) ? 0 : line.length;
+ }
+ }
+
+ /**
+ * @param {CodeMirror} cm CodeMirror object.
+ * @param {Pos} cur The position to start from.
+ * @param {int} repeat Number of words to move past.
+ * @param {boolean} forward True to search forward. False to search
+ * backward.
+ * @param {boolean} wordEnd True to move to end of word. False to move to
+ * beginning of word.
+ * @param {boolean} bigWord True if punctuation count as part of the word.
+ * False if only alphabet characters count as part of the word.
+ * @return {Cursor} The position the cursor should move to.
+ */
+ function moveToWord(cm, cur, repeat, forward, wordEnd, bigWord) {
+ var curStart = copyCursor(cur);
+ var words = [];
+ if (forward && !wordEnd || !forward && wordEnd) {
+ repeat++;
+ }
+ // For 'e', empty lines are not considered words, go figure.
+ var emptyLineIsWord = !(forward && wordEnd);
+ for (var i = 0; i < repeat; i++) {
+ var word = findWord(cm, cur, forward, bigWord, emptyLineIsWord);
+ if (!word) {
+ var eodCh = lineLength(cm, cm.lastLine());
+ words.push(forward
+ ? {line: cm.lastLine(), from: eodCh, to: eodCh}
+ : {line: 0, from: 0, to: 0});
+ break;
+ }
+ words.push(word);
+ cur = Pos(word.line, forward ? (word.to - 1) : word.from);
+ }
+ var shortCircuit = words.length != repeat;
+ var firstWord = words[0];
+ var lastWord = words.pop();
+ if (forward && !wordEnd) {
+ // w
+ if (!shortCircuit && (firstWord.from != curStart.ch || firstWord.line != curStart.line)) {
+ // We did not start in the middle of a word. Discard the extra word at the end.
+ lastWord = words.pop();
+ }
+ return Pos(lastWord.line, lastWord.from);
+ } else if (forward && wordEnd) {
+ return Pos(lastWord.line, lastWord.to - 1);
+ } else if (!forward && wordEnd) {
+ // ge
+ if (!shortCircuit && (firstWord.to != curStart.ch || firstWord.line != curStart.line)) {
+ // We did not start in the middle of a word. Discard the extra word at the end.
+ lastWord = words.pop();
+ }
+ return Pos(lastWord.line, lastWord.to);
+ } else {
+ // b
+ return Pos(lastWord.line, lastWord.from);
+ }
+ }
+
+ function moveToCharacter(cm, repeat, forward, character) {
+ var cur = cm.getCursor();
+ var start = cur.ch;
+ var idx;
+ for (var i = 0; i < repeat; i ++) {
+ var line = cm.getLine(cur.line);
+ idx = charIdxInLine(start, line, character, forward, true);
+ if (idx == -1) {
+ return null;
+ }
+ start = idx;
+ }
+ return Pos(cm.getCursor().line, idx);
+ }
+
+ function moveToColumn(cm, repeat) {
+ // repeat is always >= 1, so repeat - 1 always corresponds
+ // to the column we want to go to.
+ var line = cm.getCursor().line;
+ return clipCursorToContent(cm, Pos(line, repeat - 1));
+ }
+
+ function updateMark(cm, vim, markName, pos) {
+ if (!inArray(markName, validMarks)) {
+ return;
+ }
+ if (vim.marks[markName]) {
+ vim.marks[markName].clear();
+ }
+ vim.marks[markName] = cm.setBookmark(pos);
+ }
+
+ function charIdxInLine(start, line, character, forward, includeChar) {
+ // Search for char in line.
+ // motion_options: {forward, includeChar}
+ // If includeChar = true, include it too.
+ // If forward = true, search forward, else search backwards.
+ // If char is not found on this line, do nothing
+ var idx;
+ if (forward) {
+ idx = line.indexOf(character, start + 1);
+ if (idx != -1 && !includeChar) {
+ idx -= 1;
+ }
+ } else {
+ idx = line.lastIndexOf(character, start - 1);
+ if (idx != -1 && !includeChar) {
+ idx += 1;
+ }
+ }
+ return idx;
+ }
+
+ function findParagraph(cm, head, repeat, dir, inclusive) {
+ var line = head.line;
+ var min = cm.firstLine();
+ var max = cm.lastLine();
+ var start, end, i = line;
+ function isEmpty(i) { return !cm.getLine(i); }
+ function isBoundary(i, dir, any) {
+ if (any) { return isEmpty(i) != isEmpty(i + dir); }
+ return !isEmpty(i) && isEmpty(i + dir);
+ }
+ if (dir) {
+ while (min <= i && i <= max && repeat > 0) {
+ if (isBoundary(i, dir)) { repeat--; }
+ i += dir;
+ }
+ return new Pos(i, 0);
+ }
+
+ var vim = cm.state.vim;
+ if (vim.visualLine && isBoundary(line, 1, true)) {
+ var anchor = vim.sel.anchor;
+ if (isBoundary(anchor.line, -1, true)) {
+ if (!inclusive || anchor.line != line) {
+ line += 1;
+ }
+ }
+ }
+ var startState = isEmpty(line);
+ for (i = line; i <= max && repeat; i++) {
+ if (isBoundary(i, 1, true)) {
+ if (!inclusive || isEmpty(i) != startState) {
+ repeat--;
+ }
+ }
+ }
+ end = new Pos(i, 0);
+ // select boundary before paragraph for the last one
+ if (i > max && !startState) { startState = true; }
+ else { inclusive = false; }
+ for (i = line; i > min; i--) {
+ if (!inclusive || isEmpty(i) == startState || i == line) {
+ if (isBoundary(i, -1, true)) { break; }
+ }
+ }
+ start = new Pos(i, 0);
+ return { start: start, end: end };
+ }
+
+ // TODO: perhaps this finagling of start and end positions belonds
+ // in codemirror/replaceRange?
+ function selectCompanionObject(cm, head, symb, inclusive) {
+ var cur = head, start, end;
+
+ var bracketRegexp = ({
+ '(': /[()]/, ')': /[()]/,
+ '[': /[[\]]/, ']': /[[\]]/,
+ '{': /[{}]/, '}': /[{}]/})[symb];
+ var openSym = ({
+ '(': '(', ')': '(',
+ '[': '[', ']': '[',
+ '{': '{', '}': '{'})[symb];
+ var curChar = cm.getLine(cur.line).charAt(cur.ch);
+ // Due to the behavior of scanForBracket, we need to add an offset if the
+ // cursor is on a matching open bracket.
+ var offset = curChar === openSym ? 1 : 0;
+
+ start = cm.scanForBracket(Pos(cur.line, cur.ch + offset), -1, null, {'bracketRegex': bracketRegexp});
+ end = cm.scanForBracket(Pos(cur.line, cur.ch + offset), 1, null, {'bracketRegex': bracketRegexp});
+
+ if (!start || !end) {
+ return { start: cur, end: cur };
+ }
+
+ start = start.pos;
+ end = end.pos;
+
+ if ((start.line == end.line && start.ch > end.ch)
+ || (start.line > end.line)) {
+ var tmp = start;
+ start = end;
+ end = tmp;
+ }
+
+ if (inclusive) {
+ end.ch += 1;
+ } else {
+ start.ch += 1;
+ }
+
+ return { start: start, end: end };
+ }
+
+ // Takes in a symbol and a cursor and tries to simulate text objects that
+ // have identical opening and closing symbols
+ // TODO support across multiple lines
+ function findBeginningAndEnd(cm, head, symb, inclusive) {
+ var cur = copyCursor(head);
+ var line = cm.getLine(cur.line);
+ var chars = line.split('');
+ var start, end, i, len;
+ var firstIndex = chars.indexOf(symb);
+
+ // the decision tree is to always look backwards for the beginning first,
+ // but if the cursor is in front of the first instance of the symb,
+ // then move the cursor forward
+ if (cur.ch < firstIndex) {
+ cur.ch = firstIndex;
+ // Why is this line even here???
+ // cm.setCursor(cur.line, firstIndex+1);
+ }
+ // otherwise if the cursor is currently on the closing symbol
+ else if (firstIndex < cur.ch && chars[cur.ch] == symb) {
+ end = cur.ch; // assign end to the current cursor
+ --cur.ch; // make sure to look backwards
+ }
+
+ // if we're currently on the symbol, we've got a start
+ if (chars[cur.ch] == symb && !end) {
+ start = cur.ch + 1; // assign start to ahead of the cursor
+ } else {
+ // go backwards to find the start
+ for (i = cur.ch; i > -1 && !start; i--) {
+ if (chars[i] == symb) {
+ start = i + 1;
+ }
+ }
+ }
+
+ // look forwards for the end symbol
+ if (start && !end) {
+ for (i = start, len = chars.length; i < len && !end; i++) {
+ if (chars[i] == symb) {
+ end = i;
+ }
+ }
+ }
+
+ // nothing found
+ if (!start || !end) {
+ return { start: cur, end: cur };
+ }
+
+ // include the symbols
+ if (inclusive) {
+ --start; ++end;
+ }
+
+ return {
+ start: Pos(cur.line, start),
+ end: Pos(cur.line, end)
+ };
+ }
+
+ // Search functions
+ defineOption('pcre', true, 'boolean');
+ function SearchState() {}
+ SearchState.prototype = {
+ getQuery: function() {
+ return vimGlobalState.query;
+ },
+ setQuery: function(query) {
+ vimGlobalState.query = query;
+ },
+ getOverlay: function() {
+ return this.searchOverlay;
+ },
+ setOverlay: function(overlay) {
+ this.searchOverlay = overlay;
+ },
+ isReversed: function() {
+ return vimGlobalState.isReversed;
+ },
+ setReversed: function(reversed) {
+ vimGlobalState.isReversed = reversed;
+ },
+ getScrollbarAnnotate: function() {
+ return this.annotate;
+ },
+ setScrollbarAnnotate: function(annotate) {
+ this.annotate = annotate;
+ }
+ };
+ function getSearchState(cm) {
+ var vim = cm.state.vim;
+ return vim.searchState_ || (vim.searchState_ = new SearchState());
+ }
+ function dialog(cm, template, shortText, onClose, options) {
+ if (cm.openDialog) {
+ cm.openDialog(template, onClose, { bottom: true, value: options.value,
+ onKeyDown: options.onKeyDown, onKeyUp: options.onKeyUp,
+ selectValueOnOpen: false});
+ }
+ else {
+ onClose(prompt(shortText, ''));
+ }
+ }
+ function splitBySlash(argString) {
+ var slashes = findUnescapedSlashes(argString) || [];
+ if (!slashes.length) return [];
+ var tokens = [];
+ // in case of strings like foo/bar
+ if (slashes[0] !== 0) return;
+ for (var i = 0; i < slashes.length; i++) {
+ if (typeof slashes[i] == 'number')
+ tokens.push(argString.substring(slashes[i] + 1, slashes[i+1]));
+ }
+ return tokens;
+ }
+
+ function findUnescapedSlashes(str) {
+ var escapeNextChar = false;
+ var slashes = [];
+ for (var i = 0; i < str.length; i++) {
+ var c = str.charAt(i);
+ if (!escapeNextChar && c == '/') {
+ slashes.push(i);
+ }
+ escapeNextChar = !escapeNextChar && (c == '\\');
+ }
+ return slashes;
+ }
+
+ // Translates a search string from ex (vim) syntax into javascript form.
+ function translateRegex(str) {
+ // When these match, add a '\' if unescaped or remove one if escaped.
+ var specials = '|(){';
+ // Remove, but never add, a '\' for these.
+ var unescape = '}';
+ var escapeNextChar = false;
+ var out = [];
+ for (var i = -1; i < str.length; i++) {
+ var c = str.charAt(i) || '';
+ var n = str.charAt(i+1) || '';
+ var specialComesNext = (n && specials.indexOf(n) != -1);
+ if (escapeNextChar) {
+ if (c !== '\\' || !specialComesNext) {
+ out.push(c);
+ }
+ escapeNextChar = false;
+ } else {
+ if (c === '\\') {
+ escapeNextChar = true;
+ // Treat the unescape list as special for removing, but not adding '\'.
+ if (n && unescape.indexOf(n) != -1) {
+ specialComesNext = true;
+ }
+ // Not passing this test means removing a '\'.
+ if (!specialComesNext || n === '\\') {
+ out.push(c);
+ }
+ } else {
+ out.push(c);
+ if (specialComesNext && n !== '\\') {
+ out.push('\\');
+ }
+ }
+ }
+ }
+ return out.join('');
+ }
+
+ // Translates the replace part of a search and replace from ex (vim) syntax into
+ // javascript form. Similar to translateRegex, but additionally fixes back references
+ // (translates '\[0..9]' to '$[0..9]') and follows different rules for escaping '$'.
+ var charUnescapes = {'\\n': '\n', '\\r': '\r', '\\t': '\t'};
+ function translateRegexReplace(str) {
+ var escapeNextChar = false;
+ var out = [];
+ for (var i = -1; i < str.length; i++) {
+ var c = str.charAt(i) || '';
+ var n = str.charAt(i+1) || '';
+ if (charUnescapes[c + n]) {
+ out.push(charUnescapes[c+n]);
+ i++;
+ } else if (escapeNextChar) {
+ // At any point in the loop, escapeNextChar is true if the previous
+ // character was a '\' and was not escaped.
+ out.push(c);
+ escapeNextChar = false;
+ } else {
+ if (c === '\\') {
+ escapeNextChar = true;
+ if ((isNumber(n) || n === '$')) {
+ out.push('$');
+ } else if (n !== '/' && n !== '\\') {
+ out.push('\\');
+ }
+ } else {
+ if (c === '$') {
+ out.push('$');
+ }
+ out.push(c);
+ if (n === '/') {
+ out.push('\\');
+ }
+ }
+ }
+ }
+ return out.join('');
+ }
+
+ // Unescape \ and / in the replace part, for PCRE mode.
+ var unescapes = {'\\/': '/', '\\\\': '\\', '\\n': '\n', '\\r': '\r', '\\t': '\t'};
+ function unescapeRegexReplace(str) {
+ var stream = new CodeMirror.StringStream(str);
+ var output = [];
+ while (!stream.eol()) {
+ // Search for \.
+ while (stream.peek() && stream.peek() != '\\') {
+ output.push(stream.next());
+ }
+ var matched = false;
+ for (var matcher in unescapes) {
+ if (stream.match(matcher, true)) {
+ matched = true;
+ output.push(unescapes[matcher]);
+ break;
+ }
+ }
+ if (!matched) {
+ // Don't change anything
+ output.push(stream.next());
+ }
+ }
+ return output.join('');
+ }
+
+ /**
+ * Extract the regular expression from the query and return a Regexp object.
+ * Returns null if the query is blank.
+ * If ignoreCase is passed in, the Regexp object will have the 'i' flag set.
+ * If smartCase is passed in, and the query contains upper case letters,
+ * then ignoreCase is overridden, and the 'i' flag will not be set.
+ * If the query contains the /i in the flag part of the regular expression,
+ * then both ignoreCase and smartCase are ignored, and 'i' will be passed
+ * through to the Regex object.
+ */
+ function parseQuery(query, ignoreCase, smartCase) {
+ // First update the last search register
+ var lastSearchRegister = vimGlobalState.registerController.getRegister('/');
+ lastSearchRegister.setText(query);
+ // Check if the query is already a regex.
+ if (query instanceof RegExp) { return query; }
+ // First try to extract regex + flags from the input. If no flags found,
+ // extract just the regex. IE does not accept flags directly defined in
+ // the regex string in the form /regex/flags
+ var slashes = findUnescapedSlashes(query);
+ var regexPart;
+ var forceIgnoreCase;
+ if (!slashes.length) {
+ // Query looks like 'regexp'
+ regexPart = query;
+ } else {
+ // Query looks like 'regexp/...'
+ regexPart = query.substring(0, slashes[0]);
+ var flagsPart = query.substring(slashes[0]);
+ forceIgnoreCase = (flagsPart.indexOf('i') != -1);
+ }
+ if (!regexPart) {
+ return null;
+ }
+ if (!getOption('pcre')) {
+ regexPart = translateRegex(regexPart);
+ }
+ if (smartCase) {
+ ignoreCase = (/^[^A-Z]*$/).test(regexPart);
+ }
+ var regexp = new RegExp(regexPart,
+ (ignoreCase || forceIgnoreCase) ? 'i' : undefined);
+ return regexp;
+ }
+ function showConfirm(cm, text) {
+ if (cm.openNotification) {
+ cm.openNotification('<span style="color: red">' + text + '</span>',
+ {bottom: true, duration: 5000});
+ } else {
+ alert(text);
+ }
+ }
+ function makePrompt(prefix, desc) {
+ var raw = '<span style="font-family: monospace; white-space: pre">' +
+ (prefix || "") + '<input type="text"></span>';
+ if (desc)
+ raw += ' <span style="color: #888">' + desc + '</span>';
+ return raw;
+ }
+ var searchPromptDesc = '(Javascript regexp)';
+ function showPrompt(cm, options) {
+ var shortText = (options.prefix || '') + ' ' + (options.desc || '');
+ var prompt = makePrompt(options.prefix, options.desc);
+ dialog(cm, prompt, shortText, options.onClose, options);
+ }
+ function regexEqual(r1, r2) {
+ if (r1 instanceof RegExp && r2 instanceof RegExp) {
+ var props = ['global', 'multiline', 'ignoreCase', 'source'];
+ for (var i = 0; i < props.length; i++) {
+ var prop = props[i];
+ if (r1[prop] !== r2[prop]) {
+ return false;
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+ // Returns true if the query is valid.
+ function updateSearchQuery(cm, rawQuery, ignoreCase, smartCase) {
+ if (!rawQuery) {
+ return;
+ }
+ var state = getSearchState(cm);
+ var query = parseQuery(rawQuery, !!ignoreCase, !!smartCase);
+ if (!query) {
+ return;
+ }
+ highlightSearchMatches(cm, query);
+ if (regexEqual(query, state.getQuery())) {
+ return query;
+ }
+ state.setQuery(query);
+ return query;
+ }
+ function searchOverlay(query) {
+ if (query.source.charAt(0) == '^') {
+ var matchSol = true;
+ }
+ return {
+ token: function(stream) {
+ if (matchSol && !stream.sol()) {
+ stream.skipToEnd();
+ return;
+ }
+ var match = stream.match(query, false);
+ if (match) {
+ if (match[0].length == 0) {
+ // Matched empty string, skip to next.
+ stream.next();
+ return 'searching';
+ }
+ if (!stream.sol()) {
+ // Backtrack 1 to match \b
+ stream.backUp(1);
+ if (!query.exec(stream.next() + match[0])) {
+ stream.next();
+ return null;
+ }
+ }
+ stream.match(query);
+ return 'searching';
+ }
+ while (!stream.eol()) {
+ stream.next();
+ if (stream.match(query, false)) break;
+ }
+ },
+ query: query
+ };
+ }
+ function highlightSearchMatches(cm, query) {
+ var searchState = getSearchState(cm);
+ var overlay = searchState.getOverlay();
+ if (!overlay || query != overlay.query) {
+ if (overlay) {
+ cm.removeOverlay(overlay);
+ }
+ overlay = searchOverlay(query);
+ cm.addOverlay(overlay);
+ if (cm.showMatchesOnScrollbar) {
+ if (searchState.getScrollbarAnnotate()) {
+ searchState.getScrollbarAnnotate().clear();
+ }
+ searchState.setScrollbarAnnotate(cm.showMatchesOnScrollbar(query));
+ }
+ searchState.setOverlay(overlay);
+ }
+ }
+ function findNext(cm, prev, query, repeat) {
+ if (repeat === undefined) { repeat = 1; }
+ return cm.operation(function() {
+ var pos = cm.getCursor();
+ var cursor = cm.getSearchCursor(query, pos);
+ for (var i = 0; i < repeat; i++) {
+ var found = cursor.find(prev);
+ if (i == 0 && found && cursorEqual(cursor.from(), pos)) { found = cursor.find(prev); }
+ if (!found) {
+ // SearchCursor may have returned null because it hit EOF, wrap
+ // around and try again.
+ cursor = cm.getSearchCursor(query,
+ (prev) ? Pos(cm.lastLine()) : Pos(cm.firstLine(), 0) );
+ if (!cursor.find(prev)) {
+ return;
+ }
+ }
+ }
+ return cursor.from();
+ });
+ }
+ function clearSearchHighlight(cm) {
+ var state = getSearchState(cm);
+ cm.removeOverlay(getSearchState(cm).getOverlay());
+ state.setOverlay(null);
+ if (state.getScrollbarAnnotate()) {
+ state.getScrollbarAnnotate().clear();
+ state.setScrollbarAnnotate(null);
+ }
+ }
+ /**
+ * Check if pos is in the specified range, INCLUSIVE.
+ * Range can be specified with 1 or 2 arguments.
+ * If the first range argument is an array, treat it as an array of line
+ * numbers. Match pos against any of the lines.
+ * If the first range argument is a number,
+ * if there is only 1 range argument, check if pos has the same line
+ * number
+ * if there are 2 range arguments, then check if pos is in between the two
+ * range arguments.
+ */
+ function isInRange(pos, start, end) {
+ if (typeof pos != 'number') {
+ // Assume it is a cursor position. Get the line number.
+ pos = pos.line;
+ }
+ if (start instanceof Array) {
+ return inArray(pos, start);
+ } else {
+ if (end) {
+ return (pos >= start && pos <= end);
+ } else {
+ return pos == start;
+ }
+ }
+ }
+ function getUserVisibleLines(cm) {
+ var scrollInfo = cm.getScrollInfo();
+ var occludeToleranceTop = 6;
+ var occludeToleranceBottom = 10;
+ var from = cm.coordsChar({left:0, top: occludeToleranceTop + scrollInfo.top}, 'local');
+ var bottomY = scrollInfo.clientHeight - occludeToleranceBottom + scrollInfo.top;
+ var to = cm.coordsChar({left:0, top: bottomY}, 'local');
+ return {top: from.line, bottom: to.line};
+ }
+
+ var ExCommandDispatcher = function() {
+ this.buildCommandMap_();
+ };
+ ExCommandDispatcher.prototype = {
+ processCommand: function(cm, input, opt_params) {
+ var that = this;
+ cm.operation(function () {
+ cm.curOp.isVimOp = true;
+ that._processCommand(cm, input, opt_params);
+ });
+ },
+ _processCommand: function(cm, input, opt_params) {
+ var vim = cm.state.vim;
+ var commandHistoryRegister = vimGlobalState.registerController.getRegister(':');
+ var previousCommand = commandHistoryRegister.toString();
+ if (vim.visualMode) {
+ exitVisualMode(cm);
+ }
+ var inputStream = new CodeMirror.StringStream(input);
+ // update ": with the latest command whether valid or invalid
+ commandHistoryRegister.setText(input);
+ var params = opt_params || {};
+ params.input = input;
+ try {
+ this.parseInput_(cm, inputStream, params);
+ } catch(e) {
+ showConfirm(cm, e);
+ throw e;
+ }
+ var command;
+ var commandName;
+ if (!params.commandName) {
+ // If only a line range is defined, move to the line.
+ if (params.line !== undefined) {
+ commandName = 'move';
+ }
+ } else {
+ command = this.matchCommand_(params.commandName);
+ if (command) {
+ commandName = command.name;
+ if (command.excludeFromCommandHistory) {
+ commandHistoryRegister.setText(previousCommand);
+ }
+ this.parseCommandArgs_(inputStream, params, command);
+ if (command.type == 'exToKey') {
+ // Handle Ex to Key mapping.
+ for (var i = 0; i < command.toKeys.length; i++) {
+ CodeMirror.Vim.handleKey(cm, command.toKeys[i], 'mapping');
+ }
+ return;
+ } else if (command.type == 'exToEx') {
+ // Handle Ex to Ex mapping.
+ this.processCommand(cm, command.toInput);
+ return;
+ }
+ }
+ }
+ if (!commandName) {
+ showConfirm(cm, 'Not an editor command ":' + input + '"');
+ return;
+ }
+ try {
+ exCommands[commandName](cm, params);
+ // Possibly asynchronous commands (e.g. substitute, which might have a
+ // user confirmation), are responsible for calling the callback when
+ // done. All others have it taken care of for them here.
+ if ((!command || !command.possiblyAsync) && params.callback) {
+ params.callback();
+ }
+ } catch(e) {
+ showConfirm(cm, e);
+ throw e;
+ }
+ },
+ parseInput_: function(cm, inputStream, result) {
+ inputStream.eatWhile(':');
+ // Parse range.
+ if (inputStream.eat('%')) {
+ result.line = cm.firstLine();
+ result.lineEnd = cm.lastLine();
+ } else {
+ result.line = this.parseLineSpec_(cm, inputStream);
+ if (result.line !== undefined && inputStream.eat(',')) {
+ result.lineEnd = this.parseLineSpec_(cm, inputStream);
+ }
+ }
+
+ // Parse command name.
+ var commandMatch = inputStream.match(/^(\w+)/);
+ if (commandMatch) {
+ result.commandName = commandMatch[1];
+ } else {
+ result.commandName = inputStream.match(/.*/)[0];
+ }
+
+ return result;
+ },
+ parseLineSpec_: function(cm, inputStream) {
+ var numberMatch = inputStream.match(/^(\d+)/);
+ if (numberMatch) {
+ return parseInt(numberMatch[1], 10) - 1;
+ }
+ switch (inputStream.next()) {
+ case '.':
+ return cm.getCursor().line;
+ case '$':
+ return cm.lastLine();
+ case '\'':
+ var mark = cm.state.vim.marks[inputStream.next()];
+ if (mark && mark.find()) {
+ return mark.find().line;
+ }
+ throw new Error('Mark not set');
+ default:
+ inputStream.backUp(1);
+ return undefined;
+ }
+ },
+ parseCommandArgs_: function(inputStream, params, command) {
+ if (inputStream.eol()) {
+ return;
+ }
+ params.argString = inputStream.match(/.*/)[0];
+ // Parse command-line arguments
+ var delim = command.argDelimiter || /\s+/;
+ var args = trim(params.argString).split(delim);
+ if (args.length && args[0]) {
+ params.args = args;
+ }
+ },
+ matchCommand_: function(commandName) {
+ // Return the command in the command map that matches the shortest
+ // prefix of the passed in command name. The match is guaranteed to be
+ // unambiguous if the defaultExCommandMap's shortNames are set up
+ // correctly. (see @code{defaultExCommandMap}).
+ for (var i = commandName.length; i > 0; i--) {
+ var prefix = commandName.substring(0, i);
+ if (this.commandMap_[prefix]) {
+ var command = this.commandMap_[prefix];
+ if (command.name.indexOf(commandName) === 0) {
+ return command;
+ }
+ }
+ }
+ return null;
+ },
+ buildCommandMap_: function() {
+ this.commandMap_ = {};
+ for (var i = 0; i < defaultExCommandMap.length; i++) {
+ var command = defaultExCommandMap[i];
+ var key = command.shortName || command.name;
+ this.commandMap_[key] = command;
+ }
+ },
+ map: function(lhs, rhs, ctx) {
+ if (lhs != ':' && lhs.charAt(0) == ':') {
+ if (ctx) { throw Error('Mode not supported for ex mappings'); }
+ var commandName = lhs.substring(1);
+ if (rhs != ':' && rhs.charAt(0) == ':') {
+ // Ex to Ex mapping
+ this.commandMap_[commandName] = {
+ name: commandName,
+ type: 'exToEx',
+ toInput: rhs.substring(1),
+ user: true
+ };
+ } else {
+ // Ex to key mapping
+ this.commandMap_[commandName] = {
+ name: commandName,
+ type: 'exToKey',
+ toKeys: rhs,
+ user: true
+ };
+ }
+ } else {
+ if (rhs != ':' && rhs.charAt(0) == ':') {
+ // Key to Ex mapping.
+ var mapping = {
+ keys: lhs,
+ type: 'keyToEx',
+ exArgs: { input: rhs.substring(1) },
+ user: true};
+ if (ctx) { mapping.context = ctx; }
+ defaultKeymap.unshift(mapping);
+ } else {
+ // Key to key mapping
+ var mapping = {
+ keys: lhs,
+ type: 'keyToKey',
+ toKeys: rhs,
+ user: true
+ };
+ if (ctx) { mapping.context = ctx; }
+ defaultKeymap.unshift(mapping);
+ }
+ }
+ },
+ unmap: function(lhs, ctx) {
+ if (lhs != ':' && lhs.charAt(0) == ':') {
+ // Ex to Ex or Ex to key mapping
+ if (ctx) { throw Error('Mode not supported for ex mappings'); }
+ var commandName = lhs.substring(1);
+ if (this.commandMap_[commandName] && this.commandMap_[commandName].user) {
+ delete this.commandMap_[commandName];
+ return;
+ }
+ } else {
+ // Key to Ex or key to key mapping
+ var keys = lhs;
+ for (var i = 0; i < defaultKeymap.length; i++) {
+ if (keys == defaultKeymap[i].keys
+ && defaultKeymap[i].context === ctx
+ && defaultKeymap[i].user) {
+ defaultKeymap.splice(i, 1);
+ return;
+ }
+ }
+ }
+ throw Error('No such mapping.');
+ }
+ };
+
+ var exCommands = {
+ colorscheme: function(cm, params) {
+ if (!params.args || params.args.length < 1) {
+ showConfirm(cm, cm.getOption('theme'));
+ return;
+ }
+ cm.setOption('theme', params.args[0]);
+ },
+ map: function(cm, params, ctx) {
+ var mapArgs = params.args;
+ if (!mapArgs || mapArgs.length < 2) {
+ if (cm) {
+ showConfirm(cm, 'Invalid mapping: ' + params.input);
+ }
+ return;
+ }
+ exCommandDispatcher.map(mapArgs[0], mapArgs[1], ctx);
+ },
+ imap: function(cm, params) { this.map(cm, params, 'insert'); },
+ nmap: function(cm, params) { this.map(cm, params, 'normal'); },
+ vmap: function(cm, params) { this.map(cm, params, 'visual'); },
+ unmap: function(cm, params, ctx) {
+ var mapArgs = params.args;
+ if (!mapArgs || mapArgs.length < 1) {
+ if (cm) {
+ showConfirm(cm, 'No such mapping: ' + params.input);
+ }
+ return;
+ }
+ exCommandDispatcher.unmap(mapArgs[0], ctx);
+ },
+ move: function(cm, params) {
+ commandDispatcher.processCommand(cm, cm.state.vim, {
+ type: 'motion',
+ motion: 'moveToLineOrEdgeOfDocument',
+ motionArgs: { forward: false, explicitRepeat: true,
+ linewise: true },
+ repeatOverride: params.line+1});
+ },
+ set: function(cm, params) {
+ var setArgs = params.args;
+ // Options passed through to the setOption/getOption calls. May be passed in by the
+ // local/global versions of the set command
+ var setCfg = params.setCfg || {};
+ if (!setArgs || setArgs.length < 1) {
+ if (cm) {
+ showConfirm(cm, 'Invalid mapping: ' + params.input);
+ }
+ return;
+ }
+ var expr = setArgs[0].split('=');
+ var optionName = expr[0];
+ var value = expr[1];
+ var forceGet = false;
+
+ if (optionName.charAt(optionName.length - 1) == '?') {
+ // If post-fixed with ?, then the set is actually a get.
+ if (value) { throw Error('Trailing characters: ' + params.argString); }
+ optionName = optionName.substring(0, optionName.length - 1);
+ forceGet = true;
+ }
+ if (value === undefined && optionName.substring(0, 2) == 'no') {
+ // To set boolean options to false, the option name is prefixed with
+ // 'no'.
+ optionName = optionName.substring(2);
+ value = false;
+ }
+
+ var optionIsBoolean = options[optionName] && options[optionName].type == 'boolean';
+ if (optionIsBoolean && value == undefined) {
+ // Calling set with a boolean option sets it to true.
+ value = true;
+ }
+ // If no value is provided, then we assume this is a get.
+ if (!optionIsBoolean && value === undefined || forceGet) {
+ var oldValue = getOption(optionName, cm, setCfg);
+ if (oldValue === true || oldValue === false) {
+ showConfirm(cm, ' ' + (oldValue ? '' : 'no') + optionName);
+ } else {
+ showConfirm(cm, ' ' + optionName + '=' + oldValue);
+ }
+ } else {
+ setOption(optionName, value, cm, setCfg);
+ }
+ },
+ setlocal: function (cm, params) {
+ // setCfg is passed through to setOption
+ params.setCfg = {scope: 'local'};
+ this.set(cm, params);
+ },
+ setglobal: function (cm, params) {
+ // setCfg is passed through to setOption
+ params.setCfg = {scope: 'global'};
+ this.set(cm, params);
+ },
+ registers: function(cm, params) {
+ var regArgs = params.args;
+ var registers = vimGlobalState.registerController.registers;
+ var regInfo = '----------Registers----------<br><br>';
+ if (!regArgs) {
+ for (var registerName in registers) {
+ var text = registers[registerName].toString();
+ if (text.length) {
+ regInfo += '"' + registerName + ' ' + text + '<br>';
+ }
+ }
+ } else {
+ var registerName;
+ regArgs = regArgs.join('');
+ for (var i = 0; i < regArgs.length; i++) {
+ registerName = regArgs.charAt(i);
+ if (!vimGlobalState.registerController.isValidRegister(registerName)) {
+ continue;
+ }
+ var register = registers[registerName] || new Register();
+ regInfo += '"' + registerName + ' ' + register.toString() + '<br>';
+ }
+ }
+ showConfirm(cm, regInfo);
+ },
+ sort: function(cm, params) {
+ var reverse, ignoreCase, unique, number;
+ function parseArgs() {
+ if (params.argString) {
+ var args = new CodeMirror.StringStream(params.argString);
+ if (args.eat('!')) { reverse = true; }
+ if (args.eol()) { return; }
+ if (!args.eatSpace()) { return 'Invalid arguments'; }
+ var opts = args.match(/[a-z]+/);
+ if (opts) {
+ opts = opts[0];
+ ignoreCase = opts.indexOf('i') != -1;
+ unique = opts.indexOf('u') != -1;
+ var decimal = opts.indexOf('d') != -1 && 1;
+ var hex = opts.indexOf('x') != -1 && 1;
+ var octal = opts.indexOf('o') != -1 && 1;
+ if (decimal + hex + octal > 1) { return 'Invalid arguments'; }
+ number = decimal && 'decimal' || hex && 'hex' || octal && 'octal';
+ }
+ if (args.match(/\/.*\//)) { return 'patterns not supported'; }
+ }
+ }
+ var err = parseArgs();
+ if (err) {
+ showConfirm(cm, err + ': ' + params.argString);
+ return;
+ }
+ var lineStart = params.line || cm.firstLine();
+ var lineEnd = params.lineEnd || params.line || cm.lastLine();
+ if (lineStart == lineEnd) { return; }
+ var curStart = Pos(lineStart, 0);
+ var curEnd = Pos(lineEnd, lineLength(cm, lineEnd));
+ var text = cm.getRange(curStart, curEnd).split('\n');
+ var numberRegex = (number == 'decimal') ? /(-?)([\d]+)/ :
+ (number == 'hex') ? /(-?)(?:0x)?([0-9a-f]+)/i :
+ (number == 'octal') ? /([0-7]+)/ : null;
+ var radix = (number == 'decimal') ? 10 : (number == 'hex') ? 16 : (number == 'octal') ? 8 : null;
+ var numPart = [], textPart = [];
+ if (number) {
+ for (var i = 0; i < text.length; i++) {
+ if (numberRegex.exec(text[i])) {
+ numPart.push(text[i]);
+ } else {
+ textPart.push(text[i]);
+ }
+ }
+ } else {
+ textPart = text;
+ }
+ function compareFn(a, b) {
+ if (reverse) { var tmp; tmp = a; a = b; b = tmp; }
+ if (ignoreCase) { a = a.toLowerCase(); b = b.toLowerCase(); }
+ var anum = number && numberRegex.exec(a);
+ var bnum = number && numberRegex.exec(b);
+ if (!anum) { return a < b ? -1 : 1; }
+ anum = parseInt((anum[1] + anum[2]).toLowerCase(), radix);
+ bnum = parseInt((bnum[1] + bnum[2]).toLowerCase(), radix);
+ return anum - bnum;
+ }
+ numPart.sort(compareFn);
+ textPart.sort(compareFn);
+ text = (!reverse) ? textPart.concat(numPart) : numPart.concat(textPart);
+ if (unique) { // Remove duplicate lines
+ var textOld = text;
+ var lastLine;
+ text = [];
+ for (var i = 0; i < textOld.length; i++) {
+ if (textOld[i] != lastLine) {
+ text.push(textOld[i]);
+ }
+ lastLine = textOld[i];
+ }
+ }
+ cm.replaceRange(text.join('\n'), curStart, curEnd);
+ },
+ global: function(cm, params) {
+ // a global command is of the form
+ // :[range]g/pattern/[cmd]
+ // argString holds the string /pattern/[cmd]
+ var argString = params.argString;
+ if (!argString) {
+ showConfirm(cm, 'Regular Expression missing from global');
+ return;
+ }
+ // range is specified here
+ var lineStart = (params.line !== undefined) ? params.line : cm.firstLine();
+ var lineEnd = params.lineEnd || params.line || cm.lastLine();
+ // get the tokens from argString
+ var tokens = splitBySlash(argString);
+ var regexPart = argString, cmd;
+ if (tokens.length) {
+ regexPart = tokens[0];
+ cmd = tokens.slice(1, tokens.length).join('/');
+ }
+ if (regexPart) {
+ // If regex part is empty, then use the previous query. Otherwise
+ // use the regex part as the new query.
+ try {
+ updateSearchQuery(cm, regexPart, true /** ignoreCase */,
+ true /** smartCase */);
+ } catch (e) {
+ showConfirm(cm, 'Invalid regex: ' + regexPart);
+ return;
+ }
+ }
+ // now that we have the regexPart, search for regex matches in the
+ // specified range of lines
+ var query = getSearchState(cm).getQuery();
+ var matchedLines = [], content = '';
+ for (var i = lineStart; i <= lineEnd; i++) {
+ var matched = query.test(cm.getLine(i));
+ if (matched) {
+ matchedLines.push(i+1);
+ content+= cm.getLine(i) + '<br>';
+ }
+ }
+ // if there is no [cmd], just display the list of matched lines
+ if (!cmd) {
+ showConfirm(cm, content);
+ return;
+ }
+ var index = 0;
+ var nextCommand = function() {
+ if (index < matchedLines.length) {
+ var command = matchedLines[index] + cmd;
+ exCommandDispatcher.processCommand(cm, command, {
+ callback: nextCommand
+ });
+ }
+ index++;
+ };
+ nextCommand();
+ },
+ substitute: function(cm, params) {
+ if (!cm.getSearchCursor) {
+ throw new Error('Search feature not available. Requires searchcursor.js or ' +
+ 'any other getSearchCursor implementation.');
+ }
+ var argString = params.argString;
+ var tokens = argString ? splitBySlash(argString) : [];
+ var regexPart, replacePart = '', trailing, flagsPart, count;
+ var confirm = false; // Whether to confirm each replace.
+ var global = false; // True to replace all instances on a line, false to replace only 1.
+ if (tokens.length) {
+ regexPart = tokens[0];
+ replacePart = tokens[1];
+ if (replacePart !== undefined) {
+ if (getOption('pcre')) {
+ replacePart = unescapeRegexReplace(replacePart);
+ } else {
+ replacePart = translateRegexReplace(replacePart);
+ }
+ vimGlobalState.lastSubstituteReplacePart = replacePart;
+ }
+ trailing = tokens[2] ? tokens[2].split(' ') : [];
+ } else {
+ // either the argString is empty or its of the form ' hello/world'
+ // actually splitBySlash returns a list of tokens
+ // only if the string starts with a '/'
+ if (argString && argString.length) {
+ showConfirm(cm, 'Substitutions should be of the form ' +
+ ':s/pattern/replace/');
+ return;
+ }
+ }
+ // After the 3rd slash, we can have flags followed by a space followed
+ // by count.
+ if (trailing) {
+ flagsPart = trailing[0];
+ count = parseInt(trailing[1]);
+ if (flagsPart) {
+ if (flagsPart.indexOf('c') != -1) {
+ confirm = true;
+ flagsPart.replace('c', '');
+ }
+ if (flagsPart.indexOf('g') != -1) {
+ global = true;
+ flagsPart.replace('g', '');
+ }
+ regexPart = regexPart + '/' + flagsPart;
+ }
+ }
+ if (regexPart) {
+ // If regex part is empty, then use the previous query. Otherwise use
+ // the regex part as the new query.
+ try {
+ updateSearchQuery(cm, regexPart, true /** ignoreCase */,
+ true /** smartCase */);
+ } catch (e) {
+ showConfirm(cm, 'Invalid regex: ' + regexPart);
+ return;
+ }
+ }
+ replacePart = replacePart || vimGlobalState.lastSubstituteReplacePart;
+ if (replacePart === undefined) {
+ showConfirm(cm, 'No previous substitute regular expression');
+ return;
+ }
+ var state = getSearchState(cm);
+ var query = state.getQuery();
+ var lineStart = (params.line !== undefined) ? params.line : cm.getCursor().line;
+ var lineEnd = params.lineEnd || lineStart;
+ if (lineStart == cm.firstLine() && lineEnd == cm.lastLine()) {
+ lineEnd = Infinity;
+ }
+ if (count) {
+ lineStart = lineEnd;
+ lineEnd = lineStart + count - 1;
+ }
+ var startPos = clipCursorToContent(cm, Pos(lineStart, 0));
+ var cursor = cm.getSearchCursor(query, startPos);
+ doReplace(cm, confirm, global, lineStart, lineEnd, cursor, query, replacePart, params.callback);
+ },
+ redo: CodeMirror.commands.redo,
+ undo: CodeMirror.commands.undo,
+ write: function(cm) {
+ if (CodeMirror.commands.save) {
+ // If a save command is defined, call it.
+ CodeMirror.commands.save(cm);
+ } else if (cm.save) {
+ // Saves to text area if no save command is defined and cm.save() is available.
+ cm.save();
+ }
+ },
+ nohlsearch: function(cm) {
+ clearSearchHighlight(cm);
+ },
+ yank: function (cm) {
+ var cur = copyCursor(cm.getCursor());
+ var line = cur.line;
+ var lineText = cm.getLine(line);
+ vimGlobalState.registerController.pushText(
+ '0', 'yank', lineText, true, true);
+ },
+ delmarks: function(cm, params) {
+ if (!params.argString || !trim(params.argString)) {
+ showConfirm(cm, 'Argument required');
+ return;
+ }
+
+ var state = cm.state.vim;
+ var stream = new CodeMirror.StringStream(trim(params.argString));
+ while (!stream.eol()) {
+ stream.eatSpace();
+
+ // Record the streams position at the beginning of the loop for use
+ // in error messages.
+ var count = stream.pos;
+
+ if (!stream.match(/[a-zA-Z]/, false)) {
+ showConfirm(cm, 'Invalid argument: ' + params.argString.substring(count));
+ return;
+ }
+
+ var sym = stream.next();
+ // Check if this symbol is part of a range
+ if (stream.match('-', true)) {
+ // This symbol is part of a range.
+
+ // The range must terminate at an alphabetic character.
+ if (!stream.match(/[a-zA-Z]/, false)) {
+ showConfirm(cm, 'Invalid argument: ' + params.argString.substring(count));
+ return;
+ }
+
+ var startMark = sym;
+ var finishMark = stream.next();
+ // The range must terminate at an alphabetic character which
+ // shares the same case as the start of the range.
+ if (isLowerCase(startMark) && isLowerCase(finishMark) ||
+ isUpperCase(startMark) && isUpperCase(finishMark)) {
+ var start = startMark.charCodeAt(0);
+ var finish = finishMark.charCodeAt(0);
+ if (start >= finish) {
+ showConfirm(cm, 'Invalid argument: ' + params.argString.substring(count));
+ return;
+ }
+
+ // Because marks are always ASCII values, and we have
+ // determined that they are the same case, we can use
+ // their char codes to iterate through the defined range.
+ for (var j = 0; j <= finish - start; j++) {
+ var mark = String.fromCharCode(start + j);
+ delete state.marks[mark];
+ }
+ } else {
+ showConfirm(cm, 'Invalid argument: ' + startMark + '-');
+ return;
+ }
+ } else {
+ // This symbol is a valid mark, and is not part of a range.
+ delete state.marks[sym];
+ }
+ }
+ }
+ };
+
+ var exCommandDispatcher = new ExCommandDispatcher();
+
+ /**
+ * @param {CodeMirror} cm CodeMirror instance we are in.
+ * @param {boolean} confirm Whether to confirm each replace.
+ * @param {Cursor} lineStart Line to start replacing from.
+ * @param {Cursor} lineEnd Line to stop replacing at.
+ * @param {RegExp} query Query for performing matches with.
+ * @param {string} replaceWith Text to replace matches with. May contain $1,
+ * $2, etc for replacing captured groups using Javascript replace.
+ * @param {function()} callback A callback for when the replace is done.
+ */
+ function doReplace(cm, confirm, global, lineStart, lineEnd, searchCursor, query,
+ replaceWith, callback) {
+ // Set up all the functions.
+ cm.state.vim.exMode = true;
+ var done = false;
+ var lastPos = searchCursor.from();
+ function replaceAll() {
+ cm.operation(function() {
+ while (!done) {
+ replace();
+ next();
+ }
+ stop();
+ });
+ }
+ function replace() {
+ var text = cm.getRange(searchCursor.from(), searchCursor.to());
+ var newText = text.replace(query, replaceWith);
+ searchCursor.replace(newText);
+ }
+ function next() {
+ // The below only loops to skip over multiple occurrences on the same
+ // line when 'global' is not true.
+ while(searchCursor.findNext() &&
+ isInRange(searchCursor.from(), lineStart, lineEnd)) {
+ if (!global && lastPos && searchCursor.from().line == lastPos.line) {
+ continue;
+ }
+ cm.scrollIntoView(searchCursor.from(), 30);
+ cm.setSelection(searchCursor.from(), searchCursor.to());
+ lastPos = searchCursor.from();
+ done = false;
+ return;
+ }
+ done = true;
+ }
+ function stop(close) {
+ if (close) { close(); }
+ cm.focus();
+ if (lastPos) {
+ cm.setCursor(lastPos);
+ var vim = cm.state.vim;
+ vim.exMode = false;
+ vim.lastHPos = vim.lastHSPos = lastPos.ch;
+ }
+ if (callback) { callback(); }
+ }
+ function onPromptKeyDown(e, _value, close) {
+ // Swallow all keys.
+ CodeMirror.e_stop(e);
+ var keyName = CodeMirror.keyName(e);
+ switch (keyName) {
+ case 'Y':
+ replace(); next(); break;
+ case 'N':
+ next(); break;
+ case 'A':
+ // replaceAll contains a call to close of its own. We don't want it
+ // to fire too early or multiple times.
+ var savedCallback = callback;
+ callback = undefined;
+ cm.operation(replaceAll);
+ callback = savedCallback;
+ break;
+ case 'L':
+ replace();
+ // fall through and exit.
+ case 'Q':
+ case 'Esc':
+ case 'Ctrl-C':
+ case 'Ctrl-[':
+ stop(close);
+ break;
+ }
+ if (done) { stop(close); }
+ return true;
+ }
+
+ // Actually do replace.
+ next();
+ if (done) {
+ showConfirm(cm, 'No matches for ' + query.source);
+ return;
+ }
+ if (!confirm) {
+ replaceAll();
+ if (callback) { callback(); };
+ return;
+ }
+ showPrompt(cm, {
+ prefix: 'replace with <strong>' + replaceWith + '</strong> (y/n/a/q/l)',
+ onKeyDown: onPromptKeyDown
+ });
+ }
+
+ CodeMirror.keyMap.vim = {
+ attach: attachVimMap,
+ detach: detachVimMap,
+ call: cmKey
+ };
+
+ function exitInsertMode(cm) {
+ var vim = cm.state.vim;
+ var macroModeState = vimGlobalState.macroModeState;
+ var insertModeChangeRegister = vimGlobalState.registerController.getRegister('.');
+ var isPlaying = macroModeState.isPlaying;
+ var lastChange = macroModeState.lastInsertModeChanges;
+ // In case of visual block, the insertModeChanges are not saved as a
+ // single word, so we convert them to a single word
+ // so as to update the ". register as expected in real vim.
+ var text = [];
+ if (!isPlaying) {
+ var selLength = lastChange.inVisualBlock ? vim.lastSelection.visualBlock.height : 1;
+ var changes = lastChange.changes;
+ var text = [];
+ var i = 0;
+ // In case of multiple selections in blockwise visual,
+ // the inserted text, for example: 'f<Backspace>oo', is stored as
+ // 'f', 'f', InsertModeKey 'o', 'o', 'o', 'o'. (if you have a block with 2 lines).
+ // We push the contents of the changes array as per the following:
+ // 1. In case of InsertModeKey, just increment by 1.
+ // 2. In case of a character, jump by selLength (2 in the example).
+ while (i < changes.length) {
+ // This loop will convert 'ff<bs>oooo' to 'f<bs>oo'.
+ text.push(changes[i]);
+ if (changes[i] instanceof InsertModeKey) {
+ i++;
+ } else {
+ i+= selLength;
+ }
+ }
+ lastChange.changes = text;
+ cm.off('change', onChange);
+ CodeMirror.off(cm.getInputField(), 'keydown', onKeyEventTargetKeyDown);
+ }
+ if (!isPlaying && vim.insertModeRepeat > 1) {
+ // Perform insert mode repeat for commands like 3,a and 3,o.
+ repeatLastEdit(cm, vim, vim.insertModeRepeat - 1,
+ true /** repeatForInsert */);
+ vim.lastEditInputState.repeatOverride = vim.insertModeRepeat;
+ }
+ delete vim.insertModeRepeat;
+ vim.insertMode = false;
+ cm.setCursor(cm.getCursor().line, cm.getCursor().ch-1);
+ cm.setOption('keyMap', 'vim');
+ cm.setOption('disableInput', true);
+ cm.toggleOverwrite(false); // exit replace mode if we were in it.
+ // update the ". register before exiting insert mode
+ insertModeChangeRegister.setText(lastChange.changes.join(''));
+ CodeMirror.signal(cm, "vim-mode-change", {mode: "normal"});
+ if (macroModeState.isRecording) {
+ logInsertModeChange(macroModeState);
+ }
+ }
+
+ function _mapCommand(command) {
+ defaultKeymap.unshift(command);
+ }
+
+ function mapCommand(keys, type, name, args, extra) {
+ var command = {keys: keys, type: type};
+ command[type] = name;
+ command[type + "Args"] = args;
+ for (var key in extra)
+ command[key] = extra[key];
+ _mapCommand(command);
+ }
+
+ // The timeout in milliseconds for the two-character ESC keymap should be
+ // adjusted according to your typing speed to prevent false positives.
+ defineOption('insertModeEscKeysTimeout', 200, 'number');
+
+ CodeMirror.keyMap['vim-insert'] = {
+ // TODO: override navigation keys so that Esc will cancel automatic
+ // indentation from o, O, i_<CR>
+ 'Ctrl-N': 'autocomplete',
+ 'Ctrl-P': 'autocomplete',
+ 'Enter': function(cm) {
+ var fn = CodeMirror.commands.newlineAndIndentContinueComment ||
+ CodeMirror.commands.newlineAndIndent;
+ fn(cm);
+ },
+ fallthrough: ['default'],
+ attach: attachVimMap,
+ detach: detachVimMap,
+ call: cmKey
+ };
+
+ CodeMirror.keyMap['vim-replace'] = {
+ 'Backspace': 'goCharLeft',
+ fallthrough: ['vim-insert'],
+ attach: attachVimMap,
+ detach: detachVimMap,
+ call: cmKey
+ };
+
+ function executeMacroRegister(cm, vim, macroModeState, registerName) {
+ var register = vimGlobalState.registerController.getRegister(registerName);
+ if (registerName == ':') {
+ // Read-only register containing last Ex command.
+ if (register.keyBuffer[0]) {
+ exCommandDispatcher.processCommand(cm, register.keyBuffer[0]);
+ }
+ macroModeState.isPlaying = false;
+ return;
+ }
+ var keyBuffer = register.keyBuffer;
+ var imc = 0;
+ macroModeState.isPlaying = true;
+ macroModeState.replaySearchQueries = register.searchQueries.slice(0);
+ for (var i = 0; i < keyBuffer.length; i++) {
+ var text = keyBuffer[i];
+ var match, key;
+ while (text) {
+ // Pull off one command key, which is either a single character
+ // or a special sequence wrapped in '<' and '>', e.g. '<Space>'.
+ match = (/<\w+-.+?>|<\w+>|./).exec(text);
+ key = match[0];
+ text = text.substring(match.index + key.length);
+ CodeMirror.Vim.handleKey(cm, key, 'macro');
+ if (vim.insertMode) {
+ var changes = register.insertModeChanges[imc++].changes;
+ vimGlobalState.macroModeState.lastInsertModeChanges.changes =
+ changes;
+ repeatInsertModeChanges(cm, changes, 1);
+ exitInsertMode(cm);
+ }
+ }
+ };
+ macroModeState.isPlaying = false;
+ }
+
+ function logKey(macroModeState, key) {
+ if (macroModeState.isPlaying) { return; }
+ var registerName = macroModeState.latestRegister;
+ var register = vimGlobalState.registerController.getRegister(registerName);
+ if (register) {
+ register.pushText(key);
+ }
+ }
+
+ function logInsertModeChange(macroModeState) {
+ if (macroModeState.isPlaying) { return; }
+ var registerName = macroModeState.latestRegister;
+ var register = vimGlobalState.registerController.getRegister(registerName);
+ if (register && register.pushInsertModeChanges) {
+ register.pushInsertModeChanges(macroModeState.lastInsertModeChanges);
+ }
+ }
+
+ function logSearchQuery(macroModeState, query) {
+ if (macroModeState.isPlaying) { return; }
+ var registerName = macroModeState.latestRegister;
+ var register = vimGlobalState.registerController.getRegister(registerName);
+ if (register && register.pushSearchQuery) {
+ register.pushSearchQuery(query);
+ }
+ }
+
+ /**
+ * Listens for changes made in insert mode.
+ * Should only be active in insert mode.
+ */
+ function onChange(_cm, changeObj) {
+ var macroModeState = vimGlobalState.macroModeState;
+ var lastChange = macroModeState.lastInsertModeChanges;
+ if (!macroModeState.isPlaying) {
+ while(changeObj) {
+ lastChange.expectCursorActivityForChange = true;
+ if (changeObj.origin == '+input' || changeObj.origin == 'paste'
+ || changeObj.origin === undefined /* only in testing */) {
+ var text = changeObj.text.join('\n');
+ lastChange.changes.push(text);
+ }
+ // Change objects may be chained with next.
+ changeObj = changeObj.next;
+ }
+ }
+ }
+
+ /**
+ * Listens for any kind of cursor activity on CodeMirror.
+ */
+ function onCursorActivity(cm) {
+ var vim = cm.state.vim;
+ if (vim.insertMode) {
+ // Tracking cursor activity in insert mode (for macro support).
+ var macroModeState = vimGlobalState.macroModeState;
+ if (macroModeState.isPlaying) { return; }
+ var lastChange = macroModeState.lastInsertModeChanges;
+ if (lastChange.expectCursorActivityForChange) {
+ lastChange.expectCursorActivityForChange = false;
+ } else {
+ // Cursor moved outside the context of an edit. Reset the change.
+ lastChange.changes = [];
+ }
+ } else if (!cm.curOp.isVimOp) {
+ handleExternalSelection(cm, vim);
+ }
+ if (vim.visualMode) {
+ updateFakeCursor(cm);
+ }
+ }
+ function updateFakeCursor(cm) {
+ var vim = cm.state.vim;
+ var from = clipCursorToContent(cm, copyCursor(vim.sel.head));
+ var to = offsetCursor(from, 0, 1);
+ if (vim.fakeCursor) {
+ vim.fakeCursor.clear();
+ }
+ vim.fakeCursor = cm.markText(from, to, {className: 'cm-animate-fat-cursor'});
+ }
+ function handleExternalSelection(cm, vim) {
+ var anchor = cm.getCursor('anchor');
+ var head = cm.getCursor('head');
+ // Enter or exit visual mode to match mouse selection.
+ if (vim.visualMode && !cm.somethingSelected()) {
+ exitVisualMode(cm, false);
+ } else if (!vim.visualMode && !vim.insertMode && cm.somethingSelected()) {
+ vim.visualMode = true;
+ vim.visualLine = false;
+ CodeMirror.signal(cm, "vim-mode-change", {mode: "visual"});
+ }
+ if (vim.visualMode) {
+ // Bind CodeMirror selection model to vim selection model.
+ // Mouse selections are considered visual characterwise.
+ var headOffset = !cursorIsBefore(head, anchor) ? -1 : 0;
+ var anchorOffset = cursorIsBefore(head, anchor) ? -1 : 0;
+ head = offsetCursor(head, 0, headOffset);
+ anchor = offsetCursor(anchor, 0, anchorOffset);
+ vim.sel = {
+ anchor: anchor,
+ head: head
+ };
+ updateMark(cm, vim, '<', cursorMin(head, anchor));
+ updateMark(cm, vim, '>', cursorMax(head, anchor));
+ } else if (!vim.insertMode) {
+ // Reset lastHPos if selection was modified by something outside of vim mode e.g. by mouse.
+ vim.lastHPos = cm.getCursor().ch;
+ }
+ }
+
+ /** Wrapper for special keys pressed in insert mode */
+ function InsertModeKey(keyName) {
+ this.keyName = keyName;
+ }
+
+ /**
+ * Handles raw key down events from the text area.
+ * - Should only be active in insert mode.
+ * - For recording deletes in insert mode.
+ */
+ function onKeyEventTargetKeyDown(e) {
+ var macroModeState = vimGlobalState.macroModeState;
+ var lastChange = macroModeState.lastInsertModeChanges;
+ var keyName = CodeMirror.keyName(e);
+ if (!keyName) { return; }
+ function onKeyFound() {
+ lastChange.changes.push(new InsertModeKey(keyName));
+ return true;
+ }
+ if (keyName.indexOf('Delete') != -1 || keyName.indexOf('Backspace') != -1) {
+ CodeMirror.lookupKey(keyName, 'vim-insert', onKeyFound);
+ }
+ }
+
+ /**
+ * Repeats the last edit, which includes exactly 1 command and at most 1
+ * insert. Operator and motion commands are read from lastEditInputState,
+ * while action commands are read from lastEditActionCommand.
+ *
+ * If repeatForInsert is true, then the function was called by
+ * exitInsertMode to repeat the insert mode changes the user just made. The
+ * corresponding enterInsertMode call was made with a count.
+ */
+ function repeatLastEdit(cm, vim, repeat, repeatForInsert) {
+ var macroModeState = vimGlobalState.macroModeState;
+ macroModeState.isPlaying = true;
+ var isAction = !!vim.lastEditActionCommand;
+ var cachedInputState = vim.inputState;
+ function repeatCommand() {
+ if (isAction) {
+ commandDispatcher.processAction(cm, vim, vim.lastEditActionCommand);
+ } else {
+ commandDispatcher.evalInput(cm, vim);
+ }
+ }
+ function repeatInsert(repeat) {
+ if (macroModeState.lastInsertModeChanges.changes.length > 0) {
+ // For some reason, repeat cw in desktop VIM does not repeat
+ // insert mode changes. Will conform to that behavior.
+ repeat = !vim.lastEditActionCommand ? 1 : repeat;
+ var changeObject = macroModeState.lastInsertModeChanges;
+ repeatInsertModeChanges(cm, changeObject.changes, repeat);
+ }
+ }
+ vim.inputState = vim.lastEditInputState;
+ if (isAction && vim.lastEditActionCommand.interlaceInsertRepeat) {
+ // o and O repeat have to be interlaced with insert repeats so that the
+ // insertions appear on separate lines instead of the last line.
+ for (var i = 0; i < repeat; i++) {
+ repeatCommand();
+ repeatInsert(1);
+ }
+ } else {
+ if (!repeatForInsert) {
+ // Hack to get the cursor to end up at the right place. If I is
+ // repeated in insert mode repeat, cursor will be 1 insert
+ // change set left of where it should be.
+ repeatCommand();
+ }
+ repeatInsert(repeat);
+ }
+ vim.inputState = cachedInputState;
+ if (vim.insertMode && !repeatForInsert) {
+ // Don't exit insert mode twice. If repeatForInsert is set, then we
+ // were called by an exitInsertMode call lower on the stack.
+ exitInsertMode(cm);
+ }
+ macroModeState.isPlaying = false;
+ };
+
+ function repeatInsertModeChanges(cm, changes, repeat) {
+ function keyHandler(binding) {
+ if (typeof binding == 'string') {
+ CodeMirror.commands[binding](cm);
+ } else {
+ binding(cm);
+ }
+ return true;
+ }
+ var head = cm.getCursor('head');
+ var inVisualBlock = vimGlobalState.macroModeState.lastInsertModeChanges.inVisualBlock;
+ if (inVisualBlock) {
+ // Set up block selection again for repeating the changes.
+ var vim = cm.state.vim;
+ var lastSel = vim.lastSelection;
+ var offset = getOffset(lastSel.anchor, lastSel.head);
+ selectForInsert(cm, head, offset.line + 1);
+ repeat = cm.listSelections().length;
+ cm.setCursor(head);
+ }
+ for (var i = 0; i < repeat; i++) {
+ if (inVisualBlock) {
+ cm.setCursor(offsetCursor(head, i, 0));
+ }
+ for (var j = 0; j < changes.length; j++) {
+ var change = changes[j];
+ if (change instanceof InsertModeKey) {
+ CodeMirror.lookupKey(change.keyName, 'vim-insert', keyHandler);
+ } else {
+ var cur = cm.getCursor();
+ cm.replaceRange(change, cur, cur);
+ }
+ }
+ }
+ if (inVisualBlock) {
+ cm.setCursor(offsetCursor(head, 0, 1));
+ }
+ }
+
+ resetVimGlobalState();
+ return vimApi;
+ };
+ // Initialize Vim and make it available as an API.
+ CodeMirror.Vim = Vim();
+});
diff --git a/devtools/client/sourceeditor/codemirror/lib/codemirror.css b/devtools/client/sourceeditor/codemirror/lib/codemirror.css
new file mode 100644
index 000000000..18b0bf70d
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/lib/codemirror.css
@@ -0,0 +1,347 @@
+/* BASICS */
+
+.CodeMirror {
+ /* Set height, width, borders, and global font properties here */
+ font-family: monospace;
+ height: 300px;
+ color: black;
+}
+
+/* PADDING */
+
+.CodeMirror-lines {
+ padding: 4px 0; /* Vertical padding around content */
+}
+.CodeMirror pre {
+ padding: 0 4px; /* Horizontal padding of content */
+}
+
+.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
+ background-color: white; /* The little square between H and V scrollbars */
+}
+
+/* GUTTER */
+
+.CodeMirror-gutters {
+ border-right: 1px solid #ddd;
+ background-color: #f7f7f7;
+ white-space: nowrap;
+}
+.CodeMirror-linenumbers {}
+.CodeMirror-linenumber {
+ padding: 0 3px 0 5px;
+ min-width: 20px;
+ text-align: right;
+ color: #999;
+ white-space: nowrap;
+}
+
+.CodeMirror-guttermarker { color: black; }
+.CodeMirror-guttermarker-subtle { color: #999; }
+
+/* CURSOR */
+
+.CodeMirror-cursor {
+ border-left: 1px solid black;
+ border-right: none;
+ width: 0;
+}
+/* Shown when moving in bi-directional text */
+.CodeMirror div.CodeMirror-secondarycursor {
+ border-left: 1px solid silver;
+}
+.cm-fat-cursor .CodeMirror-cursor {
+ width: auto;
+ border: 0 !important;
+ background: #7e7;
+}
+.cm-fat-cursor div.CodeMirror-cursors {
+ z-index: 1;
+}
+
+.cm-animate-fat-cursor {
+ width: auto;
+ border: 0;
+ -webkit-animation: blink 1.06s steps(1) infinite;
+ -moz-animation: blink 1.06s steps(1) infinite;
+ animation: blink 1.06s steps(1) infinite;
+ background-color: #7e7;
+}
+@-moz-keyframes blink {
+ 0% {}
+ 50% { background-color: transparent; }
+ 100% {}
+}
+@-webkit-keyframes blink {
+ 0% {}
+ 50% { background-color: transparent; }
+ 100% {}
+}
+@keyframes blink {
+ 0% {}
+ 50% { background-color: transparent; }
+ 100% {}
+}
+
+/* Can style cursor different in overwrite (non-insert) mode */
+.CodeMirror-overwrite .CodeMirror-cursor {}
+
+.cm-tab { display: inline-block; text-decoration: inherit; }
+
+.CodeMirror-rulers {
+ position: absolute;
+ left: 0; right: 0; top: -50px; bottom: -20px;
+ overflow: hidden;
+}
+.CodeMirror-ruler {
+ border-left: 1px solid #ccc;
+ top: 0; bottom: 0;
+ position: absolute;
+}
+
+/* DEFAULT THEME */
+
+.cm-s-default .cm-header {color: blue;}
+.cm-s-default .cm-quote {color: #090;}
+.cm-negative {color: #d44;}
+.cm-positive {color: #292;}
+.cm-header, .cm-strong {font-weight: bold;}
+.cm-em {font-style: italic;}
+.cm-link {text-decoration: underline;}
+.cm-strikethrough {text-decoration: line-through;}
+
+.cm-s-default .cm-keyword {color: #708;}
+.cm-s-default .cm-atom {color: #219;}
+.cm-s-default .cm-number {color: #164;}
+.cm-s-default .cm-def {color: #00f;}
+.cm-s-default .cm-variable,
+.cm-s-default .cm-punctuation,
+.cm-s-default .cm-property,
+.cm-s-default .cm-operator {}
+.cm-s-default .cm-variable-2 {color: #05a;}
+.cm-s-default .cm-variable-3 {color: #085;}
+.cm-s-default .cm-comment {color: #a50;}
+.cm-s-default .cm-string {color: #a11;}
+.cm-s-default .cm-string-2 {color: #f50;}
+.cm-s-default .cm-meta {color: #555;}
+.cm-s-default .cm-qualifier {color: #555;}
+.cm-s-default .cm-builtin {color: #30a;}
+.cm-s-default .cm-bracket {color: #997;}
+.cm-s-default .cm-tag {color: #170;}
+.cm-s-default .cm-attribute {color: #00c;}
+.cm-s-default .cm-hr {color: #999;}
+.cm-s-default .cm-link {color: #00c;}
+
+.cm-s-default .cm-error {color: #f00;}
+.cm-invalidchar {color: #f00;}
+
+.CodeMirror-composing { border-bottom: 2px solid; }
+
+/* Default styles for common addons */
+
+div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;}
+div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}
+.CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); }
+.CodeMirror-activeline-background {background: #e8f2ff;}
+
+/* STOP */
+
+/* The rest of this file contains styles related to the mechanics of
+ the editor. You probably shouldn't touch them. */
+
+.CodeMirror {
+ position: relative;
+ overflow: hidden;
+ background: white;
+}
+
+.CodeMirror-scroll {
+ overflow: scroll !important; /* Things will break if this is overridden */
+ /* 30px is the magic margin used to hide the element's real scrollbars */
+ /* See overflow: hidden in .CodeMirror */
+ margin-bottom: -30px; margin-right: -30px;
+ padding-bottom: 30px;
+ height: 100%;
+ outline: none; /* Prevent dragging from highlighting the element */
+ position: relative;
+}
+.CodeMirror-sizer {
+ position: relative;
+ border-right: 30px solid transparent;
+}
+
+/* The fake, visible scrollbars. Used to force redraw during scrolling
+ before actual scrolling happens, thus preventing shaking and
+ flickering artifacts. */
+.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
+ position: absolute;
+ z-index: 6;
+ display: none;
+}
+.CodeMirror-vscrollbar {
+ right: 0; top: 0;
+ overflow-x: hidden;
+ overflow-y: scroll;
+}
+.CodeMirror-hscrollbar {
+ bottom: 0; left: 0;
+ overflow-y: hidden;
+ overflow-x: scroll;
+}
+.CodeMirror-scrollbar-filler {
+ right: 0; bottom: 0;
+}
+.CodeMirror-gutter-filler {
+ left: 0; bottom: 0;
+}
+
+.CodeMirror-gutters {
+ position: absolute; left: 0; top: 0;
+ min-height: 100%;
+ z-index: 3;
+}
+.CodeMirror-gutter {
+ white-space: normal;
+ height: 100%;
+ display: inline-block;
+ vertical-align: top;
+ margin-bottom: -30px;
+ /* Hack to make IE7 behave */
+ *zoom:1;
+ *display:inline;
+}
+.CodeMirror-gutter-wrapper {
+ position: absolute;
+ z-index: 4;
+ background: none !important;
+ border: none !important;
+}
+.CodeMirror-gutter-background {
+ position: absolute;
+ top: 0; bottom: 0;
+ z-index: 4;
+}
+.CodeMirror-gutter-elt {
+ position: absolute;
+ cursor: default;
+ z-index: 4;
+}
+.CodeMirror-gutter-wrapper {
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ user-select: none;
+}
+
+.CodeMirror-lines {
+ cursor: text;
+ min-height: 1px; /* prevents collapsing before first draw */
+}
+.CodeMirror pre {
+ /* Reset some styles that the rest of the page might have set */
+ -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0;
+ border-width: 0;
+ background: transparent;
+ font-family: inherit;
+ font-size: inherit;
+ margin: 0;
+ white-space: pre;
+ word-wrap: normal;
+ line-height: inherit;
+ color: inherit;
+ z-index: 2;
+ position: relative;
+ overflow: visible;
+ -webkit-tap-highlight-color: transparent;
+ -webkit-font-variant-ligatures: none;
+ font-variant-ligatures: none;
+}
+.CodeMirror-wrap pre {
+ word-wrap: break-word;
+ white-space: pre-wrap;
+ word-break: normal;
+}
+
+.CodeMirror-linebackground {
+ position: absolute;
+ left: 0; right: 0; top: 0; bottom: 0;
+ z-index: 0;
+}
+
+.CodeMirror-linewidget {
+ position: relative;
+ z-index: 2;
+ overflow: auto;
+}
+
+.CodeMirror-widget {}
+
+.CodeMirror-code {
+ outline: none;
+}
+
+/* Force content-box sizing for the elements where we expect it */
+.CodeMirror-scroll,
+.CodeMirror-sizer,
+.CodeMirror-gutter,
+.CodeMirror-gutters,
+.CodeMirror-linenumber {
+ -moz-box-sizing: content-box;
+ box-sizing: content-box;
+}
+
+.CodeMirror-measure {
+ position: absolute;
+ width: 100%;
+ height: 0;
+ overflow: hidden;
+ visibility: hidden;
+}
+
+.CodeMirror-cursor {
+ position: absolute;
+ pointer-events: none;
+}
+.CodeMirror-measure pre { position: static; }
+
+div.CodeMirror-cursors {
+ visibility: hidden;
+ position: relative;
+ z-index: 3;
+}
+div.CodeMirror-dragcursors {
+ visibility: visible;
+}
+
+.CodeMirror-focused div.CodeMirror-cursors {
+ visibility: visible;
+}
+
+.CodeMirror-selected { background: #d9d9d9; }
+.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; }
+.CodeMirror-crosshair { cursor: crosshair; }
+.CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; }
+.CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; }
+
+.cm-searching {
+ background: #ffa;
+ background: rgba(255, 255, 0, .4);
+}
+
+/* IE7 hack to prevent it from returning funny offsetTops on the spans */
+.CodeMirror span { *vertical-align: text-bottom; }
+
+/* Used to force a border model for a node */
+.cm-force-border { padding-right: .1px; }
+
+@media print {
+ /* Hide the cursor when printing */
+ .CodeMirror div.CodeMirror-cursors {
+ visibility: hidden;
+ }
+}
+
+/* See issue #2901 */
+.cm-tab-wrap-hack:after { content: ''; }
+
+/* Help users use markselection to safely style text background */
+span.CodeMirror-selectedtext { background: none; }
diff --git a/devtools/client/sourceeditor/codemirror/lib/codemirror.js b/devtools/client/sourceeditor/codemirror/lib/codemirror.js
new file mode 100644
index 000000000..7dc842d3b
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/lib/codemirror.js
@@ -0,0 +1,8922 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+// This is CodeMirror (http://codemirror.net), a code editor
+// implemented in JavaScript on top of the browser's DOM.
+//
+// You can find some technical background for some of the code below
+// at http://marijnhaverbeke.nl/blog/#cm-internals .
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ module.exports = mod();
+ else if (typeof define == "function" && define.amd) // AMD
+ return define([], mod);
+ else // Plain browser env
+ (this || window).CodeMirror = mod();
+})(function() {
+ "use strict";
+
+ // BROWSER SNIFFING
+
+ // Kludges for bugs and behavior differences that can't be feature
+ // detected are enabled based on userAgent etc sniffing.
+ var userAgent = navigator.userAgent;
+ var platform = navigator.platform;
+
+ var gecko = /gecko\/\d/i.test(userAgent);
+ var ie_upto10 = /MSIE \d/.test(userAgent);
+ var ie_11up = /Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(userAgent);
+ var ie = ie_upto10 || ie_11up;
+ var ie_version = ie && (ie_upto10 ? document.documentMode || 6 : ie_11up[1]);
+ var webkit = /WebKit\//.test(userAgent);
+ var qtwebkit = webkit && /Qt\/\d+\.\d+/.test(userAgent);
+ var chrome = /Chrome\//.test(userAgent);
+ var presto = /Opera\//.test(userAgent);
+ var safari = /Apple Computer/.test(navigator.vendor);
+ var mac_geMountainLion = /Mac OS X 1\d\D([8-9]|\d\d)\D/.test(userAgent);
+ var phantom = /PhantomJS/.test(userAgent);
+
+ var ios = /AppleWebKit/.test(userAgent) && /Mobile\/\w+/.test(userAgent);
+ // This is woefully incomplete. Suggestions for alternative methods welcome.
+ var mobile = ios || /Android|webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(userAgent);
+ var mac = ios || /Mac/.test(platform);
+ var chromeOS = /\bCrOS\b/.test(userAgent);
+ var windows = /win/i.test(platform);
+
+ var presto_version = presto && userAgent.match(/Version\/(\d*\.\d*)/);
+ if (presto_version) presto_version = Number(presto_version[1]);
+ if (presto_version && presto_version >= 15) { presto = false; webkit = true; }
+ // Some browsers use the wrong event properties to signal cmd/ctrl on OS X
+ var flipCtrlCmd = mac && (qtwebkit || presto && (presto_version == null || presto_version < 12.11));
+ var captureRightClick = gecko || (ie && ie_version >= 9);
+
+ // Optimize some code when these features are not used.
+ var sawReadOnlySpans = false, sawCollapsedSpans = false;
+
+ // EDITOR CONSTRUCTOR
+
+ // A CodeMirror instance represents an editor. This is the object
+ // that user code is usually dealing with.
+
+ function CodeMirror(place, options) {
+ if (!(this instanceof CodeMirror)) return new CodeMirror(place, options);
+
+ this.options = options = options ? copyObj(options) : {};
+ // Determine effective options based on given values and defaults.
+ copyObj(defaults, options, false);
+ setGuttersForLineNumbers(options);
+
+ var doc = options.value;
+ if (typeof doc == "string") doc = new Doc(doc, options.mode, null, options.lineSeparator);
+ this.doc = doc;
+
+ var input = new CodeMirror.inputStyles[options.inputStyle](this);
+ var display = this.display = new Display(place, doc, input);
+ display.wrapper.CodeMirror = this;
+ updateGutters(this);
+ themeChanged(this);
+ if (options.lineWrapping)
+ this.display.wrapper.className += " CodeMirror-wrap";
+ if (options.autofocus && !mobile) display.input.focus();
+ initScrollbars(this);
+
+ this.state = {
+ keyMaps: [], // stores maps added by addKeyMap
+ overlays: [], // highlighting overlays, as added by addOverlay
+ modeGen: 0, // bumped when mode/overlay changes, used to invalidate highlighting info
+ overwrite: false,
+ delayingBlurEvent: false,
+ focused: false,
+ suppressEdits: false, // used to disable editing during key handlers when in readOnly mode
+ pasteIncoming: false, cutIncoming: false, // help recognize paste/cut edits in input.poll
+ selectingText: false,
+ draggingText: false,
+ highlight: new Delayed(), // stores highlight worker timeout
+ keySeq: null, // Unfinished key sequence
+ specialChars: null
+ };
+
+ var cm = this;
+
+ // Override magic textarea content restore that IE sometimes does
+ // on our hidden textarea on reload
+ if (ie && ie_version < 11) setTimeout(function() { cm.display.input.reset(true); }, 20);
+
+ registerEventHandlers(this);
+ ensureGlobalHandlers();
+
+ startOperation(this);
+ this.curOp.forceUpdate = true;
+ attachDoc(this, doc);
+
+ if ((options.autofocus && !mobile) || cm.hasFocus())
+ setTimeout(bind(onFocus, this), 20);
+ else
+ onBlur(this);
+
+ for (var opt in optionHandlers) if (optionHandlers.hasOwnProperty(opt))
+ optionHandlers[opt](this, options[opt], Init);
+ maybeUpdateLineNumberWidth(this);
+ if (options.finishInit) options.finishInit(this);
+ for (var i = 0; i < initHooks.length; ++i) initHooks[i](this);
+ endOperation(this);
+ // Suppress optimizelegibility in Webkit, since it breaks text
+ // measuring on line wrapping boundaries.
+ if (webkit && options.lineWrapping &&
+ getComputedStyle(display.lineDiv).textRendering == "optimizelegibility")
+ display.lineDiv.style.textRendering = "auto";
+ }
+
+ // DISPLAY CONSTRUCTOR
+
+ // The display handles the DOM integration, both for input reading
+ // and content drawing. It holds references to DOM nodes and
+ // display-related state.
+
+ function Display(place, doc, input) {
+ var d = this;
+ this.input = input;
+
+ // Covers bottom-right square when both scrollbars are present.
+ d.scrollbarFiller = elt("div", null, "CodeMirror-scrollbar-filler");
+ d.scrollbarFiller.setAttribute("cm-not-content", "true");
+ // Covers bottom of gutter when coverGutterNextToScrollbar is on
+ // and h scrollbar is present.
+ d.gutterFiller = elt("div", null, "CodeMirror-gutter-filler");
+ d.gutterFiller.setAttribute("cm-not-content", "true");
+ // Will contain the actual code, positioned to cover the viewport.
+ d.lineDiv = elt("div", null, "CodeMirror-code");
+ // Elements are added to these to represent selection and cursors.
+ d.selectionDiv = elt("div", null, null, "position: relative; z-index: 1");
+ d.cursorDiv = elt("div", null, "CodeMirror-cursors");
+ // A visibility: hidden element used to find the size of things.
+ d.measure = elt("div", null, "CodeMirror-measure");
+ // When lines outside of the viewport are measured, they are drawn in this.
+ d.lineMeasure = elt("div", null, "CodeMirror-measure");
+ // Wraps everything that needs to exist inside the vertically-padded coordinate system
+ d.lineSpace = elt("div", [d.measure, d.lineMeasure, d.selectionDiv, d.cursorDiv, d.lineDiv],
+ null, "position: relative; outline: none");
+ // Moved around its parent to cover visible view.
+ d.mover = elt("div", [elt("div", [d.lineSpace], "CodeMirror-lines")], null, "position: relative");
+ // Set to the height of the document, allowing scrolling.
+ d.sizer = elt("div", [d.mover], "CodeMirror-sizer");
+ d.sizerWidth = null;
+ // Behavior of elts with overflow: auto and padding is
+ // inconsistent across browsers. This is used to ensure the
+ // scrollable area is big enough.
+ d.heightForcer = elt("div", null, null, "position: absolute; height: " + scrollerGap + "px; width: 1px;");
+ // Will contain the gutters, if any.
+ d.gutters = elt("div", null, "CodeMirror-gutters");
+ d.lineGutter = null;
+ // Actual scrollable element.
+ d.scroller = elt("div", [d.sizer, d.heightForcer, d.gutters], "CodeMirror-scroll");
+ d.scroller.setAttribute("tabIndex", "-1");
+ // The element in which the editor lives.
+ d.wrapper = elt("div", [d.scrollbarFiller, d.gutterFiller, d.scroller], "CodeMirror");
+
+ // Work around IE7 z-index bug (not perfect, hence IE7 not really being supported)
+ if (ie && ie_version < 8) { d.gutters.style.zIndex = -1; d.scroller.style.paddingRight = 0; }
+ if (!webkit && !(gecko && mobile)) d.scroller.draggable = true;
+
+ if (place) {
+ if (place.appendChild) place.appendChild(d.wrapper);
+ else place(d.wrapper);
+ }
+
+ // Current rendered range (may be bigger than the view window).
+ d.viewFrom = d.viewTo = doc.first;
+ d.reportedViewFrom = d.reportedViewTo = doc.first;
+ // Information about the rendered lines.
+ d.view = [];
+ d.renderedView = null;
+ // Holds info about a single rendered line when it was rendered
+ // for measurement, while not in view.
+ d.externalMeasured = null;
+ // Empty space (in pixels) above the view
+ d.viewOffset = 0;
+ d.lastWrapHeight = d.lastWrapWidth = 0;
+ d.updateLineNumbers = null;
+
+ d.nativeBarWidth = d.barHeight = d.barWidth = 0;
+ d.scrollbarsClipped = false;
+
+ // Used to only resize the line number gutter when necessary (when
+ // the amount of lines crosses a boundary that makes its width change)
+ d.lineNumWidth = d.lineNumInnerWidth = d.lineNumChars = null;
+ // Set to true when a non-horizontal-scrolling line widget is
+ // added. As an optimization, line widget aligning is skipped when
+ // this is false.
+ d.alignWidgets = false;
+
+ d.cachedCharWidth = d.cachedTextHeight = d.cachedPaddingH = null;
+
+ // Tracks the maximum line length so that the horizontal scrollbar
+ // can be kept static when scrolling.
+ d.maxLine = null;
+ d.maxLineLength = 0;
+ d.maxLineChanged = false;
+
+ // Used for measuring wheel scrolling granularity
+ d.wheelDX = d.wheelDY = d.wheelStartX = d.wheelStartY = null;
+
+ // True when shift is held down.
+ d.shift = false;
+
+ // Used to track whether anything happened since the context menu
+ // was opened.
+ d.selForContextMenu = null;
+
+ d.activeTouch = null;
+
+ input.init(d);
+ }
+
+ // STATE UPDATES
+
+ // Used to get the editor into a consistent state again when options change.
+
+ function loadMode(cm) {
+ cm.doc.mode = CodeMirror.getMode(cm.options, cm.doc.modeOption);
+ resetModeState(cm);
+ }
+
+ function resetModeState(cm) {
+ cm.doc.iter(function(line) {
+ if (line.stateAfter) line.stateAfter = null;
+ if (line.styles) line.styles = null;
+ });
+ cm.doc.frontier = cm.doc.first;
+ startWorker(cm, 100);
+ cm.state.modeGen++;
+ if (cm.curOp) regChange(cm);
+ }
+
+ function wrappingChanged(cm) {
+ if (cm.options.lineWrapping) {
+ addClass(cm.display.wrapper, "CodeMirror-wrap");
+ cm.display.sizer.style.minWidth = "";
+ cm.display.sizerWidth = null;
+ } else {
+ rmClass(cm.display.wrapper, "CodeMirror-wrap");
+ findMaxLine(cm);
+ }
+ estimateLineHeights(cm);
+ regChange(cm);
+ clearCaches(cm);
+ setTimeout(function(){updateScrollbars(cm);}, 100);
+ }
+
+ // Returns a function that estimates the height of a line, to use as
+ // first approximation until the line becomes visible (and is thus
+ // properly measurable).
+ function estimateHeight(cm) {
+ var th = textHeight(cm.display), wrapping = cm.options.lineWrapping;
+ var perLine = wrapping && Math.max(5, cm.display.scroller.clientWidth / charWidth(cm.display) - 3);
+ return function(line) {
+ if (lineIsHidden(cm.doc, line)) return 0;
+
+ var widgetsHeight = 0;
+ if (line.widgets) for (var i = 0; i < line.widgets.length; i++) {
+ if (line.widgets[i].height) widgetsHeight += line.widgets[i].height;
+ }
+
+ if (wrapping)
+ return widgetsHeight + (Math.ceil(line.text.length / perLine) || 1) * th;
+ else
+ return widgetsHeight + th;
+ };
+ }
+
+ function estimateLineHeights(cm) {
+ var doc = cm.doc, est = estimateHeight(cm);
+ doc.iter(function(line) {
+ var estHeight = est(line);
+ if (estHeight != line.height) updateLineHeight(line, estHeight);
+ });
+ }
+
+ function themeChanged(cm) {
+ cm.display.wrapper.className = cm.display.wrapper.className.replace(/\s*cm-s-\S+/g, "") +
+ cm.options.theme.replace(/(^|\s)\s*/g, " cm-s-");
+ clearCaches(cm);
+ }
+
+ function guttersChanged(cm) {
+ updateGutters(cm);
+ regChange(cm);
+ setTimeout(function(){alignHorizontally(cm);}, 20);
+ }
+
+ // Rebuild the gutter elements, ensure the margin to the left of the
+ // code matches their width.
+ function updateGutters(cm) {
+ var gutters = cm.display.gutters, specs = cm.options.gutters;
+ removeChildren(gutters);
+ for (var i = 0; i < specs.length; ++i) {
+ var gutterClass = specs[i];
+ var gElt = gutters.appendChild(elt("div", null, "CodeMirror-gutter " + gutterClass));
+ if (gutterClass == "CodeMirror-linenumbers") {
+ cm.display.lineGutter = gElt;
+ gElt.style.width = (cm.display.lineNumWidth || 1) + "px";
+ }
+ }
+ gutters.style.display = i ? "" : "none";
+ updateGutterSpace(cm);
+ }
+
+ function updateGutterSpace(cm) {
+ var width = cm.display.gutters.offsetWidth;
+ cm.display.sizer.style.marginLeft = width + "px";
+ }
+
+ // Compute the character length of a line, taking into account
+ // collapsed ranges (see markText) that might hide parts, and join
+ // other lines onto it.
+ function lineLength(line) {
+ if (line.height == 0) return 0;
+ var len = line.text.length, merged, cur = line;
+ while (merged = collapsedSpanAtStart(cur)) {
+ var found = merged.find(0, true);
+ cur = found.from.line;
+ len += found.from.ch - found.to.ch;
+ }
+ cur = line;
+ while (merged = collapsedSpanAtEnd(cur)) {
+ var found = merged.find(0, true);
+ len -= cur.text.length - found.from.ch;
+ cur = found.to.line;
+ len += cur.text.length - found.to.ch;
+ }
+ return len;
+ }
+
+ // Find the longest line in the document.
+ function findMaxLine(cm) {
+ var d = cm.display, doc = cm.doc;
+ d.maxLine = getLine(doc, doc.first);
+ d.maxLineLength = lineLength(d.maxLine);
+ d.maxLineChanged = true;
+ doc.iter(function(line) {
+ var len = lineLength(line);
+ if (len > d.maxLineLength) {
+ d.maxLineLength = len;
+ d.maxLine = line;
+ }
+ });
+ }
+
+ // Make sure the gutters options contains the element
+ // "CodeMirror-linenumbers" when the lineNumbers option is true.
+ function setGuttersForLineNumbers(options) {
+ var found = indexOf(options.gutters, "CodeMirror-linenumbers");
+ if (found == -1 && options.lineNumbers) {
+ options.gutters = options.gutters.concat(["CodeMirror-linenumbers"]);
+ } else if (found > -1 && !options.lineNumbers) {
+ options.gutters = options.gutters.slice(0);
+ options.gutters.splice(found, 1);
+ }
+ }
+
+ // SCROLLBARS
+
+ // Prepare DOM reads needed to update the scrollbars. Done in one
+ // shot to minimize update/measure roundtrips.
+ function measureForScrollbars(cm) {
+ var d = cm.display, gutterW = d.gutters.offsetWidth;
+ var docH = Math.round(cm.doc.height + paddingVert(cm.display));
+ return {
+ clientHeight: d.scroller.clientHeight,
+ viewHeight: d.wrapper.clientHeight,
+ scrollWidth: d.scroller.scrollWidth, clientWidth: d.scroller.clientWidth,
+ viewWidth: d.wrapper.clientWidth,
+ barLeft: cm.options.fixedGutter ? gutterW : 0,
+ docHeight: docH,
+ scrollHeight: docH + scrollGap(cm) + d.barHeight,
+ nativeBarWidth: d.nativeBarWidth,
+ gutterWidth: gutterW
+ };
+ }
+
+ function NativeScrollbars(place, scroll, cm) {
+ this.cm = cm;
+ var vert = this.vert = elt("div", [elt("div", null, null, "min-width: 1px")], "CodeMirror-vscrollbar");
+ var horiz = this.horiz = elt("div", [elt("div", null, null, "height: 100%; min-height: 1px")], "CodeMirror-hscrollbar");
+ place(vert); place(horiz);
+
+ on(vert, "scroll", function() {
+ if (vert.clientHeight) scroll(vert.scrollTop, "vertical");
+ });
+ on(horiz, "scroll", function() {
+ if (horiz.clientWidth) scroll(horiz.scrollLeft, "horizontal");
+ });
+
+ this.checkedZeroWidth = false;
+ // Need to set a minimum width to see the scrollbar on IE7 (but must not set it on IE8).
+ if (ie && ie_version < 8) this.horiz.style.minHeight = this.vert.style.minWidth = "18px";
+ }
+
+ NativeScrollbars.prototype = copyObj({
+ update: function(measure) {
+ var needsH = measure.scrollWidth > measure.clientWidth + 1;
+ var needsV = measure.scrollHeight > measure.clientHeight + 1;
+ var sWidth = measure.nativeBarWidth;
+
+ if (needsV) {
+ this.vert.style.display = "block";
+ this.vert.style.bottom = needsH ? sWidth + "px" : "0";
+ var totalHeight = measure.viewHeight - (needsH ? sWidth : 0);
+ // A bug in IE8 can cause this value to be negative, so guard it.
+ this.vert.firstChild.style.height =
+ Math.max(0, measure.scrollHeight - measure.clientHeight + totalHeight) + "px";
+ } else {
+ this.vert.style.display = "";
+ this.vert.firstChild.style.height = "0";
+ }
+
+ if (needsH) {
+ this.horiz.style.display = "block";
+ this.horiz.style.right = needsV ? sWidth + "px" : "0";
+ this.horiz.style.left = measure.barLeft + "px";
+ var totalWidth = measure.viewWidth - measure.barLeft - (needsV ? sWidth : 0);
+ this.horiz.firstChild.style.width =
+ (measure.scrollWidth - measure.clientWidth + totalWidth) + "px";
+ } else {
+ this.horiz.style.display = "";
+ this.horiz.firstChild.style.width = "0";
+ }
+
+ if (!this.checkedZeroWidth && measure.clientHeight > 0) {
+ if (sWidth == 0) this.zeroWidthHack();
+ this.checkedZeroWidth = true;
+ }
+
+ return {right: needsV ? sWidth : 0, bottom: needsH ? sWidth : 0};
+ },
+ setScrollLeft: function(pos) {
+ if (this.horiz.scrollLeft != pos) this.horiz.scrollLeft = pos;
+ if (this.disableHoriz) this.enableZeroWidthBar(this.horiz, this.disableHoriz);
+ },
+ setScrollTop: function(pos) {
+ if (this.vert.scrollTop != pos) this.vert.scrollTop = pos;
+ if (this.disableVert) this.enableZeroWidthBar(this.vert, this.disableVert);
+ },
+ zeroWidthHack: function() {
+ var w = mac && !mac_geMountainLion ? "12px" : "18px";
+ this.horiz.style.height = this.vert.style.width = w;
+ this.horiz.style.pointerEvents = this.vert.style.pointerEvents = "none";
+ this.disableHoriz = new Delayed;
+ this.disableVert = new Delayed;
+ },
+ enableZeroWidthBar: function(bar, delay) {
+ bar.style.pointerEvents = "auto";
+ function maybeDisable() {
+ // To find out whether the scrollbar is still visible, we
+ // check whether the element under the pixel in the bottom
+ // left corner of the scrollbar box is the scrollbar box
+ // itself (when the bar is still visible) or its filler child
+ // (when the bar is hidden). If it is still visible, we keep
+ // it enabled, if it's hidden, we disable pointer events.
+ var box = bar.getBoundingClientRect();
+ var elt = document.elementFromPoint(box.left + 1, box.bottom - 1);
+ if (elt != bar) bar.style.pointerEvents = "none";
+ else delay.set(1000, maybeDisable);
+ }
+ delay.set(1000, maybeDisable);
+ },
+ clear: function() {
+ var parent = this.horiz.parentNode;
+ parent.removeChild(this.horiz);
+ parent.removeChild(this.vert);
+ }
+ }, NativeScrollbars.prototype);
+
+ function NullScrollbars() {}
+
+ NullScrollbars.prototype = copyObj({
+ update: function() { return {bottom: 0, right: 0}; },
+ setScrollLeft: function() {},
+ setScrollTop: function() {},
+ clear: function() {}
+ }, NullScrollbars.prototype);
+
+ CodeMirror.scrollbarModel = {"native": NativeScrollbars, "null": NullScrollbars};
+
+ function initScrollbars(cm) {
+ if (cm.display.scrollbars) {
+ cm.display.scrollbars.clear();
+ if (cm.display.scrollbars.addClass)
+ rmClass(cm.display.wrapper, cm.display.scrollbars.addClass);
+ }
+
+ cm.display.scrollbars = new CodeMirror.scrollbarModel[cm.options.scrollbarStyle](function(node) {
+ cm.display.wrapper.insertBefore(node, cm.display.scrollbarFiller);
+ // Prevent clicks in the scrollbars from killing focus
+ on(node, "mousedown", function() {
+ if (cm.state.focused) setTimeout(function() { cm.display.input.focus(); }, 0);
+ });
+ node.setAttribute("cm-not-content", "true");
+ }, function(pos, axis) {
+ if (axis == "horizontal") setScrollLeft(cm, pos);
+ else setScrollTop(cm, pos);
+ }, cm);
+ if (cm.display.scrollbars.addClass)
+ addClass(cm.display.wrapper, cm.display.scrollbars.addClass);
+ }
+
+ function updateScrollbars(cm, measure) {
+ if (!measure) measure = measureForScrollbars(cm);
+ var startWidth = cm.display.barWidth, startHeight = cm.display.barHeight;
+ updateScrollbarsInner(cm, measure);
+ for (var i = 0; i < 4 && startWidth != cm.display.barWidth || startHeight != cm.display.barHeight; i++) {
+ if (startWidth != cm.display.barWidth && cm.options.lineWrapping)
+ updateHeightsInViewport(cm);
+ updateScrollbarsInner(cm, measureForScrollbars(cm));
+ startWidth = cm.display.barWidth; startHeight = cm.display.barHeight;
+ }
+ }
+
+ // Re-synchronize the fake scrollbars with the actual size of the
+ // content.
+ function updateScrollbarsInner(cm, measure) {
+ var d = cm.display;
+ var sizes = d.scrollbars.update(measure);
+
+ d.sizer.style.paddingRight = (d.barWidth = sizes.right) + "px";
+ d.sizer.style.paddingBottom = (d.barHeight = sizes.bottom) + "px";
+ d.heightForcer.style.borderBottom = sizes.bottom + "px solid transparent"
+
+ if (sizes.right && sizes.bottom) {
+ d.scrollbarFiller.style.display = "block";
+ d.scrollbarFiller.style.height = sizes.bottom + "px";
+ d.scrollbarFiller.style.width = sizes.right + "px";
+ } else d.scrollbarFiller.style.display = "";
+ if (sizes.bottom && cm.options.coverGutterNextToScrollbar && cm.options.fixedGutter) {
+ d.gutterFiller.style.display = "block";
+ d.gutterFiller.style.height = sizes.bottom + "px";
+ d.gutterFiller.style.width = measure.gutterWidth + "px";
+ } else d.gutterFiller.style.display = "";
+ }
+
+ // Compute the lines that are visible in a given viewport (defaults
+ // the the current scroll position). viewport may contain top,
+ // height, and ensure (see op.scrollToPos) properties.
+ function visibleLines(display, doc, viewport) {
+ var top = viewport && viewport.top != null ? Math.max(0, viewport.top) : display.scroller.scrollTop;
+ top = Math.floor(top - paddingTop(display));
+ var bottom = viewport && viewport.bottom != null ? viewport.bottom : top + display.wrapper.clientHeight;
+
+ var from = lineAtHeight(doc, top), to = lineAtHeight(doc, bottom);
+ // Ensure is a {from: {line, ch}, to: {line, ch}} object, and
+ // forces those lines into the viewport (if possible).
+ if (viewport && viewport.ensure) {
+ var ensureFrom = viewport.ensure.from.line, ensureTo = viewport.ensure.to.line;
+ if (ensureFrom < from) {
+ from = ensureFrom;
+ to = lineAtHeight(doc, heightAtLine(getLine(doc, ensureFrom)) + display.wrapper.clientHeight);
+ } else if (Math.min(ensureTo, doc.lastLine()) >= to) {
+ from = lineAtHeight(doc, heightAtLine(getLine(doc, ensureTo)) - display.wrapper.clientHeight);
+ to = ensureTo;
+ }
+ }
+ return {from: from, to: Math.max(to, from + 1)};
+ }
+
+ // LINE NUMBERS
+
+ // Re-align line numbers and gutter marks to compensate for
+ // horizontal scrolling.
+ function alignHorizontally(cm) {
+ var display = cm.display, view = display.view;
+ if (!display.alignWidgets && (!display.gutters.firstChild || !cm.options.fixedGutter)) return;
+ var comp = compensateForHScroll(display) - display.scroller.scrollLeft + cm.doc.scrollLeft;
+ var gutterW = display.gutters.offsetWidth, left = comp + "px";
+ for (var i = 0; i < view.length; i++) if (!view[i].hidden) {
+ if (cm.options.fixedGutter && view[i].gutter)
+ view[i].gutter.style.left = left;
+ var align = view[i].alignable;
+ if (align) for (var j = 0; j < align.length; j++)
+ align[j].style.left = left;
+ }
+ if (cm.options.fixedGutter)
+ display.gutters.style.left = (comp + gutterW) + "px";
+ }
+
+ // Used to ensure that the line number gutter is still the right
+ // size for the current document size. Returns true when an update
+ // is needed.
+ function maybeUpdateLineNumberWidth(cm) {
+ if (!cm.options.lineNumbers) return false;
+ var doc = cm.doc, last = lineNumberFor(cm.options, doc.first + doc.size - 1), display = cm.display;
+ if (last.length != display.lineNumChars) {
+ var test = display.measure.appendChild(elt("div", [elt("div", last)],
+ "CodeMirror-linenumber CodeMirror-gutter-elt"));
+ var innerW = test.firstChild.offsetWidth, padding = test.offsetWidth - innerW;
+ display.lineGutter.style.width = "";
+ display.lineNumInnerWidth = Math.max(innerW, display.lineGutter.offsetWidth - padding) + 1;
+ display.lineNumWidth = display.lineNumInnerWidth + padding;
+ display.lineNumChars = display.lineNumInnerWidth ? last.length : -1;
+ display.lineGutter.style.width = display.lineNumWidth + "px";
+ updateGutterSpace(cm);
+ return true;
+ }
+ return false;
+ }
+
+ function lineNumberFor(options, i) {
+ return String(options.lineNumberFormatter(i + options.firstLineNumber));
+ }
+
+ // Computes display.scroller.scrollLeft + display.gutters.offsetWidth,
+ // but using getBoundingClientRect to get a sub-pixel-accurate
+ // result.
+ function compensateForHScroll(display) {
+ return display.scroller.getBoundingClientRect().left - display.sizer.getBoundingClientRect().left;
+ }
+
+ // DISPLAY DRAWING
+
+ function DisplayUpdate(cm, viewport, force) {
+ var display = cm.display;
+
+ this.viewport = viewport;
+ // Store some values that we'll need later (but don't want to force a relayout for)
+ this.visible = visibleLines(display, cm.doc, viewport);
+ this.editorIsHidden = !display.wrapper.offsetWidth;
+ this.wrapperHeight = display.wrapper.clientHeight;
+ this.wrapperWidth = display.wrapper.clientWidth;
+ this.oldDisplayWidth = displayWidth(cm);
+ this.force = force;
+ this.dims = getDimensions(cm);
+ this.events = [];
+ }
+
+ DisplayUpdate.prototype.signal = function(emitter, type) {
+ if (hasHandler(emitter, type))
+ this.events.push(arguments);
+ };
+ DisplayUpdate.prototype.finish = function() {
+ for (var i = 0; i < this.events.length; i++)
+ signal.apply(null, this.events[i]);
+ };
+
+ function maybeClipScrollbars(cm) {
+ var display = cm.display;
+ if (!display.scrollbarsClipped && display.scroller.offsetWidth) {
+ display.nativeBarWidth = display.scroller.offsetWidth - display.scroller.clientWidth;
+ display.heightForcer.style.height = scrollGap(cm) + "px";
+ display.sizer.style.marginBottom = -display.nativeBarWidth + "px";
+ display.sizer.style.borderRightWidth = scrollGap(cm) + "px";
+ display.scrollbarsClipped = true;
+ }
+ }
+
+ // Does the actual updating of the line display. Bails out
+ // (returning false) when there is nothing to be done and forced is
+ // false.
+ function updateDisplayIfNeeded(cm, update) {
+ var display = cm.display, doc = cm.doc;
+
+ if (update.editorIsHidden) {
+ resetView(cm);
+ return false;
+ }
+
+ // Bail out if the visible area is already rendered and nothing changed.
+ if (!update.force &&
+ update.visible.from >= display.viewFrom && update.visible.to <= display.viewTo &&
+ (display.updateLineNumbers == null || display.updateLineNumbers >= display.viewTo) &&
+ display.renderedView == display.view && countDirtyView(cm) == 0)
+ return false;
+
+ if (maybeUpdateLineNumberWidth(cm)) {
+ resetView(cm);
+ update.dims = getDimensions(cm);
+ }
+
+ // Compute a suitable new viewport (from & to)
+ var end = doc.first + doc.size;
+ var from = Math.max(update.visible.from - cm.options.viewportMargin, doc.first);
+ var to = Math.min(end, update.visible.to + cm.options.viewportMargin);
+ if (display.viewFrom < from && from - display.viewFrom < 20) from = Math.max(doc.first, display.viewFrom);
+ if (display.viewTo > to && display.viewTo - to < 20) to = Math.min(end, display.viewTo);
+ if (sawCollapsedSpans) {
+ from = visualLineNo(cm.doc, from);
+ to = visualLineEndNo(cm.doc, to);
+ }
+
+ var different = from != display.viewFrom || to != display.viewTo ||
+ display.lastWrapHeight != update.wrapperHeight || display.lastWrapWidth != update.wrapperWidth;
+ adjustView(cm, from, to);
+
+ display.viewOffset = heightAtLine(getLine(cm.doc, display.viewFrom));
+ // Position the mover div to align with the current scroll position
+ cm.display.mover.style.top = display.viewOffset + "px";
+
+ var toUpdate = countDirtyView(cm);
+ if (!different && toUpdate == 0 && !update.force && display.renderedView == display.view &&
+ (display.updateLineNumbers == null || display.updateLineNumbers >= display.viewTo))
+ return false;
+
+ // For big changes, we hide the enclosing element during the
+ // update, since that speeds up the operations on most browsers.
+ var focused = activeElt();
+ if (toUpdate > 4) display.lineDiv.style.display = "none";
+ patchDisplay(cm, display.updateLineNumbers, update.dims);
+ if (toUpdate > 4) display.lineDiv.style.display = "";
+ display.renderedView = display.view;
+ // There might have been a widget with a focused element that got
+ // hidden or updated, if so re-focus it.
+ if (focused && activeElt() != focused && focused.offsetHeight) focused.focus();
+
+ // Prevent selection and cursors from interfering with the scroll
+ // width and height.
+ removeChildren(display.cursorDiv);
+ removeChildren(display.selectionDiv);
+ display.gutters.style.height = display.sizer.style.minHeight = 0;
+
+ if (different) {
+ display.lastWrapHeight = update.wrapperHeight;
+ display.lastWrapWidth = update.wrapperWidth;
+ startWorker(cm, 400);
+ }
+
+ display.updateLineNumbers = null;
+
+ return true;
+ }
+
+ function postUpdateDisplay(cm, update) {
+ var viewport = update.viewport;
+
+ for (var first = true;; first = false) {
+ if (!first || !cm.options.lineWrapping || update.oldDisplayWidth == displayWidth(cm)) {
+ // Clip forced viewport to actual scrollable area.
+ if (viewport && viewport.top != null)
+ viewport = {top: Math.min(cm.doc.height + paddingVert(cm.display) - displayHeight(cm), viewport.top)};
+ // Updated line heights might result in the drawn area not
+ // actually covering the viewport. Keep looping until it does.
+ update.visible = visibleLines(cm.display, cm.doc, viewport);
+ if (update.visible.from >= cm.display.viewFrom && update.visible.to <= cm.display.viewTo)
+ break;
+ }
+ if (!updateDisplayIfNeeded(cm, update)) break;
+ updateHeightsInViewport(cm);
+ var barMeasure = measureForScrollbars(cm);
+ updateSelection(cm);
+ updateScrollbars(cm, barMeasure);
+ setDocumentHeight(cm, barMeasure);
+ }
+
+ update.signal(cm, "update", cm);
+ if (cm.display.viewFrom != cm.display.reportedViewFrom || cm.display.viewTo != cm.display.reportedViewTo) {
+ update.signal(cm, "viewportChange", cm, cm.display.viewFrom, cm.display.viewTo);
+ cm.display.reportedViewFrom = cm.display.viewFrom; cm.display.reportedViewTo = cm.display.viewTo;
+ }
+ }
+
+ function updateDisplaySimple(cm, viewport) {
+ var update = new DisplayUpdate(cm, viewport);
+ if (updateDisplayIfNeeded(cm, update)) {
+ updateHeightsInViewport(cm);
+ postUpdateDisplay(cm, update);
+ var barMeasure = measureForScrollbars(cm);
+ updateSelection(cm);
+ updateScrollbars(cm, barMeasure);
+ setDocumentHeight(cm, barMeasure);
+ update.finish();
+ }
+ }
+
+ function setDocumentHeight(cm, measure) {
+ cm.display.sizer.style.minHeight = measure.docHeight + "px";
+ cm.display.heightForcer.style.top = measure.docHeight + "px";
+ cm.display.gutters.style.height = (measure.docHeight + cm.display.barHeight + scrollGap(cm)) + "px";
+ }
+
+ // Read the actual heights of the rendered lines, and update their
+ // stored heights to match.
+ function updateHeightsInViewport(cm) {
+ var display = cm.display;
+ var prevBottom = display.lineDiv.offsetTop;
+ for (var i = 0; i < display.view.length; i++) {
+ var cur = display.view[i], height;
+ if (cur.hidden) continue;
+ if (ie && ie_version < 8) {
+ var bot = cur.node.offsetTop + cur.node.offsetHeight;
+ height = bot - prevBottom;
+ prevBottom = bot;
+ } else {
+ var box = cur.node.getBoundingClientRect();
+ height = box.bottom - box.top;
+ }
+ var diff = cur.line.height - height;
+ if (height < 2) height = textHeight(display);
+ if (diff > .001 || diff < -.001) {
+ updateLineHeight(cur.line, height);
+ updateWidgetHeight(cur.line);
+ if (cur.rest) for (var j = 0; j < cur.rest.length; j++)
+ updateWidgetHeight(cur.rest[j]);
+ }
+ }
+ }
+
+ // Read and store the height of line widgets associated with the
+ // given line.
+ function updateWidgetHeight(line) {
+ if (line.widgets) for (var i = 0; i < line.widgets.length; ++i)
+ line.widgets[i].height = line.widgets[i].node.parentNode.offsetHeight;
+ }
+
+ // Do a bulk-read of the DOM positions and sizes needed to draw the
+ // view, so that we don't interleave reading and writing to the DOM.
+ function getDimensions(cm) {
+ var d = cm.display, left = {}, width = {};
+ var gutterLeft = d.gutters.clientLeft;
+ for (var n = d.gutters.firstChild, i = 0; n; n = n.nextSibling, ++i) {
+ left[cm.options.gutters[i]] = n.offsetLeft + n.clientLeft + gutterLeft;
+ width[cm.options.gutters[i]] = n.clientWidth;
+ }
+ return {fixedPos: compensateForHScroll(d),
+ gutterTotalWidth: d.gutters.offsetWidth,
+ gutterLeft: left,
+ gutterWidth: width,
+ wrapperWidth: d.wrapper.clientWidth};
+ }
+
+ // Sync the actual display DOM structure with display.view, removing
+ // nodes for lines that are no longer in view, and creating the ones
+ // that are not there yet, and updating the ones that are out of
+ // date.
+ function patchDisplay(cm, updateNumbersFrom, dims) {
+ var display = cm.display, lineNumbers = cm.options.lineNumbers;
+ var container = display.lineDiv, cur = container.firstChild;
+
+ function rm(node) {
+ var next = node.nextSibling;
+ // Works around a throw-scroll bug in OS X Webkit
+ if (webkit && mac && cm.display.currentWheelTarget == node)
+ node.style.display = "none";
+ else
+ node.parentNode.removeChild(node);
+ return next;
+ }
+
+ var view = display.view, lineN = display.viewFrom;
+ // Loop over the elements in the view, syncing cur (the DOM nodes
+ // in display.lineDiv) with the view as we go.
+ for (var i = 0; i < view.length; i++) {
+ var lineView = view[i];
+ if (lineView.hidden) {
+ } else if (!lineView.node || lineView.node.parentNode != container) { // Not drawn yet
+ var node = buildLineElement(cm, lineView, lineN, dims);
+ container.insertBefore(node, cur);
+ } else { // Already drawn
+ while (cur != lineView.node) cur = rm(cur);
+ var updateNumber = lineNumbers && updateNumbersFrom != null &&
+ updateNumbersFrom <= lineN && lineView.lineNumber;
+ if (lineView.changes) {
+ if (indexOf(lineView.changes, "gutter") > -1) updateNumber = false;
+ updateLineForChanges(cm, lineView, lineN, dims);
+ }
+ if (updateNumber) {
+ removeChildren(lineView.lineNumber);
+ lineView.lineNumber.appendChild(document.createTextNode(lineNumberFor(cm.options, lineN)));
+ }
+ cur = lineView.node.nextSibling;
+ }
+ lineN += lineView.size;
+ }
+ while (cur) cur = rm(cur);
+ }
+
+ // When an aspect of a line changes, a string is added to
+ // lineView.changes. This updates the relevant part of the line's
+ // DOM structure.
+ function updateLineForChanges(cm, lineView, lineN, dims) {
+ for (var j = 0; j < lineView.changes.length; j++) {
+ var type = lineView.changes[j];
+ if (type == "text") updateLineText(cm, lineView);
+ else if (type == "gutter") updateLineGutter(cm, lineView, lineN, dims);
+ else if (type == "class") updateLineClasses(lineView);
+ else if (type == "widget") updateLineWidgets(cm, lineView, dims);
+ }
+ lineView.changes = null;
+ }
+
+ // Lines with gutter elements, widgets or a background class need to
+ // be wrapped, and have the extra elements added to the wrapper div
+ function ensureLineWrapped(lineView) {
+ if (lineView.node == lineView.text) {
+ lineView.node = elt("div", null, null, "position: relative");
+ if (lineView.text.parentNode)
+ lineView.text.parentNode.replaceChild(lineView.node, lineView.text);
+ lineView.node.appendChild(lineView.text);
+ if (ie && ie_version < 8) lineView.node.style.zIndex = 2;
+ }
+ return lineView.node;
+ }
+
+ function updateLineBackground(lineView) {
+ var cls = lineView.bgClass ? lineView.bgClass + " " + (lineView.line.bgClass || "") : lineView.line.bgClass;
+ if (cls) cls += " CodeMirror-linebackground";
+ if (lineView.background) {
+ if (cls) lineView.background.className = cls;
+ else { lineView.background.parentNode.removeChild(lineView.background); lineView.background = null; }
+ } else if (cls) {
+ var wrap = ensureLineWrapped(lineView);
+ lineView.background = wrap.insertBefore(elt("div", null, cls), wrap.firstChild);
+ }
+ }
+
+ // Wrapper around buildLineContent which will reuse the structure
+ // in display.externalMeasured when possible.
+ function getLineContent(cm, lineView) {
+ var ext = cm.display.externalMeasured;
+ if (ext && ext.line == lineView.line) {
+ cm.display.externalMeasured = null;
+ lineView.measure = ext.measure;
+ return ext.built;
+ }
+ return buildLineContent(cm, lineView);
+ }
+
+ // Redraw the line's text. Interacts with the background and text
+ // classes because the mode may output tokens that influence these
+ // classes.
+ function updateLineText(cm, lineView) {
+ var cls = lineView.text.className;
+ var built = getLineContent(cm, lineView);
+ if (lineView.text == lineView.node) lineView.node = built.pre;
+ lineView.text.parentNode.replaceChild(built.pre, lineView.text);
+ lineView.text = built.pre;
+ if (built.bgClass != lineView.bgClass || built.textClass != lineView.textClass) {
+ lineView.bgClass = built.bgClass;
+ lineView.textClass = built.textClass;
+ updateLineClasses(lineView);
+ } else if (cls) {
+ lineView.text.className = cls;
+ }
+ }
+
+ function updateLineClasses(lineView) {
+ updateLineBackground(lineView);
+ if (lineView.line.wrapClass)
+ ensureLineWrapped(lineView).className = lineView.line.wrapClass;
+ else if (lineView.node != lineView.text)
+ lineView.node.className = "";
+ var textClass = lineView.textClass ? lineView.textClass + " " + (lineView.line.textClass || "") : lineView.line.textClass;
+ lineView.text.className = textClass || "";
+ }
+
+ function updateLineGutter(cm, lineView, lineN, dims) {
+ if (lineView.gutter) {
+ lineView.node.removeChild(lineView.gutter);
+ lineView.gutter = null;
+ }
+ if (lineView.gutterBackground) {
+ lineView.node.removeChild(lineView.gutterBackground);
+ lineView.gutterBackground = null;
+ }
+ if (lineView.line.gutterClass) {
+ var wrap = ensureLineWrapped(lineView);
+ lineView.gutterBackground = elt("div", null, "CodeMirror-gutter-background " + lineView.line.gutterClass,
+ "left: " + (cm.options.fixedGutter ? dims.fixedPos : -dims.gutterTotalWidth) +
+ "px; width: " + dims.gutterTotalWidth + "px");
+ wrap.insertBefore(lineView.gutterBackground, lineView.text);
+ }
+ var markers = lineView.line.gutterMarkers;
+ if (cm.options.lineNumbers || markers) {
+ var wrap = ensureLineWrapped(lineView);
+ var gutterWrap = lineView.gutter = elt("div", null, "CodeMirror-gutter-wrapper", "left: " +
+ (cm.options.fixedGutter ? dims.fixedPos : -dims.gutterTotalWidth) + "px");
+ cm.display.input.setUneditable(gutterWrap);
+ wrap.insertBefore(gutterWrap, lineView.text);
+ if (lineView.line.gutterClass)
+ gutterWrap.className += " " + lineView.line.gutterClass;
+ if (cm.options.lineNumbers && (!markers || !markers["CodeMirror-linenumbers"]))
+ lineView.lineNumber = gutterWrap.appendChild(
+ elt("div", lineNumberFor(cm.options, lineN),
+ "CodeMirror-linenumber CodeMirror-gutter-elt",
+ "left: " + dims.gutterLeft["CodeMirror-linenumbers"] + "px; width: "
+ + cm.display.lineNumInnerWidth + "px"));
+ if (markers) for (var k = 0; k < cm.options.gutters.length; ++k) {
+ var id = cm.options.gutters[k], found = markers.hasOwnProperty(id) && markers[id];
+ if (found)
+ gutterWrap.appendChild(elt("div", [found], "CodeMirror-gutter-elt", "left: " +
+ dims.gutterLeft[id] + "px; width: " + dims.gutterWidth[id] + "px"));
+ }
+ }
+ }
+
+ function updateLineWidgets(cm, lineView, dims) {
+ if (lineView.alignable) lineView.alignable = null;
+ for (var node = lineView.node.firstChild, next; node; node = next) {
+ var next = node.nextSibling;
+ if (node.className == "CodeMirror-linewidget")
+ lineView.node.removeChild(node);
+ }
+ insertLineWidgets(cm, lineView, dims);
+ }
+
+ // Build a line's DOM representation from scratch
+ function buildLineElement(cm, lineView, lineN, dims) {
+ var built = getLineContent(cm, lineView);
+ lineView.text = lineView.node = built.pre;
+ if (built.bgClass) lineView.bgClass = built.bgClass;
+ if (built.textClass) lineView.textClass = built.textClass;
+
+ updateLineClasses(lineView);
+ updateLineGutter(cm, lineView, lineN, dims);
+ insertLineWidgets(cm, lineView, dims);
+ return lineView.node;
+ }
+
+ // A lineView may contain multiple logical lines (when merged by
+ // collapsed spans). The widgets for all of them need to be drawn.
+ function insertLineWidgets(cm, lineView, dims) {
+ insertLineWidgetsFor(cm, lineView.line, lineView, dims, true);
+ if (lineView.rest) for (var i = 0; i < lineView.rest.length; i++)
+ insertLineWidgetsFor(cm, lineView.rest[i], lineView, dims, false);
+ }
+
+ function insertLineWidgetsFor(cm, line, lineView, dims, allowAbove) {
+ if (!line.widgets) return;
+ var wrap = ensureLineWrapped(lineView);
+ for (var i = 0, ws = line.widgets; i < ws.length; ++i) {
+ var widget = ws[i], node = elt("div", [widget.node], "CodeMirror-linewidget");
+ if (!widget.handleMouseEvents) node.setAttribute("cm-ignore-events", "true");
+ positionLineWidget(widget, node, lineView, dims);
+ cm.display.input.setUneditable(node);
+ if (allowAbove && widget.above)
+ wrap.insertBefore(node, lineView.gutter || lineView.text);
+ else
+ wrap.appendChild(node);
+ signalLater(widget, "redraw");
+ }
+ }
+
+ function positionLineWidget(widget, node, lineView, dims) {
+ if (widget.noHScroll) {
+ (lineView.alignable || (lineView.alignable = [])).push(node);
+ var width = dims.wrapperWidth;
+ node.style.left = dims.fixedPos + "px";
+ if (!widget.coverGutter) {
+ width -= dims.gutterTotalWidth;
+ node.style.paddingLeft = dims.gutterTotalWidth + "px";
+ }
+ node.style.width = width + "px";
+ }
+ if (widget.coverGutter) {
+ node.style.zIndex = 5;
+ node.style.position = "relative";
+ if (!widget.noHScroll) node.style.marginLeft = -dims.gutterTotalWidth + "px";
+ }
+ }
+
+ // POSITION OBJECT
+
+ // A Pos instance represents a position within the text.
+ var Pos = CodeMirror.Pos = function(line, ch) {
+ if (!(this instanceof Pos)) return new Pos(line, ch);
+ this.line = line; this.ch = ch;
+ };
+
+ // Compare two positions, return 0 if they are the same, a negative
+ // number when a is less, and a positive number otherwise.
+ var cmp = CodeMirror.cmpPos = function(a, b) { return a.line - b.line || a.ch - b.ch; };
+
+ function copyPos(x) {return Pos(x.line, x.ch);}
+ function maxPos(a, b) { return cmp(a, b) < 0 ? b : a; }
+ function minPos(a, b) { return cmp(a, b) < 0 ? a : b; }
+
+ // INPUT HANDLING
+
+ function ensureFocus(cm) {
+ if (!cm.state.focused) { cm.display.input.focus(); onFocus(cm); }
+ }
+
+ // This will be set to a {lineWise: bool, text: [string]} object, so
+ // that, when pasting, we know what kind of selections the copied
+ // text was made out of.
+ var lastCopied = null;
+
+ function applyTextInput(cm, inserted, deleted, sel, origin) {
+ var doc = cm.doc;
+ cm.display.shift = false;
+ if (!sel) sel = doc.sel;
+
+ var paste = cm.state.pasteIncoming || origin == "paste";
+ var textLines = doc.splitLines(inserted), multiPaste = null
+ // When pasing N lines into N selections, insert one line per selection
+ if (paste && sel.ranges.length > 1) {
+ if (lastCopied && lastCopied.text.join("\n") == inserted) {
+ if (sel.ranges.length % lastCopied.text.length == 0) {
+ multiPaste = [];
+ for (var i = 0; i < lastCopied.text.length; i++)
+ multiPaste.push(doc.splitLines(lastCopied.text[i]));
+ }
+ } else if (textLines.length == sel.ranges.length) {
+ multiPaste = map(textLines, function(l) { return [l]; });
+ }
+ }
+
+ // Normal behavior is to insert the new text into every selection
+ for (var i = sel.ranges.length - 1; i >= 0; i--) {
+ var range = sel.ranges[i];
+ var from = range.from(), to = range.to();
+ if (range.empty()) {
+ if (deleted && deleted > 0) // Handle deletion
+ from = Pos(from.line, from.ch - deleted);
+ else if (cm.state.overwrite && !paste) // Handle overwrite
+ to = Pos(to.line, Math.min(getLine(doc, to.line).text.length, to.ch + lst(textLines).length));
+ else if (lastCopied && lastCopied.lineWise && lastCopied.text.join("\n") == inserted)
+ from = to = Pos(from.line, 0)
+ }
+ var updateInput = cm.curOp.updateInput;
+ var changeEvent = {from: from, to: to, text: multiPaste ? multiPaste[i % multiPaste.length] : textLines,
+ origin: origin || (paste ? "paste" : cm.state.cutIncoming ? "cut" : "+input")};
+ makeChange(cm.doc, changeEvent);
+ signalLater(cm, "inputRead", cm, changeEvent);
+ }
+ if (inserted && !paste)
+ triggerElectric(cm, inserted);
+
+ ensureCursorVisible(cm);
+ cm.curOp.updateInput = updateInput;
+ cm.curOp.typing = true;
+ cm.state.pasteIncoming = cm.state.cutIncoming = false;
+ }
+
+ function handlePaste(e, cm) {
+ var pasted = e.clipboardData && e.clipboardData.getData("text/plain");
+ if (pasted) {
+ e.preventDefault();
+ if (!cm.isReadOnly() && !cm.options.disableInput)
+ runInOp(cm, function() { applyTextInput(cm, pasted, 0, null, "paste"); });
+ return true;
+ }
+ }
+
+ function triggerElectric(cm, inserted) {
+ // When an 'electric' character is inserted, immediately trigger a reindent
+ if (!cm.options.electricChars || !cm.options.smartIndent) return;
+ var sel = cm.doc.sel;
+
+ for (var i = sel.ranges.length - 1; i >= 0; i--) {
+ var range = sel.ranges[i];
+ if (range.head.ch > 100 || (i && sel.ranges[i - 1].head.line == range.head.line)) continue;
+ var mode = cm.getModeAt(range.head);
+ var indented = false;
+ if (mode.electricChars) {
+ for (var j = 0; j < mode.electricChars.length; j++)
+ if (inserted.indexOf(mode.electricChars.charAt(j)) > -1) {
+ indented = indentLine(cm, range.head.line, "smart");
+ break;
+ }
+ } else if (mode.electricInput) {
+ if (mode.electricInput.test(getLine(cm.doc, range.head.line).text.slice(0, range.head.ch)))
+ indented = indentLine(cm, range.head.line, "smart");
+ }
+ if (indented) signalLater(cm, "electricInput", cm, range.head.line);
+ }
+ }
+
+ function copyableRanges(cm) {
+ var text = [], ranges = [];
+ for (var i = 0; i < cm.doc.sel.ranges.length; i++) {
+ var line = cm.doc.sel.ranges[i].head.line;
+ var lineRange = {anchor: Pos(line, 0), head: Pos(line + 1, 0)};
+ ranges.push(lineRange);
+ text.push(cm.getRange(lineRange.anchor, lineRange.head));
+ }
+ return {text: text, ranges: ranges};
+ }
+
+ function disableBrowserMagic(field) {
+ field.setAttribute("autocorrect", "off");
+ field.setAttribute("autocapitalize", "off");
+ field.setAttribute("spellcheck", "false");
+ }
+
+ // TEXTAREA INPUT STYLE
+
+ function TextareaInput(cm) {
+ this.cm = cm;
+ // See input.poll and input.reset
+ this.prevInput = "";
+
+ // Flag that indicates whether we expect input to appear real soon
+ // now (after some event like 'keypress' or 'input') and are
+ // polling intensively.
+ this.pollingFast = false;
+ // Self-resetting timeout for the poller
+ this.polling = new Delayed();
+ // Tracks when input.reset has punted to just putting a short
+ // string into the textarea instead of the full selection.
+ this.inaccurateSelection = false;
+ // Used to work around IE issue with selection being forgotten when focus moves away from textarea
+ this.hasSelection = false;
+ this.composing = null;
+ };
+
+ function hiddenTextarea() {
+ var te = elt("textarea", null, null, "position: absolute; padding: 0; width: 1px; height: 1em; outline: none");
+ var div = elt("div", [te], null, "overflow: hidden; position: relative; width: 3px; height: 0px;");
+ // The textarea is kept positioned near the cursor to prevent the
+ // fact that it'll be scrolled into view on input from scrolling
+ // our fake cursor out of view. On webkit, when wrap=off, paste is
+ // very slow. So make the area wide instead.
+ if (webkit) te.style.width = "1000px";
+ else te.setAttribute("wrap", "off");
+ // If border: 0; -- iOS fails to open keyboard (issue #1287)
+ if (ios) te.style.border = "1px solid black";
+ disableBrowserMagic(te);
+ return div;
+ }
+
+ TextareaInput.prototype = copyObj({
+ init: function(display) {
+ var input = this, cm = this.cm;
+
+ // Wraps and hides input textarea
+ var div = this.wrapper = hiddenTextarea();
+ // The semihidden textarea that is focused when the editor is
+ // focused, and receives input.
+ var te = this.textarea = div.firstChild;
+ display.wrapper.insertBefore(div, display.wrapper.firstChild);
+
+ // Needed to hide big blue blinking cursor on Mobile Safari (doesn't seem to work in iOS 8 anymore)
+ if (ios) te.style.width = "0px";
+
+ on(te, "input", function() {
+ if (ie && ie_version >= 9 && input.hasSelection) input.hasSelection = null;
+ input.poll();
+ });
+
+ on(te, "paste", function(e) {
+ if (signalDOMEvent(cm, e) || handlePaste(e, cm)) return
+
+ cm.state.pasteIncoming = true;
+ input.fastPoll();
+ });
+
+ function prepareCopyCut(e) {
+ if (signalDOMEvent(cm, e)) return
+ if (cm.somethingSelected()) {
+ lastCopied = {lineWise: false, text: cm.getSelections()};
+ if (input.inaccurateSelection) {
+ input.prevInput = "";
+ input.inaccurateSelection = false;
+ te.value = lastCopied.text.join("\n");
+ selectInput(te);
+ }
+ } else if (!cm.options.lineWiseCopyCut) {
+ return;
+ } else {
+ var ranges = copyableRanges(cm);
+ lastCopied = {lineWise: true, text: ranges.text};
+ if (e.type == "cut") {
+ cm.setSelections(ranges.ranges, null, sel_dontScroll);
+ } else {
+ input.prevInput = "";
+ te.value = ranges.text.join("\n");
+ selectInput(te);
+ }
+ }
+ if (e.type == "cut") cm.state.cutIncoming = true;
+ }
+ on(te, "cut", prepareCopyCut);
+ on(te, "copy", prepareCopyCut);
+
+ on(display.scroller, "paste", function(e) {
+ if (eventInWidget(display, e) || signalDOMEvent(cm, e)) return;
+ cm.state.pasteIncoming = true;
+ input.focus();
+ });
+
+ // Prevent normal selection in the editor (we handle our own)
+ on(display.lineSpace, "selectstart", function(e) {
+ if (!eventInWidget(display, e)) e_preventDefault(e);
+ });
+
+ on(te, "compositionstart", function() {
+ var start = cm.getCursor("from");
+ if (input.composing) input.composing.range.clear()
+ input.composing = {
+ start: start,
+ range: cm.markText(start, cm.getCursor("to"), {className: "CodeMirror-composing"})
+ };
+ });
+ on(te, "compositionend", function() {
+ if (input.composing) {
+ input.poll();
+ input.composing.range.clear();
+ input.composing = null;
+ }
+ });
+ },
+
+ prepareSelection: function() {
+ // Redraw the selection and/or cursor
+ var cm = this.cm, display = cm.display, doc = cm.doc;
+ var result = prepareSelection(cm);
+
+ // Move the hidden textarea near the cursor to prevent scrolling artifacts
+ if (cm.options.moveInputWithCursor) {
+ var headPos = cursorCoords(cm, doc.sel.primary().head, "div");
+ var wrapOff = display.wrapper.getBoundingClientRect(), lineOff = display.lineDiv.getBoundingClientRect();
+ result.teTop = Math.max(0, Math.min(display.wrapper.clientHeight - 10,
+ headPos.top + lineOff.top - wrapOff.top));
+ result.teLeft = Math.max(0, Math.min(display.wrapper.clientWidth - 10,
+ headPos.left + lineOff.left - wrapOff.left));
+ }
+
+ return result;
+ },
+
+ showSelection: function(drawn) {
+ var cm = this.cm, display = cm.display;
+ removeChildrenAndAdd(display.cursorDiv, drawn.cursors);
+ removeChildrenAndAdd(display.selectionDiv, drawn.selection);
+ if (drawn.teTop != null) {
+ this.wrapper.style.top = drawn.teTop + "px";
+ this.wrapper.style.left = drawn.teLeft + "px";
+ }
+ },
+
+ // Reset the input to correspond to the selection (or to be empty,
+ // when not typing and nothing is selected)
+ reset: function(typing) {
+ if (this.contextMenuPending) return;
+ var minimal, selected, cm = this.cm, doc = cm.doc;
+ if (cm.somethingSelected()) {
+ this.prevInput = "";
+ var range = doc.sel.primary();
+ minimal = hasCopyEvent &&
+ (range.to().line - range.from().line > 100 || (selected = cm.getSelection()).length > 1000);
+ var content = minimal ? "-" : selected || cm.getSelection();
+ this.textarea.value = content;
+ if (cm.state.focused) selectInput(this.textarea);
+ if (ie && ie_version >= 9) this.hasSelection = content;
+ } else if (!typing) {
+ this.prevInput = this.textarea.value = "";
+ if (ie && ie_version >= 9) this.hasSelection = null;
+ }
+ this.inaccurateSelection = minimal;
+ },
+
+ getField: function() { return this.textarea; },
+
+ supportsTouch: function() { return false; },
+
+ focus: function() {
+ if (this.cm.options.readOnly != "nocursor" && (!mobile || activeElt() != this.textarea)) {
+ try { this.textarea.focus(); }
+ catch (e) {} // IE8 will throw if the textarea is display: none or not in DOM
+ }
+ },
+
+ blur: function() { this.textarea.blur(); },
+
+ resetPosition: function() {
+ this.wrapper.style.top = this.wrapper.style.left = 0;
+ },
+
+ receivedFocus: function() { this.slowPoll(); },
+
+ // Poll for input changes, using the normal rate of polling. This
+ // runs as long as the editor is focused.
+ slowPoll: function() {
+ var input = this;
+ if (input.pollingFast) return;
+ input.polling.set(this.cm.options.pollInterval, function() {
+ input.poll();
+ if (input.cm.state.focused) input.slowPoll();
+ });
+ },
+
+ // When an event has just come in that is likely to add or change
+ // something in the input textarea, we poll faster, to ensure that
+ // the change appears on the screen quickly.
+ fastPoll: function() {
+ var missed = false, input = this;
+ input.pollingFast = true;
+ function p() {
+ var changed = input.poll();
+ if (!changed && !missed) {missed = true; input.polling.set(60, p);}
+ else {input.pollingFast = false; input.slowPoll();}
+ }
+ input.polling.set(20, p);
+ },
+
+ // Read input from the textarea, and update the document to match.
+ // When something is selected, it is present in the textarea, and
+ // selected (unless it is huge, in which case a placeholder is
+ // used). When nothing is selected, the cursor sits after previously
+ // seen text (can be empty), which is stored in prevInput (we must
+ // not reset the textarea when typing, because that breaks IME).
+ poll: function() {
+ var cm = this.cm, input = this.textarea, prevInput = this.prevInput;
+ // Since this is called a *lot*, try to bail out as cheaply as
+ // possible when it is clear that nothing happened. hasSelection
+ // will be the case when there is a lot of text in the textarea,
+ // in which case reading its value would be expensive.
+ if (this.contextMenuPending || !cm.state.focused ||
+ (hasSelection(input) && !prevInput && !this.composing) ||
+ cm.isReadOnly() || cm.options.disableInput || cm.state.keySeq)
+ return false;
+
+ var text = input.value;
+ // If nothing changed, bail.
+ if (text == prevInput && !cm.somethingSelected()) return false;
+ // Work around nonsensical selection resetting in IE9/10, and
+ // inexplicable appearance of private area unicode characters on
+ // some key combos in Mac (#2689).
+ if (ie && ie_version >= 9 && this.hasSelection === text ||
+ mac && /[\uf700-\uf7ff]/.test(text)) {
+ cm.display.input.reset();
+ return false;
+ }
+
+ if (cm.doc.sel == cm.display.selForContextMenu) {
+ var first = text.charCodeAt(0);
+ if (first == 0x200b && !prevInput) prevInput = "\u200b";
+ if (first == 0x21da) { this.reset(); return this.cm.execCommand("undo"); }
+ }
+ // Find the part of the input that is actually new
+ var same = 0, l = Math.min(prevInput.length, text.length);
+ while (same < l && prevInput.charCodeAt(same) == text.charCodeAt(same)) ++same;
+
+ var self = this;
+ runInOp(cm, function() {
+ applyTextInput(cm, text.slice(same), prevInput.length - same,
+ null, self.composing ? "*compose" : null);
+
+ // Don't leave long text in the textarea, since it makes further polling slow
+ if (text.length > 1000 || text.indexOf("\n") > -1) input.value = self.prevInput = "";
+ else self.prevInput = text;
+
+ if (self.composing) {
+ self.composing.range.clear();
+ self.composing.range = cm.markText(self.composing.start, cm.getCursor("to"),
+ {className: "CodeMirror-composing"});
+ }
+ });
+ return true;
+ },
+
+ ensurePolled: function() {
+ if (this.pollingFast && this.poll()) this.pollingFast = false;
+ },
+
+ onKeyPress: function() {
+ if (ie && ie_version >= 9) this.hasSelection = null;
+ this.fastPoll();
+ },
+
+ onContextMenu: function(e) {
+ var input = this, cm = input.cm, display = cm.display, te = input.textarea;
+ var pos = posFromMouse(cm, e), scrollPos = display.scroller.scrollTop;
+ if (!pos || presto) return; // Opera is difficult.
+
+ // Reset the current text selection only if the click is done outside of the selection
+ // and 'resetSelectionOnContextMenu' option is true.
+ var reset = cm.options.resetSelectionOnContextMenu;
+ if (reset && cm.doc.sel.contains(pos) == -1)
+ operation(cm, setSelection)(cm.doc, simpleSelection(pos), sel_dontScroll);
+
+ var oldCSS = te.style.cssText, oldWrapperCSS = input.wrapper.style.cssText;
+ input.wrapper.style.cssText = "position: absolute"
+ var wrapperBox = input.wrapper.getBoundingClientRect()
+ te.style.cssText = "position: absolute; width: 30px; height: 30px; top: " + (e.clientY - wrapperBox.top - 5) +
+ "px; left: " + (e.clientX - wrapperBox.left - 5) + "px; z-index: 1000; background: " +
+ (ie ? "rgba(255, 255, 255, .05)" : "transparent") +
+ "; outline: none; border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity=5);";
+ if (webkit) var oldScrollY = window.scrollY; // Work around Chrome issue (#2712)
+ display.input.focus();
+ if (webkit) window.scrollTo(null, oldScrollY);
+ display.input.reset();
+ // Adds "Select all" to context menu in FF
+ if (!cm.somethingSelected()) te.value = input.prevInput = " ";
+ input.contextMenuPending = true;
+ display.selForContextMenu = cm.doc.sel;
+ clearTimeout(display.detectingSelectAll);
+
+ // Select-all will be greyed out if there's nothing to select, so
+ // this adds a zero-width space so that we can later check whether
+ // it got selected.
+ function prepareSelectAllHack() {
+ if (te.selectionStart != null) {
+ var selected = cm.somethingSelected();
+ var extval = "\u200b" + (selected ? te.value : "");
+ te.value = "\u21da"; // Used to catch context-menu undo
+ te.value = extval;
+ input.prevInput = selected ? "" : "\u200b";
+ te.selectionStart = 1; te.selectionEnd = extval.length;
+ // Re-set this, in case some other handler touched the
+ // selection in the meantime.
+ display.selForContextMenu = cm.doc.sel;
+ }
+ }
+ function rehide() {
+ input.contextMenuPending = false;
+ input.wrapper.style.cssText = oldWrapperCSS
+ te.style.cssText = oldCSS;
+ if (ie && ie_version < 9) display.scrollbars.setScrollTop(display.scroller.scrollTop = scrollPos);
+
+ // Try to detect the user choosing select-all
+ if (te.selectionStart != null) {
+ if (!ie || (ie && ie_version < 9)) prepareSelectAllHack();
+ var i = 0, poll = function() {
+ if (display.selForContextMenu == cm.doc.sel && te.selectionStart == 0 &&
+ te.selectionEnd > 0 && input.prevInput == "\u200b")
+ operation(cm, commands.selectAll)(cm);
+ else if (i++ < 10) display.detectingSelectAll = setTimeout(poll, 500);
+ else display.input.reset();
+ };
+ display.detectingSelectAll = setTimeout(poll, 200);
+ }
+ }
+
+ if (ie && ie_version >= 9) prepareSelectAllHack();
+ if (captureRightClick) {
+ e_stop(e);
+ var mouseup = function() {
+ off(window, "mouseup", mouseup);
+ setTimeout(rehide, 20);
+ };
+ on(window, "mouseup", mouseup);
+ } else {
+ setTimeout(rehide, 50);
+ }
+ },
+
+ readOnlyChanged: function(val) {
+ if (!val) this.reset();
+ },
+
+ setUneditable: nothing,
+
+ needsContentAttribute: false
+ }, TextareaInput.prototype);
+
+ // CONTENTEDITABLE INPUT STYLE
+
+ function ContentEditableInput(cm) {
+ this.cm = cm;
+ this.lastAnchorNode = this.lastAnchorOffset = this.lastFocusNode = this.lastFocusOffset = null;
+ this.polling = new Delayed();
+ this.gracePeriod = false;
+ }
+
+ ContentEditableInput.prototype = copyObj({
+ init: function(display) {
+ var input = this, cm = input.cm;
+ var div = input.div = display.lineDiv;
+ disableBrowserMagic(div);
+
+ on(div, "paste", function(e) {
+ if (!signalDOMEvent(cm, e)) handlePaste(e, cm);
+ })
+
+ on(div, "compositionstart", function(e) {
+ var data = e.data;
+ input.composing = {sel: cm.doc.sel, data: data, startData: data};
+ if (!data) return;
+ var prim = cm.doc.sel.primary();
+ var line = cm.getLine(prim.head.line);
+ var found = line.indexOf(data, Math.max(0, prim.head.ch - data.length));
+ if (found > -1 && found <= prim.head.ch)
+ input.composing.sel = simpleSelection(Pos(prim.head.line, found),
+ Pos(prim.head.line, found + data.length));
+ });
+ on(div, "compositionupdate", function(e) {
+ input.composing.data = e.data;
+ });
+ on(div, "compositionend", function(e) {
+ var ours = input.composing;
+ if (!ours) return;
+ if (e.data != ours.startData && !/\u200b/.test(e.data))
+ ours.data = e.data;
+ // Need a small delay to prevent other code (input event,
+ // selection polling) from doing damage when fired right after
+ // compositionend.
+ setTimeout(function() {
+ if (!ours.handled)
+ input.applyComposition(ours);
+ if (input.composing == ours)
+ input.composing = null;
+ }, 50);
+ });
+
+ on(div, "touchstart", function() {
+ input.forceCompositionEnd();
+ });
+
+ on(div, "input", function() {
+ if (input.composing) return;
+ if (cm.isReadOnly() || !input.pollContent())
+ runInOp(input.cm, function() {regChange(cm);});
+ });
+
+ function onCopyCut(e) {
+ if (signalDOMEvent(cm, e)) return
+ if (cm.somethingSelected()) {
+ lastCopied = {lineWise: false, text: cm.getSelections()};
+ if (e.type == "cut") cm.replaceSelection("", null, "cut");
+ } else if (!cm.options.lineWiseCopyCut) {
+ return;
+ } else {
+ var ranges = copyableRanges(cm);
+ lastCopied = {lineWise: true, text: ranges.text};
+ if (e.type == "cut") {
+ cm.operation(function() {
+ cm.setSelections(ranges.ranges, 0, sel_dontScroll);
+ cm.replaceSelection("", null, "cut");
+ });
+ }
+ }
+ // iOS exposes the clipboard API, but seems to discard content inserted into it
+ if (e.clipboardData && !ios) {
+ e.preventDefault();
+ e.clipboardData.clearData();
+ e.clipboardData.setData("text/plain", lastCopied.text.join("\n"));
+ } else {
+ // Old-fashioned briefly-focus-a-textarea hack
+ var kludge = hiddenTextarea(), te = kludge.firstChild;
+ cm.display.lineSpace.insertBefore(kludge, cm.display.lineSpace.firstChild);
+ te.value = lastCopied.text.join("\n");
+ var hadFocus = document.activeElement;
+ selectInput(te);
+ setTimeout(function() {
+ cm.display.lineSpace.removeChild(kludge);
+ hadFocus.focus();
+ }, 50);
+ }
+ }
+ on(div, "copy", onCopyCut);
+ on(div, "cut", onCopyCut);
+ },
+
+ prepareSelection: function() {
+ var result = prepareSelection(this.cm, false);
+ result.focus = this.cm.state.focused;
+ return result;
+ },
+
+ showSelection: function(info, takeFocus) {
+ if (!info || !this.cm.display.view.length) return;
+ if (info.focus || takeFocus) this.showPrimarySelection();
+ this.showMultipleSelections(info);
+ },
+
+ showPrimarySelection: function() {
+ var sel = window.getSelection(), prim = this.cm.doc.sel.primary();
+ var curAnchor = domToPos(this.cm, sel.anchorNode, sel.anchorOffset);
+ var curFocus = domToPos(this.cm, sel.focusNode, sel.focusOffset);
+ if (curAnchor && !curAnchor.bad && curFocus && !curFocus.bad &&
+ cmp(minPos(curAnchor, curFocus), prim.from()) == 0 &&
+ cmp(maxPos(curAnchor, curFocus), prim.to()) == 0)
+ return;
+
+ var start = posToDOM(this.cm, prim.from());
+ var end = posToDOM(this.cm, prim.to());
+ if (!start && !end) return;
+
+ var view = this.cm.display.view;
+ var old = sel.rangeCount && sel.getRangeAt(0);
+ if (!start) {
+ start = {node: view[0].measure.map[2], offset: 0};
+ } else if (!end) { // FIXME dangerously hacky
+ var measure = view[view.length - 1].measure;
+ var map = measure.maps ? measure.maps[measure.maps.length - 1] : measure.map;
+ end = {node: map[map.length - 1], offset: map[map.length - 2] - map[map.length - 3]};
+ }
+
+ try { var rng = range(start.node, start.offset, end.offset, end.node); }
+ catch(e) {} // Our model of the DOM might be outdated, in which case the range we try to set can be impossible
+ if (rng) {
+ if (!gecko && this.cm.state.focused) {
+ sel.collapse(start.node, start.offset);
+ if (!rng.collapsed) sel.addRange(rng);
+ } else {
+ sel.removeAllRanges();
+ sel.addRange(rng);
+ }
+ if (old && sel.anchorNode == null) sel.addRange(old);
+ else if (gecko) this.startGracePeriod();
+ }
+ this.rememberSelection();
+ },
+
+ startGracePeriod: function() {
+ var input = this;
+ clearTimeout(this.gracePeriod);
+ this.gracePeriod = setTimeout(function() {
+ input.gracePeriod = false;
+ if (input.selectionChanged())
+ input.cm.operation(function() { input.cm.curOp.selectionChanged = true; });
+ }, 20);
+ },
+
+ showMultipleSelections: function(info) {
+ removeChildrenAndAdd(this.cm.display.cursorDiv, info.cursors);
+ removeChildrenAndAdd(this.cm.display.selectionDiv, info.selection);
+ },
+
+ rememberSelection: function() {
+ var sel = window.getSelection();
+ this.lastAnchorNode = sel.anchorNode; this.lastAnchorOffset = sel.anchorOffset;
+ this.lastFocusNode = sel.focusNode; this.lastFocusOffset = sel.focusOffset;
+ },
+
+ selectionInEditor: function() {
+ var sel = window.getSelection();
+ if (!sel.rangeCount) return false;
+ var node = sel.getRangeAt(0).commonAncestorContainer;
+ return contains(this.div, node);
+ },
+
+ focus: function() {
+ if (this.cm.options.readOnly != "nocursor") this.div.focus();
+ },
+ blur: function() { this.div.blur(); },
+ getField: function() { return this.div; },
+
+ supportsTouch: function() { return true; },
+
+ receivedFocus: function() {
+ var input = this;
+ if (this.selectionInEditor())
+ this.pollSelection();
+ else
+ runInOp(this.cm, function() { input.cm.curOp.selectionChanged = true; });
+
+ function poll() {
+ if (input.cm.state.focused) {
+ input.pollSelection();
+ input.polling.set(input.cm.options.pollInterval, poll);
+ }
+ }
+ this.polling.set(this.cm.options.pollInterval, poll);
+ },
+
+ selectionChanged: function() {
+ var sel = window.getSelection();
+ return sel.anchorNode != this.lastAnchorNode || sel.anchorOffset != this.lastAnchorOffset ||
+ sel.focusNode != this.lastFocusNode || sel.focusOffset != this.lastFocusOffset;
+ },
+
+ pollSelection: function() {
+ if (!this.composing && !this.gracePeriod && this.selectionChanged()) {
+ var sel = window.getSelection(), cm = this.cm;
+ this.rememberSelection();
+ var anchor = domToPos(cm, sel.anchorNode, sel.anchorOffset);
+ var head = domToPos(cm, sel.focusNode, sel.focusOffset);
+ if (anchor && head) runInOp(cm, function() {
+ setSelection(cm.doc, simpleSelection(anchor, head), sel_dontScroll);
+ if (anchor.bad || head.bad) cm.curOp.selectionChanged = true;
+ });
+ }
+ },
+
+ pollContent: function() {
+ var cm = this.cm, display = cm.display, sel = cm.doc.sel.primary();
+ var from = sel.from(), to = sel.to();
+ if (from.line < display.viewFrom || to.line > display.viewTo - 1) return false;
+
+ var fromIndex;
+ if (from.line == display.viewFrom || (fromIndex = findViewIndex(cm, from.line)) == 0) {
+ var fromLine = lineNo(display.view[0].line);
+ var fromNode = display.view[0].node;
+ } else {
+ var fromLine = lineNo(display.view[fromIndex].line);
+ var fromNode = display.view[fromIndex - 1].node.nextSibling;
+ }
+ var toIndex = findViewIndex(cm, to.line);
+ if (toIndex == display.view.length - 1) {
+ var toLine = display.viewTo - 1;
+ var toNode = display.lineDiv.lastChild;
+ } else {
+ var toLine = lineNo(display.view[toIndex + 1].line) - 1;
+ var toNode = display.view[toIndex + 1].node.previousSibling;
+ }
+
+ var newText = cm.doc.splitLines(domTextBetween(cm, fromNode, toNode, fromLine, toLine));
+ var oldText = getBetween(cm.doc, Pos(fromLine, 0), Pos(toLine, getLine(cm.doc, toLine).text.length));
+ while (newText.length > 1 && oldText.length > 1) {
+ if (lst(newText) == lst(oldText)) { newText.pop(); oldText.pop(); toLine--; }
+ else if (newText[0] == oldText[0]) { newText.shift(); oldText.shift(); fromLine++; }
+ else break;
+ }
+
+ var cutFront = 0, cutEnd = 0;
+ var newTop = newText[0], oldTop = oldText[0], maxCutFront = Math.min(newTop.length, oldTop.length);
+ while (cutFront < maxCutFront && newTop.charCodeAt(cutFront) == oldTop.charCodeAt(cutFront))
+ ++cutFront;
+ var newBot = lst(newText), oldBot = lst(oldText);
+ var maxCutEnd = Math.min(newBot.length - (newText.length == 1 ? cutFront : 0),
+ oldBot.length - (oldText.length == 1 ? cutFront : 0));
+ while (cutEnd < maxCutEnd &&
+ newBot.charCodeAt(newBot.length - cutEnd - 1) == oldBot.charCodeAt(oldBot.length - cutEnd - 1))
+ ++cutEnd;
+
+ newText[newText.length - 1] = newBot.slice(0, newBot.length - cutEnd);
+ newText[0] = newText[0].slice(cutFront);
+
+ var chFrom = Pos(fromLine, cutFront);
+ var chTo = Pos(toLine, oldText.length ? lst(oldText).length - cutEnd : 0);
+ if (newText.length > 1 || newText[0] || cmp(chFrom, chTo)) {
+ replaceRange(cm.doc, newText, chFrom, chTo, "+input");
+ return true;
+ }
+ },
+
+ ensurePolled: function() {
+ this.forceCompositionEnd();
+ },
+ reset: function() {
+ this.forceCompositionEnd();
+ },
+ forceCompositionEnd: function() {
+ if (!this.composing || this.composing.handled) return;
+ this.applyComposition(this.composing);
+ this.composing.handled = true;
+ this.div.blur();
+ this.div.focus();
+ },
+ applyComposition: function(composing) {
+ if (this.cm.isReadOnly())
+ operation(this.cm, regChange)(this.cm)
+ else if (composing.data && composing.data != composing.startData)
+ operation(this.cm, applyTextInput)(this.cm, composing.data, 0, composing.sel);
+ },
+
+ setUneditable: function(node) {
+ node.contentEditable = "false"
+ },
+
+ onKeyPress: function(e) {
+ e.preventDefault();
+ if (!this.cm.isReadOnly())
+ operation(this.cm, applyTextInput)(this.cm, String.fromCharCode(e.charCode == null ? e.keyCode : e.charCode), 0);
+ },
+
+ readOnlyChanged: function(val) {
+ this.div.contentEditable = String(val != "nocursor")
+ },
+
+ onContextMenu: nothing,
+ resetPosition: nothing,
+
+ needsContentAttribute: true
+ }, ContentEditableInput.prototype);
+
+ function posToDOM(cm, pos) {
+ var view = findViewForLine(cm, pos.line);
+ if (!view || view.hidden) return null;
+ var line = getLine(cm.doc, pos.line);
+ var info = mapFromLineView(view, line, pos.line);
+
+ var order = getOrder(line), side = "left";
+ if (order) {
+ var partPos = getBidiPartAt(order, pos.ch);
+ side = partPos % 2 ? "right" : "left";
+ }
+ var result = nodeAndOffsetInLineMap(info.map, pos.ch, side);
+ result.offset = result.collapse == "right" ? result.end : result.start;
+ return result;
+ }
+
+ function badPos(pos, bad) { if (bad) pos.bad = true; return pos; }
+
+ function domToPos(cm, node, offset) {
+ var lineNode;
+ if (node == cm.display.lineDiv) {
+ lineNode = cm.display.lineDiv.childNodes[offset];
+ if (!lineNode) return badPos(cm.clipPos(Pos(cm.display.viewTo - 1)), true);
+ node = null; offset = 0;
+ } else {
+ for (lineNode = node;; lineNode = lineNode.parentNode) {
+ if (!lineNode || lineNode == cm.display.lineDiv) return null;
+ if (lineNode.parentNode && lineNode.parentNode == cm.display.lineDiv) break;
+ }
+ }
+ for (var i = 0; i < cm.display.view.length; i++) {
+ var lineView = cm.display.view[i];
+ if (lineView.node == lineNode)
+ return locateNodeInLineView(lineView, node, offset);
+ }
+ }
+
+ function locateNodeInLineView(lineView, node, offset) {
+ var wrapper = lineView.text.firstChild, bad = false;
+ if (!node || !contains(wrapper, node)) return badPos(Pos(lineNo(lineView.line), 0), true);
+ if (node == wrapper) {
+ bad = true;
+ node = wrapper.childNodes[offset];
+ offset = 0;
+ if (!node) {
+ var line = lineView.rest ? lst(lineView.rest) : lineView.line;
+ return badPos(Pos(lineNo(line), line.text.length), bad);
+ }
+ }
+
+ var textNode = node.nodeType == 3 ? node : null, topNode = node;
+ if (!textNode && node.childNodes.length == 1 && node.firstChild.nodeType == 3) {
+ textNode = node.firstChild;
+ if (offset) offset = textNode.nodeValue.length;
+ }
+ while (topNode.parentNode != wrapper) topNode = topNode.parentNode;
+ var measure = lineView.measure, maps = measure.maps;
+
+ function find(textNode, topNode, offset) {
+ for (var i = -1; i < (maps ? maps.length : 0); i++) {
+ var map = i < 0 ? measure.map : maps[i];
+ for (var j = 0; j < map.length; j += 3) {
+ var curNode = map[j + 2];
+ if (curNode == textNode || curNode == topNode) {
+ var line = lineNo(i < 0 ? lineView.line : lineView.rest[i]);
+ var ch = map[j] + offset;
+ if (offset < 0 || curNode != textNode) ch = map[j + (offset ? 1 : 0)];
+ return Pos(line, ch);
+ }
+ }
+ }
+ }
+ var found = find(textNode, topNode, offset);
+ if (found) return badPos(found, bad);
+
+ // FIXME this is all really shaky. might handle the few cases it needs to handle, but likely to cause problems
+ for (var after = topNode.nextSibling, dist = textNode ? textNode.nodeValue.length - offset : 0; after; after = after.nextSibling) {
+ found = find(after, after.firstChild, 0);
+ if (found)
+ return badPos(Pos(found.line, found.ch - dist), bad);
+ else
+ dist += after.textContent.length;
+ }
+ for (var before = topNode.previousSibling, dist = offset; before; before = before.previousSibling) {
+ found = find(before, before.firstChild, -1);
+ if (found)
+ return badPos(Pos(found.line, found.ch + dist), bad);
+ else
+ dist += after.textContent.length;
+ }
+ }
+
+ function domTextBetween(cm, from, to, fromLine, toLine) {
+ var text = "", closing = false, lineSep = cm.doc.lineSeparator();
+ function recognizeMarker(id) { return function(marker) { return marker.id == id; }; }
+ function walk(node) {
+ if (node.nodeType == 1) {
+ var cmText = node.getAttribute("cm-text");
+ if (cmText != null) {
+ if (cmText == "") cmText = node.textContent.replace(/\u200b/g, "");
+ text += cmText;
+ return;
+ }
+ var markerID = node.getAttribute("cm-marker"), range;
+ if (markerID) {
+ var found = cm.findMarks(Pos(fromLine, 0), Pos(toLine + 1, 0), recognizeMarker(+markerID));
+ if (found.length && (range = found[0].find()))
+ text += getBetween(cm.doc, range.from, range.to).join(lineSep);
+ return;
+ }
+ if (node.getAttribute("contenteditable") == "false") return;
+ for (var i = 0; i < node.childNodes.length; i++)
+ walk(node.childNodes[i]);
+ if (/^(pre|div|p)$/i.test(node.nodeName))
+ closing = true;
+ } else if (node.nodeType == 3) {
+ var val = node.nodeValue;
+ if (!val) return;
+ if (closing) {
+ text += lineSep;
+ closing = false;
+ }
+ text += val;
+ }
+ }
+ for (;;) {
+ walk(from);
+ if (from == to) break;
+ from = from.nextSibling;
+ }
+ return text;
+ }
+
+ CodeMirror.inputStyles = {"textarea": TextareaInput, "contenteditable": ContentEditableInput};
+
+ // SELECTION / CURSOR
+
+ // Selection objects are immutable. A new one is created every time
+ // the selection changes. A selection is one or more non-overlapping
+ // (and non-touching) ranges, sorted, and an integer that indicates
+ // which one is the primary selection (the one that's scrolled into
+ // view, that getCursor returns, etc).
+ function Selection(ranges, primIndex) {
+ this.ranges = ranges;
+ this.primIndex = primIndex;
+ }
+
+ Selection.prototype = {
+ primary: function() { return this.ranges[this.primIndex]; },
+ equals: function(other) {
+ if (other == this) return true;
+ if (other.primIndex != this.primIndex || other.ranges.length != this.ranges.length) return false;
+ for (var i = 0; i < this.ranges.length; i++) {
+ var here = this.ranges[i], there = other.ranges[i];
+ if (cmp(here.anchor, there.anchor) != 0 || cmp(here.head, there.head) != 0) return false;
+ }
+ return true;
+ },
+ deepCopy: function() {
+ for (var out = [], i = 0; i < this.ranges.length; i++)
+ out[i] = new Range(copyPos(this.ranges[i].anchor), copyPos(this.ranges[i].head));
+ return new Selection(out, this.primIndex);
+ },
+ somethingSelected: function() {
+ for (var i = 0; i < this.ranges.length; i++)
+ if (!this.ranges[i].empty()) return true;
+ return false;
+ },
+ contains: function(pos, end) {
+ if (!end) end = pos;
+ for (var i = 0; i < this.ranges.length; i++) {
+ var range = this.ranges[i];
+ if (cmp(end, range.from()) >= 0 && cmp(pos, range.to()) <= 0)
+ return i;
+ }
+ return -1;
+ }
+ };
+
+ function Range(anchor, head) {
+ this.anchor = anchor; this.head = head;
+ }
+
+ Range.prototype = {
+ from: function() { return minPos(this.anchor, this.head); },
+ to: function() { return maxPos(this.anchor, this.head); },
+ empty: function() {
+ return this.head.line == this.anchor.line && this.head.ch == this.anchor.ch;
+ }
+ };
+
+ // Take an unsorted, potentially overlapping set of ranges, and
+ // build a selection out of it. 'Consumes' ranges array (modifying
+ // it).
+ function normalizeSelection(ranges, primIndex) {
+ var prim = ranges[primIndex];
+ ranges.sort(function(a, b) { return cmp(a.from(), b.from()); });
+ primIndex = indexOf(ranges, prim);
+ for (var i = 1; i < ranges.length; i++) {
+ var cur = ranges[i], prev = ranges[i - 1];
+ if (cmp(prev.to(), cur.from()) >= 0) {
+ var from = minPos(prev.from(), cur.from()), to = maxPos(prev.to(), cur.to());
+ var inv = prev.empty() ? cur.from() == cur.head : prev.from() == prev.head;
+ if (i <= primIndex) --primIndex;
+ ranges.splice(--i, 2, new Range(inv ? to : from, inv ? from : to));
+ }
+ }
+ return new Selection(ranges, primIndex);
+ }
+
+ function simpleSelection(anchor, head) {
+ return new Selection([new Range(anchor, head || anchor)], 0);
+ }
+
+ // Most of the external API clips given positions to make sure they
+ // actually exist within the document.
+ function clipLine(doc, n) {return Math.max(doc.first, Math.min(n, doc.first + doc.size - 1));}
+ function clipPos(doc, pos) {
+ if (pos.line < doc.first) return Pos(doc.first, 0);
+ var last = doc.first + doc.size - 1;
+ if (pos.line > last) return Pos(last, getLine(doc, last).text.length);
+ return clipToLen(pos, getLine(doc, pos.line).text.length);
+ }
+ function clipToLen(pos, linelen) {
+ var ch = pos.ch;
+ if (ch == null || ch > linelen) return Pos(pos.line, linelen);
+ else if (ch < 0) return Pos(pos.line, 0);
+ else return pos;
+ }
+ function isLine(doc, l) {return l >= doc.first && l < doc.first + doc.size;}
+ function clipPosArray(doc, array) {
+ for (var out = [], i = 0; i < array.length; i++) out[i] = clipPos(doc, array[i]);
+ return out;
+ }
+
+ // SELECTION UPDATES
+
+ // The 'scroll' parameter given to many of these indicated whether
+ // the new cursor position should be scrolled into view after
+ // modifying the selection.
+
+ // If shift is held or the extend flag is set, extends a range to
+ // include a given position (and optionally a second position).
+ // Otherwise, simply returns the range between the given positions.
+ // Used for cursor motion and such.
+ function extendRange(doc, range, head, other) {
+ if (doc.cm && doc.cm.display.shift || doc.extend) {
+ var anchor = range.anchor;
+ if (other) {
+ var posBefore = cmp(head, anchor) < 0;
+ if (posBefore != (cmp(other, anchor) < 0)) {
+ anchor = head;
+ head = other;
+ } else if (posBefore != (cmp(head, other) < 0)) {
+ head = other;
+ }
+ }
+ return new Range(anchor, head);
+ } else {
+ return new Range(other || head, head);
+ }
+ }
+
+ // Extend the primary selection range, discard the rest.
+ function extendSelection(doc, head, other, options) {
+ setSelection(doc, new Selection([extendRange(doc, doc.sel.primary(), head, other)], 0), options);
+ }
+
+ // Extend all selections (pos is an array of selections with length
+ // equal the number of selections)
+ function extendSelections(doc, heads, options) {
+ for (var out = [], i = 0; i < doc.sel.ranges.length; i++)
+ out[i] = extendRange(doc, doc.sel.ranges[i], heads[i], null);
+ var newSel = normalizeSelection(out, doc.sel.primIndex);
+ setSelection(doc, newSel, options);
+ }
+
+ // Updates a single range in the selection.
+ function replaceOneSelection(doc, i, range, options) {
+ var ranges = doc.sel.ranges.slice(0);
+ ranges[i] = range;
+ setSelection(doc, normalizeSelection(ranges, doc.sel.primIndex), options);
+ }
+
+ // Reset the selection to a single range.
+ function setSimpleSelection(doc, anchor, head, options) {
+ setSelection(doc, simpleSelection(anchor, head), options);
+ }
+
+ // Give beforeSelectionChange handlers a change to influence a
+ // selection update.
+ function filterSelectionChange(doc, sel, options) {
+ var obj = {
+ ranges: sel.ranges,
+ update: function(ranges) {
+ this.ranges = [];
+ for (var i = 0; i < ranges.length; i++)
+ this.ranges[i] = new Range(clipPos(doc, ranges[i].anchor),
+ clipPos(doc, ranges[i].head));
+ },
+ origin: options && options.origin
+ };
+ signal(doc, "beforeSelectionChange", doc, obj);
+ if (doc.cm) signal(doc.cm, "beforeSelectionChange", doc.cm, obj);
+ if (obj.ranges != sel.ranges) return normalizeSelection(obj.ranges, obj.ranges.length - 1);
+ else return sel;
+ }
+
+ function setSelectionReplaceHistory(doc, sel, options) {
+ var done = doc.history.done, last = lst(done);
+ if (last && last.ranges) {
+ done[done.length - 1] = sel;
+ setSelectionNoUndo(doc, sel, options);
+ } else {
+ setSelection(doc, sel, options);
+ }
+ }
+
+ // Set a new selection.
+ function setSelection(doc, sel, options) {
+ setSelectionNoUndo(doc, sel, options);
+ addSelectionToHistory(doc, doc.sel, doc.cm ? doc.cm.curOp.id : NaN, options);
+ }
+
+ function setSelectionNoUndo(doc, sel, options) {
+ if (hasHandler(doc, "beforeSelectionChange") || doc.cm && hasHandler(doc.cm, "beforeSelectionChange"))
+ sel = filterSelectionChange(doc, sel, options);
+
+ var bias = options && options.bias ||
+ (cmp(sel.primary().head, doc.sel.primary().head) < 0 ? -1 : 1);
+ setSelectionInner(doc, skipAtomicInSelection(doc, sel, bias, true));
+
+ if (!(options && options.scroll === false) && doc.cm)
+ ensureCursorVisible(doc.cm);
+ }
+
+ function setSelectionInner(doc, sel) {
+ if (sel.equals(doc.sel)) return;
+
+ doc.sel = sel;
+
+ if (doc.cm) {
+ doc.cm.curOp.updateInput = doc.cm.curOp.selectionChanged = true;
+ signalCursorActivity(doc.cm);
+ }
+ signalLater(doc, "cursorActivity", doc);
+ }
+
+ // Verify that the selection does not partially select any atomic
+ // marked ranges.
+ function reCheckSelection(doc) {
+ setSelectionInner(doc, skipAtomicInSelection(doc, doc.sel, null, false), sel_dontScroll);
+ }
+
+ // Return a selection that does not partially select any atomic
+ // ranges.
+ function skipAtomicInSelection(doc, sel, bias, mayClear) {
+ var out;
+ for (var i = 0; i < sel.ranges.length; i++) {
+ var range = sel.ranges[i];
+ var old = sel.ranges.length == doc.sel.ranges.length && doc.sel.ranges[i];
+ var newAnchor = skipAtomic(doc, range.anchor, old && old.anchor, bias, mayClear);
+ var newHead = skipAtomic(doc, range.head, old && old.head, bias, mayClear);
+ if (out || newAnchor != range.anchor || newHead != range.head) {
+ if (!out) out = sel.ranges.slice(0, i);
+ out[i] = new Range(newAnchor, newHead);
+ }
+ }
+ return out ? normalizeSelection(out, sel.primIndex) : sel;
+ }
+
+ function skipAtomicInner(doc, pos, oldPos, dir, mayClear) {
+ var line = getLine(doc, pos.line);
+ if (line.markedSpans) for (var i = 0; i < line.markedSpans.length; ++i) {
+ var sp = line.markedSpans[i], m = sp.marker;
+ if ((sp.from == null || (m.inclusiveLeft ? sp.from <= pos.ch : sp.from < pos.ch)) &&
+ (sp.to == null || (m.inclusiveRight ? sp.to >= pos.ch : sp.to > pos.ch))) {
+ if (mayClear) {
+ signal(m, "beforeCursorEnter");
+ if (m.explicitlyCleared) {
+ if (!line.markedSpans) break;
+ else {--i; continue;}
+ }
+ }
+ if (!m.atomic) continue;
+
+ if (oldPos) {
+ var near = m.find(dir < 0 ? 1 : -1), diff;
+ if (dir < 0 ? m.inclusiveRight : m.inclusiveLeft)
+ near = movePos(doc, near, -dir, near && near.line == pos.line ? line : null);
+ if (near && near.line == pos.line && (diff = cmp(near, oldPos)) && (dir < 0 ? diff < 0 : diff > 0))
+ return skipAtomicInner(doc, near, pos, dir, mayClear);
+ }
+
+ var far = m.find(dir < 0 ? -1 : 1);
+ if (dir < 0 ? m.inclusiveLeft : m.inclusiveRight)
+ far = movePos(doc, far, dir, far.line == pos.line ? line : null);
+ return far ? skipAtomicInner(doc, far, pos, dir, mayClear) : null;
+ }
+ }
+ return pos;
+ }
+
+ // Ensure a given position is not inside an atomic range.
+ function skipAtomic(doc, pos, oldPos, bias, mayClear) {
+ var dir = bias || 1;
+ var found = skipAtomicInner(doc, pos, oldPos, dir, mayClear) ||
+ (!mayClear && skipAtomicInner(doc, pos, oldPos, dir, true)) ||
+ skipAtomicInner(doc, pos, oldPos, -dir, mayClear) ||
+ (!mayClear && skipAtomicInner(doc, pos, oldPos, -dir, true));
+ if (!found) {
+ doc.cantEdit = true;
+ return Pos(doc.first, 0);
+ }
+ return found;
+ }
+
+ function movePos(doc, pos, dir, line) {
+ if (dir < 0 && pos.ch == 0) {
+ if (pos.line > doc.first) return clipPos(doc, Pos(pos.line - 1));
+ else return null;
+ } else if (dir > 0 && pos.ch == (line || getLine(doc, pos.line)).text.length) {
+ if (pos.line < doc.first + doc.size - 1) return Pos(pos.line + 1, 0);
+ else return null;
+ } else {
+ return new Pos(pos.line, pos.ch + dir);
+ }
+ }
+
+ // SELECTION DRAWING
+
+ function updateSelection(cm) {
+ cm.display.input.showSelection(cm.display.input.prepareSelection());
+ }
+
+ function prepareSelection(cm, primary) {
+ var doc = cm.doc, result = {};
+ var curFragment = result.cursors = document.createDocumentFragment();
+ var selFragment = result.selection = document.createDocumentFragment();
+
+ for (var i = 0; i < doc.sel.ranges.length; i++) {
+ if (primary === false && i == doc.sel.primIndex) continue;
+ var range = doc.sel.ranges[i];
+ if (range.from().line >= cm.display.viewTo || range.to().line < cm.display.viewFrom) continue;
+ var collapsed = range.empty();
+ if (collapsed || cm.options.showCursorWhenSelecting)
+ drawSelectionCursor(cm, range.head, curFragment);
+ if (!collapsed)
+ drawSelectionRange(cm, range, selFragment);
+ }
+ return result;
+ }
+
+ // Draws a cursor for the given range
+ function drawSelectionCursor(cm, head, output) {
+ var pos = cursorCoords(cm, head, "div", null, null, !cm.options.singleCursorHeightPerLine);
+
+ var cursor = output.appendChild(elt("div", "\u00a0", "CodeMirror-cursor"));
+ cursor.style.left = pos.left + "px";
+ cursor.style.top = pos.top + "px";
+ cursor.style.height = Math.max(0, pos.bottom - pos.top) * cm.options.cursorHeight + "px";
+
+ if (pos.other) {
+ // Secondary cursor, shown when on a 'jump' in bi-directional text
+ var otherCursor = output.appendChild(elt("div", "\u00a0", "CodeMirror-cursor CodeMirror-secondarycursor"));
+ otherCursor.style.display = "";
+ otherCursor.style.left = pos.other.left + "px";
+ otherCursor.style.top = pos.other.top + "px";
+ otherCursor.style.height = (pos.other.bottom - pos.other.top) * .85 + "px";
+ }
+ }
+
+ // Draws the given range as a highlighted selection
+ function drawSelectionRange(cm, range, output) {
+ var display = cm.display, doc = cm.doc;
+ var fragment = document.createDocumentFragment();
+ var padding = paddingH(cm.display), leftSide = padding.left;
+ var rightSide = Math.max(display.sizerWidth, displayWidth(cm) - display.sizer.offsetLeft) - padding.right;
+
+ function add(left, top, width, bottom) {
+ if (top < 0) top = 0;
+ top = Math.round(top);
+ bottom = Math.round(bottom);
+ fragment.appendChild(elt("div", null, "CodeMirror-selected", "position: absolute; left: " + left +
+ "px; top: " + top + "px; width: " + (width == null ? rightSide - left : width) +
+ "px; height: " + (bottom - top) + "px"));
+ }
+
+ function drawForLine(line, fromArg, toArg) {
+ var lineObj = getLine(doc, line);
+ var lineLen = lineObj.text.length;
+ var start, end;
+ function coords(ch, bias) {
+ return charCoords(cm, Pos(line, ch), "div", lineObj, bias);
+ }
+
+ iterateBidiSections(getOrder(lineObj), fromArg || 0, toArg == null ? lineLen : toArg, function(from, to, dir) {
+ var leftPos = coords(from, "left"), rightPos, left, right;
+ if (from == to) {
+ rightPos = leftPos;
+ left = right = leftPos.left;
+ } else {
+ rightPos = coords(to - 1, "right");
+ if (dir == "rtl") { var tmp = leftPos; leftPos = rightPos; rightPos = tmp; }
+ left = leftPos.left;
+ right = rightPos.right;
+ }
+ if (fromArg == null && from == 0) left = leftSide;
+ if (rightPos.top - leftPos.top > 3) { // Different lines, draw top part
+ add(left, leftPos.top, null, leftPos.bottom);
+ left = leftSide;
+ if (leftPos.bottom < rightPos.top) add(left, leftPos.bottom, null, rightPos.top);
+ }
+ if (toArg == null && to == lineLen) right = rightSide;
+ if (!start || leftPos.top < start.top || leftPos.top == start.top && leftPos.left < start.left)
+ start = leftPos;
+ if (!end || rightPos.bottom > end.bottom || rightPos.bottom == end.bottom && rightPos.right > end.right)
+ end = rightPos;
+ if (left < leftSide + 1) left = leftSide;
+ add(left, rightPos.top, right - left, rightPos.bottom);
+ });
+ return {start: start, end: end};
+ }
+
+ var sFrom = range.from(), sTo = range.to();
+ if (sFrom.line == sTo.line) {
+ drawForLine(sFrom.line, sFrom.ch, sTo.ch);
+ } else {
+ var fromLine = getLine(doc, sFrom.line), toLine = getLine(doc, sTo.line);
+ var singleVLine = visualLine(fromLine) == visualLine(toLine);
+ var leftEnd = drawForLine(sFrom.line, sFrom.ch, singleVLine ? fromLine.text.length + 1 : null).end;
+ var rightStart = drawForLine(sTo.line, singleVLine ? 0 : null, sTo.ch).start;
+ if (singleVLine) {
+ if (leftEnd.top < rightStart.top - 2) {
+ add(leftEnd.right, leftEnd.top, null, leftEnd.bottom);
+ add(leftSide, rightStart.top, rightStart.left, rightStart.bottom);
+ } else {
+ add(leftEnd.right, leftEnd.top, rightStart.left - leftEnd.right, leftEnd.bottom);
+ }
+ }
+ if (leftEnd.bottom < rightStart.top)
+ add(leftSide, leftEnd.bottom, null, rightStart.top);
+ }
+
+ output.appendChild(fragment);
+ }
+
+ // Cursor-blinking
+ function restartBlink(cm) {
+ if (!cm.state.focused) return;
+ var display = cm.display;
+ clearInterval(display.blinker);
+ var on = true;
+ display.cursorDiv.style.visibility = "";
+ if (cm.options.cursorBlinkRate > 0)
+ display.blinker = setInterval(function() {
+ display.cursorDiv.style.visibility = (on = !on) ? "" : "hidden";
+ }, cm.options.cursorBlinkRate);
+ else if (cm.options.cursorBlinkRate < 0)
+ display.cursorDiv.style.visibility = "hidden";
+ }
+
+ // HIGHLIGHT WORKER
+
+ function startWorker(cm, time) {
+ if (cm.doc.mode.startState && cm.doc.frontier < cm.display.viewTo)
+ cm.state.highlight.set(time, bind(highlightWorker, cm));
+ }
+
+ function highlightWorker(cm) {
+ var doc = cm.doc;
+ if (doc.frontier < doc.first) doc.frontier = doc.first;
+ if (doc.frontier >= cm.display.viewTo) return;
+ var end = +new Date + cm.options.workTime;
+ var state = copyState(doc.mode, getStateBefore(cm, doc.frontier));
+ var changedLines = [];
+
+ doc.iter(doc.frontier, Math.min(doc.first + doc.size, cm.display.viewTo + 500), function(line) {
+ if (doc.frontier >= cm.display.viewFrom) { // Visible
+ var oldStyles = line.styles, tooLong = line.text.length > cm.options.maxHighlightLength;
+ var highlighted = highlightLine(cm, line, tooLong ? copyState(doc.mode, state) : state, true);
+ line.styles = highlighted.styles;
+ var oldCls = line.styleClasses, newCls = highlighted.classes;
+ if (newCls) line.styleClasses = newCls;
+ else if (oldCls) line.styleClasses = null;
+ var ischange = !oldStyles || oldStyles.length != line.styles.length ||
+ oldCls != newCls && (!oldCls || !newCls || oldCls.bgClass != newCls.bgClass || oldCls.textClass != newCls.textClass);
+ for (var i = 0; !ischange && i < oldStyles.length; ++i) ischange = oldStyles[i] != line.styles[i];
+ if (ischange) changedLines.push(doc.frontier);
+ line.stateAfter = tooLong ? state : copyState(doc.mode, state);
+ } else {
+ if (line.text.length <= cm.options.maxHighlightLength)
+ processLine(cm, line.text, state);
+ line.stateAfter = doc.frontier % 5 == 0 ? copyState(doc.mode, state) : null;
+ }
+ ++doc.frontier;
+ if (+new Date > end) {
+ startWorker(cm, cm.options.workDelay);
+ return true;
+ }
+ });
+ if (changedLines.length) runInOp(cm, function() {
+ for (var i = 0; i < changedLines.length; i++)
+ regLineChange(cm, changedLines[i], "text");
+ });
+ }
+
+ // Finds the line to start with when starting a parse. Tries to
+ // find a line with a stateAfter, so that it can start with a
+ // valid state. If that fails, it returns the line with the
+ // smallest indentation, which tends to need the least context to
+ // parse correctly.
+ function findStartLine(cm, n, precise) {
+ var minindent, minline, doc = cm.doc;
+ var lim = precise ? -1 : n - (cm.doc.mode.innerMode ? 1000 : 100);
+ for (var search = n; search > lim; --search) {
+ if (search <= doc.first) return doc.first;
+ var line = getLine(doc, search - 1);
+ if (line.stateAfter && (!precise || search <= doc.frontier)) return search;
+ var indented = countColumn(line.text, null, cm.options.tabSize);
+ if (minline == null || minindent > indented) {
+ minline = search - 1;
+ minindent = indented;
+ }
+ }
+ return minline;
+ }
+
+ function getStateBefore(cm, n, precise) {
+ var doc = cm.doc, display = cm.display;
+ if (!doc.mode.startState) return true;
+ var pos = findStartLine(cm, n, precise), state = pos > doc.first && getLine(doc, pos-1).stateAfter;
+ if (!state) state = startState(doc.mode);
+ else state = copyState(doc.mode, state);
+ doc.iter(pos, n, function(line) {
+ processLine(cm, line.text, state);
+ var save = pos == n - 1 || pos % 5 == 0 || pos >= display.viewFrom && pos < display.viewTo;
+ line.stateAfter = save ? copyState(doc.mode, state) : null;
+ ++pos;
+ });
+ if (precise) doc.frontier = pos;
+ return state;
+ }
+
+ // POSITION MEASUREMENT
+
+ function paddingTop(display) {return display.lineSpace.offsetTop;}
+ function paddingVert(display) {return display.mover.offsetHeight - display.lineSpace.offsetHeight;}
+ function paddingH(display) {
+ if (display.cachedPaddingH) return display.cachedPaddingH;
+ var e = removeChildrenAndAdd(display.measure, elt("pre", "x"));
+ var style = window.getComputedStyle ? window.getComputedStyle(e) : e.currentStyle;
+ var data = {left: parseInt(style.paddingLeft), right: parseInt(style.paddingRight)};
+ if (!isNaN(data.left) && !isNaN(data.right)) display.cachedPaddingH = data;
+ return data;
+ }
+
+ function scrollGap(cm) { return scrollerGap - cm.display.nativeBarWidth; }
+ function displayWidth(cm) {
+ return cm.display.scroller.clientWidth - scrollGap(cm) - cm.display.barWidth;
+ }
+ function displayHeight(cm) {
+ return cm.display.scroller.clientHeight - scrollGap(cm) - cm.display.barHeight;
+ }
+
+ // Ensure the lineView.wrapping.heights array is populated. This is
+ // an array of bottom offsets for the lines that make up a drawn
+ // line. When lineWrapping is on, there might be more than one
+ // height.
+ function ensureLineHeights(cm, lineView, rect) {
+ var wrapping = cm.options.lineWrapping;
+ var curWidth = wrapping && displayWidth(cm);
+ if (!lineView.measure.heights || wrapping && lineView.measure.width != curWidth) {
+ var heights = lineView.measure.heights = [];
+ if (wrapping) {
+ lineView.measure.width = curWidth;
+ var rects = lineView.text.firstChild.getClientRects();
+ for (var i = 0; i < rects.length - 1; i++) {
+ var cur = rects[i], next = rects[i + 1];
+ if (Math.abs(cur.bottom - next.bottom) > 2)
+ heights.push((cur.bottom + next.top) / 2 - rect.top);
+ }
+ }
+ heights.push(rect.bottom - rect.top);
+ }
+ }
+
+ // Find a line map (mapping character offsets to text nodes) and a
+ // measurement cache for the given line number. (A line view might
+ // contain multiple lines when collapsed ranges are present.)
+ function mapFromLineView(lineView, line, lineN) {
+ if (lineView.line == line)
+ return {map: lineView.measure.map, cache: lineView.measure.cache};
+ for (var i = 0; i < lineView.rest.length; i++)
+ if (lineView.rest[i] == line)
+ return {map: lineView.measure.maps[i], cache: lineView.measure.caches[i]};
+ for (var i = 0; i < lineView.rest.length; i++)
+ if (lineNo(lineView.rest[i]) > lineN)
+ return {map: lineView.measure.maps[i], cache: lineView.measure.caches[i], before: true};
+ }
+
+ // Render a line into the hidden node display.externalMeasured. Used
+ // when measurement is needed for a line that's not in the viewport.
+ function updateExternalMeasurement(cm, line) {
+ line = visualLine(line);
+ var lineN = lineNo(line);
+ var view = cm.display.externalMeasured = new LineView(cm.doc, line, lineN);
+ view.lineN = lineN;
+ var built = view.built = buildLineContent(cm, view);
+ view.text = built.pre;
+ removeChildrenAndAdd(cm.display.lineMeasure, built.pre);
+ return view;
+ }
+
+ // Get a {top, bottom, left, right} box (in line-local coordinates)
+ // for a given character.
+ function measureChar(cm, line, ch, bias) {
+ return measureCharPrepared(cm, prepareMeasureForLine(cm, line), ch, bias);
+ }
+
+ // Find a line view that corresponds to the given line number.
+ function findViewForLine(cm, lineN) {
+ if (lineN >= cm.display.viewFrom && lineN < cm.display.viewTo)
+ return cm.display.view[findViewIndex(cm, lineN)];
+ var ext = cm.display.externalMeasured;
+ if (ext && lineN >= ext.lineN && lineN < ext.lineN + ext.size)
+ return ext;
+ }
+
+ // Measurement can be split in two steps, the set-up work that
+ // applies to the whole line, and the measurement of the actual
+ // character. Functions like coordsChar, that need to do a lot of
+ // measurements in a row, can thus ensure that the set-up work is
+ // only done once.
+ function prepareMeasureForLine(cm, line) {
+ var lineN = lineNo(line);
+ var view = findViewForLine(cm, lineN);
+ if (view && !view.text) {
+ view = null;
+ } else if (view && view.changes) {
+ updateLineForChanges(cm, view, lineN, getDimensions(cm));
+ cm.curOp.forceUpdate = true;
+ }
+ if (!view)
+ view = updateExternalMeasurement(cm, line);
+
+ var info = mapFromLineView(view, line, lineN);
+ return {
+ line: line, view: view, rect: null,
+ map: info.map, cache: info.cache, before: info.before,
+ hasHeights: false
+ };
+ }
+
+ // Given a prepared measurement object, measures the position of an
+ // actual character (or fetches it from the cache).
+ function measureCharPrepared(cm, prepared, ch, bias, varHeight) {
+ if (prepared.before) ch = -1;
+ var key = ch + (bias || ""), found;
+ if (prepared.cache.hasOwnProperty(key)) {
+ found = prepared.cache[key];
+ } else {
+ if (!prepared.rect)
+ prepared.rect = prepared.view.text.getBoundingClientRect();
+ if (!prepared.hasHeights) {
+ ensureLineHeights(cm, prepared.view, prepared.rect);
+ prepared.hasHeights = true;
+ }
+ found = measureCharInner(cm, prepared, ch, bias);
+ if (!found.bogus) prepared.cache[key] = found;
+ }
+ return {left: found.left, right: found.right,
+ top: varHeight ? found.rtop : found.top,
+ bottom: varHeight ? found.rbottom : found.bottom};
+ }
+
+ var nullRect = {left: 0, right: 0, top: 0, bottom: 0};
+
+ function nodeAndOffsetInLineMap(map, ch, bias) {
+ var node, start, end, collapse;
+ // First, search the line map for the text node corresponding to,
+ // or closest to, the target character.
+ for (var i = 0; i < map.length; i += 3) {
+ var mStart = map[i], mEnd = map[i + 1];
+ if (ch < mStart) {
+ start = 0; end = 1;
+ collapse = "left";
+ } else if (ch < mEnd) {
+ start = ch - mStart;
+ end = start + 1;
+ } else if (i == map.length - 3 || ch == mEnd && map[i + 3] > ch) {
+ end = mEnd - mStart;
+ start = end - 1;
+ if (ch >= mEnd) collapse = "right";
+ }
+ if (start != null) {
+ node = map[i + 2];
+ if (mStart == mEnd && bias == (node.insertLeft ? "left" : "right"))
+ collapse = bias;
+ if (bias == "left" && start == 0)
+ while (i && map[i - 2] == map[i - 3] && map[i - 1].insertLeft) {
+ node = map[(i -= 3) + 2];
+ collapse = "left";
+ }
+ if (bias == "right" && start == mEnd - mStart)
+ while (i < map.length - 3 && map[i + 3] == map[i + 4] && !map[i + 5].insertLeft) {
+ node = map[(i += 3) + 2];
+ collapse = "right";
+ }
+ break;
+ }
+ }
+ return {node: node, start: start, end: end, collapse: collapse, coverStart: mStart, coverEnd: mEnd};
+ }
+
+ function measureCharInner(cm, prepared, ch, bias) {
+ var place = nodeAndOffsetInLineMap(prepared.map, ch, bias);
+ var node = place.node, start = place.start, end = place.end, collapse = place.collapse;
+
+ var rect;
+ if (node.nodeType == 3) { // If it is a text node, use a range to retrieve the coordinates.
+ for (var i = 0; i < 4; i++) { // Retry a maximum of 4 times when nonsense rectangles are returned
+ while (start && isExtendingChar(prepared.line.text.charAt(place.coverStart + start))) --start;
+ while (place.coverStart + end < place.coverEnd && isExtendingChar(prepared.line.text.charAt(place.coverStart + end))) ++end;
+ if (ie && ie_version < 9 && start == 0 && end == place.coverEnd - place.coverStart) {
+ rect = node.parentNode.getBoundingClientRect();
+ } else if (ie && cm.options.lineWrapping) {
+ var rects = range(node, start, end).getClientRects();
+ if (rects.length)
+ rect = rects[bias == "right" ? rects.length - 1 : 0];
+ else
+ rect = nullRect;
+ } else {
+ rect = range(node, start, end).getBoundingClientRect() || nullRect;
+ }
+ if (rect.left || rect.right || start == 0) break;
+ end = start;
+ start = start - 1;
+ collapse = "right";
+ }
+ if (ie && ie_version < 11) rect = maybeUpdateRectForZooming(cm.display.measure, rect);
+ } else { // If it is a widget, simply get the box for the whole widget.
+ if (start > 0) collapse = bias = "right";
+ var rects;
+ if (cm.options.lineWrapping && (rects = node.getClientRects()).length > 1)
+ rect = rects[bias == "right" ? rects.length - 1 : 0];
+ else
+ rect = node.getBoundingClientRect();
+ }
+ if (ie && ie_version < 9 && !start && (!rect || !rect.left && !rect.right)) {
+ var rSpan = node.parentNode.getClientRects()[0];
+ if (rSpan)
+ rect = {left: rSpan.left, right: rSpan.left + charWidth(cm.display), top: rSpan.top, bottom: rSpan.bottom};
+ else
+ rect = nullRect;
+ }
+
+ var rtop = rect.top - prepared.rect.top, rbot = rect.bottom - prepared.rect.top;
+ var mid = (rtop + rbot) / 2;
+ var heights = prepared.view.measure.heights;
+ for (var i = 0; i < heights.length - 1; i++)
+ if (mid < heights[i]) break;
+ var top = i ? heights[i - 1] : 0, bot = heights[i];
+ var result = {left: (collapse == "right" ? rect.right : rect.left) - prepared.rect.left,
+ right: (collapse == "left" ? rect.left : rect.right) - prepared.rect.left,
+ top: top, bottom: bot};
+ if (!rect.left && !rect.right) result.bogus = true;
+ if (!cm.options.singleCursorHeightPerLine) { result.rtop = rtop; result.rbottom = rbot; }
+
+ return result;
+ }
+
+ // Work around problem with bounding client rects on ranges being
+ // returned incorrectly when zoomed on IE10 and below.
+ function maybeUpdateRectForZooming(measure, rect) {
+ if (!window.screen || screen.logicalXDPI == null ||
+ screen.logicalXDPI == screen.deviceXDPI || !hasBadZoomedRects(measure))
+ return rect;
+ var scaleX = screen.logicalXDPI / screen.deviceXDPI;
+ var scaleY = screen.logicalYDPI / screen.deviceYDPI;
+ return {left: rect.left * scaleX, right: rect.right * scaleX,
+ top: rect.top * scaleY, bottom: rect.bottom * scaleY};
+ }
+
+ function clearLineMeasurementCacheFor(lineView) {
+ if (lineView.measure) {
+ lineView.measure.cache = {};
+ lineView.measure.heights = null;
+ if (lineView.rest) for (var i = 0; i < lineView.rest.length; i++)
+ lineView.measure.caches[i] = {};
+ }
+ }
+
+ function clearLineMeasurementCache(cm) {
+ cm.display.externalMeasure = null;
+ removeChildren(cm.display.lineMeasure);
+ for (var i = 0; i < cm.display.view.length; i++)
+ clearLineMeasurementCacheFor(cm.display.view[i]);
+ }
+
+ function clearCaches(cm) {
+ clearLineMeasurementCache(cm);
+ cm.display.cachedCharWidth = cm.display.cachedTextHeight = cm.display.cachedPaddingH = null;
+ if (!cm.options.lineWrapping) cm.display.maxLineChanged = true;
+ cm.display.lineNumChars = null;
+ }
+
+ function pageScrollX() { return window.pageXOffset || (document.documentElement || document.body).scrollLeft; }
+ function pageScrollY() { return window.pageYOffset || (document.documentElement || document.body).scrollTop; }
+
+ // Converts a {top, bottom, left, right} box from line-local
+ // coordinates into another coordinate system. Context may be one of
+ // "line", "div" (display.lineDiv), "local"/null (editor), "window",
+ // or "page".
+ function intoCoordSystem(cm, lineObj, rect, context) {
+ if (lineObj.widgets) for (var i = 0; i < lineObj.widgets.length; ++i) if (lineObj.widgets[i].above) {
+ var size = widgetHeight(lineObj.widgets[i]);
+ rect.top += size; rect.bottom += size;
+ }
+ if (context == "line") return rect;
+ if (!context) context = "local";
+ var yOff = heightAtLine(lineObj);
+ if (context == "local") yOff += paddingTop(cm.display);
+ else yOff -= cm.display.viewOffset;
+ if (context == "page" || context == "window") {
+ var lOff = cm.display.lineSpace.getBoundingClientRect();
+ yOff += lOff.top + (context == "window" ? 0 : pageScrollY());
+ var xOff = lOff.left + (context == "window" ? 0 : pageScrollX());
+ rect.left += xOff; rect.right += xOff;
+ }
+ rect.top += yOff; rect.bottom += yOff;
+ return rect;
+ }
+
+ // Coverts a box from "div" coords to another coordinate system.
+ // Context may be "window", "page", "div", or "local"/null.
+ function fromCoordSystem(cm, coords, context) {
+ if (context == "div") return coords;
+ var left = coords.left, top = coords.top;
+ // First move into "page" coordinate system
+ if (context == "page") {
+ left -= pageScrollX();
+ top -= pageScrollY();
+ } else if (context == "local" || !context) {
+ var localBox = cm.display.sizer.getBoundingClientRect();
+ left += localBox.left;
+ top += localBox.top;
+ }
+
+ var lineSpaceBox = cm.display.lineSpace.getBoundingClientRect();
+ return {left: left - lineSpaceBox.left, top: top - lineSpaceBox.top};
+ }
+
+ function charCoords(cm, pos, context, lineObj, bias) {
+ if (!lineObj) lineObj = getLine(cm.doc, pos.line);
+ return intoCoordSystem(cm, lineObj, measureChar(cm, lineObj, pos.ch, bias), context);
+ }
+
+ // Returns a box for a given cursor position, which may have an
+ // 'other' property containing the position of the secondary cursor
+ // on a bidi boundary.
+ function cursorCoords(cm, pos, context, lineObj, preparedMeasure, varHeight) {
+ lineObj = lineObj || getLine(cm.doc, pos.line);
+ if (!preparedMeasure) preparedMeasure = prepareMeasureForLine(cm, lineObj);
+ function get(ch, right) {
+ var m = measureCharPrepared(cm, preparedMeasure, ch, right ? "right" : "left", varHeight);
+ if (right) m.left = m.right; else m.right = m.left;
+ return intoCoordSystem(cm, lineObj, m, context);
+ }
+ function getBidi(ch, partPos) {
+ var part = order[partPos], right = part.level % 2;
+ if (ch == bidiLeft(part) && partPos && part.level < order[partPos - 1].level) {
+ part = order[--partPos];
+ ch = bidiRight(part) - (part.level % 2 ? 0 : 1);
+ right = true;
+ } else if (ch == bidiRight(part) && partPos < order.length - 1 && part.level < order[partPos + 1].level) {
+ part = order[++partPos];
+ ch = bidiLeft(part) - part.level % 2;
+ right = false;
+ }
+ if (right && ch == part.to && ch > part.from) return get(ch - 1);
+ return get(ch, right);
+ }
+ var order = getOrder(lineObj), ch = pos.ch;
+ if (!order) return get(ch);
+ var partPos = getBidiPartAt(order, ch);
+ var val = getBidi(ch, partPos);
+ if (bidiOther != null) val.other = getBidi(ch, bidiOther);
+ return val;
+ }
+
+ // Used to cheaply estimate the coordinates for a position. Used for
+ // intermediate scroll updates.
+ function estimateCoords(cm, pos) {
+ var left = 0, pos = clipPos(cm.doc, pos);
+ if (!cm.options.lineWrapping) left = charWidth(cm.display) * pos.ch;
+ var lineObj = getLine(cm.doc, pos.line);
+ var top = heightAtLine(lineObj) + paddingTop(cm.display);
+ return {left: left, right: left, top: top, bottom: top + lineObj.height};
+ }
+
+ // Positions returned by coordsChar contain some extra information.
+ // xRel is the relative x position of the input coordinates compared
+ // to the found position (so xRel > 0 means the coordinates are to
+ // the right of the character position, for example). When outside
+ // is true, that means the coordinates lie outside the line's
+ // vertical range.
+ function PosWithInfo(line, ch, outside, xRel) {
+ var pos = Pos(line, ch);
+ pos.xRel = xRel;
+ if (outside) pos.outside = true;
+ return pos;
+ }
+
+ // Compute the character position closest to the given coordinates.
+ // Input must be lineSpace-local ("div" coordinate system).
+ function coordsChar(cm, x, y) {
+ var doc = cm.doc;
+ y += cm.display.viewOffset;
+ if (y < 0) return PosWithInfo(doc.first, 0, true, -1);
+ var lineN = lineAtHeight(doc, y), last = doc.first + doc.size - 1;
+ if (lineN > last)
+ return PosWithInfo(doc.first + doc.size - 1, getLine(doc, last).text.length, true, 1);
+ if (x < 0) x = 0;
+
+ var lineObj = getLine(doc, lineN);
+ for (;;) {
+ var found = coordsCharInner(cm, lineObj, lineN, x, y);
+ var merged = collapsedSpanAtEnd(lineObj);
+ var mergedPos = merged && merged.find(0, true);
+ if (merged && (found.ch > mergedPos.from.ch || found.ch == mergedPos.from.ch && found.xRel > 0))
+ lineN = lineNo(lineObj = mergedPos.to.line);
+ else
+ return found;
+ }
+ }
+
+ function coordsCharInner(cm, lineObj, lineNo, x, y) {
+ var innerOff = y - heightAtLine(lineObj);
+ var wrongLine = false, adjust = 2 * cm.display.wrapper.clientWidth;
+ var preparedMeasure = prepareMeasureForLine(cm, lineObj);
+
+ function getX(ch) {
+ var sp = cursorCoords(cm, Pos(lineNo, ch), "line", lineObj, preparedMeasure);
+ wrongLine = true;
+ if (innerOff > sp.bottom) return sp.left - adjust;
+ else if (innerOff < sp.top) return sp.left + adjust;
+ else wrongLine = false;
+ return sp.left;
+ }
+
+ var bidi = getOrder(lineObj), dist = lineObj.text.length;
+ var from = lineLeft(lineObj), to = lineRight(lineObj);
+ var fromX = getX(from), fromOutside = wrongLine, toX = getX(to), toOutside = wrongLine;
+
+ if (x > toX) return PosWithInfo(lineNo, to, toOutside, 1);
+ // Do a binary search between these bounds.
+ for (;;) {
+ if (bidi ? to == from || to == moveVisually(lineObj, from, 1) : to - from <= 1) {
+ var ch = x < fromX || x - fromX <= toX - x ? from : to;
+ var outside = ch == from ? fromOutside : toOutside
+ var xDiff = x - (ch == from ? fromX : toX);
+ // This is a kludge to handle the case where the coordinates
+ // are after a line-wrapped line. We should replace it with a
+ // more general handling of cursor positions around line
+ // breaks. (Issue #4078)
+ if (toOutside && !bidi && !/\s/.test(lineObj.text.charAt(ch)) && xDiff > 0 &&
+ ch < lineObj.text.length && preparedMeasure.view.measure.heights.length > 1) {
+ var charSize = measureCharPrepared(cm, preparedMeasure, ch, "right");
+ if (innerOff <= charSize.bottom && innerOff >= charSize.top && Math.abs(x - charSize.right) < xDiff) {
+ outside = false
+ ch++
+ xDiff = x - charSize.right
+ }
+ }
+ while (isExtendingChar(lineObj.text.charAt(ch))) ++ch;
+ var pos = PosWithInfo(lineNo, ch, outside, xDiff < -1 ? -1 : xDiff > 1 ? 1 : 0);
+ return pos;
+ }
+ var step = Math.ceil(dist / 2), middle = from + step;
+ if (bidi) {
+ middle = from;
+ for (var i = 0; i < step; ++i) middle = moveVisually(lineObj, middle, 1);
+ }
+ var middleX = getX(middle);
+ if (middleX > x) {to = middle; toX = middleX; if (toOutside = wrongLine) toX += 1000; dist = step;}
+ else {from = middle; fromX = middleX; fromOutside = wrongLine; dist -= step;}
+ }
+ }
+
+ var measureText;
+ // Compute the default text height.
+ function textHeight(display) {
+ if (display.cachedTextHeight != null) return display.cachedTextHeight;
+ if (measureText == null) {
+ measureText = elt("pre");
+ // Measure a bunch of lines, for browsers that compute
+ // fractional heights.
+ for (var i = 0; i < 49; ++i) {
+ measureText.appendChild(document.createTextNode("x"));
+ measureText.appendChild(elt("br"));
+ }
+ measureText.appendChild(document.createTextNode("x"));
+ }
+ removeChildrenAndAdd(display.measure, measureText);
+ var height = measureText.offsetHeight / 50;
+ if (height > 3) display.cachedTextHeight = height;
+ removeChildren(display.measure);
+ return height || 1;
+ }
+
+ // Compute the default character width.
+ function charWidth(display) {
+ if (display.cachedCharWidth != null) return display.cachedCharWidth;
+ var anchor = elt("span", "xxxxxxxxxx");
+ var pre = elt("pre", [anchor]);
+ removeChildrenAndAdd(display.measure, pre);
+ var rect = anchor.getBoundingClientRect(), width = (rect.right - rect.left) / 10;
+ if (width > 2) display.cachedCharWidth = width;
+ return width || 10;
+ }
+
+ // OPERATIONS
+
+ // Operations are used to wrap a series of changes to the editor
+ // state in such a way that each change won't have to update the
+ // cursor and display (which would be awkward, slow, and
+ // error-prone). Instead, display updates are batched and then all
+ // combined and executed at once.
+
+ var operationGroup = null;
+
+ var nextOpId = 0;
+ // Start a new operation.
+ function startOperation(cm) {
+ cm.curOp = {
+ cm: cm,
+ viewChanged: false, // Flag that indicates that lines might need to be redrawn
+ startHeight: cm.doc.height, // Used to detect need to update scrollbar
+ forceUpdate: false, // Used to force a redraw
+ updateInput: null, // Whether to reset the input textarea
+ typing: false, // Whether this reset should be careful to leave existing text (for compositing)
+ changeObjs: null, // Accumulated changes, for firing change events
+ cursorActivityHandlers: null, // Set of handlers to fire cursorActivity on
+ cursorActivityCalled: 0, // Tracks which cursorActivity handlers have been called already
+ selectionChanged: false, // Whether the selection needs to be redrawn
+ updateMaxLine: false, // Set when the widest line needs to be determined anew
+ scrollLeft: null, scrollTop: null, // Intermediate scroll position, not pushed to DOM yet
+ scrollToPos: null, // Used to scroll to a specific position
+ focus: false,
+ id: ++nextOpId // Unique ID
+ };
+ if (operationGroup) {
+ operationGroup.ops.push(cm.curOp);
+ } else {
+ cm.curOp.ownsGroup = operationGroup = {
+ ops: [cm.curOp],
+ delayedCallbacks: []
+ };
+ }
+ }
+
+ function fireCallbacksForOps(group) {
+ // Calls delayed callbacks and cursorActivity handlers until no
+ // new ones appear
+ var callbacks = group.delayedCallbacks, i = 0;
+ do {
+ for (; i < callbacks.length; i++)
+ callbacks[i].call(null);
+ for (var j = 0; j < group.ops.length; j++) {
+ var op = group.ops[j];
+ if (op.cursorActivityHandlers)
+ while (op.cursorActivityCalled < op.cursorActivityHandlers.length)
+ op.cursorActivityHandlers[op.cursorActivityCalled++].call(null, op.cm);
+ }
+ } while (i < callbacks.length);
+ }
+
+ // Finish an operation, updating the display and signalling delayed events
+ function endOperation(cm) {
+ var op = cm.curOp, group = op.ownsGroup;
+ if (!group) return;
+
+ try { fireCallbacksForOps(group); }
+ finally {
+ operationGroup = null;
+ for (var i = 0; i < group.ops.length; i++)
+ group.ops[i].cm.curOp = null;
+ endOperations(group);
+ }
+ }
+
+ // The DOM updates done when an operation finishes are batched so
+ // that the minimum number of relayouts are required.
+ function endOperations(group) {
+ var ops = group.ops;
+ for (var i = 0; i < ops.length; i++) // Read DOM
+ endOperation_R1(ops[i]);
+ for (var i = 0; i < ops.length; i++) // Write DOM (maybe)
+ endOperation_W1(ops[i]);
+ for (var i = 0; i < ops.length; i++) // Read DOM
+ endOperation_R2(ops[i]);
+ for (var i = 0; i < ops.length; i++) // Write DOM (maybe)
+ endOperation_W2(ops[i]);
+ for (var i = 0; i < ops.length; i++) // Read DOM
+ endOperation_finish(ops[i]);
+ }
+
+ function endOperation_R1(op) {
+ var cm = op.cm, display = cm.display;
+ maybeClipScrollbars(cm);
+ if (op.updateMaxLine) findMaxLine(cm);
+
+ op.mustUpdate = op.viewChanged || op.forceUpdate || op.scrollTop != null ||
+ op.scrollToPos && (op.scrollToPos.from.line < display.viewFrom ||
+ op.scrollToPos.to.line >= display.viewTo) ||
+ display.maxLineChanged && cm.options.lineWrapping;
+ op.update = op.mustUpdate &&
+ new DisplayUpdate(cm, op.mustUpdate && {top: op.scrollTop, ensure: op.scrollToPos}, op.forceUpdate);
+ }
+
+ function endOperation_W1(op) {
+ op.updatedDisplay = op.mustUpdate && updateDisplayIfNeeded(op.cm, op.update);
+ }
+
+ function endOperation_R2(op) {
+ var cm = op.cm, display = cm.display;
+ if (op.updatedDisplay) updateHeightsInViewport(cm);
+
+ op.barMeasure = measureForScrollbars(cm);
+
+ // If the max line changed since it was last measured, measure it,
+ // and ensure the document's width matches it.
+ // updateDisplay_W2 will use these properties to do the actual resizing
+ if (display.maxLineChanged && !cm.options.lineWrapping) {
+ op.adjustWidthTo = measureChar(cm, display.maxLine, display.maxLine.text.length).left + 3;
+ cm.display.sizerWidth = op.adjustWidthTo;
+ op.barMeasure.scrollWidth =
+ Math.max(display.scroller.clientWidth, display.sizer.offsetLeft + op.adjustWidthTo + scrollGap(cm) + cm.display.barWidth);
+ op.maxScrollLeft = Math.max(0, display.sizer.offsetLeft + op.adjustWidthTo - displayWidth(cm));
+ }
+
+ if (op.updatedDisplay || op.selectionChanged)
+ op.preparedSelection = display.input.prepareSelection(op.focus);
+ }
+
+ function endOperation_W2(op) {
+ var cm = op.cm;
+
+ if (op.adjustWidthTo != null) {
+ cm.display.sizer.style.minWidth = op.adjustWidthTo + "px";
+ if (op.maxScrollLeft < cm.doc.scrollLeft)
+ setScrollLeft(cm, Math.min(cm.display.scroller.scrollLeft, op.maxScrollLeft), true);
+ cm.display.maxLineChanged = false;
+ }
+
+ var takeFocus = op.focus && op.focus == activeElt() && (!document.hasFocus || document.hasFocus())
+ if (op.preparedSelection)
+ cm.display.input.showSelection(op.preparedSelection, takeFocus);
+ if (op.updatedDisplay || op.startHeight != cm.doc.height)
+ updateScrollbars(cm, op.barMeasure);
+ if (op.updatedDisplay)
+ setDocumentHeight(cm, op.barMeasure);
+
+ if (op.selectionChanged) restartBlink(cm);
+
+ if (cm.state.focused && op.updateInput)
+ cm.display.input.reset(op.typing);
+ if (takeFocus) ensureFocus(op.cm);
+ }
+
+ function endOperation_finish(op) {
+ var cm = op.cm, display = cm.display, doc = cm.doc;
+
+ if (op.updatedDisplay) postUpdateDisplay(cm, op.update);
+
+ // Abort mouse wheel delta measurement, when scrolling explicitly
+ if (display.wheelStartX != null && (op.scrollTop != null || op.scrollLeft != null || op.scrollToPos))
+ display.wheelStartX = display.wheelStartY = null;
+
+ // Propagate the scroll position to the actual DOM scroller
+ if (op.scrollTop != null && (display.scroller.scrollTop != op.scrollTop || op.forceScroll)) {
+ doc.scrollTop = Math.max(0, Math.min(display.scroller.scrollHeight - display.scroller.clientHeight, op.scrollTop));
+ display.scrollbars.setScrollTop(doc.scrollTop);
+ display.scroller.scrollTop = doc.scrollTop;
+ }
+ if (op.scrollLeft != null && (display.scroller.scrollLeft != op.scrollLeft || op.forceScroll)) {
+ doc.scrollLeft = Math.max(0, Math.min(display.scroller.scrollWidth - display.scroller.clientWidth, op.scrollLeft));
+ display.scrollbars.setScrollLeft(doc.scrollLeft);
+ display.scroller.scrollLeft = doc.scrollLeft;
+ alignHorizontally(cm);
+ }
+ // If we need to scroll a specific position into view, do so.
+ if (op.scrollToPos) {
+ var coords = scrollPosIntoView(cm, clipPos(doc, op.scrollToPos.from),
+ clipPos(doc, op.scrollToPos.to), op.scrollToPos.margin);
+ if (op.scrollToPos.isCursor && cm.state.focused) maybeScrollWindow(cm, coords);
+ }
+
+ // Fire events for markers that are hidden/unidden by editing or
+ // undoing
+ var hidden = op.maybeHiddenMarkers, unhidden = op.maybeUnhiddenMarkers;
+ if (hidden) for (var i = 0; i < hidden.length; ++i)
+ if (!hidden[i].lines.length) signal(hidden[i], "hide");
+ if (unhidden) for (var i = 0; i < unhidden.length; ++i)
+ if (unhidden[i].lines.length) signal(unhidden[i], "unhide");
+
+ if (display.wrapper.offsetHeight)
+ doc.scrollTop = cm.display.scroller.scrollTop;
+
+ // Fire change events, and delayed event handlers
+ if (op.changeObjs)
+ signal(cm, "changes", cm, op.changeObjs);
+ if (op.update)
+ op.update.finish();
+ }
+
+ // Run the given function in an operation
+ function runInOp(cm, f) {
+ if (cm.curOp) return f();
+ startOperation(cm);
+ try { return f(); }
+ finally { endOperation(cm); }
+ }
+ // Wraps a function in an operation. Returns the wrapped function.
+ function operation(cm, f) {
+ return function() {
+ if (cm.curOp) return f.apply(cm, arguments);
+ startOperation(cm);
+ try { return f.apply(cm, arguments); }
+ finally { endOperation(cm); }
+ };
+ }
+ // Used to add methods to editor and doc instances, wrapping them in
+ // operations.
+ function methodOp(f) {
+ return function() {
+ if (this.curOp) return f.apply(this, arguments);
+ startOperation(this);
+ try { return f.apply(this, arguments); }
+ finally { endOperation(this); }
+ };
+ }
+ function docMethodOp(f) {
+ return function() {
+ var cm = this.cm;
+ if (!cm || cm.curOp) return f.apply(this, arguments);
+ startOperation(cm);
+ try { return f.apply(this, arguments); }
+ finally { endOperation(cm); }
+ };
+ }
+
+ // VIEW TRACKING
+
+ // These objects are used to represent the visible (currently drawn)
+ // part of the document. A LineView may correspond to multiple
+ // logical lines, if those are connected by collapsed ranges.
+ function LineView(doc, line, lineN) {
+ // The starting line
+ this.line = line;
+ // Continuing lines, if any
+ this.rest = visualLineContinued(line);
+ // Number of logical lines in this visual line
+ this.size = this.rest ? lineNo(lst(this.rest)) - lineN + 1 : 1;
+ this.node = this.text = null;
+ this.hidden = lineIsHidden(doc, line);
+ }
+
+ // Create a range of LineView objects for the given lines.
+ function buildViewArray(cm, from, to) {
+ var array = [], nextPos;
+ for (var pos = from; pos < to; pos = nextPos) {
+ var view = new LineView(cm.doc, getLine(cm.doc, pos), pos);
+ nextPos = pos + view.size;
+ array.push(view);
+ }
+ return array;
+ }
+
+ // Updates the display.view data structure for a given change to the
+ // document. From and to are in pre-change coordinates. Lendiff is
+ // the amount of lines added or subtracted by the change. This is
+ // used for changes that span multiple lines, or change the way
+ // lines are divided into visual lines. regLineChange (below)
+ // registers single-line changes.
+ function regChange(cm, from, to, lendiff) {
+ if (from == null) from = cm.doc.first;
+ if (to == null) to = cm.doc.first + cm.doc.size;
+ if (!lendiff) lendiff = 0;
+
+ var display = cm.display;
+ if (lendiff && to < display.viewTo &&
+ (display.updateLineNumbers == null || display.updateLineNumbers > from))
+ display.updateLineNumbers = from;
+
+ cm.curOp.viewChanged = true;
+
+ if (from >= display.viewTo) { // Change after
+ if (sawCollapsedSpans && visualLineNo(cm.doc, from) < display.viewTo)
+ resetView(cm);
+ } else if (to <= display.viewFrom) { // Change before
+ if (sawCollapsedSpans && visualLineEndNo(cm.doc, to + lendiff) > display.viewFrom) {
+ resetView(cm);
+ } else {
+ display.viewFrom += lendiff;
+ display.viewTo += lendiff;
+ }
+ } else if (from <= display.viewFrom && to >= display.viewTo) { // Full overlap
+ resetView(cm);
+ } else if (from <= display.viewFrom) { // Top overlap
+ var cut = viewCuttingPoint(cm, to, to + lendiff, 1);
+ if (cut) {
+ display.view = display.view.slice(cut.index);
+ display.viewFrom = cut.lineN;
+ display.viewTo += lendiff;
+ } else {
+ resetView(cm);
+ }
+ } else if (to >= display.viewTo) { // Bottom overlap
+ var cut = viewCuttingPoint(cm, from, from, -1);
+ if (cut) {
+ display.view = display.view.slice(0, cut.index);
+ display.viewTo = cut.lineN;
+ } else {
+ resetView(cm);
+ }
+ } else { // Gap in the middle
+ var cutTop = viewCuttingPoint(cm, from, from, -1);
+ var cutBot = viewCuttingPoint(cm, to, to + lendiff, 1);
+ if (cutTop && cutBot) {
+ display.view = display.view.slice(0, cutTop.index)
+ .concat(buildViewArray(cm, cutTop.lineN, cutBot.lineN))
+ .concat(display.view.slice(cutBot.index));
+ display.viewTo += lendiff;
+ } else {
+ resetView(cm);
+ }
+ }
+
+ var ext = display.externalMeasured;
+ if (ext) {
+ if (to < ext.lineN)
+ ext.lineN += lendiff;
+ else if (from < ext.lineN + ext.size)
+ display.externalMeasured = null;
+ }
+ }
+
+ // Register a change to a single line. Type must be one of "text",
+ // "gutter", "class", "widget"
+ function regLineChange(cm, line, type) {
+ cm.curOp.viewChanged = true;
+ var display = cm.display, ext = cm.display.externalMeasured;
+ if (ext && line >= ext.lineN && line < ext.lineN + ext.size)
+ display.externalMeasured = null;
+
+ if (line < display.viewFrom || line >= display.viewTo) return;
+ var lineView = display.view[findViewIndex(cm, line)];
+ if (lineView.node == null) return;
+ var arr = lineView.changes || (lineView.changes = []);
+ if (indexOf(arr, type) == -1) arr.push(type);
+ }
+
+ // Clear the view.
+ function resetView(cm) {
+ cm.display.viewFrom = cm.display.viewTo = cm.doc.first;
+ cm.display.view = [];
+ cm.display.viewOffset = 0;
+ }
+
+ // Find the view element corresponding to a given line. Return null
+ // when the line isn't visible.
+ function findViewIndex(cm, n) {
+ if (n >= cm.display.viewTo) return null;
+ n -= cm.display.viewFrom;
+ if (n < 0) return null;
+ var view = cm.display.view;
+ for (var i = 0; i < view.length; i++) {
+ n -= view[i].size;
+ if (n < 0) return i;
+ }
+ }
+
+ function viewCuttingPoint(cm, oldN, newN, dir) {
+ var index = findViewIndex(cm, oldN), diff, view = cm.display.view;
+ if (!sawCollapsedSpans || newN == cm.doc.first + cm.doc.size)
+ return {index: index, lineN: newN};
+ for (var i = 0, n = cm.display.viewFrom; i < index; i++)
+ n += view[i].size;
+ if (n != oldN) {
+ if (dir > 0) {
+ if (index == view.length - 1) return null;
+ diff = (n + view[index].size) - oldN;
+ index++;
+ } else {
+ diff = n - oldN;
+ }
+ oldN += diff; newN += diff;
+ }
+ while (visualLineNo(cm.doc, newN) != newN) {
+ if (index == (dir < 0 ? 0 : view.length - 1)) return null;
+ newN += dir * view[index - (dir < 0 ? 1 : 0)].size;
+ index += dir;
+ }
+ return {index: index, lineN: newN};
+ }
+
+ // Force the view to cover a given range, adding empty view element
+ // or clipping off existing ones as needed.
+ function adjustView(cm, from, to) {
+ var display = cm.display, view = display.view;
+ if (view.length == 0 || from >= display.viewTo || to <= display.viewFrom) {
+ display.view = buildViewArray(cm, from, to);
+ display.viewFrom = from;
+ } else {
+ if (display.viewFrom > from)
+ display.view = buildViewArray(cm, from, display.viewFrom).concat(display.view);
+ else if (display.viewFrom < from)
+ display.view = display.view.slice(findViewIndex(cm, from));
+ display.viewFrom = from;
+ if (display.viewTo < to)
+ display.view = display.view.concat(buildViewArray(cm, display.viewTo, to));
+ else if (display.viewTo > to)
+ display.view = display.view.slice(0, findViewIndex(cm, to));
+ }
+ display.viewTo = to;
+ }
+
+ // Count the number of lines in the view whose DOM representation is
+ // out of date (or nonexistent).
+ function countDirtyView(cm) {
+ var view = cm.display.view, dirty = 0;
+ for (var i = 0; i < view.length; i++) {
+ var lineView = view[i];
+ if (!lineView.hidden && (!lineView.node || lineView.changes)) ++dirty;
+ }
+ return dirty;
+ }
+
+ // EVENT HANDLERS
+
+ // Attach the necessary event handlers when initializing the editor
+ function registerEventHandlers(cm) {
+ var d = cm.display;
+ on(d.scroller, "mousedown", operation(cm, onMouseDown));
+ // Older IE's will not fire a second mousedown for a double click
+ if (ie && ie_version < 11)
+ on(d.scroller, "dblclick", operation(cm, function(e) {
+ if (signalDOMEvent(cm, e)) return;
+ var pos = posFromMouse(cm, e);
+ if (!pos || clickInGutter(cm, e) || eventInWidget(cm.display, e)) return;
+ e_preventDefault(e);
+ var word = cm.findWordAt(pos);
+ extendSelection(cm.doc, word.anchor, word.head);
+ }));
+ else
+ on(d.scroller, "dblclick", function(e) { signalDOMEvent(cm, e) || e_preventDefault(e); });
+ // Some browsers fire contextmenu *after* opening the menu, at
+ // which point we can't mess with it anymore. Context menu is
+ // handled in onMouseDown for these browsers.
+ if (!captureRightClick) on(d.scroller, "contextmenu", function(e) {onContextMenu(cm, e);});
+
+ // Used to suppress mouse event handling when a touch happens
+ var touchFinished, prevTouch = {end: 0};
+ function finishTouch() {
+ if (d.activeTouch) {
+ touchFinished = setTimeout(function() {d.activeTouch = null;}, 1000);
+ prevTouch = d.activeTouch;
+ prevTouch.end = +new Date;
+ }
+ };
+ function isMouseLikeTouchEvent(e) {
+ if (e.touches.length != 1) return false;
+ var touch = e.touches[0];
+ return touch.radiusX <= 1 && touch.radiusY <= 1;
+ }
+ function farAway(touch, other) {
+ if (other.left == null) return true;
+ var dx = other.left - touch.left, dy = other.top - touch.top;
+ return dx * dx + dy * dy > 20 * 20;
+ }
+ on(d.scroller, "touchstart", function(e) {
+ if (!signalDOMEvent(cm, e) && !isMouseLikeTouchEvent(e)) {
+ clearTimeout(touchFinished);
+ var now = +new Date;
+ d.activeTouch = {start: now, moved: false,
+ prev: now - prevTouch.end <= 300 ? prevTouch : null};
+ if (e.touches.length == 1) {
+ d.activeTouch.left = e.touches[0].pageX;
+ d.activeTouch.top = e.touches[0].pageY;
+ }
+ }
+ });
+ on(d.scroller, "touchmove", function() {
+ if (d.activeTouch) d.activeTouch.moved = true;
+ });
+ on(d.scroller, "touchend", function(e) {
+ var touch = d.activeTouch;
+ if (touch && !eventInWidget(d, e) && touch.left != null &&
+ !touch.moved && new Date - touch.start < 300) {
+ var pos = cm.coordsChar(d.activeTouch, "page"), range;
+ if (!touch.prev || farAway(touch, touch.prev)) // Single tap
+ range = new Range(pos, pos);
+ else if (!touch.prev.prev || farAway(touch, touch.prev.prev)) // Double tap
+ range = cm.findWordAt(pos);
+ else // Triple tap
+ range = new Range(Pos(pos.line, 0), clipPos(cm.doc, Pos(pos.line + 1, 0)));
+ cm.setSelection(range.anchor, range.head);
+ cm.focus();
+ e_preventDefault(e);
+ }
+ finishTouch();
+ });
+ on(d.scroller, "touchcancel", finishTouch);
+
+ // Sync scrolling between fake scrollbars and real scrollable
+ // area, ensure viewport is updated when scrolling.
+ on(d.scroller, "scroll", function() {
+ if (d.scroller.clientHeight) {
+ setScrollTop(cm, d.scroller.scrollTop);
+ setScrollLeft(cm, d.scroller.scrollLeft, true);
+ signal(cm, "scroll", cm);
+ }
+ });
+
+ // Listen to wheel events in order to try and update the viewport on time.
+ on(d.scroller, "mousewheel", function(e){onScrollWheel(cm, e);});
+ on(d.scroller, "DOMMouseScroll", function(e){onScrollWheel(cm, e);});
+
+ // Prevent wrapper from ever scrolling
+ on(d.wrapper, "scroll", function() { d.wrapper.scrollTop = d.wrapper.scrollLeft = 0; });
+
+ d.dragFunctions = {
+ enter: function(e) {if (!signalDOMEvent(cm, e)) e_stop(e);},
+ over: function(e) {if (!signalDOMEvent(cm, e)) { onDragOver(cm, e); e_stop(e); }},
+ start: function(e){onDragStart(cm, e);},
+ drop: operation(cm, onDrop),
+ leave: function(e) {if (!signalDOMEvent(cm, e)) { clearDragCursor(cm); }}
+ };
+
+ var inp = d.input.getField();
+ on(inp, "keyup", function(e) { onKeyUp.call(cm, e); });
+ on(inp, "keydown", operation(cm, onKeyDown));
+ on(inp, "keypress", operation(cm, onKeyPress));
+ on(inp, "focus", bind(onFocus, cm));
+ on(inp, "blur", bind(onBlur, cm));
+ }
+
+ function dragDropChanged(cm, value, old) {
+ var wasOn = old && old != CodeMirror.Init;
+ if (!value != !wasOn) {
+ var funcs = cm.display.dragFunctions;
+ var toggle = value ? on : off;
+ toggle(cm.display.scroller, "dragstart", funcs.start);
+ toggle(cm.display.scroller, "dragenter", funcs.enter);
+ toggle(cm.display.scroller, "dragover", funcs.over);
+ toggle(cm.display.scroller, "dragleave", funcs.leave);
+ toggle(cm.display.scroller, "drop", funcs.drop);
+ }
+ }
+
+ // Called when the window resizes
+ function onResize(cm) {
+ var d = cm.display;
+ if (d.lastWrapHeight == d.wrapper.clientHeight && d.lastWrapWidth == d.wrapper.clientWidth)
+ return;
+ // Might be a text scaling operation, clear size caches.
+ d.cachedCharWidth = d.cachedTextHeight = d.cachedPaddingH = null;
+ d.scrollbarsClipped = false;
+ cm.setSize();
+ }
+
+ // MOUSE EVENTS
+
+ // Return true when the given mouse event happened in a widget
+ function eventInWidget(display, e) {
+ for (var n = e_target(e); n != display.wrapper; n = n.parentNode) {
+ if (!n || (n.nodeType == 1 && n.getAttribute("cm-ignore-events") == "true") ||
+ (n.parentNode == display.sizer && n != display.mover))
+ return true;
+ }
+ }
+
+ // Given a mouse event, find the corresponding position. If liberal
+ // is false, it checks whether a gutter or scrollbar was clicked,
+ // and returns null if it was. forRect is used by rectangular
+ // selections, and tries to estimate a character position even for
+ // coordinates beyond the right of the text.
+ function posFromMouse(cm, e, liberal, forRect) {
+ var display = cm.display;
+ if (!liberal && e_target(e).getAttribute("cm-not-content") == "true") return null;
+
+ var x, y, space = display.lineSpace.getBoundingClientRect();
+ // Fails unpredictably on IE[67] when mouse is dragged around quickly.
+ try { x = e.clientX - space.left; y = e.clientY - space.top; }
+ catch (e) { return null; }
+ var coords = coordsChar(cm, x, y), line;
+ if (forRect && coords.xRel == 1 && (line = getLine(cm.doc, coords.line).text).length == coords.ch) {
+ var colDiff = countColumn(line, line.length, cm.options.tabSize) - line.length;
+ coords = Pos(coords.line, Math.max(0, Math.round((x - paddingH(cm.display).left) / charWidth(cm.display)) - colDiff));
+ }
+ return coords;
+ }
+
+ // A mouse down can be a single click, double click, triple click,
+ // start of selection drag, start of text drag, new cursor
+ // (ctrl-click), rectangle drag (alt-drag), or xwin
+ // middle-click-paste. Or it might be a click on something we should
+ // not interfere with, such as a scrollbar or widget.
+ function onMouseDown(e) {
+ var cm = this, display = cm.display;
+ if (signalDOMEvent(cm, e) || display.activeTouch && display.input.supportsTouch()) return;
+ display.shift = e.shiftKey;
+
+ if (eventInWidget(display, e)) {
+ if (!webkit) {
+ // Briefly turn off draggability, to allow widgets to do
+ // normal dragging things.
+ display.scroller.draggable = false;
+ setTimeout(function(){display.scroller.draggable = true;}, 100);
+ }
+ return;
+ }
+ if (clickInGutter(cm, e)) return;
+ var start = posFromMouse(cm, e);
+ window.focus();
+
+ switch (e_button(e)) {
+ case 1:
+ // #3261: make sure, that we're not starting a second selection
+ if (cm.state.selectingText)
+ cm.state.selectingText(e);
+ else if (start)
+ leftButtonDown(cm, e, start);
+ else if (e_target(e) == display.scroller)
+ e_preventDefault(e);
+ break;
+ case 2:
+ if (webkit) cm.state.lastMiddleDown = +new Date;
+ if (start) extendSelection(cm.doc, start);
+ setTimeout(function() {display.input.focus();}, 20);
+ e_preventDefault(e);
+ break;
+ case 3:
+ if (captureRightClick) onContextMenu(cm, e);
+ else delayBlurEvent(cm);
+ break;
+ }
+ }
+
+ var lastClick, lastDoubleClick;
+ function leftButtonDown(cm, e, start) {
+ if (ie) setTimeout(bind(ensureFocus, cm), 0);
+ else cm.curOp.focus = activeElt();
+
+ var now = +new Date, type;
+ if (lastDoubleClick && lastDoubleClick.time > now - 400 && cmp(lastDoubleClick.pos, start) == 0) {
+ type = "triple";
+ } else if (lastClick && lastClick.time > now - 400 && cmp(lastClick.pos, start) == 0) {
+ type = "double";
+ lastDoubleClick = {time: now, pos: start};
+ } else {
+ type = "single";
+ lastClick = {time: now, pos: start};
+ }
+
+ var sel = cm.doc.sel, modifier = mac ? e.metaKey : e.ctrlKey, contained;
+ if (cm.options.dragDrop && dragAndDrop && !cm.isReadOnly() &&
+ type == "single" && (contained = sel.contains(start)) > -1 &&
+ (cmp((contained = sel.ranges[contained]).from(), start) < 0 || start.xRel > 0) &&
+ (cmp(contained.to(), start) > 0 || start.xRel < 0))
+ leftButtonStartDrag(cm, e, start, modifier);
+ else
+ leftButtonSelect(cm, e, start, type, modifier);
+ }
+
+ // Start a text drag. When it ends, see if any dragging actually
+ // happen, and treat as a click if it didn't.
+ function leftButtonStartDrag(cm, e, start, modifier) {
+ var display = cm.display, startTime = +new Date;
+ var dragEnd = operation(cm, function(e2) {
+ if (webkit) display.scroller.draggable = false;
+ cm.state.draggingText = false;
+ off(document, "mouseup", dragEnd);
+ off(display.scroller, "drop", dragEnd);
+ if (Math.abs(e.clientX - e2.clientX) + Math.abs(e.clientY - e2.clientY) < 10) {
+ e_preventDefault(e2);
+ if (!modifier && +new Date - 200 < startTime)
+ extendSelection(cm.doc, start);
+ // Work around unexplainable focus problem in IE9 (#2127) and Chrome (#3081)
+ if (webkit || ie && ie_version == 9)
+ setTimeout(function() {document.body.focus(); display.input.focus();}, 20);
+ else
+ display.input.focus();
+ }
+ });
+ // Let the drag handler handle this.
+ if (webkit) display.scroller.draggable = true;
+ cm.state.draggingText = dragEnd;
+ dragEnd.copy = mac ? e.altKey : e.ctrlKey
+ // IE's approach to draggable
+ if (display.scroller.dragDrop) display.scroller.dragDrop();
+ on(document, "mouseup", dragEnd);
+ on(display.scroller, "drop", dragEnd);
+ }
+
+ // Normal selection, as opposed to text dragging.
+ function leftButtonSelect(cm, e, start, type, addNew) {
+ var display = cm.display, doc = cm.doc;
+ e_preventDefault(e);
+
+ var ourRange, ourIndex, startSel = doc.sel, ranges = startSel.ranges;
+ if (addNew && !e.shiftKey) {
+ ourIndex = doc.sel.contains(start);
+ if (ourIndex > -1)
+ ourRange = ranges[ourIndex];
+ else
+ ourRange = new Range(start, start);
+ } else {
+ ourRange = doc.sel.primary();
+ ourIndex = doc.sel.primIndex;
+ }
+
+ if (chromeOS ? e.shiftKey && e.metaKey : e.altKey) {
+ type = "rect";
+ if (!addNew) ourRange = new Range(start, start);
+ start = posFromMouse(cm, e, true, true);
+ ourIndex = -1;
+ } else if (type == "double") {
+ var word = cm.findWordAt(start);
+ if (cm.display.shift || doc.extend)
+ ourRange = extendRange(doc, ourRange, word.anchor, word.head);
+ else
+ ourRange = word;
+ } else if (type == "triple") {
+ var line = new Range(Pos(start.line, 0), clipPos(doc, Pos(start.line + 1, 0)));
+ if (cm.display.shift || doc.extend)
+ ourRange = extendRange(doc, ourRange, line.anchor, line.head);
+ else
+ ourRange = line;
+ } else {
+ ourRange = extendRange(doc, ourRange, start);
+ }
+
+ if (!addNew) {
+ ourIndex = 0;
+ setSelection(doc, new Selection([ourRange], 0), sel_mouse);
+ startSel = doc.sel;
+ } else if (ourIndex == -1) {
+ ourIndex = ranges.length;
+ setSelection(doc, normalizeSelection(ranges.concat([ourRange]), ourIndex),
+ {scroll: false, origin: "*mouse"});
+ } else if (ranges.length > 1 && ranges[ourIndex].empty() && type == "single" && !e.shiftKey) {
+ setSelection(doc, normalizeSelection(ranges.slice(0, ourIndex).concat(ranges.slice(ourIndex + 1)), 0),
+ {scroll: false, origin: "*mouse"});
+ startSel = doc.sel;
+ } else {
+ replaceOneSelection(doc, ourIndex, ourRange, sel_mouse);
+ }
+
+ var lastPos = start;
+ function extendTo(pos) {
+ if (cmp(lastPos, pos) == 0) return;
+ lastPos = pos;
+
+ if (type == "rect") {
+ var ranges = [], tabSize = cm.options.tabSize;
+ var startCol = countColumn(getLine(doc, start.line).text, start.ch, tabSize);
+ var posCol = countColumn(getLine(doc, pos.line).text, pos.ch, tabSize);
+ var left = Math.min(startCol, posCol), right = Math.max(startCol, posCol);
+ for (var line = Math.min(start.line, pos.line), end = Math.min(cm.lastLine(), Math.max(start.line, pos.line));
+ line <= end; line++) {
+ var text = getLine(doc, line).text, leftPos = findColumn(text, left, tabSize);
+ if (left == right)
+ ranges.push(new Range(Pos(line, leftPos), Pos(line, leftPos)));
+ else if (text.length > leftPos)
+ ranges.push(new Range(Pos(line, leftPos), Pos(line, findColumn(text, right, tabSize))));
+ }
+ if (!ranges.length) ranges.push(new Range(start, start));
+ setSelection(doc, normalizeSelection(startSel.ranges.slice(0, ourIndex).concat(ranges), ourIndex),
+ {origin: "*mouse", scroll: false});
+ cm.scrollIntoView(pos);
+ } else {
+ var oldRange = ourRange;
+ var anchor = oldRange.anchor, head = pos;
+ if (type != "single") {
+ if (type == "double")
+ var range = cm.findWordAt(pos);
+ else
+ var range = new Range(Pos(pos.line, 0), clipPos(doc, Pos(pos.line + 1, 0)));
+ if (cmp(range.anchor, anchor) > 0) {
+ head = range.head;
+ anchor = minPos(oldRange.from(), range.anchor);
+ } else {
+ head = range.anchor;
+ anchor = maxPos(oldRange.to(), range.head);
+ }
+ }
+ var ranges = startSel.ranges.slice(0);
+ ranges[ourIndex] = new Range(clipPos(doc, anchor), head);
+ setSelection(doc, normalizeSelection(ranges, ourIndex), sel_mouse);
+ }
+ }
+
+ var editorSize = display.wrapper.getBoundingClientRect();
+ // Used to ensure timeout re-tries don't fire when another extend
+ // happened in the meantime (clearTimeout isn't reliable -- at
+ // least on Chrome, the timeouts still happen even when cleared,
+ // if the clear happens after their scheduled firing time).
+ var counter = 0;
+
+ function extend(e) {
+ var curCount = ++counter;
+ var cur = posFromMouse(cm, e, true, type == "rect");
+ if (!cur) return;
+ if (cmp(cur, lastPos) != 0) {
+ cm.curOp.focus = activeElt();
+ extendTo(cur);
+ var visible = visibleLines(display, doc);
+ if (cur.line >= visible.to || cur.line < visible.from)
+ setTimeout(operation(cm, function(){if (counter == curCount) extend(e);}), 150);
+ } else {
+ var outside = e.clientY < editorSize.top ? -20 : e.clientY > editorSize.bottom ? 20 : 0;
+ if (outside) setTimeout(operation(cm, function() {
+ if (counter != curCount) return;
+ display.scroller.scrollTop += outside;
+ extend(e);
+ }), 50);
+ }
+ }
+
+ function done(e) {
+ cm.state.selectingText = false;
+ counter = Infinity;
+ e_preventDefault(e);
+ display.input.focus();
+ off(document, "mousemove", move);
+ off(document, "mouseup", up);
+ doc.history.lastSelOrigin = null;
+ }
+
+ var move = operation(cm, function(e) {
+ if (!e_button(e)) done(e);
+ else extend(e);
+ });
+ var up = operation(cm, done);
+ cm.state.selectingText = up;
+ on(document, "mousemove", move);
+ on(document, "mouseup", up);
+ }
+
+ // Determines whether an event happened in the gutter, and fires the
+ // handlers for the corresponding event.
+ function gutterEvent(cm, e, type, prevent) {
+ try { var mX = e.clientX, mY = e.clientY; }
+ catch(e) { return false; }
+ if (mX >= Math.floor(cm.display.gutters.getBoundingClientRect().right)) return false;
+ if (prevent) e_preventDefault(e);
+
+ var display = cm.display;
+ var lineBox = display.lineDiv.getBoundingClientRect();
+
+ if (mY > lineBox.bottom || !hasHandler(cm, type)) return e_defaultPrevented(e);
+ mY -= lineBox.top - display.viewOffset;
+
+ for (var i = 0; i < cm.options.gutters.length; ++i) {
+ var g = display.gutters.childNodes[i];
+ if (g && g.getBoundingClientRect().right >= mX) {
+ var line = lineAtHeight(cm.doc, mY);
+ var gutter = cm.options.gutters[i];
+ signal(cm, type, cm, line, gutter, e);
+ return e_defaultPrevented(e);
+ }
+ }
+ }
+
+ function clickInGutter(cm, e) {
+ return gutterEvent(cm, e, "gutterClick", true);
+ }
+
+ // Kludge to work around strange IE behavior where it'll sometimes
+ // re-fire a series of drag-related events right after the drop (#1551)
+ var lastDrop = 0;
+
+ function onDrop(e) {
+ var cm = this;
+ clearDragCursor(cm);
+ if (signalDOMEvent(cm, e) || eventInWidget(cm.display, e))
+ return;
+ e_preventDefault(e);
+ if (ie) lastDrop = +new Date;
+ var pos = posFromMouse(cm, e, true), files = e.dataTransfer.files;
+ if (!pos || cm.isReadOnly()) return;
+ // Might be a file drop, in which case we simply extract the text
+ // and insert it.
+ if (files && files.length && window.FileReader && window.File) {
+ var n = files.length, text = Array(n), read = 0;
+ var loadFile = function(file, i) {
+ if (cm.options.allowDropFileTypes &&
+ indexOf(cm.options.allowDropFileTypes, file.type) == -1)
+ return;
+
+ var reader = new FileReader;
+ reader.onload = operation(cm, function() {
+ var content = reader.result;
+ if (/[\x00-\x08\x0e-\x1f]{2}/.test(content)) content = "";
+ text[i] = content;
+ if (++read == n) {
+ pos = clipPos(cm.doc, pos);
+ var change = {from: pos, to: pos,
+ text: cm.doc.splitLines(text.join(cm.doc.lineSeparator())),
+ origin: "paste"};
+ makeChange(cm.doc, change);
+ setSelectionReplaceHistory(cm.doc, simpleSelection(pos, changeEnd(change)));
+ }
+ });
+ reader.readAsText(file);
+ };
+ for (var i = 0; i < n; ++i) loadFile(files[i], i);
+ } else { // Normal drop
+ // Don't do a replace if the drop happened inside of the selected text.
+ if (cm.state.draggingText && cm.doc.sel.contains(pos) > -1) {
+ cm.state.draggingText(e);
+ // Ensure the editor is re-focused
+ setTimeout(function() {cm.display.input.focus();}, 20);
+ return;
+ }
+ try {
+ var text = e.dataTransfer.getData("Text");
+ if (text) {
+ if (cm.state.draggingText && !cm.state.draggingText.copy)
+ var selected = cm.listSelections();
+ setSelectionNoUndo(cm.doc, simpleSelection(pos, pos));
+ if (selected) for (var i = 0; i < selected.length; ++i)
+ replaceRange(cm.doc, "", selected[i].anchor, selected[i].head, "drag");
+ cm.replaceSelection(text, "around", "paste");
+ cm.display.input.focus();
+ }
+ }
+ catch(e){}
+ }
+ }
+
+ function onDragStart(cm, e) {
+ if (ie && (!cm.state.draggingText || +new Date - lastDrop < 100)) { e_stop(e); return; }
+ if (signalDOMEvent(cm, e) || eventInWidget(cm.display, e)) return;
+
+ e.dataTransfer.setData("Text", cm.getSelection());
+ e.dataTransfer.effectAllowed = "copyMove"
+
+ // Use dummy image instead of default browsers image.
+ // Recent Safari (~6.0.2) have a tendency to segfault when this happens, so we don't do it there.
+ if (e.dataTransfer.setDragImage && !safari) {
+ var img = elt("img", null, null, "position: fixed; left: 0; top: 0;");
+ img.src = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==";
+ if (presto) {
+ img.width = img.height = 1;
+ cm.display.wrapper.appendChild(img);
+ // Force a relayout, or Opera won't use our image for some obscure reason
+ img._top = img.offsetTop;
+ }
+ e.dataTransfer.setDragImage(img, 0, 0);
+ if (presto) img.parentNode.removeChild(img);
+ }
+ }
+
+ function onDragOver(cm, e) {
+ var pos = posFromMouse(cm, e);
+ if (!pos) return;
+ var frag = document.createDocumentFragment();
+ drawSelectionCursor(cm, pos, frag);
+ if (!cm.display.dragCursor) {
+ cm.display.dragCursor = elt("div", null, "CodeMirror-cursors CodeMirror-dragcursors");
+ cm.display.lineSpace.insertBefore(cm.display.dragCursor, cm.display.cursorDiv);
+ }
+ removeChildrenAndAdd(cm.display.dragCursor, frag);
+ }
+
+ function clearDragCursor(cm) {
+ if (cm.display.dragCursor) {
+ cm.display.lineSpace.removeChild(cm.display.dragCursor);
+ cm.display.dragCursor = null;
+ }
+ }
+
+ // SCROLL EVENTS
+
+ // Sync the scrollable area and scrollbars, ensure the viewport
+ // covers the visible area.
+ function setScrollTop(cm, val) {
+ if (Math.abs(cm.doc.scrollTop - val) < 2) return;
+ cm.doc.scrollTop = val;
+ if (!gecko) updateDisplaySimple(cm, {top: val});
+ if (cm.display.scroller.scrollTop != val) cm.display.scroller.scrollTop = val;
+ cm.display.scrollbars.setScrollTop(val);
+ if (gecko) updateDisplaySimple(cm);
+ startWorker(cm, 100);
+ }
+ // Sync scroller and scrollbar, ensure the gutter elements are
+ // aligned.
+ function setScrollLeft(cm, val, isScroller) {
+ if (isScroller ? val == cm.doc.scrollLeft : Math.abs(cm.doc.scrollLeft - val) < 2) return;
+ val = Math.min(val, cm.display.scroller.scrollWidth - cm.display.scroller.clientWidth);
+ cm.doc.scrollLeft = val;
+ alignHorizontally(cm);
+ if (cm.display.scroller.scrollLeft != val) cm.display.scroller.scrollLeft = val;
+ cm.display.scrollbars.setScrollLeft(val);
+ }
+
+ // Since the delta values reported on mouse wheel events are
+ // unstandardized between browsers and even browser versions, and
+ // generally horribly unpredictable, this code starts by measuring
+ // the scroll effect that the first few mouse wheel events have,
+ // and, from that, detects the way it can convert deltas to pixel
+ // offsets afterwards.
+ //
+ // The reason we want to know the amount a wheel event will scroll
+ // is that it gives us a chance to update the display before the
+ // actual scrolling happens, reducing flickering.
+
+ var wheelSamples = 0, wheelPixelsPerUnit = null;
+ // Fill in a browser-detected starting value on browsers where we
+ // know one. These don't have to be accurate -- the result of them
+ // being wrong would just be a slight flicker on the first wheel
+ // scroll (if it is large enough).
+ if (ie) wheelPixelsPerUnit = -.53;
+ else if (gecko) wheelPixelsPerUnit = 15;
+ else if (chrome) wheelPixelsPerUnit = -.7;
+ else if (safari) wheelPixelsPerUnit = -1/3;
+
+ var wheelEventDelta = function(e) {
+ var dx = e.wheelDeltaX, dy = e.wheelDeltaY;
+ if (dx == null && e.detail && e.axis == e.HORIZONTAL_AXIS) dx = e.detail;
+ if (dy == null && e.detail && e.axis == e.VERTICAL_AXIS) dy = e.detail;
+ else if (dy == null) dy = e.wheelDelta;
+ return {x: dx, y: dy};
+ };
+ CodeMirror.wheelEventPixels = function(e) {
+ var delta = wheelEventDelta(e);
+ delta.x *= wheelPixelsPerUnit;
+ delta.y *= wheelPixelsPerUnit;
+ return delta;
+ };
+
+ function onScrollWheel(cm, e) {
+ var delta = wheelEventDelta(e), dx = delta.x, dy = delta.y;
+
+ var display = cm.display, scroll = display.scroller;
+ // Quit if there's nothing to scroll here
+ var canScrollX = scroll.scrollWidth > scroll.clientWidth;
+ var canScrollY = scroll.scrollHeight > scroll.clientHeight;
+ if (!(dx && canScrollX || dy && canScrollY)) return;
+
+ // Webkit browsers on OS X abort momentum scrolls when the target
+ // of the scroll event is removed from the scrollable element.
+ // This hack (see related code in patchDisplay) makes sure the
+ // element is kept around.
+ if (dy && mac && webkit) {
+ outer: for (var cur = e.target, view = display.view; cur != scroll; cur = cur.parentNode) {
+ for (var i = 0; i < view.length; i++) {
+ if (view[i].node == cur) {
+ cm.display.currentWheelTarget = cur;
+ break outer;
+ }
+ }
+ }
+ }
+
+ // On some browsers, horizontal scrolling will cause redraws to
+ // happen before the gutter has been realigned, causing it to
+ // wriggle around in a most unseemly way. When we have an
+ // estimated pixels/delta value, we just handle horizontal
+ // scrolling entirely here. It'll be slightly off from native, but
+ // better than glitching out.
+ if (dx && !gecko && !presto && wheelPixelsPerUnit != null) {
+ if (dy && canScrollY)
+ setScrollTop(cm, Math.max(0, Math.min(scroll.scrollTop + dy * wheelPixelsPerUnit, scroll.scrollHeight - scroll.clientHeight)));
+ setScrollLeft(cm, Math.max(0, Math.min(scroll.scrollLeft + dx * wheelPixelsPerUnit, scroll.scrollWidth - scroll.clientWidth)));
+ // Only prevent default scrolling if vertical scrolling is
+ // actually possible. Otherwise, it causes vertical scroll
+ // jitter on OSX trackpads when deltaX is small and deltaY
+ // is large (issue #3579)
+ if (!dy || (dy && canScrollY))
+ e_preventDefault(e);
+ display.wheelStartX = null; // Abort measurement, if in progress
+ return;
+ }
+
+ // 'Project' the visible viewport to cover the area that is being
+ // scrolled into view (if we know enough to estimate it).
+ if (dy && wheelPixelsPerUnit != null) {
+ var pixels = dy * wheelPixelsPerUnit;
+ var top = cm.doc.scrollTop, bot = top + display.wrapper.clientHeight;
+ if (pixels < 0) top = Math.max(0, top + pixels - 50);
+ else bot = Math.min(cm.doc.height, bot + pixels + 50);
+ updateDisplaySimple(cm, {top: top, bottom: bot});
+ }
+
+ if (wheelSamples < 20) {
+ if (display.wheelStartX == null) {
+ display.wheelStartX = scroll.scrollLeft; display.wheelStartY = scroll.scrollTop;
+ display.wheelDX = dx; display.wheelDY = dy;
+ setTimeout(function() {
+ if (display.wheelStartX == null) return;
+ var movedX = scroll.scrollLeft - display.wheelStartX;
+ var movedY = scroll.scrollTop - display.wheelStartY;
+ var sample = (movedY && display.wheelDY && movedY / display.wheelDY) ||
+ (movedX && display.wheelDX && movedX / display.wheelDX);
+ display.wheelStartX = display.wheelStartY = null;
+ if (!sample) return;
+ wheelPixelsPerUnit = (wheelPixelsPerUnit * wheelSamples + sample) / (wheelSamples + 1);
+ ++wheelSamples;
+ }, 200);
+ } else {
+ display.wheelDX += dx; display.wheelDY += dy;
+ }
+ }
+ }
+
+ // KEY EVENTS
+
+ // Run a handler that was bound to a key.
+ function doHandleBinding(cm, bound, dropShift) {
+ if (typeof bound == "string") {
+ bound = commands[bound];
+ if (!bound) return false;
+ }
+ // Ensure previous input has been read, so that the handler sees a
+ // consistent view of the document
+ cm.display.input.ensurePolled();
+ var prevShift = cm.display.shift, done = false;
+ try {
+ if (cm.isReadOnly()) cm.state.suppressEdits = true;
+ if (dropShift) cm.display.shift = false;
+ done = bound(cm) != Pass;
+ } finally {
+ cm.display.shift = prevShift;
+ cm.state.suppressEdits = false;
+ }
+ return done;
+ }
+
+ function lookupKeyForEditor(cm, name, handle) {
+ for (var i = 0; i < cm.state.keyMaps.length; i++) {
+ var result = lookupKey(name, cm.state.keyMaps[i], handle, cm);
+ if (result) return result;
+ }
+ return (cm.options.extraKeys && lookupKey(name, cm.options.extraKeys, handle, cm))
+ || lookupKey(name, cm.options.keyMap, handle, cm);
+ }
+
+ var stopSeq = new Delayed;
+ function dispatchKey(cm, name, e, handle) {
+ var seq = cm.state.keySeq;
+ if (seq) {
+ if (isModifierKey(name)) return "handled";
+ stopSeq.set(50, function() {
+ if (cm.state.keySeq == seq) {
+ cm.state.keySeq = null;
+ cm.display.input.reset();
+ }
+ });
+ name = seq + " " + name;
+ }
+ var result = lookupKeyForEditor(cm, name, handle);
+
+ if (result == "multi")
+ cm.state.keySeq = name;
+ if (result == "handled")
+ signalLater(cm, "keyHandled", cm, name, e);
+
+ if (result == "handled" || result == "multi") {
+ e_preventDefault(e);
+ restartBlink(cm);
+ }
+
+ if (seq && !result && /\'$/.test(name)) {
+ e_preventDefault(e);
+ return true;
+ }
+ return !!result;
+ }
+
+ // Handle a key from the keydown event.
+ function handleKeyBinding(cm, e) {
+ var name = keyName(e, true);
+ if (!name) return false;
+
+ if (e.shiftKey && !cm.state.keySeq) {
+ // First try to resolve full name (including 'Shift-'). Failing
+ // that, see if there is a cursor-motion command (starting with
+ // 'go') bound to the keyname without 'Shift-'.
+ return dispatchKey(cm, "Shift-" + name, e, function(b) {return doHandleBinding(cm, b, true);})
+ || dispatchKey(cm, name, e, function(b) {
+ if (typeof b == "string" ? /^go[A-Z]/.test(b) : b.motion)
+ return doHandleBinding(cm, b);
+ });
+ } else {
+ return dispatchKey(cm, name, e, function(b) { return doHandleBinding(cm, b); });
+ }
+ }
+
+ // Handle a key from the keypress event
+ function handleCharBinding(cm, e, ch) {
+ return dispatchKey(cm, "'" + ch + "'", e,
+ function(b) { return doHandleBinding(cm, b, true); });
+ }
+
+ var lastStoppedKey = null;
+ function onKeyDown(e) {
+ var cm = this;
+ cm.curOp.focus = activeElt();
+ if (signalDOMEvent(cm, e)) return;
+ // IE does strange things with escape.
+ if (ie && ie_version < 11 && e.keyCode == 27) e.returnValue = false;
+ var code = e.keyCode;
+ cm.display.shift = code == 16 || e.shiftKey;
+ var handled = handleKeyBinding(cm, e);
+ if (presto) {
+ lastStoppedKey = handled ? code : null;
+ // Opera has no cut event... we try to at least catch the key combo
+ if (!handled && code == 88 && !hasCopyEvent && (mac ? e.metaKey : e.ctrlKey))
+ cm.replaceSelection("", null, "cut");
+ }
+
+ // Turn mouse into crosshair when Alt is held on Mac.
+ if (code == 18 && !/\bCodeMirror-crosshair\b/.test(cm.display.lineDiv.className))
+ showCrossHair(cm);
+ }
+
+ function showCrossHair(cm) {
+ var lineDiv = cm.display.lineDiv;
+ addClass(lineDiv, "CodeMirror-crosshair");
+
+ function up(e) {
+ if (e.keyCode == 18 || !e.altKey) {
+ rmClass(lineDiv, "CodeMirror-crosshair");
+ off(document, "keyup", up);
+ off(document, "mouseover", up);
+ }
+ }
+ on(document, "keyup", up);
+ on(document, "mouseover", up);
+ }
+
+ function onKeyUp(e) {
+ if (e.keyCode == 16) this.doc.sel.shift = false;
+ signalDOMEvent(this, e);
+ }
+
+ function onKeyPress(e) {
+ var cm = this;
+ if (eventInWidget(cm.display, e) || signalDOMEvent(cm, e) || e.ctrlKey && !e.altKey || mac && e.metaKey) return;
+ var keyCode = e.keyCode, charCode = e.charCode;
+ if (presto && keyCode == lastStoppedKey) {lastStoppedKey = null; e_preventDefault(e); return;}
+ if ((presto && (!e.which || e.which < 10)) && handleKeyBinding(cm, e)) return;
+ var ch = String.fromCharCode(charCode == null ? keyCode : charCode);
+ if (handleCharBinding(cm, e, ch)) return;
+ cm.display.input.onKeyPress(e);
+ }
+
+ // FOCUS/BLUR EVENTS
+
+ function delayBlurEvent(cm) {
+ cm.state.delayingBlurEvent = true;
+ setTimeout(function() {
+ if (cm.state.delayingBlurEvent) {
+ cm.state.delayingBlurEvent = false;
+ onBlur(cm);
+ }
+ }, 100);
+ }
+
+ function onFocus(cm) {
+ if (cm.state.delayingBlurEvent) cm.state.delayingBlurEvent = false;
+
+ if (cm.options.readOnly == "nocursor") return;
+ if (!cm.state.focused) {
+ signal(cm, "focus", cm);
+ cm.state.focused = true;
+ addClass(cm.display.wrapper, "CodeMirror-focused");
+ // This test prevents this from firing when a context
+ // menu is closed (since the input reset would kill the
+ // select-all detection hack)
+ if (!cm.curOp && cm.display.selForContextMenu != cm.doc.sel) {
+ cm.display.input.reset();
+ if (webkit) setTimeout(function() { cm.display.input.reset(true); }, 20); // Issue #1730
+ }
+ cm.display.input.receivedFocus();
+ }
+ restartBlink(cm);
+ }
+ function onBlur(cm) {
+ if (cm.state.delayingBlurEvent) return;
+
+ if (cm.state.focused) {
+ signal(cm, "blur", cm);
+ cm.state.focused = false;
+ rmClass(cm.display.wrapper, "CodeMirror-focused");
+ }
+ clearInterval(cm.display.blinker);
+ setTimeout(function() {if (!cm.state.focused) cm.display.shift = false;}, 150);
+ }
+
+ // CONTEXT MENU HANDLING
+
+ // To make the context menu work, we need to briefly unhide the
+ // textarea (making it as unobtrusive as possible) to let the
+ // right-click take effect on it.
+ function onContextMenu(cm, e) {
+ if (eventInWidget(cm.display, e) || contextMenuInGutter(cm, e)) return;
+ if (signalDOMEvent(cm, e, "contextmenu")) return;
+ cm.display.input.onContextMenu(e);
+ }
+
+ function contextMenuInGutter(cm, e) {
+ if (!hasHandler(cm, "gutterContextMenu")) return false;
+ return gutterEvent(cm, e, "gutterContextMenu", false);
+ }
+
+ // UPDATING
+
+ // Compute the position of the end of a change (its 'to' property
+ // refers to the pre-change end).
+ var changeEnd = CodeMirror.changeEnd = function(change) {
+ if (!change.text) return change.to;
+ return Pos(change.from.line + change.text.length - 1,
+ lst(change.text).length + (change.text.length == 1 ? change.from.ch : 0));
+ };
+
+ // Adjust a position to refer to the post-change position of the
+ // same text, or the end of the change if the change covers it.
+ function adjustForChange(pos, change) {
+ if (cmp(pos, change.from) < 0) return pos;
+ if (cmp(pos, change.to) <= 0) return changeEnd(change);
+
+ var line = pos.line + change.text.length - (change.to.line - change.from.line) - 1, ch = pos.ch;
+ if (pos.line == change.to.line) ch += changeEnd(change).ch - change.to.ch;
+ return Pos(line, ch);
+ }
+
+ function computeSelAfterChange(doc, change) {
+ var out = [];
+ for (var i = 0; i < doc.sel.ranges.length; i++) {
+ var range = doc.sel.ranges[i];
+ out.push(new Range(adjustForChange(range.anchor, change),
+ adjustForChange(range.head, change)));
+ }
+ return normalizeSelection(out, doc.sel.primIndex);
+ }
+
+ function offsetPos(pos, old, nw) {
+ if (pos.line == old.line)
+ return Pos(nw.line, pos.ch - old.ch + nw.ch);
+ else
+ return Pos(nw.line + (pos.line - old.line), pos.ch);
+ }
+
+ // Used by replaceSelections to allow moving the selection to the
+ // start or around the replaced test. Hint may be "start" or "around".
+ function computeReplacedSel(doc, changes, hint) {
+ var out = [];
+ var oldPrev = Pos(doc.first, 0), newPrev = oldPrev;
+ for (var i = 0; i < changes.length; i++) {
+ var change = changes[i];
+ var from = offsetPos(change.from, oldPrev, newPrev);
+ var to = offsetPos(changeEnd(change), oldPrev, newPrev);
+ oldPrev = change.to;
+ newPrev = to;
+ if (hint == "around") {
+ var range = doc.sel.ranges[i], inv = cmp(range.head, range.anchor) < 0;
+ out[i] = new Range(inv ? to : from, inv ? from : to);
+ } else {
+ out[i] = new Range(from, from);
+ }
+ }
+ return new Selection(out, doc.sel.primIndex);
+ }
+
+ // Allow "beforeChange" event handlers to influence a change
+ function filterChange(doc, change, update) {
+ var obj = {
+ canceled: false,
+ from: change.from,
+ to: change.to,
+ text: change.text,
+ origin: change.origin,
+ cancel: function() { this.canceled = true; }
+ };
+ if (update) obj.update = function(from, to, text, origin) {
+ if (from) this.from = clipPos(doc, from);
+ if (to) this.to = clipPos(doc, to);
+ if (text) this.text = text;
+ if (origin !== undefined) this.origin = origin;
+ };
+ signal(doc, "beforeChange", doc, obj);
+ if (doc.cm) signal(doc.cm, "beforeChange", doc.cm, obj);
+
+ if (obj.canceled) return null;
+ return {from: obj.from, to: obj.to, text: obj.text, origin: obj.origin};
+ }
+
+ // Apply a change to a document, and add it to the document's
+ // history, and propagating it to all linked documents.
+ function makeChange(doc, change, ignoreReadOnly) {
+ if (doc.cm) {
+ if (!doc.cm.curOp) return operation(doc.cm, makeChange)(doc, change, ignoreReadOnly);
+ if (doc.cm.state.suppressEdits) return;
+ }
+
+ if (hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange")) {
+ change = filterChange(doc, change, true);
+ if (!change) return;
+ }
+
+ // Possibly split or suppress the update based on the presence
+ // of read-only spans in its range.
+ var split = sawReadOnlySpans && !ignoreReadOnly && removeReadOnlyRanges(doc, change.from, change.to);
+ if (split) {
+ for (var i = split.length - 1; i >= 0; --i)
+ makeChangeInner(doc, {from: split[i].from, to: split[i].to, text: i ? [""] : change.text});
+ } else {
+ makeChangeInner(doc, change);
+ }
+ }
+
+ function makeChangeInner(doc, change) {
+ if (change.text.length == 1 && change.text[0] == "" && cmp(change.from, change.to) == 0) return;
+ var selAfter = computeSelAfterChange(doc, change);
+ addChangeToHistory(doc, change, selAfter, doc.cm ? doc.cm.curOp.id : NaN);
+
+ makeChangeSingleDoc(doc, change, selAfter, stretchSpansOverChange(doc, change));
+ var rebased = [];
+
+ linkedDocs(doc, function(doc, sharedHist) {
+ if (!sharedHist && indexOf(rebased, doc.history) == -1) {
+ rebaseHist(doc.history, change);
+ rebased.push(doc.history);
+ }
+ makeChangeSingleDoc(doc, change, null, stretchSpansOverChange(doc, change));
+ });
+ }
+
+ // Revert a change stored in a document's history.
+ function makeChangeFromHistory(doc, type, allowSelectionOnly) {
+ if (doc.cm && doc.cm.state.suppressEdits) return;
+
+ var hist = doc.history, event, selAfter = doc.sel;
+ var source = type == "undo" ? hist.done : hist.undone, dest = type == "undo" ? hist.undone : hist.done;
+
+ // Verify that there is a useable event (so that ctrl-z won't
+ // needlessly clear selection events)
+ for (var i = 0; i < source.length; i++) {
+ event = source[i];
+ if (allowSelectionOnly ? event.ranges && !event.equals(doc.sel) : !event.ranges)
+ break;
+ }
+ if (i == source.length) return;
+ hist.lastOrigin = hist.lastSelOrigin = null;
+
+ for (;;) {
+ event = source.pop();
+ if (event.ranges) {
+ pushSelectionToHistory(event, dest);
+ if (allowSelectionOnly && !event.equals(doc.sel)) {
+ setSelection(doc, event, {clearRedo: false});
+ return;
+ }
+ selAfter = event;
+ }
+ else break;
+ }
+
+ // Build up a reverse change object to add to the opposite history
+ // stack (redo when undoing, and vice versa).
+ var antiChanges = [];
+ pushSelectionToHistory(selAfter, dest);
+ dest.push({changes: antiChanges, generation: hist.generation});
+ hist.generation = event.generation || ++hist.maxGeneration;
+
+ var filter = hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange");
+
+ for (var i = event.changes.length - 1; i >= 0; --i) {
+ var change = event.changes[i];
+ change.origin = type;
+ if (filter && !filterChange(doc, change, false)) {
+ source.length = 0;
+ return;
+ }
+
+ antiChanges.push(historyChangeFromChange(doc, change));
+
+ var after = i ? computeSelAfterChange(doc, change) : lst(source);
+ makeChangeSingleDoc(doc, change, after, mergeOldSpans(doc, change));
+ if (!i && doc.cm) doc.cm.scrollIntoView({from: change.from, to: changeEnd(change)});
+ var rebased = [];
+
+ // Propagate to the linked documents
+ linkedDocs(doc, function(doc, sharedHist) {
+ if (!sharedHist && indexOf(rebased, doc.history) == -1) {
+ rebaseHist(doc.history, change);
+ rebased.push(doc.history);
+ }
+ makeChangeSingleDoc(doc, change, null, mergeOldSpans(doc, change));
+ });
+ }
+ }
+
+ // Sub-views need their line numbers shifted when text is added
+ // above or below them in the parent document.
+ function shiftDoc(doc, distance) {
+ if (distance == 0) return;
+ doc.first += distance;
+ doc.sel = new Selection(map(doc.sel.ranges, function(range) {
+ return new Range(Pos(range.anchor.line + distance, range.anchor.ch),
+ Pos(range.head.line + distance, range.head.ch));
+ }), doc.sel.primIndex);
+ if (doc.cm) {
+ regChange(doc.cm, doc.first, doc.first - distance, distance);
+ for (var d = doc.cm.display, l = d.viewFrom; l < d.viewTo; l++)
+ regLineChange(doc.cm, l, "gutter");
+ }
+ }
+
+ // More lower-level change function, handling only a single document
+ // (not linked ones).
+ function makeChangeSingleDoc(doc, change, selAfter, spans) {
+ if (doc.cm && !doc.cm.curOp)
+ return operation(doc.cm, makeChangeSingleDoc)(doc, change, selAfter, spans);
+
+ if (change.to.line < doc.first) {
+ shiftDoc(doc, change.text.length - 1 - (change.to.line - change.from.line));
+ return;
+ }
+ if (change.from.line > doc.lastLine()) return;
+
+ // Clip the change to the size of this doc
+ if (change.from.line < doc.first) {
+ var shift = change.text.length - 1 - (doc.first - change.from.line);
+ shiftDoc(doc, shift);
+ change = {from: Pos(doc.first, 0), to: Pos(change.to.line + shift, change.to.ch),
+ text: [lst(change.text)], origin: change.origin};
+ }
+ var last = doc.lastLine();
+ if (change.to.line > last) {
+ change = {from: change.from, to: Pos(last, getLine(doc, last).text.length),
+ text: [change.text[0]], origin: change.origin};
+ }
+
+ change.removed = getBetween(doc, change.from, change.to);
+
+ if (!selAfter) selAfter = computeSelAfterChange(doc, change);
+ if (doc.cm) makeChangeSingleDocInEditor(doc.cm, change, spans);
+ else updateDoc(doc, change, spans);
+ setSelectionNoUndo(doc, selAfter, sel_dontScroll);
+ }
+
+ // Handle the interaction of a change to a document with the editor
+ // that this document is part of.
+ function makeChangeSingleDocInEditor(cm, change, spans) {
+ var doc = cm.doc, display = cm.display, from = change.from, to = change.to;
+
+ var recomputeMaxLength = false, checkWidthStart = from.line;
+ if (!cm.options.lineWrapping) {
+ checkWidthStart = lineNo(visualLine(getLine(doc, from.line)));
+ doc.iter(checkWidthStart, to.line + 1, function(line) {
+ if (line == display.maxLine) {
+ recomputeMaxLength = true;
+ return true;
+ }
+ });
+ }
+
+ if (doc.sel.contains(change.from, change.to) > -1)
+ signalCursorActivity(cm);
+
+ updateDoc(doc, change, spans, estimateHeight(cm));
+
+ if (!cm.options.lineWrapping) {
+ doc.iter(checkWidthStart, from.line + change.text.length, function(line) {
+ var len = lineLength(line);
+ if (len > display.maxLineLength) {
+ display.maxLine = line;
+ display.maxLineLength = len;
+ display.maxLineChanged = true;
+ recomputeMaxLength = false;
+ }
+ });
+ if (recomputeMaxLength) cm.curOp.updateMaxLine = true;
+ }
+
+ // Adjust frontier, schedule worker
+ doc.frontier = Math.min(doc.frontier, from.line);
+ startWorker(cm, 400);
+
+ var lendiff = change.text.length - (to.line - from.line) - 1;
+ // Remember that these lines changed, for updating the display
+ if (change.full)
+ regChange(cm);
+ else if (from.line == to.line && change.text.length == 1 && !isWholeLineUpdate(cm.doc, change))
+ regLineChange(cm, from.line, "text");
+ else
+ regChange(cm, from.line, to.line + 1, lendiff);
+
+ var changesHandler = hasHandler(cm, "changes"), changeHandler = hasHandler(cm, "change");
+ if (changeHandler || changesHandler) {
+ var obj = {
+ from: from, to: to,
+ text: change.text,
+ removed: change.removed,
+ origin: change.origin
+ };
+ if (changeHandler) signalLater(cm, "change", cm, obj);
+ if (changesHandler) (cm.curOp.changeObjs || (cm.curOp.changeObjs = [])).push(obj);
+ }
+ cm.display.selForContextMenu = null;
+ }
+
+ function replaceRange(doc, code, from, to, origin) {
+ if (!to) to = from;
+ if (cmp(to, from) < 0) { var tmp = to; to = from; from = tmp; }
+ if (typeof code == "string") code = doc.splitLines(code);
+ makeChange(doc, {from: from, to: to, text: code, origin: origin});
+ }
+
+ // SCROLLING THINGS INTO VIEW
+
+ // If an editor sits on the top or bottom of the window, partially
+ // scrolled out of view, this ensures that the cursor is visible.
+ function maybeScrollWindow(cm, coords) {
+ if (signalDOMEvent(cm, "scrollCursorIntoView")) return;
+
+ var display = cm.display, box = display.sizer.getBoundingClientRect(), doScroll = null;
+ if (coords.top + box.top < 0) doScroll = true;
+ else if (coords.bottom + box.top > (window.innerHeight || document.documentElement.clientHeight)) doScroll = false;
+ if (doScroll != null && !phantom) {
+ var scrollNode = elt("div", "\u200b", null, "position: absolute; top: " +
+ (coords.top - display.viewOffset - paddingTop(cm.display)) + "px; height: " +
+ (coords.bottom - coords.top + scrollGap(cm) + display.barHeight) + "px; left: " +
+ coords.left + "px; width: 2px;");
+ cm.display.lineSpace.appendChild(scrollNode);
+ scrollNode.scrollIntoView(doScroll);
+ cm.display.lineSpace.removeChild(scrollNode);
+ }
+ }
+
+ // Scroll a given position into view (immediately), verifying that
+ // it actually became visible (as line heights are accurately
+ // measured, the position of something may 'drift' during drawing).
+ function scrollPosIntoView(cm, pos, end, margin) {
+ if (margin == null) margin = 0;
+ for (var limit = 0; limit < 5; limit++) {
+ var changed = false, coords = cursorCoords(cm, pos);
+ var endCoords = !end || end == pos ? coords : cursorCoords(cm, end);
+ var scrollPos = calculateScrollPos(cm, Math.min(coords.left, endCoords.left),
+ Math.min(coords.top, endCoords.top) - margin,
+ Math.max(coords.left, endCoords.left),
+ Math.max(coords.bottom, endCoords.bottom) + margin);
+ var startTop = cm.doc.scrollTop, startLeft = cm.doc.scrollLeft;
+ if (scrollPos.scrollTop != null) {
+ setScrollTop(cm, scrollPos.scrollTop);
+ if (Math.abs(cm.doc.scrollTop - startTop) > 1) changed = true;
+ }
+ if (scrollPos.scrollLeft != null) {
+ setScrollLeft(cm, scrollPos.scrollLeft);
+ if (Math.abs(cm.doc.scrollLeft - startLeft) > 1) changed = true;
+ }
+ if (!changed) break;
+ }
+ return coords;
+ }
+
+ // Scroll a given set of coordinates into view (immediately).
+ function scrollIntoView(cm, x1, y1, x2, y2) {
+ var scrollPos = calculateScrollPos(cm, x1, y1, x2, y2);
+ if (scrollPos.scrollTop != null) setScrollTop(cm, scrollPos.scrollTop);
+ if (scrollPos.scrollLeft != null) setScrollLeft(cm, scrollPos.scrollLeft);
+ }
+
+ // Calculate a new scroll position needed to scroll the given
+ // rectangle into view. Returns an object with scrollTop and
+ // scrollLeft properties. When these are undefined, the
+ // vertical/horizontal position does not need to be adjusted.
+ function calculateScrollPos(cm, x1, y1, x2, y2) {
+ var display = cm.display, snapMargin = textHeight(cm.display);
+ if (y1 < 0) y1 = 0;
+ var screentop = cm.curOp && cm.curOp.scrollTop != null ? cm.curOp.scrollTop : display.scroller.scrollTop;
+ var screen = displayHeight(cm), result = {};
+ if (y2 - y1 > screen) y2 = y1 + screen;
+ var docBottom = cm.doc.height + paddingVert(display);
+ var atTop = y1 < snapMargin, atBottom = y2 > docBottom - snapMargin;
+ if (y1 < screentop) {
+ result.scrollTop = atTop ? 0 : y1;
+ } else if (y2 > screentop + screen) {
+ var newTop = Math.min(y1, (atBottom ? docBottom : y2) - screen);
+ if (newTop != screentop) result.scrollTop = newTop;
+ }
+
+ var screenleft = cm.curOp && cm.curOp.scrollLeft != null ? cm.curOp.scrollLeft : display.scroller.scrollLeft;
+ var screenw = displayWidth(cm) - (cm.options.fixedGutter ? display.gutters.offsetWidth : 0);
+ var tooWide = x2 - x1 > screenw;
+ if (tooWide) x2 = x1 + screenw;
+ if (x1 < 10)
+ result.scrollLeft = 0;
+ else if (x1 < screenleft)
+ result.scrollLeft = Math.max(0, x1 - (tooWide ? 0 : 10));
+ else if (x2 > screenw + screenleft - 3)
+ result.scrollLeft = x2 + (tooWide ? 0 : 10) - screenw;
+ return result;
+ }
+
+ // Store a relative adjustment to the scroll position in the current
+ // operation (to be applied when the operation finishes).
+ function addToScrollPos(cm, left, top) {
+ if (left != null || top != null) resolveScrollToPos(cm);
+ if (left != null)
+ cm.curOp.scrollLeft = (cm.curOp.scrollLeft == null ? cm.doc.scrollLeft : cm.curOp.scrollLeft) + left;
+ if (top != null)
+ cm.curOp.scrollTop = (cm.curOp.scrollTop == null ? cm.doc.scrollTop : cm.curOp.scrollTop) + top;
+ }
+
+ // Make sure that at the end of the operation the current cursor is
+ // shown.
+ function ensureCursorVisible(cm) {
+ resolveScrollToPos(cm);
+ var cur = cm.getCursor(), from = cur, to = cur;
+ if (!cm.options.lineWrapping) {
+ from = cur.ch ? Pos(cur.line, cur.ch - 1) : cur;
+ to = Pos(cur.line, cur.ch + 1);
+ }
+ cm.curOp.scrollToPos = {from: from, to: to, margin: cm.options.cursorScrollMargin, isCursor: true};
+ }
+
+ // When an operation has its scrollToPos property set, and another
+ // scroll action is applied before the end of the operation, this
+ // 'simulates' scrolling that position into view in a cheap way, so
+ // that the effect of intermediate scroll commands is not ignored.
+ function resolveScrollToPos(cm) {
+ var range = cm.curOp.scrollToPos;
+ if (range) {
+ cm.curOp.scrollToPos = null;
+ var from = estimateCoords(cm, range.from), to = estimateCoords(cm, range.to);
+ var sPos = calculateScrollPos(cm, Math.min(from.left, to.left),
+ Math.min(from.top, to.top) - range.margin,
+ Math.max(from.right, to.right),
+ Math.max(from.bottom, to.bottom) + range.margin);
+ cm.scrollTo(sPos.scrollLeft, sPos.scrollTop);
+ }
+ }
+
+ // API UTILITIES
+
+ // Indent the given line. The how parameter can be "smart",
+ // "add"/null, "subtract", or "prev". When aggressive is false
+ // (typically set to true for forced single-line indents), empty
+ // lines are not indented, and places where the mode returns Pass
+ // are left alone.
+ function indentLine(cm, n, how, aggressive) {
+ var doc = cm.doc, state;
+ if (how == null) how = "add";
+ if (how == "smart") {
+ // Fall back to "prev" when the mode doesn't have an indentation
+ // method.
+ if (!doc.mode.indent) how = "prev";
+ else state = getStateBefore(cm, n);
+ }
+
+ var tabSize = cm.options.tabSize;
+ var line = getLine(doc, n), curSpace = countColumn(line.text, null, tabSize);
+ if (line.stateAfter) line.stateAfter = null;
+ var curSpaceString = line.text.match(/^\s*/)[0], indentation;
+ if (!aggressive && !/\S/.test(line.text)) {
+ indentation = 0;
+ how = "not";
+ } else if (how == "smart") {
+ indentation = doc.mode.indent(state, line.text.slice(curSpaceString.length), line.text);
+ if (indentation == Pass || indentation > 150) {
+ if (!aggressive) return;
+ how = "prev";
+ }
+ }
+ if (how == "prev") {
+ if (n > doc.first) indentation = countColumn(getLine(doc, n-1).text, null, tabSize);
+ else indentation = 0;
+ } else if (how == "add") {
+ indentation = curSpace + cm.options.indentUnit;
+ } else if (how == "subtract") {
+ indentation = curSpace - cm.options.indentUnit;
+ } else if (typeof how == "number") {
+ indentation = curSpace + how;
+ }
+ indentation = Math.max(0, indentation);
+
+ var indentString = "", pos = 0;
+ if (cm.options.indentWithTabs)
+ for (var i = Math.floor(indentation / tabSize); i; --i) {pos += tabSize; indentString += "\t";}
+ if (pos < indentation) indentString += spaceStr(indentation - pos);
+
+ if (indentString != curSpaceString) {
+ replaceRange(doc, indentString, Pos(n, 0), Pos(n, curSpaceString.length), "+input");
+ line.stateAfter = null;
+ return true;
+ } else {
+ // Ensure that, if the cursor was in the whitespace at the start
+ // of the line, it is moved to the end of that space.
+ for (var i = 0; i < doc.sel.ranges.length; i++) {
+ var range = doc.sel.ranges[i];
+ if (range.head.line == n && range.head.ch < curSpaceString.length) {
+ var pos = Pos(n, curSpaceString.length);
+ replaceOneSelection(doc, i, new Range(pos, pos));
+ break;
+ }
+ }
+ }
+ }
+
+ // Utility for applying a change to a line by handle or number,
+ // returning the number and optionally registering the line as
+ // changed.
+ function changeLine(doc, handle, changeType, op) {
+ var no = handle, line = handle;
+ if (typeof handle == "number") line = getLine(doc, clipLine(doc, handle));
+ else no = lineNo(handle);
+ if (no == null) return null;
+ if (op(line, no) && doc.cm) regLineChange(doc.cm, no, changeType);
+ return line;
+ }
+
+ // Helper for deleting text near the selection(s), used to implement
+ // backspace, delete, and similar functionality.
+ function deleteNearSelection(cm, compute) {
+ var ranges = cm.doc.sel.ranges, kill = [];
+ // Build up a set of ranges to kill first, merging overlapping
+ // ranges.
+ for (var i = 0; i < ranges.length; i++) {
+ var toKill = compute(ranges[i]);
+ while (kill.length && cmp(toKill.from, lst(kill).to) <= 0) {
+ var replaced = kill.pop();
+ if (cmp(replaced.from, toKill.from) < 0) {
+ toKill.from = replaced.from;
+ break;
+ }
+ }
+ kill.push(toKill);
+ }
+ // Next, remove those actual ranges.
+ runInOp(cm, function() {
+ for (var i = kill.length - 1; i >= 0; i--)
+ replaceRange(cm.doc, "", kill[i].from, kill[i].to, "+delete");
+ ensureCursorVisible(cm);
+ });
+ }
+
+ // Used for horizontal relative motion. Dir is -1 or 1 (left or
+ // right), unit can be "char", "column" (like char, but doesn't
+ // cross line boundaries), "word" (across next word), or "group" (to
+ // the start of next group of word or non-word-non-whitespace
+ // chars). The visually param controls whether, in right-to-left
+ // text, direction 1 means to move towards the next index in the
+ // string, or towards the character to the right of the current
+ // position. The resulting position will have a hitSide=true
+ // property if it reached the end of the document.
+ function findPosH(doc, pos, dir, unit, visually) {
+ var line = pos.line, ch = pos.ch, origDir = dir;
+ var lineObj = getLine(doc, line);
+ function findNextLine() {
+ var l = line + dir;
+ if (l < doc.first || l >= doc.first + doc.size) return false
+ line = l;
+ return lineObj = getLine(doc, l);
+ }
+ function moveOnce(boundToLine) {
+ var next = (visually ? moveVisually : moveLogically)(lineObj, ch, dir, true);
+ if (next == null) {
+ if (!boundToLine && findNextLine()) {
+ if (visually) ch = (dir < 0 ? lineRight : lineLeft)(lineObj);
+ else ch = dir < 0 ? lineObj.text.length : 0;
+ } else return false
+ } else ch = next;
+ return true;
+ }
+
+ if (unit == "char") {
+ moveOnce()
+ } else if (unit == "column") {
+ moveOnce(true)
+ } else if (unit == "word" || unit == "group") {
+ var sawType = null, group = unit == "group";
+ var helper = doc.cm && doc.cm.getHelper(pos, "wordChars");
+ for (var first = true;; first = false) {
+ if (dir < 0 && !moveOnce(!first)) break;
+ var cur = lineObj.text.charAt(ch) || "\n";
+ var type = isWordChar(cur, helper) ? "w"
+ : group && cur == "\n" ? "n"
+ : !group || /\s/.test(cur) ? null
+ : "p";
+ if (group && !first && !type) type = "s";
+ if (sawType && sawType != type) {
+ if (dir < 0) {dir = 1; moveOnce();}
+ break;
+ }
+
+ if (type) sawType = type;
+ if (dir > 0 && !moveOnce(!first)) break;
+ }
+ }
+ var result = skipAtomic(doc, Pos(line, ch), pos, origDir, true);
+ if (!cmp(pos, result)) result.hitSide = true;
+ return result;
+ }
+
+ // For relative vertical movement. Dir may be -1 or 1. Unit can be
+ // "page" or "line". The resulting position will have a hitSide=true
+ // property if it reached the end of the document.
+ function findPosV(cm, pos, dir, unit) {
+ var doc = cm.doc, x = pos.left, y;
+ if (unit == "page") {
+ var pageSize = Math.min(cm.display.wrapper.clientHeight, window.innerHeight || document.documentElement.clientHeight);
+ y = pos.top + dir * (pageSize - (dir < 0 ? 1.5 : .5) * textHeight(cm.display));
+ } else if (unit == "line") {
+ y = dir > 0 ? pos.bottom + 3 : pos.top - 3;
+ }
+ for (;;) {
+ var target = coordsChar(cm, x, y);
+ if (!target.outside) break;
+ if (dir < 0 ? y <= 0 : y >= doc.height) { target.hitSide = true; break; }
+ y += dir * 5;
+ }
+ return target;
+ }
+
+ // EDITOR METHODS
+
+ // The publicly visible API. Note that methodOp(f) means
+ // 'wrap f in an operation, performed on its `this` parameter'.
+
+ // This is not the complete set of editor methods. Most of the
+ // methods defined on the Doc type are also injected into
+ // CodeMirror.prototype, for backwards compatibility and
+ // convenience.
+
+ CodeMirror.prototype = {
+ constructor: CodeMirror,
+ focus: function(){window.focus(); this.display.input.focus();},
+
+ setOption: function(option, value) {
+ var options = this.options, old = options[option];
+ if (options[option] == value && option != "mode") return;
+ options[option] = value;
+ if (optionHandlers.hasOwnProperty(option))
+ operation(this, optionHandlers[option])(this, value, old);
+ },
+
+ getOption: function(option) {return this.options[option];},
+ getDoc: function() {return this.doc;},
+
+ addKeyMap: function(map, bottom) {
+ this.state.keyMaps[bottom ? "push" : "unshift"](getKeyMap(map));
+ },
+ removeKeyMap: function(map) {
+ var maps = this.state.keyMaps;
+ for (var i = 0; i < maps.length; ++i)
+ if (maps[i] == map || maps[i].name == map) {
+ maps.splice(i, 1);
+ return true;
+ }
+ },
+
+ addOverlay: methodOp(function(spec, options) {
+ var mode = spec.token ? spec : CodeMirror.getMode(this.options, spec);
+ if (mode.startState) throw new Error("Overlays may not be stateful.");
+ this.state.overlays.push({mode: mode, modeSpec: spec, opaque: options && options.opaque});
+ this.state.modeGen++;
+ regChange(this);
+ }),
+ removeOverlay: methodOp(function(spec) {
+ var overlays = this.state.overlays;
+ for (var i = 0; i < overlays.length; ++i) {
+ var cur = overlays[i].modeSpec;
+ if (cur == spec || typeof spec == "string" && cur.name == spec) {
+ overlays.splice(i, 1);
+ this.state.modeGen++;
+ regChange(this);
+ return;
+ }
+ }
+ }),
+
+ indentLine: methodOp(function(n, dir, aggressive) {
+ if (typeof dir != "string" && typeof dir != "number") {
+ if (dir == null) dir = this.options.smartIndent ? "smart" : "prev";
+ else dir = dir ? "add" : "subtract";
+ }
+ if (isLine(this.doc, n)) indentLine(this, n, dir, aggressive);
+ }),
+ indentSelection: methodOp(function(how) {
+ var ranges = this.doc.sel.ranges, end = -1;
+ for (var i = 0; i < ranges.length; i++) {
+ var range = ranges[i];
+ if (!range.empty()) {
+ var from = range.from(), to = range.to();
+ var start = Math.max(end, from.line);
+ end = Math.min(this.lastLine(), to.line - (to.ch ? 0 : 1)) + 1;
+ for (var j = start; j < end; ++j)
+ indentLine(this, j, how);
+ var newRanges = this.doc.sel.ranges;
+ if (from.ch == 0 && ranges.length == newRanges.length && newRanges[i].from().ch > 0)
+ replaceOneSelection(this.doc, i, new Range(from, newRanges[i].to()), sel_dontScroll);
+ } else if (range.head.line > end) {
+ indentLine(this, range.head.line, how, true);
+ end = range.head.line;
+ if (i == this.doc.sel.primIndex) ensureCursorVisible(this);
+ }
+ }
+ }),
+
+ // Fetch the parser token for a given character. Useful for hacks
+ // that want to inspect the mode state (say, for completion).
+ getTokenAt: function(pos, precise) {
+ return takeToken(this, pos, precise);
+ },
+
+ getLineTokens: function(line, precise) {
+ return takeToken(this, Pos(line), precise, true);
+ },
+
+ getTokenTypeAt: function(pos) {
+ pos = clipPos(this.doc, pos);
+ var styles = getLineStyles(this, getLine(this.doc, pos.line));
+ var before = 0, after = (styles.length - 1) / 2, ch = pos.ch;
+ var type;
+ if (ch == 0) type = styles[2];
+ else for (;;) {
+ var mid = (before + after) >> 1;
+ if ((mid ? styles[mid * 2 - 1] : 0) >= ch) after = mid;
+ else if (styles[mid * 2 + 1] < ch) before = mid + 1;
+ else { type = styles[mid * 2 + 2]; break; }
+ }
+ var cut = type ? type.indexOf("cm-overlay ") : -1;
+ return cut < 0 ? type : cut == 0 ? null : type.slice(0, cut - 1);
+ },
+
+ getModeAt: function(pos) {
+ var mode = this.doc.mode;
+ if (!mode.innerMode) return mode;
+ return CodeMirror.innerMode(mode, this.getTokenAt(pos).state).mode;
+ },
+
+ getHelper: function(pos, type) {
+ return this.getHelpers(pos, type)[0];
+ },
+
+ getHelpers: function(pos, type) {
+ var found = [];
+ if (!helpers.hasOwnProperty(type)) return found;
+ var help = helpers[type], mode = this.getModeAt(pos);
+ if (typeof mode[type] == "string") {
+ if (help[mode[type]]) found.push(help[mode[type]]);
+ } else if (mode[type]) {
+ for (var i = 0; i < mode[type].length; i++) {
+ var val = help[mode[type][i]];
+ if (val) found.push(val);
+ }
+ } else if (mode.helperType && help[mode.helperType]) {
+ found.push(help[mode.helperType]);
+ } else if (help[mode.name]) {
+ found.push(help[mode.name]);
+ }
+ for (var i = 0; i < help._global.length; i++) {
+ var cur = help._global[i];
+ if (cur.pred(mode, this) && indexOf(found, cur.val) == -1)
+ found.push(cur.val);
+ }
+ return found;
+ },
+
+ getStateAfter: function(line, precise) {
+ var doc = this.doc;
+ line = clipLine(doc, line == null ? doc.first + doc.size - 1: line);
+ return getStateBefore(this, line + 1, precise);
+ },
+
+ cursorCoords: function(start, mode) {
+ var pos, range = this.doc.sel.primary();
+ if (start == null) pos = range.head;
+ else if (typeof start == "object") pos = clipPos(this.doc, start);
+ else pos = start ? range.from() : range.to();
+ return cursorCoords(this, pos, mode || "page");
+ },
+
+ charCoords: function(pos, mode) {
+ return charCoords(this, clipPos(this.doc, pos), mode || "page");
+ },
+
+ coordsChar: function(coords, mode) {
+ coords = fromCoordSystem(this, coords, mode || "page");
+ return coordsChar(this, coords.left, coords.top);
+ },
+
+ lineAtHeight: function(height, mode) {
+ height = fromCoordSystem(this, {top: height, left: 0}, mode || "page").top;
+ return lineAtHeight(this.doc, height + this.display.viewOffset);
+ },
+ heightAtLine: function(line, mode) {
+ var end = false, lineObj;
+ if (typeof line == "number") {
+ var last = this.doc.first + this.doc.size - 1;
+ if (line < this.doc.first) line = this.doc.first;
+ else if (line > last) { line = last; end = true; }
+ lineObj = getLine(this.doc, line);
+ } else {
+ lineObj = line;
+ }
+ return intoCoordSystem(this, lineObj, {top: 0, left: 0}, mode || "page").top +
+ (end ? this.doc.height - heightAtLine(lineObj) : 0);
+ },
+
+ defaultTextHeight: function() { return textHeight(this.display); },
+ defaultCharWidth: function() { return charWidth(this.display); },
+
+ setGutterMarker: methodOp(function(line, gutterID, value) {
+ return changeLine(this.doc, line, "gutter", function(line) {
+ var markers = line.gutterMarkers || (line.gutterMarkers = {});
+ markers[gutterID] = value;
+ if (!value && isEmpty(markers)) line.gutterMarkers = null;
+ return true;
+ });
+ }),
+
+ clearGutter: methodOp(function(gutterID) {
+ var cm = this, doc = cm.doc, i = doc.first;
+ doc.iter(function(line) {
+ if (line.gutterMarkers && line.gutterMarkers[gutterID]) {
+ line.gutterMarkers[gutterID] = null;
+ regLineChange(cm, i, "gutter");
+ if (isEmpty(line.gutterMarkers)) line.gutterMarkers = null;
+ }
+ ++i;
+ });
+ }),
+
+ lineInfo: function(line) {
+ if (typeof line == "number") {
+ if (!isLine(this.doc, line)) return null;
+ var n = line;
+ line = getLine(this.doc, line);
+ if (!line) return null;
+ } else {
+ var n = lineNo(line);
+ if (n == null) return null;
+ }
+ return {line: n, handle: line, text: line.text, gutterMarkers: line.gutterMarkers,
+ textClass: line.textClass, bgClass: line.bgClass, wrapClass: line.wrapClass,
+ widgets: line.widgets};
+ },
+
+ getViewport: function() { return {from: this.display.viewFrom, to: this.display.viewTo};},
+
+ addWidget: function(pos, node, scroll, vert, horiz) {
+ var display = this.display;
+ pos = cursorCoords(this, clipPos(this.doc, pos));
+ var top = pos.bottom, left = pos.left;
+ node.style.position = "absolute";
+ node.setAttribute("cm-ignore-events", "true");
+ this.display.input.setUneditable(node);
+ display.sizer.appendChild(node);
+ if (vert == "over") {
+ top = pos.top;
+ } else if (vert == "above" || vert == "near") {
+ var vspace = Math.max(display.wrapper.clientHeight, this.doc.height),
+ hspace = Math.max(display.sizer.clientWidth, display.lineSpace.clientWidth);
+ // Default to positioning above (if specified and possible); otherwise default to positioning below
+ if ((vert == 'above' || pos.bottom + node.offsetHeight > vspace) && pos.top > node.offsetHeight)
+ top = pos.top - node.offsetHeight;
+ else if (pos.bottom + node.offsetHeight <= vspace)
+ top = pos.bottom;
+ if (left + node.offsetWidth > hspace)
+ left = hspace - node.offsetWidth;
+ }
+ node.style.top = top + "px";
+ node.style.left = node.style.right = "";
+ if (horiz == "right") {
+ left = display.sizer.clientWidth - node.offsetWidth;
+ node.style.right = "0px";
+ } else {
+ if (horiz == "left") left = 0;
+ else if (horiz == "middle") left = (display.sizer.clientWidth - node.offsetWidth) / 2;
+ node.style.left = left + "px";
+ }
+ if (scroll)
+ scrollIntoView(this, left, top, left + node.offsetWidth, top + node.offsetHeight);
+ },
+
+ triggerOnKeyDown: methodOp(onKeyDown),
+ triggerOnKeyPress: methodOp(onKeyPress),
+ triggerOnKeyUp: onKeyUp,
+
+ execCommand: function(cmd) {
+ if (commands.hasOwnProperty(cmd))
+ return commands[cmd].call(null, this);
+ },
+
+ triggerElectric: methodOp(function(text) { triggerElectric(this, text); }),
+
+ findPosH: function(from, amount, unit, visually) {
+ var dir = 1;
+ if (amount < 0) { dir = -1; amount = -amount; }
+ for (var i = 0, cur = clipPos(this.doc, from); i < amount; ++i) {
+ cur = findPosH(this.doc, cur, dir, unit, visually);
+ if (cur.hitSide) break;
+ }
+ return cur;
+ },
+
+ moveH: methodOp(function(dir, unit) {
+ var cm = this;
+ cm.extendSelectionsBy(function(range) {
+ if (cm.display.shift || cm.doc.extend || range.empty())
+ return findPosH(cm.doc, range.head, dir, unit, cm.options.rtlMoveVisually);
+ else
+ return dir < 0 ? range.from() : range.to();
+ }, sel_move);
+ }),
+
+ deleteH: methodOp(function(dir, unit) {
+ var sel = this.doc.sel, doc = this.doc;
+ if (sel.somethingSelected())
+ doc.replaceSelection("", null, "+delete");
+ else
+ deleteNearSelection(this, function(range) {
+ var other = findPosH(doc, range.head, dir, unit, false);
+ return dir < 0 ? {from: other, to: range.head} : {from: range.head, to: other};
+ });
+ }),
+
+ findPosV: function(from, amount, unit, goalColumn) {
+ var dir = 1, x = goalColumn;
+ if (amount < 0) { dir = -1; amount = -amount; }
+ for (var i = 0, cur = clipPos(this.doc, from); i < amount; ++i) {
+ var coords = cursorCoords(this, cur, "div");
+ if (x == null) x = coords.left;
+ else coords.left = x;
+ cur = findPosV(this, coords, dir, unit);
+ if (cur.hitSide) break;
+ }
+ return cur;
+ },
+
+ moveV: methodOp(function(dir, unit) {
+ var cm = this, doc = this.doc, goals = [];
+ var collapse = !cm.display.shift && !doc.extend && doc.sel.somethingSelected();
+ doc.extendSelectionsBy(function(range) {
+ if (collapse)
+ return dir < 0 ? range.from() : range.to();
+ var headPos = cursorCoords(cm, range.head, "div");
+ if (range.goalColumn != null) headPos.left = range.goalColumn;
+ goals.push(headPos.left);
+ var pos = findPosV(cm, headPos, dir, unit);
+ if (unit == "page" && range == doc.sel.primary())
+ addToScrollPos(cm, null, charCoords(cm, pos, "div").top - headPos.top);
+ return pos;
+ }, sel_move);
+ if (goals.length) for (var i = 0; i < doc.sel.ranges.length; i++)
+ doc.sel.ranges[i].goalColumn = goals[i];
+ }),
+
+ // Find the word at the given position (as returned by coordsChar).
+ findWordAt: function(pos) {
+ var doc = this.doc, line = getLine(doc, pos.line).text;
+ var start = pos.ch, end = pos.ch;
+ if (line) {
+ var helper = this.getHelper(pos, "wordChars");
+ if ((pos.xRel < 0 || end == line.length) && start) --start; else ++end;
+ var startChar = line.charAt(start);
+ var check = isWordChar(startChar, helper)
+ ? function(ch) { return isWordChar(ch, helper); }
+ : /\s/.test(startChar) ? function(ch) {return /\s/.test(ch);}
+ : function(ch) {return !/\s/.test(ch) && !isWordChar(ch);};
+ while (start > 0 && check(line.charAt(start - 1))) --start;
+ while (end < line.length && check(line.charAt(end))) ++end;
+ }
+ return new Range(Pos(pos.line, start), Pos(pos.line, end));
+ },
+
+ toggleOverwrite: function(value) {
+ if (value != null && value == this.state.overwrite) return;
+ if (this.state.overwrite = !this.state.overwrite)
+ addClass(this.display.cursorDiv, "CodeMirror-overwrite");
+ else
+ rmClass(this.display.cursorDiv, "CodeMirror-overwrite");
+
+ signal(this, "overwriteToggle", this, this.state.overwrite);
+ },
+ hasFocus: function() { return this.display.input.getField() == activeElt(); },
+ isReadOnly: function() { return !!(this.options.readOnly || this.doc.cantEdit); },
+
+ scrollTo: methodOp(function(x, y) {
+ if (x != null || y != null) resolveScrollToPos(this);
+ if (x != null) this.curOp.scrollLeft = x;
+ if (y != null) this.curOp.scrollTop = y;
+ }),
+ getScrollInfo: function() {
+ var scroller = this.display.scroller;
+ return {left: scroller.scrollLeft, top: scroller.scrollTop,
+ height: scroller.scrollHeight - scrollGap(this) - this.display.barHeight,
+ width: scroller.scrollWidth - scrollGap(this) - this.display.barWidth,
+ clientHeight: displayHeight(this), clientWidth: displayWidth(this)};
+ },
+
+ scrollIntoView: methodOp(function(range, margin) {
+ if (range == null) {
+ range = {from: this.doc.sel.primary().head, to: null};
+ if (margin == null) margin = this.options.cursorScrollMargin;
+ } else if (typeof range == "number") {
+ range = {from: Pos(range, 0), to: null};
+ } else if (range.from == null) {
+ range = {from: range, to: null};
+ }
+ if (!range.to) range.to = range.from;
+ range.margin = margin || 0;
+
+ if (range.from.line != null) {
+ resolveScrollToPos(this);
+ this.curOp.scrollToPos = range;
+ } else {
+ var sPos = calculateScrollPos(this, Math.min(range.from.left, range.to.left),
+ Math.min(range.from.top, range.to.top) - range.margin,
+ Math.max(range.from.right, range.to.right),
+ Math.max(range.from.bottom, range.to.bottom) + range.margin);
+ this.scrollTo(sPos.scrollLeft, sPos.scrollTop);
+ }
+ }),
+
+ setSize: methodOp(function(width, height) {
+ var cm = this;
+ function interpret(val) {
+ return typeof val == "number" || /^\d+$/.test(String(val)) ? val + "px" : val;
+ }
+ if (width != null) cm.display.wrapper.style.width = interpret(width);
+ if (height != null) cm.display.wrapper.style.height = interpret(height);
+ if (cm.options.lineWrapping) clearLineMeasurementCache(this);
+ var lineNo = cm.display.viewFrom;
+ cm.doc.iter(lineNo, cm.display.viewTo, function(line) {
+ if (line.widgets) for (var i = 0; i < line.widgets.length; i++)
+ if (line.widgets[i].noHScroll) { regLineChange(cm, lineNo, "widget"); break; }
+ ++lineNo;
+ });
+ cm.curOp.forceUpdate = true;
+ signal(cm, "refresh", this);
+ }),
+
+ operation: function(f){return runInOp(this, f);},
+
+ refresh: methodOp(function() {
+ var oldHeight = this.display.cachedTextHeight;
+ regChange(this);
+ this.curOp.forceUpdate = true;
+ clearCaches(this);
+ this.scrollTo(this.doc.scrollLeft, this.doc.scrollTop);
+ updateGutterSpace(this);
+ if (oldHeight == null || Math.abs(oldHeight - textHeight(this.display)) > .5)
+ estimateLineHeights(this);
+ signal(this, "refresh", this);
+ }),
+
+ swapDoc: methodOp(function(doc) {
+ var old = this.doc;
+ old.cm = null;
+ attachDoc(this, doc);
+ clearCaches(this);
+ this.display.input.reset();
+ this.scrollTo(doc.scrollLeft, doc.scrollTop);
+ this.curOp.forceScroll = true;
+ signalLater(this, "swapDoc", this, old);
+ return old;
+ }),
+
+ getInputField: function(){return this.display.input.getField();},
+ getWrapperElement: function(){return this.display.wrapper;},
+ getScrollerElement: function(){return this.display.scroller;},
+ getGutterElement: function(){return this.display.gutters;}
+ };
+ eventMixin(CodeMirror);
+
+ // OPTION DEFAULTS
+
+ // The default configuration options.
+ var defaults = CodeMirror.defaults = {};
+ // Functions to run when options are changed.
+ var optionHandlers = CodeMirror.optionHandlers = {};
+
+ function option(name, deflt, handle, notOnInit) {
+ CodeMirror.defaults[name] = deflt;
+ if (handle) optionHandlers[name] =
+ notOnInit ? function(cm, val, old) {if (old != Init) handle(cm, val, old);} : handle;
+ }
+
+ // Passed to option handlers when there is no old value.
+ var Init = CodeMirror.Init = {toString: function(){return "CodeMirror.Init";}};
+
+ // These two are, on init, called from the constructor because they
+ // have to be initialized before the editor can start at all.
+ option("value", "", function(cm, val) {
+ cm.setValue(val);
+ }, true);
+ option("mode", null, function(cm, val) {
+ cm.doc.modeOption = val;
+ loadMode(cm);
+ }, true);
+
+ option("indentUnit", 2, loadMode, true);
+ option("indentWithTabs", false);
+ option("smartIndent", true);
+ option("tabSize", 4, function(cm) {
+ resetModeState(cm);
+ clearCaches(cm);
+ regChange(cm);
+ }, true);
+ option("lineSeparator", null, function(cm, val) {
+ cm.doc.lineSep = val;
+ if (!val) return;
+ var newBreaks = [], lineNo = cm.doc.first;
+ cm.doc.iter(function(line) {
+ for (var pos = 0;;) {
+ var found = line.text.indexOf(val, pos);
+ if (found == -1) break;
+ pos = found + val.length;
+ newBreaks.push(Pos(lineNo, found));
+ }
+ lineNo++;
+ });
+ for (var i = newBreaks.length - 1; i >= 0; i--)
+ replaceRange(cm.doc, val, newBreaks[i], Pos(newBreaks[i].line, newBreaks[i].ch + val.length))
+ });
+ option("specialChars", /[\u0000-\u001f\u007f\u00ad\u200b-\u200f\u2028\u2029\ufeff]/g, function(cm, val, old) {
+ cm.state.specialChars = new RegExp(val.source + (val.test("\t") ? "" : "|\t"), "g");
+ if (old != CodeMirror.Init) cm.refresh();
+ });
+ option("specialCharPlaceholder", defaultSpecialCharPlaceholder, function(cm) {cm.refresh();}, true);
+ option("electricChars", true);
+ option("inputStyle", mobile ? "contenteditable" : "textarea", function() {
+ throw new Error("inputStyle can not (yet) be changed in a running editor"); // FIXME
+ }, true);
+ option("rtlMoveVisually", !windows);
+ option("wholeLineUpdateBefore", true);
+
+ option("theme", "default", function(cm) {
+ themeChanged(cm);
+ guttersChanged(cm);
+ }, true);
+ option("keyMap", "default", function(cm, val, old) {
+ var next = getKeyMap(val);
+ var prev = old != CodeMirror.Init && getKeyMap(old);
+ if (prev && prev.detach) prev.detach(cm, next);
+ if (next.attach) next.attach(cm, prev || null);
+ });
+ option("extraKeys", null);
+
+ option("lineWrapping", false, wrappingChanged, true);
+ option("gutters", [], function(cm) {
+ setGuttersForLineNumbers(cm.options);
+ guttersChanged(cm);
+ }, true);
+ option("fixedGutter", true, function(cm, val) {
+ cm.display.gutters.style.left = val ? compensateForHScroll(cm.display) + "px" : "0";
+ cm.refresh();
+ }, true);
+ option("coverGutterNextToScrollbar", false, function(cm) {updateScrollbars(cm);}, true);
+ option("scrollbarStyle", "native", function(cm) {
+ initScrollbars(cm);
+ updateScrollbars(cm);
+ cm.display.scrollbars.setScrollTop(cm.doc.scrollTop);
+ cm.display.scrollbars.setScrollLeft(cm.doc.scrollLeft);
+ }, true);
+ option("lineNumbers", false, function(cm) {
+ setGuttersForLineNumbers(cm.options);
+ guttersChanged(cm);
+ }, true);
+ option("firstLineNumber", 1, guttersChanged, true);
+ option("lineNumberFormatter", function(integer) {return integer;}, guttersChanged, true);
+ option("showCursorWhenSelecting", false, updateSelection, true);
+
+ option("resetSelectionOnContextMenu", true);
+ option("lineWiseCopyCut", true);
+
+ option("readOnly", false, function(cm, val) {
+ if (val == "nocursor") {
+ onBlur(cm);
+ cm.display.input.blur();
+ cm.display.disabled = true;
+ } else {
+ cm.display.disabled = false;
+ }
+ cm.display.input.readOnlyChanged(val)
+ });
+ option("disableInput", false, function(cm, val) {if (!val) cm.display.input.reset();}, true);
+ option("dragDrop", true, dragDropChanged);
+ option("allowDropFileTypes", null);
+
+ option("cursorBlinkRate", 530);
+ option("cursorScrollMargin", 0);
+ option("cursorHeight", 1, updateSelection, true);
+ option("singleCursorHeightPerLine", true, updateSelection, true);
+ option("workTime", 100);
+ option("workDelay", 100);
+ option("flattenSpans", true, resetModeState, true);
+ option("addModeClass", false, resetModeState, true);
+ option("pollInterval", 100);
+ option("undoDepth", 200, function(cm, val){cm.doc.history.undoDepth = val;});
+ option("historyEventDelay", 1250);
+ option("viewportMargin", 10, function(cm){cm.refresh();}, true);
+ option("maxHighlightLength", 10000, resetModeState, true);
+ option("moveInputWithCursor", true, function(cm, val) {
+ if (!val) cm.display.input.resetPosition();
+ });
+
+ option("tabindex", null, function(cm, val) {
+ cm.display.input.getField().tabIndex = val || "";
+ });
+ option("autofocus", null);
+
+ // MODE DEFINITION AND QUERYING
+
+ // Known modes, by name and by MIME
+ var modes = CodeMirror.modes = {}, mimeModes = CodeMirror.mimeModes = {};
+
+ // Extra arguments are stored as the mode's dependencies, which is
+ // used by (legacy) mechanisms like loadmode.js to automatically
+ // load a mode. (Preferred mechanism is the require/define calls.)
+ CodeMirror.defineMode = function(name, mode) {
+ if (!CodeMirror.defaults.mode && name != "null") CodeMirror.defaults.mode = name;
+ if (arguments.length > 2)
+ mode.dependencies = Array.prototype.slice.call(arguments, 2);
+ modes[name] = mode;
+ };
+
+ CodeMirror.defineMIME = function(mime, spec) {
+ mimeModes[mime] = spec;
+ };
+
+ // Given a MIME type, a {name, ...options} config object, or a name
+ // string, return a mode config object.
+ CodeMirror.resolveMode = function(spec) {
+ if (typeof spec == "string" && mimeModes.hasOwnProperty(spec)) {
+ spec = mimeModes[spec];
+ } else if (spec && typeof spec.name == "string" && mimeModes.hasOwnProperty(spec.name)) {
+ var found = mimeModes[spec.name];
+ if (typeof found == "string") found = {name: found};
+ spec = createObj(found, spec);
+ spec.name = found.name;
+ } else if (typeof spec == "string" && /^[\w\-]+\/[\w\-]+\+xml$/.test(spec)) {
+ return CodeMirror.resolveMode("application/xml");
+ }
+ if (typeof spec == "string") return {name: spec};
+ else return spec || {name: "null"};
+ };
+
+ // Given a mode spec (anything that resolveMode accepts), find and
+ // initialize an actual mode object.
+ CodeMirror.getMode = function(options, spec) {
+ var spec = CodeMirror.resolveMode(spec);
+ var mfactory = modes[spec.name];
+ if (!mfactory) return CodeMirror.getMode(options, "text/plain");
+ var modeObj = mfactory(options, spec);
+ if (modeExtensions.hasOwnProperty(spec.name)) {
+ var exts = modeExtensions[spec.name];
+ for (var prop in exts) {
+ if (!exts.hasOwnProperty(prop)) continue;
+ if (modeObj.hasOwnProperty(prop)) modeObj["_" + prop] = modeObj[prop];
+ modeObj[prop] = exts[prop];
+ }
+ }
+ modeObj.name = spec.name;
+ if (spec.helperType) modeObj.helperType = spec.helperType;
+ if (spec.modeProps) for (var prop in spec.modeProps)
+ modeObj[prop] = spec.modeProps[prop];
+
+ return modeObj;
+ };
+
+ // Minimal default mode.
+ CodeMirror.defineMode("null", function() {
+ return {token: function(stream) {stream.skipToEnd();}};
+ });
+ CodeMirror.defineMIME("text/plain", "null");
+
+ // This can be used to attach properties to mode objects from
+ // outside the actual mode definition.
+ var modeExtensions = CodeMirror.modeExtensions = {};
+ CodeMirror.extendMode = function(mode, properties) {
+ var exts = modeExtensions.hasOwnProperty(mode) ? modeExtensions[mode] : (modeExtensions[mode] = {});
+ copyObj(properties, exts);
+ };
+
+ // EXTENSIONS
+
+ CodeMirror.defineExtension = function(name, func) {
+ CodeMirror.prototype[name] = func;
+ };
+ CodeMirror.defineDocExtension = function(name, func) {
+ Doc.prototype[name] = func;
+ };
+ CodeMirror.defineOption = option;
+
+ var initHooks = [];
+ CodeMirror.defineInitHook = function(f) {initHooks.push(f);};
+
+ var helpers = CodeMirror.helpers = {};
+ CodeMirror.registerHelper = function(type, name, value) {
+ if (!helpers.hasOwnProperty(type)) helpers[type] = CodeMirror[type] = {_global: []};
+ helpers[type][name] = value;
+ };
+ CodeMirror.registerGlobalHelper = function(type, name, predicate, value) {
+ CodeMirror.registerHelper(type, name, value);
+ helpers[type]._global.push({pred: predicate, val: value});
+ };
+
+ // MODE STATE HANDLING
+
+ // Utility functions for working with state. Exported because nested
+ // modes need to do this for their inner modes.
+
+ var copyState = CodeMirror.copyState = function(mode, state) {
+ if (state === true) return state;
+ if (mode.copyState) return mode.copyState(state);
+ var nstate = {};
+ for (var n in state) {
+ var val = state[n];
+ if (val instanceof Array) val = val.concat([]);
+ nstate[n] = val;
+ }
+ return nstate;
+ };
+
+ var startState = CodeMirror.startState = function(mode, a1, a2) {
+ return mode.startState ? mode.startState(a1, a2) : true;
+ };
+
+ // Given a mode and a state (for that mode), find the inner mode and
+ // state at the position that the state refers to.
+ CodeMirror.innerMode = function(mode, state) {
+ while (mode.innerMode) {
+ var info = mode.innerMode(state);
+ if (!info || info.mode == mode) break;
+ state = info.state;
+ mode = info.mode;
+ }
+ return info || {mode: mode, state: state};
+ };
+
+ // STANDARD COMMANDS
+
+ // Commands are parameter-less actions that can be performed on an
+ // editor, mostly used for keybindings.
+ var commands = CodeMirror.commands = {
+ selectAll: function(cm) {cm.setSelection(Pos(cm.firstLine(), 0), Pos(cm.lastLine()), sel_dontScroll);},
+ singleSelection: function(cm) {
+ cm.setSelection(cm.getCursor("anchor"), cm.getCursor("head"), sel_dontScroll);
+ },
+ killLine: function(cm) {
+ deleteNearSelection(cm, function(range) {
+ if (range.empty()) {
+ var len = getLine(cm.doc, range.head.line).text.length;
+ if (range.head.ch == len && range.head.line < cm.lastLine())
+ return {from: range.head, to: Pos(range.head.line + 1, 0)};
+ else
+ return {from: range.head, to: Pos(range.head.line, len)};
+ } else {
+ return {from: range.from(), to: range.to()};
+ }
+ });
+ },
+ deleteLine: function(cm) {
+ deleteNearSelection(cm, function(range) {
+ return {from: Pos(range.from().line, 0),
+ to: clipPos(cm.doc, Pos(range.to().line + 1, 0))};
+ });
+ },
+ delLineLeft: function(cm) {
+ deleteNearSelection(cm, function(range) {
+ return {from: Pos(range.from().line, 0), to: range.from()};
+ });
+ },
+ delWrappedLineLeft: function(cm) {
+ deleteNearSelection(cm, function(range) {
+ var top = cm.charCoords(range.head, "div").top + 5;
+ var leftPos = cm.coordsChar({left: 0, top: top}, "div");
+ return {from: leftPos, to: range.from()};
+ });
+ },
+ delWrappedLineRight: function(cm) {
+ deleteNearSelection(cm, function(range) {
+ var top = cm.charCoords(range.head, "div").top + 5;
+ var rightPos = cm.coordsChar({left: cm.display.lineDiv.offsetWidth + 100, top: top}, "div");
+ return {from: range.from(), to: rightPos };
+ });
+ },
+ undo: function(cm) {cm.undo();},
+ redo: function(cm) {cm.redo();},
+ undoSelection: function(cm) {cm.undoSelection();},
+ redoSelection: function(cm) {cm.redoSelection();},
+ goDocStart: function(cm) {cm.extendSelection(Pos(cm.firstLine(), 0));},
+ goDocEnd: function(cm) {cm.extendSelection(Pos(cm.lastLine()));},
+ goLineStart: function(cm) {
+ cm.extendSelectionsBy(function(range) { return lineStart(cm, range.head.line); },
+ {origin: "+move", bias: 1});
+ },
+ goLineStartSmart: function(cm) {
+ cm.extendSelectionsBy(function(range) {
+ return lineStartSmart(cm, range.head);
+ }, {origin: "+move", bias: 1});
+ },
+ goLineEnd: function(cm) {
+ cm.extendSelectionsBy(function(range) { return lineEnd(cm, range.head.line); },
+ {origin: "+move", bias: -1});
+ },
+ goLineRight: function(cm) {
+ cm.extendSelectionsBy(function(range) {
+ var top = cm.charCoords(range.head, "div").top + 5;
+ return cm.coordsChar({left: cm.display.lineDiv.offsetWidth + 100, top: top}, "div");
+ }, sel_move);
+ },
+ goLineLeft: function(cm) {
+ cm.extendSelectionsBy(function(range) {
+ var top = cm.charCoords(range.head, "div").top + 5;
+ return cm.coordsChar({left: 0, top: top}, "div");
+ }, sel_move);
+ },
+ goLineLeftSmart: function(cm) {
+ cm.extendSelectionsBy(function(range) {
+ var top = cm.charCoords(range.head, "div").top + 5;
+ var pos = cm.coordsChar({left: 0, top: top}, "div");
+ if (pos.ch < cm.getLine(pos.line).search(/\S/)) return lineStartSmart(cm, range.head);
+ return pos;
+ }, sel_move);
+ },
+ goLineUp: function(cm) {cm.moveV(-1, "line");},
+ goLineDown: function(cm) {cm.moveV(1, "line");},
+ goPageUp: function(cm) {cm.moveV(-1, "page");},
+ goPageDown: function(cm) {cm.moveV(1, "page");},
+ goCharLeft: function(cm) {cm.moveH(-1, "char");},
+ goCharRight: function(cm) {cm.moveH(1, "char");},
+ goColumnLeft: function(cm) {cm.moveH(-1, "column");},
+ goColumnRight: function(cm) {cm.moveH(1, "column");},
+ goWordLeft: function(cm) {cm.moveH(-1, "word");},
+ goGroupRight: function(cm) {cm.moveH(1, "group");},
+ goGroupLeft: function(cm) {cm.moveH(-1, "group");},
+ goWordRight: function(cm) {cm.moveH(1, "word");},
+ delCharBefore: function(cm) {cm.deleteH(-1, "char");},
+ delCharAfter: function(cm) {cm.deleteH(1, "char");},
+ delWordBefore: function(cm) {cm.deleteH(-1, "word");},
+ delWordAfter: function(cm) {cm.deleteH(1, "word");},
+ delGroupBefore: function(cm) {cm.deleteH(-1, "group");},
+ delGroupAfter: function(cm) {cm.deleteH(1, "group");},
+ indentAuto: function(cm) {cm.indentSelection("smart");},
+ indentMore: function(cm) {cm.indentSelection("add");},
+ indentLess: function(cm) {cm.indentSelection("subtract");},
+ insertTab: function(cm) {cm.replaceSelection("\t");},
+ insertSoftTab: function(cm) {
+ var spaces = [], ranges = cm.listSelections(), tabSize = cm.options.tabSize;
+ for (var i = 0; i < ranges.length; i++) {
+ var pos = ranges[i].from();
+ var col = countColumn(cm.getLine(pos.line), pos.ch, tabSize);
+ spaces.push(spaceStr(tabSize - col % tabSize));
+ }
+ cm.replaceSelections(spaces);
+ },
+ defaultTab: function(cm) {
+ if (cm.somethingSelected()) cm.indentSelection("add");
+ else cm.execCommand("insertTab");
+ },
+ transposeChars: function(cm) {
+ runInOp(cm, function() {
+ var ranges = cm.listSelections(), newSel = [];
+ for (var i = 0; i < ranges.length; i++) {
+ var cur = ranges[i].head, line = getLine(cm.doc, cur.line).text;
+ if (line) {
+ if (cur.ch == line.length) cur = new Pos(cur.line, cur.ch - 1);
+ if (cur.ch > 0) {
+ cur = new Pos(cur.line, cur.ch + 1);
+ cm.replaceRange(line.charAt(cur.ch - 1) + line.charAt(cur.ch - 2),
+ Pos(cur.line, cur.ch - 2), cur, "+transpose");
+ } else if (cur.line > cm.doc.first) {
+ var prev = getLine(cm.doc, cur.line - 1).text;
+ if (prev)
+ cm.replaceRange(line.charAt(0) + cm.doc.lineSeparator() +
+ prev.charAt(prev.length - 1),
+ Pos(cur.line - 1, prev.length - 1), Pos(cur.line, 1), "+transpose");
+ }
+ }
+ newSel.push(new Range(cur, cur));
+ }
+ cm.setSelections(newSel);
+ });
+ },
+ newlineAndIndent: function(cm) {
+ runInOp(cm, function() {
+ var len = cm.listSelections().length;
+ for (var i = 0; i < len; i++) {
+ var range = cm.listSelections()[i];
+ cm.replaceRange(cm.doc.lineSeparator(), range.anchor, range.head, "+input");
+ cm.indentLine(range.from().line + 1, null, true);
+ }
+ ensureCursorVisible(cm);
+ });
+ },
+ openLine: function(cm) {cm.replaceSelection("\n", "start")},
+ toggleOverwrite: function(cm) {cm.toggleOverwrite();}
+ };
+
+
+ // STANDARD KEYMAPS
+
+ var keyMap = CodeMirror.keyMap = {};
+
+ keyMap.basic = {
+ "Left": "goCharLeft", "Right": "goCharRight", "Up": "goLineUp", "Down": "goLineDown",
+ "End": "goLineEnd", "Home": "goLineStartSmart", "PageUp": "goPageUp", "PageDown": "goPageDown",
+ "Delete": "delCharAfter", "Backspace": "delCharBefore", "Shift-Backspace": "delCharBefore",
+ "Tab": "defaultTab", "Shift-Tab": "indentAuto",
+ "Enter": "newlineAndIndent", "Insert": "toggleOverwrite",
+ "Esc": "singleSelection"
+ };
+ // Note that the save and find-related commands aren't defined by
+ // default. User code or addons can define them. Unknown commands
+ // are simply ignored.
+ keyMap.pcDefault = {
+ "Ctrl-A": "selectAll", "Ctrl-D": "deleteLine", "Ctrl-Z": "undo", "Shift-Ctrl-Z": "redo", "Ctrl-Y": "redo",
+ "Ctrl-Home": "goDocStart", "Ctrl-End": "goDocEnd", "Ctrl-Up": "goLineUp", "Ctrl-Down": "goLineDown",
+ "Ctrl-Left": "goGroupLeft", "Ctrl-Right": "goGroupRight", "Alt-Left": "goLineStart", "Alt-Right": "goLineEnd",
+ "Ctrl-Backspace": "delGroupBefore", "Ctrl-Delete": "delGroupAfter", "Ctrl-S": "save", "Ctrl-F": "find",
+ "Ctrl-G": "findNext", "Shift-Ctrl-G": "findPrev", "Shift-Ctrl-F": "replace", "Shift-Ctrl-R": "replaceAll",
+ "Ctrl-[": "indentLess", "Ctrl-]": "indentMore",
+ "Ctrl-U": "undoSelection", "Shift-Ctrl-U": "redoSelection", "Alt-U": "redoSelection",
+ fallthrough: "basic"
+ };
+ // Very basic readline/emacs-style bindings, which are standard on Mac.
+ keyMap.emacsy = {
+ "Ctrl-F": "goCharRight", "Ctrl-B": "goCharLeft", "Ctrl-P": "goLineUp", "Ctrl-N": "goLineDown",
+ "Alt-F": "goWordRight", "Alt-B": "goWordLeft", "Ctrl-A": "goLineStart", "Ctrl-E": "goLineEnd",
+ "Ctrl-V": "goPageDown", "Shift-Ctrl-V": "goPageUp", "Ctrl-D": "delCharAfter", "Ctrl-H": "delCharBefore",
+ "Alt-D": "delWordAfter", "Alt-Backspace": "delWordBefore", "Ctrl-K": "killLine", "Ctrl-T": "transposeChars",
+ "Ctrl-O": "openLine"
+ };
+ keyMap.macDefault = {
+ "Cmd-A": "selectAll", "Cmd-D": "deleteLine", "Cmd-Z": "undo", "Shift-Cmd-Z": "redo", "Cmd-Y": "redo",
+ "Cmd-Home": "goDocStart", "Cmd-Up": "goDocStart", "Cmd-End": "goDocEnd", "Cmd-Down": "goDocEnd", "Alt-Left": "goGroupLeft",
+ "Alt-Right": "goGroupRight", "Cmd-Left": "goLineLeft", "Cmd-Right": "goLineRight", "Alt-Backspace": "delGroupBefore",
+ "Ctrl-Alt-Backspace": "delGroupAfter", "Alt-Delete": "delGroupAfter", "Cmd-S": "save", "Cmd-F": "find",
+ "Cmd-G": "findNext", "Shift-Cmd-G": "findPrev", "Cmd-Alt-F": "replace", "Shift-Cmd-Alt-F": "replaceAll",
+ "Cmd-[": "indentLess", "Cmd-]": "indentMore", "Cmd-Backspace": "delWrappedLineLeft", "Cmd-Delete": "delWrappedLineRight",
+ "Cmd-U": "undoSelection", "Shift-Cmd-U": "redoSelection", "Ctrl-Up": "goDocStart", "Ctrl-Down": "goDocEnd",
+ fallthrough: ["basic", "emacsy"]
+ };
+ keyMap["default"] = mac ? keyMap.macDefault : keyMap.pcDefault;
+
+ // KEYMAP DISPATCH
+
+ function normalizeKeyName(name) {
+ var parts = name.split(/-(?!$)/), name = parts[parts.length - 1];
+ var alt, ctrl, shift, cmd;
+ for (var i = 0; i < parts.length - 1; i++) {
+ var mod = parts[i];
+ if (/^(cmd|meta|m)$/i.test(mod)) cmd = true;
+ else if (/^a(lt)?$/i.test(mod)) alt = true;
+ else if (/^(c|ctrl|control)$/i.test(mod)) ctrl = true;
+ else if (/^s(hift)$/i.test(mod)) shift = true;
+ else throw new Error("Unrecognized modifier name: " + mod);
+ }
+ if (alt) name = "Alt-" + name;
+ if (ctrl) name = "Ctrl-" + name;
+ if (cmd) name = "Cmd-" + name;
+ if (shift) name = "Shift-" + name;
+ return name;
+ }
+
+ // This is a kludge to keep keymaps mostly working as raw objects
+ // (backwards compatibility) while at the same time support features
+ // like normalization and multi-stroke key bindings. It compiles a
+ // new normalized keymap, and then updates the old object to reflect
+ // this.
+ CodeMirror.normalizeKeyMap = function(keymap) {
+ var copy = {};
+ for (var keyname in keymap) if (keymap.hasOwnProperty(keyname)) {
+ var value = keymap[keyname];
+ if (/^(name|fallthrough|(de|at)tach)$/.test(keyname)) continue;
+ if (value == "...") { delete keymap[keyname]; continue; }
+
+ var keys = map(keyname.split(" "), normalizeKeyName);
+ for (var i = 0; i < keys.length; i++) {
+ var val, name;
+ if (i == keys.length - 1) {
+ name = keys.join(" ");
+ val = value;
+ } else {
+ name = keys.slice(0, i + 1).join(" ");
+ val = "...";
+ }
+ var prev = copy[name];
+ if (!prev) copy[name] = val;
+ else if (prev != val) throw new Error("Inconsistent bindings for " + name);
+ }
+ delete keymap[keyname];
+ }
+ for (var prop in copy) keymap[prop] = copy[prop];
+ return keymap;
+ };
+
+ var lookupKey = CodeMirror.lookupKey = function(key, map, handle, context) {
+ map = getKeyMap(map);
+ var found = map.call ? map.call(key, context) : map[key];
+ if (found === false) return "nothing";
+ if (found === "...") return "multi";
+ if (found != null && handle(found)) return "handled";
+
+ if (map.fallthrough) {
+ if (Object.prototype.toString.call(map.fallthrough) != "[object Array]")
+ return lookupKey(key, map.fallthrough, handle, context);
+ for (var i = 0; i < map.fallthrough.length; i++) {
+ var result = lookupKey(key, map.fallthrough[i], handle, context);
+ if (result) return result;
+ }
+ }
+ };
+
+ // Modifier key presses don't count as 'real' key presses for the
+ // purpose of keymap fallthrough.
+ var isModifierKey = CodeMirror.isModifierKey = function(value) {
+ var name = typeof value == "string" ? value : keyNames[value.keyCode];
+ return name == "Ctrl" || name == "Alt" || name == "Shift" || name == "Mod";
+ };
+
+ // Look up the name of a key as indicated by an event object.
+ var keyName = CodeMirror.keyName = function(event, noShift) {
+ if (presto && event.keyCode == 34 && event["char"]) return false;
+ var base = keyNames[event.keyCode], name = base;
+ if (name == null || event.altGraphKey) return false;
+ if (event.altKey && base != "Alt") name = "Alt-" + name;
+ if ((flipCtrlCmd ? event.metaKey : event.ctrlKey) && base != "Ctrl") name = "Ctrl-" + name;
+ if ((flipCtrlCmd ? event.ctrlKey : event.metaKey) && base != "Cmd") name = "Cmd-" + name;
+ if (!noShift && event.shiftKey && base != "Shift") name = "Shift-" + name;
+ return name;
+ };
+
+ function getKeyMap(val) {
+ return typeof val == "string" ? keyMap[val] : val;
+ }
+
+ // FROMTEXTAREA
+
+ CodeMirror.fromTextArea = function(textarea, options) {
+ options = options ? copyObj(options) : {};
+ options.value = textarea.value;
+ if (!options.tabindex && textarea.tabIndex)
+ options.tabindex = textarea.tabIndex;
+ if (!options.placeholder && textarea.placeholder)
+ options.placeholder = textarea.placeholder;
+ // Set autofocus to true if this textarea is focused, or if it has
+ // autofocus and no other element is focused.
+ if (options.autofocus == null) {
+ var hasFocus = activeElt();
+ options.autofocus = hasFocus == textarea ||
+ textarea.getAttribute("autofocus") != null && hasFocus == document.body;
+ }
+
+ function save() {textarea.value = cm.getValue();}
+ if (textarea.form) {
+ on(textarea.form, "submit", save);
+ // Deplorable hack to make the submit method do the right thing.
+ if (!options.leaveSubmitMethodAlone) {
+ var form = textarea.form, realSubmit = form.submit;
+ try {
+ var wrappedSubmit = form.submit = function() {
+ save();
+ form.submit = realSubmit;
+ form.submit();
+ form.submit = wrappedSubmit;
+ };
+ } catch(e) {}
+ }
+ }
+
+ options.finishInit = function(cm) {
+ cm.save = save;
+ cm.getTextArea = function() { return textarea; };
+ cm.toTextArea = function() {
+ cm.toTextArea = isNaN; // Prevent this from being ran twice
+ save();
+ textarea.parentNode.removeChild(cm.getWrapperElement());
+ textarea.style.display = "";
+ if (textarea.form) {
+ off(textarea.form, "submit", save);
+ if (typeof textarea.form.submit == "function")
+ textarea.form.submit = realSubmit;
+ }
+ };
+ };
+
+ textarea.style.display = "none";
+ var cm = CodeMirror(function(node) {
+ textarea.parentNode.insertBefore(node, textarea.nextSibling);
+ }, options);
+ return cm;
+ };
+
+ // STRING STREAM
+
+ // Fed to the mode parsers, provides helper functions to make
+ // parsers more succinct.
+
+ var StringStream = CodeMirror.StringStream = function(string, tabSize) {
+ this.pos = this.start = 0;
+ this.string = string;
+ this.tabSize = tabSize || 8;
+ this.lastColumnPos = this.lastColumnValue = 0;
+ this.lineStart = 0;
+ };
+
+ StringStream.prototype = {
+ eol: function() {return this.pos >= this.string.length;},
+ sol: function() {return this.pos == this.lineStart;},
+ peek: function() {return this.string.charAt(this.pos) || undefined;},
+ next: function() {
+ if (this.pos < this.string.length)
+ return this.string.charAt(this.pos++);
+ },
+ eat: function(match) {
+ var ch = this.string.charAt(this.pos);
+ if (typeof match == "string") var ok = ch == match;
+ else var ok = ch && (match.test ? match.test(ch) : match(ch));
+ if (ok) {++this.pos; return ch;}
+ },
+ eatWhile: function(match) {
+ var start = this.pos;
+ while (this.eat(match)){}
+ return this.pos > start;
+ },
+ eatSpace: function() {
+ var start = this.pos;
+ while (/[\s\u00a0]/.test(this.string.charAt(this.pos))) ++this.pos;
+ return this.pos > start;
+ },
+ skipToEnd: function() {this.pos = this.string.length;},
+ skipTo: function(ch) {
+ var found = this.string.indexOf(ch, this.pos);
+ if (found > -1) {this.pos = found; return true;}
+ },
+ backUp: function(n) {this.pos -= n;},
+ column: function() {
+ if (this.lastColumnPos < this.start) {
+ this.lastColumnValue = countColumn(this.string, this.start, this.tabSize, this.lastColumnPos, this.lastColumnValue);
+ this.lastColumnPos = this.start;
+ }
+ return this.lastColumnValue - (this.lineStart ? countColumn(this.string, this.lineStart, this.tabSize) : 0);
+ },
+ indentation: function() {
+ return countColumn(this.string, null, this.tabSize) -
+ (this.lineStart ? countColumn(this.string, this.lineStart, this.tabSize) : 0);
+ },
+ match: function(pattern, consume, caseInsensitive) {
+ if (typeof pattern == "string") {
+ var cased = function(str) {return caseInsensitive ? str.toLowerCase() : str;};
+ var substr = this.string.substr(this.pos, pattern.length);
+ if (cased(substr) == cased(pattern)) {
+ if (consume !== false) this.pos += pattern.length;
+ return true;
+ }
+ } else {
+ var match = this.string.slice(this.pos).match(pattern);
+ if (match && match.index > 0) return null;
+ if (match && consume !== false) this.pos += match[0].length;
+ return match;
+ }
+ },
+ current: function(){return this.string.slice(this.start, this.pos);},
+ hideFirstChars: function(n, inner) {
+ this.lineStart += n;
+ try { return inner(); }
+ finally { this.lineStart -= n; }
+ }
+ };
+
+ // TEXTMARKERS
+
+ // Created with markText and setBookmark methods. A TextMarker is a
+ // handle that can be used to clear or find a marked position in the
+ // document. Line objects hold arrays (markedSpans) containing
+ // {from, to, marker} object pointing to such marker objects, and
+ // indicating that such a marker is present on that line. Multiple
+ // lines may point to the same marker when it spans across lines.
+ // The spans will have null for their from/to properties when the
+ // marker continues beyond the start/end of the line. Markers have
+ // links back to the lines they currently touch.
+
+ var nextMarkerId = 0;
+
+ var TextMarker = CodeMirror.TextMarker = function(doc, type) {
+ this.lines = [];
+ this.type = type;
+ this.doc = doc;
+ this.id = ++nextMarkerId;
+ };
+ eventMixin(TextMarker);
+
+ // Clear the marker.
+ TextMarker.prototype.clear = function() {
+ if (this.explicitlyCleared) return;
+ var cm = this.doc.cm, withOp = cm && !cm.curOp;
+ if (withOp) startOperation(cm);
+ if (hasHandler(this, "clear")) {
+ var found = this.find();
+ if (found) signalLater(this, "clear", found.from, found.to);
+ }
+ var min = null, max = null;
+ for (var i = 0; i < this.lines.length; ++i) {
+ var line = this.lines[i];
+ var span = getMarkedSpanFor(line.markedSpans, this);
+ if (cm && !this.collapsed) regLineChange(cm, lineNo(line), "text");
+ else if (cm) {
+ if (span.to != null) max = lineNo(line);
+ if (span.from != null) min = lineNo(line);
+ }
+ line.markedSpans = removeMarkedSpan(line.markedSpans, span);
+ if (span.from == null && this.collapsed && !lineIsHidden(this.doc, line) && cm)
+ updateLineHeight(line, textHeight(cm.display));
+ }
+ if (cm && this.collapsed && !cm.options.lineWrapping) for (var i = 0; i < this.lines.length; ++i) {
+ var visual = visualLine(this.lines[i]), len = lineLength(visual);
+ if (len > cm.display.maxLineLength) {
+ cm.display.maxLine = visual;
+ cm.display.maxLineLength = len;
+ cm.display.maxLineChanged = true;
+ }
+ }
+
+ if (min != null && cm && this.collapsed) regChange(cm, min, max + 1);
+ this.lines.length = 0;
+ this.explicitlyCleared = true;
+ if (this.atomic && this.doc.cantEdit) {
+ this.doc.cantEdit = false;
+ if (cm) reCheckSelection(cm.doc);
+ }
+ if (cm) signalLater(cm, "markerCleared", cm, this);
+ if (withOp) endOperation(cm);
+ if (this.parent) this.parent.clear();
+ };
+
+ // Find the position of the marker in the document. Returns a {from,
+ // to} object by default. Side can be passed to get a specific side
+ // -- 0 (both), -1 (left), or 1 (right). When lineObj is true, the
+ // Pos objects returned contain a line object, rather than a line
+ // number (used to prevent looking up the same line twice).
+ TextMarker.prototype.find = function(side, lineObj) {
+ if (side == null && this.type == "bookmark") side = 1;
+ var from, to;
+ for (var i = 0; i < this.lines.length; ++i) {
+ var line = this.lines[i];
+ var span = getMarkedSpanFor(line.markedSpans, this);
+ if (span.from != null) {
+ from = Pos(lineObj ? line : lineNo(line), span.from);
+ if (side == -1) return from;
+ }
+ if (span.to != null) {
+ to = Pos(lineObj ? line : lineNo(line), span.to);
+ if (side == 1) return to;
+ }
+ }
+ return from && {from: from, to: to};
+ };
+
+ // Signals that the marker's widget changed, and surrounding layout
+ // should be recomputed.
+ TextMarker.prototype.changed = function() {
+ var pos = this.find(-1, true), widget = this, cm = this.doc.cm;
+ if (!pos || !cm) return;
+ runInOp(cm, function() {
+ var line = pos.line, lineN = lineNo(pos.line);
+ var view = findViewForLine(cm, lineN);
+ if (view) {
+ clearLineMeasurementCacheFor(view);
+ cm.curOp.selectionChanged = cm.curOp.forceUpdate = true;
+ }
+ cm.curOp.updateMaxLine = true;
+ if (!lineIsHidden(widget.doc, line) && widget.height != null) {
+ var oldHeight = widget.height;
+ widget.height = null;
+ var dHeight = widgetHeight(widget) - oldHeight;
+ if (dHeight)
+ updateLineHeight(line, line.height + dHeight);
+ }
+ });
+ };
+
+ TextMarker.prototype.attachLine = function(line) {
+ if (!this.lines.length && this.doc.cm) {
+ var op = this.doc.cm.curOp;
+ if (!op.maybeHiddenMarkers || indexOf(op.maybeHiddenMarkers, this) == -1)
+ (op.maybeUnhiddenMarkers || (op.maybeUnhiddenMarkers = [])).push(this);
+ }
+ this.lines.push(line);
+ };
+ TextMarker.prototype.detachLine = function(line) {
+ this.lines.splice(indexOf(this.lines, line), 1);
+ if (!this.lines.length && this.doc.cm) {
+ var op = this.doc.cm.curOp;
+ (op.maybeHiddenMarkers || (op.maybeHiddenMarkers = [])).push(this);
+ }
+ };
+
+ // Collapsed markers have unique ids, in order to be able to order
+ // them, which is needed for uniquely determining an outer marker
+ // when they overlap (they may nest, but not partially overlap).
+ var nextMarkerId = 0;
+
+ // Create a marker, wire it up to the right lines, and
+ function markText(doc, from, to, options, type) {
+ // Shared markers (across linked documents) are handled separately
+ // (markTextShared will call out to this again, once per
+ // document).
+ if (options && options.shared) return markTextShared(doc, from, to, options, type);
+ // Ensure we are in an operation.
+ if (doc.cm && !doc.cm.curOp) return operation(doc.cm, markText)(doc, from, to, options, type);
+
+ var marker = new TextMarker(doc, type), diff = cmp(from, to);
+ if (options) copyObj(options, marker, false);
+ // Don't connect empty markers unless clearWhenEmpty is false
+ if (diff > 0 || diff == 0 && marker.clearWhenEmpty !== false)
+ return marker;
+ if (marker.replacedWith) {
+ // Showing up as a widget implies collapsed (widget replaces text)
+ marker.collapsed = true;
+ marker.widgetNode = elt("span", [marker.replacedWith], "CodeMirror-widget");
+ if (!options.handleMouseEvents) marker.widgetNode.setAttribute("cm-ignore-events", "true");
+ if (options.insertLeft) marker.widgetNode.insertLeft = true;
+ }
+ if (marker.collapsed) {
+ if (conflictingCollapsedRange(doc, from.line, from, to, marker) ||
+ from.line != to.line && conflictingCollapsedRange(doc, to.line, from, to, marker))
+ throw new Error("Inserting collapsed marker partially overlapping an existing one");
+ sawCollapsedSpans = true;
+ }
+
+ if (marker.addToHistory)
+ addChangeToHistory(doc, {from: from, to: to, origin: "markText"}, doc.sel, NaN);
+
+ var curLine = from.line, cm = doc.cm, updateMaxLine;
+ doc.iter(curLine, to.line + 1, function(line) {
+ if (cm && marker.collapsed && !cm.options.lineWrapping && visualLine(line) == cm.display.maxLine)
+ updateMaxLine = true;
+ if (marker.collapsed && curLine != from.line) updateLineHeight(line, 0);
+ addMarkedSpan(line, new MarkedSpan(marker,
+ curLine == from.line ? from.ch : null,
+ curLine == to.line ? to.ch : null));
+ ++curLine;
+ });
+ // lineIsHidden depends on the presence of the spans, so needs a second pass
+ if (marker.collapsed) doc.iter(from.line, to.line + 1, function(line) {
+ if (lineIsHidden(doc, line)) updateLineHeight(line, 0);
+ });
+
+ if (marker.clearOnEnter) on(marker, "beforeCursorEnter", function() { marker.clear(); });
+
+ if (marker.readOnly) {
+ sawReadOnlySpans = true;
+ if (doc.history.done.length || doc.history.undone.length)
+ doc.clearHistory();
+ }
+ if (marker.collapsed) {
+ marker.id = ++nextMarkerId;
+ marker.atomic = true;
+ }
+ if (cm) {
+ // Sync editor state
+ if (updateMaxLine) cm.curOp.updateMaxLine = true;
+ if (marker.collapsed)
+ regChange(cm, from.line, to.line + 1);
+ else if (marker.className || marker.title || marker.startStyle || marker.endStyle || marker.css)
+ for (var i = from.line; i <= to.line; i++) regLineChange(cm, i, "text");
+ if (marker.atomic) reCheckSelection(cm.doc);
+ signalLater(cm, "markerAdded", cm, marker);
+ }
+ return marker;
+ }
+
+ // SHARED TEXTMARKERS
+
+ // A shared marker spans multiple linked documents. It is
+ // implemented as a meta-marker-object controlling multiple normal
+ // markers.
+ var SharedTextMarker = CodeMirror.SharedTextMarker = function(markers, primary) {
+ this.markers = markers;
+ this.primary = primary;
+ for (var i = 0; i < markers.length; ++i)
+ markers[i].parent = this;
+ };
+ eventMixin(SharedTextMarker);
+
+ SharedTextMarker.prototype.clear = function() {
+ if (this.explicitlyCleared) return;
+ this.explicitlyCleared = true;
+ for (var i = 0; i < this.markers.length; ++i)
+ this.markers[i].clear();
+ signalLater(this, "clear");
+ };
+ SharedTextMarker.prototype.find = function(side, lineObj) {
+ return this.primary.find(side, lineObj);
+ };
+
+ function markTextShared(doc, from, to, options, type) {
+ options = copyObj(options);
+ options.shared = false;
+ var markers = [markText(doc, from, to, options, type)], primary = markers[0];
+ var widget = options.widgetNode;
+ linkedDocs(doc, function(doc) {
+ if (widget) options.widgetNode = widget.cloneNode(true);
+ markers.push(markText(doc, clipPos(doc, from), clipPos(doc, to), options, type));
+ for (var i = 0; i < doc.linked.length; ++i)
+ if (doc.linked[i].isParent) return;
+ primary = lst(markers);
+ });
+ return new SharedTextMarker(markers, primary);
+ }
+
+ function findSharedMarkers(doc) {
+ return doc.findMarks(Pos(doc.first, 0), doc.clipPos(Pos(doc.lastLine())),
+ function(m) { return m.parent; });
+ }
+
+ function copySharedMarkers(doc, markers) {
+ for (var i = 0; i < markers.length; i++) {
+ var marker = markers[i], pos = marker.find();
+ var mFrom = doc.clipPos(pos.from), mTo = doc.clipPos(pos.to);
+ if (cmp(mFrom, mTo)) {
+ var subMark = markText(doc, mFrom, mTo, marker.primary, marker.primary.type);
+ marker.markers.push(subMark);
+ subMark.parent = marker;
+ }
+ }
+ }
+
+ function detachSharedMarkers(markers) {
+ for (var i = 0; i < markers.length; i++) {
+ var marker = markers[i], linked = [marker.primary.doc];;
+ linkedDocs(marker.primary.doc, function(d) { linked.push(d); });
+ for (var j = 0; j < marker.markers.length; j++) {
+ var subMarker = marker.markers[j];
+ if (indexOf(linked, subMarker.doc) == -1) {
+ subMarker.parent = null;
+ marker.markers.splice(j--, 1);
+ }
+ }
+ }
+ }
+
+ // TEXTMARKER SPANS
+
+ function MarkedSpan(marker, from, to) {
+ this.marker = marker;
+ this.from = from; this.to = to;
+ }
+
+ // Search an array of spans for a span matching the given marker.
+ function getMarkedSpanFor(spans, marker) {
+ if (spans) for (var i = 0; i < spans.length; ++i) {
+ var span = spans[i];
+ if (span.marker == marker) return span;
+ }
+ }
+ // Remove a span from an array, returning undefined if no spans are
+ // left (we don't store arrays for lines without spans).
+ function removeMarkedSpan(spans, span) {
+ for (var r, i = 0; i < spans.length; ++i)
+ if (spans[i] != span) (r || (r = [])).push(spans[i]);
+ return r;
+ }
+ // Add a span to a line.
+ function addMarkedSpan(line, span) {
+ line.markedSpans = line.markedSpans ? line.markedSpans.concat([span]) : [span];
+ span.marker.attachLine(line);
+ }
+
+ // Used for the algorithm that adjusts markers for a change in the
+ // document. These functions cut an array of spans at a given
+ // character position, returning an array of remaining chunks (or
+ // undefined if nothing remains).
+ function markedSpansBefore(old, startCh, isInsert) {
+ if (old) for (var i = 0, nw; i < old.length; ++i) {
+ var span = old[i], marker = span.marker;
+ var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= startCh : span.from < startCh);
+ if (startsBefore || span.from == startCh && marker.type == "bookmark" && (!isInsert || !span.marker.insertLeft)) {
+ var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= startCh : span.to > startCh);
+ (nw || (nw = [])).push(new MarkedSpan(marker, span.from, endsAfter ? null : span.to));
+ }
+ }
+ return nw;
+ }
+ function markedSpansAfter(old, endCh, isInsert) {
+ if (old) for (var i = 0, nw; i < old.length; ++i) {
+ var span = old[i], marker = span.marker;
+ var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= endCh : span.to > endCh);
+ if (endsAfter || span.from == endCh && marker.type == "bookmark" && (!isInsert || span.marker.insertLeft)) {
+ var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= endCh : span.from < endCh);
+ (nw || (nw = [])).push(new MarkedSpan(marker, startsBefore ? null : span.from - endCh,
+ span.to == null ? null : span.to - endCh));
+ }
+ }
+ return nw;
+ }
+
+ // Given a change object, compute the new set of marker spans that
+ // cover the line in which the change took place. Removes spans
+ // entirely within the change, reconnects spans belonging to the
+ // same marker that appear on both sides of the change, and cuts off
+ // spans partially within the change. Returns an array of span
+ // arrays with one element for each line in (after) the change.
+ function stretchSpansOverChange(doc, change) {
+ if (change.full) return null;
+ var oldFirst = isLine(doc, change.from.line) && getLine(doc, change.from.line).markedSpans;
+ var oldLast = isLine(doc, change.to.line) && getLine(doc, change.to.line).markedSpans;
+ if (!oldFirst && !oldLast) return null;
+
+ var startCh = change.from.ch, endCh = change.to.ch, isInsert = cmp(change.from, change.to) == 0;
+ // Get the spans that 'stick out' on both sides
+ var first = markedSpansBefore(oldFirst, startCh, isInsert);
+ var last = markedSpansAfter(oldLast, endCh, isInsert);
+
+ // Next, merge those two ends
+ var sameLine = change.text.length == 1, offset = lst(change.text).length + (sameLine ? startCh : 0);
+ if (first) {
+ // Fix up .to properties of first
+ for (var i = 0; i < first.length; ++i) {
+ var span = first[i];
+ if (span.to == null) {
+ var found = getMarkedSpanFor(last, span.marker);
+ if (!found) span.to = startCh;
+ else if (sameLine) span.to = found.to == null ? null : found.to + offset;
+ }
+ }
+ }
+ if (last) {
+ // Fix up .from in last (or move them into first in case of sameLine)
+ for (var i = 0; i < last.length; ++i) {
+ var span = last[i];
+ if (span.to != null) span.to += offset;
+ if (span.from == null) {
+ var found = getMarkedSpanFor(first, span.marker);
+ if (!found) {
+ span.from = offset;
+ if (sameLine) (first || (first = [])).push(span);
+ }
+ } else {
+ span.from += offset;
+ if (sameLine) (first || (first = [])).push(span);
+ }
+ }
+ }
+ // Make sure we didn't create any zero-length spans
+ if (first) first = clearEmptySpans(first);
+ if (last && last != first) last = clearEmptySpans(last);
+
+ var newMarkers = [first];
+ if (!sameLine) {
+ // Fill gap with whole-line-spans
+ var gap = change.text.length - 2, gapMarkers;
+ if (gap > 0 && first)
+ for (var i = 0; i < first.length; ++i)
+ if (first[i].to == null)
+ (gapMarkers || (gapMarkers = [])).push(new MarkedSpan(first[i].marker, null, null));
+ for (var i = 0; i < gap; ++i)
+ newMarkers.push(gapMarkers);
+ newMarkers.push(last);
+ }
+ return newMarkers;
+ }
+
+ // Remove spans that are empty and don't have a clearWhenEmpty
+ // option of false.
+ function clearEmptySpans(spans) {
+ for (var i = 0; i < spans.length; ++i) {
+ var span = spans[i];
+ if (span.from != null && span.from == span.to && span.marker.clearWhenEmpty !== false)
+ spans.splice(i--, 1);
+ }
+ if (!spans.length) return null;
+ return spans;
+ }
+
+ // Used for un/re-doing changes from the history. Combines the
+ // result of computing the existing spans with the set of spans that
+ // existed in the history (so that deleting around a span and then
+ // undoing brings back the span).
+ function mergeOldSpans(doc, change) {
+ var old = getOldSpans(doc, change);
+ var stretched = stretchSpansOverChange(doc, change);
+ if (!old) return stretched;
+ if (!stretched) return old;
+
+ for (var i = 0; i < old.length; ++i) {
+ var oldCur = old[i], stretchCur = stretched[i];
+ if (oldCur && stretchCur) {
+ spans: for (var j = 0; j < stretchCur.length; ++j) {
+ var span = stretchCur[j];
+ for (var k = 0; k < oldCur.length; ++k)
+ if (oldCur[k].marker == span.marker) continue spans;
+ oldCur.push(span);
+ }
+ } else if (stretchCur) {
+ old[i] = stretchCur;
+ }
+ }
+ return old;
+ }
+
+ // Used to 'clip' out readOnly ranges when making a change.
+ function removeReadOnlyRanges(doc, from, to) {
+ var markers = null;
+ doc.iter(from.line, to.line + 1, function(line) {
+ if (line.markedSpans) for (var i = 0; i < line.markedSpans.length; ++i) {
+ var mark = line.markedSpans[i].marker;
+ if (mark.readOnly && (!markers || indexOf(markers, mark) == -1))
+ (markers || (markers = [])).push(mark);
+ }
+ });
+ if (!markers) return null;
+ var parts = [{from: from, to: to}];
+ for (var i = 0; i < markers.length; ++i) {
+ var mk = markers[i], m = mk.find(0);
+ for (var j = 0; j < parts.length; ++j) {
+ var p = parts[j];
+ if (cmp(p.to, m.from) < 0 || cmp(p.from, m.to) > 0) continue;
+ var newParts = [j, 1], dfrom = cmp(p.from, m.from), dto = cmp(p.to, m.to);
+ if (dfrom < 0 || !mk.inclusiveLeft && !dfrom)
+ newParts.push({from: p.from, to: m.from});
+ if (dto > 0 || !mk.inclusiveRight && !dto)
+ newParts.push({from: m.to, to: p.to});
+ parts.splice.apply(parts, newParts);
+ j += newParts.length - 1;
+ }
+ }
+ return parts;
+ }
+
+ // Connect or disconnect spans from a line.
+ function detachMarkedSpans(line) {
+ var spans = line.markedSpans;
+ if (!spans) return;
+ for (var i = 0; i < spans.length; ++i)
+ spans[i].marker.detachLine(line);
+ line.markedSpans = null;
+ }
+ function attachMarkedSpans(line, spans) {
+ if (!spans) return;
+ for (var i = 0; i < spans.length; ++i)
+ spans[i].marker.attachLine(line);
+ line.markedSpans = spans;
+ }
+
+ // Helpers used when computing which overlapping collapsed span
+ // counts as the larger one.
+ function extraLeft(marker) { return marker.inclusiveLeft ? -1 : 0; }
+ function extraRight(marker) { return marker.inclusiveRight ? 1 : 0; }
+
+ // Returns a number indicating which of two overlapping collapsed
+ // spans is larger (and thus includes the other). Falls back to
+ // comparing ids when the spans cover exactly the same range.
+ function compareCollapsedMarkers(a, b) {
+ var lenDiff = a.lines.length - b.lines.length;
+ if (lenDiff != 0) return lenDiff;
+ var aPos = a.find(), bPos = b.find();
+ var fromCmp = cmp(aPos.from, bPos.from) || extraLeft(a) - extraLeft(b);
+ if (fromCmp) return -fromCmp;
+ var toCmp = cmp(aPos.to, bPos.to) || extraRight(a) - extraRight(b);
+ if (toCmp) return toCmp;
+ return b.id - a.id;
+ }
+
+ // Find out whether a line ends or starts in a collapsed span. If
+ // so, return the marker for that span.
+ function collapsedSpanAtSide(line, start) {
+ var sps = sawCollapsedSpans && line.markedSpans, found;
+ if (sps) for (var sp, i = 0; i < sps.length; ++i) {
+ sp = sps[i];
+ if (sp.marker.collapsed && (start ? sp.from : sp.to) == null &&
+ (!found || compareCollapsedMarkers(found, sp.marker) < 0))
+ found = sp.marker;
+ }
+ return found;
+ }
+ function collapsedSpanAtStart(line) { return collapsedSpanAtSide(line, true); }
+ function collapsedSpanAtEnd(line) { return collapsedSpanAtSide(line, false); }
+
+ // Test whether there exists a collapsed span that partially
+ // overlaps (covers the start or end, but not both) of a new span.
+ // Such overlap is not allowed.
+ function conflictingCollapsedRange(doc, lineNo, from, to, marker) {
+ var line = getLine(doc, lineNo);
+ var sps = sawCollapsedSpans && line.markedSpans;
+ if (sps) for (var i = 0; i < sps.length; ++i) {
+ var sp = sps[i];
+ if (!sp.marker.collapsed) continue;
+ var found = sp.marker.find(0);
+ var fromCmp = cmp(found.from, from) || extraLeft(sp.marker) - extraLeft(marker);
+ var toCmp = cmp(found.to, to) || extraRight(sp.marker) - extraRight(marker);
+ if (fromCmp >= 0 && toCmp <= 0 || fromCmp <= 0 && toCmp >= 0) continue;
+ if (fromCmp <= 0 && (sp.marker.inclusiveRight && marker.inclusiveLeft ? cmp(found.to, from) >= 0 : cmp(found.to, from) > 0) ||
+ fromCmp >= 0 && (sp.marker.inclusiveRight && marker.inclusiveLeft ? cmp(found.from, to) <= 0 : cmp(found.from, to) < 0))
+ return true;
+ }
+ }
+
+ // A visual line is a line as drawn on the screen. Folding, for
+ // example, can cause multiple logical lines to appear on the same
+ // visual line. This finds the start of the visual line that the
+ // given line is part of (usually that is the line itself).
+ function visualLine(line) {
+ var merged;
+ while (merged = collapsedSpanAtStart(line))
+ line = merged.find(-1, true).line;
+ return line;
+ }
+
+ // Returns an array of logical lines that continue the visual line
+ // started by the argument, or undefined if there are no such lines.
+ function visualLineContinued(line) {
+ var merged, lines;
+ while (merged = collapsedSpanAtEnd(line)) {
+ line = merged.find(1, true).line;
+ (lines || (lines = [])).push(line);
+ }
+ return lines;
+ }
+
+ // Get the line number of the start of the visual line that the
+ // given line number is part of.
+ function visualLineNo(doc, lineN) {
+ var line = getLine(doc, lineN), vis = visualLine(line);
+ if (line == vis) return lineN;
+ return lineNo(vis);
+ }
+ // Get the line number of the start of the next visual line after
+ // the given line.
+ function visualLineEndNo(doc, lineN) {
+ if (lineN > doc.lastLine()) return lineN;
+ var line = getLine(doc, lineN), merged;
+ if (!lineIsHidden(doc, line)) return lineN;
+ while (merged = collapsedSpanAtEnd(line))
+ line = merged.find(1, true).line;
+ return lineNo(line) + 1;
+ }
+
+ // Compute whether a line is hidden. Lines count as hidden when they
+ // are part of a visual line that starts with another line, or when
+ // they are entirely covered by collapsed, non-widget span.
+ function lineIsHidden(doc, line) {
+ var sps = sawCollapsedSpans && line.markedSpans;
+ if (sps) for (var sp, i = 0; i < sps.length; ++i) {
+ sp = sps[i];
+ if (!sp.marker.collapsed) continue;
+ if (sp.from == null) return true;
+ if (sp.marker.widgetNode) continue;
+ if (sp.from == 0 && sp.marker.inclusiveLeft && lineIsHiddenInner(doc, line, sp))
+ return true;
+ }
+ }
+ function lineIsHiddenInner(doc, line, span) {
+ if (span.to == null) {
+ var end = span.marker.find(1, true);
+ return lineIsHiddenInner(doc, end.line, getMarkedSpanFor(end.line.markedSpans, span.marker));
+ }
+ if (span.marker.inclusiveRight && span.to == line.text.length)
+ return true;
+ for (var sp, i = 0; i < line.markedSpans.length; ++i) {
+ sp = line.markedSpans[i];
+ if (sp.marker.collapsed && !sp.marker.widgetNode && sp.from == span.to &&
+ (sp.to == null || sp.to != span.from) &&
+ (sp.marker.inclusiveLeft || span.marker.inclusiveRight) &&
+ lineIsHiddenInner(doc, line, sp)) return true;
+ }
+ }
+
+ // LINE WIDGETS
+
+ // Line widgets are block elements displayed above or below a line.
+
+ var LineWidget = CodeMirror.LineWidget = function(doc, node, options) {
+ if (options) for (var opt in options) if (options.hasOwnProperty(opt))
+ this[opt] = options[opt];
+ this.doc = doc;
+ this.node = node;
+ };
+ eventMixin(LineWidget);
+
+ function adjustScrollWhenAboveVisible(cm, line, diff) {
+ if (heightAtLine(line) < ((cm.curOp && cm.curOp.scrollTop) || cm.doc.scrollTop))
+ addToScrollPos(cm, null, diff);
+ }
+
+ LineWidget.prototype.clear = function() {
+ var cm = this.doc.cm, ws = this.line.widgets, line = this.line, no = lineNo(line);
+ if (no == null || !ws) return;
+ for (var i = 0; i < ws.length; ++i) if (ws[i] == this) ws.splice(i--, 1);
+ if (!ws.length) line.widgets = null;
+ var height = widgetHeight(this);
+ updateLineHeight(line, Math.max(0, line.height - height));
+ if (cm) runInOp(cm, function() {
+ adjustScrollWhenAboveVisible(cm, line, -height);
+ regLineChange(cm, no, "widget");
+ });
+ };
+ LineWidget.prototype.changed = function() {
+ var oldH = this.height, cm = this.doc.cm, line = this.line;
+ this.height = null;
+ var diff = widgetHeight(this) - oldH;
+ if (!diff) return;
+ updateLineHeight(line, line.height + diff);
+ if (cm) runInOp(cm, function() {
+ cm.curOp.forceUpdate = true;
+ adjustScrollWhenAboveVisible(cm, line, diff);
+ });
+ };
+
+ function widgetHeight(widget) {
+ if (widget.height != null) return widget.height;
+ var cm = widget.doc.cm;
+ if (!cm) return 0;
+ if (!contains(document.body, widget.node)) {
+ var parentStyle = "position: relative;";
+ if (widget.coverGutter)
+ parentStyle += "margin-left: -" + cm.display.gutters.offsetWidth + "px;";
+ if (widget.noHScroll)
+ parentStyle += "width: " + cm.display.wrapper.clientWidth + "px;";
+ removeChildrenAndAdd(cm.display.measure, elt("div", [widget.node], null, parentStyle));
+ }
+ return widget.height = widget.node.parentNode.offsetHeight;
+ }
+
+ function addLineWidget(doc, handle, node, options) {
+ var widget = new LineWidget(doc, node, options);
+ var cm = doc.cm;
+ if (cm && widget.noHScroll) cm.display.alignWidgets = true;
+ changeLine(doc, handle, "widget", function(line) {
+ var widgets = line.widgets || (line.widgets = []);
+ if (widget.insertAt == null) widgets.push(widget);
+ else widgets.splice(Math.min(widgets.length - 1, Math.max(0, widget.insertAt)), 0, widget);
+ widget.line = line;
+ if (cm && !lineIsHidden(doc, line)) {
+ var aboveVisible = heightAtLine(line) < doc.scrollTop;
+ updateLineHeight(line, line.height + widgetHeight(widget));
+ if (aboveVisible) addToScrollPos(cm, null, widget.height);
+ cm.curOp.forceUpdate = true;
+ }
+ return true;
+ });
+ return widget;
+ }
+
+ // LINE DATA STRUCTURE
+
+ // Line objects. These hold state related to a line, including
+ // highlighting info (the styles array).
+ var Line = CodeMirror.Line = function(text, markedSpans, estimateHeight) {
+ this.text = text;
+ attachMarkedSpans(this, markedSpans);
+ this.height = estimateHeight ? estimateHeight(this) : 1;
+ };
+ eventMixin(Line);
+ Line.prototype.lineNo = function() { return lineNo(this); };
+
+ // Change the content (text, markers) of a line. Automatically
+ // invalidates cached information and tries to re-estimate the
+ // line's height.
+ function updateLine(line, text, markedSpans, estimateHeight) {
+ line.text = text;
+ if (line.stateAfter) line.stateAfter = null;
+ if (line.styles) line.styles = null;
+ if (line.order != null) line.order = null;
+ detachMarkedSpans(line);
+ attachMarkedSpans(line, markedSpans);
+ var estHeight = estimateHeight ? estimateHeight(line) : 1;
+ if (estHeight != line.height) updateLineHeight(line, estHeight);
+ }
+
+ // Detach a line from the document tree and its markers.
+ function cleanUpLine(line) {
+ line.parent = null;
+ detachMarkedSpans(line);
+ }
+
+ function extractLineClasses(type, output) {
+ if (type) for (;;) {
+ var lineClass = type.match(/(?:^|\s+)line-(background-)?(\S+)/);
+ if (!lineClass) break;
+ type = type.slice(0, lineClass.index) + type.slice(lineClass.index + lineClass[0].length);
+ var prop = lineClass[1] ? "bgClass" : "textClass";
+ if (output[prop] == null)
+ output[prop] = lineClass[2];
+ else if (!(new RegExp("(?:^|\s)" + lineClass[2] + "(?:$|\s)")).test(output[prop]))
+ output[prop] += " " + lineClass[2];
+ }
+ return type;
+ }
+
+ function callBlankLine(mode, state) {
+ if (mode.blankLine) return mode.blankLine(state);
+ if (!mode.innerMode) return;
+ var inner = CodeMirror.innerMode(mode, state);
+ if (inner.mode.blankLine) return inner.mode.blankLine(inner.state);
+ }
+
+ function readToken(mode, stream, state, inner) {
+ for (var i = 0; i < 10; i++) {
+ if (inner) inner[0] = CodeMirror.innerMode(mode, state).mode;
+ var style = mode.token(stream, state);
+ if (stream.pos > stream.start) return style;
+ }
+ throw new Error("Mode " + mode.name + " failed to advance stream.");
+ }
+
+ // Utility for getTokenAt and getLineTokens
+ function takeToken(cm, pos, precise, asArray) {
+ function getObj(copy) {
+ return {start: stream.start, end: stream.pos,
+ string: stream.current(),
+ type: style || null,
+ state: copy ? copyState(doc.mode, state) : state};
+ }
+
+ var doc = cm.doc, mode = doc.mode, style;
+ pos = clipPos(doc, pos);
+ var line = getLine(doc, pos.line), state = getStateBefore(cm, pos.line, precise);
+ var stream = new StringStream(line.text, cm.options.tabSize), tokens;
+ if (asArray) tokens = [];
+ while ((asArray || stream.pos < pos.ch) && !stream.eol()) {
+ stream.start = stream.pos;
+ style = readToken(mode, stream, state);
+ if (asArray) tokens.push(getObj(true));
+ }
+ return asArray ? tokens : getObj();
+ }
+
+ // Run the given mode's parser over a line, calling f for each token.
+ function runMode(cm, text, mode, state, f, lineClasses, forceToEnd) {
+ var flattenSpans = mode.flattenSpans;
+ if (flattenSpans == null) flattenSpans = cm.options.flattenSpans;
+ var curStart = 0, curStyle = null;
+ var stream = new StringStream(text, cm.options.tabSize), style;
+ var inner = cm.options.addModeClass && [null];
+ if (text == "") extractLineClasses(callBlankLine(mode, state), lineClasses);
+ while (!stream.eol()) {
+ if (stream.pos > cm.options.maxHighlightLength) {
+ flattenSpans = false;
+ if (forceToEnd) processLine(cm, text, state, stream.pos);
+ stream.pos = text.length;
+ style = null;
+ } else {
+ style = extractLineClasses(readToken(mode, stream, state, inner), lineClasses);
+ }
+ if (inner) {
+ var mName = inner[0].name;
+ if (mName) style = "m-" + (style ? mName + " " + style : mName);
+ }
+ if (!flattenSpans || curStyle != style) {
+ while (curStart < stream.start) {
+ curStart = Math.min(stream.start, curStart + 50000);
+ f(curStart, curStyle);
+ }
+ curStyle = style;
+ }
+ stream.start = stream.pos;
+ }
+ while (curStart < stream.pos) {
+ // Webkit seems to refuse to render text nodes longer than 57444 characters
+ var pos = Math.min(stream.pos, curStart + 50000);
+ f(pos, curStyle);
+ curStart = pos;
+ }
+ }
+
+ // Compute a style array (an array starting with a mode generation
+ // -- for invalidation -- followed by pairs of end positions and
+ // style strings), which is used to highlight the tokens on the
+ // line.
+ function highlightLine(cm, line, state, forceToEnd) {
+ // A styles array always starts with a number identifying the
+ // mode/overlays that it is based on (for easy invalidation).
+ var st = [cm.state.modeGen], lineClasses = {};
+ // Compute the base array of styles
+ runMode(cm, line.text, cm.doc.mode, state, function(end, style) {
+ st.push(end, style);
+ }, lineClasses, forceToEnd);
+
+ // Run overlays, adjust style array.
+ for (var o = 0; o < cm.state.overlays.length; ++o) {
+ var overlay = cm.state.overlays[o], i = 1, at = 0;
+ runMode(cm, line.text, overlay.mode, true, function(end, style) {
+ var start = i;
+ // Ensure there's a token end at the current position, and that i points at it
+ while (at < end) {
+ var i_end = st[i];
+ if (i_end > end)
+ st.splice(i, 1, end, st[i+1], i_end);
+ i += 2;
+ at = Math.min(end, i_end);
+ }
+ if (!style) return;
+ if (overlay.opaque) {
+ st.splice(start, i - start, end, "cm-overlay " + style);
+ i = start + 2;
+ } else {
+ for (; start < i; start += 2) {
+ var cur = st[start+1];
+ st[start+1] = (cur ? cur + " " : "") + "cm-overlay " + style;
+ }
+ }
+ }, lineClasses);
+ }
+
+ return {styles: st, classes: lineClasses.bgClass || lineClasses.textClass ? lineClasses : null};
+ }
+
+ function getLineStyles(cm, line, updateFrontier) {
+ if (!line.styles || line.styles[0] != cm.state.modeGen) {
+ var state = getStateBefore(cm, lineNo(line));
+ var result = highlightLine(cm, line, line.text.length > cm.options.maxHighlightLength ? copyState(cm.doc.mode, state) : state);
+ line.stateAfter = state;
+ line.styles = result.styles;
+ if (result.classes) line.styleClasses = result.classes;
+ else if (line.styleClasses) line.styleClasses = null;
+ if (updateFrontier === cm.doc.frontier) cm.doc.frontier++;
+ }
+ return line.styles;
+ }
+
+ // Lightweight form of highlight -- proceed over this line and
+ // update state, but don't save a style array. Used for lines that
+ // aren't currently visible.
+ function processLine(cm, text, state, startAt) {
+ var mode = cm.doc.mode;
+ var stream = new StringStream(text, cm.options.tabSize);
+ stream.start = stream.pos = startAt || 0;
+ if (text == "") callBlankLine(mode, state);
+ while (!stream.eol()) {
+ readToken(mode, stream, state);
+ stream.start = stream.pos;
+ }
+ }
+
+ // Convert a style as returned by a mode (either null, or a string
+ // containing one or more styles) to a CSS style. This is cached,
+ // and also looks for line-wide styles.
+ var styleToClassCache = {}, styleToClassCacheWithMode = {};
+ function interpretTokenStyle(style, options) {
+ if (!style || /^\s*$/.test(style)) return null;
+ var cache = options.addModeClass ? styleToClassCacheWithMode : styleToClassCache;
+ return cache[style] ||
+ (cache[style] = style.replace(/\S+/g, "cm-$&"));
+ }
+
+ // Render the DOM representation of the text of a line. Also builds
+ // up a 'line map', which points at the DOM nodes that represent
+ // specific stretches of text, and is used by the measuring code.
+ // The returned object contains the DOM node, this map, and
+ // information about line-wide styles that were set by the mode.
+ function buildLineContent(cm, lineView) {
+ // The padding-right forces the element to have a 'border', which
+ // is needed on Webkit to be able to get line-level bounding
+ // rectangles for it (in measureChar).
+ var content = elt("span", null, null, webkit ? "padding-right: .1px" : null);
+ var builder = {pre: elt("pre", [content], "CodeMirror-line"), content: content,
+ col: 0, pos: 0, cm: cm,
+ splitSpaces: (ie || webkit) && cm.getOption("lineWrapping")};
+ lineView.measure = {};
+
+ // Iterate over the logical lines that make up this visual line.
+ for (var i = 0; i <= (lineView.rest ? lineView.rest.length : 0); i++) {
+ var line = i ? lineView.rest[i - 1] : lineView.line, order;
+ builder.pos = 0;
+ builder.addToken = buildToken;
+ // Optionally wire in some hacks into the token-rendering
+ // algorithm, to deal with browser quirks.
+ if (hasBadBidiRects(cm.display.measure) && (order = getOrder(line)))
+ builder.addToken = buildTokenBadBidi(builder.addToken, order);
+ builder.map = [];
+ var allowFrontierUpdate = lineView != cm.display.externalMeasured && lineNo(line);
+ insertLineContent(line, builder, getLineStyles(cm, line, allowFrontierUpdate));
+ if (line.styleClasses) {
+ if (line.styleClasses.bgClass)
+ builder.bgClass = joinClasses(line.styleClasses.bgClass, builder.bgClass || "");
+ if (line.styleClasses.textClass)
+ builder.textClass = joinClasses(line.styleClasses.textClass, builder.textClass || "");
+ }
+
+ // Ensure at least a single node is present, for measuring.
+ if (builder.map.length == 0)
+ builder.map.push(0, 0, builder.content.appendChild(zeroWidthElement(cm.display.measure)));
+
+ // Store the map and a cache object for the current logical line
+ if (i == 0) {
+ lineView.measure.map = builder.map;
+ lineView.measure.cache = {};
+ } else {
+ (lineView.measure.maps || (lineView.measure.maps = [])).push(builder.map);
+ (lineView.measure.caches || (lineView.measure.caches = [])).push({});
+ }
+ }
+
+ // See issue #2901
+ if (webkit) {
+ var last = builder.content.lastChild
+ if (/\bcm-tab\b/.test(last.className) || (last.querySelector && last.querySelector(".cm-tab")))
+ builder.content.className = "cm-tab-wrap-hack";
+ }
+
+ signal(cm, "renderLine", cm, lineView.line, builder.pre);
+ if (builder.pre.className)
+ builder.textClass = joinClasses(builder.pre.className, builder.textClass || "");
+
+ return builder;
+ }
+
+ function defaultSpecialCharPlaceholder(ch) {
+ var token = elt("span", "\u2022", "cm-invalidchar");
+ token.title = "\\u" + ch.charCodeAt(0).toString(16);
+ token.setAttribute("aria-label", token.title);
+ return token;
+ }
+
+ // Build up the DOM representation for a single token, and add it to
+ // the line map. Takes care to render special characters separately.
+ function buildToken(builder, text, style, startStyle, endStyle, title, css) {
+ if (!text) return;
+ var displayText = builder.splitSpaces ? text.replace(/ {3,}/g, splitSpaces) : text;
+ var special = builder.cm.state.specialChars, mustWrap = false;
+ if (!special.test(text)) {
+ builder.col += text.length;
+ var content = document.createTextNode(displayText);
+ builder.map.push(builder.pos, builder.pos + text.length, content);
+ if (ie && ie_version < 9) mustWrap = true;
+ builder.pos += text.length;
+ } else {
+ var content = document.createDocumentFragment(), pos = 0;
+ while (true) {
+ special.lastIndex = pos;
+ var m = special.exec(text);
+ var skipped = m ? m.index - pos : text.length - pos;
+ if (skipped) {
+ var txt = document.createTextNode(displayText.slice(pos, pos + skipped));
+ if (ie && ie_version < 9) content.appendChild(elt("span", [txt]));
+ else content.appendChild(txt);
+ builder.map.push(builder.pos, builder.pos + skipped, txt);
+ builder.col += skipped;
+ builder.pos += skipped;
+ }
+ if (!m) break;
+ pos += skipped + 1;
+ if (m[0] == "\t") {
+ var tabSize = builder.cm.options.tabSize, tabWidth = tabSize - builder.col % tabSize;
+ var txt = content.appendChild(elt("span", spaceStr(tabWidth), "cm-tab"));
+ txt.setAttribute("role", "presentation");
+ txt.setAttribute("cm-text", "\t");
+ builder.col += tabWidth;
+ } else if (m[0] == "\r" || m[0] == "\n") {
+ var txt = content.appendChild(elt("span", m[0] == "\r" ? "\u240d" : "\u2424", "cm-invalidchar"));
+ txt.setAttribute("cm-text", m[0]);
+ builder.col += 1;
+ } else {
+ var txt = builder.cm.options.specialCharPlaceholder(m[0]);
+ txt.setAttribute("cm-text", m[0]);
+ if (ie && ie_version < 9) content.appendChild(elt("span", [txt]));
+ else content.appendChild(txt);
+ builder.col += 1;
+ }
+ builder.map.push(builder.pos, builder.pos + 1, txt);
+ builder.pos++;
+ }
+ }
+ if (style || startStyle || endStyle || mustWrap || css) {
+ var fullStyle = style || "";
+ if (startStyle) fullStyle += startStyle;
+ if (endStyle) fullStyle += endStyle;
+ var token = elt("span", [content], fullStyle, css);
+ if (title) token.title = title;
+ return builder.content.appendChild(token);
+ }
+ builder.content.appendChild(content);
+ }
+
+ function splitSpaces(old) {
+ var out = " ";
+ for (var i = 0; i < old.length - 2; ++i) out += i % 2 ? " " : "\u00a0";
+ out += " ";
+ return out;
+ }
+
+ // Work around nonsense dimensions being reported for stretches of
+ // right-to-left text.
+ function buildTokenBadBidi(inner, order) {
+ return function(builder, text, style, startStyle, endStyle, title, css) {
+ style = style ? style + " cm-force-border" : "cm-force-border";
+ var start = builder.pos, end = start + text.length;
+ for (;;) {
+ // Find the part that overlaps with the start of this text
+ for (var i = 0; i < order.length; i++) {
+ var part = order[i];
+ if (part.to > start && part.from <= start) break;
+ }
+ if (part.to >= end) return inner(builder, text, style, startStyle, endStyle, title, css);
+ inner(builder, text.slice(0, part.to - start), style, startStyle, null, title, css);
+ startStyle = null;
+ text = text.slice(part.to - start);
+ start = part.to;
+ }
+ };
+ }
+
+ function buildCollapsedSpan(builder, size, marker, ignoreWidget) {
+ var widget = !ignoreWidget && marker.widgetNode;
+ if (widget) builder.map.push(builder.pos, builder.pos + size, widget);
+ if (!ignoreWidget && builder.cm.display.input.needsContentAttribute) {
+ if (!widget)
+ widget = builder.content.appendChild(document.createElement("span"));
+ widget.setAttribute("cm-marker", marker.id);
+ }
+ if (widget) {
+ builder.cm.display.input.setUneditable(widget);
+ builder.content.appendChild(widget);
+ }
+ builder.pos += size;
+ }
+
+ // Outputs a number of spans to make up a line, taking highlighting
+ // and marked text into account.
+ function insertLineContent(line, builder, styles) {
+ var spans = line.markedSpans, allText = line.text, at = 0;
+ if (!spans) {
+ for (var i = 1; i < styles.length; i+=2)
+ builder.addToken(builder, allText.slice(at, at = styles[i]), interpretTokenStyle(styles[i+1], builder.cm.options));
+ return;
+ }
+
+ var len = allText.length, pos = 0, i = 1, text = "", style, css;
+ var nextChange = 0, spanStyle, spanEndStyle, spanStartStyle, title, collapsed;
+ for (;;) {
+ if (nextChange == pos) { // Update current marker set
+ spanStyle = spanEndStyle = spanStartStyle = title = css = "";
+ collapsed = null; nextChange = Infinity;
+ var foundBookmarks = [], endStyles
+ for (var j = 0; j < spans.length; ++j) {
+ var sp = spans[j], m = sp.marker;
+ if (m.type == "bookmark" && sp.from == pos && m.widgetNode) {
+ foundBookmarks.push(m);
+ } else if (sp.from <= pos && (sp.to == null || sp.to > pos || m.collapsed && sp.to == pos && sp.from == pos)) {
+ if (sp.to != null && sp.to != pos && nextChange > sp.to) {
+ nextChange = sp.to;
+ spanEndStyle = "";
+ }
+ if (m.className) spanStyle += " " + m.className;
+ if (m.css) css = (css ? css + ";" : "") + m.css;
+ if (m.startStyle && sp.from == pos) spanStartStyle += " " + m.startStyle;
+ if (m.endStyle && sp.to == nextChange) (endStyles || (endStyles = [])).push(m.endStyle, sp.to)
+ if (m.title && !title) title = m.title;
+ if (m.collapsed && (!collapsed || compareCollapsedMarkers(collapsed.marker, m) < 0))
+ collapsed = sp;
+ } else if (sp.from > pos && nextChange > sp.from) {
+ nextChange = sp.from;
+ }
+ }
+ if (endStyles) for (var j = 0; j < endStyles.length; j += 2)
+ if (endStyles[j + 1] == nextChange) spanEndStyle += " " + endStyles[j]
+
+ if (!collapsed || collapsed.from == pos) for (var j = 0; j < foundBookmarks.length; ++j)
+ buildCollapsedSpan(builder, 0, foundBookmarks[j]);
+ if (collapsed && (collapsed.from || 0) == pos) {
+ buildCollapsedSpan(builder, (collapsed.to == null ? len + 1 : collapsed.to) - pos,
+ collapsed.marker, collapsed.from == null);
+ if (collapsed.to == null) return;
+ if (collapsed.to == pos) collapsed = false;
+ }
+ }
+ if (pos >= len) break;
+
+ var upto = Math.min(len, nextChange);
+ while (true) {
+ if (text) {
+ var end = pos + text.length;
+ if (!collapsed) {
+ var tokenText = end > upto ? text.slice(0, upto - pos) : text;
+ builder.addToken(builder, tokenText, style ? style + spanStyle : spanStyle,
+ spanStartStyle, pos + tokenText.length == nextChange ? spanEndStyle : "", title, css);
+ }
+ if (end >= upto) {text = text.slice(upto - pos); pos = upto; break;}
+ pos = end;
+ spanStartStyle = "";
+ }
+ text = allText.slice(at, at = styles[i++]);
+ style = interpretTokenStyle(styles[i++], builder.cm.options);
+ }
+ }
+ }
+
+ // DOCUMENT DATA STRUCTURE
+
+ // By default, updates that start and end at the beginning of a line
+ // are treated specially, in order to make the association of line
+ // widgets and marker elements with the text behave more intuitive.
+ function isWholeLineUpdate(doc, change) {
+ return change.from.ch == 0 && change.to.ch == 0 && lst(change.text) == "" &&
+ (!doc.cm || doc.cm.options.wholeLineUpdateBefore);
+ }
+
+ // Perform a change on the document data structure.
+ function updateDoc(doc, change, markedSpans, estimateHeight) {
+ function spansFor(n) {return markedSpans ? markedSpans[n] : null;}
+ function update(line, text, spans) {
+ updateLine(line, text, spans, estimateHeight);
+ signalLater(line, "change", line, change);
+ }
+ function linesFor(start, end) {
+ for (var i = start, result = []; i < end; ++i)
+ result.push(new Line(text[i], spansFor(i), estimateHeight));
+ return result;
+ }
+
+ var from = change.from, to = change.to, text = change.text;
+ var firstLine = getLine(doc, from.line), lastLine = getLine(doc, to.line);
+ var lastText = lst(text), lastSpans = spansFor(text.length - 1), nlines = to.line - from.line;
+
+ // Adjust the line structure
+ if (change.full) {
+ doc.insert(0, linesFor(0, text.length));
+ doc.remove(text.length, doc.size - text.length);
+ } else if (isWholeLineUpdate(doc, change)) {
+ // This is a whole-line replace. Treated specially to make
+ // sure line objects move the way they are supposed to.
+ var added = linesFor(0, text.length - 1);
+ update(lastLine, lastLine.text, lastSpans);
+ if (nlines) doc.remove(from.line, nlines);
+ if (added.length) doc.insert(from.line, added);
+ } else if (firstLine == lastLine) {
+ if (text.length == 1) {
+ update(firstLine, firstLine.text.slice(0, from.ch) + lastText + firstLine.text.slice(to.ch), lastSpans);
+ } else {
+ var added = linesFor(1, text.length - 1);
+ added.push(new Line(lastText + firstLine.text.slice(to.ch), lastSpans, estimateHeight));
+ update(firstLine, firstLine.text.slice(0, from.ch) + text[0], spansFor(0));
+ doc.insert(from.line + 1, added);
+ }
+ } else if (text.length == 1) {
+ update(firstLine, firstLine.text.slice(0, from.ch) + text[0] + lastLine.text.slice(to.ch), spansFor(0));
+ doc.remove(from.line + 1, nlines);
+ } else {
+ update(firstLine, firstLine.text.slice(0, from.ch) + text[0], spansFor(0));
+ update(lastLine, lastText + lastLine.text.slice(to.ch), lastSpans);
+ var added = linesFor(1, text.length - 1);
+ if (nlines > 1) doc.remove(from.line + 1, nlines - 1);
+ doc.insert(from.line + 1, added);
+ }
+
+ signalLater(doc, "change", doc, change);
+ }
+
+ // The document is represented as a BTree consisting of leaves, with
+ // chunk of lines in them, and branches, with up to ten leaves or
+ // other branch nodes below them. The top node is always a branch
+ // node, and is the document object itself (meaning it has
+ // additional methods and properties).
+ //
+ // All nodes have parent links. The tree is used both to go from
+ // line numbers to line objects, and to go from objects to numbers.
+ // It also indexes by height, and is used to convert between height
+ // and line object, and to find the total height of the document.
+ //
+ // See also http://marijnhaverbeke.nl/blog/codemirror-line-tree.html
+
+ function LeafChunk(lines) {
+ this.lines = lines;
+ this.parent = null;
+ for (var i = 0, height = 0; i < lines.length; ++i) {
+ lines[i].parent = this;
+ height += lines[i].height;
+ }
+ this.height = height;
+ }
+
+ LeafChunk.prototype = {
+ chunkSize: function() { return this.lines.length; },
+ // Remove the n lines at offset 'at'.
+ removeInner: function(at, n) {
+ for (var i = at, e = at + n; i < e; ++i) {
+ var line = this.lines[i];
+ this.height -= line.height;
+ cleanUpLine(line);
+ signalLater(line, "delete");
+ }
+ this.lines.splice(at, n);
+ },
+ // Helper used to collapse a small branch into a single leaf.
+ collapse: function(lines) {
+ lines.push.apply(lines, this.lines);
+ },
+ // Insert the given array of lines at offset 'at', count them as
+ // having the given height.
+ insertInner: function(at, lines, height) {
+ this.height += height;
+ this.lines = this.lines.slice(0, at).concat(lines).concat(this.lines.slice(at));
+ for (var i = 0; i < lines.length; ++i) lines[i].parent = this;
+ },
+ // Used to iterate over a part of the tree.
+ iterN: function(at, n, op) {
+ for (var e = at + n; at < e; ++at)
+ if (op(this.lines[at])) return true;
+ }
+ };
+
+ function BranchChunk(children) {
+ this.children = children;
+ var size = 0, height = 0;
+ for (var i = 0; i < children.length; ++i) {
+ var ch = children[i];
+ size += ch.chunkSize(); height += ch.height;
+ ch.parent = this;
+ }
+ this.size = size;
+ this.height = height;
+ this.parent = null;
+ }
+
+ BranchChunk.prototype = {
+ chunkSize: function() { return this.size; },
+ removeInner: function(at, n) {
+ this.size -= n;
+ for (var i = 0; i < this.children.length; ++i) {
+ var child = this.children[i], sz = child.chunkSize();
+ if (at < sz) {
+ var rm = Math.min(n, sz - at), oldHeight = child.height;
+ child.removeInner(at, rm);
+ this.height -= oldHeight - child.height;
+ if (sz == rm) { this.children.splice(i--, 1); child.parent = null; }
+ if ((n -= rm) == 0) break;
+ at = 0;
+ } else at -= sz;
+ }
+ // If the result is smaller than 25 lines, ensure that it is a
+ // single leaf node.
+ if (this.size - n < 25 &&
+ (this.children.length > 1 || !(this.children[0] instanceof LeafChunk))) {
+ var lines = [];
+ this.collapse(lines);
+ this.children = [new LeafChunk(lines)];
+ this.children[0].parent = this;
+ }
+ },
+ collapse: function(lines) {
+ for (var i = 0; i < this.children.length; ++i) this.children[i].collapse(lines);
+ },
+ insertInner: function(at, lines, height) {
+ this.size += lines.length;
+ this.height += height;
+ for (var i = 0; i < this.children.length; ++i) {
+ var child = this.children[i], sz = child.chunkSize();
+ if (at <= sz) {
+ child.insertInner(at, lines, height);
+ if (child.lines && child.lines.length > 50) {
+ // To avoid memory thrashing when child.lines is huge (e.g. first view of a large file), it's never spliced.
+ // Instead, small slices are taken. They're taken in order because sequential memory accesses are fastest.
+ var remaining = child.lines.length % 25 + 25
+ for (var pos = remaining; pos < child.lines.length;) {
+ var leaf = new LeafChunk(child.lines.slice(pos, pos += 25));
+ child.height -= leaf.height;
+ this.children.splice(++i, 0, leaf);
+ leaf.parent = this;
+ }
+ child.lines = child.lines.slice(0, remaining);
+ this.maybeSpill();
+ }
+ break;
+ }
+ at -= sz;
+ }
+ },
+ // When a node has grown, check whether it should be split.
+ maybeSpill: function() {
+ if (this.children.length <= 10) return;
+ var me = this;
+ do {
+ var spilled = me.children.splice(me.children.length - 5, 5);
+ var sibling = new BranchChunk(spilled);
+ if (!me.parent) { // Become the parent node
+ var copy = new BranchChunk(me.children);
+ copy.parent = me;
+ me.children = [copy, sibling];
+ me = copy;
+ } else {
+ me.size -= sibling.size;
+ me.height -= sibling.height;
+ var myIndex = indexOf(me.parent.children, me);
+ me.parent.children.splice(myIndex + 1, 0, sibling);
+ }
+ sibling.parent = me.parent;
+ } while (me.children.length > 10);
+ me.parent.maybeSpill();
+ },
+ iterN: function(at, n, op) {
+ for (var i = 0; i < this.children.length; ++i) {
+ var child = this.children[i], sz = child.chunkSize();
+ if (at < sz) {
+ var used = Math.min(n, sz - at);
+ if (child.iterN(at, used, op)) return true;
+ if ((n -= used) == 0) break;
+ at = 0;
+ } else at -= sz;
+ }
+ }
+ };
+
+ var nextDocId = 0;
+ var Doc = CodeMirror.Doc = function(text, mode, firstLine, lineSep) {
+ if (!(this instanceof Doc)) return new Doc(text, mode, firstLine, lineSep);
+ if (firstLine == null) firstLine = 0;
+
+ BranchChunk.call(this, [new LeafChunk([new Line("", null)])]);
+ this.first = firstLine;
+ this.scrollTop = this.scrollLeft = 0;
+ this.cantEdit = false;
+ this.cleanGeneration = 1;
+ this.frontier = firstLine;
+ var start = Pos(firstLine, 0);
+ this.sel = simpleSelection(start);
+ this.history = new History(null);
+ this.id = ++nextDocId;
+ this.modeOption = mode;
+ this.lineSep = lineSep;
+ this.extend = false;
+
+ if (typeof text == "string") text = this.splitLines(text);
+ updateDoc(this, {from: start, to: start, text: text});
+ setSelection(this, simpleSelection(start), sel_dontScroll);
+ };
+
+ Doc.prototype = createObj(BranchChunk.prototype, {
+ constructor: Doc,
+ // Iterate over the document. Supports two forms -- with only one
+ // argument, it calls that for each line in the document. With
+ // three, it iterates over the range given by the first two (with
+ // the second being non-inclusive).
+ iter: function(from, to, op) {
+ if (op) this.iterN(from - this.first, to - from, op);
+ else this.iterN(this.first, this.first + this.size, from);
+ },
+
+ // Non-public interface for adding and removing lines.
+ insert: function(at, lines) {
+ var height = 0;
+ for (var i = 0; i < lines.length; ++i) height += lines[i].height;
+ this.insertInner(at - this.first, lines, height);
+ },
+ remove: function(at, n) { this.removeInner(at - this.first, n); },
+
+ // From here, the methods are part of the public interface. Most
+ // are also available from CodeMirror (editor) instances.
+
+ getValue: function(lineSep) {
+ var lines = getLines(this, this.first, this.first + this.size);
+ if (lineSep === false) return lines;
+ return lines.join(lineSep || this.lineSeparator());
+ },
+ setValue: docMethodOp(function(code) {
+ var top = Pos(this.first, 0), last = this.first + this.size - 1;
+ makeChange(this, {from: top, to: Pos(last, getLine(this, last).text.length),
+ text: this.splitLines(code), origin: "setValue", full: true}, true);
+ setSelection(this, simpleSelection(top));
+ }),
+ replaceRange: function(code, from, to, origin) {
+ from = clipPos(this, from);
+ to = to ? clipPos(this, to) : from;
+ replaceRange(this, code, from, to, origin);
+ },
+ getRange: function(from, to, lineSep) {
+ var lines = getBetween(this, clipPos(this, from), clipPos(this, to));
+ if (lineSep === false) return lines;
+ return lines.join(lineSep || this.lineSeparator());
+ },
+
+ getLine: function(line) {var l = this.getLineHandle(line); return l && l.text;},
+
+ getLineHandle: function(line) {if (isLine(this, line)) return getLine(this, line);},
+ getLineNumber: function(line) {return lineNo(line);},
+
+ getLineHandleVisualStart: function(line) {
+ if (typeof line == "number") line = getLine(this, line);
+ return visualLine(line);
+ },
+
+ lineCount: function() {return this.size;},
+ firstLine: function() {return this.first;},
+ lastLine: function() {return this.first + this.size - 1;},
+
+ clipPos: function(pos) {return clipPos(this, pos);},
+
+ getCursor: function(start) {
+ var range = this.sel.primary(), pos;
+ if (start == null || start == "head") pos = range.head;
+ else if (start == "anchor") pos = range.anchor;
+ else if (start == "end" || start == "to" || start === false) pos = range.to();
+ else pos = range.from();
+ return pos;
+ },
+ listSelections: function() { return this.sel.ranges; },
+ somethingSelected: function() {return this.sel.somethingSelected();},
+
+ setCursor: docMethodOp(function(line, ch, options) {
+ setSimpleSelection(this, clipPos(this, typeof line == "number" ? Pos(line, ch || 0) : line), null, options);
+ }),
+ setSelection: docMethodOp(function(anchor, head, options) {
+ setSimpleSelection(this, clipPos(this, anchor), clipPos(this, head || anchor), options);
+ }),
+ extendSelection: docMethodOp(function(head, other, options) {
+ extendSelection(this, clipPos(this, head), other && clipPos(this, other), options);
+ }),
+ extendSelections: docMethodOp(function(heads, options) {
+ extendSelections(this, clipPosArray(this, heads), options);
+ }),
+ extendSelectionsBy: docMethodOp(function(f, options) {
+ var heads = map(this.sel.ranges, f);
+ extendSelections(this, clipPosArray(this, heads), options);
+ }),
+ setSelections: docMethodOp(function(ranges, primary, options) {
+ if (!ranges.length) return;
+ for (var i = 0, out = []; i < ranges.length; i++)
+ out[i] = new Range(clipPos(this, ranges[i].anchor),
+ clipPos(this, ranges[i].head));
+ if (primary == null) primary = Math.min(ranges.length - 1, this.sel.primIndex);
+ setSelection(this, normalizeSelection(out, primary), options);
+ }),
+ addSelection: docMethodOp(function(anchor, head, options) {
+ var ranges = this.sel.ranges.slice(0);
+ ranges.push(new Range(clipPos(this, anchor), clipPos(this, head || anchor)));
+ setSelection(this, normalizeSelection(ranges, ranges.length - 1), options);
+ }),
+
+ getSelection: function(lineSep) {
+ var ranges = this.sel.ranges, lines;
+ for (var i = 0; i < ranges.length; i++) {
+ var sel = getBetween(this, ranges[i].from(), ranges[i].to());
+ lines = lines ? lines.concat(sel) : sel;
+ }
+ if (lineSep === false) return lines;
+ else return lines.join(lineSep || this.lineSeparator());
+ },
+ getSelections: function(lineSep) {
+ var parts = [], ranges = this.sel.ranges;
+ for (var i = 0; i < ranges.length; i++) {
+ var sel = getBetween(this, ranges[i].from(), ranges[i].to());
+ if (lineSep !== false) sel = sel.join(lineSep || this.lineSeparator());
+ parts[i] = sel;
+ }
+ return parts;
+ },
+ replaceSelection: function(code, collapse, origin) {
+ var dup = [];
+ for (var i = 0; i < this.sel.ranges.length; i++)
+ dup[i] = code;
+ this.replaceSelections(dup, collapse, origin || "+input");
+ },
+ replaceSelections: docMethodOp(function(code, collapse, origin) {
+ var changes = [], sel = this.sel;
+ for (var i = 0; i < sel.ranges.length; i++) {
+ var range = sel.ranges[i];
+ changes[i] = {from: range.from(), to: range.to(), text: this.splitLines(code[i]), origin: origin};
+ }
+ var newSel = collapse && collapse != "end" && computeReplacedSel(this, changes, collapse);
+ for (var i = changes.length - 1; i >= 0; i--)
+ makeChange(this, changes[i]);
+ if (newSel) setSelectionReplaceHistory(this, newSel);
+ else if (this.cm) ensureCursorVisible(this.cm);
+ }),
+ undo: docMethodOp(function() {makeChangeFromHistory(this, "undo");}),
+ redo: docMethodOp(function() {makeChangeFromHistory(this, "redo");}),
+ undoSelection: docMethodOp(function() {makeChangeFromHistory(this, "undo", true);}),
+ redoSelection: docMethodOp(function() {makeChangeFromHistory(this, "redo", true);}),
+
+ setExtending: function(val) {this.extend = val;},
+ getExtending: function() {return this.extend;},
+
+ historySize: function() {
+ var hist = this.history, done = 0, undone = 0;
+ for (var i = 0; i < hist.done.length; i++) if (!hist.done[i].ranges) ++done;
+ for (var i = 0; i < hist.undone.length; i++) if (!hist.undone[i].ranges) ++undone;
+ return {undo: done, redo: undone};
+ },
+ clearHistory: function() {this.history = new History(this.history.maxGeneration);},
+
+ markClean: function() {
+ this.cleanGeneration = this.changeGeneration(true);
+ },
+ changeGeneration: function(forceSplit) {
+ if (forceSplit)
+ this.history.lastOp = this.history.lastSelOp = this.history.lastOrigin = null;
+ return this.history.generation;
+ },
+ isClean: function (gen) {
+ return this.history.generation == (gen || this.cleanGeneration);
+ },
+
+ getHistory: function() {
+ return {done: copyHistoryArray(this.history.done),
+ undone: copyHistoryArray(this.history.undone)};
+ },
+ setHistory: function(histData) {
+ var hist = this.history = new History(this.history.maxGeneration);
+ hist.done = copyHistoryArray(histData.done.slice(0), null, true);
+ hist.undone = copyHistoryArray(histData.undone.slice(0), null, true);
+ },
+
+ addLineClass: docMethodOp(function(handle, where, cls) {
+ return changeLine(this, handle, where == "gutter" ? "gutter" : "class", function(line) {
+ var prop = where == "text" ? "textClass"
+ : where == "background" ? "bgClass"
+ : where == "gutter" ? "gutterClass" : "wrapClass";
+ if (!line[prop]) line[prop] = cls;
+ else if (classTest(cls).test(line[prop])) return false;
+ else line[prop] += " " + cls;
+ return true;
+ });
+ }),
+ removeLineClass: docMethodOp(function(handle, where, cls) {
+ return changeLine(this, handle, where == "gutter" ? "gutter" : "class", function(line) {
+ var prop = where == "text" ? "textClass"
+ : where == "background" ? "bgClass"
+ : where == "gutter" ? "gutterClass" : "wrapClass";
+ var cur = line[prop];
+ if (!cur) return false;
+ else if (cls == null) line[prop] = null;
+ else {
+ var found = cur.match(classTest(cls));
+ if (!found) return false;
+ var end = found.index + found[0].length;
+ line[prop] = cur.slice(0, found.index) + (!found.index || end == cur.length ? "" : " ") + cur.slice(end) || null;
+ }
+ return true;
+ });
+ }),
+
+ addLineWidget: docMethodOp(function(handle, node, options) {
+ return addLineWidget(this, handle, node, options);
+ }),
+ removeLineWidget: function(widget) { widget.clear(); },
+
+ markText: function(from, to, options) {
+ return markText(this, clipPos(this, from), clipPos(this, to), options, options && options.type || "range");
+ },
+ setBookmark: function(pos, options) {
+ var realOpts = {replacedWith: options && (options.nodeType == null ? options.widget : options),
+ insertLeft: options && options.insertLeft,
+ clearWhenEmpty: false, shared: options && options.shared,
+ handleMouseEvents: options && options.handleMouseEvents};
+ pos = clipPos(this, pos);
+ return markText(this, pos, pos, realOpts, "bookmark");
+ },
+ findMarksAt: function(pos) {
+ pos = clipPos(this, pos);
+ var markers = [], spans = getLine(this, pos.line).markedSpans;
+ if (spans) for (var i = 0; i < spans.length; ++i) {
+ var span = spans[i];
+ if ((span.from == null || span.from <= pos.ch) &&
+ (span.to == null || span.to >= pos.ch))
+ markers.push(span.marker.parent || span.marker);
+ }
+ return markers;
+ },
+ findMarks: function(from, to, filter) {
+ from = clipPos(this, from); to = clipPos(this, to);
+ var found = [], lineNo = from.line;
+ this.iter(from.line, to.line + 1, function(line) {
+ var spans = line.markedSpans;
+ if (spans) for (var i = 0; i < spans.length; i++) {
+ var span = spans[i];
+ if (!(span.to != null && lineNo == from.line && from.ch >= span.to ||
+ span.from == null && lineNo != from.line ||
+ span.from != null && lineNo == to.line && span.from >= to.ch) &&
+ (!filter || filter(span.marker)))
+ found.push(span.marker.parent || span.marker);
+ }
+ ++lineNo;
+ });
+ return found;
+ },
+ getAllMarks: function() {
+ var markers = [];
+ this.iter(function(line) {
+ var sps = line.markedSpans;
+ if (sps) for (var i = 0; i < sps.length; ++i)
+ if (sps[i].from != null) markers.push(sps[i].marker);
+ });
+ return markers;
+ },
+
+ posFromIndex: function(off) {
+ var ch, lineNo = this.first, sepSize = this.lineSeparator().length;
+ this.iter(function(line) {
+ var sz = line.text.length + sepSize;
+ if (sz > off) { ch = off; return true; }
+ off -= sz;
+ ++lineNo;
+ });
+ return clipPos(this, Pos(lineNo, ch));
+ },
+ indexFromPos: function (coords) {
+ coords = clipPos(this, coords);
+ var index = coords.ch;
+ if (coords.line < this.first || coords.ch < 0) return 0;
+ var sepSize = this.lineSeparator().length;
+ this.iter(this.first, coords.line, function (line) {
+ index += line.text.length + sepSize;
+ });
+ return index;
+ },
+
+ copy: function(copyHistory) {
+ var doc = new Doc(getLines(this, this.first, this.first + this.size),
+ this.modeOption, this.first, this.lineSep);
+ doc.scrollTop = this.scrollTop; doc.scrollLeft = this.scrollLeft;
+ doc.sel = this.sel;
+ doc.extend = false;
+ if (copyHistory) {
+ doc.history.undoDepth = this.history.undoDepth;
+ doc.setHistory(this.getHistory());
+ }
+ return doc;
+ },
+
+ linkedDoc: function(options) {
+ if (!options) options = {};
+ var from = this.first, to = this.first + this.size;
+ if (options.from != null && options.from > from) from = options.from;
+ if (options.to != null && options.to < to) to = options.to;
+ var copy = new Doc(getLines(this, from, to), options.mode || this.modeOption, from, this.lineSep);
+ if (options.sharedHist) copy.history = this.history;
+ (this.linked || (this.linked = [])).push({doc: copy, sharedHist: options.sharedHist});
+ copy.linked = [{doc: this, isParent: true, sharedHist: options.sharedHist}];
+ copySharedMarkers(copy, findSharedMarkers(this));
+ return copy;
+ },
+ unlinkDoc: function(other) {
+ if (other instanceof CodeMirror) other = other.doc;
+ if (this.linked) for (var i = 0; i < this.linked.length; ++i) {
+ var link = this.linked[i];
+ if (link.doc != other) continue;
+ this.linked.splice(i, 1);
+ other.unlinkDoc(this);
+ detachSharedMarkers(findSharedMarkers(this));
+ break;
+ }
+ // If the histories were shared, split them again
+ if (other.history == this.history) {
+ var splitIds = [other.id];
+ linkedDocs(other, function(doc) {splitIds.push(doc.id);}, true);
+ other.history = new History(null);
+ other.history.done = copyHistoryArray(this.history.done, splitIds);
+ other.history.undone = copyHistoryArray(this.history.undone, splitIds);
+ }
+ },
+ iterLinkedDocs: function(f) {linkedDocs(this, f);},
+
+ getMode: function() {return this.mode;},
+ getEditor: function() {return this.cm;},
+
+ splitLines: function(str) {
+ if (this.lineSep) return str.split(this.lineSep);
+ return splitLinesAuto(str);
+ },
+ lineSeparator: function() { return this.lineSep || "\n"; }
+ });
+
+ // Public alias.
+ Doc.prototype.eachLine = Doc.prototype.iter;
+
+ // Set up methods on CodeMirror's prototype to redirect to the editor's document.
+ var dontDelegate = "iter insert remove copy getEditor constructor".split(" ");
+ for (var prop in Doc.prototype) if (Doc.prototype.hasOwnProperty(prop) && indexOf(dontDelegate, prop) < 0)
+ CodeMirror.prototype[prop] = (function(method) {
+ return function() {return method.apply(this.doc, arguments);};
+ })(Doc.prototype[prop]);
+
+ eventMixin(Doc);
+
+ // Call f for all linked documents.
+ function linkedDocs(doc, f, sharedHistOnly) {
+ function propagate(doc, skip, sharedHist) {
+ if (doc.linked) for (var i = 0; i < doc.linked.length; ++i) {
+ var rel = doc.linked[i];
+ if (rel.doc == skip) continue;
+ var shared = sharedHist && rel.sharedHist;
+ if (sharedHistOnly && !shared) continue;
+ f(rel.doc, shared);
+ propagate(rel.doc, doc, shared);
+ }
+ }
+ propagate(doc, null, true);
+ }
+
+ // Attach a document to an editor.
+ function attachDoc(cm, doc) {
+ if (doc.cm) throw new Error("This document is already in use.");
+ cm.doc = doc;
+ doc.cm = cm;
+ estimateLineHeights(cm);
+ loadMode(cm);
+ if (!cm.options.lineWrapping) findMaxLine(cm);
+ cm.options.mode = doc.modeOption;
+ regChange(cm);
+ }
+
+ // LINE UTILITIES
+
+ // Find the line object corresponding to the given line number.
+ function getLine(doc, n) {
+ n -= doc.first;
+ if (n < 0 || n >= doc.size) throw new Error("There is no line " + (n + doc.first) + " in the document.");
+ for (var chunk = doc; !chunk.lines;) {
+ for (var i = 0;; ++i) {
+ var child = chunk.children[i], sz = child.chunkSize();
+ if (n < sz) { chunk = child; break; }
+ n -= sz;
+ }
+ }
+ return chunk.lines[n];
+ }
+
+ // Get the part of a document between two positions, as an array of
+ // strings.
+ function getBetween(doc, start, end) {
+ var out = [], n = start.line;
+ doc.iter(start.line, end.line + 1, function(line) {
+ var text = line.text;
+ if (n == end.line) text = text.slice(0, end.ch);
+ if (n == start.line) text = text.slice(start.ch);
+ out.push(text);
+ ++n;
+ });
+ return out;
+ }
+ // Get the lines between from and to, as array of strings.
+ function getLines(doc, from, to) {
+ var out = [];
+ doc.iter(from, to, function(line) { out.push(line.text); });
+ return out;
+ }
+
+ // Update the height of a line, propagating the height change
+ // upwards to parent nodes.
+ function updateLineHeight(line, height) {
+ var diff = height - line.height;
+ if (diff) for (var n = line; n; n = n.parent) n.height += diff;
+ }
+
+ // Given a line object, find its line number by walking up through
+ // its parent links.
+ function lineNo(line) {
+ if (line.parent == null) return null;
+ var cur = line.parent, no = indexOf(cur.lines, line);
+ for (var chunk = cur.parent; chunk; cur = chunk, chunk = chunk.parent) {
+ for (var i = 0;; ++i) {
+ if (chunk.children[i] == cur) break;
+ no += chunk.children[i].chunkSize();
+ }
+ }
+ return no + cur.first;
+ }
+
+ // Find the line at the given vertical position, using the height
+ // information in the document tree.
+ function lineAtHeight(chunk, h) {
+ var n = chunk.first;
+ outer: do {
+ for (var i = 0; i < chunk.children.length; ++i) {
+ var child = chunk.children[i], ch = child.height;
+ if (h < ch) { chunk = child; continue outer; }
+ h -= ch;
+ n += child.chunkSize();
+ }
+ return n;
+ } while (!chunk.lines);
+ for (var i = 0; i < chunk.lines.length; ++i) {
+ var line = chunk.lines[i], lh = line.height;
+ if (h < lh) break;
+ h -= lh;
+ }
+ return n + i;
+ }
+
+
+ // Find the height above the given line.
+ function heightAtLine(lineObj) {
+ lineObj = visualLine(lineObj);
+
+ var h = 0, chunk = lineObj.parent;
+ for (var i = 0; i < chunk.lines.length; ++i) {
+ var line = chunk.lines[i];
+ if (line == lineObj) break;
+ else h += line.height;
+ }
+ for (var p = chunk.parent; p; chunk = p, p = chunk.parent) {
+ for (var i = 0; i < p.children.length; ++i) {
+ var cur = p.children[i];
+ if (cur == chunk) break;
+ else h += cur.height;
+ }
+ }
+ return h;
+ }
+
+ // Get the bidi ordering for the given line (and cache it). Returns
+ // false for lines that are fully left-to-right, and an array of
+ // BidiSpan objects otherwise.
+ function getOrder(line) {
+ var order = line.order;
+ if (order == null) order = line.order = bidiOrdering(line.text);
+ return order;
+ }
+
+ // HISTORY
+
+ function History(startGen) {
+ // Arrays of change events and selections. Doing something adds an
+ // event to done and clears undo. Undoing moves events from done
+ // to undone, redoing moves them in the other direction.
+ this.done = []; this.undone = [];
+ this.undoDepth = Infinity;
+ // Used to track when changes can be merged into a single undo
+ // event
+ this.lastModTime = this.lastSelTime = 0;
+ this.lastOp = this.lastSelOp = null;
+ this.lastOrigin = this.lastSelOrigin = null;
+ // Used by the isClean() method
+ this.generation = this.maxGeneration = startGen || 1;
+ }
+
+ // Create a history change event from an updateDoc-style change
+ // object.
+ function historyChangeFromChange(doc, change) {
+ var histChange = {from: copyPos(change.from), to: changeEnd(change), text: getBetween(doc, change.from, change.to)};
+ attachLocalSpans(doc, histChange, change.from.line, change.to.line + 1);
+ linkedDocs(doc, function(doc) {attachLocalSpans(doc, histChange, change.from.line, change.to.line + 1);}, true);
+ return histChange;
+ }
+
+ // Pop all selection events off the end of a history array. Stop at
+ // a change event.
+ function clearSelectionEvents(array) {
+ while (array.length) {
+ var last = lst(array);
+ if (last.ranges) array.pop();
+ else break;
+ }
+ }
+
+ // Find the top change event in the history. Pop off selection
+ // events that are in the way.
+ function lastChangeEvent(hist, force) {
+ if (force) {
+ clearSelectionEvents(hist.done);
+ return lst(hist.done);
+ } else if (hist.done.length && !lst(hist.done).ranges) {
+ return lst(hist.done);
+ } else if (hist.done.length > 1 && !hist.done[hist.done.length - 2].ranges) {
+ hist.done.pop();
+ return lst(hist.done);
+ }
+ }
+
+ // Register a change in the history. Merges changes that are within
+ // a single operation, ore are close together with an origin that
+ // allows merging (starting with "+") into a single event.
+ function addChangeToHistory(doc, change, selAfter, opId) {
+ var hist = doc.history;
+ hist.undone.length = 0;
+ var time = +new Date, cur;
+
+ if ((hist.lastOp == opId ||
+ hist.lastOrigin == change.origin && change.origin &&
+ ((change.origin.charAt(0) == "+" && doc.cm && hist.lastModTime > time - doc.cm.options.historyEventDelay) ||
+ change.origin.charAt(0) == "*")) &&
+ (cur = lastChangeEvent(hist, hist.lastOp == opId))) {
+ // Merge this change into the last event
+ var last = lst(cur.changes);
+ if (cmp(change.from, change.to) == 0 && cmp(change.from, last.to) == 0) {
+ // Optimized case for simple insertion -- don't want to add
+ // new changesets for every character typed
+ last.to = changeEnd(change);
+ } else {
+ // Add new sub-event
+ cur.changes.push(historyChangeFromChange(doc, change));
+ }
+ } else {
+ // Can not be merged, start a new event.
+ var before = lst(hist.done);
+ if (!before || !before.ranges)
+ pushSelectionToHistory(doc.sel, hist.done);
+ cur = {changes: [historyChangeFromChange(doc, change)],
+ generation: hist.generation};
+ hist.done.push(cur);
+ while (hist.done.length > hist.undoDepth) {
+ hist.done.shift();
+ if (!hist.done[0].ranges) hist.done.shift();
+ }
+ }
+ hist.done.push(selAfter);
+ hist.generation = ++hist.maxGeneration;
+ hist.lastModTime = hist.lastSelTime = time;
+ hist.lastOp = hist.lastSelOp = opId;
+ hist.lastOrigin = hist.lastSelOrigin = change.origin;
+
+ if (!last) signal(doc, "historyAdded");
+ }
+
+ function selectionEventCanBeMerged(doc, origin, prev, sel) {
+ var ch = origin.charAt(0);
+ return ch == "*" ||
+ ch == "+" &&
+ prev.ranges.length == sel.ranges.length &&
+ prev.somethingSelected() == sel.somethingSelected() &&
+ new Date - doc.history.lastSelTime <= (doc.cm ? doc.cm.options.historyEventDelay : 500);
+ }
+
+ // Called whenever the selection changes, sets the new selection as
+ // the pending selection in the history, and pushes the old pending
+ // selection into the 'done' array when it was significantly
+ // different (in number of selected ranges, emptiness, or time).
+ function addSelectionToHistory(doc, sel, opId, options) {
+ var hist = doc.history, origin = options && options.origin;
+
+ // A new event is started when the previous origin does not match
+ // the current, or the origins don't allow matching. Origins
+ // starting with * are always merged, those starting with + are
+ // merged when similar and close together in time.
+ if (opId == hist.lastSelOp ||
+ (origin && hist.lastSelOrigin == origin &&
+ (hist.lastModTime == hist.lastSelTime && hist.lastOrigin == origin ||
+ selectionEventCanBeMerged(doc, origin, lst(hist.done), sel))))
+ hist.done[hist.done.length - 1] = sel;
+ else
+ pushSelectionToHistory(sel, hist.done);
+
+ hist.lastSelTime = +new Date;
+ hist.lastSelOrigin = origin;
+ hist.lastSelOp = opId;
+ if (options && options.clearRedo !== false)
+ clearSelectionEvents(hist.undone);
+ }
+
+ function pushSelectionToHistory(sel, dest) {
+ var top = lst(dest);
+ if (!(top && top.ranges && top.equals(sel)))
+ dest.push(sel);
+ }
+
+ // Used to store marked span information in the history.
+ function attachLocalSpans(doc, change, from, to) {
+ var existing = change["spans_" + doc.id], n = 0;
+ doc.iter(Math.max(doc.first, from), Math.min(doc.first + doc.size, to), function(line) {
+ if (line.markedSpans)
+ (existing || (existing = change["spans_" + doc.id] = {}))[n] = line.markedSpans;
+ ++n;
+ });
+ }
+
+ // When un/re-doing restores text containing marked spans, those
+ // that have been explicitly cleared should not be restored.
+ function removeClearedSpans(spans) {
+ if (!spans) return null;
+ for (var i = 0, out; i < spans.length; ++i) {
+ if (spans[i].marker.explicitlyCleared) { if (!out) out = spans.slice(0, i); }
+ else if (out) out.push(spans[i]);
+ }
+ return !out ? spans : out.length ? out : null;
+ }
+
+ // Retrieve and filter the old marked spans stored in a change event.
+ function getOldSpans(doc, change) {
+ var found = change["spans_" + doc.id];
+ if (!found) return null;
+ for (var i = 0, nw = []; i < change.text.length; ++i)
+ nw.push(removeClearedSpans(found[i]));
+ return nw;
+ }
+
+ // Used both to provide a JSON-safe object in .getHistory, and, when
+ // detaching a document, to split the history in two
+ function copyHistoryArray(events, newGroup, instantiateSel) {
+ for (var i = 0, copy = []; i < events.length; ++i) {
+ var event = events[i];
+ if (event.ranges) {
+ copy.push(instantiateSel ? Selection.prototype.deepCopy.call(event) : event);
+ continue;
+ }
+ var changes = event.changes, newChanges = [];
+ copy.push({changes: newChanges});
+ for (var j = 0; j < changes.length; ++j) {
+ var change = changes[j], m;
+ newChanges.push({from: change.from, to: change.to, text: change.text});
+ if (newGroup) for (var prop in change) if (m = prop.match(/^spans_(\d+)$/)) {
+ if (indexOf(newGroup, Number(m[1])) > -1) {
+ lst(newChanges)[prop] = change[prop];
+ delete change[prop];
+ }
+ }
+ }
+ }
+ return copy;
+ }
+
+ // Rebasing/resetting history to deal with externally-sourced changes
+
+ function rebaseHistSelSingle(pos, from, to, diff) {
+ if (to < pos.line) {
+ pos.line += diff;
+ } else if (from < pos.line) {
+ pos.line = from;
+ pos.ch = 0;
+ }
+ }
+
+ // Tries to rebase an array of history events given a change in the
+ // document. If the change touches the same lines as the event, the
+ // event, and everything 'behind' it, is discarded. If the change is
+ // before the event, the event's positions are updated. Uses a
+ // copy-on-write scheme for the positions, to avoid having to
+ // reallocate them all on every rebase, but also avoid problems with
+ // shared position objects being unsafely updated.
+ function rebaseHistArray(array, from, to, diff) {
+ for (var i = 0; i < array.length; ++i) {
+ var sub = array[i], ok = true;
+ if (sub.ranges) {
+ if (!sub.copied) { sub = array[i] = sub.deepCopy(); sub.copied = true; }
+ for (var j = 0; j < sub.ranges.length; j++) {
+ rebaseHistSelSingle(sub.ranges[j].anchor, from, to, diff);
+ rebaseHistSelSingle(sub.ranges[j].head, from, to, diff);
+ }
+ continue;
+ }
+ for (var j = 0; j < sub.changes.length; ++j) {
+ var cur = sub.changes[j];
+ if (to < cur.from.line) {
+ cur.from = Pos(cur.from.line + diff, cur.from.ch);
+ cur.to = Pos(cur.to.line + diff, cur.to.ch);
+ } else if (from <= cur.to.line) {
+ ok = false;
+ break;
+ }
+ }
+ if (!ok) {
+ array.splice(0, i + 1);
+ i = 0;
+ }
+ }
+ }
+
+ function rebaseHist(hist, change) {
+ var from = change.from.line, to = change.to.line, diff = change.text.length - (to - from) - 1;
+ rebaseHistArray(hist.done, from, to, diff);
+ rebaseHistArray(hist.undone, from, to, diff);
+ }
+
+ // EVENT UTILITIES
+
+ // Due to the fact that we still support jurassic IE versions, some
+ // compatibility wrappers are needed.
+
+ var e_preventDefault = CodeMirror.e_preventDefault = function(e) {
+ if (e.preventDefault) e.preventDefault();
+ else e.returnValue = false;
+ };
+ var e_stopPropagation = CodeMirror.e_stopPropagation = function(e) {
+ if (e.stopPropagation) e.stopPropagation();
+ else e.cancelBubble = true;
+ };
+ function e_defaultPrevented(e) {
+ return e.defaultPrevented != null ? e.defaultPrevented : e.returnValue == false;
+ }
+ var e_stop = CodeMirror.e_stop = function(e) {e_preventDefault(e); e_stopPropagation(e);};
+
+ function e_target(e) {return e.target || e.srcElement;}
+ function e_button(e) {
+ var b = e.which;
+ if (b == null) {
+ if (e.button & 1) b = 1;
+ else if (e.button & 2) b = 3;
+ else if (e.button & 4) b = 2;
+ }
+ if (mac && e.ctrlKey && b == 1) b = 3;
+ return b;
+ }
+
+ // EVENT HANDLING
+
+ // Lightweight event framework. on/off also work on DOM nodes,
+ // registering native DOM handlers.
+
+ var on = CodeMirror.on = function(emitter, type, f) {
+ if (emitter.addEventListener)
+ emitter.addEventListener(type, f, false);
+ else if (emitter.attachEvent)
+ emitter.attachEvent("on" + type, f);
+ else {
+ var map = emitter._handlers || (emitter._handlers = {});
+ var arr = map[type] || (map[type] = []);
+ arr.push(f);
+ }
+ };
+
+ var noHandlers = []
+ function getHandlers(emitter, type, copy) {
+ var arr = emitter._handlers && emitter._handlers[type]
+ if (copy) return arr && arr.length > 0 ? arr.slice() : noHandlers
+ else return arr || noHandlers
+ }
+
+ var off = CodeMirror.off = function(emitter, type, f) {
+ if (emitter.removeEventListener)
+ emitter.removeEventListener(type, f, false);
+ else if (emitter.detachEvent)
+ emitter.detachEvent("on" + type, f);
+ else {
+ var handlers = getHandlers(emitter, type, false)
+ for (var i = 0; i < handlers.length; ++i)
+ if (handlers[i] == f) { handlers.splice(i, 1); break; }
+ }
+ };
+
+ var signal = CodeMirror.signal = function(emitter, type /*, values...*/) {
+ var handlers = getHandlers(emitter, type, true)
+ if (!handlers.length) return;
+ var args = Array.prototype.slice.call(arguments, 2);
+ for (var i = 0; i < handlers.length; ++i) handlers[i].apply(null, args);
+ };
+
+ var orphanDelayedCallbacks = null;
+
+ // Often, we want to signal events at a point where we are in the
+ // middle of some work, but don't want the handler to start calling
+ // other methods on the editor, which might be in an inconsistent
+ // state or simply not expect any other events to happen.
+ // signalLater looks whether there are any handlers, and schedules
+ // them to be executed when the last operation ends, or, if no
+ // operation is active, when a timeout fires.
+ function signalLater(emitter, type /*, values...*/) {
+ var arr = getHandlers(emitter, type, false)
+ if (!arr.length) return;
+ var args = Array.prototype.slice.call(arguments, 2), list;
+ if (operationGroup) {
+ list = operationGroup.delayedCallbacks;
+ } else if (orphanDelayedCallbacks) {
+ list = orphanDelayedCallbacks;
+ } else {
+ list = orphanDelayedCallbacks = [];
+ setTimeout(fireOrphanDelayed, 0);
+ }
+ function bnd(f) {return function(){f.apply(null, args);};};
+ for (var i = 0; i < arr.length; ++i)
+ list.push(bnd(arr[i]));
+ }
+
+ function fireOrphanDelayed() {
+ var delayed = orphanDelayedCallbacks;
+ orphanDelayedCallbacks = null;
+ for (var i = 0; i < delayed.length; ++i) delayed[i]();
+ }
+
+ // The DOM events that CodeMirror handles can be overridden by
+ // registering a (non-DOM) handler on the editor for the event name,
+ // and preventDefault-ing the event in that handler.
+ function signalDOMEvent(cm, e, override) {
+ if (typeof e == "string")
+ e = {type: e, preventDefault: function() { this.defaultPrevented = true; }};
+ signal(cm, override || e.type, cm, e);
+ return e_defaultPrevented(e) || e.codemirrorIgnore;
+ }
+
+ function signalCursorActivity(cm) {
+ var arr = cm._handlers && cm._handlers.cursorActivity;
+ if (!arr) return;
+ var set = cm.curOp.cursorActivityHandlers || (cm.curOp.cursorActivityHandlers = []);
+ for (var i = 0; i < arr.length; ++i) if (indexOf(set, arr[i]) == -1)
+ set.push(arr[i]);
+ }
+
+ function hasHandler(emitter, type) {
+ return getHandlers(emitter, type).length > 0
+ }
+
+ // Add on and off methods to a constructor's prototype, to make
+ // registering events on such objects more convenient.
+ function eventMixin(ctor) {
+ ctor.prototype.on = function(type, f) {on(this, type, f);};
+ ctor.prototype.off = function(type, f) {off(this, type, f);};
+ }
+
+ // MISC UTILITIES
+
+ // Number of pixels added to scroller and sizer to hide scrollbar
+ var scrollerGap = 30;
+
+ // Returned or thrown by various protocols to signal 'I'm not
+ // handling this'.
+ var Pass = CodeMirror.Pass = {toString: function(){return "CodeMirror.Pass";}};
+
+ // Reused option objects for setSelection & friends
+ var sel_dontScroll = {scroll: false}, sel_mouse = {origin: "*mouse"}, sel_move = {origin: "+move"};
+
+ function Delayed() {this.id = null;}
+ Delayed.prototype.set = function(ms, f) {
+ clearTimeout(this.id);
+ this.id = setTimeout(f, ms);
+ };
+
+ // Counts the column offset in a string, taking tabs into account.
+ // Used mostly to find indentation.
+ var countColumn = CodeMirror.countColumn = function(string, end, tabSize, startIndex, startValue) {
+ if (end == null) {
+ end = string.search(/[^\s\u00a0]/);
+ if (end == -1) end = string.length;
+ }
+ for (var i = startIndex || 0, n = startValue || 0;;) {
+ var nextTab = string.indexOf("\t", i);
+ if (nextTab < 0 || nextTab >= end)
+ return n + (end - i);
+ n += nextTab - i;
+ n += tabSize - (n % tabSize);
+ i = nextTab + 1;
+ }
+ };
+
+ // The inverse of countColumn -- find the offset that corresponds to
+ // a particular column.
+ var findColumn = CodeMirror.findColumn = function(string, goal, tabSize) {
+ for (var pos = 0, col = 0;;) {
+ var nextTab = string.indexOf("\t", pos);
+ if (nextTab == -1) nextTab = string.length;
+ var skipped = nextTab - pos;
+ if (nextTab == string.length || col + skipped >= goal)
+ return pos + Math.min(skipped, goal - col);
+ col += nextTab - pos;
+ col += tabSize - (col % tabSize);
+ pos = nextTab + 1;
+ if (col >= goal) return pos;
+ }
+ }
+
+ var spaceStrs = [""];
+ function spaceStr(n) {
+ while (spaceStrs.length <= n)
+ spaceStrs.push(lst(spaceStrs) + " ");
+ return spaceStrs[n];
+ }
+
+ function lst(arr) { return arr[arr.length-1]; }
+
+ var selectInput = function(node) { node.select(); };
+ if (ios) // Mobile Safari apparently has a bug where select() is broken.
+ selectInput = function(node) { node.selectionStart = 0; node.selectionEnd = node.value.length; };
+ else if (ie) // Suppress mysterious IE10 errors
+ selectInput = function(node) { try { node.select(); } catch(_e) {} };
+
+ function indexOf(array, elt) {
+ for (var i = 0; i < array.length; ++i)
+ if (array[i] == elt) return i;
+ return -1;
+ }
+ function map(array, f) {
+ var out = [];
+ for (var i = 0; i < array.length; i++) out[i] = f(array[i], i);
+ return out;
+ }
+
+ function nothing() {}
+
+ function createObj(base, props) {
+ var inst;
+ if (Object.create) {
+ inst = Object.create(base);
+ } else {
+ nothing.prototype = base;
+ inst = new nothing();
+ }
+ if (props) copyObj(props, inst);
+ return inst;
+ };
+
+ function copyObj(obj, target, overwrite) {
+ if (!target) target = {};
+ for (var prop in obj)
+ if (obj.hasOwnProperty(prop) && (overwrite !== false || !target.hasOwnProperty(prop)))
+ target[prop] = obj[prop];
+ return target;
+ }
+
+ function bind(f) {
+ var args = Array.prototype.slice.call(arguments, 1);
+ return function(){return f.apply(null, args);};
+ }
+
+ var nonASCIISingleCaseWordChar = /[\u00df\u0587\u0590-\u05f4\u0600-\u06ff\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc\uac00-\ud7af]/;
+ var isWordCharBasic = CodeMirror.isWordChar = function(ch) {
+ return /\w/.test(ch) || ch > "\x80" &&
+ (ch.toUpperCase() != ch.toLowerCase() || nonASCIISingleCaseWordChar.test(ch));
+ };
+ function isWordChar(ch, helper) {
+ if (!helper) return isWordCharBasic(ch);
+ if (helper.source.indexOf("\\w") > -1 && isWordCharBasic(ch)) return true;
+ return helper.test(ch);
+ }
+
+ function isEmpty(obj) {
+ for (var n in obj) if (obj.hasOwnProperty(n) && obj[n]) return false;
+ return true;
+ }
+
+ // Extending unicode characters. A series of a non-extending char +
+ // any number of extending chars is treated as a single unit as far
+ // as editing and measuring is concerned. This is not fully correct,
+ // since some scripts/fonts/browsers also treat other configurations
+ // of code points as a group.
+ var extendingChars = /[\u0300-\u036f\u0483-\u0489\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u064b-\u065e\u0670\u06d6-\u06dc\u06de-\u06e4\u06e7\u06e8\u06ea-\u06ed\u0711\u0730-\u074a\u07a6-\u07b0\u07eb-\u07f3\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0900-\u0902\u093c\u0941-\u0948\u094d\u0951-\u0955\u0962\u0963\u0981\u09bc\u09be\u09c1-\u09c4\u09cd\u09d7\u09e2\u09e3\u0a01\u0a02\u0a3c\u0a41\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a70\u0a71\u0a75\u0a81\u0a82\u0abc\u0ac1-\u0ac5\u0ac7\u0ac8\u0acd\u0ae2\u0ae3\u0b01\u0b3c\u0b3e\u0b3f\u0b41-\u0b44\u0b4d\u0b56\u0b57\u0b62\u0b63\u0b82\u0bbe\u0bc0\u0bcd\u0bd7\u0c3e-\u0c40\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62\u0c63\u0cbc\u0cbf\u0cc2\u0cc6\u0ccc\u0ccd\u0cd5\u0cd6\u0ce2\u0ce3\u0d3e\u0d41-\u0d44\u0d4d\u0d57\u0d62\u0d63\u0dca\u0dcf\u0dd2-\u0dd4\u0dd6\u0ddf\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0eb1\u0eb4-\u0eb9\u0ebb\u0ebc\u0ec8-\u0ecd\u0f18\u0f19\u0f35\u0f37\u0f39\u0f71-\u0f7e\u0f80-\u0f84\u0f86\u0f87\u0f90-\u0f97\u0f99-\u0fbc\u0fc6\u102d-\u1030\u1032-\u1037\u1039\u103a\u103d\u103e\u1058\u1059\u105e-\u1060\u1071-\u1074\u1082\u1085\u1086\u108d\u109d\u135f\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17b7-\u17bd\u17c6\u17c9-\u17d3\u17dd\u180b-\u180d\u18a9\u1920-\u1922\u1927\u1928\u1932\u1939-\u193b\u1a17\u1a18\u1a56\u1a58-\u1a5e\u1a60\u1a62\u1a65-\u1a6c\u1a73-\u1a7c\u1a7f\u1b00-\u1b03\u1b34\u1b36-\u1b3a\u1b3c\u1b42\u1b6b-\u1b73\u1b80\u1b81\u1ba2-\u1ba5\u1ba8\u1ba9\u1c2c-\u1c33\u1c36\u1c37\u1cd0-\u1cd2\u1cd4-\u1ce0\u1ce2-\u1ce8\u1ced\u1dc0-\u1de6\u1dfd-\u1dff\u200c\u200d\u20d0-\u20f0\u2cef-\u2cf1\u2de0-\u2dff\u302a-\u302f\u3099\u309a\ua66f-\ua672\ua67c\ua67d\ua6f0\ua6f1\ua802\ua806\ua80b\ua825\ua826\ua8c4\ua8e0-\ua8f1\ua926-\ua92d\ua947-\ua951\ua980-\ua982\ua9b3\ua9b6-\ua9b9\ua9bc\uaa29-\uaa2e\uaa31\uaa32\uaa35\uaa36\uaa43\uaa4c\uaab0\uaab2-\uaab4\uaab7\uaab8\uaabe\uaabf\uaac1\uabe5\uabe8\uabed\udc00-\udfff\ufb1e\ufe00-\ufe0f\ufe20-\ufe26\uff9e\uff9f]/;
+ function isExtendingChar(ch) { return ch.charCodeAt(0) >= 768 && extendingChars.test(ch); }
+
+ // DOM UTILITIES
+
+ function elt(tag, content, className, style) {
+ var e = document.createElement(tag);
+ if (className) e.className = className;
+ if (style) e.style.cssText = style;
+ if (typeof content == "string") e.appendChild(document.createTextNode(content));
+ else if (content) for (var i = 0; i < content.length; ++i) e.appendChild(content[i]);
+ return e;
+ }
+
+ var range;
+ if (document.createRange) range = function(node, start, end, endNode) {
+ var r = document.createRange();
+ r.setEnd(endNode || node, end);
+ r.setStart(node, start);
+ return r;
+ };
+ else range = function(node, start, end) {
+ var r = document.body.createTextRange();
+ try { r.moveToElementText(node.parentNode); }
+ catch(e) { return r; }
+ r.collapse(true);
+ r.moveEnd("character", end);
+ r.moveStart("character", start);
+ return r;
+ };
+
+ function removeChildren(e) {
+ for (var count = e.childNodes.length; count > 0; --count)
+ e.removeChild(e.firstChild);
+ return e;
+ }
+
+ function removeChildrenAndAdd(parent, e) {
+ return removeChildren(parent).appendChild(e);
+ }
+
+ var contains = CodeMirror.contains = function(parent, child) {
+ if (child.nodeType == 3) // Android browser always returns false when child is a textnode
+ child = child.parentNode;
+ if (parent.contains)
+ return parent.contains(child);
+ do {
+ if (child.nodeType == 11) child = child.host;
+ if (child == parent) return true;
+ } while (child = child.parentNode);
+ };
+
+ function activeElt() {
+ var activeElement = document.activeElement;
+ while (activeElement && activeElement.root && activeElement.root.activeElement)
+ activeElement = activeElement.root.activeElement;
+ return activeElement;
+ }
+ // Older versions of IE throws unspecified error when touching
+ // document.activeElement in some cases (during loading, in iframe)
+ if (ie && ie_version < 11) activeElt = function() {
+ try { return document.activeElement; }
+ catch(e) { return document.body; }
+ };
+
+ function classTest(cls) { return new RegExp("(^|\\s)" + cls + "(?:$|\\s)\\s*"); }
+ var rmClass = CodeMirror.rmClass = function(node, cls) {
+ var current = node.className;
+ var match = classTest(cls).exec(current);
+ if (match) {
+ var after = current.slice(match.index + match[0].length);
+ node.className = current.slice(0, match.index) + (after ? match[1] + after : "");
+ }
+ };
+ var addClass = CodeMirror.addClass = function(node, cls) {
+ var current = node.className;
+ if (!classTest(cls).test(current)) node.className += (current ? " " : "") + cls;
+ };
+ function joinClasses(a, b) {
+ var as = a.split(" ");
+ for (var i = 0; i < as.length; i++)
+ if (as[i] && !classTest(as[i]).test(b)) b += " " + as[i];
+ return b;
+ }
+
+ // WINDOW-WIDE EVENTS
+
+ // These must be handled carefully, because naively registering a
+ // handler for each editor will cause the editors to never be
+ // garbage collected.
+
+ function forEachCodeMirror(f) {
+ if (!document.body.getElementsByClassName) return;
+ var byClass = document.body.getElementsByClassName("CodeMirror");
+ for (var i = 0; i < byClass.length; i++) {
+ var cm = byClass[i].CodeMirror;
+ if (cm) f(cm);
+ }
+ }
+
+ var globalsRegistered = false;
+ function ensureGlobalHandlers() {
+ if (globalsRegistered) return;
+ registerGlobalHandlers();
+ globalsRegistered = true;
+ }
+ function registerGlobalHandlers() {
+ // When the window resizes, we need to refresh active editors.
+ var resizeTimer;
+ on(window, "resize", function() {
+ if (resizeTimer == null) resizeTimer = setTimeout(function() {
+ resizeTimer = null;
+ forEachCodeMirror(onResize);
+ }, 100);
+ });
+ // When the window loses focus, we want to show the editor as blurred
+ on(window, "blur", function() {
+ forEachCodeMirror(onBlur);
+ });
+ }
+
+ // FEATURE DETECTION
+
+ // Detect drag-and-drop
+ var dragAndDrop = function() {
+ // There is *some* kind of drag-and-drop support in IE6-8, but I
+ // couldn't get it to work yet.
+ if (ie && ie_version < 9) return false;
+ var div = elt('div');
+ return "draggable" in div || "dragDrop" in div;
+ }();
+
+ var zwspSupported;
+ function zeroWidthElement(measure) {
+ if (zwspSupported == null) {
+ var test = elt("span", "\u200b");
+ removeChildrenAndAdd(measure, elt("span", [test, document.createTextNode("x")]));
+ if (measure.firstChild.offsetHeight != 0)
+ zwspSupported = test.offsetWidth <= 1 && test.offsetHeight > 2 && !(ie && ie_version < 8);
+ }
+ var node = zwspSupported ? elt("span", "\u200b") :
+ elt("span", "\u00a0", null, "display: inline-block; width: 1px; margin-right: -1px");
+ node.setAttribute("cm-text", "");
+ return node;
+ }
+
+ // Feature-detect IE's crummy client rect reporting for bidi text
+ var badBidiRects;
+ function hasBadBidiRects(measure) {
+ if (badBidiRects != null) return badBidiRects;
+ var txt = removeChildrenAndAdd(measure, document.createTextNode("A\u062eA"));
+ var r0 = range(txt, 0, 1).getBoundingClientRect();
+ if (!r0 || r0.left == r0.right) return false; // Safari returns null in some cases (#2780)
+ var r1 = range(txt, 1, 2).getBoundingClientRect();
+ return badBidiRects = (r1.right - r0.right < 3);
+ }
+
+ // See if "".split is the broken IE version, if so, provide an
+ // alternative way to split lines.
+ var splitLinesAuto = CodeMirror.splitLines = "\n\nb".split(/\n/).length != 3 ? function(string) {
+ var pos = 0, result = [], l = string.length;
+ while (pos <= l) {
+ var nl = string.indexOf("\n", pos);
+ if (nl == -1) nl = string.length;
+ var line = string.slice(pos, string.charAt(nl - 1) == "\r" ? nl - 1 : nl);
+ var rt = line.indexOf("\r");
+ if (rt != -1) {
+ result.push(line.slice(0, rt));
+ pos += rt + 1;
+ } else {
+ result.push(line);
+ pos = nl + 1;
+ }
+ }
+ return result;
+ } : function(string){return string.split(/\r\n?|\n/);};
+
+ var hasSelection = window.getSelection ? function(te) {
+ try { return te.selectionStart != te.selectionEnd; }
+ catch(e) { return false; }
+ } : function(te) {
+ try {var range = te.ownerDocument.selection.createRange();}
+ catch(e) {}
+ if (!range || range.parentElement() != te) return false;
+ return range.compareEndPoints("StartToEnd", range) != 0;
+ };
+
+ var hasCopyEvent = (function() {
+ var e = elt("div");
+ if ("oncopy" in e) return true;
+ e.setAttribute("oncopy", "return;");
+ return typeof e.oncopy == "function";
+ })();
+
+ var badZoomedRects = null;
+ function hasBadZoomedRects(measure) {
+ if (badZoomedRects != null) return badZoomedRects;
+ var node = removeChildrenAndAdd(measure, elt("span", "x"));
+ var normal = node.getBoundingClientRect();
+ var fromRange = range(node, 0, 1).getBoundingClientRect();
+ return badZoomedRects = Math.abs(normal.left - fromRange.left) > 1;
+ }
+
+ // KEY NAMES
+
+ var keyNames = CodeMirror.keyNames = {
+ 3: "Enter", 8: "Backspace", 9: "Tab", 13: "Enter", 16: "Shift", 17: "Ctrl", 18: "Alt",
+ 19: "Pause", 20: "CapsLock", 27: "Esc", 32: "Space", 33: "PageUp", 34: "PageDown", 35: "End",
+ 36: "Home", 37: "Left", 38: "Up", 39: "Right", 40: "Down", 44: "PrintScrn", 45: "Insert",
+ 46: "Delete", 59: ";", 61: "=", 91: "Mod", 92: "Mod", 93: "Mod",
+ 106: "*", 107: "=", 109: "-", 110: ".", 111: "/", 127: "Delete",
+ 173: "-", 186: ";", 187: "=", 188: ",", 189: "-", 190: ".", 191: "/", 192: "`", 219: "[", 220: "\\",
+ 221: "]", 222: "'", 63232: "Up", 63233: "Down", 63234: "Left", 63235: "Right", 63272: "Delete",
+ 63273: "Home", 63275: "End", 63276: "PageUp", 63277: "PageDown", 63302: "Insert"
+ };
+ (function() {
+ // Number keys
+ for (var i = 0; i < 10; i++) keyNames[i + 48] = keyNames[i + 96] = String(i);
+ // Alphabetic keys
+ for (var i = 65; i <= 90; i++) keyNames[i] = String.fromCharCode(i);
+ // Function keys
+ for (var i = 1; i <= 12; i++) keyNames[i + 111] = keyNames[i + 63235] = "F" + i;
+ })();
+
+ // BIDI HELPERS
+
+ function iterateBidiSections(order, from, to, f) {
+ if (!order) return f(from, to, "ltr");
+ var found = false;
+ for (var i = 0; i < order.length; ++i) {
+ var part = order[i];
+ if (part.from < to && part.to > from || from == to && part.to == from) {
+ f(Math.max(part.from, from), Math.min(part.to, to), part.level == 1 ? "rtl" : "ltr");
+ found = true;
+ }
+ }
+ if (!found) f(from, to, "ltr");
+ }
+
+ function bidiLeft(part) { return part.level % 2 ? part.to : part.from; }
+ function bidiRight(part) { return part.level % 2 ? part.from : part.to; }
+
+ function lineLeft(line) { var order = getOrder(line); return order ? bidiLeft(order[0]) : 0; }
+ function lineRight(line) {
+ var order = getOrder(line);
+ if (!order) return line.text.length;
+ return bidiRight(lst(order));
+ }
+
+ function lineStart(cm, lineN) {
+ var line = getLine(cm.doc, lineN);
+ var visual = visualLine(line);
+ if (visual != line) lineN = lineNo(visual);
+ var order = getOrder(visual);
+ var ch = !order ? 0 : order[0].level % 2 ? lineRight(visual) : lineLeft(visual);
+ return Pos(lineN, ch);
+ }
+ function lineEnd(cm, lineN) {
+ var merged, line = getLine(cm.doc, lineN);
+ while (merged = collapsedSpanAtEnd(line)) {
+ line = merged.find(1, true).line;
+ lineN = null;
+ }
+ var order = getOrder(line);
+ var ch = !order ? line.text.length : order[0].level % 2 ? lineLeft(line) : lineRight(line);
+ return Pos(lineN == null ? lineNo(line) : lineN, ch);
+ }
+ function lineStartSmart(cm, pos) {
+ var start = lineStart(cm, pos.line);
+ var line = getLine(cm.doc, start.line);
+ var order = getOrder(line);
+ if (!order || order[0].level == 0) {
+ var firstNonWS = Math.max(0, line.text.search(/\S/));
+ var inWS = pos.line == start.line && pos.ch <= firstNonWS && pos.ch;
+ return Pos(start.line, inWS ? 0 : firstNonWS);
+ }
+ return start;
+ }
+
+ function compareBidiLevel(order, a, b) {
+ var linedir = order[0].level;
+ if (a == linedir) return true;
+ if (b == linedir) return false;
+ return a < b;
+ }
+ var bidiOther;
+ function getBidiPartAt(order, pos) {
+ bidiOther = null;
+ for (var i = 0, found; i < order.length; ++i) {
+ var cur = order[i];
+ if (cur.from < pos && cur.to > pos) return i;
+ if ((cur.from == pos || cur.to == pos)) {
+ if (found == null) {
+ found = i;
+ } else if (compareBidiLevel(order, cur.level, order[found].level)) {
+ if (cur.from != cur.to) bidiOther = found;
+ return i;
+ } else {
+ if (cur.from != cur.to) bidiOther = i;
+ return found;
+ }
+ }
+ }
+ return found;
+ }
+
+ function moveInLine(line, pos, dir, byUnit) {
+ if (!byUnit) return pos + dir;
+ do pos += dir;
+ while (pos > 0 && isExtendingChar(line.text.charAt(pos)));
+ return pos;
+ }
+
+ // This is needed in order to move 'visually' through bi-directional
+ // text -- i.e., pressing left should make the cursor go left, even
+ // when in RTL text. The tricky part is the 'jumps', where RTL and
+ // LTR text touch each other. This often requires the cursor offset
+ // to move more than one unit, in order to visually move one unit.
+ function moveVisually(line, start, dir, byUnit) {
+ var bidi = getOrder(line);
+ if (!bidi) return moveLogically(line, start, dir, byUnit);
+ var pos = getBidiPartAt(bidi, start), part = bidi[pos];
+ var target = moveInLine(line, start, part.level % 2 ? -dir : dir, byUnit);
+
+ for (;;) {
+ if (target > part.from && target < part.to) return target;
+ if (target == part.from || target == part.to) {
+ if (getBidiPartAt(bidi, target) == pos) return target;
+ part = bidi[pos += dir];
+ return (dir > 0) == part.level % 2 ? part.to : part.from;
+ } else {
+ part = bidi[pos += dir];
+ if (!part) return null;
+ if ((dir > 0) == part.level % 2)
+ target = moveInLine(line, part.to, -1, byUnit);
+ else
+ target = moveInLine(line, part.from, 1, byUnit);
+ }
+ }
+ }
+
+ function moveLogically(line, start, dir, byUnit) {
+ var target = start + dir;
+ if (byUnit) while (target > 0 && isExtendingChar(line.text.charAt(target))) target += dir;
+ return target < 0 || target > line.text.length ? null : target;
+ }
+
+ // Bidirectional ordering algorithm
+ // See http://unicode.org/reports/tr9/tr9-13.html for the algorithm
+ // that this (partially) implements.
+
+ // One-char codes used for character types:
+ // L (L): Left-to-Right
+ // R (R): Right-to-Left
+ // r (AL): Right-to-Left Arabic
+ // 1 (EN): European Number
+ // + (ES): European Number Separator
+ // % (ET): European Number Terminator
+ // n (AN): Arabic Number
+ // , (CS): Common Number Separator
+ // m (NSM): Non-Spacing Mark
+ // b (BN): Boundary Neutral
+ // s (B): Paragraph Separator
+ // t (S): Segment Separator
+ // w (WS): Whitespace
+ // N (ON): Other Neutrals
+
+ // Returns null if characters are ordered as they appear
+ // (left-to-right), or an array of sections ({from, to, level}
+ // objects) in the order in which they occur visually.
+ var bidiOrdering = (function() {
+ // Character types for codepoints 0 to 0xff
+ var lowTypes = "bbbbbbbbbtstwsbbbbbbbbbbbbbbssstwNN%%%NNNNNN,N,N1111111111NNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNbbbbbbsbbbbbbbbbbbbbbbbbbbbbbbbbb,N%%%%NNNNLNNNNN%%11NLNNN1LNNNNNLLLLLLLLLLLLLLLLLLLLLLLNLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLN";
+ // Character types for codepoints 0x600 to 0x6ff
+ var arabicTypes = "rrrrrrrrrrrr,rNNmmmmmmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmrrrrrrrnnnnnnnnnn%nnrrrmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmmmmmmNmmmm";
+ function charType(code) {
+ if (code <= 0xf7) return lowTypes.charAt(code);
+ else if (0x590 <= code && code <= 0x5f4) return "R";
+ else if (0x600 <= code && code <= 0x6ed) return arabicTypes.charAt(code - 0x600);
+ else if (0x6ee <= code && code <= 0x8ac) return "r";
+ else if (0x2000 <= code && code <= 0x200b) return "w";
+ else if (code == 0x200c) return "b";
+ else return "L";
+ }
+
+ var bidiRE = /[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/;
+ var isNeutral = /[stwN]/, isStrong = /[LRr]/, countsAsLeft = /[Lb1n]/, countsAsNum = /[1n]/;
+ // Browsers seem to always treat the boundaries of block elements as being L.
+ var outerType = "L";
+
+ function BidiSpan(level, from, to) {
+ this.level = level;
+ this.from = from; this.to = to;
+ }
+
+ return function(str) {
+ if (!bidiRE.test(str)) return false;
+ var len = str.length, types = [];
+ for (var i = 0, type; i < len; ++i)
+ types.push(type = charType(str.charCodeAt(i)));
+
+ // W1. Examine each non-spacing mark (NSM) in the level run, and
+ // change the type of the NSM to the type of the previous
+ // character. If the NSM is at the start of the level run, it will
+ // get the type of sor.
+ for (var i = 0, prev = outerType; i < len; ++i) {
+ var type = types[i];
+ if (type == "m") types[i] = prev;
+ else prev = type;
+ }
+
+ // W2. Search backwards from each instance of a European number
+ // until the first strong type (R, L, AL, or sor) is found. If an
+ // AL is found, change the type of the European number to Arabic
+ // number.
+ // W3. Change all ALs to R.
+ for (var i = 0, cur = outerType; i < len; ++i) {
+ var type = types[i];
+ if (type == "1" && cur == "r") types[i] = "n";
+ else if (isStrong.test(type)) { cur = type; if (type == "r") types[i] = "R"; }
+ }
+
+ // W4. A single European separator between two European numbers
+ // changes to a European number. A single common separator between
+ // two numbers of the same type changes to that type.
+ for (var i = 1, prev = types[0]; i < len - 1; ++i) {
+ var type = types[i];
+ if (type == "+" && prev == "1" && types[i+1] == "1") types[i] = "1";
+ else if (type == "," && prev == types[i+1] &&
+ (prev == "1" || prev == "n")) types[i] = prev;
+ prev = type;
+ }
+
+ // W5. A sequence of European terminators adjacent to European
+ // numbers changes to all European numbers.
+ // W6. Otherwise, separators and terminators change to Other
+ // Neutral.
+ for (var i = 0; i < len; ++i) {
+ var type = types[i];
+ if (type == ",") types[i] = "N";
+ else if (type == "%") {
+ for (var end = i + 1; end < len && types[end] == "%"; ++end) {}
+ var replace = (i && types[i-1] == "!") || (end < len && types[end] == "1") ? "1" : "N";
+ for (var j = i; j < end; ++j) types[j] = replace;
+ i = end - 1;
+ }
+ }
+
+ // W7. Search backwards from each instance of a European number
+ // until the first strong type (R, L, or sor) is found. If an L is
+ // found, then change the type of the European number to L.
+ for (var i = 0, cur = outerType; i < len; ++i) {
+ var type = types[i];
+ if (cur == "L" && type == "1") types[i] = "L";
+ else if (isStrong.test(type)) cur = type;
+ }
+
+ // N1. A sequence of neutrals takes the direction of the
+ // surrounding strong text if the text on both sides has the same
+ // direction. European and Arabic numbers act as if they were R in
+ // terms of their influence on neutrals. Start-of-level-run (sor)
+ // and end-of-level-run (eor) are used at level run boundaries.
+ // N2. Any remaining neutrals take the embedding direction.
+ for (var i = 0; i < len; ++i) {
+ if (isNeutral.test(types[i])) {
+ for (var end = i + 1; end < len && isNeutral.test(types[end]); ++end) {}
+ var before = (i ? types[i-1] : outerType) == "L";
+ var after = (end < len ? types[end] : outerType) == "L";
+ var replace = before || after ? "L" : "R";
+ for (var j = i; j < end; ++j) types[j] = replace;
+ i = end - 1;
+ }
+ }
+
+ // Here we depart from the documented algorithm, in order to avoid
+ // building up an actual levels array. Since there are only three
+ // levels (0, 1, 2) in an implementation that doesn't take
+ // explicit embedding into account, we can build up the order on
+ // the fly, without following the level-based algorithm.
+ var order = [], m;
+ for (var i = 0; i < len;) {
+ if (countsAsLeft.test(types[i])) {
+ var start = i;
+ for (++i; i < len && countsAsLeft.test(types[i]); ++i) {}
+ order.push(new BidiSpan(0, start, i));
+ } else {
+ var pos = i, at = order.length;
+ for (++i; i < len && types[i] != "L"; ++i) {}
+ for (var j = pos; j < i;) {
+ if (countsAsNum.test(types[j])) {
+ if (pos < j) order.splice(at, 0, new BidiSpan(1, pos, j));
+ var nstart = j;
+ for (++j; j < i && countsAsNum.test(types[j]); ++j) {}
+ order.splice(at, 0, new BidiSpan(2, nstart, j));
+ pos = j;
+ } else ++j;
+ }
+ if (pos < i) order.splice(at, 0, new BidiSpan(1, pos, i));
+ }
+ }
+ if (order[0].level == 1 && (m = str.match(/^\s+/))) {
+ order[0].from = m[0].length;
+ order.unshift(new BidiSpan(0, 0, m[0].length));
+ }
+ if (lst(order).level == 1 && (m = str.match(/\s+$/))) {
+ lst(order).to -= m[0].length;
+ order.push(new BidiSpan(0, len - m[0].length, len));
+ }
+ if (order[0].level == 2)
+ order.unshift(new BidiSpan(1, order[0].to, order[0].to));
+ if (order[0].level != lst(order).level)
+ order.push(new BidiSpan(order[0].level, len, len));
+
+ return order;
+ };
+ })();
+
+ // THE END
+
+ CodeMirror.version = "5.16.0";
+
+ return CodeMirror;
+});
diff --git a/devtools/client/sourceeditor/codemirror/mode/clike/clike.js b/devtools/client/sourceeditor/codemirror/mode/clike/clike.js
new file mode 100644
index 000000000..a37921fda
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/mode/clike/clike.js
@@ -0,0 +1,786 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../../lib/codemirror"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function(CodeMirror) {
+"use strict";
+
+function Context(indented, column, type, info, align, prev) {
+ this.indented = indented;
+ this.column = column;
+ this.type = type;
+ this.info = info;
+ this.align = align;
+ this.prev = prev;
+}
+function pushContext(state, col, type, info) {
+ var indent = state.indented;
+ if (state.context && state.context.type != "statement" && type != "statement")
+ indent = state.context.indented;
+ return state.context = new Context(indent, col, type, info, null, state.context);
+}
+function popContext(state) {
+ var t = state.context.type;
+ if (t == ")" || t == "]" || t == "}")
+ state.indented = state.context.indented;
+ return state.context = state.context.prev;
+}
+
+function typeBefore(stream, state, pos) {
+ if (state.prevToken == "variable" || state.prevToken == "variable-3") return true;
+ if (/\S(?:[^- ]>|[*\]])\s*$|\*$/.test(stream.string.slice(0, pos))) return true;
+ if (state.typeAtEndOfLine && stream.column() == stream.indentation()) return true;
+}
+
+function isTopScope(context) {
+ for (;;) {
+ if (!context || context.type == "top") return true;
+ if (context.type == "}" && context.prev.info != "namespace") return false;
+ context = context.prev;
+ }
+}
+
+CodeMirror.defineMode("clike", function(config, parserConfig) {
+ var indentUnit = config.indentUnit,
+ statementIndentUnit = parserConfig.statementIndentUnit || indentUnit,
+ dontAlignCalls = parserConfig.dontAlignCalls,
+ keywords = parserConfig.keywords || {},
+ types = parserConfig.types || {},
+ builtin = parserConfig.builtin || {},
+ blockKeywords = parserConfig.blockKeywords || {},
+ defKeywords = parserConfig.defKeywords || {},
+ atoms = parserConfig.atoms || {},
+ hooks = parserConfig.hooks || {},
+ multiLineStrings = parserConfig.multiLineStrings,
+ indentStatements = parserConfig.indentStatements !== false,
+ indentSwitch = parserConfig.indentSwitch !== false,
+ namespaceSeparator = parserConfig.namespaceSeparator,
+ isPunctuationChar = parserConfig.isPunctuationChar || /[\[\]{}\(\),;\:\.]/,
+ numberStart = parserConfig.numberStart || /[\d\.]/,
+ number = parserConfig.number || /^(?:0x[a-f\d]+|0b[01]+|(?:\d+\.?\d*|\.\d+)(?:e[-+]?\d+)?)(u|ll?|l|f)?/i,
+ isOperatorChar = parserConfig.isOperatorChar || /[+\-*&%=<>!?|\/]/,
+ endStatement = parserConfig.endStatement || /^[;:,]$/;
+
+ var curPunc, isDefKeyword;
+
+ function tokenBase(stream, state) {
+ var ch = stream.next();
+ if (hooks[ch]) {
+ var result = hooks[ch](stream, state);
+ if (result !== false) return result;
+ }
+ if (ch == '"' || ch == "'") {
+ state.tokenize = tokenString(ch);
+ return state.tokenize(stream, state);
+ }
+ if (isPunctuationChar.test(ch)) {
+ curPunc = ch;
+ return null;
+ }
+ if (numberStart.test(ch)) {
+ stream.backUp(1)
+ if (stream.match(number)) return "number"
+ stream.next()
+ }
+ if (ch == "/") {
+ if (stream.eat("*")) {
+ state.tokenize = tokenComment;
+ return tokenComment(stream, state);
+ }
+ if (stream.eat("/")) {
+ stream.skipToEnd();
+ return "comment";
+ }
+ }
+ if (isOperatorChar.test(ch)) {
+ while (!stream.match(/^\/[\/*]/, false) && stream.eat(isOperatorChar)) {}
+ return "operator";
+ }
+ stream.eatWhile(/[\w\$_\xa1-\uffff]/);
+ if (namespaceSeparator) while (stream.match(namespaceSeparator))
+ stream.eatWhile(/[\w\$_\xa1-\uffff]/);
+
+ var cur = stream.current();
+ if (contains(keywords, cur)) {
+ if (contains(blockKeywords, cur)) curPunc = "newstatement";
+ if (contains(defKeywords, cur)) isDefKeyword = true;
+ return "keyword";
+ }
+ if (contains(types, cur)) return "variable-3";
+ if (contains(builtin, cur)) {
+ if (contains(blockKeywords, cur)) curPunc = "newstatement";
+ return "builtin";
+ }
+ if (contains(atoms, cur)) return "atom";
+ return "variable";
+ }
+
+ function tokenString(quote) {
+ return function(stream, state) {
+ var escaped = false, next, end = false;
+ while ((next = stream.next()) != null) {
+ if (next == quote && !escaped) {end = true; break;}
+ escaped = !escaped && next == "\\";
+ }
+ if (end || !(escaped || multiLineStrings))
+ state.tokenize = null;
+ return "string";
+ };
+ }
+
+ function tokenComment(stream, state) {
+ var maybeEnd = false, ch;
+ while (ch = stream.next()) {
+ if (ch == "/" && maybeEnd) {
+ state.tokenize = null;
+ break;
+ }
+ maybeEnd = (ch == "*");
+ }
+ return "comment";
+ }
+
+ function maybeEOL(stream, state) {
+ if (parserConfig.typeFirstDefinitions && stream.eol() && isTopScope(state.context))
+ state.typeAtEndOfLine = typeBefore(stream, state, stream.pos)
+ }
+
+ // Interface
+
+ return {
+ startState: function(basecolumn) {
+ return {
+ tokenize: null,
+ context: new Context((basecolumn || 0) - indentUnit, 0, "top", null, false),
+ indented: 0,
+ startOfLine: true,
+ prevToken: null
+ };
+ },
+
+ token: function(stream, state) {
+ var ctx = state.context;
+ if (stream.sol()) {
+ if (ctx.align == null) ctx.align = false;
+ state.indented = stream.indentation();
+ state.startOfLine = true;
+ }
+ if (stream.eatSpace()) { maybeEOL(stream, state); return null; }
+ curPunc = isDefKeyword = null;
+ var style = (state.tokenize || tokenBase)(stream, state);
+ if (style == "comment" || style == "meta") return style;
+ if (ctx.align == null) ctx.align = true;
+
+ if (endStatement.test(curPunc)) while (state.context.type == "statement") popContext(state);
+ else if (curPunc == "{") pushContext(state, stream.column(), "}");
+ else if (curPunc == "[") pushContext(state, stream.column(), "]");
+ else if (curPunc == "(") pushContext(state, stream.column(), ")");
+ else if (curPunc == "}") {
+ while (ctx.type == "statement") ctx = popContext(state);
+ if (ctx.type == "}") ctx = popContext(state);
+ while (ctx.type == "statement") ctx = popContext(state);
+ }
+ else if (curPunc == ctx.type) popContext(state);
+ else if (indentStatements &&
+ (((ctx.type == "}" || ctx.type == "top") && curPunc != ";") ||
+ (ctx.type == "statement" && curPunc == "newstatement"))) {
+ pushContext(state, stream.column(), "statement", stream.current());
+ }
+
+ if (style == "variable" &&
+ ((state.prevToken == "def" ||
+ (parserConfig.typeFirstDefinitions && typeBefore(stream, state, stream.start) &&
+ isTopScope(state.context) && stream.match(/^\s*\(/, false)))))
+ style = "def";
+
+ if (hooks.token) {
+ var result = hooks.token(stream, state, style);
+ if (result !== undefined) style = result;
+ }
+
+ if (style == "def" && parserConfig.styleDefs === false) style = "variable";
+
+ state.startOfLine = false;
+ state.prevToken = isDefKeyword ? "def" : style || curPunc;
+ maybeEOL(stream, state);
+ return style;
+ },
+
+ indent: function(state, textAfter) {
+ if (state.tokenize != tokenBase && state.tokenize != null || state.typeAtEndOfLine) return CodeMirror.Pass;
+ var ctx = state.context, firstChar = textAfter && textAfter.charAt(0);
+ if (ctx.type == "statement" && firstChar == "}") ctx = ctx.prev;
+ if (parserConfig.dontIndentStatements)
+ while (ctx.type == "statement" && parserConfig.dontIndentStatements.test(ctx.info))
+ ctx = ctx.prev
+ if (hooks.indent) {
+ var hook = hooks.indent(state, ctx, textAfter);
+ if (typeof hook == "number") return hook
+ }
+ var closing = firstChar == ctx.type;
+ var switchBlock = ctx.prev && ctx.prev.info == "switch";
+ if (parserConfig.allmanIndentation && /[{(]/.test(firstChar)) {
+ while (ctx.type != "top" && ctx.type != "}") ctx = ctx.prev
+ return ctx.indented
+ }
+ if (ctx.type == "statement")
+ return ctx.indented + (firstChar == "{" ? 0 : statementIndentUnit);
+ if (ctx.align && (!dontAlignCalls || ctx.type != ")"))
+ return ctx.column + (closing ? 0 : 1);
+ if (ctx.type == ")" && !closing)
+ return ctx.indented + statementIndentUnit;
+
+ return ctx.indented + (closing ? 0 : indentUnit) +
+ (!closing && switchBlock && !/^(?:case|default)\b/.test(textAfter) ? indentUnit : 0);
+ },
+
+ electricInput: indentSwitch ? /^\s*(?:case .*?:|default:|\{\}?|\})$/ : /^\s*[{}]$/,
+ blockCommentStart: "/*",
+ blockCommentEnd: "*/",
+ lineComment: "//",
+ fold: "brace"
+ };
+});
+
+ function words(str) {
+ var obj = {}, words = str.split(" ");
+ for (var i = 0; i < words.length; ++i) obj[words[i]] = true;
+ return obj;
+ }
+ function contains(words, word) {
+ if (typeof words === "function") {
+ return words(word);
+ } else {
+ return words.propertyIsEnumerable(word);
+ }
+ }
+ var cKeywords = "auto if break case register continue return default do sizeof " +
+ "static else struct switch extern typedef union for goto while enum const volatile";
+ var cTypes = "int long char short double float unsigned signed void size_t ptrdiff_t";
+
+ function cppHook(stream, state) {
+ if (!state.startOfLine) return false
+ for (var ch, next = null; ch = stream.peek();) {
+ if (ch == "\\" && stream.match(/^.$/)) {
+ next = cppHook
+ break
+ } else if (ch == "/" && stream.match(/^\/[\/\*]/, false)) {
+ break
+ }
+ stream.next()
+ }
+ state.tokenize = next
+ return "meta"
+ }
+
+ function pointerHook(_stream, state) {
+ if (state.prevToken == "variable-3") return "variable-3";
+ return false;
+ }
+
+ function cpp14Literal(stream) {
+ stream.eatWhile(/[\w\.']/);
+ return "number";
+ }
+
+ function cpp11StringHook(stream, state) {
+ stream.backUp(1);
+ // Raw strings.
+ if (stream.match(/(R|u8R|uR|UR|LR)/)) {
+ var match = stream.match(/"([^\s\\()]{0,16})\(/);
+ if (!match) {
+ return false;
+ }
+ state.cpp11RawStringDelim = match[1];
+ state.tokenize = tokenRawString;
+ return tokenRawString(stream, state);
+ }
+ // Unicode strings/chars.
+ if (stream.match(/(u8|u|U|L)/)) {
+ if (stream.match(/["']/, /* eat */ false)) {
+ return "string";
+ }
+ return false;
+ }
+ // Ignore this hook.
+ stream.next();
+ return false;
+ }
+
+ function cppLooksLikeConstructor(word) {
+ var lastTwo = /(\w+)::(\w+)$/.exec(word);
+ return lastTwo && lastTwo[1] == lastTwo[2];
+ }
+
+ // C#-style strings where "" escapes a quote.
+ function tokenAtString(stream, state) {
+ var next;
+ while ((next = stream.next()) != null) {
+ if (next == '"' && !stream.eat('"')) {
+ state.tokenize = null;
+ break;
+ }
+ }
+ return "string";
+ }
+
+ // C++11 raw string literal is <prefix>"<delim>( anything )<delim>", where
+ // <delim> can be a string up to 16 characters long.
+ function tokenRawString(stream, state) {
+ // Escape characters that have special regex meanings.
+ var delim = state.cpp11RawStringDelim.replace(/[^\w\s]/g, '\\$&');
+ var match = stream.match(new RegExp(".*?\\)" + delim + '"'));
+ if (match)
+ state.tokenize = null;
+ else
+ stream.skipToEnd();
+ return "string";
+ }
+
+ function def(mimes, mode) {
+ if (typeof mimes == "string") mimes = [mimes];
+ var words = [];
+ function add(obj) {
+ if (obj) for (var prop in obj) if (obj.hasOwnProperty(prop))
+ words.push(prop);
+ }
+ add(mode.keywords);
+ add(mode.types);
+ add(mode.builtin);
+ add(mode.atoms);
+ if (words.length) {
+ mode.helperType = mimes[0];
+ CodeMirror.registerHelper("hintWords", mimes[0], words);
+ }
+
+ for (var i = 0; i < mimes.length; ++i)
+ CodeMirror.defineMIME(mimes[i], mode);
+ }
+
+ def(["text/x-csrc", "text/x-c", "text/x-chdr"], {
+ name: "clike",
+ keywords: words(cKeywords),
+ types: words(cTypes + " bool _Complex _Bool float_t double_t intptr_t intmax_t " +
+ "int8_t int16_t int32_t int64_t uintptr_t uintmax_t uint8_t uint16_t " +
+ "uint32_t uint64_t"),
+ blockKeywords: words("case do else for if switch while struct"),
+ defKeywords: words("struct"),
+ typeFirstDefinitions: true,
+ atoms: words("null true false"),
+ hooks: {"#": cppHook, "*": pointerHook},
+ modeProps: {fold: ["brace", "include"]}
+ });
+
+ def(["text/x-c++src", "text/x-c++hdr"], {
+ name: "clike",
+ keywords: words(cKeywords + " asm dynamic_cast namespace reinterpret_cast try explicit new " +
+ "static_cast typeid catch operator template typename class friend private " +
+ "this using const_cast inline public throw virtual delete mutable protected " +
+ "alignas alignof constexpr decltype nullptr noexcept thread_local final " +
+ "static_assert override"),
+ types: words(cTypes + " bool wchar_t"),
+ blockKeywords: words("catch class do else finally for if struct switch try while"),
+ defKeywords: words("class namespace struct enum union"),
+ typeFirstDefinitions: true,
+ atoms: words("true false null"),
+ dontIndentStatements: /^template$/,
+ hooks: {
+ "#": cppHook,
+ "*": pointerHook,
+ "u": cpp11StringHook,
+ "U": cpp11StringHook,
+ "L": cpp11StringHook,
+ "R": cpp11StringHook,
+ "0": cpp14Literal,
+ "1": cpp14Literal,
+ "2": cpp14Literal,
+ "3": cpp14Literal,
+ "4": cpp14Literal,
+ "5": cpp14Literal,
+ "6": cpp14Literal,
+ "7": cpp14Literal,
+ "8": cpp14Literal,
+ "9": cpp14Literal,
+ token: function(stream, state, style) {
+ if (style == "variable" && stream.peek() == "(" &&
+ (state.prevToken == ";" || state.prevToken == null ||
+ state.prevToken == "}") &&
+ cppLooksLikeConstructor(stream.current()))
+ return "def";
+ }
+ },
+ namespaceSeparator: "::",
+ modeProps: {fold: ["brace", "include"]}
+ });
+
+ def("text/x-java", {
+ name: "clike",
+ keywords: words("abstract assert break case catch class const continue default " +
+ "do else enum extends final finally float for goto if implements import " +
+ "instanceof interface native new package private protected public " +
+ "return static strictfp super switch synchronized this throw throws transient " +
+ "try volatile while"),
+ types: words("byte short int long float double boolean char void Boolean Byte Character Double Float " +
+ "Integer Long Number Object Short String StringBuffer StringBuilder Void"),
+ blockKeywords: words("catch class do else finally for if switch try while"),
+ defKeywords: words("class interface package enum"),
+ typeFirstDefinitions: true,
+ atoms: words("true false null"),
+ endStatement: /^[;:]$/,
+ number: /^(?:0x[a-f\d_]+|0b[01_]+|(?:[\d_]+\.?\d*|\.\d+)(?:e[-+]?[\d_]+)?)(u|ll?|l|f)?/i,
+ hooks: {
+ "@": function(stream) {
+ stream.eatWhile(/[\w\$_]/);
+ return "meta";
+ }
+ },
+ modeProps: {fold: ["brace", "import"]}
+ });
+
+ def("text/x-csharp", {
+ name: "clike",
+ keywords: words("abstract as async await base break case catch checked class const continue" +
+ " default delegate do else enum event explicit extern finally fixed for" +
+ " foreach goto if implicit in interface internal is lock namespace new" +
+ " operator out override params private protected public readonly ref return sealed" +
+ " sizeof stackalloc static struct switch this throw try typeof unchecked" +
+ " unsafe using virtual void volatile while add alias ascending descending dynamic from get" +
+ " global group into join let orderby partial remove select set value var yield"),
+ types: words("Action Boolean Byte Char DateTime DateTimeOffset Decimal Double Func" +
+ " Guid Int16 Int32 Int64 Object SByte Single String Task TimeSpan UInt16 UInt32" +
+ " UInt64 bool byte char decimal double short int long object" +
+ " sbyte float string ushort uint ulong"),
+ blockKeywords: words("catch class do else finally for foreach if struct switch try while"),
+ defKeywords: words("class interface namespace struct var"),
+ typeFirstDefinitions: true,
+ atoms: words("true false null"),
+ hooks: {
+ "@": function(stream, state) {
+ if (stream.eat('"')) {
+ state.tokenize = tokenAtString;
+ return tokenAtString(stream, state);
+ }
+ stream.eatWhile(/[\w\$_]/);
+ return "meta";
+ }
+ }
+ });
+
+ function tokenTripleString(stream, state) {
+ var escaped = false;
+ while (!stream.eol()) {
+ if (!escaped && stream.match('"""')) {
+ state.tokenize = null;
+ break;
+ }
+ escaped = stream.next() == "\\" && !escaped;
+ }
+ return "string";
+ }
+
+ def("text/x-scala", {
+ name: "clike",
+ keywords: words(
+
+ /* scala */
+ "abstract case catch class def do else extends final finally for forSome if " +
+ "implicit import lazy match new null object override package private protected return " +
+ "sealed super this throw trait try type val var while with yield _ : = => <- <: " +
+ "<% >: # @ " +
+
+ /* package scala */
+ "assert assume require print println printf readLine readBoolean readByte readShort " +
+ "readChar readInt readLong readFloat readDouble " +
+
+ ":: #:: "
+ ),
+ types: words(
+ "AnyVal App Application Array BufferedIterator BigDecimal BigInt Char Console Either " +
+ "Enumeration Equiv Error Exception Fractional Function IndexedSeq Int Integral Iterable " +
+ "Iterator List Map Numeric Nil NotNull Option Ordered Ordering PartialFunction PartialOrdering " +
+ "Product Proxy Range Responder Seq Serializable Set Specializable Stream StringBuilder " +
+ "StringContext Symbol Throwable Traversable TraversableOnce Tuple Unit Vector " +
+
+ /* package java.lang */
+ "Boolean Byte Character CharSequence Class ClassLoader Cloneable Comparable " +
+ "Compiler Double Exception Float Integer Long Math Number Object Package Pair Process " +
+ "Runtime Runnable SecurityManager Short StackTraceElement StrictMath String " +
+ "StringBuffer System Thread ThreadGroup ThreadLocal Throwable Triple Void"
+ ),
+ multiLineStrings: true,
+ blockKeywords: words("catch class do else finally for forSome if match switch try while"),
+ defKeywords: words("class def object package trait type val var"),
+ atoms: words("true false null"),
+ indentStatements: false,
+ indentSwitch: false,
+ hooks: {
+ "@": function(stream) {
+ stream.eatWhile(/[\w\$_]/);
+ return "meta";
+ },
+ '"': function(stream, state) {
+ if (!stream.match('""')) return false;
+ state.tokenize = tokenTripleString;
+ return state.tokenize(stream, state);
+ },
+ "'": function(stream) {
+ stream.eatWhile(/[\w\$_\xa1-\uffff]/);
+ return "atom";
+ },
+ "=": function(stream, state) {
+ var cx = state.context
+ if (cx.type == "}" && cx.align && stream.eat(">")) {
+ state.context = new Context(cx.indented, cx.column, cx.type, cx.info, null, cx.prev)
+ return "operator"
+ } else {
+ return false
+ }
+ }
+ },
+ modeProps: {closeBrackets: {triples: '"'}}
+ });
+
+ function tokenKotlinString(tripleString){
+ return function (stream, state) {
+ var escaped = false, next, end = false;
+ while (!stream.eol()) {
+ if (!tripleString && !escaped && stream.match('"') ) {end = true; break;}
+ if (tripleString && stream.match('"""')) {end = true; break;}
+ next = stream.next();
+ if(!escaped && next == "$" && stream.match('{'))
+ stream.skipTo("}");
+ escaped = !escaped && next == "\\" && !tripleString;
+ }
+ if (end || !tripleString)
+ state.tokenize = null;
+ return "string";
+ }
+ }
+
+ def("text/x-kotlin", {
+ name: "clike",
+ keywords: words(
+ /*keywords*/
+ "package as typealias class interface this super val " +
+ "var fun for is in This throw return " +
+ "break continue object if else while do try when !in !is as? " +
+
+ /*soft keywords*/
+ "file import where by get set abstract enum open inner override private public internal " +
+ "protected catch finally out final vararg reified dynamic companion constructor init " +
+ "sealed field property receiver param sparam lateinit data inline noinline tailrec " +
+ "external annotation crossinline const operator infix"
+ ),
+ types: words(
+ /* package java.lang */
+ "Boolean Byte Character CharSequence Class ClassLoader Cloneable Comparable " +
+ "Compiler Double Exception Float Integer Long Math Number Object Package Pair Process " +
+ "Runtime Runnable SecurityManager Short StackTraceElement StrictMath String " +
+ "StringBuffer System Thread ThreadGroup ThreadLocal Throwable Triple Void"
+ ),
+ intendSwitch: false,
+ indentStatements: false,
+ multiLineStrings: true,
+ blockKeywords: words("catch class do else finally for if where try while enum"),
+ defKeywords: words("class val var object package interface fun"),
+ atoms: words("true false null this"),
+ hooks: {
+ '"': function(stream, state) {
+ state.tokenize = tokenKotlinString(stream.match('""'));
+ return state.tokenize(stream, state);
+ }
+ },
+ modeProps: {closeBrackets: {triples: '"'}}
+ });
+
+ def(["x-shader/x-vertex", "x-shader/x-fragment"], {
+ name: "clike",
+ keywords: words("sampler1D sampler2D sampler3D samplerCube " +
+ "sampler1DShadow sampler2DShadow " +
+ "const attribute uniform varying " +
+ "break continue discard return " +
+ "for while do if else struct " +
+ "in out inout"),
+ types: words("float int bool void " +
+ "vec2 vec3 vec4 ivec2 ivec3 ivec4 bvec2 bvec3 bvec4 " +
+ "mat2 mat3 mat4"),
+ blockKeywords: words("for while do if else struct"),
+ builtin: words("radians degrees sin cos tan asin acos atan " +
+ "pow exp log exp2 sqrt inversesqrt " +
+ "abs sign floor ceil fract mod min max clamp mix step smoothstep " +
+ "length distance dot cross normalize ftransform faceforward " +
+ "reflect refract matrixCompMult " +
+ "lessThan lessThanEqual greaterThan greaterThanEqual " +
+ "equal notEqual any all not " +
+ "texture1D texture1DProj texture1DLod texture1DProjLod " +
+ "texture2D texture2DProj texture2DLod texture2DProjLod " +
+ "texture3D texture3DProj texture3DLod texture3DProjLod " +
+ "textureCube textureCubeLod " +
+ "shadow1D shadow2D shadow1DProj shadow2DProj " +
+ "shadow1DLod shadow2DLod shadow1DProjLod shadow2DProjLod " +
+ "dFdx dFdy fwidth " +
+ "noise1 noise2 noise3 noise4"),
+ atoms: words("true false " +
+ "gl_FragColor gl_SecondaryColor gl_Normal gl_Vertex " +
+ "gl_MultiTexCoord0 gl_MultiTexCoord1 gl_MultiTexCoord2 gl_MultiTexCoord3 " +
+ "gl_MultiTexCoord4 gl_MultiTexCoord5 gl_MultiTexCoord6 gl_MultiTexCoord7 " +
+ "gl_FogCoord gl_PointCoord " +
+ "gl_Position gl_PointSize gl_ClipVertex " +
+ "gl_FrontColor gl_BackColor gl_FrontSecondaryColor gl_BackSecondaryColor " +
+ "gl_TexCoord gl_FogFragCoord " +
+ "gl_FragCoord gl_FrontFacing " +
+ "gl_FragData gl_FragDepth " +
+ "gl_ModelViewMatrix gl_ProjectionMatrix gl_ModelViewProjectionMatrix " +
+ "gl_TextureMatrix gl_NormalMatrix gl_ModelViewMatrixInverse " +
+ "gl_ProjectionMatrixInverse gl_ModelViewProjectionMatrixInverse " +
+ "gl_TexureMatrixTranspose gl_ModelViewMatrixInverseTranspose " +
+ "gl_ProjectionMatrixInverseTranspose " +
+ "gl_ModelViewProjectionMatrixInverseTranspose " +
+ "gl_TextureMatrixInverseTranspose " +
+ "gl_NormalScale gl_DepthRange gl_ClipPlane " +
+ "gl_Point gl_FrontMaterial gl_BackMaterial gl_LightSource gl_LightModel " +
+ "gl_FrontLightModelProduct gl_BackLightModelProduct " +
+ "gl_TextureColor gl_EyePlaneS gl_EyePlaneT gl_EyePlaneR gl_EyePlaneQ " +
+ "gl_FogParameters " +
+ "gl_MaxLights gl_MaxClipPlanes gl_MaxTextureUnits gl_MaxTextureCoords " +
+ "gl_MaxVertexAttribs gl_MaxVertexUniformComponents gl_MaxVaryingFloats " +
+ "gl_MaxVertexTextureImageUnits gl_MaxTextureImageUnits " +
+ "gl_MaxFragmentUniformComponents gl_MaxCombineTextureImageUnits " +
+ "gl_MaxDrawBuffers"),
+ indentSwitch: false,
+ hooks: {"#": cppHook},
+ modeProps: {fold: ["brace", "include"]}
+ });
+
+ def("text/x-nesc", {
+ name: "clike",
+ keywords: words(cKeywords + "as atomic async call command component components configuration event generic " +
+ "implementation includes interface module new norace nx_struct nx_union post provides " +
+ "signal task uses abstract extends"),
+ types: words(cTypes),
+ blockKeywords: words("case do else for if switch while struct"),
+ atoms: words("null true false"),
+ hooks: {"#": cppHook},
+ modeProps: {fold: ["brace", "include"]}
+ });
+
+ def("text/x-objectivec", {
+ name: "clike",
+ keywords: words(cKeywords + "inline restrict _Bool _Complex _Imaginary BOOL Class bycopy byref id IMP in " +
+ "inout nil oneway out Protocol SEL self super atomic nonatomic retain copy readwrite readonly"),
+ types: words(cTypes),
+ atoms: words("YES NO NULL NILL ON OFF true false"),
+ hooks: {
+ "@": function(stream) {
+ stream.eatWhile(/[\w\$]/);
+ return "keyword";
+ },
+ "#": cppHook,
+ indent: function(_state, ctx, textAfter) {
+ if (ctx.type == "statement" && /^@\w/.test(textAfter)) return ctx.indented
+ }
+ },
+ modeProps: {fold: "brace"}
+ });
+
+ def("text/x-squirrel", {
+ name: "clike",
+ keywords: words("base break clone continue const default delete enum extends function in class" +
+ " foreach local resume return this throw typeof yield constructor instanceof static"),
+ types: words(cTypes),
+ blockKeywords: words("case catch class else for foreach if switch try while"),
+ defKeywords: words("function local class"),
+ typeFirstDefinitions: true,
+ atoms: words("true false null"),
+ hooks: {"#": cppHook},
+ modeProps: {fold: ["brace", "include"]}
+ });
+
+ // Ceylon Strings need to deal with interpolation
+ var stringTokenizer = null;
+ function tokenCeylonString(type) {
+ return function(stream, state) {
+ var escaped = false, next, end = false;
+ while (!stream.eol()) {
+ if (!escaped && stream.match('"') &&
+ (type == "single" || stream.match('""'))) {
+ end = true;
+ break;
+ }
+ if (!escaped && stream.match('``')) {
+ stringTokenizer = tokenCeylonString(type);
+ end = true;
+ break;
+ }
+ next = stream.next();
+ escaped = type == "single" && !escaped && next == "\\";
+ }
+ if (end)
+ state.tokenize = null;
+ return "string";
+ }
+ }
+
+ def("text/x-ceylon", {
+ name: "clike",
+ keywords: words("abstracts alias assembly assert assign break case catch class continue dynamic else" +
+ " exists extends finally for function given if import in interface is let module new" +
+ " nonempty object of out outer package return satisfies super switch then this throw" +
+ " try value void while"),
+ types: function(word) {
+ // In Ceylon all identifiers that start with an uppercase are types
+ var first = word.charAt(0);
+ return (first === first.toUpperCase() && first !== first.toLowerCase());
+ },
+ blockKeywords: words("case catch class dynamic else finally for function if interface module new object switch try while"),
+ defKeywords: words("class dynamic function interface module object package value"),
+ builtin: words("abstract actual aliased annotation by default deprecated doc final formal late license" +
+ " native optional sealed see serializable shared suppressWarnings tagged throws variable"),
+ isPunctuationChar: /[\[\]{}\(\),;\:\.`]/,
+ isOperatorChar: /[+\-*&%=<>!?|^~:\/]/,
+ numberStart: /[\d#$]/,
+ number: /^(?:#[\da-fA-F_]+|\$[01_]+|[\d_]+[kMGTPmunpf]?|[\d_]+\.[\d_]+(?:[eE][-+]?\d+|[kMGTPmunpf]|)|)/i,
+ multiLineStrings: true,
+ typeFirstDefinitions: true,
+ atoms: words("true false null larger smaller equal empty finished"),
+ indentSwitch: false,
+ styleDefs: false,
+ hooks: {
+ "@": function(stream) {
+ stream.eatWhile(/[\w\$_]/);
+ return "meta";
+ },
+ '"': function(stream, state) {
+ state.tokenize = tokenCeylonString(stream.match('""') ? "triple" : "single");
+ return state.tokenize(stream, state);
+ },
+ '`': function(stream, state) {
+ if (!stringTokenizer || !stream.match('`')) return false;
+ state.tokenize = stringTokenizer;
+ stringTokenizer = null;
+ return state.tokenize(stream, state);
+ },
+ "'": function(stream) {
+ stream.eatWhile(/[\w\$_\xa1-\uffff]/);
+ return "atom";
+ },
+ token: function(_stream, state, style) {
+ if ((style == "variable" || style == "variable-3") &&
+ state.prevToken == ".") {
+ return "variable-2";
+ }
+ }
+ },
+ modeProps: {
+ fold: ["brace", "import"],
+ closeBrackets: {triples: '"'}
+ }
+ });
+
+});
diff --git a/devtools/client/sourceeditor/codemirror/mode/css/css.js b/devtools/client/sourceeditor/codemirror/mode/css/css.js
new file mode 100644
index 000000000..cf6a2f2eb
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/mode/css/css.js
@@ -0,0 +1,825 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../../lib/codemirror"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function(CodeMirror) {
+"use strict";
+
+CodeMirror.defineMode("css", function(config, parserConfig) {
+ var inline = parserConfig.inline
+ if (!parserConfig.propertyKeywords) parserConfig = CodeMirror.resolveMode("text/css");
+
+ var indentUnit = config.indentUnit,
+ tokenHooks = parserConfig.tokenHooks,
+ documentTypes = parserConfig.documentTypes || {},
+ mediaTypes = parserConfig.mediaTypes || {},
+ mediaFeatures = parserConfig.mediaFeatures || {},
+ mediaValueKeywords = parserConfig.mediaValueKeywords || {},
+ propertyKeywords = parserConfig.propertyKeywords || {},
+ nonStandardPropertyKeywords = parserConfig.nonStandardPropertyKeywords || {},
+ fontProperties = parserConfig.fontProperties || {},
+ counterDescriptors = parserConfig.counterDescriptors || {},
+ colorKeywords = parserConfig.colorKeywords || {},
+ valueKeywords = parserConfig.valueKeywords || {},
+ allowNested = parserConfig.allowNested,
+ supportsAtComponent = parserConfig.supportsAtComponent === true;
+
+ var type, override;
+ function ret(style, tp) { type = tp; return style; }
+
+ // Tokenizers
+
+ function tokenBase(stream, state) {
+ var ch = stream.next();
+ if (tokenHooks[ch]) {
+ var result = tokenHooks[ch](stream, state);
+ if (result !== false) return result;
+ }
+ if (ch == "@") {
+ stream.eatWhile(/[\w\\\-]/);
+ return ret("def", stream.current());
+ } else if (ch == "=" || (ch == "~" || ch == "|") && stream.eat("=")) {
+ return ret(null, "compare");
+ } else if (ch == "\"" || ch == "'") {
+ state.tokenize = tokenString(ch);
+ return state.tokenize(stream, state);
+ } else if (ch == "#") {
+ stream.eatWhile(/[\w\\\-]/);
+ return ret("atom", "hash");
+ } else if (ch == "!") {
+ stream.match(/^\s*\w*/);
+ return ret("keyword", "important");
+ } else if (/\d/.test(ch) || ch == "." && stream.eat(/\d/)) {
+ stream.eatWhile(/[\w.%]/);
+ return ret("number", "unit");
+ } else if (ch === "-") {
+ if (/[\d.]/.test(stream.peek())) {
+ stream.eatWhile(/[\w.%]/);
+ return ret("number", "unit");
+ } else if (stream.match(/^-[\w\\\-]+/)) {
+ stream.eatWhile(/[\w\\\-]/);
+ if (stream.match(/^\s*:/, false))
+ return ret("variable-2", "variable-definition");
+ return ret("variable-2", "variable");
+ } else if (stream.match(/^\w+-/)) {
+ return ret("meta", "meta");
+ }
+ } else if (/[,+>*\/]/.test(ch)) {
+ return ret(null, "select-op");
+ } else if (ch == "." && stream.match(/^-?[_a-z][_a-z0-9-]*/i)) {
+ return ret("qualifier", "qualifier");
+ } else if (/[:;{}\[\]\(\)]/.test(ch)) {
+ return ret(null, ch);
+ } else if ((ch == "u" && stream.match(/rl(-prefix)?\(/)) ||
+ (ch == "d" && stream.match("omain(")) ||
+ (ch == "r" && stream.match("egexp("))) {
+ stream.backUp(1);
+ state.tokenize = tokenParenthesized;
+ return ret("property", "word");
+ } else if (/[\w\\\-]/.test(ch)) {
+ stream.eatWhile(/[\w\\\-]/);
+ return ret("property", "word");
+ } else {
+ return ret(null, null);
+ }
+ }
+
+ function tokenString(quote) {
+ return function(stream, state) {
+ var escaped = false, ch;
+ while ((ch = stream.next()) != null) {
+ if (ch == quote && !escaped) {
+ if (quote == ")") stream.backUp(1);
+ break;
+ }
+ escaped = !escaped && ch == "\\";
+ }
+ if (ch == quote || !escaped && quote != ")") state.tokenize = null;
+ return ret("string", "string");
+ };
+ }
+
+ function tokenParenthesized(stream, state) {
+ stream.next(); // Must be '('
+ if (!stream.match(/\s*[\"\')]/, false))
+ state.tokenize = tokenString(")");
+ else
+ state.tokenize = null;
+ return ret(null, "(");
+ }
+
+ // Context management
+
+ function Context(type, indent, prev) {
+ this.type = type;
+ this.indent = indent;
+ this.prev = prev;
+ }
+
+ function pushContext(state, stream, type, indent) {
+ state.context = new Context(type, stream.indentation() + (indent === false ? 0 : indentUnit), state.context);
+ return type;
+ }
+
+ function popContext(state) {
+ if (state.context.prev)
+ state.context = state.context.prev;
+ return state.context.type;
+ }
+
+ function pass(type, stream, state) {
+ return states[state.context.type](type, stream, state);
+ }
+ function popAndPass(type, stream, state, n) {
+ for (var i = n || 1; i > 0; i--)
+ state.context = state.context.prev;
+ return pass(type, stream, state);
+ }
+
+ // Parser
+
+ function wordAsValue(stream) {
+ var word = stream.current().toLowerCase();
+ if (valueKeywords.hasOwnProperty(word))
+ override = "atom";
+ else if (colorKeywords.hasOwnProperty(word))
+ override = "keyword";
+ else
+ override = "variable";
+ }
+
+ var states = {};
+
+ states.top = function(type, stream, state) {
+ if (type == "{") {
+ return pushContext(state, stream, "block");
+ } else if (type == "}" && state.context.prev) {
+ return popContext(state);
+ } else if (supportsAtComponent && /@component/.test(type)) {
+ return pushContext(state, stream, "atComponentBlock");
+ } else if (/^@(-moz-)?document$/.test(type)) {
+ return pushContext(state, stream, "documentTypes");
+ } else if (/^@(media|supports|(-moz-)?document|import)$/.test(type)) {
+ return pushContext(state, stream, "atBlock");
+ } else if (/^@(font-face|counter-style)/.test(type)) {
+ state.stateArg = type;
+ return "restricted_atBlock_before";
+ } else if (/^@(-(moz|ms|o|webkit)-)?keyframes$/.test(type)) {
+ return "keyframes";
+ } else if (type && type.charAt(0) == "@") {
+ return pushContext(state, stream, "at");
+ } else if (type == "hash") {
+ override = "builtin";
+ } else if (type == "word") {
+ override = "tag";
+ } else if (type == "variable-definition") {
+ return "maybeprop";
+ } else if (type == "interpolation") {
+ return pushContext(state, stream, "interpolation");
+ } else if (type == ":") {
+ return "pseudo";
+ } else if (allowNested && type == "(") {
+ return pushContext(state, stream, "parens");
+ }
+ return state.context.type;
+ };
+
+ states.block = function(type, stream, state) {
+ if (type == "word") {
+ var word = stream.current().toLowerCase();
+ if (propertyKeywords.hasOwnProperty(word)) {
+ override = "property";
+ return "maybeprop";
+ } else if (nonStandardPropertyKeywords.hasOwnProperty(word)) {
+ override = "string-2";
+ return "maybeprop";
+ } else if (allowNested) {
+ override = stream.match(/^\s*:(?:\s|$)/, false) ? "property" : "tag";
+ return "block";
+ } else {
+ override += " error";
+ return "maybeprop";
+ }
+ } else if (type == "meta") {
+ return "block";
+ } else if (!allowNested && (type == "hash" || type == "qualifier")) {
+ override = "error";
+ return "block";
+ } else {
+ return states.top(type, stream, state);
+ }
+ };
+
+ states.maybeprop = function(type, stream, state) {
+ if (type == ":") return pushContext(state, stream, "prop");
+ return pass(type, stream, state);
+ };
+
+ states.prop = function(type, stream, state) {
+ if (type == ";") return popContext(state);
+ if (type == "{" && allowNested) return pushContext(state, stream, "propBlock");
+ if (type == "}" || type == "{") return popAndPass(type, stream, state);
+ if (type == "(") return pushContext(state, stream, "parens");
+
+ if (type == "hash" && !/^#([0-9a-fA-f]{3,4}|[0-9a-fA-f]{6}|[0-9a-fA-f]{8})$/.test(stream.current())) {
+ override += " error";
+ } else if (type == "word") {
+ wordAsValue(stream);
+ } else if (type == "interpolation") {
+ return pushContext(state, stream, "interpolation");
+ }
+ return "prop";
+ };
+
+ states.propBlock = function(type, _stream, state) {
+ if (type == "}") return popContext(state);
+ if (type == "word") { override = "property"; return "maybeprop"; }
+ return state.context.type;
+ };
+
+ states.parens = function(type, stream, state) {
+ if (type == "{" || type == "}") return popAndPass(type, stream, state);
+ if (type == ")") return popContext(state);
+ if (type == "(") return pushContext(state, stream, "parens");
+ if (type == "interpolation") return pushContext(state, stream, "interpolation");
+ if (type == "word") wordAsValue(stream);
+ return "parens";
+ };
+
+ states.pseudo = function(type, stream, state) {
+ if (type == "word") {
+ override = "variable-3";
+ return state.context.type;
+ }
+ return pass(type, stream, state);
+ };
+
+ states.documentTypes = function(type, stream, state) {
+ if (type == "word" && documentTypes.hasOwnProperty(stream.current())) {
+ override = "tag";
+ return state.context.type;
+ } else {
+ return states.atBlock(type, stream, state);
+ }
+ };
+
+ states.atBlock = function(type, stream, state) {
+ if (type == "(") return pushContext(state, stream, "atBlock_parens");
+ if (type == "}" || type == ";") return popAndPass(type, stream, state);
+ if (type == "{") return popContext(state) && pushContext(state, stream, allowNested ? "block" : "top");
+
+ if (type == "interpolation") return pushContext(state, stream, "interpolation");
+
+ if (type == "word") {
+ var word = stream.current().toLowerCase();
+ if (word == "only" || word == "not" || word == "and" || word == "or")
+ override = "keyword";
+ else if (mediaTypes.hasOwnProperty(word))
+ override = "attribute";
+ else if (mediaFeatures.hasOwnProperty(word))
+ override = "property";
+ else if (mediaValueKeywords.hasOwnProperty(word))
+ override = "keyword";
+ else if (propertyKeywords.hasOwnProperty(word))
+ override = "property";
+ else if (nonStandardPropertyKeywords.hasOwnProperty(word))
+ override = "string-2";
+ else if (valueKeywords.hasOwnProperty(word))
+ override = "atom";
+ else if (colorKeywords.hasOwnProperty(word))
+ override = "keyword";
+ else
+ override = "error";
+ }
+ return state.context.type;
+ };
+
+ states.atComponentBlock = function(type, stream, state) {
+ if (type == "}")
+ return popAndPass(type, stream, state);
+ if (type == "{")
+ return popContext(state) && pushContext(state, stream, allowNested ? "block" : "top", false);
+ if (type == "word")
+ override = "error";
+ return state.context.type;
+ };
+
+ states.atBlock_parens = function(type, stream, state) {
+ if (type == ")") return popContext(state);
+ if (type == "{" || type == "}") return popAndPass(type, stream, state, 2);
+ return states.atBlock(type, stream, state);
+ };
+
+ states.restricted_atBlock_before = function(type, stream, state) {
+ if (type == "{")
+ return pushContext(state, stream, "restricted_atBlock");
+ if (type == "word" && state.stateArg == "@counter-style") {
+ override = "variable";
+ return "restricted_atBlock_before";
+ }
+ return pass(type, stream, state);
+ };
+
+ states.restricted_atBlock = function(type, stream, state) {
+ if (type == "}") {
+ state.stateArg = null;
+ return popContext(state);
+ }
+ if (type == "word") {
+ if ((state.stateArg == "@font-face" && !fontProperties.hasOwnProperty(stream.current().toLowerCase())) ||
+ (state.stateArg == "@counter-style" && !counterDescriptors.hasOwnProperty(stream.current().toLowerCase())))
+ override = "error";
+ else
+ override = "property";
+ return "maybeprop";
+ }
+ return "restricted_atBlock";
+ };
+
+ states.keyframes = function(type, stream, state) {
+ if (type == "word") { override = "variable"; return "keyframes"; }
+ if (type == "{") return pushContext(state, stream, "top");
+ return pass(type, stream, state);
+ };
+
+ states.at = function(type, stream, state) {
+ if (type == ";") return popContext(state);
+ if (type == "{" || type == "}") return popAndPass(type, stream, state);
+ if (type == "word") override = "tag";
+ else if (type == "hash") override = "builtin";
+ return "at";
+ };
+
+ states.interpolation = function(type, stream, state) {
+ if (type == "}") return popContext(state);
+ if (type == "{" || type == ";") return popAndPass(type, stream, state);
+ if (type == "word") override = "variable";
+ else if (type != "variable" && type != "(" && type != ")") override = "error";
+ return "interpolation";
+ };
+
+ return {
+ startState: function(base) {
+ return {tokenize: null,
+ state: inline ? "block" : "top",
+ stateArg: null,
+ context: new Context(inline ? "block" : "top", base || 0, null)};
+ },
+
+ token: function(stream, state) {
+ if (!state.tokenize && stream.eatSpace()) return null;
+ var style = (state.tokenize || tokenBase)(stream, state);
+ if (style && typeof style == "object") {
+ type = style[1];
+ style = style[0];
+ }
+ override = style;
+ state.state = states[state.state](type, stream, state);
+ return override;
+ },
+
+ indent: function(state, textAfter) {
+ var cx = state.context, ch = textAfter && textAfter.charAt(0);
+ var indent = cx.indent;
+ if (cx.type == "prop" && (ch == "}" || ch == ")")) cx = cx.prev;
+ if (cx.prev) {
+ if (ch == "}" && (cx.type == "block" || cx.type == "top" ||
+ cx.type == "interpolation" || cx.type == "restricted_atBlock")) {
+ // Resume indentation from parent context.
+ cx = cx.prev;
+ indent = cx.indent;
+ } else if (ch == ")" && (cx.type == "parens" || cx.type == "atBlock_parens") ||
+ ch == "{" && (cx.type == "at" || cx.type == "atBlock")) {
+ // Dedent relative to current context.
+ indent = Math.max(0, cx.indent - indentUnit);
+ cx = cx.prev;
+ }
+ }
+ return indent;
+ },
+
+ electricChars: "}",
+ blockCommentStart: "/*",
+ blockCommentEnd: "*/",
+ fold: "brace"
+ };
+});
+
+ function keySet(array) {
+ var keys = {};
+ for (var i = 0; i < array.length; ++i) {
+ keys[array[i]] = true;
+ }
+ return keys;
+ }
+
+ var documentTypes_ = [
+ "domain", "regexp", "url", "url-prefix"
+ ], documentTypes = keySet(documentTypes_);
+
+ var mediaTypes_ = [
+ "all", "aural", "braille", "handheld", "print", "projection", "screen",
+ "tty", "tv", "embossed"
+ ], mediaTypes = keySet(mediaTypes_);
+
+ var mediaFeatures_ = [
+ "width", "min-width", "max-width", "height", "min-height", "max-height",
+ "device-width", "min-device-width", "max-device-width", "device-height",
+ "min-device-height", "max-device-height", "aspect-ratio",
+ "min-aspect-ratio", "max-aspect-ratio", "device-aspect-ratio",
+ "min-device-aspect-ratio", "max-device-aspect-ratio", "color", "min-color",
+ "max-color", "color-index", "min-color-index", "max-color-index",
+ "monochrome", "min-monochrome", "max-monochrome", "resolution",
+ "min-resolution", "max-resolution", "scan", "grid", "orientation",
+ "device-pixel-ratio", "min-device-pixel-ratio", "max-device-pixel-ratio",
+ "pointer", "any-pointer", "hover", "any-hover"
+ ], mediaFeatures = keySet(mediaFeatures_);
+
+ var mediaValueKeywords_ = [
+ "landscape", "portrait", "none", "coarse", "fine", "on-demand", "hover",
+ "interlace", "progressive"
+ ], mediaValueKeywords = keySet(mediaValueKeywords_);
+
+ var propertyKeywords_ = [
+ "align-content", "align-items", "align-self", "alignment-adjust",
+ "alignment-baseline", "anchor-point", "animation", "animation-delay",
+ "animation-direction", "animation-duration", "animation-fill-mode",
+ "animation-iteration-count", "animation-name", "animation-play-state",
+ "animation-timing-function", "appearance", "azimuth", "backface-visibility",
+ "background", "background-attachment", "background-blend-mode", "background-clip",
+ "background-color", "background-image", "background-origin", "background-position",
+ "background-repeat", "background-size", "baseline-shift", "binding",
+ "bleed", "bookmark-label", "bookmark-level", "bookmark-state",
+ "bookmark-target", "border", "border-bottom", "border-bottom-color",
+ "border-bottom-left-radius", "border-bottom-right-radius",
+ "border-bottom-style", "border-bottom-width", "border-collapse",
+ "border-color", "border-image", "border-image-outset",
+ "border-image-repeat", "border-image-slice", "border-image-source",
+ "border-image-width", "border-left", "border-left-color",
+ "border-left-style", "border-left-width", "border-radius", "border-right",
+ "border-right-color", "border-right-style", "border-right-width",
+ "border-spacing", "border-style", "border-top", "border-top-color",
+ "border-top-left-radius", "border-top-right-radius", "border-top-style",
+ "border-top-width", "border-width", "bottom", "box-decoration-break",
+ "box-shadow", "box-sizing", "break-after", "break-before", "break-inside",
+ "caption-side", "clear", "clip", "color", "color-profile", "column-count",
+ "column-fill", "column-gap", "column-rule", "column-rule-color",
+ "column-rule-style", "column-rule-width", "column-span", "column-width",
+ "columns", "content", "counter-increment", "counter-reset", "crop", "cue",
+ "cue-after", "cue-before", "cursor", "direction", "display",
+ "dominant-baseline", "drop-initial-after-adjust",
+ "drop-initial-after-align", "drop-initial-before-adjust",
+ "drop-initial-before-align", "drop-initial-size", "drop-initial-value",
+ "elevation", "empty-cells", "fit", "fit-position", "flex", "flex-basis",
+ "flex-direction", "flex-flow", "flex-grow", "flex-shrink", "flex-wrap",
+ "float", "float-offset", "flow-from", "flow-into", "font", "font-feature-settings",
+ "font-family", "font-kerning", "font-language-override", "font-size", "font-size-adjust",
+ "font-stretch", "font-style", "font-synthesis", "font-variant",
+ "font-variant-alternates", "font-variant-caps", "font-variant-east-asian",
+ "font-variant-ligatures", "font-variant-numeric", "font-variant-position",
+ "font-weight", "grid", "grid-area", "grid-auto-columns", "grid-auto-flow",
+ "grid-auto-rows", "grid-column", "grid-column-end", "grid-column-gap",
+ "grid-column-start", "grid-gap", "grid-row", "grid-row-end", "grid-row-gap",
+ "grid-row-start", "grid-template", "grid-template-areas", "grid-template-columns",
+ "grid-template-rows", "hanging-punctuation", "height", "hyphens",
+ "icon", "image-orientation", "image-rendering", "image-resolution",
+ "inline-box-align", "justify-content", "left", "letter-spacing",
+ "line-break", "line-height", "line-stacking", "line-stacking-ruby",
+ "line-stacking-shift", "line-stacking-strategy", "list-style",
+ "list-style-image", "list-style-position", "list-style-type", "margin",
+ "margin-bottom", "margin-left", "margin-right", "margin-top",
+ "marks", "marquee-direction", "marquee-loop",
+ "marquee-play-count", "marquee-speed", "marquee-style", "max-height",
+ "max-width", "min-height", "min-width", "move-to", "nav-down", "nav-index",
+ "nav-left", "nav-right", "nav-up", "object-fit", "object-position",
+ "opacity", "order", "orphans", "outline",
+ "outline-color", "outline-offset", "outline-style", "outline-width",
+ "overflow", "overflow-style", "overflow-wrap", "overflow-x", "overflow-y",
+ "padding", "padding-bottom", "padding-left", "padding-right", "padding-top",
+ "page", "page-break-after", "page-break-before", "page-break-inside",
+ "page-policy", "pause", "pause-after", "pause-before", "perspective",
+ "perspective-origin", "pitch", "pitch-range", "play-during", "position",
+ "presentation-level", "punctuation-trim", "quotes", "region-break-after",
+ "region-break-before", "region-break-inside", "region-fragment",
+ "rendering-intent", "resize", "rest", "rest-after", "rest-before", "richness",
+ "right", "rotation", "rotation-point", "ruby-align", "ruby-overhang",
+ "ruby-position", "ruby-span", "shape-image-threshold", "shape-inside", "shape-margin",
+ "shape-outside", "size", "speak", "speak-as", "speak-header",
+ "speak-numeral", "speak-punctuation", "speech-rate", "stress", "string-set",
+ "tab-size", "table-layout", "target", "target-name", "target-new",
+ "target-position", "text-align", "text-align-last", "text-decoration",
+ "text-decoration-color", "text-decoration-line", "text-decoration-skip",
+ "text-decoration-style", "text-emphasis", "text-emphasis-color",
+ "text-emphasis-position", "text-emphasis-style", "text-height",
+ "text-indent", "text-justify", "text-outline", "text-overflow", "text-shadow",
+ "text-size-adjust", "text-space-collapse", "text-transform", "text-underline-position",
+ "text-wrap", "top", "transform", "transform-origin", "transform-style",
+ "transition", "transition-delay", "transition-duration",
+ "transition-property", "transition-timing-function", "unicode-bidi",
+ "vertical-align", "visibility", "voice-balance", "voice-duration",
+ "voice-family", "voice-pitch", "voice-range", "voice-rate", "voice-stress",
+ "voice-volume", "volume", "white-space", "widows", "width", "word-break",
+ "word-spacing", "word-wrap", "z-index",
+ // SVG-specific
+ "clip-path", "clip-rule", "mask", "enable-background", "filter", "flood-color",
+ "flood-opacity", "lighting-color", "stop-color", "stop-opacity", "pointer-events",
+ "color-interpolation", "color-interpolation-filters",
+ "color-rendering", "fill", "fill-opacity", "fill-rule", "image-rendering",
+ "marker", "marker-end", "marker-mid", "marker-start", "shape-rendering", "stroke",
+ "stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-linejoin",
+ "stroke-miterlimit", "stroke-opacity", "stroke-width", "text-rendering",
+ "baseline-shift", "dominant-baseline", "glyph-orientation-horizontal",
+ "glyph-orientation-vertical", "text-anchor", "writing-mode"
+ ], propertyKeywords = keySet(propertyKeywords_);
+
+ var nonStandardPropertyKeywords_ = [
+ "scrollbar-arrow-color", "scrollbar-base-color", "scrollbar-dark-shadow-color",
+ "scrollbar-face-color", "scrollbar-highlight-color", "scrollbar-shadow-color",
+ "scrollbar-3d-light-color", "scrollbar-track-color", "shape-inside",
+ "searchfield-cancel-button", "searchfield-decoration", "searchfield-results-button",
+ "searchfield-results-decoration", "zoom"
+ ], nonStandardPropertyKeywords = keySet(nonStandardPropertyKeywords_);
+
+ var fontProperties_ = [
+ "font-family", "src", "unicode-range", "font-variant", "font-feature-settings",
+ "font-stretch", "font-weight", "font-style"
+ ], fontProperties = keySet(fontProperties_);
+
+ var counterDescriptors_ = [
+ "additive-symbols", "fallback", "negative", "pad", "prefix", "range",
+ "speak-as", "suffix", "symbols", "system"
+ ], counterDescriptors = keySet(counterDescriptors_);
+
+ var colorKeywords_ = [
+ "aliceblue", "antiquewhite", "aqua", "aquamarine", "azure", "beige",
+ "bisque", "black", "blanchedalmond", "blue", "blueviolet", "brown",
+ "burlywood", "cadetblue", "chartreuse", "chocolate", "coral", "cornflowerblue",
+ "cornsilk", "crimson", "cyan", "darkblue", "darkcyan", "darkgoldenrod",
+ "darkgray", "darkgreen", "darkkhaki", "darkmagenta", "darkolivegreen",
+ "darkorange", "darkorchid", "darkred", "darksalmon", "darkseagreen",
+ "darkslateblue", "darkslategray", "darkturquoise", "darkviolet",
+ "deeppink", "deepskyblue", "dimgray", "dodgerblue", "firebrick",
+ "floralwhite", "forestgreen", "fuchsia", "gainsboro", "ghostwhite",
+ "gold", "goldenrod", "gray", "grey", "green", "greenyellow", "honeydew",
+ "hotpink", "indianred", "indigo", "ivory", "khaki", "lavender",
+ "lavenderblush", "lawngreen", "lemonchiffon", "lightblue", "lightcoral",
+ "lightcyan", "lightgoldenrodyellow", "lightgray", "lightgreen", "lightpink",
+ "lightsalmon", "lightseagreen", "lightskyblue", "lightslategray",
+ "lightsteelblue", "lightyellow", "lime", "limegreen", "linen", "magenta",
+ "maroon", "mediumaquamarine", "mediumblue", "mediumorchid", "mediumpurple",
+ "mediumseagreen", "mediumslateblue", "mediumspringgreen", "mediumturquoise",
+ "mediumvioletred", "midnightblue", "mintcream", "mistyrose", "moccasin",
+ "navajowhite", "navy", "oldlace", "olive", "olivedrab", "orange", "orangered",
+ "orchid", "palegoldenrod", "palegreen", "paleturquoise", "palevioletred",
+ "papayawhip", "peachpuff", "peru", "pink", "plum", "powderblue",
+ "purple", "rebeccapurple", "red", "rosybrown", "royalblue", "saddlebrown",
+ "salmon", "sandybrown", "seagreen", "seashell", "sienna", "silver", "skyblue",
+ "slateblue", "slategray", "snow", "springgreen", "steelblue", "tan",
+ "teal", "thistle", "tomato", "turquoise", "violet", "wheat", "white",
+ "whitesmoke", "yellow", "yellowgreen"
+ ], colorKeywords = keySet(colorKeywords_);
+
+ var valueKeywords_ = [
+ "above", "absolute", "activeborder", "additive", "activecaption", "afar",
+ "after-white-space", "ahead", "alias", "all", "all-scroll", "alphabetic", "alternate",
+ "always", "amharic", "amharic-abegede", "antialiased", "appworkspace",
+ "arabic-indic", "armenian", "asterisks", "attr", "auto", "avoid", "avoid-column", "avoid-page",
+ "avoid-region", "background", "backwards", "baseline", "below", "bidi-override", "binary",
+ "bengali", "blink", "block", "block-axis", "bold", "bolder", "border", "border-box",
+ "both", "bottom", "break", "break-all", "break-word", "bullets", "button", "button-bevel",
+ "buttonface", "buttonhighlight", "buttonshadow", "buttontext", "calc", "cambodian",
+ "capitalize", "caps-lock-indicator", "caption", "captiontext", "caret",
+ "cell", "center", "checkbox", "circle", "cjk-decimal", "cjk-earthly-branch",
+ "cjk-heavenly-stem", "cjk-ideographic", "clear", "clip", "close-quote",
+ "col-resize", "collapse", "color", "color-burn", "color-dodge", "column", "column-reverse",
+ "compact", "condensed", "contain", "content",
+ "content-box", "context-menu", "continuous", "copy", "counter", "counters", "cover", "crop",
+ "cross", "crosshair", "currentcolor", "cursive", "cyclic", "darken", "dashed", "decimal",
+ "decimal-leading-zero", "default", "default-button", "dense", "destination-atop",
+ "destination-in", "destination-out", "destination-over", "devanagari", "difference",
+ "disc", "discard", "disclosure-closed", "disclosure-open", "document",
+ "dot-dash", "dot-dot-dash",
+ "dotted", "double", "down", "e-resize", "ease", "ease-in", "ease-in-out", "ease-out",
+ "element", "ellipse", "ellipsis", "embed", "end", "ethiopic", "ethiopic-abegede",
+ "ethiopic-abegede-am-et", "ethiopic-abegede-gez", "ethiopic-abegede-ti-er",
+ "ethiopic-abegede-ti-et", "ethiopic-halehame-aa-er",
+ "ethiopic-halehame-aa-et", "ethiopic-halehame-am-et",
+ "ethiopic-halehame-gez", "ethiopic-halehame-om-et",
+ "ethiopic-halehame-sid-et", "ethiopic-halehame-so-et",
+ "ethiopic-halehame-ti-er", "ethiopic-halehame-ti-et", "ethiopic-halehame-tig",
+ "ethiopic-numeric", "ew-resize", "exclusion", "expanded", "extends", "extra-condensed",
+ "extra-expanded", "fantasy", "fast", "fill", "fixed", "flat", "flex", "flex-end", "flex-start", "footnotes",
+ "forwards", "from", "geometricPrecision", "georgian", "graytext", "grid", "groove",
+ "gujarati", "gurmukhi", "hand", "hangul", "hangul-consonant", "hard-light", "hebrew",
+ "help", "hidden", "hide", "higher", "highlight", "highlighttext",
+ "hiragana", "hiragana-iroha", "horizontal", "hsl", "hsla", "hue", "icon", "ignore",
+ "inactiveborder", "inactivecaption", "inactivecaptiontext", "infinite",
+ "infobackground", "infotext", "inherit", "initial", "inline", "inline-axis",
+ "inline-block", "inline-flex", "inline-grid", "inline-table", "inset", "inside", "intrinsic", "invert",
+ "italic", "japanese-formal", "japanese-informal", "justify", "kannada",
+ "katakana", "katakana-iroha", "keep-all", "khmer",
+ "korean-hangul-formal", "korean-hanja-formal", "korean-hanja-informal",
+ "landscape", "lao", "large", "larger", "left", "level", "lighter", "lighten",
+ "line-through", "linear", "linear-gradient", "lines", "list-item", "listbox", "listitem",
+ "local", "logical", "loud", "lower", "lower-alpha", "lower-armenian",
+ "lower-greek", "lower-hexadecimal", "lower-latin", "lower-norwegian",
+ "lower-roman", "lowercase", "ltr", "luminosity", "malayalam", "match", "matrix", "matrix3d",
+ "media-controls-background", "media-current-time-display",
+ "media-fullscreen-button", "media-mute-button", "media-play-button",
+ "media-return-to-realtime-button", "media-rewind-button",
+ "media-seek-back-button", "media-seek-forward-button", "media-slider",
+ "media-sliderthumb", "media-time-remaining-display", "media-volume-slider",
+ "media-volume-slider-container", "media-volume-sliderthumb", "medium",
+ "menu", "menulist", "menulist-button", "menulist-text",
+ "menulist-textfield", "menutext", "message-box", "middle", "min-intrinsic",
+ "mix", "mongolian", "monospace", "move", "multiple", "multiply", "myanmar", "n-resize",
+ "narrower", "ne-resize", "nesw-resize", "no-close-quote", "no-drop",
+ "no-open-quote", "no-repeat", "none", "normal", "not-allowed", "nowrap",
+ "ns-resize", "numbers", "numeric", "nw-resize", "nwse-resize", "oblique", "octal", "open-quote",
+ "optimizeLegibility", "optimizeSpeed", "oriya", "oromo", "outset",
+ "outside", "outside-shape", "overlay", "overline", "padding", "padding-box",
+ "painted", "page", "paused", "persian", "perspective", "plus-darker", "plus-lighter",
+ "pointer", "polygon", "portrait", "pre", "pre-line", "pre-wrap", "preserve-3d",
+ "progress", "push-button", "radial-gradient", "radio", "read-only",
+ "read-write", "read-write-plaintext-only", "rectangle", "region",
+ "relative", "repeat", "repeating-linear-gradient",
+ "repeating-radial-gradient", "repeat-x", "repeat-y", "reset", "reverse",
+ "rgb", "rgba", "ridge", "right", "rotate", "rotate3d", "rotateX", "rotateY",
+ "rotateZ", "round", "row", "row-resize", "row-reverse", "rtl", "run-in", "running",
+ "s-resize", "sans-serif", "saturation", "scale", "scale3d", "scaleX", "scaleY", "scaleZ", "screen",
+ "scroll", "scrollbar", "se-resize", "searchfield",
+ "searchfield-cancel-button", "searchfield-decoration",
+ "searchfield-results-button", "searchfield-results-decoration",
+ "semi-condensed", "semi-expanded", "separate", "serif", "show", "sidama",
+ "simp-chinese-formal", "simp-chinese-informal", "single",
+ "skew", "skewX", "skewY", "skip-white-space", "slide", "slider-horizontal",
+ "slider-vertical", "sliderthumb-horizontal", "sliderthumb-vertical", "slow",
+ "small", "small-caps", "small-caption", "smaller", "soft-light", "solid", "somali",
+ "source-atop", "source-in", "source-out", "source-over", "space", "space-around", "space-between", "spell-out", "square",
+ "square-button", "start", "static", "status-bar", "stretch", "stroke", "sub",
+ "subpixel-antialiased", "super", "sw-resize", "symbolic", "symbols", "table",
+ "table-caption", "table-cell", "table-column", "table-column-group",
+ "table-footer-group", "table-header-group", "table-row", "table-row-group",
+ "tamil",
+ "telugu", "text", "text-bottom", "text-top", "textarea", "textfield", "thai",
+ "thick", "thin", "threeddarkshadow", "threedface", "threedhighlight",
+ "threedlightshadow", "threedshadow", "tibetan", "tigre", "tigrinya-er",
+ "tigrinya-er-abegede", "tigrinya-et", "tigrinya-et-abegede", "to", "top",
+ "trad-chinese-formal", "trad-chinese-informal",
+ "translate", "translate3d", "translateX", "translateY", "translateZ",
+ "transparent", "ultra-condensed", "ultra-expanded", "underline", "up",
+ "upper-alpha", "upper-armenian", "upper-greek", "upper-hexadecimal",
+ "upper-latin", "upper-norwegian", "upper-roman", "uppercase", "urdu", "url",
+ "var", "vertical", "vertical-text", "visible", "visibleFill", "visiblePainted",
+ "visibleStroke", "visual", "w-resize", "wait", "wave", "wider",
+ "window", "windowframe", "windowtext", "words", "wrap", "wrap-reverse", "x-large", "x-small", "xor",
+ "xx-large", "xx-small"
+ ], valueKeywords = keySet(valueKeywords_);
+
+ var allWords = documentTypes_.concat(mediaTypes_).concat(mediaFeatures_).concat(mediaValueKeywords_)
+ .concat(propertyKeywords_).concat(nonStandardPropertyKeywords_).concat(colorKeywords_)
+ .concat(valueKeywords_);
+ CodeMirror.registerHelper("hintWords", "css", allWords);
+
+ function tokenCComment(stream, state) {
+ var maybeEnd = false, ch;
+ while ((ch = stream.next()) != null) {
+ if (maybeEnd && ch == "/") {
+ state.tokenize = null;
+ break;
+ }
+ maybeEnd = (ch == "*");
+ }
+ return ["comment", "comment"];
+ }
+
+ CodeMirror.defineMIME("text/css", {
+ documentTypes: documentTypes,
+ mediaTypes: mediaTypes,
+ mediaFeatures: mediaFeatures,
+ mediaValueKeywords: mediaValueKeywords,
+ propertyKeywords: propertyKeywords,
+ nonStandardPropertyKeywords: nonStandardPropertyKeywords,
+ fontProperties: fontProperties,
+ counterDescriptors: counterDescriptors,
+ colorKeywords: colorKeywords,
+ valueKeywords: valueKeywords,
+ tokenHooks: {
+ "/": function(stream, state) {
+ if (!stream.eat("*")) return false;
+ state.tokenize = tokenCComment;
+ return tokenCComment(stream, state);
+ }
+ },
+ name: "css"
+ });
+
+ CodeMirror.defineMIME("text/x-scss", {
+ mediaTypes: mediaTypes,
+ mediaFeatures: mediaFeatures,
+ mediaValueKeywords: mediaValueKeywords,
+ propertyKeywords: propertyKeywords,
+ nonStandardPropertyKeywords: nonStandardPropertyKeywords,
+ colorKeywords: colorKeywords,
+ valueKeywords: valueKeywords,
+ fontProperties: fontProperties,
+ allowNested: true,
+ tokenHooks: {
+ "/": function(stream, state) {
+ if (stream.eat("/")) {
+ stream.skipToEnd();
+ return ["comment", "comment"];
+ } else if (stream.eat("*")) {
+ state.tokenize = tokenCComment;
+ return tokenCComment(stream, state);
+ } else {
+ return ["operator", "operator"];
+ }
+ },
+ ":": function(stream) {
+ if (stream.match(/\s*\{/))
+ return [null, "{"];
+ return false;
+ },
+ "$": function(stream) {
+ stream.match(/^[\w-]+/);
+ if (stream.match(/^\s*:/, false))
+ return ["variable-2", "variable-definition"];
+ return ["variable-2", "variable"];
+ },
+ "#": function(stream) {
+ if (!stream.eat("{")) return false;
+ return [null, "interpolation"];
+ }
+ },
+ name: "css",
+ helperType: "scss"
+ });
+
+ CodeMirror.defineMIME("text/x-less", {
+ mediaTypes: mediaTypes,
+ mediaFeatures: mediaFeatures,
+ mediaValueKeywords: mediaValueKeywords,
+ propertyKeywords: propertyKeywords,
+ nonStandardPropertyKeywords: nonStandardPropertyKeywords,
+ colorKeywords: colorKeywords,
+ valueKeywords: valueKeywords,
+ fontProperties: fontProperties,
+ allowNested: true,
+ tokenHooks: {
+ "/": function(stream, state) {
+ if (stream.eat("/")) {
+ stream.skipToEnd();
+ return ["comment", "comment"];
+ } else if (stream.eat("*")) {
+ state.tokenize = tokenCComment;
+ return tokenCComment(stream, state);
+ } else {
+ return ["operator", "operator"];
+ }
+ },
+ "@": function(stream) {
+ if (stream.eat("{")) return [null, "interpolation"];
+ if (stream.match(/^(charset|document|font-face|import|(-(moz|ms|o|webkit)-)?keyframes|media|namespace|page|supports)\b/, false)) return false;
+ stream.eatWhile(/[\w\\\-]/);
+ if (stream.match(/^\s*:/, false))
+ return ["variable-2", "variable-definition"];
+ return ["variable-2", "variable"];
+ },
+ "&": function() {
+ return ["atom", "atom"];
+ }
+ },
+ name: "css",
+ helperType: "less"
+ });
+
+ CodeMirror.defineMIME("text/x-gss", {
+ documentTypes: documentTypes,
+ mediaTypes: mediaTypes,
+ mediaFeatures: mediaFeatures,
+ propertyKeywords: propertyKeywords,
+ nonStandardPropertyKeywords: nonStandardPropertyKeywords,
+ fontProperties: fontProperties,
+ counterDescriptors: counterDescriptors,
+ colorKeywords: colorKeywords,
+ valueKeywords: valueKeywords,
+ supportsAtComponent: true,
+ tokenHooks: {
+ "/": function(stream, state) {
+ if (!stream.eat("*")) return false;
+ state.tokenize = tokenCComment;
+ return tokenCComment(stream, state);
+ }
+ },
+ name: "css",
+ helperType: "gss"
+ });
+
+});
diff --git a/devtools/client/sourceeditor/codemirror/mode/htmlmixed/htmlmixed.js b/devtools/client/sourceeditor/codemirror/mode/htmlmixed/htmlmixed.js
new file mode 100644
index 000000000..d74083ee1
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/mode/htmlmixed/htmlmixed.js
@@ -0,0 +1,152 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../../lib/codemirror"), require("../xml/xml"), require("../javascript/javascript"), require("../css/css"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror", "../xml/xml", "../javascript/javascript", "../css/css"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function(CodeMirror) {
+ "use strict";
+
+ var defaultTags = {
+ script: [
+ ["lang", /(javascript|babel)/i, "javascript"],
+ ["type", /^(?:text|application)\/(?:x-)?(?:java|ecma)script$|^$/i, "javascript"],
+ ["type", /./, "text/plain"],
+ [null, null, "javascript"]
+ ],
+ style: [
+ ["lang", /^css$/i, "css"],
+ ["type", /^(text\/)?(x-)?(stylesheet|css)$/i, "css"],
+ ["type", /./, "text/plain"],
+ [null, null, "css"]
+ ]
+ };
+
+ function maybeBackup(stream, pat, style) {
+ var cur = stream.current(), close = cur.search(pat);
+ if (close > -1) {
+ stream.backUp(cur.length - close);
+ } else if (cur.match(/<\/?$/)) {
+ stream.backUp(cur.length);
+ if (!stream.match(pat, false)) stream.match(cur);
+ }
+ return style;
+ }
+
+ var attrRegexpCache = {};
+ function getAttrRegexp(attr) {
+ var regexp = attrRegexpCache[attr];
+ if (regexp) return regexp;
+ return attrRegexpCache[attr] = new RegExp("\\s+" + attr + "\\s*=\\s*('|\")?([^'\"]+)('|\")?\\s*");
+ }
+
+ function getAttrValue(text, attr) {
+ var match = text.match(getAttrRegexp(attr))
+ return match ? match[2] : ""
+ }
+
+ function getTagRegexp(tagName, anchored) {
+ return new RegExp((anchored ? "^" : "") + "<\/\s*" + tagName + "\s*>", "i");
+ }
+
+ function addTags(from, to) {
+ for (var tag in from) {
+ var dest = to[tag] || (to[tag] = []);
+ var source = from[tag];
+ for (var i = source.length - 1; i >= 0; i--)
+ dest.unshift(source[i])
+ }
+ }
+
+ function findMatchingMode(tagInfo, tagText) {
+ for (var i = 0; i < tagInfo.length; i++) {
+ var spec = tagInfo[i];
+ if (!spec[0] || spec[1].test(getAttrValue(tagText, spec[0]))) return spec[2];
+ }
+ }
+
+ CodeMirror.defineMode("htmlmixed", function (config, parserConfig) {
+ var htmlMode = CodeMirror.getMode(config, {
+ name: "xml",
+ htmlMode: true,
+ multilineTagIndentFactor: parserConfig.multilineTagIndentFactor,
+ multilineTagIndentPastTag: parserConfig.multilineTagIndentPastTag
+ });
+
+ var tags = {};
+ var configTags = parserConfig && parserConfig.tags, configScript = parserConfig && parserConfig.scriptTypes;
+ addTags(defaultTags, tags);
+ if (configTags) addTags(configTags, tags);
+ if (configScript) for (var i = configScript.length - 1; i >= 0; i--)
+ tags.script.unshift(["type", configScript[i].matches, configScript[i].mode])
+
+ function html(stream, state) {
+ var style = htmlMode.token(stream, state.htmlState), tag = /\btag\b/.test(style), tagName
+ if (tag && !/[<>\s\/]/.test(stream.current()) &&
+ (tagName = state.htmlState.tagName && state.htmlState.tagName.toLowerCase()) &&
+ tags.hasOwnProperty(tagName)) {
+ state.inTag = tagName + " "
+ } else if (state.inTag && tag && />$/.test(stream.current())) {
+ var inTag = /^([\S]+) (.*)/.exec(state.inTag)
+ state.inTag = null
+ var modeSpec = stream.current() == ">" && findMatchingMode(tags[inTag[1]], inTag[2])
+ var mode = CodeMirror.getMode(config, modeSpec)
+ var endTagA = getTagRegexp(inTag[1], true), endTag = getTagRegexp(inTag[1], false);
+ state.token = function (stream, state) {
+ if (stream.match(endTagA, false)) {
+ state.token = html;
+ state.localState = state.localMode = null;
+ return null;
+ }
+ return maybeBackup(stream, endTag, state.localMode.token(stream, state.localState));
+ };
+ state.localMode = mode;
+ state.localState = CodeMirror.startState(mode, htmlMode.indent(state.htmlState, ""));
+ } else if (state.inTag) {
+ state.inTag += stream.current()
+ if (stream.eol()) state.inTag += " "
+ }
+ return style;
+ };
+
+ return {
+ startState: function () {
+ var state = CodeMirror.startState(htmlMode);
+ return {token: html, inTag: null, localMode: null, localState: null, htmlState: state};
+ },
+
+ copyState: function (state) {
+ var local;
+ if (state.localState) {
+ local = CodeMirror.copyState(state.localMode, state.localState);
+ }
+ return {token: state.token, inTag: state.inTag,
+ localMode: state.localMode, localState: local,
+ htmlState: CodeMirror.copyState(htmlMode, state.htmlState)};
+ },
+
+ token: function (stream, state) {
+ return state.token(stream, state);
+ },
+
+ indent: function (state, textAfter) {
+ if (!state.localMode || /^\s*<\//.test(textAfter))
+ return htmlMode.indent(state.htmlState, textAfter);
+ else if (state.localMode.indent)
+ return state.localMode.indent(state.localState, textAfter);
+ else
+ return CodeMirror.Pass;
+ },
+
+ innerMode: function (state) {
+ return {state: state.localState || state.htmlState, mode: state.localMode || htmlMode};
+ }
+ };
+ }, "xml", "javascript", "css");
+
+ CodeMirror.defineMIME("text/html", "htmlmixed");
+});
diff --git a/devtools/client/sourceeditor/codemirror/mode/javascript/javascript.js b/devtools/client/sourceeditor/codemirror/mode/javascript/javascript.js
new file mode 100644
index 000000000..ca875411a
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/mode/javascript/javascript.js
@@ -0,0 +1,748 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+// TODO actually recognize syntax of TypeScript constructs
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../../lib/codemirror"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function(CodeMirror) {
+"use strict";
+
+function expressionAllowed(stream, state, backUp) {
+ return /^(?:operator|sof|keyword c|case|new|[\[{}\(,;:]|=>)$/.test(state.lastType) ||
+ (state.lastType == "quasi" && /\{\s*$/.test(stream.string.slice(0, stream.pos - (backUp || 0))))
+}
+
+CodeMirror.defineMode("javascript", function(config, parserConfig) {
+ var indentUnit = config.indentUnit;
+ var statementIndent = parserConfig.statementIndent;
+ var jsonldMode = parserConfig.jsonld;
+ var jsonMode = parserConfig.json || jsonldMode;
+ var isTS = parserConfig.typescript;
+ var wordRE = parserConfig.wordCharacters || /[\w$\xa1-\uffff]/;
+
+ // Tokenizer
+
+ var keywords = function(){
+ function kw(type) {return {type: type, style: "keyword"};}
+ var A = kw("keyword a"), B = kw("keyword b"), C = kw("keyword c");
+ var operator = kw("operator"), atom = {type: "atom", style: "atom"};
+
+ var jsKeywords = {
+ "if": kw("if"), "while": A, "with": A, "else": B, "do": B, "try": B, "finally": B,
+ "return": C, "break": C, "continue": C, "new": kw("new"), "delete": C, "throw": C, "debugger": C,
+ "var": kw("var"), "const": kw("var"), "let": kw("var"),
+ "function": kw("function"), "catch": kw("catch"),
+ "for": kw("for"), "switch": kw("switch"), "case": kw("case"), "default": kw("default"),
+ "in": operator, "typeof": operator, "instanceof": operator,
+ "true": atom, "false": atom, "null": atom, "undefined": atom, "NaN": atom, "Infinity": atom,
+ "this": kw("this"), "class": kw("class"), "super": kw("atom"),
+ "yield": C, "export": kw("export"), "import": kw("import"), "extends": C,
+ "await": C, "async": kw("async")
+ };
+
+ // Extend the 'normal' keywords with the TypeScript language extensions
+ if (isTS) {
+ var type = {type: "variable", style: "variable-3"};
+ var tsKeywords = {
+ // object-like things
+ "interface": kw("class"),
+ "implements": C,
+ "namespace": C,
+ "module": kw("module"),
+ "enum": kw("module"),
+
+ // scope modifiers
+ "public": kw("modifier"),
+ "private": kw("modifier"),
+ "protected": kw("modifier"),
+ "abstract": kw("modifier"),
+
+ // operators
+ "as": operator,
+
+ // types
+ "string": type, "number": type, "boolean": type, "any": type
+ };
+
+ for (var attr in tsKeywords) {
+ jsKeywords[attr] = tsKeywords[attr];
+ }
+ }
+
+ return jsKeywords;
+ }();
+
+ var isOperatorChar = /[+\-*&%=<>!?|~^]/;
+ var isJsonldKeyword = /^@(context|id|value|language|type|container|list|set|reverse|index|base|vocab|graph)"/;
+
+ function readRegexp(stream) {
+ var escaped = false, next, inSet = false;
+ while ((next = stream.next()) != null) {
+ if (!escaped) {
+ if (next == "/" && !inSet) return;
+ if (next == "[") inSet = true;
+ else if (inSet && next == "]") inSet = false;
+ }
+ escaped = !escaped && next == "\\";
+ }
+ }
+
+ // Used as scratch variables to communicate multiple values without
+ // consing up tons of objects.
+ var type, content;
+ function ret(tp, style, cont) {
+ type = tp; content = cont;
+ return style;
+ }
+ function tokenBase(stream, state) {
+ var ch = stream.next();
+ if (ch == '"' || ch == "'") {
+ state.tokenize = tokenString(ch);
+ return state.tokenize(stream, state);
+ } else if (ch == "." && stream.match(/^\d+(?:[eE][+\-]?\d+)?/)) {
+ return ret("number", "number");
+ } else if (ch == "." && stream.match("..")) {
+ return ret("spread", "meta");
+ } else if (/[\[\]{}\(\),;\:\.]/.test(ch)) {
+ return ret(ch);
+ } else if (ch == "=" && stream.eat(">")) {
+ return ret("=>", "operator");
+ } else if (ch == "0" && stream.eat(/x/i)) {
+ stream.eatWhile(/[\da-f]/i);
+ return ret("number", "number");
+ } else if (ch == "0" && stream.eat(/o/i)) {
+ stream.eatWhile(/[0-7]/i);
+ return ret("number", "number");
+ } else if (ch == "0" && stream.eat(/b/i)) {
+ stream.eatWhile(/[01]/i);
+ return ret("number", "number");
+ } else if (/\d/.test(ch)) {
+ stream.match(/^\d*(?:\.\d*)?(?:[eE][+\-]?\d+)?/);
+ return ret("number", "number");
+ } else if (ch == "/") {
+ if (stream.eat("*")) {
+ state.tokenize = tokenComment;
+ return tokenComment(stream, state);
+ } else if (stream.eat("/")) {
+ stream.skipToEnd();
+ return ret("comment", "comment");
+ } else if (expressionAllowed(stream, state, 1)) {
+ readRegexp(stream);
+ stream.match(/^\b(([gimyu])(?![gimyu]*\2))+\b/);
+ return ret("regexp", "string-2");
+ } else {
+ stream.eatWhile(isOperatorChar);
+ return ret("operator", "operator", stream.current());
+ }
+ } else if (ch == "`") {
+ state.tokenize = tokenQuasi;
+ return tokenQuasi(stream, state);
+ } else if (ch == "#") {
+ stream.skipToEnd();
+ return ret("error", "error");
+ } else if (isOperatorChar.test(ch)) {
+ stream.eatWhile(isOperatorChar);
+ return ret("operator", "operator", stream.current());
+ } else if (wordRE.test(ch)) {
+ stream.eatWhile(wordRE);
+ var word = stream.current(), known = keywords.propertyIsEnumerable(word) && keywords[word];
+ return (known && state.lastType != ".") ? ret(known.type, known.style, word) :
+ ret("variable", "variable", word);
+ }
+ }
+
+ function tokenString(quote) {
+ return function(stream, state) {
+ var escaped = false, next;
+ if (jsonldMode && stream.peek() == "@" && stream.match(isJsonldKeyword)){
+ state.tokenize = tokenBase;
+ return ret("jsonld-keyword", "meta");
+ }
+ while ((next = stream.next()) != null) {
+ if (next == quote && !escaped) break;
+ escaped = !escaped && next == "\\";
+ }
+ if (!escaped) state.tokenize = tokenBase;
+ return ret("string", "string");
+ };
+ }
+
+ function tokenComment(stream, state) {
+ var maybeEnd = false, ch;
+ while (ch = stream.next()) {
+ if (ch == "/" && maybeEnd) {
+ state.tokenize = tokenBase;
+ break;
+ }
+ maybeEnd = (ch == "*");
+ }
+ return ret("comment", "comment");
+ }
+
+ function tokenQuasi(stream, state) {
+ var escaped = false, next;
+ while ((next = stream.next()) != null) {
+ if (!escaped && (next == "`" || next == "$" && stream.eat("{"))) {
+ state.tokenize = tokenBase;
+ break;
+ }
+ escaped = !escaped && next == "\\";
+ }
+ return ret("quasi", "string-2", stream.current());
+ }
+
+ var brackets = "([{}])";
+ // This is a crude lookahead trick to try and notice that we're
+ // parsing the argument patterns for a fat-arrow function before we
+ // actually hit the arrow token. It only works if the arrow is on
+ // the same line as the arguments and there's no strange noise
+ // (comments) in between. Fallback is to only notice when we hit the
+ // arrow, and not declare the arguments as locals for the arrow
+ // body.
+ function findFatArrow(stream, state) {
+ if (state.fatArrowAt) state.fatArrowAt = null;
+ var arrow = stream.string.indexOf("=>", stream.start);
+ if (arrow < 0) return;
+
+ var depth = 0, sawSomething = false;
+ for (var pos = arrow - 1; pos >= 0; --pos) {
+ var ch = stream.string.charAt(pos);
+ var bracket = brackets.indexOf(ch);
+ if (bracket >= 0 && bracket < 3) {
+ if (!depth) { ++pos; break; }
+ if (--depth == 0) break;
+ } else if (bracket >= 3 && bracket < 6) {
+ ++depth;
+ } else if (wordRE.test(ch)) {
+ sawSomething = true;
+ } else if (/["'\/]/.test(ch)) {
+ return;
+ } else if (sawSomething && !depth) {
+ ++pos;
+ break;
+ }
+ }
+ if (sawSomething && !depth) state.fatArrowAt = pos;
+ }
+
+ // Parser
+
+ var atomicTypes = {"atom": true, "number": true, "variable": true, "string": true, "regexp": true, "this": true, "jsonld-keyword": true};
+
+ function JSLexical(indented, column, type, align, prev, info) {
+ this.indented = indented;
+ this.column = column;
+ this.type = type;
+ this.prev = prev;
+ this.info = info;
+ if (align != null) this.align = align;
+ }
+
+ function inScope(state, varname) {
+ for (var v = state.localVars; v; v = v.next)
+ if (v.name == varname) return true;
+ for (var cx = state.context; cx; cx = cx.prev) {
+ for (var v = cx.vars; v; v = v.next)
+ if (v.name == varname) return true;
+ }
+ }
+
+ function parseJS(state, style, type, content, stream) {
+ var cc = state.cc;
+ // Communicate our context to the combinators.
+ // (Less wasteful than consing up a hundred closures on every call.)
+ cx.state = state; cx.stream = stream; cx.marked = null, cx.cc = cc; cx.style = style;
+
+ if (!state.lexical.hasOwnProperty("align"))
+ state.lexical.align = true;
+
+ while(true) {
+ var combinator = cc.length ? cc.pop() : jsonMode ? expression : statement;
+ if (combinator(type, content)) {
+ while(cc.length && cc[cc.length - 1].lex)
+ cc.pop()();
+ if (cx.marked) return cx.marked;
+ if (type == "variable" && inScope(state, content)) return "variable-2";
+ return style;
+ }
+ }
+ }
+
+ // Combinator utils
+
+ var cx = {state: null, column: null, marked: null, cc: null};
+ function pass() {
+ for (var i = arguments.length - 1; i >= 0; i--) cx.cc.push(arguments[i]);
+ }
+ function cont() {
+ pass.apply(null, arguments);
+ return true;
+ }
+ function register(varname) {
+ function inList(list) {
+ for (var v = list; v; v = v.next)
+ if (v.name == varname) return true;
+ return false;
+ }
+ var state = cx.state;
+ cx.marked = "def";
+ if (state.context) {
+ if (inList(state.localVars)) return;
+ state.localVars = {name: varname, next: state.localVars};
+ } else {
+ if (inList(state.globalVars)) return;
+ if (parserConfig.globalVars)
+ state.globalVars = {name: varname, next: state.globalVars};
+ }
+ }
+
+ // Combinators
+
+ var defaultVars = {name: "this", next: {name: "arguments"}};
+ function pushcontext() {
+ cx.state.context = {prev: cx.state.context, vars: cx.state.localVars};
+ cx.state.localVars = defaultVars;
+ }
+ function popcontext() {
+ cx.state.localVars = cx.state.context.vars;
+ cx.state.context = cx.state.context.prev;
+ }
+ function pushlex(type, info) {
+ var result = function() {
+ var state = cx.state, indent = state.indented;
+ if (state.lexical.type == "stat") indent = state.lexical.indented;
+ else for (var outer = state.lexical; outer && outer.type == ")" && outer.align; outer = outer.prev)
+ indent = outer.indented;
+ state.lexical = new JSLexical(indent, cx.stream.column(), type, null, state.lexical, info);
+ };
+ result.lex = true;
+ return result;
+ }
+ function poplex() {
+ var state = cx.state;
+ if (state.lexical.prev) {
+ if (state.lexical.type == ")")
+ state.indented = state.lexical.indented;
+ state.lexical = state.lexical.prev;
+ }
+ }
+ poplex.lex = true;
+
+ function expect(wanted) {
+ function exp(type) {
+ if (type == wanted) return cont();
+ else if (wanted == ";") return pass();
+ else return cont(exp);
+ };
+ return exp;
+ }
+
+ function statement(type, value) {
+ if (type == "var") return cont(pushlex("vardef", value.length), vardef, expect(";"), poplex);
+ if (type == "keyword a") return cont(pushlex("form"), expression, statement, poplex);
+ if (type == "keyword b") return cont(pushlex("form"), statement, poplex);
+ if (type == "{") return cont(pushlex("}"), block, poplex);
+ if (type == ";") return cont();
+ if (type == "if") {
+ if (cx.state.lexical.info == "else" && cx.state.cc[cx.state.cc.length - 1] == poplex)
+ cx.state.cc.pop()();
+ return cont(pushlex("form"), expression, statement, poplex, maybeelse);
+ }
+ if (type == "function") return cont(functiondef);
+ if (type == "for") return cont(pushlex("form"), forspec, statement, poplex);
+ if (type == "variable") return cont(pushlex("stat"), maybelabel);
+ if (type == "switch") return cont(pushlex("form"), expression, pushlex("}", "switch"), expect("{"),
+ block, poplex, poplex);
+ if (type == "case") return cont(expression, expect(":"));
+ if (type == "default") return cont(expect(":"));
+ if (type == "catch") return cont(pushlex("form"), pushcontext, expect("("), funarg, expect(")"),
+ statement, poplex, popcontext);
+ if (type == "class") return cont(pushlex("form"), className, poplex);
+ if (type == "export") return cont(pushlex("stat"), afterExport, poplex);
+ if (type == "import") return cont(pushlex("stat"), afterImport, poplex);
+ if (type == "module") return cont(pushlex("form"), pattern, pushlex("}"), expect("{"), block, poplex, poplex)
+ if (type == "async") return cont(statement)
+ return pass(pushlex("stat"), expression, expect(";"), poplex);
+ }
+ function expression(type) {
+ return expressionInner(type, false);
+ }
+ function expressionNoComma(type) {
+ return expressionInner(type, true);
+ }
+ function expressionInner(type, noComma) {
+ if (cx.state.fatArrowAt == cx.stream.start) {
+ var body = noComma ? arrowBodyNoComma : arrowBody;
+ if (type == "(") return cont(pushcontext, pushlex(")"), commasep(pattern, ")"), poplex, expect("=>"), body, popcontext);
+ else if (type == "variable") return pass(pushcontext, pattern, expect("=>"), body, popcontext);
+ }
+
+ var maybeop = noComma ? maybeoperatorNoComma : maybeoperatorComma;
+ if (atomicTypes.hasOwnProperty(type)) return cont(maybeop);
+ if (type == "function") return cont(functiondef, maybeop);
+ if (type == "keyword c") return cont(noComma ? maybeexpressionNoComma : maybeexpression);
+ if (type == "(") return cont(pushlex(")"), maybeexpression, comprehension, expect(")"), poplex, maybeop);
+ if (type == "operator" || type == "spread") return cont(noComma ? expressionNoComma : expression);
+ if (type == "[") return cont(pushlex("]"), arrayLiteral, poplex, maybeop);
+ if (type == "{") return contCommasep(objprop, "}", null, maybeop);
+ if (type == "quasi") return pass(quasi, maybeop);
+ if (type == "new") return cont(maybeTarget(noComma));
+ return cont();
+ }
+ function maybeexpression(type) {
+ if (type.match(/[;\}\)\],]/)) return pass();
+ return pass(expression);
+ }
+ function maybeexpressionNoComma(type) {
+ if (type.match(/[;\}\)\],]/)) return pass();
+ return pass(expressionNoComma);
+ }
+
+ function maybeoperatorComma(type, value) {
+ if (type == ",") return cont(expression);
+ return maybeoperatorNoComma(type, value, false);
+ }
+ function maybeoperatorNoComma(type, value, noComma) {
+ var me = noComma == false ? maybeoperatorComma : maybeoperatorNoComma;
+ var expr = noComma == false ? expression : expressionNoComma;
+ if (type == "=>") return cont(pushcontext, noComma ? arrowBodyNoComma : arrowBody, popcontext);
+ if (type == "operator") {
+ if (/\+\+|--/.test(value)) return cont(me);
+ if (value == "?") return cont(expression, expect(":"), expr);
+ return cont(expr);
+ }
+ if (type == "quasi") { return pass(quasi, me); }
+ if (type == ";") return;
+ if (type == "(") return contCommasep(expressionNoComma, ")", "call", me);
+ if (type == ".") return cont(property, me);
+ if (type == "[") return cont(pushlex("]"), maybeexpression, expect("]"), poplex, me);
+ }
+ function quasi(type, value) {
+ if (type != "quasi") return pass();
+ if (value.slice(value.length - 2) != "${") return cont(quasi);
+ return cont(expression, continueQuasi);
+ }
+ function continueQuasi(type) {
+ if (type == "}") {
+ cx.marked = "string-2";
+ cx.state.tokenize = tokenQuasi;
+ return cont(quasi);
+ }
+ }
+ function arrowBody(type) {
+ findFatArrow(cx.stream, cx.state);
+ return pass(type == "{" ? statement : expression);
+ }
+ function arrowBodyNoComma(type) {
+ findFatArrow(cx.stream, cx.state);
+ return pass(type == "{" ? statement : expressionNoComma);
+ }
+ function maybeTarget(noComma) {
+ return function(type) {
+ if (type == ".") return cont(noComma ? targetNoComma : target);
+ else return pass(noComma ? expressionNoComma : expression);
+ };
+ }
+ function target(_, value) {
+ if (value == "target") { cx.marked = "keyword"; return cont(maybeoperatorComma); }
+ }
+ function targetNoComma(_, value) {
+ if (value == "target") { cx.marked = "keyword"; return cont(maybeoperatorNoComma); }
+ }
+ function maybelabel(type) {
+ if (type == ":") return cont(poplex, statement);
+ return pass(maybeoperatorComma, expect(";"), poplex);
+ }
+ function property(type) {
+ if (type == "variable") {cx.marked = "property"; return cont();}
+ }
+ function objprop(type, value) {
+ if (type == "variable" || cx.style == "keyword") {
+ cx.marked = "property";
+ if (value == "get" || value == "set") return cont(getterSetter);
+ return cont(afterprop);
+ } else if (type == "number" || type == "string") {
+ cx.marked = jsonldMode ? "property" : (cx.style + " property");
+ return cont(afterprop);
+ } else if (type == "jsonld-keyword") {
+ return cont(afterprop);
+ } else if (type == "modifier") {
+ return cont(objprop)
+ } else if (type == "[") {
+ return cont(expression, expect("]"), afterprop);
+ } else if (type == "spread") {
+ return cont(expression);
+ }
+ }
+ function getterSetter(type) {
+ if (type != "variable") return pass(afterprop);
+ cx.marked = "property";
+ return cont(functiondef);
+ }
+ function afterprop(type) {
+ if (type == ":") return cont(expressionNoComma);
+ if (type == "(") return pass(functiondef);
+ }
+ function commasep(what, end) {
+ function proceed(type, value) {
+ if (type == ",") {
+ var lex = cx.state.lexical;
+ if (lex.info == "call") lex.pos = (lex.pos || 0) + 1;
+ return cont(what, proceed);
+ }
+ if (type == end || value == end) return cont();
+ return cont(expect(end));
+ }
+ return function(type, value) {
+ if (type == end || value == end) return cont();
+ return pass(what, proceed);
+ };
+ }
+ function contCommasep(what, end, info) {
+ for (var i = 3; i < arguments.length; i++)
+ cx.cc.push(arguments[i]);
+ return cont(pushlex(end, info), commasep(what, end), poplex);
+ }
+ function block(type) {
+ if (type == "}") return cont();
+ return pass(statement, block);
+ }
+ function maybetype(type) {
+ if (isTS && type == ":") return cont(typeexpr);
+ }
+ function maybedefault(_, value) {
+ if (value == "=") return cont(expressionNoComma);
+ }
+ function typeexpr(type) {
+ if (type == "variable") {cx.marked = "variable-3"; return cont(afterType);}
+ }
+ function afterType(type, value) {
+ if (value == "<") return cont(commasep(typeexpr, ">"), afterType)
+ if (type == "[") return cont(expect("]"), afterType)
+ }
+ function vardef() {
+ return pass(pattern, maybetype, maybeAssign, vardefCont);
+ }
+ function pattern(type, value) {
+ if (type == "modifier") return cont(pattern)
+ if (type == "variable") { register(value); return cont(); }
+ if (type == "spread") return cont(pattern);
+ if (type == "[") return contCommasep(pattern, "]");
+ if (type == "{") return contCommasep(proppattern, "}");
+ }
+ function proppattern(type, value) {
+ if (type == "variable" && !cx.stream.match(/^\s*:/, false)) {
+ register(value);
+ return cont(maybeAssign);
+ }
+ if (type == "variable") cx.marked = "property";
+ if (type == "spread") return cont(pattern);
+ if (type == "}") return pass();
+ return cont(expect(":"), pattern, maybeAssign);
+ }
+ function maybeAssign(_type, value) {
+ if (value == "=") return cont(expressionNoComma);
+ }
+ function vardefCont(type) {
+ if (type == ",") return cont(vardef);
+ }
+ function maybeelse(type, value) {
+ if (type == "keyword b" && value == "else") return cont(pushlex("form", "else"), statement, poplex);
+ }
+ function forspec(type) {
+ if (type == "(") return cont(pushlex(")"), forspec1, expect(")"), poplex);
+ }
+ function forspec1(type) {
+ if (type == "var") return cont(vardef, expect(";"), forspec2);
+ if (type == ";") return cont(forspec2);
+ if (type == "variable") return cont(formaybeinof);
+ return pass(expression, expect(";"), forspec2);
+ }
+ function formaybeinof(_type, value) {
+ if (value == "in" || value == "of") { cx.marked = "keyword"; return cont(expression); }
+ return cont(maybeoperatorComma, forspec2);
+ }
+ function forspec2(type, value) {
+ if (type == ";") return cont(forspec3);
+ if (value == "in" || value == "of") { cx.marked = "keyword"; return cont(expression); }
+ return pass(expression, expect(";"), forspec3);
+ }
+ function forspec3(type) {
+ if (type != ")") cont(expression);
+ }
+ function functiondef(type, value) {
+ if (value == "*") {cx.marked = "keyword"; return cont(functiondef);}
+ if (type == "variable") {register(value); return cont(functiondef);}
+ if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, maybetype, statement, popcontext);
+ }
+ function funarg(type) {
+ if (type == "spread") return cont(funarg);
+ return pass(pattern, maybetype, maybedefault);
+ }
+ function className(type, value) {
+ if (type == "variable") {register(value); return cont(classNameAfter);}
+ }
+ function classNameAfter(type, value) {
+ if (value == "extends") return cont(expression, classNameAfter);
+ if (type == "{") return cont(pushlex("}"), classBody, poplex);
+ }
+ function classBody(type, value) {
+ if (type == "variable" || cx.style == "keyword") {
+ if (value == "static") {
+ cx.marked = "keyword";
+ return cont(classBody);
+ }
+ cx.marked = "property";
+ if (value == "get" || value == "set") return cont(classGetterSetter, functiondef, classBody);
+ return cont(functiondef, classBody);
+ }
+ if (value == "*") {
+ cx.marked = "keyword";
+ return cont(classBody);
+ }
+ if (type == ";") return cont(classBody);
+ if (type == "}") return cont();
+ }
+ function classGetterSetter(type) {
+ if (type != "variable") return pass();
+ cx.marked = "property";
+ return cont();
+ }
+ function afterExport(_type, value) {
+ if (value == "*") { cx.marked = "keyword"; return cont(maybeFrom, expect(";")); }
+ if (value == "default") { cx.marked = "keyword"; return cont(expression, expect(";")); }
+ return pass(statement);
+ }
+ function afterImport(type) {
+ if (type == "string") return cont();
+ return pass(importSpec, maybeFrom);
+ }
+ function importSpec(type, value) {
+ if (type == "{") return contCommasep(importSpec, "}");
+ if (type == "variable") register(value);
+ if (value == "*") cx.marked = "keyword";
+ return cont(maybeAs);
+ }
+ function maybeAs(_type, value) {
+ if (value == "as") { cx.marked = "keyword"; return cont(importSpec); }
+ }
+ function maybeFrom(_type, value) {
+ if (value == "from") { cx.marked = "keyword"; return cont(expression); }
+ }
+ function arrayLiteral(type) {
+ if (type == "]") return cont();
+ return pass(expressionNoComma, maybeArrayComprehension);
+ }
+ function maybeArrayComprehension(type) {
+ if (type == "for") return pass(comprehension, expect("]"));
+ if (type == ",") return cont(commasep(maybeexpressionNoComma, "]"));
+ return pass(commasep(expressionNoComma, "]"));
+ }
+ function comprehension(type) {
+ if (type == "for") return cont(forspec, comprehension);
+ if (type == "if") return cont(expression, comprehension);
+ }
+
+ function isContinuedStatement(state, textAfter) {
+ return state.lastType == "operator" || state.lastType == "," ||
+ isOperatorChar.test(textAfter.charAt(0)) ||
+ /[,.]/.test(textAfter.charAt(0));
+ }
+
+ // Interface
+
+ return {
+ startState: function(basecolumn) {
+ var state = {
+ tokenize: tokenBase,
+ lastType: "sof",
+ cc: [],
+ lexical: new JSLexical((basecolumn || 0) - indentUnit, 0, "block", false),
+ localVars: parserConfig.localVars,
+ context: parserConfig.localVars && {vars: parserConfig.localVars},
+ indented: basecolumn || 0
+ };
+ if (parserConfig.globalVars && typeof parserConfig.globalVars == "object")
+ state.globalVars = parserConfig.globalVars;
+ return state;
+ },
+
+ token: function(stream, state) {
+ if (stream.sol()) {
+ if (!state.lexical.hasOwnProperty("align"))
+ state.lexical.align = false;
+ state.indented = stream.indentation();
+ findFatArrow(stream, state);
+ }
+ if (state.tokenize != tokenComment && stream.eatSpace()) return null;
+ var style = state.tokenize(stream, state);
+ if (type == "comment") return style;
+ state.lastType = type == "operator" && (content == "++" || content == "--") ? "incdec" : type;
+ return parseJS(state, style, type, content, stream);
+ },
+
+ indent: function(state, textAfter) {
+ if (state.tokenize == tokenComment) return CodeMirror.Pass;
+ if (state.tokenize != tokenBase) return 0;
+ var firstChar = textAfter && textAfter.charAt(0), lexical = state.lexical;
+ // Kludge to prevent 'maybelse' from blocking lexical scope pops
+ if (!/^\s*else\b/.test(textAfter)) for (var i = state.cc.length - 1; i >= 0; --i) {
+ var c = state.cc[i];
+ if (c == poplex) lexical = lexical.prev;
+ else if (c != maybeelse) break;
+ }
+ if (lexical.type == "stat" && firstChar == "}") lexical = lexical.prev;
+ if (statementIndent && lexical.type == ")" && lexical.prev.type == "stat")
+ lexical = lexical.prev;
+ var type = lexical.type, closing = firstChar == type;
+
+ if (type == "vardef") return lexical.indented + (state.lastType == "operator" || state.lastType == "," ? lexical.info + 1 : 0);
+ else if (type == "form" && firstChar == "{") return lexical.indented;
+ else if (type == "form") return lexical.indented + indentUnit;
+ else if (type == "stat")
+ return lexical.indented + (isContinuedStatement(state, textAfter) ? statementIndent || indentUnit : 0);
+ else if (lexical.info == "switch" && !closing && parserConfig.doubleIndentSwitch != false)
+ return lexical.indented + (/^(?:case|default)\b/.test(textAfter) ? indentUnit : 2 * indentUnit);
+ else if (lexical.align) return lexical.column + (closing ? 0 : 1);
+ else return lexical.indented + (closing ? 0 : indentUnit);
+ },
+
+ electricInput: /^\s*(?:case .*?:|default:|\{|\})$/,
+ blockCommentStart: jsonMode ? null : "/*",
+ blockCommentEnd: jsonMode ? null : "*/",
+ lineComment: jsonMode ? null : "//",
+ fold: "brace",
+ closeBrackets: "()[]{}''\"\"``",
+
+ helperType: jsonMode ? "json" : "javascript",
+ jsonldMode: jsonldMode,
+ jsonMode: jsonMode,
+
+ expressionAllowed: expressionAllowed,
+ skipExpression: function(state) {
+ var top = state.cc[state.cc.length - 1]
+ if (top == expression || top == expressionNoComma) state.cc.pop()
+ }
+ };
+});
+
+CodeMirror.registerHelper("wordChars", "javascript", /[\w$]/);
+
+CodeMirror.defineMIME("text/javascript", "javascript");
+CodeMirror.defineMIME("text/ecmascript", "javascript");
+CodeMirror.defineMIME("application/javascript", "javascript");
+CodeMirror.defineMIME("application/x-javascript", "javascript");
+CodeMirror.defineMIME("application/ecmascript", "javascript");
+CodeMirror.defineMIME("application/json", {name: "javascript", json: true});
+CodeMirror.defineMIME("application/x-json", {name: "javascript", json: true});
+CodeMirror.defineMIME("application/ld+json", {name: "javascript", jsonld: true});
+CodeMirror.defineMIME("text/typescript", { name: "javascript", typescript: true });
+CodeMirror.defineMIME("application/typescript", { name: "javascript", typescript: true });
+
+});
diff --git a/devtools/client/sourceeditor/codemirror/mode/wasm/wasm.js b/devtools/client/sourceeditor/codemirror/mode/wasm/wasm.js
new file mode 100644
index 000000000..2c42af391
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/mode/wasm/wasm.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/. */
+
+// WebAssembly experimental syntax highlight add-on for CodeMirror.
+
+(function (root, factory) {
+ if (typeof define === 'function' && define.amd) {
+ define(["../../lib/codemirror"], factory);
+ } else if (typeof exports !== 'undefined') {
+ factory(require("../../lib/codemirror"));
+ } else {
+ factory(root.CodeMirror);
+ }
+}(this, function (CodeMirror) {
+"use strict";
+
+var isWordChar = /[\w\$_\.\\\/@]/;
+
+function createLookupTable(list) {
+ var obj = Object.create(null);
+ list.forEach(function (key) {
+ obj[key] = true;
+ });
+ return obj;
+}
+
+CodeMirror.defineMode("wasm", function() {
+ var keywords = createLookupTable([
+ "function", "import", "export", "table", "memory", "segment", "as", "type",
+ "of", "from", "typeof", "br", "br_if", "loop", "br_table", "if", "else",
+ "call", "call_import", "call_indirect", "nop", "unreachable", "var",
+ "align", "select", "return"]);
+ var builtins = createLookupTable([
+ "i32:8", "i32:8u", "i32:8s", "i32:16", "i32:16u", "i32:16s",
+ "i64:8", "i64:8u", "i64:8s", "i64:16", "i64:16u", "i64:16s",
+ "i64:32", "i64:32u", "i64:32s",
+ "i32.add", "i32.sub", "i32.mul", "i32.div_s", "i32.div_u",
+ "i32.rem_s", "i32.rem_u", "i32.and", "i32.or", "i32.xor",
+ "i32.shl", "i32.shr_u", "i32.shr_s", "i32.rotr", "i32.rotl",
+ "i32.eq", "i32.ne", "i32.lt_s", "i32.le_s", "i32.lt_u",
+ "i32.le_u", "i32.gt_s", "i32.ge_s", "i32.gt_u", "i32.ge_u",
+ "i32.clz", "i32.ctz", "i32.popcnt", "i32.eqz", "i64.add",
+ "i64.sub", "i64.mul", "i64.div_s", "i64.div_u", "i64.rem_s",
+ "i64.rem_u", "i64.and", "i64.or", "i64.xor", "i64.shl",
+ "i64.shr_u", "i64.shr_s", "i64.rotr", "i64.rotl", "i64.eq",
+ "i64.ne", "i64.lt_s", "i64.le_s", "i64.lt_u", "i64.le_u",
+ "i64.gt_s", "i64.ge_s", "i64.gt_u", "i64.ge_u", "i64.clz",
+ "i64.ctz", "i64.popcnt", "i64.eqz", "f32.add", "f32.sub",
+ "f32.mul", "f32.div", "f32.min", "f32.max", "f32.abs",
+ "f32.neg", "f32.copysign", "f32.ceil", "f32.floor", "f32.trunc",
+ "f32.nearest", "f32.sqrt", "f32.eq", "f32.ne", "f32.lt",
+ "f32.le", "f32.gt", "f32.ge", "f64.add", "f64.sub", "f64.mul",
+ "f64.div", "f64.min", "f64.max", "f64.abs", "f64.neg",
+ "f64.copysign", "f64.ceil", "f64.floor", "f64.trunc", "f64.nearest",
+ "f64.sqrt", "f64.eq", "f64.ne", "f64.lt", "f64.le", "f64.gt",
+ "f64.ge", "i32.trunc_s/f32", "i32.trunc_s/f64", "i32.trunc_u/f32",
+ "i32.trunc_u/f64", "i32.wrap/i64", "i64.trunc_s/f32",
+ "i64.trunc_s/f64", "i64.trunc_u/f32", "i64.trunc_u/f64",
+ "i64.extend_s/i32", "i64.extend_u/i32", "f32.convert_s/i32",
+ "f32.convert_u/i32", "f32.convert_s/i64", "f32.convert_u/i64",
+ "f32.demote/f64", "f32.reinterpret/i32", "f64.convert_s/i32",
+ "f64.convert_u/i32", "f64.convert_s/i64", "f64.convert_u/i64",
+ "f64.promote/f32", "f64.reinterpret/i64", "i32.reinterpret/f32",
+ "i64.reinterpret/f64"]);
+ var dataTypes = createLookupTable(["i32", "i64", "f32", "f64"]);
+ var isUnaryOperator = /[\-!]/;
+ var operators = createLookupTable([
+ "+", "-", "*", "/", "/s", "/u", "%", "%s", "%u",
+ "<<", ">>u", ">>s", ">=", "<=", "==", "!=",
+ "<s", "<u", "<=s", "<=u", ">=s", ">=u", ">s", ">u",
+ "<", ">", "=", "&", "|", "^", "!"]);
+
+ function tokenBase(stream, state) {
+ var ch = stream.next();
+ if (ch === "$") {
+ stream.eatWhile(isWordChar);
+ return "variable";
+ }
+ if (ch === "@") {
+ stream.eatWhile(isWordChar);
+ return "meta";
+ }
+ if (ch === '"') {
+ state.tokenize = tokenString(ch);
+ return state.tokenize(stream, state);
+ }
+ if (ch == "/") {
+ if (stream.eat("*")) {
+ state.tokenize = tokenComment;
+ return tokenComment(stream, state);
+ } else if (stream.eat("/")) {
+ stream.skipToEnd();
+ return "comment";
+ }
+ }
+ if (/\d/.test(ch) ||
+ ((ch === "-" || ch === "+") && /\d/.test(stream.peek()))) {
+ stream.eatWhile(/[\w\._\-+]/);
+ return "number";
+ }
+ if (/[\[\]\(\)\{\},:]/.test(ch)) {
+ return null;
+ }
+ if (isUnaryOperator.test(ch)) {
+ return "operator";
+ }
+ stream.eatWhile(isWordChar);
+ var word = stream.current();
+
+ if (word in operators) {
+ return "operator";
+ }
+ if (word in keywords){
+ return "keyword";
+ }
+ if (word in dataTypes) {
+ if (!stream.eat(":")) {
+ return "builtin";
+ }
+ stream.eatWhile(isWordChar);
+ word = stream.current();
+ // fall thru for "builtin" check
+ }
+ if (word in builtins) {
+ return "builtin";
+ }
+
+ if (word === "Temporary") {
+ // Nightly has header with some text graphics -- skipping it.
+ state.tokenize = tokenTemporary;
+ return state.tokenize(stream, state);
+ }
+ return null;
+ }
+
+ function tokenComment(stream, state) {
+ state.commentDepth = 1;
+ var next;
+ while ((next = stream.next()) != null) {
+ if (next === "*" && stream.eat("/")) {
+ if (--state.commentDepth === 0) {
+ state.tokenize = null;
+ return "comment";
+ }
+ }
+ if (next === "/" && stream.eat("*")) {
+ // Nested comment
+ state.commentDepth++;
+ }
+ }
+ return "comment";
+ }
+
+ function tokenTemporary(stream, state) {
+ var next, endState = state.commentState;
+ // Skipping until "text support (Work In Progress):" is found.
+ while ((next = stream.next()) != null) {
+ if (endState === 0 && next === "t") {
+ endState = 1;
+ } else if (endState === 1 && next === ":") {
+ state.tokenize = null;
+ state.commentState = 0;
+ endState = 2;
+ return "comment";
+ }
+ }
+ state.commentState = endState;
+ return "comment";
+ }
+
+ function tokenString(quote) {
+ return function(stream, state) {
+ var escaped = false, next, end = false;
+ while ((next = stream.next()) != null) {
+ if (next == quote && !escaped) {
+ state.tokenize = null;
+ return "string";
+ }
+ escaped = !escaped && next === "\\";
+ }
+ return "string";
+ };
+ }
+
+ return {
+ startState: function() {
+ return {tokenize: null, commentState: 0, commentDepth: 0};
+ },
+
+ token: function(stream, state) {
+ if (stream.eatSpace()) return null;
+ var style = (state.tokenize || tokenBase)(stream, state);
+ return style;
+ }
+ };
+});
+
+CodeMirror.registerHelper("wordChars", "wasm", isWordChar);
+
+CodeMirror.defineMIME("text/wasm", "wasm");
+
+}));
diff --git a/devtools/client/sourceeditor/codemirror/mode/xml/xml.js b/devtools/client/sourceeditor/codemirror/mode/xml/xml.js
new file mode 100644
index 000000000..f987a3a3c
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/mode/xml/xml.js
@@ -0,0 +1,394 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../../lib/codemirror"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function(CodeMirror) {
+"use strict";
+
+var htmlConfig = {
+ autoSelfClosers: {'area': true, 'base': true, 'br': true, 'col': true, 'command': true,
+ 'embed': true, 'frame': true, 'hr': true, 'img': true, 'input': true,
+ 'keygen': true, 'link': true, 'meta': true, 'param': true, 'source': true,
+ 'track': true, 'wbr': true, 'menuitem': true},
+ implicitlyClosed: {'dd': true, 'li': true, 'optgroup': true, 'option': true, 'p': true,
+ 'rp': true, 'rt': true, 'tbody': true, 'td': true, 'tfoot': true,
+ 'th': true, 'tr': true},
+ contextGrabbers: {
+ 'dd': {'dd': true, 'dt': true},
+ 'dt': {'dd': true, 'dt': true},
+ 'li': {'li': true},
+ 'option': {'option': true, 'optgroup': true},
+ 'optgroup': {'optgroup': true},
+ 'p': {'address': true, 'article': true, 'aside': true, 'blockquote': true, 'dir': true,
+ 'div': true, 'dl': true, 'fieldset': true, 'footer': true, 'form': true,
+ 'h1': true, 'h2': true, 'h3': true, 'h4': true, 'h5': true, 'h6': true,
+ 'header': true, 'hgroup': true, 'hr': true, 'menu': true, 'nav': true, 'ol': true,
+ 'p': true, 'pre': true, 'section': true, 'table': true, 'ul': true},
+ 'rp': {'rp': true, 'rt': true},
+ 'rt': {'rp': true, 'rt': true},
+ 'tbody': {'tbody': true, 'tfoot': true},
+ 'td': {'td': true, 'th': true},
+ 'tfoot': {'tbody': true},
+ 'th': {'td': true, 'th': true},
+ 'thead': {'tbody': true, 'tfoot': true},
+ 'tr': {'tr': true}
+ },
+ doNotIndent: {"pre": true},
+ allowUnquoted: true,
+ allowMissing: true,
+ caseFold: true
+}
+
+var xmlConfig = {
+ autoSelfClosers: {},
+ implicitlyClosed: {},
+ contextGrabbers: {},
+ doNotIndent: {},
+ allowUnquoted: false,
+ allowMissing: false,
+ caseFold: false
+}
+
+CodeMirror.defineMode("xml", function(editorConf, config_) {
+ var indentUnit = editorConf.indentUnit
+ var config = {}
+ var defaults = config_.htmlMode ? htmlConfig : xmlConfig
+ for (var prop in defaults) config[prop] = defaults[prop]
+ for (var prop in config_) config[prop] = config_[prop]
+
+ // Return variables for tokenizers
+ var type, setStyle;
+
+ function inText(stream, state) {
+ function chain(parser) {
+ state.tokenize = parser;
+ return parser(stream, state);
+ }
+
+ var ch = stream.next();
+ if (ch == "<") {
+ if (stream.eat("!")) {
+ if (stream.eat("[")) {
+ if (stream.match("CDATA[")) return chain(inBlock("atom", "]]>"));
+ else return null;
+ } else if (stream.match("--")) {
+ return chain(inBlock("comment", "-->"));
+ } else if (stream.match("DOCTYPE", true, true)) {
+ stream.eatWhile(/[\w\._\-]/);
+ return chain(doctype(1));
+ } else {
+ return null;
+ }
+ } else if (stream.eat("?")) {
+ stream.eatWhile(/[\w\._\-]/);
+ state.tokenize = inBlock("meta", "?>");
+ return "meta";
+ } else {
+ type = stream.eat("/") ? "closeTag" : "openTag";
+ state.tokenize = inTag;
+ return "tag bracket";
+ }
+ } else if (ch == "&") {
+ var ok;
+ if (stream.eat("#")) {
+ if (stream.eat("x")) {
+ ok = stream.eatWhile(/[a-fA-F\d]/) && stream.eat(";");
+ } else {
+ ok = stream.eatWhile(/[\d]/) && stream.eat(";");
+ }
+ } else {
+ ok = stream.eatWhile(/[\w\.\-:]/) && stream.eat(";");
+ }
+ return ok ? "atom" : "error";
+ } else {
+ stream.eatWhile(/[^&<]/);
+ return null;
+ }
+ }
+ inText.isInText = true;
+
+ function inTag(stream, state) {
+ var ch = stream.next();
+ if (ch == ">" || (ch == "/" && stream.eat(">"))) {
+ state.tokenize = inText;
+ type = ch == ">" ? "endTag" : "selfcloseTag";
+ return "tag bracket";
+ } else if (ch == "=") {
+ type = "equals";
+ return null;
+ } else if (ch == "<") {
+ state.tokenize = inText;
+ state.state = baseState;
+ state.tagName = state.tagStart = null;
+ var next = state.tokenize(stream, state);
+ return next ? next + " tag error" : "tag error";
+ } else if (/[\'\"]/.test(ch)) {
+ state.tokenize = inAttribute(ch);
+ state.stringStartCol = stream.column();
+ return state.tokenize(stream, state);
+ } else {
+ stream.match(/^[^\s\u00a0=<>\"\']*[^\s\u00a0=<>\"\'\/]/);
+ return "word";
+ }
+ }
+
+ function inAttribute(quote) {
+ var closure = function(stream, state) {
+ while (!stream.eol()) {
+ if (stream.next() == quote) {
+ state.tokenize = inTag;
+ break;
+ }
+ }
+ return "string";
+ };
+ closure.isInAttribute = true;
+ return closure;
+ }
+
+ function inBlock(style, terminator) {
+ return function(stream, state) {
+ while (!stream.eol()) {
+ if (stream.match(terminator)) {
+ state.tokenize = inText;
+ break;
+ }
+ stream.next();
+ }
+ return style;
+ };
+ }
+ function doctype(depth) {
+ return function(stream, state) {
+ var ch;
+ while ((ch = stream.next()) != null) {
+ if (ch == "<") {
+ state.tokenize = doctype(depth + 1);
+ return state.tokenize(stream, state);
+ } else if (ch == ">") {
+ if (depth == 1) {
+ state.tokenize = inText;
+ break;
+ } else {
+ state.tokenize = doctype(depth - 1);
+ return state.tokenize(stream, state);
+ }
+ }
+ }
+ return "meta";
+ };
+ }
+
+ function Context(state, tagName, startOfLine) {
+ this.prev = state.context;
+ this.tagName = tagName;
+ this.indent = state.indented;
+ this.startOfLine = startOfLine;
+ if (config.doNotIndent.hasOwnProperty(tagName) || (state.context && state.context.noIndent))
+ this.noIndent = true;
+ }
+ function popContext(state) {
+ if (state.context) state.context = state.context.prev;
+ }
+ function maybePopContext(state, nextTagName) {
+ var parentTagName;
+ while (true) {
+ if (!state.context) {
+ return;
+ }
+ parentTagName = state.context.tagName;
+ if (!config.contextGrabbers.hasOwnProperty(parentTagName) ||
+ !config.contextGrabbers[parentTagName].hasOwnProperty(nextTagName)) {
+ return;
+ }
+ popContext(state);
+ }
+ }
+
+ function baseState(type, stream, state) {
+ if (type == "openTag") {
+ state.tagStart = stream.column();
+ return tagNameState;
+ } else if (type == "closeTag") {
+ return closeTagNameState;
+ } else {
+ return baseState;
+ }
+ }
+ function tagNameState(type, stream, state) {
+ if (type == "word") {
+ state.tagName = stream.current();
+ setStyle = "tag";
+ return attrState;
+ } else {
+ setStyle = "error";
+ return tagNameState;
+ }
+ }
+ function closeTagNameState(type, stream, state) {
+ if (type == "word") {
+ var tagName = stream.current();
+ if (state.context && state.context.tagName != tagName &&
+ config.implicitlyClosed.hasOwnProperty(state.context.tagName))
+ popContext(state);
+ if ((state.context && state.context.tagName == tagName) || config.matchClosing === false) {
+ setStyle = "tag";
+ return closeState;
+ } else {
+ setStyle = "tag error";
+ return closeStateErr;
+ }
+ } else {
+ setStyle = "error";
+ return closeStateErr;
+ }
+ }
+
+ function closeState(type, _stream, state) {
+ if (type != "endTag") {
+ setStyle = "error";
+ return closeState;
+ }
+ popContext(state);
+ return baseState;
+ }
+ function closeStateErr(type, stream, state) {
+ setStyle = "error";
+ return closeState(type, stream, state);
+ }
+
+ function attrState(type, _stream, state) {
+ if (type == "word") {
+ setStyle = "attribute";
+ return attrEqState;
+ } else if (type == "endTag" || type == "selfcloseTag") {
+ var tagName = state.tagName, tagStart = state.tagStart;
+ state.tagName = state.tagStart = null;
+ if (type == "selfcloseTag" ||
+ config.autoSelfClosers.hasOwnProperty(tagName)) {
+ maybePopContext(state, tagName);
+ } else {
+ maybePopContext(state, tagName);
+ state.context = new Context(state, tagName, tagStart == state.indented);
+ }
+ return baseState;
+ }
+ setStyle = "error";
+ return attrState;
+ }
+ function attrEqState(type, stream, state) {
+ if (type == "equals") return attrValueState;
+ if (!config.allowMissing) setStyle = "error";
+ return attrState(type, stream, state);
+ }
+ function attrValueState(type, stream, state) {
+ if (type == "string") return attrContinuedState;
+ if (type == "word" && config.allowUnquoted) {setStyle = "string"; return attrState;}
+ setStyle = "error";
+ return attrState(type, stream, state);
+ }
+ function attrContinuedState(type, stream, state) {
+ if (type == "string") return attrContinuedState;
+ return attrState(type, stream, state);
+ }
+
+ return {
+ startState: function(baseIndent) {
+ var state = {tokenize: inText,
+ state: baseState,
+ indented: baseIndent || 0,
+ tagName: null, tagStart: null,
+ context: null}
+ if (baseIndent != null) state.baseIndent = baseIndent
+ return state
+ },
+
+ token: function(stream, state) {
+ if (!state.tagName && stream.sol())
+ state.indented = stream.indentation();
+
+ if (stream.eatSpace()) return null;
+ type = null;
+ var style = state.tokenize(stream, state);
+ if ((style || type) && style != "comment") {
+ setStyle = null;
+ state.state = state.state(type || style, stream, state);
+ if (setStyle)
+ style = setStyle == "error" ? style + " error" : setStyle;
+ }
+ return style;
+ },
+
+ indent: function(state, textAfter, fullLine) {
+ var context = state.context;
+ // Indent multi-line strings (e.g. css).
+ if (state.tokenize.isInAttribute) {
+ if (state.tagStart == state.indented)
+ return state.stringStartCol + 1;
+ else
+ return state.indented + indentUnit;
+ }
+ if (context && context.noIndent) return CodeMirror.Pass;
+ if (state.tokenize != inTag && state.tokenize != inText)
+ return fullLine ? fullLine.match(/^(\s*)/)[0].length : 0;
+ // Indent the starts of attribute names.
+ if (state.tagName) {
+ if (config.multilineTagIndentPastTag !== false)
+ return state.tagStart + state.tagName.length + 2;
+ else
+ return state.tagStart + indentUnit * (config.multilineTagIndentFactor || 1);
+ }
+ if (config.alignCDATA && /<!\[CDATA\[/.test(textAfter)) return 0;
+ var tagAfter = textAfter && /^<(\/)?([\w_:\.-]*)/.exec(textAfter);
+ if (tagAfter && tagAfter[1]) { // Closing tag spotted
+ while (context) {
+ if (context.tagName == tagAfter[2]) {
+ context = context.prev;
+ break;
+ } else if (config.implicitlyClosed.hasOwnProperty(context.tagName)) {
+ context = context.prev;
+ } else {
+ break;
+ }
+ }
+ } else if (tagAfter) { // Opening tag spotted
+ while (context) {
+ var grabbers = config.contextGrabbers[context.tagName];
+ if (grabbers && grabbers.hasOwnProperty(tagAfter[2]))
+ context = context.prev;
+ else
+ break;
+ }
+ }
+ while (context && context.prev && !context.startOfLine)
+ context = context.prev;
+ if (context) return context.indent + indentUnit;
+ else return state.baseIndent || 0;
+ },
+
+ electricInput: /<\/[\s\w:]+>$/,
+ blockCommentStart: "<!--",
+ blockCommentEnd: "-->",
+
+ configuration: config.htmlMode ? "html" : "xml",
+ helperType: config.htmlMode ? "html" : "xml",
+
+ skipAttribute: function(state) {
+ if (state.state == attrValueState)
+ state.state = attrState
+ }
+ };
+});
+
+CodeMirror.defineMIME("text/xml", "xml");
+CodeMirror.defineMIME("application/xml", "xml");
+if (!CodeMirror.mimeModes.hasOwnProperty("text/html"))
+ CodeMirror.defineMIME("text/html", {name: "xml", htmlMode: true});
+
+});
diff --git a/devtools/client/sourceeditor/codemirror/mozilla.css b/devtools/client/sourceeditor/codemirror/mozilla.css
new file mode 100644
index 000000000..96aeef2af
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/mozilla.css
@@ -0,0 +1,263 @@
+/* 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/. */
+
+:root {
+ --breakpoint-background: url("chrome://devtools/skin/images/breakpoint.svg#light");
+ --breakpoint-hover-background: url("chrome://devtools/skin/images/breakpoint.svg#light-hover");
+ --breakpoint-active-color: rgba(44,187,15,.2);
+ --breakpoint-conditional-background: url("chrome://devtools/skin/images/breakpoint.svg#light-conditional");
+}
+
+.theme-dark:root {
+ --breakpoint-background: url("chrome://devtools/skin/images/breakpoint.svg#dark");
+ --breakpoint-hover-background: url("chrome://devtools/skin/images/breakpoint.svg#dark-hover");
+ --breakpoint-active-color: rgba(112,191,83,.4);
+ --breakpoint-conditional-background: url("chrome://devtools/skin/images/breakpoint.svg#dark-conditional");
+}
+
+.CodeMirror {
+ height: 100%;
+ cursor: text;
+}
+
+.CodeMirror .errors {
+ width: 16px;
+}
+
+.CodeMirror .error {
+ display: inline-block;
+ margin-left: 5px;
+ width: 12px;
+ height: 12px;
+ background-repeat: no-repeat;
+ background-position: center;
+ background-size: contain;
+ background-image: url("chrome://devtools/skin/images/editor-error.png");
+ opacity: 0.75;
+}
+
+.CodeMirror .hit-counts {
+ width: 6px;
+}
+
+.CodeMirror .hit-count {
+ display: inline-block;
+ height: 12px;
+ border: solid rgba(0,0,0,0.2);
+ border-width: 1px 1px 1px 0;
+ border-radius: 0 3px 3px 0;
+ padding: 0 3px;
+ font-size: 10px;
+ pointer-events: none;
+}
+
+.CodeMirror-linenumber:before {
+ content: " ";
+ display: block;
+ width: calc(100% - 3px);
+ position: absolute;
+ top: 1px;
+ left: 0;
+ height: 12px;
+ z-index: -1;
+ background-size: calc(100% - 2px) 12px;
+ background-repeat: no-repeat;
+ background-position: right center;
+ padding-inline-end: 9px;
+}
+
+.breakpoint .CodeMirror-linenumber {
+ color: var(--theme-body-background);
+}
+
+.breakpoint .CodeMirror-linenumber:before {
+ background-image: var(--breakpoint-background) !important;
+}
+
+.conditional .CodeMirror-linenumber:before {
+ background-image: var(--breakpoint-conditional-background) !important;
+}
+
+.debug-line .CodeMirror-linenumber {
+ background-color: var(--breakpoint-active-color);
+}
+
+.theme-dark .debug-line .CodeMirror-linenumber {
+ color: #c0c0c0;
+}
+
+.debug-line .CodeMirror-line {
+ background-color: var(--breakpoint-active-color) !important;
+}
+
+/* Don't display the highlight color since the debug line
+ is already highlighted */
+.debug-line .CodeMirror-activeline-background {
+ display: none;
+}
+
+.CodeMirror-gutters {
+ cursor: default;
+}
+
+/* This is to avoid the fake horizontal scrollbar div of codemirror to go 0
+height when floating scrollbars are active. Make sure that this value is equal
+to the maximum of `min-height` specific to the `scrollbar[orient="horizontal"]`
+selector in floating-scrollbar-light.css across all platforms. */
+.CodeMirror-hscrollbar {
+ min-height: 10px;
+}
+
+/* This is to avoid the fake vertical scrollbar div of codemirror to go 0
+width when floating scrollbars are active. Make sure that this value is equal
+to the maximum of `min-width` specific to the `scrollbar[orient="vertical"]`
+selector in floating-scrollbar-light.css across all platforms. */
+.CodeMirror-vscrollbar {
+ min-width: 10px;
+}
+
+.cm-trailingspace {
+ background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAACCAYAAAB/qH1jAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3QUXCToH00Y1UgAAACFJREFUCNdjPMDBUc/AwNDAAAFMTAwMDA0OP34wQgX/AQBYgwYEx4f9lQAAAABJRU5ErkJggg==");
+ opacity: 0.75;
+ background-position: left bottom;
+ background-repeat: repeat-x;
+}
+
+/* CodeMirror dialogs styling */
+
+.CodeMirror-dialog {
+ padding: 4px 3px;
+}
+
+.CodeMirror-dialog,
+.CodeMirror-dialog input {
+ font: message-box;
+}
+
+/* Fold addon */
+
+.CodeMirror-foldmarker {
+ color: blue;
+ text-shadow: #b9f 1px 1px 2px, #b9f -1px -1px 2px, #b9f 1px -1px 2px, #b9f -1px 1px 2px;
+ font-family: sans-serif;
+ line-height: .3;
+ cursor: pointer;
+}
+
+.CodeMirror-foldgutter {
+ width: 16px; /* Same as breakpoints gutter above */
+}
+
+.CodeMirror-foldgutter-open,
+.CodeMirror-foldgutter-folded {
+ color: #555;
+ cursor: pointer;
+}
+
+.CodeMirror-foldgutter-open:after {
+ font-size: 120%;
+ content: "\25BE";
+}
+
+.CodeMirror-foldgutter-folded:after {
+ font-size: 120%;
+ content: "\25B8";
+}
+
+.CodeMirror-hints {
+ position: absolute;
+ z-index: 10;
+ overflow: hidden;
+ list-style: none;
+ margin: 0;
+ padding: 2px;
+ border-radius: 3px;
+ font-size: 90%;
+ max-height: 20em;
+ overflow-y: auto;
+}
+
+.CodeMirror-hint {
+ margin: 0;
+ padding: 0 4px;
+ border-radius: 2px;
+ max-width: 19em;
+ overflow: hidden;
+ white-space: pre;
+ cursor: pointer;
+}
+
+.CodeMirror-Tern-completion {
+ padding-inline-start: 22px;
+ position: relative;
+ line-height: 18px;
+}
+
+.CodeMirror-Tern-completion:before {
+ position: absolute;
+ left: 2px;
+ bottom: 2px;
+ border-radius: 50%;
+ font-size: 12px;
+ font-weight: bold;
+ height: 15px;
+ width: 15px;
+ line-height: 16px;
+ text-align: center;
+ color: #ffffff;
+ box-sizing: border-box;
+}
+
+.CodeMirror-Tern-completion-unknown:before {
+ content: "?";
+}
+
+.CodeMirror-Tern-completion-object:before {
+ content: "O";
+}
+
+.CodeMirror-Tern-completion-fn:before {
+ content: "F";
+}
+
+.CodeMirror-Tern-completion-array:before {
+ content: "A";
+}
+
+.CodeMirror-Tern-completion-number:before {
+ content: "N";
+}
+
+.CodeMirror-Tern-completion-string:before {
+ content: "S";
+}
+
+.CodeMirror-Tern-completion-bool:before {
+ content: "B";
+}
+
+.CodeMirror-Tern-completion-guess {
+ color: #999;
+}
+
+.CodeMirror-Tern-tooltip {
+ border-radius: 3px;
+ padding: 2px 5px;
+ white-space: pre-wrap;
+ max-width: 40em;
+ position: absolute;
+ z-index: 10;
+}
+
+.CodeMirror-Tern-hint-doc {
+ max-width: 25em;
+}
+
+.CodeMirror-Tern-farg-current {
+ text-decoration: underline;
+}
+
+.CodeMirror-Tern-fhint-guess {
+ opacity: .7;
+}
diff --git a/devtools/client/sourceeditor/css-autocompleter.js b/devtools/client/sourceeditor/css-autocompleter.js
new file mode 100644
index 000000000..46c00cc5d
--- /dev/null
+++ b/devtools/client/sourceeditor/css-autocompleter.js
@@ -0,0 +1,1214 @@
+/* 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";
+
+/* eslint-disable complexity */
+const {cssTokenizer, cssTokenizerWithLineColumn} = require("devtools/shared/css/parsing-utils");
+const {getClientCssProperties} = require("devtools/shared/fronts/css-properties");
+
+/**
+ * Here is what this file (+ css-parsing-utils.js) do.
+ *
+ * The main objective here is to provide as much suggestions to the user editing
+ * a stylesheet in Style Editor. The possible things that can be suggested are:
+ * - CSS property names
+ * - CSS property values
+ * - CSS Selectors
+ * - Some other known CSS keywords
+ *
+ * Gecko provides a list of both property names and their corresponding values.
+ * We take out a list of matching selectors using the Inspector actor's
+ * `getSuggestionsForQuery` method. Now the only thing is to parse the CSS being
+ * edited by the user, figure out what token or word is being written and last
+ * but the most difficult, what is being edited.
+ *
+ * The file 'css-parsing-utils' helps to convert the CSS into meaningful tokens,
+ * each having a certain type associated with it. These tokens help us to figure
+ * out the currently edited word and to write a CSS state machine to figure out
+ * what the user is currently editing. By that, I mean, whether he is editing a
+ * selector or a property or a value, or even fine grained information like an
+ * id in the selector.
+ *
+ * The `resolveState` method iterated over the tokens spitted out by the
+ * tokenizer, using switch cases, follows a state machine logic and finally
+ * figures out these informations:
+ * - The state of the CSS at the cursor (one out of CSS_STATES)
+ * - The current token that is being edited `cmpleting`
+ * - If the state is "selector", the selector state (one of SELECTOR_STATES)
+ * - If the state is "selector", the current selector till the cursor
+ * - If the state is "value", the corresponding property name
+ *
+ * In case of "value" and "property" states, we simply use the information
+ * provided by Gecko to filter out the possible suggestions.
+ * For "selector" state, we request the Inspector actor to query the page DOM
+ * and filter out the possible suggestions.
+ * For "media" and "keyframes" state, the only possible suggestions for now are
+ * "media" and "keyframes" respectively, although "media" can have suggestions
+ * like "max-width", "orientation" etc. Similarly "value" state can also have
+ * much better logical suggestions if we fine grain identify a sub state just
+ * like we do for the "selector" state.
+ */
+
+// Autocompletion types.
+
+/* eslint-disable no-inline-comments */
+const CSS_STATES = {
+ "null": "null",
+ property: "property", // foo { bar|: … }
+ value: "value", // foo {bar: baz|}
+ selector: "selector", // f| {bar: baz}
+ media: "media", // @med| , or , @media scr| { }
+ keyframes: "keyframes", // @keyf|
+ frame: "frame", // @keyframs foobar { t|
+};
+
+const SELECTOR_STATES = {
+ "null": "null",
+ id: "id", // #f|
+ class: "class", // #foo.b|
+ tag: "tag", // fo|
+ pseudo: "pseudo", // foo:|
+ attribute: "attribute", // foo[b|
+ value: "value", // foo[bar=b|
+};
+/* eslint-enable no-inline-comments */
+
+/**
+ * Constructor for the autocompletion object.
+ *
+ * @param options {Object} An options object containing the following options:
+ * - walker {Object} The object used for query selecting from the current
+ * target's DOM.
+ * - maxEntries {Number} Maximum selectors suggestions to display.
+ * - cssProperties {Object} The database of CSS properties.
+ */
+function CSSCompleter(options = {}) {
+ this.walker = options.walker;
+ this.maxEntries = options.maxEntries || 15;
+ // If no css properties database is passed in, default to the client list.
+ this.cssProperties = options.cssProperties || getClientCssProperties();
+
+ this.propertyNames = this.cssProperties.getNames().sort();
+
+ // Array containing the [line, ch, scopeStack] for the locations where the
+ // CSS state is "null"
+ this.nullStates = [];
+}
+
+CSSCompleter.prototype = {
+
+ /**
+ * Returns a list of suggestions based on the caret position.
+ *
+ * @param source {String} String of the source code.
+ * @param caret {Object} Cursor location with line and ch properties.
+ *
+ * @returns [{object}] A sorted list of objects containing the following
+ * peroperties:
+ * - label {String} Full keyword for the suggestion
+ * - preLabel {String} Already entered part of the label
+ */
+ complete: function (source, caret) {
+ // Getting the context from the caret position.
+ if (!this.resolveState(source, caret)) {
+ // We couldn't resolve the context, we won't be able to complete.
+ return Promise.resolve([]);
+ }
+
+ // Properly suggest based on the state.
+ switch (this.state) {
+ case CSS_STATES.property:
+ return this.completeProperties(this.completing);
+
+ case CSS_STATES.value:
+ return this.completeValues(this.propertyName, this.completing);
+
+ case CSS_STATES.selector:
+ return this.suggestSelectors();
+
+ case CSS_STATES.media:
+ case CSS_STATES.keyframes:
+ if ("media".startsWith(this.completing)) {
+ return Promise.resolve([{
+ label: "media",
+ preLabel: this.completing,
+ text: "media"
+ }]);
+ } else if ("keyframes".startsWith(this.completing)) {
+ return Promise.resolve([{
+ label: "keyframes",
+ preLabel: this.completing,
+ text: "keyframes"
+ }]);
+ }
+ }
+ return Promise.resolve([]);
+ },
+
+ /**
+ * Resolves the state of CSS at the cursor location. This method implements a
+ * custom written CSS state machine. The various switch statements provide the
+ * transition rules for the state. It also finds out various informatino about
+ * the nearby CSS like the property name being completed, the complete
+ * selector, etc.
+ *
+ * @param source {String} String of the source code.
+ * @param caret {Object} Cursor location with line and ch properties.
+ *
+ * @returns CSS_STATE
+ * One of CSS_STATE enum or null if the state cannot be resolved.
+ */
+ resolveState: function (source, {line, ch}) {
+ // Function to return the last element of an array
+ let peek = arr => arr[arr.length - 1];
+ // _state can be one of CSS_STATES;
+ let _state = CSS_STATES.null;
+ let selector = "";
+ let selectorState = SELECTOR_STATES.null;
+ let propertyName = null;
+ let scopeStack = [];
+ let selectors = [];
+
+ // Fetch the closest null state line, ch from cached null state locations
+ let matchedStateIndex = this.findNearestNullState(line);
+ if (matchedStateIndex > -1) {
+ let state = this.nullStates[matchedStateIndex];
+ line -= state[0];
+ if (line == 0) {
+ ch -= state[1];
+ }
+ source = source.split("\n").slice(state[0]);
+ source[0] = source[0].slice(state[1]);
+ source = source.join("\n");
+ scopeStack = [...state[2]];
+ this.nullStates.length = matchedStateIndex + 1;
+ } else {
+ this.nullStates = [];
+ }
+ let tokens = cssTokenizerWithLineColumn(source);
+ let tokIndex = tokens.length - 1;
+ if (tokIndex >= 0 &&
+ (tokens[tokIndex].loc.end.line < line ||
+ (tokens[tokIndex].loc.end.line === line &&
+ tokens[tokIndex].loc.end.column < ch))) {
+ // If the last token ends before the cursor location, we didn't
+ // tokenize it correctly. This special case can happen if the
+ // final token is a comment.
+ return null;
+ }
+
+ let cursor = 0;
+ // This will maintain a stack of paired elements like { & }, @m & }, : & ;
+ // etc
+ let token = null;
+ let selectorBeforeNot = null;
+ while (cursor <= tokIndex && (token = tokens[cursor++])) {
+ switch (_state) {
+ case CSS_STATES.property:
+ // From CSS_STATES.property, we can either go to CSS_STATES.value
+ // state when we hit the first ':' or CSS_STATES.selector if "}" is
+ // reached.
+ if (token.tokenType === "symbol") {
+ switch (token.text) {
+ case ":":
+ scopeStack.push(":");
+ if (tokens[cursor - 2].tokenType != "whitespace") {
+ propertyName = tokens[cursor - 2].text;
+ } else {
+ propertyName = tokens[cursor - 3].text;
+ }
+ _state = CSS_STATES.value;
+ break;
+
+ case "}":
+ if (/[{f]/.test(peek(scopeStack))) {
+ let popped = scopeStack.pop();
+ if (popped == "f") {
+ _state = CSS_STATES.frame;
+ } else {
+ selector = "";
+ selectors = [];
+ _state = CSS_STATES.null;
+ }
+ }
+ break;
+ }
+ }
+ break;
+
+ case CSS_STATES.value:
+ // From CSS_STATES.value, we can go to one of CSS_STATES.property,
+ // CSS_STATES.frame, CSS_STATES.selector and CSS_STATES.null
+ if (token.tokenType === "symbol") {
+ switch (token.text) {
+ case ";":
+ if (/[:]/.test(peek(scopeStack))) {
+ scopeStack.pop();
+ _state = CSS_STATES.property;
+ }
+ break;
+
+ case "}":
+ if (peek(scopeStack) == ":") {
+ scopeStack.pop();
+ }
+
+ if (/[{f]/.test(peek(scopeStack))) {
+ let popped = scopeStack.pop();
+ if (popped == "f") {
+ _state = CSS_STATES.frame;
+ } else {
+ selector = "";
+ selectors = [];
+ _state = CSS_STATES.null;
+ }
+ }
+ break;
+ }
+ }
+ break;
+
+ case CSS_STATES.selector:
+ // From CSS_STATES.selector, we can only go to CSS_STATES.property
+ // when we hit "{"
+ if (token.tokenType === "symbol" && token.text == "{") {
+ scopeStack.push("{");
+ _state = CSS_STATES.property;
+ selectors.push(selector);
+ selector = "";
+ break;
+ }
+
+ switch (selectorState) {
+ case SELECTOR_STATES.id:
+ case SELECTOR_STATES.class:
+ case SELECTOR_STATES.tag:
+ switch (token.tokenType) {
+ case "hash":
+ case "id":
+ selectorState = SELECTOR_STATES.id;
+ selector += "#" + token.text;
+ break;
+
+ case "symbol":
+ if (token.text == ".") {
+ selectorState = SELECTOR_STATES.class;
+ selector += ".";
+ if (cursor <= tokIndex &&
+ tokens[cursor].tokenType == "ident") {
+ token = tokens[cursor++];
+ selector += token.text;
+ }
+ } else if (token.text == "#") {
+ selectorState = SELECTOR_STATES.id;
+ selector += "#";
+ } else if (/[>~+]/.test(token.text)) {
+ selectorState = SELECTOR_STATES.null;
+ selector += token.text;
+ } else if (token.text == ",") {
+ selectorState = SELECTOR_STATES.null;
+ selectors.push(selector);
+ selector = "";
+ } else if (token.text == ":") {
+ selectorState = SELECTOR_STATES.pseudo;
+ selector += ":";
+ if (cursor > tokIndex) {
+ break;
+ }
+
+ token = tokens[cursor++];
+ switch (token.tokenType) {
+ case "function":
+ if (token.text == "not") {
+ selectorBeforeNot = selector;
+ selector = "";
+ scopeStack.push("(");
+ } else {
+ selector += token.text + "(";
+ }
+ selectorState = SELECTOR_STATES.null;
+ break;
+
+ case "ident":
+ selector += token.text;
+ break;
+ }
+ } else if (token.text == "[") {
+ selectorState = SELECTOR_STATES.attribute;
+ scopeStack.push("[");
+ selector += "[";
+ } else if (token.text == ")") {
+ if (peek(scopeStack) == "(") {
+ scopeStack.pop();
+ selector = selectorBeforeNot + "not(" + selector + ")";
+ selectorBeforeNot = null;
+ } else {
+ selector += ")";
+ }
+ selectorState = SELECTOR_STATES.null;
+ }
+ break;
+
+ case "whitespace":
+ selectorState = SELECTOR_STATES.null;
+ selector && (selector += " ");
+ break;
+ }
+ break;
+
+ case SELECTOR_STATES.null:
+ // From SELECTOR_STATES.null state, we can go to one of
+ // SELECTOR_STATES.id, SELECTOR_STATES.class or
+ // SELECTOR_STATES.tag
+ switch (token.tokenType) {
+ case "hash":
+ case "id":
+ selectorState = SELECTOR_STATES.id;
+ selector += "#" + token.text;
+ break;
+
+ case "ident":
+ selectorState = SELECTOR_STATES.tag;
+ selector += token.text;
+ break;
+
+ case "symbol":
+ if (token.text == ".") {
+ selectorState = SELECTOR_STATES.class;
+ selector += ".";
+ if (cursor <= tokIndex &&
+ tokens[cursor].tokenType == "ident") {
+ token = tokens[cursor++];
+ selector += token.text;
+ }
+ } else if (token.text == "#") {
+ selectorState = SELECTOR_STATES.id;
+ selector += "#";
+ } else if (token.text == "*") {
+ selectorState = SELECTOR_STATES.tag;
+ selector += "*";
+ } else if (/[>~+]/.test(token.text)) {
+ selector += token.text;
+ } else if (token.text == ",") {
+ selectorState = SELECTOR_STATES.null;
+ selectors.push(selector);
+ selector = "";
+ } else if (token.text == ":") {
+ selectorState = SELECTOR_STATES.pseudo;
+ selector += ":";
+ if (cursor > tokIndex) {
+ break;
+ }
+
+ token = tokens[cursor++];
+ switch (token.tokenType) {
+ case "function":
+ if (token.text == "not") {
+ selectorBeforeNot = selector;
+ selector = "";
+ scopeStack.push("(");
+ } else {
+ selector += token.text + "(";
+ }
+ selectorState = SELECTOR_STATES.null;
+ break;
+
+ case "ident":
+ selector += token.text;
+ break;
+ }
+ } else if (token.text == "[") {
+ selectorState = SELECTOR_STATES.attribute;
+ scopeStack.push("[");
+ selector += "[";
+ } else if (token.text == ")") {
+ if (peek(scopeStack) == "(") {
+ scopeStack.pop();
+ selector = selectorBeforeNot + "not(" + selector + ")";
+ selectorBeforeNot = null;
+ } else {
+ selector += ")";
+ }
+ selectorState = SELECTOR_STATES.null;
+ }
+ break;
+
+ case "whitespace":
+ selector && (selector += " ");
+ break;
+ }
+ break;
+
+ case SELECTOR_STATES.pseudo:
+ switch (token.tokenType) {
+ case "symbol":
+ if (/[>~+]/.test(token.text)) {
+ selectorState = SELECTOR_STATES.null;
+ selector += token.text;
+ } else if (token.text == ",") {
+ selectorState = SELECTOR_STATES.null;
+ selectors.push(selector);
+ selector = "";
+ } else if (token.text == ":") {
+ selectorState = SELECTOR_STATES.pseudo;
+ selector += ":";
+ if (cursor > tokIndex) {
+ break;
+ }
+
+ token = tokens[cursor++];
+ switch (token.tokenType) {
+ case "function":
+ if (token.text == "not") {
+ selectorBeforeNot = selector;
+ selector = "";
+ scopeStack.push("(");
+ } else {
+ selector += token.text + "(";
+ }
+ selectorState = SELECTOR_STATES.null;
+ break;
+
+ case "ident":
+ selector += token.text;
+ break;
+ }
+ } else if (token.text == "[") {
+ selectorState = SELECTOR_STATES.attribute;
+ scopeStack.push("[");
+ selector += "[";
+ }
+ break;
+
+ case "whitespace":
+ selectorState = SELECTOR_STATES.null;
+ selector && (selector += " ");
+ break;
+ }
+ break;
+
+ case SELECTOR_STATES.attribute:
+ switch (token.tokenType) {
+ case "symbol":
+ if (/[~|^$*]/.test(token.text)) {
+ selector += token.text;
+ token = tokens[cursor++];
+ } else if (token.text == "=") {
+ selectorState = SELECTOR_STATES.value;
+ selector += token.text;
+ } else if (token.text == "]") {
+ if (peek(scopeStack) == "[") {
+ scopeStack.pop();
+ }
+
+ selectorState = SELECTOR_STATES.null;
+ selector += "]";
+ }
+ break;
+
+ case "ident":
+ case "string":
+ selector += token.text;
+ break;
+
+ case "whitespace":
+ selector && (selector += " ");
+ break;
+ }
+ break;
+
+ case SELECTOR_STATES.value:
+ switch (token.tokenType) {
+ case "string":
+ case "ident":
+ selector += token.text;
+ break;
+
+ case "symbol":
+ if (token.text == "]") {
+ if (peek(scopeStack) == "[") {
+ scopeStack.pop();
+ }
+
+ selectorState = SELECTOR_STATES.null;
+ selector += "]";
+ }
+ break;
+
+ case "whitespace":
+ selector && (selector += " ");
+ break;
+ }
+ break;
+ }
+ break;
+
+ case CSS_STATES.null:
+ // From CSS_STATES.null state, we can go to either CSS_STATES.media or
+ // CSS_STATES.selector.
+ switch (token.tokenType) {
+ case "hash":
+ case "id":
+ selectorState = SELECTOR_STATES.id;
+ selector = "#" + token.text;
+ _state = CSS_STATES.selector;
+ break;
+
+ case "ident":
+ selectorState = SELECTOR_STATES.tag;
+ selector = token.text;
+ _state = CSS_STATES.selector;
+ break;
+
+ case "symbol":
+ if (token.text == ".") {
+ selectorState = SELECTOR_STATES.class;
+ selector = ".";
+ _state = CSS_STATES.selector;
+ if (cursor <= tokIndex &&
+ tokens[cursor].tokenType == "ident") {
+ token = tokens[cursor++];
+ selector += token.text;
+ }
+ } else if (token.text == "#") {
+ selectorState = SELECTOR_STATES.id;
+ selector = "#";
+ _state = CSS_STATES.selector;
+ } else if (token.text == "*") {
+ selectorState = SELECTOR_STATES.tag;
+ selector = "*";
+ _state = CSS_STATES.selector;
+ } else if (token.text == ":") {
+ _state = CSS_STATES.selector;
+ selectorState = SELECTOR_STATES.pseudo;
+ selector += ":";
+ if (cursor > tokIndex) {
+ break;
+ }
+
+ token = tokens[cursor++];
+ switch (token.tokenType) {
+ case "function":
+ if (token.text == "not") {
+ selectorBeforeNot = selector;
+ selector = "";
+ scopeStack.push("(");
+ } else {
+ selector += token.text + "(";
+ }
+ selectorState = SELECTOR_STATES.null;
+ break;
+
+ case "ident":
+ selector += token.text;
+ break;
+ }
+ } else if (token.text == "[") {
+ _state = CSS_STATES.selector;
+ selectorState = SELECTOR_STATES.attribute;
+ scopeStack.push("[");
+ selector += "[";
+ } else if (token.text == "}") {
+ if (peek(scopeStack) == "@m") {
+ scopeStack.pop();
+ }
+ }
+ break;
+
+ case "at":
+ _state = token.text.startsWith("m") ? CSS_STATES.media
+ : CSS_STATES.keyframes;
+ break;
+ }
+ break;
+
+ case CSS_STATES.media:
+ // From CSS_STATES.media, we can only go to CSS_STATES.null state when
+ // we hit the first '{'
+ if (token.tokenType == "symbol" && token.text == "{") {
+ scopeStack.push("@m");
+ _state = CSS_STATES.null;
+ }
+ break;
+
+ case CSS_STATES.keyframes:
+ // From CSS_STATES.keyframes, we can only go to CSS_STATES.frame state
+ // when we hit the first '{'
+ if (token.tokenType == "symbol" && token.text == "{") {
+ scopeStack.push("@k");
+ _state = CSS_STATES.frame;
+ }
+ break;
+
+ case CSS_STATES.frame:
+ // From CSS_STATES.frame, we can either go to CSS_STATES.property
+ // state when we hit the first '{' or to CSS_STATES.selector when we
+ // hit '}'
+ if (token.tokenType == "symbol") {
+ if (token.text == "{") {
+ scopeStack.push("f");
+ _state = CSS_STATES.property;
+ } else if (token.text == "}") {
+ if (peek(scopeStack) == "@k") {
+ scopeStack.pop();
+ }
+
+ _state = CSS_STATES.null;
+ }
+ }
+ break;
+ }
+ if (_state == CSS_STATES.null) {
+ if (this.nullStates.length == 0) {
+ this.nullStates.push([token.loc.end.line, token.loc.end.column,
+ [...scopeStack]]);
+ continue;
+ }
+ let tokenLine = token.loc.end.line;
+ let tokenCh = token.loc.end.column;
+ if (tokenLine == 0) {
+ continue;
+ }
+ if (matchedStateIndex > -1) {
+ tokenLine += this.nullStates[matchedStateIndex][0];
+ }
+ this.nullStates.push([tokenLine, tokenCh, [...scopeStack]]);
+ }
+ }
+ this.state = _state;
+ this.propertyName = _state == CSS_STATES.value ? propertyName : null;
+ this.selectorState = _state == CSS_STATES.selector ? selectorState : null;
+ this.selectorBeforeNot = selectorBeforeNot == null ?
+ null : selectorBeforeNot;
+ if (token) {
+ selector = selector.slice(0, selector.length + token.loc.end.column - ch);
+ this.selector = selector;
+ } else {
+ this.selector = "";
+ }
+ this.selectors = selectors;
+
+ if (token && token.tokenType != "whitespace") {
+ let text;
+ if (token.tokenType == "dimension" || !token.text) {
+ text = source.substring(token.startOffset, token.endOffset);
+ } else {
+ text = token.text;
+ }
+ this.completing = (text.slice(0, ch - token.loc.start.column)
+ .replace(/^[.#]$/, ""));
+ } else {
+ this.completing = "";
+ }
+ // Special case the situation when the user just entered ":" after typing a
+ // property name.
+ if (this.completing == ":" && _state == CSS_STATES.value) {
+ this.completing = "";
+ }
+
+ // Special check for !important; case.
+ if (token && tokens[cursor - 2] && tokens[cursor - 2].text == "!" &&
+ this.completing == "important".slice(0, this.completing.length)) {
+ this.completing = "!" + this.completing;
+ }
+ return _state;
+ },
+
+ /**
+ * Queries the DOM Walker actor for suggestions regarding the selector being
+ * completed
+ */
+ suggestSelectors: function () {
+ let walker = this.walker;
+ if (!walker) {
+ return Promise.resolve([]);
+ }
+
+ let query = this.selector;
+ // Even though the selector matched atleast one node, there is still
+ // possibility of suggestions.
+ switch (this.selectorState) {
+ case SELECTOR_STATES.null:
+ if (this.completing === ",") {
+ return Promise.resolve([]);
+ }
+
+ query += "*";
+ break;
+
+ case SELECTOR_STATES.tag:
+ query = query.slice(0, query.length - this.completing.length);
+ break;
+
+ case SELECTOR_STATES.id:
+ case SELECTOR_STATES.class:
+ case SELECTOR_STATES.pseudo:
+ if (/^[.:#]$/.test(this.completing)) {
+ query = query.slice(0, query.length - this.completing.length);
+ this.completing = "";
+ } else {
+ query = query.slice(0, query.length - this.completing.length - 1);
+ }
+ break;
+ }
+
+ if (/[\s+>~]$/.test(query) &&
+ this.selectorState != SELECTOR_STATES.attribute &&
+ this.selectorState != SELECTOR_STATES.value) {
+ query += "*";
+ }
+
+ // Set the values that this request was supposed to suggest to.
+ this._currentQuery = query;
+ return walker.getSuggestionsForQuery(query, this.completing,
+ this.selectorState)
+ .then(result => this.prepareSelectorResults(result));
+ },
+
+ /**
+ * Prepares the selector suggestions returned by the walker actor.
+ */
+ prepareSelectorResults: function (result) {
+ if (this._currentQuery != result.query) {
+ return [];
+ }
+
+ result = result.suggestions;
+ let query = this.selector;
+ let completion = [];
+ for (let [value, count, state] of result) {
+ switch (this.selectorState) {
+ case SELECTOR_STATES.id:
+ case SELECTOR_STATES.class:
+ case SELECTOR_STATES.pseudo:
+ if (/^[.:#]$/.test(this.completing)) {
+ value = query.slice(0, query.length - this.completing.length) +
+ value;
+ } else {
+ value = query.slice(0, query.length - this.completing.length - 1) +
+ value;
+ }
+ break;
+
+ case SELECTOR_STATES.tag:
+ value = query.slice(0, query.length - this.completing.length) +
+ value;
+ break;
+
+ case SELECTOR_STATES.null:
+ value = query + value;
+ break;
+
+ default:
+ value = query.slice(0, query.length - this.completing.length) +
+ value;
+ }
+
+ let item = {
+ label: value,
+ preLabel: query,
+ text: value,
+ score: count
+ };
+
+ // In case the query's state is tag and the item's state is id or class
+ // adjust the preLabel
+ if (this.selectorState === SELECTOR_STATES.tag &&
+ state === SELECTOR_STATES.class) {
+ item.preLabel = "." + item.preLabel;
+ }
+ if (this.selectorState === SELECTOR_STATES.tag &&
+ state === SELECTOR_STATES.id) {
+ item.preLabel = "#" + item.preLabel;
+ }
+
+ completion.push(item);
+
+ if (completion.length > this.maxEntries - 1) {
+ break;
+ }
+ }
+ return completion;
+ },
+
+ /**
+ * Returns CSS property name suggestions based on the input.
+ *
+ * @param startProp {String} Initial part of the property being completed.
+ */
+ completeProperties: function (startProp) {
+ let finalList = [];
+ if (!startProp) {
+ return Promise.resolve(finalList);
+ }
+
+ let length = this.propertyNames.length;
+ let i = 0, count = 0;
+ for (; i < length && count < this.maxEntries; i++) {
+ if (this.propertyNames[i].startsWith(startProp)) {
+ count++;
+ let propName = this.propertyNames[i];
+ finalList.push({
+ preLabel: startProp,
+ label: propName,
+ text: propName + ": "
+ });
+ } else if (this.propertyNames[i] > startProp) {
+ // We have crossed all possible matches alphabetically.
+ break;
+ }
+ }
+ return Promise.resolve(finalList);
+ },
+
+ /**
+ * Returns CSS value suggestions based on the corresponding property.
+ *
+ * @param propName {String} The property to which the value being completed
+ * belongs.
+ * @param startValue {String} Initial part of the value being completed.
+ */
+ completeValues: function (propName, startValue) {
+ let finalList = [];
+ let list = ["!important;", ...this.cssProperties.getValues(propName)];
+ // If there is no character being completed, we are showing an initial list
+ // of possible values. Skipping '!important' in this case.
+ if (!startValue) {
+ list.splice(0, 1);
+ }
+
+ let length = list.length;
+ let i = 0, count = 0;
+ for (; i < length && count < this.maxEntries; i++) {
+ if (list[i].startsWith(startValue)) {
+ count++;
+ let value = list[i];
+ finalList.push({
+ preLabel: startValue,
+ label: value,
+ text: value
+ });
+ } else if (list[i] > startValue) {
+ // We have crossed all possible matches alphabetically.
+ break;
+ }
+ }
+ return Promise.resolve(finalList);
+ },
+
+ /**
+ * A biased binary search in a sorted array where the middle element is
+ * calculated based on the values at the lower and the upper index in each
+ * iteration.
+ *
+ * This method returns the index of the closest null state from the passed
+ * `line` argument. Once we have the closest null state, we can start applying
+ * the state machine logic from that location instead of the absolute starting
+ * of the CSS source. This speeds up the tokenizing and the state machine a
+ * lot while using autocompletion at high line numbers in a CSS source.
+ */
+ findNearestNullState: function (line) {
+ let arr = this.nullStates;
+ let high = arr.length - 1;
+ let low = 0;
+ let target = 0;
+
+ if (high < 0) {
+ return -1;
+ }
+ if (arr[high][0] <= line) {
+ return high;
+ }
+ if (arr[low][0] > line) {
+ return -1;
+ }
+
+ while (high > low) {
+ if (arr[low][0] <= line && arr[low [0] + 1] > line) {
+ return low;
+ }
+ if (arr[high][0] > line && arr[high - 1][0] <= line) {
+ return high - 1;
+ }
+
+ target = (((line - arr[low][0]) / (arr[high][0] - arr[low][0])) *
+ (high - low)) | 0;
+
+ if (arr[target][0] <= line && arr[target + 1][0] > line) {
+ return target;
+ } else if (line > arr[target][0]) {
+ low = target + 1;
+ high--;
+ } else {
+ high = target - 1;
+ low++;
+ }
+ }
+
+ return -1;
+ },
+
+ /**
+ * Invalidates the state cache for and above the line.
+ */
+ invalidateCache: function (line) {
+ this.nullStates.length = this.findNearestNullState(line) + 1;
+ },
+
+ /**
+ * Get the state information about a token surrounding the {line, ch} position
+ *
+ * @param {string} source
+ * The complete source of the CSS file. Unlike resolve state method,
+ * this method requires the full source.
+ * @param {object} caret
+ * The line, ch position of the caret.
+ *
+ * @returns {object}
+ * An object containing the state of token covered by the caret.
+ * The object has following properties when the the state is
+ * "selector", "value" or "property", null otherwise:
+ * - state {string} one of CSS_STATES - "selector", "value" etc.
+ * - selector {string} The selector at the caret when `state` is
+ * selector. OR
+ * - selectors {[string]} Array of selector strings in case when
+ * `state` is "value" or "property"
+ * - propertyName {string} The property name at the current caret or
+ * the property name corresponding to the value at
+ * the caret.
+ * - value {string} The css value at the current caret.
+ * - loc {object} An object containing the starting and the ending
+ * caret position of the whole selector, value or property.
+ * - { start: {line, ch}, end: {line, ch}}
+ */
+ getInfoAt: function (source, caret) {
+ // Limits the input source till the {line, ch} caret position
+ function limit(sourceArg, {line, ch}) {
+ line++;
+ let list = sourceArg.split("\n");
+ if (list.length < line) {
+ return sourceArg;
+ }
+ if (line == 1) {
+ return list[0].slice(0, ch);
+ }
+ return [...list.slice(0, line - 1),
+ list[line - 1].slice(0, ch)].join("\n");
+ }
+
+ // Get the state at the given line, ch
+ let state = this.resolveState(limit(source, caret), caret);
+ let propertyName = this.propertyName;
+ let {line, ch} = caret;
+ let sourceArray = source.split("\n");
+ let limitedSource = limit(source, caret);
+
+ /**
+ * Method to traverse forwards from the caret location to figure out the
+ * ending point of a selector or css value.
+ *
+ * @param {function} check
+ * A method which takes the current state as an input and determines
+ * whether the state changed or not.
+ */
+ let traverseForward = check => {
+ let location;
+ // Backward loop to determine the beginning location of the selector.
+ do {
+ let lineText = sourceArray[line];
+ if (line == caret.line) {
+ lineText = lineText.substring(caret.ch);
+ }
+
+ let prevToken = undefined;
+ let tokens = cssTokenizer(lineText);
+ let found = false;
+ let ech = line == caret.line ? caret.ch : 0;
+ for (let token of tokens) {
+ // If the line is completely spaces, handle it differently
+ if (lineText.trim() == "") {
+ limitedSource += lineText;
+ } else {
+ limitedSource += sourceArray[line]
+ .substring(ech + token.startOffset,
+ ech + token.endOffset);
+ }
+
+ // Whitespace cannot change state.
+ if (token.tokenType == "whitespace") {
+ prevToken = token;
+ continue;
+ }
+
+ let forwState = this.resolveState(limitedSource, {
+ line: line,
+ ch: token.endOffset + ech
+ });
+ if (check(forwState)) {
+ if (prevToken && prevToken.tokenType == "whitespace") {
+ token = prevToken;
+ }
+ location = {
+ line: line,
+ ch: token.startOffset + ech
+ };
+ found = true;
+ break;
+ }
+ prevToken = token;
+ }
+ limitedSource += "\n";
+ if (found) {
+ break;
+ }
+ } while (line++ < sourceArray.length);
+ return location;
+ };
+
+ /**
+ * Method to traverse backwards from the caret location to figure out the
+ * starting point of a selector or css value.
+ *
+ * @param {function} check
+ * A method which takes the current state as an input and determines
+ * whether the state changed or not.
+ * @param {boolean} isValue
+ * true if the traversal is being done for a css value state.
+ */
+ let traverseBackwards = (check, isValue) => {
+ let location;
+ // Backward loop to determine the beginning location of the selector.
+ do {
+ let lineText = sourceArray[line];
+ if (line == caret.line) {
+ lineText = lineText.substring(0, caret.ch);
+ }
+
+ let tokens = Array.from(cssTokenizer(lineText));
+ let found = false;
+ for (let i = tokens.length - 1; i >= 0; i--) {
+ let token = tokens[i];
+ // If the line is completely spaces, handle it differently
+ if (lineText.trim() == "") {
+ limitedSource = limitedSource.slice(0, -1 * lineText.length);
+ } else {
+ let length = token.endOffset - token.startOffset;
+ limitedSource = limitedSource.slice(0, -1 * length);
+ }
+
+ // Whitespace cannot change state.
+ if (token.tokenType == "whitespace") {
+ continue;
+ }
+
+ let backState = this.resolveState(limitedSource, {
+ line: line,
+ ch: token.startOffset
+ });
+ if (check(backState)) {
+ if (tokens[i + 1] && tokens[i + 1].tokenType == "whitespace") {
+ token = tokens[i + 1];
+ }
+ location = {
+ line: line,
+ ch: isValue ? token.endOffset : token.startOffset
+ };
+ found = true;
+ break;
+ }
+ }
+ limitedSource = limitedSource.slice(0, -1);
+ if (found) {
+ break;
+ }
+ } while (line-- >= 0);
+ return location;
+ };
+
+ if (state == CSS_STATES.selector) {
+ // For selector state, the ending and starting point of the selector is
+ // either when the state changes or the selector becomes empty and a
+ // single selector can span multiple lines.
+ // Backward loop to determine the beginning location of the selector.
+ let start = traverseBackwards(backState => {
+ return (backState != CSS_STATES.selector ||
+ (this.selector == "" && this.selectorBeforeNot == null));
+ });
+
+ line = caret.line;
+ limitedSource = limit(source, caret);
+ // Forward loop to determine the ending location of the selector.
+ let end = traverseForward(forwState => {
+ return (forwState != CSS_STATES.selector ||
+ (this.selector == "" && this.selectorBeforeNot == null));
+ });
+
+ // Since we have start and end positions, figure out the whole selector.
+ let selector = source.split("\n").slice(start.line, end.line + 1);
+ selector[selector.length - 1] =
+ selector[selector.length - 1].substring(0, end.ch);
+ selector[0] = selector[0].substring(start.ch);
+ selector = selector.join("\n");
+ return {
+ state: state,
+ selector: selector,
+ loc: {
+ start: start,
+ end: end
+ }
+ };
+ } else if (state == CSS_STATES.property) {
+ // A property can only be a single word and thus very easy to calculate.
+ let tokens = cssTokenizer(sourceArray[line]);
+ for (let token of tokens) {
+ // Note that, because we're tokenizing a single line, the
+ // token's offset is also the column number.
+ if (token.startOffset <= ch && token.endOffset >= ch) {
+ return {
+ state: state,
+ propertyName: token.text,
+ selectors: this.selectors,
+ loc: {
+ start: {
+ line: line,
+ ch: token.startOffset
+ },
+ end: {
+ line: line,
+ ch: token.endOffset
+ }
+ }
+ };
+ }
+ }
+ } else if (state == CSS_STATES.value) {
+ // CSS value can be multiline too, so we go forward and backwards to
+ // determine the bounds of the value at caret
+ let start = traverseBackwards(backState => backState != CSS_STATES.value, true);
+
+ line = caret.line;
+ limitedSource = limit(source, caret);
+ let end = traverseForward(forwState => forwState != CSS_STATES.value);
+
+ let value = source.split("\n").slice(start.line, end.line + 1);
+ value[value.length - 1] = value[value.length - 1].substring(0, end.ch);
+ value[0] = value[0].substring(start.ch);
+ value = value.join("\n");
+ return {
+ state: state,
+ propertyName: propertyName,
+ selectors: this.selectors,
+ value: value,
+ loc: {
+ start: start,
+ end: end
+ }
+ };
+ }
+ return null;
+ }
+};
+
+module.exports = CSSCompleter;
diff --git a/devtools/client/sourceeditor/debugger.js b/devtools/client/sourceeditor/debugger.js
new file mode 100644
index 000000000..de63962a6
--- /dev/null
+++ b/devtools/client/sourceeditor/debugger.js
@@ -0,0 +1,336 @@
+/* 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 DevToolsUtils = require("devtools/shared/DevToolsUtils");
+const promise = require("promise");
+const dbginfo = new WeakMap();
+
+// These functions implement search within the debugger. Since
+// search in the debugger is different from other components,
+// we can't use search.js CodeMirror addon. This is a slightly
+// modified version of that addon. Depends on searchcursor.js.
+
+function SearchState() {
+ this.posFrom = this.posTo = this.query = null;
+}
+
+function getSearchState(cm) {
+ return cm.state.search || (cm.state.search = new SearchState());
+}
+
+function getSearchCursor(cm, query, pos) {
+ // If the query string is all lowercase, do a case insensitive search.
+ return cm.getSearchCursor(query, pos,
+ typeof query == "string" && query == query.toLowerCase());
+}
+
+/**
+ * If there's a saved search, selects the next results.
+ * Otherwise, creates a new search and selects the first
+ * result.
+ */
+function doSearch(ctx, rev, query) {
+ let { cm } = ctx;
+ let state = getSearchState(cm);
+
+ if (state.query) {
+ searchNext(ctx, rev);
+ return;
+ }
+
+ cm.operation(function () {
+ if (state.query) {
+ return;
+ }
+
+ state.query = query;
+ state.posFrom = state.posTo = { line: 0, ch: 0 };
+ searchNext(ctx, rev);
+ });
+}
+
+/**
+ * Selects the next result of a saved search.
+ */
+function searchNext(ctx, rev) {
+ let { cm, ed } = ctx;
+ cm.operation(function () {
+ let state = getSearchState(cm);
+ let cursor = getSearchCursor(cm, state.query,
+ rev ? state.posFrom : state.posTo);
+
+ if (!cursor.find(rev)) {
+ cursor = getSearchCursor(cm, state.query, rev ?
+ { line: cm.lastLine(), ch: null } : { line: cm.firstLine(), ch: 0 });
+ if (!cursor.find(rev)) {
+ return;
+ }
+ }
+
+ ed.alignLine(cursor.from().line, "center");
+ cm.setSelection(cursor.from(), cursor.to());
+ state.posFrom = cursor.from();
+ state.posTo = cursor.to();
+ });
+}
+
+/**
+ * Clears the currently saved search.
+ */
+function clearSearch(cm) {
+ let state = getSearchState(cm);
+
+ if (!state.query) {
+ return;
+ }
+
+ state.query = null;
+}
+
+// Exported functions
+
+/**
+ * This function is called whenever Editor is extended with functions
+ * from this module. See Editor.extend for more info.
+ */
+function initialize(ctx) {
+ let { ed } = ctx;
+
+ dbginfo.set(ed, {
+ breakpoints: {},
+ debugLocation: null
+ });
+}
+
+/**
+ * True if editor has a visual breakpoint at that line, false
+ * otherwise.
+ */
+function hasBreakpoint(ctx, line) {
+ let { cm } = ctx;
+ // In some rare occasions CodeMirror might not be properly initialized yet, so
+ // return an exceptional value in that case.
+ if (cm.lineInfo(line) === null) {
+ return null;
+ }
+ let markers = cm.lineInfo(line).wrapClass;
+
+ return markers != null &&
+ markers.includes("breakpoint");
+}
+
+/**
+ * Adds a visual breakpoint for a specified line. Third
+ * parameter 'cond' can hold any object.
+ *
+ * After adding a breakpoint, this function makes Editor to
+ * emit a breakpointAdded event.
+ */
+function addBreakpoint(ctx, line, cond) {
+ function _addBreakpoint() {
+ let { ed, cm } = ctx;
+ let meta = dbginfo.get(ed);
+ let info = cm.lineInfo(line);
+
+ // The line does not exist in the editor. This is harmless, the
+ // architecture calling this assumes the editor will handle this
+ // gracefully, and make sure breakpoints exist when they need to.
+ if (!info) {
+ return;
+ }
+
+ ed.addLineClass(line, "breakpoint");
+ meta.breakpoints[line] = { condition: cond };
+
+ // TODO(jwl): why is `info` null when breaking on page reload?
+ info.handle.on("delete", function onDelete() {
+ info.handle.off("delete", onDelete);
+ meta.breakpoints[info.line] = null;
+ });
+
+ if (cond) {
+ setBreakpointCondition(ctx, line);
+ }
+ ed.emit("breakpointAdded", line);
+ deferred.resolve();
+ }
+
+ if (hasBreakpoint(ctx, line)) {
+ return null;
+ }
+
+ let deferred = promise.defer();
+ // If lineInfo() returns null, wait a tick to give the editor a chance to
+ // initialize properly.
+ if (ctx.cm.lineInfo(line) === null) {
+ DevToolsUtils.executeSoon(() => _addBreakpoint());
+ } else {
+ _addBreakpoint();
+ }
+ return deferred.promise;
+}
+
+/**
+ * Helps reset the debugger's breakpoint state
+ * - removes the breakpoints in the editor
+ * - cleares the debugger's breakpoint state
+ *
+ * Note, does not *actually* remove a source's breakpoints.
+ * The canonical state is kept in the app state.
+ *
+ */
+function removeBreakpoints(ctx) {
+ let { ed, cm } = ctx;
+
+ let meta = dbginfo.get(ed);
+ if (meta.breakpoints != null) {
+ meta.breakpoints = {};
+ }
+
+ cm.doc.iter((line) => {
+ // The hasBreakpoint is a slow operation: checks the line type, whether cm
+ // is initialized and creates several new objects. Inlining the line's
+ // wrapClass property check directly.
+ if (line.wrapClass == null || !line.wrapClass.includes("breakpoint")) {
+ return;
+ }
+ removeBreakpoint(ctx, line);
+ });
+}
+
+/**
+ * Removes a visual breakpoint from a specified line and
+ * makes Editor emit a breakpointRemoved event.
+ */
+function removeBreakpoint(ctx, line) {
+ if (!hasBreakpoint(ctx, line)) {
+ return;
+ }
+
+ let { ed, cm } = ctx;
+ let meta = dbginfo.get(ed);
+ let info = cm.lineInfo(line);
+
+ meta.breakpoints[info.line] = null;
+ ed.removeLineClass(info.line, "breakpoint");
+ ed.removeLineClass(info.line, "conditional");
+ ed.emit("breakpointRemoved", line);
+}
+
+function moveBreakpoint(ctx, fromLine, toLine) {
+ let { ed } = ctx;
+
+ ed.removeBreakpoint(fromLine);
+ ed.addBreakpoint(toLine);
+}
+
+function setBreakpointCondition(ctx, line) {
+ let { ed, cm } = ctx;
+ let info = cm.lineInfo(line);
+
+ // The line does not exist in the editor. This is harmless, the
+ // architecture calling this assumes the editor will handle this
+ // gracefully, and make sure breakpoints exist when they need to.
+ if (!info) {
+ return;
+ }
+
+ ed.addLineClass(line, "conditional");
+}
+
+function removeBreakpointCondition(ctx, line) {
+ let { ed } = ctx;
+
+ ed.removeLineClass(line, "conditional");
+}
+
+/**
+ * Returns a list of all breakpoints in the current Editor.
+ */
+function getBreakpoints(ctx) {
+ let { ed } = ctx;
+ let meta = dbginfo.get(ed);
+
+ return Object.keys(meta.breakpoints).reduce((acc, line) => {
+ if (meta.breakpoints[line] != null) {
+ acc.push({ line: line, condition: meta.breakpoints[line].condition });
+ }
+ return acc;
+ }, []);
+}
+
+/**
+ * Saves a debug location information and adds a visual anchor to
+ * the breakpoints gutter. This is used by the debugger UI to
+ * display the line on which the Debugger is currently paused.
+ */
+function setDebugLocation(ctx, line) {
+ let { ed } = ctx;
+ let meta = dbginfo.get(ed);
+
+ clearDebugLocation(ctx);
+
+ meta.debugLocation = line;
+ ed.addLineClass(line, "debug-line");
+}
+
+/**
+ * Returns a line number that corresponds to the current debug
+ * location.
+ */
+function getDebugLocation(ctx) {
+ let { ed } = ctx;
+ let meta = dbginfo.get(ed);
+
+ return meta.debugLocation;
+}
+
+/**
+ * Clears the debug location. Clearing the debug location
+ * also removes a visual anchor from the breakpoints gutter.
+ */
+function clearDebugLocation(ctx) {
+ let { ed } = ctx;
+ let meta = dbginfo.get(ed);
+
+ if (meta.debugLocation != null) {
+ ed.removeLineClass(meta.debugLocation, "debug-line");
+ meta.debugLocation = null;
+ }
+}
+
+/**
+ * Starts a new search.
+ */
+function find(ctx, query) {
+ clearSearch(ctx.cm);
+ doSearch(ctx, false, query);
+}
+
+/**
+ * Finds the next item based on the currently saved search.
+ */
+function findNext(ctx, query) {
+ doSearch(ctx, false, query);
+}
+
+/**
+ * Finds the previous item based on the currently saved search.
+ */
+function findPrev(ctx, query) {
+ doSearch(ctx, true, query);
+}
+
+// Export functions
+
+[
+ initialize, hasBreakpoint, addBreakpoint, removeBreakpoint, moveBreakpoint,
+ setBreakpointCondition, removeBreakpointCondition, getBreakpoints, removeBreakpoints,
+ setDebugLocation, getDebugLocation, clearDebugLocation, find, findNext,
+ findPrev
+].forEach(func => {
+ module.exports[func.name] = func;
+});
diff --git a/devtools/client/sourceeditor/editor.js b/devtools/client/sourceeditor/editor.js
new file mode 100644
index 000000000..ce2136afc
--- /dev/null
+++ b/devtools/client/sourceeditor/editor.js
@@ -0,0 +1,1410 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2; fill-column: 80 -*- */
+/* vim:set ts=2 sw=2 sts=2 et 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 {
+ EXPAND_TAB,
+ TAB_SIZE,
+ DETECT_INDENT,
+ getIndentationFromIteration
+} = require("devtools/shared/indentation");
+
+const ENABLE_CODE_FOLDING = "devtools.editor.enableCodeFolding";
+const KEYMAP = "devtools.editor.keymap";
+const AUTO_CLOSE = "devtools.editor.autoclosebrackets";
+const AUTOCOMPLETE = "devtools.editor.autocomplete";
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+const VALID_KEYMAPS = new Set(["emacs", "vim", "sublime"]);
+
+// Maximum allowed margin (in number of lines) from top or bottom of the editor
+// while shifting to a line which was initially out of view.
+const MAX_VERTICAL_OFFSET = 3;
+
+// Match @Scratchpad/N:LINE[:COLUMN] or (LINE[:COLUMN]) anywhere at an end of
+// line in text selection.
+const RE_SCRATCHPAD_ERROR = /(?:@Scratchpad\/\d+:|\()(\d+):?(\d+)?(?:\)|\n)/;
+const RE_JUMP_TO_LINE = /^(\d+):?(\d+)?/;
+
+const Services = require("Services");
+const promise = require("promise");
+const events = require("devtools/shared/event-emitter");
+const { PrefObserver } = require("devtools/client/styleeditor/utils");
+const { getClientCssProperties } = require("devtools/shared/fronts/css-properties");
+const {KeyShortcuts} = require("devtools/client/shared/key-shortcuts");
+
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper("devtools/client/locales/sourceeditor.properties");
+
+const { OS } = Services.appinfo;
+
+// CM_STYLES, CM_SCRIPTS and CM_IFRAME represent the HTML,
+// JavaScript and CSS that is injected into an iframe in
+// order to initialize a CodeMirror instance.
+
+const CM_STYLES = [
+ "chrome://devtools/content/sourceeditor/codemirror/lib/codemirror.css",
+ "chrome://devtools/content/sourceeditor/codemirror/addon/dialog/dialog.css",
+ "chrome://devtools/content/sourceeditor/codemirror/mozilla.css"
+];
+
+const CM_SCRIPTS = [
+ "chrome://devtools/content/sourceeditor/codemirror/codemirror.bundle.js",
+];
+
+const CM_IFRAME =
+ "data:text/html;charset=utf8,<!DOCTYPE html>" +
+ "<html dir='ltr'>" +
+ " <head>" +
+ " <style>" +
+ " html, body { height: 100%; }" +
+ " body { margin: 0; overflow: hidden; }" +
+ " .CodeMirror { width: 100% !important; line-height: 1.25 !important; }" +
+ " </style>" +
+ CM_STYLES.map(style => "<link rel='stylesheet' href='" + style + "'>").join("\n") +
+ " </head>" +
+ " <body class='theme-body devtools-monospace'></body>" +
+ "</html>";
+
+const CM_MAPPING = [
+ "focus",
+ "hasFocus",
+ "lineCount",
+ "somethingSelected",
+ "getCursor",
+ "setSelection",
+ "getSelection",
+ "replaceSelection",
+ "extendSelection",
+ "undo",
+ "redo",
+ "clearHistory",
+ "openDialog",
+ "refresh",
+ "getScrollInfo",
+ "getViewport"
+];
+
+const editors = new WeakMap();
+
+Editor.modes = {
+ text: { name: "text" },
+ html: { name: "htmlmixed" },
+ css: { name: "css" },
+ wasm: { name: "wasm" },
+ js: { name: "javascript" },
+ vs: { name: "x-shader/x-vertex" },
+ fs: { name: "x-shader/x-fragment" }
+};
+
+/**
+ * A very thin wrapper around CodeMirror. Provides a number
+ * of helper methods to make our use of CodeMirror easier and
+ * another method, appendTo, to actually create and append
+ * the CodeMirror instance.
+ *
+ * Note that Editor doesn't expose CodeMirror instance to the
+ * outside world.
+ *
+ * Constructor accepts one argument, config. It is very
+ * similar to the CodeMirror configuration object so for most
+ * properties go to CodeMirror's documentation (see below).
+ *
+ * Other than that, it accepts one additional and optional
+ * property contextMenu. This property should be an element, or
+ * an ID of an element that we can use as a context menu.
+ *
+ * This object is also an event emitter.
+ *
+ * CodeMirror docs: http://codemirror.net/doc/manual.html
+ */
+function Editor(config) {
+ const tabSize = Services.prefs.getIntPref(TAB_SIZE);
+ const useTabs = !Services.prefs.getBoolPref(EXPAND_TAB);
+ const useAutoClose = Services.prefs.getBoolPref(AUTO_CLOSE);
+
+ this.version = null;
+ this.config = {
+ value: "",
+ mode: Editor.modes.text,
+ indentUnit: tabSize,
+ tabSize: tabSize,
+ contextMenu: null,
+ matchBrackets: true,
+ extraKeys: {},
+ indentWithTabs: useTabs,
+ inputStyle: "textarea",
+ styleActiveLine: true,
+ autoCloseBrackets: "()[]{}''\"\"``",
+ autoCloseEnabled: useAutoClose,
+ theme: "mozilla",
+ themeSwitching: true,
+ autocomplete: false,
+ autocompleteOpts: {}
+ };
+
+ // Additional shortcuts.
+ this.config.extraKeys[Editor.keyFor("jumpToLine")] = () => this.jumpToLine();
+ this.config.extraKeys[Editor.keyFor("moveLineUp", { noaccel: true })] =
+ () => this.moveLineUp();
+ this.config.extraKeys[Editor.keyFor("moveLineDown", { noaccel: true })] =
+ () => this.moveLineDown();
+ this.config.extraKeys[Editor.keyFor("toggleComment")] = "toggleComment";
+
+ // Disable ctrl-[ and ctrl-] because toolbox uses those shortcuts.
+ this.config.extraKeys[Editor.keyFor("indentLess")] = false;
+ this.config.extraKeys[Editor.keyFor("indentMore")] = false;
+
+ // Overwrite default config with user-provided, if needed.
+ Object.keys(config).forEach(k => {
+ if (k != "extraKeys") {
+ this.config[k] = config[k];
+ return;
+ }
+
+ if (!config.extraKeys) {
+ return;
+ }
+
+ Object.keys(config.extraKeys).forEach(key => {
+ this.config.extraKeys[key] = config.extraKeys[key];
+ });
+ });
+
+ if (!this.config.gutters) {
+ this.config.gutters = [];
+ }
+ if (this.config.lineNumbers
+ && this.config.gutters.indexOf("CodeMirror-linenumbers") === -1) {
+ this.config.gutters.push("CodeMirror-linenumbers");
+ }
+
+ // Remember the initial value of autoCloseBrackets.
+ this.config.autoCloseBracketsSaved = this.config.autoCloseBrackets;
+
+ // Overwrite default tab behavior. If something is selected,
+ // indent those lines. If nothing is selected and we're
+ // indenting with tabs, insert one tab. Otherwise insert N
+ // whitespaces where N == indentUnit option.
+ this.config.extraKeys.Tab = cm => {
+ if (cm.somethingSelected()) {
+ cm.indentSelection("add");
+ return;
+ }
+
+ if (this.config.indentWithTabs) {
+ cm.replaceSelection("\t", "end", "+input");
+ return;
+ }
+
+ let num = cm.getOption("indentUnit");
+ if (cm.getCursor().ch !== 0) {
+ num -= 1;
+ }
+ cm.replaceSelection(" ".repeat(num), "end", "+input");
+ };
+
+ // Allow add-ons to inject scripts for their editor instances
+ if (!this.config.externalScripts) {
+ this.config.externalScripts = [];
+ }
+
+ if (this.config.cssProperties) {
+ // Ensure that autocompletion has cssProperties if it's passed in via the options.
+ this.config.autocompleteOpts.cssProperties = this.config.cssProperties;
+ } else {
+ // Use a static client-side database of CSS values if none is provided.
+ this.config.cssProperties = getClientCssProperties();
+ }
+
+ events.decorate(this);
+}
+
+Editor.prototype = {
+ container: null,
+ version: null,
+ config: null,
+ Doc: null,
+
+ /**
+ * Exposes the CodeMirror instance. We want to get away from trying to
+ * abstract away the API entirely, and this makes it easier to integrate in
+ * various environments and do complex things.
+ */
+ get codeMirror() {
+ if (!editors.has(this)) {
+ throw new Error(
+ "CodeMirror instance does not exist. You must wait " +
+ "for it to be appended to the DOM."
+ );
+ }
+ return editors.get(this);
+ },
+
+ /**
+ * Appends the current Editor instance to the element specified by
+ * 'el'. You can also provide your won iframe to host the editor as
+ * an optional second parameter. This method actually creates and
+ * loads CodeMirror and all its dependencies.
+ *
+ * This method is asynchronous and returns a promise.
+ */
+ appendTo: function (el, env) {
+ let def = promise.defer();
+ let cm = editors.get(this);
+
+ if (!env) {
+ env = el.ownerDocument.createElementNS(XUL_NS, "iframe");
+ }
+
+ env.flex = 1;
+
+ if (cm) {
+ throw new Error("You can append an editor only once.");
+ }
+
+ let onLoad = () => {
+ let win = env.contentWindow.wrappedJSObject;
+
+ if (!this.config.themeSwitching) {
+ win.document.documentElement.setAttribute("force-theme", "light");
+ }
+
+ Services.scriptloader.loadSubScript(
+ "chrome://devtools/content/shared/theme-switching.js",
+ win, "utf8"
+ );
+ this.container = env;
+ this._setup(win.document.body, el.ownerDocument);
+ env.removeEventListener("load", onLoad, true);
+
+ def.resolve();
+ };
+
+ env.addEventListener("load", onLoad, true);
+ env.setAttribute("src", CM_IFRAME);
+ el.appendChild(env);
+
+ this.once("destroy", () => el.removeChild(env));
+ return def.promise;
+ },
+
+ appendToLocalElement: function (el) {
+ this._setup(el);
+ },
+
+ /**
+ * Do the actual appending and configuring of the CodeMirror instance. This is
+ * used by both append functions above, and does all the hard work to
+ * configure CodeMirror with all the right options/modes/etc.
+ */
+ _setup: function (el, doc) {
+ doc = doc || el.ownerDocument;
+ let win = el.ownerDocument.defaultView;
+
+ let scriptsToInject = CM_SCRIPTS.concat(this.config.externalScripts);
+ scriptsToInject.forEach(url => {
+ if (url.startsWith("chrome://")) {
+ Services.scriptloader.loadSubScript(url, win, "utf8");
+ }
+ });
+
+ // Replace the propertyKeywords, colorKeywords and valueKeywords
+ // properties of the CSS MIME type with the values provided by the CSS properties
+ // database.
+ const {
+ propertyKeywords,
+ colorKeywords,
+ valueKeywords
+ } = getCSSKeywords(this.config.cssProperties);
+
+ let cssSpec = win.CodeMirror.resolveMode("text/css");
+ cssSpec.propertyKeywords = propertyKeywords;
+ cssSpec.colorKeywords = colorKeywords;
+ cssSpec.valueKeywords = valueKeywords;
+ win.CodeMirror.defineMIME("text/css", cssSpec);
+
+ let scssSpec = win.CodeMirror.resolveMode("text/x-scss");
+ scssSpec.propertyKeywords = propertyKeywords;
+ scssSpec.colorKeywords = colorKeywords;
+ scssSpec.valueKeywords = valueKeywords;
+ win.CodeMirror.defineMIME("text/x-scss", scssSpec);
+
+ win.CodeMirror.commands.save = () => this.emit("saveRequested");
+
+ // Create a CodeMirror instance add support for context menus,
+ // overwrite the default controller (otherwise items in the top and
+ // context menus won't work).
+
+ let cm = win.CodeMirror(el, this.config);
+ this.Doc = win.CodeMirror.Doc;
+
+ // Disable APZ for source editors. It currently causes the line numbers to
+ // "tear off" and swim around on top of the content. Bug 1160601 tracks
+ // finding a solution that allows APZ to work with CodeMirror.
+ cm.getScrollerElement().addEventListener("wheel", ev => {
+ // By handling the wheel events ourselves, we force the platform to
+ // scroll synchronously, like it did before APZ. However, we lose smooth
+ // scrolling for users with mouse wheels. This seems acceptible vs.
+ // doing nothing and letting the gutter slide around.
+ ev.preventDefault();
+
+ let { deltaX, deltaY } = ev;
+
+ if (ev.deltaMode == ev.DOM_DELTA_LINE) {
+ deltaX *= cm.defaultCharWidth();
+ deltaY *= cm.defaultTextHeight();
+ } else if (ev.deltaMode == ev.DOM_DELTA_PAGE) {
+ deltaX *= cm.getWrapperElement().clientWidth;
+ deltaY *= cm.getWrapperElement().clientHeight;
+ }
+
+ cm.getScrollerElement().scrollBy(deltaX, deltaY);
+ });
+
+ cm.getWrapperElement().addEventListener("contextmenu", ev => {
+ ev.preventDefault();
+
+ if (!this.config.contextMenu) {
+ return;
+ }
+
+ let popup = this.config.contextMenu;
+ if (typeof popup == "string") {
+ popup = doc.getElementById(this.config.contextMenu);
+ }
+
+ this.emit("popupOpen", ev, popup);
+ popup.openPopupAtScreen(ev.screenX, ev.screenY, true);
+ }, false);
+
+ cm.on("focus", () => this.emit("focus"));
+ cm.on("scroll", () => this.emit("scroll"));
+ cm.on("change", () => {
+ this.emit("change");
+ if (!this._lastDirty) {
+ this._lastDirty = true;
+ this.emit("dirty-change");
+ }
+ });
+ cm.on("cursorActivity", () => this.emit("cursorActivity"));
+
+ cm.on("gutterClick", (cmArg, line, gutter, ev) => {
+ let head = { line: line, ch: 0 };
+ let tail = { line: line, ch: this.getText(line).length };
+
+ // Shift-click on a gutter selects the whole line.
+ if (ev.shiftKey) {
+ cmArg.setSelection(head, tail);
+ return;
+ }
+
+ this.emit("gutterClick", line, ev.button);
+ });
+
+ win.CodeMirror.defineExtension("l10n", (name) => {
+ return L10N.getStr(name);
+ });
+
+ this._initShortcuts(win);
+
+ editors.set(this, cm);
+
+ this.reloadPreferences = this.reloadPreferences.bind(this);
+ this._prefObserver = new PrefObserver("devtools.editor.");
+ this._prefObserver.on(TAB_SIZE, this.reloadPreferences);
+ this._prefObserver.on(EXPAND_TAB, this.reloadPreferences);
+ this._prefObserver.on(KEYMAP, this.reloadPreferences);
+ this._prefObserver.on(AUTO_CLOSE, this.reloadPreferences);
+ this._prefObserver.on(AUTOCOMPLETE, this.reloadPreferences);
+ this._prefObserver.on(DETECT_INDENT, this.reloadPreferences);
+ this._prefObserver.on(ENABLE_CODE_FOLDING, this.reloadPreferences);
+
+ this.reloadPreferences();
+
+ win.editor = this;
+ let editorReadyEvent = new win.CustomEvent("editorReady");
+ win.dispatchEvent(editorReadyEvent);
+ },
+
+ /**
+ * Returns a boolean indicating whether the editor is ready to
+ * use. Use appendTo(el).then(() => {}) for most cases
+ */
+ isAppended: function () {
+ return editors.has(this);
+ },
+
+ /**
+ * Returns the currently active highlighting mode.
+ * See Editor.modes for the list of all suppoert modes.
+ */
+ getMode: function () {
+ return this.getOption("mode");
+ },
+
+ /**
+ * Loads a script into editor's containing window.
+ */
+ loadScript: function (url) {
+ if (!this.container) {
+ throw new Error("Can't load a script until the editor is loaded.");
+ }
+ let win = this.container.contentWindow.wrappedJSObject;
+ Services.scriptloader.loadSubScript(url, win, "utf8");
+ },
+
+ /**
+ * Creates a CodeMirror Document
+ * @returns CodeMirror.Doc
+ */
+ createDocument: function () {
+ return new this.Doc("");
+ },
+
+ /**
+ * Replaces the current document with a new source document
+ */
+ replaceDocument: function (doc) {
+ let cm = editors.get(this);
+ cm.swapDoc(doc);
+ },
+
+ /**
+ * Changes the value of a currently used highlighting mode.
+ * See Editor.modes for the list of all supported modes.
+ */
+ setMode: function (value) {
+ this.setOption("mode", value);
+
+ // If autocomplete was set up and the mode is changing, then
+ // turn it off and back on again so the proper mode can be used.
+ if (this.config.autocomplete) {
+ this.setOption("autocomplete", false);
+ this.setOption("autocomplete", true);
+ }
+ },
+
+ /**
+ * Returns text from the text area. If line argument is provided
+ * the method returns only that line.
+ */
+ getText: function (line) {
+ let cm = editors.get(this);
+
+ if (line == null) {
+ return cm.getValue();
+ }
+
+ let info = cm.lineInfo(line);
+ return info ? cm.lineInfo(line).text : "";
+ },
+
+ /**
+ * Replaces whatever is in the text area with the contents of
+ * the 'value' argument.
+ */
+ setText: function (value) {
+ let cm = editors.get(this);
+ cm.setValue(value);
+
+ this.resetIndentUnit();
+ },
+
+ /**
+ * Reloads the state of the editor based on all current preferences.
+ * This is called automatically when any of the relevant preferences
+ * change.
+ */
+ reloadPreferences: function () {
+ // Restore the saved autoCloseBrackets value if it is preffed on.
+ let useAutoClose = Services.prefs.getBoolPref(AUTO_CLOSE);
+ this.setOption("autoCloseBrackets",
+ useAutoClose ? this.config.autoCloseBracketsSaved : false);
+
+ // If alternative keymap is provided, use it.
+ const keyMap = Services.prefs.getCharPref(KEYMAP);
+ if (VALID_KEYMAPS.has(keyMap)) {
+ this.setOption("keyMap", keyMap);
+ } else {
+ this.setOption("keyMap", "default");
+ }
+ this.updateCodeFoldingGutter();
+
+ this.resetIndentUnit();
+ this.setupAutoCompletion();
+ },
+
+ /**
+ * Sets the editor's indentation based on the current prefs and
+ * re-detect indentation if we should.
+ */
+ resetIndentUnit: function () {
+ let cm = editors.get(this);
+
+ let iterFn = function (start, end, callback) {
+ cm.eachLine(start, end, (line) => {
+ return callback(line.text);
+ });
+ };
+
+ let {indentUnit, indentWithTabs} = getIndentationFromIteration(iterFn);
+
+ cm.setOption("tabSize", indentUnit);
+ cm.setOption("indentUnit", indentUnit);
+ cm.setOption("indentWithTabs", indentWithTabs);
+ },
+
+ /**
+ * Replaces contents of a text area within the from/to {line, ch}
+ * range. If neither `from` nor `to` arguments are provided works
+ * exactly like setText. If only `from` object is provided, inserts
+ * text at that point, *overwriting* as many characters as needed.
+ */
+ replaceText: function (value, from, to) {
+ let cm = editors.get(this);
+
+ if (!from) {
+ this.setText(value);
+ return;
+ }
+
+ if (!to) {
+ let text = cm.getRange({ line: 0, ch: 0 }, from);
+ this.setText(text + value);
+ return;
+ }
+
+ cm.replaceRange(value, from, to);
+ },
+
+ /**
+ * Inserts text at the specified {line, ch} position, shifting existing
+ * contents as necessary.
+ */
+ insertText: function (value, at) {
+ let cm = editors.get(this);
+ cm.replaceRange(value, at, at);
+ },
+
+ /**
+ * Deselects contents of the text area.
+ */
+ dropSelection: function () {
+ if (!this.somethingSelected()) {
+ return;
+ }
+
+ this.setCursor(this.getCursor());
+ },
+
+ /**
+ * Returns true if there is more than one selection in the editor.
+ */
+ hasMultipleSelections: function () {
+ let cm = editors.get(this);
+ return cm.listSelections().length > 1;
+ },
+
+ /**
+ * Gets the first visible line number in the editor.
+ */
+ getFirstVisibleLine: function () {
+ let cm = editors.get(this);
+ return cm.lineAtHeight(0, "local");
+ },
+
+ /**
+ * Scrolls the view such that the given line number is the first visible line.
+ */
+ setFirstVisibleLine: function (line) {
+ let cm = editors.get(this);
+ let { top } = cm.charCoords({line: line, ch: 0}, "local");
+ cm.scrollTo(0, top);
+ },
+
+ /**
+ * Sets the cursor to the specified {line, ch} position with an additional
+ * option to align the line at the "top", "center" or "bottom" of the editor
+ * with "top" being default value.
+ */
+ setCursor: function ({line, ch}, align) {
+ let cm = editors.get(this);
+ this.alignLine(line, align);
+ cm.setCursor({line: line, ch: ch});
+ this.emit("cursorActivity");
+ },
+
+ /**
+ * Aligns the provided line to either "top", "center" or "bottom" of the
+ * editor view with a maximum margin of MAX_VERTICAL_OFFSET lines from top or
+ * bottom.
+ */
+ alignLine: function (line, align) {
+ let cm = editors.get(this);
+ let from = cm.lineAtHeight(0, "page");
+ let to = cm.lineAtHeight(cm.getWrapperElement().clientHeight, "page");
+ let linesVisible = to - from;
+ let halfVisible = Math.round(linesVisible / 2);
+
+ // If the target line is in view, skip the vertical alignment part.
+ if (line <= to && line >= from) {
+ return;
+ }
+
+ // Setting the offset so that the line always falls in the upper half
+ // of visible lines (lower half for bottom aligned).
+ // MAX_VERTICAL_OFFSET is the maximum allowed value.
+ let offset = Math.min(halfVisible, MAX_VERTICAL_OFFSET);
+
+ let topLine = {
+ "center": Math.max(line - halfVisible, 0),
+ "bottom": Math.max(line - linesVisible + offset, 0),
+ "top": Math.max(line - offset, 0)
+ }[align || "top"] || offset;
+
+ // Bringing down the topLine to total lines in the editor if exceeding.
+ topLine = Math.min(topLine, this.lineCount());
+ this.setFirstVisibleLine(topLine);
+ },
+
+ /**
+ * Returns whether a marker of a specified class exists in a line's gutter.
+ */
+ hasMarker: function (line, gutterName, markerClass) {
+ let marker = this.getMarker(line, gutterName);
+ if (!marker) {
+ return false;
+ }
+
+ return marker.classList.contains(markerClass);
+ },
+
+ /**
+ * Adds a marker with a specified class to a line's gutter. If another marker
+ * exists on that line, the new marker class is added to its class list.
+ */
+ addMarker: function (line, gutterName, markerClass) {
+ let cm = editors.get(this);
+ let info = cm.lineInfo(line);
+ if (!info) {
+ return;
+ }
+
+ let gutterMarkers = info.gutterMarkers;
+ let marker;
+ if (gutterMarkers) {
+ marker = gutterMarkers[gutterName];
+ if (marker) {
+ marker.classList.add(markerClass);
+ return;
+ }
+ }
+
+ marker = cm.getWrapperElement().ownerDocument.createElement("div");
+ marker.className = markerClass;
+ cm.setGutterMarker(info.line, gutterName, marker);
+ },
+
+ /**
+ * The reverse of addMarker. Removes a marker of a specified class from a
+ * line's gutter.
+ */
+ removeMarker: function (line, gutterName, markerClass) {
+ if (!this.hasMarker(line, gutterName, markerClass)) {
+ return;
+ }
+
+ let cm = editors.get(this);
+ cm.lineInfo(line).gutterMarkers[gutterName].classList.remove(markerClass);
+ },
+
+ /**
+ * Adds a marker with a specified class and an HTML content to a line's
+ * gutter. If another marker exists on that line, it is overwritten by a new
+ * marker.
+ */
+ addContentMarker: function (line, gutterName, markerClass, content) {
+ let cm = editors.get(this);
+ let info = cm.lineInfo(line);
+ if (!info) {
+ return;
+ }
+
+ let marker = cm.getWrapperElement().ownerDocument.createElement("div");
+ marker.className = markerClass;
+ marker.innerHTML = content;
+ cm.setGutterMarker(info.line, gutterName, marker);
+ },
+
+ /**
+ * The reverse of addContentMarker. Removes any line's markers in the
+ * specified gutter.
+ */
+ removeContentMarker: function (line, gutterName) {
+ let cm = editors.get(this);
+ let info = cm.lineInfo(line);
+ if (!info) {
+ return;
+ }
+
+ cm.setGutterMarker(info.line, gutterName, null);
+ },
+
+ getMarker: function (line, gutterName) {
+ let cm = editors.get(this);
+ let info = cm.lineInfo(line);
+ if (!info) {
+ return null;
+ }
+
+ let gutterMarkers = info.gutterMarkers;
+ if (!gutterMarkers) {
+ return null;
+ }
+
+ return gutterMarkers[gutterName];
+ },
+
+ /**
+ * Removes all gutter markers in the gutter with the given name.
+ */
+ removeAllMarkers: function (gutterName) {
+ let cm = editors.get(this);
+ cm.clearGutter(gutterName);
+ },
+
+ /**
+ * Handles attaching a set of events listeners on a marker. They should
+ * be passed as an object literal with keys as event names and values as
+ * function listeners. The line number, marker node and optional data
+ * will be passed as arguments to the function listener.
+ *
+ * You don't need to worry about removing these event listeners.
+ * They're automatically orphaned when clearing markers.
+ */
+ setMarkerListeners: function (line, gutterName, markerClass, eventsArg, data) {
+ if (!this.hasMarker(line, gutterName, markerClass)) {
+ return;
+ }
+
+ let cm = editors.get(this);
+ let marker = cm.lineInfo(line).gutterMarkers[gutterName];
+
+ for (let name in eventsArg) {
+ let listener = eventsArg[name].bind(this, line, marker, data);
+ marker.addEventListener(name, listener);
+ }
+ },
+
+ /**
+ * Returns whether a line is decorated using the specified class name.
+ */
+ hasLineClass: function (line, className) {
+ let cm = editors.get(this);
+ let info = cm.lineInfo(line);
+
+ if (!info || !info.wrapClass) {
+ return false;
+ }
+
+ return info.wrapClass.split(" ").indexOf(className) != -1;
+ },
+
+ /**
+ * Sets a CSS class name for the given line, including the text and gutter.
+ */
+ addLineClass: function (line, className) {
+ let cm = editors.get(this);
+ cm.addLineClass(line, "wrap", className);
+ },
+
+ /**
+ * The reverse of addLineClass.
+ */
+ removeLineClass: function (line, className) {
+ let cm = editors.get(this);
+ cm.removeLineClass(line, "wrap", className);
+ },
+
+ /**
+ * Mark a range of text inside the two {line, ch} bounds. Since the range may
+ * be modified, for example, when typing text, this method returns a function
+ * that can be used to remove the mark.
+ */
+ markText: function (from, to, className = "marked-text") {
+ let cm = editors.get(this);
+ let text = cm.getRange(from, to);
+ let span = cm.getWrapperElement().ownerDocument.createElement("span");
+ span.className = className;
+ span.textContent = text;
+
+ let mark = cm.markText(from, to, { replacedWith: span });
+ return {
+ anchor: span,
+ clear: () => mark.clear()
+ };
+ },
+
+ /**
+ * Calculates and returns one or more {line, ch} objects for
+ * a zero-based index who's value is relative to the start of
+ * the editor's text.
+ *
+ * If only one argument is given, this method returns a single
+ * {line,ch} object. Otherwise it returns an array.
+ */
+ getPosition: function (...args) {
+ let cm = editors.get(this);
+ let res = args.map((ind) => cm.posFromIndex(ind));
+ return args.length === 1 ? res[0] : res;
+ },
+
+ /**
+ * The reverse of getPosition. Similarly to getPosition this
+ * method returns a single value if only one argument was given
+ * and an array otherwise.
+ */
+ getOffset: function (...args) {
+ let cm = editors.get(this);
+ let res = args.map((pos) => cm.indexFromPos(pos));
+ return args.length > 1 ? res : res[0];
+ },
+
+ /**
+ * Returns a {line, ch} object that corresponds to the
+ * left, top coordinates.
+ */
+ getPositionFromCoords: function ({left, top}) {
+ let cm = editors.get(this);
+ return cm.coordsChar({ left: left, top: top });
+ },
+
+ /**
+ * The reverse of getPositionFromCoords. Similarly, returns a {left, top}
+ * object that corresponds to the specified line and character number.
+ */
+ getCoordsFromPosition: function ({line, ch}) {
+ let cm = editors.get(this);
+ return cm.charCoords({ line: ~~line, ch: ~~ch });
+ },
+
+ /**
+ * Returns true if there's something to undo and false otherwise.
+ */
+ canUndo: function () {
+ let cm = editors.get(this);
+ return cm.historySize().undo > 0;
+ },
+
+ /**
+ * Returns true if there's something to redo and false otherwise.
+ */
+ canRedo: function () {
+ let cm = editors.get(this);
+ return cm.historySize().redo > 0;
+ },
+
+ /**
+ * Marks the contents as clean and returns the current
+ * version number.
+ */
+ setClean: function () {
+ let cm = editors.get(this);
+ this.version = cm.changeGeneration();
+ this._lastDirty = false;
+ this.emit("dirty-change");
+ return this.version;
+ },
+
+ /**
+ * Returns true if contents of the text area are
+ * clean i.e. no changes were made since the last version.
+ */
+ isClean: function () {
+ let cm = editors.get(this);
+ return cm.isClean(this.version);
+ },
+
+ /**
+ * This method opens an in-editor dialog asking for a line to
+ * jump to. Once given, it changes cursor to that line.
+ */
+ jumpToLine: function () {
+ let doc = editors.get(this).getWrapperElement().ownerDocument;
+ let div = doc.createElement("div");
+ let inp = doc.createElement("input");
+ let txt = doc.createTextNode(L10N.getStr("gotoLineCmd.promptTitle"));
+
+ inp.type = "text";
+ inp.style.width = "10em";
+ inp.style.marginInlineStart = "1em";
+
+ div.appendChild(txt);
+ div.appendChild(inp);
+
+ if (!this.hasMultipleSelections()) {
+ let cm = editors.get(this);
+ let sel = cm.getSelection();
+ // Scratchpad inserts and selects a comment after an error happens:
+ // "@Scratchpad/1:10:2". Parse this to get the line and column.
+ // In the string above this is line 10, column 2.
+ let match = sel.match(RE_SCRATCHPAD_ERROR);
+ if (match) {
+ let [, line, column ] = match;
+ inp.value = column ? line + ":" + column : line;
+ inp.selectionStart = inp.selectionEnd = inp.value.length;
+ }
+ }
+
+ this.openDialog(div, (line) => {
+ // Handle LINE:COLUMN as well as LINE
+ let match = line.toString().match(RE_JUMP_TO_LINE);
+ if (match) {
+ let [, matchLine, column ] = match;
+ this.setCursor({line: matchLine - 1, ch: column ? column - 1 : 0 });
+ }
+ });
+ },
+
+ /**
+ * Moves the content of the current line or the lines selected up a line.
+ */
+ moveLineUp: function () {
+ let cm = editors.get(this);
+ let start = cm.getCursor("start");
+ let end = cm.getCursor("end");
+
+ if (start.line === 0) {
+ return;
+ }
+
+ // Get the text in the lines selected or the current line of the cursor
+ // and append the text of the previous line.
+ let value;
+ if (start.line !== end.line) {
+ value = cm.getRange({ line: start.line, ch: 0 },
+ { line: end.line, ch: cm.getLine(end.line).length }) + "\n";
+ } else {
+ value = cm.getLine(start.line) + "\n";
+ }
+ value += cm.getLine(start.line - 1);
+
+ // Replace the previous line and the currently selected lines with the new
+ // value and maintain the selection of the text.
+ cm.replaceRange(value, { line: start.line - 1, ch: 0 },
+ { line: end.line, ch: cm.getLine(end.line).length });
+ cm.setSelection({ line: start.line - 1, ch: start.ch },
+ { line: end.line - 1, ch: end.ch });
+ },
+
+ /**
+ * Moves the content of the current line or the lines selected down a line.
+ */
+ moveLineDown: function () {
+ let cm = editors.get(this);
+ let start = cm.getCursor("start");
+ let end = cm.getCursor("end");
+
+ if (end.line + 1 === cm.lineCount()) {
+ return;
+ }
+
+ // Get the text of next line and append the text in the lines selected
+ // or the current line of the cursor.
+ let value = cm.getLine(end.line + 1) + "\n";
+ if (start.line !== end.line) {
+ value += cm.getRange({ line: start.line, ch: 0 },
+ { line: end.line, ch: cm.getLine(end.line).length });
+ } else {
+ value += cm.getLine(start.line);
+ }
+
+ // Replace the currently selected lines and the next line with the new
+ // value and maintain the selection of the text.
+ cm.replaceRange(value, { line: start.line, ch: 0 },
+ { line: end.line + 1, ch: cm.getLine(end.line + 1).length});
+ cm.setSelection({ line: start.line + 1, ch: start.ch },
+ { line: end.line + 1, ch: end.ch });
+ },
+
+ /**
+ * Intercept CodeMirror's Find and replace key shortcut to select the search input
+ */
+ findOrReplace: function (node, isReplaceAll) {
+ let cm = editors.get(this);
+ let isInput = node.tagName === "INPUT";
+ let isSearchInput = isInput && node.type === "search";
+ // replace box is a different input instance than search, and it is
+ // located in a code mirror dialog
+ let isDialogInput = isInput &&
+ node.parentNode &&
+ node.parentNode.classList.contains("CodeMirror-dialog");
+ if (!(isSearchInput || isDialogInput)) {
+ return;
+ }
+
+ if (isSearchInput || isReplaceAll) {
+ // select the search input
+ // it's the precise reason why we reimplement these key shortcuts
+ node.select();
+ }
+
+ // need to call it since we prevent the propagation of the event and
+ // cancel codemirror's key handling
+ cm.execCommand("find");
+ },
+
+ /**
+ * Intercept CodeMirror's findNext and findPrev key shortcut to allow
+ * immediately search for next occurance after typing a word to search.
+ */
+ findNextOrPrev: function (node, isFindPrev) {
+ let cm = editors.get(this);
+ let isInput = node.tagName === "INPUT";
+ let isSearchInput = isInput && node.type === "search";
+ if (!isSearchInput) {
+ return;
+ }
+ let query = node.value;
+ // cm.state.search allows to automatically start searching for the next occurance
+ // it's the precise reason why we reimplement these key shortcuts
+ if (!cm.state.search || cm.state.search.query !== query) {
+ cm.state.search = {
+ posFrom: null,
+ posTo: null,
+ overlay: null,
+ query
+ };
+ }
+
+ // need to call it since we prevent the propagation of the event and
+ // cancel codemirror's key handling
+ if (isFindPrev) {
+ cm.execCommand("findPrev");
+ } else {
+ cm.execCommand("findNext");
+ }
+ },
+
+ /**
+ * Returns current font size for the editor area, in pixels.
+ */
+ getFontSize: function () {
+ let cm = editors.get(this);
+ let el = cm.getWrapperElement();
+ let win = el.ownerDocument.defaultView;
+
+ return parseInt(win.getComputedStyle(el).getPropertyValue("font-size"), 10);
+ },
+
+ /**
+ * Sets font size for the editor area.
+ */
+ setFontSize: function (size) {
+ let cm = editors.get(this);
+ cm.getWrapperElement().style.fontSize = parseInt(size, 10) + "px";
+ cm.refresh();
+ },
+
+ /**
+ * Sets an option for the editor. For most options it just defers to
+ * CodeMirror.setOption, but certain ones are maintained within the editor
+ * instance.
+ */
+ setOption: function (o, v) {
+ let cm = editors.get(this);
+
+ // Save the state of a valid autoCloseBrackets string, so we can reset
+ // it if it gets preffed off and back on.
+ if (o === "autoCloseBrackets" && v) {
+ this.config.autoCloseBracketsSaved = v;
+ }
+
+ if (o === "autocomplete") {
+ this.config.autocomplete = v;
+ this.setupAutoCompletion();
+ } else {
+ cm.setOption(o, v);
+ this.config[o] = v;
+ }
+
+ if (o === "enableCodeFolding") {
+ // The new value maybe explicitly force foldGUtter on or off, ignoring
+ // the prefs service.
+ this.updateCodeFoldingGutter();
+ }
+ },
+
+ /**
+ * Gets an option for the editor. For most options it just defers to
+ * CodeMirror.getOption, but certain ones are maintained within the editor
+ * instance.
+ */
+ getOption: function (o) {
+ let cm = editors.get(this);
+ if (o === "autocomplete") {
+ return this.config.autocomplete;
+ }
+
+ return cm.getOption(o);
+ },
+
+ /**
+ * Sets up autocompletion for the editor. Lazily imports the required
+ * dependencies because they vary by editor mode.
+ *
+ * Autocompletion is special, because we don't want to automatically use
+ * it just because it is preffed on (it still needs to be requested by the
+ * editor), but we do want to always disable it if it is preffed off.
+ */
+ setupAutoCompletion: function () {
+ // The autocomplete module will overwrite this.initializeAutoCompletion
+ // with a mode specific autocompletion handler.
+ if (!this.initializeAutoCompletion) {
+ this.extend(require("./autocomplete"));
+ }
+
+ if (this.config.autocomplete && Services.prefs.getBoolPref(AUTOCOMPLETE)) {
+ this.initializeAutoCompletion(this.config.autocompleteOpts);
+ } else {
+ this.destroyAutoCompletion();
+ }
+ },
+
+ /**
+ * Extends an instance of the Editor object with additional
+ * functions. Each function will be called with context as
+ * the first argument. Context is a {ed, cm} object where
+ * 'ed' is an instance of the Editor object and 'cm' is an
+ * instance of the CodeMirror object. Example:
+ *
+ * function hello(ctx, name) {
+ * let { cm, ed } = ctx;
+ * cm; // CodeMirror instance
+ * ed; // Editor instance
+ * name; // 'Mozilla'
+ * }
+ *
+ * editor.extend({ hello: hello });
+ * editor.hello('Mozilla');
+ */
+ extend: function (funcs) {
+ Object.keys(funcs).forEach(name => {
+ let cm = editors.get(this);
+ let ctx = { ed: this, cm: cm, Editor: Editor};
+
+ if (name === "initialize") {
+ funcs[name](ctx);
+ return;
+ }
+
+ this[name] = funcs[name].bind(null, ctx);
+ });
+ },
+
+ destroy: function () {
+ this.container = null;
+ this.config = null;
+ this.version = null;
+
+ if (this._prefObserver) {
+ this._prefObserver.off(TAB_SIZE, this.reloadPreferences);
+ this._prefObserver.off(EXPAND_TAB, this.reloadPreferences);
+ this._prefObserver.off(KEYMAP, this.reloadPreferences);
+ this._prefObserver.off(AUTO_CLOSE, this.reloadPreferences);
+ this._prefObserver.off(AUTOCOMPLETE, this.reloadPreferences);
+ this._prefObserver.off(DETECT_INDENT, this.reloadPreferences);
+ this._prefObserver.off(ENABLE_CODE_FOLDING, this.reloadPreferences);
+ this._prefObserver.destroy();
+ }
+
+ this.emit("destroy");
+ },
+
+ updateCodeFoldingGutter: function () {
+ let shouldFoldGutter = this.config.enableCodeFolding;
+ let foldGutterIndex = this.config.gutters.indexOf("CodeMirror-foldgutter");
+ let cm = editors.get(this);
+
+ if (shouldFoldGutter === undefined) {
+ shouldFoldGutter = Services.prefs.getBoolPref(ENABLE_CODE_FOLDING);
+ }
+
+ if (shouldFoldGutter) {
+ // Add the gutter before enabling foldGutter
+ if (foldGutterIndex === -1) {
+ let gutters = this.config.gutters.slice();
+ gutters.push("CodeMirror-foldgutter");
+ this.setOption("gutters", gutters);
+ }
+
+ this.setOption("foldGutter", true);
+ } else {
+ // No code should remain folded when folding is off.
+ if (cm) {
+ cm.execCommand("unfoldAll");
+ }
+
+ // Remove the gutter so it doesn't take up space
+ if (foldGutterIndex !== -1) {
+ let gutters = this.config.gutters.slice();
+ gutters.splice(foldGutterIndex, 1);
+ this.setOption("gutters", gutters);
+ }
+
+ this.setOption("foldGutter", false);
+ }
+ },
+
+ /**
+ * Register all key shortcuts.
+ */
+ _initShortcuts: function (win) {
+ let shortcuts = new KeyShortcuts({
+ window: win
+ });
+ this._onShortcut = this._onShortcut.bind(this);
+ let keys = [
+ "find.key",
+ "findNext.key",
+ "findPrev.key"
+ ];
+
+ if (OS === "Darwin") {
+ keys.push("replaceAllMac.key");
+ } else {
+ keys.push("replaceAll.key");
+ }
+ // Process generic keys:
+ keys.forEach(name => {
+ let key = L10N.getStr(name);
+ shortcuts.on(key, (_, event) => this._onShortcut(name, event));
+ });
+ },
+ /**
+ * Key shortcut listener.
+ */
+ _onShortcut: function (name, event) {
+ if (!this._isInputOrTextarea(event.target)) {
+ return;
+ }
+ let node = event.originalTarget;
+
+ switch (name) {
+ // replaceAll.key is Alt + find.key
+ case "replaceAllMac.key":
+ this.findOrReplace(node, true);
+ break;
+ // replaceAll.key is Shift + find.key
+ case "replaceAll.key":
+ this.findOrReplace(node, true);
+ break;
+ case "find.key":
+ this.findOrReplace(node, false);
+ break;
+ // findPrev.key is Shift + findNext.key
+ case "findPrev.key":
+ this.findNextOrPrev(node, true);
+ break;
+ case "findNext.key":
+ this.findNextOrPrev(node, false);
+ break;
+ default:
+ console.error("Unexpected editor key shortcut", name);
+ return;
+ }
+ // Prevent default for this action
+ event.stopPropagation();
+ event.preventDefault();
+ },
+
+ /**
+ * Check if a node is an input or textarea
+ */
+ _isInputOrTextarea: function (element) {
+ let name = element.tagName.toLowerCase();
+ return name === "input" || name === "textarea";
+ }
+};
+
+// Since Editor is a thin layer over CodeMirror some methods
+// are mapped directly—without any changes.
+
+CM_MAPPING.forEach(name => {
+ Editor.prototype[name] = function (...args) {
+ let cm = editors.get(this);
+ return cm[name].apply(cm, args);
+ };
+});
+
+// Static methods on the Editor object itself.
+
+/**
+ * Returns a string representation of a shortcut 'key' with
+ * a OS specific modifier. Cmd- for Macs, Ctrl- for other
+ * platforms. Useful with extraKeys configuration option.
+ *
+ * CodeMirror defines all keys with modifiers in the following
+ * order: Shift - Ctrl/Cmd - Alt - Key
+ */
+Editor.accel = function (key, modifiers = {}) {
+ return (modifiers.shift ? "Shift-" : "") +
+ (Services.appinfo.OS == "Darwin" ? "Cmd-" : "Ctrl-") +
+ (modifiers.alt ? "Alt-" : "") + key;
+};
+
+/**
+ * Returns a string representation of a shortcut for a
+ * specified command 'cmd'. Append Cmd- for macs, Ctrl- for other
+ * platforms unless noaccel is specified in the options. Useful when overwriting
+ * or disabling default shortcuts.
+ */
+Editor.keyFor = function (cmd, opts = { noaccel: false }) {
+ let key = L10N.getStr(cmd + ".commandkey");
+ return opts.noaccel ? key : Editor.accel(key);
+};
+
+/**
+ * We compute the CSS property names, values, and color names to be used with
+ * CodeMirror to more closely reflect what is supported by the target platform.
+ * The database is used to replace the values used in CodeMirror while initiating
+ * an editor object. This is done here instead of the file codemirror/css.js so
+ * as to leave that file untouched and easily upgradable.
+ */
+function getCSSKeywords(cssProperties) {
+ function keySet(array) {
+ let keys = {};
+ for (let i = 0; i < array.length; ++i) {
+ keys[array[i]] = true;
+ }
+ return keys;
+ }
+
+ let propertyKeywords = cssProperties.getNames();
+ let colorKeywords = {};
+ let valueKeywords = {};
+
+ propertyKeywords.forEach(property => {
+ if (property.includes("color")) {
+ cssProperties.getValues(property).forEach(value => {
+ colorKeywords[value] = true;
+ });
+ } else {
+ cssProperties.getValues(property).forEach(value => {
+ valueKeywords[value] = true;
+ });
+ }
+ });
+
+ return {
+ propertyKeywords: keySet(propertyKeywords),
+ colorKeywords: colorKeywords,
+ valueKeywords: valueKeywords
+ };
+}
+
+module.exports = Editor;
diff --git a/devtools/client/sourceeditor/moz.build b/devtools/client/sourceeditor/moz.build
new file mode 100644
index 000000000..5081325c5
--- /dev/null
+++ b/devtools/client/sourceeditor/moz.build
@@ -0,0 +1,18 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DIRS += [
+ 'tern',
+]
+
+DevToolsModules(
+ 'autocomplete.js',
+ 'css-autocompleter.js',
+ 'debugger.js',
+ 'editor.js'
+)
+
+BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
diff --git a/devtools/client/sourceeditor/tern/README b/devtools/client/sourceeditor/tern/README
new file mode 100644
index 000000000..d41e7b456
--- /dev/null
+++ b/devtools/client/sourceeditor/tern/README
@@ -0,0 +1,13 @@
+This is the Tern code-analysis engine packaged for the Mozilla Project.
+
+Tern is a stand-alone code-analysis engine for JavaScript. It is intended to be used with a code editor plugin to enhance the editor's support for intelligent JavaScript editing
+
+
+# Upgrade
+
+Currently used version is 0.6.2. To upgrade, download the latest release from http://ternjs.net/, and copy the files from lib/ into this directory.
+
+You may also need to update the CodeMirror plugin found in devtools/client/sourceeditor/codemirror/addon/tern, but it will most likely work without updating.
+
+Replace instances of `require("acorn")` with `require("acorn/acorn")`
+Replace instances of `acorn/dist/` with `acorn/` \ No newline at end of file
diff --git a/devtools/client/sourceeditor/tern/browser.js b/devtools/client/sourceeditor/tern/browser.js
new file mode 100644
index 000000000..a37d6569a
--- /dev/null
+++ b/devtools/client/sourceeditor/tern/browser.js
@@ -0,0 +1,2921 @@
+module.exports = {
+ "!name": "browser",
+ "location": {
+ "assign": {
+ "!type": "fn(url: string)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.location",
+ "!doc": "Load the document at the provided URL."
+ },
+ "replace": {
+ "!type": "fn(url: string)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.location",
+ "!doc": "Replace the current document with the one at the provided URL. The difference from the assign() method is that after using replace() the current page will not be saved in session history, meaning the user won't be able to use the Back button to navigate to it."
+ },
+ "reload": {
+ "!type": "fn()",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.location",
+ "!doc": "Reload the document from the current URL. forceget is a boolean, which, when it is true, causes the page to always be reloaded from the server. If it is false or not specified, the browser may reload the page from its cache."
+ },
+ "origin": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.location",
+ "!doc": "The origin of the URL."
+ },
+ "hash": {
+ "!type": "string",
+ "!url": "https://developer.mthat follows the # symbol, including the # symbol."
+ },
+ "search": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.location",
+ "!doc": "The part of the URL that follows the ? symbol, including the ? symbol."
+ },
+ "pathname": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.location",
+ "!doc": "The path (relative to the host)."
+ },
+ "port": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.location",
+ "!doc": "The port number of the URL."
+ },
+ "hostname": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.location",
+ "!doc": "The host name (without the port number or square brackets)."
+ },
+ "host": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.location",
+ "!doc": "The host name and port number."
+ },
+ "protocol": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.location",
+ "!doc": "The protocol of the URL."
+ },
+ "href": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.location",
+ "!doc": "The entire URL."
+ },
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.location",
+ "!doc": "Returns a location object with information about the current location of the document. Assigning to the location property changes the current page to the new address."
+ },
+ "Node": {
+ "!type": "fn()",
+ "prototype": {
+ "parentElement": {
+ "!type": "+Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.parentElement",
+ "!doc": "Returns the DOM node's parent Element, or null if the node either has no parent, or its parent isn't a DOM Element."
+ },
+ "textContent": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.textContent",
+ "!doc": "Gets or sets the text content of a node and its descendants."
+ },
+ "baseURI": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.baseURI",
+ "!doc": "The absolute base URI of a node or null if unable to obtain an absolute URI."
+ },
+ "localName": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.localName",
+ "!doc": "Returns the local part of the qualified name of this node."
+ },
+ "prefix": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.prefix",
+ "!doc": "Returns the namespace prefix of the specified node, or null if no prefix is specified. This property is read only."
+ },
+ "namespaceURI": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.namespaceURI",
+ "!doc": "The namespace URI of the node, or null if the node is not in a namespace (read-only). When the node is a document, it returns the XML namespace for the current document."
+ },
+ "ownerDocument": {
+ "!type": "+Document",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.ownerDocument",
+ "!doc": "The ownerDocument property returns the top-level document object for this node."
+ },
+ "attributes": {
+ "!type": "+NamedNodeMap",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.attributes",
+ "!doc": "A collection of all attribute nodes registered to the specified node. It is a NamedNodeMap,not an Array, so it has no Array methods and the Attr nodes' indexes may differ among browsers."
+ },
+ "nextSibling": {
+ "!type": "+Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.nextSibling",
+ "!doc": "Returns the node immediately following the specified one in its parent's childNodes list, or null if the specified node is the last node in that list."
+ },
+ "previousSibling": {
+ "!type": "+Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.previousSibling",
+ "!doc": "Returns the node immediately preceding the specified one in its parent's childNodes list, null if the specified node is the first in that list."
+ },
+ "lastChild": {
+ "!type": "+Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.lastChild",
+ "!doc": "Returns the last child of a node."
+ },
+ "firstChild": {
+ "!type": "+Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.firstChild",
+ "!doc": "Returns the node's first child in the tree, or null if the node is childless. If the node is a Document, it returns the first node in the list of its direct children."
+ },
+ "childNodes": {
+ "!type": "+NodeList",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.childNodes",
+ "!doc": "Returns a collection of child nodes of the given element."
+ },
+ "parentNode": {
+ "!type": "+Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.parentNode",
+ "!doc": "Returns the parent of the specified node in the DOM tree."
+ },
+ "nodeType": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.nodeType",
+ "!doc": "Returns an integer code representing the type of the node."
+ },
+ "nodeValue": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.nodeValue",
+ "!doc": "Returns or sets the value of the current node."
+ },
+ "nodeName": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.nodeName",
+ "!doc": "Returns the name of the current node as a string."
+ },
+ "tagName": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.nodeName",
+ "!doc": "Returns the name of the current node as a string."
+ },
+ "insertBefore": {
+ "!type": "fn(newElt: +Element, before: +Element) -> +Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.insertBefore",
+ "!doc": "Inserts the specified node before a reference element as a child of the current node."
+ },
+ "replaceChild": {
+ "!type": "fn(newElt: +Element, oldElt: +Element) -> +Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.replaceChild",
+ "!doc": "Replaces one child node of the specified element with another."
+ },
+ "removeChild": {
+ "!type": "fn(oldElt: +Element) -> +Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.removeChild",
+ "!doc": "Removes a child node from the DOM. Returns removed node."
+ },
+ "appendChild": {
+ "!type": "fn(newElt: +Element) -> +Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.appendChild",
+ "!doc": "Adds a node to the end of the list of children of a specified parent node. If the node already exists it is removed from current parent node, then added to new parent node."
+ },
+ "hasChildNodes": {
+ "!type": "fn() -> bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.hasChildNodes",
+ "!doc": "Returns a Boolean value indicating whether the current Node has child nodes or not."
+ },
+ "cloneNode": {
+ "!type": "fn(deep: bool) -> +Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.cloneNode",
+ "!doc": "Returns a duplicate of the node on which this method was called."
+ },
+ "normalize": {
+ "!type": "fn()",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.normalize",
+ "!doc": "Puts the specified node and all of its subtree into a \"normalized\" form. In a normalized subtree, no text nodes in the subtree are empty and there are no adjacent text nodes."
+ },
+ "isSupported": {
+ "!type": "fn(features: string, version: number) -> bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.isSupported",
+ "!doc": "Tests whether the DOM implementation implements a specific feature and that feature is supported by this node."
+ },
+ "hasAttributes": {
+ "!type": "fn() -> bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.hasAttributes",
+ "!doc": "Returns a boolean value of true or false, indicating if the current element has any attributes or not."
+ },
+ "lookupPrefix": {
+ "!type": "fn(uri: string) -> string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.lookupPrefix",
+ "!doc": "Returns the prefix for a given namespaceURI if present, and null if not. When multiple prefixes are possible, the result is implementation-dependent."
+ },
+ "isDefaultNamespace": {
+ "!type": "fn(uri: string) -> bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.isDefaultNamespace",
+ "!doc": "Accepts a namespace URI as an argument and returns true if the namespace is the default namespace on the given node or false if not."
+ },
+ "lookupNamespaceURI": {
+ "!type": "fn(uri: string) -> string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.lookupNamespaceURI",
+ "!doc": "Takes a prefix and returns the namespaceURI associated with it on the given node if found (and null if not). Supplying null for the prefix will return the default namespace."
+ },
+ "addEventListener": {
+ "!type": "fn(type: string, listener: fn(e: +Event), capture: bool)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/EventTarget.addEventListener",
+ "!doc": "Registers a single event listener on a single target. The event target may be a single element in a document, the document itself, a window, or an XMLHttpRequest."
+ },
+ "removeEventListener": {
+ "!type": "fn(type: string, listener: fn(), capture: bool)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/EventTarget.removeEventListener",
+ "!doc": "Allows the removal of event listeners from the event target."
+ },
+ "isSameNode": {
+ "!type": "fn(other: +Node) -> bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.isSameNode",
+ "!doc": "Tests whether two nodes are the same, that is they reference the same object."
+ },
+ "isEqualNode": {
+ "!type": "fn(other: +Node) -> bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.isEqualNode",
+ "!doc": "Tests whether two nodes are equal."
+ },
+ "compareDocumentPosition": {
+ "!type": "fn(other: +Node) -> number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.compareDocumentPosition",
+ "!doc": "Compares the position of the current node against another node in any other document."
+ },
+ "contains": {
+ "!type": "fn(other: +Node) -> bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.contains",
+ "!doc": "Indicates whether a node is a descendent of a given node."
+ },
+ "dispatchEvent": {
+ "!type": "fn(event: +Event) -> bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/EventTarget.dispatchEvent",
+ "!doc": "Dispatches an event into the event system. The event is subject to the same capturing and bubbling behavior as directly dispatched events."
+ },
+ "ELEMENT_NODE": "number",
+ "ATTRIBUTE_NODE": "number",
+ "TEXT_NODE": "number",
+ "CDATA_SECTION_NODE": "number",
+ "ENTITY_REFERENCE_NODE": "number",
+ "ENTITY_NODE": "number",
+ "PROCESSING_INSTRUCTION_NODE": "number",
+ "COMMENT_NODE": "number",
+ "DOCUMENT_NODE": "number",
+ "DOCUMENT_TYPE_NODE": "number",
+ "DOCUMENT_FRAGMENT_NODE": "number",
+ "NOTATION_NODE": "number",
+ "DOCUMENT_POSITION_DISCONNECTED": "number",
+ "DOCUMENT_POSITION_PRECEDING": "number",
+ "DOCUMENT_POSITION_FOLLOWING": "number",
+ "DOCUMENT_POSITION_CONTAINS": "number",
+ "DOCUMENT_POSITION_CONTAINED_BY": "number",
+ "DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC": "number"
+ },
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node",
+ "!doc": "A Node is an interface from which a number of DOM types inherit, and allows these various types to be treated (or tested) similarly."
+ },
+ "Element": {
+ "!type": "fn()",
+ "prototype": {
+ "!proto": "Node.prototype",
+ "getAttribute": {
+ "!type": "fn(name: string) -> string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.getAttribute",
+ "!doc": "Returns the value of the named attribute on the specified element. If the named attribute does not exist, the value returned will either be null or \"\" (the empty string)."
+ },
+ "setAttribute": {
+ "!type": "fn(name: string, value: string)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.setAttribute",
+ "!doc": "Adds a new attribute or changes the value of an existing attribute on the specified element."
+ },
+ "removeAttribute": {
+ "!type": "fn(name: string)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.removeAttribute",
+ "!doc": "Removes an attribute from the specified element."
+ },
+ "getAttributeNode": {
+ "!type": "fn(name: string) -> +Attr",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.getAttributeNode",
+ "!doc": "Returns the specified attribute of the specified element, as an Attr node."
+ },
+ "getElementsByTagName": {
+ "!type": "fn(tagName: string) -> +NodeList",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.getElementsByTagName",
+ "!doc": "Returns a list of elements with the given tag name. The subtree underneath the specified element is searched, excluding the element itself. The returned list is live, meaning that it updates itself with the DOM tree automatically. Consequently, there is no need to call several times element.getElementsByTagName with the same element and arguments."
+ },
+ "getElementsByTagNameNS": {
+ "!type": "fn(ns: string, tagName: string) -> +NodeList",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.getElementsByTagNameNS",
+ "!doc": "Returns a list of elements with the given tag name belonging to the given namespace."
+ },
+ "getAttributeNS": {
+ "!type": "fn(ns: string, name: string) -> string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.getAttributeNS",
+ "!doc": "Returns the string value of the attribute with the specified namespace and name. If the named attribute does not exist, the value returned will either be null or \"\" (the empty string)."
+ },
+ "setAttributeNS": {
+ "!type": "fn(ns: string, name: string, value: string)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.setAttributeNS",
+ "!doc": "Adds a new attribute or changes the value of an attribute with the given namespace and name."
+ },
+ "removeAttributeNS": {
+ "!type": "fn(ns: string, name: string)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.removeAttributeNS",
+ "!doc": "removeAttributeNS removes the specified attribute from an element."
+ },
+ "getAttributeNodeNS": {
+ "!type": "fn(ns: string, name: string) -> +Attr",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.getAttributeNodeNS",
+ "!doc": "Returns the Attr node for the attribute with the given namespace and name."
+ },
+ "hasAttribute": {
+ "!type": "fn(name: string) -> bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.hasAttribute",
+ "!doc": "hasAttribute returns a boolean value indicating whether the specified element has the specified attribute or not."
+ },
+ "hasAttributeNS": {
+ "!type": "fn(ns: string, name: string) -> bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.hasAttributeNS",
+ "!doc": "hasAttributeNS returns a boolean value indicating whether the current element has the specified attribute."
+ },
+ "focus": {
+ "!type": "fn()",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.focus",
+ "!doc": "Sets focus on the specified element, if it can be focused."
+ },
+ "blur": {
+ "!type": "fn()",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.blur",
+ "!doc": "The blur method removes keyboard focus from the current element."
+ },
+ "scrollIntoView": {
+ "!type": "fn(top: bool)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.scrollIntoView",
+ "!doc": "The scrollIntoView() method scrolls the element into view."
+ },
+ "scrollByLines": {
+ "!type": "fn(lines: number)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.scrollByLines",
+ "!doc": "Scrolls the document by the given number of lines."
+ },
+ "scrollByPages": {
+ "!type": "fn(pages: number)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.scrollByPages",
+ "!doc": "Scrolls the current document by the specified number of pages."
+ },
+ "getElementsByClassName": {
+ "!type": "fn(name: string) -> +NodeList",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.getElementsByClassName",
+ "!doc": "Returns a set of elements which have all the given class names. When called on the document object, the complete document is searched, including the root node. You may also call getElementsByClassName on any element; it will return only elements which are descendants of the specified root element with the given class names."
+ },
+ "querySelector": {
+ "!type": "fn(selectors: string) -> +Node",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Element.querySelector",
+ "!doc": "Returns the first element that is a descendent of the element on which it is invoked that matches the specified group of selectors."
+ },
+ "querySelectorAll": {
+ "!type": "fn(selectors: string) -> +NodeList",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Element.querySelectorAll",
+ "!doc": "Returns a non-live NodeList of all elements descended from the element on which it is invoked that match the specified group of CSS selectors."
+ },
+ "getClientRects": {
+ "!type": "fn() -> [+ClientRect]",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.getClientRects",
+ "!doc": "Returns a collection of rectangles that indicate the bounding rectangles for each box in a client."
+ },
+ "getBoundingClientRect": {
+ "!type": "fn() -> +ClientRect",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.getBoundingClientRect",
+ "!doc": "Returns a text rectangle object that encloses a group of text rectangles."
+ },
+ "setAttributeNode": {
+ "!type": "fn(attr: +Attr) -> +Attr",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.setAttributeNode",
+ "!doc": "Adds a new Attr node to the specified element."
+ },
+ "removeAttributeNode": {
+ "!type": "fn(attr: +Attr) -> +Attr",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.removeAttributeNode",
+ "!doc": "Removes the specified attribute from the current element."
+ },
+ "setAttributeNodeNS": {
+ "!type": "fn(attr: +Attr) -> +Attr",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.setAttributeNodeNS",
+ "!doc": "Adds a new namespaced attribute node to an element."
+ },
+ "insertAdjacentHTML": {
+ "!type": "fn(position: string, text: string)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.insertAdjacentHTML",
+ "!doc": "Parses the specified text as HTML or XML and inserts the resulting nodes into the DOM tree at a specified position. It does not reparse the element it is being used on and thus it does not corrupt the existing elements inside the element. This, and avoiding the extra step of serialization make it much faster than direct innerHTML manipulation."
+ },
+ "children": {
+ "!type": "+HTMLCollection",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Element.children",
+ "!doc": "Returns a collection of child elements of the given element."
+ },
+ "childElementCount": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Element.childElementCount",
+ "!doc": "Returns the number of child elements of the given element."
+ },
+ "className": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.className",
+ "!doc": "Gets and sets the value of the class attribute of the specified element."
+ },
+ "style": {
+ "cssText": "string",
+ "alignmentBaseline": "string",
+ "background": "string",
+ "backgroundAttachment": "string",
+ "backgroundClip": "string",
+ "backgroundColor": "string",
+ "backgroundImage": "string",
+ "backgroundOrigin": "string",
+ "backgroundPosition": "string",
+ "backgroundPositionX": "string",
+ "backgroundPositionY": "string",
+ "backgroundRepeat": "string",
+ "backgroundRepeatX": "string",
+ "backgroundRepeatY": "string",
+ "backgroundSize": "string",
+ "baselineShift": "string",
+ "border": "string",
+ "borderBottom": "string",
+ "borderBottomColor": "string",
+ "borderBottomLeftRadius": "string",
+ "borderBottomRightRadius": "string",
+ "borderBottomStyle": "string",
+ "borderBottomWidth": "string",
+ "borderCollapse": "string",
+ "borderColor": "string",
+ "borderImage": "string",
+ "borderImageOutset": "string",
+ "borderImageRepeat": "string",
+ "borderImageSlice": "string",
+ "borderImageSource": "string",
+ "borderImageWidth": "string",
+ "borderLeft": "string",
+ "borderLeftColor": "string",
+ "borderLeftStyle": "string",
+ "borderLeftWidth": "string",
+ "borderRadius": "string",
+ "borderRight": "string",
+ "borderRightColor": "string",
+ "borderRightStyle": "string",
+ "borderRightWidth": "string",
+ "borderSpacing": "string",
+ "borderStyle": "string",
+ "borderTop": "string",
+ "borderTopColor": "string",
+ "borderTopLeftRadius": "string",
+ "borderTopRightRadius": "string",
+ "borderTopStyle": "string",
+ "borderTopWidth": "string",
+ "borderWidth": "string",
+ "bottom": "string",
+ "boxShadow": "string",
+ "boxSizing": "string",
+ "captionSide": "string",
+ "clear": "string",
+ "clip": "string",
+ "clipPath": "string",
+ "clipRule": "string",
+ "color": "string",
+ "colorInterpolation": "string",
+ "colorInterpolationFilters": "string",
+ "colorProfile": "string",
+ "colorRendering": "string",
+ "content": "string",
+ "counterIncrement": "string",
+ "counterReset": "string",
+ "cursor": "string",
+ "direction": "string",
+ "display": "string",
+ "dominantBaseline": "string",
+ "emptyCells": "string",
+ "enableBackground": "string",
+ "fill": "string",
+ "fillOpacity": "string",
+ "fillRule": "string",
+ "filter": "string",
+ "float": "string",
+ "floodColor": "string",
+ "floodOpacity": "string",
+ "font": "string",
+ "fontFamily": "string",
+ "fontSize": "string",
+ "fontStretch": "string",
+ "fontStyle": "string",
+ "fontVariant": "string",
+ "fontWeight": "string",
+ "glyphOrientationHorizontal": "string",
+ "glyphOrientationVertical": "string",
+ "height": "string",
+ "imageRendering": "string",
+ "kerning": "string",
+ "left": "string",
+ "letterSpacing": "string",
+ "lightingColor": "string",
+ "lineHeight": "string",
+ "listStyle": "string",
+ "listStyleImage": "string",
+ "listStylePosition": "string",
+ "listStyleType": "string",
+ "margin": "string",
+ "marginBottom": "string",
+ "marginLeft": "string",
+ "marginRight": "string",
+ "marginTop": "string",
+ "marker": "string",
+ "markerEnd": "string",
+ "markerMid": "string",
+ "markerStart": "string",
+ "mask": "string",
+ "maxHeight": "string",
+ "maxWidth": "string",
+ "minHeight": "string",
+ "minWidth": "string",
+ "opacity": "string",
+ "orphans": "string",
+ "outline": "string",
+ "outlineColor": "string",
+ "outlineOffset": "string",
+ "outlineStyle": "string",
+ "outlineWidth": "string",
+ "overflow": "string",
+ "overflowWrap": "string",
+ "overflowX": "string",
+ "overflowY": "string",
+ "padding": "string",
+ "paddingBottom": "string",
+ "paddingLeft": "string",
+ "paddingRight": "string",
+ "paddingTop": "string",
+ "page": "string",
+ "pageBreakAfter": "string",
+ "pageBreakBefore": "string",
+ "pageBreakInside": "string",
+ "pointerEvents": "string",
+ "position": "string",
+ "quotes": "string",
+ "resize": "string",
+ "right": "string",
+ "shapeRendering": "string",
+ "size": "string",
+ "speak": "string",
+ "src": "string",
+ "stopColor": "string",
+ "stopOpacity": "string",
+ "stroke": "string",
+ "strokeDasharray": "string",
+ "strokeDashoffset": "string",
+ "strokeLinecap": "string",
+ "strokeLinejoin": "string",
+ "strokeMiterlimit": "string",
+ "strokeOpacity": "string",
+ "strokeWidth": "string",
+ "tabSize": "string",
+ "tableLayout": "string",
+ "textAlign": "string",
+ "textAnchor": "string",
+ "textDecoration": "string",
+ "textIndent": "string",
+ "textLineThrough": "string",
+ "textLineThroughColor": "string",
+ "textLineThroughMode": "string",
+ "textLineThroughStyle": "string",
+ "textLineThroughWidth": "string",
+ "textOverflow": "string",
+ "textOverline": "string",
+ "textOverlineColor": "string",
+ "textOverlineMode": "string",
+ "textOverlineStyle": "string",
+ "textOverlineWidth": "string",
+ "textRendering": "string",
+ "textShadow": "string",
+ "textTransform": "string",
+ "textUnderline": "string",
+ "textUnderlineColor": "string",
+ "textUnderlineMode": "string",
+ "textUnderlineStyle": "string",
+ "textUnderlineWidth": "string",
+ "top": "string",
+ "unicodeBidi": "string",
+ "unicodeRange": "string",
+ "vectorEffect": "string",
+ "verticalAlign": "string",
+ "visibility": "string",
+ "whiteSpace": "string",
+ "width": "string",
+ "wordBreak": "string",
+ "wordSpacing": "string",
+ "wordWrap": "string",
+ "writingMode": "string",
+ "zIndex": "string",
+ "zoom": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.style",
+ "!doc": "Returns an object that represents the element's style attribute."
+ },
+ "classList": {
+ "!type": "+DOMTokenList",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.classList",
+ "!doc": "Returns a token list of the class attribute of the element."
+ },
+ "contentEditable": {
+ "!type": "bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Element.contentEditable",
+ "!doc": "Indicates whether or not the element is editable."
+ },
+ "firstElementChild": {
+ "!type": "+Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Element.firstElementChild",
+ "!doc": "Returns the element's first child element or null if there are no child elements."
+ },
+ "lastElementChild": {
+ "!type": "+Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Element.lastElementChild",
+ "!doc": "Returns the element's last child element or null if there are no child elements."
+ },
+ "nextElementSibling": {
+ "!type": "+Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Element.nextElementSibling",
+ "!doc": "Returns the element immediately following the specified one in its parent's children list, or null if the specified element is the last one in the list."
+ },
+ "previousElementSibling": {
+ "!type": "+Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Element.previousElementSibling",
+ "!doc": "Returns the element immediately prior to the specified one in its parent's children list, or null if the specified element is the first one in the list."
+ },
+ "tabIndex": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.tabIndex",
+ "!doc": "Gets/sets the tab order of the current element."
+ },
+ "title": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.title",
+ "!doc": "Establishes the text to be displayed in a 'tool tip' popup when the mouse is over the displayed node."
+ },
+ "width": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.offsetWidth",
+ "!doc": "Returns the layout width of an element."
+ },
+ "height": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.offsetHeight",
+ "!doc": "Height of an element relative to the element's offsetParent."
+ },
+ "getContext": {
+ "!type": "fn(id: string) -> CanvasRenderingContext2D",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/HTMLCanvasElement",
+ "!doc": "DOM canvas elements expose the HTMLCanvasElement interface, which provides properties and methods for manipulating the layout and presentation of canvas elements. The HTMLCanvasElement interface inherits the properties and methods of the element object interface."
+ },
+ "supportsContext": "fn(id: string) -> bool",
+ "oncopy": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.oncopy",
+ "!doc": "The oncopy property returns the onCopy event handler code on the current element."
+ },
+ "oncut": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.oncut",
+ "!doc": "The oncut property returns the onCut event handler code on the current element."
+ },
+ "onpaste": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.onpaste",
+ "!doc": "The onpaste property returns the onPaste event handler code on the current element."
+ },
+ "onbeforeunload": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/HTML/Element/body",
+ "!doc": "The HTML <body> element represents the main content of an HTML document. There is only one <body> element in a document."
+ },
+ "onfocus": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.onfocus",
+ "!doc": "The onfocus property returns the onFocus event handler code on the current element."
+ },
+ "onblur": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.onblur",
+ "!doc": "The onblur property returns the onBlur event handler code, if any, that exists on the current element."
+ },
+ "onchange": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.onchange",
+ "!doc": "The onchange property sets and returns the onChange event handler code for the current element."
+ },
+ "onclick": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.onclick",
+ "!doc": "The onclick property returns the onClick event handler code on the current element."
+ },
+ "ondblclick": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.ondblclick",
+ "!doc": "The ondblclick property returns the onDblClick event handler code on the current element."
+ },
+ "onmousedown": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.onmousedown",
+ "!doc": "The onmousedown property returns the onMouseDown event handler code on the current element."
+ },
+ "onmouseup": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.onmouseup",
+ "!doc": "The onmouseup property returns the onMouseUp event handler code on the current element."
+ },
+ "onmousewheel": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Mozilla_event_reference/wheel",
+ "!doc": "The wheel event is fired when a wheel button of a pointing device (usually a mouse) is rotated. This event deprecates the legacy mousewheel event."
+ },
+ "onmouseover": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.onmouseover",
+ "!doc": "The onmouseover property returns the onMouseOver event handler code on the current element."
+ },
+ "onmouseout": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.onmouseout",
+ "!doc": "The onmouseout property returns the onMouseOut event handler code on the current element."
+ },
+ "onmousemove": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.onmousemove",
+ "!doc": "The onmousemove property returns the mousemove event handler code on the current element."
+ },
+ "oncontextmenu": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.oncontextmenu",
+ "!doc": "An event handler property for right-click events on the window. Unless the default behavior is prevented, the browser context menu will activate. Note that this event will occur with any non-disabled right-click event and does not depend on an element possessing the \"contextmenu\" attribute."
+ },
+ "onkeydown": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.onkeydown",
+ "!doc": "The onkeydown property returns the onKeyDown event handler code on the current element."
+ },
+ "onkeyup": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.onkeyup",
+ "!doc": "The onkeyup property returns the onKeyUp event handler code for the current element."
+ },
+ "onkeypress": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.onkeypress",
+ "!doc": "The onkeypress property sets and returns the onKeyPress event handler code for the current element."
+ },
+ "onresize": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.onresize",
+ "!doc": "onresize returns the element's onresize event handler code. It can also be used to set the code to be executed when the resize event occurs."
+ },
+ "onscroll": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.onscroll",
+ "!doc": "The onscroll property returns the onScroll event handler code on the current element."
+ },
+ "ondragstart": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DragDrop/Drag_Operations",
+ "!doc": "The following describes the steps that occur during a drag and drop operation."
+ },
+ "ondragover": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Mozilla_event_reference/dragover",
+ "!doc": "The dragover event is fired when an element or text selection is being dragged over a valid drop target (every few hundred milliseconds)."
+ },
+ "ondragleave": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Mozilla_event_reference/dragleave",
+ "!doc": "The dragleave event is fired when a dragged element or text selection leaves a valid drop target."
+ },
+ "ondragenter": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Mozilla_event_reference/dragenter",
+ "!doc": "The dragenter event is fired when a dragged element or text selection enters a valid drop target."
+ },
+ "ondragend": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Mozilla_event_reference/dragend",
+ "!doc": "The dragend event is fired when a drag operation is being ended (by releasing a mouse button or hitting the escape key)."
+ },
+ "ondrag": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Mozilla_event_reference/drag",
+ "!doc": "The drag event is fired when an element or text selection is being dragged (every few hundred milliseconds)."
+ },
+ "offsetTop": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.offsetTop",
+ "!doc": "Returns the distance of the current element relative to the top of the offsetParent node."
+ },
+ "offsetLeft": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.offsetLeft",
+ "!doc": "Returns the number of pixels that the upper left corner of the current element is offset to the left within the offsetParent node."
+ },
+ "offsetHeight": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.offsetHeight",
+ "!doc": "Height of an element relative to the element's offsetParent."
+ },
+ "offsetWidth": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.offsetWidth",
+ "!doc": "Returns the layout width of an element."
+ },
+ "scrollTop": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.scrollTop",
+ "!doc": "Gets or sets the number of pixels that the content of an element is scrolled upward."
+ },
+ "scrollLeft": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.scrollLeft",
+ "!doc": "Gets or sets the number of pixels that an element's content is scrolled to the left."
+ },
+ "scrollHeight": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.scrollHeight",
+ "!doc": "Height of the scroll view of an element; it includes the element padding but not its margin."
+ },
+ "scrollWidth": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.scrollWidth",
+ "!doc": "Read-only property that returns either the width in pixels of the content of an element or the width of the element itself, whichever is greater."
+ },
+ "clientTop": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.clientTop",
+ "!doc": "The width of the top border of an element in pixels. It does not include the top margin or padding. clientTop is read-only."
+ },
+ "clientLeft": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.clientLeft",
+ "!doc": "The width of the left border of an element in pixels. It includes the width of the vertical scrollbar if the text direction of the element is right-to-left and if there is an overflow causing a left vertical scrollbar to be rendered. clientLeft does not include the left margin or the left padding. clientLeft is read-only."
+ },
+ "clientHeight": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.clientHeight",
+ "!doc": "Returns the inner height of an element in pixels, including padding but not the horizontal scrollbar height, border, or margin."
+ },
+ "clientWidth": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.clientWidth",
+ "!doc": "The inner width of an element in pixels. It includes padding but not the vertical scrollbar (if present, if rendered), border or margin."
+ },
+ "innerHTML": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.innerHTML",
+ "!doc": "Sets or gets the HTML syntax describing the element's descendants."
+ }
+ },
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Element",
+ "!doc": "Represents an element in an HTML or XML document."
+ },
+ "Text": {
+ "!type": "fn()",
+ "prototype": {
+ "!proto": "Node.prototype",
+ "wholeText": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Text.wholeText",
+ "!doc": "Returns all text of all Text nodes logically adjacent to the node. The text is concatenated in document order. This allows you to specify any text node and obtain all adjacent text as a single string."
+ },
+ "splitText": {
+ "!type": "fn(offset: number) -> +Text",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Text.splitText",
+ "!doc": "Breaks the Text node into two nodes at the specified offset, keeping both nodes in the tree as siblings."
+ }
+ },
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Text",
+ "!doc": "In the DOM, the Text interface represents the textual content of an Element or Attr. If an element has no markup within its content, it has a single child implementing Text that contains the element's text. However, if the element contains markup, it is parsed into information items and Text nodes that form its children."
+ },
+ "Document": {
+ "!type": "fn()",
+ "prototype": {
+ "!proto": "Node.prototype",
+ "activeElement": {
+ "!type": "+Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.activeElement",
+ "!doc": "Returns the currently focused element, that is, the element that will get keystroke events if the user types any. This attribute is read only."
+ },
+ "compatMode": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.compatMode",
+ "!doc": "Indicates whether the document is rendered in Quirks mode or Strict mode."
+ },
+ "designMode": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.designMode",
+ "!doc": "Can be used to make any document editable, for example in a <iframe />:"
+ },
+ "dir": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Document.dir",
+ "!doc": "This property should indicate and allow the setting of the directionality of the text of the document, whether left to right (default) or right to left."
+ },
+ "height": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.height",
+ "!doc": "Returns the height of the <body> element of the current document."
+ },
+ "width": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.width",
+ "!doc": "Returns the width of the <body> element of the current document in pixels."
+ },
+ "characterSet": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.characterSet",
+ "!doc": "Returns the character encoding of the current document."
+ },
+ "readyState": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.readyState",
+ "!doc": "Returns \"loading\" while the document is loading, \"interactive\" once it is finished parsing but still loading sub-resources, and \"complete\" once it has loaded."
+ },
+ "location": {
+ "!type": "location",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.location",
+ "!doc": "Returns a Location object, which contains information about the URL of the document and provides methods for changing that URL."
+ },
+ "lastModified": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.lastModified",
+ "!doc": "Returns a string containing the date and time on which the current document was last modified."
+ },
+ "head": {
+ "!type": "+Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.head",
+ "!doc": "Returns the <head> element of the current document. If there are more than one <head> elements, the first one is returned."
+ },
+ "body": {
+ "!type": "+Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.body",
+ "!doc": "Returns the <body> or <frameset> node of the current document."
+ },
+ "cookie": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.cookie",
+ "!doc": "Get and set the cookies associated with the current document."
+ },
+ "URL": "string",
+ "domain": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.domain",
+ "!doc": "Gets/sets the domain portion of the origin of the current document, as used by the same origin policy."
+ },
+ "referrer": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.referrer",
+ "!doc": "Returns the URI of the page that linked to this page."
+ },
+ "title": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.title",
+ "!doc": "Gets or sets the title of the document."
+ },
+ "defaultView": {
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.defaultView",
+ "!doc": "In browsers returns the window object associated with the document or null if none available."
+ },
+ "documentURI": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.documentURI",
+ "!doc": "Returns the document location as string. It is read-only per DOM4 specification."
+ },
+ "xmlStandalone": "bool",
+ "xmlVersion": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.xmlVersion",
+ "!doc": "Returns the version number as specified in the XML declaration (e.g., <?xml version=\"1.0\"?>) or \"1.0\" if the declaration is absent."
+ },
+ "xmlEncoding": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Document.xmlEncoding",
+ "!doc": "Returns the encoding as determined by the XML declaration. Should be null if unspecified or unknown."
+ },
+ "inputEncoding": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.inputEncoding",
+ "!doc": "Returns a string representing the encoding under which the document was parsed (e.g. ISO-8859-1)."
+ },
+ "documentElement": {
+ "!type": "+Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.documentElement",
+ "!doc": "Read-only"
+ },
+ "implementation": {
+ "hasFeature": "fn(feature: string, version: number) -> bool",
+ "createDocumentType": {
+ "!type": "fn(qualifiedName: string, publicId: string, systemId: string) -> +Node",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/DOMImplementation.createDocumentType",
+ "!doc": "Returns a DocumentType object which can either be used with DOMImplementation.createDocument upon document creation or they can be put into the document via Node.insertBefore() or Node.replaceChild(): http://www.w3.org/TR/DOM-Level-3-Cor...l#ID-B63ED1A31 (less ideal due to features not likely being as accessible: http://www.w3.org/TR/DOM-Level-3-Cor...createDocument ). In any case, entity declarations and notations will not be available: http://www.w3.org/TR/DOM-Level-3-Cor...-createDocType "
+ },
+ "createHTMLDocument": {
+ "!type": "fn(title: string) -> +Document",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/DOMImplementation.createHTMLDocument",
+ "!doc": "This method (available from document.implementation) creates a new HTML document."
+ },
+ "createDocument": {
+ "!type": "fn(namespaceURI: string, qualifiedName: string, type: +Node) -> +Document",
+ "!url": "https://developer.mozilla.org/en-US/docs/DOM/DOMImplementation.createHTMLDocument",
+ "!doc": "This method creates a new HTML document."
+ },
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.implementation",
+ "!doc": "Returns a DOMImplementation object associated with the current document."
+ },
+ "doctype": {
+ "!type": "+Node",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.doctype",
+ "!doc": "Returns the Document Type Declaration (DTD) associated with current document. The returned object implements the DocumentType interface. Use DOMImplementation.createDocumentType() to create a DocumentType."
+ },
+ "open": {
+ "!type": "fn()",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.open",
+ "!doc": "The document.open() method opens a document for writing."
+ },
+ "close": {
+ "!type": "fn()",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.close",
+ "!doc": "The document.close() method finishes writing to a document, opened with document.open()."
+ },
+ "write": {
+ "!type": "fn(html: string)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.write",
+ "!doc": "Writes a string of text to a document stream opened by document.open()."
+ },
+ "writeln": {
+ "!type": "fn(html: string)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.writeln",
+ "!doc": "Writes a string of text followed by a newline character to a document."
+ },
+ "clear": {
+ "!type": "fn()",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.clear",
+ "!doc": "In recent versions of Mozilla-based applications as well as in Internet Explorer and Netscape 4 this method does nothing."
+ },
+ "hasFocus": {
+ "!type": "fn() -> bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.hasFocus",
+ "!doc": "Returns a Boolean value indicating whether the document or any element inside the document has focus. This method can be used to determine whether the active element in a document has focus."
+ },
+ "createElement": {
+ "!type": "fn(tagName: string) -> +Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.createElement",
+ "!doc": "Creates the specified element."
+ },
+ "createElementNS": {
+ "!type": "fn(ns: string, tagName: string) -> +Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.createElementNS",
+ "!doc": "Creates an element with the specified namespace URI and qualified name."
+ },
+ "createDocumentFragment": {
+ "!type": "fn() -> +DocumentFragment",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.createDocumentFragment",
+ "!doc": "Creates a new empty DocumentFragment."
+ },
+ "createTextNode": {
+ "!type": "fn(content: string) -> +Text",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.createTextNode",
+ "!doc": "Creates a new Text node."
+ },
+ "createComment": {
+ "!type": "fn(content: string) -> +Node",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.createComment",
+ "!doc": "Creates a new comment node, and returns it."
+ },
+ "createCDATASection": {
+ "!type": "fn(content: string) -> +Node",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.createCDATASection",
+ "!doc": "Creates a new CDATA section node, and returns it. "
+ },
+ "createProcessingInstruction": {
+ "!type": "fn(content: string) -> +Node",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.createProcessingInstruction",
+ "!doc": "Creates a new processing instruction node, and returns it."
+ },
+ "createAttribute": {
+ "!type": "fn(name: string) -> +Attr",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.createAttribute",
+ "!doc": "Creates a new attribute node, and returns it."
+ },
+ "createAttributeNS": {
+ "!type": "fn(ns: string, name: string) -> +Attr",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Attr",
+ "!doc": "This type represents a DOM element's attribute as an object. In most DOM methods, you will probably directly retrieve the attribute as a string (e.g., Element.getAttribute(), but certain functions (e.g., Element.getAttributeNode()) or means of iterating give Attr types."
+ },
+ "importNode": {
+ "!type": "fn(node: +Node, deep: bool) -> +Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.importNode",
+ "!doc": "Creates a copy of a node from an external document that can be inserted into the current document."
+ },
+ "getElementById": {
+ "!type": "fn(id: string) -> +Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.getElementById",
+ "!doc": "Returns a reference to the element by its ID."
+ },
+ "getElementsByTagName": {
+ "!type": "fn(tagName: string) -> +NodeList",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.getElementsByTagName",
+ "!doc": "Returns a NodeList of elements with the given tag name. The complete document is searched, including the root node. The returned NodeList is live, meaning that it updates itself automatically to stay in sync with the DOM tree without having to call document.getElementsByTagName again."
+ },
+ "getElementsByTagNameNS": {
+ "!type": "fn(ns: string, tagName: string) -> +NodeList",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.getElementsByTagNameNS",
+ "!doc": "Returns a list of elements with the given tag name belonging to the given namespace. The complete document is searched, including the root node."
+ },
+ "createEvent": {
+ "!type": "fn(type: string) -> +Event",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.createEvent",
+ "!doc": "Creates an event of the type specified. The returned object should be first initialized and can then be passed to element.dispatchEvent."
+ },
+ "createRange": {
+ "!type": "fn() -> +Range",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.createRange",
+ "!doc": "Returns a new Range object."
+ },
+ "evaluate": {
+ "!type": "fn(expr: ?) -> +XPathResult",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.evaluate",
+ "!doc": "Returns an XPathResult based on an XPath expression and other given parameters."
+ },
+ "execCommand": {
+ "!type": "fn(cmd: string)",
+ "!url": "https://developer.mozilla.org/en-US/docs/Rich-Text_Editing_in_Mozilla#Executing_Commands",
+ "!doc": "Run command to manipulate the contents of an editable region."
+ },
+ "queryCommandEnabled": {
+ "!type": "fn(cmd: string) -> bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document",
+ "!doc": "Returns true if the Midas command can be executed on the current range."
+ },
+ "queryCommandIndeterm": {
+ "!type": "fn(cmd: string) -> bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document",
+ "!doc": "Returns true if the Midas command is in a indeterminate state on the current range."
+ },
+ "queryCommandState": {
+ "!type": "fn(cmd: string) -> bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document",
+ "!doc": "Returns true if the Midas command has been executed on the current range."
+ },
+ "queryCommandSupported": {
+ "!type": "fn(cmd: string) -> bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.queryCommandSupported",
+ "!doc": "Reports whether or not the specified editor query command is supported by the browser."
+ },
+ "queryCommandValue": {
+ "!type": "fn(cmd: string) -> string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document",
+ "!doc": "Returns the current value of the current range for Midas command."
+ },
+ "getElementsByName": {
+ "!type": "fn(name: string) -> +HTMLCollection",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.getElementsByName",
+ "!doc": "Returns a list of elements with a given name in the HTML document."
+ },
+ "elementFromPoint": {
+ "!type": "fn(x: number, y: number) -> +Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.elementFromPoint",
+ "!doc": "Returns the element from the document whose elementFromPoint method is being called which is the topmost element which lies under the given point. To get an element, specify the point via coordinates, in CSS pixels, relative to the upper-left-most point in the window or frame containing the document."
+ },
+ "getSelection": {
+ "!type": "fn() -> +Selection",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.getSelection",
+ "!doc": "The DOM getSelection() method is available on the Window and Document interfaces."
+ },
+ "adoptNode": {
+ "!type": "fn(node: +Node) -> +Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.adoptNode",
+ "!doc": "Adopts a node from an external document. The node and its subtree is removed from the document it's in (if any), and its ownerDocument is changed to the current document. The node can then be inserted into the current document."
+ },
+ "createTreeWalker": {
+ "!type": "fn(root: +Node, mask: number) -> ?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.createTreeWalker",
+ "!doc": "Returns a new TreeWalker object."
+ },
+ "createExpression": {
+ "!type": "fn(text: string) -> ?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.createExpression",
+ "!doc": "This method compiles an XPathExpression which can then be used for (repeated) evaluations."
+ },
+ "createNSResolver": {
+ "!type": "fn(node: +Node)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.createNSResolver",
+ "!doc": "Creates an XPathNSResolver which resolves namespaces with respect to the definitions in scope for a specified node."
+ },
+ "scripts": {
+ "!type": "+HTMLCollection",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Document.scripts",
+ "!doc": "Returns a list of the <script> elements in the document. The returned object is an HTMLCollection."
+ },
+ "plugins": {
+ "!type": "+HTMLCollection",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.plugins",
+ "!doc": "Returns an HTMLCollection object containing one or more HTMLEmbedElements or null which represent the <embed> elements in the current document."
+ },
+ "embeds": {
+ "!type": "+HTMLCollection",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.embeds",
+ "!doc": "Returns a list of the embedded OBJECTS within the current document."
+ },
+ "anchors": {
+ "!type": "+HTMLCollection",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.anchors",
+ "!doc": "Returns a list of all of the anchors in the document."
+ },
+ "links": {
+ "!type": "+HTMLCollection",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.links",
+ "!doc": "The links property returns a collection of all AREA elements and anchor elements in a document with a value for the href attribute. "
+ },
+ "forms": {
+ "!type": "+HTMLCollection",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.forms",
+ "!doc": "Returns a collection (an HTMLCollection) of the form elements within the current document."
+ },
+ "styleSheets": {
+ "!type": "+HTMLCollection",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.styleSheets",
+ "!doc": "Returns a list of stylesheet objects for stylesheets explicitly linked into or embedded in a document."
+ },
+ "querySelector": "Element.prototype.querySelector",
+ "querySelectorAll": "Element.prototype.querySelectorAll"
+ },
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document",
+ "!doc": "Each web page loaded in the browser has its own document object. This object serves as an entry point to the web page's content (the DOM tree, including elements such as <body> and <table>) and provides functionality global to the document (such as obtaining the page's URL and creating new elements in the document)."
+ },
+ "document": {
+ "!type": "+Document",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document",
+ "!doc": "Each web page loaded in the browser has its own document object. This object serves as an entry point to the web page's content (the DOM tree, including elements such as <body> and <table>) and provides functionality global to the document (such as obtaining the page's URL and creating new elements in the document)."
+ },
+ "XMLDocument": {
+ "!type": "fn()",
+ "prototype": "Document.prototype",
+ "!url": "https://developer.mozilla.org/en/docs/Parsing_and_serializing_XML",
+ "!doc": "The Web platform provides the following objects for parsing and serializing XML:"
+ },
+ "Attr": {
+ "!type": "fn()",
+ "prototype": {
+ "isId": {
+ "!type": "bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Attr",
+ "!doc": "This type represents a DOM element's attribute as an object. In most DOM methods, you will probably directly retrieve the attribute as a string (e.g., Element.getAttribute(), but certain functions (e.g., Element.getAttributeNode()) or means of iterating give Attr types."
+ },
+ "name": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Attr",
+ "!doc": "This type represents a DOM element's attribute as an object. In most DOM methods, you will probably directly retrieve the attribute as a string (e.g., Element.getAttribute(), but certain functions (e.g., Element.getAttributeNode()) or means of iterating give Attr types."
+ },
+ "value": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Attr",
+ "!doc": "This type represents a DOM element's attribute as an object. In most DOM methods, you will probably directly retrieve the attribute as a string (e.g., Element.getAttribute(), but certain functions (e.g., Element.getAttributeNode()) or means of iterating give Attr types."
+ }
+ },
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Attr",
+ "!doc": "This type represents a DOM element's attribute as an object. In most DOM methods, you will probably directly retrieve the attribute as a string (e.g., Element.getAttribute(), but certain functions (e.g., Element.getAttributeNode()) or means of iterating give Attr types."
+ },
+ "NodeList": {
+ "!type": "fn()",
+ "prototype": {
+ "length": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.length",
+ "!doc": "Returns the number of items in a NodeList."
+ },
+ "item": {
+ "!type": "fn(i: number) -> +Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/NodeList.item",
+ "!doc": "Returns a node from a NodeList by index."
+ },
+ "<i>": "+Element"
+ },
+ "!url": "https://developer.mozilla.org/en/docs/DOM/NodeList",
+ "!doc": "NodeList objects are collections of nodes returned by getElementsByTagName, getElementsByTagNameNS, Node.childNodes, querySelectorAll, getElementsByClassName, etc."
+ },
+ "HTMLCollection": {
+ "!type": "fn()",
+ "prototype": {
+ "length": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/HTMLCollection",
+ "!doc": "The number of items in the collection."
+ },
+ "item": {
+ "!type": "fn(i: number) -> +Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/HTMLCollection",
+ "!doc": "Returns the specific node at the given zero-based index into the list. Returns null if the index is out of range."
+ },
+ "namedItem": {
+ "!type": "fn(name: string) -> +Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/HTMLCollection",
+ "!doc": "Returns the specific node whose ID or, as a fallback, name matches the string specified by name. Matching by name is only done as a last resort, only in HTML, and only if the referenced element supports the name attribute. Returns null if no node exists by the given name."
+ },
+ "<i>": "+Element"
+ },
+ "!url": "https://developer.mozilla.org/en/docs/DOM/HTMLCollection",
+ "!doc": "HTMLCollection is an interface representing a generic collection of elements (in document order) and offers methods and properties for traversing the list."
+ },
+ "NamedNodeMap": {
+ "!type": "fn()",
+ "prototype": {
+ "length": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/NamedNodeMap",
+ "!doc": "The number of items in the map."
+ },
+ "getNamedItem": {
+ "!type": "fn(name: string) -> +Node",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/NamedNodeMap",
+ "!doc": "Gets a node by name."
+ },
+ "setNamedItem": {
+ "!type": "fn(node: +Node) -> +Node",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/NamedNodeMap",
+ "!doc": "Adds (or replaces) a node by its nodeName."
+ },
+ "removeNamedItem": {
+ "!type": "fn(name: string) -> +Node",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/NamedNodeMap",
+ "!doc": "Removes a node (or if an attribute, may reveal a default if present)."
+ },
+ "item": {
+ "!type": "fn(i: number) -> +Node",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/NamedNodeMap",
+ "!doc": "Returns the item at the given index (or null if the index is higher or equal to the number of nodes)."
+ },
+ "getNamedItemNS": {
+ "!type": "fn(ns: string, name: string) -> +Node",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/NamedNodeMap",
+ "!doc": "Gets a node by namespace and localName."
+ },
+ "setNamedItemNS": {
+ "!type": "fn(node: +Node) -> +Node",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/NamedNodeMap",
+ "!doc": "Adds (or replaces) a node by its localName and namespaceURI."
+ },
+ "removeNamedItemNS": {
+ "!type": "fn(ns: string, name: string) -> +Node",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/NamedNodeMap",
+ "!doc": "Removes a node (or if an attribute, may reveal a default if present)."
+ },
+ "<i>": "+Node"
+ },
+ "!url": "https://developer.mozilla.org/en/docs/DOM/NamedNodeMap",
+ "!doc": "A collection of nodes returned by Element.attributes (also potentially for DocumentType.entities, DocumentType.notations). NamedNodeMaps are not in any particular order (unlike NodeList), although they may be accessed by an index as in an array (they may also be accessed with the item() method). A NamedNodeMap object are live and will thus be auto-updated if changes are made to their contents internally or elsewhere."
+ },
+ "DocumentFragment": {
+ "!type": "fn()",
+ "prototype": {
+ "!proto": "Node.prototype"
+ },
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.createDocumentFragment",
+ "!doc": "Creates a new empty DocumentFragment."
+ },
+ "DOMTokenList": {
+ "!type": "fn()",
+ "prototype": {
+ "length": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/DOMTokenList",
+ "!doc": "The amount of items in the list."
+ },
+ "item": {
+ "!type": "fn(i: number) -> string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/DOMTokenList",
+ "!doc": "Returns an item in the list by its index."
+ },
+ "contains": {
+ "!type": "fn(token: string) -> bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/DOMTokenList",
+ "!doc": "Return true if the underlying string contains token, otherwise false."
+ },
+ "add": {
+ "!type": "fn(token: string)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/DOMTokenList",
+ "!doc": "Adds token to the underlying string."
+ },
+ "remove": {
+ "!type": "fn(token: string)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/DOMTokenList",
+ "!doc": "Remove token from the underlying string."
+ },
+ "toggle": {
+ "!type": "fn(token: string) -> bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/DOMTokenList",
+ "!doc": "Removes token from string and returns false. If token doesn't exist it's added and the function returns true."
+ },
+ "<i>": "string"
+ },
+ "!url": "https://developer.mozilla.org/en/docs/DOM/DOMTokenList",
+ "!doc": "This type represents a set of space-separated tokens. Commonly returned by HTMLElement.classList, HTMLLinkElement.relList, HTMLAnchorElement.relList or HTMLAreaElement.relList. It is indexed beginning with 0 as with JavaScript arrays. DOMTokenList is always case-sensitive."
+ },
+ "XPathResult": {
+ "!type": "fn()",
+ "prototype": {
+ "boolValue": "bool",
+ "invalidIteratorState": {
+ "!type": "bool",
+ "!url": "https://developer.mozilla.org/en/docs/Introduction_to_using_XPath_in_JavaScript",
+ "!doc": "This document describes the interface for using XPath in JavaScript internally, in extensions, and from websites. Mozilla implements a fair amount of the DOM 3 XPath. Which means that XPath expressions can be run against both HTML and XML documents."
+ },
+ "numberValue": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/XPathResult",
+ "!doc": "Refer to nsIDOMXPathResult for more detail."
+ },
+ "resultType": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.evaluate",
+ "!doc": "Returns an XPathResult based on an XPath expression and other given parameters."
+ },
+ "singleNodeValue": {
+ "!type": "+Element",
+ "!url": "https://developer.mozilla.org/en/docs/Introduction_to_using_XPath_in_JavaScript",
+ "!doc": "This document describes the interface for using XPath in JavaScript internally, in extensions, and from websites. Mozilla implements a fair amount of the DOM 3 XPath. Which means that XPath expressions can be run against both HTML and XML documents."
+ },
+ "snapshotLength": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/XPathResult",
+ "!doc": "Refer to nsIDOMXPathResult for more detail."
+ },
+ "stringValue": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/Introduction_to_using_XPath_in_JavaScript",
+ "!doc": "This document describes the interface for using XPath in JavaScript internally, in extensions, and from websites. Mozilla implements a fair amount of the DOM 3 XPath. Which means that XPath expressions can be run against both HTML and XML documents."
+ },
+ "iterateNext": {
+ "!type": "fn()",
+ "!url": "https://developer.mozilla.org/en/docs/Introduction_to_using_XPath_in_JavaScript",
+ "!doc": "This document describes the interface for using XPath in JavaScript internally, in extensions, and from websites. Mozilla implements a fair amount of the DOM 3 XPath. Which means that XPath expressions can be run against both HTML and XML documents."
+ },
+ "snapshotItem": {
+ "!type": "fn()",
+ "!url": "https://developer.mozilla.org/en-US/docs/XPathResult#snapshotItem()"
+ },
+ "ANY_TYPE": "number",
+ "NUMBER_TYPE": "number",
+ "STRING_TYPE": "number",
+ "BOOL_TYPE": "number",
+ "UNORDERED_NODE_ITERATOR_TYPE": "number",
+ "ORDERED_NODE_ITERATOR_TYPE": "number",
+ "UNORDERED_NODE_SNAPSHOT_TYPE": "number",
+ "ORDERED_NODE_SNAPSHOT_TYPE": "number",
+ "ANY_UNORDERED_NODE_TYPE": "number",
+ "FIRST_ORDERED_NODE_TYPE": "number"
+ },
+ "!url": "https://developer.mozilla.org/en/docs/XPathResult",
+ "!doc": "Refer to nsIDOMXPathResult for more detail."
+ },
+ "ClientRect": {
+ "!type": "fn()",
+ "prototype": {
+ "top": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.getClientRects",
+ "!doc": "Top of the box, in pixels, relative to the viewport."
+ },
+ "left": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.getClientRects",
+ "!doc": "Left of the box, in pixels, relative to the viewport."
+ },
+ "bottom": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.getClientRects",
+ "!doc": "Bottom of the box, in pixels, relative to the viewport."
+ },
+ "right": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.getClientRects",
+ "!doc": "Right of the box, in pixels, relative to the viewport."
+ }
+ },
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.getClientRects",
+ "!doc": "Returns a collection of rectangles that indicate the bounding rectangles for each box in a client."
+ },
+ "Event": {
+ "!type": "fn()",
+ "prototype": {
+ "stopPropagation": {
+ "!type": "fn()",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/event.stopPropagation",
+ "!doc": "Prevents further propagation of the current event."
+ },
+ "preventDefault": {
+ "!type": "fn()",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/event.preventDefault",
+ "!doc": "Cancels the event if it is cancelable, without stopping further propagation of the event."
+ },
+ "initEvent": {
+ "!type": "fn(type: string, bubbles: bool, cancelable: bool)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/event.initEvent",
+ "!doc": "The initEvent method is used to initialize the value of an event created using document.createEvent."
+ },
+ "stopImmediatePropagation": {
+ "!type": "fn()",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/event.stopImmediatePropagation",
+ "!doc": "Prevents other listeners of the same event to be called."
+ },
+ "NONE": "number",
+ "CAPTURING_PHASE": "number",
+ "AT_TARGET": "number",
+ "BUBBLING_PHASE": "number",
+ "MOUSEDOWN": "number",
+ "MOUSEUP": "number",
+ "MOUSEOVER": "number",
+ "MOUSEOUT": "number",
+ "MOUSEMOVE": "number",
+ "MOUSEDRAG": "number",
+ "CLICK": "number",
+ "DBLCLICK": "number",
+ "KEYDOWN": "number",
+ "KEYUP": "number",
+ "KEYPRESS": "number",
+ "DRAGDROP": "number",
+ "FOCUS": "number",
+ "BLUR": "number",
+ "SELECT": "number",
+ "CHANGE": "number",
+ "target": {
+ "!type": "+Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/EventTarget",
+ "!doc": "An EventTarget is a DOM interface implemented by objects that can receive DOM events and have listeners for them. The most common EventTargets are DOM elements, although other objects can be EventTargets too, for example document, window, XMLHttpRequest, and others."
+ },
+ "relatedTarget": {
+ "!type": "+Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/event.relatedTarget",
+ "!doc": "Identifies a secondary target for the event."
+ },
+ "pageX": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/event.pageX",
+ "!doc": "Returns the horizontal coordinate of the event relative to whole document."
+ },
+ "pageY": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/event.pageY",
+ "!doc": "Returns the vertical coordinate of the event relative to the whole document."
+ },
+ "clientX": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/event.clientX",
+ "!doc": "Returns the horizontal coordinate within the application's client area at which the event occurred (as opposed to the coordinates within the page). For example, clicking in the top-left corner of the client area will always result in a mouse event with a clientX value of 0, regardless of whether the page is scrolled horizontally."
+ },
+ "clientY": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/event.clientY",
+ "!doc": "Returns the vertical coordinate within the application's client area at which the event occurred (as opposed to the coordinates within the page). For example, clicking in the top-left corner of the client area will always result in a mouse event with a clientY value of 0, regardless of whether the page is scrolled vertically."
+ },
+ "keyCode": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/event.keyCode",
+ "!doc": "Returns the Unicode value of a non-character key in a keypress event or any key in any other type of keyboard event."
+ },
+ "charCode": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/event.charCode",
+ "!doc": "Returns the Unicode value of a character key pressed during a keypress event."
+ },
+ "which": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/event.which",
+ "!doc": "Returns the numeric keyCode of the key pressed, or the character code (charCode) for an alphanumeric key pressed."
+ },
+ "button": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/event.button",
+ "!doc": "Indicates which mouse button caused the event."
+ },
+ "shiftKey": {
+ "!type": "bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/event.shiftKey",
+ "!doc": "Indicates whether the SHIFT key was pressed when the event fired."
+ },
+ "ctrlKey": {
+ "!type": "bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/event.ctrlKey",
+ "!doc": "Indicates whether the CTRL key was pressed when the event fired."
+ },
+ "altKey": {
+ "!type": "bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/event.altKey",
+ "!doc": "Indicates whether the ALT key was pressed when the event fired."
+ },
+ "metaKey": {
+ "!type": "bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/event.metaKey",
+ "!doc": "Indicates whether the META key was pressed when the event fired."
+ },
+ "returnValue": {
+ "!type": "bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.onbeforeunload",
+ "!doc": "An event that fires when a window is about to unload its resources. The document is still visible and the event is still cancelable."
+ },
+ "cancelBubble": {
+ "!type": "bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/event.cancelBubble",
+ "!doc": "bool is the boolean value of true or false."
+ },
+ "dataTransfer": {
+ "dropEffect": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DragDrop/DataTransfer",
+ "!doc": "The actual effect that will be used, and should always be one of the possible values of effectAllowed."
+ },
+ "effectAllowed": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DragDrop/Drag_Operations",
+ "!doc": "Specifies the effects that are allowed for this drag."
+ },
+ "files": {
+ "!type": "+FileList",
+ "!url": "https://developer.mozilla.org/en/docs/DragDrop/DataTransfer",
+ "!doc": "Contains a list of all the local files available on the data transfer."
+ },
+ "types": {
+ "!type": "[string]",
+ "!url": "https://developer.mozilla.org/en-US/docs/DragDrop/DataTransfer",
+ "!doc": "Holds a list of the format types of the data that is stored for the first item, in the same order the data was added. An empty list will be returned if no data was added."
+ },
+ "addElement": {
+ "!type": "fn(element: +Element)",
+ "!url": "https://developer.mozilla.org/en/docs/DragDrop/DataTransfer",
+ "!doc": "Set the drag source."
+ },
+ "clearData": {
+ "!type": "fn(type?: string)",
+ "!url": "https://developer.mozilla.org/en/docs/DragDrop/Drag_Operations",
+ "!doc": "Remove the data associated with a given type."
+ },
+ "getData": {
+ "!type": "fn(type: string) -> string",
+ "!url": "https://developer.mozilla.org/en/docs/DragDrop/Drag_Operations",
+ "!doc": "Retrieves the data for a given type, or an empty string if data for that type does not exist or the data transfer contains no data."
+ },
+ "setData": {
+ "!type": "fn(type: string, data: string)",
+ "!url": "https://developer.mozilla.org/en/docs/DragDrop/Drag_Operations",
+ "!doc": "Set the data for a given type."
+ },
+ "setDragImage": {
+ "!type": "fn(image: +Element)",
+ "!url": "https://developer.mozilla.org/en/docs/DragDrop/Drag_Operations",
+ "!doc": "Set the image to be used for dragging if a custom one is desired."
+ },
+ "!url": "https://developer.mozilla.org/en/docs/DragDrop/DataTransfer",
+ "!doc": "This object is available from the dataTransfer property of all drag events. It cannot be created separately."
+ }
+ },
+ "!url": "https://developer.mozilla.org/en-US/docs/DOM/event",
+ "!doc": "The DOM Event interface is accessible from within the handler function, via the event object passed as the first argument."
+ },
+ "TouchEvent": {
+ "!type": "fn()",
+ "prototype": "Event.prototype",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Touch_events",
+ "!doc": "In order to provide quality support for touch-based user interfaces, touch events offer the ability to interpret finger activity on touch screens or trackpads."
+ },
+ "WheelEvent": {
+ "!type": "fn()",
+ "prototype": "Event.prototype",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/WheelEvent",
+ "!doc": "The DOM WheelEvent represents events that occur due to the user moving a mouse wheel or similar input device."
+ },
+ "MouseEvent": {
+ "!type": "fn()",
+ "prototype": "Event.prototype",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/MouseEvent",
+ "!doc": "The DOM MouseEvent represents events that occur due to the user interacting with a pointing device (such as a mouse). It's represented by the nsINSDOMMouseEvent interface, which extends the nsIDOMMouseEvent interface."
+ },
+ "KeyboardEvent": {
+ "!type": "fn()",
+ "prototype": "Event.prototype",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/KeyboardEvent",
+ "!doc": "KeyboardEvent objects describe a user interaction with the keyboard. Each event describes a key; the event type (keydown, keypress, or keyup) identifies what kind of activity was performed."
+ },
+ "HashChangeEvent": {
+ "!type": "fn()",
+ "prototype": "Event.prototype",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.onhashchange",
+ "!doc": "The hashchange event fires when a window's hash changes."
+ },
+ "ErrorEvent": {
+ "!type": "fn()",
+ "prototype": "Event.prototype",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/DOM_event_reference/error",
+ "!doc": "The error event is fired whenever a resource fails to load."
+ },
+ "CustomEvent": {
+ "!type": "fn()",
+ "prototype": "Event.prototype",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Event/CustomEvent",
+ "!doc": "The DOM CustomEvent are events initialized by an application for any purpose."
+ },
+ "BeforeLoadEvent": {
+ "!type": "fn()",
+ "prototype": "Event.prototype",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window",
+ "!doc": "This section provides a brief reference for all of the methods, properties, and events available through the DOM window object. The window object implements the Window interface, which in turn inherits from the AbstractView interface. Some additional global functions, namespaces objects, and constructors, not typically associated with the window, but available on it, are listed in the JavaScript Reference."
+ },
+ "WebSocket": {
+ "!type": "fn(url: string)",
+ "prototype": {
+ "close": {
+ "!type": "fn()",
+ "!url": "https://developer.mozilla.org/en/docs/WebSockets/WebSockets_reference/CloseEvent",
+ "!doc": "A CloseEvent is sent to clients using WebSockets when the connection is closed. This is delivered to the listener indicated by the WebSocket object's onclose attribute."
+ },
+ "send": {
+ "!type": "fn(data: string)",
+ "!url": "https://developer.mozilla.org/en/docs/WebSockets/WebSockets_reference/WebSocket",
+ "!doc": "The WebSocket object provides the API for creating and managing a WebSocket connection to a server, as well as for sending and receiving data on the connection."
+ },
+ "binaryType": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/WebSockets/WebSockets_reference/WebSocket",
+ "!doc": "The WebSocket object provides the API for creating and managing a WebSocket connection to a server, as well as for sending and receiving data on the connection."
+ },
+ "bufferedAmount": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/WebSockets/Writing_WebSocket_client_applications",
+ "!doc": "WebSockets is a technology that makes it possible to open an interactive communication session between the user's browser and a server. Using a WebSocket connection, Web applications can perform real-time communication instead of having to poll for changes back and forth."
+ },
+ "extensions": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/WebSockets/WebSockets_reference/WebSocket",
+ "!doc": "The WebSocket object provides the API for creating and managing a WebSocket connection to a server, as well as for sending and receiving data on the connection."
+ },
+ "onclose": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/WebSockets/WebSockets_reference/CloseEvent",
+ "!doc": "A CloseEvent is sent to clients using WebSockets when the connection is closed. This is delivered to the listener indicated by the WebSocket object's onclose attribute."
+ },
+ "onerror": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/WebSockets/Writing_WebSocket_client_applications",
+ "!doc": "WebSockets is a technology that makes it possible to open an interactive communication session between the user's browser and a server. Using a WebSocket connection, Web applications can perform real-time communication instead of having to poll for changes back and forth."
+ },
+ "onmessage": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/WebSockets/WebSockets_reference/WebSocket",
+ "!doc": "The WebSocket object provides the API for creating and managing a WebSocket connection to a server, as well as for sending and receiving data on the connection."
+ },
+ "onopen": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/WebSockets/WebSockets_reference/WebSocket",
+ "!doc": "The WebSocket object provides the API for creating and managing a WebSocket connection to a server, as well as for sending and receiving data on the connection."
+ },
+ "protocol": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/WebSockets",
+ "!doc": "WebSockets is an advanced technology that makes it possible to open an interactive communication session between the user's browser and a server. With this API, you can send messages to a server and receive event-driven responses without having to poll the server for a reply."
+ },
+ "url": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/WebSockets/Writing_WebSocket_client_applications",
+ "!doc": "WebSockets is a technology that makes it possible to open an interactive communication session between the user's browser and a server. Using a WebSocket connection, Web applications can perform real-time communication instead of having to poll for changes back and forth."
+ },
+ "CONNECTING": "number",
+ "OPEN": "number",
+ "CLOSING": "number",
+ "CLOSED": "number"
+ },
+ "!url": "https://developer.mozilla.org/en/docs/WebSockets",
+ "!doc": "WebSockets is an advanced technology that makes it possible to open an interactive communication session between the user's browser and a server. With this API, you can send messages to a server and receive event-driven responses without having to poll the server for a reply."
+ },
+ "Worker": {
+ "!type": "fn(scriptURL: string)",
+ "prototype": {
+ "postMessage": {
+ "!type": "fn(message: ?)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Worker",
+ "!doc": "Sends a message to the worker's inner scope. This accepts a single parameter, which is the data to send to the worker. The data may be any value or JavaScript object handled by the structured clone algorithm, which includes cyclical references."
+ },
+ "terminate": {
+ "!type": "fn()",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Worker",
+ "!doc": "Immediately terminates the worker. This does not offer the worker an opportunity to finish its operations; it is simply stopped at once."
+ },
+ "onmessage": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Worker",
+ "!doc": "An event listener that is called whenever a MessageEvent with type message bubbles through the worker. The message is stored in the event's data member."
+ },
+ "onerror": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Worker",
+ "!doc": "An event listener that is called whenever an ErrorEvent with type error bubbles through the worker."
+ }
+ },
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Worker",
+ "!doc": "Workers are background tasks that can be easily created and can send messages back to their creators. Creating a worker is as simple as calling the Worker() constructor, specifying a script to be run in the worker thread."
+ },
+ "localStorage": {
+ "setItem": {
+ "!type": "fn(name: string, value: string)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Storage",
+ "!doc": "Store an item in storage."
+ },
+ "getItem": {
+ "!type": "fn(name: string) -> string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Storage",
+ "!doc": "Retrieve an item from storage."
+ },
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Storage",
+ "!doc": "The DOM Storage mechanism is a means through which string key/value pairs can be securely stored and later retrieved for use."
+ },
+ "sessionStorage": {
+ "setItem": {
+ "!type": "fn(name: string, value: string)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Storage",
+ "!doc": "Store an item in storage."
+ },
+ "getItem": {
+ "!type": "fn(name: string) -> string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Storage",
+ "!doc": "Retrieve an item from storage."
+ },
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Storage",
+ "!doc": "This is a global object (sessionStorage) that maintains a storage area that's available for the duration of the page session. A page session lasts for as long as the browser is open and survives over page reloads and restores. Opening a page in a new tab or window will cause a new session to be initiated."
+ },
+ "FileList": {
+ "!type": "fn()",
+ "prototype": {
+ "length": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/FileList",
+ "!doc": "A read-only value indicating the number of files in the list."
+ },
+ "item": {
+ "!type": "fn(i: number) -> +File",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/FileList",
+ "!doc": "Returns a File object representing the file at the specified index in the file list."
+ },
+ "<i>": "+File"
+ },
+ "!url": "https://developer.mozilla.org/en/docs/DOM/FileList",
+ "!doc": "An object of this type is returned by the files property of the HTML input element; this lets you access the list of files selected with the <input type=\"file\"> element. It's also used for a list of files dropped into web content when using the drag and drop API."
+ },
+ "File": {
+ "!type": "fn()",
+ "prototype": {
+ "!proto": "Blob.prototype",
+ "fileName": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/File.fileName",
+ "!doc": "Returns the name of the file. For security reasons the path is excluded from this property."
+ },
+ "fileSize": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/File.fileSize",
+ "!doc": "Returns the size of a file in bytes."
+ },
+ "lastModifiedDate": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/File.lastModifiedDate",
+ "!doc": "Returns the last modified date of the file. Files without a known last modified date use the current date instead."
+ },
+ "name": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/File.name",
+ "!doc": "Returns the name of the file. For security reasons, the path is excluded from this property."
+ }
+ },
+ "!url": "https://developer.mozilla.org/en/docs/DOM/File",
+ "!doc": "The File object provides information about -- and access to the contents of -- files. These are generally retrieved from a FileList object returned as a result of a user selecting files using the input element, or from a drag and drop operation's DataTransfer object."
+ },
+ "Blob": {
+ "!type": "fn(parts: [?], properties?: ?)",
+ "prototype": {
+ "size": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Blob",
+ "!doc": "The size, in bytes, of the data contained in the Blob object. Read only."
+ },
+ "type": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Blob",
+ "!doc": "An ASCII-encoded string, in all lower case, indicating the MIME type of the data contained in the Blob. If the type is unknown, this string is empty. Read only."
+ },
+ "slice": {
+ "!type": "fn(start: number, end?: number, type?: string) -> +Blob",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Blob",
+ "!doc": "Returns a new Blob object containing the data in the specified range of bytes of the source Blob."
+ }
+ },
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Blob",
+ "!doc": "A Blob object represents a file-like object of immutable, raw data. Blobs represent data that isn't necessarily in a JavaScript-native format. The File interface is based on Blob, inheriting blob functionality and expanding it to support files on the user's system."
+ },
+ "FileReader": {
+ "!type": "fn()",
+ "prototype": {
+ "abort": {
+ "!type": "fn()",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
+ "!doc": "Aborts the read operation. Upon return, the readyState will be DONE."
+ },
+ "readAsArrayBuffer": {
+ "!type": "fn(blob: +Blob)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
+ "!doc": "Starts reading the contents of the specified Blob, producing an ArrayBuffer."
+ },
+ "readAsBinaryString": {
+ "!type": "fn(blob: +Blob)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
+ "!doc": "Starts reading the contents of the specified Blob, producing raw binary data."
+ },
+ "readAsDataURL": {
+ "!type": "fn(blob: +Blob)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
+ "!doc": "Starts reading the contents of the specified Blob, producing a data: url."
+ },
+ "readAsText": {
+ "!type": "fn(blob: +Blob, encoding?: string)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
+ "!doc": "Starts reading the contents of the specified Blob, producing a string."
+ },
+ "EMPTY": "number",
+ "LOADING": "number",
+ "DONE": "number",
+ "error": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
+ "!doc": "The error that occurred while reading the file. Read only."
+ },
+ "readyState": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
+ "!doc": "Indicates the state of the FileReader. This will be one of the State constants. Read only."
+ },
+ "result": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
+ "!doc": "The file's contents. This property is only valid after the read operation is complete, and the format of the data depends on which of the methods was used to initiate the read operation. Read only."
+ },
+ "onabort": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
+ "!doc": "Called when the read operation is aborted."
+ },
+ "onerror": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
+ "!doc": "Called when an error occurs."
+ },
+ "onload": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
+ "!doc": "Called when the read operation is successfully completed."
+ },
+ "onloadend": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
+ "!doc": "Called when the read is completed, whether successful or not. This is called after either onload or onerror."
+ },
+ "onloadstart": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
+ "!doc": "Called when reading the data is about to begin."
+ },
+ "onprogress": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
+ "!doc": "Called periodically while the data is being read."
+ }
+ },
+ "!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
+ "!doc": "The FileReader object lets web applications asynchronously read the contents of files (or raw data buffers) stored on the user's computer, using File or Blob objects to specify the file or data to read. File objects may be obtained from a FileList object returned as a result of a user selecting files using the <input> element, from a drag and drop operation's DataTransfer object, or from the mozGetAsFile() API on an HTMLCanvasElement."
+ },
+ "Range": {
+ "!type": "fn()",
+ "prototype": {
+ "collapsed": {
+ "!type": "bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/range.collapsed",
+ "!doc": "Returns a boolean indicating whether the range's start and end points are at the same position."
+ },
+ "commonAncestorContainer": {
+ "!type": "+Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/range.commonAncestorContainer",
+ "!doc": "Returns the deepest Node that contains the startContainer and endContainer Nodes."
+ },
+ "endContainer": {
+ "!type": "+Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/range.endContainer",
+ "!doc": "Returns the Node within which the Range ends."
+ },
+ "endOffset": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/range.endOffset",
+ "!doc": "Returns a number representing where in the endContainer the Range ends."
+ },
+ "startContainer": {
+ "!type": "+Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/range.startContainer",
+ "!doc": "Returns the Node within which the Range starts."
+ },
+ "startOffset": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/range.startOffset",
+ "!doc": "Returns a number representing where in the startContainer the Range starts."
+ },
+ "setStart": {
+ "!type": "fn(node: +Element, offset: number)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/range.setStart",
+ "!doc": "Sets the start position of a Range."
+ },
+ "setEnd": {
+ "!type": "fn(node: +Element, offset: number)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/range.setEnd",
+ "!doc": "Sets the end position of a Range."
+ },
+ "setStartBefore": {
+ "!type": "fn(node: +Element)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/range.setStartBefore",
+ "!doc": "Sets the start position of a Range relative to another Node."
+ },
+ "setStartAfter": {
+ "!type": "fn(node: +Element)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/range.setStartAfter",
+ "!doc": "Sets the start position of a Range relative to a Node."
+ },
+ "setEndBefore": {
+ "!type": "fn(node: +Element)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/range.setEndBefore",
+ "!doc": "Sets the end position of a Range relative to another Node."
+ },
+ "setEndAfter": {
+ "!type": "fn(node: +Element)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/range.setEndAfter",
+ "!doc": "Sets the end position of a Range relative to another Node."
+ },
+ "selectNode": {
+ "!type": "fn(node: +Element)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/range.selectNode",
+ "!doc": "Sets the Range to contain the Node and its contents."
+ },
+ "selectNodeContents": {
+ "!type": "fn(node: +Element)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/range.selectNodeContents",
+ "!doc": "Sets the Range to contain the contents of a Node."
+ },
+ "collapse": {
+ "!type": "fn(toStart: bool)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/range.collapse",
+ "!doc": "Collapses the Range to one of its boundary points."
+ },
+ "cloneContents": {
+ "!type": "fn() -> +DocumentFragment",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/range.cloneContents",
+ "!doc": "Returns a DocumentFragment copying the Nodes of a Range."
+ },
+ "deleteContents": {
+ "!type": "fn()",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/range.deleteContents",
+ "!doc": "Removes the contents of a Range from the Document."
+ },
+ "extractContents": {
+ "!type": "fn() -> +DocumentFragment",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/range.extractContents",
+ "!doc": "Moves contents of a Range from the document tree into a DocumentFragment."
+ },
+ "insertNode": {
+ "!type": "fn(node: +Element)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/range.insertNode",
+ "!doc": "Insert a node at the start of a Range."
+ },
+ "surroundContents": {
+ "!type": "fn(node: +Element)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/range.surroundContents",
+ "!doc": "Moves content of a Range into a new node, placing the new node at the start of the specified range."
+ },
+ "compareBoundaryPoints": {
+ "!type": "fn(how: number, other: +Range) -> number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/range.compareBoundaryPoints",
+ "!doc": "Compares the boundary points of two Ranges."
+ },
+ "cloneRange": {
+ "!type": "fn() -> +Range",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/range.cloneRange",
+ "!doc": "Returns a Range object with boundary points identical to the cloned Range."
+ },
+ "detach": {
+ "!type": "fn()",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/range.detach",
+ "!doc": "Releases a Range from use to improve performance. This lets the browser choose to release resources associated with this Range. Subsequent attempts to use the detached range will result in a DOMException being thrown with an error code of INVALID_STATE_ERR."
+ },
+ "END_TO_END": "number",
+ "END_TO_START": "number",
+ "START_TO_END": "number",
+ "START_TO_START": "number"
+ },
+ "!url": "https://developer.mozilla.org/en/docs/DOM/range.detach",
+ "!doc": "Releases a Range from use to improve performance. This lets the browser choose to release resources associated with this Range. Subsequent attempts to use the detached range will result in a DOMException being thrown with an error code of INVALID_STATE_ERR."
+ },
+ "XMLHttpRequest": {
+ "!type": "fn()",
+ "prototype": {
+ "abort": {
+ "!type": "fn()",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/XMLHttpRequest",
+ "!doc": "Aborts the request if it has already been sent."
+ },
+ "getAllResponseHeaders": {
+ "!type": "fn() -> string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/XMLHttpRequest",
+ "!doc": "Returns all the response headers as a string, or null if no response has been received. Note: For multipart requests, this returns the headers from the current part of the request, not from the original channel."
+ },
+ "getResponseHeader": {
+ "!type": "fn(header: string) -> string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/XMLHttpRequest",
+ "!doc": "Returns the string containing the text of the specified header, or null if either the response has not yet been received or the header doesn't exist in the response."
+ },
+ "open": {
+ "!type": "fn(method: string, url: string, async?: bool, user?: string, password?: string)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/XMLHttpRequest",
+ "!doc": "Initializes a request."
+ },
+ "overrideMimeType": {
+ "!type": "fn(type: string)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/XMLHttpRequest",
+ "!doc": "Overrides the MIME type returned by the server."
+ },
+ "send": {
+ "!type": "fn(data?: string)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/XMLHttpRequest",
+ "!doc": "Sends the request. If the request is asynchronous (which is the default), this method returns as soon as the request is sent. If the request is synchronous, this method doesn't return until the response has arrived."
+ },
+ "setRequestHeader": {
+ "!type": "fn(header: string, value: string)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/XMLHttpRequest",
+ "!doc": "Sets the value of an HTTP request header.You must call setRequestHeader() after open(), but before send()."
+ },
+ "onreadystatechange": {
+ "!type": "fn()",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/XMLHttpRequest",
+ "!doc": "A JavaScript function object that is called whenever the readyState attribute changes."
+ },
+ "readyState": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/XMLHttpRequest",
+ "!doc": "The state of the request. (0=unsent, 1=opened, 2=headers_received, 3=loading, 4=done)"
+ },
+ "response": {
+ "!type": "+Document",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/XMLHttpRequest",
+ "!doc": "The response entity body according to responseType, as an ArrayBuffer, Blob, Document, JavaScript object (for \"json\"), or string. This is null if the request is not complete or was not successful."
+ },
+ "responseText": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/XMLHttpRequest",
+ "!doc": "The response to the request as text, or null if the request was unsuccessful or has not yet been sent."
+ },
+ "responseType": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/XMLHttpRequest",
+ "!doc": "Can be set to change the response type."
+ },
+ "responseXML": {
+ "!type": "+Document",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/XMLHttpRequest",
+ "!doc": "The response to the request as a DOM Document object, or null if the request was unsuccessful, has not yet been sent, or cannot be parsed as XML or HTML."
+ },
+ "status": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/XMLHttpRequest",
+ "!doc": "The status of the response to the request. This is the HTTP result code"
+ },
+ "statusText": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/XMLHttpRequest",
+ "!doc": "The response string returned by the HTTP server. Unlike status, this includes the entire text of the response message (\"200 OK\", for example)."
+ },
+ "timeout": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/XMLHttpRequest/Synchronous_and_Asynchronous_Requests",
+ "!doc": "The number of milliseconds a request can take before automatically being terminated. A value of 0 (which is the default) means there is no timeout."
+ },
+ "UNSENT": "number",
+ "OPENED": "number",
+ "HEADERS_RECEIVED": "number",
+ "LOADING": "number",
+ "DONE": "number"
+ },
+ "!url": "https://developer.mozilla.org/en/docs/DOM/XMLHttpRequest",
+ "!doc": "XMLHttpRequest is a JavaScript object that was designed by Microsoft and adopted by Mozilla, Apple, and Google. It's now being standardized in the W3C. It provides an easy way to retrieve data at a URL. Despite its name, XMLHttpRequest can be used to retrieve any type of data, not just XML, and it supports protocols other than HTTP (including file and ftp)."
+ },
+ "DOMParser": {
+ "!type": "fn()",
+ "prototype": {
+ "parseFromString": {
+ "!type": "fn(data: string, mime: string) -> +Document",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/DOMParser",
+ "!doc": "DOMParser can parse XML or HTML source stored in a string into a DOM Document. DOMParser is specified in DOM Parsing and Serialization."
+ }
+ },
+ "!url": "https://developer.mozilla.org/en/docs/DOM/DOMParser",
+ "!doc": "DOMParser can parse XML or HTML source stored in a string into a DOM Document. DOMParser is specified in DOM Parsing and Serialization."
+ },
+ "Selection": {
+ "!type": "fn()",
+ "prototype": {
+ "anchorNode": {
+ "!type": "+Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Selection/anchorNode",
+ "!doc": "Returns the node in which the selection begins."
+ },
+ "anchorOffset": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Selection/anchorOffset",
+ "!doc": "Returns the number of characters that the selection's anchor is offset within the anchorNode."
+ },
+ "focusNode": {
+ "!type": "+Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Selection/focusNode",
+ "!doc": "Returns the node in which the selection ends."
+ },
+ "focusOffset": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Selection/focusOffset",
+ "!doc": "Returns the number of characters that the selection's focus is offset within the focusNode. "
+ },
+ "isCollapsed": {
+ "!type": "bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Selection/isCollapsed",
+ "!doc": "Returns a boolean indicating whether the selection's start and end points are at the same position."
+ },
+ "rangeCount": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Selection/rangeCount",
+ "!doc": "Returns the number of ranges in the selection."
+ },
+ "getRangeAt": {
+ "!type": "fn(i: number) -> +Range",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Selection/getRangeAt",
+ "!doc": "Returns a range object representing one of the ranges currently selected."
+ },
+ "collapse": {
+ "!type": "fn()",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Selection/collapse",
+ "!doc": "Collapses the current selection to a single point. The document is not modified. If the content is focused and editable, the caret will blink there."
+ },
+ "extend": {
+ "!type": "fn(node: +Element, offset: number)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Selection/extend",
+ "!doc": "Moves the focus of the selection to a specified point. The anchor of the selection does not move. The selection will be from the anchor to the new focus regardless of direction."
+ },
+ "collapseToStart": {
+ "!type": "fn()",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Selection/collapseToStart",
+ "!doc": "Collapses the selection to the start of the first range in the selection. If the content of the selection is focused and editable, the caret will blink there."
+ },
+ "collapseToEnd": {
+ "!type": "fn()",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Selection/collapseToEnd",
+ "!doc": "Collapses the selection to the end of the last range in the selection. If the content the selection is in is focused and editable, the caret will blink there."
+ },
+ "selectAllChildren": {
+ "!type": "fn(node: +Element)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Selection/selectAllChildren",
+ "!doc": "Adds all the children of the specified node to the selection. Previous selection is lost."
+ },
+ "addRange": {
+ "!type": "fn(range: +Range)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Selection/addRange",
+ "!doc": "Adds a Range to a Selection."
+ },
+ "removeRange": {
+ "!type": "fn(range: +Range)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Selection/removeRange",
+ "!doc": "Removes a range from the selection."
+ },
+ "removeAllRanges": {
+ "!type": "fn()",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Selection/removeAllRanges",
+ "!doc": "Removes all ranges from the selection, leaving the anchorNode and focusNode properties equal to null and leaving nothing selected. "
+ },
+ "deleteFromDocument": {
+ "!type": "fn()",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Selection/deleteFromDocument",
+ "!doc": "Deletes the actual text being represented by a selection object from the document's DOM."
+ },
+ "containsNode": {
+ "!type": "fn(node: +Element) -> bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Selection/containsNode",
+ "!doc": "Indicates if the node is part of the selection."
+ }
+ },
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Selection",
+ "!doc": "Selection is the class of the object returned by window.getSelection() and other methods. It represents the text selection in the greater page, possibly spanning multiple elements, when the user drags over static text and other parts of the page. For information about text selection in an individual text editing element."
+ },
+ "console": {
+ "error": {
+ "!type": "fn(text: string)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/console.error",
+ "!doc": "Outputs an error message to the Web Console."
+ },
+ "info": {
+ "!type": "fn(text: string)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/console.info",
+ "!doc": "Outputs an informational message to the Web Console."
+ },
+ "log": {
+ "!type": "fn(text: string)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/console.log",
+ "!doc": "Outputs a message to the Web Console."
+ },
+ "warn": {
+ "!type": "fn(text: string)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/console.warn",
+ "!doc": "Outputs a warning message to the Web Console."
+ },
+ "!url": "https://developer.mozilla.org/en/docs/DOM/console",
+ "!doc": "The console object provides access to the browser's debugging console. The specifics of how it works vary from browser to browser, but there is a de facto set of features that are typically provided."
+ },
+ "top": {
+ "!type": "<top>",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.top",
+ "!doc": "Returns a reference to the topmost window in the window hierarchy."
+ },
+ "parent": {
+ "!type": "<top>",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.parent",
+ "!doc": "A reference to the parent of the current window or subframe."
+ },
+ "window": {
+ "!type": "<top>",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window",
+ "!doc": "This section provides a brief reference for all of the methods, properties, and events available through the DOM window object. The window object implements the Window interface, which in turn inherits from the AbstractView interface. Some additional global functions, namespaces objects, and constructors, not typically associated with the window, but available on it, are listed in the JavaScript Reference."
+ },
+ "opener": {
+ "!type": "<top>",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.opener",
+ "!doc": "Returns a reference to the window that opened this current window."
+ },
+ "self": {
+ "!type": "<top>",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.self",
+ "!doc": "Returns an object reference to the window object. "
+ },
+ "devicePixelRatio": "number",
+ "name": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/JavaScript/Reference/Global_Objects/Function/name",
+ "!doc": "The name of the function."
+ },
+ "closed": {
+ "!type": "bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.closed",
+ "!doc": "This property indicates whether the referenced window is closed or not."
+ },
+ "pageYOffset": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.scrollY",
+ "!doc": "Returns the number of pixels that the document has already been scrolled vertically."
+ },
+ "pageXOffset": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.scrollX",
+ "!doc": "Returns the number of pixels that the document has already been scrolled vertically."
+ },
+ "scrollY": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.scrollY",
+ "!doc": "Returns the number of pixels that the document has already been scrolled vertically."
+ },
+ "scrollX": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.scrollX",
+ "!doc": "Returns the number of pixels that the document has already been scrolled vertically."
+ },
+ "screenTop": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.screen.top",
+ "!doc": "Returns the distance in pixels from the top side of the current screen."
+ },
+ "screenLeft": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.screen.left",
+ "!doc": "Returns the distance in pixels from the left side of the main screen to the left side of the current screen."
+ },
+ "screenY": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/event.screenY",
+ "!doc": "Returns the vertical coordinate of the event within the screen as a whole."
+ },
+ "screenX": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/event.screenX",
+ "!doc": "Returns the horizontal coordinate of the event within the screen as a whole."
+ },
+ "innerWidth": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.innerWidth",
+ "!doc": "Width (in pixels) of the browser window viewport including, if rendered, the vertical scrollbar."
+ },
+ "innerHeight": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.innerHeight",
+ "!doc": "Height (in pixels) of the browser window viewport including, if rendered, the horizontal scrollbar."
+ },
+ "outerWidth": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.outerWidth",
+ "!doc": "window.outerWidth gets the width of the outside of the browser window. It represents the width of the whole browser window including sidebar (if expanded), window chrome and window resizing borders/handles."
+ },
+ "outerHeight": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.outerHeight",
+ "!doc": "window.outerHeight gets the height in pixels of the whole browser window."
+ },
+ "frameElement": {
+ "!type": "+Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.frameElement",
+ "!doc": "Returns the element (such as <iframe> or <object>) in which the window is embedded, or null if the window is top-level."
+ },
+ "crypto": {
+ "getRandomValues": {
+ "!type": "fn([number])",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.crypto.getRandomValues",
+ "!doc": "This methods lets you get cryptographically random values."
+ },
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.crypto.getRandomValues",
+ "!doc": "This methods lets you get cryptographically random values."
+ },
+ "navigator": {
+ "appName": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.navigator.appName",
+ "!doc": "Returns the name of the browser. The HTML5 specification also allows any browser to return \"Netscape\" here, for compatibility reasons."
+ },
+ "appVersion": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.navigator.appVersion",
+ "!doc": "Returns the version of the browser as a string. It may be either a plain version number, like \"5.0\", or a version number followed by more detailed information. The HTML5 specification also allows any browser to return \"4.0\" here, for compatibility reasons."
+ },
+ "language": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.navigator.language",
+ "!doc": "Returns a string representing the language version of the browser."
+ },
+ "platform": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.navigator.platform",
+ "!doc": "Returns a string representing the platform of the browser."
+ },
+ "plugins": {
+ "!type": "[?]",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.navigator.plugins",
+ "!doc": "Returns a PluginArray object, listing the plugins installed in the application."
+ },
+ "userAgent": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.navigator.userAgent",
+ "!doc": "Returns the user agent string for the current browser."
+ },
+ "vendor": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.navigator.vendor",
+ "!doc": "Returns the name of the browser vendor for the current browser."
+ },
+ "javaEnabled": {
+ "!type": "bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.navigator.javaEnabled",
+ "!doc": "This method indicates whether the current browser is Java-enabled or not."
+ },
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.navigator",
+ "!doc": "Returns a reference to the navigator object, which can be queried for information about the application running the script."
+ },
+ "history": {
+ "state": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Manipulating_the_browser_history",
+ "!doc": "The DOM window object provides access to the browser's history through the history object. It exposes useful methods and properties that let you move back and forth through the user's history, as well as -- starting with HTML5 -- manipulate the contents of the history stack."
+ },
+ "length": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Manipulating_the_browser_history",
+ "!doc": "The DOM window object provides access to the browser's history through the history object. It exposes useful methods and properties that let you move back and forth through the user's history, as well as -- starting with HTML5 -- manipulate the contents of the history stack."
+ },
+ "go": {
+ "!type": "fn(delta: number)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.history",
+ "!doc": "Returns a reference to the History object, which provides an interface for manipulating the browser session history (pages visited in the tab or frame that the current page is loaded in)."
+ },
+ "forward": {
+ "!type": "fn()",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Manipulating_the_browser_history",
+ "!doc": "The DOM window object provides access to the browser's history through the history object. It exposes useful methods and properties that let you move back and forth through the user's history, as well as -- starting with HTML5 -- manipulate the contents of the history stack."
+ },
+ "back": {
+ "!type": "fn()",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Manipulating_the_browser_history",
+ "!doc": "The DOM window object provides access to the browser's history through the history object. It exposes useful methods and properties that let you move back and forth through the user's history, as well as -- starting with HTML5 -- manipulate the contents of the history stack."
+ },
+ "pushState": {
+ "!type": "fn(data: ?, title: string, url?: string)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Manipulating_the_browser_history",
+ "!doc": "The DOM window object provides access to the browser's history through the history object. It exposes useful methods and properties that let you move back and forth through the user's history, as well as -- starting with HTML5 -- manipulate the contents of the history stack."
+ },
+ "replaceState": {
+ "!type": "fn(data: ?, title: string, url?: string)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Manipulating_the_browser_history",
+ "!doc": "The DOM window object provides access to the browser's history through the history object. It exposes useful methods and properties that let you move back and forth through the user's history, as well as -- starting with HTML5 -- manipulate the contents of the history stack."
+ },
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Manipulating_the_browser_history",
+ "!doc": "The DOM window object provides access to the browser's history through the history object. It exposes useful methods and properties that let you move back and forth through the user's history, as well as -- starting with HTML5 -- manipulate the contents of the history stack."
+ },
+ "screen": {
+ "availWidth": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.screen.availWidth",
+ "!doc": "Returns the amount of horizontal space in pixels available to the window."
+ },
+ "availHeight": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.screen.availHeight",
+ "!doc": "Returns the amount of vertical space available to the window on the screen."
+ },
+ "availTop": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.screen.availTop",
+ "!doc": "Specifies the y-coordinate of the first pixel that is not allocated to permanent or semipermanent user interface features."
+ },
+ "availLeft": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.screen.availLeft",
+ "!doc": "Returns the first available pixel available from the left side of the screen."
+ },
+ "pixelDepth": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.screen.pixelDepth",
+ "!doc": "Returns the bit depth of the screen."
+ },
+ "colorDepth": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.screen.colorDepth",
+ "!doc": "Returns the color depth of the screen."
+ },
+ "width": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.screen.width",
+ "!doc": "Returns the width of the screen."
+ },
+ "height": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.screen.height",
+ "!doc": "Returns the height of the screen in pixels."
+ },
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.screen",
+ "!doc": "Returns a reference to the screen object associated with the window."
+ },
+ "postMessage": {
+ "!type": "fn(message: string, targetOrigin: string)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.postMessage",
+ "!doc": "window.postMessage, when called, causes a MessageEvent to be dispatched at the target window when any pending script that must be executed completes (e.g. remaining event handlers if window.postMessage is called from an event handler, previously-set pending timeouts, etc.). The MessageEvent has the type message, a data property which is set to the value of the first argument provided to window.postMessage, an origin property corresponding to the origin of the main document in the window calling window.postMessage at the time window.postMessage was called, and a source property which is the window from which window.postMessage is called. (Other standard properties of events are present with their expected values.)"
+ },
+ "close": {
+ "!type": "fn()",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.close",
+ "!doc": "Closes the current window, or a referenced window."
+ },
+ "blur": {
+ "!type": "fn()",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.blur",
+ "!doc": "The blur method removes keyboard focus from the current element."
+ },
+ "focus": {
+ "!type": "fn()",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.focus",
+ "!doc": "Sets focus on the specified element, if it can be focused."
+ },
+ "onload": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.onload",
+ "!doc": "An event handler for the load event of a window."
+ },
+ "onunload": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.onunload",
+ "!doc": "The unload event is raised when the window is unloading its content and resources. The resources removal is processed after the unload event occurs."
+ },
+ "onscroll": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.onscroll",
+ "!doc": "Specifies the function to be called when the window is scrolled."
+ },
+ "onresize": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.onresize",
+ "!doc": "An event handler for the resize event on the window."
+ },
+ "ononline": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.ononline",
+ "!doc": ",fgh s dgkljgsdfl dfjg sdlgj sdlg sdlfj dlg jkdfkj dfjgdfkglsdfjsdlfkgj hdflkg hdlkfjgh dfkjgh"
+ },
+ "onoffline": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/Online_and_offline_events",
+ "!doc": "Some browsers implement Online/Offline events from the WHATWG Web Applications 1.0 specification."
+ },
+ "onmousewheel": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/DOM_event_reference/mousewheel",
+ "!doc": "The DOM mousewheel event is fired asynchronously when mouse wheel or similar device is operated. It's represented by the MouseWheelEvent interface."
+ },
+ "onmouseup": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.onmouseup",
+ "!doc": "An event handler for the mouseup event on the window."
+ },
+ "onmouseover": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.onmouseover",
+ "!doc": "The onmouseover property returns the onMouseOver event handler code on the current element."
+ },
+ "onmouseout": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.onmouseout",
+ "!doc": "The onmouseout property returns the onMouseOut event handler code on the current element."
+ },
+ "onmousemove": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.onmousemove",
+ "!doc": "The onmousemove property returns the mousemove event handler code on the current element."
+ },
+ "onmousedown": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.onmousedown",
+ "!doc": "An event handler for the mousedown event on the window."
+ },
+ "onclick": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.onclick",
+ "!doc": "The onclick property returns the onClick event handler code on the current element."
+ },
+ "ondblclick": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.ondblclick",
+ "!doc": "The ondblclick property returns the onDblClick event handler code on the current element."
+ },
+ "onmessage": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Worker",
+ "!doc": "Dedicated Web Workers provide a simple means for web content to run scripts in background threads. Once created, a worker can send messages to the spawning task by posting messages to an event handler specified by the creator."
+ },
+ "onkeyup": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.onkeyup",
+ "!doc": "The onkeyup property returns the onKeyUp event handler code for the current element."
+ },
+ "onkeypress": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.onkeypress",
+ "!doc": "The onkeypress property sets and returns the onKeyPress event handler code for the current element."
+ },
+ "onkeydown": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.onkeydown",
+ "!doc": "An event handler for the keydown event on the window."
+ },
+ "oninput": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/DOM_event_reference/input",
+ "!doc": "The DOM input event is fired synchronously when the value of an <input> or <textarea> element is changed. Additionally, it's also fired on contenteditable editors when its contents are changed. In this case, the event target is the editing host element. If there are two or more elements which have contenteditable as true, \"editing host\" is the nearest ancestor element whose parent isn't editable. Similarly, it's also fired on root element of designMode editors."
+ },
+ "onpopstate": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.onpopstate",
+ "!doc": "An event handler for the popstate event on the window."
+ },
+ "onhashchange": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.onhashchange",
+ "!doc": "The hashchange event fires when a window's hash changes."
+ },
+ "onfocus": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.onfocus",
+ "!doc": "The onfocus property returns the onFocus event handler code on the current element."
+ },
+ "onblur": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.onblur",
+ "!doc": "The onblur property returns the onBlur event handler code, if any, that exists on the current element."
+ },
+ "onerror": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.onerror",
+ "!doc": "An event handler for runtime script errors."
+ },
+ "ondrop": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en-US/docs/DOM/Mozilla_event_reference/drop",
+ "!doc": "The drop event is fired when an element or text selection is dropped on a valid drop target."
+ },
+ "ondragstart": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en-US/docs/DOM/Mozilla_event_reference/dragstart",
+ "!doc": "The dragstart event is fired when the user starts dragging an element or text selection."
+ },
+ "ondragover": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en-US/docs/DOM/Mozilla_event_reference/dragover",
+ "!doc": "The dragover event is fired when an element or text selection is being dragged over a valid drop target (every few hundred milliseconds)."
+ },
+ "ondragleave": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en-US/docs/DOM/Mozilla_event_reference/dragleave",
+ "!doc": "The dragleave event is fired when a dragged element or text selection leaves a valid drop target."
+ },
+ "ondragenter": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en-US/docs/DOM/Mozilla_event_reference/dragenter",
+ "!doc": "The dragenter event is fired when a dragged element or text selection enters a valid drop target."
+ },
+ "ondragend": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en-US/docs/DOM/Mozilla_event_reference/dragend",
+ "!doc": "The dragend event is fired when a drag operation is being ended (by releasing a mouse button or hitting the escape key)."
+ },
+ "ondrag": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en-US/docs/DOM/Mozilla_event_reference/drag",
+ "!doc": "The drag event is fired when an element or text selection is being dragged (every few hundred milliseconds)."
+ },
+ "oncontextmenu": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.oncontextmenu",
+ "!doc": "An event handler property for right-click events on the window. Unless the default behavior is prevented, the browser context menu will activate (though IE8 has a bug with this and will not activate the context menu if a contextmenu event handler is defined). Note that this event will occur with any non-disabled right-click event and does not depend on an element possessing the \"contextmenu\" attribute."
+ },
+ "onchange": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.onchange",
+ "!doc": "The onchange property sets and returns the onChange event handler code for the current element."
+ },
+ "onbeforeunload": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.onbeforeunload",
+ "!doc": "An event that fires when a window is about to unload its resources. The document is still visible and the event is still cancelable."
+ },
+ "onabort": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.onabort",
+ "!doc": "An event handler for abort events sent to the window."
+ },
+ "getSelection": {
+ "!type": "fn() -> +Selection",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.getSelection",
+ "!doc": "Returns a selection object representing the range of text selected by the user. "
+ },
+ "alert": {
+ "!type": "fn(message: string)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.alert",
+ "!doc": "Display an alert dialog with the specified content and an OK button."
+ },
+ "confirm": {
+ "!type": "fn(message: string) -> bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.confirm",
+ "!doc": "Displays a modal dialog with a message and two buttons, OK and Cancel."
+ },
+ "prompt": {
+ "!type": "fn(message: string, value: string) -> string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.prompt",
+ "!doc": "Displays a dialog with a message prompting the user to input some text."
+ },
+ "scrollBy": {
+ "!type": "fn(x: number, y: number)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.scrollBy",
+ "!doc": "Scrolls the document in the window by the given amount."
+ },
+ "scrollTo": {
+ "!type": "fn(x: number, y: number)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.scrollTo",
+ "!doc": "Scrolls to a particular set of coordinates in the document."
+ },
+ "scroll": {
+ "!type": "fn(x: number, y: number)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.scroll",
+ "!doc": "Scrolls the window to a particular place in the document."
+ },
+ "setTimeout": {
+ "!type": "fn(f: fn(), ms: number) -> number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.setTimeout",
+ "!doc": "Calls a function or executes a code snippet after specified delay."
+ },
+ "clearTimeout": {
+ "!type": "fn(timeout: number)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.clearTimeout",
+ "!doc": "Clears the delay set by window.setTimeout()."
+ },
+ "setInterval": {
+ "!type": "fn(f: fn(), ms: number) -> number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.setInterval",
+ "!doc": "Calls a function or executes a code snippet repeatedly, with a fixed time delay between each call to that function."
+ },
+ "clearInterval": {
+ "!type": "fn(interval: number)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.clearInterval",
+ "!doc": "Cancels repeated action which was set up using setInterval."
+ },
+ "atob": {
+ "!type": "fn(encoded: string) -> string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.atob",
+ "!doc": "Decodes a string of data which has been encoded using base-64 encoding."
+ },
+ "btoa": {
+ "!type": "fn(data: string) -> string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.btoa",
+ "!doc": "Creates a base-64 encoded ASCII string from a string of binary data."
+ },
+ "addEventListener": {
+ "!type": "fn(type: string, listener: fn(e: +Event), capture: bool)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/EventTarget.addEventListener",
+ "!doc": "Registers a single event listener on a single target. The event target may be a single element in a document, the document itself, a window, or an XMLHttpRequest."
+ },
+ "removeEventListener": {
+ "!type": "fn(type: string, listener: fn(), capture: bool)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/EventTarget.removeEventListener",
+ "!doc": "Allows the removal of event listeners from the event target."
+ },
+ "dispatchEvent": {
+ "!type": "fn(event: +Event) -> bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/EventTarget.dispatchEvent",
+ "!doc": "Dispatches an event into the event system. The event is subject to the same capturing and bubbling behavior as directly dispatched events."
+ },
+ "getComputedStyle": {
+ "!type": "fn(node: +Element, pseudo?: string) -> Element.prototype.style",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.getComputedStyle",
+ "!doc": "Gives the final used values of all the CSS properties of an element."
+ },
+ "CanvasRenderingContext2D": {
+ "canvas": "+Element",
+ "width": "number",
+ "height": "number",
+ "commit": "fn()",
+ "save": "fn()",
+ "restore": "fn()",
+ "currentTransform": "?",
+ "scale": "fn(x: number, y: number)",
+ "rotate": "fn(angle: number)",
+ "translate": "fn(x: number, y: number)",
+ "transform": "fn(a: number, b: number, c: number, d: number, e: number, f: number)",
+ "setTransform": "fn(a: number, b: number, c: number, d: number, e: number, f: number)",
+ "resetTransform": "fn()",
+ "globalAlpha": "number",
+ "globalCompositeOperation": "string",
+ "imageSmoothingEnabled": "bool",
+ "strokeStyle": "string",
+ "fillStyle": "string",
+ "createLinearGradient": "fn(x0: number, y0: number, x1: number, y1: number) -> ?",
+ "createPattern": "fn(image: ?, repetition: string) -> ?",
+ "shadowOffsetX": "number",
+ "shadowOffsetY": "number",
+ "shadowBlur": "number",
+ "shadowColor": "string",
+ "clearRect": "fn(x: number, y: number, w: number, h: number)",
+ "fillRect": "fn(x: number, y: number, w: number, h: number)",
+ "strokeRect": "fn(x: number, y: number, w: number, h: number)",
+ "fillRule": "string",
+ "fill": "fn()",
+ "beginPath": "fn()",
+ "stroke": "fn()",
+ "clip": "fn()",
+ "resetClip": "fn()",
+ "measureText": "fn(text: string) -> ?",
+ "drawImage": "fn(image: ?, dx: number, dy: number)",
+ "createImageData": "fn(sw: number, sh: number) -> ?",
+ "getImageData": "fn(sx: number, sy: number, sw: number, sh: number) -> ?",
+ "putImageData": "fn(imagedata: ?, dx: number, dy: number)",
+ "lineWidth": "number",
+ "lineCap": "string",
+ "lineJoin": "string",
+ "miterLimit": "number",
+ "setLineDash": "fn(segments: [number])",
+ "getLineDash": "fn() -> [number]",
+ "lineDashOffset": "number",
+ "font": "string",
+ "textAlign": "string",
+ "textBaseline": "string",
+ "direction": "string",
+ "closePath": "fn()",
+ "moveTo": "fn(x: number, y: number)",
+ "lineTo": "fn(x: number, y: number)",
+ "quadraticCurveTo": "fn(cpx: number, cpy: number, x: number, y: number)",
+ "bezierCurveTo": "fn(cp1x: number, cp1y: number, cp2x: number, cp2y: number, x: number, y: number)",
+ "arcTo": "fn(x1: number, y1: number, x2: number, y2: number, radius: number)",
+ "rect": "fn(x: number, y: number, w: number, h: number)",
+ "arc": "fn(x: number, y: number, radius: number, startAngle: number, endAngle: number, anticlockwise?: bool)",
+ "ellipse": "fn(x: number, y: number, radiusX: number, radiusY: number, rotation: number, startAngle: number, endAngle: number, anticlockwise: bool)"
+ }
+}
diff --git a/devtools/client/sourceeditor/tern/comment.js b/devtools/client/sourceeditor/tern/comment.js
new file mode 100755
index 000000000..e3ff87190
--- /dev/null
+++ b/devtools/client/sourceeditor/tern/comment.js
@@ -0,0 +1,87 @@
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ return mod(exports);
+ if (typeof define == "function" && define.amd) // AMD
+ return define(["exports"], mod);
+ mod(tern.comment || (tern.comment = {}));
+})(function(exports) {
+ function isSpace(ch) {
+ return (ch < 14 && ch > 8) || ch === 32 || ch === 160;
+ }
+
+ function onOwnLine(text, pos) {
+ for (; pos > 0; --pos) {
+ var ch = text.charCodeAt(pos - 1);
+ if (ch == 10) break;
+ if (!isSpace(ch)) return false;
+ }
+ return true;
+ }
+
+ // Gather comments directly before a function
+ exports.commentsBefore = function(text, pos) {
+ var found = null, emptyLines = 0, topIsLineComment;
+ out: while (pos > 0) {
+ var prev = text.charCodeAt(pos - 1);
+ if (prev == 10) {
+ for (var scan = --pos, sawNonWS = false; scan > 0; --scan) {
+ prev = text.charCodeAt(scan - 1);
+ if (prev == 47 && text.charCodeAt(scan - 2) == 47) {
+ if (!onOwnLine(text, scan - 2)) break out;
+ var content = text.slice(scan, pos);
+ if (!emptyLines && topIsLineComment) found[0] = content + "\n" + found[0];
+ else (found || (found = [])).unshift(content);
+ topIsLineComment = true;
+ emptyLines = 0;
+ pos = scan - 2;
+ break;
+ } else if (prev == 10) {
+ if (!sawNonWS && ++emptyLines > 1) break out;
+ break;
+ } else if (!sawNonWS && !isSpace(prev)) {
+ sawNonWS = true;
+ }
+ }
+ } else if (prev == 47 && text.charCodeAt(pos - 2) == 42) {
+ for (var scan = pos - 2; scan > 1; --scan) {
+ if (text.charCodeAt(scan - 1) == 42 && text.charCodeAt(scan - 2) == 47) {
+ if (!onOwnLine(text, scan - 2)) break out;
+ (found || (found = [])).unshift(text.slice(scan, pos - 2));
+ topIsLineComment = false;
+ emptyLines = 0;
+ break;
+ }
+ }
+ pos = scan - 2;
+ } else if (isSpace(prev)) {
+ --pos;
+ } else {
+ break;
+ }
+ }
+ return found;
+ };
+
+ exports.commentAfter = function(text, pos) {
+ while (pos < text.length) {
+ var next = text.charCodeAt(pos);
+ if (next == 47) {
+ var after = text.charCodeAt(pos + 1), end;
+ if (after == 47) // line comment
+ end = text.indexOf("\n", pos + 2);
+ else if (after == 42) // block comment
+ end = text.indexOf("*/", pos + 2);
+ else
+ return;
+ return text.slice(pos + 2, end < 0 ? text.length : end);
+ } else if (isSpace(next)) {
+ ++pos;
+ }
+ }
+ };
+
+ exports.ensureCommentsBefore = function(text, node) {
+ if (node.hasOwnProperty("commentsBefore")) return node.commentsBefore;
+ return node.commentsBefore = exports.commentsBefore(text, node.start);
+ };
+});
diff --git a/devtools/client/sourceeditor/tern/condense.js b/devtools/client/sourceeditor/tern/condense.js
new file mode 100755
index 000000000..f6bf626d6
--- /dev/null
+++ b/devtools/client/sourceeditor/tern/condense.js
@@ -0,0 +1,304 @@
+// Condensing an inferred set of types to a JSON description document.
+
+// This code can be used to, after a library has been analyzed,
+// extract the types defined in that library and dump them as a JSON
+// structure (as parsed by def.js).
+
+// The idea being that big libraries can be analyzed once, dumped, and
+// then cheaply included in later analysis.
+
+(function(root, mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ return mod(exports, require("./infer"));
+ if (typeof define == "function" && define.amd) // AMD
+ return define(["exports", "./infer"], mod);
+ mod(root.tern || (root.tern = {}), tern); // Plain browser env
+})(this, function(exports, infer) {
+ "use strict";
+
+ exports.condense = function(origins, name, options) {
+ if (typeof origins == "string") origins = [origins];
+ var state = new State(origins, name || origins[0], options || {});
+
+ state.server.signal("preCondenseReach", state)
+
+ state.cx.topScope.path = "<top>";
+ state.cx.topScope.reached("", state);
+ for (var path in state.roots)
+ reach(state.roots[path], null, path, state);
+ for (var i = 0; i < state.patchUp.length; ++i)
+ patchUpSimpleInstance(state.patchUp[i], state);
+
+ state.server.signal("postCondenseReach", state)
+
+ for (var path in state.types)
+ store(createPath(path.split("."), state), state.types[path], state);
+ for (var path in state.altPaths)
+ storeAlt(path, state.altPaths[path], state);
+ var hasDef = false;
+ for (var _def in state.output["!define"]) { hasDef = true; break; }
+ if (!hasDef) delete state.output["!define"];
+
+ state.server.signal("postCondense", state)
+
+ return simplify(state.output, state.options.sortOutput);
+ };
+
+ function State(origins, name, options) {
+ this.origins = origins;
+ this.cx = infer.cx();
+ this.server = options.server || this.cx.parent || {signal: function() {}}
+ this.maxOrigin = -Infinity;
+ for (var i = 0; i < origins.length; ++i)
+ this.maxOrigin = Math.max(this.maxOrigin, this.cx.origins.indexOf(origins[i]));
+ this.output = {"!name": name, "!define": {}};
+ this.options = options;
+ this.types = Object.create(null);
+ this.altPaths = Object.create(null);
+ this.patchUp = [];
+ this.roots = Object.create(null);
+ }
+
+ State.prototype.isTarget = function(origin) {
+ return this.origins.indexOf(origin) > -1;
+ };
+
+ State.prototype.getSpan = function(node) {
+ if (this.options.spans == false || !this.isTarget(node.origin)) return null;
+ if (node.span) return node.span;
+ var srv = this.cx.parent, file;
+ if (!srv || !node.originNode || !(file = srv.findFile(node.origin))) return null;
+ var start = node.originNode.start, end = node.originNode.end;
+ var pStart = file.asLineChar(start), pEnd = file.asLineChar(end);
+ return start + "[" + pStart.line + ":" + pStart.ch + "]-" +
+ end + "[" + pEnd.line + ":" + pEnd.ch + "]";
+ };
+
+ function pathLen(path) {
+ var len = 1, pos = 0, dot;
+ while ((dot = path.indexOf(".", pos)) != -1) {
+ pos = dot + 1;
+ len += path.charAt(pos) == "!" ? 10 : 1;
+ }
+ return len;
+ }
+
+ function hop(obj, prop) {
+ return Object.prototype.hasOwnProperty.call(obj, prop);
+ }
+
+ function isSimpleInstance(o) {
+ return o.proto && !(o instanceof infer.Fn) && o.proto != infer.cx().protos.Object &&
+ o.proto.hasCtor && !o.hasCtor;
+ }
+
+ function reach(type, path, id, state, byName) {
+ var actual = type.getType(false);
+ if (!actual) return;
+ var orig = type.origin || actual.origin, relevant = false;
+ if (orig) {
+ var origPos = state.cx.origins.indexOf(orig);
+ // This is a path that is newer than the code we are interested in.
+ if (origPos > state.maxOrigin) return;
+ relevant = state.isTarget(orig);
+ }
+ var newPath = path ? path + "." + id : id, oldPath = actual.path;
+ var shorter = !oldPath || pathLen(oldPath) > pathLen(newPath);
+ if (shorter) {
+ if (!(actual instanceof infer.Prim)) actual.path = newPath;
+ if (actual.reached(newPath, state, !relevant) && relevant) {
+ var data = state.types[oldPath];
+ if (data) {
+ delete state.types[oldPath];
+ state.altPaths[oldPath] = actual;
+ } else data = {type: actual};
+ data.span = state.getSpan(type) || (actual != type && state.isTarget(actual.origin) && state.getSpan(actual)) || data.span;
+ data.doc = type.doc || (actual != type && state.isTarget(actual.origin) && actual.doc) || data.doc;
+ data.data = actual.metaData;
+ data.byName = data.byName == null ? !!byName : data.byName && byName;
+ state.types[newPath] = data;
+ }
+ } else {
+ if (relevant) state.altPaths[newPath] = actual;
+ }
+ }
+ function reachByName(aval, path, id, state) {
+ var type = aval.getType();
+ if (type) reach(type, path, id, state, true);
+ }
+
+ infer.Prim.prototype.reached = function() {return true;};
+
+ infer.Arr.prototype.reached = function(path, state, concrete) {
+ if (concrete) return true
+ if (this.tuple) {
+ for (var i = 0; i < this.tuple; i++)
+ reachByName(this.getProp(String(i)), path, String(i), state)
+ } else {
+ reachByName(this.getProp("<i>"), path, "<i>", state)
+ }
+ return true;
+ };
+
+ infer.Fn.prototype.reached = function(path, state, concrete) {
+ infer.Obj.prototype.reached.call(this, path, state, concrete);
+ if (!concrete) {
+ for (var i = 0; i < this.args.length; ++i)
+ reachByName(this.args[i], path, "!" + i, state);
+ reachByName(this.retval, path, "!ret", state);
+ }
+ return true;
+ };
+
+ infer.Obj.prototype.reached = function(path, state, concrete) {
+ if (isSimpleInstance(this) && !this.condenseForceInclude) {
+ if (state.patchUp.indexOf(this) == -1) state.patchUp.push(this);
+ return true;
+ } else if (this.proto && !concrete) {
+ reach(this.proto, path, "!proto", state);
+ }
+ var hasProps = false;
+ for (var prop in this.props) {
+ reach(this.props[prop], path, prop, state);
+ hasProps = true;
+ }
+ if (!hasProps && !this.condenseForceInclude && !(this instanceof infer.Fn)) {
+ this.nameOverride = "?";
+ return false;
+ }
+ return true;
+ };
+
+ function patchUpSimpleInstance(obj, state) {
+ var path = obj.proto.hasCtor.path;
+ if (path) {
+ obj.nameOverride = "+" + path;
+ } else {
+ path = obj.path;
+ }
+ for (var prop in obj.props)
+ reach(obj.props[prop], path, prop, state);
+ }
+
+ function createPath(parts, state) {
+ var base = state.output, defs = state.output["!define"];
+ for (var i = 0, path; i < parts.length; ++i) {
+ var part = parts[i];
+ path = path ? path + "." + part : part;
+ var me = state.types[path];
+ if (part.charAt(0) == "!" || me && me.byName) {
+ if (hop(defs, path)) base = defs[path];
+ else defs[path] = base = {};
+ } else {
+ if (hop(base, parts[i])) base = base[part];
+ else base = base[part] = {};
+ }
+ }
+ return base;
+ }
+
+ function store(out, info, state) {
+ var name = typeName(info.type);
+ if (name != info.type.path && name != "?") {
+ out["!type"] = name;
+ } else if (info.type.proto && info.type.proto != state.cx.protos.Object) {
+ var protoName = typeName(info.type.proto);
+ if (protoName != "?") out["!proto"] = protoName;
+ }
+ if (info.span) out["!span"] = info.span;
+ if (info.doc) out["!doc"] = info.doc;
+ if (info.data) out["!data"] = info.data;
+ }
+
+ function storeAlt(path, type, state) {
+ var parts = path.split("."), last = parts.pop();
+ if (last[0] == "!") return;
+ var known = state.types[parts.join(".")];
+ var base = createPath(parts, state);
+ if (known && known.type.constructor != infer.Obj) return;
+ if (!hop(base, last)) base[last] = type.nameOverride || type.path;
+ }
+
+ var typeNameStack = [];
+ function typeName(value) {
+ var isType = value instanceof infer.Type;
+ if (isType) {
+ if (typeNameStack.indexOf(value) > -1)
+ return value.path || "?";
+ typeNameStack.push(value);
+ }
+ var name = value.typeName();
+ if (isType) typeNameStack.pop();
+ return name;
+ }
+
+ infer.AVal.prototype.typeName = function() {
+ if (this.types.length == 0) return "?";
+ if (this.types.length == 1) return typeName(this.types[0]);
+ var simplified = infer.simplifyTypes(this.types);
+ if (simplified.length > 2) return "?";
+ for (var strs = [], i = 0; i < simplified.length; i++)
+ strs.push(typeName(simplified[i]));
+ return strs.join("|");
+ };
+
+ infer.ANull.typeName = function() { return "?"; };
+
+ infer.Prim.prototype.typeName = function() { return this.name; };
+
+ infer.Sym.prototype.typeName = function() { return this.asPropName }
+
+ infer.Arr.prototype.typeName = function() {
+ if (!this.tuple) return "[" + typeName(this.getProp("<i>")) + "]"
+ var content = []
+ for (var i = 0; i < this.tuple; i++)
+ content.push(typeName(this.getProp(String(i))))
+ return "[" + content.join(", ") + "]"
+ };
+
+ infer.Fn.prototype.typeName = function() {
+ var out = this.generator ? "fn*(" : "fn(";
+ for (var i = 0; i < this.args.length; ++i) {
+ if (i) out += ", ";
+ var name = this.argNames[i];
+ if (name && name != "?") out += name + ": ";
+ out += typeName(this.args[i]);
+ }
+ out += ")";
+ if (this.computeRetSource)
+ out += " -> " + this.computeRetSource;
+ else if (!this.retval.isEmpty())
+ out += " -> " + typeName(this.retval);
+ return out;
+ };
+
+ infer.Obj.prototype.typeName = function() {
+ if (this.nameOverride) return this.nameOverride;
+ if (!this.path) return "?";
+ return this.path;
+ };
+
+ function simplify(data, sort) {
+ if (typeof data != "object") return data;
+ var sawType = false, sawOther = false;
+ for (var prop in data) {
+ if (prop == "!type") sawType = true;
+ else sawOther = true;
+ if (prop != "!data")
+ data[prop] = simplify(data[prop], sort);
+ }
+ if (sawType && !sawOther) return data["!type"];
+ return sort ? sortObject(data) : data;
+ }
+
+ function sortObject(obj) {
+ var props = [], out = {};
+ for (var prop in obj) props.push(prop);
+ props.sort();
+ for (var i = 0; i < props.length; ++i) {
+ var prop = props[i];
+ out[prop] = obj[prop];
+ }
+ return out;
+ }
+});
diff --git a/devtools/client/sourceeditor/tern/def.js b/devtools/client/sourceeditor/tern/def.js
new file mode 100755
index 000000000..71f6e7991
--- /dev/null
+++ b/devtools/client/sourceeditor/tern/def.js
@@ -0,0 +1,656 @@
+// Type description parser
+//
+// Type description JSON files (such as ecma5.json and browser.json)
+// are used to
+//
+// A) describe types that come from native code
+//
+// B) to cheaply load the types for big libraries, or libraries that
+// can't be inferred well
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ return exports.init = mod;
+ if (typeof define == "function" && define.amd) // AMD
+ return define({init: mod});
+ tern.def = {init: mod};
+})(function(exports, infer) {
+ "use strict";
+
+ function hop(obj, prop) {
+ return Object.prototype.hasOwnProperty.call(obj, prop);
+ }
+
+ var TypeParser = exports.TypeParser = function(spec, start, base, forceNew) {
+ this.pos = start || 0;
+ this.spec = spec;
+ this.base = base;
+ this.forceNew = forceNew;
+ };
+
+ function unwrapType(type, self, args) {
+ return type.call ? type(self, args) : type;
+ }
+
+ function extractProp(type, prop) {
+ if (prop == "!ret") {
+ if (type.retval) return type.retval;
+ var rv = new infer.AVal;
+ type.propagate(new infer.IsCallee(infer.ANull, [], null, rv));
+ return rv;
+ } else {
+ return type.getProp(prop);
+ }
+ }
+
+ function computedFunc(name, args, retType, generator) {
+ return function(self, cArgs) {
+ var realArgs = [];
+ for (var i = 0; i < args.length; i++) realArgs.push(unwrapType(args[i], self, cArgs));
+ return new infer.Fn(name, infer.ANull, realArgs, unwrapType(retType, self, cArgs), generator);
+ };
+ }
+ function computedUnion(types) {
+ return function(self, args) {
+ var union = new infer.AVal;
+ for (var i = 0; i < types.length; i++) unwrapType(types[i], self, args).propagate(union);
+ union.maxWeight = 1e5;
+ return union;
+ };
+ }
+ function computedArray(inner) {
+ return function(self, args) {
+ return new infer.Arr(inner(self, args));
+ };
+ }
+ function computedTuple(types) {
+ return function(self, args) {
+ return new infer.Arr(types.map(function(tp) { return unwrapType(tp, self, args) }))
+ }
+ }
+
+ TypeParser.prototype = {
+ eat: function(str) {
+ if (str.length == 1 ? this.spec.charAt(this.pos) == str : this.spec.indexOf(str, this.pos) == this.pos) {
+ this.pos += str.length;
+ return true;
+ }
+ },
+ word: function(re) {
+ var word = "", ch, re = re || /[\w$]/;
+ while ((ch = this.spec.charAt(this.pos)) && re.test(ch)) { word += ch; ++this.pos; }
+ return word;
+ },
+ error: function() {
+ throw new Error("Unrecognized type spec: " + this.spec + " (at " + this.pos + ")");
+ },
+ parseFnType: function(comp, name, top, generator) {
+ var args = [], names = [], computed = false;
+ if (!this.eat(")")) for (var i = 0; ; ++i) {
+ var colon = this.spec.indexOf(": ", this.pos), argname;
+ if (colon != -1) {
+ argname = this.spec.slice(this.pos, colon);
+ if (/^[$\w?]+$/.test(argname))
+ this.pos = colon + 2;
+ else
+ argname = null;
+ }
+ names.push(argname);
+ var argType = this.parseType(comp);
+ if (argType.call) computed = true;
+ args.push(argType);
+ if (!this.eat(", ")) {
+ this.eat(")") || this.error();
+ break;
+ }
+ }
+ var retType, computeRet, computeRetStart, fn;
+ if (this.eat(" -> ")) {
+ var retStart = this.pos;
+ retType = this.parseType(true);
+ if (retType.call && !computed) {
+ computeRet = retType;
+ retType = infer.ANull;
+ computeRetStart = retStart;
+ }
+ } else {
+ retType = infer.ANull;
+ }
+ if (computed) return computedFunc(name, args, retType, generator);
+
+ if (top && (fn = this.base))
+ infer.Fn.call(this.base, name, infer.ANull, args, names, retType, generator);
+ else
+ fn = new infer.Fn(name, infer.ANull, args, names, retType, generator);
+ if (computeRet) fn.computeRet = computeRet;
+ if (computeRetStart != null) fn.computeRetSource = this.spec.slice(computeRetStart, this.pos);
+ return fn;
+ },
+ parseType: function(comp, name, top) {
+ var main = this.parseTypeMaybeProp(comp, name, top);
+ if (!this.eat("|")) return main;
+ var types = [main], computed = main.call;
+ for (;;) {
+ var next = this.parseTypeMaybeProp(comp, name, top);
+ types.push(next);
+ if (next.call) computed = true;
+ if (!this.eat("|")) break;
+ }
+ if (computed) return computedUnion(types);
+ var union = new infer.AVal;
+ for (var i = 0; i < types.length; i++) types[i].propagate(union);
+ union.maxWeight = 1e5;
+ return union;
+ },
+ parseTypeMaybeProp: function(comp, name, top) {
+ var result = this.parseTypeInner(comp, name, top);
+ while (comp && this.eat(".")) result = this.extendWithProp(result);
+ return result;
+ },
+ extendWithProp: function(base) {
+ var propName = this.word(/[\w<>$!:]/) || this.error();
+ if (base.apply) return function(self, args) {
+ return extractProp(base(self, args), propName);
+ };
+ return extractProp(base, propName);
+ },
+ parseTypeInner: function(comp, name, top) {
+ var gen
+ if (this.eat("fn(") || (gen = this.eat("fn*("))) {
+ return this.parseFnType(comp, name, top, gen);
+ } else if (this.eat("[")) {
+ var inner = this.parseType(comp), types, computed = inner.call
+ while (this.eat(", ")) {
+ if (!types) types = [inner]
+ var next = this.parseType(comp)
+ types.push(next)
+ computed = computed || next.call
+ }
+ this.eat("]") || this.error()
+ if (computed) return types ? computedTuple(types) : computedArray(inner)
+ if (top && this.base) {
+ infer.Arr.call(this.base, types || inner)
+ return this.base
+ }
+ return new infer.Arr(types || inner)
+ } else if (this.eat("+")) {
+ var path = this.word(/[\w$<>\.:!]/)
+ var base = infer.cx().localDefs[path + ".prototype"]
+ if (!base) {
+ var base = parsePath(path);
+ if (!(base instanceof infer.Obj)) return base;
+ var proto = descendProps(base, ["prototype"])
+ if (proto && (proto = proto.getObjType()))
+ base = proto
+ }
+ if (comp && this.eat("[")) return this.parsePoly(base);
+ if (top && this.forceNew) return new infer.Obj(base);
+ return infer.getInstance(base);
+ } else if (this.eat(":")) {
+ var name = this.word(/[\w$\.]/)
+ return infer.getSymbol(name)
+ } else if (comp && this.eat("!")) {
+ var arg = this.word(/\d/);
+ if (arg) {
+ arg = Number(arg);
+ return function(_self, args) {return args[arg] || infer.ANull;};
+ } else if (this.eat("this")) {
+ return function(self) {return self;};
+ } else if (this.eat("custom:")) {
+ var fname = this.word(/[\w$]/);
+ return customFunctions[fname] || function() { return infer.ANull; };
+ } else {
+ return this.fromWord("!" + this.word(/[\w$<>\.!:]/));
+ }
+ } else if (this.eat("?")) {
+ return infer.ANull;
+ } else {
+ return this.fromWord(this.word(/[\w$<>\.!:`]/));
+ }
+ },
+ fromWord: function(spec) {
+ var cx = infer.cx();
+ switch (spec) {
+ case "number": return cx.num;
+ case "string": return cx.str;
+ case "bool": return cx.bool;
+ case "<top>": return cx.topScope;
+ }
+ if (cx.localDefs && spec in cx.localDefs) return cx.localDefs[spec];
+ return parsePath(spec);
+ },
+ parsePoly: function(base) {
+ var propName = "<i>", match;
+ if (match = this.spec.slice(this.pos).match(/^\s*([\w$:]+)\s*=\s*/)) {
+ propName = match[1];
+ this.pos += match[0].length;
+ }
+ var value = this.parseType(true);
+ if (!this.eat("]")) this.error();
+ if (value.call) return function(self, args) {
+ var instance = new infer.Obj(base);
+ value(self, args).propagate(instance.defProp(propName));
+ return instance;
+ };
+ var instance = new infer.Obj(base);
+ value.propagate(instance.defProp(propName));
+ return instance;
+ }
+ };
+
+ function parseType(spec, name, base, forceNew) {
+ var type = new TypeParser(spec, null, base, forceNew).parseType(false, name, true);
+ if (/^fn\(/.test(spec)) for (var i = 0; i < type.args.length; ++i) (function(i) {
+ var arg = type.args[i];
+ if (arg instanceof infer.Fn && arg.args && arg.args.length) addEffect(type, function(_self, fArgs) {
+ var fArg = fArgs[i];
+ if (fArg) fArg.propagate(new infer.IsCallee(infer.cx().topScope, arg.args, null, infer.ANull));
+ });
+ })(i);
+ return type;
+ }
+
+ function addEffect(fn, handler, replaceRet) {
+ var oldCmp = fn.computeRet, rv = fn.retval;
+ fn.computeRet = function(self, args, argNodes) {
+ var handled = handler(self, args, argNodes);
+ var old = oldCmp ? oldCmp(self, args, argNodes) : rv;
+ return replaceRet ? handled : old;
+ };
+ }
+
+ var parseEffect = exports.parseEffect = function(effect, fn) {
+ var m;
+ if (effect.indexOf("propagate ") == 0) {
+ var p = new TypeParser(effect, 10);
+ var origin = p.parseType(true);
+ if (!p.eat(" ")) p.error();
+ var target = p.parseType(true);
+ addEffect(fn, function(self, args) {
+ unwrapType(origin, self, args).propagate(unwrapType(target, self, args));
+ });
+ } else if (effect.indexOf("call ") == 0) {
+ var andRet = effect.indexOf("and return ", 5) == 5;
+ var p = new TypeParser(effect, andRet ? 16 : 5);
+ var getCallee = p.parseType(true), getSelf = null, getArgs = [];
+ if (p.eat(" this=")) getSelf = p.parseType(true);
+ while (p.eat(" ")) getArgs.push(p.parseType(true));
+ addEffect(fn, function(self, args) {
+ var callee = unwrapType(getCallee, self, args);
+ var slf = getSelf ? unwrapType(getSelf, self, args) : infer.ANull, as = [];
+ for (var i = 0; i < getArgs.length; ++i) as.push(unwrapType(getArgs[i], self, args));
+ var result = andRet ? new infer.AVal : infer.ANull;
+ callee.propagate(new infer.IsCallee(slf, as, null, result));
+ return result;
+ }, andRet);
+ } else if (m = effect.match(/^custom (\S+)\s*(.*)/)) {
+ var customFunc = customFunctions[m[1]];
+ if (customFunc) addEffect(fn, m[2] ? customFunc(m[2]) : customFunc);
+ } else if (effect.indexOf("copy ") == 0) {
+ var p = new TypeParser(effect, 5);
+ var getFrom = p.parseType(true);
+ p.eat(" ");
+ var getTo = p.parseType(true);
+ addEffect(fn, function(self, args) {
+ var from = unwrapType(getFrom, self, args), to = unwrapType(getTo, self, args);
+ from.forAllProps(function(prop, val, local) {
+ if (local && prop != "<i>")
+ to.propagate(new infer.DefProp(prop, val));
+ });
+ });
+ } else {
+ throw new Error("Unknown effect type: " + effect);
+ }
+ };
+
+ var currentTopScope;
+
+ var parsePath = exports.parsePath = function(path, scope) {
+ var cx = infer.cx(), cached = cx.paths[path], origPath = path;
+ if (cached != null) return cached;
+ cx.paths[path] = infer.ANull;
+
+ var base = scope || currentTopScope || cx.topScope;
+
+ if (cx.localDefs) for (var name in cx.localDefs) {
+ if (path.indexOf(name) == 0) {
+ if (path == name) return cx.paths[path] = cx.localDefs[path];
+ if (path.charAt(name.length) == ".") {
+ base = cx.localDefs[name];
+ path = path.slice(name.length + 1);
+ break;
+ }
+ }
+ }
+
+ var result = descendProps(base, path.split("."))
+ // Uncomment this to get feedback on your poorly written .json files
+ // if (result == infer.ANull) console.error("bad path: " + origPath + " (" + cx.curOrigin + ")")
+ cx.paths[origPath] = result == infer.ANull ? null : result
+ return result
+ }
+
+ function descendProps(base, parts) {
+ for (var i = 0; i < parts.length && base != infer.ANull; ++i) {
+ var prop = parts[i];
+ if (prop.charAt(0) == "!") {
+ if (prop == "!proto") {
+ base = (base instanceof infer.Obj && base.proto) || infer.ANull;
+ } else {
+ var fn = base.getFunctionType();
+ if (!fn) {
+ base = infer.ANull;
+ } else if (prop == "!ret") {
+ base = fn.retval && fn.retval.getType(false) || infer.ANull;
+ } else {
+ var arg = fn.args && fn.args[Number(prop.slice(1))];
+ base = (arg && arg.getType(false)) || infer.ANull;
+ }
+ }
+ } else if (base instanceof infer.Obj) {
+ var propVal = (prop == "prototype" && base instanceof infer.Fn) ? base.getProp(prop) : base.props[prop];
+ if (!propVal || propVal.isEmpty())
+ base = infer.ANull;
+ else
+ base = propVal.types[0];
+ }
+ }
+ return base;
+ }
+
+ function emptyObj(ctor) {
+ var empty = Object.create(ctor.prototype);
+ empty.props = Object.create(null);
+ empty.isShell = true;
+ return empty;
+ }
+
+ function isSimpleAnnotation(spec) {
+ if (!spec["!type"] || /^(fn\(|\[)/.test(spec["!type"])) return false;
+ for (var prop in spec)
+ if (prop != "!type" && prop != "!doc" && prop != "!url" && prop != "!span" && prop != "!data")
+ return false;
+ return true;
+ }
+
+ function passOne(base, spec, path) {
+ if (!base) {
+ var tp = spec["!type"];
+ if (tp) {
+ if (/^fn\(/.test(tp)) base = emptyObj(infer.Fn);
+ else if (tp.charAt(0) == "[") base = emptyObj(infer.Arr);
+ else throw new Error("Invalid !type spec: " + tp);
+ } else if (spec["!stdProto"]) {
+ base = infer.cx().protos[spec["!stdProto"]];
+ } else {
+ base = emptyObj(infer.Obj);
+ }
+ base.name = path;
+ }
+
+ for (var name in spec) if (hop(spec, name) && name.charCodeAt(0) != 33) {
+ var inner = spec[name];
+ if (typeof inner == "string" || isSimpleAnnotation(inner)) continue;
+ var prop = base.defProp(name);
+ passOne(prop.getObjType(), inner, path ? path + "." + name : name).propagate(prop);
+ }
+ return base;
+ }
+
+ function passTwo(base, spec, path) {
+ if (base.isShell) {
+ delete base.isShell;
+ var tp = spec["!type"];
+ if (tp) {
+ parseType(tp, path, base);
+ } else {
+ var proto = spec["!proto"] && parseType(spec["!proto"]);
+ infer.Obj.call(base, proto instanceof infer.Obj ? proto : true, path);
+ }
+ }
+
+ var effects = spec["!effects"];
+ if (effects && base instanceof infer.Fn) for (var i = 0; i < effects.length; ++i)
+ parseEffect(effects[i], base);
+ copyInfo(spec, base);
+
+ for (var name in spec) if (hop(spec, name) && name.charCodeAt(0) != 33) {
+ var inner = spec[name], known = base.defProp(name), innerPath = path ? path + "." + name : name;
+ if (typeof inner == "string") {
+ if (known.isEmpty()) parseType(inner, innerPath).propagate(known);
+ } else {
+ if (!isSimpleAnnotation(inner))
+ passTwo(known.getObjType(), inner, innerPath);
+ else if (known.isEmpty())
+ parseType(inner["!type"], innerPath, null, true).propagate(known);
+ else
+ continue;
+ if (inner["!doc"]) known.doc = inner["!doc"];
+ if (inner["!url"]) known.url = inner["!url"];
+ if (inner["!span"]) known.span = inner["!span"];
+ }
+ }
+ return base;
+ }
+
+ function copyInfo(spec, type) {
+ if (spec["!doc"]) type.doc = spec["!doc"];
+ if (spec["!url"]) type.url = spec["!url"];
+ if (spec["!span"]) type.span = spec["!span"];
+ if (spec["!data"]) type.metaData = spec["!data"];
+ }
+
+ function doLoadEnvironment(data, scope) {
+ var cx = infer.cx(), server = cx.parent
+
+ infer.addOrigin(cx.curOrigin = data["!name"] || "env#" + cx.origins.length);
+ cx.localDefs = cx.definitions[cx.curOrigin] = Object.create(null);
+
+ if (server) server.signal("preLoadDef", data)
+
+ passOne(scope, data);
+
+ var def = data["!define"];
+ if (def) {
+ for (var name in def) {
+ var spec = def[name];
+ cx.localDefs[name] = typeof spec == "string" ? parsePath(spec) : passOne(null, spec, name);
+ }
+ for (var name in def) {
+ var spec = def[name];
+ if (typeof spec != "string") passTwo(cx.localDefs[name], def[name], name);
+ }
+ }
+
+ passTwo(scope, data);
+
+ if (server) server.signal("postLoadDef", data)
+
+ cx.curOrigin = cx.localDefs = null;
+ }
+
+ exports.load = function(data, scope) {
+ if (!scope) scope = infer.cx().topScope;
+ var oldScope = currentTopScope;
+ currentTopScope = scope;
+ try {
+ doLoadEnvironment(data, scope);
+ } finally {
+ currentTopScope = oldScope;
+ }
+ };
+
+ exports.parse = function(data, origin, path) {
+ var cx = infer.cx();
+ if (origin) {
+ cx.origin = origin;
+ cx.localDefs = cx.definitions[origin];
+ }
+
+ try {
+ if (typeof data == "string")
+ return parseType(data, path);
+ else
+ return passTwo(passOne(null, data, path), data, path);
+ } finally {
+ if (origin) cx.origin = cx.localDefs = null;
+ }
+ };
+
+ // Used to register custom logic for more involved effect or type
+ // computation.
+ var customFunctions = Object.create(null);
+ infer.registerFunction = function(name, f) { customFunctions[name] = f; };
+
+ var IsCreated = infer.constraint({
+ construct: function(created, target, spec) {
+ this.created = created;
+ this.target = target;
+ this.spec = spec;
+ },
+ addType: function(tp) {
+ if (tp instanceof infer.Obj && this.created++ < 5) {
+ var derived = new infer.Obj(tp), spec = this.spec;
+ if (spec instanceof infer.AVal) spec = spec.getObjType(false);
+ if (spec instanceof infer.Obj) for (var prop in spec.props) {
+ var cur = spec.props[prop].types[0];
+ var p = derived.defProp(prop);
+ if (cur && cur instanceof infer.Obj && cur.props.value) {
+ var vtp = cur.props.value.getType(false);
+ if (vtp) p.addType(vtp);
+ }
+ }
+ this.target.addType(derived);
+ }
+ }
+ });
+
+ infer.registerFunction("Object_create", function(_self, args, argNodes) {
+ if (argNodes && argNodes.length && argNodes[0].type == "Literal" && argNodes[0].value == null)
+ return new infer.Obj();
+
+ var result = new infer.AVal;
+ if (args[0]) args[0].propagate(new IsCreated(0, result, args[1]));
+ return result;
+ });
+
+ var PropSpec = infer.constraint({
+ construct: function(target) { this.target = target; },
+ addType: function(tp) {
+ if (!(tp instanceof infer.Obj)) return;
+ if (tp.hasProp("value"))
+ tp.getProp("value").propagate(this.target);
+ else if (tp.hasProp("get"))
+ tp.getProp("get").propagate(new infer.IsCallee(infer.ANull, [], null, this.target));
+ }
+ });
+
+ infer.registerFunction("Object_defineProperty", function(_self, args, argNodes) {
+ if (argNodes && argNodes.length >= 3 && argNodes[1].type == "Literal" &&
+ typeof argNodes[1].value == "string") {
+ var obj = args[0], connect = new infer.AVal;
+ obj.propagate(new infer.DefProp(argNodes[1].value, connect, argNodes[1]));
+ args[2].propagate(new PropSpec(connect));
+ }
+ return infer.ANull;
+ });
+
+ infer.registerFunction("Object_defineProperties", function(_self, args, argNodes) {
+ if (args.length >= 2) {
+ var obj = args[0];
+ args[1].forAllProps(function(prop, val, local) {
+ if (!local) return;
+ var connect = new infer.AVal;
+ obj.propagate(new infer.DefProp(prop, connect, argNodes && argNodes[1]));
+ val.propagate(new PropSpec(connect));
+ });
+ }
+ return infer.ANull;
+ });
+
+ var IsBound = infer.constraint({
+ construct: function(self, args, target) {
+ this.self = self; this.args = args; this.target = target;
+ },
+ addType: function(tp) {
+ if (!(tp instanceof infer.Fn)) return;
+ this.target.addType(new infer.Fn(tp.name, infer.ANull, tp.args.slice(this.args.length),
+ tp.argNames.slice(this.args.length), tp.retval, tp.generator));
+ this.self.propagate(tp.self);
+ for (var i = 0; i < Math.min(tp.args.length, this.args.length); ++i)
+ this.args[i].propagate(tp.args[i]);
+ }
+ });
+
+ infer.registerFunction("Function_bind", function(self, args) {
+ if (!args.length) return infer.ANull;
+ var result = new infer.AVal;
+ self.propagate(new IsBound(args[0], args.slice(1), result));
+ return result;
+ });
+
+ infer.registerFunction("Array_ctor", function(_self, args) {
+ var arr = new infer.Arr;
+ if (args.length != 1 || !args[0].hasType(infer.cx().num)) {
+ var content = arr.getProp("<i>");
+ for (var i = 0; i < args.length; ++i) args[i].propagate(content);
+ }
+ return arr;
+ });
+
+ infer.registerFunction("Promise_ctor", function(_self, args, argNodes) {
+ var defs6 = infer.cx().definitions.ecma6
+ if (!defs6 || args.length < 1) return infer.ANull;
+ var self = new infer.Obj(defs6["Promise.prototype"]);
+ var valProp = self.defProp(":t", argNodes && argNodes[0]);
+ var valArg = new infer.AVal;
+ valArg.propagate(valProp);
+ var exec = new infer.Fn("execute", infer.ANull, [valArg], ["value"], infer.ANull);
+ var reject = defs6.Promise_reject;
+ args[0].propagate(new infer.IsCallee(infer.ANull, [exec, reject], null, infer.ANull));
+ return self;
+ });
+
+ var PromiseResolvesTo = infer.constraint({
+ construct: function(output) { this.output = output; },
+ addType: function(tp) {
+ if (tp.constructor == infer.Obj && tp.name == "Promise" && tp.hasProp(":t"))
+ tp.getProp(":t").propagate(this.output);
+ else
+ tp.propagate(this.output);
+ }
+ });
+
+ var WG_PROMISE_KEEP_VALUE = 50;
+
+ infer.registerFunction("Promise_then", function(self, args, argNodes) {
+ var fn = args.length && args[0].getFunctionType();
+ var defs6 = infer.cx().definitions.ecma6
+ if (!fn || !defs6) return self;
+
+ var result = new infer.Obj(defs6["Promise.prototype"]);
+ var value = result.defProp(":t", argNodes && argNodes[0]), ty;
+ if (fn.retval.isEmpty() && (ty = self.getType()) instanceof infer.Obj && ty.hasProp(":t"))
+ ty.getProp(":t").propagate(value, WG_PROMISE_KEEP_VALUE);
+ fn.retval.propagate(new PromiseResolvesTo(value));
+ return result;
+ });
+
+ infer.registerFunction("getOwnPropertySymbols", function(_self, args) {
+ if (!args.length) return infer.ANull
+ var result = new infer.AVal
+ args[0].forAllProps(function(prop, _val, local) {
+ if (local && prop.charAt(0) == ":") result.addType(infer.getSymbol(prop.slice(1)))
+ })
+ return result
+ })
+
+ infer.registerFunction("getSymbol", function(_self, _args, argNodes) {
+ if (argNodes.length && argNodes[0].type == "Literal" && typeof argNodes[0].value == "string")
+ return infer.getSymbol(argNodes[0].value)
+ else
+ return infer.ANull
+ })
+
+ return exports;
+});
diff --git a/devtools/client/sourceeditor/tern/ecma5.js b/devtools/client/sourceeditor/tern/ecma5.js
new file mode 100644
index 000000000..634e414fd
--- /dev/null
+++ b/devtools/client/sourceeditor/tern/ecma5.js
@@ -0,0 +1,950 @@
+module.exports = {
+ "!name": "ecma5",
+ "!define": {"Error.prototype": "Error.prototype"},
+ "Infinity": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Infinity",
+ "!doc": "A numeric value representing infinity."
+ },
+ "undefined": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/undefined",
+ "!doc": "The value undefined."
+ },
+ "NaN": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/NaN",
+ "!doc": "A value representing Not-A-Number."
+ },
+ "Object": {
+ "!type": "fn()",
+ "getPrototypeOf": {
+ "!type": "fn(obj: ?) -> ?",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/getPrototypeOf",
+ "!doc": "Returns the prototype (i.e. the internal prototype) of the specified object."
+ },
+ "create": {
+ "!type": "fn(proto: ?) -> !custom:Object_create",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/create",
+ "!doc": "Creates a new object with the specified prototype object and properties."
+ },
+ "defineProperty": {
+ "!type": "fn(obj: ?, prop: string, desc: ?)",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/defineProperty",
+ "!doc": "Defines a new property directly on an object, or modifies an existing property on an object, and returns the object. If you want to see how to use the Object.defineProperty method with a binary-flags-like syntax, see this article."
+ },
+ "defineProperties": {
+ "!type": "fn(obj: ?, props: ?)",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/defineProperty",
+ "!doc": "Defines a new property directly on an object, or modifies an existing property on an object, and returns the object. If you want to see how to use the Object.defineProperty method with a binary-flags-like syntax, see this article."
+ },
+ "getOwnPropertyDescriptor": {
+ "!type": "fn(obj: ?, prop: string) -> ?",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor",
+ "!doc": "Returns a property descriptor for an own property (that is, one directly present on an object, not present by dint of being along an object's prototype chain) of a given object."
+ },
+ "keys": {
+ "!type": "fn(obj: ?) -> [string]",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/keys",
+ "!doc": "Returns an array of a given object's own enumerable properties, in the same order as that provided by a for-in loop (the difference being that a for-in loop enumerates properties in the prototype chain as well)."
+ },
+ "getOwnPropertyNames": {
+ "!type": "fn(obj: ?) -> [string]",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/getOwnPropertyNames",
+ "!doc": "Returns an array of all properties (enumerable or not) found directly upon a given object."
+ },
+ "seal": {
+ "!type": "fn(obj: ?)",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/seal",
+ "!doc": "Seals an object, preventing new properties from being added to it and marking all existing properties as non-configurable. Values of present properties can still be changed as long as they are writable."
+ },
+ "isSealed": {
+ "!type": "fn(obj: ?) -> bool",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/isSealed",
+ "!doc": "Determine if an object is sealed."
+ },
+ "freeze": {
+ "!type": "fn(obj: ?)",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/freeze",
+ "!doc": "Freezes an object: that is, prevents new properties from being added to it; prevents existing properties from being removed; and prevents existing properties, or their enumerability, configurability, or writability, from being changed. In essence the object is made effectively immutable. The method returns the object being frozen."
+ },
+ "isFrozen": {
+ "!type": "fn(obj: ?) -> bool",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/isFrozen",
+ "!doc": "Determine if an object is frozen."
+ },
+ "prototype": {
+ "!stdProto": "Object",
+ "toString": {
+ "!type": "fn() -> string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/toString",
+ "!doc": "Returns a string representing the object."
+ },
+ "toLocaleString": {
+ "!type": "fn() -> string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/toLocaleString",
+ "!doc": "Returns a string representing the object. This method is meant to be overriden by derived objects for locale-specific purposes."
+ },
+ "valueOf": {
+ "!type": "fn() -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/valueOf",
+ "!doc": "Returns the primitive value of the specified object"
+ },
+ "hasOwnProperty": {
+ "!type": "fn(prop: string) -> bool",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/hasOwnProperty",
+ "!doc": "Returns a boolean indicating whether the object has the specified property."
+ },
+ "propertyIsEnumerable": {
+ "!type": "fn(prop: string) -> bool",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/propertyIsEnumerable",
+ "!doc": "Returns a Boolean indicating whether the specified property is enumerable."
+ }
+ },
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object",
+ "!doc": "Creates an object wrapper."
+ },
+ "Function": {
+ "!type": "fn(body: string) -> fn()",
+ "prototype": {
+ "!stdProto": "Function",
+ "apply": {
+ "!type": "fn(this: ?, args: [?])",
+ "!effects": [
+ "call and return !this this=!0 !1.<i> !1.<i> !1.<i>"
+ ],
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Function/apply",
+ "!doc": "Calls a function with a given this value and arguments provided as an array (or an array like object)."
+ },
+ "call": {
+ "!type": "fn(this: ?, args?: ?) -> !this.!ret",
+ "!effects": [
+ "call and return !this this=!0 !1 !2 !3 !4"
+ ],
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Function/call",
+ "!doc": "Calls a function with a given this value and arguments provided individually."
+ },
+ "bind": {
+ "!type": "fn(this: ?, args?: ?) -> !custom:Function_bind",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Function/bind",
+ "!doc": "Creates a new function that, when called, has its this keyword set to the provided value, with a given sequence of arguments preceding any provided when the new function was called."
+ },
+ "prototype": "?"
+ },
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Function",
+ "!doc": "Every function in JavaScript is actually a Function object."
+ },
+ "Array": {
+ "!type": "fn(size: number) -> !custom:Array_ctor",
+ "isArray": {
+ "!type": "fn(value: ?) -> bool",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/isArray",
+ "!doc": "Returns true if an object is an array, false if it is not."
+ },
+ "prototype": {
+ "!stdProto": "Array",
+ "length": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/length",
+ "!doc": "An unsigned, 32-bit integer that specifies the number of elements in an array."
+ },
+ "concat": {
+ "!type": "fn(other: [?]) -> !this",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/concat",
+ "!doc": "Returns a new array comprised of this array joined with other array(s) and/or value(s)."
+ },
+ "join": {
+ "!type": "fn(separator?: string) -> string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/join",
+ "!doc": "Joins all elements of an array into a string."
+ },
+ "splice": {
+ "!type": "fn(pos: number, amount: number)",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/splice",
+ "!doc": "Changes the content of an array, adding new elements while removing old elements."
+ },
+ "pop": {
+ "!type": "fn() -> !this.<i>",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/pop",
+ "!doc": "Removes the last element from an array and returns that element."
+ },
+ "push": {
+ "!type": "fn(newelt: ?) -> number",
+ "!effects": [
+ "propagate !0 !this.<i>"
+ ],
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/push",
+ "!doc": "Mutates an array by appending the given elements and returning the new length of the array."
+ },
+ "shift": {
+ "!type": "fn() -> !this.<i>",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/shift",
+ "!doc": "Removes the first element from an array and returns that element. This method changes the length of the array."
+ },
+ "unshift": {
+ "!type": "fn(newelt: ?) -> number",
+ "!effects": [
+ "propagate !0 !this.<i>"
+ ],
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/unshift",
+ "!doc": "Adds one or more elements to the beginning of an array and returns the new length of the array."
+ },
+ "slice": {
+ "!type": "fn(from: number, to?: number) -> !this",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/slice",
+ "!doc": "Returns a shallow copy of a portion of an array."
+ },
+ "reverse": {
+ "!type": "fn()",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/reverse",
+ "!doc": "Reverses an array in place. The first array element becomes the last and the last becomes the first."
+ },
+ "sort": {
+ "!type": "fn(compare?: fn(a: ?, b: ?) -> number)",
+ "!effects": [
+ "call !0 !this.<i> !this.<i>"
+ ],
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/sort",
+ "!doc": "Sorts the elements of an array in place and returns the array."
+ },
+ "indexOf": {
+ "!type": "fn(elt: ?, from?: number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/indexOf",
+ "!doc": "Returns the first index at which a given element can be found in the array, or -1 if it is not present."
+ },
+ "lastIndexOf": {
+ "!type": "fn(elt: ?, from?: number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/lastIndexOf",
+ "!doc": "Returns the last index at which a given element can be found in the array, or -1 if it is not present. The array is searched backwards, starting at fromIndex."
+ },
+ "every": {
+ "!type": "fn(test: fn(elt: ?, i: number) -> bool, context?: ?) -> bool",
+ "!effects": [
+ "call !0 this=!1 !this.<i> number"
+ ],
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/every",
+ "!doc": "Tests whether all elements in the array pass the test implemented by the provided function."
+ },
+ "some": {
+ "!type": "fn(test: fn(elt: ?, i: number) -> bool, context?: ?) -> bool",
+ "!effects": [
+ "call !0 this=!1 !this.<i> number"
+ ],
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/some",
+ "!doc": "Tests whether some element in the array passes the test implemented by the provided function."
+ },
+ "filter": {
+ "!type": "fn(test: fn(elt: ?, i: number) -> bool, context?: ?) -> !this",
+ "!effects": [
+ "call !0 this=!1 !this.<i> number"
+ ],
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/filter",
+ "!doc": "Creates a new array with all elements that pass the test implemented by the provided function."
+ },
+ "forEach": {
+ "!type": "fn(f: fn(elt: ?, i: number), context?: ?)",
+ "!effects": [
+ "call !0 this=!1 !this.<i> number"
+ ],
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/forEach",
+ "!doc": "Executes a provided function once per array element."
+ },
+ "map": {
+ "!type": "fn(f: fn(elt: ?, i: number) -> ?, context?: ?) -> [!0.!ret]",
+ "!effects": [
+ "call !0 this=!1 !this.<i> number"
+ ],
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/map",
+ "!doc": "Creates a new array with the results of calling a provided function on every element in this array."
+ },
+ "reduce": {
+ "!type": "fn(combine: fn(sum: ?, elt: ?, i: number) -> ?, init?: ?) -> !0.!ret",
+ "!effects": [
+ "call !0 !1 !this.<i> number"
+ ],
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/Reduce",
+ "!doc": "Apply a function against an accumulator and each value of the array (from left-to-right) as to reduce it to a single value."
+ },
+ "reduceRight": {
+ "!type": "fn(combine: fn(sum: ?, elt: ?, i: number) -> ?, init?: ?) -> !0.!ret",
+ "!effects": [
+ "call !0 !1 !this.<i> number"
+ ],
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/ReduceRight",
+ "!doc": "Apply a function simultaneously against two values of the array (from right-to-left) as to reduce it to a single value."
+ }
+ },
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array",
+ "!doc": "The JavaScript Array global object is a constructor for arrays, which are high-level, list-like objects."
+ },
+ "String": {
+ "!type": "fn(value: ?) -> string",
+ "fromCharCode": {
+ "!type": "fn(code: number) -> string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/String/fromCharCode",
+ "!doc": "Returns a string created by using the specified sequence of Unicode values."
+ },
+ "prototype": {
+ "!stdProto": "String",
+ "length": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/JavaScript/Reference/Global_Objects/String/length",
+ "!doc": "Represents the length of a string."
+ },
+ "<i>": "string",
+ "charAt": {
+ "!type": "fn(i: number) -> string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/String/charAt",
+ "!doc": "Returns the specified character from a string."
+ },
+ "charCodeAt": {
+ "!type": "fn(i: number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/String/charCodeAt",
+ "!doc": "Returns the numeric Unicode value of the character at the given index (except for unicode codepoints > 0x10000)."
+ },
+ "indexOf": {
+ "!type": "fn(char: string, from?: number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/String/indexOf",
+ "!doc": "Returns the index within the calling String object of the first occurrence of the specified value, starting the search at fromIndex,\nreturns -1 if the value is not found."
+ },
+ "lastIndexOf": {
+ "!type": "fn(char: string, from?: number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/String/lastIndexOf",
+ "!doc": "Returns the index within the calling String object of the last occurrence of the specified value, or -1 if not found. The calling string is searched backward, starting at fromIndex."
+ },
+ "substring": {
+ "!type": "fn(from: number, to?: number) -> string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/String/substring",
+ "!doc": "Returns a subset of a string between one index and another, or through the end of the string."
+ },
+ "substr": {
+ "!type": "fn(from: number, length?: number) -> string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/String/substr",
+ "!doc": "Returns the characters in a string beginning at the specified location through the specified number of characters."
+ },
+ "slice": {
+ "!type": "fn(from: number, to?: number) -> string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/String/slice",
+ "!doc": "Extracts a section of a string and returns a new string."
+ },
+ "trim": {
+ "!type": "fn() -> string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/String/Trim",
+ "!doc": "Removes whitespace from both ends of the string."
+ },
+ "trimLeft": {
+ "!type": "fn() -> string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/String/TrimLeft",
+ "!doc": "Removes whitespace from the left end of the string."
+ },
+ "trimRight": {
+ "!type": "fn() -> string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/String/TrimRight",
+ "!doc": "Removes whitespace from the right end of the string."
+ },
+ "toUpperCase": {
+ "!type": "fn() -> string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/String/toUpperCase",
+ "!doc": "Returns the calling string value converted to uppercase."
+ },
+ "toLowerCase": {
+ "!type": "fn() -> string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/String/toLowerCase",
+ "!doc": "Returns the calling string value converted to lowercase."
+ },
+ "toLocaleUpperCase": {
+ "!type": "fn() -> string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/String/toLocaleUpperCase",
+ "!doc": "Returns the calling string value converted to upper case, according to any locale-specific case mappings."
+ },
+ "toLocaleLowerCase": {
+ "!type": "fn() -> string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/String/toLocaleLowerCase",
+ "!doc": "Returns the calling string value converted to lower case, according to any locale-specific case mappings."
+ },
+ "split": {
+ "!type": "fn(pattern: string) -> [string]",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/String/split",
+ "!doc": "Splits a String object into an array of strings by separating the string into substrings."
+ },
+ "concat": {
+ "!type": "fn(other: string) -> string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/String/concat",
+ "!doc": "Combines the text of two or more strings and returns a new string."
+ },
+ "localeCompare": {
+ "!type": "fn(other: string) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/String/localeCompare",
+ "!doc": "Returns a number indicating whether a reference string comes before or after or is the same as the given string in sort order."
+ },
+ "match": {
+ "!type": "fn(pattern: +RegExp) -> [string]",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/String/match",
+ "!doc": "Used to retrieve the matches when matching a string against a regular expression."
+ },
+ "replace": {
+ "!type": "fn(pattern: +RegExp, replacement: string) -> string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/String/replace",
+ "!doc": "Returns a new string with some or all matches of a pattern replaced by a replacement. The pattern can be a string or a RegExp, and the replacement can be a string or a function to be called for each match."
+ },
+ "search": {
+ "!type": "fn(pattern: +RegExp) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/String/search",
+ "!doc": "Executes the search for a match between a regular expression and this String object."
+ }
+ },
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/String",
+ "!doc": "The String global object is a constructor for strings, or a sequence of characters."
+ },
+ "Number": {
+ "!type": "fn(value: ?) -> number",
+ "MAX_VALUE": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Number/MAX_VALUE",
+ "!doc": "The maximum numeric value representable in JavaScript."
+ },
+ "MIN_VALUE": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Number/MIN_VALUE",
+ "!doc": "The smallest positive numeric value representable in JavaScript."
+ },
+ "POSITIVE_INFINITY": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Number/POSITIVE_INFINITY",
+ "!doc": "A value representing the positive Infinity value."
+ },
+ "NEGATIVE_INFINITY": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Number/NEGATIVE_INFINITY",
+ "!doc": "A value representing the negative Infinity value."
+ },
+ "prototype": {
+ "!stdProto": "Number",
+ "toString": {
+ "!type": "fn(radix?: number) -> string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Number/toString",
+ "!doc": "Returns a string representing the specified Number object"
+ },
+ "toFixed": {
+ "!type": "fn(digits: number) -> string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Number/toFixed",
+ "!doc": "Formats a number using fixed-point notation"
+ },
+ "toExponential": {
+ "!type": "fn(digits: number) -> string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Number/toExponential",
+ "!doc": "Returns a string representing the Number object in exponential notation"
+ }
+ },
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Number",
+ "!doc": "The Number JavaScript object is a wrapper object allowing you to work with numerical values. A Number object is created using the Number() constructor."
+ },
+ "Boolean": {
+ "!type": "fn(value: ?) -> bool",
+ "prototype": {
+ "!stdProto": "Boolean"
+ },
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Boolean",
+ "!doc": "The Boolean object is an object wrapper for a boolean value."
+ },
+ "RegExp": {
+ "!type": "fn(source: string, flags?: string)",
+ "prototype": {
+ "!stdProto": "RegExp",
+ "exec": {
+ "!type": "fn(input: string) -> [string]",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/RegExp/exec",
+ "!doc": "Executes a search for a match in a specified string. Returns a result array, or null."
+ },
+ "compile": {
+ "!type": "fn(source: string, flags?: string)",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/RegExp",
+ "!doc": "Creates a regular expression object for matching text with a pattern."
+ },
+ "test": {
+ "!type": "fn(input: string) -> bool",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/RegExp/test",
+ "!doc": "Executes the search for a match between a regular expression and a specified string. Returns true or false."
+ },
+ "global": {
+ "!type": "bool",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/RegExp",
+ "!doc": "Creates a regular expression object for matching text with a pattern."
+ },
+ "ignoreCase": {
+ "!type": "bool",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/RegExp",
+ "!doc": "Creates a regular expression object for matching text with a pattern."
+ },
+ "multiline": {
+ "!type": "bool",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/RegExp/multiline",
+ "!doc": "Reflects whether or not to search in strings across multiple lines.\n"
+ },
+ "source": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/RegExp/source",
+ "!doc": "A read-only property that contains the text of the pattern, excluding the forward slashes.\n"
+ },
+ "lastIndex": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/RegExp/lastIndex",
+ "!doc": "A read/write integer property that specifies the index at which to start the next match."
+ }
+ },
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/RegExp",
+ "!doc": "Creates a regular expression object for matching text with a pattern."
+ },
+ "Date": {
+ "!type": "fn(ms: number)",
+ "parse": {
+ "!type": "fn(source: string) -> +Date",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/parse",
+ "!doc": "Parses a string representation of a date, and returns the number of milliseconds since January 1, 1970, 00:00:00 UTC."
+ },
+ "UTC": {
+ "!type": "fn(year: number, month: number, date: number, hour?: number, min?: number, sec?: number, ms?: number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/UTC",
+ "!doc": "Accepts the same parameters as the longest form of the constructor, and returns the number of milliseconds in a Date object since January 1, 1970, 00:00:00, universal time."
+ },
+ "now": {
+ "!type": "fn() -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/now",
+ "!doc": "Returns the number of milliseconds elapsed since 1 January 1970 00:00:00 UTC."
+ },
+ "prototype": {
+ "toUTCString": {
+ "!type": "fn() -> string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/toUTCString",
+ "!doc": "Converts a date to a string, using the universal time convention."
+ },
+ "toISOString": {
+ "!type": "fn() -> string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/toISOString",
+ "!doc": "JavaScript provides a direct way to convert a date object into a string in ISO format, the ISO 8601 Extended Format."
+ },
+ "toDateString": {
+ "!type": "fn() -> string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/toDateString",
+ "!doc": "Returns the date portion of a Date object in human readable form in American English."
+ },
+ "toTimeString": {
+ "!type": "fn() -> string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/toTimeString",
+ "!doc": "Returns the time portion of a Date object in human readable form in American English."
+ },
+ "toLocaleDateString": {
+ "!type": "fn() -> string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/toLocaleDateString",
+ "!doc": "Converts a date to a string, returning the \"date\" portion using the operating system's locale's conventions.\n"
+ },
+ "toLocaleTimeString": {
+ "!type": "fn() -> string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/toLocaleTimeString",
+ "!doc": "Converts a date to a string, returning the \"time\" portion using the current locale's conventions."
+ },
+ "getTime": {
+ "!type": "fn() -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/getTime",
+ "!doc": "Returns the numeric value corresponding to the time for the specified date according to universal time."
+ },
+ "getFullYear": {
+ "!type": "fn() -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/getFullYear",
+ "!doc": "Returns the year of the specified date according to local time."
+ },
+ "getYear": {
+ "!type": "fn() -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/getYear",
+ "!doc": "Returns the year in the specified date according to local time."
+ },
+ "getMonth": {
+ "!type": "fn() -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/getMonth",
+ "!doc": "Returns the month in the specified date according to local time."
+ },
+ "getUTCMonth": {
+ "!type": "fn() -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/getUTCMonth",
+ "!doc": "Returns the month of the specified date according to universal time.\n"
+ },
+ "getDate": {
+ "!type": "fn() -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/getDate",
+ "!doc": "Returns the day of the month for the specified date according to local time."
+ },
+ "getUTCDate": {
+ "!type": "fn() -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/getUTCDate",
+ "!doc": "Returns the day (date) of the month in the specified date according to universal time.\n"
+ },
+ "getDay": {
+ "!type": "fn() -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/getDay",
+ "!doc": "Returns the day of the week for the specified date according to local time."
+ },
+ "getUTCDay": {
+ "!type": "fn() -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/getUTCDay",
+ "!doc": "Returns the day of the week in the specified date according to universal time.\n"
+ },
+ "getHours": {
+ "!type": "fn() -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/getHours",
+ "!doc": "Returns the hour for the specified date according to local time."
+ },
+ "getUTCHours": {
+ "!type": "fn() -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/getUTCHours",
+ "!doc": "Returns the hours in the specified date according to universal time.\n"
+ },
+ "getMinutes": {
+ "!type": "fn() -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/getMinutes",
+ "!doc": "Returns the minutes in the specified date according to local time."
+ },
+ "getUTCMinutes": {
+ "!type": "fn() -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date",
+ "!doc": "Creates JavaScript Date instances which let you work with dates and times."
+ },
+ "getSeconds": {
+ "!type": "fn() -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/getSeconds",
+ "!doc": "Returns the seconds in the specified date according to local time."
+ },
+ "getUTCSeconds": {
+ "!type": "fn() -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/getUTCSeconds",
+ "!doc": "Returns the seconds in the specified date according to universal time.\n"
+ },
+ "getMilliseconds": {
+ "!type": "fn() -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/getMilliseconds",
+ "!doc": "Returns the milliseconds in the specified date according to local time."
+ },
+ "getUTCMilliseconds": {
+ "!type": "fn() -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/getUTCMilliseconds",
+ "!doc": "Returns the milliseconds in the specified date according to universal time.\n"
+ },
+ "getTimezoneOffset": {
+ "!type": "fn() -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/getTimezoneOffset",
+ "!doc": "Returns the time-zone offset from UTC, in minutes, for the current locale."
+ },
+ "setTime": {
+ "!type": "fn(date: +Date) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/setTime",
+ "!doc": "Sets the Date object to the time represented by a number of milliseconds since January 1, 1970, 00:00:00 UTC.\n"
+ },
+ "setFullYear": {
+ "!type": "fn(year: number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/setFullYear",
+ "!doc": "Sets the full year for a specified date according to local time.\n"
+ },
+ "setUTCFullYear": {
+ "!type": "fn(year: number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/setUTCFullYear",
+ "!doc": "Sets the full year for a specified date according to universal time.\n"
+ },
+ "setMonth": {
+ "!type": "fn(month: number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/setMonth",
+ "!doc": "Set the month for a specified date according to local time."
+ },
+ "setUTCMonth": {
+ "!type": "fn(month: number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/setUTCMonth",
+ "!doc": "Sets the month for a specified date according to universal time.\n"
+ },
+ "setDate": {
+ "!type": "fn(day: number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/setDate",
+ "!doc": "Sets the day of the month for a specified date according to local time."
+ },
+ "setUTCDate": {
+ "!type": "fn(day: number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/setUTCDate",
+ "!doc": "Sets the day of the month for a specified date according to universal time.\n"
+ },
+ "setHours": {
+ "!type": "fn(hour: number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/setHours",
+ "!doc": "Sets the hours for a specified date according to local time, and returns the number of milliseconds since 1 January 1970 00:00:00 UTC until the time represented by the updated Date instance."
+ },
+ "setUTCHours": {
+ "!type": "fn(hour: number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/setUTCHours",
+ "!doc": "Sets the hour for a specified date according to universal time.\n"
+ },
+ "setMinutes": {
+ "!type": "fn(min: number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/setMinutes",
+ "!doc": "Sets the minutes for a specified date according to local time."
+ },
+ "setUTCMinutes": {
+ "!type": "fn(min: number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/setUTCMinutes",
+ "!doc": "Sets the minutes for a specified date according to universal time.\n"
+ },
+ "setSeconds": {
+ "!type": "fn(sec: number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/setSeconds",
+ "!doc": "Sets the seconds for a specified date according to local time."
+ },
+ "setUTCSeconds": {
+ "!type": "fn(sec: number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/setUTCSeconds",
+ "!doc": "Sets the seconds for a specified date according to universal time.\n"
+ },
+ "setMilliseconds": {
+ "!type": "fn(ms: number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/setMilliseconds",
+ "!doc": "Sets the milliseconds for a specified date according to local time.\n"
+ },
+ "setUTCMilliseconds": {
+ "!type": "fn(ms: number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/setUTCMilliseconds",
+ "!doc": "Sets the milliseconds for a specified date according to universal time.\n"
+ }
+ },
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date",
+ "!doc": "Creates JavaScript Date instances which let you work with dates and times."
+ },
+ "Error": {
+ "!type": "fn(message: string)",
+ "prototype": {
+ "name": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Error/name",
+ "!doc": "A name for the type of error."
+ },
+ "message": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Error/message",
+ "!doc": "A human-readable description of the error."
+ }
+ },
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Error",
+ "!doc": "Creates an error object."
+ },
+ "SyntaxError": {
+ "!type": "fn(message: string)",
+ "prototype": "Error.prototype",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/SyntaxError",
+ "!doc": "Represents an error when trying to interpret syntactically invalid code."
+ },
+ "ReferenceError": {
+ "!type": "fn(message: string)",
+ "prototype": "Error.prototype",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/ReferenceError",
+ "!doc": "Represents an error when a non-existent variable is referenced."
+ },
+ "URIError": {
+ "!type": "fn(message: string)",
+ "prototype": "Error.prototype",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/URIError",
+ "!doc": "Represents an error when a malformed URI is encountered."
+ },
+ "EvalError": {
+ "!type": "fn(message: string)",
+ "prototype": "Error.prototype",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/EvalError",
+ "!doc": "Represents an error regarding the eval function."
+ },
+ "RangeError": {
+ "!type": "fn(message: string)",
+ "prototype": "Error.prototype",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/RangeError",
+ "!doc": "Represents an error when a number is not within the correct range allowed."
+ },
+ "parseInt": {
+ "!type": "fn(string: string, radix?: number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/parseInt",
+ "!doc": "Parses a string argument and returns an integer of the specified radix or base."
+ },
+ "parseFloat": {
+ "!type": "fn(string: string) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/parseFloat",
+ "!doc": "Parses a string argument and returns a floating point number."
+ },
+ "isNaN": {
+ "!type": "fn(value: number) -> bool",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/isNaN",
+ "!doc": "Determines whether a value is NaN or not. Be careful, this function is broken. You may be interested in ECMAScript 6 Number.isNaN."
+ },
+ "eval": {
+ "!type": "fn(code: string) -> ?",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/eval",
+ "!doc": "Evaluates JavaScript code represented as a string."
+ },
+ "encodeURI": {
+ "!type": "fn(uri: string) -> string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/encodeURI",
+ "!doc": "Encodes a Uniform Resource Identifier (URI) by replacing each instance of certain characters by one, two, three, or four escape sequences representing the UTF-8 encoding of the character (will only be four escape sequences for characters composed of two \"surrogate\" characters)."
+ },
+ "encodeURIComponent": {
+ "!type": "fn(uri: string) -> string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/encodeURIComponent",
+ "!doc": "Encodes a Uniform Resource Identifier (URI) component by replacing each instance of certain characters by one, two, three, or four escape sequences representing the UTF-8 encoding of the character (will only be four escape sequences for characters composed of two \"surrogate\" characters)."
+ },
+ "decodeURI": {
+ "!type": "fn(uri: string) -> string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/decodeURI",
+ "!doc": "Decodes a Uniform Resource Identifier (URI) previously created by encodeURI or by a similar routine."
+ },
+ "decodeURIComponent": {
+ "!type": "fn(uri: string) -> string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/decodeURIComponent",
+ "!doc": "Decodes a Uniform Resource Identifier (URI) component previously created by encodeURIComponent or by a similar routine."
+ },
+ "Math": {
+ "E": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math/E",
+ "!doc": "The base of natural logarithms, e, approximately 2.718."
+ },
+ "LN2": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math/LN2",
+ "!doc": "The natural logarithm of 2, approximately 0.693."
+ },
+ "LN10": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math/LN10",
+ "!doc": "The natural logarithm of 10, approximately 2.302."
+ },
+ "LOG2E": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math/LOG2E",
+ "!doc": "The base 2 logarithm of E (approximately 1.442)."
+ },
+ "LOG10E": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math/LOG10E",
+ "!doc": "The base 10 logarithm of E (approximately 0.434)."
+ },
+ "SQRT1_2": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math/SQRT1_2",
+ "!doc": "The square root of 1/2; equivalently, 1 over the square root of 2, approximately 0.707."
+ },
+ "SQRT2": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math/SQRT2",
+ "!doc": "The square root of 2, approximately 1.414."
+ },
+ "PI": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math/PI",
+ "!doc": "The ratio of the circumference of a circle to its diameter, approximately 3.14159."
+ },
+ "abs": {
+ "!type": "fn(number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math/abs",
+ "!doc": "Returns the absolute value of a number."
+ },
+ "cos": {
+ "!type": "fn(number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math/cos",
+ "!doc": "Returns the cosine of a number."
+ },
+ "sin": {
+ "!type": "fn(number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math/sin",
+ "!doc": "Returns the sine of a number."
+ },
+ "tan": {
+ "!type": "fn(number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math/tan",
+ "!doc": "Returns the tangent of a number."
+ },
+ "acos": {
+ "!type": "fn(number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math/acos",
+ "!doc": "Returns the arccosine (in radians) of a number."
+ },
+ "asin": {
+ "!type": "fn(number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math/asin",
+ "!doc": "Returns the arcsine (in radians) of a number."
+ },
+ "atan": {
+ "!type": "fn(number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math/atan",
+ "!doc": "Returns the arctangent (in radians) of a number."
+ },
+ "atan2": {
+ "!type": "fn(number, number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math/atan2",
+ "!doc": "Returns the arctangent of the quotient of its arguments."
+ },
+ "ceil": {
+ "!type": "fn(number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math/ceil",
+ "!doc": "Returns the smallest integer greater than or equal to a number."
+ },
+ "floor": {
+ "!type": "fn(number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math/floor",
+ "!doc": "Returns the largest integer less than or equal to a number."
+ },
+ "round": {
+ "!type": "fn(number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math/round",
+ "!doc": "Returns the value of a number rounded to the nearest integer."
+ },
+ "exp": {
+ "!type": "fn(number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math/exp",
+ "!doc": "Returns Ex, where x is the argument, and E is Euler's constant, the base of the natural logarithms."
+ },
+ "log": {
+ "!type": "fn(number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math/log",
+ "!doc": "Returns the natural logarithm (base E) of a number."
+ },
+ "sqrt": {
+ "!type": "fn(number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math/sqrt",
+ "!doc": "Returns the square root of a number."
+ },
+ "pow": {
+ "!type": "fn(number, number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math/pow",
+ "!doc": "Returns base to the exponent power, that is, baseexponent."
+ },
+ "max": {
+ "!type": "fn(number, number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math/max",
+ "!doc": "Returns the largest of zero or more numbers."
+ },
+ "min": {
+ "!type": "fn(number, number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math/min",
+ "!doc": "Returns the smallest of zero or more numbers."
+ },
+ "random": {
+ "!type": "fn() -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math/random",
+ "!doc": "Returns a floating-point, pseudo-random number in the range [0, 1) that is, from 0 (inclusive) up to but not including 1 (exclusive), which you can then scale to your desired range."
+ },
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math",
+ "!doc": "A built-in object that has properties and methods for mathematical constants and functions."
+ },
+ "JSON": {
+ "parse": {
+ "!type": "fn(json: string) -> ?",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/JSON/parse",
+ "!doc": "Parse a string as JSON, optionally transforming the value produced by parsing."
+ },
+ "stringify": {
+ "!type": "fn(value: ?) -> string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/JSON/stringify",
+ "!doc": "Convert a value to JSON, optionally replacing values if a replacer function is specified, or optionally including only the specified properties if a replacer array is specified."
+ },
+ "!url": "https://developer.mozilla.org/en-US/docs/JSON",
+ "!doc": "JSON (JavaScript Object Notation) is a data-interchange format. It closely resembles a subset of JavaScript syntax, although it is not a strict subset. (See JSON in the JavaScript Reference for full details.) It is useful when writing any kind of JavaScript-based application, including websites and browser extensions. For example, you might store user information in JSON format in a cookie, or you might store extension preferences in JSON in a string-valued browser preference."
+ }
+}
diff --git a/devtools/client/sourceeditor/tern/infer.js b/devtools/client/sourceeditor/tern/infer.js
new file mode 100755
index 000000000..94f0a9518
--- /dev/null
+++ b/devtools/client/sourceeditor/tern/infer.js
@@ -0,0 +1,2119 @@
+// Main type inference engine
+
+// Walks an AST, building up a graph of abstract values and constraints
+// that cause types to flow from one node to another. Also defines a
+// number of utilities for accessing ASTs and scopes.
+
+// Analysis is done in a context, which is tracked by the dynamically
+// bound cx variable. Use withContext to set the current context.
+
+// For memory-saving reasons, individual types export an interface
+// similar to abstract values (which can hold multiple types), and can
+// thus be used in place abstract values that only ever contain a
+// single type.
+
+(function(root, mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ return mod(exports, require("acorn/acorn"), require("acorn/acorn_loose"), require("acorn/walk"),
+ require("./def"), require("./signal"));
+ if (typeof define == "function" && define.amd) // AMD
+ return define(["exports", "acorn/acorn", "acorn/acorn_loose", "acorn/walk", "./def", "./signal"], mod);
+ mod(root.tern || (root.tern = {}), acorn, acorn, acorn.walk, tern.def, tern.signal); // Plain browser env
+})(this, function(exports, acorn, acorn_loose, walk, def, signal) {
+ "use strict";
+
+ var toString = exports.toString = function(type, maxDepth, parent) {
+ if (!type || type == parent || maxDepth && maxDepth < -3) return "?";
+ return type.toString(maxDepth, parent);
+ };
+
+ // A variant of AVal used for unknown, dead-end values. Also serves
+ // as prototype for AVals, Types, and Constraints because it
+ // implements 'empty' versions of all the methods that the code
+ // expects.
+ var ANull = exports.ANull = signal.mixin({
+ addType: function() {},
+ propagate: function() {},
+ getProp: function() { return ANull; },
+ forAllProps: function() {},
+ hasType: function() { return false; },
+ isEmpty: function() { return true; },
+ getFunctionType: function() {},
+ getObjType: function() {},
+ getSymbolType: function() {},
+ getType: function() {},
+ gatherProperties: function() {},
+ propagatesTo: function() {},
+ typeHint: function() {},
+ propHint: function() {},
+ toString: function() { return "?"; }
+ });
+
+ function extend(proto, props) {
+ var obj = Object.create(proto);
+ if (props) for (var prop in props) obj[prop] = props[prop];
+ return obj;
+ }
+
+ // ABSTRACT VALUES
+
+ var WG_DEFAULT = 100, WG_NEW_INSTANCE = 90, WG_MADEUP_PROTO = 10,
+ WG_MULTI_MEMBER = 6, WG_CATCH_ERROR = 6,
+ WG_PHANTOM_OBJ = 1,
+ WG_GLOBAL_THIS = 90, WG_SPECULATIVE_THIS = 2, WG_SPECULATIVE_PROTO_THIS = 4;
+
+ var AVal = exports.AVal = function() {
+ this.types = [];
+ this.forward = null;
+ this.maxWeight = 0;
+ };
+ AVal.prototype = extend(ANull, {
+ addType: function(type, weight) {
+ weight = weight || WG_DEFAULT;
+ if (this.maxWeight < weight) {
+ this.maxWeight = weight;
+ if (this.types.length == 1 && this.types[0] == type) return;
+ this.types.length = 0;
+ } else if (this.maxWeight > weight || this.types.indexOf(type) > -1) {
+ return;
+ }
+
+ this.signal("addType", type);
+ this.types.push(type);
+ var forward = this.forward;
+ if (forward) withWorklist(function(add) {
+ for (var i = 0; i < forward.length; ++i) add(type, forward[i], weight);
+ });
+ },
+
+ propagate: function(target, weight) {
+ if (target == ANull || (target instanceof Type && this.forward && this.forward.length > 2)) return;
+ if (weight && weight != WG_DEFAULT) target = new Muffle(target, weight);
+ (this.forward || (this.forward = [])).push(target);
+ var types = this.types;
+ if (types.length) withWorklist(function(add) {
+ for (var i = 0; i < types.length; ++i) add(types[i], target, weight);
+ });
+ },
+
+ getProp: function(prop) {
+ if (prop == "__proto__" || prop == "✖") return ANull;
+ var found = (this.props || (this.props = Object.create(null)))[prop];
+ if (!found) {
+ found = this.props[prop] = new AVal;
+ this.propagate(new GetProp(prop, found));
+ }
+ return found;
+ },
+
+ forAllProps: function(c) {
+ this.propagate(new ForAllProps(c));
+ },
+
+ hasType: function(type) {
+ return this.types.indexOf(type) > -1;
+ },
+ isEmpty: function() { return this.types.length === 0; },
+ getFunctionType: function() {
+ for (var i = this.types.length - 1; i >= 0; --i)
+ if (this.types[i] instanceof Fn) return this.types[i];
+ },
+ getObjType: function() {
+ var seen = null;
+ for (var i = this.types.length - 1; i >= 0; --i) {
+ var type = this.types[i];
+ if (!(type instanceof Obj)) continue;
+ if (type.name) return type;
+ if (!seen) seen = type;
+ }
+ return seen;
+ },
+
+ getSymbolType: function() {
+ for (var i = this.types.length - 1; i >= 0; --i)
+ if (this.types[i] instanceof Sym) return this.types[i]
+ },
+
+ getType: function(guess) {
+ if (this.types.length === 0 && guess !== false) return this.makeupType();
+ if (this.types.length === 1) return this.types[0];
+ return canonicalType(this.types);
+ },
+
+ toString: function(maxDepth, parent) {
+ if (this.types.length == 0) return toString(this.makeupType(), maxDepth, parent);
+ if (this.types.length == 1) return toString(this.types[0], maxDepth, parent);
+ var simplified = simplifyTypes(this.types);
+ if (simplified.length > 2) return "?";
+ return simplified.map(function(tp) { return toString(tp, maxDepth, parent); }).join("|");
+ },
+
+ makeupPropType: function(obj) {
+ var propName = this.propertyName;
+
+ var protoProp = obj.proto && obj.proto.hasProp(propName);
+ if (protoProp) {
+ var fromProto = protoProp.getType();
+ if (fromProto) return fromProto;
+ }
+
+ if (propName != "<i>") {
+ var computedProp = obj.hasProp("<i>");
+ if (computedProp) return computedProp.getType();
+ } else if (obj.props["<i>"] != this) {
+ for (var prop in obj.props) {
+ var val = obj.props[prop];
+ if (!val.isEmpty()) return val.getType();
+ }
+ }
+ },
+
+ makeupType: function() {
+ var computed = this.propertyOf && this.makeupPropType(this.propertyOf);
+ if (computed) return computed;
+
+ if (!this.forward) return null;
+ for (var i = this.forward.length - 1; i >= 0; --i) {
+ var hint = this.forward[i].typeHint();
+ if (hint && !hint.isEmpty()) {guessing = true; return hint;}
+ }
+
+ var props = Object.create(null), foundProp = null;
+ for (var i = 0; i < this.forward.length; ++i) {
+ var prop = this.forward[i].propHint();
+ if (prop && prop != "length" && prop != "<i>" && prop != "✖" && prop != cx.completingProperty) {
+ props[prop] = true;
+ foundProp = prop;
+ }
+ }
+ if (!foundProp) return null;
+
+ var objs = objsWithProp(foundProp);
+ if (objs) {
+ var matches = [];
+ search: for (var i = 0; i < objs.length; ++i) {
+ var obj = objs[i];
+ for (var prop in props) if (!obj.hasProp(prop)) continue search;
+ if (obj.hasCtor) obj = getInstance(obj);
+ matches.push(obj);
+ }
+ var canon = canonicalType(matches);
+ if (canon) {guessing = true; return canon;}
+ }
+ },
+
+ typeHint: function() { return this.types.length ? this.getType() : null; },
+ propagatesTo: function() { return this; },
+
+ gatherProperties: function(f, depth) {
+ for (var i = 0; i < this.types.length; ++i)
+ this.types[i].gatherProperties(f, depth);
+ },
+
+ guessProperties: function(f) {
+ if (this.forward) for (var i = 0; i < this.forward.length; ++i) {
+ var prop = this.forward[i].propHint();
+ if (prop) f(prop, null, 0);
+ }
+ var guessed = this.makeupType();
+ if (guessed) guessed.gatherProperties(f);
+ }
+ });
+
+ function similarAVal(a, b, depth) {
+ var typeA = a.getType(false), typeB = b.getType(false);
+ if (!typeA || !typeB) return true;
+ return similarType(typeA, typeB, depth);
+ }
+
+ function similarType(a, b, depth) {
+ if (!a || depth >= 5) return b;
+ if (!a || a == b) return a;
+ if (!b) return a;
+ if (a.constructor != b.constructor) return false;
+ if (a.constructor == Arr) {
+ var innerA = a.getProp("<i>").getType(false);
+ if (!innerA) return b;
+ var innerB = b.getProp("<i>").getType(false);
+ if (!innerB || similarType(innerA, innerB, depth + 1)) return b;
+ } else if (a.constructor == Obj) {
+ var propsA = 0, propsB = 0, same = 0;
+ for (var prop in a.props) {
+ propsA++;
+ if (prop in b.props && similarAVal(a.props[prop], b.props[prop], depth + 1))
+ same++;
+ }
+ for (var prop in b.props) propsB++;
+ if (propsA && propsB && same < Math.max(propsA, propsB) / 2) return false;
+ return propsA > propsB ? a : b;
+ } else if (a.constructor == Fn) {
+ if (a.args.length != b.args.length ||
+ !a.args.every(function(tp, i) { return similarAVal(tp, b.args[i], depth + 1); }) ||
+ !similarAVal(a.retval, b.retval, depth + 1) || !similarAVal(a.self, b.self, depth + 1))
+ return false;
+ return a;
+ } else {
+ return false;
+ }
+ }
+
+ var simplifyTypes = exports.simplifyTypes = function(types) {
+ var found = [];
+ outer: for (var i = 0; i < types.length; ++i) {
+ var tp = types[i];
+ for (var j = 0; j < found.length; j++) {
+ var similar = similarType(tp, found[j], 0);
+ if (similar) {
+ found[j] = similar;
+ continue outer;
+ }
+ }
+ found.push(tp);
+ }
+ return found;
+ };
+
+ function canonicalType(types) {
+ var arrays = 0, fns = 0, objs = 0, prim = null;
+ for (var i = 0; i < types.length; ++i) {
+ var tp = types[i];
+ if (tp instanceof Arr) ++arrays;
+ else if (tp instanceof Fn) ++fns;
+ else if (tp instanceof Obj) ++objs;
+ else if (tp instanceof Prim) {
+ if (prim && tp.name != prim.name) return null;
+ prim = tp;
+ }
+ }
+ var kinds = (arrays && 1) + (fns && 1) + (objs && 1) + (prim && 1);
+ if (kinds > 1) return null;
+ if (prim) return prim;
+
+ var maxScore = 0, maxTp = null;
+ for (var i = 0; i < types.length; ++i) {
+ var tp = types[i], score = 0;
+ if (arrays) {
+ score = tp.getProp("<i>").isEmpty() ? 1 : 2;
+ } else if (fns) {
+ score = 1;
+ for (var j = 0; j < tp.args.length; ++j) if (!tp.args[j].isEmpty()) ++score;
+ if (!tp.retval.isEmpty()) ++score;
+ } else if (objs) {
+ score = tp.name ? 100 : 2;
+ }
+ if (score >= maxScore) { maxScore = score; maxTp = tp; }
+ }
+ return maxTp;
+ }
+
+ // PROPAGATION STRATEGIES
+
+ var constraint = exports.constraint = function(methods) {
+ var ctor = function() {
+ this.origin = cx.curOrigin;
+ this.construct.apply(this, arguments);
+ };
+ ctor.prototype = Object.create(ANull);
+ for (var m in methods) if (methods.hasOwnProperty(m)) ctor.prototype[m] = methods[m];
+ return ctor;
+ };
+
+ var GetProp = constraint({
+ construct: function(prop, target) {
+ this.prop = prop; this.target = target;
+ },
+ addType: function(type, weight) {
+ if (type.getProp)
+ type.getProp(this.prop).propagate(this.target, weight);
+ },
+ propHint: function() { return this.prop; },
+ propagatesTo: function() {
+ if (this.prop == "<i>" || !/[^\w_]/.test(this.prop))
+ return {target: this.target, pathExt: "." + this.prop};
+ }
+ });
+
+ var DefProp = exports.PropHasSubset = exports.DefProp = constraint({
+ construct: function(prop, type, originNode) {
+ this.prop = prop; this.type = type; this.originNode = originNode;
+ },
+ addType: function(type, weight) {
+ if (!(type instanceof Obj)) return;
+ var prop = type.defProp(this.prop, this.originNode);
+ if (!prop.origin) prop.origin = this.origin;
+ this.type.propagate(prop, weight);
+ },
+ propHint: function() { return this.prop; }
+ });
+
+ var ForAllProps = constraint({
+ construct: function(c) { this.c = c; },
+ addType: function(type) {
+ if (!(type instanceof Obj)) return;
+ type.forAllProps(this.c);
+ }
+ });
+
+ function withDisabledComputing(fn, body) {
+ cx.disabledComputing = {fn: fn, prev: cx.disabledComputing};
+ var result = body();
+ cx.disabledComputing = cx.disabledComputing.prev;
+ return result;
+ }
+ var IsCallee = exports.IsCallee = constraint({
+ construct: function(self, args, argNodes, retval) {
+ this.self = self; this.args = args; this.argNodes = argNodes; this.retval = retval;
+ this.disabled = cx.disabledComputing;
+ },
+ addType: function(fn, weight) {
+ if (!(fn instanceof Fn)) return;
+ for (var i = 0; i < this.args.length; ++i) {
+ if (i < fn.args.length) this.args[i].propagate(fn.args[i], weight);
+ if (fn.arguments) this.args[i].propagate(fn.arguments, weight);
+ }
+ this.self.propagate(fn.self, this.self == cx.topScope ? WG_GLOBAL_THIS : weight);
+ var compute = fn.computeRet, result = fn.retval
+ if (compute) for (var d = this.disabled; d; d = d.prev)
+ if (d.fn == fn || fn.originNode && d.fn.originNode == fn.originNode) compute = null;
+ if (compute) {
+ var old = cx.disabledComputing;
+ cx.disabledComputing = this.disabled;
+ result = compute(this.self, this.args, this.argNodes)
+ cx.disabledComputing = old;
+ }
+ maybeIterator(fn, result).propagate(this.retval, weight)
+ },
+ typeHint: function() {
+ var names = [];
+ for (var i = 0; i < this.args.length; ++i) names.push("?");
+ return new Fn(null, this.self, this.args, names, ANull);
+ },
+ propagatesTo: function() {
+ return {target: this.retval, pathExt: ".!ret"};
+ }
+ });
+
+ var HasMethodCall = constraint({
+ construct: function(propName, args, argNodes, retval) {
+ this.propName = propName; this.args = args; this.argNodes = argNodes; this.retval = retval;
+ this.disabled = cx.disabledComputing;
+ },
+ addType: function(obj, weight) {
+ var callee = new IsCallee(obj, this.args, this.argNodes, this.retval);
+ callee.disabled = this.disabled;
+ obj.getProp(this.propName).propagate(callee, weight);
+ },
+ propHint: function() { return this.propName; }
+ });
+
+ var IsCtor = exports.IsCtor = constraint({
+ construct: function(target, noReuse) {
+ this.target = target; this.noReuse = noReuse;
+ },
+ addType: function(f, weight) {
+ if (!(f instanceof Fn)) return;
+ if (cx.parent && !cx.parent.options.reuseInstances) this.noReuse = true;
+ f.getProp("prototype").propagate(new IsProto(this.noReuse ? false : f, this.target), weight);
+ }
+ });
+
+ var getInstance = exports.getInstance = function(obj, ctor) {
+ if (ctor === false) return new Obj(obj);
+
+ if (!ctor) ctor = obj.hasCtor;
+ if (!obj.instances) obj.instances = [];
+ for (var i = 0; i < obj.instances.length; ++i) {
+ var cur = obj.instances[i];
+ if (cur.ctor == ctor) return cur.instance;
+ }
+ var instance = new Obj(obj, ctor && ctor.name);
+ instance.origin = obj.origin;
+ obj.instances.push({ctor: ctor, instance: instance});
+ return instance;
+ };
+
+ var IsProto = exports.IsProto = constraint({
+ construct: function(ctor, target) {
+ this.ctor = ctor; this.target = target;
+ },
+ addType: function(o, _weight) {
+ if (!(o instanceof Obj)) return;
+ if ((this.count = (this.count || 0) + 1) > 8) return;
+ if (o == cx.protos.Array)
+ this.target.addType(new Arr);
+ else
+ this.target.addType(getInstance(o, this.ctor));
+ }
+ });
+
+ var FnPrototype = constraint({
+ construct: function(fn) { this.fn = fn; },
+ addType: function(o, _weight) {
+ if (o instanceof Obj && !o.hasCtor) {
+ o.hasCtor = this.fn;
+ var adder = new SpeculativeThis(o, this.fn);
+ adder.addType(this.fn);
+ o.forAllProps(function(_prop, val, local) {
+ if (local) val.propagate(adder);
+ });
+ }
+ }
+ });
+
+ var IsAdded = constraint({
+ construct: function(other, target) {
+ this.other = other; this.target = target;
+ },
+ addType: function(type, weight) {
+ if (type == cx.str)
+ this.target.addType(cx.str, weight);
+ else if (type == cx.num && this.other.hasType(cx.num))
+ this.target.addType(cx.num, weight);
+ },
+ typeHint: function() { return this.other; }
+ });
+
+ var IfObj = exports.IfObj = constraint({
+ construct: function(target) { this.target = target; },
+ addType: function(t, weight) {
+ if (t instanceof Obj) this.target.addType(t, weight);
+ },
+ propagatesTo: function() { return this.target; }
+ });
+
+ var SpeculativeThis = constraint({
+ construct: function(obj, ctor) { this.obj = obj; this.ctor = ctor; },
+ addType: function(tp) {
+ if (tp instanceof Fn && tp.self)
+ tp.self.addType(getInstance(this.obj, this.ctor), WG_SPECULATIVE_PROTO_THIS);
+ }
+ });
+
+ var HasProto = constraint({
+ construct: function(obj) { this.obj = obj },
+ addType: function(tp) {
+ if (tp instanceof Obj && this.obj.proto == cx.protos.Object)
+ this.obj.replaceProto(tp)
+ }
+ });
+
+ var Muffle = constraint({
+ construct: function(inner, weight) {
+ this.inner = inner; this.weight = weight;
+ },
+ addType: function(tp, weight) {
+ this.inner.addType(tp, Math.min(weight, this.weight));
+ },
+ propagatesTo: function() { return this.inner.propagatesTo(); },
+ typeHint: function() { return this.inner.typeHint(); },
+ propHint: function() { return this.inner.propHint(); }
+ });
+
+ // TYPE OBJECTS
+
+ var Type = exports.Type = function() {};
+ Type.prototype = extend(ANull, {
+ constructor: Type,
+ propagate: function(c, w) { c.addType(this, w); },
+ hasType: function(other) { return other == this; },
+ isEmpty: function() { return false; },
+ typeHint: function() { return this; },
+ getType: function() { return this; }
+ });
+
+ var Prim = exports.Prim = function(proto, name) { this.name = name; this.proto = proto; };
+ Prim.prototype = extend(Type.prototype, {
+ constructor: Prim,
+ toString: function() { return this.name; },
+ getProp: function(prop) {return this.proto.hasProp(prop) || ANull;},
+ gatherProperties: function(f, depth) {
+ if (this.proto) this.proto.gatherProperties(f, depth);
+ }
+ });
+
+ function isInteger(str) {
+ var c0 = str.charCodeAt(0)
+ if (c0 >= 48 && c0 <= 57) return !/\D/.test(str)
+ else return false
+ }
+
+ var Obj = exports.Obj = function(proto, name) {
+ if (!this.props) this.props = Object.create(null);
+ this.proto = proto === true ? cx.protos.Object : proto;
+ if (proto && !name && proto.name && !(this instanceof Fn)) {
+ var match = /^(.*)\.prototype$/.exec(this.proto.name);
+ if (match) name = match[1];
+ }
+ this.name = name;
+ this.maybeProps = null;
+ this.origin = cx.curOrigin;
+ };
+ Obj.prototype = extend(Type.prototype, {
+ constructor: Obj,
+ toString: function(maxDepth) {
+ if (maxDepth == null) maxDepth = 0;
+ if (maxDepth <= 0 && this.name) return this.name;
+ var props = [], etc = false;
+ for (var prop in this.props) if (prop != "<i>") {
+ if (props.length > 5) { etc = true; break; }
+ if (maxDepth)
+ props.push(prop + ": " + toString(this.props[prop], maxDepth - 1, this));
+ else
+ props.push(prop);
+ }
+ props.sort();
+ if (etc) props.push("...");
+ return "{" + props.join(", ") + "}";
+ },
+ hasProp: function(prop, searchProto) {
+ if (isInteger(prop)) prop = this.normalizeIntegerProp(prop)
+ var found = this.props[prop];
+ if (searchProto !== false)
+ for (var p = this.proto; p && !found; p = p.proto) found = p.props[prop];
+ return found;
+ },
+ defProp: function(prop, originNode) {
+ var found = this.hasProp(prop, false);
+ if (found) {
+ if (originNode && !found.originNode) found.originNode = originNode;
+ return found;
+ }
+ if (prop == "__proto__" || prop == "✖") return ANull;
+ if (isInteger(prop)) prop = this.normalizeIntegerProp(prop)
+
+ var av = this.maybeProps && this.maybeProps[prop];
+ if (av) {
+ delete this.maybeProps[prop];
+ this.maybeUnregProtoPropHandler();
+ } else {
+ av = new AVal;
+ av.propertyOf = this;
+ av.propertyName = prop;
+ }
+
+ this.props[prop] = av;
+ av.originNode = originNode;
+ av.origin = cx.curOrigin;
+ this.broadcastProp(prop, av, true);
+ return av;
+ },
+ getProp: function(prop) {
+ var found = this.hasProp(prop, true) || (this.maybeProps && this.maybeProps[prop]);
+ if (found) return found;
+ if (prop == "__proto__" || prop == "✖") return ANull;
+ if (isInteger(prop)) prop = this.normalizeIntegerProp(prop)
+ var av = this.ensureMaybeProps()[prop] = new AVal;
+ av.propertyOf = this;
+ av.propertyName = prop;
+ return av;
+ },
+ normalizeIntegerProp: function(_) { return "<i>" },
+ broadcastProp: function(prop, val, local) {
+ if (local) {
+ this.signal("addProp", prop, val);
+ // If this is a scope, it shouldn't be registered
+ if (!(this instanceof Scope)) registerProp(prop, this);
+ }
+
+ if (this.onNewProp) for (var i = 0; i < this.onNewProp.length; ++i) {
+ var h = this.onNewProp[i];
+ h.onProtoProp ? h.onProtoProp(prop, val, local) : h(prop, val, local);
+ }
+ },
+ onProtoProp: function(prop, val, _local) {
+ var maybe = this.maybeProps && this.maybeProps[prop];
+ if (maybe) {
+ delete this.maybeProps[prop];
+ this.maybeUnregProtoPropHandler();
+ this.proto.getProp(prop).propagate(maybe);
+ }
+ this.broadcastProp(prop, val, false);
+ },
+ replaceProto: function(proto) {
+ if (this.proto && this.maybeProps)
+ this.proto.unregPropHandler(this)
+ this.proto = proto
+ if (this.maybeProps)
+ this.proto.forAllProps(this)
+ },
+ ensureMaybeProps: function() {
+ if (!this.maybeProps) {
+ if (this.proto) this.proto.forAllProps(this);
+ this.maybeProps = Object.create(null);
+ }
+ return this.maybeProps;
+ },
+ removeProp: function(prop) {
+ var av = this.props[prop];
+ delete this.props[prop];
+ this.ensureMaybeProps()[prop] = av;
+ av.types.length = 0;
+ },
+ forAllProps: function(c) {
+ if (!this.onNewProp) {
+ this.onNewProp = [];
+ if (this.proto) this.proto.forAllProps(this);
+ }
+ this.onNewProp.push(c);
+ for (var o = this; o; o = o.proto) for (var prop in o.props) {
+ if (c.onProtoProp)
+ c.onProtoProp(prop, o.props[prop], o == this);
+ else
+ c(prop, o.props[prop], o == this);
+ }
+ },
+ maybeUnregProtoPropHandler: function() {
+ if (this.maybeProps) {
+ for (var _n in this.maybeProps) return;
+ this.maybeProps = null;
+ }
+ if (!this.proto || this.onNewProp && this.onNewProp.length) return;
+ this.proto.unregPropHandler(this);
+ },
+ unregPropHandler: function(handler) {
+ for (var i = 0; i < this.onNewProp.length; ++i)
+ if (this.onNewProp[i] == handler) { this.onNewProp.splice(i, 1); break; }
+ this.maybeUnregProtoPropHandler();
+ },
+ gatherProperties: function(f, depth) {
+ for (var prop in this.props) if (prop != "<i>" && prop.charAt(0) != ":")
+ f(prop, this, depth);
+ if (this.proto) this.proto.gatherProperties(f, depth + 1);
+ },
+ getObjType: function() { return this; }
+ });
+
+ var Fn = exports.Fn = function(name, self, args, argNames, retval, generator) {
+ Obj.call(this, cx.protos.Function, name);
+ this.self = self;
+ this.args = args;
+ this.argNames = argNames;
+ this.retval = retval;
+ this.generator = generator
+ };
+ Fn.prototype = extend(Obj.prototype, {
+ constructor: Fn,
+ toString: function(maxDepth) {
+ if (maxDepth == null) maxDepth = 0;
+ var str = this.generator ? "fn*(" : "fn(";
+ for (var i = 0; i < this.args.length; ++i) {
+ if (i) str += ", ";
+ var name = this.argNames[i];
+ if (name && name != "?") str += name + ": ";
+ str += maxDepth > -3 ? toString(this.args[i], maxDepth - 1, this) : "?";
+ }
+ str += ")";
+ if (!this.retval.isEmpty())
+ str += " -> " + (maxDepth > -3 ? toString(this.retval, maxDepth - 1, this) : "?");
+ return str;
+ },
+ getProp: function(prop) {
+ if (prop == "prototype") {
+ var known = this.hasProp(prop, false);
+ if (!known) {
+ known = this.defProp(prop);
+ var proto = new Obj(true, this.name && this.name + ".prototype");
+ proto.origin = this.origin;
+ known.addType(proto, WG_MADEUP_PROTO);
+ }
+ return known;
+ }
+ return Obj.prototype.getProp.call(this, prop);
+ },
+ defProp: function(prop, originNode) {
+ if (prop == "prototype") {
+ var found = this.hasProp(prop, false);
+ if (found) return found;
+ found = Obj.prototype.defProp.call(this, prop, originNode);
+ found.origin = this.origin;
+ found.propagate(new FnPrototype(this));
+ return found;
+ }
+ return Obj.prototype.defProp.call(this, prop, originNode);
+ },
+ getFunctionType: function() { return this; }
+ });
+
+ var Arr = exports.Arr = function(contentType) {
+ Obj.call(this, cx.protos.Array)
+ var content = this.defProp("<i>")
+ if (Array.isArray(contentType)) {
+ this.tuple = contentType.length
+ for (var i = 0; i < contentType.length; i++) {
+ var prop = this.defProp(String(i))
+ contentType[i].propagate(prop)
+ prop.propagate(content)
+ }
+ } else if (contentType) {
+ this.tuple = 0
+ contentType.propagate(content)
+ }
+ };
+ Arr.prototype = extend(Obj.prototype, {
+ constructor: Arr,
+ toString: function(maxDepth) {
+ if (maxDepth == null) maxDepth = 0
+ if (maxDepth <= -3) return "[?]"
+ var content = ""
+ if (this.tuple) {
+ var similar
+ for (var i = 0; i in this.props; i++) {
+ var type = toString(this.getProp(String(i)), maxDepth - 1, this)
+ if (similar == null)
+ similar = type
+ else if (similar != type)
+ similar = false
+ else
+ similar = type
+ content += (content ? ", " : "") + type
+ }
+ if (similar) content = similar
+ } else {
+ content = toString(this.getProp("<i>"), maxDepth - 1, this)
+ }
+ return "[" + content + "]"
+ },
+ normalizeIntegerProp: function(prop) {
+ if (+prop < this.tuple) return prop
+ else return "<i>"
+ }
+ });
+
+ var Sym = exports.Sym = function(name, originNode) {
+ Prim.call(this, cx.protos.Symbol, "Symbol")
+ this.symName = name
+ this.originNode = originNode
+ }
+ Sym.prototype = extend(Prim.prototype, {
+ constructor: Sym,
+ asPropName: function() { return ":" + this.symName },
+ getSymbolType: function() { return this }
+ })
+
+ exports.getSymbol = function(name, originNode) {
+ var cleanName = name.replace(/[^\w$\.]/g, "_")
+ var known = cx.symbols[cleanName]
+ if (known) {
+ if (originNode && !known.originNode) known.originNode = originNode
+ return known
+ }
+ return cx.symbols[cleanName] = new Sym(cleanName, originNode)
+ }
+
+ // THE PROPERTY REGISTRY
+
+ function registerProp(prop, obj) {
+ var data = cx.props[prop] || (cx.props[prop] = []);
+ data.push(obj);
+ }
+
+ function objsWithProp(prop) {
+ return cx.props[prop];
+ }
+
+ // INFERENCE CONTEXT
+
+ exports.Context = function(defs, parent) {
+ this.parent = parent;
+ this.props = Object.create(null);
+ this.protos = Object.create(null);
+ this.origins = [];
+ this.curOrigin = "ecma5";
+ this.paths = Object.create(null);
+ this.definitions = Object.create(null);
+ this.purgeGen = 0;
+ this.workList = null;
+ this.disabledComputing = null;
+ this.curSuperCtor = this.curSuper = null;
+ this.symbols = Object.create(null)
+
+ exports.withContext(this, function() {
+ cx.protos.Object = new Obj(null, "Object.prototype");
+ cx.topScope = new Scope();
+ cx.topScope.name = "<top>";
+ cx.protos.Array = new Obj(true, "Array.prototype");
+ cx.protos.Function = new Fn("Function.prototype", ANull, [], [], ANull);
+ cx.protos.Function.proto = cx.protos.Object;
+ cx.protos.RegExp = new Obj(true, "RegExp.prototype");
+ cx.protos.String = new Obj(true, "String.prototype");
+ cx.protos.Number = new Obj(true, "Number.prototype");
+ cx.protos.Boolean = new Obj(true, "Boolean.prototype");
+ cx.protos.Symbol = new Obj(true, "Symbol.prototype");
+ cx.str = new Prim(cx.protos.String, "string");
+ cx.bool = new Prim(cx.protos.Boolean, "bool");
+ cx.num = new Prim(cx.protos.Number, "number");
+ cx.curOrigin = null;
+
+ if (defs) for (var i = 0; i < defs.length; ++i)
+ def.load(defs[i]);
+ });
+ };
+
+ exports.Context.prototype.startAnalysis = function() {
+ this.disabledComputing = this.workList = this.curSuperCtor = this.curSuper = null;
+ };
+
+ var cx = null;
+ exports.cx = function() { return cx; };
+
+ exports.withContext = function(context, f) {
+ var old = cx;
+ cx = context;
+ try { return f(); }
+ finally { cx = old; }
+ };
+
+ exports.TimedOut = function() {
+ this.message = "Timed out";
+ this.stack = (new Error()).stack;
+ };
+ exports.TimedOut.prototype = Object.create(Error.prototype);
+ exports.TimedOut.prototype.name = "infer.TimedOut";
+
+ var timeout;
+ exports.withTimeout = function(ms, f) {
+ var end = +new Date + ms;
+ var oldEnd = timeout;
+ if (oldEnd && oldEnd < end) return f();
+ timeout = end;
+ try { return f(); }
+ finally { timeout = oldEnd; }
+ };
+
+ exports.addOrigin = function(origin) {
+ if (cx.origins.indexOf(origin) < 0) cx.origins.push(origin);
+ };
+
+ var baseMaxWorkDepth = 20, reduceMaxWorkDepth = 0.0001;
+ function withWorklist(f) {
+ if (cx.workList) return f(cx.workList);
+
+ var list = [], depth = 0;
+ var add = cx.workList = function(type, target, weight) {
+ if (depth < baseMaxWorkDepth - reduceMaxWorkDepth * list.length)
+ list.push(type, target, weight, depth);
+ };
+ var ret = f(add);
+ for (var i = 0; i < list.length; i += 4) {
+ if (timeout && +new Date >= timeout)
+ throw new exports.TimedOut();
+ depth = list[i + 3] + 1;
+ list[i + 1].addType(list[i], list[i + 2]);
+ }
+ cx.workList = null;
+ return ret;
+ }
+
+ function withSuper(ctor, obj, f) {
+ var oldCtor = cx.curSuperCtor, oldObj = cx.curSuper
+ cx.curSuperCtor = ctor; cx.curSuper = obj
+ var result = f()
+ cx.curSuperCtor = oldCtor; cx.curSuper = oldObj
+ return result
+ }
+
+ // SCOPES
+
+ var Scope = exports.Scope = function(prev, originNode, isBlock) {
+ Obj.call(this, prev || true);
+ this.prev = prev;
+ this.originNode = originNode
+ this.isBlock = !!isBlock
+ };
+ Scope.prototype = extend(Obj.prototype, {
+ constructor: Scope,
+ defVar: function(name, originNode) {
+ for (var s = this; ; s = s.proto) {
+ var found = s.props[name];
+ if (found) return found;
+ if (!s.prev) return s.defProp(name, originNode);
+ }
+ }
+ });
+
+ function functionScope(scope) {
+ while (scope.isBlock) scope = scope.prev
+ return scope
+ }
+
+
+ // RETVAL COMPUTATION HEURISTICS
+
+ function maybeInstantiate(scope, score) {
+ var fn = functionScope(scope).fnType
+ if (fn) fn.instantiateScore = (fn.instantiateScore || 0) + score;
+ }
+
+ var NotSmaller = {};
+ function nodeSmallerThan(node, n) {
+ try {
+ walk.simple(node, {Expression: function() { if (--n <= 0) throw NotSmaller; }});
+ return true;
+ } catch(e) {
+ if (e == NotSmaller) return false;
+ throw e;
+ }
+ }
+
+ function maybeTagAsInstantiated(node, fn) {
+ var score = fn.instantiateScore;
+ if (!cx.disabledComputing && score && fn.args.length && nodeSmallerThan(node, score * 5)) {
+ maybeInstantiate(functionScope(fn.originNode.scope.prev), score / 2);
+ setFunctionInstantiated(node, fn);
+ return true;
+ } else {
+ fn.instantiateScore = null;
+ }
+ }
+
+ function setFunctionInstantiated(node, fn) {
+ // Disconnect the arg avals, so that we can add info to them without side effects
+ for (var i = 0; i < fn.args.length; ++i) fn.args[i] = new AVal;
+ fn.self = new AVal;
+ fn.computeRet = function(self, args) {
+ // Prevent recursion
+ return withDisabledComputing(fn, function() {
+ var oldOrigin = cx.curOrigin;
+ cx.curOrigin = fn.origin;
+ var scope = node.scope
+ var scopeCopy = new Scope(scope.prev, scope.originNode);
+ for (var v in scope.props) {
+ var local = scopeCopy.defProp(v, scope.props[v].originNode);
+ for (var i = 0; i < args.length; ++i) if (fn.argNames[i] == v && i < args.length)
+ args[i].propagate(local);
+ }
+ var argNames = fn.argNames.length != args.length ? fn.argNames.slice(0, args.length) : fn.argNames;
+ while (argNames.length < args.length) argNames.push("?");
+ scopeCopy.fnType = new Fn(fn.name, self, args, argNames, ANull, fn.generator);
+ scopeCopy.fnType.originNode = fn.originNode;
+ if (fn.arguments) {
+ var argset = scopeCopy.fnType.arguments = new AVal;
+ scopeCopy.defProp("arguments").addType(new Arr(argset));
+ for (var i = 0; i < args.length; ++i) args[i].propagate(argset);
+ }
+ node.scope = scopeCopy;
+ walk.recursive(node.body, scopeCopy, null, scopeGatherer);
+ walk.recursive(node.body, scopeCopy, null, inferWrapper);
+ cx.curOrigin = oldOrigin;
+ return scopeCopy.fnType.retval;
+ });
+ };
+ }
+
+ function maybeTagAsGeneric(fn) {
+ var target = fn.retval;
+ if (target == ANull) return;
+ var targetInner, asArray;
+ if (!target.isEmpty() && (targetInner = target.getType()) instanceof Arr)
+ target = asArray = targetInner.getProp("<i>");
+
+ function explore(aval, path, depth) {
+ if (depth > 3 || !aval.forward) return;
+ for (var i = 0; i < aval.forward.length; ++i) {
+ var prop = aval.forward[i].propagatesTo();
+ if (!prop) continue;
+ var newPath = path, dest;
+ if (prop instanceof AVal) {
+ dest = prop;
+ } else if (prop.target instanceof AVal) {
+ newPath += prop.pathExt;
+ dest = prop.target;
+ } else continue;
+ if (dest == target) return newPath;
+ var found = explore(dest, newPath, depth + 1);
+ if (found) return found;
+ }
+ }
+
+ var foundPath = explore(fn.self, "!this", 0);
+ for (var i = 0; !foundPath && i < fn.args.length; ++i)
+ foundPath = explore(fn.args[i], "!" + i, 0);
+
+ if (foundPath) {
+ if (asArray) foundPath = "[" + foundPath + "]";
+ var p = new def.TypeParser(foundPath);
+ var parsed = p.parseType(true);
+ fn.computeRet = parsed.apply ? parsed : function() { return parsed; };
+ fn.computeRetSource = foundPath;
+ return true;
+ }
+ }
+
+ // SCOPE GATHERING PASS
+
+ function addVar(scope, nameNode) {
+ return scope.defProp(nameNode.name, nameNode);
+ }
+ function patternName(node) {
+ if (node.type == "Identifier") return node.name
+ if (node.type == "AssignmentPattern") return patternName(node.left)
+ if (node.type == "ObjectPattern") return "{" + node.properties.map(function(e) { return patternName(e.value) }).join(", ") + "}"
+ if (node.type == "ArrayPattern") return "[" + node.elements.map(patternName).join(", ") + "]"
+ if (node.type == "RestElement") return "..." + patternName(node.argument)
+ return "_"
+ }
+
+ function isBlockScopedDecl(node) {
+ return node.type == "VariableDeclaration" && node.kind != "var" ||
+ node.type == "FunctionDeclaration" ||
+ node.type == "ClassDeclaration";
+ }
+
+ function patternScopes(inner, outer) {
+ return {inner: inner, outer: outer || inner}
+ }
+
+ var scopeGatherer = exports.scopeGatherer = walk.make({
+ VariablePattern: function(node, scopes) {
+ if (scopes.inner) addVar(scopes.inner, node)
+ },
+ AssignmentPattern: function(node, scopes, c) {
+ c(node.left, scopes, "Pattern")
+ c(node.right, scopes.outer, "Expression")
+ },
+ AssignmentExpression: function(node, scope, c) {
+ if (node.left.type == "MemberExpression")
+ c(node.left, scope, "Expression")
+ else
+ c(node.left, patternScopes(false, scope), "Pattern")
+ c(node.right, scope, "Expression")
+ },
+ Function: function(node, scope, c) {
+ var inner = node.scope = new Scope(scope, node)
+ var argVals = [], argNames = []
+ for (var i = 0; i < node.params.length; ++i) {
+ var param = node.params[i]
+ argNames.push(patternName(param))
+ if (param.type == "Identifier") {
+ argVals.push(addVar(inner, param))
+ } else {
+ var arg = new AVal
+ argVals.push(arg)
+ arg.originNode = param
+ c(param, patternScopes(inner), "Pattern")
+ }
+ }
+ inner.fnType = new Fn(node.id && node.id.name, new AVal, argVals, argNames, ANull, node.generator)
+ inner.fnType.originNode = node;
+ if (node.id) {
+ var decl = node.type == "FunctionDeclaration";
+ addVar(decl ? scope : inner, node.id);
+ }
+ c(node.body, inner, node.expression ? "Expression" : "Statement");
+ },
+ BlockStatement: function(node, scope, c) {
+ if (!node.scope && node.body.some(isBlockScopedDecl))
+ scope = node.scope = new Scope(scope, node, true)
+ walk.base.BlockStatement(node, scope, c)
+ },
+ TryStatement: function(node, scope, c) {
+ c(node.block, scope, "Statement");
+ if (node.handler) {
+ if (node.handler.param.type == "Identifier") {
+ var v = addVar(scope, node.handler.param);
+ c(node.handler.body, scope, "Statement");
+ var e5 = cx.definitions.ecma5;
+ if (e5 && v.isEmpty()) getInstance(e5["Error.prototype"]).propagate(v, WG_CATCH_ERROR);
+ } else {
+ c(node.handler.param, patternScopes(scope), "Pattern")
+ }
+ }
+ if (node.finalizer) c(node.finalizer, scope, "Statement");
+ },
+ VariableDeclaration: function(node, scope, c) {
+ var targetScope = node.kind == "var" ? functionScope(scope) : scope
+ for (var i = 0; i < node.declarations.length; ++i) {
+ var decl = node.declarations[i];
+ c(decl.id, patternScopes(targetScope, scope), "Pattern")
+ if (decl.init) c(decl.init, scope, "Expression");
+ }
+ },
+ ClassDeclaration: function(node, scope, c) {
+ addVar(scope, node.id)
+ if (node.superClass) c(node.superClass, scope, "Expression")
+ for (var i = 0; i < node.body.body.length; i++)
+ c(node.body.body[i], scope)
+ },
+ ForInStatement: function(node, scope, c) {
+ if (!node.scope && isBlockScopedDecl(node.left))
+ scope = node.scope = new Scope(scope, node, true)
+ walk.base.ForInStatement(node, scope, c)
+ },
+ ForStatement: function(node, scope, c) {
+ if (!node.scope && node.init && isBlockScopedDecl(node.init))
+ scope = node.scope = new Scope(scope, node, true)
+ walk.base.ForStatement(node, scope, c)
+ },
+ ImportDeclaration: function(node, scope) {
+ for (var i = 0; i < node.specifiers.length; i++)
+ addVar(scope, node.specifiers[i].local)
+ }
+ });
+ scopeGatherer.ForOfStatement = scopeGatherer.ForInStatement
+
+ // CONSTRAINT GATHERING PASS
+
+ var propName = exports.propName = function(node, inferInScope) {
+ var key = node.property || node.key;
+ if (!node.computed && key.type == "Identifier") return key.name;
+ if (key.type == "Literal") {
+ if (typeof key.value == "string") return key.value
+ if (typeof key.value == "number") return String(key.value)
+ }
+ if (inferInScope) {
+ var symName = symbolName(infer(key, inferInScope))
+ if (symName) return node.propName = symName
+ } else if (node.propName) {
+ return node.propName
+ }
+ return "<i>";
+ }
+ function symbolName(val) {
+ var sym = val.getSymbolType()
+ if (sym) return sym.asPropName()
+ }
+
+ function unopResultType(op) {
+ switch (op) {
+ case "+": case "-": case "~": return cx.num;
+ case "!": return cx.bool;
+ case "typeof": return cx.str;
+ case "void": case "delete": return ANull;
+ }
+ }
+ function binopIsBoolean(op) {
+ switch (op) {
+ case "==": case "!=": case "===": case "!==": case "<": case ">": case ">=": case "<=":
+ case "in": case "instanceof": return true;
+ }
+ }
+ function literalType(node) {
+ if (node.regex) return getInstance(cx.protos.RegExp);
+ switch (typeof node.value) {
+ case "boolean": return cx.bool;
+ case "number": return cx.num;
+ case "string": return cx.str;
+ case "object":
+ case "function":
+ if (!node.value) return ANull;
+ return getInstance(cx.protos.RegExp);
+ }
+ }
+
+ function join(a, b) {
+ if (a == b || b == ANull) return a
+ if (a == ANull) return b
+ var joined = new AVal
+ a.propagate(joined)
+ b.propagate(joined)
+ return joined
+ }
+
+ function connectParams(node, scope) {
+ for (var i = 0; i < node.params.length; i++) {
+ var param = node.params[i]
+ if (param.type == "Identifier") continue
+ connectPattern(param, scope, node.scope.fnType.args[i])
+ }
+ }
+
+ function ensureVar(node, scope) {
+ return scope.hasProp(node.name) || cx.topScope.defProp(node.name, node)
+ }
+
+ var inferPatternVisitor = exports.inferPatternVisitor = {
+ Identifier: function(node, scope, source) {
+ source.propagate(ensureVar(node, scope))
+ },
+ MemberExpression: function(node, scope, source) {
+ var obj = infer(node.object, scope)
+ var pName = propName(node, scope)
+ obj.propagate(new DefProp(pName, source, node.property))
+ },
+ RestElement: function(node, scope, source) {
+ connectPattern(node.argument, scope, new Arr(source))
+ },
+ ObjectPattern: function(node, scope, source) {
+ for (var i = 0; i < node.properties.length; ++i) {
+ var prop = node.properties[i]
+ connectPattern(prop.value, scope, source.getProp(prop.key.name))
+ }
+ },
+ ArrayPattern: function(node, scope, source) {
+ for (var i = 0; i < node.elements.length; i++)
+ if (node.elements[i])
+ connectPattern(node.elements[i], scope, source.getProp(String(i)))
+ },
+ AssignmentPattern: function(node, scope, source) {
+ connectPattern(node.left, scope, join(source, infer(node.right, scope)))
+ }
+ }
+
+ function connectPattern(node, scope, source) {
+ var connecter = inferPatternVisitor[node.type]
+ if (connecter) connecter(node, scope, source)
+ }
+
+ function getThis(scope) {
+ var fnScope = functionScope(scope)
+ return fnScope.fnType ? fnScope.fnType.self : fnScope
+ }
+
+ function maybeAddPhantomObj(obj) {
+ if (!obj.isEmpty() || !obj.propertyOf) return
+ obj.propertyOf.getProp(obj.propertyName).addType(new Obj, WG_PHANTOM_OBJ)
+ maybeAddPhantomObj(obj.propertyOf)
+ }
+
+ function inferClass(node, scope, name) {
+ if (!name && node.id) name = node.id.name
+
+ var sup = cx.protos.Object, supCtor, delayed
+ if (node.superClass) {
+ if (node.superClass.type == "Literal" && node.superClass.value == null) {
+ sup = null
+ } else {
+ var supVal = infer(node.superClass, scope), supProto
+ supCtor = supVal.getFunctionType()
+ if (supCtor && (supProto = supCtor.getProp("prototype").getObjType())) {
+ sup = supProto
+ } else {
+ supCtor = supVal
+ delayed = supVal.getProp("prototype")
+ }
+ }
+ }
+ var proto = new Obj(sup, name && name + ".prototype")
+ if (delayed) delayed.propagate(new HasProto(proto))
+
+ return withSuper(supCtor, delayed || sup, function() {
+ var ctor, body = node.body.body
+ for (var i = 0; i < body.length; i++)
+ if (body[i].kind == "constructor") ctor = body[i].value
+ var fn = node.objType = ctor ? infer(ctor, scope) : new Fn(name, ANull, [], null, ANull)
+ fn.originNode = node.id || ctor || node
+
+ var inst = getInstance(proto, fn)
+ fn.self.addType(inst)
+ fn.defProp("prototype", node).addType(proto)
+ for (var i = 0; i < body.length; i++) {
+ var method = body[i], target
+ if (method.kind == "constructor") continue
+ var pName = propName(method, scope)
+ if (pName == "<i>" || method.kind == "set") {
+ target = ANull
+ } else {
+ target = (method.static ? fn : proto).defProp(pName, method.key)
+ target.initializer = true
+ if (method.kind == "get") target = new IsCallee(inst, [], null, target)
+ }
+ infer(method.value, scope, target)
+ var methodFn = target.getFunctionType()
+ if (methodFn) methodFn.self.addType(inst)
+ }
+ return fn
+ })
+ }
+
+ function arrayLiteralType(elements, scope, inner) {
+ var tuple = elements.length > 1 && elements.length < 6
+ if (tuple) {
+ var homogenous = true, litType
+ for (var i = 0; i < elements.length; i++) {
+ var elt = elements[i]
+ if (!elt)
+ tuple = false
+ else if (elt.type != "Literal" || (litType && litType != typeof elt.value))
+ homogenous = false
+ else
+ litType = typeof elt.value
+ }
+ if (homogenous) tuple = false
+ }
+
+ if (tuple) {
+ var types = []
+ for (var i = 0; i < elements.length; ++i)
+ types.push(inner(elements[i], scope))
+ return new Arr(types)
+ } else if (elements.length < 2) {
+ return new Arr(elements[0] && inner(elements[0], scope))
+ } else {
+ var eltVal = new AVal
+ for (var i = 0; i < elements.length; i++)
+ if (elements[i]) inner(elements[i], scope).propagate(eltVal)
+ return new Arr(eltVal)
+ }
+ }
+
+ function ret(f) {
+ return function(node, scope, out, name) {
+ var r = f(node, scope, name);
+ if (out) r.propagate(out);
+ return r;
+ };
+ }
+ function fill(f) {
+ return function(node, scope, out, name) {
+ if (!out) out = new AVal;
+ f(node, scope, out, name);
+ return out;
+ };
+ }
+
+ var inferExprVisitor = exports.inferExprVisitor = {
+ ArrayExpression: ret(function(node, scope) {
+ return arrayLiteralType(node.elements, scope, infer)
+ }),
+ ObjectExpression: ret(function(node, scope, name) {
+ var proto = true, waitForProto
+ for (var i = 0; i < node.properties.length; ++i) {
+ var prop = node.properties[i]
+ if (prop.key.name == "__proto__") {
+ if (prop.value.type == "Literal" && prop.value.value == null) {
+ proto = null
+ } else {
+ var protoVal = infer(prop.value, scope), known = protoVal.getObjType()
+ if (known) proto = known
+ else waitForProto = protoVal
+ }
+ }
+ }
+
+ var obj = node.objType = new Obj(proto, name);
+ if (waitForProto) waitForProto.propagate(new HasProto(obj))
+ obj.originNode = node;
+
+ withSuper(null, waitForProto || proto, function() {
+ for (var i = 0; i < node.properties.length; ++i) {
+ var prop = node.properties[i], key = prop.key;
+ if (prop.value.name == "✖" || prop.key.name == "__proto__") continue;
+
+ var name = propName(prop, scope), target
+ if (name == "<i>" || prop.kind == "set") {
+ target = ANull;
+ } else {
+ var val = target = obj.defProp(name, key);
+ val.initializer = true;
+ if (prop.kind == "get")
+ target = new IsCallee(obj, [], null, val);
+ }
+ infer(prop.value, scope, target, name);
+ if (prop.value.type == "FunctionExpression")
+ prop.value.scope.fnType.self.addType(obj, WG_SPECULATIVE_THIS);
+ }
+ })
+ return obj;
+ }),
+ FunctionExpression: ret(function(node, scope, name) {
+ var inner = node.scope, fn = inner.fnType;
+ if (name && !fn.name) fn.name = name;
+ connectParams(node, inner)
+ if (node.expression)
+ infer(node.body, inner, inner.fnType.retval = new AVal)
+ else
+ walk.recursive(node.body, inner, null, inferWrapper, "Statement")
+ if (node.type == "ArrowFunctionExpression") {
+ getThis(scope).propagate(fn.self)
+ fn.self = ANull
+ }
+ maybeTagAsInstantiated(node, fn) || maybeTagAsGeneric(fn);
+ if (node.id) inner.getProp(node.id.name).addType(fn);
+ return fn;
+ }),
+ ClassExpression: ret(inferClass),
+ SequenceExpression: ret(function(node, scope) {
+ for (var i = 0, l = node.expressions.length - 1; i < l; ++i)
+ infer(node.expressions[i], scope, ANull);
+ return infer(node.expressions[l], scope);
+ }),
+ UnaryExpression: ret(function(node, scope) {
+ infer(node.argument, scope, ANull);
+ return unopResultType(node.operator);
+ }),
+ UpdateExpression: ret(function(node, scope) {
+ infer(node.argument, scope, ANull);
+ return cx.num;
+ }),
+ BinaryExpression: ret(function(node, scope) {
+ if (node.operator == "+") {
+ var lhs = infer(node.left, scope);
+ var rhs = infer(node.right, scope);
+ if (lhs.hasType(cx.str) || rhs.hasType(cx.str)) return cx.str;
+ if (lhs.hasType(cx.num) && rhs.hasType(cx.num)) return cx.num;
+ var result = new AVal;
+ lhs.propagate(new IsAdded(rhs, result));
+ rhs.propagate(new IsAdded(lhs, result));
+ return result;
+ } else {
+ infer(node.left, scope, ANull);
+ infer(node.right, scope, ANull);
+ return binopIsBoolean(node.operator) ? cx.bool : cx.num;
+ }
+ }),
+ AssignmentExpression: ret(function(node, scope, name) {
+ var rhs, pName;
+ if (node.left.type == "MemberExpression") {
+ pName = propName(node.left, scope)
+ if (!name)
+ name = node.left.object.type == "Identifier" ? node.left.object.name + "." + pName : pName
+ } else if (!name && node.left.type == "Identifier") {
+ name = node.left.name
+ }
+
+ if (node.operator && node.operator != "=" && node.operator != "+=") {
+ infer(node.right, scope, ANull);
+ rhs = cx.num;
+ } else {
+ rhs = infer(node.right, scope, null, name);
+ }
+
+ if (node.left.type == "MemberExpression") {
+ var obj = infer(node.left.object, scope);
+ if (pName == "prototype") maybeInstantiate(scope, 20);
+ if (pName == "<i>") {
+ // This is a hack to recognize for/in loops that copy
+ // properties, and do the copying ourselves, insofar as we
+ // manage, because such loops tend to be relevant for type
+ // information.
+ var v = node.left.property.name, local = scope.props[v], over = local && local.iteratesOver;
+ if (over) {
+ maybeInstantiate(scope, 20);
+ var fromRight = node.right.type == "MemberExpression" && node.right.computed && node.right.property.name == v;
+ over.forAllProps(function(prop, val, local) {
+ if (local && prop != "prototype" && prop != "<i>")
+ obj.propagate(new DefProp(prop, fromRight ? val : ANull));
+ });
+ return rhs;
+ }
+ }
+
+ obj.propagate(new DefProp(pName, rhs, node.left.property));
+ maybeAddPhantomObj(obj)
+ if (node.right.type == "FunctionExpression")
+ obj.propagate(node.right.scope.fnType.self, WG_SPECULATIVE_THIS);
+ } else {
+ connectPattern(node.left, scope, rhs)
+ }
+ return rhs;
+ }),
+ LogicalExpression: fill(function(node, scope, out) {
+ infer(node.left, scope, out);
+ infer(node.right, scope, out);
+ }),
+ ConditionalExpression: fill(function(node, scope, out) {
+ infer(node.test, scope, ANull);
+ infer(node.consequent, scope, out);
+ infer(node.alternate, scope, out);
+ }),
+ NewExpression: fill(function(node, scope, out, name) {
+ if (node.callee.type == "Identifier" && node.callee.name in scope.props)
+ maybeInstantiate(scope, 20);
+
+ for (var i = 0, args = []; i < node.arguments.length; ++i)
+ args.push(infer(node.arguments[i], scope));
+ var callee = infer(node.callee, scope);
+ var self = new AVal;
+ callee.propagate(new IsCtor(self, name && /\.prototype$/.test(name)));
+ self.propagate(out, WG_NEW_INSTANCE);
+ callee.propagate(new IsCallee(self, args, node.arguments, new IfObj(out)));
+ }),
+ CallExpression: fill(function(node, scope, out) {
+ for (var i = 0, args = []; i < node.arguments.length; ++i)
+ args.push(infer(node.arguments[i], scope));
+ var outerFn = functionScope(scope).fnType
+ if (node.callee.type == "MemberExpression") {
+ var self = infer(node.callee.object, scope);
+ var pName = propName(node.callee, scope)
+ if (outerFn && (pName == "call" || pName == "apply") &&
+ outerFn.args.indexOf(self) > -1)
+ maybeInstantiate(scope, 30);
+ self.propagate(new HasMethodCall(pName, args, node.arguments, out));
+ } else if (node.callee.type == "Super" && cx.curSuperCtor) {
+ cx.curSuperCtor.propagate(new IsCallee(getThis(scope), args, node.arguments, out))
+ } else {
+ var callee = infer(node.callee, scope);
+ if (outerFn && outerFn.args.indexOf(callee) > -1)
+ maybeInstantiate(scope, 30);
+ var knownFn = callee.getFunctionType();
+ if (knownFn && knownFn.instantiateScore && outerFn)
+ maybeInstantiate(scope, knownFn.instantiateScore / 5);
+ callee.propagate(new IsCallee(cx.topScope, args, node.arguments, out));
+ }
+ }),
+ MemberExpression: fill(function(node, scope, out) {
+ var name = propName(node), wg;
+ if (name == "<i>") {
+ var propType = infer(node.property, scope)
+ var symName = symbolName(propType)
+ if (symName)
+ name = node.propName = symName
+ else if (!propType.hasType(cx.num))
+ wg = WG_MULTI_MEMBER
+ }
+ infer(node.object, scope).getProp(name).propagate(out, wg)
+ }),
+ Identifier: ret(function(node, scope) {
+ if (node.name == "arguments") {
+ var fnScope = functionScope(scope)
+ if (fnScope.fnType && !(node.name in fnScope.props))
+ scope.defProp(node.name, fnScope.fnType.originNode)
+ .addType(new Arr(fnScope.fnType.arguments = new AVal));
+ }
+ return scope.getProp(node.name);
+ }),
+ ThisExpression: ret(function(_node, scope) {
+ return getThis(scope)
+ }),
+ Super: ret(function(node) {
+ return node.superType = cx.curSuper || ANull
+ }),
+ Literal: ret(function(node) {
+ return literalType(node);
+ }),
+ TemplateLiteral: ret(function(node, scope) {
+ for (var i = 0; i < node.expressions.length; ++i)
+ infer(node.expressions[i], scope, ANull)
+ return cx.str
+ }),
+ TaggedTemplateExpression: fill(function(node, scope, out) {
+ var args = [new Arr(cx.str)]
+ for (var i = 0; i < node.quasi.expressions.length; ++i)
+ args.push(infer(node.quasi.expressions[i], scope))
+ infer(node.tag, scope, new IsCallee(cx.topScope, args, node.quasi.expressions, out))
+ }),
+ YieldExpression: ret(function(node, scope) {
+ var output = ANull, fn = functionScope(scope).fnType
+ if (fn) {
+ if (fn.retval == ANull) fn.retval = new AVal
+ if (!fn.yieldval) fn.yieldval = new AVal
+ output = fn.retval
+ }
+ if (node.argument) {
+ if (node.delegate) {
+ infer(node.argument, scope, new HasMethodCall("next", [], null,
+ new GetProp("value", output)))
+ } else {
+ infer(node.argument, scope, output)
+ }
+ }
+ return fn ? fn.yieldval : ANull
+ })
+ };
+ inferExprVisitor.ArrowFunctionExpression = inferExprVisitor.FunctionExpression
+
+ function infer(node, scope, out, name) {
+ var handler = inferExprVisitor[node.type];
+ return handler ? handler(node, scope, out, name) : ANull;
+ }
+
+ function loopPattern(init) {
+ return init.type == "VariableDeclaration" ? init.declarations[0].id : init
+ }
+
+ var inferWrapper = exports.inferWrapper = walk.make({
+ Expression: function(node, scope) {
+ infer(node, node.scope || scope, ANull);
+ },
+
+ FunctionDeclaration: function(node, scope, c) {
+ var inner = node.scope, fn = inner.fnType;
+ connectParams(node, inner)
+ c(node.body, inner, "Statement");
+ maybeTagAsInstantiated(node, fn) || maybeTagAsGeneric(fn);
+ scope.getProp(node.id.name).addType(fn)
+ },
+
+ Statement: function(node, scope, c) {
+ c(node, node.scope || scope)
+ },
+
+ VariableDeclaration: function(node, scope) {
+ for (var i = 0; i < node.declarations.length; ++i) {
+ var decl = node.declarations[i];
+ if (decl.id.type == "Identifier") {
+ var prop = scope.getProp(decl.id.name);
+ if (decl.init)
+ infer(decl.init, scope, prop, decl.id.name);
+ } else if (decl.init) {
+ connectPattern(decl.id, scope, infer(decl.init, scope))
+ }
+ }
+ },
+
+ ClassDeclaration: function(node, scope) {
+ scope.getProp(node.id.name).addType(inferClass(node, scope, node.id.name))
+ },
+
+ ReturnStatement: function(node, scope) {
+ if (!node.argument) return;
+ var output = ANull, fn = functionScope(scope).fnType
+ if (fn) {
+ if (fn.retval == ANull) fn.retval = new AVal;
+ output = fn.retval;
+ }
+ infer(node.argument, scope, output);
+ },
+
+ ForInStatement: function(node, scope, c) {
+ var source = infer(node.right, scope);
+ if ((node.right.type == "Identifier" && node.right.name in scope.props) ||
+ (node.right.type == "MemberExpression" && node.right.property.name == "prototype")) {
+ maybeInstantiate(scope, 5);
+ var pattern = loopPattern(node.left)
+ if (pattern.type == "Identifier") {
+ if (pattern.name in scope.props)
+ scope.getProp(pattern.name).iteratesOver = source
+ source.getProp("<i>").propagate(ensureVar(pattern, scope))
+ } else {
+ connectPattern(pattern, scope, source.getProp("<i>"))
+ }
+ }
+ c(node.body, scope, "Statement");
+ },
+
+ ForOfStatement: function(node, scope, c) {
+ var pattern = loopPattern(node.left), target
+ if (pattern.type == "Identifier")
+ target = ensureVar(pattern, scope)
+ else
+ connectPattern(pattern, scope, target = new AVal)
+ infer(node.right, scope, new HasMethodCall(":Symbol.iterator", [], null,
+ new HasMethodCall("next", [], null,
+ new GetProp("value", target))))
+ c(node.body, scope, "Statement")
+ }
+ });
+
+ // PARSING
+
+ var parse = exports.parse = function(text, options, thirdArg) {
+ if (!options || Array.isArray(options)) options = thirdArg
+ var ast;
+ try { ast = acorn.parse(text, options); }
+ catch(e) { ast = acorn_loose.parse_dammit(text, options); }
+ return ast;
+ };
+
+ // ANALYSIS INTERFACE
+
+ exports.analyze = function(ast, name, scope) {
+ if (typeof ast == "string") ast = parse(ast);
+
+ if (!name) name = "file#" + cx.origins.length;
+ exports.addOrigin(cx.curOrigin = name);
+
+ if (!scope) scope = cx.topScope;
+ cx.startAnalysis();
+
+ walk.recursive(ast, scope, null, scopeGatherer);
+ if (cx.parent) cx.parent.signal("preInfer", ast, scope)
+ walk.recursive(ast, scope, null, inferWrapper);
+ if (cx.parent) cx.parent.signal("postInfer", ast, scope)
+
+ cx.curOrigin = null;
+ };
+
+ // PURGING
+
+ exports.purge = function(origins, start, end) {
+ var test = makePredicate(origins, start, end);
+ ++cx.purgeGen;
+ cx.topScope.purge(test);
+ for (var prop in cx.props) {
+ var list = cx.props[prop];
+ for (var i = 0; i < list.length; ++i) {
+ var obj = list[i], av = obj.props[prop];
+ if (!av || test(av, av.originNode)) list.splice(i--, 1);
+ }
+ if (!list.length) delete cx.props[prop];
+ }
+ };
+
+ function makePredicate(origins, start, end) {
+ var arr = Array.isArray(origins);
+ if (arr && origins.length == 1) { origins = origins[0]; arr = false; }
+ if (arr) {
+ if (end == null) return function(n) { return origins.indexOf(n.origin) > -1; };
+ return function(n, pos) { return pos && pos.start >= start && pos.end <= end && origins.indexOf(n.origin) > -1; };
+ } else {
+ if (end == null) return function(n) { return n.origin == origins; };
+ return function(n, pos) { return pos && pos.start >= start && pos.end <= end && n.origin == origins; };
+ }
+ }
+
+ AVal.prototype.purge = function(test) {
+ if (this.purgeGen == cx.purgeGen) return;
+ this.purgeGen = cx.purgeGen;
+ for (var i = 0; i < this.types.length; ++i) {
+ var type = this.types[i];
+ if (test(type, type.originNode))
+ this.types.splice(i--, 1);
+ else
+ type.purge(test);
+ }
+ if (!this.types.length) this.maxWeight = 0;
+
+ if (this.forward) for (var i = 0; i < this.forward.length; ++i) {
+ var f = this.forward[i];
+ if (test(f)) {
+ this.forward.splice(i--, 1);
+ if (this.props) this.props = null;
+ } else if (f.purge) {
+ f.purge(test);
+ }
+ }
+ };
+ ANull.purge = function() {};
+ Obj.prototype.purge = function(test) {
+ if (this.purgeGen == cx.purgeGen) return true;
+ this.purgeGen = cx.purgeGen;
+ for (var p in this.props) {
+ var av = this.props[p];
+ if (test(av, av.originNode))
+ this.removeProp(p);
+ av.purge(test);
+ }
+ };
+ Fn.prototype.purge = function(test) {
+ if (Obj.prototype.purge.call(this, test)) return;
+ this.self.purge(test);
+ this.retval.purge(test);
+ for (var i = 0; i < this.args.length; ++i) this.args[i].purge(test);
+ };
+
+ // EXPRESSION TYPE DETERMINATION
+
+ function findByPropertyName(name) {
+ guessing = true;
+ var found = objsWithProp(name);
+ if (found) for (var i = 0; i < found.length; ++i) {
+ var val = found[i].getProp(name);
+ if (!val.isEmpty()) return val;
+ }
+ return ANull;
+ }
+
+ function generatorResult(input, output) {
+ var retObj = new Obj(true)
+ retObj.defProp("done").addType(cx.bool)
+ output.propagate(retObj.defProp("value"))
+ var method = new Fn(null, ANull, input ? [input] : [], input ? ["?"] : [], retObj)
+ var result = new Obj(cx.definitions.ecma6 && cx.definitions.ecma6.generator_prototype || true)
+ result.defProp("next").addType(method)
+ return result
+ }
+
+ function maybeIterator(fn, output) {
+ if (!fn.generator) return output
+ if (!fn.computeRet) { // Reuse iterator objects for non-computed return types
+ if (fn.generator === true) fn.generator = generatorResult(fn.yieldval, output)
+ return fn.generator
+ }
+ return generatorResult(fn.yieldval, output)
+ }
+
+ function computeReturnType(funcNode, argNodes, scope) {
+ var fn = findType(funcNode, scope).getFunctionType()
+ if (!fn) return ANull
+ var result = fn.retval
+ if (fn.computeRet) {
+ for (var i = 0, args = []; i < argNodes.length; ++i)
+ args.push(findType(argNodes[i], scope))
+ var self = ANull
+ if (funcNode.type == "MemberExpression")
+ self = findType(funcNode.object, scope)
+ result = fn.computeRet(self, args, argNodes);
+ }
+ return maybeIterator(fn, result)
+ }
+
+ var typeFinder = exports.typeFinder = {
+ ArrayExpression: function(node, scope) {
+ return arrayLiteralType(node.elements, scope, findType)
+ },
+ ObjectExpression: function(node) {
+ return node.objType;
+ },
+ ClassExpression: function(node) {
+ return node.objType;
+ },
+ FunctionExpression: function(node) {
+ return node.scope.fnType;
+ },
+ ArrowFunctionExpression: function(node) {
+ return node.scope.fnType;
+ },
+ SequenceExpression: function(node, scope) {
+ return findType(node.expressions[node.expressions.length-1], scope);
+ },
+ UnaryExpression: function(node) {
+ return unopResultType(node.operator);
+ },
+ UpdateExpression: function() {
+ return cx.num;
+ },
+ BinaryExpression: function(node, scope) {
+ if (binopIsBoolean(node.operator)) return cx.bool;
+ if (node.operator == "+") {
+ var lhs = findType(node.left, scope);
+ var rhs = findType(node.right, scope);
+ if (lhs.hasType(cx.str) || rhs.hasType(cx.str)) return cx.str;
+ }
+ return cx.num;
+ },
+ AssignmentExpression: function(node, scope) {
+ return findType(node.right, scope);
+ },
+ LogicalExpression: function(node, scope) {
+ var lhs = findType(node.left, scope);
+ return lhs.isEmpty() ? findType(node.right, scope) : lhs;
+ },
+ ConditionalExpression: function(node, scope) {
+ var lhs = findType(node.consequent, scope);
+ return lhs.isEmpty() ? findType(node.alternate, scope) : lhs;
+ },
+ NewExpression: function(node, scope) {
+ var f = findType(node.callee, scope).getFunctionType();
+ var proto = f && f.getProp("prototype").getObjType();
+ if (!proto) return ANull;
+ return getInstance(proto, f);
+ },
+ CallExpression: function(node, scope) {
+ return computeReturnType(node.callee, node.arguments, scope)
+ },
+ MemberExpression: function(node, scope) {
+ var propN = propName(node), obj = findType(node.object, scope).getType();
+ if (obj) return obj.getProp(propN);
+ if (propN == "<i>") return ANull;
+ return findByPropertyName(propN);
+ },
+ Identifier: function(node, scope) {
+ return scope.hasProp(node.name) || ANull;
+ },
+ ThisExpression: function(_node, scope) {
+ return getThis(scope)
+ },
+ Literal: function(node) {
+ return literalType(node);
+ },
+ Super: ret(function(node) {
+ return node.superType
+ }),
+ TemplateLiteral: function() {
+ return cx.str
+ },
+ TaggedTemplateExpression: function(node, scope) {
+ return computeReturnType(node.tag, node.quasi.expressions, scope)
+ },
+ YieldExpression: function(_node, scope) {
+ var fn = functionScope(scope).fnType
+ return fn ? fn.yieldval : ANull
+ }
+ };
+
+ function findType(node, scope) {
+ var finder = typeFinder[node.type];
+ return finder ? finder(node, scope) : ANull;
+ }
+
+ var searchVisitor = exports.searchVisitor = walk.make({
+ Function: function(node, _st, c) {
+ walk.base.Function(node, node.scope, c)
+ },
+ Property: function(node, st, c) {
+ if (node.computed) c(node.key, st, "Expression");
+ if (node.key != node.value) c(node.value, st, "Expression");
+ },
+ Statement: function(node, st, c) {
+ c(node, node.scope || st)
+ },
+ ImportSpecifier: function(node, st, c) {
+ c(node.local, st)
+ },
+ ImportDefaultSpecifier: function(node, st, c) {
+ c(node.local, st)
+ },
+ ImportNamespaceSpecifier: function(node, st, c) {
+ c(node.local, st)
+ }
+ });
+ exports.fullVisitor = walk.make({
+ MemberExpression: function(node, st, c) {
+ c(node.object, st, "Expression");
+ c(node.property, st, node.computed ? "Expression" : null);
+ },
+ ObjectExpression: function(node, st, c) {
+ for (var i = 0; i < node.properties.length; ++i) {
+ c(node.properties[i].value, st, "Expression");
+ c(node.properties[i].key, st);
+ }
+ }
+ }, searchVisitor);
+
+ exports.findExpressionAt = function(ast, start, end, defaultScope, filter) {
+ var test = filter || function(_t, node) {
+ if (node.type == "Identifier" && node.name == "✖") return false;
+ return typeFinder.hasOwnProperty(node.type);
+ };
+ return walk.findNodeAt(ast, start, end, test, searchVisitor, defaultScope || cx.topScope);
+ };
+
+ exports.findExpressionAround = function(ast, start, end, defaultScope, filter) {
+ var test = filter || function(_t, node) {
+ if (start != null && node.start > start) return false;
+ if (node.type == "Identifier" && node.name == "✖") return false;
+ return typeFinder.hasOwnProperty(node.type);
+ };
+ return walk.findNodeAround(ast, end, test, searchVisitor, defaultScope || cx.topScope);
+ };
+
+ exports.expressionType = function(found) {
+ return findType(found.node, found.state);
+ };
+
+ // Finding the expected type of something, from context
+
+ exports.parentNode = function(child, ast) {
+ var stack = [];
+ function c(node, st, override) {
+ if (node.start <= child.start && node.end >= child.end) {
+ var top = stack[stack.length - 1];
+ if (node == child) throw {found: top};
+ if (top != node) stack.push(node);
+ walk.base[override || node.type](node, st, c);
+ if (top != node) stack.pop();
+ }
+ }
+ try {
+ c(ast, null);
+ } catch (e) {
+ if (e.found) return e.found;
+ throw e;
+ }
+ };
+
+ var findTypeFromContext = exports.findTypeFromContext = {
+ ArrayExpression: function(parent, _, get) { return get(parent, true).getProp("<i>"); },
+ ObjectExpression: function(parent, node, get) {
+ for (var i = 0; i < parent.properties.length; ++i) {
+ var prop = node.properties[i];
+ if (prop.value == node)
+ return get(parent, true).getProp(prop.key.name);
+ }
+ },
+ UnaryExpression: function(parent) { return unopResultType(parent.operator); },
+ UpdateExpression: function() { return cx.num; },
+ BinaryExpression: function(parent) { return binopIsBoolean(parent.operator) ? cx.bool : cx.num; },
+ AssignmentExpression: function(parent, _, get) { return get(parent.left); },
+ LogicalExpression: function(parent, _, get) { return get(parent, true); },
+ ConditionalExpression: function(parent, node, get) {
+ if (parent.consequent == node || parent.alternate == node) return get(parent, true);
+ },
+ CallExpression: function(parent, node, get) {
+ for (var i = 0; i < parent.arguments.length; i++) {
+ var arg = parent.arguments[i];
+ if (arg == node) {
+ var calleeType = get(parent.callee).getFunctionType();
+ if (calleeType instanceof Fn)
+ return calleeType.args[i];
+ break;
+ }
+ }
+ },
+ ReturnStatement: function(_parent, node, get) {
+ var fnNode = walk.findNodeAround(node.sourceFile.ast, node.start, "Function");
+ if (fnNode) {
+ var fnType = fnNode.node.type != "FunctionDeclaration"
+ ? get(fnNode.node, true).getFunctionType()
+ : fnNode.node.scope.fnType;
+ if (fnType) return fnType.retval.getType();
+ }
+ },
+ VariableDeclarator: function(parent, node, get) {
+ if (parent.init == node) return get(parent.id)
+ }
+ };
+ findTypeFromContext.NewExpression = findTypeFromContext.CallExpression
+
+ exports.typeFromContext = function(ast, found) {
+ var parent = exports.parentNode(found.node, ast);
+ var type = null;
+ if (findTypeFromContext.hasOwnProperty(parent.type)) {
+ var finder = findTypeFromContext[parent.type];
+ type = finder && finder(parent, found.node, function(node, fromContext) {
+ var obj = {node: node, state: found.state};
+ var tp = fromContext ? exports.typeFromContext(ast, obj) : exports.expressionType(obj);
+ return tp || ANull;
+ });
+ }
+ return type || exports.expressionType(found);
+ };
+
+ // Flag used to indicate that some wild guessing was used to produce
+ // a type or set of completions.
+ var guessing = false;
+
+ exports.resetGuessing = function(val) { guessing = val; };
+ exports.didGuess = function() { return guessing; };
+
+ exports.forAllPropertiesOf = function(type, f) {
+ type.gatherProperties(f, 0);
+ };
+
+ var refFindWalker = walk.make({}, searchVisitor);
+
+ exports.findRefs = function(ast, baseScope, name, refScope, f) {
+ refFindWalker.Identifier = refFindWalker.VariablePattern = function(node, scope) {
+ if (node.name != name) return;
+ for (var s = scope; s; s = s.prev) {
+ if (s == refScope) f(node, scope);
+ if (name in s.props) return;
+ }
+ };
+ walk.recursive(ast, baseScope, null, refFindWalker);
+ };
+
+ var simpleWalker = walk.make({
+ Function: function(node, _scope, c) {
+ c(node.body, node.scope, node.expression ? "Expression" : "Statement")
+ },
+ Statement: function(node, scope, c) {
+ c(node, node.scope || scope)
+ }
+ });
+
+ exports.findPropRefs = function(ast, scope, objType, propName, f) {
+ walk.simple(ast, {
+ MemberExpression: function(node, scope) {
+ if (node.computed || node.property.name != propName) return;
+ if (findType(node.object, scope).getType() == objType) f(node.property);
+ },
+ ObjectExpression: function(node, scope) {
+ if (findType(node, scope).getType() != objType) return;
+ for (var i = 0; i < node.properties.length; ++i)
+ if (node.properties[i].key.name == propName) f(node.properties[i].key);
+ }
+ }, simpleWalker, scope);
+ };
+
+ // LOCAL-VARIABLE QUERIES
+
+ var scopeAt = exports.scopeAt = function(ast, pos, defaultScope) {
+ var found = walk.findNodeAround(ast, pos, function(_, node) {
+ return node.scope;
+ });
+ if (found) return found.node.scope;
+ else return defaultScope || cx.topScope;
+ };
+
+ exports.forAllLocalsAt = function(ast, pos, defaultScope, f) {
+ var scope = scopeAt(ast, pos, defaultScope);
+ scope.gatherProperties(f, 0);
+ };
+
+ // INIT DEF MODULE
+
+ // Delayed initialization because of cyclic dependencies.
+ def = exports.def = def.init({}, exports);
+});
diff --git a/devtools/client/sourceeditor/tern/moz.build b/devtools/client/sourceeditor/tern/moz.build
new file mode 100644
index 000000000..cba3f5a1b
--- /dev/null
+++ b/devtools/client/sourceeditor/tern/moz.build
@@ -0,0 +1,18 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+XPCSHELL_TESTS_MANIFESTS += ['tests/unit/xpcshell.ini']
+
+DevToolsModules(
+ 'browser.js',
+ 'comment.js',
+ 'condense.js',
+ 'def.js',
+ 'ecma5.js',
+ 'infer.js',
+ 'signal.js',
+ 'tern.js',
+)
diff --git a/devtools/client/sourceeditor/tern/signal.js b/devtools/client/sourceeditor/tern/signal.js
new file mode 100755
index 000000000..5ea64a438
--- /dev/null
+++ b/devtools/client/sourceeditor/tern/signal.js
@@ -0,0 +1,51 @@
+(function(root, mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ return mod(exports);
+ if (typeof define == "function" && define.amd) // AMD
+ return define(["exports"], mod);
+ mod((root.tern || (root.tern = {})).signal = {}); // Plain browser env
+})(this, function(exports) {
+
+ function on(type, f) {
+ var handlers = this._handlers || (this._handlers = Object.create(null));
+ (handlers[type] || (handlers[type] = [])).push(f);
+ }
+
+ function off(type, f) {
+ var arr = this._handlers && this._handlers[type];
+ if (arr) for (var i = 0; i < arr.length; ++i)
+ if (arr[i] == f) { arr.splice(i, 1); break; }
+ }
+
+ var noHandlers = []
+ function getHandlers(emitter, type) {
+ var arr = emitter._handlers && emitter._handlers[type];
+ return arr && arr.length ? arr.slice() : noHandlers
+ }
+
+ function signal(type, a1, a2, a3, a4) {
+ var arr = getHandlers(this, type)
+ for (var i = 0; i < arr.length; ++i) arr[i].call(this, a1, a2, a3, a4)
+ }
+
+ function signalReturnFirst(type, a1, a2, a3, a4) {
+ var arr = getHandlers(this, type)
+ for (var i = 0; i < arr.length; ++i) {
+ var result = arr[i].call(this, a1, a2, a3, a4)
+ if (result) return result
+ }
+ }
+
+ function hasHandler(type) {
+ var arr = this._handlers && this._handlers[type]
+ return arr && arr.length > 0 && arr
+ }
+
+ exports.mixin = function(obj) {
+ obj.on = on; obj.off = off;
+ obj.signal = signal;
+ obj.signalReturnFirst = signalReturnFirst;
+ obj.hasHandler = hasHandler;
+ return obj;
+ };
+});
diff --git a/devtools/client/sourceeditor/tern/tern.js b/devtools/client/sourceeditor/tern/tern.js
new file mode 100755
index 000000000..327797174
--- /dev/null
+++ b/devtools/client/sourceeditor/tern/tern.js
@@ -0,0 +1,1056 @@
+// The Tern server object
+
+// A server is a stateful object that manages the analysis for a
+// project, and defines an interface for querying the code in the
+// project.
+
+(function(root, mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ return mod(exports, require("./infer"), require("./signal"),
+ require("acorn/acorn"), require("acorn/walk"));
+ if (typeof define == "function" && define.amd) // AMD
+ return define(["exports", "./infer", "./signal", "acorn/acorn", "acorn/walk"], mod);
+ mod(root.tern || (root.tern = {}), tern, tern.signal, acorn, acorn.walk); // Plain browser env
+})(this, function(exports, infer, signal, acorn, walk) {
+ "use strict";
+
+ var plugins = Object.create(null);
+ exports.registerPlugin = function(name, init) { plugins[name] = init; };
+
+ var defaultOptions = exports.defaultOptions = {
+ debug: false,
+ async: false,
+ getFile: function(_f, c) { if (this.async) c(null, null); },
+ normalizeFilename: function(name) { return name },
+ defs: [],
+ plugins: {},
+ fetchTimeout: 1000,
+ dependencyBudget: 20000,
+ reuseInstances: true,
+ stripCRs: false,
+ ecmaVersion: 6,
+ projectDir: "/"
+ };
+
+ var queryTypes = {
+ completions: {
+ takesFile: true,
+ run: findCompletions
+ },
+ properties: {
+ run: findProperties
+ },
+ type: {
+ takesFile: true,
+ run: findTypeAt
+ },
+ documentation: {
+ takesFile: true,
+ run: findDocs
+ },
+ definition: {
+ takesFile: true,
+ run: findDef
+ },
+ refs: {
+ takesFile: true,
+ fullFile: true,
+ run: findRefs
+ },
+ rename: {
+ takesFile: true,
+ fullFile: true,
+ run: buildRename
+ },
+ files: {
+ run: listFiles
+ }
+ };
+
+ exports.defineQueryType = function(name, desc) { queryTypes[name] = desc; };
+
+ function File(name, parent) {
+ this.name = name;
+ this.parent = parent;
+ this.scope = this.text = this.ast = this.lineOffsets = null;
+ }
+ File.prototype.asLineChar = function(pos) { return asLineChar(this, pos); };
+
+ function parseFile(srv, file) {
+ var options = {
+ directSourceFile: file,
+ allowReturnOutsideFunction: true,
+ allowImportExportEverywhere: true,
+ ecmaVersion: srv.options.ecmaVersion
+ }
+ var text = srv.signalReturnFirst("preParse", file.text, options) || file.text
+ var ast = infer.parse(text, options)
+ srv.signal("postParse", ast, text)
+ return ast
+ }
+
+ function updateText(file, text, srv) {
+ file.text = srv.options.stripCRs ? text.replace(/\r\n/g, "\n") : text;
+ infer.withContext(srv.cx, function() {
+ file.ast = parseFile(srv, file)
+ });
+ file.lineOffsets = null;
+ }
+
+ var Server = exports.Server = function(options) {
+ this.cx = null;
+ this.options = options || {};
+ for (var o in defaultOptions) if (!options.hasOwnProperty(o))
+ options[o] = defaultOptions[o];
+
+ this.projectDir = options.projectDir.replace(/\\/g, "/")
+ if (!/\/$/.test(this.projectDir)) this.projectDir += "/"
+
+ this.handlers = Object.create(null);
+ this.files = [];
+ this.fileMap = Object.create(null);
+ this.needsPurge = [];
+ this.budgets = Object.create(null);
+ this.uses = 0;
+ this.pending = 0;
+ this.asyncError = null;
+ this.mod = {}
+
+ this.defs = options.defs.slice(0)
+ this.plugins = Object.create(null)
+ for (var plugin in options.plugins) if (options.plugins.hasOwnProperty(plugin))
+ this.loadPlugin(plugin, options.plugins[plugin])
+
+ this.reset();
+ };
+ Server.prototype = signal.mixin({
+ addFile: function(name, /*optional*/ text, parent) {
+ // Don't crash when sloppy plugins pass non-existent parent ids
+ if (parent && !(parent in this.fileMap)) parent = null;
+ if (!(name in this.fileMap))
+ name = this.normalizeFilename(name)
+ ensureFile(this, name, parent, text);
+ },
+ delFile: function(name) {
+ var file = this.findFile(name);
+ if (file) {
+ this.needsPurge.push(file.name);
+ this.files.splice(this.files.indexOf(file), 1);
+ delete this.fileMap[name];
+ }
+ },
+ reset: function() {
+ this.signal("reset");
+ this.cx = new infer.Context(this.defs, this);
+ this.uses = 0;
+ this.budgets = Object.create(null);
+ for (var i = 0; i < this.files.length; ++i) {
+ var file = this.files[i];
+ file.scope = null;
+ }
+ this.signal("postReset");
+ },
+
+ request: function(doc, c) {
+ var inv = invalidDoc(doc);
+ if (inv) return c(inv);
+
+ var self = this;
+ doRequest(this, doc, function(err, data) {
+ c(err, data);
+ if (self.uses > 40) {
+ self.reset();
+ analyzeAll(self, null, function(){});
+ }
+ });
+ },
+
+ findFile: function(name) {
+ return this.fileMap[name];
+ },
+
+ flush: function(c) {
+ var cx = this.cx;
+ analyzeAll(this, null, function(err) {
+ if (err) return c(err);
+ infer.withContext(cx, c);
+ });
+ },
+
+ startAsyncAction: function() {
+ ++this.pending;
+ },
+ finishAsyncAction: function(err) {
+ if (err) this.asyncError = err;
+ if (--this.pending === 0) this.signal("everythingFetched");
+ },
+
+ addDefs: function(defs, toFront) {
+ if (toFront) this.defs.unshift(defs)
+ else this.defs.push(defs)
+
+ if (this.cx) this.reset()
+ },
+
+ loadPlugin: function(name, options) {
+ if (arguments.length == 1) options = this.options.plugins[name] || true
+ if (name in this.plugins || !(name in plugins) || !options) return
+ this.plugins[name] = true
+ var init = plugins[name](this, options)
+
+ // This is for backwards-compatibilty. Don't rely on it -- use addDef and on directly
+ if (!init) return
+ if (init.defs) this.addDefs(init.defs, init.loadFirst)
+ if (init.passes) for (var type in init.passes) if (init.passes.hasOwnProperty(type))
+ this.on(type, init.passes[type])
+ },
+
+ normalizeFilename: function(name) {
+ var norm = this.options.normalizeFilename(name).replace(/\\/g, "/")
+ if (norm.indexOf(this.projectDir) == 0) norm = norm.slice(this.projectDir.length)
+ return norm
+ }
+ });
+
+ function doRequest(srv, doc, c) {
+ if (doc.query && !queryTypes.hasOwnProperty(doc.query.type))
+ return c("No query type '" + doc.query.type + "' defined");
+
+ var query = doc.query;
+ // Respond as soon as possible when this just uploads files
+ if (!query) c(null, {});
+
+ var files = doc.files || [];
+ if (files.length) ++srv.uses;
+ for (var i = 0; i < files.length; ++i) {
+ var file = files[i];
+ if (file.type == "delete")
+ srv.delFile(file.name);
+ else
+ ensureFile(srv, file.name, null, file.type == "full" ? file.text : null);
+ }
+
+ var timeBudget = typeof doc.timeout == "number" ? [doc.timeout] : null;
+ if (!query) {
+ analyzeAll(srv, timeBudget, function(){});
+ return;
+ }
+
+ var queryType = queryTypes[query.type];
+ if (queryType.takesFile) {
+ if (typeof query.file != "string") return c(".query.file must be a string");
+ if (!/^#/.test(query.file)) ensureFile(srv, query.file, null);
+ }
+
+ analyzeAll(srv, timeBudget, function(err) {
+ if (err) return c(err);
+ var file = queryType.takesFile && resolveFile(srv, files, query.file);
+ if (queryType.fullFile && file.type == "part")
+ return c("Can't run a " + query.type + " query on a file fragment");
+
+ function run() {
+ var result;
+ try {
+ result = queryType.run(srv, query, file);
+ } catch (e) {
+ if (srv.options.debug && e.name != "TernError") console.error(e.stack);
+ return c(e);
+ }
+ c(null, result);
+ }
+ infer.resetGuessing()
+ infer.withContext(srv.cx, timeBudget ? function() { infer.withTimeout(timeBudget[0], run); } : run);
+ });
+ }
+
+ function analyzeFile(srv, file) {
+ infer.withContext(srv.cx, function() {
+ file.scope = srv.cx.topScope;
+ srv.signal("beforeLoad", file);
+ infer.analyze(file.ast, file.name, file.scope);
+ srv.signal("afterLoad", file);
+ });
+ return file;
+ }
+
+ function ensureFile(srv, name, parent, text) {
+ var known = srv.findFile(name);
+ if (known) {
+ if (text != null) {
+ if (known.scope) {
+ srv.needsPurge.push(name);
+ known.scope = null;
+ }
+ updateText(known, text, srv);
+ }
+ if (parentDepth(srv, known.parent) > parentDepth(srv, parent)) {
+ known.parent = parent;
+ if (known.excluded) known.excluded = null;
+ }
+ return;
+ }
+
+ var file = new File(name, parent);
+ srv.files.push(file);
+ srv.fileMap[name] = file;
+ if (text != null) {
+ updateText(file, text, srv);
+ } else if (srv.options.async) {
+ srv.startAsyncAction();
+ srv.options.getFile(name, function(err, text) {
+ updateText(file, text || "", srv);
+ srv.finishAsyncAction(err);
+ });
+ } else {
+ updateText(file, srv.options.getFile(name) || "", srv);
+ }
+ }
+
+ function fetchAll(srv, c) {
+ var done = true, returned = false;
+ srv.files.forEach(function(file) {
+ if (file.text != null) return;
+ if (srv.options.async) {
+ done = false;
+ srv.options.getFile(file.name, function(err, text) {
+ if (err && !returned) { returned = true; return c(err); }
+ updateText(file, text || "", srv);
+ fetchAll(srv, c);
+ });
+ } else {
+ try {
+ updateText(file, srv.options.getFile(file.name) || "", srv);
+ } catch (e) { return c(e); }
+ }
+ });
+ if (done) c();
+ }
+
+ function waitOnFetch(srv, timeBudget, c) {
+ var done = function() {
+ srv.off("everythingFetched", done);
+ clearTimeout(timeout);
+ analyzeAll(srv, timeBudget, c);
+ };
+ srv.on("everythingFetched", done);
+ var timeout = setTimeout(done, srv.options.fetchTimeout);
+ }
+
+ function analyzeAll(srv, timeBudget, c) {
+ if (srv.pending) return waitOnFetch(srv, timeBudget, c);
+
+ var e = srv.fetchError;
+ if (e) { srv.fetchError = null; return c(e); }
+
+ if (srv.needsPurge.length > 0) infer.withContext(srv.cx, function() {
+ infer.purge(srv.needsPurge);
+ srv.needsPurge.length = 0;
+ });
+
+ var done = true;
+ // The second inner loop might add new files. The outer loop keeps
+ // repeating both inner loops until all files have been looked at.
+ for (var i = 0; i < srv.files.length;) {
+ var toAnalyze = [];
+ for (; i < srv.files.length; ++i) {
+ var file = srv.files[i];
+ if (file.text == null) done = false;
+ else if (file.scope == null && !file.excluded) toAnalyze.push(file);
+ }
+ toAnalyze.sort(function(a, b) {
+ return parentDepth(srv, a.parent) - parentDepth(srv, b.parent);
+ });
+ for (var j = 0; j < toAnalyze.length; j++) {
+ var file = toAnalyze[j];
+ if (file.parent && !chargeOnBudget(srv, file)) {
+ file.excluded = true;
+ } else if (timeBudget) {
+ var startTime = +new Date;
+ infer.withTimeout(timeBudget[0], function() { analyzeFile(srv, file); });
+ timeBudget[0] -= +new Date - startTime;
+ } else {
+ analyzeFile(srv, file);
+ }
+ }
+ }
+ if (done) c();
+ else waitOnFetch(srv, timeBudget, c);
+ }
+
+ function firstLine(str) {
+ var end = str.indexOf("\n");
+ if (end < 0) return str;
+ return str.slice(0, end);
+ }
+
+ function findMatchingPosition(line, file, near) {
+ var pos = Math.max(0, near - 500), closest = null;
+ if (!/^\s*$/.test(line)) for (;;) {
+ var found = file.indexOf(line, pos);
+ if (found < 0 || found > near + 500) break;
+ if (closest == null || Math.abs(closest - near) > Math.abs(found - near))
+ closest = found;
+ pos = found + line.length;
+ }
+ return closest;
+ }
+
+ function scopeDepth(s) {
+ for (var i = 0; s; ++i, s = s.prev) {}
+ return i;
+ }
+
+ function ternError(msg) {
+ var err = new Error(msg);
+ err.name = "TernError";
+ return err;
+ }
+
+ function resolveFile(srv, localFiles, name) {
+ var isRef = name.match(/^#(\d+)$/);
+ if (!isRef) return srv.findFile(name);
+
+ var file = localFiles[isRef[1]];
+ if (!file || file.type == "delete") throw ternError("Reference to unknown file " + name);
+ if (file.type == "full") return srv.findFile(file.name);
+
+ // This is a partial file
+
+ var realFile = file.backing = srv.findFile(file.name);
+ var offset = file.offset;
+ if (file.offsetLines) offset = {line: file.offsetLines, ch: 0};
+ file.offset = offset = resolvePos(realFile, file.offsetLines == null ? file.offset : {line: file.offsetLines, ch: 0}, true);
+ var line = firstLine(file.text);
+ var foundPos = findMatchingPosition(line, realFile.text, offset);
+ var pos = foundPos == null ? Math.max(0, realFile.text.lastIndexOf("\n", offset)) : foundPos;
+ var inObject, atFunction;
+
+ infer.withContext(srv.cx, function() {
+ infer.purge(file.name, pos, pos + file.text.length);
+
+ var text = file.text, m;
+ if (m = text.match(/(?:"([^"]*)"|([\w$]+))\s*:\s*function\b/)) {
+ var objNode = walk.findNodeAround(file.backing.ast, pos, "ObjectExpression");
+ if (objNode && objNode.node.objType)
+ inObject = {type: objNode.node.objType, prop: m[2] || m[1]};
+ }
+ if (foundPos && (m = line.match(/^(.*?)\bfunction\b/))) {
+ var cut = m[1].length, white = "";
+ for (var i = 0; i < cut; ++i) white += " ";
+ file.text = white + text.slice(cut);
+ atFunction = true;
+ }
+
+ var scopeStart = infer.scopeAt(realFile.ast, pos, realFile.scope);
+ var scopeEnd = infer.scopeAt(realFile.ast, pos + text.length, realFile.scope);
+ var scope = file.scope = scopeDepth(scopeStart) < scopeDepth(scopeEnd) ? scopeEnd : scopeStart;
+ file.ast = parseFile(srv, file)
+ infer.analyze(file.ast, file.name, scope);
+
+ // This is a kludge to tie together the function types (if any)
+ // outside and inside of the fragment, so that arguments and
+ // return values have some information known about them.
+ tieTogether: if (inObject || atFunction) {
+ var newInner = infer.scopeAt(file.ast, line.length, scopeStart);
+ if (!newInner.fnType) break tieTogether;
+ if (inObject) {
+ var prop = inObject.type.getProp(inObject.prop);
+ prop.addType(newInner.fnType);
+ } else if (atFunction) {
+ var inner = infer.scopeAt(realFile.ast, pos + line.length, realFile.scope);
+ if (inner == scopeStart || !inner.fnType) break tieTogether;
+ var fOld = inner.fnType, fNew = newInner.fnType;
+ if (!fNew || (fNew.name != fOld.name && fOld.name)) break tieTogether;
+ for (var i = 0, e = Math.min(fOld.args.length, fNew.args.length); i < e; ++i)
+ fOld.args[i].propagate(fNew.args[i]);
+ fOld.self.propagate(fNew.self);
+ fNew.retval.propagate(fOld.retval);
+ }
+ }
+ });
+ return file;
+ }
+
+ // Budget management
+
+ function astSize(node) {
+ var size = 0;
+ walk.simple(node, {Expression: function() { ++size; }});
+ return size;
+ }
+
+ function parentDepth(srv, parent) {
+ var depth = 0;
+ while (parent) {
+ parent = srv.findFile(parent).parent;
+ ++depth;
+ }
+ return depth;
+ }
+
+ function budgetName(srv, file) {
+ for (;;) {
+ var parent = srv.findFile(file.parent);
+ if (!parent.parent) break;
+ file = parent;
+ }
+ return file.name;
+ }
+
+ function chargeOnBudget(srv, file) {
+ var bName = budgetName(srv, file);
+ var size = astSize(file.ast);
+ var known = srv.budgets[bName];
+ if (known == null)
+ known = srv.budgets[bName] = srv.options.dependencyBudget;
+ if (known < size) return false;
+ srv.budgets[bName] = known - size;
+ return true;
+ }
+
+ // Query helpers
+
+ function isPosition(val) {
+ return typeof val == "number" || typeof val == "object" &&
+ typeof val.line == "number" && typeof val.ch == "number";
+ }
+
+ // Baseline query document validation
+ function invalidDoc(doc) {
+ if (doc.query) {
+ if (typeof doc.query.type != "string") return ".query.type must be a string";
+ if (doc.query.start && !isPosition(doc.query.start)) return ".query.start must be a position";
+ if (doc.query.end && !isPosition(doc.query.end)) return ".query.end must be a position";
+ }
+ if (doc.files) {
+ if (!Array.isArray(doc.files)) return "Files property must be an array";
+ for (var i = 0; i < doc.files.length; ++i) {
+ var file = doc.files[i];
+ if (typeof file != "object") return ".files[n] must be objects";
+ else if (typeof file.name != "string") return ".files[n].name must be a string";
+ else if (file.type == "delete") continue;
+ else if (typeof file.text != "string") return ".files[n].text must be a string";
+ else if (file.type == "part") {
+ if (!isPosition(file.offset) && typeof file.offsetLines != "number")
+ return ".files[n].offset must be a position";
+ } else if (file.type != "full") return ".files[n].type must be \"full\" or \"part\"";
+ }
+ }
+ }
+
+ var offsetSkipLines = 25;
+
+ function findLineStart(file, line) {
+ var text = file.text, offsets = file.lineOffsets || (file.lineOffsets = [0]);
+ var pos = 0, curLine = 0;
+ var storePos = Math.min(Math.floor(line / offsetSkipLines), offsets.length - 1);
+ var pos = offsets[storePos], curLine = storePos * offsetSkipLines;
+
+ while (curLine < line) {
+ ++curLine;
+ pos = text.indexOf("\n", pos) + 1;
+ if (pos === 0) return null;
+ if (curLine % offsetSkipLines === 0) offsets.push(pos);
+ }
+ return pos;
+ }
+
+ var resolvePos = exports.resolvePos = function(file, pos, tolerant) {
+ if (typeof pos != "number") {
+ var lineStart = findLineStart(file, pos.line);
+ if (lineStart == null) {
+ if (tolerant) pos = file.text.length;
+ else throw ternError("File doesn't contain a line " + pos.line);
+ } else {
+ pos = lineStart + pos.ch;
+ }
+ }
+ if (pos > file.text.length) {
+ if (tolerant) pos = file.text.length;
+ else throw ternError("Position " + pos + " is outside of file.");
+ }
+ return pos;
+ };
+
+ function asLineChar(file, pos) {
+ if (!file) return {line: 0, ch: 0};
+ var offsets = file.lineOffsets || (file.lineOffsets = [0]);
+ var text = file.text, line, lineStart;
+ for (var i = offsets.length - 1; i >= 0; --i) if (offsets[i] <= pos) {
+ line = i * offsetSkipLines;
+ lineStart = offsets[i];
+ }
+ for (;;) {
+ var eol = text.indexOf("\n", lineStart);
+ if (eol >= pos || eol < 0) break;
+ lineStart = eol + 1;
+ ++line;
+ }
+ return {line: line, ch: pos - lineStart};
+ }
+
+ var outputPos = exports.outputPos = function(query, file, pos) {
+ if (query.lineCharPositions) {
+ var out = asLineChar(file, pos);
+ if (file.type == "part")
+ out.line += file.offsetLines != null ? file.offsetLines : asLineChar(file.backing, file.offset).line;
+ return out;
+ } else {
+ return pos + (file.type == "part" ? file.offset : 0);
+ }
+ };
+
+ // Delete empty fields from result objects
+ function clean(obj) {
+ for (var prop in obj) if (obj[prop] == null) delete obj[prop];
+ return obj;
+ }
+ function maybeSet(obj, prop, val) {
+ if (val != null) obj[prop] = val;
+ }
+
+ // Built-in query types
+
+ function compareCompletions(a, b) {
+ if (typeof a != "string") { a = a.name; b = b.name; }
+ var aUp = /^[A-Z]/.test(a), bUp = /^[A-Z]/.test(b);
+ if (aUp == bUp) return a < b ? -1 : a == b ? 0 : 1;
+ else return aUp ? 1 : -1;
+ }
+
+ function isStringAround(node, start, end) {
+ return node.type == "Literal" && typeof node.value == "string" &&
+ node.start == start - 1 && node.end <= end + 1;
+ }
+
+ function pointInProp(objNode, point) {
+ for (var i = 0; i < objNode.properties.length; i++) {
+ var curProp = objNode.properties[i];
+ if (curProp.key.start <= point && curProp.key.end >= point)
+ return curProp;
+ }
+ }
+
+ var jsKeywords = ("break do instanceof typeof case else new var " +
+ "catch finally return void continue for switch while debugger " +
+ "function this with default if throw delete in try").split(" ");
+
+ var addCompletion = exports.addCompletion = function(query, completions, name, aval, depth) {
+ var typeInfo = query.types || query.docs || query.urls || query.origins;
+ var wrapAsObjs = typeInfo || query.depths;
+
+ for (var i = 0; i < completions.length; ++i) {
+ var c = completions[i];
+ if ((wrapAsObjs ? c.name : c) == name) return;
+ }
+ var rec = wrapAsObjs ? {name: name} : name;
+ completions.push(rec);
+
+ if (aval && typeInfo) {
+ infer.resetGuessing();
+ var type = aval.getType();
+ rec.guess = infer.didGuess();
+ if (query.types)
+ rec.type = infer.toString(aval);
+ if (query.docs)
+ maybeSet(rec, "doc", parseDoc(query, aval.doc || type && type.doc));
+ if (query.urls)
+ maybeSet(rec, "url", aval.url || type && type.url);
+ if (query.origins)
+ maybeSet(rec, "origin", aval.origin || type && type.origin);
+ }
+ if (query.depths) rec.depth = depth || 0;
+ return rec;
+ };
+
+ function findCompletions(srv, query, file) {
+ if (query.end == null) throw ternError("missing .query.end field");
+ var fromPlugin = srv.signalReturnFirst("completion", file, query)
+ if (fromPlugin) return fromPlugin
+
+ var wordStart = resolvePos(file, query.end), wordEnd = wordStart, text = file.text;
+ while (wordStart && acorn.isIdentifierChar(text.charCodeAt(wordStart - 1))) --wordStart;
+ if (query.expandWordForward !== false)
+ while (wordEnd < text.length && acorn.isIdentifierChar(text.charCodeAt(wordEnd))) ++wordEnd;
+ var word = text.slice(wordStart, wordEnd), completions = [], ignoreObj;
+ if (query.caseInsensitive) word = word.toLowerCase();
+
+ function gather(prop, obj, depth, addInfo) {
+ // 'hasOwnProperty' and such are usually just noise, leave them
+ // out when no prefix is provided.
+ if ((objLit || query.omitObjectPrototype !== false) && obj == srv.cx.protos.Object && !word) return;
+ if (query.filter !== false && word &&
+ (query.caseInsensitive ? prop.toLowerCase() : prop).indexOf(word) !== 0) return;
+ if (ignoreObj && ignoreObj.props[prop]) return;
+ var result = addCompletion(query, completions, prop, obj && obj.props[prop], depth);
+ if (addInfo && result && typeof result != "string") addInfo(result);
+ }
+
+ var hookname, prop, objType, isKey;
+
+ var exprAt = infer.findExpressionAround(file.ast, null, wordStart, file.scope);
+ var memberExpr, objLit;
+ // Decide whether this is an object property, either in a member
+ // expression or an object literal.
+ if (exprAt) {
+ var exprNode = exprAt.node;
+ if (exprNode.type == "MemberExpression" && exprNode.object.end < wordStart) {
+ memberExpr = exprAt;
+ } else if (isStringAround(exprNode, wordStart, wordEnd)) {
+ var parent = infer.parentNode(exprNode, file.ast);
+ if (parent.type == "MemberExpression" && parent.property == exprNode)
+ memberExpr = {node: parent, state: exprAt.state};
+ } else if (exprNode.type == "ObjectExpression") {
+ var objProp = pointInProp(exprNode, wordEnd);
+ if (objProp) {
+ objLit = exprAt;
+ prop = isKey = objProp.key.name;
+ } else if (!word && !/:\s*$/.test(file.text.slice(0, wordStart))) {
+ objLit = exprAt;
+ prop = isKey = true;
+ }
+ }
+ }
+
+ if (objLit) {
+ // Since we can't use the type of the literal itself to complete
+ // its properties (it doesn't contain the information we need),
+ // we have to try asking the surrounding expression for type info.
+ objType = infer.typeFromContext(file.ast, objLit);
+ ignoreObj = objLit.node.objType;
+ } else if (memberExpr) {
+ prop = memberExpr.node.property;
+ prop = prop.type == "Literal" ? prop.value.slice(1) : prop.name;
+ memberExpr.node = memberExpr.node.object;
+ objType = infer.expressionType(memberExpr);
+ } else if (text.charAt(wordStart - 1) == ".") {
+ var pathStart = wordStart - 1;
+ while (pathStart && (text.charAt(pathStart - 1) == "." || acorn.isIdentifierChar(text.charCodeAt(pathStart - 1)))) pathStart--;
+ var path = text.slice(pathStart, wordStart - 1);
+ if (path) {
+ objType = infer.def.parsePath(path, file.scope).getObjType();
+ prop = word;
+ }
+ }
+
+ if (prop != null) {
+ srv.cx.completingProperty = prop;
+
+ if (objType) infer.forAllPropertiesOf(objType, gather);
+
+ if (!completions.length && query.guess !== false && objType && objType.guessProperties)
+ objType.guessProperties(function(p, o, d) {if (p != prop && p != "✖") gather(p, o, d);});
+ if (!completions.length && word.length >= 2 && query.guess !== false)
+ for (var prop in srv.cx.props) gather(prop, srv.cx.props[prop][0], 0);
+ hookname = "memberCompletion";
+ } else {
+ infer.forAllLocalsAt(file.ast, wordStart, file.scope, gather);
+ if (query.includeKeywords) jsKeywords.forEach(function(kw) {
+ gather(kw, null, 0, function(rec) { rec.isKeyword = true; });
+ });
+ hookname = "variableCompletion";
+ }
+ srv.signal(hookname, file, wordStart, wordEnd, gather)
+
+ if (query.sort !== false) completions.sort(compareCompletions);
+ srv.cx.completingProperty = null;
+
+ return {start: outputPos(query, file, wordStart),
+ end: outputPos(query, file, wordEnd),
+ isProperty: !!prop,
+ isObjectKey: !!isKey,
+ completions: completions};
+ }
+
+ function findProperties(srv, query) {
+ var prefix = query.prefix, found = [];
+ for (var prop in srv.cx.props)
+ if (prop != "<i>" && (!prefix || prop.indexOf(prefix) === 0)) found.push(prop);
+ if (query.sort !== false) found.sort(compareCompletions);
+ return {completions: found};
+ }
+
+ var findExpr = exports.findQueryExpr = function(file, query, wide) {
+ if (query.end == null) throw ternError("missing .query.end field");
+
+ if (query.variable) {
+ var scope = infer.scopeAt(file.ast, resolvePos(file, query.end), file.scope);
+ return {node: {type: "Identifier", name: query.variable, start: query.end, end: query.end + 1},
+ state: scope};
+ } else {
+ var start = query.start && resolvePos(file, query.start), end = resolvePos(file, query.end);
+ var expr = infer.findExpressionAt(file.ast, start, end, file.scope);
+ if (expr) return expr;
+ expr = infer.findExpressionAround(file.ast, start, end, file.scope);
+ if (expr && (expr.node.type == "ObjectExpression" || wide ||
+ (start == null ? end : start) - expr.node.start < 20 || expr.node.end - end < 20))
+ return expr;
+ return null;
+ }
+ };
+
+ function findExprOrThrow(file, query, wide) {
+ var expr = findExpr(file, query, wide);
+ if (expr) return expr;
+ throw ternError("No expression at the given position.");
+ }
+
+ function ensureObj(tp) {
+ if (!tp || !(tp = tp.getType()) || !(tp instanceof infer.Obj)) return null;
+ return tp;
+ }
+
+ function findExprType(srv, query, file, expr) {
+ var type;
+ if (expr) {
+ infer.resetGuessing();
+ type = infer.expressionType(expr);
+ }
+ var typeHandlers = srv.hasHandler("typeAt")
+ if (typeHandlers) {
+ var pos = resolvePos(file, query.end)
+ for (var i = 0; i < typeHandlers.length; i++)
+ type = typeHandlers[i](file, pos, expr, type)
+ }
+ if (!type) throw ternError("No type found at the given position.");
+
+ var objProp;
+ if (expr.node.type == "ObjectExpression" && query.end != null &&
+ (objProp = pointInProp(expr.node, resolvePos(file, query.end)))) {
+ var name = objProp.key.name;
+ var fromCx = ensureObj(infer.typeFromContext(file.ast, expr));
+ if (fromCx && fromCx.hasProp(name)) {
+ type = fromCx.hasProp(name);
+ } else {
+ var fromLocal = ensureObj(type);
+ if (fromLocal && fromLocal.hasProp(name))
+ type = fromLocal.hasProp(name);
+ }
+ }
+ return type;
+ };
+
+ function findTypeAt(srv, query, file) {
+ var expr = findExpr(file, query), exprName;
+ var type = findExprType(srv, query, file, expr), exprType = type;
+ if (query.preferFunction)
+ type = type.getFunctionType() || type.getType();
+ else
+ type = type.getType();
+
+ if (expr) {
+ if (expr.node.type == "Identifier")
+ exprName = expr.node.name;
+ else if (expr.node.type == "MemberExpression" && !expr.node.computed)
+ exprName = expr.node.property.name;
+ }
+
+ if (query.depth != null && typeof query.depth != "number")
+ throw ternError(".query.depth must be a number");
+
+ var result = {guess: infer.didGuess(),
+ type: infer.toString(exprType, query.depth),
+ name: type && type.name,
+ exprName: exprName,
+ doc: exprType.doc,
+ url: exprType.url};
+ if (type) storeTypeDocs(query, type, result);
+
+ return clean(result);
+ }
+
+ function parseDoc(query, doc) {
+ if (!doc) return null;
+ if (query.docFormat == "full") return doc;
+ var parabreak = /.\n[\s@\n]/.exec(doc);
+ if (parabreak) doc = doc.slice(0, parabreak.index + 1);
+ doc = doc.replace(/\n\s*/g, " ");
+ if (doc.length < 100) return doc;
+ var sentenceEnd = /[\.!?] [A-Z]/g;
+ sentenceEnd.lastIndex = 80;
+ var found = sentenceEnd.exec(doc);
+ if (found) doc = doc.slice(0, found.index + 1);
+ return doc;
+ }
+
+ function findDocs(srv, query, file) {
+ var expr = findExpr(file, query);
+ var type = findExprType(srv, query, file, expr);
+ var result = {url: type.url, doc: parseDoc(query, type.doc), type: infer.toString(type)};
+ var inner = type.getType();
+ if (inner) storeTypeDocs(query, inner, result);
+ return clean(result);
+ }
+
+ function storeTypeDocs(query, type, out) {
+ if (!out.url) out.url = type.url;
+ if (!out.doc) out.doc = parseDoc(query, type.doc);
+ if (!out.origin) out.origin = type.origin;
+ var ctor, boring = infer.cx().protos;
+ if (!out.url && !out.doc && type.proto && (ctor = type.proto.hasCtor) &&
+ type.proto != boring.Object && type.proto != boring.Function && type.proto != boring.Array) {
+ out.url = ctor.url;
+ out.doc = parseDoc(query, ctor.doc);
+ }
+ }
+
+ var getSpan = exports.getSpan = function(obj) {
+ if (!obj.origin) return;
+ if (obj.originNode) {
+ var node = obj.originNode;
+ if (/^Function/.test(node.type) && node.id) node = node.id;
+ return {origin: obj.origin, node: node};
+ }
+ if (obj.span) return {origin: obj.origin, span: obj.span};
+ };
+
+ var storeSpan = exports.storeSpan = function(srv, query, span, target) {
+ target.origin = span.origin;
+ if (span.span) {
+ var m = /^(\d+)\[(\d+):(\d+)\]-(\d+)\[(\d+):(\d+)\]$/.exec(span.span);
+ target.start = query.lineCharPositions ? {line: Number(m[2]), ch: Number(m[3])} : Number(m[1]);
+ target.end = query.lineCharPositions ? {line: Number(m[5]), ch: Number(m[6])} : Number(m[4]);
+ } else {
+ var file = srv.findFile(span.origin);
+ target.start = outputPos(query, file, span.node.start);
+ target.end = outputPos(query, file, span.node.end);
+ }
+ };
+
+ function findDef(srv, query, file) {
+ var expr = findExpr(file, query);
+ var type = findExprType(srv, query, file, expr);
+ if (infer.didGuess()) return {};
+
+ var span = getSpan(type);
+ var result = {url: type.url, doc: parseDoc(query, type.doc), origin: type.origin};
+
+ if (type.types) for (var i = type.types.length - 1; i >= 0; --i) {
+ var tp = type.types[i];
+ storeTypeDocs(query, tp, result);
+ if (!span) span = getSpan(tp);
+ }
+
+ if (span && span.node) { // refers to a loaded file
+ var spanFile = span.node.sourceFile || srv.findFile(span.origin);
+ var start = outputPos(query, spanFile, span.node.start), end = outputPos(query, spanFile, span.node.end);
+ result.start = start; result.end = end;
+ result.file = span.origin;
+ var cxStart = Math.max(0, span.node.start - 50);
+ result.contextOffset = span.node.start - cxStart;
+ result.context = spanFile.text.slice(cxStart, cxStart + 50);
+ } else if (span) { // external
+ result.file = span.origin;
+ storeSpan(srv, query, span, result);
+ }
+ return clean(result);
+ }
+
+ function findRefsToVariable(srv, query, file, expr, checkShadowing) {
+ var name = expr.node.name;
+
+ for (var scope = expr.state; scope && !(name in scope.props); scope = scope.prev) {}
+ if (!scope) throw ternError("Could not find a definition for " + name);
+
+ var type, refs = [];
+ function storeRef(file) {
+ return function(node, scopeHere) {
+ if (checkShadowing) for (var s = scopeHere; s != scope; s = s.prev) {
+ var exists = s.hasProp(checkShadowing);
+ if (exists)
+ throw ternError("Renaming `" + name + "` to `" + checkShadowing + "` would make a variable at line " +
+ (asLineChar(file, node.start).line + 1) + " point to the definition at line " +
+ (asLineChar(file, exists.name.start).line + 1));
+ }
+ refs.push({file: file.name,
+ start: outputPos(query, file, node.start),
+ end: outputPos(query, file, node.end)});
+ };
+ }
+
+ if (scope.originNode) {
+ type = "local";
+ if (checkShadowing) {
+ for (var prev = scope.prev; prev; prev = prev.prev)
+ if (checkShadowing in prev.props) break;
+ if (prev) infer.findRefs(scope.originNode, scope, checkShadowing, prev, function(node) {
+ throw ternError("Renaming `" + name + "` to `" + checkShadowing + "` would shadow the definition used at line " +
+ (asLineChar(file, node.start).line + 1));
+ });
+ }
+ infer.findRefs(scope.originNode, scope, name, scope, storeRef(file));
+ } else {
+ type = "global";
+ for (var i = 0; i < srv.files.length; ++i) {
+ var cur = srv.files[i];
+ infer.findRefs(cur.ast, cur.scope, name, scope, storeRef(cur));
+ }
+ }
+
+ return {refs: refs, type: type, name: name};
+ }
+
+ function findRefsToProperty(srv, query, expr, prop) {
+ var objType = infer.expressionType(expr).getObjType();
+ if (!objType) throw ternError("Couldn't determine type of base object.");
+
+ var refs = [];
+ function storeRef(file) {
+ return function(node) {
+ refs.push({file: file.name,
+ start: outputPos(query, file, node.start),
+ end: outputPos(query, file, node.end)});
+ };
+ }
+ for (var i = 0; i < srv.files.length; ++i) {
+ var cur = srv.files[i];
+ infer.findPropRefs(cur.ast, cur.scope, objType, prop.name, storeRef(cur));
+ }
+
+ return {refs: refs, name: prop.name};
+ }
+
+ function findRefs(srv, query, file) {
+ var expr = findExprOrThrow(file, query, true);
+ if (expr && expr.node.type == "Identifier") {
+ return findRefsToVariable(srv, query, file, expr);
+ } else if (expr && expr.node.type == "MemberExpression" && !expr.node.computed) {
+ var p = expr.node.property;
+ expr.node = expr.node.object;
+ return findRefsToProperty(srv, query, expr, p);
+ } else if (expr && expr.node.type == "ObjectExpression") {
+ var pos = resolvePos(file, query.end);
+ for (var i = 0; i < expr.node.properties.length; ++i) {
+ var k = expr.node.properties[i].key;
+ if (k.start <= pos && k.end >= pos)
+ return findRefsToProperty(srv, query, expr, k);
+ }
+ }
+ throw ternError("Not at a variable or property name.");
+ }
+
+ function buildRename(srv, query, file) {
+ if (typeof query.newName != "string") throw ternError(".query.newName should be a string");
+ var expr = findExprOrThrow(file, query);
+ if (!expr || expr.node.type != "Identifier") throw ternError("Not at a variable.");
+
+ var data = findRefsToVariable(srv, query, file, expr, query.newName), refs = data.refs;
+ delete data.refs;
+ data.files = srv.files.map(function(f){return f.name;});
+
+ var changes = data.changes = [];
+ for (var i = 0; i < refs.length; ++i) {
+ var use = refs[i];
+ use.text = query.newName;
+ changes.push(use);
+ }
+
+ return data;
+ }
+
+ function listFiles(srv) {
+ return {files: srv.files.map(function(f){return f.name;})};
+ }
+
+ exports.version = "0.16.0";
+});
diff --git a/devtools/client/sourceeditor/tern/tests/unit/head_tern.js b/devtools/client/sourceeditor/tern/tests/unit/head_tern.js
new file mode 100644
index 000000000..1ab102685
--- /dev/null
+++ b/devtools/client/sourceeditor/tern/tests/unit/head_tern.js
@@ -0,0 +1,3 @@
+"use strict";
+var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
diff --git a/devtools/client/sourceeditor/tern/tests/unit/test_autocompletion.js b/devtools/client/sourceeditor/tern/tests/unit/test_autocompletion.js
new file mode 100644
index 000000000..493d6fb18
--- /dev/null
+++ b/devtools/client/sourceeditor/tern/tests/unit/test_autocompletion.js
@@ -0,0 +1,26 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test that tern autocompletions work.
+ */
+
+const tern = require("devtools/client/sourceeditor/tern/tern");
+const ecma5 = require("devtools/client/sourceeditor/tern/ecma5");
+
+function run_test() {
+ do_test_pending();
+
+ const server = new tern.Server({ defs: [ecma5] });
+ const code = "[].";
+ const query = { type: "completions", file: "test", end: code.length };
+ const files = [{ type: "full", name: "test", text: code }];
+
+ server.request({ query: query, files: files }, (error, response) => {
+ do_check_eq(error, null);
+ do_check_true(!!response);
+ do_check_true(Array.isArray(response.completions));
+ do_check_true(response.completions.indexOf("concat") != -1);
+ do_test_finished();
+ });
+}
diff --git a/devtools/client/sourceeditor/tern/tests/unit/test_import_tern.js b/devtools/client/sourceeditor/tern/tests/unit/test_import_tern.js
new file mode 100644
index 000000000..74f68fe60
--- /dev/null
+++ b/devtools/client/sourceeditor/tern/tests/unit/test_import_tern.js
@@ -0,0 +1,16 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test that we can require tern.
+ */
+
+function run_test() {
+ const tern = require("devtools/client/sourceeditor/tern/tern");
+ const ecma5 = require("devtools/client/sourceeditor/tern/ecma5");
+ const browser = require("devtools/client/sourceeditor/tern/browser");
+ do_check_true(!!tern);
+ do_check_true(!!ecma5);
+ do_check_true(!!browser);
+ do_check_eq(typeof tern.Server, "function");
+}
diff --git a/devtools/client/sourceeditor/tern/tests/unit/xpcshell.ini b/devtools/client/sourceeditor/tern/tests/unit/xpcshell.ini
new file mode 100644
index 000000000..567d8524d
--- /dev/null
+++ b/devtools/client/sourceeditor/tern/tests/unit/xpcshell.ini
@@ -0,0 +1,8 @@
+[DEFAULT]
+tags = devtools
+head = head_tern.js
+tail =
+firefox-appdir = browser
+
+[test_autocompletion.js]
+[test_import_tern.js]
diff --git a/devtools/client/sourceeditor/test/.eslintrc.js b/devtools/client/sourceeditor/test/.eslintrc.js
new file mode 100644
index 000000000..8d15a76d9
--- /dev/null
+++ b/devtools/client/sourceeditor/test/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../../.eslintrc.mochitests.js"
+};
diff --git a/devtools/client/sourceeditor/test/browser.ini b/devtools/client/sourceeditor/test/browser.ini
new file mode 100644
index 000000000..c084d1d80
--- /dev/null
+++ b/devtools/client/sourceeditor/test/browser.ini
@@ -0,0 +1,48 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ codemirror/comment_test.js
+ codemirror/doc_test.js
+ codemirror/driver.js
+ codemirror/emacs_test.js
+ codemirror/mode_test.css
+ codemirror/mode_test.js
+ codemirror/multi_test.js
+ codemirror/search_test.js
+ codemirror/sublime_test.js
+ codemirror/test.js
+ codemirror/vim_test.js
+ codemirror/codemirror.html
+ codemirror/vimemacs.html
+ codemirror/mode/javascript/test.js
+ css_statemachine_testcases.css
+ css_statemachine_tests.json
+ css_autocompletion_tests.json
+ head.js
+ helper_codemirror_runner.js
+ cm_mode_ruby.js
+ cm_script_injection_test.js
+ !/devtools/client/framework/test/shared-head.js
+
+[browser_editor_autocomplete_basic.js]
+[browser_editor_autocomplete_events.js]
+[browser_editor_autocomplete_js.js]
+[browser_editor_basic.js]
+[browser_editor_cursor.js]
+[browser_editor_find_again.js]
+[browser_editor_goto_line.js]
+[browser_editor_history.js]
+[browser_editor_markers.js]
+[browser_editor_movelines.js]
+[browser_editor_prefs.js]
+[browser_editor_script_injection.js]
+[browser_editor_addons.js]
+[browser_codemirror.js]
+[browser_css_autocompletion.js]
+[browser_css_getInfo.js]
+[browser_css_statemachine.js]
+[browser_detectindent.js]
+[browser_vimemacs.js]
+skip-if = os == 'linux'&&debug # bug 981707
+
diff --git a/devtools/client/sourceeditor/test/browser_codemirror.js b/devtools/client/sourceeditor/test/browser_codemirror.js
new file mode 100644
index 000000000..381a6530f
--- /dev/null
+++ b/devtools/client/sourceeditor/test/browser_codemirror.js
@@ -0,0 +1,18 @@
+/* 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 URI = "chrome://mochitests/content/browser/devtools/client/" +
+ "sourceeditor/test/codemirror/codemirror.html";
+loadHelperScript("helper_codemirror_runner.js");
+
+function test() {
+ requestLongerTimeout(3);
+ waitForExplicitFinish();
+
+ addTab(URI).then(function (tab) {
+ runCodeMirrorTest(tab.linkedBrowser);
+ });
+}
diff --git a/devtools/client/sourceeditor/test/browser_css_autocompletion.js b/devtools/client/sourceeditor/test/browser_css_autocompletion.js
new file mode 100644
index 000000000..e98803113
--- /dev/null
+++ b/devtools/client/sourceeditor/test/browser_css_autocompletion.js
@@ -0,0 +1,145 @@
+/* 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 CSSCompleter = require("devtools/client/sourceeditor/css-autocompleter");
+const {InspectorFront} = require("devtools/shared/fronts/inspector");
+
+const CSS_URI = "http://mochi.test:8888/browser/devtools/client/sourceeditor" +
+ "/test/css_statemachine_testcases.css";
+const TESTS_URI = "http://mochi.test:8888/browser/devtools/client" +
+ "/sourceeditor/test/css_autocompletion_tests.json";
+
+const source = read(CSS_URI);
+const tests = eval(read(TESTS_URI));
+
+const TEST_URI = "data:text/html;charset=UTF-8," + encodeURIComponent(
+ ["<!DOCTYPE html>",
+ "<html>",
+ " <head>",
+ " <title>CSS State machine tests.</title>",
+ " <style type='text/css'>",
+ "#progress {",
+ " width: 500px; height: 30px;",
+ " border: 1px solid black;",
+ " position: relative",
+ "}",
+ "#progress div {",
+ " width: 0%; height: 100%;",
+ " background: green;",
+ " position: absolute;",
+ " z-index: -1; top: 0",
+ "}",
+ "#progress.failed div {",
+ " background: red !important;",
+ "}",
+ "#progress.failed:after {",
+ " content: 'Some tests failed';",
+ " color: white",
+ "}",
+ "#progress:before {",
+ " content: 'Running test ' attr(data-progress) ' of " + tests.length + "';",
+ " color: white;",
+ " text-shadow: 0 0 2px darkgreen;",
+ "}",
+ " </style>",
+ " </head>",
+ " <body>",
+ " <h2>State machine tests for CSS autocompleter.</h2><br>",
+ " <div id='progress' data-progress='0'>",
+ " <div></div>",
+ " </div>",
+ " <div id='devtools-menu' class='devtools-toolbarbutton'></div>",
+ " <div id='devtools-toolbarbutton' class='devtools-menulist'></div>",
+ " <div id='devtools-anotherone'></div>",
+ " <div id='devtools-yetagain'></div>",
+ " <div id='devtools-itjustgoeson'></div>",
+ " <div id='devtools-okstopitnow'></div>",
+ " <div class='hidden-labels-box devtools-toolbarbutton devtools-menulist'></div>",
+ " <div class='devtools-menulist'></div>",
+ " <div class='devtools-menulist'></div>",
+ " <tabs class='devtools-toolbarbutton'><tab></tab><tab></tab><tab></tab></tabs><tabs></tabs>",
+ " <button class='category-name visible'></button>",
+ " <div class='devtools-toolbarbutton' label='true'>",
+ " <hbox class='toolbarbutton-menubutton-button'></hbox></div>",
+ " </body>",
+ " </html>"
+ ].join("\n"));
+
+let doc = null;
+let index = 0;
+let completer = null;
+let progress;
+let progressDiv;
+let inspector;
+
+function test() {
+ waitForExplicitFinish();
+ addTab(TEST_URI).then(function () {
+ doc = content.document;
+ runTests();
+ });
+}
+
+function runTests() {
+ progress = doc.getElementById("progress");
+ progressDiv = doc.querySelector("#progress > div");
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ target.makeRemote().then(() => {
+ inspector = InspectorFront(target.client, target.form);
+ inspector.getWalker().then(walker => {
+ completer = new CSSCompleter({walker: walker,
+ cssProperties: getClientCssProperties()});
+ checkStateAndMoveOn();
+ });
+ });
+}
+
+function checkStateAndMoveOn() {
+ if (index == tests.length) {
+ finishUp();
+ return;
+ }
+
+ let [lineCh, expectedSuggestions] = tests[index];
+ let [line, ch] = lineCh;
+
+ progress.dataset.progress = ++index;
+ progressDiv.style.width = 100 * index / tests.length + "%";
+
+ completer.complete(limit(source, lineCh), {line, ch})
+ .then(actualSuggestions => checkState(expectedSuggestions, actualSuggestions))
+ .then(checkStateAndMoveOn);
+}
+
+function checkState(expected, actual) {
+ if (expected.length != actual.length) {
+ ok(false, "Number of suggestions did not match up for state " + index +
+ ". Expected: " + expected.length + ", Actual: " + actual.length);
+ progress.classList.add("failed");
+ return;
+ }
+
+ for (let i = 0; i < actual.length; i++) {
+ if (expected[i] != actual[i].label) {
+ ok(false, "Suggestion " + i + " of state " + index + " did not match up" +
+ ". Expected: " + expected[i] + ". Actual: " + actual[i].label);
+ return;
+ }
+ }
+ ok(true, "Test " + index + " passed. ");
+}
+
+function finishUp() {
+ completer.walker.release().then(() => {
+ inspector.destroy();
+ inspector = null;
+ completer = null;
+ gBrowser.removeCurrentTab();
+ finish();
+ });
+ progress = null;
+ progressDiv = null;
+}
diff --git a/devtools/client/sourceeditor/test/browser_css_getInfo.js b/devtools/client/sourceeditor/test/browser_css_getInfo.js
new file mode 100644
index 000000000..21b0b92b9
--- /dev/null
+++ b/devtools/client/sourceeditor/test/browser_css_getInfo.js
@@ -0,0 +1,176 @@
+/* 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 CSSCompleter =
+ require("devtools/client/sourceeditor/css-autocompleter");
+
+const source = [
+ ".devtools-toolbar {",
+ " -moz-appearance: none;",
+ " padding:4px 3px;border-bottom-width: 1px;",
+ " border-bottom-style: solid;",
+ "}",
+ "",
+ "#devtools-menu.devtools-menulist,",
+ ".devtools-toolbarbutton#devtools-menu {",
+ " -moz-appearance: none;",
+ " -moz-box-align: center;",
+ " min-width: 78px;",
+ " min-height: 22px;",
+ " text-shadow: 0 -1px 0 hsla(210,8%,5%,.45);",
+ " border: 1px solid hsla(210,8%,5%,.45);",
+ " border-radius: 3px;",
+ " background: linear-gradient(hsla(212,7%,57%,.35),",
+ " hsla(212,7%,57%,.1)) padding-box;",
+ " margin: 0 3px;",
+ " color: inherit;",
+ "}",
+ "",
+ ".devtools-toolbarbutton > hbox.toolbarbutton-menubutton-button {",
+ " -moz-box-orient: horizontal;",
+ "}",
+ "",
+ ".devtools-menulist:active,",
+ "#devtools-toolbarbutton:focus {",
+ " outline: 1px dotted hsla(210,30%,85%,0.7);",
+ " outline-offset : -4px;",
+ "}",
+ "",
+ ".devtools-toolbarbutton:not([label]) {",
+ " min-width: 32px;",
+ "}",
+ "",
+ ".devtools-toolbarbutton:not([label]) > .toolbarbutton-text, .devtools-toolbar {",
+ " display: none;",
+ "}",
+].join("\n");
+
+// Format of test cases :
+// [
+// {line, ch}, - The caret position at which the getInfo call should be made
+// expectedState, - The expected state at the caret
+// expectedSelector, - The expected selector for the state
+// expectedProperty, - The expected property name for states value and property
+// expectedValue, - If state is value, then the expected value
+// ]
+const tests = [
+ [{line: 0, ch: 13}, "selector", ".devtools-toolbar"],
+ [{line: 8, ch: 13}, "property", ["#devtools-menu.devtools-menulist",
+ ".devtools-toolbarbutton#devtools-menu "], "-moz-appearance"],
+ [{line: 28, ch: 25}, "value", [".devtools-menulist:active",
+ "#devtools-toolbarbutton:focus "], "outline-offset", "-4px"],
+ [{line: 4, ch: 1}, "null"],
+ [{line: 5, ch: 0}, "null"],
+ [{line: 31, ch: 13}, "selector", ".devtools-toolbarbutton:not([label])"],
+ [{line: 35, ch: 23}, "selector", ".devtools-toolbarbutton:not([label]) > .toolbarbutton-text"],
+ [{line: 35, ch: 70}, "selector", ".devtools-toolbar"],
+ [{line: 27, ch: 14}, "value", [".devtools-menulist:active",
+ "#devtools-toolbarbutton:focus "], "outline", "1px dotted hsla(210,30%,85%,0.7)"],
+ [{line: 16, ch: 16}, "value", ["#devtools-menu.devtools-menulist",
+ ".devtools-toolbarbutton#devtools-menu "], "background",
+ "linear-gradient(hsla(212,7%,57%,.35),\n hsla(212,7%,57%,.1)) padding-box"],
+ [{line: 16, ch: 3}, "value", ["#devtools-menu.devtools-menulist",
+ ".devtools-toolbarbutton#devtools-menu "], "background",
+ "linear-gradient(hsla(212,7%,57%,.35),\n hsla(212,7%,57%,.1)) padding-box"],
+ [{line: 15, ch: 25}, "value", ["#devtools-menu.devtools-menulist",
+ ".devtools-toolbarbutton#devtools-menu "], "background",
+ "linear-gradient(hsla(212,7%,57%,.35),\n hsla(212,7%,57%,.1)) padding-box"],
+];
+
+const TEST_URI = "data:text/html;charset=UTF-8," + encodeURIComponent(
+ ["<!DOCTYPE html>",
+ "<html>",
+ " <head>",
+ " <title>CSS contextual information tests.</title>",
+ " <style type='text/css'>",
+ "#progress {",
+ " width: 500px; height: 30px;",
+ " border: 1px solid black;",
+ " position: relative",
+ "}",
+ "#progress div {",
+ " width: 0%; height: 100%;",
+ " background: green;",
+ " position: absolute;",
+ " z-index: -1; top: 0",
+ "}",
+ "#progress.failed div {",
+ " background: red !important;",
+ "}",
+ "#progress.failed:after {",
+ " content: 'Some tests failed';",
+ " color: white",
+ "}",
+ "#progress:before {",
+ " content: 'Running test ' attr(data-progress) ' of " + tests.length + "';",
+ " color: white;",
+ " text-shadow: 0 0 2px darkgreen;",
+ "}",
+ " </style>",
+ " </head>",
+ " <body>",
+ " <h2>State machine tests for CSS autocompleter.</h2><br>",
+ " <div id='progress' data-progress='0'>",
+ " <div></div>",
+ " </div>",
+ " </body>",
+ " </html>"
+ ].join("\n"));
+
+let doc = null;
+function test() {
+ waitForExplicitFinish();
+ gBrowser.selectedTab = gBrowser.addTab();
+ BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(() => {
+ doc = content.document;
+ runTests();
+ });
+ gBrowser.loadURI(TEST_URI);
+}
+
+function runTests() {
+ let completer = new CSSCompleter({cssProperties: getClientCssProperties()});
+ let matches = (arr, toCheck) => !arr.some((x, i) => x != toCheck[i]);
+ let checkState = (expected, actual) => {
+ if (expected[0] == "null" && actual == null) {
+ return true;
+ } else if (expected[0] == actual.state && expected[0] == "selector" &&
+ expected[1] == actual.selector) {
+ return true;
+ } else if (expected[0] == actual.state && expected[0] == "property" &&
+ matches(expected[1], actual.selectors) &&
+ expected[2] == actual.propertyName) {
+ return true;
+ } else if (expected[0] == actual.state && expected[0] == "value" &&
+ matches(expected[1], actual.selectors) &&
+ expected[2] == actual.propertyName &&
+ expected[3] == actual.value) {
+ return true;
+ }
+ return false;
+ };
+
+ let progress = doc.getElementById("progress");
+ let progressDiv = doc.querySelector("#progress > div");
+ let i = 0;
+ for (let expected of tests) {
+ let caret = expected.splice(0, 1)[0];
+ progress.dataset.progress = ++i;
+ progressDiv.style.width = 100 * i / tests.length + "%";
+ let actual = completer.getInfoAt(source, caret);
+ if (checkState(expected, actual)) {
+ ok(true, "Test " + i + " passed. ");
+ } else {
+ ok(false, "Test " + i + " failed. Expected state : [" + expected + "] " +
+ "but found [" + actual.state + ", " +
+ (actual.selector || actual.selectors) + ", " +
+ actual.propertyName + ", " + actual.value + "].");
+ progress.classList.add("failed");
+ }
+ }
+ gBrowser.removeCurrentTab();
+ finish();
+}
diff --git a/devtools/client/sourceeditor/test/browser_css_statemachine.js b/devtools/client/sourceeditor/test/browser_css_statemachine.js
new file mode 100644
index 000000000..f69d60278
--- /dev/null
+++ b/devtools/client/sourceeditor/test/browser_css_statemachine.js
@@ -0,0 +1,109 @@
+/* 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 CSSCompleter = require("devtools/client/sourceeditor/css-autocompleter");
+
+const CSS_URI = "http://mochi.test:8888/browser/devtools/client/sourceeditor" +
+ "/test/css_statemachine_testcases.css";
+const TESTS_URI = "http://mochi.test:8888/browser/devtools/client" +
+ "/sourceeditor/test/css_statemachine_tests.json";
+
+const source = read(CSS_URI);
+const tests = eval(read(TESTS_URI));
+
+const TEST_URI = "data:text/html;charset=UTF-8," + encodeURIComponent(
+ ["<!DOCTYPE html>",
+ "<html>",
+ " <head>",
+ " <title>CSS State machine tests.</title>",
+ " <style type='text/css'>",
+ "#progress {",
+ " width: 500px; height: 30px;",
+ " border: 1px solid black;",
+ " position: relative",
+ "}",
+ "#progress div {",
+ " width: 0%; height: 100%;",
+ " background: green;",
+ " position: absolute;",
+ " z-index: -1; top: 0",
+ "}",
+ "#progress.failed div {",
+ " background: red !important;",
+ "}",
+ "#progress.failed:after {",
+ " content: 'Some tests failed';",
+ " color: white",
+ "}",
+ "#progress:before {",
+ " content: 'Running test ' attr(data-progress) ' of " + tests.length + "';",
+ " color: white;",
+ " text-shadow: 0 0 2px darkgreen;",
+ "}",
+ " </style>",
+ " </head>",
+ " <body>",
+ " <h2>State machine tests for CSS autocompleter.</h2><br>",
+ " <div id='progress' data-progress='0'>",
+ " <div></div>",
+ " </div>",
+ " </body>",
+ " </html>"
+ ].join("\n"));
+
+var doc = null;
+function test() {
+ waitForExplicitFinish();
+ gBrowser.selectedTab = gBrowser.addTab(TEST_URI);
+ BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(() => {
+ doc = content.document;
+ runTests();
+ });
+}
+
+function runTests() {
+ let completer = new CSSCompleter({cssProperties: getClientCssProperties()});
+ let checkState = state => {
+ if (state[0] == "null" && (!completer.state || completer.state == "null")) {
+ return true;
+ } else if (state[0] == completer.state && state[0] == "selector" &&
+ state[1] == completer.selectorState &&
+ state[2] == completer.completing &&
+ state[3] == completer.selector) {
+ return true;
+ } else if (state[0] == completer.state && state[0] == "value" &&
+ state[2] == completer.completing &&
+ state[3] == completer.propertyName) {
+ return true;
+ } else if (state[0] == completer.state &&
+ state[2] == completer.completing &&
+ state[0] != "selector" && state[0] != "value") {
+ return true;
+ }
+ return false;
+ };
+
+ let progress = doc.getElementById("progress");
+ let progressDiv = doc.querySelector("#progress > div");
+ let i = 0;
+ for (let test of tests) {
+ progress.dataset.progress = ++i;
+ progressDiv.style.width = 100 * i / tests.length + "%";
+ completer.resolveState(limit(source, test[0]),
+ {line: test[0][0], ch: test[0][1]});
+ if (checkState(test[1])) {
+ ok(true, "Test " + i + " passed. ");
+ } else {
+ ok(false, "Test " + i + " failed. Expected state : [" + test[1] + "] " +
+ "but found [" + completer.state + ", " + completer.selectorState +
+ ", " + completer.completing + ", " +
+ (completer.propertyName || completer.selector) + "].");
+ progress.classList.add("failed");
+ }
+ }
+ gBrowser.removeCurrentTab();
+ finish();
+}
diff --git a/devtools/client/sourceeditor/test/browser_detectindent.js b/devtools/client/sourceeditor/test/browser_detectindent.js
new file mode 100644
index 000000000..89a3898d1
--- /dev/null
+++ b/devtools/client/sourceeditor/test/browser_detectindent.js
@@ -0,0 +1,102 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TWO_SPACES_CODE = [
+ "/*",
+ " * tricky comment block",
+ " */",
+ "div {",
+ " color: red;",
+ " background: blue;",
+ "}",
+ " ",
+ "span {",
+ " padding-left: 10px;",
+ "}"
+].join("\n");
+
+const FOUR_SPACES_CODE = [
+ "var obj = {",
+ " addNumbers: function() {",
+ " var x = 5;",
+ " var y = 18;",
+ " return x + y;",
+ " },",
+ " ",
+ " /*",
+ " * Do some stuff to two numbers",
+ " * ",
+ " * @param x",
+ " * @param y",
+ " * ",
+ " * @return the result of doing stuff",
+ " */",
+ " subtractNumbers: function(x, y) {",
+ " var x += 7;",
+ " var y += 18;",
+ " var result = x - y;",
+ " result %= 2;",
+ " }",
+ "}"
+].join("\n");
+
+const TABS_CODE = [
+ "/*",
+ " * tricky comment block",
+ " */",
+ "div {",
+ "\tcolor: red;",
+ "\tbackground: blue;",
+ "}",
+ "",
+ "span {",
+ "\tpadding-left: 10px;",
+ "}"
+].join("\n");
+
+const NONE_CODE = [
+ "var x = 0;",
+ " // stray thing",
+ "var y = 9;",
+ " ",
+ ""
+].join("\n");
+
+function test() {
+ waitForExplicitFinish();
+
+ setup((ed, win) => {
+ is(ed.getOption("indentUnit"), 2,
+ "2 spaces before code added");
+ is(ed.getOption("indentWithTabs"), false,
+ "spaces is default");
+
+ ed.setText(NONE_CODE);
+ is(ed.getOption("indentUnit"), 2,
+ "2 spaces after un-detectable code");
+ is(ed.getOption("indentWithTabs"), false,
+ "spaces still set after un-detectable code");
+
+ ed.setText(FOUR_SPACES_CODE);
+ is(ed.getOption("indentUnit"), 4,
+ "4 spaces detected in 4 space code");
+ is(ed.getOption("indentWithTabs"), false,
+ "spaces detected in 4 space code");
+
+ ed.setText(TWO_SPACES_CODE);
+ is(ed.getOption("indentUnit"), 2,
+ "2 spaces detected in 2 space code");
+ is(ed.getOption("indentWithTabs"), false,
+ "spaces detected in 2 space code");
+
+ ed.setText(TABS_CODE);
+ is(ed.getOption("indentUnit"), 2,
+ "2 space indentation unit");
+ is(ed.getOption("indentWithTabs"), true,
+ "tabs detected in majority tabs code");
+
+ teardown(ed, win);
+ });
+}
diff --git a/devtools/client/sourceeditor/test/browser_editor_addons.js b/devtools/client/sourceeditor/test/browser_editor_addons.js
new file mode 100644
index 000000000..6a7e9ca42
--- /dev/null
+++ b/devtools/client/sourceeditor/test/browser_editor_addons.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+function test() {
+ waitForExplicitFinish();
+
+ setup((ed, win) => {
+ let doc = win.document.querySelector("iframe").contentWindow.document;
+
+ // trailingspace.js
+ ed.setText("Hello ");
+ ed.setOption("showTrailingSpace", false);
+ ok(!doc.querySelector(".cm-trailingspace"));
+ ed.setOption("showTrailingSpace", true);
+ ok(doc.querySelector(".cm-trailingspace"));
+
+ // foldcode.js and foldgutter.js
+ ed.setMode(Editor.modes.js);
+ ed.setText("function main() {\nreturn 'Hello, World!';\n}");
+ executeSoon(() => testFold(doc, ed, win));
+ });
+}
+
+function testFold(doc, ed, win) {
+ // Wait until folding arrow is there.
+ if (!doc.querySelector(".CodeMirror-foldgutter-open")) {
+ executeSoon(() => testFold(doc, ed, win));
+ return;
+ }
+
+ teardown(ed, win);
+}
diff --git a/devtools/client/sourceeditor/test/browser_editor_autocomplete_basic.js b/devtools/client/sourceeditor/test/browser_editor_autocomplete_basic.js
new file mode 100644
index 000000000..03cdc2a4a
--- /dev/null
+++ b/devtools/client/sourceeditor/test/browser_editor_autocomplete_basic.js
@@ -0,0 +1,59 @@
+/* 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 AUTOCOMPLETION_PREF = "devtools.editor.autocomplete";
+
+// Test to make sure that different autocompletion modes can be created,
+// switched, and destroyed. This doesn't test the actual autocompletion
+// popups, only their integration with the editor.
+function test() {
+ waitForExplicitFinish();
+ setup((ed, win) => {
+ let edWin = ed.container.contentWindow.wrappedJSObject;
+ testJS(ed, edWin);
+ testCSS(ed, edWin);
+ testPref(ed, edWin);
+ teardown(ed, win);
+ });
+}
+
+function testJS(ed, win) {
+ ok(!ed.getOption("autocomplete"), "Autocompletion is not set");
+ ok(!win.tern, "Tern is not defined on the window");
+
+ ed.setMode(Editor.modes.js);
+ ed.setOption("autocomplete", true);
+
+ ok(ed.getOption("autocomplete"), "Autocompletion is set");
+ ok(win.tern, "Tern is defined on the window");
+}
+
+function testCSS(ed, win) {
+ ok(ed.getOption("autocomplete"), "Autocompletion is set");
+ ok(win.tern, "Tern is currently defined on the window");
+
+ ed.setMode(Editor.modes.css);
+ ed.setOption("autocomplete", true);
+
+ ok(ed.getOption("autocomplete"), "Autocompletion is still set");
+ ok(!win.tern, "Tern is no longer defined on the window");
+}
+
+function testPref(ed, win) {
+ ed.setMode(Editor.modes.js);
+ ed.setOption("autocomplete", true);
+
+ ok(ed.getOption("autocomplete"), "Autocompletion is set");
+ ok(win.tern, "Tern is defined on the window");
+
+ info("Preffing autocompletion off");
+ Services.prefs.setBoolPref(AUTOCOMPLETION_PREF, false);
+
+ ok(ed.getOption("autocomplete"), "Autocompletion is still set");
+ ok(!win.tern, "Tern is no longer defined on the window");
+
+ Services.prefs.clearUserPref(AUTOCOMPLETION_PREF);
+}
diff --git a/devtools/client/sourceeditor/test/browser_editor_autocomplete_events.js b/devtools/client/sourceeditor/test/browser_editor_autocomplete_events.js
new file mode 100644
index 000000000..e2f976afe
--- /dev/null
+++ b/devtools/client/sourceeditor/test/browser_editor_autocomplete_events.js
@@ -0,0 +1,126 @@
+/* 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 {InspectorFront} = require("devtools/shared/fronts/inspector");
+const AUTOCOMPLETION_PREF = "devtools.editor.autocomplete";
+const TEST_URI = "data:text/html;charset=UTF-8,<html><body><bar></bar>" +
+ "<div id='baz'></div><body></html>";
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ yield runTests();
+});
+
+function* runTests() {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ yield target.makeRemote();
+ let inspector = InspectorFront(target.client, target.form);
+ let walker = yield inspector.getWalker();
+ let {ed, win, edWin} = yield setup(null, {
+ autocomplete: true,
+ mode: Editor.modes.css,
+ autocompleteOpts: {walker: walker, cssProperties: getClientCssProperties()}
+ });
+ yield testMouse(ed, edWin);
+ yield testKeyboard(ed, edWin);
+ yield testKeyboardCycle(ed, edWin);
+ yield testKeyboardCycleForPrefixedString(ed, edWin);
+ yield testKeyboardCSSComma(ed, edWin);
+ teardown(ed, win);
+}
+
+function* testKeyboard(ed, win) {
+ ed.focus();
+ ed.setText("b");
+ ed.setCursor({line: 1, ch: 1});
+
+ let popupOpened = ed.getAutocompletionPopup().once("popup-opened");
+
+ let autocompleteKey =
+ Editor.keyFor("autocompletion", { noaccel: true }).toUpperCase();
+ EventUtils.synthesizeKey("VK_" + autocompleteKey, { ctrlKey: true }, win);
+
+ info("Waiting for popup to be opened");
+ yield popupOpened;
+
+ EventUtils.synthesizeKey("VK_RETURN", { }, win);
+ is(ed.getText(), "bar", "Editor text has been updated");
+}
+
+function* testKeyboardCycle(ed, win) {
+ ed.focus();
+ ed.setText("b");
+ ed.setCursor({line: 1, ch: 1});
+
+ let popupOpened = ed.getAutocompletionPopup().once("popup-opened");
+
+ let autocompleteKey =
+ Editor.keyFor("autocompletion", { noaccel: true }).toUpperCase();
+ EventUtils.synthesizeKey("VK_" + autocompleteKey, { ctrlKey: true }, win);
+
+ info("Waiting for popup to be opened");
+ yield popupOpened;
+
+ EventUtils.synthesizeKey("VK_DOWN", { }, win);
+ is(ed.getText(), "bar", "Editor text has been updated");
+
+ EventUtils.synthesizeKey("VK_DOWN", { }, win);
+ is(ed.getText(), "body", "Editor text has been updated");
+
+ EventUtils.synthesizeKey("VK_DOWN", { }, win);
+ is(ed.getText(), "#baz", "Editor text has been updated");
+}
+
+function* testKeyboardCycleForPrefixedString(ed, win) {
+ ed.focus();
+ ed.setText("#b");
+ ed.setCursor({line: 1, ch: 2});
+
+ let popupOpened = ed.getAutocompletionPopup().once("popup-opened");
+
+ let autocompleteKey =
+ Editor.keyFor("autocompletion", { noaccel: true }).toUpperCase();
+ EventUtils.synthesizeKey("VK_" + autocompleteKey, { ctrlKey: true }, win);
+
+ info("Waiting for popup to be opened");
+ yield popupOpened;
+
+ EventUtils.synthesizeKey("VK_DOWN", { }, win);
+ is(ed.getText(), "#baz", "Editor text has been updated");
+}
+
+function* testKeyboardCSSComma(ed, win) {
+ ed.focus();
+ ed.setText("b");
+ ed.setCursor({line: 1, ch: 1});
+
+ let isPopupOpened = false;
+ let popupOpened = ed.getAutocompletionPopup().once("popup-opened");
+ popupOpened.then(() => isPopupOpened = true);
+
+ EventUtils.synthesizeKey(",", { }, win);
+
+ yield wait(500);
+
+ ok(!isPopupOpened, "Autocompletion shouldn't be opened");
+}
+
+function* testMouse(ed, win) {
+ ed.focus();
+ ed.setText("b");
+ ed.setCursor({line: 1, ch: 1});
+
+ let popupOpened = ed.getAutocompletionPopup().once("popup-opened");
+
+ let autocompleteKey =
+ Editor.keyFor("autocompletion", { noaccel: true }).toUpperCase();
+ EventUtils.synthesizeKey("VK_" + autocompleteKey, { ctrlKey: true }, win);
+
+ info("Waiting for popup to be opened");
+ yield popupOpened;
+ ed.getAutocompletionPopup()._list.children[2].click();
+ is(ed.getText(), "#baz", "Editor text has been updated");
+}
diff --git a/devtools/client/sourceeditor/test/browser_editor_autocomplete_js.js b/devtools/client/sourceeditor/test/browser_editor_autocomplete_js.js
new file mode 100644
index 000000000..31fa6878f
--- /dev/null
+++ b/devtools/client/sourceeditor/test/browser_editor_autocomplete_js.js
@@ -0,0 +1,45 @@
+/* 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";
+
+// Test to make sure that JS autocompletion is opening popups.
+function test() {
+ waitForExplicitFinish();
+ setup((ed, win) => {
+ let edWin = ed.container.contentWindow.wrappedJSObject;
+ testJS(ed, edWin).then(() => {
+ teardown(ed, win);
+ });
+ });
+}
+
+function testJS(ed, win) {
+ ok(!ed.getOption("autocomplete"), "Autocompletion is not set");
+ ok(!win.tern, "Tern is not defined on the window");
+
+ ed.setMode(Editor.modes.js);
+ ed.setOption("autocomplete", true);
+
+ ok(ed.getOption("autocomplete"), "Autocompletion is set");
+ ok(win.tern, "Tern is defined on the window");
+
+ ed.focus();
+ ed.setText("document.");
+ ed.setCursor({line: 0, ch: 9});
+
+ let waitForSuggestion = promise.defer();
+
+ ed.on("before-suggest", () => {
+ info("before-suggest has been triggered");
+ EventUtils.synthesizeKey("VK_ESCAPE", { }, win);
+ waitForSuggestion.resolve();
+ });
+
+ let autocompleteKey =
+ Editor.keyFor("autocompletion", { noaccel: true }).toUpperCase();
+ EventUtils.synthesizeKey("VK_" + autocompleteKey, { ctrlKey: true }, win);
+
+ return waitForSuggestion.promise;
+}
diff --git a/devtools/client/sourceeditor/test/browser_editor_basic.js b/devtools/client/sourceeditor/test/browser_editor_basic.js
new file mode 100644
index 000000000..503b06afe
--- /dev/null
+++ b/devtools/client/sourceeditor/test/browser_editor_basic.js
@@ -0,0 +1,62 @@
+/* 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";
+
+function test() {
+ waitForExplicitFinish();
+ setup((ed, win) => {
+ // appendTo
+ let src = win.document.querySelector("iframe").getAttribute("src");
+ ok(~src.indexOf(".CodeMirror"), "correct iframe is there");
+
+ // getOption/setOption
+ ok(ed.getOption("styleActiveLine"), "getOption works");
+ ed.setOption("styleActiveLine", false);
+ ok(!ed.getOption("styleActiveLine"), "setOption works");
+
+ // Language modes
+ is(ed.getMode(), Editor.modes.text, "getMode");
+ ed.setMode(Editor.modes.js);
+ is(ed.getMode(), Editor.modes.js, "setMode");
+
+ // Content
+ is(ed.getText(), "Hello.", "getText");
+ ed.setText("Hi.\nHow are you?");
+ is(ed.getText(), "Hi.\nHow are you?", "setText");
+ is(ed.getText(1), "How are you?", "getText(num)");
+ is(ed.getText(5), "", "getText(num) when num is out of scope");
+
+ ed.replaceText("YOU", { line: 1, ch: 8 }, { line: 1, ch: 11 });
+ is(ed.getText(1), "How are YOU?", "replaceText(str, from, to)");
+ ed.replaceText("you?", { line: 1, ch: 8 });
+ is(ed.getText(1), "How are you?", "replaceText(str, from)");
+ ed.replaceText("Hello.");
+ is(ed.getText(), "Hello.", "replaceText(str)");
+
+ ed.insertText(", sir/madam", { line: 0, ch: 5});
+ is(ed.getText(), "Hello, sir/madam.", "insertText");
+
+ // Add-ons
+ ed.extend({ whoami: () => "Anton", whereami: () => "Mozilla" });
+ is(ed.whoami(), "Anton", "extend/1");
+ is(ed.whereami(), "Mozilla", "extend/2");
+
+ // Line classes
+ ed.setText("Hello!\nHow are you?");
+ ok(!ed.hasLineClass(0, "test"), "no test line class");
+ ed.addLineClass(0, "test");
+ ok(ed.hasLineClass(0, "test"), "test line class is there");
+ ed.removeLineClass(0, "test");
+ ok(!ed.hasLineClass(0, "test"), "test line class is gone");
+
+ // Font size
+ let size = ed.getFontSize();
+ is("number", typeof size, "we have the default font size");
+ ed.setFontSize(ed.getFontSize() + 1);
+ is(ed.getFontSize(), size + 1, "new font size was set");
+
+ teardown(ed, win);
+ });
+}
diff --git a/devtools/client/sourceeditor/test/browser_editor_cursor.js b/devtools/client/sourceeditor/test/browser_editor_cursor.js
new file mode 100644
index 000000000..236b3d152
--- /dev/null
+++ b/devtools/client/sourceeditor/test/browser_editor_cursor.js
@@ -0,0 +1,44 @@
+/* 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";
+
+function test() {
+ waitForExplicitFinish();
+ setup((ed, win) => {
+ ch(ed.getCursor(), { line: 0, ch: 0 }, "default cursor position is ok");
+ ed.setText("Hello.\nHow are you?");
+
+ ed.setCursor({ line: 1, ch: 5 });
+ ch(ed.getCursor(), { line: 1, ch: 5 }, "setCursor({ line, ch })");
+
+ ch(ed.getPosition(7), { line: 1, ch: 0}, "getPosition(num)");
+ ch(ed.getPosition(7, 1)[0], { line: 1, ch: 0}, "getPosition(num, num)[0]");
+ ch(ed.getPosition(7, 1)[1], { line: 0, ch: 1}, "getPosition(num, num)[1]");
+
+ ch(ed.getOffset({ line: 1, ch: 0 }), 7, "getOffset(num)");
+ ch(ed.getOffset({ line: 1, ch: 0 }, { line: 0, ch: 1 })[0], 7,
+ "getOffset(num, num)[0]");
+ ch(ed.getOffset({ line: 1, ch: 0 }, { line: 0, ch: 1 })[0], 2,
+ "getOffset(num, num)[1]");
+
+ is(ed.getSelection(), "", "nothing is selected");
+ ed.setSelection({ line: 0, ch: 0 }, { line: 0, ch: 5 });
+ is(ed.getSelection(), "Hello", "setSelection");
+
+ ed.dropSelection();
+ is(ed.getSelection(), "", "dropSelection");
+
+ // Check that shift-click on a gutter selects the whole line (bug 919707)
+ let iframe = win.document.querySelector("iframe");
+ let gutter =
+ iframe.contentWindow.document.querySelector(".CodeMirror-gutters");
+
+ EventUtils.sendMouseEvent({ type: "mousedown", shiftKey: true }, gutter,
+ iframe.contentWindow);
+ is(ed.getSelection(), "Hello.", "shift-click");
+
+ teardown(ed, win);
+ });
+}
diff --git a/devtools/client/sourceeditor/test/browser_editor_find_again.js b/devtools/client/sourceeditor/test/browser_editor_find_again.js
new file mode 100644
index 000000000..f3d80095b
--- /dev/null
+++ b/devtools/client/sourceeditor/test/browser_editor_find_again.js
@@ -0,0 +1,215 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2; fill-column: 80 -*- */
+/* 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 {LocalizationHelper} = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper("devtools/client/locales/sourceeditor.properties");
+
+const { OS } = Services.appinfo;
+
+// On linux, getting immediately the selection's range here fails, returning
+const FIND_KEY = L10N.getStr("find.key");
+const FINDNEXT_KEY = L10N.getStr("findNext.key");
+const FINDPREV_KEY = L10N.getStr("findPrev.key");
+// the replace's key with the appropriate modifiers based on OS
+const REPLACE_KEY = OS == "Darwin" ? L10N.getStr("replaceAllMac.key") : L10N.getStr("replaceAll.key");
+
+// values like it's not selected – even if the selection is visible.
+// For the record, setting the selection's range immediately doesn't have
+// any effect.
+// It's like the <input> is not ready yet.
+// Therefore, we trigger the UI focus event to the <input>, waiting for the
+// response.
+// Using a timeout could also work, but that is more precise, ensuring also
+// the execution of the listeners added to the <input>'s focus.
+const dispatchAndWaitForFocus = (target) => new Promise((resolve) => {
+ target.addEventListener("focus", function listener() {
+ target.removeEventListener("focus", listener);
+ resolve(target);
+ });
+
+ target.dispatchEvent(new UIEvent("focus"));
+});
+
+function openSearchBox(ed) {
+ let edDoc = ed.container.contentDocument;
+ let edWin = edDoc.defaultView;
+
+ let input = edDoc.querySelector("input[type=search]");
+ ok(!input, "search box closed");
+
+ // The editor needs the focus to properly receive the `synthesizeKey`
+ ed.focus();
+
+ synthesizeKeyShortcut(FINDNEXT_KEY, edWin);
+ input = edDoc.querySelector("input[type=search]");
+ ok(input, "find again command key opens the search box");
+}
+
+function testFindAgain(ed, inputLine, expectCursor, isFindPrev = false) {
+ let edDoc = ed.container.contentDocument;
+ let edWin = edDoc.defaultView;
+
+ let input = edDoc.querySelector("input[type=search]");
+ input.value = inputLine;
+
+ // Ensure the input has the focus before send the key – necessary on Linux,
+ // it seems that during the tests can be lost
+ input.focus();
+
+ if (isFindPrev) {
+ synthesizeKeyShortcut(FINDPREV_KEY, edWin);
+ } else {
+ synthesizeKeyShortcut(FINDNEXT_KEY, edWin);
+ }
+
+ ch(ed.getCursor(), expectCursor,
+ "find: " + inputLine + " expects cursor: " + expectCursor.toSource());
+}
+
+const testSearchBoxTextIsSelected = Task.async(function* (ed) {
+ let edDoc = ed.container.contentDocument;
+ let edWin = edDoc.defaultView;
+
+ let input = edDoc.querySelector("input[type=search]");
+ ok(input, "search box is opened");
+
+ // Ensure the input has the focus before send the key – necessary on Linux,
+ // it seems that during the tests can be lost
+ input.focus();
+
+ // Close search box
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, edWin);
+
+ input = edDoc.querySelector("input[type=search]");
+ ok(!input, "search box is closed");
+
+ // Re-open the search box
+ synthesizeKeyShortcut(FIND_KEY, edWin);
+
+ input = edDoc.querySelector("input[type=search]");
+ ok(input, "find command key opens the search box");
+
+ yield dispatchAndWaitForFocus(input);
+
+ let { selectionStart, selectionEnd, value } = input;
+
+ ok(selectionStart === 0 && selectionEnd === value.length,
+ "search box's text is selected when re-opened");
+
+ // Removing selection
+ input.setSelectionRange(0, 0);
+
+ synthesizeKeyShortcut(FIND_KEY, edWin);
+
+ ({ selectionStart, selectionEnd } = input);
+
+ ok(selectionStart === 0 && selectionEnd === value.length,
+ "search box's text is selected when find key is pressed");
+
+ // Close search box
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, edWin);
+});
+
+const testReplaceBoxTextIsSelected = Task.async(function* (ed) {
+ let edDoc = ed.container.contentDocument;
+ let edWin = edDoc.defaultView;
+
+ let input = edDoc.querySelector(".CodeMirror-dialog > input");
+ ok(!input, "dialog box with replace is closed");
+
+ // The editor needs the focus to properly receive the `synthesizeKey`
+ ed.focus();
+
+ synthesizeKeyShortcut(REPLACE_KEY, edWin);
+
+ input = edDoc.querySelector(".CodeMirror-dialog > input");
+ ok(input, "dialog box with replace is opened");
+
+ input.value = "line 5";
+
+ // Ensure the input has the focus before send the key – necessary on Linux,
+ // it seems that during the tests can be lost
+ input.focus();
+
+ yield dispatchAndWaitForFocus(input);
+
+ let { selectionStart, selectionEnd, value } = input;
+
+ ok(!(selectionStart === 0 && selectionEnd === value.length),
+ "Text in dialog box is not selected");
+
+ synthesizeKeyShortcut(REPLACE_KEY, edWin);
+
+ ({ selectionStart, selectionEnd } = input);
+
+ ok(selectionStart === 0 && selectionEnd === value.length,
+ "dialog box's text is selected when replace key is pressed");
+
+ // Close dialog box
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, edWin);
+});
+
+add_task(function* () {
+ let { ed, win } = yield setup();
+
+ ed.setText([
+ "// line 1",
+ "// line 2",
+ "// line 3",
+ "// line 4",
+ "// line 5"
+ ].join("\n"));
+
+ yield promiseWaitForFocus();
+
+ openSearchBox(ed);
+
+ let testVectors = [
+ // Starting here expect data needs to get updated for length changes to
+ // "textLines" above.
+ ["line",
+ {line: 0, ch: 7}],
+ ["line",
+ {line: 1, ch: 8}],
+ ["line",
+ {line: 2, ch: 9}],
+ ["line",
+ {line: 3, ch: 10}],
+ ["line",
+ {line: 4, ch: 11}],
+ ["ne 3",
+ {line: 2, ch: 11}],
+ ["line 1",
+ {line: 0, ch: 9}],
+ // Testing find prev
+ ["line",
+ {line: 4, ch: 11},
+ true],
+ ["line",
+ {line: 3, ch: 10},
+ true],
+ ["line",
+ {line: 2, ch: 9},
+ true],
+ ["line",
+ {line: 1, ch: 8},
+ true],
+ ["line",
+ {line: 0, ch: 7},
+ true]
+ ];
+
+ for (let v of testVectors) {
+ yield testFindAgain(ed, ...v);
+ }
+
+ yield testSearchBoxTextIsSelected(ed);
+
+ yield testReplaceBoxTextIsSelected(ed);
+
+ teardown(ed, win);
+});
diff --git a/devtools/client/sourceeditor/test/browser_editor_goto_line.js b/devtools/client/sourceeditor/test/browser_editor_goto_line.js
new file mode 100644
index 000000000..eedde41dd
--- /dev/null
+++ b/devtools/client/sourceeditor/test/browser_editor_goto_line.js
@@ -0,0 +1,131 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2; fill-column: 80 -*- */
+/* 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";
+
+function testJumpToLine(ed, inputLine, expectCursor) {
+ ed.jumpToLine();
+ let editorDoc = ed.container.contentDocument;
+ let lineInput = editorDoc.querySelector("input");
+ lineInput.value = inputLine;
+ EventUtils.synthesizeKey("VK_RETURN", { }, editorDoc.defaultView);
+ // CodeMirror lines and columns are 0-based, Scratchpad UI is 1-based.
+ ch(ed.getCursor(), expectCursor,
+ "jumpToLine " + inputLine + " expects cursor " + expectCursor.toSource());
+}
+
+function test() {
+ waitForExplicitFinish();
+ setup((ed, win) => {
+ let textLines = [
+ "// line 1",
+ "// line 2",
+ "// line 3",
+ "// line 4",
+ "// line 5",
+ ""];
+ ed.setText(textLines.join("\n"));
+ waitForFocus(function () {
+ let testVectors = [
+ // Various useless inputs go to line 0, column 0 or do nothing.
+ ["",
+ {line: 0, ch: 0}],
+ [":",
+ {line: 0, ch: 0}],
+ [" ",
+ {line: 0, ch: 0}],
+ [" : ",
+ {line: 0, ch: 0}],
+ ["a:b",
+ {line: 0, ch: 0}],
+ ["LINE: COLUMN ",
+ {line: 0, ch: 0}],
+ ["-1",
+ {line: 0, ch: 0}],
+ [":-1",
+ {line: 0, ch: 0}],
+ ["-1:-1",
+ {line: 0, ch: 0}],
+ ["0",
+ {line: 0, ch: 0}],
+ [":0",
+ {line: 0, ch: 0}],
+ ["0:0",
+ {line: 0, ch: 0}],
+ // Starting here expect data needs to get updated for length changes to
+ // "textLines" above.
+ // Just jump to line
+ ["1",
+ {line: 0, ch: 0}],
+ // Jump to second character in line
+ ["1:2",
+ {line: 0, ch: 1}],
+ // Jump to last character on line
+ ["1:9",
+ {line: 0, ch: 8}],
+ // Jump just after last character on line (end of line)
+ ["1:10",
+ {line: 0, ch: 9}],
+ // Jump one character past end of line (gets clamped to end of line)
+ ["1:11",
+ {line: 0, ch: 9}],
+ ["2",
+ {line: 1, ch: 0}],
+ ["2:2",
+ {line: 1, ch: 1}],
+ ["2:10",
+ {line: 1, ch: 9}],
+ ["2:11",
+ {line: 1, ch: 10}],
+ ["2:12",
+ {line: 1, ch: 10}],
+ ["3",
+ {line: 2, ch: 0}],
+ ["3:2",
+ {line: 2, ch: 1}],
+ ["3:11",
+ {line: 2, ch: 10}],
+ ["3:12",
+ {line: 2, ch: 11}],
+ ["3:13",
+ {line: 2, ch: 11}],
+ ["4",
+ {line: 3, ch: 0}],
+ ["4:2",
+ {line: 3, ch: 1}],
+ ["4:12",
+ {line: 3, ch: 11}],
+ ["4:13",
+ {line: 3, ch: 12}],
+ ["4:14",
+ {line: 3, ch: 12}],
+ ["5",
+ {line: 4, ch: 0}],
+ ["5:2",
+ {line: 4, ch: 1}],
+ ["5:13",
+ {line: 4, ch: 12}],
+ ["5:14",
+ {line: 4, ch: 13}],
+ ["5:15",
+ {line: 4, ch: 13}],
+ // One line beyond last newline in editor text:
+ ["6",
+ {line: 5, ch: 0}],
+ ["6:2",
+ {line: 5, ch: 0}],
+ // Two line beyond last newline in editor text (gets clamped):
+ ["7",
+ {line: 5, ch: 0}],
+ ["7:2",
+ {line: 5, ch: 0}]
+ ];
+ testVectors.forEach(vector => {
+ testJumpToLine(ed, vector[0], vector[1]);
+ });
+ teardown(ed, win);
+ });
+ });
+}
diff --git a/devtools/client/sourceeditor/test/browser_editor_history.js b/devtools/client/sourceeditor/test/browser_editor_history.js
new file mode 100644
index 000000000..9098afdf4
--- /dev/null
+++ b/devtools/client/sourceeditor/test/browser_editor_history.js
@@ -0,0 +1,32 @@
+/* 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";
+
+function test() {
+ waitForExplicitFinish();
+ setup((ed, win) => {
+ ok(ed.isClean(), "default isClean");
+ ok(!ed.canUndo(), "default canUndo");
+ ok(!ed.canRedo(), "default canRedo");
+
+ ed.setText("Hello, World!");
+ ok(!ed.isClean(), "isClean");
+ ok(ed.canUndo(), "canUndo");
+ ok(!ed.canRedo(), "canRedo");
+
+ ed.undo();
+ ok(ed.isClean(), "isClean after undo");
+ ok(!ed.canUndo(), "canUndo after undo");
+ ok(ed.canRedo(), "canRedo after undo");
+
+ ed.setText("What's up?");
+ ed.setClean();
+ ok(ed.isClean(), "isClean after setClean");
+ ok(ed.canUndo(), "canUndo after setClean");
+ ok(!ed.canRedo(), "canRedo after setClean");
+
+ teardown(ed, win);
+ });
+}
diff --git a/devtools/client/sourceeditor/test/browser_editor_markers.js b/devtools/client/sourceeditor/test/browser_editor_markers.js
new file mode 100644
index 000000000..64a78d510
--- /dev/null
+++ b/devtools/client/sourceeditor/test/browser_editor_markers.js
@@ -0,0 +1,39 @@
+/* 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";
+
+function test() {
+ waitForExplicitFinish();
+ setup((ed, win) => {
+ ok(!ed.hasMarker(0, "breakpoints", "test"), "default is ok");
+ ed.addMarker(0, "breakpoints", "test");
+ ed.addMarker(0, "breakpoints", "test2");
+ ok(ed.hasMarker(0, "breakpoints", "test"), "addMarker/1");
+ ok(ed.hasMarker(0, "breakpoints", "test2"), "addMarker/2");
+
+ ed.removeMarker(0, "breakpoints", "test");
+ ok(!ed.hasMarker(0, "breakpoints", "test"), "removeMarker/1");
+ ok(ed.hasMarker(0, "breakpoints", "test2"), "removeMarker/2");
+
+ ed.removeAllMarkers("breakpoints");
+ ok(!ed.hasMarker(0, "breakpoints", "test"), "removeAllMarkers/1");
+ ok(!ed.hasMarker(0, "breakpoints", "test2"), "removeAllMarkers/2");
+
+ ed.addMarker(0, "breakpoints", "breakpoint");
+ ed.setMarkerListeners(0, "breakpoints", "breakpoint", {
+ "click": (line, marker, param) => {
+ is(line, 0, "line is ok");
+ is(marker.className, "breakpoint", "marker is ok");
+ ok(param, "click is ok");
+
+ teardown(ed, win);
+ }
+ }, [ true ]);
+
+ const env = win.document.querySelector("iframe").contentWindow;
+ const div = env.document.querySelector("div.breakpoint");
+ div.click();
+ });
+}
diff --git a/devtools/client/sourceeditor/test/browser_editor_movelines.js b/devtools/client/sourceeditor/test/browser_editor_movelines.js
new file mode 100644
index 000000000..60a7f6865
--- /dev/null
+++ b/devtools/client/sourceeditor/test/browser_editor_movelines.js
@@ -0,0 +1,63 @@
+/* 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";
+
+function test() {
+ waitForExplicitFinish();
+ setup((ed, win) => {
+ let simpleProg = "function foo() {\n let i = 1;\n let j = 2;\n " +
+ "return bar;\n}";
+ ed.setText(simpleProg);
+
+ // Move first line up
+ ed.setCursor({ line: 0, ch: 0 });
+ ed.moveLineUp();
+ is(ed.getText(0), "function foo() {", "getText(num)");
+ ch(ed.getCursor(), { line: 0, ch: 0 }, "getCursor");
+
+ // Move last line down
+ ed.setCursor({ line: 4, ch: 0 });
+ ed.moveLineDown();
+ is(ed.getText(4), "}", "getText(num)");
+ ch(ed.getCursor(), { line: 4, ch: 0 }, "getCursor");
+
+ // Move line 2 up
+ ed.setCursor({ line: 1, ch: 5});
+ ed.moveLineUp();
+ is(ed.getText(0), " let i = 1;", "getText(num)");
+ is(ed.getText(1), "function foo() {", "getText(num)");
+ ch(ed.getCursor(), { line: 0, ch: 5 }, "getCursor");
+
+ // Undo previous move by moving line 1 down
+ ed.moveLineDown();
+ is(ed.getText(0), "function foo() {", "getText(num)");
+ is(ed.getText(1), " let i = 1;", "getText(num)");
+ ch(ed.getCursor(), { line: 1, ch: 5 }, "getCursor");
+
+ // Move line 2 and 3 up
+ ed.setSelection({ line: 1, ch: 0 }, { line: 2, ch: 0 });
+ ed.moveLineUp();
+ is(ed.getText(0), " let i = 1;", "getText(num)");
+ is(ed.getText(1), " let j = 2;", "getText(num)");
+ is(ed.getText(2), "function foo() {", "getText(num)");
+ ch(ed.getCursor("start"), { line: 0, ch: 0 }, "getCursor(string)");
+ ch(ed.getCursor("end"), { line: 1, ch: 0 }, "getCursor(string)");
+
+ // Move line 1 to 3 down twice
+ ed.dropSelection();
+ ed.setSelection({ line: 0, ch: 7 }, { line: 2, ch: 5 });
+ ed.moveLineDown();
+ ed.moveLineDown();
+ is(ed.getText(0), " return bar;", "getText(num)");
+ is(ed.getText(1), "}", "getText(num)");
+ is(ed.getText(2), " let i = 1;", "getText(num)");
+ is(ed.getText(3), " let j = 2;", "getText(num)");
+ is(ed.getText(4), "function foo() {", "getText(num)");
+ ch(ed.getCursor("start"), { line: 2, ch: 7 }, "getCursor(string)");
+ ch(ed.getCursor("end"), { line: 4, ch: 5 }, "getCursor(string)");
+
+ teardown(ed, win);
+ });
+}
diff --git a/devtools/client/sourceeditor/test/browser_editor_prefs.js b/devtools/client/sourceeditor/test/browser_editor_prefs.js
new file mode 100644
index 000000000..0c8c7782f
--- /dev/null
+++ b/devtools/client/sourceeditor/test/browser_editor_prefs.js
@@ -0,0 +1,121 @@
+/* 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";
+
+// Test to make sure that the editor reacts to preference changes
+
+const TAB_SIZE = "devtools.editor.tabsize";
+const ENABLE_CODE_FOLDING = "devtools.editor.enableCodeFolding";
+const EXPAND_TAB = "devtools.editor.expandtab";
+const KEYMAP = "devtools.editor.keymap";
+const AUTO_CLOSE = "devtools.editor.autoclosebrackets";
+const AUTOCOMPLETE = "devtools.editor.autocomplete";
+const DETECT_INDENT = "devtools.editor.detectindentation";
+
+function test() {
+ waitForExplicitFinish();
+ setup((ed, win) => {
+ Assert.deepEqual(ed.getOption("gutters"), [
+ "CodeMirror-linenumbers",
+ "breakpoints",
+ "CodeMirror-foldgutter"], "gutters is correct");
+
+ ed.setText("Checking preferences.");
+
+ info("Turning prefs off");
+
+ ed.setOption("autocomplete", true);
+
+ Services.prefs.setIntPref(TAB_SIZE, 2);
+ Services.prefs.setBoolPref(ENABLE_CODE_FOLDING, false);
+ Services.prefs.setBoolPref(EXPAND_TAB, false);
+ Services.prefs.setCharPref(KEYMAP, "default");
+ Services.prefs.setBoolPref(AUTO_CLOSE, false);
+ Services.prefs.setBoolPref(AUTOCOMPLETE, false);
+ Services.prefs.setBoolPref(DETECT_INDENT, false);
+
+ Assert.deepEqual(ed.getOption("gutters"), [
+ "CodeMirror-linenumbers",
+ "breakpoints"], "gutters is correct");
+
+ is(ed.getOption("tabSize"), 2, "tabSize is correct");
+ is(ed.getOption("indentUnit"), 2, "indentUnit is correct");
+ is(ed.getOption("foldGutter"), false, "foldGutter is correct");
+ is(ed.getOption("enableCodeFolding"), undefined,
+ "enableCodeFolding is correct");
+ is(ed.getOption("indentWithTabs"), true, "indentWithTabs is correct");
+ is(ed.getOption("keyMap"), "default", "keyMap is correct");
+ is(ed.getOption("autoCloseBrackets"), "", "autoCloseBrackets is correct");
+ is(ed.getOption("autocomplete"), true, "autocomplete is correct");
+ ok(!ed.isAutocompletionEnabled(), "Autocompletion is not enabled");
+
+ info("Turning prefs on");
+
+ Services.prefs.setIntPref(TAB_SIZE, 4);
+ Services.prefs.setBoolPref(ENABLE_CODE_FOLDING, true);
+ Services.prefs.setBoolPref(EXPAND_TAB, true);
+ Services.prefs.setCharPref(KEYMAP, "sublime");
+ Services.prefs.setBoolPref(AUTO_CLOSE, true);
+ Services.prefs.setBoolPref(AUTOCOMPLETE, true);
+
+ Assert.deepEqual(ed.getOption("gutters"), [
+ "CodeMirror-linenumbers",
+ "breakpoints",
+ "CodeMirror-foldgutter"], "gutters is correct");
+
+ is(ed.getOption("tabSize"), 4, "tabSize is correct");
+ is(ed.getOption("indentUnit"), 4, "indentUnit is correct");
+ is(ed.getOption("foldGutter"), true, "foldGutter is correct");
+ is(ed.getOption("enableCodeFolding"), undefined,
+ "enableCodeFolding is correct");
+ is(ed.getOption("indentWithTabs"), false, "indentWithTabs is correct");
+ is(ed.getOption("keyMap"), "sublime", "keyMap is correct");
+ is(ed.getOption("autoCloseBrackets"), "()[]{}''\"\"``",
+ "autoCloseBrackets is correct");
+ is(ed.getOption("autocomplete"), true, "autocomplete is correct");
+ ok(ed.isAutocompletionEnabled(), "Autocompletion is enabled");
+
+ info("Forcing foldGutter off using enableCodeFolding");
+ ed.setOption("enableCodeFolding", false);
+
+ is(ed.getOption("foldGutter"), false, "foldGutter is correct");
+ is(ed.getOption("enableCodeFolding"), false,
+ "enableCodeFolding is correct");
+ Assert.deepEqual(ed.getOption("gutters"), [
+ "CodeMirror-linenumbers",
+ "breakpoints"], "gutters is correct");
+
+ info("Forcing foldGutter on using enableCodeFolding");
+ ed.setOption("enableCodeFolding", true);
+
+ is(ed.getOption("foldGutter"), true, "foldGutter is correct");
+ is(ed.getOption("enableCodeFolding"), true, "enableCodeFolding is correct");
+ Assert.deepEqual(ed.getOption("gutters"), [
+ "CodeMirror-linenumbers",
+ "breakpoints",
+ "CodeMirror-foldgutter"], "gutters is correct");
+
+ info("Checking indentation detection");
+
+ Services.prefs.setBoolPref(DETECT_INDENT, true);
+
+ ed.setText("Detecting\n\tTabs");
+ is(ed.getOption("indentWithTabs"), true, "indentWithTabs is correct");
+ is(ed.getOption("indentUnit"), 4, "indentUnit is correct");
+
+ ed.setText("body {\n color:red;\n a:b;\n}");
+ is(ed.getOption("indentWithTabs"), false, "indentWithTabs is correct");
+ is(ed.getOption("indentUnit"), 2, "indentUnit is correct");
+
+ Services.prefs.clearUserPref(TAB_SIZE);
+ Services.prefs.clearUserPref(EXPAND_TAB);
+ Services.prefs.clearUserPref(KEYMAP);
+ Services.prefs.clearUserPref(AUTO_CLOSE);
+ Services.prefs.clearUserPref(AUTOCOMPLETE);
+ Services.prefs.clearUserPref(DETECT_INDENT);
+
+ teardown(ed, win);
+ });
+}
diff --git a/devtools/client/sourceeditor/test/browser_editor_script_injection.js b/devtools/client/sourceeditor/test/browser_editor_script_injection.js
new file mode 100644
index 000000000..05487b4f2
--- /dev/null
+++ b/devtools/client/sourceeditor/test/browser_editor_script_injection.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test the externalScripts option, which allows custom language modes or
+// other scripts to be injected into the editor window. See Bug 1089428.
+
+"use strict";
+
+add_task(function* () {
+ yield runTest();
+});
+
+function* runTest() {
+ const baseURL =
+ "chrome://mochitests/content/browser/devtools/client/sourceeditor/test";
+ const injectedText = "Script successfully injected!";
+
+ let {ed, win} = yield setup(null, {
+ mode: "ruby",
+ externalScripts: [`${baseURL}/cm_script_injection_test.js`,
+ `${baseURL}/cm_mode_ruby.js`]
+ });
+
+ is(ed.getText(), injectedText, "The text has been injected");
+ is(ed.getOption("mode"), "ruby", "The ruby mode is correctly set");
+ teardown(ed, win);
+}
diff --git a/devtools/client/sourceeditor/test/browser_vimemacs.js b/devtools/client/sourceeditor/test/browser_vimemacs.js
new file mode 100644
index 000000000..46ff02b5e
--- /dev/null
+++ b/devtools/client/sourceeditor/test/browser_vimemacs.js
@@ -0,0 +1,17 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const URI = "chrome://mochitests/content/browser/devtools/client" +
+ "/sourceeditor/test/codemirror/vimemacs.html";
+loadHelperScript("helper_codemirror_runner.js");
+
+function test() {
+ requestLongerTimeout(4);
+ waitForExplicitFinish();
+
+ addTab(URI).then(function (tab) {
+ runCodeMirrorTest(tab.linkedBrowser);
+ });
+}
diff --git a/devtools/client/sourceeditor/test/cm_mode_ruby.js b/devtools/client/sourceeditor/test/cm_mode_ruby.js
new file mode 100644
index 000000000..01f3f9a46
--- /dev/null
+++ b/devtools/client/sourceeditor/test/cm_mode_ruby.js
@@ -0,0 +1,285 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function (mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../../lib/codemirror"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function (CodeMirror) {
+ "use strict";
+
+ CodeMirror.defineMode("ruby", function (config) {
+ function wordObj(words) {
+ var o = {};
+ for (var i = 0, e = words.length; i < e; ++i) o[words[i]] = true;
+ return o;
+ }
+ var keywords = wordObj([
+ "alias", "and", "BEGIN", "begin", "break", "case", "class", "def", "defined?", "do", "else",
+ "elsif", "END", "end", "ensure", "false", "for", "if", "in", "module", "next", "not", "or",
+ "redo", "rescue", "retry", "return", "self", "super", "then", "true", "undef", "unless",
+ "until", "when", "while", "yield", "nil", "raise", "throw", "catch", "fail", "loop", "callcc",
+ "caller", "lambda", "proc", "public", "protected", "private", "require", "load",
+ "require_relative", "extend", "autoload", "__END__", "__FILE__", "__LINE__", "__dir__"
+ ]);
+ var indentWords = wordObj(["def", "class", "case", "for", "while", "module", "then",
+ "catch", "loop", "proc", "begin"]);
+ var dedentWords = wordObj(["end", "until"]);
+ var matching = {"[": "]", "{": "}", "(": ")"};
+ var curPunc;
+
+ function chain(newtok, stream, state) {
+ state.tokenize.push(newtok);
+ return newtok(stream, state);
+ }
+
+ function tokenBase(stream, state) {
+ curPunc = null;
+ if (stream.sol() && stream.match("=begin") && stream.eol()) {
+ state.tokenize.push(readBlockComment);
+ return "comment";
+ }
+ if (stream.eatSpace()) return null;
+ var ch = stream.next(), m;
+ if (ch == "`" || ch == "'" || ch == '"') {
+ return chain(readQuoted(ch, "string", ch == '"' || ch == "`"), stream, state);
+ } else if (ch == "/") {
+ var currentIndex = stream.current().length;
+ if (stream.skipTo("/")) {
+ var search_till = stream.current().length;
+ stream.backUp(stream.current().length - currentIndex);
+ var balance = 0; // balance brackets
+ while (stream.current().length < search_till) {
+ var chchr = stream.next();
+ if (chchr == "(") balance += 1;
+ else if (chchr == ")") balance -= 1;
+ if (balance < 0) break;
+ }
+ stream.backUp(stream.current().length - currentIndex);
+ if (balance == 0)
+ return chain(readQuoted(ch, "string-2", true), stream, state);
+ }
+ return "operator";
+ } else if (ch == "%") {
+ var style = "string", embed = true;
+ if (stream.eat("s")) style = "atom";
+ else if (stream.eat(/[WQ]/)) style = "string";
+ else if (stream.eat(/[r]/)) style = "string-2";
+ else if (stream.eat(/[wxq]/)) { style = "string"; embed = false; }
+ var delim = stream.eat(/[^\w\s=]/);
+ if (!delim) return "operator";
+ if (matching.propertyIsEnumerable(delim)) delim = matching[delim];
+ return chain(readQuoted(delim, style, embed, true), stream, state);
+ } else if (ch == "#") {
+ stream.skipToEnd();
+ return "comment";
+ } else if (ch == "<" && (m = stream.match(/^<-?[\`\"\']?([a-zA-Z_?]\w*)[\`\"\']?(?:;|$)/))) {
+ return chain(readHereDoc(m[1]), stream, state);
+ } else if (ch == "0") {
+ if (stream.eat("x")) stream.eatWhile(/[\da-fA-F]/);
+ else if (stream.eat("b")) stream.eatWhile(/[01]/);
+ else stream.eatWhile(/[0-7]/);
+ return "number";
+ } else if (/\d/.test(ch)) {
+ stream.match(/^[\d_]*(?:\.[\d_]+)?(?:[eE][+\-]?[\d_]+)?/);
+ return "number";
+ } else if (ch == "?") {
+ while (stream.match(/^\\[CM]-/)) {}
+ if (stream.eat("\\")) stream.eatWhile(/\w/);
+ else stream.next();
+ return "string";
+ } else if (ch == ":") {
+ if (stream.eat("'")) return chain(readQuoted("'", "atom", false), stream, state);
+ if (stream.eat('"')) return chain(readQuoted('"', "atom", true), stream, state);
+
+ // :> :>> :< :<< are valid symbols
+ if (stream.eat(/[\<\>]/)) {
+ stream.eat(/[\<\>]/);
+ return "atom";
+ }
+
+ // :+ :- :/ :* :| :& :! are valid symbols
+ if (stream.eat(/[\+\-\*\/\&\|\:\!]/)) {
+ return "atom";
+ }
+
+ // Symbols can't start by a digit
+ if (stream.eat(/[a-zA-Z$@_\xa1-\uffff]/)) {
+ stream.eatWhile(/[\w$\xa1-\uffff]/);
+ // Only one ? ! = is allowed and only as the last character
+ stream.eat(/[\?\!\=]/);
+ return "atom";
+ }
+ return "operator";
+ } else if (ch == "@" && stream.match(/^@?[a-zA-Z_\xa1-\uffff]/)) {
+ stream.eat("@");
+ stream.eatWhile(/[\w\xa1-\uffff]/);
+ return "variable-2";
+ } else if (ch == "$") {
+ if (stream.eat(/[a-zA-Z_]/)) {
+ stream.eatWhile(/[\w]/);
+ } else if (stream.eat(/\d/)) {
+ stream.eat(/\d/);
+ } else {
+ stream.next(); // Must be a special global like $: or $!
+ }
+ return "variable-3";
+ } else if (/[a-zA-Z_\xa1-\uffff]/.test(ch)) {
+ stream.eatWhile(/[\w\xa1-\uffff]/);
+ stream.eat(/[\?\!]/);
+ if (stream.eat(":")) return "atom";
+ return "ident";
+ } else if (ch == "|" && (state.varList || state.lastTok == "{" || state.lastTok == "do")) {
+ curPunc = "|";
+ return null;
+ } else if (/[\(\)\[\]{}\\;]/.test(ch)) {
+ curPunc = ch;
+ return null;
+ } else if (ch == "-" && stream.eat(">")) {
+ return "arrow";
+ } else if (/[=+\-\/*:\.^%<>~|]/.test(ch)) {
+ var more = stream.eatWhile(/[=+\-\/*:\.^%<>~|]/);
+ if (ch == "." && !more) curPunc = ".";
+ return "operator";
+ } else {
+ return null;
+ }
+ }
+
+ function tokenBaseUntilBrace(depth) {
+ if (!depth) depth = 1;
+ return function (stream, state) {
+ if (stream.peek() == "}") {
+ if (depth == 1) {
+ state.tokenize.pop();
+ return state.tokenize[state.tokenize.length - 1](stream, state);
+ } else {
+ state.tokenize[state.tokenize.length - 1] = tokenBaseUntilBrace(depth - 1);
+ }
+ } else if (stream.peek() == "{") {
+ state.tokenize[state.tokenize.length - 1] = tokenBaseUntilBrace(depth + 1);
+ }
+ return tokenBase(stream, state);
+ };
+ }
+ function tokenBaseOnce() {
+ var alreadyCalled = false;
+ return function (stream, state) {
+ if (alreadyCalled) {
+ state.tokenize.pop();
+ return state.tokenize[state.tokenize.length - 1](stream, state);
+ }
+ alreadyCalled = true;
+ return tokenBase(stream, state);
+ };
+ }
+ function readQuoted(quote, style, embed, unescaped) {
+ return function (stream, state) {
+ var escaped = false, ch;
+
+ if (state.context.type === "read-quoted-paused") {
+ state.context = state.context.prev;
+ stream.eat("}");
+ }
+
+ while ((ch = stream.next()) != null) {
+ if (ch == quote && (unescaped || !escaped)) {
+ state.tokenize.pop();
+ break;
+ }
+ if (embed && ch == "#" && !escaped) {
+ if (stream.eat("{")) {
+ if (quote == "}") {
+ state.context = {prev: state.context, type: "read-quoted-paused"};
+ }
+ state.tokenize.push(tokenBaseUntilBrace());
+ break;
+ } else if (/[@\$]/.test(stream.peek())) {
+ state.tokenize.push(tokenBaseOnce());
+ break;
+ }
+ }
+ escaped = !escaped && ch == "\\";
+ }
+ return style;
+ };
+ }
+ function readHereDoc(phrase) {
+ return function (stream, state) {
+ if (stream.match(phrase)) state.tokenize.pop();
+ else stream.skipToEnd();
+ return "string";
+ };
+ }
+ function readBlockComment(stream, state) {
+ if (stream.sol() && stream.match("=end") && stream.eol())
+ state.tokenize.pop();
+ stream.skipToEnd();
+ return "comment";
+ }
+
+ return {
+ startState: function () {
+ return {tokenize: [tokenBase],
+ indented: 0,
+ context: {type: "top", indented: -config.indentUnit},
+ continuedLine: false,
+ lastTok: null,
+ varList: false};
+ },
+
+ token: function (stream, state) {
+ if (stream.sol()) state.indented = stream.indentation();
+ var style = state.tokenize[state.tokenize.length - 1](stream, state), kwtype;
+ var thisTok = curPunc;
+ if (style == "ident") {
+ var word = stream.current();
+ style = state.lastTok == "." ? "property"
+ : keywords.propertyIsEnumerable(stream.current()) ? "keyword"
+ : /^[A-Z]/.test(word) ? "tag"
+ : (state.lastTok == "def" || state.lastTok == "class" || state.varList) ? "def"
+ : "variable";
+ if (style == "keyword") {
+ thisTok = word;
+ if (indentWords.propertyIsEnumerable(word)) kwtype = "indent";
+ else if (dedentWords.propertyIsEnumerable(word)) kwtype = "dedent";
+ else if ((word == "if" || word == "unless") && stream.column() == stream.indentation())
+ kwtype = "indent";
+ else if (word == "do" && state.context.indented < state.indented)
+ kwtype = "indent";
+ }
+ }
+ if (curPunc || (style && style != "comment")) state.lastTok = thisTok;
+ if (curPunc == "|") state.varList = !state.varList;
+
+ if (kwtype == "indent" || /[\(\[\{]/.test(curPunc))
+ state.context = {prev: state.context, type: curPunc || style, indented: state.indented};
+ else if ((kwtype == "dedent" || /[\)\]\}]/.test(curPunc)) && state.context.prev)
+ state.context = state.context.prev;
+
+ if (stream.eol())
+ state.continuedLine = (curPunc == "\\" || style == "operator");
+ return style;
+ },
+
+ indent: function (state, textAfter) {
+ if (state.tokenize[state.tokenize.length - 1] != tokenBase) return 0;
+ var firstChar = textAfter && textAfter.charAt(0);
+ var ct = state.context;
+ var closing = ct.type == matching[firstChar] ||
+ ct.type == "keyword" && /^(?:end|until|else|elsif|when|rescue)\b/.test(textAfter);
+ return ct.indented + (closing ? 0 : config.indentUnit) +
+ (state.continuedLine ? config.indentUnit : 0);
+ },
+
+ electricChars: "}de", // enD and rescuE
+ lineComment: "#"
+ };
+ });
+
+ CodeMirror.defineMIME("text/x-ruby", "ruby");
+
+});
diff --git a/devtools/client/sourceeditor/test/cm_script_injection_test.js b/devtools/client/sourceeditor/test/cm_script_injection_test.js
new file mode 100644
index 000000000..3d4a5a359
--- /dev/null
+++ b/devtools/client/sourceeditor/test/cm_script_injection_test.js
@@ -0,0 +1,8 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+window.addEventListener("editorReady", function () {
+ editor.setText("Script successfully injected!");
+});
diff --git a/devtools/client/sourceeditor/test/codemirror/codemirror.html b/devtools/client/sourceeditor/test/codemirror/codemirror.html
new file mode 100644
index 000000000..ada04a2d0
--- /dev/null
+++ b/devtools/client/sourceeditor/test/codemirror/codemirror.html
@@ -0,0 +1,210 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>CodeMirror: Basic Tests</title>
+ <link rel="stylesheet" href="chrome://devtools/content/sourceeditor/codemirror/lib/codemirror.css">
+ <link rel="stylesheet" href="cm_mode_test.css">
+ <!--<link rel="stylesheet" href="../doc/docs.css">-->
+
+ <script src="chrome://devtools/content/sourceeditor/codemirror/codemirror.bundle.js"></script>
+
+ <style type="text/css">
+ .ok {color: #090;}
+ .fail {color: #e00;}
+ .error {color: #c90;}
+ .done {font-weight: bold;}
+ #progress {
+ background: #45d;
+ color: white;
+ text-shadow: 0 0 1px #45d, 0 0 2px #45d, 0 0 3px #45d;
+ font-weight: bold;
+ white-space: pre;
+ }
+ #testground {
+ visibility: hidden;
+ }
+ #testground.offscreen {
+ visibility: visible;
+ position: absolute;
+ left: -10000px;
+ top: -10000px;
+ }
+ .CodeMirror { border: 1px solid black; }
+ </style>
+ </head>
+ <body>
+ <h1>CodeMirror: Basic Tests</h1>
+
+ <p>A limited set of programmatic sanity tests for CodeMirror.</p>
+
+ <div style="border: 1px solid black; padding: 1px; max-width: 700px;">
+ <div style="width: 0px;" id=progress><div style="padding: 3px;">Ran <span id="progress_ran">0</span><span id="progress_total"> of 0</span> tests</div></div>
+ </div>
+ <p id=status>Please enable JavaScript...</p>
+ <div id=output></div>
+
+ <div id=testground></div>
+
+ <script src="driver.js"></script>
+ <script src="test.js"></script>
+ <script src="comment_test.js"></script>
+ <script src="doc_test.js"></script>
+ <script src="driver.js"></script>
+ <script src="emacs_test.js"></script>
+ <script src="mode_test.js"></script>
+ <script src="mode/javascript/test.js"></script>
+ <script src="multi_test.js"></script>
+ <script src="search_test.js"></script>
+
+ <!-- VIM and Emacs mode tests are in vimemacs.html
+ <script src="cm_sublime_test.js"></script>
+ <script src="cm_vim_test.js"></script>
+ <script src="cm_emacs_test.js"></script>
+ -->
+
+ <!-- These modes/addons are not used by Editor
+ <script src="doc_test.js"></script>
+ <script src="../mode/css/css.js"></script>
+ <script src="../mode/css/test.js"></script>
+ <script src="../mode/css/scss_test.js"></script>
+ <script src="../mode/xml/xml.js"></script>
+ <script src="../mode/htmlmixed/htmlmixed.js"></script>
+ <script src="../mode/ruby/ruby.js"></script>
+ <script src="../mode/haml/haml.js"></script>
+ <script src="../mode/haml/test.js"></script>
+ <script src="../mode/markdown/markdown.js"></script>
+ <script src="../mode/markdown/test.js"></script>
+ <script src="../mode/gfm/gfm.js"></script>
+ <script src="../mode/gfm/test.js"></script>
+ <script src="../mode/stex/stex.js"></script>
+ <script src="../mode/stex/test.js"></script>
+ <script src="../mode/xquery/xquery.js"></script>
+ <script src="../mode/xquery/test.js"></script>
+ <script src="../addon/mode/multiplex_test.js"></script>-->
+
+ <script>
+ window.onload = runHarness;
+ CodeMirror.on(window, 'hashchange', runHarness);
+
+ function esc(str) {
+ return str.replace(/[<&]/, function(ch) { return ch == "<" ? "&lt;" : "&amp;"; });
+ }
+
+ var output = document.getElementById("output"),
+ progress = document.getElementById("progress"),
+ progressRan = document.getElementById("progress_ran").childNodes[0],
+ progressTotal = document.getElementById("progress_total").childNodes[0];
+
+ var count = 0,
+ failed = 0,
+ skipped = 0,
+ bad = "",
+ running = false, // Flag that states tests are running
+ quit = false, // Flag to quit tests ASAP
+ verbose = false, // Adds message for *every* test to output
+ phantom = false;
+
+ function runHarness(){
+ if (running) {
+ quit = true;
+ setStatus("Restarting tests...", '', true);
+ setTimeout(function(){runHarness();}, 500);
+ return;
+ }
+ filters = [];
+ verbose = false;
+ if (window.location.hash.substr(1)){
+ var strings = window.location.hash.substr(1).split(",");
+ while (strings.length) {
+ var s = strings.shift();
+ if (s === "verbose")
+ verbose = true;
+ else
+ filters.push(parseTestFilter(decodeURIComponent(s)));
+ }
+ }
+ quit = false;
+ running = true;
+ setStatus("Loading tests...");
+ count = 0;
+ failed = 0;
+ skipped = 0;
+ bad = "";
+ totalTests = countTests();
+ progressTotal.nodeValue = " of " + totalTests;
+ progressRan.nodeValue = count;
+ output.innerHTML = '';
+ document.getElementById("testground").innerHTML = "<form>" +
+ "<textarea id=\"code\" name=\"code\"></textarea>" +
+ "<input type=submit value=ok name=submit>" +
+ "</form>";
+ runTests(displayTest);
+ }
+
+ function setStatus(message, className, force){
+ if (quit && !force) return;
+ if (!message) throw("must provide message");
+ var status = document.getElementById("status").childNodes[0];
+ status.nodeValue = message;
+ status.parentNode.className = className;
+ }
+ function addOutput(name, className, code){
+ var newOutput = document.createElement("dl");
+ var newTitle = document.createElement("dt");
+ newTitle.className = className;
+ newTitle.appendChild(document.createTextNode(name));
+ newOutput.appendChild(newTitle);
+ var newMessage = document.createElement("dd");
+ newMessage.innerHTML = code;
+ newOutput.appendChild(newTitle);
+ newOutput.appendChild(newMessage);
+ output.appendChild(newOutput);
+ }
+ function displayTest(type, name, customMessage) {
+ var message = "???";
+ if (type != "done" && type != "skipped") ++count;
+ progress.style.width = (count * (progress.parentNode.clientWidth - 2) / totalTests) + "px";
+ progressRan.nodeValue = count;
+ if (type == "ok") {
+ message = "Test '" + name + "' succeeded";
+ if (!verbose) customMessage = false;
+ } else if (type == "skipped") {
+ message = "Test '" + name + "' skipped";
+ ++skipped;
+ if (!verbose) customMessage = false;
+ } else if (type == "expected") {
+ message = "Test '" + name + "' failed as expected";
+ if (!verbose) customMessage = false;
+ } else if (type == "error" || type == "fail") {
+ ++failed;
+ message = "Test '" + name + "' failed";
+ } else if (type == "done") {
+ if (failed) {
+ type += " fail";
+ message = failed + " failure" + (failed > 1 ? "s" : "");
+ } else if (count < totalTests) {
+ failed = totalTests - count;
+ type += " fail";
+ message = failed + " failure" + (failed > 1 ? "s" : "");
+ } else {
+ type += " ok";
+ message = "All passed";
+ if (skipped) {
+ message += " (" + skipped + " skipped)";
+ }
+ }
+ progressTotal.nodeValue = '';
+ customMessage = true; // Hack to avoid adding to output
+ }
+ if (window.mozilla_setStatus)
+ mozilla_setStatus(message, type, customMessage);
+ if (verbose && !customMessage) customMessage = message;
+ setStatus(message, type);
+ if (customMessage && customMessage.length > 0) {
+ addOutput(name, type, customMessage);
+ }
+ }
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/sourceeditor/test/codemirror/comment_test.js b/devtools/client/sourceeditor/test/codemirror/comment_test.js
new file mode 100644
index 000000000..26e474493
--- /dev/null
+++ b/devtools/client/sourceeditor/test/codemirror/comment_test.js
@@ -0,0 +1,100 @@
+namespace = "comment_";
+
+(function() {
+ function test(name, mode, run, before, after) {
+ return testCM(name, function(cm) {
+ run(cm);
+ eq(cm.getValue(), after);
+ }, {value: before, mode: mode});
+ }
+
+ var simpleProg = "function foo() {\n return bar;\n}";
+ var inlineBlock = "foo(/* bar */ true);";
+ var inlineBlocks = "foo(/* bar */ true, /* baz */ false);";
+ var multiLineInlineBlock = ["above();", "foo(/* bar */ true);", "below();"];
+
+ test("block", "javascript", function(cm) {
+ cm.blockComment(Pos(0, 3), Pos(3, 0), {blockCommentLead: " *"});
+ }, simpleProg + "\n", "/* function foo() {\n * return bar;\n * }\n */");
+
+ test("blockToggle", "javascript", function(cm) {
+ cm.blockComment(Pos(0, 3), Pos(2, 0), {blockCommentLead: " *"});
+ cm.uncomment(Pos(0, 3), Pos(2, 0), {blockCommentLead: " *"});
+ }, simpleProg, simpleProg);
+
+ test("blockToggle2", "javascript", function(cm) {
+ cm.setCursor({line: 0, ch: 7 /* inside the block comment */});
+ cm.execCommand("toggleComment");
+ }, inlineBlock, "foo(bar true);");
+
+ // This test should work but currently fails.
+ // test("blockToggle3", "javascript", function(cm) {
+ // cm.setCursor({line: 0, ch: 7 /* inside the first block comment */});
+ // cm.execCommand("toggleComment");
+ // }, inlineBlocks, "foo(bar true, /* baz */ false);");
+
+ test("line", "javascript", function(cm) {
+ cm.lineComment(Pos(1, 1), Pos(1, 1));
+ }, simpleProg, "function foo() {\n// return bar;\n}");
+
+ test("lineToggle", "javascript", function(cm) {
+ cm.lineComment(Pos(0, 0), Pos(2, 1));
+ cm.uncomment(Pos(0, 0), Pos(2, 1));
+ }, simpleProg, simpleProg);
+
+ test("fallbackToBlock", "css", function(cm) {
+ cm.lineComment(Pos(0, 0), Pos(2, 1));
+ }, "html {\n border: none;\n}", "/* html {\n border: none;\n} */");
+
+ test("fallbackToLine", "ruby", function(cm) {
+ cm.blockComment(Pos(0, 0), Pos(1));
+ }, "def blah()\n return hah\n", "# def blah()\n# return hah\n");
+
+ test("ignoreExternalBlockComments", "javascript", function(cm) {
+ cm.execCommand("toggleComment");
+ }, inlineBlocks, "// " + inlineBlocks);
+
+ test("ignoreExternalBlockComments2", "javascript", function(cm) {
+ cm.setCursor({line: 0, ch: null /* eol */});
+ cm.execCommand("toggleComment");
+ }, inlineBlocks, "// " + inlineBlocks);
+
+ test("ignoreExternalBlockCommentsMultiLineAbove", "javascript", function(cm) {
+ cm.setSelection({line: 0, ch: 0}, {line: 1, ch: 1});
+ cm.execCommand("toggleComment");
+ }, multiLineInlineBlock.join("\n"), ["// " + multiLineInlineBlock[0],
+ "// " + multiLineInlineBlock[1],
+ multiLineInlineBlock[2]].join("\n"));
+
+ test("ignoreExternalBlockCommentsMultiLineBelow", "javascript", function(cm) {
+ cm.setSelection({line: 1, ch: 13 /* after end of block comment */}, {line: 2, ch: 1});
+ cm.execCommand("toggleComment");
+ }, multiLineInlineBlock.join("\n"), [multiLineInlineBlock[0],
+ "// " + multiLineInlineBlock[1],
+ "// " + multiLineInlineBlock[2]].join("\n"));
+
+ test("commentRange", "javascript", function(cm) {
+ cm.blockComment(Pos(1, 2), Pos(1, 13), {fullLines: false});
+ }, simpleProg, "function foo() {\n /*return bar;*/\n}");
+
+ test("indented", "javascript", function(cm) {
+ cm.lineComment(Pos(1, 0), Pos(2), {indent: true});
+ }, simpleProg, "function foo() {\n// return bar;\n// }");
+
+ test("singleEmptyLine", "javascript", function(cm) {
+ cm.setCursor(1);
+ cm.execCommand("toggleComment");
+ }, "a;\n\nb;", "a;\n// \nb;");
+
+ test("dontMessWithStrings", "javascript", function(cm) {
+ cm.execCommand("toggleComment");
+ }, "console.log(\"/*string*/\");", "// console.log(\"/*string*/\");");
+
+ test("dontMessWithStrings2", "javascript", function(cm) {
+ cm.execCommand("toggleComment");
+ }, "console.log(\"// string\");", "// console.log(\"// string\");");
+
+ test("dontMessWithStrings3", "javascript", function(cm) {
+ cm.execCommand("toggleComment");
+ }, "// console.log(\"// string\");", "console.log(\"// string\");");
+})();
diff --git a/devtools/client/sourceeditor/test/codemirror/doc_test.js b/devtools/client/sourceeditor/test/codemirror/doc_test.js
new file mode 100644
index 000000000..5f242f658
--- /dev/null
+++ b/devtools/client/sourceeditor/test/codemirror/doc_test.js
@@ -0,0 +1,371 @@
+(function() {
+ // A minilanguage for instantiating linked CodeMirror instances and Docs
+ function instantiateSpec(spec, place, opts) {
+ var names = {}, pos = 0, l = spec.length, editors = [];
+ while (spec) {
+ var m = spec.match(/^(\w+)(\*?)(?:='([^\']*)'|<(~?)(\w+)(?:\/(\d+)-(\d+))?)\s*/);
+ var name = m[1], isDoc = m[2], cur;
+ if (m[3]) {
+ cur = isDoc ? CodeMirror.Doc(m[3]) : CodeMirror(place, clone(opts, {value: m[3]}));
+ } else {
+ var other = m[5];
+ if (!names.hasOwnProperty(other)) {
+ names[other] = editors.length;
+ editors.push(CodeMirror(place, opts));
+ }
+ var doc = editors[names[other]].linkedDoc({
+ sharedHist: !m[4],
+ from: m[6] ? Number(m[6]) : null,
+ to: m[7] ? Number(m[7]) : null
+ });
+ cur = isDoc ? doc : CodeMirror(place, clone(opts, {value: doc}));
+ }
+ names[name] = editors.length;
+ editors.push(cur);
+ spec = spec.slice(m[0].length);
+ }
+ return editors;
+ }
+
+ function clone(obj, props) {
+ if (!obj) return;
+ clone.prototype = obj;
+ var inst = new clone();
+ if (props) for (var n in props) if (props.hasOwnProperty(n))
+ inst[n] = props[n];
+ return inst;
+ }
+
+ function eqAll(val) {
+ var end = arguments.length, msg = null;
+ if (typeof arguments[end-1] == "string")
+ msg = arguments[--end];
+ if (i == end) throw new Error("No editors provided to eqAll");
+ for (var i = 1; i < end; ++i)
+ eq(arguments[i].getValue(), val, msg)
+ }
+
+ function testDoc(name, spec, run, opts, expectFail) {
+ if (!opts) opts = {};
+
+ return test("doc_" + name, function() {
+ var place = document.getElementById("testground");
+ var editors = instantiateSpec(spec, place, opts);
+ var successful = false;
+
+ try {
+ run.apply(null, editors);
+ successful = true;
+ } finally {
+ if (!successful || verbose) {
+ place.style.visibility = "visible";
+ } else {
+ for (var i = 0; i < editors.length; ++i)
+ if (editors[i] instanceof CodeMirror)
+ place.removeChild(editors[i].getWrapperElement());
+ }
+ }
+ }, expectFail);
+ }
+
+ var ie_lt8 = /MSIE [1-7]\b/.test(navigator.userAgent);
+
+ function testBasic(a, b) {
+ eqAll("x", a, b);
+ a.setValue("hey");
+ eqAll("hey", a, b);
+ b.setValue("wow");
+ eqAll("wow", a, b);
+ a.replaceRange("u\nv\nw", Pos(0, 3));
+ b.replaceRange("i", Pos(0, 4));
+ b.replaceRange("j", Pos(2, 1));
+ eqAll("wowui\nv\nwj", a, b);
+ }
+
+ testDoc("basic", "A='x' B<A", testBasic);
+ testDoc("basicSeparate", "A='x' B<~A", testBasic);
+
+ testDoc("sharedHist", "A='ab\ncd\nef' B<A", function(a, b) {
+ a.replaceRange("x", Pos(0));
+ b.replaceRange("y", Pos(1));
+ a.replaceRange("z", Pos(2));
+ eqAll("abx\ncdy\nefz", a, b);
+ a.undo();
+ a.undo();
+ eqAll("abx\ncd\nef", a, b);
+ a.redo();
+ eqAll("abx\ncdy\nef", a, b);
+ b.redo();
+ eqAll("abx\ncdy\nefz", a, b);
+ a.undo(); b.undo(); a.undo(); a.undo();
+ eqAll("ab\ncd\nef", a, b);
+ }, null, ie_lt8);
+
+ testDoc("undoIntact", "A='ab\ncd\nef' B<~A", function(a, b) {
+ a.replaceRange("x", Pos(0));
+ b.replaceRange("y", Pos(1));
+ a.replaceRange("z", Pos(2));
+ a.replaceRange("q", Pos(0));
+ eqAll("abxq\ncdy\nefz", a, b);
+ a.undo();
+ a.undo();
+ eqAll("abx\ncdy\nef", a, b);
+ b.undo();
+ eqAll("abx\ncd\nef", a, b);
+ a.redo();
+ eqAll("abx\ncd\nefz", a, b);
+ a.redo();
+ eqAll("abxq\ncd\nefz", a, b);
+ a.undo(); a.undo(); a.undo(); a.undo();
+ eqAll("ab\ncd\nef", a, b);
+ b.redo();
+ eqAll("ab\ncdy\nef", a, b);
+ });
+
+ testDoc("undoConflict", "A='ab\ncd\nef' B<~A", function(a, b) {
+ a.replaceRange("x", Pos(0));
+ a.replaceRange("z", Pos(2));
+ // This should clear the first undo event in a, but not the second
+ b.replaceRange("y", Pos(0));
+ a.undo(); a.undo();
+ eqAll("abxy\ncd\nef", a, b);
+ a.replaceRange("u", Pos(2));
+ a.replaceRange("v", Pos(0));
+ // This should clear both events in a
+ b.replaceRange("w", Pos(0));
+ a.undo(); a.undo();
+ eqAll("abxyvw\ncd\nefu", a, b);
+ });
+
+ testDoc("doubleRebase", "A='ab\ncd\nef\ng' B<~A C<B", function(a, b, c) {
+ c.replaceRange("u", Pos(3));
+ a.replaceRange("", Pos(0, 0), Pos(1, 0));
+ c.undo();
+ eqAll("cd\nef\ng", a, b, c);
+ });
+
+ testDoc("undoUpdate", "A='ab\ncd\nef' B<~A", function(a, b) {
+ a.replaceRange("x", Pos(2));
+ b.replaceRange("u\nv\nw\n", Pos(0, 0));
+ a.undo();
+ eqAll("u\nv\nw\nab\ncd\nef", a, b);
+ a.redo();
+ eqAll("u\nv\nw\nab\ncd\nefx", a, b);
+ a.undo();
+ eqAll("u\nv\nw\nab\ncd\nef", a, b);
+ b.undo();
+ a.redo();
+ eqAll("ab\ncd\nefx", a, b);
+ a.undo();
+ eqAll("ab\ncd\nef", a, b);
+ });
+
+ testDoc("undoKeepRanges", "A='abcdefg' B<A", function(a, b) {
+ var m = a.markText(Pos(0, 1), Pos(0, 3), {className: "foo"});
+ b.replaceRange("x", Pos(0, 0));
+ eqPos(m.find().from, Pos(0, 2));
+ b.replaceRange("yzzy", Pos(0, 1), Pos(0));
+ eq(m.find(), null);
+ b.undo();
+ eqPos(m.find().from, Pos(0, 2));
+ b.undo();
+ eqPos(m.find().from, Pos(0, 1));
+ });
+
+ testDoc("longChain", "A='uv' B<A C<B D<C", function(a, b, c, d) {
+ a.replaceSelection("X");
+ eqAll("Xuv", a, b, c, d);
+ d.replaceRange("Y", Pos(0));
+ eqAll("XuvY", a, b, c, d);
+ });
+
+ testDoc("broadCast", "B<A C<A D<A E<A", function(a, b, c, d, e) {
+ b.setValue("uu");
+ eqAll("uu", a, b, c, d, e);
+ a.replaceRange("v", Pos(0, 1));
+ eqAll("uvu", a, b, c, d, e);
+ });
+
+ // A and B share a history, C and D share a separate one
+ testDoc("islands", "A='x\ny\nz' B<A C<~A D<C", function(a, b, c, d) {
+ a.replaceRange("u", Pos(0));
+ d.replaceRange("v", Pos(2));
+ b.undo();
+ eqAll("x\ny\nzv", a, b, c, d);
+ c.undo();
+ eqAll("x\ny\nz", a, b, c, d);
+ a.redo();
+ eqAll("xu\ny\nz", a, b, c, d);
+ d.redo();
+ eqAll("xu\ny\nzv", a, b, c, d);
+ });
+
+ testDoc("unlink", "B<A C<A D<B", function(a, b, c, d) {
+ a.setValue("hi");
+ b.unlinkDoc(a);
+ d.setValue("aye");
+ eqAll("hi", a, c);
+ eqAll("aye", b, d);
+ a.setValue("oo");
+ eqAll("oo", a, c);
+ eqAll("aye", b, d);
+ });
+
+ testDoc("bareDoc", "A*='foo' B*<A C<B", function(a, b, c) {
+ is(a instanceof CodeMirror.Doc);
+ is(b instanceof CodeMirror.Doc);
+ is(c instanceof CodeMirror);
+ eqAll("foo", a, b, c);
+ a.replaceRange("hey", Pos(0, 0), Pos(0));
+ c.replaceRange("!", Pos(0));
+ eqAll("hey!", a, b, c);
+ b.unlinkDoc(a);
+ b.setValue("x");
+ eqAll("x", b, c);
+ eqAll("hey!", a);
+ });
+
+ testDoc("swapDoc", "A='a' B*='b' C<A", function(a, b, c) {
+ var d = a.swapDoc(b);
+ d.setValue("x");
+ eqAll("x", c, d);
+ eqAll("b", a, b);
+ });
+
+ testDoc("docKeepsScroll", "A='x' B*='y'", function(a, b) {
+ addDoc(a, 200, 200);
+ a.scrollIntoView(Pos(199, 200));
+ var c = a.swapDoc(b);
+ a.swapDoc(c);
+ var pos = a.getScrollInfo();
+ is(pos.left > 0, "not at left");
+ is(pos.top > 0, "not at top");
+ });
+
+ testDoc("copyDoc", "A='u'", function(a) {
+ var copy = a.getDoc().copy(true);
+ a.setValue("foo");
+ copy.setValue("bar");
+ var old = a.swapDoc(copy);
+ eq(a.getValue(), "bar");
+ a.undo();
+ eq(a.getValue(), "u");
+ a.swapDoc(old);
+ eq(a.getValue(), "foo");
+ eq(old.historySize().undo, 1);
+ eq(old.copy(false).historySize().undo, 0);
+ });
+
+ testDoc("docKeepsMode", "A='1+1'", function(a) {
+ var other = CodeMirror.Doc("hi", "text/x-markdown");
+ a.setOption("mode", "text/javascript");
+ var old = a.swapDoc(other);
+ eq(a.getOption("mode"), "text/x-markdown");
+ eq(a.getMode().name, "markdown");
+ a.swapDoc(old);
+ eq(a.getOption("mode"), "text/javascript");
+ eq(a.getMode().name, "javascript");
+ });
+
+ testDoc("subview", "A='1\n2\n3\n4\n5' B<~A/1-3", function(a, b) {
+ eq(b.getValue(), "2\n3");
+ eq(b.firstLine(), 1);
+ b.setCursor(Pos(4));
+ eqPos(b.getCursor(), Pos(2, 1));
+ a.replaceRange("-1\n0\n", Pos(0, 0));
+ eq(b.firstLine(), 3);
+ eqPos(b.getCursor(), Pos(4, 1));
+ a.undo();
+ eqPos(b.getCursor(), Pos(2, 1));
+ b.replaceRange("oyoy\n", Pos(2, 0));
+ eq(a.getValue(), "1\n2\noyoy\n3\n4\n5");
+ b.undo();
+ eq(a.getValue(), "1\n2\n3\n4\n5");
+ });
+
+ testDoc("subviewEditOnBoundary", "A='11\n22\n33\n44\n55' B<~A/1-4", function(a, b) {
+ a.replaceRange("x\nyy\nz", Pos(0, 1), Pos(2, 1));
+ eq(b.firstLine(), 2);
+ eq(b.lineCount(), 2);
+ eq(b.getValue(), "z3\n44");
+ a.replaceRange("q\nrr\ns", Pos(3, 1), Pos(4, 1));
+ eq(b.firstLine(), 2);
+ eq(b.getValue(), "z3\n4q");
+ eq(a.getValue(), "1x\nyy\nz3\n4q\nrr\ns5");
+ a.execCommand("selectAll");
+ a.replaceSelection("!");
+ eqAll("!", a, b);
+ });
+
+
+ testDoc("sharedMarker", "A='ab\ncd\nef\ngh' B<A C<~A/1-2", function(a, b, c) {
+ var mark = b.markText(Pos(0, 1), Pos(3, 1),
+ {className: "cm-searching", shared: true});
+ var found = a.findMarksAt(Pos(0, 2));
+ eq(found.length, 1);
+ eq(found[0], mark);
+ eq(c.findMarksAt(Pos(1, 1)).length, 1);
+ eqPos(mark.find().from, Pos(0, 1));
+ eqPos(mark.find().to, Pos(3, 1));
+ b.replaceRange("x\ny\n", Pos(0, 0));
+ eqPos(mark.find().from, Pos(2, 1));
+ eqPos(mark.find().to, Pos(5, 1));
+ var cleared = 0;
+ CodeMirror.on(mark, "clear", function() {++cleared;});
+ b.operation(function(){mark.clear();});
+ eq(a.findMarksAt(Pos(3, 1)).length, 0);
+ eq(b.findMarksAt(Pos(3, 1)).length, 0);
+ eq(c.findMarksAt(Pos(3, 1)).length, 0);
+ eq(mark.find(), null);
+ eq(cleared, 1);
+ });
+
+ testDoc("sharedMarkerCopy", "A='abcde'", function(a) {
+ var shared = a.markText(Pos(0, 1), Pos(0, 3), {shared: true});
+ var b = a.linkedDoc();
+ var found = b.findMarksAt(Pos(0, 2));
+ eq(found.length, 1);
+ eq(found[0], shared);
+ shared.clear();
+ eq(b.findMarksAt(Pos(0, 2)), 0);
+ });
+
+ testDoc("sharedMarkerDetach", "A='abcde' B<A C<B", function(a, b, c) {
+ var shared = a.markText(Pos(0, 1), Pos(0, 3), {shared: true});
+ a.unlinkDoc(b);
+ var inB = b.findMarksAt(Pos(0, 2));
+ eq(inB.length, 1);
+ is(inB[0] != shared);
+ var inC = c.findMarksAt(Pos(0, 2));
+ eq(inC.length, 1);
+ is(inC[0] != shared);
+ inC[0].clear();
+ is(shared.find());
+ });
+
+ testDoc("sharedBookmark", "A='ab\ncd\nef\ngh' B<A C<~A/1-2", function(a, b, c) {
+ var mark = b.setBookmark(Pos(1, 1), {shared: true});
+ var found = a.findMarksAt(Pos(1, 1));
+ eq(found.length, 1);
+ eq(found[0], mark);
+ eq(c.findMarksAt(Pos(1, 1)).length, 1);
+ eqPos(mark.find(), Pos(1, 1));
+ b.replaceRange("x\ny\n", Pos(0, 0));
+ eqPos(mark.find(), Pos(3, 1));
+ var cleared = 0;
+ CodeMirror.on(mark, "clear", function() {++cleared;});
+ b.operation(function() {mark.clear();});
+ eq(a.findMarks(Pos(0, 0), Pos(5)).length, 0);
+ eq(b.findMarks(Pos(0, 0), Pos(5)).length, 0);
+ eq(c.findMarks(Pos(0, 0), Pos(5)).length, 0);
+ eq(mark.find(), null);
+ eq(cleared, 1);
+ });
+
+ testDoc("undoInSubview", "A='line 0\nline 1\nline 2\nline 3\nline 4' B<A/1-4", function(a, b) {
+ b.replaceRange("x", Pos(2, 0));
+ a.undo();
+ eq(a.getValue(), "line 0\nline 1\nline 2\nline 3\nline 4");
+ eq(b.getValue(), "line 1\nline 2\nline 3");
+ });
+})();
diff --git a/devtools/client/sourceeditor/test/codemirror/driver.js b/devtools/client/sourceeditor/test/codemirror/driver.js
new file mode 100644
index 000000000..c61d4c1f3
--- /dev/null
+++ b/devtools/client/sourceeditor/test/codemirror/driver.js
@@ -0,0 +1,138 @@
+var tests = [], filters = [], allNames = [];
+
+function Failure(why) {this.message = why;}
+Failure.prototype.toString = function() { return this.message; };
+
+function indexOf(collection, elt) {
+ if (collection.indexOf) return collection.indexOf(elt);
+ for (var i = 0, e = collection.length; i < e; ++i)
+ if (collection[i] == elt) return i;
+ return -1;
+}
+
+function test(name, run, expectedFail) {
+ // Force unique names
+ var originalName = name;
+ var i = 2; // Second function would be NAME_2
+ while (indexOf(allNames, name) !== -1){
+ name = originalName + "_" + i;
+ i++;
+ }
+ allNames.push(name);
+ // Add test
+ tests.push({name: name, func: run, expectedFail: expectedFail});
+ return name;
+}
+var namespace = "";
+function testCM(name, run, opts, expectedFail) {
+ return test(namespace + name, function() {
+ var place = document.getElementById("testground"), cm = window.cm = CodeMirror(place, opts);
+ var successful = false;
+ try {
+ run(cm);
+ successful = true;
+ } finally {
+ if (!successful || verbose) {
+ place.style.visibility = "visible";
+ } else {
+ place.removeChild(cm.getWrapperElement());
+ }
+ }
+ }, expectedFail);
+}
+
+function runTests(callback) {
+ var totalTime = 0;
+ function step(i) {
+ for (;;) {
+ if (i === tests.length) {
+ running = false;
+ return callback("done");
+ }
+ var test = tests[i], skip = false;
+ if (filters.length) {
+ skip = true;
+ for (var j = 0; j < filters.length; j++)
+ if (test.name.match(filters[j])) skip = false;
+ }
+ if (skip) {
+ callback("skipped", test.name, message);
+ i++;
+ } else {
+ break;
+ }
+ }
+ var expFail = test.expectedFail, startTime = +new Date, threw = false;
+ try {
+ var message = test.func();
+ } catch(e) {
+ threw = true;
+ if (expFail) callback("expected", test.name);
+ else if (e instanceof Failure) callback("fail", test.name, e.message);
+ else {
+ var pos = /(?:\bat |@).*?([^\/:]+):(\d+)/.exec(e.stack);
+ if (pos) console["log"](e.stack);
+ callback("error", test.name, e.toString() + (pos ? " (" + pos[1] + ":" + pos[2] + ")" : ""));
+ }
+ }
+ if (!threw) {
+ if (expFail) callback("fail", test.name, message || "expected failure, but passed");
+ else callback("ok", test.name, message);
+ }
+ if (!quit) { // Run next test
+ var delay = 0;
+ totalTime += (+new Date) - startTime;
+ if (totalTime > 500){
+ totalTime = 0;
+ delay = 50;
+ }
+ setTimeout(function(){step(i + 1);}, delay);
+ } else { // Quit tests
+ running = false;
+ return null;
+ }
+ }
+ step(0);
+}
+
+function label(str, msg) {
+ if (msg) return str + " (" + msg + ")";
+ return str;
+}
+function eq(a, b, msg) {
+ if (a != b) throw new Failure(label(a + " != " + b, msg));
+}
+function near(a, b, margin, msg) {
+ if (Math.abs(a - b) > margin)
+ throw new Failure(label(a + " is not close to " + b + " (" + margin + ")", msg));
+}
+function eqPos(a, b, msg) {
+ function str(p) { return "{line:" + p.line + ",ch:" + p.ch + "}"; }
+ if (a == b) return;
+ if (a == null) throw new Failure(label("comparing null to " + str(b), msg));
+ if (b == null) throw new Failure(label("comparing " + str(a) + " to null", msg));
+ if (a.line != b.line || a.ch != b.ch) throw new Failure(label(str(a) + " != " + str(b), msg));
+}
+function is(a, msg) {
+ if (!a) throw new Failure(label("assertion failed", msg));
+}
+
+function countTests() {
+ if (!filters.length) return tests.length;
+ var sum = 0;
+ for (var i = 0; i < tests.length; ++i) {
+ var name = tests[i].name;
+ for (var j = 0; j < filters.length; j++) {
+ if (name.match(filters[j])) {
+ ++sum;
+ break;
+ }
+ }
+ }
+ return sum;
+}
+
+function parseTestFilter(s) {
+ if (/_\*$/.test(s)) return new RegExp("^" + s.slice(0, s.length - 2), "i");
+ else return new RegExp(s, "i");
+}
diff --git a/devtools/client/sourceeditor/test/codemirror/emacs_test.js b/devtools/client/sourceeditor/test/codemirror/emacs_test.js
new file mode 100644
index 000000000..124575c72
--- /dev/null
+++ b/devtools/client/sourceeditor/test/codemirror/emacs_test.js
@@ -0,0 +1,147 @@
+(function() {
+ "use strict";
+
+ var Pos = CodeMirror.Pos;
+ namespace = "emacs_";
+
+ var eventCache = {};
+ function fakeEvent(keyName) {
+ var event = eventCache[key];
+ if (event) return event;
+
+ var ctrl, shift, alt;
+ var key = keyName.replace(/\w+-/g, function(type) {
+ if (type == "Ctrl-") ctrl = true;
+ else if (type == "Alt-") alt = true;
+ else if (type == "Shift-") shift = true;
+ return "";
+ });
+ var code;
+ for (var c in CodeMirror.keyNames)
+ if (CodeMirror.keyNames[c] == key) { code = c; break; }
+ if (code == null) throw new Error("Unknown key: " + key);
+
+ return eventCache[keyName] = {
+ type: "keydown", keyCode: code, ctrlKey: ctrl, shiftKey: shift, altKey: alt,
+ preventDefault: function(){}, stopPropagation: function(){}
+ };
+ }
+
+ function sim(name, start /*, actions... */) {
+ var keys = Array.prototype.slice.call(arguments, 2);
+ testCM(name, function(cm) {
+ for (var i = 0; i < keys.length; ++i) {
+ var cur = keys[i];
+ if (cur instanceof Pos) cm.setCursor(cur);
+ else if (cur.call) cur(cm);
+ else cm.triggerOnKeyDown(fakeEvent(cur));
+ }
+ }, {keyMap: "emacs", value: start, mode: "javascript"});
+ }
+
+ function at(line, ch) { return function(cm) { eqPos(cm.getCursor(), Pos(line, ch)); }; }
+ function txt(str) { return function(cm) { eq(cm.getValue(), str); }; }
+
+ sim("motionHSimple", "abc", "Ctrl-F", "Ctrl-F", "Ctrl-B", at(0, 1));
+ sim("motionHMulti", "abcde",
+ "Ctrl-4", "Ctrl-F", at(0, 4), "Ctrl--", "Ctrl-2", "Ctrl-F", at(0, 2),
+ "Ctrl-5", "Ctrl-B", at(0, 0));
+
+ sim("motionHWord", "abc. def ghi",
+ "Alt-F", at(0, 3), "Alt-F", at(0, 8),
+ "Ctrl-B", "Alt-B", at(0, 5), "Alt-B", at(0, 0));
+ sim("motionHWordMulti", "abc. def ghi ",
+ "Ctrl-3", "Alt-F", at(0, 12), "Ctrl-2", "Alt-B", at(0, 5),
+ "Ctrl--", "Alt-B", at(0, 8));
+
+ sim("motionVSimple", "a\nb\nc\n", "Ctrl-N", "Ctrl-N", "Ctrl-P", at(1, 0));
+ sim("motionVMulti", "a\nb\nc\nd\ne\n",
+ "Ctrl-2", "Ctrl-N", at(2, 0), "Ctrl-F", "Ctrl--", "Ctrl-N", at(1, 1),
+ "Ctrl--", "Ctrl-3", "Ctrl-P", at(4, 1));
+
+ sim("killYank", "abc\ndef\nghi",
+ "Ctrl-F", "Ctrl-Space", "Ctrl-N", "Ctrl-N", "Ctrl-W", "Ctrl-E", "Ctrl-Y",
+ txt("ahibc\ndef\ng"));
+ sim("killRing", "abcdef",
+ "Ctrl-Space", "Ctrl-F", "Ctrl-W", "Ctrl-Space", "Ctrl-F", "Ctrl-W",
+ "Ctrl-Y", "Alt-Y",
+ txt("acdef"));
+ sim("copyYank", "abcd",
+ "Ctrl-Space", "Ctrl-E", "Alt-W", "Ctrl-Y",
+ txt("abcdabcd"));
+
+ sim("killLineSimple", "foo\nbar", "Ctrl-F", "Ctrl-K", txt("f\nbar"));
+ sim("killLineEmptyLine", "foo\n \nbar", "Ctrl-N", "Ctrl-K", txt("foo\nbar"));
+ sim("killLineMulti", "foo\nbar\nbaz",
+ "Ctrl-F", "Ctrl-F", "Ctrl-K", "Ctrl-K", "Ctrl-K", "Ctrl-A", "Ctrl-Y",
+ txt("o\nbarfo\nbaz"));
+
+ sim("moveByParagraph", "abc\ndef\n\n\nhij\nklm\n\n",
+ "Ctrl-F", "Ctrl-Down", at(2, 0), "Ctrl-Down", at(6, 0),
+ "Ctrl-N", "Ctrl-Up", at(3, 0), "Ctrl-Up", at(0, 0),
+ Pos(1, 2), "Ctrl-Down", at(2, 0), Pos(4, 2), "Ctrl-Up", at(3, 0));
+ sim("moveByParagraphMulti", "abc\n\ndef\n\nhij\n\nklm",
+ "Ctrl-U", "2", "Ctrl-Down", at(3, 0),
+ "Shift-Alt-.", "Ctrl-3", "Ctrl-Up", at(1, 0));
+
+ sim("moveBySentence", "sentence one! sentence\ntwo\n\nparagraph two",
+ "Alt-E", at(0, 13), "Alt-E", at(1, 3), "Ctrl-F", "Alt-A", at(0, 13));
+
+ sim("moveByExpr", "function foo(a, b) {}",
+ "Ctrl-Alt-F", at(0, 8), "Ctrl-Alt-F", at(0, 12), "Ctrl-Alt-F", at(0, 18),
+ "Ctrl-Alt-B", at(0, 12), "Ctrl-Alt-B", at(0, 9));
+ sim("moveByExprMulti", "foo bar baz bug",
+ "Ctrl-2", "Ctrl-Alt-F", at(0, 7),
+ "Ctrl--", "Ctrl-Alt-F", at(0, 4),
+ "Ctrl--", "Ctrl-2", "Ctrl-Alt-B", at(0, 11));
+ sim("delExpr", "var x = [\n a,\n b\n c\n];",
+ Pos(0, 8), "Ctrl-Alt-K", txt("var x = ;"), "Ctrl-/",
+ Pos(4, 1), "Ctrl-Alt-Backspace", txt("var x = ;"));
+ sim("delExprMulti", "foo bar baz",
+ "Ctrl-2", "Ctrl-Alt-K", txt(" baz"),
+ "Ctrl-/", "Ctrl-E", "Ctrl-2", "Ctrl-Alt-Backspace", txt("foo "));
+
+ sim("justOneSpace", "hi bye ",
+ Pos(0, 4), "Alt-Space", txt("hi bye "),
+ Pos(0, 4), "Alt-Space", txt("hi b ye "),
+ "Ctrl-A", "Alt-Space", "Ctrl-E", "Alt-Space", txt(" hi b ye "));
+
+ sim("openLine", "foo bar", "Alt-F", "Ctrl-O", txt("foo\n bar"))
+
+ sim("transposeChar", "abcd\ne",
+ "Ctrl-F", "Ctrl-T", "Ctrl-T", txt("bcad\ne"), at(0, 3),
+ "Ctrl-F", "Ctrl-T", "Ctrl-T", "Ctrl-T", txt("bcda\ne"), at(0, 4),
+ "Ctrl-F", "Ctrl-T", txt("bcde\na"), at(1, 0));
+
+ sim("manipWordCase", "foo BAR bAZ",
+ "Alt-C", "Alt-L", "Alt-U", txt("Foo bar BAZ"),
+ "Ctrl-A", "Alt-U", "Alt-L", "Alt-C", txt("FOO bar Baz"));
+ sim("manipWordCaseMulti", "foo Bar bAz",
+ "Ctrl-2", "Alt-U", txt("FOO BAR bAz"),
+ "Ctrl-A", "Ctrl-3", "Alt-C", txt("Foo Bar Baz"));
+
+ sim("upExpr", "foo {\n bar[];\n baz(blah);\n}",
+ Pos(2, 7), "Ctrl-Alt-U", at(2, 5), "Ctrl-Alt-U", at(0, 4));
+ sim("transposeExpr", "do foo[bar] dah",
+ Pos(0, 6), "Ctrl-Alt-T", txt("do [bar]foo dah"));
+
+ sim("clearMark", "abcde", Pos(0, 2), "Ctrl-Space", "Ctrl-F", "Ctrl-F",
+ "Ctrl-G", "Ctrl-W", txt("abcde"));
+
+ sim("delRegion", "abcde", "Ctrl-Space", "Ctrl-F", "Ctrl-F", "Delete", txt("cde"));
+ sim("backspaceRegion", "abcde", "Ctrl-Space", "Ctrl-F", "Ctrl-F", "Backspace", txt("cde"));
+
+ testCM("save", function(cm) {
+ var saved = false;
+ CodeMirror.commands.save = function(cm) { saved = cm.getValue(); };
+ cm.triggerOnKeyDown(fakeEvent("Ctrl-X"));
+ cm.triggerOnKeyDown(fakeEvent("Ctrl-S"));
+ is(saved, "hi");
+ }, {value: "hi", keyMap: "emacs"});
+
+ testCM("gotoInvalidLineFloat", function(cm) {
+ cm.openDialog = function(_, cb) { cb("2.2"); };
+ cm.triggerOnKeyDown(fakeEvent("Alt-G"));
+ cm.triggerOnKeyDown(fakeEvent("G"));
+ }, {value: "1\n2\n3\n4", keyMap: "emacs"});
+})();
diff --git a/devtools/client/sourceeditor/test/codemirror/mode/javascript/test.js b/devtools/client/sourceeditor/test/codemirror/mode/javascript/test.js
new file mode 100644
index 000000000..cb43d0894
--- /dev/null
+++ b/devtools/client/sourceeditor/test/codemirror/mode/javascript/test.js
@@ -0,0 +1,210 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function() {
+ var mode = CodeMirror.getMode({indentUnit: 2}, "javascript");
+ function MT(name) { test.mode(name, mode, Array.prototype.slice.call(arguments, 1)); }
+
+ MT("locals",
+ "[keyword function] [def foo]([def a], [def b]) { [keyword var] [def c] [operator =] [number 10]; [keyword return] [variable-2 a] [operator +] [variable-2 c] [operator +] [variable d]; }");
+
+ MT("comma-and-binop",
+ "[keyword function](){ [keyword var] [def x] [operator =] [number 1] [operator +] [number 2], [def y]; }");
+
+ MT("destructuring",
+ "([keyword function]([def a], [[[def b], [def c] ]]) {",
+ " [keyword let] {[def d], [property foo]: [def c][operator =][number 10], [def x]} [operator =] [variable foo]([variable-2 a]);",
+ " [[[variable-2 c], [variable y] ]] [operator =] [variable-2 c];",
+ "})();");
+
+ MT("destructure_trailing_comma",
+ "[keyword let] {[def a], [def b],} [operator =] [variable foo];",
+ "[keyword let] [def c];"); // Parser still in good state?
+
+ MT("class_body",
+ "[keyword class] [def Foo] {",
+ " [property constructor]() {}",
+ " [property sayName]() {",
+ " [keyword return] [string-2 `foo${][variable foo][string-2 }oo`];",
+ " }",
+ "}");
+
+ MT("class",
+ "[keyword class] [def Point] [keyword extends] [variable SuperThing] {",
+ " [property get] [property prop]() { [keyword return] [number 24]; }",
+ " [property constructor]([def x], [def y]) {",
+ " [keyword super]([string 'something']);",
+ " [keyword this].[property x] [operator =] [variable-2 x];",
+ " }",
+ "}");
+
+ MT("import",
+ "[keyword function] [def foo]() {",
+ " [keyword import] [def $] [keyword from] [string 'jquery'];",
+ " [keyword import] { [def encrypt], [def decrypt] } [keyword from] [string 'crypto'];",
+ "}");
+
+ MT("const",
+ "[keyword function] [def f]() {",
+ " [keyword const] [[ [def a], [def b] ]] [operator =] [[ [number 1], [number 2] ]];",
+ "}");
+
+ MT("for/of",
+ "[keyword for]([keyword let] [def of] [keyword of] [variable something]) {}");
+
+ MT("generator",
+ "[keyword function*] [def repeat]([def n]) {",
+ " [keyword for]([keyword var] [def i] [operator =] [number 0]; [variable-2 i] [operator <] [variable-2 n]; [operator ++][variable-2 i])",
+ " [keyword yield] [variable-2 i];",
+ "}");
+
+ MT("quotedStringAddition",
+ "[keyword let] [def f] [operator =] [variable a] [operator +] [string 'fatarrow'] [operator +] [variable c];");
+
+ MT("quotedFatArrow",
+ "[keyword let] [def f] [operator =] [variable a] [operator +] [string '=>'] [operator +] [variable c];");
+
+ MT("fatArrow",
+ "[variable array].[property filter]([def a] [operator =>] [variable-2 a] [operator +] [number 1]);",
+ "[variable a];", // No longer in scope
+ "[keyword let] [def f] [operator =] ([[ [def a], [def b] ]], [def c]) [operator =>] [variable-2 a] [operator +] [variable-2 c];",
+ "[variable c];");
+
+ MT("spread",
+ "[keyword function] [def f]([def a], [meta ...][def b]) {",
+ " [variable something]([variable-2 a], [meta ...][variable-2 b]);",
+ "}");
+
+ MT("comprehension",
+ "[keyword function] [def f]() {",
+ " [[([variable x] [operator +] [number 1]) [keyword for] ([keyword var] [def x] [keyword in] [variable y]) [keyword if] [variable pred]([variable-2 x]) ]];",
+ " ([variable u] [keyword for] ([keyword var] [def u] [keyword of] [variable generateValues]()) [keyword if] ([variable-2 u].[property color] [operator ===] [string 'blue']));",
+ "}");
+
+ MT("quasi",
+ "[variable re][string-2 `fofdlakj${][variable x] [operator +] ([variable re][string-2 `foo`]) [operator +] [number 1][string-2 }fdsa`] [operator +] [number 2]");
+
+ MT("quasi_no_function",
+ "[variable x] [operator =] [string-2 `fofdlakj${][variable x] [operator +] [string-2 `foo`] [operator +] [number 1][string-2 }fdsa`] [operator +] [number 2]");
+
+ MT("indent_statement",
+ "[keyword var] [def x] [operator =] [number 10]",
+ "[variable x] [operator +=] [variable y] [operator +]",
+ " [atom Infinity]",
+ "[keyword debugger];");
+
+ MT("indent_if",
+ "[keyword if] ([number 1])",
+ " [keyword break];",
+ "[keyword else] [keyword if] ([number 2])",
+ " [keyword continue];",
+ "[keyword else]",
+ " [number 10];",
+ "[keyword if] ([number 1]) {",
+ " [keyword break];",
+ "} [keyword else] [keyword if] ([number 2]) {",
+ " [keyword continue];",
+ "} [keyword else] {",
+ " [number 10];",
+ "}");
+
+ MT("indent_for",
+ "[keyword for] ([keyword var] [def i] [operator =] [number 0];",
+ " [variable i] [operator <] [number 100];",
+ " [variable i][operator ++])",
+ " [variable doSomething]([variable i]);",
+ "[keyword debugger];");
+
+ MT("indent_c_style",
+ "[keyword function] [def foo]()",
+ "{",
+ " [keyword debugger];",
+ "}");
+
+ MT("indent_else",
+ "[keyword for] (;;)",
+ " [keyword if] ([variable foo])",
+ " [keyword if] ([variable bar])",
+ " [number 1];",
+ " [keyword else]",
+ " [number 2];",
+ " [keyword else]",
+ " [number 3];");
+
+ MT("indent_funarg",
+ "[variable foo]([number 10000],",
+ " [keyword function]([def a]) {",
+ " [keyword debugger];",
+ "};");
+
+ MT("indent_below_if",
+ "[keyword for] (;;)",
+ " [keyword if] ([variable foo])",
+ " [number 1];",
+ "[number 2];");
+
+ MT("multilinestring",
+ "[keyword var] [def x] [operator =] [string 'foo\\]",
+ "[string bar'];");
+
+ MT("scary_regexp",
+ "[string-2 /foo[[/]]bar/];");
+
+ MT("indent_strange_array",
+ "[keyword var] [def x] [operator =] [[",
+ " [number 1],,",
+ " [number 2],",
+ "]];",
+ "[number 10];");
+
+ MT("param_default",
+ "[keyword function] [def foo]([def x] [operator =] [string-2 `foo${][number 10][string-2 }bar`]) {",
+ " [keyword return] [variable-2 x];",
+ "}");
+
+ MT("new_target",
+ "[keyword function] [def F]([def target]) {",
+ " [keyword if] ([variable-2 target] [operator &&] [keyword new].[keyword target].[property name]) {",
+ " [keyword return] [keyword new]",
+ " .[keyword target];",
+ " }",
+ "}");
+
+ var jsonld_mode = CodeMirror.getMode(
+ {indentUnit: 2},
+ {name: "javascript", jsonld: true}
+ );
+ function LD(name) {
+ test.mode(name, jsonld_mode, Array.prototype.slice.call(arguments, 1));
+ }
+
+ LD("json_ld_keywords",
+ '{',
+ ' [meta "@context"]: {',
+ ' [meta "@base"]: [string "http://example.com"],',
+ ' [meta "@vocab"]: [string "http://xmlns.com/foaf/0.1/"],',
+ ' [property "likesFlavor"]: {',
+ ' [meta "@container"]: [meta "@list"]',
+ ' [meta "@reverse"]: [string "@beFavoriteOf"]',
+ ' },',
+ ' [property "nick"]: { [meta "@container"]: [meta "@set"] },',
+ ' [property "nick"]: { [meta "@container"]: [meta "@index"] }',
+ ' },',
+ ' [meta "@graph"]: [[ {',
+ ' [meta "@id"]: [string "http://dbpedia.org/resource/John_Lennon"],',
+ ' [property "name"]: [string "John Lennon"],',
+ ' [property "modified"]: {',
+ ' [meta "@value"]: [string "2010-05-29T14:17:39+02:00"],',
+ ' [meta "@type"]: [string "http://www.w3.org/2001/XMLSchema#dateTime"]',
+ ' }',
+ ' } ]]',
+ '}');
+
+ LD("json_ld_fake",
+ '{',
+ ' [property "@fake"]: [string "@fake"],',
+ ' [property "@contextual"]: [string "@identifier"],',
+ ' [property "user@domain.com"]: [string "@graphical"],',
+ ' [property "@ID"]: [string "@@ID"]',
+ '}');
+})();
diff --git a/devtools/client/sourceeditor/test/codemirror/mode_test.css b/devtools/client/sourceeditor/test/codemirror/mode_test.css
new file mode 100644
index 000000000..f83271b4e
--- /dev/null
+++ b/devtools/client/sourceeditor/test/codemirror/mode_test.css
@@ -0,0 +1,23 @@
+.mt-output .mt-token {
+ border: 1px solid #ddd;
+ white-space: pre;
+ font-family: "Consolas", monospace;
+ text-align: center;
+}
+
+.mt-output .mt-style {
+ font-size: x-small;
+}
+
+.mt-output .mt-state {
+ font-size: x-small;
+ vertical-align: top;
+}
+
+.mt-output .mt-state-row {
+ display: none;
+}
+
+.mt-state-unhide .mt-output .mt-state-row {
+ display: table-row;
+}
diff --git a/devtools/client/sourceeditor/test/codemirror/mode_test.js b/devtools/client/sourceeditor/test/codemirror/mode_test.js
new file mode 100644
index 000000000..0aed50f7d
--- /dev/null
+++ b/devtools/client/sourceeditor/test/codemirror/mode_test.js
@@ -0,0 +1,192 @@
+/**
+ * Helper to test CodeMirror highlighting modes. It pretty prints output of the
+ * highlighter and can check against expected styles.
+ *
+ * Mode tests are registered by calling test.mode(testName, mode,
+ * tokens), where mode is a mode object as returned by
+ * CodeMirror.getMode, and tokens is an array of lines that make up
+ * the test.
+ *
+ * These lines are strings, in which styled stretches of code are
+ * enclosed in brackets `[]`, and prefixed by their style. For
+ * example, `[keyword if]`. Brackets in the code itself must be
+ * duplicated to prevent them from being interpreted as token
+ * boundaries. For example `a[[i]]` for `a[i]`. If a token has
+ * multiple styles, the styles must be separated by ampersands, for
+ * example `[tag&error </hmtl>]`.
+ *
+ * See the test.js files in the css, markdown, gfm, and stex mode
+ * directories for examples.
+ */
+(function() {
+ function findSingle(str, pos, ch) {
+ for (;;) {
+ var found = str.indexOf(ch, pos);
+ if (found == -1) return null;
+ if (str.charAt(found + 1) != ch) return found;
+ pos = found + 2;
+ }
+ }
+
+ var styleName = /[\w&-_]+/g;
+ function parseTokens(strs) {
+ var tokens = [], plain = "";
+ for (var i = 0; i < strs.length; ++i) {
+ if (i) plain += "\n";
+ var str = strs[i], pos = 0;
+ while (pos < str.length) {
+ var style = null, text;
+ if (str.charAt(pos) == "[" && str.charAt(pos+1) != "[") {
+ styleName.lastIndex = pos + 1;
+ var m = styleName.exec(str);
+ style = m[0].replace(/&/g, " ");
+ var textStart = pos + style.length + 2;
+ var end = findSingle(str, textStart, "]");
+ if (end == null) throw new Error("Unterminated token at " + pos + " in '" + str + "'" + style);
+ text = str.slice(textStart, end);
+ pos = end + 1;
+ } else {
+ var end = findSingle(str, pos, "[");
+ if (end == null) end = str.length;
+ text = str.slice(pos, end);
+ pos = end;
+ }
+ text = text.replace(/\[\[|\]\]/g, function(s) {return s.charAt(0);});
+ tokens.push({style: style, text: text});
+ plain += text;
+ }
+ }
+ return {tokens: tokens, plain: plain};
+ }
+
+ test.mode = function(name, mode, tokens, modeName) {
+ var data = parseTokens(tokens);
+ return test((modeName || mode.name) + "_" + name, function() {
+ return compare(data.plain, data.tokens, mode);
+ });
+ };
+
+ function esc(str) {
+ return str.replace('&', '&amp;').replace('<', '&lt;').replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
+;
+ }
+
+ function compare(text, expected, mode) {
+
+ var expectedOutput = [];
+ for (var i = 0; i < expected.length; ++i) {
+ var sty = expected[i].style;
+ if (sty && sty.indexOf(" ")) sty = sty.split(' ').sort().join(' ');
+ expectedOutput.push({style: sty, text: expected[i].text});
+ }
+
+ var observedOutput = highlight(text, mode);
+
+ var s = "";
+ var diff = highlightOutputsDifferent(expectedOutput, observedOutput);
+ if (diff != null) {
+ s += '<div class="mt-test mt-fail">';
+ s += '<pre>' + esc(text) + '</pre>';
+ s += '<div class="cm-s-default">';
+ s += 'expected:';
+ s += prettyPrintOutputTable(expectedOutput, diff);
+ s += 'observed: [<a onclick="this.parentElement.className+=\' mt-state-unhide\'">display states</a>]';
+ s += prettyPrintOutputTable(observedOutput, diff);
+ s += '</div>';
+ s += '</div>';
+ }
+ if (observedOutput.indentFailures) {
+ for (var i = 0; i < observedOutput.indentFailures.length; i++)
+ s += "<div class='mt-test mt-fail'>" + esc(observedOutput.indentFailures[i]) + "</div>";
+ }
+ if (s) throw new Failure(s);
+ }
+
+ function stringify(obj) {
+ function replacer(key, obj) {
+ if (typeof obj == "function") {
+ var m = obj.toString().match(/function\s*[^\s(]*/);
+ return m ? m[0] : "function";
+ }
+ return obj;
+ }
+ if (window.JSON && JSON.stringify)
+ return JSON.stringify(obj, replacer, 2);
+ return "[unsupported]"; // Fail safely if no native JSON.
+ }
+
+ function highlight(string, mode) {
+ var state = mode.startState();
+
+ var lines = string.replace(/\r\n/g,'\n').split('\n');
+ var st = [], pos = 0;
+ for (var i = 0; i < lines.length; ++i) {
+ var line = lines[i], newLine = true;
+ if (mode.indent) {
+ var ws = line.match(/^\s*/)[0];
+ var indent = mode.indent(state, line.slice(ws.length));
+ if (indent != CodeMirror.Pass && indent != ws.length)
+ (st.indentFailures || (st.indentFailures = [])).push(
+ "Indentation of line " + (i + 1) + " is " + indent + " (expected " + ws.length + ")");
+ }
+ var stream = new CodeMirror.StringStream(line);
+ if (line == "" && mode.blankLine) mode.blankLine(state);
+ /* Start copied code from CodeMirror.highlight */
+ while (!stream.eol()) {
+ for (var j = 0; j < 10 && stream.start >= stream.pos; j++)
+ var compare = mode.token(stream, state);
+ if (j == 10)
+ throw new Failure("Failed to advance the stream." + stream.string + " " + stream.pos);
+ var substr = stream.current();
+ if (compare && compare.indexOf(" ") > -1) compare = compare.split(' ').sort().join(' ');
+ stream.start = stream.pos;
+ if (pos && st[pos-1].style == compare && !newLine) {
+ st[pos-1].text += substr;
+ } else if (substr) {
+ st[pos++] = {style: compare, text: substr, state: stringify(state)};
+ }
+ // Give up when line is ridiculously long
+ if (stream.pos > 5000) {
+ st[pos++] = {style: null, text: this.text.slice(stream.pos)};
+ break;
+ }
+ newLine = false;
+ }
+ }
+
+ return st;
+ }
+
+ function highlightOutputsDifferent(o1, o2) {
+ var minLen = Math.min(o1.length, o2.length);
+ for (var i = 0; i < minLen; ++i)
+ if (o1[i].style != o2[i].style || o1[i].text != o2[i].text) return i;
+ if (o1.length > minLen || o2.length > minLen) return minLen;
+ }
+
+ function prettyPrintOutputTable(output, diffAt) {
+ var s = '<table class="mt-output">';
+ s += '<tr>';
+ for (var i = 0; i < output.length; ++i) {
+ var style = output[i].style, val = output[i].text;
+ s +=
+ '<td class="mt-token"' + (i == diffAt ? " style='background: pink'" : "") + '>' +
+ '<span class="cm-' + esc(String(style)) + '">' +
+ esc(val.replace(/ /g,'\xb7')) + // · MIDDLE DOT
+ '</span>' +
+ '</td>';
+ }
+ s += '</tr><tr>';
+ for (var i = 0; i < output.length; ++i) {
+ s += '<td class="mt-style"><span>' + (output[i].style || null) + '</span></td>';
+ }
+ if(output[0].state) {
+ s += '</tr><tr class="mt-state-row" title="State AFTER each token">';
+ for (var i = 0; i < output.length; ++i) {
+ s += '<td class="mt-state"><pre>' + esc(output[i].state) + '</pre></td>';
+ }
+ }
+ s += '</tr></table>';
+ return s;
+ }
+})();
diff --git a/devtools/client/sourceeditor/test/codemirror/multi_test.js b/devtools/client/sourceeditor/test/codemirror/multi_test.js
new file mode 100644
index 000000000..a8e760d27
--- /dev/null
+++ b/devtools/client/sourceeditor/test/codemirror/multi_test.js
@@ -0,0 +1,285 @@
+(function() {
+ namespace = "multi_";
+
+ function hasSelections(cm) {
+ var sels = cm.listSelections();
+ var given = (arguments.length - 1) / 4;
+ if (sels.length != given)
+ throw new Failure("expected " + given + " selections, found " + sels.length);
+ for (var i = 0, p = 1; i < given; i++, p += 4) {
+ var anchor = Pos(arguments[p], arguments[p + 1]);
+ var head = Pos(arguments[p + 2], arguments[p + 3]);
+ eqPos(sels[i].anchor, anchor, "anchor of selection " + i);
+ eqPos(sels[i].head, head, "head of selection " + i);
+ }
+ }
+ function hasCursors(cm) {
+ var sels = cm.listSelections();
+ var given = (arguments.length - 1) / 2;
+ if (sels.length != given)
+ throw new Failure("expected " + given + " selections, found " + sels.length);
+ for (var i = 0, p = 1; i < given; i++, p += 2) {
+ eqPos(sels[i].anchor, sels[i].head, "something selected for " + i);
+ var head = Pos(arguments[p], arguments[p + 1]);
+ eqPos(sels[i].head, head, "selection " + i);
+ }
+ }
+
+ testCM("getSelection", function(cm) {
+ select(cm, {anchor: Pos(0, 0), head: Pos(1, 2)}, {anchor: Pos(2, 2), head: Pos(2, 0)});
+ eq(cm.getSelection(), "1234\n56\n90");
+ eq(cm.getSelection(false).join("|"), "1234|56|90");
+ eq(cm.getSelections().join("|"), "1234\n56|90");
+ }, {value: "1234\n5678\n90"});
+
+ testCM("setSelection", function(cm) {
+ select(cm, Pos(3, 0), Pos(0, 0), {anchor: Pos(2, 5), head: Pos(1, 0)});
+ hasSelections(cm, 0, 0, 0, 0,
+ 2, 5, 1, 0,
+ 3, 0, 3, 0);
+ cm.setSelection(Pos(1, 2), Pos(1, 1));
+ hasSelections(cm, 1, 2, 1, 1);
+ select(cm, {anchor: Pos(1, 1), head: Pos(2, 4)},
+ {anchor: Pos(0, 0), head: Pos(1, 3)},
+ Pos(3, 0), Pos(2, 2));
+ hasSelections(cm, 0, 0, 2, 4,
+ 3, 0, 3, 0);
+ cm.setSelections([{anchor: Pos(0, 1), head: Pos(0, 2)},
+ {anchor: Pos(1, 1), head: Pos(1, 2)},
+ {anchor: Pos(2, 1), head: Pos(2, 2)}], 1);
+ eqPos(cm.getCursor("head"), Pos(1, 2));
+ eqPos(cm.getCursor("anchor"), Pos(1, 1));
+ eqPos(cm.getCursor("from"), Pos(1, 1));
+ eqPos(cm.getCursor("to"), Pos(1, 2));
+ cm.setCursor(Pos(1, 1));
+ hasCursors(cm, 1, 1);
+ }, {value: "abcde\nabcde\nabcde\n"});
+
+ testCM("somethingSelected", function(cm) {
+ select(cm, Pos(0, 1), {anchor: Pos(0, 3), head: Pos(0, 5)});
+ eq(cm.somethingSelected(), true);
+ select(cm, Pos(0, 1), Pos(0, 3), Pos(0, 5));
+ eq(cm.somethingSelected(), false);
+ }, {value: "123456789"});
+
+ testCM("extendSelection", function(cm) {
+ select(cm, Pos(0, 1), Pos(1, 1), Pos(2, 1));
+ cm.setExtending(true);
+ cm.extendSelections([Pos(0, 2), Pos(1, 0), Pos(2, 3)]);
+ hasSelections(cm, 0, 1, 0, 2,
+ 1, 1, 1, 0,
+ 2, 1, 2, 3);
+ cm.extendSelection(Pos(2, 4), Pos(2, 0));
+ hasSelections(cm, 2, 4, 2, 0);
+ }, {value: "1234\n1234\n1234"});
+
+ testCM("addSelection", function(cm) {
+ select(cm, Pos(0, 1), Pos(1, 1));
+ cm.addSelection(Pos(0, 0), Pos(0, 4));
+ hasSelections(cm, 0, 0, 0, 4,
+ 1, 1, 1, 1);
+ cm.addSelection(Pos(2, 2));
+ hasSelections(cm, 0, 0, 0, 4,
+ 1, 1, 1, 1,
+ 2, 2, 2, 2);
+ }, {value: "1234\n1234\n1234"});
+
+ testCM("replaceSelection", function(cm) {
+ var selections = [{anchor: Pos(0, 0), head: Pos(0, 1)},
+ {anchor: Pos(0, 2), head: Pos(0, 3)},
+ {anchor: Pos(0, 4), head: Pos(0, 5)},
+ {anchor: Pos(2, 1), head: Pos(2, 4)},
+ {anchor: Pos(2, 5), head: Pos(2, 6)}];
+ var val = "123456\n123456\n123456";
+ cm.setValue(val);
+ cm.setSelections(selections);
+ cm.replaceSelection("ab", "around");
+ eq(cm.getValue(), "ab2ab4ab6\n123456\n1ab5ab");
+ hasSelections(cm, 0, 0, 0, 2,
+ 0, 3, 0, 5,
+ 0, 6, 0, 8,
+ 2, 1, 2, 3,
+ 2, 4, 2, 6);
+ cm.setValue(val);
+ cm.setSelections(selections);
+ cm.replaceSelection("", "around");
+ eq(cm.getValue(), "246\n123456\n15");
+ hasSelections(cm, 0, 0, 0, 0,
+ 0, 1, 0, 1,
+ 0, 2, 0, 2,
+ 2, 1, 2, 1,
+ 2, 2, 2, 2);
+ cm.setValue(val);
+ cm.setSelections(selections);
+ cm.replaceSelection("X\nY\nZ", "around");
+ hasSelections(cm, 0, 0, 2, 1,
+ 2, 2, 4, 1,
+ 4, 2, 6, 1,
+ 8, 1, 10, 1,
+ 10, 2, 12, 1);
+ cm.replaceSelection("a", "around");
+ hasSelections(cm, 0, 0, 0, 1,
+ 0, 2, 0, 3,
+ 0, 4, 0, 5,
+ 2, 1, 2, 2,
+ 2, 3, 2, 4);
+ cm.replaceSelection("xy", "start");
+ hasSelections(cm, 0, 0, 0, 0,
+ 0, 3, 0, 3,
+ 0, 6, 0, 6,
+ 2, 1, 2, 1,
+ 2, 4, 2, 4);
+ cm.replaceSelection("z\nf");
+ hasSelections(cm, 1, 1, 1, 1,
+ 2, 1, 2, 1,
+ 3, 1, 3, 1,
+ 6, 1, 6, 1,
+ 7, 1, 7, 1);
+ eq(cm.getValue(), "z\nfxy2z\nfxy4z\nfxy6\n123456\n1z\nfxy5z\nfxy");
+ });
+
+ function select(cm) {
+ var sels = [];
+ for (var i = 1; i < arguments.length; i++) {
+ var arg = arguments[i];
+ if (arg.head) sels.push(arg);
+ else sels.push({head: arg, anchor: arg});
+ }
+ cm.setSelections(sels, sels.length - 1);
+ }
+
+ testCM("indentSelection", function(cm) {
+ select(cm, Pos(0, 1), Pos(1, 1));
+ cm.indentSelection(4);
+ eq(cm.getValue(), " foo\n bar\nbaz");
+
+ select(cm, Pos(0, 2), Pos(0, 3), Pos(0, 4));
+ cm.indentSelection(-2);
+ eq(cm.getValue(), " foo\n bar\nbaz");
+
+ select(cm, {anchor: Pos(0, 0), head: Pos(1, 2)},
+ {anchor: Pos(1, 3), head: Pos(2, 0)});
+ cm.indentSelection(-2);
+ eq(cm.getValue(), "foo\n bar\nbaz");
+ }, {value: "foo\nbar\nbaz"});
+
+ testCM("killLine", function(cm) {
+ select(cm, Pos(0, 1), Pos(0, 2), Pos(1, 1));
+ cm.execCommand("killLine");
+ eq(cm.getValue(), "f\nb\nbaz");
+ cm.execCommand("killLine");
+ eq(cm.getValue(), "fbbaz");
+ cm.setValue("foo\nbar\nbaz");
+ select(cm, Pos(0, 1), {anchor: Pos(0, 2), head: Pos(2, 1)});
+ cm.execCommand("killLine");
+ eq(cm.getValue(), "faz");
+ }, {value: "foo\nbar\nbaz"});
+
+ testCM("deleteLine", function(cm) {
+ select(cm, Pos(0, 0),
+ {head: Pos(0, 1), anchor: Pos(2, 0)},
+ Pos(4, 0));
+ cm.execCommand("deleteLine");
+ eq(cm.getValue(), "4\n6\n7");
+ select(cm, Pos(2, 1));
+ cm.execCommand("deleteLine");
+ eq(cm.getValue(), "4\n6\n");
+ }, {value: "1\n2\n3\n4\n5\n6\n7"});
+
+ testCM("deleteH", function(cm) {
+ select(cm, Pos(0, 4), {anchor: Pos(1, 4), head: Pos(1, 5)});
+ cm.execCommand("delWordAfter");
+ eq(cm.getValue(), "foo bar baz\nabc ef ghi\n");
+ cm.execCommand("delWordAfter");
+ eq(cm.getValue(), "foo baz\nabc ghi\n");
+ cm.execCommand("delCharBefore");
+ cm.execCommand("delCharBefore");
+ eq(cm.getValue(), "fo baz\nab ghi\n");
+ select(cm, Pos(0, 3), Pos(0, 4), Pos(0, 5));
+ cm.execCommand("delWordAfter");
+ eq(cm.getValue(), "fo \nab ghi\n");
+ }, {value: "foo bar baz\nabc def ghi\n"});
+
+ testCM("goLineStart", function(cm) {
+ select(cm, Pos(0, 2), Pos(0, 3), Pos(1, 1));
+ cm.execCommand("goLineStart");
+ hasCursors(cm, 0, 0, 1, 0);
+ select(cm, Pos(1, 1), Pos(0, 1));
+ cm.setExtending(true);
+ cm.execCommand("goLineStart");
+ hasSelections(cm, 0, 1, 0, 0,
+ 1, 1, 1, 0);
+ }, {value: "foo\nbar\nbaz"});
+
+ testCM("moveV", function(cm) {
+ select(cm, Pos(0, 2), Pos(1, 2));
+ cm.execCommand("goLineDown");
+ hasCursors(cm, 1, 2, 2, 2);
+ cm.execCommand("goLineUp");
+ hasCursors(cm, 0, 2, 1, 2);
+ cm.execCommand("goLineUp");
+ hasCursors(cm, 0, 0, 0, 2);
+ cm.execCommand("goLineUp");
+ hasCursors(cm, 0, 0);
+ select(cm, Pos(0, 2), Pos(1, 2));
+ cm.setExtending(true);
+ cm.execCommand("goLineDown");
+ hasSelections(cm, 0, 2, 2, 2);
+ }, {value: "12345\n12345\n12345"});
+
+ testCM("moveH", function(cm) {
+ select(cm, Pos(0, 1), Pos(0, 3), Pos(0, 5), Pos(2, 3));
+ cm.execCommand("goCharRight");
+ hasCursors(cm, 0, 2, 0, 4, 1, 0, 2, 4);
+ cm.execCommand("goCharLeft");
+ hasCursors(cm, 0, 1, 0, 3, 0, 5, 2, 3);
+ for (var i = 0; i < 15; i++)
+ cm.execCommand("goCharRight");
+ hasCursors(cm, 2, 4, 2, 5);
+ }, {value: "12345\n12345\n12345"});
+
+ testCM("newlineAndIndent", function(cm) {
+ select(cm, Pos(0, 5), Pos(1, 5));
+ cm.execCommand("newlineAndIndent");
+ hasCursors(cm, 1, 2, 3, 2);
+ eq(cm.getValue(), "x = [\n 1];\ny = [\n 2];");
+ cm.undo();
+ eq(cm.getValue(), "x = [1];\ny = [2];");
+ hasCursors(cm, 0, 5, 1, 5);
+ select(cm, Pos(0, 5), Pos(0, 6));
+ cm.execCommand("newlineAndIndent");
+ hasCursors(cm, 1, 2, 2, 0);
+ eq(cm.getValue(), "x = [\n 1\n];\ny = [2];");
+ }, {value: "x = [1];\ny = [2];", mode: "javascript"});
+
+ testCM("goDocStartEnd", function(cm) {
+ select(cm, Pos(0, 1), Pos(1, 1));
+ cm.execCommand("goDocStart");
+ hasCursors(cm, 0, 0);
+ select(cm, Pos(0, 1), Pos(1, 1));
+ cm.execCommand("goDocEnd");
+ hasCursors(cm, 1, 3);
+ select(cm, Pos(0, 1), Pos(1, 1));
+ cm.setExtending(true);
+ cm.execCommand("goDocEnd");
+ hasSelections(cm, 1, 1, 1, 3);
+ }, {value: "abc\ndef"});
+
+ testCM("selectionHistory", function(cm) {
+ for (var i = 0; i < 3; ++i)
+ cm.addSelection(Pos(0, i * 2), Pos(0, i * 2 + 1));
+ cm.execCommand("undoSelection");
+ eq(cm.getSelection(), "1\n2");
+ cm.execCommand("undoSelection");
+ eq(cm.getSelection(), "1");
+ cm.execCommand("undoSelection");
+ eq(cm.getSelection(), "");
+ eqPos(cm.getCursor(), Pos(0, 0));
+ cm.execCommand("redoSelection");
+ eq(cm.getSelection(), "1");
+ cm.execCommand("redoSelection");
+ eq(cm.getSelection(), "1\n2");
+ cm.execCommand("redoSelection");
+ eq(cm.getSelection(), "1\n2\n3");
+ }, {value: "1 2 3"});
+})();
diff --git a/devtools/client/sourceeditor/test/codemirror/search_test.js b/devtools/client/sourceeditor/test/codemirror/search_test.js
new file mode 100644
index 000000000..04a1e685a
--- /dev/null
+++ b/devtools/client/sourceeditor/test/codemirror/search_test.js
@@ -0,0 +1,62 @@
+(function() {
+ "use strict";
+
+ function test(name) {
+ var text = Array.prototype.slice.call(arguments, 1, arguments.length - 1).join("\n");
+ var body = arguments[arguments.length - 1];
+ return window.test("search_" + name, function() {
+ body(new CodeMirror.Doc(text));
+ });
+ }
+
+ function run(doc, query, insensitive) {
+ var cursor = doc.getSearchCursor(query, null, insensitive);
+ for (var i = 3; i < arguments.length; i += 4) {
+ var found = cursor.findNext();
+ is(found, "not enough results (forward)");
+ eqPos(Pos(arguments[i], arguments[i + 1]), cursor.from(), "from, forward, " + (i - 3) / 4);
+ eqPos(Pos(arguments[i + 2], arguments[i + 3]), cursor.to(), "to, forward, " + (i - 3) / 4);
+ }
+ is(!cursor.findNext(), "too many matches (forward)");
+ for (var i = arguments.length - 4; i >= 3; i -= 4) {
+ var found = cursor.findPrevious();
+ is(found, "not enough results (backwards)");
+ eqPos(Pos(arguments[i], arguments[i + 1]), cursor.from(), "from, backwards, " + (i - 3) / 4);
+ eqPos(Pos(arguments[i + 2], arguments[i + 3]), cursor.to(), "to, backwards, " + (i - 3) / 4);
+ }
+ is(!cursor.findPrevious(), "too many matches (backwards)");
+ }
+
+ test("simple", "abcdefg", "abcdefg", function(doc) {
+ run(doc, "cde", false, 0, 2, 0, 5, 1, 2, 1, 5);
+ });
+
+ test("multiline", "hallo", "goodbye", function(doc) {
+ run(doc, "llo\ngoo", false, 0, 2, 1, 3);
+ run(doc, "blah\nhall", false);
+ run(doc, "bye\neye", false);
+ });
+
+ test("regexp", "abcde", "abcde", function(doc) {
+ run(doc, /bcd/, false, 0, 1, 0, 4, 1, 1, 1, 4);
+ run(doc, /BCD/, false);
+ run(doc, /BCD/i, false, 0, 1, 0, 4, 1, 1, 1, 4);
+ });
+
+ test("insensitive", "hallo", "HALLO", "oink", "hAllO", function(doc) {
+ run(doc, "All", false, 3, 1, 3, 4);
+ run(doc, "All", true, 0, 1, 0, 4, 1, 1, 1, 4, 3, 1, 3, 4);
+ });
+
+ test("multilineInsensitive", "zie ginds komT", "De Stoomboot", "uit Spanje weer aan", function(doc) {
+ run(doc, "komt\nde stoomboot\nuit", false);
+ run(doc, "komt\nde stoomboot\nuit", true, 0, 10, 2, 3);
+ run(doc, "kOMt\ndE stOOmboot\nuiT", true, 0, 10, 2, 3);
+ });
+
+ test("expandingCaseFold", "<b>Ä°Ä° Ä°Ä°</b>", "<b>uu uu</b>", function(doc) {
+ if (phantom) return; // A Phantom bug makes this hang
+ run(doc, "</b>", true, 0, 8, 0, 12, 1, 8, 1, 12);
+ run(doc, "Ä°Ä°", true, 0, 3, 0, 5, 0, 6, 0, 8);
+ });
+})();
diff --git a/devtools/client/sourceeditor/test/codemirror/sublime_test.js b/devtools/client/sourceeditor/test/codemirror/sublime_test.js
new file mode 100644
index 000000000..c5c19c0a2
--- /dev/null
+++ b/devtools/client/sourceeditor/test/codemirror/sublime_test.js
@@ -0,0 +1,307 @@
+(function() {
+ "use strict";
+
+ var Pos = CodeMirror.Pos;
+ namespace = "sublime_";
+
+ function stTest(name) {
+ var actions = Array.prototype.slice.call(arguments, 1);
+ testCM(name, function(cm) {
+ for (var i = 0; i < actions.length; i++) {
+ var action = actions[i];
+ if (typeof action == "string" && i == 0)
+ cm.setValue(action);
+ else if (typeof action == "string")
+ cm.execCommand(action);
+ else if (action instanceof Pos)
+ cm.setCursor(action);
+ else
+ action(cm);
+ }
+ });
+ }
+
+ function at(line, ch, msg) {
+ return function(cm) {
+ eq(cm.listSelections().length, 1);
+ eqPos(cm.getCursor("head"), Pos(line, ch), msg);
+ eqPos(cm.getCursor("anchor"), Pos(line, ch), msg);
+ };
+ }
+
+ function val(content, msg) {
+ return function(cm) { eq(cm.getValue(), content, msg); };
+ }
+
+ function argsToRanges(args) {
+ if (args.length % 4) throw new Error("Wrong number of arguments for ranges.");
+ var ranges = [];
+ for (var i = 0; i < args.length; i += 4)
+ ranges.push({anchor: Pos(args[i], args[i + 1]),
+ head: Pos(args[i + 2], args[i + 3])});
+ return ranges;
+ }
+
+ function setSel() {
+ var ranges = argsToRanges(arguments);
+ return function(cm) { cm.setSelections(ranges, 0); };
+ }
+
+ function hasSel() {
+ var ranges = argsToRanges(arguments);
+ return function(cm) {
+ var sels = cm.listSelections();
+ if (sels.length != ranges.length)
+ throw new Failure("Expected " + ranges.length + " selections, but found " + sels.length);
+ for (var i = 0; i < sels.length; i++) {
+ eqPos(sels[i].anchor, ranges[i].anchor, "anchor " + i);
+ eqPos(sels[i].head, ranges[i].head, "head " + i);
+ }
+ };
+ }
+
+ stTest("bySubword", "the foo_bar DooDahBah \n a",
+ "goSubwordLeft", at(0, 0),
+ "goSubwordRight", at(0, 3),
+ "goSubwordRight", at(0, 7),
+ "goSubwordRight", at(0, 11),
+ "goSubwordRight", at(0, 15),
+ "goSubwordRight", at(0, 18),
+ "goSubwordRight", at(0, 21),
+ "goSubwordRight", at(0, 22),
+ "goSubwordRight", at(1, 0),
+ "goSubwordRight", at(1, 2),
+ "goSubwordRight", at(1, 2),
+ "goSubwordLeft", at(1, 1),
+ "goSubwordLeft", at(1, 0),
+ "goSubwordLeft", at(0, 22),
+ "goSubwordLeft", at(0, 18),
+ "goSubwordLeft", at(0, 15),
+ "goSubwordLeft", at(0, 12),
+ "goSubwordLeft", at(0, 8),
+ "goSubwordLeft", at(0, 4),
+ "goSubwordLeft", at(0, 0));
+
+ stTest("splitSelectionByLine", "abc\ndef\nghi",
+ setSel(0, 1, 2, 2),
+ "splitSelectionByLine",
+ hasSel(0, 1, 0, 3,
+ 1, 0, 1, 3,
+ 2, 0, 2, 2));
+
+ stTest("splitSelectionByLineMulti", "abc\ndef\nghi\njkl",
+ setSel(0, 1, 1, 1,
+ 1, 2, 3, 2,
+ 3, 3, 3, 3),
+ "splitSelectionByLine",
+ hasSel(0, 1, 0, 3,
+ 1, 0, 1, 1,
+ 1, 2, 1, 3,
+ 2, 0, 2, 3,
+ 3, 0, 3, 2,
+ 3, 3, 3, 3));
+
+ stTest("selectLine", "abc\ndef\nghi",
+ setSel(0, 1, 0, 1,
+ 2, 0, 2, 1),
+ "selectLine",
+ hasSel(0, 0, 1, 0,
+ 2, 0, 2, 3),
+ setSel(0, 1, 1, 0),
+ "selectLine",
+ hasSel(0, 0, 2, 0));
+
+ stTest("insertLineAfter", "abcde\nfghijkl\nmn",
+ setSel(0, 1, 0, 1,
+ 0, 3, 0, 3,
+ 1, 2, 1, 2,
+ 1, 3, 1, 5), "insertLineAfter",
+ hasSel(1, 0, 1, 0,
+ 3, 0, 3, 0), val("abcde\n\nfghijkl\n\nmn"));
+
+ stTest("insertLineBefore", "abcde\nfghijkl\nmn",
+ setSel(0, 1, 0, 1,
+ 0, 3, 0, 3,
+ 1, 2, 1, 2,
+ 1, 3, 1, 5), "insertLineBefore",
+ hasSel(0, 0, 0, 0,
+ 2, 0, 2, 0), val("\nabcde\n\nfghijkl\nmn"));
+
+ stTest("selectNextOccurrence", "a foo bar\nfoobar foo",
+ setSel(0, 2, 0, 5),
+ "selectNextOccurrence", hasSel(0, 2, 0, 5,
+ 1, 0, 1, 3),
+ "selectNextOccurrence", hasSel(0, 2, 0, 5,
+ 1, 0, 1, 3,
+ 1, 7, 1, 10),
+ "selectNextOccurrence", hasSel(0, 2, 0, 5,
+ 1, 0, 1, 3,
+ 1, 7, 1, 10),
+ Pos(0, 3), "selectNextOccurrence", hasSel(0, 2, 0, 5),
+ "selectNextOccurrence", hasSel(0, 2, 0, 5,
+ 1, 7, 1, 10),
+ setSel(0, 6, 0, 9),
+ "selectNextOccurrence", hasSel(0, 6, 0, 9,
+ 1, 3, 1, 6));
+
+ stTest("selectScope", "foo(a) {\n bar[1, 2];\n}",
+ "selectScope", hasSel(0, 0, 2, 1),
+ Pos(0, 4), "selectScope", hasSel(0, 4, 0, 5),
+ Pos(0, 5), "selectScope", hasSel(0, 4, 0, 5),
+ Pos(0, 6), "selectScope", hasSel(0, 0, 2, 1),
+ Pos(0, 8), "selectScope", hasSel(0, 8, 2, 0),
+ Pos(1, 2), "selectScope", hasSel(0, 8, 2, 0),
+ Pos(1, 6), "selectScope", hasSel(1, 6, 1, 10),
+ Pos(1, 9), "selectScope", hasSel(1, 6, 1, 10));
+
+ stTest("goToBracket", "foo(a) {\n bar[1, 2];\n}",
+ Pos(0, 0), "goToBracket", at(0, 0),
+ Pos(0, 4), "goToBracket", at(0, 5), "goToBracket", at(0, 4),
+ Pos(0, 8), "goToBracket", at(2, 0), "goToBracket", at(0, 8),
+ Pos(1, 2), "goToBracket", at(2, 0),
+ Pos(1, 7), "goToBracket", at(1, 10), "goToBracket", at(1, 6));
+
+ stTest("swapLine", "1\n2\n3---\n4\n5",
+ "swapLineDown", val("2\n1\n3---\n4\n5"),
+ "swapLineUp", val("1\n2\n3---\n4\n5"),
+ "swapLineUp", val("1\n2\n3---\n4\n5"),
+ Pos(4, 1), "swapLineDown", val("1\n2\n3---\n4\n5"),
+ setSel(0, 1, 0, 1,
+ 1, 0, 2, 0,
+ 2, 2, 2, 2),
+ "swapLineDown", val("4\n1\n2\n3---\n5"),
+ hasSel(1, 1, 1, 1,
+ 2, 0, 3, 0,
+ 3, 2, 3, 2),
+ "swapLineUp", val("1\n2\n3---\n4\n5"),
+ hasSel(0, 1, 0, 1,
+ 1, 0, 2, 0,
+ 2, 2, 2, 2));
+
+ stTest("swapLineEmptyBottomSel", "1\n2\n3",
+ setSel(0, 1, 1, 0),
+ "swapLineDown", val("2\n1\n3"), hasSel(1, 1, 2, 0),
+ "swapLineUp", val("1\n2\n3"), hasSel(0, 1, 1, 0),
+ "swapLineUp", val("1\n2\n3"), hasSel(0, 0, 0, 0));
+
+ stTest("swapLineUpFromEnd", "a\nb\nc",
+ Pos(2, 1), "swapLineUp",
+ hasSel(1, 1, 1, 1), val("a\nc\nb"));
+
+ stTest("joinLines", "abc\ndef\nghi\njkl",
+ "joinLines", val("abc def\nghi\njkl"), at(0, 4),
+ "undo",
+ setSel(0, 2, 1, 1), "joinLines",
+ val("abc def ghi\njkl"), hasSel(0, 2, 0, 8),
+ "undo",
+ setSel(0, 1, 0, 1,
+ 1, 1, 1, 1,
+ 3, 1, 3, 1), "joinLines",
+ val("abc def ghi\njkl"), hasSel(0, 4, 0, 4,
+ 0, 8, 0, 8,
+ 1, 3, 1, 3));
+
+ stTest("duplicateLine", "abc\ndef\nghi",
+ Pos(1, 0), "duplicateLine", val("abc\ndef\ndef\nghi"), at(2, 0),
+ "undo",
+ setSel(0, 1, 0, 1,
+ 1, 1, 1, 1,
+ 2, 1, 2, 1), "duplicateLine",
+ val("abc\nabc\ndef\ndef\nghi\nghi"), hasSel(1, 1, 1, 1,
+ 3, 1, 3, 1,
+ 5, 1, 5, 1));
+ stTest("duplicateLineSelection", "abcdef",
+ setSel(0, 1, 0, 1,
+ 0, 2, 0, 4,
+ 0, 5, 0, 5),
+ "duplicateLine",
+ val("abcdef\nabcdcdef\nabcdcdef"), hasSel(2, 1, 2, 1,
+ 2, 4, 2, 6,
+ 2, 7, 2, 7));
+
+ stTest("selectLinesUpward", "123\n345\n789\n012",
+ setSel(0, 1, 0, 1,
+ 1, 1, 1, 3,
+ 2, 0, 2, 0,
+ 3, 0, 3, 0),
+ "selectLinesUpward",
+ hasSel(0, 1, 0, 1,
+ 0, 3, 0, 3,
+ 1, 0, 1, 0,
+ 1, 1, 1, 3,
+ 2, 0, 2, 0,
+ 3, 0, 3, 0));
+
+ stTest("selectLinesDownward", "123\n345\n789\n012",
+ setSel(0, 1, 0, 1,
+ 1, 1, 1, 3,
+ 2, 0, 2, 0,
+ 3, 0, 3, 0),
+ "selectLinesDownward",
+ hasSel(0, 1, 0, 1,
+ 1, 1, 1, 3,
+ 2, 0, 2, 0,
+ 2, 3, 2, 3,
+ 3, 0, 3, 0));
+
+ stTest("sortLines", "c\nb\na\nC\nB\nA",
+ "sortLines", val("A\nB\nC\na\nb\nc"),
+ "undo",
+ setSel(0, 0, 2, 0,
+ 3, 0, 5, 0),
+ "sortLines", val("a\nb\nc\nA\nB\nC"),
+ hasSel(0, 0, 2, 1,
+ 3, 0, 5, 1),
+ "undo",
+ setSel(1, 0, 4, 0), "sortLinesInsensitive", val("c\na\nB\nb\nC\nA"));
+
+ stTest("bookmarks", "abc\ndef\nghi\njkl",
+ Pos(0, 1), "toggleBookmark",
+ setSel(1, 1, 1, 2), "toggleBookmark",
+ setSel(2, 1, 2, 2), "toggleBookmark",
+ "nextBookmark", hasSel(0, 1, 0, 1),
+ "nextBookmark", hasSel(1, 1, 1, 2),
+ "nextBookmark", hasSel(2, 1, 2, 2),
+ "prevBookmark", hasSel(1, 1, 1, 2),
+ "prevBookmark", hasSel(0, 1, 0, 1),
+ "prevBookmark", hasSel(2, 1, 2, 2),
+ "prevBookmark", hasSel(1, 1, 1, 2),
+ "toggleBookmark",
+ "prevBookmark", hasSel(2, 1, 2, 2),
+ "prevBookmark", hasSel(0, 1, 0, 1),
+ "selectBookmarks", hasSel(0, 1, 0, 1,
+ 2, 1, 2, 2),
+ "clearBookmarks",
+ Pos(0, 0), "selectBookmarks", at(0, 0));
+
+ stTest("smartBackspace", " foo\n bar",
+ setSel(0, 2, 0, 2, 1, 4, 1, 4, 1, 6, 1, 6), "smartBackspace",
+ val("foo\n br"))
+
+ stTest("upAndDowncaseAtCursor", "abc\ndef x\nghI",
+ setSel(0, 1, 0, 3,
+ 1, 1, 1, 1,
+ 1, 4, 1, 4), "upcaseAtCursor",
+ val("aBC\nDEF x\nghI"), hasSel(0, 1, 0, 3,
+ 1, 3, 1, 3,
+ 1, 4, 1, 4),
+ "downcaseAtCursor",
+ val("abc\ndef x\nghI"), hasSel(0, 1, 0, 3,
+ 1, 3, 1, 3,
+ 1, 4, 1, 4));
+
+ stTest("mark", "abc\ndef\nghi",
+ Pos(1, 1), "setSublimeMark",
+ Pos(2, 1), "selectToSublimeMark", hasSel(2, 1, 1, 1),
+ Pos(0, 1), "swapWithSublimeMark", at(1, 1), "swapWithSublimeMark", at(0, 1),
+ "deleteToSublimeMark", val("aef\nghi"),
+ "sublimeYank", val("abc\ndef\nghi"), at(1, 1));
+
+ stTest("findUnder", "foo foobar a",
+ "findUnder", hasSel(0, 4, 0, 7),
+ "findUnder", hasSel(0, 0, 0, 3),
+ "findUnderPrevious", hasSel(0, 4, 0, 7),
+ "findUnderPrevious", hasSel(0, 0, 0, 3),
+ Pos(0, 4), "findUnder", hasSel(0, 4, 0, 10),
+ Pos(0, 11), "findUnder", hasSel(0, 11, 0, 11));
+})();
diff --git a/devtools/client/sourceeditor/test/codemirror/test.js b/devtools/client/sourceeditor/test/codemirror/test.js
new file mode 100644
index 000000000..82ee231e3
--- /dev/null
+++ b/devtools/client/sourceeditor/test/codemirror/test.js
@@ -0,0 +1,2151 @@
+var Pos = CodeMirror.Pos;
+
+CodeMirror.defaults.rtlMoveVisually = true;
+
+function forEach(arr, f) {
+ for (var i = 0, e = arr.length; i < e; ++i) f(arr[i], i);
+}
+
+function addDoc(cm, width, height) {
+ var content = [], line = "";
+ for (var i = 0; i < width; ++i) line += "x";
+ for (var i = 0; i < height; ++i) content.push(line);
+ cm.setValue(content.join("\n"));
+}
+
+function byClassName(elt, cls) {
+ if (elt.getElementsByClassName) return elt.getElementsByClassName(cls);
+ var found = [], re = new RegExp("\\b" + cls + "\\b");
+ function search(elt) {
+ if (elt.nodeType == 3) return;
+ if (re.test(elt.className)) found.push(elt);
+ for (var i = 0, e = elt.childNodes.length; i < e; ++i)
+ search(elt.childNodes[i]);
+ }
+ search(elt);
+ return found;
+}
+
+var ie_lt8 = /MSIE [1-7]\b/.test(navigator.userAgent);
+var ie_lt9 = /MSIE [1-8]\b/.test(navigator.userAgent);
+var mac = /Mac/.test(navigator.platform);
+var phantom = /PhantomJS/.test(navigator.userAgent);
+var opera = /Opera\/\./.test(navigator.userAgent);
+var opera_version = opera && navigator.userAgent.match(/Version\/(\d+\.\d+)/);
+if (opera_version) opera_version = Number(opera_version);
+var opera_lt10 = opera && (!opera_version || opera_version < 10);
+
+namespace = "core_";
+
+test("core_fromTextArea", function() {
+ var te = document.getElementById("code");
+ te.value = "CONTENT";
+ var cm = CodeMirror.fromTextArea(te);
+ is(!te.offsetHeight);
+ eq(cm.getValue(), "CONTENT");
+ cm.setValue("foo\nbar");
+ eq(cm.getValue(), "foo\nbar");
+ cm.save();
+ is(/^foo\r?\nbar$/.test(te.value));
+ cm.setValue("xxx");
+ cm.toTextArea();
+ is(te.offsetHeight);
+ eq(te.value, "xxx");
+});
+
+testCM("getRange", function(cm) {
+ eq(cm.getLine(0), "1234");
+ eq(cm.getLine(1), "5678");
+ eq(cm.getLine(2), null);
+ eq(cm.getLine(-1), null);
+ eq(cm.getRange(Pos(0, 0), Pos(0, 3)), "123");
+ eq(cm.getRange(Pos(0, -1), Pos(0, 200)), "1234");
+ eq(cm.getRange(Pos(0, 2), Pos(1, 2)), "34\n56");
+ eq(cm.getRange(Pos(1, 2), Pos(100, 0)), "78");
+}, {value: "1234\n5678"});
+
+testCM("replaceRange", function(cm) {
+ eq(cm.getValue(), "");
+ cm.replaceRange("foo\n", Pos(0, 0));
+ eq(cm.getValue(), "foo\n");
+ cm.replaceRange("a\nb", Pos(0, 1));
+ eq(cm.getValue(), "fa\nboo\n");
+ eq(cm.lineCount(), 3);
+ cm.replaceRange("xyzzy", Pos(0, 0), Pos(1, 1));
+ eq(cm.getValue(), "xyzzyoo\n");
+ cm.replaceRange("abc", Pos(0, 0), Pos(10, 0));
+ eq(cm.getValue(), "abc");
+ eq(cm.lineCount(), 1);
+});
+
+testCM("selection", function(cm) {
+ cm.setSelection(Pos(0, 4), Pos(2, 2));
+ is(cm.somethingSelected());
+ eq(cm.getSelection(), "11\n222222\n33");
+ eqPos(cm.getCursor(false), Pos(2, 2));
+ eqPos(cm.getCursor(true), Pos(0, 4));
+ cm.setSelection(Pos(1, 0));
+ is(!cm.somethingSelected());
+ eq(cm.getSelection(), "");
+ eqPos(cm.getCursor(true), Pos(1, 0));
+ cm.replaceSelection("abc", "around");
+ eq(cm.getSelection(), "abc");
+ eq(cm.getValue(), "111111\nabc222222\n333333");
+ cm.replaceSelection("def", "end");
+ eq(cm.getSelection(), "");
+ eqPos(cm.getCursor(true), Pos(1, 3));
+ cm.setCursor(Pos(2, 1));
+ eqPos(cm.getCursor(true), Pos(2, 1));
+ cm.setCursor(1, 2);
+ eqPos(cm.getCursor(true), Pos(1, 2));
+}, {value: "111111\n222222\n333333"});
+
+testCM("extendSelection", function(cm) {
+ cm.setExtending(true);
+ addDoc(cm, 10, 10);
+ cm.setSelection(Pos(3, 5));
+ eqPos(cm.getCursor("head"), Pos(3, 5));
+ eqPos(cm.getCursor("anchor"), Pos(3, 5));
+ cm.setSelection(Pos(2, 5), Pos(5, 5));
+ eqPos(cm.getCursor("head"), Pos(5, 5));
+ eqPos(cm.getCursor("anchor"), Pos(2, 5));
+ eqPos(cm.getCursor("start"), Pos(2, 5));
+ eqPos(cm.getCursor("end"), Pos(5, 5));
+ cm.setSelection(Pos(5, 5), Pos(2, 5));
+ eqPos(cm.getCursor("head"), Pos(2, 5));
+ eqPos(cm.getCursor("anchor"), Pos(5, 5));
+ eqPos(cm.getCursor("start"), Pos(2, 5));
+ eqPos(cm.getCursor("end"), Pos(5, 5));
+ cm.extendSelection(Pos(3, 2));
+ eqPos(cm.getCursor("head"), Pos(3, 2));
+ eqPos(cm.getCursor("anchor"), Pos(5, 5));
+ cm.extendSelection(Pos(6, 2));
+ eqPos(cm.getCursor("head"), Pos(6, 2));
+ eqPos(cm.getCursor("anchor"), Pos(5, 5));
+ cm.extendSelection(Pos(6, 3), Pos(6, 4));
+ eqPos(cm.getCursor("head"), Pos(6, 4));
+ eqPos(cm.getCursor("anchor"), Pos(5, 5));
+ cm.extendSelection(Pos(0, 3), Pos(0, 4));
+ eqPos(cm.getCursor("head"), Pos(0, 3));
+ eqPos(cm.getCursor("anchor"), Pos(5, 5));
+ cm.extendSelection(Pos(4, 5), Pos(6, 5));
+ eqPos(cm.getCursor("head"), Pos(6, 5));
+ eqPos(cm.getCursor("anchor"), Pos(4, 5));
+ cm.setExtending(false);
+ cm.extendSelection(Pos(0, 3), Pos(0, 4));
+ eqPos(cm.getCursor("head"), Pos(0, 3));
+ eqPos(cm.getCursor("anchor"), Pos(0, 4));
+});
+
+testCM("lines", function(cm) {
+ eq(cm.getLine(0), "111111");
+ eq(cm.getLine(1), "222222");
+ eq(cm.getLine(-1), null);
+ cm.replaceRange("", Pos(1, 0), Pos(2, 0))
+ cm.replaceRange("abc", Pos(1, 0), Pos(1));
+ eq(cm.getValue(), "111111\nabc");
+}, {value: "111111\n222222\n333333"});
+
+testCM("indent", function(cm) {
+ cm.indentLine(1);
+ eq(cm.getLine(1), " blah();");
+ cm.setOption("indentUnit", 8);
+ cm.indentLine(1);
+ eq(cm.getLine(1), "\tblah();");
+ cm.setOption("indentUnit", 10);
+ cm.setOption("tabSize", 4);
+ cm.indentLine(1);
+ eq(cm.getLine(1), "\t\t blah();");
+}, {value: "if (x) {\nblah();\n}", indentUnit: 3, indentWithTabs: true, tabSize: 8});
+
+testCM("indentByNumber", function(cm) {
+ cm.indentLine(0, 2);
+ eq(cm.getLine(0), " foo");
+ cm.indentLine(0, -200);
+ eq(cm.getLine(0), "foo");
+ cm.setSelection(Pos(0, 0), Pos(1, 2));
+ cm.indentSelection(3);
+ eq(cm.getValue(), " foo\n bar\nbaz");
+}, {value: "foo\nbar\nbaz"});
+
+test("core_defaults", function() {
+ var defsCopy = {}, defs = CodeMirror.defaults;
+ for (var opt in defs) defsCopy[opt] = defs[opt];
+ defs.indentUnit = 5;
+ defs.value = "uu";
+ defs.indentWithTabs = true;
+ defs.tabindex = 55;
+ var place = document.getElementById("testground"), cm = CodeMirror(place);
+ try {
+ eq(cm.getOption("indentUnit"), 5);
+ cm.setOption("indentUnit", 10);
+ eq(defs.indentUnit, 5);
+ eq(cm.getValue(), "uu");
+ eq(cm.getOption("indentWithTabs"), true);
+ eq(cm.getInputField().tabIndex, 55);
+ }
+ finally {
+ for (var opt in defsCopy) defs[opt] = defsCopy[opt];
+ place.removeChild(cm.getWrapperElement());
+ }
+});
+
+testCM("lineInfo", function(cm) {
+ eq(cm.lineInfo(-1), null);
+ var mark = document.createElement("span");
+ var lh = cm.setGutterMarker(1, "FOO", mark);
+ var info = cm.lineInfo(1);
+ eq(info.text, "222222");
+ eq(info.gutterMarkers.FOO, mark);
+ eq(info.line, 1);
+ eq(cm.lineInfo(2).gutterMarkers, null);
+ cm.setGutterMarker(lh, "FOO", null);
+ eq(cm.lineInfo(1).gutterMarkers, null);
+ cm.setGutterMarker(1, "FOO", mark);
+ cm.setGutterMarker(0, "FOO", mark);
+ cm.clearGutter("FOO");
+ eq(cm.lineInfo(0).gutterMarkers, null);
+ eq(cm.lineInfo(1).gutterMarkers, null);
+}, {value: "111111\n222222\n333333"});
+
+testCM("coords", function(cm) {
+ cm.setSize(null, 100);
+ addDoc(cm, 32, 200);
+ var top = cm.charCoords(Pos(0, 0));
+ var bot = cm.charCoords(Pos(200, 30));
+ is(top.left < bot.left);
+ is(top.top < bot.top);
+ is(top.top < top.bottom);
+ cm.scrollTo(null, 100);
+ var top2 = cm.charCoords(Pos(0, 0));
+ is(top.top > top2.top);
+ eq(top.left, top2.left);
+});
+
+testCM("coordsChar", function(cm) {
+ addDoc(cm, 35, 70);
+ for (var i = 0; i < 2; ++i) {
+ var sys = i ? "local" : "page";
+ for (var ch = 0; ch <= 35; ch += 5) {
+ for (var line = 0; line < 70; line += 5) {
+ cm.setCursor(line, ch);
+ var coords = cm.charCoords(Pos(line, ch), sys);
+ var pos = cm.coordsChar({left: coords.left + 1, top: coords.top + 1}, sys);
+ eqPos(pos, Pos(line, ch));
+ }
+ }
+ }
+}, {lineNumbers: true});
+
+testCM("posFromIndex", function(cm) {
+ cm.setValue(
+ "This function should\n" +
+ "convert a zero based index\n" +
+ "to line and ch."
+ );
+
+ var examples = [
+ { index: -1, line: 0, ch: 0 }, // <- Tests clipping
+ { index: 0, line: 0, ch: 0 },
+ { index: 10, line: 0, ch: 10 },
+ { index: 39, line: 1, ch: 18 },
+ { index: 55, line: 2, ch: 7 },
+ { index: 63, line: 2, ch: 15 },
+ { index: 64, line: 2, ch: 15 } // <- Tests clipping
+ ];
+
+ for (var i = 0; i < examples.length; i++) {
+ var example = examples[i];
+ var pos = cm.posFromIndex(example.index);
+ eq(pos.line, example.line);
+ eq(pos.ch, example.ch);
+ if (example.index >= 0 && example.index < 64)
+ eq(cm.indexFromPos(pos), example.index);
+ }
+});
+
+testCM("undo", function(cm) {
+ cm.replaceRange("def", Pos(0, 0), Pos(0));
+ eq(cm.historySize().undo, 1);
+ cm.undo();
+ eq(cm.getValue(), "abc");
+ eq(cm.historySize().undo, 0);
+ eq(cm.historySize().redo, 1);
+ cm.redo();
+ eq(cm.getValue(), "def");
+ eq(cm.historySize().undo, 1);
+ eq(cm.historySize().redo, 0);
+ cm.setValue("1\n\n\n2");
+ cm.clearHistory();
+ eq(cm.historySize().undo, 0);
+ for (var i = 0; i < 20; ++i) {
+ cm.replaceRange("a", Pos(0, 0));
+ cm.replaceRange("b", Pos(3, 0));
+ }
+ eq(cm.historySize().undo, 40);
+ for (var i = 0; i < 40; ++i)
+ cm.undo();
+ eq(cm.historySize().redo, 40);
+ eq(cm.getValue(), "1\n\n\n2");
+}, {value: "abc"});
+
+testCM("undoDepth", function(cm) {
+ cm.replaceRange("d", Pos(0));
+ cm.replaceRange("e", Pos(0));
+ cm.replaceRange("f", Pos(0));
+ cm.undo(); cm.undo(); cm.undo();
+ eq(cm.getValue(), "abcd");
+}, {value: "abc", undoDepth: 4});
+
+testCM("undoDoesntClearValue", function(cm) {
+ cm.undo();
+ eq(cm.getValue(), "x");
+}, {value: "x"});
+
+testCM("undoMultiLine", function(cm) {
+ cm.operation(function() {
+ cm.replaceRange("x", Pos(0, 0));
+ cm.replaceRange("y", Pos(1, 0));
+ });
+ cm.undo();
+ eq(cm.getValue(), "abc\ndef\nghi");
+ cm.operation(function() {
+ cm.replaceRange("y", Pos(1, 0));
+ cm.replaceRange("x", Pos(0, 0));
+ });
+ cm.undo();
+ eq(cm.getValue(), "abc\ndef\nghi");
+ cm.operation(function() {
+ cm.replaceRange("y", Pos(2, 0));
+ cm.replaceRange("x", Pos(1, 0));
+ cm.replaceRange("z", Pos(2, 0));
+ });
+ cm.undo();
+ eq(cm.getValue(), "abc\ndef\nghi", 3);
+}, {value: "abc\ndef\nghi"});
+
+testCM("undoComposite", function(cm) {
+ cm.replaceRange("y", Pos(1));
+ cm.operation(function() {
+ cm.replaceRange("x", Pos(0));
+ cm.replaceRange("z", Pos(2));
+ });
+ eq(cm.getValue(), "ax\nby\ncz\n");
+ cm.undo();
+ eq(cm.getValue(), "a\nby\nc\n");
+ cm.undo();
+ eq(cm.getValue(), "a\nb\nc\n");
+ cm.redo(); cm.redo();
+ eq(cm.getValue(), "ax\nby\ncz\n");
+}, {value: "a\nb\nc\n"});
+
+testCM("undoSelection", function(cm) {
+ cm.setSelection(Pos(0, 2), Pos(0, 4));
+ cm.replaceSelection("");
+ cm.setCursor(Pos(1, 0));
+ cm.undo();
+ eqPos(cm.getCursor(true), Pos(0, 2));
+ eqPos(cm.getCursor(false), Pos(0, 4));
+ cm.setCursor(Pos(1, 0));
+ cm.redo();
+ eqPos(cm.getCursor(true), Pos(0, 2));
+ eqPos(cm.getCursor(false), Pos(0, 2));
+}, {value: "abcdefgh\n"});
+
+testCM("undoSelectionAsBefore", function(cm) {
+ cm.replaceSelection("abc", "around");
+ cm.undo();
+ cm.redo();
+ eq(cm.getSelection(), "abc");
+});
+
+testCM("selectionChangeConfusesHistory", function(cm) {
+ cm.replaceSelection("abc", null, "dontmerge");
+ cm.operation(function() {
+ cm.setCursor(Pos(0, 0));
+ cm.replaceSelection("abc", null, "dontmerge");
+ });
+ eq(cm.historySize().undo, 2);
+});
+
+testCM("markTextSingleLine", function(cm) {
+ forEach([{a: 0, b: 1, c: "", f: 2, t: 5},
+ {a: 0, b: 4, c: "", f: 0, t: 2},
+ {a: 1, b: 2, c: "x", f: 3, t: 6},
+ {a: 4, b: 5, c: "", f: 3, t: 5},
+ {a: 4, b: 5, c: "xx", f: 3, t: 7},
+ {a: 2, b: 5, c: "", f: 2, t: 3},
+ {a: 2, b: 5, c: "abcd", f: 6, t: 7},
+ {a: 2, b: 6, c: "x", f: null, t: null},
+ {a: 3, b: 6, c: "", f: null, t: null},
+ {a: 0, b: 9, c: "hallo", f: null, t: null},
+ {a: 4, b: 6, c: "x", f: 3, t: 4},
+ {a: 4, b: 8, c: "", f: 3, t: 4},
+ {a: 6, b: 6, c: "a", f: 3, t: 6},
+ {a: 8, b: 9, c: "", f: 3, t: 6}], function(test) {
+ cm.setValue("1234567890");
+ var r = cm.markText(Pos(0, 3), Pos(0, 6), {className: "foo"});
+ cm.replaceRange(test.c, Pos(0, test.a), Pos(0, test.b));
+ var f = r.find();
+ eq(f && f.from.ch, test.f); eq(f && f.to.ch, test.t);
+ });
+});
+
+testCM("markTextMultiLine", function(cm) {
+ function p(v) { return v && Pos(v[0], v[1]); }
+ forEach([{a: [0, 0], b: [0, 5], c: "", f: [0, 0], t: [2, 5]},
+ {a: [0, 0], b: [0, 5], c: "foo\n", f: [1, 0], t: [3, 5]},
+ {a: [0, 1], b: [0, 10], c: "", f: [0, 1], t: [2, 5]},
+ {a: [0, 5], b: [0, 6], c: "x", f: [0, 6], t: [2, 5]},
+ {a: [0, 0], b: [1, 0], c: "", f: [0, 0], t: [1, 5]},
+ {a: [0, 6], b: [2, 4], c: "", f: [0, 5], t: [0, 7]},
+ {a: [0, 6], b: [2, 4], c: "aa", f: [0, 5], t: [0, 9]},
+ {a: [1, 2], b: [1, 8], c: "", f: [0, 5], t: [2, 5]},
+ {a: [0, 5], b: [2, 5], c: "xx", f: null, t: null},
+ {a: [0, 0], b: [2, 10], c: "x", f: null, t: null},
+ {a: [1, 5], b: [2, 5], c: "", f: [0, 5], t: [1, 5]},
+ {a: [2, 0], b: [2, 3], c: "", f: [0, 5], t: [2, 2]},
+ {a: [2, 5], b: [3, 0], c: "a\nb", f: [0, 5], t: [2, 5]},
+ {a: [2, 3], b: [3, 0], c: "x", f: [0, 5], t: [2, 3]},
+ {a: [1, 1], b: [1, 9], c: "1\n2\n3", f: [0, 5], t: [4, 5]}], function(test) {
+ cm.setValue("aaaaaaaaaa\nbbbbbbbbbb\ncccccccccc\ndddddddd\n");
+ var r = cm.markText(Pos(0, 5), Pos(2, 5),
+ {className: "CodeMirror-matchingbracket"});
+ cm.replaceRange(test.c, p(test.a), p(test.b));
+ var f = r.find();
+ eqPos(f && f.from, p(test.f)); eqPos(f && f.to, p(test.t));
+ });
+});
+
+testCM("markTextUndo", function(cm) {
+ var marker1, marker2, bookmark;
+ marker1 = cm.markText(Pos(0, 1), Pos(0, 3),
+ {className: "CodeMirror-matchingbracket"});
+ marker2 = cm.markText(Pos(0, 0), Pos(2, 1),
+ {className: "CodeMirror-matchingbracket"});
+ bookmark = cm.setBookmark(Pos(1, 5));
+ cm.operation(function(){
+ cm.replaceRange("foo", Pos(0, 2));
+ cm.replaceRange("bar\nbaz\nbug\n", Pos(2, 0), Pos(3, 0));
+ });
+ var v1 = cm.getValue();
+ cm.setValue("");
+ eq(marker1.find(), null); eq(marker2.find(), null); eq(bookmark.find(), null);
+ cm.undo();
+ eqPos(bookmark.find(), Pos(1, 5), "still there");
+ cm.undo();
+ var m1Pos = marker1.find(), m2Pos = marker2.find();
+ eqPos(m1Pos.from, Pos(0, 1)); eqPos(m1Pos.to, Pos(0, 3));
+ eqPos(m2Pos.from, Pos(0, 0)); eqPos(m2Pos.to, Pos(2, 1));
+ eqPos(bookmark.find(), Pos(1, 5));
+ cm.redo(); cm.redo();
+ eq(bookmark.find(), null);
+ cm.undo();
+ eqPos(bookmark.find(), Pos(1, 5));
+ eq(cm.getValue(), v1);
+}, {value: "1234\n56789\n00\n"});
+
+testCM("markTextStayGone", function(cm) {
+ var m1 = cm.markText(Pos(0, 0), Pos(0, 1));
+ cm.replaceRange("hi", Pos(0, 2));
+ m1.clear();
+ cm.undo();
+ eq(m1.find(), null);
+}, {value: "hello"});
+
+testCM("markTextAllowEmpty", function(cm) {
+ var m1 = cm.markText(Pos(0, 1), Pos(0, 2), {clearWhenEmpty: false});
+ is(m1.find());
+ cm.replaceRange("x", Pos(0, 0));
+ is(m1.find());
+ cm.replaceRange("y", Pos(0, 2));
+ is(m1.find());
+ cm.replaceRange("z", Pos(0, 3), Pos(0, 4));
+ is(!m1.find());
+ var m2 = cm.markText(Pos(0, 1), Pos(0, 2), {clearWhenEmpty: false,
+ inclusiveLeft: true,
+ inclusiveRight: true});
+ cm.replaceRange("q", Pos(0, 1), Pos(0, 2));
+ is(m2.find());
+ cm.replaceRange("", Pos(0, 0), Pos(0, 3));
+ is(!m2.find());
+ var m3 = cm.markText(Pos(0, 1), Pos(0, 1), {clearWhenEmpty: false});
+ cm.replaceRange("a", Pos(0, 3));
+ is(m3.find());
+ cm.replaceRange("b", Pos(0, 1));
+ is(!m3.find());
+}, {value: "abcde"});
+
+testCM("markTextStacked", function(cm) {
+ var m1 = cm.markText(Pos(0, 0), Pos(0, 0), {clearWhenEmpty: false});
+ var m2 = cm.markText(Pos(0, 0), Pos(0, 0), {clearWhenEmpty: false});
+ cm.replaceRange("B", Pos(0, 1));
+ is(m1.find() && m2.find());
+}, {value: "A"});
+
+testCM("undoPreservesNewMarks", function(cm) {
+ cm.markText(Pos(0, 3), Pos(0, 4));
+ cm.markText(Pos(1, 1), Pos(1, 3));
+ cm.replaceRange("", Pos(0, 3), Pos(3, 1));
+ var mBefore = cm.markText(Pos(0, 0), Pos(0, 1));
+ var mAfter = cm.markText(Pos(0, 5), Pos(0, 6));
+ var mAround = cm.markText(Pos(0, 2), Pos(0, 4));
+ cm.undo();
+ eqPos(mBefore.find().from, Pos(0, 0));
+ eqPos(mBefore.find().to, Pos(0, 1));
+ eqPos(mAfter.find().from, Pos(3, 3));
+ eqPos(mAfter.find().to, Pos(3, 4));
+ eqPos(mAround.find().from, Pos(0, 2));
+ eqPos(mAround.find().to, Pos(3, 2));
+ var found = cm.findMarksAt(Pos(2, 2));
+ eq(found.length, 1);
+ eq(found[0], mAround);
+}, {value: "aaaa\nbbbb\ncccc\ndddd"});
+
+testCM("markClearBetween", function(cm) {
+ cm.setValue("aaa\nbbb\nccc\nddd\n");
+ cm.markText(Pos(0, 0), Pos(2));
+ cm.replaceRange("aaa\nbbb\nccc", Pos(0, 0), Pos(2));
+ eq(cm.findMarksAt(Pos(1, 1)).length, 0);
+});
+
+testCM("findMarksMiddle", function(cm) {
+ var mark = cm.markText(Pos(1, 1), Pos(3, 1));
+ var found = cm.findMarks(Pos(2, 1), Pos(2, 2));
+ eq(found.length, 1);
+ eq(found[0], mark);
+}, {value: "line 0\nline 1\nline 2\nline 3"});
+
+testCM("deleteSpanCollapsedInclusiveLeft", function(cm) {
+ var from = Pos(1, 0), to = Pos(1, 1);
+ var m = cm.markText(from, to, {collapsed: true, inclusiveLeft: true});
+ // Delete collapsed span.
+ cm.replaceRange("", from, to);
+}, {value: "abc\nX\ndef"});
+
+testCM("markTextCSS", function(cm) {
+ function present() {
+ var spans = cm.display.lineDiv.getElementsByTagName("span");
+ for (var i = 0; i < spans.length; i++)
+ if (spans[i].style.color == "cyan" && span[i].textContent == "cdefg") return true;
+ }
+ var m = cm.markText(Pos(0, 2), Pos(0, 6), {css: "color: cyan"});
+ m.clear();
+ is(!present());
+}, {value: "abcdefgh"});
+
+testCM("bookmark", function(cm) {
+ function p(v) { return v && Pos(v[0], v[1]); }
+ forEach([{a: [1, 0], b: [1, 1], c: "", d: [1, 4]},
+ {a: [1, 1], b: [1, 1], c: "xx", d: [1, 7]},
+ {a: [1, 4], b: [1, 5], c: "ab", d: [1, 6]},
+ {a: [1, 4], b: [1, 6], c: "", d: null},
+ {a: [1, 5], b: [1, 6], c: "abc", d: [1, 5]},
+ {a: [1, 6], b: [1, 8], c: "", d: [1, 5]},
+ {a: [1, 4], b: [1, 4], c: "\n\n", d: [3, 1]},
+ {bm: [1, 9], a: [1, 1], b: [1, 1], c: "\n", d: [2, 8]}], function(test) {
+ cm.setValue("1234567890\n1234567890\n1234567890");
+ var b = cm.setBookmark(p(test.bm) || Pos(1, 5));
+ cm.replaceRange(test.c, p(test.a), p(test.b));
+ eqPos(b.find(), p(test.d));
+ });
+});
+
+testCM("bookmarkInsertLeft", function(cm) {
+ var br = cm.setBookmark(Pos(0, 2), {insertLeft: false});
+ var bl = cm.setBookmark(Pos(0, 2), {insertLeft: true});
+ cm.setCursor(Pos(0, 2));
+ cm.replaceSelection("hi");
+ eqPos(br.find(), Pos(0, 2));
+ eqPos(bl.find(), Pos(0, 4));
+ cm.replaceRange("", Pos(0, 4), Pos(0, 5));
+ cm.replaceRange("", Pos(0, 2), Pos(0, 4));
+ cm.replaceRange("", Pos(0, 1), Pos(0, 2));
+ // Verify that deleting next to bookmarks doesn't kill them
+ eqPos(br.find(), Pos(0, 1));
+ eqPos(bl.find(), Pos(0, 1));
+}, {value: "abcdef"});
+
+testCM("bookmarkCursor", function(cm) {
+ var pos01 = cm.cursorCoords(Pos(0, 1)), pos11 = cm.cursorCoords(Pos(1, 1)),
+ pos20 = cm.cursorCoords(Pos(2, 0)), pos30 = cm.cursorCoords(Pos(3, 0)),
+ pos41 = cm.cursorCoords(Pos(4, 1));
+ cm.setBookmark(Pos(0, 1), {widget: document.createTextNode("â†"), insertLeft: true});
+ cm.setBookmark(Pos(2, 0), {widget: document.createTextNode("â†"), insertLeft: true});
+ cm.setBookmark(Pos(1, 1), {widget: document.createTextNode("→")});
+ cm.setBookmark(Pos(3, 0), {widget: document.createTextNode("→")});
+ var new01 = cm.cursorCoords(Pos(0, 1)), new11 = cm.cursorCoords(Pos(1, 1)),
+ new20 = cm.cursorCoords(Pos(2, 0)), new30 = cm.cursorCoords(Pos(3, 0));
+ near(new01.left, pos01.left, 1);
+ near(new01.top, pos01.top, 1);
+ is(new11.left > pos11.left, "at right, middle of line");
+ near(new11.top == pos11.top, 1);
+ near(new20.left, pos20.left, 1);
+ near(new20.top, pos20.top, 1);
+ is(new30.left > pos30.left, "at right, empty line");
+ near(new30.top, pos30, 1);
+ cm.setBookmark(Pos(4, 0), {widget: document.createTextNode("→")});
+ is(cm.cursorCoords(Pos(4, 1)).left > pos41.left, "single-char bug");
+}, {value: "foo\nbar\n\n\nx\ny"});
+
+testCM("multiBookmarkCursor", function(cm) {
+ if (phantom) return;
+ var ms = [], m;
+ function add(insertLeft) {
+ for (var i = 0; i < 3; ++i) {
+ var node = document.createElement("span");
+ node.innerHTML = "X";
+ ms.push(cm.setBookmark(Pos(0, 1), {widget: node, insertLeft: insertLeft}));
+ }
+ }
+ var base1 = cm.cursorCoords(Pos(0, 1)).left, base4 = cm.cursorCoords(Pos(0, 4)).left;
+ add(true);
+ near(base1, cm.cursorCoords(Pos(0, 1)).left, 1);
+ while (m = ms.pop()) m.clear();
+ add(false);
+ near(base4, cm.cursorCoords(Pos(0, 1)).left, 1);
+}, {value: "abcdefg"});
+
+testCM("getAllMarks", function(cm) {
+ addDoc(cm, 10, 10);
+ var m1 = cm.setBookmark(Pos(0, 2));
+ var m2 = cm.markText(Pos(0, 2), Pos(3, 2));
+ var m3 = cm.markText(Pos(1, 2), Pos(1, 8));
+ var m4 = cm.markText(Pos(8, 0), Pos(9, 0));
+ eq(cm.getAllMarks().length, 4);
+ m1.clear();
+ m3.clear();
+ eq(cm.getAllMarks().length, 2);
+});
+
+testCM("setValueClears", function(cm) {
+ cm.addLineClass(0, "wrap", "foo");
+ var mark = cm.markText(Pos(0, 0), Pos(1, 1), {inclusiveLeft: true, inclusiveRight: true});
+ cm.setValue("foo");
+ is(!cm.lineInfo(0).wrapClass);
+ is(!mark.find());
+}, {value: "a\nb"});
+
+testCM("bug577", function(cm) {
+ cm.setValue("a\nb");
+ cm.clearHistory();
+ cm.setValue("fooooo");
+ cm.undo();
+});
+
+testCM("scrollSnap", function(cm) {
+ cm.setSize(100, 100);
+ addDoc(cm, 200, 200);
+ cm.setCursor(Pos(100, 180));
+ var info = cm.getScrollInfo();
+ is(info.left > 0 && info.top > 0);
+ cm.setCursor(Pos(0, 0));
+ info = cm.getScrollInfo();
+ is(info.left == 0 && info.top == 0, "scrolled clean to top");
+ cm.setCursor(Pos(100, 180));
+ cm.setCursor(Pos(199, 0));
+ info = cm.getScrollInfo();
+ is(info.left == 0 && info.top + 2 > info.height - cm.getScrollerElement().clientHeight, "scrolled clean to bottom");
+});
+
+testCM("scrollIntoView", function(cm) {
+ if (phantom) return;
+ var outer = cm.getWrapperElement().getBoundingClientRect();
+ function test(line, ch, msg) {
+ var pos = Pos(line, ch);
+ cm.scrollIntoView(pos);
+ var box = cm.charCoords(pos, "window");
+ is(box.left >= outer.left, msg + " (left)");
+ is(box.right <= outer.right, msg + " (right)");
+ is(box.top >= outer.top, msg + " (top)");
+ is(box.bottom <= outer.bottom, msg + " (bottom)");
+ }
+ addDoc(cm, 200, 200);
+ test(199, 199, "bottom right");
+ test(0, 0, "top left");
+ test(100, 100, "center");
+ test(199, 0, "bottom left");
+ test(0, 199, "top right");
+ test(100, 100, "center again");
+});
+
+testCM("scrollBackAndForth", function(cm) {
+ addDoc(cm, 1, 200);
+ cm.operation(function() {
+ cm.scrollIntoView(Pos(199, 0));
+ cm.scrollIntoView(Pos(4, 0));
+ });
+ is(cm.getScrollInfo().top > 0);
+});
+
+testCM("selectAllNoScroll", function(cm) {
+ addDoc(cm, 1, 200);
+ cm.execCommand("selectAll");
+ eq(cm.getScrollInfo().top, 0);
+ cm.setCursor(199);
+ cm.execCommand("selectAll");
+ is(cm.getScrollInfo().top > 0);
+});
+
+testCM("selectionPos", function(cm) {
+ if (phantom || cm.getOption("inputStyle") != "textarea") return;
+ cm.setSize(100, 100);
+ addDoc(cm, 200, 100);
+ cm.setSelection(Pos(1, 100), Pos(98, 100));
+ var lineWidth = cm.charCoords(Pos(0, 200), "local").left;
+ var lineHeight = (cm.charCoords(Pos(99)).top - cm.charCoords(Pos(0)).top) / 100;
+ cm.scrollTo(0, 0);
+ var selElt = byClassName(cm.getWrapperElement(), "CodeMirror-selected");
+ var outer = cm.getWrapperElement().getBoundingClientRect();
+ var sawMiddle, sawTop, sawBottom;
+ for (var i = 0, e = selElt.length; i < e; ++i) {
+ var box = selElt[i].getBoundingClientRect();
+ var atLeft = box.left - outer.left < 30;
+ var width = box.right - box.left;
+ var atRight = box.right - outer.left > .8 * lineWidth;
+ if (atLeft && atRight) {
+ sawMiddle = true;
+ is(box.bottom - box.top > 90 * lineHeight, "middle high");
+ is(width > .9 * lineWidth, "middle wide");
+ } else {
+ is(width > .4 * lineWidth, "top/bot wide enough");
+ is(width < .6 * lineWidth, "top/bot slim enough");
+ if (atLeft) {
+ sawBottom = true;
+ is(box.top - outer.top > 96 * lineHeight, "bot below");
+ } else if (atRight) {
+ sawTop = true;
+ is(box.top - outer.top < 2.1 * lineHeight, "top above");
+ }
+ }
+ }
+ is(sawTop && sawBottom && sawMiddle, "all parts");
+}, null);
+
+testCM("restoreHistory", function(cm) {
+ cm.setValue("abc\ndef");
+ cm.replaceRange("hello", Pos(1, 0), Pos(1));
+ cm.replaceRange("goop", Pos(0, 0), Pos(0));
+ cm.undo();
+ var storedVal = cm.getValue(), storedHist = cm.getHistory();
+ if (window.JSON) storedHist = JSON.parse(JSON.stringify(storedHist));
+ eq(storedVal, "abc\nhello");
+ cm.setValue("");
+ cm.clearHistory();
+ eq(cm.historySize().undo, 0);
+ cm.setValue(storedVal);
+ cm.setHistory(storedHist);
+ cm.redo();
+ eq(cm.getValue(), "goop\nhello");
+ cm.undo(); cm.undo();
+ eq(cm.getValue(), "abc\ndef");
+});
+
+testCM("doubleScrollbar", function(cm) {
+ var dummy = document.body.appendChild(document.createElement("p"));
+ dummy.style.cssText = "height: 50px; overflow: scroll; width: 50px";
+ var scrollbarWidth = dummy.offsetWidth + 1 - dummy.clientWidth;
+ document.body.removeChild(dummy);
+ if (scrollbarWidth < 2) return;
+ cm.setSize(null, 100);
+ addDoc(cm, 1, 300);
+ var wrap = cm.getWrapperElement();
+ is(wrap.offsetWidth - byClassName(wrap, "CodeMirror-lines")[0].offsetWidth <= scrollbarWidth * 1.5);
+});
+
+testCM("weirdLinebreaks", function(cm) {
+ cm.setValue("foo\nbar\rbaz\r\nquux\n\rplop");
+ is(cm.getValue(), "foo\nbar\nbaz\nquux\n\nplop");
+ is(cm.lineCount(), 6);
+ cm.setValue("\n\n");
+ is(cm.lineCount(), 3);
+});
+
+testCM("setSize", function(cm) {
+ cm.setSize(100, 100);
+ var wrap = cm.getWrapperElement();
+ is(wrap.offsetWidth, 100);
+ is(wrap.offsetHeight, 100);
+ cm.setSize("100%", "3em");
+ is(wrap.style.width, "100%");
+ is(wrap.style.height, "3em");
+ cm.setSize(null, 40);
+ is(wrap.style.width, "100%");
+ is(wrap.style.height, "40px");
+});
+
+function foldLines(cm, start, end, autoClear) {
+ return cm.markText(Pos(start, 0), Pos(end - 1), {
+ inclusiveLeft: true,
+ inclusiveRight: true,
+ collapsed: true,
+ clearOnEnter: autoClear
+ });
+}
+
+testCM("collapsedLines", function(cm) {
+ addDoc(cm, 4, 10);
+ var range = foldLines(cm, 4, 5), cleared = 0;
+ CodeMirror.on(range, "clear", function() {cleared++;});
+ cm.setCursor(Pos(3, 0));
+ CodeMirror.commands.goLineDown(cm);
+ eqPos(cm.getCursor(), Pos(5, 0));
+ cm.replaceRange("abcdefg", Pos(3, 0), Pos(3));
+ cm.setCursor(Pos(3, 6));
+ CodeMirror.commands.goLineDown(cm);
+ eqPos(cm.getCursor(), Pos(5, 4));
+ cm.replaceRange("ab", Pos(3, 0), Pos(3));
+ cm.setCursor(Pos(3, 2));
+ CodeMirror.commands.goLineDown(cm);
+ eqPos(cm.getCursor(), Pos(5, 2));
+ cm.operation(function() {range.clear(); range.clear();});
+ eq(cleared, 1);
+});
+
+testCM("collapsedRangeCoordsChar", function(cm) {
+ var pos_1_3 = cm.charCoords(Pos(1, 3));
+ pos_1_3.left += 2; pos_1_3.top += 2;
+ var opts = {collapsed: true, inclusiveLeft: true, inclusiveRight: true};
+ var m1 = cm.markText(Pos(0, 0), Pos(2, 0), opts);
+ eqPos(cm.coordsChar(pos_1_3), Pos(3, 3));
+ m1.clear();
+ var m1 = cm.markText(Pos(0, 0), Pos(1, 1), {collapsed: true, inclusiveLeft: true});
+ var m2 = cm.markText(Pos(1, 1), Pos(2, 0), {collapsed: true, inclusiveRight: true});
+ eqPos(cm.coordsChar(pos_1_3), Pos(3, 3));
+ m1.clear(); m2.clear();
+ var m1 = cm.markText(Pos(0, 0), Pos(1, 6), opts);
+ eqPos(cm.coordsChar(pos_1_3), Pos(3, 3));
+}, {value: "123456\nabcdef\nghijkl\nmnopqr\n"});
+
+testCM("collapsedRangeBetweenLinesSelected", function(cm) {
+ if (cm.getOption("inputStyle") != "textarea") return;
+ var widget = document.createElement("span");
+ widget.textContent = "\u2194";
+ cm.markText(Pos(0, 3), Pos(1, 0), {replacedWith: widget});
+ cm.setSelection(Pos(0, 3), Pos(1, 0));
+ var selElts = byClassName(cm.getWrapperElement(), "CodeMirror-selected");
+ for (var i = 0, w = 0; i < selElts.length; i++)
+ w += selElts[i].offsetWidth;
+ is(w > 0);
+}, {value: "one\ntwo"});
+
+testCM("randomCollapsedRanges", function(cm) {
+ addDoc(cm, 20, 500);
+ cm.operation(function() {
+ for (var i = 0; i < 200; i++) {
+ var start = Pos(Math.floor(Math.random() * 500), Math.floor(Math.random() * 20));
+ if (i % 4)
+ try { cm.markText(start, Pos(start.line + 2, 1), {collapsed: true}); }
+ catch(e) { if (!/overlapping/.test(String(e))) throw e; }
+ else
+ cm.markText(start, Pos(start.line, start.ch + 4), {"className": "foo"});
+ }
+ });
+});
+
+testCM("hiddenLinesAutoUnfold", function(cm) {
+ var range = foldLines(cm, 1, 3, true), cleared = 0;
+ CodeMirror.on(range, "clear", function() {cleared++;});
+ cm.setCursor(Pos(3, 0));
+ eq(cleared, 0);
+ cm.execCommand("goCharLeft");
+ eq(cleared, 1);
+ range = foldLines(cm, 1, 3, true);
+ CodeMirror.on(range, "clear", function() {cleared++;});
+ eqPos(cm.getCursor(), Pos(3, 0));
+ cm.setCursor(Pos(0, 3));
+ cm.execCommand("goCharRight");
+ eq(cleared, 2);
+}, {value: "abc\ndef\nghi\njkl"});
+
+testCM("hiddenLinesSelectAll", function(cm) { // Issue #484
+ addDoc(cm, 4, 20);
+ foldLines(cm, 0, 10);
+ foldLines(cm, 11, 20);
+ CodeMirror.commands.selectAll(cm);
+ eqPos(cm.getCursor(true), Pos(10, 0));
+ eqPos(cm.getCursor(false), Pos(10, 4));
+});
+
+
+testCM("everythingFolded", function(cm) {
+ addDoc(cm, 2, 2);
+ function enterPress() {
+ cm.triggerOnKeyDown({type: "keydown", keyCode: 13, preventDefault: function(){}, stopPropagation: function(){}});
+ }
+ var fold = foldLines(cm, 0, 2);
+ enterPress();
+ eq(cm.getValue(), "xx\nxx");
+ fold.clear();
+ fold = foldLines(cm, 0, 2, true);
+ eq(fold.find(), null);
+ enterPress();
+ eq(cm.getValue(), "\nxx\nxx");
+});
+
+testCM("structuredFold", function(cm) {
+ if (phantom) return;
+ addDoc(cm, 4, 8);
+ var range = cm.markText(Pos(1, 2), Pos(6, 2), {
+ replacedWith: document.createTextNode("Q")
+ });
+ cm.setCursor(0, 3);
+ CodeMirror.commands.goLineDown(cm);
+ eqPos(cm.getCursor(), Pos(6, 2));
+ CodeMirror.commands.goCharLeft(cm);
+ eqPos(cm.getCursor(), Pos(1, 2));
+ CodeMirror.commands.delCharAfter(cm);
+ eq(cm.getValue(), "xxxx\nxxxx\nxxxx");
+ addDoc(cm, 4, 8);
+ range = cm.markText(Pos(1, 2), Pos(6, 2), {
+ replacedWith: document.createTextNode("M"),
+ clearOnEnter: true
+ });
+ var cleared = 0;
+ CodeMirror.on(range, "clear", function(){++cleared;});
+ cm.setCursor(0, 3);
+ CodeMirror.commands.goLineDown(cm);
+ eqPos(cm.getCursor(), Pos(6, 2));
+ CodeMirror.commands.goCharLeft(cm);
+ eqPos(cm.getCursor(), Pos(6, 1));
+ eq(cleared, 1);
+ range.clear();
+ eq(cleared, 1);
+ range = cm.markText(Pos(1, 2), Pos(6, 2), {
+ replacedWith: document.createTextNode("Q"),
+ clearOnEnter: true
+ });
+ range.clear();
+ cm.setCursor(1, 2);
+ CodeMirror.commands.goCharRight(cm);
+ eqPos(cm.getCursor(), Pos(1, 3));
+ range = cm.markText(Pos(2, 0), Pos(4, 4), {
+ replacedWith: document.createTextNode("M")
+ });
+ cm.setCursor(1, 0);
+ CodeMirror.commands.goLineDown(cm);
+ eqPos(cm.getCursor(), Pos(2, 0));
+}, null);
+
+testCM("nestedFold", function(cm) {
+ addDoc(cm, 10, 3);
+ function fold(ll, cl, lr, cr) {
+ return cm.markText(Pos(ll, cl), Pos(lr, cr), {collapsed: true});
+ }
+ var inner1 = fold(0, 6, 1, 3), inner2 = fold(0, 2, 1, 8), outer = fold(0, 1, 2, 3), inner0 = fold(0, 5, 0, 6);
+ cm.setCursor(0, 1);
+ CodeMirror.commands.goCharRight(cm);
+ eqPos(cm.getCursor(), Pos(2, 3));
+ inner0.clear();
+ CodeMirror.commands.goCharLeft(cm);
+ eqPos(cm.getCursor(), Pos(0, 1));
+ outer.clear();
+ CodeMirror.commands.goCharRight(cm);
+ eqPos(cm.getCursor(), Pos(0, 2));
+ CodeMirror.commands.goCharRight(cm);
+ eqPos(cm.getCursor(), Pos(1, 8));
+ inner2.clear();
+ CodeMirror.commands.goCharLeft(cm);
+ eqPos(cm.getCursor(), Pos(1, 7));
+ cm.setCursor(0, 5);
+ CodeMirror.commands.goCharRight(cm);
+ eqPos(cm.getCursor(), Pos(0, 6));
+ CodeMirror.commands.goCharRight(cm);
+ eqPos(cm.getCursor(), Pos(1, 3));
+});
+
+testCM("badNestedFold", function(cm) {
+ addDoc(cm, 4, 4);
+ cm.markText(Pos(0, 2), Pos(3, 2), {collapsed: true});
+ var caught;
+ try {cm.markText(Pos(0, 1), Pos(0, 3), {collapsed: true});}
+ catch(e) {caught = e;}
+ is(caught instanceof Error, "no error");
+ is(/overlap/i.test(caught.message), "wrong error");
+});
+
+testCM("nestedFoldOnSide", function(cm) {
+ var m1 = cm.markText(Pos(0, 1), Pos(2, 1), {collapsed: true, inclusiveRight: true});
+ var m2 = cm.markText(Pos(0, 1), Pos(0, 2), {collapsed: true});
+ cm.markText(Pos(0, 1), Pos(0, 2), {collapsed: true}).clear();
+ try { cm.markText(Pos(0, 1), Pos(0, 2), {collapsed: true, inclusiveLeft: true}); }
+ catch(e) { var caught = e; }
+ is(caught && /overlap/i.test(caught.message));
+ var m3 = cm.markText(Pos(2, 0), Pos(2, 1), {collapsed: true});
+ var m4 = cm.markText(Pos(2, 0), Pos(2, 1), {collapse: true, inclusiveRight: true});
+ m1.clear(); m4.clear();
+ m1 = cm.markText(Pos(0, 1), Pos(2, 1), {collapsed: true});
+ cm.markText(Pos(2, 0), Pos(2, 1), {collapsed: true}).clear();
+ try { cm.markText(Pos(2, 0), Pos(2, 1), {collapsed: true, inclusiveRight: true}); }
+ catch(e) { var caught = e; }
+ is(caught && /overlap/i.test(caught.message));
+}, {value: "ab\ncd\ef"});
+
+testCM("editInFold", function(cm) {
+ addDoc(cm, 4, 6);
+ var m = cm.markText(Pos(1, 2), Pos(3, 2), {collapsed: true});
+ cm.replaceRange("", Pos(0, 0), Pos(1, 3));
+ cm.replaceRange("", Pos(2, 1), Pos(3, 3));
+ cm.replaceRange("a\nb\nc\nd", Pos(0, 1), Pos(1, 0));
+ cm.cursorCoords(Pos(0, 0));
+});
+
+testCM("wrappingInlineWidget", function(cm) {
+ cm.setSize("11em");
+ var w = document.createElement("span");
+ w.style.color = "red";
+ w.innerHTML = "one two three four";
+ cm.markText(Pos(0, 6), Pos(0, 9), {replacedWith: w});
+ var cur0 = cm.cursorCoords(Pos(0, 0)), cur1 = cm.cursorCoords(Pos(0, 10));
+ is(cur0.top < cur1.top);
+ is(cur0.bottom < cur1.bottom);
+ var curL = cm.cursorCoords(Pos(0, 6)), curR = cm.cursorCoords(Pos(0, 9));
+ eq(curL.top, cur0.top);
+ eq(curL.bottom, cur0.bottom);
+ eq(curR.top, cur1.top);
+ eq(curR.bottom, cur1.bottom);
+ cm.replaceRange("", Pos(0, 9), Pos(0));
+ curR = cm.cursorCoords(Pos(0, 9));
+ if (phantom) return;
+ eq(curR.top, cur1.top);
+ eq(curR.bottom, cur1.bottom);
+}, {value: "1 2 3 xxx 4", lineWrapping: true});
+
+testCM("showEmptyWidgetSpan", function(cm) {
+ var marker = cm.markText(Pos(0, 2), Pos(0, 2), {
+ clearWhenEmpty: false,
+ replacedWith: document.createTextNode("X")
+ });
+ eq(cm.display.view[0].text.textContent, "abXc");
+}, {value: "abc"});
+
+testCM("changedInlineWidget", function(cm) {
+ cm.setSize("10em");
+ var w = document.createElement("span");
+ w.innerHTML = "x";
+ var m = cm.markText(Pos(0, 4), Pos(0, 5), {replacedWith: w});
+ w.innerHTML = "and now the widget is really really long all of a sudden and a scrollbar is needed";
+ m.changed();
+ var hScroll = byClassName(cm.getWrapperElement(), "CodeMirror-hscrollbar")[0];
+ is(hScroll.scrollWidth > hScroll.clientWidth);
+}, {value: "hello there"});
+
+testCM("changedBookmark", function(cm) {
+ cm.setSize("10em");
+ var w = document.createElement("span");
+ w.innerHTML = "x";
+ var m = cm.setBookmark(Pos(0, 4), {widget: w});
+ w.innerHTML = "and now the widget is really really long all of a sudden and a scrollbar is needed";
+ m.changed();
+ var hScroll = byClassName(cm.getWrapperElement(), "CodeMirror-hscrollbar")[0];
+ is(hScroll.scrollWidth > hScroll.clientWidth);
+}, {value: "abcdefg"});
+
+testCM("inlineWidget", function(cm) {
+ var w = cm.setBookmark(Pos(0, 2), {widget: document.createTextNode("uu")});
+ cm.setCursor(0, 2);
+ CodeMirror.commands.goLineDown(cm);
+ eqPos(cm.getCursor(), Pos(1, 4));
+ cm.setCursor(0, 2);
+ cm.replaceSelection("hi");
+ eqPos(w.find(), Pos(0, 2));
+ cm.setCursor(0, 1);
+ cm.replaceSelection("ay");
+ eqPos(w.find(), Pos(0, 4));
+ eq(cm.getLine(0), "uayuhiuu");
+}, {value: "uuuu\nuuuuuu"});
+
+testCM("wrappingAndResizing", function(cm) {
+ cm.setSize(null, "auto");
+ cm.setOption("lineWrapping", true);
+ var wrap = cm.getWrapperElement(), h0 = wrap.offsetHeight;
+ var doc = "xxx xxx xxx xxx xxx";
+ cm.setValue(doc);
+ for (var step = 10, w = cm.charCoords(Pos(0, 18), "div").right;; w += step) {
+ cm.setSize(w);
+ if (wrap.offsetHeight <= h0 * (opera_lt10 ? 1.2 : 1.5)) {
+ if (step == 10) { w -= 10; step = 1; }
+ else break;
+ }
+ }
+ // Ensure that putting the cursor at the end of the maximally long
+ // line doesn't cause wrapping to happen.
+ cm.setCursor(Pos(0, doc.length));
+ eq(wrap.offsetHeight, h0);
+ cm.replaceSelection("x");
+ is(wrap.offsetHeight > h0, "wrapping happens");
+ // Now add a max-height and, in a document consisting of
+ // almost-wrapped lines, go over it so that a scrollbar appears.
+ cm.setValue(doc + "\n" + doc + "\n");
+ cm.getScrollerElement().style.maxHeight = "100px";
+ cm.replaceRange("\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n!\n", Pos(2, 0));
+ forEach([Pos(0, doc.length), Pos(0, doc.length - 1),
+ Pos(0, 0), Pos(1, doc.length), Pos(1, doc.length - 1)],
+ function(pos) {
+ var coords = cm.charCoords(pos);
+ eqPos(pos, cm.coordsChar({left: coords.left + 2, top: coords.top + 5}));
+ });
+}, null, ie_lt8);
+
+testCM("measureEndOfLine", function(cm) {
+ cm.setSize(null, "auto");
+ var inner = byClassName(cm.getWrapperElement(), "CodeMirror-lines")[0].firstChild;
+ var lh = inner.offsetHeight;
+ for (var step = 10, w = cm.charCoords(Pos(0, 7), "div").right;; w += step) {
+ cm.setSize(w);
+ if (inner.offsetHeight < 2.5 * lh) {
+ if (step == 10) { w -= 10; step = 1; }
+ else break;
+ }
+ }
+ cm.setValue(cm.getValue() + "\n\n");
+ var endPos = cm.charCoords(Pos(0, 18), "local");
+ is(endPos.top > lh * .8, "not at top");
+ is(endPos.left > w - 20, "not at right");
+ endPos = cm.charCoords(Pos(0, 18));
+ eqPos(cm.coordsChar({left: endPos.left, top: endPos.top + 5}), Pos(0, 18));
+}, {mode: "text/html", value: "<!-- foo barrr -->", lineWrapping: true}, ie_lt8 || opera_lt10);
+
+testCM("scrollVerticallyAndHorizontally", function(cm) {
+ if (cm.getOption("inputStyle") != "textarea") return;
+ cm.setSize(100, 100);
+ addDoc(cm, 40, 40);
+ cm.setCursor(39);
+ var wrap = cm.getWrapperElement(), bar = byClassName(wrap, "CodeMirror-vscrollbar")[0];
+ is(bar.offsetHeight < wrap.offsetHeight, "vertical scrollbar limited by horizontal one");
+ var cursorBox = byClassName(wrap, "CodeMirror-cursor")[0].getBoundingClientRect();
+ var editorBox = wrap.getBoundingClientRect();
+ is(cursorBox.bottom < editorBox.top + cm.getScrollerElement().clientHeight,
+ "bottom line visible");
+}, {lineNumbers: true});
+
+testCM("moveVstuck", function(cm) {
+ var lines = byClassName(cm.getWrapperElement(), "CodeMirror-lines")[0].firstChild, h0 = lines.offsetHeight;
+ var val = "fooooooooooooooooooooooooo baaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaar\n";
+ cm.setValue(val);
+ for (var w = cm.charCoords(Pos(0, 26), "div").right * 2.8;; w += 5) {
+ cm.setSize(w);
+ if (lines.offsetHeight <= 3.5 * h0) break;
+ }
+ cm.setCursor(Pos(0, val.length - 1));
+ cm.moveV(-1, "line");
+ eqPos(cm.getCursor(), Pos(0, 26));
+}, {lineWrapping: true}, ie_lt8 || opera_lt10);
+
+testCM("collapseOnMove", function(cm) {
+ cm.setSelection(Pos(0, 1), Pos(2, 4));
+ cm.execCommand("goLineUp");
+ is(!cm.somethingSelected());
+ eqPos(cm.getCursor(), Pos(0, 1));
+ cm.setSelection(Pos(0, 1), Pos(2, 4));
+ cm.execCommand("goPageDown");
+ is(!cm.somethingSelected());
+ eqPos(cm.getCursor(), Pos(2, 4));
+ cm.execCommand("goLineUp");
+ cm.execCommand("goLineUp");
+ eqPos(cm.getCursor(), Pos(0, 4));
+ cm.setSelection(Pos(0, 1), Pos(2, 4));
+ cm.execCommand("goCharLeft");
+ is(!cm.somethingSelected());
+ eqPos(cm.getCursor(), Pos(0, 1));
+}, {value: "aaaaa\nb\nccccc"});
+
+testCM("clickTab", function(cm) {
+ var p0 = cm.charCoords(Pos(0, 0));
+ eqPos(cm.coordsChar({left: p0.left + 5, top: p0.top + 5}), Pos(0, 0));
+ eqPos(cm.coordsChar({left: p0.right - 5, top: p0.top + 5}), Pos(0, 1));
+}, {value: "\t\n\n", lineWrapping: true, tabSize: 8});
+
+testCM("verticalScroll", function(cm) {
+ cm.setSize(100, 200);
+ cm.setValue("foo\nbar\nbaz\n");
+ var sc = cm.getScrollerElement(), baseWidth = sc.scrollWidth;
+ cm.replaceRange("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaah", Pos(0, 0), Pos(0));
+ is(sc.scrollWidth > baseWidth, "scrollbar present");
+ cm.replaceRange("foo", Pos(0, 0), Pos(0));
+ if (!phantom) eq(sc.scrollWidth, baseWidth, "scrollbar gone");
+ cm.replaceRange("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaah", Pos(0, 0), Pos(0));
+ cm.replaceRange("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbh", Pos(1, 0), Pos(1));
+ is(sc.scrollWidth > baseWidth, "present again");
+ var curWidth = sc.scrollWidth;
+ cm.replaceRange("foo", Pos(0, 0), Pos(0));
+ is(sc.scrollWidth < curWidth, "scrollbar smaller");
+ is(sc.scrollWidth > baseWidth, "but still present");
+});
+
+testCM("extraKeys", function(cm) {
+ var outcome;
+ function fakeKey(expected, code, props) {
+ if (typeof code == "string") code = code.charCodeAt(0);
+ var e = {type: "keydown", keyCode: code, preventDefault: function(){}, stopPropagation: function(){}};
+ if (props) for (var n in props) e[n] = props[n];
+ outcome = null;
+ cm.triggerOnKeyDown(e);
+ eq(outcome, expected);
+ }
+ CodeMirror.commands.testCommand = function() {outcome = "tc";};
+ CodeMirror.commands.goTestCommand = function() {outcome = "gtc";};
+ cm.setOption("extraKeys", {"Shift-X": function() {outcome = "sx";},
+ "X": function() {outcome = "x";},
+ "Ctrl-Alt-U": function() {outcome = "cau";},
+ "End": "testCommand",
+ "Home": "goTestCommand",
+ "Tab": false});
+ fakeKey(null, "U");
+ fakeKey("cau", "U", {ctrlKey: true, altKey: true});
+ fakeKey(null, "U", {shiftKey: true, ctrlKey: true, altKey: true});
+ fakeKey("x", "X");
+ fakeKey("sx", "X", {shiftKey: true});
+ fakeKey("tc", 35);
+ fakeKey(null, 35, {shiftKey: true});
+ fakeKey("gtc", 36);
+ fakeKey("gtc", 36, {shiftKey: true});
+ fakeKey(null, 9);
+}, null, window.opera && mac);
+
+testCM("wordMovementCommands", function(cm) {
+ cm.execCommand("goWordLeft");
+ eqPos(cm.getCursor(), Pos(0, 0));
+ cm.execCommand("goWordRight"); cm.execCommand("goWordRight");
+ eqPos(cm.getCursor(), Pos(0, 7));
+ cm.execCommand("goWordLeft");
+ eqPos(cm.getCursor(), Pos(0, 5));
+ cm.execCommand("goWordRight"); cm.execCommand("goWordRight");
+ eqPos(cm.getCursor(), Pos(0, 12));
+ cm.execCommand("goWordLeft");
+ eqPos(cm.getCursor(), Pos(0, 9));
+ cm.execCommand("goWordRight"); cm.execCommand("goWordRight"); cm.execCommand("goWordRight");
+ eqPos(cm.getCursor(), Pos(0, 24));
+ cm.execCommand("goWordRight"); cm.execCommand("goWordRight");
+ eqPos(cm.getCursor(), Pos(1, 9));
+ cm.execCommand("goWordRight");
+ eqPos(cm.getCursor(), Pos(1, 13));
+ cm.execCommand("goWordRight"); cm.execCommand("goWordRight");
+ eqPos(cm.getCursor(), Pos(2, 0));
+}, {value: "this is (the) firstline.\na foo12\u00e9\u00f8\u00d7bar\n"});
+
+testCM("groupMovementCommands", function(cm) {
+ cm.execCommand("goGroupLeft");
+ eqPos(cm.getCursor(), Pos(0, 0));
+ cm.execCommand("goGroupRight");
+ eqPos(cm.getCursor(), Pos(0, 4));
+ cm.execCommand("goGroupRight");
+ eqPos(cm.getCursor(), Pos(0, 7));
+ cm.execCommand("goGroupRight");
+ eqPos(cm.getCursor(), Pos(0, 10));
+ cm.execCommand("goGroupLeft");
+ eqPos(cm.getCursor(), Pos(0, 7));
+ cm.execCommand("goGroupRight"); cm.execCommand("goGroupRight"); cm.execCommand("goGroupRight");
+ eqPos(cm.getCursor(), Pos(0, 15));
+ cm.setCursor(Pos(0, 17));
+ cm.execCommand("goGroupLeft");
+ eqPos(cm.getCursor(), Pos(0, 16));
+ cm.execCommand("goGroupLeft");
+ eqPos(cm.getCursor(), Pos(0, 14));
+ cm.execCommand("goGroupRight"); cm.execCommand("goGroupRight");
+ eqPos(cm.getCursor(), Pos(0, 20));
+ cm.execCommand("goGroupRight");
+ eqPos(cm.getCursor(), Pos(1, 0));
+ cm.execCommand("goGroupRight");
+ eqPos(cm.getCursor(), Pos(1, 2));
+ cm.execCommand("goGroupRight");
+ eqPos(cm.getCursor(), Pos(1, 5));
+ cm.execCommand("goGroupLeft"); cm.execCommand("goGroupLeft");
+ eqPos(cm.getCursor(), Pos(1, 0));
+ cm.execCommand("goGroupLeft");
+ eqPos(cm.getCursor(), Pos(0, 20));
+ cm.execCommand("goGroupLeft");
+ eqPos(cm.getCursor(), Pos(0, 16));
+}, {value: "booo ba---quux. ffff\n abc d"});
+
+testCM("groupsAndWhitespace", function(cm) {
+ var positions = [Pos(0, 0), Pos(0, 2), Pos(0, 5), Pos(0, 9), Pos(0, 11),
+ Pos(1, 0), Pos(1, 2), Pos(1, 5)];
+ for (var i = 1; i < positions.length; i++) {
+ cm.execCommand("goGroupRight");
+ eqPos(cm.getCursor(), positions[i]);
+ }
+ for (var i = positions.length - 2; i >= 0; i--) {
+ cm.execCommand("goGroupLeft");
+ eqPos(cm.getCursor(), i == 2 ? Pos(0, 6) : positions[i]);
+ }
+}, {value: " foo +++ \n bar"});
+
+testCM("charMovementCommands", function(cm) {
+ cm.execCommand("goCharLeft"); cm.execCommand("goColumnLeft");
+ eqPos(cm.getCursor(), Pos(0, 0));
+ cm.execCommand("goCharRight"); cm.execCommand("goCharRight");
+ eqPos(cm.getCursor(), Pos(0, 2));
+ cm.setCursor(Pos(1, 0));
+ cm.execCommand("goColumnLeft");
+ eqPos(cm.getCursor(), Pos(1, 0));
+ cm.execCommand("goCharLeft");
+ eqPos(cm.getCursor(), Pos(0, 5));
+ cm.execCommand("goColumnRight");
+ eqPos(cm.getCursor(), Pos(0, 5));
+ cm.execCommand("goCharRight");
+ eqPos(cm.getCursor(), Pos(1, 0));
+ cm.execCommand("goLineEnd");
+ eqPos(cm.getCursor(), Pos(1, 5));
+ cm.execCommand("goLineStartSmart");
+ eqPos(cm.getCursor(), Pos(1, 1));
+ cm.execCommand("goLineStartSmart");
+ eqPos(cm.getCursor(), Pos(1, 0));
+ cm.setCursor(Pos(2, 0));
+ cm.execCommand("goCharRight"); cm.execCommand("goColumnRight");
+ eqPos(cm.getCursor(), Pos(2, 0));
+}, {value: "line1\n ine2\n"});
+
+testCM("verticalMovementCommands", function(cm) {
+ cm.execCommand("goLineUp");
+ eqPos(cm.getCursor(), Pos(0, 0));
+ cm.execCommand("goLineDown");
+ if (!phantom) // This fails in PhantomJS, though not in a real Webkit
+ eqPos(cm.getCursor(), Pos(1, 0));
+ cm.setCursor(Pos(1, 12));
+ cm.execCommand("goLineDown");
+ eqPos(cm.getCursor(), Pos(2, 5));
+ cm.execCommand("goLineDown");
+ eqPos(cm.getCursor(), Pos(3, 0));
+ cm.execCommand("goLineUp");
+ eqPos(cm.getCursor(), Pos(2, 5));
+ cm.execCommand("goLineUp");
+ eqPos(cm.getCursor(), Pos(1, 12));
+ cm.execCommand("goPageDown");
+ eqPos(cm.getCursor(), Pos(5, 0));
+ cm.execCommand("goPageDown"); cm.execCommand("goLineDown");
+ eqPos(cm.getCursor(), Pos(5, 0));
+ cm.execCommand("goPageUp");
+ eqPos(cm.getCursor(), Pos(0, 0));
+}, {value: "line1\nlong long line2\nline3\n\nline5\n"});
+
+testCM("verticalMovementCommandsWrapping", function(cm) {
+ cm.setSize(120);
+ cm.setCursor(Pos(0, 5));
+ cm.execCommand("goLineDown");
+ eq(cm.getCursor().line, 0);
+ is(cm.getCursor().ch > 5, "moved beyond wrap");
+ for (var i = 0; ; ++i) {
+ is(i < 20, "no endless loop");
+ cm.execCommand("goLineDown");
+ var cur = cm.getCursor();
+ if (cur.line == 1) eq(cur.ch, 5);
+ if (cur.line == 2) { eq(cur.ch, 1); break; }
+ }
+}, {value: "a very long line that wraps around somehow so that we can test cursor movement\nshortone\nk",
+ lineWrapping: true});
+
+testCM("rtlMovement", function(cm) {
+ if (cm.getOption("inputStyle") != "textarea") return;
+ forEach(["خحج", "خحabcخحج", "abخحخحجcd", "abخde", "abخح2342خ1حج", "خ1ح2خح3حxج",
+ "خحcd", "1خحcd", "abcdeح1ج", "خمرحبها مها!", "foobarر", "خ ة ق",
+ "<img src=\"/בדיקה3.jpg\">", "يتم السحب ÙÙŠ 05 Ùبراير 2014"], function(line) {
+ var inv = line.charCodeAt(0) > 128;
+ cm.setValue(line + "\n"); cm.execCommand(inv ? "goLineEnd" : "goLineStart");
+ var cursors = byClassName(cm.getWrapperElement(), "CodeMirror-cursors")[0];
+ var cursor = cursors.firstChild;
+ var prevX = cursor.offsetLeft, prevY = cursor.offsetTop;
+ for (var i = 0; i <= line.length; ++i) {
+ cm.execCommand("goCharRight");
+ cursor = cursors.firstChild;
+ if (i == line.length) is(cursor.offsetTop > prevY, "next line");
+ else is(cursor.offsetLeft > prevX, "moved right");
+ prevX = cursor.offsetLeft; prevY = cursor.offsetTop;
+ }
+ cm.setCursor(0, 0); cm.execCommand(inv ? "goLineStart" : "goLineEnd");
+ prevX = cursors.firstChild.offsetLeft;
+ for (var i = 0; i < line.length; ++i) {
+ cm.execCommand("goCharLeft");
+ cursor = cursors.firstChild;
+ is(cursor.offsetLeft < prevX, "moved left");
+ prevX = cursor.offsetLeft;
+ }
+ });
+}, null, ie_lt9);
+
+// Verify that updating a line clears its bidi ordering
+testCM("bidiUpdate", function(cm) {
+ cm.setCursor(Pos(0, 2));
+ cm.replaceSelection("خحج", "start");
+ cm.execCommand("goCharRight");
+ eqPos(cm.getCursor(), Pos(0, 4));
+}, {value: "abcd\n"});
+
+testCM("movebyTextUnit", function(cm) {
+ cm.setValue("בְּרֵ×שִ\neÌeÌeÌÌ\n");
+ cm.execCommand("goLineEnd");
+ for (var i = 0; i < 4; ++i) cm.execCommand("goCharRight");
+ eqPos(cm.getCursor(), Pos(0, 0));
+ cm.execCommand("goCharRight");
+ eqPos(cm.getCursor(), Pos(1, 0));
+ cm.execCommand("goCharRight");
+ cm.execCommand("goCharRight");
+ eqPos(cm.getCursor(), Pos(1, 4));
+ cm.execCommand("goCharRight");
+ eqPos(cm.getCursor(), Pos(1, 7));
+});
+
+testCM("lineChangeEvents", function(cm) {
+ addDoc(cm, 3, 5);
+ var log = [], want = ["ch 0", "ch 1", "del 2", "ch 0", "ch 0", "del 1", "del 3", "del 4"];
+ for (var i = 0; i < 5; ++i) {
+ CodeMirror.on(cm.getLineHandle(i), "delete", function(i) {
+ return function() {log.push("del " + i);};
+ }(i));
+ CodeMirror.on(cm.getLineHandle(i), "change", function(i) {
+ return function() {log.push("ch " + i);};
+ }(i));
+ }
+ cm.replaceRange("x", Pos(0, 1));
+ cm.replaceRange("xy", Pos(1, 1), Pos(2));
+ cm.replaceRange("foo\nbar", Pos(0, 1));
+ cm.replaceRange("", Pos(0, 0), Pos(cm.lineCount()));
+ eq(log.length, want.length, "same length");
+ for (var i = 0; i < log.length; ++i)
+ eq(log[i], want[i]);
+});
+
+testCM("scrollEntirelyToRight", function(cm) {
+ if (phantom || cm.getOption("inputStyle") != "textarea") return;
+ addDoc(cm, 500, 2);
+ cm.setCursor(Pos(0, 500));
+ var wrap = cm.getWrapperElement(), cur = byClassName(wrap, "CodeMirror-cursor")[0];
+ is(wrap.getBoundingClientRect().right > cur.getBoundingClientRect().left);
+});
+
+testCM("lineWidgets", function(cm) {
+ addDoc(cm, 500, 3);
+ var last = cm.charCoords(Pos(2, 0));
+ var node = document.createElement("div");
+ node.innerHTML = "hi";
+ var widget = cm.addLineWidget(1, node);
+ is(last.top < cm.charCoords(Pos(2, 0)).top, "took up space");
+ cm.setCursor(Pos(1, 1));
+ cm.execCommand("goLineDown");
+ eqPos(cm.getCursor(), Pos(2, 1));
+ cm.execCommand("goLineUp");
+ eqPos(cm.getCursor(), Pos(1, 1));
+});
+
+testCM("lineWidgetFocus", function(cm) {
+ var place = document.getElementById("testground");
+ place.className = "offscreen";
+ try {
+ addDoc(cm, 500, 10);
+ var node = document.createElement("input");
+ var widget = cm.addLineWidget(1, node);
+ node.focus();
+ eq(document.activeElement, node);
+ cm.replaceRange("new stuff", Pos(1, 0));
+ eq(document.activeElement, node);
+ } finally {
+ place.className = "";
+ }
+});
+
+testCM("lineWidgetCautiousRedraw", function(cm) {
+ var node = document.createElement("div");
+ node.innerHTML = "hahah";
+ var w = cm.addLineWidget(0, node);
+ var redrawn = false;
+ w.on("redraw", function() { redrawn = true; });
+ cm.replaceSelection("0");
+ is(!redrawn);
+}, {value: "123\n456"});
+
+
+var knownScrollbarWidth;
+function scrollbarWidth(measure) {
+ if (knownScrollbarWidth != null) return knownScrollbarWidth;
+ var div = document.createElement('div');
+ div.style.cssText = "width: 50px; height: 50px; overflow-x: scroll";
+ document.body.appendChild(div);
+ knownScrollbarWidth = div.offsetHeight - div.clientHeight;
+ document.body.removeChild(div);
+ return knownScrollbarWidth || 0;
+}
+
+testCM("lineWidgetChanged", function(cm) {
+ addDoc(cm, 2, 300);
+ var halfScrollbarWidth = scrollbarWidth(cm.display.measure)/2;
+ cm.setOption('lineNumbers', true);
+ cm.setSize(600, cm.defaultTextHeight() * 50);
+ cm.scrollTo(null, cm.heightAtLine(125, "local"));
+
+ var expectedWidgetHeight = 60;
+ var expectedLinesInWidget = 3;
+ function w() {
+ var node = document.createElement("div");
+ // we use these children with just under half width of the line to check measurements are made with correct width
+ // when placed in the measure div.
+ // If the widget is measured at a width much narrower than it is displayed at, the underHalf children will span two lines and break the test.
+ // If the widget is measured at a width much wider than it is displayed at, the overHalf children will combine and break the test.
+ // Note that this test only checks widgets where coverGutter is true, because these require extra styling to get the width right.
+ // It may also be worthwhile to check this for non-coverGutter widgets.
+ // Visually:
+ // Good:
+ // | ------------- display width ------------- |
+ // | ------- widget-width when measured ------ |
+ // | | -- under-half -- | | -- under-half -- | |
+ // | | --- over-half --- | |
+ // | | --- over-half --- | |
+ // Height: measured as 3 lines, same as it will be when actually displayed
+
+ // Bad (too narrow):
+ // | ------------- display width ------------- |
+ // | ------ widget-width when measured ----- | < -- uh oh
+ // | | -- under-half -- | |
+ // | | -- under-half -- | | < -- when measured, shoved to next line
+ // | | --- over-half --- | |
+ // | | --- over-half --- | |
+ // Height: measured as 4 lines, more than expected . Will be displayed as 3 lines!
+
+ // Bad (too wide):
+ // | ------------- display width ------------- |
+ // | -------- widget-width when measured ------- | < -- uh oh
+ // | | -- under-half -- | | -- under-half -- | |
+ // | | --- over-half --- | | --- over-half --- | | < -- when measured, combined on one line
+ // Height: measured as 2 lines, less than expected. Will be displayed as 3 lines!
+
+ var barelyUnderHalfWidthHtml = '<div style="display: inline-block; height: 1px; width: '+(285 - halfScrollbarWidth)+'px;"></div>';
+ var barelyOverHalfWidthHtml = '<div style="display: inline-block; height: 1px; width: '+(305 - halfScrollbarWidth)+'px;"></div>';
+ node.innerHTML = new Array(3).join(barelyUnderHalfWidthHtml) + new Array(3).join(barelyOverHalfWidthHtml);
+ node.style.cssText = "background: yellow;font-size:0;line-height: " + (expectedWidgetHeight/expectedLinesInWidget) + "px;";
+ return node;
+ }
+ var info0 = cm.getScrollInfo();
+ var w0 = cm.addLineWidget(0, w(), { coverGutter: true });
+ var w150 = cm.addLineWidget(150, w(), { coverGutter: true });
+ var w300 = cm.addLineWidget(300, w(), { coverGutter: true });
+ var info1 = cm.getScrollInfo();
+ eq(info0.height + (3 * expectedWidgetHeight), info1.height);
+ eq(info0.top + expectedWidgetHeight, info1.top);
+ expectedWidgetHeight = 12;
+ w0.node.style.lineHeight = w150.node.style.lineHeight = w300.node.style.lineHeight = (expectedWidgetHeight/expectedLinesInWidget) + "px";
+ w0.changed(); w150.changed(); w300.changed();
+ var info2 = cm.getScrollInfo();
+ eq(info0.height + (3 * expectedWidgetHeight), info2.height);
+ eq(info0.top + expectedWidgetHeight, info2.top);
+});
+
+testCM("getLineNumber", function(cm) {
+ addDoc(cm, 2, 20);
+ var h1 = cm.getLineHandle(1);
+ eq(cm.getLineNumber(h1), 1);
+ cm.replaceRange("hi\nbye\n", Pos(0, 0));
+ eq(cm.getLineNumber(h1), 3);
+ cm.setValue("");
+ eq(cm.getLineNumber(h1), null);
+});
+
+testCM("jumpTheGap", function(cm) {
+ if (phantom) return;
+ var longLine = "abcdef ghiklmnop qrstuvw xyz ";
+ longLine += longLine; longLine += longLine; longLine += longLine;
+ cm.replaceRange(longLine, Pos(2, 0), Pos(2));
+ cm.setSize("200px", null);
+ cm.getWrapperElement().style.lineHeight = 2;
+ cm.refresh();
+ cm.setCursor(Pos(0, 1));
+ cm.execCommand("goLineDown");
+ eqPos(cm.getCursor(), Pos(1, 1));
+ cm.execCommand("goLineDown");
+ eqPos(cm.getCursor(), Pos(2, 1));
+ cm.execCommand("goLineDown");
+ eq(cm.getCursor().line, 2);
+ is(cm.getCursor().ch > 1);
+ cm.execCommand("goLineUp");
+ eqPos(cm.getCursor(), Pos(2, 1));
+ cm.execCommand("goLineUp");
+ eqPos(cm.getCursor(), Pos(1, 1));
+ var node = document.createElement("div");
+ node.innerHTML = "hi"; node.style.height = "30px";
+ cm.addLineWidget(0, node);
+ cm.addLineWidget(1, node.cloneNode(true), {above: true});
+ cm.setCursor(Pos(0, 2));
+ cm.execCommand("goLineDown");
+ eqPos(cm.getCursor(), Pos(1, 2));
+ cm.execCommand("goLineUp");
+ eqPos(cm.getCursor(), Pos(0, 2));
+}, {lineWrapping: true, value: "abc\ndef\nghi\njkl\n"});
+
+testCM("addLineClass", function(cm) {
+ function cls(line, text, bg, wrap, gutter) {
+ var i = cm.lineInfo(line);
+ eq(i.textClass, text);
+ eq(i.bgClass, bg);
+ eq(i.wrapClass, wrap);
+ if (typeof i.handle.gutterClass !== 'undefined') {
+ eq(i.handle.gutterClass, gutter);
+ }
+ }
+ cm.addLineClass(0, "text", "foo");
+ cm.addLineClass(0, "text", "bar");
+ cm.addLineClass(1, "background", "baz");
+ cm.addLineClass(1, "wrap", "foo");
+ cm.addLineClass(1, "gutter", "gutter-class");
+ cls(0, "foo bar", null, null, null);
+ cls(1, null, "baz", "foo", "gutter-class");
+ var lines = cm.display.lineDiv;
+ eq(byClassName(lines, "foo").length, 2);
+ eq(byClassName(lines, "bar").length, 1);
+ eq(byClassName(lines, "baz").length, 1);
+ eq(byClassName(lines, "gutter-class").length, 2); // Gutter classes are reflected in 2 nodes
+ cm.removeLineClass(0, "text", "foo");
+ cls(0, "bar", null, null, null);
+ cm.removeLineClass(0, "text", "foo");
+ cls(0, "bar", null, null, null);
+ cm.removeLineClass(0, "text", "bar");
+ cls(0, null, null, null);
+
+ cm.addLineClass(1, "wrap", "quux");
+ cls(1, null, "baz", "foo quux", "gutter-class");
+ cm.removeLineClass(1, "wrap");
+ cls(1, null, "baz", null, "gutter-class");
+ cm.removeLineClass(1, "gutter", "gutter-class");
+ eq(byClassName(lines, "gutter-class").length, 0);
+ cls(1, null, "baz", null, null);
+
+ cm.addLineClass(1, "gutter", "gutter-class");
+ cls(1, null, "baz", null, "gutter-class");
+ cm.removeLineClass(1, "gutter", "gutter-class");
+ cls(1, null, "baz", null, null);
+
+}, {value: "hohoho\n", lineNumbers: true});
+
+testCM("atomicMarker", function(cm) {
+ addDoc(cm, 10, 10);
+ function atom(ll, cl, lr, cr, li, ri) {
+ return cm.markText(Pos(ll, cl), Pos(lr, cr),
+ {atomic: true, inclusiveLeft: li, inclusiveRight: ri});
+ }
+ var m = atom(0, 1, 0, 5);
+ cm.setCursor(Pos(0, 1));
+ cm.execCommand("goCharRight");
+ eqPos(cm.getCursor(), Pos(0, 5));
+ cm.execCommand("goCharLeft");
+ eqPos(cm.getCursor(), Pos(0, 1));
+ m.clear();
+ m = atom(0, 0, 0, 5, true);
+ eqPos(cm.getCursor(), Pos(0, 5), "pushed out");
+ cm.execCommand("goCharLeft");
+ eqPos(cm.getCursor(), Pos(0, 5));
+ m.clear();
+ m = atom(8, 4, 9, 10, false, true);
+ cm.setCursor(Pos(9, 8));
+ eqPos(cm.getCursor(), Pos(8, 4), "set");
+ cm.execCommand("goCharRight");
+ eqPos(cm.getCursor(), Pos(8, 4), "char right");
+ cm.execCommand("goLineDown");
+ eqPos(cm.getCursor(), Pos(8, 4), "line down");
+ cm.execCommand("goCharLeft");
+ eqPos(cm.getCursor(), Pos(8, 3));
+ m.clear();
+ m = atom(1, 1, 3, 8);
+ cm.setCursor(Pos(0, 0));
+ cm.setCursor(Pos(2, 0));
+ eqPos(cm.getCursor(), Pos(3, 8));
+ cm.execCommand("goCharLeft");
+ eqPos(cm.getCursor(), Pos(1, 1));
+ cm.execCommand("goCharRight");
+ eqPos(cm.getCursor(), Pos(3, 8));
+ cm.execCommand("goLineUp");
+ eqPos(cm.getCursor(), Pos(1, 1));
+ cm.execCommand("goLineDown");
+ eqPos(cm.getCursor(), Pos(3, 8));
+ cm.execCommand("delCharBefore");
+ eq(cm.getValue().length, 80, "del chunk");
+ m = atom(3, 0, 5, 5);
+ cm.setCursor(Pos(3, 0));
+ cm.execCommand("delWordAfter");
+ eq(cm.getValue().length, 53, "del chunk");
+});
+
+testCM("selectionBias", function(cm) {
+ cm.markText(Pos(0, 1), Pos(0, 3), {atomic: true});
+ cm.setCursor(Pos(0, 2));
+ eqPos(cm.getCursor(), Pos(0, 1));
+ cm.setCursor(Pos(0, 2));
+ eqPos(cm.getCursor(), Pos(0, 3));
+ cm.setCursor(Pos(0, 2));
+ eqPos(cm.getCursor(), Pos(0, 1));
+ cm.setCursor(Pos(0, 2), null, {bias: -1});
+ eqPos(cm.getCursor(), Pos(0, 1));
+ cm.setCursor(Pos(0, 4));
+ cm.setCursor(Pos(0, 2), null, {bias: 1});
+ eqPos(cm.getCursor(), Pos(0, 3));
+}, {value: "12345"});
+
+testCM("selectionHomeEnd", function(cm) {
+ cm.markText(Pos(1, 0), Pos(1, 1), {atomic: true, inclusiveLeft: true});
+ cm.markText(Pos(1, 3), Pos(1, 4), {atomic: true, inclusiveRight: true});
+ cm.setCursor(Pos(1, 2));
+ cm.execCommand("goLineStart");
+ eqPos(cm.getCursor(), Pos(1, 1));
+ cm.execCommand("goLineEnd");
+ eqPos(cm.getCursor(), Pos(1, 3));
+}, {value: "ab\ncdef\ngh"});
+
+testCM("readOnlyMarker", function(cm) {
+ function mark(ll, cl, lr, cr, at) {
+ return cm.markText(Pos(ll, cl), Pos(lr, cr),
+ {readOnly: true, atomic: at});
+ }
+ var m = mark(0, 1, 0, 4);
+ cm.setCursor(Pos(0, 2));
+ cm.replaceSelection("hi", "end");
+ eqPos(cm.getCursor(), Pos(0, 2));
+ eq(cm.getLine(0), "abcde");
+ cm.execCommand("selectAll");
+ cm.replaceSelection("oops", "around");
+ eq(cm.getValue(), "oopsbcd");
+ cm.undo();
+ eqPos(m.find().from, Pos(0, 1));
+ eqPos(m.find().to, Pos(0, 4));
+ m.clear();
+ cm.setCursor(Pos(0, 2));
+ cm.replaceSelection("hi", "around");
+ eq(cm.getLine(0), "abhicde");
+ eqPos(cm.getCursor(), Pos(0, 4));
+ m = mark(0, 2, 2, 2, true);
+ cm.setSelection(Pos(1, 1), Pos(2, 4));
+ cm.replaceSelection("t", "end");
+ eqPos(cm.getCursor(), Pos(2, 3));
+ eq(cm.getLine(2), "klto");
+ cm.execCommand("goCharLeft");
+ cm.execCommand("goCharLeft");
+ eqPos(cm.getCursor(), Pos(0, 2));
+ cm.setSelection(Pos(0, 1), Pos(0, 3));
+ cm.replaceSelection("xx", "around");
+ eqPos(cm.getCursor(), Pos(0, 3));
+ eq(cm.getLine(0), "axxhicde");
+}, {value: "abcde\nfghij\nklmno\n"});
+
+testCM("dirtyBit", function(cm) {
+ eq(cm.isClean(), true);
+ cm.replaceSelection("boo", null, "test");
+ eq(cm.isClean(), false);
+ cm.undo();
+ eq(cm.isClean(), true);
+ cm.replaceSelection("boo", null, "test");
+ cm.replaceSelection("baz", null, "test");
+ cm.undo();
+ eq(cm.isClean(), false);
+ cm.markClean();
+ eq(cm.isClean(), true);
+ cm.undo();
+ eq(cm.isClean(), false);
+ cm.redo();
+ eq(cm.isClean(), true);
+});
+
+testCM("changeGeneration", function(cm) {
+ cm.replaceSelection("x");
+ var softGen = cm.changeGeneration();
+ cm.replaceSelection("x");
+ cm.undo();
+ eq(cm.getValue(), "");
+ is(!cm.isClean(softGen));
+ cm.replaceSelection("x");
+ var hardGen = cm.changeGeneration(true);
+ cm.replaceSelection("x");
+ cm.undo();
+ eq(cm.getValue(), "x");
+ is(cm.isClean(hardGen));
+});
+
+testCM("addKeyMap", function(cm) {
+ function sendKey(code) {
+ cm.triggerOnKeyDown({type: "keydown", keyCode: code,
+ preventDefault: function(){}, stopPropagation: function(){}});
+ }
+
+ sendKey(39);
+ eqPos(cm.getCursor(), Pos(0, 1));
+ var test = 0;
+ var map1 = {Right: function() { ++test; }}, map2 = {Right: function() { test += 10; }}
+ cm.addKeyMap(map1);
+ sendKey(39);
+ eqPos(cm.getCursor(), Pos(0, 1));
+ eq(test, 1);
+ cm.addKeyMap(map2, true);
+ sendKey(39);
+ eq(test, 2);
+ cm.removeKeyMap(map1);
+ sendKey(39);
+ eq(test, 12);
+ cm.removeKeyMap(map2);
+ sendKey(39);
+ eq(test, 12);
+ eqPos(cm.getCursor(), Pos(0, 2));
+ cm.addKeyMap({Right: function() { test = 55; }, name: "mymap"});
+ sendKey(39);
+ eq(test, 55);
+ cm.removeKeyMap("mymap");
+ sendKey(39);
+ eqPos(cm.getCursor(), Pos(0, 3));
+}, {value: "abc"});
+
+testCM("findPosH", function(cm) {
+ forEach([{from: Pos(0, 0), to: Pos(0, 1), by: 1},
+ {from: Pos(0, 0), to: Pos(0, 0), by: -1, hitSide: true},
+ {from: Pos(0, 0), to: Pos(0, 4), by: 1, unit: "word"},
+ {from: Pos(0, 0), to: Pos(0, 8), by: 2, unit: "word"},
+ {from: Pos(0, 0), to: Pos(2, 0), by: 20, unit: "word", hitSide: true},
+ {from: Pos(0, 7), to: Pos(0, 5), by: -1, unit: "word"},
+ {from: Pos(0, 4), to: Pos(0, 8), by: 1, unit: "word"},
+ {from: Pos(1, 0), to: Pos(1, 18), by: 3, unit: "word"},
+ {from: Pos(1, 22), to: Pos(1, 5), by: -3, unit: "word"},
+ {from: Pos(1, 15), to: Pos(1, 10), by: -5},
+ {from: Pos(1, 15), to: Pos(1, 10), by: -5, unit: "column"},
+ {from: Pos(1, 15), to: Pos(1, 0), by: -50, unit: "column", hitSide: true},
+ {from: Pos(1, 15), to: Pos(1, 24), by: 50, unit: "column", hitSide: true},
+ {from: Pos(1, 15), to: Pos(2, 0), by: 50, hitSide: true}], function(t) {
+ var r = cm.findPosH(t.from, t.by, t.unit || "char");
+ eqPos(r, t.to);
+ eq(!!r.hitSide, !!t.hitSide);
+ });
+}, {value: "line one\nline two.something.other\n"});
+
+testCM("beforeChange", function(cm) {
+ cm.on("beforeChange", function(cm, change) {
+ var text = [];
+ for (var i = 0; i < change.text.length; ++i)
+ text.push(change.text[i].replace(/\s/g, "_"));
+ change.update(null, null, text);
+ });
+ cm.setValue("hello, i am a\nnew document\n");
+ eq(cm.getValue(), "hello,_i_am_a\nnew_document\n");
+ CodeMirror.on(cm.getDoc(), "beforeChange", function(doc, change) {
+ if (change.from.line == 0) change.cancel();
+ });
+ cm.setValue("oops"); // Canceled
+ eq(cm.getValue(), "hello,_i_am_a\nnew_document\n");
+ cm.replaceRange("hey hey hey", Pos(1, 0), Pos(2, 0));
+ eq(cm.getValue(), "hello,_i_am_a\nhey_hey_hey");
+}, {value: "abcdefghijk"});
+
+testCM("beforeChangeUndo", function(cm) {
+ cm.replaceRange("hi", Pos(0, 0), Pos(0));
+ cm.replaceRange("bye", Pos(0, 0), Pos(0));
+ eq(cm.historySize().undo, 2);
+ cm.on("beforeChange", function(cm, change) {
+ is(!change.update);
+ change.cancel();
+ });
+ cm.undo();
+ eq(cm.historySize().undo, 0);
+ eq(cm.getValue(), "bye\ntwo");
+}, {value: "one\ntwo"});
+
+testCM("beforeSelectionChange", function(cm) {
+ function notAtEnd(cm, pos) {
+ var len = cm.getLine(pos.line).length;
+ if (!len || pos.ch == len) return Pos(pos.line, pos.ch - 1);
+ return pos;
+ }
+ cm.on("beforeSelectionChange", function(cm, obj) {
+ obj.update([{anchor: notAtEnd(cm, obj.ranges[0].anchor),
+ head: notAtEnd(cm, obj.ranges[0].head)}]);
+ });
+
+ addDoc(cm, 10, 10);
+ cm.execCommand("goLineEnd");
+ eqPos(cm.getCursor(), Pos(0, 9));
+ cm.execCommand("selectAll");
+ eqPos(cm.getCursor("start"), Pos(0, 0));
+ eqPos(cm.getCursor("end"), Pos(9, 9));
+});
+
+testCM("change_removedText", function(cm) {
+ cm.setValue("abc\ndef");
+
+ var removedText = [];
+ cm.on("change", function(cm, change) {
+ removedText.push(change.removed);
+ });
+
+ cm.operation(function() {
+ cm.replaceRange("xyz", Pos(0, 0), Pos(1,1));
+ cm.replaceRange("123", Pos(0,0));
+ });
+
+ eq(removedText.length, 2);
+ eq(removedText[0].join("\n"), "abc\nd");
+ eq(removedText[1].join("\n"), "");
+
+ var removedText = [];
+ cm.undo();
+ eq(removedText.length, 2);
+ eq(removedText[0].join("\n"), "123");
+ eq(removedText[1].join("\n"), "xyz");
+
+ var removedText = [];
+ cm.redo();
+ eq(removedText.length, 2);
+ eq(removedText[0].join("\n"), "abc\nd");
+ eq(removedText[1].join("\n"), "");
+});
+
+testCM("lineStyleFromMode", function(cm) {
+ CodeMirror.defineMode("test_mode", function() {
+ return {token: function(stream) {
+ if (stream.match(/^\[[^\]]*\]/)) return " line-brackets ";
+ if (stream.match(/^\([^\)]*\)/)) return " line-background-parens ";
+ if (stream.match(/^<[^>]*>/)) return " span line-line line-background-bg ";
+ stream.match(/^\s+|^\S+/);
+ }};
+ });
+ cm.setOption("mode", "test_mode");
+ var bracketElts = byClassName(cm.getWrapperElement(), "brackets");
+ eq(bracketElts.length, 1, "brackets count");
+ eq(bracketElts[0].nodeName, "PRE");
+ is(!/brackets.*brackets/.test(bracketElts[0].className));
+ var parenElts = byClassName(cm.getWrapperElement(), "parens");
+ eq(parenElts.length, 1, "parens count");
+ eq(parenElts[0].nodeName, "DIV");
+ is(!/parens.*parens/.test(parenElts[0].className));
+ eq(parenElts[0].parentElement.nodeName, "DIV");
+
+ eq(byClassName(cm.getWrapperElement(), "bg").length, 1);
+ eq(byClassName(cm.getWrapperElement(), "line").length, 1);
+ var spanElts = byClassName(cm.getWrapperElement(), "cm-span");
+ eq(spanElts.length, 2);
+ is(/^\s*cm-span\s*$/.test(spanElts[0].className));
+}, {value: "line1: [br] [br]\nline2: (par) (par)\nline3: <tag> <tag>"});
+
+testCM("lineStyleFromBlankLine", function(cm) {
+ CodeMirror.defineMode("lineStyleFromBlankLine_mode", function() {
+ return {token: function(stream) { stream.skipToEnd(); return "comment"; },
+ blankLine: function() { return "line-blank"; }};
+ });
+ cm.setOption("mode", "lineStyleFromBlankLine_mode");
+ var blankElts = byClassName(cm.getWrapperElement(), "blank");
+ eq(blankElts.length, 1);
+ eq(blankElts[0].nodeName, "PRE");
+ cm.replaceRange("x", Pos(1, 0));
+ blankElts = byClassName(cm.getWrapperElement(), "blank");
+ eq(blankElts.length, 0);
+}, {value: "foo\n\nbar"});
+
+CodeMirror.registerHelper("xxx", "a", "A");
+CodeMirror.registerHelper("xxx", "b", "B");
+CodeMirror.defineMode("yyy", function() {
+ return {
+ token: function(stream) { stream.skipToEnd(); },
+ xxx: ["a", "b", "q"]
+ };
+});
+CodeMirror.registerGlobalHelper("xxx", "c", function(m) { return m.enableC; }, "C");
+
+testCM("helpers", function(cm) {
+ cm.setOption("mode", "yyy");
+ eq(cm.getHelpers(Pos(0, 0), "xxx").join("/"), "A/B");
+ cm.setOption("mode", {name: "yyy", modeProps: {xxx: "b", enableC: true}});
+ eq(cm.getHelpers(Pos(0, 0), "xxx").join("/"), "B/C");
+ cm.setOption("mode", "javascript");
+ eq(cm.getHelpers(Pos(0, 0), "xxx").join("/"), "");
+});
+
+testCM("selectionHistory", function(cm) {
+ for (var i = 0; i < 3; i++) {
+ cm.setExtending(true);
+ cm.execCommand("goCharRight");
+ cm.setExtending(false);
+ cm.execCommand("goCharRight");
+ cm.execCommand("goCharRight");
+ }
+ cm.execCommand("undoSelection");
+ eq(cm.getSelection(), "c");
+ cm.execCommand("undoSelection");
+ eq(cm.getSelection(), "");
+ eqPos(cm.getCursor(), Pos(0, 4));
+ cm.execCommand("undoSelection");
+ eq(cm.getSelection(), "b");
+ cm.execCommand("redoSelection");
+ eq(cm.getSelection(), "");
+ eqPos(cm.getCursor(), Pos(0, 4));
+ cm.execCommand("redoSelection");
+ eq(cm.getSelection(), "c");
+ cm.execCommand("redoSelection");
+ eq(cm.getSelection(), "");
+ eqPos(cm.getCursor(), Pos(0, 6));
+}, {value: "a b c d"});
+
+testCM("selectionChangeReducesRedo", function(cm) {
+ cm.replaceSelection("X");
+ cm.execCommand("goCharRight");
+ cm.undoSelection();
+ cm.execCommand("selectAll");
+ cm.undoSelection();
+ eq(cm.getValue(), "Xabc");
+ eqPos(cm.getCursor(), Pos(0, 1));
+ cm.undoSelection();
+ eq(cm.getValue(), "abc");
+}, {value: "abc"});
+
+testCM("selectionHistoryNonOverlapping", function(cm) {
+ cm.setSelection(Pos(0, 0), Pos(0, 1));
+ cm.setSelection(Pos(0, 2), Pos(0, 3));
+ cm.execCommand("undoSelection");
+ eqPos(cm.getCursor("anchor"), Pos(0, 0));
+ eqPos(cm.getCursor("head"), Pos(0, 1));
+}, {value: "1234"});
+
+testCM("cursorMotionSplitsHistory", function(cm) {
+ cm.replaceSelection("a");
+ cm.execCommand("goCharRight");
+ cm.replaceSelection("b");
+ cm.replaceSelection("c");
+ cm.undo();
+ eq(cm.getValue(), "a1234");
+ eqPos(cm.getCursor(), Pos(0, 2));
+ cm.undo();
+ eq(cm.getValue(), "1234");
+ eqPos(cm.getCursor(), Pos(0, 0));
+}, {value: "1234"});
+
+testCM("selChangeInOperationDoesNotSplit", function(cm) {
+ for (var i = 0; i < 4; i++) {
+ cm.operation(function() {
+ cm.replaceSelection("x");
+ cm.setCursor(Pos(0, cm.getCursor().ch - 1));
+ });
+ }
+ eqPos(cm.getCursor(), Pos(0, 0));
+ eq(cm.getValue(), "xxxxa");
+ cm.undo();
+ eq(cm.getValue(), "a");
+}, {value: "a"});
+
+testCM("alwaysMergeSelEventWithChangeOrigin", function(cm) {
+ cm.replaceSelection("U", null, "foo");
+ cm.setSelection(Pos(0, 0), Pos(0, 1), {origin: "foo"});
+ cm.undoSelection();
+ eq(cm.getValue(), "a");
+ cm.replaceSelection("V", null, "foo");
+ cm.setSelection(Pos(0, 0), Pos(0, 1), {origin: "bar"});
+ cm.undoSelection();
+ eq(cm.getValue(), "Va");
+}, {value: "a"});
+
+testCM("getTokenAt", function(cm) {
+ var tokPlus = cm.getTokenAt(Pos(0, 2));
+ eq(tokPlus.type, "operator");
+ eq(tokPlus.string, "+");
+ var toks = cm.getLineTokens(0);
+ eq(toks.length, 3);
+ forEach([["number", "1"], ["operator", "+"], ["number", "2"]], function(expect, i) {
+ eq(toks[i].type, expect[0]);
+ eq(toks[i].string, expect[1]);
+ });
+}, {value: "1+2", mode: "javascript"});
+
+testCM("getTokenTypeAt", function(cm) {
+ eq(cm.getTokenTypeAt(Pos(0, 0)), "number");
+ eq(cm.getTokenTypeAt(Pos(0, 6)), "string");
+ cm.addOverlay({
+ token: function(stream) {
+ if (stream.match("foo")) return "foo";
+ else stream.next();
+ }
+ });
+ eq(byClassName(cm.getWrapperElement(), "cm-foo").length, 1);
+ eq(cm.getTokenTypeAt(Pos(0, 6)), "string");
+}, {value: "1 + 'foo'", mode: "javascript"});
+
+testCM("resizeLineWidget", function(cm) {
+ addDoc(cm, 200, 3);
+ var widget = document.createElement("pre");
+ widget.innerHTML = "imwidget";
+ widget.style.background = "yellow";
+ cm.addLineWidget(1, widget, {noHScroll: true});
+ cm.setSize(40);
+ is(widget.parentNode.offsetWidth < 42);
+});
+
+testCM("combinedOperations", function(cm) {
+ var place = document.getElementById("testground");
+ var other = CodeMirror(place, {value: "123"});
+ try {
+ cm.operation(function() {
+ cm.addLineClass(0, "wrap", "foo");
+ other.addLineClass(0, "wrap", "foo");
+ });
+ eq(byClassName(cm.getWrapperElement(), "foo").length, 1);
+ eq(byClassName(other.getWrapperElement(), "foo").length, 1);
+ cm.operation(function() {
+ cm.removeLineClass(0, "wrap", "foo");
+ other.removeLineClass(0, "wrap", "foo");
+ });
+ eq(byClassName(cm.getWrapperElement(), "foo").length, 0);
+ eq(byClassName(other.getWrapperElement(), "foo").length, 0);
+ } finally {
+ place.removeChild(other.getWrapperElement());
+ }
+}, {value: "abc"});
+
+testCM("eventOrder", function(cm) {
+ var seen = [];
+ cm.on("change", function() {
+ if (!seen.length) cm.replaceSelection(".");
+ seen.push("change");
+ });
+ cm.on("cursorActivity", function() {
+ cm.replaceSelection("!");
+ seen.push("activity");
+ });
+ cm.replaceSelection("/");
+ eq(seen.join(","), "change,change,activity,change");
+});
+
+testCM("splitSpaces_nonspecial", function(cm) {
+ eq(byClassName(cm.getWrapperElement(), "cm-invalidchar").length, 0);
+}, {
+ specialChars: /[\u00a0]/,
+ value: "spaces -> <- between"
+});
+
+test("core_rmClass", function() {
+ var node = document.createElement("div");
+ node.className = "foo-bar baz-quux yadda";
+ CodeMirror.rmClass(node, "quux");
+ eq(node.className, "foo-bar baz-quux yadda");
+ CodeMirror.rmClass(node, "baz-quux");
+ eq(node.className, "foo-bar yadda");
+ CodeMirror.rmClass(node, "yadda");
+ eq(node.className, "foo-bar");
+ CodeMirror.rmClass(node, "foo-bar");
+ eq(node.className, "");
+ node.className = " foo ";
+ CodeMirror.rmClass(node, "foo");
+ eq(node.className, "");
+});
+
+test("core_addClass", function() {
+ var node = document.createElement("div");
+ CodeMirror.addClass(node, "a");
+ eq(node.className, "a");
+ CodeMirror.addClass(node, "a");
+ eq(node.className, "a");
+ CodeMirror.addClass(node, "b");
+ eq(node.className, "a b");
+ CodeMirror.addClass(node, "a");
+ CodeMirror.addClass(node, "b");
+ eq(node.className, "a b");
+});
+
+testCM("lineSeparator", function(cm) {
+ eq(cm.lineCount(), 3);
+ eq(cm.getLine(1), "bar\r");
+ eq(cm.getLine(2), "baz\rquux");
+ cm.setOption("lineSeparator", "\r");
+ eq(cm.lineCount(), 5);
+ eq(cm.getLine(4), "quux");
+ eq(cm.getValue(), "foo\rbar\r\rbaz\rquux");
+ eq(cm.getValue("\n"), "foo\nbar\n\nbaz\nquux");
+ cm.setOption("lineSeparator", null);
+ cm.setValue("foo\nbar\r\nbaz\rquux");
+ eq(cm.lineCount(), 4);
+}, {value: "foo\nbar\r\nbaz\rquux",
+ lineSeparator: "\n"});
diff --git a/devtools/client/sourceeditor/test/codemirror/vim_test.js b/devtools/client/sourceeditor/test/codemirror/vim_test.js
new file mode 100644
index 000000000..fb612b140
--- /dev/null
+++ b/devtools/client/sourceeditor/test/codemirror/vim_test.js
@@ -0,0 +1,4011 @@
+CodeMirror.Vim.suppressErrorLogging = true;
+
+var code = '' +
+' wOrd1 (#%\n' +
+' word3] \n' +
+'aopop pop 0 1 2 3 4\n' +
+' (a) [b] {c} \n' +
+'int getchar(void) {\n' +
+' static char buf[BUFSIZ];\n' +
+' static char *bufp = buf;\n' +
+' if (n == 0) { /* buffer is empty */\n' +
+' n = read(0, buf, sizeof buf);\n' +
+' bufp = buf;\n' +
+' }\n' +
+'\n' +
+' return (--n >= 0) ? (unsigned char) *bufp++ : EOF;\n' +
+' \n' +
+'}\n';
+
+var lines = (function() {
+ lineText = code.split('\n');
+ var ret = [];
+ for (var i = 0; i < lineText.length; i++) {
+ ret[i] = {
+ line: i,
+ length: lineText[i].length,
+ lineText: lineText[i],
+ textStart: /^\s*/.exec(lineText[i])[0].length
+ };
+ }
+ return ret;
+})();
+var endOfDocument = makeCursor(lines.length - 1,
+ lines[lines.length - 1].length);
+var wordLine = lines[0];
+var bigWordLine = lines[1];
+var charLine = lines[2];
+var bracesLine = lines[3];
+var seekBraceLine = lines[4];
+
+var word1 = {
+ start: { line: wordLine.line, ch: 1 },
+ end: { line: wordLine.line, ch: 5 }
+};
+var word2 = {
+ start: { line: wordLine.line, ch: word1.end.ch + 2 },
+ end: { line: wordLine.line, ch: word1.end.ch + 4 }
+};
+var word3 = {
+ start: { line: bigWordLine.line, ch: 1 },
+ end: { line: bigWordLine.line, ch: 5 }
+};
+var bigWord1 = word1;
+var bigWord2 = word2;
+var bigWord3 = {
+ start: { line: bigWordLine.line, ch: 1 },
+ end: { line: bigWordLine.line, ch: 7 }
+};
+var bigWord4 = {
+ start: { line: bigWordLine.line, ch: bigWord1.end.ch + 3 },
+ end: { line: bigWordLine.line, ch: bigWord1.end.ch + 7 }
+};
+
+var oChars = [ { line: charLine.line, ch: 1 },
+ { line: charLine.line, ch: 3 },
+ { line: charLine.line, ch: 7 } ];
+var pChars = [ { line: charLine.line, ch: 2 },
+ { line: charLine.line, ch: 4 },
+ { line: charLine.line, ch: 6 },
+ { line: charLine.line, ch: 8 } ];
+var numChars = [ { line: charLine.line, ch: 10 },
+ { line: charLine.line, ch: 12 },
+ { line: charLine.line, ch: 14 },
+ { line: charLine.line, ch: 16 },
+ { line: charLine.line, ch: 18 }];
+var parens1 = {
+ start: { line: bracesLine.line, ch: 1 },
+ end: { line: bracesLine.line, ch: 3 }
+};
+var squares1 = {
+ start: { line: bracesLine.line, ch: 5 },
+ end: { line: bracesLine.line, ch: 7 }
+};
+var curlys1 = {
+ start: { line: bracesLine.line, ch: 9 },
+ end: { line: bracesLine.line, ch: 11 }
+};
+var seekOutside = {
+ start: { line: seekBraceLine.line, ch: 1 },
+ end: { line: seekBraceLine.line, ch: 16 }
+};
+var seekInside = {
+ start: { line: seekBraceLine.line, ch: 14 },
+ end: { line: seekBraceLine.line, ch: 11 }
+};
+
+function copyCursor(cur) {
+ return { ch: cur.ch, line: cur.line };
+}
+
+function forEach(arr, func) {
+ for (var i = 0; i < arr.length; i++) {
+ func(arr[i], i, arr);
+ }
+}
+
+function testVim(name, run, opts, expectedFail) {
+ var vimOpts = {
+ lineNumbers: true,
+ vimMode: true,
+ showCursorWhenSelecting: true,
+ value: code
+ };
+ for (var prop in opts) {
+ if (opts.hasOwnProperty(prop)) {
+ vimOpts[prop] = opts[prop];
+ }
+ }
+ return test('vim_' + name, function() {
+ var place = document.getElementById("testground");
+ var cm = CodeMirror(place, vimOpts);
+ var vim = CodeMirror.Vim.maybeInitVimState_(cm);
+
+ function doKeysFn(cm) {
+ return function(args) {
+ if (args instanceof Array) {
+ arguments = args;
+ }
+ for (var i = 0; i < arguments.length; i++) {
+ CodeMirror.Vim.handleKey(cm, arguments[i]);
+ }
+ }
+ }
+ function doInsertModeKeysFn(cm) {
+ return function(args) {
+ if (args instanceof Array) { arguments = args; }
+ function executeHandler(handler) {
+ if (typeof handler == 'string') {
+ CodeMirror.commands[handler](cm);
+ } else {
+ handler(cm);
+ }
+ return true;
+ }
+ for (var i = 0; i < arguments.length; i++) {
+ var key = arguments[i];
+ // Find key in keymap and handle.
+ var handled = CodeMirror.lookupKey(key, 'vim-insert', executeHandler);
+ // Record for insert mode.
+ if (handled == "handled" && cm.state.vim.insertMode && arguments[i] != 'Esc') {
+ var lastChange = CodeMirror.Vim.getVimGlobalState_().macroModeState.lastInsertModeChanges;
+ if (lastChange) {
+ lastChange.changes.push(new CodeMirror.Vim.InsertModeKey(key));
+ }
+ }
+ }
+ }
+ }
+ function doExFn(cm) {
+ return function(command) {
+ cm.openDialog = helpers.fakeOpenDialog(command);
+ helpers.doKeys(':');
+ }
+ }
+ function assertCursorAtFn(cm) {
+ return function(line, ch) {
+ var pos;
+ if (ch == null && typeof line.line == 'number') {
+ pos = line;
+ } else {
+ pos = makeCursor(line, ch);
+ }
+ eqPos(pos, cm.getCursor());
+ }
+ }
+ function fakeOpenDialog(result) {
+ return function(text, callback) {
+ return callback(result);
+ }
+ }
+ function fakeOpenNotification(matcher) {
+ return function(text) {
+ matcher(text);
+ }
+ }
+ var helpers = {
+ doKeys: doKeysFn(cm),
+ // Warning: Only emulates keymap events, not character insertions. Use
+ // replaceRange to simulate character insertions.
+ // Keys are in CodeMirror format, NOT vim format.
+ doInsertModeKeys: doInsertModeKeysFn(cm),
+ doEx: doExFn(cm),
+ assertCursorAt: assertCursorAtFn(cm),
+ fakeOpenDialog: fakeOpenDialog,
+ fakeOpenNotification: fakeOpenNotification,
+ getRegisterController: function() {
+ return CodeMirror.Vim.getRegisterController();
+ }
+ }
+ CodeMirror.Vim.resetVimGlobalState_();
+ var successful = false;
+ var savedOpenNotification = cm.openNotification;
+ var savedOpenDialog = cm.openDialog;
+ try {
+ run(cm, vim, helpers);
+ successful = true;
+ } finally {
+ cm.openNotification = savedOpenNotification;
+ cm.openDialog = savedOpenDialog;
+ if (!successful || verbose) {
+ place.style.visibility = "visible";
+ } else {
+ place.removeChild(cm.getWrapperElement());
+ }
+ }
+ }, expectedFail);
+};
+testVim('qq@q', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('q', 'q', 'l', 'l', 'q');
+ helpers.assertCursorAt(0,2);
+ helpers.doKeys('@', 'q');
+ helpers.assertCursorAt(0,4);
+}, { value: ' '});
+testVim('@@', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('q', 'q', 'l', 'l', 'q');
+ helpers.assertCursorAt(0,2);
+ helpers.doKeys('@', 'q');
+ helpers.assertCursorAt(0,4);
+ helpers.doKeys('@', '@');
+ helpers.assertCursorAt(0,6);
+}, { value: ' '});
+var jumplistScene = ''+
+ 'word\n'+
+ '(word)\n'+
+ '{word\n'+
+ 'word.\n'+
+ '\n'+
+ 'word search\n'+
+ '}word\n'+
+ 'word\n'+
+ 'word\n';
+function testJumplist(name, keys, endPos, startPos, dialog) {
+ endPos = makeCursor(endPos[0], endPos[1]);
+ startPos = makeCursor(startPos[0], startPos[1]);
+ testVim(name, function(cm, vim, helpers) {
+ CodeMirror.Vim.resetVimGlobalState_();
+ if(dialog)cm.openDialog = helpers.fakeOpenDialog('word');
+ cm.setCursor(startPos);
+ helpers.doKeys.apply(null, keys);
+ helpers.assertCursorAt(endPos);
+ }, {value: jumplistScene});
+};
+testJumplist('jumplist_H', ['H', '<C-o>'], [5,2], [5,2]);
+testJumplist('jumplist_M', ['M', '<C-o>'], [2,2], [2,2]);
+testJumplist('jumplist_L', ['L', '<C-o>'], [2,2], [2,2]);
+testJumplist('jumplist_[[', ['[', '[', '<C-o>'], [5,2], [5,2]);
+testJumplist('jumplist_]]', [']', ']', '<C-o>'], [2,2], [2,2]);
+testJumplist('jumplist_G', ['G', '<C-o>'], [5,2], [5,2]);
+testJumplist('jumplist_gg', ['g', 'g', '<C-o>'], [5,2], [5,2]);
+testJumplist('jumplist_%', ['%', '<C-o>'], [1,5], [1,5]);
+testJumplist('jumplist_{', ['{', '<C-o>'], [1,5], [1,5]);
+testJumplist('jumplist_}', ['}', '<C-o>'], [1,5], [1,5]);
+testJumplist('jumplist_\'', ['m', 'a', 'h', '\'', 'a', 'h', '<C-i>'], [1,0], [1,5]);
+testJumplist('jumplist_`', ['m', 'a', 'h', '`', 'a', 'h', '<C-i>'], [1,5], [1,5]);
+testJumplist('jumplist_*_cachedCursor', ['*', '<C-o>'], [1,3], [1,3]);
+testJumplist('jumplist_#_cachedCursor', ['#', '<C-o>'], [1,3], [1,3]);
+testJumplist('jumplist_n', ['#', 'n', '<C-o>'], [1,1], [2,3]);
+testJumplist('jumplist_N', ['#', 'N', '<C-o>'], [1,1], [2,3]);
+testJumplist('jumplist_repeat_<c-o>', ['*', '*', '*', '3', '<C-o>'], [2,3], [2,3]);
+testJumplist('jumplist_repeat_<c-i>', ['*', '*', '*', '3', '<C-o>', '2', '<C-i>'], [5,0], [2,3]);
+testJumplist('jumplist_repeated_motion', ['3', '*', '<C-o>'], [2,3], [2,3]);
+testJumplist('jumplist_/', ['/', '<C-o>'], [2,3], [2,3], 'dialog');
+testJumplist('jumplist_?', ['?', '<C-o>'], [2,3], [2,3], 'dialog');
+testJumplist('jumplist_skip_deleted_mark<c-o>',
+ ['*', 'n', 'n', 'k', 'd', 'k', '<C-o>', '<C-o>', '<C-o>'],
+ [0,2], [0,2]);
+testJumplist('jumplist_skip_deleted_mark<c-i>',
+ ['*', 'n', 'n', 'k', 'd', 'k', '<C-o>', '<C-i>', '<C-i>'],
+ [1,0], [0,2]);
+
+/**
+ * @param name Name of the test
+ * @param keys An array of keys or a string with a single key to simulate.
+ * @param endPos The expected end position of the cursor.
+ * @param startPos The position the cursor should start at, defaults to 0, 0.
+ */
+function testMotion(name, keys, endPos, startPos) {
+ testVim(name, function(cm, vim, helpers) {
+ if (!startPos) {
+ startPos = { line: 0, ch: 0 };
+ }
+ cm.setCursor(startPos);
+ helpers.doKeys(keys);
+ helpers.assertCursorAt(endPos);
+ });
+};
+
+function makeCursor(line, ch) {
+ return { line: line, ch: ch };
+};
+
+function offsetCursor(cur, offsetLine, offsetCh) {
+ return { line: cur.line + offsetLine, ch: cur.ch + offsetCh };
+};
+
+// Motion tests
+testMotion('|', '|', makeCursor(0, 0), makeCursor(0,4));
+testMotion('|_repeat', ['3', '|'], makeCursor(0, 2), makeCursor(0,4));
+testMotion('h', 'h', makeCursor(0, 0), word1.start);
+testMotion('h_repeat', ['3', 'h'], offsetCursor(word1.end, 0, -3), word1.end);
+testMotion('l', 'l', makeCursor(0, 1));
+testMotion('l_repeat', ['2', 'l'], makeCursor(0, 2));
+testMotion('j', 'j', offsetCursor(word1.end, 1, 0), word1.end);
+testMotion('j_repeat', ['2', 'j'], offsetCursor(word1.end, 2, 0), word1.end);
+testMotion('j_repeat_clip', ['1000', 'j'], endOfDocument);
+testMotion('k', 'k', offsetCursor(word3.end, -1, 0), word3.end);
+testMotion('k_repeat', ['2', 'k'], makeCursor(0, 4), makeCursor(2, 4));
+testMotion('k_repeat_clip', ['1000', 'k'], makeCursor(0, 4), makeCursor(2, 4));
+testMotion('w', 'w', word1.start);
+testMotion('w_multiple_newlines_no_space', 'w', makeCursor(12, 2), makeCursor(11, 2));
+testMotion('w_multiple_newlines_with_space', 'w', makeCursor(14, 0), makeCursor(12, 51));
+testMotion('w_repeat', ['2', 'w'], word2.start);
+testMotion('w_wrap', ['w'], word3.start, word2.start);
+testMotion('w_endOfDocument', 'w', endOfDocument, endOfDocument);
+testMotion('w_start_to_end', ['1000', 'w'], endOfDocument, makeCursor(0, 0));
+testMotion('W', 'W', bigWord1.start);
+testMotion('W_repeat', ['2', 'W'], bigWord3.start, bigWord1.start);
+testMotion('e', 'e', word1.end);
+testMotion('e_repeat', ['2', 'e'], word2.end);
+testMotion('e_wrap', 'e', word3.end, word2.end);
+testMotion('e_endOfDocument', 'e', endOfDocument, endOfDocument);
+testMotion('e_start_to_end', ['1000', 'e'], endOfDocument, makeCursor(0, 0));
+testMotion('b', 'b', word3.start, word3.end);
+testMotion('b_repeat', ['2', 'b'], word2.start, word3.end);
+testMotion('b_wrap', 'b', word2.start, word3.start);
+testMotion('b_startOfDocument', 'b', makeCursor(0, 0), makeCursor(0, 0));
+testMotion('b_end_to_start', ['1000', 'b'], makeCursor(0, 0), endOfDocument);
+testMotion('ge', ['g', 'e'], word2.end, word3.end);
+testMotion('ge_repeat', ['2', 'g', 'e'], word1.end, word3.start);
+testMotion('ge_wrap', ['g', 'e'], word2.end, word3.start);
+testMotion('ge_startOfDocument', ['g', 'e'], makeCursor(0, 0),
+ makeCursor(0, 0));
+testMotion('ge_end_to_start', ['1000', 'g', 'e'], makeCursor(0, 0), endOfDocument);
+testMotion('gg', ['g', 'g'], makeCursor(lines[0].line, lines[0].textStart),
+ makeCursor(3, 1));
+testMotion('gg_repeat', ['3', 'g', 'g'],
+ makeCursor(lines[2].line, lines[2].textStart));
+testMotion('G', 'G',
+ makeCursor(lines[lines.length - 1].line, lines[lines.length - 1].textStart),
+ makeCursor(3, 1));
+testMotion('G_repeat', ['3', 'G'], makeCursor(lines[2].line,
+ lines[2].textStart));
+// TODO: Make the test code long enough to test Ctrl-F and Ctrl-B.
+testMotion('0', '0', makeCursor(0, 0), makeCursor(0, 8));
+testMotion('^', '^', makeCursor(0, lines[0].textStart), makeCursor(0, 8));
+testMotion('+', '+', makeCursor(1, lines[1].textStart), makeCursor(0, 8));
+testMotion('-', '-', makeCursor(0, lines[0].textStart), makeCursor(1, 4));
+testMotion('_', ['6','_'], makeCursor(5, lines[5].textStart), makeCursor(0, 8));
+testMotion('$', '$', makeCursor(0, lines[0].length - 1), makeCursor(0, 1));
+testMotion('$_repeat', ['2', '$'], makeCursor(1, lines[1].length - 1),
+ makeCursor(0, 3));
+testMotion('f', ['f', 'p'], pChars[0], makeCursor(charLine.line, 0));
+testMotion('f_repeat', ['2', 'f', 'p'], pChars[2], pChars[0]);
+testMotion('f_num', ['f', '2'], numChars[2], makeCursor(charLine.line, 0));
+testMotion('t', ['t','p'], offsetCursor(pChars[0], 0, -1),
+ makeCursor(charLine.line, 0));
+testMotion('t_repeat', ['2', 't', 'p'], offsetCursor(pChars[2], 0, -1),
+ pChars[0]);
+testMotion('F', ['F', 'p'], pChars[0], pChars[1]);
+testMotion('F_repeat', ['2', 'F', 'p'], pChars[0], pChars[2]);
+testMotion('T', ['T', 'p'], offsetCursor(pChars[0], 0, 1), pChars[1]);
+testMotion('T_repeat', ['2', 'T', 'p'], offsetCursor(pChars[0], 0, 1), pChars[2]);
+testMotion('%_parens', ['%'], parens1.end, parens1.start);
+testMotion('%_squares', ['%'], squares1.end, squares1.start);
+testMotion('%_braces', ['%'], curlys1.end, curlys1.start);
+testMotion('%_seek_outside', ['%'], seekOutside.end, seekOutside.start);
+testMotion('%_seek_inside', ['%'], seekInside.end, seekInside.start);
+testVim('%_seek_skip', function(cm, vim, helpers) {
+ cm.setCursor(0,0);
+ helpers.doKeys(['%']);
+ helpers.assertCursorAt(0,9);
+}, {value:'01234"("()'});
+testVim('%_skip_string', function(cm, vim, helpers) {
+ cm.setCursor(0,0);
+ helpers.doKeys(['%']);
+ helpers.assertCursorAt(0,4);
+ cm.setCursor(0,2);
+ helpers.doKeys(['%']);
+ helpers.assertCursorAt(0,0);
+}, {value:'(")")'});
+testVim('%_skip_comment', function(cm, vim, helpers) {
+ cm.setCursor(0,0);
+ helpers.doKeys(['%']);
+ helpers.assertCursorAt(0,6);
+ cm.setCursor(0,3);
+ helpers.doKeys(['%']);
+ helpers.assertCursorAt(0,0);
+}, {value:'(/*)*/)'});
+// Make sure that moving down after going to the end of a line always leaves you
+// at the end of a line, but preserves the offset in other cases
+testVim('Changing lines after Eol operation', function(cm, vim, helpers) {
+ cm.setCursor(0,0);
+ helpers.doKeys(['$']);
+ helpers.doKeys(['j']);
+ // After moving to Eol and then down, we should be at Eol of line 2
+ helpers.assertCursorAt({ line: 1, ch: lines[1].length - 1 });
+ helpers.doKeys(['j']);
+ // After moving down, we should be at Eol of line 3
+ helpers.assertCursorAt({ line: 2, ch: lines[2].length - 1 });
+ helpers.doKeys(['h']);
+ helpers.doKeys(['j']);
+ // After moving back one space and then down, since line 4 is shorter than line 2, we should
+ // be at Eol of line 2 - 1
+ helpers.assertCursorAt({ line: 3, ch: lines[3].length - 1 });
+ helpers.doKeys(['j']);
+ helpers.doKeys(['j']);
+ // After moving down again, since line 3 has enough characters, we should be back to the
+ // same place we were at on line 1
+ helpers.assertCursorAt({ line: 5, ch: lines[2].length - 2 });
+});
+//making sure gj and gk recover from clipping
+testVim('gj_gk_clipping', function(cm,vim,helpers){
+ cm.setCursor(0, 1);
+ helpers.doKeys('g','j','g','j');
+ helpers.assertCursorAt(2, 1);
+ helpers.doKeys('g','k','g','k');
+ helpers.assertCursorAt(0, 1);
+},{value: 'line 1\n\nline 2'});
+//testing a mix of j/k and gj/gk
+testVim('j_k_and_gj_gk', function(cm,vim,helpers){
+ cm.setSize(120);
+ cm.setCursor(0, 0);
+ //go to the last character on the first line
+ helpers.doKeys('$');
+ //move up/down on the column within the wrapped line
+ //side-effect: cursor is not locked to eol anymore
+ helpers.doKeys('g','k');
+ var cur=cm.getCursor();
+ eq(cur.line,0);
+ is((cur.ch<176),'gk didn\'t move cursor back (1)');
+ helpers.doKeys('g','j');
+ helpers.assertCursorAt(0, 176);
+ //should move to character 177 on line 2 (j/k preserve character index within line)
+ helpers.doKeys('j');
+ //due to different line wrapping, the cursor can be on a different screen-x now
+ //gj and gk preserve screen-x on movement, much like moveV
+ helpers.doKeys('3','g','k');
+ cur=cm.getCursor();
+ eq(cur.line,1);
+ is((cur.ch<176),'gk didn\'t move cursor back (2)');
+ helpers.doKeys('g','j','2','g','j');
+ //should return to the same character-index
+ helpers.doKeys('k');
+ helpers.assertCursorAt(0, 176);
+},{ lineWrapping:true, value: 'This line is intentially long to test movement of gj and gk over wrapped lines. I will start on the end of this line, then make a step up and back to set the origin for j and k.\nThis line is supposed to be even longer than the previous. I will jump here and make another wiggle with gj and gk, before I jump back to the line above. Both wiggles should not change my cursor\'s target character but both j/k and gj/gk change each other\'s reference position.'});
+testVim('gj_gk', function(cm, vim, helpers) {
+ if (phantom) return;
+ cm.setSize(120);
+ // Test top of document edge case.
+ cm.setCursor(0, 4);
+ helpers.doKeys('g', 'j');
+ helpers.doKeys('10', 'g', 'k');
+ helpers.assertCursorAt(0, 4);
+
+ // Test moving down preserves column position.
+ helpers.doKeys('g', 'j');
+ var pos1 = cm.getCursor();
+ var expectedPos2 = { line: 0, ch: (pos1.ch - 4) * 2 + 4};
+ helpers.doKeys('g', 'j');
+ helpers.assertCursorAt(expectedPos2);
+
+ // Move to the last character
+ cm.setCursor(0, 0);
+ // Move left to reset HSPos
+ helpers.doKeys('h');
+ // Test bottom of document edge case.
+ helpers.doKeys('100', 'g', 'j');
+ var endingPos = cm.getCursor();
+ is(endingPos != 0, 'gj should not be on wrapped line 0');
+ var topLeftCharCoords = cm.charCoords(makeCursor(0, 0));
+ var endingCharCoords = cm.charCoords(endingPos);
+ is(topLeftCharCoords.left == endingCharCoords.left, 'gj should end up on column 0');
+},{ lineNumbers: false, lineWrapping:true, value: 'Thislineisintentionallylongtotestmovementofgjandgkoverwrappedlines.' });
+testVim('}', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('}');
+ helpers.assertCursorAt(1, 0);
+ cm.setCursor(0, 0);
+ helpers.doKeys('2', '}');
+ helpers.assertCursorAt(4, 0);
+ cm.setCursor(0, 0);
+ helpers.doKeys('6', '}');
+ helpers.assertCursorAt(5, 0);
+}, { value: 'a\n\nb\nc\n\nd' });
+testVim('{', function(cm, vim, helpers) {
+ cm.setCursor(5, 0);
+ helpers.doKeys('{');
+ helpers.assertCursorAt(4, 0);
+ cm.setCursor(5, 0);
+ helpers.doKeys('2', '{');
+ helpers.assertCursorAt(1, 0);
+ cm.setCursor(5, 0);
+ helpers.doKeys('6', '{');
+ helpers.assertCursorAt(0, 0);
+}, { value: 'a\n\nb\nc\n\nd' });
+testVim('paragraph_motions', function(cm, vim, helpers) {
+ cm.setCursor(10, 0);
+ helpers.doKeys('{');
+ helpers.assertCursorAt(4, 0);
+ helpers.doKeys('{');
+ helpers.assertCursorAt(0, 0);
+ helpers.doKeys('2', '}');
+ helpers.assertCursorAt(7, 0);
+ helpers.doKeys('2', '}');
+ helpers.assertCursorAt(16, 0);
+
+ cm.setCursor(9, 0);
+ helpers.doKeys('}');
+ helpers.assertCursorAt(14, 0);
+
+ cm.setCursor(6, 0);
+ helpers.doKeys('}');
+ helpers.assertCursorAt(7, 0);
+
+ // ip inside empty space
+ cm.setCursor(10, 0);
+ helpers.doKeys('v', 'i', 'p');
+ eqPos(Pos(7, 0), cm.getCursor('anchor'));
+ eqPos(Pos(12, 0), cm.getCursor('head'));
+ helpers.doKeys('i', 'p');
+ eqPos(Pos(7, 0), cm.getCursor('anchor'));
+ eqPos(Pos(13, 1), cm.getCursor('head'));
+ helpers.doKeys('2', 'i', 'p');
+ eqPos(Pos(7, 0), cm.getCursor('anchor'));
+ eqPos(Pos(16, 1), cm.getCursor('head'));
+
+ // should switch to visualLine mode
+ cm.setCursor(14, 0);
+ helpers.doKeys('<Esc>', 'v', 'i', 'p');
+ helpers.assertCursorAt(14, 0);
+
+ cm.setCursor(14, 0);
+ helpers.doKeys('<Esc>', 'V', 'i', 'p');
+ eqPos(Pos(16, 1), cm.getCursor('head'));
+
+ // ap inside empty space
+ cm.setCursor(10, 0);
+ helpers.doKeys('<Esc>', 'v', 'a', 'p');
+ eqPos(Pos(7, 0), cm.getCursor('anchor'));
+ eqPos(Pos(13, 1), cm.getCursor('head'));
+ helpers.doKeys('a', 'p');
+ eqPos(Pos(7, 0), cm.getCursor('anchor'));
+ eqPos(Pos(16, 1), cm.getCursor('head'));
+
+ cm.setCursor(13, 0);
+ helpers.doKeys('v', 'a', 'p');
+ eqPos(Pos(13, 0), cm.getCursor('anchor'));
+ eqPos(Pos(14, 0), cm.getCursor('head'));
+
+ cm.setCursor(16, 0);
+ helpers.doKeys('v', 'a', 'p');
+ eqPos(Pos(14, 0), cm.getCursor('anchor'));
+ eqPos(Pos(16, 1), cm.getCursor('head'));
+
+ cm.setCursor(0, 0);
+ helpers.doKeys('v', 'a', 'p');
+ eqPos(Pos(0, 0), cm.getCursor('anchor'));
+ eqPos(Pos(4, 0), cm.getCursor('head'));
+
+ cm.setCursor(0, 0);
+ helpers.doKeys('d', 'i', 'p');
+ var register = helpers.getRegisterController().getRegister();
+ eq('a\na\n', register.toString());
+ is(register.linewise);
+ helpers.doKeys('3', 'j', 'p');
+ helpers.doKeys('y', 'i', 'p');
+ is(register.linewise);
+ eq('b\na\na\nc\n', register.toString());
+}, { value: 'a\na\n\n\n\nb\nc\n\n\n\n\n\n\nd\n\ne\nf' });
+
+// Operator tests
+testVim('dl', function(cm, vim, helpers) {
+ var curStart = makeCursor(0, 0);
+ cm.setCursor(curStart);
+ helpers.doKeys('d', 'l');
+ eq('word1 ', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq(' ', register.toString());
+ is(!register.linewise);
+ eqPos(curStart, cm.getCursor());
+}, { value: ' word1 ' });
+testVim('dl_eol', function(cm, vim, helpers) {
+ cm.setCursor(0, 6);
+ helpers.doKeys('d', 'l');
+ eq(' word1', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq(' ', register.toString());
+ is(!register.linewise);
+ helpers.assertCursorAt(0, 5);
+}, { value: ' word1 ' });
+testVim('dl_repeat', function(cm, vim, helpers) {
+ var curStart = makeCursor(0, 0);
+ cm.setCursor(curStart);
+ helpers.doKeys('2', 'd', 'l');
+ eq('ord1 ', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq(' w', register.toString());
+ is(!register.linewise);
+ eqPos(curStart, cm.getCursor());
+}, { value: ' word1 ' });
+testVim('dh', function(cm, vim, helpers) {
+ var curStart = makeCursor(0, 3);
+ cm.setCursor(curStart);
+ helpers.doKeys('d', 'h');
+ eq(' wrd1 ', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq('o', register.toString());
+ is(!register.linewise);
+ eqPos(offsetCursor(curStart, 0 , -1), cm.getCursor());
+}, { value: ' word1 ' });
+testVim('dj', function(cm, vim, helpers) {
+ var curStart = makeCursor(0, 3);
+ cm.setCursor(curStart);
+ helpers.doKeys('d', 'j');
+ eq(' word3', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq(' word1\nword2\n', register.toString());
+ is(register.linewise);
+ helpers.assertCursorAt(0, 1);
+}, { value: ' word1\nword2\n word3' });
+testVim('dj_end_of_document', function(cm, vim, helpers) {
+ var curStart = makeCursor(0, 3);
+ cm.setCursor(curStart);
+ helpers.doKeys('d', 'j');
+ eq('', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq(' word1 \n', register.toString());
+ is(register.linewise);
+ helpers.assertCursorAt(0, 0);
+}, { value: ' word1 ' });
+testVim('dk', function(cm, vim, helpers) {
+ var curStart = makeCursor(1, 3);
+ cm.setCursor(curStart);
+ helpers.doKeys('d', 'k');
+ eq(' word3', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq(' word1\nword2\n', register.toString());
+ is(register.linewise);
+ helpers.assertCursorAt(0, 1);
+}, { value: ' word1\nword2\n word3' });
+testVim('dk_start_of_document', function(cm, vim, helpers) {
+ var curStart = makeCursor(0, 3);
+ cm.setCursor(curStart);
+ helpers.doKeys('d', 'k');
+ eq('', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq(' word1 \n', register.toString());
+ is(register.linewise);
+ helpers.assertCursorAt(0, 0);
+}, { value: ' word1 ' });
+testVim('dw_space', function(cm, vim, helpers) {
+ var curStart = makeCursor(0, 0);
+ cm.setCursor(curStart);
+ helpers.doKeys('d', 'w');
+ eq('word1 ', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq(' ', register.toString());
+ is(!register.linewise);
+ eqPos(curStart, cm.getCursor());
+}, { value: ' word1 ' });
+testVim('dw_word', function(cm, vim, helpers) {
+ var curStart = makeCursor(0, 1);
+ cm.setCursor(curStart);
+ helpers.doKeys('d', 'w');
+ eq(' word2', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq('word1 ', register.toString());
+ is(!register.linewise);
+ eqPos(curStart, cm.getCursor());
+}, { value: ' word1 word2' });
+testVim('dw_unicode_word', function(cm, vim, helpers) {
+ helpers.doKeys('d', 'w');
+ eq(cm.getValue().length, 10);
+ helpers.doKeys('d', 'w');
+ eq(cm.getValue().length, 6);
+ helpers.doKeys('d', 'w');
+ eq(cm.getValue().length, 5);
+ helpers.doKeys('d', 'e');
+ eq(cm.getValue().length, 2);
+}, { value: ' \u0562\u0561\u0580\u0587\xbbe\xb5g ' });
+testVim('dw_only_word', function(cm, vim, helpers) {
+ // Test that if there is only 1 word left, dw deletes till the end of the
+ // line.
+ cm.setCursor(0, 1);
+ helpers.doKeys('d', 'w');
+ eq(' ', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq('word1 ', register.toString());
+ is(!register.linewise);
+ helpers.assertCursorAt(0, 0);
+}, { value: ' word1 ' });
+testVim('dw_eol', function(cm, vim, helpers) {
+ // Assert that dw does not delete the newline if last word to delete is at end
+ // of line.
+ cm.setCursor(0, 1);
+ helpers.doKeys('d', 'w');
+ eq(' \nword2', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq('word1', register.toString());
+ is(!register.linewise);
+ helpers.assertCursorAt(0, 0);
+}, { value: ' word1\nword2' });
+testVim('dw_eol_with_multiple_newlines', function(cm, vim, helpers) {
+ // Assert that dw does not delete the newline if last word to delete is at end
+ // of line and it is followed by multiple newlines.
+ cm.setCursor(0, 1);
+ helpers.doKeys('d', 'w');
+ eq(' \n\nword2', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq('word1', register.toString());
+ is(!register.linewise);
+ helpers.assertCursorAt(0, 0);
+}, { value: ' word1\n\nword2' });
+testVim('dw_empty_line_followed_by_whitespace', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('d', 'w');
+ eq(' \nword', cm.getValue());
+}, { value: '\n \nword' });
+testVim('dw_empty_line_followed_by_word', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('d', 'w');
+ eq('word', cm.getValue());
+}, { value: '\nword' });
+testVim('dw_empty_line_followed_by_empty_line', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('d', 'w');
+ eq('\n', cm.getValue());
+}, { value: '\n\n' });
+testVim('dw_whitespace_followed_by_whitespace', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('d', 'w');
+ eq('\n \n', cm.getValue());
+}, { value: ' \n \n' });
+testVim('dw_whitespace_followed_by_empty_line', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('d', 'w');
+ eq('\n\n', cm.getValue());
+}, { value: ' \n\n' });
+testVim('dw_word_whitespace_word', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('d', 'w');
+ eq('\n \nword2', cm.getValue());
+}, { value: 'word1\n \nword2'})
+testVim('dw_end_of_document', function(cm, vim, helpers) {
+ cm.setCursor(1, 2);
+ helpers.doKeys('d', 'w');
+ eq('\nab', cm.getValue());
+}, { value: '\nabc' });
+testVim('dw_repeat', function(cm, vim, helpers) {
+ // Assert that dw does delete newline if it should go to the next line, and
+ // that repeat works properly.
+ cm.setCursor(0, 1);
+ helpers.doKeys('d', '2', 'w');
+ eq(' ', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq('word1\nword2', register.toString());
+ is(!register.linewise);
+ helpers.assertCursorAt(0, 0);
+}, { value: ' word1\nword2' });
+testVim('de_word_start_and_empty_lines', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('d', 'e');
+ eq('\n\n', cm.getValue());
+}, { value: 'word\n\n' });
+testVim('de_word_end_and_empty_lines', function(cm, vim, helpers) {
+ cm.setCursor(0, 3);
+ helpers.doKeys('d', 'e');
+ eq('wor', cm.getValue());
+}, { value: 'word\n\n\n' });
+testVim('de_whitespace_and_empty_lines', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('d', 'e');
+ eq('', cm.getValue());
+}, { value: ' \n\n\n' });
+testVim('de_end_of_document', function(cm, vim, helpers) {
+ cm.setCursor(1, 2);
+ helpers.doKeys('d', 'e');
+ eq('\nab', cm.getValue());
+}, { value: '\nabc' });
+testVim('db_empty_lines', function(cm, vim, helpers) {
+ cm.setCursor(2, 0);
+ helpers.doKeys('d', 'b');
+ eq('\n\n', cm.getValue());
+}, { value: '\n\n\n' });
+testVim('db_word_start_and_empty_lines', function(cm, vim, helpers) {
+ cm.setCursor(2, 0);
+ helpers.doKeys('d', 'b');
+ eq('\nword', cm.getValue());
+}, { value: '\n\nword' });
+testVim('db_word_end_and_empty_lines', function(cm, vim, helpers) {
+ cm.setCursor(2, 3);
+ helpers.doKeys('d', 'b');
+ eq('\n\nd', cm.getValue());
+}, { value: '\n\nword' });
+testVim('db_whitespace_and_empty_lines', function(cm, vim, helpers) {
+ cm.setCursor(2, 0);
+ helpers.doKeys('d', 'b');
+ eq('', cm.getValue());
+}, { value: '\n \n' });
+testVim('db_start_of_document', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('d', 'b');
+ eq('abc\n', cm.getValue());
+}, { value: 'abc\n' });
+testVim('dge_empty_lines', function(cm, vim, helpers) {
+ cm.setCursor(1, 0);
+ helpers.doKeys('d', 'g', 'e');
+ // Note: In real VIM the result should be '', but it's not quite consistent,
+ // since 2 newlines are deleted. But in the similar case of word\n\n, only
+ // 1 newline is deleted. We'll diverge from VIM's behavior since it's much
+ // easier this way.
+ eq('\n', cm.getValue());
+}, { value: '\n\n' });
+testVim('dge_word_and_empty_lines', function(cm, vim, helpers) {
+ cm.setCursor(1, 0);
+ helpers.doKeys('d', 'g', 'e');
+ eq('wor\n', cm.getValue());
+}, { value: 'word\n\n'});
+testVim('dge_whitespace_and_empty_lines', function(cm, vim, helpers) {
+ cm.setCursor(2, 0);
+ helpers.doKeys('d', 'g', 'e');
+ eq('', cm.getValue());
+}, { value: '\n \n' });
+testVim('dge_start_of_document', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('d', 'g', 'e');
+ eq('bc\n', cm.getValue());
+}, { value: 'abc\n' });
+testVim('d_inclusive', function(cm, vim, helpers) {
+ // Assert that when inclusive is set, the character the cursor is on gets
+ // deleted too.
+ var curStart = makeCursor(0, 1);
+ cm.setCursor(curStart);
+ helpers.doKeys('d', 'e');
+ eq(' ', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq('word1', register.toString());
+ is(!register.linewise);
+ eqPos(curStart, cm.getCursor());
+}, { value: ' word1 ' });
+testVim('d_reverse', function(cm, vim, helpers) {
+ // Test that deleting in reverse works.
+ cm.setCursor(1, 0);
+ helpers.doKeys('d', 'b');
+ eq(' word2 ', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq('word1\n', register.toString());
+ is(!register.linewise);
+ helpers.assertCursorAt(0, 1);
+}, { value: ' word1\nword2 ' });
+testVim('dd', function(cm, vim, helpers) {
+ cm.setCursor(0, 3);
+ var expectedBuffer = cm.getRange({ line: 0, ch: 0 },
+ { line: 1, ch: 0 });
+ var expectedLineCount = cm.lineCount() - 1;
+ helpers.doKeys('d', 'd');
+ eq(expectedLineCount, cm.lineCount());
+ var register = helpers.getRegisterController().getRegister();
+ eq(expectedBuffer, register.toString());
+ is(register.linewise);
+ helpers.assertCursorAt(0, lines[1].textStart);
+});
+testVim('dd_prefix_repeat', function(cm, vim, helpers) {
+ cm.setCursor(0, 3);
+ var expectedBuffer = cm.getRange({ line: 0, ch: 0 },
+ { line: 2, ch: 0 });
+ var expectedLineCount = cm.lineCount() - 2;
+ helpers.doKeys('2', 'd', 'd');
+ eq(expectedLineCount, cm.lineCount());
+ var register = helpers.getRegisterController().getRegister();
+ eq(expectedBuffer, register.toString());
+ is(register.linewise);
+ helpers.assertCursorAt(0, lines[2].textStart);
+});
+testVim('dd_motion_repeat', function(cm, vim, helpers) {
+ cm.setCursor(0, 3);
+ var expectedBuffer = cm.getRange({ line: 0, ch: 0 },
+ { line: 2, ch: 0 });
+ var expectedLineCount = cm.lineCount() - 2;
+ helpers.doKeys('d', '2', 'd');
+ eq(expectedLineCount, cm.lineCount());
+ var register = helpers.getRegisterController().getRegister();
+ eq(expectedBuffer, register.toString());
+ is(register.linewise);
+ helpers.assertCursorAt(0, lines[2].textStart);
+});
+testVim('dd_multiply_repeat', function(cm, vim, helpers) {
+ cm.setCursor(0, 3);
+ var expectedBuffer = cm.getRange({ line: 0, ch: 0 },
+ { line: 6, ch: 0 });
+ var expectedLineCount = cm.lineCount() - 6;
+ helpers.doKeys('2', 'd', '3', 'd');
+ eq(expectedLineCount, cm.lineCount());
+ var register = helpers.getRegisterController().getRegister();
+ eq(expectedBuffer, register.toString());
+ is(register.linewise);
+ helpers.assertCursorAt(0, lines[6].textStart);
+});
+testVim('dd_lastline', function(cm, vim, helpers) {
+ cm.setCursor(cm.lineCount(), 0);
+ var expectedLineCount = cm.lineCount() - 1;
+ helpers.doKeys('d', 'd');
+ eq(expectedLineCount, cm.lineCount());
+ helpers.assertCursorAt(cm.lineCount() - 1, 0);
+});
+testVim('dd_only_line', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ var expectedRegister = cm.getValue() + "\n";
+ helpers.doKeys('d','d');
+ eq(1, cm.lineCount());
+ eq('', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq(expectedRegister, register.toString());
+}, { value: "thisistheonlyline" });
+// Yank commands should behave the exact same as d commands, expect that nothing
+// gets deleted.
+testVim('yw_repeat', function(cm, vim, helpers) {
+ // Assert that yw does yank newline if it should go to the next line, and
+ // that repeat works properly.
+ var curStart = makeCursor(0, 1);
+ cm.setCursor(curStart);
+ helpers.doKeys('y', '2', 'w');
+ eq(' word1\nword2', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq('word1\nword2', register.toString());
+ is(!register.linewise);
+ eqPos(curStart, cm.getCursor());
+}, { value: ' word1\nword2' });
+testVim('yy_multiply_repeat', function(cm, vim, helpers) {
+ var curStart = makeCursor(0, 3);
+ cm.setCursor(curStart);
+ var expectedBuffer = cm.getRange({ line: 0, ch: 0 },
+ { line: 6, ch: 0 });
+ var expectedLineCount = cm.lineCount();
+ helpers.doKeys('2', 'y', '3', 'y');
+ eq(expectedLineCount, cm.lineCount());
+ var register = helpers.getRegisterController().getRegister();
+ eq(expectedBuffer, register.toString());
+ is(register.linewise);
+ eqPos(curStart, cm.getCursor());
+});
+// Change commands behave like d commands except that it also enters insert
+// mode. In addition, when the change is linewise, an additional newline is
+// inserted so that insert mode starts on that line.
+testVim('cw', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('c', '2', 'w');
+ eq(' word3', cm.getValue());
+ helpers.assertCursorAt(0, 0);
+}, { value: 'word1 word2 word3'});
+testVim('cw_repeat', function(cm, vim, helpers) {
+ // Assert that cw does delete newline if it should go to the next line, and
+ // that repeat works properly.
+ var curStart = makeCursor(0, 1);
+ cm.setCursor(curStart);
+ helpers.doKeys('c', '2', 'w');
+ eq(' ', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq('word1\nword2', register.toString());
+ is(!register.linewise);
+ eqPos(curStart, cm.getCursor());
+ eq('vim-insert', cm.getOption('keyMap'));
+}, { value: ' word1\nword2' });
+testVim('cc_multiply_repeat', function(cm, vim, helpers) {
+ cm.setCursor(0, 3);
+ var expectedBuffer = cm.getRange({ line: 0, ch: 0 },
+ { line: 6, ch: 0 });
+ var expectedLineCount = cm.lineCount() - 5;
+ helpers.doKeys('2', 'c', '3', 'c');
+ eq(expectedLineCount, cm.lineCount());
+ var register = helpers.getRegisterController().getRegister();
+ eq(expectedBuffer, register.toString());
+ is(register.linewise);
+ eq('vim-insert', cm.getOption('keyMap'));
+});
+testVim('ct', function(cm, vim, helpers) {
+ cm.setCursor(0, 9);
+ helpers.doKeys('c', 't', 'w');
+ eq(' word1 word3', cm.getValue());
+ helpers.doKeys('<Esc>', 'c', '|');
+ eq(' word3', cm.getValue());
+ helpers.assertCursorAt(0, 0);
+ helpers.doKeys('<Esc>', '2', 'u', 'w', 'h');
+ helpers.doKeys('c', '2', 'g', 'e');
+ eq(' wordword3', cm.getValue());
+}, { value: ' word1 word2 word3'});
+testVim('cc_should_not_append_to_document', function(cm, vim, helpers) {
+ var expectedLineCount = cm.lineCount();
+ cm.setCursor(cm.lastLine(), 0);
+ helpers.doKeys('c', 'c');
+ eq(expectedLineCount, cm.lineCount());
+});
+function fillArray(val, times) {
+ var arr = [];
+ for (var i = 0; i < times; i++) {
+ arr.push(val);
+ }
+ return arr;
+}
+testVim('c_visual_block', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.doKeys('<C-v>', '2', 'j', 'l', 'l', 'l', 'c');
+ var replacement = fillArray('hello', 3);
+ cm.replaceSelections(replacement);
+ eq('1hello\n5hello\nahellofg', cm.getValue());
+ helpers.doKeys('<Esc>');
+ cm.setCursor(2, 3);
+ helpers.doKeys('<C-v>', '2', 'k', 'h', 'C');
+ replacement = fillArray('world', 3);
+ cm.replaceSelections(replacement);
+ eq('1hworld\n5hworld\nahworld', cm.getValue());
+}, {value: '1234\n5678\nabcdefg'});
+testVim('c_visual_block_replay', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.doKeys('<C-v>', '2', 'j', 'l', 'c');
+ var replacement = fillArray('fo', 3);
+ cm.replaceSelections(replacement);
+ eq('1fo4\n5fo8\nafodefg', cm.getValue());
+ helpers.doKeys('<Esc>');
+ cm.setCursor(0, 0);
+ helpers.doKeys('.');
+ eq('foo4\nfoo8\nfoodefg', cm.getValue());
+}, {value: '1234\n5678\nabcdefg'});
+
+testVim('d_visual_block', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.doKeys('<C-v>', '2', 'j', 'l', 'l', 'l', 'd');
+ eq('1\n5\nafg', cm.getValue());
+}, {value: '1234\n5678\nabcdefg'});
+testVim('D_visual_block', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.doKeys('<C-v>', '2', 'j', 'l', 'D');
+ eq('1\n5\na', cm.getValue());
+}, {value: '1234\n5678\nabcdefg'});
+
+testVim('s_visual_block', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.doKeys('<C-v>', '2', 'j', 'l', 'l', 'l', 's');
+ var replacement = fillArray('hello{', 3);
+ cm.replaceSelections(replacement);
+ eq('1hello{\n5hello{\nahello{fg\n', cm.getValue());
+ helpers.doKeys('<Esc>');
+ cm.setCursor(2, 3);
+ helpers.doKeys('<C-v>', '1', 'k', 'h', 'S');
+ replacement = fillArray('world', 1);
+ cm.replaceSelections(replacement);
+ eq('1hello{\n world\n', cm.getValue());
+}, {value: '1234\n5678\nabcdefg\n'});
+
+// Swapcase commands edit in place and do not modify registers.
+testVim('g~w_repeat', function(cm, vim, helpers) {
+ // Assert that dw does delete newline if it should go to the next line, and
+ // that repeat works properly.
+ var curStart = makeCursor(0, 1);
+ cm.setCursor(curStart);
+ helpers.doKeys('g', '~', '2', 'w');
+ eq(' WORD1\nWORD2', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq('', register.toString());
+ is(!register.linewise);
+ eqPos(curStart, cm.getCursor());
+}, { value: ' word1\nword2' });
+testVim('g~g~', function(cm, vim, helpers) {
+ var curStart = makeCursor(0, 3);
+ cm.setCursor(curStart);
+ var expectedLineCount = cm.lineCount();
+ var expectedValue = cm.getValue().toUpperCase();
+ helpers.doKeys('2', 'g', '~', '3', 'g', '~');
+ eq(expectedValue, cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq('', register.toString());
+ is(!register.linewise);
+ eqPos(curStart, cm.getCursor());
+}, { value: ' word1\nword2\nword3\nword4\nword5\nword6' });
+testVim('gu_and_gU', function(cm, vim, helpers) {
+ var curStart = makeCursor(0, 7);
+ var value = cm.getValue();
+ cm.setCursor(curStart);
+ helpers.doKeys('2', 'g', 'U', 'w');
+ eq(cm.getValue(), 'wa wb xX WC wd');
+ eqPos(curStart, cm.getCursor());
+ helpers.doKeys('2', 'g', 'u', 'w');
+ eq(cm.getValue(), value);
+
+ helpers.doKeys('2', 'g', 'U', 'B');
+ eq(cm.getValue(), 'wa WB Xx wc wd');
+ eqPos(makeCursor(0, 3), cm.getCursor());
+
+ cm.setCursor(makeCursor(0, 4));
+ helpers.doKeys('g', 'u', 'i', 'w');
+ eq(cm.getValue(), 'wa wb Xx wc wd');
+ eqPos(makeCursor(0, 3), cm.getCursor());
+
+ // TODO: support gUgU guu
+ // eqPos(makeCursor(0, 0), cm.getCursor());
+
+ var register = helpers.getRegisterController().getRegister();
+ eq('', register.toString());
+ is(!register.linewise);
+}, { value: 'wa wb xx wc wd' });
+testVim('visual_block_~', function(cm, vim, helpers) {
+ cm.setCursor(1, 1);
+ helpers.doKeys('<C-v>', 'l', 'l', 'j', '~');
+ helpers.assertCursorAt(1, 1);
+ eq('hello\nwoRLd\naBCDe', cm.getValue());
+ cm.setCursor(2, 0);
+ helpers.doKeys('v', 'l', 'l', '~');
+ helpers.assertCursorAt(2, 0);
+ eq('hello\nwoRLd\nAbcDe', cm.getValue());
+},{value: 'hello\nwOrld\nabcde' });
+testVim('._swapCase_visualBlock', function(cm, vim, helpers) {
+ helpers.doKeys('<C-v>', 'j', 'j', 'l', '~');
+ cm.setCursor(0, 3);
+ helpers.doKeys('.');
+ eq('HelLO\nWorLd\nAbcdE', cm.getValue());
+},{value: 'hEllo\nwOrlD\naBcDe' });
+testVim('._delete_visualBlock', function(cm, vim, helpers) {
+ helpers.doKeys('<C-v>', 'j', 'x');
+ eq('ive\ne\nsome\nsugar', cm.getValue());
+ helpers.doKeys('.');
+ eq('ve\n\nsome\nsugar', cm.getValue());
+ helpers.doKeys('j', 'j', '.');
+ eq('ve\n\nome\nugar', cm.getValue());
+ helpers.doKeys('u', '<C-r>', '.');
+ eq('ve\n\nme\ngar', cm.getValue());
+},{value: 'give\nme\nsome\nsugar' });
+testVim('>{motion}', function(cm, vim, helpers) {
+ cm.setCursor(1, 3);
+ var expectedLineCount = cm.lineCount();
+ var expectedValue = ' word1\n word2\nword3 ';
+ helpers.doKeys('>', 'k');
+ eq(expectedValue, cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq('', register.toString());
+ is(!register.linewise);
+ helpers.assertCursorAt(0, 3);
+}, { value: ' word1\nword2\nword3 ', indentUnit: 2 });
+testVim('>>', function(cm, vim, helpers) {
+ cm.setCursor(0, 3);
+ var expectedLineCount = cm.lineCount();
+ var expectedValue = ' word1\n word2\nword3 ';
+ helpers.doKeys('2', '>', '>');
+ eq(expectedValue, cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq('', register.toString());
+ is(!register.linewise);
+ helpers.assertCursorAt(0, 3);
+}, { value: ' word1\nword2\nword3 ', indentUnit: 2 });
+testVim('<{motion}', function(cm, vim, helpers) {
+ cm.setCursor(1, 3);
+ var expectedLineCount = cm.lineCount();
+ var expectedValue = ' word1\nword2\nword3 ';
+ helpers.doKeys('<', 'k');
+ eq(expectedValue, cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq('', register.toString());
+ is(!register.linewise);
+ helpers.assertCursorAt(0, 1);
+}, { value: ' word1\n word2\nword3 ', indentUnit: 2 });
+testVim('<<', function(cm, vim, helpers) {
+ cm.setCursor(0, 3);
+ var expectedLineCount = cm.lineCount();
+ var expectedValue = ' word1\nword2\nword3 ';
+ helpers.doKeys('2', '<', '<');
+ eq(expectedValue, cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq('', register.toString());
+ is(!register.linewise);
+ helpers.assertCursorAt(0, 1);
+}, { value: ' word1\n word2\nword3 ', indentUnit: 2 });
+
+// Edit tests
+function testEdit(name, before, pos, edit, after) {
+ return testVim(name, function(cm, vim, helpers) {
+ var ch = before.search(pos)
+ var line = before.substring(0, ch).split('\n').length - 1;
+ if (line) {
+ ch = before.substring(0, ch).split('\n').pop().length;
+ }
+ cm.setCursor(line, ch);
+ helpers.doKeys.apply(this, edit.split(''));
+ eq(after, cm.getValue());
+ }, {value: before});
+}
+
+// These Delete tests effectively cover word-wise Change, Visual & Yank.
+// Tabs are used as differentiated whitespace to catch edge cases.
+// Normal word:
+testEdit('diw_mid_spc', 'foo \tbAr\t baz', /A/, 'diw', 'foo \t\t baz');
+testEdit('daw_mid_spc', 'foo \tbAr\t baz', /A/, 'daw', 'foo \tbaz');
+testEdit('diw_mid_punct', 'foo \tbAr.\t baz', /A/, 'diw', 'foo \t.\t baz');
+testEdit('daw_mid_punct', 'foo \tbAr.\t baz', /A/, 'daw', 'foo.\t baz');
+testEdit('diw_mid_punct2', 'foo \t,bAr.\t baz', /A/, 'diw', 'foo \t,.\t baz');
+testEdit('daw_mid_punct2', 'foo \t,bAr.\t baz', /A/, 'daw', 'foo \t,.\t baz');
+testEdit('diw_start_spc', 'bAr \tbaz', /A/, 'diw', ' \tbaz');
+testEdit('daw_start_spc', 'bAr \tbaz', /A/, 'daw', 'baz');
+testEdit('diw_start_punct', 'bAr. \tbaz', /A/, 'diw', '. \tbaz');
+testEdit('daw_start_punct', 'bAr. \tbaz', /A/, 'daw', '. \tbaz');
+testEdit('diw_end_spc', 'foo \tbAr', /A/, 'diw', 'foo \t');
+testEdit('daw_end_spc', 'foo \tbAr', /A/, 'daw', 'foo');
+testEdit('diw_end_punct', 'foo \tbAr.', /A/, 'diw', 'foo \t.');
+testEdit('daw_end_punct', 'foo \tbAr.', /A/, 'daw', 'foo.');
+// Big word:
+testEdit('diW_mid_spc', 'foo \tbAr\t baz', /A/, 'diW', 'foo \t\t baz');
+testEdit('daW_mid_spc', 'foo \tbAr\t baz', /A/, 'daW', 'foo \tbaz');
+testEdit('diW_mid_punct', 'foo \tbAr.\t baz', /A/, 'diW', 'foo \t\t baz');
+testEdit('daW_mid_punct', 'foo \tbAr.\t baz', /A/, 'daW', 'foo \tbaz');
+testEdit('diW_mid_punct2', 'foo \t,bAr.\t baz', /A/, 'diW', 'foo \t\t baz');
+testEdit('daW_mid_punct2', 'foo \t,bAr.\t baz', /A/, 'daW', 'foo \tbaz');
+testEdit('diW_start_spc', 'bAr\t baz', /A/, 'diW', '\t baz');
+testEdit('daW_start_spc', 'bAr\t baz', /A/, 'daW', 'baz');
+testEdit('diW_start_punct', 'bAr.\t baz', /A/, 'diW', '\t baz');
+testEdit('daW_start_punct', 'bAr.\t baz', /A/, 'daW', 'baz');
+testEdit('diW_end_spc', 'foo \tbAr', /A/, 'diW', 'foo \t');
+testEdit('daW_end_spc', 'foo \tbAr', /A/, 'daW', 'foo');
+testEdit('diW_end_punct', 'foo \tbAr.', /A/, 'diW', 'foo \t');
+testEdit('daW_end_punct', 'foo \tbAr.', /A/, 'daW', 'foo');
+// Deleting text objects
+// Open and close on same line
+testEdit('di(_open_spc', 'foo (bAr) baz', /\(/, 'di(', 'foo () baz');
+testEdit('di)_open_spc', 'foo (bAr) baz', /\(/, 'di)', 'foo () baz');
+testEdit('dib_open_spc', 'foo (bAr) baz', /\(/, 'dib', 'foo () baz');
+testEdit('da(_open_spc', 'foo (bAr) baz', /\(/, 'da(', 'foo baz');
+testEdit('da)_open_spc', 'foo (bAr) baz', /\(/, 'da)', 'foo baz');
+
+testEdit('di(_middle_spc', 'foo (bAr) baz', /A/, 'di(', 'foo () baz');
+testEdit('di)_middle_spc', 'foo (bAr) baz', /A/, 'di)', 'foo () baz');
+testEdit('da(_middle_spc', 'foo (bAr) baz', /A/, 'da(', 'foo baz');
+testEdit('da)_middle_spc', 'foo (bAr) baz', /A/, 'da)', 'foo baz');
+
+testEdit('di(_close_spc', 'foo (bAr) baz', /\)/, 'di(', 'foo () baz');
+testEdit('di)_close_spc', 'foo (bAr) baz', /\)/, 'di)', 'foo () baz');
+testEdit('da(_close_spc', 'foo (bAr) baz', /\)/, 'da(', 'foo baz');
+testEdit('da)_close_spc', 'foo (bAr) baz', /\)/, 'da)', 'foo baz');
+
+// delete around and inner b.
+testEdit('dab_on_(_should_delete_around_()block', 'o( in(abc) )', /\(a/, 'dab', 'o( in )');
+
+// delete around and inner B.
+testEdit('daB_on_{_should_delete_around_{}block', 'o{ in{abc} }', /{a/, 'daB', 'o{ in }');
+testEdit('diB_on_{_should_delete_inner_{}block', 'o{ in{abc} }', /{a/, 'diB', 'o{ in{} }');
+
+testEdit('da{_on_{_should_delete_inner_block', 'o{ in{abc} }', /{a/, 'da{', 'o{ in }');
+testEdit('di[_on_(_should_not_delete', 'foo (bAr) baz', /\(/, 'di[', 'foo (bAr) baz');
+testEdit('di[_on_)_should_not_delete', 'foo (bAr) baz', /\)/, 'di[', 'foo (bAr) baz');
+testEdit('da[_on_(_should_not_delete', 'foo (bAr) baz', /\(/, 'da[', 'foo (bAr) baz');
+testEdit('da[_on_)_should_not_delete', 'foo (bAr) baz', /\)/, 'da[', 'foo (bAr) baz');
+testMotion('di(_outside_should_stay', ['d', 'i', '('], { line: 0, ch: 0}, { line: 0, ch: 0});
+
+// Open and close on different lines, equally indented
+testEdit('di{_middle_spc', 'a{\n\tbar\n}b', /r/, 'di{', 'a{}b');
+testEdit('di}_middle_spc', 'a{\n\tbar\n}b', /r/, 'di}', 'a{}b');
+testEdit('da{_middle_spc', 'a{\n\tbar\n}b', /r/, 'da{', 'ab');
+testEdit('da}_middle_spc', 'a{\n\tbar\n}b', /r/, 'da}', 'ab');
+testEdit('daB_middle_spc', 'a{\n\tbar\n}b', /r/, 'daB', 'ab');
+
+// open and close on diff lines, open indented less than close
+testEdit('di{_middle_spc', 'a{\n\tbar\n\t}b', /r/, 'di{', 'a{}b');
+testEdit('di}_middle_spc', 'a{\n\tbar\n\t}b', /r/, 'di}', 'a{}b');
+testEdit('da{_middle_spc', 'a{\n\tbar\n\t}b', /r/, 'da{', 'ab');
+testEdit('da}_middle_spc', 'a{\n\tbar\n\t}b', /r/, 'da}', 'ab');
+
+// open and close on diff lines, open indented more than close
+testEdit('di[_middle_spc', 'a\t[\n\tbar\n]b', /r/, 'di[', 'a\t[]b');
+testEdit('di]_middle_spc', 'a\t[\n\tbar\n]b', /r/, 'di]', 'a\t[]b');
+testEdit('da[_middle_spc', 'a\t[\n\tbar\n]b', /r/, 'da[', 'a\tb');
+testEdit('da]_middle_spc', 'a\t[\n\tbar\n]b', /r/, 'da]', 'a\tb');
+
+function testSelection(name, before, pos, keys, sel) {
+ return testVim(name, function(cm, vim, helpers) {
+ var ch = before.search(pos)
+ var line = before.substring(0, ch).split('\n').length - 1;
+ if (line) {
+ ch = before.substring(0, ch).split('\n').pop().length;
+ }
+ cm.setCursor(line, ch);
+ helpers.doKeys.apply(this, keys.split(''));
+ eq(sel, cm.getSelection());
+ }, {value: before});
+}
+testSelection('viw_middle_spc', 'foo \tbAr\t baz', /A/, 'viw', 'bAr');
+testSelection('vaw_middle_spc', 'foo \tbAr\t baz', /A/, 'vaw', 'bAr\t ');
+testSelection('viw_middle_punct', 'foo \tbAr,\t baz', /A/, 'viw', 'bAr');
+testSelection('vaW_middle_punct', 'foo \tbAr,\t baz', /A/, 'vaW', 'bAr,\t ');
+testSelection('viw_start_spc', 'foo \tbAr\t baz', /b/, 'viw', 'bAr');
+testSelection('viw_end_spc', 'foo \tbAr\t baz', /r/, 'viw', 'bAr');
+testSelection('viw_eol', 'foo \tbAr', /r/, 'viw', 'bAr');
+testSelection('vi{_middle_spc', 'a{\n\tbar\n\t}b', /r/, 'vi{', '\n\tbar\n\t');
+testSelection('va{_middle_spc', 'a{\n\tbar\n\t}b', /r/, 'va{', '{\n\tbar\n\t}');
+
+testVim('mouse_select', function(cm, vim, helpers) {
+ cm.setSelection(Pos(0, 2), Pos(0, 4), {origin: '*mouse'});
+ is(cm.state.vim.visualMode);
+ is(!cm.state.vim.visualLine);
+ is(!cm.state.vim.visualBlock);
+ helpers.doKeys('<Esc>');
+ is(!cm.somethingSelected());
+ helpers.doKeys('g', 'v');
+ eq('cd', cm.getSelection());
+}, {value: 'abcdef'});
+
+// Operator-motion tests
+testVim('D', function(cm, vim, helpers) {
+ cm.setCursor(0, 3);
+ helpers.doKeys('D');
+ eq(' wo\nword2\n word3', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq('rd1', register.toString());
+ is(!register.linewise);
+ helpers.assertCursorAt(0, 2);
+}, { value: ' word1\nword2\n word3' });
+testVim('C', function(cm, vim, helpers) {
+ var curStart = makeCursor(0, 3);
+ cm.setCursor(curStart);
+ helpers.doKeys('C');
+ eq(' wo\nword2\n word3', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq('rd1', register.toString());
+ is(!register.linewise);
+ eqPos(curStart, cm.getCursor());
+ eq('vim-insert', cm.getOption('keyMap'));
+}, { value: ' word1\nword2\n word3' });
+testVim('Y', function(cm, vim, helpers) {
+ var curStart = makeCursor(0, 3);
+ cm.setCursor(curStart);
+ helpers.doKeys('Y');
+ eq(' word1\nword2\n word3', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq('rd1', register.toString());
+ is(!register.linewise);
+ helpers.assertCursorAt(0, 3);
+}, { value: ' word1\nword2\n word3' });
+testVim('~', function(cm, vim, helpers) {
+ helpers.doKeys('3', '~');
+ eq('ABCdefg', cm.getValue());
+ helpers.assertCursorAt(0, 3);
+}, { value: 'abcdefg' });
+
+// Action tests
+testVim('ctrl-a', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('<C-a>');
+ eq('-9', cm.getValue());
+ helpers.assertCursorAt(0, 1);
+ helpers.doKeys('2','<C-a>');
+ eq('-7', cm.getValue());
+}, {value: '-10'});
+testVim('ctrl-x', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('<C-x>');
+ eq('-1', cm.getValue());
+ helpers.assertCursorAt(0, 1);
+ helpers.doKeys('2','<C-x>');
+ eq('-3', cm.getValue());
+}, {value: '0'});
+testVim('<C-x>/<C-a> search forward', function(cm, vim, helpers) {
+ forEach(['<C-x>', '<C-a>'], function(key) {
+ cm.setCursor(0, 0);
+ helpers.doKeys(key);
+ helpers.assertCursorAt(0, 5);
+ helpers.doKeys('l');
+ helpers.doKeys(key);
+ helpers.assertCursorAt(0, 10);
+ cm.setCursor(0, 11);
+ helpers.doKeys(key);
+ helpers.assertCursorAt(0, 11);
+ });
+}, {value: '__jmp1 jmp2 jmp'});
+testVim('a', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.doKeys('a');
+ helpers.assertCursorAt(0, 2);
+ eq('vim-insert', cm.getOption('keyMap'));
+});
+testVim('a_eol', function(cm, vim, helpers) {
+ cm.setCursor(0, lines[0].length - 1);
+ helpers.doKeys('a');
+ helpers.assertCursorAt(0, lines[0].length);
+ eq('vim-insert', cm.getOption('keyMap'));
+});
+testVim('A_endOfSelectedArea', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('v', 'j', 'l');
+ helpers.doKeys('A');
+ helpers.assertCursorAt(1, 2);
+ eq('vim-insert', cm.getOption('keyMap'));
+}, {value: 'foo\nbar'});
+testVim('i', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.doKeys('i');
+ helpers.assertCursorAt(0, 1);
+ eq('vim-insert', cm.getOption('keyMap'));
+});
+testVim('i_repeat', function(cm, vim, helpers) {
+ helpers.doKeys('3', 'i');
+ cm.replaceRange('test', cm.getCursor());
+ helpers.doKeys('<Esc>');
+ eq('testtesttest', cm.getValue());
+ helpers.assertCursorAt(0, 11);
+}, { value: '' });
+testVim('i_repeat_delete', function(cm, vim, helpers) {
+ cm.setCursor(0, 4);
+ helpers.doKeys('2', 'i');
+ cm.replaceRange('z', cm.getCursor());
+ helpers.doInsertModeKeys('Backspace', 'Backspace');
+ helpers.doKeys('<Esc>');
+ eq('abe', cm.getValue());
+ helpers.assertCursorAt(0, 1);
+}, { value: 'abcde' });
+testVim('A', function(cm, vim, helpers) {
+ helpers.doKeys('A');
+ helpers.assertCursorAt(0, lines[0].length);
+ eq('vim-insert', cm.getOption('keyMap'));
+});
+testVim('A_visual_block', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.doKeys('<C-v>', '2', 'j', 'l', 'l', 'A');
+ var replacement = new Array(cm.listSelections().length+1).join('hello ').split(' ');
+ replacement.pop();
+ cm.replaceSelections(replacement);
+ eq('testhello\nmehello\npleahellose', cm.getValue());
+ helpers.doKeys('<Esc>');
+ cm.setCursor(0, 0);
+ helpers.doKeys('.');
+ // TODO this doesn't work yet
+ // eq('teshellothello\nme hello hello\nplehelloahellose', cm.getValue());
+}, {value: 'test\nme\nplease'});
+testVim('I', function(cm, vim, helpers) {
+ cm.setCursor(0, 4);
+ helpers.doKeys('I');
+ helpers.assertCursorAt(0, lines[0].textStart);
+ eq('vim-insert', cm.getOption('keyMap'));
+});
+testVim('I_repeat', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.doKeys('3', 'I');
+ cm.replaceRange('test', cm.getCursor());
+ helpers.doKeys('<Esc>');
+ eq('testtesttestblah', cm.getValue());
+ helpers.assertCursorAt(0, 11);
+}, { value: 'blah' });
+testVim('I_visual_block', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('<C-v>', '2', 'j', 'l', 'l', 'I');
+ var replacement = new Array(cm.listSelections().length+1).join('hello ').split(' ');
+ replacement.pop();
+ cm.replaceSelections(replacement);
+ eq('hellotest\nhellome\nhelloplease', cm.getValue());
+}, {value: 'test\nme\nplease'});
+testVim('o', function(cm, vim, helpers) {
+ cm.setCursor(0, 4);
+ helpers.doKeys('o');
+ eq('word1\n\nword2', cm.getValue());
+ helpers.assertCursorAt(1, 0);
+ eq('vim-insert', cm.getOption('keyMap'));
+}, { value: 'word1\nword2' });
+testVim('o_repeat', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('3', 'o');
+ cm.replaceRange('test', cm.getCursor());
+ helpers.doKeys('<Esc>');
+ eq('\ntest\ntest\ntest', cm.getValue());
+ helpers.assertCursorAt(3, 3);
+}, { value: '' });
+testVim('O', function(cm, vim, helpers) {
+ cm.setCursor(0, 4);
+ helpers.doKeys('O');
+ eq('\nword1\nword2', cm.getValue());
+ helpers.assertCursorAt(0, 0);
+ eq('vim-insert', cm.getOption('keyMap'));
+}, { value: 'word1\nword2' });
+testVim('J', function(cm, vim, helpers) {
+ cm.setCursor(0, 4);
+ helpers.doKeys('J');
+ var expectedValue = 'word1 word2\nword3\n word4';
+ eq(expectedValue, cm.getValue());
+ helpers.assertCursorAt(0, expectedValue.indexOf('word2') - 1);
+}, { value: 'word1 \n word2\nword3\n word4' });
+testVim('J_repeat', function(cm, vim, helpers) {
+ cm.setCursor(0, 4);
+ helpers.doKeys('3', 'J');
+ var expectedValue = 'word1 word2 word3\n word4';
+ eq(expectedValue, cm.getValue());
+ helpers.assertCursorAt(0, expectedValue.indexOf('word3') - 1);
+}, { value: 'word1 \n word2\nword3\n word4' });
+testVim('p', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.getRegisterController().pushText('"', 'yank', 'abc\ndef', false);
+ helpers.doKeys('p');
+ eq('__abc\ndef_', cm.getValue());
+ helpers.assertCursorAt(1, 2);
+}, { value: '___' });
+testVim('p_register', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.getRegisterController().getRegister('a').setText('abc\ndef', false);
+ helpers.doKeys('"', 'a', 'p');
+ eq('__abc\ndef_', cm.getValue());
+ helpers.assertCursorAt(1, 2);
+}, { value: '___' });
+testVim('p_wrong_register', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.getRegisterController().getRegister('a').setText('abc\ndef', false);
+ helpers.doKeys('p');
+ eq('___', cm.getValue());
+ helpers.assertCursorAt(0, 1);
+}, { value: '___' });
+testVim('p_line', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.getRegisterController().pushText('"', 'yank', ' a\nd\n', true);
+ helpers.doKeys('2', 'p');
+ eq('___\n a\nd\n a\nd', cm.getValue());
+ helpers.assertCursorAt(1, 2);
+}, { value: '___' });
+testVim('p_lastline', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.getRegisterController().pushText('"', 'yank', ' a\nd', true);
+ helpers.doKeys('2', 'p');
+ eq('___\n a\nd\n a\nd', cm.getValue());
+ helpers.assertCursorAt(1, 2);
+}, { value: '___' });
+testVim(']p_first_indent_is_smaller', function(cm, vim, helpers) {
+ helpers.getRegisterController().pushText('"', 'yank', ' abc\n def\n', true);
+ helpers.doKeys(']', 'p');
+ eq(' ___\n abc\n def', cm.getValue());
+}, { value: ' ___' });
+testVim(']p_first_indent_is_larger', function(cm, vim, helpers) {
+ helpers.getRegisterController().pushText('"', 'yank', ' abc\n def\n', true);
+ helpers.doKeys(']', 'p');
+ eq(' ___\n abc\ndef', cm.getValue());
+}, { value: ' ___' });
+testVim(']p_with_tab_indents', function(cm, vim, helpers) {
+ helpers.getRegisterController().pushText('"', 'yank', '\t\tabc\n\t\t\tdef\n', true);
+ helpers.doKeys(']', 'p');
+ eq('\t___\n\tabc\n\t\tdef', cm.getValue());
+}, { value: '\t___', indentWithTabs: true});
+testVim(']p_with_spaces_translated_to_tabs', function(cm, vim, helpers) {
+ helpers.getRegisterController().pushText('"', 'yank', ' abc\n def\n', true);
+ helpers.doKeys(']', 'p');
+ eq('\t___\n\tabc\n\t\tdef', cm.getValue());
+}, { value: '\t___', indentWithTabs: true, tabSize: 2 });
+testVim('[p', function(cm, vim, helpers) {
+ helpers.getRegisterController().pushText('"', 'yank', ' abc\n def\n', true);
+ helpers.doKeys('[', 'p');
+ eq(' abc\n def\n ___', cm.getValue());
+}, { value: ' ___' });
+testVim('P', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.getRegisterController().pushText('"', 'yank', 'abc\ndef', false);
+ helpers.doKeys('P');
+ eq('_abc\ndef__', cm.getValue());
+ helpers.assertCursorAt(1, 3);
+}, { value: '___' });
+testVim('P_line', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.getRegisterController().pushText('"', 'yank', ' a\nd\n', true);
+ helpers.doKeys('2', 'P');
+ eq(' a\nd\n a\nd\n___', cm.getValue());
+ helpers.assertCursorAt(0, 2);
+}, { value: '___' });
+testVim('r', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.doKeys('3', 'r', 'u');
+ eq('wuuuet\nanother', cm.getValue(),'3r failed');
+ helpers.assertCursorAt(0, 3);
+ cm.setCursor(0, 4);
+ helpers.doKeys('v', 'j', 'h', 'r', '<Space>');
+ eq('wuuu \n her', cm.getValue(),'Replacing selection by space-characters failed');
+}, { value: 'wordet\nanother' });
+testVim('r_visual_block', function(cm, vim, helpers) {
+ cm.setCursor(2, 3);
+ helpers.doKeys('<C-v>', 'k', 'k', 'h', 'h', 'r', 'l');
+ eq('1lll\n5lll\nalllefg', cm.getValue());
+ helpers.doKeys('<C-v>', 'l', 'j', 'r', '<Space>');
+ eq('1 l\n5 l\nalllefg', cm.getValue());
+ cm.setCursor(2, 0);
+ helpers.doKeys('o');
+ helpers.doKeys('<Esc>');
+ cm.replaceRange('\t\t', cm.getCursor());
+ helpers.doKeys('<C-v>', 'h', 'h', 'r', 'r');
+ eq('1 l\n5 l\nalllefg\nrrrrrrrr', cm.getValue());
+}, {value: '1234\n5678\nabcdefg'});
+testVim('R', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.doKeys('R');
+ helpers.assertCursorAt(0, 1);
+ eq('vim-replace', cm.getOption('keyMap'));
+ is(cm.state.overwrite, 'Setting overwrite state failed');
+});
+testVim('mark', function(cm, vim, helpers) {
+ cm.setCursor(2, 2);
+ helpers.doKeys('m', 't');
+ cm.setCursor(0, 0);
+ helpers.doKeys('`', 't');
+ helpers.assertCursorAt(2, 2);
+ cm.setCursor(2, 0);
+ cm.replaceRange(' h', cm.getCursor());
+ cm.setCursor(0, 0);
+ helpers.doKeys('\'', 't');
+ helpers.assertCursorAt(2, 3);
+});
+testVim('jumpToMark_next', function(cm, vim, helpers) {
+ cm.setCursor(2, 2);
+ helpers.doKeys('m', 't');
+ cm.setCursor(0, 0);
+ helpers.doKeys(']', '`');
+ helpers.assertCursorAt(2, 2);
+ cm.setCursor(0, 0);
+ helpers.doKeys(']', '\'');
+ helpers.assertCursorAt(2, 0);
+});
+testVim('jumpToMark_next_repeat', function(cm, vim, helpers) {
+ cm.setCursor(2, 2);
+ helpers.doKeys('m', 'a');
+ cm.setCursor(3, 2);
+ helpers.doKeys('m', 'b');
+ cm.setCursor(4, 2);
+ helpers.doKeys('m', 'c');
+ cm.setCursor(0, 0);
+ helpers.doKeys('2', ']', '`');
+ helpers.assertCursorAt(3, 2);
+ cm.setCursor(0, 0);
+ helpers.doKeys('2', ']', '\'');
+ helpers.assertCursorAt(3, 1);
+});
+testVim('jumpToMark_next_sameline', function(cm, vim, helpers) {
+ cm.setCursor(2, 0);
+ helpers.doKeys('m', 'a');
+ cm.setCursor(2, 4);
+ helpers.doKeys('m', 'b');
+ cm.setCursor(2, 2);
+ helpers.doKeys(']', '`');
+ helpers.assertCursorAt(2, 4);
+});
+testVim('jumpToMark_next_onlyprev', function(cm, vim, helpers) {
+ cm.setCursor(2, 0);
+ helpers.doKeys('m', 'a');
+ cm.setCursor(4, 0);
+ helpers.doKeys(']', '`');
+ helpers.assertCursorAt(4, 0);
+});
+testVim('jumpToMark_next_nomark', function(cm, vim, helpers) {
+ cm.setCursor(2, 2);
+ helpers.doKeys(']', '`');
+ helpers.assertCursorAt(2, 2);
+ helpers.doKeys(']', '\'');
+ helpers.assertCursorAt(2, 0);
+});
+testVim('jumpToMark_next_linewise_over', function(cm, vim, helpers) {
+ cm.setCursor(2, 2);
+ helpers.doKeys('m', 'a');
+ cm.setCursor(3, 4);
+ helpers.doKeys('m', 'b');
+ cm.setCursor(2, 1);
+ helpers.doKeys(']', '\'');
+ helpers.assertCursorAt(3, 1);
+});
+testVim('jumpToMark_next_action', function(cm, vim, helpers) {
+ cm.setCursor(2, 2);
+ helpers.doKeys('m', 't');
+ cm.setCursor(0, 0);
+ helpers.doKeys('d', ']', '`');
+ helpers.assertCursorAt(0, 0);
+ var actual = cm.getLine(0);
+ var expected = 'pop pop 0 1 2 3 4';
+ eq(actual, expected, "Deleting while jumping to the next mark failed.");
+});
+testVim('jumpToMark_next_line_action', function(cm, vim, helpers) {
+ cm.setCursor(2, 2);
+ helpers.doKeys('m', 't');
+ cm.setCursor(0, 0);
+ helpers.doKeys('d', ']', '\'');
+ helpers.assertCursorAt(0, 1);
+ var actual = cm.getLine(0);
+ var expected = ' (a) [b] {c} '
+ eq(actual, expected, "Deleting while jumping to the next mark line failed.");
+});
+testVim('jumpToMark_prev', function(cm, vim, helpers) {
+ cm.setCursor(2, 2);
+ helpers.doKeys('m', 't');
+ cm.setCursor(4, 0);
+ helpers.doKeys('[', '`');
+ helpers.assertCursorAt(2, 2);
+ cm.setCursor(4, 0);
+ helpers.doKeys('[', '\'');
+ helpers.assertCursorAt(2, 0);
+});
+testVim('jumpToMark_prev_repeat', function(cm, vim, helpers) {
+ cm.setCursor(2, 2);
+ helpers.doKeys('m', 'a');
+ cm.setCursor(3, 2);
+ helpers.doKeys('m', 'b');
+ cm.setCursor(4, 2);
+ helpers.doKeys('m', 'c');
+ cm.setCursor(5, 0);
+ helpers.doKeys('2', '[', '`');
+ helpers.assertCursorAt(3, 2);
+ cm.setCursor(5, 0);
+ helpers.doKeys('2', '[', '\'');
+ helpers.assertCursorAt(3, 1);
+});
+testVim('jumpToMark_prev_sameline', function(cm, vim, helpers) {
+ cm.setCursor(2, 0);
+ helpers.doKeys('m', 'a');
+ cm.setCursor(2, 4);
+ helpers.doKeys('m', 'b');
+ cm.setCursor(2, 2);
+ helpers.doKeys('[', '`');
+ helpers.assertCursorAt(2, 0);
+});
+testVim('jumpToMark_prev_onlynext', function(cm, vim, helpers) {
+ cm.setCursor(4, 4);
+ helpers.doKeys('m', 'a');
+ cm.setCursor(2, 0);
+ helpers.doKeys('[', '`');
+ helpers.assertCursorAt(2, 0);
+});
+testVim('jumpToMark_prev_nomark', function(cm, vim, helpers) {
+ cm.setCursor(2, 2);
+ helpers.doKeys('[', '`');
+ helpers.assertCursorAt(2, 2);
+ helpers.doKeys('[', '\'');
+ helpers.assertCursorAt(2, 0);
+});
+testVim('jumpToMark_prev_linewise_over', function(cm, vim, helpers) {
+ cm.setCursor(2, 2);
+ helpers.doKeys('m', 'a');
+ cm.setCursor(3, 4);
+ helpers.doKeys('m', 'b');
+ cm.setCursor(3, 6);
+ helpers.doKeys('[', '\'');
+ helpers.assertCursorAt(2, 0);
+});
+testVim('delmark_single', function(cm, vim, helpers) {
+ cm.setCursor(1, 2);
+ helpers.doKeys('m', 't');
+ helpers.doEx('delmarks t');
+ cm.setCursor(0, 0);
+ helpers.doKeys('`', 't');
+ helpers.assertCursorAt(0, 0);
+});
+testVim('delmark_range', function(cm, vim, helpers) {
+ cm.setCursor(1, 2);
+ helpers.doKeys('m', 'a');
+ cm.setCursor(2, 2);
+ helpers.doKeys('m', 'b');
+ cm.setCursor(3, 2);
+ helpers.doKeys('m', 'c');
+ cm.setCursor(4, 2);
+ helpers.doKeys('m', 'd');
+ cm.setCursor(5, 2);
+ helpers.doKeys('m', 'e');
+ helpers.doEx('delmarks b-d');
+ cm.setCursor(0, 0);
+ helpers.doKeys('`', 'a');
+ helpers.assertCursorAt(1, 2);
+ helpers.doKeys('`', 'b');
+ helpers.assertCursorAt(1, 2);
+ helpers.doKeys('`', 'c');
+ helpers.assertCursorAt(1, 2);
+ helpers.doKeys('`', 'd');
+ helpers.assertCursorAt(1, 2);
+ helpers.doKeys('`', 'e');
+ helpers.assertCursorAt(5, 2);
+});
+testVim('delmark_multi', function(cm, vim, helpers) {
+ cm.setCursor(1, 2);
+ helpers.doKeys('m', 'a');
+ cm.setCursor(2, 2);
+ helpers.doKeys('m', 'b');
+ cm.setCursor(3, 2);
+ helpers.doKeys('m', 'c');
+ cm.setCursor(4, 2);
+ helpers.doKeys('m', 'd');
+ cm.setCursor(5, 2);
+ helpers.doKeys('m', 'e');
+ helpers.doEx('delmarks bcd');
+ cm.setCursor(0, 0);
+ helpers.doKeys('`', 'a');
+ helpers.assertCursorAt(1, 2);
+ helpers.doKeys('`', 'b');
+ helpers.assertCursorAt(1, 2);
+ helpers.doKeys('`', 'c');
+ helpers.assertCursorAt(1, 2);
+ helpers.doKeys('`', 'd');
+ helpers.assertCursorAt(1, 2);
+ helpers.doKeys('`', 'e');
+ helpers.assertCursorAt(5, 2);
+});
+testVim('delmark_multi_space', function(cm, vim, helpers) {
+ cm.setCursor(1, 2);
+ helpers.doKeys('m', 'a');
+ cm.setCursor(2, 2);
+ helpers.doKeys('m', 'b');
+ cm.setCursor(3, 2);
+ helpers.doKeys('m', 'c');
+ cm.setCursor(4, 2);
+ helpers.doKeys('m', 'd');
+ cm.setCursor(5, 2);
+ helpers.doKeys('m', 'e');
+ helpers.doEx('delmarks b c d');
+ cm.setCursor(0, 0);
+ helpers.doKeys('`', 'a');
+ helpers.assertCursorAt(1, 2);
+ helpers.doKeys('`', 'b');
+ helpers.assertCursorAt(1, 2);
+ helpers.doKeys('`', 'c');
+ helpers.assertCursorAt(1, 2);
+ helpers.doKeys('`', 'd');
+ helpers.assertCursorAt(1, 2);
+ helpers.doKeys('`', 'e');
+ helpers.assertCursorAt(5, 2);
+});
+testVim('delmark_all', function(cm, vim, helpers) {
+ cm.setCursor(1, 2);
+ helpers.doKeys('m', 'a');
+ cm.setCursor(2, 2);
+ helpers.doKeys('m', 'b');
+ cm.setCursor(3, 2);
+ helpers.doKeys('m', 'c');
+ cm.setCursor(4, 2);
+ helpers.doKeys('m', 'd');
+ cm.setCursor(5, 2);
+ helpers.doKeys('m', 'e');
+ helpers.doEx('delmarks a b-de');
+ cm.setCursor(0, 0);
+ helpers.doKeys('`', 'a');
+ helpers.assertCursorAt(0, 0);
+ helpers.doKeys('`', 'b');
+ helpers.assertCursorAt(0, 0);
+ helpers.doKeys('`', 'c');
+ helpers.assertCursorAt(0, 0);
+ helpers.doKeys('`', 'd');
+ helpers.assertCursorAt(0, 0);
+ helpers.doKeys('`', 'e');
+ helpers.assertCursorAt(0, 0);
+});
+testVim('visual', function(cm, vim, helpers) {
+ helpers.doKeys('l', 'v', 'l', 'l');
+ helpers.assertCursorAt(0, 4);
+ eqPos(makeCursor(0, 1), cm.getCursor('anchor'));
+ helpers.doKeys('d');
+ eq('15', cm.getValue());
+}, { value: '12345' });
+testVim('visual_yank', function(cm, vim, helpers) {
+ helpers.doKeys('v', '3', 'l', 'y');
+ helpers.assertCursorAt(0, 0);
+ helpers.doKeys('p');
+ eq('aa te test for yank', cm.getValue());
+}, { value: 'a test for yank' })
+testVim('visual_w', function(cm, vim, helpers) {
+ helpers.doKeys('v', 'w');
+ eq(cm.getSelection(), 'motion t');
+}, { value: 'motion test'});
+testVim('visual_initial_selection', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.doKeys('v');
+ cm.getSelection('n');
+}, { value: 'init'});
+testVim('visual_crossover_left', function(cm, vim, helpers) {
+ cm.setCursor(0, 2);
+ helpers.doKeys('v', 'l', 'h', 'h');
+ cm.getSelection('ro');
+}, { value: 'cross'});
+testVim('visual_crossover_left', function(cm, vim, helpers) {
+ cm.setCursor(0, 2);
+ helpers.doKeys('v', 'h', 'l', 'l');
+ cm.getSelection('os');
+}, { value: 'cross'});
+testVim('visual_crossover_up', function(cm, vim, helpers) {
+ cm.setCursor(3, 2);
+ helpers.doKeys('v', 'j', 'k', 'k');
+ eqPos(Pos(2, 2), cm.getCursor('head'));
+ eqPos(Pos(3, 3), cm.getCursor('anchor'));
+ helpers.doKeys('k');
+ eqPos(Pos(1, 2), cm.getCursor('head'));
+ eqPos(Pos(3, 3), cm.getCursor('anchor'));
+}, { value: 'cross\ncross\ncross\ncross\ncross\n'});
+testVim('visual_crossover_down', function(cm, vim, helpers) {
+ cm.setCursor(1, 2);
+ helpers.doKeys('v', 'k', 'j', 'j');
+ eqPos(Pos(2, 3), cm.getCursor('head'));
+ eqPos(Pos(1, 2), cm.getCursor('anchor'));
+ helpers.doKeys('j');
+ eqPos(Pos(3, 3), cm.getCursor('head'));
+ eqPos(Pos(1, 2), cm.getCursor('anchor'));
+}, { value: 'cross\ncross\ncross\ncross\ncross\n'});
+testVim('visual_exit', function(cm, vim, helpers) {
+ helpers.doKeys('<C-v>', 'l', 'j', 'j', '<Esc>');
+ eqPos(cm.getCursor('anchor'), cm.getCursor('head'));
+ eq(vim.visualMode, false);
+}, { value: 'hello\nworld\nfoo' });
+testVim('visual_line', function(cm, vim, helpers) {
+ helpers.doKeys('l', 'V', 'l', 'j', 'j', 'd');
+ eq(' 4\n 5', cm.getValue());
+}, { value: ' 1\n 2\n 3\n 4\n 5' });
+testVim('visual_block_move_to_eol', function(cm, vim, helpers) {
+ // moveToEol should move all block cursors to end of line
+ cm.setCursor(0, 0);
+ helpers.doKeys('<C-v>', 'G', '$');
+ var selections = cm.getSelections().join();
+ eq('123,45,6', selections);
+ // Checks that with cursor at Infinity, finding words backwards still works.
+ helpers.doKeys('2', 'k', 'b');
+ selections = cm.getSelections().join();
+ eq('1', selections);
+}, {value: '123\n45\n6'});
+testVim('visual_block_different_line_lengths', function(cm, vim, helpers) {
+ // test the block selection with lines of different length
+ // i.e. extending the selection
+ // till the end of the longest line.
+ helpers.doKeys('<C-v>', 'l', 'j', 'j', '6', 'l', 'd');
+ helpers.doKeys('d', 'd', 'd', 'd');
+ eq('', cm.getValue());
+}, {value: '1234\n5678\nabcdefg'});
+testVim('visual_block_truncate_on_short_line', function(cm, vim, helpers) {
+ // check for left side selection in case
+ // of moving up to a shorter line.
+ cm.replaceRange('', cm.getCursor());
+ cm.setCursor(3, 4);
+ helpers.doKeys('<C-v>', 'l', 'k', 'k', 'd');
+ eq('hello world\n{\ntis\nsa!', cm.getValue());
+}, {value: 'hello world\n{\nthis is\nsparta!'});
+testVim('visual_block_corners', function(cm, vim, helpers) {
+ cm.setCursor(1, 2);
+ helpers.doKeys('<C-v>', '2', 'l', 'k');
+ // circle around the anchor
+ // and check the selections
+ var selections = cm.getSelections();
+ eq('345891', selections.join(''));
+ helpers.doKeys('4', 'h');
+ selections = cm.getSelections();
+ eq('123678', selections.join(''));
+ helpers.doKeys('j', 'j');
+ selections = cm.getSelections();
+ eq('678abc', selections.join(''));
+ helpers.doKeys('4', 'l');
+ selections = cm.getSelections();
+ eq('891cde', selections.join(''));
+}, {value: '12345\n67891\nabcde'});
+testVim('visual_block_mode_switch', function(cm, vim, helpers) {
+ // switch between visual modes
+ cm.setCursor(1, 1);
+ // blockwise to characterwise visual
+ helpers.doKeys('<C-v>', 'j', 'l', 'v');
+ selections = cm.getSelections();
+ eq('7891\nabc', selections.join(''));
+ // characterwise to blockwise
+ helpers.doKeys('<C-v>');
+ selections = cm.getSelections();
+ eq('78bc', selections.join(''));
+ // blockwise to linewise visual
+ helpers.doKeys('V');
+ selections = cm.getSelections();
+ eq('67891\nabcde', selections.join(''));
+}, {value: '12345\n67891\nabcde'});
+testVim('visual_block_crossing_short_line', function(cm, vim, helpers) {
+ // visual block with long and short lines
+ cm.setCursor(0, 3);
+ helpers.doKeys('<C-v>', 'j', 'j', 'j');
+ var selections = cm.getSelections().join();
+ eq('4,,d,b', selections);
+ helpers.doKeys('3', 'k');
+ selections = cm.getSelections().join();
+ eq('4', selections);
+ helpers.doKeys('5', 'j', 'k');
+ selections = cm.getSelections().join("");
+ eq(10, selections.length);
+}, {value: '123456\n78\nabcdefg\nfoobar\n}\n'});
+testVim('visual_block_curPos_on_exit', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('<C-v>', '3' , 'l', '<Esc>');
+ eqPos(makeCursor(0, 3), cm.getCursor());
+ helpers.doKeys('h', '<C-v>', '2' , 'j' ,'3' , 'l');
+ eq(cm.getSelections().join(), "3456,,cdef");
+ helpers.doKeys('4' , 'h');
+ eq(cm.getSelections().join(), "23,8,bc");
+ helpers.doKeys('2' , 'l');
+ eq(cm.getSelections().join(), "34,,cd");
+}, {value: '123456\n78\nabcdefg\nfoobar'});
+
+testVim('visual_marks', function(cm, vim, helpers) {
+ helpers.doKeys('l', 'v', 'l', 'l', 'j', 'j', 'v');
+ // Test visual mode marks
+ cm.setCursor(2, 1);
+ helpers.doKeys('\'', '<');
+ helpers.assertCursorAt(0, 1);
+ helpers.doKeys('\'', '>');
+ helpers.assertCursorAt(2, 0);
+});
+testVim('visual_join', function(cm, vim, helpers) {
+ helpers.doKeys('l', 'V', 'l', 'j', 'j', 'J');
+ eq(' 1 2 3\n 4\n 5', cm.getValue());
+ is(!vim.visualMode);
+}, { value: ' 1\n 2\n 3\n 4\n 5' });
+testVim('visual_join_2', function(cm, vim, helpers) {
+ helpers.doKeys('G', 'V', 'g', 'g', 'J');
+ eq('1 2 3 4 5 6 ', cm.getValue());
+ is(!vim.visualMode);
+}, { value: '1\n2\n3\n4\n5\n6\n'});
+testVim('visual_blank', function(cm, vim, helpers) {
+ helpers.doKeys('v', 'k');
+ eq(vim.visualMode, true);
+}, { value: '\n' });
+testVim('reselect_visual', function(cm, vim, helpers) {
+ helpers.doKeys('l', 'v', 'l', 'l', 'l', 'y', 'g', 'v');
+ helpers.assertCursorAt(0, 5);
+ eqPos(makeCursor(0, 1), cm.getCursor('anchor'));
+ helpers.doKeys('v');
+ cm.setCursor(1, 0);
+ helpers.doKeys('v', 'l', 'l', 'p');
+ eq('123456\n2345\nbar', cm.getValue());
+ cm.setCursor(0, 0);
+ helpers.doKeys('g', 'v');
+ // here the fake cursor is at (1, 3)
+ helpers.assertCursorAt(1, 4);
+ eqPos(makeCursor(1, 0), cm.getCursor('anchor'));
+ helpers.doKeys('v');
+ cm.setCursor(2, 0);
+ helpers.doKeys('v', 'l', 'l', 'g', 'v');
+ helpers.assertCursorAt(1, 4);
+ eqPos(makeCursor(1, 0), cm.getCursor('anchor'));
+ helpers.doKeys('g', 'v');
+ helpers.assertCursorAt(2, 3);
+ eqPos(makeCursor(2, 0), cm.getCursor('anchor'));
+ eq('123456\n2345\nbar', cm.getValue());
+}, { value: '123456\nfoo\nbar' });
+testVim('reselect_visual_line', function(cm, vim, helpers) {
+ helpers.doKeys('l', 'V', 'j', 'j', 'V', 'g', 'v', 'd');
+ eq('foo\nand\nbar', cm.getValue());
+ cm.setCursor(1, 0);
+ helpers.doKeys('V', 'y', 'j');
+ helpers.doKeys('V', 'p' , 'g', 'v', 'd');
+ eq('foo\nand', cm.getValue());
+}, { value: 'hello\nthis\nis\nfoo\nand\nbar' });
+testVim('reselect_visual_block', function(cm, vim, helpers) {
+ cm.setCursor(1, 2);
+ helpers.doKeys('<C-v>', 'k', 'h', '<C-v>');
+ cm.setCursor(2, 1);
+ helpers.doKeys('v', 'l', 'g', 'v');
+ eqPos(Pos(1, 2), vim.sel.anchor);
+ eqPos(Pos(0, 1), vim.sel.head);
+ // Ensure selection is done with visual block mode rather than one
+ // continuous range.
+ eq(cm.getSelections().join(''), '23oo')
+ helpers.doKeys('g', 'v');
+ eqPos(Pos(2, 1), vim.sel.anchor);
+ eqPos(Pos(2, 2), vim.sel.head);
+ helpers.doKeys('<Esc>');
+ // Ensure selection of deleted range
+ cm.setCursor(1, 1);
+ helpers.doKeys('v', '<C-v>', 'j', 'd', 'g', 'v');
+ eq(cm.getSelections().join(''), 'or');
+}, { value: '123456\nfoo\nbar' });
+testVim('s_normal', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.doKeys('s');
+ helpers.doKeys('<Esc>');
+ eq('ac', cm.getValue());
+}, { value: 'abc'});
+testVim('s_visual', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.doKeys('v', 's');
+ helpers.doKeys('<Esc>');
+ helpers.assertCursorAt(0, 0);
+ eq('ac', cm.getValue());
+}, { value: 'abc'});
+testVim('o_visual', function(cm, vim, helpers) {
+ cm.setCursor(0,0);
+ helpers.doKeys('v','l','l','l','o');
+ helpers.assertCursorAt(0,0);
+ helpers.doKeys('v','v','j','j','j','o');
+ helpers.assertCursorAt(0,0);
+ helpers.doKeys('O');
+ helpers.doKeys('l','l')
+ helpers.assertCursorAt(3, 3);
+ helpers.doKeys('d');
+ eq('p',cm.getValue());
+}, { value: 'abcd\nefgh\nijkl\nmnop'});
+testVim('o_visual_block', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.doKeys('<C-v>','3','j','l','l', 'o');
+ eqPos(Pos(3, 3), vim.sel.anchor);
+ eqPos(Pos(0, 1), vim.sel.head);
+ helpers.doKeys('O');
+ eqPos(Pos(3, 1), vim.sel.anchor);
+ eqPos(Pos(0, 3), vim.sel.head);
+ helpers.doKeys('o');
+ eqPos(Pos(0, 3), vim.sel.anchor);
+ eqPos(Pos(3, 1), vim.sel.head);
+}, { value: 'abcd\nefgh\nijkl\nmnop'});
+testVim('changeCase_visual', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('v', 'l', 'l');
+ helpers.doKeys('U');
+ helpers.assertCursorAt(0, 0);
+ helpers.doKeys('v', 'l', 'l');
+ helpers.doKeys('u');
+ helpers.assertCursorAt(0, 0);
+ helpers.doKeys('l', 'l', 'l', '.');
+ helpers.assertCursorAt(0, 3);
+ cm.setCursor(0, 0);
+ helpers.doKeys('q', 'a', 'v', 'j', 'U', 'q');
+ helpers.assertCursorAt(0, 0);
+ helpers.doKeys('j', '@', 'a');
+ helpers.assertCursorAt(1, 0);
+ cm.setCursor(3, 0);
+ helpers.doKeys('V', 'U', 'j', '.');
+ eq('ABCDEF\nGHIJKL\nMnopq\nSHORT LINE\nLONG LINE OF TEXT', cm.getValue());
+}, { value: 'abcdef\nghijkl\nmnopq\nshort line\nlong line of text'});
+testVim('changeCase_visual_block', function(cm, vim, helpers) {
+ cm.setCursor(2, 1);
+ helpers.doKeys('<C-v>', 'k', 'k', 'h', 'U');
+ eq('ABcdef\nGHijkl\nMNopq\nfoo', cm.getValue());
+ cm.setCursor(0, 2);
+ helpers.doKeys('.');
+ eq('ABCDef\nGHIJkl\nMNOPq\nfoo', cm.getValue());
+ // check when last line is shorter.
+ cm.setCursor(2, 2);
+ helpers.doKeys('.');
+ eq('ABCDef\nGHIJkl\nMNOPq\nfoO', cm.getValue());
+}, { value: 'abcdef\nghijkl\nmnopq\nfoo'});
+testVim('visual_paste', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('v', 'l', 'l', 'y');
+ helpers.assertCursorAt(0, 0);
+ helpers.doKeys('3', 'l', 'j', 'v', 'l', 'p');
+ helpers.assertCursorAt(1, 5);
+ eq('this is a\nunithitest for visual paste', cm.getValue());
+ cm.setCursor(0, 0);
+ // in case of pasting whole line
+ helpers.doKeys('y', 'y');
+ cm.setCursor(1, 6);
+ helpers.doKeys('v', 'l', 'l', 'l', 'p');
+ helpers.assertCursorAt(2, 0);
+ eq('this is a\nunithi\nthis is a\n for visual paste', cm.getValue());
+}, { value: 'this is a\nunit test for visual paste'});
+
+// This checks the contents of the register used to paste the text
+testVim('v_paste_from_register', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('"', 'a', 'y', 'w');
+ cm.setCursor(1, 0);
+ helpers.doKeys('v', 'p');
+ cm.openDialog = helpers.fakeOpenDialog('registers');
+ cm.openNotification = helpers.fakeOpenNotification(function(text) {
+ is(/a\s+register/.test(text));
+ });
+}, { value: 'register contents\nare not erased'});
+testVim('S_normal', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.doKeys('j', 'S');
+ helpers.doKeys('<Esc>');
+ helpers.assertCursorAt(1, 1);
+ eq('aa{\n \ncc', cm.getValue());
+ helpers.doKeys('j', 'S');
+ eq('aa{\n \n ', cm.getValue());
+ helpers.assertCursorAt(2, 2);
+ helpers.doKeys('<Esc>');
+ helpers.doKeys('d', 'd', 'd', 'd');
+ helpers.assertCursorAt(0, 0);
+ helpers.doKeys('S');
+ is(vim.insertMode);
+ eq('', cm.getValue());
+}, { value: 'aa{\nbb\ncc'});
+testVim('blockwise_paste', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('<C-v>', '3', 'j', 'l', 'y');
+ cm.setCursor(0, 2);
+ // paste one char after the current cursor position
+ helpers.doKeys('p');
+ eq('helhelo\nworwold\nfoofo\nbarba', cm.getValue());
+ cm.setCursor(0, 0);
+ helpers.doKeys('v', '4', 'l', 'y');
+ cm.setCursor(0, 0);
+ helpers.doKeys('<C-v>', '3', 'j', 'p');
+ eq('helheelhelo\norwold\noofo\narba', cm.getValue());
+}, { value: 'hello\nworld\nfoo\nbar'});
+testVim('blockwise_paste_long/short_line', function(cm, vim, helpers) {
+ // extend short lines in case of different line lengths.
+ cm.setCursor(0, 0);
+ helpers.doKeys('<C-v>', 'j', 'j', 'y');
+ cm.setCursor(0, 3);
+ helpers.doKeys('p');
+ eq('hellho\nfoo f\nbar b', cm.getValue());
+}, { value: 'hello\nfoo\nbar'});
+testVim('blockwise_paste_cut_paste', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('<C-v>', '2', 'j', 'x');
+ cm.setCursor(0, 0);
+ helpers.doKeys('P');
+ eq('cut\nand\npaste\nme', cm.getValue());
+}, { value: 'cut\nand\npaste\nme'});
+testVim('blockwise_paste_from_register', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('<C-v>', '2', 'j', '"', 'a', 'y');
+ cm.setCursor(0, 3);
+ helpers.doKeys('"', 'a', 'p');
+ eq('foobfar\nhellho\nworlwd', cm.getValue());
+}, { value: 'foobar\nhello\nworld'});
+testVim('blockwise_paste_last_line', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('<C-v>', '2', 'j', 'l', 'y');
+ cm.setCursor(3, 0);
+ helpers.doKeys('p');
+ eq('cut\nand\npaste\nmcue\n an\n pa', cm.getValue());
+}, { value: 'cut\nand\npaste\nme'});
+
+testVim('S_visual', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.doKeys('v', 'j', 'S');
+ helpers.doKeys('<Esc>');
+ helpers.assertCursorAt(0, 0);
+ eq('\ncc', cm.getValue());
+}, { value: 'aa\nbb\ncc'});
+
+testVim('d_/', function(cm, vim, helpers) {
+ cm.openDialog = helpers.fakeOpenDialog('match');
+ helpers.doKeys('2', 'd', '/');
+ helpers.assertCursorAt(0, 0);
+ eq('match \n next', cm.getValue());
+ cm.openDialog = helpers.fakeOpenDialog('2');
+ helpers.doKeys('d', ':');
+ // TODO eq(' next', cm.getValue());
+}, { value: 'text match match \n next' });
+testVim('/ and n/N', function(cm, vim, helpers) {
+ cm.openDialog = helpers.fakeOpenDialog('match');
+ helpers.doKeys('/');
+ helpers.assertCursorAt(0, 11);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(1, 6);
+ helpers.doKeys('N');
+ helpers.assertCursorAt(0, 11);
+
+ cm.setCursor(0, 0);
+ helpers.doKeys('2', '/');
+ helpers.assertCursorAt(1, 6);
+}, { value: 'match nope match \n nope Match' });
+testVim('/_case', function(cm, vim, helpers) {
+ cm.openDialog = helpers.fakeOpenDialog('Match');
+ helpers.doKeys('/');
+ helpers.assertCursorAt(1, 6);
+}, { value: 'match nope match \n nope Match' });
+testVim('/_2_pcre', function(cm, vim, helpers) {
+ CodeMirror.Vim.setOption('pcre', true);
+ cm.openDialog = helpers.fakeOpenDialog('(word){2}');
+ helpers.doKeys('/');
+ helpers.assertCursorAt(1, 9);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(2, 1);
+}, { value: 'word\n another wordword\n wordwordword\n' });
+testVim('/_2_nopcre', function(cm, vim, helpers) {
+ CodeMirror.Vim.setOption('pcre', false);
+ cm.openDialog = helpers.fakeOpenDialog('\\(word\\)\\{2}');
+ helpers.doKeys('/');
+ helpers.assertCursorAt(1, 9);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(2, 1);
+}, { value: 'word\n another wordword\n wordwordword\n' });
+testVim('/_nongreedy', function(cm, vim, helpers) {
+ cm.openDialog = helpers.fakeOpenDialog('aa');
+ helpers.doKeys('/');
+ helpers.assertCursorAt(0, 4);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(1, 3);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(0, 0);
+}, { value: 'aaa aa \n a aa'});
+testVim('?_nongreedy', function(cm, vim, helpers) {
+ cm.openDialog = helpers.fakeOpenDialog('aa');
+ helpers.doKeys('?');
+ helpers.assertCursorAt(1, 3);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(0, 4);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(0, 0);
+}, { value: 'aaa aa \n a aa'});
+testVim('/_greedy', function(cm, vim, helpers) {
+ cm.openDialog = helpers.fakeOpenDialog('a+');
+ helpers.doKeys('/');
+ helpers.assertCursorAt(0, 4);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(1, 1);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(1, 3);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(0, 0);
+}, { value: 'aaa aa \n a aa'});
+testVim('?_greedy', function(cm, vim, helpers) {
+ cm.openDialog = helpers.fakeOpenDialog('a+');
+ helpers.doKeys('?');
+ helpers.assertCursorAt(1, 3);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(1, 1);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(0, 4);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(0, 0);
+}, { value: 'aaa aa \n a aa'});
+testVim('/_greedy_0_or_more', function(cm, vim, helpers) {
+ cm.openDialog = helpers.fakeOpenDialog('a*');
+ helpers.doKeys('/');
+ helpers.assertCursorAt(0, 3);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(0, 4);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(0, 5);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(1, 0);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(1, 1);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(0, 0);
+}, { value: 'aaa aa\n aa'});
+testVim('?_greedy_0_or_more', function(cm, vim, helpers) {
+ cm.openDialog = helpers.fakeOpenDialog('a*');
+ helpers.doKeys('?');
+ helpers.assertCursorAt(1, 1);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(1, 0);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(0, 5);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(0, 4);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(0, 3);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(0, 0);
+}, { value: 'aaa aa\n aa'});
+testVim('? and n/N', function(cm, vim, helpers) {
+ cm.openDialog = helpers.fakeOpenDialog('match');
+ helpers.doKeys('?');
+ helpers.assertCursorAt(1, 6);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(0, 11);
+ helpers.doKeys('N');
+ helpers.assertCursorAt(1, 6);
+
+ cm.setCursor(0, 0);
+ helpers.doKeys('2', '?');
+ helpers.assertCursorAt(0, 11);
+}, { value: 'match nope match \n nope Match' });
+testVim('*', function(cm, vim, helpers) {
+ cm.setCursor(0, 9);
+ helpers.doKeys('*');
+ helpers.assertCursorAt(0, 22);
+
+ cm.setCursor(0, 9);
+ helpers.doKeys('2', '*');
+ helpers.assertCursorAt(1, 8);
+}, { value: 'nomatch match nomatch match \nnomatch Match' });
+testVim('*_no_word', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('*');
+ helpers.assertCursorAt(0, 0);
+}, { value: ' \n match \n' });
+testVim('*_symbol', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('*');
+ helpers.assertCursorAt(1, 0);
+}, { value: ' /}\n/} match \n' });
+testVim('#', function(cm, vim, helpers) {
+ cm.setCursor(0, 9);
+ helpers.doKeys('#');
+ helpers.assertCursorAt(1, 8);
+
+ cm.setCursor(0, 9);
+ helpers.doKeys('2', '#');
+ helpers.assertCursorAt(0, 22);
+}, { value: 'nomatch match nomatch match \nnomatch Match' });
+testVim('*_seek', function(cm, vim, helpers) {
+ // Should skip over space and symbols.
+ cm.setCursor(0, 3);
+ helpers.doKeys('*');
+ helpers.assertCursorAt(0, 22);
+}, { value: ' := match nomatch match \nnomatch Match' });
+testVim('#', function(cm, vim, helpers) {
+ // Should skip over space and symbols.
+ cm.setCursor(0, 3);
+ helpers.doKeys('#');
+ helpers.assertCursorAt(1, 8);
+}, { value: ' := match nomatch match \nnomatch Match' });
+testVim('g*', function(cm, vim, helpers) {
+ cm.setCursor(0, 8);
+ helpers.doKeys('g', '*');
+ helpers.assertCursorAt(0, 18);
+ cm.setCursor(0, 8);
+ helpers.doKeys('3', 'g', '*');
+ helpers.assertCursorAt(1, 8);
+}, { value: 'matches match alsoMatch\nmatchme matching' });
+testVim('g#', function(cm, vim, helpers) {
+ cm.setCursor(0, 8);
+ helpers.doKeys('g', '#');
+ helpers.assertCursorAt(0, 0);
+ cm.setCursor(0, 8);
+ helpers.doKeys('3', 'g', '#');
+ helpers.assertCursorAt(1, 0);
+}, { value: 'matches match alsoMatch\nmatchme matching' });
+testVim('macro_insert', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('q', 'a', '0', 'i');
+ cm.replaceRange('foo', cm.getCursor());
+ helpers.doKeys('<Esc>');
+ helpers.doKeys('q', '@', 'a');
+ eq('foofoo', cm.getValue());
+}, { value: ''});
+testVim('macro_insert_repeat', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('q', 'a', '$', 'a');
+ cm.replaceRange('larry.', cm.getCursor());
+ helpers.doKeys('<Esc>');
+ helpers.doKeys('a');
+ cm.replaceRange('curly.', cm.getCursor());
+ helpers.doKeys('<Esc>');
+ helpers.doKeys('q');
+ helpers.doKeys('a');
+ cm.replaceRange('moe.', cm.getCursor());
+ helpers.doKeys('<Esc>');
+ helpers.doKeys('@', 'a');
+ // At this point, the most recent edit should be the 2nd insert change
+ // inside the macro, i.e. "curly.".
+ helpers.doKeys('.');
+ eq('larry.curly.moe.larry.curly.curly.', cm.getValue());
+}, { value: ''});
+testVim('macro_space', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('<Space>', '<Space>');
+ helpers.assertCursorAt(0, 2);
+ helpers.doKeys('q', 'a', '<Space>', '<Space>', 'q');
+ helpers.assertCursorAt(0, 4);
+ helpers.doKeys('@', 'a');
+ helpers.assertCursorAt(0, 6);
+ helpers.doKeys('@', 'a');
+ helpers.assertCursorAt(0, 8);
+}, { value: 'one line of text.'});
+testVim('macro_t_search', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('q', 'a', 't', 'e', 'q');
+ helpers.assertCursorAt(0, 1);
+ helpers.doKeys('l', '@', 'a');
+ helpers.assertCursorAt(0, 6);
+ helpers.doKeys('l', ';');
+ helpers.assertCursorAt(0, 12);
+}, { value: 'one line of text.'});
+testVim('macro_f_search', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('q', 'b', 'f', 'e', 'q');
+ helpers.assertCursorAt(0, 2);
+ helpers.doKeys('@', 'b');
+ helpers.assertCursorAt(0, 7);
+ helpers.doKeys(';');
+ helpers.assertCursorAt(0, 13);
+}, { value: 'one line of text.'});
+testVim('macro_slash_search', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('q', 'c');
+ cm.openDialog = helpers.fakeOpenDialog('e');
+ helpers.doKeys('/', 'q');
+ helpers.assertCursorAt(0, 2);
+ helpers.doKeys('@', 'c');
+ helpers.assertCursorAt(0, 7);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(0, 13);
+}, { value: 'one line of text.'});
+testVim('macro_multislash_search', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('q', 'd');
+ cm.openDialog = helpers.fakeOpenDialog('e');
+ helpers.doKeys('/');
+ cm.openDialog = helpers.fakeOpenDialog('t');
+ helpers.doKeys('/', 'q');
+ helpers.assertCursorAt(0, 12);
+ helpers.doKeys('@', 'd');
+ helpers.assertCursorAt(0, 15);
+}, { value: 'one line of text to rule them all.'});
+testVim('macro_last_ex_command_register', function (cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doEx('s/a/b');
+ helpers.doKeys('2', '@', ':');
+ eq('bbbaa', cm.getValue());
+ helpers.assertCursorAt(0, 2);
+}, { value: 'aaaaa'});
+testVim('macro_parens', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('q', 'z', 'i');
+ cm.replaceRange('(', cm.getCursor());
+ helpers.doKeys('<Esc>');
+ helpers.doKeys('e', 'a');
+ cm.replaceRange(')', cm.getCursor());
+ helpers.doKeys('<Esc>');
+ helpers.doKeys('q');
+ helpers.doKeys('w', '@', 'z');
+ helpers.doKeys('w', '@', 'z');
+ eq('(see) (spot) (run)', cm.getValue());
+}, { value: 'see spot run'});
+testVim('macro_overwrite', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('q', 'z', '0', 'i');
+ cm.replaceRange('I ', cm.getCursor());
+ helpers.doKeys('<Esc>');
+ helpers.doKeys('q');
+ helpers.doKeys('e');
+ // Now replace the macro with something else.
+ helpers.doKeys('q', 'z', 'a');
+ cm.replaceRange('.', cm.getCursor());
+ helpers.doKeys('<Esc>');
+ helpers.doKeys('q');
+ helpers.doKeys('e', '@', 'z');
+ helpers.doKeys('e', '@', 'z');
+ eq('I see. spot. run.', cm.getValue());
+}, { value: 'see spot run'});
+testVim('macro_search_f', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('q', 'a', 'f', ' ');
+ helpers.assertCursorAt(0,3);
+ helpers.doKeys('q', '0');
+ helpers.assertCursorAt(0,0);
+ helpers.doKeys('@', 'a');
+ helpers.assertCursorAt(0,3);
+}, { value: 'The quick brown fox jumped over the lazy dog.'});
+testVim('macro_search_2f', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('q', 'a', '2', 'f', ' ');
+ helpers.assertCursorAt(0,9);
+ helpers.doKeys('q', '0');
+ helpers.assertCursorAt(0,0);
+ helpers.doKeys('@', 'a');
+ helpers.assertCursorAt(0,9);
+}, { value: 'The quick brown fox jumped over the lazy dog.'});
+testVim('yank_register', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('"', 'a', 'y', 'y');
+ helpers.doKeys('j', '"', 'b', 'y', 'y');
+ cm.openDialog = helpers.fakeOpenDialog('registers');
+ cm.openNotification = helpers.fakeOpenNotification(function(text) {
+ is(/a\s+foo/.test(text));
+ is(/b\s+bar/.test(text));
+ });
+ helpers.doKeys(':');
+}, { value: 'foo\nbar'});
+testVim('yank_visual_block', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.doKeys('<C-v>', 'l', 'j', '"', 'a', 'y');
+ cm.openNotification = helpers.fakeOpenNotification(function(text) {
+ is(/a\s+oo\nar/.test(text));
+ });
+ helpers.doKeys(':');
+}, { value: 'foo\nbar'});
+testVim('yank_append_line_to_line_register', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('"', 'a', 'y', 'y');
+ helpers.doKeys('j', '"', 'A', 'y', 'y');
+ cm.openDialog = helpers.fakeOpenDialog('registers');
+ cm.openNotification = helpers.fakeOpenNotification(function(text) {
+ is(/a\s+foo\nbar/.test(text));
+ is(/"\s+foo\nbar/.test(text));
+ });
+ helpers.doKeys(':');
+}, { value: 'foo\nbar'});
+testVim('yank_append_word_to_word_register', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('"', 'a', 'y', 'w');
+ helpers.doKeys('j', '"', 'A', 'y', 'w');
+ cm.openDialog = helpers.fakeOpenDialog('registers');
+ cm.openNotification = helpers.fakeOpenNotification(function(text) {
+ is(/a\s+foobar/.test(text));
+ is(/"\s+foobar/.test(text));
+ });
+ helpers.doKeys(':');
+}, { value: 'foo\nbar'});
+testVim('yank_append_line_to_word_register', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('"', 'a', 'y', 'w');
+ helpers.doKeys('j', '"', 'A', 'y', 'y');
+ cm.openDialog = helpers.fakeOpenDialog('registers');
+ cm.openNotification = helpers.fakeOpenNotification(function(text) {
+ is(/a\s+foo\nbar/.test(text));
+ is(/"\s+foo\nbar/.test(text));
+ });
+ helpers.doKeys(':');
+}, { value: 'foo\nbar'});
+testVim('yank_append_word_to_line_register', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('"', 'a', 'y', 'y');
+ helpers.doKeys('j', '"', 'A', 'y', 'w');
+ cm.openDialog = helpers.fakeOpenDialog('registers');
+ cm.openNotification = helpers.fakeOpenNotification(function(text) {
+ is(/a\s+foo\nbar/.test(text));
+ is(/"\s+foo\nbar/.test(text));
+ });
+ helpers.doKeys(':');
+}, { value: 'foo\nbar'});
+testVim('macro_register', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('q', 'a', 'i');
+ cm.replaceRange('gangnam', cm.getCursor());
+ helpers.doKeys('<Esc>');
+ helpers.doKeys('q');
+ helpers.doKeys('q', 'b', 'o');
+ cm.replaceRange('style', cm.getCursor());
+ helpers.doKeys('<Esc>');
+ helpers.doKeys('q');
+ cm.openDialog = helpers.fakeOpenDialog('registers');
+ cm.openNotification = helpers.fakeOpenNotification(function(text) {
+ is(/a\s+i/.test(text));
+ is(/b\s+o/.test(text));
+ });
+ helpers.doKeys(':');
+}, { value: ''});
+testVim('._register', function(cm,vim,helpers) {
+ cm.setCursor(0,0);
+ helpers.doKeys('i');
+ cm.replaceRange('foo',cm.getCursor());
+ helpers.doKeys('<Esc>');
+ cm.openDialog = helpers.fakeOpenDialog('registers');
+ cm.openNotification = helpers.fakeOpenNotification(function(text) {
+ is(/\.\s+foo/.test(text));
+ });
+ helpers.doKeys(':');
+}, {value: ''});
+testVim(':_register', function(cm,vim,helpers) {
+ helpers.doEx('bar');
+ cm.openDialog = helpers.fakeOpenDialog('registers');
+ cm.openNotification = helpers.fakeOpenNotification(function(text) {
+ is(/:\s+bar/.test(text));
+ });
+ helpers.doKeys(':');
+}, {value: ''});
+testVim('search_register_escape', function(cm, vim, helpers) {
+ // Check that the register is restored if the user escapes rather than confirms.
+ cm.openDialog = helpers.fakeOpenDialog('waldo');
+ helpers.doKeys('/');
+ var onKeyDown;
+ var onKeyUp;
+ var KEYCODES = {
+ f: 70,
+ o: 79,
+ Esc: 27
+ };
+ cm.openDialog = function(template, callback, options) {
+ onKeyDown = options.onKeyDown;
+ onKeyUp = options.onKeyUp;
+ };
+ var close = function() {};
+ helpers.doKeys('/');
+ // Fake some keyboard events coming in.
+ onKeyDown({keyCode: KEYCODES.f}, '', close);
+ onKeyUp({keyCode: KEYCODES.f}, '', close);
+ onKeyDown({keyCode: KEYCODES.o}, 'f', close);
+ onKeyUp({keyCode: KEYCODES.o}, 'f', close);
+ onKeyDown({keyCode: KEYCODES.o}, 'fo', close);
+ onKeyUp({keyCode: KEYCODES.o}, 'fo', close);
+ onKeyDown({keyCode: KEYCODES.Esc}, 'foo', close);
+ cm.openDialog = helpers.fakeOpenDialog('registers');
+ cm.openNotification = helpers.fakeOpenNotification(function(text) {
+ is(/waldo/.test(text));
+ is(!/foo/.test(text));
+ });
+ helpers.doKeys(':');
+}, {value: ''});
+testVim('search_register', function(cm, vim, helpers) {
+ cm.openDialog = helpers.fakeOpenDialog('foo');
+ helpers.doKeys('/');
+ cm.openDialog = helpers.fakeOpenDialog('registers');
+ cm.openNotification = helpers.fakeOpenNotification(function(text) {
+ is(/\/\s+foo/.test(text));
+ });
+ helpers.doKeys(':');
+}, {value: ''});
+testVim('search_history', function(cm, vim, helpers) {
+ cm.openDialog = helpers.fakeOpenDialog('this');
+ helpers.doKeys('/');
+ cm.openDialog = helpers.fakeOpenDialog('checks');
+ helpers.doKeys('/');
+ cm.openDialog = helpers.fakeOpenDialog('search');
+ helpers.doKeys('/');
+ cm.openDialog = helpers.fakeOpenDialog('history');
+ helpers.doKeys('/');
+ cm.openDialog = helpers.fakeOpenDialog('checks');
+ helpers.doKeys('/');
+ var onKeyDown;
+ var onKeyUp;
+ var query = '';
+ var keyCodes = {
+ Up: 38,
+ Down: 40
+ };
+ cm.openDialog = function(template, callback, options) {
+ onKeyUp = options.onKeyUp;
+ onKeyDown = options.onKeyDown;
+ };
+ var close = function(newVal) {
+ if (typeof newVal == 'string') query = newVal;
+ }
+ helpers.doKeys('/');
+ onKeyDown({keyCode: keyCodes.Up}, query, close);
+ onKeyUp({keyCode: keyCodes.Up}, query, close);
+ eq(query, 'checks');
+ onKeyDown({keyCode: keyCodes.Up}, query, close);
+ onKeyUp({keyCode: keyCodes.Up}, query, close);
+ eq(query, 'history');
+ onKeyDown({keyCode: keyCodes.Up}, query, close);
+ onKeyUp({keyCode: keyCodes.Up}, query, close);
+ eq(query, 'search');
+ onKeyDown({keyCode: keyCodes.Up}, query, close);
+ onKeyUp({keyCode: keyCodes.Up}, query, close);
+ eq(query, 'this');
+ onKeyDown({keyCode: keyCodes.Down}, query, close);
+ onKeyUp({keyCode: keyCodes.Down}, query, close);
+ eq(query, 'search');
+}, {value: ''});
+testVim('exCommand_history', function(cm, vim, helpers) {
+ cm.openDialog = helpers.fakeOpenDialog('registers');
+ helpers.doKeys(':');
+ cm.openDialog = helpers.fakeOpenDialog('sort');
+ helpers.doKeys(':');
+ cm.openDialog = helpers.fakeOpenDialog('map');
+ helpers.doKeys(':');
+ cm.openDialog = helpers.fakeOpenDialog('invalid');
+ helpers.doKeys(':');
+ var onKeyDown;
+ var onKeyUp;
+ var input = '';
+ var keyCodes = {
+ Up: 38,
+ Down: 40,
+ s: 115
+ };
+ cm.openDialog = function(template, callback, options) {
+ onKeyUp = options.onKeyUp;
+ onKeyDown = options.onKeyDown;
+ };
+ var close = function(newVal) {
+ if (typeof newVal == 'string') input = newVal;
+ }
+ helpers.doKeys(':');
+ onKeyDown({keyCode: keyCodes.Up}, input, close);
+ eq(input, 'invalid');
+ onKeyDown({keyCode: keyCodes.Up}, input, close);
+ eq(input, 'map');
+ onKeyDown({keyCode: keyCodes.Up}, input, close);
+ eq(input, 'sort');
+ onKeyDown({keyCode: keyCodes.Up}, input, close);
+ eq(input, 'registers');
+ onKeyDown({keyCode: keyCodes.s}, '', close);
+ input = 's';
+ onKeyDown({keyCode: keyCodes.Up}, input, close);
+ eq(input, 'sort');
+}, {value: ''});
+testVim('search_clear', function(cm, vim, helpers) {
+ var onKeyDown;
+ var input = '';
+ var keyCodes = {
+ Ctrl: 17,
+ u: 85
+ };
+ cm.openDialog = function(template, callback, options) {
+ onKeyDown = options.onKeyDown;
+ };
+ var close = function(newVal) {
+ if (typeof newVal == 'string') input = newVal;
+ }
+ helpers.doKeys('/');
+ input = 'foo';
+ onKeyDown({keyCode: keyCodes.Ctrl}, input, close);
+ onKeyDown({keyCode: keyCodes.u, ctrlKey: true}, input, close);
+ eq(input, '');
+});
+testVim('exCommand_clear', function(cm, vim, helpers) {
+ var onKeyDown;
+ var input = '';
+ var keyCodes = {
+ Ctrl: 17,
+ u: 85
+ };
+ cm.openDialog = function(template, callback, options) {
+ onKeyDown = options.onKeyDown;
+ };
+ var close = function(newVal) {
+ if (typeof newVal == 'string') input = newVal;
+ }
+ helpers.doKeys(':');
+ input = 'foo';
+ onKeyDown({keyCode: keyCodes.Ctrl}, input, close);
+ onKeyDown({keyCode: keyCodes.u, ctrlKey: true}, input, close);
+ eq(input, '');
+});
+testVim('.', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('2', 'd', 'w');
+ helpers.doKeys('.');
+ eq('5 6', cm.getValue());
+}, { value: '1 2 3 4 5 6'});
+testVim('._repeat', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('2', 'd', 'w');
+ helpers.doKeys('3', '.');
+ eq('6', cm.getValue());
+}, { value: '1 2 3 4 5 6'});
+testVim('._insert', function(cm, vim, helpers) {
+ helpers.doKeys('i');
+ cm.replaceRange('test', cm.getCursor());
+ helpers.doKeys('<Esc>');
+ helpers.doKeys('.');
+ eq('testestt', cm.getValue());
+ helpers.assertCursorAt(0, 6);
+}, { value: ''});
+testVim('._insert_repeat', function(cm, vim, helpers) {
+ helpers.doKeys('i');
+ cm.replaceRange('test', cm.getCursor());
+ cm.setCursor(0, 4);
+ helpers.doKeys('<Esc>');
+ helpers.doKeys('2', '.');
+ eq('testesttestt', cm.getValue());
+ helpers.assertCursorAt(0, 10);
+}, { value: ''});
+testVim('._repeat_insert', function(cm, vim, helpers) {
+ helpers.doKeys('3', 'i');
+ cm.replaceRange('te', cm.getCursor());
+ cm.setCursor(0, 2);
+ helpers.doKeys('<Esc>');
+ helpers.doKeys('.');
+ eq('tetettetetee', cm.getValue());
+ helpers.assertCursorAt(0, 10);
+}, { value: ''});
+testVim('._insert_o', function(cm, vim, helpers) {
+ helpers.doKeys('o');
+ cm.replaceRange('z', cm.getCursor());
+ cm.setCursor(1, 1);
+ helpers.doKeys('<Esc>');
+ helpers.doKeys('.');
+ eq('\nz\nz', cm.getValue());
+ helpers.assertCursorAt(2, 0);
+}, { value: ''});
+testVim('._insert_o_repeat', function(cm, vim, helpers) {
+ helpers.doKeys('o');
+ cm.replaceRange('z', cm.getCursor());
+ helpers.doKeys('<Esc>');
+ cm.setCursor(1, 0);
+ helpers.doKeys('2', '.');
+ eq('\nz\nz\nz', cm.getValue());
+ helpers.assertCursorAt(3, 0);
+}, { value: ''});
+testVim('._insert_o_indent', function(cm, vim, helpers) {
+ helpers.doKeys('o');
+ cm.replaceRange('z', cm.getCursor());
+ helpers.doKeys('<Esc>');
+ cm.setCursor(1, 2);
+ helpers.doKeys('.');
+ eq('{\n z\n z', cm.getValue());
+ helpers.assertCursorAt(2, 2);
+}, { value: '{'});
+testVim('._insert_cw', function(cm, vim, helpers) {
+ helpers.doKeys('c', 'w');
+ cm.replaceRange('test', cm.getCursor());
+ helpers.doKeys('<Esc>');
+ cm.setCursor(0, 3);
+ helpers.doKeys('2', 'l');
+ helpers.doKeys('.');
+ eq('test test word3', cm.getValue());
+ helpers.assertCursorAt(0, 8);
+}, { value: 'word1 word2 word3' });
+testVim('._insert_cw_repeat', function(cm, vim, helpers) {
+ // For some reason, repeat cw in desktop VIM will does not repeat insert mode
+ // changes. Will conform to that behavior.
+ helpers.doKeys('c', 'w');
+ cm.replaceRange('test', cm.getCursor());
+ helpers.doKeys('<Esc>');
+ cm.setCursor(0, 4);
+ helpers.doKeys('l');
+ helpers.doKeys('2', '.');
+ eq('test test', cm.getValue());
+ helpers.assertCursorAt(0, 8);
+}, { value: 'word1 word2 word3' });
+testVim('._delete', function(cm, vim, helpers) {
+ cm.setCursor(0, 5);
+ helpers.doKeys('i');
+ helpers.doInsertModeKeys('Backspace');
+ helpers.doKeys('<Esc>');
+ helpers.doKeys('.');
+ eq('zace', cm.getValue());
+ helpers.assertCursorAt(0, 1);
+}, { value: 'zabcde'});
+testVim('._delete_repeat', function(cm, vim, helpers) {
+ cm.setCursor(0, 6);
+ helpers.doKeys('i');
+ helpers.doInsertModeKeys('Backspace');
+ helpers.doKeys('<Esc>');
+ helpers.doKeys('2', '.');
+ eq('zzce', cm.getValue());
+ helpers.assertCursorAt(0, 1);
+}, { value: 'zzabcde'});
+testVim('._visual_>', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('V', 'j', '>');
+ cm.setCursor(2, 0)
+ helpers.doKeys('.');
+ eq(' 1\n 2\n 3\n 4', cm.getValue());
+ helpers.assertCursorAt(2, 2);
+}, { value: '1\n2\n3\n4'});
+testVim('f;', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('f', 'x');
+ helpers.doKeys(';');
+ helpers.doKeys('2', ';');
+ eq(9, cm.getCursor().ch);
+}, { value: '01x3xx678x'});
+testVim('F;', function(cm, vim, helpers) {
+ cm.setCursor(0, 8);
+ helpers.doKeys('F', 'x');
+ helpers.doKeys(';');
+ helpers.doKeys('2', ';');
+ eq(2, cm.getCursor().ch);
+}, { value: '01x3xx6x8x'});
+testVim('t;', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('t', 'x');
+ helpers.doKeys(';');
+ helpers.doKeys('2', ';');
+ eq(8, cm.getCursor().ch);
+}, { value: '01x3xx678x'});
+testVim('T;', function(cm, vim, helpers) {
+ cm.setCursor(0, 9);
+ helpers.doKeys('T', 'x');
+ helpers.doKeys(';');
+ helpers.doKeys('2', ';');
+ eq(2, cm.getCursor().ch);
+}, { value: '0xx3xx678x'});
+testVim('f,', function(cm, vim, helpers) {
+ cm.setCursor(0, 6);
+ helpers.doKeys('f', 'x');
+ helpers.doKeys(',');
+ helpers.doKeys('2', ',');
+ eq(2, cm.getCursor().ch);
+}, { value: '01x3xx678x'});
+testVim('F,', function(cm, vim, helpers) {
+ cm.setCursor(0, 3);
+ helpers.doKeys('F', 'x');
+ helpers.doKeys(',');
+ helpers.doKeys('2', ',');
+ eq(9, cm.getCursor().ch);
+}, { value: '01x3xx678x'});
+testVim('t,', function(cm, vim, helpers) {
+ cm.setCursor(0, 6);
+ helpers.doKeys('t', 'x');
+ helpers.doKeys(',');
+ helpers.doKeys('2', ',');
+ eq(3, cm.getCursor().ch);
+}, { value: '01x3xx678x'});
+testVim('T,', function(cm, vim, helpers) {
+ cm.setCursor(0, 4);
+ helpers.doKeys('T', 'x');
+ helpers.doKeys(',');
+ helpers.doKeys('2', ',');
+ eq(8, cm.getCursor().ch);
+}, { value: '01x3xx67xx'});
+testVim('fd,;', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('f', '4');
+ cm.setCursor(0, 0);
+ helpers.doKeys('d', ';');
+ eq('56789', cm.getValue());
+ helpers.doKeys('u');
+ cm.setCursor(0, 9);
+ helpers.doKeys('d', ',');
+ eq('01239', cm.getValue());
+}, { value: '0123456789'});
+testVim('Fd,;', function(cm, vim, helpers) {
+ cm.setCursor(0, 9);
+ helpers.doKeys('F', '4');
+ cm.setCursor(0, 9);
+ helpers.doKeys('d', ';');
+ eq('01239', cm.getValue());
+ helpers.doKeys('u');
+ cm.setCursor(0, 0);
+ helpers.doKeys('d', ',');
+ eq('56789', cm.getValue());
+}, { value: '0123456789'});
+testVim('td,;', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('t', '4');
+ cm.setCursor(0, 0);
+ helpers.doKeys('d', ';');
+ eq('456789', cm.getValue());
+ helpers.doKeys('u');
+ cm.setCursor(0, 9);
+ helpers.doKeys('d', ',');
+ eq('012349', cm.getValue());
+}, { value: '0123456789'});
+testVim('Td,;', function(cm, vim, helpers) {
+ cm.setCursor(0, 9);
+ helpers.doKeys('T', '4');
+ cm.setCursor(0, 9);
+ helpers.doKeys('d', ';');
+ eq('012349', cm.getValue());
+ helpers.doKeys('u');
+ cm.setCursor(0, 0);
+ helpers.doKeys('d', ',');
+ eq('456789', cm.getValue());
+}, { value: '0123456789'});
+testVim('fc,;', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('f', '4');
+ cm.setCursor(0, 0);
+ helpers.doKeys('c', ';', '<Esc>');
+ eq('56789', cm.getValue());
+ helpers.doKeys('u');
+ cm.setCursor(0, 9);
+ helpers.doKeys('c', ',');
+ eq('01239', cm.getValue());
+}, { value: '0123456789'});
+testVim('Fc,;', function(cm, vim, helpers) {
+ cm.setCursor(0, 9);
+ helpers.doKeys('F', '4');
+ cm.setCursor(0, 9);
+ helpers.doKeys('c', ';', '<Esc>');
+ eq('01239', cm.getValue());
+ helpers.doKeys('u');
+ cm.setCursor(0, 0);
+ helpers.doKeys('c', ',');
+ eq('56789', cm.getValue());
+}, { value: '0123456789'});
+testVim('tc,;', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('t', '4');
+ cm.setCursor(0, 0);
+ helpers.doKeys('c', ';', '<Esc>');
+ eq('456789', cm.getValue());
+ helpers.doKeys('u');
+ cm.setCursor(0, 9);
+ helpers.doKeys('c', ',');
+ eq('012349', cm.getValue());
+}, { value: '0123456789'});
+testVim('Tc,;', function(cm, vim, helpers) {
+ cm.setCursor(0, 9);
+ helpers.doKeys('T', '4');
+ cm.setCursor(0, 9);
+ helpers.doKeys('c', ';', '<Esc>');
+ eq('012349', cm.getValue());
+ helpers.doKeys('u');
+ cm.setCursor(0, 0);
+ helpers.doKeys('c', ',');
+ eq('456789', cm.getValue());
+}, { value: '0123456789'});
+testVim('fy,;', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('f', '4');
+ cm.setCursor(0, 0);
+ helpers.doKeys('y', ';', 'P');
+ eq('012340123456789', cm.getValue());
+ helpers.doKeys('u');
+ cm.setCursor(0, 9);
+ helpers.doKeys('y', ',', 'P');
+ eq('012345678456789', cm.getValue());
+}, { value: '0123456789'});
+testVim('Fy,;', function(cm, vim, helpers) {
+ cm.setCursor(0, 9);
+ helpers.doKeys('F', '4');
+ cm.setCursor(0, 9);
+ helpers.doKeys('y', ';', 'p');
+ eq('012345678945678', cm.getValue());
+ helpers.doKeys('u');
+ cm.setCursor(0, 0);
+ helpers.doKeys('y', ',', 'P');
+ eq('012340123456789', cm.getValue());
+}, { value: '0123456789'});
+testVim('ty,;', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('t', '4');
+ cm.setCursor(0, 0);
+ helpers.doKeys('y', ';', 'P');
+ eq('01230123456789', cm.getValue());
+ helpers.doKeys('u');
+ cm.setCursor(0, 9);
+ helpers.doKeys('y', ',', 'p');
+ eq('01234567895678', cm.getValue());
+}, { value: '0123456789'});
+testVim('Ty,;', function(cm, vim, helpers) {
+ cm.setCursor(0, 9);
+ helpers.doKeys('T', '4');
+ cm.setCursor(0, 9);
+ helpers.doKeys('y', ';', 'p');
+ eq('01234567895678', cm.getValue());
+ helpers.doKeys('u');
+ cm.setCursor(0, 0);
+ helpers.doKeys('y', ',', 'P');
+ eq('01230123456789', cm.getValue());
+}, { value: '0123456789'});
+testVim('HML', function(cm, vim, helpers) {
+ var lines = 35;
+ var textHeight = cm.defaultTextHeight();
+ cm.setSize(600, lines*textHeight);
+ cm.setCursor(120, 0);
+ helpers.doKeys('H');
+ helpers.assertCursorAt(86, 2);
+ helpers.doKeys('L');
+ helpers.assertCursorAt(120, 4);
+ helpers.doKeys('M');
+ helpers.assertCursorAt(103,4);
+}, { value: (function(){
+ var lines = new Array(100);
+ var upper = ' xx\n';
+ var lower = ' xx\n';
+ upper = lines.join(upper);
+ lower = lines.join(lower);
+ return upper + lower;
+})()});
+
+var zVals = [];
+forEach(['zb','zz','zt','z-','z.','z<CR>'], function(e, idx){
+ var lineNum = 250;
+ var lines = 35;
+ testVim(e, function(cm, vim, helpers) {
+ var k1 = e[0];
+ var k2 = e.substring(1);
+ var textHeight = cm.defaultTextHeight();
+ cm.setSize(600, lines*textHeight);
+ cm.setCursor(lineNum, 0);
+ helpers.doKeys(k1, k2);
+ zVals[idx] = cm.getScrollInfo().top;
+ }, { value: (function(){
+ return new Array(500).join('\n');
+ })()});
+});
+testVim('zb_to_bottom', function(cm, vim, helpers){
+ var lineNum = 250;
+ cm.setSize(600, 35*cm.defaultTextHeight());
+ cm.setCursor(lineNum, 0);
+ helpers.doKeys('z', 'b');
+ var scrollInfo = cm.getScrollInfo();
+ eq(scrollInfo.top + scrollInfo.clientHeight, cm.charCoords(Pos(lineNum, 0), 'local').bottom);
+}, { value: (function(){
+ return new Array(500).join('\n');
+})()});
+testVim('zt_to_top', function(cm, vim, helpers){
+ var lineNum = 250;
+ cm.setSize(600, 35*cm.defaultTextHeight());
+ cm.setCursor(lineNum, 0);
+ helpers.doKeys('z', 't');
+ eq(cm.getScrollInfo().top, cm.charCoords(Pos(lineNum, 0), 'local').top);
+}, { value: (function(){
+ return new Array(500).join('\n');
+})()});
+testVim('zb<zz', function(cm, vim, helpers){
+ eq(zVals[0]<zVals[1], true);
+});
+testVim('zz<zt', function(cm, vim, helpers){
+ eq(zVals[1]<zVals[2], true);
+});
+testVim('zb==z-', function(cm, vim, helpers){
+ eq(zVals[0], zVals[3]);
+});
+testVim('zz==z.', function(cm, vim, helpers){
+ eq(zVals[1], zVals[4]);
+});
+testVim('zt==z<CR>', function(cm, vim, helpers){
+ eq(zVals[2], zVals[5]);
+});
+
+var moveTillCharacterSandbox =
+ 'The quick brown fox \n';
+testVim('moveTillCharacter', function(cm, vim, helpers){
+ cm.setCursor(0, 0);
+ // Search for the 'q'.
+ cm.openDialog = helpers.fakeOpenDialog('q');
+ helpers.doKeys('/');
+ eq(4, cm.getCursor().ch);
+ // Jump to just before the first o in the list.
+ helpers.doKeys('t');
+ helpers.doKeys('o');
+ eq('The quick brown fox \n', cm.getValue());
+ // Delete that one character.
+ helpers.doKeys('d');
+ helpers.doKeys('t');
+ helpers.doKeys('o');
+ eq('The quick bown fox \n', cm.getValue());
+ // Delete everything until the next 'o'.
+ helpers.doKeys('.');
+ eq('The quick box \n', cm.getValue());
+ // An unmatched character should have no effect.
+ helpers.doKeys('d');
+ helpers.doKeys('t');
+ helpers.doKeys('q');
+ eq('The quick box \n', cm.getValue());
+ // Matches should only be possible on single lines.
+ helpers.doKeys('d');
+ helpers.doKeys('t');
+ helpers.doKeys('z');
+ eq('The quick box \n', cm.getValue());
+ // After all that, the search for 'q' should still be active, so the 'N' command
+ // can run it again in reverse. Use that to delete everything back to the 'q'.
+ helpers.doKeys('d');
+ helpers.doKeys('N');
+ eq('The ox \n', cm.getValue());
+ eq(4, cm.getCursor().ch);
+}, { value: moveTillCharacterSandbox});
+testVim('searchForPipe', function(cm, vim, helpers){
+ CodeMirror.Vim.setOption('pcre', false);
+ cm.setCursor(0, 0);
+ // Search for the '|'.
+ cm.openDialog = helpers.fakeOpenDialog('|');
+ helpers.doKeys('/');
+ eq(4, cm.getCursor().ch);
+}, { value: 'this|that'});
+
+
+var scrollMotionSandbox =
+ '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n';
+testVim('scrollMotion', function(cm, vim, helpers){
+ var prevCursor, prevScrollInfo;
+ cm.setCursor(0, 0);
+ // ctrl-y at the top of the file should have no effect.
+ helpers.doKeys('<C-y>');
+ eq(0, cm.getCursor().line);
+ prevScrollInfo = cm.getScrollInfo();
+ helpers.doKeys('<C-e>');
+ eq(1, cm.getCursor().line);
+ is(prevScrollInfo.top < cm.getScrollInfo().top);
+ // Jump to the end of the sandbox.
+ cm.setCursor(1000, 0);
+ prevCursor = cm.getCursor();
+ // ctrl-e at the bottom of the file should have no effect.
+ helpers.doKeys('<C-e>');
+ eq(prevCursor.line, cm.getCursor().line);
+ prevScrollInfo = cm.getScrollInfo();
+ helpers.doKeys('<C-y>');
+ eq(prevCursor.line - 1, cm.getCursor().line, "Y");
+ is(prevScrollInfo.top > cm.getScrollInfo().top);
+}, { value: scrollMotionSandbox});
+
+var squareBracketMotionSandbox = ''+
+ '({\n'+//0
+ ' ({\n'+//11
+ ' /*comment {\n'+//2
+ ' */(\n'+//3
+ '#else \n'+//4
+ ' /* )\n'+//5
+ '#if }\n'+//6
+ ' )}*/\n'+//7
+ ')}\n'+//8
+ '{}\n'+//9
+ '#else {{\n'+//10
+ '{}\n'+//11
+ '}\n'+//12
+ '{\n'+//13
+ '#endif\n'+//14
+ '}\n'+//15
+ '}\n'+//16
+ '#else';//17
+testVim('[[, ]]', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys(']', ']');
+ helpers.assertCursorAt(9,0);
+ helpers.doKeys('2', ']', ']');
+ helpers.assertCursorAt(13,0);
+ helpers.doKeys(']', ']');
+ helpers.assertCursorAt(17,0);
+ helpers.doKeys('[', '[');
+ helpers.assertCursorAt(13,0);
+ helpers.doKeys('2', '[', '[');
+ helpers.assertCursorAt(9,0);
+ helpers.doKeys('[', '[');
+ helpers.assertCursorAt(0,0);
+}, { value: squareBracketMotionSandbox});
+testVim('[], ][', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys(']', '[');
+ helpers.assertCursorAt(12,0);
+ helpers.doKeys('2', ']', '[');
+ helpers.assertCursorAt(16,0);
+ helpers.doKeys(']', '[');
+ helpers.assertCursorAt(17,0);
+ helpers.doKeys('[', ']');
+ helpers.assertCursorAt(16,0);
+ helpers.doKeys('2', '[', ']');
+ helpers.assertCursorAt(12,0);
+ helpers.doKeys('[', ']');
+ helpers.assertCursorAt(0,0);
+}, { value: squareBracketMotionSandbox});
+testVim('[{, ]}', function(cm, vim, helpers) {
+ cm.setCursor(4, 10);
+ helpers.doKeys('[', '{');
+ helpers.assertCursorAt(2,12);
+ helpers.doKeys('2', '[', '{');
+ helpers.assertCursorAt(0,1);
+ cm.setCursor(4, 10);
+ helpers.doKeys(']', '}');
+ helpers.assertCursorAt(6,11);
+ helpers.doKeys('2', ']', '}');
+ helpers.assertCursorAt(8,1);
+ cm.setCursor(0,1);
+ helpers.doKeys(']', '}');
+ helpers.assertCursorAt(8,1);
+ helpers.doKeys('[', '{');
+ helpers.assertCursorAt(0,1);
+}, { value: squareBracketMotionSandbox});
+testVim('[(, ])', function(cm, vim, helpers) {
+ cm.setCursor(4, 10);
+ helpers.doKeys('[', '(');
+ helpers.assertCursorAt(3,14);
+ helpers.doKeys('2', '[', '(');
+ helpers.assertCursorAt(0,0);
+ cm.setCursor(4, 10);
+ helpers.doKeys(']', ')');
+ helpers.assertCursorAt(5,11);
+ helpers.doKeys('2', ']', ')');
+ helpers.assertCursorAt(8,0);
+ helpers.doKeys('[', '(');
+ helpers.assertCursorAt(0,0);
+ helpers.doKeys(']', ')');
+ helpers.assertCursorAt(8,0);
+}, { value: squareBracketMotionSandbox});
+testVim('[*, ]*, [/, ]/', function(cm, vim, helpers) {
+ forEach(['*', '/'], function(key){
+ cm.setCursor(7, 0);
+ helpers.doKeys('2', '[', key);
+ helpers.assertCursorAt(2,2);
+ helpers.doKeys('2', ']', key);
+ helpers.assertCursorAt(7,5);
+ });
+}, { value: squareBracketMotionSandbox});
+testVim('[#, ]#', function(cm, vim, helpers) {
+ cm.setCursor(10, 3);
+ helpers.doKeys('2', '[', '#');
+ helpers.assertCursorAt(4,0);
+ helpers.doKeys('5', ']', '#');
+ helpers.assertCursorAt(17,0);
+ cm.setCursor(10, 3);
+ helpers.doKeys(']', '#');
+ helpers.assertCursorAt(14,0);
+}, { value: squareBracketMotionSandbox});
+testVim('[m, ]m, [M, ]M', function(cm, vim, helpers) {
+ cm.setCursor(11, 0);
+ helpers.doKeys('[', 'm');
+ helpers.assertCursorAt(10,7);
+ helpers.doKeys('4', '[', 'm');
+ helpers.assertCursorAt(1,3);
+ helpers.doKeys('5', ']', 'm');
+ helpers.assertCursorAt(11,0);
+ helpers.doKeys('[', 'M');
+ helpers.assertCursorAt(9,1);
+ helpers.doKeys('3', ']', 'M');
+ helpers.assertCursorAt(15,0);
+ helpers.doKeys('5', '[', 'M');
+ helpers.assertCursorAt(7,3);
+}, { value: squareBracketMotionSandbox});
+
+// Ex mode tests
+testVim('ex_go_to_line', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doEx('4');
+ helpers.assertCursorAt(3, 0);
+}, { value: 'a\nb\nc\nd\ne\n'});
+testVim('ex_write', function(cm, vim, helpers) {
+ var tmp = CodeMirror.commands.save;
+ var written;
+ var actualCm;
+ CodeMirror.commands.save = function(cm) {
+ written = true;
+ actualCm = cm;
+ };
+ // Test that w, wr, wri ... write all trigger :write.
+ var command = 'write';
+ for (var i = 1; i < command.length; i++) {
+ written = false;
+ actualCm = null;
+ helpers.doEx(command.substring(0, i));
+ eq(written, true);
+ eq(actualCm, cm);
+ }
+ CodeMirror.commands.save = tmp;
+});
+testVim('ex_sort', function(cm, vim, helpers) {
+ helpers.doEx('sort');
+ eq('Z\na\nb\nc\nd', cm.getValue());
+}, { value: 'b\nZ\nd\nc\na'});
+testVim('ex_sort_reverse', function(cm, vim, helpers) {
+ helpers.doEx('sort!');
+ eq('d\nc\nb\na', cm.getValue());
+}, { value: 'b\nd\nc\na'});
+testVim('ex_sort_range', function(cm, vim, helpers) {
+ helpers.doEx('2,3sort');
+ eq('b\nc\nd\na', cm.getValue());
+}, { value: 'b\nd\nc\na'});
+testVim('ex_sort_oneline', function(cm, vim, helpers) {
+ helpers.doEx('2sort');
+ // Expect no change.
+ eq('b\nd\nc\na', cm.getValue());
+}, { value: 'b\nd\nc\na'});
+testVim('ex_sort_ignoreCase', function(cm, vim, helpers) {
+ helpers.doEx('sort i');
+ eq('a\nb\nc\nd\nZ', cm.getValue());
+}, { value: 'b\nZ\nd\nc\na'});
+testVim('ex_sort_unique', function(cm, vim, helpers) {
+ helpers.doEx('sort u');
+ eq('Z\na\nb\nc\nd', cm.getValue());
+}, { value: 'b\nZ\na\na\nd\na\nc\na'});
+testVim('ex_sort_decimal', function(cm, vim, helpers) {
+ helpers.doEx('sort d');
+ eq('d3\n s5\n6\n.9', cm.getValue());
+}, { value: '6\nd3\n s5\n.9'});
+testVim('ex_sort_decimal_negative', function(cm, vim, helpers) {
+ helpers.doEx('sort d');
+ eq('z-9\nd3\n s5\n6\n.9', cm.getValue());
+}, { value: '6\nd3\n s5\n.9\nz-9'});
+testVim('ex_sort_decimal_reverse', function(cm, vim, helpers) {
+ helpers.doEx('sort! d');
+ eq('.9\n6\n s5\nd3', cm.getValue());
+}, { value: '6\nd3\n s5\n.9'});
+testVim('ex_sort_hex', function(cm, vim, helpers) {
+ helpers.doEx('sort x');
+ eq(' s5\n6\n.9\n&0xB\nd3', cm.getValue());
+}, { value: '6\nd3\n s5\n&0xB\n.9'});
+testVim('ex_sort_octal', function(cm, vim, helpers) {
+ helpers.doEx('sort o');
+ eq('.8\n.9\nd3\n s5\n6', cm.getValue());
+}, { value: '6\nd3\n s5\n.9\n.8'});
+testVim('ex_sort_decimal_mixed', function(cm, vim, helpers) {
+ helpers.doEx('sort d');
+ eq('y\nz\nc1\nb2\na3', cm.getValue());
+}, { value: 'a3\nz\nc1\ny\nb2'});
+testVim('ex_sort_decimal_mixed_reverse', function(cm, vim, helpers) {
+ helpers.doEx('sort! d');
+ eq('a3\nb2\nc1\nz\ny', cm.getValue());
+}, { value: 'a3\nz\nc1\ny\nb2'});
+testVim('ex_sort_patterns_not_supported', function(cm, vim, helpers) {
+ var notified = false;
+ cm.openNotification = helpers.fakeOpenNotification(function(text) {
+ notified = /patterns not supported/.test(text);
+ });
+ helpers.doEx('sort /abc/');
+ is(notified, 'No notification.');
+});
+// test for :global command
+testVim('ex_global', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doEx('g/one/s//two');
+ eq('two two\n two two\n two two', cm.getValue());
+ helpers.doEx('1,2g/two/s//one');
+ eq('one one\n one one\n two two', cm.getValue());
+}, {value: 'one one\n one one\n one one'});
+testVim('ex_global_confirm', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ var onKeyDown;
+ var openDialogSave = cm.openDialog;
+ var KEYCODES = {
+ a: 65,
+ n: 78,
+ q: 81,
+ y: 89
+ };
+ // Intercept the ex command, 'global'
+ cm.openDialog = function(template, callback, options) {
+ // Intercept the prompt for the embedded ex command, 'substitute'
+ cm.openDialog = function(template, callback, options) {
+ onKeyDown = options.onKeyDown;
+ };
+ callback('g/one/s//two/gc');
+ };
+ helpers.doKeys(':');
+ var close = function() {};
+ onKeyDown({keyCode: KEYCODES.n}, '', close);
+ onKeyDown({keyCode: KEYCODES.y}, '', close);
+ onKeyDown({keyCode: KEYCODES.a}, '', close);
+ onKeyDown({keyCode: KEYCODES.q}, '', close);
+ onKeyDown({keyCode: KEYCODES.y}, '', close);
+ eq('one two\n two two\n one one\n two one\n one one', cm.getValue());
+}, {value: 'one one\n one one\n one one\n one one\n one one'});
+// Basic substitute tests.
+testVim('ex_substitute_same_line', function(cm, vim, helpers) {
+ cm.setCursor(1, 0);
+ helpers.doEx('s/one/two/g');
+ eq('one one\n two two', cm.getValue());
+}, { value: 'one one\n one one'});
+testVim('ex_substitute_full_file', function(cm, vim, helpers) {
+ cm.setCursor(1, 0);
+ helpers.doEx('%s/one/two/g');
+ eq('two two\n two two', cm.getValue());
+}, { value: 'one one\n one one'});
+testVim('ex_substitute_input_range', function(cm, vim, helpers) {
+ cm.setCursor(1, 0);
+ helpers.doEx('1,3s/\\d/0/g');
+ eq('0\n0\n0\n4', cm.getValue());
+}, { value: '1\n2\n3\n4' });
+testVim('ex_substitute_visual_range', function(cm, vim, helpers) {
+ cm.setCursor(1, 0);
+ // Set last visual mode selection marks '< and '> at lines 2 and 4
+ helpers.doKeys('V', '2', 'j', 'v');
+ helpers.doEx('\'<,\'>s/\\d/0/g');
+ eq('1\n0\n0\n0\n5', cm.getValue());
+}, { value: '1\n2\n3\n4\n5' });
+testVim('ex_substitute_empty_query', function(cm, vim, helpers) {
+ // If the query is empty, use last query.
+ cm.setCursor(1, 0);
+ cm.openDialog = helpers.fakeOpenDialog('1');
+ helpers.doKeys('/');
+ helpers.doEx('s//b/g');
+ eq('abb ab2 ab3', cm.getValue());
+}, { value: 'a11 a12 a13' });
+testVim('ex_substitute_javascript', function(cm, vim, helpers) {
+ CodeMirror.Vim.setOption('pcre', false);
+ cm.setCursor(1, 0);
+ // Throw all the things that javascript likes to treat as special values
+ // into the replace part. All should be literal (this is VIM).
+ helpers.doEx('s/\\(\\d+\\)/$$ $\' $` $& \\1/g')
+ eq('a $$ $\' $` $& 0 b', cm.getValue());
+}, { value: 'a 0 b' });
+testVim('ex_substitute_empty_arguments', function(cm,vim,helpers) {
+ cm.setCursor(0, 0);
+ helpers.doEx('s/a/b/g');
+ cm.setCursor(1, 0);
+ helpers.doEx('s');
+ eq('b b\nb a', cm.getValue());
+}, {value: 'a a\na a'});
+
+// More complex substitute tests that test both pcre and nopcre options.
+function testSubstitute(name, options) {
+ testVim(name + '_pcre', function(cm, vim, helpers) {
+ cm.setCursor(1, 0);
+ CodeMirror.Vim.setOption('pcre', true);
+ helpers.doEx(options.expr);
+ eq(options.expectedValue, cm.getValue());
+ }, options);
+ // If no noPcreExpr is defined, assume that it's the same as the expr.
+ var noPcreExpr = options.noPcreExpr ? options.noPcreExpr : options.expr;
+ testVim(name + '_nopcre', function(cm, vim, helpers) {
+ cm.setCursor(1, 0);
+ CodeMirror.Vim.setOption('pcre', false);
+ helpers.doEx(noPcreExpr);
+ eq(options.expectedValue, cm.getValue());
+ }, options);
+}
+testSubstitute('ex_substitute_capture', {
+ value: 'a11 a12 a13',
+ expectedValue: 'a1111 a1212 a1313',
+ // $n is a backreference
+ expr: 's/(\\d+)/$1$1/g',
+ // \n is a backreference.
+ noPcreExpr: 's/\\(\\d+\\)/\\1\\1/g'});
+testSubstitute('ex_substitute_capture2', {
+ value: 'a 0 b',
+ expectedValue: 'a $00 b',
+ expr: 's/(\\d+)/$$$1$1/g',
+ noPcreExpr: 's/\\(\\d+\\)/$\\1\\1/g'});
+testSubstitute('ex_substitute_nocapture', {
+ value: 'a11 a12 a13',
+ expectedValue: 'a$1$1 a$1$1 a$1$1',
+ expr: 's/(\\d+)/$$1$$1/g',
+ noPcreExpr: 's/\\(\\d+\\)/$1$1/g'});
+testSubstitute('ex_substitute_nocapture2', {
+ value: 'a 0 b',
+ expectedValue: 'a $10 b',
+ expr: 's/(\\d+)/$$1$1/g',
+ noPcreExpr: 's/\\(\\d+\\)/\\$1\\1/g'});
+testSubstitute('ex_substitute_nocapture', {
+ value: 'a b c',
+ expectedValue: 'a $ c',
+ expr: 's/b/$$/',
+ noPcreExpr: 's/b/$/'});
+testSubstitute('ex_substitute_slash_regex', {
+ value: 'one/two \n three/four',
+ expectedValue: 'one|two \n three|four',
+ expr: '%s/\\//|'});
+testSubstitute('ex_substitute_pipe_regex', {
+ value: 'one|two \n three|four',
+ expectedValue: 'one,two \n three,four',
+ expr: '%s/\\|/,/',
+ noPcreExpr: '%s/|/,/'});
+testSubstitute('ex_substitute_or_regex', {
+ value: 'one|two \n three|four',
+ expectedValue: 'ana|twa \n thraa|faar',
+ expr: '%s/o|e|u/a/g',
+ noPcreExpr: '%s/o\\|e\\|u/a/g'});
+testSubstitute('ex_substitute_or_word_regex', {
+ value: 'one|two \n three|four',
+ expectedValue: 'five|five \n three|four',
+ expr: '%s/(one|two)/five/g',
+ noPcreExpr: '%s/\\(one\\|two\\)/five/g'});
+testSubstitute('ex_substitute_backslashslash_regex', {
+ value: 'one\\two \n three\\four',
+ expectedValue: 'one,two \n three,four',
+ expr: '%s/\\\\/,'});
+testSubstitute('ex_substitute_slash_replacement', {
+ value: 'one,two \n three,four',
+ expectedValue: 'one/two \n three/four',
+ expr: '%s/,/\\/'});
+testSubstitute('ex_substitute_backslash_replacement', {
+ value: 'one,two \n three,four',
+ expectedValue: 'one\\two \n three\\four',
+ expr: '%s/,/\\\\/g'});
+testSubstitute('ex_substitute_multibackslash_replacement', {
+ value: 'one,two \n three,four',
+ expectedValue: 'one\\\\\\\\two \n three\\\\\\\\four', // 2*8 backslashes.
+ expr: '%s/,/\\\\\\\\\\\\\\\\/g'}); // 16 backslashes.
+testSubstitute('ex_substitute_newline_replacement', {
+ value: 'one,two \n three,four',
+ expectedValue: 'one\ntwo \n three\nfour',
+ expr: '%s/,/\\n/g'});
+testSubstitute('ex_substitute_braces_word', {
+ value: 'ababab abb ab{2}',
+ expectedValue: 'ab abb ab{2}',
+ expr: '%s/(ab){2}//g',
+ noPcreExpr: '%s/\\(ab\\)\\{2\\}//g'});
+testSubstitute('ex_substitute_braces_range', {
+ value: 'a aa aaa aaaa',
+ expectedValue: 'a a',
+ expr: '%s/a{2,3}//g',
+ noPcreExpr: '%s/a\\{2,3\\}//g'});
+testSubstitute('ex_substitute_braces_literal', {
+ value: 'ababab abb ab{2}',
+ expectedValue: 'ababab abb ',
+ expr: '%s/ab\\{2\\}//g',
+ noPcreExpr: '%s/ab{2}//g'});
+testSubstitute('ex_substitute_braces_char', {
+ value: 'ababab abb ab{2}',
+ expectedValue: 'ababab ab{2}',
+ expr: '%s/ab{2}//g',
+ noPcreExpr: '%s/ab\\{2\\}//g'});
+testSubstitute('ex_substitute_braces_no_escape', {
+ value: 'ababab abb ab{2}',
+ expectedValue: 'ababab ab{2}',
+ expr: '%s/ab{2}//g',
+ noPcreExpr: '%s/ab\\{2}//g'});
+testSubstitute('ex_substitute_count', {
+ value: '1\n2\n3\n4',
+ expectedValue: '1\n0\n0\n4',
+ expr: 's/\\d/0/i 2'});
+testSubstitute('ex_substitute_count_with_range', {
+ value: '1\n2\n3\n4',
+ expectedValue: '1\n2\n0\n0',
+ expr: '1,3s/\\d/0/ 3'});
+testSubstitute('ex_substitute_not_global', {
+ value: 'aaa\nbaa\ncaa',
+ expectedValue: 'xaa\nbxa\ncxa',
+ expr: '%s/a/x/'});
+function testSubstituteConfirm(name, command, initialValue, expectedValue, keys, finalPos) {
+ testVim(name, function(cm, vim, helpers) {
+ var savedOpenDialog = cm.openDialog;
+ var savedKeyName = CodeMirror.keyName;
+ var onKeyDown;
+ var recordedCallback;
+ var closed = true; // Start out closed, set false on second openDialog.
+ function close() {
+ closed = true;
+ }
+ // First openDialog should save callback.
+ cm.openDialog = function(template, callback, options) {
+ recordedCallback = callback;
+ }
+ // Do first openDialog.
+ helpers.doKeys(':');
+ // Second openDialog should save keyDown handler.
+ cm.openDialog = function(template, callback, options) {
+ onKeyDown = options.onKeyDown;
+ closed = false;
+ };
+ // Return the command to Vim and trigger second openDialog.
+ recordedCallback(command);
+ // The event should really use keyCode, but here just mock it out and use
+ // key and replace keyName to just return key.
+ CodeMirror.keyName = function (e) { return e.key; }
+ keys = keys.toUpperCase();
+ for (var i = 0; i < keys.length; i++) {
+ is(!closed);
+ onKeyDown({ key: keys.charAt(i) }, '', close);
+ }
+ try {
+ eq(expectedValue, cm.getValue());
+ helpers.assertCursorAt(finalPos);
+ is(closed);
+ } catch(e) {
+ throw e
+ } finally {
+ // Restore overridden functions.
+ CodeMirror.keyName = savedKeyName;
+ cm.openDialog = savedOpenDialog;
+ }
+ }, { value: initialValue });
+};
+testSubstituteConfirm('ex_substitute_confirm_emptydoc',
+ '%s/x/b/c', '', '', '', makeCursor(0, 0));
+testSubstituteConfirm('ex_substitute_confirm_nomatch',
+ '%s/x/b/c', 'ba a\nbab', 'ba a\nbab', '', makeCursor(0, 0));
+testSubstituteConfirm('ex_substitute_confirm_accept',
+ '%s/a/b/cg', 'ba a\nbab', 'bb b\nbbb', 'yyy', makeCursor(1, 1));
+testSubstituteConfirm('ex_substitute_confirm_random_keys',
+ '%s/a/b/cg', 'ba a\nbab', 'bb b\nbbb', 'ysdkywerty', makeCursor(1, 1));
+testSubstituteConfirm('ex_substitute_confirm_some',
+ '%s/a/b/cg', 'ba a\nbab', 'bb a\nbbb', 'yny', makeCursor(1, 1));
+testSubstituteConfirm('ex_substitute_confirm_all',
+ '%s/a/b/cg', 'ba a\nbab', 'bb b\nbbb', 'a', makeCursor(1, 1));
+testSubstituteConfirm('ex_substitute_confirm_accept_then_all',
+ '%s/a/b/cg', 'ba a\nbab', 'bb b\nbbb', 'ya', makeCursor(1, 1));
+testSubstituteConfirm('ex_substitute_confirm_quit',
+ '%s/a/b/cg', 'ba a\nbab', 'bb a\nbab', 'yq', makeCursor(0, 3));
+testSubstituteConfirm('ex_substitute_confirm_last',
+ '%s/a/b/cg', 'ba a\nbab', 'bb b\nbab', 'yl', makeCursor(0, 3));
+testSubstituteConfirm('ex_substitute_confirm_oneline',
+ '1s/a/b/cg', 'ba a\nbab', 'bb b\nbab', 'yl', makeCursor(0, 3));
+testSubstituteConfirm('ex_substitute_confirm_range_accept',
+ '1,2s/a/b/cg', 'aa\na \na\na', 'bb\nb \na\na', 'yyy', makeCursor(1, 0));
+testSubstituteConfirm('ex_substitute_confirm_range_some',
+ '1,3s/a/b/cg', 'aa\na \na\na', 'ba\nb \nb\na', 'ynyy', makeCursor(2, 0));
+testSubstituteConfirm('ex_substitute_confirm_range_all',
+ '1,3s/a/b/cg', 'aa\na \na\na', 'bb\nb \nb\na', 'a', makeCursor(2, 0));
+testSubstituteConfirm('ex_substitute_confirm_range_last',
+ '1,3s/a/b/cg', 'aa\na \na\na', 'bb\nb \na\na', 'yyl', makeCursor(1, 0));
+//:noh should clear highlighting of search-results but allow to resume search through n
+testVim('ex_noh_clearSearchHighlight', function(cm, vim, helpers) {
+ cm.openDialog = helpers.fakeOpenDialog('match');
+ helpers.doKeys('?');
+ helpers.doEx('noh');
+ eq(vim.searchState_.getOverlay(),null,'match-highlighting wasn\'t cleared');
+ helpers.doKeys('n');
+ helpers.assertCursorAt(0, 11,'can\'t resume search after clearing highlighting');
+}, { value: 'match nope match \n nope Match' });
+testVim('ex_yank', function (cm, vim, helpers) {
+ var curStart = makeCursor(3, 0);
+ cm.setCursor(curStart);
+ helpers.doEx('y');
+ var register = helpers.getRegisterController().getRegister();
+ var line = cm.getLine(3);
+ eq(line + '\n', register.toString());
+});
+testVim('set_boolean', function(cm, vim, helpers) {
+ CodeMirror.Vim.defineOption('testoption', true, 'boolean');
+ // Test default value is set.
+ is(CodeMirror.Vim.getOption('testoption'));
+ try {
+ // Test fail to set to non-boolean
+ CodeMirror.Vim.setOption('testoption', '5');
+ fail();
+ } catch (expected) {};
+ // Test setOption
+ CodeMirror.Vim.setOption('testoption', false);
+ is(!CodeMirror.Vim.getOption('testoption'));
+});
+testVim('ex_set_boolean', function(cm, vim, helpers) {
+ CodeMirror.Vim.defineOption('testoption', true, 'boolean');
+ // Test default value is set.
+ is(CodeMirror.Vim.getOption('testoption'));
+ try {
+ // Test fail to set to non-boolean
+ helpers.doEx('set testoption=22');
+ fail();
+ } catch (expected) {};
+ // Test setOption
+ helpers.doEx('set notestoption');
+ is(!CodeMirror.Vim.getOption('testoption'));
+});
+testVim('set_string', function(cm, vim, helpers) {
+ CodeMirror.Vim.defineOption('testoption', 'a', 'string');
+ // Test default value is set.
+ eq('a', CodeMirror.Vim.getOption('testoption'));
+ try {
+ // Test fail to set non-string.
+ CodeMirror.Vim.setOption('testoption', true);
+ fail();
+ } catch (expected) {};
+ try {
+ // Test fail to set 'notestoption'
+ CodeMirror.Vim.setOption('notestoption', 'b');
+ fail();
+ } catch (expected) {};
+ // Test setOption
+ CodeMirror.Vim.setOption('testoption', 'c');
+ eq('c', CodeMirror.Vim.getOption('testoption'));
+});
+testVim('ex_set_string', function(cm, vim, helpers) {
+ CodeMirror.Vim.defineOption('testopt', 'a', 'string');
+ // Test default value is set.
+ eq('a', CodeMirror.Vim.getOption('testopt'));
+ try {
+ // Test fail to set 'notestopt'
+ helpers.doEx('set notestopt=b');
+ fail();
+ } catch (expected) {};
+ // Test setOption
+ helpers.doEx('set testopt=c')
+ eq('c', CodeMirror.Vim.getOption('testopt'));
+ helpers.doEx('set testopt=c')
+ eq('c', CodeMirror.Vim.getOption('testopt', cm)); //local || global
+ eq('c', CodeMirror.Vim.getOption('testopt', cm, {scope: 'local'})); // local
+ eq('c', CodeMirror.Vim.getOption('testopt', cm, {scope: 'global'})); // global
+ eq('c', CodeMirror.Vim.getOption('testopt')); // global
+ // Test setOption global
+ helpers.doEx('setg testopt=d')
+ eq('c', CodeMirror.Vim.getOption('testopt', cm));
+ eq('c', CodeMirror.Vim.getOption('testopt', cm, {scope: 'local'}));
+ eq('d', CodeMirror.Vim.getOption('testopt', cm, {scope: 'global'}));
+ eq('d', CodeMirror.Vim.getOption('testopt'));
+ // Test setOption local
+ helpers.doEx('setl testopt=e')
+ eq('e', CodeMirror.Vim.getOption('testopt', cm));
+ eq('e', CodeMirror.Vim.getOption('testopt', cm, {scope: 'local'}));
+ eq('d', CodeMirror.Vim.getOption('testopt', cm, {scope: 'global'}));
+ eq('d', CodeMirror.Vim.getOption('testopt'));
+});
+testVim('ex_set_callback', function(cm, vim, helpers) {
+ var global;
+
+ function cb(val, cm, cfg) {
+ if (val === undefined) {
+ // Getter
+ if (cm) {
+ return cm._local;
+ } else {
+ return global;
+ }
+ } else {
+ // Setter
+ if (cm) {
+ cm._local = val;
+ } else {
+ global = val;
+ }
+ }
+ }
+
+ CodeMirror.Vim.defineOption('testopt', 'a', 'string', cb);
+ // Test default value is set.
+ eq('a', CodeMirror.Vim.getOption('testopt'));
+ try {
+ // Test fail to set 'notestopt'
+ helpers.doEx('set notestopt=b');
+ fail();
+ } catch (expected) {};
+ // Test setOption (Identical to the string tests, but via callback instead)
+ helpers.doEx('set testopt=c')
+ eq('c', CodeMirror.Vim.getOption('testopt', cm)); //local || global
+ eq('c', CodeMirror.Vim.getOption('testopt', cm, {scope: 'local'})); // local
+ eq('c', CodeMirror.Vim.getOption('testopt', cm, {scope: 'global'})); // global
+ eq('c', CodeMirror.Vim.getOption('testopt')); // global
+ // Test setOption global
+ helpers.doEx('setg testopt=d')
+ eq('c', CodeMirror.Vim.getOption('testopt', cm));
+ eq('c', CodeMirror.Vim.getOption('testopt', cm, {scope: 'local'}));
+ eq('d', CodeMirror.Vim.getOption('testopt', cm, {scope: 'global'}));
+ eq('d', CodeMirror.Vim.getOption('testopt'));
+ // Test setOption local
+ helpers.doEx('setl testopt=e')
+ eq('e', CodeMirror.Vim.getOption('testopt', cm));
+ eq('e', CodeMirror.Vim.getOption('testopt', cm, {scope: 'local'}));
+ eq('d', CodeMirror.Vim.getOption('testopt', cm, {scope: 'global'}));
+ eq('d', CodeMirror.Vim.getOption('testopt'));
+})
+testVim('ex_set_filetype', function(cm, vim, helpers) {
+ CodeMirror.defineMode('test_mode', function() {
+ return {token: function(stream) {
+ stream.match(/^\s+|^\S+/);
+ }};
+ });
+ CodeMirror.defineMode('test_mode_2', function() {
+ return {token: function(stream) {
+ stream.match(/^\s+|^\S+/);
+ }};
+ });
+ // Test mode is set.
+ helpers.doEx('set filetype=test_mode');
+ eq('test_mode', cm.getMode().name);
+ // Test 'ft' alias also sets mode.
+ helpers.doEx('set ft=test_mode_2');
+ eq('test_mode_2', cm.getMode().name);
+});
+testVim('ex_set_filetype_null', function(cm, vim, helpers) {
+ CodeMirror.defineMode('test_mode', function() {
+ return {token: function(stream) {
+ stream.match(/^\s+|^\S+/);
+ }};
+ });
+ cm.setOption('mode', 'test_mode');
+ // Test mode is set to null.
+ helpers.doEx('set filetype=');
+ eq('null', cm.getMode().name);
+});
+// TODO: Reset key maps after each test.
+testVim('ex_map_key2key', function(cm, vim, helpers) {
+ helpers.doEx('map a x');
+ helpers.doKeys('a');
+ helpers.assertCursorAt(0, 0);
+ eq('bc', cm.getValue());
+}, { value: 'abc' });
+testVim('ex_unmap_key2key', function(cm, vim, helpers) {
+ helpers.doEx('unmap a');
+ helpers.doKeys('a');
+ eq('vim-insert', cm.getOption('keyMap'));
+}, { value: 'abc' });
+testVim('ex_unmap_key2key_does_not_remove_default', function(cm, vim, helpers) {
+ try {
+ helpers.doEx('unmap a');
+ fail();
+ } catch (expected) {}
+ helpers.doKeys('a');
+ eq('vim-insert', cm.getOption('keyMap'));
+}, { value: 'abc' });
+testVim('ex_map_key2key_to_colon', function(cm, vim, helpers) {
+ helpers.doEx('map ; :');
+ var dialogOpened = false;
+ cm.openDialog = function() {
+ dialogOpened = true;
+ }
+ helpers.doKeys(';');
+ eq(dialogOpened, true);
+});
+testVim('ex_map_ex2key:', function(cm, vim, helpers) {
+ helpers.doEx('map :del x');
+ helpers.doEx('del');
+ helpers.assertCursorAt(0, 0);
+ eq('bc', cm.getValue());
+}, { value: 'abc' });
+testVim('ex_map_ex2ex', function(cm, vim, helpers) {
+ helpers.doEx('map :del :w');
+ var tmp = CodeMirror.commands.save;
+ var written = false;
+ var actualCm;
+ CodeMirror.commands.save = function(cm) {
+ written = true;
+ actualCm = cm;
+ };
+ helpers.doEx('del');
+ CodeMirror.commands.save = tmp;
+ eq(written, true);
+ eq(actualCm, cm);
+});
+testVim('ex_map_key2ex', function(cm, vim, helpers) {
+ helpers.doEx('map a :w');
+ var tmp = CodeMirror.commands.save;
+ var written = false;
+ var actualCm;
+ CodeMirror.commands.save = function(cm) {
+ written = true;
+ actualCm = cm;
+ };
+ helpers.doKeys('a');
+ CodeMirror.commands.save = tmp;
+ eq(written, true);
+ eq(actualCm, cm);
+});
+testVim('ex_map_key2key_visual_api', function(cm, vim, helpers) {
+ CodeMirror.Vim.map('b', ':w', 'visual');
+ var tmp = CodeMirror.commands.save;
+ var written = false;
+ var actualCm;
+ CodeMirror.commands.save = function(cm) {
+ written = true;
+ actualCm = cm;
+ };
+ // Mapping should not work in normal mode.
+ helpers.doKeys('b');
+ eq(written, false);
+ // Mapping should work in visual mode.
+ helpers.doKeys('v', 'b');
+ eq(written, true);
+ eq(actualCm, cm);
+
+ CodeMirror.commands.save = tmp;
+});
+testVim('ex_imap', function(cm, vim, helpers) {
+ CodeMirror.Vim.map('jk', '<Esc>', 'insert');
+ helpers.doKeys('i');
+ is(vim.insertMode);
+ helpers.doKeys('j', 'k');
+ is(!vim.insertMode);
+});
+testVim('ex_unmap_api', function(cm, vim, helpers) {
+ CodeMirror.Vim.map('<Alt-X>', 'gg', 'normal');
+ is(CodeMirror.Vim.handleKey(cm, "<Alt-X>", "normal"), "Alt-X key is mapped");
+ CodeMirror.Vim.unmap("<Alt-X>", "normal");
+ is(!CodeMirror.Vim.handleKey(cm, "<Alt-X>", "normal"), "Alt-X key is unmapped");
+});
+
+// Testing registration of functions as ex-commands and mapping to <Key>-keys
+testVim('ex_api_test', function(cm, vim, helpers) {
+ var res=false;
+ var val='from';
+ CodeMirror.Vim.defineEx('extest','ext',function(cm,params){
+ if(params.args)val=params.args[0];
+ else res=true;
+ });
+ helpers.doEx(':ext to');
+ eq(val,'to','Defining ex-command failed');
+ CodeMirror.Vim.map('<C-CR><Space>',':ext');
+ helpers.doKeys('<C-CR>','<Space>');
+ is(res,'Mapping to key failed');
+});
+// For now, this test needs to be last because it messes up : for future tests.
+testVim('ex_map_key2key_from_colon', function(cm, vim, helpers) {
+ helpers.doEx('map : x');
+ helpers.doKeys(':');
+ helpers.assertCursorAt(0, 0);
+ eq('bc', cm.getValue());
+}, { value: 'abc' });
+
+// Test event handlers
+testVim('beforeSelectionChange', function(cm, vim, helpers) {
+ cm.setCursor(0, 100);
+ eqPos(cm.getCursor('head'), cm.getCursor('anchor'));
+}, { value: 'abc' });
+
+
diff --git a/devtools/client/sourceeditor/test/codemirror/vimemacs.html b/devtools/client/sourceeditor/test/codemirror/vimemacs.html
new file mode 100644
index 000000000..c12236a7e
--- /dev/null
+++ b/devtools/client/sourceeditor/test/codemirror/vimemacs.html
@@ -0,0 +1,212 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>CodeMirror: VIM/Emacs tests</title>
+ <link rel="stylesheet" href="chrome://devtools/content/sourceeditor/codemirror/lib/codemirror.css">
+ <link rel="stylesheet" href="cm_mode_test.css">
+ <!--<link rel="stylesheet" href="../doc/docs.css">-->
+
+ <script src="chrome://devtools/content/sourceeditor/codemirror/codemirror.bundle.js"></script>
+
+ <style type="text/css">
+ .ok {color: #090;}
+ .fail {color: #e00;}
+ .error {color: #c90;}
+ .done {font-weight: bold;}
+ #progress {
+ background: #45d;
+ color: white;
+ text-shadow: 0 0 1px #45d, 0 0 2px #45d, 0 0 3px #45d;
+ font-weight: bold;
+ white-space: pre;
+ }
+ #testground {
+ visibility: hidden;
+ }
+ #testground.offscreen {
+ visibility: visible;
+ position: absolute;
+ left: -10000px;
+ top: -10000px;
+ }
+ .CodeMirror { border: 1px solid black; }
+ </style>
+ </head>
+ <body>
+ <h1>CodeMirror: VIM/Emacs tests</h1>
+
+ <p>A limited set of programmatic sanity tests for CodeMirror.</p>
+
+ <div style="border: 1px solid black; padding: 1px; max-width: 700px;">
+ <div style="width: 0px;" id=progress><div style="padding: 3px;">Ran <span id="progress_ran">0</span><span id="progress_total"> of 0</span> tests</div></div>
+ </div>
+ <p id=status>Please enable JavaScript...</p>
+ <div id=output></div>
+
+ <div id=testground></div>
+
+ <script src="driver.js"></script>
+ <script src="sublime_test.js"></script>
+ <script src="vim_test.js"></script>
+ <script src="emacs_test.js"></script>
+
+ <!-- Basic tests are in codemirror.html
+ <script src="cm_driver.js"></script>
+ <script src="cm_test.js"></script>
+ <script src="cm_comment_test.js"></script>
+ <script src="cm_doc_test.js"></script>
+ <script src="cm_driver.js"></script>
+ <script src="cm_emacs_test.js"></script>
+ <script src="cm_mode_test.js"></script>
+ <script src="cm_mode_javascript_test.js"></script>
+ <script src="cm_multi_test.js"></script>
+ <script src="cm_search_test.js"></script>
+ -->
+
+ <!-- These modes/addons are not used by Editor
+ <script src="doc_test.js"></script>
+ <script src="../mode/css/css.js"></script>
+ <script src="../mode/css/test.js"></script>
+ <script src="../mode/css/scss_test.js"></script>
+ <script src="../mode/xml/xml.js"></script>
+ <script src="../mode/htmlmixed/htmlmixed.js"></script>
+ <script src="../mode/ruby/ruby.js"></script>
+ <script src="../mode/haml/haml.js"></script>
+ <script src="../mode/haml/test.js"></script>
+ <script src="../mode/markdown/markdown.js"></script>
+ <script src="../mode/markdown/test.js"></script>
+ <script src="../mode/gfm/gfm.js"></script>
+ <script src="../mode/gfm/test.js"></script>
+ <script src="../mode/stex/stex.js"></script>
+ <script src="../mode/stex/test.js"></script>
+ <script src="../mode/xquery/xquery.js"></script>
+ <script src="../mode/xquery/test.js"></script>
+ <script src="../addon/mode/multiplex_test.js"></script>-->
+
+ <script>
+ window.onload = runHarness;
+ CodeMirror.on(window, 'hashchange', runHarness);
+
+ function esc(str) {
+ return str.replace(/[<&]/, function(ch) { return ch == "<" ? "&lt;" : "&amp;"; });
+ }
+
+ var output = document.getElementById("output"),
+ progress = document.getElementById("progress"),
+ progressRan = document.getElementById("progress_ran").childNodes[0],
+ progressTotal = document.getElementById("progress_total").childNodes[0];
+
+ var count = 0,
+ failed = 0,
+ skipped = 0,
+ bad = "",
+ running = false, // Flag that states tests are running
+ quit = false, // Flag to quit tests ASAP
+ verbose = false, // Adds message for *every* test to output
+ phantom = false,
+ Pos = CodeMirror.Pos; // Required for VIM tests
+
+ function runHarness(){
+ if (running) {
+ quit = true;
+ setStatus("Restarting tests...", '', true);
+ setTimeout(function(){runHarness();}, 500);
+ return;
+ }
+ filters = [];
+ verbose = false;
+ if (window.location.hash.substr(1)){
+ var strings = window.location.hash.substr(1).split(",");
+ while (strings.length) {
+ var s = strings.shift();
+ if (s === "verbose")
+ verbose = true;
+ else
+ filters.push(parseTestFilter(decodeURIComponent(s)));
+ }
+ }
+ quit = false;
+ running = true;
+ setStatus("Loading tests...");
+ count = 0;
+ failed = 0;
+ skipped = 0;
+ bad = "";
+ totalTests = countTests();
+ progressTotal.nodeValue = " of " + totalTests;
+ progressRan.nodeValue = count;
+ output.innerHTML = '';
+ document.getElementById("testground").innerHTML = "<form>" +
+ "<textarea id=\"code\" name=\"code\"></textarea>" +
+ "<input type=submit value=ok name=submit>" +
+ "</form>";
+ runTests(displayTest);
+ }
+
+ function setStatus(message, className, force){
+ if (quit && !force) return;
+ if (!message) throw("must provide message");
+ var status = document.getElementById("status").childNodes[0];
+ status.nodeValue = message;
+ status.parentNode.className = className;
+ }
+ function addOutput(name, className, code){
+ var newOutput = document.createElement("dl");
+ var newTitle = document.createElement("dt");
+ newTitle.className = className;
+ newTitle.appendChild(document.createTextNode(name));
+ newOutput.appendChild(newTitle);
+ var newMessage = document.createElement("dd");
+ newMessage.innerHTML = code;
+ newOutput.appendChild(newTitle);
+ newOutput.appendChild(newMessage);
+ output.appendChild(newOutput);
+ }
+ function displayTest(type, name, customMessage) {
+ var message = "???";
+ if (type != "done" && type != "skipped") ++count;
+ progress.style.width = (count * (progress.parentNode.clientWidth - 2) / totalTests) + "px";
+ progressRan.nodeValue = count;
+ if (type == "ok") {
+ message = "Test '" + name + "' succeeded";
+ if (!verbose) customMessage = false;
+ } else if (type == "skipped") {
+ message = "Test '" + name + "' skipped";
+ ++skipped;
+ if (!verbose) customMessage = false;
+ } else if (type == "expected") {
+ message = "Test '" + name + "' failed as expected";
+ if (!verbose) customMessage = false;
+ } else if (type == "error" || type == "fail") {
+ ++failed;
+ message = "Test '" + name + "' failed";
+ } else if (type == "done") {
+ if (failed) {
+ type += " fail";
+ message = failed + " failure" + (failed > 1 ? "s" : "");
+ } else if (count < totalTests) {
+ failed = totalTests - count;
+ type += " fail";
+ message = failed + " failure" + (failed > 1 ? "s" : "");
+ } else {
+ type += " ok";
+ message = "All passed";
+ if (skipped) {
+ message += " (" + skipped + " skipped)";
+ }
+ }
+ progressTotal.nodeValue = '';
+ customMessage = true; // Hack to avoid adding to output
+ }
+ if (window.mozilla_setStatus)
+ mozilla_setStatus(message, type, customMessage);
+ if (verbose && !customMessage) customMessage = message;
+ setStatus(message, type);
+ if (customMessage && customMessage.length > 0) {
+ addOutput(name, type, customMessage);
+ }
+ }
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/sourceeditor/test/css_autocompletion_tests.json b/devtools/client/sourceeditor/test/css_autocompletion_tests.json
new file mode 100644
index 000000000..70ec5be0e
--- /dev/null
+++ b/devtools/client/sourceeditor/test/css_autocompletion_tests.json
@@ -0,0 +1,39 @@
+// Test states to be tested for css state machine in css-autocompelter.js file.
+// Test cases are of the following format:
+// [
+// [
+// line, // The line location of the cursor
+// ch // The column locaiton of the cursor
+// ],
+// suggestions // Array of expected results
+// ]
+[
+ [[0, 10], []],
+ [[4, 7], ['.devtools-menulist', '.devtools-toolbarbutton']],
+ [[5, 8], ['-moz-animation', '-moz-animation-delay', '-moz-animation-direction',
+ '-moz-animation-duration', '-moz-animation-fill-mode',
+ '-moz-animation-iteration-count', '-moz-animation-name',
+ '-moz-animation-play-state', '-moz-animation-timing-function',
+ '-moz-appearance']],
+ [[12, 20], ['none', 'number-input']],
+ [[12, 22], ['none']],
+ [[17, 22], ['hsl', 'hsla']],
+ [[19, 10], ['background', 'background-attachment', 'background-blend-mode',
+ 'background-clip', 'background-color', 'background-image',
+ 'background-origin', 'background-position', 'background-position-x',
+ 'background-position-y', 'background-repeat', 'background-size']],
+ [[21, 9], ["-moz-calc", "auto", "calc", "inherit", "initial","unset"]],
+ [[25, 26], ['.devtools-toolbarbutton > tab',
+ '.devtools-toolbarbutton > hbox',
+ '.devtools-toolbarbutton > .toolbarbutton-menubutton-button']],
+ [[25, 31], ['.devtools-toolbarbutton > hbox.toolbarbutton-menubutton-button']],
+ [[29, 20], ['.devtools-menulist:after', '.devtools-menulist:active']],
+ [[30, 10], ['#devtools-anotherone', '#devtools-itjustgoeson', '#devtools-menu',
+ '#devtools-okstopitnow', '#devtools-toolbarbutton', '#devtools-yetagain']],
+ [[39, 39], ['.devtools-toolbarbutton:not([label]) > tab']],
+ [[43, 51], ['.devtools-toolbarbutton:not([checked=true]):hover:after',
+ '.devtools-toolbarbutton:not([checked=true]):hover:active']],
+ [[58, 36], ['!important;']],
+ [[73, 42], [':lang(', ':last-of-type', ':link', ':last-child']],
+ [[77, 25], ['.visible']],
+]
diff --git a/devtools/client/sourceeditor/test/css_statemachine_testcases.css b/devtools/client/sourceeditor/test/css_statemachine_testcases.css
new file mode 100644
index 000000000..b3149030f
--- /dev/null
+++ b/devtools/client/sourceeditor/test/css_statemachine_testcases.css
@@ -0,0 +1,121 @@
+/* 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/. */
+
+.devtools-toolbar {
+ -moz-appearance: none;
+ padding:4px 3px;border-bottom-width: 1px;
+ border-bottom-style: solid;
+}
+
+#devtools-menu.devtools-menulist,
+.devtools-toolbarbutton#devtools-menu {
+ -moz-appearance: none;
+ -moz-box-align: center;
+ min-width: 78px;
+ min-height: 22px;
+ text-shadow: 0 -1px 0 hsla(210,8%,5%,.45);
+ border: 1px solid hsla(210,8%,5%,.45);
+ border-radius: 3px;
+ background: linear-gradient(hsla(212,7%,57%,.35), hsla(212,7%,57%,.1)) padding-box;
+ box-shadow: 0 1px 0 hsla(210,16%,76%,.15) inset, 0 0 0 1px hsla(210,16%,76%,.15) inset, 0 1px 0 hsla(210,16%,76%,.15);
+ margin: 0 3px;
+ color: inherit;
+}
+
+.devtools-toolbarbutton > hbox.toolbarbutton-menubutton-button {
+ -moz-box-orient: horizontal;
+}
+
+.devtools-menulist:active,
+#devtools-toolbarbutton:focus {
+ outline: 1px dotted hsla(210,30%,85%,0.7);
+ outline-offset : -4px;
+}
+
+.devtools-toolbarbutton:not([label]) {
+ min-width: 32px;
+}
+
+.devtools-toolbarbutton:not([label]) > .toolbarbutton-text {
+ display: none;
+}
+
+.devtools-toolbarbutton:not([checked=true]):hover:active {
+ border-color: hsla(210,8%,5%,.6);
+}
+
+.devtools-menulist["open" ="true"],
+.devtools-toolbarbutton["open" = true],
+.devtools-toolbarbutton[checked= "true"] {
+ border-color: hsla(210,8%,5%,.6) !important;
+}
+
+.devtools-toolbarbutton["checked"="true"] {
+ color: hsl(208,100%,60%);
+}
+
+.devtools-toolbarbutton[checked=true]:hover {
+ background-color: transparent !important;
+}
+
+.devtools-toolbarbutton[checked=true]:hover:active {
+ background-color: hsla(210,8%,5%,.2) !important;
+}
+
+.devtools-toolbarbutton[type=menu-button] > .toolbarbutton-menubutton-button {
+ -moz-appearance: none;
+}
+
+.devtools-sidebar-tabs > tabs > tab:first-of-type {
+ margin-inline-start: -3px;
+}
+
+.devtools-sidebar-tabs > tabs > tab:not(:last-of-type) {
+ background-size: calc(100% - 2px) 100%, 1px 100%;
+}
+
+.hidden-labels-box:not(.visible) > label,
+.hidden-labels-box.visible ~ .hidden-labels-box > label:last-child {
+ display: none;
+}
+
+/* Maximize the size of the viewport when the window is small */
+@media (max-width: 800px) {
+ .category-name {
+ display: none;
+ }
+}
+
+@media all and (min-width: 300px) {
+ #error-box {
+ max-width: 50%;
+ margin: 0 auto;
+ background-image: url('chrome://global/skin/icons/information-32.png');
+ min-height: 36px;
+ padding-inline-start: 38px;
+ }
+
+ button {
+ width: auto !important;
+ min-width: 150px;
+ }
+
+ @keyframes downloadsIndicatorNotificationFinish {
+ from { opacity: 0; transform: scale(1); }
+ 20% {
+ opacity: .65;
+ animation-timing-function: ease-in;
+ } to { opacity: 0;
+ transform: scale(8); }
+ }
+}
+
+@keyframes smooth {
+ from { opacity: 0; transform: scale(1); }
+ 20% { opacity: .65; animation-timing-function: ease-in; }
+ to {
+ opacity : 0;
+ transform: scale(8);
+ }
+}
diff --git a/devtools/client/sourceeditor/test/css_statemachine_tests.json b/devtools/client/sourceeditor/test/css_statemachine_tests.json
new file mode 100644
index 000000000..2e2574b36
--- /dev/null
+++ b/devtools/client/sourceeditor/test/css_statemachine_tests.json
@@ -0,0 +1,84 @@
+// Test states to be tested for css state machine in css-autocompelter.js file.
+// Test cases are of the following format:
+// [
+// [
+// line, // The line location of the cursor
+// ch // The column locaiton of the cursor
+// ],
+// [
+// state, // one of CSS_STATES
+// selectorState, // one of SELECTOR_STATES
+// completing, // what is being completed
+// propertyName, // what property is being completed in case of value state
+// // or the current selector that is being completed
+// ]
+// ]
+[
+ [[0, 10], ['null', '', '', '']],
+ [[4, 3], ['selector', 'class', 'de', '.de']],
+ [[5, 8], ['property', 'null', '-moz-a']],
+ [[5, 21], ['value', 'null', 'no', '-moz-appearance']],
+ [[6, 18], ['property', 'null', 'padding']],
+ [[6, 24], ['value', 'null', '3', 'padding']],
+ [[6, 29], ['property', 'null', 'bo']],
+ [[6, 50], ['value', 'null', '1p', 'border-bottom-width']],
+ [[7, 24], ['value', 'null', 's', 'border-bottom-style']],
+ [[9, 0], ['null', 'null', '', '']],
+ [[10, 6], ['selector', 'id', 'devto', '#devto']],
+ [[10, 17], ['selector', 'class', 'de', '#devtools-menu.de']],
+ [[11, 5], ['selector', 'class', 'devt', '.devt']],
+ [[11, 30], ['selector', 'id', 'devtoo', '.devtools-toolbarbutton#devtoo']],
+ [[12, 10], ['property', 'null', '-moz-app']],
+ [[16, 27], ['value', 'null', 'hsl', 'text-shadow']],
+ [[19, 24], ['value', 'null', 'linear-gra', 'background']],
+ [[19, 55], ['value', 'null', 'hsl', 'background']],
+ [[19, 79], ['value', 'null', 'paddin', 'background']],
+ [[20, 47], ['value', 'null', 'ins', 'box-shadow']],
+ [[22, 15], ['value', 'null', 'inheri', 'color']],
+ [[25, 26], ['selector', 'null', '', '.devtools-toolbarbutton > ']],
+ [[25, 28], ['selector', 'tag', 'hb', '.devtools-toolbarbutton > hb']],
+ [[25, 41], ['selector', 'class', 'toolbarbut', '.devtools-toolbarbutton > hbox.toolbarbut']],
+ [[29, 21], ['selector', 'pseudo', 'ac', '.devtools-menulist:ac']],
+ [[30, 27], ['selector', 'pseudo', 'foc', '#devtools-toolbarbutton:foc']],
+ [[31, 18], ['value', 'null', 'dot', 'outline']],
+ [[32, 25], ['value', 'null', '-4p', 'outline-offset']],
+ [[35, 26], ['selector', 'pseudo', 'no', '.devtools-toolbarbutton:no']],
+ [[35, 28], ['selector', 'null', 'not', '']],
+ [[35, 30], ['selector', 'attribute', 'l', '[l']],
+ [[39, 46], ['selector', 'class', 'toolba', '.devtools-toolbarbutton:not([label]) > .toolba']],
+ [[43, 39], ['selector', 'value', 'tr', '[checked=tr']],
+ [[43, 47], ['selector', 'pseudo', 'hov', '.devtools-toolbarbutton:not([checked=true]):hov']],
+ [[43, 53], ['selector', 'pseudo', 'act', '.devtools-toolbarbutton:not([checked=true]):hover:act']],
+ [[47, 22], ['selector', 'attribute', 'op', '.devtools-menulist[op']],
+ [[47, 33], ['selector', 'value', 'tr', '.devtools-menulist[open =tr']],
+ [[48, 38], ['selector', 'value', 'tr', '.devtools-toolbarbutton[open = tr']],
+ [[49, 40], ['selector', 'value', 'true', '.devtools-toolbarbutton[checked= true']],
+ [[53, 34], ['selector', 'value', '=', '.devtools-toolbarbutton[checked=']],
+ [[58, 38], ['value', 'null', '!impor', 'background-color']],
+ [[61, 41], ['selector', 'pseudo', 'hov', '.devtools-toolbarbutton[checked=true]:hov']],
+ [[65, 47], ['selector', 'class', 'to', '.devtools-toolbarbutton[type=menu-button] > .to']],
+ [[69, 44], ['selector', 'pseudo', 'first-of', '.devtools-sidebar-tabs > tabs > tab:first-of']],
+ [[73, 45], ['selector', 'pseudo', 'last', ':last']],
+ [[77, 27], ['selector', 'class', 'vis', '.vis']],
+ [[78, 34], ['selector', 'class', 'hidd', '.hidden-labels-box.visible ~ .hidd']],
+ [[83, 5], ['media', 'null', 'medi']],
+ [[83, 22], ['media', 'null', '800']],
+ [[84, 9], ['selector', 'class', 'catego', '.catego']],
+ [[89, 9], ['media', 'null', 'al']],
+ [[90, 6], ['selector', 'id', 'err', '#err']],
+ [[93, 11], ['property', 'null', 'backgro']],
+ [[98, 6], ['selector', 'tag', 'butt', 'butt']],
+ [[99, 22], ['value', 'null', '!impor', 'width']],
+ [[103, 5], ['keyframes', 'null', 'ke']],
+ [[104, 7], ['frame', 'null', 'fro']],
+ [[104, 15], ['property', 'null', 'opac']],
+ [[104, 29], ['property', 'null', 'transf']],
+ [[104, 38], ['value', 'null', 'scal', 'transform']],
+ [[105, 8], ['frame', 'null', '']],
+ [[113, 6], ['keyframes', 'null', 'keyfr']],
+ [[114, 4], ['frame', 'null', 'fr']],
+ [[115, 3], ['frame', 'null', '2']],
+ [[117, 8], ['property', 'null', 'opac']],
+ [[117, 16], ['value', 'null', '0', 'opacity']],
+ [[121, 0], ['null', '', '']],
+]
diff --git a/devtools/client/sourceeditor/test/head.js b/devtools/client/sourceeditor/test/head.js
new file mode 100644
index 000000000..4f8473eaf
--- /dev/null
+++ b/devtools/client/sourceeditor/test/head.js
@@ -0,0 +1,163 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from ../../framework/test/shared-head.js */
+"use strict";
+
+// shared-head.js handles imports, constants, and utility functions
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js",
+ this);
+
+const { NetUtil } = require("resource://gre/modules/NetUtil.jsm");
+const Editor = require("devtools/client/sourceeditor/editor");
+const {getClientCssProperties} = require("devtools/shared/fronts/css-properties");
+
+flags.testing = true;
+SimpleTest.registerCleanupFunction(() => {
+ flags.testing = false;
+});
+
+function promiseWaitForFocus() {
+ return new Promise(resolve =>
+ waitForFocus(resolve));
+}
+
+function setup(cb, additionalOpts = {}) {
+ cb = cb || function () {};
+ let def = promise.defer();
+ const opt = "chrome,titlebar,toolbar,centerscreen,resizable,dialog=no";
+ const url = "data:application/vnd.mozilla.xul+xml;charset=UTF-8," +
+ "<?xml version='1.0'?>" +
+ "<?xml-stylesheet href='chrome://global/skin/global.css'?>" +
+ "<window xmlns='http://www.mozilla.org/keymaster/gatekeeper" +
+ "/there.is.only.xul' title='Editor' width='600' height='500'>" +
+ "<box flex='1'/></window>";
+
+ let win = Services.ww.openWindow(null, url, "_blank", opt, null);
+ let opts = {
+ value: "Hello.",
+ lineNumbers: true,
+ foldGutter: true,
+ gutters: ["CodeMirror-linenumbers", "breakpoints", "CodeMirror-foldgutter"],
+ cssProperties: getClientCssProperties()
+ };
+
+ for (let o in additionalOpts) {
+ opts[o] = additionalOpts[o];
+ }
+
+ win.addEventListener("load", function onLoad() {
+ win.removeEventListener("load", onLoad, false);
+
+ waitForFocus(function () {
+ let box = win.document.querySelector("box");
+ let editor = new Editor(opts);
+
+ editor.appendTo(box)
+ .then(() => {
+ def.resolve({
+ ed: editor,
+ win: win,
+ edWin: editor.container.contentWindow.wrappedJSObject
+ });
+ cb(editor, win);
+ }, err => ok(false, err.message));
+ }, win);
+ }, false);
+
+ return def.promise;
+}
+
+function ch(exp, act, label) {
+ is(exp.line, act.line, label + " (line)");
+ is(exp.ch, act.ch, label + " (ch)");
+}
+
+function teardown(ed, win) {
+ ed.destroy();
+ win.close();
+
+ while (gBrowser.tabs.length > 1) {
+ gBrowser.removeCurrentTab();
+ }
+ finish();
+}
+
+/**
+ * Some tests may need to import one or more of the test helper scripts.
+ * A test helper script is simply a js file that contains common test code that
+ * is either not common-enough to be in head.js, or that is located in a
+ * separate directory.
+ * The script will be loaded synchronously and in the test's scope.
+ * @param {String} filePath The file path, relative to the current directory.
+ * Examples:
+ * - "helper_attributes_test_runner.js"
+ * - "../../../commandline/test/helpers.js"
+ */
+function loadHelperScript(filePath) {
+ let testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/"));
+ Services.scriptloader.loadSubScript(testDir + "/" + filePath, this);
+}
+
+/**
+ * This method returns the portion of the input string `source` up to the
+ * [line, ch] location.
+ */
+function limit(source, [line, ch]) {
+ line++;
+ let list = source.split("\n");
+ if (list.length < line) {
+ return source;
+ }
+ if (line == 1) {
+ return list[0].slice(0, ch);
+ }
+ return [...list.slice(0, line - 1), list[line - 1].slice(0, ch)].join("\n");
+}
+
+function read(url) {
+ let scriptableStream = Cc["@mozilla.org/scriptableinputstream;1"]
+ .getService(Ci.nsIScriptableInputStream);
+
+ let channel = NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true
+ });
+ let input = channel.open2();
+ scriptableStream.init(input);
+
+ let data = "";
+ while (input.available()) {
+ data = data.concat(scriptableStream.read(input.available()));
+ }
+ scriptableStream.close();
+ input.close();
+
+ return data;
+}
+
+/**
+ * This function is called by the CodeMirror test runner to report status
+ * messages from the CM tests.
+ * @see codemirror.html
+ */
+function codemirrorSetStatus(statusMsg, type, customMsg) {
+ switch (type) {
+ case "expected":
+ case "ok":
+ ok(1, statusMsg);
+ break;
+ case "error":
+ case "fail":
+ ok(0, statusMsg);
+ break;
+ default:
+ info(statusMsg);
+ break;
+ }
+
+ if (customMsg && typeof customMsg == "string" && customMsg != statusMsg) {
+ info(customMsg);
+ }
+}
diff --git a/devtools/client/sourceeditor/test/helper_codemirror_runner.js b/devtools/client/sourceeditor/test/helper_codemirror_runner.js
new file mode 100644
index 000000000..b9e458472
--- /dev/null
+++ b/devtools/client/sourceeditor/test/helper_codemirror_runner.js
@@ -0,0 +1,38 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* globals codemirrorSetStatus */
+
+"use strict";
+
+function runCodeMirrorTest(browser) {
+ let mm = browser.messageManager;
+ mm.addMessageListener("setStatus", function listener({data}) {
+ let {statusMsg, type, customMsg} = data;
+ codemirrorSetStatus(statusMsg, type, customMsg);
+ });
+ mm.addMessageListener("done", function listener({data}) {
+ ok(!data.failed, "CodeMirror tests all passed");
+ while (gBrowser.tabs.length > 1) {
+ gBrowser.removeCurrentTab();
+ }
+ mm = null;
+ finish();
+ });
+
+ // Interact with the content iframe, giving it a function to
+ // 1) Proxy CM test harness calls into ok() calls
+ // 2) Detecting when it finishes by checking the DOM and
+ // setting a timeout to check again if not.
+ mm.loadFrameScript("data:," +
+ "content.wrappedJSObject.mozilla_setStatus = function(statusMsg, type, customMsg) {" +
+ " sendSyncMessage('setStatus', {statusMsg: statusMsg, type: type, customMsg: customMsg});" +
+ "};" +
+ "function check() { " +
+ " var doc = content.document; var out = doc.getElementById('status'); " +
+ " if (!out || !out.classList.contains('done')) { return setTimeout(check, 100); }" +
+ " sendAsyncMessage('done', { failed: content.wrappedJSObject.failed });" +
+ "}" +
+ "check();"
+ , true);
+}
diff --git a/devtools/client/storage/moz.build b/devtools/client/storage/moz.build
new file mode 100644
index 000000000..0c7f2f46b
--- /dev/null
+++ b/devtools/client/storage/moz.build
@@ -0,0 +1,12 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
+
+DevToolsModules(
+ 'panel.js',
+ 'ui.js'
+)
diff --git a/devtools/client/storage/panel.js b/devtools/client/storage/panel.js
new file mode 100644
index 000000000..c24059d8e
--- /dev/null
+++ b/devtools/client/storage/panel.js
@@ -0,0 +1,87 @@
+/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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 EventEmitter = require("devtools/shared/event-emitter");
+
+loader.lazyRequireGetter(this, "StorageFront",
+ "devtools/shared/fronts/storage", true);
+loader.lazyRequireGetter(this, "StorageUI",
+ "devtools/client/storage/ui", true);
+
+var StoragePanel = this.StoragePanel =
+function StoragePanel(panelWin, toolbox) {
+ EventEmitter.decorate(this);
+
+ this._toolbox = toolbox;
+ this._target = toolbox.target;
+ this._panelWin = panelWin;
+
+ this.destroy = this.destroy.bind(this);
+};
+
+exports.StoragePanel = StoragePanel;
+
+StoragePanel.prototype = {
+ get target() {
+ return this._toolbox.target;
+ },
+
+ get panelWindow() {
+ return this._panelWin;
+ },
+
+ /**
+ * open is effectively an asynchronous constructor
+ */
+ open: function () {
+ let targetPromise;
+ // We always interact with the target as if it were remote
+ if (!this.target.isRemote) {
+ targetPromise = this.target.makeRemote();
+ } else {
+ targetPromise = Promise.resolve(this.target);
+ }
+
+ return targetPromise.then(() => {
+ this.target.on("close", this.destroy);
+ this._front = new StorageFront(this.target.client, this.target.form);
+
+ this.UI = new StorageUI(this._front, this._target,
+ this._panelWin, this._toolbox);
+ this.isReady = true;
+ this.emit("ready");
+
+ return this;
+ }).catch(e => {
+ console.log("error while opening storage panel", e);
+ this.destroy();
+ });
+ },
+
+ /**
+ * Destroy the storage inspector.
+ */
+ destroy: function () {
+ if (!this._destroyed) {
+ this.UI.destroy();
+ this.UI = null;
+
+ // Destroy front to ensure packet handler is removed from client
+ this._front.destroy();
+ this._front = null;
+ this._destroyed = true;
+
+ this._target.off("close", this.destroy);
+ this._target = null;
+ this._toolbox = null;
+ this._panelWin = null;
+ }
+
+ return Promise.resolve(null);
+ },
+};
diff --git a/devtools/client/storage/storage.xul b/devtools/client/storage/storage.xul
new file mode 100644
index 000000000..9fbef5199
--- /dev/null
+++ b/devtools/client/storage/storage.xul
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/content/shared/widgets/widgets.css" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/skin/widgets.css" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/skin/storage.css" type="text/css"?>
+
+<!DOCTYPE window [
+ <!ENTITY % storageDTD SYSTEM "chrome://devtools/locale/storage.dtd">
+ %storageDTD;
+]>
+
+<?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script type="application/javascript;version=1.8"
+ src="chrome://devtools/content/shared/theme-switching.js"/>
+ <script type="text/javascript" src="chrome://global/content/globalOverlay.js"/>
+
+ <commandset id="editMenuCommands"/>
+
+ <popupset id="storagePopupSet">
+ <menupopup id="storage-tree-popup">
+ <menuitem id="storage-tree-popup-delete-all"
+ label="&storage.popupMenu.deleteAllLabel;"/>
+ <menuitem id="storage-tree-popup-delete"/>
+ </menupopup>
+ <menupopup id="storage-table-popup">
+ <menuitem id="storage-table-popup-delete"/>
+ <menuitem id="storage-table-popup-delete-all-from"/>
+ <menuitem id="storage-table-popup-delete-all"
+ label="&storage.popupMenu.deleteAllLabel;"/>
+ </menupopup>
+ </popupset>
+
+ <box flex="1" class="devtools-responsive-container theme-body">
+ <vbox id="storage-tree"/>
+ <splitter class="devtools-side-splitter"/>
+ <vbox flex="1">
+ <hbox id="storage-toolbar" class="devtools-toolbar">
+ <textbox id="storage-searchbox"
+ class="devtools-filterinput"
+ type="search"
+ timeout="200"
+ placeholder="&searchBox.placeholder;"/>
+ </hbox>
+ <vbox id="storage-table" class="theme-sidebar" flex="1"/>
+ </vbox>
+ <splitter class="devtools-side-splitter"/>
+ <vbox id="storage-sidebar" class="devtools-sidebar-tabs" hidden="true">
+ <vbox flex="1"/>
+ </vbox>
+ </box>
+
+</window>
diff --git a/devtools/client/storage/test/.eslintrc.js b/devtools/client/storage/test/.eslintrc.js
new file mode 100644
index 000000000..8d15a76d9
--- /dev/null
+++ b/devtools/client/storage/test/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../../.eslintrc.mochitests.js"
+};
diff --git a/devtools/client/storage/test/browser.ini b/devtools/client/storage/test/browser.ini
new file mode 100644
index 000000000..dd7f48bd7
--- /dev/null
+++ b/devtools/client/storage/test/browser.ini
@@ -0,0 +1,44 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ storage-cache-error.html
+ storage-complex-values.html
+ storage-cookies.html
+ storage-empty-objectstores.html
+ storage-idb-delete-blocked.html
+ storage-listings.html
+ storage-localstorage.html
+ storage-overflow.html
+ storage-search.html
+ storage-secured-iframe.html
+ storage-sessionstorage.html
+ storage-unsecured-iframe.html
+ storage-updates.html
+ head.js
+ !/devtools/client/framework/test/shared-head.js
+
+[browser_storage_basic.js]
+[browser_storage_cache_delete.js]
+[browser_storage_cache_error.js]
+[browser_storage_cookies_delete_all.js]
+[browser_storage_cookies_domain.js]
+[browser_storage_cookies_edit.js]
+[browser_storage_cookies_edit_keyboard.js]
+[browser_storage_cookies_tab_navigation.js]
+[browser_storage_delete.js]
+[browser_storage_delete_all.js]
+[browser_storage_delete_tree.js]
+[browser_storage_dynamic_updates.js]
+[browser_storage_empty_objectstores.js]
+[browser_storage_indexeddb_delete.js]
+[browser_storage_indexeddb_delete_blocked.js]
+[browser_storage_localstorage_edit.js]
+[browser_storage_localstorage_error.js]
+[browser_storage_overflow.js]
+[browser_storage_search.js]
+[browser_storage_search_keyboard_trap.js]
+[browser_storage_sessionstorage_edit.js]
+[browser_storage_sidebar.js]
+[browser_storage_sidebar_update.js]
+[browser_storage_values.js]
diff --git a/devtools/client/storage/test/browser_storage_basic.js b/devtools/client/storage/test/browser_storage_basic.js
new file mode 100644
index 000000000..343d46170
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_basic.js
@@ -0,0 +1,118 @@
+/* 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/. */
+
+// Basic test to assert that the storage tree and table corresponding to each
+// item in the storage tree is correctly displayed
+
+// Entries that should be present in the tree for this test
+// Format for each entry in the array :
+// [
+// ["path", "to", "tree", "item"], - The path to the tree item to click formed
+// by id of each item
+// ["key_value1", "key_value2", ...] - The value of the first (unique) column
+// for each row in the table corresponding
+// to the tree item selected.
+// ]
+// These entries are formed by the cookies, local storage, session storage and
+// indexedDB entries created in storage-listings.html,
+// storage-secured-iframe.html and storage-unsecured-iframe.html
+
+"use strict";
+
+const testCases = [
+ [["cookies", "test1.example.org"],
+ ["c1", "cs2", "c3", "uc1"]],
+ [["cookies", "sectest1.example.org"],
+ ["uc1", "cs2", "sc1"]],
+ [["localStorage", "http://test1.example.org"],
+ ["ls1", "ls2"]],
+ [["localStorage", "http://sectest1.example.org"],
+ ["iframe-u-ls1"]],
+ [["localStorage", "https://sectest1.example.org"],
+ ["iframe-s-ls1"]],
+ [["sessionStorage", "http://test1.example.org"],
+ ["ss1"]],
+ [["sessionStorage", "http://sectest1.example.org"],
+ ["iframe-u-ss1", "iframe-u-ss2"]],
+ [["sessionStorage", "https://sectest1.example.org"],
+ ["iframe-s-ss1"]],
+ [["indexedDB", "http://test1.example.org"],
+ ["idb1", "idb2"]],
+ [["indexedDB", "http://test1.example.org", "idb1"],
+ ["obj1", "obj2"]],
+ [["indexedDB", "http://test1.example.org", "idb2"],
+ ["obj3"]],
+ [["indexedDB", "http://test1.example.org", "idb1", "obj1"],
+ [1, 2, 3]],
+ [["indexedDB", "http://test1.example.org", "idb1", "obj2"],
+ [1]],
+ [["indexedDB", "http://test1.example.org", "idb2", "obj3"],
+ []],
+ [["indexedDB", "http://sectest1.example.org"],
+ []],
+ [["indexedDB", "https://sectest1.example.org"],
+ ["idb-s1", "idb-s2"]],
+ [["indexedDB", "https://sectest1.example.org", "idb-s1"],
+ ["obj-s1"]],
+ [["indexedDB", "https://sectest1.example.org", "idb-s2"],
+ ["obj-s2"]],
+ [["indexedDB", "https://sectest1.example.org", "idb-s1", "obj-s1"],
+ [6, 7]],
+ [["indexedDB", "https://sectest1.example.org", "idb-s2", "obj-s2"],
+ [16]],
+ [["Cache", "http://test1.example.org", "plop"],
+ [MAIN_DOMAIN + "404_cached_file.js",
+ MAIN_DOMAIN + "browser_storage_basic.js"]],
+];
+
+/**
+ * Test that the desired number of tree items are present
+ */
+function testTree() {
+ let doc = gPanelWindow.document;
+ for (let item of testCases) {
+ ok(doc.querySelector("[data-id='" + JSON.stringify(item[0]) + "']"),
+ "Tree item " + item[0] + " should be present in the storage tree");
+ }
+}
+
+/**
+ * Test that correct table entries are shown for each of the tree item
+ */
+function* testTables() {
+ let doc = gPanelWindow.document;
+ // Expand all nodes so that the synthesized click event actually works
+ gUI.tree.expandAll();
+
+ // First tree item is already selected so no clicking and waiting for update
+ for (let id of testCases[0][1]) {
+ ok(doc.querySelector(".table-widget-cell[data-id='" + id + "']"),
+ "Table item " + id + " should be present");
+ }
+
+ // Click rest of the tree items and wait for the table to be updated
+ for (let item of testCases.slice(1)) {
+ yield selectTreeItem(item[0]);
+
+ // Check whether correct number of items are present in the table
+ is(doc.querySelectorAll(
+ ".table-widget-wrapper:first-of-type .table-widget-cell"
+ ).length, item[1].length, "Number of items in table is correct");
+
+ // Check if all the desired items are present in the table
+ for (let id of item[1]) {
+ ok(doc.querySelector(".table-widget-cell[data-id='" + id + "']"),
+ "Table item " + id + " should be present");
+ }
+ }
+}
+
+add_task(function* () {
+ yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-listings.html");
+
+ testTree();
+ yield testTables();
+
+ yield finishTests();
+});
diff --git a/devtools/client/storage/test/browser_storage_cache_delete.js b/devtools/client/storage/test/browser_storage_cache_delete.js
new file mode 100644
index 000000000..f87aa66e8
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_cache_delete.js
@@ -0,0 +1,46 @@
+/* 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/. */
+
+/* import-globals-from ../../framework/test/shared-head.js */
+
+"use strict";
+
+// Test deleting a Cache object from the tree using context menu
+
+add_task(function* () {
+ yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-listings.html");
+
+ let contextMenu = gPanelWindow.document.getElementById("storage-tree-popup");
+ let menuDeleteItem = contextMenu.querySelector("#storage-tree-popup-delete");
+
+ let cacheToDelete = ["Cache", "http://test1.example.org", "plop"];
+
+ info("test state before delete");
+ yield selectTreeItem(cacheToDelete);
+ ok(gUI.tree.isSelected(cacheToDelete), "Cache item is present in the tree");
+
+ info("do the delete");
+ let eventWait = gUI.once("store-objects-updated");
+
+ let selector = `[data-id='${JSON.stringify(cacheToDelete)}'] > .tree-widget-item`;
+ let target = gPanelWindow.document.querySelector(selector);
+ ok(target, "Cache item's tree element is present");
+
+ yield waitForContextMenu(contextMenu, target, () => {
+ info("Opened tree context menu");
+ menuDeleteItem.click();
+
+ let cacheName = cacheToDelete[2];
+ ok(menuDeleteItem.getAttribute("label").includes(cacheName),
+ `Context menu item label contains '${cacheName}')`);
+ });
+
+ yield eventWait;
+
+ info("test state after delete");
+ yield selectTreeItem(cacheToDelete);
+ ok(!gUI.tree.isSelected(cacheToDelete), "Cache item is no longer present in the tree");
+
+ yield finishTests();
+});
diff --git a/devtools/client/storage/test/browser_storage_cache_error.js b/devtools/client/storage/test/browser_storage_cache_error.js
new file mode 100644
index 000000000..dfc6056a7
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_cache_error.js
@@ -0,0 +1,19 @@
+/* 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";
+
+// Test handling errors in CacheStorage
+
+add_task(function* () {
+ yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-cache-error.html");
+
+ const cacheItemId = ["Cache", "javascript:parent.frameContent"];
+
+ yield selectTreeItem(cacheItemId);
+ ok(gUI.tree.isSelected(cacheItemId),
+ `The item ${cacheItemId.join(" > ")} is present in the tree`);
+
+ yield finishTests();
+});
diff --git a/devtools/client/storage/test/browser_storage_cookies_delete_all.js b/devtools/client/storage/test/browser_storage_cookies_delete_all.js
new file mode 100644
index 000000000..6e6008e66
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_cookies_delete_all.js
@@ -0,0 +1,74 @@
+/* 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/. */
+
+/* import-globals-from ../../framework/test/shared-head.js */
+
+"use strict";
+
+// Test deleting all cookies
+
+function* performDelete(store, rowName, deleteAll) {
+ let contextMenu = gPanelWindow.document.getElementById(
+ "storage-table-popup");
+ let menuDeleteAllItem = contextMenu.querySelector(
+ "#storage-table-popup-delete-all");
+ let menuDeleteAllFromItem = contextMenu.querySelector(
+ "#storage-table-popup-delete-all-from");
+
+ let storeName = store.join(" > ");
+
+ yield selectTreeItem(store);
+
+ let eventWait = gUI.once("store-objects-updated");
+
+ let cells = getRowCells(rowName);
+ yield waitForContextMenu(contextMenu, cells.name, () => {
+ info(`Opened context menu in ${storeName}, row '${rowName}'`);
+ if (deleteAll) {
+ menuDeleteAllItem.click();
+ } else {
+ menuDeleteAllFromItem.click();
+ let hostName = cells.host.value;
+ ok(menuDeleteAllFromItem.getAttribute("label").includes(hostName),
+ `Context menu item label contains '${hostName}'`);
+ }
+ });
+
+ yield eventWait;
+}
+
+add_task(function* () {
+ yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-listings.html");
+
+ info("test state before delete");
+ yield checkState([
+ [["cookies", "test1.example.org"], ["c1", "c3", "cs2", "uc1"]],
+ [["cookies", "sectest1.example.org"], ["cs2", "sc1", "uc1"]],
+ ]);
+
+ info("delete all from domain");
+ // delete only cookies that match the host exactly
+ yield performDelete(["cookies", "test1.example.org"], "c1", false);
+
+ info("test state after delete all from domain");
+ yield checkState([
+ // Domain cookies (.example.org) must not be deleted.
+ [["cookies", "test1.example.org"], ["cs2", "uc1"]],
+ [["cookies", "sectest1.example.org"], ["cs2", "sc1", "uc1"]],
+ ]);
+
+ info("delete all");
+ // delete all cookies for host, including domain cookies
+ yield performDelete(["cookies", "sectest1.example.org"], "uc1", true);
+
+ info("test state after delete all");
+ yield checkState([
+ // Domain cookies (.example.org) are deleted too, so deleting in sectest1
+ // also removes stuff from test1.
+ [["cookies", "test1.example.org"], []],
+ [["cookies", "sectest1.example.org"], []],
+ ]);
+
+ yield finishTests();
+});
diff --git a/devtools/client/storage/test/browser_storage_cookies_domain.js b/devtools/client/storage/test/browser_storage_cookies_domain.js
new file mode 100644
index 000000000..dc93d6e67
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_cookies_domain.js
@@ -0,0 +1,21 @@
+/* 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/. */
+
+/* import-globals-from ../../framework/test/shared-head.js */
+
+"use strict";
+
+// Test that cookies with domain equal to full host name are listed.
+// E.g., ".example.org" vs. example.org). Bug 1149497.
+
+add_task(function* () {
+ yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-cookies.html");
+
+ yield checkState([
+ [["cookies", "test1.example.org"],
+ ["test1", "test2", "test3", "test4", "test5"]],
+ ]);
+
+ yield finishTests();
+});
diff --git a/devtools/client/storage/test/browser_storage_cookies_edit.js b/devtools/client/storage/test/browser_storage_cookies_edit.js
new file mode 100644
index 000000000..5818e4864
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_cookies_edit.js
@@ -0,0 +1,22 @@
+/* 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/. */
+
+// Basic test to check the editing of cookies.
+
+"use strict";
+
+add_task(function* () {
+ yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-cookies.html");
+ showAllColumns(true);
+
+ yield editCell("test3", "name", "newTest3");
+ yield editCell("newTest3", "path", "/");
+ yield editCell("newTest3", "host", "test1.example.org");
+ yield editCell("newTest3", "expires", "Tue, 14 Feb 2040 17:41:14 GMT");
+ yield editCell("newTest3", "value", "newValue3");
+ yield editCell("newTest3", "isSecure", "true");
+ yield editCell("newTest3", "isHttpOnly", "true");
+
+ yield finishTests();
+});
diff --git a/devtools/client/storage/test/browser_storage_cookies_edit_keyboard.js b/devtools/client/storage/test/browser_storage_cookies_edit_keyboard.js
new file mode 100644
index 000000000..1208c4376
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_cookies_edit_keyboard.js
@@ -0,0 +1,23 @@
+/* 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/. */
+
+// Basic test to check the editing of cookies with the keyboard.
+
+"use strict";
+
+add_task(function* () {
+ yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-cookies.html");
+ showAllColumns(true);
+
+ yield startCellEdit("test4", "name");
+ yield typeWithTerminator("test6", "VK_TAB");
+ yield typeWithTerminator("/", "VK_TAB");
+ yield typeWithTerminator(".example.org", "VK_TAB");
+ yield typeWithTerminator("Tue, 25 Dec 2040 12:00:00 GMT", "VK_TAB");
+ yield typeWithTerminator("test6value", "VK_TAB");
+ yield typeWithTerminator("false", "VK_TAB");
+ yield typeWithTerminator("false", "VK_TAB");
+
+ yield finishTests();
+});
diff --git a/devtools/client/storage/test/browser_storage_cookies_tab_navigation.js b/devtools/client/storage/test/browser_storage_cookies_tab_navigation.js
new file mode 100644
index 000000000..783a0c844
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_cookies_tab_navigation.js
@@ -0,0 +1,24 @@
+/* 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/. */
+
+// Basic test to check cookie table tab navigation.
+
+"use strict";
+
+add_task(function* () {
+ yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-cookies.html");
+ showAllColumns(true);
+
+ yield startCellEdit("test1", "name");
+
+ PressKeyXTimes("VK_TAB", 18);
+ is(getCurrentEditorValue(), "value3",
+ "We have tabbed to the correct cell.");
+
+ PressKeyXTimes("VK_TAB", 18, {shiftKey: true});
+ is(getCurrentEditorValue(), "test1",
+ "We have shift-tabbed to the correct cell.");
+
+ yield finishTests();
+});
diff --git a/devtools/client/storage/test/browser_storage_delete.js b/devtools/client/storage/test/browser_storage_delete.js
new file mode 100644
index 000000000..c0e2b0ad7
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_delete.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/. */
+
+/* import-globals-from ../../framework/test/shared-head.js */
+
+"use strict";
+
+// Test deleting storage items
+
+const TEST_CASES = [
+ [["localStorage", "http://test1.example.org"],
+ "ls1", "name"],
+ [["sessionStorage", "http://test1.example.org"],
+ "ss1", "name"],
+ [["cookies", "test1.example.org"],
+ "c1", "name"],
+ [["indexedDB", "http://test1.example.org", "idb1", "obj1"],
+ 1, "name"],
+ [["Cache", "http://test1.example.org", "plop"],
+ MAIN_DOMAIN + "404_cached_file.js", "url"],
+];
+
+add_task(function* () {
+ yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-listings.html");
+
+ let contextMenu = gPanelWindow.document.getElementById("storage-table-popup");
+ let menuDeleteItem = contextMenu.querySelector("#storage-table-popup-delete");
+
+ for (let [ treeItem, rowName, cellToClick] of TEST_CASES) {
+ let treeItemName = treeItem.join(" > ");
+
+ info(`Selecting tree item ${treeItemName}`);
+ yield selectTreeItem(treeItem);
+
+ let row = getRowCells(rowName);
+ ok(gUI.table.items.has(rowName), `There is a row '${rowName}' in ${treeItemName}`);
+
+ let eventWait = gUI.once("store-objects-updated");
+
+ yield waitForContextMenu(contextMenu, row[cellToClick], () => {
+ info(`Opened context menu in ${treeItemName}, row '${rowName}'`);
+ menuDeleteItem.click();
+ let truncatedRowName = String(rowName).substr(0, 16);
+ ok(menuDeleteItem.getAttribute("label").includes(truncatedRowName),
+ `Context menu item label contains '${rowName}' (maybe truncated)`);
+ });
+
+ yield eventWait;
+
+ ok(!gUI.table.items.has(rowName),
+ `There is no row '${rowName}' in ${treeItemName} after deletion`);
+ }
+
+ yield finishTests();
+});
diff --git a/devtools/client/storage/test/browser_storage_delete_all.js b/devtools/client/storage/test/browser_storage_delete_all.js
new file mode 100644
index 000000000..c4b6048fb
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_delete_all.js
@@ -0,0 +1,90 @@
+/* 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/. */
+
+/* import-globals-from ../../framework/test/shared-head.js */
+
+"use strict";
+
+// Test deleting all storage items
+
+add_task(function* () {
+ yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-listings.html");
+
+ let contextMenu = gPanelWindow.document.getElementById("storage-table-popup");
+ let menuDeleteAllItem = contextMenu.querySelector(
+ "#storage-table-popup-delete-all");
+
+ info("test state before delete");
+ const beforeState = [
+ [["localStorage", "http://test1.example.org"],
+ ["ls1", "ls2"]],
+ [["localStorage", "http://sectest1.example.org"],
+ ["iframe-u-ls1"]],
+ [["localStorage", "https://sectest1.example.org"],
+ ["iframe-s-ls1"]],
+ [["sessionStorage", "http://test1.example.org"],
+ ["ss1"]],
+ [["sessionStorage", "http://sectest1.example.org"],
+ ["iframe-u-ss1", "iframe-u-ss2"]],
+ [["sessionStorage", "https://sectest1.example.org"],
+ ["iframe-s-ss1"]],
+ [["indexedDB", "http://test1.example.org", "idb1", "obj1"],
+ [1, 2, 3]],
+ [["Cache", "http://test1.example.org", "plop"],
+ [MAIN_DOMAIN + "404_cached_file.js", MAIN_DOMAIN + "browser_storage_basic.js"]],
+ ];
+
+ yield checkState(beforeState);
+
+ info("do the delete");
+ const deleteHosts = [
+ [["localStorage", "https://sectest1.example.org"], "iframe-s-ls1", "name"],
+ [["sessionStorage", "https://sectest1.example.org"], "iframe-s-ss1", "name"],
+ [["indexedDB", "http://test1.example.org", "idb1", "obj1"], 1, "name"],
+ [["Cache", "http://test1.example.org", "plop"],
+ MAIN_DOMAIN + "404_cached_file.js", "url"],
+ ];
+
+ for (let [store, rowName, cellToClick] of deleteHosts) {
+ let storeName = store.join(" > ");
+
+ yield selectTreeItem(store);
+
+ let eventWait = gUI.once("store-objects-cleared");
+
+ let cell = getRowCells(rowName)[cellToClick];
+ yield waitForContextMenu(contextMenu, cell, () => {
+ info(`Opened context menu in ${storeName}, row '${rowName}'`);
+ menuDeleteAllItem.click();
+ });
+
+ yield eventWait;
+ }
+
+ info("test state after delete");
+ const afterState = [
+ // iframes from the same host, one secure, one unsecure, are independent
+ // from each other. Delete all in one doesn't touch the other one.
+ [["localStorage", "http://test1.example.org"],
+ ["ls1", "ls2"]],
+ [["localStorage", "http://sectest1.example.org"],
+ ["iframe-u-ls1"]],
+ [["localStorage", "https://sectest1.example.org"],
+ []],
+ [["sessionStorage", "http://test1.example.org"],
+ ["ss1"]],
+ [["sessionStorage", "http://sectest1.example.org"],
+ ["iframe-u-ss1", "iframe-u-ss2"]],
+ [["sessionStorage", "https://sectest1.example.org"],
+ []],
+ [["indexedDB", "http://test1.example.org", "idb1", "obj1"],
+ []],
+ [["Cache", "http://test1.example.org", "plop"],
+ []],
+ ];
+
+ yield checkState(afterState);
+
+ yield finishTests();
+});
diff --git a/devtools/client/storage/test/browser_storage_delete_tree.js b/devtools/client/storage/test/browser_storage_delete_tree.js
new file mode 100644
index 000000000..867a1c8b6
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_delete_tree.js
@@ -0,0 +1,67 @@
+/* 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/. */
+
+/* import-globals-from ../../framework/test/shared-head.js */
+
+"use strict";
+
+// Test deleting all storage items from the tree.
+
+add_task(function* () {
+ yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-listings.html");
+
+ let contextMenu = gPanelWindow.document.getElementById("storage-tree-popup");
+ let menuDeleteAllItem = contextMenu.querySelector(
+ "#storage-tree-popup-delete-all");
+
+ info("test state before delete");
+ yield checkState([
+ [["cookies", "test1.example.org"], ["c1", "c3", "cs2", "uc1"]],
+ [["localStorage", "http://test1.example.org"], ["ls1", "ls2"]],
+ [["sessionStorage", "http://test1.example.org"], ["ss1"]],
+ [["indexedDB", "http://test1.example.org", "idb1", "obj1"], [1, 2, 3]],
+ [["Cache", "http://test1.example.org", "plop"],
+ [MAIN_DOMAIN + "404_cached_file.js", MAIN_DOMAIN + "browser_storage_basic.js"]],
+ ]);
+
+ info("do the delete");
+ const deleteHosts = [
+ ["cookies", "test1.example.org"],
+ ["localStorage", "http://test1.example.org"],
+ ["sessionStorage", "http://test1.example.org"],
+ ["indexedDB", "http://test1.example.org", "idb1", "obj1"],
+ ["Cache", "http://test1.example.org", "plop"],
+ ];
+
+ for (let store of deleteHosts) {
+ let storeName = store.join(" > ");
+
+ yield selectTreeItem(store);
+
+ let eventName = "store-objects-" +
+ (store[0] == "cookies" ? "updated" : "cleared");
+ let eventWait = gUI.once(eventName);
+
+ let selector = `[data-id='${JSON.stringify(store)}'] > .tree-widget-item`;
+ let target = gPanelWindow.document.querySelector(selector);
+ ok(target, `tree item found in ${storeName}`);
+ yield waitForContextMenu(contextMenu, target, () => {
+ info(`Opened tree context menu in ${storeName}`);
+ menuDeleteAllItem.click();
+ });
+
+ yield eventWait;
+ }
+
+ info("test state after delete");
+ yield checkState([
+ [["cookies", "test1.example.org"], []],
+ [["localStorage", "http://test1.example.org"], []],
+ [["sessionStorage", "http://test1.example.org"], []],
+ [["indexedDB", "http://test1.example.org", "idb1", "obj1"], []],
+ [["Cache", "http://test1.example.org", "plop"], []],
+ ]);
+
+ yield finishTests();
+});
diff --git a/devtools/client/storage/test/browser_storage_dynamic_updates.js b/devtools/client/storage/test/browser_storage_dynamic_updates.js
new file mode 100644
index 000000000..f881146d2
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_dynamic_updates.js
@@ -0,0 +1,213 @@
+/* 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";
+
+add_task(function* () {
+ yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-updates.html");
+
+ let $ = id => gPanelWindow.document.querySelector(id);
+ let $$ = sel => gPanelWindow.document.querySelectorAll(sel);
+
+ gUI.tree.expandAll();
+
+ ok(gUI.sidebar.hidden, "Sidebar is initially hidden");
+ yield selectTableItem("c1");
+
+ // test that value is something initially
+ let initialValue = [[
+ {name: "c1", value: "1.2.3.4.5.6.7"},
+ {name: "c1.Path", value: "/browser"}
+ ], [
+ {name: "c1", value: "Array"},
+ {name: "c1.0", value: "1"},
+ {name: "c1.6", value: "7"}
+ ]];
+
+ // test that value is something initially
+ let finalValue = [[
+ {name: "c1", value: '{"foo": 4,"bar":6}'},
+ {name: "c1.Path", value: "/browser"}
+ ], [
+ {name: "c1", value: "Object"},
+ {name: "c1.foo", value: "4"},
+ {name: "c1.bar", value: "6"}
+ ]];
+ // Check that sidebar shows correct initial value
+ yield findVariableViewProperties(initialValue[0], false);
+ yield findVariableViewProperties(initialValue[1], true);
+ // Check if table shows correct initial value
+ ok($("#value [data-id='c1'].table-widget-cell"), "cell is present");
+ is($("#value [data-id='c1'].table-widget-cell").value, "1.2.3.4.5.6.7",
+ "correct initial value in table");
+ gWindow.addCookie("c1", '{"foo": 4,"bar":6}', "/browser");
+ yield gUI.once("sidebar-updated");
+
+ yield findVariableViewProperties(finalValue[0], false);
+ yield findVariableViewProperties(finalValue[1], true);
+ ok($("#value [data-id='c1'].table-widget-cell"),
+ "cell is present after update");
+ is($("#value [data-id='c1'].table-widget-cell").value, '{"foo": 4,"bar":6}',
+ "correct final value in table");
+
+ // Add a new entry
+ is($$("#value .table-widget-cell").length, 2,
+ "Correct number of rows before update 0");
+
+ gWindow.addCookie("c3", "booyeah");
+
+ // Wait once for update and another time for value fetching
+ yield gUI.once("store-objects-updated");
+ yield gUI.once("store-objects-updated");
+
+ is($$("#value .table-widget-cell").length, 3,
+ "Correct number of rows after update 1");
+
+ // Add another
+ gWindow.addCookie("c4", "booyeah");
+
+ // Wait once for update and another time for value fetching
+ yield gUI.once("store-objects-updated");
+ yield gUI.once("store-objects-updated");
+
+ is($$("#value .table-widget-cell").length, 4,
+ "Correct number of rows after update 2");
+
+ // Removing cookies
+ gWindow.removeCookie("c1", "/browser");
+
+ yield gUI.once("sidebar-updated");
+
+ is($$("#value .table-widget-cell").length, 3,
+ "Correct number of rows after delete update 3");
+
+ ok(!$("#c1"), "Correct row got deleted");
+
+ ok(!gUI.sidebar.hidden, "Sidebar still visible for next row");
+
+ // Check if next element's value is visible in sidebar
+ yield findVariableViewProperties([{name: "c2", value: "foobar"}]);
+
+ // Keep deleting till no rows
+
+ gWindow.removeCookie("c3");
+
+ yield gUI.once("store-objects-updated");
+
+ is($$("#value .table-widget-cell").length, 2,
+ "Correct number of rows after delete update 4");
+
+ // Check if next element's value is visible in sidebar
+ yield findVariableViewProperties([{name: "c2", value: "foobar"}]);
+
+ gWindow.removeCookie("c2", "/browser");
+
+ yield gUI.once("sidebar-updated");
+
+ yield findVariableViewProperties([{name: "c4", value: "booyeah"}]);
+
+ is($$("#value .table-widget-cell").length, 1,
+ "Correct number of rows after delete update 5");
+
+ gWindow.removeCookie("c4");
+
+ yield gUI.once("store-objects-updated");
+
+ is($$("#value .table-widget-cell").length, 0,
+ "Correct number of rows after delete update 6");
+ ok(gUI.sidebar.hidden, "Sidebar is hidden when no rows");
+
+ // Testing in local storage
+ yield selectTreeItem(["localStorage", "http://test1.example.org"]);
+
+ is($$("#value .table-widget-cell").length, 7,
+ "Correct number of rows after delete update 7");
+
+ ok($(".table-widget-cell[data-id='ls4']"), "ls4 exists before deleting");
+
+ gWindow.localStorage.removeItem("ls4");
+
+ yield gUI.once("store-objects-updated");
+
+ is($$("#value .table-widget-cell").length, 6,
+ "Correct number of rows after delete update 8");
+ ok(!$(".table-widget-cell[data-id='ls4']"),
+ "ls4 does not exists after deleting");
+
+ gWindow.localStorage.setItem("ls4", "again");
+
+ yield gUI.once("store-objects-updated");
+ yield gUI.once("store-objects-updated");
+
+ is($$("#value .table-widget-cell").length, 7,
+ "Correct number of rows after delete update 9");
+ ok($(".table-widget-cell[data-id='ls4']"),
+ "ls4 came back after adding it again");
+
+ // Updating a row
+ gWindow.localStorage.setItem("ls2", "ls2-changed");
+
+ yield gUI.once("store-objects-updated");
+ yield gUI.once("store-objects-updated");
+
+ is($("#value [data-id='ls2']").value, "ls2-changed",
+ "Value got updated for local storage");
+
+ // Testing in session storage
+ yield selectTreeItem(["sessionStorage", "http://test1.example.org"]);
+
+ is($$("#value .table-widget-cell").length, 3,
+ "Correct number of rows for session storage");
+
+ gWindow.sessionStorage.setItem("ss4", "new-item");
+
+ yield gUI.once("store-objects-updated");
+ yield gUI.once("store-objects-updated");
+
+ is($$("#value .table-widget-cell").length, 4,
+ "Correct number of rows after session storage update");
+
+ // deleting item
+
+ gWindow.sessionStorage.removeItem("ss3");
+
+ yield gUI.once("store-objects-updated");
+
+ gWindow.sessionStorage.removeItem("ss1");
+
+ yield gUI.once("store-objects-updated");
+
+ is($$("#value .table-widget-cell").length, 2,
+ "Correct number of rows after removing items from session storage");
+
+ yield selectTableItem("ss2");
+
+ ok(!gUI.sidebar.hidden, "sidebar is visible");
+
+ // Checking for correct value in sidebar before update
+ yield findVariableViewProperties([{name: "ss2", value: "foobar"}]);
+
+ gWindow.sessionStorage.setItem("ss2", "changed=ss2");
+
+ yield gUI.once("sidebar-updated");
+
+ is($("#value [data-id='ss2']").value, "changed=ss2",
+ "Value got updated for session storage in the table");
+
+ yield findVariableViewProperties([{name: "ss2", value: "changed=ss2"}]);
+
+ // Clearing items. Bug 1233497 makes it so that we can no longer yield
+ // CPOWs from Tasks. We work around this by calling clear via a ContentTask
+ // instead.
+ yield ContentTask.spawn(gBrowser.selectedBrowser, null, function* () {
+ return Task.spawn(content.wrappedJSObject.clear);
+ });
+
+ yield gUI.once("store-objects-cleared");
+
+ is($$("#value .table-widget-cell").length, 0,
+ "Table should be cleared");
+
+ yield finishTests();
+});
diff --git a/devtools/client/storage/test/browser_storage_empty_objectstores.js b/devtools/client/storage/test/browser_storage_empty_objectstores.js
new file mode 100644
index 000000000..1749c91b8
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_empty_objectstores.js
@@ -0,0 +1,77 @@
+/* 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/. */
+
+// Basic test to assert that the storage tree and table corresponding to each
+// item in the storage tree is correctly displayed.
+
+"use strict";
+
+// Entries that should be present in the tree for this test
+// Format for each entry in the array:
+// [
+// ["path", "to", "tree", "item"],
+// - The path to the tree item to click formed by id of each item
+// ["key_value1", "key_value2", ...]
+// - The value of the first (unique) column for each row in the table
+// corresponding to the tree item selected.
+// ]
+// These entries are formed by the cookies, local storage, session storage and
+// indexedDB entries created in storage-listings.html,
+// storage-secured-iframe.html and storage-unsecured-iframe.html
+const storeItems = [
+ [["indexedDB", "http://test1.example.org"],
+ ["idb1", "idb2"]],
+ [["indexedDB", "http://test1.example.org", "idb1"],
+ ["obj1", "obj2"]],
+ [["indexedDB", "http://test1.example.org", "idb2"],
+ []],
+ [["indexedDB", "http://test1.example.org", "idb1", "obj1"],
+ [1, 2, 3]],
+ [["indexedDB", "http://test1.example.org", "idb1", "obj2"],
+ [1]]
+];
+
+/**
+ * Test that the desired number of tree items are present
+ */
+function testTree() {
+ let doc = gPanelWindow.document;
+ for (let [item] of storeItems) {
+ ok(doc.querySelector(`[data-id='${JSON.stringify(item)}']`),
+ `Tree item ${item} should be present in the storage tree`);
+ }
+}
+
+/**
+ * Test that correct table entries are shown for each of the tree item
+ */
+let testTables = function* () {
+ let doc = gPanelWindow.document;
+ // Expand all nodes so that the synthesized click event actually works
+ gUI.tree.expandAll();
+
+ // Click the tree items and wait for the table to be updated
+ for (let [item, ids] of storeItems) {
+ yield selectTreeItem(item);
+
+ // Check whether correct number of items are present in the table
+ is(doc.querySelectorAll(
+ ".table-widget-wrapper:first-of-type .table-widget-cell"
+ ).length, ids.length, "Number of items in table is correct");
+
+ // Check if all the desired items are present in the table
+ for (let id of ids) {
+ ok(doc.querySelector(".table-widget-cell[data-id='" + id + "']"),
+ `Table item ${id} should be present`);
+ }
+ }
+};
+
+add_task(function* () {
+ yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-empty-objectstores.html");
+
+ testTree();
+ yield testTables();
+ yield finishTests();
+});
diff --git a/devtools/client/storage/test/browser_storage_indexeddb_delete.js b/devtools/client/storage/test/browser_storage_indexeddb_delete.js
new file mode 100644
index 000000000..18a0daf69
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_indexeddb_delete.js
@@ -0,0 +1,47 @@
+/* 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/. */
+
+/* import-globals-from ../../framework/test/shared-head.js */
+
+"use strict";
+
+// Test deleting indexedDB database from the tree using context menu
+
+add_task(function* () {
+ yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-empty-objectstores.html");
+
+ let contextMenu = gPanelWindow.document.getElementById("storage-tree-popup");
+ let menuDeleteDb = contextMenu.querySelector("#storage-tree-popup-delete");
+
+ info("test state before delete");
+ yield checkState([
+ [["indexedDB", "http://test1.example.org"], ["idb1", "idb2"]],
+ ]);
+
+ info("do the delete");
+ const deletedDb = ["indexedDB", "http://test1.example.org", "idb1"];
+
+ yield selectTreeItem(deletedDb);
+
+ // Wait once for update and another time for value fetching
+ let eventWait = gUI.once("store-objects-updated").then(
+ () => gUI.once("store-objects-updated"));
+
+ let selector = `[data-id='${JSON.stringify(deletedDb)}'] > .tree-widget-item`;
+ let target = gPanelWindow.document.querySelector(selector);
+ ok(target, `tree item found in ${deletedDb.join(" > ")}`);
+ yield waitForContextMenu(contextMenu, target, () => {
+ info(`Opened tree context menu in ${deletedDb.join(" > ")}`);
+ menuDeleteDb.click();
+ });
+
+ yield eventWait;
+
+ info("test state after delete");
+ yield checkState([
+ [["indexedDB", "http://test1.example.org"], ["idb2"]],
+ ]);
+
+ yield finishTests();
+});
diff --git a/devtools/client/storage/test/browser_storage_indexeddb_delete_blocked.js b/devtools/client/storage/test/browser_storage_indexeddb_delete_blocked.js
new file mode 100644
index 000000000..6e89c4f28
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_indexeddb_delete_blocked.js
@@ -0,0 +1,58 @@
+/* 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/. */
+
+/* import-globals-from ../../framework/test/shared-head.js */
+
+"use strict";
+
+// Test what happens when deleting indexedDB database is blocked
+
+add_task(function* () {
+ yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-idb-delete-blocked.html");
+
+ info("test state before delete");
+ yield checkState([
+ [["indexedDB", "http://test1.example.org"], ["idb"]]
+ ]);
+
+ info("do the delete");
+ yield selectTreeItem(["indexedDB", "http://test1.example.org"]);
+ let actor = gUI.getCurrentActor();
+ let result = yield actor.removeDatabase("http://test1.example.org", "idb");
+
+ ok(result.blocked, "removeDatabase attempt is blocked");
+
+ info("test state after blocked delete");
+ yield checkState([
+ [["indexedDB", "http://test1.example.org"], ["idb"]]
+ ]);
+
+ let eventWait = gUI.once("store-objects-updated");
+
+ info("telling content to close the db");
+ yield ContentTask.spawn(gBrowser.selectedBrowser, null, function* () {
+ let win = content.wrappedJSObject;
+ yield win.closeDb();
+ });
+
+ info("waiting for store update events");
+ yield eventWait;
+
+ info("test state after real delete");
+ yield checkState([
+ [["indexedDB", "http://test1.example.org"], []]
+ ]);
+
+ info("try to delete database from nonexistent host");
+ let errorThrown = false;
+ try {
+ result = yield actor.removeDatabase("http://test2.example.org", "idb");
+ } catch (ex) {
+ errorThrown = true;
+ }
+
+ ok(errorThrown, "error was reported when trying to delete");
+
+ yield finishTests();
+});
diff --git a/devtools/client/storage/test/browser_storage_localstorage_edit.js b/devtools/client/storage/test/browser_storage_localstorage_edit.js
new file mode 100644
index 000000000..86409e0ac
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_localstorage_edit.js
@@ -0,0 +1,24 @@
+/* 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/. */
+
+// Basic test to check the editing of localStorage.
+
+"use strict";
+
+add_task(function* () {
+ yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-localstorage.html");
+
+ yield selectTreeItem(["localStorage", "http://test1.example.org"]);
+
+ yield editCell("TestLS1", "name", "newTestLS1");
+ yield editCell("newTestLS1", "value", "newValueLS1");
+
+ yield editCell("TestLS3", "name", "newTestLS3");
+ yield editCell("newTestLS3", "value", "newValueLS3");
+
+ yield editCell("TestLS5", "name", "newTestLS5");
+ yield editCell("newTestLS5", "value", "newValueLS5");
+
+ yield finishTests();
+});
diff --git a/devtools/client/storage/test/browser_storage_localstorage_error.js b/devtools/client/storage/test/browser_storage_localstorage_error.js
new file mode 100644
index 000000000..923ca6ca9
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_localstorage_error.js
@@ -0,0 +1,24 @@
+/* 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";
+
+// Test that for pages where local/sessionStorage is not available (like about:home),
+// the host still appears in the storage tree and no unhandled exception is thrown.
+
+add_task(function* () {
+ yield openTabAndSetupStorage("about:home");
+
+ let itemsToOpen = [
+ ["localStorage", "about:home"],
+ ["sessionStorage", "about:home"]
+ ];
+
+ for (let item of itemsToOpen) {
+ yield selectTreeItem(item);
+ ok(gUI.tree.isSelected(item), `Item ${item.join(" > ")} is present in the tree`);
+ }
+
+ yield finishTests();
+});
diff --git a/devtools/client/storage/test/browser_storage_overflow.js b/devtools/client/storage/test/browser_storage_overflow.js
new file mode 100644
index 000000000..88181ca05
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_overflow.js
@@ -0,0 +1,41 @@
+// Test endless scrolling when a lot of items are present in the storage
+// inspector table.
+"use strict";
+
+add_task(function* () {
+ yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-overflow.html");
+
+ let $ = id => gPanelWindow.document.querySelector(id);
+ let $$ = sel => gPanelWindow.document.querySelectorAll(sel);
+
+ gUI.tree.expandAll();
+ yield selectTreeItem(["localStorage", "http://test1.example.org"]);
+
+ let table = $("#storage-table .table-widget-body");
+ let cellHeight = $(".table-widget-cell").getBoundingClientRect().height;
+
+ is($$("#value .table-widget-cell").length, 50,
+ "Table should initially display 50 items");
+
+ let onStoresUpdate = gUI.once("store-objects-updated");
+ table.scrollTop += cellHeight * 50;
+ yield onStoresUpdate;
+
+ is($$("#value .table-widget-cell").length, 100,
+ "Table should display 100 items after scrolling");
+
+ onStoresUpdate = gUI.once("store-objects-updated");
+ table.scrollTop += cellHeight * 50;
+ yield onStoresUpdate;
+
+ is($$("#value .table-widget-cell").length, 150,
+ "Table should display 150 items after scrolling");
+
+ onStoresUpdate = gUI.once("store-objects-updated");
+ table.scrollTop += cellHeight * 50;
+ yield onStoresUpdate;
+
+ is($$("#value .table-widget-cell").length, 160,
+ "Table should display all 160 items after scrolling");
+ yield finishTests();
+});
diff --git a/devtools/client/storage/test/browser_storage_search.js b/devtools/client/storage/test/browser_storage_search.js
new file mode 100644
index 000000000..bbe0947b9
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_search.js
@@ -0,0 +1,87 @@
+// Tests the filter search box in the storage inspector
+"use strict";
+
+add_task(function* () {
+ yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-search.html");
+
+ let $$ = sel => gPanelWindow.document.querySelectorAll(sel);
+ gUI.tree.expandAll();
+ yield selectTreeItem(["localStorage", "http://test1.example.org"]);
+
+ // Results: 0=hidden, 1=visible
+ let testcases = [
+ // Test that search isn't case-sensitive
+ {
+ value: "FoO",
+ results: [0, 0, 1, 1, 0, 1, 0]
+ },
+ {
+ value: "OR",
+ results: [0, 1, 0, 0, 0, 1, 0]
+ },
+ {
+ value: "aNImAl",
+ results: [0, 1, 0, 0, 0, 0, 0]
+ },
+ // Test numbers
+ {
+ value: "01",
+ results: [1, 0, 0, 0, 0, 0, 1]
+ },
+ {
+ value: "2016",
+ results: [0, 0, 0, 0, 0, 0, 1]
+ },
+ {
+ value: "56789",
+ results: [1, 0, 0, 0, 0, 0, 0]
+ },
+ // Test filtering by value
+ {
+ value: "horse",
+ results: [0, 1, 0, 0, 0, 0, 0]
+ },
+ {
+ value: "$$$",
+ results: [0, 0, 0, 0, 1, 0, 0]
+ },
+ {
+ value: "bar",
+ results: [0, 0, 1, 1, 0, 0, 0]
+ },
+ // Test input with whitespace
+ {
+ value: "energy b",
+ results: [0, 0, 0, 1, 0, 0, 0]
+ },
+ // Test no input at all
+ {
+ value: "",
+ results: [1, 1, 1, 1, 1, 1, 1]
+ },
+ // Test input that matches nothing
+ {
+ value: "input that matches nothing",
+ results: [0, 0, 0, 0, 0, 0, 0]
+ }
+ ];
+
+ let names = $$("#name .table-widget-cell");
+ let rows = $$("#value .table-widget-cell");
+ for (let testcase of testcases) {
+ info(`Testing input: ${testcase.value}`);
+
+ gUI.searchBox.value = testcase.value;
+ gUI.filterItems();
+
+ for (let i = 0; i < rows.length; i++) {
+ info(`Testing row ${i}`);
+ info(`key: ${names[i].value}, value: ${rows[i].value}`);
+ let state = testcase.results[i] ? "visible" : "hidden";
+ is(rows[i].hasAttribute("hidden"), !testcase.results[i],
+ `Row ${i} should be ${state}`);
+ }
+ }
+
+ yield finishTests();
+});
diff --git a/devtools/client/storage/test/browser_storage_search_keyboard_trap.js b/devtools/client/storage/test/browser_storage_search_keyboard_trap.js
new file mode 100644
index 000000000..71dfd32c0
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_search_keyboard_trap.js
@@ -0,0 +1,15 @@
+// Test ability to focus search field by using keyboard
+"use strict";
+
+add_task(function* () {
+ yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-search.html");
+
+ gUI.tree.expandAll();
+ yield selectTreeItem(["localStorage", "http://test1.example.org"]);
+
+ yield focusSearchBoxUsingShortcut(gPanelWindow);
+ ok(containsFocus(gPanelWindow.document, gUI.searchBox),
+ "Focus is in a searchbox");
+
+ yield finishTests();
+});
diff --git a/devtools/client/storage/test/browser_storage_sessionstorage_edit.js b/devtools/client/storage/test/browser_storage_sessionstorage_edit.js
new file mode 100644
index 000000000..9629eec0b
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_sessionstorage_edit.js
@@ -0,0 +1,24 @@
+/* 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/. */
+
+// Basic test to check the editing of localStorage.
+
+"use strict";
+
+add_task(function* () {
+ yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-sessionstorage.html");
+
+ yield selectTreeItem(["sessionStorage", "http://test1.example.org"]);
+
+ yield editCell("TestSS1", "name", "newTestSS1");
+ yield editCell("newTestSS1", "value", "newValueSS1");
+
+ yield editCell("TestSS3", "name", "newTestSS3");
+ yield editCell("newTestSS3", "value", "newValueSS3");
+
+ yield editCell("TestSS5", "name", "newTestSS5");
+ yield editCell("newTestSS5", "value", "newValueSS5");
+
+ yield finishTests();
+});
diff --git a/devtools/client/storage/test/browser_storage_sidebar.js b/devtools/client/storage/test/browser_storage_sidebar.js
new file mode 100644
index 000000000..9b60026a0
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_sidebar.js
@@ -0,0 +1,125 @@
+/* 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 to verify that the sidebar opens, closes and updates
+// This test is not testing the values in the sidebar, being tested in _values
+
+// Format: [
+// <id of the table item to click> or <id array for tree item to select> or
+// null to press Escape,
+// <do we wait for the async "sidebar-updated" event>,
+// <is the sidebar open>
+// ]
+
+"use strict";
+
+const testCases = [
+ {
+ location: ["cookies", "sectest1.example.org"],
+ sidebarHidden: true
+ },
+ {
+ location: "cs2",
+ sidebarHidden: false
+ },
+ {
+ sendEscape: true
+ },
+ {
+ location: "cs2",
+ sidebarHidden: false
+ },
+ {
+ location: "uc1",
+ sidebarHidden: false
+ },
+ {
+ location: "uc1",
+ sidebarHidden: false
+ },
+
+ {
+ location: ["localStorage", "http://sectest1.example.org"],
+ sidebarHidden: true
+ },
+ {
+ location: "iframe-u-ls1",
+ sidebarHidden: false
+ },
+ {
+ location: "iframe-u-ls1",
+ sidebarHidden: false
+ },
+ {
+ sendEscape: true
+ },
+
+ {
+ location: ["sessionStorage", "http://test1.example.org"],
+ sidebarHidden: true
+ },
+ {
+ location: "ss1",
+ sidebarHidden: false
+ },
+ {
+ sendEscape: true
+ },
+
+ {
+ location: ["indexedDB", "http://test1.example.org"],
+ sidebarHidden: true
+ },
+ {
+ location: "idb2",
+ sidebarHidden: false
+ },
+
+ {
+ location: ["indexedDB", "http://test1.example.org", "idb2", "obj3"],
+ sidebarHidden: true
+ },
+
+ {
+ location: ["indexedDB", "https://sectest1.example.org", "idb-s2"],
+ sidebarHidden: true
+ },
+ {
+ location: "obj-s2",
+ sidebarHidden: false
+ },
+ {
+ sendEscape: true
+ }, {
+ location: "obj-s2",
+ sidebarHidden: false
+ }
+];
+
+add_task(function* () {
+ yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-listings.html");
+
+ for (let test of testCases) {
+ let { location, sidebarHidden, sendEscape } = test;
+
+ info("running " + JSON.stringify(test));
+
+ if (Array.isArray(location)) {
+ yield selectTreeItem(location);
+ } else if (location) {
+ yield selectTableItem(location);
+ }
+
+ if (sendEscape) {
+ EventUtils.sendKey("ESCAPE", gPanelWindow);
+ } else {
+ is(gUI.sidebar.hidden, sidebarHidden,
+ "correct visibility state of sidebar.");
+ }
+
+ info("-".repeat(80));
+ }
+
+ yield finishTests();
+});
diff --git a/devtools/client/storage/test/browser_storage_sidebar_update.js b/devtools/client/storage/test/browser_storage_sidebar_update.js
new file mode 100644
index 000000000..419d63020
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_sidebar_update.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/. */
+
+// Test to verify that the sidebar is not broken when several updates
+// come in quick succession. See bug 1260380 - it could happen that the
+// "Parsed Value" section gets duplicated.
+
+"use strict";
+
+add_task(function* () {
+ const ITEM_NAME = "ls1";
+ const UPDATE_COUNT = 3;
+
+ yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-complex-values.html");
+
+ let updated = gUI.once("sidebar-updated");
+ yield selectTreeItem(["localStorage", "http://test1.example.org"]);
+ yield selectTableItem(ITEM_NAME);
+ yield updated;
+
+ is(gUI.sidebar.hidden, false, "sidebar is visible");
+
+ // do several updates in a row and wait for them to finish
+ let updates = [];
+ for (let i = 0; i < UPDATE_COUNT; i++) {
+ info(`Performing update #${i}`);
+ updates.push(gUI.once("sidebar-updated"));
+ gUI.displayObjectSidebar();
+ }
+ yield promise.all(updates);
+
+ info("Updates performed, going to verify result");
+ let parsedScope = gUI.view.getScopeAtIndex(1);
+ let elements = parsedScope.target.querySelectorAll(
+ `.name[value="${ITEM_NAME}"]`);
+ is(elements.length, 1,
+ `There is only one displayed variable named '${ITEM_NAME}'`);
+
+ yield finishTests();
+});
diff --git a/devtools/client/storage/test/browser_storage_values.js b/devtools/client/storage/test/browser_storage_values.js
new file mode 100644
index 000000000..920ce350e
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_values.js
@@ -0,0 +1,165 @@
+/* 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 to verify that the values shown in sidebar are correct
+
+// Format: [
+// <id of the table item to click> or <id array for tree item to select> or
+// null do click nothing,
+// null to skip checking value in variables view or a key value pair object
+// which will be asserted to exist in the storage sidebar,
+// true if the check is to be made in the parsed value section
+// ]
+
+"use strict";
+
+const LONG_WORD = "a".repeat(1000);
+
+const testCases = [
+ ["cs2", [
+ {name: "cs2", value: "sessionCookie"},
+ {name: "cs2.Path", value: "/"},
+ {name: "cs2.HostOnly", value: "false"},
+ {name: "cs2.HttpOnly", value: "false"},
+ {name: "cs2.Domain", value: ".example.org"},
+ {name: "cs2.Expires", value: "Session"},
+ {name: "cs2.Secure", value: "false"},
+ ]],
+ ["c1", [
+ {name: "c1", value: JSON.stringify(["foo", "Bar", {foo: "Bar"}])},
+ {name: "c1.Path", value: "/browser"},
+ {name: "c1.HostOnly", value: "true"},
+ {name: "c1.HttpOnly", value: "false"},
+ {name: "c1.Domain", value: "test1.example.org"},
+ {name: "c1.Expires", value: new Date(2000000000000).toUTCString()},
+ {name: "c1.Secure", value: "false"},
+ ]],
+ [null, [
+ {name: "c1", value: "Array"},
+ {name: "c1.0", value: "foo"},
+ {name: "c1.1", value: "Bar"},
+ {name: "c1.2", value: "Object"},
+ {name: "c1.2.foo", value: "Bar"},
+ ], true],
+ ["c_encoded", [
+ {name: "c_encoded", value: encodeURIComponent(JSON.stringify({foo: {foo1: "bar"}}))}
+ ]],
+ [null, [
+ {name: "c_encoded", value: "Object"},
+ {name: "c_encoded.foo", value: "Object"},
+ {name: "c_encoded.foo.foo1", value: "bar"}
+ ], true],
+ [["localStorage", "http://test1.example.org"]],
+ ["ls2", [
+ {name: "ls2", value: "foobar-2"}
+ ]],
+ ["ls1", [
+ {name: "ls1", value: JSON.stringify({
+ es6: "for", the: "win", baz: [0, 2, 3, {
+ deep: "down",
+ nobody: "cares"
+ }]})}
+ ]],
+ [null, [
+ {name: "ls1", value: "Object"},
+ {name: "ls1.es6", value: "for"},
+ {name: "ls1.the", value: "win"},
+ {name: "ls1.baz", value: "Array"},
+ {name: "ls1.baz.0", value: "0"},
+ {name: "ls1.baz.1", value: "2"},
+ {name: "ls1.baz.2", value: "3"},
+ {name: "ls1.baz.3", value: "Object"},
+ {name: "ls1.baz.3.deep", value: "down"},
+ {name: "ls1.baz.3.nobody", value: "cares"},
+ ], true],
+ ["ls3", [
+ {name: "ls3", "value": "http://foobar.com/baz.php"}
+ ]],
+ [null, [
+ {name: "ls3", "value": "http://foobar.com/baz.php", dontMatch: true}
+ ], true],
+ [["sessionStorage", "http://test1.example.org"]],
+ ["ss1", [
+ {name: "ss1", value: "This#is#an#array"}
+ ]],
+ [null, [
+ {name: "ss1", value: "Array"},
+ {name: "ss1.0", value: "This"},
+ {name: "ss1.1", value: "is"},
+ {name: "ss1.2", value: "an"},
+ {name: "ss1.3", value: "array"},
+ ], true],
+ ["ss2", [
+ {name: "ss2", value: "Array"},
+ {name: "ss2.0", value: "This"},
+ {name: "ss2.1", value: "is"},
+ {name: "ss2.2", value: "another"},
+ {name: "ss2.3", value: "array"},
+ ], true],
+ ["ss3", [
+ {name: "ss3", value: "Object"},
+ {name: "ss3.this", value: "is"},
+ {name: "ss3.an", value: "object"},
+ {name: "ss3.foo", value: "bar"},
+ ], true],
+ ["ss4", [
+ {name: "ss4", value: "Array"},
+ {name: "ss4.0", value: ""},
+ {name: "ss4.1", value: "array"},
+ {name: "ss4.2", value: ""},
+ {name: "ss4.3", value: "with"},
+ {name: "ss4.4", value: "empty"},
+ {name: "ss4.5", value: "items"},
+ ], true],
+ ["ss5", [
+ {name: "ss5", value: "Array"},
+ {name: "ss5.0", value: LONG_WORD},
+ {name: "ss5.1", value: LONG_WORD},
+ {name: "ss5.2", value: LONG_WORD},
+ {name: "ss5.3", value: `${LONG_WORD}&${LONG_WORD}`},
+ {name: "ss5.4", value: `${LONG_WORD}&${LONG_WORD}`},
+ ], true],
+ [["indexedDB", "http://test1.example.org", "idb1", "obj1"]],
+ [1, [
+ {name: 1, value: JSON.stringify({id: 1, name: "foo", email: "foo@bar.com"})}
+ ]],
+ [null, [
+ {name: "1.id", value: "1"},
+ {name: "1.name", value: "foo"},
+ {name: "1.email", value: "foo@bar.com"},
+ ], true],
+ [["indexedDB", "http://test1.example.org", "idb1", "obj2"]],
+ [1, [
+ {name: 1, value: JSON.stringify({
+ id2: 1, name: "foo", email: "foo@bar.com", extra: "baz"
+ })}
+ ]],
+ [null, [
+ {name: "1.id2", value: "1"},
+ {name: "1.name", value: "foo"},
+ {name: "1.email", value: "foo@bar.com"},
+ {name: "1.extra", value: "baz"},
+ ], true]
+];
+
+add_task(function* () {
+ yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-complex-values.html");
+
+ gUI.tree.expandAll();
+
+ for (let item of testCases) {
+ info("clicking for item " + item);
+
+ if (Array.isArray(item[0])) {
+ yield selectTreeItem(item[0]);
+ continue;
+ } else if (item[0]) {
+ yield selectTableItem(item[0]);
+ }
+
+ yield findVariableViewProperties(item[1], item[2]);
+ }
+
+ yield finishTests();
+});
diff --git a/devtools/client/storage/test/head.js b/devtools/client/storage/test/head.js
new file mode 100644
index 000000000..9662393cf
--- /dev/null
+++ b/devtools/client/storage/test/head.js
@@ -0,0 +1,840 @@
+/* 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";
+
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+/* import-globals-from ../../framework/test/shared-head.js */
+
+// shared-head.js handles imports, constants, and utility functions
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js",
+ this);
+
+const {TableWidget} = require("devtools/client/shared/widgets/TableWidget");
+const SPLIT_CONSOLE_PREF = "devtools.toolbox.splitconsoleEnabled";
+const STORAGE_PREF = "devtools.storage.enabled";
+const DUMPEMIT_PREF = "devtools.dump.emit";
+const DEBUGGERLOG_PREF = "devtools.debugger.log";
+// Allows Cache API to be working on usage `http` test page
+const CACHES_ON_HTTP_PREF = "dom.caches.testing.enabled";
+const PATH = "browser/devtools/client/storage/test/";
+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;
+
+var gToolbox, gPanelWindow, gWindow, gUI;
+
+// Services.prefs.setBoolPref(DUMPEMIT_PREF, true);
+// Services.prefs.setBoolPref(DEBUGGERLOG_PREF, true);
+
+Services.prefs.setBoolPref(STORAGE_PREF, true);
+Services.prefs.setBoolPref(CACHES_ON_HTTP_PREF, true);
+registerCleanupFunction(() => {
+ gToolbox = gPanelWindow = gWindow = gUI = null;
+ Services.prefs.clearUserPref(STORAGE_PREF);
+ Services.prefs.clearUserPref(SPLIT_CONSOLE_PREF);
+ Services.prefs.clearUserPref(DUMPEMIT_PREF);
+ Services.prefs.clearUserPref(DEBUGGERLOG_PREF);
+ Services.prefs.clearUserPref(CACHES_ON_HTTP_PREF);
+});
+
+/**
+ * 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; Then
+ * opens the storage inspector and waits for the storage tree and table to be
+ * populated.
+ *
+ * @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 tab = yield addTab(url);
+ let content = tab.linkedBrowser.contentWindow;
+
+ gWindow = content.wrappedJSObject;
+
+ // 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) {
+ let readyState = win.document.readyState;
+ info(`Found a window: ${readyState}`);
+ if (readyState != "complete") {
+ yield new Promise(resolve => {
+ let onLoad = () => {
+ win.removeEventListener("load", onLoad);
+ resolve();
+ };
+ win.addEventListener("load", onLoad);
+ });
+ }
+ if (win.setup) {
+ yield win.setup();
+ }
+ }
+ });
+
+ // open storage inspector
+ return yield openStoragePanel();
+}
+
+/**
+ * Open the toolbox, with the storage tool visible.
+ *
+ * @param cb {Function} Optional callback, if you don't want to use the returned
+ * promise
+ *
+ * @return {Promise} a promise that resolves when the storage inspector is ready
+ */
+var openStoragePanel = Task.async(function* (cb) {
+ info("Opening the storage inspector");
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+
+ let storage, toolbox;
+
+ // Checking if the toolbox and the storage are already loaded
+ // The storage-updated event should only be waited for if the storage
+ // isn't loaded yet
+ toolbox = gDevTools.getToolbox(target);
+ if (toolbox) {
+ storage = toolbox.getPanel("storage");
+ if (storage) {
+ gPanelWindow = storage.panelWindow;
+ gUI = storage.UI;
+ gToolbox = toolbox;
+ info("Toolbox and storage already open");
+ if (cb) {
+ return cb(storage, toolbox);
+ }
+
+ return {
+ toolbox: toolbox,
+ storage: storage
+ };
+ }
+ }
+
+ info("Opening the toolbox");
+ toolbox = yield gDevTools.showToolbox(target, "storage");
+ storage = toolbox.getPanel("storage");
+ gPanelWindow = storage.panelWindow;
+ gUI = storage.UI;
+ gToolbox = toolbox;
+
+ // The table animation flash causes some timeouts on Linux debug tests,
+ // so we disable it
+ gUI.animationsEnabled = false;
+
+ info("Waiting for the stores to update");
+ yield gUI.once("store-objects-updated");
+
+ yield waitForToolboxFrameFocus(toolbox);
+
+ if (cb) {
+ return cb(storage, toolbox);
+ }
+
+ return {
+ toolbox: toolbox,
+ storage: storage
+ };
+});
+
+/**
+ * Wait for the toolbox frame to receive focus after it loads
+ *
+ * @param toolbox {Toolbox}
+ *
+ * @return a promise that resolves when focus has been received
+ */
+function waitForToolboxFrameFocus(toolbox) {
+ info("Making sure that the toolbox's frame is focused");
+ let def = promise.defer();
+ waitForFocus(def.resolve, toolbox.win);
+ return def.promise;
+}
+
+/**
+ * Forces GC, CC and Shrinking GC to get rid of disconnected docshells and
+ * windows.
+ */
+function forceCollections() {
+ Cu.forceGC();
+ Cu.forceCC();
+ Cu.forceShrinkingGC();
+}
+
+/**
+ * Cleans up and finishes the test
+ */
+function* finishTests() {
+ // Bug 1233497 makes it so that we can no longer yield CPOWs from Tasks.
+ // We work around this by calling clear() via a ContentTask instead.
+ 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) {
+ // Some windows (e.g., about: URLs) don't have storage available
+ try {
+ win.localStorage.clear();
+ win.sessionStorage.clear();
+ } catch (ex) {
+ // ignore
+ }
+
+ if (win.clear) {
+ yield win.clear();
+ }
+ }
+ });
+
+ Services.cookies.removeAll();
+ forceCollections();
+ finish();
+}
+
+// Sends a click event on the passed DOM node in an async manner
+function* click(node) {
+ let def = promise.defer();
+
+ node.scrollIntoView();
+
+ // We need setTimeout here to allow any scrolling to complete before clicking
+ // the node.
+ setTimeout(() => {
+ node.click();
+ def.resolve();
+ }, 200);
+
+ return def;
+}
+
+/**
+ * Recursively expand the variables view up to a given property.
+ *
+ * @param options
+ * Options for view expansion:
+ * - rootVariable: start from the given scope/variable/property.
+ * - expandTo: string made up of property names you want to expand.
+ * For example: "body.firstChild.nextSibling" given |rootVariable:
+ * document|.
+ * @return object
+ * A promise that is resolved only when the last property in |expandTo|
+ * is found, and rejected otherwise. Resolution reason is always the
+ * last property - |nextSibling| in the example above. Rejection is
+ * always the last property that was found.
+ */
+function variablesViewExpandTo(options) {
+ let root = options.rootVariable;
+ let expandTo = options.expandTo.split(".");
+ let lastDeferred = promise.defer();
+
+ function getNext(prop) {
+ let name = expandTo.shift();
+ let newProp = prop.get(name);
+
+ if (expandTo.length > 0) {
+ ok(newProp, "found property " + name);
+ if (newProp && newProp.expand) {
+ newProp.expand();
+ getNext(newProp);
+ } else {
+ lastDeferred.reject(prop);
+ }
+ } else if (newProp) {
+ lastDeferred.resolve(newProp);
+ } else {
+ lastDeferred.reject(prop);
+ }
+ }
+
+ if (root && root.expand) {
+ root.expand();
+ getNext(root);
+ } else {
+ lastDeferred.resolve(root);
+ }
+
+ return lastDeferred.promise;
+}
+
+/**
+ * Find variables or properties in a VariablesView instance.
+ *
+ * @param array ruleArray
+ * The array of rules you want to match. Each rule is an object with:
+ * - name (string|regexp): property name to match.
+ * - value (string|regexp): property value to match.
+ * - dontMatch (boolean): make sure the rule doesn't match any property.
+ * @param boolean parsed
+ * true if we want to test the rules in the parse value section of the
+ * storage sidebar
+ * @return object
+ * A promise object that is resolved when all the rules complete
+ * matching. The resolved callback is given an array of all the rules
+ * you wanted to check. Each rule has a new property: |matchedProp|
+ * which holds a reference to the Property object instance from the
+ * VariablesView. If the rule did not match, then |matchedProp| is
+ * undefined.
+ */
+function findVariableViewProperties(ruleArray, parsed) {
+ // Initialize the search.
+ function init() {
+ // If parsed is true, we are checking rules in the parsed value section of
+ // the storage sidebar. That scope uses a blank variable as a placeholder
+ // Thus, adding a blank parent to each name
+ if (parsed) {
+ ruleArray = ruleArray.map(({name, value, dontMatch}) => {
+ return {name: "." + name, value, dontMatch};
+ });
+ }
+ // Separate out the rules that require expanding properties throughout the
+ // view.
+ let expandRules = [];
+ let rules = ruleArray.filter(rule => {
+ if (typeof rule.name == "string" && rule.name.indexOf(".") > -1) {
+ expandRules.push(rule);
+ return false;
+ }
+ return true;
+ });
+
+ // Search through the view those rules that do not require any properties to
+ // be expanded. Build the array of matchers, outstanding promises to be
+ // resolved.
+ let outstanding = [];
+
+ finder(rules, gUI.view, outstanding);
+
+ // Process the rules that need to expand properties.
+ let lastStep = processExpandRules.bind(null, expandRules);
+
+ // Return the results - a promise resolved to hold the updated ruleArray.
+ let returnResults = onAllRulesMatched.bind(null, ruleArray);
+
+ return promise.all(outstanding).then(lastStep).then(returnResults);
+ }
+
+ function onMatch(prop, rule, matched) {
+ if (matched && !rule.matchedProp) {
+ rule.matchedProp = prop;
+ }
+ }
+
+ function finder(rules, view, promises) {
+ for (let scope of view) {
+ for (let [, prop] of scope) {
+ for (let rule of rules) {
+ let matcher = matchVariablesViewProperty(prop, rule);
+ promises.push(matcher.then(onMatch.bind(null, prop, rule)));
+ }
+ }
+ }
+ }
+
+ function processExpandRules(rules) {
+ let rule = rules.shift();
+ if (!rule) {
+ return promise.resolve(null);
+ }
+
+ let deferred = promise.defer();
+ let expandOptions = {
+ rootVariable: gUI.view.getScopeAtIndex(parsed ? 1 : 0),
+ expandTo: rule.name
+ };
+
+ variablesViewExpandTo(expandOptions).then(function onSuccess(prop) {
+ let name = rule.name;
+ let lastName = name.split(".").pop();
+ rule.name = lastName;
+
+ let matched = matchVariablesViewProperty(prop, rule);
+ return matched.then(onMatch.bind(null, prop, rule)).then(function () {
+ rule.name = name;
+ });
+ }, function onFailure() {
+ return promise.resolve(null);
+ }).then(processExpandRules.bind(null, rules)).then(function () {
+ deferred.resolve(null);
+ });
+
+ return deferred.promise;
+ }
+
+ function onAllRulesMatched(rules) {
+ for (let rule of rules) {
+ let matched = rule.matchedProp;
+ if (matched && !rule.dontMatch) {
+ ok(true, "rule " + rule.name + " matched for property " + matched.name);
+ } else if (matched && rule.dontMatch) {
+ ok(false, "rule " + rule.name + " should not match property " +
+ matched.name);
+ } else {
+ ok(rule.dontMatch, "rule " + rule.name + " did not match any property");
+ }
+ }
+ return rules;
+ }
+
+ return init();
+}
+
+/**
+ * Check if a given Property object from the variables view matches the given
+ * rule.
+ *
+ * @param object prop
+ * The variable's view Property instance.
+ * @param object rule
+ * Rules for matching the property. See findVariableViewProperties() for
+ * details.
+ * @return object
+ * A promise that is resolved when all the checks complete. Resolution
+ * result is a boolean that tells your promise callback the match
+ * result: true or false.
+ */
+function matchVariablesViewProperty(prop, rule) {
+ function resolve(result) {
+ return promise.resolve(result);
+ }
+
+ if (!prop) {
+ return resolve(false);
+ }
+
+ if (rule.name) {
+ let match = rule.name instanceof RegExp ?
+ rule.name.test(prop.name) :
+ prop.name == rule.name;
+ if (!match) {
+ return resolve(false);
+ }
+ }
+
+ if ("value" in rule) {
+ let displayValue = prop.displayValue;
+ if (prop.displayValueClassName == "token-string") {
+ displayValue = displayValue.substring(1, displayValue.length - 1);
+ }
+
+ let match = rule.value instanceof RegExp ?
+ rule.value.test(displayValue) :
+ displayValue == rule.value;
+ if (!match) {
+ info("rule " + rule.name + " did not match value, expected '" +
+ rule.value + "', found '" + displayValue + "'");
+ return resolve(false);
+ }
+ }
+
+ return resolve(true);
+}
+
+/**
+ * Click selects a row in the table.
+ *
+ * @param {[String]} ids
+ * The array id of the item in the tree
+ */
+function* selectTreeItem(ids) {
+ /* If this item is already selected, return */
+ if (gUI.tree.isSelected(ids)) {
+ return;
+ }
+
+ let updated = gUI.once("store-objects-updated");
+ gUI.tree.selectedItem = ids;
+ yield updated;
+}
+
+/**
+ * Click selects a row in the table.
+ *
+ * @param {String} id
+ * The id of the row in the table widget
+ */
+function* selectTableItem(id) {
+ let selector = ".table-widget-cell[data-id='" + id + "']";
+ let target = gPanelWindow.document.querySelector(selector);
+ ok(target, "table item found with ids " + id);
+
+ yield click(target);
+ yield gUI.once("sidebar-updated");
+}
+
+/**
+ * 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] 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 + ".");
+
+ let deferred = promise.defer();
+
+ for (let [add, remove] of [
+ ["addEventListener", "removeEventListener"],
+ ["addListener", "removeListener"],
+ ["on", "off"]
+ ]) {
+ if ((add in target) && (remove in target)) {
+ target[add](eventName, function onEvent(...aArgs) {
+ target[remove](eventName, onEvent, useCapture);
+ deferred.resolve.apply(deferred, aArgs);
+ }, useCapture);
+ break;
+ }
+ }
+
+ return deferred.promise;
+}
+
+/**
+ * Get values for a row.
+ *
+ * @param {String} id
+ * The uniqueId of the given row.
+ * @param {Boolean} includeHidden
+ * Include hidden columns.
+ *
+ * @return {Object}
+ * An object of column names to values for the given row.
+ */
+function getRowValues(id, includeHidden = false) {
+ let cells = getRowCells(id, includeHidden);
+ let values = {};
+
+ for (let name in cells) {
+ let cell = cells[name];
+
+ values[name] = cell.value;
+ }
+
+ return values;
+}
+
+/**
+ * Get cells for a row.
+ *
+ * @param {String} id
+ * The uniqueId of the given row.
+ * @param {Boolean} includeHidden
+ * Include hidden columns.
+ *
+ * @return {Object}
+ * An object of column names to cells for the given row.
+ */
+function getRowCells(id, includeHidden = false) {
+ let doc = gPanelWindow.document;
+ let table = gUI.table;
+ let item = doc.querySelector(".table-widget-column#" + table.uniqueId +
+ " .table-widget-cell[value='" + id + "']");
+
+ if (!item) {
+ ok(false, "Row id '" + id + "' exists");
+ }
+
+ let index = table.columns.get(table.uniqueId).visibleCellNodes.indexOf(item);
+ let cells = {};
+
+ for (let [name, column] of [...table.columns]) {
+ if (!includeHidden && column.column.parentNode.hidden) {
+ continue;
+ }
+ cells[name] = column.visibleCellNodes[index];
+ }
+
+ return cells;
+}
+
+/**
+ * Get a cell value.
+ *
+ * @param {String} id
+ * The uniqueId of the row.
+ * @param {String} column
+ * The id of the column
+ *
+ * @yield {String}
+ * The cell value.
+ */
+function getCellValue(id, column) {
+ let row = getRowValues(id, true);
+
+ return row[column];
+}
+
+/**
+ * Edit a cell value. The cell is assumed to be in edit mode, see startCellEdit.
+ *
+ * @param {String} id
+ * The uniqueId of the row.
+ * @param {String} column
+ * The id of the column
+ * @param {String} newValue
+ * Replacement value.
+ * @param {Boolean} validate
+ * Validate result? Default true.
+ *
+ * @yield {String}
+ * The uniqueId of the changed row.
+ */
+function* editCell(id, column, newValue, validate = true) {
+ let row = getRowCells(id, true);
+ let editableFieldsEngine = gUI.table._editableFieldsEngine;
+
+ editableFieldsEngine.edit(row[column]);
+
+ yield typeWithTerminator(newValue, "VK_RETURN", validate);
+}
+
+/**
+ * Begin edit mode for a cell.
+ *
+ * @param {String} id
+ * The uniqueId of the row.
+ * @param {String} column
+ * The id of the column
+ * @param {Boolean} selectText
+ * Select text? Default true.
+ */
+function* startCellEdit(id, column, selectText = true) {
+ let row = getRowCells(id, true);
+ let editableFieldsEngine = gUI.table._editableFieldsEngine;
+ let cell = row[column];
+
+ info("Selecting row " + id);
+ gUI.table.selectedRow = id;
+
+ info("Starting cell edit (" + id + ", " + column + ")");
+ editableFieldsEngine.edit(cell);
+
+ if (!selectText) {
+ let textbox = gUI.table._editableFieldsEngine.textbox;
+ textbox.selectionEnd = textbox.selectionStart;
+ }
+}
+
+/**
+ * Check a cell value.
+ *
+ * @param {String} id
+ * The uniqueId of the row.
+ * @param {String} column
+ * The id of the column
+ * @param {String} expected
+ * Expected value.
+ */
+function checkCell(id, column, expected) {
+ is(getCellValue(id, column), expected,
+ column + " column has the right value for " + id);
+}
+
+/**
+ * Show or hide a column.
+ *
+ * @param {String} id
+ * The uniqueId of the given column.
+ * @param {Boolean} state
+ * true = show, false = hide
+ */
+function showColumn(id, state) {
+ let columns = gUI.table.columns;
+ let column = columns.get(id);
+
+ if (state) {
+ column.wrapper.removeAttribute("hidden");
+ } else {
+ column.wrapper.setAttribute("hidden", true);
+ }
+}
+
+/**
+ * Show or hide all columns.
+ *
+ * @param {Boolean} state
+ * true = show, false = hide
+ */
+function showAllColumns(state) {
+ let columns = gUI.table.columns;
+
+ for (let [id] of columns) {
+ showColumn(id, state);
+ }
+}
+
+/**
+ * Type a string in the currently selected editor and then wait for the row to
+ * be updated.
+ *
+ * @param {String} str
+ * The string to type.
+ * @param {String} terminator
+ * The terminating key e.g. VK_RETURN or VK_TAB
+ * @param {Boolean} validate
+ * Validate result? Default true.
+ */
+function* typeWithTerminator(str, terminator, validate = true) {
+ let editableFieldsEngine = gUI.table._editableFieldsEngine;
+ let textbox = editableFieldsEngine.textbox;
+ let colName = textbox.closest(".table-widget-column").id;
+
+ let changeExpected = str !== textbox.value;
+
+ if (!changeExpected) {
+ return editableFieldsEngine.currentTarget.getAttribute("data-id");
+ }
+
+ info("Typing " + str);
+ EventUtils.sendString(str);
+
+ info("Pressing " + terminator);
+ EventUtils.synthesizeKey(terminator, {});
+
+ if (validate) {
+ info("Validating results... waiting for ROW_EDIT event.");
+ let uniqueId = yield gUI.table.once(TableWidget.EVENTS.ROW_EDIT);
+
+ checkCell(uniqueId, colName, str);
+ return uniqueId;
+ }
+
+ return yield gUI.table.once(TableWidget.EVENTS.ROW_EDIT);
+}
+
+function getCurrentEditorValue() {
+ let editableFieldsEngine = gUI.table._editableFieldsEngine;
+ let textbox = editableFieldsEngine.textbox;
+
+ return textbox.value;
+}
+
+/**
+ * Press a key x times.
+ *
+ * @param {String} key
+ * The key to press e.g. VK_RETURN or VK_TAB
+ * @param {Number} x
+ * The number of times to press the key.
+ * @param {Object} modifiers
+ * The event modifier e.g. {shiftKey: true}
+ */
+function PressKeyXTimes(key, x, modifiers = {}) {
+ for (let i = 0; i < x; i++) {
+ EventUtils.synthesizeKey(key, modifiers);
+ }
+}
+
+/**
+ * Verify the storage inspector state: check that given type/host exists
+ * in the tree, and that the table contains rows with specified names.
+ *
+ * @param {Array} state Array of state specifications. For example,
+ * [["cookies", "example.com"], ["c1", "c2"]] means to select the
+ * "example.com" host in cookies and then verify there are "c1" and "c2"
+ * cookies (and no other ones).
+ */
+function* checkState(state) {
+ for (let [store, names] of state) {
+ let storeName = store.join(" > ");
+ info(`Selecting tree item ${storeName}`);
+ yield selectTreeItem(store);
+
+ let items = gUI.table.items;
+
+ is(items.size, names.length,
+ `There is correct number of rows in ${storeName}`);
+ for (let name of names) {
+ ok(items.has(name),
+ `There is item with name '${name}' in ${storeName}`);
+ }
+ }
+}
+
+/**
+ * Checks if document's active element is within the given element.
+ * @param {HTMLDocument} doc document with active element in question
+ * @param {DOMNode} container element tested on focus containment
+ * @return {Boolean}
+ */
+function containsFocus(doc, container) {
+ let elm = doc.activeElement;
+ while (elm) {
+ if (elm === container) {
+ return true;
+ }
+ elm = elm.parentNode;
+ }
+ return false;
+}
+
+var focusSearchBoxUsingShortcut = Task.async(function* (panelWin, callback) {
+ info("Focusing search box");
+ let searchBox = panelWin.document.getElementById("storage-searchbox");
+ let focused = once(searchBox, "focus");
+
+ panelWin.focus();
+ let strings = Services.strings.createBundle(
+ "chrome://devtools/locale/storage.properties");
+ synthesizeKeyShortcut(strings.GetStringFromName("storage.filter.key"));
+
+ yield focused;
+
+ if (callback) {
+ callback();
+ }
+});
diff --git a/devtools/client/storage/test/storage-cache-error.html b/devtools/client/storage/test/storage-cache-error.html
new file mode 100644
index 000000000..80b14e287
--- /dev/null
+++ b/devtools/client/storage/test/storage-cache-error.html
@@ -0,0 +1,20 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Storage inspector test for handling errors in CacheStorage</title>
+</head>
+<body>
+<script type="application/javascript;version=1.7">
+"use strict";
+
+// Create an iframe with a javascript: source URL. Such iframes are
+// considered untrusted by the CacheStorage.
+let frameEl = document.createElement("iframe");
+document.body.appendChild(frameEl);
+
+window.frameContent = 'Hello World';
+frameEl.contentWindow.location.href = "javascript:parent.frameContent";
+</script>
+</body>
+</html>
diff --git a/devtools/client/storage/test/storage-complex-values.html b/devtools/client/storage/test/storage-complex-values.html
new file mode 100644
index 000000000..d96da1932
--- /dev/null
+++ b/devtools/client/storage/test/storage-complex-values.html
@@ -0,0 +1,123 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Bug 970517 - Storage inspector front end - tests
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Storage inspector test for correct values in the sidebar</title>
+</head>
+<body>
+<script type="application/javascript;version=1.7">
+"use strict";
+let partialHostname = location.hostname.match(/^[^.]+(\..*)$/)[1];
+let cookieExpiresTime = 2000000000000;
+// Setting up some cookies to eat.
+document.cookie = "c1=" + JSON.stringify([
+ "foo", "Bar", {
+ foo: "Bar"
+ }]) + "; expires=" + new Date(cookieExpiresTime).toGMTString() +
+ "; path=/browser";
+document.cookie = "cs2=sessionCookie; path=/; domain=" + partialHostname;
+// URLEncoded cookie
+document.cookie = "c_encoded=" + encodeURIComponent(JSON.stringify({foo: {foo1: "bar"}}));
+
+// ... and some local storage items ..
+const es6 = "for";
+localStorage.setItem("ls1", JSON.stringify({
+ es6, the: "win", baz: [0, 2, 3, {
+ deep: "down",
+ nobody: "cares"
+ }]}));
+localStorage.setItem("ls2", "foobar-2");
+localStorage.setItem("ls3", "http://foobar.com/baz.php");
+// ... and finally some session storage items too
+sessionStorage.setItem("ss1", "This#is#an#array");
+sessionStorage.setItem("ss2", "This~is~another~array");
+sessionStorage.setItem("ss3", "this#is~an#object~foo#bar");
+sessionStorage.setItem("ss4", "#array##with#empty#items");
+// long string that is almost an object and might trigger exponential
+// regexp backtracking
+let s = "a".repeat(1000);
+sessionStorage.setItem("ss5", `${s}=${s}=${s}=${s}&${s}=${s}&${s}`);
+console.log("added cookies and stuff from main page");
+
+let idbGenerator = function*() {
+ let request = indexedDB.open("idb1", 1);
+ request.onerror = function() {
+ throw new Error("error opening db connection");
+ };
+ let db = yield new Promise(done => {
+ request.onupgradeneeded = event => {
+ let db = event.target.result;
+ let store1 = db.createObjectStore("obj1", { keyPath: "id" });
+ store1.createIndex("name", "name", { unique: false });
+ store1.createIndex("email", "email", { unique: true });
+ db.createObjectStore("obj2", { keyPath: "id2" });
+ store1.transaction.oncomplete = () => {
+ done(db);
+ };
+ };
+ });
+
+ // Prevents AbortError
+ yield new Promise(done => {
+ request.onsuccess = done;
+ });
+
+ let transaction = db.transaction(["obj1", "obj2"], "readwrite");
+ let store1 = transaction.objectStore("obj1");
+ let store2 = transaction.objectStore("obj2");
+
+ store1.add({id: 1, name: "foo", email: "foo@bar.com"});
+ store1.add({id: 2, name: "foo2", email: "foo2@bar.com"});
+ store1.add({id: 3, name: "foo2", email: "foo3@bar.com"});
+ store2.add({
+ id2: 1,
+ name: "foo",
+ email: "foo@bar.com",
+ extra: "baz"});
+
+ db.close();
+
+ request = indexedDB.open("idb2", 1);
+ let db2 = yield new Promise(done => {
+ request.onupgradeneeded = event => {
+ let db2 = event.target.result;
+ let store3 = db2.createObjectStore("obj3", { keyPath: "id3" });
+ store3.createIndex("name2", "name2", { unique: true });
+ store3.transaction.oncomplete = () => {
+ done(db2);
+ };
+ };
+ });
+
+ // Prevents AbortError during close()
+ yield new Promise(done => {
+ request.onsuccess = done;
+ });
+
+ db2.close();
+ console.log("added cookies and stuff from main page");
+};
+
+function deleteDB(dbName) {
+ return new Promise(resolve => {
+ dump("removing database " + dbName + " from " + document.location + "\n");
+ indexedDB.deleteDatabase(dbName).onsuccess = resolve;
+ });
+}
+
+window.setup = function*() {
+ yield idbGenerator();
+};
+
+window.clear = function*() {
+ yield deleteDB("idb1");
+ yield deleteDB("idb2");
+
+ dump("removed indexedDB data from " + document.location + "\n");
+};
+</script>
+</body>
+</html>
diff --git a/devtools/client/storage/test/storage-cookies.html b/devtools/client/storage/test/storage-cookies.html
new file mode 100644
index 000000000..97c15abaa
--- /dev/null
+++ b/devtools/client/storage/test/storage-cookies.html
@@ -0,0 +1,24 @@
+<!DOCTYPE HTML>
+<html>
+ <!--
+ Bug 970517 - Storage inspector front end - tests
+ -->
+ <head>
+ <meta charset="utf-8">
+ <title>Storage inspector cookie test</title>
+ </head>
+ <body>
+ <script type="application/javascript;version=1.7">
+ "use strict";
+ let expiresIn24Hours = new Date(Date.now() + 60 * 60 * 24 * 1000).toUTCString();
+ for (let i = 1; i <= 5; i++) {
+ let cookieString = "test" + i + "=value" + i +
+ ";expires=" + expiresIn24Hours + ";path=/browser";
+ if (i % 2) {
+ cookieString += ";domain=test1.example.org";
+ }
+ document.cookie = cookieString;
+ }
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/storage/test/storage-empty-objectstores.html b/devtools/client/storage/test/storage-empty-objectstores.html
new file mode 100644
index 000000000..096e90a32
--- /dev/null
+++ b/devtools/client/storage/test/storage-empty-objectstores.html
@@ -0,0 +1,62 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for proper listing indexedDB databases with no object stores</title>
+</head>
+<body>
+<script type="application/javascript;version=1.7">
+
+window.setup = function* () {
+ let request = indexedDB.open("idb1", 1);
+ let db = yield new Promise((resolve, reject) => {
+ request.onerror = e => reject(Error("error opening db connection"));
+ request.onupgradeneeded = event => {
+ let db = event.target.result;
+ let store1 = db.createObjectStore("obj1", { keyPath: "id" });
+ store1.createIndex("name", "name", { unique: false });
+ store1.createIndex("email", "email", { unique: true });
+ let store2 = db.createObjectStore("obj2", { keyPath: "id2" });
+ store1.transaction.oncomplete = () => resolve(db);
+ };
+ });
+
+ yield new Promise(resolve => request.onsuccess = resolve);
+
+ let transaction = db.transaction(["obj1", "obj2"], "readwrite");
+ let store1 = transaction.objectStore("obj1");
+ let store2 = transaction.objectStore("obj2");
+
+ store1.add({id: 1, name: "foo", email: "foo@bar.com"});
+ store1.add({id: 2, name: "foo2", email: "foo2@bar.com"});
+ store1.add({id: 3, name: "foo2", email: "foo3@bar.com"});
+ store2.add({id2: 1, name: "foo", email: "foo@bar.com", extra: "baz"});
+
+ yield new Promise(resolve => transaction.oncomplete = resolve);
+
+ db.close();
+
+ request = indexedDB.open("idb2", 1);
+ let db2 = yield new Promise((resolve, reject) => {
+ request.onerror = e => reject(Error("error opening db2 connection"));
+ request.onupgradeneeded = event => resolve(event.target.result);
+ });
+
+ yield new Promise(resolve => request.onsuccess = resolve);
+
+ db2.close();
+ dump("added indexedDB items from main page\n");
+};
+
+window.clear = function* () {
+ for (let dbName of ["idb1", "idb2"]) {
+ yield new Promise(resolve => {
+ indexedDB.deleteDatabase(dbName).onsuccess = resolve;
+ });
+ }
+ dump("removed indexedDB items from main page\n");
+};
+
+</script>
+</body>
+</html>
diff --git a/devtools/client/storage/test/storage-idb-delete-blocked.html b/devtools/client/storage/test/storage-idb-delete-blocked.html
new file mode 100644
index 000000000..ef7017f08
--- /dev/null
+++ b/devtools/client/storage/test/storage-idb-delete-blocked.html
@@ -0,0 +1,52 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for proper listing indexedDB databases with no object stores</title>
+</head>
+<body>
+<script type="application/javascript;version=1.7">
+
+let db;
+
+window.setup = function* () {
+ db = yield new Promise((resolve, reject) => {
+ let request = indexedDB.open("idb", 1);
+
+ request.onsuccess = e => resolve(e.target.result);
+ request.onerror = e => reject(new Error("error opening db connection"));
+
+ request.onupgradeneeded = e => {
+ let db = e.target.result;
+ let store = db.createObjectStore("obj", { keyPath: "id" });
+ };
+ });
+
+ dump("opened indexedDB\n");
+};
+
+window.closeDb = function* () {
+ db.close();
+};
+
+window.deleteDb = function* () {
+ yield new Promise((resolve, reject) => {
+ let request = indexedDB.deleteDatabase("idb");
+
+ request.onsuccess = resolve;
+ request.onerror = e => reject(new Error("error deleting db"));
+ });
+};
+
+window.clear = function* () {
+ for (let dbName of ["idb1", "idb2"]) {
+ yield new Promise(resolve => {
+ indexedDB.deleteDatabase(dbName).onsuccess = resolve;
+ });
+ }
+ dump("removed indexedDB items from main page\n");
+};
+
+</script>
+</body>
+</html>
diff --git a/devtools/client/storage/test/storage-listings.html b/devtools/client/storage/test/storage-listings.html
new file mode 100644
index 000000000..de3054d3a
--- /dev/null
+++ b/devtools/client/storage/test/storage-listings.html
@@ -0,0 +1,126 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Bug 970517 - Storage inspector front end - tests
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Storage inspector test for listing hosts and storages</title>
+</head>
+<body>
+<iframe src="http://sectest1.example.org/browser/devtools/client/storage/test/storage-unsecured-iframe.html"></iframe>
+<iframe src="https://sectest1.example.org:443/browser/devtools/client/storage/test/storage-secured-iframe.html"></iframe>
+<script type="application/javascript;version=1.7">
+"use strict";
+let partialHostname = location.hostname.match(/^[^.]+(\..*)$/)[1];
+let cookieExpiresTime1 = 2000000000000;
+let cookieExpiresTime2 = 2000000001000;
+// Setting up some cookies to eat.
+document.cookie = "c1=foobar; expires=" +
+ new Date(cookieExpiresTime1).toGMTString() + "; path=/browser";
+document.cookie = "cs2=sessionCookie; path=/; domain=" + partialHostname;
+document.cookie = "c3=foobar-2; expires=" +
+ new Date(cookieExpiresTime2).toGMTString() + "; path=/";
+// ... and some local storage items ..
+localStorage.setItem("ls1", "foobar");
+localStorage.setItem("ls2", "foobar-2");
+// ... and finally some session storage items too
+sessionStorage.setItem("ss1", "foobar-3");
+dump("added cookies and storage from main page\n");
+
+let idbGenerator = function*() {
+ let request = indexedDB.open("idb1", 1);
+ request.onerror = function() {
+ throw new Error("error opening db connection");
+ };
+ let db = yield new Promise(done => {
+ request.onupgradeneeded = event => {
+ let db = event.target.result;
+ let store1 = db.createObjectStore("obj1", { keyPath: "id" });
+ store1.createIndex("name", "name", { unique: false });
+ store1.createIndex("email", "email", { unique: true });
+ let store2 = db.createObjectStore("obj2", { keyPath: "id2" });
+ store1.transaction.oncomplete = () => {
+ done(db);
+ };
+ };
+ });
+
+ // Prevents AbortError
+ yield new Promise(done => {
+ request.onsuccess = done;
+ });
+
+ let transaction = db.transaction(["obj1", "obj2"], "readwrite");
+ let store1 = transaction.objectStore("obj1");
+ let store2 = transaction.objectStore("obj2");
+ store1.add({id: 1, name: "foo", email: "foo@bar.com"});
+ store1.add({id: 2, name: "foo2", email: "foo2@bar.com"});
+ store1.add({id: 3, name: "foo2", email: "foo3@bar.com"});
+ store2.add({
+ id2: 1,
+ name: "foo",
+ email: "foo@bar.com",
+ extra: "baz"
+ });
+ // Prevents AbortError during close()
+ yield new Promise(success => {
+ transaction.oncomplete = success;
+ });
+
+ db.close();
+
+ request = indexedDB.open("idb2", 1);
+ let db2 = yield new Promise(done => {
+ request.onupgradeneeded = event => {
+ let db2 = event.target.result;
+ let store3 = db2.createObjectStore("obj3", { keyPath: "id3" });
+ store3.createIndex("name2", "name2", { unique: true });
+ store3.transaction.oncomplete = () => {
+ done(db2);
+ }
+ };
+ });
+ // Prevents AbortError during close()
+ yield new Promise(done => {
+ request.onsuccess = done;
+ });
+ db2.close();
+
+ dump("added indexedDB from main page\n");
+};
+
+function deleteDB(dbName) {
+ return new Promise(resolve => {
+ dump("removing database " + dbName + " from " + document.location + "\n");
+ indexedDB.deleteDatabase(dbName).onsuccess = resolve;
+ });
+}
+
+function fetchPut(cache, url) {
+ let response = yield fetch(url);
+ yield cache.put(url, response);
+}
+
+let cacheGenerator = function*() {
+ let cache = yield caches.open("plop");
+ yield fetchPut(cache, "404_cached_file.js");
+ yield fetchPut(cache, "browser_storage_basic.js");
+};
+
+window.setup = function*() {
+ yield idbGenerator();
+ yield cacheGenerator();
+};
+
+window.clear = function*() {
+ yield deleteDB("idb1");
+ yield deleteDB("idb2");
+
+ yield caches.delete("plop");
+
+ dump("removed indexedDB and cache data from " + document.location + "\n");
+};
+</script>
+</body>
+</html>
diff --git a/devtools/client/storage/test/storage-localstorage.html b/devtools/client/storage/test/storage-localstorage.html
new file mode 100644
index 000000000..3a560b096
--- /dev/null
+++ b/devtools/client/storage/test/storage-localstorage.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<html>
+ <!--
+ Bug 1231155 - Storage inspector front end - tests
+ -->
+ <head>
+ <meta charset="utf-8" />
+ <title>Storage inspector localStorage test</title>
+ <script type="application/javascript;version=1.7">
+ "use strict";
+
+ function setup() {
+ localStorage.setItem("TestLS1", "ValueLS1");
+ localStorage.setItem("TestLS2", "ValueLS2");
+ localStorage.setItem("TestLS3", "ValueLS3");
+ localStorage.setItem("TestLS4", "ValueLS4");
+ localStorage.setItem("TestLS5", "ValueLS5");
+ }
+ </script>
+ </head>
+ <body onload="setup()">
+ </body>
+</html>
diff --git a/devtools/client/storage/test/storage-overflow.html b/devtools/client/storage/test/storage-overflow.html
new file mode 100644
index 000000000..ee8db36e6
--- /dev/null
+++ b/devtools/client/storage/test/storage-overflow.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Bug 1171903 - Storage Inspector endless scrolling
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Storage inspector endless scrolling test</title>
+</head>
+<body>
+<script type="text/javascript;version=1.8">
+"use strict";
+
+for (let i = 0; i < 160; i++) {
+ localStorage.setItem(`item-${i}`, `value-${i}`);
+}
+</script>
+</body>
+</html>
diff --git a/devtools/client/storage/test/storage-search.html b/devtools/client/storage/test/storage-search.html
new file mode 100644
index 000000000..1a84ff622
--- /dev/null
+++ b/devtools/client/storage/test/storage-search.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Bug 1224115 - Storage Inspector table filtering
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Storage inspector table filtering test</title>
+</head>
+<body>
+<script type="text/javascript;version=1.8">
+"use strict";
+
+localStorage.setItem("01234", "56789");
+localStorage.setItem("ANIMAL", "hOrSe");
+localStorage.setItem("FOO", "bArBaz");
+localStorage.setItem("food", "energy bar");
+localStorage.setItem("money", "##$$$**");
+localStorage.setItem("sport", "football");
+localStorage.setItem("year", "2016");
+</script>
+</body>
+</html>
diff --git a/devtools/client/storage/test/storage-secured-iframe.html b/devtools/client/storage/test/storage-secured-iframe.html
new file mode 100644
index 000000000..8424fd4cd
--- /dev/null
+++ b/devtools/client/storage/test/storage-secured-iframe.html
@@ -0,0 +1,91 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Iframe for testing multiple host detetion in storage actor
+-->
+<head>
+ <meta charset="utf-8">
+</head>
+<body>
+<script type="application/javascript;version=1.7">
+"use strict";
+document.cookie = "sc1=foobar;";
+localStorage.setItem("iframe-s-ls1", "foobar");
+sessionStorage.setItem("iframe-s-ss1", "foobar-2");
+dump("added cookies and storage from secured iframe\n");
+
+let idbGenerator = function*() {
+ let request = indexedDB.open("idb-s1", 1);
+ request.onerror = function() {
+ throw new Error("error opening db connection");
+ };
+ let db = yield new Promise(done => {
+ request.onupgradeneeded = event => {
+ let db = event.target.result;
+ let store1 = db.createObjectStore("obj-s1", { keyPath: "id" });
+ store1.transaction.oncomplete = () => {
+ done(db);
+ };
+ };
+ });
+ yield new Promise(done => {
+ request.onsuccess = done;
+ });
+
+ let transaction = db.transaction(["obj-s1"], "readwrite");
+ let store1 = transaction.objectStore("obj-s1");
+ store1.add({id: 6, name: "foo", email: "foo@bar.com"});
+ store1.add({id: 7, name: "foo2", email: "foo2@bar.com"});
+ yield new Promise(success => {
+ transaction.oncomplete = success;
+ });
+
+ db.close();
+
+ request = indexedDB.open("idb-s2", 1);
+ let db2 = yield new Promise(done => {
+ request.onupgradeneeded = event => {
+ let db2 = event.target.result;
+ let store3 =
+ db2.createObjectStore("obj-s2", { keyPath: "id3", autoIncrement: true });
+ store3.createIndex("name2", "name2", { unique: true });
+ store3.transaction.oncomplete = () => {
+ done(db2);
+ };
+ };
+ });
+ yield new Promise(done => {
+ request.onsuccess = done;
+ });
+
+ transaction = db2.transaction(["obj-s2"], "readwrite");
+ let store3 = transaction.objectStore("obj-s2");
+ store3.add({id3: 16, name2: "foo", email: "foo@bar.com"});
+ yield new Promise(success => {
+ transaction.oncomplete = success;
+ });
+
+ db2.close();
+ dump("added indexedDB from secured iframe\n");
+};
+
+function deleteDB(dbName) {
+ return new Promise(resolve => {
+ dump("removing database " + dbName + " from " + document.location + "\n");
+ indexedDB.deleteDatabase(dbName).onsuccess = resolve;
+ });
+}
+
+window.setup = function*() {
+ yield idbGenerator();
+};
+
+window.clear = function*() {
+ yield deleteDB("idb-s1");
+ yield deleteDB("idb-s2");
+
+ dump("removed indexedDB data from " + document.location + "\n");
+};
+</script>
+</body>
+</html>
diff --git a/devtools/client/storage/test/storage-sessionstorage.html b/devtools/client/storage/test/storage-sessionstorage.html
new file mode 100644
index 000000000..12de07e13
--- /dev/null
+++ b/devtools/client/storage/test/storage-sessionstorage.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<html>
+ <!--
+ Bug 1231179 - Storage inspector front end - tests
+ -->
+ <head>
+ <meta charset="utf-8" />
+ <title>Storage inspector sessionStorage test</title>
+ <script type="application/javascript;version=1.7">
+ "use strict";
+
+ function setup() {
+ sessionStorage.setItem("TestSS1", "ValueSS1");
+ sessionStorage.setItem("TestSS2", "ValueSS2");
+ sessionStorage.setItem("TestSS3", "ValueSS3");
+ sessionStorage.setItem("TestSS4", "ValueSS4");
+ sessionStorage.setItem("TestSS5", "ValueSS5");
+ }
+ </script>
+ </head>
+ <body onload="setup()">
+ </body>
+</html>
diff --git a/devtools/client/storage/test/storage-unsecured-iframe.html b/devtools/client/storage/test/storage-unsecured-iframe.html
new file mode 100644
index 000000000..a69ffdfd1
--- /dev/null
+++ b/devtools/client/storage/test/storage-unsecured-iframe.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Iframe for testing multiple host detetion in storage actor
+-->
+<head>
+ <meta charset="utf-8">
+</head>
+<body>
+<script>
+"use strict";
+document.cookie = "uc1=foobar; domain=.example.org; path=/";
+localStorage.setItem("iframe-u-ls1", "foobar");
+sessionStorage.setItem("iframe-u-ss1", "foobar1");
+sessionStorage.setItem("iframe-u-ss2", "foobar2");
+dump("added cookies and storage from unsecured iframe\n");
+</script>
+</body>
+</html>
diff --git a/devtools/client/storage/test/storage-updates.html b/devtools/client/storage/test/storage-updates.html
new file mode 100644
index 000000000..a009814b2
--- /dev/null
+++ b/devtools/client/storage/test/storage-updates.html
@@ -0,0 +1,64 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Bug 965872 - Storage inspector actor with cookies, local storage and session storage.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Storage inspector blank html for tests</title>
+</head>
+<body>
+<script type="application/javascript;version=1.7">
+"use strict";
+window.addCookie = function(name, value, path, domain, expires, secure) {
+ let cookieString = name + "=" + value + ";";
+ if (path) {
+ cookieString += "path=" + path + ";";
+ }
+ if (domain) {
+ cookieString += "domain=" + domain + ";";
+ }
+ if (expires) {
+ cookieString += "expires=" + expires + ";";
+ }
+ if (secure) {
+ cookieString += "secure=true;";
+ }
+ document.cookie = cookieString;
+};
+
+window.removeCookie = function(name, path) {
+ document.cookie =
+ name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=" + path;
+};
+
+/**
+ * We keep this method here even though these items are automatically cleared
+ * after the test is complete. this is so that the store-objects-cleared event
+ * can be tested.
+ */
+window.clear = function*() {
+ sessionStorage.clear();
+
+ dump("removed sessionStorage from " + document.location + "\n");
+};
+
+window.onload = function() {
+ addCookie("c1", "1.2.3.4.5.6.7", "/browser");
+ addCookie("c2", "foobar", "/browser");
+
+ localStorage.setItem("ls1", "testing");
+ localStorage.setItem("ls2", "testing");
+ localStorage.setItem("ls3", "testing");
+ localStorage.setItem("ls4", "testing");
+ localStorage.setItem("ls5", "testing");
+ localStorage.setItem("ls6", "testing");
+ localStorage.setItem("ls7", "testing");
+
+ sessionStorage.setItem("ss1", "foobar");
+ sessionStorage.setItem("ss2", "foobar");
+ sessionStorage.setItem("ss3", "foobar");
+};
+</script>
+</body>
+</html>
diff --git a/devtools/client/storage/ui.js b/devtools/client/storage/ui.js
new file mode 100644
index 000000000..6af493e44
--- /dev/null
+++ b/devtools/client/storage/ui.js
@@ -0,0 +1,1073 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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 {Task} = require("devtools/shared/task");
+const EventEmitter = require("devtools/shared/event-emitter");
+const {LocalizationHelper, ELLIPSIS} = require("devtools/shared/l10n");
+const {KeyShortcuts} = require("devtools/client/shared/key-shortcuts");
+const JSOL = require("devtools/client/shared/vendor/jsol");
+const {KeyCodes} = require("devtools/client/shared/keycodes");
+
+loader.lazyRequireGetter(this, "TreeWidget",
+ "devtools/client/shared/widgets/TreeWidget", true);
+loader.lazyRequireGetter(this, "TableWidget",
+ "devtools/client/shared/widgets/TableWidget", true);
+loader.lazyRequireGetter(this, "ViewHelpers",
+ "devtools/client/shared/widgets/view-helpers");
+loader.lazyImporter(this, "VariablesView",
+ "resource://devtools/client/shared/widgets/VariablesView.jsm");
+
+/**
+ * Localization convenience methods.
+ */
+const STORAGE_STRINGS = "devtools/client/locales/storage.properties";
+const L10N = new LocalizationHelper(STORAGE_STRINGS);
+
+const GENERIC_VARIABLES_VIEW_SETTINGS = {
+ lazyEmpty: true,
+ // ms
+ lazyEmptyDelay: 10,
+ searchEnabled: true,
+ searchPlaceholder: L10N.getStr("storage.search.placeholder"),
+ preventDescriptorModifiers: true
+};
+
+// Columns which are hidden by default in the storage table
+const HIDDEN_COLUMNS = [
+ "creationTime",
+ "isDomain",
+ "isSecure"
+];
+
+const REASON = {
+ NEW_ROW: "new-row",
+ NEXT_50_ITEMS: "next-50-items",
+ POPULATE: "populate",
+ UPDATE: "update"
+};
+
+const COOKIE_KEY_MAP = {
+ path: "Path",
+ host: "Domain",
+ expires: "Expires",
+ isSecure: "Secure",
+ isHttpOnly: "HttpOnly",
+ isDomain: "HostOnly",
+ creationTime: "CreationTime",
+ lastAccessed: "LastAccessed"
+};
+
+// Maximum length of item name to show in context menu label - will be
+// trimmed with ellipsis if it's longer.
+const ITEM_NAME_MAX_LENGTH = 32;
+
+function addEllipsis(name) {
+ if (name.length > ITEM_NAME_MAX_LENGTH) {
+ if (/^https?:/.test(name)) {
+ // For URLs, add ellipsis in the middle
+ const halfLen = ITEM_NAME_MAX_LENGTH / 2;
+ return name.slice(0, halfLen) + ELLIPSIS + name.slice(-halfLen);
+ }
+
+ // For other strings, add ellipsis at the end
+ return name.substr(0, ITEM_NAME_MAX_LENGTH) + ELLIPSIS;
+ }
+
+ return name;
+}
+
+/**
+ * StorageUI is controls and builds the UI of the Storage Inspector.
+ *
+ * @param {Front} front
+ * Front for the storage actor
+ * @param {Target} target
+ * Interface for the page we're debugging
+ * @param {Window} panelWin
+ * Window of the toolbox panel to populate UI in.
+ */
+function StorageUI(front, target, panelWin, toolbox) {
+ EventEmitter.decorate(this);
+
+ this._target = target;
+ this._window = panelWin;
+ this._panelDoc = panelWin.document;
+ this._toolbox = toolbox;
+ this.front = front;
+
+ let treeNode = this._panelDoc.getElementById("storage-tree");
+ this.tree = new TreeWidget(treeNode, {
+ defaultType: "dir",
+ contextMenuId: "storage-tree-popup"
+ });
+ this.onHostSelect = this.onHostSelect.bind(this);
+ this.tree.on("select", this.onHostSelect);
+
+ let tableNode = this._panelDoc.getElementById("storage-table");
+ this.table = new TableWidget(tableNode, {
+ emptyText: L10N.getStr("table.emptyText"),
+ highlightUpdated: true,
+ cellContextMenuId: "storage-table-popup"
+ });
+
+ this.displayObjectSidebar = this.displayObjectSidebar.bind(this);
+ this.table.on(TableWidget.EVENTS.ROW_SELECTED, this.displayObjectSidebar);
+
+ this.handleScrollEnd = this.handleScrollEnd.bind(this);
+ this.table.on(TableWidget.EVENTS.SCROLL_END, this.handleScrollEnd);
+
+ this.editItem = this.editItem.bind(this);
+ this.table.on(TableWidget.EVENTS.CELL_EDIT, this.editItem);
+
+ this.sidebar = this._panelDoc.getElementById("storage-sidebar");
+ this.sidebar.setAttribute("width", "300");
+ this.view = new VariablesView(this.sidebar.firstChild,
+ GENERIC_VARIABLES_VIEW_SETTINGS);
+
+ this.searchBox = this._panelDoc.getElementById("storage-searchbox");
+ this.filterItems = this.filterItems.bind(this);
+ this.searchBox.addEventListener("command", this.filterItems);
+
+ let shortcuts = new KeyShortcuts({
+ window: this._panelDoc.defaultView,
+ });
+ let key = L10N.getStr("storage.filter.key");
+ shortcuts.on(key, (name, event) => {
+ event.preventDefault();
+ this.searchBox.focus();
+ });
+
+ this.front.listStores().then(storageTypes => {
+ this.populateStorageTree(storageTypes);
+ }).then(null, console.error);
+
+ this.onUpdate = this.onUpdate.bind(this);
+ this.front.on("stores-update", this.onUpdate);
+ this.onCleared = this.onCleared.bind(this);
+ this.front.on("stores-cleared", this.onCleared);
+
+ this.handleKeypress = this.handleKeypress.bind(this);
+ this._panelDoc.addEventListener("keypress", this.handleKeypress);
+
+ this.onTreePopupShowing = this.onTreePopupShowing.bind(this);
+ this._treePopup = this._panelDoc.getElementById("storage-tree-popup");
+ this._treePopup.addEventListener("popupshowing", this.onTreePopupShowing);
+
+ this.onTablePopupShowing = this.onTablePopupShowing.bind(this);
+ this._tablePopup = this._panelDoc.getElementById("storage-table-popup");
+ this._tablePopup.addEventListener("popupshowing", this.onTablePopupShowing);
+
+ this.onRemoveItem = this.onRemoveItem.bind(this);
+ this.onRemoveAllFrom = this.onRemoveAllFrom.bind(this);
+ this.onRemoveAll = this.onRemoveAll.bind(this);
+ this.onRemoveTreeItem = this.onRemoveTreeItem.bind(this);
+
+ this._tablePopupDelete = this._panelDoc.getElementById(
+ "storage-table-popup-delete");
+ this._tablePopupDelete.addEventListener("command", this.onRemoveItem);
+
+ this._tablePopupDeleteAllFrom = this._panelDoc.getElementById(
+ "storage-table-popup-delete-all-from");
+ this._tablePopupDeleteAllFrom.addEventListener("command",
+ this.onRemoveAllFrom);
+
+ this._tablePopupDeleteAll = this._panelDoc.getElementById(
+ "storage-table-popup-delete-all");
+ this._tablePopupDeleteAll.addEventListener("command", this.onRemoveAll);
+
+ this._treePopupDeleteAll = this._panelDoc.getElementById(
+ "storage-tree-popup-delete-all");
+ this._treePopupDeleteAll.addEventListener("command", this.onRemoveAll);
+
+ this._treePopupDelete = this._panelDoc.getElementById("storage-tree-popup-delete");
+ this._treePopupDelete.addEventListener("command", this.onRemoveTreeItem);
+}
+
+exports.StorageUI = StorageUI;
+
+StorageUI.prototype = {
+
+ storageTypes: null,
+ shouldLoadMoreItems: true,
+
+ set animationsEnabled(value) {
+ this._panelDoc.documentElement.classList.toggle("no-animate", !value);
+ },
+
+ destroy: function () {
+ this.table.off(TableWidget.EVENTS.ROW_SELECTED, this.displayObjectSidebar);
+ this.table.off(TableWidget.EVENTS.SCROLL_END, this.handleScrollEnd);
+ this.table.off(TableWidget.EVENTS.CELL_EDIT, this.editItem);
+ this.table.destroy();
+
+ this.front.off("stores-update", this.onUpdate);
+ this.front.off("stores-cleared", this.onCleared);
+ this._panelDoc.removeEventListener("keypress", this.handleKeypress);
+ this.searchBox.removeEventListener("input", this.filterItems);
+ this.searchBox = null;
+
+ this._treePopup.removeEventListener("popupshowing", this.onTreePopupShowing);
+ this._treePopupDeleteAll.removeEventListener("command", this.onRemoveAll);
+ this._treePopupDelete.removeEventListener("command", this.onRemoveTreeItem);
+
+ this._tablePopup.removeEventListener("popupshowing", this.onTablePopupShowing);
+ this._tablePopupDelete.removeEventListener("command", this.onRemoveItem);
+ this._tablePopupDeleteAllFrom.removeEventListener("command", this.onRemoveAllFrom);
+ this._tablePopupDeleteAll.removeEventListener("command", this.onRemoveAll);
+ },
+
+ /**
+ * Empties and hides the object viewer sidebar
+ */
+ hideSidebar: function () {
+ this.view.empty();
+ this.sidebar.hidden = true;
+ this.table.clearSelection();
+ },
+
+ getCurrentActor: function () {
+ let type = this.table.datatype;
+
+ return this.storageTypes[type];
+ },
+
+ /**
+ * Make column fields editable
+ *
+ * @param {Array} editableFields
+ * An array of keys of columns to be made editable
+ */
+ makeFieldsEditable: function* (editableFields) {
+ if (editableFields && editableFields.length > 0) {
+ this.table.makeFieldsEditable(editableFields);
+ } else if (this.table._editableFieldsEngine) {
+ this.table._editableFieldsEngine.destroy();
+ }
+ },
+
+ editItem: function (eventType, data) {
+ let actor = this.getCurrentActor();
+
+ actor.editItem(data);
+ },
+
+ /**
+ * Removes the given item from the storage table. Reselects the next item in
+ * the table and repopulates the sidebar with that item's data if the item
+ * being removed was selected.
+ */
+ removeItemFromTable: function (name) {
+ if (this.table.isSelected(name)) {
+ if (this.table.selectedIndex == 0) {
+ this.table.selectNextRow();
+ } else {
+ this.table.selectPreviousRow();
+ }
+ this.table.remove(name);
+ this.displayObjectSidebar();
+ } else {
+ this.table.remove(name);
+ }
+ },
+
+ /**
+ * Event handler for "stores-cleared" event coming from the storage actor.
+ *
+ * @param {object} response
+ * An object containing which storage types were cleared
+ */
+ onCleared: function (response) {
+ function* enumPaths() {
+ for (let type in response) {
+ if (Array.isArray(response[type])) {
+ // Handle the legacy response with array of hosts
+ for (let host of response[type]) {
+ yield [type, host];
+ }
+ } else {
+ // Handle the new format that supports clearing sub-stores in a host
+ for (let host in response[type]) {
+ let paths = response[type][host];
+
+ if (!paths.length) {
+ yield [type, host];
+ } else {
+ for (let path of paths) {
+ try {
+ path = JSON.parse(path);
+ yield [type, host, ...path];
+ } catch (ex) {
+ // ignore
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ for (let path of enumPaths()) {
+ // Find if the path is selected (there is max one) and clear it
+ if (this.tree.isSelected(path)) {
+ this.table.clear();
+ this.hideSidebar();
+ this.emit("store-objects-cleared");
+ break;
+ }
+ }
+ },
+
+ /**
+ * Event handler for "stores-update" event coming from the storage actor.
+ *
+ * @param {object} argument0
+ * An object containing the details of the added, changed and deleted
+ * storage objects.
+ * Each of these 3 objects are of the following format:
+ * {
+ * <store_type1>: {
+ * <host1>: [<store_names1>, <store_name2>...],
+ * <host2>: [<store_names34>...], ...
+ * },
+ * <store_type2>: {
+ * <host1>: [<store_names1>, <store_name2>...],
+ * <host2>: [<store_names34>...], ...
+ * }, ...
+ * }
+ * Where store_type1 and store_type2 is one of cookies, indexedDB,
+ * sessionStorage and localStorage; host1, host2 are the host in which
+ * this change happened; and [<store_namesX] is an array of the names
+ * of the changed store objects. This array is empty for deleted object
+ * if the host was completely removed.
+ */
+ onUpdate: function ({ changed, added, deleted }) {
+ if (deleted) {
+ this.handleDeletedItems(deleted);
+ }
+
+ if (added) {
+ this.handleAddedItems(added);
+ }
+
+ if (changed) {
+ this.handleChangedItems(changed);
+ }
+
+ if (added || deleted || changed) {
+ this.emit("store-objects-updated");
+ }
+ },
+
+ /**
+ * Handle added items received by onUpdate
+ *
+ * @param {object} See onUpdate docs
+ */
+ handleAddedItems: function (added) {
+ for (let type in added) {
+ for (let host in added[type]) {
+ this.tree.add([type, {id: host, type: "url"}]);
+ for (let name of added[type][host]) {
+ try {
+ name = JSON.parse(name);
+ if (name.length == 3) {
+ name.splice(2, 1);
+ }
+ this.tree.add([type, host, ...name]);
+ if (!this.tree.selectedItem) {
+ this.tree.selectedItem = [type, host, name[0], name[1]];
+ this.fetchStorageObjects(type, host, [JSON.stringify(name)],
+ REASON.NEW_ROW);
+ }
+ } catch (ex) {
+ // Do nothing
+ }
+ }
+
+ if (this.tree.isSelected([type, host])) {
+ this.fetchStorageObjects(type, host, added[type][host],
+ REASON.NEW_ROW);
+ }
+ }
+ }
+ },
+
+ /**
+ * Handle deleted items received by onUpdate
+ *
+ * @param {object} See onUpdate docs
+ */
+ handleDeletedItems: function (deleted) {
+ for (let type in deleted) {
+ for (let host in deleted[type]) {
+ if (!deleted[type][host].length) {
+ // This means that the whole host is deleted, thus the item should
+ // be removed from the storage tree
+ if (this.tree.isSelected([type, host])) {
+ this.table.clear();
+ this.hideSidebar();
+ this.tree.selectPreviousItem();
+ }
+
+ this.tree.remove([type, host]);
+ } else {
+ for (let name of deleted[type][host]) {
+ try {
+ // trying to parse names in case of indexedDB or cache
+ let names = JSON.parse(name);
+ // Is a whole cache, database or objectstore deleted?
+ // Then remove it from the tree.
+ if (names.length < 3) {
+ if (this.tree.isSelected([type, host, ...names])) {
+ this.table.clear();
+ this.hideSidebar();
+ this.tree.selectPreviousItem();
+ }
+ this.tree.remove([type, host, ...names]);
+ }
+
+ // Remove the item from table if currently displayed.
+ if (names.length > 0) {
+ let tableItemName = names.pop();
+ if (this.tree.isSelected([type, host, ...names])) {
+ this.removeItemFromTable(tableItemName);
+ }
+ }
+ } catch (ex) {
+ if (this.tree.isSelected([type, host])) {
+ this.removeItemFromTable(name);
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+
+ /**
+ * Handle changed items received by onUpdate
+ *
+ * @param {object} See onUpdate docs
+ */
+ handleChangedItems: function (changed) {
+ let [type, host, db, objectStore] = this.tree.selectedItem;
+ if (!changed[type] || !changed[type][host] ||
+ changed[type][host].length == 0) {
+ return;
+ }
+ try {
+ let toUpdate = [];
+ for (let name of changed[type][host]) {
+ let names = JSON.parse(name);
+ if (names[0] == db && names[1] == objectStore && names[2]) {
+ toUpdate.push(name);
+ }
+ }
+ this.fetchStorageObjects(type, host, toUpdate, REASON.UPDATE);
+ } catch (ex) {
+ this.fetchStorageObjects(type, host, changed[type][host], REASON.UPDATE);
+ }
+ },
+
+ /**
+ * Fetches the storage objects from the storage actor and populates the
+ * storage table with the returned data.
+ *
+ * @param {string} type
+ * The type of storage. Ex. "cookies"
+ * @param {string} host
+ * Hostname
+ * @param {array} names
+ * Names of particular store objects. Empty if all are requested
+ * @param {Constant} reason
+ * See REASON constant at top of file.
+ */
+ fetchStorageObjects: Task.async(function* (type, host, names, reason) {
+ let fetchOpts = reason === REASON.NEXT_50_ITEMS ? {offset: this.itemOffset}
+ : {};
+ let storageType = this.storageTypes[type];
+
+ if (reason !== REASON.NEXT_50_ITEMS &&
+ reason !== REASON.UPDATE &&
+ reason !== REASON.NEW_ROW &&
+ reason !== REASON.POPULATE) {
+ throw new Error("Invalid reason specified");
+ }
+
+ try {
+ if (reason === REASON.POPULATE) {
+ let subType = null;
+ // The indexedDB type could have sub-type data to fetch.
+ // If having names specified, then it means
+ // we are fetching details of specific database or of object store.
+ if (type == "indexedDB" && names) {
+ let [ dbName, objectStoreName ] = JSON.parse(names[0]);
+ if (dbName) {
+ subType = "database";
+ }
+ if (objectStoreName) {
+ subType = "object store";
+ }
+ }
+ yield this.resetColumns(type, host, subType);
+ }
+
+ let {data} = yield storageType.getStoreObjects(host, names, fetchOpts);
+ if (data.length) {
+ this.populateTable(data, reason);
+ }
+ this.emit("store-objects-updated");
+ } catch (ex) {
+ console.error(ex);
+ }
+ }),
+
+ /**
+ * Populates the storage tree which displays the list of storages present for
+ * the page.
+ *
+ * @param {object} storageTypes
+ * List of storages and their corresponding hosts returned by the
+ * StorageFront.listStores call.
+ */
+ populateStorageTree: function (storageTypes) {
+ this.storageTypes = {};
+ for (let type in storageTypes) {
+ // Ignore `from` field, which is just a protocol.js implementation
+ // artifact.
+ if (type === "from") {
+ continue;
+ }
+ let typeLabel = type;
+ try {
+ typeLabel = L10N.getStr("tree.labels." + type);
+ } catch (e) {
+ console.error("Unable to localize tree label type:" + type);
+ }
+ this.tree.add([{id: type, label: typeLabel, type: "store"}]);
+ if (!storageTypes[type].hosts) {
+ continue;
+ }
+ this.storageTypes[type] = storageTypes[type];
+ for (let host in storageTypes[type].hosts) {
+ this.tree.add([type, {id: host, type: "url"}]);
+ for (let name of storageTypes[type].hosts[host]) {
+ try {
+ let names = JSON.parse(name);
+ this.tree.add([type, host, ...names]);
+ if (!this.tree.selectedItem) {
+ this.tree.selectedItem = [type, host, names[0], names[1]];
+ }
+ } catch (ex) {
+ // Do Nothing
+ }
+ }
+ if (!this.tree.selectedItem) {
+ this.tree.selectedItem = [type, host];
+ }
+ }
+ }
+ },
+
+ /**
+ * Populates the selected entry from teh table in the sidebar for a more
+ * detailed view.
+ */
+ displayObjectSidebar: Task.async(function* () {
+ let item = this.table.selectedRow;
+ if (!item) {
+ // Make sure that sidebar is hidden and return
+ this.sidebar.hidden = true;
+ return;
+ }
+
+ // Get the string value (async action) and the update the UI synchronously.
+ let value;
+ if (item.name && item.valueActor) {
+ value = yield item.valueActor.string();
+ }
+
+ // Start updating the UI. Everything is sync beyond this point.
+ this.sidebar.hidden = false;
+ this.view.empty();
+ let mainScope = this.view.addScope(L10N.getStr("storage.data.label"));
+ mainScope.expanded = true;
+
+ if (value) {
+ let itemVar = mainScope.addItem(item.name + "", {}, {relaxed: true});
+
+ // The main area where the value will be displayed
+ itemVar.setGrip(value);
+
+ // May be the item value is a json or a key value pair itself
+ this.parseItemValue(item.name, value);
+
+ // By default the item name and value are shown. If this is the only
+ // information available, then nothing else is to be displayed.
+ let itemProps = Object.keys(item);
+ if (itemProps.length > 3) {
+ // Display any other information other than the item name and value
+ // which may be available.
+ let rawObject = Object.create(null);
+ let otherProps = itemProps.filter(
+ e => !["name", "value", "valueActor"].includes(e));
+ for (let prop of otherProps) {
+ let cookieProp = COOKIE_KEY_MAP[prop] || prop;
+ // The pseduo property of HostOnly refers to converse of isDomain property
+ rawObject[cookieProp] = (prop === "isDomain") ? !item[prop] : item[prop];
+ }
+ itemVar.populate(rawObject, {sorted: true});
+ itemVar.twisty = true;
+ itemVar.expanded = true;
+ }
+ } else {
+ // Case when displaying IndexedDB db/object store properties.
+ for (let key in item) {
+ mainScope.addItem(key, {}, true).setGrip(item[key]);
+ this.parseItemValue(key, item[key]);
+ }
+ }
+
+ this.emit("sidebar-updated");
+ }),
+
+ /**
+ * Tries to parse a string value into either a json or a key-value separated
+ * object and populates the sidebar with the parsed value. The value can also
+ * be a key separated array.
+ *
+ * @param {string} name
+ * The key corresponding to the `value` string in the object
+ * @param {string} value
+ * The string to be parsed into an object
+ */
+ parseItemValue: function (name, originalValue) {
+ // Find if value is URLEncoded ie
+ let decodedValue = "";
+ try {
+ decodedValue = decodeURIComponent(originalValue);
+ } catch (e) {
+ // Unable to decode, nothing to do
+ }
+ let value = (decodedValue && decodedValue !== originalValue)
+ ? decodedValue : originalValue;
+
+ let json = null;
+ try {
+ json = JSOL.parse(value);
+ } catch (ex) {
+ json = null;
+ }
+
+ if (!json && value) {
+ json = this._extractKeyValPairs(value);
+ }
+
+ // return if json is null, or same as value, or just a string.
+ if (!json || json == value || typeof json == "string") {
+ return;
+ }
+
+ // One special case is a url which gets separated as key value pair on :
+ if ((json.length == 2 || Object.keys(json).length == 1) &&
+ ((json[0] || Object.keys(json)[0]) + "").match(/^(http|file|ftp)/)) {
+ return;
+ }
+
+ let jsonObject = Object.create(null);
+ let view = this.view;
+ jsonObject[name] = json;
+ let valueScope = view.getScopeAtIndex(1) ||
+ view.addScope(L10N.getStr("storage.parsedValue.label"));
+ valueScope.expanded = true;
+ let jsonVar = valueScope.addItem("", Object.create(null), {relaxed: true});
+ jsonVar.expanded = true;
+ jsonVar.twisty = true;
+ jsonVar.populate(jsonObject, {expanded: true});
+ },
+
+ /**
+ * Tries to parse a string into an object on the basis of key-value pairs,
+ * separated by various separators. If failed, tries to parse for single
+ * separator separated values to form an array.
+ *
+ * @param {string} value
+ * The string to be parsed into an object or array
+ */
+ _extractKeyValPairs: function (value) {
+ let makeObject = (keySep, pairSep) => {
+ let object = {};
+ for (let pair of value.split(pairSep)) {
+ let [key, val] = pair.split(keySep);
+ object[key] = val;
+ }
+ return object;
+ };
+
+ // Possible separators.
+ const separators = ["=", ":", "~", "#", "&", "\\*", ",", "\\."];
+ // Testing for object
+ for (let i = 0; i < separators.length; i++) {
+ let kv = separators[i];
+ for (let j = 0; j < separators.length; j++) {
+ if (i == j) {
+ continue;
+ }
+ let p = separators[j];
+ let word = `[^${kv}${p}]*`;
+ let keyValue = `${word}${kv}${word}`;
+ let keyValueList = `${keyValue}(${p}${keyValue})*`;
+ let regex = new RegExp(`^${keyValueList}$`);
+ if (value.match && value.match(regex) && value.includes(kv) &&
+ (value.includes(p) || value.split(kv).length == 2)) {
+ return makeObject(kv, p);
+ }
+ }
+ }
+ // Testing for array
+ for (let p of separators) {
+ let word = `[^${p}]*`;
+ let wordList = `(${word}${p})+${word}`;
+ let regex = new RegExp(`^${wordList}$`);
+ if (value.match && value.match(regex)) {
+ return value.split(p.replace(/\\*/g, ""));
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Select handler for the storage tree. Fetches details of the selected item
+ * from the storage details and populates the storage tree.
+ *
+ * @param {string} event
+ * The name of the event fired
+ * @param {array} item
+ * An array of ids which represent the location of the selected item in
+ * the storage tree
+ */
+ onHostSelect: function (event, item) {
+ this.table.clear();
+ this.hideSidebar();
+ this.searchBox.value = "";
+
+ let [type, host] = item;
+ let names = null;
+ if (!host) {
+ return;
+ }
+ if (item.length > 2) {
+ names = [JSON.stringify(item.slice(2))];
+ }
+ this.fetchStorageObjects(type, host, names, REASON.POPULATE);
+ this.itemOffset = 0;
+ },
+
+ /**
+ * Resets the column headers in the storage table with the pased object `data`
+ *
+ * @param {string} type
+ * The type of storage corresponding to the after-reset columns in the
+ * table.
+ * @param {string} host
+ * The host name corresponding to the table after reset.
+ *
+ * @param {string} [subType]
+ * The sub type under the given type.
+ */
+ resetColumns: function* (type, host, subtype) {
+ this.table.host = host;
+ this.table.datatype = type;
+
+ let uniqueKey = null;
+ let columns = {};
+ let editableFields = [];
+ let fields = yield this.getCurrentActor().getFields(subtype);
+
+ fields.forEach(f => {
+ if (!uniqueKey) {
+ this.table.uniqueId = uniqueKey = f.name;
+ }
+
+ if (f.editable) {
+ editableFields.push(f.name);
+ }
+
+ columns[f.name] = f.name;
+ let columnName;
+ try {
+ columnName = L10N.getStr("table.headers." + type + "." + f.name);
+ } catch (e) {
+ columnName = COOKIE_KEY_MAP[f.name];
+ }
+
+ if (!columnName) {
+ console.error("Unable to localize table header type:" + type + " key:" + f.name);
+ } else {
+ columns[f.name] = columnName;
+ }
+ });
+
+ this.table.setColumns(columns, null, HIDDEN_COLUMNS);
+ this.hideSidebar();
+
+ yield this.makeFieldsEditable(editableFields);
+ },
+
+ /**
+ * Populates or updates the rows in the storage table.
+ *
+ * @param {array[object]} data
+ * Array of objects to be populated in the storage table
+ * @param {Constant} reason
+ * See REASON constant at top of file.
+ */
+ populateTable: function (data, reason) {
+ for (let item of data) {
+ if (item.value) {
+ item.valueActor = item.value;
+ item.value = item.value.initial || "";
+ }
+ if (item.expires != null) {
+ item.expires = item.expires
+ ? new Date(item.expires).toUTCString()
+ : L10N.getStr("label.expires.session");
+ }
+ if (item.creationTime != null) {
+ item.creationTime = new Date(item.creationTime).toUTCString();
+ }
+ if (item.lastAccessed != null) {
+ item.lastAccessed = new Date(item.lastAccessed).toUTCString();
+ }
+
+ switch (reason) {
+ case REASON.POPULATE:
+ // Update without flashing the row.
+ this.table.push(item, true);
+ break;
+ case REASON.NEW_ROW:
+ case REASON.NEXT_50_ITEMS:
+ // Update and flash the row.
+ this.table.push(item, false);
+ break;
+ case REASON.UPDATE:
+ this.table.update(item);
+ if (item == this.table.selectedRow && !this.sidebar.hidden) {
+ this.displayObjectSidebar();
+ }
+ break;
+ }
+
+ this.shouldLoadMoreItems = true;
+ }
+ },
+
+ /**
+ * Handles keypress event on the body table to close the sidebar when open
+ *
+ * @param {DOMEvent} event
+ * The event passed by the keypress event.
+ */
+ handleKeypress: function (event) {
+ if (event.keyCode == KeyCodes.DOM_VK_ESCAPE && !this.sidebar.hidden) {
+ // Stop Propagation to prevent opening up of split console
+ this.hideSidebar();
+ event.stopPropagation();
+ event.preventDefault();
+ }
+ },
+
+ /**
+ * Handles filtering the table
+ */
+ filterItems() {
+ let value = this.searchBox.value;
+ this.table.filterItems(value, ["valueActor"]);
+ this._panelDoc.documentElement.classList.toggle("filtering", !!value);
+ },
+
+ /**
+ * Handles endless scrolling for the table
+ */
+ handleScrollEnd: function () {
+ if (!this.shouldLoadMoreItems) {
+ return;
+ }
+ this.shouldLoadMoreItems = false;
+ this.itemOffset += 50;
+
+ let item = this.tree.selectedItem;
+ let [type, host] = item;
+ let names = null;
+ if (item.length > 2) {
+ names = [JSON.stringify(item.slice(2))];
+ }
+ this.fetchStorageObjects(type, host, names, REASON.NEXT_50_ITEMS);
+ },
+
+ /**
+ * Fires before a cell context menu with the "Delete" action is shown.
+ * If the currently selected storage object doesn't support removing items, prevent
+ * showing the menu.
+ */
+ onTablePopupShowing: function (event) {
+ let selectedItem = this.tree.selectedItem;
+ let type = selectedItem[0];
+ let actor = this.getCurrentActor();
+
+ // IndexedDB only supports removing items from object stores (level 4 of the tree)
+ if (!actor.removeItem || (type === "indexedDB" && selectedItem.length !== 4)) {
+ event.preventDefault();
+ return;
+ }
+
+ let rowId = this.table.contextMenuRowId;
+ let data = this.table.items.get(rowId);
+ let name = addEllipsis(data[this.table.uniqueId]);
+
+ this._tablePopupDelete.setAttribute("label",
+ L10N.getFormatStr("storage.popupMenu.deleteLabel", name));
+
+ if (type === "cookies") {
+ let host = addEllipsis(data.host);
+
+ this._tablePopupDeleteAllFrom.hidden = false;
+ this._tablePopupDeleteAllFrom.setAttribute("label",
+ L10N.getFormatStr("storage.popupMenu.deleteAllFromLabel", host));
+ } else {
+ this._tablePopupDeleteAllFrom.hidden = true;
+ }
+ },
+
+ onTreePopupShowing: function (event) {
+ let showMenu = false;
+ let selectedItem = this.tree.selectedItem;
+
+ if (selectedItem) {
+ let type = selectedItem[0];
+ let actor = this.storageTypes[type];
+
+ // The delete all (aka clear) action is displayed for IndexedDB object stores
+ // (level 4 of tree), for Cache objects (level 3) and for the whole host (level 2)
+ // for other storage types (cookies, localStorage, ...).
+ let showDeleteAll = false;
+ if (actor.removeAll) {
+ let level;
+ if (type == "indexedDB") {
+ level = 4;
+ } else if (type == "Cache") {
+ level = 3;
+ } else {
+ level = 2;
+ }
+
+ if (selectedItem.length == level) {
+ showDeleteAll = true;
+ }
+ }
+
+ this._treePopupDeleteAll.hidden = !showDeleteAll;
+
+ // The delete action is displayed for:
+ // - IndexedDB databases (level 3 of the tree)
+ // - Cache objects (level 3 of the tree)
+ let showDelete = (type == "indexedDB" || type == "Cache") &&
+ selectedItem.length == 3;
+ this._treePopupDelete.hidden = !showDelete;
+ if (showDelete) {
+ let itemName = addEllipsis(selectedItem[selectedItem.length - 1]);
+ this._treePopupDelete.setAttribute("label",
+ L10N.getFormatStr("storage.popupMenu.deleteLabel", itemName));
+ }
+
+ showMenu = showDeleteAll || showDelete;
+ }
+
+ if (!showMenu) {
+ event.preventDefault();
+ }
+ },
+
+ /**
+ * Handles removing an item from the storage
+ */
+ onRemoveItem: function () {
+ let [, host, ...path] = this.tree.selectedItem;
+ let actor = this.getCurrentActor();
+ let rowId = this.table.contextMenuRowId;
+ let data = this.table.items.get(rowId);
+ let name = data[this.table.uniqueId];
+ if (path.length > 0) {
+ name = JSON.stringify([...path, name]);
+ }
+ actor.removeItem(host, name);
+ },
+
+ /**
+ * Handles removing all items from the storage
+ */
+ onRemoveAll: function () {
+ // Cannot use this.currentActor() if the handler is called from the
+ // tree context menu: it returns correct value only after the table
+ // data from server are successfully fetched (and that's async).
+ let [type, host, ...path] = this.tree.selectedItem;
+ let actor = this.storageTypes[type];
+ let name = path.length > 0 ? JSON.stringify(path) : undefined;
+ actor.removeAll(host, name);
+ },
+
+ /**
+ * Handles removing all cookies with exactly the same domain as the
+ * cookie in the selected row.
+ */
+ onRemoveAllFrom: function () {
+ let [, host] = this.tree.selectedItem;
+ let actor = this.getCurrentActor();
+ let rowId = this.table.contextMenuRowId;
+ let data = this.table.items.get(rowId);
+
+ actor.removeAll(host, data.host);
+ },
+
+ onRemoveTreeItem: function () {
+ let [type, host, ...path] = this.tree.selectedItem;
+
+ if (type == "indexedDB" && path.length == 1) {
+ this.removeDatabase(host, path[0]);
+ } else if (type == "Cache" && path.length == 1) {
+ this.removeCache(host, path[0]);
+ }
+ },
+
+ removeDatabase: function (host, dbName) {
+ let actor = this.storageTypes.indexedDB;
+
+ actor.removeDatabase(host, dbName).then(result => {
+ if (result.blocked) {
+ let notificationBox = this._toolbox.getNotificationBox();
+ notificationBox.appendNotification(
+ L10N.getFormatStr("storage.idb.deleteBlocked", dbName),
+ "storage-idb-delete-blocked",
+ null,
+ notificationBox.PRIORITY_WARNING_LOW);
+ }
+ }).catch(error => {
+ let notificationBox = this._toolbox.getNotificationBox();
+ notificationBox.appendNotification(
+ L10N.getFormatStr("storage.idb.deleteError", dbName),
+ "storage-idb-delete-error",
+ null,
+ notificationBox.PRIORITY_CRITICAL_LOW);
+ });
+ },
+
+ removeCache: function (host, cacheName) {
+ let actor = this.storageTypes.Cache;
+
+ actor.removeItem(host, JSON.stringify([ cacheName ]));
+ },
+};
diff --git a/devtools/client/styleeditor/StyleEditorUI.jsm b/devtools/client/styleeditor/StyleEditorUI.jsm
new file mode 100644
index 000000000..cdb267669
--- /dev/null
+++ b/devtools/client/styleeditor/StyleEditorUI.jsm
@@ -0,0 +1,1029 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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";
+
+this.EXPORTED_SYMBOLS = ["StyleEditorUI"];
+
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const Services = require("Services");
+const {NetUtil} = require("resource://gre/modules/NetUtil.jsm");
+const {OS} = require("resource://gre/modules/osfile.jsm");
+const {Task} = require("devtools/shared/task");
+const EventEmitter = require("devtools/shared/event-emitter");
+const {gDevTools} = require("devtools/client/framework/devtools");
+const {
+ getString,
+ text,
+ wire,
+ showFilePicker,
+} = require("resource://devtools/client/styleeditor/StyleEditorUtil.jsm");
+const {SplitView} = require("resource://devtools/client/shared/SplitView.jsm");
+const {StyleSheetEditor} = require("resource://devtools/client/styleeditor/StyleSheetEditor.jsm");
+const {PluralForm} = require("devtools/shared/plural-form");
+const {PrefObserver, PREF_ORIG_SOURCES} = require("devtools/client/styleeditor/utils");
+const csscoverage = require("devtools/shared/fronts/csscoverage");
+const {console} = require("resource://gre/modules/Console.jsm");
+const promise = require("promise");
+const defer = require("devtools/shared/defer");
+const {ResponsiveUIManager} =
+ require("resource://devtools/client/responsivedesign/responsivedesign.jsm");
+const {KeyCodes} = require("devtools/client/shared/keycodes");
+
+const LOAD_ERROR = "error-load";
+const STYLE_EDITOR_TEMPLATE = "stylesheet";
+const SELECTOR_HIGHLIGHTER_TYPE = "SelectorHighlighter";
+const PREF_MEDIA_SIDEBAR = "devtools.styleeditor.showMediaSidebar";
+const PREF_SIDEBAR_WIDTH = "devtools.styleeditor.mediaSidebarWidth";
+const PREF_NAV_WIDTH = "devtools.styleeditor.navSidebarWidth";
+
+/**
+ * StyleEditorUI is controls and builds the UI of the Style Editor, including
+ * maintaining a list of editors for each stylesheet on a debuggee.
+ *
+ * Emits events:
+ * 'editor-added': A new editor was added to the UI
+ * 'editor-selected': An editor was selected
+ * 'error': An error occured
+ *
+ * @param {StyleEditorFront} debuggee
+ * Client-side front for interacting with the page's stylesheets
+ * @param {Target} target
+ * Interface for the page we're debugging
+ * @param {Document} panelDoc
+ * Document of the toolbox panel to populate UI in.
+ * @param {CssProperties} A css properties database.
+ */
+function StyleEditorUI(debuggee, target, panelDoc, cssProperties) {
+ EventEmitter.decorate(this);
+
+ this._debuggee = debuggee;
+ this._target = target;
+ this._panelDoc = panelDoc;
+ this._cssProperties = cssProperties;
+ this._window = this._panelDoc.defaultView;
+ this._root = this._panelDoc.getElementById("style-editor-chrome");
+
+ this.editors = [];
+ this.selectedEditor = null;
+ this.savedLocations = {};
+
+ this._onOptionsPopupShowing = this._onOptionsPopupShowing.bind(this);
+ this._onOptionsPopupHiding = this._onOptionsPopupHiding.bind(this);
+ this._onStyleSheetCreated = this._onStyleSheetCreated.bind(this);
+ this._onNewDocument = this._onNewDocument.bind(this);
+ this._onMediaPrefChanged = this._onMediaPrefChanged.bind(this);
+ this._updateMediaList = this._updateMediaList.bind(this);
+ this._clear = this._clear.bind(this);
+ this._onError = this._onError.bind(this);
+ this._updateOpenLinkItem = this._updateOpenLinkItem.bind(this);
+ this._openLinkNewTab = this._openLinkNewTab.bind(this);
+
+ this._prefObserver = new PrefObserver("devtools.styleeditor.");
+ this._prefObserver.on(PREF_ORIG_SOURCES, this._onNewDocument);
+ this._prefObserver.on(PREF_MEDIA_SIDEBAR, this._onMediaPrefChanged);
+}
+this.StyleEditorUI = StyleEditorUI;
+
+StyleEditorUI.prototype = {
+ /**
+ * Get whether any of the editors have unsaved changes.
+ *
+ * @return boolean
+ */
+ get isDirty() {
+ if (this._markedDirty === true) {
+ return true;
+ }
+ return this.editors.some((editor) => {
+ return editor.sourceEditor && !editor.sourceEditor.isClean();
+ });
+ },
+
+ /*
+ * Mark the style editor as having or not having unsaved changes.
+ */
+ set isDirty(value) {
+ this._markedDirty = value;
+ },
+
+ /*
+ * Index of selected stylesheet in document.styleSheets
+ */
+ get selectedStyleSheetIndex() {
+ return this.selectedEditor ?
+ this.selectedEditor.styleSheet.styleSheetIndex : -1;
+ },
+
+ /**
+ * Initiates the style editor ui creation, the inspector front to get
+ * reference to the walker and the selector highlighter if available
+ */
+ initialize: Task.async(function* () {
+ yield this.initializeHighlighter();
+
+ this.createUI();
+
+ let styleSheets = yield this._debuggee.getStyleSheets();
+ yield this._resetStyleSheetList(styleSheets);
+
+ this._target.on("will-navigate", this._clear);
+ this._target.on("navigate", this._onNewDocument);
+ }),
+
+ initializeHighlighter: Task.async(function* () {
+ let toolbox = gDevTools.getToolbox(this._target);
+ yield toolbox.initInspector();
+ this._walker = toolbox.walker;
+
+ let hUtils = toolbox.highlighterUtils;
+ if (hUtils.supportsCustomHighlighters()) {
+ try {
+ this._highlighter =
+ yield hUtils.getHighlighterByType(SELECTOR_HIGHLIGHTER_TYPE);
+ } catch (e) {
+ // The selectorHighlighter can't always be instantiated, for example
+ // it doesn't work with XUL windows (until bug 1094959 gets fixed);
+ // or the selectorHighlighter doesn't exist on the backend.
+ console.warn("The selectorHighlighter couldn't be instantiated, " +
+ "elements matching hovered selectors will not be highlighted");
+ }
+ }
+ }),
+
+ /**
+ * Build the initial UI and wire buttons with event handlers.
+ */
+ createUI: function () {
+ let viewRoot = this._root.parentNode.querySelector(".splitview-root");
+
+ this._view = new SplitView(viewRoot);
+
+ wire(this._view.rootElement, ".style-editor-newButton", () =>{
+ this._debuggee.addStyleSheet(null).then(this._onStyleSheetCreated);
+ });
+
+ wire(this._view.rootElement, ".style-editor-importButton", ()=> {
+ this._importFromFile(this._mockImportFile || null, this._window);
+ });
+
+ this._optionsButton = this._panelDoc.getElementById("style-editor-options");
+ this._panelDoc.addEventListener("contextmenu", () => {
+ this._contextMenuStyleSheet = null;
+ }, true);
+
+ this._contextMenu = this._panelDoc.getElementById("sidebar-context");
+ this._contextMenu.addEventListener("popupshowing",
+ this._updateOpenLinkItem);
+
+ this._optionsMenu =
+ this._panelDoc.getElementById("style-editor-options-popup");
+ this._optionsMenu.addEventListener("popupshowing",
+ this._onOptionsPopupShowing);
+ this._optionsMenu.addEventListener("popuphiding",
+ this._onOptionsPopupHiding);
+
+ this._sourcesItem = this._panelDoc.getElementById("options-origsources");
+ this._sourcesItem.addEventListener("command",
+ this._toggleOrigSources);
+
+ this._mediaItem = this._panelDoc.getElementById("options-show-media");
+ this._mediaItem.addEventListener("command",
+ this._toggleMediaSidebar);
+
+ this._openLinkNewTabItem =
+ this._panelDoc.getElementById("context-openlinknewtab");
+ this._openLinkNewTabItem.addEventListener("command",
+ this._openLinkNewTab);
+
+ let nav = this._panelDoc.querySelector(".splitview-controller");
+ nav.setAttribute("width", Services.prefs.getIntPref(PREF_NAV_WIDTH));
+ },
+
+ /**
+ * Listener handling the 'gear menu' popup showing event.
+ * Update options menu items to reflect current preference settings.
+ */
+ _onOptionsPopupShowing: function () {
+ this._optionsButton.setAttribute("open", "true");
+ this._sourcesItem.setAttribute("checked",
+ Services.prefs.getBoolPref(PREF_ORIG_SOURCES));
+ this._mediaItem.setAttribute("checked",
+ Services.prefs.getBoolPref(PREF_MEDIA_SIDEBAR));
+ },
+
+ /**
+ * Listener handling the 'gear menu' popup hiding event.
+ */
+ _onOptionsPopupHiding: function () {
+ this._optionsButton.removeAttribute("open");
+ },
+
+ /**
+ * Refresh editors to reflect the stylesheets in the document.
+ *
+ * @param {string} event
+ * Event name
+ * @param {StyleSheet} styleSheet
+ * StyleSheet object for new sheet
+ */
+ _onNewDocument: function () {
+ this._debuggee.getStyleSheets().then((styleSheets) => {
+ return this._resetStyleSheetList(styleSheets);
+ }).then(null, e => console.error(e));
+ },
+
+ /**
+ * Add editors for all the given stylesheets to the UI.
+ *
+ * @param {array} styleSheets
+ * Array of StyleSheetFront
+ */
+ _resetStyleSheetList: Task.async(function* (styleSheets) {
+ this._clear();
+
+ for (let sheet of styleSheets) {
+ try {
+ yield this._addStyleSheet(sheet);
+ } catch (e) {
+ this.emit("error", { key: LOAD_ERROR });
+ }
+ }
+
+ this._root.classList.remove("loading");
+
+ this.emit("stylesheets-reset");
+ }),
+
+ /**
+ * Remove all editors and add loading indicator.
+ */
+ _clear: function () {
+ // remember selected sheet and line number for next load
+ if (this.selectedEditor && this.selectedEditor.sourceEditor) {
+ let href = this.selectedEditor.styleSheet.href;
+ let {line, ch} = this.selectedEditor.sourceEditor.getCursor();
+
+ this._styleSheetToSelect = {
+ stylesheet: href,
+ line: line,
+ col: ch
+ };
+ }
+
+ // remember saved file locations
+ for (let editor of this.editors) {
+ if (editor.savedFile) {
+ let identifier = this.getStyleSheetIdentifier(editor.styleSheet);
+ this.savedLocations[identifier] = editor.savedFile;
+ }
+ }
+
+ this._clearStyleSheetEditors();
+ this._view.removeAll();
+
+ this.selectedEditor = null;
+
+ this._root.classList.add("loading");
+ },
+
+ /**
+ * Add an editor for this stylesheet. Add editors for its original sources
+ * instead (e.g. Sass sources), if applicable.
+ *
+ * @param {StyleSheetFront} styleSheet
+ * Style sheet to add to style editor
+ */
+ _addStyleSheet: Task.async(function* (styleSheet) {
+ let editor = yield this._addStyleSheetEditor(styleSheet);
+
+ if (!Services.prefs.getBoolPref(PREF_ORIG_SOURCES)) {
+ return;
+ }
+
+ let sources = yield styleSheet.getOriginalSources();
+ if (sources && sources.length) {
+ let parentEditorName = editor.friendlyName;
+ this._removeStyleSheetEditor(editor);
+
+ for (let source of sources) {
+ // set so the first sheet will be selected, even if it's a source
+ source.styleSheetIndex = styleSheet.styleSheetIndex;
+ source.relatedStyleSheet = styleSheet;
+ source.relatedEditorName = parentEditorName;
+ yield this._addStyleSheetEditor(source);
+ }
+ }
+ }),
+
+ /**
+ * Add a new editor to the UI for a source.
+ *
+ * @param {StyleSheet} styleSheet
+ * Object representing stylesheet
+ * @param {nsIfile} file
+ * Optional file object that sheet was imported from
+ * @param {Boolean} isNew
+ * Optional if stylesheet is a new sheet created by user
+ * @return {Promise} that is resolved with the created StyleSheetEditor when
+ * the editor is fully initialized or rejected on error.
+ */
+ _addStyleSheetEditor: Task.async(function* (styleSheet, file, isNew) {
+ // recall location of saved file for this sheet after page reload
+ let identifier = this.getStyleSheetIdentifier(styleSheet);
+ let savedFile = this.savedLocations[identifier];
+ if (savedFile && !file) {
+ file = savedFile;
+ }
+
+ let editor = new StyleSheetEditor(styleSheet, this._window, file, isNew,
+ this._walker, this._highlighter);
+
+ editor.on("property-change", this._summaryChange.bind(this, editor));
+ editor.on("media-rules-changed", this._updateMediaList.bind(this, editor));
+ editor.on("linked-css-file", this._summaryChange.bind(this, editor));
+ editor.on("linked-css-file-error", this._summaryChange.bind(this, editor));
+ editor.on("error", this._onError);
+
+ this.editors.push(editor);
+
+ yield editor.fetchSource();
+ this._sourceLoaded(editor);
+
+ return editor;
+ }),
+
+ /**
+ * Import a style sheet from file and asynchronously create a
+ * new stylesheet on the debuggee for it.
+ *
+ * @param {mixed} file
+ * Optional nsIFile or filename string.
+ * If not set a file picker will be shown.
+ * @param {nsIWindow} parentWindow
+ * Optional parent window for the file picker.
+ */
+ _importFromFile: function (file, parentWindow) {
+ let onFileSelected = (selectedFile) => {
+ if (!selectedFile) {
+ // nothing selected
+ return;
+ }
+ NetUtil.asyncFetch({
+ uri: NetUtil.newURI(selectedFile),
+ loadingNode: this._window.document,
+ securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_INHERITS,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER
+ }, (stream, status) => {
+ if (!Components.isSuccessCode(status)) {
+ this.emit("error", { key: LOAD_ERROR });
+ return;
+ }
+ let source =
+ NetUtil.readInputStreamToString(stream, stream.available());
+ stream.close();
+
+ this._debuggee.addStyleSheet(source).then((styleSheet) => {
+ this._onStyleSheetCreated(styleSheet, selectedFile);
+ });
+ });
+ };
+
+ showFilePicker(file, false, parentWindow, onFileSelected);
+ },
+
+ /**
+ * When a new or imported stylesheet has been added to the document.
+ * Add an editor for it.
+ */
+ _onStyleSheetCreated: function (styleSheet, file) {
+ this._addStyleSheetEditor(styleSheet, file, true);
+ },
+
+ /**
+ * Forward any error from a stylesheet.
+ *
+ * @param {string} event
+ * Event name
+ * @param {data} data
+ * The event data
+ */
+ _onError: function (event, data) {
+ this.emit("error", data);
+ },
+
+ /**
+ * Toggle the original sources pref.
+ */
+ _toggleOrigSources: function () {
+ let isEnabled = Services.prefs.getBoolPref(PREF_ORIG_SOURCES);
+ Services.prefs.setBoolPref(PREF_ORIG_SOURCES, !isEnabled);
+ },
+
+ /**
+ * Toggle the pref for showing a @media rules sidebar in each editor.
+ */
+ _toggleMediaSidebar: function () {
+ let isEnabled = Services.prefs.getBoolPref(PREF_MEDIA_SIDEBAR);
+ Services.prefs.setBoolPref(PREF_MEDIA_SIDEBAR, !isEnabled);
+ },
+
+ /**
+ * Toggle the @media sidebar in each editor depending on the setting.
+ */
+ _onMediaPrefChanged: function () {
+ this.editors.forEach(this._updateMediaList);
+ },
+
+ /**
+ * This method handles the following cases related to the context
+ * menu item "_openLinkNewTabItem":
+ *
+ * 1) There was a stylesheet clicked on and it is external: show and
+ * enable the context menu item
+ * 2) There was a stylesheet clicked on and it is inline: show and
+ * disable the context menu item
+ * 3) There was no stylesheet clicked on (the right click happened
+ * below the list): hide the context menu
+ */
+ _updateOpenLinkItem: function () {
+ this._openLinkNewTabItem.setAttribute("hidden",
+ !this._contextMenuStyleSheet);
+ if (this._contextMenuStyleSheet) {
+ this._openLinkNewTabItem.setAttribute("disabled",
+ !this._contextMenuStyleSheet.href);
+ }
+ },
+
+ /**
+ * Open a particular stylesheet in a new tab.
+ */
+ _openLinkNewTab: function () {
+ if (this._contextMenuStyleSheet) {
+ this._window.openUILinkIn(this._contextMenuStyleSheet.href, "tab");
+ }
+ },
+
+ /**
+ * Remove a particular stylesheet editor from the UI
+ *
+ * @param {StyleSheetEditor} editor
+ * The editor to remove.
+ */
+ _removeStyleSheetEditor: function (editor) {
+ if (editor.summary) {
+ this._view.removeItem(editor.summary);
+ } else {
+ let self = this;
+ this.on("editor-added", function onAdd(event, added) {
+ if (editor == added) {
+ self.off("editor-added", onAdd);
+ self._view.removeItem(editor.summary);
+ }
+ });
+ }
+
+ editor.destroy();
+ this.editors.splice(this.editors.indexOf(editor), 1);
+ },
+
+ /**
+ * Clear all the editors from the UI.
+ */
+ _clearStyleSheetEditors: function () {
+ for (let editor of this.editors) {
+ editor.destroy();
+ }
+ this.editors = [];
+ },
+
+ /**
+ * Called when a StyleSheetEditor's source has been fetched. Create a
+ * summary UI for the editor.
+ *
+ * @param {StyleSheetEditor} editor
+ * Editor to create UI for.
+ */
+ _sourceLoaded: function (editor) {
+ let ordinal = editor.styleSheet.styleSheetIndex;
+ ordinal = ordinal == -1 ? Number.MAX_SAFE_INTEGER : ordinal;
+ // add new sidebar item and editor to the UI
+ this._view.appendTemplatedItem(STYLE_EDITOR_TEMPLATE, {
+ data: {
+ editor: editor
+ },
+ disableAnimations: this._alwaysDisableAnimations,
+ ordinal: ordinal,
+ onCreate: function (summary, details, data) {
+ let createdEditor = data.editor;
+ createdEditor.summary = summary;
+ createdEditor.details = details;
+
+ wire(summary, ".stylesheet-enabled", function onToggleDisabled(event) {
+ event.stopPropagation();
+ event.target.blur();
+
+ createdEditor.toggleDisabled();
+ });
+
+ wire(summary, ".stylesheet-name", {
+ events: {
+ "keypress": (event) => {
+ if (event.keyCode == KeyCodes.DOM_VK_RETURN) {
+ this._view.activeSummary = summary;
+ }
+ }
+ }
+ });
+
+ wire(summary, ".stylesheet-saveButton", function onSaveButton(event) {
+ event.stopPropagation();
+ event.target.blur();
+
+ createdEditor.saveToFile(createdEditor.savedFile);
+ });
+
+ this._updateSummaryForEditor(createdEditor, summary);
+
+ summary.addEventListener("contextmenu", () => {
+ this._contextMenuStyleSheet = createdEditor.styleSheet;
+ }, false);
+
+ summary.addEventListener("focus", function onSummaryFocus(event) {
+ if (event.target == summary) {
+ // autofocus the stylesheet name
+ summary.querySelector(".stylesheet-name").focus();
+ }
+ }, false);
+
+ let sidebar = details.querySelector(".stylesheet-sidebar");
+ sidebar.setAttribute("width",
+ Services.prefs.getIntPref(PREF_SIDEBAR_WIDTH));
+
+ let splitter = details.querySelector(".devtools-side-splitter");
+ splitter.addEventListener("mousemove", () => {
+ let sidebarWidth = sidebar.getAttribute("width");
+ Services.prefs.setIntPref(PREF_SIDEBAR_WIDTH, sidebarWidth);
+
+ // update all @media sidebars for consistency
+ let sidebars =
+ [...this._panelDoc.querySelectorAll(".stylesheet-sidebar")];
+ for (let mediaSidebar of sidebars) {
+ mediaSidebar.setAttribute("width", sidebarWidth);
+ }
+ });
+
+ // autofocus if it's a new user-created stylesheet
+ if (createdEditor.isNew) {
+ this._selectEditor(createdEditor);
+ }
+
+ if (this._isEditorToSelect(createdEditor)) {
+ this.switchToSelectedSheet();
+ }
+
+ // If this is the first stylesheet and there is no pending request to
+ // select a particular style sheet, select this sheet.
+ if (!this.selectedEditor && !this._styleSheetBoundToSelect
+ && createdEditor.styleSheet.styleSheetIndex == 0) {
+ this._selectEditor(createdEditor);
+ }
+ this.emit("editor-added", createdEditor);
+ }.bind(this),
+
+ onShow: function (summary, details, data) {
+ let showEditor = data.editor;
+ this.selectedEditor = showEditor;
+
+ Task.spawn(function* () {
+ if (!showEditor.sourceEditor) {
+ // only initialize source editor when we switch to this view
+ let inputElement =
+ details.querySelector(".stylesheet-editor-input");
+ yield showEditor.load(inputElement, this._cssProperties);
+ }
+
+ showEditor.onShow();
+
+ this.emit("editor-selected", showEditor);
+
+ // Is there any CSS coverage markup to include?
+ let usage = yield csscoverage.getUsage(this._target);
+ if (usage == null) {
+ return;
+ }
+
+ let sheet = showEditor.styleSheet;
+ let {reports} = yield usage.createEditorReportForSheet(sheet);
+
+ showEditor.removeAllUnusedRegions();
+
+ if (reports.length > 0) {
+ // Only apply if this file isn't compressed. We detect a
+ // compressed file if there are more rules than lines.
+ let editorText = showEditor.sourceEditor.getText();
+ let lineCount = editorText.split("\n").length;
+ let ruleCount = showEditor.styleSheet.ruleCount;
+ if (lineCount >= ruleCount) {
+ showEditor.addUnusedRegions(reports);
+ } else {
+ this.emit("error", { key: "error-compressed", level: "info" });
+ }
+ }
+ }.bind(this)).then(null, e => console.error(e));
+ }.bind(this)
+ });
+ },
+
+ /**
+ * Switch to the editor that has been marked to be selected.
+ *
+ * @return {Promise}
+ * Promise that will resolve when the editor is selected.
+ */
+ switchToSelectedSheet: function () {
+ let toSelect = this._styleSheetToSelect;
+
+ for (let editor of this.editors) {
+ if (this._isEditorToSelect(editor)) {
+ // The _styleSheetBoundToSelect will always hold the latest pending
+ // requested style sheet (with line and column) which is not yet
+ // selected by the source editor. Only after we select that particular
+ // editor and go the required line and column, it will become null.
+ this._styleSheetBoundToSelect = this._styleSheetToSelect;
+ this._styleSheetToSelect = null;
+ return this._selectEditor(editor, toSelect.line, toSelect.col);
+ }
+ }
+
+ return promise.resolve();
+ },
+
+ /**
+ * Returns whether a given editor is the current editor to be selected. Tests
+ * based on href or underlying stylesheet.
+ *
+ * @param {StyleSheetEditor} editor
+ * The editor to test.
+ */
+ _isEditorToSelect: function (editor) {
+ let toSelect = this._styleSheetToSelect;
+ if (!toSelect) {
+ return false;
+ }
+ let isHref = toSelect.stylesheet === null ||
+ typeof toSelect.stylesheet == "string";
+
+ return (isHref && editor.styleSheet.href == toSelect.stylesheet) ||
+ (toSelect.stylesheet == editor.styleSheet);
+ },
+
+ /**
+ * Select an editor in the UI.
+ *
+ * @param {StyleSheetEditor} editor
+ * Editor to switch to.
+ * @param {number} line
+ * Line number to jump to
+ * @param {number} col
+ * Column number to jump to
+ * @return {Promise}
+ * Promise that will resolve when the editor is selected and ready
+ * to be used.
+ */
+ _selectEditor: function (editor, line, col) {
+ line = line || 0;
+ col = col || 0;
+
+ let editorPromise = editor.getSourceEditor().then(() => {
+ editor.sourceEditor.setCursor({line: line, ch: col});
+ this._styleSheetBoundToSelect = null;
+ });
+
+ let summaryPromise = this.getEditorSummary(editor).then((summary) => {
+ this._view.activeSummary = summary;
+ });
+
+ return promise.all([editorPromise, summaryPromise]);
+ },
+
+ getEditorSummary: function (editor) {
+ if (editor.summary) {
+ return promise.resolve(editor.summary);
+ }
+
+ let deferred = defer();
+ let self = this;
+
+ this.on("editor-added", function onAdd(e, selected) {
+ if (selected == editor) {
+ self.off("editor-added", onAdd);
+ deferred.resolve(editor.summary);
+ }
+ });
+
+ return deferred.promise;
+ },
+
+ getEditorDetails: function (editor) {
+ if (editor.details) {
+ return promise.resolve(editor.details);
+ }
+
+ let deferred = defer();
+ let self = this;
+
+ this.on("editor-added", function onAdd(e, selected) {
+ if (selected == editor) {
+ self.off("editor-added", onAdd);
+ deferred.resolve(editor.details);
+ }
+ });
+
+ return deferred.promise;
+ },
+
+ /**
+ * Returns an identifier for the given style sheet.
+ *
+ * @param {StyleSheet} styleSheet
+ * The style sheet to be identified.
+ */
+ getStyleSheetIdentifier: function (styleSheet) {
+ // Identify inline style sheets by their host page URI and index
+ // at the page.
+ return styleSheet.href ? styleSheet.href :
+ "inline-" + styleSheet.styleSheetIndex + "-at-" + styleSheet.nodeHref;
+ },
+
+ /**
+ * selects a stylesheet and optionally moves the cursor to a selected line
+ *
+ * @param {StyleSheetFront} [stylesheet]
+ * Stylesheet to select or href of stylesheet to select
+ * @param {Number} [line]
+ * Line to which the caret should be moved (zero-indexed).
+ * @param {Number} [col]
+ * Column to which the caret should be moved (zero-indexed).
+ * @return {Promise}
+ * Promise that will resolve when the editor is selected and ready
+ * to be used.
+ */
+ selectStyleSheet: function (stylesheet, line, col) {
+ this._styleSheetToSelect = {
+ stylesheet: stylesheet,
+ line: line,
+ col: col,
+ };
+
+ /* Switch to the editor for this sheet, if it exists yet.
+ Otherwise each editor will be checked when it's created. */
+ return this.switchToSelectedSheet();
+ },
+
+ /**
+ * Handler for an editor's 'property-changed' event.
+ * Update the summary in the UI.
+ *
+ * @param {StyleSheetEditor} editor
+ * Editor for which a property has changed
+ */
+ _summaryChange: function (editor) {
+ this._updateSummaryForEditor(editor);
+ },
+
+ /**
+ * Update split view summary of given StyleEditor instance.
+ *
+ * @param {StyleSheetEditor} editor
+ * @param {DOMElement} summary
+ * Optional item's summary element to update. If none, item
+ * corresponding to passed editor is used.
+ */
+ _updateSummaryForEditor: function (editor, summary) {
+ summary = summary || editor.summary;
+ if (!summary) {
+ return;
+ }
+
+ let ruleCount = editor.styleSheet.ruleCount;
+ if (editor.styleSheet.relatedStyleSheet) {
+ ruleCount = editor.styleSheet.relatedStyleSheet.ruleCount;
+ }
+ if (ruleCount === undefined) {
+ ruleCount = "-";
+ }
+
+ let flags = [];
+ if (editor.styleSheet.disabled) {
+ flags.push("disabled");
+ }
+ if (editor.unsaved) {
+ flags.push("unsaved");
+ }
+ if (editor.linkedCSSFileError) {
+ flags.push("linked-file-error");
+ }
+ this._view.setItemClassName(summary, flags.join(" "));
+
+ let label = summary.querySelector(".stylesheet-name > label");
+ label.setAttribute("value", editor.friendlyName);
+ if (editor.styleSheet.href) {
+ label.setAttribute("tooltiptext", editor.styleSheet.href);
+ }
+
+ let linkedCSSSource = "";
+ if (editor.linkedCSSFile) {
+ linkedCSSSource = OS.Path.basename(editor.linkedCSSFile);
+ } else if (editor.styleSheet.relatedEditorName) {
+ linkedCSSSource = editor.styleSheet.relatedEditorName;
+ }
+ text(summary, ".stylesheet-linked-file", linkedCSSSource);
+ text(summary, ".stylesheet-title", editor.styleSheet.title || "");
+ text(summary, ".stylesheet-rule-count",
+ PluralForm.get(ruleCount,
+ getString("ruleCount.label")).replace("#1", ruleCount));
+ },
+
+ /**
+ * Update the @media rules sidebar for an editor. Hide if there are no rules
+ * Display a list of the @media rules in the editor's associated style sheet.
+ * Emits a 'media-list-changed' event after updating the UI.
+ *
+ * @param {StyleSheetEditor} editor
+ * Editor to update @media sidebar of
+ */
+ _updateMediaList: function (editor) {
+ Task.spawn(function* () {
+ let details = yield this.getEditorDetails(editor);
+ let list = details.querySelector(".stylesheet-media-list");
+
+ while (list.firstChild) {
+ list.removeChild(list.firstChild);
+ }
+
+ let rules = editor.mediaRules;
+ let showSidebar = Services.prefs.getBoolPref(PREF_MEDIA_SIDEBAR);
+ let sidebar = details.querySelector(".stylesheet-sidebar");
+
+ let inSource = false;
+
+ for (let rule of rules) {
+ let {line, column, parentStyleSheet} = rule;
+
+ let location = {
+ line: line,
+ column: column,
+ source: editor.styleSheet.href,
+ styleSheet: parentStyleSheet
+ };
+ if (editor.styleSheet.isOriginalSource) {
+ location = yield editor.cssSheet.getOriginalLocation(line, column);
+ }
+
+ // this @media rule is from a different original source
+ if (location.source != editor.styleSheet.href) {
+ continue;
+ }
+ inSource = true;
+
+ let div = this._panelDoc.createElement("div");
+ div.className = "media-rule-label";
+ div.addEventListener("click",
+ this._jumpToLocation.bind(this, location));
+
+ let cond = this._panelDoc.createElement("div");
+ cond.className = "media-rule-condition";
+ if (!rule.matches) {
+ cond.classList.add("media-condition-unmatched");
+ }
+ if (this._target.tab.tagName == "tab") {
+ this._setConditionContents(cond, rule.conditionText);
+ } else {
+ cond.textContent = rule.conditionText;
+ }
+ div.appendChild(cond);
+
+ let link = this._panelDoc.createElement("div");
+ link.className = "media-rule-line theme-link";
+ if (location.line != -1) {
+ link.textContent = ":" + location.line;
+ }
+ div.appendChild(link);
+
+ list.appendChild(div);
+ }
+
+ sidebar.hidden = !showSidebar || !inSource;
+
+ this.emit("media-list-changed", editor);
+ }.bind(this)).then(null, e => console.error(e));
+ },
+
+ /**
+ * Used to safely inject media query links
+ *
+ * @param {HTMLElement} element
+ * The element corresponding to the media sidebar condition
+ * @param {String} rawText
+ * The raw condition text to parse
+ */
+ _setConditionContents(element, rawText) {
+ const minMaxPattern = /(min\-|max\-)(width|height):\s\d+(px)/ig;
+
+ let match = minMaxPattern.exec(rawText);
+ let lastParsed = 0;
+ while (match && match.index != minMaxPattern.lastIndex) {
+ let matchEnd = match.index + match[0].length;
+ let node = this._panelDoc.createTextNode(
+ rawText.substring(lastParsed, match.index)
+ );
+ element.appendChild(node);
+
+ let link = this._panelDoc.createElement("a");
+ link.href = "#";
+ link.className = "media-responsive-mode-toggle";
+ link.textContent = rawText.substring(match.index, matchEnd);
+ link.addEventListener("click", this._onMediaConditionClick.bind(this));
+ element.appendChild(link);
+
+ match = minMaxPattern.exec(rawText);
+ lastParsed = matchEnd;
+ }
+
+ let node = this._panelDoc.createTextNode(
+ rawText.substring(lastParsed, rawText.length)
+ );
+ element.appendChild(node);
+ },
+
+ /**
+ * Called when a media condition is clicked
+ * If a responsive mode link is clicked, it will launch it.
+ *
+ * @param {object} e
+ * Event object
+ */
+ _onMediaConditionClick: function (e) {
+ let conditionText = e.target.textContent;
+ let isWidthCond = conditionText.toLowerCase().indexOf("width") > -1;
+ let mediaVal = parseInt(/\d+/.exec(conditionText), 10);
+
+ let options = isWidthCond ? {width: mediaVal} : {height: mediaVal};
+ this._launchResponsiveMode(options);
+ e.preventDefault();
+ e.stopPropagation();
+ },
+
+ /**
+ * Launches the responsive mode with a specific width or height
+ *
+ * @param {object} options
+ * Object with width or/and height properties.
+ */
+ _launchResponsiveMode: Task.async(function* (options = {}) {
+ let tab = this._target.tab;
+ let win = this._target.tab.ownerDocument.defaultView;
+
+ yield ResponsiveUIManager.openIfNeeded(win, tab);
+ ResponsiveUIManager.getResponsiveUIForTab(tab).setViewportSize(options);
+ }),
+
+ /**
+ * Jump cursor to the editor for a stylesheet and line number for a rule.
+ *
+ * @param {object} location
+ * Location object with 'line', 'column', and 'source' properties.
+ */
+ _jumpToLocation: function (location) {
+ let source = location.styleSheet || location.source;
+ this.selectStyleSheet(source, location.line - 1, location.column - 1);
+ },
+
+ destroy: function () {
+ if (this._highlighter) {
+ this._highlighter.finalize();
+ this._highlighter = null;
+ }
+
+ this._clearStyleSheetEditors();
+
+ let sidebar = this._panelDoc.querySelector(".splitview-controller");
+ let sidebarWidth = sidebar.getAttribute("width");
+ Services.prefs.setIntPref(PREF_NAV_WIDTH, sidebarWidth);
+
+ this._optionsMenu.removeEventListener("popupshowing",
+ this._onOptionsPopupShowing);
+ this._optionsMenu.removeEventListener("popuphiding",
+ this._onOptionsPopupHiding);
+
+ this._prefObserver.off(PREF_ORIG_SOURCES, this._onNewDocument);
+ this._prefObserver.off(PREF_MEDIA_SIDEBAR, this._onMediaPrefChanged);
+ this._prefObserver.destroy();
+ }
+};
diff --git a/devtools/client/styleeditor/StyleEditorUtil.jsm b/devtools/client/styleeditor/StyleEditorUtil.jsm
new file mode 100644
index 000000000..bd2f99164
--- /dev/null
+++ b/devtools/client/styleeditor/StyleEditorUtil.jsm
@@ -0,0 +1,234 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+/* All top-level definitions here are exports. */
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [
+ "getString",
+ "assert",
+ "log",
+ "text",
+ "wire",
+ "showFilePicker"
+];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+const PROPERTIES_URL = "chrome://devtools/locale/styleeditor.properties";
+
+const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const Services = require("Services");
+const console = require("resource://gre/modules/Console.jsm").console;
+const gStringBundle = Services.strings.createBundle(PROPERTIES_URL);
+
+/**
+ * Returns a localized string with the given key name from the string bundle.
+ *
+ * @param name
+ * @param ...rest
+ * Optional arguments to format in the string.
+ * @return string
+ */
+function getString(name) {
+ try {
+ if (arguments.length == 1) {
+ return gStringBundle.GetStringFromName(name);
+ }
+ let rest = Array.prototype.slice.call(arguments, 1);
+ return gStringBundle.formatStringFromName(name, rest, rest.length);
+ } catch (ex) {
+ console.error(ex);
+ throw new Error("L10N error. '" + name + "' is missing from " +
+ PROPERTIES_URL);
+ }
+}
+
+/**
+ * Assert an expression is true or throw if false.
+ *
+ * @param expression
+ * @param message
+ * Optional message.
+ * @return expression
+ */
+function assert(expression, message) {
+ if (!expression) {
+ let msg = message ? "ASSERTION FAILURE:" + message : "ASSERTION FAILURE";
+ log(msg);
+ throw new Error(msg);
+ }
+ return expression;
+}
+
+/**
+ * Retrieve or set the text content of an element.
+ *
+ * @param DOMElement root
+ * The element to use for querySelector.
+ * @param string selector
+ * Selector string for the element to get/set the text content.
+ * @param string textContent
+ * Optional text to set.
+ * @return string
+ * Text content of matching element or null if there were no element
+ * matching selector.
+ */
+function text(root, selector, textContent) {
+ let element = root.querySelector(selector);
+ if (!element) {
+ return null;
+ }
+
+ if (textContent === undefined) {
+ return element.textContent;
+ }
+ element.textContent = textContent;
+ return textContent;
+}
+
+/**
+ * Iterates _own_ properties of an object.
+ *
+ * @param object
+ * The object to iterate.
+ * @param function callback(aKey, aValue)
+ */
+function forEach(object, callback) {
+ for (let key in object) {
+ if (object.hasOwnProperty(key)) {
+ callback(key, object[key]);
+ }
+ }
+}
+
+/**
+ * Log a message to the console.
+ *
+ * @param ...rest
+ * One or multiple arguments to log.
+ * If multiple arguments are given, they will be joined by " "
+ * in the log.
+ */
+function log() {
+ console.logStringMessage(Array.prototype.slice.call(arguments).join(" "));
+}
+
+/**
+ * Wire up element(s) matching selector with attributes, event listeners, etc.
+ *
+ * @param DOMElement root
+ * The element to use for querySelectorAll.
+ * Can be null if selector is a DOMElement.
+ * @param string|DOMElement selectorOrElement
+ * Selector string or DOMElement for the element(s) to wire up.
+ * @param object descriptor
+ * An object describing how to wire matching selector,
+ * supported properties are "events" and "attributes" taking
+ * objects themselves.
+ * Each key of properties above represents the name of the event or
+ * attribute, with the value being a function used as an event handler or
+ * string to use as attribute value.
+ * If descriptor is a function, the argument is equivalent to :
+ * {events: {'click': descriptor}}
+ */
+function wire(root, selectorOrElement, descriptor) {
+ let matches;
+ if (typeof selectorOrElement == "string") {
+ // selector
+ matches = root.querySelectorAll(selectorOrElement);
+ if (!matches.length) {
+ return;
+ }
+ } else {
+ // element
+ matches = [selectorOrElement];
+ }
+
+ if (typeof descriptor == "function") {
+ descriptor = {events: {click: descriptor}};
+ }
+
+ for (let i = 0; i < matches.length; i++) {
+ let element = matches[i];
+ forEach(descriptor.events, function (name, handler) {
+ element.addEventListener(name, handler, false);
+ });
+ forEach(descriptor.attributes, element.setAttribute);
+ }
+}
+
+/**
+ * Show file picker and return the file user selected.
+ *
+ * @param mixed file
+ * Optional nsIFile or string representing the filename to auto-select.
+ * @param boolean toSave
+ * If true, the user is selecting a filename to save.
+ * @param nsIWindow parentWindow
+ * Optional parent window. If null the parent window of the file picker
+ * will be the window of the attached input element.
+ * @param callback
+ * The callback method, which will be called passing in the selected
+ * file or null if the user did not pick one.
+ * @param AString suggestedFilename
+ * The suggested filename when toSave is true.
+ */
+function showFilePicker(path, toSave, parentWindow, callback,
+ suggestedFilename) {
+ if (typeof path == "string") {
+ try {
+ if (Services.io.extractScheme(path) == "file") {
+ let uri = Services.io.newURI(path, null, null);
+ let file = uri.QueryInterface(Ci.nsIFileURL).file;
+ callback(file);
+ return;
+ }
+ } catch (ex) {
+ callback(null);
+ return;
+ }
+ try {
+ let file =
+ Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
+ file.initWithPath(path);
+ callback(file);
+ return;
+ } catch (ex) {
+ callback(null);
+ return;
+ }
+ }
+ if (path) {
+ // "path" is an nsIFile
+ callback(path);
+ return;
+ }
+
+ let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ let mode = toSave ? fp.modeSave : fp.modeOpen;
+ let key = toSave ? "saveStyleSheet" : "importStyleSheet";
+ let fpCallback = function (result) {
+ if (result == Ci.nsIFilePicker.returnCancel) {
+ callback(null);
+ } else {
+ callback(fp.file);
+ }
+ };
+
+ if (toSave && suggestedFilename) {
+ fp.defaultString = suggestedFilename;
+ }
+
+ fp.init(parentWindow, getString(key + ".title"), mode);
+ fp.appendFilter(getString(key + ".filter"), "*.css");
+ fp.appendFilters(fp.filterAll);
+ fp.open(fpCallback);
+ return;
+}
diff --git a/devtools/client/styleeditor/StyleSheetEditor.jsm b/devtools/client/styleeditor/StyleSheetEditor.jsm
new file mode 100644
index 000000000..980e51974
--- /dev/null
+++ b/devtools/client/styleeditor/StyleSheetEditor.jsm
@@ -0,0 +1,886 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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";
+
+this.EXPORTED_SYMBOLS = ["StyleSheetEditor"];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const Editor = require("devtools/client/sourceeditor/editor");
+const promise = require("promise");
+const defer = require("devtools/shared/defer");
+const {shortSource, prettifyCSS} = require("devtools/shared/inspector/css-logic");
+const {console} = require("resource://gre/modules/Console.jsm");
+const Services = require("Services");
+const EventEmitter = require("devtools/shared/event-emitter");
+const {Task} = require("devtools/shared/task");
+const {FileUtils} = require("resource://gre/modules/FileUtils.jsm");
+const {NetUtil} = require("resource://gre/modules/NetUtil.jsm");
+const {TextDecoder, OS} = Cu.import("resource://gre/modules/osfile.jsm", {});
+const {
+ getString,
+ showFilePicker,
+} = require("resource://devtools/client/styleeditor/StyleEditorUtil.jsm");
+
+const LOAD_ERROR = "error-load";
+const SAVE_ERROR = "error-save";
+
+// max update frequency in ms (avoid potential typing lag and/or flicker)
+// @see StyleEditor.updateStylesheet
+const UPDATE_STYLESHEET_DELAY = 500;
+
+// Pref which decides if CSS autocompletion is enabled in Style Editor or not.
+const AUTOCOMPLETION_PREF = "devtools.styleeditor.autocompletion-enabled";
+
+// Pref which decides whether updates to the stylesheet use transitions
+const TRANSITION_PREF = "devtools.styleeditor.transitions";
+
+// How long to wait to update linked CSS file after original source was saved
+// to disk. Time in ms.
+const CHECK_LINKED_SHEET_DELAY = 500;
+
+// How many times to check for linked file changes
+const MAX_CHECK_COUNT = 10;
+
+// The classname used to show a line that is not used
+const UNUSED_CLASS = "cm-unused-line";
+
+// How much time should the mouse be still before the selector at that position
+// gets highlighted?
+const SELECTOR_HIGHLIGHT_TIMEOUT = 500;
+
+/**
+ * StyleSheetEditor controls the editor linked to a particular StyleSheet
+ * object.
+ *
+ * Emits events:
+ * 'property-change': A property on the underlying stylesheet has changed
+ * 'source-editor-load': The source editor for this editor has been loaded
+ * 'error': An error has occured
+ *
+ * @param {StyleSheet|OriginalSource} styleSheet
+ * Stylesheet or original source to show
+ * @param {DOMWindow} win
+ * panel window for style editor
+ * @param {nsIFile} file
+ * Optional file that the sheet was imported from
+ * @param {boolean} isNew
+ * Optional whether the sheet was created by the user
+ * @param {Walker} walker
+ * Optional walker used for selectors autocompletion
+ * @param {CustomHighlighterFront} highlighter
+ * Optional highlighter front for the SelectorHighligher used to
+ * highlight selectors
+ */
+function StyleSheetEditor(styleSheet, win, file, isNew, walker, highlighter) {
+ EventEmitter.decorate(this);
+
+ this.styleSheet = styleSheet;
+ this._inputElement = null;
+ this.sourceEditor = null;
+ this._window = win;
+ this._isNew = isNew;
+ this.walker = walker;
+ this.highlighter = highlighter;
+
+ // True when we've called update() on the style sheet.
+ this._isUpdating = false;
+ // True when we've just set the editor text based on a style-applied
+ // event from the StyleSheetActor.
+ this._justSetText = false;
+
+ // state to use when inputElement attaches
+ this._state = {
+ text: "",
+ selection: {
+ start: {line: 0, ch: 0},
+ end: {line: 0, ch: 0}
+ }
+ };
+
+ this._styleSheetFilePath = null;
+ if (styleSheet.href &&
+ Services.io.extractScheme(this.styleSheet.href) == "file") {
+ this._styleSheetFilePath = this.styleSheet.href;
+ }
+
+ this._onPropertyChange = this._onPropertyChange.bind(this);
+ this._onError = this._onError.bind(this);
+ this._onMediaRuleMatchesChange = this._onMediaRuleMatchesChange.bind(this);
+ this._onMediaRulesChanged = this._onMediaRulesChanged.bind(this);
+ this._onStyleApplied = this._onStyleApplied.bind(this);
+ this.checkLinkedFileForChanges = this.checkLinkedFileForChanges.bind(this);
+ this.markLinkedFileBroken = this.markLinkedFileBroken.bind(this);
+ this.saveToFile = this.saveToFile.bind(this);
+ this.updateStyleSheet = this.updateStyleSheet.bind(this);
+ this._updateStyleSheet = this._updateStyleSheet.bind(this);
+ this._onMouseMove = this._onMouseMove.bind(this);
+
+ this._focusOnSourceEditorReady = false;
+ this.cssSheet.on("property-change", this._onPropertyChange);
+ this.styleSheet.on("error", this._onError);
+ this.mediaRules = [];
+ if (this.cssSheet.getMediaRules) {
+ this.cssSheet.getMediaRules().then(this._onMediaRulesChanged,
+ e => console.error(e));
+ }
+ this.cssSheet.on("media-rules-changed", this._onMediaRulesChanged);
+ this.cssSheet.on("style-applied", this._onStyleApplied);
+ this.savedFile = file;
+ this.linkCSSFile();
+}
+this.StyleSheetEditor = StyleSheetEditor;
+
+StyleSheetEditor.prototype = {
+ /**
+ * Whether there are unsaved changes in the editor
+ */
+ get unsaved() {
+ return this.sourceEditor && !this.sourceEditor.isClean();
+ },
+
+ /**
+ * Whether the editor is for a stylesheet created by the user
+ * through the style editor UI.
+ */
+ get isNew() {
+ return this._isNew;
+ },
+
+ /**
+ * The style sheet or the generated style sheet for this source if it's an
+ * original source.
+ */
+ get cssSheet() {
+ if (this.styleSheet.isOriginalSource) {
+ return this.styleSheet.relatedStyleSheet;
+ }
+ return this.styleSheet;
+ },
+
+ get savedFile() {
+ return this._savedFile;
+ },
+
+ set savedFile(name) {
+ this._savedFile = name;
+
+ this.linkCSSFile();
+ },
+
+ /**
+ * Get a user-friendly name for the style sheet.
+ *
+ * @return string
+ */
+ get friendlyName() {
+ if (this.savedFile) {
+ return this.savedFile.leafName;
+ }
+
+ if (this._isNew) {
+ let index = this.styleSheet.styleSheetIndex + 1;
+ return getString("newStyleSheet", index);
+ }
+
+ if (!this.styleSheet.href) {
+ let index = this.styleSheet.styleSheetIndex + 1;
+ return getString("inlineStyleSheet", index);
+ }
+
+ if (!this._friendlyName) {
+ let sheetURI = this.styleSheet.href;
+ this._friendlyName = shortSource({ href: sheetURI });
+ try {
+ this._friendlyName = decodeURI(this._friendlyName);
+ } catch (ex) {
+ // Ignore.
+ }
+ }
+ return this._friendlyName;
+ },
+
+ /**
+ * Check if transitions are enabled for style changes.
+ *
+ * @return Boolean
+ */
+ get transitionsEnabled() {
+ return Services.prefs.getBoolPref(TRANSITION_PREF);
+ },
+
+ /**
+ * If this is an original source, get the path of the CSS file it generated.
+ */
+ linkCSSFile: function () {
+ if (!this.styleSheet.isOriginalSource) {
+ return;
+ }
+
+ let relatedSheet = this.styleSheet.relatedStyleSheet;
+ if (!relatedSheet || !relatedSheet.href) {
+ return;
+ }
+
+ let path;
+ let href = removeQuery(relatedSheet.href);
+ let uri = NetUtil.newURI(href);
+
+ if (uri.scheme == "file") {
+ let file = uri.QueryInterface(Ci.nsIFileURL).file;
+ path = file.path;
+ } else if (this.savedFile) {
+ let origHref = removeQuery(this.styleSheet.href);
+ let origUri = NetUtil.newURI(origHref);
+ path = findLinkedFilePath(uri, origUri, this.savedFile);
+ } else {
+ // we can't determine path to generated file on disk
+ return;
+ }
+
+ if (this.linkedCSSFile == path) {
+ return;
+ }
+
+ this.linkedCSSFile = path;
+
+ this.linkedCSSFileError = null;
+
+ // save last file change time so we can compare when we check for changes.
+ OS.File.stat(path).then((info) => {
+ this._fileModDate = info.lastModificationDate.getTime();
+ }, this.markLinkedFileBroken);
+
+ this.emit("linked-css-file");
+ },
+
+ /**
+ * A helper function that fetches the source text from the style
+ * sheet. The text is possibly prettified using prettifyCSS. This
+ * also sets |this._state.text| to the new text.
+ *
+ * @return {Promise} a promise that resolves to the new text
+ */
+ _getSourceTextAndPrettify: function () {
+ return this.styleSheet.getText().then((longStr) => {
+ return longStr.string();
+ }).then((source) => {
+ let ruleCount = this.styleSheet.ruleCount;
+ if (!this.styleSheet.isOriginalSource) {
+ source = prettifyCSS(source, ruleCount);
+ }
+ this._state.text = source;
+ return source;
+ });
+ },
+
+ /**
+ * Start fetching the full text source for this editor's sheet.
+ *
+ * @return {Promise}
+ * A promise that'll resolve with the source text once the source
+ * has been loaded or reject on unexpected error.
+ */
+ fetchSource: function () {
+ return this._getSourceTextAndPrettify().then((source) => {
+ this.sourceLoaded = true;
+ return source;
+ }).then(null, e => {
+ if (this._isDestroyed) {
+ console.warn("Could not fetch the source for " +
+ this.styleSheet.href +
+ ", the editor was destroyed");
+ console.error(e);
+ } else {
+ this.emit("error", { key: LOAD_ERROR, append: this.styleSheet.href });
+ throw e;
+ }
+ });
+ },
+
+ /**
+ * Add markup to a region. UNUSED_CLASS is added to specified lines
+ * @param region An object shaped like
+ * {
+ * start: { line: L1, column: C1 },
+ * end: { line: L2, column: C2 } // optional
+ * }
+ */
+ addUnusedRegion: function (region) {
+ this.sourceEditor.addLineClass(region.start.line - 1, UNUSED_CLASS);
+ if (region.end) {
+ for (let i = region.start.line; i <= region.end.line; i++) {
+ this.sourceEditor.addLineClass(i - 1, UNUSED_CLASS);
+ }
+ }
+ },
+
+ /**
+ * As addUnusedRegion except that it takes an array of regions
+ */
+ addUnusedRegions: function (regions) {
+ for (let region of regions) {
+ this.addUnusedRegion(region);
+ }
+ },
+
+ /**
+ * Remove all the unused markup regions added by addUnusedRegion
+ */
+ removeAllUnusedRegions: function () {
+ for (let i = 0; i < this.sourceEditor.lineCount(); i++) {
+ this.sourceEditor.removeLineClass(i, UNUSED_CLASS);
+ }
+ },
+
+ /**
+ * Forward property-change event from stylesheet.
+ *
+ * @param {string} event
+ * Event type
+ * @param {string} property
+ * Property that has changed on sheet
+ */
+ _onPropertyChange: function (property, value) {
+ this.emit("property-change", property, value);
+ },
+
+ /**
+ * Called when the stylesheet text changes.
+ */
+ _onStyleApplied: function () {
+ if (this._isUpdating) {
+ // We just applied an edit in the editor, so we can drop this
+ // notification.
+ this._isUpdating = false;
+ } else if (this.sourceEditor) {
+ this._getSourceTextAndPrettify().then((newText) => {
+ this._justSetText = true;
+ let firstLine = this.sourceEditor.getFirstVisibleLine();
+ let pos = this.sourceEditor.getCursor();
+ this.sourceEditor.setText(newText);
+ this.sourceEditor.setFirstVisibleLine(firstLine);
+ this.sourceEditor.setCursor(pos);
+ this.emit("style-applied");
+ });
+ }
+ },
+
+ /**
+ * Handles changes to the list of @media rules in the stylesheet.
+ * Emits 'media-rules-changed' if the list has changed.
+ *
+ * @param {array} rules
+ * Array of MediaRuleFronts for new media rules of sheet.
+ */
+ _onMediaRulesChanged: function (rules) {
+ if (!rules.length && !this.mediaRules.length) {
+ return;
+ }
+ for (let rule of this.mediaRules) {
+ rule.off("matches-change", this._onMediaRuleMatchesChange);
+ rule.destroy();
+ }
+ this.mediaRules = rules;
+
+ for (let rule of rules) {
+ rule.on("matches-change", this._onMediaRuleMatchesChange);
+ }
+ this.emit("media-rules-changed", rules);
+ },
+
+ /**
+ * Forward media-rules-changed event from stylesheet.
+ */
+ _onMediaRuleMatchesChange: function () {
+ this.emit("media-rules-changed", this.mediaRules);
+ },
+
+ /**
+ * Forward error event from stylesheet.
+ *
+ * @param {string} event
+ * Event type
+ * @param {string} errorCode
+ */
+ _onError: function (event, data) {
+ this.emit("error", data);
+ },
+
+ /**
+ * Create source editor and load state into it.
+ * @param {DOMElement} inputElement
+ * Element to load source editor in
+ * @param {CssProperties} cssProperties
+ * A css properties database.
+ *
+ * @return {Promise}
+ * Promise that will resolve when the style editor is loaded.
+ */
+ load: function (inputElement, cssProperties) {
+ if (this._isDestroyed) {
+ return promise.reject("Won't load source editor as the style sheet has " +
+ "already been removed from Style Editor.");
+ }
+
+ this._inputElement = inputElement;
+
+ let config = {
+ value: this._state.text,
+ lineNumbers: true,
+ mode: Editor.modes.css,
+ readOnly: false,
+ autoCloseBrackets: "{}()",
+ extraKeys: this._getKeyBindings(),
+ contextMenu: "sourceEditorContextMenu",
+ autocomplete: Services.prefs.getBoolPref(AUTOCOMPLETION_PREF),
+ autocompleteOpts: { walker: this.walker, cssProperties },
+ cssProperties
+ };
+ let sourceEditor = this._sourceEditor = new Editor(config);
+
+ sourceEditor.on("dirty-change", this._onPropertyChange);
+
+ return sourceEditor.appendTo(inputElement).then(() => {
+ sourceEditor.on("saveRequested", this.saveToFile);
+
+ if (this.styleSheet.update) {
+ sourceEditor.on("change", this.updateStyleSheet);
+ }
+
+ this.sourceEditor = sourceEditor;
+
+ if (this._focusOnSourceEditorReady) {
+ this._focusOnSourceEditorReady = false;
+ sourceEditor.focus();
+ }
+
+ sourceEditor.setSelection(this._state.selection.start,
+ this._state.selection.end);
+
+ if (this.highlighter && this.walker) {
+ sourceEditor.container.addEventListener("mousemove", this._onMouseMove);
+ }
+
+ this.emit("source-editor-load");
+ });
+ },
+
+ /**
+ * Get the source editor for this editor.
+ *
+ * @return {Promise}
+ * Promise that will resolve with the editor.
+ */
+ getSourceEditor: function () {
+ let deferred = defer();
+
+ if (this.sourceEditor) {
+ return promise.resolve(this);
+ }
+ this.on("source-editor-load", () => {
+ deferred.resolve(this);
+ });
+ return deferred.promise;
+ },
+
+ /**
+ * Focus the Style Editor input.
+ */
+ focus: function () {
+ if (this.sourceEditor) {
+ this.sourceEditor.focus();
+ } else {
+ this._focusOnSourceEditorReady = true;
+ }
+ },
+
+ /**
+ * Event handler for when the editor is shown.
+ */
+ onShow: function () {
+ if (this.sourceEditor) {
+ // CodeMirror needs refresh to restore scroll position after hiding and
+ // showing the editor.
+ this.sourceEditor.refresh();
+ }
+ this.focus();
+ },
+
+ /**
+ * Toggled the disabled state of the underlying stylesheet.
+ */
+ toggleDisabled: function () {
+ this.styleSheet.toggleDisabled().then(null, e => console.error(e));
+ },
+
+ /**
+ * Queue a throttled task to update the live style sheet.
+ */
+ updateStyleSheet: function () {
+ if (this._updateTask) {
+ // cancel previous queued task not executed within throttle delay
+ this._window.clearTimeout(this._updateTask);
+ }
+
+ this._updateTask = this._window.setTimeout(this._updateStyleSheet,
+ UPDATE_STYLESHEET_DELAY);
+ },
+
+ /**
+ * Update live style sheet according to modifications.
+ */
+ _updateStyleSheet: function () {
+ if (this.styleSheet.disabled) {
+ // TODO: do we want to do this?
+ return;
+ }
+
+ if (this._justSetText) {
+ this._justSetText = false;
+ return;
+ }
+
+ // reset only if we actually perform an update
+ // (stylesheet is enabled) so that 'missed' updates
+ // while the stylesheet is disabled can be performed
+ // when it is enabled back. @see enableStylesheet
+ this._updateTask = null;
+
+ if (this.sourceEditor) {
+ this._state.text = this.sourceEditor.getText();
+ }
+
+ this._isUpdating = true;
+ this.styleSheet.update(this._state.text, this.transitionsEnabled)
+ .then(null, e => console.error(e));
+ },
+
+ /**
+ * Handle mousemove events, calling _highlightSelectorAt after a delay only
+ * and reseting the delay everytime.
+ */
+ _onMouseMove: function (e) {
+ this.highlighter.hide();
+
+ if (this.mouseMoveTimeout) {
+ this._window.clearTimeout(this.mouseMoveTimeout);
+ this.mouseMoveTimeout = null;
+ }
+
+ this.mouseMoveTimeout = this._window.setTimeout(() => {
+ this._highlightSelectorAt(e.clientX, e.clientY);
+ }, SELECTOR_HIGHLIGHT_TIMEOUT);
+ },
+
+ /**
+ * Highlight nodes matching the selector found at coordinates x,y in the
+ * editor, if any.
+ *
+ * @param {Number} x
+ * @param {Number} y
+ */
+ _highlightSelectorAt: Task.async(function* (x, y) {
+ let pos = this.sourceEditor.getPositionFromCoords({left: x, top: y});
+ let info = this.sourceEditor.getInfoAt(pos);
+ if (!info || info.state !== "selector") {
+ return;
+ }
+
+ let node =
+ yield this.walker.getStyleSheetOwnerNode(this.styleSheet.actorID);
+ yield this.highlighter.show(node, {
+ selector: info.selector,
+ hideInfoBar: true,
+ showOnly: "border",
+ region: "border"
+ });
+
+ this.emit("node-highlighted");
+ }),
+
+ /**
+ * Save the editor contents into a file and set savedFile property.
+ * A file picker UI will open if file is not set and editor is not headless.
+ *
+ * @param mixed file
+ * Optional nsIFile or string representing the filename to save in the
+ * background, no UI will be displayed.
+ * If not specified, the original style sheet URI is used.
+ * To implement 'Save' instead of 'Save as', you can pass
+ * savedFile here.
+ * @param function(nsIFile aFile) callback
+ * Optional callback called when the operation has finished.
+ * aFile has the nsIFile object for saved file or null if the operation
+ * has failed or has been canceled by the user.
+ * @see savedFile
+ */
+ saveToFile: function (file, callback) {
+ let onFile = (returnFile) => {
+ if (!returnFile) {
+ if (callback) {
+ callback(null);
+ }
+ return;
+ }
+
+ if (this.sourceEditor) {
+ this._state.text = this.sourceEditor.getText();
+ }
+
+ let ostream = FileUtils.openSafeFileOutputStream(returnFile);
+ let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
+ .createInstance(Ci.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+ let istream = converter.convertToInputStream(this._state.text);
+
+ NetUtil.asyncCopy(istream, ostream, (status) => {
+ if (!Components.isSuccessCode(status)) {
+ if (callback) {
+ callback(null);
+ }
+ this.emit("error", { key: SAVE_ERROR });
+ return;
+ }
+ FileUtils.closeSafeFileOutputStream(ostream);
+
+ this.onFileSaved(returnFile);
+
+ if (callback) {
+ callback(returnFile);
+ }
+ });
+ };
+
+ let defaultName;
+ if (this._friendlyName) {
+ defaultName = OS.Path.basename(this._friendlyName);
+ }
+ showFilePicker(file || this._styleSheetFilePath, true, this._window,
+ onFile, defaultName);
+ },
+
+ /**
+ * Called when this source has been successfully saved to disk.
+ */
+ onFileSaved: function (returnFile) {
+ this._friendlyName = null;
+ this.savedFile = returnFile;
+
+ if (this.sourceEditor) {
+ this.sourceEditor.setClean();
+ }
+
+ this.emit("property-change");
+
+ // TODO: replace with file watching
+ this._modCheckCount = 0;
+ this._window.clearTimeout(this._timeout);
+
+ if (this.linkedCSSFile && !this.linkedCSSFileError) {
+ this._timeout = this._window.setTimeout(this.checkLinkedFileForChanges,
+ CHECK_LINKED_SHEET_DELAY);
+ }
+ },
+
+ /**
+ * Check to see if our linked CSS file has changed on disk, and
+ * if so, update the live style sheet.
+ */
+ checkLinkedFileForChanges: function () {
+ OS.File.stat(this.linkedCSSFile).then((info) => {
+ let lastChange = info.lastModificationDate.getTime();
+
+ if (this._fileModDate && lastChange != this._fileModDate) {
+ this._fileModDate = lastChange;
+ this._modCheckCount = 0;
+
+ this.updateLinkedStyleSheet();
+ return;
+ }
+
+ if (++this._modCheckCount > MAX_CHECK_COUNT) {
+ this.updateLinkedStyleSheet();
+ return;
+ }
+
+ // try again in a bit
+ this._timeout = this._window.setTimeout(this.checkLinkedFileForChanges,
+ CHECK_LINKED_SHEET_DELAY);
+ }, this.markLinkedFileBroken);
+ },
+
+ /**
+ * Notify that the linked CSS file (if this is an original source)
+ * doesn't exist on disk in the place we think it does.
+ *
+ * @param string error
+ * The error we got when trying to access the file.
+ */
+ markLinkedFileBroken: function (error) {
+ this.linkedCSSFileError = error || true;
+ this.emit("linked-css-file-error");
+
+ error += " querying " + this.linkedCSSFile +
+ " original source location: " + this.savedFile.path;
+ console.error(error);
+ },
+
+ /**
+ * For original sources (e.g. Sass files). Fetch contents of linked CSS
+ * file from disk and live update the stylesheet object with the contents.
+ */
+ updateLinkedStyleSheet: function () {
+ OS.File.read(this.linkedCSSFile).then((array) => {
+ let decoder = new TextDecoder();
+ let text = decoder.decode(array);
+
+ let relatedSheet = this.styleSheet.relatedStyleSheet;
+ relatedSheet.update(text, this.transitionsEnabled);
+ }, this.markLinkedFileBroken);
+ },
+
+ /**
+ * Retrieve custom key bindings objects as expected by Editor.
+ * Editor action names are not displayed to the user.
+ *
+ * @return {array} key binding objects for the source editor
+ */
+ _getKeyBindings: function () {
+ let bindings = {};
+ let keybind = Editor.accel(getString("saveStyleSheet.commandkey"));
+
+ bindings[keybind] = () => {
+ this.saveToFile(this.savedFile);
+ };
+
+ bindings["Shift-" + keybind] = () => {
+ this.saveToFile();
+ };
+
+ bindings.Esc = false;
+
+ return bindings;
+ },
+
+ /**
+ * Clean up for this editor.
+ */
+ destroy: function () {
+ if (this._sourceEditor) {
+ this._sourceEditor.off("dirty-change", this._onPropertyChange);
+ this._sourceEditor.off("saveRequested", this.saveToFile);
+ this._sourceEditor.off("change", this.updateStyleSheet);
+ if (this.highlighter && this.walker && this._sourceEditor.container) {
+ this._sourceEditor.container.removeEventListener("mousemove",
+ this._onMouseMove);
+ }
+ this._sourceEditor.destroy();
+ }
+ this.cssSheet.off("property-change", this._onPropertyChange);
+ this.cssSheet.off("media-rules-changed", this._onMediaRulesChanged);
+ this.cssSheet.off("style-applied", this._onStyleApplied);
+ this.styleSheet.off("error", this._onError);
+ this._isDestroyed = true;
+ }
+};
+
+/**
+ * Find a path on disk for a file given it's hosted uri, the uri of the
+ * original resource that generated it (e.g. Sass file), and the location of the
+ * local file for that source.
+ *
+ * @param {nsIURI} uri
+ * The uri of the resource
+ * @param {nsIURI} origUri
+ * The uri of the original source for the resource
+ * @param {nsIFile} file
+ * The local file for the resource on disk
+ *
+ * @return {string}
+ * The path of original file on disk
+ */
+function findLinkedFilePath(uri, origUri, file) {
+ let { origBranch, branch } = findUnsharedBranches(origUri, uri);
+ let project = findProjectPath(file, origBranch);
+
+ let parts = project.concat(branch);
+ let path = OS.Path.join.apply(this, parts);
+
+ return path;
+}
+
+/**
+ * Find the path of a project given a file in the project and its branch
+ * off the root. e.g.:
+ * /Users/moz/proj/src/a.css" and "src/a.css"
+ * would yield ["Users", "moz", "proj"]
+ *
+ * @param {nsIFile} file
+ * file for that resource on disk
+ * @param {array} branch
+ * path parts for branch to chop off file path.
+ * @return {array}
+ * array of path parts
+ */
+function findProjectPath(file, branch) {
+ let path = OS.Path.split(file.path).components;
+
+ for (let i = 2; i <= branch.length; i++) {
+ // work backwards until we find a differing directory name
+ if (path[path.length - i] != branch[branch.length - i]) {
+ return path.slice(0, path.length - i + 1);
+ }
+ }
+
+ // if we don't find a differing directory, just chop off the branch
+ return path.slice(0, path.length - branch.length);
+}
+
+/**
+ * Find the parts of a uri past the root it shares with another uri. e.g:
+ * "http://localhost/built/a.scss" and "http://localhost/src/a.css"
+ * would yield ["built", "a.scss"] and ["src", "a.css"]
+ *
+ * @param {nsIURI} origUri
+ * uri to find unshared branch of. Usually is uri for original source.
+ * @param {nsIURI} uri
+ * uri to compare against to get a shared root
+ * @return {object}
+ * object with 'branch' and 'origBranch' array of path parts for branch
+ */
+function findUnsharedBranches(origUri, uri) {
+ origUri = OS.Path.split(origUri.path).components;
+ uri = OS.Path.split(uri.path).components;
+
+ for (let i = 0; i < uri.length - 1; i++) {
+ if (uri[i] != origUri[i]) {
+ return {
+ branch: uri.slice(i),
+ origBranch: origUri.slice(i)
+ };
+ }
+ }
+ return {
+ branch: uri,
+ origBranch: origUri
+ };
+}
+
+/**
+ * Remove the query string from a url.
+ *
+ * @param {string} href
+ * Url to remove query string from
+ * @return {string}
+ * Url without query string
+ */
+function removeQuery(href) {
+ return href.replace(/\?.*/, "");
+}
diff --git a/devtools/client/styleeditor/moz.build b/devtools/client/styleeditor/moz.build
new file mode 100644
index 000000000..4fc06b660
--- /dev/null
+++ b/devtools/client/styleeditor/moz.build
@@ -0,0 +1,16 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
+
+DevToolsModules(
+ 'styleeditor-commands.js',
+ 'styleeditor-panel.js',
+ 'StyleEditorUI.jsm',
+ 'StyleEditorUtil.jsm',
+ 'StyleSheetEditor.jsm',
+ 'utils.js',
+)
diff --git a/devtools/client/styleeditor/styleeditor-commands.js b/devtools/client/styleeditor/styleeditor-commands.js
new file mode 100644
index 000000000..ded63e499
--- /dev/null
+++ b/devtools/client/styleeditor/styleeditor-commands.js
@@ -0,0 +1,72 @@
+/* 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/. */
+
+/* globals gDevTools */
+
+"use strict";
+
+const l10n = require("gcli/l10n");
+loader.lazyRequireGetter(this, "gDevTools",
+ "devtools/client/framework/devtools", true);
+
+/**
+ * The `edit` command opens the toolbox to the style editor, with a given
+ * stylesheet open.
+ *
+ * This command is tricky. The 'edit' command uses the toolbox, so it's
+ * clearly runAt:client, but it uses the 'resource' type which accesses the
+ * DOM, so it must also be runAt:server.
+ *
+ * Our solution is to have the command technically be runAt:server, but to not
+ * actually do anything other than basically `return args;`, and have the
+ * converter (all converters are runAt:client) do the actual work of opening
+ * a toolbox.
+ *
+ * For alternative solutions that we considered, see the comment on commit
+ * 2645af7.
+ */
+exports.items = [{
+ item: "command",
+ runAt: "server",
+ name: "edit",
+ description: l10n.lookup("editDesc"),
+ manual: l10n.lookup("editManual2"),
+ params: [
+ {
+ name: "resource",
+ type: {
+ name: "resource",
+ include: "text/css"
+ },
+ description: l10n.lookup("editResourceDesc")
+ },
+ {
+ name: "line",
+ defaultValue: 1,
+ type: {
+ name: "number",
+ min: 1,
+ step: 10
+ },
+ description: l10n.lookup("editLineToJumpToDesc")
+ }
+ ],
+ returnType: "editArgs",
+ exec: args => {
+ return { href: args.resource.name, line: args.line };
+ }
+}, {
+ item: "converter",
+ from: "editArgs",
+ to: "dom",
+ exec: function (args, context) {
+ let target = context.environment.target;
+ let toolboxOpened = gDevTools.showToolbox(target, "styleeditor");
+ return toolboxOpened.then(function (toolbox) {
+ let styleEditor = toolbox.getCurrentPanel();
+ styleEditor.selectStyleSheet(args.href, args.line);
+ return null;
+ });
+ }
+}];
diff --git a/devtools/client/styleeditor/styleeditor-panel.js b/devtools/client/styleeditor/styleeditor-panel.js
new file mode 100644
index 000000000..b5820d7a4
--- /dev/null
+++ b/devtools/client/styleeditor/styleeditor-panel.js
@@ -0,0 +1,158 @@
+/* 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 Services = require("Services");
+var promise = require("promise");
+var {Task} = require("devtools/shared/task");
+var {XPCOMUtils} = require("resource://gre/modules/XPCOMUtils.jsm");
+var EventEmitter = require("devtools/shared/event-emitter");
+
+var {StyleEditorUI} = require("resource://devtools/client/styleeditor/StyleEditorUI.jsm");
+var {getString} = require("resource://devtools/client/styleeditor/StyleEditorUtil.jsm");
+var {initCssProperties} = require("devtools/shared/fronts/css-properties");
+
+loader.lazyGetter(this, "StyleSheetsFront",
+ () => require("devtools/shared/fronts/stylesheets").StyleSheetsFront);
+
+loader.lazyGetter(this, "StyleEditorFront",
+ () => require("devtools/shared/fronts/styleeditor").StyleEditorFront);
+
+var StyleEditorPanel = function StyleEditorPanel(panelWin, toolbox) {
+ EventEmitter.decorate(this);
+
+ this._toolbox = toolbox;
+ this._target = toolbox.target;
+ this._panelWin = panelWin;
+ this._panelDoc = panelWin.document;
+
+ this.destroy = this.destroy.bind(this);
+ this._showError = this._showError.bind(this);
+};
+
+exports.StyleEditorPanel = StyleEditorPanel;
+
+StyleEditorPanel.prototype = {
+ get target() {
+ return this._toolbox.target;
+ },
+
+ get panelWindow() {
+ return this._panelWin;
+ },
+
+ /**
+ * open is effectively an asynchronous constructor
+ */
+ open: Task.async(function* () {
+ // We always interact with the target as if it were remote
+ if (!this.target.isRemote) {
+ yield this.target.makeRemote();
+ }
+
+ this.target.on("close", this.destroy);
+
+ if (this.target.form.styleSheetsActor) {
+ this._debuggee = StyleSheetsFront(this.target.client, this.target.form);
+ } else {
+ /* We're talking to a pre-Firefox 29 server-side */
+ this._debuggee = StyleEditorFront(this.target.client, this.target.form);
+ }
+
+ // Initialize the CSS properties database.
+ const {cssProperties} = yield initCssProperties(this._toolbox);
+
+ // Initialize the UI
+ this.UI = new StyleEditorUI(this._debuggee, this.target, this._panelDoc,
+ cssProperties);
+ this.UI.on("error", this._showError);
+ yield this.UI.initialize();
+
+ this.isReady = true;
+
+ return this;
+ }),
+
+ /**
+ * Show an error message from the style editor in the toolbox
+ * notification box.
+ *
+ * @param {string} event
+ * Type of event
+ * @param {string} data
+ * The parameters to customize the error message
+ */
+ _showError: function (event, data) {
+ if (!this._toolbox) {
+ // could get an async error after we've been destroyed
+ return;
+ }
+
+ let errorMessage = getString(data.key);
+ if (data.append) {
+ errorMessage += " " + data.append;
+ }
+
+ let notificationBox = this._toolbox.getNotificationBox();
+ let notification =
+ notificationBox.getNotificationWithValue("styleeditor-error");
+ let level = (data.level === "info") ?
+ notificationBox.PRIORITY_INFO_LOW :
+ notificationBox.PRIORITY_CRITICAL_LOW;
+
+ if (!notification) {
+ notificationBox.appendNotification(errorMessage, "styleeditor-error",
+ "", level);
+ }
+ },
+
+ /**
+ * Select a stylesheet.
+ *
+ * @param {string} href
+ * Url of stylesheet to find and select in editor
+ * @param {number} line
+ * Line number to jump to after selecting. One-indexed
+ * @param {number} col
+ * Column number to jump to after selecting. One-indexed
+ * @return {Promise}
+ * Promise that will resolve when the editor is selected and ready
+ * to be used.
+ */
+ selectStyleSheet: function (href, line, col) {
+ if (!this._debuggee || !this.UI) {
+ return null;
+ }
+ return this.UI.selectStyleSheet(href, line - 1, col ? col - 1 : 0);
+ },
+
+ /**
+ * Destroy the style editor.
+ */
+ destroy: function () {
+ if (!this._destroyed) {
+ this._destroyed = true;
+
+ this._target.off("close", this.destroy);
+ this._target = null;
+ this._toolbox = null;
+ this._panelWin = null;
+ this._panelDoc = null;
+ this._debuggee.destroy();
+ this._debuggee = null;
+
+ this.UI.destroy();
+ this.UI = null;
+ }
+
+ return promise.resolve(null);
+ },
+};
+
+XPCOMUtils.defineLazyGetter(StyleEditorPanel.prototype, "strings",
+ function () {
+ return Services.strings.createBundle(
+ "chrome://devtools/locale/styleeditor.properties");
+ });
diff --git a/devtools/client/styleeditor/styleeditor.xul b/devtools/client/styleeditor/styleeditor.xul
new file mode 100644
index 000000000..1a5926159
--- /dev/null
+++ b/devtools/client/styleeditor/styleeditor.xul
@@ -0,0 +1,220 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+<!DOCTYPE window [
+<!ENTITY % styleEditorDTD SYSTEM "chrome://devtools/locale/styleeditor.dtd" >
+ %styleEditorDTD;
+<!ENTITY % editMenuStrings SYSTEM "chrome://global/locale/editMenuOverlay.dtd">
+ %editMenuStrings;
+<!ENTITY % sourceEditorStrings SYSTEM "chrome://devtools/locale/sourceeditor.dtd">
+ %sourceEditorStrings;
+<!ENTITY % csscoverageDTD SYSTEM "chrome://devtools-shared/locale/csscoverage.dtd">
+ %csscoverageDTD;
+]>
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/content/shared/widgets/widgets.css" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/content/shared/splitview.css" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/skin/widgets.css" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/skin/splitview.css" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/skin/styleeditor.css" type="text/css"?>
+<?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?>
+
+<xul:window xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns="http://www.w3.org/1999/xhtml"
+ id="style-editor-chrome-window">
+
+ <script type="application/javascript;version=1.8"
+ src="chrome://devtools/content/shared/theme-switching.js"/>
+ <xul:script type="application/javascript" src="chrome://global/content/globalOverlay.js"/>
+ <xul:script type="application/javascript" src="chrome://browser/content/utilityOverlay.js"/>
+ <xul:script type="application/javascript">
+ function goUpdateSourceEditorMenuItems() {
+ goUpdateGlobalEditMenuItems();
+
+ ['cmd_undo', 'cmd_redo', 'cmd_cut', 'cmd_paste',
+ 'cmd_delete', 'cmd_find', 'cmd_findAgain'].forEach(goUpdateCommand);
+ }
+ </xul:script>
+
+ <xul:popupset id="style-editor-popups">
+ <xul:menupopup id="sourceEditorContextMenu"
+ onpopupshowing="goUpdateSourceEditorMenuItems()">
+ <xul:menuitem id="cMenu_undo"/>
+ <xul:menuseparator/>
+ <xul:menuitem id="cMenu_cut"/>
+ <xul:menuitem id="cMenu_copy"/>
+ <xul:menuitem id="cMenu_paste"/>
+ <xul:menuitem id="cMenu_delete"/>
+ <xul:menuseparator/>
+ <xul:menuitem id="cMenu_selectAll"/>
+ <xul:menuseparator/>
+ <xul:menuitem id="se-menu-find"
+ label="&findCmd.label;" accesskey="&findCmd.accesskey;" command="cmd_find"/>
+ <xul:menuitem id="cMenu_findAgain"/>
+ <xul:menuseparator/>
+ <xul:menuitem id="se-menu-gotoLine"
+ label="&gotoLineCmd.label;"
+ accesskey="&gotoLineCmd.accesskey;"
+ key="key_gotoLine"
+ command="cmd_gotoLine"/>
+ </xul:menupopup>
+ <xul:menupopup id="sidebar-context">
+ <xul:menuitem id="context-openlinknewtab"
+ label="&openLinkNewTab.label;"/>
+ </xul:menupopup>
+ <xul:menupopup id="style-editor-options-popup"
+ position="before_start">
+ <xul:menuitem id="options-origsources"
+ type="checkbox"
+ label="&showOriginalSources.label;"
+ accesskey="&showOriginalSources.accesskey;"/>
+ <xul:menuitem id="options-show-media"
+ type="checkbox"
+ label="&showMediaSidebar.label;"
+ accesskey="&showMediaSidebar.accesskey;"/>
+ </xul:menupopup>
+ </xul:popupset>
+
+ <xul:commandset id="editMenuCommands"/>
+
+ <xul:commandset id="sourceEditorCommands">
+ <xul:command id="cmd_gotoLine" oncommand="goDoCommand('cmd_gotoLine')"/>
+ <xul:command id="cmd_find" oncommand="goDoCommand('cmd_find')"/>
+ <xul:command id="cmd_findAgain" oncommand="goDoCommand('cmd_findAgain')"/>
+ </xul:commandset>
+
+ <xul:keyset id="sourceEditorKeys"/>
+
+ <xul:stack id="style-editor-chrome" class="loading theme-body">
+
+ <xul:box class="splitview-root devtools-responsive-container" context="sidebar-context">
+ <xul:box class="splitview-controller">
+ <xul:box class="splitview-main">
+ <xul:toolbar class="devtools-toolbar">
+ <xul:hbox class="devtools-toolbarbutton-group">
+ <xul:toolbarbutton class="style-editor-newButton devtools-toolbarbutton"
+ accesskey="&newButton.accesskey;"
+ tooltiptext="&newButton.tooltip;"/>
+ <xul:toolbarbutton class="style-editor-importButton devtools-toolbarbutton"
+ accesskey="&importButton.accesskey;"
+ tooltiptext="&importButton.tooltip;"/>
+ </xul:hbox>
+ <xul:spacer/>
+ <xul:toolbarbutton id="style-editor-options"
+ class="devtools-toolbarbutton devtools-option-toolbarbutton"
+ tooltiptext="&optionsButton.tooltip;"
+ popup="style-editor-options-popup"/>
+ </xul:toolbar>
+ </xul:box>
+ <xul:box id="splitview-resizer-target" class="theme-sidebar splitview-nav-container"
+ persist="height">
+ <div class="devtools-throbber"></div>
+ <ol class="splitview-nav" tabindex="0"></ol>
+ <div class="splitview-nav placeholder empty">
+ <p><strong>&noStyleSheet.label;</strong></p>
+ <p>&noStyleSheet-tip-start.label;
+ <a href="#"
+ class="style-editor-newButton">&noStyleSheet-tip-action.label;</a>
+ &noStyleSheet-tip-end.label;</p>
+ </div>
+ </xul:box> <!-- .splitview-nav-container -->
+ </xul:box> <!-- .splitview-controller -->
+ <xul:splitter class="devtools-side-splitter devtools-invisible-splitter"/>
+ <xul:box class="splitview-side-details devtools-main-content"/>
+
+ <div id="splitview-templates" hidden="true">
+ <li id="splitview-tpl-summary-stylesheet" tabindex="0">
+ <xul:label class="stylesheet-enabled" tabindex="0"
+ tooltiptext="&visibilityToggle.tooltip;"
+ accesskey="&saveButton.accesskey;"></xul:label>
+ <hgroup class="stylesheet-info">
+ <h1><a class="stylesheet-name" tabindex="0"><xul:label crop="center"/></a></h1>
+ <div class="stylesheet-more">
+ <h3 class="stylesheet-title"></h3>
+ <h3 class="stylesheet-linked-file"></h3>
+ <h3 class="stylesheet-rule-count"></h3>
+ <xul:spacer/>
+ <h3><xul:label class="stylesheet-saveButton"
+ tooltiptext="&saveButton.tooltip;"
+ accesskey="&saveButton.accesskey;">&saveButton.label;</xul:label></h3>
+ </div>
+ </hgroup>
+ </li>
+
+ <xul:box id="splitview-tpl-details-stylesheet" class="splitview-details">
+ <xul:hbox class="stylesheet-details-container">
+ <xul:box class="stylesheet-editor-input textbox"
+ data-placeholder="&editorTextbox.placeholder;"/>
+ <xul:splitter class="devtools-side-splitter"/>
+ <xul:vbox class="stylesheet-sidebar theme-sidebar" hidden="true">
+ <xul:toolbar class="devtools-toolbar">
+ &mediaRules.label;
+ </xul:toolbar>
+ <xul:vbox class="stylesheet-media-container" flex="1">
+ <div class="stylesheet-media-list" />
+ </xul:vbox>
+ </xul:vbox>
+ </xul:hbox>
+ </xul:box>
+ </div> <!-- #splitview-templates -->
+ </xul:box> <!-- .splitview-root -->
+
+ <xul:box class="csscoverage-template" hidden="true">
+ <xul:toolbar class="devtools-toolbar csscoverage-toolbar">
+ <xul:button class="devtools-toolbarbutton csscoverage-toolbarbutton"
+ label="&csscoverage.backButton;"
+ onclick="${onback}"/>
+ </xul:toolbar>
+ <!-- The data for this comes from CSSUsageActor.createPageReport -->
+ <div class="csscoverage-report-container">
+ <div class="csscoverage-report-content">
+ <div class="csscoverage-report-summary">
+ <div class="csscoverage-report-chart"/>
+ </div>
+ <div class="csscoverage-report-unused">
+ <h2>&csscoverage.unused;</h2>
+ <p>&csscoverage.noMatches;</p>
+ <div foreach="page in ${unused}">
+ <h3>${page.url}</h3>
+ <code foreach="rule in ${page.rules}"
+ href="${rule.url}"
+ class="csscoverage-list">${rule.selectorText}</code>
+ </div>
+ </div>
+ <div class="csscoverage-report-optimize">
+ <h2>&csscoverage.optimize.header;</h2>
+ <p>
+ &csscoverage.optimize.body1;
+ <code>&lt;link ...></code>
+ &csscoverage.optimize.body2;
+ <code>&lt;style>...</code>
+ &csscoverage.optimize.body3;
+ </p>
+ <div if="${preload.length == 0}">&csscoverage.optimize.bodyX;</div>
+ <div if="${preload.length > 0}">
+ <div foreach="page in ${preload}">
+ <h3>${page.url}</h3>
+ <textarea>&lt;style>
+<loop foreach="rule in ${page.rules}"
+ onclick="${rule.onclick}">${rule.formattedCssText}</loop>&lt;/style></textarea>
+ </div>
+ </div>
+ <p>
+ &csscoverage.footer1;
+ <a target="_blank" href="&csscoverage.footer2a;">&csscoverage.footer3;</a>
+ &csscoverage.footer4;
+ </p>
+ </div>
+ <p>&#160;</p>
+ </div>
+ </div>
+ </xul:box>
+
+ <xul:box class="csscoverage-report" hidden="true">
+ </xul:box>
+
+ </xul:stack>
+
+</xul:window>
diff --git a/devtools/client/styleeditor/test/.eslintrc.js b/devtools/client/styleeditor/test/.eslintrc.js
new file mode 100644
index 000000000..8d15a76d9
--- /dev/null
+++ b/devtools/client/styleeditor/test/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../../.eslintrc.mochitests.js"
+};
diff --git a/devtools/client/styleeditor/test/autocomplete.html b/devtools/client/styleeditor/test/autocomplete.html
new file mode 100644
index 000000000..801eb4d4b
--- /dev/null
+++ b/devtools/client/styleeditor/test/autocomplete.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<html>
+<head>
+ <title>testcase for autocomplete testing</title>
+ <link rel="stylesheet" type="text/css" href="resources_inpage1.css"/>
+ <style type="text/css">
+ div {
+ font-size: 4em;
+ }
+
+ div > span {
+ text-decoration: underline;
+ }
+
+ div + button {
+ border: 2px dotted red;
+ }
+ </style>
+</head>
+<body>
+ <div>parent <span>child</span></div><button>sibling</button>
+</body>
+</html>
diff --git a/devtools/client/styleeditor/test/browser.ini b/devtools/client/styleeditor/test/browser.ini
new file mode 100644
index 000000000..1a85546af
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser.ini
@@ -0,0 +1,107 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ autocomplete.html
+ browser_styleeditor_cmd_edit.html
+ four.html
+ head.js
+ import.css
+ import.html
+ import2.css
+ inline-1.html
+ inline-2.html
+ longload.html
+ media-small.css
+ media.html
+ media-rules.html
+ media-rules.css
+ media-rules-sourcemaps.html
+ minified.html
+ missing.html
+ nostyle.html
+ pretty.css
+ resources_inpage.jsi
+ resources_inpage1.css
+ resources_inpage2.css
+ simple.css
+ simple.css.gz
+ simple.css.gz^headers^
+ simple.gz.html
+ simple.html
+ sourcemap-css/contained.css
+ sourcemap-css/sourcemaps.css
+ sourcemap-css/sourcemaps.css.map
+ sourcemap-css/media-rules.css
+ sourcemap-css/media-rules.css.map
+ sourcemap-css/test-bootstrap-scss.css
+ sourcemap-css/test-stylus.css
+ sourcemap-sass/sourcemaps.scss
+ sourcemap-sass/media-rules.scss
+ sourcemap-styl/test-stylus.styl
+ sourcemaps.html
+ sourcemaps-inline.html
+ sourcemaps-large.html
+ sourcemaps-watching.html
+ test_private.css
+ test_private.html
+ doc_long.css
+ doc_uncached.css
+ doc_uncached.html
+ doc_xulpage.xul
+ sync.html
+ utf-16.css
+ !/devtools/client/commandline/test/helpers.js
+ !/devtools/client/framework/test/shared-head.js
+ !/devtools/client/inspector/shared/test/head.js
+ !/devtools/client/inspector/test/head.js
+ !/devtools/client/inspector/test/shared-head.js
+ !/devtools/client/responsive.html/test/browser/devices.json
+ !/devtools/client/shared/test/test-actor-registry.js
+ !/devtools/client/shared/test/test-actor.js
+
+[browser_styleeditor_autocomplete.js]
+[browser_styleeditor_autocomplete-disabled.js]
+[browser_styleeditor_bom.js]
+[browser_styleeditor_bug_740541_iframes.js]
+[browser_styleeditor_bug_851132_middle_click.js]
+[browser_styleeditor_bug_870339.js]
+[browser_styleeditor_cmd_edit.js]
+[browser_styleeditor_enabled.js]
+[browser_styleeditor_fetch-from-cache.js]
+[browser_styleeditor_filesave.js]
+[browser_styleeditor_highlight-selector.js]
+[browser_styleeditor_import.js]
+[browser_styleeditor_import_rule.js]
+[browser_styleeditor_init.js]
+[browser_styleeditor_inline_friendly_names.js]
+[browser_styleeditor_loading.js]
+[browser_styleeditor_loading_with_containers.js]
+[browser_styleeditor_media_sidebar.js]
+[browser_styleeditor_media_sidebar_links.js]
+skip-if = e10s && debug # Bug 1252201 - Docshell leak on debug e10s
+[browser_styleeditor_media_sidebar_sourcemaps.js]
+[browser_styleeditor_missing_stylesheet.js]
+[browser_styleeditor_navigate.js]
+[browser_styleeditor_new.js]
+[browser_styleeditor_nostyle.js]
+[browser_styleeditor_opentab.js]
+[browser_styleeditor_pretty.js]
+[browser_styleeditor_private_perwindowpb.js]
+[browser_styleeditor_reload.js]
+[browser_styleeditor_scroll.js]
+[browser_styleeditor_sv_keynav.js]
+[browser_styleeditor_sv_resize.js]
+[browser_styleeditor_selectstylesheet.js]
+[browser_styleeditor_sourcemaps.js]
+[browser_styleeditor_sourcemaps_inline.js]
+[browser_styleeditor_sourcemap_large.js]
+[browser_styleeditor_sourcemap_watching.js]
+[browser_styleeditor_sync.js]
+[browser_styleeditor_syncAddProperty.js]
+[browser_styleeditor_syncAddRule.js]
+[browser_styleeditor_syncAlreadyOpen.js]
+[browser_styleeditor_syncEditSelector.js]
+[browser_styleeditor_syncIntoRuleView.js]
+[browser_styleeditor_transition_rule.js]
+[browser_styleeditor_xul.js]
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_autocomplete-disabled.js b/devtools/client/styleeditor/test/browser_styleeditor_autocomplete-disabled.js
new file mode 100644
index 000000000..b632a7716
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_autocomplete-disabled.js
@@ -0,0 +1,26 @@
+/* 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";
+
+// Test that autocomplete can be disabled.
+
+const TESTCASE_URI = TEST_BASE_HTTP + "autocomplete.html";
+
+// Pref which decides if CSS autocompletion is enabled in Style Editor or not.
+const AUTOCOMPLETION_PREF = "devtools.styleeditor.autocompletion-enabled";
+
+add_task(function* () {
+ Services.prefs.setBoolPref(AUTOCOMPLETION_PREF, false);
+ let { ui } = yield openStyleEditorForURL(TESTCASE_URI);
+ let editor = yield ui.editors[0].getSourceEditor();
+
+ is(editor.sourceEditor.getOption("autocomplete"), false,
+ "Autocompletion option does not exist");
+ ok(!editor.sourceEditor.getAutocompletionPopup(),
+ "Autocompletion popup does not exist");
+});
+
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref(AUTOCOMPLETION_PREF);
+});
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_autocomplete.js b/devtools/client/styleeditor/test/browser_styleeditor_autocomplete.js
new file mode 100644
index 000000000..626498418
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_autocomplete.js
@@ -0,0 +1,231 @@
+/* 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";
+
+// Test that autocompletion works as expected.
+
+const TESTCASE_URI = TEST_BASE_HTTP + "autocomplete.html";
+const MAX_SUGGESTIONS = 15;
+
+const {initCssProperties} = require("devtools/shared/fronts/css-properties");
+
+// Test cases to test that autocompletion works correctly when enabled.
+// Format:
+// [
+// key,
+// {
+// total: Number of suggestions in the popup (-1 if popup is closed),
+// current: Index of selected suggestion,
+// inserted: 1 to check whether the selected suggestion is inserted into the
+// editor or not,
+// entered: 1 if the suggestion is inserted and finalized
+// }
+// ]
+
+function getTestCases(cssProperties) {
+ let keywords = getCSSKeywords(cssProperties);
+ let getSuggestionNumberFor = suggestionNumberGetter(keywords);
+
+ return [
+ ["VK_RIGHT"],
+ ["VK_RIGHT"],
+ ["VK_RIGHT"],
+ ["VK_RIGHT"],
+ ["Ctrl+Space", {total: 1, current: 0}],
+ ["VK_LEFT"],
+ ["VK_RIGHT"],
+ ["VK_DOWN"],
+ ["VK_RIGHT"],
+ ["VK_RIGHT"],
+ ["VK_RIGHT"],
+ ["Ctrl+Space", { total: getSuggestionNumberFor("font"), current: 0}],
+ ["VK_END"],
+ ["VK_RETURN"],
+ ["b", {total: getSuggestionNumberFor("b"), current: 0}],
+ ["a", {total: getSuggestionNumberFor("ba"), current: 0}],
+ ["VK_DOWN", {total: getSuggestionNumberFor("ba"), current: 0, inserted: 1}],
+ ["VK_TAB", {total: getSuggestionNumberFor("ba"), current: 1, inserted: 1}],
+ ["VK_RETURN", {current: 1, inserted: 1, entered: 1}],
+ ["b", {total: getSuggestionNumberFor("background", "b"), current: 0}],
+ ["l", {total: getSuggestionNumberFor("background", "bl"), current: 0}],
+ ["VK_TAB", {
+ total: getSuggestionNumberFor("background", "bl"),
+ current: 0, inserted: 1
+ }],
+ ["VK_DOWN", {
+ total: getSuggestionNumberFor("background", "bl"),
+ current: 1, inserted: 1
+ }],
+ ["VK_UP", {
+ total: getSuggestionNumberFor("background", "bl"),
+ current: 0,
+ inserted: 1
+ }],
+ ["VK_TAB", {
+ total: getSuggestionNumberFor("background", "bl"),
+ current: 1,
+ inserted: 1
+ }],
+ ["VK_TAB", {
+ total: getSuggestionNumberFor("background", "bl"),
+ current: 2,
+ inserted: 1
+ }],
+ [";"],
+ ["VK_RETURN"],
+ ["c", {total: getSuggestionNumberFor("c"), current: 0}],
+ ["o", {total: getSuggestionNumberFor("co"), current: 0}],
+ ["VK_RETURN", {current: 0, inserted: 1}],
+ ["r", {total: getSuggestionNumberFor("color", "r"), current: 0}],
+ ["VK_RETURN", {current: 0, inserted: 1}],
+ [";"],
+ ["VK_LEFT"],
+ ["VK_RIGHT"],
+ ["VK_DOWN"],
+ ["VK_RETURN"],
+ ["b", {total: 2, current: 0}],
+ ["u", {total: 1, current: 0}],
+ ["VK_RETURN", {current: 0, inserted: 1}],
+ ["{"],
+ ["VK_HOME"],
+ ["VK_DOWN"],
+ ["VK_DOWN"],
+ ["VK_RIGHT"],
+ ["VK_RIGHT"],
+ ["VK_RIGHT"],
+ ["VK_RIGHT"],
+ ["VK_RIGHT"],
+ ["VK_RIGHT"],
+ ["VK_RIGHT"],
+ ["VK_RIGHT"],
+ ["VK_RIGHT"],
+ ["VK_RIGHT"],
+ ["Ctrl+Space", {total: 1, current: 0}],
+ ];
+}
+
+add_task(function* () {
+ let { panel, ui } = yield openStyleEditorForURL(TESTCASE_URI);
+ let { cssProperties } = yield initCssProperties(panel._toolbox);
+ let testCases = getTestCases(cssProperties);
+
+ yield ui.selectStyleSheet(ui.editors[1].styleSheet);
+ let editor = yield ui.editors[1].getSourceEditor();
+
+ let sourceEditor = editor.sourceEditor;
+ let popup = sourceEditor.getAutocompletionPopup();
+
+ yield SimpleTest.promiseFocus(panel.panelWindow);
+
+ for (let index in testCases) {
+ yield testState(testCases, index, sourceEditor, popup, panel.panelWindow);
+ yield checkState(testCases, index, sourceEditor, popup);
+ }
+});
+
+function testState(testCases, index, sourceEditor, popup, panelWindow) {
+ let [key, details] = testCases[index];
+ let entered;
+ if (details) {
+ entered = details.entered;
+ }
+ let mods = {};
+
+ info("pressing key " + key + " to get result: " +
+ JSON.stringify(testCases[index]) + " for index " + index);
+
+ let evt = "after-suggest";
+
+ if (key == "Ctrl+Space") {
+ key = " ";
+ mods.ctrlKey = true;
+ } else if (key == "VK_RETURN" && entered) {
+ evt = "popup-hidden";
+ } else if (/(left|right|return|home|end)/ig.test(key) ||
+ (key == "VK_DOWN" && !popup.isOpen)) {
+ evt = "cursorActivity";
+ } else if (key == "VK_TAB" || key == "VK_UP" || key == "VK_DOWN") {
+ evt = "suggestion-entered";
+ }
+
+ let ready = sourceEditor.once(evt);
+ EventUtils.synthesizeKey(key, mods, panelWindow);
+
+ return ready;
+}
+
+function checkState(testCases, index, sourceEditor, popup) {
+ let deferred = defer();
+ executeSoon(() => {
+ let [, details] = testCases[index];
+ details = details || {};
+ let {total, current, inserted} = details;
+
+ if (total != undefined) {
+ ok(popup.isOpen, "Popup is open for index " + index);
+ is(total, popup.itemCount,
+ "Correct total suggestions for index " + index);
+ is(current, popup.selectedIndex,
+ "Correct index is selected for index " + index);
+ if (inserted) {
+ let { text } = popup.getItemAtIndex(current);
+ let { line, ch } = sourceEditor.getCursor();
+ let lineText = sourceEditor.getText(line);
+ is(lineText.substring(ch - text.length, ch), text,
+ "Current suggestion from the popup is inserted into the editor.");
+ }
+ } else {
+ ok(!popup.isOpen, "Popup is closed for index " + index);
+ if (inserted) {
+ let { text } = popup.getItemAtIndex(current);
+ let { line, ch } = sourceEditor.getCursor();
+ let lineText = sourceEditor.getText(line);
+ is(lineText.substring(ch - text.length, ch), text,
+ "Current suggestion from the popup is inserted into the editor.");
+ }
+ }
+ deferred.resolve();
+ });
+
+ return deferred.promise;
+}
+
+/**
+ * Returns a list of all property names and a map of property name vs possible
+ * CSS values provided by the Gecko engine.
+ *
+ * @return {Object} An object with following properties:
+ * - CSSProperties {Array} Array of string containing all the possible
+ * CSS property names.
+ * - CSSValues {Object|Map} A map where key is the property name and
+ * value is an array of string containing all the possible
+ * CSS values the property can have.
+ */
+function getCSSKeywords(cssProperties) {
+ let props = {};
+ let propNames = cssProperties.getNames();
+ propNames.forEach(prop => {
+ props[prop] = cssProperties.getValues(prop).sort();
+ });
+ return {
+ CSSValues: props,
+ CSSProperties: propNames.sort()
+ };
+}
+
+/**
+ * Returns a function that returns the number of expected suggestions for the given
+ * property and value. If the value is not null, returns the number of values starting
+ * with `value`. Returns the number of properties starting with `property` otherwise.
+ */
+function suggestionNumberGetter({CSSProperties, CSSValues}) {
+ return (property, value) => {
+ if (value == null) {
+ return CSSProperties.filter(prop => prop.startsWith(property))
+ .slice(0, MAX_SUGGESTIONS).length;
+ }
+ return CSSValues[property].filter(val => val.startsWith(value))
+ .slice(0, MAX_SUGGESTIONS).length;
+ };
+}
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_bom.js b/devtools/client/styleeditor/test/browser_styleeditor_bom.js
new file mode 100644
index 000000000..56275a3a9
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_bom.js
@@ -0,0 +1,34 @@
+/* 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 BOM_CSS = TEST_BASE_HTTPS + "utf-16.css";
+const DOCUMENT = "data:text/html;charset=UTF-8," +
+ encodeURIComponent(
+ ["<!DOCTYPE html>",
+ "<html>",
+ " <head>",
+ " <title>Bug 1301854</title>",
+ ' <link rel="stylesheet" type="text/css" href="' + BOM_CSS + '">',
+ " </head>",
+ " <body>",
+ " </body>",
+ "</html>"
+ ].join("\n"));
+
+const CONTENTS = "// Note that this file must be utf-16 with a " +
+ "BOM for the test to make sense.\n";
+
+add_task(function* () {
+ let {ui} = yield openStyleEditorForURL(DOCUMENT);
+
+ is(ui.editors.length, 1, "correct number of editors");
+
+ let editor = ui.editors[0];
+ yield editor.getSourceEditor();
+
+ let text = editor.sourceEditor.getText();
+ is(text, CONTENTS, "editor contains expected text");
+});
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_bug_740541_iframes.js b/devtools/client/styleeditor/test/browser_styleeditor_bug_740541_iframes.js
new file mode 100644
index 000000000..1a6031013
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_bug_740541_iframes.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that sheets inside iframes are shown in the editor.
+
+add_task(function* () {
+ function makeStylesheet(selector) {
+ return ("data:text/css;charset=UTF-8," +
+ encodeURIComponent(selector + " { }"));
+ }
+
+ function makeDocument(stylesheets, framedDocuments) {
+ stylesheets = stylesheets || [];
+ framedDocuments = framedDocuments || [];
+ return "data:text/html;charset=UTF-8," + encodeURIComponent(
+ Array.prototype.concat.call(
+ ["<!DOCTYPE html>",
+ "<html>",
+ "<head>",
+ "<title>Bug 740541</title>"],
+ stylesheets.map(function (sheet) {
+ return '<link rel="stylesheet" type="text/css" href="' + sheet + '">';
+ }),
+ ["</head>",
+ "<body>"],
+ framedDocuments.map(function (doc) {
+ return '<iframe src="' + doc + '"></iframe>';
+ }),
+ ["</body>",
+ "</html>"]
+ ).join("\n"));
+ }
+
+ const DOCUMENT_WITH_INLINE_STYLE = "data:text/html;charset=UTF-8," +
+ encodeURIComponent(
+ ["<!DOCTYPE html>",
+ "<html>",
+ " <head>",
+ " <title>Bug 740541</title>",
+ ' <style type="text/css">',
+ " .something {",
+ " }",
+ " </style>",
+ " </head>",
+ " <body>",
+ " </body>",
+ " </html>"
+ ].join("\n"));
+
+ const FOUR = TEST_BASE_HTTP + "four.html";
+
+ const SIMPLE = TEST_BASE_HTTP + "simple.css";
+
+ const SIMPLE_DOCUMENT = TEST_BASE_HTTP + "simple.html";
+
+ const TESTCASE_URI = makeDocument(
+ [makeStylesheet(".a")],
+ [makeDocument([],
+ [FOUR,
+ DOCUMENT_WITH_INLINE_STYLE]),
+ makeDocument([makeStylesheet(".b"),
+ SIMPLE],
+ [makeDocument([makeStylesheet(".c")],
+ [])]),
+ makeDocument([SIMPLE], []),
+ SIMPLE_DOCUMENT
+ ]);
+
+ const EXPECTED_STYLE_SHEET_COUNT = 12;
+
+ let { ui } = yield openStyleEditorForURL(TESTCASE_URI);
+
+ is(ui.editors.length, EXPECTED_STYLE_SHEET_COUNT,
+ "Got the expected number of style sheets.");
+});
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_bug_851132_middle_click.js b/devtools/client/styleeditor/test/browser_styleeditor_bug_851132_middle_click.js
new file mode 100644
index 000000000..a666156b1
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_bug_851132_middle_click.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that middle click on style sheet doesn't open styleeditor.xul in a new
+// tab (bug 851132).
+
+const TESTCASE_URI = TEST_BASE_HTTP + "four.html";
+
+add_task(function* () {
+ let { ui } = yield openStyleEditorForURL(TESTCASE_URI);
+ gBrowser.tabContainer.addEventListener("TabOpen", onTabAdded, false);
+
+ yield ui.editors[0].getSourceEditor();
+ info("first editor selected");
+
+ info("Left-clicking on the second editor link.");
+ yield clickOnStyleSheetLink(ui.editors[1], 0);
+
+ info("Waiting for the second editor to be selected.");
+ let editor = yield ui.once("editor-selected");
+
+ ok(editor.sourceEditor.hasFocus(),
+ "Left mouse click gave second editor focus.");
+
+ // middle mouse click should not open a new tab
+ info("Middle clicking on the third editor link.");
+ yield clickOnStyleSheetLink(ui.editors[2], 1);
+});
+
+/**
+ * A helper that clicks on style sheet link in the sidebar.
+ *
+ * @param {StyleSheetEditor} editor
+ * The editor of which link should be clicked.
+ * @param {MouseEvent.button} button
+ * The button to click the link with.
+ */
+function* clickOnStyleSheetLink(editor, button) {
+ let window = editor._window;
+ let link = editor.summary.querySelector(".stylesheet-name");
+
+ info("Waiting for focus.");
+ yield SimpleTest.promiseFocus(window);
+
+ info("Pressing button " + button + " on style sheet name link.");
+ EventUtils.synthesizeMouseAtCenter(link, { button }, window);
+}
+
+function onTabAdded() {
+ ok(false, "middle mouse click has opened a new tab");
+}
+
+registerCleanupFunction(function () {
+ gBrowser.tabContainer.removeEventListener("TabOpen", onTabAdded, false);
+});
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_bug_870339.js b/devtools/client/styleeditor/test/browser_styleeditor_bug_870339.js
new file mode 100644
index 000000000..9b0b7b3f4
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_bug_870339.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const SIMPLE = TEST_BASE_HTTP + "simple.css";
+const DOCUMENT_WITH_ONE_STYLESHEET = "data:text/html;charset=UTF-8," +
+ encodeURIComponent(
+ ["<!DOCTYPE html>",
+ "<html>",
+ " <head>",
+ " <title>Bug 870339</title>",
+ ' <link rel="stylesheet" type="text/css" href="' + SIMPLE + '">',
+ " </head>",
+ " <body>",
+ " </body>",
+ "</html>"
+ ].join("\n"));
+
+add_task(function* () {
+ let { ui } = yield openStyleEditorForURL(DOCUMENT_WITH_ONE_STYLESHEET);
+
+ // Spam the _onNewDocument callback multiple times before the
+ // StyleEditorActor has a chance to respond to the first one.
+ const SPAM_COUNT = 2;
+ for (let i = 0; i < SPAM_COUNT; ++i) {
+ ui._onNewDocument();
+ }
+
+ // Wait for the StyleEditorActor to respond to each "newDocument"
+ // message.
+ yield new Promise(resolve => {
+ let loadCount = 0;
+ ui.on("stylesheets-reset", function onReset() {
+ ++loadCount;
+ if (loadCount == SPAM_COUNT) {
+ ui.off("stylesheets-reset", onReset);
+ // No matter how large SPAM_COUNT is, the number of style
+ // sheets should never be more than the number of style sheets
+ // in the document.
+ is(ui.editors.length, 1, "correct style sheet count");
+ resolve();
+ }
+ });
+ });
+});
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_cmd_edit.html b/devtools/client/styleeditor/test/browser_styleeditor_cmd_edit.html
new file mode 100644
index 000000000..9907f0474
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_cmd_edit.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<html>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<head>
+ <meta charset="utf-8">
+ <title>Resources</title>
+ <script type="text/javascript;version=1.8" id="script1">
+ "use strict";
+
+ window.addEventListener("load", function onload() {
+ let pid = document.getElementById("pid");
+ let div = document.createElement("div");
+ div.id = "divid";
+ div.classList.add("divclass");
+ div.appendChild(document.createTextNode("div"));
+ div.setAttribute("data-a1", "div");
+ pid.parentNode.appendChild(div);
+ });
+ </script>
+ <script src="resources_inpage.jsi"></script>
+ <link rel="stylesheet" type="text/css" href="resources_inpage1.css"/>
+ <link rel="stylesheet" type="text/css" href="resources_inpage2.css"/>
+ <style type="text/css">
+ p { color: #800; }
+ div { color: #008; }
+ h4 { color: #080; }
+ h3 { color: #880; }
+ </style>
+</head>
+<body>
+ <style type="text/css" id=style2>
+ .pclass { background-color: #FEE; }
+ .divclass { background-color: #EEF; }
+ .h4class { background-color: #EFE; }
+ .h3class { background-color: #FFE; }
+ </style>
+
+ <p class="pclass" id="pid" data-a1="p">paragraph</p>
+
+ <script>
+ "use strict";
+ let pid = document.getElementById("pid");
+ let h4 = document.createElement("h4");
+ h4.id = "h4id";
+ h4.classList.add("h4class");
+ h4.appendChild(document.createTextNode("h4"));
+ h4.setAttribute("data-a1", "h4");
+ pid.parentNode.appendChild(h4);
+ </script>
+
+</body>
+</html>
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_cmd_edit.js b/devtools/client/styleeditor/test/browser_styleeditor_cmd_edit.js
new file mode 100644
index 000000000..96b64cc94
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_cmd_edit.js
@@ -0,0 +1,215 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the edit command works
+
+// Import the GCLI test helper
+/* import-globals-from ../../commandline/test/helpers.js */
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/commandline/test/helpers.js",
+ this);
+
+const TEST_URI = "http://example.com/browser/devtools/client/styleeditor/" +
+ "test/browser_styleeditor_cmd_edit.html";
+
+add_task(function* () {
+ let options = yield helpers.openTab(TEST_URI);
+ yield helpers.openToolbar(options);
+
+ yield helpers.audit(options, [
+ {
+ setup: "edit",
+ check: {
+ input: "edit",
+ hints: " <resource> [line]",
+ markup: "VVVV",
+ status: "ERROR",
+ args: {
+ resource: { status: "INCOMPLETE" },
+ line: { status: "VALID" },
+ }
+ },
+ },
+ {
+ setup: "edit i",
+ check: {
+ input: "edit i",
+ hints: "nline-css [line]",
+ markup: "VVVVVI",
+ status: "ERROR",
+ args: {
+ resource: { arg: " i", status: "INCOMPLETE" },
+ line: { status: "VALID" },
+ }
+ },
+ },
+ {
+ setup: "edit c",
+ check: {
+ input: "edit c",
+ hints: "ss#style2 [line]",
+ markup: "VVVVVI",
+ status: "ERROR",
+ args: {
+ resource: { arg: " c", status: "INCOMPLETE" },
+ line: { status: "VALID" },
+ }
+ },
+ },
+ {
+ setup: "edit http",
+ check: {
+ input: "edit http",
+ hints: "://example.com/browser/devtools/client/styleeditor/test/" +
+ "resources_inpage1.css [line]",
+ markup: "VVVVVIIII",
+ status: "ERROR",
+ args: {
+ resource: {
+ arg: " http",
+ status: "INCOMPLETE",
+ message: "Value required for \u2018resource\u2019."
+ },
+ line: { status: "VALID" },
+ }
+ },
+ },
+ {
+ setup: "edit page1",
+ check: {
+ input: "edit page1",
+ hints: " [line] -> http://example.com/browser/devtools/client/" +
+ "styleeditor/test/resources_inpage1.css",
+ markup: "VVVVVIIIII",
+ status: "ERROR",
+ args: {
+ resource: {
+ arg: " page1",
+ status: "INCOMPLETE",
+ message: "Value required for \u2018resource\u2019."
+ },
+ line: { status: "VALID" },
+ }
+ },
+ },
+ {
+ setup: "edit page2",
+ check: {
+ input: "edit page2",
+ hints: " [line] -> http://example.com/browser/devtools/client/" +
+ "styleeditor/test/resources_inpage2.css",
+ markup: "VVVVVIIIII",
+ status: "ERROR",
+ args: {
+ resource: {
+ arg: " page2",
+ status: "INCOMPLETE",
+ message: "Value required for \u2018resource\u2019."
+ },
+ line: { status: "VALID" },
+ }
+ },
+ },
+ {
+ setup: "edit stylez",
+ check: {
+ input: "edit stylez",
+ hints: " [line]",
+ markup: "VVVVVEEEEEE",
+ status: "ERROR",
+ args: {
+ resource: {
+ arg: " stylez",
+ status: "ERROR", message: "Can\u2019t use \u2018stylez\u2019." },
+ line: { status: "VALID" },
+ }
+ },
+ },
+ {
+ setup: "edit css#style2",
+ check: {
+ input: "edit css#style2",
+ hints: " [line]",
+ markup: "VVVVVVVVVVVVVVV",
+ status: "VALID",
+ args: {
+ resource: { arg: " css#style2", status: "VALID", message: "" },
+ line: { status: "VALID" },
+ }
+ },
+ },
+ {
+ setup: "edit css#style2 5",
+ check: {
+ input: "edit css#style2 5",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVVVV",
+ status: "VALID",
+ args: {
+ resource: { arg: " css#style2", status: "VALID", message: "" },
+ line: { value: 5, arg: " 5", status: "VALID" },
+ }
+ },
+ },
+ {
+ setup: "edit css#style2 0",
+ check: {
+ input: "edit css#style2 0",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVVVE",
+ status: "ERROR",
+ args: {
+ resource: { arg: " css#style2", status: "VALID", message: "" },
+ line: {
+ arg: " 0",
+ status: "ERROR",
+ message: "0 is smaller than minimum allowed: 1."
+ },
+ }
+ },
+ },
+ {
+ setup: "edit css#style2 -1",
+ check: {
+ input: "edit css#style2 -1",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVVVEE",
+ status: "ERROR",
+ args: {
+ resource: { arg: " css#style2", status: "VALID", message: "" },
+ line: {
+ arg: " -1",
+ status: "ERROR",
+ message: "-1 is smaller than minimum allowed: 1."
+ },
+ }
+ },
+ }
+ ]);
+
+ let toolbox = gDevTools.getToolbox(options.target);
+ ok(toolbox == null, "toolbox is closed");
+
+ yield helpers.audit(options, [
+ {
+ setup: "edit css#style2",
+ check: {
+ input: "edit css#style2",
+ },
+ exec: { output: "" }
+ },
+ ]);
+
+ toolbox = gDevTools.getToolbox(options.target);
+ ok(toolbox != null, "toolbox is open");
+
+ let styleEditor = toolbox.getCurrentPanel();
+ ok(typeof styleEditor.selectStyleSheet === "function", "styleeditor is open");
+
+ yield toolbox.destroy();
+
+ yield helpers.closeToolbar(options);
+ yield helpers.closeTab(options);
+});
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_enabled.js b/devtools/client/styleeditor/test/browser_styleeditor_enabled.js
new file mode 100644
index 000000000..993225de0
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_enabled.js
@@ -0,0 +1,56 @@
+/* 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";
+
+// Test that style sheets can be disabled and enabled.
+
+// https rather than chrome to improve coverage
+const TESTCASE_URI = TEST_BASE_HTTPS + "simple.html";
+
+add_task(function* () {
+ let { panel, ui } = yield openStyleEditorForURL(TESTCASE_URI);
+ let editor = yield ui.editors[0].getSourceEditor();
+
+ let summary = editor.summary;
+ let enabledToggle = summary.querySelector(".stylesheet-enabled");
+ ok(enabledToggle, "enabled toggle button exists");
+
+ is(editor.styleSheet.disabled, false,
+ "first stylesheet is initially enabled");
+
+ is(summary.classList.contains("disabled"), false,
+ "first stylesheet is initially enabled, UI does not have DISABLED class");
+
+ info("Disabling the first stylesheet.");
+ yield toggleEnabled(editor, enabledToggle, panel.panelWindow);
+
+ is(editor.styleSheet.disabled, true, "first stylesheet is now disabled");
+ is(summary.classList.contains("disabled"), true,
+ "first stylesheet is now disabled, UI has DISABLED class");
+
+ info("Enabling the first stylesheet again.");
+ yield toggleEnabled(editor, enabledToggle, panel.panelWindow);
+
+ is(editor.styleSheet.disabled, false,
+ "first stylesheet is now enabled again");
+ is(summary.classList.contains("disabled"), false,
+ "first stylesheet is now enabled again, UI does not have DISABLED class");
+});
+
+function* toggleEnabled(editor, enabledToggle, panelWindow) {
+ let changed = editor.once("property-change");
+
+ info("Waiting for focus.");
+ yield SimpleTest.promiseFocus(panelWindow);
+
+ info("Clicking on the toggle.");
+ EventUtils.synthesizeMouseAtCenter(enabledToggle, {}, panelWindow);
+
+ info("Waiting for stylesheet to be disabled.");
+ let property = yield changed;
+ while (property !== "disabled") {
+ info("Ignoring property-change for '" + property + "'.");
+ property = yield editor.once("property-change");
+ }
+}
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_fetch-from-cache.js b/devtools/client/styleeditor/test/browser_styleeditor_fetch-from-cache.js
new file mode 100644
index 000000000..7aeb24d69
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_fetch-from-cache.js
@@ -0,0 +1,40 @@
+/* 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";
+
+// A test to ensure Style Editor doesn't bybass cache when loading style sheet
+// contents (bug 978688).
+
+const TEST_URL = TEST_BASE_HTTP + "doc_uncached.html";
+
+add_task(function* () {
+ info("Opening netmonitor");
+ let tab = yield addTab("about:blank");
+ let target = TargetFactory.forTab(tab);
+ let toolbox = yield gDevTools.showToolbox(target, "netmonitor");
+ let netmonitor = toolbox.getPanel("netmonitor");
+ netmonitor._view.RequestsMenu.lazyUpdate = false;
+
+ info("Navigating to test page");
+ yield navigateTo(TEST_URL);
+
+ info("Opening Style Editor");
+ let styleeditor = yield toolbox.selectTool("styleeditor");
+
+ info("Waiting for the source to be loaded.");
+ yield styleeditor.UI.editors[0].getSourceEditor();
+
+ info("Checking Netmonitor contents.");
+ let attachments = [];
+ for (let item of netmonitor._view.RequestsMenu) {
+ if (item.attachment.url.endsWith("doc_uncached.css")) {
+ attachments.push(item.attachment);
+ }
+ }
+
+ is(attachments.length, 2,
+ "Got two requests for doc_uncached.css after Style Editor was loaded.");
+ ok(attachments[1].fromCache,
+ "Second request was loaded from browser cache");
+});
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_filesave.js b/devtools/client/styleeditor/test/browser_styleeditor_filesave.js
new file mode 100644
index 000000000..b04b74c80
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_filesave.js
@@ -0,0 +1,99 @@
+/* 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";
+
+// Test that 'Save' function works.
+
+const TESTCASE_URI_HTML = TEST_BASE_HTTP + "simple.html";
+const TESTCASE_URI_CSS = TEST_BASE_HTTP + "simple.css";
+
+var tempScope = {};
+Components.utils.import("resource://gre/modules/FileUtils.jsm", tempScope);
+Components.utils.import("resource://gre/modules/NetUtil.jsm", tempScope);
+var FileUtils = tempScope.FileUtils;
+var NetUtil = tempScope.NetUtil;
+
+add_task(function* () {
+ let htmlFile = yield copy(TESTCASE_URI_HTML, "simple.html");
+ yield copy(TESTCASE_URI_CSS, "simple.css");
+ let uri = Services.io.newFileURI(htmlFile);
+ let filePath = uri.resolve("");
+
+ let { ui } = yield openStyleEditorForURL(filePath);
+
+ let editor = ui.editors[0];
+ yield editor.getSourceEditor();
+
+ info("Editing the style sheet.");
+ let dirty = editor.sourceEditor.once("dirty-change");
+ let beginCursor = {line: 0, ch: 0};
+ editor.sourceEditor.replaceText("DIRTY TEXT", beginCursor, beginCursor);
+
+ yield dirty;
+
+ is(editor.sourceEditor.isClean(), false, "Editor is dirty.");
+ ok(editor.summary.classList.contains("unsaved"),
+ "Star icon is present in the corresponding summary.");
+
+ info("Saving the changes.");
+ dirty = editor.sourceEditor.once("dirty-change");
+
+ editor.saveToFile(null, function (file) {
+ ok(file, "file should get saved directly when using a file:// URI");
+ });
+
+ yield dirty;
+
+ is(editor.sourceEditor.isClean(), true, "Editor is clean.");
+ ok(!editor.summary.classList.contains("unsaved"),
+ "Star icon is not present in the corresponding summary.");
+});
+
+function copy(srcChromeURL, destFileName) {
+ let deferred = defer();
+ let destFile = FileUtils.getFile("ProfD", [destFileName]);
+ write(read(srcChromeURL), destFile, deferred.resolve);
+
+ return deferred.promise;
+}
+
+function read(srcChromeURL) {
+ let scriptableStream = Cc["@mozilla.org/scriptableinputstream;1"]
+ .getService(Ci.nsIScriptableInputStream);
+
+ let channel = NetUtil.newChannel({
+ uri: srcChromeURL,
+ loadUsingSystemPrincipal: true
+ });
+ let input = channel.open2();
+ scriptableStream.init(input);
+
+ let data = "";
+ while (input.available()) {
+ data = data.concat(scriptableStream.read(input.available()));
+ }
+ scriptableStream.close();
+ input.close();
+
+ return data;
+}
+
+function write(data, file, callback) {
+ let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
+ .createInstance(Ci.nsIScriptableUnicodeConverter);
+
+ converter.charset = "UTF-8";
+
+ let istream = converter.convertToInputStream(data);
+ let ostream = FileUtils.openSafeFileOutputStream(file);
+
+ NetUtil.asyncCopy(istream, ostream, function (status) {
+ if (!Components.isSuccessCode(status)) {
+ info("Couldn't write to " + file.path);
+ return;
+ }
+
+ callback(file);
+ });
+}
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_highlight-selector.js b/devtools/client/styleeditor/test/browser_styleeditor_highlight-selector.js
new file mode 100644
index 000000000..8effbf208
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_highlight-selector.js
@@ -0,0 +1,48 @@
+/* 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";
+
+// Test that hovering over a simple selector in the style-editor requests the
+// highlighting of the corresponding nodes
+
+const TEST_URL = "data:text/html;charset=utf8," +
+ "<style>div{color:red}</style><div>highlighter test</div>";
+
+add_task(function* () {
+ let { ui } = yield openStyleEditorForURL(TEST_URL);
+ let editor = ui.editors[0];
+
+ // Mock the highlighter so we can locally assert that things happened
+ // correctly instead of accessing the highlighter elements
+ editor.highlighter = {
+ isShown: false,
+ options: null,
+
+ show: function (node, options) {
+ this.isShown = true;
+ this.options = options;
+ return promise.resolve();
+ },
+
+ hide: function () {
+ this.isShown = false;
+ }
+ };
+
+ info("Expecting a node-highlighted event");
+ let onHighlighted = editor.once("node-highlighted");
+
+ info("Simulate a mousemove event on the div selector");
+ editor._onMouseMove({clientX: 56, clientY: 10});
+ yield onHighlighted;
+
+ ok(editor.highlighter.isShown, "The highlighter is now shown");
+ is(editor.highlighter.options.selector, "div", "The selector is correct");
+
+ info("Simulate a mousemove event elsewhere in the editor");
+ editor._onMouseMove({clientX: 16, clientY: 0});
+
+ ok(!editor.highlighter.isShown, "The highlighter is now hidden");
+});
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_import.js b/devtools/client/styleeditor/test/browser_styleeditor_import.js
new file mode 100644
index 000000000..f31f72ce7
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_import.js
@@ -0,0 +1,55 @@
+/* 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";
+
+// Test that the import button in the UI works.
+
+// http rather than chrome to improve coverage
+const TESTCASE_URI = TEST_BASE_HTTP + "simple.html";
+
+var tempScope = {};
+Components.utils.import("resource://gre/modules/FileUtils.jsm", tempScope);
+var FileUtils = tempScope.FileUtils;
+
+const FILENAME = "styleeditor-import-test.css";
+const SOURCE = "body{background:red;}";
+
+add_task(function* () {
+ let { panel, ui } = yield openStyleEditorForURL(TESTCASE_URI);
+
+ let added = ui.once("editor-added");
+ importSheet(ui, panel.panelWindow);
+
+ info("Waiting for editor to be added for the imported sheet.");
+ let editor = yield added;
+
+ is(editor.savedFile.leafName, FILENAME,
+ "imported stylesheet will be saved directly into the same file");
+ is(editor.friendlyName, FILENAME,
+ "imported stylesheet has the same name as the filename");
+});
+
+function importSheet(ui, panelWindow) {
+ // create file to import first
+ let file = FileUtils.getFile("ProfD", [FILENAME]);
+ let ostream = FileUtils.openSafeFileOutputStream(file);
+ let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
+ .createInstance(Ci.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+ let istream = converter.convertToInputStream(SOURCE);
+ NetUtil.asyncCopy(istream, ostream, function () {
+ FileUtils.closeSafeFileOutputStream(ostream);
+
+ // click the import button now that the file to import is ready
+ ui._mockImportFile = file;
+
+ waitForFocus(function () {
+ let document = panelWindow.document;
+ let importButton = document.querySelector(".style-editor-importButton");
+ ok(importButton, "import button exists");
+
+ EventUtils.synthesizeMouseAtCenter(importButton, {}, panelWindow);
+ }, panelWindow);
+ });
+}
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_import_rule.js b/devtools/client/styleeditor/test/browser_styleeditor_import_rule.js
new file mode 100644
index 000000000..46d0a0968
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_import_rule.js
@@ -0,0 +1,25 @@
+/* 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";
+
+// Test that style editor shows sheets loaded with @import rules.
+
+// http rather than chrome to improve coverage
+const TESTCASE_URI = TEST_BASE_HTTP + "import.html";
+
+add_task(function* () {
+ let { ui } = yield openStyleEditorForURL(TESTCASE_URI);
+
+ is(ui.editors.length, 3,
+ "there are 3 stylesheets after loading @imports");
+
+ is(ui.editors[0].styleSheet.href, TEST_BASE_HTTP + "simple.css",
+ "stylesheet 1 is simple.css");
+
+ is(ui.editors[1].styleSheet.href, TEST_BASE_HTTP + "import.css",
+ "stylesheet 2 is import.css");
+
+ is(ui.editors[2].styleSheet.href, TEST_BASE_HTTP + "import2.css",
+ "stylesheet 3 is import2.css");
+});
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_init.js b/devtools/client/styleeditor/test/browser_styleeditor_init.js
new file mode 100644
index 000000000..47a8f1e05
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_init.js
@@ -0,0 +1,45 @@
+"use strict";
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Checks that style editor contains correct stylesheets after initialization.
+
+const TESTCASE_URI = TEST_BASE_HTTP + "simple.html";
+const EXPECTED_SHEETS = [
+ {
+ sheetIndex: 0,
+ name: /^simple.css$/,
+ rules: 1,
+ active: true
+ }, {
+ sheetIndex: 1,
+ name: /^<.*>$/,
+ rules: 3,
+ active: false
+ }
+];
+
+add_task(function* () {
+ let { ui } = yield openStyleEditorForURL(TESTCASE_URI);
+
+ is(ui.editors.length, 2, "The UI contains two style sheets.");
+ checkSheet(ui.editors[0], EXPECTED_SHEETS[0]);
+ checkSheet(ui.editors[1], EXPECTED_SHEETS[1]);
+});
+
+function checkSheet(editor, expected) {
+ is(editor.styleSheet.styleSheetIndex, expected.sheetIndex,
+ "Style sheet has correct index.");
+
+ let summary = editor.summary;
+ let name = summary.querySelector(".stylesheet-name > label")
+ .getAttribute("value");
+ ok(expected.name.test(name), "The name '" + name + "' is correct.");
+
+ let ruleCount = summary.querySelector(".stylesheet-rule-count").textContent;
+ is(parseInt(ruleCount, 10), expected.rules, "the rule count is correct");
+
+ is(summary.classList.contains("splitview-active"), expected.active,
+ "The active status for this sheet is correct.");
+}
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_inline_friendly_names.js b/devtools/client/styleeditor/test/browser_styleeditor_inline_friendly_names.js
new file mode 100644
index 000000000..fcf1192b5
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_inline_friendly_names.js
@@ -0,0 +1,88 @@
+/* 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";
+
+// Test that inline style sheets get correct names if they are saved to disk and
+// that those names survice a reload but not navigation to another page.
+
+const FIRST_TEST_PAGE = TEST_BASE_HTTP + "inline-1.html";
+const SECOND_TEST_PAGE = TEST_BASE_HTTP + "inline-2.html";
+const SAVE_PATH = "test.css";
+
+add_task(function* () {
+ let { ui } = yield openStyleEditorForURL(FIRST_TEST_PAGE);
+
+ testIndentifierGeneration(ui);
+
+ yield saveFirstInlineStyleSheet(ui);
+ yield testFriendlyNamesAfterSave(ui);
+ yield reloadPageAndWaitForStyleSheets(ui);
+ yield testFriendlyNamesAfterSave(ui);
+ yield navigateToAndWaitForStyleSheets(SECOND_TEST_PAGE, ui);
+ yield testFriendlyNamesAfterNavigation(ui);
+});
+
+function testIndentifierGeneration(ui) {
+ let fakeStyleSheetFile = {
+ "href": "http://example.com/test.css",
+ "nodeHref": "http://example.com/",
+ "styleSheetIndex": 1
+ };
+
+ let fakeInlineStyleSheet = {
+ "href": null,
+ "nodeHref": "http://example.com/",
+ "styleSheetIndex": 2
+ };
+
+ is(ui.getStyleSheetIdentifier(fakeStyleSheetFile),
+ "http://example.com/test.css",
+ "URI is the identifier of style sheet file.");
+
+ is(ui.getStyleSheetIdentifier(fakeInlineStyleSheet),
+ "inline-2-at-http://example.com/",
+ "Inline sheets are identified by their page and position in the page.");
+}
+
+function saveFirstInlineStyleSheet(ui) {
+ let deferred = defer();
+ let editor = ui.editors[0];
+
+ let destFile = FileUtils.getFile("ProfD", [SAVE_PATH]);
+
+ editor.saveToFile(destFile, function (file) {
+ ok(file, "File was correctly saved.");
+ deferred.resolve();
+ });
+
+ return deferred.promise;
+}
+
+function testFriendlyNamesAfterSave(ui) {
+ let firstEditor = ui.editors[0];
+ let secondEditor = ui.editors[1];
+
+ // The friendly name of first sheet should've been remembered, the second
+ // should not be the same (bug 969900).
+ is(firstEditor.friendlyName, SAVE_PATH,
+ "Friendly name is correct for the saved inline style sheet.");
+ isnot(secondEditor.friendlyName, SAVE_PATH,
+ "Friendly name for the second inline sheet isn't the same as the first.");
+
+ return promise.resolve(null);
+}
+
+function testFriendlyNamesAfterNavigation(ui) {
+ let firstEditor = ui.editors[0];
+ let secondEditor = ui.editors[1];
+
+ // Inline style sheets shouldn't have the name of previously saved file as the
+ // page is different.
+ isnot(firstEditor.friendlyName, SAVE_PATH,
+ "The first editor doesn't have the save path as a friendly name.");
+ isnot(secondEditor.friendlyName, SAVE_PATH,
+ "The second editor doesn't have the save path as a friendly name.");
+
+ return promise.resolve(null);
+}
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_loading.js b/devtools/client/styleeditor/test/browser_styleeditor_loading.js
new file mode 100644
index 000000000..4657a0dce
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_loading.js
@@ -0,0 +1,36 @@
+/* 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";
+
+// Test that style editor loads correctly.
+
+const TESTCASE_URI = TEST_BASE_HTTP + "longload.html";
+
+add_task(function* () {
+ // launch Style Editor right when the tab is created (before load)
+ // this checks that the Style Editor still launches correctly when it is
+ // opened *while* the page is still loading. The Style Editor should not
+ // signal that it is loaded until the accompanying content page is loaded.
+ let tabAdded = addTab(TESTCASE_URI);
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ let styleEditorLoaded = gDevTools.showToolbox(target, "styleeditor");
+
+ yield Promise.all([tabAdded, styleEditorLoaded]);
+
+ let toolbox = gDevTools.getToolbox(target);
+ let panel = toolbox.getPanel("styleeditor");
+ let { panelWindow } = panel;
+
+ let root = panelWindow.document.querySelector(".splitview-root");
+ ok(!root.classList.contains("loading"),
+ "style editor root element does not have 'loading' class name anymore");
+
+ let button = panelWindow.document.querySelector(".style-editor-newButton");
+ ok(!button.hasAttribute("disabled"),
+ "new style sheet button is enabled");
+
+ button = panelWindow.document.querySelector(".style-editor-importButton");
+ ok(!button.hasAttribute("disabled"),
+ "import button is enabled");
+});
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_loading_with_containers.js b/devtools/client/styleeditor/test/browser_styleeditor_loading_with_containers.js
new file mode 100644
index 000000000..a00628c8b
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_loading_with_containers.js
@@ -0,0 +1,63 @@
+/* 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";
+
+// Test that the stylesheets can be loaded correctly with containers
+// (bug 1282660).
+
+const TESTCASE_URI = TEST_BASE_HTTP + "simple.html";
+const EXPECTED_SHEETS = [
+ {
+ sheetIndex: 0,
+ name: /^simple.css$/,
+ rules: 1,
+ active: true
+ }, {
+ sheetIndex: 1,
+ name: /^<.*>$/,
+ rules: 3,
+ active: false
+ }
+];
+
+add_task(function* () {
+ // Using the personal container.
+ let userContextId = 1;
+ let { tab } = yield* openTabInUserContext(TESTCASE_URI, userContextId);
+ let { ui } = yield openStyleEditor(tab);
+
+ is(ui.editors.length, 2, "The UI contains two style sheets.");
+ checkSheet(ui.editors[0], EXPECTED_SHEETS[0]);
+ checkSheet(ui.editors[1], EXPECTED_SHEETS[1]);
+});
+
+function* openTabInUserContext(uri, userContextId) {
+ // Open the tab in the correct userContextId.
+ let tab = gBrowser.addTab(uri, {userContextId});
+
+ // Select tab and make sure its browser is focused.
+ gBrowser.selectedTab = tab;
+ tab.ownerDocument.defaultView.focus();
+
+ let browser = gBrowser.getBrowserForTab(tab);
+ yield BrowserTestUtils.browserLoaded(browser);
+ return {tab, browser};
+}
+
+function checkSheet(editor, expected) {
+ is(editor.styleSheet.styleSheetIndex, expected.sheetIndex,
+ "Style sheet has correct index.");
+
+ let summary = editor.summary;
+ let name = summary.querySelector(".stylesheet-name > label")
+ .getAttribute("value");
+ ok(expected.name.test(name), "The name '" + name + "' is correct.");
+
+ let ruleCount = summary.querySelector(".stylesheet-rule-count").textContent;
+ is(parseInt(ruleCount, 10), expected.rules, "the rule count is correct");
+
+ is(summary.classList.contains("splitview-active"), expected.active,
+ "The active status for this sheet is correct.");
+}
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_media_sidebar.js b/devtools/client/styleeditor/test/browser_styleeditor_media_sidebar.js
new file mode 100644
index 000000000..15895fac4
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_media_sidebar.js
@@ -0,0 +1,143 @@
+/* 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";
+
+// https rather than chrome to improve coverage
+const TESTCASE_URI = TEST_BASE_HTTPS + "media-rules.html";
+const MEDIA_PREF = "devtools.styleeditor.showMediaSidebar";
+
+const RESIZE = 300;
+const LABELS = ["not all", "all", "(max-width: 400px)",
+ "(min-height: 300px) and (max-height: 320px)",
+ "(max-width: 600px)"];
+const LINE_NOS = [1, 7, 19, 25, 31];
+const NEW_RULE = "\n@media (max-width: 600px) { div { color: blue; } }";
+
+waitForExplicitFinish();
+
+add_task(function* () {
+ let { ui } = yield openStyleEditorForURL(TESTCASE_URI);
+
+ is(ui.editors.length, 2, "correct number of editors");
+
+ // Test first plain css editor
+ let plainEditor = ui.editors[0];
+ yield openEditor(plainEditor);
+ testPlainEditor(plainEditor);
+
+ // Test editor with @media rules
+ let mediaEditor = ui.editors[1];
+ yield openEditor(mediaEditor);
+ testMediaEditor(mediaEditor);
+
+ // Test that sidebar hides when flipping pref
+ yield testShowHide(ui, mediaEditor);
+
+ // Test adding a rule updates the list
+ yield testMediaRuleAdded(ui, mediaEditor);
+
+ // Test resizing and seeing @media matching state change
+ let originalWidth = window.outerWidth;
+ let originalHeight = window.outerHeight;
+
+ let onMatchesChange = listenForMediaChange(ui);
+ window.resizeTo(RESIZE, RESIZE);
+ yield onMatchesChange;
+
+ testMediaMatchChanged(mediaEditor);
+
+ window.resizeTo(originalWidth, originalHeight);
+});
+
+function testPlainEditor(editor) {
+ let sidebar = editor.details.querySelector(".stylesheet-sidebar");
+ is(sidebar.hidden, true, "sidebar is hidden on editor without @media");
+}
+
+function testMediaEditor(editor) {
+ let sidebar = editor.details.querySelector(".stylesheet-sidebar");
+ is(sidebar.hidden, false, "sidebar is showing on editor with @media");
+
+ let entries = [...sidebar.querySelectorAll(".media-rule-label")];
+ is(entries.length, 4, "four @media rules displayed in sidebar");
+
+ testRule(entries[0], LABELS[0], false, LINE_NOS[0]);
+ testRule(entries[1], LABELS[1], true, LINE_NOS[1]);
+ testRule(entries[2], LABELS[2], false, LINE_NOS[2]);
+ testRule(entries[3], LABELS[3], false, LINE_NOS[3]);
+}
+
+function testMediaMatchChanged(editor) {
+ let sidebar = editor.details.querySelector(".stylesheet-sidebar");
+
+ let cond = sidebar.querySelectorAll(".media-rule-condition")[2];
+ is(cond.textContent, "(max-width: 400px)",
+ "third rule condition text is correct");
+ ok(!cond.classList.contains("media-condition-unmatched"),
+ "media rule is now matched after resizing");
+}
+
+function* testShowHide(UI, editor) {
+ let sidebarChange = listenForMediaChange(UI);
+ Services.prefs.setBoolPref(MEDIA_PREF, false);
+ yield sidebarChange;
+
+ let sidebar = editor.details.querySelector(".stylesheet-sidebar");
+ is(sidebar.hidden, true, "sidebar is hidden after flipping pref");
+
+ sidebarChange = listenForMediaChange(UI);
+ Services.prefs.clearUserPref(MEDIA_PREF);
+ yield sidebarChange;
+
+ is(sidebar.hidden, false, "sidebar is showing after flipping pref back");
+}
+
+function* testMediaRuleAdded(UI, editor) {
+ yield editor.getSourceEditor();
+ let text = editor.sourceEditor.getText();
+ text += NEW_RULE;
+
+ let listChange = listenForMediaChange(UI);
+ editor.sourceEditor.setText(text);
+ yield listChange;
+
+ let sidebar = editor.details.querySelector(".stylesheet-sidebar");
+ let entries = [...sidebar.querySelectorAll(".media-rule-label")];
+ is(entries.length, 5, "five @media rules after changing text");
+
+ testRule(entries[4], LABELS[4], false, LINE_NOS[4]);
+}
+
+function testRule(rule, text, matches, lineno) {
+ let cond = rule.querySelector(".media-rule-condition");
+ is(cond.textContent, text, "media label is correct for " + text);
+
+ let matched = !cond.classList.contains("media-condition-unmatched");
+ ok(matches ? matched : !matched,
+ "media rule is " + (matches ? "matched" : "unmatched"));
+
+ let line = rule.querySelector(".media-rule-line");
+ is(line.textContent, ":" + lineno, "correct line number shown");
+}
+
+/* Helpers */
+
+function openEditor(editor) {
+ getLinkFor(editor).click();
+
+ return editor.getSourceEditor();
+}
+
+function listenForMediaChange(UI) {
+ let deferred = defer();
+ UI.once("media-list-changed", () => {
+ deferred.resolve();
+ });
+ return deferred.promise;
+}
+
+function getLinkFor(editor) {
+ return editor.summary.querySelector(".stylesheet-name");
+}
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_media_sidebar_links.js b/devtools/client/styleeditor/test/browser_styleeditor_media_sidebar_links.js
new file mode 100644
index 000000000..85c41b9dc
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_media_sidebar_links.js
@@ -0,0 +1,144 @@
+/* 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";
+
+/* Tests responsive mode links for
+ * @media sidebar width and height related conditions */
+
+const asyncStorage = require("devtools/shared/async-storage");
+Services.prefs.setCharPref("devtools.devices.url",
+ "http://example.com/browser/devtools/client/responsive.html/test/browser/devices.json");
+
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("devtools.devices.url");
+ asyncStorage.removeItem("devtools.devices.url_cache");
+});
+
+const mgr = "resource://devtools/client/responsivedesign/responsivedesign.jsm";
+const {ResponsiveUIManager} = Cu.import(mgr, {});
+const TESTCASE_URI = TEST_BASE_HTTPS + "media-rules.html";
+const responsiveModeToggleClass = ".media-responsive-mode-toggle";
+
+add_task(function* () {
+ let {ui} = yield openStyleEditorForURL(TESTCASE_URI);
+
+ let editor = ui.editors[1];
+ yield openEditor(editor);
+
+ let tab = gBrowser.selectedTab;
+ testNumberOfLinks(editor);
+ yield testMediaLink(editor, tab, ui, 2, "width", 400);
+ yield testMediaLink(editor, tab, ui, 3, "height", 300);
+
+ yield closeRDM(tab, ui);
+ doFinalChecks(editor);
+});
+
+function testNumberOfLinks(editor) {
+ let sidebar = editor.details.querySelector(".stylesheet-sidebar");
+ let conditions = sidebar.querySelectorAll(".media-rule-condition");
+
+ info("Testing if media rules have the appropriate number of links");
+ ok(!conditions[0].querySelector(responsiveModeToggleClass),
+ "There should be no links in the first media rule.");
+ ok(!conditions[1].querySelector(responsiveModeToggleClass),
+ "There should be no links in the second media rule.");
+ ok(conditions[2].querySelector(responsiveModeToggleClass),
+ "There should be 1 responsive mode link in the media rule");
+ is(conditions[3].querySelectorAll(responsiveModeToggleClass).length, 2,
+ "There should be 2 responsive mode links in the media rule");
+}
+
+function* testMediaLink(editor, tab, ui, itemIndex, type, value) {
+ let sidebar = editor.details.querySelector(".stylesheet-sidebar");
+ let conditions = sidebar.querySelectorAll(".media-rule-condition");
+
+ let onMediaChange = once(ui, "media-list-changed");
+
+ info("Launching responsive mode");
+ conditions[itemIndex].querySelector(responsiveModeToggleClass).click();
+
+ let rdmUI = ResponsiveUIManager.getResponsiveUIForTab(tab);
+ let onContentResize = waitForResizeTo(rdmUI, type, value);
+ rdmUI.transitionsEnabled = false;
+
+ info("Waiting for the @media list to update");
+ yield onMediaChange;
+ yield onContentResize;
+
+ ok(ResponsiveUIManager.isActiveForTab(tab),
+ "Responsive mode should be active.");
+ conditions = sidebar.querySelectorAll(".media-rule-condition");
+ ok(!conditions[itemIndex].classList.contains("media-condition-unmatched"),
+ "media rule should now be matched after responsive mode is active");
+
+ let dimension = (yield getSizing(rdmUI))[type];
+ is(dimension, value, `${type} should be properly set.`);
+}
+
+function* closeRDM(tab, ui) {
+ info("Closing responsive mode");
+ ResponsiveUIManager.toggle(window, tab);
+ let onMediaChange = waitForNEvents(ui, "media-list-changed", 2);
+ yield once(ResponsiveUIManager, "off");
+ yield onMediaChange;
+ ok(!ResponsiveUIManager.isActiveForTab(tab),
+ "Responsive mode should no longer be active.");
+}
+
+function doFinalChecks(editor) {
+ let sidebar = editor.details.querySelector(".stylesheet-sidebar");
+ let conditions = sidebar.querySelectorAll(".media-rule-condition");
+ conditions = sidebar.querySelectorAll(".media-rule-condition");
+ ok(conditions[2].classList.contains("media-condition-unmatched"),
+ "The width condition should now be unmatched");
+ ok(conditions[3].classList.contains("media-condition-unmatched"),
+ "The height condition should now be unmatched");
+}
+
+/* Helpers */
+function waitForResizeTo(rdmUI, type, value) {
+ return new Promise(resolve => {
+ let onResize = (_, data) => {
+ if (data[type] != value) {
+ return;
+ }
+ ResponsiveUIManager.off("content-resize", onResize);
+ if (rdmUI.off) {
+ rdmUI.off("content-resize", onResize);
+ }
+ info(`Got content-resize to a ${type} of ${value}`);
+ resolve();
+ };
+ info(`Waiting for content-resize to a ${type} of ${value}`);
+ // Old RDM emits on manager
+ ResponsiveUIManager.on("content-resize", onResize);
+ // New RDM emits on ui
+ if (rdmUI.on) {
+ rdmUI.on("content-resize", onResize);
+ }
+ });
+}
+
+function* getSizing(rdmUI) {
+ let browser = rdmUI.getViewportBrowser();
+ let sizing = yield ContentTask.spawn(browser, {}, function* () {
+ return {
+ width: content.innerWidth,
+ height: content.innerHeight
+ };
+ });
+ return sizing;
+}
+
+function openEditor(editor) {
+ getLinkFor(editor).click();
+
+ return editor.getSourceEditor();
+}
+
+function getLinkFor(editor) {
+ return editor.summary.querySelector(".stylesheet-name");
+}
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_media_sidebar_sourcemaps.js b/devtools/client/styleeditor/test/browser_styleeditor_media_sidebar_sourcemaps.js
new file mode 100644
index 000000000..9214d93fd
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_media_sidebar_sourcemaps.js
@@ -0,0 +1,71 @@
+/* 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";
+
+// https rather than chrome to improve coverage
+const TESTCASE_URI = TEST_BASE_HTTPS + "media-rules-sourcemaps.html";
+const MAP_PREF = "devtools.styleeditor.source-maps-enabled";
+
+const LABELS = ["screen and (max-width: 320px)",
+ "screen and (min-width: 1200px)"];
+const LINE_NOS = [5, 8];
+
+waitForExplicitFinish();
+
+add_task(function* () {
+ Services.prefs.setBoolPref(MAP_PREF, true);
+
+ let { ui } = yield openStyleEditorForURL(TESTCASE_URI);
+
+ yield listenForMediaChange(ui);
+
+ is(ui.editors.length, 1, "correct number of editors");
+
+ // Test editor with @media rules
+ let mediaEditor = ui.editors[0];
+ yield openEditor(mediaEditor);
+ testMediaEditor(mediaEditor);
+
+ Services.prefs.clearUserPref(MAP_PREF);
+});
+
+function testMediaEditor(editor) {
+ let sidebar = editor.details.querySelector(".stylesheet-sidebar");
+ is(sidebar.hidden, false, "sidebar is showing on editor with @media");
+
+ let entries = [...sidebar.querySelectorAll(".media-rule-label")];
+ is(entries.length, 2, "two @media rules displayed in sidebar");
+
+ testRule(entries[0], LABELS[0], LINE_NOS[0]);
+ testRule(entries[1], LABELS[1], LINE_NOS[1]);
+}
+
+function testRule(rule, text, lineno) {
+ let cond = rule.querySelector(".media-rule-condition");
+ is(cond.textContent, text, "media label is correct for " + text);
+
+ let line = rule.querySelector(".media-rule-line");
+ is(line.textContent, ":" + lineno, "correct line number shown");
+}
+
+/* Helpers */
+
+function openEditor(editor) {
+ getLinkFor(editor).click();
+
+ return editor.getSourceEditor();
+}
+
+function listenForMediaChange(UI) {
+ let deferred = defer();
+ UI.once("media-list-changed", () => {
+ deferred.resolve();
+ });
+ return deferred.promise;
+}
+
+function getLinkFor(editor) {
+ return editor.summary.querySelector(".stylesheet-name");
+}
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_missing_stylesheet.js b/devtools/client/styleeditor/test/browser_styleeditor_missing_stylesheet.js
new file mode 100644
index 000000000..e7511c914
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_missing_stylesheet.js
@@ -0,0 +1,30 @@
+"use strict";
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Checks that the style editor manages to finalize its stylesheet loading phase
+// even if one stylesheet is missing, and that an error message is displayed.
+
+const TESTCASE_URI = TEST_BASE + "missing.html";
+
+add_task(function* () {
+ let { ui, toolbox, panel } = yield openStyleEditorForURL(TESTCASE_URI);
+
+ // Note that we're not testing for a specific number of stylesheet editors
+ // below because the test-page is loaded with chrome:// URL and, right now,
+ // that means UA stylesheets are shown. So we avoid hardcoding the number of
+ // stylesheets here.
+ ok(ui.editors.length, "The UI contains style sheets.");
+
+ let rootEl = panel.panelWindow.document.getElementById("style-editor-chrome");
+ ok(!rootEl.classList.contains("loading"), "The loading indicator is hidden");
+
+ let notifBox = toolbox.getNotificationBox();
+ let notif = notifBox.getCurrentNotification();
+ ok(notif, "The notification box contains a message");
+ ok(notif.label.indexOf("Style sheet could not be loaded") !== -1,
+ "The error message is the correct one");
+ ok(notif.label.indexOf("missing-stylesheet.css") !== -1,
+ "The error message contains the missing stylesheet's URL");
+});
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_navigate.js b/devtools/client/styleeditor/test/browser_styleeditor_navigate.js
new file mode 100644
index 000000000..4d44fca70
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_navigate.js
@@ -0,0 +1,32 @@
+/* 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";
+
+// Test that selected sheet and cursor position is reset during navigation.
+
+const TESTCASE_URI = TEST_BASE_HTTPS + "simple.html";
+const NEW_URI = TEST_BASE_HTTPS + "media.html";
+
+const LINE_NO = 5;
+const COL_NO = 3;
+
+add_task(function* () {
+ let { ui } = yield openStyleEditorForURL(TESTCASE_URI);
+
+ is(ui.editors.length, 2, "Two sheets present after load.");
+
+ info("Selecting the second editor");
+ yield ui.selectStyleSheet(ui.editors[1].styleSheet, LINE_NO, COL_NO);
+
+ yield navigateToAndWaitForStyleSheets(NEW_URI, ui);
+
+ info("Waiting for source editor to be ready.");
+ yield ui.editors[0].getSourceEditor();
+
+ is(ui.selectedEditor, ui.editors[0], "first editor is selected");
+
+ let {line, ch} = ui.selectedEditor.sourceEditor.getCursor();
+ is(line, 0, "first line is selected");
+ is(ch, 0, "first column is selected");
+});
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_new.js b/devtools/client/styleeditor/test/browser_styleeditor_new.js
new file mode 100644
index 000000000..1fc5b4638
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_new.js
@@ -0,0 +1,113 @@
+/* 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";
+
+// Test that new sheets can be added and edited.
+
+const TESTCASE_URI = TEST_BASE_HTTP + "simple.html";
+
+const TESTCASE_CSS_SOURCE = "body{background-color:red;";
+
+add_task(function* () {
+ let { panel, ui } = yield openStyleEditorForURL(TESTCASE_URI);
+
+ let editor = yield createNew(ui, panel.panelWindow);
+ yield testInitialState(editor);
+
+ let originalHref = editor.styleSheet.href;
+ let waitForPropertyChange = onPropertyChange(editor);
+
+ yield typeInEditor(editor, panel.panelWindow);
+
+ yield waitForPropertyChange;
+
+ testUpdated(editor, originalHref);
+});
+
+function createNew(ui, panelWindow) {
+ info("Creating a new stylesheet now");
+ let deferred = defer();
+
+ ui.once("editor-added", (ev, editor) => {
+ editor.getSourceEditor().then(deferred.resolve);
+ });
+
+ waitForFocus(function () {
+ // create a new style sheet
+ let newButton = panelWindow.document
+ .querySelector(".style-editor-newButton");
+ ok(newButton, "'new' button exists");
+
+ EventUtils.synthesizeMouseAtCenter(newButton, {}, panelWindow);
+ }, panelWindow);
+
+ return deferred.promise;
+}
+
+function onPropertyChange(editor) {
+ let deferred = defer();
+
+ editor.styleSheet.on("property-change", function onProp(property) {
+ // wait for text to be entered fully
+ let text = editor.sourceEditor.getText();
+ if (property == "ruleCount" && text == TESTCASE_CSS_SOURCE + "}") {
+ editor.styleSheet.off("property-change", onProp);
+ deferred.resolve();
+ }
+ });
+
+ return deferred.promise;
+}
+
+function* testInitialState(editor) {
+ info("Testing the initial state of the new editor");
+
+ let summary = editor.summary;
+
+ ok(editor.sourceLoaded, "new editor is loaded when attached");
+ ok(editor.isNew, "new editor has isNew flag");
+
+ ok(editor.sourceEditor.hasFocus(), "new editor has focus");
+
+ summary = editor.summary;
+ let ruleCount = summary.querySelector(".stylesheet-rule-count").textContent;
+ is(parseInt(ruleCount, 10), 0, "new editor initially shows 0 rules");
+
+ let color = yield getComputedStyleProperty({
+ selector: "body",
+ name: "background-color"
+ });
+ is(color, "rgb(255, 255, 255)",
+ "content's background color is initially white");
+}
+
+function typeInEditor(editor, panelWindow) {
+ let deferred = defer();
+
+ waitForFocus(function () {
+ for (let c of TESTCASE_CSS_SOURCE) {
+ EventUtils.synthesizeKey(c, {}, panelWindow);
+ }
+ ok(editor.unsaved, "new editor has unsaved flag");
+
+ deferred.resolve();
+ }, panelWindow);
+
+ return deferred.promise;
+}
+
+function testUpdated(editor, originalHref) {
+ info("Testing the state of the new editor after editing it");
+
+ is(editor.sourceEditor.getText(), TESTCASE_CSS_SOURCE + "}",
+ "rule bracket has been auto-closed");
+
+ let ruleCount = editor.summary.querySelector(".stylesheet-rule-count")
+ .textContent;
+ is(parseInt(ruleCount, 10), 1,
+ "new editor shows 1 rule after modification");
+
+ is(editor.styleSheet.href, originalHref,
+ "style sheet href did not change");
+}
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_nostyle.js b/devtools/client/styleeditor/test/browser_styleeditor_nostyle.js
new file mode 100644
index 000000000..c01b7c572
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_nostyle.js
@@ -0,0 +1,28 @@
+/* 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";
+
+// Test that 'no styles' indicator is shown if a page doesn't contain any style
+// sheets.
+
+const TESTCASE_URI = TEST_BASE_HTTP + "nostyle.html";
+
+add_task(function* () {
+ let { panel } = yield openStyleEditorForURL(TESTCASE_URI);
+ let { panelWindow } = panel;
+
+ let root = panelWindow.document.querySelector(".splitview-root");
+ ok(!root.classList.contains("loading"),
+ "style editor root element does not have 'loading' class name anymore");
+
+ ok(root.querySelector(".empty.placeholder"), "showing 'no style' indicator");
+
+ let button = panelWindow.document.querySelector(".style-editor-newButton");
+ ok(!button.hasAttribute("disabled"),
+ "new style sheet button is enabled");
+
+ button = panelWindow.document.querySelector(".style-editor-importButton");
+ ok(!button.hasAttribute("disabled"),
+ "import button is enabled");
+});
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_opentab.js b/devtools/client/styleeditor/test/browser_styleeditor_opentab.js
new file mode 100644
index 000000000..c16e9d51b
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_opentab.js
@@ -0,0 +1,121 @@
+/* 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";
+
+// A test to check the 'Open Link in new tab' functionality in the
+// context menu item for stylesheets (bug 992947).
+const TESTCASE_URI = TEST_BASE_HTTPS + "simple.html";
+
+add_task(function* () {
+ let { ui } = yield openStyleEditorForURL(TESTCASE_URI);
+
+ yield rightClickStyleSheet(ui, ui.editors[0]);
+ is(ui._openLinkNewTabItem.getAttribute("disabled"), "false",
+ "The menu item is not disabled");
+ is(ui._openLinkNewTabItem.getAttribute("hidden"), "false",
+ "The menu item is not hidden");
+
+ let url = "https://example.com/browser/devtools/client/styleeditor/test/" +
+ "simple.css";
+ is(ui._contextMenuStyleSheet.href, url, "Correct URL for sheet");
+
+ let originalOpenUILinkIn = ui._window.openUILinkIn;
+ let tabOpenedDefer = defer();
+
+ ui._window.openUILinkIn = newUrl => {
+ // Reset the actual openUILinkIn function before proceeding.
+ ui._window.openUILinkIn = originalOpenUILinkIn;
+
+ is(newUrl, url, "The correct tab has been opened");
+ tabOpenedDefer.resolve();
+ };
+
+ ui._openLinkNewTabItem.click();
+
+ info(`Waiting for a tab to open - ${url}`);
+ yield tabOpenedDefer.promise;
+
+ yield rightClickInlineStyleSheet(ui, ui.editors[1]);
+ is(ui._openLinkNewTabItem.getAttribute("disabled"), "true",
+ "The menu item is disabled");
+ is(ui._openLinkNewTabItem.getAttribute("hidden"), "false",
+ "The menu item is not hidden");
+
+ yield rightClickNoStyleSheet(ui);
+ is(ui._openLinkNewTabItem.getAttribute("hidden"), "true",
+ "The menu item is not hidden");
+});
+
+function onPopupShow(contextMenu) {
+ let deferred = defer();
+ contextMenu.addEventListener("popupshown", function onpopupshown() {
+ contextMenu.removeEventListener("popupshown", onpopupshown);
+ deferred.resolve();
+ });
+ return deferred.promise;
+}
+
+function onPopupHide(contextMenu) {
+ let deferred = defer();
+ contextMenu.addEventListener("popuphidden", function popuphidden() {
+ contextMenu.removeEventListener("popuphidden", popuphidden);
+ deferred.resolve();
+ });
+ return deferred.promise;
+}
+
+function rightClickStyleSheet(ui, editor) {
+ let deferred = defer();
+
+ onPopupShow(ui._contextMenu).then(()=> {
+ onPopupHide(ui._contextMenu).then(() => {
+ deferred.resolve();
+ });
+ ui._contextMenu.hidePopup();
+ });
+
+ EventUtils.synthesizeMouseAtCenter(
+ editor.summary.querySelector(".stylesheet-name"),
+ {button: 2, type: "contextmenu"},
+ ui._window);
+
+ return deferred.promise;
+}
+
+function rightClickInlineStyleSheet(ui, editor) {
+ let deferred = defer();
+
+ onPopupShow(ui._contextMenu).then(()=> {
+ onPopupHide(ui._contextMenu).then(() => {
+ deferred.resolve();
+ });
+ ui._contextMenu.hidePopup();
+ });
+
+ EventUtils.synthesizeMouseAtCenter(
+ editor.summary.querySelector(".stylesheet-name"),
+ {button: 2, type: "contextmenu"},
+ ui._window);
+
+ return deferred.promise;
+}
+
+function rightClickNoStyleSheet(ui) {
+ let deferred = defer();
+
+ onPopupShow(ui._contextMenu).then(()=> {
+ onPopupHide(ui._contextMenu).then(() => {
+ deferred.resolve();
+ });
+ ui._contextMenu.hidePopup();
+ });
+
+ EventUtils.synthesizeMouseAtCenter(
+ ui._panelDoc.querySelector("#splitview-tpl-summary-stylesheet"),
+ {button: 2, type: "contextmenu"},
+ ui._window);
+
+ return deferred.promise;
+}
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_pretty.js b/devtools/client/styleeditor/test/browser_styleeditor_pretty.js
new file mode 100644
index 000000000..212cab12d
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_pretty.js
@@ -0,0 +1,68 @@
+/* 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";
+
+// Test that minified sheets are automatically prettified but other are left
+// untouched.
+
+const TESTCASE_URI = TEST_BASE_HTTP + "minified.html";
+
+/*
+ body {
+ background:white;
+ }
+ div {
+ font-size:4em;
+ color:red
+ }
+ span {
+ color:green;
+ }
+*/
+const PRETTIFIED_SOURCE = "" +
+"body \{\r?\n" +
+ "\tbackground\:white;\r?\n" +
+"\}\r?\n" +
+"div \{\r?\n" +
+ "\tfont\-size\:4em;\r?\n" +
+ "\tcolor\:red\r?\n" +
+"\}\r?\n" +
+"span \{\r?\n" +
+ "\tcolor\:green;\r?\n" +
+"\}\r?\n";
+
+/*
+ body { background: red; }
+ div {
+ font-size: 5em;
+ color: red
+ }
+*/
+const ORIGINAL_SOURCE = "" +
+"body \{ background\: red; \}\r?\n" +
+"div \{\r?\n" +
+ "font\-size\: 5em;\r?\n" +
+ "color\: red\r?\n" +
+"\}";
+
+add_task(function* () {
+ let { ui } = yield openStyleEditorForURL(TESTCASE_URI);
+ is(ui.editors.length, 2, "Two sheets present.");
+
+ info("Testing minified style sheet.");
+ let editor = yield ui.editors[0].getSourceEditor();
+
+ let prettifiedSourceRE = new RegExp(PRETTIFIED_SOURCE);
+ ok(prettifiedSourceRE.test(editor.sourceEditor.getText()),
+ "minified source has been prettified automatically");
+
+ info("Selecting second, non-minified style sheet.");
+ yield ui.selectStyleSheet(ui.editors[1].styleSheet);
+
+ editor = ui.editors[1];
+
+ let originalSourceRE = new RegExp(ORIGINAL_SOURCE);
+ ok(originalSourceRE.test(editor.sourceEditor.getText()),
+ "non-minified source has been left untouched");
+});
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_private_perwindowpb.js b/devtools/client/styleeditor/test/browser_styleeditor_private_perwindowpb.js
new file mode 100644
index 000000000..4381704e9
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_private_perwindowpb.js
@@ -0,0 +1,85 @@
+/* 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";
+
+// This test makes sure that the style editor does not store any
+// content CSS files in the permanent cache when opened from PB mode.
+
+const TEST_URL = "http://" + TEST_HOST + "/browser/devtools/client/" +
+ "styleeditor/test/test_private.html";
+const {LoadContextInfo} =
+ Cu.import("resource://gre/modules/LoadContextInfo.jsm", {});
+const cache = Cc["@mozilla.org/netwerk/cache-storage-service;1"]
+ .getService(Ci.nsICacheStorageService);
+
+add_task(function* () {
+ info("Opening a new private window");
+ let win = OpenBrowserWindow({private: true});
+ yield waitForDelayedStartupFinished(win);
+
+ info("Clearing the browser cache");
+ cache.clear();
+
+ let { toolbox, ui } = yield openStyleEditorForURL(TEST_URL, win);
+
+ is(ui.editors.length, 1, "The style editor contains one sheet.");
+ let editor = ui.editors[0];
+
+ yield editor.getSourceEditor();
+ yield checkDiskCacheFor(TEST_HOST);
+
+ yield toolbox.destroy();
+
+ let onUnload = new Promise(done => {
+ win.addEventListener("unload", function listener(event) {
+ if (event.target == win.document) {
+ win.removeEventListener("unload", listener);
+ done();
+ }
+ });
+ });
+ win.close();
+ yield onUnload;
+});
+
+function checkDiskCacheFor(host) {
+ let foundPrivateData = false;
+ let deferred = defer();
+
+ Visitor.prototype = {
+ onCacheStorageInfo: function (num) {
+ info("disk storage contains " + num + " entries");
+ },
+ onCacheEntryInfo: function (uri) {
+ let urispec = uri.asciiSpec;
+ info(urispec);
+ foundPrivateData |= urispec.includes(host);
+ },
+ onCacheEntryVisitCompleted: function () {
+ is(foundPrivateData, false, "web content present in disk cache");
+ deferred.resolve();
+ }
+ };
+ function Visitor() {}
+
+ let storage = cache.diskCacheStorage(LoadContextInfo.default, false);
+ storage.asyncVisitStorage(new Visitor(),
+ /* Do walk entries */
+ true);
+
+ return deferred.promise;
+}
+
+function waitForDelayedStartupFinished(win) {
+ let deferred = defer();
+ Services.obs.addObserver(function observer(subject, topic) {
+ if (win == subject) {
+ Services.obs.removeObserver(observer, topic);
+ deferred.resolve();
+ }
+ }, "browser-delayed-startup-finished", false);
+
+ return deferred.promise;
+}
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_reload.js b/devtools/client/styleeditor/test/browser_styleeditor_reload.js
new file mode 100644
index 000000000..a4991fb44
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_reload.js
@@ -0,0 +1,34 @@
+/* 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";
+
+// Test that selected sheet and cursor position persists during reload.
+
+const TESTCASE_URI = TEST_BASE_HTTPS + "simple.html";
+
+const LINE_NO = 5;
+const COL_NO = 3;
+
+add_task(function* () {
+ let { ui } = yield openStyleEditorForURL(TESTCASE_URI);
+
+ is(ui.editors.length, 2, "Two sheets present after load.");
+
+ info("Selecting the second editor");
+ yield ui.selectStyleSheet(ui.editors[1].styleSheet, LINE_NO, COL_NO);
+
+ yield reloadPageAndWaitForStyleSheets(ui);
+
+ is(ui.editors.length, 2, "Two sheets present after reload.");
+
+ info("Waiting for source editor to be ready.");
+ yield ui.editors[1].getSourceEditor();
+
+ is(ui.selectedEditor, ui.editors[1],
+ "second editor is selected after reload");
+
+ let {line, ch} = ui.selectedEditor.sourceEditor.getCursor();
+ is(line, LINE_NO, "correct line selected");
+ is(ch, COL_NO, "correct column selected");
+});
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_scroll.js b/devtools/client/styleeditor/test/browser_styleeditor_scroll.js
new file mode 100644
index 000000000..6524c1403
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_scroll.js
@@ -0,0 +1,91 @@
+/* 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";
+
+// Test that editor scrolls to correct line if it's selected with
+// * selectStyleSheet (specified line)
+// * click on the sidebar item (line before the editor was unselected)
+// See bug 1148086.
+
+const SIMPLE = TEST_BASE_HTTP + "simple.css";
+const LONG = TEST_BASE_HTTP + "doc_long.css";
+const DOCUMENT_WITH_LONG_SHEET = "data:text/html;charset=UTF-8," +
+ encodeURIComponent(
+ ["<!DOCTYPE html>",
+ "<html>",
+ " <head>",
+ " <title>Editor scroll test page</title>",
+ ' <link rel="stylesheet" type="text/css" href="' + SIMPLE + '">',
+ ' <link rel="stylesheet" type="text/css" href="' + LONG + '">',
+ " </head>",
+ " <body>Editor scroll test page</body>",
+ "</html>"
+ ].join("\n"));
+const LINE_TO_SELECT = 201;
+
+add_task(function* () {
+ let { ui } = yield openStyleEditorForURL(DOCUMENT_WITH_LONG_SHEET);
+
+ is(ui.editors.length, 2, "Two editors present.");
+
+ let simpleEditor = ui.editors[0];
+ let longEditor = ui.editors[1];
+
+ info(`Selecting doc_long.css and scrolling to line ${LINE_TO_SELECT}`);
+
+ // We need to wait for editor-selected if we want to check the scroll
+ // position as scrolling occurs after selectStyleSheet resolves but before the
+ // event is emitted.
+ let selectEventPromise = waitForEditorToBeSelected(longEditor, ui);
+ ui.selectStyleSheet(longEditor.styleSheet, LINE_TO_SELECT);
+ yield selectEventPromise;
+
+ info("Checking that the correct line is visible after initial load");
+
+ let { from, to } = longEditor.sourceEditor.getViewport();
+ info(`Lines ${from}-${to} are visible (expected ${LINE_TO_SELECT}).`);
+
+ ok(from <= LINE_TO_SELECT, "The editor scrolled too much.");
+ ok(to >= LINE_TO_SELECT, "The editor scrolled too little.");
+
+ let initialScrollTop = longEditor.sourceEditor.getScrollInfo().top;
+ info(`Storing scrollTop = ${initialScrollTop} for later comparison.`);
+
+ info("Selecting the first editor (simple.css)");
+ yield ui.selectStyleSheet(simpleEditor.styleSheet);
+
+ info("Selecting doc_long.css again.");
+ selectEventPromise = waitForEditorToBeSelected(longEditor, ui);
+
+ // Can't use ui.selectStyleSheet here as it will scroll the editor back to top
+ // and we want to check that the previous scroll position is restored.
+ let summary = yield ui.getEditorSummary(longEditor);
+ ui._view.activeSummary = summary;
+
+ info("Waiting for doc_long.css to be selected.");
+ yield selectEventPromise;
+
+ let scrollTop = longEditor.sourceEditor.getScrollInfo().top;
+ is(scrollTop, initialScrollTop,
+ "Scroll top was restored after the sheet was selected again.");
+});
+
+/**
+ * A helper that waits "editor-selected" event for given editor.
+ *
+ * @param {StyleSheetEditor} editor
+ * The editor to wait for.
+ * @param {StyleEditorUI} ui
+ * The StyleEditorUI the editor belongs to.
+ */
+var waitForEditorToBeSelected = Task.async(function* (editor, ui) {
+ info(`Waiting for ${editor.friendlyName} to be selected.`);
+ let selected = yield ui.once("editor-selected");
+ while (selected != editor) {
+ info(`Ignored editor-selected for editor ${editor.friendlyName}.`);
+ selected = yield ui.once("editor-selected");
+ }
+
+ info(`Got editor-selected for ${editor.friendlyName}.`);
+});
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_selectstylesheet.js b/devtools/client/styleeditor/test/browser_styleeditor_selectstylesheet.js
new file mode 100644
index 000000000..4d9692f9f
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_selectstylesheet.js
@@ -0,0 +1,26 @@
+/* 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";
+
+// Test that StyleEditorUI.selectStyleSheet selects the correct sheet, line and
+// column.
+
+const TESTCASE_URI = TEST_BASE_HTTPS + "simple.html";
+
+const LINE_NO = 5;
+const COL_NO = 0;
+
+add_task(function* () {
+ let { ui } = yield openStyleEditorForURL(TESTCASE_URI);
+ let editor = ui.editors[1];
+
+ info("Selecting style sheet #1.");
+ yield ui.selectStyleSheet(editor.styleSheet.href, LINE_NO);
+
+ is(ui.selectedEditor, ui.editors[1], "Second editor is selected.");
+ let {line, ch} = ui.selectedEditor.sourceEditor.getCursor();
+
+ is(line, LINE_NO, "correct line selected");
+ is(ch, COL_NO, "correct column selected");
+});
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_sourcemap_large.js b/devtools/client/styleeditor/test/browser_styleeditor_sourcemap_large.js
new file mode 100644
index 000000000..cddba6877
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_sourcemap_large.js
@@ -0,0 +1,33 @@
+/* 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";
+
+// Covers the case from Bug 1128747, where loading a sourcemapped
+// file prevents the correct editor from being selected on load,
+// and causes a second iframe to be appended when the user clicks
+// editor in the list.
+
+const TESTCASE_URI = TEST_BASE_HTTPS + "sourcemaps-large.html";
+
+add_task(function* () {
+ let { ui } = yield openStyleEditorForURL(TESTCASE_URI);
+
+ yield openEditor(ui.editors[0]);
+ let iframes = ui.selectedEditor.details.querySelectorAll("iframe");
+
+ is(iframes.length, 1, "There is only one editor iframe");
+ ok(ui.selectedEditor.summary.classList.contains("splitview-active"),
+ "The editor is selected");
+});
+
+function openEditor(editor) {
+ getLinkFor(editor).click();
+
+ return editor.getSourceEditor();
+}
+
+function getLinkFor(editor) {
+ return editor.summary.querySelector(".stylesheet-name");
+}
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_sourcemap_watching.js b/devtools/client/styleeditor/test/browser_styleeditor_sourcemap_watching.js
new file mode 100644
index 000000000..b73738fd6
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_sourcemap_watching.js
@@ -0,0 +1,162 @@
+/* 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 TESTCASE_URI_HTML = TEST_BASE_HTTP + "sourcemaps-watching.html";
+const TESTCASE_URI_CSS = TEST_BASE_HTTP + "sourcemap-css/sourcemaps.css";
+const TESTCASE_URI_REG_CSS = TEST_BASE_HTTP + "simple.css";
+const TESTCASE_URI_SCSS = TEST_BASE_HTTP + "sourcemap-sass/sourcemaps.scss";
+const TESTCASE_URI_MAP = TEST_BASE_HTTP + "sourcemap-css/sourcemaps.css.map";
+const TESTCASE_SCSS_NAME = "sourcemaps.scss";
+
+const TRANSITIONS_PREF = "devtools.styleeditor.transitions";
+
+const CSS_TEXT = "* { color: blue }";
+
+const {FileUtils} = Components.utils.import("resource://gre/modules/FileUtils.jsm", {});
+const {NetUtil} = Components.utils.import("resource://gre/modules/NetUtil.jsm", {});
+
+add_task(function* () {
+ yield new Promise(resolve => {
+ SpecialPowers.pushPrefEnv({"set": [
+ [TRANSITIONS_PREF, false]
+ ]}, resolve);
+ });
+
+ // copy all our files over so we don't screw them up for other tests
+ let HTMLFile = yield copy(TESTCASE_URI_HTML, ["sourcemaps.html"]);
+ let CSSFile = yield copy(TESTCASE_URI_CSS,
+ ["sourcemap-css", "sourcemaps.css"]);
+ yield copy(TESTCASE_URI_SCSS, ["sourcemap-sass", "sourcemaps.scss"]);
+ yield copy(TESTCASE_URI_MAP, ["sourcemap-css", "sourcemaps.css.map"]);
+ yield copy(TESTCASE_URI_REG_CSS, ["simple.css"]);
+
+ let uri = Services.io.newFileURI(HTMLFile);
+ let testcaseURI = uri.resolve("");
+
+ let { ui } = yield openStyleEditorForURL(testcaseURI);
+
+ let editor = ui.editors[1];
+ if (getStylesheetNameFor(editor) != TESTCASE_SCSS_NAME) {
+ editor = ui.editors[2];
+ }
+
+ is(getStylesheetNameFor(editor), TESTCASE_SCSS_NAME, "found scss editor");
+
+ let link = getLinkFor(editor);
+ link.click();
+
+ yield editor.getSourceEditor();
+
+ let color = yield getComputedStyleProperty({selector: "div", name: "color"});
+ is(color, "rgb(255, 0, 102)", "div is red before saving file");
+
+ // let styleApplied = defer();
+ let styleApplied = editor.once("style-applied");
+
+ yield pauseForTimeChange();
+
+ // Edit and save Sass in the editor. This will start off a file-watching
+ // process waiting for the CSS file to change.
+ yield editSCSS(editor);
+
+ // We can't run Sass or another compiler, so we fake it by just
+ // directly changing the CSS file.
+ yield editCSSFile(CSSFile);
+
+ info("wrote to CSS file, waiting for style-applied event");
+
+ yield styleApplied;
+
+ color = yield getComputedStyleProperty({selector: "div", name: "color"});
+ is(color, "rgb(0, 0, 255)", "div is blue after saving file");
+});
+
+function editSCSS(editor) {
+ let deferred = defer();
+
+ editor.sourceEditor.setText(CSS_TEXT);
+
+ editor.saveToFile(null, function (file) {
+ ok(file, "Scss file should be saved");
+ deferred.resolve();
+ });
+
+ return deferred.promise;
+}
+
+function editCSSFile(CSSFile) {
+ return write(CSS_TEXT, CSSFile);
+}
+
+function pauseForTimeChange() {
+ let deferred = defer();
+
+ // We have to wait for the system time to turn over > 1000 ms so that
+ // our file's last change time will show a change. This reflects what
+ // would happen in real life with a user manually saving the file.
+ setTimeout(deferred.resolve, 2000);
+
+ return deferred.promise;
+}
+
+/* Helpers */
+
+function getLinkFor(editor) {
+ return editor.summary.querySelector(".stylesheet-name");
+}
+
+function getStylesheetNameFor(editor) {
+ return editor.summary.querySelector(".stylesheet-name > label")
+ .getAttribute("value");
+}
+
+function copy(srcChromeURL, destFilePath) {
+ let destFile = FileUtils.getFile("ProfD", destFilePath);
+ return write(read(srcChromeURL), destFile);
+}
+
+function read(srcChromeURL) {
+ let scriptableStream = Cc["@mozilla.org/scriptableinputstream;1"]
+ .getService(Ci.nsIScriptableInputStream);
+
+ let channel = NetUtil.newChannel({
+ uri: srcChromeURL,
+ loadUsingSystemPrincipal: true
+ });
+ let input = channel.open2();
+ scriptableStream.init(input);
+
+ let data = "";
+ while (input.available()) {
+ data = data.concat(scriptableStream.read(input.available()));
+ }
+ scriptableStream.close();
+ input.close();
+
+ return data;
+}
+
+function write(data, file) {
+ let deferred = defer();
+
+ let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
+ .createInstance(Ci.nsIScriptableUnicodeConverter);
+
+ converter.charset = "UTF-8";
+
+ let istream = converter.convertToInputStream(data);
+ let ostream = FileUtils.openSafeFileOutputStream(file);
+
+ NetUtil.asyncCopy(istream, ostream, function (status) {
+ if (!Components.isSuccessCode(status)) {
+ info("Coudln't write to " + file.path);
+ return;
+ }
+ deferred.resolve(file);
+ });
+
+ return deferred.promise;
+}
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_sourcemaps.js b/devtools/client/styleeditor/test/browser_styleeditor_sourcemaps.js
new file mode 100644
index 000000000..fefca7648
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_sourcemaps.js
@@ -0,0 +1,149 @@
+/* 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";
+
+// https rather than chrome to improve coverage
+const TESTCASE_URI = TEST_BASE_HTTPS + "sourcemaps.html";
+const PREF = "devtools.styleeditor.source-maps-enabled";
+
+const contents = {
+ "sourcemaps.scss": [
+ "",
+ "$paulrougetpink: #f06;",
+ "",
+ "div {",
+ " color: $paulrougetpink;",
+ "}",
+ "",
+ "span {",
+ " background-color: #EEE;",
+ "}"
+ ].join("\n"),
+ "contained.scss": [
+ "$pink: #f06;",
+ "",
+ "#header {",
+ " color: $pink;",
+ "}"
+ ].join("\n"),
+ "sourcemaps.css": [
+ "div {",
+ " color: #ff0066; }",
+ "",
+ "span {",
+ " background-color: #EEE; }",
+ "",
+ "/*# sourceMappingURL=sourcemaps.css.map */"
+ ].join("\n"),
+ "contained.css": [
+ "#header {",
+ " color: #f06; }",
+ "",
+ "/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJma" +
+ "WxlIjoiIiwic291cmNlcyI6WyJzYXNzL2NvbnRhaW5lZC5zY3NzIl0sIm5hbWVzIjpbXSwi" +
+ "bWFwcGluZ3MiOiJBQUVBO0VBQ0UsT0FISyIsInNvdXJjZXNDb250ZW50IjpbIiRwaW5rOiA" +
+ "jZjA2O1xuXG4jaGVhZGVyIHtcbiAgY29sb3I6ICRwaW5rO1xufSJdfQ==*/"
+ ].join("\n"),
+ "test-stylus.styl": [
+ "paulrougetpink = #f06;",
+ "",
+ "div",
+ " color: paulrougetpink",
+ "",
+ "span",
+ " background-color: #EEE",
+ ""
+ ].join("\n"),
+ "test-stylus.css": [
+ "div {",
+ " color: #f06;",
+ "}",
+ "span {",
+ " background-color: #eee;",
+ "}",
+ "/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb" +
+ "3VyY2VzIjpbInRlc3Qtc3R5bHVzLnN0eWwiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFB" +
+ "RUE7RUFDRSxPQUFPLEtBQVA7O0FBRUY7RUFDRSxrQkFBa0IsS0FBbEIiLCJmaWxlIjoidGV" +
+ "zdC1zdHlsdXMuY3NzIiwic291cmNlc0NvbnRlbnQiOlsicGF1bHJvdWdldHBpbmsgPSAjZj" +
+ "A2O1xuXG5kaXZcbiAgY29sb3I6IHBhdWxyb3VnZXRwaW5rXG5cbnNwYW5cbiAgYmFja2dyb" +
+ "3VuZC1jb2xvcjogI0VFRVxuIl19 */"
+ ].join("\n")
+};
+
+const cssNames = ["sourcemaps.css", "contained.css", "test-stylus.css"];
+const origNames = ["sourcemaps.scss", "contained.scss", "test-stylus.styl"];
+
+waitForExplicitFinish();
+
+add_task(function* () {
+ let {ui} = yield openStyleEditorForURL(TESTCASE_URI);
+
+ is(ui.editors.length, 4,
+ "correct number of editors with source maps enabled");
+
+ // Test first plain css editor
+ testFirstEditor(ui.editors[0]);
+
+ // Test Scss editors
+ yield testEditor(ui.editors[1], origNames);
+ yield testEditor(ui.editors[2], origNames);
+ yield testEditor(ui.editors[3], origNames);
+
+ // Test disabling original sources
+ yield togglePref(ui);
+
+ is(ui.editors.length, 4, "correct number of editors after pref toggled");
+
+ // Test CSS editors
+ yield testEditor(ui.editors[1], cssNames);
+ yield testEditor(ui.editors[2], cssNames);
+ yield testEditor(ui.editors[3], cssNames);
+
+ Services.prefs.clearUserPref(PREF);
+});
+
+function testFirstEditor(editor) {
+ let name = getStylesheetNameFor(editor);
+ is(name, "simple.css", "First style sheet display name is correct");
+}
+
+function testEditor(editor, possibleNames) {
+ let name = getStylesheetNameFor(editor);
+ ok(possibleNames.indexOf(name) >= 0, name + " editor name is correct");
+
+ return openEditor(editor).then(() => {
+ let expectedText = contents[name];
+
+ let text = editor.sourceEditor.getText();
+
+ is(text, expectedText, name + " editor contains expected text");
+ });
+}
+
+/* Helpers */
+
+function togglePref(UI) {
+ let editorsPromise = UI.once("stylesheets-reset");
+ let selectedPromise = UI.once("editor-selected");
+
+ Services.prefs.setBoolPref(PREF, false);
+
+ return promise.all([editorsPromise, selectedPromise]);
+}
+
+function openEditor(editor) {
+ getLinkFor(editor).click();
+
+ return editor.getSourceEditor();
+}
+
+function getLinkFor(editor) {
+ return editor.summary.querySelector(".stylesheet-name");
+}
+
+function getStylesheetNameFor(editor) {
+ return editor.summary.querySelector(".stylesheet-name > label")
+ .getAttribute("value");
+}
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_sourcemaps_inline.js b/devtools/client/styleeditor/test/browser_styleeditor_sourcemaps_inline.js
new file mode 100644
index 000000000..ae7279858
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_sourcemaps_inline.js
@@ -0,0 +1,85 @@
+/* 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";
+
+// https rather than chrome to improve coverage
+const TESTCASE_URI = TEST_BASE_HTTPS + "sourcemaps-inline.html";
+const PREF = "devtools.styleeditor.source-maps-enabled";
+
+const sassContent = `body {
+ background-color: black;
+ & > h1 {
+ color: white;
+ }
+}
+`;
+
+const cssContent = `body {
+ background-color: black;
+}
+body > h1 {
+ color: white;
+}
+` +
+"/*# sourceMappingURL=data:application/json;base64,ewoidmVyc2lvbiI6IDMsCiJtY" +
+"XBwaW5ncyI6ICJBQUFBLElBQUs7RUFDSCxnQkFBZ0IsRUFBRSxLQUFLO0VBQ3ZCLFNBQU87SUFD" +
+"TCxLQUFLLEVBQUUsS0FBSyIsCiJzb3VyY2VzIjogWyJ0ZXN0LnNjc3MiXSwKInNvdXJjZXNDb25" +
+"0ZW50IjogWyJib2R5IHtcbiAgYmFja2dyb3VuZC1jb2xvcjogYmxhY2s7XG4gICYgPiBoMSB7XG" +
+"4gICAgY29sb3I6IHdoaXRlO1xuICB9XG59XG4iXSwKIm5hbWVzIjogW10sCiJmaWxlIjogInRlc" +
+"3QuY3NzIgp9Cg== */";
+
+add_task(function* () {
+ let {ui} = yield openStyleEditorForURL(TESTCASE_URI);
+
+ is(ui.editors.length, 1,
+ "correct number of editors with source maps enabled");
+
+ yield testEditor(ui.editors[0], "test.scss", sassContent);
+
+ // Test disabling original sources
+ yield togglePref(ui);
+
+ is(ui.editors.length, 1, "correct number of editors after pref toggled");
+
+ // Test CSS editors
+ yield testEditor(ui.editors[0], "<inline style sheet #1>", cssContent);
+
+ Services.prefs.clearUserPref(PREF);
+});
+
+function* testEditor(editor, expectedName, expectedText) {
+ let name = getStylesheetNameFor(editor);
+ is(expectedName, name, name + " editor name is correct");
+
+ yield openEditor(editor);
+ let text = editor.sourceEditor.getText();
+ is(text, expectedText, name + " editor contains expected text");
+}
+
+/* Helpers */
+
+function togglePref(UI) {
+ let editorsPromise = UI.once("stylesheets-reset");
+ let selectedPromise = UI.once("editor-selected");
+
+ Services.prefs.setBoolPref(PREF, false);
+
+ return promise.all([editorsPromise, selectedPromise]);
+}
+
+function openEditor(editor) {
+ getLinkFor(editor).click();
+
+ return editor.getSourceEditor();
+}
+
+function getLinkFor(editor) {
+ return editor.summary.querySelector(".stylesheet-name");
+}
+
+function getStylesheetNameFor(editor) {
+ return editor.summary.querySelector(".stylesheet-name > label")
+ .getAttribute("value");
+}
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_sv_keynav.js b/devtools/client/styleeditor/test/browser_styleeditor_sv_keynav.js
new file mode 100644
index 000000000..a9c0073c3
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_sv_keynav.js
@@ -0,0 +1,65 @@
+/* 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";
+
+// Test that the style sheet list can be navigated with keyboard.
+
+const TESTCASE_URI = TEST_BASE_HTTP + "four.html";
+
+add_task(function* () {
+ let { panel, ui } = yield openStyleEditorForURL(TESTCASE_URI);
+
+ info("Waiting for source editor to load.");
+ yield ui.editors[0].getSourceEditor();
+
+ let selected = ui.once("editor-selected");
+
+ info("Testing keyboard navigation on the sheet list.");
+ testKeyboardNavigation(ui.editors[0], panel);
+
+ info("Waiting for editor #2 to be selected due to keyboard navigation.");
+ yield selected;
+
+ ok(ui.editors[2].sourceEditor.hasFocus(), "Editor #2 has focus.");
+});
+
+function getStylesheetNameLinkFor(editor) {
+ return editor.summary.querySelector(".stylesheet-name");
+}
+
+function testKeyboardNavigation(editor, panel) {
+ let panelWindow = panel.panelWindow;
+ let ui = panel.UI;
+ waitForFocus(function () {
+ let summary = editor.summary;
+ EventUtils.synthesizeMouseAtCenter(summary, {}, panelWindow);
+
+ let item = getStylesheetNameLinkFor(ui.editors[0]);
+ is(panelWindow.document.activeElement, item,
+ "editor 0 item is the active element");
+
+ EventUtils.synthesizeKey("VK_DOWN", {}, panelWindow);
+ item = getStylesheetNameLinkFor(ui.editors[1]);
+ is(panelWindow.document.activeElement, item,
+ "editor 1 item is the active element");
+
+ EventUtils.synthesizeKey("VK_HOME", {}, panelWindow);
+ item = getStylesheetNameLinkFor(ui.editors[0]);
+ is(panelWindow.document.activeElement, item,
+ "fist editor item is the active element");
+
+ EventUtils.synthesizeKey("VK_END", {}, panelWindow);
+ item = getStylesheetNameLinkFor(ui.editors[3]);
+ is(panelWindow.document.activeElement, item,
+ "last editor item is the active element");
+
+ EventUtils.synthesizeKey("VK_UP", {}, panelWindow);
+ item = getStylesheetNameLinkFor(ui.editors[2]);
+ is(panelWindow.document.activeElement, item,
+ "editor 2 item is the active element");
+
+ EventUtils.synthesizeKey("VK_RETURN", {}, panelWindow);
+ // this will attach and give focus editor 2
+ }, panelWindow);
+}
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_sv_resize.js b/devtools/client/styleeditor/test/browser_styleeditor_sv_resize.js
new file mode 100644
index 000000000..d684905fc
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_sv_resize.js
@@ -0,0 +1,48 @@
+/* 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";
+
+// Test that resizing the source editor container doesn't move the caret.
+
+const TESTCASE_URI = TEST_BASE_HTTP + "simple.html";
+
+const {Toolbox} = require("devtools/client/framework/toolbox");
+
+add_task(function* () {
+ let { toolbox, ui } = yield openStyleEditorForURL(TESTCASE_URI);
+
+ is(ui.editors.length, 2, "There are 2 style sheets initially");
+
+ info("Changing toolbox host to a window.");
+ yield toolbox.switchHost(Toolbox.HostType.WINDOW);
+
+ let editor = yield ui.editors[0].getSourceEditor();
+ let originalSourceEditor = editor.sourceEditor;
+
+ let hostWindow = toolbox.win.parent;
+ let originalWidth = hostWindow.outerWidth;
+ let originalHeight = hostWindow.outerHeight;
+
+ // to check the caret is preserved
+ originalSourceEditor.setCursor(originalSourceEditor.getPosition(4));
+
+ info("Resizing window.");
+ hostWindow.resizeTo(120, 480);
+
+ let sourceEditor = ui.editors[0].sourceEditor;
+ is(sourceEditor, originalSourceEditor,
+ "the editor still references the same Editor instance");
+
+ is(sourceEditor.getOffset(sourceEditor.getCursor()), 4,
+ "the caret position has been preserved");
+
+ info("Restoring window to original size.");
+ hostWindow.resizeTo(originalWidth, originalHeight);
+});
+
+registerCleanupFunction(() => {
+ // Restore the host type for other tests.
+ Services.prefs.clearUserPref("devtools.toolbox.host");
+});
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_sync.js b/devtools/client/styleeditor/test/browser_styleeditor_sync.js
new file mode 100644
index 000000000..4bff84101
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_sync.js
@@ -0,0 +1,72 @@
+/* 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";
+
+// Test that changes in the style inspector are synchronized into the
+// style editor.
+
+const TESTCASE_URI = TEST_BASE_HTTP + "sync.html";
+
+const expectedText = `
+ body {
+ border-width: 15px;
+ /*! color: red; */
+ }
+
+ #testid {
+ /*! font-size: 4em; */
+ }
+ `;
+
+function* closeAndReopenToolbox() {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ yield gDevTools.closeToolbox(target);
+ let { ui: newui } = yield openStyleEditor();
+ return newui;
+}
+
+add_task(function* () {
+ yield addTab(TESTCASE_URI);
+ let { inspector, view } = yield openRuleView();
+ yield selectNode("#testid", inspector);
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+
+ // Disable the "font-size" property.
+ let propEditor = ruleEditor.rule.textProps[0].editor;
+ let onModification = view.once("ruleview-changed");
+ propEditor.enable.click();
+ yield onModification;
+
+ // Disable the "color" property. Note that this property is in a
+ // rule that also contains a non-inherited property -- so this test
+ // is also testing that property editing works properly in this
+ // situation.
+ ruleEditor = getRuleViewRuleEditor(view, 3);
+ propEditor = ruleEditor.rule.textProps[1].editor;
+ onModification = view.once("ruleview-changed");
+ propEditor.enable.click();
+ yield onModification;
+
+ let { ui } = yield openStyleEditor();
+
+ let editor = yield ui.editors[0].getSourceEditor();
+ let text = editor.sourceEditor.getText();
+ is(text, expectedText, "style inspector changes are synced");
+
+ // Close and reopen the toolbox, to see that the edited text remains
+ // available.
+ ui = yield closeAndReopenToolbox();
+ editor = yield ui.editors[0].getSourceEditor();
+ text = editor.sourceEditor.getText();
+ is(text, expectedText, "changes remain after close and reopen");
+
+ // For the time being, the actor does not update the style's owning
+ // node's textContent. See bug 1205380.
+ let textContent = yield ContentTask.spawn(gBrowser.selectedBrowser, null,
+ function* () {
+ return content.document.querySelector("style").textContent;
+ });
+
+ isnot(textContent, expectedText, "changes not written back to style node");
+});
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_syncAddProperty.js b/devtools/client/styleeditor/test/browser_styleeditor_syncAddProperty.js
new file mode 100644
index 000000000..e197157ad
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_syncAddProperty.js
@@ -0,0 +1,45 @@
+/* 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";
+
+// Test that adding a new rule is synced to the style editor.
+
+const TESTCASE_URI = TEST_BASE_HTTP + "sync.html";
+
+const expectedText = `
+ body {
+ border-width: 15px;
+ color: red;
+ }
+
+ #testid {
+ font-size: 4em;
+ /*! background-color: yellow; */
+ }
+ `;
+
+add_task(function* () {
+ yield addTab(TESTCASE_URI);
+ let { inspector, view } = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ info("Focusing a new property name in the rule-view");
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+ let editor = yield focusEditableField(view, ruleEditor.closeBrace);
+ is(inplaceEditor(ruleEditor.newPropSpan), editor,
+ "The new property editor has focus");
+
+ let input = editor.input;
+ input.value = "/* background-color: yellow; */";
+
+ info("Pressing return to commit and focus the new value field");
+ let onModifications = view.once("ruleview-changed");
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
+ yield onModifications;
+
+ let { ui } = yield openStyleEditor();
+ let sourceEditor = yield ui.editors[0].getSourceEditor();
+ let text = sourceEditor.sourceEditor.getText();
+ is(text, expectedText, "selector edits are synced");
+});
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_syncAddRule.js b/devtools/client/styleeditor/test/browser_styleeditor_syncAddRule.js
new file mode 100644
index 000000000..348b9c630
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_syncAddRule.js
@@ -0,0 +1,31 @@
+/* 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";
+
+// Test that adding a new rule is synced to the style editor.
+
+const TESTCASE_URI = TEST_BASE_HTTP + "sync.html";
+
+const expectedText = `
+#testid {
+}`;
+
+add_task(function* () {
+ yield addTab(TESTCASE_URI);
+ let { inspector, view } = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ let onRuleViewChanged = once(view, "ruleview-changed");
+ view.addRuleButton.click();
+ yield onRuleViewChanged;
+
+ let { ui } = yield openStyleEditor();
+
+ info("Selecting the second editor");
+ yield ui.selectStyleSheet(ui.editors[1].styleSheet);
+
+ let editor = ui.editors[1];
+ let text = editor.sourceEditor.getText();
+ is(text, expectedText, "selector edits are synced");
+});
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_syncAlreadyOpen.js b/devtools/client/styleeditor/test/browser_styleeditor_syncAlreadyOpen.js
new file mode 100644
index 000000000..84c1c2a7a
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_syncAlreadyOpen.js
@@ -0,0 +1,50 @@
+/* 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";
+
+// Test that changes in the style inspector are synchronized into the
+// style editor.
+
+const TESTCASE_URI = TEST_BASE_HTTP + "sync.html";
+
+const expectedText = `
+ body {
+ border-width: 15px;
+ color: red;
+ }
+
+ #testid {
+ /*! font-size: 4em; */
+ }
+ `;
+
+add_task(function* () {
+ yield addTab(TESTCASE_URI);
+
+ let { inspector, view, toolbox } = yield openRuleView();
+
+ // In this test, make sure the style editor is open before making
+ // changes in the inspector.
+ let { ui } = yield openStyleEditor();
+ let editor = yield ui.editors[0].getSourceEditor();
+
+ let onEditorChange = defer();
+ editor.sourceEditor.on("change", onEditorChange.resolve);
+
+ yield toolbox.getPanel("inspector");
+ yield selectNode("#testid", inspector);
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+
+ // Disable the "font-size" property.
+ let propEditor = ruleEditor.rule.textProps[0].editor;
+ let onModification = view.once("ruleview-changed");
+ propEditor.enable.click();
+ yield onModification;
+
+ yield openStyleEditor();
+ yield onEditorChange.promise;
+
+ let text = editor.sourceEditor.getText();
+ is(text, expectedText, "style inspector changes are synced");
+});
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_syncEditSelector.js b/devtools/client/styleeditor/test/browser_styleeditor_syncEditSelector.js
new file mode 100644
index 000000000..ea7d7515d
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_syncEditSelector.js
@@ -0,0 +1,39 @@
+/* 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";
+
+// Test that changes in the style inspector are synchronized into the
+// style editor.
+
+const TESTCASE_URI = TEST_BASE_HTTP + "sync.html";
+
+const expectedText = `
+ body {
+ border-width: 15px;
+ color: red;
+ }
+
+ #testid, span {
+ font-size: 4em;
+ }
+ `;
+
+add_task(function* () {
+ yield addTab(TESTCASE_URI);
+ let { inspector, view } = yield openRuleView();
+ yield selectNode("#testid", inspector);
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+
+ let editor = yield focusEditableField(view, ruleEditor.selectorText);
+ editor.input.value = "#testid, span";
+ let onRuleViewChanged = once(view, "ruleview-changed");
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ yield onRuleViewChanged;
+
+ let { ui } = yield openStyleEditor();
+
+ editor = yield ui.editors[0].getSourceEditor();
+ let text = editor.sourceEditor.getText();
+ is(text, expectedText, "selector edits are synced");
+});
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_syncIntoRuleView.js b/devtools/client/styleeditor/test/browser_styleeditor_syncIntoRuleView.js
new file mode 100644
index 000000000..8f939e203
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_syncIntoRuleView.js
@@ -0,0 +1,48 @@
+/* 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";
+
+// Test that changes in the style editor are synchronized into the
+// style inspector.
+
+const TEST_URI = `
+ <style type='text/css'>
+ </style>
+ <div id='testid' class='testclass'>Styled Node</div>
+`;
+
+const TESTCASE_CSS_SOURCE = "#testid { color: chartreuse; }";
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ let { panel, ui } = yield openStyleEditor();
+
+ let editor = yield ui.editors[0].getSourceEditor();
+
+ let waitForRuleView = view.once("ruleview-refreshed");
+ yield typeInEditor(editor, panel.panelWindow);
+ yield waitForRuleView;
+
+ let value = getRuleViewPropertyValue(view, "#testid", "color");
+ is(value, "chartreuse", "check that edits were synced to rule view");
+});
+
+function typeInEditor(editor, panelWindow) {
+ let deferred = defer();
+
+ waitForFocus(function () {
+ for (let c of TESTCASE_CSS_SOURCE) {
+ EventUtils.synthesizeKey(c, {}, panelWindow);
+ }
+ ok(editor.unsaved, "new editor has unsaved flag");
+
+ deferred.resolve();
+ }, panelWindow);
+
+ return deferred.promise;
+}
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_transition_rule.js b/devtools/client/styleeditor/test/browser_styleeditor_transition_rule.js
new file mode 100644
index 000000000..d52ec41fc
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_transition_rule.js
@@ -0,0 +1,51 @@
+/* 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 TESTCASE_URI = TEST_BASE_HTTPS + "simple.html";
+
+const NEW_RULE = "body { background-color: purple; }";
+
+add_task(function* () {
+ let { ui } = yield openStyleEditorForURL(TESTCASE_URI);
+
+ is(ui.editors.length, 2, "correct number of editors");
+
+ let editor = ui.editors[0];
+ yield openEditor(editor);
+
+ // Set text twice in a row
+ let styleChanges = listenForStyleChange(editor.styleSheet);
+
+ editor.sourceEditor.setText(NEW_RULE);
+ editor.sourceEditor.setText(NEW_RULE + " ");
+
+ yield styleChanges;
+
+ let rules = yield ContentTask.spawn(gBrowser.selectedBrowser, 0,
+ function* (index) {
+ let sheet = content.document.styleSheets[index];
+ return [...sheet.cssRules].map(rule => rule.cssText);
+ });
+
+ // Test that we removed the transition rule, but kept the rule we added
+ is(rules.length, 1, "only one rule in stylesheet");
+ is(rules[0], NEW_RULE, "stylesheet only contains rule we added");
+});
+
+/* Helpers */
+
+function openEditor(editor) {
+ let link = editor.summary.querySelector(".stylesheet-name");
+ link.click();
+
+ return editor.getSourceEditor();
+}
+
+function listenForStyleChange(sheet) {
+ let deferred = defer();
+ sheet.on("style-applied", deferred.resolve);
+ return deferred.promise;
+}
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_xul.js b/devtools/client/styleeditor/test/browser_styleeditor_xul.js
new file mode 100644
index 000000000..931ad92e7
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_xul.js
@@ -0,0 +1,22 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that the style-editor initializes correctly for XUL windows.
+
+"use strict";
+
+waitForExplicitFinish();
+
+const TEST_URL = TEST_BASE + "doc_xulpage.xul";
+
+add_task(function* () {
+ let tab = yield addTab(TEST_URL);
+ let target = TargetFactory.forTab(tab);
+
+ let toolbox = yield gDevTools.showToolbox(target, "styleeditor");
+ let panel = toolbox.getCurrentPanel();
+
+ ok(panel,
+ "The style-editor panel did initialize correctly for the XUL window");
+});
diff --git a/devtools/client/styleeditor/test/doc_long.css b/devtools/client/styleeditor/test/doc_long.css
new file mode 100644
index 000000000..671416333
--- /dev/null
+++ b/devtools/client/styleeditor/test/doc_long.css
@@ -0,0 +1,403 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+div {
+ z-index: 1;
+}
+
+div {
+ z-index: 2;
+}
+
+div {
+ z-index: 3;
+}
+
+div {
+ z-index: 4;
+}
+
+div {
+ z-index: 5;
+}
+
+div {
+ z-index: 6;
+}
+
+div {
+ z-index: 7;
+}
+
+div {
+ z-index: 8;
+}
+
+div {
+ z-index: 9;
+}
+
+div {
+ z-index: 10;
+}
+
+div {
+ z-index: 11;
+}
+
+div {
+ z-index: 12;
+}
+
+div {
+ z-index: 13;
+}
+
+div {
+ z-index: 14;
+}
+
+div {
+ z-index: 15;
+}
+
+div {
+ z-index: 16;
+}
+
+div {
+ z-index: 17;
+}
+
+div {
+ z-index: 18;
+}
+
+div {
+ z-index: 19;
+}
+
+div {
+ z-index: 20;
+}
+
+div {
+ z-index: 21;
+}
+
+div {
+ z-index: 22;
+}
+
+div {
+ z-index: 23;
+}
+
+div {
+ z-index: 24;
+}
+
+div {
+ z-index: 25;
+}
+
+div {
+ z-index: 26;
+}
+
+div {
+ z-index: 27;
+}
+
+div {
+ z-index: 28;
+}
+
+div {
+ z-index: 29;
+}
+
+div {
+ z-index: 30;
+}
+
+div {
+ z-index: 31;
+}
+
+div {
+ z-index: 32;
+}
+
+div {
+ z-index: 33;
+}
+
+div {
+ z-index: 34;
+}
+
+div {
+ z-index: 35;
+}
+
+div {
+ z-index: 36;
+}
+
+div {
+ z-index: 37;
+}
+
+div {
+ z-index: 38;
+}
+
+div {
+ z-index: 39;
+}
+
+div {
+ z-index: 40;
+}
+
+div {
+ z-index: 41;
+}
+
+div {
+ z-index: 42;
+}
+
+div {
+ z-index: 43;
+}
+
+div {
+ z-index: 44;
+}
+
+div {
+ z-index: 45;
+}
+
+div {
+ z-index: 46;
+}
+
+div {
+ z-index: 47;
+}
+
+div {
+ z-index: 48;
+}
+
+div {
+ z-index: 49;
+}
+
+div {
+ z-index: 50;
+}
+
+div {
+ z-index: 51;
+}
+
+div {
+ z-index: 52;
+}
+
+div {
+ z-index: 53;
+}
+
+div {
+ z-index: 54;
+}
+
+div {
+ z-index: 55;
+}
+
+div {
+ z-index: 56;
+}
+
+div {
+ z-index: 57;
+}
+
+div {
+ z-index: 58;
+}
+
+div {
+ z-index: 59;
+}
+
+div {
+ z-index: 60;
+}
+
+div {
+ z-index: 61;
+}
+
+div {
+ z-index: 62;
+}
+
+div {
+ z-index: 63;
+}
+
+div {
+ z-index: 64;
+}
+
+div {
+ z-index: 65;
+}
+
+div {
+ z-index: 66;
+}
+
+div {
+ z-index: 67;
+}
+
+div {
+ z-index: 68;
+}
+
+div {
+ z-index: 69;
+}
+
+div {
+ z-index: 70;
+}
+
+div {
+ z-index: 71;
+}
+
+div {
+ z-index: 72;
+}
+
+div {
+ z-index: 73;
+}
+
+div {
+ z-index: 74;
+}
+
+div {
+ z-index: 75;
+}
+
+div {
+ z-index: 76;
+}
+
+div {
+ z-index: 77;
+}
+
+div {
+ z-index: 78;
+}
+
+div {
+ z-index: 79;
+}
+
+div {
+ z-index: 80;
+}
+
+div {
+ z-index: 81;
+}
+
+div {
+ z-index: 82;
+}
+
+div {
+ z-index: 83;
+}
+
+div {
+ z-index: 84;
+}
+
+div {
+ z-index: 85;
+}
+
+div {
+ z-index: 86;
+}
+
+div {
+ z-index: 87;
+}
+
+div {
+ z-index: 88;
+}
+
+div {
+ z-index: 89;
+}
+
+div {
+ z-index: 90;
+}
+
+div {
+ z-index: 91;
+}
+
+div {
+ z-index: 92;
+}
+
+div {
+ z-index: 93;
+}
+
+div {
+ z-index: 94;
+}
+
+div {
+ z-index: 95;
+}
+
+div {
+ z-index: 96;
+}
+
+div {
+ z-index: 97;
+}
+
+div {
+ z-index: 98;
+}
+
+div {
+ z-index: 99;
+}
+
+div {
+ z-index: 100;
+}
diff --git a/devtools/client/styleeditor/test/doc_uncached.css b/devtools/client/styleeditor/test/doc_uncached.css
new file mode 100644
index 000000000..492256c93
--- /dev/null
+++ b/devtools/client/styleeditor/test/doc_uncached.css
@@ -0,0 +1,16 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* ☺ */
+
+body {
+ background: white;
+}
+
+div {
+ font-size: 4em;
+}
+
+div > span {
+ text-decoration: underline;
+}
diff --git a/devtools/client/styleeditor/test/doc_uncached.html b/devtools/client/styleeditor/test/doc_uncached.html
new file mode 100644
index 000000000..bd83d1db6
--- /dev/null
+++ b/devtools/client/styleeditor/test/doc_uncached.html
@@ -0,0 +1,10 @@
+<!doctype html>
+<html>
+<head>
+ <title>uncached testcase</title>
+ <link rel="stylesheet" charset="UTF-8" type="text/css" media="screen" href="doc_uncached.css"/>
+</head>
+<body>
+ <div>uncached <span>testcase</span></div>
+</body>
+</html>
diff --git a/devtools/client/styleeditor/test/doc_xulpage.xul b/devtools/client/styleeditor/test/doc_xulpage.xul
new file mode 100644
index 000000000..155be25ec
--- /dev/null
+++ b/devtools/client/styleeditor/test/doc_xulpage.xul
@@ -0,0 +1,7 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="simple.css" type="text/css"?>
+<!DOCTYPE window>
+<window xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <label value="Simple XUL document" />
+</window> \ No newline at end of file
diff --git a/devtools/client/styleeditor/test/four.html b/devtools/client/styleeditor/test/four.html
new file mode 100644
index 000000000..c0d51d691
--- /dev/null
+++ b/devtools/client/styleeditor/test/four.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<html>
+<head>
+ <title>four stylesheets</title>
+ <link rel="stylesheet" type="text/css" media="scren" href="simple.css"/>
+ <style type="text/css">
+ div {
+ font-size: 2em;
+ }
+ </style>
+ <style type="text/css">
+ span {
+ font-size: 3em;
+ }
+ </style>
+ <style type="text/css">
+ p {
+ font-size: 4em;
+ }
+ </style>
+</head>
+<body>
+ <div>four <span>stylesheets</span></div>
+</body>
+</html>
diff --git a/devtools/client/styleeditor/test/head.js b/devtools/client/styleeditor/test/head.js
new file mode 100644
index 000000000..c7abaa435
--- /dev/null
+++ b/devtools/client/styleeditor/test/head.js
@@ -0,0 +1,121 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* All top-level definitions here are exports. */
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+
+"use strict";
+
+/* import-globals-from ../../inspector/shared/test/head.js */
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/inspector/shared/test/head.js", this);
+
+const TEST_BASE = "chrome://mochitests/content/browser/devtools/client/styleeditor/test/";
+const TEST_BASE_HTTP = "http://example.com/browser/devtools/client/styleeditor/test/";
+const TEST_BASE_HTTPS = "https://example.com/browser/devtools/client/styleeditor/test/";
+const TEST_HOST = "mochi.test:8888";
+
+/**
+ * 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
+ * @param {Window} win The window to add the tab to (default: current window).
+ * @return a promise that resolves to the tab object when the url is loaded
+ */
+var addTab = function (url, win) {
+ info("Adding a new tab with URL: '" + url + "'");
+ let def = defer();
+
+ let targetWindow = win || window;
+ let targetBrowser = targetWindow.gBrowser;
+
+ let tab = targetBrowser.selectedTab = targetBrowser.addTab(url);
+ BrowserTestUtils.browserLoaded(targetBrowser.selectedBrowser)
+ .then(function () {
+ info("URL '" + url + "' loading complete");
+ def.resolve(tab);
+ });
+
+ return def.promise;
+};
+
+/**
+ * Navigate the currently selected tab to a new URL and wait for it to load.
+ * @param {String} url The url to be loaded in the current tab.
+ * @return a promise that resolves when the page has fully loaded.
+ */
+var navigateTo = Task.async(function* (url) {
+ info(`Navigating to ${url}`);
+ let browser = gBrowser.selectedBrowser;
+
+ let navigating = defer();
+ browser.addEventListener("load", function onload() {
+ browser.removeEventListener("load", onload, true);
+ navigating.resolve();
+ }, true);
+
+ browser.loadURI(url);
+
+ yield navigating.promise;
+});
+
+var navigateToAndWaitForStyleSheets = Task.async(function* (url, ui) {
+ let onReset = ui.once("stylesheets-reset");
+ yield navigateTo(url);
+ yield onReset;
+});
+
+var reloadPageAndWaitForStyleSheets = Task.async(function* (ui) {
+ info("Reloading the page.");
+
+ let onReset = ui.once("stylesheets-reset");
+ let browser = gBrowser.selectedBrowser;
+ yield ContentTask.spawn(browser, null, "() => content.location.reload()");
+ yield onReset;
+});
+
+/**
+ * Open the style editor for the current tab.
+ */
+var openStyleEditor = Task.async(function* (tab) {
+ if (!tab) {
+ tab = gBrowser.selectedTab;
+ }
+ let target = TargetFactory.forTab(tab);
+ let toolbox = yield gDevTools.showToolbox(target, "styleeditor");
+ let panel = toolbox.getPanel("styleeditor");
+ let ui = panel.UI;
+
+ return { toolbox, panel, ui };
+});
+
+/**
+ * Creates a new tab in specified window navigates it to the given URL and
+ * opens style editor in it.
+ */
+var openStyleEditorForURL = Task.async(function* (url, win) {
+ let tab = yield addTab(url, win);
+ let result = yield openStyleEditor(tab);
+ result.tab = tab;
+ return result;
+});
+
+/**
+ * Send an async message to the frame script and get back the requested
+ * computed style property.
+ *
+ * @param {String} selector
+ * The selector used to obtain the element.
+ * @param {String} pseudo
+ * pseudo id to query, or null.
+ * @param {String} name
+ * name of the property.
+ */
+var getComputedStyleProperty = function* (args) {
+ return yield ContentTask.spawn(gBrowser.selectedBrowser, args,
+ function ({selector, pseudo, name}) {
+ let element = content.document.querySelector(selector);
+ let style = content.getComputedStyle(element, pseudo);
+ return style.getPropertyValue(name);
+ }
+ );
+};
diff --git a/devtools/client/styleeditor/test/import.css b/devtools/client/styleeditor/test/import.css
new file mode 100644
index 000000000..df532fb96
--- /dev/null
+++ b/devtools/client/styleeditor/test/import.css
@@ -0,0 +1,10 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+@import url(import2.css);
+
+body {
+ margin: 0;
+}
+
diff --git a/devtools/client/styleeditor/test/import.html b/devtools/client/styleeditor/test/import.html
new file mode 100644
index 000000000..bc92baeba
--- /dev/null
+++ b/devtools/client/styleeditor/test/import.html
@@ -0,0 +1,11 @@
+<!doctype html>
+<html>
+<head>
+ <title>import testcase</title>
+ <link rel="stylesheet" charset="UTF-8" type="text/css" media="screen" href="simple.css"/>
+ <link rel="stylesheet" charset="UTF-8" type="text/css" media="screen" href="import.css"/>
+</head>
+<body>
+ <div>import <span>testcase</span></div>
+</body>
+</html>
diff --git a/devtools/client/styleeditor/test/import2.css b/devtools/client/styleeditor/test/import2.css
new file mode 100644
index 000000000..fbbe14d9a
--- /dev/null
+++ b/devtools/client/styleeditor/test/import2.css
@@ -0,0 +1,10 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+@import url(import.css);
+
+p {
+ padding: 5px;
+}
+
diff --git a/devtools/client/styleeditor/test/inline-1.html b/devtools/client/styleeditor/test/inline-1.html
new file mode 100644
index 000000000..76478893b
--- /dev/null
+++ b/devtools/client/styleeditor/test/inline-1.html
@@ -0,0 +1,19 @@
+<!doctype html>
+<html>
+ <head>
+ <title>Inline test page #1</title>
+ <style type="text/css">
+ .second {
+ font-size:2em;
+ }
+ </style>
+ <style type="text/css">
+ .first {
+ font-size:3em;
+ }
+ </style>
+ </head>
+ <body class="first">
+ Inline test page #1
+ </body>
+</html>
diff --git a/devtools/client/styleeditor/test/inline-2.html b/devtools/client/styleeditor/test/inline-2.html
new file mode 100644
index 000000000..e25285c31
--- /dev/null
+++ b/devtools/client/styleeditor/test/inline-2.html
@@ -0,0 +1,19 @@
+<!doctype html>
+<html>
+ <head>
+ <title>Inline test page #2</title>
+ <style type="text/css">
+ .second {
+ font-size:2em;
+ }
+ </style>
+ <style type="text/css">
+ .first {
+ font-size:3em;
+ }
+ </style>
+ </head>
+ <body class="second">
+ Inline test page #2
+ </body>
+</html>
diff --git a/devtools/client/styleeditor/test/longload.html b/devtools/client/styleeditor/test/longload.html
new file mode 100644
index 000000000..d23a9cd77
--- /dev/null
+++ b/devtools/client/styleeditor/test/longload.html
@@ -0,0 +1,29 @@
+<!doctype html>
+<html>
+<head>
+ <title>Long load</title>
+ <link rel="stylesheet" charset="UTF-8" type="text/css" media="screen" href="simple.css"/>
+ <style type="text/css">
+ body {
+ background: white;
+ }
+
+ div {
+ font-size: 4em;
+ }
+
+ div > span {
+ text-decoration: underline;
+ }
+ </style>
+</head>
+<body>
+ Time passes:
+ <script type="application/javascript;version=1.8">
+ "use strict";
+ for (let i = 0; i < 5000; i++) {
+ document.write("<br>...");
+ }
+ </script>
+</body>
+</html>
diff --git a/devtools/client/styleeditor/test/media-rules-sourcemaps.html b/devtools/client/styleeditor/test/media-rules-sourcemaps.html
new file mode 100644
index 000000000..4876ef795
--- /dev/null
+++ b/devtools/client/styleeditor/test/media-rules-sourcemaps.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="UTF-8">
+ <link rel="stylesheet" type="text/css" href="sourcemap-css/media-rules.css"
+</head>
+<body>
+ <div>
+ Testing style editor media sidebar with source maps
+ </div>
+</body>
+</html> \ No newline at end of file
diff --git a/devtools/client/styleeditor/test/media-rules.css b/devtools/client/styleeditor/test/media-rules.css
new file mode 100644
index 000000000..5eea7c380
--- /dev/null
+++ b/devtools/client/styleeditor/test/media-rules.css
@@ -0,0 +1,29 @@
+@media not all {
+ div {
+ color: blue;
+ }
+}
+
+@media all {
+ div {
+ color: red;
+ }
+}
+
+div {
+ width: 20px;
+ height: 20px;
+ background-color: ghostwhite;
+}
+
+@media (max-width: 400px) {
+ div {
+ color: green;
+ }
+}
+
+@media (min-height: 300px) and (max-height: 320px) {
+ div {
+ color: orange;
+ }
+}
diff --git a/devtools/client/styleeditor/test/media-rules.html b/devtools/client/styleeditor/test/media-rules.html
new file mode 100644
index 000000000..edc45ccae
--- /dev/null
+++ b/devtools/client/styleeditor/test/media-rules.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="UTF-8">
+ <link rel="stylesheet" href="simple.css"/>
+ <link rel="stylesheet" href="media-rules.css"/>
+</head>
+<body>
+ <div>
+ Testing style editor media sidebar
+ </div>
+</body>
+</html> \ No newline at end of file
diff --git a/devtools/client/styleeditor/test/media-small.css b/devtools/client/styleeditor/test/media-small.css
new file mode 100644
index 000000000..d64756ddc
--- /dev/null
+++ b/devtools/client/styleeditor/test/media-small.css
@@ -0,0 +1,5 @@
+/* this stylesheet applies when min-width<400px */
+body {
+ background: red;
+}
+
diff --git a/devtools/client/styleeditor/test/media.html b/devtools/client/styleeditor/test/media.html
new file mode 100644
index 000000000..ef05818c5
--- /dev/null
+++ b/devtools/client/styleeditor/test/media.html
@@ -0,0 +1,11 @@
+<!doctype html>
+<html>
+<head>
+ <link rel="stylesheet" type="text/css" href="simple.css" media="screen,print"/>
+ <link rel="stylesheet" type="text/css" href="media-small.css" media="screen and (min-width: 200px)"/>
+</head>
+<body>
+ <div>test for media labels</div>
+</body>
+</html>
+
diff --git a/devtools/client/styleeditor/test/minified.html b/devtools/client/styleeditor/test/minified.html
new file mode 100644
index 000000000..ab8c67d25
--- /dev/null
+++ b/devtools/client/styleeditor/test/minified.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<html>
+<head>
+ <title>minified testcase</title>
+ <link rel="stylesheet" href="pretty.css"/>
+ <style type="text/css">body { background: red; }
+div {
+font-size: 5em;
+color: red
+}</style>
+</head>
+<body>
+ <div>minified <span>testcase</span></div>
+</body>
+</html>
diff --git a/devtools/client/styleeditor/test/missing.html b/devtools/client/styleeditor/test/missing.html
new file mode 100644
index 000000000..ce4ec08be
--- /dev/null
+++ b/devtools/client/styleeditor/test/missing.html
@@ -0,0 +1,11 @@
+<!doctype html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>missing stylesheet testcase</title>
+ <link rel="stylesheet" charset="utf-8" type="text/css" media="screen" href="missing-stylesheet.css"/>
+ <link rel="stylesheet" charset="utf-8" type="text/css" media="screen" href="simple.css"/>
+</head>
+<body>
+</body>
+</html>
diff --git a/devtools/client/styleeditor/test/nostyle.html b/devtools/client/styleeditor/test/nostyle.html
new file mode 100644
index 000000000..f6a6769e6
--- /dev/null
+++ b/devtools/client/styleeditor/test/nostyle.html
@@ -0,0 +1,5 @@
+<html>
+ <div>
+ Page with no stylesheets
+ </div>
+</html> \ No newline at end of file
diff --git a/devtools/client/styleeditor/test/pretty.css b/devtools/client/styleeditor/test/pretty.css
new file mode 100644
index 000000000..e597afa4b
--- /dev/null
+++ b/devtools/client/styleeditor/test/pretty.css
@@ -0,0 +1,2 @@
+
+body{background:white;}div{font-size:4em;color:red}span{color:green;}
diff --git a/devtools/client/styleeditor/test/resources_inpage.jsi b/devtools/client/styleeditor/test/resources_inpage.jsi
new file mode 100644
index 000000000..8b7895af5
--- /dev/null
+++ b/devtools/client/styleeditor/test/resources_inpage.jsi
@@ -0,0 +1,12 @@
+
+// This script is used from within browser_styleeditor_cmd_edit.html
+
+window.addEventListener('load', function() {
+ var pid = document.getElementById('pid');
+ var h3 = document.createElement('h3');
+ h3.id = 'h3id';
+ h3.classList.add('h3class');
+ h3.appendChild(document.createTextNode('h3'));
+ h3.setAttribute('data-a1', 'h3');
+ pid.parentNode.appendChild(h3);
+});
diff --git a/devtools/client/styleeditor/test/resources_inpage1.css b/devtools/client/styleeditor/test/resources_inpage1.css
new file mode 100644
index 000000000..644deaaea
--- /dev/null
+++ b/devtools/client/styleeditor/test/resources_inpage1.css
@@ -0,0 +1,11 @@
+@charset "utf-8";
+
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+#pid { border-top: 2px dotted #F00; }
+#divid { border-top: 2px dotted #00F; }
+#h4id { border-top: 2px dotted #0F0; }
+#h3id { border-top: 2px dotted #FF0; }
diff --git a/devtools/client/styleeditor/test/resources_inpage2.css b/devtools/client/styleeditor/test/resources_inpage2.css
new file mode 100644
index 000000000..e4fa48e53
--- /dev/null
+++ b/devtools/client/styleeditor/test/resources_inpage2.css
@@ -0,0 +1,11 @@
+@charset "utf-8";
+
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+*[data-a1=p] { border-left: 4px solid #F00; }
+*[data-a1=div] { border-left: 4px solid #00F; }
+*[data-a1=h4] { border-left: 4px solid #0F0; }
+*[data-a1=h3] { border-left: 4px solid #FF0; }
diff --git a/devtools/client/styleeditor/test/simple.css b/devtools/client/styleeditor/test/simple.css
new file mode 100644
index 000000000..829fe9e6c
--- /dev/null
+++ b/devtools/client/styleeditor/test/simple.css
@@ -0,0 +1,9 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* ☺ */
+
+body {
+ margin: 0;
+}
+
diff --git a/devtools/client/styleeditor/test/simple.css.gz b/devtools/client/styleeditor/test/simple.css.gz
new file mode 100644
index 000000000..ee3b9efbc
--- /dev/null
+++ b/devtools/client/styleeditor/test/simple.css.gz
Binary files differ
diff --git a/devtools/client/styleeditor/test/simple.css.gz^headers^ b/devtools/client/styleeditor/test/simple.css.gz^headers^
new file mode 100644
index 000000000..092020ab0
--- /dev/null
+++ b/devtools/client/styleeditor/test/simple.css.gz^headers^
@@ -0,0 +1,4 @@
+Vary: Accept-Encoding
+Content-Encoding: gzip
+Content-Type: text/css
+
diff --git a/devtools/client/styleeditor/test/simple.gz.html b/devtools/client/styleeditor/test/simple.gz.html
new file mode 100644
index 000000000..d63362b8e
--- /dev/null
+++ b/devtools/client/styleeditor/test/simple.gz.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<html>
+<head>
+ <title>simple testcase</title>
+ <link rel="stylesheet" type="text/css" media="scren" href="simple.css.gz"/>
+ <style type="text/css">
+ body {
+ background: white;
+ }
+
+ div {
+ font-size: 4em;
+ }
+
+ div > span {
+ text-decoration: underline;
+ }
+ </style>
+</head>
+<body>
+ <div>simple <span>testcase</span></div>
+</body>
+</html>
diff --git a/devtools/client/styleeditor/test/simple.html b/devtools/client/styleeditor/test/simple.html
new file mode 100644
index 000000000..8f25cdf61
--- /dev/null
+++ b/devtools/client/styleeditor/test/simple.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>simple testcase</title>
+ <link rel="stylesheet" charset="UTF-8" type="text/css" media="screen" href="simple.css"/>
+ <style type="text/css">
+ body {
+ background: white;
+ }
+
+ div {
+ font-size: 4em;
+ }
+
+ div > span {
+ text-decoration: underline;
+ }
+ </style>
+</head>
+<body>
+ <div>simple <span>testcase</span></div>
+</body>
+</html>
diff --git a/devtools/client/styleeditor/test/sourcemap-css/contained.css b/devtools/client/styleeditor/test/sourcemap-css/contained.css
new file mode 100644
index 000000000..79572f606
--- /dev/null
+++ b/devtools/client/styleeditor/test/sourcemap-css/contained.css
@@ -0,0 +1,4 @@
+#header {
+ color: #f06; }
+
+/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiIiwic291cmNlcyI6WyJzYXNzL2NvbnRhaW5lZC5zY3NzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUVBO0VBQ0UsT0FISyIsInNvdXJjZXNDb250ZW50IjpbIiRwaW5rOiAjZjA2O1xuXG4jaGVhZGVyIHtcbiAgY29sb3I6ICRwaW5rO1xufSJdfQ==*/ \ No newline at end of file
diff --git a/devtools/client/styleeditor/test/sourcemap-css/media-rules.css b/devtools/client/styleeditor/test/sourcemap-css/media-rules.css
new file mode 100644
index 000000000..fad540a96
--- /dev/null
+++ b/devtools/client/styleeditor/test/sourcemap-css/media-rules.css
@@ -0,0 +1,8 @@
+@media screen and (max-width: 320px) {
+ div {
+ width: 100px; } }
+@media screen and (min-width: 1200px) {
+ div {
+ width: 400px; } }
+
+/*# sourceMappingURL=media-rules.css.map */ \ No newline at end of file
diff --git a/devtools/client/styleeditor/test/sourcemap-css/media-rules.css.map b/devtools/client/styleeditor/test/sourcemap-css/media-rules.css.map
new file mode 100644
index 000000000..76cd48fe2
--- /dev/null
+++ b/devtools/client/styleeditor/test/sourcemap-css/media-rules.css.map
@@ -0,0 +1,6 @@
+{
+"version": 3,
+"mappings": "AAIE,oCAA4C;EAD9C,GAAI;IAEA,KAAK,EAAE,KAAK;AAEd,qCAA4C;EAJ9C,GAAI;IAKA,KAAK,EAAE,KAAK",
+"sources": ["../sourcemap-sass/media-rules.scss"],
+"file": "media-rules.css"
+}
diff --git a/devtools/client/styleeditor/test/sourcemap-css/sourcemaps.css b/devtools/client/styleeditor/test/sourcemap-css/sourcemaps.css
new file mode 100644
index 000000000..7246a9082
--- /dev/null
+++ b/devtools/client/styleeditor/test/sourcemap-css/sourcemaps.css
@@ -0,0 +1,7 @@
+div {
+ color: #ff0066; }
+
+span {
+ background-color: #EEE; }
+
+/*# sourceMappingURL=sourcemaps.css.map */ \ No newline at end of file
diff --git a/devtools/client/styleeditor/test/sourcemap-css/sourcemaps.css.map b/devtools/client/styleeditor/test/sourcemap-css/sourcemaps.css.map
new file mode 100644
index 000000000..2e8f2911c
--- /dev/null
+++ b/devtools/client/styleeditor/test/sourcemap-css/sourcemaps.css.map
@@ -0,0 +1,6 @@
+{
+"version": 3,
+"mappings": "AAGA,GAAI;EACF,KAAK,EAHU,OAAI;;AAMrB,IAAK;EACH,gBAAgB,EAAE,IAAI",
+"sources": ["../sourcemap-sass/sourcemaps.scss"],
+"file": "sourcemaps.css"
+}
diff --git a/devtools/client/styleeditor/test/sourcemap-css/test-bootstrap-scss.css b/devtools/client/styleeditor/test/sourcemap-css/test-bootstrap-scss.css
new file mode 100644
index 000000000..e943c6ef4
--- /dev/null
+++ b/devtools/client/styleeditor/test/sourcemap-css/test-bootstrap-scss.css
@@ -0,0 +1,4513 @@
+/*! normalize.css v3.0.1 | MIT License | git.io/normalize */
+html {
+ font-family: sans-serif;
+ -ms-text-size-adjust: 100%;
+ -webkit-text-size-adjust: 100%; }
+
+body {
+ margin: 0; }
+
+article, aside, details, figcaption, figure, footer, header, hgroup, main, nav, section, summary {
+ display: block; }
+
+audio, canvas, progress, video {
+ display: inline-block;
+ vertical-align: baseline; }
+
+audio:not([controls]) {
+ display: none;
+ height: 0; }
+
+[hidden], template {
+ display: none; }
+
+a {
+ background: transparent; }
+
+a:active, a:hover {
+ outline: 0; }
+
+abbr[title] {
+ border-bottom: 1px dotted; }
+
+b, strong {
+ font-weight: bold; }
+
+dfn {
+ font-style: italic; }
+
+h1 {
+ font-size: 2em;
+ margin: 0.67em 0; }
+
+mark {
+ background: #ff0;
+ color: #000; }
+
+small {
+ font-size: 80%; }
+
+sub, sup {
+ font-size: 75%;
+ line-height: 0;
+ position: relative;
+ vertical-align: baseline; }
+
+sup {
+ top: -0.5em; }
+
+sub {
+ bottom: -0.25em; }
+
+img {
+ border: 0; }
+
+svg:not(:root) {
+ overflow: hidden; }
+
+figure {
+ margin: 1em 40px; }
+
+hr {
+ box-sizing: content-box;
+ height: 0; }
+
+pre {
+ overflow: auto; }
+
+code, kbd, pre, samp {
+ font-family: monospace, monospace;
+ font-size: 1em; }
+
+button, input, optgroup, select, textarea {
+ color: inherit;
+ font: inherit;
+ margin: 0; }
+
+button {
+ overflow: visible; }
+
+button, select {
+ text-transform: none; }
+
+button, html input[type="button"], input[type="reset"], input[type="submit"] {
+ -webkit-appearance: button;
+ cursor: pointer; }
+
+button[disabled], html input[disabled] {
+ cursor: default; }
+
+button::-moz-focus-inner, input::-moz-focus-inner {
+ border: 0;
+ padding: 0; }
+
+input {
+ line-height: normal; }
+
+input[type="checkbox"], input[type="radio"] {
+ box-sizing: border-box;
+ padding: 0; }
+
+input[type="number"]::-webkit-inner-spin-button, input[type="number"]::-webkit-outer-spin-button {
+ height: auto; }
+
+input[type="search"] {
+ -webkit-appearance: textfield;
+ box-sizing: content-box; }
+
+input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webkit-search-decoration {
+ -webkit-appearance: none; }
+
+fieldset {
+ border: 1px solid #c0c0c0;
+ margin: 0 2px;
+ padding: 0.35em 0.625em 0.75em; }
+
+legend {
+ border: 0;
+ padding: 0; }
+
+textarea {
+ overflow: auto; }
+
+optgroup {
+ font-weight: bold; }
+
+table {
+ border-collapse: collapse;
+ border-spacing: 0; }
+
+td, th {
+ padding: 0; }
+
+@media print {
+ * {
+ text-shadow: none !important;
+ color: #000 !important;
+ background: transparent !important;
+ box-shadow: none !important; }
+ a, a:visited {
+ text-decoration: underline; }
+ a[href]:after {
+ content: " (" attr(href) ")"; }
+ abbr[title]:after {
+ content: " (" attr(title) ")"; }
+ a[href^="javascript:"]:after, a[href^="#"]:after {
+ content: ""; }
+ pre, blockquote {
+ border: 1px solid #999;
+ page-break-inside: avoid; }
+ thead {
+ display: table-header-group; }
+ tr, img {
+ page-break-inside: avoid; }
+ img {
+ max-width: 100% !important; }
+ p, h2, h3 {
+ orphans: 3;
+ widows: 3; }
+ h2, h3 {
+ page-break-after: avoid; }
+ select {
+ background: #fff !important; }
+ .navbar {
+ display: none; }
+ .table td, .table th {
+ background-color: #fff !important; }
+ .btn > .caret, .dropup > .btn > .caret {
+ border-top-color: #000 !important; }
+ .label {
+ border: 1px solid #000; }
+ .table {
+ border-collapse: collapse !important; }
+ .table-bordered th, .table-bordered td {
+ border: 1px solid #ddd !important; } }
+
+@font-face {
+ font-family: 'Glyphicons Halflings';
+ src: url('../bower_components/bootstrap-sass-official/vendor/assets/fonts/bootstrap/glyphicons-halflings-regular.eot');
+ src: url('../bower_components/bootstrap-sass-official/vendor/assets/fonts/bootstrap/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), url('../bower_components/bootstrap-sass-official/vendor/assets/fonts/bootstrap/glyphicons-halflings-regular.woff') format('woff'), url('../bower_components/bootstrap-sass-official/vendor/assets/fonts/bootstrap/glyphicons-halflings-regular.ttf') format('truetype'), url('../bower_components/bootstrap-sass-official/vendor/assets/fonts/bootstrap/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg'); }
+
+.glyphicon {
+ position: relative;
+ top: 1px;
+ display: inline-block;
+ font-family: 'Glyphicons Halflings';
+ font-style: normal;
+ font-weight: normal;
+ line-height: 1;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale; }
+
+.glyphicon-asterisk:before {
+ content: "\2a"; }
+
+.glyphicon-plus:before {
+ content: "\2b"; }
+
+.glyphicon-euro:before {
+ content: "\20ac"; }
+
+.glyphicon-minus:before {
+ content: "\2212"; }
+
+.glyphicon-cloud:before {
+ content: "\2601"; }
+
+.glyphicon-envelope:before {
+ content: "\2709"; }
+
+.glyphicon-pencil:before {
+ content: "\270f"; }
+
+.glyphicon-glass:before {
+ content: "\e001"; }
+
+.glyphicon-music:before {
+ content: "\e002"; }
+
+.glyphicon-search:before {
+ content: "\e003"; }
+
+.glyphicon-heart:before {
+ content: "\e005"; }
+
+.glyphicon-star:before {
+ content: "\e006"; }
+
+.glyphicon-star-empty:before {
+ content: "\e007"; }
+
+.glyphicon-user:before {
+ content: "\e008"; }
+
+.glyphicon-film:before {
+ content: "\e009"; }
+
+.glyphicon-th-large:before {
+ content: "\e010"; }
+
+.glyphicon-th:before {
+ content: "\e011"; }
+
+.glyphicon-th-list:before {
+ content: "\e012"; }
+
+.glyphicon-ok:before {
+ content: "\e013"; }
+
+.glyphicon-remove:before {
+ content: "\e014"; }
+
+.glyphicon-zoom-in:before {
+ content: "\e015"; }
+
+.glyphicon-zoom-out:before {
+ content: "\e016"; }
+
+.glyphicon-off:before {
+ content: "\e017"; }
+
+.glyphicon-signal:before {
+ content: "\e018"; }
+
+.glyphicon-cog:before {
+ content: "\e019"; }
+
+.glyphicon-trash:before {
+ content: "\e020"; }
+
+.glyphicon-home:before {
+ content: "\e021"; }
+
+.glyphicon-file:before {
+ content: "\e022"; }
+
+.glyphicon-time:before {
+ content: "\e023"; }
+
+.glyphicon-road:before {
+ content: "\e024"; }
+
+.glyphicon-download-alt:before {
+ content: "\e025"; }
+
+.glyphicon-download:before {
+ content: "\e026"; }
+
+.glyphicon-upload:before {
+ content: "\e027"; }
+
+.glyphicon-inbox:before {
+ content: "\e028"; }
+
+.glyphicon-play-circle:before {
+ content: "\e029"; }
+
+.glyphicon-repeat:before {
+ content: "\e030"; }
+
+.glyphicon-refresh:before {
+ content: "\e031"; }
+
+.glyphicon-list-alt:before {
+ content: "\e032"; }
+
+.glyphicon-lock:before {
+ content: "\e033"; }
+
+.glyphicon-flag:before {
+ content: "\e034"; }
+
+.glyphicon-headphones:before {
+ content: "\e035"; }
+
+.glyphicon-volume-off:before {
+ content: "\e036"; }
+
+.glyphicon-volume-down:before {
+ content: "\e037"; }
+
+.glyphicon-volume-up:before {
+ content: "\e038"; }
+
+.glyphicon-qrcode:before {
+ content: "\e039"; }
+
+.glyphicon-barcode:before {
+ content: "\e040"; }
+
+.glyphicon-tag:before {
+ content: "\e041"; }
+
+.glyphicon-tags:before {
+ content: "\e042"; }
+
+.glyphicon-book:before {
+ content: "\e043"; }
+
+.glyphicon-bookmark:before {
+ content: "\e044"; }
+
+.glyphicon-print:before {
+ content: "\e045"; }
+
+.glyphicon-camera:before {
+ content: "\e046"; }
+
+.glyphicon-font:before {
+ content: "\e047"; }
+
+.glyphicon-bold:before {
+ content: "\e048"; }
+
+.glyphicon-italic:before {
+ content: "\e049"; }
+
+.glyphicon-text-height:before {
+ content: "\e050"; }
+
+.glyphicon-text-width:before {
+ content: "\e051"; }
+
+.glyphicon-align-left:before {
+ content: "\e052"; }
+
+.glyphicon-align-center:before {
+ content: "\e053"; }
+
+.glyphicon-align-right:before {
+ content: "\e054"; }
+
+.glyphicon-align-justify:before {
+ content: "\e055"; }
+
+.glyphicon-list:before {
+ content: "\e056"; }
+
+.glyphicon-indent-left:before {
+ content: "\e057"; }
+
+.glyphicon-indent-right:before {
+ content: "\e058"; }
+
+.glyphicon-facetime-video:before {
+ content: "\e059"; }
+
+.glyphicon-picture:before {
+ content: "\e060"; }
+
+.glyphicon-map-marker:before {
+ content: "\e062"; }
+
+.glyphicon-adjust:before {
+ content: "\e063"; }
+
+.glyphicon-tint:before {
+ content: "\e064"; }
+
+.glyphicon-edit:before {
+ content: "\e065"; }
+
+.glyphicon-share:before {
+ content: "\e066"; }
+
+.glyphicon-check:before {
+ content: "\e067"; }
+
+.glyphicon-move:before {
+ content: "\e068"; }
+
+.glyphicon-step-backward:before {
+ content: "\e069"; }
+
+.glyphicon-fast-backward:before {
+ content: "\e070"; }
+
+.glyphicon-backward:before {
+ content: "\e071"; }
+
+.glyphicon-play:before {
+ content: "\e072"; }
+
+.glyphicon-pause:before {
+ content: "\e073"; }
+
+.glyphicon-stop:before {
+ content: "\e074"; }
+
+.glyphicon-forward:before {
+ content: "\e075"; }
+
+.glyphicon-fast-forward:before {
+ content: "\e076"; }
+
+.glyphicon-step-forward:before {
+ content: "\e077"; }
+
+.glyphicon-eject:before {
+ content: "\e078"; }
+
+.glyphicon-chevron-left:before {
+ content: "\e079"; }
+
+.glyphicon-chevron-right:before {
+ content: "\e080"; }
+
+.glyphicon-plus-sign:before {
+ content: "\e081"; }
+
+.glyphicon-minus-sign:before {
+ content: "\e082"; }
+
+.glyphicon-remove-sign:before {
+ content: "\e083"; }
+
+.glyphicon-ok-sign:before {
+ content: "\e084"; }
+
+.glyphicon-question-sign:before {
+ content: "\e085"; }
+
+.glyphicon-info-sign:before {
+ content: "\e086"; }
+
+.glyphicon-screenshot:before {
+ content: "\e087"; }
+
+.glyphicon-remove-circle:before {
+ content: "\e088"; }
+
+.glyphicon-ok-circle:before {
+ content: "\e089"; }
+
+.glyphicon-ban-circle:before {
+ content: "\e090"; }
+
+.glyphicon-arrow-left:before {
+ content: "\e091"; }
+
+.glyphicon-arrow-right:before {
+ content: "\e092"; }
+
+.glyphicon-arrow-up:before {
+ content: "\e093"; }
+
+.glyphicon-arrow-down:before {
+ content: "\e094"; }
+
+.glyphicon-share-alt:before {
+ content: "\e095"; }
+
+.glyphicon-resize-full:before {
+ content: "\e096"; }
+
+.glyphicon-resize-small:before {
+ content: "\e097"; }
+
+.glyphicon-exclamation-sign:before {
+ content: "\e101"; }
+
+.glyphicon-gift:before {
+ content: "\e102"; }
+
+.glyphicon-leaf:before {
+ content: "\e103"; }
+
+.glyphicon-fire:before {
+ content: "\e104"; }
+
+.glyphicon-eye-open:before {
+ content: "\e105"; }
+
+.glyphicon-eye-close:before {
+ content: "\e106"; }
+
+.glyphicon-warning-sign:before {
+ content: "\e107"; }
+
+.glyphicon-plane:before {
+ content: "\e108"; }
+
+.glyphicon-calendar:before {
+ content: "\e109"; }
+
+.glyphicon-random:before {
+ content: "\e110"; }
+
+.glyphicon-comment:before {
+ content: "\e111"; }
+
+.glyphicon-magnet:before {
+ content: "\e112"; }
+
+.glyphicon-chevron-up:before {
+ content: "\e113"; }
+
+.glyphicon-chevron-down:before {
+ content: "\e114"; }
+
+.glyphicon-retweet:before {
+ content: "\e115"; }
+
+.glyphicon-shopping-cart:before {
+ content: "\e116"; }
+
+.glyphicon-folder-close:before {
+ content: "\e117"; }
+
+.glyphicon-folder-open:before {
+ content: "\e118"; }
+
+.glyphicon-resize-vertical:before {
+ content: "\e119"; }
+
+.glyphicon-resize-horizontal:before {
+ content: "\e120"; }
+
+.glyphicon-hdd:before {
+ content: "\e121"; }
+
+.glyphicon-bullhorn:before {
+ content: "\e122"; }
+
+.glyphicon-bell:before {
+ content: "\e123"; }
+
+.glyphicon-certificate:before {
+ content: "\e124"; }
+
+.glyphicon-thumbs-up:before {
+ content: "\e125"; }
+
+.glyphicon-thumbs-down:before {
+ content: "\e126"; }
+
+.glyphicon-hand-right:before {
+ content: "\e127"; }
+
+.glyphicon-hand-left:before {
+ content: "\e128"; }
+
+.glyphicon-hand-up:before {
+ content: "\e129"; }
+
+.glyphicon-hand-down:before {
+ content: "\e130"; }
+
+.glyphicon-circle-arrow-right:before {
+ content: "\e131"; }
+
+.glyphicon-circle-arrow-left:before {
+ content: "\e132"; }
+
+.glyphicon-circle-arrow-up:before {
+ content: "\e133"; }
+
+.glyphicon-circle-arrow-down:before {
+ content: "\e134"; }
+
+.glyphicon-globe:before {
+ content: "\e135"; }
+
+.glyphicon-wrench:before {
+ content: "\e136"; }
+
+.glyphicon-tasks:before {
+ content: "\e137"; }
+
+.glyphicon-filter:before {
+ content: "\e138"; }
+
+.glyphicon-briefcase:before {
+ content: "\e139"; }
+
+.glyphicon-fullscreen:before {
+ content: "\e140"; }
+
+.glyphicon-dashboard:before {
+ content: "\e141"; }
+
+.glyphicon-paperclip:before {
+ content: "\e142"; }
+
+.glyphicon-heart-empty:before {
+ content: "\e143"; }
+
+.glyphicon-link:before {
+ content: "\e144"; }
+
+.glyphicon-phone:before {
+ content: "\e145"; }
+
+.glyphicon-pushpin:before {
+ content: "\e146"; }
+
+.glyphicon-usd:before {
+ content: "\e148"; }
+
+.glyphicon-gbp:before {
+ content: "\e149"; }
+
+.glyphicon-sort:before {
+ content: "\e150"; }
+
+.glyphicon-sort-by-alphabet:before {
+ content: "\e151"; }
+
+.glyphicon-sort-by-alphabet-alt:before {
+ content: "\e152"; }
+
+.glyphicon-sort-by-order:before {
+ content: "\e153"; }
+
+.glyphicon-sort-by-order-alt:before {
+ content: "\e154"; }
+
+.glyphicon-sort-by-attributes:before {
+ content: "\e155"; }
+
+.glyphicon-sort-by-attributes-alt:before {
+ content: "\e156"; }
+
+.glyphicon-unchecked:before {
+ content: "\e157"; }
+
+.glyphicon-expand:before {
+ content: "\e158"; }
+
+.glyphicon-collapse-down:before {
+ content: "\e159"; }
+
+.glyphicon-collapse-up:before {
+ content: "\e160"; }
+
+.glyphicon-log-in:before {
+ content: "\e161"; }
+
+.glyphicon-flash:before {
+ content: "\e162"; }
+
+.glyphicon-log-out:before {
+ content: "\e163"; }
+
+.glyphicon-new-window:before {
+ content: "\e164"; }
+
+.glyphicon-record:before {
+ content: "\e165"; }
+
+.glyphicon-save:before {
+ content: "\e166"; }
+
+.glyphicon-open:before {
+ content: "\e167"; }
+
+.glyphicon-saved:before {
+ content: "\e168"; }
+
+.glyphicon-import:before {
+ content: "\e169"; }
+
+.glyphicon-export:before {
+ content: "\e170"; }
+
+.glyphicon-send:before {
+ content: "\e171"; }
+
+.glyphicon-floppy-disk:before {
+ content: "\e172"; }
+
+.glyphicon-floppy-saved:before {
+ content: "\e173"; }
+
+.glyphicon-floppy-remove:before {
+ content: "\e174"; }
+
+.glyphicon-floppy-save:before {
+ content: "\e175"; }
+
+.glyphicon-floppy-open:before {
+ content: "\e176"; }
+
+.glyphicon-credit-card:before {
+ content: "\e177"; }
+
+.glyphicon-transfer:before {
+ content: "\e178"; }
+
+.glyphicon-cutlery:before {
+ content: "\e179"; }
+
+.glyphicon-header:before {
+ content: "\e180"; }
+
+.glyphicon-compressed:before {
+ content: "\e181"; }
+
+.glyphicon-earphone:before {
+ content: "\e182"; }
+
+.glyphicon-phone-alt:before {
+ content: "\e183"; }
+
+.glyphicon-tower:before {
+ content: "\e184"; }
+
+.glyphicon-stats:before {
+ content: "\e185"; }
+
+.glyphicon-sd-video:before {
+ content: "\e186"; }
+
+.glyphicon-hd-video:before {
+ content: "\e187"; }
+
+.glyphicon-subtitles:before {
+ content: "\e188"; }
+
+.glyphicon-sound-stereo:before {
+ content: "\e189"; }
+
+.glyphicon-sound-dolby:before {
+ content: "\e190"; }
+
+.glyphicon-sound-5-1:before {
+ content: "\e191"; }
+
+.glyphicon-sound-6-1:before {
+ content: "\e192"; }
+
+.glyphicon-sound-7-1:before {
+ content: "\e193"; }
+
+.glyphicon-copyright-mark:before {
+ content: "\e194"; }
+
+.glyphicon-registration-mark:before {
+ content: "\e195"; }
+
+.glyphicon-cloud-download:before {
+ content: "\e197"; }
+
+.glyphicon-cloud-upload:before {
+ content: "\e198"; }
+
+.glyphicon-tree-conifer:before {
+ content: "\e199"; }
+
+.glyphicon-tree-deciduous:before {
+ content: "\e200"; }
+
+* {
+ box-sizing: border-box; }
+
+*:before, *:after {
+ box-sizing: border-box; }
+
+html {
+ font-size: 62.5%;
+ -webkit-tap-highlight-color: rgba(0, 0, 0, 0); }
+
+body {
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ font-size: 14px;
+ line-height: 1.42857;
+ color: #333333;
+ background-color: #fff; }
+
+input, button, select, textarea {
+ font-family: inherit;
+ font-size: inherit;
+ line-height: inherit; }
+
+a {
+ color: #428bca;
+ text-decoration: none; }
+ a:hover, a:focus {
+ color: #2a6596;
+ text-decoration: underline; }
+ a:focus {
+ outline: thin dotted;
+ outline: 5px auto -webkit-focus-ring-color;
+ outline-offset: -2px; }
+
+figure {
+ margin: 0; }
+
+img {
+ vertical-align: middle; }
+
+.img-responsive {
+ display: block;
+ max-width: 100%;
+ height: auto; }
+
+.img-rounded {
+ border-radius: 6px; }
+
+.img-thumbnail {
+ padding: 4px;
+ line-height: 1.42857;
+ background-color: #fff;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ transition: all 0.2s ease-in-out;
+ display: inline-block;
+ max-width: 100%;
+ height: auto; }
+
+.img-circle {
+ border-radius: 50%; }
+
+hr {
+ margin-top: 20px;
+ margin-bottom: 20px;
+ border: 0;
+ border-top: 1px solid #eeeeee; }
+
+.sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ margin: -1px;
+ padding: 0;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ border: 0; }
+
+.sr-only-focusable:active, .sr-only-focusable:focus {
+ position: static;
+ width: auto;
+ height: auto;
+ margin: 0;
+ overflow: visible;
+ clip: auto; }
+
+h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6 {
+ font-family: inherit;
+ font-weight: 500;
+ line-height: 1.1;
+ color: inherit; }
+ h1 small, h1 .small, h2 small, h2 .small, h3 small, h3 .small, h4 small, h4 .small, h5 small, h5 .small, h6 small, h6 .small, .h1 small, .h1 .small, .h2 small, .h2 .small, .h3 small, .h3 .small, .h4 small, .h4 .small, .h5 small, .h5 .small, .h6 small, .h6 .small {
+ font-weight: normal;
+ line-height: 1;
+ color: #999999; }
+
+h1, .h1, h2, .h2, h3, .h3 {
+ margin-top: 20px;
+ margin-bottom: 10px; }
+ h1 small, h1 .small, .h1 small, .h1 .small, h2 small, h2 .small, .h2 small, .h2 .small, h3 small, h3 .small, .h3 small, .h3 .small {
+ font-size: 65%; }
+
+h4, .h4, h5, .h5, h6, .h6 {
+ margin-top: 10px;
+ margin-bottom: 10px; }
+ h4 small, h4 .small, .h4 small, .h4 .small, h5 small, h5 .small, .h5 small, .h5 .small, h6 small, h6 .small, .h6 small, .h6 .small {
+ font-size: 75%; }
+
+h1, .h1 {
+ font-size: 36px; }
+
+h2, .h2 {
+ font-size: 30px; }
+
+h3, .h3 {
+ font-size: 24px; }
+
+h4, .h4 {
+ font-size: 18px; }
+
+h5, .h5 {
+ font-size: 14px; }
+
+h6, .h6 {
+ font-size: 12px; }
+
+p {
+ margin: 0 0 10px; }
+
+.lead {
+ margin-bottom: 20px;
+ font-size: 16px;
+ font-weight: 200;
+ line-height: 1.4; }
+ @media (min-width: 768px) {
+ .lead {
+ font-size: 21px; } }
+
+small, .small {
+ font-size: 85%; }
+
+cite {
+ font-style: normal; }
+
+mark, .mark {
+ background-color: #fcf8e3;
+ padding: 0.2em; }
+
+.text-left {
+ text-align: left; }
+
+.text-right {
+ text-align: right; }
+
+.text-center {
+ text-align: center; }
+
+.text-justify {
+ text-align: justify; }
+
+.text-muted {
+ color: #999999; }
+
+.text-primary {
+ color: #428bca; }
+
+a.text-primary:hover {
+ color: #3073a9; }
+
+.text-success {
+ color: #3c763d; }
+
+a.text-success:hover {
+ color: #2b542b; }
+
+.text-info {
+ color: #31708f; }
+
+a.text-info:hover {
+ color: #245369; }
+
+.text-warning {
+ color: #8a6d3b; }
+
+a.text-warning:hover {
+ color: #66502c; }
+
+.text-danger {
+ color: #a94442; }
+
+a.text-danger:hover {
+ color: #843534; }
+
+.bg-primary {
+ color: #fff; }
+
+.bg-primary {
+ background-color: #428bca; }
+
+a.bg-primary:hover {
+ background-color: #3073a9; }
+
+.bg-success {
+ background-color: #dff0d8; }
+
+a.bg-success:hover {
+ background-color: #c1e2b3; }
+
+.bg-info {
+ background-color: #d9edf7; }
+
+a.bg-info:hover {
+ background-color: #afdaee; }
+
+.bg-warning {
+ background-color: #fcf8e3; }
+
+a.bg-warning:hover {
+ background-color: #f7ecb5; }
+
+.bg-danger {
+ background-color: #f2dede; }
+
+a.bg-danger:hover {
+ background-color: #e4b9b9; }
+
+.page-header {
+ padding-bottom: 9px;
+ margin: 40px 0 20px;
+ border-bottom: 1px solid #eeeeee; }
+
+ul, ol {
+ margin-top: 0;
+ margin-bottom: 10px; }
+ ul ul, ul ol, ol ul, ol ol {
+ margin-bottom: 0; }
+
+.list-unstyled, .list-inline {
+ padding-left: 0;
+ list-style: none; }
+
+.list-inline {
+ margin-left: -5px; }
+ .list-inline > li {
+ display: inline-block;
+ padding-left: 5px;
+ padding-right: 5px; }
+
+dl {
+ margin-top: 0;
+ margin-bottom: 20px; }
+
+dt, dd {
+ line-height: 1.42857; }
+
+dt {
+ font-weight: bold; }
+
+dd {
+ margin-left: 0; }
+
+.dl-horizontal dd:before, .dl-horizontal dd:after {
+ content: " ";
+ display: table; }
+.dl-horizontal dd:after {
+ clear: both; }
+@media (min-width: 768px) {
+ .dl-horizontal dt {
+ float: left;
+ width: 160px;
+ clear: left;
+ text-align: right;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap; }
+ .dl-horizontal dd {
+ margin-left: 180px; } }
+
+abbr[title], abbr[data-original-title] {
+ cursor: help;
+ border-bottom: 1px dotted #999999; }
+
+.initialism {
+ font-size: 90%;
+ text-transform: uppercase; }
+
+blockquote {
+ padding: 10px 20px;
+ margin: 0 0 20px;
+ font-size: 17.5px;
+ border-left: 5px solid #eeeeee; }
+ blockquote p:last-child, blockquote ul:last-child, blockquote ol:last-child {
+ margin-bottom: 0; }
+ blockquote footer, blockquote small, blockquote .small {
+ display: block;
+ font-size: 80%;
+ line-height: 1.42857;
+ color: #999999; }
+ blockquote footer:before, blockquote small:before, blockquote .small:before {
+ content: '\2014 \00A0'; }
+
+.blockquote-reverse, blockquote.pull-right {
+ padding-right: 15px;
+ padding-left: 0;
+ border-right: 5px solid #eeeeee;
+ border-left: 0;
+ text-align: right; }
+ .blockquote-reverse footer:before, .blockquote-reverse small:before, .blockquote-reverse .small:before, blockquote.pull-right footer:before, blockquote.pull-right small:before, blockquote.pull-right .small:before {
+ content: ''; }
+ .blockquote-reverse footer:after, .blockquote-reverse small:after, .blockquote-reverse .small:after, blockquote.pull-right footer:after, blockquote.pull-right small:after, blockquote.pull-right .small:after {
+ content: '\00A0 \2014'; }
+
+blockquote:before, blockquote:after {
+ content: ""; }
+
+address {
+ margin-bottom: 20px;
+ font-style: normal;
+ line-height: 1.42857; }
+
+code, kbd, pre, samp {
+ font-family: Menlo, Monaco, Consolas, "Courier New", monospace; }
+
+code {
+ padding: 2px 4px;
+ font-size: 90%;
+ color: #c7254e;
+ background-color: #f9f2f4;
+ border-radius: 4px; }
+
+kbd {
+ padding: 2px 4px;
+ font-size: 90%;
+ color: #fff;
+ background-color: #333;
+ border-radius: 3px;
+ box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25); }
+
+pre {
+ display: block;
+ padding: 9.5px;
+ margin: 0 0 10px;
+ font-size: 13px;
+ line-height: 1.42857;
+ word-break: break-all;
+ word-wrap: break-word;
+ color: #333333;
+ background-color: #f5f5f5;
+ border: 1px solid #ccc;
+ border-radius: 4px; }
+ pre code {
+ padding: 0;
+ font-size: inherit;
+ color: inherit;
+ white-space: pre-wrap;
+ background-color: transparent;
+ border-radius: 0; }
+
+.pre-scrollable {
+ max-height: 340px;
+ overflow-y: scroll; }
+
+.container {
+ margin-right: auto;
+ margin-left: auto;
+ padding-left: 15px;
+ padding-right: 15px; }
+ .container:before, .container:after {
+ content: " ";
+ display: table; }
+ .container:after {
+ clear: both; }
+ @media (min-width: 768px) {
+ .container {
+ width: 750px; } }
+ @media (min-width: 992px) {
+ .container {
+ width: 970px; } }
+ @media (min-width: 1200px) {
+ .container {
+ width: 1170px; } }
+
+.container-fluid {
+ margin-right: auto;
+ margin-left: auto;
+ padding-left: 15px;
+ padding-right: 15px; }
+ .container-fluid:before, .container-fluid:after {
+ content: " ";
+ display: table; }
+ .container-fluid:after {
+ clear: both; }
+
+.row {
+ margin-left: -15px;
+ margin-right: -15px; }
+ .row:before, .row:after {
+ content: " ";
+ display: table; }
+ .row:after {
+ clear: both; }
+
+.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12 {
+ position: relative;
+ min-height: 1px;
+ padding-left: 15px;
+ padding-right: 15px; }
+
+.col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12 {
+ float: left; }
+
+.col-xs-1 {
+ width: 8.33333%; }
+
+.col-xs-2 {
+ width: 16.66667%; }
+
+.col-xs-3 {
+ width: 25%; }
+
+.col-xs-4 {
+ width: 33.33333%; }
+
+.col-xs-5 {
+ width: 41.66667%; }
+
+.col-xs-6 {
+ width: 50%; }
+
+.col-xs-7 {
+ width: 58.33333%; }
+
+.col-xs-8 {
+ width: 66.66667%; }
+
+.col-xs-9 {
+ width: 75%; }
+
+.col-xs-10 {
+ width: 83.33333%; }
+
+.col-xs-11 {
+ width: 91.66667%; }
+
+.col-xs-12 {
+ width: 100%; }
+
+.col-xs-pull-0 {
+ right: auto; }
+
+.col-xs-pull-1 {
+ right: 8.33333%; }
+
+.col-xs-pull-2 {
+ right: 16.66667%; }
+
+.col-xs-pull-3 {
+ right: 25%; }
+
+.col-xs-pull-4 {
+ right: 33.33333%; }
+
+.col-xs-pull-5 {
+ right: 41.66667%; }
+
+.col-xs-pull-6 {
+ right: 50%; }
+
+.col-xs-pull-7 {
+ right: 58.33333%; }
+
+.col-xs-pull-8 {
+ right: 66.66667%; }
+
+.col-xs-pull-9 {
+ right: 75%; }
+
+.col-xs-pull-10 {
+ right: 83.33333%; }
+
+.col-xs-pull-11 {
+ right: 91.66667%; }
+
+.col-xs-pull-12 {
+ right: 100%; }
+
+.col-xs-push-0 {
+ left: auto; }
+
+.col-xs-push-1 {
+ left: 8.33333%; }
+
+.col-xs-push-2 {
+ left: 16.66667%; }
+
+.col-xs-push-3 {
+ left: 25%; }
+
+.col-xs-push-4 {
+ left: 33.33333%; }
+
+.col-xs-push-5 {
+ left: 41.66667%; }
+
+.col-xs-push-6 {
+ left: 50%; }
+
+.col-xs-push-7 {
+ left: 58.33333%; }
+
+.col-xs-push-8 {
+ left: 66.66667%; }
+
+.col-xs-push-9 {
+ left: 75%; }
+
+.col-xs-push-10 {
+ left: 83.33333%; }
+
+.col-xs-push-11 {
+ left: 91.66667%; }
+
+.col-xs-push-12 {
+ left: 100%; }
+
+.col-xs-offset-0 {
+ margin-left: 0%; }
+
+.col-xs-offset-1 {
+ margin-left: 8.33333%; }
+
+.col-xs-offset-2 {
+ margin-left: 16.66667%; }
+
+.col-xs-offset-3 {
+ margin-left: 25%; }
+
+.col-xs-offset-4 {
+ margin-left: 33.33333%; }
+
+.col-xs-offset-5 {
+ margin-left: 41.66667%; }
+
+.col-xs-offset-6 {
+ margin-left: 50%; }
+
+.col-xs-offset-7 {
+ margin-left: 58.33333%; }
+
+.col-xs-offset-8 {
+ margin-left: 66.66667%; }
+
+.col-xs-offset-9 {
+ margin-left: 75%; }
+
+.col-xs-offset-10 {
+ margin-left: 83.33333%; }
+
+.col-xs-offset-11 {
+ margin-left: 91.66667%; }
+
+.col-xs-offset-12 {
+ margin-left: 100%; }
+
+@media (min-width: 768px) {
+ .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12 {
+ float: left; }
+ .col-sm-1 {
+ width: 8.33333%; }
+ .col-sm-2 {
+ width: 16.66667%; }
+ .col-sm-3 {
+ width: 25%; }
+ .col-sm-4 {
+ width: 33.33333%; }
+ .col-sm-5 {
+ width: 41.66667%; }
+ .col-sm-6 {
+ width: 50%; }
+ .col-sm-7 {
+ width: 58.33333%; }
+ .col-sm-8 {
+ width: 66.66667%; }
+ .col-sm-9 {
+ width: 75%; }
+ .col-sm-10 {
+ width: 83.33333%; }
+ .col-sm-11 {
+ width: 91.66667%; }
+ .col-sm-12 {
+ width: 100%; }
+ .col-sm-pull-0 {
+ right: auto; }
+ .col-sm-pull-1 {
+ right: 8.33333%; }
+ .col-sm-pull-2 {
+ right: 16.66667%; }
+ .col-sm-pull-3 {
+ right: 25%; }
+ .col-sm-pull-4 {
+ right: 33.33333%; }
+ .col-sm-pull-5 {
+ right: 41.66667%; }
+ .col-sm-pull-6 {
+ right: 50%; }
+ .col-sm-pull-7 {
+ right: 58.33333%; }
+ .col-sm-pull-8 {
+ right: 66.66667%; }
+ .col-sm-pull-9 {
+ right: 75%; }
+ .col-sm-pull-10 {
+ right: 83.33333%; }
+ .col-sm-pull-11 {
+ right: 91.66667%; }
+ .col-sm-pull-12 {
+ right: 100%; }
+ .col-sm-push-0 {
+ left: auto; }
+ .col-sm-push-1 {
+ left: 8.33333%; }
+ .col-sm-push-2 {
+ left: 16.66667%; }
+ .col-sm-push-3 {
+ left: 25%; }
+ .col-sm-push-4 {
+ left: 33.33333%; }
+ .col-sm-push-5 {
+ left: 41.66667%; }
+ .col-sm-push-6 {
+ left: 50%; }
+ .col-sm-push-7 {
+ left: 58.33333%; }
+ .col-sm-push-8 {
+ left: 66.66667%; }
+ .col-sm-push-9 {
+ left: 75%; }
+ .col-sm-push-10 {
+ left: 83.33333%; }
+ .col-sm-push-11 {
+ left: 91.66667%; }
+ .col-sm-push-12 {
+ left: 100%; }
+ .col-sm-offset-0 {
+ margin-left: 0%; }
+ .col-sm-offset-1 {
+ margin-left: 8.33333%; }
+ .col-sm-offset-2 {
+ margin-left: 16.66667%; }
+ .col-sm-offset-3 {
+ margin-left: 25%; }
+ .col-sm-offset-4 {
+ margin-left: 33.33333%; }
+ .col-sm-offset-5 {
+ margin-left: 41.66667%; }
+ .col-sm-offset-6 {
+ margin-left: 50%; }
+ .col-sm-offset-7 {
+ margin-left: 58.33333%; }
+ .col-sm-offset-8 {
+ margin-left: 66.66667%; }
+ .col-sm-offset-9 {
+ margin-left: 75%; }
+ .col-sm-offset-10 {
+ margin-left: 83.33333%; }
+ .col-sm-offset-11 {
+ margin-left: 91.66667%; }
+ .col-sm-offset-12 {
+ margin-left: 100%; } }
+
+@media (min-width: 992px) {
+ .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12 {
+ float: left; }
+ .col-md-1 {
+ width: 8.33333%; }
+ .col-md-2 {
+ width: 16.66667%; }
+ .col-md-3 {
+ width: 25%; }
+ .col-md-4 {
+ width: 33.33333%; }
+ .col-md-5 {
+ width: 41.66667%; }
+ .col-md-6 {
+ width: 50%; }
+ .col-md-7 {
+ width: 58.33333%; }
+ .col-md-8 {
+ width: 66.66667%; }
+ .col-md-9 {
+ width: 75%; }
+ .col-md-10 {
+ width: 83.33333%; }
+ .col-md-11 {
+ width: 91.66667%; }
+ .col-md-12 {
+ width: 100%; }
+ .col-md-pull-0 {
+ right: auto; }
+ .col-md-pull-1 {
+ right: 8.33333%; }
+ .col-md-pull-2 {
+ right: 16.66667%; }
+ .col-md-pull-3 {
+ right: 25%; }
+ .col-md-pull-4 {
+ right: 33.33333%; }
+ .col-md-pull-5 {
+ right: 41.66667%; }
+ .col-md-pull-6 {
+ right: 50%; }
+ .col-md-pull-7 {
+ right: 58.33333%; }
+ .col-md-pull-8 {
+ right: 66.66667%; }
+ .col-md-pull-9 {
+ right: 75%; }
+ .col-md-pull-10 {
+ right: 83.33333%; }
+ .col-md-pull-11 {
+ right: 91.66667%; }
+ .col-md-pull-12 {
+ right: 100%; }
+ .col-md-push-0 {
+ left: auto; }
+ .col-md-push-1 {
+ left: 8.33333%; }
+ .col-md-push-2 {
+ left: 16.66667%; }
+ .col-md-push-3 {
+ left: 25%; }
+ .col-md-push-4 {
+ left: 33.33333%; }
+ .col-md-push-5 {
+ left: 41.66667%; }
+ .col-md-push-6 {
+ left: 50%; }
+ .col-md-push-7 {
+ left: 58.33333%; }
+ .col-md-push-8 {
+ left: 66.66667%; }
+ .col-md-push-9 {
+ left: 75%; }
+ .col-md-push-10 {
+ left: 83.33333%; }
+ .col-md-push-11 {
+ left: 91.66667%; }
+ .col-md-push-12 {
+ left: 100%; }
+ .col-md-offset-0 {
+ margin-left: 0%; }
+ .col-md-offset-1 {
+ margin-left: 8.33333%; }
+ .col-md-offset-2 {
+ margin-left: 16.66667%; }
+ .col-md-offset-3 {
+ margin-left: 25%; }
+ .col-md-offset-4 {
+ margin-left: 33.33333%; }
+ .col-md-offset-5 {
+ margin-left: 41.66667%; }
+ .col-md-offset-6 {
+ margin-left: 50%; }
+ .col-md-offset-7 {
+ margin-left: 58.33333%; }
+ .col-md-offset-8 {
+ margin-left: 66.66667%; }
+ .col-md-offset-9 {
+ margin-left: 75%; }
+ .col-md-offset-10 {
+ margin-left: 83.33333%; }
+ .col-md-offset-11 {
+ margin-left: 91.66667%; }
+ .col-md-offset-12 {
+ margin-left: 100%; } }
+
+@media (min-width: 1200px) {
+ .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12 {
+ float: left; }
+ .col-lg-1 {
+ width: 8.33333%; }
+ .col-lg-2 {
+ width: 16.66667%; }
+ .col-lg-3 {
+ width: 25%; }
+ .col-lg-4 {
+ width: 33.33333%; }
+ .col-lg-5 {
+ width: 41.66667%; }
+ .col-lg-6 {
+ width: 50%; }
+ .col-lg-7 {
+ width: 58.33333%; }
+ .col-lg-8 {
+ width: 66.66667%; }
+ .col-lg-9 {
+ width: 75%; }
+ .col-lg-10 {
+ width: 83.33333%; }
+ .col-lg-11 {
+ width: 91.66667%; }
+ .col-lg-12 {
+ width: 100%; }
+ .col-lg-pull-0 {
+ right: auto; }
+ .col-lg-pull-1 {
+ right: 8.33333%; }
+ .col-lg-pull-2 {
+ right: 16.66667%; }
+ .col-lg-pull-3 {
+ right: 25%; }
+ .col-lg-pull-4 {
+ right: 33.33333%; }
+ .col-lg-pull-5 {
+ right: 41.66667%; }
+ .col-lg-pull-6 {
+ right: 50%; }
+ .col-lg-pull-7 {
+ right: 58.33333%; }
+ .col-lg-pull-8 {
+ right: 66.66667%; }
+ .col-lg-pull-9 {
+ right: 75%; }
+ .col-lg-pull-10 {
+ right: 83.33333%; }
+ .col-lg-pull-11 {
+ right: 91.66667%; }
+ .col-lg-pull-12 {
+ right: 100%; }
+ .col-lg-push-0 {
+ left: auto; }
+ .col-lg-push-1 {
+ left: 8.33333%; }
+ .col-lg-push-2 {
+ left: 16.66667%; }
+ .col-lg-push-3 {
+ left: 25%; }
+ .col-lg-push-4 {
+ left: 33.33333%; }
+ .col-lg-push-5 {
+ left: 41.66667%; }
+ .col-lg-push-6 {
+ left: 50%; }
+ .col-lg-push-7 {
+ left: 58.33333%; }
+ .col-lg-push-8 {
+ left: 66.66667%; }
+ .col-lg-push-9 {
+ left: 75%; }
+ .col-lg-push-10 {
+ left: 83.33333%; }
+ .col-lg-push-11 {
+ left: 91.66667%; }
+ .col-lg-push-12 {
+ left: 100%; }
+ .col-lg-offset-0 {
+ margin-left: 0%; }
+ .col-lg-offset-1 {
+ margin-left: 8.33333%; }
+ .col-lg-offset-2 {
+ margin-left: 16.66667%; }
+ .col-lg-offset-3 {
+ margin-left: 25%; }
+ .col-lg-offset-4 {
+ margin-left: 33.33333%; }
+ .col-lg-offset-5 {
+ margin-left: 41.66667%; }
+ .col-lg-offset-6 {
+ margin-left: 50%; }
+ .col-lg-offset-7 {
+ margin-left: 58.33333%; }
+ .col-lg-offset-8 {
+ margin-left: 66.66667%; }
+ .col-lg-offset-9 {
+ margin-left: 75%; }
+ .col-lg-offset-10 {
+ margin-left: 83.33333%; }
+ .col-lg-offset-11 {
+ margin-left: 91.66667%; }
+ .col-lg-offset-12 {
+ margin-left: 100%; } }
+
+table {
+ max-width: 100%;
+ background-color: transparent; }
+
+th {
+ text-align: left; }
+
+.table {
+ width: 100%;
+ margin-bottom: 20px; }
+ .table > thead > tr > th, .table > thead > tr > td, .table > tbody > tr > th, .table > tbody > tr > td, .table > tfoot > tr > th, .table > tfoot > tr > td {
+ padding: 8px;
+ line-height: 1.42857;
+ vertical-align: top;
+ border-top: 1px solid #ddd; }
+ .table > thead > tr > th {
+ vertical-align: bottom;
+ border-bottom: 2px solid #ddd; }
+ .table > caption + thead > tr:first-child > th, .table > caption + thead > tr:first-child > td, .table > colgroup + thead > tr:first-child > th, .table > colgroup + thead > tr:first-child > td, .table > thead:first-child > tr:first-child > th, .table > thead:first-child > tr:first-child > td {
+ border-top: 0; }
+ .table > tbody + tbody {
+ border-top: 2px solid #ddd; }
+ .table .table {
+ background-color: #fff; }
+
+.table-condensed > thead > tr > th, .table-condensed > thead > tr > td, .table-condensed > tbody > tr > th, .table-condensed > tbody > tr > td, .table-condensed > tfoot > tr > th, .table-condensed > tfoot > tr > td {
+ padding: 5px; }
+
+.table-bordered {
+ border: 1px solid #ddd; }
+ .table-bordered > thead > tr > th, .table-bordered > thead > tr > td, .table-bordered > tbody > tr > th, .table-bordered > tbody > tr > td, .table-bordered > tfoot > tr > th, .table-bordered > tfoot > tr > td {
+ border: 1px solid #ddd; }
+ .table-bordered > thead > tr > th, .table-bordered > thead > tr > td {
+ border-bottom-width: 2px; }
+
+.table-striped > tbody > tr:nth-child(odd) > td, .table-striped > tbody > tr:nth-child(odd) > th {
+ background-color: #f9f9f9; }
+
+.table-hover > tbody > tr:hover > td, .table-hover > tbody > tr:hover > th {
+ background-color: #f5f5f5; }
+
+table col[class*="col-"] {
+ position: static;
+ float: none;
+ display: table-column; }
+
+table td[class*="col-"], table th[class*="col-"] {
+ position: static;
+ float: none;
+ display: table-cell; }
+
+.table > thead > tr > td.active, .table > thead > tr > th.active, .table > thead > tr.active > td, .table > thead > tr.active > th, .table > tbody > tr > td.active, .table > tbody > tr > th.active, .table > tbody > tr.active > td, .table > tbody > tr.active > th, .table > tfoot > tr > td.active, .table > tfoot > tr > th.active, .table > tfoot > tr.active > td, .table > tfoot > tr.active > th {
+ background-color: #f5f5f5; }
+
+.table-hover > tbody > tr > td.active:hover, .table-hover > tbody > tr > th.active:hover, .table-hover > tbody > tr.active:hover > td, .table-hover > tbody > tr:hover > .active, .table-hover > tbody > tr.active:hover > th {
+ background-color: #e8e8e8; }
+
+.table > thead > tr > td.success, .table > thead > tr > th.success, .table > thead > tr.success > td, .table > thead > tr.success > th, .table > tbody > tr > td.success, .table > tbody > tr > th.success, .table > tbody > tr.success > td, .table > tbody > tr.success > th, .table > tfoot > tr > td.success, .table > tfoot > tr > th.success, .table > tfoot > tr.success > td, .table > tfoot > tr.success > th {
+ background-color: #dff0d8; }
+
+.table-hover > tbody > tr > td.success:hover, .table-hover > tbody > tr > th.success:hover, .table-hover > tbody > tr.success:hover > td, .table-hover > tbody > tr:hover > .success, .table-hover > tbody > tr.success:hover > th {
+ background-color: #d0e9c6; }
+
+.table > thead > tr > td.info, .table > thead > tr > th.info, .table > thead > tr.info > td, .table > thead > tr.info > th, .table > tbody > tr > td.info, .table > tbody > tr > th.info, .table > tbody > tr.info > td, .table > tbody > tr.info > th, .table > tfoot > tr > td.info, .table > tfoot > tr > th.info, .table > tfoot > tr.info > td, .table > tfoot > tr.info > th {
+ background-color: #d9edf7; }
+
+.table-hover > tbody > tr > td.info:hover, .table-hover > tbody > tr > th.info:hover, .table-hover > tbody > tr.info:hover > td, .table-hover > tbody > tr:hover > .info, .table-hover > tbody > tr.info:hover > th {
+ background-color: #c4e4f3; }
+
+.table > thead > tr > td.warning, .table > thead > tr > th.warning, .table > thead > tr.warning > td, .table > thead > tr.warning > th, .table > tbody > tr > td.warning, .table > tbody > tr > th.warning, .table > tbody > tr.warning > td, .table > tbody > tr.warning > th, .table > tfoot > tr > td.warning, .table > tfoot > tr > th.warning, .table > tfoot > tr.warning > td, .table > tfoot > tr.warning > th {
+ background-color: #fcf8e3; }
+
+.table-hover > tbody > tr > td.warning:hover, .table-hover > tbody > tr > th.warning:hover, .table-hover > tbody > tr.warning:hover > td, .table-hover > tbody > tr:hover > .warning, .table-hover > tbody > tr.warning:hover > th {
+ background-color: #faf2cc; }
+
+.table > thead > tr > td.danger, .table > thead > tr > th.danger, .table > thead > tr.danger > td, .table > thead > tr.danger > th, .table > tbody > tr > td.danger, .table > tbody > tr > th.danger, .table > tbody > tr.danger > td, .table > tbody > tr.danger > th, .table > tfoot > tr > td.danger, .table > tfoot > tr > th.danger, .table > tfoot > tr.danger > td, .table > tfoot > tr.danger > th {
+ background-color: #f2dede; }
+
+.table-hover > tbody > tr > td.danger:hover, .table-hover > tbody > tr > th.danger:hover, .table-hover > tbody > tr.danger:hover > td, .table-hover > tbody > tr:hover > .danger, .table-hover > tbody > tr.danger:hover > th {
+ background-color: #ebcccc; }
+
+@media screen and (max-width: 767px) {
+ .table-responsive {
+ width: 100%;
+ margin-bottom: 15px;
+ overflow-y: hidden;
+ overflow-x: scroll;
+ -ms-overflow-style: -ms-autohiding-scrollbar;
+ border: 1px solid #ddd;
+ -webkit-overflow-scrolling: touch; }
+ .table-responsive > .table {
+ margin-bottom: 0; }
+ .table-responsive > .table > thead > tr > th, .table-responsive > .table > thead > tr > td, .table-responsive > .table > tbody > tr > th, .table-responsive > .table > tbody > tr > td, .table-responsive > .table > tfoot > tr > th, .table-responsive > .table > tfoot > tr > td {
+ white-space: nowrap; }
+ .table-responsive > .table-bordered {
+ border: 0; }
+ .table-responsive > .table-bordered > thead > tr > th:first-child, .table-responsive > .table-bordered > thead > tr > td:first-child, .table-responsive > .table-bordered > tbody > tr > th:first-child, .table-responsive > .table-bordered > tbody > tr > td:first-child, .table-responsive > .table-bordered > tfoot > tr > th:first-child, .table-responsive > .table-bordered > tfoot > tr > td:first-child {
+ border-left: 0; }
+ .table-responsive > .table-bordered > thead > tr > th:last-child, .table-responsive > .table-bordered > thead > tr > td:last-child, .table-responsive > .table-bordered > tbody > tr > th:last-child, .table-responsive > .table-bordered > tbody > tr > td:last-child, .table-responsive > .table-bordered > tfoot > tr > th:last-child, .table-responsive > .table-bordered > tfoot > tr > td:last-child {
+ border-right: 0; }
+ .table-responsive > .table-bordered > tbody > tr:last-child > th, .table-responsive > .table-bordered > tbody > tr:last-child > td, .table-responsive > .table-bordered > tfoot > tr:last-child > th, .table-responsive > .table-bordered > tfoot > tr:last-child > td {
+ border-bottom: 0; } }
+
+fieldset {
+ padding: 0;
+ margin: 0;
+ border: 0;
+ min-width: 0; }
+
+legend {
+ display: block;
+ width: 100%;
+ padding: 0;
+ margin-bottom: 20px;
+ font-size: 21px;
+ line-height: inherit;
+ color: #333333;
+ border: 0;
+ border-bottom: 1px solid #e5e5e5; }
+
+label {
+ display: inline-block;
+ max-width: 100%;
+ margin-bottom: 5px;
+ font-weight: bold; }
+
+input[type="search"] {
+ box-sizing: border-box; }
+
+input[type="radio"], input[type="checkbox"] {
+ margin: 4px 0 0;
+ margin-top: 1px \9;
+ line-height: normal; }
+
+input[type="file"] {
+ display: block; }
+
+input[type="range"] {
+ display: block;
+ width: 100%; }
+
+select[multiple], select[size] {
+ height: auto; }
+
+input[type="file"]:focus, input[type="radio"]:focus, input[type="checkbox"]:focus {
+ outline: thin dotted;
+ outline: 5px auto -webkit-focus-ring-color;
+ outline-offset: -2px; }
+
+output {
+ display: block;
+ padding-top: 7px;
+ font-size: 14px;
+ line-height: 1.42857;
+ color: #555555; }
+
+.form-control {
+ display: block;
+ width: 100%;
+ height: 34px;
+ padding: 6px 12px;
+ font-size: 14px;
+ line-height: 1.42857;
+ color: #555555;
+ background-color: #fff;
+ background-image: none;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+ transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; }
+ .form-control:focus {
+ border-color: #66afe9;
+ outline: 0;
+ box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6); }
+ .form-control::placeholder {
+ color: #999999;
+ opacity: 1; }
+ .form-control:-ms-input-placeholder {
+ color: #999999; }
+ .form-control::-webkit-input-placeholder {
+ color: #999999; }
+ .form-control[disabled], .form-control[readonly], fieldset[disabled] .form-control {
+ cursor: not-allowed;
+ background-color: #eeeeee;
+ opacity: 1; }
+
+textarea.form-control {
+ height: auto; }
+
+input[type="search"] {
+ -webkit-appearance: none; }
+
+input[type="date"], input[type="time"], input[type="datetime-local"], input[type="month"] {
+ line-height: 34px;
+ line-height: 1.42857 \0; }
+ input[type="date"].input-sm, .input-group-sm > input[type="date"].form-control, .input-group-sm > input[type="date"].input-group-addon, .input-group-sm > .input-group-btn > input[type="date"].btn, input[type="time"].input-sm, .input-group-sm > input[type="time"].form-control, .input-group-sm > input[type="time"].input-group-addon, .input-group-sm > .input-group-btn > input[type="time"].btn, input[type="datetime-local"].input-sm, .input-group-sm > input[type="datetime-local"].form-control, .input-group-sm > input[type="datetime-local"].input-group-addon, .input-group-sm > .input-group-btn > input[type="datetime-local"].btn, input[type="month"].input-sm, .input-group-sm > input[type="month"].form-control, .input-group-sm > input[type="month"].input-group-addon, .input-group-sm > .input-group-btn > input[type="month"].btn {
+ line-height: 30px; }
+ input[type="date"].input-lg, .input-group-lg > input[type="date"].form-control, .input-group-lg > input[type="date"].input-group-addon, .input-group-lg > .input-group-btn > input[type="date"].btn, input[type="time"].input-lg, .input-group-lg > input[type="time"].form-control, .input-group-lg > input[type="time"].input-group-addon, .input-group-lg > .input-group-btn > input[type="time"].btn, input[type="datetime-local"].input-lg, .input-group-lg > input[type="datetime-local"].form-control, .input-group-lg > input[type="datetime-local"].input-group-addon, .input-group-lg > .input-group-btn > input[type="datetime-local"].btn, input[type="month"].input-lg, .input-group-lg > input[type="month"].form-control, .input-group-lg > input[type="month"].input-group-addon, .input-group-lg > .input-group-btn > input[type="month"].btn {
+ line-height: 46px; }
+
+.form-group {
+ margin-bottom: 15px; }
+
+.radio, .checkbox {
+ display: block;
+ min-height: 20px;
+ margin-top: 10px;
+ margin-bottom: 10px; }
+ .radio label, .checkbox label {
+ padding-left: 20px;
+ margin-bottom: 0;
+ font-weight: normal;
+ cursor: pointer; }
+
+.radio input[type="radio"], .radio-inline input[type="radio"], .checkbox input[type="checkbox"], .checkbox-inline input[type="checkbox"] {
+ float: left;
+ margin-left: -20px; }
+
+.radio + .radio, .checkbox + .checkbox {
+ margin-top: -5px; }
+
+.radio-inline, .checkbox-inline {
+ display: inline-block;
+ padding-left: 20px;
+ margin-bottom: 0;
+ vertical-align: middle;
+ font-weight: normal;
+ cursor: pointer; }
+
+.radio-inline + .radio-inline, .checkbox-inline + .checkbox-inline {
+ margin-top: 0;
+ margin-left: 10px; }
+
+input[type="radio"][disabled], fieldset[disabled] input[type="radio"], input[type="checkbox"][disabled], fieldset[disabled] input[type="checkbox"], .radio[disabled], fieldset[disabled] .radio, .radio-inline[disabled], fieldset[disabled] .radio-inline, .checkbox[disabled], fieldset[disabled] .checkbox, .checkbox-inline[disabled], fieldset[disabled] .checkbox-inline {
+ cursor: not-allowed; }
+
+.input-sm, .input-group-sm > .form-control, .input-group-sm > .input-group-addon, .input-group-sm > .input-group-btn > .btn {
+ height: 30px;
+ padding: 5px 10px;
+ font-size: 12px;
+ line-height: 1.5;
+ border-radius: 3px; }
+
+select.input-sm, .input-group-sm > select.form-control, .input-group-sm > select.input-group-addon, .input-group-sm > .input-group-btn > select.btn {
+ height: 30px;
+ line-height: 30px; }
+
+textarea.input-sm, .input-group-sm > textarea.form-control, .input-group-sm > textarea.input-group-addon, .input-group-sm > .input-group-btn > textarea.btn, select[multiple].input-sm, .input-group-sm > select[multiple].form-control, .input-group-sm > select[multiple].input-group-addon, .input-group-sm > .input-group-btn > select[multiple].btn {
+ height: auto; }
+
+.input-lg, .input-group-lg > .form-control, .input-group-lg > .input-group-addon, .input-group-lg > .input-group-btn > .btn {
+ height: 46px;
+ padding: 10px 16px;
+ font-size: 18px;
+ line-height: 1.33;
+ border-radius: 6px; }
+
+select.input-lg, .input-group-lg > select.form-control, .input-group-lg > select.input-group-addon, .input-group-lg > .input-group-btn > select.btn {
+ height: 46px;
+ line-height: 46px; }
+
+textarea.input-lg, .input-group-lg > textarea.form-control, .input-group-lg > textarea.input-group-addon, .input-group-lg > .input-group-btn > textarea.btn, select[multiple].input-lg, .input-group-lg > select[multiple].form-control, .input-group-lg > select[multiple].input-group-addon, .input-group-lg > .input-group-btn > select[multiple].btn {
+ height: auto; }
+
+.has-feedback {
+ position: relative; }
+ .has-feedback .form-control {
+ padding-right: 42.5px; }
+
+.form-control-feedback {
+ position: absolute;
+ top: 25px;
+ right: 0;
+ z-index: 2;
+ display: block;
+ width: 34px;
+ height: 34px;
+ line-height: 34px;
+ text-align: center; }
+
+.input-lg + .form-control-feedback, .input-lg + .input-group-lg > .form-control, .input-group-lg > .input-lg + .form-control, .input-lg + .input-group-lg > .input-group-addon, .input-group-lg > .input-lg + .input-group-addon, .input-lg + .input-group-lg > .input-group-btn > .btn, .input-group-lg > .input-group-btn > .input-lg + .btn {
+ width: 46px;
+ height: 46px;
+ line-height: 46px; }
+
+.input-sm + .form-control-feedback, .input-sm + .input-group-sm > .form-control, .input-group-sm > .input-sm + .form-control, .input-sm + .input-group-sm > .input-group-addon, .input-group-sm > .input-sm + .input-group-addon, .input-sm + .input-group-sm > .input-group-btn > .btn, .input-group-sm > .input-group-btn > .input-sm + .btn {
+ width: 30px;
+ height: 30px;
+ line-height: 30px; }
+
+.has-success .help-block, .has-success .control-label, .has-success .radio, .has-success .checkbox, .has-success .radio-inline, .has-success .checkbox-inline {
+ color: #3c763d; }
+.has-success .form-control {
+ border-color: #3c763d;
+ box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); }
+ .has-success .form-control:focus {
+ border-color: #2b542b;
+ box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168; }
+.has-success .input-group-addon {
+ color: #3c763d;
+ border-color: #3c763d;
+ background-color: #dff0d8; }
+.has-success .form-control-feedback {
+ color: #3c763d; }
+
+.has-warning .help-block, .has-warning .control-label, .has-warning .radio, .has-warning .checkbox, .has-warning .radio-inline, .has-warning .checkbox-inline {
+ color: #8a6d3b; }
+.has-warning .form-control {
+ border-color: #8a6d3b;
+ box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); }
+ .has-warning .form-control:focus {
+ border-color: #66502c;
+ box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c09f6b; }
+.has-warning .input-group-addon {
+ color: #8a6d3b;
+ border-color: #8a6d3b;
+ background-color: #fcf8e3; }
+.has-warning .form-control-feedback {
+ color: #8a6d3b; }
+
+.has-error .help-block, .has-error .control-label, .has-error .radio, .has-error .checkbox, .has-error .radio-inline, .has-error .checkbox-inline {
+ color: #a94442; }
+.has-error .form-control {
+ border-color: #a94442;
+ box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); }
+ .has-error .form-control:focus {
+ border-color: #843534;
+ box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483; }
+.has-error .input-group-addon {
+ color: #a94442;
+ border-color: #a94442;
+ background-color: #f2dede; }
+.has-error .form-control-feedback {
+ color: #a94442; }
+
+.form-control-static {
+ margin-bottom: 0; }
+
+.help-block {
+ display: block;
+ margin-top: 5px;
+ margin-bottom: 10px;
+ color: #737373; }
+
+@media (min-width: 768px) {
+ .form-inline .form-group, .form-inline .navbar-form {
+ display: inline-block;
+ margin-bottom: 0;
+ vertical-align: middle; }
+ .form-inline .form-control, .form-inline .navbar-form {
+ display: inline-block;
+ width: auto;
+ vertical-align: middle; }
+ .form-inline .input-group, .form-inline .navbar-form {
+ display: inline-table;
+ vertical-align: middle; }
+ .form-inline .input-group .input-group-addon, .form-inline .input-group .navbar-form, .form-inline .input-group .input-group-btn, .form-inline .input-group .navbar-form, .form-inline .input-group .form-control, .form-inline .input-group .navbar-form {
+ width: auto; }
+ .form-inline .input-group > .form-control, .form-inline .input-group > .navbar-form {
+ width: 100%; }
+ .form-inline .control-label, .form-inline .navbar-form {
+ margin-bottom: 0;
+ vertical-align: middle; }
+ .form-inline .radio, .form-inline .navbar-form, .form-inline .checkbox, .form-inline .navbar-form {
+ display: inline-block;
+ margin-top: 0;
+ margin-bottom: 0;
+ padding-left: 0;
+ vertical-align: middle; }
+ .form-inline .radio input[type="radio"], .form-inline .radio .navbar-form, .form-inline .checkbox input[type="checkbox"], .form-inline .checkbox .navbar-form {
+ float: none;
+ margin-left: 0; }
+ .form-inline .has-feedback .form-control-feedback, .form-inline .has-feedback .navbar-form {
+ top: 0; } }
+
+.form-horizontal .radio, .form-horizontal .checkbox, .form-horizontal .radio-inline, .form-horizontal .checkbox-inline {
+ margin-top: 0;
+ margin-bottom: 0;
+ padding-top: 7px; }
+.form-horizontal .radio, .form-horizontal .checkbox {
+ min-height: 27px; }
+.form-horizontal .form-group {
+ margin-left: -15px;
+ margin-right: -15px; }
+ .form-horizontal .form-group:before, .form-horizontal .form-group:after {
+ content: " ";
+ display: table; }
+ .form-horizontal .form-group:after {
+ clear: both; }
+.form-horizontal .form-control-static {
+ padding-top: 7px;
+ padding-bottom: 7px; }
+@media (min-width: 768px) {
+ .form-horizontal .control-label {
+ text-align: right;
+ margin-bottom: 0;
+ padding-top: 7px; } }
+.form-horizontal .has-feedback .form-control-feedback {
+ top: 0;
+ right: 15px; }
+
+.btn {
+ display: inline-block;
+ margin-bottom: 0;
+ font-weight: normal;
+ text-align: center;
+ vertical-align: middle;
+ cursor: pointer;
+ background-image: none;
+ border: 1px solid transparent;
+ white-space: nowrap;
+ padding: 6px 12px;
+ font-size: 14px;
+ line-height: 1.42857;
+ border-radius: 4px;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none; }
+ .btn:focus, .btn:active:focus, .btn.active:focus {
+ outline: thin dotted;
+ outline: 5px auto -webkit-focus-ring-color;
+ outline-offset: -2px; }
+ .btn:hover, .btn:focus {
+ color: #333;
+ text-decoration: none; }
+ .btn:active, .btn.active {
+ outline: 0;
+ background-image: none;
+ box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); }
+ .btn.disabled, .btn[disabled], fieldset[disabled] .btn {
+ cursor: not-allowed;
+ pointer-events: none;
+ opacity: 0.65;
+ filter: alpha(opacity=65);
+ box-shadow: none; }
+
+.btn-default {
+ color: #333;
+ background-color: #fff;
+ border-color: #ccc; }
+ .btn-default:hover, .btn-default:focus, .btn-default:active, .btn-default.active, .open > .btn-default.dropdown-toggle {
+ color: #333;
+ background-color: #e6e6e6;
+ border-color: #adadad; }
+ .btn-default:active, .btn-default.active, .open > .btn-default.dropdown-toggle {
+ background-image: none; }
+ .btn-default.disabled, .btn-default.disabled:hover, .btn-default.disabled:focus, .btn-default.disabled:active, .btn-default.disabled.active, .btn-default[disabled], .btn-default[disabled]:hover, .btn-default[disabled]:focus, .btn-default[disabled]:active, .btn-default[disabled].active, fieldset[disabled] .btn-default, fieldset[disabled] .btn-default:hover, fieldset[disabled] .btn-default:focus, fieldset[disabled] .btn-default:active, fieldset[disabled] .btn-default.active {
+ background-color: #fff;
+ border-color: #ccc; }
+ .btn-default .badge {
+ color: #fff;
+ background-color: #333; }
+
+.btn-primary {
+ color: #fff;
+ background-color: #428bca;
+ border-color: #3580bd; }
+ .btn-primary:hover, .btn-primary:focus, .btn-primary:active, .btn-primary.active, .open > .btn-primary.dropdown-toggle {
+ color: #fff;
+ background-color: #3073a9;
+ border-color: #28608e; }
+ .btn-primary:active, .btn-primary.active, .open > .btn-primary.dropdown-toggle {
+ background-image: none; }
+ .btn-primary.disabled, .btn-primary.disabled:hover, .btn-primary.disabled:focus, .btn-primary.disabled:active, .btn-primary.disabled.active, .btn-primary[disabled], .btn-primary[disabled]:hover, .btn-primary[disabled]:focus, .btn-primary[disabled]:active, .btn-primary[disabled].active, fieldset[disabled] .btn-primary, fieldset[disabled] .btn-primary:hover, fieldset[disabled] .btn-primary:focus, fieldset[disabled] .btn-primary:active, fieldset[disabled] .btn-primary.active {
+ background-color: #428bca;
+ border-color: #3580bd; }
+ .btn-primary .badge {
+ color: #428bca;
+ background-color: #fff; }
+
+.btn-success {
+ color: #fff;
+ background-color: #5cb85c;
+ border-color: #4eae4c; }
+ .btn-success:hover, .btn-success:focus, .btn-success:active, .btn-success.active, .open > .btn-success.dropdown-toggle {
+ color: #fff;
+ background-color: #469d44;
+ border-color: #3b8439; }
+ .btn-success:active, .btn-success.active, .open > .btn-success.dropdown-toggle {
+ background-image: none; }
+ .btn-success.disabled, .btn-success.disabled:hover, .btn-success.disabled:focus, .btn-success.disabled:active, .btn-success.disabled.active, .btn-success[disabled], .btn-success[disabled]:hover, .btn-success[disabled]:focus, .btn-success[disabled]:active, .btn-success[disabled].active, fieldset[disabled] .btn-success, fieldset[disabled] .btn-success:hover, fieldset[disabled] .btn-success:focus, fieldset[disabled] .btn-success:active, fieldset[disabled] .btn-success.active {
+ background-color: #5cb85c;
+ border-color: #4eae4c; }
+ .btn-success .badge {
+ color: #5cb85c;
+ background-color: #fff; }
+
+.btn-info {
+ color: #fff;
+ background-color: #5bc0de;
+ border-color: #46bada; }
+ .btn-info:hover, .btn-info:focus, .btn-info:active, .btn-info.active, .open > .btn-info.dropdown-toggle {
+ color: #fff;
+ background-color: #31b2d5;
+ border-color: #269cbc; }
+ .btn-info:active, .btn-info.active, .open > .btn-info.dropdown-toggle {
+ background-image: none; }
+ .btn-info.disabled, .btn-info.disabled:hover, .btn-info.disabled:focus, .btn-info.disabled:active, .btn-info.disabled.active, .btn-info[disabled], .btn-info[disabled]:hover, .btn-info[disabled]:focus, .btn-info[disabled]:active, .btn-info[disabled].active, fieldset[disabled] .btn-info, fieldset[disabled] .btn-info:hover, fieldset[disabled] .btn-info:focus, fieldset[disabled] .btn-info:active, fieldset[disabled] .btn-info.active {
+ background-color: #5bc0de;
+ border-color: #46bada; }
+ .btn-info .badge {
+ color: #5bc0de;
+ background-color: #fff; }
+
+.btn-warning {
+ color: #fff;
+ background-color: #f0ad4e;
+ border-color: #eea236; }
+ .btn-warning:hover, .btn-warning:focus, .btn-warning:active, .btn-warning.active, .open > .btn-warning.dropdown-toggle {
+ color: #fff;
+ background-color: #ec971f;
+ border-color: #d58112; }
+ .btn-warning:active, .btn-warning.active, .open > .btn-warning.dropdown-toggle {
+ background-image: none; }
+ .btn-warning.disabled, .btn-warning.disabled:hover, .btn-warning.disabled:focus, .btn-warning.disabled:active, .btn-warning.disabled.active, .btn-warning[disabled], .btn-warning[disabled]:hover, .btn-warning[disabled]:focus, .btn-warning[disabled]:active, .btn-warning[disabled].active, fieldset[disabled] .btn-warning, fieldset[disabled] .btn-warning:hover, fieldset[disabled] .btn-warning:focus, fieldset[disabled] .btn-warning:active, fieldset[disabled] .btn-warning.active {
+ background-color: #f0ad4e;
+ border-color: #eea236; }
+ .btn-warning .badge {
+ color: #f0ad4e;
+ background-color: #fff; }
+
+.btn-danger {
+ color: #fff;
+ background-color: #d9534f;
+ border-color: #d43d3a; }
+ .btn-danger:hover, .btn-danger:focus, .btn-danger:active, .btn-danger.active, .open > .btn-danger.dropdown-toggle {
+ color: #fff;
+ background-color: #c92e2c;
+ border-color: #ac2525; }
+ .btn-danger:active, .btn-danger.active, .open > .btn-danger.dropdown-toggle {
+ background-image: none; }
+ .btn-danger.disabled, .btn-danger.disabled:hover, .btn-danger.disabled:focus, .btn-danger.disabled:active, .btn-danger.disabled.active, .btn-danger[disabled], .btn-danger[disabled]:hover, .btn-danger[disabled]:focus, .btn-danger[disabled]:active, .btn-danger[disabled].active, fieldset[disabled] .btn-danger, fieldset[disabled] .btn-danger:hover, fieldset[disabled] .btn-danger:focus, fieldset[disabled] .btn-danger:active, fieldset[disabled] .btn-danger.active {
+ background-color: #d9534f;
+ border-color: #d43d3a; }
+ .btn-danger .badge {
+ color: #d9534f;
+ background-color: #fff; }
+
+.btn-link {
+ color: #428bca;
+ font-weight: normal;
+ cursor: pointer;
+ border-radius: 0; }
+ .btn-link, .btn-link:active, .btn-link[disabled], fieldset[disabled] .btn-link {
+ background-color: transparent;
+ box-shadow: none; }
+ .btn-link, .btn-link:hover, .btn-link:focus, .btn-link:active {
+ border-color: transparent; }
+ .btn-link:hover, .btn-link:focus {
+ color: #2a6596;
+ text-decoration: underline;
+ background-color: transparent; }
+ .btn-link[disabled]:hover, .btn-link[disabled]:focus, fieldset[disabled] .btn-link:hover, fieldset[disabled] .btn-link:focus {
+ color: #999999;
+ text-decoration: none; }
+
+.btn-lg, .btn-group-lg > .btn {
+ padding: 10px 16px;
+ font-size: 18px;
+ line-height: 1.33;
+ border-radius: 6px; }
+
+.btn-sm, .btn-group-sm > .btn {
+ padding: 5px 10px;
+ font-size: 12px;
+ line-height: 1.5;
+ border-radius: 3px; }
+
+.btn-xs, .btn-group-xs > .btn {
+ padding: 1px 5px;
+ font-size: 12px;
+ line-height: 1.5;
+ border-radius: 3px; }
+
+.btn-block {
+ display: block;
+ width: 100%;
+ padding-left: 0;
+ padding-right: 0; }
+
+.btn-block + .btn-block {
+ margin-top: 5px; }
+
+input[type="submit"].btn-block, input[type="reset"].btn-block, input[type="button"].btn-block {
+ width: 100%; }
+
+.fade {
+ opacity: 0;
+ transition: opacity 0.15s linear; }
+ .fade.in {
+ opacity: 1; }
+
+.collapse {
+ display: none; }
+ .collapse.in {
+ display: block; }
+
+tr.collapse.in {
+ display: table-row; }
+
+tbody.collapse.in {
+ display: table-row-group; }
+
+.collapsing {
+ position: relative;
+ height: 0;
+ overflow: hidden;
+ transition: height 0.35s ease; }
+
+.caret {
+ display: inline-block;
+ width: 0;
+ height: 0;
+ margin-left: 2px;
+ vertical-align: middle;
+ border-top: 4px solid;
+ border-right: 4px solid transparent;
+ border-left: 4px solid transparent; }
+
+.dropdown {
+ position: relative; }
+
+.dropdown-toggle:focus {
+ outline: 0; }
+
+.dropdown-menu {
+ position: absolute;
+ top: 100%;
+ left: 0;
+ z-index: 1000;
+ display: none;
+ float: left;
+ min-width: 160px;
+ padding: 5px 0;
+ margin: 2px 0 0;
+ list-style: none;
+ font-size: 14px;
+ text-align: left;
+ background-color: #fff;
+ border: 1px solid #ccc;
+ border: 1px solid rgba(0, 0, 0, 0.15);
+ border-radius: 4px;
+ box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
+ background-clip: padding-box; }
+ .dropdown-menu.pull-right {
+ right: 0;
+ left: auto; }
+ .dropdown-menu .divider {
+ height: 1px;
+ margin: 9px 0;
+ overflow: hidden;
+ background-color: #e5e5e5; }
+ .dropdown-menu > li > a {
+ display: block;
+ padding: 3px 20px;
+ clear: both;
+ font-weight: normal;
+ line-height: 1.42857;
+ color: #333333;
+ white-space: nowrap; }
+
+.dropdown-menu > li > a:hover, .dropdown-menu > li > a:focus {
+ text-decoration: none;
+ color: #262626;
+ background-color: #f5f5f5; }
+
+.dropdown-menu > .active > a, .dropdown-menu > .active > a:hover, .dropdown-menu > .active > a:focus {
+ color: #fff;
+ text-decoration: none;
+ outline: 0;
+ background-color: #428bca; }
+
+.dropdown-menu > .disabled > a, .dropdown-menu > .disabled > a:hover, .dropdown-menu > .disabled > a:focus {
+ color: #999999; }
+
+.dropdown-menu > .disabled > a:hover, .dropdown-menu > .disabled > a:focus {
+ text-decoration: none;
+ background-color: transparent;
+ background-image: none;
+ filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
+ cursor: not-allowed; }
+
+.open > .dropdown-menu {
+ display: block; }
+.open > a {
+ outline: 0; }
+
+.dropdown-menu-right {
+ left: auto;
+ right: 0; }
+
+.dropdown-menu-left {
+ left: 0;
+ right: auto; }
+
+.dropdown-header {
+ display: block;
+ padding: 3px 20px;
+ font-size: 12px;
+ line-height: 1.42857;
+ color: #999999; }
+
+.dropdown-backdrop {
+ position: fixed;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ top: 0;
+ z-index: 990; }
+
+.pull-right > .dropdown-menu {
+ right: 0;
+ left: auto; }
+
+.dropup .caret, .navbar-fixed-bottom .dropdown .caret {
+ border-top: 0;
+ border-bottom: 4px solid;
+ content: ""; }
+.dropup .dropdown-menu, .navbar-fixed-bottom .dropdown .dropdown-menu {
+ top: auto;
+ bottom: 100%;
+ margin-bottom: 1px; }
+
+@media (min-width: 768px) {
+ .navbar-right .dropdown-menu {
+ right: 0;
+ left: auto; }
+ .navbar-right .dropdown-menu-left {
+ left: 0;
+ right: auto; } }
+
+.btn-group, .btn-group-vertical {
+ position: relative;
+ display: inline-block;
+ vertical-align: middle; }
+ .btn-group > .btn, .btn-group-vertical > .btn {
+ position: relative;
+ float: left; }
+ .btn-group > .btn:hover, .btn-group > .btn:focus, .btn-group > .btn:active, .btn-group > .btn.active, .btn-group-vertical > .btn:hover, .btn-group-vertical > .btn:focus, .btn-group-vertical > .btn:active, .btn-group-vertical > .btn.active {
+ z-index: 2; }
+ .btn-group > .btn:focus, .btn-group-vertical > .btn:focus {
+ outline: 0; }
+
+.btn-group .btn + .btn, .btn-group .btn + .btn-group, .btn-group .btn-group + .btn, .btn-group .btn-group + .btn-group {
+ margin-left: -1px; }
+
+.btn-toolbar {
+ margin-left: -5px; }
+ .btn-toolbar:before, .btn-toolbar:after {
+ content: " ";
+ display: table; }
+ .btn-toolbar:after {
+ clear: both; }
+ .btn-toolbar .btn-group, .btn-toolbar .input-group {
+ float: left; }
+ .btn-toolbar > .btn, .btn-toolbar > .btn-group, .btn-toolbar > .input-group {
+ margin-left: 5px; }
+
+.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) {
+ border-radius: 0; }
+
+.btn-group > .btn:first-child {
+ margin-left: 0; }
+ .btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle) {
+ border-bottom-right-radius: 0;
+ border-top-right-radius: 0; }
+
+.btn-group > .btn:last-child:not(:first-child), .btn-group > .dropdown-toggle:not(:first-child) {
+ border-bottom-left-radius: 0;
+ border-top-left-radius: 0; }
+
+.btn-group > .btn-group {
+ float: left; }
+
+.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn {
+ border-radius: 0; }
+
+.btn-group > .btn-group:first-child > .btn:last-child, .btn-group > .btn-group:first-child > .dropdown-toggle {
+ border-bottom-right-radius: 0;
+ border-top-right-radius: 0; }
+
+.btn-group > .btn-group:last-child > .btn:first-child {
+ border-bottom-left-radius: 0;
+ border-top-left-radius: 0; }
+
+.btn-group .dropdown-toggle:active, .btn-group.open .dropdown-toggle {
+ outline: 0; }
+
+.btn-group > .btn + .dropdown-toggle {
+ padding-left: 8px;
+ padding-right: 8px; }
+
+.btn-group > .btn-lg + .dropdown-toggle, .btn-group > .btn-lg + .btn-group-lg > .btn, .btn-group-lg > .btn-group > .btn-lg + .btn {
+ padding-left: 12px;
+ padding-right: 12px; }
+
+.btn-group.open .dropdown-toggle {
+ box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); }
+ .btn-group.open .dropdown-toggle.btn-link {
+ box-shadow: none; }
+
+.btn .caret {
+ margin-left: 0; }
+
+.btn-lg .caret, .btn-lg .btn-group-lg > .btn, .btn-group-lg > .btn-lg .btn {
+ border-width: 5px 5px 0;
+ border-bottom-width: 0; }
+
+.dropup .btn-lg .caret, .dropup .btn-lg .btn-group-lg > .btn, .btn-group-lg > .dropup .btn-lg .btn {
+ border-width: 0 5px 5px; }
+
+.btn-group-vertical > .btn, .btn-group-vertical > .btn-group, .btn-group-vertical > .btn-group > .btn {
+ display: block;
+ float: none;
+ width: 100%;
+ max-width: 100%; }
+.btn-group-vertical > .btn-group:before, .btn-group-vertical > .btn-group:after {
+ content: " ";
+ display: table; }
+.btn-group-vertical > .btn-group:after {
+ clear: both; }
+.btn-group-vertical > .btn-group > .btn {
+ float: none; }
+.btn-group-vertical > .btn + .btn, .btn-group-vertical > .btn + .btn-group, .btn-group-vertical > .btn-group + .btn, .btn-group-vertical > .btn-group + .btn-group {
+ margin-top: -1px;
+ margin-left: 0; }
+
+.btn-group-vertical > .btn:not(:first-child):not(:last-child) {
+ border-radius: 0; }
+.btn-group-vertical > .btn:first-child:not(:last-child) {
+ border-top-right-radius: 4px;
+ border-bottom-right-radius: 0;
+ border-bottom-left-radius: 0; }
+.btn-group-vertical > .btn:last-child:not(:first-child) {
+ border-bottom-left-radius: 4px;
+ border-top-right-radius: 0;
+ border-top-left-radius: 0; }
+
+.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn {
+ border-radius: 0; }
+
+.btn-group-vertical > .btn-group:first-child:not(:last-child) > .btn:last-child, .btn-group-vertical > .btn-group:first-child:not(:last-child) > .dropdown-toggle {
+ border-bottom-right-radius: 0;
+ border-bottom-left-radius: 0; }
+
+.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child {
+ border-top-right-radius: 0;
+ border-top-left-radius: 0; }
+
+.btn-group-justified {
+ display: table;
+ width: 100%;
+ table-layout: fixed;
+ border-collapse: separate; }
+ .btn-group-justified > .btn, .btn-group-justified > .btn-group {
+ float: none;
+ display: table-cell;
+ width: 1%; }
+ .btn-group-justified > .btn-group .btn {
+ width: 100%; }
+
+[data-toggle="buttons"] > .btn > input[type="radio"], [data-toggle="buttons"] > .btn > input[type="checkbox"] {
+ position: absolute;
+ z-index: -1;
+ opacity: 0; }
+
+.input-group {
+ position: relative;
+ display: table;
+ border-collapse: separate; }
+ .input-group[class*="col-"] {
+ float: none;
+ padding-left: 0;
+ padding-right: 0; }
+ .input-group .form-control {
+ position: relative;
+ z-index: 2;
+ float: left;
+ width: 100%;
+ margin-bottom: 0; }
+
+.input-group-addon, .input-group-btn, .input-group .form-control {
+ display: table-cell; }
+ .input-group-addon:not(:first-child):not(:last-child), .input-group-btn:not(:first-child):not(:last-child), .input-group .form-control:not(:first-child):not(:last-child) {
+ border-radius: 0; }
+
+.input-group-addon, .input-group-btn {
+ width: 1%;
+ white-space: nowrap;
+ vertical-align: middle; }
+
+.input-group-addon {
+ padding: 6px 12px;
+ font-size: 14px;
+ font-weight: normal;
+ line-height: 1;
+ color: #555555;
+ text-align: center;
+ background-color: #eeeeee;
+ border: 1px solid #ccc;
+ border-radius: 4px; }
+ .input-group-addon.input-sm, .input-group-sm > .input-group-addon.form-control, .input-group-sm > .input-group-addon, .input-group-sm > .input-group-btn > .input-group-addon.btn {
+ padding: 5px 10px;
+ font-size: 12px;
+ border-radius: 3px; }
+ .input-group-addon.input-lg, .input-group-lg > .input-group-addon.form-control, .input-group-lg > .input-group-addon, .input-group-lg > .input-group-btn > .input-group-addon.btn {
+ padding: 10px 16px;
+ font-size: 18px;
+ border-radius: 6px; }
+ .input-group-addon input[type="radio"], .input-group-addon input[type="checkbox"] {
+ margin-top: 0; }
+
+.input-group .form-control:first-child, .input-group-addon:first-child, .input-group-btn:first-child > .btn, .input-group-btn:first-child > .btn-group > .btn, .input-group-btn:first-child > .dropdown-toggle, .input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle), .input-group-btn:last-child > .btn-group:not(:last-child) > .btn {
+ border-bottom-right-radius: 0;
+ border-top-right-radius: 0; }
+
+.input-group-addon:first-child {
+ border-right: 0; }
+
+.input-group .form-control:last-child, .input-group-addon:last-child, .input-group-btn:last-child > .btn, .input-group-btn:last-child > .btn-group > .btn, .input-group-btn:last-child > .dropdown-toggle, .input-group-btn:first-child > .btn:not(:first-child), .input-group-btn:first-child > .btn-group:not(:first-child) > .btn {
+ border-bottom-left-radius: 0;
+ border-top-left-radius: 0; }
+
+.input-group-addon:last-child {
+ border-left: 0; }
+
+.input-group-btn {
+ position: relative;
+ font-size: 0;
+ white-space: nowrap; }
+ .input-group-btn > .btn {
+ position: relative; }
+ .input-group-btn > .btn + .btn {
+ margin-left: -1px; }
+ .input-group-btn > .btn:hover, .input-group-btn > .btn:focus, .input-group-btn > .btn:active {
+ z-index: 2; }
+ .input-group-btn:first-child > .btn, .input-group-btn:first-child > .btn-group {
+ margin-right: -1px; }
+ .input-group-btn:last-child > .btn, .input-group-btn:last-child > .btn-group {
+ margin-left: -1px; }
+
+.nav {
+ margin-bottom: 0;
+ padding-left: 0;
+ list-style: none; }
+ .nav:before, .nav:after {
+ content: " ";
+ display: table; }
+ .nav:after {
+ clear: both; }
+ .nav > li {
+ position: relative;
+ display: block; }
+ .nav > li > a {
+ position: relative;
+ display: block;
+ padding: 10px 15px; }
+ .nav > li > a:hover, .nav > li > a:focus {
+ text-decoration: none;
+ background-color: #eeeeee; }
+ .nav > li.disabled > a {
+ color: #999999; }
+ .nav > li.disabled > a:hover, .nav > li.disabled > a:focus {
+ color: #999999;
+ text-decoration: none;
+ background-color: transparent;
+ cursor: not-allowed; }
+ .nav .open > a, .nav .open > a:hover, .nav .open > a:focus {
+ background-color: #eeeeee;
+ border-color: #428bca; }
+ .nav .nav-divider {
+ height: 1px;
+ margin: 9px 0;
+ overflow: hidden;
+ background-color: #e5e5e5; }
+ .nav > li > a > img {
+ max-width: none; }
+
+.nav-tabs {
+ border-bottom: 1px solid #ddd; }
+ .nav-tabs > li {
+ float: left;
+ margin-bottom: -1px; }
+ .nav-tabs > li > a {
+ margin-right: 2px;
+ line-height: 1.42857;
+ border: 1px solid transparent;
+ border-radius: 4px 4px 0 0; }
+ .nav-tabs > li > a:hover {
+ border-color: #eeeeee #eeeeee #ddd; }
+ .nav-tabs > li.active > a, .nav-tabs > li.active > a:hover, .nav-tabs > li.active > a:focus {
+ color: #555555;
+ background-color: #fff;
+ border: 1px solid #ddd;
+ border-bottom-color: transparent;
+ cursor: default; }
+
+.nav-pills > li {
+ float: left; }
+ .nav-pills > li > a {
+ border-radius: 4px; }
+ .nav-pills > li + li {
+ margin-left: 2px; }
+ .nav-pills > li.active > a, .nav-pills > li.active > a:hover, .nav-pills > li.active > a:focus {
+ color: #fff;
+ background-color: #428bca; }
+
+.nav-stacked > li {
+ float: none; }
+ .nav-stacked > li + li {
+ margin-top: 2px;
+ margin-left: 0; }
+
+.nav-justified, .nav-tabs.nav-justified {
+ width: 100%; }
+ .nav-justified > li, .nav-justified > .nav-tabs.nav-justified {
+ float: none; }
+ .nav-justified > li > a, .nav-justified > li > .nav-tabs.nav-justified {
+ text-align: center;
+ margin-bottom: 5px; }
+ .nav-justified > .dropdown .dropdown-menu, .nav-justified > .dropdown .nav-tabs.nav-justified {
+ top: auto;
+ left: auto; }
+ @media (min-width: 768px) {
+ .nav-justified > li, .nav-justified > .nav-tabs.nav-justified {
+ display: table-cell;
+ width: 1%; }
+ .nav-justified > li > a, .nav-justified > li > .nav-tabs.nav-justified {
+ margin-bottom: 0; } }
+
+.nav-tabs-justified, .nav-tabs.nav-justified, .nav-tabs.nav-justified {
+ border-bottom: 0; }
+ .nav-tabs-justified > li > a, .nav-tabs-justified > li > .nav-tabs.nav-justified, .nav-tabs-justified > li > .nav-tabs.nav-justified {
+ margin-right: 0;
+ border-radius: 4px; }
+ .nav-tabs-justified > .active > a, .nav-tabs-justified > .active > .nav-tabs.nav-justified, .nav-tabs-justified > .active > .nav-tabs.nav-justified, .nav-tabs-justified > .active > a:hover, .nav-tabs-justified > .active > .nav-tabs.nav-justified, .nav-tabs-justified > .active > .nav-tabs.nav-justified, .nav-tabs-justified > .active > a:focus, .nav-tabs-justified > .active > .nav-tabs.nav-justified, .nav-tabs-justified > .active > .nav-tabs.nav-justified {
+ border: 1px solid #ddd; }
+ @media (min-width: 768px) {
+ .nav-tabs-justified > li > a, .nav-tabs-justified > li > .nav-tabs.nav-justified, .nav-tabs-justified > li > .nav-tabs.nav-justified {
+ border-bottom: 1px solid #ddd;
+ border-radius: 4px 4px 0 0; }
+ .nav-tabs-justified > .active > a, .nav-tabs-justified > .active > .nav-tabs.nav-justified, .nav-tabs-justified > .active > .nav-tabs.nav-justified, .nav-tabs-justified > .active > a:hover, .nav-tabs-justified > .active > .nav-tabs.nav-justified, .nav-tabs-justified > .active > .nav-tabs.nav-justified, .nav-tabs-justified > .active > a:focus, .nav-tabs-justified > .active > .nav-tabs.nav-justified, .nav-tabs-justified > .active > .nav-tabs.nav-justified {
+ border-bottom-color: #fff; } }
+
+.tab-content > .tab-pane {
+ display: none; }
+.tab-content > .active {
+ display: block; }
+
+.nav-tabs .dropdown-menu {
+ margin-top: -1px;
+ border-top-right-radius: 0;
+ border-top-left-radius: 0; }
+
+.navbar {
+ position: relative;
+ min-height: 50px;
+ margin-bottom: 20px;
+ border: 1px solid transparent; }
+ .navbar:before, .navbar:after {
+ content: " ";
+ display: table; }
+ .navbar:after {
+ clear: both; }
+ @media (min-width: 768px) {
+ .navbar {
+ border-radius: 4px; } }
+
+.navbar-header:before, .navbar-header:after {
+ content: " ";
+ display: table; }
+.navbar-header:after {
+ clear: both; }
+@media (min-width: 768px) {
+ .navbar-header {
+ float: left; } }
+
+.navbar-collapse {
+ overflow-x: visible;
+ padding-right: 15px;
+ padding-left: 15px;
+ border-top: 1px solid transparent;
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1);
+ -webkit-overflow-scrolling: touch; }
+ .navbar-collapse:before, .navbar-collapse:after {
+ content: " ";
+ display: table; }
+ .navbar-collapse:after {
+ clear: both; }
+ .navbar-collapse.in {
+ overflow-y: auto; }
+ @media (min-width: 768px) {
+ .navbar-collapse {
+ width: auto;
+ border-top: 0;
+ box-shadow: none; }
+ .navbar-collapse.collapse {
+ display: block !important;
+ height: auto !important;
+ padding-bottom: 0;
+ overflow: visible !important; }
+ .navbar-collapse.in {
+ overflow-y: visible; }
+ .navbar-fixed-top .navbar-collapse, .navbar-static-top .navbar-collapse, .navbar-fixed-bottom .navbar-collapse {
+ padding-left: 0;
+ padding-right: 0; } }
+
+.navbar-fixed-top .navbar-collapse, .navbar-fixed-bottom .navbar-collapse {
+ max-height: 340px; }
+ @media (max-width: 480px) and (orientation: landscape) {
+ .navbar-fixed-top .navbar-collapse, .navbar-fixed-bottom .navbar-collapse {
+ max-height: 200px; } }
+
+.container > .navbar-header, .container > .navbar-collapse, .container-fluid > .navbar-header, .container-fluid > .navbar-collapse {
+ margin-right: -15px;
+ margin-left: -15px; }
+ @media (min-width: 768px) {
+ .container > .navbar-header, .container > .navbar-collapse, .container-fluid > .navbar-header, .container-fluid > .navbar-collapse {
+ margin-right: 0;
+ margin-left: 0; } }
+
+.navbar-static-top {
+ z-index: 1000;
+ border-width: 0 0 1px; }
+ @media (min-width: 768px) {
+ .navbar-static-top {
+ border-radius: 0; } }
+
+.navbar-fixed-top, .navbar-fixed-bottom {
+ position: fixed;
+ right: 0;
+ left: 0;
+ z-index: 1030; }
+ @media (min-width: 768px) {
+ .navbar-fixed-top, .navbar-fixed-bottom {
+ border-radius: 0; } }
+
+.navbar-fixed-top {
+ top: 0;
+ border-width: 0 0 1px; }
+
+.navbar-fixed-bottom {
+ bottom: 0;
+ margin-bottom: 0;
+ border-width: 1px 0 0; }
+
+.navbar-brand {
+ float: left;
+ padding: 15px 15px;
+ font-size: 18px;
+ line-height: 20px;
+ height: 50px; }
+ .navbar-brand:hover, .navbar-brand:focus {
+ text-decoration: none; }
+ @media (min-width: 768px) {
+ .navbar > .container .navbar-brand, .navbar > .container-fluid .navbar-brand {
+ margin-left: -15px; } }
+
+.navbar-toggle {
+ position: relative;
+ float: right;
+ margin-right: 15px;
+ padding: 9px 10px;
+ margin-top: 8px;
+ margin-bottom: 8px;
+ background-color: transparent;
+ background-image: none;
+ border: 1px solid transparent;
+ border-radius: 4px; }
+ .navbar-toggle:focus {
+ outline: 0; }
+ .navbar-toggle .icon-bar {
+ display: block;
+ width: 22px;
+ height: 2px;
+ border-radius: 1px; }
+ .navbar-toggle .icon-bar + .icon-bar {
+ margin-top: 4px; }
+ @media (min-width: 768px) {
+ .navbar-toggle {
+ display: none; } }
+
+.navbar-nav {
+ margin: 7.5px -15px; }
+ .navbar-nav > li > a {
+ padding-top: 10px;
+ padding-bottom: 10px;
+ line-height: 20px; }
+ @media (max-width: 767px) {
+ .navbar-nav .open .dropdown-menu {
+ position: static;
+ float: none;
+ width: auto;
+ margin-top: 0;
+ background-color: transparent;
+ border: 0;
+ box-shadow: none; }
+ .navbar-nav .open .dropdown-menu > li > a, .navbar-nav .open .dropdown-menu .dropdown-header {
+ padding: 5px 15px 5px 25px; }
+ .navbar-nav .open .dropdown-menu > li > a {
+ line-height: 20px; }
+ .navbar-nav .open .dropdown-menu > li > a:hover, .navbar-nav .open .dropdown-menu > li > a:focus {
+ background-image: none; } }
+ @media (min-width: 768px) {
+ .navbar-nav {
+ float: left;
+ margin: 0; }
+ .navbar-nav > li {
+ float: left; }
+ .navbar-nav > li > a {
+ padding-top: 15px;
+ padding-bottom: 15px; }
+ .navbar-nav.navbar-right:last-child {
+ margin-right: -15px; } }
+
+@media (min-width: 768px) {
+ .navbar-left {
+ float: left !important; }
+ .navbar-right {
+ float: right !important; } }
+
+.navbar-form {
+ margin-left: -15px;
+ margin-right: -15px;
+ padding: 10px 15px;
+ border-top: 1px solid transparent;
+ border-bottom: 1px solid transparent;
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);
+ margin-top: 8px;
+ margin-bottom: 8px; }
+ @media (max-width: 767px) {
+ .navbar-form .form-group {
+ margin-bottom: 5px; } }
+ @media (min-width: 768px) {
+ .navbar-form {
+ width: auto;
+ border: 0;
+ margin-left: 0;
+ margin-right: 0;
+ padding-top: 0;
+ padding-bottom: 0;
+ box-shadow: none; }
+ .navbar-form.navbar-right:last-child {
+ margin-right: -15px; } }
+
+.navbar-nav > li > .dropdown-menu {
+ margin-top: 0;
+ border-top-right-radius: 0;
+ border-top-left-radius: 0; }
+
+.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu {
+ border-bottom-right-radius: 0;
+ border-bottom-left-radius: 0; }
+
+.navbar-btn {
+ margin-top: 8px;
+ margin-bottom: 8px; }
+ .navbar-btn.btn-sm, .btn-group-sm > .navbar-btn.btn {
+ margin-top: 10px;
+ margin-bottom: 10px; }
+ .navbar-btn.btn-xs, .btn-group-xs > .navbar-btn.btn {
+ margin-top: 14px;
+ margin-bottom: 14px; }
+
+.navbar-text {
+ margin-top: 15px;
+ margin-bottom: 15px; }
+ @media (min-width: 768px) {
+ .navbar-text {
+ float: left;
+ margin-left: 15px;
+ margin-right: 15px; }
+ .navbar-text.navbar-right:last-child {
+ margin-right: 0; } }
+
+.navbar-default {
+ background-color: #f8f8f8;
+ border-color: #e7e7e7; }
+ .navbar-default .navbar-brand {
+ color: #777; }
+ .navbar-default .navbar-brand:hover, .navbar-default .navbar-brand:focus {
+ color: #5e5e5e;
+ background-color: transparent; }
+ .navbar-default .navbar-text {
+ color: #777; }
+ .navbar-default .navbar-nav > li > a {
+ color: #777; }
+ .navbar-default .navbar-nav > li > a:hover, .navbar-default .navbar-nav > li > a:focus {
+ color: #333;
+ background-color: transparent; }
+ .navbar-default .navbar-nav > .active > a, .navbar-default .navbar-nav > .active > a:hover, .navbar-default .navbar-nav > .active > a:focus {
+ color: #555;
+ background-color: #e7e7e7; }
+ .navbar-default .navbar-nav > .disabled > a, .navbar-default .navbar-nav > .disabled > a:hover, .navbar-default .navbar-nav > .disabled > a:focus {
+ color: #ccc;
+ background-color: transparent; }
+ .navbar-default .navbar-toggle {
+ border-color: #ddd; }
+ .navbar-default .navbar-toggle:hover, .navbar-default .navbar-toggle:focus {
+ background-color: #ddd; }
+ .navbar-default .navbar-toggle .icon-bar {
+ background-color: #888; }
+ .navbar-default .navbar-collapse, .navbar-default .navbar-form {
+ border-color: #e7e7e7; }
+ .navbar-default .navbar-nav > .open > a, .navbar-default .navbar-nav > .open > a:hover, .navbar-default .navbar-nav > .open > a:focus {
+ background-color: #e7e7e7;
+ color: #555; }
+ @media (max-width: 767px) {
+ .navbar-default .navbar-nav .open .dropdown-menu > li > a {
+ color: #777; }
+ .navbar-default .navbar-nav .open .dropdown-menu > li > a:hover, .navbar-default .navbar-nav .open .dropdown-menu > li > a:focus {
+ color: #333;
+ background-color: transparent; }
+ .navbar-default .navbar-nav .open .dropdown-menu > .active > a, .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover, .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus {
+ color: #555;
+ background-color: #e7e7e7; }
+ .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a, .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:hover, .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:focus {
+ color: #ccc;
+ background-color: transparent; } }
+ .navbar-default .navbar-link {
+ color: #777; }
+ .navbar-default .navbar-link:hover {
+ color: #333; }
+ .navbar-default .btn-link {
+ color: #777; }
+ .navbar-default .btn-link:hover, .navbar-default .btn-link:focus {
+ color: #333; }
+ .navbar-default .btn-link[disabled]:hover, .navbar-default .btn-link[disabled]:focus, fieldset[disabled] .navbar-default .btn-link:hover, fieldset[disabled] .navbar-default .btn-link:focus {
+ color: #ccc; }
+
+.navbar-inverse {
+ background-color: #222;
+ border-color: #090909; }
+ .navbar-inverse .navbar-brand {
+ color: #999999; }
+ .navbar-inverse .navbar-brand:hover, .navbar-inverse .navbar-brand:focus {
+ color: #fff;
+ background-color: transparent; }
+ .navbar-inverse .navbar-text {
+ color: #999999; }
+ .navbar-inverse .navbar-nav > li > a {
+ color: #999999; }
+ .navbar-inverse .navbar-nav > li > a:hover, .navbar-inverse .navbar-nav > li > a:focus {
+ color: #fff;
+ background-color: transparent; }
+ .navbar-inverse .navbar-nav > .active > a, .navbar-inverse .navbar-nav > .active > a:hover, .navbar-inverse .navbar-nav > .active > a:focus {
+ color: #fff;
+ background-color: #090909; }
+ .navbar-inverse .navbar-nav > .disabled > a, .navbar-inverse .navbar-nav > .disabled > a:hover, .navbar-inverse .navbar-nav > .disabled > a:focus {
+ color: #444;
+ background-color: transparent; }
+ .navbar-inverse .navbar-toggle {
+ border-color: #333; }
+ .navbar-inverse .navbar-toggle:hover, .navbar-inverse .navbar-toggle:focus {
+ background-color: #333; }
+ .navbar-inverse .navbar-toggle .icon-bar {
+ background-color: #fff; }
+ .navbar-inverse .navbar-collapse, .navbar-inverse .navbar-form {
+ border-color: #101010; }
+ .navbar-inverse .navbar-nav > .open > a, .navbar-inverse .navbar-nav > .open > a:hover, .navbar-inverse .navbar-nav > .open > a:focus {
+ background-color: #090909;
+ color: #fff; }
+ @media (max-width: 767px) {
+ .navbar-inverse .navbar-nav .open .dropdown-menu > .dropdown-header {
+ border-color: #090909; }
+ .navbar-inverse .navbar-nav .open .dropdown-menu .divider {
+ background-color: #090909; }
+ .navbar-inverse .navbar-nav .open .dropdown-menu > li > a {
+ color: #999999; }
+ .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:hover, .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:focus {
+ color: #fff;
+ background-color: transparent; }
+ .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a, .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:hover, .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:focus {
+ color: #fff;
+ background-color: #090909; }
+ .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a, .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:hover, .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:focus {
+ color: #444;
+ background-color: transparent; } }
+ .navbar-inverse .navbar-link {
+ color: #999999; }
+ .navbar-inverse .navbar-link:hover {
+ color: #fff; }
+ .navbar-inverse .btn-link {
+ color: #999999; }
+ .navbar-inverse .btn-link:hover, .navbar-inverse .btn-link:focus {
+ color: #fff; }
+ .navbar-inverse .btn-link[disabled]:hover, .navbar-inverse .btn-link[disabled]:focus, fieldset[disabled] .navbar-inverse .btn-link:hover, fieldset[disabled] .navbar-inverse .btn-link:focus {
+ color: #444; }
+
+.breadcrumb {
+ padding: 8px 15px;
+ margin-bottom: 20px;
+ list-style: none;
+ background-color: #f5f5f5;
+ border-radius: 4px; }
+ .breadcrumb > li {
+ display: inline-block; }
+ .breadcrumb > li + li:before {
+ content: "/\00a0";
+ padding: 0 5px;
+ color: #ccc; }
+ .breadcrumb > .active {
+ color: #999999; }
+
+.pagination {
+ display: inline-block;
+ padding-left: 0;
+ margin: 20px 0;
+ border-radius: 4px; }
+ .pagination > li {
+ display: inline; }
+ .pagination > li > a, .pagination > li > span {
+ position: relative;
+ float: left;
+ padding: 6px 12px;
+ line-height: 1.42857;
+ text-decoration: none;
+ color: #428bca;
+ background-color: #fff;
+ border: 1px solid #ddd;
+ margin-left: -1px; }
+ .pagination > li:first-child > a, .pagination > li:first-child > span {
+ margin-left: 0;
+ border-bottom-left-radius: 4px;
+ border-top-left-radius: 4px; }
+ .pagination > li:last-child > a, .pagination > li:last-child > span {
+ border-bottom-right-radius: 4px;
+ border-top-right-radius: 4px; }
+ .pagination > li > a:hover, .pagination > li > a:focus, .pagination > li > span:hover, .pagination > li > span:focus {
+ color: #2a6596;
+ background-color: #eeeeee;
+ border-color: #ddd; }
+ .pagination > .active > a, .pagination > .active > a:hover, .pagination > .active > a:focus, .pagination > .active > span, .pagination > .active > span:hover, .pagination > .active > span:focus {
+ z-index: 2;
+ color: #fff;
+ background-color: #428bca;
+ border-color: #428bca;
+ cursor: default; }
+ .pagination > .disabled > span, .pagination > .disabled > span:hover, .pagination > .disabled > span:focus, .pagination > .disabled > a, .pagination > .disabled > a:hover, .pagination > .disabled > a:focus {
+ color: #999999;
+ background-color: #fff;
+ border-color: #ddd;
+ cursor: not-allowed; }
+
+.pagination-lg > li > a, .pagination-lg > li > span {
+ padding: 10px 16px;
+ font-size: 18px; }
+.pagination-lg > li:first-child > a, .pagination-lg > li:first-child > span {
+ border-bottom-left-radius: 6px;
+ border-top-left-radius: 6px; }
+.pagination-lg > li:last-child > a, .pagination-lg > li:last-child > span {
+ border-bottom-right-radius: 6px;
+ border-top-right-radius: 6px; }
+
+.pagination-sm > li > a, .pagination-sm > li > span {
+ padding: 5px 10px;
+ font-size: 12px; }
+.pagination-sm > li:first-child > a, .pagination-sm > li:first-child > span {
+ border-bottom-left-radius: 3px;
+ border-top-left-radius: 3px; }
+.pagination-sm > li:last-child > a, .pagination-sm > li:last-child > span {
+ border-bottom-right-radius: 3px;
+ border-top-right-radius: 3px; }
+
+.pager {
+ padding-left: 0;
+ margin: 20px 0;
+ list-style: none;
+ text-align: center; }
+ .pager:before, .pager:after {
+ content: " ";
+ display: table; }
+ .pager:after {
+ clear: both; }
+ .pager li {
+ display: inline; }
+ .pager li > a, .pager li > span {
+ display: inline-block;
+ padding: 5px 14px;
+ background-color: #fff;
+ border: 1px solid #ddd;
+ border-radius: 15px; }
+ .pager li > a:hover, .pager li > a:focus {
+ text-decoration: none;
+ background-color: #eeeeee; }
+ .pager .next > a, .pager .next > span {
+ float: right; }
+ .pager .previous > a, .pager .previous > span {
+ float: left; }
+ .pager .disabled > a, .pager .disabled > a:hover, .pager .disabled > a:focus, .pager .disabled > span {
+ color: #999999;
+ background-color: #fff;
+ cursor: not-allowed; }
+
+.label {
+ display: inline;
+ padding: 0.2em 0.6em 0.3em;
+ font-size: 75%;
+ font-weight: bold;
+ line-height: 1;
+ color: #fff;
+ text-align: center;
+ white-space: nowrap;
+ vertical-align: baseline;
+ border-radius: 0.25em; }
+ .label:empty {
+ display: none; }
+ .btn .label {
+ position: relative;
+ top: -1px; }
+
+a.label:hover, a.label:focus {
+ color: #fff;
+ text-decoration: none;
+ cursor: pointer; }
+
+.label-default {
+ background-color: #999999; }
+ .label-default[href]:hover, .label-default[href]:focus {
+ background-color: #808080; }
+
+.label-primary {
+ background-color: #428bca; }
+ .label-primary[href]:hover, .label-primary[href]:focus {
+ background-color: #3073a9; }
+
+.label-success {
+ background-color: #5cb85c; }
+ .label-success[href]:hover, .label-success[href]:focus {
+ background-color: #469d44; }
+
+.label-info {
+ background-color: #5bc0de; }
+ .label-info[href]:hover, .label-info[href]:focus {
+ background-color: #31b2d5; }
+
+.label-warning {
+ background-color: #f0ad4e; }
+ .label-warning[href]:hover, .label-warning[href]:focus {
+ background-color: #ec971f; }
+
+.label-danger {
+ background-color: #d9534f; }
+ .label-danger[href]:hover, .label-danger[href]:focus {
+ background-color: #c92e2c; }
+
+.badge {
+ display: inline-block;
+ min-width: 10px;
+ padding: 3px 7px;
+ font-size: 12px;
+ font-weight: bold;
+ color: #fff;
+ line-height: 1;
+ vertical-align: baseline;
+ white-space: nowrap;
+ text-align: center;
+ background-color: #999999;
+ border-radius: 10px; }
+ .badge:empty {
+ display: none; }
+ .btn .badge {
+ position: relative;
+ top: -1px; }
+ .btn-xs .badge, .btn-xs .btn-group-xs > .btn, .btn-group-xs > .btn-xs .btn {
+ top: 0;
+ padding: 1px 5px; }
+ a.list-group-item.active > .badge, .nav-pills > .active > a > .badge {
+ color: #428bca;
+ background-color: #fff; }
+ .nav-pills > li > a > .badge {
+ margin-left: 3px; }
+
+a.badge:hover, a.badge:focus {
+ color: #fff;
+ text-decoration: none;
+ cursor: pointer; }
+
+.jumbotron {
+ padding: 30px;
+ margin-bottom: 30px;
+ color: inherit;
+ background-color: #eeeeee; }
+ .jumbotron h1, .jumbotron .h1 {
+ color: inherit; }
+ .jumbotron p {
+ margin-bottom: 15px;
+ font-size: 21px;
+ font-weight: 200; }
+ .jumbotron > hr {
+ border-top-color: #d5d5d5; }
+ .container .jumbotron {
+ border-radius: 6px; }
+ .jumbotron .container {
+ max-width: 100%; }
+ @media screen and (min-width: 768px) {
+ .jumbotron {
+ padding-top: 48px;
+ padding-bottom: 48px; }
+ .container .jumbotron {
+ padding-left: 60px;
+ padding-right: 60px; }
+ .jumbotron h1, .jumbotron .h1 {
+ font-size: 63px; } }
+
+.thumbnail {
+ display: block;
+ padding: 4px;
+ margin-bottom: 20px;
+ line-height: 1.42857;
+ background-color: #fff;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ transition: all 0.2s ease-in-out; }
+ .thumbnail > img, .thumbnail a > img {
+ display: block;
+ max-width: 100%;
+ height: auto;
+ margin-left: auto;
+ margin-right: auto; }
+ .thumbnail .caption {
+ padding: 9px;
+ color: #333333; }
+
+a.thumbnail:hover, a.thumbnail:focus, a.thumbnail.active {
+ border-color: #428bca; }
+
+.alert {
+ padding: 15px;
+ margin-bottom: 20px;
+ border: 1px solid transparent;
+ border-radius: 4px; }
+ .alert h4 {
+ margin-top: 0;
+ color: inherit; }
+ .alert .alert-link {
+ font-weight: bold; }
+ .alert > p, .alert > ul {
+ margin-bottom: 0; }
+ .alert > p + p {
+ margin-top: 5px; }
+
+.alert-dismissable {
+ padding-right: 35px; }
+ .alert-dismissable .close {
+ position: relative;
+ top: -2px;
+ right: -21px;
+ color: inherit; }
+
+.alert-success {
+ background-color: #dff0d8;
+ border-color: #d7e9c6;
+ color: #3c763d; }
+ .alert-success hr {
+ border-top-color: #cae2b3; }
+ .alert-success .alert-link {
+ color: #2b542b; }
+
+.alert-info {
+ background-color: #d9edf7;
+ border-color: #bce9f1;
+ color: #31708f; }
+ .alert-info hr {
+ border-top-color: #a6e2ec; }
+ .alert-info .alert-link {
+ color: #245369; }
+
+.alert-warning {
+ background-color: #fcf8e3;
+ border-color: #faeacc;
+ color: #8a6d3b; }
+ .alert-warning hr {
+ border-top-color: #f7e0b5; }
+ .alert-warning .alert-link {
+ color: #66502c; }
+
+.alert-danger {
+ background-color: #f2dede;
+ border-color: #ebccd1;
+ color: #a94442; }
+ .alert-danger hr {
+ border-top-color: #e4b9c0; }
+ .alert-danger .alert-link {
+ color: #843534; }
+
+@-webkit-keyframes progress-bar-stripes {
+ from {
+ background-position: 40px 0; }
+
+ to {
+ background-position: 0 0; } }
+
+@keyframes progress-bar-stripes {
+ from {
+ background-position: 40px 0; }
+
+ to {
+ background-position: 0 0; } }
+
+.progress {
+ overflow: hidden;
+ height: 20px;
+ margin-bottom: 20px;
+ background-color: #f5f5f5;
+ border-radius: 4px;
+ box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); }
+
+.progress-bar {
+ float: left;
+ width: 0%;
+ height: 100%;
+ font-size: 12px;
+ line-height: 20px;
+ color: #fff;
+ text-align: center;
+ background-color: #428bca;
+ box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
+ transition: width 0.6s ease; }
+
+.progress-striped .progress-bar {
+ background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+ background-size: 40px 40px; }
+
+.progress.active .progress-bar {
+ -webkit-animation: progress-bar-stripes 2s linear infinite;
+ animation: progress-bar-stripes 2s linear infinite; }
+
+.progress-bar[aria-valuenow="1"], .progress-bar[aria-valuenow="2"] {
+ min-width: 30px; }
+.progress-bar[aria-valuenow="0"] {
+ color: #999999;
+ min-width: 30px;
+ background-color: transparent;
+ background-image: none;
+ box-shadow: none; }
+
+.progress-bar-success {
+ background-color: #5cb85c; }
+ .progress-striped .progress-bar-success {
+ background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); }
+
+.progress-bar-info {
+ background-color: #5bc0de; }
+ .progress-striped .progress-bar-info {
+ background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); }
+
+.progress-bar-warning {
+ background-color: #f0ad4e; }
+ .progress-striped .progress-bar-warning {
+ background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); }
+
+.progress-bar-danger {
+ background-color: #d9534f; }
+ .progress-striped .progress-bar-danger {
+ background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); }
+
+.media, .media-body {
+ overflow: hidden;
+ zoom: 1; }
+
+.media, .media .media {
+ margin-top: 15px; }
+
+.media:first-child {
+ margin-top: 0; }
+
+.media-object {
+ display: block; }
+
+.media-heading {
+ margin: 0 0 5px; }
+
+.media > .pull-left {
+ margin-right: 10px; }
+.media > .pull-right {
+ margin-left: 10px; }
+
+.media-list {
+ padding-left: 0;
+ list-style: none; }
+
+.list-group {
+ margin-bottom: 20px;
+ padding-left: 0; }
+
+.list-group-item {
+ position: relative;
+ display: block;
+ padding: 10px 15px;
+ margin-bottom: -1px;
+ background-color: #fff;
+ border: 1px solid #ddd; }
+ .list-group-item:first-child {
+ border-top-right-radius: 4px;
+ border-top-left-radius: 4px; }
+ .list-group-item:last-child {
+ margin-bottom: 0;
+ border-bottom-right-radius: 4px;
+ border-bottom-left-radius: 4px; }
+ .list-group-item > .badge {
+ float: right; }
+ .list-group-item > .badge + .badge {
+ margin-right: 5px; }
+
+a.list-group-item {
+ color: #555; }
+ a.list-group-item .list-group-item-heading {
+ color: #333; }
+ a.list-group-item:hover, a.list-group-item:focus {
+ text-decoration: none;
+ color: #555;
+ background-color: #f5f5f5; }
+
+.list-group-item.disabled, .list-group-item.disabled:hover, .list-group-item.disabled:focus {
+ background-color: #eeeeee;
+ color: #999999; }
+ .list-group-item.disabled .list-group-item-heading, .list-group-item.disabled:hover .list-group-item-heading, .list-group-item.disabled:focus .list-group-item-heading {
+ color: inherit; }
+ .list-group-item.disabled .list-group-item-text, .list-group-item.disabled:hover .list-group-item-text, .list-group-item.disabled:focus .list-group-item-text {
+ color: #999999; }
+.list-group-item.active, .list-group-item.active:hover, .list-group-item.active:focus {
+ z-index: 2;
+ color: #fff;
+ background-color: #428bca;
+ border-color: #428bca; }
+ .list-group-item.active .list-group-item-heading, .list-group-item.active:hover .list-group-item-heading, .list-group-item.active:focus .list-group-item-heading {
+ color: inherit; }
+ .list-group-item.active .list-group-item-text, .list-group-item.active:hover .list-group-item-text, .list-group-item.active:focus .list-group-item-text {
+ color: #e1edf7; }
+
+.list-group-item-success {
+ color: #3c763d;
+ background-color: #dff0d8; }
+
+a.list-group-item-success {
+ color: #3c763d; }
+ a.list-group-item-success .list-group-item-heading {
+ color: inherit; }
+ a.list-group-item-success:hover, a.list-group-item-success:focus {
+ color: #3c763d;
+ background-color: #d0e9c6; }
+ a.list-group-item-success.active, a.list-group-item-success.active:hover, a.list-group-item-success.active:focus {
+ color: #fff;
+ background-color: #3c763d;
+ border-color: #3c763d; }
+
+.list-group-item-info {
+ color: #31708f;
+ background-color: #d9edf7; }
+
+a.list-group-item-info {
+ color: #31708f; }
+ a.list-group-item-info .list-group-item-heading {
+ color: inherit; }
+ a.list-group-item-info:hover, a.list-group-item-info:focus {
+ color: #31708f;
+ background-color: #c4e4f3; }
+ a.list-group-item-info.active, a.list-group-item-info.active:hover, a.list-group-item-info.active:focus {
+ color: #fff;
+ background-color: #31708f;
+ border-color: #31708f; }
+
+.list-group-item-warning {
+ color: #8a6d3b;
+ background-color: #fcf8e3; }
+
+a.list-group-item-warning {
+ color: #8a6d3b; }
+ a.list-group-item-warning .list-group-item-heading {
+ color: inherit; }
+ a.list-group-item-warning:hover, a.list-group-item-warning:focus {
+ color: #8a6d3b;
+ background-color: #faf2cc; }
+ a.list-group-item-warning.active, a.list-group-item-warning.active:hover, a.list-group-item-warning.active:focus {
+ color: #fff;
+ background-color: #8a6d3b;
+ border-color: #8a6d3b; }
+
+.list-group-item-danger {
+ color: #a94442;
+ background-color: #f2dede; }
+
+a.list-group-item-danger {
+ color: #a94442; }
+ a.list-group-item-danger .list-group-item-heading {
+ color: inherit; }
+ a.list-group-item-danger:hover, a.list-group-item-danger:focus {
+ color: #a94442;
+ background-color: #ebcccc; }
+ a.list-group-item-danger.active, a.list-group-item-danger.active:hover, a.list-group-item-danger.active:focus {
+ color: #fff;
+ background-color: #a94442;
+ border-color: #a94442; }
+
+.list-group-item-heading {
+ margin-top: 0;
+ margin-bottom: 5px; }
+
+.list-group-item-text {
+ margin-bottom: 0;
+ line-height: 1.3; }
+
+.panel {
+ margin-bottom: 20px;
+ background-color: #fff;
+ border: 1px solid transparent;
+ border-radius: 4px;
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05); }
+
+.panel-body {
+ padding: 15px; }
+ .panel-body:before, .panel-body:after {
+ content: " ";
+ display: table; }
+ .panel-body:after {
+ clear: both; }
+
+.panel-heading {
+ padding: 10px 15px;
+ border-bottom: 1px solid transparent;
+ border-top-right-radius: 3px;
+ border-top-left-radius: 3px; }
+ .panel-heading > .dropdown .dropdown-toggle {
+ color: inherit; }
+
+.panel-title {
+ margin-top: 0;
+ margin-bottom: 0;
+ font-size: 16px;
+ color: inherit; }
+ .panel-title > a {
+ color: inherit; }
+
+.panel-footer {
+ padding: 10px 15px;
+ background-color: #f5f5f5;
+ border-top: 1px solid #ddd;
+ border-bottom-right-radius: 3px;
+ border-bottom-left-radius: 3px; }
+
+.panel > .list-group {
+ margin-bottom: 0; }
+ .panel > .list-group .list-group-item {
+ border-width: 1px 0;
+ border-radius: 0; }
+ .panel > .list-group:first-child .list-group-item:first-child {
+ border-top: 0;
+ border-top-right-radius: 3px;
+ border-top-left-radius: 3px; }
+ .panel > .list-group:last-child .list-group-item:last-child {
+ border-bottom: 0;
+ border-bottom-right-radius: 3px;
+ border-bottom-left-radius: 3px; }
+
+.panel-heading + .list-group .list-group-item:first-child {
+ border-top-width: 0; }
+
+.panel > .table, .panel > .table-responsive > .table {
+ margin-bottom: 0; }
+.panel > .table:first-child, .panel > .table-responsive:first-child > .table:first-child {
+ border-top-right-radius: 3px;
+ border-top-left-radius: 3px; }
+ .panel > .table:first-child > thead:first-child > tr:first-child td:first-child, .panel > .table:first-child > thead:first-child > tr:first-child th:first-child, .panel > .table:first-child > tbody:first-child > tr:first-child td:first-child, .panel > .table:first-child > tbody:first-child > tr:first-child th:first-child, .panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:first-child, .panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:first-child, .panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:first-child, .panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:first-child {
+ border-top-left-radius: 3px; }
+ .panel > .table:first-child > thead:first-child > tr:first-child td:last-child, .panel > .table:first-child > thead:first-child > tr:first-child th:last-child, .panel > .table:first-child > tbody:first-child > tr:first-child td:last-child, .panel > .table:first-child > tbody:first-child > tr:first-child th:last-child, .panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:last-child, .panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:last-child, .panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:last-child, .panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:last-child {
+ border-top-right-radius: 3px; }
+.panel > .table:last-child, .panel > .table-responsive:last-child > .table:last-child {
+ border-bottom-right-radius: 3px;
+ border-bottom-left-radius: 3px; }
+ .panel > .table:last-child > tbody:last-child > tr:last-child td:first-child, .panel > .table:last-child > tbody:last-child > tr:last-child th:first-child, .panel > .table:last-child > tfoot:last-child > tr:last-child td:first-child, .panel > .table:last-child > tfoot:last-child > tr:last-child th:first-child, .panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:first-child, .panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:first-child, .panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:first-child, .panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:first-child {
+ border-bottom-left-radius: 3px; }
+ .panel > .table:last-child > tbody:last-child > tr:last-child td:last-child, .panel > .table:last-child > tbody:last-child > tr:last-child th:last-child, .panel > .table:last-child > tfoot:last-child > tr:last-child td:last-child, .panel > .table:last-child > tfoot:last-child > tr:last-child th:last-child, .panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:last-child, .panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:last-child, .panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:last-child, .panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:last-child {
+ border-bottom-right-radius: 3px; }
+.panel > .panel-body + .table, .panel > .panel-body + .table-responsive {
+ border-top: 1px solid #ddd; }
+.panel > .table > tbody:first-child > tr:first-child th, .panel > .table > tbody:first-child > tr:first-child td {
+ border-top: 0; }
+.panel > .table-bordered, .panel > .table-responsive > .table-bordered {
+ border: 0; }
+ .panel > .table-bordered > thead > tr > th:first-child, .panel > .table-bordered > thead > tr > td:first-child, .panel > .table-bordered > tbody > tr > th:first-child, .panel > .table-bordered > tbody > tr > td:first-child, .panel > .table-bordered > tfoot > tr > th:first-child, .panel > .table-bordered > tfoot > tr > td:first-child, .panel > .table-responsive > .table-bordered > thead > tr > th:first-child, .panel > .table-responsive > .table-bordered > thead > tr > td:first-child, .panel > .table-responsive > .table-bordered > tbody > tr > th:first-child, .panel > .table-responsive > .table-bordered > tbody > tr > td:first-child, .panel > .table-responsive > .table-bordered > tfoot > tr > th:first-child, .panel > .table-responsive > .table-bordered > tfoot > tr > td:first-child {
+ border-left: 0; }
+ .panel > .table-bordered > thead > tr > th:last-child, .panel > .table-bordered > thead > tr > td:last-child, .panel > .table-bordered > tbody > tr > th:last-child, .panel > .table-bordered > tbody > tr > td:last-child, .panel > .table-bordered > tfoot > tr > th:last-child, .panel > .table-bordered > tfoot > tr > td:last-child, .panel > .table-responsive > .table-bordered > thead > tr > th:last-child, .panel > .table-responsive > .table-bordered > thead > tr > td:last-child, .panel > .table-responsive > .table-bordered > tbody > tr > th:last-child, .panel > .table-responsive > .table-bordered > tbody > tr > td:last-child, .panel > .table-responsive > .table-bordered > tfoot > tr > th:last-child, .panel > .table-responsive > .table-bordered > tfoot > tr > td:last-child {
+ border-right: 0; }
+ .panel > .table-bordered > thead > tr:first-child > td, .panel > .table-bordered > thead > tr:first-child > th, .panel > .table-bordered > tbody > tr:first-child > td, .panel > .table-bordered > tbody > tr:first-child > th, .panel > .table-responsive > .table-bordered > thead > tr:first-child > td, .panel > .table-responsive > .table-bordered > thead > tr:first-child > th, .panel > .table-responsive > .table-bordered > tbody > tr:first-child > td, .panel > .table-responsive > .table-bordered > tbody > tr:first-child > th {
+ border-bottom: 0; }
+ .panel > .table-bordered > tbody > tr:last-child > td, .panel > .table-bordered > tbody > tr:last-child > th, .panel > .table-bordered > tfoot > tr:last-child > td, .panel > .table-bordered > tfoot > tr:last-child > th, .panel > .table-responsive > .table-bordered > tbody > tr:last-child > td, .panel > .table-responsive > .table-bordered > tbody > tr:last-child > th, .panel > .table-responsive > .table-bordered > tfoot > tr:last-child > td, .panel > .table-responsive > .table-bordered > tfoot > tr:last-child > th {
+ border-bottom: 0; }
+.panel > .table-responsive {
+ border: 0;
+ margin-bottom: 0; }
+
+.panel-group {
+ margin-bottom: 20px; }
+ .panel-group .panel {
+ margin-bottom: 0;
+ border-radius: 4px; }
+ .panel-group .panel + .panel {
+ margin-top: 5px; }
+ .panel-group .panel-heading {
+ border-bottom: 0; }
+ .panel-group .panel-heading + .panel-collapse .panel-body {
+ border-top: 1px solid #ddd; }
+ .panel-group .panel-footer {
+ border-top: 0; }
+ .panel-group .panel-footer + .panel-collapse .panel-body {
+ border-bottom: 1px solid #ddd; }
+
+.panel-default {
+ border-color: #ddd; }
+ .panel-default > .panel-heading {
+ color: #333333;
+ background-color: #f5f5f5;
+ border-color: #ddd; }
+ .panel-default > .panel-heading + .panel-collapse > .panel-body {
+ border-top-color: #ddd; }
+ .panel-default > .panel-footer + .panel-collapse > .panel-body {
+ border-bottom-color: #ddd; }
+
+.panel-primary {
+ border-color: #428bca; }
+ .panel-primary > .panel-heading {
+ color: #fff;
+ background-color: #428bca;
+ border-color: #428bca; }
+ .panel-primary > .panel-heading + .panel-collapse > .panel-body {
+ border-top-color: #428bca; }
+ .panel-primary > .panel-footer + .panel-collapse > .panel-body {
+ border-bottom-color: #428bca; }
+
+.panel-success {
+ border-color: #d7e9c6; }
+ .panel-success > .panel-heading {
+ color: #3c763d;
+ background-color: #dff0d8;
+ border-color: #d7e9c6; }
+ .panel-success > .panel-heading + .panel-collapse > .panel-body {
+ border-top-color: #d7e9c6; }
+ .panel-success > .panel-footer + .panel-collapse > .panel-body {
+ border-bottom-color: #d7e9c6; }
+
+.panel-info {
+ border-color: #bce9f1; }
+ .panel-info > .panel-heading {
+ color: #31708f;
+ background-color: #d9edf7;
+ border-color: #bce9f1; }
+ .panel-info > .panel-heading + .panel-collapse > .panel-body {
+ border-top-color: #bce9f1; }
+ .panel-info > .panel-footer + .panel-collapse > .panel-body {
+ border-bottom-color: #bce9f1; }
+
+.panel-warning {
+ border-color: #faeacc; }
+ .panel-warning > .panel-heading {
+ color: #8a6d3b;
+ background-color: #fcf8e3;
+ border-color: #faeacc; }
+ .panel-warning > .panel-heading + .panel-collapse > .panel-body {
+ border-top-color: #faeacc; }
+ .panel-warning > .panel-footer + .panel-collapse > .panel-body {
+ border-bottom-color: #faeacc; }
+
+.panel-danger {
+ border-color: #ebccd1; }
+ .panel-danger > .panel-heading {
+ color: #a94442;
+ background-color: #f2dede;
+ border-color: #ebccd1; }
+ .panel-danger > .panel-heading + .panel-collapse > .panel-body {
+ border-top-color: #ebccd1; }
+ .panel-danger > .panel-footer + .panel-collapse > .panel-body {
+ border-bottom-color: #ebccd1; }
+
+.embed-responsive {
+ position: relative;
+ display: block;
+ height: 0;
+ padding: 0;
+ overflow: hidden; }
+ .embed-responsive .embed-responsive-item, .embed-responsive iframe, .embed-responsive embed, .embed-responsive object {
+ position: absolute;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ height: 100%;
+ width: 100%;
+ border: 0; }
+ .embed-responsive.embed-responsive-16by9 {
+ padding-bottom: 56.25%; }
+ .embed-responsive.embed-responsive-4by3 {
+ padding-bottom: 75%; }
+
+.well {
+ min-height: 20px;
+ padding: 19px;
+ margin-bottom: 20px;
+ background-color: #f5f5f5;
+ border: 1px solid #e3e3e3;
+ border-radius: 4px;
+ box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); }
+ .well blockquote {
+ border-color: #ddd;
+ border-color: rgba(0, 0, 0, 0.15); }
+
+.well-lg {
+ padding: 24px;
+ border-radius: 6px; }
+
+.well-sm {
+ padding: 9px;
+ border-radius: 3px; }
+
+.close {
+ float: right;
+ font-size: 21px;
+ font-weight: bold;
+ line-height: 1;
+ color: #000;
+ text-shadow: 0 1px 0 #fff;
+ opacity: 0.2;
+ filter: alpha(opacity=20); }
+ .close:hover, .close:focus {
+ color: #000;
+ text-decoration: none;
+ cursor: pointer;
+ opacity: 0.5;
+ filter: alpha(opacity=50); }
+
+button.close {
+ padding: 0;
+ cursor: pointer;
+ background: transparent;
+ border: 0;
+ -webkit-appearance: none; }
+
+.modal-open {
+ overflow: hidden; }
+
+.modal {
+ display: none;
+ overflow: auto;
+ overflow-y: scroll;
+ position: fixed;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ z-index: 1050;
+ -webkit-overflow-scrolling: touch;
+ outline: 0; }
+ .modal.fade .modal-dialog {
+ -webkit-transform: translate(0, -25%);
+ transform: translate(0, -25%);
+ transition: -webkit-transform 0.3s ease-out;
+ transition: transform 0.3s ease-out; }
+ .modal.in .modal-dialog {
+ -webkit-transform: translate(0, 0);
+ transform: translate(0, 0); }
+
+.modal-dialog {
+ position: relative;
+ width: auto;
+ margin: 10px; }
+
+.modal-content {
+ position: relative;
+ background-color: #fff;
+ border: 1px solid #999;
+ border: 1px solid rgba(0, 0, 0, 0.2);
+ border-radius: 6px;
+ box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5);
+ background-clip: padding-box;
+ outline: 0; }
+
+.modal-backdrop {
+ position: fixed;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ z-index: 1040;
+ background-color: #000; }
+ .modal-backdrop.fade {
+ opacity: 0;
+ filter: alpha(opacity=0); }
+ .modal-backdrop.in {
+ opacity: 0.5;
+ filter: alpha(opacity=50); }
+
+.modal-header {
+ padding: 15px;
+ border-bottom: 1px solid #e5e5e5;
+ min-height: 16.42857px; }
+
+.modal-header .close {
+ margin-top: -2px; }
+
+.modal-title {
+ margin: 0;
+ line-height: 1.42857; }
+
+.modal-body {
+ position: relative;
+ padding: 15px; }
+
+.modal-footer {
+ padding: 15px;
+ text-align: right;
+ border-top: 1px solid #e5e5e5; }
+ .modal-footer:before, .modal-footer:after {
+ content: " ";
+ display: table; }
+ .modal-footer:after {
+ clear: both; }
+ .modal-footer .btn + .btn {
+ margin-left: 5px;
+ margin-bottom: 0; }
+ .modal-footer .btn-group .btn + .btn {
+ margin-left: -1px; }
+ .modal-footer .btn-block + .btn-block {
+ margin-left: 0; }
+
+.modal-scrollbar-measure {
+ position: absolute;
+ top: -9999px;
+ width: 50px;
+ height: 50px;
+ overflow: scroll; }
+
+@media (min-width: 768px) {
+ .modal-dialog {
+ width: 600px;
+ margin: 30px auto; }
+ .modal-content {
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5); }
+ .modal-sm {
+ width: 300px; } }
+
+@media (min-width: 992px) {
+ .modal-lg {
+ width: 900px; } }
+
+.tooltip {
+ position: absolute;
+ z-index: 1070;
+ display: block;
+ visibility: visible;
+ font-size: 12px;
+ line-height: 1.4;
+ opacity: 0;
+ filter: alpha(opacity=0); }
+ .tooltip.in {
+ opacity: 0.9;
+ filter: alpha(opacity=90); }
+ .tooltip.top {
+ margin-top: -3px;
+ padding: 5px 0; }
+ .tooltip.right {
+ margin-left: 3px;
+ padding: 0 5px; }
+ .tooltip.bottom {
+ margin-top: 3px;
+ padding: 5px 0; }
+ .tooltip.left {
+ margin-left: -3px;
+ padding: 0 5px; }
+
+.tooltip-inner {
+ max-width: 200px;
+ padding: 3px 8px;
+ color: #fff;
+ text-align: center;
+ text-decoration: none;
+ background-color: #000;
+ border-radius: 4px; }
+
+.tooltip-arrow {
+ position: absolute;
+ width: 0;
+ height: 0;
+ border-color: transparent;
+ border-style: solid; }
+
+.tooltip.top .tooltip-arrow {
+ bottom: 0;
+ left: 50%;
+ margin-left: -5px;
+ border-width: 5px 5px 0;
+ border-top-color: #000; }
+.tooltip.top-left .tooltip-arrow {
+ bottom: 0;
+ left: 5px;
+ border-width: 5px 5px 0;
+ border-top-color: #000; }
+.tooltip.top-right .tooltip-arrow {
+ bottom: 0;
+ right: 5px;
+ border-width: 5px 5px 0;
+ border-top-color: #000; }
+.tooltip.right .tooltip-arrow {
+ top: 50%;
+ left: 0;
+ margin-top: -5px;
+ border-width: 5px 5px 5px 0;
+ border-right-color: #000; }
+.tooltip.left .tooltip-arrow {
+ top: 50%;
+ right: 0;
+ margin-top: -5px;
+ border-width: 5px 0 5px 5px;
+ border-left-color: #000; }
+.tooltip.bottom .tooltip-arrow {
+ top: 0;
+ left: 50%;
+ margin-left: -5px;
+ border-width: 0 5px 5px;
+ border-bottom-color: #000; }
+.tooltip.bottom-left .tooltip-arrow {
+ top: 0;
+ left: 5px;
+ border-width: 0 5px 5px;
+ border-bottom-color: #000; }
+.tooltip.bottom-right .tooltip-arrow {
+ top: 0;
+ right: 5px;
+ border-width: 0 5px 5px;
+ border-bottom-color: #000; }
+
+.popover {
+ position: absolute;
+ top: 0;
+ left: 0;
+ z-index: 1060;
+ display: none;
+ max-width: 276px;
+ padding: 1px;
+ text-align: left;
+ background-color: #fff;
+ background-clip: padding-box;
+ border: 1px solid #ccc;
+ border: 1px solid rgba(0, 0, 0, 0.2);
+ border-radius: 6px;
+ box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
+ white-space: normal; }
+ .popover.top {
+ margin-top: -10px; }
+ .popover.right {
+ margin-left: 10px; }
+ .popover.bottom {
+ margin-top: 10px; }
+ .popover.left {
+ margin-left: -10px; }
+
+.popover-title {
+ margin: 0;
+ padding: 8px 14px;
+ font-size: 14px;
+ font-weight: normal;
+ line-height: 18px;
+ background-color: #f7f7f7;
+ border-bottom: 1px solid #ebebeb;
+ border-radius: 5px 5px 0 0; }
+
+.popover-content {
+ padding: 9px 14px; }
+
+.popover > .arrow, .popover > .arrow:after {
+ position: absolute;
+ display: block;
+ width: 0;
+ height: 0;
+ border-color: transparent;
+ border-style: solid; }
+
+.popover > .arrow {
+ border-width: 11px; }
+
+.popover > .arrow:after {
+ border-width: 10px;
+ content: ""; }
+
+.popover.top > .arrow {
+ left: 50%;
+ margin-left: -11px;
+ border-bottom-width: 0;
+ border-top-color: #999999;
+ border-top-color: rgba(0, 0, 0, 0.05);
+ bottom: -11px; }
+ .popover.top > .arrow:after {
+ content: " ";
+ bottom: 1px;
+ margin-left: -10px;
+ border-bottom-width: 0;
+ border-top-color: #fff; }
+.popover.right > .arrow {
+ top: 50%;
+ left: -11px;
+ margin-top: -11px;
+ border-left-width: 0;
+ border-right-color: #999999;
+ border-right-color: rgba(0, 0, 0, 0.05); }
+ .popover.right > .arrow:after {
+ content: " ";
+ left: 1px;
+ bottom: -10px;
+ border-left-width: 0;
+ border-right-color: #fff; }
+.popover.bottom > .arrow {
+ left: 50%;
+ margin-left: -11px;
+ border-top-width: 0;
+ border-bottom-color: #999999;
+ border-bottom-color: rgba(0, 0, 0, 0.05);
+ top: -11px; }
+ .popover.bottom > .arrow:after {
+ content: " ";
+ top: 1px;
+ margin-left: -10px;
+ border-top-width: 0;
+ border-bottom-color: #fff; }
+.popover.left > .arrow {
+ top: 50%;
+ right: -11px;
+ margin-top: -11px;
+ border-right-width: 0;
+ border-left-color: #999999;
+ border-left-color: rgba(0, 0, 0, 0.05); }
+ .popover.left > .arrow:after {
+ content: " ";
+ right: 1px;
+ border-right-width: 0;
+ border-left-color: #fff;
+ bottom: -10px; }
+
+.carousel {
+ position: relative; }
+
+.carousel-inner {
+ position: relative;
+ overflow: hidden;
+ width: 100%; }
+ .carousel-inner > .item {
+ display: none;
+ position: relative;
+ transition: 0.6s ease-in-out left; }
+ .carousel-inner > .item > img, .carousel-inner > .item > a > img {
+ display: block;
+ max-width: 100%;
+ height: auto;
+ line-height: 1; }
+ .carousel-inner > .active, .carousel-inner > .next, .carousel-inner > .prev {
+ display: block; }
+ .carousel-inner > .active {
+ left: 0; }
+ .carousel-inner > .next, .carousel-inner > .prev {
+ position: absolute;
+ top: 0;
+ width: 100%; }
+ .carousel-inner > .next {
+ left: 100%; }
+ .carousel-inner > .prev {
+ left: -100%; }
+ .carousel-inner > .next.left, .carousel-inner > .prev.right {
+ left: 0; }
+ .carousel-inner > .active.left {
+ left: -100%; }
+ .carousel-inner > .active.right {
+ left: 100%; }
+
+.carousel-control {
+ position: absolute;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ width: 15%;
+ opacity: 0.5;
+ filter: alpha(opacity=50);
+ font-size: 20px;
+ color: #fff;
+ text-align: center;
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6); }
+ .carousel-control.left {
+ background-image: linear-gradient(to right, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);
+ background-repeat: repeat-x;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1); }
+ .carousel-control.right {
+ left: auto;
+ right: 0;
+ background-image: linear-gradient(to right, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);
+ background-repeat: repeat-x;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1); }
+ .carousel-control:hover, .carousel-control:focus {
+ outline: 0;
+ color: #fff;
+ text-decoration: none;
+ opacity: 0.9;
+ filter: alpha(opacity=90); }
+ .carousel-control .icon-prev, .carousel-control .icon-next, .carousel-control .glyphicon-chevron-left, .carousel-control .glyphicon-chevron-right {
+ position: absolute;
+ top: 50%;
+ z-index: 5;
+ display: inline-block; }
+ .carousel-control .icon-prev, .carousel-control .glyphicon-chevron-left {
+ left: 50%;
+ margin-left: -10px; }
+ .carousel-control .icon-next, .carousel-control .glyphicon-chevron-right {
+ right: 50%;
+ margin-right: -10px; }
+ .carousel-control .icon-prev, .carousel-control .icon-next {
+ width: 20px;
+ height: 20px;
+ margin-top: -10px;
+ font-family: serif; }
+ .carousel-control .icon-prev:before {
+ content: '\2039'; }
+ .carousel-control .icon-next:before {
+ content: '\203a'; }
+
+.carousel-indicators {
+ position: absolute;
+ bottom: 10px;
+ left: 50%;
+ z-index: 15;
+ width: 60%;
+ margin-left: -30%;
+ padding-left: 0;
+ list-style: none;
+ text-align: center; }
+ .carousel-indicators li {
+ display: inline-block;
+ width: 10px;
+ height: 10px;
+ margin: 1px;
+ text-indent: -999px;
+ border: 1px solid #fff;
+ border-radius: 10px;
+ cursor: pointer;
+ background-color: #000 \9;
+ background-color: rgba(0, 0, 0, 0); }
+ .carousel-indicators .active {
+ margin: 0;
+ width: 12px;
+ height: 12px;
+ background-color: #fff; }
+
+.carousel-caption {
+ position: absolute;
+ left: 15%;
+ right: 15%;
+ bottom: 20px;
+ z-index: 10;
+ padding-top: 20px;
+ padding-bottom: 20px;
+ color: #fff;
+ text-align: center;
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6); }
+ .carousel-caption .btn {
+ text-shadow: none; }
+
+@media screen and (min-width: 768px) {
+ .carousel-control .glyphicon-chevron-left, .carousel-control .glyphicon-chevron-right, .carousel-control .icon-prev, .carousel-control .icon-next {
+ width: 30px;
+ height: 30px;
+ margin-top: -15px;
+ font-size: 30px; }
+ .carousel-control .glyphicon-chevron-left, .carousel-control .icon-prev {
+ margin-left: -15px; }
+ .carousel-control .glyphicon-chevron-right, .carousel-control .icon-next {
+ margin-right: -15px; }
+ .carousel-caption {
+ left: 20%;
+ right: 20%;
+ padding-bottom: 30px; }
+ .carousel-indicators {
+ bottom: 20px; } }
+
+.clearfix:before, .clearfix:after {
+ content: " ";
+ display: table; }
+.clearfix:after {
+ clear: both; }
+
+.center-block {
+ display: block;
+ margin-left: auto;
+ margin-right: auto; }
+
+.pull-right {
+ float: right !important; }
+
+.pull-left {
+ float: left !important; }
+
+.hide {
+ display: none !important; }
+
+.show {
+ display: block !important; }
+
+.invisible {
+ visibility: hidden; }
+
+.text-hide {
+ font: 0/0 a;
+ color: transparent;
+ text-shadow: none;
+ background-color: transparent;
+ border: 0; }
+
+.hidden {
+ display: none !important;
+ visibility: hidden !important; }
+
+.affix {
+ position: fixed; }
+
+@-ms-viewport {
+ width: device-width; }
+
+.visible-xs, .visible-sm, .visible-md, .visible-lg {
+ display: none !important; }
+
+.visible-xs-block, .visible-xs-inline, .visible-xs-inline-block, .visible-sm-block, .visible-sm-inline, .visible-sm-inline-block, .visible-md-block, .visible-md-inline, .visible-md-inline-block, .visible-lg-block, .visible-lg-inline, .visible-lg-inline-block {
+ display: none !important; }
+
+@media (max-width: 767px) {
+ .visible-xs {
+ display: block !important; }
+ table.visible-xs {
+ display: table; }
+ tr.visible-xs {
+ display: table-row !important; }
+ th.visible-xs, td.visible-xs {
+ display: table-cell !important; } }
+
+@media (max-width: 767px) {
+ .visible-xs-block {
+ display: block !important; } }
+
+@media (max-width: 767px) {
+ .visible-xs-inline {
+ display: inline !important; } }
+
+@media (max-width: 767px) {
+ .visible-xs-inline-block {
+ display: inline-block !important; } }
+
+@media (min-width: 768px) and (max-width: 991px) {
+ .visible-sm {
+ display: block !important; }
+ table.visible-sm {
+ display: table; }
+ tr.visible-sm {
+ display: table-row !important; }
+ th.visible-sm, td.visible-sm {
+ display: table-cell !important; } }
+
+@media (min-width: 768px) and (max-width: 991px) {
+ .visible-sm-block {
+ display: block !important; } }
+
+@media (min-width: 768px) and (max-width: 991px) {
+ .visible-sm-inline {
+ display: inline !important; } }
+
+@media (min-width: 768px) and (max-width: 991px) {
+ .visible-sm-inline-block {
+ display: inline-block !important; } }
+
+@media (min-width: 992px) and (max-width: 1199px) {
+ .visible-md {
+ display: block !important; }
+ table.visible-md {
+ display: table; }
+ tr.visible-md {
+ display: table-row !important; }
+ th.visible-md, td.visible-md {
+ display: table-cell !important; } }
+
+@media (min-width: 992px) and (max-width: 1199px) {
+ .visible-md-block {
+ display: block !important; } }
+
+@media (min-width: 992px) and (max-width: 1199px) {
+ .visible-md-inline {
+ display: inline !important; } }
+
+@media (min-width: 992px) and (max-width: 1199px) {
+ .visible-md-inline-block {
+ display: inline-block !important; } }
+
+@media (min-width: 1200px) {
+ .visible-lg {
+ display: block !important; }
+ table.visible-lg {
+ display: table; }
+ tr.visible-lg {
+ display: table-row !important; }
+ th.visible-lg, td.visible-lg {
+ display: table-cell !important; } }
+
+@media (min-width: 1200px) {
+ .visible-lg-block {
+ display: block !important; } }
+
+@media (min-width: 1200px) {
+ .visible-lg-inline {
+ display: inline !important; } }
+
+@media (min-width: 1200px) {
+ .visible-lg-inline-block {
+ display: inline-block !important; } }
+
+@media (max-width: 767px) {
+ .hidden-xs {
+ display: none !important; } }
+
+@media (min-width: 768px) and (max-width: 991px) {
+ .hidden-sm {
+ display: none !important; } }
+
+@media (min-width: 992px) and (max-width: 1199px) {
+ .hidden-md {
+ display: none !important; } }
+
+@media (min-width: 1200px) {
+ .hidden-lg {
+ display: none !important; } }
+
+.visible-print {
+ display: none !important; }
+
+@media print {
+ .visible-print {
+ display: block !important; }
+ table.visible-print {
+ display: table; }
+ tr.visible-print {
+ display: table-row !important; }
+ th.visible-print, td.visible-print {
+ display: table-cell !important; } }
+
+.visible-print-block {
+ display: none !important; }
+ @media print {
+ .visible-print-block {
+ display: block !important; } }
+
+.visible-print-inline {
+ display: none !important; }
+ @media print {
+ .visible-print-inline {
+ display: inline !important; } }
+
+.visible-print-inline-block {
+ display: none !important; }
+ @media print {
+ .visible-print-inline-block {
+ display: inline-block !important; } }
+
+@media print {
+ .hidden-print {
+ display: none !important; } }
+
+.browsehappy {
+ margin: 0.2em 0;
+ background: #ccc;
+ color: #000;
+ padding: 0.2em 0; }
+
+/* Space out content a bit */
+body {
+ padding-top: 20px;
+ padding-bottom: 20px; }
+
+/* Everything but the jumbotron gets side spacing for mobile first views */
+.header, .marketing, .footer {
+ padding-left: 15px;
+ padding-right: 15px; }
+
+/* Custom page header */
+.header {
+ border-bottom: 1px solid #e5e5e5;
+ /* Make the masthead heading the same height as the navigation */ }
+ .header h3 {
+ margin-top: 0;
+ margin-bottom: 0;
+ line-height: 40px;
+ padding-bottom: 19px; }
+
+/* Custom page footer */
+.footer {
+ padding-top: 19px;
+ color: #777;
+ border-top: 1px solid #e5e5e5; }
+
+.container-narrow > hr {
+ margin: 30px 0; }
+
+/* Main marketing message and sign up button */
+.jumbotron {
+ text-align: center;
+ border-bottom: 1px solid #e5e5e5; }
+ .jumbotron .btn {
+ font-size: 21px;
+ padding: 14px 24px; }
+
+/* Supporting marketing content */
+.marketing {
+ margin: 40px 0; }
+ .marketing p + h4 {
+ margin-top: 28px; }
+
+/* Responsive: Portrait tablets and up */
+@media screen and (min-width: 768px) {
+ /* Remove the padding we set earlier */
+ /* Space out the masthead */
+ /* Remove the bottom border on the jumbotron for visual effect */
+ .container {
+ max-width: 730px; }
+ .header, .marketing, .footer {
+ padding-left: 0;
+ padding-right: 0; }
+ .header {
+ margin-bottom: 30px; }
+ .jumbotron {
+ border-bottom: 300; } }
+
+/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWFpbi5jc3MiLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiIiwic291cmNlcyI6WyJtYWluLnNjc3MiXSwic291cmNlc0NvbnRlbnQiOlsiJGljb24tZm9udC1wYXRoOiBcIi4uL2Jvd2VyX2NvbXBvbmVudHMvYm9vdHN0cmFwLXNhc3Mtb2ZmaWNpYWwvdmVuZG9yL2Fzc2V0cy9mb250cy9ib290c3RyYXAvXCI7XG5cbi8vIGJvd2VyOnNjc3NcbkBpbXBvcnQgXCIuLi9ib3dlcl9jb21wb25lbnRzL2Jvb3RzdHJhcC1zYXNzLW9mZmljaWFsL3ZlbmRvci9hc3NldHMvc3R5bGVzaGVldHMvYm9vdHN0cmFwLnNjc3NcIjtcbi8vIGVuZGJvd2VyXG5cbi5icm93c2VoYXBweSB7XG4gICAgbWFyZ2luOiAwLjJlbSAwO1xuICAgIGJhY2tncm91bmQ6ICNjY2M7XG4gICAgY29sb3I6ICMwMDA7XG4gICAgcGFkZGluZzogMC4yZW0gMDtcbn1cblxuLyogU3BhY2Ugb3V0IGNvbnRlbnQgYSBiaXQgKi9cbmJvZHkge1xuICAgIHBhZGRpbmctdG9wOiAyMHB4O1xuICAgIHBhZGRpbmctYm90dG9tOiAyMHB4O1xufVxuXG4vKiBFdmVyeXRoaW5nIGJ1dCB0aGUganVtYm90cm9uIGdldHMgc2lkZSBzcGFjaW5nIGZvciBtb2JpbGUgZmlyc3Qgdmlld3MgKi9cbi5oZWFkZXIsXG4ubWFya2V0aW5nLFxuLmZvb3RlciB7XG4gICAgcGFkZGluZy1sZWZ0OiAxNXB4O1xuICAgIHBhZGRpbmctcmlnaHQ6IDE1cHg7XG59XG5cbi8qIEN1c3RvbSBwYWdlIGhlYWRlciAqL1xuLmhlYWRlciB7XG4gICAgYm9yZGVyLWJvdHRvbTogMXB4IHNvbGlkICNlNWU1ZTU7XG5cbiAgICAvKiBNYWtlIHRoZSBtYXN0aGVhZCBoZWFkaW5nIHRoZSBzYW1lIGhlaWdodCBhcyB0aGUgbmF2aWdhdGlvbiAqL1xuICAgIGgzIHtcbiAgICAgICAgbWFyZ2luLXRvcDogMDtcbiAgICAgICAgbWFyZ2luLWJvdHRvbTogMDtcbiAgICAgICAgbGluZS1oZWlnaHQ6IDQwcHg7XG4gICAgICAgIHBhZGRpbmctYm90dG9tOiAxOXB4O1xuICAgIH1cbn1cblxuLyogQ3VzdG9tIHBhZ2UgZm9vdGVyICovXG4uZm9vdGVyIHtcbiAgICBwYWRkaW5nLXRvcDogMTlweDtcbiAgICBjb2xvcjogIzc3NztcbiAgICBib3JkZXItdG9wOiAxcHggc29saWQgI2U1ZTVlNTtcbn1cblxuLmNvbnRhaW5lci1uYXJyb3cgPiBociB7XG4gICAgbWFyZ2luOiAzMHB4IDA7XG59XG5cbi8qIE1haW4gbWFya2V0aW5nIG1lc3NhZ2UgYW5kIHNpZ24gdXAgYnV0dG9uICovXG4uanVtYm90cm9uIHtcbiAgICB0ZXh0LWFsaWduOiBjZW50ZXI7XG4gICAgYm9yZGVyLWJvdHRvbTogMXB4IHNvbGlkICNlNWU1ZTU7XG4gICAgLmJ0biB7XG4gICAgICAgIGZvbnQtc2l6ZTogMjFweDtcbiAgICAgICAgcGFkZGluZzogMTRweCAyNHB4O1xuICAgIH1cbn1cblxuLyogU3VwcG9ydGluZyBtYXJrZXRpbmcgY29udGVudCAqL1xuLm1hcmtldGluZyB7XG4gICAgbWFyZ2luOiA0MHB4IDA7XG4gICAgcCArIGg0IHtcbiAgICAgICAgbWFyZ2luLXRvcDogMjhweDtcbiAgICB9XG59XG5cbi8qIFJlc3BvbnNpdmU6IFBvcnRyYWl0IHRhYmxldHMgYW5kIHVwICovXG5AbWVkaWEgc2NyZWVuIGFuZCAobWluLXdpZHRoOiA3NjhweCkge1xuICAgIC5jb250YWluZXIge1xuICAgICAgICBtYXgtd2lkdGg6IDczMHB4O1xuICAgIH1cblxuICAgIC8qIFJlbW92ZSB0aGUgcGFkZGluZyB3ZSBzZXQgZWFybGllciAqL1xuICAgIC5oZWFkZXIsXG4gICAgLm1hcmtldGluZyxcbiAgICAuZm9vdGVyIHtcbiAgICAgICAgcGFkZGluZy1sZWZ0OiAwO1xuICAgICAgICBwYWRkaW5nLXJpZ2h0OiAwO1xuICAgIH1cblxuICAgIC8qIFNwYWNlIG91dCB0aGUgbWFzdGhlYWQgKi9cbiAgICAuaGVhZGVyIHtcbiAgICAgICAgbWFyZ2luLWJvdHRvbTogMzBweDtcbiAgICB9XG5cbiAgICAvKiBSZW1vdmUgdGhlIGJvdHRvbSBib3JkZXIgb24gdGhlIGp1bWJvdHJvbiBmb3IgdmlzdWFsIGVmZmVjdCAqL1xuICAgIC5qdW1ib3Ryb24ge1xuICAgICAgICBib3JkZXItYm90dG9tOiAzMDA7XG4gICAgfVxufVxuXG4vLyB0aGlzIGlzIGEgY29tbWVudC4uLlxuIl0sInNvdXJjZVJvb3QiOiIvc291cmNlLyJ9 */
diff --git a/devtools/client/styleeditor/test/sourcemap-css/test-stylus.css b/devtools/client/styleeditor/test/sourcemap-css/test-stylus.css
new file mode 100644
index 000000000..0ec51da3b
--- /dev/null
+++ b/devtools/client/styleeditor/test/sourcemap-css/test-stylus.css
@@ -0,0 +1,7 @@
+div {
+ color: #f06;
+}
+span {
+ background-color: #eee;
+}
+/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInRlc3Qtc3R5bHVzLnN0eWwiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBRUE7RUFDRSxPQUFPLEtBQVA7O0FBRUY7RUFDRSxrQkFBa0IsS0FBbEIiLCJmaWxlIjoidGVzdC1zdHlsdXMuY3NzIiwic291cmNlc0NvbnRlbnQiOlsicGF1bHJvdWdldHBpbmsgPSAjZjA2O1xuXG5kaXZcbiAgY29sb3I6IHBhdWxyb3VnZXRwaW5rXG5cbnNwYW5cbiAgYmFja2dyb3VuZC1jb2xvcjogI0VFRVxuIl19 */ \ No newline at end of file
diff --git a/devtools/client/styleeditor/test/sourcemap-sass/media-rules.scss b/devtools/client/styleeditor/test/sourcemap-sass/media-rules.scss
new file mode 100644
index 000000000..4f1c8f216
--- /dev/null
+++ b/devtools/client/styleeditor/test/sourcemap-sass/media-rules.scss
@@ -0,0 +1,11 @@
+$break-small: 320px;
+$break-large: 1200px;
+
+div {
+ @media screen and (max-width: $break-small) {
+ width: 100px;
+ }
+ @media screen and (min-width: $break-large) {
+ width: 400px;
+ }
+}
diff --git a/devtools/client/styleeditor/test/sourcemap-sass/sourcemaps.scss b/devtools/client/styleeditor/test/sourcemap-sass/sourcemaps.scss
new file mode 100644
index 000000000..0ff6c471b
--- /dev/null
+++ b/devtools/client/styleeditor/test/sourcemap-sass/sourcemaps.scss
@@ -0,0 +1,10 @@
+
+$paulrougetpink: #f06;
+
+div {
+ color: $paulrougetpink;
+}
+
+span {
+ background-color: #EEE;
+} \ No newline at end of file
diff --git a/devtools/client/styleeditor/test/sourcemap-styl/test-stylus.styl b/devtools/client/styleeditor/test/sourcemap-styl/test-stylus.styl
new file mode 100644
index 000000000..76ff25c29
--- /dev/null
+++ b/devtools/client/styleeditor/test/sourcemap-styl/test-stylus.styl
@@ -0,0 +1,7 @@
+paulrougetpink = #f06;
+
+div
+ color: paulrougetpink
+
+span
+ background-color: #EEE \ No newline at end of file
diff --git a/devtools/client/styleeditor/test/sourcemaps-inline.html b/devtools/client/styleeditor/test/sourcemaps-inline.html
new file mode 100644
index 000000000..45846fe28
--- /dev/null
+++ b/devtools/client/styleeditor/test/sourcemaps-inline.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>testcase for testing CSS source maps in inline style</title>
+ <style type="text/css">body {
+ background-color: black;
+}
+body > h1 {
+ color: white;
+}
+/*# sourceMappingURL=data:application/json;base64,ewoidmVyc2lvbiI6IDMsCiJtYXBwaW5ncyI6ICJBQUFBLElBQUs7RUFDSCxnQkFBZ0IsRUFBRSxLQUFLO0VBQ3ZCLFNBQU87SUFDTCxLQUFLLEVBQUUsS0FBSyIsCiJzb3VyY2VzIjogWyJ0ZXN0LnNjc3MiXSwKInNvdXJjZXNDb250ZW50IjogWyJib2R5IHtcbiAgYmFja2dyb3VuZC1jb2xvcjogYmxhY2s7XG4gICYgPiBoMSB7XG4gICAgY29sb3I6IHdoaXRlO1xuICB9XG59XG4iXSwKIm5hbWVzIjogW10sCiJmaWxlIjogInRlc3QuY3NzIgp9Cg== */</style>
+</head>
+<body>
+ <h1>Source maps testcase</div>
+</body>
+</html>
diff --git a/devtools/client/styleeditor/test/sourcemaps-large.html b/devtools/client/styleeditor/test/sourcemaps-large.html
new file mode 100644
index 000000000..b8c92e0c9
--- /dev/null
+++ b/devtools/client/styleeditor/test/sourcemaps-large.html
@@ -0,0 +1,11 @@
+<!doctype html>
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>testcase for a loading error with CSS source maps</title>
+ <link rel="stylesheet" type="text/css" href="sourcemap-css/test-bootstrap-scss.css"/>
+</head>
+<body>
+ <div>source maps <span>testcase</span> (see Bug 1128747)</div>
+</body>
+</html>
diff --git a/devtools/client/styleeditor/test/sourcemaps-watching.html b/devtools/client/styleeditor/test/sourcemaps-watching.html
new file mode 100644
index 000000000..fc9909ea5
--- /dev/null
+++ b/devtools/client/styleeditor/test/sourcemaps-watching.html
@@ -0,0 +1,11 @@
+<!doctype html>
+<html>
+<head>
+ <title>testcase for testing CSS source maps</title>
+ <link rel="stylesheet" type="text/css" href="simple.css"/>
+ <link rel="stylesheet" type="text/css" href="sourcemap-css/sourcemaps.css?test=1"/>
+</head>
+<body>
+ <div>source maps <span>testcase</span></div>
+</body>
+</html>
diff --git a/devtools/client/styleeditor/test/sourcemaps.html b/devtools/client/styleeditor/test/sourcemaps.html
new file mode 100644
index 000000000..887e0ed98
--- /dev/null
+++ b/devtools/client/styleeditor/test/sourcemaps.html
@@ -0,0 +1,13 @@
+<!doctype html>
+<html>
+<head>
+ <title>testcase for testing CSS source maps</title>
+ <link rel="stylesheet" type="text/css" href="simple.css"/>
+ <link rel="stylesheet" type="text/css" href="sourcemap-css/sourcemaps.css?test=1"/>
+ <link rel="stylesheet" type="text/css" href="sourcemap-css/contained.css"/>
+ <link rel="stylesheet" type="text/css" href="sourcemap-css/test-stylus.css"/>
+</head>
+<body>
+ <div>source maps <span>testcase</span></div>
+</body>
+</html>
diff --git a/devtools/client/styleeditor/test/sync.html b/devtools/client/styleeditor/test/sync.html
new file mode 100644
index 000000000..83da8c57e
--- /dev/null
+++ b/devtools/client/styleeditor/test/sync.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>simple testcase</title>
+ <style type="text/css">
+ body {
+ border-width: 15px;
+ color: red;
+ }
+
+ #testid {
+ font-size: 4em;
+ }
+ </style>
+</head>
+<body>
+ <div id="testid">simple testcase</div>
+</body>
+</html>
diff --git a/devtools/client/styleeditor/test/test_private.css b/devtools/client/styleeditor/test/test_private.css
new file mode 100644
index 000000000..438954d36
--- /dev/null
+++ b/devtools/client/styleeditor/test/test_private.css
@@ -0,0 +1,3 @@
+body {
+ background-color: red;
+} \ No newline at end of file
diff --git a/devtools/client/styleeditor/test/test_private.html b/devtools/client/styleeditor/test/test_private.html
new file mode 100644
index 000000000..bfde3520e
--- /dev/null
+++ b/devtools/client/styleeditor/test/test_private.html
@@ -0,0 +1,7 @@
+<html>
+<head>
+<link rel="stylesheet" href="test_private.css"></link>
+</head>
+<body>
+</body>
+</html>
diff --git a/devtools/client/styleeditor/test/utf-16.css b/devtools/client/styleeditor/test/utf-16.css
new file mode 100644
index 000000000..92ff5eac5
--- /dev/null
+++ b/devtools/client/styleeditor/test/utf-16.css
Binary files differ
diff --git a/devtools/client/styleeditor/utils.js b/devtools/client/styleeditor/utils.js
new file mode 100644
index 000000000..6cb1aa8cc
--- /dev/null
+++ b/devtools/client/styleeditor/utils.js
@@ -0,0 +1,40 @@
+/* -*- 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 Services = require("Services");
+const EventEmitter = require("devtools/shared/event-emitter");
+
+exports.PREF_ORIG_SOURCES = "devtools.styleeditor.source-maps-enabled";
+
+/**
+ * A PreferenceObserver observes a pref branch for pref changes.
+ * It emits an event for each preference change.
+ */
+function PrefObserver(branchName) {
+ this.branchName = branchName;
+ this.branch = Services.prefs.getBranch(branchName);
+ this.branch.addObserver("", this, false);
+
+ EventEmitter.decorate(this);
+}
+
+exports.PrefObserver = PrefObserver;
+
+PrefObserver.prototype = {
+ observe: function (subject, topic, data) {
+ if (topic == "nsPref:changed") {
+ this.emit(this.branchName + data);
+ }
+ },
+
+ destroy: function () {
+ if (this.branch) {
+ this.branch.removeObserver("", this);
+ }
+ }
+};
diff --git a/devtools/client/themes/animationinspector.css b/devtools/client/themes/animationinspector.css
new file mode 100644
index 000000000..6a17ca02f
--- /dev/null
+++ b/devtools/client/themes/animationinspector.css
@@ -0,0 +1,623 @@
+/* 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/. */
+
+/* Animation-inspector specific theme variables */
+
+.theme-dark {
+ --even-animation-timeline-background-color: rgba(255,255,255,0.03);
+ --command-pick-image: url(chrome://devtools/skin/images/command-pick.svg);
+ --pause-image: url(chrome://devtools/skin/images/pause.svg);
+ --rewind-image: url(chrome://devtools/skin/images/rewind.svg);
+ --play-image: url(chrome://devtools/skin/images/play.svg);
+}
+
+.theme-light {
+ --even-animation-timeline-background-color: rgba(128,128,128,0.03);
+ --command-pick-image: url(chrome://devtools/skin/images/command-pick.svg);
+ --pause-image: url(chrome://devtools/skin/images/pause.svg);
+ --rewind-image: url(chrome://devtools/skin/images/rewind.svg);
+ --play-image: url(chrome://devtools/skin/images/play.svg);
+}
+
+.theme-firebug {
+ --even-animation-timeline-background-color: rgba(128,128,128,0.03);
+ --command-pick-image: url(chrome://devtools/skin/images/firebug/command-pick.svg);
+ --pause-image: url(chrome://devtools/skin/images/firebug/pause.svg);
+ --rewind-image: url(chrome://devtools/skin/images/firebug/rewind.svg);
+ --play-image: url(chrome://devtools/skin/images/firebug/play.svg);
+}
+
+:root {
+ /* How high should toolbars be */
+ --toolbar-height: 20px;
+ /* How wide should the sidebar be (should be wide enough to contain long
+ property names like 'border-bottom-right-radius' without ellipsis) */
+ --timeline-sidebar-width: 200px;
+ /* How high should animations displayed in the timeline be */
+ --timeline-animation-height: 20px;
+ /* The size of a keyframe marker in the keyframes diagram */
+ --keyframes-marker-size: 10px;
+ /* The color of the time graduation borders */
+ --time-graduation-border-color: rgba(128, 136, 144, .5);
+}
+
+.animation {
+ --timeline-border-color: var(--theme-body-color);
+ --timeline-background-color: var(--theme-splitter-color);
+ /* The color of the endDelay hidden progress */
+ --enddelay-hidden-progress-color: var(--theme-graphs-grey);
+ /* The color of none fill mode */
+ --fill-none-color: var(--theme-highlight-gray);
+ /* The color of enable fill mode */
+ --fill-enable-color: var(--timeline-border-color);
+}
+
+.animation.cssanimation {
+ --timeline-border-color: var(--theme-highlight-lightorange);
+ --timeline-background-color: var(--theme-contrast-background);
+}
+
+.animation.csstransition {
+ --timeline-border-color: var(--theme-highlight-bluegrey);
+ --timeline-background-color: var(--theme-highlight-blue);
+}
+
+.animation.scriptanimation {
+ --timeline-border-color: var(--theme-highlight-green);
+ --timeline-background-color: var(--theme-graphs-green);
+}
+
+html {
+ height: 100%;
+}
+
+body {
+ margin: 0;
+ padding: 0;
+ display : flex;
+ flex-direction: column;
+ height: 100%;
+ overflow: hidden;
+ color: var(--theme-content-color3);
+}
+
+/* The top toolbar, containing the toggle-all button. And the timeline toolbar,
+ containing playback control buttons, shown only when there are animations
+ displayed in the timeline */
+
+#global-toolbar,
+#timeline-toolbar {
+ border-bottom: 1px solid var(--theme-splitter-color);
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: flex-end;
+ height: var(--toolbar-height);
+}
+
+#timeline-toolbar {
+ display: none;
+ justify-content: flex-start;
+}
+
+[timeline] #global-toolbar {
+ display: none;
+}
+
+[timeline] #timeline-toolbar {
+ display: flex;
+}
+
+/* The main animations container */
+
+#sidebar-panel-animationinspector {
+ height: 100%;
+ width: 100%;
+}
+
+#players {
+ height: calc(100% - var(--toolbar-height));
+ overflow-x: hidden;
+ overflow-y: auto;
+}
+
+[empty] #players {
+ display: none;
+}
+
+/* The error message, shown when an invalid/unanimated element is selected */
+
+#error-message {
+ padding-top: 10%;
+ text-align: center;
+ flex: 1;
+ overflow: auto;
+
+ /* The error message is hidden by default */
+ display: none;
+}
+
+[empty] #error-message {
+ display: block;
+}
+
+/* Element picker, toggle-all buttons, timeline pause button, ... */
+
+#global-toolbar > *,
+#timeline-toolbar > * {
+ min-height: var(--toolbar-height);
+ border-color: var(--theme-splitter-color);
+ border-width: 0 0 0 1px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+#global-toolbar .label,
+#timeline-toolbar .label {
+ padding: 0 5px;
+ border-style: solid;
+}
+
+#global-toolbar .devtools-button,
+#timeline-toolbar .devtools-button {
+ margin: 0;
+ padding: 0;
+}
+
+#timeline-toolbar .devtools-button,
+#timeline-toolbar .label {
+ border-width: 0 1px 0 0;
+}
+
+#element-picker::before {
+ background-image: var(--command-pick-image);
+}
+
+.pause-button::before {
+ background-image: var(--pause-image);
+}
+
+#rewind-timeline::before {
+ background-image: var(--rewind-image);
+}
+
+.pause-button.paused::before {
+ background-image: var(--play-image);
+}
+
+#timeline-rate select.devtools-button {
+ -moz-appearance: none;
+ text-align: center;
+ font-family: inherit;
+ color: var(--theme-body-color);
+ font-size: 1em;
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-image: url("chrome://devtools/skin/images/dropmarker.svg");
+ background-repeat: no-repeat;
+ background-position: calc(100% - 4px) center;
+ padding-right: 1em;
+}
+
+#timeline-rate {
+ position: relative;
+ width: 4.5em;
+}
+
+/* Animation timeline component */
+
+.animation-timeline {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+}
+
+/* Useful for positioning animations or keyframes in the timeline */
+.animation-timeline .track-container {
+ position: absolute;
+ top: 0;
+ left: var(--timeline-sidebar-width);
+ /* Leave the width of a marker right of a track so the 100% markers can be
+ selected easily */
+ right: var(--keyframes-marker-size);
+ height: var(--timeline-animation-height);
+}
+
+.animation-timeline .scrubber-wrapper {
+ position: absolute;
+ left: var(--timeline-sidebar-width);
+ /* Leave the width of a marker right of a track so the 100% markers can be
+ selected easily */
+ right: var(--keyframes-marker-size);
+ height: 100%;
+}
+
+.animation-timeline .scrubber {
+ z-index: 5;
+ pointer-events: none;
+ position: absolute;
+ /* Make the scrubber as tall as the viewport minus the toolbar height and the
+ header-wrapper's borders */
+ height: calc(100vh - var(--toolbar-height) - 1px);
+ min-height: 100%;
+ width: 0;
+ border-right: 1px solid red;
+ box-sizing: border-box;
+}
+
+/* The scrubber handle is a transparent element displayed on top of the scrubber
+ line that allows users to drag it */
+.animation-timeline .scrubber .scrubber-handle {
+ position: absolute;
+ height: 100%;
+ /* Make it thick enough for easy dragging */
+ width: 6px;
+ right: -1.5px;
+ cursor: col-resize;
+ pointer-events: all;
+}
+
+.animation-timeline .scrubber .scrubber-handle::before {
+ content: "";
+ position: sticky;
+ top: 0;
+ width: 1px;
+ border-top: 5px solid red;
+ border-left: 5px solid transparent;
+ border-right: 5px solid transparent;
+}
+
+.animation-timeline .time-header {
+ min-height: var(--timeline-animation-height);
+ cursor: col-resize;
+ -moz-user-select: none;
+}
+
+.animation-timeline .time-header .header-item {
+ position: absolute;
+ height: 100%;
+ padding-top: 3px;
+ border-left: 0.5px solid var(--time-graduation-border-color);
+}
+
+.animation-timeline .header-wrapper {
+ position: sticky;
+ top: 0;
+ background-color: var(--theme-body-background);
+ border-bottom: 1px solid var(--time-graduation-border-color);
+ z-index: 3;
+ height: var(--timeline-animation-height);
+ overflow: hidden;
+}
+
+.animation-timeline .time-body {
+ height: 100%;
+}
+
+.animation-timeline .time-body .time-tick {
+ -moz-user-select: none;
+ position: absolute;
+ width: 0;
+ /* When scroll bar is shown, make it covers entire time-body */
+ height: 100%;
+ /* When scroll bar is hidden, make it as tall as the viewport minus the
+ timeline animation height and the header-wrapper's borders */
+ min-height: calc(100vh - var(--timeline-animation-height) - 1px);
+ border-left: 0.5px solid var(--time-graduation-border-color);
+}
+
+.animation-timeline .animations {
+ width: 100%;
+ height: 100%;
+ padding: 0;
+ list-style-type: none;
+ margin-top: 0;
+}
+
+/* Animation block widgets */
+
+.animation-timeline .animation {
+ margin: 2px 0;
+ height: var(--timeline-animation-height);
+ position: relative;
+}
+
+/* We want animations' background colors to alternate, but each animation has
+ a sibling (hidden by default) that contains the animated properties and
+ keyframes, so we need to alternate every 4 elements. */
+.animation-timeline .animation:nth-child(4n+1) {
+ background-color: var(--even-animation-timeline-background-color);
+}
+
+.animation-timeline .animation .target {
+ width: var(--timeline-sidebar-width);
+ height: 100%;
+ overflow: hidden;
+ display: flex;
+ align-items: center;
+}
+
+.animation-timeline .animation-target {
+ background-color: transparent;
+}
+
+.animation-timeline .animation .time-block {
+ cursor: pointer;
+}
+
+/* Animation summary graph */
+.animation-timeline .animation .summary {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+}
+
+.animation-timeline .animation .summary path {
+ fill: var(--timeline-background-color);
+ stroke: var(--timeline-border-color);
+}
+
+.animation-timeline .animation .summary .infinity.copied {
+ opacity: .3;
+}
+
+.animation-timeline .animation .summary path.delay-path.negative,
+.animation-timeline .animation .summary path.enddelay-path.negative {
+ fill: none;
+ stroke: var(--enddelay-hidden-progress-color);
+ stroke-dasharray: 2, 2;
+}
+
+.animation-timeline .animation .name {
+ position: absolute;
+ color: var(--theme-content-color3);
+ top: 0px;
+ left: 0px;
+ height: 100%;
+ width: 100%;
+ display: flex;
+ align-items: center;
+ padding: 0 2px;
+ box-sizing: border-box;
+ --fast-track-icon-width: 15px;
+ z-index: 1;
+}
+
+.animation-timeline .animation .name div {
+ /* Flex items don't support text-overflow, so a child div is used */
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ background-color: rgba(255, 255, 255, 0.7);
+ max-width: 50%;
+}
+
+.animation-timeline .fast-track .name::after {
+ /* Animations running on the compositor have the fast-track background image*/
+ content: "";
+ display: block;
+ position: absolute;
+ top: 1px;
+ right: 0;
+ height: 100%;
+ width: var(--fast-track-icon-width);
+ z-index: 1;
+}
+
+.animation-timeline .all-properties .name::after {
+ background-color: var(--theme-content-color3);
+ clip-path: url(images/animation-fast-track.svg#thunderbolt);
+ background-repeat: no-repeat;
+ background-position: center;
+}
+
+.animation-timeline .some-properties .name::after {
+ background-color: var(--theme-content-color3);
+ clip-path: url(images/animation-fast-track.svg#thunderbolt);
+ background-repeat: no-repeat;
+ background-position: center;
+}
+
+.animation-timeline .animation .delay,
+.animation-timeline .animation .end-delay {
+ position: absolute;
+ border-bottom: 3px solid var(--fill-none-color);
+ bottom: -0.5px;
+}
+
+.animation-timeline .animation .delay::after,
+.animation-timeline .animation .end-delay::after {
+ content: "";
+ position: absolute;
+ top: -2px;
+ width: 3px;
+ height: 3px;
+ border: 2px solid var(--fill-none-color);
+ background-color: var(--fill-none-color);
+ border-radius: 50%;
+}
+
+.animation-timeline .animation .negative.delay::after,
+.animation-timeline .animation .positive.end-delay::after {
+ right: -3px;
+}
+
+.animation-timeline .animation .positive.delay::after,
+.animation-timeline .animation .negative.end-delay::after {
+ left: -3px;
+}
+
+.animation-timeline .animation .fill.delay,
+.animation-timeline .animation .fill.end-delay {
+ border-color: var(--fill-enable-color);
+}
+
+.animation-timeline .animation .fill.delay::after,
+.animation-timeline .animation .fill.end-delay::after {
+ border-color: var(--fill-enable-color);
+ background-color: var(--fill-enable-color);
+}
+
+/* Animation target node gutter, contains a preview of the dom node */
+
+.animation-target {
+ background-color: var(--theme-toolbar-background);
+ padding: 0 4px;
+ box-sizing: border-box;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ cursor: pointer;
+}
+
+.animation-target .attribute-name {
+ padding-left: 4px;
+}
+
+.animation-target .node-highlighter {
+ background: url("chrome://devtools/skin/images/vview-open-inspector.png") no-repeat 0 0;
+ padding-left: 16px;
+ margin-right: 5px;
+ cursor: pointer;
+}
+
+.animation-target .node-highlighter:hover {
+ filter: url(images/filters.svg#checked-icon-state);
+}
+
+.animation-target .node-highlighter:active,
+.animation-target .node-highlighter.selected {
+ filter: url(images/filters.svg#checked-icon-state) brightness(0.9);
+}
+
+/* Inline keyframes info in the timeline */
+
+.animation-timeline .animated-properties:not(.selected) {
+ display: none;
+}
+
+.animation-timeline .animated-properties {
+ background-color: var(--theme-selection-background-semitransparent);
+}
+
+.animation-timeline .animated-properties ul {
+ margin: 0;
+ padding: 0;
+ list-style-type: none;
+}
+
+.animation-timeline .animated-properties .property {
+ height: var(--timeline-animation-height);
+ position: relative;
+}
+
+.animation-timeline .animated-properties .property:nth-child(2n) {
+ background-color: var(--even-animation-timeline-background-color);
+}
+
+.animation-timeline .animated-properties .name {
+ width: var(--timeline-sidebar-width);
+ padding-right: var(--keyframes-marker-size);
+ box-sizing: border-box;
+ height: 100%;
+ color: var(--theme-body-color-alt);
+ white-space: nowrap;
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+}
+
+.animation-timeline .animated-properties .name div {
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.animated-properties.cssanimation {
+ --background-color: var(--theme-contrast-background);
+}
+
+.animated-properties.csstransition {
+ --background-color: var(--theme-highlight-blue);
+}
+
+.animated-properties.scriptanimation {
+ --background-color: var(--theme-graphs-green);
+}
+
+.animation-timeline .animated-properties .oncompositor::before {
+ content: "";
+ display: inline-block;
+ width: 17px;
+ height: 17px;
+ background-color: var(--background-color);
+ clip-path: url(images/animation-fast-track.svg#thunderbolt);
+ vertical-align: middle;
+}
+
+.animation-timeline .animated-properties .warning {
+ text-decoration: underline dotted;
+}
+
+.animation-timeline .animated-properties .frames {
+ /* The frames list is absolutely positioned and the left and width properties
+ are dynamically set from javascript to match the animation's startTime and
+ duration */
+ position: absolute;
+ top: 0;
+ height: 100%;
+ /* Using flexbox to vertically center the frames */
+ display: flex;
+ align-items: center;
+}
+
+/* Keyframes diagram, displayed below the timeline, inside the animation-details
+ element. */
+
+.keyframes {
+ /* Actual keyframe markers are positioned absolutely within this container and
+ their position is relative to its size (we know the offset of each frame
+ in percentage) */
+ position: relative;
+ width: 100%;
+ height: 0;
+}
+
+.keyframes.cssanimation {
+ background-color: var(--theme-contrast-background);
+}
+
+.keyframes.csstransition {
+ background-color: var(--theme-highlight-blue);
+}
+
+.keyframes.scriptanimation {
+ background-color: var(--theme-graphs-green);
+}
+
+.keyframes .frame {
+ position: absolute;
+ top: 0;
+ width: 0;
+ height: 0;
+ background-color: inherit;
+ cursor: pointer;
+}
+
+.keyframes .frame::before {
+ content: "";
+ display: block;
+ transform:
+ translateX(calc(var(--keyframes-marker-size) * -.5))
+ /* The extra pixel on the Y axis is so that markers are centered on the
+ horizontal line in the keyframes diagram. */
+ translateY(calc(var(--keyframes-marker-size) * -.5 + 1px));
+ width: var(--keyframes-marker-size);
+ height: var(--keyframes-marker-size);
+ border-radius: 100%;
+ background-color: inherit;
+}
diff --git a/devtools/client/themes/audio/moz.build b/devtools/client/themes/audio/moz.build
new file mode 100644
index 000000000..b68b29b8d
--- /dev/null
+++ b/devtools/client/themes/audio/moz.build
@@ -0,0 +1,9 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'shutter.wav',
+)
diff --git a/devtools/client/themes/audio/shutter.wav b/devtools/client/themes/audio/shutter.wav
new file mode 100644
index 000000000..e9d742913
--- /dev/null
+++ b/devtools/client/themes/audio/shutter.wav
Binary files differ
diff --git a/devtools/client/themes/boxmodel.css b/devtools/client/themes/boxmodel.css
new file mode 100644
index 000000000..5a3289fae
--- /dev/null
+++ b/devtools/client/themes/boxmodel.css
@@ -0,0 +1,258 @@
+/* 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/ */
+
+#boxmodel-wrapper {
+ border-bottom-style: solid;
+ border-bottom-width: 1px;
+ border-color: var(--theme-splitter-color);
+}
+
+#boxmodel-container {
+ /* The view will grow bigger as the window gets resized, until 400px */
+ max-width: 400px;
+ margin: 0px auto;
+ padding: 0;
+}
+
+/* Header */
+
+#boxmodel-header,
+#boxmodel-info {
+ display: flex;
+ align-items: center;
+ padding: 4px 17px;
+}
+
+#layout-geometry-editor {
+ visibility: hidden;
+}
+
+#layout-geometry-editor::before {
+ background: url(images/geometry-editor.svg) no-repeat center center / 16px 16px;
+}
+
+/* Main: contains the box-model regions */
+
+#boxmodel-main {
+ position: relative;
+ box-sizing: border-box;
+ /* The regions are semi-transparent, so the white background is partly
+ visible */
+ background-color: white;
+ color: var(--theme-selection-color);
+ /* Make sure there is some space between the window's edges and the regions */
+ margin: 0 14px 4px 14px;
+ width: calc(100% - 2 * 14px);
+}
+
+.boxmodel-margin,
+.boxmodel-size {
+ color: var(--theme-highlight-blue);
+}
+
+/* Regions are 3 nested elements with wide borders and outlines */
+
+#boxmodel-content {
+ height: 18px;
+}
+
+#boxmodel-margins,
+#boxmodel-borders,
+#boxmodel-padding {
+ border-color: hsla(210,100%,85%,0.2);
+ border-width: 18px;
+ border-style: solid;
+ outline: dotted 1px hsl(210,100%,85%);
+}
+
+#boxmodel-margins {
+ /* This opacity applies to all of the regions, since they are nested */
+ opacity: .8;
+}
+
+/* Regions colors */
+
+#boxmodel-margins {
+ border-color: #edff64;
+}
+
+#boxmodel-borders {
+ border-color: #444444;
+}
+
+#boxmodel-padding {
+ border-color: #6a5acd;
+}
+
+#boxmodel-content {
+ background-color: #87ceeb;
+}
+
+.theme-firebug #boxmodel-main,
+.theme-firebug #boxmodel-borders,
+.theme-firebug #boxmodel-content {
+ border-style: solid;
+}
+
+.theme-firebug #boxmodel-main,
+.theme-firebug #boxmodel-header {
+ font-family: var(--proportional-font-family);
+}
+
+.theme-firebug #boxmodel-main {
+ color: var(--theme-body-color);
+ font-size: var(--theme-toolbar-font-size);
+}
+
+.theme-firebug #boxmodel-header {
+ font-size: var(--theme-toolbar-font-size);
+}
+
+/* Editable region sizes are contained in absolutely positioned <p> */
+
+#boxmodel-main > p {
+ position: absolute;
+ pointer-events: none;
+ margin: 0;
+ text-align: center;
+}
+
+#boxmodel-main > p > span,
+#boxmodel-main > p > input {
+ vertical-align: middle;
+ pointer-events: auto;
+}
+
+/* Coordinates for the region sizes */
+
+.boxmodel-top,
+.boxmodel-bottom {
+ width: calc(100% - 2px);
+ text-align: center;
+}
+
+.boxmodel-padding.boxmodel-top {
+ top: 37px;
+}
+
+.boxmodel-padding.boxmodel-bottom {
+ bottom: 38px;
+}
+
+.boxmodel-border.boxmodel-top {
+ top: 19px;
+}
+
+.boxmodel-border.boxmodel-bottom {
+ bottom: 20px;
+}
+
+.boxmodel-margin.boxmodel-top {
+ top: 1px;
+}
+
+.boxmodel-margin.boxmodel-bottom {
+ bottom: 2px;
+}
+
+.boxmodel-size,
+.boxmodel-margin.boxmodel-left,
+.boxmodel-margin.boxmodel-right,
+.boxmodel-border.boxmodel-left,
+.boxmodel-border.boxmodel-right,
+.boxmodel-padding.boxmodel-left,
+.boxmodel-padding.boxmodel-right {
+ top: 22px;
+ line-height: 80px;
+}
+
+.boxmodel-size {
+ width: calc(100% - 2px);
+}
+
+.boxmodel-margin.boxmodel-right,
+.boxmodel-margin.boxmodel-left,
+.boxmodel-border.boxmodel-left,
+.boxmodel-border.boxmodel-right,
+.boxmodel-padding.boxmodel-right,
+.boxmodel-padding.boxmodel-left {
+ width: 21px;
+}
+
+.boxmodel-padding.boxmodel-left {
+ left: 35px;
+}
+
+.boxmodel-padding.boxmodel-right {
+ right: 35px;
+}
+
+.boxmodel-border.boxmodel-left {
+ left: 16px;
+}
+
+.boxmodel-border.boxmodel-right {
+ right: 17px;
+}
+
+.boxmodel-margin.boxmodel-right {
+ right: 0;
+}
+
+.boxmodel-margin.boxmodel-left {
+ left: 0;
+}
+
+.boxmodel-rotate.boxmodel-left:not(.boxmodel-editing) {
+ transform: rotate(-90deg);
+}
+
+.boxmodel-rotate.boxmodel-right:not(.boxmodel-editing) {
+ transform: rotate(90deg);
+}
+
+/* Legend: displayed inside regions */
+
+.boxmodel-legend {
+ position: absolute;
+ margin: 2px 6px;
+ z-index: 1;
+}
+
+.boxmodel-legend[data-box="margin"] {
+ color: var(--theme-highlight-blue);
+}
+
+/* Editable fields */
+
+.boxmodel-editable {
+ border: 1px dashed transparent;
+ -moz-user-select: text;
+}
+
+.boxmodel-editable:hover {
+ border-bottom-color: hsl(0, 0%, 50%);
+}
+
+.styleinspector-propertyeditor {
+ border: 1px solid #ccc;
+ padding: 0;
+}
+
+/* Make sure the content size doesn't appear as editable like the other sizes */
+
+.boxmodel-size > span {
+ cursor: default;
+}
+
+/* Box Model Info: contains the position and size of the element */
+
+#boxmodel-element-size {
+ flex: 1;
+}
+
+#boxmodel-position-group {
+ display: flex;
+ align-items: center;
+}
diff --git a/devtools/client/themes/canvasdebugger.css b/devtools/client/themes/canvasdebugger.css
new file mode 100644
index 000000000..39162a96f
--- /dev/null
+++ b/devtools/client/themes/canvasdebugger.css
@@ -0,0 +1,353 @@
+/* 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/. */
+
+:root {
+ --gutter-width: 3em;
+ --gutter-padding-start: 22px;
+ --checkerboard-pattern: linear-gradient(45deg, rgba(128,128,128,0.2) 25%, transparent 25%, transparent 75%, rgba(128,128,128,0.2) 75%, rgba(128,128,128,0.2)),
+ linear-gradient(45deg, rgba(128,128,128,0.2) 25%, transparent 25%, transparent 75%, rgba(128,128,128,0.2) 75%, rgba(128,128,128,0.2));
+}
+
+:root.theme-dark {
+ --draw-call-background: rgba(112,191,83,0.15);
+ --interesting-call-background: rgba(223,128,255,0.15);
+}
+
+:root.theme-light {
+ --draw-call-background: rgba(44,187,15,0.1);
+ --interesting-call-background: rgba(184,46,229,0.1);
+}
+
+/* Reload and waiting notices */
+
+.notice-container {
+ margin-top: -50vh;
+ color: var(--theme-body-color-alt);
+}
+
+#empty-notice > button {
+ min-width: 30px;
+ min-height: 28px;
+ margin: 0;
+ list-style-image: url(images/profiler-stopwatch.svg);
+}
+
+#empty-notice > button .button-text {
+ display: none;
+}
+
+#waiting-notice {
+ font-size: 110%;
+}
+
+/* Snapshots pane */
+
+#snapshots-pane {
+ border-inline-end: 1px solid var(--theme-splitter-color);
+}
+
+#record-snapshot {
+ list-style-image: url("chrome://devtools/skin/images/profiler-stopwatch.svg");
+}
+
+#import-snapshot {
+ list-style-image: url("images/import.svg");
+}
+
+/* Snapshots items */
+
+.snapshot-item-thumbnail {
+ image-rendering: -moz-crisp-edges;
+ background-image: var(--checkerboard-pattern);
+ background-size: 12px 12px, 12px 12px;
+ background-position: 0px 0px, 6px 6px;
+ background-repeat: repeat, repeat;
+}
+
+.snapshot-item-thumbnail[flipped=true] {
+ transform: scaleY(-1);
+}
+
+.snapshot-item-thumbnail {
+ background-color: var(--theme-body-background);
+}
+
+.snapshot-item-details {
+ padding-inline-start: 6px;
+}
+
+.snapshot-item-calls {
+ padding-top: 4px;
+ font-size: 80%;
+}
+
+.snapshot-item-save {
+ padding-bottom: 2px;
+ font-size: 90%;
+}
+
+.snapshot-item-calls,
+.snapshot-item-save {
+ color: var(--theme-body-color-alt);
+}
+
+.snapshot-item-save {
+ text-decoration: underline;
+ cursor: pointer;
+}
+
+.snapshot-item-save[disabled=true] {
+ text-decoration: none;
+ pointer-events: none;
+}
+
+.snapshot-item-footer.devtools-throbber::before {
+ margin-top: -2px;
+}
+
+#snapshots-list .selected label {
+ /* Text inside a selected item should not be custom colored. */
+ color: inherit !important;
+}
+
+/* Debugging pane controls */
+#resume {
+ list-style-image: url(images/play.svg);
+}
+
+#step-over {
+ list-style-image: url(images/debugger-step-over.svg);
+}
+
+#step-in {
+ list-style-image: url(images/debugger-step-in.svg);
+}
+
+#step-out {
+ list-style-image: url(images/debugger-step-out.svg);
+}
+
+#calls-slider {
+ padding-inline-end: 24px;
+}
+
+#calls-slider .scale-slider {
+ margin: 0;
+}
+
+#debugging-toolbar-sizer-button {
+ /* This button's only purpose in life is to make the
+ container .devtools-toolbar have the right height. */
+ visibility: hidden;
+ min-width: 1px;
+}
+
+/* Calls list pane */
+
+#calls-list .side-menu-widget-container {
+ background: transparent;
+}
+
+/* Calls list items */
+
+#calls-list .side-menu-widget-item {
+ padding: 0;
+ border-color: var(--theme-splitter-color);
+ border-bottom-color: transparent;
+}
+
+.call-item-view:hover {
+ background-color: rgba(128,128,128,0.05);
+}
+
+.call-item-view[draw-call] {
+ background-color: var(--draw-call-background);
+}
+
+.call-item-view[interesting-call] {
+ background-color: var(--interesting-call-background);
+}
+
+.call-item-gutter {
+ width: calc(var(--gutter-width) + var(--gutter-padding-start));
+ padding-inline-start: var(--gutter-padding-start);
+ padding-inline-end: 4px;
+ padding-top: 2px;
+ padding-bottom: 2px;
+ border-inline-end: 1px solid var(--theme-splitter-color);
+ margin-inline-end: 6px;
+ background-color: var(--theme-sidebar-background);
+ color: var(--theme-content-color3);
+}
+
+.selected .call-item-gutter {
+ background-color: #2cbb0f;
+ color: white;
+}
+
+.call-item-index {
+ text-align: end;
+}
+
+.call-item-context {
+ color: var(--theme-highlight-orange);
+}
+
+.call-item-name {
+ color: var(--theme-highlight-blue);
+}
+
+.call-item-location {
+ padding-inline-start: 2px;
+ padding-inline-end: 6px;
+ text-align: end;
+ cursor: pointer;
+ color: var(--theme-highlight-bluegrey);
+ border-color: var(--theme-splitter-color);
+}
+
+.call-item-location:hover {
+ color: var(--theme-highlight-blue);
+}
+
+.call-item-view:hover .call-item-location,
+.call-item-view[expanded] .call-item-location {
+ text-decoration: underline;
+}
+
+.call-item-stack {
+ padding-inline-start: calc(var(--gutter-width) + var(--gutter-padding-start));
+ padding-bottom: 10px;
+}
+
+.theme-dark .call-item-stack {
+ background: rgba(0,0,0,0.9);
+}
+
+.theme-light .call-item-stack {
+ background: rgba(255,255,255,0.9);
+}
+
+.call-item-stack-fn {
+ padding-top: 2px;
+ padding-bottom: 2px;
+}
+
+.call-item-stack-fn-location {
+ padding-inline-start: 2px;
+ padding-inline-end: 6px;
+ text-align: end;
+ cursor: pointer;
+ text-decoration: underline;
+}
+
+.call-item-stack-fn-name {
+ color: var(--theme-content-color3);
+}
+
+.call-item-stack-fn-location {
+ color: var(--theme-highlight-bluegrey);
+}
+
+.call-item-stack-fn-location:hover {
+ color: var(--theme-highlight-blue);
+}
+
+#calls-list .selected .call-item-contents > label:not(.call-item-gutter) {
+ /* Text inside a selected item should not be custom colored. */
+ color: inherit !important;
+}
+
+/* Rendering preview */
+
+#screenshot-container {
+ background-color: var(--theme-body-background);
+ background-image: var(--checkerboard-pattern);
+ background-size: 30px 30px, 30px 30px;
+ background-position: 0px 0px, 15px 15px;
+ background-repeat: repeat, repeat;
+}
+
+@media (min-width: 701px) {
+ #screenshot-container {
+ width: 30vw;
+ max-width: 50vw;
+ min-width: 100px;
+ }
+}
+
+@media (max-width: 700px) {
+ #screenshot-container {
+ height: 40vh;
+ max-height: 70vh;
+ min-height: 100px;
+ }
+}
+
+#screenshot-image {
+ background-image: -moz-element(#screenshot-rendering);
+ background-size: contain;
+ background-position: center, center;
+ background-repeat: no-repeat;
+}
+
+#screenshot-image[flipped=true] {
+ transform: scaleY(-1);
+}
+
+#screenshot-dimensions {
+ padding-top: 4px;
+ padding-bottom: 4px;
+ text-align: center;
+}
+
+.theme-dark #screenshot-dimensions {
+ background-color: rgba(0,0,0,0.4);
+}
+
+.theme-light #screenshot-dimensions {
+ background-color: rgba(255,255,255,0.8);
+}
+
+/* Snapshot filmstrip */
+
+#snapshot-filmstrip {
+ border-top: 1px solid var(--theme-splitter-color);
+ overflow: hidden;
+}
+
+.theme-dark #snapshot-filmstrip {
+ color: var(--theme-selection-color);
+}
+
+.theme-light #snapshot-filmstrip {
+ color: var(--theme-body-color-alt);
+}
+
+.filmstrip-thumbnail {
+ image-rendering: -moz-crisp-edges;
+ background-color: var(--theme-body-background);
+ background-image: var(--checkerboard-pattern);
+ background-size: 12px 12px, 12px 12px;
+ background-position: 0px -1px, 6px 5px;
+ background-repeat: repeat, repeat;
+ background-origin: content-box;
+ cursor: pointer;
+ padding-top: 1px;
+ padding-bottom: 1px;
+ border-inline-end: 1px solid var(--theme-splitter-color);
+ transition: opacity 0.1s ease-in-out;
+}
+
+.filmstrip-thumbnail[flipped=true] {
+ transform: scaleY(-1);
+}
+
+#snapshot-filmstrip > .filmstrip-thumbnail:hover,
+#snapshot-filmstrip:not(:hover) > .filmstrip-thumbnail[highlighted] {
+ border: 1px solid var(--theme-highlight-blue);
+ margin: 0 0 0 -1px;
+ padding: 0;
+ opacity: 0.66;
+}
diff --git a/devtools/client/themes/commandline.css b/devtools/client/themes/commandline.css
new file mode 100644
index 000000000..9dce964ed
--- /dev/null
+++ b/devtools/client/themes/commandline.css
@@ -0,0 +1,172 @@
+/* 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/. */
+
+
+/* NOTE: THESE NEED TO STAY IN SYNC WITH LIGHT-THEME.CSS AND DARK-THEME.CSS.
+ We are copy/pasting variables from light-theme and dark-theme,
+ since they aren't loaded in this context (within commandlineoutput.xhtml
+ and commandlinetooltip.xhtml). */
+:root[devtoolstheme="light"] {
+ --gcli-background-color: #fcfcfc; /* --theme-tab-toolbar-background */
+ --gcli-input-focused-background: #ffffff; /* --theme-sidebar-background */
+ --gcli-input-color: #393f4c; /* --theme-body-color */
+ --gcli-border-color: #dde1e4; /* --theme-splitter-color */
+}
+
+:root[devtoolstheme="dark"] {
+ --gcli-background-color: #272b35; /* --theme-toolbar-background */
+ --gcli-input-focused-background: #272b35; /* --theme-tab-toolbar-background */
+ --gcli-input-color: #b6babf; /* --theme-body-color-alt */
+ --gcli-border-color: #454d5d; /* --theme-splitter-color */
+}
+
+.gcli-body {
+ margin: 0;
+ font: message-box;
+ color: var(--gcli-input-color);
+}
+
+#gcli-output-root,
+#gcli-tooltip-root {
+ border: 1px solid var(--gcli-border-color);
+ border-radius: 3px;
+ background-color: var(--gcli-background-color);
+}
+
+#gcli-output-root {
+ padding: 5px 10px;
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+ border-bottom: 0;
+}
+
+#gcli-tooltip-root {
+ padding: 5px 0px;
+}
+
+#gcli-tooltip-connector {
+ margin-top: -1px;
+ margin-left: 8px;
+ width: 20px;
+ height: 10px;
+ border-left: 1px solid var(--gcli-border-color);
+ border-right: 1px solid var(--gcli-border-color);
+ background-color: var(--gcli-background-color);
+}
+
+.gcli-tt-description,
+.gcli-tt-error {
+ padding: 0 10px;
+}
+
+.gcli-row-out {
+ padding: 0 5px;
+ line-height: 1.2em;
+ border-top: none;
+ border-bottom: none;
+ color: var(--gcli-input-color);
+}
+
+.gcli-row-out p,
+.gcli-row-out h1,
+.gcli-row-out h2,
+.gcli-row-out h3 {
+ margin: 5px 0;
+}
+
+.gcli-row-out h1,
+.gcli-row-out h2,
+.gcli-row-out h3,
+.gcli-row-out h4,
+.gcli-row-out h5,
+.gcli-row-out th,
+.gcli-row-out strong,
+.gcli-row-out pre {
+ color: var(--gcli-input-color);
+}
+
+.gcli-row-out pre {
+ font-size: 80%;
+}
+
+.gcli-row-out td {
+ white-space: nowrap;
+}
+
+.gcli-out-shortcut,
+.gcli-help-synopsis {
+ padding: 0 3px;
+ margin: 0 4px;
+ font-weight: normal;
+ font-size: 90%;
+ border-radius: 3px;
+ background-color: var(--gcli-background-color);
+ border: 1px solid var(--gcli-border-color);
+}
+
+.gcli-out-shortcut:before,
+.gcli-help-synopsis:before {
+ color: var(--gcli-input-color);
+ padding-inline-end: 2px;
+}
+
+.gcli-help-arrow {
+ color: #666;
+}
+
+.gcli-help-description {
+ margin: 0 20px;
+ padding: 0;
+}
+
+.gcli-help-parameter {
+ margin: 0 30px;
+ padding: 0;
+}
+
+.gcli-help-header {
+ margin: 10px 0 6px;
+}
+
+.gcli-menu-name {
+ padding-inline-start: 8px;
+}
+
+.gcli-menu-desc {
+ padding-inline-end: 8px;
+ color: var(--gcli-input-color);
+}
+
+.gcli-menu-name:hover,
+.gcli-menu-desc:hover {
+ background-color: var(--gcli-input-focused-background);
+}
+
+.gcli-menu-highlight,
+.gcli-menu-highlight:hover {
+ background-color: hsla(0,100%,100%,.1);
+}
+
+.gcli-menu-typed {
+ color: hsl(25,78%,50%);
+}
+
+.gcli-menu-more {
+ font-size: 80%;
+ text-align: end;
+ padding-inline-end: 8px;
+}
+
+.gcli-addon-disabled {
+ opacity: 0.6;
+ text-decoration: line-through;
+}
+
+.gcli-breakpoint-label {
+ font-weight: bold;
+}
+
+.gcli-breakpoint-lineText {
+ font-family: monospace;
+}
diff --git a/devtools/client/themes/commandline.inc.css b/devtools/client/themes/commandline.inc.css
new file mode 100644
index 000000000..db78bedf3
--- /dev/null
+++ b/devtools/client/themes/commandline.inc.css
@@ -0,0 +1,217 @@
+%if 0
+/* 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/. */
+%endif
+
+/* Developer toolbar */
+
+/* NOTE: THESE NEED TO STAY IN SYNC WITH LIGHT-THEME.CSS AND DARK-THEME.CSS.
+ We are copy/pasting variables from light-theme and dark-theme,
+ since they aren't loaded in this context (within browser.css). */
+:root[devtoolstheme="light"] #developer-toolbar {
+ --gcli-background-color: #fcfcfc; /* --theme-tab-toolbar-background */
+ --gcli-input-background: #fcfcfc; /* --theme-toolbar-background */
+ --gcli-input-focused-background: #ffffff; /* --theme-sidebar-background */
+ --gcli-input-color: #393f4c; /* --theme-body-color */
+ --gcli-border-color: #dde1e4; /* --theme-splitter-color */
+ --selection-background: #4c9ed9; /* --theme-selection-background */
+ --selection-color: #f5f7fa; /* --theme-selection-color */
+ --command-line-image: url(chrome://devtools/skin/images/commandline-icon.svg#light-theme); /* --theme-command-line-image */
+ --command-line-image-focus: url(chrome://devtools/skin/images/commandline-icon.svg#light-theme-focus); /* --theme-command-line-image-focus */
+}
+
+:root[devtoolstheme="dark"] #developer-toolbar {
+ --gcli-background-color: #272b35; /* --theme-toolbar-background */
+ --gcli-input-background: #272b35; /* --theme-tab-toolbar-background */
+ --gcli-input-focused-background: #272b35; /* --theme-tab-toolbar-background */
+ --gcli-input-color: #b6babf; /* --theme-body-color-alt */
+ --gcli-border-color: #454d5d; /* --theme-splitter-color */
+ --selection-background: #5675b9; /* --theme-selection-background */
+ --selection-color: #f5f7fa; /* --theme-selection-color */
+ --command-line-image: url(chrome://devtools/skin/images/commandline-icon.svg#dark-theme); /* --theme-command-line-image */
+ --command-line-image-focus: url(chrome://devtools/skin/images/commandline-icon.svg#dark-theme-focus); /* --theme-command-line-image-focus */
+}
+
+#developer-toolbar {
+ -moz-appearance: none;
+ padding: 0;
+ min-height: 32px;
+ background-color: var(--gcli-background-color);
+ border-top: 1px solid var(--gcli-border-color);
+}
+
+#developer-toolbar > toolbarbutton {
+ -moz-appearance: none;
+ border: none;
+ background-color: transparent;
+ margin: 0;
+ padding: 0 10px;
+ width: 32px;
+}
+
+.developer-toolbar-button > image {
+ margin: auto 10px;
+}
+
+:root[devtoolstheme="light"] #developer-toolbar > .developer-toolbar-button:not([checked=true]) > image,
+:root[devtoolstheme="light"] .gclitoolbar-input-node:not([focused=true])::before {
+ filter: invert(1);
+}
+
+.developer-toolbar-button > .toolbarbutton-icon {
+ width: 16px;
+ height: 16px;
+}
+
+/* The toolkit close button is low contrast in the dark theme so invert it. */
+:root[devtoolstheme="dark"] #developer-toolbar > .close-icon:not(:hover) > image {
+ filter: invert(1);
+}
+
+#developer-toolbar-toolbox-button {
+ list-style-image: url("chrome://devtools/skin/images/toggle-tools.png");
+ -moz-image-region: rect(0px, 16px, 16px, 0px);
+}
+
+#developer-toolbar-toolbox-button > label {
+ display: none;
+}
+
+#developer-toolbar-toolbox-button:hover {
+ -moz-image-region: rect(0px, 32px, 16px, 16px);
+}
+
+#developer-toolbar-toolbox-button:hover:active {
+ -moz-image-region: rect(0px, 48px, 16px, 32px);
+}
+
+#developer-toolbar-toolbox-button[checked=true] {
+ -moz-image-region: rect(0px, 64px, 16px, 48px);
+}
+
+@media (min-resolution: 1.1dppx) {
+ #developer-toolbar-toolbox-button {
+ list-style-image: url("chrome://devtools/skin/images/toggle-tools@2x.png");
+ -moz-image-region: rect(0px, 32px, 32px, 0px);
+ }
+
+ #developer-toolbar-toolbox-button:hover {
+ -moz-image-region: rect(0px, 64px, 32px, 32px);
+ }
+
+ #developer-toolbar-toolbox-button:hover:active {
+ -moz-image-region: rect(0px, 96px, 32px, 64px);
+ }
+
+ #developer-toolbar-toolbox-button[checked=true] {
+ -moz-image-region: rect(0px, 128px, 32px, 96px);
+ }
+}
+
+/* GCLI */
+
+html|*#gcli-tooltip-frame,
+html|*#gcli-output-frame {
+ padding: 0;
+ border-width: 0;
+ background-color: transparent;
+}
+
+#gcli-output,
+#gcli-tooltip {
+ border-width: 0;
+ background-color: transparent;
+ -moz-appearance: none;
+}
+
+.gclitoolbar-input-node,
+.gclitoolbar-complete-node {
+ margin: 0;
+ -moz-box-align: center;
+ padding-top: 0;
+ padding-bottom: 0;
+ padding-right: 8px;
+ text-shadow: none;
+ box-shadow: none;
+ border-width: 0;
+ background-color: transparent;
+ border-radius: 0;
+}
+
+.gclitoolbar-input-node {
+ -moz-appearance: none;
+ color: var(--gcli-input-color);
+ background-color: var(--gcli-input-background);
+ background-repeat: no-repeat;
+ background-position: 4px center;
+ box-shadow: 1px 0 0 var(--gcli-border-color) inset,
+ -1px 0 0 var(--gcli-border-color) inset;
+
+ line-height: 32px;
+ outline-style: none;
+ padding: 0;
+}
+
+.gclitoolbar-input-node[focused="true"] {
+ background-color: var(--gcli-input-focused-background);
+}
+
+.gclitoolbar-input-node::before {
+ content: "";
+ display: inline-block;
+ -moz-box-ordinal-group: 0;
+ width: 16px;
+ height: 16px;
+ margin: 0 2px;
+ background-image: var(--command-line-image);
+}
+
+.gclitoolbar-input-node[focused="true"]::before {
+ background-image: var(--command-line-image-focus);
+}
+
+.gclitoolbar-input-node > .textbox-input-box > html|*.textbox-input::-moz-selection {
+ background-color: var(--selection-background);
+ color: var(--selection-color);
+ text-shadow: none;
+}
+
+.gclitoolbar-complete-node {
+ padding-left: 21px;
+ background-color: transparent;
+ color: transparent;
+ z-index: 100;
+ pointer-events: none;
+}
+
+.gcli-in-incomplete,
+.gcli-in-error,
+.gcli-in-ontab,
+.gcli-in-todo,
+.gcli-in-closebrace,
+.gcli-in-param,
+.gcli-in-valid {
+ margin: 0;
+ padding: 0;
+}
+
+.gcli-in-incomplete {
+ border-bottom: 2px dotted #999;
+}
+
+.gcli-in-error {
+ border-bottom: 2px dotted #F00;
+}
+
+.gcli-in-ontab {
+ color: hsl(210,0%,35%);
+}
+
+.gcli-in-todo {
+ color: hsl(210,50%,35%);
+}
+
+.gcli-in-closebrace {
+ color: hsl(0,0%,80%);
+}
diff --git a/devtools/client/themes/common.css b/devtools/client/themes/common.css
new file mode 100644
index 000000000..3d713ada7
--- /dev/null
+++ b/devtools/client/themes/common.css
@@ -0,0 +1,791 @@
+/* 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/. */
+
+@import url("resource://devtools/client/themes/splitters.css");
+
+:root {
+ font: message-box;
+}
+
+:root[platform="mac"] {
+ --monospace-font-family: Menlo, monospace;
+}
+
+:root[platform="win"] {
+ --monospace-font-family: Consolas, monospace;
+}
+
+:root[platform="linux"],
+:root.theme-firebug {
+ --monospace-font-family: monospace;
+}
+
+:root.theme-firebug {
+ --proportional-font-family: Lucida Grande, Tahoma, sans-serif;
+}
+
+.devtools-monospace {
+ font-family: var(--monospace-font-family);
+}
+
+:root[platform="linux"] .devtools-monospace {
+ font-size: 80%;
+}
+
+
+/* Autocomplete Popup */
+
+.devtools-autocomplete-popup {
+ box-shadow: 0 1px 0 hsla(209,29%,72%,.25) inset;
+ background-color: transparent;
+ border-radius: 3px;
+ overflow-x: hidden;
+ max-height: 20rem;
+
+ /* Devtools autocompletes display technical english keywords and should be displayed
+ using LTR direction. */
+ direction: ltr !important;
+}
+
+/* Reset list styles. */
+.devtools-autocomplete-popup ul {
+ list-style: none;
+}
+
+.devtools-autocomplete-popup ul,
+.devtools-autocomplete-popup li {
+ margin: 0;
+}
+
+:root[platform="linux"] .devtools-autocomplete-popup {
+ /* Root font size is bigger on Linux, adjust rem-based values. */
+ max-height: 16rem;
+}
+
+.devtools-autocomplete-listbox {
+ -moz-appearance: none !important;
+ background-color: transparent;
+ border-width: 0px !important;
+ margin: 0;
+ padding: 2px;
+}
+
+.devtools-autocomplete-listbox .autocomplete-item {
+ width: 100%;
+ background-color: transparent;
+ border-radius: 4px;
+ padding: 1px 0;
+}
+
+.devtools-autocomplete-listbox .autocomplete-selected {
+ background-color: rgba(0,0,0,0.2);
+}
+
+.devtools-autocomplete-listbox.dark-theme .autocomplete-selected,
+.devtools-autocomplete-listbox.dark-theme .autocomplete-item:hover {
+ background-color: rgba(0,0,0,0.5);
+}
+
+.devtools-autocomplete-listbox.dark-theme .autocomplete-selected > .autocomplete-value,
+.devtools-autocomplete-listbox:focus.dark-theme .autocomplete-selected > .initial-value {
+ color: hsl(208,100%,60%);
+}
+
+.devtools-autocomplete-listbox.dark-theme .autocomplete-selected > span {
+ color: #eee;
+}
+
+.devtools-autocomplete-listbox.dark-theme .autocomplete-item > span {
+ color: #ccc;
+}
+
+.devtools-autocomplete-listbox .autocomplete-item > .initial-value,
+.devtools-autocomplete-listbox .autocomplete-item > .autocomplete-value {
+ margin: 0;
+ padding: 0;
+ cursor: default;
+}
+
+.devtools-autocomplete-listbox .autocomplete-item > .autocomplete-count {
+ text-align: end;
+}
+
+/* Rest of the dark and light theme */
+
+.devtools-autocomplete-popup,
+.theme-dark .CodeMirror-hints,
+.theme-dark .CodeMirror-Tern-tooltip {
+ border: 1px solid hsl(210,11%,10%);
+ background-image: linear-gradient(to bottom, hsla(209,18%,18%,0.9), hsl(210,11%,16%));
+}
+
+.devtools-autocomplete-popup.light-theme,
+.light-theme .CodeMirror-hints,
+.light-theme .CodeMirror-Tern-tooltip {
+ border: 1px solid hsl(210,24%,90%);
+ background-image: linear-gradient(to bottom, hsla(209,18%,100%,0.9), hsl(210,24%,95%));
+}
+
+.devtools-autocomplete-popup.light-theme {
+ box-shadow: 0 1px 0 hsla(209,29%,90%,.25) inset;
+}
+
+.theme-firebug .devtools-autocomplete-popup {
+ border-color: var(--theme-splitter-color);
+ border-radius: 5px;
+ font-size: var(--theme-autompletion-font-size);
+}
+
+.devtools-autocomplete-popup.firebug-theme {
+ background: var(--theme-body-background);
+}
+
+.devtools-autocomplete-listbox.firebug-theme .autocomplete-selected,
+.devtools-autocomplete-listbox.firebug-theme .autocomplete-item:hover,
+.devtools-autocomplete-listbox.light-theme .autocomplete-selected,
+.devtools-autocomplete-listbox.light-theme .autocomplete-item:hover {
+ background-color: rgba(128,128,128,0.3);
+}
+
+.devtools-autocomplete-listbox.firebug-theme .autocomplete-selected > .autocomplete-value,
+.devtools-autocomplete-listbox:focus.firebug-theme .autocomplete-selected > .initial-value,
+.devtools-autocomplete-listbox.light-theme .autocomplete-selected > .autocomplete-value,
+.devtools-autocomplete-listbox:focus.light-theme .autocomplete-selected > .initial-value {
+ color: #222;
+}
+
+.devtools-autocomplete-listbox.firebug-theme .autocomplete-item > span,
+.devtools-autocomplete-listbox.light-theme .autocomplete-item > span {
+ color: #666;
+}
+
+/* Autocomplete list clone used for accessibility. */
+
+.devtools-autocomplete-list-aria-clone {
+ /* Cannot use display:none or visibility:hidden : screen readers ignore the element. */
+ position: fixed;
+ overflow: hidden;
+ margin: 0;
+ width: 0;
+ height: 0;
+}
+
+.devtools-autocomplete-list-aria-clone li {
+ /* Prevent screen readers from prefacing every item with 'bullet'. */
+ list-style-type: none;
+}
+
+/* links to source code, like displaying `myfile.js:45` */
+
+.devtools-source-link {
+ font-family: var(--monospace-font-family);
+ color: var(--theme-highlight-blue);
+ cursor: pointer;
+ white-space: nowrap;
+ display: flex;
+ text-decoration: none;
+ font-size: 11px;
+ width: 12em; /* probably should be changed for each tool */
+}
+
+.devtools-source-link:hover {
+ text-decoration: underline;
+}
+
+.devtools-source-link > .filename {
+ text-overflow: ellipsis;
+ text-align: end;
+ overflow: hidden;
+ margin: 2px 0px;
+ cursor: pointer;
+}
+
+.devtools-source-link > .line-number {
+ flex: none;
+ margin: 2px 0px;
+ cursor: pointer;
+}
+
+/* Keyboard focus highlight styles */
+
+:-moz-focusring {
+ outline: var(--theme-focus-outline);
+ outline-offset: -1px;
+}
+
+textbox[focused="true"] {
+ border-color: var(--theme-focus-border-color-textbox);
+ box-shadow: var(--theme-focus-box-shadow-textbox);
+ transition: all 0.2s ease-in-out
+}
+
+textbox :-moz-focusring {
+ box-shadow: none;
+ outline: none;
+}
+
+/* Form fields should already have box-shadow hightlight */
+select:-moz-focusring,
+input[type="radio"]:-moz-focusring,
+input[type="checkbox"]:-moz-focusring,
+checkbox:-moz-focusring {
+ outline: none;
+}
+
+/* Toolbar buttons */
+.devtools-menulist,
+.devtools-toolbarbutton,
+.devtools-button {
+ -moz-appearance: none;
+ background: transparent;
+ min-height: 18px;
+ text-shadow: none;
+ border: none;
+ border-radius: 0;
+ color: var(--theme-body-color);
+ transition: background 0.05s ease-in-out;
+}
+
+.devtools-menulist,
+.devtools-toolbarbutton {
+ -moz-box-align: center;
+ min-width: 78px;
+ padding: 1px;
+ margin: 2px 1px;
+}
+
+.devtools-toolbarbutton:not([label]) > .toolbarbutton-icon,
+.devtools-button::before {
+ width: 16px;
+ height: 16px;
+ transition: opacity 0.05s ease-in-out;
+}
+
+/* HTML buttons */
+.devtools-button {
+ margin: 2px 1px;
+ padding: 1px;
+ min-width: 32px;
+ /* The icon is absolutely positioned in the button using ::before */
+ position: relative;
+}
+
+.devtools-button::before {
+ content: "";
+ display: block;
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ margin: -8px 0 0 -8px;
+ background-size: cover;
+ background-repeat: no-repeat;
+ transition: opacity 0.05s ease-in-out;
+}
+
+.devtools-button:-moz-focusring {
+ outline: none;
+}
+
+/* Standalone buttons */
+.devtools-button[standalone],
+.devtools-button[data-standalone],
+.devtools-toolbarbutton[standalone],
+.devtools-toolbarbutton[data-standalone] {
+ border-width: 1px;
+ border-style: solid;
+ min-height: 32px;
+ background-color: var(--theme-toolbar-background);
+}
+
+.devtools-toolbarbutton[standalone], .devtools-toolbarbutton[data-standalone] {
+ margin-inline-end: 5px;
+}
+
+.devtools-toolbarbutton[label][standalone] {
+ min-height: 2em;
+}
+
+.devtools-menulist,
+.devtools-toolbarbutton,
+.devtools-button {
+ border-color: var(--toolbar-button-border-color);
+}
+
+/* Icon button styles */
+.devtools-toolbarbutton:not([label]),
+.devtools-toolbarbutton[text-as-image] {
+ min-width: 32px;
+}
+
+.devtools-toolbarbutton:not([label]) > .toolbarbutton-text {
+ display: none;
+}
+
+.devtools-toolbarbutton > .toolbarbutton-icon {
+ margin: 0;
+}
+
+/* Menu button styles (eg. web console filters) */
+.devtools-toolbarbutton[type=menu-button] > .toolbarbutton-menubutton-button {
+ -moz-appearance: none;
+ color: inherit;
+ border-width: 0;
+ -moz-box-orient: horizontal;
+ padding: 0;
+}
+
+.devtools-toolbarbutton[type=menu-button] {
+ padding: 0 1px;
+ -moz-box-align: stretch;
+}
+
+.devtools-toolbarbutton > .toolbarbutton-menubutton-button > .toolbarbutton-icon {
+ margin-inline-end: 4px;
+}
+
+.devtools-menulist > .menulist-dropmarker {
+ -moz-appearance: none;
+ display: -moz-box;
+ list-style-image: url("chrome://devtools/skin/images/dropmarker.svg");
+ -moz-box-align: center;
+ min-width: 16px;
+}
+
+.devtools-toolbarbutton[type=menu] > .toolbarbutton-menu-dropmarker,
+.devtools-toolbarbutton[type=menu-button] > .toolbarbutton-menubutton-dropmarker {
+ -moz-appearance: none !important;
+ list-style-image: url("chrome://devtools/skin/images/dropmarker.svg");
+ -moz-box-align: center;
+ padding: 0 3px;
+}
+
+/* Icon-only buttons */
+.devtools-button:empty::before,
+.devtools-toolbarbutton:not([label]):not([disabled]) > image {
+ opacity: 0.8;
+}
+
+.devtools-button:hover:empty:not(:disabled):before,
+.devtools-button.checked:empty::before,
+.devtools-button[checked]:empty::before,
+.devtools-button[open]:empty::before,
+.devtools-toolbarbutton:not([label]):not([disabled=true]):hover > image,
+.devtools-toolbarbutton:not([label])[checked=true] > image,
+.devtools-toolbarbutton:not([label])[open=true] > image {
+ opacity: 1;
+}
+
+.devtools-button:disabled,
+.devtools-button[disabled],
+.devtools-toolbarbutton[disabled] {
+ opacity: 0.5 !important;
+}
+
+.devtools-button[checked]:empty::before,
+.devtools-button[open]:empty::before,
+.devtools-button.checked::before,
+.devtools-toolbarbutton:not([label])[checked=true] > image,
+.devtools-toolbarbutton:not([label])[open=true] > image {
+ filter: var(--checked-icon-filter);
+}
+
+/* Checked/opened icon button background */
+.theme-firebug .devtools-button[checked]:empty,
+.theme-firebug .devtools-button[open]:empty,
+.theme-firebug .devtools-button.checked,
+.theme-firebug .devtools-toolbarbutton:not([label])[checked=true],
+.theme-firebug .devtools-toolbarbutton:not([label])[open=true] {
+ background-color: #C8D8E7;
+}
+
+/* Icon-and-text buttons */
+.devtools-toolbarbutton.icon-and-text .toolbarbutton-text {
+ margin-inline-start: .5em !important;
+ font-weight: 600;
+}
+
+/* Text-only buttons */
+.theme-light .devtools-toolbarbutton[label]:not([text-as-image]):not([type=menu-button]),
+.theme-light .devtools-toolbarbutton[data-text-only],
+.theme-light .devtools-button:not(:empty) {
+ background-color: var(--toolbar-tab-hover);
+}
+.theme-dark .devtools-toolbarbutton[label]:not([text-as-image]):not([type=menu-button]),
+.theme-dark .devtools-toolbarbutton[data-text-only],
+.theme-dark .devtools-button:not(:empty) {
+ background-color: rgba(0, 0, 0, .2); /* Splitter */
+}
+
+/* Text-only button states */
+.theme-dark .devtools-button:not(:empty):not(:disabled):hover,
+.theme-dark .devtools-toolbarbutton:not(:-moz-any([checked=true],[disabled],[text-as-image]))[label]:hover {
+ background: rgba(0, 0, 0, .3); /* Splitters */
+}
+.theme-light .devtools-button:not(:empty):not(:disabled):hover,
+.theme-light .devtools-toolbarbutton:not(:-moz-any([checked=true],[disabled],[text-as-image]))[label]:hover {
+ background: rgba(170, 170, 170, .3); /* Splitters */
+}
+
+.theme-dark .devtools-button:not(:empty):not(:disabled):hover:active,
+.theme-dark .devtools-toolbarbutton:not(:-moz-any([checked=true],[disabled],[text-as-image]))[label]:hover:active {
+ background: rgba(0, 0, 0, .4); /* Splitters */
+}
+.theme-light .devtools-button:not(:empty):not(:disabled):hover:active,
+.theme-light .devtools-toolbarbutton:not(:-moz-any([checked=true],[disabled],[text-as-image]))[label]:hover:active {
+ background: var(--toolbar-tab-hover-active);
+}
+
+.theme-dark .devtools-toolbarbutton:not([disabled])[label][checked=true],
+.theme-dark .devtools-toolbarbutton:not([disabled])[label][open],
+.theme-dark .devtools-button:not(:empty)[checked=true] {
+ background: var(--theme-selection-background-semitransparent);
+ color: var(--theme-selection-color);
+}
+.theme-light .devtools-toolbarbutton:not([disabled])[label][checked=true],
+.theme-light .devtools-toolbarbutton:not([disabled])[label][open],
+.theme-light .devtools-button:not(:empty)[checked=true] {
+ background: rgba(76, 158, 217, .3); /* Select highlight blue */
+}
+
+:root {
+ --clear-icon-url: url("chrome://devtools/skin/images/clear.svg");
+}
+
+.devtools-button.devtools-clear-icon::before {
+ background-image: var(--clear-icon-url);
+}
+
+.devtools-button.devtools-filter-icon::before {
+ background-image: var(--filter-image);
+}
+
+.devtools-toolbarbutton.devtools-clear-icon {
+ list-style-image: var(--clear-icon-url);
+}
+
+.devtools-option-toolbarbutton {
+ list-style-image: var(--tool-options-image);
+}
+
+.devtools-toolbarbutton-group > .devtools-toolbarbutton:last-child {
+ margin-inline-end: 0;
+}
+
+.devtools-toolbarbutton-group + .devtools-toolbarbutton {
+ margin-inline-start: 3px;
+}
+
+.devtools-separator + .devtools-toolbarbutton {
+ margin-inline-start: 1px;
+}
+
+/*
+ * Filter buttons
+ * @TODO : Fix when https://bugzilla.mozilla.org/show_bug.cgi?id=1255116 lands
+ */
+.menu-filter-button {
+ -moz-appearance: none;
+ background: rgba(128,128,128,0.1);
+ border: none;
+ border-radius: 2px;
+ min-width: 0;
+ padding: 0 5px;
+ margin: 2px;
+ color: var(--theme-body-color);
+}
+
+.menu-filter-button:hover {
+ background: rgba(128,128,128,0.2);
+}
+
+.menu-filter-button:hover:active {
+ background-color: var(--theme-selection-background-semitransparent);
+}
+
+.menu-filter-button:not(:active).checked {
+ background-color: var(--theme-selection-background);
+ color: var(--theme-selection-color);
+}
+
+/* Text input */
+
+.devtools-textinput,
+.devtools-searchinput,
+.devtools-filterinput {
+ -moz-appearance: none;
+ margin: 1px 3px;
+ border: 1px solid;
+ border-radius: 2px;
+ padding: 4px 6px;
+ border-color: var(--theme-splitter-color);
+ font: message-box;
+}
+
+:root[platform="mac"] .devtools-textinput,
+:root[platform="mac"] .devtools-searchinput,
+:root[platform="mac"] .devtools-filterinput {
+ border-radius: 20px;
+}
+
+.devtools-searchinput,
+.devtools-filterinput {
+ padding: 0;
+ padding-inline-start: 22px;
+ padding-inline-end: 4px;
+ background-position: 8px center;
+ background-size: 11px 11px;
+ background-repeat: no-repeat;
+ font-size: inherit;
+}
+
+/*
+ * @TODO : has-clear-btn class was added for bug 1296187 and we should remove it
+ * once we have a standardized search and filter input across the toolboxes.
+ */
+.has-clear-btn > .devtools-searchinput,
+.has-clear-btn > .devtools-filterinput {
+ padding-inline-end: 23px;
+}
+
+.devtools-searchinput {
+ background-image: var(--magnifying-glass-image);
+}
+
+.devtools-filterinput {
+ background-image: url(chrome://devtools/skin/images/filter.svg#filterinput);
+}
+
+.devtools-searchinput:-moz-locale-dir(rtl),
+.devtools-searchinput:dir(rtl),
+.devtools-filterinput:-moz-locale-dir(rtl),
+.devtools-filterinput:dir(rtl) {
+ background-position: calc(100% - 8px) center;
+}
+
+.devtools-searchinput > .textbox-input-box > .textbox-search-icons > .textbox-search-icon,
+.devtools-filterinput > .textbox-input-box > .textbox-search-icons > .textbox-search-icon {
+ visibility: hidden;
+}
+
+.devtools-searchinput .textbox-input::placeholder,
+.devtools-filterinput .textbox-input::placeholder {
+ font-style: normal;
+}
+
+.devtools-plaininput {
+ border-color: transparent;
+ background-color: transparent;
+}
+
+.theme-dark .devtools-plaininput {
+ color: var(--theme-highlight-gray);
+}
+
+/* Searchbox is a div container element for a search input element */
+.devtools-searchbox {
+ display: inline-flex;
+ flex: 1;
+ height: 23px;
+ position: relative;
+ padding: 0 3px;
+}
+
+/* The spacing is accomplished with a padding on the searchbox */
+.devtools-searchbox > .devtools-textinput,
+.devtools-searchbox > .devtools-searchinput,
+.devtools-searchbox > .devtools-filterinput {
+ margin-left: 0;
+ margin-right: 0;
+ width: 100%;
+}
+
+.devtools-textinput:focus,
+.devtools-searchinput:focus,
+.devtools-filterinput:focus {
+ border-color: var(--theme-focus-border-color-textbox);
+ box-shadow: var(--theme-focus-box-shadow-textbox);
+ transition: all 0.2s ease-in-out;
+ outline: none;
+}
+
+/* Don't add 'double spacing' for inputs that are at beginning / end
+ of a toolbar (since the toolbar has it's own spacing). */
+.devtools-toolbar > .devtools-textinput:first-child,
+.devtools-toolbar > .devtools-searchinput:first-child,
+.devtools-toolbar > .devtools-filterinput:first-child {
+ margin-inline-start: 0;
+}
+.devtools-toolbar > .devtools-textinput:last-child,
+.devtools-toolbar > .devtools-searchinput:last-child,
+.devtools-toolbar > .devtools-filterinput:last-child {
+ margin-inline-end: 0;
+}
+.devtools-toolbar > .devtools-searchbox:first-child {
+ padding-inline-start: 0;
+}
+.devtools-toolbar > .devtools-searchbox:last-child {
+ padding-inline-end: 0;
+}
+
+.devtools-rule-searchbox {
+ -moz-box-flex: 1;
+ width: 100%;
+}
+
+.devtools-filterinput:-moz-any([filled],.filled) {
+ background-color: var(--searchbox-background-color);
+ border-color: var(--searchbox-border-color);
+}
+
+.devtools-style-searchbox-no-match {
+ background-color: var(--searcbox-no-match-background-color) !important;
+ border-color: var(--searcbox-no-match-border-color) !important;
+}
+
+.devtools-searchinput-clear {
+ position: absolute;
+ top: 3.5px;
+ offset-inline-end: 7px;
+ padding: 0;
+ border: 0;
+ width: 16px;
+ height: 16px;
+ background-position: 0 0;
+ background-repeat: no-repeat;
+ background-color: transparent;
+}
+
+.devtools-searchinput-clear:dir(rtl) {
+ right: unset;
+ left: 7px;
+}
+
+.theme-dark .devtools-searchinput-clear {
+ background-image: url("chrome://devtools/skin/images/search-clear-dark.svg");
+}
+
+.theme-light .devtools-searchinput-clear {
+ background-image: url("chrome://devtools/skin/images/search-clear-light.svg");
+}
+
+.devtools-style-searchbox-no-match + .devtools-searchinput-clear {
+ background-image: url("chrome://devtools/skin/images/search-clear-failed.svg") !important;
+}
+
+.devtools-searchinput-clear:hover {
+ background-position: -16px 0;
+}
+
+.theme-dark .devtools-searchinput > .textbox-input-box > .textbox-search-icons > .textbox-search-clear,
+.theme-dark .devtools-filterinput > .textbox-input-box > .textbox-search-icons > .textbox-search-clear {
+ list-style-image: url("chrome://devtools/skin/images/search-clear-dark.svg");
+ -moz-image-region: rect(0, 16px, 16px, 0);
+}
+
+.theme-light .devtools-searchinput > .textbox-input-box > .textbox-search-icons > .textbox-search-clear,
+.theme-light .devtools-filterinput > .textbox-input-box > .textbox-search-icons > .textbox-search-clear {
+ list-style-image: url("chrome://devtools/skin/images/search-clear-light.svg");
+ -moz-image-region: rect(0, 16px, 16px, 0);
+}
+
+.devtools-searchinput > .textbox-input-box > .textbox-search-icons > .textbox-search-clear,
+.devtools-filterinput > .textbox-input-box > .textbox-search-icons > .textbox-search-clear {
+ margin-bottom: 0;
+}
+
+.devtools-searchinput > .textbox-input-box > .textbox-search-icons > .textbox-search-clear:hover,
+.devtools-filterinput > .textbox-input-box > .textbox-search-icons > .textbox-search-clear:hover {
+ -moz-image-region: rect(0, 32px, 16px, 16px);
+}
+
+/* Twisty and checkbox controls */
+.theme-twisty, .theme-checkbox {
+ width: 14px;
+ height: 14px;
+ background-repeat: no-repeat;
+ background-image: url("chrome://devtools/skin/images/controls.png");
+ background-size: 56px 28px;
+}
+
+.theme-twisty {
+ cursor: pointer;
+ background-position: 0 -14px;
+}
+
+.theme-selected ~ .theme-twisty,
+.theme-dark .theme-twisty {
+ background-position: -28px -14px;
+}
+
+.theme-twisty:-moz-focusring {
+ outline-style: none;
+}
+
+.theme-twisty[open], .theme-twisty.open {
+ background-position: -14px -14px;
+}
+
+.theme-selected ~ .theme-twisty[open],
+.theme-dark .theme-twisty[open], .theme-dark .theme-twisty.open {
+ background-position: -42px -14px;
+}
+
+.theme-twisty[invisible] {
+ visibility: hidden;
+}
+
+/* Mirror the twisty for rtl direction */
+.theme-twisty:dir(rtl),
+.theme-twisty:-moz-locale-dir(rtl) {
+ transform: scaleX(-1);
+}
+
+.theme-checkbox {
+ display: inline-block;
+ border: 0;
+ padding: 0;
+ outline: none;
+ background-position: 0 0;
+}
+
+.theme-dark .theme-checkbox {
+ background-position: -28px 0;
+}
+
+.theme-checkbox[checked] {
+ background-position: -14px 0;
+}
+
+.theme-dark .theme-checkbox[checked] {
+ background-position: -42px 0;
+}
+
+@media (min-resolution: 1.1dppx) {
+ .theme-twisty, .theme-checkbox {
+ background-image: url("chrome://devtools/skin/images/controls@2x.png");
+ }
+}
+
+/* Throbbers */
+.devtools-throbber::before {
+ content: "";
+ display: inline-block;
+ vertical-align: bottom;
+ margin-inline-end: 0.5em;
+ width: 1em;
+ height: 1em;
+ border: 2px solid currentColor;
+ border-right-color: transparent;
+ border-radius: 50%;
+ animation: 1.1s linear throbber-spin infinite;
+}
+
+@keyframes throbber-spin {
+ from {
+ transform: none;
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
diff --git a/devtools/client/themes/components-frame.css b/devtools/client/themes/components-frame.css
new file mode 100644
index 000000000..cbdc3d2cf
--- /dev/null
+++ b/devtools/client/themes/components-frame.css
@@ -0,0 +1,53 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+/**
+ * Frame Component
+ * Styles for React component at `devtools/client/shared/components/frame.js`
+ */
+
+.frame-link {
+ display: flex;
+ justify-content: space-between;
+}
+
+.frame-link-async-cause {
+ color: var(--theme-body-color-inactive);
+}
+
+.frame-link .frame-link-source {
+ flex: initial;
+ color: var(--theme-highlight-blue);
+}
+
+.frame-link a.frame-link-source {
+ cursor: pointer;
+ text-decoration: none;
+ font-style: normal;
+}
+
+.frame-link a.frame-link-source:hover {
+ text-decoration: underline;
+}
+
+.frame-link .frame-link-host {
+ margin-inline-start: 5px;
+ font-size: 90%;
+ color: var(--theme-content-color2);
+}
+
+.frame-link .frame-link-function-display-name {
+ margin-inline-end: 5px;
+}
+
+.frame-link .frame-link-line {
+ color: var(--theme-highlight-orange);
+}
+
+.focused .frame-link .frame-link-source,
+.focused .frame-link .frame-link-line,
+.focused .frame-link .frame-link-host {
+ color: var(--theme-selection-color);
+}
diff --git a/devtools/client/themes/components-h-split-box.css b/devtools/client/themes/components-h-split-box.css
new file mode 100644
index 000000000..270b007c7
--- /dev/null
+++ b/devtools/client/themes/components-h-split-box.css
@@ -0,0 +1,24 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+/**
+ * HSplitBox Component
+ * Styles for React component at `devtools/client/shared/components/h-split-box.js`
+ */
+
+.h-split-box,
+.h-split-box-pane {
+ overflow: auto;
+ margin: 0;
+ padding: 0;
+ width: 100%;
+ height: 100%;
+}
+
+.h-split-box {
+ display: flex;
+ flex-direction: row;
+ flex: 1;
+}
diff --git a/devtools/client/themes/computed.css b/devtools/client/themes/computed.css
new file mode 100644
index 000000000..d36637ac9
--- /dev/null
+++ b/devtools/client/themes/computed.css
@@ -0,0 +1,237 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+#sidebar-panel-computedview {
+ margin: 0;
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ height: 100%;
+}
+
+#computedview-container {
+ overflow: auto;
+ height: 100%;
+}
+
+/* This extra wrapper only serves as a way to get the content of the view focusable.
+ So that when the user reaches it either via keyboard or mouse, we know that the view
+ is focused and therefore can handle shortcuts.
+ However, for accessibility reasons, tabindex is set to -1 to avoid having to tab
+ through it, and the outline is hidden. */
+#computedview-container-focusable {
+ height: 100%;
+ outline: none;
+}
+
+#computedview-toolbar {
+ display: flex;
+ align-items: center;
+}
+
+#browser-style-checkbox {
+ /* Bug 1200073 - extra space before the browser styles checkbox so
+ they aren't squished together in a small window. Put also
+ an extra space after. */
+ margin-inline-start: 5px;
+ margin-inline-end: 0;
+}
+
+#browser-style-checkbox-label {
+ padding-inline-start: 5px;
+ margin-inline-end: 5px;
+}
+
+#propertyContainer {
+ -moz-user-select: text;
+ overflow-y: auto;
+ overflow-x: hidden;
+ flex: auto;
+}
+
+.row-striped {
+ background: var(--theme-body-background);
+}
+
+.property-view-hidden,
+.property-content-hidden {
+ display: none;
+}
+
+.property-view {
+ padding: 2px 0px;
+ padding-inline-start: 5px;
+ display: flex;
+ flex-wrap: wrap;
+}
+
+.property-name-container {
+ width: 202px;
+}
+
+.property-value-container {
+ display: flex;
+ flex: 1 1 168px;
+ overflow: hidden;
+}
+
+.property-name-container > *,
+.property-value-container > * {
+ display: inline-block;
+ vertical-align: middle;
+}
+
+.property-name {
+ overflow-x: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ outline: 0 !important;
+}
+
+.other-property-value {
+ background-image: url(images/arrow-e.png);
+ background-repeat: no-repeat;
+ background-size: 5px 8px;
+}
+
+@media (min-resolution: 1.1dppx) {
+ .other-property-value {
+ background-image: url(images/arrow-e@2x.png);
+ }
+}
+
+.property-value {
+ overflow-x: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ padding-inline-start: 14px;
+ outline: 0 !important;
+}
+
+.other-property-value {
+ background-position: left center;
+ padding-inline-start: 8px;
+}
+
+.other-property-value:dir(rtl) {
+ background-position-x: right;
+}
+
+.property-content {
+ padding-inline-start: 17px;
+}
+
+.theme-firebug .property-view,
+.theme-firebug .property-content {
+ font-family: var(--proportional-font-family);
+}
+
+.theme-firebug .property-view {
+ border-bottom: 1px solid rgba(0, 0, 0, 0.1);
+}
+
+/* From skin */
+.expander {
+ visibility: hidden;
+}
+
+.expandable {
+ visibility: visible;
+}
+
+.match {
+ visibility: hidden;
+}
+
+.matchedselectors > p {
+ clear: both;
+ margin: 0;
+ margin-inline-end: 2px;
+ padding: 2px;
+ overflow-x: hidden;
+ border-style: dotted;
+ border-color: rgba(128,128,128,0.4);
+ border-width: 1px 1px 0 1px;
+}
+
+.matchedselectors > p:last-of-type {
+ border-bottom-width: 1px;
+}
+
+.matched {
+ text-decoration: line-through;
+}
+
+.parentmatch {
+ opacity: 0.5;
+}
+
+#computedview-no-results {
+ height: 100%;
+}
+
+.onlyuserstyles {
+ cursor: pointer;
+}
+
+.legendKey {
+ margin: 0 5px;
+}
+
+.link {
+ padding: 0 3px;
+ cursor: pointer;
+ float: right;
+}
+
+/* Workaround until float: inline-end; is enabled by default */
+.link:dir(rtl) {
+ float: left;
+}
+
+/* Take away these two :visited rules to get a core dumper */
+/* See https://bugzilla.mozilla.org/show_bug.cgi?id=575675#c30 */
+
+.link,
+.link:visited {
+ color: #0091ff;
+}
+
+.link,
+.helplink,
+.link:visited,
+.helplink:visited {
+ text-decoration: none;
+}
+
+.link:hover {
+ text-decoration: underline;
+}
+
+.computedview-colorswatch {
+ border-radius: 50%;
+ width: 0.9em;
+ height: 0.9em;
+ vertical-align: middle;
+ margin-inline-end: 5px;
+ display: inline-block;
+ position: relative;
+}
+
+.computedview-colorswatch::before {
+ content: '';
+ background-color: #eee;
+ background-image: linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%, #ccc),
+ linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%, #ccc);
+ background-size: 12px 12px;
+ background-position: 0 0, 6px 6px;
+ position: absolute;
+ border-radius: 50%;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ z-index: -1;
+}
diff --git a/devtools/client/themes/dark-theme.css b/devtools/client/themes/dark-theme.css
new file mode 100644
index 000000000..2035f8b22
--- /dev/null
+++ b/devtools/client/themes/dark-theme.css
@@ -0,0 +1,348 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+@import url(resource://devtools/client/themes/variables.css);
+@import url(resource://devtools/client/themes/common.css);
+@import url(toolbars.css);
+@import url(tooltips.css);
+
+body {
+ margin: 0;
+}
+
+.theme-body {
+ background: var(--theme-body-background);
+ color: var(--theme-body-color);
+}
+
+.theme-sidebar {
+ background: var(--theme-sidebar-background);
+ color: var(--theme-content-color1);
+}
+
+::-moz-selection {
+ background-color: var(--theme-selection-background);
+ color: var(--theme-selection-color);
+}
+
+.theme-bg-darker {
+ background-color: var(--theme-selection-background-semitransparent);
+}
+
+.theme-selected,
+.CodeMirror-hint-active {
+ background-color: var(--theme-selection-background);
+ color: var(--theme-selection-color);
+}
+
+.theme-bg-contrast,
+.variable-or-property:not([overridden])[changed] {
+ background: var(--theme-contrast-background);
+}
+
+.theme-link,
+.cm-s-mozilla .cm-link,
+.cm-s-mozilla .cm-keyword {
+ color: var(--theme-highlight-green);
+}
+
+/*
+ * FIXME: http://bugzil.la/575675 CSS links without :visited set cause assertion
+ * failures in debug builds.
+ */
+.theme-link:visited,
+.cm-s-mozilla .cm-link:visited,
+.CodeMirror-Tern-type {
+ color: var(--theme-highlight-blue);
+}
+
+
+.theme-comment,
+.cm-s-mozilla .cm-meta,
+.cm-s-mozilla .cm-hr,
+.cm-s-mozilla .cm-comment,
+.variable-or-property .token-undefined,
+.variable-or-property .token-null,
+.CodeMirror-Tern-completion-unknown:before {
+ color: var(--theme-comment);
+}
+
+.theme-gutter {
+ background-color: var(--theme-tab-toolbar-background);
+ color: var(--theme-content-color3);
+ border-color: var(--theme-splitter-color);
+}
+
+.theme-separator {
+ border-color: var(--theme-splitter-color);
+}
+
+.theme-fg-color1,
+.cm-s-mozilla .cm-number,
+.variable-or-property .token-number,
+.variable-or-property[return] > .title > .name,
+.variable-or-property[scope] > .title > .name {
+ color: var(--theme-highlight-red);
+}
+
+.CodeMirror-Tern-completion-number:before {
+ background-color: #5c9966;
+}
+
+.theme-fg-color2,
+.cm-s-mozilla .cm-attribute,
+.cm-s-mozilla .cm-def,
+.cm-s-mozilla .cm-property,
+.cm-s-mozilla .cm-qualifier,
+.variables-view-variable > .title > .name {
+ color: var(--theme-highlight-purple);
+}
+
+.CodeMirror-Tern-completion-object:before {
+ background-color: #3689b2;
+}
+
+.cm-s-mozilla .cm-unused-line {
+ text-decoration: line-through;
+ text-decoration-color: #0072ab;
+}
+
+.cm-s-mozilla .cm-executed-line {
+ background-color: #133c26;
+}
+
+.theme-fg-color3,
+.cm-s-mozilla .cm-builtin,
+.cm-s-mozilla .cm-tag,
+.cm-s-mozilla .cm-header,
+.cm-s-mozilla .cm-bracket,
+.variables-view-property > .title > .name {
+ color: var(--theme-highlight-green);
+}
+
+.CodeMirror-Tern-completion-array:before {
+ background-color: var(--theme-highlight-bluegrey);
+}
+
+.theme-fg-color4 {
+ color: var(--theme-highlight-purple);
+}
+
+.theme-fg-color5 {
+ color: var(--theme-highlight-purple);
+}
+
+.theme-fg-color6,
+.cm-s-mozilla .cm-string,
+.cm-s-mozilla .cm-string-2,
+.variable-or-property .token-string,
+.cm-s-mozilla .cm-variable,
+.CodeMirror-Tern-farg {
+ color: var(--theme-highlight-gray);
+}
+
+.CodeMirror-Tern-completion-string:before,
+.CodeMirror-Tern-completion-fn:before {
+ background-color: #b26b47;
+}
+
+.theme-fg-color7,
+.cm-s-mozilla .cm-atom,
+.cm-s-mozilla .cm-quote,
+.cm-s-mozilla .cm-error,
+.variable-or-property .token-boolean,
+.variable-or-property .token-domnode,
+.variable-or-property[exception] > .title > .name {
+ color: var(--theme-highlight-red);
+}
+
+.CodeMirror-Tern-completion-bool:before {
+ background-color: #bf5656;
+}
+
+.variable-or-property .token-domnode {
+ font-weight: bold;
+}
+
+.theme-toolbar,
+.devtools-toolbar,
+.devtools-sidebar-tabs tabs,
+.devtools-sidebar-alltabs,
+.cm-s-mozilla .CodeMirror-dialog { /* General toolbar styling */
+ color: var(--theme-body-color-alt);
+ background-color: var(--theme-toolbar-background);
+ border-color: var(--theme-splitter-color);
+}
+
+.theme-fg-contrast { /* To be used for text on theme-bg-contrast */
+ color: black;
+}
+
+.ruleview-swatch,
+.computedview-colorswatch {
+ box-shadow: 0 0 0 1px #818181;
+}
+
+/* CodeMirror specific styles.
+ * Best effort to match the existing theme, some of the colors
+ * are duplicated here to prevent weirdness in the main theme. */
+
+.CodeMirror.cm-s-mozilla { /* Inherit platform specific font sizing and styles */
+ font-family: inherit;
+ font-size: inherit;
+ background: transparent;
+}
+
+.CodeMirror.cm-s-mozilla pre,
+.cm-s-mozilla .cm-variable-2,
+.cm-s-mozilla .cm-variable-3,
+.cm-s-mozilla .cm-operator,
+.cm-s-mozilla .cm-special {
+ color: var(--theme-content-color1);
+}
+
+.cm-s-mozilla .CodeMirror-lines .CodeMirror-cursor {
+ border-left: solid 1px #fff;
+}
+
+.cm-s-mozilla.CodeMirror-focused .CodeMirror-selected { /* selected text (focused) */
+ background: rgb(185, 215, 253);
+}
+
+.cm-s-mozilla .CodeMirror-selected { /* selected text (unfocused) */
+ background: rgb(176, 176, 176);
+}
+
+.cm-s-mozilla .CodeMirror-activeline-background { /* selected color with alpha */
+ background: rgba(185, 215, 253, .15);
+}
+
+div.cm-s-mozilla span.CodeMirror-matchingbracket { /* highlight brackets */
+ outline: solid 1px rgba(255, 255, 255, .25);
+ color: white;
+}
+
+/* Highlight for a line that contains an error. */
+div.CodeMirror div.error-line {
+ background: rgba(255,0,0,0.2);
+}
+
+/* Generic highlighted text */
+div.CodeMirror span.marked-text {
+ background: rgba(255,255,0,0.2);
+ border: 1px dashed rgba(192,192,0,0.6);
+ margin-inline-start: -1px;
+ margin-inline-end: -1px;
+}
+
+/* Highlight for evaluating current statement. */
+div.CodeMirror span.eval-text {
+ background-color: #556;
+}
+
+.cm-s-mozilla .CodeMirror-linenumber { /* line number text */
+ color: var(--theme-content-color3);
+}
+
+.cm-s-mozilla .CodeMirror-gutters { /* vertical line next to line numbers */
+ border-right-color: var(--theme-toolbar-background);
+ background-color: var(--theme-sidebar-background);
+}
+
+.cm-s-markup-view pre {
+ line-height: 1.4em;
+ min-height: 1.4em;
+}
+
+/* XUL panel styling (see devtools/client/shared/widgets/tooltip/Tooltip.js) */
+
+.theme-tooltip-panel .panel-arrowcontent {
+ padding: 5px;
+ background: rgba(19, 28, 38, .9);
+ border-radius: 5px;
+ box-shadow: none;
+ border: 3px solid #434850;
+}
+
+/* Overring panel arrow images to fit with our light and dark themes */
+
+.theme-tooltip-panel .panel-arrow {
+ --arrow-margin: -4px;
+}
+
+:root[platform="win"] .theme-tooltip-panel .panel-arrow {
+ --arrow-margin: -7px;
+}
+
+.theme-tooltip-panel .panel-arrow[side="top"],
+.theme-tooltip-panel .panel-arrow[side="bottom"] {
+ list-style-image: url("chrome://devtools/skin/tooltip/arrow-vertical-dark.png");
+ /* !important is needed to override the popup.css rules in toolkit/themes */
+ width: 39px !important;
+ height: 16px !important;
+}
+
+.theme-tooltip-panel .panel-arrow[side="left"],
+.theme-tooltip-panel .panel-arrow[side="right"] {
+ list-style-image: url("chrome://devtools/skin/tooltip/arrow-horizontal-dark.png");
+ /* !important is needed to override the popup.css rules in toolkit/themes */
+ width: 16px !important;
+ height: 39px !important;
+}
+
+.theme-tooltip-panel .panel-arrow[side="top"] {
+ margin-bottom: var(--arrow-margin);
+}
+
+.theme-tooltip-panel .panel-arrow[side="bottom"] {
+ margin-top: var(--arrow-margin);
+}
+
+.theme-tooltip-panel .panel-arrow[side="left"] {
+ margin-right: var(--arrow-margin);
+}
+
+.theme-tooltip-panel .panel-arrow[side="right"] {
+ margin-left: var(--arrow-margin);
+}
+
+@media (min-resolution: 1.1dppx) {
+ .theme-tooltip-panel .panel-arrow[side="top"],
+ .theme-tooltip-panel .panel-arrow[side="bottom"] {
+ list-style-image: url("chrome://devtools/skin/tooltip/arrow-vertical-dark@2x.png");
+ }
+
+ .theme-tooltip-panel .panel-arrow[side="left"],
+ .theme-tooltip-panel .panel-arrow[side="right"] {
+ list-style-image: url("chrome://devtools/skin/tooltip/arrow-horizontal-dark@2x.png");
+ }
+}
+
+.theme-tooltip-panel .devtools-tooltip-simple-text {
+ color: white;
+ border-bottom: 1px solid #434850;
+}
+
+.theme-tooltip-panel .devtools-tooltip-simple-text:last-child {
+ border-bottom: 0;
+}
+
+.devtools-textinput,
+.devtools-searchinput,
+.devtools-filterinput {
+ background-color: rgba(24, 29, 32, 1);
+ color: rgba(184, 200, 217, 1);
+}
+
+.CodeMirror-Tern-fname {
+ color: #f7f7f7;
+}
+
+.CodeMirror-hints,
+.CodeMirror-Tern-tooltip {
+ box-shadow: 0 0 4px rgba(255, 255, 255, .3);
+ background-color: #0f171f;
+ color: var(--theme-body-color);
+}
diff --git a/devtools/client/themes/debugger.css b/devtools/client/themes/debugger.css
new file mode 100644
index 000000000..3f2d49a0f
--- /dev/null
+++ b/devtools/client/themes/debugger.css
@@ -0,0 +1,670 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+/* Sources and breakpoints pane */
+
+#sources-pane[selectedIndex="0"] + #sources-and-editor-splitter {
+ border-color: transparent;
+}
+
+#sources-pane > tabs {
+ border-inline-end: 1px solid;
+}
+
+#sources-pane .devtools-toolbar {
+ border: none; /* Remove the devtools-toolbar bottom border. */
+ border-inline-end: 1px solid;
+}
+
+#sources-pane > tabs,
+#sources-pane .devtools-toolbar {
+ border-inline-end-color: var(--theme-splitter-color);
+}
+
+/* Sources and breakpoints list */
+
+.dbg-source-item {
+ padding: 2px 0px;
+}
+
+.dbg-wasm-item .icon {
+ display: block;
+ background-image: url(chrome://devtools/skin/images/webconsole.svg);
+ background-repeat: no-repeat;
+ background-size: 72px 60px;
+ /* show warning icon */
+ background-position: -24px -24px;
+ width: 10px;
+ height: 10px;
+ position: absolute;
+ margin-inline-start: -15px;
+ margin-top: 3px;
+}
+
+.dbg-breakpoint-line {
+ font-weight: 600;
+}
+
+.dbg-breakpoint-text {
+ padding-inline-start: 6px;
+ font-style: italic;
+ font-size: 90%;
+}
+
+.dbg-breakpoint-checkbox {
+ width: 16px;
+ height: 16px;
+ margin: 2px;
+}
+
+/* Firebug theme uses breakpoint icon istead of a checkbox */
+
+.theme-firebug #sources-pane .dbg-breakpoint-checkbox .checkbox-check {
+ -moz-appearance: none;
+ border: none;
+ background: url(chrome://devtools/skin/images/firebug/breakpoint.svg) no-repeat 50% 50%;
+}
+
+.theme-firebug #sources-pane .dbg-breakpoint-checkbox:not([checked="true"]) > .checkbox-check,
+.theme-firebug #sources-pane .dbg-breakpoint-checkbox:not([checked="true"]) ~ * {
+ opacity: 0.5;
+}
+
+.theme-firebug #sources-pane .dbg-breakpoint-checkbox {
+ padding-inline-end: 0;
+ margin-inline-end: 0;
+}
+
+.dbg-breakpoint-condition-thrown-message {
+ display: none;
+ color: var(--theme-highlight-red);
+}
+
+.dbg-breakpoint.dbg-breakpoint-condition-thrown .dbg-breakpoint-condition-thrown-message {
+ display: block;
+ padding-inline-start: 0;
+}
+
+/* Sources toolbar */
+
+#sources-toolbar > .devtools-toolbarbutton,
+#sources-controls > .devtools-toolbarbutton {
+ min-width: 32px;
+}
+
+#black-box {
+ list-style-image: url(images/item-toggle.svg);
+}
+
+.theme-firebug #black-box {
+ list-style-image: url(images/firebug/debugger-blackbox.svg);
+}
+
+#pretty-print {
+ list-style-image: url(images/tool-styleeditor.svg);
+}
+
+.theme-firebug #pretty-print {
+ list-style-image: url(images/firebug/debugger-prettyprint.svg);
+}
+
+#toggle-breakpoints {
+ list-style-image: url(images/debugger-toggleBreakpoints.svg);
+ -moz-image-region: rect(0,32px,16px,16px);
+}
+
+.theme-firebug #toggle-breakpoints {
+ list-style-image: url(images/firebug/debugger-toggleBreakpoints.svg);
+ -moz-image-region: unset;
+}
+
+#toggle-breakpoints[checked] {
+ -moz-image-region: rect(0,16px,16px,0);
+}
+
+#toggle-breakpoints[checked] > image {
+ /* This button has a special checked image, don't make it blue */
+ filter: none;
+}
+
+#sources .black-boxed {
+ color: rgba(128,128,128,0.4);
+}
+
+#sources .selected .black-boxed {
+ color: rgba(255,255,255,0.4);
+}
+
+#sources .black-boxed ~ .dbg-breakpoint {
+ display: none;
+}
+
+/* Debugger unblackbox button */
+
+#black-boxed-message-button > .button-box > .button-icon {
+ width: 16px;
+ height: 16px;
+ background-image: url(images/item-toggle.svg);
+ background-position: 0 0;
+ background-size: cover;
+}
+
+/* Black box message and source progress meter */
+
+#black-boxed-message,
+#source-progress-container {
+ /* Prevent the container deck from aquiring the size from this message. */
+ min-width: 1px;
+ min-height: 1px;
+}
+
+#source-progress {
+ min-height: 2em;
+ min-width: 40em;
+}
+
+#black-boxed-message-label,
+#black-boxed-message-button {
+ text-align: center;
+ font-size: 120%;
+}
+
+#black-boxed-message-button {
+ margin-top: 1em;
+ padding: .25em;
+}
+
+/* Breadcrumbs stack frames view */
+
+.dbg-stackframe-details {
+ padding-inline-start: 4px;
+}
+
+/* Classic stack frames view */
+
+.dbg-classic-stackframe {
+ display: block;
+}
+
+.dbg-classic-stackframe-title {
+ font-weight: 600;
+}
+
+.dbg-classic-stackframe-details:-moz-locale-dir(ltr) {
+ float: right;
+}
+
+.dbg-classic-stackframe-details:-moz-locale-dir(rtl) {
+ float: left;
+}
+
+.dbg-classic-stackframe-details-url {
+ max-width: 90%;
+ text-align: end;
+}
+
+.dbg-classic-stackframe-details-url {
+ color: var(--theme-content-color1);
+}
+
+.dbg-classic-stackframe-details-sep {
+ color: var(--theme-body-color-alt)
+}
+
+.dbg-classic-stackframe-details-line {
+ color: var(--theme-highlight-bluegrey);
+}
+
+#callstack-list .selected label {
+ /* Text inside a selected item should not be custom colored. */
+ color: inherit !important;
+}
+
+/* Tracer */
+
+#trace {
+ list-style-image: url(images/tracer-icon.png);
+}
+
+@media (min-resolution: 1.1dppx) {
+ #trace {
+ list-style-image: url(images/tracer-icon@2x.png);
+ }
+}
+
+#clear-tracer {
+ /* Make this button as narrow as the text inside it. */
+ min-width: 1px;
+}
+
+.trace-name {
+ padding-inline-start: 4px;
+}
+
+/* Tracer dark theme */
+
+.theme-dark .trace-item {
+ color: var(--theme-selection-color);
+}
+
+.theme-dark .trace-item.black-boxed {
+ color: rgba(128,128,128,0.4);
+}
+
+.theme-dark .trace-item.selected-matching {
+ background-color: rgba(86, 117, 185, .4); /* Select highlight blue at 40% alpha */
+}
+
+.theme-dark .selected > .trace-item {
+ background-color: rgba(86, 117, 185, .6); /* Select highlight blue at 60% alpha */
+}
+
+.trace-call {
+ color: var(--theme-highlight-blue);
+}
+
+.trace-return,
+.trace-yield {
+ color: var(--theme-highlight-green);
+}
+
+.trace-throw {
+ color: var(--theme-highlight-red);
+}
+
+.trace-param {
+ color: var(--theme-content-color1);
+}
+
+.theme-dark .trace-syntax {
+ color: var(--theme-content-color2);
+}
+
+/* Tracer light theme */
+.theme-light .trace-item {
+ color: var(--theme-content-color1);
+}
+
+.theme-light .trace-item.black-boxed {
+ color: rgba(128,128,128,0.4);
+}
+
+.theme-light .trace-item.selected-matching {
+ background-color: rgba(76,158,217,.4); /* Select highlight blue at 40% alpha */
+}
+
+.theme-light .selected > .trace-item {
+ background-color: rgba(76,158,217,.6); /* Select highlight blue at 60% alpha */
+}
+
+#tracer-traces .selected label {
+ /* Text inside a selected item should not be custom colored. */
+ color: inherit !important;
+}
+
+/* Watch expressions view */
+
+#expressions {
+ min-height: 10px;
+ max-height: 125px;
+}
+
+.dbg-expression {
+ height: 20px;
+}
+
+.dbg-expression-arrow {
+ background-image: var(--theme-command-line-image-focus);
+ width: 16px;
+ height: 16px;
+ margin: 2px;
+}
+
+.dbg-expression-input {
+ color: inherit;
+}
+
+.dbg-expression-button {
+ -moz-appearance: none;
+ border: none;
+ background: none;
+ text-decoration: underline;
+ cursor: pointer;
+}
+
+.dbg-expression-button {
+ color: var(--theme-highlight-blue);
+}
+
+/* Event listeners view */
+
+.dbg-event-listener-type {
+ font-weight: 600;
+}
+
+.dbg-event-listener-location {
+ color: var(--theme-content-color1);
+}
+
+.dbg-event-listener-separator {
+ color: var(--theme-body-color-alt);
+}
+
+.dbg-event-listener-targets {
+ color: var(--theme-highlight-bluegrey);
+}
+
+.theme-dark #event-listeners .selected {
+ /* Selected items shouldn't be displayed differently. */
+ background: none;
+ color: #fff;
+}
+
+.theme-light #event-listeners .selected {
+ /* Selected items shouldn't be displayed differently. */
+ background: none;
+ color: #000;
+}
+
+/* Searchbox and the search operations help panel */
+
+#searchbox {
+ min-width: 220px;
+ margin-inline-start: 1px;
+}
+
+#filter-label {
+ margin-inline-start: 2px;
+}
+
+#searchbox-panel-operators {
+ margin-top: 5px;
+ margin-bottom: 8px;
+ margin-inline-start: 2px;
+}
+
+.searchbox-panel-operator-button {
+ min-width: 26px;
+ margin-top: 0;
+ margin-bottom: 0;
+ margin-inline-start: 2px;
+ margin-inline-end: 6px;
+ text-align: center;
+}
+
+.searchbox-panel-operator-label {
+ padding-bottom: 2px;
+}
+
+/* Searchbox results panel */
+
+#results-panel {
+ border: none;
+}
+
+.results-panel-item {
+ padding: 6px 8px;
+ border-top: 1px solid rgba(128,128,128,0.2);
+}
+
+.results-panel-item:first-of-type {
+ border-top: none;
+}
+
+.results-panel-item-label {
+ font-weight: 600;
+}
+
+.results-panel-item-label-before {
+ padding-inline-end: 6px;
+}
+
+.theme-dark .results-panel-item-label {
+ color: var(--theme-selection-color);
+}
+
+.theme-light .results-panel-item-label {
+ color: var(--theme-body-color);
+}
+
+.results-panel-item-label-before {
+ color: var(--theme-highlight-bluegrey);
+}
+
+.results-panel-item-label-below {
+ color: var(--theme-content-color3);
+}
+
+#results-panel .selected label {
+ /* Text inside a selected item should not be custom colored. */
+ color: inherit !important;
+}
+
+/* Sources search view */
+
+#globalsearch {
+ min-height: 10px;
+ max-height: 50vh;
+}
+
+.dbg-results-header {
+ padding-inline-start: 6px;
+}
+
+.dbg-results-header-location {
+ font-weight: 600;
+}
+
+.dbg-results-header-match-count {
+ padding-inline-start: 6px;
+}
+
+.dbg-results-line-number {
+ min-width: 3em;
+ border-inline-end: 1px solid rgba(128,128,128,0.2);
+ padding-inline-end: 4px;
+ text-align: end;
+}
+
+.dbg-results-line-contents {
+ padding-inline-start: 4px;
+}
+
+.dbg-results-line-contents-string[match=true] {
+ background-color: rgba(255,255,0,0.2);
+ border: 1px solid rgba(128,128,128,0.7);
+ border-radius: 4px;
+ margin-top: -1px !important;
+ margin-bottom: -1px !important;
+ cursor: pointer;
+}
+
+.dbg-results-line-contents-string[match=true][focusing] {
+ transition: transform 0.3s ease-in-out;
+}
+
+.dbg-results-line-contents-string[match=true][focused] {
+ transition-duration: 0.1s;
+ transform: scale(1.75, 1.75);
+}
+
+.dbg-source-results:not(.selected):hover {
+ background-color: var(--theme-sidebar-background);
+}
+
+.dbg-results-header {
+ background-color: var(--theme-tab-toolbar-background);
+}
+
+.theme-dark .dbg-results-header {
+ color: var(--theme-content-color1);
+}
+
+.theme-light .dbg-results-header {
+ color: var(--theme-content-color3);
+}
+
+.theme-dark .dbg-search-result:hover {
+ background-color: rgba(86, 117, 185, .2); /* Select highlight blue at 40% alpha */
+}
+
+.theme-light .dbg-search-result:hover {
+ background-color: rgba(76,158,217,.2); /* Select highlight blue at 40% alpha */
+}
+
+.dbg-results-header-match-count {
+ color: var(--theme-content-color3);
+}
+
+.dbg-results-line-number {
+ background-color: var(--theme-tab-toolbar-background);
+ color: var(--theme-body-color-alt);
+}
+
+.dbg-results-line-contents-string {
+ color: var(--theme-body-color-alt);
+}
+
+.theme-dark .dbg-results-line-contents-string[match=true] {
+ color: var(--theme-selection-color);
+}
+
+.theme-light .dbg-results-line-contents-string[match=true] {
+ color: var(--theme-body-color);
+}
+
+/* Toolbar controls */
+
+#resume {
+ list-style-image: url(images/pause.svg);
+}
+
+#resume[checked] {
+ list-style-image: url(images/play.svg);
+}
+
+.theme-firebug #resume {
+ list-style-image: url(images/firebug/pause.svg);
+}
+
+.theme-firebug #resume[checked] {
+ list-style-image: url(images/firebug/play.svg);
+}
+
+#resume[break-on-next] {
+ background: var(--theme-highlight-lightorange);
+}
+
+#step-over {
+ list-style-image: url(images/debugger-step-over.svg);
+}
+
+#step-in {
+ list-style-image: url(images/debugger-step-in.svg);
+}
+
+#step-out {
+ list-style-image: url(images/debugger-step-out.svg);
+}
+
+.theme-firebug #step-over {
+ list-style-image: url(images/firebug/debugger-step-over.svg);
+}
+
+.theme-firebug #step-in {
+ list-style-image: url(images/firebug/debugger-step-in.svg);
+}
+
+.theme-firebug #step-out {
+ list-style-image: url(images/firebug/debugger-step-out.svg);
+}
+
+#instruments-pane-toggle {
+ list-style-image: var(--theme-pane-collapse-image);
+}
+
+#instruments-pane-toggle.pane-collapsed {
+ list-style-image: var(--theme-pane-expand-image);
+}
+
+/* Horizontal vs. vertical layout */
+
+#vertical-layout-panes-container {
+ min-height: 35vh;
+ max-height: 80vh;
+}
+
+#body[layout=vertical] #sources-pane > tabs {
+ border-inline-end: none;
+}
+
+#body[layout=vertical] #instruments-pane {
+ margin: 0 !important;
+ /* To prevent all the margin hacks to hide the sidebar. */
+}
+
+#body[layout=vertical] .side-menu-widget-container,
+#body[layout=vertical] .side-menu-widget-empty-text {
+ box-shadow: none !important;
+}
+
+#body[layout=vertical] .side-menu-widget-item-arrow {
+ background-image: none !important;
+}
+
+#body[layout=vertical] .side-menu-widget-group,
+#body[layout=vertical] .side-menu-widget-item {
+ margin-inline-end: 0;
+}
+
+/* Firebug theme customization of source group title */
+.theme-firebug #sources-pane .side-menu-widget-group-title {
+ border-bottom: none;
+ padding: 2px 4px;
+ background: var(--theme-header-background);
+ font-weight: bold;
+}
+
+/* Sections titles (toolbars) in Variables panel they have different height */
+.theme-firebug #variables-tabpanel .title.devtools-toolbar {
+ display: -moz-box;
+ height: 20px !important;
+}
+
+/* Firebug theme support for the Callstack Panel */
+
+.theme-firebug #callstack-list {
+ font-family: var(--proportional-font-family);
+}
+
+.theme-firebug #callstack-list .dbg-classic-stackframe-title {
+ color: var(--theme-content-color2);
+ font-weight: normal;
+ font-family: monospace;
+}
+
+.theme-firebug #callstack-list .side-menu-widget-item {
+ padding-top: 2px;
+ padding-bottom: 2px;
+}
+
+.theme-firebug #callstack-list .dbg-classic-stackframe-details-url,
+.theme-firebug #callstack-list .dbg-classic-stackframe-details-sep,
+.theme-firebug #callstack-list .dbg-classic-stackframe-details-line {
+ color: blue;
+ font-weight: bold;
+}
+
+.theme-firebug #callstack-list .side-menu-widget-item {
+ margin: 0 4px;
+}
+
+.theme-firebug #callstack-list .side-menu-widget-item.selected {
+ color: var(--theme-selection-color);
+}
+
+.theme-firebug #callstack-list .side-menu-widget-item:first-child {
+ border-top: none;
+}
diff --git a/devtools/client/themes/devtools-browser.css b/devtools/client/themes/devtools-browser.css
new file mode 100644
index 000000000..de846b8f3
--- /dev/null
+++ b/devtools/client/themes/devtools-browser.css
@@ -0,0 +1,26 @@
+/* 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/. */
+
+@import url("resource://devtools/client/themes/splitters.css");
+
+/* Bottom-docked toolbox minimize transition */
+.devtools-toolbox-bottom-iframe {
+ transition: margin-bottom .1s;
+}
+
+.devtools-toolbox-side-iframe {
+ min-width: 465px;
+}
+
+/* Eyedropper Widget */
+/* <panel> added to mainPopupSet */
+
+.devtools-eyedropper-panel {
+ pointer-events: none;
+ -moz-appearance: none;
+ width: 156px;
+ height: 120px;
+ background-color: transparent;
+ border: none;
+}
diff --git a/devtools/client/themes/dom.css b/devtools/client/themes/dom.css
new file mode 100644
index 000000000..53eb8bb28
--- /dev/null
+++ b/devtools/client/themes/dom.css
@@ -0,0 +1,9 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+:root.theme-dark {
+}
+:root.theme-light {
+}
diff --git a/devtools/client/themes/firebug-theme.css b/devtools/client/themes/firebug-theme.css
new file mode 100644
index 000000000..ea06235c9
--- /dev/null
+++ b/devtools/client/themes/firebug-theme.css
@@ -0,0 +1,235 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+@import url(resource://devtools/client/themes/variables.css);
+@import url(resource://devtools/client/themes/common.css);
+@import url(light-theme.css);
+
+:root {
+ font-size: 11px;
+ font-family: var(--proportional-font-family);
+}
+
+/* CodeMirror Color Syntax */
+
+.theme-firebug .cm-keyword {color: BlueViolet; font-weight: bold;}
+.theme-firebug .cm-atom {color: #219;}
+.theme-firebug .cm-number {color: #164;}
+.theme-firebug .cm-def {color: #00f;}
+.theme-firebug .cm-variable {color: black;}
+.theme-firebug .cm-variable-2 {color: black;}
+.theme-firebug .cm-variable-3 {color: black;}
+.theme-firebug .cm-property {color: black;}
+.theme-firebug .cm-operator {color: black;}
+.theme-firebug .cm-comment {color: Silver;}
+.theme-firebug .cm-string {color: Red;}
+.theme-firebug .cm-string-2 {color: Red;}
+.theme-firebug .cm-meta {color: rgb(120, 120, 120); font-style: italic;}
+.theme-firebug .cm-error {color: #f00;}
+.theme-firebug .cm-qualifier {color: #555;}
+.theme-firebug .cm-builtin {color: #30a;}
+.theme-firebug .cm-bracket {color: #997;}
+.theme-firebug .cm-tag {color: blue;}
+.theme-firebug .cm-attribute {color: rgb(0, 0, 136);}
+.theme-firebug .cm-header {color: blue;}
+.theme-firebug .cm-quote {color: #090;}
+.theme-firebug .cm-hr {color: #999;}
+.theme-firebug .cm-link {color: #00c;}
+
+.theme-firebug .theme-fg-color3,
+.theme-firebug .cm-s-mozilla .kind-Object .cm-variable{ /* dark blue */
+ color: #006400;
+ font-style: normal;
+ font-weight: bold;
+}
+
+.theme-firebug .console-string {
+ color: #FF183C;
+}
+
+/* Variables View */
+
+.theme-firebug .variables-view-variable > .title > .name,
+.theme-firebug .variables-view-variable > .title > .value {
+ color: var(--theme-body-color);
+}
+
+/* Firebug theme support for tabbar and panel tabs
+ (both, main and side panels )*/
+
+/* Only apply bottom-border for:
+ 1) The main tab list.
+ 2) The side tab list if there is no scroll-box that has its own border.
+
+ Use !important to override even the rule in webconsole.css that uses
+ ID in the selector. */
+.theme-firebug .devtools-tabbar,
+.theme-firebug .devtools-sidebar-tabs tabs {
+ background-image: linear-gradient(rgba(253, 253, 253, 0.2), rgba(253, 253, 253, 0));
+ border-bottom: 1px solid rgb(170, 188, 207) !important;
+}
+
+.theme-firebug .devtools-sidebar-tabs tabs {
+ background-color: var(--theme-tab-toolbar-background) !important;
+ background-image: linear-gradient(rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0.2));
+}
+
+/* Add a negative bottom margin to overlap bottom border
+ of the parent element (see also the next comment for 'tabs') */
+.theme-firebug .devtools-tab,
+.theme-firebug .devtools-sidebar-tabs tab {
+ margin: 3px 0 -1px 0;
+ padding: 2px 0 0 0;
+ border: 1px solid transparent !important;
+ border-radius: 4px 4px 0 0;
+ font-weight: bold;
+ color: var(--theme-body-color);
+ -moz-box-flex: initial;
+ min-width: 0;
+}
+
+/* Also add negative bottom margin for side panel tabs*/
+.theme-firebug .devtools-sidebar-tabs tab {
+}
+
+/* In order to hide bottom-border of side panel tabs we need
+ to make the parent element overflow visible, so child element
+ can move one pixel down to hide the bottom border of the parent. */
+.theme-firebug .devtools-sidebar-tabs tabs {
+ overflow: visible;
+}
+
+.theme-firebug .devtools-tab:hover,
+.theme-firebug .devtools-sidebar-tabs tab:hover {
+ border: 1px solid #C8C8C8 !important;
+ border-bottom: 1px solid transparent;
+}
+
+.theme-firebug .devtools-tab[selected],
+.theme-firebug .devtools-sidebar-tabs tab[selected] {
+ background-color: rgb(247, 251, 254);
+ border: 1px solid rgb(170, 188, 207) !important;
+ border-bottom-width: 0 !important;
+ padding-bottom: 2px;
+ color: inherit;
+}
+
+.theme-firebug .devtools-tab spacer,
+.theme-firebug .devtools-tab image {
+ display: none;
+}
+
+.theme-firebug .toolbox-tab label {
+ margin: 0;
+}
+
+.theme-firebug .devtools-sidebar-tabs tab label {
+ margin: 2px 0 0 0;
+}
+
+/* Use different padding for labels inside tabs on Win platform.
+ Make sure this overrides the default in global.css */
+:root[platform="win"].theme-firebug .devtools-sidebar-tabs tab label {
+ margin: 0 4px !important;
+}
+
+.theme-firebug #panelSideBox .devtools-tab[selected],
+.theme-firebug .devtools-sidebar-tabs tab[selected] {
+ background-color: white;
+}
+
+.theme-firebug #panelSideBox .devtools-tab:first-child,
+.theme-firebug .devtools-sidebar-tabs tab:first-child {
+ margin-inline-start: 5px;
+}
+
+/* Firebug theme support for the Option (panel) tab */
+
+.theme-firebug #toolbox-tab-options {
+ margin-inline-end: 4px;
+ background-color: white;
+}
+
+.theme-firebug #toolbox-tab-options::before {
+ content: url(chrome://devtools/skin/images/firebug/tool-options.svg);
+ display: block;
+ margin: 4px 7px 0;
+}
+
+.theme-firebug #toolbox-tab-options:not([selected]):hover::before {
+ filter: brightness(80%);
+}
+
+/* Toolbar */
+
+.theme-firebug .theme-toolbar,
+.theme-firebug toolbar,
+.theme-firebug .devtools-toolbar {
+ border-bottom: 1px solid rgb(170, 188, 207) !important;
+ background-color: var(--theme-tab-toolbar-background) !important;
+ background-image: linear-gradient(rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0.2));
+ padding-inline-end: 4px;
+}
+
+/* The vbox for panel content also uses theme-toolbar class from some reason
+ but it shouldn't have the padding as defined above, so fix it here */
+.theme-firebug #toolbox-deck > .toolbox-panel.theme-toolbar {
+ padding-inline-end: 0;
+}
+
+/* Space around toolbar buttons */
+.theme-firebug .devtools-toolbar {
+ padding: 3px;
+}
+
+/* The height is the same for all toolbars and side panels tabs */
+.theme-firebug .theme-toolbar,
+.theme-firebug .devtools-sidebar-tabs tabs,
+.theme-firebug .devtools-toolbar {
+ height: 28px !important;
+}
+
+/* Do not set the fixed height for rule viewtoolbar. This toolbar
+ is changing its height to show pseudo classes. */
+.theme-firebug #ruleview-toolbar-container {
+ height: auto !important;
+}
+
+/* The Inspector panel side panels are using both
+ .devtools-toolbar and .theme-toolbar. We want the
+ proportional font for all labels in these toolbars */
+.theme-firebug .devtools-toolbar label,
+.theme-firebug .devtools-toolbar .label,
+.theme-firebug .theme-toolbar label,
+.theme-firebug .theme-toolbar .label {
+ font-family: var(--proportional-font-family);
+}
+
+/* Toolbar Buttons */
+
+.theme-firebug .theme-toolbar button,
+.theme-firebug .devtools-button,
+.theme-firebug toolbarbutton {
+ margin: 1px;
+ border-radius: 2px;
+ color: var(--theme-body-color);
+ line-height: var(--theme-toolbar-font-size);
+ font-size: var(--theme-toolbar-font-size);
+}
+
+.theme-firebug .theme-toolbar button,
+.theme-firebug .devtools-button {
+ border-width: 1px !important;
+ min-width: 24px;
+}
+
+.theme-firebug .devtools-toolbarbutton {
+ min-width: 24px;
+}
+
+
+.theme-firebug #element-picker {
+ min-height: 21px;
+}
diff --git a/devtools/client/themes/floating-scrollbars-dark-theme.css b/devtools/client/themes/floating-scrollbars-dark-theme.css
new file mode 100644
index 000000000..042fe28cc
--- /dev/null
+++ b/devtools/client/themes/floating-scrollbars-dark-theme.css
@@ -0,0 +1,59 @@
+@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
+@namespace html url("http://www.w3.org/1999/xhtml");
+
+/* Restrict all styles to `*|*:not(html|select) > scrollbar` so that scrollbars
+ inside a <select> are excluded (including them hides the select arrow on
+ Windows). We want to include both the root scrollbars for the document as
+ well as any overflow: scroll elements within the page, while excluding
+ <select>. */
+*|*:not(html|select) > scrollbar {
+ -moz-appearance: none !important;
+ position: relative;
+ background-color: transparent;
+ background-image: none;
+ z-index: 2147483647;
+ padding: 2px;
+ pointer-events: auto;
+}
+
+*|*:root[platform="mac"] > scrollbar,
+*|*:root[platform="mac"] *|*:not(html|select) > scrollbar {
+ border: none;
+}
+
+/* Scrollbar code will reset the margin to the correct side depending on
+ where layout actually puts the scrollbar */
+*|*:not(html|select) > scrollbar[orient="vertical"] {
+ margin-left: -10px;
+ min-width: 10px;
+ max-width: 10px;
+}
+
+*|*:not(html|select) > scrollbar[orient="horizontal"] {
+ margin-top: -10px;
+ min-height: 10px;
+ max-height: 10px;
+}
+
+*|*:not(html|select) > scrollbar thumb {
+ background-color: rgba(170, 170, 170, .2) !important; /* --toolbar-tab-hover */
+ -moz-appearance: none !important;
+ border-width: 0px !important;
+ border-radius: 3px !important;
+}
+
+*|*:root[platform="mac"] > scrollbar slider,
+*|*:root[platform="mac"] *|*:not(html|select) > scrollbar slider {
+ -moz-appearance: none !important;
+}
+
+*|*:root[platform="win"] > scrollbar scrollbarbutton,
+*|*:root[platform="linux"] > scrollbar scrollbarbutton,
+*|*:root[platform="win"] > scrollbar gripper,
+*|*:root[platform="linux"] > scrollbar gripper,
+*|*:root[platform="win"] *|*:not(html|select) > scrollbar scrollbarbutton,
+*|*:root[platform="linux"] *|*:not(html|select) > scrollbar scrollbarbutton,
+*|*:root[platform="win"] *|*:not(html|select) > scrollbar gripper,
+*|*:root[platform="linux"] *|*:not(html|select) > scrollbar gripper {
+ display: none;
+}
diff --git a/devtools/client/themes/floating-scrollbars-responsive-design.css b/devtools/client/themes/floating-scrollbars-responsive-design.css
new file mode 100644
index 000000000..7709bdd34
--- /dev/null
+++ b/devtools/client/themes/floating-scrollbars-responsive-design.css
@@ -0,0 +1,47 @@
+@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
+@namespace html url("http://www.w3.org/1999/xhtml");
+
+/* Restrict all styles to `*|*:not(html|select) > scrollbar` so that scrollbars
+ inside a <select> are excluded (including them hides the select arrow on
+ Windows). We want to include both the root scrollbars for the document as
+ well as any overflow: scroll elements within the page, while excluding
+ <select>. */
+*|*:not(html|select) > scrollbar {
+ -moz-appearance: none !important;
+ position: relative;
+ background-color: transparent;
+ background-image: none;
+ z-index: 2147483647;
+ padding: 2px;
+ border: none;
+}
+
+/* Scrollbar code will reset the margin to the correct side depending on
+ where layout actually puts the scrollbar */
+*|*:not(html|select) > scrollbar[orient="vertical"] {
+ margin-left: -10px;
+ min-width: 10px;
+ max-width: 10px;
+}
+
+*|*:not(html|select) > scrollbar[orient="horizontal"] {
+ margin-top: -10px;
+ min-height: 10px;
+ max-height: 10px;
+}
+
+*|*:not(html|select) > scrollbar slider {
+ -moz-appearance: none !important;
+}
+
+*|*:not(html|select) > scrollbar thumb {
+ -moz-appearance: none !important;
+ background-color: rgba(0,0,0,0.2);
+ border-width: 0px !important;
+ border-radius: 3px !important;
+}
+
+*|*:not(html|select) > scrollbar scrollbarbutton,
+*|*:not(html|select) > scrollbar gripper {
+ display: none;
+}
diff --git a/devtools/client/themes/fonts.css b/devtools/client/themes/fonts.css
new file mode 100644
index 000000000..8305e34f3
--- /dev/null
+++ b/devtools/client/themes/fonts.css
@@ -0,0 +1,128 @@
+/* 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/. */
+
+#sidebar-panel-fontinspector {
+ margin: 0;
+ display: flex;
+ flex-direction: column;
+ padding-bottom: 20px;
+ width: 100%;
+ height: 100%;
+}
+
+#sidebar-panel-fontinspector > .devtools-toolbar {
+ display: flex;
+}
+
+#font-container {
+ overflow: auto;
+ flex: auto;
+}
+
+#all-fonts {
+ padding: 0;
+ margin: 0;
+}
+
+#font-showall {
+ cursor: pointer;
+ flex-shrink: 0;
+}
+
+#font-showall:hover {
+ text-decoration: underline;
+}
+
+.dim > #font-container,
+.font:not(.has-code) .font-css-code,
+.font-is-local,
+.font-is-remote,
+.font.is-local .font-format-url,
+#font-template {
+ display: none;
+}
+
+.font.is-remote .font-is-remote,
+.font.is-local .font-is-local {
+ display: inline;
+}
+
+.font-format::before {
+ content: "(";
+}
+
+.font-format::after {
+ content: ")";
+}
+
+.preview-input-toolbar {
+ display: flex;
+ width: 100%;
+}
+
+.font-preview-container {
+ overflow-x: auto;
+}
+
+#font-preview-text-input {
+ margin-top: 1px;
+ margin-bottom: 1px;
+ padding-top: 0;
+ padding-bottom: 0;
+ flex: 1;
+}
+
+.font {
+ padding: 10px 10px;
+}
+
+.theme-dark .font {
+ border-bottom: 1px solid #444;
+}
+
+.theme-light .font {
+ border-bottom: 1px solid #DDD;
+}
+
+.font:last-of-type {
+ border-bottom: 0;
+}
+
+.theme-light .font:nth-child(even) {
+ background: #F4F4F4;
+}
+
+.font-preview {
+ margin-left: -4px;
+ height: 60px;
+ display: block;
+}
+
+.font-info {
+ display: block;
+}
+
+.font-name {
+ display: inline;
+}
+
+.font-css-code {
+ max-width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ padding: 5px;
+}
+
+.theme-light .font-css-code,
+.theme-light .font-url {
+ border: 1px solid #CCC;
+ background: white;
+}
+
+.theme-dark .font-css-code,
+.theme-dark .font-url {
+ border: 1px solid #333;
+ background: black;
+ color: white;
+}
diff --git a/devtools/client/themes/images/add.svg b/devtools/client/themes/images/add.svg
new file mode 100644
index 000000000..5eb43e78f
--- /dev/null
+++ b/devtools/client/themes/images/add.svg
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="#0b0b0b">
+ <path d="M8 8v5.5c0 .276-.224.5-.5.5s-.5-.224-.5-.5V8H1.5c-.276 0-.5-.224-.5-.5s.224-.5.5-.5H7V1.5c0-.276.224-.5.5-.5s.5.224.5.5V7h5.5c.276 0 .5.224.5.5s-.224.5-.5.5H8z"/>
+</svg>
diff --git a/devtools/client/themes/images/alerticon-warning.png b/devtools/client/themes/images/alerticon-warning.png
new file mode 100644
index 000000000..5c5d0aec5
--- /dev/null
+++ b/devtools/client/themes/images/alerticon-warning.png
Binary files differ
diff --git a/devtools/client/themes/images/alerticon-warning@2x.png b/devtools/client/themes/images/alerticon-warning@2x.png
new file mode 100644
index 000000000..dc3a3b108
--- /dev/null
+++ b/devtools/client/themes/images/alerticon-warning@2x.png
Binary files differ
diff --git a/devtools/client/themes/images/angle-swatch.svg b/devtools/client/themes/images/angle-swatch.svg
new file mode 100644
index 000000000..83f9393a9
--- /dev/null
+++ b/devtools/client/themes/images/angle-swatch.svg
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12" width="12px" height="12px">
+ <mask id="angle-mask">
+ <rect width="100%" height="100%" fill="#fff"/>
+ <polygon points="6 6, 12 12, 0 12, 0 0, 6 0, 6 6"/>
+ </mask>
+ <mask id="circle-mask">
+ <circle cx="6" cy="6" r="6" fill="#fff"/>
+ </mask>
+ <circle cx="6" cy="6" r="6" fill="#fff"/>
+ <circle cx="6" cy="6" r="6" mask="url(#angle-mask)" fill="#aeb0b1"/>
+ <line x1="6" y1="0" x2="6" y2="6" stroke-width="0.5" stroke="rgba(0,0,0,0.5)"></line>
+ <line x1="6" y1="6" x2="12" y2="12" stroke-width="0.5" stroke="rgba(0,0,0,0.5)" mask="url(#circle-mask)"></line>
+</svg>
diff --git a/devtools/client/themes/images/animation-fast-track.svg b/devtools/client/themes/images/animation-fast-track.svg
new file mode 100644
index 000000000..4723bf582
--- /dev/null
+++ b/devtools/client/themes/images/animation-fast-track.svg
@@ -0,0 +1,8 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0">
+ <clipPath id="thunderbolt" transform="scale(1.4)">
+ <path d="M5.75 0l-1 5.5 2 .5-3.5 6 1-5-2-.5z"/>
+ </clipPath>
+</svg>
diff --git a/devtools/client/themes/images/arrow-e.png b/devtools/client/themes/images/arrow-e.png
new file mode 100644
index 000000000..cfa950a1f
--- /dev/null
+++ b/devtools/client/themes/images/arrow-e.png
Binary files differ
diff --git a/devtools/client/themes/images/arrow-e@2x.png b/devtools/client/themes/images/arrow-e@2x.png
new file mode 100644
index 000000000..c628ca0a5
--- /dev/null
+++ b/devtools/client/themes/images/arrow-e@2x.png
Binary files differ
diff --git a/devtools/client/themes/images/breadcrumbs-scrollbutton.png b/devtools/client/themes/images/breadcrumbs-scrollbutton.png
new file mode 100644
index 000000000..19af4c042
--- /dev/null
+++ b/devtools/client/themes/images/breadcrumbs-scrollbutton.png
Binary files differ
diff --git a/devtools/client/themes/images/breadcrumbs-scrollbutton@2x.png b/devtools/client/themes/images/breadcrumbs-scrollbutton@2x.png
new file mode 100644
index 000000000..043702401
--- /dev/null
+++ b/devtools/client/themes/images/breadcrumbs-scrollbutton@2x.png
Binary files differ
diff --git a/devtools/client/themes/images/breakpoint.svg b/devtools/client/themes/images/breakpoint.svg
new file mode 100644
index 000000000..9be328b83
--- /dev/null
+++ b/devtools/client/themes/images/breakpoint.svg
@@ -0,0 +1,45 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="33" height="12" viewBox="0 0 33 12">
+ <defs>
+ <style>
+ use:not(:target) {
+ display: none;
+ }
+ #light {
+ fill: #46afe3;
+ }
+ #light-hover {
+ fill: #9aa6b3;
+ }
+ #light-active {
+ fill: #2cbb0f;
+ }
+ #light-conditional {
+ fill: #d97e00;
+ }
+ #dark {
+ fill: #46afe3;
+ }
+ #dark-hover {
+ fill: #3d454d;
+ }
+ #dark-active {
+ fill: #70bf53;
+ }
+ #dark-conditional {
+ fill: #d89b28;
+ }
+ </style>
+ <path id="base-path" d="M27.1,0H1C0.4,0,0,0.4,0,1v10c0,0.6,0.4,1,1,1h26.1 c0.6,0,1.2-0.3,1.5-0.7L33,6l-4.4-5.3C28.2,0.3,27.7,0,27.1,0z"/>
+ </defs>
+ <use xlink:href="#base-path" id="light"/>
+ <use xlink:href="#base-path" id="light-hover"/>
+ <use xlink:href="#base-path" id="light-active"/>
+ <use xlink:href="#base-path" id="light-conditional"/>
+ <use xlink:href="#base-path" id="dark"/>
+ <use xlink:href="#base-path" id="dark-hover"/>
+ <use xlink:href="#base-path" id="dark-active"/>
+ <use xlink:href="#base-path" id="dark-conditional"/>
+</svg>
diff --git a/devtools/client/themes/images/clear.svg b/devtools/client/themes/images/clear.svg
new file mode 100644
index 000000000..f9ba7087f
--- /dev/null
+++ b/devtools/client/themes/images/clear.svg
@@ -0,0 +1,7 @@
+<!-- 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/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="#0b0b0b">
+ <path d="M5 3h3V2c0-.003-3 0-3 0-.002 0 0 1 0 1zm-5 .5A.5.5 0 0 1 .494 3h12.012a.5.5 0 0 1 0 1H.494A.502.502 0 0 1 0 3.5zM4 3V2c0-.553.444-1 1-1h3c.552 0 1 .443 1 1v1H4zM5 11V6a.5.5 0 0 0-1 0v5a.5.5 0 1 0 1 0zM7 11V6a.5.5 0 0 0-1 0v5a.5.5 0 1 0 1 0zM9 11V6a.5.5 0 0 0-1 0v5a.5.5 0 1 0 1 0z"/>
+ <path d="M3 4v9h7V4H3zm0-1h7a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1z"/>
+</svg>
diff --git a/devtools/client/themes/images/close.svg b/devtools/client/themes/images/close.svg
new file mode 100644
index 000000000..c5a597bae
--- /dev/null
+++ b/devtools/client/themes/images/close.svg
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="#0b0b0b">
+ <path d="M8.707 8l4.23 4.23a.5.5 0 1 1-.707.707L8 8.707l-4.23 4.23a.5.5 0 1 1-.707-.707L7.293 8l-4.23-4.23a.5.5 0 1 1 .707-.707L8 7.293l4.23-4.23a.5.5 0 0 1 .707.707L8.707 8z" fill-rule="evenodd"/>
+</svg>
diff --git a/devtools/client/themes/images/command-console.svg b/devtools/client/themes/images/command-console.svg
new file mode 100644
index 000000000..77bb3a8e8
--- /dev/null
+++ b/devtools/client/themes/images/command-console.svg
@@ -0,0 +1,7 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="#0b0b0b">
+ <path d="M6.8 9.7c0-.2 0-.3-.2-.4L4.9 7.6c-.3-.3-.7-.3-.9 0s-.3.6 0 .9l1.3 1.4L4 11.3c-.3.3-.3.6 0 .9s.6.3.9 0l1.8-1.8c.1-.2.2-.5.1-.7z"/>
+ <path d="M14.2 2H1.8c-.4 0-.8.4-.8.9v11.2c0 .4.3.9.8.9h12.4c.4 0 .8-.4.8-.9V2.9c0-.7-.6-.9-.8-.9zM14 14H2V6h12v8zm0-9H2V3h12v2z"/>
+</svg>
diff --git a/devtools/client/themes/images/command-eyedropper.svg b/devtools/client/themes/images/command-eyedropper.svg
new file mode 100644
index 000000000..dff215857
--- /dev/null
+++ b/devtools/client/themes/images/command-eyedropper.svg
@@ -0,0 +1,7 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="#0b0b0b">
+ <path d="M8.3 2.9l4.9 4.9c.2.2.5.4.8.1.2-.2.1-.5-.1-.8L11.6 5l1.8-1.8c.2-.2.4-.5.1-.8-.2-.2-.5-.1-.8.1L11 4.3l-2.1-2c-.2-.3-.5-.4-.8-.2-.2.3-.1.6.2.8zM10.4 7.4l-6.1 6-2.4.8.7-2.4 6.2-6.1-.7-.7L2 11c-.1.1-.2.3-.2.4L1 13.7s-.1.7.1 1c.3.3.9.3 1.2.2l2.3-.8c.2-.1.3-.1.4-.3L11 8l-.6-.6z"/>
+ <path opacity="0.5" d="M7.1 7.1l-4.2 3.8-1.4 3.5 2.9-.6 2.8-2.7z"/>
+</svg>
diff --git a/devtools/client/themes/images/command-frames.svg b/devtools/client/themes/images/command-frames.svg
new file mode 100644
index 000000000..6462c43c7
--- /dev/null
+++ b/devtools/client/themes/images/command-frames.svg
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="#0b0b0b">
+ <path d="M14.2 2H1.8c-.4 0-.8.4-.8.9v11.2c0 .4.3.9.8.9h12.4c.4 0 .8-.4.8-.9V2.9s-.6-.9-.8-.9zM8 14H2v-4h6v4zm6 0H9v-4h5v4zm0-5H2V3h12v6z"/>
+</svg>
diff --git a/devtools/client/themes/images/command-measure.svg b/devtools/client/themes/images/command-measure.svg
new file mode 100644
index 000000000..0aa830f15
--- /dev/null
+++ b/devtools/client/themes/images/command-measure.svg
@@ -0,0 +1,7 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="#0b0b0b">
+ <path d="M1 11h14V6H1v5zm15 .2c0 .5-.4.8-.8.8H.8c-.4 0-.8-.4-.8-.8V5.8c0-.4.4-.8.8-.8h14.3c.5 0 .9.4.9.8v5.4z"/>
+ <path d="M9 6v3c0 .2.3.4.5.4s.5-.2.5-.4V6H9zM13 6v3c0 .2.3.4.5.4.2-.1.5-.2.5-.4V6h-1zM5.1 5.7L4 6v2.8c0 .2.4.4.6.4s.5-.2.5-.4V5.7zM11 5v2.7c0 .2.3.4.5.4s.5-.2.5-.4V5h-1zM6 5.1v2.6c0 .2.5.4.7.4.2 0 .6-.2.6-.4l-.1-2.6H6zM2 5.1v2.6c0 .2.3.4.5.4s.5-.2.5-.4V5.1H2z"/>
+</svg>
diff --git a/devtools/client/themes/images/command-noautohide.svg b/devtools/client/themes/images/command-noautohide.svg
new file mode 100755
index 000000000..ac53c7d54
--- /dev/null
+++ b/devtools/client/themes/images/command-noautohide.svg
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="#0b0b0b">
+ <path d="M2 1.99v4.02C2 6 2 6 1.99 6h4.02C6 6 6 6 6 6.01V1.99C6 2 6 2 6.01 2H1.99C2 2 2 2 2 1.99zm-1 0c0-.546.451-.99.99-.99h4.02c.546 0 .99.451.99.99v4.02c0 .546-.451.99-.99.99H1.99A.996.996 0 0 1 1 6.01V1.99zM10 1.99v4.02C10 6 10 6 9.99 6h4.02C14 6 14 6 14 6.01V1.99c0 .01 0 .01.01.01H9.99C10 2 10 2 10 1.99zm-1 0c0-.546.451-.99.99-.99h4.02c.546 0 .99.451.99.99v4.02c0 .546-.451.99-.99.99H9.99A.996.996 0 0 1 9 6.01V1.99zM10 9.99v4.02c0-.01 0-.01-.01-.01h4.02c-.01 0-.01 0-.01.01V9.99c0 .01 0 .01.01.01H9.99c.01 0 .01 0 .01-.01zm-1 0c0-.546.451-.99.99-.99h4.02c.546 0 .99.451.99.99v4.02c0 .546-.451.99-.99.99H9.99a.996.996 0 0 1-.99-.99V9.99zM2 9.99v4.02C2 14 2 14 1.99 14h4.02C6 14 6 14 6 14.01V9.99c0 .01 0 .01.01.01H1.99C2 10 2 10 2 9.99zm-1 0c0-.546.451-.99.99-.99h4.02c.546 0 .99.451.99.99v4.02c0 .546-.451.99-.99.99H1.99a.996.996 0 0 1-.99-.99V9.99z"/>
+</svg>
diff --git a/devtools/client/themes/images/command-paintflashing.svg b/devtools/client/themes/images/command-paintflashing.svg
new file mode 100644
index 000000000..dfd627a8d
--- /dev/null
+++ b/devtools/client/themes/images/command-paintflashing.svg
@@ -0,0 +1,7 @@
+<!-- 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/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="#0b0b0b">
+ <path d="M3 6.997v6.006c0-.006.003-.003.002-.003h9.996c-.001 0 .002-.003.002.003V6.997c0 .006-.003.003-.002.003H3.002C3.003 7 3 7.003 3 6.997zm-1 0C2 6.447 2.456 6 3.002 6h9.996C13.55 6 14 6.453 14 6.997v6.006c0 .55-.456.997-1.002.997H3.002A1.004 1.004 0 0 1 2 13.003V6.997zM8.5 4V1.5a.5.5 0 0 0-1 0V4H4a.5.5 0 0 0 0 1h8a.5.5 0 1 0 0-1H8.5z"/>
+ <path fill-opacity=".3" d="M13 10v3H3v-3z"/>
+</svg>
diff --git a/devtools/client/themes/images/command-pick.svg b/devtools/client/themes/images/command-pick.svg
new file mode 100644
index 000000000..382d236f9
--- /dev/null
+++ b/devtools/client/themes/images/command-pick.svg
@@ -0,0 +1,9 @@
+<!-- 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/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="#0b0b0b">
+ <path d="M15 7.667V3.002A1.01 1.01 0 0 0 13.993 2H2.007C1.45 2 1 2.449 1 3.002v9.996C1 13.544 1.45 14 2.007 14h6.818l-.37-1H2V3h12v4.334l1 .333z"/>
+ <path fill-opacity=".3" d="M9 8l1.981 5.843 4.044-3.966z"/>
+ <path d="M8.526 8.16l1.982 5.844a.5.5 0 0 0 .824.196l4.043-3.966a.5.5 0 0 0-.202-.835L9.15 7.523a.5.5 0 0 0-.623.638zm.948-.32l-.623.637 6.025 1.877-.201-.834-4.044 3.966.824.197-1.981-5.844z"/>
+ <path d="M12.674 12.39l1.973 1.964a.5.5 0 1 0 .706-.708L13.38 11.68a.5.5 0 0 0-.706.709z"/>
+</svg>
diff --git a/devtools/client/themes/images/command-responsivemode.svg b/devtools/client/themes/images/command-responsivemode.svg
new file mode 100644
index 000000000..583e59c1a
--- /dev/null
+++ b/devtools/client/themes/images/command-responsivemode.svg
@@ -0,0 +1,9 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" stroke="#0b0b0b" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round" fill="transparent">
+ <circle cx="-5.5" cy="8.5" r="1"/>
+ <path d="M3.8 1.5h7.5v13H3.8z"/>
+ <path d="M11.2 8.5H3.8"/>
+ <circle cx="7.5" cy="11.5" r="1"/>
+</svg>
diff --git a/devtools/client/themes/images/command-rulers.svg b/devtools/client/themes/images/command-rulers.svg
new file mode 100644
index 000000000..995eb53e4
--- /dev/null
+++ b/devtools/client/themes/images/command-rulers.svg
@@ -0,0 +1,7 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="#0b0b0b">
+ <path d="M11.4 3c-.2 0-.4.2-.4.4v2.5c0 .2.3.4.5.4s.5-.2.5-.4V3.4c0-.2-.3-.4-.5-.4M13.5 3.2c-.2 0-.5.2-.5.4v1.5c0 .2.3.4.5.4s.5-.2.5-.4V3.6c0-.2-.3-.4-.5-.4M9.6 3.2c-.2 0-.6.2-.6.4v1.5c0 .2.3.4.6.4.2 0 .4-.2.4-.4V3.6c0-.2-.2-.4-.4-.4M7.4 3c-.2 0-.4.2-.4.4v2.5c0 .2.3.4.5.4s.5-.2.5-.4V3.4c0-.2-.2-.4-.6-.4M5.5 3.2c-.3 0-.5.2-.5.4v1.5c0 .2.3.4.5.4s.5-.2.5-.4V3.6c0-.2-.3-.4-.5-.4M4.3 8.5c0-.2-.2-.5-.4-.5H2.4c-.2 0-.4.3-.4.5s.2.5.4.5h1.5c.2 0 .4-.3.4-.5M4.3 12.5c0-.2-.2-.5-.4-.5H2.4c-.2 0-.4.3-.4.5s.2.5.4.5h1.5c.2 0 .4-.3.4-.5M5.1 10.5c0-.2-.2-.5-.4-.5H3.2c-.2 0-.4.3-.4.5s.2.5.4.5h1.5c.2 0 .4-.3.4-.5M5.1 6.5c0-.2-.2-.5-.4-.5H3.2c-.2 0-.4.3-.4.5s.2.5.4.5h1.5c.2 0 .4-.3.4-.5"/>
+ <path d="M15.1 3H3.5C2.9 3 2 3.6 2 4.1V14c0 .6 1 1 1.5 1h3.2c.6 0 1.3-.5 1.3-1V9h7c.6 0 1-.4 1-.9v-4c0-1-.4-1.1-.9-1.1zM15 8H7.4c-.6 0-.5.1-.4 0v6H3V4h12v4z"/>
+</svg>
diff --git a/devtools/client/themes/images/command-screenshot.svg b/devtools/client/themes/images/command-screenshot.svg
new file mode 100644
index 000000000..8e81a334f
--- /dev/null
+++ b/devtools/client/themes/images/command-screenshot.svg
@@ -0,0 +1,7 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="#0b0b0b">
+ <path d="M13.6 5H11V3.5C11 2.7 10 2 9.2 2H6.8C6 2 5 2.7 5 3.5V5H2.4C1.6 5 1 5.6 1 6.4v6.5c0 .8.6 1.1 1.4 1.1h11.2c.8 0 1.4-.3 1.4-1.1V6.4c0-.8-.6-1.4-1.4-1.4zm.4 8H2V6h4V3h4v3h3.9l.1 7z"/>
+ <path d="M8 6.8c-1.3 0-2.4 1.1-2.4 2.4s1.1 2.4 2.4 2.4 2.4-1.1 2.4-2.4c0-1.3-1.1-2.4-2.4-2.4zm0 3.5c-.7 0-1.2-.5-1.2-1.1S7.3 8.1 8 8.1s1.2.5 1.2 1.1-.5 1.1-1.2 1.1z"/>
+</svg>
diff --git a/devtools/client/themes/images/commandline-icon.svg b/devtools/client/themes/images/commandline-icon.svg
new file mode 100644
index 000000000..429d6a73b
--- /dev/null
+++ b/devtools/client/themes/images/commandline-icon.svg
@@ -0,0 +1,42 @@
+<!-- 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/. -->
+<svg width="16px" height="16px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
+ <defs>
+ <style>
+ g {
+ display: none;
+ }
+
+ #light-theme:target,
+ #light-theme-focus:target ~ #light-theme,
+ #dark-theme:target,
+ #dark-theme-focus:target ~ #dark-theme {
+ display: inline;
+ }
+
+ #light-theme-focus:target ~ #light-theme {
+ fill: #4A90E2;
+ }
+ #dark-theme-focus:target ~ #dark-theme {
+ fill: #00FF7F;
+ }
+
+ /* Unfocused states */
+ #light-theme,
+ #dark-theme {
+ fill: rgba(128, 128, 128, .5);
+ }
+ </style>
+ </defs>
+ <g id="light-theme-focus"/>
+ <g id="light-theme">
+ <path d="M7.29 13.907l7-5a.5.5 0 0 0 .033-.789l-6.5-5.5a.5.5 0 1 0-.646.764l6.5 5.5.032-.789-7 5a.5.5 0 1 0 .582.814z"/>
+ <path d="M2.29 13.907l7-5a.5.5 0 0 0 .033-.789l-6.5-5.5a.5.5 0 1 0-.646.764l6.5 5.5.032-.789-7 5a.5.5 0 1 0 .582.814z"/>
+ </g>
+ <g id="dark-theme-focus"/>
+ <g id="dark-theme">
+ <path d="M7.29 13.907l7-5a.5.5 0 0 0 .033-.789l-6.5-5.5a.5.5 0 1 0-.646.764l6.5 5.5.032-.789-7 5a.5.5 0 1 0 .582.814z"/>
+ <path d="M2.29 13.907l7-5a.5.5 0 0 0 .033-.789l-6.5-5.5a.5.5 0 1 0-.646.764l6.5 5.5.032-.789-7 5a.5.5 0 1 0 .582.814z"/>
+ </g>
+</svg> \ No newline at end of file
diff --git a/devtools/client/themes/images/controls.png b/devtools/client/themes/images/controls.png
new file mode 100644
index 000000000..569c266e4
--- /dev/null
+++ b/devtools/client/themes/images/controls.png
Binary files differ
diff --git a/devtools/client/themes/images/controls@2x.png b/devtools/client/themes/images/controls@2x.png
new file mode 100644
index 000000000..fb062516d
--- /dev/null
+++ b/devtools/client/themes/images/controls@2x.png
Binary files differ
diff --git a/devtools/client/themes/images/cubic-bezier-swatch.png b/devtools/client/themes/images/cubic-bezier-swatch.png
new file mode 100644
index 000000000..2dcb58a2a
--- /dev/null
+++ b/devtools/client/themes/images/cubic-bezier-swatch.png
Binary files differ
diff --git a/devtools/client/themes/images/cubic-bezier-swatch@2x.png b/devtools/client/themes/images/cubic-bezier-swatch@2x.png
new file mode 100644
index 000000000..64dbef89b
--- /dev/null
+++ b/devtools/client/themes/images/cubic-bezier-swatch@2x.png
Binary files differ
diff --git a/devtools/client/themes/images/debugger-step-in.svg b/devtools/client/themes/images/debugger-step-in.svg
new file mode 100644
index 000000000..a74add83b
--- /dev/null
+++ b/devtools/client/themes/images/debugger-step-in.svg
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="#0b0b0b">
+ <path d="M1.5 14.042h4.095a.5.5 0 0 0 0-1H1.5a.5.5 0 1 0 0 1zM7.5 3v6.983L4.364 6.657a.5.5 0 0 0-.728.686l4 4.243a.51.51 0 0 0 .021.02.5.5 0 0 0 .71-.024l3.997-4.239a.5.5 0 1 0-.728-.686L8.5 9.983V2.5a.5.5 0 0 0-.536-.5H1.536C1.24 2 1 2.224 1 2.5s.24.5.536.5H7.5zM10.5 14.042h4.095a.5.5 0 0 0 0-1H10.5a.5.5 0 1 0 0 1z"/>
+</svg>
diff --git a/devtools/client/themes/images/debugger-step-out.svg b/devtools/client/themes/images/debugger-step-out.svg
new file mode 100644
index 000000000..abef878f1
--- /dev/null
+++ b/devtools/client/themes/images/debugger-step-out.svg
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="#0b0b0b">
+ <path d="M5 13.5H1a.5.5 0 1 0 0 1h4a.5.5 0 1 0 0-1zM12 13.5H8a.5.5 0 1 0 0 1h4a.5.5 0 1 0 0-1zM6.11 5.012A.427.427 0 0 1 6.21 5h7.083L9.646 1.354a.5.5 0 1 1 .708-.708l4.5 4.5a.498.498 0 0 1 0 .708l-4.5 4.5a.5.5 0 0 1-.708-.708L13.293 6H6.5v5.5a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .61-.488z"/>
+</svg>
diff --git a/devtools/client/themes/images/debugger-step-over.png b/devtools/client/themes/images/debugger-step-over.png
new file mode 100644
index 000000000..ec28fc05e
--- /dev/null
+++ b/devtools/client/themes/images/debugger-step-over.png
Binary files differ
diff --git a/devtools/client/themes/images/debugger-step-over.svg b/devtools/client/themes/images/debugger-step-over.svg
new file mode 100644
index 000000000..bdd5bde37
--- /dev/null
+++ b/devtools/client/themes/images/debugger-step-over.svg
@@ -0,0 +1,7 @@
+<!-- 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/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="#0b0b0b">
+ <path d="M13.297 6.912C12.595 4.39 10.167 2.5 7.398 2.5A5.898 5.898 0 0 0 1.5 8.398a.5.5 0 0 0 1 0A4.898 4.898 0 0 1 7.398 3.5c2.75 0 5.102 2.236 5.102 4.898v.004L8.669 7.029a.5.5 0 0 0-.338.942l4.462 1.598a.5.5 0 0 0 .651-.34.506.506 0 0 0 .02-.043l2-5a.5.5 0 1 0-.928-.372l-1.24 3.098z"/>
+ <circle cx="7" cy="12" r="1"/>
+</svg>
diff --git a/devtools/client/themes/images/debugger-step-over@2x.png b/devtools/client/themes/images/debugger-step-over@2x.png
new file mode 100644
index 000000000..452f0d459
--- /dev/null
+++ b/devtools/client/themes/images/debugger-step-over@2x.png
Binary files differ
diff --git a/devtools/client/themes/images/debugger-toggleBreakpoints.svg b/devtools/client/themes/images/debugger-toggleBreakpoints.svg
new file mode 100644
index 000000000..9d0b23004
--- /dev/null
+++ b/devtools/client/themes/images/debugger-toggleBreakpoints.svg
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+<svg width="32" height="16" viewBox="0 0 32 16" xmlns="http://www.w3.org/2000/svg" fill="#0b0b0b">
+ <path d="M3.233 11.25l-.417 1H1.712C.763 12.25 0 11.574 0 10.747V6.503C0 5.675.755 5 1.712 5h4.127l-.417 1H1.597C1.257 6 1 6.225 1 6.503v4.244c0 .277.267.503.597.503h1.636zM7.405 11.02L7 12.056c.865.01 2.212-.024 2.315-.04.112-.016.112-.016.185-.035.075-.02.156-.046.251-.082.152-.056.349-.138.592-.244.415-.182.962-.435 1.612-.744l.138-.066a179.35 179.35 0 0 0 2.255-1.094c1.191-.546 1.191-2.074-.025-2.632l-.737-.34A3547.554 3547.554 0 0 0 9.732 5c-.029.11-.065.222-.11.336l-.232.596c.894.408 4.56 2.107 4.56 2.107.458.21.458.596 0 .806L9.197 11.02H7.405zM20.462 14.192l5-12a.5.5 0 0 0-.924-.384l-5 12a.5.5 0 0 0 .924.384zM19.233 11.25l-.417 1h-1.104c-.949 0-1.712-.676-1.712-1.503V6.503C16 5.675 16.755 5 17.712 5h4.127l-.417 1h-3.825c-.34 0-.597.225-.597.503v4.244c0 .277.267.503.597.503h1.636zM23.405 11.02L23 12.056c.865.01 2.212-.024 2.315-.04.112-.016.112-.016.185-.035.075-.02.156-.046.251-.082.152-.056.349-.138.592-.244.415-.182.962-.435 1.612-.744l.138-.066a179.35 179.35 0 0 0 2.255-1.094c1.191-.546 1.191-2.074-.025-2.632l-.737-.34A3547.554 3547.554 0 0 0 25.732 5c-.029.11-.065.222-.11.336l-.232.596c.894.408 4.56 2.107 4.56 2.107.458.21.458.596 0 .806l-4.753 2.174h-1.792z"/>
+</svg>
diff --git a/devtools/client/themes/images/debugging-addons.svg b/devtools/client/themes/images/debugging-addons.svg
new file mode 100644
index 000000000..8a98d0275
--- /dev/null
+++ b/devtools/client/themes/images/debugging-addons.svg
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+<svg height="16" width="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18" fill="#fbfbfb">
+ <path d="M12,17c0.5,0,1-0.5,1-1v-4c0,0,0.2-0.8,0.8-0.8c0.6,0,0.6,0.8,1.8,0.8 c0.6,0,1.5-0.2,1.5-2c0-1.8-0.9-2-1.5-2c-1.1,0-1.2,0.8-1.8,0.8C13.2,8.8,13,8,13,8V6c0-0.6-0.4-1-1-1H9c0,0-0.8-0.1-0.8-0.8 S9,3.6,9,2.5C9,1.9,8.8,1,7,1S5,1.9,5,2.5c0,1.1,0.8,1.2,0.8,1.8S5,5,5,5H2C1.4,5,1,5.4,1,6l0,2.5c0,0-0.1,1.5,1.1,1.5 c0.8,0,0.9-1,1.9-1c0.5,0,1,0.5,1,1.6c0,1-0.5,1.6-1,1.6c-1,0-1.1-1-1.9-1C0.9,11,1,12.5,1,12.5L1,16c0,0.6,0.4,1,1,1h3.9 c0,0,1.5,0.1,1.5-1.1c0-0.8-1-0.9-1-1.9c0-0.5,0.7-1.2,1.8-1.2s1.9,0.7,1.9,1.2c0,1-1,1.1-1,1.9c0,1.2,1.5,1.1,1.5,1.1H12z" />
+</svg>
diff --git a/devtools/client/themes/images/debugging-devices.svg b/devtools/client/themes/images/debugging-devices.svg
new file mode 100644
index 000000000..be2cd5af6
--- /dev/null
+++ b/devtools/client/themes/images/debugging-devices.svg
@@ -0,0 +1,7 @@
+<!-- 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/. -->
+<svg height="16" width="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="#fbfbfb">
+ <path d="M10.7,14.4H5.3c-1.2,0-1.5-0.4-1.5-1.2V2.8c0-0.9,0.3-1.2,1.5-1.2h5.3c1.1,0,1.5,0.3,1.5,1.2v10.3
+ C12.2,14.1,11.8,14.4,10.7,14.4z M5,12.6h6V3.5H5V12.6z"/>
+</svg>
diff --git a/devtools/client/themes/images/debugging-tabs.svg b/devtools/client/themes/images/debugging-tabs.svg
new file mode 100644
index 000000000..91ad8e925
--- /dev/null
+++ b/devtools/client/themes/images/debugging-tabs.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18">
+ <path d="M17,12v2a1,1,0,0,1-1,1H2a1,1,0,0,1-1-1V12a1,1,0,0,1,1-1H1.142c2.3,0,2.536-1.773,2.874-4,0.351-2.316.083-4,3.13-4h3.707C13.917,3,13.647,4.684,14,7c0.34,2.228.582,4,2.89,4H16A1,1,0,0,1,17,12Z" fill="white"/>
+</svg>
diff --git a/devtools/client/themes/images/debugging-workers.svg b/devtools/client/themes/images/debugging-workers.svg
new file mode 100644
index 000000000..c04c63252
--- /dev/null
+++ b/devtools/client/themes/images/debugging-workers.svg
@@ -0,0 +1,11 @@
+<!-- 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/. -->
+<svg height="16" width="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="#fbfbfb">
+<path d="M14.6,6.1L13.5,5l0,0c0.1-0.1,0.2-0.4,0.2-0.6c0-0.2-0.1-0.4-0.2-0.6l-0.4-0.4c-0.3-0.3-0.8-0.3-1.1,0l0,0
+ L10.5,2c-0.2-0.2-0.3-0.2-0.5-0.2c-0.2,0-0.3,0.1-0.5,0.2L8.3,3.2C8.1,3.3,8.1,3.4,8.1,3.6S8.2,4,8.3,4.1l1.6,1.6L7.8,7.8L5.6,5.7
+ l1.5-1.5C7.3,4,7.4,3.8,7.4,3.6c0-0.2-0.1-0.4-0.2-0.6l-1-1C5.8,1.7,5.3,1.7,5,2L0.9,6.1C0.7,6.3,0.6,6.5,0.6,6.7
+ c0,0.2,0.1,0.4,0.2,0.6l1,1c0.3,0.3,0.9,0.3,1.2,0l1.4-1.4l2,2.1l-3.4,3.3c-0.3,0.3-0.3,0.8,0,1.1l0.3,0.3c0.3,0.3,0.8,0.3,1.1,0
+ l3.3-3.4l3.3,3.4c0.1,0.1,0.3,0.2,0.6,0.2c0.2,0,0.4-0.1,0.6-0.2l0.3-0.3c0.3-0.3,0.3-0.8,0-1.1L9,9l2-2.1l1.4,1.4
+ c0.1,0.1,2.3,1.1,2.7,0.7C15.5,8.6,14.8,6.3,14.6,6.1z"/>
+</svg>
diff --git a/devtools/client/themes/images/diff.svg b/devtools/client/themes/images/diff.svg
new file mode 100644
index 000000000..ce237eb75
--- /dev/null
+++ b/devtools/client/themes/images/diff.svg
@@ -0,0 +1,9 @@
+<!-- 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/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="#0b0b0b">
+ <path d="M6 13A5 5 0 1 0 6 3a5 5 0 0 0 0 10zm0-.91a4.09 4.09 0 1 1 0-8.18 4.09 4.09 0 0 1 0 8.18z"/>
+ <path d="M10 13a5 5 0 1 0 0-10 5 5 0 0 0 0 10zm0-.91a4.09 4.09 0 1 1 0-8.18 4.09 4.09 0 0 1 0 8.18z"/>
+ <path d="M7.146 8.854l1 1a.5.5 0 0 0 .708-.708l-1-1a.5.5 0 1 0-.708.708zM7.146 6.854l1 1a.5.5 0 1 0 .708-.708l-1-1a.5.5 0 1 0-.708.708z"/>
+ <path d="M12.656 11.723c-2.044 1.169-3.872 1.015-4.282.577-.41-.438 2.115-1.269 2.115-3.925 0-2.657-2.115-4.827-2.115-4.827s2.919-.47 4.282.624c1.364 1.094 2.12 1.975 1.85 3.828-.103.703.194 2.555-1.85 3.723z" fill-opacity=".3"/>
+</svg>
diff --git a/devtools/client/themes/images/dock-bottom.svg b/devtools/client/themes/images/dock-bottom.svg
new file mode 100644
index 000000000..09ce70258
--- /dev/null
+++ b/devtools/client/themes/images/dock-bottom.svg
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="#0b0b0b">
+ <path d="M10.004 3H.996C.999 3 1 3 1 3.002v9.996c0-.001.003.002-.004.002h9.008c-.003 0-.004 0-.004-.002V3.002c0 .001-.003-.002.004-.002zm0-1c.55 0 .996.456.996 1.002v9.996A.998.998 0 0 1 10.004 14H.996C.446 14 0 13.544 0 12.998V3.002A.998.998 0 0 1 .996 2h9.008zm-.41 8H.996v1h9.01v-1h-.41z"/>
+</svg>
diff --git a/devtools/client/themes/images/dock-side.svg b/devtools/client/themes/images/dock-side.svg
new file mode 100644
index 000000000..3855ca614
--- /dev/null
+++ b/devtools/client/themes/images/dock-side.svg
@@ -0,0 +1,3 @@
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="#0b0b0b">
+ <path d="M1 2.996v9.008c0-.003 0-.004.002-.004h9.996c-.001 0 .002-.003.002.004V2.996c0 .003 0 .004-.002.004H1.002C1.003 3 1 3.003 1 2.996zm-1 0C0 2.446.456 2 1.002 2h9.996A.998.998 0 0 1 12 2.996v9.008c0 .55-.456.996-1.002.996H1.002A.998.998 0 0 1 0 12.004V2.996zm8 .413V12h1V3H8v.41z"/>
+</svg>
diff --git a/devtools/client/themes/images/dock-undock.svg b/devtools/client/themes/images/dock-undock.svg
new file mode 100644
index 000000000..d4b100a03
--- /dev/null
+++ b/devtools/client/themes/images/dock-undock.svg
@@ -0,0 +1,8 @@
+<!-- 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/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="#0b0b0b">
+ <path d="M13.003 1.941H6.997c.008 0 .003.004.003.008v6.102c0 .004.004.008-.003.008h6.006c-.008 0-.003-.004-.003-.008V1.949c0-.004-.004-.008.003-.008zm0-.941c.55 0 .997.43.997.95v6.1c0 .525-.453.95-.997.95H6.997C6.447 9 6 8.57 6 8.05v-6.1c0-.525.453-.95.997-.95h6.006z"/>
+ <path d="M9 9.91v-.278h1v1.183c0 .516-.453.935-.997.935H2.997c-.55 0-.997-.43-.997-.95V4.7c0-.525.444-.95 1.006-.95h2.288v.941H3.006C3 4.691 3 4.691 3 4.7v6.102c0 .004.004.008-.003.008h6.006c-.004 0-.003-.001-.003.006v-.248-.657-.278h1v1.183c0 .516-.453.935-.997.935H2.997c-.55 0-.997-.43-.997-.95V4.7c0-.525.444-.95 1.006-.95h2.288v.941H3.006C3 4.691 3 4.691 3 4.7v6.102c0 .004.004.008-.003.008h6.006c-.004 0-.003-.001-.003.006v-.248-.657z"/>
+ <path d="M12.52 5H6.976v1h6.046V5zM6.5 7H2.975v1H7V7z"/>
+</svg>
diff --git a/devtools/client/themes/images/dropmarker.svg b/devtools/client/themes/images/dropmarker.svg
new file mode 100644
index 000000000..7592790c4
--- /dev/null
+++ b/devtools/client/themes/images/dropmarker.svg
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="8" height="4" viewBox="0 0 8 4">
+ <polygon points="0,0 4,4 8,0" fill="#b6babf"/>
+</svg>
diff --git a/devtools/client/themes/images/editor-error.png b/devtools/client/themes/images/editor-error.png
new file mode 100644
index 000000000..39ef81e20
--- /dev/null
+++ b/devtools/client/themes/images/editor-error.png
Binary files differ
diff --git a/devtools/client/themes/images/emojis/emoji-command-pick.svg b/devtools/client/themes/images/emojis/emoji-command-pick.svg
new file mode 100755
index 000000000..4be436766
--- /dev/null
+++ b/devtools/client/themes/images/emojis/emoji-command-pick.svg
@@ -0,0 +1,7 @@
+<!-- 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/. -->
+<svg height="16" width="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 72 72" opacity="1.0">
+ <path fill="#C89979" d="M57.2 20.6c-1 0-2 .4-2.7 1 0 0-.1 0-.2.1-.8.8-1.6.8-2.4 0-.7-.7-1.7-1.1-2.8-1.1-1.1 0-2 .4-2.7 1 0 0-.1 0-.2.1-.8.8-1.6.8-2.4.1-.7-.7-1.8-1.2-2.9-1.2-1.1 0-2 .4-2.7 1 0 0-.1 0-.2.1-.3.3-.6.5-.9.6V4c0-2.2-1.8-4.1-4.1-4.1S29 1.7 29 4v35c-1-1.2-1.9-1.6-3.2-2.9-7.2-6.9-10.7-5-12.3-3.6-1.5 1.5-1.1 4 .5 5.5 7.9 6.8 12.1 17.5 13.8 20.8 0 0 0 .1.1.1.2.4.4.7.5.8 1 1.4 2.3 2.5 3.8 3.2V69c0 1.6 1.3 2.9 2.9 2.9h18.6c1.6 0 2.9-1.3 2.9-2.9v-8c2.8-2.3 4.6-5.9 4.6-9.8V24.7c.1-2.3-1.8-4.1-4-4.1z"/>
+ <path fill="#AD7E5E" d="M48.6 63.9h-.2H36.1c-1.4 0-2.7-.3-3.9-.9v1.3c0 1.2 1 2.2 2.2 2.2h20c1.2 0 2.2-1 2.2-2.2V61c-2.2 1.8-5 2.9-8 2.9z"/>
+</svg> \ No newline at end of file
diff --git a/devtools/client/themes/images/emojis/emoji-tool-canvas.svg b/devtools/client/themes/images/emojis/emoji-tool-canvas.svg
new file mode 100644
index 000000000..97d8a91a0
--- /dev/null
+++ b/devtools/client/themes/images/emojis/emoji-tool-canvas.svg
@@ -0,0 +1,11 @@
+<!-- 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/. -->
+<svg height="16" width="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" opacity="1.0">
+ <path fill="#98D4FA" d="M488.95 10.383H21.717c-6.6 0-12 5.4-12 12v467.234c0 6.6 5.4 12 12 12H488.95c6.6 0 12-5.4 12-12V22.383c0-6.6-5.4-12-12-12z"/>
+ <path fill="#0096D1" d="M215.218 489.383c0 4.1.107 8.176.278 12.234h40.953c-.2-4.053-.306-8.132-.306-12.234 0-135.203 109.604-244.807 244.807-244.807V203.65c-157.805 0-285.732 127.927-285.732 285.733z"/>
+ <path fill="#21C14B" d="M167.45 489.383c0 4.098.1 8.172.247 12.234h47.8c-.172-4.06-.28-8.134-.28-12.234 0-157.806 127.928-285.733 285.734-285.733v-47.768c-184.187 0-333.5 149.313-333.5 333.5z"/>
+ <path fill="#FFD469" d="M121.973 489.383c0 4.094.073 8.17.2 12.234h45.524c-.147-4.062-.247-8.136-.247-12.234 0-184.187 149.313-333.5 333.5-333.5v-45.478c-209.303 0-378.977 169.674-378.977 378.978z"/>
+ <path fill="#FF7B39" d="M70.294 489.383c0 4.092.063 8.17.176 12.234h51.704c-.13-4.063-.2-8.14-.2-12.234 0-209.304 169.673-378.978 378.977-378.978v-51.68c-237.845 0-430.656 192.813-430.656 430.658z"/>
+ <path fill="#FF473E" d="M488.95 10.383H400.24c-221.962 46.434-388.67 243.245-388.67 479 0 2.228.017 4.45.047 6.672 2.138 3.335 5.868 5.563 10.102 5.563H70.47c-.113-4.065-.176-8.142-.176-12.234 0-237.845 192.812-430.657 430.657-430.657V22.383c0-6.6-5.4-12-12-12z"/>
+</svg> \ No newline at end of file
diff --git a/devtools/client/themes/images/emojis/emoji-tool-debugger.svg b/devtools/client/themes/images/emojis/emoji-tool-debugger.svg
new file mode 100644
index 000000000..07b9ebbb9
--- /dev/null
+++ b/devtools/client/themes/images/emojis/emoji-tool-debugger.svg
@@ -0,0 +1,11 @@
+<!-- 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/. -->
+<svg height="16" width="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 72 72" opacity="1.0">
+ <path fill="#EDC0A2" d="M44.3 11.6c-.2 0-.5 0-.7.1V6.4c0-2.2-1.8-4-4-4-1.7 0-3.1 1-3.7 2.4-.6-1.4-2-2.4-3.7-2.4-2.2 0-4 1.8-4 4v5.3c-.2 0-.5-.1-.7-.1-2.2 0-4 1.8-4 4v31.7c0 2.2 1.8 4 4 4 1.1 0 2.2-.5 2.9-1.2.5.3 1.2.5 1.8.5v.2h10.1c.6.3 1.2.5 2 .5 2.2 0 4-1.8 4-4V15.6c0-2.2-1.7-4-4-4z"/>
+ <path fill="#357BA8" d="M66.7 44.5c-.3-.5-.6-.9-1-1.3-3.5-3.5-10.8-1.7-16.4 3.9-5.6 5.6-7.4 13-3.9 16.4.3.3.5.5.8.7L54 72h17.8l.2-22.2-5.3-5.3z"/>
+ <path fill="#FFD3B6" d="M61.1 42.5c-.2-.4-.5-.8-.9-1.1-6.8-6-14.5-14.8-14.5-17.9V7.7c0-2.2-1.8-4-4-4s-4 1.8-4 4v32.5c0 12.5 7.3 19.7 7.6 20 .6.5 1.4.9 2.2 1.1h.8c1.8 0 5.5-.7 10-5.2 5.2-5.1 4.2-10.9 2.8-13.6z"/>
+ <path fill="#357BA8" d="M22.6 47.1c-5.6-5.6-13-7.4-16.4-3.9-.4.4-.7.8-1 1.3l-5.3 5.3L0 72h17.8l7.8-7.8c.3-.2.6-.4.8-.7 3.6-3.5 1.8-10.8-3.8-16.4z"/>
+ <path fill="#FFD3B6" d="M30.2 3.7c-2.2 0-4 1.8-4 4v15.8c0 3.1-7.6 11.9-14.5 17.9-.4.3-.7.7-.9 1.1-1.4 2.7-2.4 8.5 2.7 13.7 4.5 4.5 8.2 5.2 10 5.2h.8c.8-.1 1.6-.5 2.2-1.1.3-.3 7.6-7.4 7.6-20V7.7c.1-2.2-1.7-4-3.9-4z"/>
+ <path fill="#00BEEA" d="M54.5 13.8c.1 0 .1 0 .2-.1l7.1-4c.1-.1.2-.2.2-.3 0-.1 0-.2-.1-.3L58.8 6c-.1 0-.2-.1-.4 0-.1 0-.2.1-.3.2l-4 7.1c-.1.2-.1.3.1.5h.3zM10.5 9.8l7.1 4c.1 0 .1.1.2.1.3 0 .4-.2.4-.4 0-.1 0-.2-.1-.3l-3.9-7c-.1-.2-.2-.2-.3-.2-.1 0-.2 0-.3.1l-3.1 3.1c-.1.1-.1.2-.1.3 0 .1 0 .2.1.3zM62.1 18.7c-.1-.1-.2-.1-.3-.1L54 20.8c-.2 0-.3.2-.3.4s.1.3.3.4l7.8 2.2h.1c.1 0 .2 0 .2-.1.1-.1.2-.2.2-.3V19c-.1-.1-.1-.3-.2-.3zM18.4 20.8l-7.8-2.2c-.1 0-.2 0-.3.1-.2 0-.3.2-.3.3v4.3c0 .1.1.2.2.3.1.1.2.1.2.1h.1l7.8-2.2c.2 0 .3-.2.3-.4s-.1-.3-.2-.3z"/>
+</svg> \ No newline at end of file
diff --git a/devtools/client/themes/images/emojis/emoji-tool-dom.svg b/devtools/client/themes/images/emojis/emoji-tool-dom.svg
new file mode 100644
index 000000000..1d3e40fad
--- /dev/null
+++ b/devtools/client/themes/images/emojis/emoji-tool-dom.svg
@@ -0,0 +1,11 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
+ <path fill="#51BA7B" d="M487.819 258.669H439.1c-10.041 0-18.181-8.14-18.181-18.181s8.14-18.181 18.181-18.181h48.719c10.041 0 18.181 8.14 18.181 18.181s-8.14 18.181-18.181 18.181z"/>
+ <path fill="#BADEBE" d="M415.747 69.674s-.387.603-1.059 1.667a8.05 8.05 0 0 1-.638.799 59.208 59.208 0 0 0-1.057 1.933c-.357.806-.812 1.618-1.199 2.599a55.875 55.875 0 0 0-1.233 3.083c-.433 1.086-.783 2.248-1.19 3.426-.364 1.183-.742 2.395-1.064 3.614-.316 1.219-.645 2.445-.884 3.643-.286 1.219-.475 2.381-.679 3.523-.188 1.142-.308 2.214-.434 3.243-.041.505-.077.988-.118 1.457-.043.483-.07.939-.063 1.338-.007.812-.063 1.646 0 2.221.014.315.034.609.048.882 0 .14.007.273.015.399.014.105.027.209.041.301.113.777.161 1.26.161 1.26l.19 2.025a7.085 7.085 0 0 1-6.396 7.712c-2.829.267-5.421-1.177-6.774-3.467 0 0-.489-.826-1.308-2.353-.099-.189-.204-.392-.316-.603-.084-.203-.181-.413-.274-.63-.195-.448-.398-.932-.616-1.45-.484-1.058-.806-2.164-1.233-3.432a23.41 23.41 0 0 1-.561-1.933c-.174-.665-.349-1.352-.532-2.066-.301-1.387-.622-2.879-.876-4.393-.231-1.506-.491-3.089-.638-4.659-.195-1.583-.308-3.18-.419-4.784-.106-1.604-.147-3.201-.19-4.791 0-1.576-.027-3.145.043-4.665.034-1.513.125-2.998.238-4.42.077-1.408.28-2.802.414-4.063.188-1.31.371-2.48.588-3.622.301-1.31.622-2.529.853-3.411.31-1.212.477-1.913.477-1.913 1.968-7.887 9.967-12.692 17.856-10.724 7.894 1.975 12.691 9.968 10.725 17.862a14.708 14.708 0 0 1-1.815 4.266l-.083.126zm-56.35 17.01a14.892 14.892 0 0 0 1.036-4.518c.559-8.111-5.562-15.145-13.681-15.705-8.118-.56-15.151 5.562-15.711 13.681 0 0-.05.715-.133 1.976a70.1 70.1 0 0 0-.258 4.133c.014.665.055 1.233.091 1.912.048.638.063 1.366.154 2.025.174 1.331.308 2.823.595 4.245l.407 2.221c.168.729.335 1.478.511 2.234.322 1.527.783 3.033 1.19 4.575.477 1.526.91 3.068 1.457 4.574a88.52 88.52 0 0 0 1.654 4.483c.552 1.464 1.211 2.893 1.806 4.259a121.46 121.46 0 0 0 1.905 3.936c.694 1.254 1.255 2.41 1.948 3.496.665 1.086 1.226 2.052 1.864 2.936.622.882 1.079 1.611 1.618 2.248l1.59 1.941c1.849 2.255 4.973 3.243 7.887 2.234 3.74-1.289 5.715-5.373 4.426-9.107l-.469-1.338s-.168-.497-.469-1.366c-.162-.386-.303-1.051-.498-1.688-.204-.631-.357-1.478-.554-2.347-.217-.833-.344-1.891-.532-2.906a93.503 93.503 0 0 1-.428-3.362c-.097-1.198-.231-2.396-.274-3.664a61.991 61.991 0 0 1-.118-3.797c-.029-1.268.041-2.543.063-3.775.091-1.219.111-2.438.251-3.566.063-.56.12-1.121.174-1.661.077-.518.162-1.03.233-1.526.132-1.016.371-1.829.525-2.627.077-.407.21-.693.301-1.016.097-.294.181-.645.267-.841.188-.127.258-.147.342-.294l.751-1.829.079-.176z"/>
+ <path fill="#8ACCA0" d="M456.112 143.24c-11.449-34.634-48.8-53.434-83.441-41.983-34.634 11.449-53.431 48.807-41.982 83.442 17.717 53.598 16.873 94.849-2.59 126.04-1.716 2.393-3.661 5.076-4.907 6.585a66.228 66.228 0 0 0-7.289 6.104 66.535 66.535 0 0 0-3.758 2.035l-2.851 1.674c-.113.077-.154.112-.246.189-.041.035-.077.07-.125.112-.022.021-.029.035-.07.07l-.233.127c-.301.182-.629.371-.973.575-.364.203-.749.413-1.156.637a56.07 56.07 0 0 1-6.494 2.956c-2.711 1.044-6.03 2.109-10.03 3.04a104.442 104.442 0 0 1-13.996 2.179c-5.317.483-11.27.672-17.694.476a202.645 202.645 0 0 1-4.912-.224c-1.597-.105-3.208-.21-4.826-.322-4.134-.393-8.308-.784-12.517-1.191-1.907-.168-3.875-.42-5.821-.623-1.962-.217-3.776-.469-5.681-.693-1.828-.259-3.656-.504-5.437-.777-1.765-.287-3.537-.553-5.288-.882-1.738-.294-3.538-.673-5.331-1.023l-2.774-.603c-.925-.203-1.864-.406-2.865-.652l-2.943-.693-3.095-.77c-2.136-.539-4.26-1.078-6.382-1.618-8.713-2.241-17.861-4.617-26.604-6.732l-1.64-.398-.202-.05-.099-.028c-1.233-.357-.398-.104-.699-.189l-.4-.07-.792-.147a365.722 365.722 0 0 1-3.152-.56c-2.634-.476-5.24-.953-7.824-1.415a44.945 44.945 0 0 0-1.955-.322c-.631-.097-1.26-.196-1.891-.287-1.254-.189-2.5-.371-3.74-.56-2.516-.316-4.966-.658-7.439-.924-9.863-1.1-19.447-1.646-28.545-1.619-9.107.05-17.682.61-25.54 1.661-7.839 1.023-14.949 2.522-21.051 4.175a137.419 137.419 0 0 0-8.419 2.578 129.198 129.198 0 0 0-6.851 2.592c-2.032.847-3.79 1.646-5.204 2.326l-2.039 1.001c-.982.505-1.479.757-1.479.757-26.17 13.548-36.403 45.75-22.857 71.92 13.555 26.178 45.756 36.405 71.927 22.857l-1.45.735c-.301.14-.742.351-1.324.63-.344.155-.848.386-1.507.693-.251.133-.342.203-.21.21.169.015.504-.014 1.072-.063 1.163-.091 3.138-.259 5.955-.231 2.774 0 6.41.231 10.661.757 4.272.54 9.225 1.457 14.668 2.767 1.358.321 2.767.721 4.175 1.071.715.203 1.437.413 2.165.617l1.086.308c.378.105.735.203 1.023.302 1.394.441 2.808.882 4.231 1.331 1.765.575 3.544 1.149 5.337 1.731 7.824 2.472 15.711 5.092 24.357 7.978 2.227.743 4.462 1.478 6.709 2.221l3.496 1.142 3.692 1.17 1.864.589 1.946.588 3.923 1.177c2.704.77 5.387 1.555 8.175 2.269 2.76.75 5.527 1.415 8.294 2.087 2.745.658 5.464 1.248 8.168 1.843 2.634.54 5.344 1.121 7.901 1.604 2.584.476 5.107.981 7.704 1.429 2.543.441 5.072.876 7.586 1.31 2.208.364 4.407.722 6.6 1.086 1.443.217 2.885.427 4.315.644 1.366.182 2.724.371 4.077.553 2.711.351 5.428.666 8.139.946 10.851 1.128 21.682 1.647 32.23 1.513a239.007 239.007 0 0 0 30.479-2.291c9.666-1.366 18.689-3.313 26.716-5.548a191.408 191.408 0 0 0 20.791-7.061c1.428-.595 2.781-1.162 4.055-1.695 1.269-.568 2.459-1.093 3.566-1.59l.812-.364.912-.434c.602-.295 1.177-.568 1.731-.834 1.086-.54 2.073-1.023 2.955-1.464 1.919-.981 2.943-1.5 2.943-1.5l1.975-1.008a66.247 66.247 0 0 0 15.966-11.503 66.576 66.576 0 0 0 6.185-3.588c18.693-12.238 30.142-28.256 38.55-40.018l.926-1.294.862-1.338c23.738-36.839 36.169-79.029 36.947-125.397.602-35.782-5.867-74.417-19.227-114.833z"/>
+ <path fill="#FFF" d="M379.069 155.928l4.301 16.062h-.021c.007.028.027.028.034.042 1.688 6.311-2.059 12.798-8.363 14.486-6.312 1.688-12.799-2.059-14.487-8.364-.007-.014-.007-.021-.014-.049l-.05.014-4.301-16.062.14-.035c-1.057-5.989 2.55-11.887 8.532-13.492 5.969-1.597 12.048 1.709 14.116 7.425l.113-.027zm48.159-20.587c-2.697-5.45-9.107-8.048-14.858-5.792-5.765 2.263-8.693 8.532-6.971 14.367l-.132.049 6.08 15.481.05-.021c.007.021.007.027.014.042 2.387 6.08 9.254 9.071 15.334 6.683 6.08-2.381 9.071-9.254 6.682-15.334 0-.008-.027-.008-.034-.028l.021-.015-6.08-15.474-.106.042z"/>
+ <path fill="#51BA7B" d="M386.722 311.106l40.557 17.233c9.247 3.93 13.555 14.612 9.632 23.859-3.93 9.253-14.619 13.561-23.866 9.632a21.165 21.165 0 0 1-1.24-.581l-39.143-20.223c-8.118-4.196-11.292-14.171-7.104-22.29 3.994-7.728 13.284-10.95 21.164-7.63"/>
+ <path fill="#74C48D" d="M394.574 405.513c-12.623 0-25.31-3.56-36.185-10.523a5.688 5.688 0 0 1 6.134-9.581c15.343 9.826 35.001 11.501 51.304 4.37a5.69 5.69 0 0 1 4.558 10.424c-8.137 3.557-16.959 5.31-25.811 5.31zm-52.133 40.603a5.69 5.69 0 0 0-5.396-5.966c-14.406-.721-28.217-7.446-37.895-18.452a5.689 5.689 0 0 0-8.542 7.513c11.694 13.299 28.412 21.428 45.868 22.301a5.686 5.686 0 0 0 5.965-5.396zm81.464-133.345c15.644-.293 30.09-5.857 41.777-16.091a5.688 5.688 0 0 0-7.496-8.558c-9.641 8.443-21.568 13.033-34.493 13.275a5.69 5.69 0 0 0-5.581 5.794 5.687 5.687 0 0 0 5.684 5.581l.109-.001z"/>
+</svg>
diff --git a/devtools/client/themes/images/emojis/emoji-tool-inspector.svg b/devtools/client/themes/images/emojis/emoji-tool-inspector.svg
new file mode 100644
index 000000000..093fb91b5
--- /dev/null
+++ b/devtools/client/themes/images/emojis/emoji-tool-inspector.svg
@@ -0,0 +1,13 @@
+<!-- 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/. -->
+<svg height="16" width="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" opacity="1.0">
+ <path fill="#FFB636" d="M378.553 355.648L45.117 500.733c-21.735 8.65-43.335-12.764-34.874-34.572l145.71-338.683"/>
+ <path fill="#FFD469" d="M10.243 466.16l11.58-26.915c.993-1.514 1.983-3.03 2.977-4.543 57.597-87.744 116.038-174.952 176.475-260.768l67.765 69.46C217.91 278.496 51.89 450.064 17.115 495.57c-7.57-6.962-11.25-18.127-6.872-29.41z"/>
+ <path fill="#A06C33" d="M304.382 204.434c61.854 61.854 95.685 128.308 75.564 148.43-20.12 20.12-86.575-13.71-148.43-75.564s-95.685-128.308-75.564-148.43 86.575 13.71 148.43 75.564z"/>
+ <path fill="#F7F9AA" d="M155.6 327.572c0 6.012-4.873 10.885-10.884 10.885s-10.885-4.873-10.885-10.885 4.874-10.885 10.886-10.885 10.885 4.873 10.885 10.885z"/>
+ <path fill="#FFB636" d="M501.986 213.16c0 8.628-6.994 15.622-15.622 15.622s-15.622-6.994-15.622-15.622 6.994-15.622 15.622-15.622 15.622 6.994 15.622 15.622zM397.663 421.182c-8.628 0-15.622 6.994-15.622 15.622s6.995 15.622 15.623 15.622 15.622-6.994 15.622-15.622-6.995-15.622-15.622-15.622z"/>
+ <path fill="#BEA4FF" d="M355.95 79.523c-1.34 9.065-7.198 17.072-16.07 21.968-6.127 3.38-13.33 5.138-20.808 5.138-2.354 0-4.734-.174-7.117-.526-5.288-.782-10.58.016-14.52 2.19-1.766.973-4.8 3.104-5.293 6.437-.492 3.332 1.796 6.25 3.203 7.693 3.058 3.135 7.725 5.38 12.85 6.22.14.015.28.02.42.04 21.62 3.197 37.062 20.32 34.422 38.174-1.34 9.066-7.197 17.073-16.07 21.97-6.127 3.38-13.33 5.136-20.807 5.136-2.354 0-4.734-.174-7.117-.526-5.287-.783-10.582.015-14.52 2.19-1.767.973-4.8 3.104-5.294 6.437-.79 5.35 5.777 12.41 16.47 13.99 5.816.86 9.835 6.274 8.975 12.092-.782 5.29-5.328 9.092-10.52 9.092-.52 0-1.043-.038-1.57-.116-21.62-3.196-37.06-20.32-34.422-38.173 1.34-9.067 7.197-17.074 16.07-21.97 8.056-4.444 17.973-6.082 27.925-4.61 5.288.78 10.58-.017 14.52-2.19 1.766-.974 4.8-3.105 5.293-6.438.778-5.262-5.576-12.17-15.962-13.898-.17-.017-.34-.03-.512-.056-9.95-1.472-18.97-5.908-25.395-12.493-7.076-7.254-10.366-16.614-9.025-25.68 1.34-9.066 7.197-17.073 16.07-21.97 8.055-4.443 17.972-6.08 27.924-4.61 5.285.78 10.58-.016 14.52-2.19 1.765-.973 4.8-3.104 5.292-6.437s-1.796-6.25-3.203-7.694c-3.143-3.22-7.978-5.516-13.268-6.297-5.817-.86-9.836-6.273-8.976-12.09.86-5.82 6.274-9.833 12.09-8.978 9.952 1.47 18.972 5.907 25.396 12.492 7.078 7.255 10.368 16.615 9.027 25.68z"/>
+ <path fill="#FF6E83" d="M81.73 159.69c0 9.776-7.925 17.702-17.702 17.702s-17.703-7.926-17.703-17.703c0-9.778 7.926-17.704 17.703-17.704S81.73 149.91 81.73 159.69zm316.446-20.454c-11.296 0-20.452 9.157-20.452 20.452s9.157 20.452 20.452 20.452 20.452-9.157 20.452-20.452-9.156-20.452-20.452-20.452zM215.53 395.9c-11.297 0-20.453 9.156-20.453 20.45s9.157 20.453 20.452 20.453c11.295 0 20.45-9.157 20.45-20.452s-9.155-20.45-20.45-20.45zm271.302-93.647c3.093-5.99.745-13.352-5.244-16.445-2.388-1.232-5.238-2.868-8.538-4.76-28.993-16.634-89.32-51.243-160.352 6.108-5.245 4.234-6.063 11.92-1.83 17.163 4.234 5.244 11.918 6.064 17.164 1.828 58.035-46.856 104.882-19.985 132.87-3.928 3.404 1.952 6.618 3.796 9.484 5.276 1.79.925 3.705 1.363 5.59 1.363 4.42 0 8.688-2.41 10.856-6.607z"/>
+ <path fill="#59CAFC" d="M434.834 62.776c0 6.012-4.874 10.885-10.885 10.885-6.013 0-10.886-4.872-10.886-10.884s4.873-10.885 10.885-10.885c6.01 0 10.884 4.874 10.884 10.886zM46.324 11.894c-6.012 0-10.885 4.873-10.885 10.885s4.872 10.884 10.884 10.884S57.21 28.79 57.21 22.78s-4.874-10.886-10.886-10.886zm170.68 142.057c1.232-2.413 2.75-5.162 4.357-8.072 8.155-14.77 19.32-35 19.993-58.56.807-28.303-13.934-54-43.812-76.38-5.186-3.884-12.538-2.827-16.42 2.358-3.884 5.186-2.83 12.538 2.357 16.42 23.75 17.79 35.01 36.412 34.425 56.934-.51 17.872-9.697 34.516-17.08 47.89-1.7 3.082-3.31 5.993-4.713 8.746-2.946 5.77-.655 12.836 5.115 15.78 1.708.873 3.53 1.286 5.323 1.286 4.267 0 8.384-2.338 10.457-6.4z"/>
+</svg> \ No newline at end of file
diff --git a/devtools/client/themes/images/emojis/emoji-tool-memory.svg b/devtools/client/themes/images/emojis/emoji-tool-memory.svg
new file mode 100644
index 000000000..6387d5649
--- /dev/null
+++ b/devtools/client/themes/images/emojis/emoji-tool-memory.svg
@@ -0,0 +1,9 @@
+<!-- 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/. -->
+<svg height="16" width="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" opacity="1.0">
+ <path fill="#A9B8C2" d="M509.507 277.512c-.132-6.324-.47-12.764-1.04-19.213-.632-6.447-1.428-12.904-2.614-19.245-1.206-6.336-2.63-12.568-4.475-18.537-1.87-5.96-3.99-11.684-6.497-16.98-.595-1.332-1.307-2.607-1.945-3.884l-.975-1.893-1.06-1.824c-.712-1.2-1.388-2.395-2.105-3.543l-2.23-3.322c-.705-1.1-1.51-2.11-2.28-3.112-.78-.994-1.507-1.99-2.287-2.913l-2.348-2.653-1.13-1.268-1.182-1.164c-1.56-1.52-2.987-2.98-4.48-4.16-1.473-1.22-2.712-2.35-4.158-3.326-1.088-.78-2.095-1.49-3-2.118-17.824-20.005-42.044-32.73-73.602-32.262-65.688.978-93.648 12.662-135.097-21.204-18.746-41.434-58.357-72.004-117.605-57.282C50.467 79.708 17.34 103.01 10.62 151.743c-1.177 3.953-1.833 8.13-1.86 12.463l-.058 9.042-.018 2.794-.056 2.945c-.038 1.238-.08 2.662-.13 4.256-.217 6.425-.762 15.903-1.466 27.59-.686 11.68-1.51 25.55-1.832 40.698-.166 7.57-.185 15.47.016 23.567.214 8.102.645 16.406 1.457 24.8.798 8.392 1.978 16.873 3.64 25.29 1.69 8.415 3.835 16.766 6.67 24.84 2.778 8.07 6.28 15.85 10.322 23.028 4.088 7.17 8.782 13.704 13.74 19.314 4.985 5.6 10.21 10.248 15.127 13.945 4.91 3.723 9.528 6.466 13.38 8.522l2.747 1.44c.874.414 1.694.8 2.457 1.16 1.526.725 2.832 1.31 3.903 1.732l3.276 1.337c7.498 3.063 16.404.77 21.39-6.07 5.868-8.047 4.1-19.328-3.946-25.196l-4.27-3.112-1.967-1.435c-.647-.447-1.408-1.05-2.277-1.77l-1.4-1.138-1.507-1.362c-2.102-1.915-4.49-4.328-6.8-7.322-2.325-2.975-4.593-6.484-6.567-10.504-1.954-4.048-3.62-8.6-4.9-13.612-1.242-5.053-2.145-10.52-2.577-16.423-.47-5.864-.484-12.145-.174-18.62.17-3.342.436-6.743.782-10.183 2.13-5.07 4.632-11.54 7.39-19.013 2.704.653 5.46 1.06 8.26 1.23l.002.014c7.48 1.072 23.013 9.41 23.51 16.948 2.443 37.036 15.236 59.378 26.407 72.06 10.125 11.492 16.997 25.48 20.296 40.437 7.742 35.107.006 76.796 46.07 76.796s20.08-48.648 34.932-78.647c14.85-30 51.294-24 51.294-24 32.974 0 47.557 10.897 55.47 25.767 10.23 19.23-3.268 76.878 47.38 76.878 49.846 0 35.89-67.9 50.29-86.76 18.117-23.724 41.402-51.33 54.37-84.85-.154 2.273-.32 4.483-.492 6.603-.398 4.603-.795 8.822-1.228 12.556-.387 3.738-.84 6.987-1.168 9.668l-1.175 8.41-.013.09c-.468 3.384 1.82 6.558 5.213 7.158 3.472.613 6.783-1.704 7.397-5.175 0 0 .55-3.12 1.516-8.58.435-2.74 1.02-6.057 1.557-9.887.583-3.823 1.15-8.152 1.73-12.882.558-4.734 1.1-9.874 1.552-15.33.2-2.73.46-5.532.603-8.408l.254-4.357.18-4.45c.248-5.99.284-12.198.185-18.52z"/>
+ <path fill="#2B3B47" d="M80.323 159.56c-8.07-.432-14.963 5.762-15.394 13.833l-1.35 25.27c-.43 8.072 5.763 14.964 13.834 15.395 8.07.43 14.963-5.763 15.394-13.834l1.35-25.27c.43-8.072-5.764-14.964-13.835-15.395z"/>
+ <path fill="#7E9AA8" d="M155.92 254.302l.545.627.67.768c.26.278.558.6.897.966 1.375 1.45 3.472 3.58 6.338 5.903 2.878 2.305 6.577 4.818 11.15 6.742 4.558 1.923 9.945 3.32 15.91 3.456 5.952.167 12.446-.82 19.002-3.106 6.56-2.253 13.2-5.68 19.5-10.12 6.315-4.412 12.35-9.754 17.747-15.804 5.403-6.053 10.17-12.735 14.56-19.53 4.383-6.795 8.28-13.91 10.84-21.304 2.587-7.375 3.857-14.943 3.643-22.166-.105-3.612-.592-7.137-1.44-10.51-.212-.842-.44-1.678-.7-2.5l-.405-1.277-.47-1.426-1.845-5.463c-2.468-7.185-5.09-13.94-7.617-20.116-2.512-6.19-5.044-11.765-7.338-16.683-2.314-4.908-4.522-9.077-6.41-12.475-1.93-3.36-3.527-5.955-4.663-7.686l-1.782-2.63-.063-.093c-.648-.957-.4-2.258.558-2.906.846-.572 1.96-.443 2.657.256l2.36 2.37c1.52 1.56 3.67 3.92 6.24 7.056 2.53 3.15 5.532 7.045 8.752 11.65 3.233 4.6 6.64 9.935 10.217 15.844 3.557 5.925 7.206 12.444 10.864 19.52.906 1.786 1.846 3.653 2.698 5.42l.657 1.35.352.733.347.79c.47 1.057.9 2.138 1.31 3.235 1.643 4.386 2.833 9.065 3.514 13.883 1.38 9.652.618 19.764-1.692 29.466-2.33 9.71-6.1 19.137-11.242 27.675-5.142 8.547-11.762 16.038-18.96 22.523-7.2 6.49-14.974 12.09-23.122 16.485-8.143 4.383-16.626 7.64-25.07 9.388-8.43 1.745-16.805 2.01-24.324.718-7.525-1.26-14.11-3.94-19.253-7.182-5.183-3.21-9.01-6.814-11.86-9.924-2.86-3.128-4.708-5.822-5.948-7.694l-.812-1.252-.53-.872-.43-.71c-.828-1.365-.393-3.14.97-3.97 1.22-.74 2.77-.47 3.676.572z"/>
+ <path fill="#E8EBED" d="M88.503 271.766l-1.664 1.06-2.395 1.56c-1.02.69-2.11 1.282-3.305 1.943-1.196.655-2.56 1.31-3.992 1.95l-2.24.927c-.77.293-1.57.556-2.372.832-.798.29-1.628.498-2.456.73-.83.237-1.664.45-2.507.615-1.684.39-3.368.628-5.034.87-1.654.162-3.297.342-4.87.387-1.585.1-3.098.074-4.562.082-1.453-.027-2.84-.078-4.145-.14l-3.636-.25c-2.228-.2-4.065-.38-5.34-.542l-2.003-.233c-2.678-.313-4.597-2.737-4.286-5.415.234-2.008 1.655-3.59 3.48-4.124l.13-.035 1.852-.54c1.183-.33 2.847-.85 4.817-1.48l3.128-1.073c1.09-.403 2.223-.835 3.37-1.296 1.127-.49 2.305-.962 3.41-1.526 1.14-.52 2.198-1.14 3.26-1.716 1.006-.636 2.008-1.242 2.884-1.915.458-.313.88-.654 1.278-.997.402-.34.812-.648 1.163-1.008.36-.35.72-.67 1.05-1.007l.936-1.013c.587-.684 1.12-1.315 1.59-1.953.465-.64.914-1.22 1.16-1.69.295-.493.47-.798.904-1.41l1.122-1.625c4.45-6.442 13.282-8.055 19.724-3.604 6.442 4.45 8.055 13.282 3.604 19.724-1.096 1.586-2.457 2.88-3.982 3.86l-.075.05z"/>
+</svg> \ No newline at end of file
diff --git a/devtools/client/themes/images/emojis/emoji-tool-network.svg b/devtools/client/themes/images/emojis/emoji-tool-network.svg
new file mode 100644
index 000000000..249ac7b62
--- /dev/null
+++ b/devtools/client/themes/images/emojis/emoji-tool-network.svg
@@ -0,0 +1,8 @@
+<!-- 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/. -->
+<svg height="16" width="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" opacity="1.0">
+ <path fill="#FFA1E0" d="M464.142 414.09l-1.358-.873-3.944-2.48c-3.46-2.172-8.603-5.185-15.26-8.886-3.328-1.855-7.038-3.87-11.11-6.027-4.07-2.172-8.512-4.446-13.312-6.755-9.597-4.637-20.576-9.7-32.827-14.838-12.24-5.22-25.786-10.227-40.458-15.154-7.34-2.45-14.965-4.768-22.865-7.003-3.88-1.142-8.122-2.202-12.357-3.29l-5.8-1.557c-.953-.26-1.91-.518-2.868-.777l-.36-.098c-.218-.016.236-.033-.535-.048l-.136-.034-.542-.137-1.086-.272c-11.586-2.748-21.596-8.346-27.23-14.272-2.876-2.97-4.743-5.804-6.32-8.223-1.587-2.436-2.93-4.598-4.884-6.982-1.902-2.393-4.34-5.185-7.156-8.804-1.372-1.822-2.726-3.88-4.222-6.077-.73-1.113-1.458-2.27-2.185-3.476-.71-1.22-1.456-2.46-2.23-3.74-6.005-10.315-12.3-23.12-19.05-36.77-3.392-6.83-6.876-13.896-10.57-21.048-3.73-7.144-7.543-14.41-11.644-21.65-4.12-7.233-8.497-14.457-13.318-21.5-4.782-7.053-10-13.954-15.843-20.344-5.85-6.367-12.246-12.29-19.073-17.195-6.814-4.91-13.93-8.87-20.864-11.594-3.406-1.368-6.757-2.46-9.987-3.312.233-1.248.453-2.515.64-3.806.37-2.414.66-4.892.857-7.394.216-2.5.295-5.03.298-7.54.007-2.51-.145-5.008-.383-7.443-.236-2.435-.65-4.813-1.148-7.08-.493-2.266-1.18-4.42-1.914-6.405-.755-1.985-1.626-3.794-2.52-5.375-.442-.79-.928-1.515-1.35-2.19l-1.318-1.81c-.872-1.064-1.583-1.89-2.13-2.418l-.815-.824c-.227-.226-.5-.473-.76-.675-3.37-2.612-8.217-1.998-10.83 1.37s-1.997 8.216 1.37 10.828l.18.14.436.34c.298.197.697.568 1.23 1.035l.84.867c.283.358.618.713.943 1.147.654.862 1.36 1.917 2.05 3.188.67 1.283 1.376 2.736 1.97 4.404.597 1.66 1.175 3.477 1.623 5.44.45 1.96.856 4.03 1.128 6.187.274 2.153.497 4.372.59 6.618.113 2.244.14 4.51.088 6.747-.013.924-.054 1.838-.1 2.75-.174-.026-.354-.06-.528-.083-6-.85-11.352-1.042-16.037-.84-2.52.085-4.844.282-6.99.53.02-.635.048-1.26.06-1.91.03-2.3.036-4.69-.08-7.125-.093-2.435-.277-4.918-.557-7.407-.26-2.49-.662-4.984-1.133-7.444-.47-2.46-1.09-4.88-1.785-7.222-.692-2.342-1.55-4.594-2.467-6.72-.912-2.13-1.995-4.11-3.09-5.916-1.118-1.802-2.316-3.41-3.493-4.79-.585-.692-1.2-1.31-1.742-1.893l-1.64-1.525c-1.058-.877-1.913-1.55-2.55-1.965l-.958-.653c-.298-.2-.65-.408-.982-.57-3.836-1.863-8.456-.263-10.318 3.573-1.863 3.836-.263 8.456 3.573 10.318l.168.08.496.242c.333.132.8.414 1.418.763l1.003.677c.352.293.754.573 1.162.933.82.713 1.73 1.604 2.666 2.712.92 1.123 1.91 2.407 2.834 3.924.926 1.51 1.863 3.177 2.704 5.014.84 1.834 1.66 3.787 2.367 5.85.708 2.06 1.378 4.196 1.926 6.384.567 2.182 1.054 4.4 1.457 6.612.424 2.207.73 4.41 1.01 6.55.113.912.208 1.807.298 2.695-.745.15-1.458.304-2.13.457-1.42.335-2.617.604-3.835.915-1.18.302-2.222.568-3.122.8l-1.818.648-1.48.535c-3.023 1.095-5.71 3.215-7.467 6.203-4 6.808-1.724 15.57 5.084 19.57l4.115 2.42.397.232c.263.16.654.376 1.17.652.515.272 1.16.642 1.918 1.02.636.396 1.373.854 2.207 1.37 1.656 1.048 3.78 2.25 6.04 3.635 2.263 1.432 4.734 2.96 7.195 4.75 2.475 1.707 4.91 3.712 7.16 5.728 1.1 1.076 2.203 2.05 3.178 3.194.51.53.987 1.08 1.467 1.622.455.576.918 1.128 1.345 1.716 1.747 2.3 3.135 4.883 4.322 7.815 1.183 2.938 2.178 6.265 3.04 10.19 1.718 7.817 2.865 18.03 3.547 30.368.39 6.15.76 12.794 1.08 19.934.368 7.128.852 14.708 1.567 22.754 1.484 16.086 3.876 34.026 9.6 53.96 2.888 9.948 6.627 20.44 11.828 31.207 5.104 10.79 11.837 21.844 20.557 32.22 8.664 10.374 19.436 19.857 31.322 27.306 2.955 1.884 5.992 3.62 9.066 5.223 3.062 1.628 6.172 3.122 9.308 4.455 6.242 2.747 12.604 4.848 18.834 6.613 12.494 3.533 24.638 5.127 35.972 5.852 2.852.192 5.63.268 8.4.345 2.74.022 5.472.05 8.126.025 5.34-.033 10.51-.267 15.515-.657l1.875-.146.935-.075.234-.02c-.797-.01-.118-.018-.25-.027l.362-.055 2.878-.442 5.594-.866c3.233-.527 6.35-1.092 9.82-1.542 6.765-.992 13.448-1.706 20.007-2.47 6.56-.748 12.99-1.322 19.242-1.83l9.242-.655c3.036-.224 6.022-.415 8.953-.577 5.863-.34 11.503-.57 16.883-.738 5.39-.225 10.51-.334 15.335-.418l13.54-.145c4.19-.024 8.043-.01 11.512.04 6.947.09 12.365.312 16.018.373 3.656.098 5.46.17 5.24.105 8.083 2.4 16.582-2.204 18.983-10.286 2.402-8.083-2.204-16.58-10.286-18.983z" id="Layer_2"/>
+ <path fill="#D19B61" d="M420.322 245.86c-48.02-73.215-148.485-87.3-222.8-41.717-1.518.93-3.028 1.877-4.523 2.857 0 0 14 91.176 71.178 122.95-5.975 17.118-4.205 36.725 6.507 53.057 25.402 38.73 77.39 49.534 116.12 24.132 9.452-6.2 17.7-13.54 24.693-21.72 32.814-38.37 37.907-95.22 8.824-139.56z" id="Layer_3"/>
+ <path fill="#B7834F" d="M340.81 426.833c-6.172 0-12.386-.634-18.568-1.92-23.63-4.91-43.932-18.728-57.168-38.908-11.687-17.817-14.357-39.587-7.327-59.727 4.965-14.225 14.263-26.12 26.888-34.4 11.587-7.6 25.438-10.23 39.004-7.413 13.564 2.82 25.22 10.752 32.82 22.34 5.9 8.994 7.943 19.748 5.754 30.28-2.188 10.534-8.35 19.584-17.345 25.483-7.13 4.677-15.655 6.297-24.004 4.562-8.35-1.735-15.522-6.617-20.2-13.747-7.835-11.947-4.49-28.042 7.458-35.878 4.825-3.166 11.305-1.82 14.47 3.008 3.167 4.826 1.82 11.306-3.007 14.47-2.31 1.516-2.956 4.627-1.44 6.937 1.613 2.46 4.09 4.146 6.973 4.745 2.886.603 5.826.04 8.286-1.574 4.327-2.837 7.29-7.19 8.343-12.256 1.052-5.066.07-10.24-2.77-14.565-9.364-14.28-28.602-18.278-42.88-8.913-8.744 5.735-15.182 13.97-18.618 23.812-4.87 13.953-3.02 29.033 5.07 41.372 10.175 15.513 25.78 26.134 43.943 29.908 18.16 3.773 36.707.25 52.22-9.924 22.452-14.726 37.825-37.313 43.29-63.603 5.462-26.29.36-53.13-14.365-75.583-43.45-66.246-132.694-84.792-198.94-41.343-4.828 3.166-11.306 1.82-14.472-3.007-3.166-4.827-1.82-11.306 3.007-14.47 75.884-49.772 178.113-28.53 227.884 47.356 17.787 27.12 23.95 59.545 17.35 91.3-6.6 31.756-25.17 59.04-52.29 76.828-14.903 9.772-31.957 14.83-49.37 14.83z" />
+</svg> \ No newline at end of file
diff --git a/devtools/client/themes/images/emojis/emoji-tool-profiler.svg b/devtools/client/themes/images/emojis/emoji-tool-profiler.svg
new file mode 100644
index 000000000..bf4b660cc
--- /dev/null
+++ b/devtools/client/themes/images/emojis/emoji-tool-profiler.svg
@@ -0,0 +1,11 @@
+<!-- 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/. -->
+<svg height="16" width="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 511.9 511.9" opacity="1.0">
+ <circle fill="#E2A042" cx="254.5" cy="206.8" r="170.9"/>
+ <path fill="#AF773F" d="M494.6 226.6c0-37.9-30.7-68.6-68.6-68.6-7 0-13.8 1.1-20.2 3-18.5-18.8-44.2-30.5-72.7-30.5-23 0-44.1 7.6-61.1 20.4-10.9 8.2-25.6 8.3-36.5 0-19.6-14.8-44.7-22.7-71.8-19.9-23.4 2.4-46.2 12.8-63 28.6-4.7-1-9.7-1.6-14.8-1.6-37.9 0-68.6 30.7-68.6 68.6 0 37.5 30.1 68 67.5 68.5 27.3 83.3 99.9 115 169.7 115 76.7 0 148.3-33.6 172.1-114.9 37.6-.4 68-30.9 68-68.6z"/>
+ <path fill="#FFB636" d="M330.2 165.8c-25.3-.3-47.7 11.8-61.7 30.6-6.7 9.1-19.8 9.1-26.5 0-13.9-18.8-36.4-30.9-61.7-30.6-39.6.5-72.9 32.9-74.4 72.5-1.5 39.9 27.9 73.2 66.2 77.8 5.2.6 9.2 4.7 9.8 9.8 4.6 36.4 35.7 64.6 73.4 64.6 37.5 0 68.4-27.9 73.3-64 .7-5.5 5-9.8 10.5-10.5 38-4.9 67.1-38.1 65.6-77.8-1.6-39.4-34.9-71.9-74.5-72.4z"/>
+ <path fill="#E576CB" d="M271.8 272.4s-2.3 2.9-5.6 5c-4.2 2.9-9.2 4.7-12.4 3.7-4.5-.1-9-1.5-12.4-3.7-3.4-2.1-5.6-5-5.6-5-.5-.7-.5-1.6 0-2.2 0 0 2.3-2.9 5.6-5 3.4-2.2 7.9-3.7 12.4-3.7 4.5.1 9 1.5 12.4 3.7 3.4 2.1 5.6 5 5.6 5 .6.6.5 1.6 0 2.2zM253.8 376.9c-18.1 0-34.1-14.9-41.6-38.8-.3-1-.4-1.9-.4-2.9 0-1.8.5-3.7 1.7-5.3 9.8-13.7 24.2-21.8 39.7-22.1 15.6-.3 31 8 41 22.1 1.1 1.5 1.7 3.4 1.7 5.2v.3c0 .9-.1 1.8-.4 2.7-7.6 23.9-23.6 38.8-41.7 38.8zm-23-40c5.3 13.7 13.9 22 23 22s17.6-8.3 23-22c-6.4-7.1-15-11.3-23.2-11.1-8.4.1-16.6 4.2-22.8 11.1z"/>
+ <path fill="#E2A042" d="M221.7 196.3c-18.8-18.8-49.2-18.8-67.9 0l-17.3 17.3c2.8-5 2.1-11.3-2.1-15.6-5.1-5.1-13.3-5.1-18.4 0-10.5 10.5-21.5 23.7-22.7 41.1-.5 7.6.8 15.3 4.1 23.3-2.4 2.5-4.6 5-7.1 7.9-1.4 1.6-2.8 3.4-4.3 5.1-1.5 1.8-3 3.6-4.5 5.7-1.5 2-3.1 4-4.5 6.2l-4.5 6.6c-1.5 2.3-3 4.7-4.4 7.1-1.4 2.5-2.9 4.9-4.3 7.5-1.3 2.6-2.7 5.2-4 7.8-.6 1.3-1.3 2.7-1.9 4-.6 1.3-1.3 2.7-1.8 4.1-1.2 2.7-2.4 5.5-3.4 8.3-.5 1.4-1.1 2.8-1.6 4.2l-1.5 4.2c-1 2.8-1.8 5.6-2.7 8.4-.8 2.8-1.6 5.6-2.2 8.4-1.3 5.5-2.4 11-3.2 16.2-.8 5.2-1.4 10.2-1.7 14.9-.4 4.7-.5 9-.5 13 0 3.9.1 7.5.3 10.5.2 3 .5 5.6.7 7.5.1.5.1 1 .2 1.4.1.5.1.9.2 1.2.1.4.1.8.2 1 .1.4.1.5.1.5 5.5 25.8 30.8 42.2 56.6 36.7 25.6-5.5 42-30.5 36.8-56.2V404.9s0 .1 0 0v-.1c-.1-.3-.3-.9-.5-1.9-.2-1.1-.5-2.5-.7-4.3-.2-1.8-.4-3.9-.5-6.3-.1-2.4-.1-5 0-7.8s.3-5.8.7-8.9c.2-1.5.4-3.1.7-4.7.3-1.6.5-3.2.9-4.8.2-.8.4-1.6.5-2.5.2-.8.4-1.6.6-2.5.4-1.7.9-3.3 1.4-4.9.2-.8.5-1.6.8-2.5l.8-2.4 1.8-4.8c.7-1.6 1.4-3.2 2-4.7.7-1.5 1.5-3.1 2.2-4.6.8-1.5 1.6-2.9 2.3-4.4.7-1.4 1.6-2.8 2.4-4.1.8-1.4 1.6-2.6 2.4-3.9.8-1.2 1.5-2.5 2.4-3.6 1.6-2.2 3.1-4.5 4.3-5.9.6-.8 1.2-1.6 1.7-2.3.1-.2.3-.3.4-.5l.2-.3.3-.3c.4-.4.7-.8 1.1-1.2.3-.4.6-.7.9-1 9.6-1.5 18.9-6 26.3-13.4l32-32c18.6-18.8 18.6-49.2-.1-68zM473.8 388.8c-.3-4.7-.9-9.7-1.7-14.9-.8-5.2-1.9-10.6-3.2-16.2-.7-2.8-1.4-5.5-2.2-8.4-.9-2.8-1.7-5.6-2.7-8.4l-1.5-4.2c-.5-1.4-1.1-2.8-1.6-4.2-1-2.8-2.2-5.5-3.4-8.3-.6-1.4-1.2-2.7-1.8-4.1-.6-1.3-1.3-2.7-1.9-4-1.3-2.7-2.7-5.2-4-7.8-1.4-2.5-2.9-5-4.3-7.5-1.5-2.4-3-4.8-4.4-7.1-1.5-2.3-3-4.5-4.5-6.6-1.5-2.2-3.1-4.2-4.5-6.2-1.5-2-3-3.9-4.5-5.7-1.5-1.8-2.9-3.6-4.3-5.1-2.1-2.4-4-4.6-6-6.7 3.7-8.4 5.2-16.5 4.6-24.5-1.3-17.5-12.2-30.6-22.7-41.1-5.1-5.1-13.3-5.1-18.4 0-4.2 4.2-4.9 10.6-2.1 15.6l-17.3-17.3c-18.8-18.8-49.2-18.8-67.9 0s-18.8 49.2 0 67.9l32 32c6.9 6.9 15.4 11.3 24.3 13.1.4.4.8.9 1.2 1.4.3.4.7.8 1.1 1.2l.3.3.2.3c.1.2.3.3.4.5.5.7 1.1 1.4 1.7 2.3 1.2 1.5 2.7 3.7 4.3 5.9.8 1.1 1.5 2.4 2.4 3.6.8 1.2 1.7 2.5 2.4 3.9.8 1.4 1.6 2.7 2.4 4.1.8 1.4 1.5 2.9 2.3 4.4.7 1.5 1.5 3 2.2 4.6.7 1.6 1.3 3.1 2 4.7.6 1.6 1.3 3.2 1.8 4.8l.8 2.4c.3.8.6 1.6.8 2.5.5 1.6 1 3.3 1.4 4.9.2.8.4 1.6.6 2.5.2.8.4 1.6.5 2.5.4 1.6.6 3.2.9 4.8.2 1.6.5 3.2.7 4.7.4 3.1.6 6.1.7 8.9.1 2.8.1 5.4 0 7.8-.1 2.4-.3 4.5-.5 6.3-.2 1.8-.5 3.2-.7 4.3-.2 1.1-.4 1.7-.5 1.9v-.1-.1c-5.2 25.6 11.2 50.7 36.8 56.2 25.8 5.5 51.1-10.9 56.6-36.7 0 0 0-.2.1-.5 0-.2.1-.6.2-1 0-.3.1-.7.2-1.2.1-.4.1-.9.2-1.4.2-1.9.5-4.5.7-7.5.2-3 .3-6.6.3-10.5 0-3.9-.1-8.3-.5-13z"/>
+ <path fill="#2B3B47" d="M230.8 336.9c5.3 13.7 13.9 22 23 22s17.6-8.3 23-22c-6.4-7.1-15-11.3-23.2-11.1-8.4.1-16.6 4.2-22.8 11.1z"/>
+</svg> \ No newline at end of file
diff --git a/devtools/client/themes/images/emojis/emoji-tool-scratchpad.svg b/devtools/client/themes/images/emojis/emoji-tool-scratchpad.svg
new file mode 100644
index 000000000..08128eb5d
--- /dev/null
+++ b/devtools/client/themes/images/emojis/emoji-tool-scratchpad.svg
@@ -0,0 +1,10 @@
+<!-- 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/. -->
+<svg height="16" width="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 72 72" opacity="1.0">
+ <path fill="#D1CFC3" d="M22.54 39.677c-8.686 0-16.823-3.548-21.237-9.26-1.818-2.352-1.386-5.733.968-7.553 2.35-1.814 5.734-1.385 7.552.97 2.383 3.082 7.375 5.074 12.72 5.074 2.975 0 5.384 2.41 5.384 5.384s-2.41 5.385-5.385 5.385zm48.324-9.26c1.818-2.352 1.386-5.733-.968-7.553-2.35-1.814-5.73-1.385-7.55.97-2.384 3.082-7.376 5.074-12.72 5.074-2.976 0-5.385 2.41-5.385 5.384s2.41 5.384 5.385 5.384c8.687 0 16.825-3.546 21.24-9.26z"/>
+ <path fill="#E5E4DF" d="M60.38 68.154c-.842.896-2.084 1.43-3.32 1.43-.036-.004-.078-.004-.118 0-1.786 0-3.454-.942-4.462-2.514-.562-.874-1.437-1.383-2.4-1.397-.924.013-1.796.522-2.358 1.397-1.026 1.597-2.652 2.513-4.462 2.513h-.117c-1.786 0-3.454-.94-4.462-2.513-.56-.874-1.436-1.383-2.398-1.397-.925.013-1.797.522-2.36 1.397-1.025 1.597-2.654 2.513-4.464 2.513-.035-.003-.075-.003-.115 0-1.786 0-3.454-.94-4.462-2.513-.56-.874-1.436-1.383-2.398-1.397-.925.013-1.8.522-2.36 1.394-1.025 1.6-2.654 2.516-4.464 2.516-.075 0-.152-.008-.227-.024-1.302-.057-2.43-.577-3.21-1.406-.61-.642-1.31-1.783-1.206-3.577.138-2.402 4.83-40.32 5.078-42.323C16.15 11.17 25.18 2.168 36.278 2.168c11.097 0 20.13 9 20.188 20.083.247 1.987 4.98 39.922 5.118 42.327.105 1.794-.596 2.937-1.204 3.577z"/>
+ <path fill="#2B3B47" d="M29.464 22.523v3.656h-.005c0 .004.004.006.004.01 0 1.435-1.165 2.6-2.6 2.6s-2.602-1.165-2.602-2.6v-.01h-.01v-3.657h.03c.116-1.332 1.22-2.38 2.58-2.38s2.462 1.048 2.576 2.38h.026zm18.708-1.126c-.18-2.103-1.92-3.758-4.066-3.758-2.15 0-3.892 1.654-4.075 3.757h-.048v5.773H40v.016c0 2.267 1.84 4.107 4.107 4.107s4.107-1.84 4.107-4.107c0-.004-.01-.007-.01-.016h.01v-5.773h-.042zM49.78 33.91c-.053-.1-.13-.18-.24-.192-1.108-.11-25.417-.106-26.522 0-.048.004-.095.027-.14.057-.005.004-.013.003-.02.008-.15.115-.185.344-.08.51l.24.386c.553.873 1.105 1.625 1.658 2.26.554.657 1.105 1.206 1.66 1.73 1.1 1.02 2.212 1.765 3.314 2.41.45.265.655.325.906.43.018-.012.046-.023.065-.035 1.797.874 3.726 1.375 5.747 1.375 1.982 0 3.876-.482 5.643-1.324l.004.002c.298-.138.597-.258.898-.423 1.104-.637 2.21-1.39 3.314-2.414.553-.523 1.105-1.074 1.655-1.736.553-.636 1.106-1.39 1.66-2.274l.242-.388c.03-.052.052-.112.056-.177.006-.077-.026-.142-.06-.204z"/>
+ <path fill="#FF473E" d="M42.35 33.637v7.606c0 3.358-2.716 6.08-6.066 6.08-3.358 0-6.077-2.722-6.077-6.08v-7.606H42.35z"/>
+ <path fill="#2B3B47" d="M29.62 36.086v-.83c0-2.12-5.67-1.62 6.227-1.62s7.09-.502 7.09 1.62v.83H29.62zm7.6 8.15v-6.395c0-.517-.42-.94-.94-.94s-.942.422-.942.94v6.397c0 .518.42.94.94.94s.942-.422.942-.94z"/>
+</svg> \ No newline at end of file
diff --git a/devtools/client/themes/images/emojis/emoji-tool-shadereditor.svg b/devtools/client/themes/images/emojis/emoji-tool-shadereditor.svg
new file mode 100644
index 000000000..1356caad1
--- /dev/null
+++ b/devtools/client/themes/images/emojis/emoji-tool-shadereditor.svg
@@ -0,0 +1,96 @@
+<!-- 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/. -->
+<svg height="16" width="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 277.9 279.4">
+ <style>
+ .st0{fill:#344555;} .st1{fill:url(#SVGID_1_);} .st2{fill:url(#SVGID_2_);} .st3{fill:url(#SVGID_3_);} .st4{fill:url(#SVGID_4_);} .st5{fill:url(#SVGID_5_);} .st6{fill:url(#SVGID_6_);} .st7{fill:url(#SVGID_7_);} .st8{fill:url(#SVGID_8_);} .st9{fill:url(#SVGID_9_);} .st10{fill:url(#SVGID_10_);} .st11{fill:#E9CCFF;} .st12{fill:none;stroke:#344555;stroke-width:0.4;stroke-miterlimit:10;} .st13{fill:#FBF6FA;} .st14{fill:#CEDAE6;} .st15{fill:#A2B3BF;}
+ </style>
+ <path class="st0" d="M245.8 78c-1.1-1.5-2.3-2.9-3.6-4.2 1-1.2 1.9-2.5 2.4-4 .5-1.3.7-2.5.7-3.8 0-.8-.1-1.4-.1-1.7 1.5-1.3 2.5-2.6 3.2-3.6 1.9-2.8 2.4-5.3 2.5-6.5.2-1.9-.2-3.1-.3-3.6l-.5-1.5c2.8-1.6 5.2-3.9 6.6-6.4 1.9-3.4 1.7-6.3 1.5-7.8-.1-.6-.3-1.3-.5-1.8.9-.9 1.6-1.8 2.1-2.8 1.2-2.3 1.7-4.8 1.1-7-.1-.6-.4-1.2-.7-1.7.4-.4.8-.9 1.1-1.4 1.2-1.8 1.7-3.9 1.4-5.7-.2-1-.5-1.9-1.1-2.6.8-1.1 1.4-2.3 1.7-3.6.3-1.2.4-2.5.2-3.6-.3-2.3-2.4-3.2-3.4-3.7-.3-.1-1.4-.6-2.2-.8-1-.2-1.9-.3-2.8-.3l-.1.1c-.7 0-1.3.1-1.9.2-.3 0-.5.1-.9.2h-.6l-.2.1c-.3.1-.5.2-.7.3-3.1 1.5-5.1 4.1-5.8 7.4-.1.8-.2 1.4-.2 1.9-1.3.4-2.5.8-3.5 1.5-2 1.2-3.6 2.9-4.6 4.8-.9 1.7-1 3.3-1 4.3-1.3.5-2.5 1.1-3.7 1.9-2.2 1.5-4.1 3.5-5.2 5.7-.2 0-.5-.1-.7-.1-.9 0-1.8.3-2.5.9-1.3 1-1.8 2.7-1.3 4.2v.6c0 .2-.1.4-.1.5-.1.2-.3.4-.5.6-.4.3-1.1.7-1.9 1.1-.7.3-1.6.6-2.7.9-.3.1-.5.1-.7.2-.4.1-.7.2-1.2.3l-.1-1.6V35c-.2-3.4-.4-6.7-.7-9.5-.4-5.3-.9-8.6-.9-8.7-.2-1.6-1.4-2.9-3-3.4-.3-.1-.7-.1-1-.1-1.3 0-2.5.6-3.2 1.6-.1.1-1.9 2.6-4.3 6.7-.9 1.5-1.7 3-2.4 4.5-.3-.4-.5-.8-.8-1.1-1.5-1.9-3.1-3.5-4.8-4.8-1.2-1-2-1.5-2.2-1.5-.6-.4-1.3-.5-2-.5-.8 0-1.6.2-2.3.7-1.3.9-1.9 2.5-1.6 4 0 0 .1.5.1 1.3.1 1.2 0 2.7-.2 4.2-.1.9-.3 1.9-.5 2.9l-.2 1.1-.1.6V33.3c-.1.6-.2 1.4-.3 2.1-.1.9-.2 1.9-.2 2.8-.5-.8-.9-1.5-1.3-2.2-3.1-5.2-5.3-8.3-5.4-8.4-.8-1.1-2-1.7-3.3-1.7h-.6c-1.5.2-2.7 1.3-3.2 2.8-.1.2-1.3 4.1-2.4 10.4-.7 3.9-1.1 7.6-1.2 11.2 0 1.1-.1 2.3 0 3.5-1.6-1.7-3.1-3.2-4.4-4.5-3.7-3.5-6.2-5.4-6.3-5.5-.7-.5-1.6-.8-2.4-.8-.7 0-1.3.2-1.9.5-1.4.7-2.2 2.2-2.1 3.8 0 .2.3 3.9 1.3 9.6.6 3.5 1.4 6.8 2.2 9.9.2.8.5 1.7.7 2.5l-5.1-4.1c-.7-.6-1.6-.9-2.5-.9-.7 0-1.4.2-2 .5-1.4.8-2.2 2.5-1.9 4.1.1.3.6 3.5 2.5 7.8.2.4.4.8.5 1.1-2.5-.8-5.1-1.4-7.8-1.8-2-.3-4-.4-6-.4h-1c-1.7 0-2.6.2-2.7.2-1.3.2-2.4 1-3 2.1s-.6 2.5 0 3.7c.1.1 1.6 3 4 7.3.6 1 1.2 2.1 1.9 3.2-5.5-3.3-9.7-4.8-10-4.9-.4-.1-.9-.2-1.3-.2-1.1 0-2.1.4-2.9 1.2-1.1 1.1-1.4 2.8-.9 4.2.1.4 2.6 6.8 7.5 14-.4-.1-.7-.2-1.1-.3-.1-.1-.3-.1-.4-.2h-.1c-2-.7-4.1-1.2-6.3-1.4-.9-.1-1.9-.1-2.7-.1-1.9 0-3.1.2-3.4.3-1.6.3-2.8 1.5-3.2 3.1-.3 1.6.4 3.2 1.7 4.1-.7.5-1.3 1.2-1.6 2-.1.2-.1.3-.1.5s-.1.3-.1.5c-.1.4-.2 1.7.1 3.5.1.8.5 2.6 1.6 4.5-6.3 1.9-12.2 4.7-17.5 8.4 0-.8-.1-1.5-.2-2.4-.8-5.2-2.7-8.4-4.1-10.2-.8-.9-1.6-1.8-2.5-2.3-.8-.5-1.4-.8-1.8-.9-.4-.1-.8-.2-1.2-.2-.4 0-.7 0-1.1.1-.1-.2-.3-.4-.5-.6-1.3-1.7-2.7-3.2-4.4-4.5-2.6-2.1-4.3-2.8-4.7-3-.4-.2-.9-.2-1.3-.2-.9 0-1.7.3-2.4.8-.9-.8-1.7-1.6-2.5-2.3-2.2-2-4.5-3.7-6.8-5.2-1.5-1-2.9-1.8-4.3-2.5-1-.5-1.5-.7-1.6-.8-.5-.2-1-.3-1.5-.3-1.4 0-2.7.7-3.4 1.9l-.3.6c-3.2-1.6-6.4-2.9-9.7-4.1-2.8-1-5.3-1.7-7.6-2.1-1.7-.4-2.7-.4-2.8-.5h-.3c-1.4 0-2.7.8-3.5 2-.2.4-.4.8-.5 1.3-1.4-1-2.8-2-4.2-2.9-3.1-2-6-3.5-8.7-4.7-2-.9-3.1-1.2-3.3-1.2-.3-.1-.7-.1-1-.1-1.2 0-2.4.6-3.2 1.6-1 1.3-1.1 3-.3 4.4 0 0 .6 1 1.7 2.8 1 1.5 2.6 4.1 5.1 7.7 2.4 3.5 5.5 7.9 8.5 12.2-.7-.3-1.4-.5-2.1-.7-.9-.3-1.6-.4-2.1-.5l-.5-.1c-.2 0-.4-.1-.7-.1-1.3 0-2.4.6-3.2 1.6-.9 1.2-1 2.8-.4 4.2.1.1 1.6 3.2 4.8 8.8 1.7 3.1 3.8 6.6 6.1 10.4l1.7 2.9 2.2 3.8v.1c.4.6.8 1.3 1.2 1.9-.3-.1-.6-.1-.9-.1-.8 0-1.7.3-2.4.8-1.2.9-1.8 2.4-1.6 3.8 0 .2.2 1 .6 2.4.6 1.8 1.5 3.6 2.5 5.5 1.7 2.9 3.7 5.6 6.1 8.1 1.8 1.9 3.5 3.4 5.3 4.7 2.2 1.6 4.6 3.2 7.9 4.5 3 1.1 5.8 1.8 8.6 2.1 1 .1 2 .2 2.9.2h1.6c.9 0 1.8 0 2.7-.1 2.4-.2 4.7-.5 6.9-1.1-.1.7-.1 1.5-.2 2.2-.6 9.2 1.3 18.4 5.4 26.6-1.3 1.8-2.5 3.5-3.6 5.2-4.5 6.8-11.4 20.6-14.2 26.2l-.1.3c-1.6 3.1-1.6 7.3 0 12 2.5 7.2 9.9 17.8 22.6 23.7 6.2 2.9 13.1 4.4 20.1 4.4 11.4 0 20.3-4 22.6-10.3.2-.6.4-1.2.5-1.9h.2v-1.6c.1-.8 0-1.7 0-2.6 0-.2-.3-14.4 1-26.7 2.7.4 5.5.8 8.4.8.9 0 1.7 0 2.5-.1 4.8-.4 9.5-1.2 14-2.4 4.3 11.9 7.3 24.4 7.7 25.9.2 1.3.7 4.1 1.6 5.7 2.5 4.5 8.7 7.1 17 7.1 8.9 0 18.4-3 26-8.2 11.5-7.9 17.1-19.5 18.4-27.1.8-4.9.2-9-1.9-11.9l-.1-.1c-3.6-5.1-12.7-17.6-18.2-23.6-1.8-2-3.9-4.1-6.1-6.2 2-5.8 2.9-11.9 2.8-18.2 0-1.8-.2-3.5-.4-5.3 1.1-3.8 3.1-10.1 6.1-15.4 3.1-5.5 6.9-10.6 9.1-13.3 3.9 2.5 8.6 4.5 14.1 4.5.9 0 1.8-.1 2.7-.2 7.2-.8 13.3-2.3 18.3-4.4.1 0 .2-.1.3-.1h.1c1.9.3 3.8.5 5.6.5 8.9 0 17.2-3.9 23.4-11.1 5.8-6.7 9.1-15.6 9.1-24.5 0-16.8-12.8-29-32.1-30.7z"/>
+ <linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="22.015" y1="164.168" x2="71.347" y2="164.168">
+ <stop offset="0" stop-color="#D116C5"/>
+ <stop offset="1" stop-color="#B50DD8"/>
+ </linearGradient>
+ <path class="st1" d="M69.5 170.7c-1.2-.2-3-.6-5.3-1s-5.1-.9-8.4-1.5c-3.2-.6-6.9-1.4-10.6-2.8-3.5-1.4-6.8-3.4-9.6-5.2-2.9-1.8-5.3-3.3-7.3-4.5-4-2.5-6.4-3.6-6.4-3.6s.1.6.5 1.7 1 2.7 2.2 4.8c1.2 2.1 2.9 4.6 5.5 7.3 1.3 1.4 2.9 2.8 4.7 4.2 1.9 1.4 4.1 2.8 6.9 3.9 2.7 1 5.3 1.6 7.7 1.8 1.2.2 2.3.2 3.4.2s2.1 0 3-.1c3.8-.3 6.8-1 9.1-1.8s3.8-1.6 4.8-2.2c1-.6 1.4-1 1.4-1s-.4 0-1.6-.2z"/>
+ <linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="10.494" y1="142.827" x2="68.624" y2="142.827">
+ <stop offset="0" stop-color="#F83510"/>
+ <stop offset="1" stop-color="#D92E0E"/>
+ </linearGradient>
+ <path class="st2" d="M68.6 166.5s-2.3-1.5-7.5-4.7c-2.6-1.7-5.8-3.8-9.6-6.7-1.9-1.4-3.9-3.1-6-4.9-1.1-.9-2.1-2-3.2-3-.5-.5-1-1.1-1.6-1.7-.5-.6-1.1-1.1-1.6-1.8-1-1.3-2.1-2.4-2.9-3.7-.4-.6-.9-1.2-1.3-1.8-.4-.6-.9-1.3-1.3-1.9-1.7-2.5-3.5-4.6-5.2-6.5-3.4-3.7-6.7-6.2-9.4-7.9-2.7-1.6-4.9-2.5-6.3-2.9-.7-.2-1.2-.3-1.6-.4-.4-.1-.5-.1-.5-.1s1.5 3 4.7 8.6c1.6 2.8 3.6 6.2 6 10.3 1.2 2 2.5 4.2 4 6.6 1.5 2.4 2.7 4.8 5 7.8.5.8 1.1 1.4 1.7 2.1.6.6 1.2 1.3 1.7 1.9.6.6 1.2 1.1 1.9 1.7.6.5 1.2 1.1 1.9 1.5 2.5 1.9 5 3.3 7.4 4.4 4.8 2.1 9.1 2.9 12.4 3.3 3.4.3 5.9.1 7.6-.1 1.7-.2 2.5-.5 2.5-.5s-.1 0-.2-.1c.9.4 1.4.5 1.4.5z"/>
+ <linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="4" y1="129.162" x2="77.422" y2="129.162">
+ <stop offset="0" stop-color="#F6A500"/>
+ <stop offset="1" stop-color="#F5A100"/>
+ </linearGradient>
+ <path class="st3" d="M72 160c1.1.8 2.1 1.4 2.8 2 .4.3.8.6 1.1.8.3.3.6.5.8.6.4.3.6.5.6.5s-.1-.2-.3-.7c-.2-.4-.6-1.1-1.1-2-1.1-1.7-3-4.2-5.7-7.3-2.8-3.2-6.5-7-11-11.7-4.5-4.7-9.9-10.3-15.4-17.3-1.4-1.8-2.8-3.6-4.1-5.3-1.3-1.7-2.8-3.3-4.1-4.8-1.4-1.5-2.7-2.9-4-4.2-1.3-1.3-2.6-2.6-3.8-3.7-5-4.6-9.4-7.8-13-10.1-3.6-2.3-6.4-3.6-8.2-4.5-1.8-.8-2.8-1-2.8-1s.5.9 1.6 2.6c1.1 1.7 2.7 4.3 5 7.6s5.2 7.4 8.6 12.3c1.7 2.4 3.6 5 5.6 7.9 1 1.4 2 2.9 3 4.4 1 1.5 2.1 3.2 3.2 4.8 9.1 13.6 18.7 22.3 26.3 26.1 3.7 1.9 6.7 2.8 8.6 3.4 1 .3 1.7.5 2.2.6 5.5 4.2 8.7 5.9 8.7 5.9s-.9-2.4-4.6-6.9z"/>
+ <linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="25.262" y1="129.634" x2="83.392" y2="129.634">
+ <stop offset="0" stop-color="#6EDC00"/>
+ <stop offset="1" stop-color="#66CB00"/>
+ </linearGradient>
+ <path class="st4" d="M79.7 143.1c-1.3-3-3-6.9-5.7-11.5-1.3-2.3-2.9-4.8-4.8-7.4-1.9-2.6-4.1-5.4-6.9-8.1-1.3-1.4-2.8-2.6-4.1-3.8-.7-.6-1.4-1.1-2.1-1.6-.7-.5-1.4-1-2-1.5-2.7-1.8-5.2-3.3-7.6-4.6-4.7-2.5-8.7-4.1-11.8-5.2-3.2-1.1-5.5-1.7-7.1-2-1.6-.3-2.3-.4-2.3-.4s.5.8 1.6 2.2c1.1 1.5 2.7 3.6 4.9 6.4 1.1 1.4 2.2 3 3.6 4.7.6.9 1.3 1.8 2 2.8.7 1 1.4 2 2.1 3.1 3.1 4.4 6.9 9.4 11.6 14.9 2.3 2.7 4.6 5.2 6.8 7.5l3.3 3.3c1 1 2 1.9 3 2.8 1.8 1.7 3.4 3.5 4.9 5.1 1.5 1.6 2.8 3.1 4 4.4 2.4 2.6 4.3 4.6 5.6 5.9 1.3 1.3 2 1.9 2 1.9s-.1-.8-.6-2.4c-.5-1.6-1.3-4-2.7-7.3-1.4-3.2-3.3-7.3-6.7-12-.4-.6-.9-1.2-1.4-1.8-.5-.6-.9-1.1-1.4-1.7-.9-1.1-1.9-2.3-2.9-3.5-2.1-2.5-4.3-5.2-6.8-8.1-.4-.5-.9-1-1.3-1.5 4.1 4.1 7.4 8.2 10.4 11.7 3.2 3.8 5.9 7.1 8.3 9.6 2.3 2.5 4.3 4.3 5.7 5.4.7.6 1.3.9 1.6 1.1l.6.3s-1.2-2.6-3.8-8.7z"/>
+ <linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="49.456" y1="121.778" x2="86.397" y2="121.778">
+ <stop offset="0" stop-color="#0F92F7"/>
+ <stop offset="1" stop-color="#0988F5"/>
+ </linearGradient>
+ <path class="st5" d="M86.1 130c-.6-4.1-2.1-6.8-3.3-8.3-.6-.8-1.2-1.3-1.6-1.6-.4-.3-.7-.4-.7-.4v.8c0 .5-.1 1.2-.1 2.2 0 1.3 0 2.9.1 4.9-.2-.8-.5-1.7-.7-2.6-.5-1.5-1.1-3-1.8-4.2-.7-1.2-1.4-2.2-2.1-3.1-1.4-1.8-2.7-3-3.8-3.9-2.2-1.8-3.5-2.3-3.5-2.3s.4 1.5 1.5 4.1c.5 1.3 1.3 2.9 2.1 4.7.4.9.8 1.9 1.3 2.9.2.5.4 1.1.6 1.6l.6 1.8v.1c-1.5-2.6-3.3-5.6-5.6-8.9-2.8-3.8-5.6-6.6-8.1-8.8-2.5-2.2-4.7-3.7-6.4-4.9-1.7-1.1-3.1-1.9-3.9-2.3-.9-.4-1.3-.6-1.3-.6s.4.4 1.2 1.1c.8.7 2 1.8 3.6 3.2 1.6 1.4 3.5 3.3 5.7 5.5 2.2 2.3 4.7 5 7.1 8.4 4.8 6.9 8.2 12.4 10.6 15.9.3.5.7 1 1 1.5.3.6.5 1.1.8 1.6 1.3 2.6 2.3 3.8 2.3 3.8s.4-1.3.4-4.1c0-.6 0-1.4-.1-2.2.3 1.1.5 2 .7 2.7.6 1.9.8 2.8.8 2.8s.2-.1.6-.5c.3-.4.8-1 1.2-1.9.8-1.8 1.5-4.8.8-9z"/>
+ <linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="110.05" y1="107.846" x2="147.716" y2="107.846">
+ <stop offset="0" stop-color="#D116C5"/>
+ <stop offset="1" stop-color="#B50DD8"/>
+ </linearGradient>
+ <path class="st6" d="M118.8 118.8c-1-1.1-2.2-1.7-3.2-2.2-1-.4-2-.6-2.8-.8-1.6-.2-2.7-.1-2.7-.1s.2.3.6.7c-.4-.3-.7-.4-.7-.4s-.1.9.1 2.2c.2 1.3.8 3.1 2.1 4.4.9 1 2 1.4 2.8 1.5h1c.2 0 .4-.1.4-.1s.1-.6 0-1.3c0-.3-.1-.6-.2-.9 1.1 1 2.1 1.5 2.9 1.7.9.2 1.5.2 1.5.2s.1-.6-.1-1.6c-.2-.9-.6-2.1-1.7-3.3zm12-5c-2.8-1.4-5.5-2.3-7.9-2.9-.6-.2-1.1-.3-1.7-.4-.2-.1-.4-.2-.6-.2-2-.7-3.8-1-5.4-1.2-3.1-.3-4.9.1-4.9.1s1.5 1 3.5 2.8c2 1.7 4.4 4.1 6.8 6.7 1.9 2 3.4 3.2 4.6 4 1.2.8 2 1.2 2 1.2s.4-.9.3-2.5v-.3c.7.4 1.3.6 1.8.8 1.5.6 2.5.6 2.5.6s0-.9-.6-2.4c2.1 1.1 3.8 1.8 5.1 2.4 1.8.7 2.9 1 2.9 1s-.1-1.2-1.2-3c-1.1-2.1-3.2-4.6-7.2-6.7zm10.3-8.3c-10-10.1-21.5-14-21.5-14s4.6 12.1 14 21.5c7.3 7.3 14.1 6.6 14.1 6.6s.6-6.9-6.6-14.1z"/>
+ <linearGradient id="SVGID_7_" gradientUnits="userSpaceOnUse" x1="128.647" y1="96.959" x2="162.052" y2="96.959">
+ <stop offset="0" stop-color="#F83510"/>
+ <stop offset="1" stop-color="#D92E0E"/>
+ </linearGradient>
+ <path class="st7" d="M161.2 97.1c-.7-2.2-2-4.8-4.2-7.2-3.1-3.4-6.8-5.6-10.3-7-3.5-1.4-6.7-2-9.4-2.4-2.7-.4-4.8-.4-6.3-.4s-2.3.1-2.3.1 1.5 2.9 3.9 7.2c1.2 2.1 2.7 4.6 4.3 7.2.8 1.3 1.6 2.7 2.5 4 .9 1.4 1.7 2.7 2.6 4.2 2.9 4.5 5.8 7.2 7.9 8.8 2.1 1.6 3.6 2.1 3.6 2.1s.3-1.5-.2-4.2-1.7-6.5-4.6-11c-.3-.4-.5-.8-.8-1.2-.3-.4-.6-.8-.8-1.2-.5-.8-1.1-1.5-1.7-2.2-1.2-1.4-2.3-2.7-3.5-3.8-2.3-2.3-4.6-4.2-6.5-5.7l-1.2-.9 1.8 1.2c2.1 1.5 4.5 3.3 6.9 5.5 1.2 1.1 2.4 2.3 3.5 3.5 1.1 1.3 2.2 2.6 3.1 4.1 3.1 4.5 4.9 8 6.3 10.3 1.3 2.3 2.1 3.5 2.1 3.5s.1-.3.3-1c.1-.7.3-1.8.2-3.2-.1-1.4-.3-3.3-1-5.4-.7-2.1-1.7-4.6-3.5-7.1-.6-.9-1.3-1.7-1.9-2.5l-.4-.4 1.5 1.5c3.6 4 5.3 7.6 6.5 10 1.2 2.5 1.7 3.7 1.7 3.7s.2-.3.4-1 .4-1.8.5-3.3c-.1-1.6-.3-3.6-1-5.8z"/>
+ <linearGradient id="SVGID_8_" gradientUnits="userSpaceOnUse" x1="146.483" y1="72.45" x2="176.962" y2="72.45">
+ <stop offset="0" stop-color="#F6A500"/>
+ <stop offset="1" stop-color="#F5A100"/>
+ </linearGradient>
+ <path class="st8" d="M164.2 83.5c-.3-.4-.6-.8-.9-1.1l-.5-.6-.6-.6-.6-.5-.4-.3c-.3-.2-.6-.4-.8-.6-1.1-.9-2.2-1.7-3.2-2.5-2.1-1.7-3.9-3.2-5.5-4.4-3.3-2.6-5.2-4.2-5.2-4.2s.5 2.8 2.2 6.9c.9 2 2.1 4.3 3.8 6.6.8 1.2 1.8 2.3 2.9 3.5 1.4 1.3 1.7 1.7 2.4 2.7 1.1 1.5 2.2 3 3.3 4.3 1 1.2 2 2.2 2.9 3 1.7 1.5 3 2.1 3 2.1s.6-1.3.7-3.7c0-1.2-.1-2.7-.6-4.5-.5-1.9-1.2-3.9-2.9-6.1zm12.6-4.6c-.2-1.3-.6-3-1.3-4.9-.7-1.9-1.7-4.1-3.2-6.5-4.2-6.7-9-11.5-12.5-15-3.5-3.4-6-5.2-6-5.2s.2 3.6 1.2 9.2c.5 2.8 1.2 6 2.1 9.6 1 3.5 2.2 7.3 4.2 11.1.4.8.7 1.4 1.1 2 .4.6.7 1.2 1 1.8.7 1.1 1.3 2.2 1.8 3.1 1.1 1.9 2.1 3.4 2.9 4.6 1.6 2.4 2.5 3.5 2.5 3.5s.3-.3.6-.9c.4-.7.9-1.7 1.3-3.2.4-1.5.7-3.5.5-6-.1-1.4-.4-3-.9-4.6.8 1.1 1.4 2 2 2.7.8 1 1.4 1.7 1.8 2.2.4.5.6.8.6.8s.1-.3.2-1c.3-1 .3-2 .1-3.3z"/>
+ <linearGradient id="SVGID_9_" gradientUnits="userSpaceOnUse" x1="170.938" y1="55.875" x2="192.731" y2="55.875">
+ <stop offset="0" stop-color="#6EDC00"/>
+ <stop offset="1" stop-color="#66CB00"/>
+ </linearGradient>
+ <path class="st9" d="M192.1 63.9c-.2-.8-.5-1.6-.8-2.5-.3-.8-.6-1.5-.9-2.2-3.8-8.4-7.7-15.7-10.8-20.8-3.1-5.1-5.2-8.2-5.2-8.2s-1.2 3.8-2.2 9.8c-.5 3-1 6.6-1.2 10.6-.1 4-.1 8.4 1.2 13.1 2 6.5 3.8 10.8 5.2 13.7 1.4 2.8 2.3 4.1 2.3 4.1s.4-1.6.3-4.8c-.1-2.2-.4-5.1-1.1-8.7.1.3.3.6.4.9.1.3.2.5.3.8.1.3.2.5.2.8.2.5.3 1 .4 1.5.5 1.9.5 3.6.4 5-.1 1.4-.4 2.6-.6 3.4l-.3 1.2s.5 0 1.3-.4 2-1.1 3.2-2.6c1.1-1.4 2.3-3.5 2.6-6.3.1.7.2 1.4.2 2.1.1 1.4 0 2.5-.1 3.3-.1.8-.2 1.2-.2 1.2s.4-.1 1.2-.5c.8-.4 1.9-1.3 2.9-2.9 1-1.6 1.9-3.9 1.9-6.9.1-1.5-.2-3.1-.6-4.7z"/>
+ <linearGradient id="SVGID_10_" gradientUnits="userSpaceOnUse" x1="188.198" y1="40.946" x2="228.677" y2="40.946">
+ <stop offset="0" stop-color="#0F92F7"/>
+ <stop offset="1" stop-color="#0988F5"/>
+ </linearGradient>
+ <path class="st10" d="M228.6 37c-.2-1.1-.6-2-1-2.6-.4-.6-.8-1-1.1-1.2-.3-.2-.4-.3-.4-.3s.1.2.1.6c.1.4.1.9 0 1.5s-.2 1.3-.6 2c-.4.7-.9 1.3-1.6 1.8-.7.6-1.6 1-2.6 1.5s-2.1.8-3.4 1.2c-1.3.4-2.8.7-4.7 1.6-.6.3-1.3.6-1.9 1.1 0-1-.1-2.1-.1-3-.1-1.9-.2-3.8-.3-5.6-.2-3.5-.4-6.7-.6-9.4-.4-5.3-.9-8.5-.9-8.5s-1.7 2.4-4 6.3c-1.1 2-2.4 4.3-3.6 7.1-.9 1.9-1.6 4.1-2.1 6.4-.1-.5-.2-.9-.3-1.4-.7-3.6-2.4-6.3-3.9-8.3-1.6-2-3-3.3-4.1-4.2-1.1-.8-1.7-1.2-1.7-1.2s.1.7.2 1.9 0 2.9-.2 4.9c-.1 1-.3 2.1-.5 3.2-.1.6-.2 1.1-.3 1.7l-.3 2.1c-.3 2.8-.4 5.8-.2 8.9.3 4.7 1.7 8 2.9 10s2.3 2.9 2.3 2.9.9-1.1 1.6-3.4c.6-1.8 1.2-4.3 1.2-7.5v.2c1.2 5.4 3 9 4.3 11.3 1.4 2.2 2.4 3.1 2.4 3.1s.2-1.3.1-3.9c-.1-2-.4-4.8-1-8.4.2-2.2.4-4.9.8-8.4.2-1.6.4-3.2.8-4.7.1-.8.3-1.5.5-2.2l.6-2.1c.1-.2.1-.4.2-.6-.3 1.4-.7 2.8-1 4.3-.7 3.6-1 7.5-.9 11.5.1 6.2 1 10.5 1.7 13.3.8 2.8 1.4 4 1.4 4s.3-.4.7-1.1c.5.8.8 1.2.8 1.2s1.2-1.1 2.7-3.2c.7-1 1.6-2.3 2.4-3.8.4-.7.8-1.5 1.3-2.3.2-.4.4-.8.7-1.2l.3-.6c.1-.1.2-.3.3-.4.5-.8 1-1.3 2.1-2 1-.7 2.6-1.4 4.1-2.4 1.5-1 2.9-2.1 4-3.4 1.1-1.2 1.9-2.6 2.4-4 .5-1.8.6-3.2.4-4.3z"/>
+ <path class="st0" d="M255.1 30.5l-1.3 2.1c-.1.1-.2.3-.6.5-.4.3-1.3.9-2.7 1.3-1.2.4-2.7.5-4.7.6h-.9c-.8 0-1.7 0-2.7-.1h-1.5l-.6-.1c-.8-.1-2.1-.1-3.3-.1h-.3c-4.7 0-9.5 1.4-13.2 3.9-3.7 2.4-5.5 5.4-6.5 7.5-.8 1.9-1.3 3.8-1.2 5.7 0 .6.1 1.2.2 1.6.1.3.1.5.2.6l.8 2.3 2.1-1.2 1.4-.8c1.2-.6 2.4-1.2 3.6-1.7 1.6-.6 3.4-1.1 5.4-1.4 2.3-.4 4.6-.5 7.1-.5H239.3c1 0 2.2 0 3.4-.2l1.2-.2h.5c.5-.1.9-.2 1.3-.3l.2-.1c1.1-.3 2.1-.7 2.9-1 3.5-1.4 6.5-4 8.1-6.9 1.7-3.1 1.5-5.8 1.3-6.8-.1-.9-.4-1.5-.7-1.9-.2-.4-.4-.6-.5-.7l-1.9-2.1z"/>
+ <path class="st0" d="M255.9 20.5l-1.1 1.5s-.1.1-.3.2c-.2.1-.9.4-1.8.4h-.6c-.8 0-1.6-.1-2.8-.2-.3 0-.7-.1-1.1-.1-.4 0-.9-.1-1.4-.1h-.2c-.3 0-.6-.1-1-.1l-.5-.1c-.5-.1-1.5-.1-2.4-.1h-.2c-3.6 0-7.1 1-9.8 2.9-3 2-4.4 4.5-4.9 5.8-.6 1.5-.9 3.1-.8 4.6.1 1.1.4 1.7.5 1.9l.8 1.6 1.7-.7s.4-.2 1.1-.4c.6-.2 1.6-.5 2.7-.7 1-.2 2.3-.3 3.8-.4 1.2-.1 2.5-.1 4.2-.1H244.6c.8 0 1.6-.1 2.4-.2.6-.1 1.5-.2 2.3-.5.3-.1.7-.2 1-.3h.2l.6-.2.4-.2c1.3-.5 2.6-1.3 3.7-2.3 1.1-1 1.9-2 2.4-3 1.4-2.6 1.1-4.7.9-5.6-.2-.8-.5-1.3-.7-1.6-.2-.3-.4-.5-.6-.7l-1.3-1.3zM249 54.3c.2-1.5-.1-2.4-.2-2.7l-.5-1.4-1.5.1h-.1c-.2 0-.6 0-1.1-.1-.8-.1-2-.3-3.6-.6l-.3-.1c-1.5-.3-3.1-.6-5.1-.8h-.5c-.4 0-.7-.1-1.1-.1H230c-.6.1-1.1.1-1.7.2-4.1.6-7.7 2.2-10.8 4.7-2 1.6-3.5 3.5-4.7 5.5l-.1.1-.1.1c-.5 1.2-1.7 3.5-.5 4.7 6.5 6.3 24.1 11 28.5 11.6-.1-5.1-.9-9.8 0-10.2 2.6-1.5 4.9-3.5 6.4-5.8 1.5-2 1.9-4.1 2-5.2z"/>
+ <path class="st11" d="M240.7 65.8c-.4.2-1 .6-1.8 1.1-.8.4-1.7 1-2.7 1.4-1.1.4-2.3.8-3.8.9-.8.1-1.7.1-2.5.1s-1.6 0-2.2.1c-1.4.1-2.6.2-3.5.3-.3 0-.6.1-.8.1 1.1.4 2.2.9 3.3 1.3 3.1 1.1 6.2 2 9.2 2.7 1-.4 1.9-1 2.6-1.6 1.3-1.2 2-2.4 2.4-3.5.4-1 .5-1.9.5-2.4 0-.6-.1-.8-.1-.8s-.1.1-.6.3zm13.5-31.6c-.2-.3-.2-.4-.2-.4s-.3.5-1 1-1.7 1.2-3.1 1.6c-1.4.4-2.9.6-5 .7-1 0-2.2 0-3.5-.1-.3 0-.7 0-1-.1-.4 0-.7-.1-1.1-.1-.6-.1-1.8-.1-2.9-.1-4.6-.1-8.7 1.5-11.5 3.5-2.9 2-4.5 4.5-5.4 6.5-.9 2.1-1 3.7-1 4.7 0 .5.1.9.1 1.2.1.3.1.4.1.4s.5-.3 1.4-.8c.9-.5 2.1-1.1 3.7-1.8 1.5-.6 3.4-1.1 5.4-1.5 2.1-.3 4.4-.6 7.3-.6H238.9c1 0 1.9 0 2.9-.2.5-.1.9-.1 1.4-.2.5-.1.9-.2 1.3-.3.9-.2 1.7-.6 2.5-.9 3.2-1.4 5.5-3.7 6.6-5.9 1.2-2.2 1.2-4.2 1-5.4 0-.5-.2-1-.4-1.2zm-8.9 18.1c-1-.1-2.3-.3-3.8-.6s-3.2-.6-5.3-.8c-.5 0-1-.1-1.6-.1-.5 0-1.1-.1-1.7-.1H229.8c-.5.1-1 .1-1.6.2-4.3.7-7.5 2.5-9.8 4.3-2 1.6-3.3 3.4-4.2 4.9-.3.9-.7 2.4-.4 3.6.1.5.3 1 .5 1.4 1.2.8 2.6 1.5 3.9 2.2h.2c1.7.1 3.7.1 5.8 0 2.1-.1 4.2-.3 6.5-.7.8-.1 2.9-.5 4.4-.9 1.6-.5 3-1.1 4.3-1.8 2.6-1.5 4.5-3.3 5.7-5.1 1.2-1.8 1.7-3.5 1.8-4.7.1-1.2-.1-1.8-.1-1.8s-.5.1-1.5 0zm11.2-28.7c-.1-.2-.2-.3-.2-.3s-.3.3-.9.6c-.6.3-1.5.6-2.6.6-1.1.1-2.2 0-3.6-.2-.7-.1-1.5-.1-2.5-.3-.5 0-1-.1-1.5-.2-.4 0-1.4-.1-2.2-.1-3.5 0-6.7 1-8.9 2.5-2.3 1.5-3.6 3.4-4.2 4.9-.6 1.6-.7 2.8-.6 3.7.1.8.3 1.2.3 1.2s.4-.2 1.2-.4c.7-.3 1.8-.6 3-.8 1.2-.2 2.6-.4 4.1-.4 1.5-.1 3.1-.1 5.2-.1h1.8c.8 0 1.5 0 2.2-.2.7-.1 1.4-.2 2.1-.4.3-.1.7-.2 1-.3.3-.1.6-.3.9-.4 1.2-.5 2.3-1.2 3.2-2 .9-.8 1.6-1.6 2-2.5.9-1.7 1-3.2.8-4.1-.3-.3-.4-.6-.6-.8zm1.8-9s-.2.2-.7.3c-.4.1-1.1.1-1.8 0s-1.5-.3-2.5-.5c-.3 0-.5-.1-.8-.1-.3 0-.6-.1-.9-.1-.7-.1-1.2-.2-2.5-.2-2.3 0-4.3.6-5.9 1.5-1.5.9-2.5 2.1-3.1 3.1-.5 1.1-.6 1.9-.6 2.5 0 .6.2.9.2.9s1.3-.4 3-.4 3.6.2 6.4.2h1.1c.5 0 1 0 1.4-.1.9-.1 1.8-.4 2.6-.7 1.6-.7 2.9-1.7 3.6-2.8.8-1.1.9-2.2.8-2.9.1-.5-.3-.7-.3-.7zm1.3-8.9c0-.3-.1-.5-.2-.6l-.1-.1s-.1.1-.2.1-.3 0-.5-.1c-.4-.1-.9-.3-1.5-.4-.6-.1-1.3-.2-2.1-.2-.4 0-.8 0-1.2.1-.2 0-.4.1-.6.1h-.2l-.2.1c-.2.1-.3.1-.5.2-2.4 1.2-3.4 3.1-3.7 4.6-.2.8-.1 1.5 0 1.9.1.5.3.7.3.7.1-.4 3.8 1.4 6.8-.1.2-.1.3-.2.6-.3.3-.1.5-.3.7-.5.5-.3.8-.7 1.1-1.1.6-.8 1-1.6 1.2-2.4.4-.7.4-1.5.3-2z"/>
+ <path class="st12" d="M240.7 65.8c-.4.2-1 .6-1.8 1.1-.8.4-1.7 1-2.7 1.4-1.1.4-2.3.8-3.8.9-.8.1-1.7.1-2.5.1s-1.6 0-2.2.1c-1.4.1-2.6.2-3.5.3-.3 0-.6.1-.8.1 1.1.4 2.2.9 3.3 1.3 3.1 1.1 6.2 2 9.2 2.7 1-.4 1.9-1 2.6-1.6 1.3-1.2 2-2.4 2.4-3.5.4-1 .5-1.9.5-2.4 0-.6-.1-.8-.1-.8s-.1.1-.6.3zm13.5-31.6c-.2-.3-.2-.4-.2-.4s-.3.5-1 1-1.7 1.2-3.1 1.6c-1.4.4-2.9.6-5 .7-1 0-2.2 0-3.5-.1-.3 0-.7 0-1-.1-.4 0-.7-.1-1.1-.1-.6-.1-1.8-.1-2.9-.1-4.6-.1-8.7 1.5-11.5 3.5-2.9 2-4.5 4.5-5.4 6.5-.9 2.1-1 3.7-1 4.7 0 .5.1.9.1 1.2.1.3.1.4.1.4s.5-.3 1.4-.8c.9-.5 2.1-1.1 3.7-1.8 1.5-.6 3.4-1.1 5.4-1.5 2.1-.3 4.4-.6 7.3-.6H238.9c1 0 1.9 0 2.9-.2.5-.1.9-.1 1.4-.2.5-.1.9-.2 1.3-.3.9-.2 1.7-.6 2.5-.9 3.2-1.4 5.5-3.7 6.6-5.9 1.2-2.2 1.2-4.2 1-5.4 0-.5-.2-1-.4-1.2zm-8.9 18.1c-1-.1-2.3-.3-3.8-.6s-3.2-.6-5.3-.8c-.5 0-1-.1-1.6-.1-.5 0-1.1-.1-1.7-.1H229.8c-.5.1-1 .1-1.6.2-4.3.7-7.5 2.5-9.8 4.3-2 1.6-3.3 3.4-4.2 4.9-.3.9-.7 2.4-.4 3.6.1.5.3 1 .5 1.4 1.2.8 2.6 1.5 3.9 2.2h.2c1.7.1 3.7.1 5.8 0 2.1-.1 4.2-.3 6.5-.7.8-.1 2.9-.5 4.4-.9 1.6-.5 3-1.1 4.3-1.8 2.6-1.5 4.5-3.3 5.7-5.1 1.2-1.8 1.7-3.5 1.8-4.7.1-1.2-.1-1.8-.1-1.8s-.5.1-1.5 0zm11.2-28.7c-.1-.2-.2-.3-.2-.3s-.3.3-.9.6c-.6.3-1.5.6-2.6.6-1.1.1-2.2 0-3.6-.2-.7-.1-1.5-.1-2.5-.3-.5 0-1-.1-1.5-.2-.4 0-1.4-.1-2.2-.1-3.5 0-6.7 1-8.9 2.5-2.3 1.5-3.6 3.4-4.2 4.9-.6 1.6-.7 2.8-.6 3.7.1.8.3 1.2.3 1.2s.4-.2 1.2-.4c.7-.3 1.8-.6 3-.8 1.2-.2 2.6-.4 4.1-.4 1.5-.1 3.1-.1 5.2-.1h1.8c.8 0 1.5 0 2.2-.2.7-.1 1.4-.2 2.1-.4.3-.1.7-.2 1-.3.3-.1.6-.3.9-.4 1.2-.5 2.3-1.2 3.2-2 .9-.8 1.6-1.6 2-2.5.9-1.7 1-3.2.8-4.1-.3-.3-.4-.6-.6-.8zm1.8-9s-.2.2-.7.3c-.4.1-1.1.1-1.8 0s-1.5-.3-2.5-.5c-.3 0-.5-.1-.8-.1-.3 0-.6-.1-.9-.1-.7-.1-1.2-.2-2.5-.2-2.3 0-4.3.6-5.9 1.5-1.5.9-2.5 2.1-3.1 3.1-.5 1.1-.6 1.9-.6 2.5 0 .6.2.9.2.9s1.3-.4 3-.4 3.6.2 6.4.2h1.1c.5 0 1 0 1.4-.1.9-.1 1.8-.4 2.6-.7 1.6-.7 2.9-1.7 3.6-2.8.8-1.1.9-2.2.8-2.9.1-.5-.3-.7-.3-.7zm1.3-8.9c0-.3-.1-.5-.2-.6l-.1-.1s-.1.1-.2.1-.3 0-.5-.1c-.4-.1-.9-.3-1.5-.4-.6-.1-1.3-.2-2.1-.2-.4 0-.8 0-1.2.1-.2 0-.4.1-.6.1h-.2l-.2.1c-.2.1-.3.1-.5.2-2.4 1.2-3.4 3.1-3.7 4.6-.2.8-.1 1.5 0 1.9.1.5.3.7.3.7.1-.4 3.8 1.4 6.8-.1.2-.1.3-.2.6-.3.3-.1.5-.3.7-.5.5-.3.8-.7 1.1-1.1.6-.8 1-1.6 1.2-2.4.4-.7.4-1.5.3-2z"/>
+ <path class="st0" d="M182.3 46.7c8.9 2.9 18.2 11 23.1 18.3 1.6-.2 3.2-.3 4.8-.3 14.2 0 26.6 7 33.4 17.5h.9c16.2 1.1 29.4 10.6 29.4 26.9 0 16.3-12.2 31.6-28.5 31.6-1.7 0-3.3-.1-5-.4-.3 0-.5-.1-.8-.1-.6 0-1.2.1-1.8.4-4.3 1.8-9.9 3.3-17.2 4.1-.8.1-1.5.1-2.2.1-5.3 0-9.8-2.3-14.7-5.7-.5 0-7 7.6-12 16.5-3.3 5.9-5.5 12.9-6.7 17 .2 1.9.4 3.8.4 5.7.1 6.9-1.1 13.3-3.4 19.2 2.3 2.2 5.1 4.9 7.8 7.8 6.4 6.9 17.8 23 18.1 23.4 4.4 6.1-1.1 23.4-15.6 33.3-7.6 5.2-16.5 7.5-23.7 7.5-6.5 0-11.7-1.8-13.5-5.1-.7-1.2-1.2-4.5-1.2-4.5s-3.5-14.8-8.5-28.3c-.2-.6-.4-1.2-.6-1.7-5.3 1.7-10.9 2.8-16.8 3.3-.7.1-1.4.1-2.2.1-4.1 0-8.1-.9-11.9-1.4-1.9 13.6-1.5 31.4-1.5 31.4.2 1.8.1 3.3-.4 4.7-1.7 4.5-9.4 7.7-18.8 7.7-5.8 0-12.3-1.2-18.4-4.1-15.8-7.4-24.1-23.6-20.7-30.3.2-.5 8.9-18.3 14.1-26.1 1.7-2.5 3.4-4.9 5-7.1-4.5-7.9-6.8-17.1-6.1-26.7 2-30.2 24.9-56 57.8-56.6h1.3c4.1 0 8.1.4 11.9 1.1 3.5-1.1 10.5-3.9 18-11.3 7.7-7.5 13.5-17 16.2-21.8 1.2-4.8 3.5-9.3 6.5-13.2-.8-2.7-1.2-5.9-1.2-9.6-.1-9 1.7-17.1 4.7-23.3m-1.5-3.7l-1.2 2.3c-3.3 6.7-5.1 15.3-5.1 24.6 0 3.3.3 6.3 1 9-2.8 3.9-4.9 8.2-6.1 12.7-2.8 4.9-8.3 13.8-15.5 20.8-6.6 6.5-12.7 9.2-16.1 10.4-3.9-.7-7.8-1-11.7-1h-1.3c-16.3.3-31.4 6.8-42.6 18.1-10.7 10.9-17.1 25.6-18.1 41.3-.6 9.3 1.3 18.4 5.6 26.6-1.4 2-2.8 3.9-4 5.7-4.5 6.7-11.4 20.5-14.1 26.1l-.1.3c-1.5 2.9-1.4 6.8.1 11.3 2.4 7.1 9.7 17.4 22 23.2 6.1 2.8 12.9 4.3 19.7 4.3 10.8 0 19.5-3.9 21.6-9.7.3-.8.5-1.7.6-2.6h.1v-.5c.1-.8 0-1.7 0-2.6v-.1c0-.2-.3-15.2 1.1-27.9h.2c2.9.5 5.9 1 9.1 1 .8 0 1.6 0 2.4-.1 5-.4 10-1.3 14.7-2.6 4.5 12.3 7.7 25.6 8 27 .2 1.2.7 3.9 1.5 5.4 2.4 4.2 8.2 6.6 16.2 6.6 8.7 0 18-2.9 25.4-8 11.3-7.7 16.8-19.1 18-26.4.8-4.7.2-8.5-1.7-11.1l-.2-.3c-3.6-5.1-12.6-17.5-18.2-23.5-2-2.1-4.1-4.3-6.5-6.6 2.1-5.9 3-12.1 2.9-18.5 0-1.8-.2-3.6-.4-5.4 1.1-3.8 3.2-10.3 6.3-15.8 3.5-6.1 7.6-11.6 9.8-14.1 4 2.6 8.7 4.9 14.3 4.9.8 0 1.7-.1 2.6-.2 7.1-.8 13.1-2.3 18-4.3.2-.1.4-.1.6-.1h.3c1.8.3 3.6.5 5.5.5 8.6 0 16.6-3.8 22.6-10.7 5.6-6.5 8.9-15.2 8.9-23.9 0-16.4-12.7-28.4-31.6-29.8-7.8-11-20.8-17.5-35-17.5-1.1 0-2.2 0-3.4.1-5-6.8-14.1-14.9-23.7-18l-2.5-.9z"/>
+ <path class="st0" d="M206.7 67.2l3.7-2.9c-6.1-9-17.2-18.1-26.8-21.2l-3.3-1.1-1.5 3.1c-3.4 6.9-5.2 15.5-5.2 25.1 0 3.2.1 5.4 1 8.9s3.2 5.5 3.2 5.5l4-3.7 24.9-13.7z"/>
+ <path class="st13" d="M244.5 82.2h-.9c-6.8-10.5-19.2-17.5-33.4-17.5-1.6 0-3.2.1-4.8.3-4.9-7.4-14.3-15.5-23.1-18.3-3.1 6.2-4.8 14.3-4.8 23.3 0 3.7.4 6.9 1.2 9.6-3.1 3.9-5.3 8.4-6.5 13.2-2.7 4.8-8.5 14.3-16.2 21.8-7.6 7.4-14.5 10.2-18 11.3-4.3-.8-8.7-1.2-13.2-1.1-32.9.7-55.8 26.5-57.8 56.6-.6 9.6 1.6 18.8 6.1 26.7-1.6 2.1-3.3 4.6-5 7.1-5.2 7.8-13.9 25.6-14.1 26.1-3.4 6.7 4.9 22.9 20.7 30.4 15.8 7.4 34.5 3.7 37.2-3.6.5-1.3.6-2.9.4-4.7 0 0-.4-17.8 1.5-31.4 4.5.6 9.2 1.8 14.1 1.3 5.9-.5 11.5-1.6 16.8-3.3.2.6.4 1.1.6 1.7 5 13.6 8.5 28.3 8.5 28.3s.5 3.3 1.2 4.5c3.8 6.8 22.9 7.5 37.3-2.4 14.4-9.9 20-27.2 15.6-33.3-.3-.4-11.7-16.6-18.1-23.4-2.7-2.9-5.4-5.6-7.8-7.8 2.3-5.9 3.6-12.3 3.4-19.2 0-1.9-.2-3.8-.4-5.7 1.2-4.2 3.4-11.1 6.7-17 5.1-9.1 11.8-16.7 12.1-16.5 5.5 3.9 10.6 6.3 16.9 5.6 7.3-.9 13-2.3 17.2-4.1.8-.3 1.7-.4 2.6-.3 1.6.3 3.3.4 5 .4 16.3 0 28.5-15.3 28.5-31.6-.1-16.4-13.3-25.9-29.5-27z"/>
+ <path class="st13" d="M205.5 65c-4.9-7.4-14.3-15.5-23.1-18.3-3.1 6.2-4.8 14.3-4.8 23.3 0 3.7.4 6.9 1.2 9.6 1.3 3.7 7.6 11.6 19.4 4 11.9-7.6 8.7-16.3 7.3-18.6z"/>
+ <path class="st0" d="M249.3 140.9c-10.1-.9-18.5-6.8-23.1-15.1 1.8 6.5 5.9 12.2 11.3 16.1 1 .2 2 .3 3 .4 3.8.4 7.4-.2 10.8-1.4-.7.1-1.3.1-2 0z"/>
+ <ellipse transform="rotate(-14.647 259.34 101.424)" class="st0" cx="259.3" cy="101.4" rx="1.9" ry="4.2"/>
+ <path class="st0" d="M218.8 99.9c-1.2-2.3-.5-5.1 1.7-6.5 2.2-1.4 5-.8 6.5 1.1l4.1-2.7c-2.7-3.8-7.9-4.8-11.9-2.3-4 2.6-5.3 7.8-2.9 12l2.5-1.6zM197.1 121.8c0-5.8 1.6-9.8 4.6-12.6-4.8 2.7-7.3 6.9-7.3 14.1 0 12.6 10.3 24.5 22.8 23.1-12.4-2.5-20.1-12.6-20.1-24.6zM178 77.4l-1.8 2.8c2.3 4.4 5.6 6.6 9.7 7.3-3.4-2-6.1-5.3-7.9-10.1zM207.8 63.1c-1 0-2.1.1-3 .1 1.1 2.3 2 4.6 2.5 6.7.4 1.7.8 3.3 1.1 4.8.9-2.7 1.2-5.4.7-7.7-.4-1.2-.8-2.5-1.3-3.9zM185.5 60.4s-.6 7.9 2.3 13.8c2.1 4.1 4.3 6.5 5.4 7.5.4.4 1 .5 1.6.4 1-.3 2.8-1.1 4.4-2.5 1.3-1.2 2.1-2.1 2.5-2.7.4-.5.4-1.1.1-1.6-.7-1.2-2.3-3.9-6.3-7.8-4.2-4.3-10-7.1-10-7.1zM94 236.1c-13.3-6.2-27.6-4.3-33.9.5 7.3-2.8 18.8-3.2 29.6 1.9 15.8 7.4 24 22.8 21.3 30.2-.5 1.3-1.4 2.4-2.7 3.4 3.6-1.4 5.1-6.6 5.3-9.2.5-8.4-3.8-19.4-19.6-26.8zM155.3 264c1.2-7.1 9.5-18.5 22.2-25.9 15.5-9.1 29.9-7.6 33.2-2.5.1.2.3.5.4.7.3-2 0-3.6-.7-4.8-3.3-5.1-17.8-6.7-33.2 2.5-13.2 7.9-22.6 20.5-21.9 30 0-.1 0-.1 0 0z"/>
+ <path class="st0" d="M78.6 252.8c2.3 0 5 .4 7.7 1.5 6.9 2.8 11.1 8.5 10.3 11.2-.5 1.6-3.4 2.7-7.3 2.7-2.5 0-5.3-.5-8-1.6-6.9-2.8-11.1-8.8-10.1-11.2.7-1.5 3.6-2.6 7.4-2.6m0-3c-3.9 0-8.6 1.2-10.1 4.4-.4 1-.8 2.6.2 4.9 1.4 3.4 5.4 7.9 11.5 10.3 2.9 1.2 6.1 1.8 9.1 1.8 5.5 0 9.3-1.8 10.1-4.9 1.3-4.7-4.3-11.6-12.1-14.8-2.7-1.1-5.8-1.7-8.7-1.7zM195.6 244.2c.6 0 1.1.1 1.3.4 1.2 1.2-3.6 7.3-10.6 12-5.5 3.6-11 5.6-13.6 5.6-.7 0-1.2-.2-1.4-.5-.8-1.4 3.5-7.1 10.5-11.8 5.7-3.8 11.2-5.7 13.8-5.7m0-3c-3.6 0-9.8 2.5-15.4 6.1-4.1 2.7-14.2 10.9-11.5 15.7.5.9 1.6 2 4 2 3.6 0 9.8-2.5 15.2-6.1 5.6-3.7 11.8-9.7 12.2-13.5.1-1.2-.2-2.3-1-3.1-.9-.7-2.1-1.1-3.5-1.1z"/>
+ <path class="st11" d="M96.6 265.5c-.8 2.7-8.3 4-15.3 1.2-6.9-2.8-11.1-8.8-10.1-11.2 1.1-2.4 8.1-3.9 15-1.1 7 2.7 11.1 8.4 10.4 11.1zM196.9 244.6c1.2 1.2-3.6 7.3-10.6 12s-14.1 6.5-14.9 5.1c-.8-1.4 3.5-7.1 10.5-11.8 7-4.7 13.8-6.5 15-5.3z"/>
+ <path class="st0" d="M103.9 196.3c-9.7-3.4-19.7-.8-22.6 2.5 4-1.9 11.7-2.8 19.5-.1 10.2 3.6 16.5 11.6 16 15.2-.4 2.1-1 5-1.6 8-1.1 5.9-1.6 12.4-1.6 12.4l2.9.1s.1-3.9 1-9.4c.9-5.8 2-10 2-10 .3-.4.5-.9.6-1.5.9-4.1-5.4-13.4-16.2-17.2zM178.8 190.3c-.9-1-2.9-3.4-3.9-4.5-.1-.2-.2-.4-.4-.5-1.9-2-11.6.2-21.3 6.6-8.6 5.7-11.8 11.6-14.2 16 2.2-3.2 6.7-7.7 12.7-11.7 9.2-6.1 19.8-9.3 21.5-7.8 0 0 1.5 1.8 4 4.3 2.4 2.5 5 5.1 5 5.1l1.7-2.2s-3.9-4-5.1-5.3z"/>
+ <path class="st14" d="M172.3 194.4l-4.5 3.8M167.8 198.8c-.2 0-.3-.1-.5-.2-.2-.3-.2-.6.1-.8l4.5-3.8c.3-.2.6-.2.8.1.2.3.2.6-.1.8l-4.5 3.8c-.1 0-.2.1-.3.1zM124.2 221.8l-.5 7.8M134.9 222.8l2.3 5.1M175.6 200.6l-5.3 2.6M170.3 203.8c-.2 0-.4-.1-.5-.3-.1-.3 0-.7.3-.8l5.3-2.6c.3-.1.7 0 .8.3.1.3 0 .7-.3.8l-5.3 2.6h-.3zM186.1 220.5l-4.3 2.8M181.8 223.8c-.2 0-.3-.1-.4-.2-.2-.2-.1-.5.1-.7l4.3-2.8c.2-.2.5-.1.7.1.2.2.1.5-.1.7l-4.3 2.8c-.1.1-.2.1-.3.1zM185.8 162.6c-.2-.2-.5-.2-.7-.1l-1 .8c-.1-.3-.1-.5-.2-.8l1.1-.7c.3-.2.3-.6.2-.8-.2-.3-.6-.3-.8-.2l-.8.5c-2.1-6.6-5.4-12.6-9.8-17.8l1.3-1.1c.3-.3.4-.7.1-1.1-.3-.3-.7-.4-1.1-.1l-1.3 1.1c-4-4.5-8.8-8.3-14.1-11.4l1.4-2c.2-.2.1-.5-.1-.7-.2-.2-.5-.1-.7.1l-1.5 2.1-3.3-1.8c-.3-.1-.6-.1-.8.1l-.6.5c-.4.3-.3 1 .2 1.2 1.1.5 2.2 1.1 3.2 1.7l-.7 1c-.2.2-.1.5.1.7.1.1.2.1.3.1.2 0 .3-.1.4-.2l.8-1c5.1 3 9.7 6.8 13.6 11.1l-.8.7c-.3.3-.4.7-.1 1.1.1.2.4.3.6.3.2 0 .3-.1.5-.2l.8-.7c4.4 5.2 7.8 11 10 17.4l-1.2.8c-.3.2-.3.6-.2.8.1.2.3.3.5.3.1 0 .2 0 .3-.1l.9-.6.3.9-1.2 1c-.2.2-.2.5-.1.7.1.1.2.2.4.2.1 0 .2 0 .3-.1l.8-.7.6 2.4 1.1-1.9c.1-.2.1-.4.1-.5l-.3-1.2 1.3-1.1c.4-.2.4-.5.2-.7zM93.9 198.2c-.2 0-.5.3-.7.8l-3.2 6.5-2.1-.5c-.3-.1-.6.1-.7.4-.1.3.1.6.4.7l1.8.5-6.2 12.6-1.9.1c-.3 0-.5.2-.5.5s.2.5.5.5l1.3-.1-.9 1.8-1.7-.5c-.3-.1-.5.1-.6.3-.1.3.1.5.3.6l1.6.5-3.1 6.4c-.2.5-.1.9.3.9s1-.4 1.2-.9l2.8-6 1 .3h.1c.2 0 .4-.1.5-.4.1-.3-.1-.5-.3-.6l-.9-.3 1-2.2 1.3-.1c.3 0 .5-.2.5-.5s-.3-.5-.5-.5h-.8l5.7-12.4 1.2.3h.1c.3 0 .5-.2.6-.5.1-.3-.1-.6-.4-.7l-1-.2 3-6.6c.4-.3.5-.7.3-.7z"/>
+ <path class="st14" d="M185.6 226.8c-.2 0-.4-.1-.5-.3 0-.1-2.1-6.2-9-18.5s-10.3-16.3-10.3-16.3c-.2-.2-.2-.5 0-.7.2-.2.5-.2.7 0 .1.2 3.5 4.1 10.4 16.5s9 18.7 9 18.7c.1.3-.1.5-.3.6.1 0 0 0 0 0zM140.6 223.5c-.2-.4-.7-.6-1.1-.4 0 0-1.3.6-3.3 1.3l-.8-1.9c-.1-.2-.3-.3-.5-.2-.2.1-.3.3-.2.5l.8 1.8c-2 .6-4.5 1.2-7.4 1.5-1.4.1-2.7.2-3.7.2l.3-4.5c0-.2-.2-.4-.4-.4s-.4.2-.4.4l-.3 4.6c-2.8-.1-3.8-.4-3.9-.5-.4-.2-.9 0-1.1.5-.2.4 0 .9.5 1.1.2.1 1.4.5 4.4.6l-.1 1.6c0 .2.2.4.4.4s.4-.2.4-.4l.1-1.6c1.1 0 2.4 0 3.9-.2 3.1-.2 5.8-.9 7.9-1.6l.8 1.8c.1.1.2.2.4.2h.2c.2-.1.3-.3.2-.5l-.8-1.7c2-.7 3.3-1.3 3.4-1.4.3-.2.5-.8.3-1.2z"/>
+ <path class="st0" d="M46.4 112.8s2.4 2.5 6 6.6c1.8 2 3.8 4.4 6 7.2 1.1 1.4 2.1 2.8 3.2 4.3s2.2 3.1 3.3 4.7c3.6 4.9 6.5 8 8.5 10s3.1 2.7 3.1 2.7-.6-1.2-2.1-3.6c-1.5-2.4-3.9-5.9-7.4-10.7-1.2-1.7-2.4-3.3-3.6-4.8-1.2-1.6-2.4-3-3.6-4.3-2.4-2.6-4.7-4.8-6.7-6.6-4-3.7-6.7-5.5-6.7-5.5zM5.7 93.4s4.5 5.4 11.3 13.9c3.4 4.3 7.4 9.3 11.6 14.9 2.1 2.8 4.2 5.8 6 8.9 1.9 3.4 4.1 6.6 6.5 9.8 3.8 4.9 7.5 8.7 10.8 11.6 3.3 3 6.4 5 8.9 6.3 2.5 1.3 4.4 2 5.7 2.5 1.3.4 1.9.6 1.9.6s-.6-.3-1.8-1c-1.2-.7-3-1.7-5.3-3.3-2.3-1.6-5-3.8-8-6.8s-6.5-6.8-10.1-11.5c-2.5-3.3-4.8-6.6-6.7-10-2.1-3.4-4.4-6.4-6.7-9.2-4.5-5.6-8.8-10.3-12.3-14.3-7.2-7.9-11.8-12.4-11.8-12.4z"/>
+ <path class="st0" d="M27.1 136.4s.2.9.8 2.5c.6 1.6 1.7 3.9 3.4 6.4 1.7 2.5 4.1 5.3 7.3 7.7l1.2.9c.4.3.9.6 1.3.8.4.3.9.6 1.3.8.5.2.9.5 1.4.8 1.9 1 3.8 1.8 5.8 2.7 6.2 2.6 10.9 3.8 14 4.3 3.1.5 4.5.4 4.5.4s-1.4-.5-4.2-1.7c-2.9-1.1-7.2-2.9-13.2-5.4-1-.5-2.1-.9-3.1-1.3-.5-.2-1-.4-1.5-.7-.5-.2-.9-.5-1.4-.7-.5-.2-.9-.4-1.4-.7-.4-.3-.9-.5-1.3-.8-.9-.5-1.7-1.1-2.5-1.6-3.1-2.2-5.6-4.6-7.4-6.8-1.8-2.2-3.1-4.2-3.9-5.5-.8-1.2-1.1-2.1-1.1-2.1zM121.8 114.8s2.1 3.6 5.5 6.5c2.6 2.3 4.5 2.2 4.5 2.2s-.3-2-2.8-4.2c-3.6-3.2-7.2-4.5-7.2-4.5zM112.5 118.4s1 2.1 2.7 4c1.3 1.5 2.3 1.6 2.3 1.6s-.1-1.1-1.3-2.5c-1.8-1.9-3.7-3.1-3.7-3.1zM124.2 98.4s1.1 2.1 3.2 5.3c1.1 1.6 2.4 3.4 4 5.2.8.9 1.7 1.9 2.6 2.8.9.9 2 1.8 3.1 2.6 3.3 2.5 6 4 7.7 4.8 1.8.8 2.7 1 2.7 1s-.6-.8-2-2.1c-1.4-1.4-3.6-3.3-6.8-5.8-1.2-.9-2.2-1.7-3.2-2.5-1-.9-1.9-1.7-2.8-2.5-1.7-1.7-3.3-3.2-4.5-4.5-2.5-2.7-4-4.3-4-4.3zM145.2 89.1s1.2 1.4 2.9 3.8c.8 1.2 1.7 2.6 2.6 4.2.8 1.6 1.5 3.4 2.1 5.3.9 3 1.5 5.2 2 6.6.5 1.4.8 2.1.8 2.1s.3-.7.5-2.3c.2-1.6.2-4-.8-7.2-.7-2.2-1.7-4.3-2.9-5.9-1.2-1.6-2.4-2.9-3.5-3.9-2.2-1.9-3.7-2.7-3.7-2.7z"/>
+ <g>
+ <path class="st0" d="M158.5 58.1s-.1 2.7.4 6.9c.3 2.1.7 4.6 1.4 7.2.4 1.3.9 2.7 1.5 4.1.3.7.7 1.4 1.1 2.1.4.7.8 1.3 1.3 1.9 2.8 3.8 5.3 6.1 7 7.5 1.7 1.4 2.6 1.8 2.6 1.8s-.5-.9-1.7-2.7c-1.2-1.8-3.1-4.5-5.8-8.1-.5-.6-.9-1.3-1.3-1.9l-.6-.9c-.2-.3-.3-.6-.5-.9-.6-1.3-1.2-2.5-1.7-3.8-.9-2.5-1.6-4.8-2.2-6.7-.9-4.1-1.5-6.5-1.5-6.5z"/>
+ </g>
+ <g>
+ <path class="st0" d="M152.2 77.7s1.5 1.1 3.7 3c1.1 1 2.3 2.1 3.4 3.6.6.7 1 1.5 1.5 2.4.5.9.9 1.8 1.4 2.7 1.4 2.9 2.5 4.9 3.2 6.2.7 1.3 1.2 1.8 1.2 1.8s-.1-.7-.6-2.1c-.5-1.4-1.2-3.6-2.7-6.5-1-2-2-3.9-3.3-5.4-1.3-1.4-2.7-2.5-3.9-3.3-2.3-1.6-3.9-2.4-3.9-2.4z"/>
+ </g>
+ <g>
+ <path class="st0" d="M174.5 37.4s-.2 1.3-.2 3.3c0 1 0 2.2.1 3.5.1 1.3.3 2.7.7 4.1.5 2.2 1 3.7 1.4 4.7.4 1 .7 1.4.7 1.4s0-.5-.1-1.6c-.1-1-.3-2.6-.8-4.8-.7-3-1.1-5.6-1.4-7.6-.2-1.8-.4-3-.4-3z"/>
+ </g>
+ <g>
+ <path class="st0" d="M206.2 30.7s-1.2 1.7-2.3 4.8c-.6 1.5-1.2 3.4-1.6 5.4-.4 2.1-.6 4.3-.7 6.7-.1 3.6.2 6.1.6 7.8.4 1.6.7 2.4.7 2.4s.3-.8.6-2.4c.3-1.6.6-4.2.7-7.6.1-2.4.2-4.7.3-6.7.2-2 .4-3.8.7-5.4.5-3.2 1-5 1-5z"/>
+ </g>
+ <g>
+ <path class="st0" d="M194.9 33.4s.5 1.5 1.1 4c.3 1.2.5 2.6.7 4.2.2 1.6.4 3.3.8 5.1 1 5.5 2.8 7.5 2.8 7.5s.9-2.6-.2-8c-.4-1.9-.8-3.6-1.2-5.3-.5-1.6-1.1-3-1.7-4.1-1.3-2.3-2.3-3.4-2.3-3.4z"/>
+ </g>
+ <g>
+ <path class="st0" d="M192.6 36.4s.3 4.1 1.2 8.5c.7 3.5 1.5 4.8 1.5 4.8s.2-1.6-.5-5c-1-4.8-2.2-8.3-2.2-8.3z"/>
+ </g>
+ <path class="st15" d="M98.2 155c.8-.8 2.2-2.8 2.2-4.6v.1s-.2.3-.6.8v-.2c.3-.6.5-1.3.6-2 .2-1.1-.3-2.1-.6-2.5v.1l-.1-.1v.3c-.2-.8-.8-1.3-.8-1.3s0 .1-.1.4c-.9-1.5-2.5-2.6-4.5-3-2-.4-4.1.1-6 1.2 0 .1.1.2.1.4 0 0 .6 0 1.2.2 1.4-.6 2.9-.8 4.3-.6 1.8.3 3.2 1.4 4 2.9 0 .1.1.2.1.2-.3-.4-.7-.7-1-.8.5.7 1.1 2.2.2 4.5 0-.4-.2-.8-.3-1-.6 2.4-1 2.9-1.3 3.5.1-.3 0-.5 0-.7 0 0-.3.7-1.1 1.7-.6.7-1.1.9-1.2.9-.1-.1 0-.2 0-.3 0 0-.3.1-.6.3-.3.2-.6.4-.7.3l.3-.3c-.1 0-.4.2-.9.2-.2 0-1.2 0-2.1-.8.2 0 .5 0 .6.2-.1-.3-.6-.3-.9-.5-.3-.2-.5-.7-.6-.9.8.4 1.8.4 2.5.1.7-.3 1.1-.5 1.4-.4.3.1.6-.2.4-.5-.1-.3-.6-.8-1.4-.7-.6 0-1.5.5-2.3-.3-.7-.7-.5-1.1-.4-1.4.1-.2.3-.4.5-.5.1.1.2.1.2.1v-.1c.1 0 .3.2.3.2.1.1.1.2.1.2v-.1s0-.1-.1-.2c.1 0 .1.1.2.2.1-.1.2-.3.2-.5v-.3c0-.1 0-.1.1 0v-.1c.2-.2 1.5-.6 1.6-.6.1-.1.3-.2.4-.3.1 0 .3-.3.4-.5 0-.1-.1-.2-.2-.3 0 0-.2-.1-.4-.1s-.4-.1-.8-.2c-.3-.1-.5-.3-.6-.5v-.1-.2c.3-.5.8-1 1.4-1.3h-.1l.3-.1c.1 0-.2-.1-.4-.1s-.3 0-.4.1c.1 0 .2-.1.2-.1-.3 0-.6.1-.9.3v-.1c-.1 0-.5.2-.6.4v-.1c-.1.1-.2.1-.3.2-.7-.3-1.3-.4-1.9-.3-.1-.2-.3-.5-.4-.9 0 0 0 .1-.1 0v-.9-.1s-.1 0-.3.2c-.1.1-.2.2-.2.3 0 .1-.1.1-.1.2 0 0 .1-.1 0-.1l-.2.2c0 .1-.1.1-.1.2v-.1c-.1.2-.2.3-.4.5-.1.1-.2.3-.3.5-.1.2-.2.5-.3.8v.1c-.3.2-.5.5-.6.6-.4.4-.9 1.1-1.5 2.3 0 0 .3-.4.7-.9-.4.6-.9 1.5-1.3 3 0 0 .2-.4.5-.9-.3 1-.4 2.2-.1 3.6.2 1 .6 1.9 1.1 2.5.1.1.2.3.3.4.6.8 1.7 1.9 2.8 2.3-.4-.2-.5-.5-.5-.5s1.2.7 2.2.8c-.3-.1-.3-.3-.3-.3s2.8.7 4.7-.3c.4-.2.7-.5.9-.7.5-.2 1.1-.4 1.8-.8 1.1-.7 1.4-1.3 1.7-1.9v.1c.4-.3.4-.5.4-.6z"/>
+</svg> \ No newline at end of file
diff --git a/devtools/client/themes/images/emojis/emoji-tool-storage.svg b/devtools/client/themes/images/emojis/emoji-tool-storage.svg
new file mode 100644
index 000000000..d9be26108
--- /dev/null
+++ b/devtools/client/themes/images/emojis/emoji-tool-storage.svg
@@ -0,0 +1,8 @@
+<!-- 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/. -->
+<svg height="16" width="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" opacity="1.0">
+ <path fill="#D19B61" d="M30.588 157.435C45.694 92.48 111.838 11.252 210.984 8.688S399.89 31.77 458.878 108.708s58.986 220.88 25.646 265.6-41.87 142.173-192.764 131.914c-150.894-10.258-204.75-42.22-253.48-112.154S10.037 245.802 30.59 157.435z"/>
+ <path fill="#4F3D30" d="M161.834 173.737c0 10.843-18.425 19.634-41.154 19.634s-41.154-8.79-41.154-19.633c0-26.124 43.257-30.846 43.79-41.152.1-1.933 2.78-3.17 5.054-2.25 11.912 4.824 33.464 17.178 33.464 43.402zm161.668 236.25c-2.04-.663-4.442.227-4.532 1.617-.48 7.408-39.28 10.802-39.28 29.58 0 7.793 16.527 14.11 36.914 14.11s36.915-6.317 36.915-14.11c0-18.85-19.333-27.73-30.018-31.198zm87.547-118.38c-2.04-.968-4.443.333-4.533 2.366-.48 10.834-39.28 15.797-39.28 43.258 0 11.4 16.527 20.64 36.914 20.64s36.916-9.24 36.916-20.64c0-27.565-19.333-40.55-30.017-45.623z"/>
+ <path fill="#332A23" d="M264.82 274.863c0 10.345-16.527 18.73-36.914 18.73s-36.915-8.385-36.915-18.73c0-24.923 38.803-29.428 39.282-39.26.09-1.846 2.494-3.027 4.532-2.148 10.684 4.603 30.017 16.39 30.017 41.408zm6.9-210.826c-2.04-.662-4.443.228-4.533 1.618-.48 7.408-39.28 10.802-39.28 29.58 0 7.793 16.527 14.11 36.914 14.11s36.916-6.317 36.916-14.11c0-18.85-19.333-27.73-30.017-31.198zm121.1 54.782c-1.857-.914-4.048.314-4.13 2.232-.437 10.227-35.806 14.912-35.806 40.833 0 10.76 15.065 19.482 33.65 19.482 18.584 0 33.65-8.722 33.65-19.482-.002-26.02-17.624-38.28-27.363-43.066zM157.535 356.05c-2.272-1.118-4.95.384-5.052 2.73-.534 12.504-43.782 18.233-43.782 49.93 0 13.155 18.42 23.82 41.145 23.82 22.724 0 41.145-10.665 41.145-23.82 0-31.82-21.548-46.807-33.456-52.66z"/>
+</svg> \ No newline at end of file
diff --git a/devtools/client/themes/images/emojis/emoji-tool-styleeditor.svg b/devtools/client/themes/images/emojis/emoji-tool-styleeditor.svg
new file mode 100644
index 000000000..ef7153312
--- /dev/null
+++ b/devtools/client/themes/images/emojis/emoji-tool-styleeditor.svg
@@ -0,0 +1,11 @@
+<!-- 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/. -->
+<svg height="16" width="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 72 72" opacity="1.0">
+ <path fill="#FF473E" d="M45.4 35.2V2.1c0-4.9-18.8 9.8-18.8 15.1v18h18.8z"/>
+ <path fill="#D1CFC3" d="M25.8 41.1c-.4 0-.7-.3-.7-.7v-7.2c0-.4.3-.7.7-.7h20.9c.4 0 .7.3.7.7v7.2c0 .4-.3.7-.7.7H25.8z"/>
+ <path fill="#BFBCAF" d="M50.1 39.6c0-.4-.3-.7-.7-.7H22.6c-.4 0-.7.3-.7.7V67.5C21.9 70 28.2 72 36 72s14.1-2 14.1-4.5v-.1-27.8z"/>
+ <ellipse transform="rotate(45 35.977 9.37)" fill="#FF6E83" cx="36" cy="9.4" rx="4" ry="12.6"/>
+ <path fill="#FC7570" d="M41.9 30.5c-.8 0-1.5-.7-1.5-1.5V17.5c0-.8.7-1.5 1.5-1.5s1.5.7 1.5 1.5V29c-.1.9-.7 1.5-1.5 1.5z"/>
+ <circle fill="#C4F0F2" cx="36" cy="53.9" r="3.8"/>
+</svg> \ No newline at end of file
diff --git a/devtools/client/themes/images/emojis/emoji-tool-webaudio.svg b/devtools/client/themes/images/emojis/emoji-tool-webaudio.svg
new file mode 100644
index 000000000..b57c0f886
--- /dev/null
+++ b/devtools/client/themes/images/emojis/emoji-tool-webaudio.svg
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+<svg height="16" width="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 511.9 511.9" opacity="1.0">
+ <circle fill="#E2A042" cx="254.5" cy="217.6" r="167.4"/>
+ <path fill="#AF773F" d="M443.1 215.3c-2.3-14-4.1-29.8-3.8-43.5-5.4-1.4-11-2.1-16.8-2.1-6.9 0-13.5 1-19.8 3-18.1-18.4-43.3-29.9-71.2-29.9-22.5 0-43.2 7.5-59.9 20-10.7 8.1-25.1 8.1-35.8 0-19.2-14.5-43.8-22.2-70.3-19.5-22.9 2.3-45.2 12.6-61.7 28-4.7-1-9.5-1.6-14.5-1.6-5.1 0-10.1.6-14.9 1.7.4 13.8-1.4 29.7-3.8 43.9 0 0 8.2 52.1-5.1 82.4.7.9 1.3 1.8 1.9 2.7 6.5 2.2 13.5 3.5 20.8 3.6 26.7 81.6 97.9 112.6 166.2 112.6 75.1 0 145.3-32.9 168.6-112.6 7.8-.1 15.3-1.5 22.3-4 .7-1.2 1.5-2.3 2.3-3.3-12.5-30.3-4.5-81.4-4.5-81.4z"/>
+ <path fill="#FFB636" d="M328.7 177.4c-24.8-.3-46.8 11.5-60.4 30-6.6 8.9-19.4 8.9-26 0-13.6-18.4-35.6-30.3-60.4-30-38.8.5-71.4 32.3-72.9 71-1.5 39.1 27.3 71.7 64.8 76.3 5.1.6 9 4.6 9.6 9.6 4.5 35.7 34.9 63.3 71.9 63.3 36.7 0 67-27.3 71.8-62.7.7-5.4 4.9-9.6 10.3-10.3 37.2-4.8 65.7-37.3 64.2-76.2-1.4-38.7-34.1-70.5-72.9-71z"/>
+ <path fill="#E576CB" d="M271.5 281.8s-2.2 2.9-5.5 4.9c-3.3 2.2-7.7 3.6-12.2 3.7-4.4-.1-8.8-1.5-12.2-3.6-3.3-2-5.5-4.9-5.5-4.9-.5-.7-.5-1.6 0-2.2 0 0 2.2-2.9 5.5-4.9 3.3-2.2 7.7-3.6 12.2-3.6 4.4.1 8.8 1.5 12.2 3.7 3.3 2 5.5 4.9 5.5 4.9.5.5.5 1.4 0 2zM293.4 338.1c-9.8-13.9-24.8-22-40.2-21.7-15.2.3-29.4 8.2-38.9 21.7-1.1 1.6-1.6 3.4-1.6 5.2 0 .9.1 1.9.4 2.8 7.4 23.5 23 38 40.8 38s33.4-14.6 40.8-38c.3-.9.4-1.8.4-2.7v-.3c-.1-1.7-.6-3.5-1.7-5z"/>
+ <path fill="#2B3B47" d="M253.8 366.5c-8.9 0-17.3-8.1-22.5-21.5 6-6.7 14.1-10.7 22.2-10.9 8.1-.2 16.5 3.9 22.8 10.9-5.2 13.4-13.6 21.5-22.5 21.5z"/>
+ <path fill="#E2A042" d="M147.3 412.6s.1.1.3.2c0 0 .1.1.2.1l.1.1v.1s0 .1.1.1v.1s-.1.1-.2.1c-.4 0-1.1-.1-2.4-.5-1.2-.4-2.8-1-4.7-1.9-1.9-.9-4.1-2-6.4-3.4-2.4-1.4-4.9-3-7.6-4.9-2.7-1.9-5.4-4.1-8.2-6.5-1.4-1.2-2.8-2.5-4.2-3.8-1.4-1.4-2.8-2.7-4.2-4.2-.7-.7-1.4-1.5-2.1-2.2-.7-.8-1.3-1.5-2-2.3-1.4-1.5-2.7-3.2-4-4.8-.7-.8-1.3-1.6-1.9-2.5l-1.9-2.5c-1.2-1.7-2.4-3.5-3.6-5.2l-3.3-5.4c-1-1.8-2.1-3.7-3.1-5.4-1-1.8-1.8-3.7-2.8-5.5-.9-1.8-1.7-3.6-2.5-5.4-.8-1.8-1.5-3.5-2.2-5.2-.7-1.7-1.4-3.4-1.9-5-1.1-3.2-2.2-6.3-3-8.9-.8-2.6-1.6-4.7-2-7.1-1-4.4-1.6-6.9-1.6-6.9l-1-4.5c-1-4.3-3-8.3-5.7-11.7 13.3-30.3 5.1-82.4 5.1-82.4 5.4-32.2 7.9-74-12-77.3-20-3.3-44.9 26.4-53.8 80-6.4 38.5-1.6 71.8 9 88.6-.9 2.7-1.5 5.7-1.6 8.7 0 0-.1 2.5-.2 6.9 0 .6 0 1.1-.1 1.7V326.3c0 1.7.1 3.6.1 5.6 0 4.1.3 8.1.6 12.8.1 2.3.4 4.7.7 7.2.2 2.5.5 5 .9 7.7.4 2.6.7 5.3 1.2 8.1s1 5.6 1.6 8.5c.6 2.9 1.3 5.8 2 8.7.8 2.9 1.6 5.9 2.4 8.9.9 3 1.8 6 2.9 9 .5 1.5 1.1 3 1.6 4.5s1.1 3 1.7 4.5c1.2 3 2.4 6 3.7 8.9.7 1.5 1.3 2.9 2 4.4.7 1.4 1.4 2.9 2.1 4.3 1.4 2.9 2.9 5.6 4.4 8.4 1.5 2.7 3.1 5.4 4.7 8 3.2 5.2 6.5 10.1 9.8 14.7 3.4 4.6 6.7 8.8 10 12.6 3.3 3.8 6.4 7.2 9.4 10.2 3 3 5.8 5.6 8.2 7.7 2.4 2.1 4.6 3.9 6.2 5.2l1.2.9c.4.3.8.6 1 .8.4.3.7.5.9.6.3.2.5.3.5.3 23.7 15.4 55.4 8.6 70.8-15.2s8.5-55.6-15.2-71zM505.2 348.8c-.1-2.3-.1-4.5-.3-6.7-.4-4.3-.6-8.1-1.1-11.7-.4-3.5-.8-6.7-1.2-8.8-.8-4.4-1.3-6.9-1.3-6.9l-.9-4.6c-.2-1-.4-2-.7-3 10.9-16.7 15.9-50.2 9.3-89.2-9-53.5-33.9-83.3-53.8-80-20 3.3-17.4 45.1-12 77.3 0 0-8 51 4.7 81.4-3.7 4.6-6.2 10.3-6.7 16.7 0 0-.2 2.5-.5 6.9 0 .5-.1 1.1-.1 1.7v.8c0 .2-.1.4-.1.7-.1.9-.3 1.9-.4 3-.2 2-.8 4.9-1.3 7.8-.2 1.5-.6 3-.9 4.5-.3 1.6-.7 3.2-1.1 4.8-.4 1.6-.8 3.3-1.3 4.9-.5 1.7-1 3.4-1.6 5.1l-1.8 5.1c-.7 1.7-1.4 3.4-2 5.1-.8 1.7-1.5 3.4-2.3 5.1l-1.2 2.5c-.4.8-.8 1.7-1.3 2.5-.9 1.6-1.7 3.3-2.7 4.8-.5.8-.9 1.6-1.4 2.3-.5.8-1 1.5-1.5 2.3-.9 1.5-2 2.9-3 4.4-1 1.4-2 2.8-3.1 4.1-2.1 2.6-4.2 5.1-6.2 7.3-2.1 2.2-4 4.2-5.9 5.9-1.9 1.7-3.6 3.2-5.1 4.4-1.5 1.2-2.8 2.1-3.8 2.8-1 .7-1.6 1-1.9 1.1h-.1s.1 0 .1-.1l.1-.1c.1 0 .1-.1.1-.1-23.4 15.5-30 46.9-14.7 70.5 15.4 23.7 47.1 30.5 70.8 15.2 0 0 .2-.1.5-.3.1-.1.2-.1.3-.2 0 0 .1-.1.2-.1l.1-.1c.1-.1.2-.2.4-.3.1-.1.3-.2.5-.4.1-.1.2-.1.3-.2.1-.1.2-.1.2-.2.3-.3.7-.6 1.1-1 1.5-1.4 3.5-3.4 5.7-5.8s4.7-5.3 7.3-8.6c2.6-3.3 5.4-7 8.1-11.2 2.8-4.1 5.6-8.7 8.3-13.5 2.7-4.9 5.3-10.1 7.8-15.5 1.2-2.7 2.4-5.5 3.5-8.3 1.1-2.8 2.2-5.7 3.2-8.6.5-1.5 1-2.9 1.5-4.4.4-1.5.9-2.9 1.3-4.4.9-2.9 1.6-5.9 2.4-8.9.4-1.5.7-3 1-4.5.3-1.5.6-3 .9-4.4.6-3 1.1-5.9 1.6-8.8.4-2.9.8-5.8 1.1-8.7.3-2.9.5-5.7.8-8.4.2-2.8.3-5.5.4-8.1.1-2.6.1-5.2.1-7.7-.2-2.5-.3-4.9-.4-7.2z"/>
+ <path fill="#2B3B47" d="M188.5 241.3c-1-11.2-10.2-20-21.6-20-11.4 0-20.7 8.8-21.7 20L145 272h.1v.1c0 12.1 9.8 21.8 21.8 21.8 12.1 0 21.8-9.8 21.8-21.8v-.1l-.2-30.7zM363.9 241.3c-1-11.2-10.2-20-21.7-20-11.4 0-20.7 8.8-21.6 20l-.3 30.7h.1v.1c0 12.1 9.8 21.8 21.8 21.8 12.1 0 21.8-9.8 21.8-21.8v-.1l-.1-30.7z"/>
+</svg> \ No newline at end of file
diff --git a/devtools/client/themes/images/emojis/emoji-tool-webconsole.svg b/devtools/client/themes/images/emojis/emoji-tool-webconsole.svg
new file mode 100644
index 000000000..78843dfd3
--- /dev/null
+++ b/devtools/client/themes/images/emojis/emoji-tool-webconsole.svg
@@ -0,0 +1,8 @@
+<!-- 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/. -->
+<svg height="16" width="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 511.9 511.9" opacity="1.0">
+ <path fill="#FFD469" d="M255.9 35.3C134 35.3 35.2 134.1 35.2 256c0 3.3.1 6.6.2 9.9-12.8 1.7-22.8 12.6-22.8 25.9 0 14.5 11.7 26.2 26.2 26.2H44c26.8 91.7 111.4 158.7 211.7 158.7 121.8 0 220.6-98.8 220.6-220.6.2-122-98.6-220.8-220.4-220.8z"/>
+ <path fill="#FFB636" d="M476.2 265.8c.1-3.3.2-6.6.2-9.9 0-58.9-23.1-112.5-60.8-152 21.3 34.5 33.6 75.2 33.6 118.8 0 125.2-101.5 226.7-226.7 226.7-43.6 0-84.2-12.3-118.8-33.6 39.6 37.7 93.1 60.8 152 60.8 100.3 0 185-67 211.7-158.7h5.2c14.5 0 26.2-11.7 26.2-26.2.2-13.3-9.7-24.2-22.6-25.9z"/>
+ <path fill="#2B3B47" d="M172.3 251.3H121c-6.8 0-12.3-5.5-12.3-12.3 0-6.8 5.5-12.3 12.3-12.3h51.3c6.8 0 12.3 5.5 12.3 12.3 0 6.8-5.5 12.3-12.3 12.3zM391.4 251.3h-51.3c-6.8 0-12.3-5.5-12.3-12.3 0-6.8 5.5-12.3 12.3-12.3h51.3c6.8 0 12.3 5.5 12.3 12.3 0 6.8-5.5 12.3-12.3 12.3zM328.6 342.6H185c-6.8 0-12.3-5.5-12.3-12.3 0-6.8 5.5-12.3 12.3-12.3h143.6c6.8 0 12.3 5.5 12.3 12.3-.1 6.8-5.6 12.3-12.3 12.3z"/>
+</svg> \ No newline at end of file
diff --git a/devtools/client/themes/images/fast-forward.svg b/devtools/client/themes/images/fast-forward.svg
new file mode 100644
index 000000000..813c672d8
--- /dev/null
+++ b/devtools/client/themes/images/fast-forward.svg
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="#0b0b0b">
+ <path d="M4 12.5l8-5-8-5v10zm-1 0v-10a1 1 0 0 1 1.53-.848l8 5a1 1 0 0 1 0 1.696l-8 5A1 1 0 0 1 3 12.5zM15 12.497l-.04-7.342-.01-1.658A.488.488 0 0 0 14.474 3a.488.488 0 0 0-.473.503l.05 9a.488.488 0 0 0 .477.497.488.488 0 0 0 .473-.503z"/>
+</svg>
diff --git a/devtools/client/themes/images/filetypes/dir-close.svg b/devtools/client/themes/images/filetypes/dir-close.svg
new file mode 100644
index 000000000..07d087816
--- /dev/null
+++ b/devtools/client/themes/images/filetypes/dir-close.svg
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" stroke="#0b0b0b" fill="none" fill-rule="evenodd">
+ <path d="M.5 5.5v-2c0-.553.448-1 1-1H4c.553 0 1.268.358 1.6.802L6.5 4.5h5.997c.554 0 1.003.446 1.003.998v7.004c0 .55-.447.998-1 .998h-11c-.553 0-1-.453-1-.997V5.5zM1.5 6.5h11"/>
+</svg>
diff --git a/devtools/client/themes/images/filetypes/dir-open.svg b/devtools/client/themes/images/filetypes/dir-open.svg
new file mode 100644
index 000000000..6528b7e09
--- /dev/null
+++ b/devtools/client/themes/images/filetypes/dir-open.svg
@@ -0,0 +1,7 @@
+<!-- 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/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" stroke="#0b0b0b" fill="none" fill-rule="evenodd">
+ <path d="M.5 5.5v-2c0-.553.448-1 1-1H4c.553 0 1.268.358 1.6.802L6.5 4.5h5.997c.554 0 1.003.443 1.003 1v2H4.495c-.55 0-1.192.394-1.444.898l-2.1 4.204c-.25.496-.45.445-.45-.1V5.5z"/>
+ <path d="M.5 12v.508c0 .548.456.992 1.002.992h9.996c.553 0 1.2-.394 1.45-.898l2.103-4.204c.25-.496.004-.898-.55-.898H13"/>
+</svg>
diff --git a/devtools/client/themes/images/filetypes/globe.svg b/devtools/client/themes/images/filetypes/globe.svg
new file mode 100644
index 000000000..440499941
--- /dev/null
+++ b/devtools/client/themes/images/filetypes/globe.svg
@@ -0,0 +1,7 @@
+<!-- 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/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="#0b0b0b">
+ <path d="M8.507 3.508l-1.978.003-.107.107v1.334l.848.891.455.031.107-.107v-.57l.75-.705.032-.877zM11.675 8.811l-1.389-1.348h-1.49l-.52.476-.032 1.023.665.708 1.22.032-.107-.107v1.108l.897.942.29.031.108-.107v-1.066l.39-.345.03-.855-.107.107h.24l.377-.334v-.151l-.116-.114-.531-.031z"/>
+ <path d="M7.973 13.145a5.177 5.177 0 0 1-5.171-5.172c0-.034 0-.034.002-.066v-.003-.03l.17-.081.667.54.453.031.458.502-.03.832.291.352h.434l.105.088-.03 1.752.483.512h.097l.023-.31.927-.88.05-.435.489-.427v-.82l-.543-.035-1.032-1.066H4.29l-.142-.084v-.796l.142-.093h1.074v-.251l.11-.105.648.03.455-.408.031-.69-.576-.603h-.668v.26l-.102.064h-.826l-.105-.084.023-.59.442-.397v-.226h-.352c-.094 0-.14-.119-.073-.184a5.154 5.154 0 0 1 7.005-.187.104.104 0 0 1-.069.184l-1.056-.031-.745.74.48.484v.148l-.354.357h-.146l-.162-.163v-.148l.07-.07-.232-.232-.342.334.49.49c.065.066.004.18-.09.179l-.694-.004v.779h.723l.03-.516.147-.133h.148l.574.562v.248l.533.025h.003l.166-.028 1.14 1.143.695-.653h.353l.107.1c0 .018.003.015.003.033v.002c0 .028-.002.028-.001.058a5.179 5.179 0 0 1-5.174 5.172M7.973 2A5.98 5.98 0 0 0 2 7.974a5.98 5.98 0 0 0 5.973 5.973 5.98 5.98 0 0 0 5.974-5.974A5.98 5.98 0 0 0 7.973 2"/>
+</svg>
diff --git a/devtools/client/themes/images/filter-swatch.svg b/devtools/client/themes/images/filter-swatch.svg
new file mode 100644
index 000000000..1f63b4f08
--- /dev/null
+++ b/devtools/client/themes/images/filter-swatch.svg
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12" width="12px" height="12px">
+ <mask id="mask">
+ <rect width="100%" height="100%" fill="#fff"/>
+ <polygon points="12,0 0,0 0,12"/>
+ </mask>
+ <circle cx="6" cy="6" r="6" fill="#fff"/>
+ <circle cx="6" cy="6" r="6" mask="url(#mask)" fill="#aeb0b1"/>
+</svg>
diff --git a/devtools/client/themes/images/filter.svg b/devtools/client/themes/images/filter.svg
new file mode 100644
index 000000000..90bc6165f
--- /dev/null
+++ b/devtools/client/themes/images/filter.svg
@@ -0,0 +1,16 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="#0b0b0b">
+ <style>
+ /* Use a fill that's visible on both light and dark themes for filter inputs */
+ #filterinput:target + #icon {
+ fill: #aaa;
+ }
+ </style>
+ <g id="filterinput"/>
+ <g id="icon">
+ <path fill-opacity=".3" d="M6.6 8.4c0-.6-1.7.3-1.7-.3 0-.4-1.7-2.7-1.7-2.7H13s-1.8 2-1.8 2.7c0 .3-2.1-.1-2.1.3v6.1H7s-.4-4.1-.4-6.1z"/>
+ <path d="M2 2v2.3L4.7 9H6v5.4l2.1 1 1.8-.9V9h1.3L14 4.3V2H2zm11 2l-2.2 4H9v5.8l-.9.4-1.1-.5V8H5.2L3 4V3h10v1z"/>
+ </g>
+</svg>
diff --git a/devtools/client/themes/images/filters.svg b/devtools/client/themes/images/filters.svg
new file mode 100644
index 000000000..12a5f7cc9
--- /dev/null
+++ b/devtools/client/themes/images/filters.svg
@@ -0,0 +1,31 @@
+<!-- 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/. -->
+
+<svg height="0" xmlns="http://www.w3.org/2000/svg">
+ <filter id="checked-icon-state">
+ <feColorMatrix in="SourceGraphic" type="matrix"
+ values="0 0 0 0 0.043
+ 0 0 0 0 0.415
+ 0 0 0 0 0.79
+ 0 0 0 1 0"/>
+ </filter>
+ <filter id="dark-theme-checked-icon-state">
+ <feColorMatrix in="SourceGraphic" type="matrix"
+ values="0 0 0 0 0
+ 0 0 0 0 1
+ 0 0 0 0 0.212
+ 0 0 0 1 0"/>
+ </filter>
+
+ <!-- Web Audio Gradients -->
+ <linearGradient id="bypass-light" x1="8%" y1="10%" x2="16%" y2="16%" spreadMethod="repeat">
+ <stop offset="0%" stop-color="#dde1e4a0"/> <!-- theme-splitter-color (0.5 opacity) -->
+ <stop offset="50%" stop-color="transparent"/>
+ </linearGradient>
+
+ <linearGradient id="bypass-dark" x1="8%" y1="10%" x2="16%" y2="16%" spreadMethod="repeat">
+ <stop offset="0%" stop-color="#454d5d"/> <!-- theme-splitter-color -->
+ <stop offset="50%" stop-color="transparent"/>
+ </linearGradient>
+</svg>
diff --git a/devtools/client/themes/images/firebug/arrow-down.svg b/devtools/client/themes/images/firebug/arrow-down.svg
new file mode 100644
index 000000000..30a31389b
--- /dev/null
+++ b/devtools/client/themes/images/firebug/arrow-down.svg
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="7" height="7">
+ <path d="M1.774 4.486L.257 1.86-1.259-.768h6.067L3.29 1.859z" transform="matrix(.88859 0 0 1.0498 1.923 1.549)" stroke-linejoin="round" fill="#39424a" stroke="#39424a"/>
+</svg>
diff --git a/devtools/client/themes/images/firebug/arrow-up.svg b/devtools/client/themes/images/firebug/arrow-up.svg
new file mode 100644
index 000000000..dd59e90dc
--- /dev/null
+++ b/devtools/client/themes/images/firebug/arrow-up.svg
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="7" height="7">
+ <path d="M1.774 4.486L.257 1.86-1.259-.768h6.067L3.29 1.859z" transform="matrix(.88859 0 0 -1.0498 1.923 5.452)" stroke-linejoin="round" fill="#39424a" stroke="#39424a"/>
+</svg>
diff --git a/devtools/client/themes/images/firebug/breadcrumbs-divider.svg b/devtools/client/themes/images/firebug/breadcrumbs-divider.svg
new file mode 100644
index 000000000..e132ae2ab
--- /dev/null
+++ b/devtools/client/themes/images/firebug/breadcrumbs-divider.svg
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="5" height="7">
+ <defs>
+ <linearGradient id="b">
+ <stop offset="0" stop-color="#9a9aba"/>
+ <stop offset="1" stop-color="#a6a6c2"/>
+ </linearGradient>
+ <linearGradient id="a">
+ <stop offset="0" stop-color="#8e8eb2"/>
+ <stop offset="1" stop-color="#9a9aba"/>
+ </linearGradient>
+ <linearGradient x1="3.616" y1="3.893" x2="1.285" y2="-.757" id="d" xlink:href="#a" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1 0 0 .8684 0 1046.257)"/>
+ <linearGradient x1="2.232" y1="4.162" x2=".629" y2=".966" id="c" xlink:href="#b" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1 0 0 .8684 0 1046.257)"/>
+ </defs>
+ <path d="M.2 1045.562l4.6 3.3-4.6 3.3 2-3.3z" fill="url(#c)" stroke="url(#d)" stroke-width=".4" stroke-linejoin="round" transform="translate(0 -1045.362)"/>
+</svg>
diff --git a/devtools/client/themes/images/firebug/breakpoint.svg b/devtools/client/themes/images/firebug/breakpoint.svg
new file mode 100644
index 000000000..7dc2ff8ce
--- /dev/null
+++ b/devtools/client/themes/images/firebug/breakpoint.svg
@@ -0,0 +1,13 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="12" height="12">
+ <defs>
+ <linearGradient id="a">
+ <stop offset="0" stop-color="#c80000"/>
+ <stop offset="1" stop-color="#780000"/>
+ </linearGradient>
+ <radialGradient cx="4.8" cy="4.665" r="5.59" fx="4.8" fy="4.665" id="b" xlink:href="#a" gradientUnits="userSpaceOnUse"/>
+ </defs>
+ <path d="M11.553 5.995a5.59 5.59 0 1 1-11.18 0 5.59 5.59 0 1 1 11.18 0z" transform="translate(-.4 -.435) scale(1.0734)" fill="url(#b)"/>
+</svg>
diff --git a/devtools/client/themes/images/firebug/close.svg b/devtools/client/themes/images/firebug/close.svg
new file mode 100644
index 000000000..2e5a04af3
--- /dev/null
+++ b/devtools/client/themes/images/firebug/close.svg
@@ -0,0 +1,25 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16">
+ <defs>
+ <linearGradient id="a">
+ <stop offset="0" stop-color="#f2451d"/>
+ <stop offset=".101" stop-color="#f01428" stop-opacity=".8"/>
+ <stop offset=".897" stop-color="#de8493"/>
+ <stop offset="1" stop-color="#efc3cc"/>
+ </linearGradient>
+ <linearGradient id="b">
+ <stop offset="0" stop-color="#520e0d"/>
+ <stop offset="1" stop-color="#c4181d"/>
+ </linearGradient>
+ <linearGradient x1="8.769" y1="1049.931" x2="8.769" y2="1038.668" id="c" xlink:href="#a" gradientUnits="userSpaceOnUse" gradientTransform="translate(0 -1036.362)"/>
+ <linearGradient x1="7.231" y1="1051.323" x2="7.231" y2="1037.401" id="d" xlink:href="#b" gradientUnits="userSpaceOnUse" gradientTransform="translate(0 -1036.362)"/>
+ <filter x="-.24" y="-.24" width="1.48" height="1.48" color-interpolation-filters="sRGB" id="e">
+ <feGaussianBlur stdDeviation=".713"/>
+ </filter>
+ </defs>
+ <rect width="13" height="13" rx="2" ry="2" x="1.5" y="1.5" fill="url(#c)" stroke="url(#d)" stroke-linejoin="round"/>
+ <path d="M6.5 5.437L4.938 7l2 2-2 2L6.5 12.562l2-2 2 2L12.064 11l-2-2 2-2L10.5 5.437l-2 2-2-2z" opacity=".4" filter="url(#e)"/>
+ <path d="M6 4.438L4.437 6l2 2-2 2L6 11.563l2-2 2 2L11.563 10l-2-2 2-2L10 4.437l-2 2-2-2z" fill="#fff"/>
+</svg>
diff --git a/devtools/client/themes/images/firebug/command-console.svg b/devtools/client/themes/images/firebug/command-console.svg
new file mode 100644
index 000000000..ff23daa64
--- /dev/null
+++ b/devtools/client/themes/images/firebug/command-console.svg
@@ -0,0 +1,31 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16">
+ <defs>
+ <linearGradient id="a">
+ <stop offset="0" stop-color="#325de6"/>
+ <stop offset="1" stop-color="#8ba3f1"/>
+ </linearGradient>
+ <linearGradient id="b">
+ <stop offset="0" stop-color="#1a47d6"/>
+ <stop offset="1" stop-color="#6786ed"/>
+ </linearGradient>
+ <linearGradient x1="7.771" y1="13.61" x2="7.771" y2="2.16" id="e" xlink:href="#a" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-.70047 0 0 .8 8.145 1037.962)"/>
+ <linearGradient x1="7.21" y1="14.919" x2="7.21" y2="1.081" id="f" xlink:href="#b" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-.70047 0 0 .8 8.145 1037.962)"/>
+ <linearGradient id="c">
+ <stop offset="0" stop-color="#282828"/>
+ <stop offset="1" stop-color="#505050"/>
+ </linearGradient>
+ <linearGradient id="d">
+ <stop offset="0"/>
+ <stop offset="1" stop-color="#3c3c3c"/>
+ </linearGradient>
+ <linearGradient x1="7.771" y1="15.451" x2="7.771" y2="3.941" id="g" xlink:href="#c" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-.9204 0 0 .56 15.804 1039.472)"/>
+ <linearGradient x1="7.21" y1="16.411" x2="7.21" y2="3.037" id="h" xlink:href="#d" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-.9204 0 0 .56 15.804 1039.472)"/>
+ </defs>
+ <g stroke-linejoin="round">
+ <path d="M1.14 1039.162l4.56 5.167-4.56 5.233-.84-.8 3.166-4.433-3.166-4.367z" fill="url(#e)" stroke="url(#f)" stroke-width=".6" transform="translate(0 -1036.362)"/>
+ <path d="M5.688 1040.05v1.125h10.125v-1.125H5.687zm2.5 3.75v1.125h7.624v-1.125H8.189zm-2.5 3.75v1.125h10.125v-1.125H5.687z" style="marker:none" color="#000" fill="url(#g)" stroke="url(#h)" stroke-width=".4" overflow="visible" transform="translate(0 -1036.362)"/>
+ </g>
+</svg>
diff --git a/devtools/client/themes/images/firebug/command-eyedropper.svg b/devtools/client/themes/images/firebug/command-eyedropper.svg
new file mode 100644
index 000000000..be8f15130
--- /dev/null
+++ b/devtools/client/themes/images/firebug/command-eyedropper.svg
@@ -0,0 +1,38 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 16 16" height="16" width="16">
+ <defs>
+ <linearGradient id="e">
+ <stop offset="0" stop-color="#e97f7f"/>
+ <stop offset="1" stop-color="#efa1a1"/>
+ </linearGradient>
+ <linearGradient id="c">
+ <stop offset="0" stop-color="#e2e9ea"/>
+ <stop offset="1" stop-color="#fff"/>
+ </linearGradient>
+ <linearGradient id="d">
+ <stop offset="0" stop-color="#65888b"/>
+ <stop offset="1" stop-color="#91adaf"/>
+ </linearGradient>
+ <linearGradient id="b">
+ <stop offset="0" stop-color="#746b54"/>
+ <stop offset="1" stop-color="#454033"/>
+ </linearGradient>
+ <linearGradient id="a">
+ <stop offset="0" stop-color="#8c7f64"/>
+ <stop offset="1" stop-color="#5d5543"/>
+ </linearGradient>
+ <radialGradient xlink:href="#a" id="h" cx="8.847" cy="1.845" fx="8.847" fy="1.845" r="1.587" gradientTransform="matrix(1.27453 .37742 -.46407 1.55272 -1.03 -4.65)" gradientUnits="userSpaceOnUse"/>
+ <linearGradient gradientTransform="matrix(1.11829 0 0 1.11313 -.505 -.258)" xlink:href="#b" id="i" x1="8.531" y1=".95" x2="9.908" y2="4.42" gradientUnits="userSpaceOnUse"/>
+ <linearGradient gradientTransform="matrix(1.11794 0 0 1.11348 .554 .12)" xlink:href="#b" id="k" x1="-6.81" y1="-1.866" x2="-12.495" y2="-1.812" gradientUnits="userSpaceOnUse"/>
+ <linearGradient gradientTransform="matrix(1.11794 0 0 1.11348 .554 .12)" xlink:href="#a" id="j" x1="-7.216" y1="-2.356" x2="-12.008" y2="-2.36" gradientUnits="userSpaceOnUse"/>
+ <linearGradient gradientTransform="matrix(1.11829 0 0 1.11313 -.505 -.258)" xlink:href="#c" id="f" x1="7.153" y1="11.831" x2="6.271" y2="11.424" gradientUnits="userSpaceOnUse"/>
+ <linearGradient gradientTransform="matrix(1.11829 0 0 1.11313 -.505 -.258)" xlink:href="#d" id="g" x1="8.328" y1="9.463" x2="6.703" y2="8.785" gradientUnits="userSpaceOnUse"/>
+ <linearGradient xlink:href="#e" id="l" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.11829 0 0 1.11313 -.505 -.258)" x1="7.153" y1="11.831" x2="6.271" y2="11.424"/>
+ </defs>
+ <path d="M6.705 15.7c1.12-1.51 1.556-2.841 1.973-4.387.502-1.793.766-3.668 1.251-5.471l-1.73-.462c-.485 1.804-1.194 3.562-1.665 5.363-.406 1.532-.717 2.951-.495 4.78z" fill="url(#f)" stroke="url(#g)" stroke-width=".4" stroke-linejoin="round"/>
+ <path d="M10.647 4.88a1.677 2.784 15.142 0 0 .76-1.525 1.677 2.784 15.142 0 0-.896-3.12 1.677 2.784 15.142 0 0-2.344 2.256 1.677 2.784 15.142 0 0-.109 1.7l2.589.69z" fill="url(#h)" stroke="url(#i)" stroke-width=".4" stroke-linejoin="round"/>
+ <rect width="6.708" height="1.113" x="-13.565" y="-3.086" rx=".5" ry=".5" transform="rotate(-165.066) skewX(-.132)" fill="url(#j)" stroke="url(#k)" stroke-width=".4" stroke-linejoin="round"/>
+ <path d="M8.558 10.973l-2.003.496c-.315 1.252-.487 2.44-.332 3.901l.406.107c1.03-1.428 1.416-2.582 1.86-4.226.028-.107.05-.199.07-.278z" fill="url(#l)"/>
+</svg>
diff --git a/devtools/client/themes/images/firebug/command-frames.svg b/devtools/client/themes/images/firebug/command-frames.svg
new file mode 100644
index 000000000..257d036f4
--- /dev/null
+++ b/devtools/client/themes/images/firebug/command-frames.svg
@@ -0,0 +1,25 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 16 16" height="16" width="16">
+ <defs>
+ <linearGradient id="b">
+ <stop offset="0" stop-color="#fff"/>
+ <stop offset="1" stop-color="#f0f0f0"/>
+ </linearGradient>
+ <linearGradient id="c">
+ <stop offset="0" stop-color="#e1e8ff"/>
+ <stop offset="1" stop-color="#b9c9ff"/>
+ </linearGradient>
+ <linearGradient id="a">
+ <stop offset="0" stop-color="#505050"/>
+ <stop offset="1" stop-color="#787878"/>
+ </linearGradient>
+ <linearGradient gradientTransform="translate(.078 -1.018) scale(1.07692)" gradientUnits="userSpaceOnUse" y2="2.767" x2="1.624" y1="14.154" x1="13.01" id="d" xlink:href="#a"/>
+ <linearGradient gradientUnits="userSpaceOnUse" y2="12.503" x2="12.396" y1="3.285" x1="3.179" id="e" xlink:href="#b"/>
+ <linearGradient y2="12.503" x2="12.396" y1="3.285" x1="3.179" gradientUnits="userSpaceOnUse" id="f" xlink:href="#c"/>
+ </defs>
+ <rect ry="1" rx="1" y="1" x="1" height="14" width="14" fill="url(#d)"/>
+ <path d="M7 2h7v3H7zM2 2h4v12H2z" fill="url(#e)"/>
+ <path d="M7 6h7v8H7z" fill="url(#f)"/>
+</svg>
diff --git a/devtools/client/themes/images/firebug/command-measure.svg b/devtools/client/themes/images/firebug/command-measure.svg
new file mode 100644
index 000000000..92964c950
--- /dev/null
+++ b/devtools/client/themes/images/firebug/command-measure.svg
@@ -0,0 +1,26 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 16 16" height="16" width="16">
+ <defs>
+ <linearGradient id="b">
+ <stop offset="0" stop-color="#505424"/>
+ <stop offset="1" stop-color="#6c6f31"/>
+ </linearGradient>
+ <linearGradient id="a">
+ <stop offset="0" stop-color="#e8eace"/>
+ <stop offset="1" stop-color="#f5f6ea"/>
+ </linearGradient>
+ <linearGradient id="c">
+ <stop offset="0" stop-color="#484a2e"/>
+ <stop offset="1" stop-color="#61633d"/>
+ </linearGradient>
+ <linearGradient xlink:href="#a" id="d" x1="-.831" y1="11.595" x2="2.378" y2="1.336" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.2174 0 0 1 -11.66 -4.6)"/>
+ <linearGradient xlink:href="#b" id="e" x1="1.536" y1="14.334" x2="5.493" y2="1.678" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.2174 0 0 1 -11.66 -4.6)"/>
+ <linearGradient xlink:href="#c" id="f" x1="1.695" y1="14.28" x2="6.421" y2="1.672" gradientUnits="userSpaceOnUse" gradientTransform="rotate(-90 3.4 8)"/>
+ </defs>
+ <g transform="translate(4.6 .6)">
+ <rect transform="rotate(-90)" ry=".5" rx=".5" y="-3.4" x="-10.2" height="13.6" width="5.6" fill="url(#d)" stroke="url(#e)" stroke-width=".4" stroke-linejoin="round"/>
+ <path d="M-2.1 10.4v-3m11 3v-3m-2.75 3V8.9M3.4 10.4v-3m-2.75 3V8.9" fill="none" stroke="url(#f)"/>
+ </g>
+</svg>
diff --git a/devtools/client/themes/images/firebug/command-noautohide.svg b/devtools/client/themes/images/firebug/command-noautohide.svg
new file mode 100644
index 000000000..cd6f4c98d
--- /dev/null
+++ b/devtools/client/themes/images/firebug/command-noautohide.svg
@@ -0,0 +1,47 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 16 16" height="16" width="16">
+ <defs>
+ <linearGradient id="b">
+ <stop offset="0" stop-color="#747254"/>
+ <stop offset="1" stop-color="#8b8965"/>
+ </linearGradient>
+ <linearGradient id="a">
+ <stop offset="0" stop-color="#e1e1d7"/>
+ <stop offset="1" stop-color="#f2f2ee"/>
+ </linearGradient>
+ <linearGradient gradientTransform="translate(-2.38 -2.38) scale(1.17241)" gradientUnits="userSpaceOnUse" y2="4.847" x2="2.94" y1="12.978" x1="13.037" id="g" xlink:href="#a"/>
+ <linearGradient gradientTransform="translate(-2.38 -2.38) scale(1.17241)" gradientUnits="userSpaceOnUse" y2="2.729" x2="4.832" y1="11.063" x1="14.997" id="h" xlink:href="#b"/>
+ <linearGradient gradientTransform="matrix(.64 0 0 .6988 .88 .987)" gradientUnits="userSpaceOnUse" xlink:href="#c" id="i" y2=".583" x2="6.34" y1="4.311" x1="8.637"/>
+ <linearGradient id="c">
+ <stop offset="0" stop-color="#c8c8c8"/>
+ <stop offset="1" stop-color="#dcdcdc"/>
+ </linearGradient>
+ <linearGradient gradientTransform="matrix(.64 0 0 .6988 .88 .987)" gradientUnits="userSpaceOnUse" xlink:href="#d" id="j" y2="1.392" x2="4.956" y1="5.078" x1="7.188"/>
+ <linearGradient id="d">
+ <stop offset="0" stop-color="#a0a0a0"/>
+ <stop offset="1" stop-color="#c8c8c8"/>
+ </linearGradient>
+ <linearGradient gradientTransform="matrix(.62152 0 0 .5895 1.028 -609.403)" gradientUnits="userSpaceOnUse" xlink:href="#c" id="k" y2="1040.666" x2="4.559" y1="1052.085" x1="11.377"/>
+ <linearGradient gradientTransform="matrix(.62152 0 0 .5895 1.028 -609.403)" gradientUnits="userSpaceOnUse" xlink:href="#d" id="l" y2="1041.923" x2="1.917" y1="1053.385" x1="8.842"/>
+ <linearGradient gradientTransform="matrix(.71429 0 0 .71492 .286 .276)" gradientUnits="userSpaceOnUse" xlink:href="#e" id="m" y2="7.825" x2="6.608" y1="12.498" x1="8.54"/>
+ <linearGradient id="e">
+ <stop offset="0" stop-color="#505050"/>
+ <stop offset="1" stop-color="#787878"/>
+ </linearGradient>
+ <linearGradient gradientTransform="matrix(.71429 0 0 .71492 .286 .276)" gradientUnits="userSpaceOnUse" xlink:href="#f" id="n" y2="7.414" x2="7.402" y1="12.116" x1="9.392"/>
+ <linearGradient id="f">
+ <stop offset="0" stop-color="#787878"/>
+ <stop offset="1" stop-color="#b4b4b4"/>
+ </linearGradient>
+ </defs>
+ <g stroke-linejoin="round">
+ <path d="M9.351.2l1.603 1.575.833.819h.84c.65 0 1.173.52 1.173 1.167v8.872c0 .646-.523 1.167-1.172 1.167H1.372C.722 13.8.2 13.28.2 12.633V3.76c0-.647.523-1.167 1.172-1.167h5.543l.834-.819L9.35.2z" fill="url(#g)" stroke="url(#h)" stroke-width=".4"/>
+ <g transform="matrix(.9821 0 0 .98213 5.107 5.107)">
+ <path d="M6 1.215a2.982 2.982 0 0 0-2.991 2.904c.114-.019.238-.045.357-.045h.938C4.386 3.194 5.1 2.421 6 2.421c.899 0 1.614.773 1.696 1.653h.938c.119 0 .243.026.357.045A2.982 2.982 0 0 0 6 1.215z" fill="url(#i)" stroke="url(#j)" stroke-width=".611"/>
+ <rect y="4.065" x="1.214" ry=".787" rx=".787" height="6.72" width="9.571" fill="url(#k)" stroke="url(#l)" stroke-width=".611"/>
+ <path d="M6 5.504A1.2 1.2 0 0 0 4.795 6.71c0 .562.375 1.023.893 1.162v1.475h.625V7.872c.517-.139.892-.6.892-1.162A1.2 1.2 0 0 0 6 5.504z" fill="url(#m)" stroke="url(#n)" stroke-width=".407"/>
+ </g>
+ </g>
+</svg>
diff --git a/devtools/client/themes/images/firebug/command-paintflashing.svg b/devtools/client/themes/images/firebug/command-paintflashing.svg
new file mode 100644
index 000000000..84d5741b3
--- /dev/null
+++ b/devtools/client/themes/images/firebug/command-paintflashing.svg
@@ -0,0 +1,38 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 16 16" height="16" width="16">
+ <defs>
+ <linearGradient id="d">
+ <stop offset="0" stop-color="#8c8c8c"/>
+ <stop offset="1" stop-color="#b4b4b4"/>
+ </linearGradient>
+ <linearGradient id="a">
+ <stop offset="0" stop-color="#544024"/>
+ <stop offset="1" stop-color="#8b6b3d"/>
+ </linearGradient>
+ <linearGradient id="b">
+ <stop offset="0" stop-color="#a0a0a0"/>
+ <stop offset="1" stop-color="#a0a0a0" stop-opacity="0"/>
+ </linearGradient>
+ <linearGradient id="e">
+ <stop offset="0" stop-color="#a0a0a0"/>
+ <stop offset="1" stop-color="#c8c8c8"/>
+ </linearGradient>
+ <linearGradient id="c">
+ <stop offset="0" stop-color="#382b18"/>
+ <stop offset="1" stop-color="#6f5631"/>
+ </linearGradient>
+ <linearGradient gradientUnits="userSpaceOnUse" y2="3.409" x2="6.735" y1="3.859" x1="9.123" id="f" xlink:href="#a"/>
+ <linearGradient gradientUnits="userSpaceOnUse" y2="8.347" x2="2.4" y1="9.586" x1="13.352" id="j" xlink:href="#a"/>
+ <linearGradient gradientUnits="userSpaceOnUse" y2="11.675" x2="7.974" y1="14.423" x1="7.974" id="l" xlink:href="#b" gradientTransform="translate(0 -.2)"/>
+ <linearGradient xlink:href="#c" id="g" x1="9.32" y1="7.243" x2="6.728" y2="6.716" gradientUnits="userSpaceOnUse"/>
+ <linearGradient xlink:href="#c" id="k" x1="12.451" y1="11.469" x2="1.56" y2="10.342" gradientUnits="userSpaceOnUse"/>
+ <linearGradient xlink:href="#d" id="i" x1="13.218" y1="13.627" x2="2.686" y2="13.627" gradientUnits="userSpaceOnUse"/>
+ <linearGradient xlink:href="#e" id="h" x1="12.607" y1="12.021" x2="3.321" y2="12.021" gradientUnits="userSpaceOnUse"/>
+ </defs>
+ <path d="M8 1c-.828 0-1.5 1.343-1.5 3v3.5c0 .554.446 1 1 1h1c.554 0 1-.446 1-1V4c0-1.657-.672-3-1.5-3z" fill="url(#f)" stroke="url(#g)" stroke-width=".4" stroke-linejoin="round"/>
+ <rect width="11" height="5" x="2.5" y="10" rx=".5" ry=".5" fill="url(#h)" stroke="url(#i)" stroke-width=".4" stroke-linejoin="round"/>
+ <rect width="12" height="2" x="2" y="8" rx=".5" ry=".5" fill="url(#j)" stroke="url(#k)" stroke-width=".4" stroke-linejoin="round"/>
+ <path d="M11.5 10.8h1v4h-1zm-2 0h1v4h-1zm-2 0h1v4h-1zm-2 0h1v4h-1zm-2 0h1v4h-1z" fill="url(#l)"/>
+</svg>
diff --git a/devtools/client/themes/images/firebug/command-pick.svg b/devtools/client/themes/images/firebug/command-pick.svg
new file mode 100644
index 000000000..157460da3
--- /dev/null
+++ b/devtools/client/themes/images/firebug/command-pick.svg
@@ -0,0 +1,20 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16">
+ <defs>
+ <linearGradient id="a">
+ <stop offset="0" stop-color="#6f9fdf"/>
+ <stop offset="1" stop-color="#b8d0f1"/>
+ </linearGradient>
+ <linearGradient id="b">
+ <stop offset="0" stop-color="#133cb8"/>
+ <stop offset="1" stop-color="#7faae8"/>
+ </linearGradient>
+ <linearGradient x1="11.304" y1="9.268" x2="7.065" y2="4.197" id="d" xlink:href="#a" gradientUnits="userSpaceOnUse"/>
+ <linearGradient x1="6.587" y1="7.594" x2="2.992" y2=".487" id="c" xlink:href="#b" gradientUnits="userSpaceOnUse"/>
+ </defs>
+ <path d="M.5.813c-.273.04-.502.354-.5.687v5.625c0 .36.265.688.563.688h4.406v-1h-3.5a.52.52 0 0 1-.5-.5V2.218a.517.517 0 0 1 .437-.5h3.75c.227-.4.634-.711 1.094-.75.415-.031.84.134 1.125.437l.313.313h6.843c.262 0 .5.238.5.5v4.094a.52.52 0 0 1-.5.5h-1.969l.938 1h1.938c.297 0 .562-.328.562-.688V1.5c0-.36-.265-.688-.563-.688H.5z" fill="#fff"/>
+ <path d="M.5 0C.227.041-.002.355 0 .688v5.625C0 6.673.265 7 .563 7h4.406V6h-3.5a.52.52 0 0 1-.5-.5V1.406a.517.517 0 0 1 .437-.5h13.125c.262 0 .5.238.5.5V5.5a.52.52 0 0 1-.5.5h-2.75l.969 1h2.688c.297 0 .562-.328.562-.688V.688c0-.36-.265-.687-.563-.687H.5z" fill="url(#c)"/>
+ <path d="M6.375 2.375v9.906l2.094-.844 1.625 3.844 2.531-1.062-1.594-3.781 2.188-.876L9.813 5.97 6.374 2.375z" fill="url(#d)" stroke="#4673ce" stroke-linejoin="round"/>
+</svg>
diff --git a/devtools/client/themes/images/firebug/command-responsivemode.svg b/devtools/client/themes/images/firebug/command-responsivemode.svg
new file mode 100644
index 000000000..b292ffe7a
--- /dev/null
+++ b/devtools/client/themes/images/firebug/command-responsivemode.svg
@@ -0,0 +1,39 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 16 16" height="16" width="16">
+ <defs>
+ <linearGradient id="d">
+ <stop offset="0" stop-color="#141414"/>
+ <stop offset="1" stop-color="#3c3c3c"/>
+ </linearGradient>
+ <linearGradient id="c">
+ <stop offset="0" stop-color="#cdf0ff"/>
+ <stop offset="1" stop-color="#f5fcff"/>
+ </linearGradient>
+ <linearGradient id="b">
+ <stop offset="0" stop-color="#787878"/>
+ <stop offset="1" stop-color="#505050"/>
+ </linearGradient>
+ <linearGradient id="a">
+ <stop offset="0" stop-color="#282828"/>
+ <stop offset="1" stop-color="#505050"/>
+ </linearGradient>
+ <linearGradient gradientTransform="matrix(1.04615 0 0 1.06667 -.415 .667)" gradientUnits="userSpaceOnUse" y2="1.576" x2="3.337" y1="8.108" x1="14.078" id="e" xlink:href="#a"/>
+ <linearGradient gradientTransform="matrix(2 0 0 2 -13 -4)" gradientUnits="userSpaceOnUse" y2="4.752" x2="13.239" y1="5.261" x1="13.748" id="h" xlink:href="#b"/>
+ <linearGradient gradientTransform="translate(0 1)" gradientUnits="userSpaceOnUse" y2="1.854" x2="3.829" y1="8.432" x1="12.299" id="g" xlink:href="#c"/>
+ <linearGradient gradientTransform="translate(-.5 -.5)" gradientUnits="userSpaceOnUse" y2="5.946" x2=".987" y1="15.102" x1="5.989" id="i" xlink:href="#a"/>
+ <linearGradient gradientTransform="matrix(1.10989 0 0 .79278 -.4 .958)" gradientUnits="userSpaceOnUse" y2="10.774" x2="4.865" y1="10.774" x1="1.261" id="k" xlink:href="#c"/>
+ <linearGradient gradientTransform="matrix(1.5 0 0 1.5 -17.25 6)" gradientUnits="userSpaceOnUse" y2="4.752" x2="13.239" y1="5.261" x1="13.748" id="l" xlink:href="#b"/>
+ <linearGradient gradientTransform="matrix(.683 0 0 .6166 .985 2.273)" gradientUnits="userSpaceOnUse" y2="5.597" x2="2.826" y1="6.539" x1="3.079" id="m" xlink:href="#b"/>
+ <linearGradient xlink:href="#d" id="f" x1="13.677" y1="12.328" x2=".512" y2="4.058" gradientUnits="userSpaceOnUse" gradientTransform="matrix(.97143 0 0 .96 .257 .24)"/>
+ <linearGradient xlink:href="#d" id="j" x1="5.489" y1="14.602" x2=".487" y2="5.446" gradientUnits="userSpaceOnUse"/>
+ </defs>
+ <rect ry="1" rx="1" y="1.2" x="2.2" height="9.6" width="13.6" fill="url(#e)" stroke="url(#f)" stroke-width=".4" stroke-linejoin="round"/>
+ <rect ry=".5" rx=".5" y="2.5" x="3.5" height="7" width="9" fill="url(#g)"/>
+ <circle r="1" cy="6" cx="14" fill="url(#h)"/>
+ <rect ry="1" rx="1" y="5" height="10" width="6" fill="url(#i)" stroke="url(#j)" stroke-width=".4" stroke-linejoin="round"/>
+ <rect ry=".5" rx=".5" y="7" x="1" height="5" width="4" fill="url(#k)"/>
+ <circle r=".75" cy="13.5" cx="3" fill="url(#l)"/>
+ <rect ry=".5" rx=".5" y="5.75" x="2" height=".5" width="2" fill="url(#m)"/>
+</svg>
diff --git a/devtools/client/themes/images/firebug/command-rulers.svg b/devtools/client/themes/images/firebug/command-rulers.svg
new file mode 100644
index 000000000..0a7d4aaef
--- /dev/null
+++ b/devtools/client/themes/images/firebug/command-rulers.svg
@@ -0,0 +1,20 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 16 16" height="16" width="16">
+ <defs>
+ <linearGradient id="b">
+ <stop offset="0" stop-color="#e2e6ea"/>
+ <stop offset="1" stop-color="#f9fafb"/>
+ </linearGradient>
+ <linearGradient id="a">
+ <stop offset="0" stop-color="#323b46"/>
+ <stop offset="1" stop-color="#546374"/>
+ </linearGradient>
+ <linearGradient gradientUnits="userSpaceOnUse" y2=".822" x2="4.016" y1="13.198" x1="8.663" id="d" xlink:href="#a"/>
+ <linearGradient gradientUnits="userSpaceOnUse" y2="1.975" x2="2.064" y1="13.044" x1="6.113" id="c" xlink:href="#b"/>
+ <linearGradient gradientUnits="userSpaceOnUse" y2="1.511" x2="3.948" y1="13.163" x1="7.795" id="e" xlink:href="#a"/>
+ </defs>
+ <path d="M1.7 1.2c-.278 0-.5.222-.5.5v12.6c0 .278.222.5.5.5h3.6c.278 0 .5-.222.5-.5V5.8h8.5c.278 0 .5-.222.5-.5V1.7c0-.278-.222-.5-.5-.5H1.7z" fill="url(#c)" stroke="url(#d)" stroke-width=".4" stroke-linejoin="round"/>
+ <path d="M1 4.5h1.5M1 7.5h3m-3 3h1.5m-1.5 3h3M4.5 1v1.5m3-1.5v3m3-3v1.5m3-1.5v3" fill="none" stroke="url(#e)"/>
+</svg>
diff --git a/devtools/client/themes/images/firebug/command-scratchpad.svg b/devtools/client/themes/images/firebug/command-scratchpad.svg
new file mode 100644
index 000000000..a227f3c4b
--- /dev/null
+++ b/devtools/client/themes/images/firebug/command-scratchpad.svg
@@ -0,0 +1,38 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 16 16" height="16" width="16">
+ <defs>
+ <linearGradient id="e">
+ <stop offset="0" stop-color="#434f5d"/>
+ <stop offset="1" stop-color="#65788b"/>
+ </linearGradient>
+ <linearGradient id="d">
+ <stop offset="0" stop-color="#787878"/>
+ <stop offset="1" stop-color="#8c8c8c"/>
+ </linearGradient>
+ <linearGradient id="c">
+ <stop offset="0" stop-color="#8c8c8c"/>
+ <stop offset="1" stop-color="#a0a0a0"/>
+ </linearGradient>
+ <linearGradient id="b">
+ <stop offset="0" stop-color="#b6b38a"/>
+ <stop offset="1" stop-color="#d3d2bd"/>
+ </linearGradient>
+ <linearGradient id="a">
+ <stop offset="0" stop-color="#ecebe0"/>
+ <stop offset="1" stop-color="#fbfbf9" stop-opacity="0"/>
+ </linearGradient>
+ <linearGradient gradientTransform="translate(-1.018 -.726)" gradientUnits="userSpaceOnUse" y2="4.549" x2="4.08" y1="14.382" x1="13.934" id="f" xlink:href="#a"/>
+ <linearGradient gradientTransform="translate(-1.018 -.726)" gradientUnits="userSpaceOnUse" y2="4.836" x2="1.893" y1="16.614" x1="13.78" id="g" xlink:href="#b"/>
+ <linearGradient y2="2.41" x2="4.751" y1="4.023" x1="5.458" gradientTransform="translate(-1.018 -1.026)" gradientUnits="userSpaceOnUse" id="h" xlink:href="#c"/>
+ <linearGradient y2=".94" x2="4.252" y1="3.313" x1="5.323" gradientTransform="translate(0 -.3)" gradientUnits="userSpaceOnUse" id="i" xlink:href="#d"/>
+ <linearGradient gradientUnits="userSpaceOnUse" y2="9.29" x2="11.377" y1="9.29" x1="4.575" id="j" xlink:href="#e"/>
+ </defs>
+ <path style="marker:none" color="#000" overflow="visible" fill="url(#f)" stroke="url(#g)" stroke-linejoin="round" d="M2 2.75h12v12H2z"/>
+ <path style="marker:none" d="M4 .75c-.553 0-1 .672-1 1.5 0 .829.399 1.474 1 1.5.106.006.2-.08.2-.25 0-.168-.11-.25-.2-.25-.277 0-.5-.447-.5-1 0-.552.223-1 .5-1 .275 0 .5.448.5 1H5c0-.828-.448-1.5-1-1.5z" id="k" color="#000" overflow="visible" fill="url(#h)" stroke="url(#i)" stroke-width=".2" stroke-linejoin="round"/>
+ <path d="M4 11.45h7v1H4zm1-5.321h5v1H5zm1 2.66h6v1H6z" style="marker:none" color="#000" overflow="visible" fill="url(#j)"/>
+ <use height="100%" width="100%" transform="translate(2.667)" id="l" xlink:href="#k"/>
+ <use height="100%" width="100%" transform="translate(2.667)" id="m" xlink:href="#l"/>
+ <use height="100%" width="100%" transform="translate(2.667)" xlink:href="#m"/>
+</svg>
diff --git a/devtools/client/themes/images/firebug/command-screenshot.svg b/devtools/client/themes/images/firebug/command-screenshot.svg
new file mode 100644
index 000000000..9cc343e01
--- /dev/null
+++ b/devtools/client/themes/images/firebug/command-screenshot.svg
@@ -0,0 +1,39 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16">
+ <defs>
+ <linearGradient id="e">
+ <stop offset="0" stop-color="#65808b"/>
+ <stop offset="1" stop-color="#7c939c"/>
+ </linearGradient>
+ <linearGradient id="a">
+ <stop offset="0" stop-color="#43555d"/>
+ <stop offset="1" stop-color="#566a72"/>
+ </linearGradient>
+ <linearGradient id="d">
+ <stop offset="0" stop-color="#54a0ec"/>
+ <stop offset="1" stop-color="#99c5f7"/>
+ </linearGradient>
+ <linearGradient id="c">
+ <stop offset="0" stop-color="#f6fafe"/>
+ <stop offset="1" stop-color="#99c5f7"/>
+ </linearGradient>
+ <linearGradient id="b">
+ <stop offset="0" stop-color="#324046"/>
+ <stop offset="1" stop-color="#45555b"/>
+ </linearGradient>
+ <linearGradient xlink:href="#a" id="h" x1="13.661" y1="12.474" x2="2.358" y2="5.025" gradientUnits="userSpaceOnUse" gradientTransform="matrix(.97143 0 0 .96 .229 .612)"/>
+ <linearGradient xlink:href="#b" id="i" x1="16.505" y1="11.096" x2="2.974" y2="2.807" gradientUnits="userSpaceOnUse" gradientTransform="matrix(.97143 0 0 .96 .229 .612)"/>
+ <linearGradient xlink:href="#b" id="k" x1="10.3" y1="11.02" x2="5.662" y2="6.382" gradientUnits="userSpaceOnUse" gradientTransform="translate(0 .236)"/>
+ <linearGradient xlink:href="#a" id="j" x1="10.582" y1="9.815" x2="6.843" y2="6.172" gradientUnits="userSpaceOnUse" gradientTransform="translate(0 .236)"/>
+ <radialGradient xlink:href="#c" id="l" cx="7.075" cy="7.944" fx="7.075" fy="7.944" r="2.5" gradientUnits="userSpaceOnUse" gradientTransform="rotate(45 9.338 9.078) scale(1.00392 1.2642)"/>
+ <linearGradient xlink:href="#d" id="m" x1="8.873" y1="11.096" x2="5.628" y2="7.85" gradientUnits="userSpaceOnUse" gradientTransform="translate(0 .236)"/>
+ <linearGradient xlink:href="#a" id="g" x1="10.226" y1="3.728" x2="6.522" y2="2.07" gradientUnits="userSpaceOnUse" gradientTransform="translate(0 .314)"/>
+ <linearGradient xlink:href="#e" id="f" x1="9.212" y1="4.437" x2="6.127" y2="3.047" gradientUnits="userSpaceOnUse" gradientTransform="translate(0 .314)"/>
+ </defs>
+ <path d="M6.5 2.225c-.277 0-.641.14-.816.316L4.316 3.908c-.175.175-.093.317.184.317h7c.277 0 .359-.142.184-.317l-1.368-1.367c-.175-.175-.539-.316-.816-.316h-2z" fill="url(#f)" stroke="url(#g)" stroke-width=".4" stroke-linejoin="round"/>
+ <rect ry="1" rx="1" y="4.2" x="1.2" height="9.6" width="13.6" fill="url(#h)" stroke="url(#i)" stroke-width=".4" stroke-linejoin="round"/>
+ <circle r="3.5" cy="9" cx="8" fill="url(#j)" stroke="url(#k)" stroke-width=".4" stroke-linejoin="round"/>
+ <circle cx="8" cy="9" r="2.5" fill="url(#l)" stroke="url(#m)" stroke-width=".4" stroke-linejoin="round"/>
+</svg>
diff --git a/devtools/client/themes/images/firebug/commandline-icon.svg b/devtools/client/themes/images/firebug/commandline-icon.svg
new file mode 100644
index 000000000..7770f2b61
--- /dev/null
+++ b/devtools/client/themes/images/firebug/commandline-icon.svg
@@ -0,0 +1,26 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="14" height="14">
+ <defs>
+ <style>
+ path {
+ opacity: 0.5;
+ }
+ path:target {
+ opacity: 1;
+ }
+ </style>
+ <linearGradient id="b">
+ <stop offset="0" stop-color="#234ccd"/>
+ <stop offset="1" stop-color="#5d7de3"/>
+ </linearGradient>
+ <linearGradient id="a">
+ <stop offset="0" stop-color="#1e3faa"/>
+ <stop offset="1" stop-color="#3a61de"/>
+ </linearGradient>
+ <linearGradient x1="2.002" y1="12.252" x2="-.099" y2="6.755" id="d" xlink:href="#a" gradientUnits="userSpaceOnUse" gradientTransform="translate(5.841 1034.646)"/>
+ <linearGradient x1="3.309" y1="11.177" x2="1.468" y2="6.456" id="c" xlink:href="#b" gradientUnits="userSpaceOnUse" gradientTransform="translate(5.841 1034.646)"/>
+ </defs>
+ <path id="focus" d="M6.841 1040.052l-.437.406 2.469 3.688-2.47 3.687.438.407 3.438-4.094z" fill="url(#c)" stroke="url(#d)" stroke-width=".4" stroke-linecap="round" stroke-linejoin="round" transform="translate(-1.341 -1037.146)"/>
+</svg>
diff --git a/devtools/client/themes/images/firebug/debugger-blackbox.svg b/devtools/client/themes/images/firebug/debugger-blackbox.svg
new file mode 100644
index 000000000..e36e5e2e4
--- /dev/null
+++ b/devtools/client/themes/images/firebug/debugger-blackbox.svg
@@ -0,0 +1,30 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16">
+ <defs>
+ <linearGradient id="d">
+ <stop offset="0" stop-color="#323232"/>
+ <stop offset="1" stop-color="#646464"/>
+ </linearGradient>
+ <linearGradient id="c">
+ <stop offset="0" stop-color="#b4b4b4"/>
+ <stop offset="1" stop-color="#dcdcdc"/>
+ </linearGradient>
+ <linearGradient id="b">
+ <stop offset="0" stop-color="#3c3c3c"/>
+ <stop offset="1" stop-color="#646464"/>
+ </linearGradient>
+ <linearGradient id="a">
+ <stop offset="0" stop-color="#505050"/>
+ <stop offset="1" stop-color="#8c8c8c"/>
+ </linearGradient>
+ <linearGradient gradientTransform="matrix(.97143 0 0 1.08571 .229 -1125.879)" xlink:href="#a" id="e" x1="10.803" y1="1047.39" x2="4.726" y2="1041.559" gradientUnits="userSpaceOnUse"/>
+ <linearGradient gradientTransform="matrix(.97143 0 0 1.08571 .229 -1125.879)" xlink:href="#b" id="f" x1="12.563" y1="1046.633" x2="5.974" y2="1040.229" gradientUnits="userSpaceOnUse"/>
+ <linearGradient gradientTransform="translate(-1.333 -1210.423) scale(1.16667)" xlink:href="#c" id="g" x1="9.698" y1="1046.429" x2="5.893" y2="1042.623" gradientUnits="userSpaceOnUse"/>
+ <linearGradient gradientTransform="translate(0 -1036.362)" xlink:href="#d" id="h" x1="9.023" y1="1045.897" x2="6.49" y2="1043.363" gradientUnits="userSpaceOnUse"/>
+ </defs>
+ <path d="M14.8 8c0 1.085-3.044 3.8-6.8 3.8S1.2 9.085 1.2 8 4.244 4.2 8 4.2s6.8 2.715 6.8 3.8z" style="marker:none" color="#000" overflow="visible" fill="url(#e)" stroke="url(#f)" stroke-width=".4" stroke-linejoin="round"/>
+ <circle r="3.5" cy="8" cx="8" style="marker:none" color="#000" overflow="visible" fill="url(#g)"/>
+ <circle r="2" cy="8" cx="8" style="marker:none" color="#000" overflow="visible" fill="url(#h)"/>
+</svg>
diff --git a/devtools/client/themes/images/firebug/debugger-prettyprint.svg b/devtools/client/themes/images/firebug/debugger-prettyprint.svg
new file mode 100644
index 000000000..b720b39b0
--- /dev/null
+++ b/devtools/client/themes/images/firebug/debugger-prettyprint.svg
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16">
+ <defs>
+ <linearGradient id="a">
+ <stop offset="0" stop-color="#285a8c"/>
+ <stop offset="1" stop-color="#508cc8"/>
+ </linearGradient>
+ <linearGradient id="b">
+ <stop offset="0" stop-color="#1c3b5c"/>
+ <stop offset="1" stop-color="#285078"/>
+ </linearGradient>
+ <linearGradient x1="17.286" y1="1046.293" x2="-18.065" y2="1003.191" id="c" xlink:href="#a" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-1 0 0 1 28.42 0)"/>
+ <linearGradient x1="12.826" y1="1050.761" x2="-24.272" y2="1006.022" id="d" xlink:href="#b" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-1 0 0 1 28.42 0)"/>
+ </defs>
+ <path d="M21.734 1045.673v5.125h-1.816c-4.864 0-9.028-.723-10.688-2.168-1.64-1.445-2.46-4.326-2.46-8.643v-6.098c0-2.95-.528-4.99-1.583-6.123-1.054-1.133-2.968-1.7-5.742-1.7h-1.787v-4.189h1.787c2.793 0 4.707-.556 5.742-1.67 1.055-1.133 1.582-3.154 1.582-6.064v-6.127c0-4.317.82-7.188 2.461-8.613 1.66-1.446 5.824-2.168 10.688-2.168h1.816v5.093h-1.992c-2.754 0-4.55.43-5.39 1.29-.84.859-1.26 1.761-1.26 4.515v6.361c0 3.067-1.352 5.293-2.25 6.68-.88 1.387-1.49 2.324-3.64 2.813 2.169.527 2.79 1.484 3.669 2.87.879 1.387 2.22 3.604 2.22 6.65v6.363c0 2.754.42 3.654 1.26 4.514.84.859 2.96 1.289 5.391 1.289zm12.579 0v5.125h1.816c4.864 0 9.028-.723 10.688-2.168 1.64-1.445 2.46-4.326 2.46-8.643v-6.098c0-2.95.528-4.99 1.583-6.123 1.054-1.133 2.968-1.7 5.742-1.7h1.787v-4.189h-1.787c-2.793 0-4.707-.556-5.742-1.67-1.055-1.133-1.582-3.154-1.582-6.064v-6.127c0-4.317-.82-7.188-2.461-8.613-1.66-1.446-5.824-2.168-10.688-2.168h-1.816v5.093h1.992c2.754 0 4.55.43 5.39 1.29.84.859 1.26 1.761 1.26 4.515v6.361c0 3.067 1.352 5.293 2.25 6.68.88 1.387 1.49 2.324 3.64 2.813-2.169.527-2.79 1.484-3.669 2.87-.879 1.387-2.22 3.604-2.22 6.65v6.363c0 2.754-.42 3.654-1.26 4.514-.84.859-2.96 1.289-5.391 1.289z" style="-inkscape-font-specification:Fixedsys" font-size="60" fill="url(#c)" stroke="url(#d)" font-family="Fixedsys" transform="matrix(-.22157 0 0 .22103 14.138 -218.338)" font-weight="400" letter-spacing="0" word-spacing="0" stroke-width="1.807" stroke-linejoin="round"/>
+</svg>
diff --git a/devtools/client/themes/images/firebug/debugger-step-in.svg b/devtools/client/themes/images/firebug/debugger-step-in.svg
new file mode 100644
index 000000000..e9db77159
--- /dev/null
+++ b/devtools/client/themes/images/firebug/debugger-step-in.svg
@@ -0,0 +1,26 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16">
+ <defs>
+ <linearGradient id="c">
+ <stop offset="0" stop-color="#dd8506"/>
+ <stop offset="1" stop-color="#f4a24b"/>
+ </linearGradient>
+ <linearGradient id="b">
+ <stop offset="0" stop-color="#e68507"/>
+ <stop offset="1" stop-color="#f4b65f"/>
+ </linearGradient>
+ <linearGradient id="a">
+ <stop offset="0" stop-color="#f3a952"/>
+ <stop offset="1" stop-color="#fadbba"/>
+ </linearGradient>
+ <linearGradient x1="9.06" y1="13.305" x2="9.06" y2="1.704" id="d" xlink:href="#a" gradientUnits="userSpaceOnUse"/>
+ <linearGradient x1="3.865" y1="14.919" x2="3.865" y2="13.049" id="f" xlink:href="#b" gradientUnits="userSpaceOnUse" gradientTransform="matrix(.9974 0 0 1.0026 -1.01 -.02)"/>
+ <linearGradient x1="14.005" y1="14.902" x2="14.005" y2="13.07" id="g" xlink:href="#b" gradientUnits="userSpaceOnUse" gradientTransform="matrix(.9974 0 0 1.0026 -.959 -.02)"/>
+ <linearGradient x1="10.576" y1="11.641" x2=".835" y2="1.901" id="e" xlink:href="#c" gradientUnits="userSpaceOnUse"/>
+ </defs>
+ <path d="M.534 2.46h3.864C7.45 2.46 9.5 4.288 9.5 7.028v3.565h1.961L8.076 13.5l-3.382-2.914h1.899V7.74c0-1.554-.866-2.492-2.966-2.492H.534z" fill="url(#d)" stroke="url(#e)" stroke-linejoin="round"/>
+ <path fill="url(#f)" d="M1 13h4v2H1z"/>
+ <path fill="url(#g)" d="M11 13h4v2h-4z"/>
+</svg>
diff --git a/devtools/client/themes/images/firebug/debugger-step-out.svg b/devtools/client/themes/images/firebug/debugger-step-out.svg
new file mode 100644
index 000000000..017995859
--- /dev/null
+++ b/devtools/client/themes/images/firebug/debugger-step-out.svg
@@ -0,0 +1,26 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16">
+ <defs>
+ <linearGradient id="c">
+ <stop offset="0" stop-color="#dd8506"/>
+ <stop offset="1" stop-color="#f4a24b"/>
+ </linearGradient>
+ <linearGradient id="b">
+ <stop offset="0" stop-color="#e68507"/>
+ <stop offset="1" stop-color="#f4b65f"/>
+ </linearGradient>
+ <linearGradient id="a">
+ <stop offset="0" stop-color="#f3a952"/>
+ <stop offset="1" stop-color="#fadbba"/>
+ </linearGradient>
+ <linearGradient x1="-.161" y1="7.678" x2="12.316" y2="7.678" id="d" xlink:href="#a" gradientUnits="userSpaceOnUse" gradientTransform="rotate(-90 7.756 5.205)"/>
+ <linearGradient x1="14.005" y1="14.902" x2="14.005" y2="13.07" id="g" xlink:href="#b" gradientUnits="userSpaceOnUse" gradientTransform="matrix(.9974 0 0 1.0026 -.959 -.02)"/>
+ <linearGradient x1="3.865" y1="14.919" x2="3.865" y2="13.049" id="f" xlink:href="#b" gradientUnits="userSpaceOnUse" gradientTransform="matrix(.9974 0 0 1.0026 -1.01 -.02)"/>
+ <linearGradient x1="11.034" y1="9.145" x2="6.593" y2="4.703" id="e" xlink:href="#c" gradientUnits="userSpaceOnUse" gradientTransform="translate(0 -1.116)"/>
+ </defs>
+ <path d="M6.486 12.5V7.009c0-3.051 1.555-4.548 4.295-4.548h1.73V.5l2.907 3.385-2.914 3.382V5.368H11.7c-1.554 0-2.222.518-2.222 2.618V12.5z" fill="url(#d)" stroke="url(#e)" stroke-linejoin="round"/>
+ <path fill="url(#f)" d="M1 13h4v2H1z"/>
+ <path fill="url(#g)" d="M11 13h4v2h-4z"/>
+</svg>
diff --git a/devtools/client/themes/images/firebug/debugger-step-over.svg b/devtools/client/themes/images/firebug/debugger-step-over.svg
new file mode 100644
index 000000000..79960cb10
--- /dev/null
+++ b/devtools/client/themes/images/firebug/debugger-step-over.svg
@@ -0,0 +1,24 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16">
+ <defs>
+ <linearGradient id="c">
+ <stop offset="0" stop-color="#dd8506"/>
+ <stop offset="1" stop-color="#f4a24b"/>
+ </linearGradient>
+ <linearGradient id="b">
+ <stop offset="0" stop-color="#e68507"/>
+ <stop offset="1" stop-color="#f4b65f"/>
+ </linearGradient>
+ <linearGradient id="a">
+ <stop offset="0" stop-color="#f3a952"/>
+ <stop offset="1" stop-color="#fadbba"/>
+ </linearGradient>
+ <linearGradient x1="9.06" y1="13.305" x2="9.06" y2="1.704" id="d" xlink:href="#a" gradientUnits="userSpaceOnUse" gradientTransform="translate(3)"/>
+ <linearGradient x1="3.865" y1="14.919" x2="3.865" y2="13.049" id="f" xlink:href="#b" gradientUnits="userSpaceOnUse" gradientTransform="translate(3)"/>
+ <linearGradient x1="12.911" y1="12.657" x2="2.554" y2="2.3" id="e" xlink:href="#c" gradientUnits="userSpaceOnUse"/>
+ </defs>
+ <path d="M4.698 2.46h3.7c3.052 0 5.102 1.828 5.102 4.568v3.565h1.962L12.077 13.5l-3.383-2.914h1.9V7.74c0-1.793-.486-2.454-2.047-2.492-.791-.02-1.842 0-2.647 0-1.821 0-2.368.81-2.368 2.488V12.5H.522S.518 9.04.518 7.03c0-2.72 2.209-4.57 4.179-4.57z" fill="url(#d)" stroke="url(#e)" stroke-linejoin="round"/>
+ <path fill="url(#f)" d="M5.016 12.987h4.01v1.995h-4.01z"/>
+</svg>
diff --git a/devtools/client/themes/images/firebug/debugger-toggleBreakpoints.svg b/devtools/client/themes/images/firebug/debugger-toggleBreakpoints.svg
new file mode 100644
index 000000000..d85ab6391
--- /dev/null
+++ b/devtools/client/themes/images/firebug/debugger-toggleBreakpoints.svg
@@ -0,0 +1,13 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="16" width="16">
+ <defs>
+ <linearGradient id="a">
+ <stop offset="0" stop-color="#c80000"/>
+ <stop offset="1" stop-color="#780000"/>
+ </linearGradient>
+ <radialGradient gradientUnits="userSpaceOnUse" xlink:href="#a" id="b" fy="4.665" fx="4.8" r="5.59" cy="4.665" cx="4.8"/>
+ </defs>
+ <path transform="translate(1.599 1.565) scale(1.07342)" d="M11.553 5.995a5.59 5.59 0 1 1-11.179 0 5.59 5.59 0 1 1 11.18 0z" fill="url(#b)"/>
+</svg>
diff --git a/devtools/client/themes/images/firebug/disable.svg b/devtools/client/themes/images/firebug/disable.svg
new file mode 100644
index 000000000..d1f178099
--- /dev/null
+++ b/devtools/client/themes/images/firebug/disable.svg
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12">
+ <path d="M5.563 0A6 6 0 0 0 0 6a6 6 0 0 0 12 0 6 6 0 0 0-6.438-6zm.156 2a4 4 0 0 1 2.25.5L2.5 7.97A4 4 0 0 1 2 6a4 4 0 0 1 3.72-4zm3.685 1.906A4 4 0 0 1 10 6a4 4 0 0 1-6.094 3.406l5.5-5.5z" fill="red"/>
+</svg>
diff --git a/devtools/client/themes/images/firebug/dock-bottom.svg b/devtools/client/themes/images/firebug/dock-bottom.svg
new file mode 100644
index 000000000..2e7efa341
--- /dev/null
+++ b/devtools/client/themes/images/firebug/dock-bottom.svg
@@ -0,0 +1,25 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16">
+ <defs>
+ <linearGradient id="b">
+ <stop offset="0" stop-color="#f2451d"/>
+ <stop offset=".101" stop-color="#f01428" stop-opacity=".8"/>
+ <stop offset=".897" stop-color="#de8493"/>
+ <stop offset="1" stop-color="#efc3cc"/>
+ </linearGradient>
+ <linearGradient id="a">
+ <stop offset="0" stop-color="#520e0d"/>
+ <stop offset="1" stop-color="#c4181d"/>
+ </linearGradient>
+ <linearGradient x1="7.231" y1="1051.323" x2="7.231" y2="1037.401" id="d" xlink:href="#a" gradientUnits="userSpaceOnUse" gradientTransform="translate(0 -1036.362)"/>
+ <linearGradient x1="8.769" y1="1049.931" x2="8.769" y2="1038.668" id="c" xlink:href="#b" gradientUnits="userSpaceOnUse" gradientTransform="translate(0 -1036.362)"/>
+ <filter height="1.48" y="-.24" width="1.48" x="-.24" id="e" color-interpolation-filters="sRGB">
+ <feGaussianBlur stdDeviation=".8"/>
+ </filter>
+ </defs>
+ <rect y="1.5" x="1.5" ry="2" rx="2" height="13" width="13" fill="url(#c)" stroke="url(#d)" stroke-linejoin="round"/>
+ <path style="marker:none" d="M4.5 5v8h8V5zm1 1h6v4h-6z" color="#000" overflow="visible" opacity=".4" filter="url(#e)"/>
+ <path style="marker:none" d="M4 4v8h8V4zm1 1h6v4H5z" color="#000" overflow="visible" fill="#fff"/>
+</svg>
diff --git a/devtools/client/themes/images/firebug/dock-side.svg b/devtools/client/themes/images/firebug/dock-side.svg
new file mode 100644
index 000000000..e275eeaeb
--- /dev/null
+++ b/devtools/client/themes/images/firebug/dock-side.svg
@@ -0,0 +1,25 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16">
+ <defs>
+ <linearGradient id="b">
+ <stop offset="0" stop-color="#f2451d"/>
+ <stop offset=".101" stop-color="#f01428" stop-opacity=".8"/>
+ <stop offset=".897" stop-color="#de8493"/>
+ <stop offset="1" stop-color="#efc3cc"/>
+ </linearGradient>
+ <linearGradient id="a">
+ <stop offset="0" stop-color="#520e0d"/>
+ <stop offset="1" stop-color="#c4181d"/>
+ </linearGradient>
+ <linearGradient x1="7.231" y1="1051.323" x2="7.231" y2="1037.401" id="d" xlink:href="#a" gradientUnits="userSpaceOnUse" gradientTransform="translate(0 -1036.362)"/>
+ <linearGradient x1="8.769" y1="1049.931" x2="8.769" y2="1038.668" id="c" xlink:href="#b" gradientUnits="userSpaceOnUse" gradientTransform="translate(0 -1036.362)"/>
+ <filter height="1.48" y="-.24" width="1.48" x="-.24" id="e" color-interpolation-filters="sRGB">
+ <feGaussianBlur stdDeviation=".8"/>
+ </filter>
+ </defs>
+ <rect y="1.5" x="1.5" ry="2" rx="2" height="13" width="13" fill="url(#c)" stroke="url(#d)" stroke-linejoin="round"/>
+ <path style="marker:none" d="M4.5 5v8h8V5zm1 1h6v4h-6z" transform="rotate(-90 8.5 9)" color="#000" overflow="visible" opacity=".4" filter="url(#e)"/>
+ <path style="marker:none" d="M4 12h8V4H4zm1-1V5h4v6z" color="#000" overflow="visible" fill="#fff"/>
+</svg>
diff --git a/devtools/client/themes/images/firebug/dock-undock.svg b/devtools/client/themes/images/firebug/dock-undock.svg
new file mode 100644
index 000000000..7d24ef4b6
--- /dev/null
+++ b/devtools/client/themes/images/firebug/dock-undock.svg
@@ -0,0 +1,27 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16">
+ <defs>
+ <linearGradient id="b">
+ <stop offset="0" stop-color="#f2451d"/>
+ <stop offset=".101" stop-color="#f01428" stop-opacity=".8"/>
+ <stop offset=".897" stop-color="#de8493"/>
+ <stop offset="1" stop-color="#efc3cc"/>
+ </linearGradient>
+ <linearGradient id="a">
+ <stop offset="0" stop-color="#520e0d"/>
+ <stop offset="1" stop-color="#c4181d"/>
+ </linearGradient>
+ <linearGradient x1="7.231" y1="1051.323" x2="7.231" y2="1037.401" id="d" xlink:href="#a" gradientUnits="userSpaceOnUse"/>
+ <linearGradient x1="8.769" y1="1049.931" x2="8.769" y2="1038.668" id="c" xlink:href="#b" gradientUnits="userSpaceOnUse"/>
+ <filter x="-.24" y="-.24" width="1.48" height="1.48" color-interpolation-filters="sRGB" id="e">
+ <feGaussianBlur stdDeviation=".8"/>
+ </filter>
+ </defs>
+ <g transform="translate(0 -1036.362)">
+ <rect width="13" height="13" rx="2" ry="2" x="1.5" y="1037.862" fill="url(#c)" stroke="url(#d)" stroke-linejoin="round"/>
+ <path d="M6.5 1041.362v2h-2v6h6v-2h2v-6h-6zm1 1h4v4h-1v-3h-3v-1zm-2 2h4v4h-4v-4z" opacity=".4" filter="url(#e)"/>
+ <path d="M6 1040.362v2H4v6h6v-2h2v-6H6zm1 1h4v4h-1v-3H7v-1zm-2 2h4v4H5v-4z" fill="#fff"/>
+ </g>
+</svg>
diff --git a/devtools/client/themes/images/firebug/moz.build b/devtools/client/themes/images/firebug/moz.build
new file mode 100644
index 000000000..1516b4030
--- /dev/null
+++ b/devtools/client/themes/images/firebug/moz.build
@@ -0,0 +1,11 @@
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'read-only.svg',
+ 'spinner.png',
+ 'twisty-closed-firebug.svg',
+ 'twisty-open-firebug.svg',
+)
diff --git a/devtools/client/themes/images/firebug/pane-collapse.svg b/devtools/client/themes/images/firebug/pane-collapse.svg
new file mode 100644
index 000000000..7e4f00781
--- /dev/null
+++ b/devtools/client/themes/images/firebug/pane-collapse.svg
@@ -0,0 +1,29 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16">
+ <defs>
+ <linearGradient id="c">
+ <stop offset="0" stop-color="#fff" stop-opacity=".196"/>
+ <stop offset="1" stop-color="#fff" stop-opacity=".784"/>
+ </linearGradient>
+ <linearGradient id="a">
+ <stop offset="0" stop-color="#353593"/>
+ <stop offset="1" stop-color="#7373cd"/>
+ </linearGradient>
+ <linearGradient id="b">
+ <stop offset="0" stop-color="#2a2a76"/>
+ <stop offset="1" stop-color="#5656c2"/>
+ </linearGradient>
+ <linearGradient id="d">
+ <stop offset="0" stop-color="#aabccf"/>
+ <stop offset="1" stop-color="#c5d2df"/>
+ </linearGradient>
+ <linearGradient x1="9.29" y1="6.369" x2="5.581" y2="3.673" id="g" xlink:href="#a" gradientUnits="userSpaceOnUse" gradientTransform="matrix(.53813 0 0 .74017 3.298 3.873)"/>
+ <linearGradient x1="7.02" y1="7.949" x2="2.721" y2="4.824" id="h" xlink:href="#b" gradientUnits="userSpaceOnUse" gradientTransform="matrix(.53813 0 0 .74017 3.298 3.873)"/>
+ <linearGradient x1="14.692" y1="1049.087" x2="5.246" y2="1039.64" id="e" xlink:href="#c" gradientUnits="userSpaceOnUse" gradientTransform="translate(-1.167 -949.332) scale(.91667)"/>
+ <linearGradient x1="13.658" y1="1050.509" x2="3.64" y2="1040.492" id="f" xlink:href="#d" gradientUnits="userSpaceOnUse" gradientTransform="translate(-2 -1036.362)"/>
+ </defs>
+ <path fill="url(#e)" stroke="url(#f)" d="M2.5 2.5h11v11h-11z"/>
+ <path d="M9.7 8l-3.4 2.7V5.3z" fill="url(#g)" stroke="url(#h)" stroke-width=".6" stroke-linejoin="round"/>
+</svg>
diff --git a/devtools/client/themes/images/firebug/pane-expand.svg b/devtools/client/themes/images/firebug/pane-expand.svg
new file mode 100644
index 000000000..c4857a065
--- /dev/null
+++ b/devtools/client/themes/images/firebug/pane-expand.svg
@@ -0,0 +1,29 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16">
+ <defs>
+ <linearGradient id="a">
+ <stop offset="0" stop-color="#353593"/>
+ <stop offset="1" stop-color="#7373cd"/>
+ </linearGradient>
+ <linearGradient id="b">
+ <stop offset="0" stop-color="#2a2a76"/>
+ <stop offset="1" stop-color="#5656c2"/>
+ </linearGradient>
+ <linearGradient x1="11.709" y1="6.295" x2="8.675" y2="4.089" id="g" xlink:href="#a" gradientUnits="userSpaceOnUse" gradientTransform="matrix(.53813 0 0 .74017 3.298 3.873)"/>
+ <linearGradient x1="11.445" y1="8.382" x2="7.061" y2="5.195" id="h" xlink:href="#b" gradientUnits="userSpaceOnUse" gradientTransform="matrix(.53813 0 0 .74017 3.298 3.873)"/>
+ <linearGradient id="c">
+ <stop offset="0" stop-color="#fff" stop-opacity=".196"/>
+ <stop offset="1" stop-color="#fff" stop-opacity=".784"/>
+ </linearGradient>
+ <linearGradient id="d">
+ <stop offset="0" stop-color="#aabccf"/>
+ <stop offset="1" stop-color="#c5d2df"/>
+ </linearGradient>
+ <linearGradient x1="14.692" y1="1049.087" x2="5.246" y2="1039.64" id="e" xlink:href="#c" gradientUnits="userSpaceOnUse" gradientTransform="translate(-1.167 -949.332) scale(.91667)"/>
+ <linearGradient x1="13.658" y1="1050.509" x2="3.64" y2="1040.492" id="f" xlink:href="#d" gradientUnits="userSpaceOnUse" gradientTransform="translate(-2 -1036.362)"/>
+ </defs>
+ <path fill="url(#e)" stroke="url(#f)" d="M2.5 2.5h11v11h-11z"/>
+ <path d="M6.3 8l3.4 2.7V5.3z" fill="url(#g)" stroke="url(#h)" stroke-width=".6" stroke-linejoin="round"/>
+</svg>
diff --git a/devtools/client/themes/images/firebug/pause.svg b/devtools/client/themes/images/firebug/pause.svg
new file mode 100644
index 000000000..8763c376d
--- /dev/null
+++ b/devtools/client/themes/images/firebug/pause.svg
@@ -0,0 +1,31 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16">
+ <defs>
+ <linearGradient id="b">
+ <stop offset="0" stop-color="#dd8506"/>
+ <stop offset="1" stop-color="#f9c06e" stop-opacity=".988"/>
+ </linearGradient>
+ <linearGradient id="a">
+ <stop offset="0" stop-color="#f5a742"/>
+ <stop offset="1" stop-color="#f9cb8a"/>
+ </linearGradient>
+ <linearGradient x1="4.779" y1="1048.788" x2="3.117" y2="1039.853" id="e" xlink:href="#a" gradientUnits="userSpaceOnUse" gradientTransform="matrix(.993 0 0 .998 .028 2.025)"/>
+ <linearGradient x1="5.527" y1="1049.91" x2="2.514" y2="1038.877" id="f" xlink:href="#b" gradientUnits="userSpaceOnUse" gradientTransform="matrix(.993 0 0 .998 .028 2.025)"/>
+ <linearGradient id="c">
+ <stop offset="0" stop-color="#f5a742"/>
+ <stop offset="1" stop-color="#f9cb8a"/>
+ </linearGradient>
+ <linearGradient id="d">
+ <stop offset="0" stop-color="#dd8506"/>
+ <stop offset="1" stop-color="#f9c06e" stop-opacity=".988"/>
+ </linearGradient>
+ <linearGradient x1="4.779" y1="1048.788" x2="3.117" y2="1039.853" id="g" xlink:href="#c" gradientUnits="userSpaceOnUse" gradientTransform="matrix(.993 0 0 .998 7.028 2.025)"/>
+ <linearGradient x1="5.527" y1="1049.91" x2="2.514" y2="1038.877" id="h" xlink:href="#d" gradientUnits="userSpaceOnUse" gradientTransform="matrix(.993 0 0 .998 7.028 2.025)"/>
+ </defs>
+ <g stroke-linejoin="round">
+ <path fill="url(#e)" stroke="url(#f)" d="M2.5 1038.862h3v11h-3z" transform="translate(0 -1036.362)"/>
+ <path fill="url(#g)" stroke="url(#h)" d="M9.5 1038.862h3v11h-3z" transform="translate(0 -1036.362)"/>
+ </g>
+</svg>
diff --git a/devtools/client/themes/images/firebug/play.svg b/devtools/client/themes/images/firebug/play.svg
new file mode 100644
index 000000000..7762ed8e9
--- /dev/null
+++ b/devtools/client/themes/images/firebug/play.svg
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16">
+ <defs>
+ <linearGradient id="b">
+ <stop offset="0" stop-color="#2959b8"/>
+ <stop offset="1" stop-color="#83ace8"/>
+ </linearGradient>
+ <linearGradient id="a">
+ <stop offset="0" stop-color="#5c87d0"/>
+ <stop offset="1" stop-color="#abc7ed"/>
+ </linearGradient>
+ <linearGradient x1="1.472" y1="-4.098" x2="1.472" y2="6.772" id="c" xlink:href="#a" gradientUnits="userSpaceOnUse" gradientTransform="matrix(.65609 0 0 -1.01925 4.494 9.401)"/>
+ <linearGradient x1="10.18" y1="8.767" x2="3.926" y2="2.99" id="d" xlink:href="#b" gradientUnits="userSpaceOnUse"/>
+ </defs>
+ <path d="M11.788 8L4.5 1.204v13.592z" fill="url(#c)" stroke="url(#d)" stroke-linejoin="round"/>
+</svg>
diff --git a/devtools/client/themes/images/firebug/read-only.svg b/devtools/client/themes/images/firebug/read-only.svg
new file mode 100644
index 000000000..7521a093c
--- /dev/null
+++ b/devtools/client/themes/images/firebug/read-only.svg
@@ -0,0 +1,34 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="12" height="12">
+ <defs>
+ <linearGradient id="d">
+ <stop offset="0" stop-color="#787878"/>
+ <stop offset="1" stop-color="#b4b4b4"/>
+ </linearGradient>
+ <linearGradient id="c">
+ <stop offset="0" stop-color="#505050"/>
+ <stop offset="1" stop-color="#787878"/>
+ </linearGradient>
+ <linearGradient id="a">
+ <stop offset="0" stop-color="#c8c8c8"/>
+ <stop offset="1" stop-color="#dcdcdc"/>
+ </linearGradient>
+ <linearGradient id="b">
+ <stop offset="0" stop-color="#a0a0a0"/>
+ <stop offset="1" stop-color="#c8c8c8"/>
+ </linearGradient>
+ <linearGradient x1="8.637" y1="4.311" x2="6.34" y2=".583" id="e" xlink:href="#a" gradientUnits="userSpaceOnUse" gradientTransform="matrix(.64 0 0 .6988 .88 .987)"/>
+ <linearGradient x1="7.188" y1="5.078" x2="4.956" y2="1.392" id="f" xlink:href="#b" gradientUnits="userSpaceOnUse" gradientTransform="matrix(.64 0 0 .6988 .88 .987)"/>
+ <linearGradient x1="11.377" y1="1052.085" x2="4.559" y2="1040.666" id="g" xlink:href="#a" gradientUnits="userSpaceOnUse" gradientTransform="matrix(.62152 0 0 .5895 1.028 -609.403)"/>
+ <linearGradient x1="8.842" y1="1053.385" x2="1.917" y2="1041.923" id="h" xlink:href="#b" gradientUnits="userSpaceOnUse" gradientTransform="matrix(.62152 0 0 .5895 1.028 -609.403)"/>
+ <linearGradient x1="8.54" y1="12.498" x2="6.608" y2="7.825" id="i" xlink:href="#c" gradientUnits="userSpaceOnUse" gradientTransform="matrix(.71429 0 0 .71492 .286 .276)"/>
+ <linearGradient x1="9.392" y1="12.116" x2="7.402" y2="7.414" id="j" xlink:href="#d" gradientUnits="userSpaceOnUse" gradientTransform="matrix(.71429 0 0 .71492 .286 .276)"/>
+ </defs>
+ <g transform="matrix(1.19102 0 0 1.19106 -1.146 -1.146)" stroke-linejoin="round">
+ <path d="M6 1.215a2.982 2.982 0 0 0-2.991 2.904c.114-.019.238-.045.357-.045h.938C4.386 3.194 5.1 2.421 6 2.421c.899 0 1.614.773 1.696 1.653h.938c.119 0 .243.026.357.045A2.982 2.982 0 0 0 6 1.215z" fill="url(#e)" stroke="url(#f)" stroke-width=".504"/>
+ <rect width="9.571" height="6.72" rx="1.679" ry="1.679" x="1.214" y="4.065" fill="url(#g)" stroke="url(#h)" stroke-width=".504"/>
+ <path d="M6 5.504A1.2 1.2 0 0 0 4.795 6.71c0 .562.375 1.023.893 1.162v1.475h.625V7.872c.517-.139.892-.6.892-1.162A1.2 1.2 0 0 0 6 5.504z" fill="url(#i)" stroke="url(#j)" stroke-width=".336"/>
+ </g>
+</svg>
diff --git a/devtools/client/themes/images/firebug/rewind.svg b/devtools/client/themes/images/firebug/rewind.svg
new file mode 100644
index 000000000..88e7bb9aa
--- /dev/null
+++ b/devtools/client/themes/images/firebug/rewind.svg
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16">
+ <defs>
+ <linearGradient id="b">
+ <stop offset="0" stop-color="#2959b8"/>
+ <stop offset="1" stop-color="#83ace8"/>
+ </linearGradient>
+ <linearGradient id="a">
+ <stop offset="0" stop-color="#5c87d0"/>
+ <stop offset="1" stop-color="#abc7ed"/>
+ </linearGradient>
+ <linearGradient x1="1.472" y1="-4.098" x2="1.472" y2="6.772" id="c" xlink:href="#a" gradientUnits="userSpaceOnUse" gradientTransform="matrix(.65609 0 0 -1.01925 4.494 9.401)"/>
+ <linearGradient x1="10.18" y1="8.767" x2="3.926" y2="2.99" id="d" xlink:href="#b" gradientUnits="userSpaceOnUse"/>
+ </defs>
+ <path d="M11.788 8L4.5 1.204v13.592z" transform="matrix(-1 0 0 1 16 0)" fill="url(#c)" stroke="url(#d)" stroke-linejoin="round"/>
+</svg>
diff --git a/devtools/client/themes/images/firebug/spinner.png b/devtools/client/themes/images/firebug/spinner.png
new file mode 100644
index 000000000..eec57a809
--- /dev/null
+++ b/devtools/client/themes/images/firebug/spinner.png
Binary files differ
diff --git a/devtools/client/themes/images/firebug/tool-debugger-paused.svg b/devtools/client/themes/images/firebug/tool-debugger-paused.svg
new file mode 100644
index 000000000..67e5b3f1b
--- /dev/null
+++ b/devtools/client/themes/images/firebug/tool-debugger-paused.svg
@@ -0,0 +1,14 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="11" height="12">
+ <defs>
+ <linearGradient id="a">
+ <stop offset="0" stop-color="#b4aa00"/>
+ <stop offset=".659" stop-color="#f5e600"/>
+ <stop offset="1" stop-color="#f5e600"/>
+ </linearGradient>
+ <linearGradient x1="1.256" y1="6.226" x2=".157" y2=".942" id="b" xlink:href="#a" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.1625 0 0 1.2744 1.663 1040.82)"/>
+ </defs>
+ <path d="M8.553 1046.88l-2.742 1.735c-4.951 3.273-5.215 3.09-5.215.035v-6.845c0-2.706.26-2.927 5.215.208l2.593 1.641c2.642 1.553 2.642 1.648.149 3.226z" fill="url(#b)" stroke-width="1.217" stroke-linejoin="round" transform="matrix(1 0 0 .95762 0 -995.06)" stroke="#888000"/>
+</svg>
diff --git a/devtools/client/themes/images/firebug/tool-options.svg b/devtools/client/themes/images/firebug/tool-options.svg
new file mode 100644
index 000000000..4fee4fc40
--- /dev/null
+++ b/devtools/client/themes/images/firebug/tool-options.svg
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16">
+ <defs>
+ <linearGradient id="b">
+ <stop offset="0" stop-color="#464f5a"/>
+ <stop offset="1" stop-color="#7e8b9a"/>
+ </linearGradient>
+ <linearGradient id="a">
+ <stop offset="0" stop-color="#6a7786"/>
+ <stop offset="1" stop-color="#abb3bd"/>
+ </linearGradient>
+ <linearGradient xlink:href="#a" id="c" x1="13.108" y1="13.135" x2="2.763" y2="2.791" gradientUnits="userSpaceOnUse" gradientTransform="translate(.306 1036.661) scale(.9618)"/>
+ <linearGradient xlink:href="#b" id="d" x1="14.815" y1="11.602" x2="4.34" y2="1.127" gradientUnits="userSpaceOnUse" gradientTransform="translate(.306 1036.661) scale(.9618)"/>
+ </defs>
+ <path style="marker:none" d="M7.23 1036.661c-.426 0-.665.445-.768.962l-.25 1.25a5.77 5.77 0 0 0-.825.345l-1.063-.71c-.477-.205-.924-.437-1.225-.135l-1.088 1.087c-.301.302-.157.786.136 1.225l.706 1.062a5.77 5.77 0 0 0-.333.824l-1.258.252c-.482.193-.962.344-.962.77v1.538c0 .427.445.665.962.769l1.258.252a5.77 5.77 0 0 0 .34.815l-.713 1.073c-.205.477-.437.92-.136 1.223l1.088 1.089c.301.301.786.155 1.225-.137l1.069-.712a5.77 5.77 0 0 0 .815.33l.254 1.268c.192.482.342.962.768.962h1.54c.426 0 .665-.445.768-.962l.254-1.264a5.77 5.77 0 0 0 .81-.338l1.074.716c.477.204.924.438 1.225.137l1.088-1.09c.301-.301.157-.784-.136-1.222l-.71-1.067a5.77 5.77 0 0 0 .333-.821l1.262-.252c.482-.193.962-.342.962-.769v-1.538c0-.426-.445-.667-.962-.77l-1.253-.252a5.77 5.77 0 0 0-.342-.819l.71-1.067c.205-.477.437-.923.136-1.225l-1.088-1.087c-.301-.302-.786-.158-1.225.135l-1.057.706a5.77 5.77 0 0 0-.829-.336l-.252-1.255c-.192-.482-.342-.962-.768-.962H7.23zm.77 4.81a2.885 2.885 0 0 1 2.885 2.885A2.885 2.885 0 0 1 8 1047.24a2.885 2.885 0 0 1-2.885-2.885A2.885 2.885 0 0 1 8 1041.47z" color="#000" overflow="visible" fill="url(#c)" stroke="url(#d)" stroke-width=".6" stroke-linejoin="round" transform="translate(0 -1036.362)"/>
+</svg>
diff --git a/devtools/client/themes/images/firebug/twisty-closed-firebug.svg b/devtools/client/themes/images/firebug/twisty-closed-firebug.svg
new file mode 100644
index 000000000..a3cfc324e
--- /dev/null
+++ b/devtools/client/themes/images/firebug/twisty-closed-firebug.svg
@@ -0,0 +1,14 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="11" height="11">
+ <defs>
+ <linearGradient id="a">
+ <stop offset="0" stop-color="#c3baaa"/>
+ <stop offset="1" stop-color="#fff"/>
+ </linearGradient>
+ <linearGradient x1="6.053" y1="7.093" x2="2.888" y2="1.8" id="b" xlink:href="#a" gradientUnits="userSpaceOnUse" gradientTransform="translate(.885 .885) scale(1.02564)"/>
+ </defs>
+ <rect width="8" height="8" rx="1" ry="1" x="1.5" y="1.5" fill="url(#b)" stroke="#7898b5" stroke-linecap="round" stroke-linejoin="round"/>
+ <path d="M5 3v2H3v1h2v2h1V6h2V5H6V3H5z"/>
+</svg>
diff --git a/devtools/client/themes/images/firebug/twisty-open-firebug.svg b/devtools/client/themes/images/firebug/twisty-open-firebug.svg
new file mode 100644
index 000000000..e20d7f6e0
--- /dev/null
+++ b/devtools/client/themes/images/firebug/twisty-open-firebug.svg
@@ -0,0 +1,14 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="11" height="11">
+ <defs>
+ <linearGradient id="a">
+ <stop offset="0" stop-color="#c3baaa"/>
+ <stop offset="1" stop-color="#fff"/>
+ </linearGradient>
+ <linearGradient x1="6.053" y1="7.093" x2="2.888" y2="1.8" id="b" xlink:href="#a" gradientUnits="userSpaceOnUse" gradientTransform="translate(.885 .885) scale(1.02564)"/>
+ </defs>
+ <rect width="8" height="8" rx="1" ry="1" x="1.5" y="1.5" fill="url(#b)" stroke="#7898b5" stroke-linecap="round" stroke-linejoin="round"/>
+ <path d="M3 5h5v1H3z"/>
+</svg>
diff --git a/devtools/client/themes/images/geometry-editor.svg b/devtools/client/themes/images/geometry-editor.svg
new file mode 100644
index 000000000..b766740cd
--- /dev/null
+++ b/devtools/client/themes/images/geometry-editor.svg
@@ -0,0 +1,7 @@
+<!-- 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/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="#0b0b0b">
+ <path d="M14,8 L12,8 L12,11.25 L12,12 L11.5,12 L3.5,12 L3,12 L3,11.75 L3,11.5 L3,8 L1,8 L1,8 L1,8.5 L1,9 L0,9 L0,8.5 L0,6.5 L0,6 L1,6 L1,6.5 L1,7 L3,7 L3,3.5 L3,3 L3.72222222,3 L3.72222222,3 L10.5555556,3 L11,3 L11,4 L10.5555556,4 L4,4 L4,11 L11,11 L11,3.5 L11,3 L12,3 L12,3.5 L12,7 L14,7 L14,6.5 L14,6 L15,6 L15,6.5 L15,8.5 L15,9 L14,9 L14,8.5 L14,8 Z M8,14 L8.5,14 L9,14 L9,15 L8.5,15 L6.5,15 L6,15 L6,14 L6.5,14 L7,14 L7,11.5 L7,11 L8,11 L8,11.5 L8,14 Z M7,1 L6.5,1 L6,1 L6,0 L6.5,0 L8.5,0 L9,0 L9,1 L8.5,1 L8,1 L8,3.5 L8,4 L7,4 L7,3.5 L7,1 L7,1 Z"/>
+ <path d="M3.5,9 C4.32842712,9 5,8.32842712 5,7.5 C5,6.67157288 4.32842712,6 3.5,6 C2.67157288,6 2,6.67157288 2,7.5 C2,8.32842712 2.67157288,9 3.5,9 Z M7.5,13 C8.32842712,13 9,12.3284271 9,11.5 C9,10.6715729 8.32842712,10 7.5,10 C6.67157288,10 6,10.6715729 6,11.5 C6,12.3284271 6.67157288,13 7.5,13 Z M11.5,9 C12.3284271,9 13,8.32842712 13,7.5 C13,6.67157288 12.3284271,6 11.5,6 C10.6715729,6 10,6.67157288 10,7.5 C10,8.32842712 10.6715729,9 11.5,9 Z M7.5,5 C8.32842712,5 9,4.32842712 9,3.5 C9,2.67157288 8.32842712,2 7.5,2 C6.67157288,2 6,2.67157288 6,3.5 C6,4.32842712 6.67157288,5 7.5,5 Z"/>
+</svg> \ No newline at end of file
diff --git a/devtools/client/themes/images/globe.svg b/devtools/client/themes/images/globe.svg
new file mode 100644
index 000000000..a57a5af23
--- /dev/null
+++ b/devtools/client/themes/images/globe.svg
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+ <circle fill="#a6a6a6" cx="8" cy="8" r="7" />
+ <path transform="translate(1 1)" fill="#fff" d="M5.31617536,1.74095137 C5.29841561,1.73995137 5.27868256,1.74095137 5.26190947,1.74795137 C5.25796286,1.74995137 5.2530296,1.75395137 5.24908299,1.75895137 C5.2550029,1.75895137 5.26190947,1.75895137 5.26684273,1.75795137 C5.28460248,1.75395137 5.29841561,1.74195137 5.31617536,1.74095137 L5.31617536,1.74095137 Z M5.33886837,2.59995137 C5.36156138,2.57095137 5.30729549,2.54695137 5.27670926,2.54895137 C5.28460248,2.51395137 5.32900184,2.49595137 5.31716201,2.45195137 C5.30630884,2.40595137 5.25105629,2.41495137 5.22145672,2.43995137 C5.1948171,2.46295137 5.18100396,2.50295137 5.15831095,2.52995137 C5.14548447,2.54495137 5.12180481,2.54995137 5.11292494,2.56795137 C5.10503172,2.58495137 5.11489824,2.61395137 5.11391159,2.63295137 C5.15041773,2.63795137 5.18889718,2.62695137 5.2155368,2.60095137 L5.23329655,2.59295137 C5.22934994,2.59595137 5.22737663,2.60295137 5.22540333,2.60695137 C5.24316307,2.62895137 5.32209528,2.62295137 5.33886837,2.59995137 L5.33886837,2.59995137 Z M5.37636117,1.37295137 C5.37438786,1.42695137 5.42668044,1.43295137 5.46515989,1.45395137 C5.45332006,1.48495137 5.410894,1.48395137 5.39116095,1.50895137 C5.36748129,1.53995137 5.410894,1.56695137 5.43260036,1.58095137 C5.47502642,1.60695137 5.45134676,1.63695137 5.44345354,1.67395137 C5.43161371,1.72595137 5.54310544,1.71195137 5.56777176,1.71095137 C5.61019782,1.70895137 5.67729019,1.71595137 5.71774294,1.69595137 C5.76115565,1.67195137 5.78384866,1.61895137 5.82923468,1.59295137 C5.86672748,1.57095137 5.92000671,1.55895137 5.96144612,1.57395137 C6.00485883,1.58895137 5.99992557,1.64495137 6.03544506,1.66895137 C6.07688447,1.69795137 6.12227048,1.70695137 6.15778997,1.66395137 C6.18048298,1.63695137 6.23080226,1.60295137 6.23277557,1.57395137 C6.23672218,1.52295137 6.25152196,1.48295137 6.30776116,1.47195137 C6.35314718,1.46295137 6.34328065,1.50695137 6.37485353,1.51495137 C6.44490586,1.53295137 6.47845205,1.31895137 6.55442429,1.38195137 C6.57218404,1.39695137 6.5771173,1.45495137 6.60770353,1.44995137 C6.63927641,1.44495137 6.64026306,1.39895137 6.67380925,1.39795137 C6.68466243,1.42895137 6.61559675,1.46695137 6.60671688,1.50095137 C6.64914294,1.46595137 6.66986264,1.47095137 6.71820861,1.46595137 C6.7310351,1.49895137 6.63631645,1.55295137 6.61165014,1.55795137 C6.5771173,1.56695137 6.5563976,1.54695137 6.52975798,1.56595137 C6.50903828,1.57995137 6.48042535,1.57895137 6.45575904,1.58095137 C6.4212262,1.58495137 6.35610713,1.63095137 6.35709379,1.66895137 C6.35709379,1.68395137 6.36893362,1.71795137 6.35610713,1.72995137 C6.3442673,1.74295137 6.31565438,1.73095137 6.31269442,1.71795137 C6.28309485,1.76195137 6.2446154,1.68495137 6.21994908,1.74695137 C6.25941518,1.75695137 6.29592133,1.79495137 6.34032069,1.80595137 C6.3837334,1.81695137 6.42714612,1.82795137 6.46957217,1.83995137 C6.54159781,1.86195137 6.64914294,1.77495137 6.70439548,1.73295137 C6.75668806,1.69395137 6.82279378,1.60595137 6.83660692,1.54295137 C6.85239336,1.47395137 6.92737895,1.39495137 6.91159251,1.32695137 C6.89777937,1.26295137 6.88791285,1.23295137 6.95993848,1.20995137 C6.99052471,1.19995137 7.06452365,1.18395137 7.07537683,1.14895137 C7.09116327,1.09695137 6.9283656,1.11095137 6.90369929,1.09895137 C6.82180713,1.06195137 6.78628764,1.02095137 6.69156899,1.05795137 C6.64223637,1.07695137 6.59389039,1.09295137 6.54258446,1.10695137 C6.51594484,1.11395137 6.48930523,1.11595137 6.47450544,1.13895137 C6.46858552,1.14795137 6.4606923,1.15495137 6.45082578,1.15995137 C6.40839972,1.17695137 6.4606923,1.09595137 6.46562556,1.09095137 C6.4794387,1.07495137 6.50213171,1.02595137 6.45773234,1.03695137 C6.39261328,1.05195137 6.34525395,1.15195137 6.27520162,1.15695137 C6.22192239,1.16095137 6.23869548,1.11395137 6.25250862,1.08695137 C6.27914824,1.03795137 6.20317599,1.03195137 6.1696298,1.03195137 C6.12227048,1.03195137 6.08675099,1.05895137 6.04136497,1.06395137 C5.99893892,1.06795137 5.94960629,1.07595137 5.90718023,1.07495137 C5.82232811,1.07195137 5.76608892,1.12195137 5.68222345,1.09395137 C5.59342472,1.06495137 5.49771943,1.13895137 5.41188066,1.14895137 C5.38326773,1.15295137 5.34182833,1.14695137 5.3299885,1.18095137 C5.32012197,1.20895137 5.3299885,1.25195137 5.35169485,1.27295137 L5.35860142,1.26695137 C5.33985502,1.28595137 5.33788172,1.31295137 5.31025545,1.32295137 C5.28361583,1.33195137 5.25697621,1.36695137 5.24316307,1.39095137 C5.2323099,1.40895137 5.20172367,1.48395137 5.2550029,1.44495137 C5.29348235,1.41595137 5.31518871,1.36195137 5.37636117,1.37295137 L5.37636117,1.37295137 Z M2.18355356,6.10795137 C2.09278153,6.04195137 1.88657115,6.02595137 1.91222411,5.87195137 C1.92801055,5.77795137 2.0247025,5.70495137 2.10264805,5.65895137 C2.20525992,5.59895137 2.31971161,5.59695137 2.43514996,5.60695137 C2.46277623,5.60995137 2.51506881,5.60495137 2.5298686,5.62695137 C2.53776182,5.63795137 2.55354826,5.64495137 2.56637474,5.64895137 C2.59696097,5.65795137 2.62853385,5.65895137 2.66010674,5.66495137 C2.70746606,5.67395137 2.74101224,5.71495137 2.78837156,5.68095137 C2.84263745,5.64295137 2.85151733,5.63495137 2.91762305,5.64295137 C2.9768222,5.64995137 3.01234169,5.60495137 3.06167432,5.60895137 C3.07746076,5.60995137 3.09127389,5.61295137 3.10311372,5.61795137 C3.10804699,5.60095137 3.11495355,5.58595137 3.12580673,5.58295137 C3.15047305,5.57595137 3.20473893,5.63595137 3.2303919,5.64095137 C3.29551097,5.65495137 3.29156436,5.60895137 3.29649762,5.56195137 C3.32905715,5.55595137 3.34484359,5.60095137 3.37444317,5.57295137 C3.37345652,5.58195137 3.37937643,5.59595137 3.37937643,5.60495137 C3.38529635,5.60895137 3.39220292,5.60895137 3.39812283,5.60395137 C3.40108279,5.59895137 3.40206944,5.59395137 3.40009614,5.58795137 C3.41588258,5.59295137 3.4237758,5.58195137 3.4257491,5.56295137 C3.43758893,5.56395137 3.45633533,5.55695137 3.46817516,5.55895137 C3.47705503,5.52495137 3.49678809,5.47995137 3.47212177,5.44895137 C3.47804169,5.44795137 3.48494825,5.44595137 3.49185482,5.44495137 C3.49185482,5.41095137 3.51454783,5.39595137 3.51553448,5.36895137 C3.48001499,5.36395137 3.44054889,5.36595137 3.40404275,5.36695137 C3.4257491,5.34695137 3.47804169,5.30295137 3.48297495,5.27595137 C3.49284148,5.22795137 3.43068237,5.19895137 3.43561563,5.14195137 C3.44153554,5.17195137 3.47508173,5.24095137 3.50665461,5.25095137 C3.57769359,5.27495137 3.55697389,5.20395137 3.56190715,5.16695137 C3.5796669,5.04995137 3.68425207,5.14695137 3.68622537,5.20795137 C3.7168116,5.13795137 3.79278385,5.21595137 3.75825101,5.27595137 C3.74147791,5.30495137 3.71878491,5.29395137 3.73950461,5.33195137 C3.7543044,5.35895137 3.77601075,5.35995137 3.80758363,5.35295137 C3.81547685,5.33695137 3.82238342,5.31895137 3.82238342,5.29995137 C3.87664931,5.28295137 3.9121688,5.34795137 3.88059592,5.38695137 C3.92104868,5.36495137 3.96248808,5.34395137 4.00590079,5.33295137 C3.98024783,5.24295137 3.95360821,5.15495137 3.9703813,5.05895137 C3.97432791,5.03795137 3.97728787,5.01395137 3.99307431,4.99795137 C4.01280736,4.97695137 3.98814105,4.98495137 3.98616774,4.97095137 C3.98024783,4.92895137 4.02464719,4.88595137 4.04142028,4.84795137 C3.99504762,4.83795137 4.03747367,4.74595137 4.0680599,4.72995137 C4.10160609,4.71295137 4.20027134,4.74095137 4.20717791,4.71395137 C4.22691096,4.72495137 4.24565736,4.74095137 4.26933702,4.74095137 C4.32360291,4.74195137 4.36010905,4.74295137 4.39760185,4.78695137 C4.41634825,4.80995137 4.44397452,4.86095137 4.47752071,4.86495137 C4.47653405,4.90295137 4.51994676,4.93095137 4.47456075,4.96295137 C4.43904126,4.98795137 4.38970863,4.98195137 4.37490884,5.02995137 C4.36504232,5.05995137 4.33642939,5.07395137 4.3798421,5.09495137 C4.3985885,5.10495137 4.42226816,5.10695137 4.44298787,5.10695137 C4.44792113,5.13595137 4.46272092,5.17495137 4.50021371,5.16995137 C4.573226,5.16095137 4.58901244,5.06895137 4.64722494,5.03795137 C4.74194358,4.98795137 4.7271438,5.20395137 4.80903596,5.14995137 C4.82876901,5.13695137 4.82876901,5.08195137 4.83863553,5.06095137 C4.85836858,5.01695137 4.88106159,4.97195137 4.90967452,4.93295137 C4.94618066,4.88295137 4.99156668,4.83095137 4.97578024,4.76595137 C4.96690036,4.72995137 4.89783469,4.71495137 4.8662618,4.68995137 C4.82876901,4.65895137 4.79226286,4.62595137 4.76956986,4.58295137 C4.75575672,4.55695137 4.7478635,4.54795137 4.76956986,4.53495137 C4.78239634,4.52795137 4.77844973,4.51395137 4.77351647,4.50395137 C4.74983681,4.45395137 4.68570439,4.36495137 4.77548977,4.33395137 C4.79522282,4.32695137 4.83666223,4.26295137 4.83962219,4.23795137 C4.84455545,4.19595137 4.78140969,4.15795137 4.81002261,4.11595137 C4.83074231,4.08495137 4.8830349,4.06495137 4.90967452,4.03395137 C4.922501,4.01895137 4.93730079,4.00595137 4.95703384,4.00195137 C4.95802049,3.98495137 4.9619671,3.96595137 4.97676689,3.95495137 C5.00044655,3.93695137 5.03793935,3.94595137 5.06556562,3.93695137 C5.11095163,3.92295137 5.13068468,3.87595137 5.16620418,3.84995137 C5.19580375,3.82795137 5.22934994,3.83595137 5.26092282,3.81995137 C5.27769591,3.81195137 5.28460248,3.79395137 5.30137557,3.78595137 C5.34281498,3.76595137 5.3901743,3.79795137 5.4089207,3.83295137 C5.45332006,3.91695137 5.5085726,4.04695137 5.63486413,4.01295137 C5.68617006,3.99895137 5.72464951,3.95695137 5.74043595,3.90895137 C5.75523574,3.86295137 5.73747599,3.82495137 5.74043595,3.77995137 C5.74438256,3.69995137 5.82232811,3.64895137 5.83120798,3.56995137 C5.77200883,3.57095137 5.80259506,3.53395137 5.78286201,3.49995137 C5.76115565,3.46195137 5.71182303,3.48995137 5.67926349,3.48395137 C5.71280968,3.40295137 5.71280968,3.37495137 5.63387748,3.33595137 C5.59934464,3.31895137 5.54211879,3.23895137 5.51547917,3.24195137 C5.53718553,3.21195137 5.58849146,3.26195137 5.6042779,3.27595137 C5.63881074,3.30895137 5.66939697,3.32395137 5.71774294,3.32795137 C5.70392981,3.30695137 5.69702324,3.26895137 5.70590311,3.24495137 C5.71478298,3.22295137 5.69307663,3.19995137 5.69504993,3.17195137 C5.75030248,3.24295137 5.7414226,3.32395137 5.77299548,3.40195137 C5.78582197,3.43495137 5.8183815,3.45695137 5.83219464,3.49095137 C5.84995438,3.53395137 5.83811455,3.53295137 5.87560735,3.55895137 C5.89830036,3.57495137 5.90619358,3.60295137 5.91014019,3.62795137 C5.91704675,3.67195137 5.9328332,3.65295137 5.95651286,3.67795137 C5.97032599,3.69295137 6.00584548,3.69495137 5.99893892,3.72595137 C5.99400565,3.74795137 5.97920586,3.76595137 5.97624591,3.78895137 C5.96736603,3.85495137 6.09661752,3.76495137 6.109444,3.75595137 C6.13707027,3.73495137 6.18245629,3.73095137 6.20416264,3.70595137 C6.22685565,3.67995137 6.22192239,3.64195137 6.24560205,3.61795137 C6.27520162,3.58695137 6.30381455,3.60795137 6.33933404,3.60195137 C6.38077345,3.59595137 6.41629294,3.56295137 6.44687917,3.53795137 C6.51199823,3.48295137 6.55343764,3.42295137 6.60770353,3.35995137 C6.58402387,3.36595137 6.50311836,3.42495137 6.4981851,3.36995137 C6.46759887,3.36995137 6.39655989,3.36495137 6.38570671,3.33095137 C6.37682684,3.30595137 6.37978679,3.27795137 6.37978679,3.25295137 C6.37880014,3.22595137 6.34624061,3.23495137 6.32453425,3.22095137 C6.28112154,3.19295137 6.25941518,3.14095137 6.21304252,3.11695137 C6.13904358,3.07795137 6.09464421,3.01495137 6.05024485,2.94795137 C6.02459188,2.90895137 5.93381985,2.82995137 5.94072642,2.78295137 C5.94467303,2.75195137 5.97032599,2.71895137 5.96835269,2.68795137 C5.96736603,2.65995137 5.94565968,2.64495137 5.94861964,2.61395137 C5.95157959,2.57795137 5.86475417,2.51495137 5.94072642,2.50795137 C5.96440608,2.50595137 5.96835269,2.47695137 5.9949923,2.46095137 C6.02459188,2.44295137 6.01768531,2.42695137 6.05024485,2.43595137 C6.10253743,2.45195137 6.13904358,2.39395137 6.17456307,2.36295137 C6.23573552,2.30895137 6.13805692,2.30795137 6.13312366,2.26695137 C6.1281904,2.22595137 6.10451074,2.19595137 6.09760417,2.14795137 C6.09365756,2.11295137 6.06109802,2.12695137 6.04235163,2.13595137 C6.01669866,2.14795137 5.99104569,2.12995137 5.96637938,2.12495137 C5.94368637,2.11995137 5.92493997,2.08195137 5.8973137,2.09395137 C5.876594,2.10395137 5.87758065,2.12895137 5.84798108,2.12595137 C5.82627472,2.12395137 5.81246159,2.10295137 5.79075523,2.09895137 C5.75720904,2.09495137 5.78680862,2.12695137 5.74931582,2.12995137 C5.72267621,2.13195137 5.63683743,2.09595137 5.63486413,2.12995137 C5.60822451,2.08395137 5.59737133,2.16195137 5.56875841,2.16995137 C5.53718553,2.17895137 5.50363934,2.17095137 5.47206646,2.18295137 C5.40300078,2.21095137 5.42569379,2.27995137 5.49179951,2.29095137 C5.54507875,2.29895137 5.47601307,2.33595137 5.49377282,2.37195137 C5.50955926,2.40395137 5.51449252,2.42595137 5.54902536,2.43895137 C5.60625121,2.45995137 5.66742366,2.47695137 5.64769061,2.55095137 C5.62203765,2.64295137 5.55790523,2.72995137 5.46811985,2.77195137 C5.38228108,2.81195137 5.35860142,2.70295137 5.29348235,2.67495137 C5.2530296,2.65795137 5.20764358,2.66395137 5.16521752,2.66895137 C5.15831095,2.67995137 5.22441667,2.70095137 5.23526985,2.71995137 C5.2550029,2.75895137 5.20073701,2.75395137 5.1967904,2.78395137 C5.19284379,2.80895137 5.16028426,2.82695137 5.17804401,2.85195137 C5.15929761,2.82895137 5.12279146,2.85995137 5.10996498,2.87395137 C5.09121858,2.89395137 5.09516519,2.90695137 5.10305841,2.93195137 C5.11884485,2.98295137 5.04188596,3.03595137 4.99649994,3.02995137 C4.95802049,3.02395137 4.92151435,3.02695137 4.8850082,3.00895137 C4.84159549,2.98795137 4.85639528,3.00095137 4.84751541,2.95195137 C4.83863553,2.90595137 4.77548977,2.88595137 4.81298257,2.82895137 C4.83962219,2.78695137 4.8267957,2.79095137 4.82186244,2.75095137 C4.81594253,2.70895137 4.83468892,2.70295137 4.86823511,2.69695137 C4.90474125,2.68995137 4.92052769,2.62495137 4.94223405,2.59395137 C4.94716731,2.58695137 4.96986032,2.52895137 4.93434083,2.54395137 C4.91460778,2.55295137 4.92940757,2.57795137 4.89882134,2.58195137 C4.87711498,2.58595137 4.85540863,2.57095137 4.83271562,2.57095137 C4.80706265,2.57095137 4.78042303,2.58395137 4.75674337,2.56795137 C4.7685832,2.55395137 4.85343532,2.48395137 4.78634295,2.46995137 C4.75970333,2.46395137 4.78140969,2.50795137 4.7458902,2.50195137 C4.73898363,2.53695137 4.69655757,2.53395137 4.67583787,2.55595137 C4.68471774,2.51895137 4.76266329,2.49095137 4.73701032,2.46095137 C4.79324952,2.41195137 4.80508935,2.40295137 4.7291171,2.37595137 C4.60973215,2.33395137 4.61861202,2.21095137 4.70050418,2.13695137 C4.77548977,2.06895137 4.89882134,1.98295137 4.97183363,2.09595137 C5.04977918,2.21695137 5.0991118,2.12895137 5.16324422,2.05095137 C5.14153786,2.04195137 5.16127091,2.03595137 5.15436434,2.00895137 C5.08332536,2.03795137 5.0201796,1.94595137 5.06852557,1.89095137 C5.09812515,1.85795137 5.14351117,1.86695137 5.18297727,1.85695137 C5.21751011,1.84795137 5.24908299,1.81395137 5.26388278,1.78195137 C5.2342832,1.78995137 5.23822981,1.77195137 5.24908299,1.75895137 C5.23132324,1.75695137 5.21159019,1.74895137 5.1967904,1.74395137 C5.15436434,1.72895137 5.1573243,1.69595137 5.11193829,1.68995137 C5.00439316,1.67395137 5.22441667,1.54995137 5.11687155,1.54995137 C5.08233871,1.54895137 5.05175248,1.49695137 5.02609952,1.50695137 C5.00833977,1.51395137 5.00340651,1.52795137 4.98170015,1.51895137 C4.96690036,1.51295137 4.94914062,1.49995137 4.93138087,1.50995137 C4.89290142,1.53395137 4.8850082,1.50495137 4.84751541,1.51595137 C4.81692918,1.52595137 4.80015608,1.55595137 4.76463659,1.54795137 C4.80015608,1.49995137 4.8435688,1.45995137 4.87514168,1.40995137 C4.89586138,1.37595137 4.92151435,1.34495137 4.95604719,1.32395137 C4.97479358,1.31295137 5.02807282,1.30195137 5.03103278,1.27595137 C5.03596604,1.23295137 5.00932642,1.23695137 4.97972685,1.25395137 C4.90276795,1.29995137 4.82284909,1.34895137 4.7478635,1.39795137 C4.70247748,1.42695137 4.66695799,1.45195137 4.6107188,1.44395137 C4.56730609,1.43695137 4.54954634,1.48495137 4.5150135,1.48095137 C4.49824041,1.41395137 4.12824571,1.65695137 4.08285969,1.67795137 C4.01083406,1.70995137 3.92992855,1.76495137 3.85296965,1.78395137 C3.82139677,1.79195137 3.75529105,1.86595137 3.75923766,1.78395137 C3.71977156,1.77895137 3.69017198,1.81895137 3.66353236,1.83895137 C3.62603957,1.86795137 3.5816402,1.88595137 3.54118745,1.91095137 C3.45436203,1.96695137 3.37246987,2.03395137 3.29156436,2.09695137 C3.21460546,2.15695137 3.13764656,2.22695137 3.05674105,2.28095137 C3.02911478,2.29995137 2.92748957,2.35195137 2.93044953,2.39095137 C3.00247516,2.40495137 3.24815165,2.09695137 3.31721732,2.17995137 C3.33497707,2.20095137 3.21263216,2.26295137 3.1928991,2.27495137 C3.17612601,2.28395137 3.15639296,2.28295137 3.13961987,2.29195137 C3.11791351,2.30495137 3.10410038,2.32695137 3.08338067,2.34095137 C3.02812813,2.37595137 2.98175546,2.42095137 2.94130271,2.47095137 C2.91268978,2.50795137 2.89197008,2.55595137 2.8603972,2.58995137 C2.86533046,2.55395137 2.85842389,2.52795137 2.85941055,2.49295137 C2.81895779,2.51895137 2.8021847,2.56295137 2.74594551,2.55095137 C2.69463957,2.53895137 2.65418682,2.59095137 2.61768068,2.61895137 C2.53282856,2.68395137 2.47560271,2.75595137 2.40456373,2.83195137 C2.36509763,2.87495137 2.32267157,2.90495137 2.29800525,2.95795137 C2.27136564,3.01495137 2.23387284,3.06595137 2.19934,3.11895137 C2.13323428,3.21595137 2.05726204,3.30495137 1.99214297,3.40195137 C1.85894488,3.60095137 1.7711328,3.82895137 1.66161437,4.04095137 C1.60537517,4.15095137 1.55110929,4.25895137 1.52841628,4.38195137 C1.50868323,4.48795137 1.50769657,4.59595137 1.50966988,4.70395137 C1.56985568,4.65695137 1.56689573,4.75495137 1.55110929,4.78395137 C1.52841628,4.82895137 1.5195364,4.87995137 1.51262984,4.92995137 C1.50276331,4.99495137 1.49092348,5.05995137 1.49092348,5.12595137 C1.49092348,5.18195137 1.47316374,5.23395137 1.47217708,5.28795137 C1.45145738,5.27195137 1.49585674,5.20395137 1.45639064,5.21395137 C1.43665759,5.21895137 1.43567094,5.24795137 1.43073768,5.26295137 C1.41495124,5.31495137 1.34489891,5.30995137 1.33404573,5.36895137 C1.32812581,5.40495137 1.3241792,5.42595137 1.30049954,5.45495137 C1.28175314,5.47695137 1.29951289,5.48695137 1.3034595,5.50895137 C1.31233937,5.56095137 1.245247,5.63295137 1.26300675,5.67495137 C1.27977984,5.71595137 1.26794001,5.76195137 1.28668641,5.80095137 C1.29655293,5.82095137 1.31924594,5.84695137 1.31036607,5.87195137 C1.26794001,5.87995137 1.3222059,5.97795137 1.32615251,6.00795137 C1.33207242,6.05695137 1.37548513,6.21095137 1.42284446,6.23295137 C1.48204361,6.32395137 1.56294912,6.45095137 1.66753428,6.49695137 C1.74153322,6.52895137 1.76817284,6.43295137 1.80961225,6.39295137 C1.86190483,6.34095137 1.92998386,6.30795137 1.99904954,6.28395137 C2.05726204,6.26295137 2.30096521,6.19195137 2.18355356,6.10795137 L2.18355356,6.10795137 Z M2.28616542,9.39295137 C2.29800525,9.37295137 2.28912538,9.32195137 2.26741903,9.30495137 C2.21512644,9.26095137 2.19440674,9.36495137 2.22795292,9.39595137 C2.24077941,9.42895137 2.27136564,9.41795137 2.28616542,9.39295137 L2.28616542,9.39295137 Z M2.50026902,6.36895137 C2.48546924,6.35595137 2.47461606,6.36395137 2.47362941,6.33695137 C2.47461606,6.31295137 2.47658936,6.26695137 2.44501648,6.29595137 C2.43613661,6.29895137 2.44797644,6.30395137 2.43514996,6.30895137 C2.42627008,6.31195137 2.41936352,6.30495137 2.4134436,6.30195137 C2.39667051,6.29495137 2.38680398,6.29395137 2.3739775,6.31195137 C2.36509763,6.32395137 2.36509763,6.33795137 2.35029784,6.34795137 L2.32464487,6.35695137 C2.315765,6.35995137 2.29011203,6.37795137 2.28912538,6.38795137 C2.28517877,6.40295137 2.30787178,6.41395137 2.32365822,6.41795137 C2.3364847,6.42695137 2.3532578,6.43495137 2.36608428,6.44395137 C2.37891076,6.45295137 2.39963047,6.46895137 2.41541691,6.47295137 C2.4509364,6.49295137 2.50618894,6.51495137 2.53381521,6.47295137 C2.54170843,6.45695137 2.54762835,6.44495137 2.53677517,6.43095137 C2.52690864,6.41595137 2.5111222,6.41195137 2.50618894,6.39995137 C2.50224233,6.38695137 2.51309551,6.37895137 2.50026902,6.36895137 L2.50026902,6.36895137 Z M7.24508107,7.12395137 C7.22633467,7.12495137 7.19278848,7.13695137 7.17798869,7.14995137 C7.14838912,7.17595137 7.21153488,7.19095137 7.23620119,7.19795137 C7.26382747,7.21395137 7.30329357,7.22195137 7.32993319,7.23795137 C7.35262619,7.25495137 7.36841263,7.27795137 7.3940656,7.28895137 C7.42563848,7.30395137 7.46905119,7.31095137 7.50358403,7.31995137 C7.51838382,7.32495137 7.54107683,7.32395137 7.56080988,7.32795137 C7.58251623,7.34095137 7.59238276,7.36095137 7.61014251,7.37495137 C7.64072873,7.40295137 7.68414145,7.40995137 7.7245942,7.40795137 C7.76307365,7.41195137 7.79168657,7.41895137 7.82621941,7.40995137 C7.86568551,7.39995137 7.89331178,7.41995137 7.92981793,7.41995137 C7.94461771,7.41995137 7.9594175,7.40795137 7.97323064,7.40895137 C7.99197704,7.40895137 7.99395034,7.41695137 8.00283021,7.43295137 C8.01861666,7.45595137 8.05906941,7.49095137 8.08768233,7.49195137 C8.10544208,7.49195137 8.11925521,7.48895137 8.134055,7.49395137 C8.15082809,7.50395137 8.15773466,7.50395137 8.16957449,7.51395137 C8.1902942,7.52295137 8.20805394,7.52895137 8.21693381,7.54495137 C8.23272026,7.57295137 8.2317336,7.60395137 8.25639992,7.62595137 C8.27317301,7.63895137 8.29093275,7.65295137 8.3086925,7.66595137 C8.32053233,7.67695137 8.31066581,7.67495137 8.32842555,7.67495137 C8.33829208,7.67695137 8.35703847,7.67695137 8.36986496,7.67295137 C8.41919758,7.66995137 8.39255797,7.59995137 8.37677153,7.57695137 C8.366905,7.55695137 8.35802513,7.54095137 8.36197174,7.52195137 C8.36493169,7.49895137 8.37578487,7.48295137 8.36098508,7.46395137 C8.35309186,7.45195137 8.34223869,7.44595137 8.33138551,7.43995137 C8.32546559,7.43195137 8.32250564,7.42395137 8.31559907,7.41195137 C8.30079928,7.39295137 8.27218636,7.38695137 8.25343996,7.36895137 C8.22186708,7.33695137 8.20509398,7.29095137 8.16464123,7.26095137 C8.14293487,7.24795137 8.12320182,7.25795137 8.09656221,7.24695137 C8.08570903,7.23995137 8.07978911,7.23295137 8.06400267,7.22795137 C8.04920288,7.22295137 8.0363764,7.22595137 8.02256327,7.22495137 C7.99395034,7.22295137 7.96928403,7.19795137 7.94165776,7.19995137 C7.91107153,7.20395137 7.90515161,7.23695137 7.88739187,7.25495137 C7.87160543,7.26795137 7.85384568,7.26795137 7.84792577,7.24695137 C7.84595246,7.21995137 7.85581899,7.20395137 7.86963212,7.18695137 C7.89133848,7.16395137 7.86963212,7.15095137 7.8410192,7.14895137 C7.80451305,7.14895137 7.79760649,7.17795137 7.7828067,7.20895137 C7.75912704,7.24195137 7.74432725,7.21895137 7.71078106,7.21395137 C7.68808806,7.21495137 7.67230162,7.22395137 7.65059526,7.21495137 C7.63579547,7.20995137 7.63283551,7.19795137 7.62198234,7.19095137 C7.60520924,7.18195137 7.59238276,7.18495137 7.58054293,7.19295137 C7.56376984,7.19695137 7.56376984,7.19695137 7.54699674,7.18795137 C7.53219696,7.18195137 7.52825034,7.16995137 7.50950395,7.16595137 C7.47990437,7.15995137 7.44931814,7.18495137 7.42465183,7.17795137 C7.41379865,7.17195137 7.40491878,7.15595137 7.39011899,7.15095137 C7.3733459,7.14095137 7.37630585,7.14995137 7.36545268,7.16195137 C7.34670628,7.17995137 7.32105331,7.18595137 7.30329357,7.17195137 C7.28060056,7.15595137 7.27862725,7.12895137 7.24508107,7.12395137 L7.24508107,7.12395137 Z M8.37183826,8.30595137 C8.3876247,8.30395137 8.39551792,8.28795137 8.40933106,8.28995137 C8.4251175,8.28695137 8.41722428,8.30295137 8.42807746,8.31295137 C8.43794398,8.32195137 8.44781051,8.32195137 8.45767703,8.32195137 C8.47543678,8.32495137 8.50996962,8.32695137 8.51687619,8.31095137 C8.52476941,8.28595137 8.48333,8.28095137 8.47247682,8.26095137 C8.4626103,8.23195137 8.4853033,8.20395137 8.49319652,8.17895137 C8.50503635,8.14495137 8.4626103,8.12995137 8.46655691,8.10295137 C8.46557025,8.07395137 8.4853033,8.06395137 8.47938339,8.03595137 C8.47445013,8.01495137 8.45669038,7.99195137 8.4438639,7.97695137 C8.43202407,7.96095137 8.40933106,7.94595137 8.41130436,7.92295137 C8.41327767,7.89895137 8.45669038,7.89995137 8.43597068,7.87095137 C8.42413085,7.84595137 8.39255797,7.85095137 8.36394504,7.84695137 C8.35407852,7.84695137 8.34421199,7.84795137 8.33434547,7.83795137 C8.32546559,7.82395137 8.3294122,7.81695137 8.3294122,7.80695137 C8.32349229,7.77995137 8.30277259,7.76995137 8.27909292,7.75895137 C8.2711997,7.75495137 8.25935987,7.74995137 8.25442661,7.73795137 C8.25048,7.72595137 8.26231983,7.72195137 8.25837322,7.71095137 C8.24554674,7.68495137 8.19818742,7.72095137 8.17845437,7.71195137 C8.16464123,7.70995137 8.16661454,7.69695137 8.15773466,7.68295137 L8.134055,7.67195137 C8.10149547,7.65695137 8.08866899,7.68395137 8.0945889,7.71095137 C8.10938869,7.77195137 8.15378805,7.81195137 8.14885479,7.87295137 C8.15181475,7.89795137 8.15576136,7.90995137 8.16464123,7.93295137 C8.17253445,7.96595137 8.18141432,7.98195137 8.16661454,8.01395137 C8.14293487,8.03195137 8.16464123,8.05395137 8.17253445,8.07695137 C8.17746771,8.10795137 8.18536093,8.13195137 8.18437428,8.16495137 C8.17845437,8.22495137 8.15970797,8.28395137 8.16464123,8.34495137 C8.16760119,8.36995137 8.16562788,8.39295137 8.17450776,8.41695137 C8.17845437,8.44795137 8.20312068,8.45895137 8.22877365,8.47595137 C8.25343996,8.49695137 8.36789165,8.56595137 8.33434547,8.48195137 C8.32447894,8.46295137 8.3086925,8.43595137 8.30375924,8.41395137 C8.29586602,8.39095137 8.32349229,8.37495137 8.32447894,8.35095137 C8.32842555,8.32395137 8.30770585,8.31495137 8.3461853,8.30795137 C8.35407852,8.30195137 8.36591835,8.30795137 8.37183826,8.30595137 L8.37183826,8.30595137 Z M7.1819353,1.09995137 C7.21252153,1.09295137 7.24310776,1.10195137 7.27172069,1.09095137 C7.28652047,1.08495137 7.33486645,1.06795137 7.33190649,1.04795137 C7.32697323,1.01095137 7.17009547,1.03495137 7.14444251,1.04595137 C7.13654929,1.06895137 7.16022895,1.08695137 7.18094865,1.09295137 C7.18094865,1.09495137 7.1819353,1.09795137 7.1819353,1.09995137 L7.1819353,1.09995137 Z M7.93573784,7.78795137 C7.92981793,7.77495137 7.93573784,7.76295137 7.93573784,7.74995137 C7.93277788,7.72895137 7.92685797,7.72295137 7.92981793,7.70095137 C7.93672449,7.68895137 7.93672449,7.66995137 7.93376454,7.65495137 C7.92784462,7.64295137 7.9179781,7.63295137 7.90909822,7.62395137 C7.90909822,7.61795137 7.90613827,7.60795137 7.8992317,7.60195137 C7.88739187,7.58995137 7.87456538,7.60795137 7.8617389,7.61395137 C7.85187238,7.62295137 7.83312598,7.62895137 7.83016602,7.63895137 C7.8202995,7.65395137 7.82621941,7.66595137 7.82621941,7.67895137 L7.82917937,7.69395137 C7.80747301,7.71595137 7.82819272,7.77395137 7.82523276,7.79595137 C7.82523276,7.82095137 7.78971327,7.89095137 7.83707259,7.86995137 C7.84989907,7.86395137 7.8597656,7.85495137 7.87160543,7.84895137 C7.88739187,7.83995137 7.90712492,7.83995137 7.92587132,7.83395137 C7.93179123,7.83395137 7.96632407,7.83095137 7.96632407,7.82495137 C7.96731072,7.81195137 7.9386978,7.80295137 7.93573784,7.78795137 L7.93573784,7.78795137 Z M7.0447906,9.05195137 C7.0842567,9.07095137 7.15332238,9.03295137 7.19081518,9.02095137 C7.2381745,9.00595137 7.31316009,8.95595137 7.36150607,8.98395137 C7.38123912,8.99495137 7.39110564,9.01795137 7.41182535,9.02695137 C7.43747831,9.03795137 7.46806454,9.02795137 7.49371751,9.02195137 C7.52035712,9.01595137 7.55094335,9.01195137 7.57560967,8.99995137 C7.59731602,8.98895137 7.61112916,8.97095137 7.62987556,8.95695137 C7.67822153,8.91995137 7.71966094,8.95495137 7.77294017,8.94695137 C7.8035264,8.94295137 7.83213933,8.92795137 7.8617389,8.91995137 C7.88344526,8.91495137 7.92192471,8.91495137 7.9386978,8.89895137 C7.9574442,8.88095137 7.94856432,8.84195137 7.94856432,8.81895137 C7.94757767,8.78795137 7.94955098,8.75595137 7.9386978,8.72695137 C7.91699144,8.66995137 7.83805924,8.60295137 7.9199514,8.55795137 C7.93573784,8.45795137 7.81931284,8.47495137 7.78576666,8.40295137 C7.7640603,8.35595137 7.75715373,8.31995137 7.69499462,8.31495137 C7.64270204,8.30995137 7.61112916,8.33795137 7.56574314,8.35595137 C7.51443721,8.37495137 7.47497111,8.35795137 7.43057174,8.33295137 C7.40393213,8.31795137 7.34769293,8.28295137 7.33683975,8.32895137 C7.32697323,8.36895137 7.36545268,8.40795137 7.3338798,8.44395137 C7.30625352,8.47595137 7.25790755,8.48995137 7.21844145,8.49895137 C7.13260268,8.51695137 7.06452365,8.58295137 7.00236454,8.63995137 L7.00927111,8.64595137 C6.9846048,8.64495137 6.94809865,8.71095137 6.947112,8.73095137 C6.95697853,8.73395137 6.9658584,8.73695137 6.97671158,8.73995137 C6.97572493,8.77395137 7.01420437,8.75095137 7.01716433,8.72695137 C7.02505755,8.72895137 7.03295077,8.73395137 7.04084399,8.73495137 C7.04775056,8.73695137 7.06255035,8.73595137 7.06847026,8.73895137 C7.08524336,8.74595137 7.08820331,8.76195137 7.10892302,8.76395137 C7.09708319,8.81595137 7.10793636,8.87095137 7.08327005,8.91995137 C7.06748361,8.94995137 6.98756476,9.02395137 7.0447906,9.05195137 L7.0447906,9.05195137 Z M7.4522781,1.35995137 C7.48187768,1.39195137 7.51838382,1.40095137 7.51147725,1.45095137 C7.54897005,1.45595137 7.57264971,1.46995137 7.59435606,1.43795137 C7.6081692,1.41795137 7.6288889,1.40195137 7.65158191,1.39395137 C7.67920818,1.38295137 7.79267322,1.38395137 7.78773996,1.43095137 C7.78478,1.45395137 7.77096687,1.47395137 7.76702026,1.49695137 C7.762087,1.52895137 7.79661983,1.50595137 7.81141962,1.51395137 C7.79464653,1.52595137 7.77392683,1.53295137 7.75320712,1.53795137 C7.762087,1.54395137 7.76800691,1.55195137 7.76899356,1.56195137 C7.7433406,1.56795137 7.73051411,1.63995137 7.6851281,1.65395137 C7.65750183,1.66295137 7.61704907,1.64395137 7.5894228,1.64095137 C7.55686327,1.63695137 7.53219696,1.62695137 7.49963742,1.62495137 C7.46806454,1.62295137 7.49371751,1.58095137 7.4542514,1.58895137 C7.44734484,1.61695137 7.46115797,1.68795137 7.46609124,1.71595137 C7.4710245,1.75095137 7.50062407,1.77095137 7.53515691,1.77695137 C7.58350289,1.78495137 7.6061959,1.80095137 7.6476353,1.82495137 C7.68019484,1.84295137 7.71670098,1.83195137 7.75222047,1.83495137 C7.77590013,1.83695137 7.79563318,1.84595137 7.81339293,1.86095137 C7.80944632,1.87195137 7.80056644,1.88995137 7.80648636,1.90195137 C7.81339293,1.91795137 7.86371221,1.89995137 7.87555204,1.89895137 C7.91107153,1.89495137 7.94461771,1.85595137 7.9781639,1.86095137 C7.99099038,1.86295137 8.05018954,1.88095137 8.04722958,1.89595137 C8.0156567,1.88295137 7.99493699,1.92195137 7.97027068,1.90095137 C7.94856432,1.88195137 7.89035183,1.89795137 7.92784462,1.92495137 C7.93080458,1.92795137 7.9406711,2.00295137 7.9406711,2.01095137 C7.93771115,2.03895137 7.88739187,2.06895137 7.89133848,2.08695137 C7.89824505,2.08795137 7.94363106,2.09095137 7.95349759,2.09995137 C7.95645755,2.08795137 7.94757767,2.08295137 7.97520394,2.07495137 C7.99592365,2.06895137 8.02058996,2.06695137 8.04130966,2.07695137 C8.04920288,2.11195137 8.02354992,2.14795137 8.07189589,2.13895137 C8.11728191,2.12995137 8.13701496,2.15995137 8.18437428,2.12895137 C8.21397386,2.11095137 8.24554674,2.11395137 8.2711997,2.13995137 C8.30573254,2.17395137 8.23568021,2.22095137 8.27613297,2.25395137 C8.29191941,2.26695137 8.30474589,2.30695137 8.32053233,2.31395137 C8.33138551,2.31895137 8.39058466,2.29795137 8.40143784,2.29295137 C8.42018424,2.32695137 8.43695733,2.27495137 8.45175712,2.27195137 C8.45767703,2.24995137 8.4853033,2.22495137 8.51194292,2.22195137 C8.55140902,2.21795137 8.55239568,2.22495137 8.5790353,2.24395137 C8.65698085,2.29795137 8.64612767,2.16595137 8.68658042,2.13195137 C8.75959271,2.07195137 8.79609885,2.01495137 8.84641813,1.93795137 C8.88588423,1.87595137 8.94113678,1.86095137 9.01118911,1.84995137 C9.06644165,1.84095137 9.15129377,1.82795137 9.17398677,1.76795137 C9.20062639,1.69795137 9.13649398,1.65995137 9.08025478,1.63895137 C9.01710902,1.61695137 8.94607004,1.59295137 8.97369631,1.51295137 C9.00625584,1.41995137 8.97764292,1.36595137 8.87897767,1.33595137 C8.67079398,1.27095137 8.48333,1.16195137 8.2711997,1.10195137 C8.08373572,1.04895137 7.89429844,1.02995137 7.70190119,1.01995137 C7.61606242,0.98995137 7.43451835,0.98695137 7.38222577,1.05995137 C7.34867958,1.10695137 7.39110564,1.14795137 7.38715903,1.19695137 C7.38222577,1.25595137 7.41083869,1.31595137 7.4522781,1.35995137 L7.4522781,1.35995137 Z M10.7269779,10.6309514 L10.7259912,10.6299514 C10.7289512,10.6349514 10.7269779,10.6439514 10.7279645,10.6509514 C10.766444,10.6509514 10.7832171,10.6859514 10.8246565,10.6729514 C10.8670825,10.6609514 10.8917488,10.6199514 10.8582027,10.5859514 C10.8286031,10.5569514 10.8029501,10.5319514 10.7595374,10.5399514 C10.7082315,10.5499514 10.7190846,10.5909514 10.7269779,10.6309514 L10.7269779,10.6309514 Z M12.0678387,9.29395137 C12.0658654,9.28495137 12.0638921,9.27695137 12.0619187,9.26795137 C12.021466,9.25595137 11.995813,9.29795137 11.9583202,9.26695137 C11.8862946,9.31595137 11.9632535,9.41295137 11.8448552,9.40695137 C11.8655749,9.43195137 11.8636016,9.45995137 11.8537351,9.48895137 C11.8389353,9.53395137 11.8270954,9.52995137 11.7965092,9.53595137 C11.7323768,9.54595137 11.7017906,9.50595137 11.6820575,9.45195137 C11.6189118,9.45395137 11.5320863,9.55195137 11.4827537,9.58295137 C11.4699272,9.58995137 11.4472342,9.61095137 11.4334211,9.61995137 C11.4225679,9.62595137 11.3959283,9.63895137 11.3821151,9.64695137 C11.348569,9.66395137 11.2765433,9.68695137 11.2725967,9.72495137 C11.2558236,9.72195137 11.2301707,9.73195137 11.2133976,9.72995137 C11.2074776,9.73795137 11.2074776,9.74695137 11.2133976,9.75595137 C11.2903565,9.76895137 11.3308092,9.74295137 11.3959283,9.71495137 C11.4640073,9.68395137 11.5370196,9.69095137 11.601152,9.66695137 C11.6317382,9.65595137 11.6327249,9.62195137 11.6830442,9.64195137 C11.7047505,9.65195137 11.7304035,9.68395137 11.7353368,9.70595137 C11.7452033,9.75595137 11.6929107,9.82995137 11.6406181,9.83295137 C11.6277916,9.80195137 11.646538,9.76995137 11.6524579,9.74495137 C11.5833923,9.72195137 11.4699272,9.81995137 11.4511808,9.87795137 C11.5222198,9.89295137 11.5518194,9.99695137 11.5133399,10.0539514 C11.5005135,10.0679514 11.4857137,10.0859514 11.4610474,10.0939514 C11.4205946,10.1059514 11.4018482,10.0689514 11.3623821,10.0979514 C11.3110762,10.1369514 11.3673154,10.2439514 11.3377158,10.3039514 C11.3150228,10.3499514 11.2765433,10.3669514 11.2439838,10.3989514 C11.2222774,10.4219514 11.209451,10.4469514 11.1798514,10.4669514 C11.1413719,10.4929514 11.0476399,10.5489514 11.0555332,10.6029514 C11.1403853,10.6319514 11.3160094,10.4839514 11.3890217,10.4349514 C11.4353944,10.4039514 11.4640073,10.3559514 11.5113666,10.3249514 C11.5646459,10.2919514 11.6346982,10.2749514 11.669231,10.2159514 C11.6889641,10.1819514 11.6731776,10.1519514 11.6850175,10.1179514 C11.6958707,10.0879514 11.7165904,10.0779514 11.7363234,10.0549514 C11.7728296,10.0109514 11.8063757,9.99695137 11.8478151,9.96095137 C11.8991211,9.91495137 11.8872812,9.84295137 11.9109609,9.78195137 C11.9316806,9.72895137 11.9721334,9.68795137 12.0007463,9.63795137 C12.0451457,9.55895137 12.1615707,9.37095137 12.112238,9.28195137 C12.1003982,9.29195137 12.0786918,9.28895137 12.0678387,9.29395137 L12.0678387,9.29395137 Z M13.0752109,6.73495137 C13.0495579,6.68695137 13.0880374,6.54895137 13.0880374,6.49195137 C13.0870507,6.38695137 13.0554778,6.30795137 13.0406781,6.20995137 C13.0317982,6.11795137 13.0189717,5.87395137 13.0525179,5.79095137 C13.0998772,5.67395137 12.8690005,5.47595137 12.856174,5.34895137 C12.8443342,5.23895137 12.7821751,5.13495137 12.6923897,5.07195137 C12.6558836,5.04495137 12.5769514,4.68195137 12.5305787,4.69895137 C12.5078857,4.70995137 12.555245,4.78995137 12.5522851,4.81495137 C12.5394586,4.90295137 12.4950592,4.81495137 12.4486865,4.83495137 C12.3628478,4.86995137 12.2720757,4.95295137 12.2612226,5.03795137 C12.2207698,5.35295137 11.9977863,5.02695137 12.0155461,5.01395137 C12.0648787,4.97595137 12.0826384,4.98795137 12.1408509,4.97995137 C12.2049834,4.95695137 12.1053315,4.91095137 12.20597,4.90095137 C12.1822904,4.83595137 12.2355696,4.81495137 12.2099166,4.76395137 C12.1714372,4.68895137 12.1438109,4.69795137 12.1822904,4.61695137 C12.1990634,4.57295137 12.0984249,4.43395137 12.0905317,4.38095137 C12.0826384,4.32895137 12.0816518,4.26095137 12.0747452,4.20295137 C12.0707986,4.16595137 12.1309844,4.13095137 12.1201312,4.10195137 C12.1181579,3.99895137 12.1408509,3.88795137 12.1043448,3.78795137 C12.0786918,3.71995137 12.0490923,3.62995137 12.0056796,3.57195137 C11.9908798,3.55195137 11.9445071,3.44895137 11.9395738,3.41995137 C11.927734,3.35595137 11.8991211,3.37995137 11.8636016,3.35495137 C11.8438685,3.32995137 11.7550698,3.24695137 11.7294168,3.23495137 C11.7057372,3.22395137 11.5340596,3.06695137 11.530113,3.05395137 C11.5153132,3.00895137 11.4186213,2.97395137 11.4294745,2.92495137 C11.4452609,2.85095137 11.1877446,2.65895137 11.115719,2.64595137 C11.0693463,2.63795137 11.2577969,2.86395137 11.2568103,2.85895137 C11.2597702,2.87195137 11.3781685,3.02295137 11.3781685,3.02295137 C11.4048082,3.03195137 11.4699272,3.21695137 11.4679539,3.24095137 C11.4610474,3.31095137 11.2804899,3.12595137 11.2666768,3.10095137 C11.1778781,2.99195137 11.0170537,2.90395137 10.9154285,2.83095137 C10.8434029,2.76395137 10.8789224,2.72595137 10.7555908,2.66895137 C10.7102048,2.64795137 10.5868732,2.54695137 10.5483938,2.54395137 C10.5020211,2.54195137 10.5553003,2.63995137 10.556287,2.65095137 C10.5631935,2.72095137 10.6391658,2.72595137 10.6845518,2.77195137 C10.7210579,2.80995137 10.7536175,2.85695137 10.7220446,2.89895137 C10.7210579,2.89895137 10.6648188,3.00295137 10.6618588,2.99395137 C10.6776452,3.03795137 10.80887,3.13495137 10.8414296,3.17095137 C10.8355096,3.16195137 11.0131071,3.39495137 11.0279069,3.27095137 C11.0338268,3.22595137 10.9835075,3.17195137 10.9904141,3.13295137 C10.9953474,3.10895137 11.1936645,3.35995137 11.2045177,3.38195137 C11.2528637,3.51495137 11.2489171,3.36195137 11.2992363,3.37795137 C11.3406757,3.39095137 11.4521675,3.52995137 11.3594221,3.53595137 C11.2183308,3.54495137 11.3850751,3.66795137 11.4245412,3.68695137 C11.5064334,3.72695137 11.5626726,3.81995137 11.6475247,3.85495137 C11.7807228,3.90895137 11.7530965,4.00495137 11.8201889,4.10295137 C11.8418952,4.13395137 11.4373677,4.10295137 11.4057948,4.12095137 C11.3525156,4.16295137 11.6090452,4.44995137 11.6100319,4.49295137 C11.6120052,4.58295137 11.6633111,4.64895137 11.6771243,4.73895137 C11.6850175,4.82195137 11.675151,4.93095137 11.7294168,4.99795137 C11.7738162,5.03895137 11.8152556,4.92995137 11.8853079,4.99495137 C11.9109609,5.00695137 11.9474671,5.03595137 11.9553603,5.05795137 C11.9790399,5.11995137 12.1132247,5.49895137 11.9524003,5.47095137 C11.8813613,5.45795137 11.9218141,5.76895137 11.9267473,5.81395137 C11.9484537,5.91195137 11.9879198,5.90395137 11.9622668,6.02795137 C11.9652268,6.13095137 11.882348,6.18295137 11.8231488,6.25695137 C11.7955226,6.29095137 11.7777628,6.33095137 11.7649363,6.37395137 C11.7323768,6.34195137 11.7165904,6.29095137 11.6712043,6.27395137 C11.6218717,6.25495137 11.5133399,6.31495137 11.4699272,6.33595137 C11.3653421,6.38895137 11.442301,6.48495137 11.4008615,6.56795137 C11.371262,6.62895137 11.2824632,6.65895137 11.2242507,6.68895137 C11.1541984,6.72495137 11.0604664,6.76295137 10.9914007,6.70495137 C10.9322016,6.65695137 10.9578546,6.55995137 10.8956954,6.51795137 C10.8256431,6.47095137 10.8187366,6.57595137 10.8029501,6.61795137 C10.7723639,6.69695137 10.6806052,6.72395137 10.7042849,6.82295137 C10.7141514,6.86395137 10.7348711,6.90095137 10.7427643,6.94195137 C10.7526308,6.99295137 10.7269779,7.03895137 10.7240179,7.08995137 C10.718098,7.17695137 10.80887,7.19695137 10.8325497,7.26795137 C10.8532694,7.33195137 10.831563,7.43095137 10.7605241,7.45495137 C10.6845518,7.48195137 10.6006863,7.41295137 10.5257007,7.40495137 C10.4507152,7.39695137 10.3550099,7.41795137 10.3411967,7.50395137 C10.3283702,7.57995137 10.4053291,7.64195137 10.3678363,7.71995137 C10.3520499,7.75295137 10.3244236,7.77895137 10.3046906,7.80895137 C10.2701577,7.85895137 10.2504247,7.91695137 10.2178652,7.96795137 C10.2563446,7.96895137 10.252398,7.94495137 10.2869308,7.95195137 C10.323437,7.95995137 10.3559965,7.92295137 10.3865827,7.91095137 C10.3925027,7.93495137 10.3895427,7.95995137 10.3925027,7.98395137 C10.4181556,7.99195137 10.4438086,7.98195137 10.4665016,7.97295137 C10.4694616,7.99395137 10.459595,8.01795137 10.4684749,8.03895137 C10.4753815,8.05695137 10.4961012,8.06295137 10.507941,8.07695137 C10.5385272,8.11395137 10.5010344,8.17495137 10.4793281,8.20695137 C10.417169,8.29895137 10.3106105,8.34995137 10.2415448,8.43595137 C10.1764257,8.51595137 10.1705058,8.61295137 10.1221599,8.69995137 C10.1053868,8.72995137 10.0886137,8.77095137 10.133013,8.78495137 C10.1428796,8.76895137 10.1576794,8.75595137 10.1783991,8.75595137 C10.2089853,8.75495137 10.1971455,8.77795137 10.2129319,8.79595137 C10.2770643,8.87795137 10.3451433,8.74295137 10.3727696,8.70395137 C10.4003959,8.66195137 10.5148476,8.59895137 10.5464205,8.66895137 C10.5710868,8.72195137 10.5424738,8.79695137 10.5178075,8.84495137 C10.5592469,8.86395137 10.5474071,8.89395137 10.5572736,8.92995137 C10.5701001,8.97995137 10.6154861,9.01195137 10.6154861,9.06695137 C10.6154861,9.13295137 10.4714349,9.26395137 10.5276741,9.31395137 C10.5977264,9.37595137 10.6806052,9.20395137 10.7082315,9.16695137 C10.7605241,9.09595137 10.879909,9.08595137 10.9095086,8.99895137 C10.9420681,8.89995137 10.9312149,8.84095137 11.0624397,8.83795137 C11.1176923,8.83695137 11.158145,8.80195137 11.2104376,8.79095137 C11.2676635,8.77995137 11.2933164,8.77395137 11.3298226,8.72995137 C11.3821151,8.66695137 11.4294745,8.74195137 11.4314478,8.79195137 C11.4334211,8.84295137 11.4107281,8.90695137 11.442301,8.95295137 C11.4807804,9.00895137 11.5232065,8.93495137 11.5626726,8.89895137 C11.558726,8.93695137 11.6090452,8.95895137 11.6386448,8.97095137 C11.6840308,8.93995137 11.7126437,8.88895137 11.7609897,8.86095137 C11.7836827,8.84795137 11.8093357,8.84295137 11.8349887,8.83895137 C11.8418952,8.87995137 11.8488018,8.92395137 11.8853079,8.94395137 C11.9376005,8.97395137 11.8734681,9.00295137 11.9425338,9.03495137 C12.0283726,9.06795137 12.0569855,9.15495137 12.0984249,9.22495137 C12.1181579,9.25695137 12.2977287,9.06195137 12.3667944,9.05495137 C12.5956978,9.02895137 12.7150827,8.72995137 12.7999348,8.55295137 C12.9222798,8.29995137 12.9775323,8.01895137 13.0091052,7.75795137 C13.0870507,7.59695137 13.1186236,7.30195137 13.0870507,7.11495137 C13.0683043,7.00095137 13.1334234,6.84295137 13.0752109,6.73495137 L13.0752109,6.73495137 Z M11.0032406,10.5319514 C11.0091605,10.5039514 11.0683596,10.3999514 11.0131071,10.3849514 C10.993374,10.3799514 10.976601,10.4099514 10.9588412,10.4149514 C10.9351615,10.4229514 10.9095086,10.4079514 10.8878022,10.4189514 C10.8680692,10.4299514 10.8493228,10.4619514 10.8374829,10.4799514 C10.8226832,10.5019514 10.8286031,10.5109514 10.8522827,10.5229514 C10.8759624,10.5359514 10.9065486,10.5419514 10.9203618,10.5679514 C10.9322016,10.5909514 10.9262817,10.6219514 10.9233217,10.6459514 C10.9233217,10.6449514 10.9272683,10.6409514 10.928255,10.6369514 C10.9322016,10.6359514 10.9391082,10.6349514 10.9430548,10.6359514 L10.9381215,10.6459514 C11.0012673,10.6559514 10.996334,10.5729514 11.0032406,10.5319514 L11.0032406,10.5319514 Z M11.7422433,9.28095137 C11.7442166,9.31095137 11.7767762,9.30795137 11.7984825,9.29995137 C11.8182156,9.29395137 11.8310421,9.27695137 11.8438685,9.26195137 C11.8616283,9.23895137 11.8724815,9.21595137 11.856695,9.18895137 C11.8409086,9.16095137 11.8310421,9.14095137 11.8231488,9.10795137 C11.8103223,9.11495137 11.7945359,9.12695137 11.7807228,9.13095137 C11.7669096,9.13595137 11.7649363,9.13195137 11.7491499,9.13095137 C11.7126437,9.12995137 11.720537,9.15795137 11.7047505,9.18095137 C11.691924,9.20095137 11.6633111,9.20895137 11.6741643,9.23495137 C11.6820575,9.25495137 11.7146171,9.27195137 11.7333635,9.28095137 L11.7382967,9.27495137 C11.7373101,9.27695137 11.7363234,9.27795137 11.7353368,9.27995137 C11.7373101,9.28095137 11.74027,9.28095137 11.7422433,9.28095137 L11.7422433,9.28095137 Z M8.18042767,11.4279514 C8.21693381,11.3629514 8.28205288,11.3219514 8.34026538,11.2769514 C8.41031771,11.2229514 8.47247682,11.1599514 8.52772936,11.0919514 C8.49516983,11.0839514 8.49319652,11.0529514 8.47247682,11.0329514 C8.44090394,11.0019514 8.39255797,11.0219514 8.3856514,10.9749514 C8.37874483,10.9329514 8.34421199,10.9239514 8.31066581,10.9069514 C8.23370691,10.8679514 8.20213403,10.7919514 8.13997492,10.7389514 C8.07189589,10.6789514 7.97915055,10.6989514 7.89627174,10.6829514 C7.82325945,10.6689514 7.74926051,10.5519514 7.67131496,10.6019514 C7.62198234,10.6329514 7.59928933,10.7119514 7.63283551,10.7609514 C7.65947513,10.7989514 7.70486115,10.8179514 7.72262089,10.8629514 C7.69598128,10.8879514 7.69006136,10.9039514 7.72262089,10.9269514 C7.76110034,10.9539514 7.83509928,10.9819514 7.81635289,11.0409514 C7.80648636,11.0729514 7.77984674,11.1039514 7.7453139,11.1099514 C7.72064759,11.1149514 7.66046178,11.1009514 7.67328827,11.1459514 C7.645662,11.0719514 7.56771645,11.1879514 7.52529039,11.1269514 C7.49075755,11.0779514 7.46905119,11.0339514 7.4147853,11.0009514 C7.34473297,10.9579514 7.44339823,10.9159514 7.4315584,10.8509514 C7.41379865,10.7559514 7.2983603,10.7819514 7.2569209,10.7119514 C7.23225458,10.6719514 7.26580077,10.6399514 7.28158721,10.6049514 C7.29737365,10.5689514 7.33979971,10.5979514 7.36249272,10.6079514 C7.43649166,10.6429514 7.54502344,10.6299514 7.60718255,10.5789514 C7.63579547,10.5549514 7.69894123,10.4439514 7.61902238,10.4439514 C7.56376984,10.4449514 7.52134378,10.4929514 7.46905119,10.4959514 C7.46115797,10.4329514 7.4315584,10.3259514 7.49865077,10.2839514 C7.55982323,10.2459514 7.68808806,10.2019514 7.63382217,10.1039514 C7.61408912,10.0699514 7.57955628,10.1259514 7.55094335,10.1009514 C7.53910352,10.0909514 7.5479834,10.0679514 7.55193001,10.0569514 C7.53318361,10.0399514 7.51542386,10.0189514 7.50555734,9.99495137 C7.46214463,9.88895137 7.59040945,9.80595137 7.53614357,9.69395137 C7.51345056,9.64695137 7.47497111,9.61895137 7.43254505,9.58995137 C7.39011899,9.55995137 7.38814568,9.52195137 7.37235924,9.47695137 C7.36446602,9.45195137 7.32302662,9.39295137 7.28750713,9.40795137 C7.2569209,9.41995137 7.24804102,9.47295137 7.22436136,9.49495137 C7.17108213,9.54695137 7.05860374,9.56695137 6.98756476,9.54995137 C6.93033891,9.53695137 6.93329887,9.51495137 6.9056726,9.47695137 C6.89679272,9.46295137 6.87705967,9.46195137 6.86225988,9.45595137 C6.83660692,9.44595137 6.83364696,9.42295137 6.82772704,9.39995137 C6.80404738,9.31295137 6.63236984,9.42095137 6.60573022,9.29895137 C6.59981031,9.27095137 6.60967683,9.22395137 6.56922408,9.21795137 C6.52383806,9.20995137 6.52186476,9.16595137 6.52186476,9.12895137 C6.52186476,9.09895137 6.52383806,9.05695137 6.49226518,9.03995137 C6.45181243,9.01795137 6.4419459,9.02795137 6.42911942,8.98195137 C6.41431963,8.92295137 6.37386688,8.98395137 6.34032069,8.97195137 C6.26928171,8.94395137 6.28210819,8.97895137 6.22488235,9.00895137 C6.12720374,9.06095137 6.11635057,8.81995137 6.08280438,8.77295137 C6.01768531,8.68295137 6.03445841,8.88395137 5.99005904,8.90895137 C5.94960629,8.93195137 5.90718023,8.87895137 5.89238044,8.84795137 C5.88350057,8.82995137 5.87856731,8.80995137 5.86771413,8.79195137 C5.85094103,8.76595137 5.82134146,8.75495137 5.80456837,8.72895137 C5.79075523,8.70595137 5.77003553,8.67895137 5.760169,8.65395137 C5.75128913,8.63195137 5.75326243,8.60395137 5.73648934,8.58595137 C5.71576964,8.56295137 5.7414226,8.52495137 5.75622239,8.49595137 C5.78187536,8.48595137 5.82035481,8.50595137 5.8391012,8.52295137 C5.88547387,8.56195137 5.9555262,8.73295137 6.03643171,8.70095137 C6.01965862,8.67895137 6.0305118,8.65195137 6.01867197,8.62795137 C6.00584548,8.60295137 5.98117917,8.58795137 5.96243277,8.56795137 C5.92099336,8.51995137 5.87560735,8.47195137 5.84798108,8.41395137 C5.82430142,8.36395137 5.81246159,8.31095137 5.76411561,8.27595137 C5.72464951,8.24695137 5.64670396,8.21895137 5.66347705,8.15695137 C5.66347705,8.15595137 5.66446371,8.15495137 5.66446371,8.15495137 C5.69702324,8.16195137 5.71971625,8.18595137 5.74339591,8.20695137 C5.77792875,8.23695137 5.82232811,8.25195137 5.86278087,8.27195137 C5.93677981,8.30795137 6.02261858,8.33295137 6.08576434,8.38795137 C6.12523044,8.42095137 6.10451074,8.49495137 6.15384336,8.53595137 C6.19034951,8.56595137 6.2446154,8.66695137 6.31170777,8.62695137 C6.33637408,8.61195137 6.34722726,8.58295137 6.37189357,8.56595137 C6.39853319,8.54695137 6.44293256,8.52995137 6.47351878,8.51595137 C6.49226518,8.50695137 6.52383806,8.50995137 6.53863785,8.49495137 C6.56231751,8.47195137 6.50607832,8.40595137 6.49325184,8.38895137 C6.44293256,8.32395137 6.39655989,8.25295137 6.32946751,8.20395137 C6.29493468,8.17895137 6.26138849,8.15195137 6.22093574,8.13495137 C6.19922938,8.12595137 6.16074993,8.12695137 6.15680332,8.09695137 C6.1676565,8.10395137 6.17357641,8.10195137 6.17554972,8.09095137 C6.17456307,8.07095137 6.14595014,8.06995137 6.13213701,8.06595137 C6.09859082,8.05695137 6.07589781,8.05695137 6.06307133,8.02895137 C6.04629824,7.99495137 5.98709908,7.99595137 5.9555262,7.98795137 C5.90816688,7.97595137 5.87067409,7.93995137 5.82528807,7.92195137 C5.77200883,7.90195137 5.73155608,7.92295137 5.67926349,7.93495137 C5.67038362,7.93695137 5.65262388,7.96795137 5.63585078,7.99395137 C5.59835799,7.98495137 5.55691858,7.98895137 5.5253457,8.01395137 C5.47798638,8.05095137 5.45036011,8.10695137 5.41286731,8.15295137 C5.39708087,8.17195137 5.37438786,8.19095137 5.35169485,8.18395137 C5.34774824,8.18195137 5.34972155,8.17695137 5.34676159,8.17495137 C5.37537451,7.96995137 5.39116095,7.76295137 5.37241456,7.80395137 C5.33492176,7.88395137 5.30729549,7.93995137 5.27868256,7.99795137 C5.23724316,7.97995137 5.18889718,7.97895137 5.17113744,8.02295137 C5.15239104,8.06995137 5.17705735,8.13095137 5.14745778,8.17195137 C5.14055121,8.18295137 5.12969803,8.18195137 5.11983151,8.18695137 C5.1178582,8.18195137 5.10799168,8.16895137 5.10897833,8.16795137 C5.10009846,8.18295137 5.0991118,8.18795137 5.09121858,8.20095137 C5.06161901,8.20195137 5.02511286,8.18995137 4.98860672,8.17795137 C4.98860672,8.17795137 4.98860672,8.17495137 4.98762007,8.17495137 C4.98663341,8.17595137 4.98663341,8.17595137 4.98564676,8.17695137 C4.94223405,8.16195137 4.89684803,8.14795137 4.85738193,8.16195137 C4.77844973,8.18995137 4.77548977,8.30295137 4.72517049,8.37195137 C4.6501849,8.47695137 4.456801,8.43195137 4.42325482,8.30695137 C4.45088109,8.27295137 4.47752071,8.23895137 4.50514698,8.20495137 C4.46272092,8.09695137 4.34925588,8.02195137 4.23480418,8.02495137 C4.20224465,8.02595137 4.16771181,8.03195137 4.13811223,8.01795137 C4.107526,8.00295137 4.09075291,7.97095137 4.06411329,7.95095137 C3.98123448,7.88895137 3.8707294,7.97095137 3.80166372,8.04795137 C3.68326541,8.06795137 3.57374698,8.13495137 3.49974804,8.23095137 C3.45238872,8.22695137 3.4050294,8.22295137 3.35865673,8.21895137 C3.386283,8.29495137 3.29452432,8.35695137 3.25308491,8.42695137 C3.20177898,8.51195137 3.2284186,8.61095137 3.27873787,8.70195137 C3.27281796,8.71595137 3.26887135,8.73095137 3.25604487,8.73695137 C3.19585906,8.76895137 3.2116455,8.78795137 3.22940525,8.85295137 C3.24519169,8.90895137 3.23927177,9.01395137 3.22644529,9.06995137 C3.21657877,9.11395137 3.17316605,9.21995137 3.11988682,9.19495137 C3.09226055,9.18095137 3.06266097,9.16995137 3.037008,9.19595137 C3.02516817,9.20695137 3.01727495,9.22095137 3.01332834,9.23595137 C2.9955686,9.23695137 2.97780885,9.23895137 2.96103576,9.24295137 C2.92748957,9.24995137 2.89197008,9.25795137 2.85941055,9.24395137 C2.82685101,9.22995137 2.7834383,9.20295137 2.74693216,9.21495137 C2.71634593,9.22495137 2.65616013,9.25195137 2.64333364,9.28395137 C2.63741373,9.29795137 2.65616013,9.33495137 2.65616013,9.35395137 C2.65517347,9.38795137 2.68181309,9.43895137 2.67095991,9.46995137 C2.6462936,9.45795137 2.60978746,9.45395137 2.59301436,9.42895137 C2.57722792,9.40795137 2.55354826,9.41295137 2.53578852,9.39095137 C2.53184191,9.42895137 2.51802877,9.48195137 2.47264275,9.49195137 C2.42923004,9.50195137 2.38680398,9.46695137 2.34240462,9.47895137 C2.22597962,9.50895137 2.41245695,9.65495137 2.43712326,9.68295137 C2.47856267,9.72995137 2.4923758,9.79195137 2.52296203,9.84495137 C2.55650822,9.90295137 2.6255739,9.92195137 2.66602665,9.97295137 C2.69957284,10.0159514 2.7064794,10.0739514 2.75482538,10.1059514 C2.80810462,10.1429514 2.85645059,10.1759514 2.87815695,10.2389514 C2.90084995,10.2169514 2.94820928,10.3209514 2.99655525,10.2379514 C3.02220822,10.1929514 3.06759423,10.1539514 3.09620716,10.2269514 C3.12087347,10.2899514 3.09620716,10.3299514 3.15047305,10.3849514 C3.19191245,10.4279514 3.18993915,10.4789514 3.11890016,10.4739514 C3.13073999,10.5059514 3.14948639,10.5379514 3.11890016,10.5669514 C3.10508703,10.5809514 3.06562093,10.6099514 3.09423385,10.6299514 C3.12679338,10.6149514 3.16132622,10.6059514 3.19388576,10.5909514 C3.22940525,10.5759514 3.26393809,10.5399514 3.30537749,10.5409514 C3.3073508,10.5539514 3.25012495,10.5919514 3.28564444,10.5949514 C3.31524402,10.5979514 3.35767008,10.5669514 3.38134974,10.5929514 C3.40798936,10.6209514 3.37444317,10.6639514 3.39022961,10.6949514 C3.40601605,10.7269514 3.45929529,10.7029514 3.48494825,10.7079514 C3.47409508,10.7359514 3.43265567,10.7309514 3.40996266,10.7419514 C3.46225525,10.8059514 3.39417622,10.8999514 3.31820398,10.9019514 C3.28169783,10.9019514 3.15244635,10.7529514 3.14652644,10.8489514 C3.14553978,10.8769514 3.15441966,10.9119514 3.16329953,10.9389514 C3.17513936,10.9739514 3.25999148,10.9589514 3.29057771,10.9719514 C3.33497707,10.9899514 3.386283,11.0329514 3.40404275,11.0779514 C3.42081584,11.1239514 3.45929529,11.1539514 3.47409508,11.1979514 C3.502708,11.2799514 3.58065355,11.2909514 3.66155906,11.3149514 C3.76910418,11.3469514 3.7168116,11.5139514 3.71089169,11.5939514 C3.70595842,11.6729514 3.81646351,11.6919514 3.86678279,11.7369514 C3.92302198,11.7859514 3.93190185,11.8809514 3.83915652,11.8889514 C3.79179719,11.8929514 3.71286499,11.8709514 3.69510524,11.9319514 C3.66945228,12.0179514 3.79969041,12.0089514 3.85691626,12.0279514 C3.88355588,12.0369514 3.99110101,12.0479514 4.00096753,12.0729514 C4.01576732,12.1119514 4.00392749,12.1649514 4.01774062,12.2059514 C4.05128681,12.3109514 4.14797876,12.3809514 4.24171075,12.4329514 C4.44298787,12.5459514 4.68175778,12.6169514 4.90276795,12.6799514 C5.02609952,12.7159514 5.15140439,12.7439514 5.27769591,12.7609514 C5.40004083,12.7769514 5.50758595,12.7669514 5.61217112,12.8349514 C5.68419676,12.8819514 5.72958277,12.8469514 5.80358171,12.8599514 C5.83515459,12.8659514 5.84896773,12.8949514 5.87264739,12.9119514 C5.89928701,12.9329514 5.92987324,12.9059514 5.95848616,12.9169514 C5.96341942,12.8979514 5.96144612,12.8799514 5.95256625,12.8619514 C6.00880544,12.8829514 6.07787112,12.9429514 6.13707027,12.8949514 C6.16666985,12.8709514 6.1864029,12.8379514 6.21698913,12.8149514 C6.25349527,12.8179514 6.28901476,12.8199514 6.3255209,12.8199514 C6.47845205,12.8199514 6.59882366,12.7499514 6.72116857,12.6679514 C6.85239336,12.5799514 7.01025776,12.5779514 7.16220225,12.5639514 C7.32302662,12.5479514 7.49371751,12.5269514 7.64072873,12.4569514 C7.76899356,12.3949514 7.8015531,12.2819514 7.83805924,12.1569514 C7.87752534,12.0209514 7.99592365,11.9659514 8.0738692,11.8559514 C8.16562788,11.7279514 8.10544208,11.5609514 8.18042767,11.4279514 L8.18042767,11.4279514 Z M2.2950453,9.62395137 C2.29011203,9.59195137 2.2782722,9.57195137 2.25360589,9.55295137 C2.25261924,9.55595137 2.25163259,9.55795137 2.25163259,9.56195137 C2.2180864,9.54495137 2.21413979,9.48095137 2.16875377,9.48595137 C2.13126098,9.42595137 2.03654233,9.45295137 2.00694276,9.50795137 C1.98720971,9.54495137 2.01088937,9.56195137 2.03259572,9.58895137 C2.06022199,9.62295137 2.05528873,9.64895137 2.06515526,9.68795137 C2.08982157,9.78895137 2.17368704,9.71995137 2.23288619,9.75895137 C2.2555792,9.77395137 2.26445907,9.81695137 2.29800525,9.80995137 C2.33549805,9.80095137 2.33352475,9.74495137 2.32365822,9.71995137 C2.30984509,9.68395137 2.29997856,9.66295137 2.2950453,9.62395137 L2.2950453,9.62395137 Z M3.08338067,10.8149514 C3.08930059,10.7979514 3.06266097,10.7789514 3.04490122,10.7799514 C3.02911478,10.7809514 3.014315,10.8029514 3.00839508,10.8149514 C2.98866203,10.8499514 3.01036839,10.8969514 3.0557544,10.8969514 C3.06660758,10.8779514 3.06266097,10.8409514 3.09127389,10.8379514 C3.08930059,10.8289514 3.08338067,10.8259514 3.07548745,10.8229514 L3.08338067,10.8149514 L3.08338067,10.8149514 Z M2.24472602,9.54595137 C2.24768598,9.54795137 2.25064593,9.54995137 2.25360589,9.55295137 C2.25656585,9.54795137 2.25952581,9.54395137 2.26149911,9.53795137 L2.24472602,9.54595137 L2.24472602,9.54595137 Z M11.4896603,10.9489514 C11.4728872,10.9649514 11.4778204,10.9829514 11.4699272,11.0019514 C11.4610474,11.0249514 11.4265145,11.0339514 11.4077681,11.0459514 C11.3781685,11.0649514 11.368302,11.1099514 11.3357425,11.1209514 C11.3219293,11.0999514 11.3012096,11.0409514 11.2725967,11.0919514 C11.2558236,11.1249514 11.2666768,11.1579514 11.2400372,11.1879514 C11.2133976,11.2159514 11.2153709,11.2499514 11.1966245,11.2809514 C11.1680115,11.3299514 11.1393986,11.3589514 11.0910527,11.3879514 C11.0525732,11.4109514 11.04468,11.4539514 11.0170537,11.4859514 C10.9874541,11.5209514 10.9420681,11.5339514 10.9016154,11.5519514 C10.8730024,11.5639514 10.8256431,11.5979514 10.7930836,11.5789514 C10.7496709,11.5519514 10.80887,11.5039514 10.8295897,11.4869514 C10.8493228,11.4709514 10.9430548,11.4139514 10.9213484,11.3819514 C10.9065486,11.3609514 10.8532694,11.3639514 10.831563,11.3659514 C10.7871637,11.3709514 10.7536175,11.4159514 10.7161247,11.4369514 C10.6736986,11.4609514 10.6371925,11.4809514 10.5908198,11.4969514 C10.5375406,11.5159514 10.533594,11.5659514 10.4911679,11.5959514 C10.4576217,11.6219514 10.414209,11.6419514 10.3707963,11.6419514 C10.3135705,11.6419514 10.3165304,11.5939514 10.2997573,11.5539514 C10.278051,11.5569514 10.2593046,11.5849514 10.2385849,11.5939514 C10.2050387,11.6079514 10.1833323,11.6239514 10.1981321,11.6609514 C10.2119452,11.6989514 10.0590141,11.7339514 10.0323745,11.7529514 C10.0264546,11.7339514 10.0560541,11.7139514 10.067894,11.7029514 C10.0205346,11.6989514 9.96725541,11.7399514 9.91890944,11.7469514 C9.87253677,11.7529514 9.81531092,11.7849514 9.80840435,11.8329514 C9.80347109,11.8709514 9.75315181,11.8699514 9.72157893,11.8829514 C9.66928635,11.9049514 9.6909927,11.9359514 9.68112618,11.9779514 C9.66139313,12.0569514 9.49662215,11.9969514 9.58048762,11.8919514 C9.61008719,11.8549514 9.65448656,11.8309514 9.68112618,11.7929514 C9.71171241,11.7489514 9.71664567,11.6939514 9.74032533,11.6469514 C9.68803274,11.6619514 9.64856664,11.6919514 9.60416728,11.7209514 C9.553848,11.7539514 9.51142194,11.7459514 9.45518275,11.7349514 C9.39006368,11.7209514 9.34467766,11.7539514 9.28449186,11.7689514 C9.24601241,11.7779514 9.16017364,11.7749514 9.15524038,11.8309514 C9.15228042,11.8669514 9.21443953,11.8739514 9.23515923,11.8949514 C9.26574546,11.9269514 9.29929165,11.9739514 9.32494461,12.0099514 C9.34566432,12.0379514 9.42262321,12.0769514 9.41768995,12.1129514 C9.40979673,12.1829514 9.32198465,12.1709514 9.27462533,12.1829514 C9.22726601,12.1949514 9.22035944,12.2379514 9.1858266,12.2639514 C9.1463605,12.2929514 9.09308127,12.2589514 9.04966855,12.2769514 C9.00526919,12.2939514 8.97369631,12.3339514 8.93521686,12.3599514 C8.87009779,12.4049514 8.82767173,12.3559514 8.76057936,12.3529514 C8.70631347,12.3509514 8.65698085,12.3759514 8.60567491,12.3869514 C8.55930224,12.3969514 8.50306305,12.4049514 8.46754356,12.4379514 C8.38959801,12.5079514 8.64020775,12.4889514 8.66388741,12.4869514 C8.65303424,12.5269514 8.64218106,12.5739514 8.60567491,12.5999514 C8.5602889,12.6329514 8.49615648,12.6249514 8.4438639,12.6369514 C8.40341114,12.6469514 8.34717195,12.6919514 8.4063711,12.7239514 C8.45965034,12.7509514 8.52772936,12.7369514 8.5810086,12.7179514 C8.64218106,12.6969514 8.6994069,12.6639514 8.76353932,12.6499514 C8.83063169,12.6349514 8.90068402,12.6419514 8.96777639,12.6299514 C9.03980203,12.6159514 9.10294779,12.5769514 9.17004016,12.5499514 C9.23417258,12.5239514 9.30126495,12.5139514 9.36934398,12.5119514 C9.35553084,12.5369514 9.28843847,12.5349514 9.2627855,12.5409514 C9.21246622,12.5509514 9.17793338,12.5949514 9.1256408,12.5919514 C9.06644165,12.5899514 9.07334822,12.6319514 9.03092216,12.6419514 C9.00329589,12.6489514 8.93817682,12.7129514 8.91745711,12.6729514 C8.90167067,12.6419514 8.87108445,12.6479514 8.86121792,12.6859514 C8.8533247,12.7139514 8.86911114,12.7239514 8.83063169,12.7249514 C8.80103212,12.7249514 8.78721898,12.7129514 8.76057936,12.7059514 C8.70730012,12.6919514 8.68362046,12.7469514 8.64612767,12.7609514 C8.59087513,12.7819514 8.53167597,12.7749514 8.47839674,12.8099514 C8.44781051,12.8299514 8.41426432,12.8359514 8.37775818,12.8469514 C8.31165246,12.8679514 8.24949335,12.8929514 8.18338763,12.9149514 C8.1320817,12.9329514 8.08077576,12.9549514 8.02552322,12.9559514 C8.00283021,12.9559514 7.91205818,12.9399514 7.89725839,12.9669514 C7.86963212,13.0169514 7.95448424,12.9979514 7.97224399,12.9879514 C8.02256327,12.9609514 8.08373572,12.9769514 8.13997492,12.9769514 C8.20904059,12.9769514 8.26626644,12.9629514 8.32842555,12.9309514 C8.34519864,12.9219514 8.45669038,12.8979514 8.4626103,12.9109514 C8.47247682,12.9169514 8.54647576,12.8899514 8.55930224,12.8869514 C8.61948805,12.8729514 8.67967385,12.8599514 8.73887301,12.8449514 C8.92140372,12.7979514 9.10097449,12.7269514 9.27857194,12.6659514 C9.6327802,12.5459514 9.95837554,12.3429514 10.2662111,12.1369514 C10.4053291,12.0439514 10.5187942,11.9219514 10.669752,11.8459514 C10.8216965,11.7699514 10.9578546,11.6689514 11.0969726,11.5739514 C11.2331306,11.4809514 11.3367291,11.3529514 11.4501942,11.2359514 C11.5646459,11.1169514 11.6613378,11.0049514 11.7116571,10.8469514 C11.6830442,10.8399514 11.6534446,10.8989514 11.6297649,10.9099514 C11.5902988,10.9289514 11.5212332,10.9189514 11.4896603,10.9489514 L11.4896603,10.9489514 Z M10.6391658,10.7879514 C10.6736986,10.7409514 10.6440991,10.6769514 10.5829266,10.7139514 C10.5602336,10.7269514 10.5631935,10.7529514 10.5454338,10.7689514 C10.5266874,10.7859514 10.5247141,10.7659514 10.5059677,10.7609514 C10.4793281,10.7549514 10.4359154,10.7909514 10.4270355,10.8149514 C10.3905294,10.8139514 10.3579698,10.8549514 10.3747429,10.8869514 C10.4230889,10.8689514 10.4526885,10.8239514 10.504981,10.8379514 C10.5464205,10.8489514 10.6125262,10.8229514 10.6391658,10.7879514 L10.6391658,10.7879514 Z" />
+</svg>
diff --git a/devtools/client/themes/images/grid.svg b/devtools/client/themes/images/grid.svg
new file mode 100644
index 000000000..108ccafee
--- /dev/null
+++ b/devtools/client/themes/images/grid.svg
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" stroke="#696969">
+ <path fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round" d="M2 4h12M2 8h12M2 12h12M4 14V2M8 14V2M12 14V2"/>
+</svg>
diff --git a/devtools/client/themes/images/import.svg b/devtools/client/themes/images/import.svg
new file mode 100644
index 000000000..2cf948e3f
--- /dev/null
+++ b/devtools/client/themes/images/import.svg
@@ -0,0 +1,8 @@
+<!-- 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/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="#0b0b0b">
+ <path d="M7.864 1.417c-.123-.13-.305-.185-.48-.144-.173.04-.312.172-.363.343-.05.17-.007.357.116.487l4 4.243c.19.2.506.21.707.02.2-.188.21-.505.02-.706l-4-4.243z"/>
+ <path d="M7.136 1.414l-4 4.243c-.19.2-.18.518.02.707.202.19.52.18.708-.02l4-4.244c.123-.13.166-.316.115-.487-.052-.17-.19-.302-.365-.343-.174-.04-.356.014-.48.144zM1.5 8c-.276 0-.5.224-.5.5v5c0 .2.224.5.5.5h12c.276 0 .5-.3.5-.5v-5c0-.276-.224-.5-.5-.5h-3c-.28 0-.5.224-.5.5s.22.5.5.5H13v4H2V9h2.5c.27 0 .5-.224.5-.5S4.77 8 4.5 8h-3z"/>
+ <path d="M7 2v9c0 .276.224.5.5.5s.5-.224.5-.5V2c0-.276-.224-.5-.5-.5S7 1.724 7 2z"/>
+</svg>
diff --git a/devtools/client/themes/images/item-arrow-dark-ltr.svg b/devtools/client/themes/images/item-arrow-dark-ltr.svg
new file mode 100644
index 000000000..c2accabde
--- /dev/null
+++ b/devtools/client/themes/images/item-arrow-dark-ltr.svg
@@ -0,0 +1,7 @@
+<!-- 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/. -->
+<svg width="7" xmlns="http://www.w3.org/2000/svg" height="12" viewBox="0 0 7 12">
+ <path fill="#181d20" d="M7,11.6 7,.4 1.5,6z"/>
+ <path fill="#000" d="M7,0 6,0 0,6 6,12 7,12 7,11.6 1.5,6 7,.4z"/>
+</svg>
diff --git a/devtools/client/themes/images/item-arrow-dark-rtl.svg b/devtools/client/themes/images/item-arrow-dark-rtl.svg
new file mode 100644
index 000000000..5ccd34c09
--- /dev/null
+++ b/devtools/client/themes/images/item-arrow-dark-rtl.svg
@@ -0,0 +1,7 @@
+<!-- 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/. -->
+<svg width="7" xmlns="http://www.w3.org/2000/svg" height="12" viewBox="0 0 7 12">
+ <path fill="#181d20" d="M0,11.6 0,.4 5.5,6z"/>
+ <path fill="#000" d="M1,0 0,0 0,.4 5.5,6 0,11.6 0,12 1,12 7,6z"/>
+</svg>
diff --git a/devtools/client/themes/images/item-arrow-ltr.svg b/devtools/client/themes/images/item-arrow-ltr.svg
new file mode 100644
index 000000000..a9f7b33d9
--- /dev/null
+++ b/devtools/client/themes/images/item-arrow-ltr.svg
@@ -0,0 +1,7 @@
+<!-- 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/. -->
+<svg width="7" xmlns="http://www.w3.org/2000/svg" height="12" viewBox="0 0 7 12">
+ <path fill="#ffffff" d="M7,11.6 7,.4 1.5,6z"/>
+ <path fill="#dde1e4" d="M7,0 6,0 0,6 6,12 7,12 7,11.6 1.5,6 7,.4z"/>
+</svg>
diff --git a/devtools/client/themes/images/item-arrow-rtl.svg b/devtools/client/themes/images/item-arrow-rtl.svg
new file mode 100755
index 000000000..80ea9e89c
--- /dev/null
+++ b/devtools/client/themes/images/item-arrow-rtl.svg
@@ -0,0 +1,7 @@
+<!-- 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/. -->
+<svg width="7" xmlns="http://www.w3.org/2000/svg" height="12" viewBox="0 0 7 12">
+ <path fill="#ffffff" d="M0,11.6 0,.4 5.5,6z"/>
+ <path fill="#dde1e4" d="M1,0 0,0 0,.4 5.5,6 0,11.6 0,12 1,12 7,6z"/>
+</svg>
diff --git a/devtools/client/themes/images/item-toggle.svg b/devtools/client/themes/images/item-toggle.svg
new file mode 100644
index 000000000..fd914c5c6
--- /dev/null
+++ b/devtools/client/themes/images/item-toggle.svg
@@ -0,0 +1,7 @@
+<!-- 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/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="#0b0b0b">
+ <circle cx="8" cy="8.5" r="1.5"/>
+ <path d="M15.498 8.28l-.001-.03v-.002-.004l-.002-.018-.004-.031c0-.002 0-.002 0 0l-.004-.035.006.082c-.037-.296-.133-.501-.28-.661-.4-.522-.915-1.042-1.562-1.604-1.36-1.182-2.74-1.975-4.178-2.309a6.544 6.544 0 0 0-2.755-.042c-.78.153-1.565.462-2.369.91C3.252 5.147 2.207 6 1.252 7.035c-.216.233-.36.398-.499.577-.338.437-.338 1 0 1.437.428.552.941 1.072 1.59 1.635 1.359 1.181 2.739 1.975 4.177 2.308.907.21 1.829.223 2.756.043.78-.153 1.564-.462 2.369-.91 1.097-.612 2.141-1.464 3.097-2.499.217-.235.36-.398.498-.578.12-.128.216-.334.248-.554 0 .01 0 .01-.008.04l.013-.079-.001.011.003-.031.001-.017v.005l.001-.02v.008l.002-.03.001-.05-.001-.044v-.004-.004zm-.954.045v.007l.001.004V8.33v.012l-.001.01v-.005-.005l.002-.015-.001.008c-.002.014-.002.014 0 0l-.007.084c.003-.057-.004-.041-.014-.031-.143.182-.27.327-.468.543-.89.963-1.856 1.752-2.86 2.311-.724.404-1.419.677-2.095.81a5.63 5.63 0 0 1-2.374-.036c-1.273-.295-2.523-1.014-3.774-2.101-.604-.525-1.075-1.001-1.457-1.496-.054-.07-.054-.107 0-.177.117-.152.244-.298.442-.512.89-.963 1.856-1.752 2.86-2.311.724-.404 1.419-.678 2.095-.81a5.631 5.631 0 0 1 2.374.036c1.272.295 2.523 1.014 3.774 2.101.603.524 1.074 1 1.457 1.496.035.041.043.057.046.076 0 .01 0 .01.008.043l-.009-.047.003.02-.002-.013v-.008.016c0-.004 0-.004 0 0v-.004z"/>
+</svg>
diff --git a/devtools/client/themes/images/magnifying-glass.png b/devtools/client/themes/images/magnifying-glass.png
new file mode 100644
index 000000000..bd09ffb4f
--- /dev/null
+++ b/devtools/client/themes/images/magnifying-glass.png
Binary files differ
diff --git a/devtools/client/themes/images/magnifying-glass@2x.png b/devtools/client/themes/images/magnifying-glass@2x.png
new file mode 100644
index 000000000..17842853a
--- /dev/null
+++ b/devtools/client/themes/images/magnifying-glass@2x.png
Binary files differ
diff --git a/devtools/client/themes/images/noise.png b/devtools/client/themes/images/noise.png
new file mode 100644
index 000000000..01d340aaa
--- /dev/null
+++ b/devtools/client/themes/images/noise.png
Binary files differ
diff --git a/devtools/client/themes/images/pane-collapse.svg b/devtools/client/themes/images/pane-collapse.svg
new file mode 100644
index 000000000..4490fd546
--- /dev/null
+++ b/devtools/client/themes/images/pane-collapse.svg
@@ -0,0 +1,7 @@
+<!-- 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/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="#0b0b0b">
+ <path fill-opacity=".3" d="M12,3h2v10h-2V3z M5,9.9V6.1L8,8L5,9.9z"/>
+ <path d="M14,2H2C1.4,2,1,2.4,1,3v10c0,0.6,0.4,1,1,1h12c0.6,0,1-0.4,1-1V3C15,2.4,14.6,2,14,2z M2,13L2,13V3h0h9v10 H2L2,13z M14,13C14,13,14,13,14,13h-2V3h2c0,0,0,0,0,0V13z M8.5,7.2l-3-1.9C4.6,4.7,4,5,4,6.1v3.8c0,1.1,0.6,1.4,1.5,0.8l3-1.9 C9.5,8.3,9.5,7.8,8.5,7.2z M5,9.9V6.1L8,8L5,9.9z"/>
+</svg>
diff --git a/devtools/client/themes/images/pane-expand.svg b/devtools/client/themes/images/pane-expand.svg
new file mode 100644
index 000000000..db6c3bcb1
--- /dev/null
+++ b/devtools/client/themes/images/pane-expand.svg
@@ -0,0 +1,7 @@
+<!-- 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/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="#0b0b0b">
+ <path fill-opacity=".3" d="M4,13H2V3h2V13z M11,6.1v3.8L8,8L11,6.1z"/>
+ <path d="M2,14h12c0.6,0,1-0.4,1-1V3c0-0.6-0.4-1-1-1H2C1.4,2,1,2.4,1,3v10C1,13.6,1.4,14,2,14z M14,3L14,3v10h0H5V3 H14L14,3z M2,3C2,3,2,3,2,3h2v10H2c0,0,0,0,0,0V3z M7.5,8.8l3,1.9c1,0.6,1.5,0.3,1.5-0.8V6.1c0-1.1-0.6-1.4-1.5-0.8l-3,1.9 C6.5,7.7,6.5,8.2,7.5,8.8z M11,6.1v3.8L8,8L11,6.1z"/>
+</svg>
diff --git a/devtools/client/themes/images/pause.svg b/devtools/client/themes/images/pause.svg
new file mode 100644
index 000000000..308570294
--- /dev/null
+++ b/devtools/client/themes/images/pause.svg
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="#0b0b0b">
+ <path d="M5 12.503l.052-9a.5.5 0 0 0-1-.006l-.052 9a.5.5 0 0 0 1 .006zM12 12.497l-.05-9A.488.488 0 0 0 11.474 3a.488.488 0 0 0-.473.503l.05 9a.488.488 0 0 0 .477.497.488.488 0 0 0 .473-.503z"/>
+</svg>
diff --git a/devtools/client/themes/images/performance-icons.svg b/devtools/client/themes/images/performance-icons.svg
new file mode 100644
index 000000000..d0d5e881e
--- /dev/null
+++ b/devtools/client/themes/images/performance-icons.svg
@@ -0,0 +1,42 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#0b0b0b">
+ <style>
+ g:not(:target) {
+ display: none;
+ }
+ </style>
+ <g id="overview-markers">
+ <rect x="0" y="4" width="5" height="1"/>
+ <rect x="7" y="4" width="9" height="1"/>
+ <rect x="0" y="8" width="8" height="1"/>
+ <rect x="10" y="8" width="6" height="1"/>
+ <rect x="0" y="12" width="9" height="1"/>
+ <rect x="12" y="12" width="4" height="1"/>
+ </g>
+ <g id="overview-frames">
+ <rect x="1" y="4" width="2" height="12" rx="1" ry="1"/>
+ <rect x="5" y="12" width="2" height="4" rx="1" ry="1"/>
+ <rect x="9" y="9" width="2" height="7" rx="1" ry="1"/>
+ <rect x="13" y="7" width="2" height="9" rx="1" ry="1"/>
+ </g>
+ <g id="details-waterfall">
+ <rect x="0" y="4" width="9" height="1"/>
+ <rect x="5" y="8" width="8" height="1"/>
+ <rect x="7" y="12" width="9" height="1"/>
+ </g>
+ <g id="details-call-tree">
+ <rect x="1" y="4" width="10" height="1"/>
+ <rect x="5" y="7" width="10" height="1"/>
+ <rect x="1" y="10" width="10" height="1"/>
+ <rect x="5" y="13" width="10" height="1"/>
+ </g>
+ <g id="details-flamegraph">
+ <rect x="0" y="4" width="16" height="1"/>
+ <rect x="0" y="7" width="8" height="1"/>
+ <rect x="10" y="7" width="6" height="1"/>
+ <rect x="2" y="10" width="6" height="1"/>
+ <rect x="5" y="13" width="3" height="1"/>
+ </g>
+</svg>
diff --git a/devtools/client/themes/images/play.svg b/devtools/client/themes/images/play.svg
new file mode 100644
index 000000000..d677b4bd0
--- /dev/null
+++ b/devtools/client/themes/images/play.svg
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="#0b0b0b">
+ <path d="M4 12.5l8-5-8-5v10zm-1 0v-10a1 1 0 0 1 1.53-.848l8 5a1 1 0 0 1 0 1.696l-8 5A1 1 0 0 1 3 12.5z" fill-rule="evenodd"/>
+</svg>
diff --git a/devtools/client/themes/images/power.svg b/devtools/client/themes/images/power.svg
new file mode 100644
index 000000000..6975c1036
--- /dev/null
+++ b/devtools/client/themes/images/power.svg
@@ -0,0 +1,7 @@
+<!-- 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/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="#0b0b0b">
+ <path d="M8 14.5a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11zm0-1a4.5 4.5 0 1 1 0-9 4.5 4.5 0 0 1 0 9z"/>
+ <path d="M8.5 7.5v-6a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0z"/>
+</svg>
diff --git a/devtools/client/themes/images/profiler-stopwatch.svg b/devtools/client/themes/images/profiler-stopwatch.svg
new file mode 100644
index 000000000..deafdb8b4
--- /dev/null
+++ b/devtools/client/themes/images/profiler-stopwatch.svg
@@ -0,0 +1,11 @@
+<!-- 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/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="#0b0b0b">
+ <g fill-rule="evenodd">
+ <path d="M15 9.004C14.51 12.394 11.578 15 8.035 15 4.15 15 1 11.866 1 8s3.15-7 7.036-7c1.941 0 3.7.783 4.972 2.048l-.709.709A6.027 6.027 0 0 0 8.036 2c-3.33 0-6.03 2.686-6.03 6s2.7 6 6.03 6a6.023 6.023 0 0 0 5.946-4.993l1.017-.003z"/>
+ <path d="M4.137 9H3.1a5.002 5.002 0 0 0 9.8 0h-.965a4.023 4.023 0 0 1-3.9 3 4.023 4.023 0 0 1-3.898-3z" fill-opacity=".5"/>
+ <path d="M8.036 11a2.994 2.994 0 0 0 2.987-3c0-1.657-1.338-3-2.987-3a2.994 2.994 0 0 0-2.988 3c0 1.657 1.338 3 2.988 3zm0-1c1.11 0 2.011-.895 2.011-2s-.9-2-2.011-2c-1.111 0-2.012.895-2.012 2s.9 2 2.012 2z"/>
+ <path d="M10.354 6.354l4-4a.5.5 0 0 0-.708-.708l-4 4a.5.5 0 1 0 .708.708z"/>
+ </g>
+</svg>
diff --git a/devtools/client/themes/images/pseudo-class.svg b/devtools/client/themes/images/pseudo-class.svg
new file mode 100644
index 000000000..8062cd31a
--- /dev/null
+++ b/devtools/client/themes/images/pseudo-class.svg
@@ -0,0 +1,7 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="#0b0b0b">
+ <path d="M11 7V5.5c0-.3-.2-.5-.5-.5h-5c-.3 0-.5.2-.5.5v5c0 .3.2.5.5.5h1.9V7.5c0-.3.2-.5.5-.5H11zM3 7H.8c-.1 0-.6 0-.7-.7-.1-.2-.1-.4-.1-.6v-5C0 .5 0 .3.2.1.4 0 .6 0 .7 0h5.2c.3 0 .6 0 .8.2.2.1.3.3.3.5V3H3v4zM1 6h1V2.7c0-.2.1-.4.2-.5.3-.2.6-.2.8-.2h3V1H1v5z"/>
+ <path d="M9 9h1v1H9V9zm5 1h-1V9h1v1zm-2 0h-1V9h1v1zm3-1h1v1h-1V9zm1 5h-1v-1h1v1zm0-2h-1v-1h1v1zm-1 3h1v1h-1v-1zm-1 1h-1v-1h1v1zm-2 0h-1v-1h1v1zm-3-1h1v1H9v-1zm1-1H9v-1h1v1zm0-2H9v-1h1v1z"/>
+</svg>
diff --git a/devtools/client/themes/images/reload.svg b/devtools/client/themes/images/reload.svg
new file mode 100644
index 000000000..b04262784
--- /dev/null
+++ b/devtools/client/themes/images/reload.svg
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 14" width="14" height="14">
+ <path d="M12,7H6l2.4-2.4C7.6,4,6.6,3.8,5.5,4.1C4.3,4.5,3.3,5.5,3,6.8 C2.6,9,4.3,11,6.5,11c1,0,2-0.5,2.6-1.2l1.7,1c-1.3,1.6-3.3,2.5-5.6,2c-2-0.5-3.6-2.1-4-4.1C0.4,5.1,3.1,2,6.5,2 c1.3,0,2.4,0.4,3.3,1.2L12,1V7z"/>
+</svg>
diff --git a/devtools/client/themes/images/responsivemode/responsive-horizontal-resizer.png b/devtools/client/themes/images/responsivemode/responsive-horizontal-resizer.png
new file mode 100644
index 000000000..7d113f0df
--- /dev/null
+++ b/devtools/client/themes/images/responsivemode/responsive-horizontal-resizer.png
Binary files differ
diff --git a/devtools/client/themes/images/responsivemode/responsive-horizontal-resizer@2x.png b/devtools/client/themes/images/responsivemode/responsive-horizontal-resizer@2x.png
new file mode 100644
index 000000000..bb3c4bde0
--- /dev/null
+++ b/devtools/client/themes/images/responsivemode/responsive-horizontal-resizer@2x.png
Binary files differ
diff --git a/devtools/client/themes/images/responsivemode/responsive-se-resizer.png b/devtools/client/themes/images/responsivemode/responsive-se-resizer.png
new file mode 100644
index 000000000..35b54d62c
--- /dev/null
+++ b/devtools/client/themes/images/responsivemode/responsive-se-resizer.png
Binary files differ
diff --git a/devtools/client/themes/images/responsivemode/responsive-se-resizer@2x.png b/devtools/client/themes/images/responsivemode/responsive-se-resizer@2x.png
new file mode 100644
index 000000000..9dbf4fe8e
--- /dev/null
+++ b/devtools/client/themes/images/responsivemode/responsive-se-resizer@2x.png
Binary files differ
diff --git a/devtools/client/themes/images/responsivemode/responsive-vertical-resizer.png b/devtools/client/themes/images/responsivemode/responsive-vertical-resizer.png
new file mode 100644
index 000000000..3b4e78c6f
--- /dev/null
+++ b/devtools/client/themes/images/responsivemode/responsive-vertical-resizer.png
Binary files differ
diff --git a/devtools/client/themes/images/responsivemode/responsive-vertical-resizer@2x.png b/devtools/client/themes/images/responsivemode/responsive-vertical-resizer@2x.png
new file mode 100644
index 000000000..cbae60621
--- /dev/null
+++ b/devtools/client/themes/images/responsivemode/responsive-vertical-resizer@2x.png
Binary files differ
diff --git a/devtools/client/themes/images/responsivemode/responsiveui-home.png b/devtools/client/themes/images/responsivemode/responsiveui-home.png
new file mode 100644
index 000000000..43379d0e9
--- /dev/null
+++ b/devtools/client/themes/images/responsivemode/responsiveui-home.png
Binary files differ
diff --git a/devtools/client/themes/images/responsivemode/responsiveui-rotate.png b/devtools/client/themes/images/responsivemode/responsiveui-rotate.png
new file mode 100644
index 000000000..2bacbd2d5
--- /dev/null
+++ b/devtools/client/themes/images/responsivemode/responsiveui-rotate.png
Binary files differ
diff --git a/devtools/client/themes/images/responsivemode/responsiveui-rotate@2x.png b/devtools/client/themes/images/responsivemode/responsiveui-rotate@2x.png
new file mode 100644
index 000000000..eeeb82328
--- /dev/null
+++ b/devtools/client/themes/images/responsivemode/responsiveui-rotate@2x.png
Binary files differ
diff --git a/devtools/client/themes/images/responsivemode/responsiveui-screenshot.png b/devtools/client/themes/images/responsivemode/responsiveui-screenshot.png
new file mode 100644
index 000000000..084220ed1
--- /dev/null
+++ b/devtools/client/themes/images/responsivemode/responsiveui-screenshot.png
Binary files differ
diff --git a/devtools/client/themes/images/responsivemode/responsiveui-screenshot@2x.png b/devtools/client/themes/images/responsivemode/responsiveui-screenshot@2x.png
new file mode 100644
index 000000000..927c5cf0b
--- /dev/null
+++ b/devtools/client/themes/images/responsivemode/responsiveui-screenshot@2x.png
Binary files differ
diff --git a/devtools/client/themes/images/responsivemode/responsiveui-touch.png b/devtools/client/themes/images/responsivemode/responsiveui-touch.png
new file mode 100644
index 000000000..90587034c
--- /dev/null
+++ b/devtools/client/themes/images/responsivemode/responsiveui-touch.png
Binary files differ
diff --git a/devtools/client/themes/images/responsivemode/responsiveui-touch@2x.png b/devtools/client/themes/images/responsivemode/responsiveui-touch@2x.png
new file mode 100644
index 000000000..e4645039c
--- /dev/null
+++ b/devtools/client/themes/images/responsivemode/responsiveui-touch@2x.png
Binary files differ
diff --git a/devtools/client/themes/images/rewind.svg b/devtools/client/themes/images/rewind.svg
new file mode 100644
index 000000000..13a309626
--- /dev/null
+++ b/devtools/client/themes/images/rewind.svg
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="#0b0b0b">
+ <path d="M13 2.5l-8 5 8 5v-10zm1 0v10a1 1 0 0 1-1.53.848l-8-5a1 1 0 0 1 0-1.696l8-5A1 1 0 0 1 14 2.5zM2 12.497l-.04-7.342-.01-1.658A.488.488 0 0 0 1.474 3 .488.488 0 0 0 1 3.503l.05 9a.488.488 0 0 0 .477.497.488.488 0 0 0 .473-.503z"/>
+</svg>
diff --git a/devtools/client/themes/images/search-clear-dark.svg b/devtools/client/themes/images/search-clear-dark.svg
new file mode 100644
index 000000000..422a7ce7f
--- /dev/null
+++ b/devtools/client/themes/images/search-clear-dark.svg
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="16" viewBox="0 0 32 16">
+ <defs>
+ <path id="glyphShape-clear" d="M8,0C3.6,0,0,3.6,0,8c0,4.4,3.6,8,8,8s8-3.6,8-8C16,3.6,12.4,0,8,0 z M11.9,10.5l-1.4,1.4L8,9.4l-2.4,2.4l-1.4-1.4L6.6,8L4.2,5.6l1.4-1.4L8,6.6l2.4-2.4l1.4,1.4L9.4,8L11.9,10.5z"/>
+ <style>
+ .icon-state-default { fill: #f5f7fa; fill-opacity: .6; }
+ .icon-state-pressed { fill: #7d7e80; fill-opacity: .8; }
+ </style>
+ </defs>
+ <use xlink:href="#glyphShape-clear" class="icon-state-default"/>
+ <use xlink:href="#glyphShape-clear" class="icon-state-pressed" transform="translate(16)"/>
+</svg>
diff --git a/devtools/client/themes/images/search-clear-failed.svg b/devtools/client/themes/images/search-clear-failed.svg
new file mode 100644
index 000000000..a8f9fd8ee
--- /dev/null
+++ b/devtools/client/themes/images/search-clear-failed.svg
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="16" viewBox="0 0 32 16">
+ <defs>
+ <path id="glyphShape-clear" d="M8,0C3.6,0,0,3.6,0,8c0,4.4,3.6,8,8,8s8-3.6,8-8C16,3.6,12.4,0,8,0 z M11.9,10.5l-1.4,1.4L8,9.4l-2.4,2.4l-1.4-1.4L6.6,8L4.2,5.6l1.4-1.4L8,6.6l2.4-2.4l1.4,1.4L9.4,8L11.9,10.5z"/>
+ <style>
+ .icon-state-default { fill: #cc3d3d; fill-opacity: 1; }
+ .icon-state-pressed { fill: #802d2d; fill-opacity: 1; }
+ </style>
+ </defs>
+ <use xlink:href="#glyphShape-clear" class="icon-state-default"/>
+ <use xlink:href="#glyphShape-clear" class="icon-state-pressed" transform="translate(16)"/>
+</svg>
diff --git a/devtools/client/themes/images/search-clear-light.svg b/devtools/client/themes/images/search-clear-light.svg
new file mode 100644
index 000000000..066ef4439
--- /dev/null
+++ b/devtools/client/themes/images/search-clear-light.svg
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="16" viewBox="0 0 32 16">
+ <defs>
+ <path id="glyphShape-clear" d="M8,0C3.6,0,0,3.6,0,8c0,4.4,3.6,8,8,8s8-3.6,8-8C16,3.6,12.4,0,8,0 z M11.9,10.5l-1.4,1.4L8,9.4l-2.4,2.4l-1.4-1.4L6.6,8L4.2,5.6l1.4-1.4L8,6.6l2.4-2.4l1.4,1.4L9.4,8L11.9,10.5z"/>
+ <style>
+ .icon-state-default { fill: #1d2126; fill-opacity: .5; }
+ .icon-state-pressed { fill: #1d2126; fill-opacity: .8; }
+ </style>
+ </defs>
+ <use xlink:href="#glyphShape-clear" class="icon-state-default"/>
+ <use xlink:href="#glyphShape-clear" class="icon-state-pressed" transform="translate(16)"/>
+</svg>
diff --git a/devtools/client/themes/images/search.svg b/devtools/client/themes/images/search.svg
new file mode 100644
index 000000000..49f8cc605
--- /dev/null
+++ b/devtools/client/themes/images/search.svg
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="#aaa">
+ <path d="M10.716 10.032C11.516 9.077 12 7.845 12 6.5 12 3.462 9.538 1 6.5 1S1 3.462 1 6.5 3.462 12 6.5 12c1.345 0 2.577-.483 3.532-1.284l4.143 4.142c.19.19.495.19.683 0 .19-.188.19-.494 0-.683l-4.142-4.143zM6.5 11C8.985 11 11 8.985 11 6.5S8.985 2 6.5 2 2 4.015 2 6.5 4.015 11 6.5 11z" fill-rule="evenodd"/>
+</svg>
diff --git a/devtools/client/themes/images/security-state-broken.svg b/devtools/client/themes/images/security-state-broken.svg
new file mode 100644
index 000000000..5f122c3ee
--- /dev/null
+++ b/devtools/client/themes/images/security-state-broken.svg
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
+ width="16" height="16" viewBox="0 0 16 16">
+ <path fill="#808080" d="M14.8,12.5L9.3,1.9C9,1.3,8.5,1,8,1C7.5,1,7,1.3,6.7,1.9L1.2,12.5c-0.3,0.6-0.3,1.2,0,1.7C1.5,14.7,2,15,2.6,15h10.8 c0.6,0,1.1-0.3,1.4-0.8C15.1,13.7,15.1,13.1,14.8,12.5z"/>
+ <path fill="#fff" d="M8,11c-0.8,0-1.5,0.7-1.5,1.5C6.5,13.3,7.2,14,8,14 c0.8,0,1.5-0.7,1.5-1.5C9.5,11.7,8.8,11,8,11z M8,10L8,10C8.6,10,9,9.6,9,9l0.2-4.2c0-0.7-0.5-1.2-1.2-1.2S6.8,4.1,6.8,4.8L7,9 C7,9.6,7.4,10,8,10z"/>
+</svg>
diff --git a/devtools/client/themes/images/security-state-insecure.svg b/devtools/client/themes/images/security-state-insecure.svg
new file mode 100644
index 000000000..b7191a867
--- /dev/null
+++ b/devtools/client/themes/images/security-state-insecure.svg
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
+ width="16" height="16" viewBox="0 0 16 16">
+ <style>
+ .icon-default {
+ fill: #999;
+ }
+ </style>
+
+ <defs>
+ <rect id="shape-lock-clasp-outer" x="4" y="2" width="8" height="10" rx="4" ry="4" />
+ <rect id="shape-lock-clasp-inner" x="6" y="4" width="4" height="6" rx="2" ry="2" />
+ <rect id="shape-lock-base" x="3" y="7" width="10" height="7" rx="1" ry="1" />
+
+ <mask id="mask-clasp-cutout">
+ <rect width="16" height="16" fill="#000" />
+ <use xlink:href="#shape-lock-clasp-outer" fill="#fff" />
+ <use xlink:href="#shape-lock-clasp-inner" fill="#000" />
+ <line x1="2" y1="13" x2="14" y2="1.5" stroke="#000" stroke-width="2" />
+ <line x1="2" y1="15" x2="14" y2="3.5" stroke="#000" stroke-width="2" />
+ <rect x="3" y="7" width="10" height="7" rx="1" ry="1" fill="#000" />
+ </mask>
+
+ <mask id="mask-base-cutout">
+ <rect width="16" height="16" fill="#000" />
+ <use xlink:href="#shape-lock-base" fill="#fff" />
+ <line x1="2" y1="14.8" x2="14" y2="3.2" stroke="#000" stroke-width="1.8" />
+ </mask>
+ </defs>
+
+ <use xlink:href="#shape-lock-clasp-outer" mask="url(#mask-clasp-cutout)" class="icon-default" />
+ <use xlink:href="#shape-lock-base" mask="url(#mask-base-cutout)" class="icon-default" />
+
+ <line x1="2" y1="14.1" x2="14" y2="2.5" stroke="#d92d21" stroke-width="1.8" />
+</svg>
diff --git a/devtools/client/themes/images/security-state-secure.svg b/devtools/client/themes/images/security-state-secure.svg
new file mode 100644
index 000000000..5dad8903b
--- /dev/null
+++ b/devtools/client/themes/images/security-state-secure.svg
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
+ width="16" height="16" viewBox="0 0 16 16">
+ <style>
+ .icon-default {
+ fill: #4d9a26;
+ }
+ </style>
+
+ <defs>
+ <rect id="shape-lock-clasp-outer" x="4" y="2" width="8" height="10" rx="4" ry="4" />
+ <rect id="shape-lock-clasp-inner" x="6" y="4" width="4" height="6" rx="2" ry="2" />
+ <rect id="shape-lock-base" x="3" y="7" width="10" height="7" rx="1" ry="1" />
+
+ <mask id="mask-clasp-cutout">
+ <rect width="16" height="16" fill="#000" />
+ <use xlink:href="#shape-lock-clasp-outer" fill="#fff" />
+ <use xlink:href="#shape-lock-clasp-inner" fill="#000" />
+ </mask>
+ </defs>
+
+ <use xlink:href="#shape-lock-clasp-outer" mask="url(#mask-clasp-cutout)" class="icon-default" />
+ <use xlink:href="#shape-lock-base" class="icon-default" />
+</svg>
diff --git a/devtools/client/themes/images/security-state-weak.svg b/devtools/client/themes/images/security-state-weak.svg
new file mode 100644
index 000000000..d1e9dbd4b
--- /dev/null
+++ b/devtools/client/themes/images/security-state-weak.svg
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
+ width="16" height="16" viewBox="0 0 16 16">
+ <style>
+ .icon-default {
+ fill: #808080;
+ }
+ </style>
+
+ <defs>
+ <rect id="shape-lock-clasp-outer" x="2" y="1" width="8" height="10" rx="4" ry="4" />
+ <rect id="shape-lock-clasp-inner" x="4" y="3" width="4" height="6" rx="2" ry="2" />
+ <rect id="shape-lock-base" x="1" y="6" width="10" height="7" rx="1" ry="1" />
+
+ <mask id="mask-clasp-cutout">
+ <rect width="16" height="16" fill="#000" />
+ <use xlink:href="#shape-lock-clasp-outer" fill="#fff" />
+ <use xlink:href="#shape-lock-clasp-inner" fill="#000" />
+ </mask>
+ </defs>
+
+ <use xlink:href="#shape-lock-clasp-outer" mask="url(#mask-clasp-cutout)" class="icon-default" />
+ <use xlink:href="#shape-lock-base" class="icon-default" />
+ <path fill="#fff" d="M10.5,5C9.8,5,9.1,5.4,8.8,6.2l-3.5,6.8c-0.4,0.7-0.4,1.4,0,2c0.4,0.6,1,1,1.8,1H14c0.8,0,1.4-0.4,1.8-1 c0.3-0.6,0.3-1.4,0-2l-3.5-6.8C11.9,5.4,11.2,5,10.5,5L10.5,5z"/>
+ <path fill="#ffbf00" d="M14.8,13.4l-3.5-6.8C11.2,6.2,10.9,6,10.5,6c-0.3,0-0.7,0.2-0.9,0.6l-3.5,6.8c-0.2,0.4-0.2,0.8,0,1.1C6.3,14.8,6.6,15,7,15 H14c0.4,0,0.7-0.2,0.9-0.5C15.1,14.2,15,13.8,14.8,13.4z"/>
+ <path fill="#fff" d="M10,8.5C10,8.2,10.2,8,10.5,8S11,8.2,11,8.5L10.8,11h-0.6L10,8.5z" />
+ <circle fill="#fff" cx="10.5" cy="12.5" r=".75" />
+</svg>
diff --git a/devtools/client/themes/images/sort-arrows.svg b/devtools/client/themes/images/sort-arrows.svg
new file mode 100644
index 000000000..289b07530
--- /dev/null
+++ b/devtools/client/themes/images/sort-arrows.svg
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="7" height="4" fill="#edf0f1" fill-opacity="0.8">
+ <style>
+ polygon:not(:target) {
+ display: none;
+ }
+ </style>
+ <polygon points="0,4 3.5,0 7,4" id="ascending"/>
+ <polygon points="0,0 3.5,4 7,0" id="descending"/>
+</svg>
diff --git a/devtools/client/themes/images/toggle-tools.png b/devtools/client/themes/images/toggle-tools.png
new file mode 100644
index 000000000..495439391
--- /dev/null
+++ b/devtools/client/themes/images/toggle-tools.png
Binary files differ
diff --git a/devtools/client/themes/images/toggle-tools@2x.png b/devtools/client/themes/images/toggle-tools@2x.png
new file mode 100644
index 000000000..971f41431
--- /dev/null
+++ b/devtools/client/themes/images/toggle-tools@2x.png
Binary files differ
diff --git a/devtools/client/themes/images/tool-canvas.svg b/devtools/client/themes/images/tool-canvas.svg
new file mode 100644
index 000000000..7edebeaf0
--- /dev/null
+++ b/devtools/client/themes/images/tool-canvas.svg
@@ -0,0 +1,9 @@
+<!-- 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/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="#0b0b0b">
+ <g fill-rule="evenodd">
+ <path d="M1 2.007C1 1.45 1.45 1 2.007 1h11.986C14.55 1 15 1.45 15 2.007v11.986C15 14.55 14.55 15 13.993 15H2.007C1.45 15 1 14.55 1 13.993V2.007zM2 2h12v12H2V2z"/>
+ <path d="M3 3h2v2H3zM11 3h2v2h-2zM7 3h2v2H7zM3 7h2v2H3zM11 7h2v2h-2zM7 7h2v2H7zM5 5h2v2H5zM9 5h2v2H9zM3 11h2v2H3zM11 11h2v2h-2zM7 11h2v2H7zM5 9h2v2H5zM9 9h2v2H9z" opacity="0.5"/>
+ </g>
+</svg>
diff --git a/devtools/client/themes/images/tool-debugger-paused.svg b/devtools/client/themes/images/tool-debugger-paused.svg
new file mode 100644
index 000000000..8ac16b26f
--- /dev/null
+++ b/devtools/client/themes/images/tool-debugger-paused.svg
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="#5FC749">
+ <path d="M2 5v6c0 .109.039.342.144.553.15.297.374.447.856.447h9l-.78.375 4-5v1.25l-4-5L12 4H3c-.482 0-.707.15-.856.447A1.403 1.403 0 0 0 2 5zM1 5s0-2 2-2h9l4 5-4 5H3c-2 0-2-2-2-2V5z"/>
+</svg>
diff --git a/devtools/client/themes/images/tool-debugger.svg b/devtools/client/themes/images/tool-debugger.svg
new file mode 100644
index 000000000..9a8fac4d7
--- /dev/null
+++ b/devtools/client/themes/images/tool-debugger.svg
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="#0b0b0b">
+ <path d="M2 5v6c0 .109.039.342.144.553.15.297.374.447.856.447h9l-.78.375 4-5v1.25l-4-5L12 4H3c-.482 0-.707.15-.856.447A1.403 1.403 0 0 0 2 5zM1 5s0-2 2-2h9l4 5-4 5H3c-2 0-2-2-2-2V5z"/>
+</svg>
diff --git a/devtools/client/themes/images/tool-dom.svg b/devtools/client/themes/images/tool-dom.svg
new file mode 100644
index 000000000..88aecabf5
--- /dev/null
+++ b/devtools/client/themes/images/tool-dom.svg
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="#0b0b0b">
+ <path d="M6.052 1.13L1.164 5.57a.5.5 0 0 0 0 .74l5 4.56a.5.5 0 0 0 .673-.74l-5-4.559v.74l4.887-4.44a.5.5 0 0 0-.672-.741zM10.948 14.87l4.888-4.44a.5.5 0 0 0 0-.74l-5-4.56a.5.5 0 1 0-.673.74l5 4.559v-.74l-4.887 4.44a.5.5 0 0 0 .672.741z"/>
+</svg>
diff --git a/devtools/client/themes/images/tool-inspector.svg b/devtools/client/themes/images/tool-inspector.svg
new file mode 100644
index 000000000..a580f71a4
--- /dev/null
+++ b/devtools/client/themes/images/tool-inspector.svg
@@ -0,0 +1,7 @@
+<!-- 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/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="#0b0b0b">
+ <path d="M3 3.995v8.01c0-.01.005-.005.002-.005h9.996c-.001 0 .002-.003.002.005v-8.01c0 .01-.005.005-.002.005H3.002C3.003 4 3 4.003 3 3.995zm-1 0C2 3.445 2.456 3 3.002 3h9.996C13.55 3 14 3.456 14 3.995v8.01c0 .55-.456.995-1.002.995H3.002A1.005 1.005 0 0 1 2 12.005v-8.01z"/>
+ <path d="M8.5 3.5V2a.5.5 0 0 0-1 0v1.5a.5.5 0 0 0 1 0zM1 8.5h1a.5.5 0 0 0 0-1H1a.5.5 0 0 0 0 1zM14 8.5h1a.5.5 0 1 0 0-1h-1a.5.5 0 1 0 0 1zM8.5 14v-1.5a.5.5 0 1 0-1 0V14a.5.5 0 1 0 1 0z"/>
+</svg>
diff --git a/devtools/client/themes/images/tool-memory-active.svg b/devtools/client/themes/images/tool-memory-active.svg
new file mode 100644
index 000000000..81d787101
--- /dev/null
+++ b/devtools/client/themes/images/tool-memory-active.svg
@@ -0,0 +1,10 @@
+<!-- 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/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="#5FC749">
+ <path d="M4.727 8.055l-1.96-1a.5.5 0 0 0-.573.083L.655 8.602a.5.5 0 1 0 .69.725l1.539-1.465-.572.083 1.96 1a.5.5 0 1 0 .455-.89z"/>
+ <path d="M4.727 10.055l-1.96-1a.5.5 0 0 0-.573.083L.655 10.602a.5.5 0 1 0 .69.725l1.539-1.465-.572.083 1.96 1a.5.5 0 1 0 .455-.89zM11.727 10.945l1.961-1-.572-.083 1.54 1.465a.5.5 0 1 0 .689-.725l-1.54-1.464a.5.5 0 0 0-.571-.083l-1.961 1a.5.5 0 1 0 .454.89z"/>
+ <path d="M11.727 8.945l1.961-1-.572-.083 1.54 1.465a.5.5 0 1 0 .689-.725l-1.54-1.464a.5.5 0 0 0-.571-.083l-1.961 1a.5.5 0 1 0 .454.89z"/>
+ <path d="M11.727 6.945l1.961-1-.572-.083 1.54 1.465a.5.5 0 1 0 .689-.725l-1.54-1.464a.5.5 0 0 0-.571-.083l-1.961 1a.5.5 0 1 0 .454.89zM4.727 6.055l-1.96-1a.5.5 0 0 0-.573.083L.655 6.602a.5.5 0 1 0 .69.725l1.539-1.465-.572.083 1.96 1a.5.5 0 1 0 .455-.89z"/>
+ <path d="M5 3.002v9.996c0-.001.003.002-.003.002h6.006c-.006 0-.003-.003-.003-.002V3.002c0 .001-.003-.002.003-.002H4.997c.006 0 .003.003.003.002zm-1 0C4 2.45 4.453 2 4.997 2h6.006c.55 0 .997.456.997 1.002v9.996c0 .553-.453 1.002-.997 1.002H4.997C4.447 14 4 13.544 4 12.998V3.002z"/>
+</svg>
diff --git a/devtools/client/themes/images/tool-memory.svg b/devtools/client/themes/images/tool-memory.svg
new file mode 100644
index 000000000..fd9be2bbd
--- /dev/null
+++ b/devtools/client/themes/images/tool-memory.svg
@@ -0,0 +1,10 @@
+<!-- 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/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="#0b0b0b">
+ <path d="M4.727 8.055l-1.96-1a.5.5 0 0 0-.573.083L.655 8.602a.5.5 0 1 0 .69.725l1.539-1.465-.572.083 1.96 1a.5.5 0 1 0 .455-.89z"/>
+ <path d="M4.727 10.055l-1.96-1a.5.5 0 0 0-.573.083L.655 10.602a.5.5 0 1 0 .69.725l1.539-1.465-.572.083 1.96 1a.5.5 0 1 0 .455-.89zM11.727 10.945l1.961-1-.572-.083 1.54 1.465a.5.5 0 1 0 .689-.725l-1.54-1.464a.5.5 0 0 0-.571-.083l-1.961 1a.5.5 0 1 0 .454.89z"/>
+ <path d="M11.727 8.945l1.961-1-.572-.083 1.54 1.465a.5.5 0 1 0 .689-.725l-1.54-1.464a.5.5 0 0 0-.571-.083l-1.961 1a.5.5 0 1 0 .454.89z"/>
+ <path d="M11.727 6.945l1.961-1-.572-.083 1.54 1.465a.5.5 0 1 0 .689-.725l-1.54-1.464a.5.5 0 0 0-.571-.083l-1.961 1a.5.5 0 1 0 .454.89zM4.727 6.055l-1.96-1a.5.5 0 0 0-.573.083L.655 6.602a.5.5 0 1 0 .69.725l1.539-1.465-.572.083 1.96 1a.5.5 0 1 0 .455-.89z"/>
+ <path d="M5 3.002v9.996c0-.001.003.002-.003.002h6.006c-.006 0-.003-.003-.003-.002V3.002c0 .001-.003-.002.003-.002H4.997c.006 0 .003.003.003.002zm-1 0C4 2.45 4.453 2 4.997 2h6.006c.55 0 .997.456.997 1.002v9.996c0 .553-.453 1.002-.997 1.002H4.997C4.447 14 4 13.544 4 12.998V3.002z"/>
+</svg>
diff --git a/devtools/client/themes/images/tool-network.svg b/devtools/client/themes/images/tool-network.svg
new file mode 100644
index 000000000..48d1361d8
--- /dev/null
+++ b/devtools/client/themes/images/tool-network.svg
@@ -0,0 +1,9 @@
+<!-- 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/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="#0b0b0b">
+ <rect fill-opacity=".5" x="2" y="3" width="8" height="1" rx=".5"/>
+ <rect x="6" y="6" width="8" height="1" rx=".5"/>
+ <rect fill-opacity=".5" x="4" y="9" width="8" height="1" rx=".5"/>
+ <rect x="2" y="12" width="5" height="1" rx=".5"/>
+</svg>
diff --git a/devtools/client/themes/images/tool-options.svg b/devtools/client/themes/images/tool-options.svg
new file mode 100644
index 000000000..e8516bdfa
--- /dev/null
+++ b/devtools/client/themes/images/tool-options.svg
@@ -0,0 +1,7 @@
+<!-- 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/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="#0b0b0b">
+ <path d="M8.513 3.416v-.918A.502.502 0 0 0 8.012 2h-.999c-.273 0-.5.226-.5.498V4.07l-.6.262c-.274.12-.534.27-.775.449l-.527.39-.567-.328-.796-.46a.502.502 0 0 0-.682.185l-.5.864a.504.504 0 0 0 .182.683l.795.46.567.326-.073.65a4.055 4.055 0 0 0 0 .898l.073.65-.567.327-.795.459a.502.502 0 0 0-.181.683l.499.864c.137.237.446.32.682.185l.796-.46.567-.327.527.39c.24.177.5.328.775.448l.6.262v1.572c0 .272.225.498.5.498h.999c.273 0 .5-.226.5-.498V11.93l.6-.262c.274-.12.534-.27.775-.449l.527-.39.567.328.796.46a.502.502 0 0 0 .682-.185l.5-.864a.504.504 0 0 0-.182-.683l-.795-.46-.567-.326.073-.65a4.055 4.055 0 0 0 0-.898l-.073-.65.567-.327.795-.459a.502.502 0 0 0 .181-.683l-.499-.864a.504.504 0 0 0-.682-.185l-.796.46-.567.327-.527-.39c-.24-.177-.5-.328-.775-.448l-.6-.262v-.654zm1 0c.345.15.67.34.968.56l.796-.459a1.504 1.504 0 0 1 2.048.55l.5.865a1.502 1.502 0 0 1-.548 2.05l-.795.459a5.055 5.055 0 0 1 0 1.118l.795.46c.717.414.958 1.337.547 2.049l-.499.864a1.502 1.502 0 0 1-2.048.55l-.796-.458c-.299.22-.623.41-.968.56v.918c0 .827-.679 1.498-1.501 1.498h-.999c-.829 0-1.5-.675-1.5-1.498v-.918c-.345-.15-.67-.34-.97-.56l-.795.459a1.504 1.504 0 0 1-2.048-.55l-.5-.865a1.502 1.502 0 0 1 .548-2.05l.795-.459a5.055 5.055 0 0 1 0-1.118l-.795-.46A1.504 1.504 0 0 1 1.2 4.932l.499-.864a1.502 1.502 0 0 1 2.048-.55l.796.458c.299-.22.624-.41.969-.56v-.918c0-.827.678-1.498 1.5-1.498h.999c.829 0 1.5.675 1.5 1.498v.918z"/>
+ <path d="M7.5 9a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm0 1a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/>
+</svg>
diff --git a/devtools/client/themes/images/tool-profiler-active.svg b/devtools/client/themes/images/tool-profiler-active.svg
new file mode 100644
index 000000000..2c77b1aad
--- /dev/null
+++ b/devtools/client/themes/images/tool-profiler-active.svg
@@ -0,0 +1,9 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="#5FC749" fill-rule="evenodd">
+ <path d="M15 9.004C14.51 12.394 11.578 15 8.035 15 4.15 15 1 11.866 1 8s3.15-7 7.036-7c1.941 0 3.7.783 4.972 2.048l-.709.709A6.027 6.027 0 0 0 8.036 2c-3.33 0-6.03 2.686-6.03 6s2.7 6 6.03 6a6.023 6.023 0 0 0 5.946-4.993l1.017-.003z"/>
+ <path d="M4.137 9H3.1a5.002 5.002 0 0 0 9.8 0h-.965a4.023 4.023 0 0 1-3.9 3 4.023 4.023 0 0 1-3.898-3z" fill-opacity=".5"/>
+ <path d="M8.036 11a2.994 2.994 0 0 0 2.987-3c0-1.657-1.338-3-2.987-3a2.994 2.994 0 0 0-2.988 3c0 1.657 1.338 3 2.988 3zm0-1c1.11 0 2.011-.895 2.011-2s-.9-2-2.011-2c-1.111 0-2.012.895-2.012 2s.9 2 2.012 2z"/>
+ <path d="M10.354 6.354l4-4a.5.5 0 0 0-.708-.708l-4 4a.5.5 0 1 0 .708.708z"/>
+</svg>
diff --git a/devtools/client/themes/images/tool-profiler.svg b/devtools/client/themes/images/tool-profiler.svg
new file mode 100644
index 000000000..92c33bb06
--- /dev/null
+++ b/devtools/client/themes/images/tool-profiler.svg
@@ -0,0 +1,9 @@
+<!-- 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/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="#0b0b0b" fill-rule="evenodd">
+ <path d="M15 9.004C14.51 12.394 11.578 15 8.035 15 4.15 15 1 11.866 1 8s3.15-7 7.036-7c1.941 0 3.7.783 4.972 2.048l-.709.709A6.027 6.027 0 0 0 8.036 2c-3.33 0-6.03 2.686-6.03 6s2.7 6 6.03 6a6.023 6.023 0 0 0 5.946-4.993l1.017-.003z"/>
+ <path d="M4.137 9H3.1a5.002 5.002 0 0 0 9.8 0h-.965a4.023 4.023 0 0 1-3.9 3 4.023 4.023 0 0 1-3.898-3z" fill-opacity=".5"/>
+ <path d="M8.036 11a2.994 2.994 0 0 0 2.987-3c0-1.657-1.338-3-2.987-3a2.994 2.994 0 0 0-2.988 3c0 1.657 1.338 3 2.988 3zm0-1c1.11 0 2.011-.895 2.011-2s-.9-2-2.011-2c-1.111 0-2.012.895-2.012 2s.9 2 2.012 2z"/>
+ <path d="M10.354 6.354l4-4a.5.5 0 0 0-.708-.708l-4 4a.5.5 0 1 0 .708.708z"/>
+</svg>
diff --git a/devtools/client/themes/images/tool-scratchpad.svg b/devtools/client/themes/images/tool-scratchpad.svg
new file mode 100644
index 000000000..a75985cf3
--- /dev/null
+++ b/devtools/client/themes/images/tool-scratchpad.svg
@@ -0,0 +1,7 @@
+<!-- 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/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="#0b0b0b">
+ <path d="M5 1.5a.5.5 0 0 0-1 0v2a.5.5 0 0 0 1 0v-2zM8.5 3.5v-2a.5.5 0 0 0-1 0v2a.5.5 0 0 0 1 0zM12 3.5v-2a.5.5 0 1 0-1 0v2a.5.5 0 1 0 1 0zM5 7h4a.5.5 0 0 0 0-1H5a.5.5 0 0 0 0 1zM5 11h2a.5.5 0 1 0 0-1H5a.5.5 0 1 0 0 1zM6 9h5a.5.5 0 1 0 0-1H6a.5.5 0 0 0 0 1z"/>
+ <path d="M3 3.996v9.008c0-.003 0-.004.002-.004h9.996c-.001 0 .002-.003.002.004V3.996c0 .003 0 .004-.002.004H3.002C3.003 4 3 4.003 3 3.996zm-1 0C2 3.446 2.456 3 3.002 3h9.996A.998.998 0 0 1 14 3.996v9.008c0 .55-.456.996-1.002.996H3.002A.998.998 0 0 1 2 13.004V3.996z"/>
+</svg>
diff --git a/devtools/client/themes/images/tool-shadereditor.svg b/devtools/client/themes/images/tool-shadereditor.svg
new file mode 100644
index 000000000..f668e5f67
--- /dev/null
+++ b/devtools/client/themes/images/tool-shadereditor.svg
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="#0b0b0b">
+ <path d="M2 4v8h12V4H2zm0-1h12a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1z"/>
+ <circle cx="1" cy="3" r="1"/>
+ <circle cx="1" cy="13" r="1"/>
+ <circle cx="15" cy="13" r="1"/>
+ <circle cx="15" cy="3" r="1"/>
+ <path d="M1.215 3.911l13 9 .411.285.57-.822-.411-.285-13-9-.411-.285-.57.822z"/>
+ <path fill-opacity=".3" d="M8 5h2v2H8zM8 8h2v2L9 8.711zM5 5.962V5h2v2h-.828l-.729-.368zM11 5h2v2h-2zM11 8h2v2h-2z"/>
+</svg>
diff --git a/devtools/client/themes/images/tool-storage.svg b/devtools/client/themes/images/tool-storage.svg
new file mode 100644
index 000000000..7b8863662
--- /dev/null
+++ b/devtools/client/themes/images/tool-storage.svg
@@ -0,0 +1,7 @@
+<!-- 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/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="#0b0b0b">
+ <path d="M7.5 7.556c3.006 0 5.5-1.136 5.5-2.778C13 3.136 10.506 2 7.5 2S2 3.136 2 4.778C2 6.42 4.494 7.556 7.5 7.556zm0-1c-2.517 0-4.5-.903-4.5-1.778S4.983 3 7.5 3s4.5.903 4.5 1.778-1.983 1.778-4.5 1.778zM7.5 14.445c3.006 0 5.5-1.137 5.5-2.778 0-.878-.595-1.606-1.657-2.081-.244-.11-.473-.107-.778-.033-.056.014-.565.158-.765.205-.626.148-1.342.231-2.3.231-.973 0-1.683-.082-2.273-.225a18.574 18.574 0 0 1-.673-.193c-.277-.076-.479-.089-.707-.005l-.035.014C2.638 10.064 2 10.756 2 11.667c0 1.641 2.494 2.778 5.5 2.778zm0-1c-2.517 0-4.5-.904-4.5-1.778 0-.432.354-.816 1.194-1.163h-.002c-.012.005.003.006.097.032-.056-.016.474.144.702.2.669.162 1.458.253 2.509.253 1.035 0 1.828-.092 2.53-.257.228-.054.74-.2.77-.207a.756.756 0 0 1 .134-.027c.734.329 1.066.735 1.066 1.169 0 .874-1.983 1.778-4.5 1.778z"/>
+ <path d="M7.5 10.945c3.006 0 5.5-1.137 5.5-2.778 0-.873-.62-1.601-1.693-2.082-.244-.109-.472-.106-.773-.032-.051.013-.551.158-.75.206-.615.147-1.326.23-2.284.23-.973 0-1.68-.082-2.265-.225a17.077 17.077 0 0 1-.66-.19c-.27-.076-.467-.092-.692-.015l-.054.02C2.65 6.568 2 7.259 2 8.168c0 1.641 2.494 2.778 5.5 2.778zm0-1C4.983 9.945 3 9.04 3 8.167c0-.426.364-.813 1.21-1.163l-.003.001c-.011.004.005.005.099.032-.079-.022.465.143.69.198.665.163 1.452.254 2.504.254 1.036 0 1.825-.092 2.517-.258.228-.054.733-.2.758-.207a.766.766 0 0 1 .124-.026c.748.335 1.101.75 1.101 1.169 0 .874-1.983 1.778-4.5 1.778z"/>
+</svg>
diff --git a/devtools/client/themes/images/tool-styleeditor.svg b/devtools/client/themes/images/tool-styleeditor.svg
new file mode 100644
index 000000000..dbe5ddb6b
--- /dev/null
+++ b/devtools/client/themes/images/tool-styleeditor.svg
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="#0b0b0b">
+ <path d="M5.5 2C3.565 2 2.806 3.12 3.065 4.587c.239 1.346.117 1.76-.435 2.39l-.054.06c-.252.288-.39.474-.523.74L1.94 8l.112.224c.132.265.27.45.523.739l.054.06c.552.63.674 1.044.435 2.39C2.802 12.904 3.527 14 5.5 14a.5.5 0 1 0 0-1c-1.291 0-1.614-.487-1.45-1.413.292-1.65.081-2.37-.669-3.223l-.053-.06c-.2-.229-.296-.357-.38-.528v.448c.084-.17.18-.299.38-.528l.053-.06c.75-.854.961-1.573.67-3.223C3.89 3.515 4.24 3 5.5 3a.5.5 0 1 0 0-1zM10.5 3c1.26 0 1.609.515 1.45 1.413-.292 1.65-.081 2.37.669 3.223l.053.06c.2.229.296.357.38.528v-.448c-.084.17-.18.299-.38.528l-.053.06c-.75.854-.961 1.573-.67 3.223.165.926-.158 1.413-1.449 1.413a.5.5 0 1 0 0 1c1.973 0 2.698-1.096 2.435-2.587-.239-1.346-.117-1.76.435-2.39l.054-.06c.252-.288.39-.474.523-.74L14.06 8l-.112-.224c-.132-.265-.27-.45-.523-.739l-.054-.06c-.552-.63-.674-1.044-.435-2.39C13.194 3.12 12.435 2 10.5 2a.5.5 0 0 0 0 1z"/>
+</svg>
diff --git a/devtools/client/themes/images/tool-webaudio.svg b/devtools/client/themes/images/tool-webaudio.svg
new file mode 100644
index 000000000..8275dc200
--- /dev/null
+++ b/devtools/client/themes/images/tool-webaudio.svg
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" stroke="#0b0b0b">
+ <path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" d="M2.7 8.6c-.1-2.9.2-4.1 1.4-4.1 1.8 0 .6 6.6 2.5 6.6s1-6.4 3-6.4.7 6.4 2.7 6.4c1.4 0 1.5-3.5 1.5-3.5"/>
+</svg>
diff --git a/devtools/client/themes/images/tool-webconsole.svg b/devtools/client/themes/images/tool-webconsole.svg
new file mode 100644
index 000000000..d6752f6c4
--- /dev/null
+++ b/devtools/client/themes/images/tool-webconsole.svg
@@ -0,0 +1,7 @@
+<!-- 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/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="#0b0b0b" fill-rule="evenodd">
+ <path d="M14 4V3H2v1h12zm0 1v8H2V5h12zM1 3.002C1 2.45 1.45 2 2.007 2h11.986A1.01 1.01 0 0 1 15 3.002v9.996C15 13.55 14.55 14 13.993 14H2.007A1.01 1.01 0 0 1 1 12.998V3.002z"/>
+ <path d="M4.09 7.859l2.062 2-.006-.713-2.061 2.062a.5.5 0 0 0 .707.707l2.062-2.061a.5.5 0 0 0-.006-.713l-2.061-2a.5.5 0 1 0-.697.718z"/>
+</svg>
diff --git a/devtools/client/themes/images/tracer-icon.png b/devtools/client/themes/images/tracer-icon.png
new file mode 100644
index 000000000..2b5aaf0aa
--- /dev/null
+++ b/devtools/client/themes/images/tracer-icon.png
Binary files differ
diff --git a/devtools/client/themes/images/tracer-icon@2x.png b/devtools/client/themes/images/tracer-icon@2x.png
new file mode 100644
index 000000000..33c4c7d7f
--- /dev/null
+++ b/devtools/client/themes/images/tracer-icon@2x.png
Binary files differ
diff --git a/devtools/client/themes/images/vview-delete.png b/devtools/client/themes/images/vview-delete.png
new file mode 100644
index 000000000..6c0ef603c
--- /dev/null
+++ b/devtools/client/themes/images/vview-delete.png
Binary files differ
diff --git a/devtools/client/themes/images/vview-delete@2x.png b/devtools/client/themes/images/vview-delete@2x.png
new file mode 100644
index 000000000..d0718a1d6
--- /dev/null
+++ b/devtools/client/themes/images/vview-delete@2x.png
Binary files differ
diff --git a/devtools/client/themes/images/vview-edit.png b/devtools/client/themes/images/vview-edit.png
new file mode 100644
index 000000000..b3009981f
--- /dev/null
+++ b/devtools/client/themes/images/vview-edit.png
Binary files differ
diff --git a/devtools/client/themes/images/vview-edit@2x.png b/devtools/client/themes/images/vview-edit@2x.png
new file mode 100644
index 000000000..d0923fd0a
--- /dev/null
+++ b/devtools/client/themes/images/vview-edit@2x.png
Binary files differ
diff --git a/devtools/client/themes/images/vview-lock.png b/devtools/client/themes/images/vview-lock.png
new file mode 100644
index 000000000..e9b257e3a
--- /dev/null
+++ b/devtools/client/themes/images/vview-lock.png
Binary files differ
diff --git a/devtools/client/themes/images/vview-lock@2x.png b/devtools/client/themes/images/vview-lock@2x.png
new file mode 100644
index 000000000..11d86275f
--- /dev/null
+++ b/devtools/client/themes/images/vview-lock@2x.png
Binary files differ
diff --git a/devtools/client/themes/images/vview-open-inspector.png b/devtools/client/themes/images/vview-open-inspector.png
new file mode 100644
index 000000000..877d195c9
--- /dev/null
+++ b/devtools/client/themes/images/vview-open-inspector.png
Binary files differ
diff --git a/devtools/client/themes/images/vview-open-inspector@2x.png b/devtools/client/themes/images/vview-open-inspector@2x.png
new file mode 100644
index 000000000..207e5376f
--- /dev/null
+++ b/devtools/client/themes/images/vview-open-inspector@2x.png
Binary files differ
diff --git a/devtools/client/themes/images/webconsole.svg b/devtools/client/themes/images/webconsole.svg
new file mode 100644
index 000000000..6c21e549e
--- /dev/null
+++ b/devtools/client/themes/images/webconsole.svg
@@ -0,0 +1,101 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="72" height="60" viewBox="0 0 72 60">
+ <defs>
+ <rect id="glyphShape-colorSwatch" width="8" height="8" ry="2" rx="2"/>
+ <rect id="glyphShape-colorSwatch-border" width="10" height="10" ry="2" rx="2"/>
+ <polygon id="glyphShape-errorX" points="9.9,8.5 8.5,9.9 6,7.4 3.6,9.8 2.2,8.4 4.6,6 2.2,3.6 3.6,2.2 6,4.6 8.4,2.2 9.8,3.6 7.4,6"/>
+ <path id="glyphShape-warningTriangle" d="M9.9,8.6l-3.1-6C6.6,2.2,6.3,2,6,2C5.7,2,5.4,2.2,5.2,2.5l-3.1,6C2,8.9,2,9.3,2.1,9.6C2.3,9.8,2.6,10,2.9,10 h6.1c0.4,0,0.6-0.2,0.8-0.4C10,9.3,10,8.9,9.9,8.6z"/>
+ <path id="glyphShape-exclamationPoint" d="M6,7.7c-0.6,0-1,0.4-1,0.8C5,9,5.4,9.3,6,9.3c0.6,0,1-0.4,1-0.8 C7,8.1,6.6,7.7,6,7.7z M6,7c0.6,0,1-0.4,1-1V5c0-0.6-0.4-1-1-1S5,4.4,5,5v1C5,6.6,5.4,7,6,7z"/>
+ <circle id="glyphShape-infoCircle" cx="6" cy="6" r="4"/>
+ <path id="glyphShape-infoGlyph" d="M6,6C5.4,6,5,6.4,5,7v1c0,0.6,0.4,1,1,1s1-0.4,1-1V7C7,6.4,6.6,6,6,6z M6,5c0.6,0,1-0.4,1-1S6.6,3,6,3S5,3.4,5,4S5.4,5,6,5z"/>
+ <style>
+ .icon-colorSwatch-border {
+ fill: #fff;
+ fill-opacity: .7;
+ }
+ .icon-colorSwatch-network {
+ fill: #000;
+ }
+ .icon-colorSwatch-css {
+ fill: #00b6f0;
+ }
+ .icon-colorSwatch-js {
+ fill: #fb9500;
+ }
+ .icon-colorSwatch-logging {
+ fill: #808080;
+ }
+ .icon-colorSwatch-security {
+ fill: #ec1e0d;
+ }
+ .icon-glyphOverlay {
+ fill: #fff;
+ }
+
+ #icon-indicator-input {
+ fill: #8fa1b2;
+ }
+ #icon-indicator-output {
+ fill: #667380;
+ }
+ #light-icons:target #icon-indicator-input {
+ fill: #45494d;
+ }
+ #light-icons:target #icon-indicator-output {
+ fill: #8a9199;
+ }
+ </style>
+ </defs>
+ <g id="icon-colorSwatch-network">
+ <use xlink:href="#glyphShape-colorSwatch-border" class="icon-colorSwatch-border" x="1" y="1"/>
+ <use xlink:href="#glyphShape-colorSwatch" class="icon-colorSwatch-network" x="2" y="2"/>
+ </g>
+ <g id="icon-colorSwatch-css" transform="translate(0 12)">
+ <use xlink:href="#glyphShape-colorSwatch-border" class="icon-colorSwatch-border" x="1" y="1"/>
+ <use xlink:href="#glyphShape-colorSwatch" class="icon-colorSwatch-css" x="2" y="2"/>
+ </g>
+ <g id="icon-colorSwatch-js" transform="translate(0 24)">
+ <use xlink:href="#glyphShape-colorSwatch-border" class="icon-colorSwatch-border" x="1" y="1"/>
+ <use xlink:href="#glyphShape-colorSwatch" class="icon-colorSwatch-js" x="2" y="2"/>
+ </g>
+ <g id="icon-colorSwatch-logging" transform="translate(0 36)">
+ <use xlink:href="#glyphShape-colorSwatch-border" class="icon-colorSwatch-border" x="1" y="1"/>
+ <use xlink:href="#glyphShape-colorSwatch" class="icon-colorSwatch-logging" x="2" y="2"/>
+ </g>
+ <g id="icon-colorSwatch-security" transform="translate(0 48)">
+ <use xlink:href="#glyphShape-colorSwatch-border" class="icon-colorSwatch-border" x="1" y="1"/>
+ <use xlink:href="#glyphShape-colorSwatch" class="icon-colorSwatch-security" x="2" y="2"/>
+ </g>
+ <use xlink:href="#glyphShape-errorX" id="icon-errorX-network" class="icon-colorSwatch-network" transform="translate(12)"/>
+ <use xlink:href="#glyphShape-errorX" id="icon-errorX-css" class="icon-colorSwatch-css" transform="translate(12 12)"/>
+ <use xlink:href="#glyphShape-errorX" id="icon-errorX-js" class="icon-colorSwatch-js" transform="translate(12 24)"/>
+ <use xlink:href="#glyphShape-errorX" id="icon-errorX-logging" class="icon-colorSwatch-logging" transform="translate(12 36)"/>
+ <use xlink:href="#glyphShape-errorX" id="icon-errorX-security" class="icon-colorSwatch-security" transform="translate(12 48)"/>
+ <g id="icon-warningTriangle-css" transform="translate(24 12)">
+ <use xlink:href="#glyphShape-warningTriangle" class="icon-colorSwatch-css"/>
+ <use xlink:href="#glyphShape-exclamationPoint" class="icon-glyphOverlay"/>
+ </g>
+ <g id="icon-warningTriangle-js" transform="translate(24 24)">
+ <use xlink:href="#glyphShape-warningTriangle" class="icon-colorSwatch-js"/>
+ <use xlink:href="#glyphShape-exclamationPoint" class="icon-glyphOverlay"/>
+ </g>
+ <g id="icon-warningTriangle-logging" transform="translate(24 36)">
+ <use xlink:href="#glyphShape-warningTriangle" class="icon-colorSwatch-logging"/>
+ <use xlink:href="#glyphShape-exclamationPoint" class="icon-glyphOverlay"/>
+ </g>
+ <g id="icon-warningTriangle-security" transform="translate(24 48)">
+ <use xlink:href="#glyphShape-warningTriangle" class="icon-colorSwatch-security"/>
+ <use xlink:href="#glyphShape-exclamationPoint" class="icon-glyphOverlay"/>
+ </g>
+ <g id="icon-infoCircle-logging" transform="translate(36 36)">
+ <use xlink:href="#glyphShape-infoCircle" class="icon-colorSwatch-logging"/>
+ <use xlink:href="#glyphShape-infoGlyph" class="icon-glyphOverlay"/>
+ </g>
+ <g id="light-icons">
+ <path id="icon-indicator-input" d="M6.5,1.2L5.4,2.3L9,6L5.3,9.7l1.1,1.1L11,6L6.5,1.2z M1.5,1.2 L0.4,2.3L4,6L0.3,9.7l1.1,1.1L6,6L1.5,1.2z" transform="translate(48 36)"/>
+ <polygon id="icon-indicator-output" points="10,5 4.3,5 6.8,2.4 5.5,1.2 1,6 5.5,10.8 6.9,9.6 4.3,7 10,7" transform="translate(60 36)"/>
+ </g>
+</svg> \ No newline at end of file
diff --git a/devtools/client/themes/inspector.css b/devtools/client/themes/inspector.css
new file mode 100644
index 000000000..28a959ab8
--- /dev/null
+++ b/devtools/client/themes/inspector.css
@@ -0,0 +1,216 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+:root {
+ --eyedropper-image: url(images/command-eyedropper.svg);
+}
+
+.theme-firebug {
+ --eyedropper-image: url(images/firebug/command-eyedropper.svg);
+}
+
+:root.theme-light {
+ --breadcrumbs-border-color: #f3f3f3;
+}
+
+:root.theme-dark {
+ --breadcrumbs-border-color: #454d5d;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+/* Make sure to hide scroll bars for the parent window */
+window {
+ overflow: hidden;
+}
+
+/* The main Inspector panel container. */
+.inspector-responsive-container {
+ height: 100vh;
+}
+
+/* The main panel layout. This area consists of a toolbar, markup view
+ and breadcrumbs bar. */
+#inspector-main-content {
+ /* Subtract 1 pixel from the panel height. It's puzzling why this
+ is needed, but if not presented the entire Inspector panel
+ content jumps 1 pixel up when the Toolbox is opened. */
+ height: calc(100% - 1px);
+ /* This min-width avoids a visual glitch when moving the splitter quickly to the left.
+ See bug 1307408 comment #12. */
+ min-width: 125px;
+ display: flex;
+ flex-direction: column;
+ flex: 1 1 auto;
+}
+
+/* Inspector Panel Splitter */
+
+#inspector-splitter-box {
+ height: 100vh;
+ width: 100vw;
+ position: fixed;
+}
+
+/* Minimum dimensions for the Inspector splitter areas. */
+#inspector-splitter-box .uncontrolled,
+#inspector-splitter-box .controlled {
+ min-height: 50px;
+ min-width: 50px;
+ overflow-x: hidden;
+}
+
+/* Set a minimum width of 200px for tab content to avoid breaking the layout when resizing
+ the sidebar tab to small width. If a specific panel supports smaller width, this should
+ be overridden on a panel-by-panel basis */
+.inspector-tabpanel {
+ min-width: 200px;
+}
+
+#inspector-splitter-box .controlled.pane-collapsed {
+ visibility: collapse;
+}
+
+/* Use flex layout for the Inspector toolbar. For now, it's done
+ specifically for the Inspector toolbar since general rule applied
+ on .devtools-toolbar breaks breadcrumbs and also toolbars in other
+ panels (e.g. webconsole, debugger), these are not ready for HTML
+ layout yet. */
+#inspector-toolbar.devtools-toolbar {
+ display: flex;
+}
+
+#inspector-toolbar.devtools-toolbar .devtools-toolbar-spacer {
+ flex-grow: 1;
+ display: inline-block;
+}
+
+/* Add element toolbar button */
+#inspector-element-add-button::before {
+ background-image: url("chrome://devtools/skin/images/add.svg");
+ list-style-image: url("chrome://devtools/skin/images/add.svg");
+ -moz-user-focus: normal;
+}
+
+#inspector-searchlabel {
+ overflow: hidden;
+ margin-inline-end: 2px;
+}
+
+#inspector-search {
+ flex: unset;
+}
+
+/* Eyedropper toolbar button */
+
+#inspector-eyedropper-toggle {
+ /* Required to display tooltip when eyedropper is disabled in non-HTML documents */
+ pointer-events: auto;
+}
+
+#inspector-eyedropper-toggle::before {
+ background-image: var(--eyedropper-image);
+}
+
+#inspector-sidebar-toggle-box {
+ line-height: initial;
+}
+
+#inspector-breadcrumbs-toolbar {
+ padding: 0px;
+ border-bottom-width: 0px;
+ border-top-width: 1px;
+ border-top-color: var(--breadcrumbs-border-color);
+ /* Bug 1262668 - Use the same background as the body so the breadcrumbs toolbar doesn't
+ get mistaken as a splitter */
+ background-color: var(--theme-body-background);
+ display: block;
+ position: relative;
+}
+
+#inspector-breadcrumbs-toolbar,
+#inspector-breadcrumbs-toolbar * {
+ box-sizing: border-box;
+}
+
+#inspector-breadcrumbs {
+ display: flex;
+
+ /* Break out of the XUL flexbox, so the splitter can still shrink the
+ markup view even if the contents of the breadcrumbs are wider than
+ the new width. */
+ position: absolute;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ right: 0;
+}
+
+#inspector-breadcrumbs .scrollbutton-up,
+#inspector-breadcrumbs .scrollbutton-down {
+ flex: 0;
+ display: flex;
+ align-items: center;
+}
+
+#inspector-breadcrumbs .html-arrowscrollbox-inner {
+ flex: 1;
+ display: flex;
+ overflow: hidden;
+}
+
+#inspector-breadcrumbs .breadcrumbs-widget-item {
+ white-space: nowrap;
+ flex-shrink: 0;
+ font: message-box;
+}
+
+#inspector-sidebar-container {
+ overflow: hidden;
+ position: relative;
+ height: 100%;
+}
+
+#inspector-sidebar {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+}
+
+/* Override `-moz-user-focus:ignore;` from toolkit/content/minimal-xul.css */
+.inspector-tabpanel > * {
+ -moz-user-focus: normal;
+}
+
+/* "no results" warning message displayed in the ruleview and in the computed view */
+
+#ruleview-no-results,
+#computedview-no-results {
+ color: var(--theme-body-color-inactive);
+ text-align: center;
+ margin: 5px;
+}
+
+/* Markup Box */
+
+iframe {
+ border: 0;
+}
+
+#markup-box {
+ width: 100%;
+ flex: 1;
+ min-height: 0;
+}
+
+#markup-box > iframe {
+ height: 100%;
+ width: 100%;
+}
+
diff --git a/devtools/client/themes/jit-optimizations.css b/devtools/client/themes/jit-optimizations.css
new file mode 100644
index 000000000..363810504
--- /dev/null
+++ b/devtools/client/themes/jit-optimizations.css
@@ -0,0 +1,108 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+/**
+ * JIT View
+ */
+
+#jit-optimizations-view {
+ width: 350px;
+ min-width: 200px;
+ white-space: nowrap;
+ --jit-tree-row-height: 14;
+ --jit-tree-header-height: 16;
+}
+
+/* Override layout styles applied by minimal-xul.css */
+#jit-optimizations-view div {
+ display: block;
+}
+#jit-optimizations-view span {
+ display: inline-block;
+}
+
+#jit-optimizations-view > div {
+ /* For elements that need to flex to fill the available space and/or
+ * scroll on overflow, we need to use the old flexbox model, since the
+ * parent nodes are in the XUL namespace. The new flexbox model can't
+ * properly compute dimensions and will ignore `flex: ${number}` properties,
+ * since no other parent node has a flex display. */
+ display: -moz-box;
+ -moz-box-flex: 1;
+ -moz-box-orient: vertical;
+}
+
+#jit-optimizations-view .optimization-header,
+#jit-optimizations-view .tree * {
+ /* We can, however, display child nodes as flex to take advantage of
+ * horizontal/vertical inlining. */
+ display: flex;
+}
+
+#jit-optimizations-view .optimization-header {
+ height: var(--jit-tree-header-height);
+ padding: 2px 5px;
+ background-color: var(--theme-tab-toolbar-background);
+}
+
+#jit-optimizations-view .header-title {
+ font-weight: bold;
+ padding-inline-end: 7px;
+}
+
+#jit-optimizations-view .tree {
+ display: -moz-box;
+ -moz-box-flex: 1;
+ -moz-box-orient: vertical;
+ overflow: auto;
+ background-color: var(--theme-body-background);
+}
+
+#jit-optimizations-view .tree-node {
+ height: var(--jit-tree-row-height);
+}
+
+#jit-optimizations-view .tree-node button {
+ display: none;
+}
+
+#jit-optimizations-view .optimization-outcome.success {
+ color: var(--theme-highlight-green);
+}
+#jit-optimizations-view .optimization-outcome.failure {
+ color: var(--theme-highlight-red);
+}
+
+.theme-dark .opt-icon::before {
+ background-image: url(chrome://devtools/skin/images/webconsole.svg);
+}
+.theme-light .opt-icon::before {
+ background-image: url(chrome://devtools/skin/images/webconsole.svg#light-icons);
+}
+
+.opt-icon::before {
+ display: inline-block;
+ content: "";
+ background-repeat: no-repeat;
+ background-size: 72px 60px;
+ /* show grey "i" bubble by default */
+ background-position: -36px -36px;
+ width: 10px;
+ height: 10px;
+ max-height: 12px;
+}
+
+.opt-icon::before {
+ margin: 1px 6px 0 0;
+}
+
+.opt-icon.warning::before {
+ background-position: -24px -24px;
+}
+
+/* Frame Component */
+.frame-link {
+ margin-inline-start: 7px;
+}
diff --git a/devtools/client/themes/layout.css b/devtools/client/themes/layout.css
new file mode 100644
index 000000000..d04d3ff4c
--- /dev/null
+++ b/devtools/client/themes/layout.css
@@ -0,0 +1,14 @@
+/* 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/. */
+
+#layoutview-container {
+ height: 100%;
+ width: 100%;
+}
+
+.layoutview-no-grids {
+ font-style: italic;
+ text-align: center;
+ padding: 0.5em;
+}
diff --git a/devtools/client/themes/light-theme.css b/devtools/client/themes/light-theme.css
new file mode 100644
index 000000000..7604123e8
--- /dev/null
+++ b/devtools/client/themes/light-theme.css
@@ -0,0 +1,338 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+@import url(resource://devtools/client/themes/variables.css);
+@import url(resource://devtools/client/themes/common.css);
+@import url(toolbars.css);
+@import url(tooltips.css);
+
+body {
+ margin: 0;
+}
+
+.theme-body {
+ background: var(--theme-body-background);
+ color: var(--theme-body-color);
+}
+
+.theme-sidebar {
+ background: var(--theme-sidebar-background);
+ color: var(--theme-body-color);
+}
+
+::-moz-selection {
+ background-color: var(--theme-selection-background);
+ color: var(--theme-selection-color);
+}
+
+.theme-bg-darker {
+ background: var(--theme-selection-background-semitransparent);
+}
+
+.theme-selected,
+.CodeMirror-hint-active {
+ background-color: var(--theme-selection-background);
+ color: var(--theme-selection-color);
+}
+
+.theme-bg-contrast,
+.variable-or-property:not([overridden])[changed] {
+ background: var(--theme-contrast-background);
+}
+
+.theme-link,
+.cm-s-mozilla .cm-link,
+.CodeMirror-Tern-type {
+ color: var(--theme-highlight-blue);
+}
+
+/*
+ * FIXME: http://bugzil.la/575675 CSS links without :visited set cause assertion
+ * failures in debug builds.
+ */
+.theme-link:visited,
+.cm-s-mozilla .cm-link:visited {
+ color: var(--theme-highlight-blue);
+}
+
+.theme-comment,
+.cm-s-mozilla .cm-meta,
+.cm-s-mozilla .cm-hr,
+.cm-s-mozilla .cm-comment,
+.variable-or-property .token-undefined,
+.variable-or-property .token-null,
+.CodeMirror-Tern-completion-unknown:before {
+ color: var(--theme-comment);
+}
+
+.theme-gutter {
+ background-color: var(--theme-tab-toolbar-background);
+ color: var(--theme-content-color3);
+ border-color: var(--theme-splitter-color);
+}
+
+.theme-separator { /* grey */
+ border-color: #cddae5;
+}
+
+.cm-s-mozilla .cm-unused-line {
+ text-decoration: line-through;
+ text-decoration-color: var(--theme-highlight-bluegrey);
+}
+
+.cm-s-mozilla .cm-executed-line {
+ background-color: #fcfffc;
+}
+
+.theme-fg-color1,
+.cm-s-mozilla .cm-number,
+.variable-or-property .token-number,
+.variable-or-property[return] > .title > .name,
+.variable-or-property[scope] > .title > .name {
+ color: var(--theme-highlight-purple);
+}
+
+.CodeMirror-Tern-completion-number:before {
+ background-color: hsl(72,100%,27%);
+}
+
+.theme-fg-color2,
+.cm-s-mozilla .cm-attribute,
+.cm-s-mozilla .cm-builtin,
+.cm-s-mozilla .cm-property,
+.variables-view-variable > .title > .name {
+ color: var(--theme-highlight-red);
+}
+
+.cm-s-mozilla .cm-def {
+ color: var(--theme-body-color);
+}
+
+.CodeMirror-Tern-completion-object:before {
+ background-color: hsl(208,56%,40%);
+}
+
+.theme-fg-color3,
+.cm-s-mozilla .cm-variable,
+.cm-s-mozilla .cm-tag,
+.cm-s-mozilla .cm-header,
+.cm-s-mozilla .cm-bracket,
+.cm-s-mozilla .cm-qualifier,
+.variables-view-property > .title > .name {
+ color: var(--theme-highlight-blue);
+}
+
+.CodeMirror-Tern-completion-array:before {
+ background-color: var(--theme-highlight-bluegrey);
+}
+
+.theme-fg-color4 {
+ color: var(--theme-highlight-orange);
+}
+
+.theme-fg-color5,
+.cm-s-mozilla .cm-keyword {
+ color: var(--theme-highlight-red);
+}
+
+.theme-fg-color6,
+.cm-s-mozilla .cm-string,
+.cm-s-mozilla .cm-string-2,
+.variable-or-property .token-string,
+.CodeMirror-Tern-farg {
+ color: var(--theme-highlight-purple);
+}
+
+.CodeMirror-Tern-completion-string:before,
+.CodeMirror-Tern-completion-fn:before {
+ background-color: hsl(24,85%,39%);
+}
+
+.theme-fg-color7,
+.cm-s-mozilla .cm-atom,
+.cm-s-mozilla .cm-quote,
+.cm-s-mozilla .cm-error,
+.variable-or-property .token-boolean,
+.variable-or-property .token-domnode,
+.variable-or-property[exception] > .title > .name {
+ color: var(--theme-highlight-red);
+}
+
+.CodeMirror-Tern-completion-bool:before {
+ background-color: #bf5656;
+}
+
+.variable-or-property .token-domnode {
+ font-weight: bold;
+}
+
+.theme-fg-contrast { /* To be used for text on theme-bg-contrast */
+ color: black;
+}
+
+.theme-toolbar,
+.devtools-toolbar,
+.devtools-sidebar-tabs tabs,
+.devtools-sidebar-alltabs,
+.cm-s-mozilla .CodeMirror-dialog { /* General toolbar styling */
+ color: var(--theme-body-color);
+ background-color: var(--theme-toolbar-background);
+ border-color: var(--theme-splitter-color);
+}
+
+.ruleview-swatch,
+.computedview-colorswatch {
+ box-shadow: 0 0 0 1px #c4c4c4;
+}
+
+/* CodeMirror specific styles.
+ * Best effort to match the existing theme, some of the colors
+ * are duplicated here to prevent weirdness in the main theme. */
+
+.CodeMirror.cm-s-mozilla { /* Inherit platform specific font sizing and styles */
+ font-family: inherit;
+ font-size: inherit;
+ background: transparent;
+}
+
+.CodeMirror.cm-s-mozilla pre,
+.cm-s-mozilla .cm-variable-2,
+.cm-s-mozilla .cm-variable-3,
+.cm-s-mozilla .cm-operator,
+.cm-s-mozilla .cm-special {
+ color: var(--theme-body-color);
+}
+
+.cm-s-mozilla .CodeMirror-lines .CodeMirror-cursor {
+ border-left: solid 1px black;
+}
+
+.cm-s-mozilla.CodeMirror-focused .CodeMirror-selected { /* selected text (focused) */
+ background: rgb(185, 215, 253);
+}
+
+.cm-s-mozilla .CodeMirror-selected { /* selected text (unfocused) */
+ background: rgb(176, 176, 176);
+}
+
+.cm-s-mozilla .CodeMirror-activeline-background { /* selected color with alpha */
+ background: rgba(185, 215, 253, .35);
+}
+
+div.cm-s-mozilla span.CodeMirror-matchingbracket { /* highlight brackets */
+ outline: solid 1px rgba(0, 0, 0, .25);
+ color: black;
+}
+
+/* Highlight for a line that contains an error. */
+div.CodeMirror div.error-line {
+ background: rgba(255,0,0,0.2);
+}
+
+/* Generic highlighted text */
+div.CodeMirror span.marked-text {
+ background: rgba(255,255,0,0.2);
+ border: 1px dashed rgba(192,192,0,0.6);
+ margin-inline-start: -1px;
+ margin-inline-end: -1px;
+}
+
+/* Highlight for evaluating current statement. */
+div.CodeMirror span.eval-text {
+ background-color: #ccd;
+}
+
+.cm-s-mozilla .CodeMirror-linenumber { /* line number text */
+ color: var(--theme-content-color3);
+}
+
+.cm-s-mozilla .CodeMirror-gutters { /* vertical line next to line numbers */
+ border-right-color: var(--theme-splitter-color);
+ background-color: var(--theme-sidebar-background);
+}
+
+.cm-s-markup-view pre {
+ line-height: 1.4em;
+ min-height: 1.4em;
+}
+
+/* XUL panel styling (see devtools/client/shared/widgets/tooltip/Tooltip.js) */
+
+.theme-tooltip-panel .panel-arrowcontent {
+ padding: 4px;
+ background: rgba(255, 255, 255, .9);
+ border-radius: 5px;
+ box-shadow: none;
+ border: 3px solid #d9e1e8;
+}
+
+/* Overring panel arrow images to fit with our light and dark themes */
+
+.theme-tooltip-panel .panel-arrow {
+ --arrow-margin: -4px;
+}
+
+:root[platform="win"] .theme-tooltip-panel .panel-arrow {
+ --arrow-margin: -7px;
+}
+
+.theme-tooltip-panel .panel-arrow[side="top"],
+.theme-tooltip-panel .panel-arrow[side="bottom"] {
+ list-style-image: url("chrome://devtools/skin/tooltip/arrow-vertical-light.png");
+ /* !important is needed to override the popup.css rules in toolkit/themes */
+ width: 39px !important;
+ height: 16px !important;
+}
+
+.theme-tooltip-panel .panel-arrow[side="left"],
+.theme-tooltip-panel .panel-arrow[side="right"] {
+ list-style-image: url("chrome://devtools/skin/tooltip/arrow-horizontal-light.png");
+ /* !important is needed to override the popup.css rules in toolkit/themes */
+ width: 16px !important;
+ height: 39px !important;
+}
+
+.theme-tooltip-panel .panel-arrow[side="top"] {
+ margin-bottom: var(--arrow-margin);
+}
+
+.theme-tooltip-panel .panel-arrow[side="bottom"] {
+ margin-top: var(--arrow-margin);
+}
+
+.theme-tooltip-panel .panel-arrow[side="left"] {
+ margin-right: var(--arrow-margin);
+}
+
+.theme-tooltip-panel .panel-arrow[side="right"] {
+ margin-left: var(--arrow-margin);
+}
+
+@media (min-resolution: 1.1dppx) {
+ .theme-tooltip-panel .panel-arrow[side="top"],
+ .theme-tooltip-panel .panel-arrow[side="bottom"] {
+ list-style-image: url("chrome://devtools/skin/tooltip/arrow-vertical-light@2x.png");
+ }
+
+ .theme-tooltip-panel .panel-arrow[side="left"],
+ .theme-tooltip-panel .panel-arrow[side="right"] {
+ list-style-image: url("chrome://devtools/skin/tooltip/arrow-horizontal-light@2x.png");
+ }
+}
+
+.theme-tooltip-panel .devtools-tooltip-simple-text {
+ color: black;
+ border-bottom: 1px solid #d9e1e8;
+}
+
+.theme-tooltip-panel .devtools-tooltip-simple-text:last-child {
+ border-bottom: 0;
+}
+
+.CodeMirror-hints,
+.CodeMirror-Tern-tooltip {
+ box-shadow: 0 0 4px rgba(128, 128, 128, .5);
+ background-color: var(--theme-sidebar-background);
+}
diff --git a/devtools/client/themes/markup.css b/devtools/client/themes/markup.css
new file mode 100644
index 000000000..4b4cfd031
--- /dev/null
+++ b/devtools/client/themes/markup.css
@@ -0,0 +1,351 @@
+/* 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/. */
+
+:root {
+ --markup-outline: var(--theme-splitter-color);
+}
+
+.theme-dark:root {
+ --markup-outline: var(--theme-highlight-pink);
+}
+
+* {
+ padding: 0;
+ margin: 0;
+}
+
+:root {
+ -moz-control-character-visibility: visible;
+}
+
+body {
+ -moz-user-select: none;
+}
+
+/* Force height and width (possibly overflowing) from inline elements.
+ * This allows long overflows of text or input fields to still be styled with
+ * the container, rather than the background disappearing when scrolling */
+#root {
+ float: left;
+ min-width: 100%;
+}
+
+/* Don't display a parent-child outline for the root elements */
+#root > ul > li > .children {
+ background: none;
+}
+
+html.dragging {
+ overflow-x: hidden;
+}
+
+body.dragging .tag-line {
+ cursor: grabbing;
+}
+
+#root-wrapper:after {
+ content: "";
+ display: block;
+ clear: both;
+ position:relative;
+}
+
+.html-editor {
+ display: none;
+ position: absolute;
+ z-index: 2;
+
+ /* Use the same margin/padding trick used by .child tags to ensure that
+ * the editor covers up any content to the left (including expander arrows
+ * and hover effects). */
+ margin-left: -1000em;
+ padding-left: 1000em;
+}
+
+.html-editor-inner {
+ border: solid .1px;
+ flex: 1 1 auto;
+
+ /* Keep the editor away from the markup view floating scrollbars */
+ margin-inline-end: 12px;
+}
+
+.html-editor iframe {
+ height: 100%;
+ width: 100%;
+ border: none;
+ margin: 0;
+ padding: 0;
+}
+
+.children {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+/* Tags are organized in a UL/LI tree and indented thanks to a left padding.
+ * A very large padding is used in combination with a slightly smaller margin
+ * to make sure childs actually span from edge-to-edge. */
+.child {
+ margin-left: -1000em;
+ padding-left: 1001em;
+}
+
+/* Normally this element takes space in the layout even if it's position: relative
+ * by adding height: 0 we let surrounding elements to fill the blank space */
+.child.dragging {
+ position: relative;
+ pointer-events: none;
+ opacity: 0.7;
+ z-index: 1;
+ height: 0;
+}
+
+/* Indicates a tag-line in the markup-view as being an active drop target by
+ * drawing a horizontal line where the dragged element would be inserted if
+ * dropped here */
+.tag-line.drop-target::before,
+.tag-line.drag-target::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ width: 100%;
+ /* Offset these by 1000px to make sure they cover the full width of the view */
+ padding-left: 1000px;
+ left: -1000px;
+}
+
+.tag-line.drag-target::before {
+ border-top: 2px solid var(--theme-content-color2);
+}
+
+.tag-line.drop-target::before {
+ border-top: 2px solid var(--theme-contrast-background);
+}
+
+/* In case the indicator is put on the closing .tag-line, the indentation level
+ * will become misleading, so we push it forward to match the indentation level */
+ul.children + .tag-line::before {
+ margin-left: 14px;
+}
+
+.tag-line {
+ min-height: 1.4em;
+ line-height: 1.4em;
+ position: relative;
+ cursor: default;
+ padding-left: 2px;
+}
+
+.tag-line[selected] + .children {
+ background-image: linear-gradient(to top, var(--markup-outline), var(--markup-outline));
+ background-repeat: no-repeat;
+ /* Shorten the outline height by 4px to account for the 2px top padding and
+ * allow for a 2px bottom padding */
+ background-size: 1.5px calc(100% - 4px);
+ /* Align the outline to under the expander arrow and provide 2px top
+ * padding */
+ background-position: -6px 2px;
+ border-left: 6px solid transparent;
+ margin-left: -6px;
+}
+
+.html-editor-container {
+ position: relative;
+ min-height: 200px;
+}
+
+/* This extra element placed in each tag is positioned absolutely to cover the
+ * whole tag line and is used for background styling (when a selection is made
+ * or when the tag is flashing) */
+.tag-line .tag-state {
+ position: absolute;
+ left: -1000em;
+ right: 0;
+ height: 100%;
+ z-index: 0;
+}
+
+.expander {
+ display: inline-block;
+ margin-left: -14px;
+ vertical-align: middle;
+ /* Make sure the expander still appears above the tag-state */
+ position: relative;
+ z-index: 1;
+}
+
+.child.collapsed .child, .child.collapsed .children {
+ display: none;
+}
+
+.child > .tag-line:first-child .close {
+ display: none;
+}
+
+.child.collapsed > .tag-line:first-child .close {
+ display: inline;
+}
+
+.child.collapsed > .tag-line ~ .tag-line {
+ display: none;
+}
+
+.child.collapsed .close {
+ display: inline;
+}
+
+/* Hide HTML void elements (img, hr, br, …) closing tag when the element is not
+ * expanded (it can be if it has pseudo-elements attached) */
+.child.collapsed > .tag-line .void-element .close {
+ display: none;
+}
+
+.closing-bracket {
+ pointer-events: none;
+}
+
+.newattr {
+ display: inline-block;
+ width: 1em;
+ height: 1ex;
+ margin-right: -1em;
+ padding: 1px 0;
+}
+
+.attr-value .link {
+ text-decoration: underline;
+}
+
+.newattr:focus {
+ margin-right: 0;
+}
+
+.flash-out {
+ transition: background .5s;
+}
+
+.markupview-events {
+ display: none;
+ cursor: pointer;
+}
+
+.editor {
+ /* Make sure the editor still appears above the tag-state */
+ position: relative;
+ z-index: 1;
+}
+
+.editor.text {
+ display: inline-block;
+}
+
+.editor.text pre,
+.editor.comment pre {
+ font: inherit;
+}
+
+/* Whitespace only text nodes are sometimes shown in the markup-view, and when they do
+ they get a greyed-out whitespace symbol so users know what they are */
+.editor.text .whitespace {
+ padding: 0 .5em;
+}
+
+.editor.text .whitespace::before {
+ content: "";
+ display: inline-block;
+ height: 4px;
+ width: 4px;
+ border: 1px solid var(--theme-body-color-inactive);
+ border-radius: 50%;
+}
+
+.tag-line[selected] .editor.text .whitespace::before {
+ border-color: white;
+}
+
+.more-nodes {
+ padding-left: 16px;
+}
+
+.styleinspector-propertyeditor {
+ border: 1px solid #CCC;
+}
+
+/* Draw a circle next to nodes that have a pseudo class lock.
+ Center vertically with the 1.4em line height on .tag-line */
+.child.pseudoclass-locked::before {
+ content: "";
+ background: var(--theme-highlight-lightorange);
+ border-radius: 50%;
+ width: .8em;
+ height: .8em;
+ margin-top: .3em;
+ left: 1px;
+ position: absolute;
+ z-index: 1;
+}
+
+/* Firebug Theme */
+
+.theme-firebug .theme-fg-color3 {
+ color: var(--theme-graphs-full-blue);
+}
+
+.theme-firebug .open,
+.theme-firebug .close,
+.theme-firebug .attr-name.theme-fg-color2 {
+ color: var(--theme-highlight-purple);
+}
+
+.theme-firebug .attr-value.theme-fg-color6 {
+ color: var(--theme-highlight-red);
+}
+
+.theme-firebug .markupview-events {
+ font-size: var(--theme-toolbar-font-size);
+}
+
+/* Selected nodes in the tree should have light selected text.
+ theme-selected doesn't work in this case since the text is a
+ sibling of the class, not a child. */
+.theme-selected ~ .editor,
+.theme-selected ~ .editor .theme-fg-color1,
+.theme-selected ~ .editor .theme-fg-color2,
+.theme-selected ~ .editor .theme-fg-color3,
+.theme-selected ~ .editor .theme-fg-color4,
+.theme-selected ~ .editor .theme-fg-color5,
+.theme-selected ~ .editor .theme-fg-color6,
+.theme-selected ~ .editor .theme-fg-color7 {
+ color: var(--theme-selection-color);
+}
+
+/* Make sure even text nodes are white when selected in the Inspector panel. */
+.theme-firebug .theme-selected ~ .editor .open,
+.theme-firebug .theme-selected ~ .editor .close {
+ color: var(--theme-selection-color);
+}
+
+/* In case a node isn't displayed in the page, we fade the syntax highlighting */
+.not-displayed .open,
+.not-displayed .close {
+ opacity: .7;
+}
+
+/* Events */
+.markupview-events {
+ font-size: 8px;
+ font-weight: bold;
+ line-height: 10px;
+ border-radius: 3px;
+ padding: 0px 2px;
+ margin-inline-start: 5px;
+ -moz-user-select: none;
+}
+
+.markupview-events {
+ background-color: var(--theme-body-color-alt);
+ color: var(--theme-body-background);
+}
diff --git a/devtools/client/themes/memory.css b/devtools/client/themes/memory.css
new file mode 100644
index 000000000..f77609621
--- /dev/null
+++ b/devtools/client/themes/memory.css
@@ -0,0 +1,637 @@
+/* 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/. */
+
+/* CSS Variables specific to this panel that aren't defined by the themes */
+.theme-dark {
+ --cell-border-color: rgba(255,255,255,0.15);
+ --cell-border-color-light: rgba(255,255,255,0.1);
+ --focus-cell-border-color: rgba(255,255,255,0.5);
+ --row-alt-background-color: rgba(86, 117, 185, 0.15);
+ --row-hover-background-color: rgba(86, 117, 185, 0.25);
+}
+
+.theme-light {
+ --cell-border-color: rgba(0,0,0,0.15);
+ --cell-border-color-light: rgba(0,0,0,0.1);
+ --focus-cell-border-color: rgba(0,0,0,0.3);
+ --row-alt-background-color: rgba(76,158,217,0.1);
+ --row-hover-background-color: rgba(76,158,217,0.2);
+}
+
+html, body, #app, #memory-tool {
+ height: 100%;
+}
+
+#memory-tool {
+ /**
+ * Flex: contains two children: .devtools-toolbar and #memory-tool-container,
+ * which need to be laid out vertically. The toolbar has a fixed height and
+ * the container needs to flex to fill out all remaining vertical space.
+ */
+ display: flex;
+ flex-direction: column;
+ --sidebar-width: 185px;
+ /**
+ * If --heap-tree-row-height changes, be sure to change HEAP_TREE_ROW_HEIGHT
+ * in `devtools/client/memory/components/heap.js`.
+ */
+ --heap-tree-row-height: 18px;
+ --heap-tree-header-height: 18px;
+}
+
+/**
+ * Toolbar
+ */
+
+.devtools-toolbar {
+ /**
+ * Flex: contains several children, which need to be laid out horizontally,
+ * and aligned vertically in the middle of the container.
+ */
+ display: flex;
+ align-items: center;
+}
+
+.devtools-toolbar > .toolbar-group:nth-of-type(1) {
+ /**
+ * We want this to be exactly at a `--sidebar-width` distance from the
+ * toolbar's start boundary. A `.devtools-toolbar` has a 3px start padding.
+ */
+ flex: 0 0 calc(var(--sidebar-width) - 4px);
+ border-inline-end: 1px solid var(--theme-splitter-color);
+ margin-inline-end: 5px;
+ padding-right: 1px;
+}
+
+.devtools-toolbar > .toolbar-group {
+ /**
+ * Flex: contains several children, which need to be laid out horizontally,
+ * and aligned vertically in the middle of the container.
+ */
+ display: flex;
+ align-items: center;
+ flex: 1;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.devtools-toolbar > .toolbar-group > label {
+ /**
+ * Flex: contains form controls and text, which need to be laid out
+ * horizontally, vertically aligned in the middle of the container.
+ */
+ display: flex;
+ align-items: center;
+ margin-inline-end: 5px;
+}
+
+.devtools-toolbar > .toolbar-group > label.display-by > span {
+ margin-inline-end: 5px;
+}
+
+.devtools-toolbar > .toolbar-group > label.label-by > span {
+ margin-inline-end: 5px;
+}
+
+.devtools-toolbar > label {
+ margin-inline-end: 5px;
+}
+
+#select-view {
+ margin-inline-start: 5px;
+}
+
+#take-snapshot::before {
+ background-image: url(images/command-screenshot.svg);
+}
+
+#clear-snapshots::before {
+ background-image: url(chrome://devtools/skin/images/clear.svg);
+}
+
+#diff-snapshots::before {
+ background-image: url(chrome://devtools/skin/images/diff.svg);
+}
+
+#import-snapshot::before {
+ background-image: url(chrome://devtools/skin/images/import.svg);
+}
+
+#record-allocation-stacks-label,
+#pop-view-button-label {
+ border-inline-end: 1px solid var(--theme-splitter-color);
+ padding-inline-end: 5px;
+}
+
+.spacer {
+ flex: 1;
+}
+
+#filter {
+ align-self: stretch;
+ margin: 2px;
+}
+
+/**
+ * Container (sidebar + main panel)
+ */
+
+#memory-tool-container {
+ /**
+ * Flex: contains two children: .list (sidebar) and #heap-view (main panel),
+ * which need to be laid out horizontally. The sidebar has a fixed width and
+ * the main panel needs to flex to fill out all remaining horizontal space.
+ */
+ display: flex;
+ /**
+ * Flexing to fill out remaining vertical space. The preceeding sibling is
+ * the toolbar. @see #memory-tool.
+ */
+ flex: 1;
+ overflow: hidden;
+}
+
+/**
+ * Sidebar
+ */
+
+.list {
+ width: var(--sidebar-width);
+ min-width: var(--sidebar-width);
+ overflow-y: auto;
+ margin: 0;
+ padding: 0;
+ background-color: var(--theme-sidebar-background);
+ border-inline-end: 1px solid var(--theme-splitter-color);
+}
+
+.snapshot-list-item {
+ /**
+ * Flex: contains several children, which need to be laid out vertically.
+ */
+ display: flex;
+ flex-direction: column;
+ color: var(--theme-body-color);
+ border-bottom: 1px solid rgba(128,128,128,0.15);
+ padding: 8px;
+ cursor: default;
+}
+
+.snapshot-list-item.selected {
+ background-color: var(--theme-selection-background);
+ color: var(--theme-selection-color);
+}
+
+.snapshot-list-item.selected ::-moz-selection {
+ background-color: var(--theme-selection-color);
+ color: var(--theme-selection-background);
+}
+
+.snapshot-list-item .snapshot-info {
+ display: flex;
+ justify-content: space-between;
+ font-size: 90%;
+}
+
+.snapshot-list-item .snapshot-title {
+ display: flex;
+ justify-content: space-between;
+}
+
+.snapshot-list-item .save {
+ text-decoration: underline;
+ cursor: pointer;
+}
+
+.snapshot-list-item .delete {
+ cursor: pointer;
+ position: relative;
+ min-height: 1em;
+ min-width: 1.3em;
+}
+
+.snapshot-list-item.selected .delete::before {
+ filter: invert(1);
+}
+
+.snapshot-list-item .delete::before {
+ background-image: url("chrome://devtools/skin/images/close.svg");
+ background-position: 0.2em 0;
+}
+
+.snapshot-list-item > .snapshot-title {
+ margin-bottom: 14px;
+}
+
+.snapshot-list-item > .snapshot-title > input[type=checkbox] {
+ margin: 0;
+ margin-inline-end: 5px;
+}
+
+.snapshot-list-item > .snapshot-state,
+.snapshot-list-item > .snapshot-totals {
+ font-size: 90%;
+ color: var(--theme-body-color-alt);
+}
+
+.snapshot-list-item.selected > .snapshot-state,
+.snapshot-list-item.selected > .snapshot-totals {
+ /* Text inside a selected item should not be custom colored. */
+ color: inherit !important;
+}
+
+/**
+ * Main panel
+ */
+
+.vbox {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ height: 100%;
+ overflow: auto;
+ padding: 0;
+ margin: 0;
+}
+
+.vbox > * {
+ flex: 1;
+
+ /**
+ * By default, flex items have min-width: auto;
+ * (https://drafts.csswg.org/css-flexbox/#min-size-auto)
+ */
+ min-width: 0;
+}
+
+#heap-view {
+ /**
+ * Flex: contains a .heap-view-panel which needs to fill out all the
+ * available space, horizontally and vertically.
+ */;
+ display: flex;
+ /**
+ * Flexing to fill out remaining horizontal space. The preceeding sibling
+ * is the sidebar. @see #memory-tool-container.
+ */
+ flex: 1;
+ background-color: var(--theme-body-background);
+
+ /**
+ * By default, flex items have min-width: auto;
+ * (https://drafts.csswg.org/css-flexbox/#min-size-auto)
+ */
+ min-width: 0;
+ font-size: 90%;
+}
+
+#heap-view > .heap-view-panel {
+ /**
+ * Flex: can contain several children, including a tree with a header and
+ * multiple rows, all of which need to be laid out vertically. When the
+ * tree is visible, the header has a fixed height and tree body needs to flex
+ * to fill out all remaining vertical space.
+ */
+ display: flex;
+ flex-direction: column;
+ /**
+ * Flexing to fill out remaining horizontal space. @see #heap-view.
+ */
+ flex: 1;
+
+ /**
+ * By default, flex items have min-width: auto;
+ * (https://drafts.csswg.org/css-flexbox/#min-size-auto)
+ */
+ min-width: 0;
+}
+
+#heap-view > .heap-view-panel > .snapshot-status,
+#heap-view > .heap-view-panel > .take-snapshot,
+#heap-view .empty,
+#shortest-paths-select-node-msg {
+ margin: auto;
+ margin-top: 65px;
+ font-size: 120%;
+}
+
+#heap-view > .heap-view-panel > .take-snapshot {
+ padding: 5px;
+}
+
+#heap-view > .heap-view-panel[data-state="snapshot-state-error"] pre {
+ background-color: var(--theme-body-background);
+ margin: 20px;
+ padding: 20px;
+}
+
+/**
+ * Heap tree view header
+ */
+
+.header {
+ /**
+ * Flex: contains several span columns, all of which need to be laid out
+ * horizontally. All columns except the last one have percentage widths, and
+ * the last one needs to flex to fill out all remaining horizontal space.
+ */
+ display: flex;
+ color: var(--theme-body-color);
+ background-color: var(--theme-tab-toolbar-background);
+ border-bottom: 1px solid var(--cell-border-color);
+ flex: 0;
+}
+
+.header > span,
+#shortest-paths-header {
+ text-overflow: ellipsis;
+ line-height: var(--heap-tree-header-height);
+ justify-content: center;
+ justify-self: center;
+ white-space: nowrap;
+}
+
+.header > span {
+ overflow: hidden;
+}
+
+.header > .heap-tree-item-name {
+ justify-content: flex-start;
+}
+
+#shortest-paths {
+ background-color: var(--theme-body-background);
+ overflow: hidden;
+ height: 100%;
+ width: 100%;
+}
+
+#shortest-paths-select-node-msg {
+ justify-self: center;
+}
+
+/**
+ * Heap tree view body
+ */
+
+.tree {
+ /**
+ * Flexing to fill out remaining vertical space. @see .heap-view-panel
+ */
+ flex: 1;
+ overflow-y: auto;
+ background-color: var(--theme-body-background);
+}
+
+.tree-node {
+ height: var(--heap-tree-row-height);
+ line-height: var(--heap-tree-row-height);
+ cursor: default;
+}
+
+.children-pointer {
+ padding-inline-end: 5px;
+}
+
+.children-pointer:dir(rtl) {
+ transform: scaleX(-1);
+}
+
+/**
+ * Heap tree view columns
+ */
+
+.heap-tree-item {
+ /**
+ * Flex: contains several span columns, all of which need to be laid out
+ * horizontally. All columns except the last one have percentage widths, and
+ * the last one needs to flex to fill out all remaining horizontal space.
+ */
+ display: flex;
+}
+
+.tree-node-odd {
+ background-color: var(--row-alt-background-color);
+}
+
+.tree-node:hover {
+ background-color: var(--row-hover-background-color);
+}
+
+.heap-tree-item.focused {
+ background-color: var(--theme-selection-background);
+ color: var(--theme-selection-color);
+}
+
+.heap-tree-item.focused ::-moz-selection {
+ background-color: var(--theme-selection-color);
+ color: var(--theme-selection-background);
+}
+
+.heap-tree-item-individuals,
+.heap-tree-item-bytes,
+.heap-tree-item-count,
+.heap-tree-item-total-bytes,
+.heap-tree-item-total-count {
+ /**
+ * Flex: contains several subcolumns, which need to be laid out horizontally.
+ * These subcolumns may have specific widths or need to flex.
+ */
+ display: flex;
+ /* Make sure units/decimals/... are always vertically aligned to right in both LTR and RTL locales */
+ text-align: right;
+ border-inline-end: var(--cell-border-color) 1px solid;
+}
+
+.heap-tree-item-count,
+.heap-tree-item-total-count,
+.heap-tree-item-bytes,
+.heap-tree-item-total-bytes {
+ width: 10%;
+ /*
+ * Provision for up to 19 characters:
+ *
+ * GG_MMM_KKK_BBB_100%
+ * | ||| |
+ * '------------'|'--'
+ * 14 ch for 10s | 4 ch for the largest % we will
+ * of GB and | normally see: "100%"
+ * spaces every |
+ * 3 digits |
+ * |
+ * A space between the number and percent
+ */
+ min-width: 19ch;
+}
+
+.heap-tree-item-name {
+ /**
+ * Flex: contains an .arrow and some text, which need to be laid out
+ * horizontally, vertically aligned in the middle of the container.
+ */
+ display: flex;
+ align-items: center;
+ /**
+ * Flexing to fill out remaining vertical space.
+ * @see .header and .heap-tree-item */
+ flex: 1;
+ padding-inline-start: 5px;
+}
+
+/**
+ * Heap tree view subcolumns
+ */
+
+.heap-tree-number,
+.heap-tree-percent,
+.heap-tree-item-name {
+ white-space: nowrap;
+}
+
+.heap-tree-number {
+ padding: 0 3px;
+ flex: 1;
+ color: var(--theme-content-color3);
+ /* Make sure number doesn't appear backwards on RTL locales */
+ direction: ltr;
+}
+
+.heap-tree-percent {
+ padding-inline-start: 3px;
+ padding-inline-end: 3px;
+}
+
+.heap-tree-number,
+.heap-tree-percent {
+ font-family: var(--monospace-font-family);
+}
+
+.heap-tree-percent {
+ width: 4ch;
+}
+
+.heap-tree-item.focused .heap-tree-number,
+.heap-tree-item.focused .heap-tree-percent {
+ color: inherit;
+}
+
+.heap-tree-item-individuals {
+ width: 38px;
+ min-width: 20px;
+ overflow: hidden;
+ margin: 0;
+}
+
+.heap-tree-item-individuals > button {
+ height: 10px;
+ width: 32px;
+
+ /* Override default styles for toolbar buttons to fix entire row height. */
+ margin: 0 auto !important;
+ padding: 0;
+}
+
+/**
+ * Tree map
+ */
+
+.tree-map-container {
+ width: 100%;
+ height: 100%;
+ position: relative;
+ overflow: hidden;
+}
+
+/**
+ * Heap tree errors.
+ */
+
+.error::before {
+ content: "";
+ display: inline-block;
+ width: 12px;
+ height: 12px;
+ max-height: 12px;
+ background-image: url(chrome://devtools/skin/images/webconsole.svg);
+ background-size: 72px 60px;
+ background-position: -24px -24px;
+ background-repeat: no-repeat;
+ margin: 0px;
+ margin-top: 2px;
+ margin-inline-end: 5px;
+}
+
+.theme-light .error::before {
+ background-image: url(chrome://devtools/skin/images/webconsole.svg#light-icons);
+}
+
+/**
+ * Frame View components
+ */
+
+.separator,
+.not-available,
+.heap-tree-item-address {
+ opacity: .5;
+ margin-left: .5em;
+ margin-right: .5em;
+}
+
+.heap-tree-item-address {
+ font-family: monospace;
+}
+
+.no-allocation-stacks {
+ border-color: var(--theme-splitter-color);
+ border-style: solid;
+ border-width: 0px 0px 1px 0px;
+ text-align: center;
+ padding: 5px;
+}
+
+/**
+ * Dagre-D3 graphs
+ */
+
+svg {
+ --arrow-color: var(--theme-splitter-color);
+ --text-color: var(--theme-body-color-alt);
+}
+
+.theme-dark svg {
+ --arrow-color: var(--theme-body-color-alt);
+}
+
+svg #arrowhead {
+ /* !important is needed to override inline style */
+ fill: var(--arrow-color) !important;
+}
+
+.edgePath path {
+ stroke-width: 1px;
+ fill: none;
+ stroke: var(--arrow-color);
+}
+
+g.edgeLabel rect {
+ fill: var(--theme-body-background);
+}
+
+g.edgeLabel tspan {
+ fill: var(--text-color);
+}
+
+.nodes rect {
+ stroke-width: 1px;
+ stroke: var(--theme-splitter-color);
+ fill: var(--theme-toolbar-background);
+}
+
+text {
+ font-size: 1.25em;
+ fill: var(--text-color);
+ /* Make sure text stays inside its container in RTL locales */
+ direction: ltr;
+}
diff --git a/devtools/client/themes/moz.build b/devtools/client/themes/moz.build
new file mode 100644
index 000000000..543f4eff0
--- /dev/null
+++ b/devtools/client/themes/moz.build
@@ -0,0 +1,16 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DIRS += [
+ 'audio',
+]
+
+DevToolsModules(
+ 'common.css',
+ 'splitters.css',
+ 'toolbars.css',
+ 'variables.css',
+)
diff --git a/devtools/client/themes/netmonitor.css b/devtools/client/themes/netmonitor.css
new file mode 100644
index 000000000..fea634a0e
--- /dev/null
+++ b/devtools/client/themes/netmonitor.css
@@ -0,0 +1,975 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+#toolbar-labels {
+ overflow: hidden;
+}
+
+#react-clear-button-hook,
+#react-details-pane-toggle-hook {
+ display: flex;
+}
+
+/**
+ * Collapsed details pane needs to be truly hidden to prevent both accessibility
+ * tools and keyboard from accessing its contents.
+ */
+#details-pane.pane-collapsed {
+ visibility: hidden;
+}
+
+#details-pane-toggle[disabled] {
+ display: none;
+}
+
+#custom-pane {
+ overflow: auto;
+}
+
+#response-content-image-box {
+ overflow: auto;
+}
+
+#network-statistics-charts {
+ overflow: auto;
+}
+
+.cropped-textbox .textbox-input {
+ /* workaround for textbox not supporting the @crop attribute */
+ text-overflow: ellipsis;
+}
+
+/* Responsive sidebar */
+@media (max-width: 700px) {
+ #toolbar-spacer,
+ #details-pane-toggle,
+ #details-pane.pane-collapsed,
+ .requests-menu-waterfall,
+ #requests-menu-network-summary-button > .toolbarbutton-text {
+ display: none;
+ }
+}
+
+:root.theme-dark {
+ --table-splitter-color: rgba(255,255,255,0.15);
+ --table-zebra-background: rgba(255,255,255,0.05);
+
+ --timing-blocked-color: rgba(235, 83, 104, 0.8);
+ --timing-dns-color: rgba(223, 128, 255, 0.8); /* pink */
+ --timing-connect-color: rgba(217, 102, 41, 0.8); /* orange */
+ --timing-send-color: rgba(70, 175, 227, 0.8); /* light blue */
+ --timing-wait-color: rgba(94, 136, 176, 0.8); /* blue grey */
+ --timing-receive-color: rgba(112, 191, 83, 0.8); /* green */
+
+ --sort-ascending-image: url(chrome://devtools/skin/images/sort-arrows.svg#ascending);
+ --sort-descending-image: url(chrome://devtools/skin/images/sort-arrows.svg#descending);
+}
+
+:root.theme-light {
+ --table-splitter-color: rgba(0,0,0,0.15);
+ --table-zebra-background: rgba(0,0,0,0.05);
+
+ --timing-blocked-color: rgba(235, 83, 104, 0.8);
+ --timing-dns-color: rgba(223, 128, 255, 0.8); /* pink */
+ --timing-connect-color: rgba(217, 102, 41, 0.8); /* orange */
+ --timing-send-color: rgba(0, 136, 204, 0.8); /* blue */
+ --timing-wait-color: rgba(95, 136, 176, 0.8); /* blue grey */
+ --timing-receive-color: rgba(44, 187, 15, 0.8); /* green */
+
+ --sort-ascending-image: url(chrome://devtools/skin/images/sort-arrows.svg#ascending);
+ --sort-descending-image: url(chrome://devtools/skin/images/sort-arrows.svg#descending);
+}
+
+:root.theme-firebug {
+ --table-splitter-color: rgba(0,0,0,0.15);
+ --table-zebra-background: rgba(0,0,0,0.05);
+
+ --timing-blocked-color: rgba(235, 83, 104, 0.8); /* red */
+ --timing-dns-color: rgba(223, 128, 255, 0.8); /* pink */
+ --timing-connect-color: rgba(217, 102, 41, 0.8); /* orange */
+ --timing-send-color: rgba(70, 175, 227, 0.8); /* light blue */
+ --timing-wait-color: rgba(94, 136, 176, 0.8); /* blue grey */
+ --timing-receive-color: rgba(112, 191, 83, 0.8); /* green */
+
+ --sort-ascending-image: url(chrome://devtools/skin/images/firebug/arrow-up.svg);
+ --sort-descending-image: url(chrome://devtools/skin/images/firebug/arrow-down.svg);
+}
+
+#requests-menu-empty-notice {
+ margin: 0;
+ padding: 12px;
+ font-size: 120%;
+}
+
+#notice-perf-message {
+ margin-top: 2px;
+}
+
+#requests-menu-perf-notice-button {
+ min-width: 30px;
+ min-height: 26px;
+ margin: 0;
+ list-style-image: url(images/profiler-stopwatch.svg);
+}
+
+/* Make sure the icon is visible on Linux (to overwrite a rule
+ in xul.css that hides the icon if there is no label.
+ See also bug 1278050. */
+#requests-menu-perf-notice-button .button-icon {
+ display: block;
+}
+
+#requests-menu-perf-notice-button .button-text {
+ display: none;
+}
+
+#requests-menu-reload-notice-button {
+ min-height: 26px;
+ margin: 0;
+ background-color: var(--theme-toolbar-background);
+}
+
+/* Network requests table */
+
+#requests-menu-toolbar {
+ padding: 0;
+}
+
+.theme-firebug #requests-menu-toolbar {
+ height: 16px !important;
+}
+
+.requests-menu-subitem {
+ padding: 3px;
+}
+
+.requests-menu-header-button {
+ -moz-appearance: none;
+ background-color: transparent;
+ border-image: linear-gradient(transparent 15%,
+ var(--theme-splitter-color) 15%,
+ var(--theme-splitter-color) 85%,
+ transparent 85%) 1 1;
+ border-style: solid;
+ border-width: 0;
+ border-inline-start-width: 1px;
+ min-width: 1px;
+ min-height: 24px;
+ margin: 0;
+ padding-bottom: 2px;
+ padding-inline-start: 13px;
+ padding-top: 2px;
+ text-align: center;
+ color: inherit;
+ font-weight: inherit !important;
+}
+
+.requests-menu-header:first-child .requests-menu-header-button {
+ border-width: 0;
+}
+
+.requests-menu-header-button:hover {
+ background-color: rgba(0, 0, 0, 0.1);
+}
+
+.requests-menu-header-button > .button-box > .button-icon,
+#requests-menu-waterfall-image {
+ display: -moz-box;
+ height: 4px;
+ margin-inline-end: 6px;
+ -moz-box-ordinal-group: 2;
+ width: 7px;
+}
+
+.requests-menu-header-button[sorted=ascending] > .button-box > .button-icon,
+.requests-menu-header-button[sorted=ascending] #requests-menu-waterfall-image {
+ list-style-image: var(--sort-ascending-image);
+}
+
+.requests-menu-header-button[sorted=descending] > .button-box > .button-icon,
+.requests-menu-header-button[sorted=descending] #requests-menu-waterfall-image {
+ list-style-image: var(--sort-descending-image);
+}
+
+.requests-menu-header-button > .button-box > .button-text,
+#requests-menu-waterfall-label-wrapper {
+ -moz-box-flex: 1;
+}
+
+.requests-menu-header-button[sorted],
+.requests-menu-header-button[sorted]:hover {
+ background-color: var(--theme-selection-background);
+ color: var(--theme-selection-color);
+}
+
+.requests-menu-header-button[sorted],
+.requests-menu-header[active] + .requests-menu-header .requests-menu-header-button {
+ border-image: linear-gradient(var(--theme-splitter-color), var(--theme-splitter-color)) 1 1;
+}
+
+/* Firebug theme support for Network panel header */
+
+.theme-firebug .requests-menu-header {
+ padding: 0 !important;
+ font-weight: bold;
+ background: linear-gradient(rgba(255, 255, 255, 0.05),
+ rgba(0, 0, 0, 0.05)),
+ #C8D2DC;
+}
+
+.theme-firebug .requests-menu-header-button {
+ min-height: 17px;
+}
+
+.theme-firebug .requests-header-menu-button[sorted] {
+ background-color: #AAC3DC;
+}
+
+.theme-firebug .requests-header-menu:hover:active {
+ background-image: linear-gradient(rgba(0, 0, 0, 0.1),
+ transparent);
+}
+
+
+/* Network requests table: specific column dimensions */
+
+.requests-menu-status {
+ max-width: 6em;
+ text-align: center;
+ width: 10vw;
+}
+
+.requests-menu-method,
+.requests-menu-method-box {
+ max-width: 7em;
+ text-align: center;
+ width: 10vw;
+}
+
+.requests-menu-icon-and-file {
+ width: 22vw;
+}
+
+.requests-menu-icon {
+ background: #fff;
+ width: calc(1em + 4px);
+ height: calc(1em + 4px);
+ margin: -4px 0px;
+ margin-inline-end: 4px;
+}
+
+.requests-menu-icon {
+ outline: 1px solid var(--table-splitter-color);
+}
+
+.requests-menu-security-and-domain {
+ width: 14vw;
+}
+
+.requests-security-state-icon {
+ width: 16px;
+ height: 16px;
+ margin-inline-end: 4px;
+}
+
+.side-menu-widget-item.selected .requests-security-state-icon {
+ filter: brightness(1.3);
+}
+
+.security-state-insecure {
+ list-style-image: url(chrome://devtools/skin/images/security-state-insecure.svg);
+}
+
+.security-state-secure {
+ list-style-image: url(chrome://devtools/skin/images/security-state-secure.svg);
+}
+
+.security-state-weak {
+ list-style-image: url(chrome://devtools/skin/images/security-state-weak.svg);
+}
+
+.security-state-broken {
+ list-style-image: url(chrome://devtools/skin/images/security-state-broken.svg);
+}
+
+.security-state-local {
+ list-style-image: url(chrome://devtools/skin/images/globe.svg);
+}
+
+.requests-menu-type,
+.requests-menu-size {
+ max-width: 6em;
+ text-align: center;
+ width: 8vw;
+}
+
+.requests-menu-cause {
+ max-width: 8em;
+ width: 8vw;
+}
+
+.requests-menu-cause-stack {
+ background-color: var(--theme-body-color-alt);
+ color: var(--theme-body-background);
+ font-size: 8px;
+ font-weight: bold;
+ line-height: 10px;
+ border-radius: 3px;
+ padding: 0 2px;
+ margin: 0;
+ margin-inline-end: 3px;
+ -moz-user-select: none;
+}
+
+.requests-menu-transferred {
+ max-width: 8em;
+ text-align: center;
+ width: 8vw;
+}
+
+.side-menu-widget-item.selected .requests-menu-transferred.theme-comment {
+ color: var(--theme-selection-color);
+}
+
+/* Network requests table: status codes */
+
+.requests-menu-status-code {
+ margin-inline-start: 3px !important;
+ width: 3em;
+ margin-inline-end: -3em !important;
+}
+
+.requests-menu-status-icon {
+ background: #fff;
+ height: 10px;
+ width: 10px;
+ margin-inline-start: 5px;
+ margin-inline-end: 5px;
+ border-radius: 10px;
+ transition: box-shadow 0.5s ease-in-out;
+}
+
+.side-menu-widget-item.selected .requests-menu-status-icon {
+ filter: brightness(1.3);
+}
+
+.requests-menu-status-icon:not([code]) {
+ background-color: var(--theme-content-color2);
+}
+
+.requests-menu-status-icon[code="cached"] {
+ border: 2px solid var(--theme-content-color2);
+ background-color: transparent;
+}
+
+.requests-menu-status-icon[code^="1"] {
+ background-color: var(--theme-highlight-blue);
+}
+
+.requests-menu-status-icon[code^="2"] {
+ background-color: var(--theme-highlight-green);
+}
+
+/* 3xx are triangles */
+.requests-menu-status-icon[code^="3"] {
+ background-color: transparent;
+ width: 0;
+ height: 0;
+ border-left: 5px solid transparent;
+ border-right: 5px solid transparent;
+ border-bottom: 10px solid var(--theme-highlight-lightorange);
+ border-radius: 0;
+}
+
+/* 4xx and 5xx are squares - error codes */
+.requests-menu-status-icon[code^="4"] {
+ background-color: var(--theme-highlight-red);
+ border-radius: 0; /* squares */
+}
+
+.requests-menu-status-icon[code^="5"] {
+ background-color: var(--theme-highlight-pink);
+ border-radius: 0;
+ transform: rotate(45deg);
+}
+
+/* Network requests table: waterfall header */
+
+.requests-menu-waterfall {
+ padding-inline-start: 0;
+}
+
+#requests-menu-waterfall-label:not(.requests-menu-waterfall-visible) {
+ padding-inline-start: 13px;
+}
+
+.requests-menu-timings-division {
+ width: 100px;
+ padding-top: 2px;
+ padding-inline-start: 4px;
+ font-size: 75%;
+ pointer-events: none;
+ box-sizing: border-box;
+ text-align: start;
+}
+
+.requests-menu-timings-division:first-child {
+ width: 98px; /* Substract 2px for borders */
+}
+
+.requests-menu-timings-division:not(:first-child) {
+ border-inline-start: 1px dashed;
+ margin-inline-start: -100px !important; /* Don't affect layout. */
+}
+
+.requests-menu-timings-division:-moz-locale-dir(ltr) {
+ transform-origin: left center;
+}
+
+.requests-menu-timings-division:-moz-locale-dir(rtl) {
+ transform-origin: right center;
+}
+
+.theme-dark .requests-menu-timings-division {
+ border-inline-start-color: #5a6169 !important;
+}
+
+.theme-light .requests-menu-timings-division {
+ border-inline-start-color: #585959 !important;
+}
+
+.requests-menu-timings-division[division-scale=second],
+.requests-menu-timings-division[division-scale=minute] {
+ font-weight: 600;
+}
+
+/* Network requests table: waterfall items */
+
+.requests-menu-subitem.requests-menu-waterfall {
+ padding-inline-start: 0px;
+ padding-inline-end: 4px;
+ /* Background created on a <canvas> in js. */
+ /* @see devtools/client/netmonitor/netmonitor-view.js */
+ background-image: -moz-element(#waterfall-background);
+ background-repeat: repeat-y;
+ background-position: -1px center;
+}
+
+.requests-menu-subitem.requests-menu-waterfall:-moz-locale-dir(rtl) {
+ background-position: right center;
+}
+
+.requests-menu-timings:-moz-locale-dir(ltr) {
+ transform-origin: left center;
+}
+
+.requests-menu-timings:-moz-locale-dir(rtl) {
+ transform-origin: right center;
+}
+
+.requests-menu-timings-total:-moz-locale-dir(ltr) {
+ transform-origin: left center;
+}
+
+.requests-menu-timings-total:-moz-locale-dir(rtl) {
+ transform-origin: right center;
+}
+
+.requests-menu-timings-total {
+ padding-inline-start: 4px;
+ font-size: 85%;
+ font-weight: 600;
+}
+
+.requests-menu-timings-box {
+ height: 9px;
+}
+
+.theme-firebug .requests-menu-timings-box {
+ background-image: linear-gradient(rgba(255, 255, 255, 0.3), rgba(0, 0, 0, 0.2));
+ height: 16px;
+}
+
+.requests-menu-timings-box.blocked {
+ background-color: var(--timing-blocked-color);
+}
+
+.requests-menu-timings-box.dns {
+ background-color: var(--timing-dns-color);
+}
+
+.requests-menu-timings-box.connect {
+ background-color: var(--timing-connect-color);
+}
+
+.requests-menu-timings-box.send {
+ background-color: var(--timing-send-color);
+}
+
+.requests-menu-timings-box.wait {
+ background-color: var(--timing-wait-color);
+}
+
+.requests-menu-timings-box.receive {
+ background-color: var(--timing-receive-color);
+}
+
+/* SideMenuWidget */
+#network-table .side-menu-widget-empty-text,
+#network-table .side-menu-widget-container {
+ background-color: var(--theme-body-background);
+}
+
+#network-table .side-menu-widget-item {
+ border-top-color: transparent;
+ border-bottom-color: transparent;
+}
+
+.side-menu-widget-item-contents {
+ padding: 0px;
+}
+
+.side-menu-widget-item:not(.selected)[odd] {
+ background-color: var(--table-zebra-background);
+}
+
+.side-menu-widget-item:not(.selected):hover {
+ background-color: var(--theme-selection-background-semitransparent);
+}
+
+.theme-firebug .side-menu-widget-item:not(.selected):hover {
+ background: #EFEFEF;
+}
+
+.theme-firebug .requests-menu-subitem {
+ padding: 1px;
+}
+
+/* HTTP Status Column */
+.theme-firebug .requests-menu-subitem.requests-menu-status {
+ font-weight: bold;
+}
+
+/* Method Column */
+
+.theme-firebug .requests-menu-subitem.requests-menu-method-box {
+ color: rgb(128, 128, 128);
+}
+
+.side-menu-widget-item.selected .requests-menu-method {
+ color: var(--theme-selection-color);
+}
+
+/* Size Column */
+.theme-firebug .requests-menu-subitem.requests-menu-size {
+ text-align: end;
+ padding-inline-end: 4px;
+}
+
+/* Network request details */
+
+#details-pane-toggle:-moz-locale-dir(ltr)::before,
+#details-pane-toggle.pane-collapsed:-moz-locale-dir(rtl)::before {
+ background-image: var(--theme-pane-collapse-image);
+}
+
+#details-pane-toggle.pane-collapsed:-moz-locale-dir(ltr)::before,
+#details-pane-toggle:-moz-locale-dir(rtl)::before {
+ background-image: var(--theme-pane-expand-image);
+}
+
+/* Network request details tabpanels */
+
+.tabpanel-content {
+ background-color: var(--theme-sidebar-background);
+}
+
+.theme-dark .tabpanel-content {
+ color: var(--theme-selection-color);
+}
+
+#headers-tabpanel {
+ background-color: var(--theme-toolbar-background);
+}
+
+.theme-firebug .variables-view-scope:focus > .title {
+ color: var(--theme-body-color);
+}
+
+/* Summary tabpanel */
+
+.tabpanel-summary-container {
+ padding: 1px;
+}
+
+.tabpanel-summary-label {
+ padding-inline-start: 4px;
+ padding-inline-end: 3px;
+ font-weight: 600;
+}
+
+.tabpanel-summary-value {
+ color: inherit;
+ padding-inline-start: 3px;
+}
+
+.theme-dark .tabpanel-summary-value {
+ color: var(--theme-selection-color);
+}
+
+/* Headers tabpanel */
+
+#headers-summary-status,
+#headers-summary-version {
+ padding-bottom: 2px;
+}
+
+#headers-summary-size {
+ padding-top: 2px;
+}
+
+#headers-summary-resend {
+ margin-top: -10px;
+ margin-inline-end: 6px;
+}
+
+#toggle-raw-headers {
+ margin-top: -10px;
+ margin-inline-end: 6px;
+}
+
+.raw-response-textarea {
+ height: 50vh;
+}
+
+/* Response tabpanel */
+
+#response-content-info-header {
+ margin: 0;
+ padding: 3px 8px;
+ background-color: var(--theme-highlight-red);
+ color: var(--theme-selection-color);
+}
+
+#response-content-image-box {
+ padding-top: 10px;
+ padding-bottom: 10px;
+}
+
+#response-content-image {
+ background: #fff;
+ border: 1px dashed GrayText;
+ margin-bottom: 10px;
+}
+
+/* Preview tabpanel */
+
+#preview-tabpanel {
+ background: #fff;
+}
+
+#response-preview {
+ display: -moz-box;
+ -moz-box-flex: 1;
+}
+
+/* Timings tabpanel */
+
+#timings-tabpanel .tabpanel-summary-label {
+ width: 10em;
+}
+
+#timings-tabpanel .requests-menu-timings-box {
+ transition: transform 0.2s ease-out;
+ border: none;
+ min-width: 1px;
+}
+
+#timings-tabpanel .requests-menu-timings-total {
+ transition: transform 0.2s ease-out;
+}
+
+.theme-firebug #timings-tabpanel .requests-menu-timings-total {
+ color: var(--theme-body-color);
+}
+
+/* Security tabpanel */
+.security-info-section {
+ padding-inline-start: 1em;
+}
+
+.theme-dark #security-error-message {
+ color: var(--theme-selection-color);
+}
+
+#security-tabpanel {
+ overflow: auto;
+}
+
+.security-warning-icon {
+ background-image: url(images/alerticon-warning.png);
+ background-size: 13px 12px;
+ margin-inline-start: 5px;
+ vertical-align: top;
+ width: 13px;
+ height: 12px;
+}
+
+@media (min-resolution: 1.1dppx) {
+ .security-warning-icon {
+ background-image: url(images/alerticon-warning@2x.png);
+ }
+}
+
+/* Custom request form */
+
+#custom-pane {
+ padding: 0.6em 0.5em;
+}
+
+.custom-header {
+ font-size: 1.1em;
+}
+
+.custom-section {
+ margin-top: 0.5em;
+}
+
+#custom-method-value {
+ width: 4.5em;
+}
+
+/* Performance analysis buttons */
+
+#requests-menu-network-summary-button {
+ background: none;
+ box-shadow: none;
+ border-color: transparent;
+ list-style-image: url(images/profiler-stopwatch.svg);
+ padding-inline-end: 0;
+ cursor: pointer;
+ margin-inline-end: 1em;
+ min-width: 0;
+}
+
+/* Performance analysis view */
+
+#network-statistics-toolbar {
+ border: none;
+ margin: 0;
+ padding: 0;
+}
+
+#network-statistics-back-button {
+ min-width: 4em;
+ min-height: 100vh;
+ margin: 0;
+ padding: 0;
+ border-radius: 0;
+ border-top: none;
+ border-bottom: none;
+ border-inline-start: none;
+}
+
+#network-statistics-view-splitter {
+ border-color: rgba(0,0,0,0.2);
+ cursor: default;
+ pointer-events: none;
+}
+
+#network-statistics-charts {
+ min-height: 1px;
+}
+
+#network-statistics-charts {
+ background-color: var(--theme-sidebar-background);
+}
+
+#network-statistics-charts .pie-chart-container {
+ margin-inline-start: 3vw;
+ margin-inline-end: 1vw;
+}
+
+#network-statistics-charts .table-chart-container {
+ margin-inline-start: 1vw;
+ margin-inline-end: 3vw;
+}
+
+.chart-colored-blob[name=html] {
+ fill: var(--theme-highlight-bluegrey);
+ background: var(--theme-highlight-bluegrey);
+}
+
+.chart-colored-blob[name=css] {
+ fill: var(--theme-highlight-blue);
+ background: var(--theme-highlight-blue);
+}
+
+.chart-colored-blob[name=js] {
+ fill: var(--theme-highlight-lightorange);
+ background: var(--theme-highlight-lightorange);
+}
+
+.chart-colored-blob[name=xhr] {
+ fill: var(--theme-highlight-orange);
+ background: var(--theme-highlight-orange);
+}
+
+.chart-colored-blob[name=fonts] {
+ fill: var(--theme-highlight-purple);
+ background: var(--theme-highlight-purple);
+}
+
+.chart-colored-blob[name=images] {
+ fill: var(--theme-highlight-pink);
+ background: var(--theme-highlight-pink);
+}
+
+.chart-colored-blob[name=media] {
+ fill: var(--theme-highlight-green);
+ background: var(--theme-highlight-green);
+}
+
+.chart-colored-blob[name=flash] {
+ fill: var(--theme-highlight-red);
+ background: var(--theme-highlight-red);
+}
+
+.table-chart-row-label[name=cached] {
+ display: none;
+}
+
+.table-chart-row-label[name=count] {
+ width: 3em;
+ text-align: end;
+}
+
+.table-chart-row-label[name=label] {
+ width: 7em;
+}
+
+.table-chart-row-label[name=size] {
+ width: 7em;
+}
+
+.table-chart-row-label[name=time] {
+ width: 7em;
+}
+
+/* Firebug theme support for network charts */
+
+.theme-firebug .chart-colored-blob[name=html] {
+ fill: rgba(94, 136, 176, 0.8); /* Blue-Grey highlight */
+ background: rgba(94, 136, 176, 0.8);
+}
+
+.theme-firebug .chart-colored-blob[name=css] {
+ fill: rgba(70, 175, 227, 0.8); /* light blue */
+ background: rgba(70, 175, 227, 0.8);
+}
+
+.theme-firebug .chart-colored-blob[name=js] {
+ fill: rgba(235, 83, 104, 0.8); /* red */
+ background: rgba(235, 83, 104, 0.8);
+}
+
+.theme-firebug .chart-colored-blob[name=xhr] {
+ fill: rgba(217, 102, 41, 0.8); /* orange */
+ background: rgba(217, 102, 41, 0.8);
+}
+
+.theme-firebug .chart-colored-blob[name=fonts] {
+ fill: rgba(223, 128, 255, 0.8); /* pink */
+ background: rgba(223, 128, 255, 0.8);
+}
+
+.theme-firebug .chart-colored-blob[name=images] {
+ fill: rgba(112, 191, 83, 0.8); /* pink */
+ background: rgba(112, 191, 83, 0.8);
+}
+
+.theme-firebug .chart-colored-blob[name=media] {
+ fill: rgba(235, 235, 84, 0.8); /* yellow */
+ background: rgba(235, 235, 84, 0.8);
+}
+
+.theme-firebug .chart-colored-blob[name=flash] {
+ fill: rgba(84, 235, 159, 0.8); /* cyan */
+ background: rgba(84, 235, 159, 0.8);
+}
+
+/* Responsive sidebar */
+@media (max-width: 700px) {
+ #requests-menu-toolbar {
+ height: 22px;
+ }
+
+ .requests-menu-header-button {
+ min-height: 22px;
+ padding-left: 8px;
+ }
+
+ #details-pane {
+ margin: 0 !important;
+ /* To prevent all the margin hacks to hide the sidebar. */
+ }
+
+ .requests-menu-status {
+ max-width: none;
+ width: 12vw;
+ }
+
+ .requests-menu-status-code {
+ width: auto;
+ }
+
+ .requests-menu-method,
+ .requests-menu-method-box {
+ max-width: none;
+ width: 14vw;
+ }
+
+ .requests-menu-icon-and-file {
+ width: 22vw;
+ }
+
+ .requests-menu-security-and-domain {
+ width: 18vw;
+ }
+
+ .requests-menu-type {
+ width: 10vw;
+ }
+
+ .requests-menu-transferred,
+ .requests-menu-size {
+ width: 12vw;
+ }
+}
+
+/* Platform overrides (copied in from the old platform specific files) */
+:root[platform="win"] .requests-menu-header-button > .button-box {
+ padding: 0;
+}
+
+:root[platform="win"] .requests-menu-timings-division {
+ padding-top: 1px;
+ font-size: 90%;
+}
+
+:root[platform="linux"] #headers-summary-resend {
+ padding: 4px;
+}
+
+:root[platform="linux"] #toggle-raw-headers {
+ padding: 4px;
+}
+
+/* Responsive sidebar */
+@media (max-width: 700px) {
+ :root[platform="linux"] .requests-menu-header-button {
+ font-size: 85%;
+ }
+}
diff --git a/devtools/client/themes/performance.css b/devtools/client/themes/performance.css
new file mode 100644
index 000000000..5ed3b6352
--- /dev/null
+++ b/devtools/client/themes/performance.css
@@ -0,0 +1,794 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+/* CSS Variables specific to this panel that aren't defined by the themes */
+.theme-dark {
+ --cell-border-color: rgba(255,255,255,0.15);
+ --cell-border-color-light: rgba(255,255,255,0.1);
+ --focus-cell-border-color: rgba(255,255,255,0.5);
+ --row-alt-background-color: rgba(86, 117, 185, 0.15);
+ --row-hover-background-color: rgba(86, 117, 185, 0.25);
+}
+
+.theme-light {
+ --cell-border-color: rgba(0,0,0,0.15);
+ --cell-border-color-light: rgba(0,0,0,0.1);
+ --focus-cell-border-color: rgba(0,0,0,0.3);
+ --row-alt-background-color: rgba(76,158,217,0.1);
+ --row-hover-background-color: rgba(76,158,217,0.2);
+}
+
+.theme-firebug {
+ --cell-border-color: rgba(0,0,0,0.15);
+ --cell-border-color-light: rgba(0,0,0,0.1);
+ --focus-cell-border-color: rgba(0,0,0,0.3);
+ --row-alt-background-color: rgba(76,158,217,0.1);
+ --row-hover-background-color: rgba(76,158,217,0.2);
+}
+
+/*
+ * DE-XUL: Set a sidebar width because inline XUL components will cause the flex
+ * to overflow if dynamically sized.
+ */
+.performance-tool {
+ --sidebar-width: 185px;
+}
+
+/**
+ * A generic class to hide elements, replacing the `element.hidden` attribute
+ * that we use to hide elements that can later be active
+ */
+.hidden {
+ display: none;
+ width: 0px;
+ height: 0px;
+}
+
+/* Toolbar */
+
+#performance-toolbar-control-other {
+ padding-inline-end: 5px;
+}
+
+#performance-toolbar-controls-detail-views .toolbarbutton-text {
+ padding-inline-start: 4px;
+ padding-inline-end: 8px;
+}
+
+#filter-button {
+ list-style-image: url(chrome://devtools/skin/images/filter.svg);
+}
+
+#performance-filter-menupopup > menuitem .menu-iconic-left::after {
+ content: "";
+ display: block;
+ width: 8px;
+ height: 8px;
+ margin: 0 8px;
+ border-radius: 1px;
+}
+
+/* Details panel buttons */
+
+#select-waterfall-view {
+ list-style-image: url(images/performance-icons.svg#details-waterfall);
+}
+
+#select-js-calltree-view,
+#select-memory-calltree-view {
+ list-style-image: url(images/performance-icons.svg#details-call-tree);
+}
+
+#select-js-flamegraph-view,
+#select-memory-flamegraph-view {
+ list-style-image: url(images/performance-icons.svg#details-flamegraph);
+}
+
+#select-optimizations-view {
+ list-style-image: url(images/profiler-stopwatch.svg);
+}
+
+/* Recording buttons */
+
+#clear-button::before {
+ background-image: var(--clear-icon-url);
+}
+
+#main-record-button::before {
+ background-image: url(images/profiler-stopwatch.svg);
+}
+
+#import-button::before {
+ background-image: url(images/import.svg);
+}
+
+#main-record-button .button-icon, #import-button .button-icon {
+ margin: 0;
+}
+
+#main-record-button .button-text, #import-button .button-text {
+ display: none;
+}
+
+.notice-container .record-button {
+ padding: 5px !important;
+}
+
+.notice-container .record-button.checked,
+.notice-container .record-button.checked {
+ color: var(--theme-selection-color) !important;
+ background: var(--theme-selection-background) !important;
+}
+
+/* Sidebar & recording items */
+
+#recordings-pane {
+ border-inline-end: 1px solid var(--theme-splitter-color);
+ width: var(--sidebar-width);
+}
+
+#recording-controls-mount {
+ width: var(--sidebar-width);
+}
+
+#recording-controls-mount > div {
+ width: var(--sidebar-width);
+}
+
+/*
+ * DE-XUL: The height of the toolbar is not correct without tweaking the line-height.
+ */
+#recordings-pane .devtools-toolbar {
+ line-height: 0;
+}
+
+.theme-sidebar {
+ position: relative;
+}
+
+/**
+ * DE-XUL: This is probably only needed for the html:div inside of a vbox.
+ */
+#recordings-list > div {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ overflow-y: auto;
+ overflow-x: hidden;
+}
+
+.recording-list {
+ width: var(--sidebar-width);
+ min-width: var(--sidebar-width);
+ margin: 0;
+ padding: 0;
+ background-color: var(--theme-sidebar-background);
+ border-inline-end: 1px solid var(--theme-splitter-color);
+}
+
+.recording-list-item {
+ display: flex;
+ flex-direction: column;
+ color: var(--theme-body-color);
+ border-bottom: 1px solid rgba(128,128,128,0.15);
+ padding: 8px;
+ cursor: default;
+}
+
+.recording-list-item.selected {
+ background-color: var(--theme-selection-background);
+ color: var(--theme-selection-color);
+}
+
+.recording-list-empty {
+ padding: 8px;
+}
+
+.recording-list-item-label {
+ font-size: 110%;
+}
+
+.recording-list-item-footer {
+ padding-top: 4px;
+ font-size: 90%;
+ display: flex;
+ justify-content: space-between;
+}
+
+.recording-list-item-save {
+ background: none;
+ border: none;
+ text-decoration: underline;
+ cursor: pointer;
+ font-size: 90%;
+ padding:0;
+}
+
+.recording-list-item-duration,
+.recording-list-item-save {
+ color: var(--theme-body-color-alt);
+}
+
+.recording-list-item.selected .recording-list-item-duration,
+.recording-list-item.selected .recording-list-item-save {
+ color: var(--theme-body-color-alt);
+ color: var(--theme-selection-color);
+}
+
+#recordings-list .selected label {
+ /* Text inside a selected item should not be custom colored. */
+ color: inherit !important;
+}
+
+/* Recording notices */
+
+.notice-container {
+ font-size: 120%;
+ background-color: var(--theme-body-background);
+ color: var(--theme-body-color);
+ padding-bottom: 20vh;
+}
+
+.tool-disabled-message {
+ text-align: center;
+}
+
+.console-profile-recording-notice,
+.console-profile-stop-notice {
+ overflow: hidden;
+}
+
+.console-profile-command {
+ font-family: monospace;
+ margin: 3px 2px;
+}
+
+.realtime-disabled-message,
+.realtime-disabled-on-e10s-message {
+ display: none;
+}
+
+#performance-view[e10s="disabled"] .realtime-disabled-on-e10s-message,
+#performance-view[e10s="unsupported"] .realtime-disabled-message {
+ display: initial;
+ opacity: 0.5;
+}
+
+.buffer-status-message,
+.buffer-status-message-full {
+ display: none;
+}
+
+#details-pane-container[buffer-status="in-progress"] .buffer-status-message {
+ display: initial;
+ opacity: 0.5;
+}
+
+#details-pane-container[buffer-status="full"] .buffer-status-message {
+ display: initial;
+ color: var(--theme-highlight-red);
+ font-weight: bold;
+ opacity: 1;
+}
+
+#details-pane-container[buffer-status="full"] .buffer-status-message-full {
+ display: initial;
+}
+
+/* Profile call tree */
+
+.call-tree-cells-container {
+ overflow: auto;
+}
+
+.call-tree-cells-container[categories-hidden] .call-tree-category {
+ display: none;
+}
+
+.call-tree-header {
+ font-size: 90%;
+ padding-top: 2px !important;
+ padding-bottom: 2px !important;
+}
+
+.call-tree-header[type="duration"],
+.call-tree-cell[type="duration"],
+.call-tree-header[type="self-duration"],
+.call-tree-cell[type="self-duration"] {
+ min-width: 6vw;
+ width: 6vw;
+}
+
+.call-tree-header[type="percentage"],
+.call-tree-cell[type="percentage"],
+.call-tree-header[type="self-percentage"],
+.call-tree-cell[type="self-percentage"] {
+ min-width: 5vw;
+ width: 5vw;
+}
+
+.call-tree-header[type="samples"],
+.call-tree-cell[type="samples"] {
+ min-width: 4.5vw;
+ width: 4.5vw;
+}
+
+.call-tree-header[type="count"],
+.call-tree-cell[type="count"],
+.call-tree-header[type="self-count"],
+.call-tree-cell[type="self-count"],
+.call-tree-header[type="size"],
+.call-tree-cell[type="size"],
+.call-tree-header[type="self-size"],
+.call-tree-cell[type="self-size"],
+.call-tree-header[type="count-percentage"],
+.call-tree-cell[type="count-percentage"],
+.call-tree-header[type="self-count-percentage"],
+.call-tree-cell[type="self-count-percentage"],
+.call-tree-header[type="size-percentage"],
+.call-tree-cell[type="size-percentage"],
+.call-tree-header[type="self-size-percentage"],
+.call-tree-cell[type="self-size-percentage"] {
+ min-width: 6vw;
+ width: 6vw;
+}
+
+.call-tree-header,
+.call-tree-cell {
+ -moz-box-align: center;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ padding: 1px 4px;
+ color: var(--theme-body-color);
+ border-inline-end-color: var(--cell-border-color);
+}
+
+.call-tree-header:not(:last-child),
+.call-tree-cell:not(:last-child) {
+ border-inline-end-width: 1px;
+ border-inline-end-style: solid;
+}
+
+.call-tree-header:not(:last-child) {
+ text-align: center;
+}
+
+.call-tree-cell:not(:last-child) {
+ text-align: end;
+}
+
+.call-tree-header {
+ background-color: var(--theme-tab-toolbar-background);
+}
+
+.call-tree-item .call-tree-cell,
+.call-tree-item .call-tree-cell[type=function] description {
+ -moz-user-select: text;
+ /* so that optimizations view doesn't break the lines in call tree */
+ white-space: nowrap;
+}
+
+.call-tree-item .call-tree-cell::-moz-selection,
+.call-tree-item .call-tree-cell[type=function] description::-moz-selection {
+ background-color: var(--theme-highlight-orange);
+}
+
+.call-tree-item:last-child {
+ border-bottom: 1px solid var(--cell-border-color);
+}
+
+.call-tree-item:nth-child(2n) {
+ background-color: var(--row-alt-background-color);
+}
+
+.call-tree-item:hover {
+ background-color: var(--row-hover-background-color);
+}
+
+.call-tree-item:focus {
+ background-color: var(--theme-selection-background);
+}
+
+.call-tree-item:focus description {
+ color: var(--theme-selection-color) !important;
+}
+
+.call-tree-item:focus .call-tree-cell {
+ border-inline-end-color: var(--focus-cell-border-color);
+}
+
+.call-tree-item:not([origin="content"]) .call-tree-name,
+.call-tree-item:not([origin="content"]) .call-tree-url,
+.call-tree-item:not([origin="content"]) .call-tree-line,
+.call-tree-item:not([origin="content"]) .call-tree-column {
+ /* Style chrome and non-JS nodes differently. */
+ opacity: 0.6;
+}
+
+.call-tree-name {
+ margin-inline-end: 4px !important;
+}
+
+.call-tree-url {
+ cursor: pointer;
+}
+
+.call-tree-url:hover {
+ text-decoration: underline;
+}
+
+.call-tree-url, .tree-widget-item:not(.theme-selected) .opt-url {
+ color: var(--theme-highlight-blue);
+}
+
+.call-tree-line, .tree-widget-item:not(.theme-selected) .opt-line {
+ color: var(--theme-highlight-orange);
+}
+
+.call-tree-column {
+ color: var(--theme-highlight-orange);
+ opacity: 0.6;
+}
+
+.call-tree-host {
+ margin-inline-start: 8px !important;
+ font-size: 90%;
+ color: var(--theme-content-color2);
+}
+
+.call-tree-category {
+ transform: scale(0.75);
+ transform-origin: center right;
+}
+
+/**
+ * Waterfall markers tree
+ */
+
+#waterfall-tree {
+ /* DE-XUL: convert this to display: flex once performance.xul is converted to HTML */
+ display: -moz-box;
+ -moz-box-orient: vertical;
+ -moz-box-flex: 1;
+}
+
+.waterfall-markers {
+ /* DE-XUL: convert this to display: flex once performance.xul is converted to HTML */
+ display: -moz-box;
+ -moz-box-orient: vertical;
+ -moz-box-flex: 1;
+}
+
+.waterfall-header {
+ display: flex;
+}
+
+.waterfall-header-ticks {
+ display: flex;
+ flex: auto;
+ align-items: center;
+ overflow: hidden;
+}
+
+.waterfall-header-name {
+ padding: 2px 4px;
+ font-size: 90%;
+}
+
+.waterfall-header-tick {
+ width: 100px;
+ font-size: 9px;
+ transform-origin: left center;
+ color: var(--theme-body-color);
+}
+
+.waterfall-header-tick:not(:first-child) {
+ margin-inline-start: -100px !important; /* Don't affect layout. */
+}
+
+.waterfall-background-ticks {
+ /* Background created on a <canvas> in js. */
+ /* @see devtools/client/performance/modules/widgets/waterfall-ticks.js */
+ background-image: -moz-element(#waterfall-background);
+ background-repeat: repeat-y;
+ background-position: -1px center;
+}
+
+/**
+ * Markers waterfall breakdown
+ */
+
+.waterfall-markers .tree {
+ /* DE-XUL: convert this to display: flex once performance.xul is converted to HTML */
+ display: -moz-box;
+ -moz-box-orient: vertical;
+ -moz-box-flex: 1;
+ overflow-x: hidden;
+ overflow-y: auto;
+ --waterfall-tree-row-height: 15px;
+}
+
+.waterfall-markers .tree-node {
+ display: flex;
+ height: var(--waterfall-tree-row-height);
+ line-height: var(--waterfall-tree-row-height);
+}
+
+.waterfall-tree-item {
+ display: flex;
+ flex: auto;
+}
+
+.theme-light .waterfall-markers .tree-node:not([data-depth="0"]) {
+ background-image: repeating-linear-gradient(
+ -45deg,
+ transparent 0px,
+ transparent 2px,
+ rgba(0,0,0,0.025) 2px,
+ rgba(0,0,0,0.025) 4px
+ );
+}
+
+.theme-dark .waterfall-markers .tree-node:not([data-depth="0"]) {
+ background-image: repeating-linear-gradient(
+ -45deg,
+ transparent 0px,
+ transparent 2px,
+ rgba(255,255,255,0.05) 2px,
+ rgba(255,255,255,0.05) 4px
+ );
+}
+
+.theme-light .waterfall-tree-item[data-expandable] .waterfall-marker-bullet,
+.theme-light .waterfall-tree-item[data-expandable] .waterfall-marker-bar {
+ background-image: repeating-linear-gradient(
+ -45deg,
+ transparent 0px,
+ transparent 5px,
+ rgba(255,255,255,0.35) 5px,
+ rgba(255,255,255,0.35) 10px
+ );
+}
+
+.theme-dark .waterfall-tree-item[data-expandable] .waterfall-marker-bullet,
+.theme-dark .waterfall-tree-item[data-expandable] .waterfall-marker-bar {
+ background-image: repeating-linear-gradient(
+ -45deg,
+ transparent 0px,
+ transparent 5px,
+ rgba(0,0,0,0.35) 5px,
+ rgba(0,0,0,0.35) 10px
+ );
+}
+
+.waterfall-markers .tree-node[data-expanded],
+.waterfall-markers .tree-node:not([data-depth="0"]) + .tree-node[data-depth="0"] {
+ box-shadow: 0 -1px var(--cell-border-color-light);
+}
+
+.tree-node-odd .waterfall-marker {
+ background-color: var(--row-alt-background-color);
+}
+
+.waterfall-markers .tree-node:hover {
+ background-color: var(--row-hover-background-color);
+}
+
+.waterfall-markers .tree-node-last {
+ border-bottom: 1px solid var(--cell-border-color);
+}
+
+.waterfall-tree-item.focused {
+ background-color: var(--theme-selection-background);
+}
+
+/**
+ * Marker left sidebar
+ */
+
+.waterfall-sidebar {
+ display: flex;
+ align-items: center;
+ box-sizing: border-box;
+ border-inline-end: 1px solid var(--cell-border-color);
+}
+
+.waterfall-tree-item > .waterfall-sidebar:hover,
+.waterfall-tree-item:hover > .waterfall-sidebar,
+.waterfall-tree-item.focused > .waterfall-sidebar {
+ background: transparent;
+}
+
+.waterfall-tree-item.focused > .waterfall-sidebar {
+ color: var(--theme-selection-color);
+}
+
+.waterfall-marker-bullet {
+ width: 8px;
+ height: 8px;
+ margin-inline-start: 8px;
+ margin-inline-end: 6px;
+ border-radius: 1px;
+ box-sizing: border-box;
+}
+
+.waterfall-marker-name {
+ font-size: 95%;
+ padding-bottom: 1px !important;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+/**
+ * Marker timebar
+ */
+
+.waterfall-marker {
+ display: flex;
+ flex: auto;
+ overflow: hidden;
+}
+
+.waterfall-marker-wrap {
+ display: flex;
+ align-items: center;
+ transform-origin: left center;
+}
+
+.waterfall-marker-bar {
+ height: 9px;
+ border-radius: 1px;
+ box-sizing: border-box;
+}
+
+/**
+ * OTMT markers
+ */
+
+.waterfall-tree-item[data-otmt=true] .waterfall-marker-bullet,
+.waterfall-tree-item[data-otmt=true] .waterfall-marker-bar {
+ background-color: transparent;
+ border-width: 1px;
+ border-style: solid;
+}
+
+/**
+ * Marker details view
+ */
+
+#waterfall-details {
+ padding-inline-start: 8px;
+ padding-inline-end: 8px;
+ padding-top: 2vh;
+ overflow: auto;
+ min-width: 50px;
+}
+
+#waterfall-details > * {
+ padding-top: 3px;
+}
+
+.marker-details-bullet {
+ width: 8px;
+ height: 8px;
+ border-radius: 1px;
+}
+
+.marker-details-name-label {
+ padding-inline-end: 4px;
+}
+
+.marker-details-type {
+ font-size: 1.2em;
+ font-weight: bold;
+}
+
+.marker-details-duration {
+ font-weight: bold;
+}
+
+.marker-details-customcontainer .custom-button {
+ padding: 2px 5px;
+ border-width: 1px;
+}
+
+/**
+ * Marker colors
+ */
+
+menuitem.marker-color-graphs-full-red .menu-iconic-left::after,
+.marker-color-graphs-full-red {
+ background-color: var(--theme-graphs-full-red);
+ border-color: var(--theme-graphs-full-red);
+}
+menuitem.marker-color-graphs-full-blue .menu-iconic-left::after,
+.marker-color-graphs-full-blue {
+ background-color: var(--theme-graphs-full-blue);
+ border-color: var(--theme-graphs-full-blue);
+}
+
+menuitem.marker-color-graphs-green .menu-iconic-left::after,
+.marker-color-graphs-green {
+ background-color: var(--theme-graphs-green);
+ border-color: var(--theme-graphs-green);
+}
+menuitem.marker-color-graphs-blue .menu-iconic-left::after,
+.marker-color-graphs-blue {
+ background-color: var(--theme-graphs-blue);
+ border-color: var(--theme-graphs-blue);
+}
+menuitem.marker-color-graphs-bluegrey .menu-iconic-left::after,
+.marker-color-graphs-bluegrey {
+ background-color: var(--theme-graphs-bluegrey);
+ border-color: var(--theme-graphs-bluegrey);
+}
+menuitem.marker-color-graphs-purple .menu-iconic-left::after,
+.marker-color-graphs-purple {
+ background-color: var(--theme-graphs-purple);
+ border-color: var(--theme-graphs-purple);
+}
+menuitem.marker-color-graphs-yellow .menu-iconic-left::after,
+.marker-color-graphs-yellow {
+ background-color: var(--theme-graphs-yellow);
+ border-color: var(--theme-graphs-yellow);
+}
+menuitem.marker-color-graphs-orange .menu-iconic-left::after,
+.marker-color-graphs-orange {
+ background-color: var(--theme-graphs-orange);
+ border-color: var(--theme-graphs-orange);
+}
+menuitem.marker-color-graphs-red .menu-iconic-left::after,
+.marker-color-graphs-red {
+ background-color: var(--theme-graphs-red);
+ border-color: var(--theme-graphs-red);
+}
+menuitem.marker-color-graphs-grey .menu-iconic-left::after,
+.marker-color-graphs-grey{
+ background-color: var(--theme-graphs-grey);
+ border-color: var(--theme-graphs-grey);
+}
+
+/**
+ * Configurable Options
+ *
+ * Elements can be tagged with a class and visibility is controlled via a
+ * preference being applied or removed.
+ */
+
+/**
+ * devtools.performance.ui.experimental
+ */
+menuitem.experimental-option::before {
+ content: "";
+ background-image: url(chrome://devtools/skin/images/webconsole.svg);
+ background-repeat: no-repeat;
+ background-size: 72px 60px;
+ width: 12px;
+ height: 12px;
+ display: inline-block;
+
+ background-position: -24px -24px;
+ margin: 2px 5px 0 0;
+ max-height: 12px;
+}
+.theme-light menuitem.experimental-option::before {
+ background-image: url(chrome://devtools/skin/images/webconsole.svg#light-icons);
+}
+
+#performance-options-menupopup:not(.experimental-enabled) .experimental-option,
+#performance-options-menupopup:not(.experimental-enabled) .experimental-option::before {
+ display: none;
+}
+
+/* for call tree */
+description.opt-icon {
+ margin: 0px 0px 0px 0px;
+}
+description.opt-icon::before {
+ margin: 1px 4px 0px 0px;
+}
diff --git a/devtools/client/themes/projecteditor/projecteditor.css b/devtools/client/themes/projecteditor/projecteditor.css
new file mode 100644
index 000000000..58de798e5
--- /dev/null
+++ b/devtools/client/themes/projecteditor/projecteditor.css
@@ -0,0 +1,184 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+.view-project-detail {
+ overflow: auto;
+}
+
+.plugin-hidden {
+ display: none;
+}
+
+.arrow {
+ -moz-appearance: treetwisty;
+ width: 20px;
+ height: 20px;
+}
+
+.arrow[open] {
+ -moz-appearance: treetwistyopen;
+}
+
+.arrow[invisible] {
+ visibility: hidden;
+}
+
+#projecteditor-menubar {
+ display: none;
+}
+
+#projecteditor-toolbar,
+#projecteditor-toolbar-bottom {
+ display: none; /* For now don't show the status bars */
+ min-height: 22px;
+ height: 22px;
+ background: rgb(237, 237, 237);
+}
+
+#sources {
+ overflow: auto;
+}
+
+.sources-tree {
+ overflow:auto;
+ overflow-x: hidden;
+ -moz-user-focus: normal;
+
+ /* Allows this to expand inside of parent xul element, while
+ still supporting child flexbox elements, including ellipses. */
+ -moz-box-flex: 1;
+ display: block;
+}
+
+.sources-tree input {
+ margin: -1px;
+ border: 1px solid gray;
+}
+
+#main-deck .sources-tree {
+ background: rgb(225, 225, 225);
+ min-width: 100px;
+}
+
+.entry {
+ color: #18191A;
+ display: flex;
+ align-items: center;
+}
+
+.entry .file-label {
+ display: flex;
+ flex: 1;
+ align-items: center;
+}
+
+.entry {
+ border: none;
+ box-shadow: none;
+ white-space: nowrap;
+ cursor: pointer;
+}
+
+.entry:hover:not(.entry-group-title):not(.selected) {
+ background: rgba(0, 0, 0, .05);
+}
+
+.entry.selected {
+ background: rgba(56, 117, 215, 1);
+ color: #F5F7FA;
+ outline: none;
+}
+
+.entry-group-title {
+ background: rgba(56, 117, 215, 0.8);
+ color: #F5F7FA;
+ font-weight: bold;
+ font-size: 1.05em;
+ line-height: 35px;
+ padding: 0 10px;
+}
+
+.sources-tree .entry-group-title .expander {
+ display: none;
+}
+
+.entry .expander {
+ width: 16px;
+ padding: 0;
+}
+
+.tree-collapsed .children {
+ display: none;
+}
+
+/* Plugins */
+
+#projecteditor-toolbar textbox {
+ margin: 0;
+}
+
+.projecteditor-basic-display {
+ padding: 0 3px;
+}
+
+/* App Manager */
+.project-name-label {
+ font-weight: bold;
+ padding-left: 10px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.project-flex {
+ flex: 1;
+}
+
+.project-image {
+ max-height: 25px;
+ margin-left: -10px;
+}
+
+.project-image,
+.project-status,
+.project-options {
+ flex-shrink: 0;
+}
+
+.project-status {
+ width: 10px;
+ height: 10px;
+ border-radius: 50%;
+ border: solid 1px rgba(255, 255, 255, .5);
+ margin-right: 10px;
+ visibility: hidden;
+}
+
+.project-status[status=valid] {
+ background: #70bf53;
+ visibility: visible;
+}
+
+.project-status[status=warning] {
+ background: #d99b28;
+ visibility: visible;
+}
+
+.project-status[status=error] {
+ background: #ed2655;
+ visibility: visible;
+}
+
+/* Status Bar */
+.projecteditor-file-label {
+ font-weight: bold;
+ padding-left: 29px;
+ padding-right: 10px;
+ flex: 1;
+}
+
+/* Image View */
+.editor-image {
+ padding: 10px;
+}
diff --git a/devtools/client/themes/responsivedesign.inc.css b/devtools/client/themes/responsivedesign.inc.css
new file mode 100644
index 000000000..b06359ae1
--- /dev/null
+++ b/devtools/client/themes/responsivedesign.inc.css
@@ -0,0 +1,355 @@
+%if 0
+/* 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/. */
+%endif
+
+/* Responsive Mode */
+
+.browserContainer[responsivemode] {
+ background-color: #222;
+ padding: 0 20px 20px 20px;
+}
+
+.browserStack[responsivemode] {
+ box-shadow: 0 0 7px black;
+}
+
+.devtools-responsiveui-toolbar {
+ -moz-appearance: none;
+ background: transparent;
+ /* text color is textColor from dark theme, since no theme is applied to
+ * the responsive toolbar.
+ */
+ color: hsl(210,30%,85%);
+ margin: 10px 0;
+ padding: 0;
+ box-shadow: none;
+ border-bottom-width: 0;
+}
+
+.devtools-responsiveui-textinput {
+ -moz-appearance: none;
+ background: #333;
+ color: #fff;
+ border: 1px solid #111;
+ border-radius: 2px;
+ padding: 0 5px;
+ width: 200px;
+ margin: 0;
+}
+
+.devtools-responsiveui-textinput[attention] {
+ border-color: #38ace6;
+ background: rgba(56,172,230,0.4);
+}
+
+.devtools-responsiveui-menulist,
+.devtools-responsiveui-toolbarbutton {
+ -moz-appearance: none;
+ -moz-box-align: center;
+ min-width: 32px;
+ min-height: 22px;
+ text-shadow: 0 -1px 0 hsla(210,8%,5%,.45);
+ border: 1px solid hsla(210,8%,5%,.45);
+ border-radius: 0;
+ background: linear-gradient(hsla(212,7%,57%,.35), hsla(212,7%,57%,.1)) padding-box;
+ box-shadow: 0 1px 0 hsla(210,16%,76%,.15) inset, 0 0 0 1px hsla(210,16%,76%,.15) inset, 0 1px 0 hsla(210,16%,76%,.15);
+ margin: 0 3px;
+ color: inherit;
+}
+
+.devtools-responsiveui-menulist .menulist-editable-box {
+ -moz-appearance: none;
+ background-color: transparent;
+}
+
+.devtools-responsiveui-menulist html|*.menulist-editable-input {
+ -moz-appearance: none;
+ color: inherit;
+ text-align: center;
+}
+
+.devtools-responsiveui-menulist html|*.menulist-editable-input::-moz-selection {
+ background: hsla(212,7%,57%,.35);
+}
+
+.devtools-responsiveui-toolbarbutton > .toolbarbutton-icon {
+ width: 16px;
+ height: 16px;
+}
+
+.devtools-responsiveui-toolbarbutton > .toolbarbutton-menubutton-button {
+ -moz-box-orient: horizontal;
+}
+
+.devtools-responsiveui-menulist:-moz-focusring,
+.devtools-responsiveui-toolbarbutton:-moz-focusring {
+ outline: 1px dotted hsla(210,30%,85%,0.7);
+ outline-offset: -4px;
+}
+
+.devtools-responsiveui-toolbarbutton:not([label]) > .toolbarbutton-text {
+ display: none;
+}
+
+.devtools-responsiveui-toolbarbutton:not([checked=true]):hover:active {
+ border-color: hsla(210,8%,5%,.6);
+ background: linear-gradient(hsla(220,6%,10%,.3), hsla(212,7%,57%,.15) 65%, hsla(212,7%,57%,.3));
+ box-shadow: 0 0 3px hsla(210,8%,5%,.25) inset, 0 1px 3px hsla(210,8%,5%,.25) inset, 0 1px 0 hsla(210,16%,76%,.15);
+}
+
+.devtools-responsiveui-menulist[open=true],
+.devtools-responsiveui-toolbarbutton[open=true],
+.devtools-responsiveui-toolbarbutton[checked=true] {
+ border-color: hsla(210,8%,5%,.6) !important;
+ background: linear-gradient(hsla(220,6%,10%,.6), hsla(210,11%,18%,.45) 75%, hsla(210,11%,30%,.4));
+ box-shadow: 0 1px 3px hsla(210,8%,5%,.25) inset, 0 1px 3px hsla(210,8%,5%,.25) inset, 0 1px 0 hsla(210,16%,76%,.15);
+}
+
+.devtools-responsiveui-toolbarbutton[checked=true] {
+ color: hsl(208,100%,60%);
+}
+
+.devtools-responsiveui-toolbarbutton[checked=true]:hover {
+ background-color: transparent !important;
+}
+
+.devtools-responsiveui-toolbarbutton[checked=true]:hover:active {
+ background-color: hsla(210,8%,5%,.2) !important;
+}
+
+.devtools-responsiveui-menulist > .menulist-label-box {
+ text-align: center;
+}
+
+.devtools-responsiveui-menulist > .menulist-dropmarker {
+ -moz-appearance: none;
+ display: -moz-box;
+ background-color: transparent;
+ list-style-image: url("chrome://devtools/skin/images/dropmarker.svg");
+ -moz-box-align: center;
+ border-width: 0;
+ min-width: 16px;
+}
+
+.devtools-responsiveui-toolbarbutton[type=menu-button] > .toolbarbutton-menubutton-button {
+ -moz-appearance: none;
+ color: inherit;
+ border-width: 0;
+ border-inline-end: 1px solid hsla(210,8%,5%,.45);
+ box-shadow: -1px 0 0 hsla(210,16%,76%,.15) inset, 1px 0 0 hsla(210,16%,76%,.15);
+}
+
+.devtools-responsiveui-toolbarbutton[type=menu-button]:-moz-locale-dir(rtl) > .toolbarbutton-menubutton-button {
+ box-shadow: 1px 0 0 hsla(210,16%,76%,.15) inset, -1px 0 0 hsla(210,16%,76%,.15);
+}
+
+.devtools-responsiveui-toolbarbutton[type=menu-button] {
+ padding: 0 1px;
+ -moz-box-align: stretch;
+}
+
+.devtools-responsiveui-toolbarbutton[type=menu] > .toolbarbutton-menu-dropmarker,
+.devtools-responsiveui-toolbarbutton[type=menu-button] > .toolbarbutton-menubutton-dropmarker {
+ -moz-appearance: none !important;
+ list-style-image: url("chrome://devtools/skin/images/dropmarker.svg");
+ -moz-box-align: center;
+ padding: 0 3px;
+}
+
+.devtools-responsiveui-toolbar:-moz-locale-dir(ltr) > *:first-child,
+.devtools-responsiveui-toolbar:-moz-locale-dir(rtl) > *:last-child {
+ margin-left: 0;
+}
+
+.devtools-responsiveui-close {
+ list-style-image: url("chrome://devtools/skin/images/close.svg");
+}
+
+.devtools-responsiveui-close > image {
+ filter: invert(1);
+}
+
+.devtools-responsiveui-rotate {
+ list-style-image: url("chrome://devtools/skin/images/responsivemode/responsiveui-rotate.png");
+}
+
+@media (min-resolution: 1.1dppx) {
+ .devtools-responsiveui-rotate {
+ list-style-image: url("chrome://devtools/skin/images/responsivemode/responsiveui-rotate@2x.png");
+ }
+}
+
+.devtools-responsiveui-touch {
+ list-style-image: url("chrome://devtools/skin/images/responsivemode/responsiveui-touch.png");
+ -moz-image-region: rect(0px,16px,16px,0px);
+}
+
+.devtools-responsiveui-touch[checked] {
+ -moz-image-region: rect(0px,32px,16px,16px);
+}
+
+@media (min-resolution: 1.1dppx) {
+ .devtools-responsiveui-touch {
+ list-style-image: url("chrome://devtools/skin/images/responsivemode/responsiveui-touch@2x.png");
+ -moz-image-region: rect(0px,32px,32px,0px);
+ }
+
+ .devtools-responsiveui-touch[checked] {
+ -moz-image-region: rect(0px,64px,32px,32px);
+ }
+}
+
+.devtools-responsiveui-screenshot {
+ list-style-image: url("chrome://devtools/skin/images/responsivemode/responsiveui-screenshot.png");
+}
+
+@media (min-resolution: 1.1dppx) {
+ .devtools-responsiveui-screenshot {
+ list-style-image: url("chrome://devtools/skin/images/responsivemode/responsiveui-screenshot@2x.png");
+ }
+}
+
+.devtools-responsiveui-resizebarV {
+ width: 7px;
+ height: 24px;
+ cursor: ew-resize;
+ transform: translate(12px, -12px);
+ background-size: cover;
+ background-image: url("chrome://devtools/skin/images/responsivemode/responsive-vertical-resizer.png");
+}
+
+.devtools-responsiveui-resizebarH {
+ width: 24px;
+ height: 7px;
+ cursor: ns-resize;
+ transform: translate(-12px, 12px);
+ background-size: cover;
+ background-image: url("chrome://devtools/skin/images/responsivemode/responsive-horizontal-resizer.png");
+}
+
+.devtools-responsiveui-resizehandle {
+ width: 16px;
+ height: 16px;
+ cursor: se-resize;
+ transform: translate(12px, 12px);
+ background-size: cover;
+ background-image: url("chrome://devtools/skin/images/responsivemode/responsive-se-resizer.png");
+}
+
+/* FxOS custom mode with additional buttons and phone look'n feel */
+
+/* Hide devtools manual resizer */
+.browserStack[responsivemode].fxos-mode .devtools-responsiveui-resizehandle,
+.browserStack[responsivemode].fxos-mode .devtools-responsiveui-resizebarH,
+.browserStack[responsivemode].fxos-mode .devtools-responsiveui-resizebarV {
+ display: none;
+}
+
+/* Gives responsive mode a phone look'n feel */
+.browserStack[responsivemode].fxos-mode {
+ padding: 60px 15px 0;
+
+ border-radius: 25px / 20px;
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+ border: 1px solid #FFFFFF;
+ border-bottom-width: 0;
+
+ background-color: #353535;
+
+ box-shadow: 0 3px 0.7px 1px #777777, 0 5px rgba(0, 0, 0, 0.4) inset;
+
+ background-image: linear-gradient(to right, #111 11%, #333 56%);
+ min-width: 320px;
+}
+
+.devtools-responsiveui-hardware-buttons {
+ -moz-appearance: none;
+ padding: 20px;
+
+ border: 1px solid #FFFFFF;
+ border-bottom-left-radius: 25px;
+ border-bottom-right-radius: 25px;
+ border-top-width: 0;
+
+ box-shadow: 0 3px 0.7px 1px #777777, 0 -7px rgba(0, 0, 0, 0.4) inset;
+
+ background-image: linear-gradient(to right, #111 11%, #333 56%);
+}
+
+.devtools-responsiveui-home-button {
+ -moz-user-focus: ignore;
+ width: 40px;
+ height: 30px;
+ list-style-image: url("chrome://devtools/skin/images/responsivemode/responsiveui-home.png");
+}
+
+.devtools-responsiveui-sleep-button {
+ -moz-user-focus: ignore;
+ -moz-appearance: none;
+ /* compensate browserStack top padding */
+ margin-top: -67px;
+ margin-right: 10px;
+
+ min-width: 10px;
+ width: 50px;
+ height: 5px;
+
+ border: 1px solid #444;
+ border-top-right-radius: 12px;
+ border-top-left-radius: 12px;
+ border-bottom-color: transparent;
+
+ background-image: linear-gradient(to top, #111 11%, #333 56%);
+}
+
+.devtools-responsiveui-sleep-button:hover:active {
+ background-image: linear-gradient(to top, #aaa 11%, #ddd 56%);
+}
+
+.devtools-responsiveui-volume-buttons {
+ margin-left: -29px;
+}
+
+.devtools-responsiveui-volume-up-button,
+.devtools-responsiveui-volume-down-button {
+ -moz-user-focus: ignore;
+ -moz-appearance: none;
+ border: 1px solid red;
+ min-width: 8px;
+ height: 40px;
+
+ border: 1px solid #444;
+ border-right-color: transparent;
+
+ background-image: linear-gradient(to right, #111 11%, #333 56%);
+}
+
+.devtools-responsiveui-volume-up-button:hover:active,
+.devtools-responsiveui-volume-down-button:hover:active {
+ background-image: linear-gradient(to right, #aaa 11%, #ddd 56%);
+}
+
+.devtools-responsiveui-volume-up-button {
+ border-top-left-radius: 12px;
+}
+
+.devtools-responsiveui-volume-down-button {
+ border-bottom-left-radius: 12px;
+}
+
+@media (min-resolution: 1.1dppx) {
+ .devtools-responsiveui-resizebarV {
+ background-image: url("chrome://devtools/skin/images/responsivemode/responsive-vertical-resizer@2x.png");
+ }
+
+ .devtools-responsiveui-resizebarH {
+ background-image: url("chrome://devtools/skin/images/responsivemode/responsive-horizontal-resizer@2x.png");
+ }
+
+ .devtools-responsiveui-resizehandle {
+ background-image: url("chrome://devtools/skin/images/responsivemode/responsive-se-resizer@2x.png");
+ }
+}
diff --git a/devtools/client/themes/rules.css b/devtools/client/themes/rules.css
new file mode 100644
index 000000000..9055101f5
--- /dev/null
+++ b/devtools/client/themes/rules.css
@@ -0,0 +1,561 @@
+/* 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/. */
+
+/* CSS Variables specific to this panel that aren't defined by the themes */
+.theme-light {
+ --rule-highlight-background-color: #ffee99;
+}
+
+.theme-dark {
+ --rule-highlight-background-color: #594724;
+}
+
+.theme-firebug {
+ --rule-highlight-background-color: #ffee99;
+ --rule-property-name: darkgreen;
+ --rule-property-value: darkblue;
+}
+
+/* Rule View Tabpanel */
+
+.theme-firebug .ruleview {
+ font-family: monospace;
+ font-size: 11px;
+}
+
+#sidebar-panel-ruleview {
+ margin: 0;
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ height: 100%;
+ /* Override the min-width from .inspector-tabpanel, as the rule panel can support small
+ widths */
+ min-width: 100px;
+}
+
+/* Rule View Toolbar */
+
+#ruleview-toolbar-container {
+ display: flex;
+ flex-direction: column;
+ height: auto;
+}
+
+#ruleview-toolbar {
+ display: flex;
+}
+
+#ruleview-toolbar > .devtools-searchbox:first-child {
+ padding-inline-start: 0px;
+}
+
+#ruleview-command-toolbar {
+ display: flex;
+}
+
+#pseudo-class-panel {
+ display: flex;
+ height: 24px;
+ overflow: hidden;
+ transition: height 150ms ease;
+}
+
+#pseudo-class-panel[hidden] {
+ height: 0px;
+}
+
+#pseudo-class-panel > label {
+ -moz-user-select: none;
+ flex-grow: 1;
+ display: flex;
+ align-items: center;
+}
+
+/* Rule View Container */
+
+#ruleview-container {
+ -moz-user-select: text;
+ overflow: auto;
+ flex: auto;
+ height: 100%;
+}
+
+/* This extra wrapper only serves as a way to get the content of the view focusable.
+ So that when the user reaches it either via keyboard or mouse, we know that the view
+ is focused and therefore can handle shortcuts.
+ However, for accessibility reasons, tabindex is set to -1 to avoid having to tab
+ through it, and the outline is hidden. */
+#ruleview-container-focusable {
+ height: 100%;
+ outline: none;
+}
+
+#ruleview-container.non-interactive {
+ pointer-events: none;
+ visibility: collapse;
+ transition: visibility 0.25s;
+}
+
+.ruleview-code {
+ direction: ltr;
+}
+
+.ruleview-property:not(:hover) > .ruleview-enableproperty {
+ pointer-events: none;
+}
+
+.ruleview-expandable-container[hidden] {
+ display: none;
+}
+
+.ruleview-expandable-container {
+ display: block;
+}
+
+.ruleview-namecontainer {
+ cursor: text;
+}
+
+.ruleview-propertyvaluecontainer {
+ cursor: text;
+ padding-right: 5px;
+}
+
+.ruleview-propertyvaluecontainer a {
+ cursor: pointer;
+}
+
+.ruleview-computedlist,
+.ruleview-overridden-rule-filter[hidden],
+.ruleview-warning[hidden] {
+ display: none;
+}
+
+.ruleview-computedlist[user-open],
+.ruleview-computedlist[filter-open] {
+ display: block;
+}
+
+.ruleview-rule-source {
+ text-align: end;
+ float: right;
+ max-width: 100%;
+
+ /* Force RTL direction to crop the source link at the beginning. */
+ direction: rtl;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ -moz-user-select: none;
+ margin-bottom: 2px;
+}
+
+.ruleview-rule-source-label {
+ white-space: nowrap;
+ margin: 0;
+ cursor: pointer;
+
+ /* Create an LTR embed to avoid special characters being shifted to the start due to the
+ parent node direction: rtl; */
+ direction: ltr;
+ unicode-bidi: embed
+}
+
+.ruleview-rule-source[unselectable],
+.ruleview-rule-source[unselectable] > .ruleview-rule-source-label {
+ cursor: default;
+}
+
+.theme-firebug .ruleview-rule-source-label {
+ font-family: var(--proportional-font-family);
+ font-weight: bold;
+ color: #0000FF;
+}
+
+.ruleview-rule-source:not([unselectable]):hover {
+ text-decoration: underline;
+}
+
+.ruleview-header {
+ border-top-width: 1px;
+ border-bottom-width: 1px;
+ border-top-style: solid;
+ border-bottom-style: solid;
+ padding: 1px 4px;
+ -moz-user-select: none;
+ word-wrap: break-word;
+ vertical-align: middle;
+ min-height: 1.5em;
+ line-height: 1.5em;
+ margin-top: -1px;
+}
+
+.theme-firebug .theme-gutter.ruleview-header {
+ font-family: var(--proportional-font-family);
+ font-weight: bold;
+ color: inherit;
+ border: none;
+ margin: 4px 0;
+ padding: 3px 4px 2px 4px;
+ line-height: inherit;
+ min-height: 0;
+ background: var(--theme-header-background);
+}
+
+:root[platform="win"] .ruleview-header,
+:root[platform="linux"] .ruleview-header {
+ margin-top: 4px;
+}
+
+.ruleview-header.ruleview-expandable-header {
+ cursor: pointer;
+}
+
+.ruleview-rule-pseudo-element {
+ padding-left:20px;
+ border-left: solid 10px;
+}
+
+.ruleview-rule {
+ padding: 2px 4px;
+}
+
+/**
+ * Display rules that don't match the current selected element and uneditable
+ * user agent styles differently
+ */
+.ruleview-rule[unmatched=true],
+.ruleview-rule[uneditable=true] {
+ background: var(--theme-tab-toolbar-background);
+}
+
+.ruleview-rule[unmatched=true] {
+ opacity: 0.5;
+}
+
+.ruleview-rule[uneditable=true] :focus {
+ outline: none;
+}
+
+.ruleview-rule[uneditable=true] .theme-link {
+ color: var(--theme-highlight-bluegrey);
+}
+
+.ruleview-rule[uneditable=true] .ruleview-enableproperty {
+ visibility: hidden;
+}
+
+.ruleview-rule[uneditable=true] .ruleview-swatch {
+ cursor: default;
+}
+
+.ruleview-rule[uneditable=true] .ruleview-namecontainer > .ruleview-propertyname,
+.ruleview-rule[uneditable=true] .ruleview-propertyvaluecontainer >
+.ruleview-propertyvalue {
+ border-bottom-color: transparent;
+}
+
+.theme-firebug .ruleview-namecontainer > .ruleview-propertyname,
+.theme-firebug .ruleview-propertyvaluecontainer > .ruleview-propertyvalue {
+ border-bottom: none;
+}
+
+.theme-firebug .ruleview-namecontainer > .ruleview-propertyname {
+ color: var(--rule-property-name);
+}
+
+.theme-firebug .ruleview-propertyvaluecontainer > .ruleview-propertyvalue {
+ color: var(--rule-property-value);
+}
+
+.theme-firebug .ruleview-overridden .ruleview-propertyname,
+.theme-firebug .ruleview-overridden .ruleview-propertyvalue {
+ text-decoration: line-through;
+}
+
+.theme-firebug .ruleview-enableproperty:not([checked]) ~ .ruleview-namecontainer,
+.theme-firebug .ruleview-enableproperty:not([checked]) ~ .ruleview-namecontainer *,
+.theme-firebug .ruleview-enableproperty:not([checked]) ~ .ruleview-propertyvaluecontainer,
+.theme-firebug .ruleview-enableproperty:not([checked]) ~ .ruleview-propertyvaluecontainer *,
+.theme-firebug .ruleview-overridden > * > .ruleview-computed:not(.ruleview-overridden),
+.theme-firebug .ruleview-overridden > * > .ruleview-computed:not(.ruleview-overridden) * {
+ color: #CCCCCC;
+}
+
+.ruleview-rule + .ruleview-rule {
+ border-top-width: 1px;
+ border-top-style: dotted;
+}
+
+.theme-firebug .ruleview-rule + .ruleview-rule {
+ border-top: none;
+}
+
+.ruleview-warning {
+ background-image: url(images/alerticon-warning.png);
+ background-size: 13px 12px;
+ margin-inline-start: 5px;
+ display: inline-block;
+ width: 13px;
+ height: 12px;
+}
+
+@media (min-resolution: 1.1dppx) {
+ .ruleview-warning {
+ background-image: url(images/alerticon-warning@2x.png);
+ }
+}
+
+.ruleview-overridden-rule-filter {
+ background-image: url(chrome://devtools/skin/images/filter.svg#filterinput);
+ background-size: 11px 11px;
+ margin-inline-start: 5px;
+ display: inline-block;
+ width: 11px;
+ height: 11px;
+}
+
+.ruleview-ruleopen {
+ padding-inline-end: 5px;
+}
+
+.ruleview-ruleclose {
+ cursor: text;
+ padding-right: 20px;
+}
+
+.ruleview-propertylist {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+.ruleview-rule:not(:hover) .ruleview-enableproperty {
+ visibility: hidden;
+}
+
+.ruleview-expander {
+ vertical-align: middle;
+ display: inline-block;
+}
+
+.ruleview-rule .ruleview-expander.theme-twisty:dir(rtl) {
+ /* for preventing .theme-twisty's wrong direction in rtl; Bug 1296648 */
+ transform: none;
+}
+
+.ruleview-newproperty {
+ /* (enable checkbox width: 12px) + (expander width: 15px) */
+ margin-inline-start: 27px;
+}
+
+.ruleview-namecontainer,
+.ruleview-propertyvaluecontainer,
+.ruleview-propertyname,
+.ruleview-propertyvalue {
+ text-decoration: inherit;
+}
+
+.ruleview-computedlist {
+ list-style: none;
+ padding: 0;
+}
+
+.ruleview-computed {
+ margin-inline-start: 35px;
+}
+
+.ruleview-grid,
+.ruleview-swatch {
+ cursor: pointer;
+ border-radius: 50%;
+ width: 0.9em;
+ height: 0.9em;
+ vertical-align: middle;
+ /* align the swatch with its value */
+ margin-top: -1px;
+ margin-inline-end: 5px;
+ display: inline-block;
+ position: relative;
+}
+
+.ruleview-grid {
+ background: url("chrome://devtools/skin/images/grid.svg");
+ background-size: 1em;
+ border-radius: 0;
+}
+
+.ruleview-colorswatch::before {
+ content: '';
+ background-color: #eee;
+ background-image: linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%, #ccc),
+ linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%, #ccc);
+ background-size: 12px 12px;
+ background-position: 0 0, 6px 6px;
+ position: absolute;
+ border-radius: 50%;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ z-index: -1;
+}
+
+.ruleview-bezierswatch {
+ background: url("chrome://devtools/skin/images/cubic-bezier-swatch.png");
+ background-size: 1em;
+}
+
+.ruleview-filterswatch {
+ background: url("chrome://devtools/skin/images/filter-swatch.svg");
+ background-size: 1em;
+}
+
+.ruleview-angleswatch {
+ background: url("chrome://devtools/skin/images/angle-swatch.svg");
+ background-size: 1em;
+}
+
+@media (min-resolution: 1.1dppx) {
+ .ruleview-bezierswatch {
+ background: url("chrome://devtools/skin/images/cubic-bezier-swatch@2x.png");
+ background-size: 1em;
+ }
+}
+
+.ruleview-overridden {
+ text-decoration: line-through;
+}
+
+.theme-light .ruleview-overridden {
+ text-decoration-color: var(--theme-content-color3);
+}
+
+.styleinspector-propertyeditor {
+ border: 1px solid #CCC;
+ padding: 0;
+ margin: -1px -3px -1px -1px;
+}
+
+.theme-firebug .styleinspector-propertyeditor {
+ border: 1px solid var(--theme-splitter-color);
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.5);
+}
+
+.ruleview-property {
+ border-left: 3px solid transparent;
+ clear: right;
+}
+
+.ruleview-propertycontainer > * {
+ vertical-align: middle;
+}
+
+.ruleview-property[dirty] {
+ border-left-color: var(--theme-highlight-green);
+}
+
+.ruleview-highlight {
+ background-color: var(--rule-highlight-background-color);
+}
+
+.ruleview-namecontainer > .ruleview-propertyname,
+.ruleview-propertyvaluecontainer > .ruleview-propertyvalue {
+ border-bottom: 1px dashed transparent;
+}
+
+.ruleview-namecontainer:hover > .ruleview-propertyname,
+.ruleview-propertyvaluecontainer:hover > .ruleview-propertyvalue {
+ border-bottom-color: hsl(0,0%,50%);
+}
+
+.ruleview-selectorcontainer {
+ word-wrap: break-word;
+ cursor: text;
+}
+
+.ruleview-selector-separator,
+.ruleview-selector-unmatched {
+ color: #888;
+}
+
+.ruleview-selector-matched > .ruleview-selector-attribute {
+ /* TODO: Bug 1178535 Awaiting UX feedback on highlight colors */
+}
+
+.ruleview-selector-matched > .ruleview-selector-pseudo-class {
+ /* TODO: Bug 1178535 Awaiting UX feedback on highlight colors */
+}
+
+.ruleview-selector-matched > .ruleview-selector-pseudo-class-lock {
+ font-weight: bold;
+ color: var(--theme-highlight-orange);
+}
+
+.theme-firebug .ruleview-selector > .ruleview-selector-matched,
+.theme-firebug .ruleview-selector > .ruleview-selector-separator,
+.theme-firebug .ruleview-selector > .ruleview-selector-unmatched {
+ color: inherit;
+}
+
+.ruleview-selectorhighlighter {
+ background: url("chrome://devtools/skin/images/vview-open-inspector.png") no-repeat 0 0;
+ padding-left: 16px;
+ margin-left: 5px;
+ cursor: pointer;
+}
+
+.ruleview-selectorhighlighter:hover {
+ filter: url(images/filters.svg#checked-icon-state);
+}
+
+.ruleview-grid.active,
+.ruleview-selectorhighlighter:active,
+.ruleview-selectorhighlighter.highlighted {
+ filter: url(images/filters.svg#checked-icon-state) brightness(0.9);
+}
+
+#ruleview-add-rule-button::before {
+ background-image: url("chrome://devtools/skin/images/add.svg");
+ background-size: cover;
+}
+
+#pseudo-class-panel-toggle::before {
+ background-image: url("chrome://devtools/skin/images/pseudo-class.svg");
+ background-size: cover;
+}
+
+.ruleview-overridden-rule-filter {
+ opacity: 0.8;
+}
+.ruleview-overridden-rule-filter:hover {
+ opacity: 1;
+}
+
+.theme-firebug .ruleview-overridden {
+ text-decoration: none;
+}
+
+/* Firebug theme disable/enable CSS rule. Firebug theme uses its own
+ icons to indicate when CSS rules can be disabled or enabled. */
+
+.theme-firebug .ruleview-rule .theme-checkbox {
+ background-repeat: no-repeat;
+ background-size: 12px 12px;
+ background-image: url(chrome://devtools/skin/images/firebug/disable.svg);
+ background-position: 0 0;
+}
+
+.theme-firebug .ruleview-rule .theme-checkbox:not([checked]){
+ filter: grayscale(1);
+}
+
+.theme-firebug .ruleview-rule .theme-checkbox[checked] {
+ background-position: 0 0;
+}
+
+.theme-firebug .ruleview-property:not(:hover) .ruleview-enableproperty {
+ visibility: hidden;
+}
diff --git a/devtools/client/themes/scratchpad.css b/devtools/client/themes/scratchpad.css
new file mode 100644
index 000000000..651d4efe9
--- /dev/null
+++ b/devtools/client/themes/scratchpad.css
@@ -0,0 +1,12 @@
+/* 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/. */
+
+#scratchpad-sidebar > tabs {
+ height: 0;
+ border: none;
+}
+
+#sp-toolbar {
+ border: none;
+}
diff --git a/devtools/client/themes/shadereditor.css b/devtools/client/themes/shadereditor.css
new file mode 100644
index 000000000..53ef94235
--- /dev/null
+++ b/devtools/client/themes/shadereditor.css
@@ -0,0 +1,109 @@
+/* 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/. */
+
+/* Reload and waiting notices */
+
+.notice-container {
+ margin-top: -50vh;
+ color: var(--theme-body-color-alt);
+}
+
+#reload-notice {
+ font-size: 120%;
+}
+
+#waiting-notice {
+ font-size: 110%;
+}
+
+/* Shaders pane */
+
+#shaders-pane {
+ min-width: 150px;
+}
+
+.program-item {
+ padding: 2px 0px;
+}
+
+.side-menu-widget-item-checkbox {
+ -moz-appearance: none;
+ opacity: 0;
+ transition: opacity .15s ease-out 0s;
+}
+
+/* Only show the checkbox when the source is hovered over, is selected, or if it
+ * is not checked. */
+.side-menu-widget-item:hover > .side-menu-widget-item-checkbox,
+.side-menu-widget-item.selected > .side-menu-widget-item-checkbox,
+.side-menu-widget-item-checkbox:not([checked]) {
+ opacity: 1;
+ transition: opacity .15s ease-out 0s;
+}
+
+.side-menu-widget-item-checkbox .checkbox-check {
+ -moz-appearance: none;
+ background-image: url(images/item-toggle.svg);
+ background-color: transparent;
+ width: 16px;
+ height: 16px;
+ border: 0;
+}
+
+.side-menu-widget-item-checkbox:not([checked]) .checkbox-check,
+.side-menu-widget-item-checkbox:not([checked]) + vbox {
+ opacity: 0.3;
+}
+
+.side-menu-widget-item:not(.selected) .checkbox-check {
+ filter: var(--icon-filter);
+}
+
+/* Make sure icon is white when the item is selected */
+.side-menu-widget-item.selected .checkbox-check {
+ filter: invert(1);
+}
+
+/* Shader source editors */
+
+.editor-label {
+ padding: 1px 12px;
+ border-top: 1px solid;
+}
+
+.editor-label {
+ background: var(--theme-toolbar-background);
+ border-color: var(--theme-splitter-color);
+ color: var(--theme-body-color-alt);
+}
+
+.editor-label[selected] {
+ background-color: var(--theme-selection-background);
+ color: var(--theme-selection-color);
+}
+
+/* Responsive sidebar */
+
+@media (max-width: 700px) {
+ #shaders-pane {
+ max-height: 60vh;
+ }
+
+ #editors-splitter {
+ border-color: transparent;
+ }
+
+ .side-menu-widget-container {
+ box-shadow: none !important;
+ }
+
+ .side-menu-widget-item-arrow {
+ background-image: none !important;
+ }
+
+ .editor-label {
+ -moz-box-ordinal-group: 0;
+ border-bottom: 1px solid;
+ }
+}
diff --git a/devtools/client/themes/shims/common.css b/devtools/client/themes/shims/common.css
new file mode 100644
index 000000000..c8a99561f
--- /dev/null
+++ b/devtools/client/themes/shims/common.css
@@ -0,0 +1,10 @@
+/* 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/. */
+
+ /**
+ * This file only exists to support add-ons which import this style sheet at a
+ * specific path.
+ */
+
+@import url("resource://devtools/client/themes/common.css");
diff --git a/devtools/client/themes/shims/jar.mn b/devtools/client/themes/shims/jar.mn
new file mode 100644
index 000000000..e1025dce7
--- /dev/null
+++ b/devtools/client/themes/shims/jar.mn
@@ -0,0 +1,6 @@
+# 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/.
+
+browser.jar:
+ skin/classic/browser/devtools/common.css (common.css)
diff --git a/devtools/client/themes/shims/moz.build b/devtools/client/themes/shims/moz.build
new file mode 100644
index 000000000..c44334263
--- /dev/null
+++ b/devtools/client/themes/shims/moz.build
@@ -0,0 +1,8 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+# Shim old theme paths used by DevTools add-ons
+JAR_MANIFESTS += ['jar.mn']
diff --git a/devtools/client/themes/splitters.css b/devtools/client/themes/splitters.css
new file mode 100644
index 000000000..ade867d52
--- /dev/null
+++ b/devtools/client/themes/splitters.css
@@ -0,0 +1,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/. */
+
+/* This file is loaded by both browser.xul and toolbox.xul. Therefore, rules
+ defined here can not rely on toolbox.xul variables. */
+
+/* Splitters */
+
+:root {
+ /* Define the widths of the draggable areas on each side of a splitter. top
+ and bottom widths are used for horizontal splitters, inline-start and
+ inline-end for side splitters.*/
+
+ --devtools-splitter-top-width: 2px;
+ --devtools-splitter-bottom-width: 2px;
+
+ /* Small draggable area on inline-start to avoid overlaps on scrollbars.*/
+ --devtools-splitter-inline-start-width: 1px;
+ --devtools-splitter-inline-end-width: 4px;
+}
+
+:root[devtoolstheme="light"] {
+ /* These variables are used in browser.xul but inside the toolbox they are overridden by --theme-splitter-color */
+ --devtools-splitter-color: #dde1e4;
+}
+
+:root[devtoolstheme="dark"],
+:root[devtoolstheme="firebug"] {
+ --devtools-splitter-color: #42484f;
+}
+
+.devtools-horizontal-splitter,
+.devtools-side-splitter {
+ -moz-appearance: none;
+ background-image: none;
+ border: 0;
+ border-style: solid;
+ border-color: transparent;
+ background-color: var(--devtools-splitter-color);
+ background-clip: content-box;
+ position: relative;
+
+ box-sizing: border-box;
+
+ /* Positive z-index positions the splitter on top of its siblings and makes
+ it clickable on both sides. */
+ z-index: 1;
+}
+
+.devtools-horizontal-splitter {
+ min-height: calc(var(--devtools-splitter-top-width) +
+ var(--devtools-splitter-bottom-width) + 1px);
+
+ border-top-width: var(--devtools-splitter-top-width);
+ border-bottom-width: var(--devtools-splitter-bottom-width);
+
+ margin-top: calc(-1 * var(--devtools-splitter-top-width) - 1px);
+ margin-bottom: calc(-1 * var(--devtools-splitter-bottom-width));
+
+ cursor: n-resize;
+}
+
+.devtools-side-splitter {
+ min-width: calc(var(--devtools-splitter-inline-start-width) +
+ var(--devtools-splitter-inline-end-width) + 1px);
+
+ border-inline-start-width: var(--devtools-splitter-inline-start-width);
+ border-inline-end-width: var(--devtools-splitter-inline-end-width);
+
+ margin-inline-start: calc(-1 * var(--devtools-splitter-inline-start-width) - 1px);
+ margin-inline-end: calc(-1 * var(--devtools-splitter-inline-end-width));
+
+ cursor: e-resize;
+}
+
+.devtools-horizontal-splitter.disabled,
+.devtools-side-splitter.disabled {
+ pointer-events: none;
+}
diff --git a/devtools/client/themes/splitview.css b/devtools/client/themes/splitview.css
new file mode 100644
index 000000000..291867f3d
--- /dev/null
+++ b/devtools/client/themes/splitview.css
@@ -0,0 +1,75 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+.theme-dark {
+ --sidemenu-selected-arrow: url(images/item-arrow-dark-ltr.svg);
+ --sidemenu-selected-arrow-rtl: url(images/item-arrow-dark-rtl.svg);
+}
+.theme-light {
+ --sidemenu-selected-arrow: url(images/item-arrow-ltr.svg);
+ --sidemenu-selected-arrow-rtl: url(images/item-arrow-rtl.svg);
+}
+
+.splitview-nav-container .devtools-throbber {
+ display: none;
+ text-align: center;
+}
+
+.loading .splitview-nav-container .devtools-throbber {
+ display: block;
+}
+
+.splitview-nav {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ background-color: var(--theme-sidebar-background);
+}
+
+.splitview-nav > li {
+ padding-inline-end: 8px;
+ -moz-box-align: center;
+ outline: 0;
+ vertical-align: bottom;
+ border-bottom: 1px solid rgba(128,128,128,0.15);
+}
+
+.placeholder {
+ -moz-box-flex: 1;
+ text-align: center;
+}
+
+.splitview-nav > li.splitview-active {
+ background-color: var(--theme-selection-background);
+ color: var(--theme-selection-color);
+ background-image: var(--sidemenu-selected-arrow);
+ background-repeat: no-repeat;
+ background-position: center right;
+}
+
+.splitview-nav > li.splitview-active:-moz-locale-dir(rtl) {
+ background-image: var(--sidemenu-selected-arrow-rtl);
+ background-position: center left;
+}
+
+/* Toolbars */
+
+.splitview-main > .devtools-toolbar {
+ background-origin: border-box;
+ background-clip: border-box;
+}
+
+.splitview-main > toolbar,
+.loading .splitview-nav-container {
+ border-inline-end: 1px solid var(--theme-splitter-color);
+}
+
+.splitview-main > .devtools-toolbarbutton {
+ font-size: 11px;
+ padding: 0 8px;
+ width: auto;
+ min-width: 48px;
+ min-height: 0;
+}
diff --git a/devtools/client/themes/storage.css b/devtools/client/themes/storage.css
new file mode 100644
index 000000000..1e611f842
--- /dev/null
+++ b/devtools/client/themes/storage.css
@@ -0,0 +1,49 @@
+/* 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/. */
+
+/* Storage Host Tree */
+
+#storage-tree {
+ min-width: 220px;
+ max-width: 500px;
+ overflow: auto;
+}
+
+#storage-tree {
+ background: var(--theme-sidebar-background);
+}
+
+#storage-tree .tree-widget-item[type="store"]:after {
+ background-image: url(chrome://devtools/skin/images/tool-storage.svg);
+ background-size: 18px 18px;
+ background-position: -1px 0;
+}
+
+/* Columns with date should have a min width so that date is visible */
+#expires, #lastAccessed, #creationTime {
+ min-width: 150px;
+}
+
+/* Variables View Sidebar */
+
+#storage-sidebar {
+ max-width: 500px;
+ min-width: 250px;
+}
+
+/* Responsive sidebar */
+@media (max-width: 700px) {
+ #storage-tree,
+ #storage-sidebar {
+ max-width: 100%;
+ }
+
+ #storage-table #path {
+ display: none;
+ }
+
+ #storage-table .table-widget-cell {
+ min-width: 100px;
+ }
+}
diff --git a/devtools/client/themes/styleeditor.css b/devtools/client/themes/styleeditor.css
new file mode 100644
index 000000000..db70a340a
--- /dev/null
+++ b/devtools/client/themes/styleeditor.css
@@ -0,0 +1,445 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+#style-editor-chrome {
+ -moz-box-flex: 1;
+}
+
+.splitview-nav > li,
+.stylesheet-info,
+.stylesheet-more,
+.stylesheet-rule-count,
+li.splitview-active > hgroup > .stylesheet-more > h3 > .stylesheet-saveButton,
+li:hover > hgroup > .stylesheet-more > h3 > .stylesheet-saveButton {
+ display: -moz-box;
+}
+
+.devtools-toolbar > spacer {
+ -moz-box-flex: 1;
+}
+
+.style-editor-newButton {
+ list-style-image: url(images/add.svg);
+}
+
+.style-editor-importButton {
+ list-style-image: url(images/import.svg);
+}
+
+.stylesheet-details-container {
+ -moz-box-flex: 1;
+}
+
+.stylesheet-media-container {
+ overflow-y: auto;
+}
+
+.stylesheet-error-message {
+ display: none;
+}
+
+li.error > .stylesheet-info > .stylesheet-more > .stylesheet-error-message {
+ display: block;
+}
+
+.stylesheet-title,
+.stylesheet-name {
+ text-decoration: none;
+}
+
+.stylesheet-name {
+ font-size: 13px;
+ white-space: nowrap;
+}
+
+.stylesheet-name > label {
+ display: inline;
+ cursor: pointer;
+}
+
+.stylesheet-info > h1 {
+ -moz-box-flex: 1;
+}
+
+.splitview-nav > li > hgroup.stylesheet-info {
+ -moz-box-pack: center;
+}
+
+.stylesheet-more > spacer {
+ -moz-box-flex: 1;
+}
+
+.theme-dark .stylesheet-title,
+.theme-dark .stylesheet-name {
+ color: var(--theme-selection-color);
+}
+
+.theme-dark .stylesheet-rule-count,
+.theme-dark .stylesheet-linked-file,
+.theme-dark .stylesheet-saveButton {
+ color: var(--theme-body-color-alt);
+}
+
+.theme-light .stylesheet-title,
+.theme-light .stylesheet-name {
+ color: var(--theme-body-color-alt);
+}
+
+.theme-light .stylesheet-rule-count,
+.theme-light .stylesheet-linked-file,
+.theme-light .stylesheet-saveButton {
+ color: var(--theme-body-color);
+}
+
+.stylesheet-saveButton {
+ display: none;
+ margin-top: 0px;
+ margin-bottom: 0px;
+ text-decoration: underline;
+ cursor: pointer;
+}
+
+.splitview-active .stylesheet-title,
+.splitview-active .stylesheet-name,
+.theme-light .splitview-active .stylesheet-rule-count,
+.theme-light .splitview-active .stylesheet-linked-file,
+.theme-light .splitview-active .stylesheet-saveButton {
+ color: var(--theme-selection-color);
+}
+
+.splitview-nav:focus {
+ outline: 0; /* focus ring is on the stylesheet name */
+}
+
+.splitview-nav > li {
+ -moz-box-orient: horizontal;
+}
+
+.splitview-nav > li > hgroup {
+ display: -moz-box;
+ -moz-box-orient: vertical;
+ -moz-box-flex: 1;
+}
+
+.splitview-nav > li.unsaved > hgroup .stylesheet-name {
+ font-style: italic;
+}
+
+.splitview-nav:-moz-locale-dir(ltr) > li.unsaved > hgroup .stylesheet-name:before,
+.splitview-nav:-moz-locale-dir(rtl) > li.unsaved > hgroup .stylesheet-name:after {
+ font-style: italic;
+}
+
+.splitview-nav.empty > p {
+ padding: 0 10px;
+}
+
+.stylesheet-sidebar {
+ max-width: 400px;
+ min-width: 100px;
+ border-color: var(--theme-splitter-color);
+}
+
+.theme-light .media-rule-label {
+ border-bottom-color: #cddae5; /* Grey */
+}
+
+.theme-dark .media-rule-label {
+ border-bottom-color: #303b47; /* Grey */
+}
+
+.media-rule-label {
+ display: flex;
+ padding: 4px;
+ cursor: pointer;
+ border-bottom: 1px solid;
+}
+
+.media-responsive-mode-toggle {
+ color: var(--theme-highlight-blue);
+ text-decoration: underline;
+}
+
+.media-rule-line {
+ padding-inline-start: 4px;
+}
+
+.media-condition-unmatched {
+ opacity: 0.4;
+}
+
+.media-rule-condition {
+ flex: 1;
+ overflow: hidden;
+}
+
+.stylesheet-enabled {
+ display: -moz-box;
+ cursor: pointer;
+ padding: 8px 0;
+ margin: 0 8px;
+ background-image: url(images/item-toggle.svg);
+ background-repeat: no-repeat;
+ background-clip: content-box;
+ background-position: center;
+ background-size: 16px;
+ width: 24px;
+ height: 40px;
+ filter: var(--icon-filter);
+}
+
+.disabled > .stylesheet-enabled {
+ opacity: 0.3;
+}
+
+/* Invert the toggle icon in the active row for light theme */
+.theme-light .splitview-nav > li.splitview-active .stylesheet-enabled {
+ filter: invert(1);
+}
+
+.splitview-nav > li > .stylesheet-enabled:focus,
+.splitview-nav > li:hover > .stylesheet-enabled {
+ outline: 0;
+}
+
+.stylesheet-linked-file:not(:empty){
+ margin-inline-end: 0.4em;
+}
+
+.stylesheet-linked-file:not(:empty):before {
+ margin-inline-start: 0.4em;
+ content: " ↳ ";
+}
+
+li.unsaved > hgroup > h1 > .stylesheet-name:before {
+ content: "*";
+}
+
+li.linked-file-error .stylesheet-linked-file {
+ text-decoration: line-through;
+}
+
+li.linked-file-error .stylesheet-linked-file:after {
+ font-size: 110%;
+ content: " ✘";
+}
+
+li.linked-file-error .stylesheet-rule-count {
+ visibility: hidden;
+}
+
+.stylesheet-more > h3 {
+ font-size: 11px;
+ margin-inline-end: 2px;
+}
+
+.devtools-searchinput,
+.devtools-filterinput {
+ max-width: 25ex;
+ font-size: 11px;
+}
+
+.placeholder a {
+ text-decoration: underline;
+}
+
+h1,
+h2,
+h3 {
+ font-size: inherit;
+ font-weight: normal;
+ margin: 0;
+ padding: 0;
+}
+
+@media (max-width: 700px) {
+ .stylesheet-sidebar {
+ width: 150px;
+ }
+}
+
+/* portrait mode */
+@media (max-width: 550px) {
+ li.splitview-active > hgroup > .stylesheet-more > .stylesheet-rule-count,
+ li:hover > hgroup > .stylesheet-more > .stylesheet-rule-count {
+ display: none;
+ }
+
+ .splitview-nav {
+ box-shadow: none;
+ }
+
+ .splitview-nav > li.splitview-active {
+ background-size: 0 0, 0 0, auto;
+ }
+
+ .stylesheet-enabled {
+ padding: 0;
+ background-position: 0 0;
+ height: 24px;
+ }
+
+ .disabled > .stylesheet-enabled {
+ background-position: -24px 0;
+ }
+
+ .splitview-nav > li > hgroup.stylesheet-info {
+ -moz-box-align: baseline;
+ -moz-box-orient: horizontal;
+ -moz-box-flex: 1;
+ }
+
+ .stylesheet-sidebar {
+ width: 180px;
+ }
+
+ .stylesheet-more {
+ -moz-box-flex: 1;
+ -moz-box-pack: end;
+ }
+
+ .stylesheet-more > spacer {
+ -moz-box-flex: 0;
+ }
+}
+
+/* CSS coverage */
+.csscoverage-report {
+ background-color: var(--theme-toolbar-background);
+ -moz-box-orient: horizontal;
+}
+
+.csscoverage-report-container {
+ height: 100vh;
+ padding: 0 10px;
+ overflow-x: hidden;
+ overflow-y: auto;
+ -moz-box-flex: 1;
+}
+
+.csscoverage-report-content {
+ margin: 20px auto;
+ -moz-column-width: 300px;
+ font-size: 13px;
+ -moz-user-select: text;
+}
+
+.csscoverage-report-summary,
+.csscoverage-report-unused,
+.csscoverage-report-optimize {
+ display: inline-block;
+}
+
+.csscoverage-report-unused,
+.csscoverage-report-optimize {
+ flex: 1;
+ min-width: 0;
+}
+
+@media (max-width: 950px) {
+ .csscoverage-report-content {
+ display: block;
+ }
+
+ .csscoverage-report-summary {
+ display: block;
+ text-align: center;
+ }
+}
+
+.csscoverage-report h1 {
+ font-size: 120%;
+}
+
+.csscoverage-report h2 {
+ font-size: 110%;
+}
+
+.csscoverage-report h1,
+.csscoverage-report h2,
+.csscoverage-report h3 {
+ font-weight: bold;
+ margin: 10px 0;
+}
+
+.csscoverage-report code,
+.csscoverage-report textarea {
+ font-family: var(--monospace-font-family);
+ font-size: inherit;
+}
+
+.csscoverage-list:after {
+ content: ', ';
+}
+
+.csscoverage-list:last-child:after {
+ display: none;
+}
+
+.csscoverage-report textarea {
+ width: 100%;
+ height: 100px;
+}
+
+.csscoverage-report a {
+ cursor: pointer;
+ text-decoration: underline;
+}
+
+.csscoverage-report > .csscoverage-toolbar {
+ border: none;
+ margin: 0;
+ padding: 0;
+}
+
+.csscoverage-report > .csscoverage-toolbarbutton {
+ min-width: 4em;
+ min-height: 100vh;
+ margin: 0;
+ padding: 0;
+ border-radius: 0;
+ border-top: none;
+ border-bottom: none;
+ border-inline-start: none;
+}
+
+.csscoverage-report .pie-table-chart-container {
+ -moz-box-orient: vertical;
+ text-align: start;
+}
+
+.chart-colored-blob[name="Used Preload"] {
+ fill: var(--theme-highlight-pink);
+ background: var(--theme-highlight-pink);
+}
+
+.chart-colored-blob[name=Used] {
+ fill: var(--theme-highlight-green);
+ background: var(--theme-highlight-green);
+}
+
+.chart-colored-blob[name=Unused] {
+ fill: var(--theme-highlight-lightorange);
+ background: var(--theme-highlight-lightorange);
+}
+
+/* Undo 'largest' customization */
+.theme-dark .pie-chart-slice[largest] {
+ stroke-width: 1px;
+ stroke: rgba(0,0,0,0.2);
+}
+
+.theme-light .pie-chart-slice[largest] {
+ stroke-width: 1px;
+ stroke: rgba(255,255,255,0.8);
+}
+
+.csscoverage-report .pie-chart-slice {
+ cursor: default;
+}
+
+.csscoverage-report-chart {
+ margin: 0 20px;
+}
diff --git a/devtools/client/themes/toolbars.css b/devtools/client/themes/toolbars.css
new file mode 100644
index 000000000..75a807d51
--- /dev/null
+++ b/devtools/client/themes/toolbars.css
@@ -0,0 +1,216 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+/* CSS Variables specific to the devtools toolbar that aren't defined by the themes */
+.theme-light {
+ --toolbar-tab-hover: rgba(170, 170, 170, .2);
+ --toolbar-tab-hover-active: rgba(170, 170, 170, .4);
+ --searchbox-background-color: #ffee99;
+ --searchbox-border-color: #ffbf00;
+ --searcbox-no-match-background-color: #ffe5e5;
+ --searcbox-no-match-border-color: #e52e2e;
+ --magnifying-glass-image: url(chrome://devtools/skin/images/search.svg);
+ --filter-image: url(chrome://devtools/skin/images/filter.svg);
+ --tool-options-image: url(chrome://devtools/skin/images/tool-options.svg);
+ --icon-filter: none;
+ --checked-icon-filter: url(chrome://devtools/skin/images/filters.svg#checked-icon-state);
+ --toolbar-button-border-color: rgba(170, 170, 170, .5);
+}
+
+.theme-dark {
+ --toolbar-tab-hover: hsla(206, 37%, 4%, .2);
+ --toolbar-tab-hover-active: hsla(206, 37%, 4%, .4);
+ --searchbox-background-color: #4d4222;
+ --searchbox-border-color: #d99f2b;
+ --searcbox-no-match-background-color: #402325;
+ --searcbox-no-match-border-color: #cc3d3d;
+ --magnifying-glass-image: url(chrome://devtools/skin/images/search.svg);
+ --filter-image: url(chrome://devtools/skin/images/filter.svg);
+ --tool-options-image: url(chrome://devtools/skin/images/tool-options.svg);
+ --icon-filter: invert(1);
+ --checked-icon-filter: url(chrome://devtools/skin/images/filters.svg#dark-theme-checked-icon-state);
+ --toolbar-button-border-color: rgba(0, 0, 0, .4);
+}
+
+.theme-firebug {
+ --magnifying-glass-image: url(chrome://devtools/skin/images/search.svg);
+ --tool-options-image: url(chrome://devtools/skin/images/firebug/tool-options.svg);
+ --icon-filter: none;
+ --checked-icon-filter: none;
+ --toolbar-button-border-color: rgba(170, 170, 170, .5);
+}
+
+
+/* Toolbars */
+.devtools-toolbar,
+.devtools-sidebar-tabs tabs {
+ -moz-appearance: none;
+ padding: 0;
+ border-width: 0;
+ border-bottom-width: 1px;
+ border-style: solid;
+ height: 24px;
+ line-height: 24px;
+ box-sizing: border-box;
+}
+
+.devtools-toolbar {
+ padding: 0 3px;
+}
+
+.devtools-toolbar checkbox {
+ margin: 0 2px;
+ padding: 0;
+ line-height: -moz-block-height;
+}
+
+.devtools-toolbar checkbox .checkbox-check {
+ margin: 0;
+ padding: 0;
+ vertical-align: bottom;
+}
+
+.devtools-toolbar checkbox .checkbox-label-box {
+ border: none !important; /* overrides .checkbox-label-box from checkbox.css */
+}
+
+.devtools-toolbar checkbox .checkbox-label-box .checkbox-label {
+ margin: 0 6px !important; /* overrides .checkbox-label from checkbox.css */
+ padding: 0;
+}
+
+.devtools-separator {
+ margin: 0 2px;
+ width: 2px;
+ background-image: linear-gradient(transparent 15%, var(--theme-splitter-color) 15%, var(--theme-splitter-color) 85%, transparent 85%);
+ background-size: 1px 100%;
+ background-repeat: no-repeat;
+ background-position: 0, 1px, 2px;
+}
+
+/* In-tools sidebar */
+.devtools-sidebar-tabs {
+ -moz-appearance: none;
+ margin: 0;
+ height: 100%;
+}
+
+.devtools-sidebar-tabs > tabpanels {
+ -moz-appearance: none;
+ background: transparent;
+ padding: 0;
+ border: 0;
+}
+
+.theme-light .devtools-sidebar-tabs > tabpanels {
+ background: var(--theme-sidebar-background);
+ color: var(--theme-body-color);
+}
+
+.devtools-sidebar-tabs tabs {
+ position: static;
+ font: inherit;
+ margin-bottom: 0;
+ overflow: hidden;
+}
+
+.devtools-sidebar-alltabs {
+ -moz-appearance: none;
+ height: 24px;
+ line-height: 24px;
+ padding: 0 4px;
+ margin: 0;
+ border-width: 0 0 1px 0;
+ border-inline-start-width: 1px;
+ border-style: solid;
+}
+
+.devtools-sidebar-alltabs .toolbarbutton-icon {
+ display: none;
+}
+
+.devtools-sidebar-tabs tabs > .tabs-right,
+.devtools-sidebar-tabs tabs > .tabs-left {
+ display: none;
+}
+
+.devtools-sidebar-tabs tabs > tab {
+ -moz-appearance: none;
+ /* We want to match the height of a toolbar with a toolbarbutton
+ * First, we need to replicated the padding of toolbar (4px),
+ * then we need to take the border of the buttons into account (1px).
+ */
+ padding: 0 3px;
+ margin: 0;
+ min-width: 78px;
+ text-align: center;
+ background-color: transparent;
+ color: inherit;
+ -moz-box-flex: 1;
+ border-width: 0;
+ border-inline-start-width: 1px;
+ border-style: solid;
+ border-radius: 0;
+ position: static;
+ text-shadow: none;
+}
+
+.devtools-sidebar-tabs tabs > tab {
+ border-image: linear-gradient(transparent 15%, var(--theme-splitter-color) 15%, var(--theme-splitter-color) 85%, transparent 85%) 1 1;
+}
+
+.devtools-sidebar-tabs tabs > tab[selected],
+.devtools-sidebar-tabs tabs > tab[selected] + tab {
+ border-image: linear-gradient(var(--theme-splitter-color), var(--theme-splitter-color)) 1 1;
+}
+
+.devtools-sidebar-tabs tabs > tab:first-child {
+ border-inline-start-width: 0;
+}
+
+.devtools-sidebar-tabs tabs > tab:hover {
+ background: rgba(0, 0, 0, 0.12);
+}
+
+.devtools-sidebar-tabs tabs > tab:hover:active {
+ background: rgba(0, 0, 0, 0.2);
+}
+
+.devtools-sidebar-tabs tabs > tab[selected],
+.devtools-sidebar-tabs tabs > tab[selected]:hover:active {
+ color: var(--theme-selection-color);
+ background: var(--theme-selection-background);
+}
+
+/* Invert the colors of certain light theme images for displaying
+ * inside of the dark theme.
+ */
+.devtools-tab[icon-invertable] > image,
+.devtools-toolbarbutton > image,
+.devtools-button::before,
+.scrollbutton-up > .toolbarbutton-icon,
+.scrollbutton-down > .toolbarbutton-icon,
+#black-boxed-message-button .button-icon,
+#requests-menu-perf-notice-button .button-icon,
+#canvas-debugging-empty-notice-button .button-icon,
+#toggle-breakpoints[checked] > image,
+.event-tooltip-debugger-icon {
+ filter: var(--icon-filter);
+}
+
+.hidden-labels-box:not(.visible) > label,
+.hidden-labels-box.visible ~ .hidden-labels-box > label:last-child {
+ display: none;
+}
+
+.devtools-invisible-splitter {
+ border-color: transparent;
+ background-color: transparent;
+}
+
+.devtools-horizontal-splitter,
+.devtools-side-splitter {
+ background-color: var(--theme-splitter-color);
+}
diff --git a/devtools/client/themes/toolbox.css b/devtools/client/themes/toolbox.css
new file mode 100644
index 000000000..1db2bd01c
--- /dev/null
+++ b/devtools/client/themes/toolbox.css
@@ -0,0 +1,408 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+:root {
+ --close-button-image: url(chrome://devtools/skin/images/close.svg);
+ --dock-bottom-image: url(chrome://devtools/skin/images/dock-bottom.svg);
+ --dock-side-image: url(chrome://devtools/skin/images/dock-side.svg);
+ --dock-undock-image: url(chrome://devtools/skin/images/dock-undock.svg);
+
+ --command-paintflashing-image: url(images/command-paintflashing.svg);
+ --command-screenshot-image: url(images/command-screenshot.svg);
+ --command-responsive-image: url(images/command-responsivemode.svg);
+ --command-scratchpad-image: url(images/tool-scratchpad.svg);
+ --command-pick-image: url(images/command-pick.svg);
+ --command-frames-image: url(images/command-frames.svg);
+ --command-splitconsole-image: url(images/command-console.svg);
+ --command-noautohide-image: url(images/command-noautohide.svg);
+ --command-rulers-image: url(images/command-rulers.svg);
+ --command-measure-image: url(images/command-measure.svg);
+}
+
+.theme-firebug {
+ --close-button-image: url(chrome://devtools/skin/images/firebug/close.svg);
+ --dock-bottom-image: url(chrome://devtools/skin/images/firebug/dock-bottom.svg);
+ --dock-side-image: url(chrome://devtools/skin/images/firebug/dock-side.svg);
+ --dock-undock-image: url(chrome://devtools/skin/images/firebug/dock-undock.svg);
+
+ --command-paintflashing-image: url(images/firebug/command-paintflashing.svg);
+ --command-screenshot-image: url(images/firebug/command-screenshot.svg);
+ --command-responsive-image: url(images/firebug/command-responsivemode.svg);
+ --command-scratchpad-image: url(images/firebug/command-scratchpad.svg);
+ --command-pick-image: url(images/firebug/command-pick.svg);
+ --command-frames-image: url(images/firebug/command-frames.svg);
+ --command-splitconsole-image: url(images/firebug/command-console.svg);
+ --command-noautohide-image: url(images/firebug/command-noautohide.svg);
+ --command-rulers-image: url(images/firebug/command-rulers.svg);
+ --command-measure-image: url(images/firebug/command-measure.svg);
+}
+
+/* Toolbox tabbar */
+
+.devtools-tabbar {
+ -moz-appearance: none;
+ min-height: 24px;
+ border: 0px solid;
+ border-bottom-width: 1px;
+ padding: 0;
+ background: var(--theme-tab-toolbar-background);
+ border-bottom-color: var(--theme-splitter-color);
+}
+
+#toolbox-tabs {
+ margin: 0;
+}
+
+/* Set flex attribute to Toolbox buttons and Picker container so,
+ they don't overlapp with the tab bar */
+#toolbox-buttons {
+ display: flex;
+}
+
+#toolbox-picker-container {
+ display: flex;
+}
+
+/* Toolbox tabs */
+
+.devtools-tab {
+ -moz-appearance: none;
+ -moz-binding: url("chrome://global/content/bindings/general.xml#control-item");
+ -moz-box-align: center;
+ min-width: 32px;
+ min-height: 24px;
+ max-width: 100px;
+ margin: 0;
+ padding: 0;
+ border-style: solid;
+ border-width: 0;
+ border-inline-start-width: 1px;
+ -moz-box-align: center;
+ -moz-box-flex: 1;
+}
+
+/* Save space on the tab-strip in Firebug theme */
+.theme-firebug .devtools-tab {
+ -moz-box-flex: initial;
+}
+
+.theme-dark .devtools-tab {
+ color: var(--theme-body-color-alt);
+ border-color: #42484f;
+}
+
+.theme-light .devtools-tab {
+ color: var(--theme-body-color);
+ border-color: var(--theme-splitter-color);
+}
+
+.theme-dark .devtools-tab:hover {
+ color: #ced3d9;
+}
+
+.devtools-tab:hover {
+ background-color: var(--toolbar-tab-hover);
+}
+
+.theme-dark .devtools-tab:hover:active {
+ color: var(--theme-selection-color);
+}
+
+.devtools-tab:hover:active {
+ background-color: var(--toolbar-tab-hover-active);
+}
+
+.theme-dark .devtools-tab:not([selected])[highlighted] {
+ background-color: hsla(99, 100%, 14%, .3);
+}
+
+.theme-light .devtools-tab:not([selected])[highlighted] {
+ background-color: rgba(44, 187, 15, .2);
+}
+
+/* Display execution pointer in the Debugger tab to indicate
+ that the debugger is paused. */
+.theme-firebug #toolbox-tab-jsdebugger.devtools-tab:not([selected])[highlighted] {
+ background-color: rgba(89, 178, 234, .2);
+ background-image: url(chrome://devtools/skin/images/firebug/tool-debugger-paused.svg);
+ background-repeat: no-repeat;
+ padding-left: 13px !important;
+ background-position: 3px 6px;
+}
+
+.devtools-tab > image {
+ border: none;
+ margin: 0;
+ margin-inline-start: 4px;
+ opacity: 0.6;
+ max-height: 16px;
+ width: 16px; /* Prevents collapse during theme switching */
+}
+
+/* Support invertable icon flags and make icon white when it's on a blue background */
+.theme-light .devtools-tab[icon-invertable="light-theme"]:not([selected]) > image,
+.devtools-tab[icon-invertable="dark-theme"][selected] > image {
+ filter: invert(1);
+}
+
+/* Don't apply any filter to non-invertable command button icons */
+.command-button:not(.command-button-invertable),
+/* [icon-invertable="light-theme"] icons are white, so do not invert them for the dark theme */
+.theme-dark .devtools-tab[icon-invertable="light-theme"] > image,
+/* Since "highlighted" icons are green, we should omit the filter */
+.devtools-tab[icon-invertable][highlighted]:not([selected]) > image {
+ filter: none;
+}
+
+.devtools-tab > label {
+ white-space: nowrap;
+ margin: 0 4px;
+}
+
+.devtools-tab:hover > image {
+ opacity: 0.8;
+}
+
+.devtools-tab:active > image,
+.devtools-tab[selected] > image {
+ opacity: 1;
+}
+
+.devtools-tabbar .devtools-tab[selected],
+.devtools-tabbar .devtools-tab[selected]:hover:active {
+ color: var(--theme-selection-color);
+ background-color: var(--theme-selection-background);
+}
+
+#toolbox-tabs .devtools-tab[selected],
+#toolbox-tabs .devtools-tab[highlighted] {
+ border-width: 0;
+ padding-inline-start: 1px;
+}
+
+#toolbox-tabs .devtools-tab[selected]:last-child,
+#toolbox-tabs .devtools-tab[highlighted]:last-child {
+ padding-inline-end: 1px;
+}
+
+#toolbox-tabs .devtools-tab[selected] + .devtools-tab,
+#toolbox-tabs .devtools-tab[highlighted] + .devtools-tab {
+ border-inline-start-width: 0;
+ padding-inline-start: 1px;
+}
+
+#toolbox-tabs .devtools-tab:first-child[selected] {
+ border-inline-start-width: 0;
+}
+
+#toolbox-tabs .devtools-tab:last-child {
+ border-inline-end-width: 1px;
+}
+
+.devtools-tab:not([highlighted]) > .highlighted-icon,
+.devtools-tab[selected] > .highlighted-icon,
+.devtools-tab:not([selected])[highlighted] > .default-icon {
+ visibility: collapse;
+}
+
+/* The options tab is special - it doesn't have the same parent
+ as the other tabs (toolbox-option-container vs toolbox-tabs) */
+#toolbox-option-container .devtools-tab:not([selected]) {
+ background-color: transparent;
+}
+#toolbox-option-container .devtools-tab {
+ border-color: transparent;
+ border-width: 0;
+ padding-inline-start: 1px;
+}
+#toolbox-tab-options > image {
+ margin: 0 8px;
+}
+
+/* Toolbox controls */
+
+#toolbox-controls > button,
+#toolbox-dock-buttons > button {
+ -moz-appearance: none;
+ border: none;
+ margin: 0 4px;
+ min-width: 16px;
+ width: 16px;
+}
+
+/* Save space in Firebug theme */
+.theme-firebug #toolbox-controls button {
+ margin-inline-start: 0 !important;
+ min-width: 12px;
+ margin: 0 1px;
+}
+
+#toolbox-close::before {
+ background-image: var(--close-button-image);
+}
+
+#toolbox-dock-bottom::before {
+ background-image: var(--dock-bottom-image);
+}
+
+#toolbox-dock-side::before {
+ background-image: var(--dock-side-image);
+}
+
+#toolbox-dock-window::before {
+ background-image: var(--dock-undock-image);
+}
+
+#toolbox-dock-bottom-minimize::before {
+ background-image: url("chrome://devtools/skin/images/dock-bottom-minimize@2x.png");
+}
+
+#toolbox-dock-bottom-minimize.minimized::before {
+ background-image: url("chrome://devtools/skin/images/dock-bottom-maximize@2x.png");
+}
+
+#toolbox-buttons:empty + .devtools-separator,
+.devtools-separator[invisible] {
+ visibility: hidden;
+}
+
+#toolbox-controls-separator {
+ margin: 0;
+}
+
+/* Command buttons */
+
+.command-button {
+ padding: 0;
+ margin: 0;
+ position: relative;
+}
+
+.command-button::before {
+ opacity: 0.7;
+}
+
+.command-button:hover {
+ background-color: var(--toolbar-tab-hover);
+}
+
+.theme-light .command-button:hover {
+ background-color: inherit;
+}
+
+.command-button:hover:active,
+.command-button[checked=true]:not(:hover) {
+ background-color: var(--toolbar-tab-hover-active)
+}
+
+.theme-light .command-button:hover:active,
+.theme-light .command-button[checked=true]:not(:hover) {
+ background-color: inherit;
+}
+
+.command-button:hover::before {
+ opacity: 0.85;
+}
+
+.command-button:hover:active::before,
+.command-button[checked=true]::before,
+.command-button[open=true]::before {
+ opacity: 1;
+}
+
+/* Command button images */
+
+#command-button-paintflashing::before {
+ background-image: var(--command-paintflashing-image);
+}
+
+#command-button-screenshot::before {
+ background-image: var(--command-screenshot-image);
+}
+
+#command-button-responsive::before {
+ background-image: var(--command-responsive-image);
+}
+
+#command-button-scratchpad::before {
+ background-image: var(--command-scratchpad-image);
+}
+
+#command-button-pick::before {
+ background-image: var(--command-pick-image);
+}
+
+#command-button-splitconsole::before {
+ background-image: var(--command-splitconsole-image);
+}
+
+#command-button-noautohide::before {
+ background-image: var(--command-noautohide-image);
+}
+
+#command-button-eyedropper::before {
+ background-image: var(--command-eyedropper-image);
+}
+
+#command-button-rulers::before {
+ background-image: var(--command-rulers-image);
+}
+
+#command-button-measure::before {
+ background-image: var(--command-measure-image);
+}
+
+#command-button-frames::before {
+ background-image: var(--command-frames-image);
+}
+
+#command-button-frames {
+ background-image: url("chrome://devtools/skin/images/dropmarker.svg");
+ background-repeat: no-repeat;
+
+ /* Override background-size from the command-button.
+ The drop down arrow is smaller */
+ background-size: 8px 4px !important;
+ min-width: 32px;
+}
+
+#command-button-frames:-moz-locale-dir(ltr) {
+ background-position: right;
+}
+
+#command-button-frames:-moz-locale-dir(rtl) {
+ background-position: left;
+}
+
+/* Toolbox panels */
+
+.toolbox-panel {
+ display: -moz-box;
+ -moz-box-flex: 1;
+ visibility: collapse;
+}
+
+.toolbox-panel[selected] {
+ visibility: visible;
+}
+
+/**
+ * When panels are collapsed or hidden, making sure that they are also
+ * inaccessible by keyboard. This is not the case by default because the are
+ * predominantly hidden using visibility: collapse; style or collapsed
+ * attribute.
+ */
+.toolbox-panel *,
+#toolbox-panel-webconsole[collapsed] * {
+ -moz-user-focus: ignore;
+}
+
+/**
+ * Enrure that selected toolbox panel's contents are keyboard accessible as they
+ * are explicitly made not to be when hidden (default).
+ */
+.toolbox-panel[selected] * {
+ -moz-user-focus: normal;
+}
diff --git a/devtools/client/themes/tooltip/arrow-horizontal-dark.png b/devtools/client/themes/tooltip/arrow-horizontal-dark.png
new file mode 100644
index 000000000..751fbc3d3
--- /dev/null
+++ b/devtools/client/themes/tooltip/arrow-horizontal-dark.png
Binary files differ
diff --git a/devtools/client/themes/tooltip/arrow-horizontal-dark@2x.png b/devtools/client/themes/tooltip/arrow-horizontal-dark@2x.png
new file mode 100644
index 000000000..e4db35e72
--- /dev/null
+++ b/devtools/client/themes/tooltip/arrow-horizontal-dark@2x.png
Binary files differ
diff --git a/devtools/client/themes/tooltip/arrow-horizontal-light.png b/devtools/client/themes/tooltip/arrow-horizontal-light.png
new file mode 100644
index 000000000..298ced115
--- /dev/null
+++ b/devtools/client/themes/tooltip/arrow-horizontal-light.png
Binary files differ
diff --git a/devtools/client/themes/tooltip/arrow-horizontal-light@2x.png b/devtools/client/themes/tooltip/arrow-horizontal-light@2x.png
new file mode 100644
index 000000000..7dec13406
--- /dev/null
+++ b/devtools/client/themes/tooltip/arrow-horizontal-light@2x.png
Binary files differ
diff --git a/devtools/client/themes/tooltip/arrow-vertical-dark.png b/devtools/client/themes/tooltip/arrow-vertical-dark.png
new file mode 100644
index 000000000..dfd535433
--- /dev/null
+++ b/devtools/client/themes/tooltip/arrow-vertical-dark.png
Binary files differ
diff --git a/devtools/client/themes/tooltip/arrow-vertical-dark@2x.png b/devtools/client/themes/tooltip/arrow-vertical-dark@2x.png
new file mode 100644
index 000000000..721bb0d88
--- /dev/null
+++ b/devtools/client/themes/tooltip/arrow-vertical-dark@2x.png
Binary files differ
diff --git a/devtools/client/themes/tooltip/arrow-vertical-light.png b/devtools/client/themes/tooltip/arrow-vertical-light.png
new file mode 100644
index 000000000..5a57fc353
--- /dev/null
+++ b/devtools/client/themes/tooltip/arrow-vertical-light.png
Binary files differ
diff --git a/devtools/client/themes/tooltip/arrow-vertical-light@2x.png b/devtools/client/themes/tooltip/arrow-vertical-light@2x.png
new file mode 100644
index 000000000..c2b95c45a
--- /dev/null
+++ b/devtools/client/themes/tooltip/arrow-vertical-light@2x.png
Binary files differ
diff --git a/devtools/client/themes/tooltips.css b/devtools/client/themes/tooltips.css
new file mode 100644
index 000000000..4cd6f3bf3
--- /dev/null
+++ b/devtools/client/themes/tooltips.css
@@ -0,0 +1,456 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+/* Tooltip specific theme variables */
+
+.theme-dark {
+ --bezier-diagonal-color: #eee;
+ --bezier-grid-color: rgba(0, 0, 0, 0.2);
+}
+
+.theme-light {
+ --bezier-diagonal-color: rgba(0, 0, 0, 0.2);
+ --bezier-grid-color: rgba(0, 0, 0, 0.05);
+}
+
+/* Tooltip widget (see devtools/client/shared/widgets/tooltip/Tooltip.js) */
+
+.devtools-tooltip .panel-arrowcontent {
+ padding: 4px;
+}
+
+.devtools-tooltip .panel-arrowcontainer {
+ /* Reseting the transition used when panels are shown */
+ transition: none;
+ /* Panels slide up/down/left/right when they appear using a transform.
+ Since we want to remove the transition, we don't need to transform anymore
+ plus it can interfeer by causing mouseleave events on the underlying nodes */
+ transform: none;
+}
+
+.devtools-tooltip[clamped-dimensions] {
+ min-height: 100px;
+ max-height: 400px;
+ min-width: 100px;
+ max-width: 400px;
+}
+.devtools-tooltip[clamped-dimensions-no-min-height] {
+ min-height: 0;
+ max-height: 400px;
+ min-width: 100px;
+ max-width: 400px;
+}
+.devtools-tooltip[clamped-dimensions-no-max-or-min-height] {
+ min-width: 400px;
+ max-width: 400px;
+}
+.devtools-tooltip[clamped-dimensions] .panel-arrowcontent,
+.devtools-tooltip[clamped-dimensions-no-min-height] .panel-arrowcontent,
+.devtools-tooltip[clamped-dimensions-no-max-or-min-height] .panel-arrowcontent {
+ overflow: hidden;
+}
+.devtools-tooltip[wide] {
+ max-width: 600px;
+}
+
+/* Tooltip: Simple Text */
+
+.devtools-tooltip-simple-text {
+ max-width: 400px;
+ margin: 0 -4px; /* Compensate for the .panel-arrowcontent padding. */
+ padding: 8px 12px;
+ white-space: pre-wrap;
+}
+
+.devtools-tooltip-simple-text:first-child {
+ margin-top: -4px;
+}
+
+.devtools-tooltip-simple-text:last-child {
+ margin-bottom: -4px;
+}
+
+/* Tooltip: Variables View */
+
+.devtools-tooltip-variables-view-box {
+ margin: -4px; /* Compensate for the .panel-arrowcontent padding. */
+}
+
+.devtools-tooltip-variables-view-box .variable-or-property > .title {
+ padding-inline-end: 6px;
+}
+
+/* Tooltip: Tiles */
+
+.devtools-tooltip-tiles {
+ background-color: #eee;
+ background-image: linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%, #ccc),
+ linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%, #ccc);
+ background-size: 20px 20px;
+ background-position: 0 0, 10px 10px;
+}
+
+.devtools-tooltip-iframe {
+ border: none;
+ background: transparent;
+}
+
+.tooltip-container {
+ display: none;
+ position: fixed;
+ z-index: 9999;
+ display: none;
+ background: transparent;
+ pointer-events: none;
+ overflow: hidden;
+ filter: drop-shadow(0 3px 4px var(--theme-tooltip-shadow));
+}
+
+.tooltip-xul-wrapper {
+ -moz-appearance: none;
+ background: transparent;
+ overflow: visible;
+ border-style: none;
+}
+
+.tooltip-xul-wrapper .tooltip-container {
+ position: absolute;
+}
+
+.tooltip-top {
+ flex-direction: column;
+}
+
+.tooltip-bottom {
+ flex-direction: column-reverse;
+}
+
+.tooltip-panel{
+ background-color: var(--theme-tooltip-background);
+ pointer-events: all;
+ flex-grow: 1;
+}
+
+.tooltip-visible {
+ display: flex;
+}
+
+.tooltip-hidden {
+ display: flex;
+ visibility: hidden;
+}
+
+/* Tooltip : flexible height styles */
+
+.tooltip-flexible-height .tooltip-panel {
+ /* In flexible mode the tooltip panel should only grow according to its content. */
+ flex-grow: 0;
+}
+
+.tooltip-flexible-height .tooltip-filler {
+ /* In flexible mode the filler should grow as much as possible. */
+ flex-grow: 1;
+}
+
+/* type="arrow" overrides: remove arrow decorations for the xul <panel> wrapper */
+
+.tooltip-xul-wrapper[type="arrow"] {
+ margin: 0;
+}
+
+/* The arrow image is hidden because the panel is opened using openPopupAtScreen(). */
+
+/* Remove all decorations on .panel-arrowcontent is the tooltip content container. */
+.tooltip-xul-wrapper[type="arrow"] .panel-arrowcontent {
+ margin: 0;
+ padding: 0;
+ background: transparent;
+ border: none;
+ box-shadow: none;
+}
+
+/* Tooltip : arrow style */
+
+.tooltip-xul-wrapper .tooltip-container {
+ /* When displayed in a XUL panel the drop shadow would be abruptly cut by the panel */
+ filter: none;
+}
+
+.tooltip-container[type="arrow"] > .tooltip-panel {
+ position: relative;
+ min-height: 10px;
+ box-sizing: border-box;
+ width: 100%;
+
+ border: 3px solid var(--theme-tooltip-border);
+ border-radius: 5px;
+}
+
+.tooltip-top[type="arrow"] .tooltip-panel {
+ top: 0;
+}
+
+.tooltip-bottom[type="arrow"] .tooltip-panel {
+ bottom: 0;
+}
+
+.tooltip-arrow {
+ position: relative;
+ height: 16px;
+ width: 32px;
+ overflow: hidden;
+ flex-shrink: 0;
+}
+
+/* In RTL locales, only use RTL on the tooltip content, keep LTR for positioning */
+.tooltip-container:-moz-locale-dir(rtl) {
+ direction: ltr;
+}
+
+.tooltip-panel:-moz-locale-dir(rtl) {
+ direction: rtl;
+}
+
+.tooltip-top .tooltip-arrow {
+ margin-top: -3px;
+}
+
+.tooltip-bottom .tooltip-arrow {
+ margin-bottom: -3px;
+}
+
+.tooltip-arrow:before {
+ content: "";
+ position: absolute;
+ width: 21px;
+ height: 21px;
+ margin-left: 4px;
+ background: linear-gradient(-45deg,
+ var(--theme-tooltip-background) 50%, transparent 50%);
+ border-color: var(--theme-tooltip-border);
+ border-style: solid;
+ border-width: 0px 3px 3px 0px;
+ border-radius: 3px;
+ pointer-events: all;
+}
+
+.tooltip-bottom .tooltip-arrow:before {
+ margin-top: 4px;
+ transform: rotate(225deg);
+}
+
+.tooltip-top .tooltip-arrow:before {
+ margin-top: -12px;
+ transform: rotate(45deg);
+}
+
+/* Tooltip: Events */
+
+.event-header {
+ display: flex;
+ align-items: center;
+ cursor: pointer;
+ overflow: hidden;
+}
+
+.event-header:first-child {
+ border-width: 0;
+}
+
+.event-header:not(:first-child) {
+ border-width: 1px 0 0 0;
+}
+
+.devtools-tooltip-events-container {
+ height: 100%;
+ overflow-y: auto;
+}
+
+.event-tooltip-event-type,
+.event-tooltip-filename,
+.event-tooltip-attributes {
+ margin-inline-start: 0;
+ flex-shrink: 0;
+ cursor: pointer;
+}
+
+.event-tooltip-event-type {
+ font-weight: bold;
+ font-size: 13px;
+}
+
+.event-tooltip-filename {
+ margin: 0 5px;
+ font-size: 100%;
+ flex-shrink: 1;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ /* Force ellipsis to be displayed on the left */
+ direction: rtl;
+}
+
+.event-tooltip-debugger-icon {
+ width: 16px;
+ height: 16px;
+ margin-inline-end: 4px;
+ opacity: 0.6;
+ flex-shrink: 0;
+}
+
+.event-tooltip-debugger-icon:hover {
+ opacity: 1;
+}
+
+.event-tooltip-content-box {
+ display: none;
+ height: 100px;
+ overflow: hidden;
+ margin-inline-end: 0;
+ border: 1px solid var(--theme-splitter-color);
+ border-width: 1px 0 0 0;
+}
+
+.event-toolbox-content-box iframe {
+ height: 100%;
+ border-style: none;
+}
+
+.event-tooltip-content-box[open] {
+ display: block;
+}
+
+.event-tooltip-source-container {
+ margin-top: 5px;
+ margin-bottom: 10px;
+ margin-inline-start: 5px;
+ margin-inline-end: 0;
+}
+
+.event-tooltip-source {
+ margin-bottom: 0;
+}
+
+.event-tooltip-attributes-container {
+ display: flex;
+ flex-shrink: 0;
+ flex-grow: 1;
+ justify-content: flex-end;
+}
+
+.event-tooltip-attributes-box {
+ display: flex;
+ flex-shrink: 0;
+ align-items: center;
+ height: 14px;
+ border-radius: 3px;
+ padding: 2px;
+ margin-inline-start: 5px;
+ background-color: var(--theme-body-color-alt);
+ color: var(--theme-toolbar-background);
+}
+
+.event-tooltip-attributes {
+ margin: 0;
+ font-size: 9px;
+ padding-top: 2px;
+}
+
+/*
+ * Tooltip: JS stack traces
+ */
+
+.stack-trace-tooltip {
+ direction: ltr;
+ height: 100%;
+ overflow-y: auto;
+}
+
+.stack-trace-tooltip > .stack-frame {
+ margin-left: 5px;
+ margin-right: 5px;
+}
+
+.stack-trace-tooltip > .stack-frame:first-child {
+ margin-top: 5px;
+}
+
+.stack-trace-tooltip > .stack-frame:last-child {
+ margin-bottom: 5px;
+}
+
+.stack-frame-call {
+ color: var(--theme-body-color-alt);
+ cursor: pointer;
+ display: flex;
+}
+
+.stack-frame-call:hover {
+ background-color: var(--theme-selection-background-semitransparent);
+}
+
+.stack-frame-async {
+ color: var(--theme-body-color-inactive);
+}
+
+.stack-frame-function-name {
+ color: var(--theme-highlight-blue);
+ max-width: 50%;
+ margin-inline-end: 1em;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.stack-frame-source-name {
+ flex: 1 1;
+ /* Makes the file name truncated (and ellipsis shown) on the left side */
+ direction: rtl;
+ text-align: right;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+/* Enforce LTR direction for the file name - fixes bug 1290056 */
+.stack-frame-source-name-inner {
+ direction: ltr;
+ unicode-bidi: embed;
+}
+
+.stack-frame-line {
+ color: var(--theme-highlight-orange);
+}
+
+/* Tooltip: HTML Search */
+
+#searchbox-panel-listbox {
+ width: 250px;
+ max-width: 250px;
+ overflow-x: hidden;
+}
+
+#searchbox-panel-listbox .autocomplete-item,
+#searchbox-panel-listbox .autocomplete-item[selected] {
+ overflow-x: hidden;
+}
+
+#searchbox-panel-listbox .autocomplete-item > .initial-value {
+ max-width: 130px;
+ margin-left: 15px;
+}
+
+#searchbox-panel-listbox .autocomplete-item > .autocomplete-value {
+ max-width: 150px;
+}
+
+/* Tooltip: Image tooltip */
+
+.devtools-tooltip-image-broken {
+ box-sizing: border-box;
+ height: 100%;
+ text-align: center;
+ line-height: 30px;
+}
diff --git a/devtools/client/themes/variables.css b/devtools/client/themes/variables.css
new file mode 100644
index 000000000..84f9282a6
--- /dev/null
+++ b/devtools/client/themes/variables.css
@@ -0,0 +1,203 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+/* Variable declarations for light and dark devtools themes.
+ * Colors are taken from:
+ * https://developer.mozilla.org/en-US/docs/Tools/DevToolsColors.
+ * Changes should be kept in sync with commandline.css and commandline.inc.css.
+ */
+
+/* IMPORTANT NOTE:
+ * This file is parsed in js (see client/shared/theme.js)
+ * so the formatting should be consistent (i.e. no '}' inside a rule).
+ */
+
+:root.theme-light {
+ --theme-body-background: white;
+ --theme-sidebar-background: white;
+ --theme-contrast-background: #e6b064;
+
+ --theme-tab-toolbar-background: #fcfcfc;
+ --theme-toolbar-background: #fcfcfc;
+ --theme-selection-background: #4c9ed9;
+ --theme-selection-background-semitransparent: rgba(76, 158, 217, 0.15);
+ --theme-selection-color: #f5f7fa;
+ --theme-splitter-color: #dde1e4;
+ --theme-comment: #696969;
+
+ --theme-body-color: #393f4c;
+ --theme-body-color-alt: #585959;
+ --theme-body-color-inactive: #999797;
+ --theme-content-color1: #292e33;
+ --theme-content-color2: #8fa1b2;
+ --theme-content-color3: #667380;
+
+ --theme-highlight-green: #2cbb0f;
+ --theme-highlight-blue: #0088cc;
+ --theme-highlight-bluegrey: #0072ab;
+ --theme-highlight-purple: #5b5fff;
+ --theme-highlight-lightorange: #d97e00;
+ --theme-highlight-orange: #f13c00;
+ --theme-highlight-red: #ed2655;
+ --theme-highlight-pink: #b82ee5;
+ --theme-highlight-gray: #dde1e4;
+
+ /* For accessibility purposes we want to enhance the focus styling. This
+ * should improve keyboard navigation usability. */
+ --theme-focus-outline-color: #000000;
+
+ /* Colors used in Graphs, like performance tools. Similar colors to Chrome's timeline. */
+ --theme-graphs-green: #85d175;
+ --theme-graphs-blue: #83b7f6;
+ --theme-graphs-bluegrey: #0072ab;
+ --theme-graphs-purple: #b693eb;
+ --theme-graphs-yellow: #efc052;
+ --theme-graphs-orange: #d97e00;
+ --theme-graphs-red: #e57180;
+ --theme-graphs-grey: #cccccc;
+ --theme-graphs-full-red: #f00;
+ --theme-graphs-full-blue: #00f;
+
+ /* Images */
+ --theme-pane-collapse-image: url(chrome://devtools/skin/images/pane-collapse.svg);
+ --theme-pane-expand-image: url(chrome://devtools/skin/images/pane-expand.svg);
+
+ /* Tooltips */
+ --theme-tooltip-border: #d9e1e8;
+ --theme-tooltip-background: rgba(255, 255, 255, .9);
+ --theme-tooltip-shadow: rgba(155, 155, 155, 0.26);
+
+ /* Command line */
+ --theme-command-line-image: url(chrome://devtools/skin/images/commandline-icon.svg#light-theme);
+ --theme-command-line-image-focus: url(chrome://devtools/skin/images/commandline-icon.svg#light-theme-focus);
+}
+
+:root.theme-dark {
+ --theme-body-background: #393f4c;
+ --theme-sidebar-background: #393f4c;
+ --theme-contrast-background: #ffb35b;
+
+ --theme-tab-toolbar-background: #272b35;
+ --theme-toolbar-background: #272b35;
+ --theme-selection-background: #5675B9;
+ --theme-selection-background-semitransparent: rgba(86, 117, 185, 0.5);
+ --theme-selection-color: #f5f7fa;
+ --theme-splitter-color: #454d5d;
+ --theme-comment: #757873;
+
+ --theme-body-color: #8fa1b2;
+ --theme-body-color-alt: #b6babf;
+ --theme-body-color-inactive: #8fa1b2;
+ --theme-content-color1: #a9bacb;
+ --theme-content-color2: #8fa1b2;
+ --theme-content-color3: #5f7387;
+
+ --theme-highlight-green: #00ff7f;
+ --theme-highlight-blue: #46afe3;
+ --theme-highlight-bluegrey: #5e88b0;
+ --theme-highlight-purple: #bcb8db;
+ --theme-highlight-lightorange: #d99b28;
+ --theme-highlight-orange: #d96629;
+ --theme-highlight-red: #eb5368;
+ --theme-highlight-pink: #df80ff;
+ --theme-highlight-gray: #e9f4fe;
+
+ /* For accessibility purposes we want to enhance the focus styling. This
+ * should improve keyboard navigation usability. */
+ --theme-focus-outline-color: #ced3d9;
+
+ /* Colors used in Graphs, like performance tools. Mostly similar to some "highlight-*" colors. */
+ --theme-graphs-green: #70bf53;
+ --theme-graphs-blue: #46afe3;
+ --theme-graphs-bluegrey: #5e88b0;
+ --theme-graphs-purple: #df80ff;
+ --theme-graphs-yellow: #d99b28;
+ --theme-graphs-orange: #d96629;
+ --theme-graphs-red: #eb5368;
+ --theme-graphs-grey: #757873;
+ --theme-graphs-full-red: #f00;
+ --theme-graphs-full-blue: #00f;
+
+ /* Images */
+ --theme-pane-collapse-image: url(chrome://devtools/skin/images/pane-collapse.svg);
+ --theme-pane-expand-image: url(chrome://devtools/skin/images/pane-expand.svg);
+
+ /* Tooltips */
+ --theme-tooltip-border: #434850;
+ --theme-tooltip-background: rgba(19, 28, 38, .9);
+ --theme-tooltip-shadow: rgba(25, 25, 25, 0.76);
+
+ /* Command line */
+ --theme-command-line-image: url(chrome://devtools/skin/images/commandline-icon.svg#dark-theme);
+ --theme-command-line-image-focus: url(chrome://devtools/skin/images/commandline-icon.svg#dark-theme-focus);
+}
+
+:root.theme-firebug {
+ --theme-body-background: #fcfcfc;
+ --theme-sidebar-background: #fcfcfc;
+ --theme-contrast-background: #e6b064;
+
+ --theme-tab-toolbar-background: #d8eaf9;
+ --theme-toolbar-background: #f0f1f2;
+ --theme-selection-background: #3399ff;
+ --theme-selection-background-semitransparent: rgba(128,128,128,0.2);
+ --theme-selection-color: white;
+ --theme-splitter-color: #aabccf;
+ --theme-comment: green;
+
+ --theme-body-color: #000000;
+ --theme-body-color-alt: #585959;
+ --theme-content-color1: #292e33;
+ --theme-content-color2: #8fa1b2;
+ --theme-content-color3: #667380;
+
+ --theme-highlight-green: #2cbb0f;
+ --theme-highlight-blue: #3455db;
+ --theme-highlight-bluegrey: #0072ab;
+ --theme-highlight-purple: #887ce6;
+ --theme-highlight-lightorange: #d97e00;
+ --theme-highlight-orange: #f13c00;
+ --theme-highlight-red: #e22f6f;
+ --theme-highlight-pink: #b82ee5;
+ --theme-highlight-gray: #dde1e4;
+
+ /* Colors used in Graphs, like performance tools. Similar colors to Chrome's timeline. */
+ --theme-graphs-green: #85d175;
+ --theme-graphs-blue: #83b7f6;
+ --theme-graphs-bluegrey: #0072ab;
+ --theme-graphs-purple: #b693eb;
+ --theme-graphs-yellow: #efc052;
+ --theme-graphs-orange: #d97e00;
+ --theme-graphs-red: #e57180;
+ --theme-graphs-grey: #cccccc;
+ --theme-graphs-full-red: #f00;
+ --theme-graphs-full-blue: #00f;
+
+ /* Images */
+ --theme-pane-collapse-image: url(chrome://devtools/skin/images/firebug/pane-collapse.svg);
+ --theme-pane-expand-image: url(chrome://devtools/skin/images/firebug/pane-expand.svg);
+
+ /* Font size */
+ --theme-toolbar-font-size: 12px;
+
+ /* Header */
+ --theme-header-background: #F0F0F0 linear-gradient(to top,
+ rgba(0, 0, 0, 0.1),
+ transparent) repeat-x;
+
+ /* Command line */
+ --theme-command-line-image: url(chrome://devtools/skin/images/firebug/commandline-icon.svg);
+ --theme-command-line-image-focus: url(chrome://devtools/skin/images/firebug/commandline-icon.svg#focus);
+}
+
+:root {
+ --theme-focus-border-color-textbox: #0675d3;
+ --theme-textbox-box-shadow: rgba(97,181,255,.75);
+
+ /* For accessibility purposes we want to enhance the focus styling. This
+ * should improve keyboard navigation usability. */
+ --theme-focus-outline: 1px dotted var(--theme-focus-outline-color);
+ --theme-focus-box-shadow-textbox: 0 0 0 1px var(--theme-textbox-box-shadow);
+}
diff --git a/devtools/client/themes/webaudioeditor.css b/devtools/client/themes/webaudioeditor.css
new file mode 100644
index 000000000..3ba9dad88
--- /dev/null
+++ b/devtools/client/themes/webaudioeditor.css
@@ -0,0 +1,195 @@
+/* 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/. */
+
+/* Reload and waiting notices */
+.notice-container {
+ margin-top: -50vh;
+ color: var(--theme-body-color-alt);
+}
+
+#reload-notice {
+ font-size: 120%;
+}
+
+#waiting-notice {
+ font-size: 110%;
+}
+
+/* Context Graph */
+svg {
+ overflow: hidden;
+ -moz-box-flex: 1;
+ --arrow-color: var(--theme-splitter-color);
+ --text-color: var(--theme-body-color-alt);
+}
+
+.theme-dark svg {
+ --arrow-color: var(--theme-body-color-alt);
+}
+
+/* Edges in graph */
+.edgePath path {
+ stroke-width: 1px;
+ stroke: var(--arrow-color);
+}
+svg #arrowhead {
+ /* !important is needed to override inline style */
+ fill: var(--arrow-color) !important;
+}
+
+/* AudioParam connection edges */
+g.edgePath.param-connection path {
+ stroke-dasharray: 5,5;
+ stroke: var(--arrow-colo);
+}
+
+/* Labels in AudioParam connection should have background that match
+ * the main background so there's whitespace around the label, on top of the
+ * dotted lines. */
+g.edgeLabel rect {
+ fill: var(--theme-body-background);
+}
+g.edgeLabel tspan {
+ fill: var(--text-color);
+}
+
+/* Audio Nodes */
+.nodes rect {
+ stroke-width: 1px;
+ cursor: pointer;
+ stroke: var(--theme-splitter-color);
+ fill: var(--theme-toolbar-background);
+}
+
+/**
+ * Bypassed Nodes
+ */
+
+.theme-light .nodes g.bypassed rect {
+ fill: url(chrome://devtools/skin/images/filters.svg#bypass-light);
+}
+
+.theme-dark .nodes g.bypassed rect {
+ fill: url(chrome://devtools/skin/images/filters.svg#bypass-dark);
+}
+
+.nodes g.bypassed.selected rect {
+ stroke: var(--theme-selection-background);
+}
+
+.nodes g.bypassed text {
+ opacity: 0.6;
+}
+
+/**
+ * Selected Nodes
+ */
+.nodes g.selected rect {
+ fill: var(--theme-selection-background);
+}
+
+/* Don't style bypassed nodes text differently because it'd be illegible in light-theme */
+g.selected:not(.bypassed) text {
+ fill: var(--theme-selection-color);
+}
+
+
+/* Text in nodes and edges */
+text {
+ cursor: default; /* override the "text" cursor */
+ fill: var(--text-color);
+ font-size: 1.25em;
+ /* Make sure text stays inside its container in RTL locales */
+ direction: ltr;
+}
+
+.nodes text {
+ cursor: pointer;
+}
+
+/**
+ * Inspector Styles
+ */
+
+/* hide the variables view scope title as its redundant,
+ * because there's only one scope displayed. */
+.variables-view-scope > .title {
+ display: none;
+}
+
+#web-audio-inspector-title {
+ margin: 6px;
+}
+
+.web-audio-inspector .error {
+ background-image: url(images/alerticon-warning.png);
+ background-size: 13px 12px;
+ -moz-appearance: none;
+ opacity: 0;
+ transition: opacity .5s ease-out 0s;
+}
+
+#inspector-pane-toggle {
+ background: none;
+ box-shadow: none;
+ border: none;
+ list-style-image: var(--theme-pane-collapse-image);
+}
+
+#inspector-pane-toggle > .toolbarbutton-icon {
+ width: 16px;
+ height: 16px;
+}
+
+#inspector-pane-toggle.pane-collapsed {
+ list-style-image: var(--theme-pane-expand-image);
+}
+
+/**
+ * Automation Styles
+ */
+
+#automation-param-toolbar .automation-param-button[selected] {
+ color: var(--theme-selection-color);
+ background-color: var(--theme-selection-background);
+}
+
+#automation-graph {
+ overflow: hidden;
+ -moz-box-flex: 1;
+}
+
+@media (min-resolution: 1.1dppx) {
+ .web-audio-inspector .error {
+ background-image: url(images/alerticon-warning@2x.png);
+ }
+}
+
+/**
+ * Inspector toolbar
+ */
+
+#audio-node-toolbar .bypass {
+ list-style-image: url(images/power.svg);
+}
+
+/**
+ * Responsive Styles
+ * `.devtools-responsive-container` takes care of most of
+ * the changing of host types.
+ */
+@media (max-width: 700px) {
+ /**
+ * Override the inspector toggle so it's always open
+ * in the portrait view, with the toggle button hidden.
+ */
+ #inspector-pane-toggle {
+ display: none;
+ }
+
+ #web-audio-inspector {
+ margin-left: 0px !important;
+ margin-right: 0px !important;
+ }
+}
diff --git a/devtools/client/themes/webconsole.css b/devtools/client/themes/webconsole.css
new file mode 100644
index 000000000..c0c0177e1
--- /dev/null
+++ b/devtools/client/themes/webconsole.css
@@ -0,0 +1,793 @@
+/* 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/. */
+
+/* Webconsole specific theme variables */
+.theme-light,
+.theme-firebug {
+ --error-color: #FF0000;
+ --error-background-color: #FFEBEB;
+ --warning-background-color: #FFFFC8;
+}
+
+/* General output styles */
+
+a {
+ -moz-user-focus: normal;
+ -moz-user-input: enabled;
+ cursor: pointer;
+ text-decoration: underline;
+}
+
+/* Workaround for Bug 575675 - FindChildWithRules aRelevantLinkVisited
+ * assertion when loading HTML page with links in XUL iframe */
+*:visited { }
+
+.message {
+ display: flex;
+ padding: 0 7px;
+ width: 100%;
+ box-sizing: border-box;
+}
+
+.message > .prefix,
+.message > .timestamp {
+ flex: none;
+ color: var(--theme-comment);
+ margin: 3px 6px 0 0;
+}
+
+.message > .indent {
+ flex: none;
+}
+
+.message > .icon {
+ flex: none;
+ margin: 3px 6px 0 0;
+ padding: 0 4px;
+ height: 1em;
+ align-self: flex-start;
+}
+
+.theme-firebug .message > .icon {
+ margin: 0;
+ margin-inline-end: 6px;
+}
+
+.theme-firebug .message[severity="error"],
+.theme-light .message.error,
+.theme-firebug .message.error {
+ color: var(--error-color);
+ background-color: var(--error-background-color);
+}
+
+.theme-firebug .message[severity="warn"],
+.theme-light .message.warn,
+.theme-firebug .message.warn {
+ background-color: var(--warning-background-color);
+}
+
+.message > .icon::before {
+ content: "";
+ background-image: url(chrome://devtools/skin/images/webconsole.svg);
+ background-position: 12px 12px;
+ background-repeat: no-repeat;
+ background-size: 72px 60px;
+ width: 12px;
+ height: 12px;
+ display: inline-block;
+}
+
+.theme-light .message > .icon::before {
+ background-image: url(chrome://devtools/skin/images/webconsole.svg#light-icons);
+}
+
+.message > .message-body-wrapper {
+ flex: auto;
+ min-width: 0px;
+ margin: 3px;
+}
+
+.message-body-wrapper .table-widget-body {
+ overflow: visible;
+}
+
+/* The red bubble that shows the number of times a message is repeated */
+.message-repeats {
+ -moz-user-select: none;
+ flex: none;
+ margin: 2px 6px;
+ padding: 0 6px;
+ height: 1.25em;
+ color: white;
+ background-color: red;
+ border-radius: 40px;
+ font: message-box;
+ font-size: 0.9em;
+ font-weight: 600;
+}
+
+.message-repeats[value="1"] {
+ display: none;
+}
+
+.message-location {
+ max-width: 40%;
+}
+
+.stack-trace {
+ /* The markup contains extra whitespace to improve formatting of clipboard text.
+ Make sure this whitespace doesn't affect the HTML rendering */
+ white-space: normal;
+}
+
+.stack-trace .frame-link-source,
+.message-location .frame-link-source {
+ /* Makes the file name truncated (and ellipsis shown) on the left side */
+ direction: rtl;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.stack-trace .frame-link-source-inner,
+.message-location .frame-link-source-inner {
+ /* Enforce LTR direction for the file name - fixes bug 1290056 */
+ direction: ltr;
+ unicode-bidi: embed;
+}
+
+.stack-trace .frame-link-function-display-name {
+ max-width: 50%;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.message-flex-body {
+ display: flex;
+}
+
+.message-body > * {
+ white-space: pre-wrap;
+ word-wrap: break-word;
+}
+
+.message-flex-body > .message-body {
+ display: block;
+ flex: auto;
+}
+
+#output-wrapper {
+ direction: ltr;
+ overflow: auto;
+ -moz-user-select: text;
+ position: relative;
+}
+
+/* The width on #output-container is set to a hardcoded px in webconsole.js
+ since it's way faster than using 100% with -moz-box-flex (see Bug 1237368) */
+
+#output-container.hideTimestamps > .message {
+ padding-inline-start: 0;
+ margin-inline-start: 7px;
+ width: calc(100% - 7px);
+}
+
+#output-container.hideTimestamps > .message > .timestamp {
+ display: none;
+}
+
+#output-container.hideTimestamps > .message > .indent {
+ background-color: var(--theme-body-background);
+}
+
+.filtered-by-type,
+.filtered-by-string {
+ display: none;
+}
+
+.hidden-message {
+ display: block;
+ visibility: hidden;
+ height: 0;
+ overflow: hidden;
+}
+
+/* WebConsole colored drops */
+
+.webconsole-filter-button {
+ -moz-user-focus: normal;
+}
+
+.webconsole-filter-button > .toolbarbutton-menubutton-button:before {
+ content: "";
+ display: inline-block;
+ height: 8px;
+ width: 8px;
+ border-radius: 50%;
+ margin-inline-start: 5px;
+ border-width: 1px;
+ border-style: solid;
+}
+
+/* Network styles */
+.webconsole-filter-button[category="net"] > .toolbarbutton-menubutton-button:before {
+ background-image: linear-gradient(#444444, #000000);
+ border-color: #777;
+}
+
+.message:hover {
+ background-color: var(--theme-selection-background-semitransparent) !important;
+}
+
+.theme-light .message[severity=error],
+.theme-light .message.error {
+ background-color: rgba(255, 150, 150, 0.3);
+}
+
+.theme-dark .message[severity=error],
+.theme-dark .message.error {
+ background-color: rgba(235, 83, 104, 0.17);
+}
+
+.console-string {
+ color: var(--theme-highlight-lightorange);
+}
+
+.theme-selected .console-string,
+.theme-selected .cm-number,
+.theme-selected .cm-variable,
+.theme-selected .kind-ArrayLike {
+ color: #f5f7fa !important; /* Selection Text Color */
+}
+
+.message[category=network] > .indent {
+ border-inline-end: solid var(--theme-body-color-alt) 6px;
+}
+
+.message[category=network][severity=error] > .icon::before,
+.message.network.error > .icon::before {
+ background-position: -12px 0;
+}
+
+.message[category=network] > .message-body,
+.message.network > .message-body {
+ display: flex;
+ flex-wrap: wrap;
+}
+
+.message[category=network] .method,
+.message.network .method {
+ flex: none;
+}
+
+.message[category=network]:not(.navigation-marker) .url,
+.message.network:not(.navigation-marker) .url {
+ flex: 1 1 auto;
+ /* Make sure the URL is very small initially, let flex change width as needed. */
+ width: 100px;
+ min-width: 5em;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.message[category=network] .status,
+.message.network .status {
+ flex: none;
+ margin-inline-start: 6px;
+}
+
+.message[category=network].mixed-content .url,
+.message.network.mixed-content .url {
+ color: var(--theme-highlight-red);
+}
+
+.message .learn-more-link {
+ color: var(--theme-highlight-blue);
+ margin: 0 6px;
+}
+
+.message[category=network] .xhr,
+.message.network .xhr {
+ background-color: var(--theme-body-color-alt);
+ color: var(--theme-body-background);
+ border-radius: 3px;
+ font-weight: bold;
+ font-size: 10px;
+ padding: 2px;
+ line-height: 10px;
+ margin-inline-start: 3px;
+ margin-inline-end: 1ex;
+}
+
+/* CSS styles */
+.webconsole-filter-button[category="css"] > .toolbarbutton-menubutton-button:before {
+ background-image: linear-gradient(#2DC3F3, #00B6F0);
+ border-color: #1BA2CC;
+}
+
+.message[category=cssparser] > .indent,
+.message.cssparser > .indent {
+ border-inline-end: solid #00b6f0 6px;
+}
+
+.message[category=cssparser][severity=error] > .icon::before,
+.message.cssparser.error > .icon::before {
+ background-position: -12px -12px;
+}
+
+.message[category=cssparser][severity=warn] > .icon::before,
+.message.cssparser.warn > .icon::before {
+ background-position: -24px -12px;
+}
+
+/* JS styles */
+.webconsole-filter-button[category="js"] > .toolbarbutton-menubutton-button:before {
+ background-image: linear-gradient(#FCB142, #FB9500);
+ border-color: #E98A00;
+}
+
+.message[category=exception] > .indent,
+.message.exception > .indent {
+ border-inline-end: solid #fb9500 6px;
+}
+
+.message[category=exception][severity=error] > .icon::before,
+.message.exception.error > .icon::before {
+ background-position: -12px -24px;
+}
+
+.message[category=exception][severity=warn] > .icon::before,
+.message.exception.warn > .icon::before {
+ background-position: -24px -24px;
+}
+
+/* Web Developer styles */
+.webconsole-filter-button[category="logging"] > .toolbarbutton-menubutton-button:before {
+ background-image: linear-gradient(#B9B9B9, #AAAAAA);
+ border-color: #929292;
+}
+
+.message[category=console] > .indent,
+.message.console-api > .indent {
+ border-inline-end: solid #cbcbcb 6px;
+}
+
+.message[category=console][severity=error] > .icon::before,
+.message[category=output][severity=error] > .icon::before,
+.message[category=server][severity=error] > .icon::before {
+ background-position: -12px -36px;
+}
+
+.message[category=console][severity=warn] > .icon::before,
+.message[category=server][severity=warn] > .icon::before {
+ background-position: -24px -36px;
+}
+
+.message[category=console][severity=info] > .icon::before,
+.message[category=server][severity=info] > .icon::before {
+ background-position: -36px -36px;
+}
+
+/* Server Logging Styles */
+
+.webconsole-filter-button[category="server"] > .toolbarbutton-menubutton-button:before {
+ background-image: linear-gradient(rgb(144, 176, 144), rgb(99, 151, 99));
+ border-color: rgb(76, 143, 76);
+}
+
+.message[category=server] > .indent,
+.message.server > .indent {
+ border-inline-end: solid #90B090 6px;
+}
+
+/* Input and output styles */
+.message[category=input] > .indent,
+.message[category=output] > .indent,
+.message.command > .indent,
+.message.result > .indent {
+ border-inline-end: solid #808080 6px;
+}
+
+.message[category=input] > .icon::before,
+.message.command > .icon::before {
+ background-position: -48px -36px;
+}
+
+.message[category=output] > .icon::before,
+.message.result > .icon::before {
+ background-position: -60px -36px;
+}
+
+/* JSTerm Styles */
+.jsterm-input-container {
+ background-color: var(--theme-tab-toolbar-background);
+ border-top: 1px solid var(--theme-splitter-color);
+}
+
+.theme-light .jsterm-input-container {
+ /* For light theme use a white background for the input - it looks better
+ than off-white */
+ background-color: #fff;
+ border-top-color: #e0e0e0;
+}
+
+.theme-firebug .jsterm-input-container {
+ border-top: 1px solid #ccc;
+}
+
+.jsterm-input-node,
+.jsterm-complete-node {
+ border: none;
+ padding: 0;
+ padding-inline-start: 20px;
+ margin: 0;
+ -moz-appearance: none;
+ background-color: transparent;
+}
+
+.jsterm-input-node[focused="true"] {
+ background-image: var(--theme-command-line-image-focus);
+ box-shadow: none;
+}
+
+.jsterm-complete-node {
+ color: var(--theme-comment);
+}
+
+.jsterm-input-node {
+ /* Always allow scrolling on input - it auto expands in js by setting height,
+ but don't want it to get bigger than the window. 24px = toolbar height. */
+ max-height: calc(90vh - 24px);
+ background-image: var(--theme-command-line-image);
+ background-repeat: no-repeat;
+ background-size: 16px 16px;
+ background-position: 4px 50%;
+ color: var(--theme-content-color1);
+}
+
+:-moz-any(.jsterm-input-node,
+ .jsterm-complete-node) > .textbox-input-box > .textbox-textarea {
+ overflow-x: hidden;
+ /* Set padding for console input on textbox to make sure it is inlcuded in
+ scrollHeight that is used when resizing JSTerminal's input. Note: textbox
+ default style has important already */
+ padding: 4px 0 !important;
+}
+
+.inlined-variables-view .message-body {
+ display: flex;
+ flex-direction: column;
+ resize: vertical;
+ overflow: auto;
+ min-height: 200px;
+}
+.inlined-variables-view iframe {
+ display: block;
+ flex: 1;
+ margin-top: 5px;
+ margin-bottom: 15px;
+ margin-inline-end: 15px;
+ border: 1px solid var(--theme-splitter-color);
+ border-radius: 3px;
+}
+
+#webconsole-sidebar > tabs {
+ height: 0;
+ border: none;
+}
+
+/* Firebug theme has the tabs visible. */
+.theme-firebug #webconsole-sidebar > tabs {
+ height: 28px;
+}
+
+.devtools-side-splitter ~ #webconsole-sidebar[hidden] {
+ display: none;
+}
+
+/* Security styles */
+
+.message[category=security] > .indent,
+.message.security > .indent {
+ border-inline-end: solid red 6px;
+}
+
+.webconsole-filter-button[category="security"] > .toolbarbutton-menubutton-button:before {
+ background-image: linear-gradient(#FF3030, #FF7D7D);
+ border-color: #D12C2C;
+}
+
+.message[category=security][severity=error] > .icon::before,
+.message.security.error > .icon::before {
+ background-position: -12px -48px;
+}
+
+.message[category=security][severity=warn] > .icon::before,
+.message.security.warn > .icon::before {
+ background-position: -24px -48px;
+}
+
+.navigation-marker {
+ color: #aaa;
+ background: linear-gradient(#aaa, #aaa) no-repeat left 50%;
+ background-size: 100% 2px;
+ margin-top: 6px;
+ margin-bottom: 6px;
+ font-size: 0.9em;
+}
+
+.navigation-marker .url {
+ padding-inline-end: 9px;
+ text-decoration: none;
+ background: var(--theme-body-background);
+}
+
+.theme-light .navigation-marker .url {
+ background: #fff;
+}
+
+.stacktrace {
+ display: none;
+ padding: 5px 10px;
+ margin: 5px 0 0 0;
+ overflow-y: auto;
+ border: 1px solid var(--theme-splitter-color);
+ border-radius: 3px;
+}
+
+.consoletable {
+ margin: 5px 0 0 0;
+}
+
+/* Force cells to only show one row of contents. Getting normal ellipses
+ behavior has proven impossible so far, so this is better than letting
+ rows get out of vertical alignment when one cell has a lot of content. */
+.consoletable .table-widget-cell > span {
+ overflow: hidden;
+ display: flex;
+ height: 1.25em;
+ line-height: 1.25em;
+}
+
+.theme-light .message[severity=error] .stacktrace,
+.theme-light .message.error .stacktrace {
+ background-color: rgba(255, 255, 255, 0.5);
+}
+
+.theme-dark .message[severity=error] .stacktrace,
+.theme-dark .message.error .stacktrace {
+ background-color: rgba(0, 0, 0, 0.5);
+}
+
+.message[open] .stacktrace,
+.message.open .stacktrace {
+ display: block;
+}
+
+.message .theme-twisty {
+ display: inline-block;
+ vertical-align: middle;
+ margin: 3px 0 0 0;
+ flex-shrink: 0;
+}
+
+/*Do not mirror the twisty because container force to ltr */
+.message .theme-twisty:dir(rtl),
+.message .theme-twisty:-moz-locale-dir(rtl) {
+ transform: none;
+}
+
+.cm-s-mozilla a[class] {
+ font-style: italic;
+ text-decoration: none;
+}
+
+.cm-s-mozilla a[class]:hover,
+.cm-s-mozilla a[class]:focus {
+ text-decoration: underline;
+}
+
+a.learn-more-link.webconsole-learn-more-link {
+ font-style: normal;
+}
+
+/* Open DOMNode in inspector button */
+.open-inspector {
+ background: url("chrome://devtools/skin/images/vview-open-inspector.png") no-repeat 0 0;
+ padding-left: 16px;
+ margin-left: 5px;
+ cursor: pointer;
+}
+
+.elementNode:hover .open-inspector,
+.open-inspector:hover {
+ filter: url(images/filters.svg#checked-icon-state);
+}
+
+.elementNode:hover .open-inspector:active,
+.open-inspector:active {
+ filter: url(images/filters.svg#checked-icon-state) brightness(0.9);
+}
+
+@media (max-width: 500px) {
+ .message > .timestamp {
+ display: none;
+ }
+ .hud-console-filter-toolbar .webconsole-filter-button .toolbarbutton-text {
+ display: none;
+ }
+ .hud-console-filter-toolbar .webconsole-filter-button {
+ min-width: 40px;
+ }
+ .hud-console-filter-toolbar .webconsole-clear-console-button {
+ min-width: 25px;
+ }
+ .webconsole-filter-button > .toolbarbutton-menubutton-button:before {
+ width: 12px;
+ height: 12px;
+ margin-inline-start: 1px;
+ }
+ .toolbarbutton-menubutton-dropmarker {
+ margin: 0px;
+ }
+}
+
+@media (max-width: 300px) {
+ .hud-console-filter-toolbar {
+ -moz-box-orient: vertical;
+ }
+ .toolbarbutton-text {
+ display: -moz-box;
+ }
+ .devtools-toolbarbutton {
+ margin-top: 3px;
+ }
+ .hud-console-filter-toolbar .hud-filter-box,
+ .hud-console-filter-toolbar .devtools-toolbarbutton {
+ margin-top: 5px;
+ }
+}
+
+/*
+ * This hardcoded width likely due to a toolkit Windows specific bug.
+ * See http://hg.mozilla.org/mozilla-central/annotate/f38d6df93cad/toolkit/themes/winstripe/global/textbox-aero.css#l7
+ */
+
+:root[platform="win"] .hud-filter-box {
+ width: 200px;
+}
+
+/* Firebug theme support for console.table() */
+
+.theme-firebug .consoletable .theme-body {
+ width: 100%;
+ border-top: 1px solid #D7D7D7;
+ border-bottom: 2px solid #D7D7D7;
+ border-left: 1px solid #D7D7D7;
+ border-right: 1px solid #D7D7D7;
+}
+
+
+/* NEW CONSOLE STYLES */
+
+#output-wrapper > div {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+}
+
+#output-container {
+ height: 100%;
+}
+
+.webconsole-output-wrapper {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ -moz-user-focus: normal;
+}
+
+.webconsole-filterbar-wrapper {
+ flex-grow: 0;
+}
+
+.webconsole-output {
+ flex: 1;
+ overflow: auto;
+}
+
+.webconsole-filterbar-primary {
+ display: flex;
+}
+
+.devtools-toolbar.webconsole-filterbar-secondary {
+ height: initial;
+}
+
+.webconsole-filterbar-primary .devtools-plaininput {
+ flex: 1 1 100%;
+}
+
+.message.startGroup .message-body,
+.message.startGroupCollapsed .message-body {
+ color: var(--theme-body-color);
+ font-weight: bold;
+}
+
+.webconsole-output-wrapper .message > .icon {
+ margin: 3px 0 0 0;
+ padding: 0 0 0 6px;
+}
+
+.message.error > .icon::before {
+ background-position: -12px -36px;
+}
+
+.message.warn > .icon::before {
+ background-position: -24px -36px;
+}
+
+.message.info > .icon::before {
+ background-position: -36px -36px;
+}
+
+.message.network .method {
+ margin-inline-end: 5px;
+}
+
+.webconsole-output-wrapper .message .indent {
+ display: inline-block;
+ border-inline-end: solid 1px var(--theme-splitter-color);
+}
+
+.message.startGroup .indent,
+.message.startGroupCollapsed .indent {
+ border-inline-end-color: transparent;
+ margin-inline-end: 5px;
+}
+
+.message.startGroup .icon,
+.message.startGroupCollapsed .icon {
+ display: none;
+}
+
+/* console.table() */
+.new-consoletable {
+ width: 100%;
+ border-collapse: collapse;
+ --consoletable-border: 1px solid var(--table-splitter-color);
+}
+
+.new-consoletable thead,
+.new-consoletable tbody {
+ background-color: var(--theme-body-background);
+}
+
+.new-consoletable th {
+ background-color: var(--theme-selection-background);
+ color: var(--theme-selection-color);
+ margin: 0;
+ padding: 5px 0 0;
+ font-weight: inherit;
+ border-inline-end: var(--consoletable-border);
+ border-bottom: var(--consoletable-border);
+}
+
+.new-consoletable tr:nth-of-type(even) {
+ background-color: var(--table-zebra-background);
+}
+
+.new-consoletable td {
+ padding: 3px 4px;
+ min-width: 100px;
+ -moz-user-focus: normal;
+ color: var(--theme-body-color);
+ border-inline-end: var(--consoletable-border);
+ height: 1.25em;
+ line-height: 1.25em;
+}
diff --git a/devtools/client/themes/widgets.css b/devtools/client/themes/widgets.css
new file mode 100644
index 000000000..a8c1dc734
--- /dev/null
+++ b/devtools/client/themes/widgets.css
@@ -0,0 +1,1621 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+.theme-dark {
+ --table-splitter-color: rgba(255,255,255,0.15);
+ --table-zebra-background: rgba(255,255,255,0.05);
+ --sidemenu-selected-arrow: url(images/item-arrow-dark-ltr.svg);
+ --sidemenu-selected-arrow-rtl: url(images/item-arrow-dark-rtl.svg);
+ --delete-icon: url(chrome://devtools/skin/images/vview-delete.png);
+ --delete-icon-2x: url(chrome://devtools/skin/images/vview-delete@2x.png);
+}
+
+.theme-light {
+ --table-splitter-color: rgba(0,0,0,0.15);
+ --table-zebra-background: rgba(0,0,0,0.05);
+ --sidemenu-selected-arrow: url(images/item-arrow-ltr.svg);
+ --sidemenu-selected-arrow-rtl: url(images/item-arrow-rtl.svg);
+ --delete-icon: url(chrome://devtools/skin/images/vview-delete.png);
+ --delete-icon-2x: url(chrome://devtools/skin/images/vview-delete@2x.png);
+}
+
+.theme-firebug {
+ --table-splitter-color: rgba(0,0,0,0.15);
+ --table-zebra-background: rgba(0,0,0,0.05);
+ --sidemenu-selected-arrow: url(images/item-arrow-ltr.svg);
+ --sidemenu-selected-arrow-rtl: url(images/item-arrow-rtl.svg);
+ --delete-icon: url(chrome://devtools/skin/images/firebug/close.svg);
+ --delete-icon-2x: url(chrome://devtools/skin/images/firebug/close.svg);
+}
+
+
+/* Generic pane helpers */
+
+.generic-toggled-pane {
+ margin-inline-start: 0 !important;
+ /* Unfortunately, transitions don't work properly with locale-aware properties,
+ so both the left and right margins are set via js, while the start margin
+ is always overridden here. */
+}
+
+.generic-toggled-pane[animated] {
+ transition: margin 0.25s ease-in-out;
+}
+
+/* Responsive container */
+
+.devtools-responsive-container {
+ -moz-box-orient: horizontal;
+}
+
+.devtools-main-content {
+ min-width: 50px;
+}
+
+.devtools-main-content,
+.devtools-sidebar-tabs {
+ /* Prevent some children that should be hidden from remaining visible as this is shrunk (Bug 971959) */
+ position: relative;
+}
+
+@media (min-width: 701px) {
+ .devtools-responsive-container .generic-toggled-pane {
+ /* To hide generic-toggled-pane, negative margins are applied dynamically.
+ * In the default horizontal layout, the pane is on the side and should be
+ * hidden using negative margin-inline-end only.
+ */
+ margin-top: 0 !important;
+ margin-bottom: 0 !important;
+ }
+}
+
+@media (max-width: 700px) {
+ .devtools-responsive-container {
+ -moz-box-orient: vertical;
+ }
+
+ .devtools-responsive-container > .devtools-side-splitter {
+ /* This is a normally vertical splitter, but we have turned it horizontal
+ due to the smaller resolution */
+ min-height: calc(var(--devtools-splitter-top-width) +
+ var(--devtools-splitter-bottom-width) + 1px);
+ border-top-width: var(--devtools-splitter-top-width);
+ border-bottom-width: var(--devtools-splitter-bottom-width);
+ margin-top: calc(-1 * var(--devtools-splitter-top-width) - 1px);
+ margin-bottom: calc(-1 * var(--devtools-splitter-bottom-width));
+
+ /* Reset the vertical splitter styles */
+ min-width: 0;
+ border-inline-end-width: 0;
+ border-inline-start-width: 0;
+ margin-inline-end: 0;
+ margin-inline-start: 0;
+
+ /* In some edge case the cursor is not changed to n-resize */
+ cursor: n-resize;
+ }
+
+ .devtools-responsive-container > .devtools-sidebar-tabs:not(.pane-collapsed) {
+ /* When the panel is collapsed min/max height should not be applied because
+ collapsing relies on negative margins, which implies constant height. */
+ min-height: 35vh;
+ max-height: 75vh;
+ }
+
+ .devtools-responsive-container .generic-toggled-pane {
+ /* To hide generic-toggled-pane, negative margins are applied dynamically.
+ * If a vertical layout, the pane is on the bottom and should be hidden
+ * using negative bottom margin only.
+ */
+ margin-inline-end: 0 !important;
+ }
+}
+
+/* BreacrumbsWidget */
+
+.breadcrumbs-widget-container {
+ margin-inline-end: 3px;
+ max-height: 24px; /* Set max-height for proper sizing on linux */
+ height: 24px; /* Set height to prevent starting small waiting for content */
+}
+
+.scrollbutton-up,
+.scrollbutton-down {
+ -moz-appearance: none;
+ background: transparent;
+ box-shadow: none;
+ border: none;
+ list-style-image: none;
+ margin: 0;
+ padding: 0;
+}
+
+.scrollbutton-up > .toolbarbutton-icon,
+.scrollbutton-down > .toolbarbutton-icon {
+ -moz-appearance: none;
+ width: 7px;
+ height: 16px;
+ background-size: 14px 16px;
+ background-position: 0 center;
+ background-repeat: no-repeat;
+ background-image: url("images/breadcrumbs-scrollbutton.png");
+ list-style-image: none;
+ margin: 0 8px;
+ padding: 0;
+}
+
+@media (min-resolution: 1.1dppx) {
+ .scrollbutton-up > .toolbarbutton-icon,
+ .scrollbutton-down > .toolbarbutton-icon {
+ background-image: url("images/breadcrumbs-scrollbutton@2x.png");
+ }
+}
+
+.scrollbutton-up:not([disabled]):active:hover > .toolbarbutton-icon,
+.scrollbutton-down:not([disabled]):active:hover > .toolbarbutton-icon {
+ background-position: -7px center;
+}
+
+.scrollbutton-up[disabled] > .toolbarbutton-icon,
+.scrollbutton-down[disabled] > .toolbarbutton-icon {
+ opacity: 0.5;
+}
+
+/* Draw shadows to indicate there is more content 'behind' scrollbuttons. */
+.scrollbutton-up:-moz-locale-dir(ltr),
+.scrollbutton-down:-moz-locale-dir(rtl) {
+ border-right: solid 1px rgba(255, 255, 255, .1);
+ border-left: solid 1px transparent;
+ box-shadow: 3px 0px 3px -3px var(--theme-sidebar-background);
+}
+
+.scrollbutton-down:-moz-locale-dir(ltr),
+.scrollbutton-up:-moz-locale-dir(rtl) {
+ border-right: solid 1px transparent;
+ border-left: solid 1px rgba(255, 255, 255, .1);
+ box-shadow: -3px 0px 3px -3px var(--theme-sidebar-background);
+}
+
+.scrollbutton-up[disabled],
+.scrollbutton-down[disabled] {
+ box-shadow: none;
+ border-color: transparent;
+}
+
+.scrollbutton-up > .toolbarbutton-icon:-moz-locale-dir(rtl),
+.scrollbutton-down > .toolbarbutton-icon:-moz-locale-dir(ltr) {
+ transform: scaleX(-1);
+}
+
+.breadcrumbs-widget-item {
+ background-color: transparent;
+ -moz-appearance: none;
+ min-height: 24px;
+ min-width: 65px;
+ margin: 0;
+ padding: 0 8px 0 20px;
+ border: none;
+ outline: none;
+ color: hsl(210,30%,85%);
+ position: relative;
+}
+
+.breadcrumbs-widget-item > .button-box {
+ border: none;
+ padding-top: 0;
+ padding-bottom: 0;
+}
+
+:root[platform="win"] .breadcrumbs-widget-item:-moz-focusring > .button-box {
+ border-width: 0;
+}
+
+.breadcrumbs-widget-item::before {
+ content: "";
+ position: absolute;
+ top: 1px;
+ offset-inline-start: 0;
+ width: 12px;
+ height: 22px;
+ background-repeat: no-repeat;
+ /* Given the 1/2 aspect ratio of the separator pseudo-element and the 45deg angle of
+ the arrow shape, we need the arrow edges to be at this position from the start of
+ the gradient line. */
+ --position: 66.5%;
+ /* The color of the thin line in the arrow-shaped separator between 2 unselected
+ crumbs. There is no theme variable for this, this used to be an image. */
+ --line-color: #ACACAC;
+ --background-color: var(--theme-body-background);
+}
+
+#debugger-toolbar .breadcrumbs-widget-item::before {
+ --background-color: var(--theme-toolbar-background);
+}
+
+.theme-dark .breadcrumbs-widget-item::before {
+ --line-color: #6E6E6E;
+}
+
+.breadcrumbs-widget-item:first-child::before {
+ /* The first crumb does not need any separator before itself */
+ content: unset;
+}
+
+.breadcrumbs-widget-item:dir(rtl)::before {
+ transform: scaleX(-1);
+}
+
+.breadcrumbs-widget-item:not([checked])::before {
+ background-color: var(--background-color);
+ background-image:
+ linear-gradient(45deg,
+ var(--background-color) 30%,
+ transparent),
+ linear-gradient(-45deg,
+ transparent,
+ var(--background-color) 70%,
+ var(--background-color)),
+ linear-gradient(45deg,
+ transparent var(--position),
+ var(--line-color) var(--position),
+ var(--line-color) calc(var(--position) + 1px),
+ transparent 0),
+ linear-gradient(-45deg,
+ transparent calc(100% - var(--position)),
+ var(--line-color) calc(100% - var(--position)),
+ var(--line-color) calc(calc(100% - var(--position)) + 1px),
+ transparent 0);
+ background-size:
+ 100% 50%,
+ 100% 50%,
+ 100%,
+ 100%;
+ background-position:
+ left bottom,
+ left top,
+ left top,
+ left top;
+}
+
+.breadcrumbs-widget-item[checked] + .breadcrumbs-widget-item::before {
+ background-color: var(--theme-selection-background);
+ background-image:
+ linear-gradient(45deg,
+ transparent var(--position),
+ var(--background-color) 0),
+ linear-gradient(-45deg,
+ var(--background-color) calc(100% - var(--position)),
+ transparent 0);
+ background-size: unset;
+}
+
+.breadcrumbs-widget-item[checked]::before {
+ background-image:
+ linear-gradient(45deg,
+ transparent var(--position),
+ var(--theme-selection-background) 0),
+ linear-gradient(-45deg,
+ var(--theme-selection-background) calc(100% - var(--position)),
+ var(--background-color) 0);
+}
+
+.breadcrumbs-widget-item[checked] {
+ background-color: var(--theme-selection-background);
+}
+
+.breadcrumbs-widget-item:first-child {
+ background-image: none;
+}
+
+/* RTL support: move the images that were on the left to the right,
+ * and move images that were on the right to the left.
+ */
+.breadcrumbs-widget-item:dir(rtl) {
+ padding: 0 20px 0 8px;
+}
+
+.breadcrumbs-widget-item:dir(rtl),
+.breadcrumbs-widget-item[checked] + .breadcrumbs-widget-item:dir(rtl) {
+ background-position: center right;
+}
+
+.breadcrumbs-widget-item[checked] .breadcrumbs-widget-item-id,
+.breadcrumbs-widget-item[checked] .breadcrumbs-widget-item-tag,
+.breadcrumbs-widget-item[checked] .breadcrumbs-widget-item-pseudo-classes,
+.breadcrumbs-widget-item[checked] .breadcrumbs-widget-item-classes {
+ color: var(--theme-selection-color);
+}
+
+.theme-dark .breadcrumbs-widget-item {
+ color: var(--theme-selection-color);
+}
+
+.theme-light .breadcrumbs-widget-item {
+ color: var(--theme-body-color);
+}
+
+.breadcrumbs-widget-item-id {
+ color: var(--theme-body-color-alt);
+}
+
+.breadcrumbs-widget-item-classes {
+ color: var(--theme-content-color1);
+}
+
+.breadcrumbs-widget-item-pseudo-classes {
+ color: var(--theme-highlight-lightorange);
+}
+
+.theme-dark .breadcrumbs-widget-item:not([checked]):hover label {
+ color: white;
+}
+
+.theme-light .breadcrumbs-widget-item:not([checked]):hover label {
+ color: black;
+}
+
+/* Firebug theme support for breadcrumbs widget. */
+
+.theme-firebug .breadcrumbs-widget-item {
+ margin-inline-start: 10px;
+ margin-inline-end: 1px;
+ background-image: none;
+ border: 1px solid transparent;
+ color: #141414;
+ border-radius: 2px;
+ min-width: 0;
+ min-height: 0;
+ padding: 0;
+ font-size: var(--theme-toolbar-font-size);
+}
+
+.theme-firebug .breadcrumbs-widget-item:hover {
+ border-color: rgba(0, 0, 0, 0.2);
+ background: transparent linear-gradient(
+ rgba(255, 255, 255, 0.4),
+ rgba(255, 255, 255, 0.2)) no-repeat;
+ box-shadow: 1px 1px 1px rgba(255, 255, 255, 0.6) inset,
+ 0 0 1px rgba(255, 255, 255, 0.6) inset,
+ 0 0 2px rgba(0, 0, 0, 0.05);
+}
+
+.theme-firebug .breadcrumbs-widget-item > .button-box {
+ padding-left: 0;
+ padding-right: 0;
+}
+
+.theme-firebug .breadcrumbs-widget-item:first-child {
+ margin: 0;
+}
+
+.theme-firebug .breadcrumbs-widget-item:not(:first-child)::before {
+ content: url(chrome://devtools/skin/images/firebug/breadcrumbs-divider.svg);
+ background: none;
+ position: relative;
+ left: -3px;
+ margin: 0 0 0 -5px;
+ padding: 0;
+ width: 5px;
+}
+
+/* Breadcrumbs Separators (reset selection styles) */
+.theme-firebug .breadcrumbs-widget-item[checked],
+.theme-firebug .breadcrumbs-widget-item[checked] .breadcrumbs-widget-item-id,
+.theme-firebug .breadcrumbs-widget-item[checked] .breadcrumbs-widget-item-tag,
+.theme-firebug .breadcrumbs-widget-item[checked] .breadcrumbs-widget-item-pseudo-classes,
+.theme-firebug .breadcrumbs-widget-item[checked] .breadcrumbs-widget-item-classes {
+ background: none;
+ font-weight: bold;
+ color: inherit;
+}
+
+/* The first rule is there only to make sure the default rule from
+widgets.css is overwritten. */
+.theme-firebug .breadcrumbs-widget-item[checked] + .breadcrumbs-widget-item {
+ background: none;
+}
+
+.theme-firebug .breadcrumbs-widget-item .breadcrumbs-widget-item-tag {
+ padding-left: 4px;
+ padding-right: 4px;
+}
+
+/* Breadcrumbs Scrolling Buttons */
+
+.theme-firebug .breadcrumbs-widget-container .scrollbutton-up,
+.theme-firebug .breadcrumbs-widget-container .scrollbutton-down {
+ padding: 0;
+ box-shadow: none;
+}
+
+.theme-firebug .breadcrumbs-widget-container .scrollbutton-up:hover,
+.theme-firebug .breadcrumbs-widget-container .scrollbutton-down:hover {
+ border: 1px transparent solid !important;
+ box-shadow: none !important;
+}
+
+.theme-firebug .breadcrumbs-widget-container .scrollbutton-up:active,
+.theme-firebug .breadcrumbs-widget-container .scrollbutton-down:active {
+ background: none !important;
+}
+
+.theme-firebug .breadcrumbs-widget-container .scrollbutton-up > .toolbarbutton-icon {
+ background-image: url(chrome://global/skin/arrow/arrow-lft-sharp.gif);
+}
+
+.theme-firebug .breadcrumbs-widget-container .scrollbutton-down > .toolbarbutton-icon {
+ background-image: url(chrome://global/skin/arrow/arrow-lft-sharp.gif);
+}
+
+.theme-firebug .breadcrumbs-widget-container .scrollbutton-up:not([disabled]):active:hover > .toolbarbutton-icon,
+.theme-firebug .breadcrumbs-widget-container .scrollbutton-down:not([disabled]):active:hover > .toolbarbutton-icon {
+ background-position: 0 center;
+}
+
+/* SimpleListWidget */
+
+.simple-list-widget-container {
+ /* Hack: force hardware acceleration */
+ transform: translateZ(1px);
+}
+
+.simple-list-widget-item.selected {
+ background-color: var(--theme-selection-background);
+ color: var(--theme-selection-color);
+}
+
+.theme-dark .simple-list-widget-item:not(.selected):hover {
+ background-color: rgba(255,255,255,.05);
+}
+
+.theme-light .simple-list-widget-item:not(.selected):hover {
+ background-color: rgba(0,0,0,.05);
+}
+
+.simple-list-widget-empty-text,
+.simple-list-widget-perma-text {
+ padding: 4px 8px;
+}
+
+.simple-list-widget-empty-text,
+.simple-list-widget-perma-text {
+ color: var(--theme-body-color-alt);
+}
+
+/* FastListWidget */
+
+.fast-list-widget-container {
+ /* Hack: force hardware acceleration */
+ transform: translateZ(1px);
+}
+
+.fast-list-widget-empty-text {
+ padding: 4px 8px;
+}
+
+.fast-list-widget-empty-text {
+ color: var(--theme-body-color-alt);
+}
+
+/* SideMenuWidget */
+
+.side-menu-widget-container {
+ /* Hack: force hardware acceleration */
+ transform: translateZ(1px);
+}
+
+/* SideMenuWidget container */
+
+.side-menu-widget-container[with-arrows=true] .side-menu-widget-item {
+ /* To compensate for the arrow image's dark margin. */
+ margin-inline-end: -1px;
+}
+
+/* SideMenuWidget groups */
+
+.side-menu-widget-group-title {
+ padding: 4px;
+ font-weight: 600;
+ border-bottom: 1px solid rgba(128,128,128,0.15);
+}
+
+.side-menu-widget-group-title + .side-menu-widget-group-list .side-menu-widget-item-contents {
+ padding-inline-start: 20px;
+}
+
+/* SideMenuWidget items */
+
+.side-menu-widget-item {
+ border-bottom: 1px solid rgba(128,128,128,0.15);
+ background-clip: padding-box;
+}
+
+.side-menu-widget-item.selected {
+ background-color: var(--theme-selection-background);
+ color: var(--theme-selection-color);
+}
+
+.side-menu-widget-item-arrow {
+ margin-inline-start: -7px;
+ width: 7px; /* The image's width is 7 pixels */
+}
+
+.side-menu-widget-item.selected > .side-menu-widget-item-arrow {
+ background-image: var(--sidemenu-selected-arrow);
+ background-size: auto;
+ background-repeat: no-repeat;
+ background-position: center right;
+}
+
+.side-menu-widget-item.selected > .side-menu-widget-item-arrow:-moz-locale-dir(rtl) {
+ background-image: var(--sidemenu-selected-arrow-rtl);
+ background-position: center left;
+}
+
+/* SideMenuWidget items contents */
+
+.side-menu-widget-item-contents {
+ padding: 4px;
+ /* To avoid having content overlapping the arrow image. */
+ padding-inline-end: 8px;
+}
+
+.side-menu-widget-item-other {
+ /* To avoid having content overlapping the arrow image. */
+ padding-inline-end: 8px;
+ /* To compensate for the .side-menu-widget-item-contents padding. */
+ margin-inline-start: -4px;
+ margin-inline-end: -8px;
+}
+
+.side-menu-widget-group-title + .side-menu-widget-group-list .side-menu-widget-item-other {
+ /* To compensate for the .side-menu-widget-item-contents padding. */
+ margin-inline-start: -20px;
+}
+
+.side-menu-widget-item.selected .side-menu-widget-item-other:not(.selected) {
+ background-color: var(--theme-sidebar-background);
+ box-shadow: inset 2px 0 0 var(--theme-selection-background);
+ color: var(--theme-body-color);
+}
+
+.side-menu-widget-item.selected .side-menu-widget-item-other.selected {
+ background-color: var(--theme-selection-background);
+}
+
+.side-menu-widget-item-other:first-of-type {
+ margin-top: 4px;
+}
+
+.side-menu-widget-item-other:last-of-type {
+ margin-bottom: -4px;
+}
+
+/* SideMenuWidget checkboxes */
+
+.side-menu-widget-group-checkbox {
+ margin: 0;
+ margin-inline-end: 4px;
+}
+
+.side-menu-widget-item-checkbox {
+ margin: 0;
+ margin-inline-start: 4px;
+}
+
+/* SideMenuWidget misc */
+
+.side-menu-widget-empty-text {
+ padding: 4px 8px;
+ background-color: var(--theme-sidebar-background);
+}
+
+/* VariablesView */
+
+.variables-view-container {
+ /* Hack: force hardware acceleration */
+ transform: translateZ(1px);
+}
+
+.variables-view-empty-notice {
+ padding: 2px;
+}
+
+.variables-view-empty-notice {
+ color: var(--theme-body-color-alt);
+}
+
+.variables-view-scope:focus > .title,
+.variable-or-property:focus > .title {
+ background-color: var(--theme-selection-background);
+ color: var(--theme-selection-color);
+}
+
+.variables-view-scope > .title {
+ border-top-width: 1px;
+ border-top-style: solid;
+ margin-top: -1px;
+}
+
+/* Custom scope stylings */
+
+.variables-view-watch-expressions .title > .name {
+ max-width: 14em;
+}
+
+/* Generic variables traits */
+
+.variables-view-variable:not(:last-child) {
+ border-bottom: 1px solid rgba(128, 128, 128, .15);
+}
+
+.theme-firebug .variables-view-variable {
+ border-bottom: 1px solid transparent;
+}
+
+.variables-view-variable > .title > .name {
+ font-weight: 600;
+}
+
+/* Generic variables *and* properties traits */
+
+.variable-or-property:focus > .title > label {
+ color: inherit !important;
+}
+
+.variables-view-container .theme-twisty {
+ margin: 2px;
+}
+
+.variable-or-property > .title > .theme-twisty {
+ margin-inline-start: 5px;
+}
+
+.variable-or-property:not([untitled]) > .variables-view-element-details {
+ margin-inline-start: 7px;
+}
+
+/* Traits applied when variables or properties are changed or overridden */
+
+.variable-or-property:not([overridden]) {
+ transition: background 1s ease-in-out;
+}
+
+.variable-or-property:not([overridden])[changed] {
+ transition-duration: .4s;
+}
+
+.variable-or-property[overridden] {
+ background: rgba(128,128,128,0.05);
+}
+
+.variable-or-property[overridden] .title > label {
+ /* Cross out the title for this variable and all child properties. */
+ font-style: italic;
+ text-decoration: line-through;
+ border-bottom: none !important;
+ color: rgba(128,128,128,0.9);
+ opacity: 0.7;
+}
+
+/* Traits applied when variables or properties are editable */
+
+.variable-or-property[editable] > .title > .value {
+ cursor: text;
+}
+
+.variable-or-property[overridden] .title > .value {
+ /* Disallow editing this variable and all child properties. */
+ pointer-events: none;
+}
+
+/* Custom configurable/enumerable/writable or frozen/sealed/extensible
+ * variables and properties */
+
+.variable-or-property[non-enumerable]:not([self]):not([pseudo-item]) > .title > .name {
+ opacity: 0.6;
+}
+
+.variable-or-property-non-writable-icon {
+ background: url("chrome://devtools/skin/images/vview-lock.png") no-repeat;
+ background-size: cover;
+ width: 16px;
+ height: 16px;
+}
+
+@media (min-resolution: 1.1dppx) {
+ .variable-or-property-non-writable-icon {
+ background-image: url("chrome://devtools/skin/images/vview-lock@2x.png");
+ }
+}
+
+.variable-or-property-frozen-label,
+.variable-or-property-sealed-label,
+.variable-or-property-non-extensible-label {
+ height: 16px;
+ padding-inline-end: 4px;
+}
+
+.variable-or-property:not(:focus) > .title > .variable-or-property-frozen-label,
+.variable-or-property:not(:focus) > .title > .variable-or-property-sealed-label,
+.variable-or-property:not(:focus) > .title > .variable-or-property-non-extensible-label {
+ color: #666;
+}
+
+/* Aligned values */
+
+.variables-view-container[aligned-values] .title > .separator {
+ -moz-box-flex: 1;
+}
+
+.variables-view-container[aligned-values] .title > .value {
+ -moz-box-flex: 0;
+ width: 70vw;
+}
+
+.variables-view-container[aligned-values] .title > .element-value-input {
+ width: calc(70vw - 10px);
+}
+
+/* Actions first */
+
+.variables-view-open-inspector {
+ -moz-box-ordinal-group: 1;
+}
+
+.variables-view-edit,
+.variables-view-add-property {
+ -moz-box-ordinal-group: 2;
+}
+
+.variable-or-property-frozen-label,
+.variable-or-property-sealed-label,
+.variable-or-property-non-extensible-label,
+.variable-or-property-non-writable-icon {
+ -moz-box-ordinal-group: 3;
+}
+
+.variables-view-delete {
+ -moz-box-ordinal-group: 4;
+}
+
+.variables-view-container[actions-first] .variables-view-delete,
+.variables-view-container[actions-first] .variables-view-add-property,
+.variables-view-container[actions-first] .variables-view-open-inspector {
+ -moz-box-ordinal-group: 0;
+}
+
+.variables-view-container[actions-first] [invisible] {
+ visibility: hidden;
+}
+
+/* Variables and properties tooltips */
+
+.variable-or-property > tooltip > label {
+ margin: 0 2px 0 2px;
+}
+
+.variable-or-property[non-enumerable] > tooltip > label.enumerable,
+.variable-or-property[non-configurable] > tooltip > label.configurable,
+.variable-or-property[non-writable] > tooltip > label.writable,
+.variable-or-property[non-extensible] > tooltip > label.extensible {
+ color: #800;
+ text-decoration: line-through;
+}
+
+.variable-or-property[overridden] > tooltip > label.overridden {
+ padding-inline-start: 4px;
+ border-inline-start: 1px dotted #000;
+}
+
+.variable-or-property[safe-getter] > tooltip > label.WebIDL {
+ padding-inline-start: 4px;
+ border-inline-start: 1px dotted #000;
+ color: #080;
+}
+
+/* Variables and properties editing */
+.variables-view-delete,
+.variables-view-edit,
+.variables-view-open-inspector {
+ width: 16px;
+ height: 16px;
+ background-size: cover;
+ cursor: pointer;
+}
+
+.variables-view-delete:hover,
+.variables-view-edit:hover,
+.variables-view-open-inspector:hover {
+ filter: url(images/filters.svg#checked-icon-state);
+}
+
+.variables-view-delete:active,
+.variables-view-edit:active,
+.variables-view-open-inspector:active {
+ filter: url(images/filters.svg#checked-icon-state) brightness(0.9);
+}
+
+.variable-or-property:focus > .title > .variables-view-delete,
+.variable-or-property:focus > .title > .variables-view-edit,
+.variable-or-property:focus > .title > .variables-view-open-inspector {
+ filter: none;
+}
+
+.variables-view-delete {
+ background-image: var(--delete-icon);
+}
+
+@media (min-resolution: 1.1dppx) {
+ .variables-view-delete {
+ background-image: var(--delete-icon-2x);
+ }
+}
+
+.variables-view-edit {
+ background-image: url("chrome://devtools/skin/images/vview-edit.png");
+}
+
+@media (min-resolution: 1.1dppx) {
+ .variables-view-edit {
+ background-image: url("chrome://devtools/skin/images/vview-edit@2x.png");
+ }
+}
+
+.variables-view-open-inspector {
+ background-image: url("chrome://devtools/skin/images/vview-open-inspector.png");
+}
+
+@media (min-resolution: 1.1dppx) {
+ .variables-view-open-inspector {
+ background-image: url("chrome://devtools/skin/images/vview-open-inspector@2x.png");
+ }
+}
+
+/* Variables and properties input boxes */
+
+.variable-or-property > .title > .separator + .element-value-input {
+ margin-inline-start: -2px !important;
+ margin-inline-end: 2px !important;
+}
+
+.variable-or-property > .title > .separator[hidden=true] + .element-value-input {
+ margin-inline-start: 4px !important;
+ margin-inline-end: 2px !important;
+}
+
+.element-name-input {
+ margin-inline-start: -2px !important;
+ margin-inline-end: 2px !important;
+ font-weight: 600;
+}
+
+.element-value-input,
+.element-name-input {
+ border: 1px solid rgba(128, 128, 128, .5) !important;
+ border-radius: 0;
+ color: inherit;
+}
+
+/* Variables and properties searching */
+
+.variable-or-property[unmatched] {
+ border: none;
+ margin: 0;
+}
+
+/* Canvas graphs */
+
+.graph-widget-container {
+ position: relative;
+}
+
+.graph-widget-canvas {
+ width: 100%;
+ height: 100%;
+}
+
+.graph-widget-canvas[input=hovering-background] {
+ cursor: text;
+}
+
+.graph-widget-canvas[input=hovering-region] {
+ cursor: pointer;
+}
+
+.graph-widget-canvas[input=hovering-selection-start-boundary],
+.graph-widget-canvas[input=hovering-selection-end-boundary],
+.graph-widget-canvas[input=adjusting-selection-boundary] {
+ cursor: col-resize;
+}
+
+.graph-widget-canvas[input=adjusting-view-area] {
+ cursor: grabbing;
+}
+
+.graph-widget-canvas[input=hovering-selection-contents] {
+ cursor: grab;
+}
+
+.graph-widget-canvas[input=dragging-selection-contents] {
+ cursor: grabbing;
+}
+
+/* Line graph widget */
+
+.line-graph-widget-gutter {
+ position: absolute;
+ width: 10px;
+ height: 100%;
+ top: 0;
+ left: 0;
+ pointer-events: none;
+ border-inline-end: 1px solid;
+}
+
+.theme-light .line-graph-widget-gutter {
+ background: rgba(255,255,255,0.75);
+ border-inline-end-color: rgba(255,255,255,0.25);
+}
+
+.theme-dark .line-graph-widget-gutter {
+ background: rgba(0,0,0,0.5);
+ border-inline-end-color: rgba(0,0,0,0.25);
+}
+
+.line-graph-widget-gutter-line {
+ position: absolute;
+ width: 100%;
+ border-top: 1px solid;
+}
+
+.line-graph-widget-gutter-line[type=maximum] {
+ border-color: #2cbb0f;
+}
+
+.line-graph-widget-gutter-line[type=minimum] {
+ border-color: #ed2655;
+}
+
+.line-graph-widget-gutter-line[type=average] {
+ border-color: #d97e00;
+}
+
+.line-graph-widget-tooltip {
+ position: absolute;
+ border-radius: 2px;
+ line-height: 15px;
+ padding-inline-start: 6px;
+ padding-inline-end: 6px;
+ transform: translateY(-50%);
+ font-size: 0.8rem !important;
+ z-index: 1;
+ pointer-events: none;
+}
+
+.theme-light .line-graph-widget-tooltip {
+ background: rgba(255,255,255,0.75);
+}
+
+.theme-dark .line-graph-widget-tooltip {
+ background: rgba(0,0,0,0.5);
+}
+
+.line-graph-widget-tooltip[with-arrows=true]::before {
+ content: "";
+ position: absolute;
+ border-top: 3px solid transparent;
+ border-bottom: 3px solid transparent;
+ top: calc(50% - 3px);
+}
+
+.line-graph-widget-tooltip[arrow=start][with-arrows=true]::before {
+ border-inline-end: 3px solid;
+ left: -3px;
+}
+
+.line-graph-widget-tooltip[arrow=end][with-arrows=true]::before {
+ border-inline-start: 3px solid;
+ right: -3px;
+}
+
+.theme-light .line-graph-widget-tooltip[arrow=start][with-arrows=true]::before {
+ border-inline-end-color: rgba(255,255,255,0.75);
+}
+
+.theme-dark .line-graph-widget-tooltip[arrow=start][with-arrows=true]::before {
+ border-inline-end-color: rgba(0,0,0,0.5);
+}
+
+.theme-light .line-graph-widget-tooltip[arrow=end][with-arrows=true]::before {
+ border-inline-start-color: rgba(255,255,255,0.75);
+}
+
+.theme-dark .line-graph-widget-tooltip[arrow=end][with-arrows=true]::before {
+ border-inline-start-color: rgba(0,0,0,0.5);
+}
+
+.line-graph-widget-tooltip[type=maximum] {
+ left: 14px;
+}
+
+.line-graph-widget-tooltip[type=minimum] {
+ left: 14px;
+}
+
+.line-graph-widget-tooltip[type=average] {
+ right: 4px;
+}
+
+.line-graph-widget-tooltip > [text=info] {
+ color: var(--theme-content-color1);
+}
+
+.line-graph-widget-tooltip > [text=value] {
+ margin-inline-start: 3px;
+}
+
+.line-graph-widget-tooltip > [text=metric] {
+ margin-inline-start: 1px;
+ color: var(--theme-content-color3);
+}
+
+.theme-light .line-graph-widget-tooltip > [text=value],
+.theme-light .line-graph-widget-tooltip > [text=metric] {
+ text-shadow: 1px 0px rgba(255,255,255,0.5),
+ -1px 0px rgba(255,255,255,0.5),
+ 0px -1px rgba(255,255,255,0.5),
+ 0px 1px rgba(255,255,255,0.5);
+}
+
+.theme-dark .line-graph-widget-tooltip > [text=value],
+.theme-dark .line-graph-widget-tooltip > [text=metric] {
+ text-shadow: 1px 0px rgba(0,0,0,0.5),
+ -1px 0px rgba(0,0,0,0.5),
+ 0px -1px rgba(0,0,0,0.5),
+ 0px 1px rgba(0,0,0,0.5);
+}
+
+.line-graph-widget-tooltip[type=maximum] > [text=value] {
+ color: var(--theme-highlight-green);
+}
+
+.line-graph-widget-tooltip[type=minimum] > [text=value] {
+ color: var(--theme-highlight-red);
+}
+
+.line-graph-widget-tooltip[type=average] > [text=value] {
+ color: var(--theme-highlight-orange);
+}
+
+/* Bar graph widget */
+
+.bar-graph-widget-legend {
+ position: absolute;
+ top: 4px;
+ left: 8px;
+ color: #292e33;
+ font-size: 80%;
+ pointer-events: none;
+}
+
+.bar-graph-widget-legend-item {
+ float: left;
+ margin-inline-end: 8px;
+}
+
+.bar-graph-widget-legend-item > [view="color"],
+.bar-graph-widget-legend-item > [view="label"] {
+ vertical-align: middle;
+}
+
+.bar-graph-widget-legend-item > [view="color"] {
+ display: inline-block;
+ width: 8px;
+ height: 8px;
+ border: 1px solid #fff;
+ border-radius: 1px;
+ margin-inline-end: 4px;
+ pointer-events: all;
+ cursor: pointer;
+}
+
+.bar-graph-widget-legend-item > [view="label"] {
+ text-shadow: 1px 0px rgba(255,255,255,0.8),
+ -1px 0px rgba(255,255,255,0.8),
+ 0px -1px rgba(255,255,255,0.8),
+ 0px 1px rgba(255,255,255,0.8);
+}
+
+/* Charts */
+
+.generic-chart-container {
+ /* Hack: force hardware acceleration */
+ transform: translateZ(1px);
+}
+
+.theme-dark .generic-chart-container {
+ color: var(--theme-selection-color);
+}
+
+.theme-light .generic-chart-container {
+ color: var(--theme-body-color-alt);
+}
+
+.chart-colored-blob {
+ fill: var(--theme-content-color2);
+ background: var(--theme-content-color2);
+}
+
+/* Charts: Pie */
+
+.pie-chart-slice {
+ stroke-width: 1px;
+ cursor: pointer;
+}
+
+.theme-dark .pie-chart-slice {
+ stroke: rgba(0,0,0,0.2);
+}
+
+.theme-light .pie-chart-slice {
+ stroke: rgba(255,255,255,0.8);
+}
+
+.theme-dark .pie-chart-slice[largest] {
+ stroke-width: 2px;
+ stroke: #fff;
+}
+
+.theme-light .pie-chart-slice[largest] {
+ stroke: #000;
+}
+
+.pie-chart-label {
+ text-anchor: middle;
+ dominant-baseline: middle;
+ pointer-events: none;
+}
+
+.theme-dark .pie-chart-label {
+ fill: #000;
+}
+
+.theme-light .pie-chart-label {
+ fill: #fff;
+}
+
+.pie-chart-container[slices="1"] > .pie-chart-slice {
+ stroke-width: 0px;
+}
+
+.pie-chart-slice,
+.pie-chart-label {
+ transition: all 0.1s ease-out;
+}
+
+.pie-chart-slice:not(:hover):not([focused]),
+.pie-chart-slice:not(:hover):not([focused]) + .pie-chart-label {
+ transform: none !important;
+}
+
+/* Charts: Table */
+
+.table-chart-title {
+ padding-bottom: 10px;
+ font-size: 120%;
+ font-weight: 600;
+}
+
+.table-chart-row {
+ margin-top: 1px;
+ cursor: pointer;
+}
+
+.table-chart-grid:hover > .table-chart-row {
+ transition: opacity 0.1s ease-in-out;
+}
+
+.table-chart-grid:not(:hover) > .table-chart-row {
+ transition: opacity 0.2s ease-in-out;
+}
+
+.generic-chart-container:hover > .table-chart-grid:hover > .table-chart-row:not(:hover),
+.generic-chart-container:hover ~ .table-chart-container > .table-chart-grid > .table-chart-row:not([focused]) {
+ opacity: 0.4;
+}
+
+.table-chart-row-box {
+ width: 8px;
+ height: 1.5em;
+ margin-inline-end: 10px;
+}
+
+.table-chart-row-label {
+ width: 8em;
+ padding-inline-end: 6px;
+ cursor: inherit;
+}
+
+.table-chart-totals {
+ margin-top: 8px;
+ padding-top: 6px;
+}
+
+.table-chart-totals {
+ border-top: 1px solid var(--theme-body-color-alt); /* Grey foreground text */
+}
+
+.table-chart-summary-label {
+ font-weight: 600;
+ padding: 1px 0px;
+}
+
+.theme-dark .table-chart-summary-label {
+ color: var(--theme-selection-color);
+}
+
+.theme-light .table-chart-summary-label {
+ color: var(--theme-body-color);
+}
+
+/* Table Widget */
+
+/* Table body */
+
+.table-widget-body > .devtools-side-splitter {
+ background-color: transparent;
+}
+
+.table-widget-body {
+ overflow: auto;
+}
+
+.table-widget-body,
+.table-widget-empty-text {
+ background-color: var(--theme-body-background);
+}
+
+/* Column Headers */
+
+.table-widget-column-header,
+.table-widget-cell {
+ border-inline-end: 1px solid var(--table-splitter-color) !important;
+}
+
+/* Table widget column header colors are taken from netmonitor.inc.css to match
+ the look of both the tables. */
+
+.table-widget-column-header {
+ position: sticky;
+ top: 0;
+ width: 100%;
+ margin: 0;
+ padding: 5px 0 0 !important;
+ color: inherit;
+ text-align: center;
+ font-weight: inherit !important;
+ border-image: linear-gradient(transparent 15%,
+ var(--theme-splitter-color) 15%,
+ var(--theme-splitter-color) 85%,
+ transparent 85%,
+ transparent calc(100% - 1px),
+ var(--theme-splitter-color) calc(100% - 1px)) 1 1;
+ background-repeat: no-repeat;
+}
+
+.table-widget-column-header:not([sorted]):hover {
+ background-image: linear-gradient(rgba(0,0,0,0.1),rgba(0,0,0,0.1));
+}
+
+.table-widget-column-header[sorted] {
+ background-color: var(--theme-selection-background);
+ color: var(--theme-selection-color);
+ border-image: linear-gradient(var(--theme-splitter-color), var(--theme-splitter-color)) 1 1;
+ box-shadow: -0.5px -0.5px 0 0.5px var(--theme-splitter-color);
+ background-position: right 6px center;
+}
+
+.table-widget-column-header[sorted]:-moz-locale-dir(rtl) {
+ background-position: 6px center;
+}
+
+.table-widget-column-header[sorted=ascending] {
+ background-image: url("chrome://devtools/skin/images/sort-arrows.svg#ascending");
+}
+
+.table-widget-column-header[sorted=descending] {
+ background-image: url("chrome://devtools/skin/images/sort-arrows.svg#descending");
+}
+
+.theme-dark .table-widget-column[readonly] {
+ background-color: rgba(255,255,255,0.1);
+}
+
+.theme-light .table-widget-column[readonly] {
+ background-color: rgba(0,0,0,0.1);
+}
+
+.table-widget-body .devtools-side-splitter:last-of-type {
+ display: none;
+}
+
+/* Firebug theme support for table widget */
+
+.theme-firebug .devtools-toolbar.table-widget-column-header {
+ font-family: var(--proportional-font-family);
+ color: var(--theme-body-color);
+
+ /* Make sure to override the default Firebug devtools-toolbar height */
+ height: 19px !important;
+
+ /* Make sure to override the dafault .table-widget-column-header font-weight and background */
+ font-weight: bold !important;
+ background-image: linear-gradient(rgba(255, 255, 255, 0.05), rgba(0, 0, 0, 0.05));
+ background-color: #C8D2DC !important;
+
+ /* Vertically center header label */
+ padding-top: 2px !important;
+}
+
+.theme-firebug .devtools-toolbar.table-widget-column-header[sorted] {
+ background-color: #AAC3DC !important;
+}
+
+.theme-firebug .devtools-toolbar.table-widget-column-header:hover:active {
+ background-image: linear-gradient(rgba(0, 0, 0, 0.1),
+ transparent);
+}
+
+.theme-firebug .devtools-toolbar.table-widget-column-header[sorted=descending]:not(:active) {
+ background-image: url(chrome://devtools/skin/images/firebug/arrow-down.svg);
+}
+
+.theme-firebug .devtools-toolbar.table-widget-column-header[sorted=ascending]:not(:active) {
+ background-image: url(chrome://devtools/skin/images/firebug/arrow-up.svg);
+}
+
+/* Cells */
+
+.table-widget-cell {
+ width: 100%;
+ padding: 3px 4px;
+ min-width: 100px;
+ -moz-user-focus: normal;
+ color: var(--theme-body-color);
+}
+
+.table-widget-cell[hidden] {
+ display: none;
+}
+
+.table-widget-cell.even:not(.theme-selected) {
+ background-color: var(--table-zebra-background);
+}
+
+:root:not(.no-animate) .table-widget-cell.flash-out {
+ animation: flash-out 0.5s ease-in;
+}
+
+@keyframes flash-out {
+ to {
+ background: var(--theme-contrast-background);
+ }
+}
+
+/* Empty text and initial text */
+
+.table-widget-empty-text {
+ display: none;
+ text-align: center;
+ font-size: large;
+ margin-top: -20px !important;
+}
+
+.table-widget-body:empty + .table-widget-empty-text:not([value=""]),
+.table-widget-body[empty] + .table-widget-empty-text:not([value=""]) {
+ display: block;
+}
+
+/* Tree Widget */
+
+.tree-widget-container {
+ padding: 0;
+ margin: 0;
+ width: 100%;
+ height: 100%;
+ list-style: none;
+ overflow: hidden;
+ margin-inline-end: 40px;
+}
+
+.tree-widget-container:-moz-focusring,
+.tree-widget-container *:-moz-focusring {
+ outline-style: none;
+}
+
+.tree-widget-empty-text {
+ padding: 10px 20px;
+ font-size: medium;
+ background: transparent;
+ pointer-events: none;
+}
+
+/* Tree Item */
+
+.tree-widget-container .tree-widget-item {
+ padding: 2px 0px 4px;
+ /* OSX has line-height 14px by default, which causes weird alignment issues
+ * because of 20px high icons. thus making line-height consistent with that of
+ * windows.
+ */
+ line-height: 17px !important;
+ display: inline-block;
+ width: 100%;
+ word-break: keep-all; /* To prevent long urls like http://foo.com/bar from
+ breaking in multiple lines */
+}
+
+.tree-widget-container .tree-widget-children {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+
+.tree-widget-item[level="1"] {
+ font-weight: 700;
+}
+
+/* Twisties */
+.tree-widget-item::before {
+ content: "";
+ width: 14px;
+ height: 14px;
+ float: left;
+ margin: 3px 2px -3px;
+ background-repeat: no-repeat;
+ background-image: url("chrome://devtools/skin/images/controls.png");
+ background-size: 56px 28px;
+ cursor: pointer;
+ background-position: -28px -14px;
+}
+
+.tree-widget-item:-moz-locale-dir(rtl)::before {
+ float: right;
+ transform: scaleX(-1);
+}
+
+.theme-light .tree-widget-item:not(.theme-selected)::before {
+ background-position: 0 -14px;
+}
+
+.tree-widget-item[empty]::before {
+ background: transparent;
+}
+
+.tree-widget-item[expanded]::before {
+ background-position: -42px -14px;
+}
+
+.theme-light .tree-widget-item:not(.theme-selected)[expanded]:before {
+ background-position: -14px -14px;
+}
+
+.tree-widget-item + ul {
+ overflow: hidden;
+ animation: collapse-tree-item 0.2s;
+ max-height: 0;
+}
+
+.tree-widget-item[expanded] + ul {
+ animation: expand-tree-item 0.3s;
+ max-height: unset;
+}
+
+@keyframes collapse-tree-item {
+ from {
+ max-height: 300px;
+ }
+ to {
+ max-height: 0;
+ }
+}
+
+@keyframes expand-tree-item {
+ from {
+ max-height: 0;
+ }
+ to {
+ max-height: 500px;
+ }
+}
+
+@media (min-resolution: 1.1dppx) {
+ .tree-widget-item:before {
+ background-image: url("chrome://devtools/skin/images/controls@2x.png");
+ }
+}
+
+/* Indentation of child items in the tree */
+
+/* For level > 6 */
+.tree-widget-item[level] + ul > li > .tree-widget-item {
+ padding-inline-start: 98px;
+}
+
+/* First level */
+.tree-widget-item[level="1"] + ul > li > .tree-widget-item {
+ padding-inline-start: 14px;
+}
+
+/* Second level */
+.tree-widget-item[level="2"] + ul > li > .tree-widget-item {
+ padding-inline-start: 28px;
+}
+
+/* Third level */
+.tree-widget-item[level="3"] + ul > li > .tree-widget-item {
+ padding-inline-start: 42px;
+}
+
+/* Fourth level */
+.tree-widget-item[level="4"] + ul > li > .tree-widget-item {
+ padding-inline-start: 56px;
+}
+
+/* Fifth level */
+.tree-widget-item[level="5"] + ul > li > .tree-widget-item {
+ padding-inline-start: 70px;
+}
+
+/* Sixth level */
+.tree-widget-item[level="6"] + ul > li > .tree-widget-item {
+ padding-inline-start: 84px;
+}
+
+/* Custom icons for certain tree items indicating the type of the item */
+
+.tree-widget-item[type]::after {
+ content: "";
+ float: left;
+ width: 16px;
+ height: 17px;
+ margin-inline-end: 4px;
+ background-repeat: no-repeat;
+ background-size: 20px auto;
+ background-position: 0 0;
+ background-size: auto 20px;
+ opacity: 0.75;
+}
+
+.tree-widget-item.theme-selected[type]::after {
+ opacity: 1;
+}
+
+.tree-widget-item:-moz-locale-dir(rtl)::after {
+ float: right;
+}
+
+.theme-light .tree-widget-item.theme-selected[type]::after,
+.theme-dark .tree-widget-item[type]::after {
+ filter: invert(1);
+}
+
+.tree-widget-item[type="dir"]::after {
+ background-image: url(chrome://devtools/skin/images/filetypes/dir-close.svg);
+ background-position: 2px 0;
+ background-size: auto 16px;
+ width: 20px;
+}
+
+.tree-widget-item[type="dir"][expanded]:not([empty])::after {
+ background-image: url(chrome://devtools/skin/images/filetypes/dir-open.svg);
+}
+
+.tree-widget-item[type="url"]::after {
+ background-image: url(chrome://devtools/skin/images/filetypes/globe.svg);
+ background-size: auto 18px;
+ width: 18px;
+}
diff --git a/devtools/client/webaudioeditor/controller.js b/devtools/client/webaudioeditor/controller.js
new file mode 100644
index 000000000..248a2a6f3
--- /dev/null
+++ b/devtools/client/webaudioeditor/controller.js
@@ -0,0 +1,232 @@
+/* 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/. */
+
+/**
+ * A collection of `AudioNodeModel`s used throughout the editor
+ * to keep track of audio nodes within the audio context.
+ */
+var gAudioNodes = new AudioNodesCollection();
+
+/**
+ * Initializes the web audio editor views
+ */
+function startupWebAudioEditor() {
+ return all([
+ WebAudioEditorController.initialize(),
+ ContextView.initialize(),
+ InspectorView.initialize(),
+ PropertiesView.initialize(),
+ AutomationView.initialize()
+ ]);
+}
+
+/**
+ * Destroys the web audio editor controller and views.
+ */
+function shutdownWebAudioEditor() {
+ return all([
+ WebAudioEditorController.destroy(),
+ ContextView.destroy(),
+ InspectorView.destroy(),
+ PropertiesView.destroy(),
+ AutomationView.destroy()
+ ]);
+}
+
+/**
+ * Functions handling target-related lifetime events.
+ */
+var WebAudioEditorController = {
+ /**
+ * Listen for events emitted by the current tab target.
+ */
+ initialize: Task.async(function* () {
+ this._onTabNavigated = this._onTabNavigated.bind(this);
+ this._onThemeChange = this._onThemeChange.bind(this);
+
+ gTarget.on("will-navigate", this._onTabNavigated);
+ gTarget.on("navigate", this._onTabNavigated);
+ gFront.on("start-context", this._onStartContext);
+ gFront.on("create-node", this._onCreateNode);
+ gFront.on("connect-node", this._onConnectNode);
+ gFront.on("connect-param", this._onConnectParam);
+ gFront.on("disconnect-node", this._onDisconnectNode);
+ gFront.on("change-param", this._onChangeParam);
+ gFront.on("destroy-node", this._onDestroyNode);
+
+ // Hook into theme change so we can change
+ // the graph's marker styling, since we can't do this
+ // with CSS
+ gDevTools.on("pref-changed", this._onThemeChange);
+
+ // Store the AudioNode definitions from the WebAudioFront, if the method exists.
+ // If not, get the JSON directly. Using the actor method is preferable so the client
+ // knows exactly what methods are supported on the server.
+ let actorHasDefinition = yield gTarget.actorHasMethod("webaudio", "getDefinition");
+ if (actorHasDefinition) {
+ AUDIO_NODE_DEFINITION = yield gFront.getDefinition();
+ } else {
+ AUDIO_NODE_DEFINITION = require("devtools/server/actors/utils/audionodes.json");
+ }
+
+ // Make sure the backend is prepared to handle audio contexts.
+ // Since actors are created lazily on the first request to them, we need to send an
+ // early request to ensure the CallWatcherActor is running and watching for new window
+ // globals.
+ gFront.setup({ reload: false });
+ }),
+
+ /**
+ * Remove events emitted by the current tab target.
+ */
+ destroy: function () {
+ gTarget.off("will-navigate", this._onTabNavigated);
+ gTarget.off("navigate", this._onTabNavigated);
+ gFront.off("start-context", this._onStartContext);
+ gFront.off("create-node", this._onCreateNode);
+ gFront.off("connect-node", this._onConnectNode);
+ gFront.off("connect-param", this._onConnectParam);
+ gFront.off("disconnect-node", this._onDisconnectNode);
+ gFront.off("change-param", this._onChangeParam);
+ gFront.off("destroy-node", this._onDestroyNode);
+ gDevTools.off("pref-changed", this._onThemeChange);
+ },
+
+ /**
+ * Called when page is reloaded to show the reload notice and waiting
+ * for an audio context notice.
+ */
+ reset: function () {
+ $("#content").hidden = true;
+ ContextView.resetUI();
+ InspectorView.resetUI();
+ PropertiesView.resetUI();
+ },
+
+ // Since node events (create, disconnect, connect) are all async,
+ // we have to make sure to wait that the node has finished creating
+ // before performing an operation on it.
+ getNode: function* (nodeActor) {
+ let id = nodeActor.actorID;
+ let node = gAudioNodes.get(id);
+
+ if (!node) {
+ let { resolve, promise } = defer();
+ gAudioNodes.on("add", function createNodeListener(createdNode) {
+ if (createdNode.id === id) {
+ gAudioNodes.off("add", createNodeListener);
+ resolve(createdNode);
+ }
+ });
+ node = yield promise;
+ }
+ return node;
+ },
+
+ /**
+ * Fired when the devtools theme changes (light, dark, etc.)
+ * so that the graph can update marker styling, as that
+ * cannot currently be done with CSS.
+ */
+ _onThemeChange: function (event, data) {
+ window.emit(EVENTS.THEME_CHANGE, data.newValue);
+ },
+
+ /**
+ * Called for each location change in the debugged tab.
+ */
+ _onTabNavigated: Task.async(function* (event, {isFrameSwitching}) {
+ switch (event) {
+ case "will-navigate": {
+ // Clear out current UI.
+ this.reset();
+
+ // When switching to an iframe, ensure displaying the reload button.
+ // As the document has already been loaded without being hooked.
+ if (isFrameSwitching) {
+ $("#reload-notice").hidden = false;
+ $("#waiting-notice").hidden = true;
+ } else {
+ // Otherwise, we are loading a new top level document,
+ // so we don't need to reload anymore and should receive
+ // new node events.
+ $("#reload-notice").hidden = true;
+ $("#waiting-notice").hidden = false;
+ }
+
+ // Clear out stored audio nodes
+ gAudioNodes.reset();
+
+ window.emit(EVENTS.UI_RESET);
+ break;
+ }
+ case "navigate": {
+ // TODO Case of bfcache, needs investigating
+ // bug 994250
+ break;
+ }
+ }
+ }),
+
+ /**
+ * Called after the first audio node is created in an audio context,
+ * signaling that the audio context is being used.
+ */
+ _onStartContext: function () {
+ $("#reload-notice").hidden = true;
+ $("#waiting-notice").hidden = true;
+ $("#content").hidden = false;
+ window.emit(EVENTS.START_CONTEXT);
+ },
+
+ /**
+ * Called when a new node is created. Creates an `AudioNodeView` instance
+ * for tracking throughout the editor.
+ */
+ _onCreateNode: function (nodeActor) {
+ gAudioNodes.add(nodeActor);
+ },
+
+ /**
+ * Called on `destroy-node` when an AudioNode is GC'd. Removes
+ * from the AudioNode array and fires an event indicating the removal.
+ */
+ _onDestroyNode: function (nodeActor) {
+ gAudioNodes.remove(gAudioNodes.get(nodeActor.actorID));
+ },
+
+ /**
+ * Called when a node is connected to another node.
+ */
+ _onConnectNode: Task.async(function* ({ source: sourceActor, dest: destActor }) {
+ let source = yield WebAudioEditorController.getNode(sourceActor);
+ let dest = yield WebAudioEditorController.getNode(destActor);
+ source.connect(dest);
+ }),
+
+ /**
+ * Called when a node is conneceted to another node's AudioParam.
+ */
+ _onConnectParam: Task.async(function* ({ source: sourceActor, dest: destActor, param }) {
+ let source = yield WebAudioEditorController.getNode(sourceActor);
+ let dest = yield WebAudioEditorController.getNode(destActor);
+ source.connect(dest, param);
+ }),
+
+ /**
+ * Called when a node is disconnected.
+ */
+ _onDisconnectNode: Task.async(function* (nodeActor) {
+ let node = yield WebAudioEditorController.getNode(nodeActor);
+ node.disconnect();
+ }),
+
+ /**
+ * Called when a node param is changed.
+ */
+ _onChangeParam: Task.async(function* ({ actor, param, value }) {
+ let node = yield WebAudioEditorController.getNode(actor);
+ window.emit(EVENTS.CHANGE_PARAM, node, param, value);
+ })
+};
diff --git a/devtools/client/webaudioeditor/includes.js b/devtools/client/webaudioeditor/includes.js
new file mode 100644
index 000000000..c0b727800
--- /dev/null
+++ b/devtools/client/webaudioeditor/includes.js
@@ -0,0 +1,110 @@
+/* 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 { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+const { loader, require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
+const { EventTarget } = require("sdk/event/target");
+const { Task } = require("devtools/shared/task");
+const { Class } = require("sdk/core/heritage");
+const EventEmitter = require("devtools/shared/event-emitter");
+const DevToolsUtils = require("devtools/shared/DevToolsUtils");
+const Services = require("Services");
+const { gDevTools } = require("devtools/client/framework/devtools");
+const { LocalizationHelper } = require("devtools/shared/l10n");
+const { ViewHelpers } = require("devtools/client/shared/widgets/view-helpers");
+
+const STRINGS_URI = "devtools/client/locales/webaudioeditor.properties";
+const L10N = new LocalizationHelper(STRINGS_URI);
+
+loader.lazyRequireGetter(this, "LineGraphWidget",
+ "devtools/client/shared/widgets/LineGraphWidget");
+
+// `AUDIO_NODE_DEFINITION` defined in the controller's initialization,
+// which describes all the properties of an AudioNode
+var AUDIO_NODE_DEFINITION;
+
+// Override DOM promises with Promise.jsm helpers
+const { defer, all } = require("promise");
+
+/* Events fired on `window` to indicate state or actions*/
+const EVENTS = {
+ // Fired when the first AudioNode has been created, signifying
+ // that the AudioContext is being used and should be tracked via the editor.
+ START_CONTEXT: "WebAudioEditor:StartContext",
+
+ // When the devtools theme changes.
+ THEME_CHANGE: "WebAudioEditor:ThemeChange",
+
+ // When the UI is reset from tab navigation.
+ UI_RESET: "WebAudioEditor:UIReset",
+
+ // When a param has been changed via the UI and successfully
+ // pushed via the actor to the raw audio node.
+ UI_SET_PARAM: "WebAudioEditor:UISetParam",
+
+ // When a node is to be set in the InspectorView.
+ UI_SELECT_NODE: "WebAudioEditor:UISelectNode",
+
+ // When the inspector is finished setting a new node.
+ UI_INSPECTOR_NODE_SET: "WebAudioEditor:UIInspectorNodeSet",
+
+ // When the inspector is finished rendering in or out of view.
+ UI_INSPECTOR_TOGGLED: "WebAudioEditor:UIInspectorToggled",
+
+ // When an audio node is finished loading in the Properties tab.
+ UI_PROPERTIES_TAB_RENDERED: "WebAudioEditor:UIPropertiesTabRendered",
+
+ // When an audio node is finished loading in the Automation tab.
+ UI_AUTOMATION_TAB_RENDERED: "WebAudioEditor:UIAutomationTabRendered",
+
+ // When the Audio Context graph finishes rendering.
+ // Is called with two arguments, first representing number of nodes
+ // rendered, second being the number of edge connections rendering (not counting
+ // param edges), followed by the count of the param edges rendered.
+ UI_GRAPH_RENDERED: "WebAudioEditor:UIGraphRendered",
+
+ // Called when the inspector splitter is moved and resized.
+ UI_INSPECTOR_RESIZE: "WebAudioEditor:UIInspectorResize"
+};
+XPCOMUtils.defineConstant(this, "EVENTS", EVENTS);
+
+/**
+ * The current target and the Web Audio Editor front, set by this tool's host.
+ */
+var gToolbox, gTarget, gFront;
+
+/**
+ * Convenient way of emitting events from the panel window.
+ */
+EventEmitter.decorate(this);
+
+/**
+ * DOM query helper.
+ */
+function $(selector, target = document) { return target.querySelector(selector); }
+function $$(selector, target = document) { return target.querySelectorAll(selector); }
+
+/**
+ * Takes an iterable collection, and a hash. Return the first
+ * object in the collection that matches the values in the hash.
+ * From Backbone.Collection#findWhere
+ * http://backbonejs.org/#Collection-findWhere
+ */
+function findWhere(collection, attrs) {
+ let keys = Object.keys(attrs);
+ for (let model of collection) {
+ if (keys.every(key => model[key] === attrs[key])) {
+ return model;
+ }
+ }
+ return void 0;
+}
+
+function mixin(source, ...args) {
+ args.forEach(obj => Object.keys(obj).forEach(prop => source[prop] = obj[prop]));
+ return source;
+}
diff --git a/devtools/client/webaudioeditor/models.js b/devtools/client/webaudioeditor/models.js
new file mode 100644
index 000000000..b4659d8ce
--- /dev/null
+++ b/devtools/client/webaudioeditor/models.js
@@ -0,0 +1,288 @@
+/* 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";
+
+// Import as different name `coreEmit`, so we don't conflict
+// with the global `window` listener itself.
+const { emit: coreEmit } = require("sdk/event/core");
+
+/**
+ * Representational wrapper around AudioNodeActors. Adding and destroying
+ * AudioNodes should be performed through the AudioNodes collection.
+ *
+ * Events:
+ * - `connect`: node, destinationNode, parameter
+ * - `disconnect`: node
+ */
+const AudioNodeModel = Class({
+ extends: EventTarget,
+
+ // Will be added via AudioNodes `add`
+ collection: null,
+
+ initialize: function (actor) {
+ this.actor = actor;
+ this.id = actor.actorID;
+ this.type = actor.type;
+ this.bypassable = actor.bypassable;
+ this._bypassed = false;
+ this.connections = [];
+ },
+
+ /**
+ * Stores connection data inside this instance of this audio node connecting
+ * to another node (destination). If connecting to another node's AudioParam,
+ * the second argument (param) must be populated with a string.
+ *
+ * Connecting nodes is idempotent. Upon new connection, emits "connect" event.
+ *
+ * @param AudioNodeModel destination
+ * @param String param
+ */
+ connect: function (destination, param) {
+ let edge = findWhere(this.connections, { destination: destination.id, param: param });
+
+ if (!edge) {
+ this.connections.push({ source: this.id, destination: destination.id, param: param });
+ coreEmit(this, "connect", this, destination, param);
+ }
+ },
+
+ /**
+ * Clears out all internal connection data. Emits "disconnect" event.
+ */
+ disconnect: function () {
+ this.connections.length = 0;
+ coreEmit(this, "disconnect", this);
+ },
+
+ /**
+ * Gets the bypass status of the audio node.
+ *
+ * @return Boolean
+ */
+ isBypassed: function () {
+ return this._bypassed;
+ },
+
+ /**
+ * Sets the bypass value of an AudioNode.
+ *
+ * @param Boolean enable
+ * @return Promise
+ */
+ bypass: function (enable) {
+ this._bypassed = enable;
+ return this.actor.bypass(enable).then(() => coreEmit(this, "bypass", this, enable));
+ },
+
+ /**
+ * Returns a promise that resolves to an array of objects containing
+ * both a `param` name property and a `value` property.
+ *
+ * @return Promise->Object
+ */
+ getParams: function () {
+ return this.actor.getParams();
+ },
+
+ /**
+ * Returns a promise that resolves to an object containing an
+ * array of event information and an array of automation data.
+ *
+ * @param String paramName
+ * @return Promise->Array
+ */
+ getAutomationData: function (paramName) {
+ return this.actor.getAutomationData(paramName);
+ },
+
+ /**
+ * Takes a `dagreD3.Digraph` object and adds this node to
+ * the graph to be rendered.
+ *
+ * @param dagreD3.Digraph
+ */
+ addToGraph: function (graph) {
+ graph.addNode(this.id, {
+ type: this.type,
+ label: this.type.replace(/Node$/, ""),
+ id: this.id,
+ bypassed: this._bypassed
+ });
+ },
+
+ /**
+ * Takes a `dagreD3.Digraph` object and adds edges to
+ * the graph to be rendered. Separate from `addToGraph`,
+ * as while we depend on D3/Dagre's constraints, we cannot
+ * add edges for nodes that have not yet been added to the graph.
+ *
+ * @param dagreD3.Digraph
+ */
+ addEdgesToGraph: function (graph) {
+ for (let edge of this.connections) {
+ let options = {
+ source: this.id,
+ target: edge.destination
+ };
+
+ // Only add `label` if `param` specified, as this is an AudioParam
+ // connection then. `label` adds the magic to render with dagre-d3,
+ // and `param` is just more explicitly the param, ignoring
+ // implementation details.
+ if (edge.param) {
+ options.label = options.param = edge.param;
+ }
+
+ graph.addEdge(null, this.id, edge.destination, options);
+ }
+ },
+
+ toString: () => "[object AudioNodeModel]",
+});
+
+
+/**
+ * Constructor for a Collection of `AudioNodeModel` models.
+ *
+ * Events:
+ * - `add`: node
+ * - `remove`: node
+ * - `connect`: node, destinationNode, parameter
+ * - `disconnect`: node
+ */
+const AudioNodesCollection = Class({
+ extends: EventTarget,
+
+ model: AudioNodeModel,
+
+ initialize: function () {
+ this.models = new Set();
+ this._onModelEvent = this._onModelEvent.bind(this);
+ },
+
+ /**
+ * Iterates over all models within the collection, calling `fn` with the
+ * model as the first argument.
+ *
+ * @param Function fn
+ */
+ forEach: function (fn) {
+ this.models.forEach(fn);
+ },
+
+ /**
+ * Creates a new AudioNodeModel, passing through arguments into the AudioNodeModel
+ * constructor, and adds the model to the internal collection store of this
+ * instance.
+ *
+ * Emits "add" event on instance when completed.
+ *
+ * @param Object obj
+ * @return AudioNodeModel
+ */
+ add: function (obj) {
+ let node = new this.model(obj);
+ node.collection = this;
+
+ this.models.add(node);
+
+ node.on("*", this._onModelEvent);
+ coreEmit(this, "add", node);
+ return node;
+ },
+
+ /**
+ * Removes an AudioNodeModel from the internal collection. Calls `delete` method
+ * on the model, and emits "remove" on this instance.
+ *
+ * @param AudioNodeModel node
+ */
+ remove: function (node) {
+ this.models.delete(node);
+ coreEmit(this, "remove", node);
+ },
+
+ /**
+ * Empties out the internal collection of all AudioNodeModels.
+ */
+ reset: function () {
+ this.models.clear();
+ },
+
+ /**
+ * Takes an `id` from an AudioNodeModel and returns the corresponding
+ * AudioNodeModel within the collection that matches that id. Returns `null`
+ * if not found.
+ *
+ * @param Number id
+ * @return AudioNodeModel|null
+ */
+ get: function (id) {
+ return findWhere(this.models, { id: id });
+ },
+
+ /**
+ * Returns the count for how many models are a part of this collection.
+ *
+ * @return Number
+ */
+ get length() {
+ return this.models.size;
+ },
+
+ /**
+ * Returns detailed information about the collection. used during tests
+ * to query state. Returns an object with information on node count,
+ * how many edges are within the data graph, as well as how many of those edges
+ * are for AudioParams.
+ *
+ * @return Object
+ */
+ getInfo: function () {
+ let info = {
+ nodes: this.length,
+ edges: 0,
+ paramEdges: 0
+ };
+
+ this.models.forEach(node => {
+ let paramEdgeCount = node.connections.filter(edge => edge.param).length;
+ info.edges += node.connections.length - paramEdgeCount;
+ info.paramEdges += paramEdgeCount;
+ });
+ return info;
+ },
+
+ /**
+ * Adds all nodes within the collection to the passed in graph,
+ * as well as their corresponding edges.
+ *
+ * @param dagreD3.Digraph
+ */
+ populateGraph: function (graph) {
+ this.models.forEach(node => node.addToGraph(graph));
+ this.models.forEach(node => node.addEdgesToGraph(graph));
+ },
+
+ /**
+ * Called when a stored model emits any event. Used to manage
+ * event propagation, or listening to model events to react, like
+ * removing a model from the collection when it's destroyed.
+ */
+ _onModelEvent: function (eventName, node, ...args) {
+ if (eventName === "remove") {
+ // If a `remove` event from the model, remove it
+ // from the collection, and let the method handle the emitting on
+ // the collection
+ this.remove(node);
+ } else {
+ // Pipe the event to the collection
+ coreEmit(this, eventName, node, ...args);
+ }
+ },
+
+ toString: () => "[object AudioNodeCollection]",
+});
diff --git a/devtools/client/webaudioeditor/moz.build b/devtools/client/webaudioeditor/moz.build
new file mode 100644
index 000000000..684fabc22
--- /dev/null
+++ b/devtools/client/webaudioeditor/moz.build
@@ -0,0 +1,10 @@
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'panel.js'
+)
+
+BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
diff --git a/devtools/client/webaudioeditor/panel.js b/devtools/client/webaudioeditor/panel.js
new file mode 100644
index 000000000..86a44c595
--- /dev/null
+++ b/devtools/client/webaudioeditor/panel.js
@@ -0,0 +1,71 @@
+/* -*- 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 { Cc, Ci, Cu, Cr } = require("chrome");
+const EventEmitter = require("devtools/shared/event-emitter");
+const { WebAudioFront } = require("devtools/shared/fronts/webaudio");
+var Promise = require("promise");
+
+function WebAudioEditorPanel(iframeWindow, toolbox) {
+ this.panelWin = iframeWindow;
+ this._toolbox = toolbox;
+ this._destroyer = null;
+
+ EventEmitter.decorate(this);
+}
+
+exports.WebAudioEditorPanel = WebAudioEditorPanel;
+
+WebAudioEditorPanel.prototype = {
+ open: function () {
+ let targetPromise;
+
+ // Local debugging needs to make the target remote.
+ if (!this.target.isRemote) {
+ targetPromise = this.target.makeRemote();
+ } else {
+ targetPromise = Promise.resolve(this.target);
+ }
+
+ return targetPromise
+ .then(() => {
+ this.panelWin.gToolbox = this._toolbox;
+ this.panelWin.gTarget = this.target;
+
+ this.panelWin.gFront = new WebAudioFront(this.target.client, this.target.form);
+ return this.panelWin.startupWebAudioEditor();
+ })
+ .then(() => {
+ this.isReady = true;
+ this.emit("ready");
+ return this;
+ })
+ .then(null, function onError(aReason) {
+ console.error("WebAudioEditorPanel open failed. " +
+ aReason.error + ": " + aReason.message);
+ });
+ },
+
+ // DevToolPanel API
+
+ get target() {
+ return this._toolbox.target;
+ },
+
+ destroy: function () {
+ // Make sure this panel is not already destroyed.
+ if (this._destroyer) {
+ return this._destroyer;
+ }
+
+ return this._destroyer = this.panelWin.shutdownWebAudioEditor().then(() => {
+ // Destroy front to ensure packet handler is removed from client
+ this.panelWin.gFront.destroy();
+ this.emit("destroyed");
+ });
+ }
+};
diff --git a/devtools/client/webaudioeditor/test/.eslintrc.js b/devtools/client/webaudioeditor/test/.eslintrc.js
new file mode 100644
index 000000000..8d15a76d9
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../../.eslintrc.mochitests.js"
+};
diff --git a/devtools/client/webaudioeditor/test/440hz_sine.ogg b/devtools/client/webaudioeditor/test/440hz_sine.ogg
new file mode 100644
index 000000000..bd84564e2
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/440hz_sine.ogg
Binary files differ
diff --git a/devtools/client/webaudioeditor/test/browser.ini b/devtools/client/webaudioeditor/test/browser.ini
new file mode 100644
index 000000000..cad17a530
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/browser.ini
@@ -0,0 +1,77 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ doc_simple-context.html
+ doc_complex-context.html
+ doc_simple-node-creation.html
+ doc_buffer-and-array.html
+ doc_media-node-creation.html
+ doc_destroy-nodes.html
+ doc_connect-param.html
+ doc_connect-multi-param.html
+ doc_iframe-context.html
+ doc_automation.html
+ doc_bug_1112378.html
+ doc_bug_1125817.html
+ doc_bug_1130901.html
+ doc_bug_1141261.html
+ 440hz_sine.ogg
+ head.js
+
+[browser_audionode-actor-get-param-flags.js]
+[browser_audionode-actor-get-params-01.js]
+[browser_audionode-actor-get-params-02.js]
+[browser_audionode-actor-get-set-param.js]
+[browser_audionode-actor-type.js]
+[browser_audionode-actor-source.js]
+[browser_audionode-actor-bypass.js]
+[browser_audionode-actor-bypassable.js]
+[browser_audionode-actor-connectnode-disconnect.js]
+[browser_audionode-actor-connectparam.js]
+skip-if = true # bug 1092571
+# [browser_audionode-actor-add-automation-event.js] bug 1134036
+# [browser_audionode-actor-get-automation-data-01.js] bug 1134036
+# [browser_audionode-actor-get-automation-data-02.js] bug 1134036
+# [browser_audionode-actor-get-automation-data-03.js] bug 1134036
+[browser_callwatcher-01.js]
+[browser_callwatcher-02.js]
+[browser_webaudio-actor-simple.js]
+[browser_webaudio-actor-destroy-node.js]
+[browser_webaudio-actor-connect-param.js]
+# [browser_webaudio-actor-automation-event.js] bug 1134036
+
+# [browser_wa_automation-view-01.js] bug 1134036
+# [browser_wa_automation-view-02.js] bug 1134036
+[browser_wa_controller-01.js]
+[browser_wa_destroy-node-01.js]
+[browser_wa_first-run.js]
+[browser_wa_graph-click.js]
+[browser_wa_graph-markers.js]
+[browser_wa_graph-render-01.js]
+[browser_wa_graph-render-02.js]
+[browser_wa_graph-render-03.js]
+[browser_wa_graph-render-04.js]
+[browser_wa_graph-render-05.js]
+skip-if = true # bug 1092571
+[browser_wa_graph-render-06.js]
+[browser_wa_graph-selected.js]
+[browser_wa_graph-zoom.js]
+[browser_wa_inspector.js]
+[browser_wa_inspector-toggle.js]
+[browser_wa_inspector-width.js]
+[browser_wa_inspector-bypass-01.js]
+[browser_wa_navigate.js]
+[browser_wa_properties-view.js]
+[browser_wa_properties-view-edit-01.js]
+skip-if = true # bug 1010423
+[browser_wa_properties-view-edit-02.js]
+skip-if = true # bug 1010423
+[browser_wa_properties-view-media-nodes.js]
+skip-if = os == 'mac' # bug 1216542
+[browser_wa_properties-view-params.js]
+[browser_wa_properties-view-params-objects.js]
+[browser_wa_reset-01.js]
+[browser_wa_reset-02.js]
+[browser_wa_reset-03.js]
+[browser_wa_reset-04.js]
diff --git a/devtools/client/webaudioeditor/test/browser_audionode-actor-add-automation-event.js b/devtools/client/webaudioeditor/test/browser_audionode-actor-add-automation-event.js
new file mode 100644
index 000000000..4b451c826
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/browser_audionode-actor-add-automation-event.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test AudioNode#addAutomationEvent();
+ */
+
+add_task(function* () {
+ let { target, front } = yield initBackend(SIMPLE_CONTEXT_URL);
+ let [_, [destNode, oscNode, gainNode]] = yield Promise.all([
+ front.setup({ reload: true }),
+ get3(front, "create-node")
+ ]);
+ let count = 0;
+ let counter = () => count++;
+ front.on("automation-event", counter);
+
+ let t0 = 0, t1 = 0.1, t2 = 0.2, t3 = 0.3, t4 = 0.4, t5 = 0.6, t6 = 0.7, t7 = 1;
+ let curve = [-1, 0, 1];
+ yield oscNode.addAutomationEvent("frequency", "setValueAtTime", [0.2, t0]);
+ yield oscNode.addAutomationEvent("frequency", "setValueAtTime", [0.3, t1]);
+ yield oscNode.addAutomationEvent("frequency", "setValueAtTime", [0.4, t2]);
+ yield oscNode.addAutomationEvent("frequency", "linearRampToValueAtTime", [1, t3]);
+ yield oscNode.addAutomationEvent("frequency", "linearRampToValueAtTime", [0.15, t4]);
+ yield oscNode.addAutomationEvent("frequency", "exponentialRampToValueAtTime", [0.75, t5]);
+ yield oscNode.addAutomationEvent("frequency", "exponentialRampToValueAtTime", [0.5, t6]);
+ yield oscNode.addAutomationEvent("frequency", "setValueCurveAtTime", [curve, t7, t7 - t6]);
+ yield oscNode.addAutomationEvent("frequency", "setTargetAtTime", [20, 2, 5]);
+
+ ok(true, "successfully set automation events for valid automation events");
+
+ try {
+ yield oscNode.addAutomationEvent("frequency", "notAMethod", 20, 2, 5);
+ ok(false, "non-automation methods should not be successful");
+ } catch (e) {
+ ok(/invalid/.test(e.message), "AudioNode:addAutomationEvent fails for invalid automation methods");
+ }
+
+ try {
+ yield oscNode.addAutomationEvent("invalidparam", "setValueAtTime", 0.2, t0);
+ ok(false, "automating non-AudioParams should not be successful");
+ } catch (e) {
+ ok(/invalid/.test(e.message), "AudioNode:addAutomationEvent fails for a non AudioParam");
+ }
+
+ front.off("automation-event", counter);
+
+ is(count, 9,
+ "when calling `addAutomationEvent`, the WebAudioActor should still fire `automation-event`.");
+
+ yield removeTab(target.tab);
+});
diff --git a/devtools/client/webaudioeditor/test/browser_audionode-actor-bypass.js b/devtools/client/webaudioeditor/test/browser_audionode-actor-bypass.js
new file mode 100644
index 000000000..e9fc7f321
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/browser_audionode-actor-bypass.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test AudioNode#bypass(), AudioNode#isBypassed()
+ */
+
+add_task(function* () {
+ let { target, front } = yield initBackend(SIMPLE_CONTEXT_URL);
+ let [_, [destNode, oscNode, gainNode]] = yield Promise.all([
+ front.setup({ reload: true }),
+ get3(front, "create-node")
+ ]);
+
+ is((yield gainNode.isBypassed()), false, "Nodes start off unbypassed.");
+
+ info("Calling node#bypass(true)");
+ let isBypassed = yield gainNode.bypass(true);
+
+ is(isBypassed, true, "node.bypass(true) resolves to true");
+ is((yield gainNode.isBypassed()), true, "Node is now bypassed.");
+
+ info("Calling node#bypass(false)");
+ isBypassed = yield gainNode.bypass(false);
+
+ is(isBypassed, false, "node.bypass(false) resolves to false");
+ is((yield gainNode.isBypassed()), false, "Node back to being unbypassed.");
+
+ info("Calling node#bypass(true) on unbypassable node");
+ isBypassed = yield destNode.bypass(true);
+
+ is(isBypassed, false, "node.bypass(true) resolves to false for unbypassable node");
+ is((yield gainNode.isBypassed()), false, "Unbypassable node is unaffect");
+
+ yield removeTab(target.tab);
+});
diff --git a/devtools/client/webaudioeditor/test/browser_audionode-actor-bypassable.js b/devtools/client/webaudioeditor/test/browser_audionode-actor-bypassable.js
new file mode 100644
index 000000000..2f093f2e4
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/browser_audionode-actor-bypassable.js
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test AudioNode#bypassable
+ */
+
+add_task(function* () {
+ let { target, front } = yield initBackend(SIMPLE_NODES_URL);
+ let [_, nodes] = yield Promise.all([
+ front.setup({ reload: true }),
+ getN(front, "create-node", 14)
+ ]);
+
+ let actualBypassability = nodes.map(node => node.bypassable);
+ let expectedBypassability = [
+ false, // AudioDestinationNode
+ true, // AudioBufferSourceNode
+ true, // ScriptProcessorNode
+ true, // AnalyserNode
+ true, // GainNode
+ true, // DelayNode
+ true, // BiquadFilterNode
+ true, // WaveShaperNode
+ true, // PannerNode
+ true, // ConvolverNode
+ false, // ChannelSplitterNode
+ false, // ChannelMergerNode
+ true, // DynamicsCompressNode
+ true, // OscillatorNode
+ ];
+
+ expectedBypassability.forEach((bypassable, i) => {
+ is(actualBypassability[i], bypassable, `${nodes[i].type} has correct ".bypassable" status`);
+ });
+
+ yield removeTab(target.tab);
+});
diff --git a/devtools/client/webaudioeditor/test/browser_audionode-actor-connectnode-disconnect.js b/devtools/client/webaudioeditor/test/browser_audionode-actor-connectnode-disconnect.js
new file mode 100644
index 000000000..dcd1689f5
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/browser_audionode-actor-connectnode-disconnect.js
@@ -0,0 +1,39 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that AudioNodeActor#connectNode() and AudioNodeActor#disconnect() work.
+ * Uses the editor front as the actors do not retain connect state.
+ */
+
+add_task(function* () {
+ let { target, panel } = yield initWebAudioEditor(SIMPLE_CONTEXT_URL);
+ let { panelWin } = panel;
+ let { gFront, $, $$, EVENTS, gAudioNodes } = panelWin;
+
+ let events = Promise.all([
+ get3(gFront, "create-node"),
+ waitForGraphRendered(panelWin, 3, 2)
+ ]);
+ reload(target);
+ let [actors] = yield events;
+ let [dest, osc, gain] = actors;
+
+ info("Disconnecting oscillator...");
+ osc.disconnect();
+ yield Promise.all([
+ waitForGraphRendered(panelWin, 3, 1),
+ once(gAudioNodes, "disconnect")
+ ]);
+ ok(true, "Oscillator disconnected, event emitted.");
+
+ info("Reconnecting oscillator...");
+ osc.connectNode(gain);
+ yield Promise.all([
+ waitForGraphRendered(panelWin, 3, 2),
+ once(gAudioNodes, "connect")
+ ]);
+ ok(true, "Oscillator reconnected.");
+
+ yield teardown(target);
+});
diff --git a/devtools/client/webaudioeditor/test/browser_audionode-actor-connectparam.js b/devtools/client/webaudioeditor/test/browser_audionode-actor-connectparam.js
new file mode 100644
index 000000000..454f0d563
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/browser_audionode-actor-connectparam.js
@@ -0,0 +1,32 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that AudioNodeActor#connectParam() work.
+ * Uses the editor front as the actors do not retain connect state.
+ */
+
+add_task(function* () {
+ let { target, panel } = yield initWebAudioEditor(SIMPLE_CONTEXT_URL);
+ let { panelWin } = panel;
+ let { gFront, $, $$, EVENTS, gAudioNodes } = panelWin;
+
+ let events = Promise.all([
+ get3(gFront, "create-node"),
+ waitForGraphRendered(panelWin, 3, 2)
+ ]);
+ reload(target);
+ let [actors] = yield events;
+ let [dest, osc, gain] = actors;
+
+ yield osc.disconnect();
+
+ osc.connectParam(gain, "gain");
+ yield Promise.all([
+ waitForGraphRendered(panelWin, 3, 1, 1),
+ once(gAudioNodes, "connect")
+ ]);
+ ok(true, "Oscillator connect to Gain's Gain AudioParam, event emitted.");
+
+ yield teardown(target);
+});
diff --git a/devtools/client/webaudioeditor/test/browser_audionode-actor-get-automation-data-01.js b/devtools/client/webaudioeditor/test/browser_audionode-actor-get-automation-data-01.js
new file mode 100644
index 000000000..640b3e351
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/browser_audionode-actor-get-automation-data-01.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test AudioNode#addAutomationEvent() checking automation values, also using
+ * a curve as the last event to check duration spread.
+ */
+
+add_task(function* () {
+ let { target, front } = yield initBackend(SIMPLE_CONTEXT_URL);
+ let [_, [destNode, oscNode, gainNode]] = yield Promise.all([
+ front.setup({ reload: true }),
+ get3(front, "create-node")
+ ]);
+
+ let t0 = 0, t1 = 0.1, t2 = 0.2, t3 = 0.3, t4 = 0.4, t5 = 0.6, t6 = 0.7, t7 = 1;
+ let curve = [-1, 0, 1];
+ yield oscNode.addAutomationEvent("frequency", "setValueAtTime", [0.2, t0]);
+ yield oscNode.addAutomationEvent("frequency", "setValueAtTime", [0.3, t1]);
+ yield oscNode.addAutomationEvent("frequency", "setValueAtTime", [0.4, t2]);
+ yield oscNode.addAutomationEvent("frequency", "linearRampToValueAtTime", [1, t3]);
+ yield oscNode.addAutomationEvent("frequency", "linearRampToValueAtTime", [0.15, t4]);
+ yield oscNode.addAutomationEvent("frequency", "exponentialRampToValueAtTime", [0.75, t5]);
+ yield oscNode.addAutomationEvent("frequency", "exponentialRampToValueAtTime", [0.05, t6]);
+ // End with a curve here so we can get proper results on the last event (which takes into account
+ // duration)
+ yield oscNode.addAutomationEvent("frequency", "setValueCurveAtTime", [curve, t6, t7 - t6]);
+
+ let { events, values } = yield oscNode.getAutomationData("frequency");
+
+ is(events.length, 8, "8 recorded events returned.");
+ is(values.length, 2000, "2000 value points returned.");
+
+ checkAutomationValue(values, 0.05, 0.2);
+ checkAutomationValue(values, 0.1, 0.3);
+ checkAutomationValue(values, 0.15, 0.3);
+ checkAutomationValue(values, 0.2, 0.4);
+ checkAutomationValue(values, 0.25, 0.7);
+ checkAutomationValue(values, 0.3, 1);
+ checkAutomationValue(values, 0.35, 0.575);
+ checkAutomationValue(values, 0.4, 0.15);
+ checkAutomationValue(values, 0.45, 0.15 * Math.pow(0.75 / 0.15, 0.05 / 0.2));
+ checkAutomationValue(values, 0.5, 0.15 * Math.pow(0.75 / 0.15, 0.5));
+ checkAutomationValue(values, 0.55, 0.15 * Math.pow(0.75 / 0.15, 0.15 / 0.2));
+ checkAutomationValue(values, 0.6, 0.75);
+ checkAutomationValue(values, 0.65, 0.75 * Math.pow(0.05 / 0.75, 0.5));
+ checkAutomationValue(values, 0.705, -1); // Increase this time a bit to prevent off by the previous exponential amount
+ checkAutomationValue(values, 0.8, 0);
+ checkAutomationValue(values, 0.9, 1);
+ checkAutomationValue(values, 1, 1);
+
+ yield removeTab(target.tab);
+});
diff --git a/devtools/client/webaudioeditor/test/browser_audionode-actor-get-automation-data-02.js b/devtools/client/webaudioeditor/test/browser_audionode-actor-get-automation-data-02.js
new file mode 100644
index 000000000..f24fb1905
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/browser_audionode-actor-get-automation-data-02.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test AudioNode#addAutomationEvent() when automation series ends with
+ * `setTargetAtTime`, which approaches its target to infinity.
+ */
+
+add_task(function* () {
+ let { target, front } = yield initBackend(SIMPLE_CONTEXT_URL);
+ let [_, [destNode, oscNode, gainNode]] = yield Promise.all([
+ front.setup({ reload: true }),
+ get3(front, "create-node")
+ ]);
+
+ yield oscNode.addAutomationEvent("frequency", "setValueAtTime", [300, 0.1]);
+ yield oscNode.addAutomationEvent("frequency", "linearRampToValueAtTime", [500, 0.4]);
+ yield oscNode.addAutomationEvent("frequency", "exponentialRampToValueAtTime", [200, 0.6]);
+ // End with a setTargetAtTime event, as the target approaches infinity, which will
+ // give us more points to render than the default 2000
+ yield oscNode.addAutomationEvent("frequency", "setTargetAtTime", [1000, 2, 0.5]);
+
+ var { events, values } = yield oscNode.getAutomationData("frequency");
+
+ is(events.length, 4, "4 recorded events returned.");
+ is(values.length, 4000, "4000 value points returned when ending with exponentiall approaching automator.");
+
+ checkAutomationValue(values, 2.01, 215.055);
+ checkAutomationValue(values, 2.1, 345.930);
+ checkAutomationValue(values, 3, 891.601);
+ checkAutomationValue(values, 5, 998.01);
+
+ // Refetch the automation data to ensure it recalculates correctly (bug 1118071)
+ var { events, values } = yield oscNode.getAutomationData("frequency");
+
+ checkAutomationValue(values, 2.01, 215.055);
+ checkAutomationValue(values, 2.1, 345.930);
+ checkAutomationValue(values, 3, 891.601);
+ checkAutomationValue(values, 5, 998.01);
+
+ yield removeTab(target.tab);
+});
diff --git a/devtools/client/webaudioeditor/test/browser_audionode-actor-get-automation-data-03.js b/devtools/client/webaudioeditor/test/browser_audionode-actor-get-automation-data-03.js
new file mode 100644
index 000000000..7de509ccd
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/browser_audionode-actor-get-automation-data-03.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test that `cancelScheduledEvents` clears out events on and after
+ * its argument.
+ */
+
+add_task(function* () {
+ let { target, front } = yield initBackend(SIMPLE_CONTEXT_URL);
+ let [_, [destNode, oscNode, gainNode]] = yield Promise.all([
+ front.setup({ reload: true }),
+ get3(front, "create-node")
+ ]);
+
+ yield oscNode.addAutomationEvent("frequency", "setValueAtTime", [300, 0]);
+ yield oscNode.addAutomationEvent("frequency", "linearRampToValueAtTime", [500, 0.9]);
+ yield oscNode.addAutomationEvent("frequency", "setValueAtTime", [700, 1]);
+ yield oscNode.addAutomationEvent("frequency", "exponentialRampToValueAtTime", [1000, 2]);
+ yield oscNode.addAutomationEvent("frequency", "cancelScheduledValues", [1]);
+
+ var { events, values } = yield oscNode.getAutomationData("frequency");
+
+ is(events.length, 2, "2 recorded events returned.");
+ is(values.length, 2000, "2000 value points returned");
+
+ checkAutomationValue(values, 0, 300);
+ checkAutomationValue(values, 0.5, 411.15);
+ checkAutomationValue(values, 0.9, 499.9);
+ checkAutomationValue(values, 1, 499.9);
+ checkAutomationValue(values, 2, 499.9);
+
+ yield removeTab(target.tab);
+});
diff --git a/devtools/client/webaudioeditor/test/browser_audionode-actor-get-param-flags.js b/devtools/client/webaudioeditor/test/browser_audionode-actor-get-param-flags.js
new file mode 100644
index 000000000..17f7bf846
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/browser_audionode-actor-get-param-flags.js
@@ -0,0 +1,47 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test AudioNode#getParamFlags()
+ */
+
+add_task(function* () {
+ let { target, front } = yield initBackend(SIMPLE_NODES_URL);
+ let [_, nodes] = yield Promise.all([
+ front.setup({ reload: true }),
+ getN(front, "create-node", 15)
+ ]);
+
+ let allNodeParams = yield Promise.all(nodes.map(node => node.getParams()));
+ let nodeTypes = [
+ "AudioDestinationNode",
+ "AudioBufferSourceNode", "ScriptProcessorNode", "AnalyserNode", "GainNode",
+ "DelayNode", "BiquadFilterNode", "WaveShaperNode", "PannerNode", "ConvolverNode",
+ "ChannelSplitterNode", "ChannelMergerNode", "DynamicsCompressorNode", "OscillatorNode",
+ "StereoPannerNode"
+ ];
+
+ // For some reason nodeTypes.forEach and params.forEach fail here so we use
+ // simple for loops.
+ for (let i = 0; i < nodeTypes.length; i++) {
+ let type = nodeTypes[i];
+ let params = allNodeParams[i];
+
+ for (let {param, value, flags} of params) {
+ let testFlags = yield nodes[i].getParamFlags(param);
+ ok(typeof testFlags === "object", type + " has flags from #getParamFlags(" + param + ")");
+
+ if (param === "buffer") {
+ is(flags.Buffer, true, "`buffer` params have Buffer flag");
+ }
+ else if (param === "bufferSize" || param === "frequencyBinCount") {
+ is(flags.readonly, true, param + " is readonly");
+ }
+ else if (param === "curve") {
+ is(flags["Float32Array"], true, "`curve` param has Float32Array flag");
+ }
+ }
+ }
+
+ yield removeTab(target.tab);
+});
diff --git a/devtools/client/webaudioeditor/test/browser_audionode-actor-get-params-01.js b/devtools/client/webaudioeditor/test/browser_audionode-actor-get-params-01.js
new file mode 100644
index 000000000..6cfabbe85
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/browser_audionode-actor-get-params-01.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test AudioNode#getParams()
+ */
+
+add_task(function* () {
+ let { target, front } = yield initBackend(SIMPLE_NODES_URL);
+ let [_, nodes] = yield Promise.all([
+ front.setup({ reload: true }),
+ getN(front, "create-node", 15)
+ ]);
+
+ yield loadFrameScripts();
+
+ let allNodeParams = yield Promise.all(nodes.map(node => node.getParams()));
+ let nodeTypes = [
+ "AudioDestinationNode",
+ "AudioBufferSourceNode", "ScriptProcessorNode", "AnalyserNode", "GainNode",
+ "DelayNode", "BiquadFilterNode", "WaveShaperNode", "PannerNode", "ConvolverNode",
+ "ChannelSplitterNode", "ChannelMergerNode", "DynamicsCompressorNode", "OscillatorNode",
+ "StereoPannerNode"
+ ];
+
+ let defaults = yield Promise.all(nodeTypes.map(type => nodeDefaultValues(type)));
+
+ nodeTypes.map((type, i) => {
+ let params = allNodeParams[i];
+
+ params.forEach(({param, value, flags}) => {
+ ok(param in defaults[i], "expected parameter for " + type);
+
+ ok(typeof flags === "object", type + " has a flags object");
+
+ if (param === "buffer") {
+ is(flags.Buffer, true, "`buffer` params have Buffer flag");
+ }
+ else if (param === "bufferSize" || param === "frequencyBinCount") {
+ is(flags.readonly, true, param + " is readonly");
+ }
+ else if (param === "curve") {
+ is(flags["Float32Array"], true, "`curve` param has Float32Array flag");
+ }
+ });
+ });
+
+ yield removeTab(target.tab);
+});
diff --git a/devtools/client/webaudioeditor/test/browser_audionode-actor-get-params-02.js b/devtools/client/webaudioeditor/test/browser_audionode-actor-get-params-02.js
new file mode 100644
index 000000000..8d60a5e4d
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/browser_audionode-actor-get-params-02.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that default properties are returned with the correct type
+ * from the AudioNode actors.
+ */
+
+add_task(function* () {
+ let { target, front } = yield initBackend(SIMPLE_NODES_URL);
+ let [_, nodes] = yield Promise.all([
+ front.setup({ reload: true }),
+ getN(front, "create-node", 15)
+ ]);
+
+ yield loadFrameScripts();
+
+ let allParams = yield Promise.all(nodes.map(node => node.getParams()));
+ let types = [
+ "AudioDestinationNode", "AudioBufferSourceNode", "ScriptProcessorNode",
+ "AnalyserNode", "GainNode", "DelayNode", "BiquadFilterNode", "WaveShaperNode",
+ "PannerNode", "ConvolverNode", "ChannelSplitterNode", "ChannelMergerNode",
+ "DynamicsCompressorNode", "OscillatorNode", "StereoPannerNode"
+ ];
+
+ let defaults = yield Promise.all(types.map(type => nodeDefaultValues(type)));
+
+ info(JSON.stringify(defaults));
+
+ allParams.forEach((params, i) => {
+ compare(params, defaults[i], types[i]);
+ });
+
+ yield removeTab(target.tab);
+});
+
+function compare(actual, expected, type) {
+ actual.forEach(({ value, param }) => {
+ value = getGripValue(value);
+ if (typeof expected[param] === "function") {
+ ok(expected[param](value), type + " has a passing value for " + param);
+ }
+ else {
+ is(value, expected[param], type + " has correct default value and type for " + param);
+ }
+ });
+
+ info(Object.keys(expected).join(",") + " - " + JSON.stringify(expected));
+
+ is(actual.length, Object.keys(expected).length,
+ type + " has correct amount of properties.");
+}
diff --git a/devtools/client/webaudioeditor/test/browser_audionode-actor-get-set-param.js b/devtools/client/webaudioeditor/test/browser_audionode-actor-get-set-param.js
new file mode 100644
index 000000000..0d4c7c5c7
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/browser_audionode-actor-get-set-param.js
@@ -0,0 +1,47 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test AudioNode#getParam() / AudioNode#setParam()
+ */
+
+add_task(function* () {
+ let { target, front } = yield initBackend(SIMPLE_CONTEXT_URL);
+ let [_, [destNode, oscNode, gainNode]] = yield Promise.all([
+ front.setup({ reload: true }),
+ get3(front, "create-node")
+ ]);
+
+ let freq = yield oscNode.getParam("frequency");
+ info(typeof freq);
+ is(freq, 440, "AudioNode:getParam correctly fetches AudioParam");
+
+ let type = yield oscNode.getParam("type");
+ is(type, "sine", "AudioNode:getParam correctly fetches non-AudioParam");
+
+ type = yield oscNode.getParam("not-a-valid-param");
+ ok(type.type === "undefined",
+ "AudioNode:getParam correctly returns a grip value for `undefined` for an invalid param.");
+
+ let resSuccess = yield oscNode.setParam("frequency", 220);
+ freq = yield oscNode.getParam("frequency");
+ is(freq, 220, "AudioNode:setParam correctly sets a `number` AudioParam");
+ is(resSuccess, undefined, "AudioNode:setParam returns undefined for correctly set AudioParam");
+
+ resSuccess = yield oscNode.setParam("type", "square");
+ type = yield oscNode.getParam("type");
+ is(type, "square", "AudioNode:setParam correctly sets a `string` non-AudioParam");
+ is(resSuccess, undefined, "AudioNode:setParam returns undefined for correctly set AudioParam");
+
+ try {
+ yield oscNode.setParam("frequency", "hello");
+ ok(false, "setParam with invalid types should throw");
+ } catch (e) {
+ ok(/is not a finite floating-point/.test(e.message), "AudioNode:setParam returns error with correct message when attempting an invalid assignment");
+ is(e.type, "TypeError", "AudioNode:setParam returns error with correct type when attempting an invalid assignment");
+ freq = yield oscNode.getParam("frequency");
+ is(freq, 220, "AudioNode:setParam does not modify value when an error occurs");
+ }
+
+ yield removeTab(target.tab);
+});
diff --git a/devtools/client/webaudioeditor/test/browser_audionode-actor-source.js b/devtools/client/webaudioeditor/test/browser_audionode-actor-source.js
new file mode 100644
index 000000000..203e88012
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/browser_audionode-actor-source.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test AudioNode#source
+ */
+
+add_task(function* () {
+ let { target, front } = yield initBackend(SIMPLE_NODES_URL);
+ let [_, nodes] = yield Promise.all([
+ front.setup({ reload: true }),
+ getN(front, "create-node", 14)
+ ]);
+
+ let actualTypes = nodes.map(node => node.type);
+ let isSourceResult = nodes.map(node => node.source);
+
+ actualTypes.forEach((type, i) => {
+ let shouldBeSource = type === "AudioBufferSourceNode" || type === "OscillatorNode";
+ if (shouldBeSource)
+ is(isSourceResult[i], true, type + "'s `source` is `true`");
+ else
+ is(isSourceResult[i], false, type + "'s `source` is `false`");
+ });
+
+ yield removeTab(target.tab);
+});
diff --git a/devtools/client/webaudioeditor/test/browser_audionode-actor-type.js b/devtools/client/webaudioeditor/test/browser_audionode-actor-type.js
new file mode 100644
index 000000000..58712067e
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/browser_audionode-actor-type.js
@@ -0,0 +1,28 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test AudioNode#type
+ */
+
+add_task(function* () {
+ let { target, front } = yield initBackend(SIMPLE_NODES_URL);
+ let [_, nodes] = yield Promise.all([
+ front.setup({ reload: true }),
+ getN(front, "create-node", 14)
+ ]);
+
+ let actualTypes = nodes.map(node => node.type);
+ let expectedTypes = [
+ "AudioDestinationNode",
+ "AudioBufferSourceNode", "ScriptProcessorNode", "AnalyserNode", "GainNode",
+ "DelayNode", "BiquadFilterNode", "WaveShaperNode", "PannerNode", "ConvolverNode",
+ "ChannelSplitterNode", "ChannelMergerNode", "DynamicsCompressorNode", "OscillatorNode"
+ ];
+
+ expectedTypes.forEach((type, i) => {
+ is(actualTypes[i], type, type + " successfully created with correct type");
+ });
+
+ yield removeTab(target.tab);
+});
diff --git a/devtools/client/webaudioeditor/test/browser_callwatcher-01.js b/devtools/client/webaudioeditor/test/browser_callwatcher-01.js
new file mode 100644
index 000000000..11c3ad11c
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/browser_callwatcher-01.js
@@ -0,0 +1,26 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Bug 1130901
+ * Tests to ensure that calling call/apply on methods wrapped
+ * via CallWatcher do not throw a security permissions error:
+ * "Error: Permission denied to access property 'call'"
+ */
+
+const BUG_1130901_URL = EXAMPLE_URL + "doc_bug_1130901.html";
+
+add_task(function* () {
+ let { target, panel } = yield initWebAudioEditor(BUG_1130901_URL);
+ let { panelWin } = panel;
+ let { gFront, $, $$, EVENTS, gAudioNodes } = panelWin;
+
+ let rendered = waitForGraphRendered(panelWin, 3, 0);
+ reload(target);
+ yield rendered;
+
+ ok(true, "Successfully created a node from AudioContext via `call`.");
+ ok(true, "Successfully created a node from AudioContext via `apply`.");
+
+ yield teardown(target);
+});
diff --git a/devtools/client/webaudioeditor/test/browser_callwatcher-02.js b/devtools/client/webaudioeditor/test/browser_callwatcher-02.js
new file mode 100644
index 000000000..f901efdcb
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/browser_callwatcher-02.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Bug 1112378
+ * Tests to ensure that errors called on wrapped functions via call-watcher
+ * correctly looks like the error comes from the content, not from within the devtools.
+ */
+
+const BUG_1112378_URL = EXAMPLE_URL + "doc_bug_1112378.html";
+
+add_task(function* () {
+ let { target, panel } = yield initWebAudioEditor(BUG_1112378_URL);
+ let { panelWin } = panel;
+ let { gFront, $, $$, EVENTS, gAudioNodes } = panelWin;
+
+ loadFrameScripts();
+
+ let rendered = waitForGraphRendered(panelWin, 2, 0);
+ reload(target);
+ yield rendered;
+
+ let error = yield evalInDebuggee("throwError()");
+ is(error.lineNumber, 21, "error has correct lineNumber");
+ is(error.columnNumber, 11, "error has correct columnNumber");
+ is(error.name, "TypeError", "error has correct name");
+ is(error.message, "Argument 1 is not valid for any of the 2-argument overloads of AudioNode.connect.", "error has correct message");
+ is(error.stringified, "TypeError: Argument 1 is not valid for any of the 2-argument overloads of AudioNode.connect.", "error is stringified correctly");
+ is(error.instanceof, true, "error is correctly an instanceof TypeError");
+ is(error.fileName, "http://example.com/browser/devtools/client/webaudioeditor/test/doc_bug_1112378.html", "error has correct fileName");
+
+ error = yield evalInDebuggee("throwDOMException()");
+ is(error.lineNumber, 37, "exception has correct lineNumber");
+ is(error.columnNumber, 0, "exception has correct columnNumber");
+ is(error.code, 9, "exception has correct code");
+ is(error.result, 2152923145, "exception has correct result");
+ is(error.name, "NotSupportedError", "exception has correct name");
+ is(error.message, "Operation is not supported", "exception has correct message");
+ is(error.stringified, "NotSupportedError: Operation is not supported", "exception is stringified correctly");
+ is(error.instanceof, true, "exception is correctly an instance of DOMException");
+ is(error.filename, "http://example.com/browser/devtools/client/webaudioeditor/test/doc_bug_1112378.html", "exception has correct filename");
+
+ yield teardown(target);
+});
diff --git a/devtools/client/webaudioeditor/test/browser_wa_automation-view-01.js b/devtools/client/webaudioeditor/test/browser_wa_automation-view-01.js
new file mode 100644
index 000000000..1e0034b5b
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/browser_wa_automation-view-01.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that automation view shows the correct view depending on if events
+ * or params exist.
+ */
+
+add_task(function* () {
+ let { target, panel } = yield initWebAudioEditor(AUTOMATION_URL);
+ let { panelWin } = panel;
+ let { gFront, $, $$, EVENTS } = panelWin;
+
+ let started = once(gFront, "start-context");
+
+ let events = Promise.all([
+ get3(gFront, "create-node"),
+ waitForGraphRendered(panelWin, 3, 2)
+ ]);
+ reload(target);
+ let [actors] = yield events;
+ let nodeIds = actors.map(actor => actor.actorID);
+
+ let $tabbox = $("#web-audio-editor-tabs");
+
+ // Oscillator node
+ click(panelWin, findGraphNode(panelWin, nodeIds[1]));
+ yield waitForInspectorRender(panelWin, EVENTS);
+ $tabbox.selectedIndex = 1;
+
+ ok(isVisible($("#automation-graph-container")), "graph container should be visible");
+ ok(isVisible($("#automation-content")), "automation content should be visible");
+ ok(!isVisible($("#automation-no-events")), "no-events panel should not be visible");
+ ok(!isVisible($("#automation-empty")), "empty panel should not be visible");
+
+ // Gain node
+ click(panelWin, findGraphNode(panelWin, nodeIds[2]));
+ yield waitForInspectorRender(panelWin, EVENTS);
+ $tabbox.selectedIndex = 1;
+
+ ok(!isVisible($("#automation-graph-container")), "graph container should not be visible");
+ ok(isVisible($("#automation-content")), "automation content should be visible");
+ ok(isVisible($("#automation-no-events")), "no-events panel should be visible");
+ ok(!isVisible($("#automation-empty")), "empty panel should not be visible");
+
+ // destination node
+ click(panelWin, findGraphNode(panelWin, nodeIds[0]));
+ yield waitForInspectorRender(panelWin, EVENTS);
+ $tabbox.selectedIndex = 1;
+
+ ok(!isVisible($("#automation-graph-container")), "graph container should not be visible");
+ ok(!isVisible($("#automation-content")), "automation content should not be visible");
+ ok(!isVisible($("#automation-no-events")), "no-events panel should not be visible");
+ ok(isVisible($("#automation-empty")), "empty panel should be visible");
+
+ yield teardown(target);
+});
diff --git a/devtools/client/webaudioeditor/test/browser_wa_automation-view-02.js b/devtools/client/webaudioeditor/test/browser_wa_automation-view-02.js
new file mode 100644
index 000000000..a0f5f5a04
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/browser_wa_automation-view-02.js
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that automation view selects the first parameter by default and
+ * switching between AudioParam rerenders the graph.
+ */
+
+add_task(function* () {
+ let { target, panel } = yield initWebAudioEditor(AUTOMATION_URL);
+ let { panelWin } = panel;
+ let { gFront, $, $$, EVENTS, AutomationView } = panelWin;
+
+ let started = once(gFront, "start-context");
+
+ let events = Promise.all([
+ get3(gFront, "create-node"),
+ waitForGraphRendered(panelWin, 3, 2)
+ ]);
+ reload(target);
+ let [actors] = yield events;
+ let nodeIds = actors.map(actor => actor.actorID);
+
+ // Oscillator node
+ click(panelWin, findGraphNode(panelWin, nodeIds[1]));
+ yield waitForInspectorRender(panelWin, EVENTS);
+ click(panelWin, $("#automation-tab"));
+
+ ok(AutomationView._selectedParamName, "frequency",
+ "AutomatioView is set on 'frequency'");
+ ok($(".automation-param-button[data-param='frequency']").getAttribute("selected"),
+ "frequency param should be selected on load");
+ ok(!$(".automation-param-button[data-param='detune']").getAttribute("selected"),
+ "detune param should not be selected on load");
+ ok(isVisible($("#automation-content")), "automation content should be visible");
+ ok(isVisible($("#automation-graph-container")), "graph container should be visible");
+ ok(!isVisible($("#automation-no-events")), "no-events panel should not be visible");
+
+ click(panelWin, $(".automation-param-button[data-param='detune']"));
+ yield once(panelWin, EVENTS.UI_AUTOMATION_TAB_RENDERED);
+
+ ok(true, "automation tab rerendered");
+
+ ok(AutomationView._selectedParamName, "detune",
+ "AutomatioView is set on 'detune'");
+ ok(!$(".automation-param-button[data-param='frequency']").getAttribute("selected"),
+ "frequency param should not be selected after clicking detune");
+ ok($(".automation-param-button[data-param='detune']").getAttribute("selected"),
+ "detune param should be selected after clicking detune");
+ ok(isVisible($("#automation-content")), "automation content should be visible");
+ ok(!isVisible($("#automation-graph-container")), "graph container should not be visible");
+ ok(isVisible($("#automation-no-events")), "no-events panel should be visible");
+
+ yield teardown(target);
+});
diff --git a/devtools/client/webaudioeditor/test/browser_wa_controller-01.js b/devtools/client/webaudioeditor/test/browser_wa_controller-01.js
new file mode 100644
index 000000000..a7064df1f
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/browser_wa_controller-01.js
@@ -0,0 +1,28 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Bug 1125817
+ * Tests to ensure that disconnecting a node immediately
+ * after creating it does not fail.
+ */
+
+const BUG_1125817_URL = EXAMPLE_URL + "doc_bug_1125817.html";
+
+add_task(function* () {
+ let { target, panel } = yield initWebAudioEditor(BUG_1125817_URL);
+ let { panelWin } = panel;
+ let { gFront, $, $$, EVENTS, gAudioNodes } = panelWin;
+
+ let events = Promise.all([
+ once(gAudioNodes, "add", 2),
+ once(gAudioNodes, "disconnect"),
+ waitForGraphRendered(panelWin, 2, 0)
+ ]);
+ reload(target);
+ yield events;
+
+ ok(true, "Successfully disconnected a just-created node.");
+
+ yield teardown(target);
+});
diff --git a/devtools/client/webaudioeditor/test/browser_wa_destroy-node-01.js b/devtools/client/webaudioeditor/test/browser_wa_destroy-node-01.js
new file mode 100644
index 000000000..d7dde4d97
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/browser_wa_destroy-node-01.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the destruction node event is fired and that the nodes are no
+ * longer stored internally in the tool, that the graph is updated properly, and
+ * that selecting a soon-to-be dead node clears the inspector.
+ *
+ * All done in one test since this test takes a few seconds to clear GC.
+ */
+
+add_task(function* () {
+ let { target, panel } = yield initWebAudioEditor(DESTROY_NODES_URL);
+ let { panelWin } = panel;
+ let { gFront, $, $$, gAudioNodes } = panelWin;
+
+ let started = once(gFront, "start-context");
+
+ let events = Promise.all([
+ getNSpread(gAudioNodes, "add", 13),
+ waitForGraphRendered(panelWin, 13, 2)
+ ]);
+ reload(target);
+ let [created] = yield events;
+
+ // Flatten arrays of event arguments and take the first (AudioNodeModel)
+ // and get its ID.
+ let actorIDs = created.map(ev => ev[0].id);
+
+ // Click a soon-to-be dead buffer node
+ yield clickGraphNode(panelWin, actorIDs[5]);
+
+ let destroyed = getN(gAudioNodes, "remove", 10);
+
+ // Force a CC in the child process to collect the orphaned nodes.
+ forceNodeCollection();
+
+ // Wait for destruction and graph to re-render
+ yield Promise.all([destroyed, waitForGraphRendered(panelWin, 3, 2)]);
+
+ // Test internal storage
+ is(panelWin.gAudioNodes.length, 3, "All nodes should be GC'd except one gain, osc and dest node.");
+
+ // Test graph rendering
+ ok(findGraphNode(panelWin, actorIDs[0]), "dest should be in graph");
+ ok(findGraphNode(panelWin, actorIDs[1]), "osc should be in graph");
+ ok(findGraphNode(panelWin, actorIDs[2]), "gain should be in graph");
+
+ let { nodes, edges } = countGraphObjects(panelWin);
+
+ is(nodes, 3, "Only 3 nodes rendered in graph.");
+ is(edges, 2, "Only 2 edges rendered in graph.");
+
+ // Test that the inspector reset to no node selected
+ ok(isVisible($("#web-audio-editor-details-pane-empty")),
+ "InspectorView empty message should show if the currently selected node gets collected.");
+
+ yield teardown(target);
+});
diff --git a/devtools/client/webaudioeditor/test/browser_wa_first-run.js b/devtools/client/webaudioeditor/test/browser_wa_first-run.js
new file mode 100644
index 000000000..d2d96932e
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/browser_wa_first-run.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+//
+// Whitelisting this test.
+// As part of bug 1077403, the leaking uncaught rejection should be fixed.
+//
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Connection closed");
+
+/**
+ * Tests that the reloading/onContentLoaded hooks work.
+ */
+
+add_task(function* () {
+ let { target, panel } = yield initWebAudioEditor(SIMPLE_CONTEXT_URL);
+ let { gFront, $ } = panel.panelWin;
+
+ is($("#reload-notice").hidden, false,
+ "The 'reload this page' notice should initially be visible.");
+ is($("#waiting-notice").hidden, true,
+ "The 'waiting for an audio context' notice should initially be hidden.");
+ is($("#content").hidden, true,
+ "The tool's content should initially be hidden.");
+
+ let navigating = once(target, "will-navigate");
+ let started = once(gFront, "start-context");
+
+ reload(target);
+
+ yield navigating;
+
+ is($("#reload-notice").hidden, true,
+ "The 'reload this page' notice should be hidden when navigating.");
+ is($("#waiting-notice").hidden, false,
+ "The 'waiting for an audio context' notice should be visible when navigating.");
+ is($("#content").hidden, true,
+ "The tool's content should still be hidden.");
+
+ yield started;
+
+ is($("#reload-notice").hidden, true,
+ "The 'reload this page' notice should be hidden after context found.");
+ is($("#waiting-notice").hidden, true,
+ "The 'waiting for an audio context' notice should be hidden after context found.");
+ is($("#content").hidden, false,
+ "The tool's content should not be hidden anymore.");
+
+ yield teardown(target);
+});
diff --git a/devtools/client/webaudioeditor/test/browser_wa_graph-click.js b/devtools/client/webaudioeditor/test/browser_wa_graph-click.js
new file mode 100644
index 000000000..b075d30db
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/browser_wa_graph-click.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the clicking on a node in the GraphView opens and sets
+ * the correct node in the InspectorView
+ */
+
+add_task(function* () {
+ let { target, panel } = yield initWebAudioEditor(COMPLEX_CONTEXT_URL);
+ let panelWin = panel.panelWin;
+ let { gFront, $, $$, InspectorView } = panelWin;
+
+ let started = once(gFront, "start-context");
+
+ let events = Promise.all([
+ getN(gFront, "create-node", 8),
+ waitForGraphRendered(panel.panelWin, 8, 8)
+ ]);
+ reload(target);
+ let [actors, _] = yield events;
+ let nodeIds = actors.map(actor => actor.actorID);
+
+ ok(!InspectorView.isVisible(), "InspectorView hidden on start.");
+
+ yield clickGraphNode(panelWin, nodeIds[1], true);
+
+ ok(InspectorView.isVisible(), "InspectorView visible after selecting a node.");
+ is(InspectorView.getCurrentAudioNode().id, nodeIds[1], "InspectorView has correct node set.");
+
+ yield clickGraphNode(panelWin, nodeIds[2]);
+
+ ok(InspectorView.isVisible(), "InspectorView still visible after selecting another node.");
+ is(InspectorView.getCurrentAudioNode().id, nodeIds[2], "InspectorView has correct node set on second node.");
+
+ yield clickGraphNode(panelWin, nodeIds[2]);
+ is(InspectorView.getCurrentAudioNode().id, nodeIds[2], "Clicking the same node again works (idempotent).");
+
+ yield clickGraphNode(panelWin, $("rect", findGraphNode(panelWin, nodeIds[3])));
+ is(InspectorView.getCurrentAudioNode().id, nodeIds[3], "Clicking on a <rect> works as expected.");
+
+ yield clickGraphNode(panelWin, $("tspan", findGraphNode(panelWin, nodeIds[4])));
+ is(InspectorView.getCurrentAudioNode().id, nodeIds[4], "Clicking on a <tspan> works as expected.");
+
+ ok(InspectorView.isVisible(),
+ "InspectorView still visible after several nodes have been clicked.");
+
+ yield teardown(target);
+});
diff --git a/devtools/client/webaudioeditor/test/browser_wa_graph-markers.js b/devtools/client/webaudioeditor/test/browser_wa_graph-markers.js
new file mode 100644
index 000000000..adc15d0c3
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/browser_wa_graph-markers.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the SVG marker styling is updated when devtools theme changes.
+ */
+
+const { setTheme } = require("devtools/client/shared/theme");
+
+add_task(function* () {
+ let { target, panel } = yield initWebAudioEditor(SIMPLE_CONTEXT_URL);
+ let { panelWin } = panel;
+ let { gFront, $, $$, MARKER_STYLING } = panelWin;
+
+ let currentTheme = Services.prefs.getCharPref("devtools.theme");
+
+ ok(MARKER_STYLING.light, "Marker styling exists for light theme.");
+ ok(MARKER_STYLING.dark, "Marker styling exists for dark theme.");
+
+ let started = once(gFront, "start-context");
+
+ let events = Promise.all([
+ get3(gFront, "create-node"),
+ waitForGraphRendered(panelWin, 3, 2)
+ ]);
+ reload(target);
+ let [actors] = yield events;
+
+ is(getFill($("#arrowhead")), MARKER_STYLING[currentTheme],
+ "marker initially matches theme.");
+
+ // Switch to light
+ setTheme("light");
+ is(getFill($("#arrowhead")), MARKER_STYLING.light,
+ "marker styling matches light theme on change.");
+
+ // Switch to dark
+ setTheme("dark");
+ is(getFill($("#arrowhead")), MARKER_STYLING.dark,
+ "marker styling matches dark theme on change.");
+
+ // Switch to dark again
+ setTheme("dark");
+ is(getFill($("#arrowhead")), MARKER_STYLING.dark,
+ "marker styling remains dark.");
+
+ // Switch to back to light again
+ setTheme("light");
+ is(getFill($("#arrowhead")), MARKER_STYLING.light,
+ "marker styling switches back to light once again.");
+
+ yield teardown(target);
+});
+
+/**
+ * Returns a hex value found in styling for an element. So parses
+ * <marker style="fill: #abcdef"> and returns "#abcdef"
+ */
+function getFill(el) {
+ return el.getAttribute("style").match(/(#.*)$/)[1];
+}
diff --git a/devtools/client/webaudioeditor/test/browser_wa_graph-render-01.js b/devtools/client/webaudioeditor/test/browser_wa_graph-render-01.js
new file mode 100644
index 000000000..cee1987b9
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/browser_wa_graph-render-01.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that SVG nodes and edges were created for the Graph View.
+ */
+
+var connectCount = 0;
+
+add_task(function* () {
+ let { target, panel } = yield initWebAudioEditor(SIMPLE_CONTEXT_URL);
+ let { panelWin } = panel;
+ let { gFront, $, $$, EVENTS, gAudioNodes } = panelWin;
+
+ let started = once(gFront, "start-context");
+
+ gAudioNodes.on("connect", onConnectNode);
+
+ let events = Promise.all([
+ get3(gFront, "create-node"),
+ waitForGraphRendered(panelWin, 3, 2)
+ ]);
+ reload(target);
+ let [actors] = yield events;
+ let [destId, oscId, gainId] = actors.map(actor => actor.actorID);
+
+ ok(findGraphNode(panelWin, oscId).classList.contains("type-OscillatorNode"), "found OscillatorNode with class");
+ ok(findGraphNode(panelWin, gainId).classList.contains("type-GainNode"), "found GainNode with class");
+ ok(findGraphNode(panelWin, destId).classList.contains("type-AudioDestinationNode"), "found AudioDestinationNode with class");
+ is(findGraphEdge(panelWin, oscId, gainId).toString(), "[object SVGGElement]", "found edge for osc -> gain");
+ is(findGraphEdge(panelWin, gainId, destId).toString(), "[object SVGGElement]", "found edge for gain -> dest");
+
+ yield wait(1000);
+
+ is(connectCount, 2, "Only two node connect events should be fired.");
+
+ gAudioNodes.off("connect", onConnectNode);
+
+ yield teardown(target);
+});
+
+function onConnectNode() {
+ ++connectCount;
+}
diff --git a/devtools/client/webaudioeditor/test/browser_wa_graph-render-02.js b/devtools/client/webaudioeditor/test/browser_wa_graph-render-02.js
new file mode 100644
index 000000000..00a63c6b2
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/browser_wa_graph-render-02.js
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests more edge rendering for complex graphs.
+ */
+
+add_task(function* () {
+ let { target, panel } = yield initWebAudioEditor(COMPLEX_CONTEXT_URL);
+ let { panelWin } = panel;
+ let { gFront, $, $$ } = panelWin;
+
+ let started = once(gFront, "start-context");
+
+ let events = Promise.all([
+ getN(gFront, "create-node", 8),
+ waitForGraphRendered(panelWin, 8, 8)
+ ]);
+ reload(target);
+ let [actors] = yield events;
+ let nodeIDs = actors.map(actor => actor.actorID);
+
+ let types = ["AudioDestinationNode", "OscillatorNode", "GainNode", "ScriptProcessorNode",
+ "OscillatorNode", "GainNode", "AudioBufferSourceNode", "BiquadFilterNode"];
+
+
+ types.forEach((type, i) => {
+ ok(findGraphNode(panelWin, nodeIDs[i]).classList.contains("type-" + type), "found " + type + " with class");
+ });
+
+ let edges = [
+ [1, 2, "osc1 -> gain1"],
+ [1, 3, "osc1 -> proc"],
+ [2, 0, "gain1 -> dest"],
+ [4, 5, "osc2 -> gain2"],
+ [5, 0, "gain2 -> dest"],
+ [6, 7, "buf -> filter"],
+ [4, 7, "osc2 -> filter"],
+ [7, 0, "filter -> dest"],
+ ];
+
+ edges.forEach(([source, target, msg], i) => {
+ is(findGraphEdge(panelWin, nodeIDs[source], nodeIDs[target]).toString(), "[object SVGGElement]",
+ "found edge for " + msg);
+ });
+
+ yield teardown(target);
+});
diff --git a/devtools/client/webaudioeditor/test/browser_wa_graph-render-03.js b/devtools/client/webaudioeditor/test/browser_wa_graph-render-03.js
new file mode 100644
index 000000000..ffd9b9881
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/browser_wa_graph-render-03.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests to ensure that selected nodes stay selected on graph redraw.
+ */
+
+add_task(function* () {
+ let { target, panel } = yield initWebAudioEditor(SIMPLE_CONTEXT_URL);
+ let { panelWin } = panel;
+ let { gFront, $, $$, EVENTS } = panelWin;
+
+ let events = Promise.all([
+ getN(gFront, "create-node", 3),
+ waitForGraphRendered(panelWin, 3, 2)
+ ]);
+ reload(target);
+ let [actors] = yield events;
+ let [dest, osc, gain] = actors;
+
+ yield clickGraphNode(panelWin, gain.actorID);
+ ok(findGraphNode(panelWin, gain.actorID).classList.contains("selected"),
+ "Node selected once.");
+
+ // Disconnect a node to trigger a rerender
+ osc.disconnect();
+
+ yield once(panelWin, EVENTS.UI_GRAPH_RENDERED);
+
+ ok(findGraphNode(panelWin, gain.actorID).classList.contains("selected"),
+ "Node still selected after rerender.");
+
+ yield teardown(target);
+});
diff --git a/devtools/client/webaudioeditor/test/browser_wa_graph-render-04.js b/devtools/client/webaudioeditor/test/browser_wa_graph-render-04.js
new file mode 100644
index 000000000..9ed3ceffd
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/browser_wa_graph-render-04.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests audio param connection rendering.
+ */
+
+add_task(function* () {
+ let { target, panel } = yield initWebAudioEditor(CONNECT_MULTI_PARAM_URL);
+ let { panelWin } = panel;
+ let { gFront, $, $$, EVENTS } = panelWin;
+
+ let started = once(gFront, "start-context");
+
+ let events = Promise.all([
+ getN(gFront, "create-node", 5),
+ waitForGraphRendered(panelWin, 5, 2, 3)
+ ]);
+ reload(target);
+ let [actors] = yield events;
+ let nodeIDs = actors.map(actor => actor.actorID);
+
+ let [, carrier, gain, mod1, mod2] = nodeIDs;
+
+ let edges = [
+ [mod1, gain, "gain", "mod1 -> gain[gain]"],
+ [mod2, carrier, "frequency", "mod2 -> carrier[frequency]"],
+ [mod2, carrier, "detune", "mod2 -> carrier[detune]"]
+ ];
+
+ edges.forEach(([source, target, param, msg], i) => {
+ let edge = findGraphEdge(panelWin, source, target, param);
+ ok(edge.classList.contains("param-connection"), "edge is classified as a param-connection");
+ });
+
+ yield teardown(target);
+});
diff --git a/devtools/client/webaudioeditor/test/browser_wa_graph-render-05.js b/devtools/client/webaudioeditor/test/browser_wa_graph-render-05.js
new file mode 100644
index 000000000..2748984d0
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/browser_wa_graph-render-05.js
@@ -0,0 +1,28 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests to ensure that param connections trigger graph redraws
+ */
+
+add_task(function* () {
+ let { target, panel } = yield initWebAudioEditor(SIMPLE_CONTEXT_URL);
+ let { panelWin } = panel;
+ let { gFront, $, $$, EVENTS } = panelWin;
+
+ let events = Promise.all([
+ getN(gFront, "create-node", 3),
+ waitForGraphRendered(panelWin, 3, 2, 0)
+ ]);
+ reload(target);
+ let [actors] = yield events;
+ let [dest, osc, gain] = actors;
+
+ yield osc.disconnect();
+
+ osc.connectParam(gain, "gain");
+ yield waitForGraphRendered(panelWin, 3, 1, 1);
+ ok(true, "Graph re-rendered upon param connection");
+
+ yield teardown(target);
+});
diff --git a/devtools/client/webaudioeditor/test/browser_wa_graph-render-06.js b/devtools/client/webaudioeditor/test/browser_wa_graph-render-06.js
new file mode 100644
index 000000000..c47d60b7c
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/browser_wa_graph-render-06.js
@@ -0,0 +1,25 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests to ensure that param connections trigger graph redraws
+ */
+
+const BUG_1141261_URL = EXAMPLE_URL + "doc_bug_1141261.html";
+
+add_task(function* () {
+ let { target, panel } = yield initWebAudioEditor(BUG_1141261_URL);
+ let { panelWin } = panel;
+ let { gFront, $, $$, EVENTS } = panelWin;
+
+ let events = Promise.all([
+ getN(gFront, "create-node", 3),
+ waitForGraphRendered(panelWin, 3, 1, 0)
+ ]);
+ reload(target);
+ yield events;
+
+ ok(true, "Graph correctly shows gain node as disconnected");
+
+ yield teardown(target);
+});
diff --git a/devtools/client/webaudioeditor/test/browser_wa_graph-selected.js b/devtools/client/webaudioeditor/test/browser_wa_graph-selected.js
new file mode 100644
index 000000000..72044b7bd
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/browser_wa_graph-selected.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that SVG nodes and edges were created for the Graph View.
+ */
+
+add_task(function* () {
+ let { target, panel } = yield initWebAudioEditor(SIMPLE_CONTEXT_URL);
+ let { panelWin } = panel;
+ let { gFront, $, $$, EVENTS } = panelWin;
+
+ let started = once(gFront, "start-context");
+
+ let events = Promise.all([
+ get3(gFront, "create-node"),
+ waitForGraphRendered(panelWin, 3, 2)
+ ]);
+ reload(target);
+ let [actors] = yield events;
+ let [destId, oscId, gainId] = actors.map(actor => actor.actorID);
+
+ ok(!findGraphNode(panelWin, destId).classList.contains("selected"),
+ "No nodes selected on start. (destination)");
+ ok(!findGraphNode(panelWin, oscId).classList.contains("selected"),
+ "No nodes selected on start. (oscillator)");
+ ok(!findGraphNode(panelWin, gainId).classList.contains("selected"),
+ "No nodes selected on start. (gain)");
+
+ yield clickGraphNode(panelWin, oscId);
+
+ ok(findGraphNode(panelWin, oscId).classList.contains("selected"),
+ "Selected node has class 'selected'.");
+ ok(!findGraphNode(panelWin, destId).classList.contains("selected"),
+ "Non-selected nodes do not have class 'selected'.");
+ ok(!findGraphNode(panelWin, gainId).classList.contains("selected"),
+ "Non-selected nodes do not have class 'selected'.");
+
+ yield clickGraphNode(panelWin, gainId);
+
+ ok(!findGraphNode(panelWin, oscId).classList.contains("selected"),
+ "Previously selected node no longer has class 'selected'.");
+ ok(!findGraphNode(panelWin, destId).classList.contains("selected"),
+ "Non-selected nodes do not have class 'selected'.");
+ ok(findGraphNode(panelWin, gainId).classList.contains("selected"),
+ "Newly selected node now has class 'selected'.");
+
+ yield teardown(target);
+});
diff --git a/devtools/client/webaudioeditor/test/browser_wa_graph-zoom.js b/devtools/client/webaudioeditor/test/browser_wa_graph-zoom.js
new file mode 100644
index 000000000..240b6d5a1
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/browser_wa_graph-zoom.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the graph's scale and position is reset on a page reload.
+ */
+
+add_task(function* () {
+ let { target, panel } = yield initWebAudioEditor(SIMPLE_CONTEXT_URL);
+ let { panelWin } = panel;
+ let { gFront, $, $$, EVENTS, ContextView } = panelWin;
+
+ let started = once(gFront, "start-context");
+
+ yield Promise.all([
+ waitForGraphRendered(panelWin, 3, 2),
+ reload(target),
+ ]);
+
+ is(ContextView.getCurrentScale(), 1, "Default graph scale is 1.");
+ is(ContextView.getCurrentTranslation()[0], 20, "Default x-translation is 20.");
+ is(ContextView.getCurrentTranslation()[1], 20, "Default y-translation is 20.");
+
+ // Change both attribute and D3's internal store
+ panelWin.d3.select("#graph-target").attr("transform", "translate([100, 400]) scale(10)");
+ ContextView._zoomBinding.scale(10);
+ ContextView._zoomBinding.translate([100, 400]);
+
+ is(ContextView.getCurrentScale(), 10, "After zoom, scale is 10.");
+ is(ContextView.getCurrentTranslation()[0], 100, "After zoom, x-translation is 100.");
+ is(ContextView.getCurrentTranslation()[1], 400, "After zoom, y-translation is 400.");
+
+ yield Promise.all([
+ waitForGraphRendered(panelWin, 3, 2),
+ reload(target),
+ ]);
+
+ is(ContextView.getCurrentScale(), 1, "After refresh, graph scale is 1.");
+ is(ContextView.getCurrentTranslation()[0], 20, "After refresh, x-translation is 20.");
+ is(ContextView.getCurrentTranslation()[1], 20, "After refresh, y-translation is 20.");
+
+ yield teardown(target);
+});
diff --git a/devtools/client/webaudioeditor/test/browser_wa_inspector-bypass-01.js b/devtools/client/webaudioeditor/test/browser_wa_inspector-bypass-01.js
new file mode 100644
index 000000000..c9d3450e3
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/browser_wa_inspector-bypass-01.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that nodes are correctly bypassed when bypassing.
+ */
+
+add_task(function* () {
+ let { target, panel } = yield initWebAudioEditor(SIMPLE_CONTEXT_URL);
+ let { panelWin } = panel;
+ let { gFront, $, $$, EVENTS, gAudioNodes } = panelWin;
+
+ let events = Promise.all([
+ get3(gFront, "create-node"),
+ waitForGraphRendered(panelWin, 3, 2)
+ ]);
+ reload(target);
+ let [actors] = yield events;
+ let nodeIds = actors.map(actor => actor.actorID);
+
+ // Wait for the node to be set as well as the inspector to come fully into the view
+ yield clickGraphNode(panelWin, findGraphNode(panelWin, nodeIds[1]), true);
+
+ let $bypass = $("toolbarbutton.bypass");
+
+ is((yield actors[1].isBypassed()), false, "AudioNodeActor is not bypassed by default.");
+ is($bypass.checked, true, "Button is 'on' for normal nodes");
+ is($bypass.disabled, false, "Bypass button is not disabled for normal nodes");
+
+ command($bypass);
+ yield once(gAudioNodes, "bypass");
+
+ is((yield actors[1].isBypassed()), true, "AudioNodeActor is bypassed.");
+ is($bypass.checked, false, "Button is 'off' when clicked");
+ is($bypass.disabled, false, "Bypass button is not disabled after click");
+ ok(findGraphNode(panelWin, nodeIds[1]).classList.contains("bypassed"),
+ "AudioNode has 'bypassed' class.");
+
+ command($bypass);
+ yield once(gAudioNodes, "bypass");
+
+ is((yield actors[1].isBypassed()), false, "AudioNodeActor is no longer bypassed.");
+ is($bypass.checked, true, "Button is back on when clicked");
+ is($bypass.disabled, false, "Bypass button is not disabled after click");
+ ok(!findGraphNode(panelWin, nodeIds[1]).classList.contains("bypassed"),
+ "AudioNode no longer has 'bypassed' class.");
+
+ yield clickGraphNode(panelWin, findGraphNode(panelWin, nodeIds[0]));
+
+ is((yield actors[0].isBypassed()), false, "Unbypassable AudioNodeActor is not bypassed.");
+ is($bypass.checked, false, "Button is 'off' for unbypassable nodes");
+ is($bypass.disabled, true, "Bypass button is disabled for unbypassable nodes");
+
+ command($bypass);
+ is((yield actors[0].isBypassed()), false,
+ "Clicking button on unbypassable node does not change bypass state on actor.");
+ is($bypass.checked, false, "Button is still 'off' for unbypassable nodes");
+ is($bypass.disabled, true, "Bypass button is still disabled for unbypassable nodes");
+
+ yield teardown(target);
+});
diff --git a/devtools/client/webaudioeditor/test/browser_wa_inspector-toggle.js b/devtools/client/webaudioeditor/test/browser_wa_inspector-toggle.js
new file mode 100644
index 000000000..251f92471
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/browser_wa_inspector-toggle.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the inspector toggle button shows and hides
+ * the inspector panel as intended.
+ */
+
+add_task(function* () {
+ let { target, panel } = yield initWebAudioEditor(SIMPLE_CONTEXT_URL);
+ let { panelWin } = panel;
+ let { gFront, $, $$, EVENTS, InspectorView } = panelWin;
+ let gVars = InspectorView._propsView;
+
+ let started = once(gFront, "start-context");
+
+ let events = Promise.all([
+ get3(gFront, "create-node"),
+ waitForGraphRendered(panelWin, 3, 2)
+ ]);
+ reload(target);
+ let [actors] = yield events;
+ let nodeIds = actors.map(actor => actor.actorID);
+
+ ok(!InspectorView.isVisible(), "InspectorView hidden on start.");
+
+ // Open inspector pane
+ $("#inspector-pane-toggle").click();
+ yield once(panelWin, EVENTS.UI_INSPECTOR_TOGGLED);
+
+ ok(InspectorView.isVisible(), "InspectorView shown after toggling.");
+
+ ok(isVisible($("#web-audio-editor-details-pane-empty")),
+ "InspectorView empty message should still be visible.");
+ ok(!isVisible($("#web-audio-editor-tabs")),
+ "InspectorView tabs view should still be hidden.");
+
+ // Close inspector pane
+ $("#inspector-pane-toggle").click();
+ yield once(panelWin, EVENTS.UI_INSPECTOR_TOGGLED);
+
+ ok(!InspectorView.isVisible(), "InspectorView back to being hidden.");
+
+ // Open again to test node loading while open
+ $("#inspector-pane-toggle").click();
+ yield once(panelWin, EVENTS.UI_INSPECTOR_TOGGLED);
+
+ ok(InspectorView.isVisible(), "InspectorView being shown.");
+ ok(!isVisible($("#web-audio-editor-tabs")),
+ "InspectorView tabs are still hidden.");
+
+ yield clickGraphNode(panelWin, findGraphNode(panelWin, nodeIds[1]));
+
+ ok(!isVisible($("#web-audio-editor-details-pane-empty")),
+ "Empty message hides even when loading node while open.");
+ ok(isVisible($("#web-audio-editor-tabs")),
+ "Switches to tab view when loading node while open.");
+
+ yield teardown(target);
+});
diff --git a/devtools/client/webaudioeditor/test/browser_wa_inspector-width.js b/devtools/client/webaudioeditor/test/browser_wa_inspector-width.js
new file mode 100644
index 000000000..d37774013
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/browser_wa_inspector-width.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test that the WebAudioInspector's Width is saved as
+ * a preference
+ */
+
+add_task(function* () {
+ let { target, panel } = yield initWebAudioEditor(SIMPLE_CONTEXT_URL);
+ let { panelWin } = panel;
+ let { gFront, $, $$, EVENTS, InspectorView } = panelWin;
+ let gVars = InspectorView._propsView;
+
+ let started = once(gFront, "start-context");
+
+ let events = Promise.all([
+ get3(gFront, "create-node"),
+ waitForGraphRendered(panelWin, 3, 2)
+ ]);
+ reload(target);
+ let [actors] = yield events;
+ let nodeIds = actors.map(actor => actor.actorID);
+
+ ok(!InspectorView.isVisible(), "InspectorView hidden on start.");
+
+ // Open inspector pane
+ $("#inspector-pane-toggle").click();
+ yield once(panelWin, EVENTS.UI_INSPECTOR_TOGGLED);
+
+ let newInspectorWidth = 500;
+
+ // Setting width to new_inspector_width
+ $("#web-audio-inspector").setAttribute("width", newInspectorWidth);
+
+ // Width should be 500 after reloading
+ events = Promise.all([
+ get3(gFront, "create-node"),
+ waitForGraphRendered(panelWin, 3, 2)
+ ]);
+ reload(target);
+ [actors] = yield events;
+ nodeIds = actors.map(actor => actor.actorID);
+
+ // Open inspector pane
+ $("#inspector-pane-toggle").click();
+ yield once(panelWin, EVENTS.UI_INSPECTOR_TOGGLED);
+
+ yield clickGraphNode(panelWin, findGraphNode(panelWin, nodeIds[1]));
+
+ // Getting the width of the audio inspector
+ let width = $("#web-audio-inspector").getAttribute("width");
+
+ is(width, newInspectorWidth, "WebAudioEditor's Inspector width should be saved as a preference");
+
+ yield teardown(target);
+});
diff --git a/devtools/client/webaudioeditor/test/browser_wa_inspector.js b/devtools/client/webaudioeditor/test/browser_wa_inspector.js
new file mode 100644
index 000000000..5599ad36f
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/browser_wa_inspector.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that inspector view opens on graph node click, and
+ * loads the correct node inside the inspector.
+ */
+
+add_task(function* () {
+ let { target, panel } = yield initWebAudioEditor(SIMPLE_CONTEXT_URL);
+ let { panelWin } = panel;
+ let { gFront, $, $$, EVENTS, InspectorView } = panelWin;
+ let gVars = InspectorView._propsView;
+
+ let started = once(gFront, "start-context");
+
+ let events = Promise.all([
+ get3(gFront, "create-node"),
+ waitForGraphRendered(panelWin, 3, 2)
+ ]);
+ reload(target);
+ let [actors] = yield events;
+ let nodeIds = actors.map(actor => actor.actorID);
+
+ ok(!InspectorView.isVisible(), "InspectorView hidden on start.");
+ ok(isVisible($("#web-audio-editor-details-pane-empty")),
+ "InspectorView empty message should show when no node's selected.");
+ ok(!isVisible($("#web-audio-editor-tabs")),
+ "InspectorView tabs view should be hidden when no node's selected.");
+
+ // Wait for the node to be set as well as the inspector to come fully into the view
+ yield clickGraphNode(panelWin, findGraphNode(panelWin, nodeIds[1]), true);
+
+ ok(InspectorView.isVisible(), "InspectorView shown once node selected.");
+ ok(!isVisible($("#web-audio-editor-details-pane-empty")),
+ "InspectorView empty message hidden when node selected.");
+ ok(isVisible($("#web-audio-editor-tabs")),
+ "InspectorView tabs view visible when node selected.");
+
+ is($("#web-audio-editor-tabs").selectedIndex, 0,
+ "default tab selected should be the parameters tab.");
+
+ yield clickGraphNode(panelWin, findGraphNode(panelWin, nodeIds[2]));
+
+ yield teardown(target);
+});
diff --git a/devtools/client/webaudioeditor/test/browser_wa_navigate.js b/devtools/client/webaudioeditor/test/browser_wa_navigate.js
new file mode 100644
index 000000000..e1f094384
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/browser_wa_navigate.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests naviating from a page to another will repopulate
+ * the audio graph if both pages have an AudioContext.
+ */
+
+add_task(function* () {
+ let { target, panel } = yield initWebAudioEditor(SIMPLE_CONTEXT_URL);
+ let { panelWin } = panel;
+ let { gFront, $ } = panelWin;
+
+ let events = Promise.all([
+ get3(gFront, "create-node"),
+ waitForGraphRendered(panelWin, 3, 2)
+ ]);
+ reload(target);
+ yield events;
+
+ var { nodes, edges } = countGraphObjects(panelWin);
+ is(nodes, 3, "should only be 3 nodes.");
+ is(edges, 2, "should only be 2 edges.");
+
+ events = Promise.all([
+ getN(gFront, "create-node", 15),
+ waitForGraphRendered(panelWin, 15, 0)
+ ]);
+ navigate(target, SIMPLE_NODES_URL);
+ yield events;
+
+ is($("#reload-notice").hidden, true,
+ "The 'reload this page' notice should be hidden after context found after navigation.");
+ is($("#waiting-notice").hidden, true,
+ "The 'waiting for an audio context' notice should be hidden after context found after navigation.");
+ is($("#content").hidden, false,
+ "The tool's content should reappear without closing and reopening the toolbox.");
+
+ var { nodes, edges } = countGraphObjects(panelWin);
+ is(nodes, 15, "after navigation, should have 15 nodes");
+ is(edges, 0, "after navigation, should have 0 edges.");
+
+ yield teardown(target);
+});
diff --git a/devtools/client/webaudioeditor/test/browser_wa_properties-view-edit-01.js b/devtools/client/webaudioeditor/test/browser_wa_properties-view-edit-01.js
new file mode 100644
index 000000000..ac7deca26
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/browser_wa_properties-view-edit-01.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that properties are updated when modifying the VariablesView.
+ */
+
+add_task(function* () {
+ let { target, panel } = yield initWebAudioEditor(SIMPLE_CONTEXT_URL);
+ let { panelWin } = panel;
+ let { gFront, $, $$, EVENTS, PropertiesView } = panelWin;
+ let gVars = PropertiesView._propsView;
+
+ let started = once(gFront, "start-context");
+
+ let events = Promise.all([
+ get3(gFront, "create-node"),
+ waitForGraphRendered(panelWin, 3, 2)
+ ]);
+ reload(target);
+ let [actors] = yield events;
+ let nodeIds = actors.map(actor => actor.actorID);
+
+ click(panelWin, findGraphNode(panelWin, nodeIds[1]));
+ // Wait for the node to be set as well as the inspector to come fully into the view
+ yield Promise.all([
+ waitForInspectorRender(panelWin, EVENTS),
+ once(panelWin, EVENTS.UI_INSPECTOR_TOGGLED)
+ ]);
+
+ let setAndCheck = setAndCheckVariable(panelWin, gVars);
+
+ checkVariableView(gVars, 0, {
+ "type": "sine",
+ "frequency": 440,
+ "detune": 0
+ }, "default loaded string");
+
+ click(panelWin, findGraphNode(panelWin, nodeIds[2]));
+ yield waitForInspectorRender(panelWin, EVENTS),
+ checkVariableView(gVars, 0, {
+ "gain": 0
+ }, "default loaded number");
+
+ click(panelWin, findGraphNode(panelWin, nodeIds[1]));
+ yield waitForInspectorRender(panelWin, EVENTS),
+ yield setAndCheck(0, "type", "square", "square", "sets string as string");
+
+ click(panelWin, findGraphNode(panelWin, nodeIds[2]));
+ yield waitForInspectorRender(panelWin, EVENTS),
+ yield setAndCheck(0, "gain", "0.005", 0.005, "sets number as number");
+ yield setAndCheck(0, "gain", "0.1", 0.1, "sets float as float");
+ yield setAndCheck(0, "gain", ".2", 0.2, "sets float without leading zero as float");
+
+ yield teardown(target);
+});
+
+function setAndCheckVariable(panelWin, gVars) {
+ return Task.async(function* (varNum, prop, value, expected, desc) {
+ yield modifyVariableView(panelWin, gVars, varNum, prop, value);
+ var props = {};
+ props[prop] = expected;
+ checkVariableView(gVars, varNum, props, desc);
+ });
+}
diff --git a/devtools/client/webaudioeditor/test/browser_wa_properties-view-edit-02.js b/devtools/client/webaudioeditor/test/browser_wa_properties-view-edit-02.js
new file mode 100644
index 000000000..d7c54822d
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/browser_wa_properties-view-edit-02.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that properties are not updated when modifying the VariablesView.
+ */
+
+add_task(function* () {
+ let { target, panel } = yield initWebAudioEditor(COMPLEX_CONTEXT_URL);
+ let { panelWin } = panel;
+ let { gFront, $, $$, EVENTS, PropertiesView } = panelWin;
+ let gVars = PropertiesView._propsView;
+
+ let started = once(gFront, "start-context");
+
+ let events = Promise.all([
+ getN(gFront, "create-node", 8),
+ waitForGraphRendered(panelWin, 8, 8)
+ ]);
+ reload(target);
+ let [actors] = yield events;
+ let nodeIds = actors.map(actor => actor.actorID);
+
+ click(panelWin, findGraphNode(panelWin, nodeIds[3]));
+ // Wait for the node to be set as well as the inspector to come fully into the view
+ yield Promise.all([
+ waitForInspectorRender(panelWin, EVENTS),
+ once(panelWin, EVENTS.UI_INSPECTOR_TOGGLED),
+ ]);
+
+ let errorEvent = once(panelWin, EVENTS.UI_SET_PARAM_ERROR);
+
+ try {
+ yield modifyVariableView(panelWin, gVars, 0, "bufferSize", 2048);
+ } catch (e) {
+ // we except modifyVariableView to fail here, because bufferSize is not writable
+ }
+
+ yield errorEvent;
+
+ checkVariableView(gVars, 0, {bufferSize: 4096}, "check that unwritable variable is not updated");
+
+ yield teardown(target);
+});
diff --git a/devtools/client/webaudioeditor/test/browser_wa_properties-view-media-nodes.js b/devtools/client/webaudioeditor/test/browser_wa_properties-view-media-nodes.js
new file mode 100644
index 000000000..c1a916a1f
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/browser_wa_properties-view-media-nodes.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that params view correctly displays all properties for nodes
+ * correctly, with default values and correct types.
+ */
+
+var MEDIA_PERMISSION = "media.navigator.permission.disabled";
+
+function waitForDeviceClosed() {
+ info("Checking that getUserMedia streams are no longer in use.");
+
+ let temp = {};
+ Cu.import("resource:///modules/webrtcUI.jsm", temp);
+ let webrtcUI = temp.webrtcUI;
+
+ if (!webrtcUI.showGlobalIndicator)
+ return Promise.resolve();
+
+ let deferred = Promise.defer();
+
+ const message = "webrtc:UpdateGlobalIndicators";
+ let ppmm = Cc["@mozilla.org/parentprocessmessagemanager;1"]
+ .getService(Ci.nsIMessageBroadcaster);
+ ppmm.addMessageListener(message, function listener(aMessage) {
+ info("Received " + message + " message");
+ if (!aMessage.data.showGlobalIndicator) {
+ ppmm.removeMessageListener(message, listener);
+ deferred.resolve();
+ }
+ });
+
+ return deferred.promise;
+}
+
+add_task(function* () {
+ let { target, panel } = yield initWebAudioEditor(MEDIA_NODES_URL);
+ let { panelWin } = panel;
+ let { gFront, $, $$, EVENTS, PropertiesView } = panelWin;
+ let gVars = PropertiesView._propsView;
+
+ // Auto enable getUserMedia
+ let mediaPermissionPref = Services.prefs.getBoolPref(MEDIA_PERMISSION);
+ Services.prefs.setBoolPref(MEDIA_PERMISSION, true);
+
+ yield loadFrameScripts();
+
+ let events = Promise.all([
+ getN(gFront, "create-node", 4),
+ waitForGraphRendered(panelWin, 4, 0)
+ ]);
+ reload(target);
+ let [actors] = yield events;
+ let nodeIds = actors.map(actor => actor.actorID);
+
+ let types = [
+ "AudioDestinationNode", "MediaElementAudioSourceNode",
+ "MediaStreamAudioSourceNode", "MediaStreamAudioDestinationNode"
+ ];
+
+ let defaults = yield Promise.all(types.map(type => nodeDefaultValues(type)));
+
+ for (let i = 0; i < types.length; i++) {
+ click(panelWin, findGraphNode(panelWin, nodeIds[i]));
+ yield waitForInspectorRender(panelWin, EVENTS);
+ checkVariableView(gVars, 0, defaults[i], types[i]);
+ }
+
+ // Reset permissions on getUserMedia
+ Services.prefs.setBoolPref(MEDIA_PERMISSION, mediaPermissionPref);
+
+ yield teardown(target);
+
+ yield waitForDeviceClosed();
+});
diff --git a/devtools/client/webaudioeditor/test/browser_wa_properties-view-params-objects.js b/devtools/client/webaudioeditor/test/browser_wa_properties-view-params-objects.js
new file mode 100644
index 000000000..e0a555201
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/browser_wa_properties-view-params-objects.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that params view correctly displays non-primitive properties
+ * like AudioBuffer and Float32Array in properties of AudioNodes.
+ */
+
+add_task(function* () {
+ let { target, panel } = yield initWebAudioEditor(BUFFER_AND_ARRAY_URL);
+ let { panelWin } = panel;
+ let { gFront, $, $$, EVENTS, PropertiesView } = panelWin;
+ let gVars = PropertiesView._propsView;
+
+ let started = once(gFront, "start-context");
+
+ let events = Promise.all([
+ getN(gFront, "create-node", 3),
+ waitForGraphRendered(panelWin, 3, 2)
+ ]);
+ reload(target);
+ let [actors] = yield events;
+ let nodeIds = actors.map(actor => actor.actorID);
+
+ click(panelWin, findGraphNode(panelWin, nodeIds[2]));
+ yield waitForInspectorRender(panelWin, EVENTS);
+ checkVariableView(gVars, 0, {
+ "curve": "Float32Array"
+ }, "WaveShaper's `curve` is listed as an `Float32Array`.");
+
+ let aVar = gVars.getScopeAtIndex(0).get("curve");
+ let state = aVar.target.querySelector(".theme-twisty").hasAttribute("invisible");
+ ok(state, "Float32Array property should not have a dropdown.");
+
+ click(panelWin, findGraphNode(panelWin, nodeIds[1]));
+ yield waitForInspectorRender(panelWin, EVENTS);
+ checkVariableView(gVars, 0, {
+ "buffer": "AudioBuffer"
+ }, "AudioBufferSourceNode's `buffer` is listed as an `AudioBuffer`.");
+
+ aVar = gVars.getScopeAtIndex(0).get("buffer");
+ state = aVar.target.querySelector(".theme-twisty").hasAttribute("invisible");
+ ok(state, "AudioBuffer property should not have a dropdown.");
+
+ yield teardown(target);
+});
diff --git a/devtools/client/webaudioeditor/test/browser_wa_properties-view-params.js b/devtools/client/webaudioeditor/test/browser_wa_properties-view-params.js
new file mode 100644
index 000000000..31319e8c5
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/browser_wa_properties-view-params.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that params view correctly displays all properties for nodes
+ * correctly, with default values and correct types.
+ */
+
+add_task(function* () {
+ let { target, panel } = yield initWebAudioEditor(SIMPLE_NODES_URL);
+ let { panelWin } = panel;
+ let { gFront, $, $$, EVENTS, PropertiesView } = panelWin;
+ let gVars = PropertiesView._propsView;
+
+ let started = once(gFront, "start-context");
+
+ yield loadFrameScripts();
+
+ let events = Promise.all([
+ getN(gFront, "create-node", 15),
+ waitForGraphRendered(panelWin, 15, 0)
+ ]);
+ reload(target);
+ let [actors] = yield events;
+ let nodeIds = actors.map(actor => actor.actorID);
+
+ let types = [
+ "AudioDestinationNode", "AudioBufferSourceNode", "ScriptProcessorNode",
+ "AnalyserNode", "GainNode", "DelayNode", "BiquadFilterNode", "WaveShaperNode",
+ "PannerNode", "ConvolverNode", "ChannelSplitterNode", "ChannelMergerNode",
+ "DynamicsCompressorNode", "OscillatorNode"
+ ];
+
+ let defaults = yield Promise.all(types.map(type => nodeDefaultValues(type)));
+
+ for (let i = 0; i < types.length; i++) {
+ click(panelWin, findGraphNode(panelWin, nodeIds[i]));
+ yield waitForInspectorRender(panelWin, EVENTS);
+ checkVariableView(gVars, 0, defaults[i], types[i]);
+ }
+
+ yield teardown(target);
+});
diff --git a/devtools/client/webaudioeditor/test/browser_wa_properties-view.js b/devtools/client/webaudioeditor/test/browser_wa_properties-view.js
new file mode 100644
index 000000000..bda51e4ac
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/browser_wa_properties-view.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that params view shows params when they exist, and are hidden otherwise.
+ */
+
+add_task(function* () {
+ let { target, panel } = yield initWebAudioEditor(SIMPLE_CONTEXT_URL);
+ let { panelWin } = panel;
+ let { gFront, $, $$, EVENTS, PropertiesView } = panelWin;
+ let gVars = PropertiesView._propsView;
+
+ let started = once(gFront, "start-context");
+
+ let events = Promise.all([
+ get3(gFront, "create-node"),
+ waitForGraphRendered(panelWin, 3, 2)
+ ]);
+ reload(target);
+ let [actors] = yield events;
+ let nodeIds = actors.map(actor => actor.actorID);
+
+ // Gain node
+ click(panelWin, findGraphNode(panelWin, nodeIds[2]));
+ yield waitForInspectorRender(panelWin, EVENTS);
+
+ ok(isVisible($("#properties-content")), "Parameters shown when they exist.");
+ ok(!isVisible($("#properties-empty")),
+ "Empty message hidden when AudioParams exist.");
+
+ // Destination node
+ click(panelWin, findGraphNode(panelWin, nodeIds[0]));
+ yield waitForInspectorRender(panelWin, EVENTS);
+
+ ok(!isVisible($("#properties-content")),
+ "Parameters hidden when they don't exist.");
+ ok(isVisible($("#properties-empty")),
+ "Empty message shown when no AudioParams exist.");
+
+ yield teardown(target);
+});
diff --git a/devtools/client/webaudioeditor/test/browser_wa_reset-01.js b/devtools/client/webaudioeditor/test/browser_wa_reset-01.js
new file mode 100644
index 000000000..67a0c33ff
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/browser_wa_reset-01.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+//
+// Whitelisting this test.
+// As part of bug 1077403, the leaking uncaught rejection should be fixed.
+//
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Connection closed");
+
+/**
+ * Tests that reloading a tab will properly listen for the `start-context`
+ * event and reshow the tools after reloading.
+ */
+
+add_task(function* () {
+ let { target, panel } = yield initWebAudioEditor(SIMPLE_CONTEXT_URL);
+ let { gFront, $ } = panel.panelWin;
+
+ is($("#reload-notice").hidden, false,
+ "The 'reload this page' notice should initially be visible.");
+ is($("#waiting-notice").hidden, true,
+ "The 'waiting for an audio context' notice should initially be hidden.");
+ is($("#content").hidden, true,
+ "The tool's content should initially be hidden.");
+
+ let navigating = once(target, "will-navigate");
+ let started = once(gFront, "start-context");
+
+ reload(target);
+
+ yield navigating;
+
+ is($("#reload-notice").hidden, true,
+ "The 'reload this page' notice should be hidden when navigating.");
+ is($("#waiting-notice").hidden, false,
+ "The 'waiting for an audio context' notice should be visible when navigating.");
+ is($("#content").hidden, true,
+ "The tool's content should still be hidden.");
+
+ yield started;
+
+ is($("#reload-notice").hidden, true,
+ "The 'reload this page' notice should be hidden after context found.");
+ is($("#waiting-notice").hidden, true,
+ "The 'waiting for an audio context' notice should be hidden after context found.");
+ is($("#content").hidden, false,
+ "The tool's content should not be hidden anymore.");
+
+ navigating = once(target, "will-navigate");
+ started = once(gFront, "start-context");
+
+ reload(target);
+
+ yield Promise.all([navigating, started]);
+ let rendered = waitForGraphRendered(panel.panelWin, 3, 2);
+
+ is($("#reload-notice").hidden, true,
+ "The 'reload this page' notice should be hidden after context found after reload.");
+ is($("#waiting-notice").hidden, true,
+ "The 'waiting for an audio context' notice should be hidden after context found after reload.");
+ is($("#content").hidden, false,
+ "The tool's content should reappear without closing and reopening the toolbox.");
+
+ yield rendered;
+
+ yield teardown(target);
+});
diff --git a/devtools/client/webaudioeditor/test/browser_wa_reset-02.js b/devtools/client/webaudioeditor/test/browser_wa_reset-02.js
new file mode 100644
index 000000000..c9516b364
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/browser_wa_reset-02.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests reloading a tab with the tools open properly cleans up
+ * the graph.
+ */
+
+add_task(function* () {
+ let { target, panel } = yield initWebAudioEditor(SIMPLE_CONTEXT_URL);
+ let { panelWin } = panel;
+ let { gFront, $ } = panelWin;
+
+ let events = Promise.all([
+ get3(gFront, "create-node"),
+ waitForGraphRendered(panelWin, 3, 2)
+ ]);
+ reload(target);
+ yield events;
+
+ let { nodes, edges } = countGraphObjects(panelWin);
+ is(nodes, 3, "should only be 3 nodes.");
+ is(edges, 2, "should only be 2 edges.");
+
+ events = Promise.all([
+ get3(gFront, "create-node"),
+ waitForGraphRendered(panelWin, 3, 2)
+ ]);
+ reload(target);
+ yield events;
+
+ ({ nodes, edges } = countGraphObjects(panelWin));
+ is(nodes, 3, "after reload, should only be 3 nodes.");
+ is(edges, 2, "after reload, should only be 2 edges.");
+
+ yield teardown(target);
+});
diff --git a/devtools/client/webaudioeditor/test/browser_wa_reset-03.js b/devtools/client/webaudioeditor/test/browser_wa_reset-03.js
new file mode 100644
index 000000000..1207793f5
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/browser_wa_reset-03.js
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests reloading a tab with the tools open properly cleans up
+ * the inspector and selected node.
+ */
+
+add_task(function* () {
+ let { target, panel } = yield initWebAudioEditor(SIMPLE_CONTEXT_URL);
+ let { panelWin } = panel;
+ let { gFront, $, InspectorView } = panelWin;
+
+ let events = Promise.all([
+ get3(gFront, "create-node"),
+ waitForGraphRendered(panelWin, 3, 2)
+ ]);
+ reload(target);
+ let [actors] = yield events;
+ let nodeIds = actors.map(actor => actor.actorID);
+
+ yield clickGraphNode(panelWin, nodeIds[1], true);
+ ok(InspectorView.isVisible(), "InspectorView visible after selecting a node.");
+ is(InspectorView.getCurrentAudioNode().id, nodeIds[1], "InspectorView has correct node set.");
+
+ /**
+ * Reload
+ */
+
+ events = Promise.all([
+ get3(gFront, "create-node"),
+ waitForGraphRendered(panelWin, 3, 2)
+ ]);
+ reload(target);
+ [actors] = yield events;
+ nodeIds = actors.map(actor => actor.actorID);
+
+ ok(!InspectorView.isVisible(), "InspectorView hidden on start.");
+ is(InspectorView.getCurrentAudioNode(), null,
+ "InspectorView has no current node set on reset.");
+
+ yield clickGraphNode(panelWin, nodeIds[2], true);
+ ok(InspectorView.isVisible(),
+ "InspectorView visible after selecting a node after a reset.");
+ is(InspectorView.getCurrentAudioNode().id, nodeIds[2], "InspectorView has correct node set upon clicking graph node after a reset.");
+
+ yield teardown(target);
+});
diff --git a/devtools/client/webaudioeditor/test/browser_wa_reset-04.js b/devtools/client/webaudioeditor/test/browser_wa_reset-04.js
new file mode 100644
index 000000000..7ad009020
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/browser_wa_reset-04.js
@@ -0,0 +1,66 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+//
+// Whitelisting this test.
+// As part of bug 1077403, the leaking uncaught rejection should be fixed.
+//
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Connection closed");
+
+/**
+ * Tests that switching to an iframe works fine.
+ */
+
+add_task(function* () {
+ Services.prefs.setBoolPref("devtools.command-button-frames.enabled", true);
+
+ let { target, panel, toolbox } = yield initWebAudioEditor(IFRAME_CONTEXT_URL);
+ let { gFront, $ } = panel.panelWin;
+
+ is($("#reload-notice").hidden, false,
+ "The 'reload this page' notice should initially be visible.");
+ is($("#waiting-notice").hidden, true,
+ "The 'waiting for an audio context' notice should initially be hidden.");
+ is($("#content").hidden, true,
+ "The tool's content should initially be hidden.");
+
+ let btn = toolbox.doc.getElementById("command-button-frames");
+ ok(!btn.firstChild, "The frame list button has no children");
+
+ // Open frame menu and wait till it's available on the screen.
+ let menu = toolbox.showFramesMenu({target: btn});
+ yield once(menu, "open");
+
+ let frames = menu.items;
+ is(frames.length, 2, "We have both frames in the list");
+
+ // Select the iframe
+ frames[1].click();
+
+ let navigating = once(target, "will-navigate");
+
+ yield navigating;
+
+ is($("#reload-notice").hidden, false,
+ "The 'reload this page' notice should still be visible when switching to a frame.");
+ is($("#waiting-notice").hidden, true,
+ "The 'waiting for an audio context' notice should be kept hidden when switching to a frame.");
+ is($("#content").hidden, true,
+ "The tool's content should still be hidden.");
+
+ navigating = once(target, "will-navigate");
+ let started = once(gFront, "start-context");
+
+ reload(target);
+
+ yield Promise.all([navigating, started]);
+
+ is($("#reload-notice").hidden, true,
+ "The 'reload this page' notice should be hidden after reloading the frame.");
+ is($("#waiting-notice").hidden, true,
+ "The 'waiting for an audio context' notice should be hidden after reloading the frame.");
+ is($("#content").hidden, false,
+ "The tool's content should appear after reload.");
+
+ yield teardown(target);
+});
diff --git a/devtools/client/webaudioeditor/test/browser_webaudio-actor-automation-event.js b/devtools/client/webaudioeditor/test/browser_webaudio-actor-automation-event.js
new file mode 100644
index 000000000..9d542d5f0
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/browser_webaudio-actor-automation-event.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test that the WebAudioActor receives and emits the `automation-event` events
+ * with correct arguments from the content.
+ */
+
+add_task(function* () {
+ let { target, front } = yield initBackend(AUTOMATION_URL);
+ let events = [];
+
+ let expected = [
+ ["setValueAtTime", 0.2, 0],
+ ["linearRampToValueAtTime", 1, 0.3],
+ ["exponentialRampToValueAtTime", 0.75, 0.6],
+ ["setValueCurveAtTime", [-1, 0, 1], 0.7, 0.3],
+ ];
+
+ front.on("automation-event", onAutomationEvent);
+
+ let [_, __, [destNode, oscNode, gainNode], [connect1, connect2]] = yield Promise.all([
+ front.setup({ reload: true }),
+ once(front, "start-context"),
+ get3(front, "create-node"),
+ get2(front, "connect-node")
+ ]);
+
+ is(events.length, 4, "correct number of events fired");
+
+ function onAutomationEvent(e) {
+ let { eventName, paramName, args } = e;
+ let exp = expected[events.length];
+
+ is(eventName, exp[0], "correct eventName in event");
+ is(paramName, "frequency", "correct paramName in event");
+ is(args.length, exp.length - 1, "correct length in args");
+
+ args.forEach((a, i) => {
+ // In the case of an array
+ if (typeof a === "object") {
+ a.forEach((f, j) => is(f, exp[i + 1][j], `correct argument in Float32Array: ${f}`));
+ } else {
+ is(a, exp[i + 1], `correct ${i + 1}th argument in args: ${a}`);
+ }
+ });
+ events.push([eventName].concat(args));
+ }
+
+ front.off("automation-event", onAutomationEvent);
+ yield removeTab(target.tab);
+});
diff --git a/devtools/client/webaudioeditor/test/browser_webaudio-actor-connect-param.js b/devtools/client/webaudioeditor/test/browser_webaudio-actor-connect-param.js
new file mode 100644
index 000000000..2910d5bd1
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/browser_webaudio-actor-connect-param.js
@@ -0,0 +1,25 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test the `connect-param` event on the web audio actor.
+ */
+
+add_task(function* () {
+ let { target, front } = yield initBackend(CONNECT_PARAM_URL);
+ let [, , [destNode, carrierNode, modNode, gainNode], , connectParam] = yield Promise.all([
+ front.setup({ reload: true }),
+ once(front, "start-context"),
+ getN(front, "create-node", 4),
+ get2(front, "connect-node"),
+ once(front, "connect-param")
+ ]);
+
+ info(connectParam);
+
+ is(connectParam.source.actorID, modNode.actorID, "`connect-param` has correct actor for `source`");
+ is(connectParam.dest.actorID, gainNode.actorID, "`connect-param` has correct actor for `dest`");
+ is(connectParam.param, "gain", "`connect-param` has correct parameter name for `param`");
+
+ yield removeTab(target.tab);
+});
diff --git a/devtools/client/webaudioeditor/test/browser_webaudio-actor-destroy-node.js b/devtools/client/webaudioeditor/test/browser_webaudio-actor-destroy-node.js
new file mode 100644
index 000000000..e48836c3f
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/browser_webaudio-actor-destroy-node.js
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test `destroy-node` event on WebAudioActor.
+ */
+
+add_task(function* () {
+ let { target, front } = yield initBackend(DESTROY_NODES_URL);
+
+ let [, , created] = yield Promise.all([
+ front.setup({ reload: true }),
+ once(front, "start-context"),
+ // Should create dest, gain, and oscillator node and 10
+ // disposable buffer nodes
+ getN(front, "create-node", 13)
+ ]);
+
+ let waitUntilDestroyed = getN(front, "destroy-node", 10);
+
+ // Force CC so we can ensure it's run to clear out dead AudioNodes
+ forceNodeCollection();
+
+ let destroyed = yield waitUntilDestroyed;
+
+ destroyed.forEach((node, i) => {
+ ok(node.type, "AudioBufferSourceNode", "Only buffer nodes are destroyed");
+ ok(actorIsInList(created, destroyed[i]),
+ "`destroy-node` called only on AudioNodes in current document.");
+ });
+
+ yield removeTab(target.tab);
+});
+
+function actorIsInList(list, actor) {
+ for (let i = 0; i < list.length; i++) {
+ if (list[i].actorID === actor.actorID)
+ return list[i];
+ }
+ return null;
+}
diff --git a/devtools/client/webaudioeditor/test/browser_webaudio-actor-simple.js b/devtools/client/webaudioeditor/test/browser_webaudio-actor-simple.js
new file mode 100644
index 000000000..28ff75651
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/browser_webaudio-actor-simple.js
@@ -0,0 +1,30 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test basic communication of Web Audio actor
+ */
+
+add_task(function* () {
+ let { target, front } = yield initBackend(SIMPLE_CONTEXT_URL);
+ let [_, __, [destNode, oscNode, gainNode], [connect1, connect2]] = yield Promise.all([
+ front.setup({ reload: true }),
+ once(front, "start-context"),
+ get3(front, "create-node"),
+ get2(front, "connect-node")
+ ]);
+
+ is(destNode.type, "AudioDestinationNode", "WebAudioActor:create-node returns AudioNodeActor for AudioDestination");
+ is(oscNode.type, "OscillatorNode", "WebAudioActor:create-node returns AudioNodeActor");
+ is(gainNode.type, "GainNode", "WebAudioActor:create-node returns AudioNodeActor");
+
+ let { source, dest } = connect1;
+ is(source.actorID, oscNode.actorID, "WebAudioActor:connect-node returns correct actor with ID on source (osc->gain)");
+ is(dest.actorID, gainNode.actorID, "WebAudioActor:connect-node returns correct actor with ID on dest (osc->gain)");
+
+ ({ source, dest } = connect2);
+ is(source.actorID, gainNode.actorID, "WebAudioActor:connect-node returns correct actor with ID on source (gain->dest)");
+ is(dest.actorID, destNode.actorID, "WebAudioActor:connect-node returns correct actor with ID on dest (gain->dest)");
+
+ yield removeTab(target.tab);
+});
diff --git a/devtools/client/webaudioeditor/test/doc_automation.html b/devtools/client/webaudioeditor/test/doc_automation.html
new file mode 100644
index 000000000..6f074208c
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/doc_automation.html
@@ -0,0 +1,30 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Web Audio Editor test page</title>
+ </head>
+
+ <body>
+
+ <script type="text/javascript;version=1.8">
+ "use strict";
+
+ let ctx = new AudioContext();
+ let osc = ctx.createOscillator();
+ let gain = ctx.createGain();
+ gain.gain.value = 0;
+ osc.frequency.setValueAtTime(0.2, 0);
+ osc.frequency.linearRampToValueAtTime(1, 0.3);
+ osc.frequency.exponentialRampToValueAtTime(0.75, 0.6);
+ osc.frequency.setValueCurveAtTime(new Float32Array([-1, 0, 1]), 0.7, 0.3);
+ osc.connect(gain);
+ gain.connect(ctx.destination);
+ osc.start(0);
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/webaudioeditor/test/doc_buffer-and-array.html b/devtools/client/webaudioeditor/test/doc_buffer-and-array.html
new file mode 100644
index 000000000..ef4cec8b6
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/doc_buffer-and-array.html
@@ -0,0 +1,56 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Web Audio Editor test page</title>
+ </head>
+
+ <body>
+
+ <script type="text/javascript;version=1.8">
+ "use strict";
+
+ let audioURL = "http://example.com/browser/devtools/client/webaudioeditor/test/440hz_sine.ogg";
+
+ let ctx = new AudioContext();
+ let bufferNode = ctx.createBufferSource();
+ let shaperNode = ctx.createWaveShaper();
+ shaperNode.curve = generateWaveShapingCurve();
+
+ let xhr = getBuffer(audioURL, () => {
+ ctx.decodeAudioData(xhr.response, (buffer) => {
+ bufferNode.buffer = buffer;
+ bufferNode.connect(shaperNode);
+ shaperNode.connect(ctx.destination);
+ });
+ });
+
+ function generateWaveShapingCurve() {
+ let frames = 65536;
+ let curve = new Float32Array(frames);
+ let n = frames;
+ let n2 = n / 2;
+
+ for (let i = 0; i < n; ++i) {
+ let x = (i - n2) / n2;
+ let y = Math.atan(5 * x) / (0.5 * Math.PI);
+ }
+
+ return curve;
+ }
+
+ function getBuffer (url, callback) {
+ let xhr = new XMLHttpRequest();
+ xhr.open("GET", url, true);
+ xhr.responseType = "arraybuffer";
+ xhr.onload = callback;
+ xhr.send();
+ return xhr;
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/webaudioeditor/test/doc_bug_1112378.html b/devtools/client/webaudioeditor/test/doc_bug_1112378.html
new file mode 100644
index 000000000..ecdfd7d63
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/doc_bug_1112378.html
@@ -0,0 +1,57 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Web Audio Editor test page</title>
+ </head>
+
+ <body>
+
+ <script type="text/javascript;version=1.8">
+ "use strict";
+
+ let ctx = new AudioContext();
+ let osc = ctx.createOscillator();
+
+ function throwError () {
+ try {
+ osc.connect({});
+ } catch (e) {
+ return {
+ lineNumber: e.lineNumber,
+ fileName: e.fileName,
+ columnNumber: e.columnNumber,
+ message: e.message,
+ instanceof: e instanceof TypeError,
+ stringified: e.toString(),
+ name: e.name
+ }
+ }
+ }
+
+ function throwDOMException () {
+ try {
+ osc.frequency.setValueAtTime(0, -1);
+ } catch (e) {
+ return {
+ lineNumber: e.lineNumber,
+ columnNumber: e.columnNumber,
+ filename: e.filename,
+ message: e.message,
+ code: e.code,
+ result: e.result,
+ instanceof: e instanceof DOMException,
+ stringified: e.toString(),
+ name: e.name
+ }
+ }
+ }
+
+
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/webaudioeditor/test/doc_bug_1125817.html b/devtools/client/webaudioeditor/test/doc_bug_1125817.html
new file mode 100644
index 000000000..49a2be11a
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/doc_bug_1125817.html
@@ -0,0 +1,23 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Web Audio Editor test page</title>
+ </head>
+
+ <body>
+
+ <script type="text/javascript;version=1.8">
+ "use strict";
+
+ let ctx = new AudioContext();
+ let osc = ctx.createOscillator();
+ osc.frequency.value = 200;
+ osc.disconnect();
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/webaudioeditor/test/doc_bug_1130901.html b/devtools/client/webaudioeditor/test/doc_bug_1130901.html
new file mode 100644
index 000000000..1ce1ebf55
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/doc_bug_1130901.html
@@ -0,0 +1,22 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Web Audio Editor test page</title>
+ </head>
+
+ <body>
+
+ <script type="text/javascript;version=1.8">
+ "use strict";
+
+ let ctx = new AudioContext();
+ ctx.createOscillator.call(ctx);
+ ctx.createGain.apply(ctx, []);
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/webaudioeditor/test/doc_bug_1141261.html b/devtools/client/webaudioeditor/test/doc_bug_1141261.html
new file mode 100644
index 000000000..87c1210a4
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/doc_bug_1141261.html
@@ -0,0 +1,25 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Web Audio Editor test page</title>
+ </head>
+
+ <body>
+
+ <script type="text/javascript;version=1.8">
+ "use strict";
+
+ let ctx = new AudioContext();
+ let osc = ctx.createOscillator();
+ let gain = ctx.createGain();
+ osc.connect(gain);
+ gain.connect(ctx.destination);
+ gain.disconnect();
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/webaudioeditor/test/doc_complex-context.html b/devtools/client/webaudioeditor/test/doc_complex-context.html
new file mode 100644
index 000000000..396bbce3f
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/doc_complex-context.html
@@ -0,0 +1,44 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Web Audio Editor test page</title>
+ </head>
+
+ <body>
+
+ <script type="text/javascript;version=1.8">
+ "use strict";
+
+/*
+ ↱ proc
+ osc → gain →
+ osc → gain → destination
+ buffer →↳ filter →
+*/
+ let ctx = new AudioContext();
+ let osc1 = ctx.createOscillator();
+ let gain1 = ctx.createGain();
+ let proc = ctx.createScriptProcessor();
+ osc1.connect(gain1);
+ osc1.connect(proc);
+ gain1.connect(ctx.destination);
+
+ let osc2 = ctx.createOscillator();
+ let gain2 = ctx.createGain();
+ osc2.connect(gain2);
+ gain2.connect(ctx.destination);
+
+ let buf = ctx.createBufferSource();
+ let filter = ctx.createBiquadFilter();
+ buf.connect(filter);
+ osc2.connect(filter);
+ filter.connect(ctx.destination);
+
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/webaudioeditor/test/doc_connect-multi-param.html b/devtools/client/webaudioeditor/test/doc_connect-multi-param.html
new file mode 100644
index 000000000..ed4bd84e8
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/doc_connect-multi-param.html
@@ -0,0 +1,32 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Web Audio Editor test page</title>
+ </head>
+
+ <body>
+
+ <script type="text/javascript;version=1.8">
+ "use strict";
+
+ let ctx = new AudioContext();
+ let carrier = ctx.createOscillator();
+ let gain = ctx.createGain();
+ let modulator = ctx.createOscillator();
+ let modulator2 = ctx.createOscillator();
+ carrier.connect(gain);
+ gain.connect(ctx.destination);
+ modulator.connect(gain.gain);
+ modulator2.connect(carrier.frequency);
+ modulator2.connect(carrier.detune);
+ modulator.start(0);
+ modulator2.start(0);
+ carrier.start(0);
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/webaudioeditor/test/doc_connect-param.html b/devtools/client/webaudioeditor/test/doc_connect-param.html
new file mode 100644
index 000000000..9185c0b05
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/doc_connect-param.html
@@ -0,0 +1,28 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Web Audio Editor test page</title>
+ </head>
+
+ <body>
+
+ <script type="text/javascript;version=1.8">
+ "use strict";
+
+ let ctx = new AudioContext();
+ let carrier = ctx.createOscillator();
+ let modulator = ctx.createOscillator();
+ let gain = ctx.createGain();
+ carrier.connect(gain);
+ gain.connect(ctx.destination);
+ modulator.connect(gain.gain);
+ modulator.start(0);
+ carrier.start(0);
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/webaudioeditor/test/doc_destroy-nodes.html b/devtools/client/webaudioeditor/test/doc_destroy-nodes.html
new file mode 100644
index 000000000..98dfc9ad2
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/doc_destroy-nodes.html
@@ -0,0 +1,36 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Web Audio Editor test page</title>
+ </head>
+
+ <body>
+
+ <script type="text/javascript;version=1.8">
+ "use strict";
+ // Keep the nodes we want to GC alive until we are ready for them to
+ // be collected. We will zero this reference by force from the devtools
+ // side.
+ var keepAlive = [];
+ (function () {
+ let ctx = new AudioContext();
+ let osc = ctx.createOscillator();
+ let gain = ctx.createGain();
+
+ for (let i = 0; i < 10; i++) {
+ keepAlive.push(ctx.createBufferSource());
+ }
+
+ osc.connect(gain);
+ gain.connect(ctx.destination);
+ gain.gain.value = 0;
+ osc.start();
+ })();
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/webaudioeditor/test/doc_iframe-context.html b/devtools/client/webaudioeditor/test/doc_iframe-context.html
new file mode 100644
index 000000000..a0a411a47
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/doc_iframe-context.html
@@ -0,0 +1,14 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Web Audio Editor test page with an iframe</title>
+ </head>
+
+ <body>
+ <iframe id="frame" src="doc_simple-context.html" />
+ </body>
+</html>
diff --git a/devtools/client/webaudioeditor/test/doc_media-node-creation.html b/devtools/client/webaudioeditor/test/doc_media-node-creation.html
new file mode 100644
index 000000000..d88233034
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/doc_media-node-creation.html
@@ -0,0 +1,29 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Web Audio Editor test page</title>
+ </head>
+
+ <body>
+
+ <script type="text/javascript;version=1.8">
+ "use strict";
+
+ let ctx = new AudioContext();
+ let audio = new Audio();
+ let meNode, msNode, mdNode;
+ navigator.getUserMedia = navigator.getUserMedia || navigator.mozGetUserMedia;
+
+ navigator.getUserMedia({ audio: true, fake: true }, stream => {
+ meNode = ctx.createMediaElementSource(audio);
+ msNode = ctx.createMediaStreamSource(stream);
+ mdNode = ctx.createMediaStreamDestination();
+ }, () => {});
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/webaudioeditor/test/doc_simple-context.html b/devtools/client/webaudioeditor/test/doc_simple-context.html
new file mode 100644
index 000000000..89a84b882
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/doc_simple-context.html
@@ -0,0 +1,33 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Web Audio Editor test page</title>
+ </head>
+
+ <body>
+
+ <script type="text/javascript;version=1.8">
+ "use strict";
+
+ let ctx = new AudioContext();
+ let osc = ctx.createOscillator();
+ let gain = ctx.createGain();
+ gain.gain.value = 0;
+
+ // Connect multiple times to test that it's disregarded.
+ osc.connect(gain);
+ gain.connect(ctx.destination);
+ gain.connect(ctx.destination);
+ gain.connect(ctx.destination);
+ gain.connect(ctx.destination);
+ gain.connect(ctx.destination);
+ gain.connect(ctx.destination);
+ osc.start(0);
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/webaudioeditor/test/doc_simple-node-creation.html b/devtools/client/webaudioeditor/test/doc_simple-node-creation.html
new file mode 100644
index 000000000..e6dcf7b32
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/doc_simple-node-creation.html
@@ -0,0 +1,28 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Web Audio Editor test page</title>
+ </head>
+
+ <body>
+
+ <script type="text/javascript;version=1.8">
+ "use strict";
+
+ let ctx = new AudioContext();
+ let NODE_CREATION_METHODS = [
+ "createBufferSource", "createScriptProcessor", "createAnalyser",
+ "createGain", "createDelay", "createBiquadFilter", "createWaveShaper",
+ "createPanner", "createConvolver", "createChannelSplitter", "createChannelMerger",
+ "createDynamicsCompressor", "createOscillator", "createStereoPanner"
+ ];
+ let nodes = NODE_CREATION_METHODS.map(method => ctx[method]());
+
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/webaudioeditor/test/head.js b/devtools/client/webaudioeditor/test/head.js
new file mode 100644
index 000000000..7b0b0f01a
--- /dev/null
+++ b/devtools/client/webaudioeditor/test/head.js
@@ -0,0 +1,556 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+var { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+var { Task } = require("devtools/shared/task");
+var Services = require("Services");
+var { gDevTools } = require("devtools/client/framework/devtools");
+var { TargetFactory } = require("devtools/client/framework/target");
+var { DebuggerServer } = require("devtools/server/main");
+var { generateUUID } = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
+
+var Promise = require("promise");
+var Services = require("Services");
+var { WebAudioFront } = require("devtools/shared/fronts/webaudio");
+var DevToolsUtils = require("devtools/shared/DevToolsUtils");
+var flags = require("devtools/shared/flags");
+var audioNodes = require("devtools/server/actors/utils/audionodes.json");
+var mm = null;
+
+const FRAME_SCRIPT_UTILS_URL = "chrome://devtools/content/shared/frame-script-utils.js";
+const EXAMPLE_URL = "http://example.com/browser/devtools/client/webaudioeditor/test/";
+const SIMPLE_CONTEXT_URL = EXAMPLE_URL + "doc_simple-context.html";
+const COMPLEX_CONTEXT_URL = EXAMPLE_URL + "doc_complex-context.html";
+const SIMPLE_NODES_URL = EXAMPLE_URL + "doc_simple-node-creation.html";
+const MEDIA_NODES_URL = EXAMPLE_URL + "doc_media-node-creation.html";
+const BUFFER_AND_ARRAY_URL = EXAMPLE_URL + "doc_buffer-and-array.html";
+const DESTROY_NODES_URL = EXAMPLE_URL + "doc_destroy-nodes.html";
+const CONNECT_PARAM_URL = EXAMPLE_URL + "doc_connect-param.html";
+const CONNECT_MULTI_PARAM_URL = EXAMPLE_URL + "doc_connect-multi-param.html";
+const IFRAME_CONTEXT_URL = EXAMPLE_URL + "doc_iframe-context.html";
+const AUTOMATION_URL = EXAMPLE_URL + "doc_automation.html";
+
+// Enable logging for all the tests. Both the debugger server and frontend will
+// be affected by this pref.
+var gEnableLogging = Services.prefs.getBoolPref("devtools.debugger.log");
+Services.prefs.setBoolPref("devtools.debugger.log", false);
+
+// All tests are asynchronous.
+waitForExplicitFinish();
+
+var gToolEnabled = Services.prefs.getBoolPref("devtools.webaudioeditor.enabled");
+
+flags.testing = true;
+
+registerCleanupFunction(() => {
+ flags.testing = false;
+ info("finish() was called, cleaning up...");
+ Services.prefs.setBoolPref("devtools.debugger.log", gEnableLogging);
+ Services.prefs.setBoolPref("devtools.webaudioeditor.enabled", gToolEnabled);
+ Cu.forceGC();
+});
+
+/**
+ * Call manually in tests that use frame script utils after initializing
+ * the web audio editor. Call after init but before navigating to a different page.
+ */
+function loadFrameScripts() {
+ mm = gBrowser.selectedBrowser.messageManager;
+ mm.loadFrameScript(FRAME_SCRIPT_UTILS_URL, false);
+}
+
+function addTab(aUrl, aWindow) {
+ info("Adding tab: " + aUrl);
+
+ let deferred = Promise.defer();
+ let targetWindow = aWindow || window;
+ let targetBrowser = targetWindow.gBrowser;
+
+ targetWindow.focus();
+ let tab = targetBrowser.selectedTab = targetBrowser.addTab(aUrl);
+ let linkedBrowser = tab.linkedBrowser;
+
+ BrowserTestUtils.browserLoaded(linkedBrowser).then(function () {
+ info("Tab added and finished loading: " + aUrl);
+ deferred.resolve(tab);
+ });
+
+ return deferred.promise;
+}
+
+function removeTab(aTab, aWindow) {
+ info("Removing tab.");
+
+ let deferred = Promise.defer();
+ let targetWindow = aWindow || window;
+ let targetBrowser = targetWindow.gBrowser;
+ let tabContainer = targetBrowser.tabContainer;
+
+ tabContainer.addEventListener("TabClose", function onClose(aEvent) {
+ tabContainer.removeEventListener("TabClose", onClose, false);
+ info("Tab removed and finished closing.");
+ deferred.resolve();
+ }, false);
+
+ targetBrowser.removeTab(aTab);
+ return deferred.promise;
+}
+
+function once(aTarget, aEventName, aUseCapture = false) {
+ info("Waiting for event: '" + aEventName + "' on " + aTarget + ".");
+
+ let deferred = Promise.defer();
+
+ for (let [add, remove] of [
+ ["on", "off"], // Use event emitter before DOM events for consistency
+ ["addEventListener", "removeEventListener"],
+ ["addListener", "removeListener"]
+ ]) {
+ if ((add in aTarget) && (remove in aTarget)) {
+ aTarget[add](aEventName, function onEvent(...aArgs) {
+ aTarget[remove](aEventName, onEvent, aUseCapture);
+ info("Got event: '" + aEventName + "' on " + aTarget + ".");
+ deferred.resolve(...aArgs);
+ }, aUseCapture);
+ break;
+ }
+ }
+
+ return deferred.promise;
+}
+
+function reload(aTarget, aWaitForTargetEvent = "navigate") {
+ aTarget.activeTab.reload();
+ return once(aTarget, aWaitForTargetEvent);
+}
+
+function navigate(aTarget, aUrl, aWaitForTargetEvent = "navigate") {
+ executeSoon(() => aTarget.activeTab.navigateTo(aUrl));
+ return once(aTarget, aWaitForTargetEvent);
+}
+
+/**
+ * Call manually in tests that use frame script utils after initializing
+ * the shader editor. Call after init but before navigating to different pages.
+ */
+function loadFrameScripts() {
+ mm = gBrowser.selectedBrowser.messageManager;
+ mm.loadFrameScript(FRAME_SCRIPT_UTILS_URL, false);
+}
+
+/**
+ * Adds a new tab, and instantiate a WebAudiFront object.
+ * This requires calling removeTab before the test ends.
+ */
+function initBackend(aUrl) {
+ info("Initializing a web audio editor front.");
+
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+
+ return Task.spawn(function* () {
+ let tab = yield addTab(aUrl);
+ let target = TargetFactory.forTab(tab);
+
+ yield target.makeRemote();
+
+ let front = new WebAudioFront(target.client, target.form);
+ return { target, front };
+ });
+}
+
+/**
+ * Adds a new tab, and open the toolbox for that tab, selecting the audio editor
+ * panel.
+ * This requires calling teardown before the test ends.
+ */
+function initWebAudioEditor(aUrl) {
+ info("Initializing a web audio editor pane.");
+
+ return Task.spawn(function* () {
+ let tab = yield addTab(aUrl);
+ let target = TargetFactory.forTab(tab);
+
+ yield target.makeRemote();
+
+ Services.prefs.setBoolPref("devtools.webaudioeditor.enabled", true);
+ let toolbox = yield gDevTools.showToolbox(target, "webaudioeditor");
+ let panel = toolbox.getCurrentPanel();
+ return { target, panel, toolbox };
+ });
+}
+
+/**
+ * Close the toolbox, destroying all panels, and remove the added test tabs.
+ */
+function teardown(aTarget) {
+ info("Destroying the web audio editor.");
+
+ return gDevTools.closeToolbox(aTarget).then(() => {
+ while (gBrowser.tabs.length > 1) {
+ gBrowser.removeCurrentTab();
+ }
+ });
+}
+
+// Due to web audio will fire most events synchronously back-to-back,
+// and we can't yield them in a chain without missing actors, this allows
+// us to listen for `n` events and return a promise resolving to them.
+//
+// Takes a `front` object that is an event emitter, the number of
+// programs that should be listened to and waited on, and an optional
+// `onAdd` function that calls with the entire actors array on program link
+function getN(front, eventName, count, spread) {
+ let actors = [];
+ let deferred = Promise.defer();
+ info(`Waiting for ${count} ${eventName} events`);
+ front.on(eventName, function onEvent(...args) {
+ let actor = args[0];
+ if (actors.length !== count) {
+ actors.push(spread ? args : actor);
+ }
+ info(`Got ${actors.length} / ${count} ${eventName} events`);
+ if (actors.length === count) {
+ front.off(eventName, onEvent);
+ deferred.resolve(actors);
+ }
+ });
+ return deferred.promise;
+}
+
+function get(front, eventName) { return getN(front, eventName, 1); }
+function get2(front, eventName) { return getN(front, eventName, 2); }
+function get3(front, eventName) { return getN(front, eventName, 3); }
+function getSpread(front, eventName) { return getN(front, eventName, 1, true); }
+function get2Spread(front, eventName) { return getN(front, eventName, 2, true); }
+function get3Spread(front, eventName) { return getN(front, eventName, 3, true); }
+function getNSpread(front, eventName, count) { return getN(front, eventName, count, true); }
+
+/**
+ * Waits for the UI_GRAPH_RENDERED event to fire, but only
+ * resolves when the graph was rendered with the correct count of
+ * nodes and edges.
+ */
+function waitForGraphRendered(front, nodeCount, edgeCount, paramEdgeCount) {
+ let deferred = Promise.defer();
+ let eventName = front.EVENTS.UI_GRAPH_RENDERED;
+ info(`Wait for graph rendered with ${nodeCount} nodes, ${edgeCount} edges`);
+ front.on(eventName, function onGraphRendered(_, nodes, edges, pEdges) {
+ let paramEdgesDone = paramEdgeCount != null ? paramEdgeCount === pEdges : true;
+ info(`Got graph rendered with ${nodes} / ${nodeCount} nodes, ` +
+ `${edges} / ${edgeCount} edges`);
+ if (nodes === nodeCount && edges === edgeCount && paramEdgesDone) {
+ front.off(eventName, onGraphRendered);
+ deferred.resolve();
+ }
+ });
+ return deferred.promise;
+}
+
+function checkVariableView(view, index, hash, description = "") {
+ info("Checking Variable View");
+ let scope = view.getScopeAtIndex(index);
+ let variables = Object.keys(hash);
+
+ // If node shouldn't display any properties, ensure that the 'empty' message is
+ // visible
+ if (!variables.length) {
+ ok(isVisible(scope.window.$("#properties-empty")),
+ description + " should show the empty properties tab.");
+ return;
+ }
+
+ // Otherwise, iterate over expected properties
+ variables.forEach(variable => {
+ let aVar = scope.get(variable);
+ is(aVar.target.querySelector(".name").getAttribute("value"), variable,
+ "Correct property name for " + variable);
+ let value = aVar.target.querySelector(".value").getAttribute("value");
+
+ // Cast value with JSON.parse if possible;
+ // will fail when displaying Object types like "ArrayBuffer"
+ // and "Float32Array", but will match the original value.
+ try {
+ value = JSON.parse(value);
+ }
+ catch (e) {}
+ if (typeof hash[variable] === "function") {
+ ok(hash[variable](value),
+ "Passing property value of " + value + " for " + variable + " " + description);
+ }
+ else {
+ is(value, hash[variable],
+ "Correct property value of " + hash[variable] + " for " + variable + " " + description);
+ }
+ });
+}
+
+function modifyVariableView(win, view, index, prop, value) {
+ let deferred = Promise.defer();
+ let scope = view.getScopeAtIndex(index);
+ let aVar = scope.get(prop);
+ scope.expand();
+
+ win.on(win.EVENTS.UI_SET_PARAM, handleSetting);
+ win.on(win.EVENTS.UI_SET_PARAM_ERROR, handleSetting);
+
+ // Focus and select the variable to begin editing
+ win.focus();
+ aVar.focus();
+ EventUtils.sendKey("RETURN", win);
+
+ // Must wait for the scope DOM to be available to receive
+ // events
+ executeSoon(() => {
+ info("Setting " + value + " for " + prop + "....");
+ for (let c of (value + "")) {
+ EventUtils.synthesizeKey(c, {}, win);
+ }
+ EventUtils.sendKey("RETURN", win);
+ });
+
+ function handleSetting(eventName) {
+ win.off(win.EVENTS.UI_SET_PARAM, handleSetting);
+ win.off(win.EVENTS.UI_SET_PARAM_ERROR, handleSetting);
+ if (eventName === win.EVENTS.UI_SET_PARAM)
+ deferred.resolve();
+ if (eventName === win.EVENTS.UI_SET_PARAM_ERROR)
+ deferred.reject();
+ }
+
+ return deferred.promise;
+}
+
+function findGraphEdge(win, source, target, param) {
+ let selector = ".edgePaths .edgePath[data-source='" + source + "'][data-target='" + target + "']";
+ if (param) {
+ selector += "[data-param='" + param + "']";
+ }
+ return win.document.querySelector(selector);
+}
+
+function findGraphNode(win, node) {
+ let selector = ".nodes > g[data-id='" + node + "']";
+ return win.document.querySelector(selector);
+}
+
+function click(win, element) {
+ EventUtils.sendMouseEvent({ type: "click" }, element, win);
+}
+
+function mouseOver(win, element) {
+ EventUtils.sendMouseEvent({ type: "mouseover" }, element, win);
+}
+
+function command(button) {
+ let ev = button.ownerDocument.createEvent("XULCommandEvent");
+ ev.initCommandEvent("command", true, true, button.ownerDocument.defaultView, 0, false, false, false, false, null);
+ button.dispatchEvent(ev);
+}
+
+function isVisible(element) {
+ return !element.getAttribute("hidden");
+}
+
+/**
+ * Used in debugging, returns a promise that resolves in `n` milliseconds.
+ */
+function wait(n) {
+ let { promise, resolve } = Promise.defer();
+ setTimeout(resolve, n);
+ info("Waiting " + n / 1000 + " seconds.");
+ return promise;
+}
+
+/**
+ * Clicks a graph node based on actorID or passing in an element.
+ * Returns a promise that resolves once UI_INSPECTOR_NODE_SET is fired and
+ * the tabs have rendered, completing all RDP requests for the node.
+ */
+function clickGraphNode(panelWin, el, waitForToggle = false) {
+ let { promise, resolve } = Promise.defer();
+ let promises = [
+ once(panelWin, panelWin.EVENTS.UI_INSPECTOR_NODE_SET),
+ once(panelWin, panelWin.EVENTS.UI_PROPERTIES_TAB_RENDERED),
+ once(panelWin, panelWin.EVENTS.UI_AUTOMATION_TAB_RENDERED)
+ ];
+
+ if (waitForToggle) {
+ promises.push(once(panelWin, panelWin.EVENTS.UI_INSPECTOR_TOGGLED));
+ }
+
+ // Use `el` as the element if it is one, otherwise
+ // assume it's an ID and find the related graph node
+ let element = el.tagName ? el : findGraphNode(panelWin, el);
+ click(panelWin, element);
+
+ return Promise.all(promises);
+}
+
+/**
+ * Returns the primitive value of a grip's value, or the
+ * original form that the string grip.type comes from.
+ */
+function getGripValue(value) {
+ if (~["boolean", "string", "number"].indexOf(typeof value)) {
+ return value;
+ }
+
+ switch (value.type) {
+ case "undefined": return undefined;
+ case "Infinity": return Infinity;
+ case "-Infinity": return -Infinity;
+ case "NaN": return NaN;
+ case "-0": return -0;
+ case "null": return null;
+ default: return value;
+ }
+}
+
+/**
+ * Counts how many nodes and edges are currently in the graph.
+ */
+function countGraphObjects(win) {
+ return {
+ nodes: win.document.querySelectorAll(".nodes > .audionode").length,
+ edges: win.document.querySelectorAll(".edgePaths > .edgePath").length
+ };
+}
+
+/**
+* Forces cycle collection and GC, used in AudioNode destruction tests.
+*/
+function forceNodeCollection() {
+ ContentTask.spawn(gBrowser.selectedBrowser, {}, function*() {
+ // Kill the reference keeping stuff alive.
+ content.wrappedJSObject.keepAlive = null;
+
+ // Collect the now-deceased nodes.
+ Cu.forceGC();
+ Cu.forceCC();
+ Cu.forceGC();
+ Cu.forceCC();
+ });
+}
+
+/**
+ * Takes a `values` array of automation value entries,
+ * looking for the value at `time` seconds, checking
+ * to see if the value is close to `expected`.
+ */
+function checkAutomationValue(values, time, expected) {
+ // Remain flexible on values as we can approximate points
+ let EPSILON = 0.01;
+
+ let value = getValueAt(values, time);
+ ok(Math.abs(value - expected) < EPSILON, "Timeline value at " + time + " with value " + value + " should have value very close to " + expected);
+
+ /**
+ * Entries are ordered in `values` according to time, so if we can't find an exact point
+ * on a time of interest, return the point in between the threshold. This should
+ * get us a very close value.
+ */
+ function getValueAt(values, time) {
+ for (let i = 0; i < values.length; i++) {
+ if (values[i].delta === time) {
+ return values[i].value;
+ }
+ if (values[i].delta > time) {
+ return (values[i - 1].value + values[i].value) / 2;
+ }
+ }
+ return values[values.length - 1].value;
+ }
+}
+
+/**
+ * Wait for all inspector tabs to complete rendering.
+ */
+function waitForInspectorRender(panelWin, EVENTS) {
+ return Promise.all([
+ once(panelWin, EVENTS.UI_PROPERTIES_TAB_RENDERED),
+ once(panelWin, EVENTS.UI_AUTOMATION_TAB_RENDERED)
+ ]);
+}
+
+/**
+ * Takes a string `script` and evaluates it directly in the content
+ * in potentially a different process.
+ */
+function evalInDebuggee(script) {
+ let deferred = Promise.defer();
+
+ if (!mm) {
+ throw new Error("`loadFrameScripts()` must be called when using MessageManager.");
+ }
+
+ let id = generateUUID().toString();
+ mm.sendAsyncMessage("devtools:test:eval", { script: script, id: id });
+ mm.addMessageListener("devtools:test:eval:response", handler);
+
+ function handler({ data }) {
+ if (id !== data.id) {
+ return;
+ }
+
+ mm.removeMessageListener("devtools:test:eval:response", handler);
+ deferred.resolve(data.value);
+ }
+
+ return deferred.promise;
+}
+
+/**
+ * Takes an AudioNode type and returns it's properties (from audionode.json)
+ * as keys and their default values as keys
+ */
+function nodeDefaultValues(nodeName) {
+ let fn = NODE_CONSTRUCTORS[nodeName];
+
+ if (typeof fn === "undefined") return {};
+
+ let init = nodeName === "AudioDestinationNode" ? "destination" : `create${fn}()`;
+
+ let definition = JSON.stringify(audioNodes[nodeName].properties);
+
+ let evalNode = evalInDebuggee(`
+ let ins = (new AudioContext()).${init};
+ let props = ${definition};
+ let answer = {};
+
+ for(let k in props) {
+ if (props[k].param) {
+ answer[k] = ins[k].defaultValue;
+ } else if (typeof ins[k] === "object" && ins[k] !== null) {
+ answer[k] = ins[k].toString().slice(8, -1);
+ } else {
+ answer[k] = ins[k];
+ }
+ }
+ answer;`);
+
+ return evalNode;
+}
+
+const NODE_CONSTRUCTORS = {
+ "MediaStreamAudioDestinationNode": "MediaStreamDestination",
+ "AudioBufferSourceNode": "BufferSource",
+ "ScriptProcessorNode": "ScriptProcessor",
+ "AnalyserNode": "Analyser",
+ "GainNode": "Gain",
+ "DelayNode": "Delay",
+ "BiquadFilterNode": "BiquadFilter",
+ "WaveShaperNode": "WaveShaper",
+ "PannerNode": "Panner",
+ "ConvolverNode": "Convolver",
+ "ChannelSplitterNode": "ChannelSplitter",
+ "ChannelMergerNode": "ChannelMerger",
+ "DynamicsCompressorNode": "DynamicsCompressor",
+ "OscillatorNode": "Oscillator",
+ "StereoPannerNode": "StereoPanner"
+};
diff --git a/devtools/client/webaudioeditor/views/automation.js b/devtools/client/webaudioeditor/views/automation.js
new file mode 100644
index 000000000..2fab262bd
--- /dev/null
+++ b/devtools/client/webaudioeditor/views/automation.js
@@ -0,0 +1,159 @@
+/* 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";
+
+/**
+ * Functions handling the audio node inspector UI.
+ */
+
+var AutomationView = {
+
+ /**
+ * Initialization function called when the tool starts up.
+ */
+ initialize: function () {
+ this._buttons = $("#automation-param-toolbar-buttons");
+ this.graph = new LineGraphWidget($("#automation-graph"), { avg: false });
+ this.graph.selectionEnabled = false;
+
+ this._onButtonClick = this._onButtonClick.bind(this);
+ this._onNodeSet = this._onNodeSet.bind(this);
+ this._onResize = this._onResize.bind(this);
+
+ this._buttons.addEventListener("click", this._onButtonClick);
+ window.on(EVENTS.UI_INSPECTOR_RESIZE, this._onResize);
+ window.on(EVENTS.UI_INSPECTOR_NODE_SET, this._onNodeSet);
+ },
+
+ /**
+ * Destruction function called when the tool cleans up.
+ */
+ destroy: function () {
+ this._buttons.removeEventListener("click", this._onButtonClick);
+ window.off(EVENTS.UI_INSPECTOR_RESIZE, this._onResize);
+ window.off(EVENTS.UI_INSPECTOR_NODE_SET, this._onNodeSet);
+ },
+
+ /**
+ * Empties out the props view.
+ */
+ resetUI: function () {
+ this._currentNode = null;
+ },
+
+ /**
+ * On a new node selection, create the Automation panel for
+ * that specific node.
+ */
+ build: Task.async(function* () {
+ let node = this._currentNode;
+
+ let props = yield node.getParams();
+ let params = props.filter(({ flags }) => flags && flags.param);
+
+ this._createParamButtons(params);
+
+ this._selectedParamName = params[0] ? params[0].param : null;
+ this.render();
+ }),
+
+ /**
+ * Renders the graph for specified `paramName`. Called when
+ * the parameter view is changed, or when new param data events
+ * are fired for the currently specified param.
+ */
+ render: Task.async(function* () {
+ let node = this._currentNode;
+ let paramName = this._selectedParamName;
+ // Escape if either node or parameter name does not exist.
+ if (!node || !paramName) {
+ this._setState("no-params");
+ window.emit(EVENTS.UI_AUTOMATION_TAB_RENDERED, null);
+ return;
+ }
+
+ let { values, events } = yield node.getAutomationData(paramName);
+ this._setState(events.length ? "show" : "no-events");
+ yield this.graph.setDataWhenReady(values);
+ window.emit(EVENTS.UI_AUTOMATION_TAB_RENDERED, node.id);
+ }),
+
+ /**
+ * Create the buttons for each AudioParam, that when clicked,
+ * render the graph for that AudioParam.
+ */
+ _createParamButtons: function (params) {
+ this._buttons.innerHTML = "";
+ params.forEach((param, i) => {
+ let button = document.createElement("toolbarbutton");
+ button.setAttribute("class", "devtools-toolbarbutton automation-param-button");
+ button.setAttribute("data-param", param.param);
+ // Set label to the parameter name, should not be L10N'd
+ button.setAttribute("label", param.param);
+
+ // If first button, set to 'selected' for styling
+ if (i === 0) {
+ button.setAttribute("selected", true);
+ }
+
+ this._buttons.appendChild(button);
+ });
+ },
+
+ /**
+ * Internally sets the current audio node and rebuilds appropriate
+ * views.
+ */
+ _setAudioNode: function (node) {
+ this._currentNode = node;
+ if (this._currentNode) {
+ this.build();
+ }
+ },
+
+ /**
+ * Toggles the subviews to display messages whether or not
+ * the audio node has no AudioParams, no automation events, or
+ * shows the graph.
+ */
+ _setState: function (state) {
+ let contentView = $("#automation-content");
+ let emptyView = $("#automation-empty");
+
+ let graphView = $("#automation-graph-container");
+ let noEventsView = $("#automation-no-events");
+
+ contentView.hidden = state === "no-params";
+ emptyView.hidden = state !== "no-params";
+
+ graphView.hidden = state !== "show";
+ noEventsView.hidden = state !== "no-events";
+ },
+
+ /**
+ * Event handlers
+ */
+
+ _onButtonClick: function (e) {
+ Array.forEach($$(".automation-param-button"), $btn => $btn.removeAttribute("selected"));
+ let paramName = e.target.getAttribute("data-param");
+ e.target.setAttribute("selected", true);
+ this._selectedParamName = paramName;
+ this.render();
+ },
+
+ /**
+ * Called when the inspector is resized.
+ */
+ _onResize: function () {
+ this.graph.refresh();
+ },
+
+ /**
+ * Called when the inspector view determines a node is selected.
+ */
+ _onNodeSet: function (_, id) {
+ this._setAudioNode(id != null ? gAudioNodes.get(id) : null);
+ }
+};
diff --git a/devtools/client/webaudioeditor/views/context.js b/devtools/client/webaudioeditor/views/context.js
new file mode 100644
index 000000000..69ecc141e
--- /dev/null
+++ b/devtools/client/webaudioeditor/views/context.js
@@ -0,0 +1,314 @@
+/* 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";
+
+/* import-globals-from ../includes.js */
+
+const { debounce } = require("sdk/lang/functional");
+const flags = require("devtools/shared/flags");
+
+// Globals for d3 stuff
+// Default properties of the graph on rerender
+const GRAPH_DEFAULTS = {
+ translate: [20, 20],
+ scale: 1
+};
+
+// Sizes of SVG arrows in graph
+const ARROW_HEIGHT = 5;
+const ARROW_WIDTH = 8;
+
+// Styles for markers as they cannot be done with CSS.
+const MARKER_STYLING = {
+ light: "#AAA",
+ dark: "#CED3D9"
+};
+Object.defineProperty(this, "MARKER_STYLING", {
+ value: MARKER_STYLING,
+ enumerable: true,
+ writable: false
+});
+
+const GRAPH_DEBOUNCE_TIMER = 100;
+
+// `gAudioNodes` events that should require the graph
+// to redraw
+const GRAPH_REDRAW_EVENTS = ["add", "connect", "disconnect", "remove"];
+
+/**
+ * Functions handling the graph UI.
+ */
+var ContextView = {
+ /**
+ * Initialization function, called when the tool is started.
+ */
+ initialize: function () {
+ this._onGraphClick = this._onGraphClick.bind(this);
+ this._onThemeChange = this._onThemeChange.bind(this);
+ this._onStartContext = this._onStartContext.bind(this);
+ this._onEvent = this._onEvent.bind(this);
+
+ this.draw = debounce(this.draw.bind(this), GRAPH_DEBOUNCE_TIMER);
+ $("#graph-target").addEventListener("click", this._onGraphClick, false);
+
+ window.on(EVENTS.THEME_CHANGE, this._onThemeChange);
+ window.on(EVENTS.START_CONTEXT, this._onStartContext);
+ gAudioNodes.on("*", this._onEvent);
+ },
+
+ /**
+ * Destruction function, called when the tool is closed.
+ */
+ destroy: function () {
+ // If the graph was rendered at all, then the handler
+ // for zooming in will be set. We must remove it to prevent leaks.
+ if (this._zoomBinding) {
+ this._zoomBinding.on("zoom", null);
+ }
+ $("#graph-target").removeEventListener("click", this._onGraphClick, false);
+
+ window.off(EVENTS.THEME_CHANGE, this._onThemeChange);
+ window.off(EVENTS.START_CONTEXT, this._onStartContext);
+ gAudioNodes.off("*", this._onEvent);
+ },
+
+ /**
+ * Called when a page is reloaded and waiting for a "start-context" event
+ * and clears out old content
+ */
+ resetUI: function () {
+ this.clearGraph();
+ this.resetGraphTransform();
+ },
+
+ /**
+ * Clears out the rendered graph, called when resetting the SVG elements to draw again,
+ * or when resetting the entire UI tool
+ */
+ clearGraph: function () {
+ $("#graph-target").innerHTML = "";
+ },
+
+ /**
+ * Moves the graph back to its original scale and translation.
+ */
+ resetGraphTransform: function () {
+ // Only reset if the graph was ever drawn.
+ if (this._zoomBinding) {
+ let { translate, scale } = GRAPH_DEFAULTS;
+ // Must set the `zoomBinding` so the next `zoom` event is in sync with
+ // where the graph is visually (set by the `transform` attribute).
+ this._zoomBinding.scale(scale);
+ this._zoomBinding.translate(translate);
+ d3.select("#graph-target")
+ .attr("transform", "translate(" + translate + ") scale(" + scale + ")");
+ }
+ },
+
+ getCurrentScale: function () {
+ return this._zoomBinding ? this._zoomBinding.scale() : null;
+ },
+
+ getCurrentTranslation: function () {
+ return this._zoomBinding ? this._zoomBinding.translate() : null;
+ },
+
+ /**
+ * Makes the corresponding graph node appear "focused", removing
+ * focused styles from all other nodes. If no `actorID` specified,
+ * make all nodes appear unselected.
+ */
+ focusNode: function (actorID) {
+ // Remove class "selected" from all nodes
+ Array.forEach($$(".nodes > g"), $node => $node.classList.remove("selected"));
+ // Add to "selected"
+ if (actorID) {
+ this._getNodeByID(actorID).classList.add("selected");
+ }
+ },
+
+ /**
+ * Takes an actorID and returns the corresponding DOM SVG element in the graph
+ */
+ _getNodeByID: function (actorID) {
+ return $(".nodes > g[data-id='" + actorID + "']");
+ },
+
+ /**
+ * Sets the appropriate class on an SVG node when its bypass
+ * status is toggled.
+ */
+ _bypassNode: function (node, enabled) {
+ let el = this._getNodeByID(node.id);
+ el.classList[enabled ? "add" : "remove"]("bypassed");
+ },
+
+ /**
+ * This method renders the nodes currently available in `gAudioNodes` and is
+ * throttled to be called at most every `GRAPH_DEBOUNCE_TIMER` milliseconds.
+ * It's called whenever the audio context routing changes, after being debounced.
+ */
+ draw: function () {
+ // Clear out previous SVG information
+ this.clearGraph();
+
+ let graph = new dagreD3.Digraph();
+ let renderer = new dagreD3.Renderer();
+ gAudioNodes.populateGraph(graph);
+
+ // Post-render manipulation of the nodes
+ let oldDrawNodes = renderer.drawNodes();
+ renderer.drawNodes(function (graph, root) {
+ let svgNodes = oldDrawNodes(graph, root);
+ svgNodes.each(function (n) {
+ let node = graph.node(n);
+ let classString = "audionode type-" + node.type + (node.bypassed ? " bypassed" : "");
+ this.setAttribute("class", classString);
+ this.setAttribute("data-id", node.id);
+ this.setAttribute("data-type", node.type);
+ });
+ return svgNodes;
+ });
+
+ // Post-render manipulation of edges
+ let oldDrawEdgePaths = renderer.drawEdgePaths();
+ let defaultClasses = "edgePath enter";
+
+ renderer.drawEdgePaths(function (graph, root) {
+ let svgEdges = oldDrawEdgePaths(graph, root);
+ svgEdges.each(function (e) {
+ let edge = graph.edge(e);
+
+ // We have to manually specify the default classes on the edges
+ // as to not overwrite them
+ let edgeClass = defaultClasses + (edge.param ? (" param-connection " + edge.param) : "");
+
+ this.setAttribute("data-source", edge.source);
+ this.setAttribute("data-target", edge.target);
+ this.setAttribute("data-param", edge.param ? edge.param : null);
+ this.setAttribute("class", edgeClass);
+ });
+
+ return svgEdges;
+ });
+
+ // Override Dagre-d3's post render function by passing in our own.
+ // This way we can leave styles out of it.
+ renderer.postRender((graph, root) => {
+ // We have to manually set the marker styling since we cannot
+ // do this currently with CSS, although it is in spec for SVG2
+ // https://svgwg.org/svg2-draft/painting.html#VertexMarkerProperties
+ // For now, manually set it on creation, and the `_onThemeChange`
+ // function will fire when the devtools theme changes to update the
+ // styling manually.
+ let theme = Services.prefs.getCharPref("devtools.theme");
+ let markerColor = MARKER_STYLING[theme];
+ if (graph.isDirected() && root.select("#arrowhead").empty()) {
+ root
+ .append("svg:defs")
+ .append("svg:marker")
+ .attr("id", "arrowhead")
+ .attr("viewBox", "0 0 10 10")
+ .attr("refX", ARROW_WIDTH)
+ .attr("refY", ARROW_HEIGHT)
+ .attr("markerUnits", "strokewidth")
+ .attr("markerWidth", ARROW_WIDTH)
+ .attr("markerHeight", ARROW_HEIGHT)
+ .attr("orient", "auto")
+ .attr("style", "fill: " + markerColor)
+ .append("svg:path")
+ .attr("d", "M 0 0 L 10 5 L 0 10 z");
+ }
+
+ // Reselect the previously selected audio node
+ let currentNode = InspectorView.getCurrentAudioNode();
+ if (currentNode) {
+ this.focusNode(currentNode.id);
+ }
+
+ // Fire an event upon completed rendering, with extra information
+ // if in testing mode only.
+ let info = {};
+ if (flags.testing) {
+ info = gAudioNodes.getInfo();
+ }
+ window.emit(EVENTS.UI_GRAPH_RENDERED, info.nodes, info.edges, info.paramEdges);
+ });
+
+ let layout = dagreD3.layout().rankDir("LR");
+ renderer.layout(layout).run(graph, d3.select("#graph-target"));
+
+ // Handle the sliding and zooming of the graph,
+ // store as `this._zoomBinding` so we can unbind during destruction
+ if (!this._zoomBinding) {
+ this._zoomBinding = d3.behavior.zoom().on("zoom", function () {
+ var ev = d3.event;
+ d3.select("#graph-target")
+ .attr("transform", "translate(" + ev.translate + ") scale(" + ev.scale + ")");
+ });
+ d3.select("svg").call(this._zoomBinding);
+
+ // Set initial translation and scale -- this puts D3's awareness of
+ // the graph in sync with what the user sees originally.
+ this.resetGraphTransform();
+ }
+ },
+
+ /**
+ * Event handlers
+ */
+
+ /**
+ * Called once "start-context" is fired, indicating that there is an audio
+ * context being created to view so render the graph.
+ */
+ _onStartContext: function () {
+ this.draw();
+ },
+
+ /**
+ * Called when `gAudioNodes` fires an event -- most events (listed
+ * in GRAPH_REDRAW_EVENTS) qualify as a redraw event.
+ */
+ _onEvent: function (eventName, ...args) {
+ // If bypassing, just toggle the class on the SVG node
+ // rather than rerendering everything
+ if (eventName === "bypass") {
+ this._bypassNode.apply(this, args);
+ }
+ if (~GRAPH_REDRAW_EVENTS.indexOf(eventName)) {
+ this.draw();
+ }
+ },
+
+ /**
+ * Fired when the devtools theme changes.
+ */
+ _onThemeChange: function (eventName, theme) {
+ let markerColor = MARKER_STYLING[theme];
+ let marker = $("#arrowhead");
+ if (marker) {
+ marker.setAttribute("style", "fill: " + markerColor);
+ }
+ },
+
+ /**
+ * Fired when a click occurs in the graph.
+ *
+ * @param Event e
+ * Click event.
+ */
+ _onGraphClick: function (e) {
+ let node = findGraphNodeParent(e.target);
+ // If node not found (clicking outside of an audio node in the graph),
+ // then ignore this event
+ if (!node)
+ return;
+
+ let id = node.getAttribute("data-id");
+
+ this.focusNode(id);
+ window.emit(EVENTS.UI_SELECT_NODE, id);
+ }
+};
diff --git a/devtools/client/webaudioeditor/views/inspector.js b/devtools/client/webaudioeditor/views/inspector.js
new file mode 100644
index 000000000..1f50bb137
--- /dev/null
+++ b/devtools/client/webaudioeditor/views/inspector.js
@@ -0,0 +1,189 @@
+/* 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";
+
+/* import-globals-from ../includes.js */
+
+const MIN_INSPECTOR_WIDTH = 300;
+
+// Strings for rendering
+const EXPAND_INSPECTOR_STRING = L10N.getStr("expandInspector");
+const COLLAPSE_INSPECTOR_STRING = L10N.getStr("collapseInspector");
+
+/**
+ * Functions handling the audio node inspector UI.
+ */
+
+var InspectorView = {
+ _currentNode: null,
+
+ // Set up config for view toggling
+ _collapseString: COLLAPSE_INSPECTOR_STRING,
+ _expandString: EXPAND_INSPECTOR_STRING,
+ _toggleEvent: EVENTS.UI_INSPECTOR_TOGGLED,
+ _animated: true,
+ _delayed: true,
+
+ /**
+ * Initialization function called when the tool starts up.
+ */
+ initialize: function () {
+ // Set up view controller
+ this.el = $("#web-audio-inspector");
+ this.splitter = $("#inspector-splitter");
+ this.el.setAttribute("width", Services.prefs.getIntPref("devtools.webaudioeditor.inspectorWidth"));
+ this.button = $("#inspector-pane-toggle");
+ mixin(this, ToggleMixin);
+ this.bindToggle();
+
+ // Hide inspector view on startup
+ this.hideImmediately();
+
+ this._onNodeSelect = this._onNodeSelect.bind(this);
+ this._onDestroyNode = this._onDestroyNode.bind(this);
+ this._onResize = this._onResize.bind(this);
+ this._onCommandClick = this._onCommandClick.bind(this);
+
+ this.splitter.addEventListener("mouseup", this._onResize);
+ for (let $el of $$("#audio-node-toolbar toolbarbutton")) {
+ $el.addEventListener("command", this._onCommandClick);
+ }
+ window.on(EVENTS.UI_SELECT_NODE, this._onNodeSelect);
+ gAudioNodes.on("remove", this._onDestroyNode);
+ },
+
+ /**
+ * Destruction function called when the tool cleans up.
+ */
+ destroy: function () {
+ this.unbindToggle();
+ this.splitter.removeEventListener("mouseup", this._onResize);
+
+ $("#audio-node-toolbar toolbarbutton").removeEventListener("command", this._onCommandClick);
+ for (let $el of $$("#audio-node-toolbar toolbarbutton")) {
+ $el.removeEventListener("command", this._onCommandClick);
+ }
+ window.off(EVENTS.UI_SELECT_NODE, this._onNodeSelect);
+ gAudioNodes.off("remove", this._onDestroyNode);
+
+ this.el = null;
+ this.button = null;
+ this.splitter = null;
+ },
+
+ /**
+ * Takes a AudioNodeView `node` and sets it as the current
+ * node and scaffolds the inspector view based off of the new node.
+ */
+ setCurrentAudioNode: Task.async(function* (node) {
+ this._currentNode = node || null;
+
+ // If no node selected, set the inspector back to "no AudioNode selected"
+ // view.
+ if (!node) {
+ $("#web-audio-editor-details-pane-empty").removeAttribute("hidden");
+ $("#web-audio-editor-tabs").setAttribute("hidden", "true");
+ window.emit(EVENTS.UI_INSPECTOR_NODE_SET, null);
+ }
+ // Otherwise load up the tabs view and hide the empty placeholder
+ else {
+ $("#web-audio-editor-details-pane-empty").setAttribute("hidden", "true");
+ $("#web-audio-editor-tabs").removeAttribute("hidden");
+ this._buildToolbar();
+ window.emit(EVENTS.UI_INSPECTOR_NODE_SET, this._currentNode.id);
+ }
+ }),
+
+ /**
+ * Returns the current AudioNodeView.
+ */
+ getCurrentAudioNode: function () {
+ return this._currentNode;
+ },
+
+ /**
+ * Empties out the props view.
+ */
+ resetUI: function () {
+ // Set current node to empty to load empty view
+ this.setCurrentAudioNode();
+
+ // Reset AudioNode inspector and hide
+ this.hideImmediately();
+ },
+
+ _buildToolbar: function () {
+ let node = this.getCurrentAudioNode();
+
+ let bypassable = node.bypassable;
+ let bypassed = node.isBypassed();
+ let button = $("#audio-node-toolbar .bypass");
+
+ if (!bypassable) {
+ button.setAttribute("disabled", true);
+ } else {
+ button.removeAttribute("disabled");
+ }
+
+ if (!bypassable || bypassed) {
+ button.removeAttribute("checked");
+ } else {
+ button.setAttribute("checked", true);
+ }
+ },
+
+ /**
+ * Event handlers
+ */
+
+ /**
+ * Called on EVENTS.UI_SELECT_NODE, and takes an actorID `id`
+ * and calls `setCurrentAudioNode` to scaffold the inspector view.
+ */
+ _onNodeSelect: function (_, id) {
+ this.setCurrentAudioNode(gAudioNodes.get(id));
+
+ // Ensure inspector is visible when selecting a new node
+ this.show();
+ },
+
+ _onResize: function () {
+ if (this.el.getAttribute("width") < MIN_INSPECTOR_WIDTH) {
+ this.el.setAttribute("width", MIN_INSPECTOR_WIDTH);
+ }
+ Services.prefs.setIntPref("devtools.webaudioeditor.inspectorWidth", this.el.getAttribute("width"));
+ window.emit(EVENTS.UI_INSPECTOR_RESIZE);
+ },
+
+ /**
+ * Called when `DESTROY_NODE` is fired to remove the node from props view if
+ * it's currently selected.
+ */
+ _onDestroyNode: function (node) {
+ if (this._currentNode && this._currentNode.id === node.id) {
+ this.setCurrentAudioNode(null);
+ }
+ },
+
+ _onCommandClick: function (e) {
+ let node = this.getCurrentAudioNode();
+ let button = e.target;
+ let command = button.getAttribute("data-command");
+ let checked = button.getAttribute("checked");
+
+ if (button.getAttribute("disabled")) {
+ return;
+ }
+
+ if (command === "bypass") {
+ if (checked) {
+ button.removeAttribute("checked");
+ node.bypass(true);
+ } else {
+ button.setAttribute("checked", true);
+ node.bypass(false);
+ }
+ }
+ }
+};
diff --git a/devtools/client/webaudioeditor/views/properties.js b/devtools/client/webaudioeditor/views/properties.js
new file mode 100644
index 000000000..efd691e5a
--- /dev/null
+++ b/devtools/client/webaudioeditor/views/properties.js
@@ -0,0 +1,163 @@
+/* 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 { VariablesView } = require("resource://devtools/client/shared/widgets/VariablesView.jsm");
+
+const GENERIC_VARIABLES_VIEW_SETTINGS = {
+ searchEnabled: false,
+ editableValueTooltip: "",
+ editableNameTooltip: "",
+ preventDisableOnChange: true,
+ preventDescriptorModifiers: false,
+ eval: () => {}
+};
+
+/**
+ * Functions handling the audio node inspector UI.
+ */
+
+var PropertiesView = {
+
+ /**
+ * Initialization function called when the tool starts up.
+ */
+ initialize: function () {
+ this._onEval = this._onEval.bind(this);
+ this._onNodeSet = this._onNodeSet.bind(this);
+
+ window.on(EVENTS.UI_INSPECTOR_NODE_SET, this._onNodeSet);
+ this._propsView = new VariablesView($("#properties-content"), GENERIC_VARIABLES_VIEW_SETTINGS);
+ this._propsView.eval = this._onEval;
+ },
+
+ /**
+ * Destruction function called when the tool cleans up.
+ */
+ destroy: function () {
+ window.off(EVENTS.UI_INSPECTOR_NODE_SET, this._onNodeSet);
+ this._propsView = null;
+ },
+
+ /**
+ * Empties out the props view.
+ */
+ resetUI: function () {
+ this._propsView.empty();
+ this._currentNode = null;
+ },
+
+ /**
+ * Internally sets the current audio node and rebuilds appropriate
+ * views.
+ */
+ _setAudioNode: function (node) {
+ this._currentNode = node;
+ if (this._currentNode) {
+ this._buildPropertiesView();
+ }
+ },
+
+ /**
+ * Reconstructs the `Properties` tab in the inspector
+ * with the `this._currentNode` as it's source.
+ */
+ _buildPropertiesView: Task.async(function* () {
+ let propsView = this._propsView;
+ let node = this._currentNode;
+ propsView.empty();
+
+ let audioParamsScope = propsView.addScope("AudioParams");
+ let props = yield node.getParams();
+
+ // Disable AudioParams VariableView expansion
+ // when there are no props i.e. AudioDestinationNode
+ this._togglePropertiesView(!!props.length);
+
+ props.forEach(({ param, value, flags }) => {
+ let descriptor = {
+ value: value,
+ writable: !flags || !flags.readonly,
+ };
+ let item = audioParamsScope.addItem(param, descriptor);
+
+ // No items should currently display a dropdown
+ item.twisty = false;
+ });
+
+ audioParamsScope.expanded = true;
+
+ window.emit(EVENTS.UI_PROPERTIES_TAB_RENDERED, node.id);
+ }),
+
+ /**
+ * Toggles the display of the "empty" properties view when
+ * node has no properties to display.
+ */
+ _togglePropertiesView: function (show) {
+ let propsView = $("#properties-content");
+ let emptyView = $("#properties-empty");
+ (show ? propsView : emptyView).removeAttribute("hidden");
+ (show ? emptyView : propsView).setAttribute("hidden", "true");
+ },
+
+ /**
+ * Returns the scope for AudioParams in the
+ * VariablesView.
+ *
+ * @return Scope
+ */
+ _getAudioPropertiesScope: function () {
+ return this._propsView.getScopeAtIndex(0);
+ },
+
+ /**
+ * Event handlers
+ */
+
+ /**
+ * Called when the inspector view determines a node is selected.
+ */
+ _onNodeSet: function (_, id) {
+ this._setAudioNode(gAudioNodes.get(id));
+ },
+
+ /**
+ * Executed when an audio prop is changed in the UI.
+ */
+ _onEval: Task.async(function* (variable, value) {
+ let ownerScope = variable.ownerView;
+ let node = this._currentNode;
+ let propName = variable.name;
+ let error;
+
+ if (!variable._initialDescriptor.writable) {
+ error = new Error("Variable " + propName + " is not writable.");
+ } else {
+ // Cast value to proper type
+ try {
+ let number = parseFloat(value);
+ if (!isNaN(number)) {
+ value = number;
+ } else {
+ value = JSON.parse(value);
+ }
+ error = yield node.actor.setParam(propName, value);
+ }
+ catch (e) {
+ error = e;
+ }
+ }
+
+ // TODO figure out how to handle and display set prop errors
+ // and enable `test/brorwser_wa_properties-view-edit.js`
+ // Bug 994258
+ if (!error) {
+ ownerScope.get(propName).setGrip(value);
+ window.emit(EVENTS.UI_SET_PARAM, node.id, propName, value);
+ } else {
+ window.emit(EVENTS.UI_SET_PARAM_ERROR, node.id, propName, value);
+ }
+ })
+};
diff --git a/devtools/client/webaudioeditor/views/utils.js b/devtools/client/webaudioeditor/views/utils.js
new file mode 100644
index 000000000..6d6a96946
--- /dev/null
+++ b/devtools/client/webaudioeditor/views/utils.js
@@ -0,0 +1,103 @@
+/* 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";
+
+/**
+ * Takes an element in an SVG graph and iterates over
+ * ancestors until it finds the graph node container. If not found,
+ * returns null.
+ */
+
+function findGraphNodeParent(el) {
+ // Some targets may not contain `classList` property
+ if (!el.classList)
+ return null;
+
+ while (!el.classList.contains("nodes")) {
+ if (el.classList.contains("audionode"))
+ return el;
+ else
+ el = el.parentNode;
+ }
+ return null;
+}
+
+/**
+ * Object for use with `mix` into a view.
+ * Must have the following properties defined on the view:
+ * - `el`
+ * - `button`
+ * - `_collapseString`
+ * - `_expandString`
+ * - `_toggleEvent`
+ *
+ * Optional properties on the view can be defined to specify default
+ * visibility options.
+ * - `_animated`
+ * - `_delayed`
+ */
+var ToggleMixin = {
+
+ bindToggle: function () {
+ this._onToggle = this._onToggle.bind(this);
+ this.button.addEventListener("mousedown", this._onToggle, false);
+ },
+
+ unbindToggle: function () {
+ this.button.removeEventListener("mousedown", this._onToggle);
+ },
+
+ show: function () {
+ this._viewController({ visible: true });
+ },
+
+ hide: function () {
+ this._viewController({ visible: false });
+ },
+
+ hideImmediately: function () {
+ this._viewController({ visible: false, delayed: false, animated: false });
+ },
+
+ /**
+ * Returns a boolean indicating whether or not the view.
+ * is currently being shown.
+ */
+ isVisible: function () {
+ return !this.el.classList.contains("pane-collapsed");
+ },
+
+ /**
+ * Toggles the visibility of the view.
+ *
+ * @param object visible
+ * - visible: boolean indicating whether the panel should be shown or not
+ * - animated: boolean indiciating whether the pane should be animated
+ * - delayed: boolean indicating whether the pane's opening should wait
+ * a few cycles or not
+ */
+ _viewController: function ({ visible, animated, delayed }) {
+ let flags = {
+ visible: visible,
+ animated: animated != null ? animated : !!this._animated,
+ delayed: delayed != null ? delayed : !!this._delayed,
+ callback: () => window.emit(this._toggleEvent, visible)
+ };
+
+ ViewHelpers.togglePane(flags, this.el);
+
+ if (flags.visible) {
+ this.button.classList.remove("pane-collapsed");
+ this.button.setAttribute("tooltiptext", this._collapseString);
+ }
+ else {
+ this.button.classList.add("pane-collapsed");
+ this.button.setAttribute("tooltiptext", this._expandString);
+ }
+ },
+
+ _onToggle: function () {
+ this._viewController({ visible: !this.isVisible() });
+ }
+};
diff --git a/devtools/client/webaudioeditor/webaudioeditor.xul b/devtools/client/webaudioeditor/webaudioeditor.xul
new file mode 100644
index 000000000..f35ce3d9c
--- /dev/null
+++ b/devtools/client/webaudioeditor/webaudioeditor.xul
@@ -0,0 +1,141 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/content/shared/widgets/widgets.css" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/skin/widgets.css" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/skin/webaudioeditor.css" type="text/css"?>
+<!DOCTYPE window [
+ <!ENTITY % debuggerDTD SYSTEM "chrome://devtools/locale/webaudioeditor.dtd">
+ %debuggerDTD;
+]>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script type="application/javascript;version=1.8"
+ src="chrome://devtools/content/shared/theme-switching.js"/>
+
+ <script type="application/javascript" src="chrome://devtools/content/shared/vendor/d3.js"/>
+ <script type="application/javascript" src="chrome://devtools/content/shared/vendor/dagre-d3.js"/>
+ <script type="application/javascript" src="includes.js"/>
+ <script type="application/javascript" src="models.js"/>
+ <script type="application/javascript" src="controller.js"/>
+ <script type="application/javascript" src="views/utils.js"/>
+ <script type="application/javascript" src="views/context.js"/>
+ <script type="application/javascript" src="views/inspector.js"/>
+ <script type="application/javascript" src="views/properties.js"/>
+ <script type="application/javascript" src="views/automation.js"/>
+
+ <vbox class="theme-body" flex="1">
+ <hbox id="reload-notice"
+ class="notice-container"
+ align="center"
+ pack="center"
+ flex="1">
+ <button id="requests-menu-reload-notice-button"
+ class="devtools-toolbarbutton"
+ standalone="true"
+ label="&webAudioEditorUI.reloadNotice1;"
+ oncommand="gFront.setup({ reload: true });"/>
+ <label id="requests-menu-reload-notice-label"
+ class="plain"
+ value="&webAudioEditorUI.reloadNotice2;"/>
+ </hbox>
+ <hbox id="waiting-notice"
+ class="notice-container devtools-throbber"
+ align="center"
+ pack="center"
+ flex="1"
+ hidden="true">
+ <label id="requests-menu-waiting-notice-label"
+ class="plain"
+ value="&webAudioEditorUI.emptyNotice;"/>
+ </hbox>
+
+ <vbox id="content"
+ flex="1"
+ hidden="true">
+ <toolbar id="web-audio-toolbar" class="devtools-toolbar">
+ <spacer flex="1"></spacer>
+ <toolbarbutton id="inspector-pane-toggle" class="devtools-toolbarbutton"
+ tabindex="0"/>
+ </toolbar>
+ <splitter class="devtools-horizontal-splitter"/>
+ <box id="web-audio-content-pane"
+ class="devtools-responsive-container"
+ flex="1">
+ <hbox flex="1">
+ <box id="web-audio-graph" flex="1">
+ <vbox flex="1">
+ <svg id="graph-svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink">
+ <g id="graph-target" transform="translate(20,20)"/>
+ </svg>
+ </vbox>
+ </box>
+ </hbox>
+ <splitter id="inspector-splitter" class="devtools-side-splitter"/>
+ <vbox id="web-audio-inspector" hidden="true">
+ <deck id="web-audio-editor-details-pane" flex="1">
+ <vbox id="web-audio-editor-details-pane-empty" flex="1">
+ <label value="&webAudioEditorUI.inspectorEmpty;"></label>
+ </vbox>
+ <tabbox id="web-audio-editor-tabs"
+ class="devtools-sidebar-tabs"
+ handleCtrlTab="false">
+ <toolbar id="audio-node-toolbar" class="devtools-toolbar">
+ <hbox class="devtools-toolbarbutton-group">
+ <toolbarbutton class="bypass devtools-toolbarbutton"
+ data-command="bypass"
+ tabindex="0"/>
+ </hbox>
+ </toolbar>
+ <tabs>
+ <tab id="properties-tab"
+ label="&webAudioEditorUI.tab.properties2;"/>
+ <!-- bug 1134036
+ <tab id="automation-tab"
+ label="&webAudioEditorUI.tab.automation;"/>
+ -->
+ </tabs>
+ <tabpanels flex="1">
+ <!-- Properties Panel -->
+ <tabpanel id="properties-tabpanel"
+ class="tabpanel-content">
+ <vbox id="properties-content" flex="1" hidden="true">
+ </vbox>
+ <vbox id="properties-empty" flex="1" hidden="true">
+ <label value="&webAudioEditorUI.propertiesEmpty;"></label>
+ </vbox>
+ </tabpanel>
+
+ <!-- Automation Panel -->
+ <tabpanel id="automation-tabpanel"
+ class="tabpanel-content">
+ <vbox id="automation-content" flex="1" hidden="true">
+ <toolbar id="automation-param-toolbar" class="devtools-toolbar">
+ <hbox id="automation-param-toolbar-buttons" class="devtools-toolbarbutton-group">
+ </hbox>
+ </toolbar>
+ <box id="automation-graph-container" flex="1">
+ <canvas id="automation-graph"></canvas>
+ </box>
+ <vbox id="automation-no-events" flex="1" hidden="true">
+ <label value="&webAudioEditorUI.automationNoEvents;"></label>
+ </vbox>
+ </vbox>
+ <vbox id="automation-empty" flex="1" hidden="true">
+ <label value="&webAudioEditorUI.automationEmpty;"></label>
+ </vbox>
+ </tabpanel>
+ </tabpanels>
+ </tabbox>
+ </deck>
+ </vbox>
+ </box>
+ </vbox>
+ </vbox>
+
+</window>
diff --git a/devtools/client/webconsole/.babelrc b/devtools/client/webconsole/.babelrc
new file mode 100644
index 000000000..af0f0c3d3
--- /dev/null
+++ b/devtools/client/webconsole/.babelrc
@@ -0,0 +1,3 @@
+{
+ "presets": ["es2015"]
+} \ No newline at end of file
diff --git a/devtools/client/webconsole/console-commands.js b/devtools/client/webconsole/console-commands.js
new file mode 100644
index 000000000..0bc9e8edb
--- /dev/null
+++ b/devtools/client/webconsole/console-commands.js
@@ -0,0 +1,103 @@
+/* -*- 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 l10n = require("gcli/l10n");
+loader.lazyRequireGetter(this, "gDevTools",
+ "devtools/client/framework/devtools", true);
+
+exports.items = [
+ {
+ item: "command",
+ runAt: "client",
+ name: "splitconsole",
+ hidden: true,
+ buttonId: "command-button-splitconsole",
+ buttonClass: "command-button command-button-invertable",
+ tooltipText: l10n.lookup("splitconsoleTooltip"),
+ isRemoteSafe: true,
+ state: {
+ isChecked: function (target) {
+ let toolbox = gDevTools.getToolbox(target);
+ return !!(toolbox && toolbox.splitConsole);
+ },
+ onChange: function (target, changeHandler) {
+ // Register handlers for when a change event should be fired
+ // (which resets the checked state of the button).
+ let toolbox = gDevTools.getToolbox(target);
+ let callback = changeHandler.bind(null, "changed", { target: target });
+
+ if (!toolbox) {
+ return;
+ }
+
+ toolbox.on("split-console", callback);
+ toolbox.once("destroyed", () => {
+ toolbox.off("split-console", callback);
+ });
+ }
+ },
+ exec: function (args, context) {
+ let target = context.environment.target;
+ let toolbox = gDevTools.getToolbox(target);
+
+ if (!toolbox) {
+ return gDevTools.showToolbox(target, "inspector").then((newToolbox) => {
+ newToolbox.toggleSplitConsole();
+ });
+ }
+ return toolbox.toggleSplitConsole();
+ }
+ },
+ {
+ name: "console",
+ description: l10n.lookup("consoleDesc"),
+ manual: l10n.lookup("consoleManual")
+ },
+ {
+ item: "command",
+ runAt: "client",
+ name: "console clear",
+ description: l10n.lookup("consoleclearDesc"),
+ exec: function (args, context) {
+ let toolbox = gDevTools.getToolbox(context.environment.target);
+ if (toolbox == null) {
+ return null;
+ }
+
+ let panel = toolbox.getPanel("webconsole");
+ if (panel == null) {
+ return null;
+ }
+
+ let onceMessagesCleared = panel.hud.jsterm.once("messages-cleared");
+ panel.hud.jsterm.clearOutput();
+ return onceMessagesCleared;
+ }
+ },
+ {
+ item: "command",
+ runAt: "client",
+ name: "console close",
+ description: l10n.lookup("consolecloseDesc"),
+ exec: function (args, context) {
+ // Don't return a value to GCLI
+ return gDevTools.closeToolbox(context.environment.target).then(() => {});
+ }
+ },
+ {
+ item: "command",
+ runAt: "client",
+ name: "console open",
+ description: l10n.lookup("consoleopenDesc"),
+ exec: function (args, context) {
+ const target = context.environment.target;
+ // Don't return a value to GCLI
+ return gDevTools.showToolbox(target, "webconsole").then(() => {});
+ }
+ }
+];
diff --git a/devtools/client/webconsole/console-output.js b/devtools/client/webconsole/console-output.js
new file mode 100644
index 000000000..52d848494
--- /dev/null
+++ b/devtools/client/webconsole/console-output.js
@@ -0,0 +1,3638 @@
+/* -*- 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 {Ci, Cu} = require("chrome");
+
+loader.lazyImporter(this, "VariablesView", "resource://devtools/client/shared/widgets/VariablesView.jsm");
+loader.lazyImporter(this, "escapeHTML", "resource://devtools/client/shared/widgets/VariablesView.jsm");
+
+loader.lazyRequireGetter(this, "promise");
+loader.lazyRequireGetter(this, "gDevTools", "devtools/client/framework/devtools", true);
+loader.lazyRequireGetter(this, "TableWidget", "devtools/client/shared/widgets/TableWidget", true);
+loader.lazyRequireGetter(this, "ObjectClient", "devtools/shared/client/main", true);
+
+const { extend } = require("sdk/core/heritage");
+const XHTML_NS = "http://www.w3.org/1999/xhtml";
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+const STRINGS_URI = "devtools/client/locales/webconsole.properties";
+
+const WebConsoleUtils = require("devtools/client/webconsole/utils").Utils;
+const { getSourceNames } = require("devtools/client/shared/source-utils");
+const {Task} = require("devtools/shared/task");
+const l10n = new WebConsoleUtils.L10n(STRINGS_URI);
+const nodeConstants = require("devtools/shared/dom-node-constants");
+const {PluralForm} = require("devtools/shared/plural-form");
+
+const MAX_STRING_GRIP_LENGTH = 36;
+const {ELLIPSIS} = require("devtools/shared/l10n");
+
+const validProtocols = /^(http|https|ftp|data|javascript|resource|chrome):/i;
+
+// Constants for compatibility with the Web Console output implementation before
+// bug 778766.
+// TODO: remove these once bug 778766 is fixed.
+const COMPAT = {
+ // The various categories of messages.
+ CATEGORIES: {
+ NETWORK: 0,
+ CSS: 1,
+ JS: 2,
+ WEBDEV: 3,
+ INPUT: 4,
+ OUTPUT: 5,
+ SECURITY: 6,
+ SERVER: 7,
+ },
+
+ // The possible message severities.
+ SEVERITIES: {
+ ERROR: 0,
+ WARNING: 1,
+ INFO: 2,
+ LOG: 3,
+ },
+
+ // The preference keys to use for each category/severity combination, indexed
+ // first by category (rows) and then by severity (columns).
+ //
+ // Most of these rather idiosyncratic names are historical and predate the
+ // division of message type into "category" and "severity".
+ /* eslint-disable no-multi-spaces */
+ /* eslint-disable max-len */
+ /* eslint-disable no-inline-comments */
+ PREFERENCE_KEYS: [
+ // Error Warning Info Log
+ [ "network", "netwarn", null, "networkinfo", ], // Network
+ [ "csserror", "cssparser", null, null, ], // CSS
+ [ "exception", "jswarn", null, "jslog", ], // JS
+ [ "error", "warn", "info", "log", ], // Web Developer
+ [ null, null, null, null, ], // Input
+ [ null, null, null, null, ], // Output
+ [ "secerror", "secwarn", null, null, ], // Security
+ [ "servererror", "serverwarn", "serverinfo", "serverlog", ], // Server Logging
+ ],
+ /* eslint-enable no-inline-comments */
+ /* eslint-enable max-len */
+ /* eslint-enable no-multi-spaces */
+
+ // The fragment of a CSS class name that identifies each category.
+ CATEGORY_CLASS_FRAGMENTS: [ "network", "cssparser", "exception", "console",
+ "input", "output", "security", "server" ],
+
+ // The fragment of a CSS class name that identifies each severity.
+ SEVERITY_CLASS_FRAGMENTS: [ "error", "warn", "info", "log" ],
+
+ // The indent of a console group in pixels.
+ GROUP_INDENT: 12,
+};
+
+// A map from the console API call levels to the Web Console severities.
+const CONSOLE_API_LEVELS_TO_SEVERITIES = {
+ error: "error",
+ exception: "error",
+ assert: "error",
+ warn: "warning",
+ info: "info",
+ log: "log",
+ clear: "log",
+ trace: "log",
+ table: "log",
+ debug: "log",
+ dir: "log",
+ dirxml: "log",
+ group: "log",
+ groupCollapsed: "log",
+ groupEnd: "log",
+ time: "log",
+ timeEnd: "log",
+ count: "log"
+};
+
+// Array of known message source URLs we need to hide from output.
+const IGNORED_SOURCE_URLS = ["debugger eval code"];
+
+// The maximum length of strings to be displayed by the Web Console.
+const MAX_LONG_STRING_LENGTH = 200000;
+
+// Regular expression that matches the allowed CSS property names when using
+// the `window.console` API.
+const RE_ALLOWED_STYLES = /^(?:-moz-)?(?:background|border|box|clear|color|cursor|display|float|font|line|margin|padding|text|transition|outline|white-space|word|writing|(?:min-|max-)?width|(?:min-|max-)?height)/;
+
+// Regular expressions to search and replace with 'notallowed' in the styles
+// given to the `window.console` API methods.
+const RE_CLEANUP_STYLES = [
+ // url(), -moz-element()
+ /\b(?:url|(?:-moz-)?element)[\s('"]+/gi,
+
+ // various URL protocols
+ /['"(]*(?:chrome|resource|about|app|data|https?|ftp|file):+\/*/gi,
+];
+
+// Maximum number of rows to display in console.table().
+const TABLE_ROW_MAX_ITEMS = 1000;
+
+// Maximum number of columns to display in console.table().
+const TABLE_COLUMN_MAX_ITEMS = 10;
+
+/**
+ * The ConsoleOutput object is used to manage output of messages in the Web
+ * Console.
+ *
+ * @constructor
+ * @param object owner
+ * The console output owner. This usually the WebConsoleFrame instance.
+ * Any other object can be used, as long as it has the following
+ * properties and methods:
+ * - window
+ * - document
+ * - outputMessage(category, methodOrNode[, methodArguments])
+ * TODO: this is needed temporarily, until bug 778766 is fixed.
+ */
+function ConsoleOutput(owner)
+{
+ this.owner = owner;
+ this._onFlushOutputMessage = this._onFlushOutputMessage.bind(this);
+}
+
+ConsoleOutput.prototype = {
+ _dummyElement: null,
+
+ /**
+ * The output container.
+ * @type DOMElement
+ */
+ get element() {
+ return this.owner.outputNode;
+ },
+
+ /**
+ * The document that holds the output.
+ * @type DOMDocument
+ */
+ get document() {
+ return this.owner ? this.owner.document : null;
+ },
+
+ /**
+ * The DOM window that holds the output.
+ * @type Window
+ */
+ get window() {
+ return this.owner.window;
+ },
+
+ /**
+ * Getter for the debugger WebConsoleClient.
+ * @type object
+ */
+ get webConsoleClient() {
+ return this.owner.webConsoleClient;
+ },
+
+ /**
+ * Getter for the current toolbox debuggee target.
+ * @type Target
+ */
+ get toolboxTarget() {
+ return this.owner.owner.target;
+ },
+
+ /**
+ * Release an actor.
+ *
+ * @private
+ * @param string actorId
+ * The actor ID you want to release.
+ */
+ _releaseObject: function (actorId)
+ {
+ this.owner._releaseObject(actorId);
+ },
+
+ /**
+ * Add a message to output.
+ *
+ * @param object ...args
+ * Any number of Message objects.
+ * @return this
+ */
+ addMessage: function (...args)
+ {
+ for (let msg of args) {
+ msg.init(this);
+ this.owner.outputMessage(msg._categoryCompat, this._onFlushOutputMessage,
+ [msg]);
+ }
+ return this;
+ },
+
+ /**
+ * Message renderer used for compatibility with the current Web Console output
+ * implementation. This method is invoked for every message object that is
+ * flushed to output. The message object is initialized and rendered, then it
+ * is displayed.
+ *
+ * TODO: remove this method once bug 778766 is fixed.
+ *
+ * @private
+ * @param object message
+ * The message object to render.
+ * @return DOMElement
+ * The message DOM element that can be added to the console output.
+ */
+ _onFlushOutputMessage: function (message)
+ {
+ return message.render().element;
+ },
+
+ /**
+ * Get an array of selected messages. This list is based on the text selection
+ * start and end points.
+ *
+ * @param number [limit]
+ * Optional limit of selected messages you want. If no value is given,
+ * all of the selected messages are returned.
+ * @return array
+ * Array of DOM elements for each message that is currently selected.
+ */
+ getSelectedMessages: function (limit)
+ {
+ let selection = this.window.getSelection();
+ if (selection.isCollapsed) {
+ return [];
+ }
+
+ if (selection.containsNode(this.element, true)) {
+ return Array.slice(this.element.children);
+ }
+
+ let anchor = this.getMessageForElement(selection.anchorNode);
+ let focus = this.getMessageForElement(selection.focusNode);
+ if (!anchor || !focus) {
+ return [];
+ }
+
+ let start, end;
+ if (anchor.timestamp > focus.timestamp) {
+ start = focus;
+ end = anchor;
+ } else {
+ start = anchor;
+ end = focus;
+ }
+
+ let result = [];
+ let current = start;
+ while (current) {
+ result.push(current);
+ if (current == end || (limit && result.length == limit)) {
+ break;
+ }
+ current = current.nextSibling;
+ }
+ return result;
+ },
+
+ /**
+ * Find the DOM element of a message for any given descendant.
+ *
+ * @param DOMElement elem
+ * The element to start the search from.
+ * @return DOMElement|null
+ * The DOM element of the message, if any.
+ */
+ getMessageForElement: function (elem)
+ {
+ while (elem && elem.parentNode) {
+ if (elem.classList && elem.classList.contains("message")) {
+ return elem;
+ }
+ elem = elem.parentNode;
+ }
+ return null;
+ },
+
+ /**
+ * Select all messages.
+ */
+ selectAllMessages: function ()
+ {
+ let selection = this.window.getSelection();
+ selection.removeAllRanges();
+ let range = this.document.createRange();
+ range.selectNodeContents(this.element);
+ selection.addRange(range);
+ },
+
+ /**
+ * Add a message to the selection.
+ *
+ * @param DOMElement elem
+ * The message element to select.
+ */
+ selectMessage: function (elem)
+ {
+ let selection = this.window.getSelection();
+ selection.removeAllRanges();
+ let range = this.document.createRange();
+ range.selectNodeContents(elem);
+ selection.addRange(range);
+ },
+
+ /**
+ * Open an URL in a new tab.
+ * @see WebConsole.openLink() in hudservice.js
+ */
+ openLink: function ()
+ {
+ this.owner.owner.openLink.apply(this.owner.owner, arguments);
+ },
+
+ openLocationInDebugger: function ({url, line}) {
+ return this.owner.owner.viewSourceInDebugger(url, line);
+ },
+
+ /**
+ * Open the variables view to inspect an object actor.
+ * @see JSTerm.openVariablesView() in webconsole.js
+ */
+ openVariablesView: function ()
+ {
+ this.owner.jsterm.openVariablesView.apply(this.owner.jsterm, arguments);
+ },
+
+ /**
+ * Destroy this ConsoleOutput instance.
+ */
+ destroy: function ()
+ {
+ this._dummyElement = null;
+ this.owner = null;
+ },
+}; // ConsoleOutput.prototype
+
+/**
+ * Message objects container.
+ * @type object
+ */
+var Messages = {};
+
+/**
+ * The BaseMessage object is used for all types of messages. Every kind of
+ * message should use this object as its base.
+ *
+ * @constructor
+ */
+Messages.BaseMessage = function ()
+{
+ this.widgets = new Set();
+ this._onClickAnchor = this._onClickAnchor.bind(this);
+ this._repeatID = { uid: gSequenceId() };
+ this.textContent = "";
+};
+
+Messages.BaseMessage.prototype = {
+ /**
+ * Reference to the ConsoleOutput owner.
+ *
+ * @type object|null
+ * This is |null| if the message is not yet initialized.
+ */
+ output: null,
+
+ /**
+ * Reference to the parent message object, if this message is in a group or if
+ * it is otherwise owned by another message.
+ *
+ * @type object|null
+ */
+ parent: null,
+
+ /**
+ * Message DOM element.
+ *
+ * @type DOMElement|null
+ * This is |null| if the message is not yet rendered.
+ */
+ element: null,
+
+ /**
+ * Tells if this message is visible or not.
+ * @type boolean
+ */
+ get visible() {
+ return this.element && this.element.parentNode;
+ },
+
+ /**
+ * The owner DOM document.
+ * @type DOMElement
+ */
+ get document() {
+ return this.output.document;
+ },
+
+ /**
+ * Holds the text-only representation of the message.
+ * @type string
+ */
+ textContent: null,
+
+ /**
+ * Set of widgets included in this message.
+ * @type Set
+ */
+ widgets: null,
+
+ // Properties that allow compatibility with the current Web Console output
+ // implementation.
+ _categoryCompat: null,
+ _severityCompat: null,
+ _categoryNameCompat: null,
+ _severityNameCompat: null,
+ _filterKeyCompat: null,
+
+ /**
+ * Object that is JSON-ified and used as a non-unique ID for tracking
+ * duplicate messages.
+ * @private
+ * @type object
+ */
+ _repeatID: null,
+
+ /**
+ * Initialize the message.
+ *
+ * @param object output
+ * The ConsoleOutput owner.
+ * @param object [parent=null]
+ * Optional: a different message object that owns this instance.
+ * @return this
+ */
+ init: function (output, parent = null)
+ {
+ this.output = output;
+ this.parent = parent;
+ return this;
+ },
+
+ /**
+ * Non-unique ID for this message object used for tracking duplicate messages.
+ * Different message kinds can identify themselves based their own criteria.
+ *
+ * @return string
+ */
+ getRepeatID: function ()
+ {
+ return JSON.stringify(this._repeatID);
+ },
+
+ /**
+ * Render the message. After this method is invoked the |element| property
+ * will point to the DOM element of this message.
+ * @return this
+ */
+ render: function ()
+ {
+ if (!this.element) {
+ this.element = this._renderCompat();
+ }
+ return this;
+ },
+
+ /**
+ * Prepare the message container for the Web Console, such that it is
+ * compatible with the current implementation.
+ * TODO: remove this once bug 778766 is fixed.
+ *
+ * @private
+ * @return Element
+ * The DOM element that wraps the message.
+ */
+ _renderCompat: function ()
+ {
+ let doc = this.output.document;
+ let container = doc.createElementNS(XHTML_NS, "div");
+ container.id = "console-msg-" + gSequenceId();
+ container.className = "message";
+ if (this.category == "input") {
+ // Assistive technology tools shouldn't echo input to the user,
+ // as the user knows what they've just typed.
+ container.setAttribute("aria-live", "off");
+ }
+ container.category = this._categoryCompat;
+ container.severity = this._severityCompat;
+ container.setAttribute("category", this._categoryNameCompat);
+ container.setAttribute("severity", this._severityNameCompat);
+ container.setAttribute("filter", this._filterKeyCompat);
+ container.clipboardText = this.textContent;
+ container.timestamp = this.timestamp;
+ container._messageObject = this;
+
+ return container;
+ },
+
+ /**
+ * Add a click callback to a given DOM element.
+ *
+ * @private
+ * @param Element element
+ * The DOM element to which you want to add a click event handler.
+ * @param function [callback=this._onClickAnchor]
+ * Optional click event handler. The default event handler is
+ * |this._onClickAnchor|.
+ */
+ _addLinkCallback: function (element, callback = this._onClickAnchor)
+ {
+ // This is going into the WebConsoleFrame object instance that owns
+ // the ConsoleOutput object. The WebConsoleFrame owner is the WebConsole
+ // object instance from hudservice.js.
+ // TODO: move _addMessageLinkCallback() into ConsoleOutput once bug 778766
+ // is fixed.
+ this.output.owner._addMessageLinkCallback(element, callback);
+ },
+
+ /**
+ * The default |click| event handler for links in the output. This function
+ * opens the anchor's link in a new tab.
+ *
+ * @private
+ * @param Event event
+ * The DOM event that invoked this function.
+ */
+ _onClickAnchor: function (event)
+ {
+ this.output.openLink(event.target.href);
+ },
+
+ destroy: function ()
+ {
+ // Destroy all widgets that have registered themselves in this.widgets
+ for (let widget of this.widgets) {
+ widget.destroy();
+ }
+ this.widgets.clear();
+ }
+};
+
+/**
+ * The NavigationMarker is used to show a page load event.
+ *
+ * @constructor
+ * @extends Messages.BaseMessage
+ * @param object response
+ * The response received from the back end.
+ * @param number timestamp
+ * The message date and time, milliseconds elapsed since 1 January 1970
+ * 00:00:00 UTC.
+ */
+Messages.NavigationMarker = function (response, timestamp) {
+ Messages.BaseMessage.call(this);
+
+ // Store the response packet received from the server. It might
+ // be useful for extensions customizing the console output.
+ this.response = response;
+ this._url = response.url;
+ this.textContent = "------ " + this._url;
+ this.timestamp = timestamp;
+};
+
+Messages.NavigationMarker.prototype = extend(Messages.BaseMessage.prototype, {
+ /**
+ * The address of the loading page.
+ * @private
+ * @type string
+ */
+ _url: null,
+
+ /**
+ * Message timestamp.
+ *
+ * @type number
+ * Milliseconds elapsed since 1 January 1970 00:00:00 UTC.
+ */
+ timestamp: 0,
+
+ _categoryCompat: COMPAT.CATEGORIES.NETWORK,
+ _severityCompat: COMPAT.SEVERITIES.LOG,
+ _categoryNameCompat: "network",
+ _severityNameCompat: "info",
+ _filterKeyCompat: "networkinfo",
+
+ /**
+ * Prepare the DOM element for this message.
+ * @return this
+ */
+ render: function () {
+ if (this.element) {
+ return this;
+ }
+
+ let url = this._url;
+ let pos = url.indexOf("?");
+ if (pos > -1) {
+ url = url.substr(0, pos);
+ }
+
+ let doc = this.output.document;
+ let urlnode = doc.createElementNS(XHTML_NS, "a");
+ urlnode.className = "url";
+ urlnode.textContent = url;
+ urlnode.title = this._url;
+ urlnode.href = this._url;
+ urlnode.draggable = false;
+ this._addLinkCallback(urlnode);
+
+ let render = Messages.BaseMessage.prototype.render.bind(this);
+ render().element.appendChild(urlnode);
+ this.element.classList.add("navigation-marker");
+ this.element.url = this._url;
+ this.element.appendChild(doc.createTextNode("\n"));
+
+ return this;
+ },
+});
+
+/**
+ * The Simple message is used to show any basic message in the Web Console.
+ *
+ * @constructor
+ * @extends Messages.BaseMessage
+ * @param string|Node|function message
+ * The message to display.
+ * @param object [options]
+ * Options for this message:
+ * - category: (string) category that this message belongs to. Defaults
+ * to no category.
+ * - severity: (string) severity of the message. Defaults to no severity.
+ * - timestamp: (number) date and time when the message was recorded.
+ * Defaults to |Date.now()|.
+ * - link: (string) if provided, the message will be wrapped in an anchor
+ * pointing to the given URL here.
+ * - linkCallback: (function) if provided, the message will be wrapped in
+ * an anchor. The |linkCallback| function will be added as click event
+ * handler.
+ * - location: object that tells the message source: url, line, column
+ * and lineText.
+ * - stack: array that tells the message source stack.
+ * - className: (string) additional element class names for styling
+ * purposes.
+ * - private: (boolean) mark this as a private message.
+ * - filterDuplicates: (boolean) true if you do want this message to be
+ * filtered as a potential duplicate message, false otherwise.
+ */
+Messages.Simple = function (message, options = {}) {
+ Messages.BaseMessage.call(this);
+
+ this.category = options.category;
+ this.severity = options.severity;
+ this.location = options.location;
+ this.stack = options.stack;
+ this.timestamp = options.timestamp || Date.now();
+ this.prefix = options.prefix;
+ this.private = !!options.private;
+
+ this._message = message;
+ this._className = options.className;
+ this._link = options.link;
+ this._linkCallback = options.linkCallback;
+ this._filterDuplicates = options.filterDuplicates;
+
+ this._onClickCollapsible = this._onClickCollapsible.bind(this);
+};
+
+Messages.Simple.prototype = extend(Messages.BaseMessage.prototype, {
+ /**
+ * Message category.
+ * @type string
+ */
+ category: null,
+
+ /**
+ * Message severity.
+ * @type string
+ */
+ severity: null,
+
+ /**
+ * Message source location. Properties: url, line, column, lineText.
+ * @type object
+ */
+ location: null,
+
+ /**
+ * Holds the stackframes received from the server.
+ *
+ * @private
+ * @type array
+ */
+ stack: null,
+
+ /**
+ * Message prefix
+ * @type string|null
+ */
+ prefix: null,
+
+ /**
+ * Tells if this message comes from a private browsing context.
+ * @type boolean
+ */
+ private: false,
+
+ /**
+ * Custom class name for the DOM element of the message.
+ * @private
+ * @type string
+ */
+ _className: null,
+
+ /**
+ * Message link - if this message is clicked then this URL opens in a new tab.
+ * @private
+ * @type string
+ */
+ _link: null,
+
+ /**
+ * Message click event handler.
+ * @private
+ * @type function
+ */
+ _linkCallback: null,
+
+ /**
+ * Tells if this message should be checked if it is a duplicate of another
+ * message or not.
+ */
+ _filterDuplicates: false,
+
+ /**
+ * The raw message displayed by this Message object. This can be a function,
+ * DOM node or a string.
+ *
+ * @private
+ * @type mixed
+ */
+ _message: null,
+
+ /**
+ * The message's "attachment" element to be displayed under the message.
+ * Used for things like stack traces or tables in console.table().
+ *
+ * @private
+ * @type DOMElement|null
+ */
+ _attachment: null,
+
+ _objectActors: null,
+ _groupDepthCompat: 0,
+
+ /**
+ * Message timestamp.
+ *
+ * @type number
+ * Milliseconds elapsed since 1 January 1970 00:00:00 UTC.
+ */
+ timestamp: 0,
+
+ get _categoryCompat() {
+ return this.category ?
+ COMPAT.CATEGORIES[this.category.toUpperCase()] : null;
+ },
+ get _severityCompat() {
+ return this.severity ?
+ COMPAT.SEVERITIES[this.severity.toUpperCase()] : null;
+ },
+ get _categoryNameCompat() {
+ return this.category ?
+ COMPAT.CATEGORY_CLASS_FRAGMENTS[this._categoryCompat] : null;
+ },
+ get _severityNameCompat() {
+ return this.severity ?
+ COMPAT.SEVERITY_CLASS_FRAGMENTS[this._severityCompat] : null;
+ },
+
+ get _filterKeyCompat() {
+ return this._categoryCompat !== null && this._severityCompat !== null ?
+ COMPAT.PREFERENCE_KEYS[this._categoryCompat][this._severityCompat] :
+ null;
+ },
+
+ init: function ()
+ {
+ Messages.BaseMessage.prototype.init.apply(this, arguments);
+ this._groupDepthCompat = this.output.owner.groupDepth;
+ this._initRepeatID();
+ return this;
+ },
+
+ /**
+ * Tells if the message can be expanded/collapsed.
+ * @type boolean
+ */
+ collapsible: false,
+
+ /**
+ * Getter that tells if this message is collapsed - no details are shown.
+ * @type boolean
+ */
+ get collapsed() {
+ return this.collapsible && this.element && !this.element.hasAttribute("open");
+ },
+
+ _initRepeatID: function ()
+ {
+ if (!this._filterDuplicates) {
+ return;
+ }
+
+ // Add the properties we care about for identifying duplicate messages.
+ let rid = this._repeatID;
+ delete rid.uid;
+
+ rid.category = this.category;
+ rid.severity = this.severity;
+ rid.prefix = this.prefix;
+ rid.private = this.private;
+ rid.location = this.location;
+ rid.link = this._link;
+ rid.linkCallback = this._linkCallback + "";
+ rid.className = this._className;
+ rid.groupDepth = this._groupDepthCompat;
+ rid.textContent = "";
+ },
+
+ getRepeatID: function ()
+ {
+ // No point in returning a string that includes other properties when there
+ // is a unique ID.
+ if (this._repeatID.uid) {
+ return JSON.stringify({ uid: this._repeatID.uid });
+ }
+
+ return JSON.stringify(this._repeatID);
+ },
+
+ render: function ()
+ {
+ if (this.element) {
+ return this;
+ }
+
+ let timestamp = new Widgets.MessageTimestamp(this, this.timestamp).render();
+
+ let icon = this.document.createElementNS(XHTML_NS, "span");
+ icon.className = "icon";
+ icon.title = l10n.getStr("severity." + this._severityNameCompat);
+ if (this.stack) {
+ icon.addEventListener("click", this._onClickCollapsible);
+ }
+
+ let prefixNode;
+ if (this.prefix) {
+ prefixNode = this.document.createElementNS(XHTML_NS, "span");
+ prefixNode.className = "prefix devtools-monospace";
+ prefixNode.textContent = this.prefix + ":";
+ }
+
+ // Apply the current group by indenting appropriately.
+ // TODO: remove this once bug 778766 is fixed.
+ let indent = this._groupDepthCompat * COMPAT.GROUP_INDENT;
+ let indentNode = this.document.createElementNS(XHTML_NS, "span");
+ indentNode.className = "indent";
+ indentNode.style.width = indent + "px";
+
+ let body = this._renderBody();
+
+ Messages.BaseMessage.prototype.render.call(this);
+ if (this._className) {
+ this.element.className += " " + this._className;
+ }
+
+ this.element.appendChild(timestamp.element);
+ this.element.appendChild(indentNode);
+ this.element.appendChild(icon);
+ if (prefixNode) {
+ this.element.appendChild(prefixNode);
+ }
+
+ if (this.stack) {
+ let twisty = this.document.createElementNS(XHTML_NS, "a");
+ twisty.className = "theme-twisty";
+ twisty.href = "#";
+ twisty.title = l10n.getStr("messageToggleDetails");
+ twisty.addEventListener("click", this._onClickCollapsible);
+ this.element.appendChild(twisty);
+ this.collapsible = true;
+ this.element.setAttribute("collapsible", true);
+ }
+
+ this.element.appendChild(body);
+
+ this.element.clipboardText = this.element.textContent;
+
+ if (this.private) {
+ this.element.setAttribute("private", true);
+ }
+
+ // TODO: handle object releasing in a more elegant way once all console
+ // messages use the new API - bug 778766.
+ this.element._objectActors = this._objectActors;
+ this._objectActors = null;
+
+ return this;
+ },
+
+ /**
+ * Render the message body DOM element.
+ * @private
+ * @return Element
+ */
+ _renderBody: function ()
+ {
+ let bodyWrapper = this.document.createElementNS(XHTML_NS, "span");
+ bodyWrapper.className = "message-body-wrapper";
+
+ let bodyFlex = this.document.createElementNS(XHTML_NS, "span");
+ bodyFlex.className = "message-flex-body";
+ bodyWrapper.appendChild(bodyFlex);
+
+ let body = this.document.createElementNS(XHTML_NS, "span");
+ body.className = "message-body devtools-monospace";
+ bodyFlex.appendChild(body);
+
+ let anchor, container = body;
+ if (this._link || this._linkCallback) {
+ container = anchor = this.document.createElementNS(XHTML_NS, "a");
+ anchor.href = this._link || "#";
+ anchor.draggable = false;
+ this._addLinkCallback(anchor, this._linkCallback);
+ body.appendChild(anchor);
+ }
+
+ if (typeof this._message == "function") {
+ container.appendChild(this._message(this));
+ } else if (this._message instanceof Ci.nsIDOMNode) {
+ container.appendChild(this._message);
+ } else {
+ container.textContent = this._message;
+ }
+
+ // do this before repeatNode is rendered - it has no effect afterwards
+ this._repeatID.textContent += "|" + container.textContent;
+
+ let repeatNode = this._renderRepeatNode();
+ let location = this._renderLocation();
+
+ if (repeatNode) {
+ bodyFlex.appendChild(this.document.createTextNode(" "));
+ bodyFlex.appendChild(repeatNode);
+ }
+ if (location) {
+ bodyFlex.appendChild(this.document.createTextNode(" "));
+ bodyFlex.appendChild(location);
+ }
+
+ bodyFlex.appendChild(this.document.createTextNode("\n"));
+
+ if (this.stack) {
+ this._attachment = new Widgets.Stacktrace(this, this.stack).render().element;
+ }
+
+ if (this._attachment) {
+ bodyWrapper.appendChild(this._attachment);
+ }
+
+ return bodyWrapper;
+ },
+
+ /**
+ * Render the repeat bubble DOM element part of the message.
+ * @private
+ * @return Element
+ */
+ _renderRepeatNode: function ()
+ {
+ if (!this._filterDuplicates) {
+ return null;
+ }
+
+ let repeatNode = this.document.createElementNS(XHTML_NS, "span");
+ repeatNode.setAttribute("value", "1");
+ repeatNode.className = "message-repeats";
+ repeatNode.textContent = 1;
+ repeatNode._uid = this.getRepeatID();
+ return repeatNode;
+ },
+
+ /**
+ * Render the message source location DOM element.
+ * @private
+ * @return Element
+ */
+ _renderLocation: function ()
+ {
+ if (!this.location) {
+ return null;
+ }
+
+ let {url, line, column} = this.location;
+ if (IGNORED_SOURCE_URLS.indexOf(url) != -1) {
+ return null;
+ }
+
+ // The ConsoleOutput owner is a WebConsoleFrame instance from webconsole.js.
+ // TODO: move createLocationNode() into this file when bug 778766 is fixed.
+ return this.output.owner.createLocationNode({url, line, column });
+ },
+
+ /**
+ * The click event handler for the message expander arrow element. This method
+ * toggles the display of message details.
+ *
+ * @private
+ * @param nsIDOMEvent ev
+ * The DOM event object.
+ * @see this.toggleDetails()
+ */
+ _onClickCollapsible: function (ev)
+ {
+ ev.preventDefault();
+ this.toggleDetails();
+ },
+
+ /**
+ * Expand/collapse message details.
+ */
+ toggleDetails: function ()
+ {
+ let twisty = this.element.querySelector(".theme-twisty");
+ if (this.element.hasAttribute("open")) {
+ this.element.removeAttribute("open");
+ twisty.removeAttribute("open");
+ } else {
+ this.element.setAttribute("open", true);
+ twisty.setAttribute("open", true);
+ }
+ },
+}); // Messages.Simple.prototype
+
+
+/**
+ * The Extended message.
+ *
+ * @constructor
+ * @extends Messages.Simple
+ * @param array messagePieces
+ * The message to display given as an array of elements. Each array
+ * element can be a DOM node, function, ObjectActor, LongString or
+ * a string.
+ * @param object [options]
+ * Options for rendering this message:
+ * - quoteStrings: boolean that tells if you want strings to be wrapped
+ * in quotes or not.
+ */
+Messages.Extended = function (messagePieces, options = {})
+{
+ Messages.Simple.call(this, null, options);
+
+ this._messagePieces = messagePieces;
+
+ if ("quoteStrings" in options) {
+ this._quoteStrings = options.quoteStrings;
+ }
+
+ this._repeatID.quoteStrings = this._quoteStrings;
+ this._repeatID.messagePieces = JSON.stringify(messagePieces);
+ this._repeatID.actors = new Set(); // using a set to avoid duplicates
+};
+
+Messages.Extended.prototype = extend(Messages.Simple.prototype, {
+ /**
+ * The message pieces displayed by this message instance.
+ * @private
+ * @type array
+ */
+ _messagePieces: null,
+
+ /**
+ * Boolean that tells if the strings displayed in this message are wrapped.
+ * @private
+ * @type boolean
+ */
+ _quoteStrings: true,
+
+ getRepeatID: function ()
+ {
+ if (this._repeatID.uid) {
+ return JSON.stringify({ uid: this._repeatID.uid });
+ }
+
+ // Sets are not stringified correctly. Temporarily switching to an array.
+ let actors = this._repeatID.actors;
+ this._repeatID.actors = [...actors];
+ let result = JSON.stringify(this._repeatID);
+ this._repeatID.actors = actors;
+ return result;
+ },
+
+ render: function ()
+ {
+ let result = this.document.createDocumentFragment();
+
+ for (let i = 0; i < this._messagePieces.length; i++) {
+ let separator = i > 0 ? this._renderBodyPieceSeparator() : null;
+ if (separator) {
+ result.appendChild(separator);
+ }
+
+ let piece = this._messagePieces[i];
+ result.appendChild(this._renderBodyPiece(piece));
+ }
+
+ this._message = result;
+ this._messagePieces = null;
+ return Messages.Simple.prototype.render.call(this);
+ },
+
+ /**
+ * Render the separator between the pieces of the message.
+ *
+ * @private
+ * @return Element
+ */
+ _renderBodyPieceSeparator: function () { return null; },
+
+ /**
+ * Render one piece/element of the message array.
+ *
+ * @private
+ * @param mixed piece
+ * Message element to display - this can be a LongString, ObjectActor,
+ * DOM node or a function to invoke.
+ * @return Element
+ */
+ _renderBodyPiece: function (piece, options = {})
+ {
+ if (piece instanceof Ci.nsIDOMNode) {
+ return piece;
+ }
+ if (typeof piece == "function") {
+ return piece(this);
+ }
+
+ return this._renderValueGrip(piece, options);
+ },
+
+ /**
+ * Render a grip that represents a value received from the server. This method
+ * picks the appropriate widget to render the value with.
+ *
+ * @private
+ * @param object grip
+ * The value grip received from the server.
+ * @param object options
+ * Options for displaying the value. Available options:
+ * - noStringQuotes - boolean that tells the renderer to not use quotes
+ * around strings.
+ * - concise - boolean that tells the renderer to compactly display the
+ * grip. This is typically set to true when the object needs to be
+ * displayed in an array preview, or as a property value in object
+ * previews, etc.
+ * - shorten - boolean that tells the renderer to display a truncated
+ * grip.
+ * @return DOMElement
+ * The DOM element that displays the given grip.
+ */
+ _renderValueGrip: function (grip, options = {})
+ {
+ let isPrimitive = VariablesView.isPrimitive({ value: grip });
+ let isActorGrip = WebConsoleUtils.isActorGrip(grip);
+ let noStringQuotes = !this._quoteStrings;
+ if ("noStringQuotes" in options) {
+ noStringQuotes = options.noStringQuotes;
+ }
+
+ if (isActorGrip) {
+ this._repeatID.actors.add(grip.actor);
+
+ if (!isPrimitive) {
+ return this._renderObjectActor(grip, options);
+ }
+ if (grip.type == "longString") {
+ let widget = new Widgets.LongString(this, grip, options).render();
+ return widget.element;
+ }
+ }
+
+ let unshortenedGrip = grip;
+ if (options.shorten) {
+ grip = this.shortenValueGrip(grip);
+ }
+
+ let result = this.document.createElementNS(XHTML_NS, "span");
+ if (isPrimitive) {
+ if (Widgets.URLString.prototype.containsURL.call(Widgets.URLString.prototype, grip)) {
+ let widget = new Widgets.URLString(this, grip, unshortenedGrip).render();
+ return widget.element;
+ }
+
+ let className = this.getClassNameForValueGrip(grip);
+ if (className) {
+ result.className = className;
+ }
+
+ result.textContent = VariablesView.getString(grip, {
+ noStringQuotes: noStringQuotes,
+ concise: options.concise,
+ });
+ } else {
+ result.textContent = grip;
+ }
+
+ return result;
+ },
+
+ /**
+ * Shorten grips of the type string, leaves other grips unmodified.
+ *
+ * @param object grip
+ * Value grip from the server.
+ * @return object
+ * Possible values of object:
+ * - A shortened string, if original grip was of string type.
+ * - The unmodified input grip, if it wasn't of string type.
+ */
+ shortenValueGrip: function (grip)
+ {
+ let shortVal = grip;
+ if (typeof (grip) == "string") {
+ shortVal = grip.replace(/(\r\n|\n|\r)/gm, " ");
+ if (shortVal.length > MAX_STRING_GRIP_LENGTH) {
+ shortVal = shortVal.substring(0, MAX_STRING_GRIP_LENGTH - 1) + ELLIPSIS;
+ }
+ }
+
+ return shortVal;
+ },
+
+ /**
+ * Get a CodeMirror-compatible class name for a given value grip.
+ *
+ * @param object grip
+ * Value grip from the server.
+ * @return string
+ * The class name for the grip.
+ */
+ getClassNameForValueGrip: function (grip)
+ {
+ let map = {
+ "number": "cm-number",
+ "longstring": "console-string",
+ "string": "console-string",
+ "regexp": "cm-string-2",
+ "boolean": "cm-atom",
+ "-infinity": "cm-atom",
+ "infinity": "cm-atom",
+ "null": "cm-atom",
+ "undefined": "cm-comment",
+ "symbol": "cm-atom"
+ };
+
+ let className = map[typeof grip];
+ if (!className && grip && grip.type) {
+ className = map[grip.type.toLowerCase()];
+ }
+ if (!className && grip && grip.class) {
+ className = map[grip.class.toLowerCase()];
+ }
+
+ return className;
+ },
+
+ /**
+ * Display an object actor with the appropriate renderer.
+ *
+ * @private
+ * @param object objectActor
+ * The ObjectActor to display.
+ * @param object options
+ * Options to use for displaying the ObjectActor.
+ * @see this._renderValueGrip for the available options.
+ * @return DOMElement
+ * The DOM element that displays the object actor.
+ */
+ _renderObjectActor: function (objectActor, options = {})
+ {
+ let widget = Widgets.ObjectRenderers.byClass[objectActor.class];
+
+ let { preview } = objectActor;
+ if ((!widget || (widget.canRender && !widget.canRender(objectActor)))
+ && preview
+ && preview.kind) {
+ widget = Widgets.ObjectRenderers.byKind[preview.kind];
+ }
+
+ if (!widget || (widget.canRender && !widget.canRender(objectActor))) {
+ widget = Widgets.JSObject;
+ }
+
+ let instance = new widget(this, objectActor, options).render();
+ return instance.element;
+ },
+}); // Messages.Extended.prototype
+
+
+
+/**
+ * The JavaScriptEvalOutput message.
+ *
+ * @constructor
+ * @extends Messages.Extended
+ * @param object evalResponse
+ * The evaluation response packet received from the server.
+ * @param string [errorMessage]
+ * Optional error message to display.
+ * @param string [errorDocLink]
+ * Optional error doc URL to link to.
+ */
+Messages.JavaScriptEvalOutput = function (evalResponse, errorMessage, errorDocLink)
+{
+ let severity = "log", msg, quoteStrings = true;
+
+ // Store also the response packet from the back end. It might
+ // be useful to extensions customizing the console output.
+ this.response = evalResponse;
+
+ if (typeof (errorMessage) !== "undefined") {
+ severity = "error";
+ msg = errorMessage;
+ quoteStrings = false;
+ } else {
+ msg = evalResponse.result;
+ }
+
+ let options = {
+ className: "cm-s-mozilla",
+ timestamp: evalResponse.timestamp,
+ category: "output",
+ severity: severity,
+ quoteStrings: quoteStrings,
+ };
+
+ let messages = [msg];
+ if (errorDocLink) {
+ messages.push(errorDocLink);
+ }
+
+ Messages.Extended.call(this, messages, options);
+};
+
+Messages.JavaScriptEvalOutput.prototype = Messages.Extended.prototype;
+
+/**
+ * The ConsoleGeneric message is used for console API calls.
+ *
+ * @constructor
+ * @extends Messages.Extended
+ * @param object packet
+ * The Console API call packet received from the server.
+ */
+Messages.ConsoleGeneric = function (packet)
+{
+ let options = {
+ className: "cm-s-mozilla",
+ timestamp: packet.timeStamp,
+ category: packet.category || "webdev",
+ severity: CONSOLE_API_LEVELS_TO_SEVERITIES[packet.level],
+ prefix: packet.prefix,
+ private: packet.private,
+ filterDuplicates: true,
+ location: {
+ url: packet.filename,
+ line: packet.lineNumber,
+ column: packet.columnNumber
+ },
+ };
+
+ switch (packet.level) {
+ case "count": {
+ let counter = packet.counter, label = counter.label;
+ if (!label) {
+ label = l10n.getStr("noCounterLabel");
+ }
+ Messages.Extended.call(this, [label + ": " + counter.count], options);
+ break;
+ }
+ default:
+ Messages.Extended.call(this, packet.arguments, options);
+ break;
+ }
+
+ this._repeatID.consoleApiLevel = packet.level;
+ this._repeatID.styles = packet.styles;
+ this.stack = this._repeatID.stacktrace = packet.stacktrace;
+ this._styles = packet.styles || [];
+};
+
+Messages.ConsoleGeneric.prototype = extend(Messages.Extended.prototype, {
+ _styles: null,
+
+ _renderBodyPieceSeparator: function ()
+ {
+ return this.document.createTextNode(" ");
+ },
+
+ render: function ()
+ {
+ let result = this.document.createDocumentFragment();
+ this._renderBodyPieces(result);
+
+ this._message = result;
+ this._stacktrace = null;
+
+ Messages.Simple.prototype.render.call(this);
+
+ return this;
+ },
+
+ _renderBodyPieces: function (container)
+ {
+ let lastStyle = null;
+ let stylePieces = this._styles.length > 0 ? this._styles.length : 1;
+
+ for (let i = 0; i < this._messagePieces.length; i++) {
+ // Pieces with an associated style definition come from "%c" formatting.
+ // For body pieces beyond that, add a separator before each one.
+ if (i >= stylePieces) {
+ container.appendChild(this._renderBodyPieceSeparator());
+ }
+
+ let piece = this._messagePieces[i];
+ let style = this._styles[i];
+
+ // No long string support.
+ lastStyle = (style && typeof style == "string") ?
+ this.cleanupStyle(style) : null;
+
+ container.appendChild(this._renderBodyPiece(piece, lastStyle));
+ }
+
+ this._messagePieces = null;
+ this._styles = null;
+ },
+
+ _renderBodyPiece: function (piece, style)
+ {
+ // Skip quotes for top-level strings.
+ let options = { noStringQuotes: true };
+ let elem = Messages.Extended.prototype._renderBodyPiece.call(this, piece, options);
+ let result = elem;
+
+ if (style) {
+ if (elem.nodeType == nodeConstants.ELEMENT_NODE) {
+ elem.style = style;
+ } else {
+ let span = this.document.createElementNS(XHTML_NS, "span");
+ span.style = style;
+ span.appendChild(elem);
+ result = span;
+ }
+ }
+
+ return result;
+ },
+
+ /**
+ * Given a style attribute value, return a cleaned up version of the string
+ * such that:
+ *
+ * - no external URL is allowed to load. See RE_CLEANUP_STYLES.
+ * - only some of the properties are allowed, based on a whitelist. See
+ * RE_ALLOWED_STYLES.
+ *
+ * @param string style
+ * The style string to cleanup.
+ * @return string
+ * The style value after cleanup.
+ */
+ cleanupStyle: function (style)
+ {
+ for (let r of RE_CLEANUP_STYLES) {
+ style = style.replace(r, "notallowed");
+ }
+
+ let dummy = this.output._dummyElement;
+ if (!dummy) {
+ dummy = this.output._dummyElement =
+ this.document.createElementNS(XHTML_NS, "div");
+ }
+ dummy.style = style;
+
+ let toRemove = [];
+ for (let i = 0; i < dummy.style.length; i++) {
+ let prop = dummy.style[i];
+ if (!RE_ALLOWED_STYLES.test(prop)) {
+ toRemove.push(prop);
+ }
+ }
+
+ for (let prop of toRemove) {
+ dummy.style.removeProperty(prop);
+ }
+
+ style = dummy.style.cssText;
+
+ dummy.style = "";
+
+ return style;
+ },
+}); // Messages.ConsoleGeneric.prototype
+
+/**
+ * The ConsoleTrace message is used for console.trace() calls.
+ *
+ * @constructor
+ * @extends Messages.Simple
+ * @param object packet
+ * The Console API call packet received from the server.
+ */
+Messages.ConsoleTrace = function (packet)
+{
+ let options = {
+ className: "cm-s-mozilla",
+ timestamp: packet.timeStamp,
+ category: packet.category || "webdev",
+ severity: CONSOLE_API_LEVELS_TO_SEVERITIES[packet.level],
+ private: packet.private,
+ filterDuplicates: true,
+ location: {
+ url: packet.filename,
+ line: packet.lineNumber,
+ },
+ };
+
+ Messages.Simple.call(this, null, options);
+
+ this._repeatID.consoleApiLevel = packet.level;
+ this._stacktrace = this._repeatID.stacktrace = packet.stacktrace;
+ this._arguments = packet.arguments;
+};
+
+Messages.ConsoleTrace.prototype = extend(Messages.Simple.prototype, {
+ /**
+ * Holds the stackframes received from the server.
+ *
+ * @private
+ * @type array
+ */
+ _stacktrace: null,
+
+ /**
+ * Holds the arguments the content script passed to the console.trace()
+ * method. This array is cleared when the message is initialized, and
+ * associated actors are released.
+ *
+ * @private
+ * @type array
+ */
+ _arguments: null,
+
+ init: function ()
+ {
+ let result = Messages.Simple.prototype.init.apply(this, arguments);
+
+ // We ignore console.trace() arguments. Release object actors.
+ if (Array.isArray(this._arguments)) {
+ for (let arg of this._arguments) {
+ if (WebConsoleUtils.isActorGrip(arg)) {
+ this.output._releaseObject(arg.actor);
+ }
+ }
+ }
+ this._arguments = null;
+
+ return result;
+ },
+
+ render: function () {
+ this._message = this._renderMessage();
+ this._attachment = this._renderStack();
+
+ Messages.Simple.prototype.render.apply(this, arguments);
+ this.element.setAttribute("open", true);
+ return this;
+ },
+
+ /**
+ * Render the console messageNode
+ */
+ _renderMessage: function () {
+ let cmvar = this.document.createElementNS(XHTML_NS, "span");
+ cmvar.className = "cm-variable";
+ cmvar.textContent = "console";
+
+ let cmprop = this.document.createElementNS(XHTML_NS, "span");
+ cmprop.className = "cm-property";
+ cmprop.textContent = "trace";
+
+ let frag = this.document.createDocumentFragment();
+ frag.appendChild(cmvar);
+ frag.appendChild(this.document.createTextNode("."));
+ frag.appendChild(cmprop);
+ frag.appendChild(this.document.createTextNode("():"));
+
+ return frag;
+ },
+
+ /**
+ * Render the stack frames.
+ *
+ * @private
+ * @return DOMElement
+ */
+ _renderStack: function () {
+ return new Widgets.Stacktrace(this, this._stacktrace).render().element;
+ },
+}); // Messages.ConsoleTrace.prototype
+
+/**
+ * The ConsoleTable message is used for console.table() calls.
+ *
+ * @constructor
+ * @extends Messages.Extended
+ * @param object packet
+ * The Console API call packet received from the server.
+ */
+Messages.ConsoleTable = function (packet)
+{
+ let options = {
+ className: "cm-s-mozilla",
+ timestamp: packet.timeStamp,
+ category: packet.category || "webdev",
+ severity: CONSOLE_API_LEVELS_TO_SEVERITIES[packet.level],
+ private: packet.private,
+ filterDuplicates: false,
+ location: {
+ url: packet.filename,
+ line: packet.lineNumber,
+ },
+ };
+
+ this._populateTableData = this._populateTableData.bind(this);
+ this._renderMessage = this._renderMessage.bind(this);
+ Messages.Extended.call(this, [this._renderMessage], options);
+
+ this._repeatID.consoleApiLevel = packet.level;
+ this._arguments = packet.arguments;
+};
+
+Messages.ConsoleTable.prototype = extend(Messages.Extended.prototype, {
+ /**
+ * Holds the arguments the content script passed to the console.table()
+ * method.
+ *
+ * @private
+ * @type array
+ */
+ _arguments: null,
+
+ /**
+ * Array of objects that holds the data to log in the table.
+ *
+ * @private
+ * @type array
+ */
+ _data: null,
+
+ /**
+ * Key value pair of the id and display name for the columns in the table.
+ * Refer to the TableWidget API.
+ *
+ * @private
+ * @type object
+ */
+ _columns: null,
+
+ /**
+ * A promise that resolves when the table data is ready or null if invalid
+ * arguments are provided.
+ *
+ * @private
+ * @type promise|null
+ */
+ _populatePromise: null,
+
+ init: function ()
+ {
+ let result = Messages.Extended.prototype.init.apply(this, arguments);
+ this._data = [];
+ this._columns = {};
+
+ this._populatePromise = this._populateTableData();
+
+ return result;
+ },
+
+ /**
+ * Sets the key value pair of the id and display name for the columns in the
+ * table.
+ *
+ * @private
+ * @param array|string columns
+ * Either a string or array containing the names for the columns in
+ * the output table.
+ */
+ _setColumns: function (columns)
+ {
+ if (columns.class == "Array") {
+ let items = columns.preview.items;
+
+ for (let item of items) {
+ if (typeof item == "string") {
+ this._columns[item] = item;
+ }
+ }
+ } else if (typeof columns == "string" && columns) {
+ this._columns[columns] = columns;
+ }
+ },
+
+ /**
+ * Retrieves the table data and columns from the arguments received from the
+ * server.
+ *
+ * @return Promise|null
+ * Returns a promise that resolves when the table data is ready or
+ * null if the arguments are invalid.
+ */
+ _populateTableData: function ()
+ {
+ let deferred = promise.defer();
+
+ if (this._arguments.length <= 0) {
+ return;
+ }
+
+ let data = this._arguments[0];
+ if (data.class != "Array" && data.class != "Object" &&
+ data.class != "Map" && data.class != "Set" &&
+ data.class != "WeakMap" && data.class != "WeakSet") {
+ return;
+ }
+
+ let hasColumnsArg = false;
+ if (this._arguments.length > 1) {
+ if (data.class == "Object" || data.class == "Array") {
+ this._columns["_index"] = l10n.getStr("table.index");
+ } else {
+ this._columns["_index"] = l10n.getStr("table.iterationIndex");
+ }
+
+ this._setColumns(this._arguments[1]);
+ hasColumnsArg = true;
+ }
+
+ if (data.class == "Object" || data.class == "Array") {
+ // Get the object properties, and parse the key and value properties into
+ // the table data and columns.
+ this.client = new ObjectClient(this.output.owner.jsterm.hud.proxy.client,
+ data);
+ this.client.getPrototypeAndProperties(aResponse => {
+ let {ownProperties} = aResponse;
+ let rowCount = 0;
+ let columnCount = 0;
+
+ for (let index of Object.keys(ownProperties || {})) {
+ // Avoid outputting the length property if the data argument provided
+ // is an array
+ if (data.class == "Array" && index == "length") {
+ continue;
+ }
+
+ if (!hasColumnsArg) {
+ this._columns["_index"] = l10n.getStr("table.index");
+ }
+
+ if (data.class == "Array") {
+ if (index == parseInt(index)) {
+ index = parseInt(index);
+ }
+ }
+
+ let property = ownProperties[index].value;
+ let item = { _index: index };
+
+ if (property.class == "Object" || property.class == "Array") {
+ let {preview} = property;
+ let entries = property.class == "Object" ?
+ preview.ownProperties : preview.items;
+
+ for (let key of Object.keys(entries)) {
+ let value = property.class == "Object" ?
+ preview.ownProperties[key].value : preview.items[key];
+
+ item[key] = this._renderValueGrip(value, { concise: true });
+
+ if (!hasColumnsArg && !(key in this._columns) &&
+ (++columnCount <= TABLE_COLUMN_MAX_ITEMS)) {
+ this._columns[key] = key;
+ }
+ }
+ } else {
+ // Display the value for any non-object data input.
+ item["_value"] = this._renderValueGrip(property, { concise: true });
+
+ if (!hasColumnsArg && !("_value" in this._columns)) {
+ this._columns["_value"] = l10n.getStr("table.value");
+ }
+ }
+
+ this._data.push(item);
+
+ if (++rowCount == TABLE_ROW_MAX_ITEMS) {
+ break;
+ }
+ }
+
+ deferred.resolve();
+ });
+ } else if (data.class == "Map" || data.class == "WeakMap") {
+ let entries = data.preview.entries;
+
+ if (!hasColumnsArg) {
+ this._columns["_index"] = l10n.getStr("table.iterationIndex");
+ this._columns["_key"] = l10n.getStr("table.key");
+ this._columns["_value"] = l10n.getStr("table.value");
+ }
+
+ let rowCount = 0;
+ for (let [key, value] of entries) {
+ let item = {
+ _index: rowCount,
+ _key: this._renderValueGrip(key, { concise: true }),
+ _value: this._renderValueGrip(value, { concise: true })
+ };
+
+ this._data.push(item);
+
+ if (++rowCount == TABLE_ROW_MAX_ITEMS) {
+ break;
+ }
+ }
+
+ deferred.resolve();
+ } else if (data.class == "Set" || data.class == "WeakSet") {
+ let entries = data.preview.items;
+
+ if (!hasColumnsArg) {
+ this._columns["_index"] = l10n.getStr("table.iterationIndex");
+ this._columns["_value"] = l10n.getStr("table.value");
+ }
+
+ let rowCount = 0;
+ for (let entry of entries) {
+ let item = {
+ _index : rowCount,
+ _value: this._renderValueGrip(entry, { concise: true })
+ };
+
+ this._data.push(item);
+
+ if (++rowCount == TABLE_ROW_MAX_ITEMS) {
+ break;
+ }
+ }
+
+ deferred.resolve();
+ }
+
+ return deferred.promise;
+ },
+
+ render: function ()
+ {
+ this._attachment = this._renderTable();
+ Messages.Extended.prototype.render.apply(this, arguments);
+ this.element.setAttribute("open", true);
+ return this;
+ },
+
+ _renderMessage: function () {
+ let cmvar = this.document.createElementNS(XHTML_NS, "span");
+ cmvar.className = "cm-variable";
+ cmvar.textContent = "console";
+
+ let cmprop = this.document.createElementNS(XHTML_NS, "span");
+ cmprop.className = "cm-property";
+ cmprop.textContent = "table";
+
+ let frag = this.document.createDocumentFragment();
+ frag.appendChild(cmvar);
+ frag.appendChild(this.document.createTextNode("."));
+ frag.appendChild(cmprop);
+ frag.appendChild(this.document.createTextNode("():"));
+
+ return frag;
+ },
+
+ /**
+ * Render the table.
+ *
+ * @private
+ * @return DOMElement
+ */
+ _renderTable: function () {
+ let result = this.document.createElementNS(XHTML_NS, "div");
+
+ if (this._populatePromise) {
+ this._populatePromise.then(() => {
+ if (this._data.length > 0) {
+ let widget = new Widgets.Table(this, this._data, this._columns).render();
+ result.appendChild(widget.element);
+ }
+
+ result.scrollIntoView();
+ this.output.owner.emit("messages-table-rendered");
+
+ // Release object actors
+ if (Array.isArray(this._arguments)) {
+ for (let arg of this._arguments) {
+ if (WebConsoleUtils.isActorGrip(arg)) {
+ this.output._releaseObject(arg.actor);
+ }
+ }
+ }
+ this._arguments = null;
+ });
+ }
+
+ return result;
+ },
+}); // Messages.ConsoleTable.prototype
+
+var Widgets = {};
+
+/**
+ * The base widget class.
+ *
+ * @constructor
+ * @param object message
+ * The owning message.
+ */
+Widgets.BaseWidget = function (message)
+{
+ this.message = message;
+};
+
+Widgets.BaseWidget.prototype = {
+ /**
+ * The owning message object.
+ * @type object
+ */
+ message: null,
+
+ /**
+ * The DOM element of the rendered widget.
+ * @type Element
+ */
+ element: null,
+
+ /**
+ * Getter for the DOM document that holds the output.
+ * @type Document
+ */
+ get document() {
+ return this.message.document;
+ },
+
+ /**
+ * The ConsoleOutput instance that owns this widget instance.
+ */
+ get output() {
+ return this.message.output;
+ },
+
+ /**
+ * Render the widget DOM element.
+ * @return this
+ */
+ render: function () { },
+
+ /**
+ * Destroy this widget instance.
+ */
+ destroy: function () { },
+
+ /**
+ * Helper for creating DOM elements for widgets.
+ *
+ * Usage:
+ * this.el("tag#id.class.names"); // create element "tag" with ID "id" and
+ * two class names, .class and .names.
+ *
+ * this.el("span", { attr1: "value1", ... }) // second argument can be an
+ * object that holds element attributes and values for the new DOM element.
+ *
+ * this.el("p", { attr1: "value1", ... }, "text content"); // the third
+ * argument can include the default .textContent of the new DOM element.
+ *
+ * this.el("p", "text content"); // if the second argument is not an object,
+ * it will be used as .textContent for the new DOM element.
+ *
+ * @param string tagNameIdAndClasses
+ * Tag name for the new element, optionally followed by an ID and/or
+ * class names. Examples: "span", "div#fooId", "div.class.names",
+ * "p#id.class".
+ * @param string|object [attributesOrTextContent]
+ * If this argument is an object it will be used to set the attributes
+ * of the new DOM element. Otherwise, the value becomes the
+ * .textContent of the new DOM element.
+ * @param string [textContent]
+ * If this argument is provided the value is used as the textContent of
+ * the new DOM element.
+ * @return DOMElement
+ * The new DOM element.
+ */
+ el: function (tagNameIdAndClasses)
+ {
+ let attrs, text;
+ if (typeof arguments[1] == "object") {
+ attrs = arguments[1];
+ text = arguments[2];
+ } else {
+ text = arguments[1];
+ }
+
+ let tagName = tagNameIdAndClasses.split(/#|\./)[0];
+
+ let elem = this.document.createElementNS(XHTML_NS, tagName);
+ for (let name of Object.keys(attrs || {})) {
+ elem.setAttribute(name, attrs[name]);
+ }
+ if (text !== undefined && text !== null) {
+ elem.textContent = text;
+ }
+
+ let idAndClasses = tagNameIdAndClasses.match(/([#.][^#.]+)/g);
+ for (let idOrClass of (idAndClasses || [])) {
+ if (idOrClass.charAt(0) == "#") {
+ elem.id = idOrClass.substr(1);
+ } else {
+ elem.classList.add(idOrClass.substr(1));
+ }
+ }
+
+ return elem;
+ },
+};
+
+/**
+ * The timestamp widget.
+ *
+ * @constructor
+ * @param object message
+ * The owning message.
+ * @param number timestamp
+ * The UNIX timestamp to display.
+ */
+Widgets.MessageTimestamp = function (message, timestamp)
+{
+ Widgets.BaseWidget.call(this, message);
+ this.timestamp = timestamp;
+};
+
+Widgets.MessageTimestamp.prototype = extend(Widgets.BaseWidget.prototype, {
+ /**
+ * The UNIX timestamp.
+ * @type number
+ */
+ timestamp: 0,
+
+ render: function ()
+ {
+ if (this.element) {
+ return this;
+ }
+
+ this.element = this.document.createElementNS(XHTML_NS, "span");
+ this.element.className = "timestamp devtools-monospace";
+ this.element.textContent = l10n.timestampString(this.timestamp) + " ";
+
+ return this;
+ },
+}); // Widgets.MessageTimestamp.prototype
+
+
+/**
+ * The URLString widget, for rendering strings where at least one token is a
+ * URL.
+ *
+ * @constructor
+ * @param object message
+ * The owning message.
+ * @param string str
+ * The string, which contains at least one valid URL.
+ * @param string unshortenedStr
+ * The unshortened form of the string, if it was shortened.
+ */
+Widgets.URLString = function (message, str, unshortenedStr)
+{
+ Widgets.BaseWidget.call(this, message);
+ this.str = str;
+ this.unshortenedStr = unshortenedStr;
+};
+
+Widgets.URLString.prototype = extend(Widgets.BaseWidget.prototype, {
+ /**
+ * The string to format, which contains at least one valid URL.
+ * @type string
+ */
+ str: "",
+
+ render: function ()
+ {
+ if (this.element) {
+ return this;
+ }
+
+ // The rendered URLString will be a <span> containing a number of text
+ // <spans> for non-URL tokens and <a>'s for URL tokens.
+ this.element = this.el("span", {
+ class: "console-string"
+ });
+ this.element.appendChild(this._renderText("\""));
+
+ // As we walk through the tokens of the source string, we make sure to preserve
+ // the original whitespace that separated the tokens.
+ let tokens = this.str.split(/\s+/);
+ let textStart = 0;
+ let tokenStart;
+ for (let i = 0; i < tokens.length; i++) {
+ let token = tokens[i];
+ let unshortenedToken;
+ tokenStart = this.str.indexOf(token, textStart);
+ if (this._isURL(token)) {
+ // The last URL in the string might be shortened. If so, get the
+ // real URL so the rendered link can point to it.
+ if (i === tokens.length - 1 && this.unshortenedStr) {
+ unshortenedToken = this.unshortenedStr.slice(tokenStart).split(/\s+/, 1)[0];
+ }
+ this.element.appendChild(this._renderText(this.str.slice(textStart, tokenStart)));
+ textStart = tokenStart + token.length;
+ this.element.appendChild(this._renderURL(token, unshortenedToken));
+ }
+ }
+
+ // Clean up any non-URL text at the end of the source string.
+ this.element.appendChild(this._renderText(this.str.slice(textStart, this.str.length)));
+ this.element.appendChild(this._renderText("\""));
+
+ return this;
+ },
+
+ /**
+ * Determines whether a grip is a string containing a URL.
+ *
+ * @param string grip
+ * The grip, which may contain a URL.
+ * @return boolean
+ * Whether the grip is a string containing a URL.
+ */
+ containsURL: function (grip)
+ {
+ if (typeof grip != "string") {
+ return false;
+ }
+
+ let tokens = grip.split(/\s+/);
+ return tokens.some(this._isURL);
+ },
+
+ /**
+ * Determines whether a string token is a valid URL.
+ *
+ * @param string token
+ * The token.
+ * @return boolean
+ * Whenther the token is a URL.
+ */
+ _isURL: function (token) {
+ try {
+ if (!validProtocols.test(token)) {
+ return false;
+ }
+ new URL(token);
+ return true;
+ } catch (e) {
+ return false;
+ }
+ },
+
+ /**
+ * Renders a string as a URL.
+ *
+ * @param string url
+ * The string to be rendered as a url.
+ * @param string fullUrl
+ * The unshortened form of the URL, if it was shortened.
+ * @return DOMElement
+ * An element containing the rendered string.
+ */
+ _renderURL: function (url, fullUrl)
+ {
+ let unshortened = fullUrl || url;
+ let result = this.el("a", {
+ class: "url",
+ title: unshortened,
+ href: unshortened,
+ draggable: false
+ }, url);
+ this.message._addLinkCallback(result);
+ return result;
+ },
+
+ _renderText: function (text) {
+ return this.el("span", text);
+ },
+}); // Widgets.URLString.prototype
+
+/**
+ * Widget used for displaying ObjectActors that have no specialised renderers.
+ *
+ * @constructor
+ * @param object message
+ * The owning message.
+ * @param object objectActor
+ * The ObjectActor to display.
+ * @param object [options]
+ * Options for displaying the given ObjectActor. See
+ * Messages.Extended.prototype._renderValueGrip for the available
+ * options.
+ */
+Widgets.JSObject = function (message, objectActor, options = {})
+{
+ Widgets.BaseWidget.call(this, message);
+ this.objectActor = objectActor;
+ this.options = options;
+ this._onClick = this._onClick.bind(this);
+};
+
+Widgets.JSObject.prototype = extend(Widgets.BaseWidget.prototype, {
+ /**
+ * The ObjectActor displayed by the widget.
+ * @type object
+ */
+ objectActor: null,
+
+ render: function ()
+ {
+ if (!this.element) {
+ this._render();
+ }
+
+ return this;
+ },
+
+ _render: function ()
+ {
+ let str = VariablesView.getString(this.objectActor, this.options);
+ let className = this.message.getClassNameForValueGrip(this.objectActor);
+ if (!className && this.objectActor.class == "Object") {
+ className = "cm-variable";
+ }
+
+ this.element = this._anchor(str, { className: className });
+ },
+
+ /**
+ * Render a concise representation of an object.
+ */
+ _renderConciseObject: function ()
+ {
+ this.element = this._anchor(this.objectActor.class,
+ { className: "cm-variable" });
+ },
+
+ /**
+ * Render the `<class> { ` prefix of an object.
+ */
+ _renderObjectPrefix: function ()
+ {
+ let { kind } = this.objectActor.preview;
+ this.element = this.el("span.kind-" + kind);
+ this._anchor(this.objectActor.class, { className: "cm-variable" });
+ this._text(" { ");
+ },
+
+ /**
+ * Render the ` }` suffix of an object.
+ */
+ _renderObjectSuffix: function ()
+ {
+ this._text(" }");
+ },
+
+ /**
+ * Render an object property.
+ *
+ * @param String key
+ * The property name.
+ * @param Object value
+ * The property value, as an RDP grip.
+ * @param nsIDOMNode container
+ * The container node to render to.
+ * @param Boolean needsComma
+ * True if there was another property before this one and we need to
+ * separate them with a comma.
+ * @param Boolean valueIsText
+ * Add the value as is, don't treat it as a grip and pass it to
+ * `_renderValueGrip`.
+ */
+ _renderObjectProperty: function (key, value, container, needsComma, valueIsText = false)
+ {
+ if (needsComma) {
+ this._text(", ");
+ }
+
+ container.appendChild(this.el("span.cm-property", key));
+ this._text(": ");
+
+ if (valueIsText) {
+ this._text(value);
+ } else {
+ let valueElem = this.message._renderValueGrip(value, { concise: true, shorten: true });
+ container.appendChild(valueElem);
+ }
+ },
+
+ /**
+ * Render this object's properties.
+ *
+ * @param nsIDOMNode container
+ * The container node to render to.
+ * @param Boolean needsComma
+ * True if there was another property before this one and we need to
+ * separate them with a comma.
+ */
+ _renderObjectProperties: function (container, needsComma)
+ {
+ let { preview } = this.objectActor;
+ let { ownProperties, safeGetterValues } = preview;
+
+ let shown = 0;
+
+ let getValue = desc => {
+ if (desc.get) {
+ return "Getter";
+ } else if (desc.set) {
+ return "Setter";
+ } else {
+ return desc.value;
+ }
+ };
+
+ for (let key of Object.keys(ownProperties || {})) {
+ this._renderObjectProperty(key, getValue(ownProperties[key]), container,
+ shown > 0 || needsComma,
+ ownProperties[key].get || ownProperties[key].set);
+ shown++;
+ }
+
+ let ownPropertiesShown = shown;
+
+ for (let key of Object.keys(safeGetterValues || {})) {
+ this._renderObjectProperty(key, safeGetterValues[key].getterValue,
+ container, shown > 0 || needsComma);
+ shown++;
+ }
+
+ if (typeof preview.ownPropertiesLength == "number" &&
+ ownPropertiesShown < preview.ownPropertiesLength) {
+ this._text(", ");
+
+ let n = preview.ownPropertiesLength - ownPropertiesShown;
+ let str = VariablesView.stringifiers._getNMoreString(n);
+ this._anchor(str);
+ }
+ },
+
+ /**
+ * Render an anchor with a given text content and link.
+ *
+ * @private
+ * @param string text
+ * Text to show in the anchor.
+ * @param object [options]
+ * Available options:
+ * - onClick (function): "click" event handler.By default a click on
+ * the anchor opens the variables view for the current object actor
+ * (this.objectActor).
+ * - href (string): if given the string is used as a link, and clicks
+ * on the anchor open the link in a new tab.
+ * - appendTo (DOMElement): append the element to the given DOM
+ * element. If not provided, the anchor is appended to |this.element|
+ * if it is available. If |appendTo| is provided and if it is a falsy
+ * value, the anchor is not appended to any element.
+ * @return DOMElement
+ * The DOM element of the new anchor.
+ */
+ _anchor: function (text, options = {})
+ {
+ if (!options.onClick) {
+ // If the anchor has an URL, open it in a new tab. If not, show the
+ // current object actor.
+ options.onClick = options.href ? this._onClickAnchor : this._onClick;
+ }
+
+ options.onContextMenu = options.onContextMenu || this._onContextMenu;
+
+ let anchor = this.el("a", {
+ class: options.className,
+ draggable: false,
+ href: options.href || "#",
+ }, text);
+
+ this.message._addLinkCallback(anchor, options.onClick);
+
+ anchor.addEventListener("contextmenu", options.onContextMenu.bind(this));
+
+ if (options.appendTo) {
+ options.appendTo.appendChild(anchor);
+ } else if (!("appendTo" in options) && this.element) {
+ this.element.appendChild(anchor);
+ }
+
+ return anchor;
+ },
+
+ openObjectInVariablesView: function ()
+ {
+ this.output.openVariablesView({
+ label: VariablesView.getString(this.objectActor, { concise: true }),
+ objectActor: this.objectActor,
+ autofocus: true,
+ });
+ },
+
+ storeObjectInWindow: function ()
+ {
+ let evalString = `{ let i = 0;
+ while (this.hasOwnProperty("temp" + i) && i < 1000) {
+ i++;
+ }
+ this["temp" + i] = _self;
+ "temp" + i;
+ }`;
+ let options = {
+ selectedObjectActor: this.objectActor.actor,
+ };
+
+ this.output.owner.jsterm.requestEvaluation(evalString, options).then((res) => {
+ this.output.owner.jsterm.focus();
+ this.output.owner.jsterm.setInputValue(res.result);
+ });
+ },
+
+ /**
+ * The click event handler for objects shown inline.
+ * @private
+ */
+ _onClick: function ()
+ {
+ this.openObjectInVariablesView();
+ },
+
+ _onContextMenu: function (ev) {
+ // TODO offer a nice API for the context menu.
+ // Probably worth to take a look at Firebug's way
+ // https://github.com/firebug/firebug/blob/master/extension/content/firebug/chrome/menu.js
+ let doc = ev.target.ownerDocument;
+ let cmPopup = doc.getElementById("output-contextmenu");
+
+ let openInVarViewCmd = doc.getElementById("menu_openInVarView");
+ let openVarView = this.openObjectInVariablesView.bind(this);
+ openInVarViewCmd.addEventListener("command", openVarView);
+ openInVarViewCmd.removeAttribute("disabled");
+ cmPopup.addEventListener("popuphiding", function onPopupHiding() {
+ cmPopup.removeEventListener("popuphiding", onPopupHiding);
+ openInVarViewCmd.removeEventListener("command", openVarView);
+ openInVarViewCmd.setAttribute("disabled", "true");
+ });
+
+ // 'Store as global variable' command isn't supported on pre-44 servers,
+ // so remove it from the menu in that case.
+ let storeInGlobalCmd = doc.getElementById("menu_storeAsGlobal");
+ if (!this.output.webConsoleClient.traits.selectedObjectActor) {
+ storeInGlobalCmd.remove();
+ } else if (storeInGlobalCmd) {
+ let storeObjectInWindow = this.storeObjectInWindow.bind(this);
+ storeInGlobalCmd.addEventListener("command", storeObjectInWindow);
+ storeInGlobalCmd.removeAttribute("disabled");
+ cmPopup.addEventListener("popuphiding", function onPopupHiding() {
+ cmPopup.removeEventListener("popuphiding", onPopupHiding);
+ storeInGlobalCmd.removeEventListener("command", storeObjectInWindow);
+ storeInGlobalCmd.setAttribute("disabled", "true");
+ });
+ }
+ },
+
+ /**
+ * Add a string to the message.
+ *
+ * @private
+ * @param string str
+ * String to add.
+ * @param DOMElement [target = this.element]
+ * Optional DOM element to append the string to. The default is
+ * this.element.
+ */
+ _text: function (str, target = this.element)
+ {
+ target.appendChild(this.document.createTextNode(str));
+ },
+}); // Widgets.JSObject.prototype
+
+Widgets.ObjectRenderers = {};
+Widgets.ObjectRenderers.byKind = {};
+Widgets.ObjectRenderers.byClass = {};
+
+/**
+ * Add an object renderer.
+ *
+ * @param object obj
+ * An object that represents the renderer. Properties:
+ * - byClass (string, optional): this renderer will be used for the given
+ * object class.
+ * - byKind (string, optional): this renderer will be used for the given
+ * object kind.
+ * One of byClass or byKind must be provided.
+ * - extends (object, optional): the renderer object extends the given
+ * object. Default: Widgets.JSObject.
+ * - canRender (function, optional): this method is invoked when
+ * a candidate object needs to be displayed. The method is invoked as
+ * a static method, as such, none of the properties of the renderer
+ * object will be available. You get one argument: the object actor grip
+ * received from the server. If the method returns true, then this
+ * renderer is used for displaying the object, otherwise not.
+ * - initialize (function, optional): the constructor of the renderer
+ * widget. This function is invoked with the following arguments: the
+ * owner message object instance, the object actor grip to display, and
+ * an options object. See Messages.Extended.prototype._renderValueGrip()
+ * for details about the options object.
+ * - render (function, required): the method that displays the given
+ * object actor.
+ */
+Widgets.ObjectRenderers.add = function (obj)
+{
+ let extendObj = obj.extends || Widgets.JSObject;
+
+ let constructor = function () {
+ if (obj.initialize) {
+ obj.initialize.apply(this, arguments);
+ } else {
+ extendObj.apply(this, arguments);
+ }
+ };
+
+ let proto = WebConsoleUtils.cloneObject(obj, false, function (key) {
+ if (key == "initialize" || key == "canRender" ||
+ (key == "render" && extendObj === Widgets.JSObject)) {
+ return false;
+ }
+ return true;
+ });
+
+ if (extendObj === Widgets.JSObject) {
+ proto._render = obj.render;
+ }
+
+ constructor.canRender = obj.canRender;
+ constructor.prototype = extend(extendObj.prototype, proto);
+
+ if (obj.byClass) {
+ Widgets.ObjectRenderers.byClass[obj.byClass] = constructor;
+ } else if (obj.byKind) {
+ Widgets.ObjectRenderers.byKind[obj.byKind] = constructor;
+ } else {
+ throw new Error("You are adding an object renderer without any byClass or " +
+ "byKind property.");
+ }
+};
+
+
+/**
+ * The widget used for displaying Date objects.
+ */
+Widgets.ObjectRenderers.add({
+ byClass: "Date",
+
+ render: function ()
+ {
+ let {preview} = this.objectActor;
+ this.element = this.el("span.class-" + this.objectActor.class);
+
+ let anchorText = this.objectActor.class;
+ let anchorClass = "cm-variable";
+ if (preview && "timestamp" in preview && typeof preview.timestamp != "number") {
+ anchorText = new Date(preview.timestamp).toString(); // invalid date
+ anchorClass = "";
+ }
+
+ this._anchor(anchorText, { className: anchorClass });
+
+ if (!preview || !("timestamp" in preview) || typeof preview.timestamp != "number") {
+ return;
+ }
+
+ this._text(" ");
+
+ let elem = this.el("span.cm-string-2", new Date(preview.timestamp).toISOString());
+ this.element.appendChild(elem);
+ },
+});
+
+/**
+ * The widget used for displaying Function objects.
+ */
+Widgets.ObjectRenderers.add({
+ byClass: "Function",
+
+ render: function ()
+ {
+ let grip = this.objectActor;
+ this.element = this.el("span.class-" + this.objectActor.class);
+
+ // TODO: Bug 948484 - support arrow functions and ES6 generators
+ let name = grip.userDisplayName || grip.displayName || grip.name || "";
+ name = VariablesView.getString(name, { noStringQuotes: true });
+
+ let str = this.options.concise ? name || "function " : "function " + name;
+
+ if (this.options.concise) {
+ this._anchor(name || "function", {
+ className: name ? "cm-variable" : "cm-keyword",
+ });
+ if (!name) {
+ this._text(" ");
+ }
+ } else if (name) {
+ this.element.appendChild(this.el("span.cm-keyword", "function"));
+ this._text(" ");
+ this._anchor(name, { className: "cm-variable" });
+ } else {
+ this._anchor("function", { className: "cm-keyword" });
+ this._text(" ");
+ }
+
+ this._text("(");
+
+ // TODO: Bug 948489 - Support functions with destructured parameters and
+ // rest parameters
+ let params = grip.parameterNames || [];
+ let shown = 0;
+ for (let param of params) {
+ if (shown > 0) {
+ this._text(", ");
+ }
+ this.element.appendChild(this.el("span.cm-def", param));
+ shown++;
+ }
+
+ this._text(")");
+ },
+
+ _onClick: function () {
+ let location = this.objectActor.location;
+ if (location && IGNORED_SOURCE_URLS.indexOf(location.url) === -1) {
+ this.output.openLocationInDebugger(location);
+ }
+ else {
+ this.openObjectInVariablesView();
+ }
+ }
+}); // Widgets.ObjectRenderers.byClass.Function
+
+/**
+ * The widget used for displaying ArrayLike objects.
+ */
+Widgets.ObjectRenderers.add({
+ byKind: "ArrayLike",
+
+ render: function ()
+ {
+ let {preview} = this.objectActor;
+ let {items} = preview;
+ this.element = this.el("span.kind-" + preview.kind);
+
+ this._anchor(this.objectActor.class, { className: "cm-variable" });
+
+ if (!items || this.options.concise) {
+ this._text("[");
+ this.element.appendChild(this.el("span.cm-number", preview.length));
+ this._text("]");
+ return this;
+ }
+
+ this._text(" [ ");
+
+ let isFirst = true;
+ let emptySlots = 0;
+ // A helper that renders a comma between items if isFirst == false.
+ let renderSeparator = () => !isFirst && this._text(", ");
+
+ for (let item of items) {
+ if (item === null) {
+ emptySlots++;
+ }
+ else {
+ renderSeparator();
+ isFirst = false;
+
+ if (emptySlots) {
+ this._renderEmptySlots(emptySlots);
+ emptySlots = 0;
+ }
+
+ let elem = this.message._renderValueGrip(item, { concise: true, shorten: true });
+ this.element.appendChild(elem);
+ }
+ }
+
+ if (emptySlots) {
+ renderSeparator();
+ this._renderEmptySlots(emptySlots, false);
+ }
+
+ let shown = items.length;
+ if (shown < preview.length) {
+ this._text(", ");
+
+ let n = preview.length - shown;
+ let str = VariablesView.stringifiers._getNMoreString(n);
+ this._anchor(str);
+ }
+
+ this._text(" ]");
+ },
+
+ _renderEmptySlots: function (aNumSlots, aAppendComma = true) {
+ let slotLabel = l10n.getStr("emptySlotLabel");
+ let slotText = PluralForm.get(aNumSlots, slotLabel);
+ this._text("<" + slotText.replace("#1", aNumSlots) + ">");
+ if (aAppendComma) {
+ this._text(", ");
+ }
+ },
+
+}); // Widgets.ObjectRenderers.byKind.ArrayLike
+
+/**
+ * The widget used for displaying MapLike objects.
+ */
+Widgets.ObjectRenderers.add({
+ byKind: "MapLike",
+
+ render: function ()
+ {
+ let {preview} = this.objectActor;
+ let {entries} = preview;
+
+ let container = this.element = this.el("span.kind-" + preview.kind);
+ this._anchor(this.objectActor.class, { className: "cm-variable" });
+
+ if (!entries || this.options.concise) {
+ if (typeof preview.size == "number") {
+ this._text("[");
+ container.appendChild(this.el("span.cm-number", preview.size));
+ this._text("]");
+ }
+ return;
+ }
+
+ this._text(" { ");
+
+ let shown = 0;
+ for (let [key, value] of entries) {
+ if (shown > 0) {
+ this._text(", ");
+ }
+
+ let keyElem = this.message._renderValueGrip(key, {
+ concise: true,
+ noStringQuotes: true,
+ });
+
+ // Strings are property names.
+ if (keyElem.classList && keyElem.classList.contains("console-string")) {
+ keyElem.classList.remove("console-string");
+ keyElem.classList.add("cm-property");
+ }
+
+ container.appendChild(keyElem);
+
+ this._text(": ");
+
+ let valueElem = this.message._renderValueGrip(value, { concise: true });
+ container.appendChild(valueElem);
+
+ shown++;
+ }
+
+ if (typeof preview.size == "number" && shown < preview.size) {
+ this._text(", ");
+
+ let n = preview.size - shown;
+ let str = VariablesView.stringifiers._getNMoreString(n);
+ this._anchor(str);
+ }
+
+ this._text(" }");
+ },
+}); // Widgets.ObjectRenderers.byKind.MapLike
+
+/**
+ * The widget used for displaying objects with a URL.
+ */
+Widgets.ObjectRenderers.add({
+ byKind: "ObjectWithURL",
+
+ render: function ()
+ {
+ this.element = this._renderElement(this.objectActor,
+ this.objectActor.preview.url);
+ },
+
+ _renderElement: function (objectActor, url)
+ {
+ let container = this.el("span.kind-" + objectActor.preview.kind);
+
+ this._anchor(objectActor.class, {
+ className: "cm-variable",
+ appendTo: container,
+ });
+
+ if (!VariablesView.isFalsy({ value: url })) {
+ this._text(" \u2192 ", container);
+ let shortUrl = getSourceNames(url)[this.options.concise ? "short" : "long"];
+ this._anchor(shortUrl, { href: url, appendTo: container });
+ }
+
+ return container;
+ },
+}); // Widgets.ObjectRenderers.byKind.ObjectWithURL
+
+/**
+ * The widget used for displaying objects with a string next to them.
+ */
+Widgets.ObjectRenderers.add({
+ byKind: "ObjectWithText",
+
+ render: function ()
+ {
+ let {preview} = this.objectActor;
+ this.element = this.el("span.kind-" + preview.kind);
+
+ this._anchor(this.objectActor.class, { className: "cm-variable" });
+
+ if (!this.options.concise) {
+ this._text(" ");
+ this.element.appendChild(this.el("span.theme-fg-color6",
+ VariablesView.getString(preview.text)));
+ }
+ },
+});
+
+/**
+ * The widget used for displaying DOM event previews.
+ */
+Widgets.ObjectRenderers.add({
+ byKind: "DOMEvent",
+
+ render: function ()
+ {
+ let {preview} = this.objectActor;
+
+ let container = this.element = this.el("span.kind-" + preview.kind);
+
+ this._anchor(preview.type || this.objectActor.class,
+ { className: "cm-variable" });
+
+ if (this.options.concise) {
+ return;
+ }
+
+ if (preview.eventKind == "key" && preview.modifiers &&
+ preview.modifiers.length) {
+ this._text(" ");
+
+ let mods = 0;
+ for (let mod of preview.modifiers) {
+ if (mods > 0) {
+ this._text("-");
+ }
+ container.appendChild(this.el("span.cm-keyword", mod));
+ mods++;
+ }
+ }
+
+ this._text(" { ");
+
+ let shown = 0;
+ if (preview.target) {
+ container.appendChild(this.el("span.cm-property", "target"));
+ this._text(": ");
+ let target = this.message._renderValueGrip(preview.target, { concise: true });
+ container.appendChild(target);
+ shown++;
+ }
+
+ for (let key of Object.keys(preview.properties || {})) {
+ if (shown > 0) {
+ this._text(", ");
+ }
+
+ container.appendChild(this.el("span.cm-property", key));
+ this._text(": ");
+
+ let value = preview.properties[key];
+ let valueElem = this.message._renderValueGrip(value, { concise: true });
+ container.appendChild(valueElem);
+
+ shown++;
+ }
+
+ this._text(" }");
+ },
+}); // Widgets.ObjectRenderers.byKind.DOMEvent
+
+/**
+ * The widget used for displaying DOM node previews.
+ */
+Widgets.ObjectRenderers.add({
+ byKind: "DOMNode",
+
+ canRender: function (objectActor) {
+ let {preview} = objectActor;
+ if (!preview) {
+ return false;
+ }
+
+ switch (preview.nodeType) {
+ case nodeConstants.DOCUMENT_NODE:
+ case nodeConstants.ATTRIBUTE_NODE:
+ case nodeConstants.TEXT_NODE:
+ case nodeConstants.COMMENT_NODE:
+ case nodeConstants.DOCUMENT_FRAGMENT_NODE:
+ case nodeConstants.ELEMENT_NODE:
+ return true;
+ default:
+ return false;
+ }
+ },
+
+ render: function ()
+ {
+ switch (this.objectActor.preview.nodeType) {
+ case nodeConstants.DOCUMENT_NODE:
+ this._renderDocumentNode();
+ break;
+ case nodeConstants.ATTRIBUTE_NODE: {
+ let {preview} = this.objectActor;
+ this.element = this.el("span.attributeNode.kind-" + preview.kind);
+ let attr = this._renderAttributeNode(preview.nodeName, preview.value, true);
+ this.element.appendChild(attr);
+ break;
+ }
+ case nodeConstants.TEXT_NODE:
+ this._renderTextNode();
+ break;
+ case nodeConstants.COMMENT_NODE:
+ this._renderCommentNode();
+ break;
+ case nodeConstants.DOCUMENT_FRAGMENT_NODE:
+ this._renderDocumentFragmentNode();
+ break;
+ case nodeConstants.ELEMENT_NODE:
+ this._renderElementNode();
+ break;
+ default:
+ throw new Error("Unsupported nodeType: " + preview.nodeType);
+ }
+ },
+
+ _renderDocumentNode: function ()
+ {
+ let fn =
+ Widgets.ObjectRenderers.byKind.ObjectWithURL.prototype._renderElement;
+ this.element = fn.call(this, this.objectActor,
+ this.objectActor.preview.location);
+ this.element.classList.add("documentNode");
+ },
+
+ _renderAttributeNode: function (nodeName, nodeValue, addLink)
+ {
+ let value = VariablesView.getString(nodeValue, { noStringQuotes: true });
+
+ let fragment = this.document.createDocumentFragment();
+ if (addLink) {
+ this._anchor(nodeName, { className: "cm-attribute", appendTo: fragment });
+ } else {
+ fragment.appendChild(this.el("span.cm-attribute", nodeName));
+ }
+
+ this._text("=\"", fragment);
+ fragment.appendChild(this.el("span.theme-fg-color6", escapeHTML(value)));
+ this._text("\"", fragment);
+
+ return fragment;
+ },
+
+ _renderTextNode: function ()
+ {
+ let {preview} = this.objectActor;
+ this.element = this.el("span.textNode.kind-" + preview.kind);
+
+ this._anchor(preview.nodeName, { className: "cm-variable" });
+ this._text(" ");
+
+ let text = VariablesView.getString(preview.textContent);
+ this.element.appendChild(this.el("span.console-string", text));
+ },
+
+ _renderCommentNode: function ()
+ {
+ let {preview} = this.objectActor;
+ let comment = "<!-- " + VariablesView.getString(preview.textContent, {
+ noStringQuotes: true,
+ }) + " -->";
+
+ this.element = this._anchor(comment, {
+ className: "kind-" + preview.kind + " commentNode cm-comment",
+ });
+ },
+
+ _renderDocumentFragmentNode: function ()
+ {
+ let {preview} = this.objectActor;
+ let {childNodes} = preview;
+ let container = this.element = this.el("span.documentFragmentNode.kind-" +
+ preview.kind);
+
+ this._anchor(this.objectActor.class, { className: "cm-variable" });
+
+ if (!childNodes || this.options.concise) {
+ this._text("[");
+ container.appendChild(this.el("span.cm-number", preview.childNodesLength));
+ this._text("]");
+ return;
+ }
+
+ this._text(" [ ");
+
+ let shown = 0;
+ for (let item of childNodes) {
+ if (shown > 0) {
+ this._text(", ");
+ }
+
+ let elem = this.message._renderValueGrip(item, { concise: true });
+ container.appendChild(elem);
+ shown++;
+ }
+
+ if (shown < preview.childNodesLength) {
+ this._text(", ");
+
+ let n = preview.childNodesLength - shown;
+ let str = VariablesView.stringifiers._getNMoreString(n);
+ this._anchor(str);
+ }
+
+ this._text(" ]");
+ },
+
+ _renderElementNode: function ()
+ {
+ let doc = this.document;
+ let {attributes, nodeName} = this.objectActor.preview;
+
+ this.element = this.el("span." + "kind-" + this.objectActor.preview.kind + ".elementNode");
+
+ this._text("<");
+ let openTag = this.el("span.cm-tag");
+ this.element.appendChild(openTag);
+
+ let tagName = this._anchor(nodeName, {
+ className: "cm-tag",
+ appendTo: openTag
+ });
+
+ if (this.options.concise) {
+ if (attributes.id) {
+ tagName.appendChild(this.el("span.cm-attribute", "#" + attributes.id));
+ }
+ if (attributes.class) {
+ tagName.appendChild(this.el("span.cm-attribute", "." + attributes.class.split(/\s+/g).join(".")));
+ }
+ } else {
+ for (let name of Object.keys(attributes)) {
+ let attr = this._renderAttributeNode(" " + name, attributes[name]);
+ this.element.appendChild(attr);
+ }
+ }
+
+ this._text(">");
+
+ // Register this widget in the owner message so that it gets destroyed when
+ // the message is destroyed.
+ this.message.widgets.add(this);
+
+ this.linkToInspector().then(null, e => console.error(e));
+ },
+
+ /**
+ * If the DOMNode being rendered can be highlit in the page, this function
+ * will attach mouseover/out event listeners to do so, and the inspector icon
+ * to open the node in the inspector.
+ * @return a promise that resolves when the node has been linked to the
+ * inspector, or rejects if it wasn't (either if no toolbox could be found to
+ * access the inspector, or if the node isn't present in the inspector, i.e.
+ * if the node is in a DocumentFragment or not part of the tree, or not of
+ * type nodeConstants.ELEMENT_NODE).
+ */
+ linkToInspector: Task.async(function* ()
+ {
+ if (this._linkedToInspector) {
+ return;
+ }
+
+ // Checking the node type
+ if (this.objectActor.preview.nodeType !== nodeConstants.ELEMENT_NODE) {
+ throw new Error("The object cannot be linked to the inspector as it " +
+ "isn't an element node");
+ }
+
+ // Checking the presence of a toolbox
+ let target = this.message.output.toolboxTarget;
+ this.toolbox = gDevTools.getToolbox(target);
+ if (!this.toolbox) {
+ // In cases like the browser console, there is no toolbox.
+ return;
+ }
+
+ // Checking that the inspector supports the node
+ yield this.toolbox.initInspector();
+ this._nodeFront = yield this.toolbox.walker.getNodeActorFromObjectActor(this.objectActor.actor);
+ if (!this._nodeFront) {
+ throw new Error("The object cannot be linked to the inspector, the " +
+ "corresponding nodeFront could not be found");
+ }
+
+ // At this stage, the message may have been cleared already
+ if (!this.document) {
+ throw new Error("The object cannot be linked to the inspector, the " +
+ "message was got cleared away");
+ }
+
+ // Check it again as this method is async!
+ if (this._linkedToInspector) {
+ return;
+ }
+ this._linkedToInspector = true;
+
+ this.highlightDomNode = this.highlightDomNode.bind(this);
+ this.element.addEventListener("mouseover", this.highlightDomNode, false);
+ this.unhighlightDomNode = this.unhighlightDomNode.bind(this);
+ this.element.addEventListener("mouseout", this.unhighlightDomNode, false);
+
+ this._openInspectorNode = this._anchor("", {
+ className: "open-inspector",
+ onClick: this.openNodeInInspector.bind(this)
+ });
+ this._openInspectorNode.title = l10n.getStr("openNodeInInspector");
+ }),
+
+ /**
+ * Highlight the DOMNode corresponding to the ObjectActor in the page.
+ * @return a promise that resolves when the node has been highlighted, or
+ * rejects if the node cannot be highlighted (detached from the DOM)
+ */
+ highlightDomNode: Task.async(function* ()
+ {
+ yield this.linkToInspector();
+ let isAttached = yield this.toolbox.walker.isInDOMTree(this._nodeFront);
+ if (isAttached) {
+ yield this.toolbox.highlighterUtils.highlightNodeFront(this._nodeFront);
+ } else {
+ throw null;
+ }
+ }),
+
+ /**
+ * Unhighlight a previously highlit node
+ * @see highlightDomNode
+ * @return a promise that resolves when the highlighter has been hidden
+ */
+ unhighlightDomNode: function ()
+ {
+ return this.linkToInspector().then(() => {
+ return this.toolbox.highlighterUtils.unhighlight();
+ }).then(null, e => console.error(e));
+ },
+
+ /**
+ * Open the DOMNode corresponding to the ObjectActor in the inspector panel
+ * @return a promise that resolves when the inspector has been switched to
+ * and the node has been selected, or rejects if the node cannot be selected
+ * (detached from the DOM). Note that in any case, the inspector panel will
+ * be switched to.
+ */
+ openNodeInInspector: Task.async(function* ()
+ {
+ yield this.linkToInspector();
+ yield this.toolbox.selectTool("inspector");
+
+ let isAttached = yield this.toolbox.walker.isInDOMTree(this._nodeFront);
+ if (isAttached) {
+ let onReady = promise.defer();
+ this.toolbox.inspector.once("inspector-updated", onReady.resolve);
+ yield this.toolbox.selection.setNodeFront(this._nodeFront, "console");
+ yield onReady.promise;
+ } else {
+ throw null;
+ }
+ }),
+
+ destroy: function ()
+ {
+ if (this.toolbox && this._nodeFront) {
+ this.element.removeEventListener("mouseover", this.highlightDomNode, false);
+ this.element.removeEventListener("mouseout", this.unhighlightDomNode, false);
+ this._openInspectorNode.removeEventListener("mousedown", this.openNodeInInspector, true);
+
+ if (this._linkedToInspector) {
+ this.unhighlightDomNode().then(() => {
+ this.toolbox = null;
+ this._nodeFront = null;
+ });
+ } else {
+ this.toolbox = null;
+ this._nodeFront = null;
+ }
+ }
+ },
+}); // Widgets.ObjectRenderers.byKind.DOMNode
+
+/**
+ * The widget user for displaying Promise objects.
+ */
+Widgets.ObjectRenderers.add({
+ byClass: "Promise",
+
+ render: function ()
+ {
+ let { ownProperties, safeGetterValues } = this.objectActor.preview || {};
+ if ((!ownProperties && !safeGetterValues) || this.options.concise) {
+ this._renderConciseObject();
+ return;
+ }
+
+ this._renderObjectPrefix();
+ let container = this.element;
+ let addedPromiseInternalProps = false;
+
+ if (this.objectActor.promiseState) {
+ const { state, value, reason } = this.objectActor.promiseState;
+
+ this._renderObjectProperty("<state>", state, container, false);
+ addedPromiseInternalProps = true;
+
+ if (state == "fulfilled") {
+ this._renderObjectProperty("<value>", value, container, true);
+ } else if (state == "rejected") {
+ this._renderObjectProperty("<reason>", reason, container, true);
+ }
+ }
+
+ this._renderObjectProperties(container, addedPromiseInternalProps);
+ this._renderObjectSuffix();
+ }
+}); // Widgets.ObjectRenderers.byClass.Promise
+
+/*
+ * A renderer used for wrapped primitive objects.
+ */
+
+function WrappedPrimitiveRenderer() {
+ let { ownProperties, safeGetterValues } = this.objectActor.preview || {};
+ if ((!ownProperties && !safeGetterValues) || this.options.concise) {
+ this._renderConciseObject();
+ return;
+ }
+
+ this._renderObjectPrefix();
+
+ let elem =
+ this.message._renderValueGrip(this.objectActor.preview.wrappedValue);
+ this.element.appendChild(elem);
+
+ this._renderObjectProperties(this.element, true);
+ this._renderObjectSuffix();
+}
+
+/**
+ * The widget used for displaying Boolean previews.
+ */
+Widgets.ObjectRenderers.add({
+ byClass: "Boolean",
+
+ render: WrappedPrimitiveRenderer,
+});
+
+/**
+ * The widget used for displaying Number previews.
+ */
+Widgets.ObjectRenderers.add({
+ byClass: "Number",
+
+ render: WrappedPrimitiveRenderer,
+});
+
+/**
+ * The widget used for displaying String previews.
+ */
+Widgets.ObjectRenderers.add({
+ byClass: "String",
+
+ render: WrappedPrimitiveRenderer,
+});
+
+/**
+ * The widget used for displaying generic JS object previews.
+ */
+Widgets.ObjectRenderers.add({
+ byKind: "Object",
+
+ render: function ()
+ {
+ let { ownProperties, safeGetterValues } = this.objectActor.preview || {};
+ if ((!ownProperties && !safeGetterValues) || this.options.concise) {
+ this._renderConciseObject();
+ return;
+ }
+
+ this._renderObjectPrefix();
+ this._renderObjectProperties(this.element, false);
+ this._renderObjectSuffix();
+ },
+}); // Widgets.ObjectRenderers.byKind.Object
+
+/**
+ * The long string widget.
+ *
+ * @constructor
+ * @param object message
+ * The owning message.
+ * @param object longStringActor
+ * The LongStringActor to display.
+ * @param object options
+ * Options, such as noStringQuotes
+ */
+Widgets.LongString = function (message, longStringActor, options)
+{
+ Widgets.BaseWidget.call(this, message);
+ this.longStringActor = longStringActor;
+ this.noStringQuotes = (options && "noStringQuotes" in options) ?
+ options.noStringQuotes : !this.message._quoteStrings;
+
+ this._onClick = this._onClick.bind(this);
+ this._onSubstring = this._onSubstring.bind(this);
+};
+
+Widgets.LongString.prototype = extend(Widgets.BaseWidget.prototype, {
+ /**
+ * The LongStringActor displayed by the widget.
+ * @type object
+ */
+ longStringActor: null,
+
+ render: function ()
+ {
+ if (this.element) {
+ return this;
+ }
+
+ let result = this.element = this.document.createElementNS(XHTML_NS, "span");
+ result.className = "longString console-string";
+ this._renderString(this.longStringActor.initial);
+ result.appendChild(this._renderEllipsis());
+
+ return this;
+ },
+
+ /**
+ * Render the long string in the widget element.
+ * @private
+ * @param string str
+ * The string to display.
+ */
+ _renderString: function (str)
+ {
+ this.element.textContent = VariablesView.getString(str, {
+ noStringQuotes: this.noStringQuotes,
+ noEllipsis: true,
+ });
+ },
+
+ /**
+ * Render the anchor ellipsis that allows the user to expand the long string.
+ *
+ * @private
+ * @return Element
+ */
+ _renderEllipsis: function ()
+ {
+ let ellipsis = this.document.createElementNS(XHTML_NS, "a");
+ ellipsis.className = "longStringEllipsis";
+ ellipsis.textContent = l10n.getStr("longStringEllipsis");
+ ellipsis.href = "#";
+ ellipsis.draggable = false;
+ this.message._addLinkCallback(ellipsis, this._onClick);
+
+ return ellipsis;
+ },
+
+ /**
+ * The click event handler for the ellipsis shown after the short string. This
+ * function expands the element to show the full string.
+ * @private
+ */
+ _onClick: function ()
+ {
+ let longString = this.output.webConsoleClient.longString(this.longStringActor);
+ let toIndex = Math.min(longString.length, MAX_LONG_STRING_LENGTH);
+
+ longString.substring(longString.initial.length, toIndex, this._onSubstring);
+ },
+
+ /**
+ * The longString substring response callback.
+ *
+ * @private
+ * @param object response
+ * Response packet.
+ */
+ _onSubstring: function (response)
+ {
+ if (response.error) {
+ console.error("LongString substring failure: " + response.error);
+ return;
+ }
+
+ this.element.lastChild.remove();
+ this.element.classList.remove("longString");
+
+ this._renderString(this.longStringActor.initial + response.substring);
+
+ this.output.owner.emit("new-messages", new Set([{
+ update: true,
+ node: this.message.element,
+ response: response,
+ }]));
+
+ let toIndex = Math.min(this.longStringActor.length, MAX_LONG_STRING_LENGTH);
+ if (toIndex != this.longStringActor.length) {
+ this._logWarningAboutStringTooLong();
+ }
+ },
+
+ /**
+ * Inform user that the string he tries to view is too long.
+ * @private
+ */
+ _logWarningAboutStringTooLong: function ()
+ {
+ let msg = new Messages.Simple(l10n.getStr("longStringTooLong"), {
+ category: "output",
+ severity: "warning",
+ });
+ this.output.addMessage(msg);
+ },
+}); // Widgets.LongString.prototype
+
+
+/**
+ * The stacktrace widget.
+ *
+ * @constructor
+ * @extends Widgets.BaseWidget
+ * @param object message
+ * The owning message.
+ * @param array stacktrace
+ * The stacktrace to display, array of frames as supplied by the server,
+ * over the remote protocol.
+ */
+Widgets.Stacktrace = function (message, stacktrace) {
+ Widgets.BaseWidget.call(this, message);
+ this.stacktrace = stacktrace;
+};
+
+Widgets.Stacktrace.prototype = extend(Widgets.BaseWidget.prototype, {
+ /**
+ * The stackframes received from the server.
+ * @type array
+ */
+ stacktrace: null,
+
+ render() {
+ if (this.element) {
+ return this;
+ }
+
+ let result = this.element = this.document.createElementNS(XHTML_NS, "div");
+ result.className = "stacktrace devtools-monospace";
+
+ if (this.stacktrace) {
+ this.output.owner.ReactDOM.render(this.output.owner.StackTraceView({
+ stacktrace: this.stacktrace,
+ onViewSourceInDebugger: frame => this.output.openLocationInDebugger(frame)
+ }), result);
+ }
+
+ return this;
+ }
+});
+
+/**
+ * The table widget.
+ *
+ * @constructor
+ * @extends Widgets.BaseWidget
+ * @param object message
+ * The owning message.
+ * @param array data
+ * Array of objects that holds the data to log in the table.
+ * @param object columns
+ * Object containing the key value pair of the id and display name for
+ * the columns in the table.
+ */
+Widgets.Table = function (message, data, columns)
+{
+ Widgets.BaseWidget.call(this, message);
+ this.data = data;
+ this.columns = columns;
+};
+
+Widgets.Table.prototype = extend(Widgets.BaseWidget.prototype, {
+ /**
+ * Array of objects that holds the data to output in the table.
+ * @type array
+ */
+ data: null,
+
+ /**
+ * Object containing the key value pair of the id and display name for
+ * the columns in the table.
+ * @type object
+ */
+ columns: null,
+
+ render: function () {
+ if (this.element) {
+ return this;
+ }
+
+ let result = this.element = this.document.createElementNS(XHTML_NS, "div");
+ result.className = "consoletable devtools-monospace";
+
+ this.table = new TableWidget(result, {
+ wrapTextInElements: true,
+ initialColumns: this.columns,
+ uniqueId: "_index",
+ firstColumn: "_index"
+ });
+
+ for (let row of this.data) {
+ this.table.push(row);
+ }
+
+ return this;
+ }
+}); // Widgets.Table.prototype
+
+function gSequenceId()
+{
+ return gSequenceId.n++;
+}
+gSequenceId.n = 0;
+
+exports.ConsoleOutput = ConsoleOutput;
+exports.Messages = Messages;
+exports.Widgets = Widgets;
diff --git a/devtools/client/webconsole/hudservice.js b/devtools/client/webconsole/hudservice.js
new file mode 100644
index 000000000..46b4f2a13
--- /dev/null
+++ b/devtools/client/webconsole/hudservice.js
@@ -0,0 +1,718 @@
+/* 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");
+
+var WebConsoleUtils = require("devtools/client/webconsole/utils").Utils;
+var { extend } = require("sdk/core/heritage");
+var {TargetFactory} = require("devtools/client/framework/target");
+var {Tools} = require("devtools/client/definitions");
+const { Task } = require("devtools/shared/task");
+var promise = require("promise");
+var Services = require("Services");
+
+loader.lazyRequireGetter(this, "Telemetry", "devtools/client/shared/telemetry");
+loader.lazyRequireGetter(this, "WebConsoleFrame", "devtools/client/webconsole/webconsole", true);
+loader.lazyRequireGetter(this, "gDevTools", "devtools/client/framework/devtools", true);
+loader.lazyRequireGetter(this, "DebuggerServer", "devtools/server/main", true);
+loader.lazyRequireGetter(this, "DebuggerClient", "devtools/shared/client/main", true);
+loader.lazyRequireGetter(this, "showDoorhanger", "devtools/client/shared/doorhanger", true);
+loader.lazyRequireGetter(this, "viewSource", "devtools/client/shared/view-source");
+
+const STRINGS_URI = "devtools/client/locales/webconsole.properties";
+var l10n = new WebConsoleUtils.L10n(STRINGS_URI);
+
+const BROWSER_CONSOLE_WINDOW_FEATURES = "chrome,titlebar,toolbar,centerscreen,resizable,dialog=no";
+
+// The preference prefix for all of the Browser Console filters.
+const BROWSER_CONSOLE_FILTER_PREFS_PREFIX = "devtools.browserconsole.filter.";
+
+var gHudId = 0;
+
+// The HUD service
+
+function HUD_SERVICE()
+{
+ this.consoles = new Map();
+ this.lastFinishedRequest = { callback: null };
+}
+
+HUD_SERVICE.prototype =
+{
+ _browserConsoleID: null,
+ _browserConsoleDefer: null,
+
+ /**
+ * Keeps a reference for each Web Console / Browser Console that is created.
+ * @type Map
+ */
+ consoles: null,
+
+ /**
+ * Assign a function to this property to listen for every request that
+ * completes. Used by unit tests. The callback takes one argument: the HTTP
+ * activity object as received from the remote Web Console.
+ *
+ * @type object
+ * Includes a property named |callback|. Assign the function to the
+ * |callback| property of this object.
+ */
+ lastFinishedRequest: null,
+
+ /**
+ * Get the current context, which is the main application window.
+ *
+ * @returns nsIDOMWindow
+ */
+ currentContext: function HS_currentContext() {
+ return Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
+ },
+
+ /**
+ * Open a Web Console for the given target.
+ *
+ * @see devtools/framework/target.js for details about targets.
+ *
+ * @param object aTarget
+ * The target that the web console will connect to.
+ * @param nsIDOMWindow aIframeWindow
+ * The window where the web console UI is already loaded.
+ * @param nsIDOMWindow aChromeWindow
+ * The window of the web console owner.
+ * @return object
+ * A promise object for the opening of the new WebConsole instance.
+ */
+ openWebConsole:
+ function HS_openWebConsole(aTarget, aIframeWindow, aChromeWindow)
+ {
+ let hud = new WebConsole(aTarget, aIframeWindow, aChromeWindow);
+ this.consoles.set(hud.hudId, hud);
+ return hud.init();
+ },
+
+ /**
+ * Open a Browser Console for the given target.
+ *
+ * @see devtools/framework/target.js for details about targets.
+ *
+ * @param object aTarget
+ * The target that the browser console will connect to.
+ * @param nsIDOMWindow aIframeWindow
+ * The window where the browser console UI is already loaded.
+ * @param nsIDOMWindow aChromeWindow
+ * The window of the browser console owner.
+ * @return object
+ * A promise object for the opening of the new BrowserConsole instance.
+ */
+ openBrowserConsole:
+ function HS_openBrowserConsole(aTarget, aIframeWindow, aChromeWindow)
+ {
+ let hud = new BrowserConsole(aTarget, aIframeWindow, aChromeWindow);
+ this._browserConsoleID = hud.hudId;
+ this.consoles.set(hud.hudId, hud);
+ return hud.init();
+ },
+
+ /**
+ * Returns the Web Console object associated to a content window.
+ *
+ * @param nsIDOMWindow aContentWindow
+ * @returns object
+ */
+ getHudByWindow: function HS_getHudByWindow(aContentWindow)
+ {
+ for (let [hudId, hud] of this.consoles) {
+ let target = hud.target;
+ if (target && target.tab && target.window === aContentWindow) {
+ return hud;
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Returns the console instance for a given id.
+ *
+ * @param string aId
+ * @returns Object
+ */
+ getHudReferenceById: function HS_getHudReferenceById(aId)
+ {
+ return this.consoles.get(aId);
+ },
+
+ /**
+ * Find if there is a Web Console open for the current tab and return the
+ * instance.
+ * @return object|null
+ * The WebConsole object or null if the active tab has no open Web
+ * Console.
+ */
+ getOpenWebConsole: function HS_getOpenWebConsole()
+ {
+ let tab = this.currentContext().gBrowser.selectedTab;
+ if (!tab || !TargetFactory.isKnownTab(tab)) {
+ return null;
+ }
+ let target = TargetFactory.forTab(tab);
+ let toolbox = gDevTools.getToolbox(target);
+ let panel = toolbox ? toolbox.getPanel("webconsole") : null;
+ return panel ? panel.hud : null;
+ },
+
+ /**
+ * Toggle the Browser Console.
+ */
+ toggleBrowserConsole: function HS_toggleBrowserConsole()
+ {
+ if (this._browserConsoleID) {
+ let hud = this.getHudReferenceById(this._browserConsoleID);
+ return hud.destroy();
+ }
+
+ if (this._browserConsoleDefer) {
+ return this._browserConsoleDefer.promise;
+ }
+
+ this._browserConsoleDefer = promise.defer();
+
+ function connect()
+ {
+ let deferred = promise.defer();
+
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+ DebuggerServer.allowChromeProcess = true;
+
+ let client = new DebuggerClient(DebuggerServer.connectPipe());
+ return client.connect()
+ .then(() => client.getProcess())
+ .then(aResponse => {
+ // Set chrome:false in order to attach to the target
+ // (i.e. send an `attach` request to the chrome actor)
+ return { form: aResponse.form, client: client, chrome: false };
+ });
+ }
+
+ let target;
+ function getTarget(aConnection)
+ {
+ return TargetFactory.forRemoteTab(aConnection);
+ }
+
+ function openWindow(aTarget)
+ {
+ target = aTarget;
+
+ let deferred = promise.defer();
+
+ let win = Services.ww.openWindow(null, Tools.webConsole.url, "_blank",
+ BROWSER_CONSOLE_WINDOW_FEATURES, null);
+ win.addEventListener("DOMContentLoaded", function onLoad() {
+ win.removeEventListener("DOMContentLoaded", onLoad);
+
+ // Set the correct Browser Console title.
+ let root = win.document.documentElement;
+ root.setAttribute("title", root.getAttribute("browserConsoleTitle"));
+
+ deferred.resolve(win);
+ });
+
+ return deferred.promise;
+ }
+
+ connect().then(getTarget).then(openWindow).then((aWindow) => {
+ return this.openBrowserConsole(target, aWindow, aWindow)
+ .then((aBrowserConsole) => {
+ this._browserConsoleDefer.resolve(aBrowserConsole);
+ this._browserConsoleDefer = null;
+ });
+ }, console.error.bind(console));
+
+ return this._browserConsoleDefer.promise;
+ },
+
+ /**
+ * Opens or focuses the Browser Console.
+ */
+ openBrowserConsoleOrFocus: function HS_openBrowserConsoleOrFocus()
+ {
+ let hud = this.getBrowserConsole();
+ if (hud) {
+ hud.iframeWindow.focus();
+ return promise.resolve(hud);
+ }
+ else {
+ return this.toggleBrowserConsole();
+ }
+ },
+
+ /**
+ * Get the Browser Console instance, if open.
+ *
+ * @return object|null
+ * A BrowserConsole instance or null if the Browser Console is not
+ * open.
+ */
+ getBrowserConsole: function HS_getBrowserConsole()
+ {
+ return this.getHudReferenceById(this._browserConsoleID);
+ },
+};
+
+
+/**
+ * A WebConsole instance is an interactive console initialized *per target*
+ * that displays console log data as well as provides an interactive terminal to
+ * manipulate the target's document content.
+ *
+ * This object only wraps the iframe that holds the Web Console UI. This is
+ * meant to be an integration point between the Firefox UI and the Web Console
+ * UI and features.
+ *
+ * @constructor
+ * @param object aTarget
+ * The target that the web console will connect to.
+ * @param nsIDOMWindow aIframeWindow
+ * The window where the web console UI is already loaded.
+ * @param nsIDOMWindow aChromeWindow
+ * The window of the web console owner.
+ */
+function WebConsole(aTarget, aIframeWindow, aChromeWindow)
+{
+ this.iframeWindow = aIframeWindow;
+ this.chromeWindow = aChromeWindow;
+ this.hudId = "hud_" + ++gHudId;
+ this.target = aTarget;
+
+ this.browserWindow = this.chromeWindow.top;
+
+ let element = this.browserWindow.document.documentElement;
+ if (element.getAttribute("windowtype") != gDevTools.chromeWindowType) {
+ this.browserWindow = HUDService.currentContext();
+ }
+
+ this.ui = new WebConsoleFrame(this);
+}
+
+WebConsole.prototype = {
+ iframeWindow: null,
+ chromeWindow: null,
+ browserWindow: null,
+ hudId: null,
+ target: null,
+ ui: null,
+ _browserConsole: false,
+ _destroyer: null,
+
+ /**
+ * Getter for a function to to listen for every request that completes. Used
+ * by unit tests. The callback takes one argument: the HTTP activity object as
+ * received from the remote Web Console.
+ *
+ * @type function
+ */
+ get lastFinishedRequestCallback()
+ {
+ return HUDService.lastFinishedRequest.callback;
+ },
+
+ /**
+ * Getter for the window that can provide various utilities that the web
+ * console makes use of, like opening links, managing popups, etc. In
+ * most cases, this will be |this.browserWindow|, but in some uses (such as
+ * the Browser Toolbox), there is no browser window, so an alternative window
+ * hosts the utilities there.
+ * @type nsIDOMWindow
+ */
+ get chromeUtilsWindow()
+ {
+ if (this.browserWindow) {
+ return this.browserWindow;
+ }
+ return this.chromeWindow.top;
+ },
+
+ /**
+ * Getter for the xul:popupset that holds any popups we open.
+ * @type nsIDOMElement
+ */
+ get mainPopupSet()
+ {
+ return this.chromeUtilsWindow.document.getElementById("mainPopupSet");
+ },
+
+ /**
+ * Getter for the output element that holds messages we display.
+ * @type nsIDOMElement
+ */
+ get outputNode()
+ {
+ return this.ui ? this.ui.outputNode : null;
+ },
+
+ get gViewSourceUtils()
+ {
+ return this.chromeUtilsWindow.gViewSourceUtils;
+ },
+
+ /**
+ * Initialize the Web Console instance.
+ *
+ * @return object
+ * A promise for the initialization.
+ */
+ init: function WC_init()
+ {
+ return this.ui.init().then(() => this);
+ },
+
+ /**
+ * Retrieve the Web Console panel title.
+ *
+ * @return string
+ * The Web Console panel title.
+ */
+ getPanelTitle: function WC_getPanelTitle()
+ {
+ let url = this.ui ? this.ui.contentLocation : "";
+ return l10n.getFormatStr("webConsoleWindowTitleAndURL", [url]);
+ },
+
+ /**
+ * The JSTerm object that manages the console's input.
+ * @see webconsole.js::JSTerm
+ * @type object
+ */
+ get jsterm()
+ {
+ return this.ui ? this.ui.jsterm : null;
+ },
+
+ /**
+ * The clear output button handler.
+ * @private
+ */
+ _onClearButton: function WC__onClearButton()
+ {
+ if (this.target.isLocalTab) {
+ this.browserWindow.DeveloperToolbar.resetErrorsCount(this.target.tab);
+ }
+ },
+
+ /**
+ * Alias for the WebConsoleFrame.setFilterState() method.
+ * @see webconsole.js::WebConsoleFrame.setFilterState()
+ */
+ setFilterState: function WC_setFilterState()
+ {
+ this.ui && this.ui.setFilterState.apply(this.ui, arguments);
+ },
+
+ /**
+ * Open a link in a new tab.
+ *
+ * @param string aLink
+ * The URL you want to open in a new tab.
+ */
+ openLink: function WC_openLink(aLink)
+ {
+ this.chromeUtilsWindow.openUILinkIn(aLink, "tab");
+ },
+
+ /**
+ * Open a link in Firefox's view source.
+ *
+ * @param string aSourceURL
+ * The URL of the file.
+ * @param integer aSourceLine
+ * The line number which should be highlighted.
+ */
+ viewSource: function WC_viewSource(aSourceURL, aSourceLine) {
+ // Attempt to access view source via a browser first, which may display it in
+ // a tab, if enabled.
+ let browserWin = Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
+ if (browserWin && browserWin.BrowserViewSourceOfDocument) {
+ return browserWin.BrowserViewSourceOfDocument({
+ URL: aSourceURL,
+ lineNumber: aSourceLine
+ });
+ }
+ this.gViewSourceUtils.viewSource(aSourceURL, null, this.iframeWindow.document, aSourceLine || 0);
+ },
+
+ /**
+ * Tries to open a Stylesheet file related to the web page for the web console
+ * instance in the Style Editor. If the file is not found, it is opened in
+ * source view instead.
+ *
+ * Manually handle the case where toolbox does not exist (Browser Console).
+ *
+ * @param string aSourceURL
+ * The URL of the file.
+ * @param integer aSourceLine
+ * The line number which you want to place the caret.
+ */
+ viewSourceInStyleEditor: function WC_viewSourceInStyleEditor(aSourceURL, aSourceLine) {
+ let toolbox = gDevTools.getToolbox(this.target);
+ if (!toolbox) {
+ this.viewSource(aSourceURL, aSourceLine);
+ return;
+ }
+ toolbox.viewSourceInStyleEditor(aSourceURL, aSourceLine);
+ },
+
+ /**
+ * Tries to open a JavaScript file related to the web page for the web console
+ * instance in the Script Debugger. If the file is not found, it is opened in
+ * source view instead.
+ *
+ * Manually handle the case where toolbox does not exist (Browser Console).
+ *
+ * @param string aSourceURL
+ * The URL of the file.
+ * @param integer aSourceLine
+ * The line number which you want to place the caret.
+ */
+ viewSourceInDebugger: function WC_viewSourceInDebugger(aSourceURL, aSourceLine) {
+ let toolbox = gDevTools.getToolbox(this.target);
+ if (!toolbox) {
+ this.viewSource(aSourceURL, aSourceLine);
+ return;
+ }
+ toolbox.viewSourceInDebugger(aSourceURL, aSourceLine).then(() => {
+ this.ui.emit("source-in-debugger-opened");
+ });
+ },
+
+ /**
+ * Tries to open a JavaScript file related to the web page for the web console
+ * instance in the corresponding Scratchpad.
+ *
+ * @param string aSourceURL
+ * The URL of the file which corresponds to a Scratchpad id.
+ */
+ viewSourceInScratchpad: function WC_viewSourceInScratchpad(aSourceURL, aSourceLine) {
+ viewSource.viewSourceInScratchpad(aSourceURL, aSourceLine);
+ },
+
+ /**
+ * Retrieve information about the JavaScript debugger's stackframes list. This
+ * is used to allow the Web Console to evaluate code in the selected
+ * stackframe.
+ *
+ * @return object|null
+ * An object which holds:
+ * - frames: the active ThreadClient.cachedFrames array.
+ * - selected: depth/index of the selected stackframe in the debugger
+ * UI.
+ * If the debugger is not open or if it's not paused, then |null| is
+ * returned.
+ */
+ getDebuggerFrames: function WC_getDebuggerFrames()
+ {
+ let toolbox = gDevTools.getToolbox(this.target);
+ if (!toolbox) {
+ return null;
+ }
+ let panel = toolbox.getPanel("jsdebugger");
+
+ if (!panel) {
+ return null;
+ }
+
+ return panel.getFrames();
+ },
+
+ /**
+ * Retrieves the current selection from the Inspector, if such a selection
+ * exists. This is used to pass the ID of the selected actor to the Web
+ * Console server for the $0 helper.
+ *
+ * @return object|null
+ * A Selection referring to the currently selected node in the
+ * Inspector.
+ * If the inspector was never opened, or no node was ever selected,
+ * then |null| is returned.
+ */
+ getInspectorSelection: function WC_getInspectorSelection()
+ {
+ let toolbox = gDevTools.getToolbox(this.target);
+ if (!toolbox) {
+ return null;
+ }
+ let panel = toolbox.getPanel("inspector");
+ if (!panel || !panel.selection) {
+ return null;
+ }
+ return panel.selection;
+ },
+
+ /**
+ * Destroy the object. Call this method to avoid memory leaks when the Web
+ * Console is closed.
+ *
+ * @return object
+ * A promise object that is resolved once the Web Console is closed.
+ */
+ destroy: function WC_destroy()
+ {
+ if (this._destroyer) {
+ return this._destroyer.promise;
+ }
+
+ HUDService.consoles.delete(this.hudId);
+
+ this._destroyer = promise.defer();
+
+ // The document may already be removed
+ if (this.chromeUtilsWindow && this.mainPopupSet) {
+ let popupset = this.mainPopupSet;
+ let panels = popupset.querySelectorAll("panel[hudId=" + this.hudId + "]");
+ for (let panel of panels) {
+ panel.hidePopup();
+ }
+ }
+
+ let onDestroy = Task.async(function* () {
+ if (!this._browserConsole) {
+ try {
+ yield this.target.activeTab.focus();
+ }
+ catch (ex) {
+ // Tab focus can fail if the tab or target is closed.
+ }
+ }
+
+ let id = WebConsoleUtils.supportsString(this.hudId);
+ Services.obs.notifyObservers(id, "web-console-destroyed", null);
+ this._destroyer.resolve(null);
+ }.bind(this));
+
+ if (this.ui) {
+ this.ui.destroy().then(onDestroy);
+ }
+ else {
+ onDestroy();
+ }
+
+ return this._destroyer.promise;
+ },
+};
+
+/**
+ * A BrowserConsole instance is an interactive console initialized *per target*
+ * that displays console log data as well as provides an interactive terminal to
+ * manipulate the target's document content.
+ *
+ * This object only wraps the iframe that holds the Browser Console UI. This is
+ * meant to be an integration point between the Firefox UI and the Browser Console
+ * UI and features.
+ *
+ * @constructor
+ * @param object aTarget
+ * The target that the browser console will connect to.
+ * @param nsIDOMWindow aIframeWindow
+ * The window where the browser console UI is already loaded.
+ * @param nsIDOMWindow aChromeWindow
+ * The window of the browser console owner.
+ */
+function BrowserConsole()
+{
+ WebConsole.apply(this, arguments);
+ this._telemetry = new Telemetry();
+}
+
+BrowserConsole.prototype = extend(WebConsole.prototype, {
+ _browserConsole: true,
+ _bc_init: null,
+ _bc_destroyer: null,
+
+ $init: WebConsole.prototype.init,
+
+ /**
+ * Initialize the Browser Console instance.
+ *
+ * @return object
+ * A promise for the initialization.
+ */
+ init: function BC_init()
+ {
+ if (this._bc_init) {
+ return this._bc_init;
+ }
+
+ this.ui._filterPrefsPrefix = BROWSER_CONSOLE_FILTER_PREFS_PREFIX;
+
+ let window = this.iframeWindow;
+
+ // Make sure that the closing of the Browser Console window destroys this
+ // instance.
+ let onClose = () => {
+ window.removeEventListener("unload", onClose);
+ window.removeEventListener("focus", onFocus);
+ this.destroy();
+ };
+ window.addEventListener("unload", onClose);
+
+ this._telemetry.toolOpened("browserconsole");
+
+ // Create an onFocus handler just to display the dev edition promo.
+ // This is to prevent race conditions in some environments.
+ // Hook to display promotional Developer Edition doorhanger. Only displayed once.
+ let onFocus = () => showDoorhanger({ window, type: "deveditionpromo" });
+ window.addEventListener("focus", onFocus);
+
+ this._bc_init = this.$init();
+ return this._bc_init;
+ },
+
+ $destroy: WebConsole.prototype.destroy,
+
+ /**
+ * Destroy the object.
+ *
+ * @return object
+ * A promise object that is resolved once the Browser Console is closed.
+ */
+ destroy: function BC_destroy()
+ {
+ if (this._bc_destroyer) {
+ return this._bc_destroyer.promise;
+ }
+
+ this._telemetry.toolClosed("browserconsole");
+
+ this._bc_destroyer = promise.defer();
+
+ let chromeWindow = this.chromeWindow;
+ this.$destroy().then(() =>
+ this.target.client.close().then(() => {
+ HUDService._browserConsoleID = null;
+ chromeWindow.close();
+ this._bc_destroyer.resolve(null);
+ }));
+
+ return this._bc_destroyer.promise;
+ },
+});
+
+const HUDService = new HUD_SERVICE();
+
+(() => {
+ let methods = ["openWebConsole", "openBrowserConsole",
+ "toggleBrowserConsole", "getOpenWebConsole",
+ "getBrowserConsole", "getHudByWindow",
+ "openBrowserConsoleOrFocus", "getHudReferenceById"];
+ for (let method of methods) {
+ exports[method] = HUDService[method].bind(HUDService);
+ }
+
+ exports.consoles = HUDService.consoles;
+ exports.lastFinishedRequest = HUDService.lastFinishedRequest;
+})();
diff --git a/devtools/client/webconsole/jsterm.js b/devtools/client/webconsole/jsterm.js
new file mode 100644
index 000000000..8e3259afa
--- /dev/null
+++ b/devtools/client/webconsole/jsterm.js
@@ -0,0 +1,1766 @@
+/* -*- 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 {Utils: WebConsoleUtils} =
+ require("devtools/client/webconsole/utils");
+const promise = require("promise");
+const Debugger = require("Debugger");
+const Services = require("Services");
+const {KeyCodes} = require("devtools/client/shared/keycodes");
+
+loader.lazyServiceGetter(this, "clipboardHelper",
+ "@mozilla.org/widget/clipboardhelper;1",
+ "nsIClipboardHelper");
+loader.lazyRequireGetter(this, "EventEmitter", "devtools/shared/event-emitter");
+loader.lazyRequireGetter(this, "AutocompletePopup", "devtools/client/shared/autocomplete-popup", true);
+loader.lazyRequireGetter(this, "ToolSidebar", "devtools/client/framework/sidebar", true);
+loader.lazyRequireGetter(this, "Messages", "devtools/client/webconsole/console-output", true);
+loader.lazyRequireGetter(this, "asyncStorage", "devtools/shared/async-storage");
+loader.lazyRequireGetter(this, "EnvironmentClient", "devtools/shared/client/main", true);
+loader.lazyRequireGetter(this, "ObjectClient", "devtools/shared/client/main", true);
+loader.lazyImporter(this, "VariablesView", "resource://devtools/client/shared/widgets/VariablesView.jsm");
+loader.lazyImporter(this, "VariablesViewController", "resource://devtools/client/shared/widgets/VariablesViewController.jsm");
+loader.lazyRequireGetter(this, "gDevTools", "devtools/client/framework/devtools", true);
+
+const STRINGS_URI = "devtools/client/locales/webconsole.properties";
+var l10n = new WebConsoleUtils.L10n(STRINGS_URI);
+
+// Constants used for defining the direction of JSTerm input history navigation.
+const HISTORY_BACK = -1;
+const HISTORY_FORWARD = 1;
+
+const XHTML_NS = "http://www.w3.org/1999/xhtml";
+
+const HELP_URL = "https://developer.mozilla.org/docs/Tools/Web_Console/Helpers";
+
+const VARIABLES_VIEW_URL = "chrome://devtools/content/shared/widgets/VariablesView.xul";
+
+const PREF_INPUT_HISTORY_COUNT = "devtools.webconsole.inputHistoryCount";
+const PREF_AUTO_MULTILINE = "devtools.webconsole.autoMultiline";
+
+/**
+ * Create a JSTerminal (a JavaScript command line). This is attached to an
+ * existing HeadsUpDisplay (a Web Console instance). This code is responsible
+ * with handling command line input, code evaluation and result output.
+ *
+ * @constructor
+ * @param object webConsoleFrame
+ * The WebConsoleFrame object that owns this JSTerm instance.
+ */
+function JSTerm(webConsoleFrame) {
+ this.hud = webConsoleFrame;
+ this.hudId = this.hud.hudId;
+ this.inputHistoryCount = Services.prefs.getIntPref(PREF_INPUT_HISTORY_COUNT);
+
+ this.lastCompletion = { value: null };
+ this._loadHistory();
+
+ this._objectActorsInVariablesViews = new Map();
+
+ this._keyPress = this._keyPress.bind(this);
+ this._inputEventHandler = this._inputEventHandler.bind(this);
+ this._focusEventHandler = this._focusEventHandler.bind(this);
+ this._onKeypressInVariablesView = this._onKeypressInVariablesView.bind(this);
+ this._blurEventHandler = this._blurEventHandler.bind(this);
+
+ EventEmitter.decorate(this);
+}
+
+JSTerm.prototype = {
+ SELECTED_FRAME: -1,
+
+ /**
+ * Load the console history from previous sessions.
+ * @private
+ */
+ _loadHistory: function () {
+ this.history = [];
+ this.historyIndex = this.historyPlaceHolder = 0;
+
+ this.historyLoaded = asyncStorage.getItem("webConsoleHistory")
+ .then(value => {
+ if (Array.isArray(value)) {
+ // Since it was gotten asynchronously, there could be items already in
+ // the history. It's not likely but stick them onto the end anyway.
+ this.history = value.concat(this.history);
+
+ // Holds the number of entries in history. This value is incremented
+ // in this.execute().
+ this.historyIndex = this.history.length;
+
+ // Holds the index of the history entry that the user is currently
+ // viewing. This is reset to this.history.length when this.execute()
+ // is invoked.
+ this.historyPlaceHolder = this.history.length;
+ }
+ }, console.error);
+ },
+
+ /**
+ * Clear the console history altogether. Note that this will not affect
+ * other consoles that are already opened (since they have their own copy),
+ * but it will reset the array for all newly-opened consoles.
+ * @returns Promise
+ * Resolves once the changes have been persisted.
+ */
+ clearHistory: function () {
+ this.history = [];
+ this.historyIndex = this.historyPlaceHolder = 0;
+ return this.storeHistory();
+ },
+
+ /**
+ * Stores the console history for future console instances.
+ * @returns Promise
+ * Resolves once the changes have been persisted.
+ */
+ storeHistory: function () {
+ return asyncStorage.setItem("webConsoleHistory", this.history);
+ },
+
+ /**
+ * Stores the data for the last completion.
+ * @type object
+ */
+ lastCompletion: null,
+
+ /**
+ * Array that caches the user input suggestions received from the server.
+ * @private
+ * @type array
+ */
+ _autocompleteCache: null,
+
+ /**
+ * The input that caused the last request to the server, whose response is
+ * cached in the _autocompleteCache array.
+ * @private
+ * @type string
+ */
+ _autocompleteQuery: null,
+
+ /**
+ * The frameActorId used in the last autocomplete query. Whenever this changes
+ * the autocomplete cache must be invalidated.
+ * @private
+ * @type string
+ */
+ _lastFrameActorId: null,
+
+ /**
+ * The Web Console sidebar.
+ * @see this._createSidebar()
+ * @see Sidebar.jsm
+ */
+ sidebar: null,
+
+ /**
+ * The Variables View instance shown in the sidebar.
+ * @private
+ * @type object
+ */
+ _variablesView: null,
+
+ /**
+ * Tells if you want the variables view UI updates to be lazy or not. Tests
+ * disable lazy updates.
+ *
+ * @private
+ * @type boolean
+ */
+ _lazyVariablesView: true,
+
+ /**
+ * Holds a map between VariablesView instances and sets of ObjectActor IDs
+ * that have been retrieved from the server. This allows us to release the
+ * objects when needed.
+ *
+ * @private
+ * @type Map
+ */
+ _objectActorsInVariablesViews: null,
+
+ /**
+ * Last input value.
+ * @type string
+ */
+ lastInputValue: "",
+
+ /**
+ * Tells if the input node changed since the last focus.
+ *
+ * @private
+ * @type boolean
+ */
+ _inputChanged: false,
+
+ /**
+ * Tells if the autocomplete popup was navigated since the last open.
+ *
+ * @private
+ * @type boolean
+ */
+ _autocompletePopupNavigated: false,
+
+ /**
+ * History of code that was executed.
+ * @type array
+ */
+ history: null,
+ autocompletePopup: null,
+ inputNode: null,
+ completeNode: null,
+
+ /**
+ * Getter for the element that holds the messages we display.
+ * @type nsIDOMElement
+ */
+ get outputNode() {
+ return this.hud.outputNode;
+ },
+
+ /**
+ * Getter for the debugger WebConsoleClient.
+ * @type object
+ */
+ get webConsoleClient() {
+ return this.hud.webConsoleClient;
+ },
+
+ COMPLETE_FORWARD: 0,
+ COMPLETE_BACKWARD: 1,
+ COMPLETE_HINT_ONLY: 2,
+ COMPLETE_PAGEUP: 3,
+ COMPLETE_PAGEDOWN: 4,
+
+ /**
+ * Initialize the JSTerminal UI.
+ */
+ init: function () {
+ let autocompleteOptions = {
+ onSelect: this.onAutocompleteSelect.bind(this),
+ onClick: this.acceptProposedCompletion.bind(this),
+ listId: "webConsole_autocompletePopupListBox",
+ position: "top",
+ theme: "auto",
+ autoSelect: true
+ };
+
+ let doc = this.hud.document;
+ let toolbox = gDevTools.getToolbox(this.hud.owner.target);
+ let tooltipDoc = toolbox ? toolbox.doc : doc;
+ // The popup will be attached to the toolbox document or HUD document in the case
+ // such as the browser console which doesn't have a toolbox.
+ this.autocompletePopup = new AutocompletePopup(tooltipDoc, autocompleteOptions);
+
+ let inputContainer = doc.querySelector(".jsterm-input-container");
+ this.completeNode = doc.querySelector(".jsterm-complete-node");
+ this.inputNode = doc.querySelector(".jsterm-input-node");
+
+ if (this.hud.isBrowserConsole &&
+ !Services.prefs.getBoolPref("devtools.chrome.enabled")) {
+ inputContainer.style.display = "none";
+ } else {
+ let okstring = l10n.getStr("selfxss.okstring");
+ let msg = l10n.getFormatStr("selfxss.msg", [okstring]);
+ this._onPaste = WebConsoleUtils.pasteHandlerGen(
+ this.inputNode, doc.getElementById("webconsole-notificationbox"),
+ msg, okstring);
+ this.inputNode.addEventListener("keypress", this._keyPress, false);
+ this.inputNode.addEventListener("paste", this._onPaste);
+ this.inputNode.addEventListener("drop", this._onPaste);
+ this.inputNode.addEventListener("input", this._inputEventHandler, false);
+ this.inputNode.addEventListener("keyup", this._inputEventHandler, false);
+ this.inputNode.addEventListener("focus", this._focusEventHandler, false);
+ }
+
+ this.hud.window.addEventListener("blur", this._blurEventHandler, false);
+ this.lastInputValue && this.setInputValue(this.lastInputValue);
+ },
+
+ focus: function () {
+ if (!this.inputNode.getAttribute("focused")) {
+ this.inputNode.focus();
+ }
+ },
+
+ /**
+ * The JavaScript evaluation response handler.
+ *
+ * @private
+ * @param function [callback]
+ * Optional function to invoke when the evaluation result is added to
+ * the output.
+ * @param object response
+ * The message received from the server.
+ */
+ _executeResultCallback: function (callback, response) {
+ if (!this.hud) {
+ return;
+ }
+ if (response.error) {
+ console.error("Evaluation error " + response.error + ": " +
+ response.message);
+ return;
+ }
+ let errorMessage = response.exceptionMessage;
+ let errorDocURL = response.exceptionDocURL;
+
+ let errorDocLink;
+ if (errorDocURL) {
+ errorMessage += " ";
+ errorDocLink = this.hud.document.createElementNS(XHTML_NS, "a");
+ errorDocLink.className = "learn-more-link webconsole-learn-more-link";
+ errorDocLink.textContent = `[${l10n.getStr("webConsoleMoreInfoLabel")}]`;
+ errorDocLink.title = errorDocURL.split("?")[0];
+ errorDocLink.href = "#";
+ errorDocLink.draggable = false;
+ errorDocLink.addEventListener("click", () => {
+ this.hud.owner.openLink(errorDocURL);
+ });
+ }
+
+ // Wrap thrown strings in Error objects, so `throw "foo"` outputs
+ // "Error: foo"
+ if (typeof response.exception === "string") {
+ errorMessage = new Error(errorMessage).toString();
+ }
+ let result = response.result;
+ let helperResult = response.helperResult;
+ let helperHasRawOutput = !!(helperResult || {}).rawOutput;
+
+ if (helperResult && helperResult.type) {
+ switch (helperResult.type) {
+ case "clearOutput":
+ this.clearOutput();
+ break;
+ case "clearHistory":
+ this.clearHistory();
+ break;
+ case "inspectObject":
+ this.openVariablesView({
+ label:
+ VariablesView.getString(helperResult.object, { concise: true }),
+ objectActor: helperResult.object,
+ });
+ break;
+ case "error":
+ try {
+ errorMessage = l10n.getStr(helperResult.message);
+ } catch (ex) {
+ errorMessage = helperResult.message;
+ }
+ break;
+ case "help":
+ this.hud.owner.openLink(HELP_URL);
+ break;
+ case "copyValueToClipboard":
+ clipboardHelper.copyString(helperResult.value);
+ break;
+ }
+ }
+
+ // Hide undefined results coming from JSTerm helper functions.
+ if (!errorMessage && result && typeof result == "object" &&
+ result.type == "undefined" &&
+ helperResult && !helperHasRawOutput) {
+ callback && callback();
+ return;
+ }
+
+ if (this.hud.NEW_CONSOLE_OUTPUT_ENABLED) {
+ this.hud.newConsoleOutput.dispatchMessageAdd(response, true).then(callback);
+ return;
+ }
+ let msg = new Messages.JavaScriptEvalOutput(response,
+ errorMessage, errorDocLink);
+ this.hud.output.addMessage(msg);
+
+ if (callback) {
+ let oldFlushCallback = this.hud._flushCallback;
+ this.hud._flushCallback = () => {
+ callback(msg.element);
+ if (oldFlushCallback) {
+ oldFlushCallback();
+ this.hud._flushCallback = oldFlushCallback;
+ return true;
+ }
+
+ return false;
+ };
+ }
+
+ msg._objectActors = new Set();
+
+ if (WebConsoleUtils.isActorGrip(response.exception)) {
+ msg._objectActors.add(response.exception.actor);
+ }
+
+ if (WebConsoleUtils.isActorGrip(result)) {
+ msg._objectActors.add(result.actor);
+ }
+ },
+
+ /**
+ * Execute a string. Execution happens asynchronously in the content process.
+ *
+ * @param string [executeString]
+ * The string you want to execute. If this is not provided, the current
+ * user input is used - taken from |this.getInputValue()|.
+ * @param function [callback]
+ * Optional function to invoke when the result is displayed.
+ * This is deprecated - please use the promise return value instead.
+ * @returns Promise
+ * Resolves with the message once the result is displayed.
+ */
+ execute: function (executeString, callback) {
+ let deferred = promise.defer();
+ let resultCallback;
+ if (this.hud.NEW_CONSOLE_OUTPUT_ENABLED) {
+ resultCallback = (msg) => deferred.resolve(msg);
+ } else {
+ resultCallback = (msg) => {
+ deferred.resolve(msg);
+ if (callback) {
+ callback(msg);
+ }
+ };
+ }
+
+ // attempt to execute the content of the inputNode
+ executeString = executeString || this.getInputValue();
+ if (!executeString) {
+ return null;
+ }
+
+ let selectedNodeActor = null;
+ let inspectorSelection = this.hud.owner.getInspectorSelection();
+ if (inspectorSelection && inspectorSelection.nodeFront) {
+ selectedNodeActor = inspectorSelection.nodeFront.actorID;
+ }
+
+ if (this.hud.NEW_CONSOLE_OUTPUT_ENABLED) {
+ const { ConsoleCommand } = require("devtools/client/webconsole/new-console-output/types");
+ let message = new ConsoleCommand({
+ messageText: executeString,
+ });
+ this.hud.proxy.dispatchMessageAdd(message);
+ } else {
+ let message = new Messages.Simple(executeString, {
+ category: "input",
+ severity: "log",
+ });
+ this.hud.output.addMessage(message);
+ }
+ let onResult = this._executeResultCallback.bind(this, resultCallback);
+
+ let options = {
+ frame: this.SELECTED_FRAME,
+ selectedNodeActor: selectedNodeActor,
+ };
+
+ this.requestEvaluation(executeString, options).then(onResult, onResult);
+
+ // Append a new value in the history of executed code, or overwrite the most
+ // recent entry. The most recent entry may contain the last edited input
+ // value that was not evaluated yet.
+ this.history[this.historyIndex++] = executeString;
+ this.historyPlaceHolder = this.history.length;
+
+ if (this.history.length > this.inputHistoryCount) {
+ this.history.splice(0, this.history.length - this.inputHistoryCount);
+ this.historyIndex = this.historyPlaceHolder = this.history.length;
+ }
+ this.storeHistory();
+ WebConsoleUtils.usageCount++;
+ this.setInputValue("");
+ this.clearCompletion();
+ return deferred.promise;
+ },
+
+ /**
+ * Request a JavaScript string evaluation from the server.
+ *
+ * @param string str
+ * String to execute.
+ * @param object [options]
+ * Options for evaluation:
+ * - bindObjectActor: tells the ObjectActor ID for which you want to do
+ * the evaluation. The Debugger.Object of the OA will be bound to
+ * |_self| during evaluation, such that it's usable in the string you
+ * execute.
+ * - frame: tells the stackframe depth to evaluate the string in. If
+ * the jsdebugger is paused, you can pick the stackframe to be used for
+ * evaluation. Use |this.SELECTED_FRAME| to always pick the
+ * user-selected stackframe.
+ * If you do not provide a |frame| the string will be evaluated in the
+ * global content window.
+ * - selectedNodeActor: tells the NodeActor ID of the current selection
+ * in the Inspector, if such a selection exists. This is used by
+ * helper functions that can evaluate on the current selection.
+ * @return object
+ * A promise object that is resolved when the server response is
+ * received.
+ */
+ requestEvaluation: function (str, options = {}) {
+ let deferred = promise.defer();
+
+ function onResult(response) {
+ if (!response.error) {
+ deferred.resolve(response);
+ } else {
+ deferred.reject(response);
+ }
+ }
+
+ let frameActor = null;
+ if ("frame" in options) {
+ frameActor = this.getFrameActor(options.frame);
+ }
+
+ let evalOptions = {
+ bindObjectActor: options.bindObjectActor,
+ frameActor: frameActor,
+ selectedNodeActor: options.selectedNodeActor,
+ selectedObjectActor: options.selectedObjectActor,
+ };
+
+ this.webConsoleClient.evaluateJSAsync(str, onResult, evalOptions);
+ return deferred.promise;
+ },
+
+ /**
+ * Retrieve the FrameActor ID given a frame depth.
+ *
+ * @param number frame
+ * Frame depth.
+ * @return string|null
+ * The FrameActor ID for the given frame depth.
+ */
+ getFrameActor: function (frame) {
+ let state = this.hud.owner.getDebuggerFrames();
+ if (!state) {
+ return null;
+ }
+
+ let grip;
+ if (frame == this.SELECTED_FRAME) {
+ grip = state.frames[state.selected];
+ } else {
+ grip = state.frames[frame];
+ }
+
+ return grip ? grip.actor : null;
+ },
+
+ /**
+ * Opens a new variables view that allows the inspection of the given object.
+ *
+ * @param object options
+ * Options for the variables view:
+ * - objectActor: grip of the ObjectActor you want to show in the
+ * variables view.
+ * - rawObject: the raw object you want to show in the variables view.
+ * - label: label to display in the variables view for inspected
+ * object.
+ * - hideFilterInput: optional boolean, |true| if you want to hide the
+ * variables view filter input.
+ * - targetElement: optional nsIDOMElement to append the variables view
+ * to. An iframe element is used as a container for the view. If this
+ * option is not used, then the variables view opens in the sidebar.
+ * - autofocus: optional boolean, |true| if you want to give focus to
+ * the variables view window after open, |false| otherwise.
+ * @return object
+ * A promise object that is resolved when the variables view has
+ * opened. The new variables view instance is given to the callbacks.
+ */
+ openVariablesView: function (options) {
+ let onContainerReady = (window) => {
+ let container = window.document.querySelector("#variables");
+ let view = this._variablesView;
+ if (!view || options.targetElement) {
+ let viewOptions = {
+ container: container,
+ hideFilterInput: options.hideFilterInput,
+ };
+ view = this._createVariablesView(viewOptions);
+ if (!options.targetElement) {
+ this._variablesView = view;
+ window.addEventListener("keypress", this._onKeypressInVariablesView);
+ }
+ }
+ options.view = view;
+ this._updateVariablesView(options);
+
+ if (!options.targetElement && options.autofocus) {
+ window.focus();
+ }
+
+ this.emit("variablesview-open", view, options);
+ return view;
+ };
+
+ let openPromise;
+ if (options.targetElement) {
+ let deferred = promise.defer();
+ openPromise = deferred.promise;
+ let document = options.targetElement.ownerDocument;
+ let iframe = document.createElementNS(XHTML_NS, "iframe");
+
+ iframe.addEventListener("load", function onIframeLoad() {
+ iframe.removeEventListener("load", onIframeLoad, true);
+ iframe.style.visibility = "visible";
+ deferred.resolve(iframe.contentWindow);
+ }, true);
+
+ iframe.flex = 1;
+ iframe.style.visibility = "hidden";
+ iframe.setAttribute("src", VARIABLES_VIEW_URL);
+ options.targetElement.appendChild(iframe);
+ } else {
+ if (!this.sidebar) {
+ this._createSidebar();
+ }
+ openPromise = this._addVariablesViewSidebarTab();
+ }
+
+ return openPromise.then(onContainerReady);
+ },
+
+ /**
+ * Create the Web Console sidebar.
+ *
+ * @see devtools/framework/sidebar.js
+ * @private
+ */
+ _createSidebar: function () {
+ let tabbox = this.hud.document.querySelector("#webconsole-sidebar");
+ this.sidebar = new ToolSidebar(tabbox, this, "webconsole");
+ this.sidebar.show();
+ this.emit("sidebar-opened");
+ },
+
+ /**
+ * Add the variables view tab to the sidebar.
+ *
+ * @private
+ * @return object
+ * A promise object for the adding of the new tab.
+ */
+ _addVariablesViewSidebarTab: function () {
+ let deferred = promise.defer();
+
+ let onTabReady = () => {
+ let window = this.sidebar.getWindowForTab("variablesview");
+ deferred.resolve(window);
+ };
+
+ let tabPanel = this.sidebar.getTabPanel("variablesview");
+ if (tabPanel) {
+ if (this.sidebar.getCurrentTabID() == "variablesview") {
+ onTabReady();
+ } else {
+ this.sidebar.once("variablesview-selected", onTabReady);
+ this.sidebar.select("variablesview");
+ }
+ } else {
+ this.sidebar.once("variablesview-ready", onTabReady);
+ this.sidebar.addTab("variablesview", VARIABLES_VIEW_URL, {selected: true});
+ }
+
+ return deferred.promise;
+ },
+
+ /**
+ * The keypress event handler for the Variables View sidebar. Currently this
+ * is used for removing the sidebar when Escape is pressed.
+ *
+ * @private
+ * @param nsIDOMEvent event
+ * The keypress DOM event object.
+ */
+ _onKeypressInVariablesView: function (event) {
+ let tag = event.target.nodeName;
+ if (event.keyCode != KeyCodes.DOM_VK_ESCAPE || event.shiftKey ||
+ event.altKey || event.ctrlKey || event.metaKey ||
+ ["input", "textarea", "select", "textbox"].indexOf(tag) > -1) {
+ return;
+ }
+
+ this._sidebarDestroy();
+ this.focus();
+ event.stopPropagation();
+ },
+
+ /**
+ * Create a variables view instance.
+ *
+ * @private
+ * @param object options
+ * Options for the new Variables View instance:
+ * - container: the DOM element where the variables view is inserted.
+ * - hideFilterInput: boolean, if true the variables filter input is
+ * hidden.
+ * @return object
+ * The new Variables View instance.
+ */
+ _createVariablesView: function (options) {
+ let view = new VariablesView(options.container);
+ view.toolbox = gDevTools.getToolbox(this.hud.owner.target);
+ view.searchPlaceholder = l10n.getStr("propertiesFilterPlaceholder");
+ view.emptyText = l10n.getStr("emptyPropertiesList");
+ view.searchEnabled = !options.hideFilterInput;
+ view.lazyEmpty = this._lazyVariablesView;
+
+ VariablesViewController.attach(view, {
+ getEnvironmentClient: grip => {
+ return new EnvironmentClient(this.hud.proxy.client, grip);
+ },
+ getObjectClient: grip => {
+ return new ObjectClient(this.hud.proxy.client, grip);
+ },
+ getLongStringClient: grip => {
+ return this.webConsoleClient.longString(grip);
+ },
+ releaseActor: actor => {
+ this.hud._releaseObject(actor);
+ },
+ simpleValueEvalMacro: simpleValueEvalMacro,
+ overrideValueEvalMacro: overrideValueEvalMacro,
+ getterOrSetterEvalMacro: getterOrSetterEvalMacro,
+ });
+
+ // Relay events from the VariablesView.
+ view.on("fetched", (event, type, variableObject) => {
+ this.emit("variablesview-fetched", variableObject);
+ });
+
+ return view;
+ },
+
+ /**
+ * Update the variables view.
+ *
+ * @private
+ * @param object options
+ * Options for updating the variables view:
+ * - view: the view you want to update.
+ * - objectActor: the grip of the new ObjectActor you want to show in
+ * the view.
+ * - rawObject: the new raw object you want to show.
+ * - label: the new label for the inspected object.
+ */
+ _updateVariablesView: function (options) {
+ let view = options.view;
+ view.empty();
+
+ // We need to avoid pruning the object inspection starting point.
+ // That one is pruned when the console message is removed.
+ view.controller.releaseActors(actor => {
+ return view._consoleLastObjectActor != actor;
+ });
+
+ if (options.objectActor &&
+ (!this.hud.isBrowserConsole ||
+ Services.prefs.getBoolPref("devtools.chrome.enabled"))) {
+ // Make sure eval works in the correct context.
+ view.eval = this._variablesViewEvaluate.bind(this, options);
+ view.switch = this._variablesViewSwitch.bind(this, options);
+ view.delete = this._variablesViewDelete.bind(this, options);
+ } else {
+ view.eval = null;
+ view.switch = null;
+ view.delete = null;
+ }
+
+ let { variable, expanded } = view.controller.setSingleVariable(options);
+ variable.evaluationMacro = simpleValueEvalMacro;
+
+ if (options.objectActor) {
+ view._consoleLastObjectActor = options.objectActor.actor;
+ } else if (options.rawObject) {
+ view._consoleLastObjectActor = null;
+ } else {
+ throw new Error(
+ "Variables View cannot open without giving it an object display.");
+ }
+
+ expanded.then(() => {
+ this.emit("variablesview-updated", view, options);
+ });
+ },
+
+ /**
+ * The evaluation function used by the variables view when editing a property
+ * value.
+ *
+ * @private
+ * @param object options
+ * The options used for |this._updateVariablesView()|.
+ * @param object variableObject
+ * The Variable object instance for the edited property.
+ * @param string value
+ * The value the edited property was changed to.
+ */
+ _variablesViewEvaluate: function (options, variableObject, value) {
+ let updater = this._updateVariablesView.bind(this, options);
+ let onEval = this._silentEvalCallback.bind(this, updater);
+ let string = variableObject.evaluationMacro(variableObject, value);
+
+ let evalOptions = {
+ frame: this.SELECTED_FRAME,
+ bindObjectActor: options.objectActor.actor,
+ };
+
+ this.requestEvaluation(string, evalOptions).then(onEval, onEval);
+ },
+
+ /**
+ * The property deletion function used by the variables view when a property
+ * is deleted.
+ *
+ * @private
+ * @param object options
+ * The options used for |this._updateVariablesView()|.
+ * @param object variableObject
+ * The Variable object instance for the deleted property.
+ */
+ _variablesViewDelete: function (options, variableObject) {
+ let onEval = this._silentEvalCallback.bind(this, null);
+
+ let evalOptions = {
+ frame: this.SELECTED_FRAME,
+ bindObjectActor: options.objectActor.actor,
+ };
+
+ this.requestEvaluation("delete _self" +
+ variableObject.symbolicName, evalOptions).then(onEval, onEval);
+ },
+
+ /**
+ * The property rename function used by the variables view when a property
+ * is renamed.
+ *
+ * @private
+ * @param object options
+ * The options used for |this._updateVariablesView()|.
+ * @param object variableObject
+ * The Variable object instance for the renamed property.
+ * @param string newName
+ * The new name for the property.
+ */
+ _variablesViewSwitch: function (options, variableObject, newName) {
+ let updater = this._updateVariablesView.bind(this, options);
+ let onEval = this._silentEvalCallback.bind(this, updater);
+
+ let evalOptions = {
+ frame: this.SELECTED_FRAME,
+ bindObjectActor: options.objectActor.actor,
+ };
+
+ let newSymbolicName =
+ variableObject.ownerView.symbolicName + '["' + newName + '"]';
+ if (newSymbolicName == variableObject.symbolicName) {
+ return;
+ }
+
+ let code = "_self" + newSymbolicName + " = _self" +
+ variableObject.symbolicName + ";" + "delete _self" +
+ variableObject.symbolicName;
+
+ this.requestEvaluation(code, evalOptions).then(onEval, onEval);
+ },
+
+ /**
+ * A noop callback for JavaScript evaluation. This method releases any
+ * result ObjectActors that come from the server for evaluation requests. This
+ * is used for editing, renaming and deleting properties in the variables
+ * view.
+ *
+ * Exceptions are displayed in the output.
+ *
+ * @private
+ * @param function callback
+ * Function to invoke once the response is received.
+ * @param object response
+ * The response packet received from the server.
+ */
+ _silentEvalCallback: function (callback, response) {
+ if (response.error) {
+ console.error("Web Console evaluation failed. " + response.error + ":" +
+ response.message);
+
+ callback && callback(response);
+ return;
+ }
+
+ if (response.exceptionMessage) {
+ let message = new Messages.Simple(response.exceptionMessage, {
+ category: "output",
+ severity: "error",
+ timestamp: response.timestamp,
+ });
+ this.hud.output.addMessage(message);
+ message._objectActors = new Set();
+ if (WebConsoleUtils.isActorGrip(response.exception)) {
+ message._objectActors.add(response.exception.actor);
+ }
+ }
+
+ let helper = response.helperResult || { type: null };
+ let helperGrip = null;
+ if (helper.type == "inspectObject") {
+ helperGrip = helper.object;
+ }
+
+ let grips = [response.result, helperGrip];
+ for (let grip of grips) {
+ if (WebConsoleUtils.isActorGrip(grip)) {
+ this.hud._releaseObject(grip.actor);
+ }
+ }
+
+ callback && callback(response);
+ },
+
+ /**
+ * Clear the Web Console output.
+ *
+ * This method emits the "messages-cleared" notification.
+ *
+ * @param boolean clearStorage
+ * True if you want to clear the console messages storage associated to
+ * this Web Console.
+ */
+ clearOutput: function (clearStorage) {
+ let hud = this.hud;
+ let outputNode = hud.outputNode;
+ let node;
+ while ((node = outputNode.firstChild)) {
+ hud.removeOutputMessage(node);
+ }
+
+ hud.groupDepth = 0;
+ hud._outputQueue.forEach(hud._destroyItem, hud);
+ hud._outputQueue = [];
+ this.webConsoleClient.clearNetworkRequests();
+ hud._repeatNodes = {};
+
+ if (clearStorage) {
+ this.webConsoleClient.clearMessagesCache();
+ }
+
+ this._sidebarDestroy();
+
+ if (hud.NEW_CONSOLE_OUTPUT_ENABLED) {
+ hud.newConsoleOutput.dispatchMessagesClear();
+ }
+
+ this.emit("messages-cleared");
+ },
+
+ /**
+ * Remove all of the private messages from the Web Console output.
+ *
+ * This method emits the "private-messages-cleared" notification.
+ */
+ clearPrivateMessages: function () {
+ let nodes = this.hud.outputNode.querySelectorAll(".message[private]");
+ for (let node of nodes) {
+ this.hud.removeOutputMessage(node);
+ }
+ this.emit("private-messages-cleared");
+ },
+
+ /**
+ * Updates the size of the input field (command line) to fit its contents.
+ *
+ * @returns void
+ */
+ resizeInput: function () {
+ let inputNode = this.inputNode;
+
+ // Reset the height so that scrollHeight will reflect the natural height of
+ // the contents of the input field.
+ inputNode.style.height = "auto";
+
+ // Now resize the input field to fit its contents.
+ let scrollHeight = inputNode.inputField.scrollHeight;
+ if (scrollHeight > 0) {
+ inputNode.style.height = scrollHeight + "px";
+ }
+ },
+
+ /**
+ * Sets the value of the input field (command line), and resizes the field to
+ * fit its contents. This method is preferred over setting "inputNode.value"
+ * directly, because it correctly resizes the field.
+ *
+ * @param string newValue
+ * The new value to set.
+ * @returns void
+ */
+ setInputValue: function (newValue) {
+ this.inputNode.value = newValue;
+ this.lastInputValue = newValue;
+ this.completeNode.value = "";
+ this.resizeInput();
+ this._inputChanged = true;
+ this.emit("set-input-value");
+ },
+
+ /**
+ * Gets the value from the input field
+ * @returns string
+ */
+ getInputValue: function () {
+ return this.inputNode.value || "";
+ },
+
+ /**
+ * The inputNode "input" and "keyup" event handler.
+ * @private
+ */
+ _inputEventHandler: function () {
+ if (this.lastInputValue != this.getInputValue()) {
+ this.resizeInput();
+ this.complete(this.COMPLETE_HINT_ONLY);
+ this.lastInputValue = this.getInputValue();
+ this._inputChanged = true;
+ }
+ },
+
+ /**
+ * The window "blur" event handler.
+ * @private
+ */
+ _blurEventHandler: function () {
+ if (this.autocompletePopup) {
+ this.clearCompletion();
+ }
+ },
+
+ /* eslint-disable complexity */
+ /**
+ * The inputNode "keypress" event handler.
+ *
+ * @private
+ * @param nsIDOMEvent event
+ */
+ _keyPress: function (event) {
+ let inputNode = this.inputNode;
+ let inputValue = this.getInputValue();
+ let inputUpdated = false;
+
+ if (event.ctrlKey) {
+ switch (event.charCode) {
+ case 101:
+ // control-e
+ if (Services.appinfo.OS == "WINNT") {
+ break;
+ }
+ let lineEndPos = inputValue.length;
+ if (this.hasMultilineInput()) {
+ // find index of closest newline >= cursor
+ for (let i = inputNode.selectionEnd; i < lineEndPos; i++) {
+ if (inputValue.charAt(i) == "\r" ||
+ inputValue.charAt(i) == "\n") {
+ lineEndPos = i;
+ break;
+ }
+ }
+ }
+ inputNode.setSelectionRange(lineEndPos, lineEndPos);
+ event.preventDefault();
+ this.clearCompletion();
+ break;
+
+ case 110:
+ // Control-N differs from down arrow: it ignores autocomplete state.
+ // Note that we preserve the default 'down' navigation within
+ // multiline text.
+ if (Services.appinfo.OS == "Darwin" &&
+ this.canCaretGoNext() &&
+ this.historyPeruse(HISTORY_FORWARD)) {
+ event.preventDefault();
+ // Ctrl-N is also used to focus the Network category button on
+ // MacOSX. The preventDefault() call doesn't prevent the focus
+ // from moving away from the input.
+ this.focus();
+ }
+ this.clearCompletion();
+ break;
+
+ case 112:
+ // Control-P differs from up arrow: it ignores autocomplete state.
+ // Note that we preserve the default 'up' navigation within
+ // multiline text.
+ if (Services.appinfo.OS == "Darwin" &&
+ this.canCaretGoPrevious() &&
+ this.historyPeruse(HISTORY_BACK)) {
+ event.preventDefault();
+ // Ctrl-P may also be used to focus some category button on MacOSX.
+ // The preventDefault() call doesn't prevent the focus from moving
+ // away from the input.
+ this.focus();
+ }
+ this.clearCompletion();
+ break;
+ default:
+ break;
+ }
+ return;
+ } else if (event.keyCode == KeyCodes.DOM_VK_RETURN) {
+ let autoMultiline = Services.prefs.getBoolPref(PREF_AUTO_MULTILINE);
+ if (event.shiftKey ||
+ (!Debugger.isCompilableUnit(inputNode.value) && autoMultiline)) {
+ // shift return or incomplete statement
+ return;
+ }
+ }
+
+ switch (event.keyCode) {
+ case KeyCodes.DOM_VK_ESCAPE:
+ if (this.autocompletePopup.isOpen) {
+ this.clearCompletion();
+ event.preventDefault();
+ event.stopPropagation();
+ } else if (this.sidebar) {
+ this._sidebarDestroy();
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ break;
+
+ case KeyCodes.DOM_VK_RETURN:
+ if (this._autocompletePopupNavigated &&
+ this.autocompletePopup.isOpen &&
+ this.autocompletePopup.selectedIndex > -1) {
+ this.acceptProposedCompletion();
+ } else {
+ this.execute();
+ this._inputChanged = false;
+ }
+ event.preventDefault();
+ break;
+
+ case KeyCodes.DOM_VK_UP:
+ if (this.autocompletePopup.isOpen) {
+ inputUpdated = this.complete(this.COMPLETE_BACKWARD);
+ if (inputUpdated) {
+ this._autocompletePopupNavigated = true;
+ }
+ } else if (this.canCaretGoPrevious()) {
+ inputUpdated = this.historyPeruse(HISTORY_BACK);
+ }
+ if (inputUpdated) {
+ event.preventDefault();
+ }
+ break;
+
+ case KeyCodes.DOM_VK_DOWN:
+ if (this.autocompletePopup.isOpen) {
+ inputUpdated = this.complete(this.COMPLETE_FORWARD);
+ if (inputUpdated) {
+ this._autocompletePopupNavigated = true;
+ }
+ } else if (this.canCaretGoNext()) {
+ inputUpdated = this.historyPeruse(HISTORY_FORWARD);
+ }
+ if (inputUpdated) {
+ event.preventDefault();
+ }
+ break;
+
+ case KeyCodes.DOM_VK_PAGE_UP:
+ if (this.autocompletePopup.isOpen) {
+ inputUpdated = this.complete(this.COMPLETE_PAGEUP);
+ if (inputUpdated) {
+ this._autocompletePopupNavigated = true;
+ }
+ } else {
+ this.hud.outputScroller.scrollTop =
+ Math.max(0,
+ this.hud.outputScroller.scrollTop -
+ this.hud.outputScroller.clientHeight
+ );
+ }
+ event.preventDefault();
+ break;
+
+ case KeyCodes.DOM_VK_PAGE_DOWN:
+ if (this.autocompletePopup.isOpen) {
+ inputUpdated = this.complete(this.COMPLETE_PAGEDOWN);
+ if (inputUpdated) {
+ this._autocompletePopupNavigated = true;
+ }
+ } else {
+ this.hud.outputScroller.scrollTop =
+ Math.min(this.hud.outputScroller.scrollHeight,
+ this.hud.outputScroller.scrollTop +
+ this.hud.outputScroller.clientHeight
+ );
+ }
+ event.preventDefault();
+ break;
+
+ case KeyCodes.DOM_VK_HOME:
+ if (this.autocompletePopup.isOpen) {
+ this.autocompletePopup.selectedIndex = 0;
+ event.preventDefault();
+ } else if (inputValue.length <= 0) {
+ this.hud.outputScroller.scrollTop = 0;
+ event.preventDefault();
+ }
+ break;
+
+ case KeyCodes.DOM_VK_END:
+ if (this.autocompletePopup.isOpen) {
+ this.autocompletePopup.selectedIndex =
+ this.autocompletePopup.itemCount - 1;
+ event.preventDefault();
+ } else if (inputValue.length <= 0) {
+ this.hud.outputScroller.scrollTop =
+ this.hud.outputScroller.scrollHeight;
+ event.preventDefault();
+ }
+ break;
+
+ case KeyCodes.DOM_VK_LEFT:
+ if (this.autocompletePopup.isOpen || this.lastCompletion.value) {
+ this.clearCompletion();
+ }
+ break;
+
+ case KeyCodes.DOM_VK_RIGHT:
+ let cursorAtTheEnd = this.inputNode.selectionStart ==
+ this.inputNode.selectionEnd &&
+ this.inputNode.selectionStart ==
+ inputValue.length;
+ let haveSuggestion = this.autocompletePopup.isOpen ||
+ this.lastCompletion.value;
+ let useCompletion = cursorAtTheEnd || this._autocompletePopupNavigated;
+ if (haveSuggestion && useCompletion &&
+ this.complete(this.COMPLETE_HINT_ONLY) &&
+ this.lastCompletion.value &&
+ this.acceptProposedCompletion()) {
+ event.preventDefault();
+ }
+ if (this.autocompletePopup.isOpen) {
+ this.clearCompletion();
+ }
+ break;
+
+ case KeyCodes.DOM_VK_TAB:
+ // Generate a completion and accept the first proposed value.
+ if (this.complete(this.COMPLETE_HINT_ONLY) &&
+ this.lastCompletion &&
+ this.acceptProposedCompletion()) {
+ event.preventDefault();
+ } else if (this._inputChanged) {
+ this.updateCompleteNode(l10n.getStr("Autocomplete.blank"));
+ event.preventDefault();
+ }
+ break;
+ default:
+ break;
+ }
+ },
+ /* eslint-enable complexity */
+
+ /**
+ * The inputNode "focus" event handler.
+ * @private
+ */
+ _focusEventHandler: function () {
+ this._inputChanged = false;
+ },
+
+ /**
+ * Go up/down the history stack of input values.
+ *
+ * @param number direction
+ * History navigation direction: HISTORY_BACK or HISTORY_FORWARD.
+ *
+ * @returns boolean
+ * True if the input value changed, false otherwise.
+ */
+ historyPeruse: function (direction) {
+ if (!this.history.length) {
+ return false;
+ }
+
+ // Up Arrow key
+ if (direction == HISTORY_BACK) {
+ if (this.historyPlaceHolder <= 0) {
+ return false;
+ }
+ let inputVal = this.history[--this.historyPlaceHolder];
+
+ // Save the current input value as the latest entry in history, only if
+ // the user is already at the last entry.
+ // Note: this code does not store changes to items that are already in
+ // history.
+ if (this.historyPlaceHolder + 1 == this.historyIndex) {
+ this.history[this.historyIndex] = this.getInputValue() || "";
+ }
+
+ this.setInputValue(inputVal);
+ } else if (direction == HISTORY_FORWARD) {
+ // Down Arrow key
+ if (this.historyPlaceHolder >= (this.history.length - 1)) {
+ return false;
+ }
+
+ let inputVal = this.history[++this.historyPlaceHolder];
+ this.setInputValue(inputVal);
+ } else {
+ throw new Error("Invalid argument 0");
+ }
+
+ return true;
+ },
+
+ /**
+ * Test for multiline input.
+ *
+ * @return boolean
+ * True if CR or LF found in node value; else false.
+ */
+ hasMultilineInput: function () {
+ return /[\r\n]/.test(this.getInputValue());
+ },
+
+ /**
+ * Check if the caret is at a location that allows selecting the previous item
+ * in history when the user presses the Up arrow key.
+ *
+ * @return boolean
+ * True if the caret is at a location that allows selecting the
+ * previous item in history when the user presses the Up arrow key,
+ * otherwise false.
+ */
+ canCaretGoPrevious: function () {
+ let node = this.inputNode;
+ if (node.selectionStart != node.selectionEnd) {
+ return false;
+ }
+
+ let multiline = /[\r\n]/.test(node.value);
+ return node.selectionStart == 0 ? true :
+ node.selectionStart == node.value.length && !multiline;
+ },
+
+ /**
+ * Check if the caret is at a location that allows selecting the next item in
+ * history when the user presses the Down arrow key.
+ *
+ * @return boolean
+ * True if the caret is at a location that allows selecting the next
+ * item in history when the user presses the Down arrow key, otherwise
+ * false.
+ */
+ canCaretGoNext: function () {
+ let node = this.inputNode;
+ if (node.selectionStart != node.selectionEnd) {
+ return false;
+ }
+
+ let multiline = /[\r\n]/.test(node.value);
+ return node.selectionStart == node.value.length ? true :
+ node.selectionStart == 0 && !multiline;
+ },
+
+ /**
+ * Completes the current typed text in the inputNode. Completion is performed
+ * only if the selection/cursor is at the end of the string. If no completion
+ * is found, the current inputNode value and cursor/selection stay.
+ *
+ * @param int type possible values are
+ * - this.COMPLETE_FORWARD: If there is more than one possible completion
+ * and the input value stayed the same compared to the last time this
+ * function was called, then the next completion of all possible
+ * completions is used. If the value changed, then the first possible
+ * completion is used and the selection is set from the current
+ * cursor position to the end of the completed text.
+ * If there is only one possible completion, then this completion
+ * value is used and the cursor is put at the end of the completion.
+ * - this.COMPLETE_BACKWARD: Same as this.COMPLETE_FORWARD but if the
+ * value stayed the same as the last time the function was called,
+ * then the previous completion of all possible completions is used.
+ * - this.COMPLETE_PAGEUP: Scroll up one page if available or select the
+ * first item.
+ * - this.COMPLETE_PAGEDOWN: Scroll down one page if available or select
+ * the last item.
+ * - this.COMPLETE_HINT_ONLY: If there is more than one possible
+ * completion and the input value stayed the same compared to the
+ * last time this function was called, then the same completion is
+ * used again. If there is only one possible completion, then
+ * the this.getInputValue() is set to this value and the selection
+ * is set from the current cursor position to the end of the
+ * completed text.
+ * @param function callback
+ * Optional function invoked when the autocomplete properties are
+ * updated.
+ * @returns boolean true if there existed a completion for the current input,
+ * or false otherwise.
+ */
+ complete: function (type, callback) {
+ let inputNode = this.inputNode;
+ let inputValue = this.getInputValue();
+ let frameActor = this.getFrameActor(this.SELECTED_FRAME);
+
+ // If the inputNode has no value, then don't try to complete on it.
+ if (!inputValue) {
+ this.clearCompletion();
+ callback && callback(this);
+ this.emit("autocomplete-updated");
+ return false;
+ }
+
+ // Only complete if the selection is empty.
+ if (inputNode.selectionStart != inputNode.selectionEnd) {
+ this.clearCompletion();
+ callback && callback(this);
+ this.emit("autocomplete-updated");
+ return false;
+ }
+
+ // Update the completion results.
+ if (this.lastCompletion.value != inputValue ||
+ frameActor != this._lastFrameActorId) {
+ this._updateCompletionResult(type, callback);
+ return false;
+ }
+
+ let popup = this.autocompletePopup;
+ let accepted = false;
+
+ if (type != this.COMPLETE_HINT_ONLY && popup.itemCount == 1) {
+ this.acceptProposedCompletion();
+ accepted = true;
+ } else if (type == this.COMPLETE_BACKWARD) {
+ popup.selectPreviousItem();
+ } else if (type == this.COMPLETE_FORWARD) {
+ popup.selectNextItem();
+ } else if (type == this.COMPLETE_PAGEUP) {
+ popup.selectPreviousPageItem();
+ } else if (type == this.COMPLETE_PAGEDOWN) {
+ popup.selectNextPageItem();
+ }
+
+ callback && callback(this);
+ this.emit("autocomplete-updated");
+ return accepted || popup.itemCount > 0;
+ },
+
+ /**
+ * Update the completion result. This operation is performed asynchronously by
+ * fetching updated results from the content process.
+ *
+ * @private
+ * @param int type
+ * Completion type. See this.complete() for details.
+ * @param function [callback]
+ * Optional, function to invoke when completion results are received.
+ */
+ _updateCompletionResult: function (type, callback) {
+ let frameActor = this.getFrameActor(this.SELECTED_FRAME);
+ if (this.lastCompletion.value == this.getInputValue() &&
+ frameActor == this._lastFrameActorId) {
+ return;
+ }
+
+ let requestId = gSequenceId();
+ let cursor = this.inputNode.selectionStart;
+ let input = this.getInputValue().substring(0, cursor);
+ let cache = this._autocompleteCache;
+
+ // If the current input starts with the previous input, then we already
+ // have a list of suggestions and we just need to filter the cached
+ // suggestions. When the current input ends with a non-alphanumeric
+ // character we ask the server again for suggestions.
+
+ // Check if last character is non-alphanumeric
+ if (!/[a-zA-Z0-9]$/.test(input) || frameActor != this._lastFrameActorId) {
+ this._autocompleteQuery = null;
+ this._autocompleteCache = null;
+ }
+
+ if (this._autocompleteQuery && input.startsWith(this._autocompleteQuery)) {
+ let filterBy = input;
+ // Find the last non-alphanumeric other than _ or $ if it exists.
+ let lastNonAlpha = input.match(/[^a-zA-Z0-9_$][a-zA-Z0-9_$]*$/);
+ // If input contains non-alphanumerics, use the part after the last one
+ // to filter the cache
+ if (lastNonAlpha) {
+ filterBy = input.substring(input.lastIndexOf(lastNonAlpha) + 1);
+ }
+
+ let newList = cache.sort().filter(function (l) {
+ return l.startsWith(filterBy);
+ });
+
+ this.lastCompletion = {
+ requestId: null,
+ completionType: type,
+ value: null,
+ };
+
+ let response = { matches: newList, matchProp: filterBy };
+ this._receiveAutocompleteProperties(null, callback, response);
+ return;
+ }
+
+ this._lastFrameActorId = frameActor;
+
+ this.lastCompletion = {
+ requestId: requestId,
+ completionType: type,
+ value: null,
+ };
+
+ let autocompleteCallback =
+ this._receiveAutocompleteProperties.bind(this, requestId, callback);
+
+ this.webConsoleClient.autocomplete(
+ input, cursor, autocompleteCallback, frameActor);
+ },
+
+ /**
+ * Handler for the autocompletion results. This method takes
+ * the completion result received from the server and updates the UI
+ * accordingly.
+ *
+ * @param number requestId
+ * Request ID.
+ * @param function [callback=null]
+ * Optional, function to invoke when the completion result is received.
+ * @param object message
+ * The JSON message which holds the completion results received from
+ * the content process.
+ */
+ _receiveAutocompleteProperties: function (requestId, callback, message) {
+ let inputNode = this.inputNode;
+ let inputValue = this.getInputValue();
+ if (this.lastCompletion.value == inputValue ||
+ requestId != this.lastCompletion.requestId) {
+ return;
+ }
+ // Cache whatever came from the server if the last char is
+ // alphanumeric or '.'
+ let cursor = inputNode.selectionStart;
+ let inputUntilCursor = inputValue.substring(0, cursor);
+
+ if (requestId != null && /[a-zA-Z0-9.]$/.test(inputUntilCursor)) {
+ this._autocompleteCache = message.matches;
+ this._autocompleteQuery = inputUntilCursor;
+ }
+
+ let matches = message.matches;
+ let lastPart = message.matchProp;
+ if (!matches.length) {
+ this.clearCompletion();
+ callback && callback(this);
+ this.emit("autocomplete-updated");
+ return;
+ }
+
+ let items = matches.reverse().map(function (match) {
+ return { preLabel: lastPart, label: match };
+ });
+
+ let popup = this.autocompletePopup;
+ popup.setItems(items);
+
+ let completionType = this.lastCompletion.completionType;
+ this.lastCompletion = {
+ value: inputValue,
+ matchProp: lastPart,
+ };
+
+ if (items.length > 1 && !popup.isOpen) {
+ let str = this.getInputValue().substr(0, this.inputNode.selectionStart);
+ let offset = str.length - (str.lastIndexOf("\n") + 1) - lastPart.length;
+ let x = offset * this.hud._inputCharWidth;
+ popup.openPopup(inputNode, x + this.hud._chevronWidth);
+ this._autocompletePopupNavigated = false;
+ } else if (items.length < 2 && popup.isOpen) {
+ popup.hidePopup();
+ this._autocompletePopupNavigated = false;
+ }
+
+ if (items.length == 1) {
+ popup.selectedIndex = 0;
+ }
+
+ this.onAutocompleteSelect();
+
+ if (completionType != this.COMPLETE_HINT_ONLY && popup.itemCount == 1) {
+ this.acceptProposedCompletion();
+ } else if (completionType == this.COMPLETE_BACKWARD) {
+ popup.selectPreviousItem();
+ } else if (completionType == this.COMPLETE_FORWARD) {
+ popup.selectNextItem();
+ }
+
+ callback && callback(this);
+ this.emit("autocomplete-updated");
+ },
+
+ onAutocompleteSelect: function () {
+ // Render the suggestion only if the cursor is at the end of the input.
+ if (this.inputNode.selectionStart != this.getInputValue().length) {
+ return;
+ }
+
+ let currentItem = this.autocompletePopup.selectedItem;
+ if (currentItem && this.lastCompletion.value) {
+ let suffix =
+ currentItem.label.substring(this.lastCompletion.matchProp.length);
+ this.updateCompleteNode(suffix);
+ } else {
+ this.updateCompleteNode("");
+ }
+ },
+
+ /**
+ * Clear the current completion information and close the autocomplete popup,
+ * if needed.
+ */
+ clearCompletion: function () {
+ this.autocompletePopup.clearItems();
+ this.lastCompletion = { value: null };
+ this.updateCompleteNode("");
+ if (this.autocompletePopup.isOpen) {
+ // Trigger a blur/focus of the JSTerm input to force screen readers to read the
+ // value again.
+ this.inputNode.blur();
+ this.autocompletePopup.once("popup-closed", () => {
+ this.inputNode.focus();
+ });
+ this.autocompletePopup.hidePopup();
+ this._autocompletePopupNavigated = false;
+ }
+ },
+
+ /**
+ * Accept the proposed input completion.
+ *
+ * @return boolean
+ * True if there was a selected completion item and the input value
+ * was updated, false otherwise.
+ */
+ acceptProposedCompletion: function () {
+ let updated = false;
+
+ let currentItem = this.autocompletePopup.selectedItem;
+ if (currentItem && this.lastCompletion.value) {
+ let suffix =
+ currentItem.label.substring(this.lastCompletion.matchProp.length);
+ let cursor = this.inputNode.selectionStart;
+ let value = this.getInputValue();
+ this.setInputValue(value.substr(0, cursor) +
+ suffix + value.substr(cursor));
+ let newCursor = cursor + suffix.length;
+ this.inputNode.selectionStart = this.inputNode.selectionEnd = newCursor;
+ updated = true;
+ }
+
+ this.clearCompletion();
+
+ return updated;
+ },
+
+ /**
+ * Update the node that displays the currently selected autocomplete proposal.
+ *
+ * @param string suffix
+ * The proposed suffix for the inputNode value.
+ */
+ updateCompleteNode: function (suffix) {
+ // completion prefix = input, with non-control chars replaced by spaces
+ let prefix = suffix ? this.getInputValue().replace(/[\S]/g, " ") : "";
+ this.completeNode.value = prefix + suffix;
+ },
+
+ /**
+ * Destroy the sidebar.
+ * @private
+ */
+ _sidebarDestroy: function () {
+ if (this._variablesView) {
+ this._variablesView.controller.releaseActors();
+ this._variablesView = null;
+ }
+
+ if (this.sidebar) {
+ this.sidebar.hide();
+ this.sidebar.destroy();
+ this.sidebar = null;
+ }
+
+ this.emit("sidebar-closed");
+ },
+
+ /**
+ * Destroy the JSTerm object. Call this method to avoid memory leaks.
+ */
+ destroy: function () {
+ this._sidebarDestroy();
+
+ this.clearCompletion();
+ this.clearOutput();
+
+ this.autocompletePopup.destroy();
+ this.autocompletePopup = null;
+
+ if (this._onPaste) {
+ this.inputNode.removeEventListener("paste", this._onPaste, false);
+ this.inputNode.removeEventListener("drop", this._onPaste, false);
+ this._onPaste = null;
+ }
+
+ this.inputNode.removeEventListener("keypress", this._keyPress, false);
+ this.inputNode.removeEventListener("input", this._inputEventHandler, false);
+ this.inputNode.removeEventListener("keyup", this._inputEventHandler, false);
+ this.inputNode.removeEventListener("focus", this._focusEventHandler, false);
+ this.hud.window.removeEventListener("blur", this._blurEventHandler, false);
+
+ this.hud = null;
+ },
+};
+
+function gSequenceId() {
+ return gSequenceId.n++;
+}
+gSequenceId.n = 0;
+exports.gSequenceId = gSequenceId;
+
+/**
+ * @see VariablesView.simpleValueEvalMacro
+ */
+function simpleValueEvalMacro(item, currentString) {
+ return VariablesView.simpleValueEvalMacro(item, currentString, "_self");
+}
+
+/**
+ * @see VariablesView.overrideValueEvalMacro
+ */
+function overrideValueEvalMacro(item, currentString) {
+ return VariablesView.overrideValueEvalMacro(item, currentString, "_self");
+}
+
+/**
+ * @see VariablesView.getterOrSetterEvalMacro
+ */
+function getterOrSetterEvalMacro(item, currentString) {
+ return VariablesView.getterOrSetterEvalMacro(item, currentString, "_self");
+}
+
+exports.JSTerm = JSTerm;
diff --git a/devtools/client/webconsole/moz.build b/devtools/client/webconsole/moz.build
new file mode 100644
index 000000000..c8324b315
--- /dev/null
+++ b/devtools/client/webconsole/moz.build
@@ -0,0 +1,22 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
+
+DIRS += [
+ 'net',
+ 'new-console-output',
+]
+
+DevToolsModules(
+ 'console-commands.js',
+ 'console-output.js',
+ 'hudservice.js',
+ 'jsterm.js',
+ 'panel.js',
+ 'utils.js',
+ 'webconsole.js',
+)
diff --git a/devtools/client/webconsole/net/.eslintrc.js b/devtools/client/webconsole/net/.eslintrc.js
new file mode 100644
index 000000000..e105ac6e2
--- /dev/null
+++ b/devtools/client/webconsole/net/.eslintrc.js
@@ -0,0 +1,20 @@
+"use strict";
+
+module.exports = {
+ "globals": {
+ "Locale": true,
+ "Document": true,
+ "document": true,
+ "Node": true,
+ "Element": true,
+ "MessageEvent": true,
+ "BrowserLoader": true,
+ "addEventListener": true,
+ "DOMParser": true,
+ "dispatchEvent": true,
+ "setTimeout": true
+ },
+ "rules": {
+ "no-unused-vars": ["error", {"args": "none"}],
+ }
+};
diff --git a/devtools/client/webconsole/net/components/cookies-tab.js b/devtools/client/webconsole/net/components/cookies-tab.js
new file mode 100644
index 000000000..d76414679
--- /dev/null
+++ b/devtools/client/webconsole/net/components/cookies-tab.js
@@ -0,0 +1,75 @@
+/* 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 React = require("devtools/client/shared/vendor/react");
+const NetInfoGroupList = React.createFactory(require("./net-info-group-list"));
+const Spinner = React.createFactory(require("./spinner"));
+
+// Shortcuts
+const DOM = React.DOM;
+const PropTypes = React.PropTypes;
+
+/**
+ * This template represents 'Cookies' tab displayed when the user
+ * expands network log in the Console panel. It's responsible for rendering
+ * sent and received cookies.
+ */
+var CookiesTab = React.createClass({
+ propTypes: {
+ actions: PropTypes.shape({
+ requestData: PropTypes.func.isRequired
+ }),
+ data: PropTypes.object.isRequired,
+ },
+
+ displayName: "CookiesTab",
+
+ componentDidMount() {
+ let { actions, data } = this.props;
+ let requestCookies = data.request.cookies;
+ let responseCookies = data.response.cookies;
+
+ // TODO: use async action objects as soon as Redux is in place
+ if (!requestCookies || !requestCookies.length) {
+ actions.requestData("requestCookies");
+ }
+
+ if (!responseCookies || !responseCookies.length) {
+ actions.requestData("responseCookies");
+ }
+ },
+
+ render() {
+ let { actions, data: file } = this.props;
+ let requestCookies = file.request.cookies;
+ let responseCookies = file.response.cookies;
+
+ // The cookie panel displays two groups of cookies:
+ // 1) Response Cookies
+ // 2) Request Cookies
+ let groups = [{
+ key: "responseCookies",
+ name: Locale.$STR("responseCookies"),
+ params: responseCookies
+ }, {
+ key: "requestCookies",
+ name: Locale.$STR("requestCookies"),
+ params: requestCookies
+ }];
+
+ return (
+ DOM.div({className: "cookiesTabBox"},
+ DOM.div({className: "panelContent"},
+ NetInfoGroupList({
+ groups: groups
+ })
+ )
+ )
+ );
+ }
+});
+
+// Exports from this module
+module.exports = CookiesTab;
diff --git a/devtools/client/webconsole/net/components/headers-tab.js b/devtools/client/webconsole/net/components/headers-tab.js
new file mode 100644
index 000000000..2eca3fd2f
--- /dev/null
+++ b/devtools/client/webconsole/net/components/headers-tab.js
@@ -0,0 +1,79 @@
+/* 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 React = require("devtools/client/shared/vendor/react");
+const NetInfoGroupList = React.createFactory(require("./net-info-group-list"));
+const Spinner = React.createFactory(require("./spinner"));
+
+// Shortcuts
+const DOM = React.DOM;
+const PropTypes = React.PropTypes;
+
+/**
+ * This template represents 'Headers' tab displayed when the user
+ * expands network log in the Console panel. It's responsible for rendering
+ * request and response HTTP headers.
+ */
+var HeadersTab = React.createClass({
+ propTypes: {
+ actions: PropTypes.shape({
+ requestData: PropTypes.func.isRequired
+ }),
+ data: PropTypes.object.isRequired,
+ },
+
+ displayName: "HeadersTab",
+
+ componentDidMount() {
+ let { actions, data } = this.props;
+ let requestHeaders = data.request.headers;
+ let responseHeaders = data.response.headers;
+
+ // Request headers if they are not available yet.
+ // TODO: use async action objects as soon as Redux is in place
+ if (!requestHeaders) {
+ actions.requestData("requestHeaders");
+ }
+
+ if (!responseHeaders) {
+ actions.requestData("responseHeaders");
+ }
+ },
+
+ render() {
+ let { data } = this.props;
+ let requestHeaders = data.request.headers;
+ let responseHeaders = data.response.headers;
+
+ // TODO: Another groups to implement:
+ // 1) Cached Headers
+ // 2) Headers from upload stream
+ let groups = [{
+ key: "responseHeaders",
+ name: Locale.$STR("responseHeaders"),
+ params: responseHeaders
+ }, {
+ key: "requestHeaders",
+ name: Locale.$STR("requestHeaders"),
+ params: requestHeaders
+ }];
+
+ // If response headers are not available yet, display a spinner
+ if (!responseHeaders || !responseHeaders.length) {
+ groups[0].content = Spinner();
+ }
+
+ return (
+ DOM.div({className: "headersTabBox"},
+ DOM.div({className: "panelContent"},
+ NetInfoGroupList({groups: groups})
+ )
+ )
+ );
+ }
+});
+
+// Exports from this module
+module.exports = HeadersTab;
diff --git a/devtools/client/webconsole/net/components/moz.build b/devtools/client/webconsole/net/components/moz.build
new file mode 100644
index 000000000..0053de780
--- /dev/null
+++ b/devtools/client/webconsole/net/components/moz.build
@@ -0,0 +1,25 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'cookies-tab.js',
+ 'headers-tab.js',
+ 'net-info-body.css',
+ 'net-info-body.js',
+ 'net-info-group-list.js',
+ 'net-info-group.css',
+ 'net-info-group.js',
+ 'net-info-params.css',
+ 'net-info-params.js',
+ 'params-tab.js',
+ 'post-tab.js',
+ 'response-tab.css',
+ 'response-tab.js',
+ 'size-limit.css',
+ 'size-limit.js',
+ 'spinner.js',
+ 'stacktrace-tab.js',
+)
diff --git a/devtools/client/webconsole/net/components/net-info-body.css b/devtools/client/webconsole/net/components/net-info-body.css
new file mode 100644
index 000000000..2d0bac70e
--- /dev/null
+++ b/devtools/client/webconsole/net/components/net-info-body.css
@@ -0,0 +1,112 @@
+/* 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/. */
+
+/******************************************************************************/
+/* Network Info Body */
+
+.netInfoBody {
+ margin: 10px 0 0 0;
+ width: 100%;
+ cursor: default;
+ display: block;
+}
+
+.netInfoBody *:focus {
+ outline: 0 !important;
+}
+
+.netInfoBody .panelContent {
+ word-break: break-all;
+}
+
+/******************************************************************************/
+/* Network Info Body Tabs */
+
+.netInfoBody > .tabs {
+ background-color: transparent;
+ background-image: none;
+ height: 100%;
+}
+
+.netInfoBody > .tabs .tabs-navigation {
+ border-bottom-color: var(--net-border);
+ background-color: transparent;
+ text-decoration: none;
+ padding-top: 3px;
+ padding-left: 7px;
+ padding-bottom: 1px;
+ border-bottom: 1px solid var(--net-border);
+}
+
+.netInfoBody > .tabs .tabs-menu {
+ display: table;
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+/* This is the trick that makes the tab bottom border invisible */
+.netInfoBody > .tabs .tabs-menu-item {
+ position: relative;
+ bottom: -2px;
+ float: left;
+}
+
+.netInfoBody > .tabs .tabs-menu-item a {
+ display: block;
+ border: 1px solid transparent;
+ text-decoration: none;
+ padding: 5px 8px 4px 8px;;
+ font-weight: bold;
+ color: var(--theme-body-color);
+ border-radius: 4px 4px 0 0;
+}
+
+.netInfoBody > .tabs .tab-panel {
+ background-color: var(--theme-body-background);
+ border: 1px solid transparent;
+ border-top: none;
+ padding: 10px;
+ overflow: auto;
+ height: calc(100% - 31px); /* minus the height of the tab bar */
+}
+
+.netInfoBody > .tabs .tab-panel > div,
+.netInfoBody > .tabs .tab-panel > div > div {
+ height: 100%;
+}
+
+.netInfoBody > .tabs .tabs-menu-item.is-active a,
+.netInfoBody > .tabs .tabs-menu-item.is-active a:focus,
+.netInfoBody > .tabs .tabs-menu-item.is-active:hover a {
+ background-color: var(--theme-body-background);
+ border: 1px solid transparent;
+ border-bottom-color: var(--theme-highlight-bluegrey);
+ color: var(--theme-highlight-bluegrey);
+}
+
+.netInfoBody > .tabs .tabs-menu-item:hover a {
+ border: 1px solid transparent;
+ border-bottom: 1px solid var(--net-border);
+ background-color: var(--theme-body-background);
+}
+
+
+/******************************************************************************/
+/* Themes */
+
+.theme-firebug .netInfoBody > .tabs .tab-panel {
+ border-color: var(--net-border);
+}
+
+.theme-firebug .netInfoBody > .tabs .tabs-menu-item.is-active a,
+.theme-firebug .netInfoBody > .tabs .tabs-menu-item.is-active:hover a,
+.theme-firebug .netInfoBody > .tabs .tabs-menu-item.is-active a:focus {
+ border: 1px solid var(--net-border);
+ border-bottom-color: transparent;
+}
+
+.theme-firebug .netInfoBody > .tabs .tabs-menu-item:hover a {
+ border-bottom-color: transparent;
+}
diff --git a/devtools/client/webconsole/net/components/net-info-body.js b/devtools/client/webconsole/net/components/net-info-body.js
new file mode 100644
index 000000000..c5eccd458
--- /dev/null
+++ b/devtools/client/webconsole/net/components/net-info-body.js
@@ -0,0 +1,179 @@
+/* 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 React = require("devtools/client/shared/vendor/react");
+const { createFactories } = require("devtools/client/shared/components/reps/rep-utils");
+const { Tabs, TabPanel } = createFactories(require("devtools/client/shared/components/tabs/tabs"));
+
+// Network
+const HeadersTab = React.createFactory(require("./headers-tab"));
+const ResponseTab = React.createFactory(require("./response-tab"));
+const ParamsTab = React.createFactory(require("./params-tab"));
+const CookiesTab = React.createFactory(require("./cookies-tab"));
+const PostTab = React.createFactory(require("./post-tab"));
+const StackTraceTab = React.createFactory(require("./stacktrace-tab"));
+const NetUtils = require("../utils/net");
+
+// Shortcuts
+const PropTypes = React.PropTypes;
+
+/**
+ * This template renders the basic Network log info body. It's not
+ * visible by default, the user needs to expand the network log
+ * to see it.
+ *
+ * This is the set of tabs displaying details about network events:
+ * 1) Headers - request and response headers
+ * 2) Params - URL parameters
+ * 3) Response - response body
+ * 4) Cookies - request and response cookies
+ * 5) Post - posted data
+ */
+var NetInfoBody = React.createClass({
+ propTypes: {
+ tabActive: PropTypes.number.isRequired,
+ actions: PropTypes.object.isRequired,
+ data: PropTypes.shape({
+ request: PropTypes.object.isRequired,
+ response: PropTypes.object.isRequired
+ })
+ },
+
+ displayName: "NetInfoBody",
+
+ getDefaultProps() {
+ return {
+ tabActive: 0
+ };
+ },
+
+ getInitialState() {
+ return {
+ data: {
+ request: {},
+ response: {}
+ },
+ tabActive: this.props.tabActive,
+ };
+ },
+
+ onTabChanged(index) {
+ this.setState({tabActive: index});
+ },
+
+ hasCookies() {
+ let {request, response} = this.state.data;
+ return this.state.hasCookies ||
+ NetUtils.getHeaderValue(request.headers, "Cookie") ||
+ NetUtils.getHeaderValue(response.headers, "Set-Cookie");
+ },
+
+ hasStackTrace() {
+ let {cause} = this.state.data;
+ return cause && cause.stacktrace && cause.stacktrace.length > 0;
+ },
+
+ getTabPanels() {
+ let actions = this.props.actions;
+ let data = this.state.data;
+ let {request} = data;
+
+ // Flags for optional tabs. Some tabs are visible only if there
+ // are data to display.
+ let hasParams = request.queryString && request.queryString.length;
+ let hasPostData = request.bodySize > 0;
+
+ let panels = [];
+
+ // Headers tab
+ panels.push(
+ TabPanel({
+ className: "headers",
+ key: "headers",
+ title: Locale.$STR("netRequest.headers")},
+ HeadersTab({data: data, actions: actions})
+ )
+ );
+
+ // URL parameters tab
+ if (hasParams) {
+ panels.push(
+ TabPanel({
+ className: "params",
+ key: "params",
+ title: Locale.$STR("netRequest.params")},
+ ParamsTab({data: data, actions: actions})
+ )
+ );
+ }
+
+ // Posted data tab
+ if (hasPostData) {
+ panels.push(
+ TabPanel({
+ className: "post",
+ key: "post",
+ title: Locale.$STR("netRequest.post")},
+ PostTab({data: data, actions: actions})
+ )
+ );
+ }
+
+ // Response tab
+ panels.push(
+ TabPanel({className: "response", key: "response",
+ title: Locale.$STR("netRequest.response")},
+ ResponseTab({data: data, actions: actions})
+ )
+ );
+
+ // Cookies tab
+ if (this.hasCookies()) {
+ panels.push(
+ TabPanel({
+ className: "cookies",
+ key: "cookies",
+ title: Locale.$STR("netRequest.cookies")},
+ CookiesTab({
+ data: data,
+ actions: actions
+ })
+ )
+ );
+ }
+
+ // Stacktrace tab
+ if (this.hasStackTrace()) {
+ panels.push(
+ TabPanel({
+ className: "stacktrace-tab",
+ key: "stacktrace",
+ title: Locale.$STR("netRequest.callstack")},
+ StackTraceTab({
+ data: data,
+ actions: actions
+ })
+ )
+ );
+ }
+
+ return panels;
+ },
+
+ render() {
+ let tabActive = this.state.tabActive;
+ let tabPanels = this.getTabPanels();
+ return (
+ Tabs({
+ tabActive: tabActive,
+ onAfterChange: this.onTabChanged},
+ tabPanels
+ )
+ );
+ }
+});
+
+// Exports from this module
+module.exports = NetInfoBody;
diff --git a/devtools/client/webconsole/net/components/net-info-group-list.js b/devtools/client/webconsole/net/components/net-info-group-list.js
new file mode 100644
index 000000000..247a23bb7
--- /dev/null
+++ b/devtools/client/webconsole/net/components/net-info-group-list.js
@@ -0,0 +1,47 @@
+/* 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 React = require("devtools/client/shared/vendor/react");
+const NetInfoGroup = React.createFactory(require("./net-info-group"));
+
+// Shortcuts
+const DOM = React.DOM;
+const PropTypes = React.PropTypes;
+
+/**
+ * This template is responsible for rendering sections/groups inside tabs.
+ * It's used e.g to display Response and Request headers as separate groups.
+ */
+var NetInfoGroupList = React.createClass({
+ propTypes: {
+ groups: PropTypes.array.isRequired,
+ },
+
+ displayName: "NetInfoGroupList",
+
+ render() {
+ let groups = this.props.groups;
+
+ // Filter out empty groups.
+ groups = groups.filter(group => {
+ return group && ((group.params && group.params.length) || group.content);
+ });
+
+ // Render groups
+ groups = groups.map(group => {
+ group.type = group.key;
+ return NetInfoGroup(group);
+ });
+
+ return (
+ DOM.div({className: "netInfoGroupList"},
+ groups
+ )
+ );
+ }
+});
+
+// Exports from this module
+module.exports = NetInfoGroupList;
diff --git a/devtools/client/webconsole/net/components/net-info-group.css b/devtools/client/webconsole/net/components/net-info-group.css
new file mode 100644
index 000000000..43800019f
--- /dev/null
+++ b/devtools/client/webconsole/net/components/net-info-group.css
@@ -0,0 +1,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/. */
+
+/******************************************************************************/
+/* Net Info Group */
+
+.netInfoBody .netInfoGroup {
+ padding-bottom: 6px;
+}
+
+/* Last group doesn't need bottom padding */
+.netInfoBody .netInfoGroup:last-child {
+ padding-bottom: 0;
+}
+
+.netInfoBody .netInfoGroup:last-child .netInfoGroupContent {
+ padding-bottom: 0;
+}
+
+.netInfoBody .netInfoGroupTitle {
+ cursor: pointer;
+ font-weight: bold;
+ -moz-user-select: none;
+ cursor: pointer;
+ padding-left: 3px;
+}
+
+.netInfoBody .netInfoGroupTwisty {
+ background-image: url("chrome://devtools/skin/images/controls.png");
+ background-size: 56px 28px;
+ background-position: 0 -14px;
+ background-repeat: no-repeat;
+ width: 14px;
+ height: 14px;
+ cursor: pointer;
+ display: inline-block;
+ vertical-align: middle;
+}
+
+.netInfoBody .netInfoGroup.opened .netInfoGroupTwisty {
+ background-position: -14px -14px;
+}
+
+/* Group content is expandable/collapsible by clicking on the title */
+.netInfoBody .netInfoGroupContent {
+ padding-top: 7px;
+ margin-top: 3px;
+ padding-bottom: 14px;
+ border-top: 1px solid var(--net-border);
+ display: none;
+}
+
+/* Toggle group visibility */
+.netInfoBody .netInfoGroup.opened .netInfoGroupContent {
+ display: block;
+}
+
+/******************************************************************************/
+/* Themes */
+
+.theme-dark .netInfoBody .netInfoGroup {
+ color: var(--theme-body-color);
+}
+
+.theme-dark .netInfoBody .netInfoGroup .netInfoGroupTwisty {
+ filter: invert(1);
+}
+
+/* Twisties */
+.theme-firebug .netInfoBody .netInfoGroup .netInfoGroupTwisty {
+ background-image: url("chrome://devtools/skin/images/firebug/twisty-closed-firebug.svg");
+ background-position: 0 2px;
+ background-size: 11px 11px;
+ width: 15px;
+}
+
+.theme-firebug .netInfoBody .netInfoGroup.opened .netInfoGroupTwisty {
+ background-image: url("chrome://devtools/skin/images/firebug/twisty-open-firebug.svg");
+}
diff --git a/devtools/client/webconsole/net/components/net-info-group.js b/devtools/client/webconsole/net/components/net-info-group.js
new file mode 100644
index 000000000..d9794652e
--- /dev/null
+++ b/devtools/client/webconsole/net/components/net-info-group.js
@@ -0,0 +1,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 React = require("devtools/client/shared/vendor/react");
+const NetInfoParams = React.createFactory(require("./net-info-params"));
+
+// Shortcuts
+const DOM = React.DOM;
+const PropTypes = React.PropTypes;
+
+/**
+ * This template represents a group of data within a tab. For example,
+ * Headers tab has two groups 'Request Headers' and 'Response Headers'
+ * The Response tab can also have two groups 'Raw Data' and 'JSON'
+ */
+var NetInfoGroup = React.createClass({
+ propTypes: {
+ type: PropTypes.string.isRequired,
+ name: PropTypes.string.isRequired,
+ params: PropTypes.array,
+ content: PropTypes.element,
+ open: PropTypes.bool
+ },
+
+ displayName: "NetInfoGroup",
+
+ getDefaultProps() {
+ return {
+ open: true,
+ };
+ },
+
+ getInitialState() {
+ return {
+ open: this.props.open,
+ };
+ },
+
+ onToggle(event) {
+ this.setState({
+ open: !this.state.open
+ });
+ },
+
+ render() {
+ let content = this.props.content;
+
+ if (!content && this.props.params) {
+ content = NetInfoParams({
+ params: this.props.params
+ });
+ }
+
+ let open = this.state.open;
+ let className = open ? "opened" : "";
+
+ return (
+ DOM.div({className: "netInfoGroup" + " " + className + " " +
+ this.props.type},
+ DOM.span({
+ className: "netInfoGroupTwisty",
+ onClick: this.onToggle
+ }),
+ DOM.span({
+ className: "netInfoGroupTitle",
+ onClick: this.onToggle},
+ this.props.name
+ ),
+ DOM.div({className: "netInfoGroupContent"},
+ content
+ )
+ )
+ );
+ }
+});
+
+// Exports from this module
+module.exports = NetInfoGroup;
diff --git a/devtools/client/webconsole/net/components/net-info-params.css b/devtools/client/webconsole/net/components/net-info-params.css
new file mode 100644
index 000000000..4ec7140f8
--- /dev/null
+++ b/devtools/client/webconsole/net/components/net-info-params.css
@@ -0,0 +1,23 @@
+/* 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/. */
+
+/******************************************************************************/
+/* Net Info Params */
+
+.netInfoBody .netInfoParamName {
+ padding: 0 10px 0 0;
+ font-weight: bold;
+ vertical-align: top;
+ text-align: right;
+ white-space: nowrap;
+}
+
+.netInfoBody .netInfoParamValue {
+ width: 100%;
+ word-wrap: break-word;
+}
+
+.netInfoBody .netInfoParamValue > code {
+ font-family: var(--monospace-font-family);
+}
diff --git a/devtools/client/webconsole/net/components/net-info-params.js b/devtools/client/webconsole/net/components/net-info-params.js
new file mode 100644
index 000000000..573257b28
--- /dev/null
+++ b/devtools/client/webconsole/net/components/net-info-params.js
@@ -0,0 +1,58 @@
+/* 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 React = require("devtools/client/shared/vendor/react");
+
+// Shortcuts
+const DOM = React.DOM;
+const PropTypes = React.PropTypes;
+
+/**
+ * This template renders list of parameters within a group.
+ * It's essentially a list of name + value pairs.
+ */
+var NetInfoParams = React.createClass({
+ displayName: "NetInfoParams",
+
+ propTypes: {
+ params: PropTypes.arrayOf(PropTypes.shape({
+ name: PropTypes.string.isRequired,
+ value: PropTypes.string.isRequired
+ })).isRequired,
+ },
+
+ render() {
+ let params = this.props.params || [];
+
+ params.sort(function (a, b) {
+ return a.name > b.name ? 1 : -1;
+ });
+
+ let rows = [];
+ params.forEach((param, index) => {
+ rows.push(
+ DOM.tr({key: index},
+ DOM.td({className: "netInfoParamName"},
+ DOM.span({title: param.name}, param.name)
+ ),
+ DOM.td({className: "netInfoParamValue"},
+ DOM.code({}, param.value)
+ )
+ )
+ );
+ });
+
+ return (
+ DOM.table({cellPadding: 0, cellSpacing: 0},
+ DOM.tbody({},
+ rows
+ )
+ )
+ );
+ }
+});
+
+// Exports from this module
+module.exports = NetInfoParams;
diff --git a/devtools/client/webconsole/net/components/params-tab.js b/devtools/client/webconsole/net/components/params-tab.js
new file mode 100644
index 000000000..c3fefc669
--- /dev/null
+++ b/devtools/client/webconsole/net/components/params-tab.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 React = require("devtools/client/shared/vendor/react");
+const NetInfoParams = React.createFactory(require("./net-info-params"));
+
+// Shortcuts
+const DOM = React.DOM;
+const PropTypes = React.PropTypes;
+
+/**
+ * This template represents 'Params' tab displayed when the user
+ * expands network log in the Console panel. It's responsible for
+ * displaying URL parameters (query string).
+ */
+var ParamsTab = React.createClass({
+ propTypes: {
+ data: PropTypes.shape({
+ request: PropTypes.object.isRequired
+ })
+ },
+
+ displayName: "ParamsTab",
+
+ render() {
+ let data = this.props.data;
+
+ return (
+ DOM.div({className: "paramsTabBox"},
+ DOM.div({className: "panelContent"},
+ NetInfoParams({params: data.request.queryString})
+ )
+ )
+ );
+ }
+});
+
+// Exports from this module
+module.exports = ParamsTab;
diff --git a/devtools/client/webconsole/net/components/post-tab.js b/devtools/client/webconsole/net/components/post-tab.js
new file mode 100644
index 000000000..6d06eb40b
--- /dev/null
+++ b/devtools/client/webconsole/net/components/post-tab.js
@@ -0,0 +1,279 @@
+/* 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 React = require("devtools/client/shared/vendor/react");
+
+// Reps
+const { createFactories, parseURLEncodedText } = require("devtools/client/shared/components/reps/rep-utils");
+const TreeView = React.createFactory(require("devtools/client/shared/components/tree/tree-view"));
+const { Rep } = createFactories(require("devtools/client/shared/components/reps/rep"));
+
+// Network
+const NetInfoParams = React.createFactory(require("./net-info-params"));
+const NetInfoGroupList = React.createFactory(require("./net-info-group-list"));
+const Spinner = React.createFactory(require("./spinner"));
+const SizeLimit = React.createFactory(require("./size-limit"));
+const NetUtils = require("../utils/net");
+const Json = require("../utils/json");
+
+// Shortcuts
+const DOM = React.DOM;
+const PropTypes = React.PropTypes;
+
+/**
+ * This template represents 'Post' tab displayed when the user
+ * expands network log in the Console panel. It's responsible for
+ * displaying posted data (HTTP post body).
+ */
+var PostTab = React.createClass({
+ propTypes: {
+ data: PropTypes.shape({
+ request: PropTypes.object.isRequired
+ }),
+ actions: PropTypes.object.isRequired
+ },
+
+ displayName: "PostTab",
+
+ isJson(file) {
+ let text = file.request.postData.text;
+ let value = NetUtils.getHeaderValue(file.request.headers, "content-type");
+ return Json.isJSON(value, text);
+ },
+
+ parseJson(file) {
+ let postData = file.request.postData;
+ if (!postData) {
+ return null;
+ }
+
+ let jsonString = new String(postData.text);
+ return Json.parseJSONString(jsonString);
+ },
+
+ /**
+ * Render JSON post data as an expandable tree.
+ */
+ renderJson(file) {
+ let text = file.request.postData.text;
+ if (!text || isLongString(text)) {
+ return null;
+ }
+
+ if (!this.isJson(file)) {
+ return null;
+ }
+
+ let json = this.parseJson(file);
+ if (!json) {
+ return null;
+ }
+
+ return {
+ key: "json",
+ content: TreeView({
+ columns: [{id: "value"}],
+ object: json,
+ mode: "tiny",
+ renderValue: props => Rep(Object.assign({}, props, {
+ cropLimit: 50,
+ })),
+ }),
+ name: Locale.$STR("jsonScopeName")
+ };
+ },
+
+ parseXml(file) {
+ let text = file.request.postData.text;
+ if (isLongString(text)) {
+ return null;
+ }
+
+ return NetUtils.parseXml({
+ mimeType: NetUtils.getHeaderValue(file.request.headers, "content-type"),
+ text: text,
+ });
+ },
+
+ isXml(file) {
+ if (isLongString(file.request.postData.text)) {
+ return false;
+ }
+
+ let value = NetUtils.getHeaderValue(file.request.headers, "content-type");
+ if (!value) {
+ return false;
+ }
+
+ return NetUtils.isHTML(value);
+ },
+
+ renderXml(file) {
+ let text = file.request.postData.text;
+ if (!text || isLongString(text)) {
+ return null;
+ }
+
+ if (!this.isXml(file)) {
+ return null;
+ }
+
+ let doc = this.parseXml(file);
+ if (!doc) {
+ return null;
+ }
+
+ // Proper component for rendering XML should be used (see bug 1247392)
+ return null;
+ },
+
+ /**
+ * Multipart post data are parsed and nicely rendered
+ * as an expandable tree of individual parts.
+ */
+ renderMultiPart(file) {
+ let text = file.request.postData.text;
+ if (!text || isLongString(text)) {
+ return;
+ }
+
+ if (NetUtils.isMultiPartRequest(file)) {
+ // TODO: render multi part request (bug: 1247423)
+ }
+
+ return;
+ },
+
+ /**
+ * URL encoded post data are nicely rendered as a list
+ * of parameters.
+ */
+ renderUrlEncoded(file) {
+ let text = file.request.postData.text;
+ if (!text || isLongString(text)) {
+ return null;
+ }
+
+ if (!NetUtils.isURLEncodedRequest(file)) {
+ return null;
+ }
+
+ let lines = text.split("\n");
+ let params = parseURLEncodedText(lines[lines.length - 1]);
+
+ return {
+ key: "url-encoded",
+ content: NetInfoParams({params: params}),
+ name: Locale.$STR("netRequest.params")
+ };
+ },
+
+ renderRawData(file) {
+ let text = file.request.postData.text;
+
+ let group;
+
+ // The post body might reached the limit, so check if we are
+ // dealing with a long string.
+ if (typeof text == "object") {
+ group = {
+ key: "raw-longstring",
+ name: Locale.$STR("netRequest.rawData"),
+ content: DOM.div({className: "netInfoResponseContent"},
+ sanitize(text.initial),
+ SizeLimit({
+ actions: this.props.actions,
+ data: file.request.postData,
+ message: Locale.$STR("netRequest.sizeLimitMessage"),
+ link: Locale.$STR("netRequest.sizeLimitMessageLink")
+ })
+ )
+ };
+ } else {
+ group = {
+ key: "raw",
+ name: Locale.$STR("netRequest.rawData"),
+ content: DOM.div({className: "netInfoResponseContent"},
+ sanitize(text)
+ )
+ };
+ }
+
+ return group;
+ },
+
+ componentDidMount() {
+ let { actions, data: file } = this.props;
+
+ if (!file.request.postData) {
+ // TODO: use async action objects as soon as Redux is in place
+ actions.requestData("requestPostData");
+ }
+ },
+
+ render() {
+ let { actions, data: file } = this.props;
+
+ if (file.discardRequestBody) {
+ return DOM.span({className: "netInfoBodiesDiscarded"},
+ Locale.$STR("netRequest.requestBodyDiscarded")
+ );
+ }
+
+ if (!file.request.postData) {
+ return (
+ Spinner()
+ );
+ }
+
+ // Render post body data. The right representation of the data
+ // is picked according to the content type.
+ let groups = [];
+ groups.push(this.renderUrlEncoded(file));
+ // TODO: render multi part request (bug: 1247423)
+ // groups.push(this.renderMultiPart(file));
+ groups.push(this.renderJson(file));
+ groups.push(this.renderXml(file));
+ groups.push(this.renderRawData(file));
+
+ // Filter out empty groups.
+ groups = groups.filter(group => group);
+
+ // The raw response is collapsed by default if a nice formatted
+ // version is available.
+ if (groups.length > 1) {
+ groups[groups.length - 1].open = false;
+ }
+
+ return (
+ DOM.div({className: "postTabBox"},
+ DOM.div({className: "panelContent"},
+ NetInfoGroupList({
+ groups: groups
+ })
+ )
+ )
+ );
+ }
+});
+
+// Helpers
+
+/**
+ * Workaround for a "not well-formed" error that react
+ * reports when there's multipart data passed to render.
+ */
+function sanitize(text) {
+ text = JSON.stringify(text);
+ text = text.replace(/\\r\\n/g, "\r\n").replace(/\\"/g, "\"");
+ return text.slice(1, text.length - 1);
+}
+
+function isLongString(text) {
+ return typeof text == "object";
+}
+
+// Exports from this module
+module.exports = PostTab;
diff --git a/devtools/client/webconsole/net/components/response-tab.css b/devtools/client/webconsole/net/components/response-tab.css
new file mode 100644
index 000000000..e1c31fca4
--- /dev/null
+++ b/devtools/client/webconsole/net/components/response-tab.css
@@ -0,0 +1,21 @@
+/* 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/. */
+
+/******************************************************************************/
+/* Response Tab */
+
+.netInfoBody .netInfoBodiesDiscarded {
+ font-style: italic;
+ color: gray;
+}
+
+.netInfoBody .netInfoResponseContent {
+ font-family: var(--monospace-font-family);
+ word-wrap: break-word;
+}
+
+.netInfoBody .responseTabBox img {
+ max-width: 300px;
+ max-height: 300px;
+}
diff --git a/devtools/client/webconsole/net/components/response-tab.js b/devtools/client/webconsole/net/components/response-tab.js
new file mode 100644
index 000000000..78d8b2f77
--- /dev/null
+++ b/devtools/client/webconsole/net/components/response-tab.js
@@ -0,0 +1,277 @@
+/* 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 React = require("devtools/client/shared/vendor/react");
+
+// Reps
+const { createFactories } = require("devtools/client/shared/components/reps/rep-utils");
+const TreeView = React.createFactory(require("devtools/client/shared/components/tree/tree-view"));
+const { Rep } = createFactories(require("devtools/client/shared/components/reps/rep"));
+
+// Network
+const SizeLimit = React.createFactory(require("./size-limit"));
+const NetInfoGroupList = React.createFactory(require("./net-info-group-list"));
+const Spinner = React.createFactory(require("./spinner"));
+const Json = require("../utils/json");
+const NetUtils = require("../utils/net");
+
+// Shortcuts
+const DOM = React.DOM;
+const PropTypes = React.PropTypes;
+
+/**
+ * This template represents 'Response' tab displayed when the user
+ * expands network log in the Console panel. It's responsible for
+ * rendering HTTP response body.
+ *
+ * In case of supported response mime-type (e.g. application/json,
+ * text/xml, etc.), the response is parsed using appropriate parser
+ * and rendered accordingly.
+ */
+var ResponseTab = React.createClass({
+ propTypes: {
+ data: PropTypes.shape({
+ request: PropTypes.object.isRequired,
+ response: PropTypes.object.isRequired
+ }),
+ actions: PropTypes.object.isRequired
+ },
+
+ displayName: "ResponseTab",
+
+ // Response Types
+
+ isJson(content) {
+ if (isLongString(content.text)) {
+ return false;
+ }
+
+ return Json.isJSON(content.mimeType, content.text);
+ },
+
+ parseJson(file) {
+ let content = file.response.content;
+ if (isLongString(content.text)) {
+ return null;
+ }
+
+ let jsonString = new String(content.text);
+ return Json.parseJSONString(jsonString);
+ },
+
+ isImage(content) {
+ if (isLongString(content.text)) {
+ return false;
+ }
+
+ return NetUtils.isImage(content.mimeType);
+ },
+
+ isXml(content) {
+ if (isLongString(content.text)) {
+ return false;
+ }
+
+ return NetUtils.isHTML(content.mimeType);
+ },
+
+ parseXml(file) {
+ let content = file.response.content;
+ if (isLongString(content.text)) {
+ return null;
+ }
+
+ return NetUtils.parseXml(content);
+ },
+
+ // Rendering
+
+ renderJson(file) {
+ let content = file.response.content;
+ if (!this.isJson(content)) {
+ return null;
+ }
+
+ let json = this.parseJson(file);
+ if (!json) {
+ return null;
+ }
+
+ return {
+ key: "json",
+ content: TreeView({
+ columns: [{id: "value"}],
+ object: json,
+ mode: "tiny",
+ renderValue: props => Rep(Object.assign({}, props, {
+ cropLimit: 50,
+ })),
+ }),
+ name: Locale.$STR("jsonScopeName")
+ };
+ },
+
+ renderImage(file) {
+ let content = file.response.content;
+ if (!this.isImage(content)) {
+ return null;
+ }
+
+ let dataUri = "data:" + content.mimeType + ";base64," + content.text;
+ return {
+ key: "image",
+ content: DOM.img({src: dataUri}),
+ name: Locale.$STR("netRequest.image")
+ };
+ },
+
+ renderXml(file) {
+ let content = file.response.content;
+ if (!this.isXml(content)) {
+ return null;
+ }
+
+ let doc = this.parseXml(file);
+ if (!doc) {
+ return null;
+ }
+
+ // Proper component for rendering XML should be used (see bug 1247392)
+ return null;
+ },
+
+ /**
+ * If full response text is available, let's try to parse and
+ * present nicely according to the underlying format.
+ */
+ renderFormattedResponse(file) {
+ let content = file.response.content;
+ if (typeof content.text == "object") {
+ return null;
+ }
+
+ let group = this.renderJson(file);
+ if (group) {
+ return group;
+ }
+
+ group = this.renderImage(file);
+ if (group) {
+ return group;
+ }
+
+ group = this.renderXml(file);
+ if (group) {
+ return group;
+ }
+ },
+
+ renderRawResponse(file) {
+ let group;
+ let content = file.response.content;
+
+ // The response might reached the limit, so check if we are
+ // dealing with a long string.
+ if (typeof content.text == "object") {
+ group = {
+ key: "raw-longstring",
+ name: Locale.$STR("netRequest.rawData"),
+ content: DOM.div({className: "netInfoResponseContent"},
+ content.text.initial,
+ SizeLimit({
+ actions: this.props.actions,
+ data: content,
+ message: Locale.$STR("netRequest.sizeLimitMessage"),
+ link: Locale.$STR("netRequest.sizeLimitMessageLink")
+ })
+ )
+ };
+ } else {
+ group = {
+ key: "raw",
+ name: Locale.$STR("netRequest.rawData"),
+ content: DOM.div({className: "netInfoResponseContent"},
+ content.text
+ )
+ };
+ }
+
+ return group;
+ },
+
+ componentDidMount() {
+ let { actions, data: file } = this.props;
+ let content = file.response.content;
+
+ if (!content || typeof (content.text) == "undefined") {
+ // TODO: use async action objects as soon as Redux is in place
+ actions.requestData("responseContent");
+ }
+ },
+
+ /**
+ * The response panel displays two groups:
+ *
+ * 1) Formatted response (in case of supported format, e.g. JSON, XML, etc.)
+ * 2) Raw response data (always displayed if not discarded)
+ */
+ render() {
+ let { actions, data: file } = this.props;
+
+ // If response bodies are discarded (not collected) let's just
+ // display a info message indicating what to do to collect even
+ // response bodies.
+ if (file.discardResponseBody) {
+ return DOM.span({className: "netInfoBodiesDiscarded"},
+ Locale.$STR("netRequest.responseBodyDiscarded")
+ );
+ }
+
+ // Request for the response content is done only if the response
+ // is not fetched yet - i.e. the `content.text` is undefined.
+ // Empty content.text` can also be a valid response either
+ // empty or not available yet.
+ let content = file.response.content;
+ if (!content || typeof (content.text) == "undefined") {
+ return (
+ Spinner()
+ );
+ }
+
+ // Render response body data. The right representation of the data
+ // is picked according to the content type.
+ let groups = [];
+ groups.push(this.renderFormattedResponse(file));
+ groups.push(this.renderRawResponse(file));
+
+ // Filter out empty groups.
+ groups = groups.filter(group => group);
+
+ // The raw response is collapsed by default if a nice formatted
+ // version is available.
+ if (groups.length > 1) {
+ groups[1].open = false;
+ }
+
+ return (
+ DOM.div({className: "responseTabBox"},
+ DOM.div({className: "panelContent"},
+ NetInfoGroupList({
+ groups: groups
+ })
+ )
+ )
+ );
+ }
+});
+
+// Helpers
+
+function isLongString(text) {
+ return typeof text == "object";
+}
+
+// Exports from this module
+module.exports = ResponseTab;
diff --git a/devtools/client/webconsole/net/components/size-limit.css b/devtools/client/webconsole/net/components/size-limit.css
new file mode 100644
index 000000000..a5c214d9e
--- /dev/null
+++ b/devtools/client/webconsole/net/components/size-limit.css
@@ -0,0 +1,15 @@
+/* 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/. */
+
+/******************************************************************************/
+/* Response Size Limit */
+
+.netInfoBody .netInfoSizeLimit {
+ font-weight: bold;
+ padding-top: 10px;
+}
+
+.netInfoBody .netInfoSizeLimit .objectLink {
+ color: var(--theme-highlight-blue);
+}
diff --git a/devtools/client/webconsole/net/components/size-limit.js b/devtools/client/webconsole/net/components/size-limit.js
new file mode 100644
index 000000000..de8839314
--- /dev/null
+++ b/devtools/client/webconsole/net/components/size-limit.js
@@ -0,0 +1,62 @@
+/* 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 React = require("devtools/client/shared/vendor/react");
+
+// Shortcuts
+const DOM = React.DOM;
+const PropTypes = React.PropTypes;
+
+/**
+ * This template represents a size limit notification message
+ * used e.g. in the Response tab when response body exceeds
+ * size limit. The message contains a link allowing the user
+ * to fetch the rest of the data from the backend (debugger server).
+ */
+var SizeLimit = React.createClass({
+ propTypes: {
+ data: PropTypes.object.isRequired,
+ message: PropTypes.string.isRequired,
+ link: PropTypes.string.isRequired,
+ actions: PropTypes.shape({
+ resolveString: PropTypes.func.isRequired
+ }),
+ },
+
+ displayName: "SizeLimit",
+
+ // Event Handlers
+
+ onClickLimit(event) {
+ let actions = this.props.actions;
+ let content = this.props.data;
+
+ actions.resolveString(content, "text");
+ },
+
+ // Rendering
+
+ render() {
+ let message = this.props.message;
+ let link = this.props.link;
+ let reLink = /^(.*)\{\{link\}\}(.*$)/;
+ let m = message.match(reLink);
+
+ return (
+ DOM.div({className: "netInfoSizeLimit"},
+ DOM.span({}, m[1]),
+ DOM.a({
+ className: "objectLink",
+ onClick: this.onClickLimit},
+ link
+ ),
+ DOM.span({}, m[2])
+ )
+ );
+ }
+});
+
+// Exports from this module
+module.exports = SizeLimit;
diff --git a/devtools/client/webconsole/net/components/spinner.js b/devtools/client/webconsole/net/components/spinner.js
new file mode 100644
index 000000000..fe79f7dd1
--- /dev/null
+++ b/devtools/client/webconsole/net/components/spinner.js
@@ -0,0 +1,26 @@
+/* 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 React = require("devtools/client/shared/vendor/react");
+
+// Shortcuts
+const DOM = React.DOM;
+
+/**
+ * This template represents a throbber displayed when the UI
+ * is waiting for data coming from the backend (debugging server).
+ */
+var Spinner = React.createClass({
+ displayName: "Spinner",
+
+ render() {
+ return (
+ DOM.div({className: "devtools-throbber"})
+ );
+ }
+});
+
+// Exports from this module
+module.exports = Spinner;
diff --git a/devtools/client/webconsole/net/components/stacktrace-tab.js b/devtools/client/webconsole/net/components/stacktrace-tab.js
new file mode 100644
index 000000000..51eb7689b
--- /dev/null
+++ b/devtools/client/webconsole/net/components/stacktrace-tab.js
@@ -0,0 +1,29 @@
+/* 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 { PropTypes, createClass, createFactory } = require("devtools/client/shared/vendor/react");
+const StackTrace = createFactory(require("devtools/client/shared/components/stack-trace"));
+
+const StackTraceTab = createClass({
+ displayName: "StackTraceTab",
+
+ propTypes: {
+ data: PropTypes.object.isRequired,
+ actions: PropTypes.shape({
+ onViewSourceInDebugger: PropTypes.func.isRequired
+ })
+ },
+
+ render() {
+ let { stacktrace } = this.props.data.cause;
+ let { actions } = this.props;
+ let onViewSourceInDebugger = actions.onViewSourceInDebugger.bind(actions);
+
+ return StackTrace({ stacktrace, onViewSourceInDebugger });
+ }
+});
+
+// Exports from this module
+module.exports = StackTraceTab;
diff --git a/devtools/client/webconsole/net/data-provider.js b/devtools/client/webconsole/net/data-provider.js
new file mode 100644
index 000000000..d8a70d72d
--- /dev/null
+++ b/devtools/client/webconsole/net/data-provider.js
@@ -0,0 +1,66 @@
+/* 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");
+
+/**
+ * Map of pending requests. Used mainly by tests to wait
+ * till things are ready.
+ */
+var promises = new Map();
+
+/**
+ * This object is used to fetch network data from the backend.
+ * Communication with the chrome scope is based on message
+ * exchange.
+ */
+var DataProvider = {
+ hasPendingRequests: function () {
+ return promises.size > 0;
+ },
+
+ requestData: function (client, actor, method) {
+ let key = actor + ":" + method;
+ let p = promises.get(key);
+ if (p) {
+ return p;
+ }
+
+ let deferred = promise.defer();
+ let realMethodName = "get" + method.charAt(0).toUpperCase() +
+ method.slice(1);
+
+ if (!client[realMethodName]) {
+ return null;
+ }
+
+ client[realMethodName](actor, response => {
+ promises.delete(key);
+ deferred.resolve(response);
+ });
+
+ promises.set(key, deferred.promise);
+ return deferred.promise;
+ },
+
+ resolveString: function (client, stringGrip) {
+ let key = stringGrip.actor + ":getString";
+ let p = promises.get(key);
+ if (p) {
+ return p;
+ }
+
+ p = client.getString(stringGrip).then(result => {
+ promises.delete(key);
+ return result;
+ });
+
+ promises.set(key, p);
+ return p;
+ },
+};
+
+// Exports from this module
+module.exports = DataProvider;
diff --git a/devtools/client/webconsole/net/main.js b/devtools/client/webconsole/net/main.js
new file mode 100644
index 000000000..6fdf9494d
--- /dev/null
+++ b/devtools/client/webconsole/net/main.js
@@ -0,0 +1,98 @@
+/* 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";
+
+/* global BrowserLoader */
+
+var { utils: Cu } = Components;
+
+// Initialize module loader and load all modules of the new inline
+// preview feature. The entire code-base doesn't need any extra
+// privileges and runs entirely in content scope.
+const rootUrl = "resource://devtools/client/webconsole/net/";
+const require = BrowserLoader({
+ baseURI: rootUrl,
+ window}).require;
+
+const NetRequest = require("./net-request");
+const { loadSheet } = require("sdk/stylesheet/utils");
+
+// Localization
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper("devtools/client/locales/netmonitor.properties");
+
+// Stylesheets
+var styleSheets = [
+ "resource://devtools/client/jsonview/css/toolbar.css",
+ "resource://devtools/client/shared/components/tree/tree-view.css",
+ "resource://devtools/client/shared/components/reps/reps.css",
+ "resource://devtools/client/webconsole/net/net-request.css",
+ "resource://devtools/client/webconsole/net/components/size-limit.css",
+ "resource://devtools/client/webconsole/net/components/net-info-body.css",
+ "resource://devtools/client/webconsole/net/components/net-info-group.css",
+ "resource://devtools/client/webconsole/net/components/net-info-params.css",
+ "resource://devtools/client/webconsole/net/components/response-tab.css"
+];
+
+// Load theme stylesheets into the Console frame. This should be
+// done automatically by UI Components as soon as we have consensus
+// on the right CSS strategy FIXME.
+// It would also be nice to include them using @import.
+styleSheets.forEach(url => {
+ loadSheet(this, url, "author");
+});
+
+// Localization API used by React components
+// accessing strings from *.properties file.
+// Example:
+// let localizedString = Locale.$STR('string-key');
+//
+// Resources:
+// http://l20n.org/
+// https://github.com/yahoo/react-intl
+this.Locale = {
+ $STR: key => {
+ try {
+ return L10N.getStr(key);
+ } catch (err) {
+ console.error(key + ": " + err);
+ }
+ }
+};
+
+// List of NetRequest instances represents the state.
+// As soon as Redux is in place it should be maintained using a reducer.
+var netRequests = new Map();
+
+/**
+ * This function handles network events received from the backend. It's
+ * executed from within the webconsole.js
+ */
+function onNetworkEvent(log) {
+ // The 'from' field is set only in case of a 'networkEventUpdate' packet.
+ // The initial 'networkEvent' packet uses 'actor'.
+ // Check if NetRequest object is already created for this event actor and
+ // if there is none make sure to create one.
+ let response = log.response;
+ let netRequest = response.from ? netRequests.get(response.from) : null;
+ if (!netRequest && !log.update) {
+ netRequest = new NetRequest(log);
+ netRequests.set(response.actor, netRequest);
+ }
+
+ if (!netRequest) {
+ return;
+ }
+
+ if (log.update) {
+ netRequest.updateBody(response);
+ }
+
+ return;
+}
+
+// Make the 'onNetworkEvent' accessible from chrome (see webconsole.js)
+this.NetRequest = {
+ onNetworkEvent: onNetworkEvent
+};
diff --git a/devtools/client/webconsole/net/moz.build b/devtools/client/webconsole/net/moz.build
new file mode 100644
index 000000000..1b9eca7fe
--- /dev/null
+++ b/devtools/client/webconsole/net/moz.build
@@ -0,0 +1,19 @@
+# vim: set filetype=python:
+# 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/.
+
+DIRS += [
+ 'components',
+ 'utils'
+]
+
+DevToolsModules(
+ 'data-provider.js',
+ 'main.js',
+ 'net-request.css',
+ 'net-request.js',
+)
+
+XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
+BROWSER_CHROME_MANIFESTS += ['test/mochitest/browser.ini']
diff --git a/devtools/client/webconsole/net/net-request.css b/devtools/client/webconsole/net/net-request.css
new file mode 100644
index 000000000..82b6a027f
--- /dev/null
+++ b/devtools/client/webconsole/net/net-request.css
@@ -0,0 +1,35 @@
+ /* 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/. */
+
+/******************************************************************************/
+/* General */
+
+:root {
+ --net-border: #d7d7d7;
+}
+
+:root.theme-dark {
+ --net-border: #5f7387;
+}
+
+/******************************************************************************/
+/* Network log */
+
+/* No background if a Net log is opened */
+.netRequest.message.opened,
+.netRequest.message.opened:hover {
+ background: transparent !important;
+}
+
+/******************************************************************************/
+/* Themes */
+
+.theme-dark .netRequest.opened:hover,
+.theme-dark .netRequest.opened {
+ background: transparent;
+}
+
+.theme-firebug .netRequest.message.opened:hover {
+ background-image: linear-gradient(rgba(214, 233, 246, 0.8), rgba(255, 255, 255, 1.6)) !important;
+}
diff --git a/devtools/client/webconsole/net/net-request.js b/devtools/client/webconsole/net/net-request.js
new file mode 100644
index 000000000..48cf66fdd
--- /dev/null
+++ b/devtools/client/webconsole/net/net-request.js
@@ -0,0 +1,323 @@
+/* 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";
+
+// React
+const React = require("devtools/client/shared/vendor/react");
+const ReactDOM = require("devtools/client/shared/vendor/react-dom");
+
+// Reps
+const { parseURLParams } = require("devtools/client/shared/components/reps/rep-utils");
+
+// Network
+const { cancelEvent, isLeftClick } = require("./utils/events");
+const NetInfoBody = React.createFactory(require("./components/net-info-body"));
+const DataProvider = require("./data-provider");
+
+// Constants
+const XHTML_NS = "http://www.w3.org/1999/xhtml";
+
+/**
+ * This object represents a network log in the Console panel (and in the
+ * Network panel in the future).
+ * It's associated with an existing log and so, also with an existing
+ * element in the DOM.
+ *
+ * The object neither render no request for more data by default. It only
+ * reqisters a click listener to the associated log entry (a network event)
+ * and changes the class attribute of the log entry, so a twisty icon
+ * appears to indicates that there are more details displayed if the
+ * log entry is expanded.
+ *
+ * When the user expands the log, data are requested from the backend
+ * and rendered directly within the Console iframe.
+ */
+function NetRequest(log) {
+ this.initialize(log);
+}
+
+NetRequest.prototype = {
+ initialize: function (log) {
+ this.client = log.consoleFrame.webConsoleClient;
+ this.owner = log.consoleFrame.owner;
+
+ // 'this.file' field is following HAR spec.
+ // http://www.softwareishard.com/blog/har-12-spec/
+ this.file = log.response;
+ this.parentNode = log.node;
+ this.file.request.queryString = parseURLParams(this.file.request.url);
+ this.hasCookies = false;
+
+ // Map of fetched responses (to avoid unnecessary RDP round trip).
+ this.cachedResponses = new Map();
+
+ let doc = this.parentNode.ownerDocument;
+ let twisty = doc.createElementNS(XHTML_NS, "a");
+ twisty.className = "theme-twisty";
+ twisty.href = "#";
+
+ let messageBody = this.parentNode.querySelector(".message-body-wrapper");
+ this.parentNode.insertBefore(twisty, messageBody);
+ this.parentNode.setAttribute("collapsible", true);
+
+ this.parentNode.classList.add("netRequest");
+
+ // Register a click listener.
+ this.addClickListener();
+ },
+
+ addClickListener: function () {
+ // Add an event listener to toggle the expanded state when clicked.
+ // The event bubbling is canceled if the user clicks on the log
+ // itself (not on the expanded body), so opening of the default
+ // modal dialog is avoided.
+ this.parentNode.addEventListener("click", (event) => {
+ if (!isLeftClick(event)) {
+ return;
+ }
+
+ // Clicking on the toggle button or the method expands/collapses
+ // the body with HTTP details.
+ let classList = event.originalTarget.classList;
+ if (!(classList.contains("theme-twisty") ||
+ classList.contains("method"))) {
+ return;
+ }
+
+ // Alright, the user is clicking fine, let's open HTTP details!
+ this.onToggleBody(event);
+
+ // Avoid the default modal dialog
+ cancelEvent(event);
+ }, true);
+ },
+
+ onToggleBody: function (event) {
+ let target = event.currentTarget;
+ let logRow = target.closest(".netRequest");
+ logRow.classList.toggle("opened");
+
+ let twisty = this.parentNode.querySelector(".theme-twisty");
+ if (logRow.classList.contains("opened")) {
+ twisty.setAttribute("open", true);
+ } else {
+ twisty.removeAttribute("open");
+ }
+
+ let isOpen = logRow.classList.contains("opened");
+ if (isOpen) {
+ this.renderBody();
+ } else {
+ this.closeBody();
+ }
+ },
+
+ updateCookies: function(method, response) {
+ // TODO: This code will be part of a reducer.
+ let result;
+ if (response.cookies > 0 &&
+ ["requestCookies", "responseCookies"].includes(method)) {
+ this.hasCookies = true;
+ this.refresh();
+ }
+ },
+
+ /**
+ * Executed when 'networkEventUpdate' is received from the backend.
+ */
+ updateBody: function (response) {
+ // 'networkEventUpdate' event indicates that there are new data
+ // available on the backend. The following logic checks the response
+ // cache and if this data has been already requested before they
+ // need to be updated now (re-requested).
+ let method = response.updateType;
+ this.updateCookies(method, response);
+ if (this.cachedResponses.get(method)) {
+ this.cachedResponses.delete(method);
+ this.requestData(method);
+ }
+ },
+
+ /**
+ * Close network inline preview body.
+ */
+ closeBody: function () {
+ this.netInfoBodyBox.parentNode.removeChild(this.netInfoBodyBox);
+ },
+
+ /**
+ * Render network inline preview body.
+ */
+ renderBody: function () {
+ let messageBody = this.parentNode.querySelector(".message-body-wrapper");
+
+ // Create box for all markup rendered by ReactJS. Since we are
+ // rendering within webconsole.xul (i.e. XUL document) we need
+ // to explicitly specify XHTML namespace.
+ let doc = messageBody.ownerDocument;
+ this.netInfoBodyBox = doc.createElementNS(XHTML_NS, "div");
+ this.netInfoBodyBox.classList.add("netInfoBody");
+ messageBody.appendChild(this.netInfoBodyBox);
+
+ // As soon as Redux is in place state and actions will come from
+ // separate modules.
+ let body = NetInfoBody({
+ actions: this
+ });
+
+ // Render net info body!
+ this.body = ReactDOM.render(body, this.netInfoBodyBox);
+
+ this.refresh();
+ },
+
+ /**
+ * Render top level ReactJS component.
+ */
+ refresh: function () {
+ if (!this.netInfoBodyBox) {
+ return;
+ }
+
+ // TODO: As soon as Redux is in place there will be reducer
+ // computing a new state.
+ let newState = Object.assign({}, this.body.state, {
+ data: this.file,
+ hasCookies: this.hasCookies
+ });
+
+ this.body.setState(newState);
+ },
+
+ // Communication with the backend
+
+ requestData: function (method) {
+ // If the response has already been received bail out.
+ let response = this.cachedResponses.get(method);
+ if (response) {
+ return;
+ }
+
+ // Set an attribute indicating that this net log is waiting for
+ // data coming from the backend. Intended mainly for tests.
+ this.parentNode.setAttribute("loading", "true");
+
+ let actor = this.file.actor;
+ DataProvider.requestData(this.client, actor, method).then(args => {
+ this.cachedResponses.set(method, args);
+ this.onRequestData(method, args);
+
+ if (!DataProvider.hasPendingRequests()) {
+ this.parentNode.removeAttribute("loading");
+
+ // Fire an event indicating that all pending requests for
+ // data from the backend has finished. Intended for tests.
+ // Do it asynchronously so, it's done after all handlers
+ // for the current promise are executed.
+ setTimeout(() => {
+ let event = document.createEvent("Event");
+ event.initEvent("netlog-no-pending-requests", true, true);
+ this.parentNode.dispatchEvent(event);
+ });
+ }
+ });
+ },
+
+ onRequestData: function (method, response) {
+ // TODO: This code will be part of a reducer.
+ let result;
+ switch (method) {
+ case "requestHeaders":
+ result = this.onRequestHeaders(response);
+ break;
+ case "responseHeaders":
+ result = this.onResponseHeaders(response);
+ break;
+ case "requestCookies":
+ result = this.onRequestCookies(response);
+ break;
+ case "responseCookies":
+ result = this.onResponseCookies(response);
+ break;
+ case "responseContent":
+ result = this.onResponseContent(response);
+ break;
+ case "requestPostData":
+ result = this.onRequestPostData(response);
+ break;
+ }
+
+ result.then(() => {
+ this.refresh();
+ });
+ },
+
+ onRequestHeaders: function (response) {
+ this.file.request.headers = response.headers;
+
+ return this.resolveHeaders(this.file.request.headers);
+ },
+
+ onResponseHeaders: function (response) {
+ this.file.response.headers = response.headers;
+
+ return this.resolveHeaders(this.file.response.headers);
+ },
+
+ onResponseContent: function (response) {
+ let content = response.content;
+
+ for (let p in content) {
+ this.file.response.content[p] = content[p];
+ }
+
+ return Promise.resolve();
+ },
+
+ onRequestPostData: function (response) {
+ this.file.request.postData = response.postData;
+ return Promise.resolve();
+ },
+
+ onRequestCookies: function (response) {
+ this.file.request.cookies = response.cookies;
+ return this.resolveHeaders(this.file.request.cookies);
+ },
+
+ onResponseCookies: function (response) {
+ this.file.response.cookies = response.cookies;
+ return this.resolveHeaders(this.file.response.cookies);
+ },
+
+ onViewSourceInDebugger: function (frame) {
+ this.owner.viewSourceInDebugger(frame.source, frame.line);
+ },
+
+ resolveHeaders: function (headers) {
+ let promises = [];
+
+ for (let header of headers) {
+ if (typeof header.value == "object") {
+ promises.push(this.resolveString(header.value).then(value => {
+ header.value = value;
+ }));
+ }
+ }
+
+ return Promise.all(promises);
+ },
+
+ resolveString: function (object, propName) {
+ let stringGrip = object[propName];
+ if (typeof stringGrip == "object") {
+ DataProvider.resolveString(this.client, stringGrip).then(args => {
+ object[propName] = args;
+ this.refresh();
+ });
+ }
+ }
+};
+
+// Exports from this module
+module.exports = NetRequest;
diff --git a/devtools/client/webconsole/net/test/mochitest/.eslintrc.js b/devtools/client/webconsole/net/test/mochitest/.eslintrc.js
new file mode 100644
index 000000000..76904829d
--- /dev/null
+++ b/devtools/client/webconsole/net/test/mochitest/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../../../../.eslintrc.mochitests.js",
+};
diff --git a/devtools/client/webconsole/net/test/mochitest/browser.ini b/devtools/client/webconsole/net/test/mochitest/browser.ini
new file mode 100644
index 000000000..9414414c6
--- /dev/null
+++ b/devtools/client/webconsole/net/test/mochitest/browser.ini
@@ -0,0 +1,22 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ head.js
+ page_basic.html
+ test.json
+ test.json^headers^
+ test-cookies.json
+ test-cookies.json^headers^
+ test.txt
+ test.xml
+ test.xml^headers^
+ !/devtools/client/webconsole/test/head.js
+ !/devtools/client/framework/test/shared-head.js
+
+[browser_net_basic.js]
+[browser_net_cookies.js]
+[browser_net_headers.js]
+[browser_net_params.js]
+[browser_net_post.js]
+[browser_net_response.js]
diff --git a/devtools/client/webconsole/net/test/mochitest/browser_net_basic.js b/devtools/client/webconsole/net/test/mochitest/browser_net_basic.js
new file mode 100644
index 000000000..57273bec0
--- /dev/null
+++ b/devtools/client/webconsole/net/test/mochitest/browser_net_basic.js
@@ -0,0 +1,33 @@
+/* -*- 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 TEST_PAGE_URL = URL_ROOT + "page_basic.html";
+const JSON_XHR_URL = URL_ROOT + "test.json";
+
+/**
+ * Basic test that generates XHR in the content and
+ * checks the related log in the Console panel can
+ * be expanded.
+ */
+add_task(function* () {
+ info("Test XHR Spy basic started");
+
+ let {hud} = yield addTestTab(TEST_PAGE_URL);
+
+ let netInfoBody = yield executeAndInspectXhr(hud, {
+ method: "GET",
+ url: JSON_XHR_URL
+ });
+
+ ok(netInfoBody, "The network details must be available");
+
+ // There should be at least two tabs: Headers and Response
+ ok(netInfoBody.querySelector(".tabs .tabs-menu-item.headers"),
+ "Headers tab must be available");
+ ok(netInfoBody.querySelector(".tabs .tabs-menu-item.response"),
+ "Response tab must be available");
+});
diff --git a/devtools/client/webconsole/net/test/mochitest/browser_net_cookies.js b/devtools/client/webconsole/net/test/mochitest/browser_net_cookies.js
new file mode 100644
index 000000000..cfd85c2ed
--- /dev/null
+++ b/devtools/client/webconsole/net/test/mochitest/browser_net_cookies.js
@@ -0,0 +1,54 @@
+/* -*- 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 TEST_PAGE_URL = URL_ROOT + "page_basic.html";
+const JSON_XHR_URL = URL_ROOT + "test-cookies.json";
+
+/**
+ * This test generates XHR requests in the page, expands
+ * networks details in the Console panel and checks that
+ * Cookies are properly displayed.
+ */
+add_task(function* () {
+ info("Test XHR Spy cookies started");
+
+ let {hud} = yield addTestTab(TEST_PAGE_URL);
+
+ let netInfoBody = yield executeAndInspectXhr(hud, {
+ method: "GET",
+ url: JSON_XHR_URL
+ });
+
+ // Select "Cookies" tab
+ let tabBody = yield selectNetInfoTab(hud, netInfoBody, "cookies");
+
+ let requestCookieName = tabBody.querySelector(
+ ".netInfoGroup.requestCookies .netInfoParamName > span[title='bar']");
+
+ // Verify request cookies (name and value)
+ ok(requestCookieName, "Request Cookie name must exist");
+ is(requestCookieName.textContent, "bar",
+ "The cookie name must have proper value");
+
+ let requestCookieValue = requestCookieName.parentNode.nextSibling;
+ ok(requestCookieValue, "Request Cookie value must exist");
+ is(requestCookieValue.textContent, "foo",
+ "The cookie value must have proper value");
+
+ let responseCookieName = tabBody.querySelector(
+ ".netInfoGroup.responseCookies .netInfoParamName > span[title='test']");
+
+ // Verify response cookies (name and value)
+ ok(responseCookieName, "Response Cookie name must exist");
+ is(responseCookieName.textContent, "test",
+ "The cookie name must have proper value");
+
+ let responseCookieValue = responseCookieName.parentNode.nextSibling;
+ ok(responseCookieValue, "Response Cookie value must exist");
+ is(responseCookieValue.textContent, "abc",
+ "The cookie value must have proper value");
+});
diff --git a/devtools/client/webconsole/net/test/mochitest/browser_net_headers.js b/devtools/client/webconsole/net/test/mochitest/browser_net_headers.js
new file mode 100644
index 000000000..4a47074ee
--- /dev/null
+++ b/devtools/client/webconsole/net/test/mochitest/browser_net_headers.js
@@ -0,0 +1,40 @@
+/* -*- 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 TEST_PAGE_URL = URL_ROOT + "page_basic.html";
+const JSON_XHR_URL = URL_ROOT + "test.json";
+
+/**
+ * This test generates XHR requests in the page, expands
+ * networks details in the Console panel and checks that
+ * HTTP headers are there.
+ */
+add_task(function* () {
+ info("Test XHR Spy headers started");
+
+ let {hud} = yield addTestTab(TEST_PAGE_URL);
+
+ let netInfoBody = yield executeAndInspectXhr(hud, {
+ method: "GET",
+ url: JSON_XHR_URL
+ });
+
+ // Select "Headers" tab
+ let tabBody = yield selectNetInfoTab(hud, netInfoBody, "headers");
+ let paramName = tabBody.querySelector(
+ ".netInfoParamName > span[title='Content-Type']");
+
+ // Verify "Content-Type" header (name and value)
+ ok(paramName, "Header name must exist");
+ is(paramName.textContent, "Content-Type",
+ "The header name must have proper value");
+
+ let paramValue = paramName.parentNode.nextSibling;
+ ok(paramValue, "Header value must exist");
+ is(paramValue.textContent, "application/json; charset=utf-8",
+ "The header value must have proper value");
+});
diff --git a/devtools/client/webconsole/net/test/mochitest/browser_net_params.js b/devtools/client/webconsole/net/test/mochitest/browser_net_params.js
new file mode 100644
index 000000000..d8b0e2c84
--- /dev/null
+++ b/devtools/client/webconsole/net/test/mochitest/browser_net_params.js
@@ -0,0 +1,69 @@
+/* -*- 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 TEST_PAGE_URL = URL_ROOT + "page_basic.html";
+const JSON_XHR_URL = URL_ROOT + "test.json";
+
+/**
+ * This test generates XHR requests in the page, expands
+ * networks details in the Console panel and checks that
+ * HTTP parameters (query string) are there.
+ */
+add_task(function* () {
+ info("Test XHR Spy params started");
+
+ let {hud} = yield addTestTab(TEST_PAGE_URL);
+
+ let netInfoBody = yield executeAndInspectXhr(hud, {
+ method: "GET",
+ url: JSON_XHR_URL,
+ queryString: "?foo=bar"
+ });
+
+ // Check headers
+ let tabBody = yield selectNetInfoTab(hud, netInfoBody, "params");
+
+ let paramName = tabBody.querySelector(
+ ".netInfoParamName > span[title='foo']");
+
+ // Verify "Content-Type" header (name and value)
+ ok(paramName, "Header name must exist");
+ is(paramName.textContent, "foo",
+ "The param name must have proper value");
+
+ let paramValue = paramName.parentNode.nextSibling;
+ ok(paramValue, "param value must exist");
+ is(paramValue.textContent, "bar",
+ "The param value must have proper value");
+});
+
+/**
+ * Test URL parameters with the same name.
+ */
+add_task(function* () {
+ info("Test XHR Spy params started");
+
+ let {hud} = yield addTestTab(TEST_PAGE_URL);
+
+ let netInfoBody = yield executeAndInspectXhr(hud, {
+ method: "GET",
+ url: JSON_XHR_URL,
+ queryString: "?box[]=123&box[]=456"
+ });
+
+ // Check headers
+ let tabBody = yield selectNetInfoTab(hud, netInfoBody, "params");
+
+ let params = tabBody.querySelectorAll(
+ ".netInfoParamName > span[title='box[]']");
+ is(params.length, 2, "Two URI parameters must exist");
+
+ let values = tabBody.querySelectorAll(
+ ".netInfoParamValue > code");
+ is(values[0].textContent, 123, "First value must match");
+ is(values[1].textContent, 456, "Second value must match");
+});
diff --git a/devtools/client/webconsole/net/test/mochitest/browser_net_post.js b/devtools/client/webconsole/net/test/mochitest/browser_net_post.js
new file mode 100644
index 000000000..f6e776ef0
--- /dev/null
+++ b/devtools/client/webconsole/net/test/mochitest/browser_net_post.js
@@ -0,0 +1,88 @@
+/* -*- 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 TEST_PAGE_URL = URL_ROOT + "page_basic.html";
+const JSON_XHR_URL = URL_ROOT + "test.json";
+
+const plainPostBody = "test-data";
+const jsonData = "{\"bar\": \"baz\"}";
+const jsonRendered = "bar\"baz\"";
+const xmlPostBody = "<xml><name>John</name></xml>";
+
+/**
+ * This test generates XHR requests in the page, expands
+ * networks details in the Console panel and checks that
+ * Post data are properly rendered.
+ */
+add_task(function* () {
+ info("Test XHR Spy post plain body started");
+
+ let {hud} = yield addTestTab(TEST_PAGE_URL);
+
+ let netInfoBody = yield executeAndInspectXhr(hud, {
+ method: "POST",
+ url: JSON_XHR_URL,
+ body: plainPostBody
+ });
+
+ // Check post body data
+ let tabBody = yield selectNetInfoTab(hud, netInfoBody, "post");
+ let postContent = tabBody.querySelector(
+ ".netInfoGroup.raw.opened .netInfoGroupContent");
+ is(postContent.textContent, plainPostBody,
+ "Post body must be properly rendered");
+});
+
+add_task(function* () {
+ info("Test XHR Spy post JSON body started");
+
+ let {hud} = yield addTestTab(TEST_PAGE_URL);
+
+ let netInfoBody = yield executeAndInspectXhr(hud, {
+ method: "POST",
+ url: JSON_XHR_URL,
+ body: jsonData,
+ requestHeaders: [{
+ name: "Content-Type",
+ value: "application/json"
+ }]
+ });
+
+ // Check post body data
+ let tabBody = yield selectNetInfoTab(hud, netInfoBody, "post");
+ let postContent = tabBody.querySelector(
+ ".netInfoGroup.json.opened .netInfoGroupContent");
+ is(postContent.textContent, jsonRendered,
+ "Post body must be properly rendered");
+
+ let rawPostContent = tabBody.querySelector(
+ ".netInfoGroup.raw.opened .netInfoGroupContent");
+ ok(!rawPostContent, "Raw response group must be collapsed");
+});
+
+add_task(function* () {
+ info("Test XHR Spy post XML body started");
+
+ let {hud} = yield addTestTab(TEST_PAGE_URL);
+
+ let netInfoBody = yield executeAndInspectXhr(hud, {
+ method: "POST",
+ url: JSON_XHR_URL,
+ body: xmlPostBody,
+ requestHeaders: [{
+ name: "Content-Type",
+ value: "application/xml"
+ }]
+ });
+
+ // Check post body data
+ let tabBody = yield selectNetInfoTab(hud, netInfoBody, "post");
+ let rawPostContent = tabBody.querySelector(
+ ".netInfoGroup.raw.opened .netInfoGroupContent");
+ is(rawPostContent.textContent, xmlPostBody,
+ "Raw response group must not be collapsed");
+});
diff --git a/devtools/client/webconsole/net/test/mochitest/browser_net_response.js b/devtools/client/webconsole/net/test/mochitest/browser_net_response.js
new file mode 100644
index 000000000..ec5543043
--- /dev/null
+++ b/devtools/client/webconsole/net/test/mochitest/browser_net_response.js
@@ -0,0 +1,86 @@
+/* -*- 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 TEST_PAGE_URL = URL_ROOT + "page_basic.html";
+const TEXT_XHR_URL = URL_ROOT + "test.txt";
+const JSON_XHR_URL = URL_ROOT + "test.json";
+const XML_XHR_URL = URL_ROOT + "test.xml";
+
+const textResponseBody = "this is a response";
+const jsonResponseBody = "name\"John\"";
+
+// Individual tests below generate XHR request in the page, expand
+// network details in the Console panel and checks various types
+// of response bodies.
+
+/**
+ * Validate plain text response
+ */
+add_task(function* () {
+ info("Test XHR Spy respone plain body started");
+
+ let {hud} = yield addTestTab(TEST_PAGE_URL);
+
+ let netInfoBody = yield executeAndInspectXhr(hud, {
+ method: "GET",
+ url: TEXT_XHR_URL,
+ });
+
+ // Check response body data
+ let tabBody = yield selectNetInfoTab(hud, netInfoBody, "response");
+ let responseContent = tabBody.querySelector(
+ ".netInfoGroup.raw.opened .netInfoGroupContent");
+
+ ok(responseContent.textContent.indexOf(textResponseBody) > -1,
+ "Response body must be properly rendered");
+});
+
+/**
+ * Validate XML response
+ */
+add_task(function* () {
+ info("Test XHR Spy response XML body started");
+
+ let {hud} = yield addTestTab(TEST_PAGE_URL);
+
+ let netInfoBody = yield executeAndInspectXhr(hud, {
+ method: "GET",
+ url: XML_XHR_URL,
+ });
+
+ // Check response body data
+ let tabBody = yield selectNetInfoTab(hud, netInfoBody, "response");
+ let rawResponseContent = tabBody.querySelector(
+ ".netInfoGroup.raw.opened .netInfoGroupContent");
+ ok(rawResponseContent, "Raw response group must not be collapsed");
+});
+
+/**
+ * Validate JSON response
+ */
+add_task(function* () {
+ info("Test XHR Spy response JSON body started");
+
+ let {hud} = yield addTestTab(TEST_PAGE_URL);
+
+ let netInfoBody = yield executeAndInspectXhr(hud, {
+ method: "GET",
+ url: JSON_XHR_URL,
+ });
+
+ // Check response body data
+ let tabBody = yield selectNetInfoTab(hud, netInfoBody, "response");
+ let responseContent = tabBody.querySelector(
+ ".netInfoGroup.json .netInfoGroupContent");
+
+ is(responseContent.textContent, jsonResponseBody,
+ "Response body must be properly rendered");
+
+ let rawResponseContent = tabBody.querySelector(
+ ".netInfoGroup.raw.opened .netInfoGroupContent");
+ ok(!rawResponseContent, "Raw response group must be collapsed");
+});
diff --git a/devtools/client/webconsole/net/test/mochitest/head.js b/devtools/client/webconsole/net/test/mochitest/head.js
new file mode 100644
index 000000000..c01206948
--- /dev/null
+++ b/devtools/client/webconsole/net/test/mochitest/head.js
@@ -0,0 +1,209 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint no-unused-vars: [2, {"vars": "local", "args": "none"}] */
+/* import-globals-from ../../../test/head.js */
+
+"use strict";
+
+// Load Web Console head.js, it implements helper console test API
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/webconsole/test/head.js", this);
+
+const FRAME_SCRIPT_UTILS_URL =
+ "chrome://devtools/content/shared/frame-script-utils.js";
+
+const NET_INFO_PREF = "devtools.webconsole.filter.networkinfo";
+const NET_XHR_PREF = "devtools.webconsole.filter.netxhr";
+
+// Enable XHR logging for the test
+Services.prefs.setBoolPref(NET_INFO_PREF, true);
+Services.prefs.setBoolPref(NET_XHR_PREF, true);
+
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref(NET_INFO_PREF, true);
+ Services.prefs.clearUserPref(NET_XHR_PREF, true);
+});
+
+// Use the old webconsole since the new one doesn't yet support
+// XHR spy. See Bug 1304794.
+Services.prefs.setBoolPref("devtools.webconsole.new-frontend-enabled", false);
+registerCleanupFunction(function* () {
+ Services.prefs.clearUserPref("devtools.webconsole.new-frontend-enabled");
+});
+
+/**
+ * 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 tab object when the url is loaded
+ */
+function addTestTab(url) {
+ info("Adding a new JSON tab with URL: '" + url + "'");
+
+ return Task.spawn(function* () {
+ let tab = yield addTab(url);
+
+ // Load devtools/shared/frame-script-utils.js
+ loadCommonFrameScript(tab);
+
+ // Open the Console panel
+ let hud = yield openConsole();
+
+ return {
+ tab: tab,
+ browser: tab.linkedBrowser,
+ hud: hud
+ };
+ });
+}
+
+/**
+ *
+ * @param hud
+ * @param options
+ */
+function executeAndInspectXhr(hud, options) {
+ hud.jsterm.clearOutput();
+
+ options.queryString = options.queryString || "";
+
+ // Execute XHR in the content scope.
+ performRequestsInContent({
+ method: options.method,
+ url: options.url + options.queryString,
+ body: options.body,
+ nocache: options.nocache,
+ requestHeaders: options.requestHeaders
+ });
+
+ return Task.spawn(function* () {
+ // Wait till the appropriate Net log appears in the Console panel.
+ let rules = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: options.url,
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_INFO,
+ isXhr: true,
+ }]
+ });
+
+ // The log is here, get its parent element (className: 'message').
+ let msg = [...rules[0].matched][0];
+ let body = msg.querySelector(".message-body");
+
+ // Open XHR HTTP details body and wait till the UI fetches
+ // all necessary data from the backend. All RPD requests
+ // needs to be finished before we can continue testing.
+ yield synthesizeMouseClickSoon(hud, body);
+ yield waitForBackend(msg);
+ let netInfoBody = body.querySelector(".netInfoBody");
+ ok(netInfoBody, "Net info body must exist");
+ return netInfoBody;
+ });
+}
+
+/**
+ * Wait till XHR data are fetched from the backend (i.e. there are
+ * no pending RDP requests.
+ */
+function waitForBackend(element) {
+ if (!element.hasAttribute("loading")) {
+ return;
+ }
+ return once(element, "netlog-no-pending-requests", true);
+}
+
+/**
+ * Select specific tab in XHR info body.
+ *
+ * @param netInfoBody The main XHR info body
+ * @param tabId Tab ID (possible values: 'headers', 'cookies', 'params',
+ * 'post', 'response');
+ *
+ * @returns Tab body element.
+ */
+function selectNetInfoTab(hud, netInfoBody, tabId) {
+ let tab = netInfoBody.querySelector(".tabs-menu-item." + tabId);
+ ok(tab, "Tab must exist " + tabId);
+
+ // Click to select specified tab and wait till its
+ // UI is populated with data from the backend.
+ // There must be no pending RDP requests before we can
+ // continue testing the UI.
+ return Task.spawn(function* () {
+ yield synthesizeMouseClickSoon(hud, tab);
+ let msg = getAncestorByClass(netInfoBody, "message");
+ yield waitForBackend(msg);
+ let tabBody = netInfoBody.querySelector("." + tabId + "TabBox");
+ ok(tabBody, "Tab body must exist");
+ return tabBody;
+ });
+}
+
+/**
+ * Return parent node with specified class.
+ *
+ * @param node A child element
+ * @param className Specified class name.
+ *
+ * @returns A parent element.
+ */
+function getAncestorByClass(node, className) {
+ for (let parent = node; parent; parent = parent.parentNode) {
+ if (parent.classList && parent.classList.contains(className)) {
+ return parent;
+ }
+ }
+ return null;
+}
+
+/**
+ * Synthesize asynchronous click event (with clean stack trace).
+ */
+function synthesizeMouseClickSoon(hud, element) {
+ return new Promise((resolve) => {
+ executeSoon(() => {
+ EventUtils.synthesizeMouse(element, 2, 2, {}, hud.iframeWindow);
+ resolve();
+ });
+ });
+}
+
+/**
+ * Execute XHR in the content scope.
+ */
+function performRequestsInContent(requests) {
+ info("Performing requests in the context of the content.");
+ return executeInContent("devtools:test:xhr", requests);
+}
+
+function executeInContent(name, data = {}, objects = {},
+ expectResponse = true) {
+ let mm = gBrowser.selectedBrowser.messageManager;
+
+ mm.sendAsyncMessage(name, data, objects);
+ if (expectResponse) {
+ return waitForContentMessage(name);
+ }
+
+ return Promise.resolve();
+}
+
+function waitForContentMessage(name) {
+ info("Expecting message " + name + " from content");
+
+ let mm = gBrowser.selectedBrowser.messageManager;
+
+ return new Promise((resolve) => {
+ mm.addMessageListener(name, function onMessage(msg) {
+ mm.removeMessageListener(name, onMessage);
+ resolve(msg.data);
+ });
+ });
+}
+
+function loadCommonFrameScript(tab) {
+ let browser = tab ? tab.linkedBrowser : gBrowser.selectedBrowser;
+ browser.messageManager.loadFrameScript(FRAME_SCRIPT_UTILS_URL, false);
+}
diff --git a/devtools/client/webconsole/net/test/mochitest/page_basic.html b/devtools/client/webconsole/net/test/mochitest/page_basic.html
new file mode 100644
index 000000000..da7158492
--- /dev/null
+++ b/devtools/client/webconsole/net/test/mochitest/page_basic.html
@@ -0,0 +1,14 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>XHR Spy test page</title>
+ </head>
+ <body>
+ <script type="text/javascript">
+ document.cookie = "bar=foo";
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/net/test/mochitest/test-cookies.json b/devtools/client/webconsole/net/test/mochitest/test-cookies.json
new file mode 100644
index 000000000..b5e739025
--- /dev/null
+++ b/devtools/client/webconsole/net/test/mochitest/test-cookies.json
@@ -0,0 +1 @@
+{"name":"Cookies Test"}
diff --git a/devtools/client/webconsole/net/test/mochitest/test-cookies.json^headers^ b/devtools/client/webconsole/net/test/mochitest/test-cookies.json^headers^
new file mode 100644
index 000000000..94a8c0c69
--- /dev/null
+++ b/devtools/client/webconsole/net/test/mochitest/test-cookies.json^headers^
@@ -0,0 +1,2 @@
+Content-Type: application/json; charset=utf-8
+Set-Cookie: test=abc
diff --git a/devtools/client/webconsole/net/test/mochitest/test.json b/devtools/client/webconsole/net/test/mochitest/test.json
new file mode 100644
index 000000000..6548f8e3e
--- /dev/null
+++ b/devtools/client/webconsole/net/test/mochitest/test.json
@@ -0,0 +1 @@
+{"name":"John"}
diff --git a/devtools/client/webconsole/net/test/mochitest/test.json^headers^ b/devtools/client/webconsole/net/test/mochitest/test.json^headers^
new file mode 100644
index 000000000..6010bfd18
--- /dev/null
+++ b/devtools/client/webconsole/net/test/mochitest/test.json^headers^
@@ -0,0 +1 @@
+Content-Type: application/json; charset=utf-8
diff --git a/devtools/client/webconsole/net/test/mochitest/test.txt b/devtools/client/webconsole/net/test/mochitest/test.txt
new file mode 100644
index 000000000..af7014e11
--- /dev/null
+++ b/devtools/client/webconsole/net/test/mochitest/test.txt
@@ -0,0 +1 @@
+this is a response
diff --git a/devtools/client/webconsole/net/test/mochitest/test.xml b/devtools/client/webconsole/net/test/mochitest/test.xml
new file mode 100644
index 000000000..3749c8e5a
--- /dev/null
+++ b/devtools/client/webconsole/net/test/mochitest/test.xml
@@ -0,0 +1 @@
+<xml><name>John</name></xml>
diff --git a/devtools/client/webconsole/net/test/mochitest/test.xml^headers^ b/devtools/client/webconsole/net/test/mochitest/test.xml^headers^
new file mode 100644
index 000000000..10ecdf5f4
--- /dev/null
+++ b/devtools/client/webconsole/net/test/mochitest/test.xml^headers^
@@ -0,0 +1 @@
+Content-Type: application/xml; charset=utf-8
diff --git a/devtools/client/webconsole/net/test/unit/.eslintrc.js b/devtools/client/webconsole/net/test/unit/.eslintrc.js
new file mode 100644
index 000000000..54a9a6361
--- /dev/null
+++ b/devtools/client/webconsole/net/test/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/client/webconsole/net/test/unit/test_json-utils.js b/devtools/client/webconsole/net/test/unit/test_json-utils.js
new file mode 100644
index 000000000..f8ccdf3aa
--- /dev/null
+++ b/devtools/client/webconsole/net/test/unit/test_json-utils.js
@@ -0,0 +1,45 @@
+/* -*- 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";
+
+var Cu = Components.utils;
+const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const { parseJSONString, isJSON } = require("devtools/client/webconsole/net/utils/json");
+
+// Test data
+const simpleJson = '{"name":"John"}';
+const jsonInFunc = 'someFunc({"name":"John"})';
+
+const json1 = "{'a': 1}";
+const json2 = " {'a': 1}";
+const json3 = "\t {'a': 1}";
+const json4 = "\n\n\t {'a': 1}";
+const json5 = "\n\n\t ";
+
+const textMimeType = "text/plain";
+const jsonMimeType = "text/javascript";
+const unknownMimeType = "text/unknown";
+
+/**
+ * Testing API provided by webconsole/net/utils/json.js
+ */
+function run_test() {
+ // parseJSONString
+ equal(parseJSONString(simpleJson).name, "John");
+ equal(parseJSONString(jsonInFunc).name, "John");
+
+ // isJSON
+ equal(isJSON(textMimeType, json1), true);
+ equal(isJSON(textMimeType, json2), true);
+ equal(isJSON(jsonMimeType, json3), true);
+ equal(isJSON(jsonMimeType, json4), true);
+
+ equal(isJSON(unknownMimeType, json1), true);
+ equal(isJSON(textMimeType, json1), true);
+
+ equal(isJSON(unknownMimeType), false);
+ equal(isJSON(unknownMimeType, json5), false);
+}
diff --git a/devtools/client/webconsole/net/test/unit/test_net-utils.js b/devtools/client/webconsole/net/test/unit/test_net-utils.js
new file mode 100644
index 000000000..512ebcbc7
--- /dev/null
+++ b/devtools/client/webconsole/net/test/unit/test_net-utils.js
@@ -0,0 +1,77 @@
+/* -*- 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";
+
+var Cu = Components.utils;
+const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const {
+ isImage,
+ isHTML,
+ getHeaderValue,
+ isURLEncodedRequest,
+ isMultiPartRequest
+} = require("devtools/client/webconsole/net/utils/net");
+
+// Test data
+const imageMimeTypes = ["image/jpeg", "image/jpg", "image/gif",
+ "image/png", "image/bmp"];
+
+const htmlMimeTypes = ["text/html", "text/xml", "application/xml",
+ "application/rss+xml", "application/atom+xml", "application/xhtml+xml",
+ "application/mathml+xml", "application/rdf+xml"];
+
+const headers = [{name: "headerName", value: "value1"}];
+
+const har1 = {
+ request: {
+ postData: {
+ text: "content-type: application/x-www-form-urlencoded"
+ }
+ }
+};
+
+const har2 = {
+ request: {
+ headers: [{
+ name: "content-type",
+ value: "application/x-www-form-urlencoded"
+ }]
+ }
+};
+
+const har3 = {
+ request: {
+ headers: [{
+ name: "content-type",
+ value: "multipart/form-data"
+ }]
+ }
+};
+
+/**
+ * Testing API provided by webconsole/net/utils/net.js
+ */
+function run_test() {
+ // isImage
+ imageMimeTypes.forEach(mimeType => {
+ ok(isImage(mimeType));
+ });
+
+ // isHTML
+ htmlMimeTypes.forEach(mimeType => {
+ ok(isHTML(mimeType));
+ });
+
+ // getHeaderValue
+ equal(getHeaderValue(headers, "headerName"), "value1");
+
+ // isURLEncodedRequest
+ ok(isURLEncodedRequest(har1));
+ ok(isURLEncodedRequest(har2));
+
+ // isMultiPartRequest
+ ok(isMultiPartRequest(har3));
+}
diff --git a/devtools/client/webconsole/net/test/unit/xpcshell.ini b/devtools/client/webconsole/net/test/unit/xpcshell.ini
new file mode 100644
index 000000000..d988a2ad0
--- /dev/null
+++ b/devtools/client/webconsole/net/test/unit/xpcshell.ini
@@ -0,0 +1,9 @@
+[DEFAULT]
+tags = devtools
+head =
+tail =
+firefox-appdir = browser
+skip-if = toolkit == 'android'
+
+[test_json-utils.js]
+[test_net-utils.js]
diff --git a/devtools/client/webconsole/net/utils/events.js b/devtools/client/webconsole/net/utils/events.js
new file mode 100644
index 000000000..9f8705593
--- /dev/null
+++ b/devtools/client/webconsole/net/utils/events.js
@@ -0,0 +1,21 @@
+/* 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";
+
+function isLeftClick(event, allowKeyModifiers) {
+ return event.button === 0 && (allowKeyModifiers || noKeyModifiers(event));
+}
+
+function noKeyModifiers(event) {
+ return !event.ctrlKey && !event.shiftKey && !event.altKey && !event.metaKey;
+}
+
+function cancelEvent(event) {
+ event.stopPropagation();
+ event.preventDefault();
+}
+
+// Exports from this module
+exports.isLeftClick = isLeftClick;
+exports.cancelEvent = cancelEvent;
diff --git a/devtools/client/webconsole/net/utils/json.js b/devtools/client/webconsole/net/utils/json.js
new file mode 100644
index 000000000..70d733f28
--- /dev/null
+++ b/devtools/client/webconsole/net/utils/json.js
@@ -0,0 +1,234 @@
+/* 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";
+
+// List of JSON content types.
+const contentTypes = {
+ "text/plain": 1,
+ "text/javascript": 1,
+ "text/x-javascript": 1,
+ "text/json": 1,
+ "text/x-json": 1,
+ "application/json": 1,
+ "application/x-json": 1,
+ "application/javascript": 1,
+ "application/x-javascript": 1,
+ "application/json-rpc": 1
+};
+
+// Implementation
+var Json = {};
+
+/**
+ * Parsing JSON
+ */
+Json.parseJSONString = function (jsonString) {
+ if (!jsonString.length) {
+ return null;
+ }
+
+ let regex, matches;
+
+ let first = firstNonWs(jsonString);
+ if (first !== "[" && first !== "{") {
+ // This (probably) isn't pure JSON. Let's try to strip various sorts
+ // of XSSI protection/wrapping and see if that works better.
+
+ // Prototype-style secure requests
+ regex = /^\s*\/\*-secure-([\s\S]*)\*\/\s*$/;
+ matches = regex.exec(jsonString);
+ if (matches) {
+ jsonString = matches[1];
+
+ if (jsonString[0] === "\\" && jsonString[1] === "n") {
+ jsonString = jsonString.substr(2);
+ }
+
+ if (jsonString[jsonString.length - 2] === "\\" &&
+ jsonString[jsonString.length - 1] === "n") {
+ jsonString = jsonString.substr(0, jsonString.length - 2);
+ }
+ }
+
+ // Google-style (?) delimiters
+ if (jsonString.indexOf("&&&START&&&") !== -1) {
+ regex = /&&&START&&&([\s\S]*)&&&END&&&/;
+ matches = regex.exec(jsonString);
+ if (matches) {
+ jsonString = matches[1];
+ }
+ }
+
+ // while(1);, for(;;);, and )]}'
+ regex = /^\s*(\)\]\}[^\n]*\n|while\s*\(1\);|for\s*\(;;\);)([\s\S]*)/;
+ matches = regex.exec(jsonString);
+ if (matches) {
+ jsonString = matches[2];
+ }
+
+ // JSONP
+ regex = /^\s*([A-Za-z0-9_$.]+\s*(?:\[.*\]|))\s*\(([\s\S]*)\)/;
+ matches = regex.exec(jsonString);
+ if (matches) {
+ jsonString = matches[2];
+ }
+ }
+
+ try {
+ return JSON.parse(jsonString);
+ } catch (err) {
+ // eslint-disable-line no-empty
+ }
+
+ // Give up if we don't have valid start, to avoid some unnecessary overhead.
+ first = firstNonWs(jsonString);
+ if (first !== "[" && first !== "{" && isNaN(first) && first !== '"') {
+ return null;
+ }
+
+ // Remove JavaScript comments, quote non-quoted identifiers, and merge
+ // multi-line structures like |{"a": 1} \n {"b": 2}| into a single JSON
+ // object [{"a": 1}, {"b": 2}].
+ jsonString = pseudoJsonToJson(jsonString);
+
+ try {
+ return JSON.parse(jsonString);
+ } catch (err) {
+ // eslint-disable-line no-empty
+ }
+
+ return null;
+};
+
+function firstNonWs(str) {
+ for (let i = 0, len = str.length; i < len; i++) {
+ let ch = str[i];
+ if (ch !== " " && ch !== "\n" && ch !== "\t" && ch !== "\r") {
+ return ch;
+ }
+ }
+ return "";
+}
+
+function pseudoJsonToJson(json) {
+ let ret = "";
+ let at = 0, lasti = 0, lastch = "", hasMultipleParts = false;
+ for (let i = 0, len = json.length; i < len; ++i) {
+ let ch = json[i];
+ if (/\s/.test(ch)) {
+ continue;
+ }
+
+ if (ch === '"') {
+ // Consume a string.
+ ++i;
+ while (i < len) {
+ if (json[i] === "\\") {
+ ++i;
+ } else if (json[i] === '"') {
+ break;
+ }
+ ++i;
+ }
+ } else if (ch === "'") {
+ // Convert an invalid string into a valid one.
+ ret += json.slice(at, i) + "\"";
+ at = i + 1;
+ ++i;
+
+ while (i < len) {
+ if (json[i] === "\\") {
+ ++i;
+ } else if (json[i] === "'") {
+ break;
+ }
+ ++i;
+ }
+
+ if (i < len) {
+ ret += json.slice(at, i) + "\"";
+ at = i + 1;
+ }
+ } else if ((ch === "[" || ch === "{") &&
+ (lastch === "]" || lastch === "}")) {
+ // Multiple JSON messages in one... Make it into a single array by
+ // inserting a comma and setting the "multiple parts" flag.
+ ret += json.slice(at, i) + ",";
+ hasMultipleParts = true;
+ at = i;
+ } else if (lastch === "," && (ch === "]" || ch === "}")) {
+ // Trailing commas in arrays/objects.
+ ret += json.slice(at, lasti);
+ at = i;
+ } else if (lastch === "/" && lasti === i - 1) {
+ // Some kind of comment; remove it.
+ if (ch === "/") {
+ ret += json.slice(at, i - 1);
+ at = i + json.slice(i).search(/\n|\r|$/);
+ i = at - 1;
+ } else if (ch === "*") {
+ ret += json.slice(at, i - 1);
+ at = json.indexOf("*/", i + 1) + 2;
+ if (at === 1) {
+ at = len;
+ }
+ i = at - 1;
+ }
+ ch = "\0";
+ } else if (/[a-zA-Z$_]/.test(ch) && lastch !== ":") {
+ // Non-quoted identifier. Quote it.
+ ret += json.slice(at, i) + "\"";
+ at = i;
+ i = i + json.slice(i).search(/[^a-zA-Z0-9$_]|$/);
+ ret += json.slice(at, i) + "\"";
+ at = i;
+ }
+
+ lastch = ch;
+ lasti = i;
+ }
+
+ ret += json.slice(at);
+ if (hasMultipleParts) {
+ ret = "[" + ret + "]";
+ }
+
+ return ret;
+}
+
+Json.isJSON = function (contentType, data) {
+ // Workaround for JSON responses without proper content type
+ // Let's consider all responses starting with "{" as JSON. In the worst
+ // case there will be an exception when parsing. This means that no-JSON
+ // responses (and post data) (with "{") can be parsed unnecessarily,
+ // which represents a little overhead, but this happens only if the request
+ // is actually expanded by the user in the UI (Net & Console panels).
+ // Do a manual string search instead of checking (data.strip()[0] === "{")
+ // to improve performance/memory usage.
+ let len = data ? data.length : 0;
+ for (let i = 0; i < len; i++) {
+ let ch = data.charAt(i);
+ if (ch === "{") {
+ return true;
+ }
+
+ if (ch === " " || ch === "\t" || ch === "\n" || ch === "\r") {
+ continue;
+ }
+
+ break;
+ }
+
+ if (!contentType) {
+ return false;
+ }
+
+ contentType = contentType.split(";")[0];
+ contentType = contentType.trim();
+ return !!contentTypes[contentType];
+};
+
+// Exports from this module
+module.exports = Json;
+
diff --git a/devtools/client/webconsole/net/utils/moz.build b/devtools/client/webconsole/net/utils/moz.build
new file mode 100644
index 000000000..3fdc458e3
--- /dev/null
+++ b/devtools/client/webconsole/net/utils/moz.build
@@ -0,0 +1,11 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'events.js',
+ 'json.js',
+ 'net.js',
+)
diff --git a/devtools/client/webconsole/net/utils/net.js b/devtools/client/webconsole/net/utils/net.js
new file mode 100644
index 000000000..782ec032a
--- /dev/null
+++ b/devtools/client/webconsole/net/utils/net.js
@@ -0,0 +1,134 @@
+/* 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 mimeCategoryMap = {
+ "text/plain": "txt",
+ "application/octet-stream": "bin",
+ "text/html": "html",
+ "text/xml": "html",
+ "application/xml": "html",
+ "application/rss+xml": "html",
+ "application/atom+xml": "html",
+ "application/xhtml+xml": "html",
+ "application/mathml+xml": "html",
+ "application/rdf+xml": "html",
+ "text/css": "css",
+ "application/x-javascript": "js",
+ "text/javascript": "js",
+ "application/javascript": "js",
+ "text/ecmascript": "js",
+ "application/ecmascript": "js",
+ "image/jpeg": "image",
+ "image/jpg": "image",
+ "image/gif": "image",
+ "image/png": "image",
+ "image/bmp": "image",
+ "application/x-shockwave-flash": "plugin",
+ "application/x-silverlight-app": "plugin",
+ "video/x-flv": "media",
+ "audio/mpeg3": "media",
+ "audio/x-mpeg-3": "media",
+ "video/mpeg": "media",
+ "video/x-mpeg": "media",
+ "video/webm": "media",
+ "video/mp4": "media",
+ "video/ogg": "media",
+ "audio/ogg": "media",
+ "application/ogg": "media",
+ "application/x-ogg": "media",
+ "application/x-midi": "media",
+ "audio/midi": "media",
+ "audio/x-mid": "media",
+ "audio/x-midi": "media",
+ "music/crescendo": "media",
+ "audio/wav": "media",
+ "audio/x-wav": "media",
+ "application/x-woff": "font",
+ "application/font-woff": "font",
+ "application/x-font-woff": "font",
+ "application/x-ttf": "font",
+ "application/x-font-ttf": "font",
+ "font/ttf": "font",
+ "font/woff": "font",
+ "application/x-otf": "font",
+ "application/x-font-otf": "font"
+};
+
+var NetUtils = {};
+
+NetUtils.isImage = function (contentType) {
+ if (!contentType) {
+ return false;
+ }
+
+ contentType = contentType.split(";")[0];
+ contentType = contentType.trim();
+ return mimeCategoryMap[contentType] == "image";
+};
+
+NetUtils.isHTML = function (contentType) {
+ if (!contentType) {
+ return false;
+ }
+
+ contentType = contentType.split(";")[0];
+ contentType = contentType.trim();
+ return mimeCategoryMap[contentType] == "html";
+};
+
+NetUtils.getHeaderValue = function (headers, name) {
+ if (!headers) {
+ return null;
+ }
+
+ name = name.toLowerCase();
+ for (let i = 0; i < headers.length; ++i) {
+ let headerName = headers[i].name.toLowerCase();
+ if (headerName == name) {
+ return headers[i].value;
+ }
+ }
+};
+
+NetUtils.parseXml = function (content) {
+ let contentType = content.mimeType.split(";")[0];
+ contentType = contentType.trim();
+
+ let parser = new DOMParser();
+ let doc = parser.parseFromString(content.text, contentType);
+ let root = doc.documentElement;
+
+ // Error handling
+ let nsURI = "http://www.mozilla.org/newlayout/xml/parsererror.xml";
+ if (root.namespaceURI == nsURI && root.nodeName == "parsererror") {
+ return null;
+ }
+
+ return doc;
+};
+
+NetUtils.isURLEncodedRequest = function (file) {
+ let mimeType = "application/x-www-form-urlencoded";
+
+ let postData = file.request.postData;
+ if (postData && postData.text) {
+ let text = postData.text.toLowerCase();
+ if (text.startsWith("content-type: " + mimeType)) {
+ return true;
+ }
+ }
+
+ let value = NetUtils.getHeaderValue(file.request.headers, "content-type");
+ return value && value.startsWith(mimeType);
+};
+
+NetUtils.isMultiPartRequest = function (file) {
+ let mimeType = "multipart/form-data";
+ let value = NetUtils.getHeaderValue(file.request.headers, "content-type");
+ return value && value.startsWith(mimeType);
+};
+
+// Exports from this module
+module.exports = NetUtils;
diff --git a/devtools/client/webconsole/new-console-output/actions/enhancers.js b/devtools/client/webconsole/new-console-output/actions/enhancers.js
new file mode 100644
index 000000000..5553942e2
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/actions/enhancers.js
@@ -0,0 +1,20 @@
+/* -*- 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 { BATCH_ACTIONS } = require("../constants");
+
+function batchActions(batchedActions) {
+ return {
+ type: BATCH_ACTIONS,
+ actions: batchedActions,
+ };
+}
+
+module.exports = {
+ batchActions
+};
diff --git a/devtools/client/webconsole/new-console-output/actions/filters.js b/devtools/client/webconsole/new-console-output/actions/filters.js
new file mode 100644
index 000000000..05d080219
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/actions/filters.js
@@ -0,0 +1,55 @@
+/* -*- 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 { getAllFilters } = require("devtools/client/webconsole/new-console-output/selectors/filters");
+const Services = require("Services");
+
+const {
+ FILTER_TEXT_SET,
+ FILTER_TOGGLE,
+ FILTERS_CLEAR,
+ PREFS,
+} = require("devtools/client/webconsole/new-console-output/constants");
+
+function filterTextSet(text) {
+ return {
+ type: FILTER_TEXT_SET,
+ text
+ };
+}
+
+function filterToggle(filter) {
+ return (dispatch, getState) => {
+ dispatch({
+ type: FILTER_TOGGLE,
+ filter,
+ });
+ const filterState = getAllFilters(getState());
+ Services.prefs.setBoolPref(PREFS.FILTER[filter.toUpperCase()],
+ filterState.get(filter));
+ };
+}
+
+function filtersClear() {
+ return (dispatch, getState) => {
+ dispatch({
+ type: FILTERS_CLEAR,
+ });
+
+ const filterState = getAllFilters(getState());
+ for (let filter in filterState) {
+ Services.prefs.clearUserPref(PREFS.FILTER[filter.toUpperCase()]);
+ }
+ };
+}
+
+module.exports = {
+ filterTextSet,
+ filterToggle,
+ filtersClear
+};
diff --git a/devtools/client/webconsole/new-console-output/actions/index.js b/devtools/client/webconsole/new-console-output/actions/index.js
new file mode 100644
index 000000000..5ce76a402
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/actions/index.js
@@ -0,0 +1,18 @@
+/* -*- 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 actionModules = [
+ "enhancers",
+ "filters",
+ "messages",
+ "ui",
+].map(filename => require(`./${filename}`));
+
+const actions = Object.assign({}, ...actionModules);
+
+module.exports = actions;
diff --git a/devtools/client/webconsole/new-console-output/actions/messages.js b/devtools/client/webconsole/new-console-output/actions/messages.js
new file mode 100644
index 000000000..467e27503
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/actions/messages.js
@@ -0,0 +1,100 @@
+/* -*- 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 {
+ prepareMessage
+} = require("devtools/client/webconsole/new-console-output/utils/messages");
+const { IdGenerator } = require("devtools/client/webconsole/new-console-output/utils/id-generator");
+const { batchActions } = require("devtools/client/webconsole/new-console-output/actions/enhancers");
+const {
+ MESSAGE_ADD,
+ MESSAGES_CLEAR,
+ MESSAGE_OPEN,
+ MESSAGE_CLOSE,
+ MESSAGE_TYPE,
+ MESSAGE_TABLE_RECEIVE,
+} = require("../constants");
+
+const defaultIdGenerator = new IdGenerator();
+
+function messageAdd(packet, idGenerator = null) {
+ if (idGenerator == null) {
+ idGenerator = defaultIdGenerator;
+ }
+ let message = prepareMessage(packet, idGenerator);
+ const addMessageAction = {
+ type: MESSAGE_ADD,
+ message
+ };
+
+ if (message.type === MESSAGE_TYPE.CLEAR) {
+ return batchActions([
+ messagesClear(),
+ addMessageAction,
+ ]);
+ }
+ return addMessageAction;
+}
+
+function messagesClear() {
+ return {
+ type: MESSAGES_CLEAR
+ };
+}
+
+function messageOpen(id) {
+ return {
+ type: MESSAGE_OPEN,
+ id
+ };
+}
+
+function messageClose(id) {
+ return {
+ type: MESSAGE_CLOSE,
+ id
+ };
+}
+
+function messageTableDataGet(id, client, dataType) {
+ return (dispatch) => {
+ let fetchObjectActorData;
+ if (["Map", "WeakMap", "Set", "WeakSet"].includes(dataType)) {
+ fetchObjectActorData = (cb) => client.enumEntries(cb);
+ } else {
+ fetchObjectActorData = (cb) => client.enumProperties({
+ ignoreNonIndexedProperties: dataType === "Array"
+ }, cb);
+ }
+
+ fetchObjectActorData(enumResponse => {
+ const {iterator} = enumResponse;
+ iterator.slice(0, iterator.count, sliceResponse => {
+ let {ownProperties} = sliceResponse;
+ dispatch(messageTableDataReceive(id, ownProperties));
+ });
+ });
+ };
+}
+
+function messageTableDataReceive(id, data) {
+ return {
+ type: MESSAGE_TABLE_RECEIVE,
+ id,
+ data
+ };
+}
+
+module.exports = {
+ messageAdd,
+ messagesClear,
+ messageOpen,
+ messageClose,
+ messageTableDataGet,
+};
+
diff --git a/devtools/client/webconsole/new-console-output/actions/moz.build b/devtools/client/webconsole/new-console-output/actions/moz.build
new file mode 100644
index 000000000..c7a8ed52c
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/actions/moz.build
@@ -0,0 +1,12 @@
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'enhancers.js',
+ 'filters.js',
+ 'index.js',
+ 'messages.js',
+ 'ui.js',
+)
diff --git a/devtools/client/webconsole/new-console-output/actions/ui.js b/devtools/client/webconsole/new-console-output/actions/ui.js
new file mode 100644
index 000000000..cf9814d79
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/actions/ui.js
@@ -0,0 +1,27 @@
+/* -*- 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 { getAllUi } = require("devtools/client/webconsole/new-console-output/selectors/ui");
+const Services = require("Services");
+
+const {
+ FILTER_BAR_TOGGLE,
+ PREFS,
+} = require("devtools/client/webconsole/new-console-output/constants");
+
+function filterBarToggle(show) {
+ return (dispatch, getState) => {
+ dispatch({
+ type: FILTER_BAR_TOGGLE
+ });
+ const uiState = getAllUi(getState());
+ Services.prefs.setBoolPref(PREFS.UI.FILTER_BAR, uiState.get("filterBarVisible"));
+ };
+}
+
+exports.filterBarToggle = filterBarToggle;
diff --git a/devtools/client/webconsole/new-console-output/components/collapse-button.js b/devtools/client/webconsole/new-console-output/components/collapse-button.js
new file mode 100644
index 000000000..ab72fcf4d
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/components/collapse-button.js
@@ -0,0 +1,50 @@
+/* -*- 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";
+
+// React & Redux
+const {
+ createClass,
+ DOM: dom,
+ PropTypes,
+} = require("devtools/client/shared/vendor/react");
+
+const { l10n } = require("devtools/client/webconsole/new-console-output/utils/messages");
+
+const CollapseButton = createClass({
+
+ displayName: "CollapseButton",
+
+ propTypes: {
+ open: PropTypes.bool.isRequired,
+ title: PropTypes.string,
+ },
+
+ getDefaultProps: function () {
+ return {
+ title: l10n.getStr("messageToggleDetails")
+ };
+ },
+
+ render: function () {
+ const { open, onClick, title } = this.props;
+
+ let classes = ["theme-twisty"];
+
+ if (open) {
+ classes.push("open");
+ }
+
+ return dom.a({
+ className: classes.join(" "),
+ onClick,
+ title: title,
+ });
+ }
+});
+
+module.exports = CollapseButton;
diff --git a/devtools/client/webconsole/new-console-output/components/console-output.js b/devtools/client/webconsole/new-console-output/components/console-output.js
new file mode 100644
index 000000000..1ba7f8dda
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/components/console-output.js
@@ -0,0 +1,125 @@
+/* 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 {
+ createClass,
+ createFactory,
+ DOM: dom,
+ PropTypes
+} = require("devtools/client/shared/vendor/react");
+const ReactDOM = require("devtools/client/shared/vendor/react-dom");
+const { connect } = require("devtools/client/shared/vendor/react-redux");
+
+const {
+ getAllMessages,
+ getAllMessagesUiById,
+ getAllMessagesTableDataById,
+ getAllGroupsById,
+} = require("devtools/client/webconsole/new-console-output/selectors/messages");
+const { getScrollSetting } = require("devtools/client/webconsole/new-console-output/selectors/ui");
+const MessageContainer = createFactory(require("devtools/client/webconsole/new-console-output/components/message-container").MessageContainer);
+
+const ConsoleOutput = createClass({
+
+ displayName: "ConsoleOutput",
+
+ propTypes: {
+ messages: PropTypes.object.isRequired,
+ messagesUi: PropTypes.object.isRequired,
+ serviceContainer: PropTypes.shape({
+ attachRefToHud: PropTypes.func.isRequired,
+ }),
+ autoscroll: PropTypes.bool.isRequired,
+ },
+
+ componentDidMount() {
+ scrollToBottom(this.outputNode);
+ this.props.serviceContainer.attachRefToHud("outputScroller", this.outputNode);
+ },
+
+ componentWillUpdate(nextProps, nextState) {
+ if (!this.outputNode) {
+ return;
+ }
+
+ const outputNode = this.outputNode;
+
+ // Figure out if we are at the bottom. If so, then any new message should be scrolled
+ // into view.
+ if (this.props.autoscroll && outputNode.lastChild) {
+ this.shouldScrollBottom = isScrolledToBottom(outputNode.lastChild, outputNode);
+ }
+ },
+
+ componentDidUpdate() {
+ if (this.shouldScrollBottom) {
+ scrollToBottom(this.outputNode);
+ }
+ },
+
+ render() {
+ let {
+ dispatch,
+ autoscroll,
+ messages,
+ messagesUi,
+ messagesTableData,
+ serviceContainer,
+ groups,
+ } = this.props;
+
+ let messageNodes = messages.map((message) => {
+ const parentGroups = message.groupId ? (
+ (groups.get(message.groupId) || [])
+ .concat([message.groupId])
+ ) : [];
+
+ return (
+ MessageContainer({
+ dispatch,
+ message,
+ key: message.id,
+ serviceContainer,
+ open: messagesUi.includes(message.id),
+ tableData: messagesTableData.get(message.id),
+ autoscroll,
+ indent: parentGroups.length,
+ })
+ );
+ });
+ return (
+ dom.div({
+ className: "webconsole-output",
+ ref: node => {
+ this.outputNode = node;
+ },
+ }, messageNodes
+ )
+ );
+ }
+});
+
+function scrollToBottom(node) {
+ node.scrollTop = node.scrollHeight;
+}
+
+function isScrolledToBottom(outputNode, scrollNode) {
+ let lastNodeHeight = outputNode.lastChild ?
+ outputNode.lastChild.clientHeight : 0;
+ return scrollNode.scrollTop + scrollNode.clientHeight >=
+ scrollNode.scrollHeight - lastNodeHeight / 2;
+}
+
+function mapStateToProps(state, props) {
+ return {
+ messages: getAllMessages(state),
+ messagesUi: getAllMessagesUiById(state),
+ messagesTableData: getAllMessagesTableDataById(state),
+ autoscroll: getScrollSetting(state),
+ groups: getAllGroupsById(state),
+ };
+}
+
+module.exports = connect(mapStateToProps)(ConsoleOutput);
diff --git a/devtools/client/webconsole/new-console-output/components/console-table.js b/devtools/client/webconsole/new-console-output/components/console-table.js
new file mode 100644
index 000000000..bf8fdcbd8
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/components/console-table.js
@@ -0,0 +1,202 @@
+/* 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 {
+ createClass,
+ createFactory,
+ DOM: dom,
+ PropTypes
+} = require("devtools/client/shared/vendor/react");
+const { ObjectClient } = require("devtools/shared/client/main");
+const actions = require("devtools/client/webconsole/new-console-output/actions/messages");
+const {l10n} = require("devtools/client/webconsole/new-console-output/utils/messages");
+const GripMessageBody = createFactory(require("devtools/client/webconsole/new-console-output/components/grip-message-body"));
+
+const TABLE_ROW_MAX_ITEMS = 1000;
+const TABLE_COLUMN_MAX_ITEMS = 10;
+
+const ConsoleTable = createClass({
+
+ displayName: "ConsoleTable",
+
+ propTypes: {
+ dispatch: PropTypes.func.isRequired,
+ parameters: PropTypes.array.isRequired,
+ serviceContainer: PropTypes.shape({
+ hudProxyClient: PropTypes.object.isRequired,
+ }),
+ id: PropTypes.string.isRequired,
+ },
+
+ componentWillMount: function () {
+ const {id, dispatch, serviceContainer, parameters} = this.props;
+
+ if (!Array.isArray(parameters) || parameters.length === 0) {
+ return;
+ }
+
+ const client = new ObjectClient(serviceContainer.hudProxyClient, parameters[0]);
+ let dataType = getParametersDataType(parameters);
+
+ // Get all the object properties.
+ dispatch(actions.messageTableDataGet(id, client, dataType));
+ },
+
+ getHeaders: function (columns) {
+ let headerItems = [];
+ columns.forEach((value, key) => headerItems.push(dom.th({}, value)));
+ return headerItems;
+ },
+
+ getRows: function (columns, items) {
+ return items.map(item => {
+ let cells = [];
+ columns.forEach((value, key) => {
+ cells.push(
+ dom.td(
+ {},
+ GripMessageBody({
+ grip: item[key]
+ })
+ )
+ );
+ });
+ return dom.tr({}, cells);
+ });
+ },
+
+ render: function () {
+ const {parameters, tableData} = this.props;
+ const headersGrip = parameters[1];
+ const headers = headersGrip && headersGrip.preview ? headersGrip.preview.items : null;
+
+ // if tableData is nullable, we don't show anything.
+ if (!tableData) {
+ return null;
+ }
+
+ const {columns, items} = getTableItems(
+ tableData,
+ getParametersDataType(parameters),
+ headers
+ );
+
+ return (
+ dom.table({className: "new-consoletable devtools-monospace"},
+ dom.thead({}, this.getHeaders(columns)),
+ dom.tbody({}, this.getRows(columns, items))
+ )
+ );
+ }
+});
+
+function getParametersDataType(parameters = null) {
+ if (!Array.isArray(parameters) || parameters.length === 0) {
+ return null;
+ }
+ return parameters[0].class;
+}
+
+function getTableItems(data = {}, type, headers = null) {
+ const INDEX_NAME = "_index";
+ const VALUE_NAME = "_value";
+ const namedIndexes = {
+ [INDEX_NAME]: (
+ ["Object", "Array"].includes(type) ?
+ l10n.getStr("table.index") : l10n.getStr("table.iterationIndex")
+ ),
+ [VALUE_NAME]: l10n.getStr("table.value"),
+ key: l10n.getStr("table.key")
+ };
+
+ let columns = new Map();
+ let items = [];
+
+ let addItem = function (item) {
+ items.push(item);
+ Object.keys(item).forEach(key => addColumn(key));
+ };
+
+ let addColumn = function (columnIndex) {
+ let columnExists = columns.has(columnIndex);
+ let hasMaxColumns = columns.size == TABLE_COLUMN_MAX_ITEMS;
+ let hasCustomHeaders = Array.isArray(headers);
+
+ if (
+ !columnExists &&
+ !hasMaxColumns && (
+ !hasCustomHeaders ||
+ headers.includes(columnIndex) ||
+ columnIndex === INDEX_NAME
+ )
+ ) {
+ columns.set(columnIndex, namedIndexes[columnIndex] || columnIndex);
+ }
+ };
+
+ for (let index of Object.keys(data)) {
+ if (type !== "Object" && index == parseInt(index, 10)) {
+ index = parseInt(index, 10);
+ }
+
+ let item = {
+ [INDEX_NAME]: index
+ };
+
+ let property = data[index].value;
+
+ if (property.preview) {
+ let {preview} = property;
+ let entries = preview.ownProperties || preview.items;
+ if (entries) {
+ for (let key of Object.keys(entries)) {
+ let entry = entries[key];
+ item[key] = entry.value || entry;
+ }
+ } else {
+ if (preview.key) {
+ item.key = preview.key;
+ }
+
+ item[VALUE_NAME] = preview.value || property;
+ }
+ } else {
+ item[VALUE_NAME] = property;
+ }
+
+ addItem(item);
+
+ if (items.length === TABLE_ROW_MAX_ITEMS) {
+ break;
+ }
+ }
+
+ // Some headers might not be present in the items, so we make sure to
+ // return all the headers set by the user.
+ if (Array.isArray(headers)) {
+ headers.forEach(header => addColumn(header));
+ }
+
+ // We want to always have the index column first
+ if (columns.has(INDEX_NAME)) {
+ let index = columns.get(INDEX_NAME);
+ columns.delete(INDEX_NAME);
+ columns = new Map([[INDEX_NAME, index], ...columns.entries()]);
+ }
+
+ // We want to always have the values column last
+ if (columns.has(VALUE_NAME)) {
+ let index = columns.get(VALUE_NAME);
+ columns.delete(VALUE_NAME);
+ columns.set(VALUE_NAME, index);
+ }
+
+ return {
+ columns,
+ items
+ };
+}
+
+module.exports = ConsoleTable;
diff --git a/devtools/client/webconsole/new-console-output/components/filter-bar.js b/devtools/client/webconsole/new-console-output/components/filter-bar.js
new file mode 100644
index 000000000..a386a414a
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/components/filter-bar.js
@@ -0,0 +1,170 @@
+/* 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 {
+ createFactory,
+ createClass,
+ DOM: dom,
+ PropTypes
+} = require("devtools/client/shared/vendor/react");
+const { connect } = require("devtools/client/shared/vendor/react-redux");
+const { getAllFilters } = require("devtools/client/webconsole/new-console-output/selectors/filters");
+const { getAllUi } = require("devtools/client/webconsole/new-console-output/selectors/ui");
+const { filterTextSet, filtersClear } = require("devtools/client/webconsole/new-console-output/actions/index");
+const { messagesClear } = require("devtools/client/webconsole/new-console-output/actions/index");
+const uiActions = require("devtools/client/webconsole/new-console-output/actions/index");
+const {
+ MESSAGE_LEVEL
+} = require("../constants");
+const FilterButton = createFactory(require("devtools/client/webconsole/new-console-output/components/filter-button"));
+
+const FilterBar = createClass({
+
+ displayName: "FilterBar",
+
+ propTypes: {
+ filter: PropTypes.object.isRequired,
+ serviceContainer: PropTypes.shape({
+ attachRefToHud: PropTypes.func.isRequired,
+ }).isRequired,
+ ui: PropTypes.object.isRequired
+ },
+
+ componentDidMount() {
+ this.props.serviceContainer.attachRefToHud("filterBox",
+ this.wrapperNode.querySelector(".text-filter"));
+ },
+
+ onClickMessagesClear: function () {
+ this.props.dispatch(messagesClear());
+ },
+
+ onClickFilterBarToggle: function () {
+ this.props.dispatch(uiActions.filterBarToggle());
+ },
+
+ onClickFiltersClear: function () {
+ this.props.dispatch(filtersClear());
+ },
+
+ onSearchInput: function (e) {
+ this.props.dispatch(filterTextSet(e.target.value));
+ },
+
+ render() {
+ const {dispatch, filter, ui} = this.props;
+ let filterBarVisible = ui.filterBarVisible;
+ let children = [];
+
+ children.push(dom.div({className: "devtools-toolbar webconsole-filterbar-primary"},
+ dom.button({
+ className: "devtools-button devtools-clear-icon",
+ title: "Clear output",
+ onClick: this.onClickMessagesClear
+ }),
+ dom.button({
+ className: "devtools-button devtools-filter-icon" + (
+ filterBarVisible ? " checked" : ""),
+ title: "Toggle filter bar",
+ onClick: this.onClickFilterBarToggle
+ }),
+ dom.input({
+ className: "devtools-plaininput text-filter",
+ type: "search",
+ value: filter.text,
+ placeholder: "Filter output",
+ onInput: this.onSearchInput
+ })
+ ));
+
+ if (filterBarVisible) {
+ children.push(
+ dom.div({className: "devtools-toolbar webconsole-filterbar-secondary"},
+ FilterButton({
+ active: filter.error,
+ label: "Errors",
+ filterKey: MESSAGE_LEVEL.ERROR,
+ dispatch
+ }),
+ FilterButton({
+ active: filter.warn,
+ label: "Warnings",
+ filterKey: MESSAGE_LEVEL.WARN,
+ dispatch
+ }),
+ FilterButton({
+ active: filter.log,
+ label: "Logs",
+ filterKey: MESSAGE_LEVEL.LOG,
+ dispatch
+ }),
+ FilterButton({
+ active: filter.info,
+ label: "Info",
+ filterKey: MESSAGE_LEVEL.INFO,
+ dispatch
+ }),
+ FilterButton({
+ active: filter.debug,
+ label: "Debug",
+ filterKey: MESSAGE_LEVEL.DEBUG,
+ dispatch
+ }),
+ dom.span({
+ className: "devtools-separator",
+ }),
+ FilterButton({
+ active: filter.netxhr,
+ label: "XHR",
+ filterKey: "netxhr",
+ dispatch
+ }),
+ FilterButton({
+ active: filter.net,
+ label: "Requests",
+ filterKey: "net",
+ dispatch
+ })
+ )
+ );
+ }
+
+ if (ui.filteredMessageVisible) {
+ children.push(
+ dom.div({className: "devtools-toolbar"},
+ dom.span({
+ className: "clear"},
+ "You have filters set that may hide some results. " +
+ "Learn more about our filtering syntax ",
+ dom.a({}, "here"),
+ "."),
+ dom.button({
+ className: "menu-filter-button",
+ onClick: this.onClickFiltersClear
+ }, "Remove filters")
+ )
+ );
+ }
+
+ return (
+ dom.div({
+ className: "webconsole-filteringbar-wrapper",
+ ref: node => {
+ this.wrapperNode = node;
+ }
+ }, ...children
+ )
+ );
+ }
+});
+
+function mapStateToProps(state) {
+ return {
+ filter: getAllFilters(state),
+ ui: getAllUi(state)
+ };
+}
+
+module.exports = connect(mapStateToProps)(FilterBar);
diff --git a/devtools/client/webconsole/new-console-output/components/filter-button.js b/devtools/client/webconsole/new-console-output/components/filter-button.js
new file mode 100644
index 000000000..4116bb524
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/components/filter-button.js
@@ -0,0 +1,46 @@
+/* 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 {
+ createClass,
+ DOM: dom,
+ PropTypes
+} = require("devtools/client/shared/vendor/react");
+const actions = require("devtools/client/webconsole/new-console-output/actions/index");
+
+const FilterButton = createClass({
+
+ displayName: "FilterButton",
+
+ propTypes: {
+ label: PropTypes.string.isRequired,
+ filterKey: PropTypes.string.isRequired,
+ active: PropTypes.bool.isRequired,
+ dispatch: PropTypes.func.isRequired,
+ },
+
+ onClick: function () {
+ this.props.dispatch(actions.filterToggle(this.props.filterKey));
+ },
+
+ render() {
+ const {active, label, filterKey} = this.props;
+
+ let classList = [
+ "menu-filter-button",
+ filterKey,
+ ];
+ if (active) {
+ classList.push("checked");
+ }
+
+ return dom.button({
+ className: classList.join(" "),
+ onClick: this.onClick
+ }, label);
+ }
+});
+
+module.exports = FilterButton;
diff --git a/devtools/client/webconsole/new-console-output/components/grip-message-body.js b/devtools/client/webconsole/new-console-output/components/grip-message-body.js
new file mode 100644
index 000000000..29c2e6a4f
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/components/grip-message-body.js
@@ -0,0 +1,102 @@
+/* -*- 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";
+
+// If this is being run from Mocha, then the browser loader hasn't set up
+// define. We need to do that before loading Rep.
+if (typeof define === "undefined") {
+ require("amd-loader");
+}
+
+// React
+const {
+ createFactory,
+ PropTypes
+} = require("devtools/client/shared/vendor/react");
+const { createFactories } = require("devtools/client/shared/components/reps/rep-utils");
+const { Rep } = createFactories(require("devtools/client/shared/components/reps/rep"));
+const StringRep = createFactories(require("devtools/client/shared/components/reps/string").StringRep).rep;
+const VariablesViewLink = createFactory(require("devtools/client/webconsole/new-console-output/components/variables-view-link"));
+const { Grip } = require("devtools/client/shared/components/reps/grip");
+
+GripMessageBody.displayName = "GripMessageBody";
+
+GripMessageBody.propTypes = {
+ grip: PropTypes.oneOfType([
+ PropTypes.string,
+ PropTypes.number,
+ PropTypes.object,
+ ]).isRequired,
+ serviceContainer: PropTypes.shape({
+ createElement: PropTypes.func.isRequired,
+ }),
+ userProvidedStyle: PropTypes.string,
+};
+
+function GripMessageBody(props) {
+ const { grip, userProvidedStyle, serviceContainer } = props;
+
+ let styleObject;
+ if (userProvidedStyle && userProvidedStyle !== "") {
+ styleObject = cleanupStyle(userProvidedStyle, serviceContainer.createElement);
+ }
+
+ return (
+ // @TODO once there is a longString rep, also turn off quotes for those.
+ typeof grip === "string"
+ ? StringRep({
+ object: grip,
+ useQuotes: false,
+ mode: props.mode,
+ style: styleObject
+ })
+ : Rep({
+ object: grip,
+ objectLink: VariablesViewLink,
+ defaultRep: Grip,
+ mode: props.mode,
+ })
+ );
+}
+
+function cleanupStyle(userProvidedStyle, createElement) {
+ // Regular expression that matches the allowed CSS property names.
+ const allowedStylesRegex = new RegExp(
+ "^(?:-moz-)?(?:background|border|box|clear|color|cursor|display|float|font|line|" +
+ "margin|padding|text|transition|outline|white-space|word|writing|" +
+ "(?:min-|max-)?width|(?:min-|max-)?height)"
+ );
+
+ // Regular expression that matches the forbidden CSS property values.
+ const forbiddenValuesRegexs = [
+ // url(), -moz-element()
+ /\b(?:url|(?:-moz-)?element)[\s('"]+/gi,
+
+ // various URL protocols
+ /['"(]*(?:chrome|resource|about|app|data|https?|ftp|file):+\/*/gi,
+ ];
+
+ // Use a dummy element to parse the style string.
+ let dummy = createElement("div");
+ dummy.style = userProvidedStyle;
+
+ // Return a style object as expected by React DOM components, e.g.
+ // {color: "red"}
+ // without forbidden properties and values.
+ return [...dummy.style]
+ .filter(name => {
+ return allowedStylesRegex.test(name)
+ && !forbiddenValuesRegexs.some(regex => regex.test(dummy.style[name]));
+ })
+ .reduce((object, name) => {
+ return Object.assign({
+ [name]: dummy.style[name]
+ }, object);
+ }, {});
+}
+
+module.exports = GripMessageBody;
diff --git a/devtools/client/webconsole/new-console-output/components/message-container.js b/devtools/client/webconsole/new-console-output/components/message-container.js
new file mode 100644
index 000000000..115e9e291
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/components/message-container.js
@@ -0,0 +1,92 @@
+/* -*- 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";
+
+// React & Redux
+const {
+ createClass,
+ createFactory,
+ PropTypes
+} = require("devtools/client/shared/vendor/react");
+
+const {
+ MESSAGE_SOURCE,
+ MESSAGE_TYPE
+} = require("devtools/client/webconsole/new-console-output/constants");
+
+const componentMap = new Map([
+ ["ConsoleApiCall", require("./message-types/console-api-call")],
+ ["ConsoleCommand", require("./message-types/console-command")],
+ ["DefaultRenderer", require("./message-types/default-renderer")],
+ ["EvaluationResult", require("./message-types/evaluation-result")],
+ ["NetworkEventMessage", require("./message-types/network-event-message")],
+ ["PageError", require("./message-types/page-error")]
+]);
+
+const MessageContainer = createClass({
+ displayName: "MessageContainer",
+
+ propTypes: {
+ message: PropTypes.object.isRequired,
+ open: PropTypes.bool.isRequired,
+ serviceContainer: PropTypes.object.isRequired,
+ autoscroll: PropTypes.bool.isRequired,
+ indent: PropTypes.number.isRequired,
+ },
+
+ getDefaultProps: function () {
+ return {
+ open: false,
+ indent: 0,
+ };
+ },
+
+ shouldComponentUpdate(nextProps, nextState) {
+ const repeatChanged = this.props.message.repeat !== nextProps.message.repeat;
+ const openChanged = this.props.open !== nextProps.open;
+ const tableDataChanged = this.props.tableData !== nextProps.tableData;
+ return repeatChanged || openChanged || tableDataChanged;
+ },
+
+ render() {
+ const { message } = this.props;
+
+ let MessageComponent = createFactory(getMessageComponent(message));
+ return MessageComponent(this.props);
+ }
+});
+
+function getMessageComponent(message) {
+ switch (message.source) {
+ case MESSAGE_SOURCE.CONSOLE_API:
+ return componentMap.get("ConsoleApiCall");
+ case MESSAGE_SOURCE.NETWORK:
+ return componentMap.get("NetworkEventMessage");
+ case MESSAGE_SOURCE.JAVASCRIPT:
+ switch (message.type) {
+ case MESSAGE_TYPE.COMMAND:
+ return componentMap.get("ConsoleCommand");
+ case MESSAGE_TYPE.RESULT:
+ return componentMap.get("EvaluationResult");
+ // @TODO this is probably not the right behavior, but works for now.
+ // Chrome doesn't distinguish between page errors and log messages. We
+ // may want to remove the PageError component and just handle errors
+ // with ConsoleApiCall.
+ case MESSAGE_TYPE.LOG:
+ return componentMap.get("PageError");
+ default:
+ return componentMap.get("DefaultRenderer");
+ }
+ }
+
+ return componentMap.get("DefaultRenderer");
+}
+
+module.exports.MessageContainer = MessageContainer;
+
+// Exported so we can test it with unit tests.
+module.exports.getMessageComponent = getMessageComponent;
diff --git a/devtools/client/webconsole/new-console-output/components/message-icon.js b/devtools/client/webconsole/new-console-output/components/message-icon.js
new file mode 100644
index 000000000..b4c32fda0
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/components/message-icon.js
@@ -0,0 +1,32 @@
+/* -*- 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";
+
+// React & Redux
+const {
+ DOM: dom,
+ PropTypes
+} = require("devtools/client/shared/vendor/react");
+const {l10n} = require("devtools/client/webconsole/new-console-output/utils/messages");
+
+MessageIcon.displayName = "MessageIcon";
+
+MessageIcon.propTypes = {
+ level: PropTypes.string.isRequired,
+};
+
+function MessageIcon(props) {
+ const { level } = props;
+
+ const title = l10n.getStr("level." + level);
+ return dom.div({
+ className: "icon",
+ title
+ });
+}
+
+module.exports = MessageIcon;
diff --git a/devtools/client/webconsole/new-console-output/components/message-indent.js b/devtools/client/webconsole/new-console-output/components/message-indent.js
new file mode 100644
index 000000000..354e13589
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/components/message-indent.js
@@ -0,0 +1,37 @@
+/* -*- 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";
+
+// React & Redux
+const {
+ createClass,
+ DOM: dom,
+ PropTypes,
+} = require("devtools/client/shared/vendor/react");
+
+const INDENT_WIDTH = 12;
+const MessageIndent = createClass({
+
+ displayName: "MessageIndent",
+
+ propTypes: {
+ indent: PropTypes.number.isRequired,
+ },
+
+ render: function () {
+ const { indent } = this.props;
+ return dom.span({
+ className: "indent",
+ style: {"width": indent * INDENT_WIDTH}
+ });
+ }
+});
+
+module.exports.MessageIndent = MessageIndent;
+
+// Exported so we can test it with unit tests.
+module.exports.INDENT_WIDTH = INDENT_WIDTH;
diff --git a/devtools/client/webconsole/new-console-output/components/message-repeat.js b/devtools/client/webconsole/new-console-output/components/message-repeat.js
new file mode 100644
index 000000000..1820340ea
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/components/message-repeat.js
@@ -0,0 +1,36 @@
+
+/* -*- 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";
+
+// React & Redux
+const {
+ DOM: dom,
+ PropTypes
+} = require("devtools/client/shared/vendor/react");
+const { PluralForm } = require("devtools/shared/plural-form");
+const { l10n } = require("devtools/client/webconsole/new-console-output/utils/messages");
+
+MessageRepeat.displayName = "MessageRepeat";
+
+MessageRepeat.propTypes = {
+ repeat: PropTypes.number.isRequired
+};
+
+function MessageRepeat(props) {
+ const { repeat } = props;
+ const visibility = repeat > 1 ? "visible" : "hidden";
+
+ return dom.span({
+ className: "message-repeats",
+ style: {visibility},
+ title: PluralForm.get(repeat, l10n.getStr("messageRepeats.tooltip2"))
+ .replace("#1", repeat)
+ }, repeat);
+}
+
+module.exports = MessageRepeat;
diff --git a/devtools/client/webconsole/new-console-output/components/message-types/console-api-call.js b/devtools/client/webconsole/new-console-output/components/message-types/console-api-call.js
new file mode 100644
index 000000000..7200648fa
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/components/message-types/console-api-call.js
@@ -0,0 +1,132 @@
+/* -*- 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";
+
+// React & Redux
+const {
+ createFactory,
+ DOM: dom,
+ PropTypes
+} = require("devtools/client/shared/vendor/react");
+const GripMessageBody = createFactory(require("devtools/client/webconsole/new-console-output/components/grip-message-body"));
+const ConsoleTable = createFactory(require("devtools/client/webconsole/new-console-output/components/console-table"));
+const {isGroupType, l10n} = require("devtools/client/webconsole/new-console-output/utils/messages");
+
+const Message = createFactory(require("devtools/client/webconsole/new-console-output/components/message"));
+
+ConsoleApiCall.displayName = "ConsoleApiCall";
+
+ConsoleApiCall.propTypes = {
+ message: PropTypes.object.isRequired,
+ open: PropTypes.bool,
+ serviceContainer: PropTypes.object.isRequired,
+ indent: PropTypes.number.isRequired,
+};
+
+ConsoleApiCall.defaultProps = {
+ open: false,
+ indent: 0,
+};
+
+function ConsoleApiCall(props) {
+ const {
+ dispatch,
+ message,
+ open,
+ tableData,
+ serviceContainer,
+ indent,
+ } = props;
+ const {
+ id: messageId,
+ source,
+ type,
+ level,
+ repeat,
+ stacktrace,
+ frame,
+ parameters,
+ messageText,
+ userProvidedStyles,
+ } = message;
+
+ let messageBody;
+ if (type === "trace") {
+ messageBody = dom.span({className: "cm-variable"}, "console.trace()");
+ } else if (type === "assert") {
+ let reps = formatReps(parameters);
+ messageBody = dom.span({ className: "cm-variable" }, "Assertion failed: ", reps);
+ } else if (type === "table") {
+ // TODO: Chrome does not output anything, see if we want to keep this
+ messageBody = dom.span({className: "cm-variable"}, "console.table()");
+ } else if (parameters) {
+ messageBody = formatReps(parameters, userProvidedStyles, serviceContainer);
+ } else {
+ messageBody = messageText;
+ }
+
+ let attachment = null;
+ if (type === "table") {
+ attachment = ConsoleTable({
+ dispatch,
+ id: message.id,
+ serviceContainer,
+ parameters: message.parameters,
+ tableData
+ });
+ }
+
+ let collapseTitle = null;
+ if (isGroupType(type)) {
+ collapseTitle = l10n.getStr("groupToggle");
+ }
+
+ const collapsible = isGroupType(type)
+ || (type === "error" && Array.isArray(stacktrace));
+ const topLevelClasses = ["cm-s-mozilla"];
+
+ return Message({
+ messageId,
+ open,
+ collapsible,
+ collapseTitle,
+ source,
+ type,
+ level,
+ topLevelClasses,
+ messageBody,
+ repeat,
+ frame,
+ stacktrace,
+ attachment,
+ serviceContainer,
+ dispatch,
+ indent,
+ });
+}
+
+function formatReps(parameters, userProvidedStyles, serviceContainer) {
+ return (
+ parameters
+ // Get all the grips.
+ .map((grip, key) => GripMessageBody({
+ grip,
+ key,
+ userProvidedStyle: userProvidedStyles ? userProvidedStyles[key] : null,
+ serviceContainer
+ }))
+ // Interleave spaces.
+ .reduce((arr, v, i) => {
+ return i + 1 < parameters.length
+ ? arr.concat(v, dom.span({}, " "))
+ : arr.concat(v);
+ }, [])
+ );
+}
+
+module.exports = ConsoleApiCall;
+
diff --git a/devtools/client/webconsole/new-console-output/components/message-types/console-command.js b/devtools/client/webconsole/new-console-output/components/message-types/console-command.js
new file mode 100644
index 000000000..d87229fa9
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/components/message-types/console-command.js
@@ -0,0 +1,57 @@
+/* -*- 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";
+
+// React & Redux
+const {
+ createFactory,
+ PropTypes
+} = require("devtools/client/shared/vendor/react");
+const Message = createFactory(require("devtools/client/webconsole/new-console-output/components/message"));
+
+ConsoleCommand.displayName = "ConsoleCommand";
+
+ConsoleCommand.propTypes = {
+ message: PropTypes.object.isRequired,
+ autoscroll: PropTypes.bool.isRequired,
+ indent: PropTypes.number.isRequired,
+};
+
+ConsoleCommand.defaultProps = {
+ indent: 0,
+};
+
+/**
+ * Displays input from the console.
+ */
+function ConsoleCommand(props) {
+ const { autoscroll, indent, message } = props;
+ const {
+ source,
+ type,
+ level,
+ messageText: messageBody,
+ } = message;
+
+ const {
+ serviceContainer,
+ } = props;
+
+ const childProps = {
+ source,
+ type,
+ level,
+ topLevelClasses: [],
+ messageBody,
+ scrollToMessage: autoscroll,
+ serviceContainer,
+ indent: indent,
+ };
+ return Message(childProps);
+}
+
+module.exports = ConsoleCommand;
diff --git a/devtools/client/webconsole/new-console-output/components/message-types/default-renderer.js b/devtools/client/webconsole/new-console-output/components/message-types/default-renderer.js
new file mode 100644
index 000000000..d07089531
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/components/message-types/default-renderer.js
@@ -0,0 +1,22 @@
+/* -*- 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";
+
+// React & Redux
+const {
+ DOM: dom,
+} = require("devtools/client/shared/vendor/react");
+
+DefaultRenderer.displayName = "DefaultRenderer";
+
+function DefaultRenderer(props) {
+ return dom.div({},
+ "This message type is not supported yet."
+ );
+}
+
+module.exports = DefaultRenderer;
diff --git a/devtools/client/webconsole/new-console-output/components/message-types/evaluation-result.js b/devtools/client/webconsole/new-console-output/components/message-types/evaluation-result.js
new file mode 100644
index 000000000..992dc62cf
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/components/message-types/evaluation-result.js
@@ -0,0 +1,64 @@
+/* -*- 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";
+
+// React & Redux
+const {
+ createFactory,
+ PropTypes
+} = require("devtools/client/shared/vendor/react");
+const Message = createFactory(require("devtools/client/webconsole/new-console-output/components/message"));
+const GripMessageBody = createFactory(require("devtools/client/webconsole/new-console-output/components/grip-message-body"));
+
+EvaluationResult.displayName = "EvaluationResult";
+
+EvaluationResult.propTypes = {
+ message: PropTypes.object.isRequired,
+ indent: PropTypes.number.isRequired,
+};
+
+EvaluationResult.defaultProps = {
+ indent: 0,
+};
+
+function EvaluationResult(props) {
+ const { message, serviceContainer, indent } = props;
+ const {
+ source,
+ type,
+ level,
+ id: messageId,
+ exceptionDocURL,
+ frame,
+ } = message;
+
+ let messageBody;
+ if (message.messageText) {
+ messageBody = message.messageText;
+ } else {
+ messageBody = GripMessageBody({grip: message.parameters});
+ }
+
+ const topLevelClasses = ["cm-s-mozilla"];
+
+ const childProps = {
+ source,
+ type,
+ level,
+ indent,
+ topLevelClasses,
+ messageBody,
+ messageId,
+ scrollToMessage: props.autoscroll,
+ serviceContainer,
+ exceptionDocURL,
+ frame,
+ };
+ return Message(childProps);
+}
+
+module.exports = EvaluationResult;
diff --git a/devtools/client/webconsole/new-console-output/components/message-types/moz.build b/devtools/client/webconsole/new-console-output/components/message-types/moz.build
new file mode 100644
index 000000000..9b9f72017
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/components/message-types/moz.build
@@ -0,0 +1,13 @@
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'console-api-call.js',
+ 'console-command.js',
+ 'default-renderer.js',
+ 'evaluation-result.js',
+ 'network-event-message.js',
+ 'page-error.js',
+)
diff --git a/devtools/client/webconsole/new-console-output/components/message-types/network-event-message.js b/devtools/client/webconsole/new-console-output/components/message-types/network-event-message.js
new file mode 100644
index 000000000..e3c81a487
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/components/message-types/network-event-message.js
@@ -0,0 +1,63 @@
+/* -*- 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";
+
+// React & Redux
+const {
+ createFactory,
+ DOM: dom,
+ PropTypes
+} = require("devtools/client/shared/vendor/react");
+const Message = createFactory(require("devtools/client/webconsole/new-console-output/components/message"));
+const { l10n } = require("devtools/client/webconsole/new-console-output/utils/messages");
+
+NetworkEventMessage.displayName = "NetworkEventMessage";
+
+NetworkEventMessage.propTypes = {
+ message: PropTypes.object.isRequired,
+ serviceContainer: PropTypes.shape({
+ openNetworkPanel: PropTypes.func.isRequired,
+ }),
+ indent: PropTypes.number.isRequired,
+};
+
+NetworkEventMessage.defaultProps = {
+ indent: 0,
+};
+
+function NetworkEventMessage(props) {
+ const { message, serviceContainer, indent } = props;
+ const { actor, source, type, level, request, isXHR } = message;
+
+ const topLevelClasses = [ "cm-s-mozilla" ];
+
+ function onUrlClick() {
+ serviceContainer.openNetworkPanel(actor);
+ }
+
+ const method = dom.span({className: "method" }, request.method);
+ const xhr = isXHR
+ ? dom.span({ className: "xhr" }, l10n.getStr("webConsoleXhrIndicator"))
+ : null;
+ const url = dom.a({ className: "url", title: request.url, onClick: onUrlClick },
+ request.url.replace(/\?.+/, ""));
+
+ const messageBody = dom.span({}, method, xhr, url);
+
+ const childProps = {
+ source,
+ type,
+ level,
+ indent,
+ topLevelClasses,
+ messageBody,
+ serviceContainer,
+ };
+ return Message(childProps);
+}
+
+module.exports = NetworkEventMessage;
diff --git a/devtools/client/webconsole/new-console-output/components/message-types/page-error.js b/devtools/client/webconsole/new-console-output/components/message-types/page-error.js
new file mode 100644
index 000000000..77ea75ff7
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/components/message-types/page-error.js
@@ -0,0 +1,69 @@
+/* -*- 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";
+
+// React & Redux
+const {
+ createFactory,
+ PropTypes
+} = require("devtools/client/shared/vendor/react");
+const Message = createFactory(require("devtools/client/webconsole/new-console-output/components/message"));
+
+PageError.displayName = "PageError";
+
+PageError.propTypes = {
+ message: PropTypes.object.isRequired,
+ open: PropTypes.bool,
+ indent: PropTypes.number.isRequired,
+};
+
+PageError.defaultProps = {
+ open: false,
+ indent: 0,
+};
+
+function PageError(props) {
+ const {
+ dispatch,
+ message,
+ open,
+ serviceContainer,
+ indent,
+ } = props;
+ const {
+ id: messageId,
+ source,
+ type,
+ level,
+ messageText: messageBody,
+ repeat,
+ stacktrace,
+ frame,
+ exceptionDocURL,
+ } = message;
+
+ const childProps = {
+ dispatch,
+ messageId,
+ open,
+ collapsible: Array.isArray(stacktrace),
+ source,
+ type,
+ level,
+ topLevelClasses: [],
+ indent,
+ messageBody,
+ repeat,
+ frame,
+ stacktrace,
+ serviceContainer,
+ exceptionDocURL,
+ };
+ return Message(childProps);
+}
+
+module.exports = PageError;
diff --git a/devtools/client/webconsole/new-console-output/components/message.js b/devtools/client/webconsole/new-console-output/components/message.js
new file mode 100644
index 000000000..f36bff7e4
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/components/message.js
@@ -0,0 +1,176 @@
+/* -*- 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";
+
+// React & Redux
+const {
+ createClass,
+ createFactory,
+ DOM: dom,
+ PropTypes
+} = require("devtools/client/shared/vendor/react");
+const { l10n } = require("devtools/client/webconsole/new-console-output/utils/messages");
+const actions = require("devtools/client/webconsole/new-console-output/actions/index");
+const CollapseButton = createFactory(require("devtools/client/webconsole/new-console-output/components/collapse-button"));
+const MessageIndent = createFactory(require("devtools/client/webconsole/new-console-output/components/message-indent").MessageIndent);
+const MessageIcon = createFactory(require("devtools/client/webconsole/new-console-output/components/message-icon"));
+const MessageRepeat = createFactory(require("devtools/client/webconsole/new-console-output/components/message-repeat"));
+const FrameView = createFactory(require("devtools/client/shared/components/frame"));
+const StackTrace = createFactory(require("devtools/client/shared/components/stack-trace"));
+
+const Message = createClass({
+ displayName: "Message",
+
+ propTypes: {
+ open: PropTypes.bool,
+ collapsible: PropTypes.bool,
+ collapseTitle: PropTypes.string,
+ source: PropTypes.string.isRequired,
+ type: PropTypes.string.isRequired,
+ level: PropTypes.string.isRequired,
+ indent: PropTypes.number.isRequired,
+ topLevelClasses: PropTypes.array.isRequired,
+ messageBody: PropTypes.any.isRequired,
+ repeat: PropTypes.any,
+ frame: PropTypes.any,
+ attachment: PropTypes.any,
+ stacktrace: PropTypes.any,
+ messageId: PropTypes.string,
+ scrollToMessage: PropTypes.bool,
+ exceptionDocURL: PropTypes.string,
+ serviceContainer: PropTypes.shape({
+ emitNewMessage: PropTypes.func.isRequired,
+ onViewSourceInDebugger: PropTypes.func.isRequired,
+ sourceMapService: PropTypes.any,
+ }),
+ },
+
+ getDefaultProps: function () {
+ return {
+ indent: 0
+ };
+ },
+
+ componentDidMount() {
+ if (this.messageNode) {
+ if (this.props.scrollToMessage) {
+ this.messageNode.scrollIntoView();
+ }
+ // Event used in tests. Some message types don't pass it in because existing tests
+ // did not emit for them.
+ if (this.props.serviceContainer) {
+ this.props.serviceContainer.emitNewMessage(this.messageNode, this.props.messageId);
+ }
+ }
+ },
+
+ onLearnMoreClick: function () {
+ let {exceptionDocURL} = this.props;
+ this.props.serviceContainer.openLink(exceptionDocURL);
+ },
+
+ render() {
+ const {
+ messageId,
+ open,
+ collapsible,
+ collapseTitle,
+ source,
+ type,
+ level,
+ indent,
+ topLevelClasses,
+ messageBody,
+ frame,
+ stacktrace,
+ serviceContainer,
+ dispatch,
+ exceptionDocURL,
+ } = this.props;
+
+ topLevelClasses.push("message", source, type, level);
+ if (open) {
+ topLevelClasses.push("open");
+ }
+
+ const icon = MessageIcon({level});
+
+ // Figure out if there is an expandable part to the message.
+ let attachment = null;
+ if (this.props.attachment) {
+ attachment = this.props.attachment;
+ } else if (stacktrace) {
+ const child = open ? StackTrace({
+ stacktrace: stacktrace,
+ onViewSourceInDebugger: serviceContainer.onViewSourceInDebugger
+ }) : null;
+ attachment = dom.div({ className: "stacktrace devtools-monospace" }, child);
+ }
+
+ // If there is an expandable part, make it collapsible.
+ let collapse = null;
+ if (collapsible) {
+ collapse = CollapseButton({
+ open,
+ title: collapseTitle,
+ onClick: function () {
+ if (open) {
+ dispatch(actions.messageClose(messageId));
+ } else {
+ dispatch(actions.messageOpen(messageId));
+ }
+ },
+ });
+ }
+
+ const repeat = this.props.repeat ? MessageRepeat({repeat: this.props.repeat}) : null;
+
+ // Configure the location.
+ const location = dom.span({ className: "message-location devtools-monospace" },
+ frame ? FrameView({
+ frame,
+ onClick: serviceContainer ? serviceContainer.onViewSourceInDebugger : undefined,
+ showEmptyPathAsHost: true,
+ sourceMapService: serviceContainer ? serviceContainer.sourceMapService : undefined
+ }) : null
+ );
+
+ let learnMore;
+ if (exceptionDocURL) {
+ learnMore = dom.a({
+ className: "learn-more-link webconsole-learn-more-link",
+ title: exceptionDocURL.split("?")[0],
+ onClick: this.onLearnMoreClick,
+ }, `[${l10n.getStr("webConsoleMoreInfoLabel")}]`);
+ }
+
+ return dom.div({
+ className: topLevelClasses.join(" "),
+ ref: node => {
+ this.messageNode = node;
+ }
+ },
+ // @TODO add timestamp
+ MessageIndent({indent}),
+ icon,
+ collapse,
+ dom.span({ className: "message-body-wrapper" },
+ dom.span({ className: "message-flex-body" },
+ dom.span({ className: "message-body devtools-monospace" },
+ messageBody,
+ learnMore
+ ),
+ repeat,
+ location
+ ),
+ attachment
+ )
+ );
+ }
+});
+
+module.exports = Message;
diff --git a/devtools/client/webconsole/new-console-output/components/moz.build b/devtools/client/webconsole/new-console-output/components/moz.build
new file mode 100644
index 000000000..8c0022314
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/components/moz.build
@@ -0,0 +1,23 @@
+# vim: set filetype=python:
+# 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/.
+
+DIRS += [
+ 'message-types'
+]
+
+DevToolsModules(
+ 'collapse-button.js',
+ 'console-output.js',
+ 'console-table.js',
+ 'filter-bar.js',
+ 'filter-button.js',
+ 'grip-message-body.js',
+ 'message-container.js',
+ 'message-icon.js',
+ 'message-indent.js',
+ 'message-repeat.js',
+ 'message.js',
+ 'variables-view-link.js'
+)
diff --git a/devtools/client/webconsole/new-console-output/components/variables-view-link.js b/devtools/client/webconsole/new-console-output/components/variables-view-link.js
new file mode 100644
index 000000000..4d79c322f
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/components/variables-view-link.js
@@ -0,0 +1,34 @@
+/* -*- 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";
+
+// React & Redux
+const {
+ DOM: dom,
+ PropTypes
+} = require("devtools/client/shared/vendor/react");
+const {openVariablesView} = require("devtools/client/webconsole/new-console-output/utils/variables-view");
+
+VariablesViewLink.displayName = "VariablesViewLink";
+
+VariablesViewLink.propTypes = {
+ object: PropTypes.object.isRequired
+};
+
+function VariablesViewLink(props) {
+ const { object, children } = props;
+
+ return (
+ dom.a({
+ onClick: openVariablesView.bind(null, object),
+ className: "cm-variable",
+ draggable: false,
+ }, children)
+ );
+}
+
+module.exports = VariablesViewLink;
diff --git a/devtools/client/webconsole/new-console-output/constants.js b/devtools/client/webconsole/new-console-output/constants.js
new file mode 100644
index 000000000..ef11d6eb8
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/constants.js
@@ -0,0 +1,81 @@
+/* -*- 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 actionTypes = {
+ BATCH_ACTIONS: "BATCH_ACTIONS",
+ MESSAGE_ADD: "MESSAGE_ADD",
+ MESSAGES_CLEAR: "MESSAGES_CLEAR",
+ MESSAGE_OPEN: "MESSAGE_OPEN",
+ MESSAGE_CLOSE: "MESSAGE_CLOSE",
+ MESSAGE_TABLE_RECEIVE: "MESSAGE_TABLE_RECEIVE",
+ FILTER_TOGGLE: "FILTER_TOGGLE",
+ FILTER_TEXT_SET: "FILTER_TEXT_SET",
+ FILTERS_CLEAR: "FILTERS_CLEAR",
+ FILTER_BAR_TOGGLE: "FILTER_BAR_TOGGLE",
+};
+
+const prefs = {
+ PREFS: {
+ FILTER: {
+ ERROR: "devtools.webconsole.filter.error",
+ WARN: "devtools.webconsole.filter.warn",
+ INFO: "devtools.webconsole.filter.info",
+ LOG: "devtools.webconsole.filter.log",
+ DEBUG: "devtools.webconsole.filter.debug",
+ NET: "devtools.webconsole.filter.net",
+ NETXHR: "devtools.webconsole.filter.netxhr",
+ },
+ UI: {
+ FILTER_BAR: "devtools.webconsole.ui.filterbar"
+ }
+ }
+};
+
+const chromeRDPEnums = {
+ MESSAGE_SOURCE: {
+ XML: "xml",
+ JAVASCRIPT: "javascript",
+ NETWORK: "network",
+ CONSOLE_API: "console-api",
+ STORAGE: "storage",
+ APPCACHE: "appcache",
+ RENDERING: "rendering",
+ SECURITY: "security",
+ OTHER: "other",
+ DEPRECATION: "deprecation"
+ },
+ MESSAGE_TYPE: {
+ LOG: "log",
+ DIR: "dir",
+ TABLE: "table",
+ TRACE: "trace",
+ CLEAR: "clear",
+ START_GROUP: "startGroup",
+ START_GROUP_COLLAPSED: "startGroupCollapsed",
+ END_GROUP: "endGroup",
+ ASSERT: "assert",
+ PROFILE: "profile",
+ PROFILE_END: "profileEnd",
+ // Undocumented in Chrome RDP, but is used for evaluation results.
+ RESULT: "result",
+ // Undocumented in Chrome RDP, but is used for input.
+ COMMAND: "command",
+ // Undocumented in Chrome RDP, but is used for messages that should not
+ // output anything (e.g. `console.time()` calls).
+ NULL_MESSAGE: "nullMessage",
+ },
+ MESSAGE_LEVEL: {
+ LOG: "log",
+ ERROR: "error",
+ WARN: "warn",
+ DEBUG: "debug",
+ INFO: "info"
+ }
+};
+
+// Combine into a single constants object
+module.exports = Object.assign({}, actionTypes, prefs, chromeRDPEnums);
diff --git a/devtools/client/webconsole/new-console-output/main.js b/devtools/client/webconsole/new-console-output/main.js
new file mode 100644
index 000000000..29db5e337
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/main.js
@@ -0,0 +1,23 @@
+/* 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/. */
+
+ /* global BrowserLoader */
+
+"use strict";
+
+var { utils: Cu } = Components;
+
+const { XPCOMUtils } = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {});
+const { BrowserLoader } = Cu.import("resource://devtools/client/shared/browser-loader.js", {});
+
+// Initialize module loader and load all modules of the new inline
+// preview feature. The entire code-base doesn't need any extra
+// privileges and runs entirely in content scope.
+const NewConsoleOutputWrapper = BrowserLoader({
+ baseURI: "resource://devtools/client/webconsole/new-console-output/",
+ window}).require("./new-console-output-wrapper");
+
+this.NewConsoleOutput = function (parentNode, jsterm, toolbox, owner, serviceContainer) {
+ return new NewConsoleOutputWrapper(parentNode, jsterm, toolbox, owner, serviceContainer);
+};
diff --git a/devtools/client/webconsole/new-console-output/moz.build b/devtools/client/webconsole/new-console-output/moz.build
new file mode 100644
index 000000000..7d0905aaa
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/moz.build
@@ -0,0 +1,21 @@
+# vim: set filetype=python:
+# 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/.
+
+DIRS += [
+ 'actions',
+ 'components',
+ 'reducers',
+ 'selectors',
+ 'test',
+ 'utils',
+]
+
+DevToolsModules(
+ 'constants.js',
+ 'main.js',
+ 'new-console-output-wrapper.js',
+ 'store.js',
+ 'types.js',
+)
diff --git a/devtools/client/webconsole/new-console-output/new-console-output-wrapper.js b/devtools/client/webconsole/new-console-output/new-console-output-wrapper.js
new file mode 100644
index 000000000..17c1e767d
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/new-console-output-wrapper.js
@@ -0,0 +1,134 @@
+/* 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";
+
+// React & Redux
+const React = require("devtools/client/shared/vendor/react");
+const ReactDOM = require("devtools/client/shared/vendor/react-dom");
+const { Provider } = require("devtools/client/shared/vendor/react-redux");
+
+const actions = require("devtools/client/webconsole/new-console-output/actions/index");
+const { configureStore } = require("devtools/client/webconsole/new-console-output/store");
+
+const ConsoleOutput = React.createFactory(require("devtools/client/webconsole/new-console-output/components/console-output"));
+const FilterBar = React.createFactory(require("devtools/client/webconsole/new-console-output/components/filter-bar"));
+
+const store = configureStore();
+let queuedActions = [];
+let throttledDispatchTimeout = false;
+
+function NewConsoleOutputWrapper(parentNode, jsterm, toolbox, owner, document) {
+ this.parentNode = parentNode;
+ this.jsterm = jsterm;
+ this.toolbox = toolbox;
+ this.owner = owner;
+ this.document = document;
+
+ this.init = this.init.bind(this);
+}
+
+NewConsoleOutputWrapper.prototype = {
+ init: function () {
+ const attachRefToHud = (id, node) => {
+ this.jsterm.hud[id] = node;
+ };
+
+ let childComponent = ConsoleOutput({
+ serviceContainer: {
+ attachRefToHud,
+ emitNewMessage: (node, messageId) => {
+ this.jsterm.hud.emit("new-messages", new Set([{
+ node,
+ messageId,
+ }]));
+ },
+ hudProxyClient: this.jsterm.hud.proxy.client,
+ onViewSourceInDebugger: frame => this.toolbox.viewSourceInDebugger.call(
+ this.toolbox,
+ frame.url,
+ frame.line
+ ),
+ openNetworkPanel: (requestId) => {
+ return this.toolbox.selectTool("netmonitor").then(panel => {
+ return panel.panelWin.NetMonitorController.inspectRequest(requestId);
+ });
+ },
+ sourceMapService: this.toolbox ? this.toolbox._sourceMapService : null,
+ openLink: url => this.jsterm.hud.owner.openLink.call(this.jsterm.hud.owner, url),
+ createElement: nodename => {
+ return this.document.createElementNS("http://www.w3.org/1999/xhtml", nodename);
+ }
+ }
+ });
+ let filterBar = FilterBar({
+ serviceContainer: {
+ attachRefToHud
+ }
+ });
+ let provider = React.createElement(
+ Provider,
+ { store },
+ React.DOM.div(
+ {className: "webconsole-output-wrapper"},
+ filterBar,
+ childComponent
+ ));
+
+ this.body = ReactDOM.render(provider, this.parentNode);
+ },
+
+ dispatchMessageAdd: function (message, waitForResponse) {
+ let action = actions.messageAdd(message);
+ batchedMessageAdd(action);
+
+ // Wait for the message to render to resolve with the DOM node.
+ // This is just for backwards compatibility with old tests, and should
+ // be removed once it's not needed anymore.
+ // Can only wait for response if the action contains a valid message.
+ if (waitForResponse && action.message) {
+ let messageId = action.message.get("id");
+ return new Promise(resolve => {
+ let jsterm = this.jsterm;
+ jsterm.hud.on("new-messages", function onThisMessage(e, messages) {
+ for (let m of messages) {
+ if (m.messageId == messageId) {
+ resolve(m.node);
+ jsterm.hud.off("new-messages", onThisMessage);
+ return;
+ }
+ }
+ });
+ });
+ }
+
+ return Promise.resolve();
+ },
+
+ dispatchMessagesAdd: function (messages) {
+ const batchedActions = messages.map(message => actions.messageAdd(message));
+ store.dispatch(actions.batchActions(batchedActions));
+ },
+
+ dispatchMessagesClear: function () {
+ store.dispatch(actions.messagesClear());
+ },
+ // Should be used for test purpose only.
+ getStore: function () {
+ return store;
+ }
+};
+
+function batchedMessageAdd(action) {
+ queuedActions.push(action);
+ if (!throttledDispatchTimeout) {
+ throttledDispatchTimeout = setTimeout(() => {
+ store.dispatch(actions.batchActions(queuedActions));
+ queuedActions = [];
+ throttledDispatchTimeout = null;
+ }, 50);
+ }
+}
+
+// Exports from this module
+module.exports = NewConsoleOutputWrapper;
diff --git a/devtools/client/webconsole/new-console-output/reducers/filters.js b/devtools/client/webconsole/new-console-output/reducers/filters.js
new file mode 100644
index 000000000..cd5f4bf7c
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/reducers/filters.js
@@ -0,0 +1,39 @@
+/* -*- 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 Immutable = require("devtools/client/shared/vendor/immutable");
+const constants = require("devtools/client/webconsole/new-console-output/constants");
+
+const FilterState = Immutable.Record({
+ debug: true,
+ error: true,
+ info: true,
+ log: true,
+ net: false,
+ netxhr: false,
+ text: "",
+ warn: true,
+});
+
+function filters(state = new FilterState(), action) {
+ switch (action.type) {
+ case constants.FILTER_TOGGLE:
+ const {filter} = action;
+ const active = !state.get(filter);
+ return state.set(filter, active);
+ case constants.FILTERS_CLEAR:
+ return new FilterState();
+ case constants.FILTER_TEXT_SET:
+ let {text} = action;
+ return state.set("text", text);
+ }
+
+ return state;
+}
+
+exports.FilterState = FilterState;
+exports.filters = filters;
diff --git a/devtools/client/webconsole/new-console-output/reducers/index.js b/devtools/client/webconsole/new-console-output/reducers/index.js
new file mode 100644
index 000000000..6ab10d565
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/reducers/index.js
@@ -0,0 +1,18 @@
+/* -*- 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 { filters } = require("./filters");
+const { messages } = require("./messages");
+const { prefs } = require("./prefs");
+const { ui } = require("./ui");
+
+exports.reducers = {
+ filters,
+ messages,
+ prefs,
+ ui,
+};
diff --git a/devtools/client/webconsole/new-console-output/reducers/messages.js b/devtools/client/webconsole/new-console-output/reducers/messages.js
new file mode 100644
index 000000000..0693fed60
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/reducers/messages.js
@@ -0,0 +1,135 @@
+/* -*- 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 Immutable = require("devtools/client/shared/vendor/immutable");
+const constants = require("devtools/client/webconsole/new-console-output/constants");
+const {isGroupType} = require("devtools/client/webconsole/new-console-output/utils/messages");
+
+const MessageState = Immutable.Record({
+ // List of all the messages added to the console.
+ messagesById: Immutable.List(),
+ // List of the message ids which are opened.
+ messagesUiById: Immutable.List(),
+ // Map of the form {messageId : tableData}, which represent the data passed
+ // as an argument in console.table calls.
+ messagesTableDataById: Immutable.Map(),
+ // Map of the form {groupMessageId : groupArray},
+ // where groupArray is the list of of all the parent groups' ids of the groupMessageId.
+ groupsById: Immutable.Map(),
+ // Message id of the current group (no corresponding console.groupEnd yet).
+ currentGroup: null,
+});
+
+function messages(state = new MessageState(), action) {
+ const {
+ messagesById,
+ messagesUiById,
+ messagesTableDataById,
+ groupsById,
+ currentGroup
+ } = state;
+
+ switch (action.type) {
+ case constants.MESSAGE_ADD:
+ let newMessage = action.message;
+
+ if (newMessage.type === constants.MESSAGE_TYPE.NULL_MESSAGE) {
+ // When the message has a NULL type, we don't add it.
+ return state;
+ }
+
+ if (newMessage.type === constants.MESSAGE_TYPE.END_GROUP) {
+ // Compute the new current group.
+ return state.set("currentGroup", getNewCurrentGroup(currentGroup, groupsById));
+ }
+
+ if (newMessage.allowRepeating && messagesById.size > 0) {
+ let lastMessage = messagesById.last();
+ if (lastMessage.repeatId === newMessage.repeatId) {
+ return state.withMutations(function (record) {
+ record.set("messagesById", messagesById.pop().push(
+ newMessage.set("repeat", lastMessage.repeat + 1)
+ ));
+ });
+ }
+ }
+
+ return state.withMutations(function (record) {
+ // Add the new message with a reference to the parent group.
+ record.set(
+ "messagesById",
+ messagesById.push(newMessage.set("groupId", currentGroup))
+ );
+
+ if (newMessage.type === "trace") {
+ // We want the stacktrace to be open by default.
+ record.set("messagesUiById", messagesUiById.push(newMessage.id));
+ } else if (isGroupType(newMessage.type)) {
+ record.set("currentGroup", newMessage.id);
+ record.set("groupsById",
+ groupsById.set(
+ newMessage.id,
+ getParentGroups(currentGroup, groupsById)
+ )
+ );
+
+ if (newMessage.type === constants.MESSAGE_TYPE.START_GROUP) {
+ // We want the group to be open by default.
+ record.set("messagesUiById", messagesUiById.push(newMessage.id));
+ }
+ }
+ });
+ case constants.MESSAGES_CLEAR:
+ return state.withMutations(function (record) {
+ record.set("messagesById", Immutable.List());
+ record.set("messagesUiById", Immutable.List());
+ record.set("groupsById", Immutable.Map());
+ record.set("currentGroup", null);
+ });
+ case constants.MESSAGE_OPEN:
+ return state.set("messagesUiById", messagesUiById.push(action.id));
+ case constants.MESSAGE_CLOSE:
+ let index = state.messagesUiById.indexOf(action.id);
+ return state.deleteIn(["messagesUiById", index]);
+ case constants.MESSAGE_TABLE_RECEIVE:
+ const {id, data} = action;
+ return state.set("messagesTableDataById", messagesTableDataById.set(id, data));
+ }
+
+ return state;
+}
+
+function getNewCurrentGroup(currentGoup, groupsById) {
+ let newCurrentGroup = null;
+ if (currentGoup) {
+ // Retrieve the parent groups of the current group.
+ let parents = groupsById.get(currentGoup);
+ if (Array.isArray(parents) && parents.length > 0) {
+ // If there's at least one parent, make the first one the new currentGroup.
+ newCurrentGroup = parents[0];
+ }
+ }
+ return newCurrentGroup;
+}
+
+function getParentGroups(currentGroup, groupsById) {
+ let groups = [];
+ if (currentGroup) {
+ // If there is a current group, we add it as a parent
+ groups = [currentGroup];
+
+ // As well as all its parents, if it has some.
+ let parentGroups = groupsById.get(currentGroup);
+ if (Array.isArray(parentGroups) && parentGroups.length > 0) {
+ groups = groups.concat(parentGroups);
+ }
+ }
+
+ return groups;
+}
+
+exports.messages = messages;
diff --git a/devtools/client/webconsole/new-console-output/reducers/moz.build b/devtools/client/webconsole/new-console-output/reducers/moz.build
new file mode 100644
index 000000000..651512f85
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/reducers/moz.build
@@ -0,0 +1,12 @@
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'filters.js',
+ 'index.js',
+ 'messages.js',
+ 'prefs.js',
+ 'ui.js',
+)
diff --git a/devtools/client/webconsole/new-console-output/reducers/prefs.js b/devtools/client/webconsole/new-console-output/reducers/prefs.js
new file mode 100644
index 000000000..0707105e1
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/reducers/prefs.js
@@ -0,0 +1,18 @@
+/* -*- 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 Immutable = require("devtools/client/shared/vendor/immutable");
+const PrefState = Immutable.Record({
+ logLimit: 1000
+});
+
+function prefs(state = new PrefState(), action) {
+ return state;
+}
+
+exports.PrefState = PrefState;
+exports.prefs = prefs;
diff --git a/devtools/client/webconsole/new-console-output/reducers/ui.js b/devtools/client/webconsole/new-console-output/reducers/ui.js
new file mode 100644
index 000000000..aa91dceeb
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/reducers/ui.js
@@ -0,0 +1,39 @@
+/* -*- 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 {
+ FILTER_BAR_TOGGLE,
+ MESSAGE_ADD,
+} = require("devtools/client/webconsole/new-console-output/constants");
+const Immutable = require("devtools/client/shared/vendor/immutable");
+
+const UiState = Immutable.Record({
+ filterBarVisible: false,
+ filteredMessageVisible: false,
+ autoscroll: true,
+});
+
+function ui(state = new UiState(), action) {
+ // Autoscroll should be set for all action types. If the last action was not message
+ // add, then turn it off. This prevents us from scrolling after someone toggles a
+ // filter, or to the bottom of the attachement when an expandable message at the bottom
+ // of the list is expanded. It does depend on the MESSAGE_ADD action being the last in
+ // its batch, though.
+ state = state.set("autoscroll", action.type == MESSAGE_ADD);
+
+ switch (action.type) {
+ case FILTER_BAR_TOGGLE:
+ return state.set("filterBarVisible", !state.filterBarVisible);
+ }
+
+ return state;
+}
+
+module.exports = {
+ UiState,
+ ui,
+};
diff --git a/devtools/client/webconsole/new-console-output/selectors/filters.js b/devtools/client/webconsole/new-console-output/selectors/filters.js
new file mode 100644
index 000000000..36afa60cc
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/selectors/filters.js
@@ -0,0 +1,12 @@
+/* -*- 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";
+
+function getAllFilters(state) {
+ return state.filters;
+}
+
+exports.getAllFilters = getAllFilters;
diff --git a/devtools/client/webconsole/new-console-output/selectors/messages.js b/devtools/client/webconsole/new-console-output/selectors/messages.js
new file mode 100644
index 000000000..c4b1aee28
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/selectors/messages.js
@@ -0,0 +1,168 @@
+/* -*- 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 { l10n } = require("devtools/client/webconsole/new-console-output/utils/messages");
+const { getAllFilters } = require("devtools/client/webconsole/new-console-output/selectors/filters");
+const { getLogLimit } = require("devtools/client/webconsole/new-console-output/selectors/prefs");
+const {
+ MESSAGE_TYPE,
+ MESSAGE_SOURCE
+} = require("devtools/client/webconsole/new-console-output/constants");
+
+function getAllMessages(state) {
+ let messages = getAllMessagesById(state);
+ let logLimit = getLogLimit(state);
+ let filters = getAllFilters(state);
+
+ let groups = getAllGroupsById(state);
+ let messagesUI = getAllMessagesUiById(state);
+
+ return prune(
+ messages.filter(message => {
+ return (
+ isInOpenedGroup(message, groups, messagesUI)
+ && (
+ isUnfilterable(message)
+ || (
+ matchLevelFilters(message, filters)
+ && matchNetworkFilters(message, filters)
+ && matchSearchFilters(message, filters)
+ )
+ )
+ );
+ }),
+ logLimit
+ );
+}
+
+function getAllMessagesById(state) {
+ return state.messages.messagesById;
+}
+
+function getAllMessagesUiById(state) {
+ return state.messages.messagesUiById;
+}
+
+function getAllMessagesTableDataById(state) {
+ return state.messages.messagesTableDataById;
+}
+
+function getAllGroupsById(state) {
+ return state.messages.groupsById;
+}
+
+function getCurrentGroup(state) {
+ return state.messages.currentGroup;
+}
+
+function isUnfilterable(message) {
+ return [
+ MESSAGE_TYPE.COMMAND,
+ MESSAGE_TYPE.RESULT,
+ MESSAGE_TYPE.START_GROUP,
+ MESSAGE_TYPE.START_GROUP_COLLAPSED,
+ ].includes(message.type);
+}
+
+function isInOpenedGroup(message, groups, messagesUI) {
+ return !message.groupId
+ || (
+ !isGroupClosed(message.groupId, messagesUI)
+ && !hasClosedParentGroup(groups.get(message.groupId), messagesUI)
+ );
+}
+
+function hasClosedParentGroup(group, messagesUI) {
+ return group.some(groupId => isGroupClosed(groupId, messagesUI));
+}
+
+function isGroupClosed(groupId, messagesUI) {
+ return messagesUI.includes(groupId) === false;
+}
+
+function matchLevelFilters(message, filters) {
+ return filters.get(message.level) === true;
+}
+
+function matchNetworkFilters(message, filters) {
+ return (
+ message.source !== MESSAGE_SOURCE.NETWORK
+ || (filters.get("net") === true && message.isXHR === false)
+ || (filters.get("netxhr") === true && message.isXHR === true)
+ );
+}
+
+function matchSearchFilters(message, filters) {
+ let text = filters.text || "";
+ return (
+ text === ""
+ // @TODO currently we return true for any object grip. We should find a way to
+ // search object grips.
+ || (message.parameters !== null && !Array.isArray(message.parameters))
+ // Look for a match in location.
+ || isTextInFrame(text, message.frame)
+ // Look for a match in stacktrace.
+ || (
+ Array.isArray(message.stacktrace) &&
+ message.stacktrace.some(frame => isTextInFrame(text,
+ // isTextInFrame expect the properties of the frame object to be in the same
+ // order they are rendered in the Frame component.
+ {
+ functionName: frame.functionName ||
+ l10n.getStr("stacktrace.anonymousFunction"),
+ filename: frame.filename,
+ lineNumber: frame.lineNumber,
+ columnNumber: frame.columnNumber
+ }))
+ )
+ // Look for a match in messageText.
+ || (message.messageText !== null
+ && message.messageText.toLocaleLowerCase().includes(text.toLocaleLowerCase()))
+ // Look for a match in parameters. Currently only checks value grips.
+ || (message.parameters !== null
+ && message.parameters.join("").toLocaleLowerCase()
+ .includes(text.toLocaleLowerCase()))
+ );
+}
+
+function isTextInFrame(text, frame) {
+ if (!frame) {
+ return false;
+ }
+ // @TODO Change this to Object.values once it's supported in Node's version of V8
+ return Object.keys(frame)
+ .map(key => frame[key])
+ .join(":")
+ .toLocaleLowerCase()
+ .includes(text.toLocaleLowerCase());
+}
+
+function prune(messages, logLimit) {
+ let messageCount = messages.count();
+ if (messageCount > logLimit) {
+ // If the second non-pruned message is in a group,
+ // we want to return the group as the first non-pruned message.
+ let firstIndex = messages.size - logLimit;
+ let groupId = messages.get(firstIndex + 1).groupId;
+
+ if (groupId) {
+ return messages.splice(0, firstIndex + 1)
+ .unshift(
+ messages.findLast((message) => message.id === groupId)
+ );
+ }
+ return messages.splice(0, firstIndex);
+ }
+
+ return messages;
+}
+
+exports.getAllMessages = getAllMessages;
+exports.getAllMessagesUiById = getAllMessagesUiById;
+exports.getAllMessagesTableDataById = getAllMessagesTableDataById;
+exports.getAllGroupsById = getAllGroupsById;
+exports.getCurrentGroup = getCurrentGroup;
diff --git a/devtools/client/webconsole/new-console-output/selectors/moz.build b/devtools/client/webconsole/new-console-output/selectors/moz.build
new file mode 100644
index 000000000..547f53542
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/selectors/moz.build
@@ -0,0 +1,11 @@
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'filters.js',
+ 'messages.js',
+ 'prefs.js',
+ 'ui.js',
+)
diff --git a/devtools/client/webconsole/new-console-output/selectors/prefs.js b/devtools/client/webconsole/new-console-output/selectors/prefs.js
new file mode 100644
index 000000000..18d8b678c
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/selectors/prefs.js
@@ -0,0 +1,12 @@
+/* -*- 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";
+
+function getLogLimit(state) {
+ return state.prefs.logLimit;
+}
+
+exports.getLogLimit = getLogLimit;
diff --git a/devtools/client/webconsole/new-console-output/selectors/ui.js b/devtools/client/webconsole/new-console-output/selectors/ui.js
new file mode 100644
index 000000000..c9729e92d
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/selectors/ui.js
@@ -0,0 +1,20 @@
+/* -*- 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";
+
+function getAllUi(state) {
+ return state.ui;
+}
+
+function getScrollSetting(state) {
+ return getAllUi(state).autoscroll;
+}
+
+module.exports = {
+ getAllUi,
+ getScrollSetting,
+};
diff --git a/devtools/client/webconsole/new-console-output/store.js b/devtools/client/webconsole/new-console-output/store.js
new file mode 100644
index 000000000..8ad7947e9
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/store.js
@@ -0,0 +1,74 @@
+/* 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 {FilterState} = require("devtools/client/webconsole/new-console-output/reducers/filters");
+const {PrefState} = require("devtools/client/webconsole/new-console-output/reducers/prefs");
+const {UiState} = require("devtools/client/webconsole/new-console-output/reducers/ui");
+const {
+ applyMiddleware,
+ combineReducers,
+ compose,
+ createStore
+} = require("devtools/client/shared/vendor/redux");
+const { thunk } = require("devtools/client/shared/redux/middleware/thunk");
+const {
+ BATCH_ACTIONS,
+ PREFS,
+} = require("devtools/client/webconsole/new-console-output/constants");
+const { reducers } = require("./reducers/index");
+const Services = require("Services");
+
+function configureStore() {
+ const initialState = {
+ prefs: new PrefState({
+ logLimit: Math.max(Services.prefs.getIntPref("devtools.hud.loglimit"), 1),
+ }),
+ filters: new FilterState({
+ error: Services.prefs.getBoolPref(PREFS.FILTER.ERROR),
+ warn: Services.prefs.getBoolPref(PREFS.FILTER.WARN),
+ info: Services.prefs.getBoolPref(PREFS.FILTER.INFO),
+ log: Services.prefs.getBoolPref(PREFS.FILTER.LOG),
+ net: Services.prefs.getBoolPref(PREFS.FILTER.NET),
+ netxhr: Services.prefs.getBoolPref(PREFS.FILTER.NETXHR),
+ }),
+ ui: new UiState({
+ filterBarVisible: Services.prefs.getBoolPref(PREFS.UI.FILTER_BAR),
+ })
+ };
+
+ return createStore(
+ combineReducers(reducers),
+ initialState,
+ compose(applyMiddleware(thunk), enableBatching())
+ );
+}
+
+/**
+ * A enhancer for the store to handle batched actions.
+ */
+function enableBatching() {
+ return next => (reducer, initialState, enhancer) => {
+ function batchingReducer(state, action) {
+ switch (action.type) {
+ case BATCH_ACTIONS:
+ return action.actions.reduce(batchingReducer, state);
+ default:
+ return reducer(state, action);
+ }
+ }
+
+ if (typeof initialState === "function" && typeof enhancer === "undefined") {
+ enhancer = initialState;
+ initialState = undefined;
+ }
+
+ return next(batchingReducer, initialState, enhancer);
+ };
+}
+
+// Provide the store factory for test code so that each test is working with
+// its own instance.
+module.exports.configureStore = configureStore;
+
diff --git a/devtools/client/webconsole/new-console-output/test/.eslintrc.js b/devtools/client/webconsole/new-console-output/test/.eslintrc.js
new file mode 100644
index 000000000..e010df386
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ "extends": ["../../../../.eslintrc.xpcshell.js"]
+};
diff --git a/devtools/client/webconsole/new-console-output/test/chrome/chrome.ini b/devtools/client/webconsole/new-console-output/test/chrome/chrome.ini
new file mode 100644
index 000000000..0543ae5c6
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/chrome/chrome.ini
@@ -0,0 +1,7 @@
+[DEFAULT]
+
+support-files =
+ head.js
+
+[test_render_perf.html]
+skip-if = true # Bug 1306783
diff --git a/devtools/client/webconsole/new-console-output/test/chrome/head.js b/devtools/client/webconsole/new-console-output/test/chrome/head.js
new file mode 100644
index 000000000..e8a5fd22e
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/chrome/head.js
@@ -0,0 +1,16 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var { utils: Cu } = Components;
+
+var { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+var { Assert } = require("resource://testing-common/Assert.jsm");
+var { BrowserLoader } = Cu.import("resource://devtools/client/shared/browser-loader.js", {});
+var { Task } = require("devtools/shared/task");
+
+var { require: browserRequire } = BrowserLoader({
+ baseURI: "resource://devtools/client/webconsole/",
+ window
+});
diff --git a/devtools/client/webconsole/new-console-output/test/chrome/test_render_perf.html b/devtools/client/webconsole/new-console-output/test/chrome/test_render_perf.html
new file mode 100644
index 000000000..d22819a2b
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/chrome/test_render_perf.html
@@ -0,0 +1,90 @@
+<!DOCTYPE HTML>
+<html lang="en">
+<head>
+ <meta charset="utf8">
+ <title>Test for getRepeatId()</title>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript;version=1.8" src="head.js"></script>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+</head>
+<body>
+<p>Test for render perf</p>
+<div id="output"></div>
+
+<script type="text/javascript;version=1.8">
+const testPackets = [];
+const numMessages = 1000;
+for (let id = 0; id < numMessages; id++) {
+ let message = "Odd text";
+ if (id % 2 === 0) {
+ message = "Even text";
+ }
+ testPackets.push({
+ "from": "server1.conn4.child1/consoleActor2",
+ "type": "consoleAPICall",
+ "message": {
+ "arguments": [
+ "foobar",
+ message,
+ id
+ ],
+ "columnNumber": 1,
+ "counter": null,
+ "filename": "file:///test.html",
+ "functionName": "",
+ "groupName": "",
+ "level": "log",
+ "lineNumber": 1,
+ "private": false,
+ "styles": [],
+ "timeStamp": 1455064271115 + id,
+ "timer": null,
+ "workerType": "none",
+ "category": "webdev"
+ }
+ });
+}
+
+function timeit(cb) {
+ // Return a Promise that resolves the number of seconds cb takes.
+ return new Promise(resolve => {
+ let start = performance.now();
+ cb();
+ let elapsed = performance.now() - start;
+ resolve(elapsed / 1000);
+ });
+}
+
+window.onload = Task.async(function* () {
+ const { configureStore } = browserRequire("devtools/client/webconsole/new-console-output/store");
+ const { filterTextSet, filtersClear } = browserRequire("devtools/client/webconsole/new-console-output/actions/index");
+ const NewConsoleOutputWrapper = browserRequire("devtools/client/webconsole/new-console-output/new-console-output-wrapper");
+ const wrapper = new NewConsoleOutputWrapper(document.querySelector("#output"), {});
+
+ const store = configureStore();
+
+ let time = yield timeit(() => {
+ testPackets.forEach((message) => {
+ wrapper.dispatchMessageAdd(message);
+ });
+ });
+ info("took " + time + " seconds to render messages");
+
+ time = yield timeit(() => {
+ store.dispatch(filterTextSet("Odd text"));
+ });
+ info("took " + time + " seconds to search filter half the messages");
+
+ time = yield timeit(() => {
+ store.dispatch(filtersClear());
+ });
+ info("took " + time + " seconds to clear the filter");
+
+ ok(true, "Yay, it didn't time out!");
+
+ SimpleTest.finish();
+});
+</script>
+</body>
+</html>
diff --git a/devtools/client/webconsole/new-console-output/test/components/console-api-call.test.js b/devtools/client/webconsole/new-console-output/test/components/console-api-call.test.js
new file mode 100644
index 000000000..3b4e2b196
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/components/console-api-call.test.js
@@ -0,0 +1,230 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test utils.
+const expect = require("expect");
+const { render, mount } = require("enzyme");
+const sinon = require("sinon");
+
+// React
+const { createFactory } = require("devtools/client/shared/vendor/react");
+const Provider = createFactory(require("react-redux").Provider);
+const { setupStore } = require("devtools/client/webconsole/new-console-output/test/helpers");
+
+// Components under test.
+const ConsoleApiCall = createFactory(require("devtools/client/webconsole/new-console-output/components/message-types/console-api-call"));
+const {
+ MESSAGE_OPEN,
+ MESSAGE_CLOSE,
+} = require("devtools/client/webconsole/new-console-output/constants");
+const { INDENT_WIDTH } = require("devtools/client/webconsole/new-console-output/components/message-indent");
+
+// Test fakes.
+const { stubPreparedMessages } = require("devtools/client/webconsole/new-console-output/test/fixtures/stubs/index");
+const serviceContainer = require("devtools/client/webconsole/new-console-output/test/fixtures/serviceContainer");
+
+const tempfilePath = "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js";
+
+describe("ConsoleAPICall component:", () => {
+ describe("console.log", () => {
+ it("renders string grips", () => {
+ const message = stubPreparedMessages.get("console.log('foobar', 'test')");
+ const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
+
+ expect(wrapper.find(".message-body").text()).toBe("foobar test");
+ expect(wrapper.find(".objectBox-string").length).toBe(2);
+ expect(wrapper.find("div.message.cm-s-mozilla span span.message-flex-body span.message-body.devtools-monospace").length).toBe(1);
+
+ // There should be the location
+ const locationLink = wrapper.find(`.message-location`);
+ expect(locationLink.length).toBe(1);
+ expect(locationLink.text()).toBe("test-tempfile.js:1:27");
+ });
+
+ it("renders string grips with custom style", () => {
+ const message = stubPreparedMessages.get("console.log(%cfoobar)");
+ const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
+
+ const elements = wrapper.find(".objectBox-string");
+ expect(elements.text()).toBe("foobar");
+ expect(elements.length).toBe(2);
+
+ const firstElementStyle = elements.eq(0).prop("style");
+ // Allowed styles are applied accordingly on the first element.
+ expect(firstElementStyle.color).toBe(`blue`);
+ expect(firstElementStyle["font-size"]).toBe(`1.3em`);
+ // Forbidden styles are not applied.
+ expect(firstElementStyle["background-image"]).toBe(undefined);
+ expect(firstElementStyle.position).toBe(undefined);
+ expect(firstElementStyle.top).toBe(undefined);
+
+ const secondElementStyle = elements.eq(1).prop("style");
+ // Allowed styles are applied accordingly on the second element.
+ expect(secondElementStyle.color).toBe(`red`);
+ // Forbidden styles are not applied.
+ expect(secondElementStyle.background).toBe(undefined);
+ });
+
+ it("renders repeat node", () => {
+ const message =
+ stubPreparedMessages.get("console.log('foobar', 'test')")
+ .set("repeat", 107);
+ const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
+
+ expect(wrapper.find(".message-repeats").text()).toBe("107");
+ expect(wrapper.find(".message-repeats").prop("title")).toBe("107 repeats");
+
+ expect(wrapper.find("span > span.message-flex-body > span.message-body.devtools-monospace + span.message-repeats").length).toBe(1);
+ });
+
+ it("has the expected indent", () => {
+ const message = stubPreparedMessages.get("console.log('foobar', 'test')");
+
+ const indent = 10;
+ let wrapper = render(ConsoleApiCall({ message, serviceContainer, indent }));
+ expect(wrapper.find(".indent").prop("style").width)
+ .toBe(`${indent * INDENT_WIDTH}px`);
+
+ wrapper = render(ConsoleApiCall({ message, serviceContainer}));
+ expect(wrapper.find(".indent").prop("style").width).toBe(`0`);
+ });
+ });
+
+ describe("console.count", () => {
+ it("renders", () => {
+ const message = stubPreparedMessages.get("console.count('bar')");
+ const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
+
+ expect(wrapper.find(".message-body").text()).toBe("bar: 1");
+ });
+ });
+
+ describe("console.assert", () => {
+ it("renders", () => {
+ const message = stubPreparedMessages.get("console.assert(false, {message: 'foobar'})");
+ const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
+
+ expect(wrapper.find(".message-body").text()).toBe("Assertion failed: Object { message: \"foobar\" }");
+ });
+ });
+
+ describe("console.time", () => {
+ it("does not show anything", () => {
+ const message = stubPreparedMessages.get("console.time('bar')");
+ const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
+
+ expect(wrapper.find(".message-body").text()).toBe("");
+ });
+ });
+
+ describe("console.timeEnd", () => {
+ it("renders as expected", () => {
+ const message = stubPreparedMessages.get("console.timeEnd('bar')");
+ const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
+
+ expect(wrapper.find(".message-body").text()).toBe(message.messageText);
+ expect(wrapper.find(".message-body").text()).toMatch(/^bar: \d+(\.\d+)?ms$/);
+ });
+ });
+
+ describe("console.trace", () => {
+ it("renders", () => {
+ const message = stubPreparedMessages.get("console.trace()");
+ const wrapper = render(ConsoleApiCall({ message, serviceContainer, open: true }));
+ const filepath = `${tempfilePath}`;
+
+ expect(wrapper.find(".message-body").text()).toBe("console.trace()");
+
+ const frameLinks = wrapper.find(`.stack-trace span.frame-link[data-url='${filepath}']`);
+ expect(frameLinks.length).toBe(3);
+
+ expect(frameLinks.eq(0).find(".frame-link-function-display-name").text()).toBe("testStacktraceFiltering");
+ expect(frameLinks.eq(0).find(".frame-link-filename").text()).toBe(filepath);
+
+ expect(frameLinks.eq(1).find(".frame-link-function-display-name").text()).toBe("foo");
+ expect(frameLinks.eq(1).find(".frame-link-filename").text()).toBe(filepath);
+
+ expect(frameLinks.eq(2).find(".frame-link-function-display-name").text()).toBe("triggerPacket");
+ expect(frameLinks.eq(2).find(".frame-link-filename").text()).toBe(filepath);
+
+ //it should not be collapsible.
+ expect(wrapper.find(`.theme-twisty`).length).toBe(0);
+ });
+ });
+
+ describe("console.group", () => {
+ it("renders", () => {
+ const message = stubPreparedMessages.get("console.group('bar')");
+ const wrapper = render(ConsoleApiCall({ message, serviceContainer, open: true }));
+
+ expect(wrapper.find(".message-body").text()).toBe(message.messageText);
+ expect(wrapper.find(".theme-twisty.open").length).toBe(1);
+ });
+
+ it("toggle the group when the collapse button is clicked", () => {
+ const store = setupStore([]);
+ store.dispatch = sinon.spy();
+ const message = stubPreparedMessages.get("console.group('bar')");
+
+ let wrapper = mount(Provider({store},
+ ConsoleApiCall({
+ message,
+ open: true,
+ dispatch: store.dispatch,
+ serviceContainer,
+ })
+ ));
+ wrapper.find(".theme-twisty.open").simulate("click");
+ let call = store.dispatch.getCall(0);
+ expect(call.args[0]).toEqual({
+ id: message.id,
+ type: MESSAGE_CLOSE
+ });
+
+ wrapper = mount(Provider({store},
+ ConsoleApiCall({
+ message,
+ open: false,
+ dispatch: store.dispatch,
+ serviceContainer,
+ })
+ ));
+ wrapper.find(".theme-twisty").simulate("click");
+ call = store.dispatch.getCall(1);
+ expect(call.args[0]).toEqual({
+ id: message.id,
+ type: MESSAGE_OPEN
+ });
+ });
+ });
+
+ describe("console.groupEnd", () => {
+ it("does not show anything", () => {
+ const message = stubPreparedMessages.get("console.groupEnd('bar')");
+ const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
+
+ expect(wrapper.find(".message-body").text()).toBe("");
+ });
+ });
+
+ describe("console.groupCollapsed", () => {
+ it("renders", () => {
+ const message = stubPreparedMessages.get("console.groupCollapsed('foo')");
+ const wrapper = render(ConsoleApiCall({ message, serviceContainer, open: false}));
+
+ expect(wrapper.find(".message-body").text()).toBe(message.messageText);
+ expect(wrapper.find(".theme-twisty:not(.open)").length).toBe(1);
+ });
+ });
+
+ describe("console.dirxml", () => {
+ it("renders", () => {
+ const message = stubPreparedMessages.get("console.dirxml(window)");
+ const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
+
+ expect(wrapper.find(".message-body").text())
+ .toBe("Window http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html");
+ });
+ });
+});
diff --git a/devtools/client/webconsole/new-console-output/test/components/evaluation-result.test.js b/devtools/client/webconsole/new-console-output/test/components/evaluation-result.test.js
new file mode 100644
index 000000000..4d7890807
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/components/evaluation-result.test.js
@@ -0,0 +1,84 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test utils.
+const expect = require("expect");
+const { render, mount } = require("enzyme");
+const sinon = require("sinon");
+
+// React
+const { createFactory } = require("devtools/client/shared/vendor/react");
+const Provider = createFactory(require("react-redux").Provider);
+const { setupStore } = require("devtools/client/webconsole/new-console-output/test/helpers");
+
+// Components under test.
+const EvaluationResult = createFactory(require("devtools/client/webconsole/new-console-output/components/message-types/evaluation-result"));
+const { INDENT_WIDTH } = require("devtools/client/webconsole/new-console-output/components/message-indent");
+
+// Test fakes.
+const { stubPreparedMessages } = require("devtools/client/webconsole/new-console-output/test/fixtures/stubs/index");
+const serviceContainer = require("devtools/client/webconsole/new-console-output/test/fixtures/serviceContainer");
+
+describe("EvaluationResult component:", () => {
+ it("renders a grip result", () => {
+ const message = stubPreparedMessages.get("new Date(0)");
+ const wrapper = render(EvaluationResult({ message }));
+
+ expect(wrapper.find(".message-body").text()).toBe("Date 1970-01-01T00:00:00.000Z");
+
+ expect(wrapper.find(".message.log").length).toBe(1);
+ });
+
+ it("renders an error", () => {
+ const message = stubPreparedMessages.get("asdf()");
+ const wrapper = render(EvaluationResult({ message }));
+
+ expect(wrapper.find(".message-body").text())
+ .toBe("ReferenceError: asdf is not defined[Learn More]");
+
+ expect(wrapper.find(".message.error").length).toBe(1);
+ });
+
+ it("displays a [Learn more] link", () => {
+ const store = setupStore([]);
+
+ const message = stubPreparedMessages.get("asdf()");
+
+ serviceContainer.openLink = sinon.spy();
+ const wrapper = mount(Provider({store},
+ EvaluationResult({message, serviceContainer})
+ ));
+
+ const url =
+ "https://developer.mozilla.org/docs/Web/JavaScript/Reference/Errors/Not_defined";
+ const learnMore = wrapper.find(".learn-more-link");
+ expect(learnMore.length).toBe(1);
+ expect(learnMore.prop("title")).toBe(url);
+
+ learnMore.simulate("click");
+ let call = serviceContainer.openLink.getCall(0);
+ expect(call.args[0]).toEqual(message.exceptionDocURL);
+ });
+
+ it("has the expected indent", () => {
+ const message = stubPreparedMessages.get("new Date(0)");
+
+ const indent = 10;
+ let wrapper = render(EvaluationResult({ message, indent}));
+ expect(wrapper.find(".indent").prop("style").width)
+ .toBe(`${indent * INDENT_WIDTH}px`);
+
+ wrapper = render(EvaluationResult({ message}));
+ expect(wrapper.find(".indent").prop("style").width).toBe(`0`);
+ });
+
+ it("has location information", () => {
+ const message = stubPreparedMessages.get("1 + @");
+ const wrapper = render(EvaluationResult({ message }));
+
+ const locationLink = wrapper.find(`.message-location`);
+ expect(locationLink.length).toBe(1);
+ expect(locationLink.text()).toBe("debugger eval code:1:4");
+ });
+});
diff --git a/devtools/client/webconsole/new-console-output/test/components/filter-bar.test.js b/devtools/client/webconsole/new-console-output/test/components/filter-bar.test.js
new file mode 100644
index 000000000..23f958cd9
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/components/filter-bar.test.js
@@ -0,0 +1,96 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const expect = require("expect");
+const sinon = require("sinon");
+const { render, mount } = require("enzyme");
+
+const { createFactory } = require("devtools/client/shared/vendor/react");
+const Provider = createFactory(require("react-redux").Provider);
+
+const FilterButton = createFactory(require("devtools/client/webconsole/new-console-output/components/filter-button"));
+const FilterBar = createFactory(require("devtools/client/webconsole/new-console-output/components/filter-bar"));
+const { getAllUi } = require("devtools/client/webconsole/new-console-output/selectors/ui");
+const {
+ MESSAGES_CLEAR,
+ MESSAGE_LEVEL
+} = require("devtools/client/webconsole/new-console-output/constants");
+
+const { setupStore } = require("devtools/client/webconsole/new-console-output/test/helpers");
+const serviceContainer = require("devtools/client/webconsole/new-console-output/test/fixtures/serviceContainer");
+
+describe("FilterBar component:", () => {
+ it("initial render", () => {
+ const store = setupStore([]);
+
+ const wrapper = render(Provider({store}, FilterBar({ serviceContainer })));
+ const toolbar = wrapper.find(
+ ".devtools-toolbar.webconsole-filterbar-primary"
+ );
+
+ // Clear button
+ expect(toolbar.children().eq(0).attr("class"))
+ .toBe("devtools-button devtools-clear-icon");
+ expect(toolbar.children().eq(0).attr("title")).toBe("Clear output");
+
+ // Filter bar toggle
+ expect(toolbar.children().eq(1).attr("class"))
+ .toBe("devtools-button devtools-filter-icon");
+ expect(toolbar.children().eq(1).attr("title")).toBe("Toggle filter bar");
+
+ // Text filter
+ expect(toolbar.children().eq(2).attr("class")).toBe("devtools-plaininput text-filter");
+ expect(toolbar.children().eq(2).attr("placeholder")).toBe("Filter output");
+ expect(toolbar.children().eq(2).attr("type")).toBe("search");
+ expect(toolbar.children().eq(2).attr("value")).toBe("");
+ });
+
+ it("displays filter bar when button is clicked", () => {
+ const store = setupStore([]);
+
+ expect(getAllUi(store.getState()).filterBarVisible).toBe(false);
+
+ const wrapper = mount(Provider({store}, FilterBar({ serviceContainer })));
+ wrapper.find(".devtools-filter-icon").simulate("click");
+
+ expect(getAllUi(store.getState()).filterBarVisible).toBe(true);
+
+ // Buttons are displayed
+ const buttonProps = {
+ active: true,
+ dispatch: store.dispatch
+ };
+ const logButton = FilterButton(Object.assign({}, buttonProps,
+ { label: "Logs", filterKey: MESSAGE_LEVEL.LOG }));
+ const debugButton = FilterButton(Object.assign({}, buttonProps,
+ { label: "Debug", filterKey: MESSAGE_LEVEL.DEBUG }));
+ const infoButton = FilterButton(Object.assign({}, buttonProps,
+ { label: "Info", filterKey: MESSAGE_LEVEL.INFO }));
+ const warnButton = FilterButton(Object.assign({}, buttonProps,
+ { label: "Warnings", filterKey: MESSAGE_LEVEL.WARN }));
+ const errorButton = FilterButton(Object.assign({}, buttonProps,
+ { label: "Errors", filterKey: MESSAGE_LEVEL.ERROR }));
+ expect(wrapper.contains([errorButton, warnButton, logButton, infoButton, debugButton])).toBe(true);
+ });
+
+ it("fires MESSAGES_CLEAR action when clear button is clicked", () => {
+ const store = setupStore([]);
+ store.dispatch = sinon.spy();
+
+ const wrapper = mount(Provider({store}, FilterBar({ serviceContainer })));
+ wrapper.find(".devtools-clear-icon").simulate("click");
+ const call = store.dispatch.getCall(0);
+ expect(call.args[0]).toEqual({
+ type: MESSAGES_CLEAR
+ });
+ });
+
+ it("sets filter text when text is typed", () => {
+ const store = setupStore([]);
+
+ const wrapper = mount(Provider({store}, FilterBar({ serviceContainer })));
+ wrapper.find(".devtools-plaininput").simulate("input", { target: { value: "a" } });
+ expect(store.getState().filters.text).toBe("a");
+ });
+});
diff --git a/devtools/client/webconsole/new-console-output/test/components/filter-button.test.js b/devtools/client/webconsole/new-console-output/test/components/filter-button.test.js
new file mode 100644
index 000000000..3774da0b8
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/components/filter-button.test.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const expect = require("expect");
+const { render } = require("enzyme");
+
+const { createFactory } = require("devtools/client/shared/vendor/react");
+
+const FilterButton = createFactory(require("devtools/client/webconsole/new-console-output/components/filter-button"));
+const { MESSAGE_LEVEL } = require("devtools/client/webconsole/new-console-output/constants");
+
+describe("FilterButton component:", () => {
+ const props = {
+ active: true,
+ label: "Error",
+ filterKey: MESSAGE_LEVEL.ERROR,
+ };
+
+ it("displays as active when turned on", () => {
+ const wrapper = render(FilterButton(props));
+ expect(wrapper.html()).toBe(
+ "<button class=\"menu-filter-button error checked\">Error</button>"
+ );
+ });
+
+ it("displays as inactive when turned off", () => {
+ const inactiveProps = Object.assign({}, props, { active: false });
+ const wrapper = render(FilterButton(inactiveProps));
+ expect(wrapper.html()).toBe(
+ "<button class=\"menu-filter-button error\">Error</button>"
+ );
+ });
+});
diff --git a/devtools/client/webconsole/new-console-output/test/components/message-container.test.js b/devtools/client/webconsole/new-console-output/test/components/message-container.test.js
new file mode 100644
index 000000000..2377af906
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/components/message-container.test.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test utils.
+const expect = require("expect");
+const {
+ renderComponent,
+ shallowRenderComponent
+} = require("devtools/client/webconsole/new-console-output/test/helpers");
+
+// Components under test.
+const { MessageContainer } = require("devtools/client/webconsole/new-console-output/components/message-container");
+const ConsoleApiCall = require("devtools/client/webconsole/new-console-output/components/message-types/console-api-call");
+const EvaluationResult = require("devtools/client/webconsole/new-console-output/components/message-types/evaluation-result");
+const PageError = require("devtools/client/webconsole/new-console-output/components/message-types/page-error");
+
+// Test fakes.
+const { stubPreparedMessages } = require("devtools/client/webconsole/new-console-output/test/fixtures/stubs/index");
+const serviceContainer = require("devtools/client/webconsole/new-console-output/test/fixtures/serviceContainer");
+
+describe("MessageContainer component:", () => {
+ it("pipes data to children as expected", () => {
+ const message = stubPreparedMessages.get("console.log('foobar', 'test')");
+ const rendered = renderComponent(MessageContainer, {message, serviceContainer});
+
+ expect(rendered.textContent.includes("foobar")).toBe(true);
+ });
+ it("picks correct child component", () => {
+ const messageTypes = [
+ {
+ component: ConsoleApiCall,
+ message: stubPreparedMessages.get("console.log('foobar', 'test')")
+ },
+ {
+ component: EvaluationResult,
+ message: stubPreparedMessages.get("new Date(0)")
+ },
+ {
+ component: PageError,
+ message: stubPreparedMessages.get("ReferenceError: asdf is not defined")
+ }
+ ];
+
+ messageTypes.forEach(info => {
+ const { component, message } = info;
+ const rendered = shallowRenderComponent(MessageContainer, {
+ message,
+ serviceContainer,
+ });
+ expect(rendered.type).toBe(component);
+ });
+ });
+});
diff --git a/devtools/client/webconsole/new-console-output/test/components/message-icon.test.js b/devtools/client/webconsole/new-console-output/test/components/message-icon.test.js
new file mode 100644
index 000000000..0244f08cf
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/components/message-icon.test.js
@@ -0,0 +1,23 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const {
+ MESSAGE_LEVEL,
+} = require("devtools/client/webconsole/new-console-output/constants");
+const MessageIcon = require("devtools/client/webconsole/new-console-output/components/message-icon");
+
+const expect = require("expect");
+
+const {
+ renderComponent
+} = require("devtools/client/webconsole/new-console-output/test/helpers");
+
+describe("MessageIcon component:", () => {
+ it("renders icon based on level", () => {
+ const rendered = renderComponent(MessageIcon, { level: MESSAGE_LEVEL.ERROR });
+
+ expect(rendered.classList.contains("icon")).toBe(true);
+ expect(rendered.getAttribute("title")).toBe("Error");
+ });
+});
diff --git a/devtools/client/webconsole/new-console-output/test/components/message-repeat.test.js b/devtools/client/webconsole/new-console-output/test/components/message-repeat.test.js
new file mode 100644
index 000000000..0257a3aad
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/components/message-repeat.test.js
@@ -0,0 +1,25 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const MessageRepeat = require("devtools/client/webconsole/new-console-output/components/message-repeat");
+
+const expect = require("expect");
+
+const {
+ renderComponent
+} = require("devtools/client/webconsole/new-console-output/test/helpers");
+
+describe("MessageRepeat component:", () => {
+ it("renders repeated value correctly", () => {
+ const rendered = renderComponent(MessageRepeat, { repeat: 99 });
+ expect(rendered.classList.contains("message-repeats")).toBe(true);
+ expect(rendered.style.visibility).toBe("visible");
+ expect(rendered.textContent).toBe("99");
+ });
+
+ it("renders an un-repeated value correctly", () => {
+ const rendered = renderComponent(MessageRepeat, { repeat: 1 });
+ expect(rendered.style.visibility).toBe("hidden");
+ });
+});
diff --git a/devtools/client/webconsole/new-console-output/test/components/network-event-message.test.js b/devtools/client/webconsole/new-console-output/test/components/network-event-message.test.js
new file mode 100644
index 000000000..8d0c5307e
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/components/network-event-message.test.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test utils.
+const expect = require("expect");
+const { render } = require("enzyme");
+
+// React
+const { createFactory } = require("devtools/client/shared/vendor/react");
+
+// Components under test.
+const NetworkEventMessage = createFactory(require("devtools/client/webconsole/new-console-output/components/message-types/network-event-message"));
+const { INDENT_WIDTH } = require("devtools/client/webconsole/new-console-output/components/message-indent");
+
+// Test fakes.
+const { stubPreparedMessages } = require("devtools/client/webconsole/new-console-output/test/fixtures/stubs/index");
+const serviceContainer = require("devtools/client/webconsole/new-console-output/test/fixtures/serviceContainer");
+
+const EXPECTED_URL = "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/inexistent.html";
+
+describe("NetworkEventMessage component:", () => {
+ describe("GET request", () => {
+ it("renders as expected", () => {
+ const message = stubPreparedMessages.get("GET request");
+ const wrapper = render(NetworkEventMessage({ message, serviceContainer }));
+
+ expect(wrapper.find(".message-body .method").text()).toBe("GET");
+ expect(wrapper.find(".message-body .xhr").length).toBe(0);
+ expect(wrapper.find(".message-body .url").length).toBe(1);
+ expect(wrapper.find(".message-body .url").text()).toBe(EXPECTED_URL);
+ expect(wrapper.find("div.message.cm-s-mozilla span.message-body.devtools-monospace").length).toBe(1);
+ });
+
+ it("has the expected indent", () => {
+ const message = stubPreparedMessages.get("GET request");
+
+ const indent = 10;
+ let wrapper = render(NetworkEventMessage({ message, serviceContainer, indent}));
+ expect(wrapper.find(".indent").prop("style").width)
+ .toBe(`${indent * INDENT_WIDTH}px`);
+
+ wrapper = render(NetworkEventMessage({ message, serviceContainer }));
+ expect(wrapper.find(".indent").prop("style").width).toBe(`0`);
+ });
+ });
+
+ describe("XHR GET request", () => {
+ it("renders as expected", () => {
+ const message = stubPreparedMessages.get("XHR GET request");
+ const wrapper = render(NetworkEventMessage({ message, serviceContainer }));
+
+ expect(wrapper.find(".message-body .method").text()).toBe("GET");
+ expect(wrapper.find(".message-body .xhr").length).toBe(1);
+ expect(wrapper.find(".message-body .xhr").text()).toBe("XHR");
+ expect(wrapper.find(".message-body .url").text()).toBe(EXPECTED_URL);
+ expect(wrapper.find("div.message.cm-s-mozilla span.message-body.devtools-monospace").length).toBe(1);
+ });
+ });
+
+ describe("XHR POST request", () => {
+ it("renders as expected", () => {
+ const message = stubPreparedMessages.get("XHR POST request");
+ const wrapper = render(NetworkEventMessage({ message, serviceContainer }));
+
+ expect(wrapper.find(".message-body .method").text()).toBe("POST");
+ expect(wrapper.find(".message-body .xhr").length).toBe(1);
+ expect(wrapper.find(".message-body .xhr").text()).toBe("XHR");
+ expect(wrapper.find(".message-body .url").length).toBe(1);
+ expect(wrapper.find(".message-body .url").text()).toBe(EXPECTED_URL);
+ expect(wrapper.find("div.message.cm-s-mozilla span.message-body.devtools-monospace").length).toBe(1);
+ });
+ });
+});
diff --git a/devtools/client/webconsole/new-console-output/test/components/page-error.test.js b/devtools/client/webconsole/new-console-output/test/components/page-error.test.js
new file mode 100644
index 000000000..93f3a9ea5
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/components/page-error.test.js
@@ -0,0 +1,126 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test utils.
+const expect = require("expect");
+const { render, mount } = require("enzyme");
+const sinon = require("sinon");
+
+// React
+const { createFactory } = require("devtools/client/shared/vendor/react");
+const Provider = createFactory(require("react-redux").Provider);
+const { setupStore } = require("devtools/client/webconsole/new-console-output/test/helpers");
+
+// Components under test.
+const PageError = require("devtools/client/webconsole/new-console-output/components/message-types/page-error");
+const {
+ MESSAGE_OPEN,
+ MESSAGE_CLOSE,
+} = require("devtools/client/webconsole/new-console-output/constants");
+const { INDENT_WIDTH } = require("devtools/client/webconsole/new-console-output/components/message-indent");
+
+// Test fakes.
+const { stubPreparedMessages } = require("devtools/client/webconsole/new-console-output/test/fixtures/stubs/index");
+const serviceContainer = require("devtools/client/webconsole/new-console-output/test/fixtures/serviceContainer");
+
+describe("PageError component:", () => {
+ it("renders", () => {
+ const message = stubPreparedMessages.get("ReferenceError: asdf is not defined");
+ const wrapper = render(PageError({ message, serviceContainer }));
+
+ expect(wrapper.find(".message-body").text())
+ .toBe("ReferenceError: asdf is not defined[Learn More]");
+
+ // The stacktrace should be closed by default.
+ const frameLinks = wrapper.find(`.stack-trace`);
+ expect(frameLinks.length).toBe(0);
+
+ // There should be the location.
+ const locationLink = wrapper.find(`.message-location`);
+ expect(locationLink.length).toBe(1);
+ // @TODO Will likely change. See https://github.com/devtools-html/gecko-dev/issues/285
+ expect(locationLink.text()).toBe("test-tempfile.js:3:5");
+ });
+
+ it("displays a [Learn more] link", () => {
+ const store = setupStore([]);
+
+ const message = stubPreparedMessages.get("ReferenceError: asdf is not defined");
+
+ serviceContainer.openLink = sinon.spy();
+ const wrapper = mount(Provider({store},
+ PageError({message, serviceContainer})
+ ));
+
+ // There should be a [Learn more] link.
+ const url =
+ "https://developer.mozilla.org/docs/Web/JavaScript/Reference/Errors/Not_defined";
+ const learnMore = wrapper.find(".learn-more-link");
+ expect(learnMore.length).toBe(1);
+ expect(learnMore.prop("title")).toBe(url);
+
+ learnMore.simulate("click");
+ let call = serviceContainer.openLink.getCall(0);
+ expect(call.args[0]).toEqual(message.exceptionDocURL);
+ });
+
+ it("has a stacktrace which can be openned", () => {
+ const message = stubPreparedMessages.get("ReferenceError: asdf is not defined");
+ const wrapper = render(PageError({ message, serviceContainer, open: true }));
+
+ // There should be a collapse button.
+ expect(wrapper.find(".theme-twisty.open").length).toBe(1);
+
+ // There should be three stacktrace items.
+ const frameLinks = wrapper.find(`.stack-trace span.frame-link`);
+ expect(frameLinks.length).toBe(3);
+ });
+
+ it("toggle the stacktrace when the collapse button is clicked", () => {
+ const store = setupStore([]);
+ store.dispatch = sinon.spy();
+ const message = stubPreparedMessages.get("ReferenceError: asdf is not defined");
+
+ let wrapper = mount(Provider({store},
+ PageError({
+ message,
+ open: true,
+ dispatch: store.dispatch,
+ serviceContainer,
+ })
+ ));
+ wrapper.find(".theme-twisty.open").simulate("click");
+ let call = store.dispatch.getCall(0);
+ expect(call.args[0]).toEqual({
+ id: message.id,
+ type: MESSAGE_CLOSE
+ });
+
+ wrapper = mount(Provider({store},
+ PageError({
+ message,
+ open: false,
+ dispatch: store.dispatch,
+ serviceContainer,
+ })
+ ));
+ wrapper.find(".theme-twisty").simulate("click");
+ call = store.dispatch.getCall(1);
+ expect(call.args[0]).toEqual({
+ id: message.id,
+ type: MESSAGE_OPEN
+ });
+ });
+
+ it("has the expected indent", () => {
+ const message = stubPreparedMessages.get("ReferenceError: asdf is not defined");
+ const indent = 10;
+ let wrapper = render(PageError({ message, serviceContainer, indent}));
+ expect(wrapper.find(".indent").prop("style").width)
+ .toBe(`${indent * INDENT_WIDTH}px`);
+
+ wrapper = render(PageError({ message, serviceContainer}));
+ expect(wrapper.find(".indent").prop("style").width).toBe(`0`);
+ });
+});
diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/L10n.js b/devtools/client/webconsole/new-console-output/test/fixtures/L10n.js
new file mode 100644
index 000000000..bb34bb477
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/L10n.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// @TODO Load the actual strings from webconsole.properties instead.
+class L10n {
+ getStr(str) {
+ switch (str) {
+ case "level.error":
+ return "Error";
+ case "consoleCleared":
+ return "Console was cleared.";
+ case "webConsoleXhrIndicator":
+ return "XHR";
+ case "webConsoleMoreInfoLabel":
+ return "Learn More";
+ }
+ return str;
+ }
+
+ getFormatStr(str) {
+ return this.getStr(str);
+ }
+}
+
+module.exports = L10n;
diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/LocalizationHelper.js b/devtools/client/webconsole/new-console-output/test/fixtures/LocalizationHelper.js
new file mode 100644
index 000000000..8e6e9428c
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/LocalizationHelper.js
@@ -0,0 +1,10 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const LocalizationHelper = require("devtools/client/webconsole/new-console-output/test/fixtures/L10n");
+
+module.exports = {
+ LocalizationHelper
+};
diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/ObjectClient.js b/devtools/client/webconsole/new-console-output/test/fixtures/ObjectClient.js
new file mode 100644
index 000000000..87a058d5c
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/ObjectClient.js
@@ -0,0 +1,9 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+class ObjectClient {
+}
+
+module.exports = ObjectClient;
diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/PluralForm.js b/devtools/client/webconsole/new-console-output/test/fixtures/PluralForm.js
new file mode 100644
index 000000000..9ab3ad3ec
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/PluralForm.js
@@ -0,0 +1,18 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+module.exports = {
+ PluralForm: {
+ get: function (occurence, str) {
+ // @TODO Remove when loading the actual strings from webconsole.properties
+ // is done in the L10n fixture.
+ if (str === "messageRepeats.tooltip2") {
+ return `${occurence} repeats`;
+ }
+
+ return str;
+ }
+ }
+};
diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/Services.js b/devtools/client/webconsole/new-console-output/test/fixtures/Services.js
new file mode 100644
index 000000000..61b3d5e13
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/Services.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { PREFS } = require("devtools/client/webconsole/new-console-output/constants");
+
+module.exports = {
+ prefs: {
+ getIntPref: pref => {
+ switch (pref) {
+ case "devtools.hud.loglimit":
+ return 1000;
+ }
+ },
+ getBoolPref: pref => {
+ const falsey = [
+ PREFS.FILTER.NET,
+ PREFS.FILTER.NETXHR,
+ PREFS.UI.FILTER_BAR,
+ ];
+ return !falsey.includes(pref);
+ },
+ setBoolPref: () => {},
+ clearUserPref: () => {},
+ }
+};
diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/WebConsoleUtils.js b/devtools/client/webconsole/new-console-output/test/fixtures/WebConsoleUtils.js
new file mode 100644
index 000000000..5ab1c0bb4
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/WebConsoleUtils.js
@@ -0,0 +1,14 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const L10n = require("devtools/client/webconsole/new-console-output/test/fixtures/L10n");
+
+const Utils = {
+ L10n
+};
+
+module.exports = {
+ Utils
+};
diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/moz.build b/devtools/client/webconsole/new-console-output/test/fixtures/moz.build
new file mode 100644
index 000000000..ff41d6c80
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/moz.build
@@ -0,0 +1,9 @@
+# vim: set filetype=python:
+# 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/.
+
+DIRS += [
+ 'stub-generators',
+ 'stubs'
+]
diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/serviceContainer.js b/devtools/client/webconsole/new-console-output/test/fixtures/serviceContainer.js
new file mode 100644
index 000000000..04b15c88b
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/serviceContainer.js
@@ -0,0 +1,17 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+module.exports = {
+ attachRefToHud: () => {},
+ emitNewMessage: () => {},
+ hudProxyClient: {},
+ onViewSourceInDebugger: () => {},
+ openNetworkPanel: () => {},
+ sourceMapService: {
+ subscribe: () => {},
+ },
+ openLink: () => {},
+ createElement: tagName => document.createElement(tagName)
+};
diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/browser.ini b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/browser.ini
new file mode 100644
index 000000000..9f348544f
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/browser.ini
@@ -0,0 +1,18 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ head.js
+ !/devtools/client/framework/test/shared-head.js
+ test-console-api.html
+ test-network-event.html
+ test-tempfile.js
+
+[browser_webconsole_update_stubs_console_api.js]
+skip-if=true # This is only used to update stubs. It is not an actual test.
+[browser_webconsole_update_stubs_evaluation_result.js]
+skip-if=true # This is only used to update stubs. It is not an actual test.
+[browser_webconsole_update_stubs_network_event.js]
+skip-if=true # This is only used to update stubs. It is not an actual test.
+[browser_webconsole_update_stubs_page_error.js]
+skip-if=true # This is only used to update stubs. It is not an actual test.
diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/browser_webconsole_update_stubs_console_api.js b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/browser_webconsole_update_stubs_console_api.js
new file mode 100644
index 000000000..fc859a002
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/browser_webconsole_update_stubs_console_api.js
@@ -0,0 +1,56 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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";
+requestLongerTimeout(2)
+
+Cu.import("resource://gre/modules/osfile.jsm");
+const { consoleApi: snippets } = require("devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/stub-snippets.js");
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html";
+
+let stubs = {
+ preparedMessages: [],
+ packets: [],
+};
+
+add_task(function* () {
+ for (var [key, {keys, code}] of snippets) {
+ yield OS.File.writeAtomic(TEMP_FILE_PATH, `function triggerPacket() {${code}}`);
+
+ let toolbox = yield openNewTabAndToolbox(TEST_URI, "webconsole");
+ let {ui} = toolbox.getCurrentPanel().hud;
+
+ ok(ui.jsterm, "jsterm exists");
+ ok(ui.newConsoleOutput, "newConsoleOutput exists");
+
+ let received = new Promise(resolve => {
+ let i = 0;
+ let listener = (type, res) => {
+ stubs.packets.push(formatPacket(keys[i], res));
+ stubs.preparedMessages.push(formatStub(keys[i], res));
+ if(++i === keys.length ){
+ toolbox.target.client.removeListener("consoleAPICall", listener);
+ resolve();
+ }
+ };
+ toolbox.target.client.addListener("consoleAPICall", listener);
+ });
+
+ yield ContentTask.spawn(gBrowser.selectedBrowser, key, function(key) {
+ var script = content.document.createElement("script");
+ script.src = "test-tempfile.js?key=" + encodeURIComponent(key);
+ script.onload = function() { content.wrappedJSObject.triggerPacket(); }
+ content.document.body.appendChild(script);
+ });
+
+ yield received;
+
+ yield closeTabAndToolbox();
+ }
+ let filePath = OS.Path.join(`${BASE_PATH}/stubs`, "consoleApi.js");
+ OS.File.writeAtomic(filePath, formatFile(stubs));
+ OS.File.writeAtomic(TEMP_FILE_PATH, "");
+});
diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/browser_webconsole_update_stubs_evaluation_result.js b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/browser_webconsole_update_stubs_evaluation_result.js
new file mode 100644
index 000000000..507201a24
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/browser_webconsole_update_stubs_evaluation_result.js
@@ -0,0 +1,32 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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";
+
+Cu.import("resource://gre/modules/osfile.jsm");
+const TEST_URI = "data:text/html;charset=utf-8,stub generation";
+
+const { evaluationResult: snippets} = require("devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/stub-snippets.js");
+
+let stubs = {
+ preparedMessages: [],
+ packets: [],
+};
+
+add_task(function* () {
+ let toolbox = yield openNewTabAndToolbox(TEST_URI, "webconsole");
+ ok(true, "make the test not fail");
+
+ for (var [code,key] of snippets) {
+ const packet = yield new Promise(resolve => {
+ toolbox.target.activeConsole.evaluateJS(code, resolve);
+ });
+ stubs.packets.push(formatPacket(key, packet));
+ stubs.preparedMessages.push(formatStub(key, packet));
+ }
+
+ let filePath = OS.Path.join(`${BASE_PATH}/stubs`, "evaluationResult.js");
+ OS.File.writeAtomic(filePath, formatFile(stubs));
+});
diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/browser_webconsole_update_stubs_network_event.js b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/browser_webconsole_update_stubs_network_event.js
new file mode 100644
index 000000000..cc018f634
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/browser_webconsole_update_stubs_network_event.js
@@ -0,0 +1,47 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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";
+
+Cu.import("resource://gre/modules/osfile.jsm");
+const TARGET = "networkEvent";
+const { [TARGET]: snippets } = require("devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/stub-snippets.js");
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-network-event.html";
+
+let stubs = {
+ preparedMessages: [],
+ packets: [],
+};
+
+add_task(function* () {
+ for (var [key, {keys, code}] of snippets) {
+ OS.File.writeAtomic(TEMP_FILE_PATH, `function triggerPacket() {${code}}`);
+ let toolbox = yield openNewTabAndToolbox(TEST_URI, "webconsole");
+ let {ui} = toolbox.getCurrentPanel().hud;
+
+ ok(ui.jsterm, "jsterm exists");
+ ok(ui.newConsoleOutput, "newConsoleOutput exists");
+
+ let received = new Promise(resolve => {
+ let i = 0;
+ toolbox.target.client.addListener(TARGET, (type, res) => {
+ stubs.packets.push(formatPacket(keys[i], res));
+ stubs.preparedMessages.push(formatNetworkStub(keys[i], res));
+ if(++i === keys.length ){
+ resolve();
+ }
+ });
+ });
+
+ yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function() {
+ content.wrappedJSObject.triggerPacket();
+ });
+
+ yield received;
+ }
+ let filePath = OS.Path.join(`${BASE_PATH}/stubs/${TARGET}.js`);
+ OS.File.writeAtomic(filePath, formatFile(stubs));
+ OS.File.writeAtomic(TEMP_FILE_PATH, "");
+});
diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/browser_webconsole_update_stubs_page_error.js b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/browser_webconsole_update_stubs_page_error.js
new file mode 100644
index 000000000..9323e0031
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/browser_webconsole_update_stubs_page_error.js
@@ -0,0 +1,48 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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";
+
+Cu.import("resource://gre/modules/osfile.jsm");
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html";
+
+const { pageError: snippets} = require("devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/stub-snippets.js");
+
+let stubs = {
+ preparedMessages: [],
+ packets: [],
+};
+
+add_task(function* () {
+ let toolbox = yield openNewTabAndToolbox(TEST_URI, "webconsole");
+ ok(true, "make the test not fail");
+
+ for (var [key,code] of snippets) {
+ OS.File.writeAtomic(TEMP_FILE_PATH, `${code}`);
+ let received = new Promise(resolve => {
+ toolbox.target.client.addListener("pageError", function onPacket(e, packet) {
+ toolbox.target.client.removeListener("pageError", onPacket);
+ info("Received page error:" + e + " " + JSON.stringify(packet, null, "\t"));
+
+ let message = prepareMessage(packet, {getNextId: () => 1});
+ stubs.packets.push(formatPacket(message.messageText, packet));
+ stubs.preparedMessages.push(formatStub(message.messageText, packet));
+ resolve();
+ });
+ });
+
+ yield ContentTask.spawn(gBrowser.selectedBrowser, key, function(key) {
+ var script = content.document.createElement("script");
+ script.src = "test-tempfile.js?key=" + encodeURIComponent(key);
+ content.document.body.appendChild(script);
+ });
+
+ yield received;
+ }
+
+ let filePath = OS.Path.join(`${BASE_PATH}/stubs`, "pageError.js");
+ OS.File.writeAtomic(filePath, formatFile(stubs));
+ OS.File.writeAtomic(TEMP_FILE_PATH, "");
+});
diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/head.js b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/head.js
new file mode 100644
index 000000000..be988b9d8
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/head.js
@@ -0,0 +1,192 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+/* import-globals-from ../../../../framework/test/shared-head.js */
+
+"use strict";
+
+// shared-head.js handles imports, constants, and utility functions
+// Load the shared-head file first.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js",
+ this);
+
+Services.prefs.setBoolPref("devtools.webconsole.new-frontend-enabled", true);
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("devtools.webconsole.new-frontend-enabled");
+});
+
+const { prepareMessage } = require("devtools/client/webconsole/new-console-output/utils/messages");
+const { stubPackets } = require("devtools/client/webconsole/new-console-output/test/fixtures/stubs/index.js");
+
+const BASE_PATH = "../../../../devtools/client/webconsole/new-console-output/test/fixtures";
+const TEMP_FILE_PATH = OS.Path.join(`${BASE_PATH}/stub-generators`, "test-tempfile.js");
+
+let cachedPackets = {};
+
+function getCleanedPacket(key, packet) {
+ if(Object.keys(cachedPackets).includes(key)) {
+ return cachedPackets[key];
+ }
+
+ // Strip escaped characters.
+ let safeKey = key
+ .replace(/\\n/g, "\n")
+ .replace(/\\r/g, "\r")
+ .replace(/\\\"/g, `\"`)
+ .replace(/\\\'/g, `\'`);
+
+ // If the stub already exist, we want to ignore irrelevant properties
+ // (actor, timeStamp, timer, ...) that might changed and "pollute"
+ // the diff resulting from this stub generation.
+ let res;
+ if(stubPackets.has(safeKey)) {
+
+ let existingPacket = stubPackets.get(safeKey);
+ res = Object.assign({}, packet, {
+ from: existingPacket.from
+ });
+
+ // Clean root timestamp.
+ if(res.timestamp) {
+ res.timestamp = existingPacket.timestamp;
+ }
+
+ if (res.message) {
+ // Clean timeStamp on the message prop.
+ res.message.timeStamp = existingPacket.message.timeStamp;
+ if (res.message.timer) {
+ // Clean timer properties on the message.
+ // Those properties are found on console.time and console.timeEnd calls,
+ // and those time can vary, which is why we need to clean them.
+ if (res.message.timer.started) {
+ res.message.timer.started = existingPacket.message.timer.started;
+ }
+ if (res.message.timer.duration) {
+ res.message.timer.duration = existingPacket.message.timer.duration;
+ }
+ }
+
+ if(Array.isArray(res.message.arguments)) {
+ // Clean actor ids on each message.arguments item.
+ res.message.arguments.forEach((argument, i) => {
+ if (argument && argument.actor) {
+ argument.actor = existingPacket.message.arguments[i].actor;
+ }
+ });
+ }
+ }
+
+ if (res.result) {
+ // Clean actor ids on evaluation result messages.
+ res.result.actor = existingPacket.result.actor;
+ if (res.result.preview) {
+ if(res.result.preview.timestamp) {
+ // Clean timestamp there too.
+ res.result.preview.timestamp = existingPacket.result.preview.timestamp;
+ }
+ }
+ }
+
+ if (res.exception) {
+ // Clean actor ids on exception messages.
+ res.exception.actor = existingPacket.exception.actor;
+ if (res.exception.preview) {
+ if(res.exception.preview.timestamp) {
+ // Clean timestamp there too.
+ res.exception.preview.timestamp = existingPacket.exception.preview.timestamp;
+ }
+ }
+ }
+
+ if (res.eventActor) {
+ // Clean actor ids, timeStamp and startedDateTime on network messages.
+ res.eventActor.actor = existingPacket.eventActor.actor;
+ res.eventActor.startedDateTime = existingPacket.eventActor.startedDateTime;
+ res.eventActor.timeStamp = existingPacket.eventActor.timeStamp;
+ }
+
+ if (res.pageError) {
+ // Clean timeStamp on pageError messages.
+ res.pageError.timeStamp = existingPacket.pageError.timeStamp;
+ }
+
+ } else {
+ res = packet;
+ }
+
+ cachedPackets[key] = res;
+ return res;
+}
+
+function formatPacket(key, packet) {
+ return `
+stubPackets.set("${key}", ${JSON.stringify(getCleanedPacket(key, packet), null, "\t")});
+`;
+}
+
+function formatStub(key, packet) {
+ let prepared = prepareMessage(
+ getCleanedPacket(key, packet),
+ {getNextId: () => "1"}
+ );
+
+ return `
+stubPreparedMessages.set("${key}", new ConsoleMessage(${JSON.stringify(prepared, null, "\t")}));
+`;
+}
+
+function formatNetworkStub(key, packet) {
+ let actor = packet.eventActor;
+ let networkInfo = {
+ _type: "NetworkEvent",
+ timeStamp: actor.timeStamp,
+ node: null,
+ actor: actor.actor,
+ discardRequestBody: true,
+ discardResponseBody: true,
+ startedDateTime: actor.startedDateTime,
+ request: {
+ url: actor.url,
+ method: actor.method,
+ },
+ isXHR: actor.isXHR,
+ cause: actor.cause,
+ response: {},
+ timings: {},
+ // track the list of network event updates
+ updates: [],
+ private: actor.private,
+ fromCache: actor.fromCache,
+ fromServiceWorker: actor.fromServiceWorker
+ };
+ let prepared = prepareMessage(networkInfo, {getNextId: () => "1"});
+ return `
+stubPreparedMessages.set("${key}", new NetworkEventMessage(${JSON.stringify(prepared, null, "\t")}));
+`;
+}
+
+function formatFile(stubs) {
+ return `/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/*
+ * THIS FILE IS AUTOGENERATED. DO NOT MODIFY BY HAND. RUN TESTS IN FIXTURES/ TO UPDATE.
+ */
+
+const { ConsoleMessage, NetworkEventMessage } = require("devtools/client/webconsole/new-console-output/types");
+
+let stubPreparedMessages = new Map();
+let stubPackets = new Map();
+
+${stubs.preparedMessages.join("")}
+${stubs.packets.join("")}
+
+module.exports = {
+ stubPreparedMessages,
+ stubPackets,
+}`;
+}
diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/moz.build b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/moz.build
new file mode 100644
index 000000000..4b4e8a1d8
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/moz.build
@@ -0,0 +1,8 @@
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'stub-snippets.js',
+)
diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/stub-snippets.js b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/stub-snippets.js
new file mode 100644
index 000000000..f79548e7b
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/stub-snippets.js
@@ -0,0 +1,148 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var {DebuggerServer} = require("devtools/server/main");
+var longString = (new Array(DebuggerServer.LONG_STRING_LENGTH + 4)).join("a");
+var initialString = longString.substring(0, DebuggerServer.LONG_STRING_INITIAL_LENGTH);
+
+// Console API
+
+const consoleApiCommands = [
+ "console.log('foobar', 'test')",
+ "console.log(undefined)",
+ "console.warn('danger, will robinson!')",
+ "console.log(NaN)",
+ "console.log(null)",
+ "console.log('\u9f2c')",
+ "console.clear()",
+ "console.count('bar')",
+ "console.assert(false, {message: 'foobar'})",
+ "console.log('hello \\nfrom \\rthe \\\"string world!')",
+ "console.log('\xFA\u1E47\u0129\xE7\xF6d\xEA \u021B\u0115\u0219\u0165')",
+ "console.dirxml(window)",
+];
+
+let consoleApi = new Map(consoleApiCommands.map(
+ cmd => [cmd, {keys: [cmd], code: cmd}]));
+
+consoleApi.set("console.trace()", {
+ keys: ["console.trace()"],
+ code: `
+function testStacktraceFiltering() {
+ console.trace()
+}
+function foo() {
+ testStacktraceFiltering()
+}
+
+foo()
+`});
+
+consoleApi.set("console.time('bar')", {
+ keys: ["console.time('bar')", "console.timeEnd('bar')"],
+ code: `
+console.time("bar");
+console.timeEnd("bar");
+`});
+
+consoleApi.set("console.table('bar')", {
+ keys: ["console.table('bar')"],
+ code: `
+console.table('bar');
+`});
+
+consoleApi.set("console.table(['a', 'b', 'c'])", {
+ keys: ["console.table(['a', 'b', 'c'])"],
+ code: `
+console.table(['a', 'b', 'c']);
+`});
+
+consoleApi.set("console.group('bar')", {
+ keys: ["console.group('bar')", "console.groupEnd('bar')"],
+ code: `
+console.group("bar");
+console.groupEnd("bar");
+`});
+
+consoleApi.set("console.groupCollapsed('foo')", {
+ keys: ["console.groupCollapsed('foo')", "console.groupEnd('foo')"],
+ code: `
+console.groupCollapsed("foo");
+console.groupEnd("foo");
+`});
+
+consoleApi.set("console.group()", {
+ keys: ["console.group()", "console.groupEnd()"],
+ code: `
+console.group();
+console.groupEnd();
+`});
+
+consoleApi.set("console.log(%cfoobar)", {
+ keys: ["console.log(%cfoobar)"],
+ code: `
+console.log(
+ "%cfoo%cbar",
+ "color:blue;font-size:1.3em;background:url('http://example.com/test');position:absolute;top:10px",
+ "color:red;background:\\165rl('http://example.com/test')");
+`});
+
+// Evaluation Result
+const evaluationResultCommands = [
+ "new Date(0)",
+ "asdf()",
+ "1 + @"
+];
+
+let evaluationResult = new Map(evaluationResultCommands.map(cmd => [cmd, cmd]));
+
+// Network Event
+
+let networkEvent = new Map();
+
+networkEvent.set("GET request", {
+ keys: ["GET request"],
+ code: `
+let i = document.createElement("img");
+i.src = "inexistent.html";
+`});
+
+networkEvent.set("XHR GET request", {
+ keys: ["XHR GET request"],
+ code: `
+const xhr = new XMLHttpRequest();
+xhr.open("GET", "inexistent.html");
+xhr.send();
+`});
+
+networkEvent.set("XHR POST request", {
+ keys: ["XHR POST request"],
+ code: `
+const xhr = new XMLHttpRequest();
+xhr.open("POST", "inexistent.html");
+xhr.send();
+`});
+
+// Page Error
+
+let pageError = new Map();
+
+pageError.set("Reference Error", `
+ function bar() {
+ asdf()
+ }
+ function foo() {
+ bar()
+ }
+
+ foo()
+`);
+
+module.exports = {
+ consoleApi,
+ evaluationResult,
+ networkEvent,
+ pageError,
+};
diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html
new file mode 100644
index 000000000..3246cff15
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>Stub generator</title>
+ </head>
+ <body>
+ <p>Stub generator</p>
+ <script src="test-tempfile.js"></script>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-network-event.html b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-network-event.html
new file mode 100644
index 000000000..c234acea6
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-network-event.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>Stub generator for network event</title>
+ </head>
+ <body>
+ <p>Stub generator for network event</p>
+ <script src="test-tempfile.js"></script>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js
diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/stubs/consoleApi.js b/devtools/client/webconsole/new-console-output/test/fixtures/stubs/consoleApi.js
new file mode 100644
index 000000000..26e95fe39
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/stubs/consoleApi.js
@@ -0,0 +1,1482 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/*
+ * THIS FILE IS AUTOGENERATED. DO NOT MODIFY BY HAND. RUN TESTS IN FIXTURES/ TO UPDATE.
+ */
+
+const { ConsoleMessage, NetworkEventMessage } = require("devtools/client/webconsole/new-console-output/types");
+
+let stubPreparedMessages = new Map();
+let stubPackets = new Map();
+
+
+stubPreparedMessages.set("console.log('foobar', 'test')", new ConsoleMessage({
+ "id": "1",
+ "allowRepeating": true,
+ "source": "console-api",
+ "type": "log",
+ "level": "log",
+ "messageText": null,
+ "parameters": [
+ "foobar",
+ "test"
+ ],
+ "repeat": 1,
+ "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"log\",\"level\":\"log\",\"messageText\":null,\"parameters\":[\"foobar\",\"test\"],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(%27foobar%27%2C%20%27test%27)\",\"line\":1,\"column\":27},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}",
+ "stacktrace": null,
+ "frame": {
+ "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(%27foobar%27%2C%20%27test%27)",
+ "line": 1,
+ "column": 27
+ },
+ "groupId": null,
+ "exceptionDocURL": null,
+ "userProvidedStyles": []
+}));
+
+stubPreparedMessages.set("console.log(undefined)", new ConsoleMessage({
+ "id": "1",
+ "allowRepeating": true,
+ "source": "console-api",
+ "type": "log",
+ "level": "log",
+ "messageText": null,
+ "parameters": [
+ {
+ "type": "undefined"
+ }
+ ],
+ "repeat": 1,
+ "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"log\",\"level\":\"log\",\"messageText\":null,\"parameters\":[{\"type\":\"undefined\"}],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(undefined)\",\"line\":1,\"column\":27},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}",
+ "stacktrace": null,
+ "frame": {
+ "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(undefined)",
+ "line": 1,
+ "column": 27
+ },
+ "groupId": null,
+ "exceptionDocURL": null,
+ "userProvidedStyles": []
+}));
+
+stubPreparedMessages.set("console.warn('danger, will robinson!')", new ConsoleMessage({
+ "id": "1",
+ "allowRepeating": true,
+ "source": "console-api",
+ "type": "warn",
+ "level": "warn",
+ "messageText": null,
+ "parameters": [
+ "danger, will robinson!"
+ ],
+ "repeat": 1,
+ "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"warn\",\"level\":\"warn\",\"messageText\":null,\"parameters\":[\"danger, will robinson!\"],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.warn(%27danger%2C%20will%20robinson!%27)\",\"line\":1,\"column\":27},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}",
+ "stacktrace": null,
+ "frame": {
+ "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.warn(%27danger%2C%20will%20robinson!%27)",
+ "line": 1,
+ "column": 27
+ },
+ "groupId": null,
+ "exceptionDocURL": null,
+ "userProvidedStyles": []
+}));
+
+stubPreparedMessages.set("console.log(NaN)", new ConsoleMessage({
+ "id": "1",
+ "allowRepeating": true,
+ "source": "console-api",
+ "type": "log",
+ "level": "log",
+ "messageText": null,
+ "parameters": [
+ {
+ "type": "NaN"
+ }
+ ],
+ "repeat": 1,
+ "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"log\",\"level\":\"log\",\"messageText\":null,\"parameters\":[{\"type\":\"NaN\"}],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(NaN)\",\"line\":1,\"column\":27},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}",
+ "stacktrace": null,
+ "frame": {
+ "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(NaN)",
+ "line": 1,
+ "column": 27
+ },
+ "groupId": null,
+ "exceptionDocURL": null,
+ "userProvidedStyles": []
+}));
+
+stubPreparedMessages.set("console.log(null)", new ConsoleMessage({
+ "id": "1",
+ "allowRepeating": true,
+ "source": "console-api",
+ "type": "log",
+ "level": "log",
+ "messageText": null,
+ "parameters": [
+ {
+ "type": "null"
+ }
+ ],
+ "repeat": 1,
+ "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"log\",\"level\":\"log\",\"messageText\":null,\"parameters\":[{\"type\":\"null\"}],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(null)\",\"line\":1,\"column\":27},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}",
+ "stacktrace": null,
+ "frame": {
+ "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(null)",
+ "line": 1,
+ "column": 27
+ },
+ "groupId": null,
+ "exceptionDocURL": null,
+ "userProvidedStyles": []
+}));
+
+stubPreparedMessages.set("console.log('鼬')", new ConsoleMessage({
+ "id": "1",
+ "allowRepeating": true,
+ "source": "console-api",
+ "type": "log",
+ "level": "log",
+ "messageText": null,
+ "parameters": [
+ "鼬"
+ ],
+ "repeat": 1,
+ "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"log\",\"level\":\"log\",\"messageText\":null,\"parameters\":[\"鼬\"],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(%27%E9%BC%AC%27)\",\"line\":1,\"column\":27},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}",
+ "stacktrace": null,
+ "frame": {
+ "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(%27%E9%BC%AC%27)",
+ "line": 1,
+ "column": 27
+ },
+ "groupId": null,
+ "exceptionDocURL": null,
+ "userProvidedStyles": []
+}));
+
+stubPreparedMessages.set("console.clear()", new ConsoleMessage({
+ "id": "1",
+ "allowRepeating": true,
+ "source": "console-api",
+ "type": "clear",
+ "level": "log",
+ "messageText": null,
+ "parameters": [
+ "Console was cleared."
+ ],
+ "repeat": 1,
+ "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"clear\",\"level\":\"log\",\"messageText\":null,\"parameters\":[\"Console was cleared.\"],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.clear()\",\"line\":1,\"column\":27},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}",
+ "stacktrace": null,
+ "frame": {
+ "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.clear()",
+ "line": 1,
+ "column": 27
+ },
+ "groupId": null,
+ "exceptionDocURL": null,
+ "userProvidedStyles": []
+}));
+
+stubPreparedMessages.set("console.count('bar')", new ConsoleMessage({
+ "id": "1",
+ "allowRepeating": true,
+ "source": "console-api",
+ "type": "log",
+ "level": "debug",
+ "messageText": "bar: 1",
+ "parameters": null,
+ "repeat": 1,
+ "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"log\",\"level\":\"debug\",\"messageText\":\"bar: 1\",\"parameters\":null,\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.count(%27bar%27)\",\"line\":1,\"column\":27},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}",
+ "stacktrace": null,
+ "frame": {
+ "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.count(%27bar%27)",
+ "line": 1,
+ "column": 27
+ },
+ "groupId": null,
+ "exceptionDocURL": null,
+ "userProvidedStyles": []
+}));
+
+stubPreparedMessages.set("console.assert(false, {message: 'foobar'})", new ConsoleMessage({
+ "id": "1",
+ "allowRepeating": true,
+ "source": "console-api",
+ "type": "assert",
+ "level": "error",
+ "messageText": null,
+ "parameters": [
+ {
+ "type": "object",
+ "actor": "server1.conn8.child1/obj31",
+ "class": "Object",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 1,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {
+ "message": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": "foobar"
+ }
+ },
+ "ownPropertiesLength": 1,
+ "safeGetterValues": {}
+ }
+ }
+ ],
+ "repeat": 1,
+ "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"assert\",\"level\":\"error\",\"messageText\":null,\"parameters\":[{\"type\":\"object\",\"actor\":\"server1.conn8.child1/obj31\",\"class\":\"Object\",\"extensible\":true,\"frozen\":false,\"sealed\":false,\"ownPropertyLength\":1,\"preview\":{\"kind\":\"Object\",\"ownProperties\":{\"message\":{\"configurable\":true,\"enumerable\":true,\"writable\":true,\"value\":\"foobar\"}},\"ownPropertiesLength\":1,\"safeGetterValues\":{}}}],\"repeatId\":null,\"stacktrace\":[{\"columnNumber\":27,\"filename\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.assert(false%2C%20%7Bmessage%3A%20%27foobar%27%7D)\",\"functionName\":\"triggerPacket\",\"language\":2,\"lineNumber\":1}],\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.assert(false%2C%20%7Bmessage%3A%20%27foobar%27%7D)\",\"line\":1,\"column\":27},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}",
+ "stacktrace": [
+ {
+ "columnNumber": 27,
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.assert(false%2C%20%7Bmessage%3A%20%27foobar%27%7D)",
+ "functionName": "triggerPacket",
+ "language": 2,
+ "lineNumber": 1
+ }
+ ],
+ "frame": {
+ "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.assert(false%2C%20%7Bmessage%3A%20%27foobar%27%7D)",
+ "line": 1,
+ "column": 27
+ },
+ "groupId": null,
+ "exceptionDocURL": null,
+ "userProvidedStyles": []
+}));
+
+stubPreparedMessages.set("console.log('hello \nfrom \rthe \"string world!')", new ConsoleMessage({
+ "id": "1",
+ "allowRepeating": true,
+ "source": "console-api",
+ "type": "log",
+ "level": "log",
+ "messageText": null,
+ "parameters": [
+ "hello \nfrom \rthe \"string world!"
+ ],
+ "repeat": 1,
+ "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"log\",\"level\":\"log\",\"messageText\":null,\"parameters\":[\"hello \\nfrom \\rthe \\\"string world!\"],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(%27hello%20%5Cnfrom%20%5Crthe%20%5C%22string%20world!%27)\",\"line\":1,\"column\":27},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}",
+ "stacktrace": null,
+ "frame": {
+ "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(%27hello%20%5Cnfrom%20%5Crthe%20%5C%22string%20world!%27)",
+ "line": 1,
+ "column": 27
+ },
+ "groupId": null,
+ "exceptionDocURL": null,
+ "userProvidedStyles": []
+}));
+
+stubPreparedMessages.set("console.log('úṇĩçödê țĕșť')", new ConsoleMessage({
+ "id": "1",
+ "allowRepeating": true,
+ "source": "console-api",
+ "type": "log",
+ "level": "log",
+ "messageText": null,
+ "parameters": [
+ "úṇĩçödê țĕșť"
+ ],
+ "repeat": 1,
+ "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"log\",\"level\":\"log\",\"messageText\":null,\"parameters\":[\"úṇĩçödê țĕșť\"],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(%27%C3%BA%E1%B9%87%C4%A9%C3%A7%C3%B6d%C3%AA%20%C8%9B%C4%95%C8%99%C5%A5%27)\",\"line\":1,\"column\":27},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}",
+ "stacktrace": null,
+ "frame": {
+ "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(%27%C3%BA%E1%B9%87%C4%A9%C3%A7%C3%B6d%C3%AA%20%C8%9B%C4%95%C8%99%C5%A5%27)",
+ "line": 1,
+ "column": 27
+ },
+ "groupId": null,
+ "exceptionDocURL": null,
+ "userProvidedStyles": []
+}));
+
+stubPreparedMessages.set("console.dirxml(window)", new ConsoleMessage({
+ "id": "1",
+ "allowRepeating": true,
+ "source": "console-api",
+ "type": "log",
+ "level": "log",
+ "messageText": null,
+ "parameters": [
+ {
+ "type": "object",
+ "actor": "server1.conn11.child1/obj31",
+ "class": "Window",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 804,
+ "preview": {
+ "kind": "ObjectWithURL",
+ "url": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html"
+ }
+ }
+ ],
+ "repeat": 1,
+ "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"log\",\"level\":\"log\",\"messageText\":null,\"parameters\":[{\"type\":\"object\",\"actor\":\"server1.conn11.child1/obj31\",\"class\":\"Window\",\"extensible\":true,\"frozen\":false,\"sealed\":false,\"ownPropertyLength\":804,\"preview\":{\"kind\":\"ObjectWithURL\",\"url\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\"}}],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.dirxml(window)\",\"line\":1,\"column\":27},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}",
+ "stacktrace": null,
+ "frame": {
+ "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.dirxml(window)",
+ "line": 1,
+ "column": 27
+ },
+ "groupId": null,
+ "exceptionDocURL": null,
+ "userProvidedStyles": []
+}));
+
+stubPreparedMessages.set("console.trace()", new ConsoleMessage({
+ "id": "1",
+ "allowRepeating": true,
+ "source": "console-api",
+ "type": "trace",
+ "level": "log",
+ "messageText": null,
+ "parameters": [],
+ "repeat": 1,
+ "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"trace\",\"level\":\"log\",\"messageText\":null,\"parameters\":[],\"repeatId\":null,\"stacktrace\":[{\"columnNumber\":3,\"filename\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.trace()\",\"functionName\":\"testStacktraceFiltering\",\"language\":2,\"lineNumber\":3},{\"columnNumber\":3,\"filename\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.trace()\",\"functionName\":\"foo\",\"language\":2,\"lineNumber\":6},{\"columnNumber\":1,\"filename\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.trace()\",\"functionName\":\"triggerPacket\",\"language\":2,\"lineNumber\":9}],\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.trace()\",\"line\":3,\"column\":3},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}",
+ "stacktrace": [
+ {
+ "columnNumber": 3,
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.trace()",
+ "functionName": "testStacktraceFiltering",
+ "language": 2,
+ "lineNumber": 3
+ },
+ {
+ "columnNumber": 3,
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.trace()",
+ "functionName": "foo",
+ "language": 2,
+ "lineNumber": 6
+ },
+ {
+ "columnNumber": 1,
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.trace()",
+ "functionName": "triggerPacket",
+ "language": 2,
+ "lineNumber": 9
+ }
+ ],
+ "frame": {
+ "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.trace()",
+ "line": 3,
+ "column": 3
+ },
+ "groupId": null,
+ "exceptionDocURL": null,
+ "userProvidedStyles": []
+}));
+
+stubPreparedMessages.set("console.time('bar')", new ConsoleMessage({
+ "id": "1",
+ "allowRepeating": true,
+ "source": "console-api",
+ "type": "nullMessage",
+ "level": "log",
+ "messageText": null,
+ "parameters": null,
+ "repeat": 1,
+ "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"nullMessage\",\"level\":\"log\",\"messageText\":null,\"parameters\":null,\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.time(%27bar%27)\",\"line\":2,\"column\":1},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}",
+ "stacktrace": null,
+ "frame": {
+ "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.time(%27bar%27)",
+ "line": 2,
+ "column": 1
+ },
+ "groupId": null,
+ "exceptionDocURL": null,
+ "userProvidedStyles": []
+}));
+
+stubPreparedMessages.set("console.timeEnd('bar')", new ConsoleMessage({
+ "id": "1",
+ "allowRepeating": true,
+ "source": "console-api",
+ "type": "timeEnd",
+ "level": "log",
+ "messageText": "bar: 1.36ms",
+ "parameters": null,
+ "repeat": 1,
+ "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"timeEnd\",\"level\":\"log\",\"messageText\":\"bar: 1.36ms\",\"parameters\":null,\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.time(%27bar%27)\",\"line\":3,\"column\":1},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}",
+ "stacktrace": null,
+ "frame": {
+ "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.time(%27bar%27)",
+ "line": 3,
+ "column": 1
+ },
+ "groupId": null,
+ "exceptionDocURL": null,
+ "userProvidedStyles": []
+}));
+
+stubPreparedMessages.set("console.table('bar')", new ConsoleMessage({
+ "id": "1",
+ "allowRepeating": true,
+ "source": "console-api",
+ "type": "log",
+ "level": "log",
+ "messageText": null,
+ "parameters": [
+ "bar"
+ ],
+ "repeat": 1,
+ "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"log\",\"level\":\"log\",\"messageText\":null,\"parameters\":[\"bar\"],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.table(%27bar%27)\",\"line\":2,\"column\":1},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}",
+ "stacktrace": null,
+ "frame": {
+ "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.table(%27bar%27)",
+ "line": 2,
+ "column": 1
+ },
+ "groupId": null,
+ "exceptionDocURL": null,
+ "userProvidedStyles": []
+}));
+
+stubPreparedMessages.set("console.table(['a', 'b', 'c'])", new ConsoleMessage({
+ "id": "1",
+ "allowRepeating": true,
+ "source": "console-api",
+ "type": "table",
+ "level": "log",
+ "messageText": null,
+ "parameters": [
+ {
+ "type": "object",
+ "actor": "server1.conn15.child1/obj31",
+ "class": "Array",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 4,
+ "preview": {
+ "kind": "ArrayLike",
+ "length": 3,
+ "items": [
+ "a",
+ "b",
+ "c"
+ ]
+ }
+ }
+ ],
+ "repeat": 1,
+ "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"table\",\"level\":\"log\",\"messageText\":null,\"parameters\":[{\"type\":\"object\",\"actor\":\"server1.conn15.child1/obj31\",\"class\":\"Array\",\"extensible\":true,\"frozen\":false,\"sealed\":false,\"ownPropertyLength\":4,\"preview\":{\"kind\":\"ArrayLike\",\"length\":3,\"items\":[\"a\",\"b\",\"c\"]}}],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.table(%5B%27a%27%2C%20%27b%27%2C%20%27c%27%5D)\",\"line\":2,\"column\":1},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}",
+ "stacktrace": null,
+ "frame": {
+ "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.table(%5B%27a%27%2C%20%27b%27%2C%20%27c%27%5D)",
+ "line": 2,
+ "column": 1
+ },
+ "groupId": null,
+ "exceptionDocURL": null,
+ "userProvidedStyles": []
+}));
+
+stubPreparedMessages.set("console.group('bar')", new ConsoleMessage({
+ "id": "1",
+ "allowRepeating": true,
+ "source": "console-api",
+ "type": "startGroup",
+ "level": "log",
+ "messageText": "bar",
+ "parameters": null,
+ "repeat": 1,
+ "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"startGroup\",\"level\":\"log\",\"messageText\":\"bar\",\"parameters\":null,\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.group(%27bar%27)\",\"line\":2,\"column\":1},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}",
+ "stacktrace": null,
+ "frame": {
+ "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.group(%27bar%27)",
+ "line": 2,
+ "column": 1
+ },
+ "groupId": null,
+ "exceptionDocURL": null,
+ "userProvidedStyles": []
+}));
+
+stubPreparedMessages.set("console.groupEnd('bar')", new ConsoleMessage({
+ "id": "1",
+ "allowRepeating": true,
+ "source": "console-api",
+ "type": "endGroup",
+ "level": "log",
+ "messageText": null,
+ "parameters": null,
+ "repeat": 1,
+ "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"endGroup\",\"level\":\"log\",\"messageText\":null,\"parameters\":null,\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.group(%27bar%27)\",\"line\":3,\"column\":1},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}",
+ "stacktrace": null,
+ "frame": {
+ "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.group(%27bar%27)",
+ "line": 3,
+ "column": 1
+ },
+ "groupId": null,
+ "exceptionDocURL": null,
+ "userProvidedStyles": []
+}));
+
+stubPreparedMessages.set("console.groupCollapsed('foo')", new ConsoleMessage({
+ "id": "1",
+ "allowRepeating": true,
+ "source": "console-api",
+ "type": "startGroupCollapsed",
+ "level": "log",
+ "messageText": "foo",
+ "parameters": null,
+ "repeat": 1,
+ "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"startGroupCollapsed\",\"level\":\"log\",\"messageText\":\"foo\",\"parameters\":null,\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.groupCollapsed(%27foo%27)\",\"line\":2,\"column\":1},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}",
+ "stacktrace": null,
+ "frame": {
+ "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.groupCollapsed(%27foo%27)",
+ "line": 2,
+ "column": 1
+ },
+ "groupId": null,
+ "exceptionDocURL": null,
+ "userProvidedStyles": []
+}));
+
+stubPreparedMessages.set("console.groupEnd('foo')", new ConsoleMessage({
+ "id": "1",
+ "allowRepeating": true,
+ "source": "console-api",
+ "type": "endGroup",
+ "level": "log",
+ "messageText": null,
+ "parameters": null,
+ "repeat": 1,
+ "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"endGroup\",\"level\":\"log\",\"messageText\":null,\"parameters\":null,\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.groupCollapsed(%27foo%27)\",\"line\":3,\"column\":1},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}",
+ "stacktrace": null,
+ "frame": {
+ "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.groupCollapsed(%27foo%27)",
+ "line": 3,
+ "column": 1
+ },
+ "groupId": null,
+ "exceptionDocURL": null,
+ "userProvidedStyles": []
+}));
+
+stubPreparedMessages.set("console.group()", new ConsoleMessage({
+ "id": "1",
+ "allowRepeating": true,
+ "source": "console-api",
+ "type": "startGroup",
+ "level": "log",
+ "messageText": "<no group label>",
+ "parameters": null,
+ "repeat": 1,
+ "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"startGroup\",\"level\":\"log\",\"messageText\":\"<no group label>\",\"parameters\":null,\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.group()\",\"line\":2,\"column\":1},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}",
+ "stacktrace": null,
+ "frame": {
+ "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.group()",
+ "line": 2,
+ "column": 1
+ },
+ "groupId": null,
+ "exceptionDocURL": null,
+ "userProvidedStyles": []
+}));
+
+stubPreparedMessages.set("console.groupEnd()", new ConsoleMessage({
+ "id": "1",
+ "allowRepeating": true,
+ "source": "console-api",
+ "type": "endGroup",
+ "level": "log",
+ "messageText": null,
+ "parameters": null,
+ "repeat": 1,
+ "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"endGroup\",\"level\":\"log\",\"messageText\":null,\"parameters\":null,\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.group()\",\"line\":3,\"column\":1},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}",
+ "stacktrace": null,
+ "frame": {
+ "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.group()",
+ "line": 3,
+ "column": 1
+ },
+ "groupId": null,
+ "exceptionDocURL": null,
+ "userProvidedStyles": []
+}));
+
+stubPreparedMessages.set("console.log(%cfoobar)", new ConsoleMessage({
+ "id": "1",
+ "allowRepeating": true,
+ "source": "console-api",
+ "type": "log",
+ "level": "log",
+ "messageText": null,
+ "parameters": [
+ "foo",
+ "bar"
+ ],
+ "repeat": 1,
+ "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"log\",\"level\":\"log\",\"messageText\":null,\"parameters\":[\"foo\",\"bar\"],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(%25cfoobar)\",\"line\":2,\"column\":1},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[\"color:blue;font-size:1.3em;background:url('http://example.com/test');position:absolute;top:10px\",\"color:red;background:url('http://example.com/test')\"]}",
+ "stacktrace": null,
+ "frame": {
+ "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(%25cfoobar)",
+ "line": 2,
+ "column": 1
+ },
+ "groupId": null,
+ "exceptionDocURL": null,
+ "userProvidedStyles": [
+ "color:blue;font-size:1.3em;background:url('http://example.com/test');position:absolute;top:10px",
+ "color:red;background:url('http://example.com/test')"
+ ]
+}));
+
+
+stubPackets.set("console.log('foobar', 'test')", {
+ "from": "server1.conn0.child1/consoleActor2",
+ "type": "consoleAPICall",
+ "message": {
+ "arguments": [
+ "foobar",
+ "test"
+ ],
+ "columnNumber": 27,
+ "counter": null,
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(%27foobar%27%2C%20%27test%27)",
+ "functionName": "triggerPacket",
+ "groupName": "",
+ "level": "log",
+ "lineNumber": 1,
+ "originAttributes": {
+ "addonId": "",
+ "appId": 0,
+ "firstPartyDomain": "",
+ "inIsolatedMozBrowser": false,
+ "privateBrowsingId": 0,
+ "userContextId": 0
+ },
+ "private": false,
+ "styles": [],
+ "timeStamp": 1477086261590,
+ "timer": null,
+ "workerType": "none",
+ "category": "webdev"
+ }
+});
+
+stubPackets.set("console.log(undefined)", {
+ "from": "server1.conn1.child1/consoleActor2",
+ "type": "consoleAPICall",
+ "message": {
+ "arguments": [
+ {
+ "type": "undefined"
+ }
+ ],
+ "columnNumber": 27,
+ "counter": null,
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(undefined)",
+ "functionName": "triggerPacket",
+ "groupName": "",
+ "level": "log",
+ "lineNumber": 1,
+ "originAttributes": {
+ "addonId": "",
+ "appId": 0,
+ "firstPartyDomain": "",
+ "inIsolatedMozBrowser": false,
+ "privateBrowsingId": 0,
+ "userContextId": 0
+ },
+ "private": false,
+ "styles": [],
+ "timeStamp": 1477086264886,
+ "timer": null,
+ "workerType": "none",
+ "category": "webdev"
+ }
+});
+
+stubPackets.set("console.warn('danger, will robinson!')", {
+ "from": "server1.conn2.child1/consoleActor2",
+ "type": "consoleAPICall",
+ "message": {
+ "arguments": [
+ "danger, will robinson!"
+ ],
+ "columnNumber": 27,
+ "counter": null,
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.warn(%27danger%2C%20will%20robinson!%27)",
+ "functionName": "triggerPacket",
+ "groupName": "",
+ "level": "warn",
+ "lineNumber": 1,
+ "originAttributes": {
+ "addonId": "",
+ "appId": 0,
+ "firstPartyDomain": "",
+ "inIsolatedMozBrowser": false,
+ "privateBrowsingId": 0,
+ "userContextId": 0
+ },
+ "private": false,
+ "styles": [],
+ "timeStamp": 1477086267284,
+ "timer": null,
+ "workerType": "none",
+ "category": "webdev"
+ }
+});
+
+stubPackets.set("console.log(NaN)", {
+ "from": "server1.conn3.child1/consoleActor2",
+ "type": "consoleAPICall",
+ "message": {
+ "arguments": [
+ {
+ "type": "NaN"
+ }
+ ],
+ "columnNumber": 27,
+ "counter": null,
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(NaN)",
+ "functionName": "triggerPacket",
+ "groupName": "",
+ "level": "log",
+ "lineNumber": 1,
+ "originAttributes": {
+ "addonId": "",
+ "appId": 0,
+ "firstPartyDomain": "",
+ "inIsolatedMozBrowser": false,
+ "privateBrowsingId": 0,
+ "userContextId": 0
+ },
+ "private": false,
+ "styles": [],
+ "timeStamp": 1477086269484,
+ "timer": null,
+ "workerType": "none",
+ "category": "webdev"
+ }
+});
+
+stubPackets.set("console.log(null)", {
+ "from": "server1.conn4.child1/consoleActor2",
+ "type": "consoleAPICall",
+ "message": {
+ "arguments": [
+ {
+ "type": "null"
+ }
+ ],
+ "columnNumber": 27,
+ "counter": null,
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(null)",
+ "functionName": "triggerPacket",
+ "groupName": "",
+ "level": "log",
+ "lineNumber": 1,
+ "originAttributes": {
+ "addonId": "",
+ "appId": 0,
+ "firstPartyDomain": "",
+ "inIsolatedMozBrowser": false,
+ "privateBrowsingId": 0,
+ "userContextId": 0
+ },
+ "private": false,
+ "styles": [],
+ "timeStamp": 1477086271418,
+ "timer": null,
+ "workerType": "none",
+ "category": "webdev"
+ }
+});
+
+stubPackets.set("console.log('鼬')", {
+ "from": "server1.conn5.child1/consoleActor2",
+ "type": "consoleAPICall",
+ "message": {
+ "arguments": [
+ "鼬"
+ ],
+ "columnNumber": 27,
+ "counter": null,
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(%27%E9%BC%AC%27)",
+ "functionName": "triggerPacket",
+ "groupName": "",
+ "level": "log",
+ "lineNumber": 1,
+ "originAttributes": {
+ "addonId": "",
+ "appId": 0,
+ "firstPartyDomain": "",
+ "inIsolatedMozBrowser": false,
+ "privateBrowsingId": 0,
+ "userContextId": 0
+ },
+ "private": false,
+ "styles": [],
+ "timeStamp": 1477086273549,
+ "timer": null,
+ "workerType": "none",
+ "category": "webdev"
+ }
+});
+
+stubPackets.set("console.clear()", {
+ "from": "server1.conn6.child1/consoleActor2",
+ "type": "consoleAPICall",
+ "message": {
+ "arguments": [],
+ "columnNumber": 27,
+ "counter": null,
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.clear()",
+ "functionName": "triggerPacket",
+ "groupName": "",
+ "level": "clear",
+ "lineNumber": 1,
+ "originAttributes": {
+ "addonId": "",
+ "appId": 0,
+ "firstPartyDomain": "",
+ "inIsolatedMozBrowser": false,
+ "privateBrowsingId": 0,
+ "userContextId": 0
+ },
+ "private": false,
+ "timeStamp": 1477086275587,
+ "timer": null,
+ "workerType": "none",
+ "styles": [],
+ "category": "webdev"
+ }
+});
+
+stubPackets.set("console.count('bar')", {
+ "from": "server1.conn7.child1/consoleActor2",
+ "type": "consoleAPICall",
+ "message": {
+ "arguments": [
+ "bar"
+ ],
+ "columnNumber": 27,
+ "counter": {
+ "count": 1,
+ "label": "bar"
+ },
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.count(%27bar%27)",
+ "functionName": "triggerPacket",
+ "groupName": "",
+ "level": "count",
+ "lineNumber": 1,
+ "originAttributes": {
+ "addonId": "",
+ "appId": 0,
+ "firstPartyDomain": "",
+ "inIsolatedMozBrowser": false,
+ "privateBrowsingId": 0,
+ "userContextId": 0
+ },
+ "private": false,
+ "timeStamp": 1477086277812,
+ "timer": null,
+ "workerType": "none",
+ "styles": [],
+ "category": "webdev"
+ }
+});
+
+stubPackets.set("console.assert(false, {message: 'foobar'})", {
+ "from": "server1.conn8.child1/consoleActor2",
+ "type": "consoleAPICall",
+ "message": {
+ "arguments": [
+ {
+ "type": "object",
+ "actor": "server1.conn8.child1/obj31",
+ "class": "Object",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 1,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {
+ "message": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": "foobar"
+ }
+ },
+ "ownPropertiesLength": 1,
+ "safeGetterValues": {}
+ }
+ }
+ ],
+ "columnNumber": 27,
+ "counter": null,
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.assert(false%2C%20%7Bmessage%3A%20%27foobar%27%7D)",
+ "functionName": "triggerPacket",
+ "groupName": "",
+ "level": "assert",
+ "lineNumber": 1,
+ "originAttributes": {
+ "addonId": "",
+ "appId": 0,
+ "firstPartyDomain": "",
+ "inIsolatedMozBrowser": false,
+ "privateBrowsingId": 0,
+ "userContextId": 0
+ },
+ "private": false,
+ "styles": [],
+ "timeStamp": 1477086280131,
+ "timer": null,
+ "stacktrace": [
+ {
+ "columnNumber": 27,
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.assert(false%2C%20%7Bmessage%3A%20%27foobar%27%7D)",
+ "functionName": "triggerPacket",
+ "language": 2,
+ "lineNumber": 1
+ }
+ ],
+ "workerType": "none",
+ "category": "webdev"
+ }
+});
+
+stubPackets.set("console.log('hello \nfrom \rthe \"string world!')", {
+ "from": "server1.conn9.child1/consoleActor2",
+ "type": "consoleAPICall",
+ "message": {
+ "arguments": [
+ "hello \nfrom \rthe \"string world!"
+ ],
+ "columnNumber": 27,
+ "counter": null,
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(%27hello%20%5Cnfrom%20%5Crthe%20%5C%22string%20world!%27)",
+ "functionName": "triggerPacket",
+ "groupName": "",
+ "level": "log",
+ "lineNumber": 1,
+ "originAttributes": {
+ "addonId": "",
+ "appId": 0,
+ "firstPartyDomain": "",
+ "inIsolatedMozBrowser": false,
+ "privateBrowsingId": 0,
+ "userContextId": 0
+ },
+ "private": false,
+ "styles": [],
+ "timeStamp": 1477086281936,
+ "timer": null,
+ "workerType": "none",
+ "category": "webdev"
+ }
+});
+
+stubPackets.set("console.log('úṇĩçödê țĕșť')", {
+ "from": "server1.conn10.child1/consoleActor2",
+ "type": "consoleAPICall",
+ "message": {
+ "arguments": [
+ "úṇĩçödê țĕșť"
+ ],
+ "columnNumber": 27,
+ "counter": null,
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(%27%C3%BA%E1%B9%87%C4%A9%C3%A7%C3%B6d%C3%AA%20%C8%9B%C4%95%C8%99%C5%A5%27)",
+ "functionName": "triggerPacket",
+ "groupName": "",
+ "level": "log",
+ "lineNumber": 1,
+ "originAttributes": {
+ "addonId": "",
+ "appId": 0,
+ "firstPartyDomain": "",
+ "inIsolatedMozBrowser": false,
+ "privateBrowsingId": 0,
+ "userContextId": 0
+ },
+ "private": false,
+ "styles": [],
+ "timeStamp": 1477086283713,
+ "timer": null,
+ "workerType": "none",
+ "category": "webdev"
+ }
+});
+
+stubPackets.set("console.dirxml(window)", {
+ "from": "server1.conn11.child1/consoleActor2",
+ "type": "consoleAPICall",
+ "message": {
+ "arguments": [
+ {
+ "type": "object",
+ "actor": "server1.conn11.child1/obj31",
+ "class": "Window",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 804,
+ "preview": {
+ "kind": "ObjectWithURL",
+ "url": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html"
+ }
+ }
+ ],
+ "columnNumber": 27,
+ "counter": null,
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.dirxml(window)",
+ "functionName": "triggerPacket",
+ "groupName": "",
+ "level": "dirxml",
+ "lineNumber": 1,
+ "originAttributes": {
+ "addonId": "",
+ "appId": 0,
+ "firstPartyDomain": "",
+ "inIsolatedMozBrowser": false,
+ "privateBrowsingId": 0,
+ "userContextId": 0
+ },
+ "private": false,
+ "timeStamp": 1477086285483,
+ "timer": null,
+ "workerType": "none",
+ "styles": [],
+ "category": "webdev"
+ }
+});
+
+stubPackets.set("console.trace()", {
+ "from": "server1.conn12.child1/consoleActor2",
+ "type": "consoleAPICall",
+ "message": {
+ "arguments": [],
+ "columnNumber": 3,
+ "counter": null,
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.trace()",
+ "functionName": "testStacktraceFiltering",
+ "groupName": "",
+ "level": "trace",
+ "lineNumber": 3,
+ "originAttributes": {
+ "addonId": "",
+ "appId": 0,
+ "firstPartyDomain": "",
+ "inIsolatedMozBrowser": false,
+ "privateBrowsingId": 0,
+ "userContextId": 0
+ },
+ "private": false,
+ "timeStamp": 1477086287286,
+ "timer": null,
+ "stacktrace": [
+ {
+ "columnNumber": 3,
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.trace()",
+ "functionName": "testStacktraceFiltering",
+ "language": 2,
+ "lineNumber": 3
+ },
+ {
+ "columnNumber": 3,
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.trace()",
+ "functionName": "foo",
+ "language": 2,
+ "lineNumber": 6
+ },
+ {
+ "columnNumber": 1,
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.trace()",
+ "functionName": "triggerPacket",
+ "language": 2,
+ "lineNumber": 9
+ }
+ ],
+ "workerType": "none",
+ "styles": [],
+ "category": "webdev"
+ }
+});
+
+stubPackets.set("console.time('bar')", {
+ "from": "server1.conn13.child1/consoleActor2",
+ "type": "consoleAPICall",
+ "message": {
+ "arguments": [
+ "bar"
+ ],
+ "columnNumber": 1,
+ "counter": null,
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.time(%27bar%27)",
+ "functionName": "triggerPacket",
+ "groupName": "",
+ "level": "time",
+ "lineNumber": 2,
+ "originAttributes": {
+ "addonId": "",
+ "appId": 0,
+ "firstPartyDomain": "",
+ "inIsolatedMozBrowser": false,
+ "privateBrowsingId": 0,
+ "userContextId": 0
+ },
+ "private": false,
+ "timeStamp": 1477086289137,
+ "timer": {
+ "name": "bar",
+ "started": 1166.305
+ },
+ "workerType": "none",
+ "styles": [],
+ "category": "webdev"
+ }
+});
+
+stubPackets.set("console.timeEnd('bar')", {
+ "from": "server1.conn13.child1/consoleActor2",
+ "type": "consoleAPICall",
+ "message": {
+ "arguments": [
+ "bar"
+ ],
+ "columnNumber": 1,
+ "counter": null,
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.time(%27bar%27)",
+ "functionName": "triggerPacket",
+ "groupName": "",
+ "level": "timeEnd",
+ "lineNumber": 3,
+ "originAttributes": {
+ "addonId": "",
+ "appId": 0,
+ "firstPartyDomain": "",
+ "inIsolatedMozBrowser": false,
+ "privateBrowsingId": 0,
+ "userContextId": 0
+ },
+ "private": false,
+ "timeStamp": 1477086289138,
+ "timer": {
+ "duration": 1.3550000000000182,
+ "name": "bar"
+ },
+ "workerType": "none",
+ "styles": [],
+ "category": "webdev"
+ }
+});
+
+stubPackets.set("console.table('bar')", {
+ "from": "server1.conn14.child1/consoleActor2",
+ "type": "consoleAPICall",
+ "message": {
+ "arguments": [
+ "bar"
+ ],
+ "columnNumber": 1,
+ "counter": null,
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.table(%27bar%27)",
+ "functionName": "triggerPacket",
+ "groupName": "",
+ "level": "table",
+ "lineNumber": 2,
+ "originAttributes": {
+ "addonId": "",
+ "appId": 0,
+ "firstPartyDomain": "",
+ "inIsolatedMozBrowser": false,
+ "privateBrowsingId": 0,
+ "userContextId": 0
+ },
+ "private": false,
+ "timeStamp": 1477086290984,
+ "timer": null,
+ "workerType": "none",
+ "styles": [],
+ "category": "webdev"
+ }
+});
+
+stubPackets.set("console.table(['a', 'b', 'c'])", {
+ "from": "server1.conn15.child1/consoleActor2",
+ "type": "consoleAPICall",
+ "message": {
+ "arguments": [
+ {
+ "type": "object",
+ "actor": "server1.conn15.child1/obj31",
+ "class": "Array",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 4,
+ "preview": {
+ "kind": "ArrayLike",
+ "length": 3,
+ "items": [
+ "a",
+ "b",
+ "c"
+ ]
+ }
+ }
+ ],
+ "columnNumber": 1,
+ "counter": null,
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.table(%5B%27a%27%2C%20%27b%27%2C%20%27c%27%5D)",
+ "functionName": "triggerPacket",
+ "groupName": "",
+ "level": "table",
+ "lineNumber": 2,
+ "originAttributes": {
+ "addonId": "",
+ "appId": 0,
+ "firstPartyDomain": "",
+ "inIsolatedMozBrowser": false,
+ "privateBrowsingId": 0,
+ "userContextId": 0
+ },
+ "private": false,
+ "timeStamp": 1477086292762,
+ "timer": null,
+ "workerType": "none",
+ "styles": [],
+ "category": "webdev"
+ }
+});
+
+stubPackets.set("console.group('bar')", {
+ "from": "server1.conn16.child1/consoleActor2",
+ "type": "consoleAPICall",
+ "message": {
+ "arguments": [
+ "bar"
+ ],
+ "columnNumber": 1,
+ "counter": null,
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.group(%27bar%27)",
+ "functionName": "triggerPacket",
+ "groupName": "bar",
+ "level": "group",
+ "lineNumber": 2,
+ "originAttributes": {
+ "addonId": "",
+ "appId": 0,
+ "firstPartyDomain": "",
+ "inIsolatedMozBrowser": false,
+ "privateBrowsingId": 0,
+ "userContextId": 0
+ },
+ "private": false,
+ "timeStamp": 1477086294628,
+ "timer": null,
+ "workerType": "none",
+ "styles": [],
+ "category": "webdev"
+ }
+});
+
+stubPackets.set("console.groupEnd('bar')", {
+ "from": "server1.conn16.child1/consoleActor2",
+ "type": "consoleAPICall",
+ "message": {
+ "arguments": [
+ "bar"
+ ],
+ "columnNumber": 1,
+ "counter": null,
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.group(%27bar%27)",
+ "functionName": "triggerPacket",
+ "groupName": "bar",
+ "level": "groupEnd",
+ "lineNumber": 3,
+ "originAttributes": {
+ "addonId": "",
+ "appId": 0,
+ "firstPartyDomain": "",
+ "inIsolatedMozBrowser": false,
+ "privateBrowsingId": 0,
+ "userContextId": 0
+ },
+ "private": false,
+ "timeStamp": 1477086294630,
+ "timer": null,
+ "workerType": "none",
+ "styles": [],
+ "category": "webdev"
+ }
+});
+
+stubPackets.set("console.groupCollapsed('foo')", {
+ "from": "server1.conn17.child1/consoleActor2",
+ "type": "consoleAPICall",
+ "message": {
+ "arguments": [
+ "foo"
+ ],
+ "columnNumber": 1,
+ "counter": null,
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.groupCollapsed(%27foo%27)",
+ "functionName": "triggerPacket",
+ "groupName": "foo",
+ "level": "groupCollapsed",
+ "lineNumber": 2,
+ "originAttributes": {
+ "addonId": "",
+ "appId": 0,
+ "firstPartyDomain": "",
+ "inIsolatedMozBrowser": false,
+ "privateBrowsingId": 0,
+ "userContextId": 0
+ },
+ "private": false,
+ "timeStamp": 1477086296567,
+ "timer": null,
+ "workerType": "none",
+ "styles": [],
+ "category": "webdev"
+ }
+});
+
+stubPackets.set("console.groupEnd('foo')", {
+ "from": "server1.conn17.child1/consoleActor2",
+ "type": "consoleAPICall",
+ "message": {
+ "arguments": [
+ "foo"
+ ],
+ "columnNumber": 1,
+ "counter": null,
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.groupCollapsed(%27foo%27)",
+ "functionName": "triggerPacket",
+ "groupName": "foo",
+ "level": "groupEnd",
+ "lineNumber": 3,
+ "originAttributes": {
+ "addonId": "",
+ "appId": 0,
+ "firstPartyDomain": "",
+ "inIsolatedMozBrowser": false,
+ "privateBrowsingId": 0,
+ "userContextId": 0
+ },
+ "private": false,
+ "timeStamp": 1477086296570,
+ "timer": null,
+ "workerType": "none",
+ "styles": [],
+ "category": "webdev"
+ }
+});
+
+stubPackets.set("console.group()", {
+ "from": "server1.conn18.child1/consoleActor2",
+ "type": "consoleAPICall",
+ "message": {
+ "arguments": [],
+ "columnNumber": 1,
+ "counter": null,
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.group()",
+ "functionName": "triggerPacket",
+ "groupName": "",
+ "level": "group",
+ "lineNumber": 2,
+ "originAttributes": {
+ "addonId": "",
+ "appId": 0,
+ "firstPartyDomain": "",
+ "inIsolatedMozBrowser": false,
+ "privateBrowsingId": 0,
+ "userContextId": 0
+ },
+ "private": false,
+ "timeStamp": 1477086298462,
+ "timer": null,
+ "workerType": "none",
+ "styles": [],
+ "category": "webdev"
+ }
+});
+
+stubPackets.set("console.groupEnd()", {
+ "from": "server1.conn18.child1/consoleActor2",
+ "type": "consoleAPICall",
+ "message": {
+ "arguments": [],
+ "columnNumber": 1,
+ "counter": null,
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.group()",
+ "functionName": "triggerPacket",
+ "groupName": "",
+ "level": "groupEnd",
+ "lineNumber": 3,
+ "originAttributes": {
+ "addonId": "",
+ "appId": 0,
+ "firstPartyDomain": "",
+ "inIsolatedMozBrowser": false,
+ "privateBrowsingId": 0,
+ "userContextId": 0
+ },
+ "private": false,
+ "timeStamp": 1477086298464,
+ "timer": null,
+ "workerType": "none",
+ "styles": [],
+ "category": "webdev"
+ }
+});
+
+stubPackets.set("console.log(%cfoobar)", {
+ "from": "server1.conn19.child1/consoleActor2",
+ "type": "consoleAPICall",
+ "message": {
+ "arguments": [
+ "foo",
+ "bar"
+ ],
+ "columnNumber": 1,
+ "counter": null,
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(%25cfoobar)",
+ "functionName": "triggerPacket",
+ "groupName": "",
+ "level": "log",
+ "lineNumber": 2,
+ "originAttributes": {
+ "addonId": "",
+ "appId": 0,
+ "firstPartyDomain": "",
+ "inIsolatedMozBrowser": false,
+ "privateBrowsingId": 0,
+ "userContextId": 0
+ },
+ "private": false,
+ "styles": [
+ "color:blue;font-size:1.3em;background:url('http://example.com/test');position:absolute;top:10px",
+ "color:red;background:url('http://example.com/test')"
+ ],
+ "timeStamp": 1477086300265,
+ "timer": null,
+ "workerType": "none",
+ "category": "webdev"
+ }
+});
+
+
+module.exports = {
+ stubPreparedMessages,
+ stubPackets,
+} \ No newline at end of file
diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/stubs/evaluationResult.js b/devtools/client/webconsole/new-console-output/test/fixtures/stubs/evaluationResult.js
new file mode 100644
index 000000000..098086044
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/stubs/evaluationResult.js
@@ -0,0 +1,182 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/*
+ * THIS FILE IS AUTOGENERATED. DO NOT MODIFY BY HAND. RUN TESTS IN FIXTURES/ TO UPDATE.
+ */
+
+const { ConsoleMessage, NetworkEventMessage } = require("devtools/client/webconsole/new-console-output/types");
+
+let stubPreparedMessages = new Map();
+let stubPackets = new Map();
+
+
+stubPreparedMessages.set("new Date(0)", new ConsoleMessage({
+ "id": "1",
+ "allowRepeating": true,
+ "source": "javascript",
+ "type": "result",
+ "level": "log",
+ "parameters": {
+ "type": "object",
+ "actor": "server1.conn0.child1/obj30",
+ "class": "Date",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "timestamp": 0
+ }
+ },
+ "repeat": 1,
+ "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"javascript\",\"type\":\"result\",\"level\":\"log\",\"parameters\":{\"type\":\"object\",\"actor\":\"server1.conn0.child1/obj30\",\"class\":\"Date\",\"extensible\":true,\"frozen\":false,\"sealed\":false,\"ownPropertyLength\":0,\"preview\":{\"timestamp\":0}},\"repeatId\":null,\"stacktrace\":null,\"frame\":null,\"groupId\":null,\"userProvidedStyles\":null}",
+ "stacktrace": null,
+ "frame": null,
+ "groupId": null,
+ "userProvidedStyles": null
+}));
+
+stubPreparedMessages.set("asdf()", new ConsoleMessage({
+ "id": "1",
+ "allowRepeating": true,
+ "source": "javascript",
+ "type": "result",
+ "level": "error",
+ "messageText": "ReferenceError: asdf is not defined",
+ "parameters": {
+ "type": "undefined"
+ },
+ "repeat": 1,
+ "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"javascript\",\"type\":\"result\",\"level\":\"error\",\"messageText\":\"ReferenceError: asdf is not defined\",\"parameters\":{\"type\":\"undefined\"},\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"debugger eval code\",\"line\":1,\"column\":1},\"groupId\":null,\"exceptionDocURL\":\"https://developer.mozilla.org/docs/Web/JavaScript/Reference/Errors/Not_defined?utm_source=mozilla&utm_medium=firefox-console-errors&utm_campaign=default\",\"userProvidedStyles\":null}",
+ "stacktrace": null,
+ "frame": {
+ "source": "debugger eval code",
+ "line": 1,
+ "column": 1
+ },
+ "groupId": null,
+ "exceptionDocURL": "https://developer.mozilla.org/docs/Web/JavaScript/Reference/Errors/Not_defined?utm_source=mozilla&utm_medium=firefox-console-errors&utm_campaign=default",
+ "userProvidedStyles": null
+}));
+
+stubPreparedMessages.set("1 + @", new ConsoleMessage({
+ "id": "1",
+ "allowRepeating": true,
+ "source": "javascript",
+ "type": "result",
+ "level": "error",
+ "messageText": "SyntaxError: illegal character",
+ "parameters": {
+ "type": "undefined"
+ },
+ "repeat": 1,
+ "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"javascript\",\"type\":\"result\",\"level\":\"error\",\"messageText\":\"SyntaxError: illegal character\",\"parameters\":{\"type\":\"undefined\"},\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"debugger eval code\",\"line\":1,\"column\":4},\"groupId\":null,\"userProvidedStyles\":null}",
+ "stacktrace": null,
+ "frame": {
+ "source": "debugger eval code",
+ "line": 1,
+ "column": 4
+ },
+ "groupId": null,
+ "userProvidedStyles": null
+}));
+
+
+stubPackets.set("new Date(0)", {
+ "from": "server1.conn0.child1/consoleActor2",
+ "input": "new Date(0)",
+ "result": {
+ "type": "object",
+ "actor": "server1.conn0.child1/obj30",
+ "class": "Date",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "timestamp": 0
+ }
+ },
+ "timestamp": 1476573073424,
+ "exception": null,
+ "frame": null,
+ "helperResult": null
+});
+
+stubPackets.set("asdf()", {
+ "from": "server1.conn0.child1/consoleActor2",
+ "input": "asdf()",
+ "result": {
+ "type": "undefined"
+ },
+ "timestamp": 1476573073442,
+ "exception": {
+ "type": "object",
+ "actor": "server1.conn0.child1/obj32",
+ "class": "Error",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 4,
+ "preview": {
+ "kind": "Error",
+ "name": "ReferenceError",
+ "message": "asdf is not defined",
+ "stack": "@debugger eval code:1:1\n",
+ "fileName": "debugger eval code",
+ "lineNumber": 1,
+ "columnNumber": 1
+ }
+ },
+ "exceptionMessage": "ReferenceError: asdf is not defined",
+ "exceptionDocURL": "https://developer.mozilla.org/docs/Web/JavaScript/Reference/Errors/Not_defined?utm_source=mozilla&utm_medium=firefox-console-errors&utm_campaign=default",
+ "frame": {
+ "source": "debugger eval code",
+ "line": 1,
+ "column": 1
+ },
+ "helperResult": null
+});
+
+stubPackets.set("1 + @", {
+ "from": "server1.conn0.child1/consoleActor2",
+ "input": "1 + @",
+ "result": {
+ "type": "undefined"
+ },
+ "timestamp": 1478755616654,
+ "exception": {
+ "type": "object",
+ "actor": "server1.conn0.child1/obj33",
+ "class": "Error",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 4,
+ "preview": {
+ "kind": "Error",
+ "name": "SyntaxError",
+ "message": "illegal character",
+ "stack": "",
+ "fileName": "debugger eval code",
+ "lineNumber": 1,
+ "columnNumber": 4
+ }
+ },
+ "exceptionMessage": "SyntaxError: illegal character",
+ "frame": {
+ "source": "debugger eval code",
+ "line": 1,
+ "column": 4
+ },
+ "helperResult": null
+});
+
+
+module.exports = {
+ stubPreparedMessages,
+ stubPackets,
+} \ No newline at end of file
diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/stubs/index.js b/devtools/client/webconsole/new-console-output/test/fixtures/stubs/index.js
new file mode 100644
index 000000000..59b420180
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/stubs/index.js
@@ -0,0 +1,29 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+let maps = [];
+
+[
+ "consoleApi",
+ "evaluationResult",
+ "networkEvent",
+ "pageError",
+].forEach((filename) => {
+ maps[filename] = require(`./${filename}`);
+});
+
+// Combine all the maps into a single map.
+module.exports = {
+ stubPreparedMessages: new Map([
+ ...maps.consoleApi.stubPreparedMessages,
+ ...maps.evaluationResult.stubPreparedMessages,
+ ...maps.networkEvent.stubPreparedMessages,
+ ...maps.pageError.stubPreparedMessages, ]),
+ stubPackets: new Map([
+ ...maps.consoleApi.stubPackets,
+ ...maps.evaluationResult.stubPackets,
+ ...maps.networkEvent.stubPackets,
+ ...maps.pageError.stubPackets, ]),
+};
diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/stubs/moz.build b/devtools/client/webconsole/new-console-output/test/fixtures/stubs/moz.build
new file mode 100644
index 000000000..88e9c46df
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/stubs/moz.build
@@ -0,0 +1,11 @@
+# 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/.
+
+DevToolsModules(
+ 'consoleApi.js',
+ 'evaluationResult.js',
+ 'index.js',
+ 'networkEvent.js',
+ 'pageError.js',
+)
diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/stubs/networkEvent.js b/devtools/client/webconsole/new-console-output/test/fixtures/stubs/networkEvent.js
new file mode 100644
index 000000000..58a40d30b
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/stubs/networkEvent.js
@@ -0,0 +1,189 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/*
+ * THIS FILE IS AUTOGENERATED. DO NOT MODIFY BY HAND. RUN TESTS IN FIXTURES/ TO UPDATE.
+ */
+
+const { ConsoleMessage, NetworkEventMessage } = require("devtools/client/webconsole/new-console-output/types");
+
+let stubPreparedMessages = new Map();
+let stubPackets = new Map();
+
+
+stubPreparedMessages.set("GET request", new NetworkEventMessage({
+ "id": "1",
+ "actor": "server1.conn0.child1/netEvent29",
+ "level": "log",
+ "isXHR": false,
+ "request": {
+ "url": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/inexistent.html",
+ "method": "GET"
+ },
+ "response": {},
+ "source": "network",
+ "type": "log",
+ "groupId": null
+}));
+
+stubPreparedMessages.set("XHR GET request", new NetworkEventMessage({
+ "id": "1",
+ "actor": "server1.conn1.child1/netEvent29",
+ "level": "log",
+ "isXHR": true,
+ "request": {
+ "url": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/inexistent.html",
+ "method": "GET"
+ },
+ "response": {},
+ "source": "network",
+ "type": "log",
+ "groupId": null
+}));
+
+stubPreparedMessages.set("XHR POST request", new NetworkEventMessage({
+ "id": "1",
+ "actor": "server1.conn2.child1/netEvent29",
+ "level": "log",
+ "isXHR": true,
+ "request": {
+ "url": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/inexistent.html",
+ "method": "POST"
+ },
+ "response": {},
+ "source": "network",
+ "type": "log",
+ "groupId": null
+}));
+
+
+stubPackets.set("GET request", {
+ "from": "server1.conn0.child1/consoleActor2",
+ "type": "networkEvent",
+ "eventActor": {
+ "actor": "server1.conn0.child1/netEvent29",
+ "startedDateTime": "2016-10-15T23:12:04.196Z",
+ "timeStamp": 1476573124196,
+ "url": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/inexistent.html",
+ "method": "GET",
+ "isXHR": false,
+ "cause": {
+ "type": 3,
+ "loadingDocumentUri": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-network-event.html",
+ "stacktrace": [
+ {
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js",
+ "lineNumber": 3,
+ "columnNumber": 1,
+ "functionName": "triggerPacket",
+ "asyncCause": null
+ },
+ {
+ "filename": "chrome://mochikit/content/tests/BrowserTestUtils/content-task.js line 52 > eval",
+ "lineNumber": 4,
+ "columnNumber": 7,
+ "functionName": null,
+ "asyncCause": null
+ },
+ {
+ "filename": "chrome://mochikit/content/tests/BrowserTestUtils/content-task.js",
+ "lineNumber": 53,
+ "columnNumber": 20,
+ "functionName": null,
+ "asyncCause": null
+ }
+ ]
+ },
+ "private": false
+ }
+});
+
+stubPackets.set("XHR GET request", {
+ "from": "server1.conn1.child1/consoleActor2",
+ "type": "networkEvent",
+ "eventActor": {
+ "actor": "server1.conn1.child1/netEvent29",
+ "startedDateTime": "2016-10-15T23:12:05.690Z",
+ "timeStamp": 1476573125690,
+ "url": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/inexistent.html",
+ "method": "GET",
+ "isXHR": true,
+ "cause": {
+ "type": 11,
+ "loadingDocumentUri": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-network-event.html",
+ "stacktrace": [
+ {
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js",
+ "lineNumber": 4,
+ "columnNumber": 1,
+ "functionName": "triggerPacket",
+ "asyncCause": null
+ },
+ {
+ "filename": "chrome://mochikit/content/tests/BrowserTestUtils/content-task.js line 52 > eval",
+ "lineNumber": 4,
+ "columnNumber": 7,
+ "functionName": null,
+ "asyncCause": null
+ },
+ {
+ "filename": "chrome://mochikit/content/tests/BrowserTestUtils/content-task.js",
+ "lineNumber": 53,
+ "columnNumber": 20,
+ "functionName": null,
+ "asyncCause": null
+ }
+ ]
+ },
+ "private": false
+ }
+});
+
+stubPackets.set("XHR POST request", {
+ "from": "server1.conn2.child1/consoleActor2",
+ "type": "networkEvent",
+ "eventActor": {
+ "actor": "server1.conn2.child1/netEvent29",
+ "startedDateTime": "2016-10-15T23:12:07.158Z",
+ "timeStamp": 1476573127158,
+ "url": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/inexistent.html",
+ "method": "POST",
+ "isXHR": true,
+ "cause": {
+ "type": 11,
+ "loadingDocumentUri": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-network-event.html",
+ "stacktrace": [
+ {
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js",
+ "lineNumber": 4,
+ "columnNumber": 1,
+ "functionName": "triggerPacket",
+ "asyncCause": null
+ },
+ {
+ "filename": "chrome://mochikit/content/tests/BrowserTestUtils/content-task.js line 52 > eval",
+ "lineNumber": 4,
+ "columnNumber": 7,
+ "functionName": null,
+ "asyncCause": null
+ },
+ {
+ "filename": "chrome://mochikit/content/tests/BrowserTestUtils/content-task.js",
+ "lineNumber": 53,
+ "columnNumber": 20,
+ "functionName": null,
+ "asyncCause": null
+ }
+ ]
+ },
+ "private": false
+ }
+});
+
+
+module.exports = {
+ stubPreparedMessages,
+ stubPackets,
+} \ No newline at end of file
diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/stubs/pageError.js b/devtools/client/webconsole/new-console-output/test/fixtures/stubs/pageError.js
new file mode 100644
index 000000000..eda8e8b83
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/stubs/pageError.js
@@ -0,0 +1,102 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/*
+ * THIS FILE IS AUTOGENERATED. DO NOT MODIFY BY HAND. RUN TESTS IN FIXTURES/ TO UPDATE.
+ */
+
+const { ConsoleMessage, NetworkEventMessage } = require("devtools/client/webconsole/new-console-output/types");
+
+let stubPreparedMessages = new Map();
+let stubPackets = new Map();
+
+
+stubPreparedMessages.set("ReferenceError: asdf is not defined", new ConsoleMessage({
+ "id": "1",
+ "allowRepeating": true,
+ "source": "javascript",
+ "type": "log",
+ "level": "error",
+ "messageText": "ReferenceError: asdf is not defined",
+ "parameters": null,
+ "repeat": 1,
+ "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"javascript\",\"type\":\"log\",\"level\":\"error\",\"messageText\":\"ReferenceError: asdf is not defined\",\"parameters\":null,\"repeatId\":null,\"stacktrace\":[{\"filename\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=Reference%20Error\",\"lineNumber\":3,\"columnNumber\":5,\"functionName\":\"bar\"},{\"filename\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=Reference%20Error\",\"lineNumber\":6,\"columnNumber\":5,\"functionName\":\"foo\"},{\"filename\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=Reference%20Error\",\"lineNumber\":9,\"columnNumber\":3,\"functionName\":null}],\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=Reference%20Error\",\"line\":3,\"column\":5},\"groupId\":null,\"exceptionDocURL\":\"https://developer.mozilla.org/docs/Web/JavaScript/Reference/Errors/Not_defined?utm_source=mozilla&utm_medium=firefox-console-errors&utm_campaign=default\"}",
+ "stacktrace": [
+ {
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=Reference%20Error",
+ "lineNumber": 3,
+ "columnNumber": 5,
+ "functionName": "bar"
+ },
+ {
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=Reference%20Error",
+ "lineNumber": 6,
+ "columnNumber": 5,
+ "functionName": "foo"
+ },
+ {
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=Reference%20Error",
+ "lineNumber": 9,
+ "columnNumber": 3,
+ "functionName": null
+ }
+ ],
+ "frame": {
+ "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=Reference%20Error",
+ "line": 3,
+ "column": 5
+ },
+ "groupId": null,
+ "exceptionDocURL": "https://developer.mozilla.org/docs/Web/JavaScript/Reference/Errors/Not_defined?utm_source=mozilla&utm_medium=firefox-console-errors&utm_campaign=default"
+}));
+
+
+stubPackets.set("ReferenceError: asdf is not defined", {
+ "from": "server1.conn0.child1/consoleActor2",
+ "type": "pageError",
+ "pageError": {
+ "errorMessage": "ReferenceError: asdf is not defined",
+ "errorMessageName": "JSMSG_NOT_DEFINED",
+ "exceptionDocURL": "https://developer.mozilla.org/docs/Web/JavaScript/Reference/Errors/Not_defined?utm_source=mozilla&utm_medium=firefox-console-errors&utm_campaign=default",
+ "sourceName": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=Reference%20Error",
+ "lineText": "",
+ "lineNumber": 3,
+ "columnNumber": 5,
+ "category": "content javascript",
+ "timeStamp": 1476573167137,
+ "warning": false,
+ "error": false,
+ "exception": true,
+ "strict": false,
+ "info": false,
+ "private": false,
+ "stacktrace": [
+ {
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=Reference%20Error",
+ "lineNumber": 3,
+ "columnNumber": 5,
+ "functionName": "bar"
+ },
+ {
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=Reference%20Error",
+ "lineNumber": 6,
+ "columnNumber": 5,
+ "functionName": "foo"
+ },
+ {
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=Reference%20Error",
+ "lineNumber": 9,
+ "columnNumber": 3,
+ "functionName": null
+ }
+ ]
+ }
+});
+
+
+module.exports = {
+ stubPreparedMessages,
+ stubPackets,
+} \ No newline at end of file
diff --git a/devtools/client/webconsole/new-console-output/test/helpers.js b/devtools/client/webconsole/new-console-output/test/helpers.js
new file mode 100644
index 000000000..39807eaed
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/helpers.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+let ReactDOM = require("devtools/client/shared/vendor/react-dom");
+let React = require("devtools/client/shared/vendor/react");
+var TestUtils = React.addons.TestUtils;
+
+const actions = require("devtools/client/webconsole/new-console-output/actions/index");
+const { configureStore } = require("devtools/client/webconsole/new-console-output/store");
+const { IdGenerator } = require("devtools/client/webconsole/new-console-output/utils/id-generator");
+const { stubPackets } = require("devtools/client/webconsole/new-console-output/test/fixtures/stubs/index");
+
+/**
+ * Prepare actions for use in testing.
+ */
+function setupActions() {
+ // Some actions use dependency injection. This helps them avoid using state in
+ // a hard-to-test way. We need to inject stubbed versions of these dependencies.
+ const wrappedActions = Object.assign({}, actions);
+
+ const idGenerator = new IdGenerator();
+ wrappedActions.messageAdd = (packet) => {
+ return actions.messageAdd(packet, idGenerator);
+ };
+
+ return wrappedActions;
+}
+
+/**
+ * Prepare the store for use in testing.
+ */
+function setupStore(input) {
+ const store = configureStore();
+
+ // Add the messages from the input commands to the store.
+ input.forEach((cmd) => {
+ store.dispatch(actions.messageAdd(stubPackets.get(cmd)));
+ });
+
+ return store;
+}
+
+function renderComponent(component, props) {
+ const el = React.createElement(component, props, {});
+ // By default, renderIntoDocument() won't work for stateless components, but
+ // it will work if the stateless component is wrapped in a stateful one.
+ // See https://github.com/facebook/react/issues/4839
+ const wrappedEl = React.DOM.span({}, [el]);
+ const renderedComponent = TestUtils.renderIntoDocument(wrappedEl);
+ return ReactDOM.findDOMNode(renderedComponent).children[0];
+}
+
+function shallowRenderComponent(component, props) {
+ const el = React.createElement(component, props);
+ const renderer = TestUtils.createRenderer();
+ renderer.render(el, {});
+ return renderer.getRenderOutput();
+}
+
+module.exports = {
+ setupActions,
+ setupStore,
+ renderComponent,
+ shallowRenderComponent
+};
diff --git a/devtools/client/webconsole/new-console-output/test/mochitest/browser.ini b/devtools/client/webconsole/new-console-output/test/mochitest/browser.ini
new file mode 100644
index 000000000..9881d0559
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser.ini
@@ -0,0 +1,21 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ head.js
+ test-batching.html
+ test-console.html
+ test-console-filters.html
+ test-console-group.html
+ test-console-table.html
+ !/devtools/client/framework/test/shared-head.js
+
+[browser_webconsole_batching.js]
+[browser_webconsole_console_group.js]
+[browser_webconsole_console_table.js]
+[browser_webconsole_filters.js]
+[browser_webconsole_init.js]
+[browser_webconsole_input_focus.js]
+[browser_webconsole_keyboard_accessibility.js]
+[browser_webconsole_observer_notifications.js]
+[browser_webconsole_vview_close_on_esc_key.js]
diff --git a/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_batching.js b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_batching.js
new file mode 100644
index 000000000..0bfdccc3c
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_batching.js
@@ -0,0 +1,51 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 adding console calls as batch keep the order of the message.
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/new-console-output/test/mochitest/test-batching.html";
+const { l10n } = require("devtools/client/webconsole/new-console-output/utils/messages");
+
+add_task(function* () {
+ let hud = yield openNewTabAndConsole(TEST_URI);
+ const messageNumber = 100;
+ yield testSimpleBatchLogging(hud, messageNumber);
+ yield testBatchLoggingAndClear(hud, messageNumber);
+});
+
+function* testSimpleBatchLogging(hud, messageNumber) {
+ yield ContentTask.spawn(gBrowser.selectedBrowser, messageNumber,
+ function (numMessages) {
+ content.wrappedJSObject.batchLog(numMessages);
+ }
+ );
+
+ for (let i = 0; i < messageNumber; i++) {
+ let node = yield waitFor(() => findMessageAtIndex(hud, i, i));
+ is(node.textContent, i.toString(), `message at index "${i}" is the expected one`);
+ }
+}
+
+function* testBatchLoggingAndClear(hud, messageNumber) {
+ yield ContentTask.spawn(gBrowser.selectedBrowser, messageNumber,
+ function (numMessages) {
+ content.wrappedJSObject.batchLogAndClear(numMessages);
+ }
+ );
+ yield waitFor(() => findMessage(hud, l10n.getStr("consoleCleared")));
+ ok(true, "console cleared message is displayed");
+
+ // Passing the text argument as an empty string will returns all the message,
+ // whatever their content is.
+ const messages = findMessages(hud, "");
+ is(messages.length, 1, "console was cleared as expected");
+}
+
+function findMessageAtIndex(hud, text, index) {
+ const selector = `.message:nth-of-type(${index + 1}) .message-body`;
+ return findMessage(hud, text, selector);
+}
diff --git a/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_console_group.js b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_console_group.js
new file mode 100644
index 000000000..94de78f13
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_console_group.js
@@ -0,0 +1,91 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 console.group, console.groupCollapsed and console.groupEnd calls
+// behave as expected.
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/new-console-output/test/mochitest/test-console-group.html";
+const { INDENT_WIDTH } = require("devtools/client/webconsole/new-console-output/components/message-indent");
+
+add_task(function* () {
+ let toolbox = yield openNewTabAndToolbox(TEST_URI, "webconsole");
+ let hud = toolbox.getCurrentPanel().hud;
+
+ const store = hud.ui.newConsoleOutput.getStore();
+ // Adding loggin each time the store is modified in order to check
+ // the store state in case of failure.
+ store.subscribe(() => {
+ const messages = store.getState().messages.messagesById.toJS()
+ .map(message => {
+ return {
+ id: message.id,
+ type: message.type,
+ parameters: message.parameters,
+ messageText: message.messageText
+ };
+ }
+ );
+ info("messages : " + JSON.stringify(messages));
+ });
+
+ yield ContentTask.spawn(gBrowser.selectedBrowser, null, function () {
+ content.wrappedJSObject.doLog();
+ });
+
+ info("Test a group at root level");
+ let node = yield waitFor(() => findMessage(hud, "group-1"));
+ testClass(node, "startGroup");
+ testIndent(node, 0);
+
+ info("Test a message in a 1 level deep group");
+ node = yield waitFor(() => findMessage(hud, "log-1"));
+ testClass(node, "log");
+ testIndent(node, 1);
+
+ info("Test a group in a 1 level deep group");
+ node = yield waitFor(() => findMessage(hud, "group-2"));
+ testClass(node, "startGroup");
+ testIndent(node, 1);
+
+ info("Test a message in a 2 level deep group");
+ node = yield waitFor(() => findMessage(hud, "log-2"));
+ testClass(node, "log");
+ testIndent(node, 2);
+
+ info("Test a message in a 1 level deep group, after closing a 2 level deep group");
+ node = yield waitFor(() => findMessage(hud, "log-3"));
+ testClass(node, "log");
+ testIndent(node, 1);
+
+ info("Test a message at root level, after closing all the groups");
+ node = yield waitFor(() => findMessage(hud, "log-4"));
+ testClass(node, "log");
+ testIndent(node, 0);
+
+ info("Test a collapsed group at root level");
+ node = yield waitFor(() => findMessage(hud, "group-3"));
+ testClass(node, "startGroupCollapsed");
+ testIndent(node, 0);
+
+ info("Test a message at root level, after closing a collapsed group");
+ node = yield waitFor(() => findMessage(hud, "log-6"));
+ testClass(node, "log");
+ testIndent(node, 0);
+
+ let nodes = hud.ui.experimentalOutputNode.querySelectorAll(".message");
+ is(nodes.length, 8, "expected number of messages are displayed");
+});
+
+function testClass(node, className) {
+ ok(node.classList.contains(className), `message has the expected "${className}" class`);
+}
+
+function testIndent(node, indent) {
+ indent = `${indent * INDENT_WIDTH}px`;
+ is(node.querySelector(".indent").style.width, indent,
+ "message has the expected level of indentation");
+}
diff --git a/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_console_table.js b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_console_table.js
new file mode 100644
index 000000000..a90ae1af1
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_console_table.js
@@ -0,0 +1,173 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 console.table calls with all the test cases shown
+// in the MDN doc (https://developer.mozilla.org/en-US/docs/Web/API/Console/table)
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/new-console-output/test/mochitest/test-console-table.html";
+
+add_task(function* () {
+ let toolbox = yield openNewTabAndToolbox(TEST_URI, "webconsole");
+ let hud = toolbox.getCurrentPanel().hud;
+
+ function Person(firstName, lastName) {
+ this.firstName = firstName;
+ this.lastName = lastName;
+ }
+
+ const testCases = [{
+ info: "Testing when data argument is an array",
+ input: ["apples", "oranges", "bananas"],
+ expected: {
+ columns: ["(index)", "Values"],
+ rows: [
+ ["0", "apples"],
+ ["1", "oranges"],
+ ["2", "bananas"],
+ ]
+ }
+ }, {
+ info: "Testing when data argument is an object",
+ input: new Person("John", "Smith"),
+ expected: {
+ columns: ["(index)", "Values"],
+ rows: [
+ ["firstName", "John"],
+ ["lastName", "Smith"],
+ ]
+ }
+ }, {
+ info: "Testing when data argument is an array of arrays",
+ input: [["Jane", "Doe"], ["Emily", "Jones"]],
+ expected: {
+ columns: ["(index)", "0", "1"],
+ rows: [
+ ["0", "Jane", "Doe"],
+ ["1", "Emily", "Jones"],
+ ]
+ }
+ }, {
+ info: "Testing when data argument is an array of objects",
+ input: [
+ new Person("Jack", "Foo"),
+ new Person("Emma", "Bar"),
+ new Person("Michelle", "Rax"),
+ ],
+ expected: {
+ columns: ["(index)", "firstName", "lastName"],
+ rows: [
+ ["0", "Jack", "Foo"],
+ ["1", "Emma", "Bar"],
+ ["2", "Michelle", "Rax"],
+ ]
+ }
+ }, {
+ info: "Testing when data argument is an object whose properties are objects",
+ input: {
+ father: new Person("Darth", "Vader"),
+ daughter: new Person("Leia", "Organa"),
+ son: new Person("Luke", "Skywalker"),
+ },
+ expected: {
+ columns: ["(index)", "firstName", "lastName"],
+ rows: [
+ ["father", "Darth", "Vader"],
+ ["daughter", "Leia", "Organa"],
+ ["son", "Luke", "Skywalker"],
+ ]
+ }
+ }, {
+ info: "Testing when data argument is a Set",
+ input: new Set(["a", "b", "c"]),
+ expected: {
+ columns: ["(iteration index)", "Values"],
+ rows: [
+ ["0", "a"],
+ ["1", "b"],
+ ["2", "c"],
+ ]
+ }
+ }, {
+ info: "Testing when data argument is a Map",
+ input: new Map([["key-a", "value-a"], ["key-b", "value-b"]]),
+ expected: {
+ columns: ["(iteration index)", "Key", "Values"],
+ rows: [
+ ["0", "key-a", "value-a"],
+ ["1", "key-b", "value-b"],
+ ]
+ }
+ }, {
+ info: "Testing restricting the columns displayed",
+ input: [
+ new Person("Sam", "Wright"),
+ new Person("Elena", "Bartz"),
+ ],
+ headers: ["firstName"],
+ expected: {
+ columns: ["(index)", "firstName"],
+ rows: [
+ ["0", "Sam"],
+ ["1", "Elena"],
+ ]
+ }
+ }];
+
+ yield ContentTask.spawn(gBrowser.selectedBrowser, testCases, function (tests) {
+ tests.forEach((test) => {
+ content.wrappedJSObject.doConsoleTable(test.input, test.headers);
+ });
+ });
+
+ let nodes = [];
+ for (let testCase of testCases) {
+ let node = yield waitFor(
+ () => findConsoleTable(hud.ui.experimentalOutputNode, testCases.indexOf(testCase))
+ );
+ nodes.push(node);
+ }
+
+ let consoleTableNodes = hud.ui.experimentalOutputNode.querySelectorAll(
+ ".message .new-consoletable");
+
+ is(consoleTableNodes.length, testCases.length,
+ "console has the expected number of consoleTable items");
+
+ testCases.forEach((testCase, index) => {
+ info(testCase.info);
+
+ let node = nodes[index];
+ let columns = Array.from(node.querySelectorAll("thead th"));
+ let rows = Array.from(node.querySelectorAll("tbody tr"));
+
+ is(
+ JSON.stringify(testCase.expected.columns),
+ JSON.stringify(columns.map(column => column.textContent)),
+ "table has the expected columns"
+ );
+
+ is(testCase.expected.rows.length, rows.length,
+ "table has the expected number of rows");
+
+ testCase.expected.rows.forEach((expectedRow, rowIndex) => {
+ let row = rows[rowIndex];
+ let cells = row.querySelectorAll("td");
+ is(expectedRow.length, cells.length, "row has the expected number of cells");
+
+ expectedRow.forEach((expectedCell, cellIndex) => {
+ let cell = cells[cellIndex];
+ is(expectedCell, cell.textContent, "cell has the expected content");
+ });
+ });
+ });
+});
+
+function findConsoleTable(node, index) {
+ let condition = node.querySelector(
+ `.message:nth-of-type(${index + 1}) .new-consoletable`);
+ return condition;
+}
diff --git a/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_filters.js b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_filters.js
new file mode 100644
index 000000000..8eb536926
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_filters.js
@@ -0,0 +1,72 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests filters.
+
+"use strict";
+
+const { MESSAGE_LEVEL } = require("devtools/client/webconsole/new-console-output/constants");
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/new-console-output/test/mochitest/test-console-filters.html";
+
+add_task(function* () {
+ let hud = yield openNewTabAndConsole(TEST_URI);
+ const outputNode = hud.ui.experimentalOutputNode;
+
+ const toolbar = yield waitFor(() => {
+ return outputNode.querySelector(".webconsole-filterbar-primary");
+ });
+ ok(toolbar, "Toolbar found");
+
+ // Show the filter bar
+ toolbar.querySelector(".devtools-filter-icon").click();
+ const filterBar = yield waitFor(() => {
+ return outputNode.querySelector(".webconsole-filterbar-secondary");
+ });
+ ok(filterBar, "Filter bar is shown when filter icon is clicked.");
+
+ // Check defaults.
+ Object.values(MESSAGE_LEVEL).forEach(level => {
+ ok(filterIsEnabled(filterBar.querySelector(`.${level}`)),
+ `Filter button for ${level} is on by default`);
+ });
+ ["net", "netxhr"].forEach(category => {
+ ok(!filterIsEnabled(filterBar.querySelector(`.${category}`)),
+ `Filter button for ${category} is off by default`);
+ });
+
+ // Check that messages are shown as expected. This depends on cached messages being
+ // shown.
+ ok(findMessages(hud, "").length == 5,
+ "Messages of all levels shown when filters are on.");
+
+ // Check that messages are not shown when their filter is turned off.
+ filterBar.querySelector(".error").click();
+ yield waitFor(() => findMessages(hud, "").length == 4);
+ ok(true, "When a filter is turned off, its messages are not shown.");
+
+ // Check that the ui settings were persisted.
+ yield closeTabAndToolbox();
+ yield testFilterPersistence();
+});
+
+function filterIsEnabled(button) {
+ return button.classList.contains("checked");
+}
+
+function* testFilterPersistence() {
+ let hud = yield openNewTabAndConsole(TEST_URI);
+ const outputNode = hud.ui.experimentalOutputNode;
+ const filterBar = yield waitFor(() => {
+ return outputNode.querySelector(".webconsole-filterbar-secondary");
+ });
+ ok(filterBar, "Filter bar ui setting is persisted.");
+
+ // Check that the filter settings were persisted.
+ ok(!filterIsEnabled(filterBar.querySelector(".error")),
+ "Filter button setting is persisted");
+ ok(findMessages(hud, "").length == 4,
+ "Messages of all levels shown when filters are on.");
+}
diff --git a/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_init.js b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_init.js
new file mode 100644
index 000000000..4280270dd
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_init.js
@@ -0,0 +1,35 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 TEST_URI = "http://example.com/browser/devtools/client/webconsole/new-console-output/test/mochitest/test-console.html";
+
+add_task(function* () {
+ let toolbox = yield openNewTabAndToolbox(TEST_URI, "webconsole");
+ let hud = toolbox.getCurrentPanel().hud;
+ let {ui} = hud;
+
+ ok(ui.jsterm, "jsterm exists");
+ ok(ui.newConsoleOutput, "newConsoleOutput exists");
+
+ // @TODO: fix proptype errors
+ let receievedMessages = waitForMessages({
+ hud,
+ messages: [{
+ text: '0',
+ }, {
+ text: '1',
+ }, {
+ text: '2',
+ }],
+ });
+
+ yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function() {
+ content.wrappedJSObject.doLogs(3);
+ });
+
+ yield receievedMessages;
+});
diff --git a/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_input_focus.js b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_input_focus.js
new file mode 100644
index 000000000..7660df238
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_input_focus.js
@@ -0,0 +1,57 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests that the input field is focused when the console is opened.
+
+"use strict";
+
+const TEST_URI =
+ `data:text/html;charset=utf-8,Test input focused
+ <script>
+ console.log("console message 1");
+ </script>`;
+
+add_task(function* () {
+ let hud = yield openNewTabAndConsole(TEST_URI);
+ hud.jsterm.clearOutput();
+
+ let inputNode = hud.jsterm.inputNode;
+ ok(inputNode.getAttribute("focused"), "input node is focused after output is cleared");
+
+ ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ content.wrappedJSObject.console.log("console message 2");
+ });
+ let msg = yield waitFor(() => findMessage(hud, "console message 2"));
+ let outputItem = msg.querySelector(".message-body");
+
+ inputNode = hud.jsterm.inputNode;
+ ok(inputNode.getAttribute("focused"), "input node is focused, first");
+
+ yield waitForBlurredInput(inputNode);
+
+ EventUtils.sendMouseEvent({type: "click"}, hud.outputNode);
+ ok(inputNode.getAttribute("focused"), "input node is focused, second time");
+
+ yield waitForBlurredInput(inputNode);
+
+ info("Setting a text selection and making sure a click does not re-focus");
+ let selection = hud.iframeWindow.getSelection();
+ selection.selectAllChildren(outputItem);
+
+ EventUtils.sendMouseEvent({type: "click"}, hud.outputNode);
+ ok(!inputNode.getAttribute("focused"),
+ "input node focused after text is selected");
+});
+
+function waitForBlurredInput(inputNode) {
+ return new Promise(resolve => {
+ let lostFocus = () => {
+ ok(!inputNode.getAttribute("focused"), "input node is not focused");
+ resolve();
+ };
+ inputNode.addEventListener("blur", lostFocus, { once: true });
+ document.getElementById("urlbar").click();
+ });
+}
diff --git a/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_keyboard_accessibility.js b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_keyboard_accessibility.js
new file mode 100644
index 000000000..1038194b9
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_keyboard_accessibility.js
@@ -0,0 +1,71 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Check that basic keyboard shortcuts work in the web console.
+
+"use strict";
+
+const TEST_URI =
+ `data:text/html;charset=utf-8,<p>Test keyboard accessibility</p>
+ <script>
+ for (let i = 1; i <= 100; i++) {
+ console.log("console message " + i);
+ }
+ </script>
+ `;
+
+add_task(function* () {
+ let hud = yield openNewTabAndConsole(TEST_URI);
+ info("Web Console opened");
+
+ const outputScroller = hud.ui.outputScroller;
+
+ yield waitFor(() => findMessages(hud, "").length == 100);
+
+ let currentPosition = outputScroller.scrollTop;
+ const bottom = currentPosition;
+
+ EventUtils.sendMouseEvent({type: "click"}, hud.jsterm.inputNode);
+
+ // Page up.
+ EventUtils.synthesizeKey("VK_PAGE_UP", {});
+ isnot(outputScroller.scrollTop, currentPosition,
+ "scroll position changed after page up");
+
+ // Page down.
+ currentPosition = outputScroller.scrollTop;
+ EventUtils.synthesizeKey("VK_PAGE_DOWN", {});
+ ok(outputScroller.scrollTop > currentPosition,
+ "scroll position now at bottom");
+
+ // Home
+ EventUtils.synthesizeKey("VK_HOME", {});
+ is(outputScroller.scrollTop, 0, "scroll position now at top");
+
+ // End
+ EventUtils.synthesizeKey("VK_END", {});
+ let scrollTop = outputScroller.scrollTop;
+ ok(scrollTop > 0 && Math.abs(scrollTop - bottom) <= 5,
+ "scroll position now at bottom");
+
+ // Clear output
+ info("try ctrl-l to clear output");
+ let clearShortcut;
+ if (Services.appinfo.OS === "Darwin") {
+ clearShortcut = WCUL10n.getStr("webconsole.clear.keyOSX");
+ } else {
+ clearShortcut = WCUL10n.getStr("webconsole.clear.key");
+ }
+ synthesizeKeyShortcut(clearShortcut);
+ yield waitFor(() => findMessages(hud, "").length == 0);
+ is(hud.jsterm.inputNode.getAttribute("focused"), "true", "jsterm input is focused");
+
+ // Focus filter
+ info("try ctrl-f to focus filter");
+ synthesizeKeyShortcut(WCUL10n.getStr("webconsole.find.key"));
+ ok(!hud.jsterm.inputNode.getAttribute("focused"), "jsterm input is not focused");
+ is(hud.ui.filterBox, outputScroller.ownerDocument.activeElement,
+ "filter input is focused");
+});
diff --git a/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_observer_notifications.js b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_observer_notifications.js
new file mode 100644
index 000000000..5225a6ac1
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_observer_notifications.js
@@ -0,0 +1,47 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 TEST_URI = "data:text/html;charset=utf-8,<p>Web Console test for " +
+ "obeserver notifications";
+
+let created = false;
+let destroyed = false;
+
+add_task(function* () {
+ setupObserver();
+ yield openNewTabAndConsole(TEST_URI);
+ yield waitFor(() => created);
+
+ yield closeTabAndToolbox(gBrowser.selectedTab);
+ yield waitFor(() => destroyed);
+});
+
+function setupObserver() {
+ const observer = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
+
+ observe: function observe(subject, topic) {
+ subject = subject.QueryInterface(Ci.nsISupportsString);
+
+ switch (topic) {
+ case "web-console-created":
+ ok(HUDService.getHudReferenceById(subject.data), "We have a hud reference");
+ Services.obs.removeObserver(observer, "web-console-created");
+ created = true;
+ break;
+ case "web-console-destroyed":
+ ok(!HUDService.getHudReferenceById(subject.data), "We do not have a hud reference");
+ Services.obs.removeObserver(observer, "web-console-destroyed");
+ destroyed = true;
+ break;
+ }
+ },
+ };
+
+ Services.obs.addObserver(observer, "web-console-created", false);
+ Services.obs.addObserver(observer, "web-console-destroyed", false);
+}
diff --git a/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_vview_close_on_esc_key.js b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_vview_close_on_esc_key.js
new file mode 100644
index 000000000..712a990b4
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_vview_close_on_esc_key.js
@@ -0,0 +1,46 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Check that the variables view sidebar can be closed by pressing Escape in the
+// web console.
+
+"use strict";
+
+const TEST_URI =
+ "data:text/html;charset=utf8,<script>let fooObj = {testProp: 'testValue'}</script>";
+
+add_task(function* () {
+ let hud = yield openNewTabAndConsole(TEST_URI);
+ let jsterm = hud.jsterm;
+ let vview;
+
+ yield openSidebar("fooObj", 'testProp: "testValue"');
+ vview.window.focus();
+
+ let sidebarClosed = jsterm.once("sidebar-closed");
+ EventUtils.synthesizeKey("VK_ESCAPE", {});
+ yield sidebarClosed;
+
+ function* openSidebar(objName, expectedText) {
+ yield jsterm.execute(objName);
+ info("JSTerm executed");
+
+ let msg = yield waitFor(() => findMessage(hud, "Object"));
+ ok(msg, "Message found");
+
+ let anchor = msg.querySelector("a");
+ let body = msg.querySelector(".message-body");
+ ok(anchor, "object anchor");
+ ok(body, "message body");
+ ok(body.textContent.includes(expectedText), "message text check");
+
+ msg.scrollIntoView();
+ yield EventUtils.synthesizeMouse(anchor, 2, 2, {}, hud.iframeWindow);
+
+ let vviewVar = yield jsterm.once("variablesview-fetched");
+ vview = vviewVar._variablesView;
+ ok(vview, "variables view object exists");
+ }
+});
diff --git a/devtools/client/webconsole/new-console-output/test/mochitest/head.js b/devtools/client/webconsole/new-console-output/test/mochitest/head.js
new file mode 100644
index 000000000..b71eaec4f
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/head.js
@@ -0,0 +1,137 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+/* import-globals-from ../../../../framework/test/shared-head.js */
+
+"use strict";
+
+// shared-head.js handles imports, constants, and utility functions
+// Load the shared-head file first.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js",
+ this);
+
+var {Utils: WebConsoleUtils} = require("devtools/client/webconsole/utils");
+const WEBCONSOLE_STRINGS_URI = "devtools/client/locales/webconsole.properties";
+var WCUL10n = new WebConsoleUtils.L10n(WEBCONSOLE_STRINGS_URI);
+
+Services.prefs.setBoolPref("devtools.webconsole.new-frontend-enabled", true);
+registerCleanupFunction(function* () {
+ Services.prefs.clearUserPref("devtools.webconsole.new-frontend-enabled");
+
+ let browserConsole = HUDService.getBrowserConsole();
+ if (browserConsole) {
+ if (browserConsole.jsterm) {
+ browserConsole.jsterm.clearOutput(true);
+ }
+ yield HUDService.toggleBrowserConsole();
+ }
+});
+
+/**
+ * Add a new tab and open the toolbox in it, and select the webconsole.
+ *
+ * @param string url
+ * The URL for the tab to be opened.
+ * @return Promise
+ * Resolves when the tab has been added, loaded and the toolbox has been opened.
+ * Resolves to the toolbox.
+ */
+var openNewTabAndConsole = Task.async(function* (url) {
+ let toolbox = yield openNewTabAndToolbox(TEST_URI, "webconsole");
+ let hud = toolbox.getCurrentPanel().hud;
+ hud.jsterm._lazyVariablesView = false;
+ return hud;
+});
+
+/**
+ * Wait for messages in the web console output, resolving once they are receieved.
+ *
+ * @param object options
+ * - hud: the webconsole
+ * - messages: Array[Object]. An array of messages to match. Current supported options:
+ * - text: Exact text match in .message-body
+ */
+function waitForMessages({ hud, messages }) {
+ return new Promise(resolve => {
+ let numMatched = 0;
+ let receivedLog = hud.ui.on("new-messages", function messagesReceieved(e, newMessages) {
+ for (let message of messages) {
+ if (message.matched) {
+ continue;
+ }
+
+ for (let newMessage of newMessages) {
+ if (newMessage.node.querySelector(".message-body").textContent == message.text) {
+ numMatched++;
+ message.matched = true;
+ info("Matched a message with text: " + message.text + ", still waiting for " + (messages.length - numMatched) + " messages");
+ break;
+ }
+ }
+
+ if (numMatched === messages.length) {
+ hud.ui.off("new-messages", messagesReceieved);
+ resolve(receivedLog);
+ return;
+ }
+ }
+ });
+ });
+}
+
+/**
+ * Wait for a predicate to return a result.
+ *
+ * @param function condition
+ * Invoked once in a while until it returns a truthy value. This should be an
+ * idempotent function, since we have to run it a second time after it returns
+ * true in order to return the value.
+ * @param string message [optional]
+ * A message to output if the condition failes.
+ * @param number interval [optional]
+ * How often the predicate is invoked, in milliseconds.
+ * @return object
+ * A promise that is resolved with the result of the condition.
+ */
+function* waitFor(condition, message = "waitFor", interval = 10, maxTries = 500) {
+ return new Promise(resolve => {
+ BrowserTestUtils.waitForCondition(condition, message, interval, maxTries)
+ .then(() => resolve(condition()));
+ });
+}
+
+/**
+ * Find a message in the output.
+ *
+ * @param object hud
+ * The web console.
+ * @param string text
+ * A substring that can be found in the message.
+ * @param selector [optional]
+ * The selector to use in finding the message.
+ */
+function findMessage(hud, text, selector = ".message") {
+ const elements = findMessages(hud, text, selector);
+ return elements.pop();
+}
+
+/**
+ * Find multiple messages in the output.
+ *
+ * @param object hud
+ * The web console.
+ * @param string text
+ * A substring that can be found in the message.
+ * @param selector [optional]
+ * The selector to use in finding the message.
+ */
+function findMessages(hud, text, selector = ".message") {
+ const messages = hud.ui.experimentalOutputNode.querySelectorAll(selector);
+ const elements = Array.prototype.filter.call(
+ messages,
+ (el) => el.textContent.includes(text)
+ );
+ return elements;
+}
diff --git a/devtools/client/webconsole/new-console-output/test/mochitest/test-batching.html b/devtools/client/webconsole/new-console-output/test/mochitest/test-batching.html
new file mode 100644
index 000000000..9d122387a
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/test-batching.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>Webconsole batch console calls test page</title>
+ </head>
+ <body>
+ <p>batch console calls test page</p>
+ <script>
+ "use strict";
+
+ function batchLog(numMessages = 0) {
+ for (let i = 0; i < numMessages; i++) {
+ console.log(i);
+ }
+ }
+
+ function batchLogAndClear(numMessages = 0) {
+ for (let i = 0; i < numMessages; i++) {
+ console.log(i);
+ if (i === numMessages - 1) {
+ console.clear();
+ }
+ }
+ }
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/new-console-output/test/mochitest/test-console-filters.html b/devtools/client/webconsole/new-console-output/test/mochitest/test-console-filters.html
new file mode 100644
index 000000000..293421549
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/test-console-filters.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>Webconsole filters test page</title>
+ </head>
+ <body>
+ <p>Webconsole filters test page</p>
+ <script>
+ console.log("console log");
+ console.warn("console warn");
+ console.error("console error");
+ console.info("console info");
+ console.count("console debug");
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/new-console-output/test/mochitest/test-console-group.html b/devtools/client/webconsole/new-console-output/test/mochitest/test-console-group.html
new file mode 100644
index 000000000..47373d3b9
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/test-console-group.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>Webconsole console.group test page</title>
+ </head>
+ <body>
+ <p>console.group() & console.groupCollapsed() test page</p>
+ <script>
+ "use strict";
+
+ function doLog() {
+ console.group("group-1");
+ console.log("log-1");
+ console.group("group-2");
+ console.log("log-2");
+ console.groupEnd("group-2");
+ console.log("log-3");
+ console.groupEnd("group-1");
+ console.log("log-4");
+ console.groupCollapsed("group-3");
+ console.log("log-5");
+ console.groupEnd("group-3");
+ console.log("log-6");
+ }
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/new-console-output/test/mochitest/test-console-table.html b/devtools/client/webconsole/new-console-output/test/mochitest/test-console-table.html
new file mode 100644
index 000000000..b7666e50b
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/test-console-table.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>Simple webconsole test page</title>
+ </head>
+ <body>
+ <p>console.table() test page</p>
+ <script>
+ function doConsoleTable(data, constrainedHeaders = null) {
+ if (constrainedHeaders) {
+ console.table(data, constrainedHeaders);
+ } else {
+ console.table(data);
+ }
+ }
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/new-console-output/test/mochitest/test-console.html b/devtools/client/webconsole/new-console-output/test/mochitest/test-console.html
new file mode 100644
index 000000000..7ef09d9a1
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/test-console.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>Simple webconsole test page</title>
+ </head>
+ <body>
+ <p>Simple webconsole test page</p>
+ <script>
+ function doLogs(num) {
+ num = num || 1;
+ for (var i = 0; i < num; i++) {
+ console.log(i);
+ }
+ }
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/new-console-output/test/moz.build b/devtools/client/webconsole/new-console-output/test/moz.build
new file mode 100644
index 000000000..da06c3162
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/moz.build
@@ -0,0 +1,17 @@
+# vim: set filetype=python:
+# 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/.
+
+BROWSER_CHROME_MANIFESTS += [
+ 'fixtures/stub-generators/browser.ini',
+ 'mochitest/browser.ini',
+]
+
+DIRS += [
+ 'fixtures'
+]
+
+MOCHITEST_CHROME_MANIFESTS += [
+ 'chrome/chrome.ini',
+]
diff --git a/devtools/client/webconsole/new-console-output/test/requireHelper.js b/devtools/client/webconsole/new-console-output/test/requireHelper.js
new file mode 100644
index 000000000..ac6205808
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/requireHelper.js
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const requireHacker = require("require-hacker");
+
+requireHacker.global_hook("default", path => {
+ switch (path) {
+ // For Enzyme
+ case "react-dom/server":
+ return `const React = require('react-dev'); module.exports = React`;
+ case "react-addons-test-utils":
+ return `const React = require('react-dev'); module.exports = React.addons.TestUtils`;
+ // Use react-dev. This would be handled by browserLoader in Firefox.
+ case "react":
+ case "devtools/client/shared/vendor/react":
+ return `const React = require('react-dev'); module.exports = React`;
+ // For Rep's use of AMD
+ case "devtools/client/shared/vendor/react.default":
+ return `const React = require('react-dev'); module.exports = React`;
+ }
+
+ // Some modules depend on Chrome APIs which don't work in mocha. When such a module
+ // is required, replace it with a mock version.
+ switch (path) {
+ case "devtools/client/webconsole/utils":
+ return `module.exports = require("devtools/client/webconsole/new-console-output/test/fixtures/WebConsoleUtils")`;
+ case "devtools/shared/l10n":
+ return `module.exports = require("devtools/client/webconsole/new-console-output/test/fixtures/LocalizationHelper")`;
+ case "devtools/shared/plural-form":
+ return `module.exports = require("devtools/client/webconsole/new-console-output/test/fixtures/PluralForm")`;
+ case "Services":
+ case "Services.default":
+ return `module.exports = require("devtools/client/webconsole/new-console-output/test/fixtures/Services")`;
+ case "devtools/shared/client/main":
+ return `module.exports = require("devtools/client/webconsole/new-console-output/test/fixtures/ObjectClient")`;
+ }
+});
diff --git a/devtools/client/webconsole/new-console-output/test/store/filters.test.js b/devtools/client/webconsole/new-console-output/test/store/filters.test.js
new file mode 100644
index 000000000..3c38a255a
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/store/filters.test.js
@@ -0,0 +1,215 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const expect = require("expect");
+
+const actions = require("devtools/client/webconsole/new-console-output/actions/index");
+const { messageAdd } = require("devtools/client/webconsole/new-console-output/actions/index");
+const { ConsoleCommand } = require("devtools/client/webconsole/new-console-output/types");
+const { getAllMessages } = require("devtools/client/webconsole/new-console-output/selectors/messages");
+const { getAllFilters } = require("devtools/client/webconsole/new-console-output/selectors/filters");
+const { setupStore } = require("devtools/client/webconsole/new-console-output/test/helpers");
+const { MESSAGE_LEVEL } = require("devtools/client/webconsole/new-console-output/constants");
+const { stubPackets } = require("devtools/client/webconsole/new-console-output/test/fixtures/stubs/index");
+const { stubPreparedMessages } = require("devtools/client/webconsole/new-console-output/test/fixtures/stubs/index");
+
+describe("Filtering", () => {
+ let store;
+ let numMessages;
+ // Number of messages in prepareBaseStore which are not filtered out, i.e. Evaluation
+ // Results, console commands and console.groups .
+ const numUnfilterableMessages = 3;
+
+ beforeEach(() => {
+ store = prepareBaseStore();
+ store.dispatch(actions.filtersClear());
+ numMessages = getAllMessages(store.getState()).size;
+ });
+
+ describe("Level filter", () => {
+ it("filters log messages", () => {
+ store.dispatch(actions.filterToggle(MESSAGE_LEVEL.LOG));
+
+ let messages = getAllMessages(store.getState());
+ expect(messages.size).toEqual(numMessages - 3);
+ });
+
+ it("filters debug messages", () => {
+ store.dispatch(actions.filterToggle(MESSAGE_LEVEL.DEBUG));
+
+ let messages = getAllMessages(store.getState());
+ expect(messages.size).toEqual(numMessages - 1);
+ });
+
+ // @TODO add info stub
+ it("filters info messages");
+
+ it("filters warning messages", () => {
+ store.dispatch(actions.filterToggle(MESSAGE_LEVEL.WARN));
+
+ let messages = getAllMessages(store.getState());
+ expect(messages.size).toEqual(numMessages - 1);
+ });
+
+ it("filters error messages", () => {
+ store.dispatch(actions.filterToggle(MESSAGE_LEVEL.ERROR));
+
+ let messages = getAllMessages(store.getState());
+ expect(messages.size).toEqual(numMessages - 1);
+ });
+
+ it("filters xhr messages", () => {
+ let message = stubPreparedMessages.get("XHR GET request");
+ store.dispatch(messageAdd(message));
+
+ let messages = getAllMessages(store.getState());
+ expect(messages.size).toEqual(numMessages);
+
+ store.dispatch(actions.filterToggle("netxhr"));
+ messages = getAllMessages(store.getState());
+ expect(messages.size).toEqual(numMessages + 1);
+ });
+
+ it("filters network messages", () => {
+ let message = stubPreparedMessages.get("GET request");
+ store.dispatch(messageAdd(message));
+
+ let messages = getAllMessages(store.getState());
+ expect(messages.size).toEqual(numMessages);
+
+ store.dispatch(actions.filterToggle("net"));
+ messages = getAllMessages(store.getState());
+ expect(messages.size).toEqual(numMessages + 1);
+ });
+ });
+
+ describe("Text filter", () => {
+ it("matches on value grips", () => {
+ store.dispatch(actions.filterTextSet("danger"));
+
+ let messages = getAllMessages(store.getState());
+ expect(messages.size - numUnfilterableMessages).toEqual(1);
+ });
+
+ it("matches unicode values", () => {
+ store.dispatch(actions.filterTextSet("鼬"));
+
+ let messages = getAllMessages(store.getState());
+ expect(messages.size - numUnfilterableMessages).toEqual(1);
+ });
+
+ it("matches locations", () => {
+ // Add a message with a different filename.
+ let locationMsg =
+ Object.assign({}, stubPackets.get("console.log('foobar', 'test')"));
+ locationMsg.message =
+ Object.assign({}, locationMsg.message, { filename: "search-location-test.js" });
+ store.dispatch(messageAdd(locationMsg));
+
+ store.dispatch(actions.filterTextSet("search-location-test.js"));
+
+ let messages = getAllMessages(store.getState());
+ expect(messages.size - numUnfilterableMessages).toEqual(1);
+ });
+
+ it("matches stacktrace functionName", () => {
+ let traceMessage = stubPackets.get("console.trace()");
+ store.dispatch(messageAdd(traceMessage));
+
+ store.dispatch(actions.filterTextSet("testStacktraceFiltering"));
+
+ let messages = getAllMessages(store.getState());
+ expect(messages.size - numUnfilterableMessages).toEqual(1);
+ });
+
+ it("matches stacktrace location", () => {
+ let traceMessage = stubPackets.get("console.trace()");
+ traceMessage.message =
+ Object.assign({}, traceMessage.message, {
+ filename: "search-location-test.js",
+ lineNumber: 85,
+ columnNumber: 13
+ });
+
+ store.dispatch(messageAdd(traceMessage));
+
+ store.dispatch(actions.filterTextSet("search-location-test.js:85:13"));
+
+ let messages = getAllMessages(store.getState());
+ expect(messages.size - numUnfilterableMessages).toEqual(1);
+ });
+
+ it("restores all messages once text is cleared", () => {
+ store.dispatch(actions.filterTextSet("danger"));
+ store.dispatch(actions.filterTextSet(""));
+
+ let messages = getAllMessages(store.getState());
+ expect(messages.size).toEqual(numMessages);
+ });
+ });
+
+ describe("Combined filters", () => {
+ // @TODO add test
+ it("filters");
+ });
+});
+
+describe("Clear filters", () => {
+ it("clears all filters", () => {
+ const store = setupStore([]);
+
+ // Setup test case
+ store.dispatch(actions.filterToggle(MESSAGE_LEVEL.ERROR));
+ store.dispatch(actions.filterToggle("netxhr"));
+ store.dispatch(actions.filterTextSet("foobar"));
+
+ let filters = getAllFilters(store.getState());
+ expect(filters.toJS()).toEqual({
+ "debug": true,
+ "error": false,
+ "info": true,
+ "log": true,
+ "net": false,
+ "netxhr": true,
+ "warn": true,
+ "text": "foobar"
+ });
+
+ store.dispatch(actions.filtersClear());
+
+ filters = getAllFilters(store.getState());
+ expect(filters.toJS()).toEqual({
+ "debug": true,
+ "error": true,
+ "info": true,
+ "log": true,
+ "net": false,
+ "netxhr": false,
+ "warn": true,
+ "text": ""
+ });
+ });
+});
+
+function prepareBaseStore() {
+ const store = setupStore([
+ // Console API
+ "console.log('foobar', 'test')",
+ "console.warn('danger, will robinson!')",
+ "console.log(undefined)",
+ "console.count('bar')",
+ "console.log('鼬')",
+ // Evaluation Result - never filtered
+ "new Date(0)",
+ // PageError
+ "ReferenceError: asdf is not defined",
+ "console.group('bar')"
+ ]);
+
+ // Console Command - never filtered
+ store.dispatch(messageAdd(new ConsoleCommand({ messageText: `console.warn("x")` })));
+
+ return store;
+}
diff --git a/devtools/client/webconsole/new-console-output/test/store/messages.test.js b/devtools/client/webconsole/new-console-output/test/store/messages.test.js
new file mode 100644
index 000000000..582ca36e3
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/store/messages.test.js
@@ -0,0 +1,353 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const {
+ getAllMessages,
+ getAllMessagesUiById,
+ getAllGroupsById,
+ getCurrentGroup,
+} = require("devtools/client/webconsole/new-console-output/selectors/messages");
+const {
+ setupActions,
+ setupStore
+} = require("devtools/client/webconsole/new-console-output/test/helpers");
+const { stubPackets, stubPreparedMessages } = require("devtools/client/webconsole/new-console-output/test/fixtures/stubs/index");
+const {
+ MESSAGE_TYPE,
+} = require("devtools/client/webconsole/new-console-output/constants");
+
+const expect = require("expect");
+
+describe("Message reducer:", () => {
+ let actions;
+
+ before(() => {
+ actions = setupActions();
+ });
+
+ describe("messagesById", () => {
+ it("adds a message to an empty store", () => {
+ const { dispatch, getState } = setupStore([]);
+
+ const packet = stubPackets.get("console.log('foobar', 'test')");
+ const message = stubPreparedMessages.get("console.log('foobar', 'test')");
+ dispatch(actions.messageAdd(packet));
+
+ const messages = getAllMessages(getState());
+
+ expect(messages.first()).toEqual(message);
+ });
+
+ it("increments repeat on a repeating message", () => {
+ const { dispatch, getState } = setupStore([
+ "console.log('foobar', 'test')",
+ "console.log('foobar', 'test')"
+ ]);
+
+ const packet = stubPackets.get("console.log('foobar', 'test')");
+ dispatch(actions.messageAdd(packet));
+ dispatch(actions.messageAdd(packet));
+
+ const messages = getAllMessages(getState());
+
+ expect(messages.size).toBe(1);
+ expect(messages.first().repeat).toBe(4);
+ });
+
+ it("does not clobber a unique message", () => {
+ const { dispatch, getState } = setupStore([
+ "console.log('foobar', 'test')",
+ "console.log('foobar', 'test')"
+ ]);
+
+ const packet = stubPackets.get("console.log('foobar', 'test')");
+ dispatch(actions.messageAdd(packet));
+
+ const packet2 = stubPackets.get("console.log(undefined)");
+ dispatch(actions.messageAdd(packet2));
+
+ const messages = getAllMessages(getState());
+
+ expect(messages.size).toBe(2);
+ expect(messages.first().repeat).toBe(3);
+ expect(messages.last().repeat).toBe(1);
+ });
+
+ it("adds a message in response to console.clear()", () => {
+ const { dispatch, getState } = setupStore([]);
+
+ dispatch(actions.messageAdd(stubPackets.get("console.clear()")));
+
+ const messages = getAllMessages(getState());
+
+ expect(messages.size).toBe(1);
+ expect(messages.first().parameters[0]).toBe("Console was cleared.");
+ });
+
+ it("clears the messages list in response to MESSAGES_CLEAR action", () => {
+ const { dispatch, getState } = setupStore([
+ "console.log('foobar', 'test')",
+ "console.log(undefined)"
+ ]);
+
+ dispatch(actions.messagesClear());
+
+ const messages = getAllMessages(getState());
+ expect(messages.size).toBe(0);
+ });
+
+ it("limits the number of messages displayed", () => {
+ const { dispatch, getState } = setupStore([]);
+
+ const logLimit = 1000;
+ const packet = stubPackets.get("console.log(undefined)");
+ for (let i = 1; i <= logLimit + 1; i++) {
+ packet.message.arguments = [`message num ${i}`];
+ dispatch(actions.messageAdd(packet));
+ }
+
+ const messages = getAllMessages(getState());
+ expect(messages.count()).toBe(logLimit);
+ expect(messages.first().parameters[0]).toBe(`message num 2`);
+ expect(messages.last().parameters[0]).toBe(`message num ${logLimit + 1}`);
+ });
+
+ it("does not add null messages to the store", () => {
+ const { dispatch, getState } = setupStore([]);
+
+ const message = stubPackets.get("console.time('bar')");
+ dispatch(actions.messageAdd(message));
+
+ const messages = getAllMessages(getState());
+ expect(messages.size).toBe(0);
+ });
+
+ it("adds console.table call with unsupported type as console.log", () => {
+ const { dispatch, getState } = setupStore([]);
+
+ const packet = stubPackets.get("console.table('bar')");
+ dispatch(actions.messageAdd(packet));
+
+ const messages = getAllMessages(getState());
+ const tableMessage = messages.last();
+ expect(tableMessage.level).toEqual(MESSAGE_TYPE.LOG);
+ });
+
+ it("adds console.group messages to the store", () => {
+ const { dispatch, getState } = setupStore([]);
+
+ const message = stubPackets.get("console.group('bar')");
+ dispatch(actions.messageAdd(message));
+
+ const messages = getAllMessages(getState());
+ expect(messages.size).toBe(1);
+ });
+
+ it("sets groupId property as expected", () => {
+ const { dispatch, getState } = setupStore([]);
+
+ dispatch(actions.messageAdd(
+ stubPackets.get("console.group('bar')")));
+
+ const packet = stubPackets.get("console.log('foobar', 'test')");
+ dispatch(actions.messageAdd(packet));
+
+ const messages = getAllMessages(getState());
+ expect(messages.size).toBe(2);
+ expect(messages.last().groupId).toBe(messages.first().id);
+ });
+
+ it("does not display console.groupEnd messages to the store", () => {
+ const { dispatch, getState } = setupStore([]);
+
+ const message = stubPackets.get("console.groupEnd('bar')");
+ dispatch(actions.messageAdd(message));
+
+ const messages = getAllMessages(getState());
+ expect(messages.size).toBe(0);
+ });
+
+ it("filters out message added after a console.groupCollapsed message", () => {
+ const { dispatch, getState } = setupStore([]);
+
+ const message = stubPackets.get("console.groupCollapsed('foo')");
+ dispatch(actions.messageAdd(message));
+
+ dispatch(actions.messageAdd(
+ stubPackets.get("console.log('foobar', 'test')")));
+
+ const messages = getAllMessages(getState());
+ expect(messages.size).toBe(1);
+ });
+
+ it("shows the group of the first displayed message when messages are pruned", () => {
+ const { dispatch, getState } = setupStore([]);
+
+ const logLimit = 1000;
+
+ const groupMessage = stubPreparedMessages.get("console.group('bar')");
+ dispatch(actions.messageAdd(
+ stubPackets.get("console.group('bar')")));
+
+ const packet = stubPackets.get("console.log(undefined)");
+ for (let i = 1; i <= logLimit + 1; i++) {
+ packet.message.arguments = [`message num ${i}`];
+ dispatch(actions.messageAdd(packet));
+ }
+
+ const messages = getAllMessages(getState());
+ expect(messages.count()).toBe(logLimit);
+ expect(messages.first().messageText).toBe(groupMessage.messageText);
+ expect(messages.get(1).parameters[0]).toBe(`message num 3`);
+ expect(messages.last().parameters[0]).toBe(`message num ${logLimit + 1}`);
+ });
+
+ it("adds console.dirxml call as console.log", () => {
+ const { dispatch, getState } = setupStore([]);
+
+ const packet = stubPackets.get("console.dirxml(window)");
+ dispatch(actions.messageAdd(packet));
+
+ const messages = getAllMessages(getState());
+ const dirxmlMessage = messages.last();
+ expect(dirxmlMessage.level).toEqual(MESSAGE_TYPE.LOG);
+ });
+ });
+
+ describe("messagesUiById", () => {
+ it("opens console.trace messages when they are added", () => {
+ const { dispatch, getState } = setupStore([]);
+
+ const message = stubPackets.get("console.trace()");
+ dispatch(actions.messageAdd(message));
+
+ const messages = getAllMessages(getState());
+ const messagesUi = getAllMessagesUiById(getState());
+ expect(messagesUi.size).toBe(1);
+ expect(messagesUi.first()).toBe(messages.first().id);
+ });
+
+ it("clears the messages UI list in response to MESSAGES_CLEAR action", () => {
+ const { dispatch, getState } = setupStore([
+ "console.log('foobar', 'test')",
+ "console.log(undefined)"
+ ]);
+
+ const traceMessage = stubPackets.get("console.trace()");
+ dispatch(actions.messageAdd(traceMessage));
+
+ dispatch(actions.messagesClear());
+
+ const messagesUi = getAllMessagesUiById(getState());
+ expect(messagesUi.size).toBe(0);
+ });
+
+ it("opens console.group messages when they are added", () => {
+ const { dispatch, getState } = setupStore([]);
+
+ const message = stubPackets.get("console.group('bar')");
+ dispatch(actions.messageAdd(message));
+
+ const messages = getAllMessages(getState());
+ const messagesUi = getAllMessagesUiById(getState());
+ expect(messagesUi.size).toBe(1);
+ expect(messagesUi.first()).toBe(messages.first().id);
+ });
+
+ it("does not open console.groupCollapsed messages when they are added", () => {
+ const { dispatch, getState } = setupStore([]);
+
+ const message = stubPackets.get("console.groupCollapsed('foo')");
+ dispatch(actions.messageAdd(message));
+
+ const messagesUi = getAllMessagesUiById(getState());
+ expect(messagesUi.size).toBe(0);
+ });
+ });
+
+ describe("currentGroup", () => {
+ it("sets the currentGroup when console.group message is added", () => {
+ const { dispatch, getState } = setupStore([]);
+
+ const packet = stubPackets.get("console.group('bar')");
+ dispatch(actions.messageAdd(packet));
+
+ const messages = getAllMessages(getState());
+ const currentGroup = getCurrentGroup(getState());
+ expect(currentGroup).toBe(messages.first().id);
+ });
+
+ it("sets currentGroup to expected value when console.groupEnd is added", () => {
+ const { dispatch, getState } = setupStore([
+ "console.group('bar')",
+ "console.groupCollapsed('foo')"
+ ]);
+
+ let messages = getAllMessages(getState());
+ let currentGroup = getCurrentGroup(getState());
+ expect(currentGroup).toBe(messages.last().id);
+
+ const endFooPacket = stubPackets.get("console.groupEnd('foo')");
+ dispatch(actions.messageAdd(endFooPacket));
+ messages = getAllMessages(getState());
+ currentGroup = getCurrentGroup(getState());
+ expect(currentGroup).toBe(messages.first().id);
+
+ const endBarPacket = stubPackets.get("console.groupEnd('foo')");
+ dispatch(actions.messageAdd(endBarPacket));
+ messages = getAllMessages(getState());
+ currentGroup = getCurrentGroup(getState());
+ expect(currentGroup).toBe(null);
+ });
+
+ it("resets the currentGroup to null in response to MESSAGES_CLEAR action", () => {
+ const { dispatch, getState } = setupStore([
+ "console.group('bar')"
+ ]);
+
+ dispatch(actions.messagesClear());
+
+ const currentGroup = getCurrentGroup(getState());
+ expect(currentGroup).toBe(null);
+ });
+ });
+
+ describe("groupsById", () => {
+ it("adds the group with expected array when console.group message is added", () => {
+ const { dispatch, getState } = setupStore([]);
+
+ const barPacket = stubPackets.get("console.group('bar')");
+ dispatch(actions.messageAdd(barPacket));
+
+ let messages = getAllMessages(getState());
+ let groupsById = getAllGroupsById(getState());
+ expect(groupsById.size).toBe(1);
+ expect(groupsById.has(messages.first().id)).toBe(true);
+ expect(groupsById.get(messages.first().id)).toEqual([]);
+
+ const fooPacket = stubPackets.get("console.groupCollapsed('foo')");
+ dispatch(actions.messageAdd(fooPacket));
+ messages = getAllMessages(getState());
+ groupsById = getAllGroupsById(getState());
+ expect(groupsById.size).toBe(2);
+ expect(groupsById.has(messages.last().id)).toBe(true);
+ expect(groupsById.get(messages.last().id)).toEqual([messages.first().id]);
+ });
+
+ it("resets groupsById in response to MESSAGES_CLEAR action", () => {
+ const { dispatch, getState } = setupStore([
+ "console.group('bar')",
+ "console.groupCollapsed('foo')",
+ ]);
+
+ let groupsById = getAllGroupsById(getState());
+ expect(groupsById.size).toBe(2);
+
+ dispatch(actions.messagesClear());
+
+ groupsById = getAllGroupsById(getState());
+ expect(groupsById.size).toBe(0);
+ });
+ });
+});
diff --git a/devtools/client/webconsole/new-console-output/test/utils/getRepeatId.test.js b/devtools/client/webconsole/new-console-output/test/utils/getRepeatId.test.js
new file mode 100644
index 000000000..d27238e14
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/utils/getRepeatId.test.js
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const { getRepeatId } = require("devtools/client/webconsole/new-console-output/utils/messages");
+const { stubPreparedMessages } = require("devtools/client/webconsole/new-console-output/test/fixtures/stubs/index");
+
+const expect = require("expect");
+
+describe("getRepeatId:", () => {
+ it("returns same repeatId for duplicate values", () => {
+ const message1 = stubPreparedMessages.get("console.log('foobar', 'test')");
+ const message2 = message1.set("repeat", 3);
+ expect(getRepeatId(message1)).toEqual(getRepeatId(message2));
+ });
+
+ it("returns different repeatIds for different values", () => {
+ const message1 = stubPreparedMessages.get("console.log('foobar', 'test')");
+ const message2 = message1.set("parameters", ["funny", "monkey"]);
+ expect(getRepeatId(message1)).toNotEqual(getRepeatId(message2));
+ });
+
+ it("returns different repeatIds for different severities", () => {
+ const message1 = stubPreparedMessages.get("console.log('foobar', 'test')");
+ const message2 = message1.set("level", "error");
+ expect(getRepeatId(message1)).toNotEqual(getRepeatId(message2));
+ });
+
+ it("handles falsy values distinctly", () => {
+ const messageNaN = stubPreparedMessages.get("console.log(NaN)");
+ const messageUnd = stubPreparedMessages.get("console.log(undefined)");
+ const messageNul = stubPreparedMessages.get("console.log(null)");
+
+ const repeatIds = new Set([
+ getRepeatId(messageNaN),
+ getRepeatId(messageUnd),
+ getRepeatId(messageNul)]
+ );
+ expect(repeatIds.size).toEqual(3);
+ });
+});
diff --git a/devtools/client/webconsole/new-console-output/types.js b/devtools/client/webconsole/new-console-output/types.js
new file mode 100644
index 000000000..897ae5d3a
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/types.js
@@ -0,0 +1,53 @@
+/* -*- 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 Immutable = require("devtools/client/shared/vendor/immutable");
+
+const {
+ MESSAGE_SOURCE,
+ MESSAGE_TYPE,
+ MESSAGE_LEVEL
+} = require("devtools/client/webconsole/new-console-output/constants");
+
+exports.ConsoleCommand = Immutable.Record({
+ id: null,
+ allowRepeating: false,
+ messageText: null,
+ source: MESSAGE_SOURCE.JAVASCRIPT,
+ type: MESSAGE_TYPE.COMMAND,
+ level: MESSAGE_LEVEL.LOG,
+ groupId: null,
+});
+
+exports.ConsoleMessage = Immutable.Record({
+ id: null,
+ allowRepeating: true,
+ source: null,
+ type: null,
+ level: null,
+ messageText: null,
+ parameters: null,
+ repeat: 1,
+ repeatId: null,
+ stacktrace: null,
+ frame: null,
+ groupId: null,
+ exceptionDocURL: null,
+ userProvidedStyles: null,
+});
+
+exports.NetworkEventMessage = Immutable.Record({
+ id: null,
+ actor: null,
+ level: MESSAGE_LEVEL.LOG,
+ isXHR: false,
+ request: null,
+ response: null,
+ source: MESSAGE_SOURCE.NETWORK,
+ type: MESSAGE_TYPE.LOG,
+ groupId: null,
+});
diff --git a/devtools/client/webconsole/new-console-output/utils/id-generator.js b/devtools/client/webconsole/new-console-output/utils/id-generator.js
new file mode 100644
index 000000000..7d875b750
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/utils/id-generator.js
@@ -0,0 +1,22 @@
+/* -*- 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";
+
+exports.IdGenerator = class IdGenerator {
+ constructor() {
+ this.messageId = 1;
+ }
+
+ getNextId() {
+ // Return the next message id, as a string.
+ return "" + this.messageId++;
+ }
+
+ getCurrentId() {
+ return this.messageId;
+ }
+};
diff --git a/devtools/client/webconsole/new-console-output/utils/messages.js b/devtools/client/webconsole/new-console-output/utils/messages.js
new file mode 100644
index 000000000..f91209e9d
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/utils/messages.js
@@ -0,0 +1,283 @@
+/* -*- 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 WebConsoleUtils = require("devtools/client/webconsole/utils").Utils;
+const STRINGS_URI = "devtools/client/locales/webconsole.properties";
+const l10n = new WebConsoleUtils.L10n(STRINGS_URI);
+
+const {
+ MESSAGE_SOURCE,
+ MESSAGE_TYPE,
+ MESSAGE_LEVEL,
+} = require("../constants");
+const {
+ ConsoleMessage,
+ NetworkEventMessage,
+} = require("../types");
+
+function prepareMessage(packet, idGenerator) {
+ // This packet is already in the expected packet structure. Simply return.
+ if (!packet.source) {
+ packet = transformPacket(packet);
+ }
+
+ if (packet.allowRepeating) {
+ packet = packet.set("repeatId", getRepeatId(packet));
+ }
+ return packet.set("id", idGenerator.getNextId());
+}
+
+/**
+ * Transforms a packet from Firefox RDP structure to Chrome RDP structure.
+ */
+function transformPacket(packet) {
+ if (packet._type) {
+ packet = convertCachedPacket(packet);
+ }
+
+ switch (packet.type) {
+ case "consoleAPICall": {
+ let { message } = packet;
+
+ let parameters = message.arguments;
+ let type = message.level;
+ let level = getLevelFromType(type);
+ let messageText = null;
+ const timer = message.timer;
+
+ // Special per-type conversion.
+ switch (type) {
+ case "clear":
+ // We show a message to users when calls console.clear() is called.
+ parameters = [l10n.getStr("consoleCleared")];
+ break;
+ case "count":
+ // Chrome RDP doesn't have a special type for count.
+ type = MESSAGE_TYPE.LOG;
+ let {counter} = message;
+ let label = counter.label ? counter.label : l10n.getStr("noCounterLabel");
+ messageText = `${label}: ${counter.count}`;
+ parameters = null;
+ break;
+ case "time":
+ // We don't show anything for console.time calls to match Chrome's behaviour.
+ parameters = null;
+ type = MESSAGE_TYPE.NULL_MESSAGE;
+ break;
+ case "timeEnd":
+ parameters = null;
+ if (timer) {
+ // We show the duration to users when calls console.timeEnd() is called,
+ // if corresponding console.time() was called before.
+ let duration = Math.round(timer.duration * 100) / 100;
+ messageText = l10n.getFormatStr("timeEnd", [timer.name, duration]);
+ } else {
+ // If the `timer` property does not exists, we don't output anything.
+ type = MESSAGE_TYPE.NULL_MESSAGE;
+ }
+ break;
+ case "table":
+ const supportedClasses = [
+ "Array", "Object", "Map", "Set", "WeakMap", "WeakSet"];
+ if (
+ !Array.isArray(parameters) ||
+ parameters.length === 0 ||
+ !supportedClasses.includes(parameters[0].class)
+ ) {
+ // If the class of the first parameter is not supported,
+ // we handle the call as a simple console.log
+ type = "log";
+ }
+ break;
+ case "group":
+ type = MESSAGE_TYPE.START_GROUP;
+ parameters = null;
+ messageText = message.groupName || l10n.getStr("noGroupLabel");
+ break;
+ case "groupCollapsed":
+ type = MESSAGE_TYPE.START_GROUP_COLLAPSED;
+ parameters = null;
+ messageText = message.groupName || l10n.getStr("noGroupLabel");
+ break;
+ case "groupEnd":
+ type = MESSAGE_TYPE.END_GROUP;
+ parameters = null;
+ break;
+ case "dirxml":
+ // Handle console.dirxml calls as simple console.log
+ type = "log";
+ break;
+ }
+
+ const frame = message.filename ? {
+ source: message.filename,
+ line: message.lineNumber,
+ column: message.columnNumber,
+ } : null;
+
+ return new ConsoleMessage({
+ source: MESSAGE_SOURCE.CONSOLE_API,
+ type,
+ level,
+ parameters,
+ messageText,
+ stacktrace: message.stacktrace ? message.stacktrace : null,
+ frame,
+ userProvidedStyles: message.styles,
+ });
+ }
+
+ case "navigationMessage": {
+ let { message } = packet;
+ return new ConsoleMessage({
+ source: MESSAGE_SOURCE.CONSOLE_API,
+ type: MESSAGE_TYPE.LOG,
+ level: MESSAGE_LEVEL.LOG,
+ messageText: "Navigated to " + message.url,
+ });
+ }
+
+ case "pageError": {
+ let { pageError } = packet;
+ let level = MESSAGE_LEVEL.ERROR;
+ if (pageError.warning || pageError.strict) {
+ level = MESSAGE_LEVEL.WARN;
+ } else if (pageError.info) {
+ level = MESSAGE_LEVEL.INFO;
+ }
+
+ const frame = pageError.sourceName ? {
+ source: pageError.sourceName,
+ line: pageError.lineNumber,
+ column: pageError.columnNumber
+ } : null;
+
+ return new ConsoleMessage({
+ source: MESSAGE_SOURCE.JAVASCRIPT,
+ type: MESSAGE_TYPE.LOG,
+ level,
+ messageText: pageError.errorMessage,
+ stacktrace: pageError.stacktrace ? pageError.stacktrace : null,
+ frame,
+ exceptionDocURL: pageError.exceptionDocURL,
+ });
+ }
+
+ case "networkEvent": {
+ let { networkEvent } = packet;
+
+ return new NetworkEventMessage({
+ actor: networkEvent.actor,
+ isXHR: networkEvent.isXHR,
+ request: networkEvent.request,
+ response: networkEvent.response,
+ });
+ }
+
+ case "evaluationResult":
+ default: {
+ let {
+ exceptionMessage: messageText,
+ exceptionDocURL,
+ frame,
+ result: parameters
+ } = packet;
+
+ const level = messageText ? MESSAGE_LEVEL.ERROR : MESSAGE_LEVEL.LOG;
+ return new ConsoleMessage({
+ source: MESSAGE_SOURCE.JAVASCRIPT,
+ type: MESSAGE_TYPE.RESULT,
+ level,
+ messageText,
+ parameters,
+ exceptionDocURL,
+ frame,
+ });
+ }
+ }
+}
+
+// Helpers
+function getRepeatId(message) {
+ message = message.toJS();
+ delete message.repeat;
+ return JSON.stringify(message);
+}
+
+function convertCachedPacket(packet) {
+ // The devtools server provides cached message packets in a different shape, so we
+ // transform them here.
+ let convertPacket = {};
+ if (packet._type === "ConsoleAPI") {
+ convertPacket.message = packet;
+ convertPacket.type = "consoleAPICall";
+ } else if (packet._type === "PageError") {
+ convertPacket.pageError = packet;
+ convertPacket.type = "pageError";
+ } else if ("_navPayload" in packet) {
+ convertPacket.type = "navigationMessage";
+ convertPacket.message = packet;
+ } else if (packet._type === "NetworkEvent") {
+ convertPacket.networkEvent = packet;
+ convertPacket.type = "networkEvent";
+ } else {
+ throw new Error("Unexpected packet type");
+ }
+ return convertPacket;
+}
+
+/**
+ * Maps a Firefox RDP type to its corresponding level.
+ */
+function getLevelFromType(type) {
+ const levels = {
+ LEVEL_ERROR: "error",
+ LEVEL_WARNING: "warn",
+ LEVEL_INFO: "info",
+ LEVEL_LOG: "log",
+ LEVEL_DEBUG: "debug",
+ };
+
+ // A mapping from the console API log event levels to the Web Console levels.
+ const levelMap = {
+ error: levels.LEVEL_ERROR,
+ exception: levels.LEVEL_ERROR,
+ assert: levels.LEVEL_ERROR,
+ warn: levels.LEVEL_WARNING,
+ info: levels.LEVEL_INFO,
+ log: levels.LEVEL_LOG,
+ clear: levels.LEVEL_LOG,
+ trace: levels.LEVEL_LOG,
+ table: levels.LEVEL_LOG,
+ debug: levels.LEVEL_LOG,
+ dir: levels.LEVEL_LOG,
+ dirxml: levels.LEVEL_LOG,
+ group: levels.LEVEL_LOG,
+ groupCollapsed: levels.LEVEL_LOG,
+ groupEnd: levels.LEVEL_LOG,
+ time: levels.LEVEL_LOG,
+ timeEnd: levels.LEVEL_LOG,
+ count: levels.LEVEL_DEBUG,
+ };
+
+ return levelMap[type] || MESSAGE_TYPE.LOG;
+}
+
+function isGroupType(type) {
+ return [
+ MESSAGE_TYPE.START_GROUP,
+ MESSAGE_TYPE.START_GROUP_COLLAPSED
+ ].includes(type);
+}
+
+exports.prepareMessage = prepareMessage;
+// Export for use in testing.
+exports.getRepeatId = getRepeatId;
+
+exports.l10n = l10n;
+exports.isGroupType = isGroupType;
diff --git a/devtools/client/webconsole/new-console-output/utils/moz.build b/devtools/client/webconsole/new-console-output/utils/moz.build
new file mode 100644
index 000000000..00378baa4
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/utils/moz.build
@@ -0,0 +1,10 @@
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'id-generator.js',
+ 'messages.js',
+ 'variables-view.js',
+)
diff --git a/devtools/client/webconsole/new-console-output/utils/variables-view.js b/devtools/client/webconsole/new-console-output/utils/variables-view.js
new file mode 100644
index 000000000..3cfee875a
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/utils/variables-view.js
@@ -0,0 +1,20 @@
+/* -*- 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/. */
+
+/* global window */
+"use strict";
+
+/**
+ * @TODO Remove this.
+ *
+ * Once JSTerm is also written in React/Redux, these will be actions.
+ */
+exports.openVariablesView = (objectActor) => {
+ window.jsterm.openVariablesView({
+ objectActor,
+ autofocus: true,
+ });
+};
diff --git a/devtools/client/webconsole/package.json b/devtools/client/webconsole/package.json
new file mode 100644
index 000000000..6349f2057
--- /dev/null
+++ b/devtools/client/webconsole/package.json
@@ -0,0 +1,20 @@
+{
+ "name": "webconsole",
+ "version": "0.0.1",
+ "devDependencies": {
+ "amd-loader": "0.0.5",
+ "babel-preset-es2015": "^6.6.0",
+ "babel-register": "^6.7.2",
+ "enzyme": "^2.4.1",
+ "expect": "^1.16.0",
+ "jsdom": "^9.4.1",
+ "jsdom-global": "^2.0.0",
+ "mocha": "^2.5.3",
+ "require-hacker": "^2.1.4",
+ "sinon": "^1.17.5"
+ },
+ "scripts": {
+ "postinstall": "cd ../ && npm install && cd webconsole",
+ "test": "NODE_PATH=`pwd`/../../../:`pwd`/../../../devtools/client/shared/vendor/ mocha new-console-output/test/**/*.test.js --compilers js:babel-register -r jsdom-global/register -r ./new-console-output/test/requireHelper.js"
+ }
+}
diff --git a/devtools/client/webconsole/panel.js b/devtools/client/webconsole/panel.js
new file mode 100644
index 000000000..3e3a4f4b9
--- /dev/null
+++ b/devtools/client/webconsole/panel.js
@@ -0,0 +1,118 @@
+/* -*- 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");
+
+loader.lazyGetter(this, "HUDService", () => require("devtools/client/webconsole/hudservice"));
+loader.lazyGetter(this, "EventEmitter", () => require("devtools/shared/event-emitter"));
+
+/**
+ * A DevToolPanel that controls the Web Console.
+ */
+function WebConsolePanel(iframeWindow, toolbox) {
+ this._frameWindow = iframeWindow;
+ this._toolbox = toolbox;
+ EventEmitter.decorate(this);
+}
+
+exports.WebConsolePanel = WebConsolePanel;
+
+WebConsolePanel.prototype = {
+ hud: null,
+
+ /**
+ * Called by the WebConsole's onkey command handler.
+ * If the WebConsole is opened, check if the JSTerm's input line has focus.
+ * If not, focus it.
+ */
+ focusInput: function () {
+ this.hud.jsterm.focus();
+ },
+
+ /**
+ * Open is effectively an asynchronous constructor.
+ *
+ * @return object
+ * A promise that is resolved when the Web Console completes opening.
+ */
+ open: function () {
+ let parentDoc = this._toolbox.doc;
+ let iframe = parentDoc.getElementById("toolbox-panel-iframe-webconsole");
+
+ // Make sure the iframe content window is ready.
+ let deferredIframe = promise.defer();
+ let win, doc;
+ if ((win = iframe.contentWindow) &&
+ (doc = win.document) &&
+ doc.readyState == "complete") {
+ deferredIframe.resolve(null);
+ } else {
+ iframe.addEventListener("load", function onIframeLoad() {
+ iframe.removeEventListener("load", onIframeLoad, true);
+ deferredIframe.resolve(null);
+ }, true);
+ }
+
+ // Local debugging needs to make the target remote.
+ let promiseTarget;
+ if (!this.target.isRemote) {
+ promiseTarget = this.target.makeRemote();
+ } else {
+ promiseTarget = promise.resolve(this.target);
+ }
+
+ // 1. Wait for the iframe to load.
+ // 2. Wait for the remote target.
+ // 3. Open the Web Console.
+ return deferredIframe.promise
+ .then(() => promiseTarget)
+ .then((target) => {
+ this._frameWindow._remoteTarget = target;
+
+ let webConsoleUIWindow = iframe.contentWindow.wrappedJSObject;
+ let chromeWindow = iframe.ownerDocument.defaultView;
+ return HUDService.openWebConsole(this.target, webConsoleUIWindow,
+ chromeWindow);
+ })
+ .then((webConsole) => {
+ this.hud = webConsole;
+ this._isReady = true;
+ this.emit("ready");
+ return this;
+ }, (reason) => {
+ let msg = "WebConsolePanel open failed. " +
+ reason.error + ": " + reason.message;
+ dump(msg + "\n");
+ console.error(msg);
+ });
+ },
+
+ get target() {
+ return this._toolbox.target;
+ },
+
+ _isReady: false,
+ get isReady() {
+ return this._isReady;
+ },
+
+ destroy: function () {
+ if (this._destroyer) {
+ return this._destroyer;
+ }
+
+ this._destroyer = this.hud.destroy();
+ this._destroyer.then(() => {
+ this._frameWindow = null;
+ this._toolbox = null;
+ this.emit("destroyed");
+ });
+
+ return this._destroyer;
+ },
+};
diff --git a/devtools/client/webconsole/test/.eslintrc.js b/devtools/client/webconsole/test/.eslintrc.js
new file mode 100644
index 000000000..8d15a76d9
--- /dev/null
+++ b/devtools/client/webconsole/test/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../../.eslintrc.mochitests.js"
+};
diff --git a/devtools/client/webconsole/test/browser.ini b/devtools/client/webconsole/test/browser.ini
new file mode 100644
index 000000000..918411182
--- /dev/null
+++ b/devtools/client/webconsole/test/browser.ini
@@ -0,0 +1,396 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ head.js
+ test-bug-585956-console-trace.html
+ test-bug-593003-iframe-wrong-hud-iframe.html
+ test-bug-593003-iframe-wrong-hud.html
+ test-bug-595934-canvas-css.html
+ test-bug-595934-canvas-css.js
+ test-bug-595934-css-loader.css
+ test-bug-595934-css-loader.css^headers^
+ test-bug-595934-css-loader.html
+ test-bug-595934-css-parser.css
+ test-bug-595934-css-parser.html
+ test-bug-595934-empty-getelementbyid.html
+ test-bug-595934-empty-getelementbyid.js
+ test-bug-595934-html.html
+ test-bug-595934-image.html
+ test-bug-595934-image.jpg
+ test-bug-595934-imagemap.html
+ test-bug-595934-malformedxml-external.html
+ test-bug-595934-malformedxml-external.xml
+ test-bug-595934-malformedxml.xhtml
+ test-bug-595934-svg.xhtml
+ test-bug-595934-workers.html
+ test-bug-595934-workers.js
+ test-bug-597136-external-script-errors.html
+ test-bug-597136-external-script-errors.js
+ test-bug-597756-reopen-closed-tab.html
+ test-bug-599725-response-headers.sjs
+ test-bug-600183-charset.html
+ test-bug-600183-charset.html^headers^
+ test-bug-601177-log-levels.html
+ test-bug-601177-log-levels.js
+ test-bug-603750-websocket.html
+ test-bug-603750-websocket.js
+ test-bug-613013-console-api-iframe.html
+ test-bug-618078-network-exceptions.html
+ test-bug-621644-jsterm-dollar.html
+ test-bug-630733-response-redirect-headers.sjs
+ test-bug-632275-getters.html
+ test-bug-632347-iterators-generators.html
+ test-bug-644419-log-limits.html
+ test-bug-646025-console-file-location.html
+ test-bug-658368-time-methods.html
+ test-bug-737873-mixedcontent.html
+ test-bug-752559-ineffective-iframe-sandbox-warning0.html
+ test-bug-752559-ineffective-iframe-sandbox-warning1.html
+ test-bug-752559-ineffective-iframe-sandbox-warning2.html
+ test-bug-752559-ineffective-iframe-sandbox-warning3.html
+ test-bug-752559-ineffective-iframe-sandbox-warning4.html
+ test-bug-752559-ineffective-iframe-sandbox-warning5.html
+ test-bug-752559-ineffective-iframe-sandbox-warning-inner.html
+ test-bug-752559-ineffective-iframe-sandbox-warning-nested1.html
+ test-bug-752559-ineffective-iframe-sandbox-warning-nested2.html
+ test-bug-762593-insecure-passwords-about-blank-web-console-warning.html
+ test-bug-762593-insecure-passwords-web-console-warning.html
+ test-bug-766001-console-log.js
+ test-bug-766001-js-console-links.html
+ test-bug-766001-js-errors.js
+ test-bug-782653-css-errors-1.css
+ test-bug-782653-css-errors-2.css
+ test-bug-782653-css-errors.html
+ test-bug-837351-security-errors.html
+ test-bug-859170-longstring-hang.html
+ test-bug-869003-iframe.html
+ test-bug-869003-top-window.html
+ test-closure-optimized-out.html
+ test-closures.html
+ test-console-assert.html
+ test-console-clear.html
+ test-console-count.html
+ test-console-count-external-file.js
+ test-console-extras.html
+ test-console-replaced-api.html
+ test-console-server-logging.sjs
+ test-console-server-logging-array.sjs
+ test-console.html
+ test-console-workers.html
+ test-console-table.html
+ test-console-output-02.html
+ test-console-output-03.html
+ test-console-output-04.html
+ test-console-output-dom-elements.html
+ test-console-output-events.html
+ test-console-output-regexp.html
+ test-console-column.html
+ test-consoleiframes.html
+ test-console-trace-async.html
+ test-certificate-messages.html
+ test-cu-reporterror.js
+ test-data.json
+ test-data.json^headers^
+ test-duplicate-error.html
+ test-encoding-ISO-8859-1.html
+ test-error.html
+ test-eval-in-stackframe.html
+ test-file-location.js
+ test-filter.html
+ test-for-of.html
+ test_hpkp-invalid-headers.sjs
+ test_hsts-invalid-headers.sjs
+ test-iframe-762593-insecure-form-action.html
+ test-iframe-762593-insecure-frame.html
+ test-iframe1.html
+ test-iframe2.html
+ test-iframe3.html
+ test-image.png
+ test-mixedcontent-securityerrors.html
+ test-mutation.html
+ test-network-request.html
+ test-network.html
+ test-observe-http-ajax.html
+ test-own-console.html
+ test-property-provider.html
+ test-repeated-messages.html
+ test-result-format-as-string.html
+ test-trackingprotection-securityerrors.html
+ test-webconsole-error-observer.html
+ test_bug_770099_violation.html
+ test_bug_770099_violation.html^headers^
+ test-autocomplete-in-stackframe.html
+ testscript.js
+ test-bug_923281_console_log_filter.html
+ test-bug_923281_test1.js
+ test-bug_923281_test2.js
+ test-bug_939783_console_trace_duplicates.html
+ test-bug-952277-highlight-nodes-in-vview.html
+ test-bug-609872-cd-iframe-parent.html
+ test-bug-609872-cd-iframe-child.html
+ test-bug-989025-iframe-parent.html
+ test-bug_1050691_click_function_to_source.html
+ test-bug_1050691_click_function_to_source.js
+ test-console-api-stackframe.html
+ test-exception-stackframe.html
+ test_bug_1010953_cspro.html^headers^
+ test_bug_1010953_cspro.html
+ test_bug1045902_console_csp_ignore_reflected_xss_message.html^headers^
+ test_bug1045902_console_csp_ignore_reflected_xss_message.html
+ test_bug1092055_shouldwarn.js^headers^
+ test_bug1092055_shouldwarn.js
+ test_bug1092055_shouldwarn.html
+ test_bug_1247459_violation.html
+ !/devtools/client/framework/test/shared-head.js
+ !/devtools/client/netmonitor/test/sjs_cors-test-server.sjs
+ !/image/test/mochitest/blue.png
+
+[browser_bug1045902_console_csp_ignore_reflected_xss_message.js]
+skip-if = (e10s && debug) || (e10s && os == 'win') # Bug 1221499 enabled these on windows
+[browser_bug664688_sandbox_update_after_navigation.js]
+[browser_bug_638949_copy_link_location.js]
+subsuite = clipboard
+[browser_bug_862916_console_dir_and_filter_off.js]
+skip-if = (e10s && (os == 'win' || os == 'mac')) # Bug 1243976
+[browser_bug_865288_repeat_different_objects.js]
+[browser_bug_865871_variables_view_close_on_esc_key.js]
+[browser_bug_869003_inspect_cross_domain_object.js]
+[browser_bug_871156_ctrlw_close_tab.js]
+[browser_cached_messages.js]
+[browser_console.js]
+[browser_console_addonsdk_loader_exception.js]
+[browser_console_clear_method.js]
+[browser_console_clear_on_reload.js]
+[browser_console_click_focus.js]
+[browser_console_consolejsm_output.js]
+[browser_console_copy_command.js]
+subsuite = clipboard
+[browser_console_dead_objects.js]
+skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s
+[browser_console_copy_entire_message_context_menu.js]
+subsuite = clipboard
+[browser_console_error_source_click.js]
+[browser_console_filters.js]
+[browser_console_iframe_messages.js]
+[browser_console_keyboard_accessibility.js]
+[browser_console_log_inspectable_object.js]
+[browser_console_native_getters.js]
+[browser_console_navigation_marker.js]
+[browser_console_netlogging.js]
+[browser_console_nsiconsolemessage.js]
+[browser_console_optimized_out_vars.js]
+[browser_console_private_browsing.js]
+skip-if = e10s # Bug 1042253 - webconsole e10s tests
+[browser_console_server_logging.js]
+[browser_console_variables_view.js]
+[browser_console_variables_view_filter.js]
+[browser_console_variables_view_dom_nodes.js]
+[browser_console_variables_view_dont_sort_non_sortable_classes_properties.js]
+[browser_console_variables_view_special_names.js]
+[browser_console_variables_view_while_debugging.js]
+[browser_console_variables_view_while_debugging_and_inspecting.js]
+[browser_eval_in_debugger_stackframe.js]
+[browser_eval_in_debugger_stackframe2.js]
+[browser_jsterm_inspect.js]
+skip-if = e10s && debug && (os == 'win' || os == 'mac') # Bug 1243966
+[browser_longstring_hang.js]
+[browser_output_breaks_after_console_dir_uninspectable.js]
+[browser_output_longstring_expand.js]
+[browser_repeated_messages_accuracy.js]
+[browser_result_format_as_string.js]
+[browser_warn_user_about_replaced_api.js]
+[browser_webconsole_allow_mixedcontent_securityerrors.js]
+tags = mcb
+[browser_webconsole_script_errordoc_urls.js]
+skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s
+[browser_webconsole_assert.js]
+[browser_webconsole_block_mixedcontent_securityerrors.js]
+tags = mcb
+[browser_webconsole_bug_579412_input_focus.js]
+[browser_webconsole_bug_580001_closing_after_completion.js]
+[browser_webconsole_bug_580030_errors_after_page_reload.js]
+[browser_webconsole_bug_580454_timestamp_l10n.js]
+[browser_webconsole_bug_582201_duplicate_errors.js]
+[browser_webconsole_bug_583816_No_input_and_Tab_key_pressed.js]
+[browser_webconsole_bug_585237_line_limit.js]
+[browser_webconsole_bug_585956_console_trace.js]
+[browser_webconsole_bug_585991_autocomplete_keys.js]
+[browser_webconsole_bug_585991_autocomplete_popup.js]
+[browser_webconsole_bug_586388_select_all.js]
+[browser_webconsole_bug_587617_output_copy.js]
+subsuite = clipboard
+[browser_webconsole_bug_588342_document_focus.js]
+[browser_webconsole_bug_588730_text_node_insertion.js]
+[browser_webconsole_bug_588967_input_expansion.js]
+[browser_webconsole_bug_589162_css_filter.js]
+[browser_webconsole_bug_592442_closing_brackets.js]
+[browser_webconsole_bug_593003_iframe_wrong_hud.js]
+[browser_webconsole_bug_594497_history_arrow_keys.js]
+[browser_webconsole_bug_595223_file_uri.js]
+[browser_webconsole_bug_595350_multiple_windows_and_tabs.js]
+skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s
+[browser_webconsole_bug_595934_message_categories.js]
+skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s
+[browser_webconsole_bug_597103_deactivateHUDForContext_unfocused_window.js]
+[browser_webconsole_bug_597136_external_script_errors.js]
+[browser_webconsole_bug_597136_network_requests_from_chrome.js]
+[browser_webconsole_bug_597460_filter_scroll.js]
+[browser_webconsole_bug_597756_reopen_closed_tab.js]
+[browser_webconsole_bug_599725_response_headers.js]
+[browser_webconsole_bug_600183_charset.js]
+[browser_webconsole_bug_601177_log_levels.js]
+[browser_webconsole_bug_601352_scroll.js]
+[browser_webconsole_bug_601667_filter_buttons.js]
+[browser_webconsole_bug_603750_websocket.js]
+[browser_webconsole_bug_611795.js]
+[browser_webconsole_bug_613013_console_api_iframe.js]
+[browser_webconsole_bug_613280_jsterm_copy.js]
+subsuite = clipboard
+[browser_webconsole_bug_613642_maintain_scroll.js]
+[browser_webconsole_bug_613642_prune_scroll.js]
+[browser_webconsole_bug_614793_jsterm_scroll.js]
+[browser_webconsole_bug_618078_network_exceptions.js]
+[browser_webconsole_bug_621644_jsterm_dollar.js]
+[browser_webconsole_bug_622303_persistent_filters.js]
+[browser_webconsole_bug_623749_ctrl_a_select_all_winnt.js]
+skip-if = os != "win"
+[browser_webconsole_bug_630733_response_redirect_headers.js]
+[browser_webconsole_bug_632275_getters_document_width.js]
+[browser_webconsole_bug_632347_iterators_generators.js]
+skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s
+[browser_webconsole_bug_632817.js]
+skip-if = true # Bug 1244707
+[browser_webconsole_bug_642108_pruneTest.js]
+[browser_webconsole_autocomplete_and_selfxss.js]
+subsuite = clipboard
+[browser_webconsole_bug_644419_log_limits.js]
+[browser_webconsole_bug_646025_console_file_location.js]
+[browser_webconsole_bug_651501_document_body_autocomplete.js]
+[browser_webconsole_bug_653531_highlighter_console_helper.js]
+skip-if = true # Requires direct access to content nodes
+[browser_webconsole_bug_658368_time_methods.js]
+[browser_webconsole_bug_659907_console_dir.js]
+[browser_webconsole_bug_660806_history_nav.js]
+[browser_webconsole_bug_664131_console_group.js]
+[browser_webconsole_bug_686937_autocomplete_JSTerm_helpers.js]
+[browser_webconsole_bug_704295.js]
+[browser_webconsole_bug_734061_No_input_change_and_Tab_key_pressed.js]
+[browser_webconsole_bug_737873_mixedcontent.js]
+tags = mcb
+[browser_webconsole_bug_752559_ineffective_iframe_sandbox_warning.js]
+[browser_webconsole_bug_762593_insecure_passwords_about_blank_web_console_warning.js]
+[browser_webconsole_bug_762593_insecure_passwords_web_console_warning.js]
+skip-if = true # Bug 1110500 - mouse event failure in test
+[browser_webconsole_bug_764572_output_open_url.js]
+skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s
+[browser_webconsole_bug_766001_JS_Console_in_Debugger.js]
+[browser_webconsole_bug_770099_violation.js]
+skip-if = e10s && (os == 'win' || os == 'mac') # Bug 1243978
+[browser_webconsole_bug_782653_CSS_links_in_Style_Editor.js]
+[browser_webconsole_bug_804845_ctrl_key_nav.js]
+skip-if = os != "mac"
+[browser_webconsole_bug_817834_add_edited_input_to_history.js]
+[browser_webconsole_bug_837351_securityerrors.js]
+[browser_webconsole_filter_buttons_contextmenu.js]
+[browser_webconsole_bug_1006027_message_timestamps_incorrect.js]
+skip-if = e10s # Bug 1042253 - webconsole e10s tests (Linux debug intermittent)
+[browser_webconsole_bug_1010953_cspro.js]
+skip-if = e10s && (os == 'win' || os == 'mac') # Bug 1243967
+[browser_webconsole_bug_1247459_violation.js]
+skip-if = e10s && (os == 'win') # Bug 1264955
+[browser_webconsole_certificate_messages.js]
+skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s
+[browser_webconsole_show_subresource_security_errors.js]
+skip-if = e10s && (os == 'win' || os == 'mac') # Bug 1243987
+[browser_webconsole_cached_autocomplete.js]
+[browser_webconsole_chrome.js]
+[browser_webconsole_clear_method.js]
+[browser_webconsole_clickable_urls.js]
+[browser_webconsole_closure_inspection.js]
+[browser_webconsole_completion.js]
+[browser_webconsole_console_extras.js]
+[browser_webconsole_console_logging_api.js]
+[browser_webconsole_console_logging_workers_api.js]
+[browser_webconsole_console_trace_async.js]
+[browser_webconsole_count.js]
+[browser_webconsole_dont_navigate_on_doubleclick.js]
+[browser_webconsole_execution_scope.js]
+[browser_webconsole_for_of.js]
+[browser_webconsole_history.js]
+[browser_webconsole_hpkp_invalid-headers.js]
+[browser_webconsole_hsts_invalid-headers.js]
+skip-if = e10s # Bug 1042253 - webconsole e10s tests
+[browser_webconsole_input_field_focus_on_panel_select.js]
+[browser_webconsole_inspect-parsed-documents.js]
+[browser_webconsole_js_input_expansion.js]
+[browser_webconsole_jsterm.js]
+skip-if = e10s # Bug 1042253 - webconsole e10s tests (Linux debug timeout)
+[browser_webconsole_live_filtering_of_message_types.js]
+[browser_webconsole_live_filtering_on_search_strings.js]
+[browser_webconsole_message_node_id.js]
+[browser_webconsole_multiline_input.js]
+[browser_webconsole_netlogging.js]
+skip-if = true # Bug 1298364
+[browser_webconsole_netlogging_basic.js]
+[browser_webconsole_netlogging_panel.js]
+[browser_webconsole_netlogging_reset_filter.js]
+[browser_webconsole_notifications.js]
+[browser_webconsole_open-links-without-callback.js]
+[browser_webconsole_promise.js]
+[browser_webconsole_output_copy_newlines.js]
+subsuite = clipboard
+[browser_webconsole_output_order.js]
+[browser_webconsole_property_provider.js]
+skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s
+[browser_webconsole_scratchpad_panel_link.js]
+[browser_webconsole_split.js]
+[browser_webconsole_split_escape_key.js]
+[browser_webconsole_split_focus.js]
+[browser_webconsole_split_persist.js]
+[browser_webconsole_trackingprotection_errors.js]
+tags = trackingprotection
+[browser_webconsole_view_source.js]
+[browser_webconsole_reflow.js]
+[browser_webconsole_log_file_filter.js]
+[browser_webconsole_expandable_timestamps.js]
+[browser_webconsole_autocomplete_accessibility.js]
+[browser_webconsole_autocomplete_in_debugger_stackframe.js]
+[browser_webconsole_autocomplete_popup_close_on_tab_switch.js]
+[browser_webconsole_autocomplete-properties-with-non-alphanumeric-names.js]
+[browser_console_hide_jsterm_when_devtools_chrome_enabled_false.js]
+[browser_console_history_persist.js]
+[browser_webconsole_output_01.js]
+[browser_webconsole_output_02.js]
+[browser_webconsole_output_03.js]
+[browser_webconsole_output_04.js]
+[browser_webconsole_output_05.js]
+[browser_webconsole_output_06.js]
+[browser_webconsole_output_dom_elements_01.js]
+[browser_webconsole_output_dom_elements_02.js]
+skip-if = e10s # Bug 1042253 - webconsole e10s tests (Linux debug timeout)
+[browser_webconsole_output_dom_elements_03.js]
+skip-if = e10s # Bug 1241019
+[browser_webconsole_output_dom_elements_04.js]
+skip-if = e10s # Bug 1042253 - webconsole e10s tests (Linux debug timeout)
+[browser_webconsole_output_dom_elements_05.js]
+[browser_webconsole_output_events.js]
+[browser_webconsole_output_regexp.js]
+[browser_webconsole_output_table.js]
+[browser_console_variables_view_highlighter.js]
+[browser_webconsole_start_netmon_first.js]
+[browser_webconsole_console_trace_duplicates.js]
+[browser_webconsole_cd_iframe.js]
+[browser_webconsole_autocomplete_crossdomain_iframe.js]
+[browser_webconsole_console_custom_styles.js]
+[browser_webconsole_console_api_stackframe.js]
+[browser_webconsole_exception_stackframe.js]
+[browser_webconsole_column_numbers.js]
+[browser_console_open_or_focus.js]
+[browser_webconsole_bug_922212_console_dirxml.js]
+[browser_webconsole_shows_reqs_in_netmonitor.js]
+[browser_netmonitor_shows_reqs_in_webconsole.js]
+[browser_webconsole_bug_1050691_click_function_to_source.js]
+[browser_webconsole_context_menu_open_in_var_view.js]
+[browser_webconsole_context_menu_store_as_global.js]
+[browser_webconsole_strict_mode_errors.js]
diff --git a/devtools/client/webconsole/test/browser_bug1045902_console_csp_ignore_reflected_xss_message.js b/devtools/client/webconsole/test/browser_bug1045902_console_csp_ignore_reflected_xss_message.js
new file mode 100644
index 000000000..cfbf61795
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_bug1045902_console_csp_ignore_reflected_xss_message.js
@@ -0,0 +1,52 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests that a file with an unsupported CSP directive ('reflected-xss filter')
+// displays the appropriate message to the console.
+
+"use strict";
+
+const EXPECTED_RESULT = "Not supporting directive \u2018reflected-xss\u2019. " +
+ "Directive and values will be ignored.";
+const TEST_FILE = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test_bug1045902_console_csp_ignore_reflected_xss_" +
+ "message.html";
+
+var hud = undefined;
+
+var TEST_URI = "data:text/html;charset=utf8,Web Console CSP ignoring " +
+ "reflected XSS (bug 1045902)";
+
+add_task(function* () {
+ let { browser } = yield loadTab(TEST_URI);
+
+ hud = yield openConsole();
+
+ yield loadDocument(browser);
+ yield testViolationMessage();
+
+ hud = null;
+});
+
+function loadDocument(browser) {
+ hud.jsterm.clearOutput();
+ browser.loadURI(TEST_FILE);
+ return BrowserTestUtils.browserLoaded(browser);
+}
+
+function testViolationMessage() {
+ let aOutputNode = hud.outputNode;
+
+ return waitForSuccess({
+ name: "Confirming that CSP logs messages to the console when " +
+ "\u2018reflected-xss\u2019 directive is used!",
+ validator: function () {
+ console.log(aOutputNode.textContent);
+ let success = false;
+ success = aOutputNode.textContent.indexOf(EXPECTED_RESULT) > -1;
+ return success;
+ }
+ });
+}
diff --git a/devtools/client/webconsole/test/browser_bug664688_sandbox_update_after_navigation.js b/devtools/client/webconsole/test/browser_bug664688_sandbox_update_after_navigation.js
new file mode 100644
index 000000000..1aacb61c1
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_bug664688_sandbox_update_after_navigation.js
@@ -0,0 +1,92 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests if the JSTerm sandbox is updated when the user navigates from one
+// domain to another, in order to avoid permission denied errors with a sandbox
+// created for a different origin.
+
+"use strict";
+
+add_task(function* () {
+ const TEST_URI1 = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console.html";
+ const TEST_URI2 = "http://example.org/browser/devtools/client/webconsole/" +
+ "test/test-console.html";
+
+ yield loadTab(TEST_URI1);
+ let hud = yield openConsole();
+
+ hud.jsterm.clearOutput();
+ hud.jsterm.execute("window.location.href");
+
+ info("wait for window.location.href");
+
+ let msgForLocation1 = {
+ webconsole: hud,
+ messages: [
+ {
+ name: "window.location.href jsterm input",
+ text: "window.location.href",
+ category: CATEGORY_INPUT,
+ },
+ {
+ name: "window.location.href result is displayed",
+ text: TEST_URI1,
+ category: CATEGORY_OUTPUT,
+ },
+ ],
+ };
+
+ yield waitForMessages(msgForLocation1);
+
+ // load second url
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_URI2);
+ yield loadBrowser(gBrowser.selectedBrowser);
+
+ is(hud.outputNode.textContent.indexOf("Permission denied"), -1,
+ "no permission denied errors");
+
+ hud.jsterm.clearOutput();
+ hud.jsterm.execute("window.location.href");
+
+ info("wait for window.location.href after page navigation");
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ name: "window.location.href jsterm input",
+ text: "window.location.href",
+ category: CATEGORY_INPUT,
+ },
+ {
+ name: "window.location.href result is displayed",
+ text: TEST_URI2,
+ category: CATEGORY_OUTPUT,
+ },
+ ],
+ });
+
+ is(hud.outputNode.textContent.indexOf("Permission denied"), -1,
+ "no permission denied errors");
+
+ // Navigation clears messages. Wait for that clear to happen before
+ // continuing the test or it might destroy messages we wait later on (Bug
+ // 1270234).
+ let cleared = hud.jsterm.once("messages-cleared");
+
+ gBrowser.goBack();
+
+ info("Waiting for messages to be cleared due to navigation");
+ yield cleared;
+
+ info("Messages cleared after navigation; checking location");
+ hud.jsterm.execute("window.location.href");
+
+ info("wait for window.location.href after goBack()");
+ yield waitForMessages(msgForLocation1);
+ is(hud.outputNode.textContent.indexOf("Permission denied"), -1,
+ "no permission denied errors");
+});
diff --git a/devtools/client/webconsole/test/browser_bug_638949_copy_link_location.js b/devtools/client/webconsole/test/browser_bug_638949_copy_link_location.js
new file mode 100644
index 000000000..54bdbe499
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_bug_638949_copy_link_location.js
@@ -0,0 +1,107 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Test for the "Copy link location" context menu item shown when you right
+// click network requests in the output.
+
+"use strict";
+
+add_task(function* () {
+ const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console.html?_date=" + Date.now();
+ const COMMAND_NAME = "consoleCmd_copyURL";
+ const CONTEXT_MENU_ID = "#menu_copyURL";
+
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("devtools.webconsole.filter.networkinfo");
+ });
+
+ Services.prefs.setBoolPref("devtools.webconsole.filter.networkinfo", true);
+
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+ let output = hud.outputNode;
+ let menu = hud.iframeWindow.document.getElementById("output-contextmenu");
+
+ hud.jsterm.clearOutput();
+ content.console.log("bug 638949");
+
+ // Test that the "Copy Link Location" command is disabled for non-network
+ // messages.
+ let [result] = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "bug 638949",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+
+ output.focus();
+ let message = [...result.matched][0];
+
+ goUpdateCommand(COMMAND_NAME);
+ ok(!isEnabled(), COMMAND_NAME + " is disabled");
+
+ // Test that the "Copy Link Location" menu item is hidden for non-network
+ // messages.
+ yield waitForContextMenu(menu, message, () => {
+ let isHidden = menu.querySelector(CONTEXT_MENU_ID).hidden;
+ ok(isHidden, CONTEXT_MENU_ID + " is hidden");
+ });
+
+ hud.jsterm.clearOutput();
+ // Reloading will produce network logging
+ content.location.reload();
+
+ // Test that the "Copy Link Location" command is enabled and works
+ // as expected for any network-related message.
+ // This command should copy only the URL.
+ [result] = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "test-console.html",
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_LOG,
+ }],
+ });
+
+ output.focus();
+ message = [...result.matched][0];
+ hud.ui.output.selectMessage(message);
+
+ goUpdateCommand(COMMAND_NAME);
+ ok(isEnabled(), COMMAND_NAME + " is enabled");
+
+ info("expected clipboard value: " + message.url);
+
+ let deferred = promise.defer();
+
+ waitForClipboard((aData) => {
+ return aData.trim() == message.url;
+ }, () => {
+ goDoCommand(COMMAND_NAME);
+ }, () => {
+ deferred.resolve(null);
+ }, () => {
+ deferred.reject(null);
+ });
+
+ yield deferred.promise;
+
+ // Test that the "Copy Link Location" menu item is visible for network-related
+ // messages.
+ yield waitForContextMenu(menu, message, () => {
+ let isVisible = !menu.querySelector(CONTEXT_MENU_ID).hidden;
+ ok(isVisible, CONTEXT_MENU_ID + " is visible");
+ });
+
+ // Return whether "Copy Link Location" command is enabled or not.
+ function isEnabled() {
+ let controller = top.document.commandDispatcher
+ .getControllerForCommand(COMMAND_NAME);
+ return controller && controller.isCommandEnabled(COMMAND_NAME);
+ }
+});
diff --git a/devtools/client/webconsole/test/browser_bug_862916_console_dir_and_filter_off.js b/devtools/client/webconsole/test/browser_bug_862916_console_dir_and_filter_off.js
new file mode 100644
index 000000000..9d04076ee
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_bug_862916_console_dir_and_filter_off.js
@@ -0,0 +1,31 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Check that the output for console.dir() works even if Logging filter is off.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf8,<p>test for bug 862916";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+
+ ok(hud, "web console opened");
+
+ hud.setFilterState("log", false);
+ registerCleanupFunction(() => hud.setFilterState("log", true));
+
+ hud.jsterm.execute("window.fooBarz = 'bug862916'; " +
+ "console.dir(window)");
+
+ let varView = yield hud.jsterm.once("variablesview-fetched");
+ ok(varView, "variables view object");
+
+ yield findVariableViewProperties(varView, [
+ { name: "fooBarz", value: "bug862916" },
+ ], { webconsole: hud });
+});
+
diff --git a/devtools/client/webconsole/test/browser_bug_865288_repeat_different_objects.js b/devtools/client/webconsole/test/browser_bug_865288_repeat_different_objects.js
new file mode 100644
index 000000000..86ab5bd39
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_bug_865288_repeat_different_objects.js
@@ -0,0 +1,63 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Test that makes sure messages are not considered repeated when console.log()
+// is invoked with different objects, see bug 865288.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-repeated-messages.html";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+
+ info("waiting for 3 console.log objects");
+
+ hud.jsterm.clearOutput(true);
+ hud.jsterm.execute("window.testConsoleObjects()");
+
+ let [result] = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "3 console.log messages",
+ text: "abba",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ count: 3,
+ repeats: 1,
+ objects: true,
+ }],
+ });
+
+ let msgs = [...result.matched];
+ is(msgs.length, 3, "3 message elements");
+
+ for (let i = 0; i < msgs.length; i++) {
+ info("test message element #" + i);
+
+ let msg = msgs[i];
+ let clickable = msg.querySelector(".message-body a");
+ ok(clickable, "clickable object #" + i);
+
+ msg.scrollIntoView(false);
+ yield clickObject(clickable, i);
+ }
+
+ function* clickObject(obj, i) {
+ executeSoon(() => {
+ EventUtils.synthesizeMouse(obj, 2, 2, {}, hud.iframeWindow);
+ });
+
+ let varView = yield hud.jsterm.once("variablesview-fetched");
+ ok(varView, "variables view fetched #" + i);
+
+ yield findVariableViewProperties(varView, [
+ { name: "id", value: "abba" + i },
+ ], { webconsole: hud });
+ }
+});
+
diff --git a/devtools/client/webconsole/test/browser_bug_865871_variables_view_close_on_esc_key.js b/devtools/client/webconsole/test/browser_bug_865871_variables_view_close_on_esc_key.js
new file mode 100644
index 000000000..044525b28
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_bug_865871_variables_view_close_on_esc_key.js
@@ -0,0 +1,75 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Check that the variables view sidebar can be closed by pressing Escape in the
+// web console.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-eval-in-stackframe.html";
+
+function test() {
+ let hud;
+
+ Task.spawn(runner).then(finishTest);
+
+ function* runner() {
+ let {tab} = yield loadTab(TEST_URI);
+ hud = yield openConsole(tab);
+ let jsterm = hud.jsterm;
+ let result;
+ let vview;
+ let msg;
+
+ yield openSidebar("fooObj",
+ 'testProp: "testValue"',
+ { name: "testProp", value: "testValue" });
+
+ let prop = result.matchedProp;
+ ok(prop, "matched the |testProp| property in the variables view");
+
+ vview.window.focus();
+
+ let sidebarClosed = jsterm.once("sidebar-closed");
+ EventUtils.synthesizeKey("VK_ESCAPE", {});
+ yield sidebarClosed;
+
+ jsterm.clearOutput();
+
+ yield openSidebar("window.location",
+ "Location \u2192 http://example.com/browser/",
+ { name: "host", value: "example.com" });
+
+ vview.window.focus();
+
+ msg.scrollIntoView();
+ sidebarClosed = jsterm.once("sidebar-closed");
+ EventUtils.synthesizeKey("VK_ESCAPE", {});
+ yield sidebarClosed;
+
+ function* openSidebar(objName, expectedText, expectedObj) {
+ msg = yield jsterm.execute(objName);
+ ok(msg, "output message found");
+
+ let anchor = msg.querySelector("a");
+ let body = msg.querySelector(".message-body");
+ ok(anchor, "object anchor");
+ ok(body, "message body");
+ ok(body.textContent.includes(expectedText), "message text check");
+
+ msg.scrollIntoView();
+ yield EventUtils.synthesizeMouse(anchor, 2, 2, {}, hud.iframeWindow);
+
+ let vviewVar = yield jsterm.once("variablesview-fetched");
+ vview = vviewVar._variablesView;
+ ok(vview, "variables view object exists");
+
+ [result] = yield findVariableViewProperties(vviewVar, [
+ expectedObj,
+ ], { webconsole: hud });
+ }
+ }
+}
diff --git a/devtools/client/webconsole/test/browser_bug_869003_inspect_cross_domain_object.js b/devtools/client/webconsole/test/browser_bug_869003_inspect_cross_domain_object.js
new file mode 100644
index 000000000..685148fc7
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_bug_869003_inspect_cross_domain_object.js
@@ -0,0 +1,77 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Check that users can inspect objects logged from cross-domain iframes -
+// bug 869003.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-bug-869003-top-window.html";
+
+add_task(function* () {
+ // This test is slightly more involved: it opens the web console, then the
+ // variables view for a given object, it updates a property in the view and
+ // checks the result. We can get a timeout with debug builds on slower
+ // machines.
+ requestLongerTimeout(2);
+
+ yield loadTab("data:text/html;charset=utf8,<p>hello");
+ let hud = yield openConsole();
+
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_URI);
+
+ let [result] = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "console.log message",
+ text: "foobar",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ objects: true,
+ }],
+ });
+
+ let msg = [...result.matched][0];
+ ok(msg, "message element");
+
+ let body = msg.querySelector(".message-body");
+ ok(body, "message body");
+
+ let clickable = result.clickableElements[0];
+ ok(clickable, "clickable object found");
+ ok(body.textContent.includes('{ hello: "world!",'), "message text check");
+
+ executeSoon(() => {
+ EventUtils.synthesizeMouse(clickable, 2, 2, {}, hud.iframeWindow);
+ });
+
+ let aVar = yield hud.jsterm.once("variablesview-fetched");
+ ok(aVar, "variables view fetched");
+ ok(aVar._variablesView, "variables view object");
+
+ [result] = yield findVariableViewProperties(aVar, [
+ { name: "hello", value: "world!" },
+ { name: "bug", value: 869003 },
+ ], { webconsole: hud });
+
+ let prop = result.matchedProp;
+ ok(prop, "matched the |hello| property in the variables view");
+
+ // Check that property value updates work.
+ aVar = yield updateVariablesViewProperty({
+ property: prop,
+ field: "value",
+ string: "'omgtest'",
+ webconsole: hud,
+ });
+
+ info("onFetchAfterUpdate");
+
+ yield findVariableViewProperties(aVar, [
+ { name: "hello", value: "omgtest" },
+ { name: "bug", value: 869003 },
+ ], { webconsole: hud });
+});
diff --git a/devtools/client/webconsole/test/browser_bug_871156_ctrlw_close_tab.js b/devtools/client/webconsole/test/browser_bug_871156_ctrlw_close_tab.js
new file mode 100644
index 000000000..c1698cf91
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_bug_871156_ctrlw_close_tab.js
@@ -0,0 +1,78 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Check that Ctrl-W closes the Browser Console and that Ctrl-W closes the
+// current tab when using the Web Console - bug 871156.
+
+"use strict";
+
+add_task(function* () {
+ const TEST_URI = "data:text/html;charset=utf8,<title>bug871156</title>\n" +
+ "<p>hello world";
+ let firstTab = gBrowser.selectedTab;
+
+ Services.prefs.setBoolPref("browser.tabs.animate", false);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.tabs.animate");
+ });
+
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+ ok(hud, "Web Console opened");
+
+ let tabClosed = promise.defer();
+ let toolboxDestroyed = promise.defer();
+ let tabSelected = promise.defer();
+
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ let toolbox = gDevTools.getToolbox(target);
+
+ gBrowser.tabContainer.addEventListener("TabClose", function onTabClose() {
+ gBrowser.tabContainer.removeEventListener("TabClose", onTabClose);
+ info("tab closed");
+ tabClosed.resolve(null);
+ });
+
+ gBrowser.tabContainer.addEventListener("TabSelect", function onTabSelect() {
+ gBrowser.tabContainer.removeEventListener("TabSelect", onTabSelect);
+ if (gBrowser.selectedTab == firstTab) {
+ info("tab selected");
+ tabSelected.resolve(null);
+ }
+ });
+
+ toolbox.once("destroyed", () => {
+ info("toolbox destroyed");
+ toolboxDestroyed.resolve(null);
+ });
+
+ // Get out of the web console initialization.
+ executeSoon(() => {
+ EventUtils.synthesizeKey("w", { accelKey: true });
+ });
+
+ yield promise.all([tabClosed.promise, toolboxDestroyed.promise,
+ tabSelected.promise]);
+ info("promise.all resolved. start testing the Browser Console");
+
+ hud = yield HUDService.toggleBrowserConsole();
+ ok(hud, "Browser Console opened");
+
+ let deferred = promise.defer();
+
+ Services.obs.addObserver(function onDestroy() {
+ Services.obs.removeObserver(onDestroy, "web-console-destroyed");
+ ok(true, "the Browser Console closed");
+
+ deferred.resolve(null);
+ }, "web-console-destroyed", false);
+
+ waitForFocus(() => {
+ EventUtils.synthesizeKey("w", { accelKey: true }, hud.iframeWindow);
+ }, hud.iframeWindow);
+
+ yield deferred.promise;
+});
diff --git a/devtools/client/webconsole/test/browser_cached_messages.js b/devtools/client/webconsole/test/browser_cached_messages.js
new file mode 100644
index 000000000..bf69deee3
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_cached_messages.js
@@ -0,0 +1,59 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Test to see if the cached messages are displayed when the console UI is
+// opened.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-webconsole-error-observer.html";
+
+// On e10s, the exception is triggered in child process
+// and is ignored by test harness
+if (!Services.appinfo.browserTabsRemoteAutostart) {
+ expectUncaughtException();
+}
+
+function test() {
+ waitForExplicitFinish();
+
+ loadTab(TEST_URI).then(testOpenUI);
+}
+
+function testOpenUI(aTestReopen) {
+ openConsole().then((hud) => {
+ waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ text: "log Bazzle",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ },
+ {
+ text: "error Bazzle",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_ERROR,
+ },
+ {
+ text: "bazBug611032",
+ category: CATEGORY_JS,
+ severity: SEVERITY_ERROR,
+ },
+ {
+ text: "cssColorBug611032",
+ category: CATEGORY_CSS,
+ severity: SEVERITY_WARNING,
+ },
+ ],
+ }).then(() => {
+ closeConsole(gBrowser.selectedTab).then(() => {
+ aTestReopen && info("will reopen the Web Console");
+ executeSoon(aTestReopen ? testOpenUI : finishTest);
+ });
+ });
+ });
+}
diff --git a/devtools/client/webconsole/test/browser_console.js b/devtools/client/webconsole/test/browser_console.js
new file mode 100644
index 000000000..7bd1ffdc2
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console.js
@@ -0,0 +1,160 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Test the basic features of the Browser Console, bug 587757.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console.html?" + Date.now();
+const TEST_FILE = "chrome://mochitests/content/browser/devtools/client/" +
+ "webconsole/test/test-cu-reporterror.js";
+
+const TEST_XHR_ERROR_URI = `http://example.com/404.html?${Date.now()}`;
+
+const TEST_IMAGE = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-image.png";
+
+"use strict";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let opened = waitForConsole();
+
+ let hud = HUDService.getBrowserConsole();
+ ok(!hud, "browser console is not open");
+ info("wait for the browser console to open with ctrl-shift-j");
+ EventUtils.synthesizeKey("j", { accelKey: true, shiftKey: true }, window);
+
+ hud = yield opened;
+ ok(hud, "browser console opened");
+
+ yield consoleOpened(hud);
+});
+
+function consoleOpened(hud) {
+ hud.jsterm.clearOutput(true);
+
+ expectUncaughtException();
+ executeSoon(() => {
+ foobarExceptionBug587757();
+ });
+
+ // Add a message from a chrome window.
+ hud.iframeWindow.console.log("bug587757a");
+
+ // Check Cu.reportError stack.
+ // Use another js script to not depend on the test file line numbers.
+ Services.scriptloader.loadSubScript(TEST_FILE, hud.iframeWindow);
+
+ // Add a message from a content window.
+ content.console.log("bug587757b");
+
+ // Test eval.
+ hud.jsterm.execute("document.location.href");
+
+ // Check for network requests.
+ let xhr = new XMLHttpRequest();
+ xhr.onload = () => console.log("xhr loaded, status is: " + xhr.status);
+ xhr.open("get", TEST_URI, true);
+ xhr.send();
+
+ // Check for xhr error.
+ let xhrErr = new XMLHttpRequest();
+ xhrErr.onload = () => {
+ console.log("xhr error loaded, status is: " + xhrErr.status);
+ };
+ xhrErr.open("get", TEST_XHR_ERROR_URI, true);
+ xhrErr.send();
+
+ // Check that Fetch requests are categorized as "XHR".
+ fetch(TEST_IMAGE).then(() => { console.log("fetch loaded"); });
+
+ return waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ name: "chrome window console.log() is displayed",
+ text: "bug587757a",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ },
+ {
+ name: "Cu.reportError is displayed",
+ text: "bug1141222",
+ category: CATEGORY_JS,
+ severity: SEVERITY_ERROR,
+ stacktrace: [{
+ file: TEST_FILE,
+ line: 2,
+ }, {
+ file: TEST_FILE,
+ line: 4,
+ },
+ // Ignore the rest of the stack,
+ // just assert Cu.reportError call site
+ // and consoleOpened call
+ ]
+ },
+ {
+ name: "content window console.log() is displayed",
+ text: "bug587757b",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ },
+ {
+ name: "jsterm eval result",
+ text: "browser.xul",
+ category: CATEGORY_OUTPUT,
+ severity: SEVERITY_LOG,
+ },
+ {
+ name: "exception message",
+ text: "foobarExceptionBug587757",
+ category: CATEGORY_JS,
+ severity: SEVERITY_ERROR,
+ },
+ {
+ name: "network message",
+ text: "test-console.html",
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_INFO,
+ isXhr: true,
+ },
+ {
+ name: "xhr error message",
+ text: "404.html",
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_ERROR,
+ isXhr: true,
+ },
+ {
+ name: "network message",
+ text: "test-image.png",
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_INFO,
+ isXhr: true,
+ },
+ ],
+ });
+}
+
+function waitForConsole() {
+ let deferred = promise.defer();
+
+ Services.obs.addObserver(function observer(aSubject) {
+ Services.obs.removeObserver(observer, "web-console-created");
+ aSubject.QueryInterface(Ci.nsISupportsString);
+
+ let hud = HUDService.getBrowserConsole();
+ ok(hud, "browser console is open");
+ is(aSubject.data, hud.hudId, "notification hudId is correct");
+
+ executeSoon(() => deferred.resolve(hud));
+ }, "web-console-created", false);
+
+ return deferred.promise;
+}
diff --git a/devtools/client/webconsole/test/browser_console_addonsdk_loader_exception.js b/devtools/client/webconsole/test/browser_console_addonsdk_loader_exception.js
new file mode 100644
index 000000000..3eec65de3
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_addonsdk_loader_exception.js
@@ -0,0 +1,92 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Check that exceptions from scripts loaded with the addon-sdk loader are
+// opened correctly in View Source from the Browser Console.
+// See bug 866950.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf8,<p>hello world from bug 866950";
+
+function test() {
+ requestLongerTimeout(2);
+
+ let webconsole, browserconsole;
+
+ Task.spawn(runner).then(finishTest);
+
+ function* runner() {
+ let {tab} = yield loadTab(TEST_URI);
+ webconsole = yield openConsole(tab);
+ ok(webconsole, "web console opened");
+
+ browserconsole = yield HUDService.toggleBrowserConsole();
+ ok(browserconsole, "browser console opened");
+
+ // Cause an exception in a script loaded with the addon-sdk loader.
+ let toolbox = gDevTools.getToolbox(webconsole.target);
+ let oldPanels = toolbox._toolPanels;
+ // non-iterable
+ toolbox._toolPanels = {};
+
+ function fixToolbox() {
+ toolbox._toolPanels = oldPanels;
+ }
+
+ info("generate exception and wait for message");
+
+ executeSoon(() => {
+ executeSoon(fixToolbox);
+ expectUncaughtException();
+ toolbox.getToolPanels();
+ });
+
+ let [result] = yield waitForMessages({
+ webconsole: browserconsole,
+ messages: [{
+ text: "TypeError: this._toolPanels is not iterable",
+ category: CATEGORY_JS,
+ severity: SEVERITY_ERROR,
+ }],
+ });
+
+ fixToolbox();
+
+ let msg = [...result.matched][0];
+ ok(msg, "message element found");
+ let locationNode = msg
+ .querySelector(".message .message-location > .frame-link");
+ ok(locationNode, "message location element found");
+
+ let url = locationNode.getAttribute("data-url");
+ info("location node url: " + url);
+ ok(url.indexOf("resource://") === 0, "error comes from a subscript");
+
+ let viewSource = browserconsole.viewSource;
+ let URL = null;
+ let clickPromise = promise.defer();
+ browserconsole.viewSourceInDebugger = (sourceURL) => {
+ info("browserconsole.viewSourceInDebugger() was invoked: " + sourceURL);
+ URL = sourceURL;
+ clickPromise.resolve(null);
+ };
+
+ msg.scrollIntoView();
+ EventUtils.synthesizeMouse(locationNode, 2, 2, {},
+ browserconsole.iframeWindow);
+
+ info("wait for click on locationNode");
+ yield clickPromise.promise;
+
+ info("view-source url: " + URL);
+ ok(URL, "we have some source URL after the click");
+ isnot(URL.indexOf("toolbox.js"), -1,
+ "we have the expected view source URL");
+ is(URL.indexOf("->"), -1, "no -> in the URL given to view-source");
+
+ browserconsole.viewSourceInDebugger = viewSource;
+ }
+}
diff --git a/devtools/client/webconsole/test/browser_console_clear_method.js b/devtools/client/webconsole/test/browser_console_clear_method.js
new file mode 100644
index 000000000..33b43850e
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_clear_method.js
@@ -0,0 +1,41 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Check that console.clear() does not clear the output of the browser console.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf8,<p>Bug 1296870";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+ let hud = yield HUDService.toggleBrowserConsole();
+
+ info("Log a new message from the content page");
+ ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ content.wrappedJSObject.console.log("msg");
+ });
+ yield waitForMessage("msg", hud);
+
+ info("Send a console.clear() from the content page");
+ ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ content.wrappedJSObject.console.clear();
+ });
+ yield waitForMessage("Console was cleared", hud);
+
+ info("Check that the messages logged after the first clear are still displayed");
+ isnot(hud.outputNode.textContent.indexOf("msg"), -1, "msg is in the output");
+});
+
+function waitForMessage(message, webconsole) {
+ return waitForMessages({
+ webconsole,
+ messages: [{
+ text: message,
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+}
diff --git a/devtools/client/webconsole/test/browser_console_clear_on_reload.js b/devtools/client/webconsole/test/browser_console_clear_on_reload.js
new file mode 100644
index 000000000..223eb028d
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_clear_on_reload.js
@@ -0,0 +1,86 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Check that clear output on page reload works - bug 705921.
+// Check that clear output and page reload remove the sidebar - bug 971967.
+
+"use strict";
+
+add_task(function* () {
+ const PREF = "devtools.webconsole.persistlog";
+ const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console.html";
+
+ Services.prefs.setBoolPref(PREF, false);
+ registerCleanupFunction(() => Services.prefs.clearUserPref(PREF));
+
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+ ok(hud, "Web Console opened");
+
+ yield openSidebar("fooObj", { name: "testProp", value: "testValue" });
+
+ let sidebarClosed = hud.jsterm.once("sidebar-closed");
+ hud.jsterm.clearOutput();
+ yield sidebarClosed;
+
+ hud.jsterm.execute("console.log('foobarz1')");
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "foobarz1",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+
+ yield openSidebar("fooObj", { name: "testProp", value: "testValue" });
+
+ BrowserReload();
+
+ sidebarClosed = hud.jsterm.once("sidebar-closed");
+ loadBrowser(gBrowser.selectedBrowser);
+ yield sidebarClosed;
+
+ hud.jsterm.execute("console.log('foobarz2')");
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "test-console.html",
+ category: CATEGORY_NETWORK,
+ },
+ {
+ text: "foobarz2",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+
+ is(hud.outputNode.textContent.indexOf("foobarz1"), -1,
+ "foobarz1 has been removed from output");
+
+ function* openSidebar(objName, expectedObj) {
+ let msg = yield hud.jsterm.execute(objName);
+ ok(msg, "output message found");
+
+ let anchor = msg.querySelector("a");
+ let body = msg.querySelector(".message-body");
+ ok(anchor, "object anchor");
+ ok(body, "message body");
+
+ yield EventUtils.synthesizeMouse(anchor, 2, 2, {}, hud.iframeWindow);
+
+ let vviewVar = yield hud.jsterm.once("variablesview-fetched");
+ let vview = vviewVar._variablesView;
+ ok(vview, "variables view object exists");
+
+ yield findVariableViewProperties(vviewVar, [
+ expectedObj,
+ ], { webconsole: hud });
+ }
+});
diff --git a/devtools/client/webconsole/test/browser_console_click_focus.js b/devtools/client/webconsole/test/browser_console_click_focus.js
new file mode 100644
index 000000000..f405f0bbf
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_click_focus.js
@@ -0,0 +1,59 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests that the input field is focused when the console is opened.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console.html";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+
+ let [result] = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "Dolske Digs Bacon",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+
+ let msg = [...result.matched][0];
+ let outputItem = msg.querySelector(".message-body");
+ ok(outputItem, "found a logged message");
+
+ let inputNode = hud.jsterm.inputNode;
+ ok(inputNode.getAttribute("focused"), "input node is focused, first");
+
+ yield waitForBlurredInput(inputNode);
+
+ EventUtils.sendMouseEvent({type: "click"}, hud.outputNode);
+ ok(inputNode.getAttribute("focused"), "input node is focused, second time");
+
+ yield waitForBlurredInput(inputNode);
+
+ info("Setting a text selection and making sure a click does not re-focus");
+ let selection = hud.iframeWindow.getSelection();
+ selection.selectAllChildren(outputItem);
+
+ EventUtils.sendMouseEvent({type: "click"}, hud.outputNode);
+ ok(!inputNode.getAttribute("focused"),
+ "input node is not focused after drag");
+});
+
+function waitForBlurredInput(inputNode) {
+ return new Promise(resolve => {
+ let lostFocus = () => {
+ inputNode.removeEventListener("blur", lostFocus);
+ ok(!inputNode.getAttribute("focused"), "input node is not focused");
+ resolve();
+ };
+ inputNode.addEventListener("blur", lostFocus);
+ document.getElementById("urlbar").click();
+ });
+}
diff --git a/devtools/client/webconsole/test/browser_console_consolejsm_output.js b/devtools/client/webconsole/test/browser_console_consolejsm_output.js
new file mode 100644
index 000000000..e5b37843e
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_consolejsm_output.js
@@ -0,0 +1,285 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Test that Console.jsm outputs messages to the Browser Console, bug 851231.
+
+"use strict";
+
+function onNewMessage(aEvent, aNewMessages) {
+ for (let msg of aNewMessages) {
+ // Messages that shouldn't be output contain the substring FAIL_TEST
+ if (msg.node.textContent.includes("FAIL_TEST")) {
+ ok(false, "Message shouldn't have been output: " + msg.node.textContent);
+ }
+ }
+}
+
+add_task(function* () {
+ let consoleStorage = Cc["@mozilla.org/consoleAPI-storage;1"];
+ let storage = consoleStorage.getService(Ci.nsIConsoleAPIStorage);
+ storage.clearEvents();
+
+ let {console} = Cu.import("resource://gre/modules/Console.jsm", {});
+ console.log("bug861338-log-cached");
+
+ let hud = yield HUDService.toggleBrowserConsole();
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "cached console.log message",
+ text: "bug861338-log-cached",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+
+ hud.jsterm.clearOutput(true);
+
+ function testTrace() {
+ console.trace();
+ }
+
+ console.time("foobarTimer");
+ let foobar = { bug851231prop: "bug851231value" };
+
+ console.log("bug851231-log");
+ console.info("bug851231-info");
+ console.warn("bug851231-warn");
+ console.error("bug851231-error", foobar);
+ console.debug("bug851231-debug");
+ console.dir(document);
+ testTrace();
+ console.timeEnd("foobarTimer");
+
+ info("wait for the Console.jsm messages");
+
+ let results = yield waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ name: "console.log output",
+ text: "bug851231-log",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ },
+ {
+ name: "console.info output",
+ text: "bug851231-info",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_INFO,
+ },
+ {
+ name: "console.warn output",
+ text: "bug851231-warn",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_WARNING,
+ },
+ {
+ name: "console.error output",
+ text: /\bbug851231-error\b.+\{\s*bug851231prop:\s"bug851231value"\s*\}/,
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_ERROR,
+ objects: true,
+ },
+ {
+ name: "console.debug output",
+ text: "bug851231-debug",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ },
+ {
+ name: "console.trace output",
+ consoleTrace: {
+ file: "browser_console_consolejsm_output.js",
+ fn: "testTrace",
+ },
+ },
+ {
+ name: "console.dir output",
+ consoleDir: /XULDocument\s+.+\s+chrome:\/\/.+\/browser\.xul/,
+ },
+ {
+ name: "console.time output",
+ consoleTime: "foobarTimer",
+ },
+ {
+ name: "console.timeEnd output",
+ consoleTimeEnd: "foobarTimer",
+ },
+ ],
+ });
+
+ let consoleErrorMsg = results[3];
+ ok(consoleErrorMsg, "console.error message element found");
+ let clickable = consoleErrorMsg.clickableElements[0];
+ ok(clickable, "clickable object found for console.error");
+
+ let deferred = promise.defer();
+
+ let onFetch = (aEvent, aVar) => {
+ // Skip the notification from console.dir variablesview-fetched.
+ if (aVar._variablesView != hud.jsterm._variablesView) {
+ return;
+ }
+ hud.jsterm.off("variablesview-fetched", onFetch);
+
+ deferred.resolve(aVar);
+ };
+
+ hud.jsterm.on("variablesview-fetched", onFetch);
+
+ clickable.scrollIntoView(false);
+
+ info("wait for variablesview-fetched");
+ executeSoon(() =>
+ EventUtils.synthesizeMouse(clickable, 2, 2, {}, hud.iframeWindow));
+
+ let varView = yield deferred.promise;
+ ok(varView, "object inspector opened on click");
+
+ yield findVariableViewProperties(varView, [{
+ name: "bug851231prop",
+ value: "bug851231value",
+ }], { webconsole: hud });
+
+ yield HUDService.toggleBrowserConsole();
+});
+
+add_task(function* testPrefix() {
+ let consoleStorage = Cc["@mozilla.org/consoleAPI-storage;1"];
+ let storage = consoleStorage.getService(Ci.nsIConsoleAPIStorage);
+ storage.clearEvents();
+
+ let {ConsoleAPI} = Cu.import("resource://gre/modules/Console.jsm", {});
+ let consoleOptions = {
+ maxLogLevel: "error",
+ prefix: "Log Prefix",
+ };
+ let console2 = new ConsoleAPI(consoleOptions);
+ console2.error("Testing a prefix");
+ console2.log("FAIL_TEST: Below the maxLogLevel");
+
+ let hud = yield HUDService.toggleBrowserConsole();
+ hud.ui.on("new-messages", onNewMessage);
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "cached console.error message",
+ prefix: "Log Prefix:",
+ severity: SEVERITY_ERROR,
+ text: "Testing a prefix",
+ }],
+ });
+
+ hud.jsterm.clearOutput(true);
+ hud.ui.off("new-messages", onNewMessage);
+ yield HUDService.toggleBrowserConsole();
+});
+
+add_task(function* testMaxLogLevelPrefMissing() {
+ let consoleStorage = Cc["@mozilla.org/consoleAPI-storage;1"];
+ let storage = consoleStorage.getService(Ci.nsIConsoleAPIStorage);
+ storage.clearEvents();
+
+ let {ConsoleAPI} = Cu.import("resource://gre/modules/Console.jsm", {});
+ let consoleOptions = {
+ maxLogLevel: "error",
+ maxLogLevelPref: "testing.maxLogLevel",
+ };
+ let console = new ConsoleAPI(consoleOptions);
+
+ is(Services.prefs.getPrefType(consoleOptions.maxLogLevelPref),
+ Services.prefs.PREF_INVALID,
+ "Check log level pref is missing");
+
+ // Since the maxLogLevelPref doesn't exist, we should fallback to the passed
+ // maxLogLevel of "error".
+ console.warn("FAIL_TEST: Below the maxLogLevel");
+ console.error("Error should be shown");
+
+ let hud = yield HUDService.toggleBrowserConsole();
+
+ hud.ui.on("new-messages", onNewMessage);
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "defaulting to error level",
+ severity: SEVERITY_ERROR,
+ text: "Error should be shown",
+ }],
+ });
+
+ hud.jsterm.clearOutput(true);
+ hud.ui.off("new-messages", onNewMessage);
+ yield HUDService.toggleBrowserConsole();
+});
+
+add_task(function* testMaxLogLevelPref() {
+ let consoleStorage = Cc["@mozilla.org/consoleAPI-storage;1"];
+ let storage = consoleStorage.getService(Ci.nsIConsoleAPIStorage);
+ storage.clearEvents();
+
+ let {ConsoleAPI} = Cu.import("resource://gre/modules/Console.jsm", {});
+ let consoleOptions = {
+ maxLogLevel: "error",
+ maxLogLevelPref: "testing.maxLogLevel",
+ };
+
+ info("Setting the pref to warn");
+ Services.prefs.setCharPref(consoleOptions.maxLogLevelPref, "Warn");
+
+ let console = new ConsoleAPI(consoleOptions);
+
+ is(console.maxLogLevel, "warn", "Check pref was read at initialization");
+
+ console.info("FAIL_TEST: info is below the maxLogLevel");
+ console.error("Error should be shown");
+ console.warn("Warn should be shown due to the initial pref value");
+
+ info("Setting the pref to info");
+ Services.prefs.setCharPref(consoleOptions.maxLogLevelPref, "INFO");
+ is(console.maxLogLevel, "info", "Check pref was lowercased");
+
+ console.info("info should be shown due to the pref change being observed");
+
+ info("Clearing the pref");
+ Services.prefs.clearUserPref(consoleOptions.maxLogLevelPref);
+
+ console.warn("FAIL_TEST: Shouldn't be shown due to defaulting to error");
+ console.error("Should be shown due to defaulting to error");
+
+ let hud = yield HUDService.toggleBrowserConsole();
+ hud.ui.on("new-messages", onNewMessage);
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "error > warn",
+ severity: SEVERITY_ERROR,
+ text: "Error should be shown",
+ },
+ {
+ name: "warn is the inital pref value",
+ severity: SEVERITY_WARNING,
+ text: "Warn should be shown due to the initial pref value",
+ },
+ {
+ name: "pref changed to info",
+ severity: SEVERITY_INFO,
+ text: "info should be shown due to the pref change being observed",
+ },
+ {
+ name: "default to intial maxLogLevel if pref is removed",
+ severity: SEVERITY_ERROR,
+ text: "Should be shown due to defaulting to error",
+ }],
+ });
+
+ hud.jsterm.clearOutput(true);
+ hud.ui.off("new-messages", onNewMessage);
+ yield HUDService.toggleBrowserConsole();
+});
diff --git a/devtools/client/webconsole/test/browser_console_copy_command.js b/devtools/client/webconsole/test/browser_console_copy_command.js
new file mode 100644
index 000000000..c4ed4360f
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_copy_command.js
@@ -0,0 +1,76 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests that the `copy` console helper works as intended.
+
+"use strict";
+
+var gWebConsole, gJSTerm;
+
+var TEXT = "Lorem ipsum dolor sit amet, consectetur adipisicing " +
+ "elit, sed do eiusmod tempor incididunt ut labore et dolore magna " +
+ "aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco " +
+ "laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure " +
+ "dolor in reprehenderit in voluptate velit esse cillum dolore eu " +
+ "fugiat nulla pariatur. Excepteur sint occaecat cupidatat non " +
+ "proident, sunt in culpa qui officia deserunt mollit anim id est laborum." +
+ new Date();
+
+var ID = "select-me";
+
+add_task(function* init() {
+ yield loadTab("data:text/html;charset=utf-8," +
+ "<body>" +
+ " <div>" +
+ " <h1>Testing copy command</h1>" +
+ " <p>This is some example text</p>" +
+ " <p id='select-me'>" + TEXT + "</p>" +
+ " </div>" +
+ " <div><p></p></div>" +
+ "</body>");
+
+ gWebConsole = yield openConsole();
+ gJSTerm = gWebConsole.jsterm;
+});
+
+add_task(function* testCopy() {
+ let RANDOM = Math.random();
+ let string = "Text: " + RANDOM;
+ let obj = {a: 1, b: "foo", c: RANDOM};
+
+ let samples = [
+ [RANDOM, RANDOM],
+ [JSON.stringify(string), string],
+ [obj.toSource(), JSON.stringify(obj, null, " ")],
+ [
+ "$('#" + ID + "')",
+ content.document.getElementById(ID).outerHTML
+ ]
+ ];
+ for (let [source, reference] of samples) {
+ let deferredResult = promise.defer();
+
+ SimpleTest.waitForClipboard(
+ "" + reference,
+ () => {
+ let command = "copy(" + source + ")";
+ info("Attempting to copy: " + source);
+ info("Executing command: " + command);
+ gJSTerm.execute(command, msg => {
+ is(msg, undefined, "Command success: " + command);
+ });
+ },
+ deferredResult.resolve,
+ deferredResult.reject);
+
+ yield deferredResult.promise;
+ }
+});
+
+add_task(function* cleanup() {
+ gWebConsole = gJSTerm = null;
+ gBrowser.removeTab(gBrowser.selectedTab);
+ finishTest();
+});
diff --git a/devtools/client/webconsole/test/browser_console_copy_entire_message_context_menu.js b/devtools/client/webconsole/test/browser_console_copy_entire_message_context_menu.js
new file mode 100644
index 000000000..bdd4f7179
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_copy_entire_message_context_menu.js
@@ -0,0 +1,97 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/* globals goDoCommand */
+
+"use strict";
+
+// Test copying of the entire console message when right-clicked
+// with no other text selected. See Bug 1100562.
+
+add_task(function* () {
+ let hud;
+ let outputNode;
+ let contextMenu;
+
+ const TEST_URI = "http://example.com/browser/devtools/client/webconsole/test/test-console.html";
+
+ const { tab, browser } = yield loadTab(TEST_URI);
+ hud = yield openConsole(tab);
+ outputNode = hud.outputNode;
+ contextMenu = hud.iframeWindow.document.getElementById("output-contextmenu");
+
+ registerCleanupFunction(() => {
+ hud = outputNode = contextMenu = null;
+ });
+
+ hud.jsterm.clearOutput();
+
+ yield ContentTask.spawn(browser, {}, function* () {
+ let button = content.document.getElementById("testTrace");
+ button.click();
+ });
+
+ let results = yield waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ text: "bug 1100562",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ lines: 1,
+ },
+ {
+ name: "console.trace output",
+ consoleTrace: true,
+ lines: 3,
+ },
+ ]
+ });
+
+ outputNode.focus();
+
+ for (let result of results) {
+ let message = [...result.matched][0];
+
+ yield waitForContextMenu(contextMenu, message, () => {
+ let copyItem = contextMenu.querySelector("#cMenu_copy");
+ copyItem.doCommand();
+
+ let controller = top.document.commandDispatcher
+ .getControllerForCommand("cmd_copy");
+ is(controller.isCommandEnabled("cmd_copy"), true, "cmd_copy is enabled");
+ });
+
+ let clipboardText;
+
+ yield waitForClipboardPromise(
+ () => goDoCommand("cmd_copy"),
+ (str) => {
+ clipboardText = str;
+ return message.textContent == clipboardText;
+ }
+ );
+
+ ok(clipboardText, "Clipboard text was found and saved");
+
+ let lines = clipboardText.split("\n");
+ ok(lines.length > 0, "There is at least one newline in the message");
+ is(lines.pop(), "", "There is a newline at the end");
+ is(lines.length, result.lines, `There are ${result.lines} lines in the message`);
+
+ // Test the first line for "timestamp message repeat file:line"
+ let firstLine = lines.shift();
+ ok(/^[\d:.]+ .+ \d+ .+:\d+$/.test(firstLine),
+ "The message's first line has the right format");
+
+ // Test the remaining lines (stack trace) for "TABfunctionName sourceURL:line:col"
+ for (let line of lines) {
+ ok(/^\t.+ .+:\d+:\d+$/.test(line), "The stack trace line has the right format");
+ }
+ }
+
+ yield closeConsole(tab);
+ yield finishTest();
+});
diff --git a/devtools/client/webconsole/test/browser_console_dead_objects.js b/devtools/client/webconsole/test/browser_console_dead_objects.js
new file mode 100644
index 000000000..46b15d59b
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_dead_objects.js
@@ -0,0 +1,88 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Check that Dead Objects do not break the Web/Browser Consoles.
+// See bug 883649.
+// This test does:
+// - opens a new tab,
+// - opens the Browser Console,
+// - stores a reference to the content document of the tab on the chrome
+// window object,
+// - closes the tab,
+// - tries to use the object that was pointing to the now-defunct content
+// document. This is the dead object.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf8,<p>dead objects!";
+
+function test() {
+ let hud = null;
+
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("devtools.chrome.enabled");
+ });
+
+ Task.spawn(runner).then(finishTest);
+
+ function* runner() {
+ Services.prefs.setBoolPref("devtools.chrome.enabled", true);
+ yield loadTab(TEST_URI);
+
+ info("open the browser console");
+
+ hud = yield HUDService.toggleBrowserConsole();
+ ok(hud, "browser console opened");
+
+ let jsterm = hud.jsterm;
+
+ jsterm.clearOutput();
+
+ // Add the reference to the content document.
+ yield jsterm.execute("Cu = Components.utils;" +
+ "Cu.import('resource://gre/modules/Services.jsm');" +
+ "chromeWindow = Services.wm.getMostRecentWindow('" +
+ "navigator:browser');" +
+ "foobarzTezt = chromeWindow.content.document;" +
+ "delete chromeWindow");
+
+ gBrowser.removeCurrentTab();
+
+ let msg = yield jsterm.execute("foobarzTezt");
+
+ isnot(hud.outputNode.textContent.indexOf("[object DeadObject]"), -1,
+ "dead object found");
+
+ jsterm.setInputValue("foobarzTezt");
+
+ for (let c of ".hello") {
+ EventUtils.synthesizeKey(c, {}, hud.iframeWindow);
+ }
+
+ yield jsterm.execute();
+
+ isnot(hud.outputNode.textContent.indexOf("can't access dead object"), -1,
+ "'cannot access dead object' message found");
+
+ // Click the second execute output.
+ let clickable = msg.querySelector("a");
+ ok(clickable, "clickable object found");
+ isnot(clickable.textContent.indexOf("[object DeadObject]"), -1,
+ "message text check");
+
+ msg.scrollIntoView();
+
+ executeSoon(() => {
+ EventUtils.synthesizeMouseAtCenter(clickable, {}, hud.iframeWindow);
+ });
+
+ yield jsterm.once("variablesview-fetched");
+ ok(true, "variables view fetched");
+
+ msg = yield jsterm.execute("delete window.foobarzTezt; 2013-26");
+
+ isnot(msg.textContent.indexOf("1987"), -1, "result message found");
+ }
+}
diff --git a/devtools/client/webconsole/test/browser_console_error_source_click.js b/devtools/client/webconsole/test/browser_console_error_source_click.js
new file mode 100644
index 000000000..5839f20d5
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_error_source_click.js
@@ -0,0 +1,79 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Check that JS errors and CSS warnings open view source when their source link
+// is clicked in the Browser Console. See bug 877778.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf8,<p>hello world from bug 877778 " +
+ "<button onclick='foobar.explode()' " +
+ "style='test-color: green-please'>click!</button>";
+
+add_task(function* () {
+ yield new Promise(resolve => {
+ SpecialPowers.pushPrefEnv({"set": [
+ ["devtools.browserconsole.filter.cssparser", true]
+ ]}, resolve);
+ });
+
+ yield loadTab(TEST_URI);
+ let hud = yield HUDService.toggleBrowserConsole();
+ ok(hud, "browser console opened");
+
+ // On e10s, the exception is triggered in child process
+ // and is ignored by test harness
+ if (!Services.appinfo.browserTabsRemoteAutostart) {
+ expectUncaughtException();
+ }
+
+ info("generate exception and wait for the message");
+ ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ let button = content.document.querySelector("button");
+ button.click();
+ });
+
+ let results = yield waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ text: "ReferenceError: foobar is not defined",
+ category: CATEGORY_JS,
+ severity: SEVERITY_ERROR,
+ },
+ {
+ text: "Unknown property \u2018test-color\u2019",
+ category: CATEGORY_CSS,
+ severity: SEVERITY_WARNING,
+ },
+ ],
+ });
+
+ let viewSourceCalled = false;
+
+ let viewSource = hud.viewSource;
+ hud.viewSource = () => {
+ viewSourceCalled = true;
+ };
+
+ for (let result of results) {
+ viewSourceCalled = false;
+
+ let msg = [...result.matched][0];
+ ok(msg, "message element found for: " + result.text);
+ ok(!msg.classList.contains("filtered-by-type"), "message element is not filtered");
+ let selector = ".message .message-location .frame-link-source";
+ let locationNode = msg.querySelector(selector);
+ ok(locationNode, "message location element found");
+
+ locationNode.click();
+
+ ok(viewSourceCalled, "view source opened");
+ }
+
+ hud.viewSource = viewSource;
+
+ yield finishTest();
+});
diff --git a/devtools/client/webconsole/test/browser_console_filters.js b/devtools/client/webconsole/test/browser_console_filters.js
new file mode 100644
index 000000000..072766fdb
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_filters.js
@@ -0,0 +1,60 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Check that the Browser Console does not use the same filter prefs as the Web
+// Console. See bug 878186.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf8,<p>browser console filters";
+const WEB_CONSOLE_PREFIX = "devtools.webconsole.filter.";
+const BROWSER_CONSOLE_PREFIX = "devtools.browserconsole.filter.";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ info("open the web console");
+ let hud = yield openConsole();
+ ok(hud, "web console opened");
+
+ is(Services.prefs.getBoolPref(BROWSER_CONSOLE_PREFIX + "exception"), true,
+ "'exception' filter is enabled (browser console)");
+ is(Services.prefs.getBoolPref(WEB_CONSOLE_PREFIX + "exception"), true,
+ "'exception' filter is enabled (web console)");
+
+ info("toggle 'exception' filter");
+ hud.setFilterState("exception", false);
+
+ is(Services.prefs.getBoolPref(BROWSER_CONSOLE_PREFIX + "exception"), true,
+ "'exception' filter is enabled (browser console)");
+ is(Services.prefs.getBoolPref(WEB_CONSOLE_PREFIX + "exception"), false,
+ "'exception' filter is disabled (web console)");
+
+ hud.setFilterState("exception", true);
+
+ // We need to let the console opening event loop to finish.
+ let deferred = promise.defer();
+ executeSoon(() => closeConsole().then(() => deferred.resolve(null)));
+ yield deferred.promise;
+
+ info("web console closed");
+ hud = yield HUDService.toggleBrowserConsole();
+ ok(hud, "browser console opened");
+
+ is(Services.prefs.getBoolPref(BROWSER_CONSOLE_PREFIX + "exception"), true,
+ "'exception' filter is enabled (browser console)");
+ is(Services.prefs.getBoolPref(WEB_CONSOLE_PREFIX + "exception"), true,
+ "'exception' filter is enabled (web console)");
+
+ info("toggle 'exception' filter");
+ hud.setFilterState("exception", false);
+
+ is(Services.prefs.getBoolPref(BROWSER_CONSOLE_PREFIX + "exception"), false,
+ "'exception' filter is disabled (browser console)");
+ is(Services.prefs.getBoolPref(WEB_CONSOLE_PREFIX + "exception"), true,
+ "'exception' filter is enabled (web console)");
+
+ hud.setFilterState("exception", true);
+});
diff --git a/devtools/client/webconsole/test/browser_console_hide_jsterm_when_devtools_chrome_enabled_false.js b/devtools/client/webconsole/test/browser_console_hide_jsterm_when_devtools_chrome_enabled_false.js
new file mode 100644
index 000000000..d3fdb08be
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_hide_jsterm_when_devtools_chrome_enabled_false.js
@@ -0,0 +1,114 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/*
+ * Bug 922161 - Hide Browser Console JS input field if devtools.chrome.enabled
+ * is false.
+ * when devtools.chrome.enabled then
+ * -browser console jsterm should be enabled
+ * -browser console object inspector properties should be set.
+ * -webconsole jsterm should be enabled
+ * -webconsole object inspector properties should be set.
+ *
+ * when devtools.chrome.enabled == false then
+ * -browser console jsterm should be disabled
+ * -browser console object inspector properties should not be set.
+ * -webconsole jsterm should be enabled
+ * -webconsole object inspector properties should be set.
+ */
+
+"use strict";
+
+function testObjectInspectorPropertiesAreNotSet(variablesView) {
+ is(variablesView.eval, null, "vview.eval is null");
+ is(variablesView.switch, null, "vview.switch is null");
+ is(variablesView.delete, null, "vview.delete is null");
+}
+
+function* getVariablesView(hud) {
+ function openVariablesView(event, vview) {
+ deferred.resolve(vview._variablesView);
+ }
+
+ let deferred = promise.defer();
+
+ // Filter out other messages to ensure ours stays visible.
+ hud.ui.filterBox.value = "browser_console_hide_jsterm_test";
+
+ hud.jsterm.clearOutput();
+ hud.jsterm.execute("new Object({ browser_console_hide_jsterm_test: true })");
+
+ let [message] = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "Object { browser_console_hide_jsterm_test: true }",
+ category: CATEGORY_OUTPUT,
+ }],
+ });
+
+ hud.jsterm.once("variablesview-fetched", openVariablesView);
+
+ let anchor = [...message.matched][0].querySelector("a");
+
+ executeSoon(() =>
+ EventUtils.synthesizeMouse(anchor, 2, 2, {}, hud.iframeWindow)
+ );
+
+ return deferred.promise;
+}
+
+function testJSTermIsVisible(hud) {
+ let inputContainer = hud.ui.window.document
+ .querySelector(".jsterm-input-container");
+ isnot(inputContainer.style.display, "none", "input is visible");
+}
+
+function testObjectInspectorPropertiesAreSet(variablesView) {
+ isnot(variablesView.eval, null, "vview.eval is set");
+ isnot(variablesView.switch, null, "vview.switch is set");
+ isnot(variablesView.delete, null, "vview.delete is set");
+}
+
+function testJSTermIsNotVisible(hud) {
+ let inputContainer = hud.ui.window.document
+ .querySelector(".jsterm-input-container");
+ is(inputContainer.style.display, "none", "input is not visible");
+}
+
+function* testRunner() {
+ let browserConsole, webConsole, variablesView;
+
+ Services.prefs.setBoolPref("devtools.chrome.enabled", true);
+
+ browserConsole = yield HUDService.toggleBrowserConsole();
+ variablesView = yield getVariablesView(browserConsole);
+ testJSTermIsVisible(browserConsole);
+ testObjectInspectorPropertiesAreSet(variablesView);
+
+ let {tab: browserTab} = yield loadTab("data:text/html;charset=utf8,hello world");
+ webConsole = yield openConsole(browserTab);
+ variablesView = yield getVariablesView(webConsole);
+ testJSTermIsVisible(webConsole);
+ testObjectInspectorPropertiesAreSet(variablesView);
+ yield closeConsole(browserTab);
+
+ yield HUDService.toggleBrowserConsole();
+ Services.prefs.setBoolPref("devtools.chrome.enabled", false);
+
+ browserConsole = yield HUDService.toggleBrowserConsole();
+ variablesView = yield getVariablesView(browserConsole);
+ testJSTermIsNotVisible(browserConsole);
+ testObjectInspectorPropertiesAreNotSet(variablesView);
+
+ webConsole = yield openConsole(browserTab);
+ variablesView = yield getVariablesView(webConsole);
+ testJSTermIsVisible(webConsole);
+ testObjectInspectorPropertiesAreSet(variablesView);
+ yield closeConsole(browserTab);
+}
+
+function test() {
+ Task.spawn(testRunner).then(finishTest);
+}
diff --git a/devtools/client/webconsole/test/browser_console_history_persist.js b/devtools/client/webconsole/test/browser_console_history_persist.js
new file mode 100644
index 000000000..61c4cbf4d
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_history_persist.js
@@ -0,0 +1,119 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Test that console command input is persisted across toolbox loads.
+// See Bug 943306.
+
+"use strict";
+
+requestLongerTimeout(2);
+
+const TEST_URI = "data:text/html;charset=utf-8,Web Console test for " +
+ "persisting history - bug 943306";
+const INPUT_HISTORY_COUNT = 10;
+
+add_task(function* () {
+ info("Setting custom input history pref to " + INPUT_HISTORY_COUNT);
+ Services.prefs.setIntPref("devtools.webconsole.inputHistoryCount",
+ INPUT_HISTORY_COUNT);
+
+ // First tab: run a bunch of commands and then make sure that you can
+ // navigate through their history.
+ yield loadTab(TEST_URI);
+ let hud1 = yield openConsole();
+ is(JSON.stringify(hud1.jsterm.history), "[]",
+ "No history on first tab initially");
+ yield populateInputHistory(hud1);
+ is(JSON.stringify(hud1.jsterm.history),
+ '["0","1","2","3","4","5","6","7","8","9"]',
+ "First tab has populated history");
+
+ // Second tab: Just make sure that you can navigate through the history
+ // generated by the first tab.
+ yield loadTab(TEST_URI);
+ let hud2 = yield openConsole();
+ is(JSON.stringify(hud2.jsterm.history),
+ '["0","1","2","3","4","5","6","7","8","9"]',
+ "Second tab has populated history");
+ yield testNaviatingHistoryInUI(hud2);
+ is(JSON.stringify(hud2.jsterm.history),
+ '["0","1","2","3","4","5","6","7","8","9",""]',
+ "An empty entry has been added in the second tab due to history perusal");
+
+ // Third tab: Should have the same history as first tab, but if we run a
+ // command, then the history of the first and second shouldn't be affected
+ yield loadTab(TEST_URI);
+ let hud3 = yield openConsole();
+ is(JSON.stringify(hud3.jsterm.history),
+ '["0","1","2","3","4","5","6","7","8","9"]',
+ "Third tab has populated history");
+
+ // Set input value separately from execute so UP arrow accurately navigates
+ // history.
+ hud3.jsterm.setInputValue('"hello from third tab"');
+ hud3.jsterm.execute();
+
+ is(JSON.stringify(hud1.jsterm.history),
+ '["0","1","2","3","4","5","6","7","8","9"]',
+ "First tab history hasn't changed due to command in third tab");
+ is(JSON.stringify(hud2.jsterm.history),
+ '["0","1","2","3","4","5","6","7","8","9",""]',
+ "Second tab history hasn't changed due to command in third tab");
+ is(JSON.stringify(hud3.jsterm.history),
+ '["1","2","3","4","5","6","7","8","9","\\"hello from third tab\\""]',
+ "Third tab has updated history (and purged the first result) after " +
+ "running a command");
+
+ // Fourth tab: Should have the latest command from the third tab, followed
+ // by the rest of the history from the first tab.
+ yield loadTab(TEST_URI);
+ let hud4 = yield openConsole();
+ is(JSON.stringify(hud4.jsterm.history),
+ '["1","2","3","4","5","6","7","8","9","\\"hello from third tab\\""]',
+ "Fourth tab has most recent history");
+
+ yield hud4.jsterm.clearHistory();
+ is(JSON.stringify(hud4.jsterm.history), "[]",
+ "Clearing history for a tab works");
+
+ yield loadTab(TEST_URI);
+ let hud5 = yield openConsole();
+ is(JSON.stringify(hud5.jsterm.history), "[]",
+ "Clearing history carries over to a new tab");
+
+ info("Clearing custom input history pref");
+ Services.prefs.clearUserPref("devtools.webconsole.inputHistoryCount");
+});
+
+/**
+ * Populate the history by running the following commands:
+ * [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
+ */
+function* populateInputHistory(hud) {
+ let jsterm = hud.jsterm;
+
+ for (let i = 0; i < INPUT_HISTORY_COUNT; i++) {
+ // Set input value separately from execute so UP arrow accurately navigates
+ // history.
+ jsterm.setInputValue(i);
+ jsterm.execute();
+ }
+}
+
+/**
+ * Check pressing up results in history traversal like:
+ * [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
+ */
+function* testNaviatingHistoryInUI(hud) {
+ let jsterm = hud.jsterm;
+ jsterm.focus();
+
+ // Count backwards from original input and make sure that pressing up
+ // restores this.
+ for (let i = INPUT_HISTORY_COUNT - 1; i >= 0; i--) {
+ EventUtils.synthesizeKey("VK_UP", {});
+ is(jsterm.getInputValue(), i, "Pressing up restores last input");
+ }
+}
diff --git a/devtools/client/webconsole/test/browser_console_iframe_messages.js b/devtools/client/webconsole/test/browser_console_iframe_messages.js
new file mode 100644
index 000000000..9bf3fe2b7
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_iframe_messages.js
@@ -0,0 +1,114 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Check that cached messages from nested iframes are displayed in the
+// Web/Browser Console.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-consoleiframes.html";
+
+const expectedMessages = [
+ {
+ text: "main file",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ },
+ {
+ text: "blah",
+ category: CATEGORY_JS,
+ severity: SEVERITY_ERROR
+ },
+ {
+ text: "iframe 2",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG
+ },
+ {
+ text: "iframe 3",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG
+ }
+];
+
+// "iframe 1" console messages can be coalesced into one if they follow each
+// other in the sequence of messages (depending on timing). If they do not, then
+// they will be displayed in the console output independently, as separate
+// messages. This is why we need to match any of the following two rules.
+const expectedMessagesAny = [
+ {
+ name: "iframe 1 (count: 2)",
+ text: "iframe 1",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ count: 2
+ },
+ {
+ name: "iframe 1 (repeats: 2)",
+ text: "iframe 1",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ repeats: 2
+ },
+];
+
+add_task(function* () {
+ // On e10s, the exception is triggered in child process
+ // and is ignored by test harness
+ if (!Services.appinfo.browserTabsRemoteAutostart) {
+ expectUncaughtException();
+ }
+
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+ ok(hud, "web console opened");
+
+ yield testWebConsole(hud);
+ yield closeConsole();
+ info("web console closed");
+
+ hud = yield HUDService.toggleBrowserConsole();
+ yield testBrowserConsole(hud);
+ yield closeConsole();
+});
+
+function* testWebConsole(hud) {
+ yield waitForMessages({
+ webconsole: hud,
+ messages: expectedMessages,
+ });
+
+ info("first messages matched");
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: expectedMessagesAny,
+ matchCondition: "any",
+ });
+}
+
+function* testBrowserConsole(hud) {
+ ok(hud, "browser console opened");
+
+ // TODO: The browser console doesn't show page's console.log statements
+ // in e10s windows. See Bug 1241289.
+ if (Services.appinfo.browserTabsRemoteAutostart) {
+ todo(false, "Bug 1241289");
+ return;
+ }
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: expectedMessages,
+ });
+
+ info("first messages matched");
+ yield waitForMessages({
+ webconsole: hud,
+ messages: expectedMessagesAny,
+ matchCondition: "any",
+ });
+}
diff --git a/devtools/client/webconsole/test/browser_console_keyboard_accessibility.js b/devtools/client/webconsole/test/browser_console_keyboard_accessibility.js
new file mode 100644
index 000000000..c64e45f5d
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_keyboard_accessibility.js
@@ -0,0 +1,89 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Check that basic keyboard shortcuts work in the web console.
+
+"use strict";
+
+add_task(function* () {
+ const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console.html";
+
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+ ok(hud, "Web Console opened");
+
+ info("dump some spew into the console for scrolling");
+ hud.jsterm.execute("(function() { for (var i = 0; i < 100; i++) { " +
+ "console.log('foobarz' + i);" +
+ "}})();");
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "foobarz99",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+
+ let currentPosition = hud.ui.outputWrapper.scrollTop;
+ let bottom = currentPosition;
+
+ EventUtils.synthesizeKey("VK_PAGE_UP", {});
+ isnot(hud.ui.outputWrapper.scrollTop, currentPosition,
+ "scroll position changed after page up");
+
+ currentPosition = hud.ui.outputWrapper.scrollTop;
+ EventUtils.synthesizeKey("VK_PAGE_DOWN", {});
+ ok(hud.ui.outputWrapper.scrollTop > currentPosition,
+ "scroll position now at bottom");
+
+ EventUtils.synthesizeKey("VK_HOME", {});
+ is(hud.ui.outputWrapper.scrollTop, 0, "scroll position now at top");
+
+ EventUtils.synthesizeKey("VK_END", {});
+
+ let scrollTop = hud.ui.outputWrapper.scrollTop;
+ ok(scrollTop > 0 && Math.abs(scrollTop - bottom) <= 5,
+ "scroll position now at bottom");
+
+ info("try ctrl-l to clear output");
+ executeSoon(() => {
+ let clearShortcut;
+ if (Services.appinfo.OS === "Darwin") {
+ clearShortcut = WCUL10n.getStr("webconsole.clear.keyOSX");
+ } else {
+ clearShortcut = WCUL10n.getStr("webconsole.clear.key");
+ }
+ synthesizeKeyShortcut(clearShortcut);
+ });
+ yield hud.jsterm.once("messages-cleared");
+
+ is(hud.outputNode.textContent.indexOf("foobarz1"), -1, "output cleared");
+ is(hud.jsterm.inputNode.getAttribute("focused"), "true",
+ "jsterm input is focused");
+
+ info("try ctrl-f to focus filter");
+ synthesizeKeyShortcut(WCUL10n.getStr("webconsole.find.key"));
+ ok(!hud.jsterm.inputNode.getAttribute("focused"),
+ "jsterm input is not focused");
+ is(hud.ui.filterBox.getAttribute("focused"), "true",
+ "filter input is focused");
+
+ if (Services.appinfo.OS == "Darwin") {
+ ok(hud.ui.getFilterState("network"), "network category is enabled");
+ EventUtils.synthesizeKey("t", { ctrlKey: true });
+ ok(!hud.ui.getFilterState("network"), "accesskey for Network works");
+ EventUtils.synthesizeKey("t", { ctrlKey: true });
+ ok(hud.ui.getFilterState("network"), "accesskey for Network works (again)");
+ } else {
+ EventUtils.synthesizeKey("N", { altKey: true });
+ let net = hud.ui.document.querySelector("toolbarbutton[category=net]");
+ is(hud.ui.document.activeElement, net,
+ "accesskey for Network category focuses the Net button");
+ }
+});
diff --git a/devtools/client/webconsole/test/browser_console_log_inspectable_object.js b/devtools/client/webconsole/test/browser_console_log_inspectable_object.js
new file mode 100644
index 000000000..f9fd85295
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_log_inspectable_object.js
@@ -0,0 +1,52 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Test that objects given to console.log() are inspectable.
+
+"use strict";
+
+add_task(function* () {
+ yield loadTab("data:text/html;charset=utf8,test for bug 676722 - " +
+ "inspectable objects for window.console");
+
+ let hud = yield openConsole();
+ hud.jsterm.clearOutput(true);
+
+ yield hud.jsterm.execute("myObj = {abba: 'omgBug676722'}");
+ hud.jsterm.execute("console.log('fooBug676722', myObj)");
+
+ let [result] = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "fooBug676722",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ objects: true,
+ }],
+ });
+
+ let msg = [...result.matched][0];
+ ok(msg, "message element");
+
+ let body = msg.querySelector(".message-body");
+ ok(body, "message body");
+
+ let clickable = result.clickableElements[0];
+ ok(clickable, "the console.log() object anchor was found");
+ ok(body.textContent.includes('{ abba: "omgBug676722" }'),
+ "clickable node content is correct");
+
+ executeSoon(() => {
+ EventUtils.synthesizeMouse(clickable, 2, 2, {}, hud.iframeWindow);
+ });
+
+ let varView = yield hud.jsterm.once("variablesview-fetched");
+ ok(varView, "object inspector opened on click");
+
+ yield findVariableViewProperties(varView, [{
+ name: "abba",
+ value: "omgBug676722",
+ }], { webconsole: hud });
+});
diff --git a/devtools/client/webconsole/test/browser_console_native_getters.js b/devtools/client/webconsole/test/browser_console_native_getters.js
new file mode 100644
index 000000000..1afb70796
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_native_getters.js
@@ -0,0 +1,101 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Check that native getters and setters for DOM elements work as expected in
+// variables view - bug 870220.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf8,<title>bug870220</title>\n" +
+ "<p>hello world\n<p>native getters!";
+
+requestLongerTimeout(2);
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+ let jsterm = hud.jsterm;
+
+ jsterm.execute("document");
+
+ let [result] = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "HTMLDocument \u2192 data:text/html;charset=utf8",
+ category: CATEGORY_OUTPUT,
+ objects: true,
+ }],
+ });
+
+ let clickable = result.clickableElements[0];
+ ok(clickable, "clickable object found");
+
+ executeSoon(() => {
+ EventUtils.synthesizeMouse(clickable, 2, 2, {}, hud.iframeWindow);
+ });
+
+ let fetchedVar = yield jsterm.once("variablesview-fetched");
+
+ let variablesView = fetchedVar._variablesView;
+ ok(variablesView, "variables view object");
+
+ let results = yield findVariableViewProperties(fetchedVar, [
+ { name: "title", value: "bug870220" },
+ { name: "bgColor" },
+ ], { webconsole: hud });
+
+ let prop = results[1].matchedProp;
+ ok(prop, "matched the |bgColor| property in the variables view");
+
+ // Check that property value updates work.
+ let updatedVar = yield updateVariablesViewProperty({
+ property: prop,
+ field: "value",
+ string: "'red'",
+ webconsole: hud,
+ });
+
+ info("on fetch after background update");
+
+ jsterm.clearOutput(true);
+ jsterm.execute("document.bgColor");
+
+ [result] = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "red",
+ category: CATEGORY_OUTPUT,
+ }],
+ });
+
+ yield findVariableViewProperties(updatedVar, [
+ { name: "bgColor", value: "red" },
+ ], { webconsole: hud });
+
+ jsterm.execute("$$('p')");
+
+ [result] = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "Array [",
+ category: CATEGORY_OUTPUT,
+ objects: true,
+ }],
+ });
+
+ clickable = result.clickableElements[0];
+ ok(clickable, "clickable object found");
+
+ executeSoon(() => {
+ EventUtils.synthesizeMouse(clickable, 2, 2, {}, hud.iframeWindow);
+ });
+
+ fetchedVar = yield jsterm.once("variablesview-fetched");
+
+ yield findVariableViewProperties(fetchedVar, [
+ { name: "0.textContent", value: /hello world/ },
+ { name: "1.textContent", value: /native getters/ },
+ ], { webconsole: hud });
+});
diff --git a/devtools/client/webconsole/test/browser_console_navigation_marker.js b/devtools/client/webconsole/test/browser_console_navigation_marker.js
new file mode 100644
index 000000000..e8ec84caf
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_navigation_marker.js
@@ -0,0 +1,81 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Check that the navigation marker shows on page reload - bug 793996.
+
+"use strict";
+
+const PREF = "devtools.webconsole.persistlog";
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console.html";
+
+var hud;
+
+add_task(function* () {
+ Services.prefs.setBoolPref(PREF, true);
+
+ let { browser } = yield loadTab(TEST_URI);
+ hud = yield openConsole();
+
+ yield consoleOpened();
+
+ let loaded = loadBrowser(browser);
+ BrowserReload();
+ yield loaded;
+
+ yield onReload();
+
+ isnot(hud.outputNode.textContent.indexOf("foobarz1"), -1,
+ "foobarz1 is still in the output");
+
+ Services.prefs.clearUserPref(PREF);
+
+ hud = null;
+});
+
+function consoleOpened() {
+ ok(hud, "Web Console opened");
+
+ hud.jsterm.clearOutput();
+
+ ContentTask.spawn(gBrowser.selectedBrowser, null, function* () {
+ content.console.log("foobarz1");
+ });
+
+ return waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "foobarz1",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+}
+
+function onReload() {
+ ContentTask.spawn(gBrowser.selectedBrowser, null, function* () {
+ content.console.log("foobarz2");
+ });
+
+ return waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "page reload",
+ text: "test-console.html",
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_LOG,
+ },
+ {
+ text: "foobarz2",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ },
+ {
+ name: "navigation marker",
+ text: "test-console.html",
+ type: Messages.NavigationMarker,
+ }],
+ });
+}
diff --git a/devtools/client/webconsole/test/browser_console_netlogging.js b/devtools/client/webconsole/test/browser_console_netlogging.js
new file mode 100644
index 000000000..a6f7bec48
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_netlogging.js
@@ -0,0 +1,38 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests that network log messages bring up the network panel.
+
+"use strict";
+
+const TEST_NETWORK_REQUEST_URI =
+ "http://example.com/browser/devtools/client/webconsole/test/" +
+ "test-network-request.html";
+
+add_task(function* () {
+ let finishedRequest = waitForFinishedRequest(({ request }) => {
+ return request.url === TEST_NETWORK_REQUEST_URI;
+ });
+
+ const hud = yield loadPageAndGetHud(TEST_NETWORK_REQUEST_URI,
+ "browserConsole");
+ let request = yield finishedRequest;
+
+ ok(request, "Page load was logged");
+
+ let client = hud.ui.webConsoleClient;
+ let args = [request.actor];
+ const postData = yield getPacket(client, "getRequestPostData", args);
+ const responseContent = yield getPacket(client, "getResponseContent", args);
+
+ is(request.request.url, TEST_NETWORK_REQUEST_URI,
+ "Logged network entry is page load");
+ is(request.request.method, "GET", "Method is correct");
+ ok(!postData.postData.text, "No request body was stored");
+ ok(postData.postDataDiscarded, "Request body was discarded");
+ ok(!responseContent.content.text, "No response body was stored");
+ ok(responseContent.contentDiscarded || request.fromCache,
+ "Response body was discarded or response came from the cache");
+});
diff --git a/devtools/client/webconsole/test/browser_console_nsiconsolemessage.js b/devtools/client/webconsole/test/browser_console_nsiconsolemessage.js
new file mode 100644
index 000000000..fedd0c71c
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_nsiconsolemessage.js
@@ -0,0 +1,85 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Check that nsIConsoleMessages are displayed in the Browser Console.
+// See bug 859756.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf8,<title>bug859756</title>\n" +
+ "<p>hello world\n<p>nsIConsoleMessages ftw!";
+
+function test() {
+ const FILTER_PREF = "devtools.browserconsole.filter.jslog";
+ Services.prefs.setBoolPref(FILTER_PREF, true);
+
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref(FILTER_PREF);
+ });
+
+ Task.spawn(function* () {
+ const {tab} = yield loadTab(TEST_URI);
+
+ // Test for cached nsIConsoleMessages.
+ Services.console.logStringMessage("test1 for bug859756");
+
+ info("open web console");
+ let hud = yield openConsole(tab);
+
+ ok(hud, "web console opened");
+ Services.console.logStringMessage("do-not-show-me");
+
+ ContentTask.spawn(gBrowser.selectedBrowser, null, function* () {
+ content.console.log("foobarz");
+ });
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "foobarz",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+
+ let text = hud.outputNode.textContent;
+ is(text.indexOf("do-not-show-me"), -1,
+ "nsIConsoleMessages are not displayed");
+ is(text.indexOf("test1 for bug859756"), -1,
+ "nsIConsoleMessages are not displayed (confirmed)");
+
+ yield closeConsole(tab);
+
+ info("web console closed");
+ hud = yield HUDService.toggleBrowserConsole();
+ ok(hud, "browser console opened");
+
+ Services.console.logStringMessage("test2 for bug859756");
+
+ let results = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "test1 for bug859756",
+ category: CATEGORY_JS,
+ }, {
+ text: "test2 for bug859756",
+ category: CATEGORY_JS,
+ }, {
+ text: "do-not-show-me",
+ category: CATEGORY_JS,
+ }],
+ });
+
+ let msg = [...results[2].matched][0];
+ ok(msg, "message element for do-not-show-me (nsIConsoleMessage)");
+ isnot(msg.textContent.indexOf("do-not-show"), -1,
+ "element content is correct");
+ ok(!msg.classList.contains("filtered-by-type"), "element is not filtered");
+
+ hud.setFilterState("jslog", false);
+
+ ok(msg.classList.contains("filtered-by-type"), "element is filtered");
+ }).then(finishTest);
+}
diff --git a/devtools/client/webconsole/test/browser_console_open_or_focus.js b/devtools/client/webconsole/test/browser_console_open_or_focus.js
new file mode 100644
index 000000000..d537c9aad
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_open_or_focus.js
@@ -0,0 +1,46 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Test that the "browser console" menu item opens or focuses (if already open)
+// the console window instead of toggling it open/close.
+
+"use strict";
+
+var {Tools} = require("devtools/client/definitions");
+
+add_task(function* () {
+ let currWindow, hud, mainWindow;
+
+ mainWindow = Services.wm.getMostRecentWindow(null);
+
+ yield HUDService.openBrowserConsoleOrFocus();
+
+ hud = HUDService.getBrowserConsole();
+
+ console.log("testmessage");
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "testmessage"
+ }],
+ });
+
+ currWindow = Services.wm.getMostRecentWindow(null);
+ is(currWindow.document.documentURI, Tools.webConsole.url,
+ "The Browser Console is open and has focus");
+
+ mainWindow.focus();
+
+ yield HUDService.openBrowserConsoleOrFocus();
+
+ currWindow = Services.wm.getMostRecentWindow(null);
+ is(currWindow.document.documentURI, Tools.webConsole.url,
+ "The Browser Console is open and has focus");
+
+ yield HUDService.toggleBrowserConsole();
+
+ hud = HUDService.getBrowserConsole();
+ ok(!hud, "Browser Console has been closed");
+});
diff --git a/devtools/client/webconsole/test/browser_console_optimized_out_vars.js b/devtools/client/webconsole/test/browser_console_optimized_out_vars.js
new file mode 100644
index 000000000..dc898eb2b
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_optimized_out_vars.js
@@ -0,0 +1,91 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Check that inspecting an optimized out variable works when execution is
+// paused.
+
+"use strict";
+
+// Force the old debugger UI since it's directly used (see Bug 1301705)
+Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false);
+registerCleanupFunction(function* () {
+ Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend");
+});
+
+function test() {
+ Task.spawn(function* () {
+ const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-closure-optimized-out.html";
+ let {tab} = yield loadTab(TEST_URI);
+ let hud = yield openConsole(tab);
+ let { toolbox, panel, panelWin } = yield openDebugger();
+
+ let sources = panelWin.DebuggerView.Sources;
+ yield panel.addBreakpoint({ actor: sources.values[0], line: 18 });
+ yield ensureThreadClientState(panel, "resumed");
+
+ let fetchedScopes = panelWin.once(panelWin.EVENTS.FETCHED_SCOPES);
+
+ // Cause the debuggee to pause
+ ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ let button = content.document.querySelector("button");
+ button.click();
+ });
+
+ yield fetchedScopes;
+ ok(true, "Scopes were fetched");
+
+ yield toolbox.selectTool("webconsole");
+
+ // This is the meat of the test: evaluate the optimized out variable.
+ hud.jsterm.execute("upvar");
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "optimized out",
+ category: CATEGORY_OUTPUT,
+ }]
+ });
+
+ finishTest();
+ }).then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+}
+
+// Debugger helper functions stolen from devtools/client/debugger/test/head.js.
+
+function ensureThreadClientState(aPanel, aState) {
+ let thread = aPanel.panelWin.gThreadClient;
+ let state = thread.state;
+
+ info("Thread is: '" + state + "'.");
+
+ if (state == aState) {
+ return promise.resolve(null);
+ }
+ return waitForThreadEvents(aPanel, aState);
+}
+
+function waitForThreadEvents(aPanel, aEventName, aEventRepeat = 1) {
+ info("Waiting for thread event: '" + aEventName + "' to fire: " +
+ aEventRepeat + " time(s).");
+
+ let deferred = promise.defer();
+ let thread = aPanel.panelWin.gThreadClient;
+ let count = 0;
+
+ thread.addListener(aEventName, function onEvent(eventName, ...args) {
+ info("Thread event '" + eventName + "' fired: " + (++count) + " time(s).");
+
+ if (count == aEventRepeat) {
+ ok(true, "Enough '" + eventName + "' thread events have been fired.");
+ thread.removeListener(eventName, onEvent);
+ deferred.resolve.apply(deferred, args);
+ }
+ });
+
+ return deferred.promise;
+}
diff --git a/devtools/client/webconsole/test/browser_console_private_browsing.js b/devtools/client/webconsole/test/browser_console_private_browsing.js
new file mode 100644
index 000000000..4b3a79329
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_private_browsing.js
@@ -0,0 +1,192 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Bug 874061: test for how the browser and web consoles display messages coming
+// from private windows. See bug for description of expected behavior.
+
+"use strict";
+
+function test() {
+ const TEST_URI = "data:text/html;charset=utf8,<p>hello world! bug 874061" +
+ "<button onclick='console.log(\"foobar bug 874061\");" +
+ "fooBazBaz.yummy()'>click</button>";
+ let ConsoleAPIStorage = Cc["@mozilla.org/consoleAPI-storage;1"]
+ .getService(Ci.nsIConsoleAPIStorage);
+ let privateWindow, privateBrowser, privateTab, privateContent;
+ let hud, expectedMessages, nonPrivateMessage;
+
+ // This test is slightly more involved: it opens the web console twice,
+ // a new private window once, and the browser console twice. We can get
+ // a timeout with debug builds on slower machines.
+ requestLongerTimeout(2);
+ start();
+
+ function start() {
+ gBrowser.selectedTab = gBrowser.addTab("data:text/html;charset=utf8," +
+ "<p>hello world! I am not private!");
+ gBrowser.selectedBrowser.addEventListener("load", onLoadTab, true);
+ }
+
+ function onLoadTab() {
+ gBrowser.selectedBrowser.removeEventListener("load", onLoadTab, true);
+ info("onLoadTab()");
+
+ // Make sure we have a clean state to start with.
+ Services.console.reset();
+ ConsoleAPIStorage.clearEvents();
+
+ // Add a non-private message to the browser console.
+ ContentTask.spawn(gBrowser.selectedBrowser, null, function* () {
+ content.console.log("bug874061-not-private");
+ });
+
+ nonPrivateMessage = {
+ name: "console message from a non-private window",
+ text: "bug874061-not-private",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ };
+
+ privateWindow = OpenBrowserWindow({ private: true });
+ ok(privateWindow, "new private window");
+ ok(PrivateBrowsingUtils.isWindowPrivate(privateWindow), "window's private");
+
+ whenDelayedStartupFinished(privateWindow, onPrivateWindowReady);
+ }
+
+ function onPrivateWindowReady() {
+ info("private browser window opened");
+ privateBrowser = privateWindow.gBrowser;
+
+ privateTab = privateBrowser.selectedTab = privateBrowser.addTab(TEST_URI);
+ privateBrowser.selectedBrowser.addEventListener("load", function onLoad() {
+ info("private tab opened");
+ privateBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
+ privateContent = privateBrowser.selectedBrowser.contentWindow;
+ ok(PrivateBrowsingUtils.isBrowserPrivate(privateBrowser.selectedBrowser),
+ "tab window is private");
+ openConsole(privateTab).then(consoleOpened);
+ }, true);
+ }
+
+ function addMessages() {
+ let button = privateContent.document.querySelector("button");
+ ok(button, "button in page");
+ EventUtils.synthesizeMouse(button, 2, 2, {}, privateContent);
+ }
+
+ function consoleOpened(injectedHud) {
+ hud = injectedHud;
+ ok(hud, "web console opened");
+
+ addMessages();
+ expectedMessages = [
+ {
+ name: "script error",
+ text: "fooBazBaz is not defined",
+ category: CATEGORY_JS,
+ severity: SEVERITY_ERROR,
+ },
+ {
+ name: "console message",
+ text: "foobar bug 874061",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ },
+ ];
+
+ // Make sure messages are displayed in the web console as they happen, even
+ // if this is a private tab.
+ waitForMessages({
+ webconsole: hud,
+ messages: expectedMessages,
+ }).then(testCachedMessages);
+ }
+
+ function testCachedMessages() {
+ info("testCachedMessages()");
+ closeConsole(privateTab).then(() => {
+ info("web console closed");
+ openConsole(privateTab).then(consoleReopened);
+ });
+ }
+
+ function consoleReopened(injectedHud) {
+ hud = injectedHud;
+ ok(hud, "web console reopened");
+
+ // Make sure that cached messages are displayed in the web console, even
+ // if this is a private tab.
+ waitForMessages({
+ webconsole: hud,
+ messages: expectedMessages,
+ }).then(testBrowserConsole);
+ }
+
+ function testBrowserConsole() {
+ info("testBrowserConsole()");
+ closeConsole(privateTab).then(() => {
+ info("web console closed");
+ HUDService.toggleBrowserConsole().then(onBrowserConsoleOpen);
+ });
+ }
+
+ // Make sure that the cached messages from private tabs are not displayed in
+ // the browser console.
+ function checkNoPrivateMessages() {
+ let text = hud.outputNode.textContent;
+ is(text.indexOf("fooBazBaz"), -1, "no exception displayed");
+ is(text.indexOf("bug 874061"), -1, "no console message displayed");
+ }
+
+ function onBrowserConsoleOpen(injectedHud) {
+ hud = injectedHud;
+ ok(hud, "browser console opened");
+
+ checkNoPrivateMessages();
+ addMessages();
+ expectedMessages.push(nonPrivateMessage);
+
+ // Make sure that live messages are displayed in the browser console, even
+ // from private tabs.
+ waitForMessages({
+ webconsole: hud,
+ messages: expectedMessages,
+ }).then(testPrivateWindowClose);
+ }
+
+ function testPrivateWindowClose() {
+ info("close the private window and check if private messages are removed");
+ hud.jsterm.once("private-messages-cleared", () => {
+ isnot(hud.outputNode.textContent.indexOf("bug874061-not-private"), -1,
+ "non-private messages are still shown after private window closed");
+ checkNoPrivateMessages();
+
+ info("close the browser console");
+ HUDService.toggleBrowserConsole().then(() => {
+ info("reopen the browser console");
+ executeSoon(() =>
+ HUDService.toggleBrowserConsole().then(onBrowserConsoleReopen));
+ });
+ });
+ privateWindow.BrowserTryToCloseWindow();
+ }
+
+ function onBrowserConsoleReopen(injectedHud) {
+ hud = injectedHud;
+ ok(hud, "browser console reopened");
+
+ // Make sure that the non-private message is still shown after reopen.
+ waitForMessages({
+ webconsole: hud,
+ messages: [nonPrivateMessage],
+ }).then(() => {
+ // Make sure that no private message is displayed after closing the
+ // private window and reopening the Browser Console.
+ checkNoPrivateMessages();
+ executeSoon(finishTest);
+ });
+ }
+}
diff --git a/devtools/client/webconsole/test/browser_console_server_logging.js b/devtools/client/webconsole/test/browser_console_server_logging.js
new file mode 100644
index 000000000..eaef12330
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_server_logging.js
@@ -0,0 +1,74 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 server log appears in the console panel - bug 1168872
+add_task(function* () {
+ const TEST_URI = "http://example.com/browser/devtools/client/webconsole/test/test-console-server-logging.sjs";
+
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ // Set logging filter and wait till it's set on the backend
+ hud.setFilterState("serverlog", true);
+ yield updateServerLoggingListener(hud);
+
+ BrowserReloadSkipCache();
+
+ // Note that the test is also checking out the (printf like)
+ // formatters and encoding of UTF8 characters (see the one at the end).
+ let text = "values: string Object { a: 10 } 123 1.12 \u2713";
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: text,
+ category: CATEGORY_SERVER,
+ severity: SEVERITY_LOG,
+ }],
+ });
+ // Clean up filter
+ hud.setFilterState("serverlog", false);
+ yield updateServerLoggingListener(hud);
+});
+
+add_task(function* () {
+ const TEST_URI = "http://example.com/browser/devtools/client/webconsole/test/test-console-server-logging-array.sjs";
+
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ // Set logging filter and wait till it's set on the backend
+ hud.setFilterState("serverlog", true);
+ yield updateServerLoggingListener(hud);
+
+ BrowserReloadSkipCache();
+ // Note that the test is also checking out the (printf like)
+ // formatters and encoding of UTF8 characters (see the one at the end).
+ let text = "Object { best: \"Firefox\", reckless: \"Chrome\", " +
+ "new_ie: \"Safari\", new_new_ie: \"Edge\" }";
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: text,
+ category: CATEGORY_SERVER,
+ severity: SEVERITY_LOG,
+ }],
+ });
+ // Clean up filter
+ hud.setFilterState("serverlog", false);
+ yield updateServerLoggingListener(hud);
+});
+
+function updateServerLoggingListener(hud) {
+ let deferred = promise.defer();
+ hud.ui._updateServerLoggingListener(response => {
+ deferred.resolve(response);
+ });
+ return deferred.promise;
+}
diff --git a/devtools/client/webconsole/test/browser_console_variables_view.js b/devtools/client/webconsole/test/browser_console_variables_view.js
new file mode 100644
index 000000000..ecd8071ce
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_variables_view.js
@@ -0,0 +1,204 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Check that variables view works as expected in the web console.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-eval-in-stackframe.html";
+
+var hud, gVariablesView;
+
+registerCleanupFunction(function () {
+ hud = gVariablesView = null;
+});
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ hud = yield openConsole();
+
+ let msg = yield hud.jsterm.execute("(function foo(){})");
+
+ ok(msg, "output message found");
+ ok(msg.textContent.includes("function foo()"),
+ "message text check");
+
+ executeSoon(() => {
+ EventUtils.synthesizeMouse(msg.querySelector("a"), 2, 2, {}, hud.iframeWindow);
+ });
+
+ let varView = yield hud.jsterm.once("variablesview-fetched");
+ ok(varView, "object inspector opened on click");
+
+ yield findVariableViewProperties(varView, [{
+ name: "name",
+ value: "foo",
+ }], { webconsole: hud });
+});
+
+add_task(function* () {
+ let msg = yield hud.jsterm.execute("fooObj");
+
+ ok(msg, "output message found");
+ ok(msg.textContent.includes('{ testProp: "testValue" }'),
+ "message text check");
+
+ let anchor = msg.querySelector("a");
+ ok(anchor, "object link found");
+
+ let fetched = hud.jsterm.once("variablesview-fetched");
+
+ // executeSoon
+ EventUtils.synthesizeMouse(anchor, 2, 2, {}, hud.iframeWindow);
+
+ let view = yield fetched;
+
+ let results = yield onFooObjFetch(view);
+
+ let vView = yield onTestPropFound(results);
+ let results2 = yield onFooObjFetchAfterUpdate(vView);
+
+ let vView2 = yield onUpdatedTestPropFound(results2);
+ let results3 = yield onFooObjFetchAfterPropRename(vView2);
+
+ let vView3 = yield onRenamedTestPropFound(results3);
+ let results4 = yield onPropUpdateError(vView3);
+
+ yield onRenamedTestPropFoundAgain(results4);
+
+ let prop = results4[0].matchedProp;
+ yield testPropDelete(prop);
+});
+
+function onFooObjFetch(aVar) {
+ gVariablesView = aVar._variablesView;
+ ok(gVariablesView, "variables view object");
+
+ return findVariableViewProperties(aVar, [
+ { name: "testProp", value: "testValue" },
+ ], { webconsole: hud });
+}
+
+function onTestPropFound(aResults) {
+ let prop = aResults[0].matchedProp;
+ ok(prop, "matched the |testProp| property in the variables view");
+
+ is("testValue", aResults[0].value,
+ "|fooObj.testProp| value is correct");
+
+ // Check that property value updates work and that jsterm functions can be
+ // used.
+ return updateVariablesViewProperty({
+ property: prop,
+ field: "value",
+ string: "document.title + window.location + $('p')",
+ webconsole: hud
+ });
+}
+
+function onFooObjFetchAfterUpdate(aVar) {
+ info("onFooObjFetchAfterUpdate");
+ let expectedValue = content.document.title + content.location +
+ "[object HTMLParagraphElement]";
+
+ return findVariableViewProperties(aVar, [
+ { name: "testProp", value: expectedValue },
+ ], { webconsole: hud });
+}
+
+function onUpdatedTestPropFound(aResults) {
+ let prop = aResults[0].matchedProp;
+ ok(prop, "matched the updated |testProp| property value");
+
+ is(content.wrappedJSObject.fooObj.testProp, aResults[0].value,
+ "|fooObj.testProp| value has been updated");
+
+ // Check that property name updates work.
+ return updateVariablesViewProperty({
+ property: prop,
+ field: "name",
+ string: "testUpdatedProp",
+ webconsole: hud
+ });
+}
+
+function* onFooObjFetchAfterPropRename(aVar) {
+ info("onFooObjFetchAfterPropRename");
+
+ let expectedValue = yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ let para = content.wrappedJSObject.document.querySelector("p");
+ return content.document.title + content.location + para;
+ });
+
+ // Check that the new value is in the variables view.
+ return findVariableViewProperties(aVar, [
+ { name: "testUpdatedProp", value: expectedValue },
+ ], { webconsole: hud });
+}
+
+function onRenamedTestPropFound(aResults) {
+ let prop = aResults[0].matchedProp;
+ ok(prop, "matched the renamed |testProp| property");
+
+ ok(!content.wrappedJSObject.fooObj.testProp,
+ "|fooObj.testProp| has been deleted");
+ is(content.wrappedJSObject.fooObj.testUpdatedProp, aResults[0].value,
+ "|fooObj.testUpdatedProp| is correct");
+
+ // Check that property value updates that cause exceptions are reported in
+ // the web console output.
+ return updateVariablesViewProperty({
+ property: prop,
+ field: "value",
+ string: "foobarzFailure()",
+ webconsole: hud
+ });
+}
+
+function* onPropUpdateError(aVar) {
+ info("onPropUpdateError");
+
+ let expectedValue = yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ let para = content.wrappedJSObject.document.querySelector("p");
+ return content.document.title + content.location + para;
+ });
+
+ // Make sure the property did not change.
+ return findVariableViewProperties(aVar, [
+ { name: "testUpdatedProp", value: expectedValue },
+ ], { webconsole: hud });
+}
+
+function onRenamedTestPropFoundAgain(aResults) {
+ let prop = aResults[0].matchedProp;
+ ok(prop, "matched the renamed |testProp| property again");
+
+ return waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "exception in property update reported in the web console output",
+ text: "foobarzFailure",
+ category: CATEGORY_OUTPUT,
+ severity: SEVERITY_ERROR,
+ }],
+ });
+}
+
+function testPropDelete(aProp) {
+ gVariablesView.window.focus();
+ aProp.focus();
+
+ executeSoon(() => {
+ EventUtils.synthesizeKey("VK_DELETE", {}, gVariablesView.window);
+ });
+
+ return waitForSuccess({
+ name: "property deleted",
+ timeout: 60000,
+ validator: () => !("testUpdatedProp" in content.wrappedJSObject.fooObj)
+ });
+}
diff --git a/devtools/client/webconsole/test/browser_console_variables_view_dom_nodes.js b/devtools/client/webconsole/test/browser_console_variables_view_dom_nodes.js
new file mode 100644
index 000000000..522fe4754
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_variables_view_dom_nodes.js
@@ -0,0 +1,59 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Test that ensures DOM nodes are rendered correctly in VariablesView.
+
+"use strict";
+
+function test() {
+ const TEST_URI = `
+ data:text/html;charset=utf-8,
+ <html>
+ <head>
+ <title>Test for DOM nodes in variables view</title>
+ </head>
+ <body>
+ <div></div>
+ <div id="testID"></div>
+ <div class="single-class"></div>
+ <div class="multiple-classes another-class"></div>
+ <div class="class-and-id" id="class-and-id"></div>
+ <div class="multiple-classes-and-id another-class"
+ id="multiple-classes-and-id"></div>
+ <div class=" whitespace-start"></div>
+ <div class="whitespace-end "></div>
+ <div class="multiple spaces"></div>
+ </body>
+ </html>
+`;
+
+ Task.spawn(runner).then(finishTest);
+
+ function* runner() {
+ const {tab} = yield loadTab(TEST_URI);
+ const hud = yield openConsole(tab);
+ const jsterm = hud.jsterm;
+
+ let deferred = promise.defer();
+ jsterm.once("variablesview-fetched", (_, val) => deferred.resolve(val));
+ jsterm.execute("inspect(document.querySelectorAll('div'))");
+
+ let variableScope = yield deferred.promise;
+ ok(variableScope, "Variables view opened");
+
+ yield findVariableViewProperties(variableScope, [
+ { name: "0", value: "<div>"},
+ { name: "1", value: "<div#testID>"},
+ { name: "2", value: "<div.single-class>"},
+ { name: "3", value: "<div.multiple-classes.another-class>"},
+ { name: "4", value: "<div#class-and-id.class-and-id>"},
+ { name: "5", value: "<div#multiple-classes-and-id." +
+ "multiple-classes-and-id.another-class>"},
+ { name: "6", value: "<div.whitespace-start>"},
+ { name: "7", value: "<div.whitespace-end>"},
+ { name: "8", value: "<div.multiple.spaces>"},
+ ], { webconsole: hud});
+ }
+}
diff --git a/devtools/client/webconsole/test/browser_console_variables_view_dont_sort_non_sortable_classes_properties.js b/devtools/client/webconsole/test/browser_console_variables_view_dont_sort_non_sortable_classes_properties.js
new file mode 100644
index 000000000..ec9ffa7b7
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_variables_view_dont_sort_non_sortable_classes_properties.js
@@ -0,0 +1,135 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 case that ensures Array and other list types are not sorted in variables
+ * view.
+ *
+ * The tested types are:
+ * - Array
+ * - Int8Array
+ * - Int16Array
+ * - Int32Array
+ * - Uint8Array
+ * - Uint16Array
+ * - Uint32Array
+ * - Uint8ClampedArray
+ * - Float32Array
+ * - Float64Array
+ * - NodeList
+ */
+
+function test() {
+ const TEST_URI = "data:text/html;charset=utf-8, \
+ <html> \
+ <head> \
+ <title>Test document for bug 977500</title> \
+ </head> \
+ <body> \
+ <div></div> \
+ <div></div> \
+ <div></div> \
+ <div></div> \
+ <div></div> \
+ <div></div> \
+ <div></div> \
+ <div></div> \
+ <div></div> \
+ <div></div> \
+ <div></div> \
+ <div></div> \
+ </body> \
+ </html>";
+
+ let jsterm;
+
+ function* runner() {
+ const typedArrayTypes = ["Int8Array", "Int16Array", "Int32Array",
+ "Uint8Array", "Uint16Array", "Uint32Array",
+ "Uint8ClampedArray", "Float32Array",
+ "Float64Array"];
+
+ const {tab} = yield loadTab(TEST_URI);
+ const hud = yield openConsole(tab);
+ jsterm = hud.jsterm;
+
+ // Create an ArrayBuffer of 80 bytes to test TypedArrays. 80 bytes is
+ // enough to get 10 items in all different TypedArrays.
+ yield jsterm.execute("let buf = new ArrayBuffer(80);");
+
+ // Array
+ yield testNotSorted("Array(0,1,2,3,4,5,6,7,8,9,10)");
+ // NodeList
+ yield testNotSorted("document.querySelectorAll('div')");
+ // Object
+ yield testSorted("Object({'hello':1,1:5,10:2,4:2,'abc':1})");
+
+ // Typed arrays.
+ for (let type of typedArrayTypes) {
+ yield testNotSorted("new " + type + "(buf)");
+ }
+ }
+
+ /**
+ * A helper that ensures the properties are not sorted when an object
+ * specified by aObject is inspected.
+ *
+ * @param string aObject
+ * A string that, once executed, creates and returns the object to
+ * inspect.
+ */
+ function* testNotSorted(aObject) {
+ info("Testing " + aObject);
+ let deferred = promise.defer();
+ jsterm.once("variablesview-fetched", (_, aVar) => deferred.resolve(aVar));
+ jsterm.execute("inspect(" + aObject + ")");
+
+ let variableScope = yield deferred.promise;
+ ok(variableScope, "Variables view opened");
+
+ // If the properties are sorted: keys = ["0", "1", "10",...] <- incorrect
+ // If the properties are not sorted: keys = ["0", "1", "2",...] <- correct
+ let keyIterator = variableScope._store.keys();
+ is(keyIterator.next().value, "0", "First key is 0");
+ is(keyIterator.next().value, "1", "Second key is 1");
+
+ // If the properties are sorted, the next one will be 10.
+ is(keyIterator.next().value, "2", "Third key is 2, not 10");
+ }
+ /**
+ * A helper that ensures the properties are sorted when an object
+ * specified by aObject is inspected.
+ *
+ * @param string aObject
+ * A string that, once executed, creates and returns the object to
+ * inspect.
+ */
+ function* testSorted(aObject) {
+ info("Testing " + aObject);
+ let deferred = promise.defer();
+ jsterm.once("variablesview-fetched", (_, aVar) => deferred.resolve(aVar));
+ jsterm.execute("inspect(" + aObject + ")");
+
+ let variableScope = yield deferred.promise;
+ ok(variableScope, "Variables view opened");
+
+ // If the properties are sorted:
+ // keys = ["1", "4", "10",..., "abc", "hello"] <- correct
+ // If the properties are not sorted:
+ // keys = ["1", "10", "4",...] <- incorrect
+ let keyIterator = variableScope._store.keys();
+ is(keyIterator.next().value, "1", "First key should be 1");
+ is(keyIterator.next().value, "4", "Second key should be 4");
+
+ // If the properties are sorted, the next one will be 10.
+ is(keyIterator.next().value, "10", "Third key is 10");
+ // If sorted next properties should be "abc" then "hello"
+ is(keyIterator.next().value, "abc", "Fourth key is abc");
+ is(keyIterator.next().value, "hello", "Fifth key is hello");
+ }
+
+ Task.spawn(runner).then(finishTest);
+}
diff --git a/devtools/client/webconsole/test/browser_console_variables_view_filter.js b/devtools/client/webconsole/test/browser_console_variables_view_filter.js
new file mode 100644
index 000000000..142410839
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_variables_view_filter.js
@@ -0,0 +1,80 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Check that variables view filter feature works fine in the console.
+
+function props(view, prefix = "") {
+ // First match only the visible one, not hidden by a search
+ let visible = [...view].filter(([id, prop]) => prop._isMatch);
+ // Then flatten the list into a list of strings
+ // being the jsonpath of each attribute being visible in the view
+ return visible.reduce((list, [id, prop]) => {
+ list.push(prefix + id);
+ return list.concat(props(prop, prefix + id + "."));
+ }, []);
+}
+
+function assertAttrs(view, expected, message) {
+ is(props(view).join(","), expected, message);
+}
+
+add_task(function* () {
+ yield loadTab("data:text/html;charset=utf-8,webconsole-filter");
+
+ let hud = yield openConsole();
+
+ let jsterm = hud.jsterm;
+
+ let fetched = jsterm.once("variablesview-fetched");
+
+ yield jsterm.execute("inspect({ foo: { bar : \"baz\" } })");
+
+ let view = yield fetched;
+ let variablesView = view._variablesView;
+ let searchbox = variablesView._searchboxNode;
+
+ assertAttrs(view, "foo,__proto__",
+ "To start with, we just see the top level foo attr");
+
+ fetched = jsterm.once("variablesview-fetched");
+ searchbox.value = "bar";
+ searchbox.doCommand();
+ view = yield fetched;
+
+ assertAttrs(view, "",
+ "If we don't manually expand nested attr, we don't see them");
+
+ fetched = jsterm.once("variablesview-fetched");
+ searchbox.value = "";
+ searchbox.doCommand();
+ view = yield fetched;
+
+ assertAttrs(view, "foo",
+ "If we reset the search, we get back to original state");
+
+ yield [...view][0][1].expand();
+
+ fetched = jsterm.once("variablesview-fetched");
+ searchbox.value = "bar";
+ searchbox.doCommand();
+ view = yield fetched;
+
+ assertAttrs(view, "foo,foo.bar", "Now if we expand, we see the nested attr");
+
+ fetched = jsterm.once("variablesview-fetched");
+ searchbox.value = "baz";
+ searchbox.doCommand();
+ view = yield fetched;
+
+ assertAttrs(view, "foo,foo.bar", "We can also search for attr values");
+
+ fetched = jsterm.once("variablesview-fetched");
+ searchbox.value = "";
+ searchbox.doCommand();
+ view = yield fetched;
+
+ assertAttrs(view, "foo",
+ "If we reset again, we get back to original state again");
+});
diff --git a/devtools/client/webconsole/test/browser_console_variables_view_highlighter.js b/devtools/client/webconsole/test/browser_console_variables_view_highlighter.js
new file mode 100644
index 000000000..c1b2194de
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_variables_view_highlighter.js
@@ -0,0 +1,97 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Check that variables view is linked to the inspector for highlighting and
+// selecting DOM nodes
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-bug-952277-highlight-nodes-in-vview.html";
+
+var gWebConsole, gJSTerm, gVariablesView, gToolbox;
+
+function test() {
+ loadTab(TEST_URI).then(() => {
+ openConsole().then(hud => {
+ consoleOpened(hud);
+ });
+ });
+}
+
+function consoleOpened(hud) {
+ gWebConsole = hud;
+ gJSTerm = hud.jsterm;
+ gToolbox = gDevTools.getToolbox(hud.target);
+ gJSTerm.execute("document.querySelectorAll('p')").then(onQSAexecuted);
+}
+
+function onQSAexecuted(msg) {
+ ok(msg, "output message found");
+ let anchor = msg.querySelector("a");
+ ok(anchor, "object link found");
+
+ gJSTerm.once("variablesview-fetched", onNodeListViewFetched);
+
+ executeSoon(() =>
+ EventUtils.synthesizeMouse(anchor, 2, 2, {}, gWebConsole.iframeWindow)
+ );
+}
+
+function onNodeListViewFetched(event, variable) {
+ gVariablesView = variable._variablesView;
+ ok(gVariablesView, "variables view object");
+
+ // Transform the vview into an array we can filter properties from
+ let props = [...variable].map(([id, prop]) => [id, prop]);
+
+ // These properties are the DOM nodes ones
+ props = props.filter(v => v[0].match(/[0-9]+/));
+
+ function hoverOverDomNodeVariableAndAssertHighlighter(index) {
+ if (props[index]) {
+ let prop = props[index][1];
+
+ gToolbox.once("node-highlight", () => {
+ ok(true, "The highlighter was shown on hover of the DOMNode");
+ gToolbox.highlighterUtils.unhighlight().then(() => {
+ clickOnDomNodeVariableAndAssertInspectorSelected(index);
+ });
+ });
+
+ // Rather than trying to emulate a mouseenter event, let's call the
+ // variable's highlightDomNode and see if it has the desired effect
+ prop.highlightDomNode();
+ } else {
+ finishUp();
+ }
+ }
+
+ function clickOnDomNodeVariableAndAssertInspectorSelected(index) {
+ let prop = props[index][1];
+
+ // Make sure the inspector is initialized so we can listen to its events
+ gToolbox.initInspector().then(() => {
+ // Rather than trying to click on the value here, let's just call the
+ // variable's openNodeInInspector function and see if it has the
+ // desired effect
+ prop.openNodeInInspector().then(() => {
+ is(gToolbox.currentToolId, "inspector",
+ "The toolbox switched over the inspector on DOMNode click");
+ gToolbox.selectTool("webconsole").then(() => {
+ hoverOverDomNodeVariableAndAssertHighlighter(index + 1);
+ });
+ });
+ });
+ }
+
+ hoverOverDomNodeVariableAndAssertHighlighter(0);
+}
+
+function finishUp() {
+ gWebConsole = gJSTerm = gVariablesView = gToolbox = null;
+
+ finishTest();
+}
diff --git a/devtools/client/webconsole/test/browser_console_variables_view_special_names.js b/devtools/client/webconsole/test/browser_console_variables_view_special_names.js
new file mode 100644
index 000000000..fa0dd4b92
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_variables_view_special_names.js
@@ -0,0 +1,38 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Check that variables view handles special names like "<return>"
+// properly for ordinary displays.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf8,<p>test for bug 1084430";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+
+ ok(hud, "web console opened");
+
+ hud.setFilterState("log", false);
+ registerCleanupFunction(() => hud.setFilterState("log", true));
+
+ hud.jsterm.execute("inspect({ '<return>': 47, '<exception>': 91 })");
+
+ let varView = yield hud.jsterm.once("variablesview-fetched");
+ ok(varView, "variables view object");
+
+ let props = yield findVariableViewProperties(varView, [
+ { name: "<return>", value: 47 },
+ { name: "<exception>", value: 91 },
+ ], { webconsole: hud });
+
+ for (let prop of props) {
+ ok(!prop.matchedProp._internalItem, prop.name + " is not marked internal");
+ let target = prop.matchedProp._target;
+ ok(!target.hasAttribute("pseudo-item"),
+ prop.name + " is not a pseudo-item");
+ }
+});
diff --git a/devtools/client/webconsole/test/browser_console_variables_view_while_debugging.js b/devtools/client/webconsole/test/browser_console_variables_view_while_debugging.js
new file mode 100644
index 000000000..e83a8d626
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_variables_view_while_debugging.js
@@ -0,0 +1,109 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Test that makes sure web console eval happens in the user-selected stackframe
+// from the js debugger, when changing the value of a property in the variables
+// view.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-eval-in-stackframe.html";
+
+// Force the old debugger UI since it's directly used (see Bug 1301705)
+Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false);
+registerCleanupFunction(function* () {
+ Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend");
+});
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+
+ let dbgPanel = yield openDebugger();
+ yield waitForFrameAdded();
+ yield openConsole();
+ yield testVariablesView(hud);
+});
+
+function* waitForFrameAdded() {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ let toolbox = gDevTools.getToolbox(target);
+ let thread = toolbox.threadClient;
+
+ info("Waiting for framesadded");
+ yield new Promise(resolve => {
+ thread.addOneTimeListener("framesadded", resolve);
+ ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ content.wrappedJSObject.firstCall();
+ });
+ });
+}
+
+function* testVariablesView(hud) {
+ let jsterm = hud.jsterm;
+ let msg = yield jsterm.execute("fooObj");
+ ok(msg, "output message found");
+ ok(msg.textContent.includes('{ testProp2: "testValue2" }'),
+ "message text check");
+
+ let anchor = msg.querySelector("a");
+ ok(anchor, "object link found");
+
+ info("Waiting for variable view to appear");
+ let variable = yield new Promise(resolve => {
+ jsterm.once("variablesview-fetched", (e, variable) => {
+ resolve(variable);
+ });
+ executeSoon(() => EventUtils.synthesizeMouse(anchor, 2, 2, {},
+ hud.iframeWindow));
+ });
+
+ info("Waiting for findVariableViewProperties");
+ let results = yield findVariableViewProperties(variable, [
+ { name: "testProp2", value: "testValue2" },
+ { name: "testProp", value: "testValue", dontMatch: true },
+ ], { webconsole: hud });
+
+ let prop = results[0].matchedProp;
+ ok(prop, "matched the |testProp2| property in the variables view");
+
+ // Check that property value updates work and that jsterm functions can be
+ // used.
+ variable = yield updateVariablesViewProperty({
+ property: prop,
+ field: "value",
+ string: "document.title + foo2 + $('p')",
+ webconsole: hud
+ });
+
+ info("onFooObjFetchAfterUpdate");
+ let expectedValue = yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ let para = content.wrappedJSObject.document.querySelector("p");
+ return content.document.title + "foo2SecondCall" + para;
+ });
+
+ results = yield findVariableViewProperties(variable, [
+ { name: "testProp2", value: expectedValue },
+ ], { webconsole: hud });
+
+ prop = results[0].matchedProp;
+ ok(prop, "matched the updated |testProp2| property value");
+
+ // Check that testProp2 was updated.
+ yield new Promise(resolve => {
+ executeSoon(() => {
+ jsterm.execute("fooObj.testProp2").then(resolve);
+ });
+ });
+
+ expectedValue = yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ let para = content.wrappedJSObject.document.querySelector("p");
+ return content.document.title + "foo2SecondCall" + para;
+ });
+
+ isnot(hud.outputNode.textContent.indexOf(expectedValue), -1,
+ "fooObj.testProp2 is correct");
+}
diff --git a/devtools/client/webconsole/test/browser_console_variables_view_while_debugging_and_inspecting.js b/devtools/client/webconsole/test/browser_console_variables_view_while_debugging_and_inspecting.js
new file mode 100644
index 000000000..556e7275d
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_variables_view_while_debugging_and_inspecting.js
@@ -0,0 +1,112 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Test that makes sure web console eval works while the js debugger paused the
+// page, and while the inspector is active. See bug 886137.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-eval-in-stackframe.html";
+
+// Force the old debugger UI since it's directly used (see Bug 1301705)
+Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false);
+registerCleanupFunction(function* () {
+ Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend");
+});
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+
+ let dbgPanel = yield openDebugger();
+ yield openInspector();
+ yield waitForFrameAdded();
+
+ yield openConsole();
+ yield testVariablesView(hud);
+});
+
+function* waitForFrameAdded() {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ let toolbox = gDevTools.getToolbox(target);
+ let thread = toolbox.threadClient;
+
+ info("Waiting for framesadded");
+ yield new Promise(resolve => {
+ thread.addOneTimeListener("framesadded", resolve);
+ ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ content.wrappedJSObject.firstCall();
+ });
+ });
+}
+
+function* testVariablesView(hud) {
+ info("testVariablesView");
+ let jsterm = hud.jsterm;
+
+ let msg = yield jsterm.execute("fooObj");
+ ok(msg, "output message found");
+ ok(msg.textContent.includes('{ testProp2: "testValue2" }'),
+ "message text check");
+
+ let anchor = msg.querySelector("a");
+ ok(anchor, "object link found");
+
+ info("Waiting for variable view to appear");
+ let variable = yield new Promise(resolve => {
+ jsterm.once("variablesview-fetched", (e, variable) => {
+ resolve(variable);
+ });
+ executeSoon(() => EventUtils.synthesizeMouse(anchor, 2, 2, {},
+ hud.iframeWindow));
+ });
+
+ info("Waiting for findVariableViewProperties");
+ let results = yield findVariableViewProperties(variable, [
+ { name: "testProp2", value: "testValue2" },
+ { name: "testProp", value: "testValue", dontMatch: true },
+ ], { webconsole: hud });
+
+ let prop = results[0].matchedProp;
+ ok(prop, "matched the |testProp2| property in the variables view");
+
+ // Check that property value updates work and that jsterm functions can be
+ // used.
+ variable = yield updateVariablesViewProperty({
+ property: prop,
+ field: "value",
+ string: "document.title + foo2 + $('p')",
+ webconsole: hud
+ });
+
+ info("onFooObjFetchAfterUpdate");
+ let expectedValue = yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ let para = content.wrappedJSObject.document.querySelector("p");
+ return content.document.title + "foo2SecondCall" + para;
+ });
+
+ results = yield findVariableViewProperties(variable, [
+ { name: "testProp2", value: expectedValue },
+ ], { webconsole: hud });
+
+ prop = results[0].matchedProp;
+ ok(prop, "matched the updated |testProp2| property value");
+
+ // Check that testProp2 was updated.
+ yield new Promise(resolve => {
+ executeSoon(() => {
+ jsterm.execute("fooObj.testProp2").then(resolve);
+ });
+ });
+
+ expectedValue = yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ let para = content.wrappedJSObject.document.querySelector("p");
+ return content.document.title + "foo2SecondCall" + para;
+ });
+
+ isnot(hud.outputNode.textContent.indexOf(expectedValue), -1,
+ "fooObj.testProp2 is correct");
+}
diff --git a/devtools/client/webconsole/test/browser_eval_in_debugger_stackframe.js b/devtools/client/webconsole/test/browser_eval_in_debugger_stackframe.js
new file mode 100644
index 000000000..bc923ff44
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_eval_in_debugger_stackframe.js
@@ -0,0 +1,157 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Test that makes sure web console eval happens in the user-selected stackframe
+// from the js debugger.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-eval-in-stackframe.html";
+
+var gWebConsole, gJSTerm, gDebuggerWin, gThread, gDebuggerController;
+var gStackframes;
+
+// Force the old debugger UI since it's directly used (see Bug 1301705)
+Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false);
+registerCleanupFunction(function* () {
+ Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend");
+});
+
+function test() {
+ loadTab(TEST_URI).then(() => {
+ openConsole().then(consoleOpened);
+ });
+}
+
+function consoleOpened(hud) {
+ gWebConsole = hud;
+ gJSTerm = hud.jsterm;
+ gJSTerm.execute("foo").then(onExecuteFoo);
+}
+
+function onExecuteFoo() {
+ isnot(gWebConsole.outputNode.textContent.indexOf("globalFooBug783499"), -1,
+ "|foo| value is correct");
+
+ gJSTerm.clearOutput();
+
+ // Test for Bug 690529 - Web Console and Scratchpad should evaluate
+ // expressions in the scope of the content window, not in a sandbox.
+ executeSoon(() => {
+ gJSTerm.execute("foo2 = 'newFoo'; window.foo2").then(onNewFoo2);
+ });
+}
+
+function onNewFoo2(msg) {
+ is(gWebConsole.outputNode.textContent.indexOf("undefined"), -1,
+ "|undefined| is not displayed after adding |foo2|");
+
+ ok(msg, "output result found");
+
+ isnot(msg.textContent.indexOf("newFoo"), -1,
+ "'newFoo' is displayed after adding |foo2|");
+
+ gJSTerm.clearOutput();
+
+ info("openDebugger");
+ executeSoon(() => openDebugger().then(debuggerOpened));
+}
+
+function debuggerOpened(aResult) {
+ gDebuggerWin = aResult.panelWin;
+ gDebuggerController = gDebuggerWin.DebuggerController;
+ gThread = gDebuggerController.activeThread;
+ gStackframes = gDebuggerController.StackFrames;
+
+ info("openConsole");
+ executeSoon(() =>
+ openConsole().then(() =>
+ gJSTerm.execute("foo + foo2").then(onExecuteFooAndFoo2)
+ )
+ );
+}
+
+function onExecuteFooAndFoo2() {
+ let expected = "globalFooBug783499newFoo";
+ isnot(gWebConsole.outputNode.textContent.indexOf(expected), -1,
+ "|foo + foo2| is displayed after starting the debugger");
+
+ executeSoon(() => {
+ gJSTerm.clearOutput();
+
+ info("openDebugger");
+ openDebugger().then(() => {
+ gThread.addOneTimeListener("framesadded", onFramesAdded);
+
+ info("firstCall()");
+ ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ content.wrappedJSObject.firstCall();
+ });
+ });
+ });
+}
+
+function onFramesAdded() {
+ info("onFramesAdded, openConsole() now");
+ executeSoon(() =>
+ openConsole().then(() =>
+ gJSTerm.execute("foo + foo2").then(onExecuteFooAndFoo2InSecondCall)
+ )
+ );
+}
+
+function onExecuteFooAndFoo2InSecondCall() {
+ let expected = "globalFooBug783499foo2SecondCall";
+ isnot(gWebConsole.outputNode.textContent.indexOf(expected), -1,
+ "|foo + foo2| from |secondCall()|");
+
+ function runOpenConsole() {
+ openConsole().then(() => {
+ gJSTerm.execute("foo + foo2 + foo3").then(onExecuteFoo23InFirstCall);
+ });
+ }
+
+ executeSoon(() => {
+ gJSTerm.clearOutput();
+
+ info("openDebugger and selectFrame(1)");
+
+ openDebugger().then(() => {
+ gStackframes.selectFrame(1);
+
+ info("openConsole");
+ executeSoon(() => runOpenConsole());
+ });
+ });
+}
+
+function onExecuteFoo23InFirstCall() {
+ let expected = "fooFirstCallnewFoofoo3FirstCall";
+ isnot(gWebConsole.outputNode.textContent.indexOf(expected), -1,
+ "|foo + foo2 + foo3| from |firstCall()|");
+
+ executeSoon(() =>
+ gJSTerm.execute("foo = 'abba'; foo3 = 'bug783499'; foo + foo3").then(
+ onExecuteFooAndFoo3ChangesInFirstCall));
+}
+
+var onExecuteFooAndFoo3ChangesInFirstCall = Task.async(function*() {
+ let expected = "abbabug783499";
+ isnot(gWebConsole.outputNode.textContent.indexOf(expected), -1,
+ "|foo + foo3| updated in |firstCall()|");
+
+ yield ContentTask.spawn(gBrowser.selectedBrowser, null, function*() {
+ is(content.wrappedJSObject.foo, "globalFooBug783499",
+ "|foo| in content window");
+ is(content.wrappedJSObject.foo2, "newFoo", "|foo2| in content window");
+ ok(!content.wrappedJSObject.foo3,
+ "|foo3| was not added to the content window");
+ });
+
+ gWebConsole = gJSTerm = gDebuggerWin = gThread = gDebuggerController =
+ gStackframes = null;
+ executeSoon(finishTest);
+});
diff --git a/devtools/client/webconsole/test/browser_eval_in_debugger_stackframe2.js b/devtools/client/webconsole/test/browser_eval_in_debugger_stackframe2.js
new file mode 100644
index 000000000..bc116d443
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_eval_in_debugger_stackframe2.js
@@ -0,0 +1,71 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Test to make sure that web console commands can fire while paused at a
+// breakpoint that was triggered from a JS call. Relies on asynchronous js
+// evaluation over the protocol - see Bug 1088861.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-eval-in-stackframe.html";
+
+// Force the old debugger UI since it's directly used (see Bug 1301705)
+Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false);
+registerCleanupFunction(function* () {
+ Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend");
+});
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ info("open the web console");
+ let hud = yield openConsole();
+ let {jsterm} = hud;
+
+ info("open the debugger");
+ let {panelWin} = yield openDebugger();
+ let {DebuggerController} = panelWin;
+ let {activeThread} = DebuggerController;
+
+ let firstCall = promise.defer();
+ let frameAdded = promise.defer();
+ executeSoon(() => {
+ info("Executing firstCall");
+ activeThread.addOneTimeListener("framesadded", () => {
+ executeSoon(frameAdded.resolve);
+ });
+ jsterm.execute("firstCall()").then(firstCall.resolve);
+ });
+
+ info("Waiting for a frame to be added");
+ yield frameAdded.promise;
+
+ info("Executing basic command while paused");
+ yield executeAndConfirm(jsterm, "1 + 2", "3");
+
+ info("Executing command using scoped variables while paused");
+ yield executeAndConfirm(jsterm, "foo + foo2",
+ '"globalFooBug783499foo2SecondCall"');
+
+ info("Resuming the thread");
+ activeThread.resume();
+
+ info("Checking the first command, which is the last to resolve since it " +
+ "paused");
+ let node = yield firstCall.promise;
+ is(node.querySelector(".message-body").textContent,
+ "undefined",
+ "firstCall() returned correct value");
+});
+
+function* executeAndConfirm(jsterm, input, output) {
+ info("Executing command `" + input + "`");
+
+ let node = yield jsterm.execute(input);
+
+ is(node.querySelector(".message-body").textContent, output,
+ "Expected result from call to " + input);
+}
diff --git a/devtools/client/webconsole/test/browser_jsterm_inspect.js b/devtools/client/webconsole/test/browser_jsterm_inspect.js
new file mode 100644
index 000000000..aa18cbff6
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_jsterm_inspect.js
@@ -0,0 +1,47 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Check that the inspect() jsterm helper function works.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf8,<p>hello bug 869981";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+ let jsterm = hud.jsterm;
+
+ /* Check that the window object is inspected */
+ jsterm.execute("testProp = 'testValue'");
+
+ let updated = jsterm.once("variablesview-updated");
+ jsterm.execute("inspect(window)");
+ let view = yield updated;
+ ok(view, "variables view object");
+
+ // The single variable view contains a scope with the variable name
+ // and unnamed subitem that contains the properties
+ let variable = view.getScopeAtIndex(0).get(undefined);
+ ok(variable, "variable object");
+
+ yield findVariableViewProperties(variable, [
+ { name: "testProp", value: "testValue" },
+ { name: "document", value: /HTMLDocument \u2192 data:/ },
+ ], { webconsole: hud });
+
+ /* Check that a primitive value can be inspected, too */
+ let updated2 = jsterm.once("variablesview-updated");
+ jsterm.execute("inspect(1)");
+ let view2 = yield updated2;
+ ok(view2, "variables view object");
+
+ // Check the label of the scope - it should contain the value
+ let scope = view.getScopeAtIndex(0);
+ ok(scope, "variable object");
+
+ is(scope.name, "1", "The value of the primitive var is correct");
+});
diff --git a/devtools/client/webconsole/test/browser_longstring_hang.js b/devtools/client/webconsole/test/browser_longstring_hang.js
new file mode 100644
index 000000000..036ad6e88
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_longstring_hang.js
@@ -0,0 +1,57 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Test that very long strings do not hang the browser.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-bug-859170-longstring-hang.html";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ info("wait for the initial long string");
+
+ let results = yield waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ name: "find 'foobar', no 'foobaz', in long string output",
+ text: "foobar",
+ noText: "foobaz",
+ category: CATEGORY_WEBDEV,
+ longString: true,
+ },
+ ],
+ });
+
+ let clickable = results[0].longStrings[0];
+ ok(clickable, "long string ellipsis is shown");
+ clickable.scrollIntoView(false);
+
+ EventUtils.synthesizeMouse(clickable, 2, 2, {}, hud.iframeWindow);
+
+ info("wait for long string expansion");
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ name: "find 'foobaz' after expand, but no 'boom!' at the end",
+ text: "foobaz",
+ noText: "boom!",
+ category: CATEGORY_WEBDEV,
+ longString: false,
+ },
+ {
+ text: "too long to be displayed",
+ longString: false,
+ },
+ ],
+ });
+});
diff --git a/devtools/client/webconsole/test/browser_netmonitor_shows_reqs_in_webconsole.js b/devtools/client/webconsole/test/browser_netmonitor_shows_reqs_in_webconsole.js
new file mode 100644
index 000000000..f6f057549
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_netmonitor_shows_reqs_in_webconsole.js
@@ -0,0 +1,74 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 TEST_URI = "data:text/html;charset=utf8,Test that the netmonitor " +
+ "displays requests that have been recorded in the " +
+ "web console, even if the netmonitor hadn't opened yet.";
+
+const TEST_FILE = "test-network-request.html";
+const TEST_PATH = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/" + TEST_FILE;
+
+const NET_PREF = "devtools.webconsole.filter.networkinfo";
+Services.prefs.setBoolPref(NET_PREF, true);
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref(NET_PREF);
+});
+
+add_task(function* () {
+ let { tab, browser } = yield loadTab(TEST_URI);
+
+ // Test that the request appears in the console.
+ let hud = yield openConsole();
+ info("Web console is open");
+
+ yield loadDocument(browser);
+ info("Document loaded.");
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ name: "network message",
+ text: TEST_FILE,
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_LOG
+ }
+ ]
+ });
+
+ // Test that the request appears in the network panel.
+ let target = TargetFactory.forTab(tab);
+ let toolbox = yield gDevTools.showToolbox(target, "netmonitor");
+ info("Network panel is open.");
+
+ testNetmonitor(toolbox);
+});
+
+function loadDocument(browser) {
+ let deferred = promise.defer();
+
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ deferred.resolve();
+ }, true);
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_PATH);
+
+ return deferred.promise;
+}
+
+function testNetmonitor(toolbox) {
+ let monitor = toolbox.getCurrentPanel();
+ let { RequestsMenu } = monitor.panelWin.NetMonitorView;
+ RequestsMenu.lazyUpdate = false;
+
+ is(RequestsMenu.itemCount, 1, "Network request appears in the network panel");
+
+ let item = RequestsMenu.getItemAtIndex(0);
+ is(item.attachment.method, "GET", "The attached method is correct.");
+ is(item.attachment.url, TEST_PATH, "The attached url is correct.");
+}
diff --git a/devtools/client/webconsole/test/browser_output_breaks_after_console_dir_uninspectable.js b/devtools/client/webconsole/test/browser_output_breaks_after_console_dir_uninspectable.js
new file mode 100644
index 000000000..38a5b5419
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_output_breaks_after_console_dir_uninspectable.js
@@ -0,0 +1,47 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Make sure that the Web Console output does not break after we try to call
+// console.dir() for objects that are not inspectable.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf8,test for bug 773466";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ hud.jsterm.clearOutput(true);
+
+ hud.jsterm.execute("console.log('fooBug773466a')");
+ hud.jsterm.execute("myObj = Object.create(null)");
+ hud.jsterm.execute("console.dir(myObj)");
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "fooBug773466a",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ },
+ {
+ name: "console.dir output",
+ consoleDir: "[object Object]",
+ }],
+ });
+
+ content.console.log("fooBug773466b");
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "fooBug773466b",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+});
diff --git a/devtools/client/webconsole/test/browser_output_longstring_expand.js b/devtools/client/webconsole/test/browser_output_longstring_expand.js
new file mode 100644
index 000000000..bae8ca128
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_output_longstring_expand.js
@@ -0,0 +1,85 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Test that long strings can be expanded in the console output.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf8,test for bug 787981 - check " +
+ "that long strings can be expanded in the output.";
+
+add_task(function* () {
+ let { DebuggerServer } = require("devtools/server/main");
+
+ let longString = (new Array(DebuggerServer.LONG_STRING_LENGTH + 4))
+ .join("a") + "foobar";
+ let initialString =
+ longString.substring(0, DebuggerServer.LONG_STRING_INITIAL_LENGTH);
+
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ hud.jsterm.clearOutput(true);
+ hud.jsterm.execute("console.log('bazbaz', '" + longString + "', 'boom')");
+
+ let [result] = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "console.log output",
+ text: ["bazbaz", "boom", initialString],
+ noText: "foobar",
+ longString: true,
+ }],
+ });
+
+ let clickable = result.longStrings[0];
+ ok(clickable, "long string ellipsis is shown");
+
+ clickable.scrollIntoView(false);
+
+ EventUtils.synthesizeMouse(clickable, 2, 2, {}, hud.iframeWindow);
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "full string",
+ text: ["bazbaz", "boom", longString],
+ category: CATEGORY_WEBDEV,
+ longString: false,
+ }],
+ });
+
+ hud.jsterm.clearOutput(true);
+ let msg = yield execute(hud, "'" + longString + "'");
+
+ isnot(msg.textContent.indexOf(initialString), -1,
+ "initial string is shown");
+ is(msg.textContent.indexOf(longString), -1,
+ "full string is not shown");
+
+ clickable = msg.querySelector(".longStringEllipsis");
+ ok(clickable, "long string ellipsis is shown");
+
+ clickable.scrollIntoView(false);
+
+ EventUtils.synthesizeMouse(clickable, 3, 4, {}, hud.iframeWindow);
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "full string",
+ text: longString,
+ category: CATEGORY_OUTPUT,
+ longString: false,
+ }],
+ });
+});
+
+function execute(hud, str) {
+ let deferred = promise.defer();
+ hud.jsterm.execute(str, deferred.resolve);
+ return deferred.promise;
+}
diff --git a/devtools/client/webconsole/test/browser_repeated_messages_accuracy.js b/devtools/client/webconsole/test/browser_repeated_messages_accuracy.js
new file mode 100644
index 000000000..36b13ce02
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_repeated_messages_accuracy.js
@@ -0,0 +1,178 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Test that makes sure messages are not considered repeated when coming from
+// different lines of code, or from different severities, etc.
+// See bugs 720180 and 800510.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-repeated-messages.html";
+const PREF = "devtools.webconsole.persistlog";
+
+add_task(function* () {
+ Services.prefs.setBoolPref(PREF, true);
+
+ let { browser } = yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ yield consoleOpened(hud);
+
+ let loaded = loadBrowser(browser);
+ BrowserReload();
+ yield loaded;
+
+ yield testCSSRepeats(hud);
+ yield testCSSRepeatsAfterReload(hud);
+ yield testConsoleRepeats(hud);
+ yield testConsoleFalsyValues(hud);
+
+ Services.prefs.clearUserPref(PREF);
+});
+
+function consoleOpened(hud) {
+ // Check that css warnings are not coalesced if they come from different
+ // lines.
+ info("waiting for 2 css warnings");
+
+ return waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "two css warnings",
+ category: CATEGORY_CSS,
+ count: 2,
+ repeats: 1,
+ }],
+ });
+}
+
+function testCSSRepeats(hud) {
+ info("wait for repeats after page reload");
+
+ return waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "two css warnings, repeated twice",
+ category: CATEGORY_CSS,
+ repeats: 2,
+ count: 2,
+ }],
+ });
+}
+
+function testCSSRepeatsAfterReload(hud) {
+ hud.jsterm.clearOutput(true);
+ hud.jsterm.execute("testConsole()");
+
+ info("wait for repeats with the console API");
+
+ return waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ name: "console.log 'foo repeat' repeated twice",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ repeats: 2,
+ },
+ {
+ name: "console.log 'foo repeat' repeated once",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ repeats: 1,
+ },
+ {
+ name: "console.error 'foo repeat' repeated once",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_ERROR,
+ repeats: 1,
+ },
+ ],
+ });
+}
+
+function testConsoleRepeats(hud) {
+ hud.jsterm.clearOutput(true);
+ hud.jsterm.execute("undefined");
+
+ ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ content.console.log("undefined");
+ });
+
+ info("make sure console API messages are not coalesced with jsterm output");
+
+ return waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ name: "'undefined' jsterm input message",
+ text: "undefined",
+ category: CATEGORY_INPUT,
+ },
+ {
+ name: "'undefined' jsterm output message",
+ text: "undefined",
+ category: CATEGORY_OUTPUT,
+ },
+ {
+ name: "'undefined' console.log message",
+ text: "undefined",
+ category: CATEGORY_WEBDEV,
+ repeats: 1,
+ },
+ ],
+ });
+}
+
+function testConsoleFalsyValues(hud) {
+ hud.jsterm.clearOutput(true);
+ hud.jsterm.execute("testConsoleFalsyValues()");
+
+ info("wait for repeats of falsy values with the console API");
+
+ return waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ name: "console.log 'NaN' repeated once",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ repeats: 1,
+ },
+ {
+ name: "console.log 'undefined' repeated once",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ repeats: 1,
+ },
+ {
+ name: "console.log 'null' repeated once",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ repeats: 1,
+ },
+ {
+ name: "console.log 'NaN' repeated twice",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ repeats: 2,
+ },
+ {
+ name: "console.log 'undefined' repeated twice",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ repeats: 2,
+ },
+ {
+ name: "console.log 'null' repeated twice",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ repeats: 2,
+ },
+ ],
+ });
+}
diff --git a/devtools/client/webconsole/test/browser_result_format_as_string.js b/devtools/client/webconsole/test/browser_result_format_as_string.js
new file mode 100644
index 000000000..0352d0afa
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_result_format_as_string.js
@@ -0,0 +1,40 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Make sure that JS eval result are properly formatted as strings.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-result-format-as-string.html";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ hud.jsterm.clearOutput(true);
+
+ let msg = yield execute(hud, "document.querySelector('p')");
+
+ is(hud.outputNode.textContent.indexOf("bug772506_content"), -1,
+ "no content element found");
+ ok(!hud.outputNode.querySelector("#foobar"), "no #foobar element found");
+
+ ok(msg, "eval output node found");
+ is(msg.textContent.indexOf("<div>"), -1,
+ "<div> string is not displayed");
+ isnot(msg.textContent.indexOf("<p>"), -1,
+ "<p> string is displayed");
+
+ EventUtils.synthesizeMouseAtCenter(msg, {type: "mousemove"});
+ ok(!gBrowser._bug772506, "no content variable");
+});
+
+function execute(hud, str) {
+ let deferred = promise.defer();
+ hud.jsterm.execute(str, deferred.resolve);
+ return deferred.promise;
+}
diff --git a/devtools/client/webconsole/test/browser_warn_user_about_replaced_api.js b/devtools/client/webconsole/test/browser_warn_user_about_replaced_api.js
new file mode 100644
index 000000000..0eeb6eaa3
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_warn_user_about_replaced_api.js
@@ -0,0 +1,86 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 TEST_REPLACED_API_URI = "http://example.com/browser/devtools/client/" +
+ "webconsole/test/test-console-replaced-api.html";
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/testscript.js";
+const PREF = "devtools.webconsole.persistlog";
+
+add_task(function* () {
+ Services.prefs.setBoolPref(PREF, true);
+
+ let { browser } = yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+
+ yield testWarningNotPresent(hud);
+
+ let loaded = loadBrowser(browser);
+ BrowserTestUtils.loadURI(browser, TEST_REPLACED_API_URI);
+ yield loaded;
+
+ let hud2 = yield openConsole();
+
+ yield testWarningPresent(hud2);
+
+ Services.prefs.clearUserPref(PREF);
+});
+
+function testWarningNotPresent(hud) {
+ let deferred = promise.defer();
+
+ is(hud.outputNode.textContent.indexOf("logging API"), -1,
+ "no warning displayed");
+
+ // Bug 862024: make sure the warning doesn't show after page reload.
+ info("reload " + TEST_URI);
+ executeSoon(function () {
+ let browser = gBrowser.selectedBrowser;
+ ContentTask.spawn(browser, null, "() => content.location.reload()");
+ });
+
+ waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "testscript.js",
+ category: CATEGORY_NETWORK,
+ }],
+ }).then(() => executeSoon(() => {
+ is(hud.outputNode.textContent.indexOf("logging API"), -1,
+ "no warning displayed");
+ closeConsole().then(deferred.resolve);
+ }));
+
+ return deferred.promise;
+}
+
+function testWarningPresent(hud) {
+ info("wait for the warning to show");
+ let deferred = promise.defer();
+
+ let warning = {
+ webconsole: hud,
+ messages: [{
+ text: /logging API .+ disabled by a script/,
+ category: CATEGORY_JS,
+ severity: SEVERITY_WARNING,
+ }],
+ };
+
+ waitForMessages(warning).then(() => {
+ hud.jsterm.clearOutput();
+
+ executeSoon(() => {
+ info("reload the test page and wait for the warning to show");
+ waitForMessages(warning).then(deferred.resolve);
+ let browser = gBrowser.selectedBrowser;
+ ContentTask.spawn(browser, null, "() => content.location.reload()");
+ });
+ });
+
+ return deferred.promise;
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_allow_mixedcontent_securityerrors.js b/devtools/client/webconsole/test/browser_webconsole_allow_mixedcontent_securityerrors.js
new file mode 100644
index 000000000..07f6372d0
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_allow_mixedcontent_securityerrors.js
@@ -0,0 +1,69 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// The test loads a web page with mixed active and display content
+// on it while the "block mixed content" settings are _off_.
+// It then checks that the loading mixed content warning messages
+// are logged to the console and have the correct "Learn More"
+// url appended to them.
+// Bug 875456 - Log mixed content messages from the Mixed Content
+// Blocker to the Security Pane in the Web Console
+
+"use strict";
+
+const TEST_URI = "https://example.com/browser/devtools/client/webconsole/" +
+ "test/test-mixedcontent-securityerrors.html";
+const LEARN_MORE_URI = "https://developer.mozilla.org/docs/Web/Security/" +
+ "Mixed_content" + DOCS_GA_PARAMS;
+
+add_task(function* () {
+ yield pushPrefEnv();
+
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ let results = yield waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ name: "Logged mixed active content",
+ text: "Loading mixed (insecure) active content " +
+ "\u201chttp://example.com/\u201d on a secure page",
+ category: CATEGORY_SECURITY,
+ severity: SEVERITY_WARNING,
+ objects: true,
+ },
+ {
+ name: "Logged mixed passive content - image",
+ text: "Loading mixed (insecure) display content " +
+ "\u201chttp://example.com/tests/image/test/mochitest/blue.png\u201d " +
+ "on a secure page",
+ category: CATEGORY_SECURITY,
+ severity: SEVERITY_WARNING,
+ objects: true,
+ },
+ ],
+ });
+
+ yield testClickOpenNewTab(hud, results);
+});
+
+function pushPrefEnv() {
+ let deferred = promise.defer();
+ let options = {"set":
+ [["security.mixed_content.block_active_content", false],
+ ["security.mixed_content.block_display_content", false]
+ ]};
+ SpecialPowers.pushPrefEnv(options, deferred.resolve);
+ return deferred.promise;
+}
+
+function testClickOpenNewTab(hud, results) {
+ let warningNode = results[0].clickableElements[0];
+ ok(warningNode, "link element");
+ ok(warningNode.classList.contains("learn-more-link"), "link class name");
+ return simulateMessageLinkClick(warningNode, LEARN_MORE_URI);
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_assert.js b/devtools/client/webconsole/test/browser_webconsole_assert.js
new file mode 100644
index 000000000..7fc9693f1
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_assert.js
@@ -0,0 +1,56 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Test that console.assert() works as expected (i.e. outputs only on falsy
+// asserts). See bug 760193.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console-assert.html";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+ yield consoleOpened(hud);
+});
+
+function consoleOpened(hud) {
+ hud.jsterm.execute("test()");
+
+ return waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "undefined",
+ category: CATEGORY_OUTPUT,
+ severity: SEVERITY_LOG,
+ },
+ {
+ text: "start",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ },
+ {
+ text: "false assert",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_ERROR,
+ },
+ {
+ text: "falsy assert",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_ERROR,
+ },
+ {
+ text: "end",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ }).then(() => {
+ let nodes = hud.outputNode.querySelectorAll(".message");
+ is(nodes.length, 6,
+ "only six messages are displayed, no output from the true assert");
+ });
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_autocomplete-properties-with-non-alphanumeric-names.js b/devtools/client/webconsole/test/browser_webconsole_autocomplete-properties-with-non-alphanumeric-names.js
new file mode 100644
index 000000000..0ba168078
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_autocomplete-properties-with-non-alphanumeric-names.js
@@ -0,0 +1,47 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 properties starting with underscores or dollars can be
+// autocompleted (bug 967468).
+
+add_task(function* () {
+ const TEST_URI = "data:text/html;charset=utf8,test autocompletion with " +
+ "$ or _";
+ yield loadTab(TEST_URI);
+
+ function* autocomplete(term) {
+ let deferred = promise.defer();
+
+ jsterm.setInputValue(term);
+ jsterm.complete(jsterm.COMPLETE_HINT_ONLY, deferred.resolve);
+
+ yield deferred.promise;
+
+ ok(popup.itemCount > 0,
+ "There's " + popup.itemCount + " suggestions for '" + term + "'");
+ }
+
+ let { jsterm } = yield openConsole();
+ let popup = jsterm.autocompletePopup;
+
+ yield jsterm.execute("var testObject = {$$aaab: '', $$aaac: ''}");
+
+ // Should work with bug 967468.
+ yield autocomplete("Object.__d");
+ yield autocomplete("testObject.$$a");
+
+ // Here's when things go wrong in bug 967468.
+ yield autocomplete("Object.__de");
+ yield autocomplete("testObject.$$aa");
+
+ // Should work with bug 1207868.
+ yield jsterm.execute("let foobar = {a: ''}; const blargh = {a: 1};");
+ yield autocomplete("foobar");
+ yield autocomplete("blargh");
+ yield autocomplete("foobar.a");
+ yield autocomplete("blargh.a");
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_autocomplete_accessibility.js b/devtools/client/webconsole/test/browser_webconsole_autocomplete_accessibility.js
new file mode 100644
index 000000000..bcd2e22d0
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_autocomplete_accessibility.js
@@ -0,0 +1,60 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests that the autocomplete input is being blurred and focused when selecting a value.
+// This will help screen-readers notify users of the value that was set in the input.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf8,<p>test code completion";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ let jsterm = hud.jsterm;
+ let input = jsterm.inputNode;
+
+ info("Type 'd' to open the autocomplete popup");
+ yield autocomplete(jsterm, "d");
+
+ // Add listeners for focus and blur events.
+ let wasBlurred = false;
+ input.addEventListener("blur", () => {
+ wasBlurred = true;
+ }, {
+ once: true
+ });
+
+ let wasFocused = false;
+ input.addEventListener("blur", () => {
+ ok(wasBlurred, "jsterm input received a blur event before received back the focus");
+ wasFocused = true;
+ }, {
+ once: true
+ });
+
+ info("Close the autocomplete popup by simulating a TAB key event");
+ let onPopupClosed = jsterm.autocompletePopup.once("popup-closed");
+ EventUtils.synthesizeKey("VK_TAB", {});
+
+ info("Wait for the autocomplete popup to be closed");
+ yield onPopupClosed;
+
+ ok(wasFocused, "jsterm input received a focus event");
+});
+
+function* autocomplete(jsterm, value) {
+ let popup = jsterm.autocompletePopup;
+
+ yield new Promise(resolve => {
+ jsterm.setInputValue(value);
+ jsterm.complete(jsterm.COMPLETE_HINT_ONLY, resolve);
+ });
+
+ ok(popup.isOpen && popup.itemCount > 0,
+ "Autocomplete popup is open and contains suggestions");
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_autocomplete_and_selfxss.js b/devtools/client/webconsole/test/browser_webconsole_autocomplete_and_selfxss.js
new file mode 100644
index 000000000..d0c6eb673
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_autocomplete_and_selfxss.js
@@ -0,0 +1,130 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 TEST_URI = "data:text/html;charset=utf-8,<p>test for bug 642615";
+
+XPCOMUtils.defineLazyServiceGetter(this, "clipboardHelper",
+ "@mozilla.org/widget/clipboardhelper;1",
+ "nsIClipboardHelper");
+var WebConsoleUtils = require("devtools/client/webconsole/utils").Utils;
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ yield consoleOpened(hud);
+});
+
+function consoleOpened(HUD) {
+ let deferred = promise.defer();
+
+ let jsterm = HUD.jsterm;
+ let stringToCopy = "foobazbarBug642615";
+
+ jsterm.clearOutput();
+
+ ok(!jsterm.completeNode.value, "no completeNode.value");
+
+ jsterm.setInputValue("doc");
+
+ let completionValue;
+
+ // wait for key "u"
+ function onCompletionValue() {
+ completionValue = jsterm.completeNode.value;
+
+ // Arguments: expected, setup, success, failure.
+ waitForClipboard(
+ stringToCopy,
+ function () {
+ clipboardHelper.copyString(stringToCopy);
+ },
+ onClipboardCopy,
+ finishTest);
+ }
+
+ function onClipboardCopy() {
+ testSelfXss();
+
+ jsterm.setInputValue("docu");
+ info("wait for completion update after clipboard paste");
+ updateEditUIVisibility();
+ jsterm.once("autocomplete-updated", onClipboardPaste);
+ goDoCommand("cmd_paste");
+ }
+
+ // Self xss prevention tests (bug 994134)
+ function testSelfXss() {
+ info("Self-xss paste tests");
+ WebConsoleUtils.usageCount = 0;
+ is(WebConsoleUtils.usageCount, 0, "Test for usage count getter");
+ // Input some commands to check if usage counting is working
+ for (let i = 0; i <= 3; i++) {
+ jsterm.setInputValue(i);
+ jsterm.execute();
+ }
+ is(WebConsoleUtils.usageCount, 4, "Usage count incremented");
+ WebConsoleUtils.usageCount = 0;
+ updateEditUIVisibility();
+
+ let oldVal = jsterm.getInputValue();
+ goDoCommand("cmd_paste");
+ let notificationbox = jsterm.hud.document.getElementById("webconsole-notificationbox");
+ let notification = notificationbox.getNotificationWithValue("selfxss-notification");
+ ok(notification, "Self-xss notification shown");
+ is(oldVal, jsterm.getInputValue(), "Paste blocked by self-xss prevention");
+
+ // Allow pasting
+ jsterm.setInputValue("allow pasting");
+ let evt = document.createEvent("KeyboardEvent");
+ evt.initKeyEvent("keyup", true, true, window,
+ 0, 0, 0, 0,
+ 0, " ".charCodeAt(0));
+ jsterm.inputNode.dispatchEvent(evt);
+ jsterm.setInputValue("");
+ goDoCommand("cmd_paste");
+ isnot("", jsterm.getInputValue(), "Paste works");
+ }
+ function onClipboardPaste() {
+ ok(!jsterm.completeNode.value, "no completion value after paste");
+
+ info("wait for completion update after undo");
+ jsterm.once("autocomplete-updated", onCompletionValueAfterUndo);
+
+ // Get out of the webconsole event loop.
+ executeSoon(() => {
+ goDoCommand("cmd_undo");
+ });
+ }
+
+ function onCompletionValueAfterUndo() {
+ is(jsterm.completeNode.value, completionValue,
+ "same completeNode.value after undo");
+
+ info("wait for completion update after clipboard paste (ctrl-v)");
+ jsterm.once("autocomplete-updated", () => {
+ ok(!jsterm.completeNode.value,
+ "no completion value after paste (ctrl-v)");
+
+ // using executeSoon() to get out of the webconsole event loop.
+ executeSoon(deferred.resolve);
+ });
+
+ // Get out of the webconsole event loop.
+ executeSoon(() => {
+ EventUtils.synthesizeKey("v", {accelKey: true});
+ });
+ }
+
+ info("wait for completion value after typing 'docu'");
+ jsterm.once("autocomplete-updated", onCompletionValue);
+
+ EventUtils.synthesizeKey("u", {});
+
+ return deferred.promise;
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_autocomplete_crossdomain_iframe.js b/devtools/client/webconsole/test/browser_webconsole_autocomplete_crossdomain_iframe.js
new file mode 100644
index 000000000..b4471bd6b
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_autocomplete_crossdomain_iframe.js
@@ -0,0 +1,64 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Test that autocomplete doesn't break when trying to reach into objects from
+// a different domain, bug 989025.
+
+"use strict";
+
+function test() {
+ let hud;
+
+ const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-bug-989025-iframe-parent.html";
+
+ Task.spawn(function* () {
+ const {tab} = yield loadTab(TEST_URI);
+ hud = yield openConsole(tab);
+
+ hud.jsterm.execute("document.title");
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "989025 - iframe parent",
+ category: CATEGORY_OUTPUT,
+ }],
+ });
+
+ let autocompleteUpdated = hud.jsterm.once("autocomplete-updated");
+
+ hud.jsterm.setInputValue("window[0].document");
+ executeSoon(() => {
+ EventUtils.synthesizeKey(".", {});
+ });
+
+ yield autocompleteUpdated;
+
+ hud.jsterm.setInputValue("window[0].document.title");
+ EventUtils.synthesizeKey("VK_RETURN", {});
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "Permission denied",
+ category: CATEGORY_OUTPUT,
+ severity: SEVERITY_ERROR,
+ }],
+ });
+
+ hud.jsterm.execute("window.location");
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "test-bug-989025-iframe-parent.html",
+ category: CATEGORY_OUTPUT,
+ }],
+ });
+
+ yield closeConsole(tab);
+ }).then(finishTest);
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_autocomplete_in_debugger_stackframe.js b/devtools/client/webconsole/test/browser_webconsole_autocomplete_in_debugger_stackframe.js
new file mode 100644
index 000000000..60ba5ff0e
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_autocomplete_in_debugger_stackframe.js
@@ -0,0 +1,245 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Test that makes sure web console autocomplete happens in the user-selected
+// stackframe from the js debugger.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-autocomplete-in-stackframe.html";
+
+// Force the old debugger UI since it's directly used (see Bug 1301705)
+Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false);
+registerCleanupFunction(function* () {
+ Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend");
+});
+
+var gStackframes;
+registerCleanupFunction(function () {
+ gStackframes = null;
+});
+
+requestLongerTimeout(2);
+add_task(function* () {
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+ yield testCompletion(hud);
+});
+
+function* testCompletion(hud) {
+ let jsterm = hud.jsterm;
+ let input = jsterm.inputNode;
+ let popup = jsterm.autocompletePopup;
+
+ // Test that document.title gives string methods. Native getters must execute.
+ input.value = "document.title.";
+ input.setSelectionRange(input.value.length, input.value.length);
+ yield new Promise(resolve => {
+ jsterm.complete(jsterm.COMPLETE_HINT_ONLY, resolve);
+ });
+
+ let newItems = popup.getItems();
+ ok(newItems.length > 0, "'document.title.' gave a list of suggestions");
+ ok(newItems.some(function (item) {
+ return item.label == "substr";
+ }), "autocomplete results do contain substr");
+ ok(newItems.some(function (item) {
+ return item.label == "toLowerCase";
+ }), "autocomplete results do contain toLowerCase");
+ ok(newItems.some(function (item) {
+ return item.label == "strike";
+ }), "autocomplete results do contain strike");
+
+ // Test if 'f' gives 'foo1' but not 'foo2' or 'foo3'
+ input.value = "f";
+ input.setSelectionRange(1, 1);
+ yield new Promise(resolve => {
+ jsterm.complete(jsterm.COMPLETE_HINT_ONLY, resolve);
+ });
+
+ newItems = popup.getItems();
+ ok(newItems.length > 0, "'f' gave a list of suggestions");
+ ok(!newItems.every(function (item) {
+ return item.label != "foo1";
+ }), "autocomplete results do contain foo1");
+ ok(!newItems.every(function (item) {
+ return item.label != "foo1Obj";
+ }), "autocomplete results do contain foo1Obj");
+ ok(newItems.every(function (item) {
+ return item.label != "foo2";
+ }), "autocomplete results do not contain foo2");
+ ok(newItems.every(function (item) {
+ return item.label != "foo2Obj";
+ }), "autocomplete results do not contain foo2Obj");
+ ok(newItems.every(function (item) {
+ return item.label != "foo3";
+ }), "autocomplete results do not contain foo3");
+ ok(newItems.every(function (item) {
+ return item.label != "foo3Obj";
+ }), "autocomplete results do not contain foo3Obj");
+
+ // Test if 'foo1Obj.' gives 'prop1' and 'prop2'
+ input.value = "foo1Obj.";
+ input.setSelectionRange(8, 8);
+ yield new Promise(resolve => {
+ jsterm.complete(jsterm.COMPLETE_HINT_ONLY, resolve);
+ });
+
+ newItems = popup.getItems();
+ ok(!newItems.every(function (item) {
+ return item.label != "prop1";
+ }), "autocomplete results do contain prop1");
+ ok(!newItems.every(function (item) {
+ return item.label != "prop2";
+ }), "autocomplete results do contain prop2");
+
+ // Test if 'foo1Obj.prop2.' gives 'prop21'
+ input.value = "foo1Obj.prop2.";
+ input.setSelectionRange(14, 14);
+ yield new Promise(resolve => {
+ jsterm.complete(jsterm.COMPLETE_HINT_ONLY, resolve);
+ });
+
+ newItems = popup.getItems();
+ ok(!newItems.every(function (item) {
+ return item.label != "prop21";
+ }), "autocomplete results do contain prop21");
+
+ info("Opening Debugger");
+ let dbg = yield openDebugger();
+
+ info("Waiting for pause");
+ yield pauseDebugger(dbg);
+
+ info("Opening Console again");
+ yield openConsole();
+
+ // From this point on the
+ // Test if 'f' gives 'foo3' and 'foo1' but not 'foo2'
+ input.value = "f";
+ input.setSelectionRange(1, 1);
+ yield new Promise(resolve => {
+ jsterm.complete(jsterm.COMPLETE_HINT_ONLY, resolve);
+ });
+
+ newItems = popup.getItems();
+ ok(newItems.length > 0, "'f' gave a list of suggestions");
+ ok(!newItems.every(function (item) {
+ return item.label != "foo3";
+ }), "autocomplete results do contain foo3");
+ ok(!newItems.every(function (item) {
+ return item.label != "foo3Obj";
+ }), "autocomplete results do contain foo3Obj");
+ ok(!newItems.every(function (item) {
+ return item.label != "foo1";
+ }), "autocomplete results do contain foo1");
+ ok(!newItems.every(function (item) {
+ return item.label != "foo1Obj";
+ }), "autocomplete results do contain foo1Obj");
+ ok(newItems.every(function (item) {
+ return item.label != "foo2";
+ }), "autocomplete results do not contain foo2");
+ ok(newItems.every(function (item) {
+ return item.label != "foo2Obj";
+ }), "autocomplete results do not contain foo2Obj");
+
+ yield openDebugger();
+
+ gStackframes.selectFrame(1);
+
+ info("openConsole");
+ yield openConsole();
+
+ // Test if 'f' gives 'foo2' and 'foo1' but not 'foo3'
+ input.value = "f";
+ input.setSelectionRange(1, 1);
+ yield new Promise(resolve => {
+ jsterm.complete(jsterm.COMPLETE_HINT_ONLY, resolve);
+ });
+
+ newItems = popup.getItems();
+ ok(newItems.length > 0, "'f' gave a list of suggestions");
+ ok(!newItems.every(function (item) {
+ return item.label != "foo2";
+ }), "autocomplete results do contain foo2");
+ ok(!newItems.every(function (item) {
+ return item.label != "foo2Obj";
+ }), "autocomplete results do contain foo2Obj");
+ ok(!newItems.every(function (item) {
+ return item.label != "foo1";
+ }), "autocomplete results do contain foo1");
+ ok(!newItems.every(function (item) {
+ return item.label != "foo1Obj";
+ }), "autocomplete results do contain foo1Obj");
+ ok(newItems.every(function (item) {
+ return item.label != "foo3";
+ }), "autocomplete results do not contain foo3");
+ ok(newItems.every(function (item) {
+ return item.label != "foo3Obj";
+ }), "autocomplete results do not contain foo3Obj");
+
+ // Test if 'foo2Obj.' gives 'prop1'
+ input.value = "foo2Obj.";
+ input.setSelectionRange(8, 8);
+ yield new Promise(resolve => {
+ jsterm.complete(jsterm.COMPLETE_HINT_ONLY, resolve);
+ });
+
+ newItems = popup.getItems();
+ ok(!newItems.every(function (item) {
+ return item.label != "prop1";
+ }), "autocomplete results do contain prop1");
+
+ // Test if 'foo2Obj.prop1.' gives 'prop11'
+ input.value = "foo2Obj.prop1.";
+ input.setSelectionRange(14, 14);
+ yield new Promise(resolve => {
+ jsterm.complete(jsterm.COMPLETE_HINT_ONLY, resolve);
+ });
+
+ newItems = popup.getItems();
+ ok(!newItems.every(function (item) {
+ return item.label != "prop11";
+ }), "autocomplete results do contain prop11");
+
+ // Test if 'foo2Obj.prop1.prop11.' gives suggestions for a string
+ // i.e. 'length'
+ input.value = "foo2Obj.prop1.prop11.";
+ input.setSelectionRange(21, 21);
+ yield new Promise(resolve => {
+ jsterm.complete(jsterm.COMPLETE_HINT_ONLY, resolve);
+ });
+
+ newItems = popup.getItems();
+ ok(!newItems.every(function (item) {
+ return item.label != "length";
+ }), "autocomplete results do contain length");
+
+ // Test if 'foo1Obj[0].' throws no errors.
+ input.value = "foo2Obj[0].";
+ input.setSelectionRange(11, 11);
+ yield new Promise(resolve => {
+ jsterm.complete(jsterm.COMPLETE_HINT_ONLY, resolve);
+ });
+
+ newItems = popup.getItems();
+ is(newItems.length, 0, "no items for foo2Obj[0]");
+}
+
+function pauseDebugger(aResult) {
+ let debuggerWin = aResult.panelWin;
+ let debuggerController = debuggerWin.DebuggerController;
+ let thread = debuggerController.activeThread;
+ gStackframes = debuggerController.StackFrames;
+ return new Promise(resolve => {
+ thread.addOneTimeListener("framesadded", resolve);
+
+ info("firstCall()");
+ ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ content.wrappedJSObject.firstCall();
+ });
+ });
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_autocomplete_popup_close_on_tab_switch.js b/devtools/client/webconsole/test/browser_webconsole_autocomplete_popup_close_on_tab_switch.js
new file mode 100644
index 000000000..afa3dd55d
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_autocomplete_popup_close_on_tab_switch.js
@@ -0,0 +1,27 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Test that the autocomplete popup closes on switching tabs. See bug 900448.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8,<p>bug 900448 - autocomplete " +
+ "popup closes on tab switch";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+ let popup = hud.jsterm.autocompletePopup;
+ let popupShown = once(popup, "popup-opened");
+
+ hud.jsterm.setInputValue("sc");
+ EventUtils.synthesizeKey("r", {});
+
+ yield popupShown;
+
+ yield loadTab("data:text/html;charset=utf-8,<p>testing autocomplete closes");
+
+ ok(!popup.isOpen, "Popup closes on tab switch");
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_block_mixedcontent_securityerrors.js b/devtools/client/webconsole/test/browser_webconsole_block_mixedcontent_securityerrors.js
new file mode 100644
index 000000000..ff4157a3b
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_block_mixedcontent_securityerrors.js
@@ -0,0 +1,110 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// The test loads a web page with mixed active and display content
+// on it while the "block mixed content" settings are _on_.
+// It then checks that the blocked mixed content warning messages
+// are logged to the console and have the correct "Learn More"
+// url appended to them. After the first test finishes, it invokes
+// a second test that overrides the mixed content blocker settings
+// by clicking on the doorhanger shield and validates that the
+// appropriate messages are logged to console.
+// Bug 875456 - Log mixed content messages from the Mixed Content
+// Blocker to the Security Pane in the Web Console
+
+"use strict";
+
+const TEST_URI = "https://example.com/browser/devtools/client/webconsole/" +
+ "test/test-mixedcontent-securityerrors.html";
+const LEARN_MORE_URI = "https://developer.mozilla.org/docs/Web/Security/" +
+ "Mixed_content" + DOCS_GA_PARAMS;
+
+add_task(function* () {
+ yield pushPrefEnv();
+
+ let { browser } = yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ let results = yield waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ name: "Logged blocking mixed active content",
+ text: "Blocked loading mixed active content \u201chttp://example.com/\u201d",
+ category: CATEGORY_SECURITY,
+ severity: SEVERITY_ERROR,
+ objects: true,
+ },
+ {
+ name: "Logged blocking mixed passive content - image",
+ text: "Blocked loading mixed active content \u201chttp://example.com/\u201d",
+ category: CATEGORY_SECURITY,
+ severity: SEVERITY_ERROR,
+ objects: true,
+ },
+ ],
+ });
+
+ yield testClickOpenNewTab(hud, results[0]);
+
+ let results2 = yield mixedContentOverrideTest2(hud, browser);
+
+ yield testClickOpenNewTab(hud, results2[0]);
+});
+
+function pushPrefEnv() {
+ let deferred = promise.defer();
+ let options = {
+ "set": [
+ ["security.mixed_content.block_active_content", true],
+ ["security.mixed_content.block_display_content", true],
+ ["security.mixed_content.use_hsts", false],
+ ["security.mixed_content.send_hsts_priming", false],
+ ]
+ };
+ SpecialPowers.pushPrefEnv(options, deferred.resolve);
+ return deferred.promise;
+}
+
+function mixedContentOverrideTest2(hud, browser) {
+ let deferred = promise.defer();
+ let {gIdentityHandler} = browser.ownerGlobal;
+ ok(gIdentityHandler._identityBox.classList.contains("mixedActiveBlocked"),
+ "Mixed Active Content state appeared on identity box");
+ gIdentityHandler.disableMixedContentProtection();
+
+ waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ name: "Logged blocking mixed active content",
+ text: "Loading mixed (insecure) active content " +
+ "\u201chttp://example.com/\u201d on a secure page",
+ category: CATEGORY_SECURITY,
+ severity: SEVERITY_WARNING,
+ objects: true,
+ },
+ {
+ name: "Logged blocking mixed passive content - image",
+ text: "Loading mixed (insecure) display content" +
+ " \u201chttp://example.com/tests/image/test/mochitest/blue.png\u201d" +
+ " on a secure page",
+ category: CATEGORY_SECURITY,
+ severity: SEVERITY_WARNING,
+ objects: true,
+ },
+ ],
+ }).then(msgs => deferred.resolve(msgs), e => console.error(e));
+
+ return deferred.promise;
+}
+
+function testClickOpenNewTab(hud, match) {
+ let warningNode = match.clickableElements[0];
+ ok(warningNode, "link element");
+ ok(warningNode.classList.contains("learn-more-link"), "link class name");
+ return simulateMessageLinkClick(warningNode, LEARN_MORE_URI);
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_1006027_message_timestamps_incorrect.js b/devtools/client/webconsole/test/browser_webconsole_bug_1006027_message_timestamps_incorrect.js
new file mode 100644
index 000000000..ee141a72f
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_1006027_message_timestamps_incorrect.js
@@ -0,0 +1,45 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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";
+
+function test() {
+ Task.spawn(runner).then(finishTest);
+
+ function* runner() {
+ const {tab} = yield loadTab("data:text/html;charset=utf8,<title>Test for " +
+ "Bug 1006027");
+
+ const hud = yield openConsole(tab);
+
+ hud.jsterm.execute("console.log('bug1006027')");
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "console.log",
+ text: "bug1006027",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+
+ info("hud.outputNode.textContent:\n" + hud.outputNode.textContent);
+ let timestampNodes = hud.outputNode.querySelectorAll("span.timestamp");
+ let aTimestampMilliseconds = Array.prototype.map.call(timestampNodes,
+ function (value) {
+ // We are parsing timestamps as local time, relative to the begin of
+ // the epoch.
+ // This is not the correct value of the timestamp, but good enough for
+ // comparison.
+ return Date.parse("T" + String.trim(value.textContent));
+ });
+
+ let minTimestamp = Math.min.apply(null, aTimestampMilliseconds);
+ let maxTimestamp = Math.max.apply(null, aTimestampMilliseconds);
+ ok(Math.abs(maxTimestamp - minTimestamp) < 2000,
+ "console.log message timestamp spread < 2000ms confirmed");
+ }
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_1010953_cspro.js b/devtools/client/webconsole/test/browser_webconsole_bug_1010953_cspro.js
new file mode 100644
index 000000000..ace13f8d1
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_1010953_cspro.js
@@ -0,0 +1,55 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/* We are loading:
+a script that is allowed by the CSP header but not by the CSPRO header
+an image which is allowed by the CSPRO header but not by the CSP header.
+
+So we expect a warning (image has been blocked) and a report
+ (script should not load and was reported)
+
+The expected console messages in the constants CSP_VIOLATION_MSG and
+CSP_REPORT_MSG are confirmed to be found in the console messages.
+*/
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf8,Web Console CSP report only " +
+ "test (bug 1010953)";
+const TEST_VIOLATION = "http://example.com/browser/devtools/client/" +
+ "webconsole/test/test_bug_1010953_cspro.html";
+const CSP_VIOLATION_MSG = "Content Security Policy: The page\u2019s settings " +
+ "blocked the loading of a resource at " +
+ "http://some.example.com/test.png " +
+ "(\u201cimg-src http://example.com\u201d).";
+const CSP_REPORT_MSG = "Content Security Policy: The page\u2019s settings " +
+ "observed the loading of a resource at " +
+ "http://some.example.com/test_bug_1010953_cspro.js " +
+ "(\u201cscript-src http://example.com\u201d). A CSP report is " +
+ "being sent.";
+
+add_task(function* () {
+ let { browser } = yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ hud.jsterm.clearOutput();
+
+ let loaded = loadBrowser(browser);
+ BrowserTestUtils.loadURI(browser, TEST_VIOLATION);
+ yield loaded;
+
+ yield waitForSuccess({
+ name: "Confirmed that CSP and CSP-Report-Only log different messages to " +
+ "the console.",
+ validator: function () {
+ console.log(hud.outputNode.textContent);
+ let success = false;
+ success = hud.outputNode.textContent.indexOf(CSP_VIOLATION_MSG) > -1 &&
+ hud.outputNode.textContent.indexOf(CSP_REPORT_MSG) > -1;
+ return success;
+ }
+ });
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_1050691_click_function_to_source.js b/devtools/client/webconsole/test/browser_webconsole_bug_1050691_click_function_to_source.js
new file mode 100644
index 000000000..9b220b4a2
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_1050691_click_function_to_source.js
@@ -0,0 +1,60 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests that clicking on a function displays its source in the debugger.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-bug_1050691_click_function_to_source.html";
+
+// Force the old debugger UI since it's directly used (see Bug 1301705)
+Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false);
+registerCleanupFunction(function* () {
+ Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend");
+});
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+
+ // Open the Debugger panel.
+ let debuggerPanel = yield openDebugger();
+ // And right after come back to the Console panel.
+ yield openConsole();
+ yield testWithDebuggerOpen(hud, debuggerPanel);
+});
+
+function* testWithDebuggerOpen(hud, debuggerPanel) {
+ let clickable = yield printFunction(hud);
+ let panelWin = debuggerPanel.panelWin;
+ let onEditorLocationSet = panelWin.once(panelWin.EVENTS.EDITOR_LOCATION_SET);
+ synthesizeClick(clickable, hud);
+ yield onEditorLocationSet;
+ ok(isDebuggerCaretPos(debuggerPanel, 7),
+ "Clicking on a function should go to its source in the debugger view");
+}
+
+function synthesizeClick(clickable, hud) {
+ EventUtils.synthesizeMouse(clickable, 2, 2, {}, hud.iframeWindow);
+}
+
+var printFunction = Task.async(function* (hud) {
+ hud.jsterm.clearOutput();
+ ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ content.wrappedJSObject.foo();
+ });
+ let [result] = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+ let msg = [...result.matched][0];
+ let clickable = msg.querySelector("a");
+ ok(clickable, "clickable item for object should exist");
+ return clickable;
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_1247459_violation.js b/devtools/client/webconsole/test/browser_webconsole_bug_1247459_violation.js
new file mode 100644
index 000000000..26bac7f57
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_1247459_violation.js
@@ -0,0 +1,40 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests that the Web Console CSP messages for two META policies
+// are correctly displayed.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf8,Web Console CSP violation test";
+const TEST_VIOLATION = "https://example.com/browser/devtools/client/" +
+ "webconsole/test/test_bug_1247459_violation.html";
+const CSP_VIOLATION_MSG = "Content Security Policy: The page\u2019s settings " +
+ "blocked the loading of a resource at " +
+ "http://some.example.com/test.png (\u201cimg-src " +
+ "https://example.com\u201d).";
+
+add_task(function* () {
+ let { browser } = yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ hud.jsterm.clearOutput();
+
+ let loaded = loadBrowser(browser);
+ BrowserTestUtils.loadURI(browser, TEST_VIOLATION);
+ yield loaded;
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ name: "CSP policy URI warning displayed successfully",
+ text: CSP_VIOLATION_MSG,
+ repeats: 2
+ }
+ ]
+ });
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_578437_page_reload.js b/devtools/client/webconsole/test/browser_webconsole_bug_578437_page_reload.js
new file mode 100644
index 000000000..fb0182d8b
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_578437_page_reload.js
@@ -0,0 +1,41 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests that the console object still exists after a page reload.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console.html";
+
+var browser;
+
+function test() {
+ loadTab(TEST_URI).then(() => {
+ openConsole().then((tab) => {
+ browser = tab.browser;
+
+ browser.addEventListener("DOMContentLoaded", testPageReload, false);
+ content.location.reload();
+ });
+ });
+ browser.addEventListener("DOMContentLoaded", onLoad, false);
+}
+
+function testPageReload() {
+ browser.removeEventListener("DOMContentLoaded", testPageReload, false);
+
+ let console = browser.contentWindow.wrappedJSObject.console;
+
+ is(typeof console, "object", "window.console is an object, after page reload");
+ is(typeof console.log, "function", "console.log is a function");
+ is(typeof console.info, "function", "console.info is a function");
+ is(typeof console.warn, "function", "console.warn is a function");
+ is(typeof console.error, "function", "console.error is a function");
+ is(typeof console.exception, "function", "console.exception is a function");
+
+ browser = null;
+ finishTest();
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_579412_input_focus.js b/devtools/client/webconsole/test/browser_webconsole_bug_579412_input_focus.js
new file mode 100644
index 000000000..551dbd361
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_579412_input_focus.js
@@ -0,0 +1,20 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests that the input field is focused when the console is opened.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console.html";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+ hud.jsterm.clearOutput();
+
+ let inputNode = hud.jsterm.inputNode;
+ ok(inputNode.getAttribute("focused"), "input node is focused");
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_580001_closing_after_completion.js b/devtools/client/webconsole/test/browser_webconsole_bug_580001_closing_after_completion.js
new file mode 100644
index 000000000..4c5fbf9c8
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_580001_closing_after_completion.js
@@ -0,0 +1,47 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests to ensure that errors don't appear when the console is closed while a
+// completion is being performed.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console.html";
+
+add_task(function* () {
+ let { browser } = yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+ yield testClosingAfterCompletion(hud, browser);
+});
+
+function testClosingAfterCompletion(hud, browser) {
+ let deferred = promise.defer();
+
+ let errorWhileClosing = false;
+ function errorListener() {
+ errorWhileClosing = true;
+ }
+
+ browser.addEventListener("error", errorListener, false);
+
+ // Focus the jsterm and perform the keycombo to close the WebConsole.
+ hud.jsterm.focus();
+
+ gDevTools.once("toolbox-destroyed", function () {
+ browser.removeEventListener("error", errorListener, false);
+ is(errorWhileClosing, false, "no error while closing the WebConsole");
+ deferred.resolve();
+ });
+
+ if (Services.appinfo.OS == "Darwin") {
+ EventUtils.synthesizeKey("i", { accelKey: true, altKey: true });
+ } else {
+ EventUtils.synthesizeKey("i", { accelKey: true, shiftKey: true });
+ }
+
+ return deferred.promise;
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_580030_errors_after_page_reload.js b/devtools/client/webconsole/test/browser_webconsole_bug_580030_errors_after_page_reload.js
new file mode 100644
index 000000000..af00bf913
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_580030_errors_after_page_reload.js
@@ -0,0 +1,50 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests that errors still show up in the Web Console after a page reload.
+// See bug 580030: the error handler fails silently after page reload.
+// https://bugzilla.mozilla.org/show_bug.cgi?id=580030
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-error.html";
+
+function test() {
+ Task.spawn(function* () {
+ const {tab} = yield loadTab(TEST_URI);
+ const hud = yield openConsole(tab);
+ info("console opened");
+
+ executeSoon(() => {
+ hud.jsterm.clearOutput();
+ info("wait for reload");
+ content.location.reload();
+ });
+
+ yield hud.target.once("navigate");
+ info("target navigated");
+
+ let button = content.document.querySelector("button");
+ ok(button, "button found");
+
+ // On e10s, the exception is triggered in child process
+ // and is ignored by test harness
+ if (!Services.appinfo.browserTabsRemoteAutostart) {
+ expectUncaughtException();
+ }
+
+ EventUtils.sendMouseEvent({type: "click"}, button, content);
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "fooBazBaz is not defined",
+ category: CATEGORY_JS,
+ severity: SEVERITY_ERROR,
+ }],
+ });
+ }).then(finishTest);
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_580454_timestamp_l10n.js b/devtools/client/webconsole/test/browser_webconsole_bug_580454_timestamp_l10n.js
new file mode 100644
index 000000000..6cd03164b
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_580454_timestamp_l10n.js
@@ -0,0 +1,26 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests that appropriately-localized timestamps are printed.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console.html";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+ const TEST_TIMESTAMP = 12345678;
+ let date = new Date(TEST_TIMESTAMP);
+ let localizedString = WCUL10n.timestampString(TEST_TIMESTAMP);
+ isnot(localizedString.indexOf(date.getHours()), -1, "the localized " +
+ "timestamp contains the hours");
+ isnot(localizedString.indexOf(date.getMinutes()), -1, "the localized " +
+ "timestamp contains the minutes");
+ isnot(localizedString.indexOf(date.getSeconds()), -1, "the localized " +
+ "timestamp contains the seconds");
+ isnot(localizedString.indexOf(date.getMilliseconds()), -1, "the localized " +
+ "timestamp contains the milliseconds");
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_582201_duplicate_errors.js b/devtools/client/webconsole/test/browser_webconsole_bug_582201_duplicate_errors.js
new file mode 100644
index 000000000..5e7b141eb
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_582201_duplicate_errors.js
@@ -0,0 +1,49 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests that exceptions thrown by content don't show up twice in the Web
+// Console.
+
+"use strict";
+
+const INIT_URI = "data:text/html;charset=utf8,hello world";
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-duplicate-error.html";
+
+add_task(function* () {
+ yield loadTab(INIT_URI);
+
+ let hud = yield openConsole();
+
+ // On e10s, the exception is triggered in child process
+ // and is ignored by test harness
+ if (!Services.appinfo.browserTabsRemoteAutostart) {
+ expectUncaughtException();
+ }
+
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_URI);
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "fooDuplicateError1",
+ category: CATEGORY_JS,
+ severity: SEVERITY_ERROR,
+ },
+ {
+ text: "test-duplicate-error.html",
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_LOG,
+ }],
+ });
+
+ let text = hud.outputNode.textContent;
+ let error1pos = text.indexOf("fooDuplicateError1");
+ ok(error1pos > -1, "found fooDuplicateError1");
+ if (error1pos > -1) {
+ ok(text.indexOf("fooDuplicateError1", error1pos + 1) == -1,
+ "no duplicate for fooDuplicateError1");
+ }
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_583816_No_input_and_Tab_key_pressed.js b/devtools/client/webconsole/test/browser_webconsole_bug_583816_No_input_and_Tab_key_pressed.js
new file mode 100644
index 000000000..7dd271388
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_583816_No_input_and_Tab_key_pressed.js
@@ -0,0 +1,35 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/browser/test-console.html";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+ testCompletion(hud);
+});
+
+function testCompletion(hud) {
+ let jsterm = hud.jsterm;
+ let input = jsterm.inputNode;
+
+ jsterm.setInputValue("");
+ EventUtils.synthesizeKey("VK_TAB", {});
+ is(jsterm.completeNode.value, "<- no result", "<- no result - matched");
+ is(input.value, "", "inputnode is empty - matched");
+ is(input.getAttribute("focused"), "true", "input is still focused");
+
+ // Any thing which is not in property autocompleter
+ jsterm.setInputValue("window.Bug583816");
+ EventUtils.synthesizeKey("VK_TAB", {});
+ is(jsterm.completeNode.value, " <- no result",
+ "completenode content - matched");
+ is(input.value, "window.Bug583816", "inputnode content - matched");
+ is(input.getAttribute("focused"), "true", "input is still focused");
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_585237_line_limit.js b/devtools/client/webconsole/test/browser_webconsole_bug_585237_line_limit.js
new file mode 100644
index 000000000..974557ec0
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_585237_line_limit.js
@@ -0,0 +1,89 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests that the Web Console limits the number of lines displayed according to
+// the user's preferences.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf8,test for bug 585237";
+
+var outputNode;
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ outputNode = hud.outputNode;
+
+ hud.jsterm.clearOutput();
+
+ let prefBranch = Services.prefs.getBranch("devtools.hud.loglimit.");
+ prefBranch.setIntPref("console", 20);
+
+ for (let i = 0; i < 30; i++) {
+ yield ContentTask.spawn(gBrowser.selectedBrowser, i, function (i) {
+ // must change message to prevent repeats
+ content.console.log("foo #" + i);
+ });
+ }
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "foo #29",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+
+ is(countMessageNodes(), 20, "there are 20 message nodes in the output " +
+ "when the log limit is set to 20");
+
+ yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function () {
+ content.console.log("bar bug585237");
+ });
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "bar bug585237",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+
+ is(countMessageNodes(), 20, "there are still 20 message nodes in the " +
+ "output when adding one more");
+
+ prefBranch.setIntPref("console", 30);
+ for (let i = 0; i < 20; i++) {
+ yield ContentTask.spawn(gBrowser.selectedBrowser, i, function (i) {
+ // must change message to prevent repeats
+ content.console.log("boo #" + i);
+ });
+ }
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "boo #19",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+
+ is(countMessageNodes(), 30, "there are 30 message nodes in the output " +
+ "when the log limit is set to 30");
+
+ prefBranch.clearUserPref("console");
+
+ outputNode = null;
+});
+
+function countMessageNodes() {
+ return outputNode.querySelectorAll(".message").length;
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_585956_console_trace.js b/devtools/client/webconsole/test/browser_webconsole_bug_585956_console_trace.js
new file mode 100644
index 000000000..c38fd52c1
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_585956_console_trace.js
@@ -0,0 +1,70 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 TEST_URI = "http://example.com/browser/devtools/client/" +
+ "webconsole/test/test-bug-585956-console-trace.html";
+
+add_task(function* () {
+ let {tab} = yield loadTab("data:text/html;charset=utf8,<p>hello");
+ let hud = yield openConsole(tab);
+
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_URI);
+
+ let [result] = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "console.trace output",
+ consoleTrace: {
+ file: "test-bug-585956-console-trace.html",
+ fn: "window.foobar585956c",
+ },
+ }],
+ });
+
+ let node = [...result.matched][0];
+ ok(node, "found trace log node");
+
+ let obj = node._messageObject;
+ ok(obj, "console.trace message object");
+
+ // The expected stack trace object.
+ let stacktrace = [
+ {
+ columnNumber: 3,
+ filename: TEST_URI,
+ functionName: "window.foobar585956c",
+ language: 2,
+ lineNumber: 9
+ },
+ {
+ columnNumber: 10,
+ filename: TEST_URI,
+ functionName: "foobar585956b",
+ language: 2,
+ lineNumber: 14
+ },
+ {
+ columnNumber: 10,
+ filename: TEST_URI,
+ functionName: "foobar585956a",
+ language: 2,
+ lineNumber: 18
+ },
+ {
+ columnNumber: 1,
+ filename: TEST_URI,
+ functionName: "",
+ language: 2,
+ lineNumber: 21
+ }
+ ];
+
+ ok(obj._stacktrace, "found stacktrace object");
+ is(obj._stacktrace.toSource(), stacktrace.toSource(),
+ "stacktrace is correct");
+ isnot(node.textContent.indexOf("bug-585956"), -1, "found file name");
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_585991_autocomplete_keys.js b/devtools/client/webconsole/test/browser_webconsole_bug_585991_autocomplete_keys.js
new file mode 100644
index 000000000..0021a8cc1
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_585991_autocomplete_keys.js
@@ -0,0 +1,367 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 TEST_URI = "data:text/html;charset=utf-8,<p>bug 585991 - autocomplete " +
+ "popup keyboard usage test";
+
+// We should turn off auto-multiline editing during these tests
+const PREF_AUTO_MULTILINE = "devtools.webconsole.autoMultiline";
+var HUD, popup, jsterm, inputNode, completeNode;
+
+add_task(function* () {
+ Services.prefs.setBoolPref(PREF_AUTO_MULTILINE, false);
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+
+ yield consoleOpened(hud);
+ yield popupHideAfterTab();
+ yield testReturnKey();
+ yield dontShowArrayNumbers();
+ yield testReturnWithNoSelection();
+ yield popupHideAfterReturnWithNoSelection();
+ yield testCompletionInText();
+ yield popupHideAfterCompletionInText();
+
+ HUD = popup = jsterm = inputNode = completeNode = null;
+ Services.prefs.setBoolPref(PREF_AUTO_MULTILINE, true);
+});
+
+var consoleOpened = Task.async(function* (hud) {
+ let deferred = promise.defer();
+ HUD = hud;
+ info("web console opened");
+
+ jsterm = HUD.jsterm;
+
+ yield jsterm.execute("window.foobarBug585991={" +
+ "'item0': 'value0'," +
+ "'item1': 'value1'," +
+ "'item2': 'value2'," +
+ "'item3': 'value3'" +
+ "}");
+ yield jsterm.execute("window.testBug873250a = 'hello world';"
+ + "window.testBug873250b = 'hello world 2';");
+ popup = jsterm.autocompletePopup;
+ completeNode = jsterm.completeNode;
+ inputNode = jsterm.inputNode;
+
+ ok(!popup.isOpen, "popup is not open");
+
+ popup.once("popup-opened", () => {
+ ok(popup.isOpen, "popup is open");
+
+ // 4 values, and the following properties:
+ // __defineGetter__ __defineSetter__ __lookupGetter__ __lookupSetter__
+ // __proto__ hasOwnProperty isPrototypeOf propertyIsEnumerable
+ // toLocaleString toString toSource unwatch valueOf watch constructor.
+ is(popup.itemCount, 19, "popup.itemCount is correct");
+
+ let sameItems = popup.getItems().reverse().map(function (e) {
+ return e.label;
+ });
+
+ ok(sameItems.every(function (prop, index) {
+ return [
+ "__defineGetter__",
+ "__defineSetter__",
+ "__lookupGetter__",
+ "__lookupSetter__",
+ "__proto__",
+ "constructor",
+ "hasOwnProperty",
+ "isPrototypeOf",
+ "item0",
+ "item1",
+ "item2",
+ "item3",
+ "propertyIsEnumerable",
+ "toLocaleString",
+ "toSource",
+ "toString",
+ "unwatch",
+ "valueOf",
+ "watch",
+ ][index] === prop;
+ }), "getItems returns the items we expect");
+
+ is(popup.selectedIndex, 18,
+ "Index of the first item from bottom is selected.");
+ EventUtils.synthesizeKey("VK_DOWN", {});
+
+ let prefix = jsterm.getInputValue().replace(/[\S]/g, " ");
+
+ is(popup.selectedIndex, 0, "index 0 is selected");
+ is(popup.selectedItem.label, "watch", "watch is selected");
+ is(completeNode.value, prefix + "watch",
+ "completeNode.value holds watch");
+
+ EventUtils.synthesizeKey("VK_DOWN", {});
+
+ is(popup.selectedIndex, 1, "index 1 is selected");
+ is(popup.selectedItem.label, "valueOf", "valueOf is selected");
+ is(completeNode.value, prefix + "valueOf",
+ "completeNode.value holds valueOf");
+
+ EventUtils.synthesizeKey("VK_UP", {});
+
+ is(popup.selectedIndex, 0, "index 0 is selected");
+ is(popup.selectedItem.label, "watch", "watch is selected");
+ is(completeNode.value, prefix + "watch",
+ "completeNode.value holds watch");
+
+ let currentSelectionIndex = popup.selectedIndex;
+
+ EventUtils.synthesizeKey("VK_PAGE_DOWN", {});
+
+ ok(popup.selectedIndex > currentSelectionIndex,
+ "Index is greater after PGDN");
+
+ currentSelectionIndex = popup.selectedIndex;
+ EventUtils.synthesizeKey("VK_PAGE_UP", {});
+
+ ok(popup.selectedIndex < currentSelectionIndex,
+ "Index is less after Page UP");
+
+ EventUtils.synthesizeKey("VK_END", {});
+ is(popup.selectedIndex, 18, "index is last after End");
+
+ EventUtils.synthesizeKey("VK_HOME", {});
+ is(popup.selectedIndex, 0, "index is first after Home");
+
+ info("press Tab and wait for popup to hide");
+ popup.once("popup-closed", () => {
+ deferred.resolve();
+ });
+ EventUtils.synthesizeKey("VK_TAB", {});
+ });
+
+ jsterm.setInputValue("window.foobarBug585991");
+ EventUtils.synthesizeKey(".", {});
+
+ return deferred.promise;
+});
+
+function popupHideAfterTab() {
+ let deferred = promise.defer();
+
+ // At this point the completion suggestion should be accepted.
+ ok(!popup.isOpen, "popup is not open");
+
+ is(jsterm.getInputValue(), "window.foobarBug585991.watch",
+ "completion was successful after VK_TAB");
+
+ ok(!completeNode.value, "completeNode is empty");
+
+ popup.once("popup-opened", function onShown() {
+ ok(popup.isOpen, "popup is open");
+
+ is(popup.itemCount, 19, "popup.itemCount is correct");
+
+ is(popup.selectedIndex, 18, "First index from bottom is selected");
+ EventUtils.synthesizeKey("VK_DOWN", {});
+
+ let prefix = jsterm.getInputValue().replace(/[\S]/g, " ");
+
+ is(popup.selectedIndex, 0, "index 0 is selected");
+ is(popup.selectedItem.label, "watch", "watch is selected");
+ is(completeNode.value, prefix + "watch",
+ "completeNode.value holds watch");
+
+ popup.once("popup-closed", function onHidden() {
+ ok(!popup.isOpen, "popup is not open after VK_ESCAPE");
+
+ is(jsterm.getInputValue(), "window.foobarBug585991.",
+ "completion was cancelled");
+
+ ok(!completeNode.value, "completeNode is empty");
+
+ deferred.resolve();
+ }, false);
+
+ info("press Escape to close the popup");
+ executeSoon(function () {
+ EventUtils.synthesizeKey("VK_ESCAPE", {});
+ });
+ }, false);
+
+ info("wait for completion: window.foobarBug585991.");
+ executeSoon(function () {
+ jsterm.setInputValue("window.foobarBug585991");
+ EventUtils.synthesizeKey(".", {});
+ });
+
+ return deferred.promise;
+}
+
+function testReturnKey() {
+ let deferred = promise.defer();
+
+ popup.once("popup-opened", function onShown() {
+ ok(popup.isOpen, "popup is open");
+
+ is(popup.itemCount, 19, "popup.itemCount is correct");
+
+ is(popup.selectedIndex, 18, "First index from bottom is selected");
+ EventUtils.synthesizeKey("VK_DOWN", {});
+
+ let prefix = jsterm.getInputValue().replace(/[\S]/g, " ");
+
+ is(popup.selectedIndex, 0, "index 0 is selected");
+ is(popup.selectedItem.label, "watch", "watch is selected");
+ is(completeNode.value, prefix + "watch",
+ "completeNode.value holds watch");
+
+ EventUtils.synthesizeKey("VK_DOWN", {});
+
+ is(popup.selectedIndex, 1, "index 1 is selected");
+ is(popup.selectedItem.label, "valueOf", "valueOf is selected");
+ is(completeNode.value, prefix + "valueOf",
+ "completeNode.value holds valueOf");
+
+ popup.once("popup-closed", function onHidden() {
+ ok(!popup.isOpen, "popup is not open after VK_RETURN");
+
+ is(jsterm.getInputValue(), "window.foobarBug585991.valueOf",
+ "completion was successful after VK_RETURN");
+
+ ok(!completeNode.value, "completeNode is empty");
+
+ deferred.resolve();
+ }, false);
+
+ info("press Return to accept suggestion. wait for popup to hide");
+
+ executeSoon(() => EventUtils.synthesizeKey("VK_RETURN", {}));
+ }, false);
+
+ info("wait for completion suggestions: window.foobarBug585991.");
+
+ executeSoon(function () {
+ jsterm.setInputValue("window.foobarBug58599");
+ EventUtils.synthesizeKey("1", {});
+ EventUtils.synthesizeKey(".", {});
+ });
+
+ return deferred.promise;
+}
+
+function* dontShowArrayNumbers() {
+ let deferred = promise.defer();
+
+ info("dontShowArrayNumbers");
+ yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ content.wrappedJSObject.foobarBug585991 = ["Sherlock Holmes"];
+ });
+
+ jsterm = HUD.jsterm;
+ popup = jsterm.autocompletePopup;
+
+ popup.once("popup-opened", function onShown() {
+ let sameItems = popup.getItems().map(function (e) {
+ return e.label;
+ });
+ ok(!sameItems.some(function (prop) {
+ prop === "0";
+ }), "Completing on an array doesn't show numbers.");
+
+ popup.once("popup-closed", function popupHidden() {
+ deferred.resolve();
+ }, false);
+
+ info("wait for popup to hide");
+ executeSoon(() => EventUtils.synthesizeKey("VK_ESCAPE", {}));
+ }, false);
+
+ info("wait for popup to show");
+ executeSoon(() => {
+ jsterm.setInputValue("window.foobarBug585991");
+ EventUtils.synthesizeKey(".", {});
+ });
+
+ return deferred.promise;
+}
+
+function testReturnWithNoSelection() {
+ let deferred = promise.defer();
+
+ info("test pressing return with open popup, but no selection, see bug 873250");
+
+ popup.once("popup-opened", function onShown() {
+ ok(popup.isOpen, "popup is open");
+ is(popup.itemCount, 2, "popup.itemCount is correct");
+ isnot(popup.selectedIndex, -1, "popup.selectedIndex is correct");
+
+ info("press Return and wait for popup to hide");
+ popup.once("popup-closed", function popupHidden() {
+ deferred.resolve();
+ });
+ executeSoon(() => EventUtils.synthesizeKey("VK_RETURN", {}));
+ });
+
+ executeSoon(() => {
+ info("wait for popup to show");
+ jsterm.setInputValue("window.testBu");
+ EventUtils.synthesizeKey("g", {});
+ });
+
+ return deferred.promise;
+}
+
+function popupHideAfterReturnWithNoSelection() {
+ ok(!popup.isOpen, "popup is not open after VK_RETURN");
+
+ is(jsterm.getInputValue(), "", "inputNode is empty after VK_RETURN");
+ is(completeNode.value, "", "completeNode is empty");
+ is(jsterm.history[jsterm.history.length - 1], "window.testBug",
+ "jsterm history is correct");
+
+ return promise.resolve();
+}
+
+function testCompletionInText() {
+ info("test that completion works inside text, see bug 812618");
+
+ let deferred = promise.defer();
+
+ popup.once("popup-opened", function onShown() {
+ ok(popup.isOpen, "popup is open");
+ is(popup.itemCount, 2, "popup.itemCount is correct");
+
+ EventUtils.synthesizeKey("VK_DOWN", {});
+ is(popup.selectedIndex, 0, "popup.selectedIndex is correct");
+ ok(!completeNode.value, "completeNode.value is empty");
+
+ let items = popup.getItems().reverse().map(e => e.label);
+ let sameItems = items.every((prop, index) =>
+ ["testBug873250a", "testBug873250b"][index] === prop);
+ ok(sameItems, "getItems returns the items we expect");
+
+ info("press Tab and wait for popup to hide");
+ popup.once("popup-closed", function popupHidden() {
+ deferred.resolve();
+ });
+ EventUtils.synthesizeKey("VK_TAB", {});
+ });
+
+ jsterm.setInputValue("dump(window.testBu)");
+ inputNode.selectionStart = inputNode.selectionEnd = 18;
+ EventUtils.synthesizeKey("g", {});
+ return deferred.promise;
+}
+
+function popupHideAfterCompletionInText() {
+ // At this point the completion suggestion should be accepted.
+ ok(!popup.isOpen, "popup is not open");
+ is(jsterm.getInputValue(), "dump(window.testBug873250b)",
+ "completion was successful after VK_TAB");
+ is(inputNode.selectionStart, 26, "cursor location is correct");
+ is(inputNode.selectionStart, inputNode.selectionEnd,
+ "cursor location (confirmed)");
+ ok(!completeNode.value, "completeNode is empty");
+
+ return promise.resolve();
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_585991_autocomplete_popup.js b/devtools/client/webconsole/test/browser_webconsole_bug_585991_autocomplete_popup.js
new file mode 100644
index 000000000..df1a42edf
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_585991_autocomplete_popup.js
@@ -0,0 +1,123 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 TEST_URI = "data:text/html;charset=utf-8,<p>bug 585991 - autocomplete " +
+ "popup test";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+
+ yield consoleOpened(hud);
+});
+
+function consoleOpened(HUD) {
+ let deferred = promise.defer();
+
+ let items = [
+ {label: "item0", value: "value0"},
+ {label: "item1", value: "value1"},
+ {label: "item2", value: "value2"},
+ ];
+
+ let popup = HUD.jsterm.autocompletePopup;
+ let input = HUD.jsterm.inputNode;
+
+ ok(!popup.isOpen, "popup is not open");
+ ok(!input.hasAttribute("aria-activedescendant"), "no aria-activedescendant");
+
+ popup.once("popup-opened", () => {
+ ok(popup.isOpen, "popup is open");
+
+ is(popup.itemCount, 0, "no items");
+ ok(!input.hasAttribute("aria-activedescendant"), "no aria-activedescendant");
+
+ popup.setItems(items);
+
+ is(popup.itemCount, items.length, "items added");
+
+ let sameItems = popup.getItems();
+ is(sameItems.every(function (item, index) {
+ return item === items[index];
+ }), true, "getItems returns back the same items");
+
+ is(popup.selectedIndex, 2, "Index of the first item from bottom is selected.");
+ is(popup.selectedItem, items[2], "First item from bottom is selected");
+ checkActiveDescendant(popup, input);
+
+ popup.selectedIndex = 1;
+
+ is(popup.selectedIndex, 1, "index 1 is selected");
+ is(popup.selectedItem, items[1], "item1 is selected");
+ checkActiveDescendant(popup, input);
+
+ popup.selectedItem = items[2];
+
+ is(popup.selectedIndex, 2, "index 2 is selected");
+ is(popup.selectedItem, items[2], "item2 is selected");
+ checkActiveDescendant(popup, input);
+
+ is(popup.selectPreviousItem(), items[1], "selectPreviousItem() works");
+
+ is(popup.selectedIndex, 1, "index 1 is selected");
+ is(popup.selectedItem, items[1], "item1 is selected");
+ checkActiveDescendant(popup, input);
+
+ is(popup.selectNextItem(), items[2], "selectNextItem() works");
+
+ is(popup.selectedIndex, 2, "index 2 is selected");
+ is(popup.selectedItem, items[2], "item2 is selected");
+ checkActiveDescendant(popup, input);
+
+ ok(popup.selectNextItem(), "selectNextItem() works");
+
+ is(popup.selectedIndex, 0, "index 0 is selected");
+ is(popup.selectedItem, items[0], "item0 is selected");
+ checkActiveDescendant(popup, input);
+
+ items.push({label: "label3", value: "value3"});
+ popup.appendItem(items[3]);
+
+ is(popup.itemCount, items.length, "item3 appended");
+
+ popup.selectedIndex = 3;
+ is(popup.selectedItem, items[3], "item3 is selected");
+ checkActiveDescendant(popup, input);
+
+ popup.removeItem(items[2]);
+
+ is(popup.selectedIndex, 2, "index2 is selected");
+ is(popup.selectedItem, items[3], "item3 is still selected");
+ checkActiveDescendant(popup, input);
+ is(popup.itemCount, items.length - 1, "item2 removed");
+
+ popup.clearItems();
+ is(popup.itemCount, 0, "items cleared");
+ ok(!input.hasAttribute("aria-activedescendant"), "no aria-activedescendant");
+
+ popup.once("popup-closed", () => {
+ deferred.resolve();
+ });
+ popup.hidePopup();
+ });
+
+ popup.openPopup(input);
+
+ return deferred.promise;
+}
+
+function checkActiveDescendant(popup, input) {
+ let activeElement = input.ownerDocument.activeElement;
+ let descendantId = activeElement.getAttribute("aria-activedescendant");
+ let popupItem = popup._tooltip.panel.querySelector("#" + descendantId);
+ let cloneItem = input.ownerDocument.querySelector("#" + descendantId);
+
+ ok(popupItem, "Active descendant is found in the popup list");
+ ok(cloneItem, "Active descendant is found in the list clone");
+ is(popupItem.innerHTML, cloneItem.innerHTML,
+ "Cloned item has the same HTML as the original element");
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_586388_select_all.js b/devtools/client/webconsole/test/browser_webconsole_bug_586388_select_all.js
new file mode 100644
index 000000000..cda31191e
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_586388_select_all.js
@@ -0,0 +1,84 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 TEST_URI = "http://example.com/";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+ yield testSelectionWhenMovingBetweenBoxes(hud);
+ performTestsAfterOutput(hud);
+});
+
+var testSelectionWhenMovingBetweenBoxes = Task.async(function* (hud) {
+ let jsterm = hud.jsterm;
+
+ // Fill the console with some output.
+ jsterm.clearOutput();
+ yield jsterm.execute("1 + 2");
+ yield jsterm.execute("3 + 4");
+ yield jsterm.execute("5 + 6");
+
+ return waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "3",
+ category: CATEGORY_OUTPUT,
+ },
+ {
+ text: "7",
+ category: CATEGORY_OUTPUT,
+ },
+ {
+ text: "11",
+ category: CATEGORY_OUTPUT,
+ }],
+ });
+});
+
+function performTestsAfterOutput(hud) {
+ let outputNode = hud.outputNode;
+
+ ok(outputNode.childNodes.length >= 3, "the output node has children after " +
+ "executing some JavaScript");
+
+ // Test that the global Firefox "Select All" functionality (e.g. Edit >
+ // Select All) works properly in the Web Console.
+ let commandController = hud.ui._commandController;
+ ok(commandController != null, "the window has a command controller object");
+
+ commandController.selectAll();
+
+ let selectedCount = hud.ui.output.getSelectedMessages().length;
+ is(selectedCount, outputNode.childNodes.length,
+ "all console messages are selected after performing a regular browser " +
+ "select-all operation");
+
+ hud.iframeWindow.getSelection().removeAllRanges();
+
+ // Test the context menu "Select All" (which has a different code path) works
+ // properly as well.
+ let contextMenuId = hud.ui.outputWrapper.getAttribute("context");
+ let contextMenu = hud.ui.document.getElementById(contextMenuId);
+ ok(contextMenu != null, "the output node has a context menu");
+
+ let selectAllItem = contextMenu.querySelector("*[command='cmd_selectAll']");
+ ok(selectAllItem != null,
+ "the context menu on the output node has a \"Select All\" item");
+
+ outputNode.focus();
+
+ selectAllItem.doCommand();
+
+ selectedCount = hud.ui.output.getSelectedMessages().length;
+ is(selectedCount, outputNode.childNodes.length,
+ "all console messages are selected after performing a select-all " +
+ "operation from the context menu");
+
+ hud.iframeWindow.getSelection().removeAllRanges();
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_587617_output_copy.js b/devtools/client/webconsole/test/browser_webconsole_bug_587617_output_copy.js
new file mode 100644
index 000000000..208baf3d6
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_587617_output_copy.js
@@ -0,0 +1,106 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+/* globals goUpdateCommand goDoCommand */
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console.html";
+
+var HUD, outputNode;
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+ yield consoleOpened(hud);
+ yield testContextMenuCopy();
+
+ HUD = outputNode = null;
+});
+
+function consoleOpened(hud) {
+ HUD = hud;
+
+ let deferred = promise.defer();
+
+ // See bugs 574036, 586386 and 587617.
+ outputNode = HUD.outputNode;
+
+ HUD.jsterm.clearOutput();
+
+ let controller = top.document.commandDispatcher
+ .getControllerForCommand("cmd_copy");
+ is(controller.isCommandEnabled("cmd_copy"), false, "cmd_copy is disabled");
+
+ ContentTask.spawn(gBrowser.selectedBrowser, null,
+ "() => content.console.log('Hello world! bug587617')");
+
+ waitForMessages({
+ webconsole: HUD,
+ messages: [{
+ text: "bug587617",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ }).then(([result]) => {
+ let msg = [...result.matched][0];
+ HUD.ui.output.selectMessage(msg);
+
+ outputNode.focus();
+
+ goUpdateCommand("cmd_copy");
+ controller = top.document.commandDispatcher
+ .getControllerForCommand("cmd_copy");
+ is(controller.isCommandEnabled("cmd_copy"), true, "cmd_copy is enabled");
+
+ // Remove new lines and whitespace since getSelection() includes
+ // a new line between message and line number, but the clipboard doesn't
+ // @see bug 1119503
+ let selection = (HUD.iframeWindow.getSelection() + "")
+ .replace(/\r?\n|\r| /g, "");
+ isnot(selection.indexOf("bug587617"), -1,
+ "selection text includes 'bug587617'");
+
+ waitForClipboard((str) => {
+ // Strip out spaces for comparison ease
+ return selection.trim() == str.trim().replace(/ /g, "");
+ }, () => {
+ goDoCommand("cmd_copy");
+ }, deferred.resolve, deferred.resolve);
+ });
+ return deferred.promise;
+}
+
+// Test that the context menu "Copy" (which has a different code path) works
+// properly as well.
+function testContextMenuCopy() {
+ let deferred = promise.defer();
+
+ let contextMenuId = HUD.ui.outputWrapper.getAttribute("context");
+ let contextMenu = HUD.ui.document.getElementById(contextMenuId);
+ ok(contextMenu, "the output node has a context menu");
+
+ let copyItem = contextMenu.querySelector("*[command='cmd_copy']");
+ ok(copyItem, "the context menu on the output node has a \"Copy\" item");
+
+ // Remove new lines and whitespace since getSelection() includes
+ // a new line between message and line number, but the clipboard doesn't
+ // @see bug 1119503
+ let selection = (HUD.iframeWindow.getSelection() + "")
+ .replace(/\r?\n|\r| /g, "");
+
+ copyItem.doCommand();
+
+ waitForClipboard((str) => {
+ // Strip out spaces for comparison ease
+ return selection.trim() == str.trim().replace(/ /g, "");
+ }, () => {
+ goDoCommand("cmd_copy");
+ }, deferred.resolve, deferred.resolve);
+ HUD = outputNode = null;
+
+ return deferred.promise;
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_588342_document_focus.js b/devtools/client/webconsole/test/browser_webconsole_bug_588342_document_focus.js
new file mode 100644
index 000000000..ff926fc13
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_588342_document_focus.js
@@ -0,0 +1,36 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 TEST_URI = "data:text/html;charset=utf-8,Web Console test for bug 588342";
+
+add_task(function* () {
+ let { browser } = yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+
+ yield checkConsoleFocus(hud);
+
+ let isFocused = yield ContentTask.spawn(browser, { }, function* () {
+ var fm = Components.classes["@mozilla.org/focus-manager;1"].
+ getService(Components.interfaces.nsIFocusManager);
+ return fm.focusedWindow == content;
+ });
+
+ ok(isFocused, "content document has focus");
+});
+
+function* checkConsoleFocus(hud) {
+ let fm = Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager);
+
+ yield new Promise(resolve => {
+ waitForFocus(resolve);
+ });
+
+ is(hud.jsterm.inputNode.getAttribute("focused"), "true",
+ "jsterm input is focused on web console open");
+ is(fm.focusedWindow, hud.iframeWindow, "hud window is focused");
+ yield closeConsole(null);
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_588730_text_node_insertion.js b/devtools/client/webconsole/test/browser_webconsole_bug_588730_text_node_insertion.js
new file mode 100644
index 000000000..94a0ad77e
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_588730_text_node_insertion.js
@@ -0,0 +1,53 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests that adding text to one of the output labels doesn't cause errors.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console.html";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ yield testTextNodeInsertion(hud);
+});
+
+// Test for bug 588730: Adding a text node to an existing label element causes
+// warnings
+function testTextNodeInsertion(hud) {
+ let deferred = promise.defer();
+ let outputNode = hud.outputNode;
+
+ let label = document.createElementNS(
+ "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "label");
+ outputNode.appendChild(label);
+
+ let error = false;
+ let listener = {
+ observe: function (aMessage) {
+ let messageText = aMessage.message;
+ if (messageText.indexOf("JavaScript Warning") !== -1) {
+ error = true;
+ }
+ }
+ };
+
+ Services.console.registerListener(listener);
+
+ // This shouldn't fail.
+ label.appendChild(document.createTextNode("foo"));
+
+ executeSoon(function () {
+ Services.console.unregisterListener(listener);
+ ok(!error, "no error when adding text nodes as children of labels");
+
+ return deferred.resolve();
+ });
+ return deferred.promise;
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_588967_input_expansion.js b/devtools/client/webconsole/test/browser_webconsole_bug_588967_input_expansion.js
new file mode 100644
index 000000000..c590495c4
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_588967_input_expansion.js
@@ -0,0 +1,44 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console.html";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ testInputExpansion(hud);
+});
+
+function testInputExpansion(hud) {
+ let input = hud.jsterm.inputNode;
+
+ input.focus();
+
+ is(input.getAttribute("multiline"), "true", "multiline is enabled");
+
+ let ordinaryHeight = input.clientHeight;
+
+ // Tests if the inputNode expands.
+ input.value = "hello\nworld\n";
+ let length = input.value.length;
+ input.selectionEnd = length;
+ input.selectionStart = length;
+ // Performs an "d". This will trigger/test for the input event that should
+ // change the height of the inputNode.
+ EventUtils.synthesizeKey("d", {});
+ ok(input.clientHeight > ordinaryHeight, "the input expanded");
+
+ // Test if the inputNode shrinks again.
+ input.value = "";
+ EventUtils.synthesizeKey("d", {});
+ is(input.clientHeight, ordinaryHeight, "the input's height is normal again");
+
+ input = length = null;
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_589162_css_filter.js b/devtools/client/webconsole/test/browser_webconsole_bug_589162_css_filter.js
new file mode 100644
index 000000000..509c875f8
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_589162_css_filter.js
@@ -0,0 +1,39 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 TEST_URI = "data:text/html;charset=utf-8,<div style='font-size:3em;" +
+ "foobarCssParser:baz'>test CSS parser filter</div>";
+
+/**
+ * Unit test for bug 589162:
+ * CSS filtering on the console does not work
+ */
+add_task(function* () {
+ let {tab} = yield loadTab(TEST_URI);
+ let hud = yield openConsole(tab);
+
+ // CSS warnings are disabled by default.
+ hud.setFilterState("cssparser", true);
+ hud.jsterm.clearOutput();
+
+ BrowserReload();
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "foobarCssParser",
+ category: CATEGORY_CSS,
+ severity: SEVERITY_WARNING,
+ }],
+ });
+
+ hud.setFilterState("cssparser", false);
+
+ let msg = "the unknown CSS property warning is not displayed, " +
+ "after filtering";
+ testLogEntry(hud.outputNode, "foobarCssParser", msg, true, true);
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_592442_closing_brackets.js b/devtools/client/webconsole/test/browser_webconsole_bug_592442_closing_brackets.js
new file mode 100644
index 000000000..adbf13086
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_592442_closing_brackets.js
@@ -0,0 +1,29 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests that, when the user types an extraneous closing bracket, no error
+// appears.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8,test for bug 592442";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+ hud.jsterm.clearOutput();
+ let jsterm = hud.jsterm;
+
+ jsterm.setInputValue("document.getElementById)");
+
+ let error = false;
+ try {
+ jsterm.complete(jsterm.COMPLETE_HINT_ONLY);
+ } catch (ex) {
+ error = true;
+ }
+
+ ok(!error, "no error was thrown when an extraneous bracket was inserted");
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_593003_iframe_wrong_hud.js b/devtools/client/webconsole/test/browser_webconsole_bug_593003_iframe_wrong_hud.js
new file mode 100644
index 000000000..9f429a3d1
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_593003_iframe_wrong_hud.js
@@ -0,0 +1,68 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-bug-593003-iframe-wrong-hud.html";
+
+const TEST_IFRAME_URI = "http://example.com/browser/devtools/client/" +
+ "webconsole/test/test-bug-593003-iframe-wrong-" +
+ "hud-iframe.html";
+
+const TEST_DUMMY_URI = "http://example.com/browser/devtools/client/" +
+ "webconsole/test/test-console.html";
+
+add_task(function* () {
+
+ let tab1 = (yield loadTab(TEST_URI)).tab;
+ yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ content.console.log("FOO");
+ });
+ yield openConsole();
+
+ let tab2 = (yield loadTab(TEST_DUMMY_URI)).tab;
+ yield openConsole(gBrowser.selectedTab);
+
+ info("Reloading tab 1");
+ yield reloadTab(tab1);
+
+ info("Checking for messages");
+ yield checkMessages(tab1, tab2);
+
+ info("Cleaning up");
+ yield closeConsole(tab1);
+ yield closeConsole(tab2);
+});
+
+function* reloadTab(tab) {
+ let loaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ tab.linkedBrowser.reload();
+ yield loaded;
+}
+
+function* checkMessages(tab1, tab2) {
+ let hud1 = yield openConsole(tab1);
+ let outputNode1 = hud1.outputNode;
+
+ info("Waiting for messages");
+ yield waitForMessages({
+ webconsole: hud1,
+ messages: [{
+ text: TEST_IFRAME_URI,
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_LOG,
+ }]
+ });
+
+ let hud2 = yield openConsole(tab2);
+ let outputNode2 = hud2.outputNode;
+
+ isnot(outputNode1, outputNode2,
+ "the two HUD outputNodes must be different");
+
+ let msg = "Didn't find the iframe network request in tab2";
+ testLogEntry(outputNode2, TEST_IFRAME_URI, msg, true, true);
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_594497_history_arrow_keys.js b/devtools/client/webconsole/test/browser_webconsole_bug_594497_history_arrow_keys.js
new file mode 100644
index 000000000..514f875c0
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_594497_history_arrow_keys.js
@@ -0,0 +1,155 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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";
+
+var jsterm, inputNode, values;
+
+var TEST_URI = "data:text/html;charset=utf-8,Web Console test for " +
+ "bug 594497 and bug 619598";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ setup(hud);
+ performTests();
+
+ jsterm = inputNode = values = null;
+});
+
+function setup(HUD) {
+ jsterm = HUD.jsterm;
+ inputNode = jsterm.inputNode;
+
+ jsterm.focus();
+
+ ok(!jsterm.getInputValue(), "jsterm.getInputValue() is empty");
+
+ values = ["document", "window", "document.body"];
+ values.push(values.join(";\n"), "document.location");
+
+ // Execute each of the values;
+ for (let i = 0; i < values.length; i++) {
+ jsterm.setInputValue(values[i]);
+ jsterm.execute();
+ }
+}
+
+function performTests() {
+ EventUtils.synthesizeKey("VK_UP", {});
+
+
+ is(jsterm.getInputValue(), values[4],
+ "VK_UP: jsterm.getInputValue() #4 is correct");
+
+ ok(inputNode.selectionStart == values[4].length &&
+ inputNode.selectionStart == inputNode.selectionEnd,
+ "caret location is correct");
+
+ EventUtils.synthesizeKey("VK_UP", {});
+
+ is(jsterm.getInputValue(), values[3],
+ "VK_UP: jsterm.getInputValue() #3 is correct");
+
+ ok(inputNode.selectionStart == values[3].length &&
+ inputNode.selectionStart == inputNode.selectionEnd,
+ "caret location is correct");
+
+ inputNode.setSelectionRange(values[3].length - 2, values[3].length - 2);
+
+ EventUtils.synthesizeKey("VK_UP", {});
+ EventUtils.synthesizeKey("VK_UP", {});
+
+ is(jsterm.getInputValue(), values[3],
+ "VK_UP two times: jsterm.getInputValue() #3 is correct");
+
+ ok(inputNode.selectionStart == jsterm.getInputValue().indexOf("\n") &&
+ inputNode.selectionStart == inputNode.selectionEnd,
+ "caret location is correct");
+
+ EventUtils.synthesizeKey("VK_UP", {});
+
+ is(jsterm.getInputValue(), values[3],
+ "VK_UP again: jsterm.getInputValue() #3 is correct");
+
+ ok(inputNode.selectionStart == 0 &&
+ inputNode.selectionStart == inputNode.selectionEnd,
+ "caret location is correct");
+
+ EventUtils.synthesizeKey("VK_UP", {});
+
+ is(jsterm.getInputValue(), values[2],
+ "VK_UP: jsterm.getInputValue() #2 is correct");
+
+ EventUtils.synthesizeKey("VK_UP", {});
+
+ is(jsterm.getInputValue(), values[1],
+ "VK_UP: jsterm.getInputValue() #1 is correct");
+
+ EventUtils.synthesizeKey("VK_UP", {});
+
+ is(jsterm.getInputValue(), values[0],
+ "VK_UP: jsterm.getInputValue() #0 is correct");
+
+ ok(inputNode.selectionStart == values[0].length &&
+ inputNode.selectionStart == inputNode.selectionEnd,
+ "caret location is correct");
+
+ EventUtils.synthesizeKey("VK_DOWN", {});
+
+ is(jsterm.getInputValue(), values[1],
+ "VK_DOWN: jsterm.getInputValue() #1 is correct");
+
+ ok(inputNode.selectionStart == values[1].length &&
+ inputNode.selectionStart == inputNode.selectionEnd,
+ "caret location is correct");
+
+ EventUtils.synthesizeKey("VK_DOWN", {});
+
+ is(jsterm.getInputValue(), values[2],
+ "VK_DOWN: jsterm.getInputValue() #2 is correct");
+
+ EventUtils.synthesizeKey("VK_DOWN", {});
+
+ is(jsterm.getInputValue(), values[3],
+ "VK_DOWN: jsterm.getInputValue() #3 is correct");
+
+ ok(inputNode.selectionStart == values[3].length &&
+ inputNode.selectionStart == inputNode.selectionEnd,
+ "caret location is correct");
+
+ inputNode.setSelectionRange(2, 2);
+
+ EventUtils.synthesizeKey("VK_DOWN", {});
+ EventUtils.synthesizeKey("VK_DOWN", {});
+
+ is(jsterm.getInputValue(), values[3],
+ "VK_DOWN two times: jsterm.getInputValue() #3 is correct");
+
+ ok(inputNode.selectionStart > jsterm.getInputValue().lastIndexOf("\n") &&
+ inputNode.selectionStart == inputNode.selectionEnd,
+ "caret location is correct");
+
+ EventUtils.synthesizeKey("VK_DOWN", {});
+
+ is(jsterm.getInputValue(), values[3],
+ "VK_DOWN again: jsterm.getInputValue() #3 is correct");
+
+ ok(inputNode.selectionStart == values[3].length &&
+ inputNode.selectionStart == inputNode.selectionEnd,
+ "caret location is correct");
+
+ EventUtils.synthesizeKey("VK_DOWN", {});
+
+ is(jsterm.getInputValue(), values[4],
+ "VK_DOWN: jsterm.getInputValue() #4 is correct");
+
+ EventUtils.synthesizeKey("VK_DOWN", {});
+
+ ok(!jsterm.getInputValue(),
+ "VK_DOWN: jsterm.getInputValue() is empty");
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_595223_file_uri.js b/devtools/client/webconsole/test/browser_webconsole_bug_595223_file_uri.js
new file mode 100644
index 000000000..d57d724ca
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_595223_file_uri.js
@@ -0,0 +1,64 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 PREF = "devtools.webconsole.persistlog";
+const TEST_FILE = "test-network.html";
+const TEST_URI = "data:text/html;charset=utf8,<p>test file URI";
+
+var hud;
+
+add_task(function* () {
+ Services.prefs.setBoolPref(PREF, true);
+
+ let jar = getJar(getRootDirectory(gTestPath));
+ let dir = jar ?
+ extractJarToTmp(jar) :
+ getChromeDir(getResolvedURI(gTestPath));
+
+ dir.append(TEST_FILE);
+ let uri = Services.io.newFileURI(dir);
+
+ let { browser } = yield loadTab(TEST_URI);
+
+ hud = yield openConsole();
+ hud.jsterm.clearOutput();
+
+ let loaded = loadBrowser(browser);
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, uri.spec);
+ yield loaded;
+
+ yield testMessages();
+
+ Services.prefs.clearUserPref(PREF);
+ hud = null;
+});
+
+function testMessages() {
+ return waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "running network console logging tests",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ },
+ {
+ text: "test-network.html",
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_LOG,
+ },
+ {
+ text: "test-image.png",
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_LOG,
+ },
+ {
+ text: "testscript.js",
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_LOG,
+ }],
+ });
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_595350_multiple_windows_and_tabs.js b/devtools/client/webconsole/test/browser_webconsole_bug_595350_multiple_windows_and_tabs.js
new file mode 100644
index 000000000..1951cb366
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_595350_multiple_windows_and_tabs.js
@@ -0,0 +1,100 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests that the Web Console doesn't leak when multiple tabs and windows are
+// opened and then closed.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8,Web Console test for bug 595350";
+
+var win1 = window, win2;
+var openTabs = [];
+var loadedTabCount = 0;
+
+function test() {
+ requestLongerTimeout(3);
+
+ // Add two tabs in the main window.
+ addTabs(win1);
+
+ // Open a new window.
+ win2 = OpenBrowserWindow();
+ win2.addEventListener("load", onWindowLoad, true);
+}
+
+function onWindowLoad(aEvent) {
+ win2.removeEventListener(aEvent.type, onWindowLoad, true);
+
+ // Add two tabs in the new window.
+ addTabs(win2);
+}
+
+function addTabs(aWindow) {
+ for (let i = 0; i < 2; i++) {
+ let tab = aWindow.gBrowser.addTab(TEST_URI);
+ openTabs.push(tab);
+
+ tab.linkedBrowser.addEventListener("load", function onLoad(aEvent) {
+ tab.linkedBrowser.removeEventListener(aEvent.type, onLoad, true);
+
+ loadedTabCount++;
+ info("tabs loaded: " + loadedTabCount);
+ if (loadedTabCount >= 4) {
+ executeSoon(openConsoles);
+ }
+ }, true);
+ }
+}
+
+function openConsoles() {
+ function open(i) {
+ let tab = openTabs[i];
+ openConsole(tab).then(function (hud) {
+ ok(hud, "HUD is open for tab " + i);
+ let window = hud.target.tab.linkedBrowser.contentWindow;
+ window.console.log("message for tab " + i);
+
+ if (i >= openTabs.length - 1) {
+ // Use executeSoon() to allow the promise to resolve.
+ executeSoon(closeConsoles);
+ }
+ else {
+ executeSoon(() => open(i + 1));
+ }
+ });
+ }
+
+ // open the Web Console for each of the four tabs and log a message.
+ open(0);
+}
+
+function closeConsoles() {
+ let consolesClosed = 0;
+
+ function onWebConsoleClose(aSubject, aTopic) {
+ if (aTopic == "web-console-destroyed") {
+ consolesClosed++;
+ info("consoles destroyed: " + consolesClosed);
+ if (consolesClosed == 4) {
+ // Use executeSoon() to allow all the observers to execute.
+ executeSoon(finishTest);
+ }
+ }
+ }
+
+ Services.obs.addObserver(onWebConsoleClose, "web-console-destroyed", false);
+
+ registerCleanupFunction(() => {
+ Services.obs.removeObserver(onWebConsoleClose, "web-console-destroyed");
+ });
+
+ win2.close();
+
+ win1.gBrowser.removeTab(openTabs[0]);
+ win1.gBrowser.removeTab(openTabs[1]);
+
+ openTabs = win1 = win2 = null;
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_595934_message_categories.js b/devtools/client/webconsole/test/browser_webconsole_bug_595934_message_categories.js
new file mode 100644
index 000000000..855cfbb88
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_595934_message_categories.js
@@ -0,0 +1,211 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 TEST_URI = "data:text/html;charset=utf-8,Web Console test for " +
+ "bug 595934 - message categories coverage.";
+const TESTS_PATH = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/";
+const TESTS = [
+ {
+ // #0
+ file: "test-bug-595934-css-loader.html",
+ category: "CSS Loader",
+ matchString: "text/css",
+ },
+ {
+ // #1
+ file: "test-bug-595934-imagemap.html",
+ category: "Layout: ImageMap",
+ matchString: "shape=\"rect\"",
+ },
+ {
+ // #2
+ file: "test-bug-595934-html.html",
+ category: "HTML",
+ matchString: "multipart/form-data",
+ onload: function () {
+ let form = content.document.querySelector("form");
+ form.submit();
+ },
+ },
+ {
+ // #3
+ file: "test-bug-595934-workers.html",
+ category: "Web Worker",
+ matchString: "fooBarWorker",
+ },
+ {
+ // #4
+ file: "test-bug-595934-malformedxml.xhtml",
+ category: "malformed-xml",
+ matchString: "no root element found",
+ },
+ {
+ // #5
+ file: "test-bug-595934-svg.xhtml",
+ category: "SVG",
+ matchString: "fooBarSVG",
+ },
+ {
+ // #6
+ file: "test-bug-595934-css-parser.html",
+ category: "CSS Parser",
+ matchString: "foobarCssParser",
+ },
+ {
+ // #7
+ file: "test-bug-595934-malformedxml-external.html",
+ category: "malformed-xml",
+ matchString: "</html>",
+ },
+ {
+ // #8
+ file: "test-bug-595934-empty-getelementbyid.html",
+ category: "DOM",
+ matchString: "getElementById",
+ },
+ {
+ // #9
+ file: "test-bug-595934-canvas-css.html",
+ category: "CSS Parser",
+ matchString: "foobarCanvasCssParser",
+ },
+ {
+ // #10
+ file: "test-bug-595934-image.html",
+ category: "Image",
+ matchString: "corrupt",
+ },
+];
+
+var pos = -1;
+
+var foundCategory = false;
+var foundText = false;
+var pageLoaded = false;
+var pageError = false;
+var output = null;
+var jsterm = null;
+var hud = null;
+var testEnded = false;
+
+var TestObserver = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
+
+ observe: function testObserve(subject) {
+ if (testEnded || !(subject instanceof Ci.nsIScriptError)) {
+ return;
+ }
+
+ let expectedCategory = TESTS[pos].category;
+
+ info("test #" + pos + " console observer got " + subject.category +
+ ", is expecting " + expectedCategory);
+
+ if (subject.category == expectedCategory) {
+ foundCategory = true;
+ startNextTest();
+ } else {
+ info("unexpected message was: " + subject.sourceName + ":" +
+ subject.lineNumber + "; " + subject.errorMessage);
+ }
+ }
+};
+
+function consoleOpened(hudConsole) {
+ hud = hudConsole;
+ output = hud.outputNode;
+ jsterm = hud.jsterm;
+
+ Services.console.registerListener(TestObserver);
+
+ registerCleanupFunction(testEnd);
+
+ testNext();
+}
+
+function testNext() {
+ jsterm.clearOutput();
+ foundCategory = false;
+ foundText = false;
+ pageLoaded = false;
+ pageError = false;
+
+ pos++;
+ info("testNext: #" + pos);
+ if (pos < TESTS.length) {
+ test = TESTS[pos];
+
+ waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "message for test #" + pos + ": '" + test.matchString + "'",
+ text: test.matchString,
+ }],
+ }).then(() => {
+ foundText = true;
+ startNextTest();
+ });
+
+ let testLocation = TESTS_PATH + test.file;
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad(evt) {
+ if (content.location.href != testLocation) {
+ return;
+ }
+ gBrowser.selectedBrowser.removeEventListener(evt.type, onLoad, true);
+
+ pageLoaded = true;
+ test.onload && test.onload(evt);
+
+ if (test.expectError) {
+ content.addEventListener("error", function _onError() {
+ content.removeEventListener("error", _onError);
+ pageError = true;
+ startNextTest();
+ });
+ // On e10s, the exception is triggered in child process
+ // and is ignored by test harness
+ if (!Services.appinfo.browserTabsRemoteAutostart) {
+ expectUncaughtException();
+ }
+ } else {
+ pageError = true;
+ }
+
+ startNextTest();
+ }, true);
+
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, testLocation);
+ } else {
+ testEnded = true;
+ finishTest();
+ }
+}
+
+function testEnd() {
+ if (!testEnded) {
+ info("foundCategory " + foundCategory + " foundText " + foundText +
+ " pageLoaded " + pageLoaded + " pageError " + pageError);
+ }
+
+ Services.console.unregisterListener(TestObserver);
+ hud = TestObserver = output = jsterm = null;
+}
+
+function startNextTest() {
+ if (!testEnded && foundCategory && foundText && pageLoaded && pageError) {
+ testNext();
+ }
+}
+
+function test() {
+ requestLongerTimeout(2);
+
+ loadTab(TEST_URI).then(() => {
+ openConsole().then(consoleOpened);
+ });
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_597103_deactivateHUDForContext_unfocused_window.js b/devtools/client/webconsole/test/browser_webconsole_bug_597103_deactivateHUDForContext_unfocused_window.js
new file mode 100644
index 000000000..e14c3a069
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_597103_deactivateHUDForContext_unfocused_window.js
@@ -0,0 +1,97 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console.html";
+
+var tab1, tab2, win1, win2;
+var noErrors = true;
+
+function tab1Loaded() {
+ win2 = OpenBrowserWindow();
+ whenDelayedStartupFinished(win2, win2Loaded);
+}
+
+function win2Loaded() {
+ tab2 = win2.gBrowser.addTab(TEST_URI);
+ win2.gBrowser.selectedTab = tab2;
+ tab2.linkedBrowser.addEventListener("load", tab2Loaded, true);
+}
+
+function tab2Loaded(aEvent) {
+ tab2.linkedBrowser.removeEventListener(aEvent.type, tab2Loaded, true);
+
+ let consolesOpened = 0;
+ function onWebConsoleOpen() {
+ consolesOpened++;
+ if (consolesOpened == 2) {
+ executeSoon(closeConsoles);
+ }
+ }
+
+ function openConsoles() {
+ try {
+ let target1 = TargetFactory.forTab(tab1);
+ gDevTools.showToolbox(target1, "webconsole").then(onWebConsoleOpen);
+ } catch (ex) {
+ ok(false, "gDevTools.showToolbox(target1) exception: " + ex);
+ noErrors = false;
+ }
+
+ try {
+ let target2 = TargetFactory.forTab(tab2);
+ gDevTools.showToolbox(target2, "webconsole").then(onWebConsoleOpen);
+ } catch (ex) {
+ ok(false, "gDevTools.showToolbox(target2) exception: " + ex);
+ noErrors = false;
+ }
+ }
+
+ function closeConsoles() {
+ try {
+ let target1 = TargetFactory.forTab(tab1);
+ gDevTools.closeToolbox(target1).then(function () {
+ try {
+ let target2 = TargetFactory.forTab(tab2);
+ gDevTools.closeToolbox(target2).then(testEnd);
+ } catch (ex) {
+ ok(false, "gDevTools.closeToolbox(target2) exception: " + ex);
+ noErrors = false;
+ }
+ });
+ } catch (ex) {
+ ok(false, "gDevTools.closeToolbox(target1) exception: " + ex);
+ noErrors = false;
+ }
+ }
+
+ function testEnd() {
+ ok(noErrors, "there were no errors");
+
+ win1.gBrowser.removeTab(tab1);
+
+ Array.forEach(win2.gBrowser.tabs, function (aTab) {
+ win2.gBrowser.removeTab(aTab);
+ });
+
+ executeSoon(function () {
+ win2.close();
+ tab1 = tab2 = win1 = win2 = null;
+ finishTest();
+ });
+ }
+
+ openConsoles();
+}
+
+function test() {
+ loadTab(TEST_URI).then(() => {
+ tab1 = gBrowser.selectedTab;
+ win1 = window;
+ tab1Loaded();
+ });
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_597136_external_script_errors.js b/devtools/client/webconsole/test/browser_webconsole_bug_597136_external_script_errors.js
new file mode 100644
index 000000000..336700ada
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_597136_external_script_errors.js
@@ -0,0 +1,33 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 TEST_URI = "http://example.com/browser/devtools/client/" +
+ "webconsole/test/test-bug-597136-external-script-" +
+ "errors.html";
+
+function test() {
+ Task.spawn(function* () {
+ const {tab} = yield loadTab(TEST_URI);
+ const hud = yield openConsole(tab);
+
+ // On e10s, the exception is triggered in child process
+ // and is ignored by test harness
+ if (!Services.appinfo.browserTabsRemoteAutostart) {
+ expectUncaughtException();
+ }
+ BrowserTestUtils.synthesizeMouseAtCenter("button", {}, gBrowser.selectedBrowser);
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "bogus is not defined",
+ category: CATEGORY_JS,
+ severity: SEVERITY_ERROR,
+ }],
+ });
+ }).then(finishTest);
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_597136_network_requests_from_chrome.js b/devtools/client/webconsole/test/browser_webconsole_bug_597136_network_requests_from_chrome.js
new file mode 100644
index 000000000..473f02ccc
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_597136_network_requests_from_chrome.js
@@ -0,0 +1,52 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests that network requests from chrome don't cause the Web Console to
+// throw exceptions.
+
+"use strict";
+
+const TEST_URI = "http://example.com/";
+
+var good = true;
+var listener = {
+ QueryInterface: XPCOMUtils.generateQI([ Ci.nsIObserver ]),
+ observe: function (subject) {
+ if (subject instanceof Ci.nsIScriptError &&
+ subject.category === "XPConnect JavaScript" &&
+ subject.sourceName.includes("webconsole")) {
+ good = false;
+ }
+ }
+};
+
+var xhr;
+
+function test() {
+ Services.console.registerListener(listener);
+
+ // trigger a lazy-load of the HUD Service
+ HUDService;
+
+ xhr = new XMLHttpRequest();
+ xhr.addEventListener("load", xhrComplete, false);
+ xhr.open("GET", TEST_URI, true);
+ xhr.send(null);
+}
+
+function xhrComplete() {
+ xhr.removeEventListener("load", xhrComplete, false);
+ window.setTimeout(checkForException, 0);
+}
+
+function checkForException() {
+ ok(good, "no exception was thrown when sending a network request from a " +
+ "chrome window");
+
+ Services.console.unregisterListener(listener);
+ listener = xhr = null;
+
+ finishTest();
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_597460_filter_scroll.js b/devtools/client/webconsole/test/browser_webconsole_bug_597460_filter_scroll.js
new file mode 100644
index 000000000..2de4c9f21
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_597460_filter_scroll.js
@@ -0,0 +1,80 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-network.html";
+const PREF = "devtools.webconsole.persistlog";
+
+add_task(function* () {
+ Services.prefs.setBoolPref(PREF, true);
+
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+
+ let results = yield consoleOpened(hud);
+
+ testScroll(results, hud);
+
+ Services.prefs.clearUserPref(PREF);
+});
+
+function consoleOpened(hud) {
+ let deferred = promise.defer();
+
+ for (let i = 0; i < 200; i++) {
+ content.console.log("test message " + i);
+ }
+
+ hud.setFilterState("network", false);
+ hud.setFilterState("networkinfo", false);
+
+ hud.ui.filterBox.value = "test message";
+ hud.ui.adjustVisibilityOnSearchStringChange();
+
+ waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "console messages displayed",
+ text: "test message 199",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ }).then(() => {
+ waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "test-network.html",
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_LOG,
+ }],
+ }).then(deferred.resolve);
+
+ content.location.reload();
+ });
+
+ return deferred.promise;
+}
+
+function testScroll([result], hud) {
+ let scrollNode = hud.ui.outputWrapper;
+ let msgNode = [...result.matched][0];
+ ok(msgNode.classList.contains("filtered-by-type"),
+ "network message is filtered by type");
+ ok(msgNode.classList.contains("filtered-by-string"),
+ "network message is filtered by string");
+
+ ok(scrollNode.scrollTop > 0, "scroll location is not at the top");
+
+ // Make sure the Web Console output is scrolled as near as possible to the
+ // bottom.
+ let nodeHeight = msgNode.clientHeight;
+ ok(scrollNode.scrollTop >= scrollNode.scrollHeight - scrollNode.clientHeight -
+ nodeHeight * 2, "scroll location is correct");
+
+ hud.setFilterState("network", true);
+ hud.setFilterState("networkinfo", true);
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_597756_reopen_closed_tab.js b/devtools/client/webconsole/test/browser_webconsole_bug_597756_reopen_closed_tab.js
new file mode 100644
index 000000000..5a8280eed
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_597756_reopen_closed_tab.js
@@ -0,0 +1,70 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-bug-597756-reopen-closed-tab.html";
+
+var HUD;
+
+add_task(function* () {
+ // On e10s, the exception is triggered in child process
+ // and is ignored by test harness
+ if (!Services.appinfo.browserTabsRemoteAutostart) {
+ expectUncaughtException();
+ }
+
+ let { browser } = yield loadTab(TEST_URI);
+ HUD = yield openConsole();
+
+ if (!Services.appinfo.browserTabsRemoteAutostart) {
+ expectUncaughtException();
+ }
+
+ yield reload(browser);
+
+ yield testMessages();
+
+ yield closeConsole();
+
+ // Close and reopen
+ gBrowser.removeCurrentTab();
+
+ if (!Services.appinfo.browserTabsRemoteAutostart) {
+ expectUncaughtException();
+ }
+
+ let tab = yield loadTab(TEST_URI);
+ HUD = yield openConsole();
+
+ if (!Services.appinfo.browserTabsRemoteAutostart) {
+ expectUncaughtException();
+ }
+
+ yield reload(tab.browser);
+
+ yield testMessages();
+
+ HUD = null;
+});
+
+function reload(browser) {
+ let loaded = loadBrowser(browser);
+ browser.reload();
+ return loaded;
+}
+
+function testMessages() {
+ return waitForMessages({
+ webconsole: HUD,
+ messages: [{
+ name: "error message displayed",
+ text: "fooBug597756_error",
+ category: CATEGORY_JS,
+ severity: SEVERITY_ERROR,
+ }],
+ });
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_599725_response_headers.js b/devtools/client/webconsole/test/browser_webconsole_bug_599725_response_headers.js
new file mode 100644
index 000000000..4849793cb
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_599725_response_headers.js
@@ -0,0 +1,67 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 INIT_URI = "data:text/plain;charset=utf8,hello world";
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-bug-599725-response-headers.sjs";
+
+function performTest(request, hud) {
+ let deferred = promise.defer();
+
+ let headers = null;
+
+ function readHeader(name) {
+ for (let header of headers) {
+ if (header.name == name) {
+ return header.value;
+ }
+ }
+ return null;
+ }
+
+ hud.ui.proxy.webConsoleClient.getResponseHeaders(request.actor,
+ function (response) {
+ headers = response.headers;
+ ok(headers, "we have the response headers for reload");
+
+ let contentType = readHeader("Content-Type");
+ let contentLength = readHeader("Content-Length");
+
+ ok(!contentType, "we do not have the Content-Type header");
+ isnot(contentLength, 60, "Content-Length != 60");
+
+ executeSoon(deferred.resolve);
+ });
+
+ return deferred.promise;
+}
+
+let waitForRequest = Task.async(function*(hud) {
+ let request = yield waitForFinishedRequest(req=> {
+ return req.response.status === "304";
+ });
+
+ yield performTest(request, hud);
+});
+
+add_task(function* () {
+ let { browser } = yield loadTab(INIT_URI);
+
+ let hud = yield openConsole();
+
+ let gotLastRequest = waitForRequest(hud);
+
+ let loaded = loadBrowser(browser);
+ BrowserTestUtils.loadURI(browser, TEST_URI);
+ yield loaded;
+
+ let reloaded = loadBrowser(browser);
+ ContentTask.spawn(browser, null, "() => content.location.reload()");
+ yield reloaded;
+
+ yield gotLastRequest;
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_600183_charset.js b/devtools/client/webconsole/test/browser_webconsole_bug_600183_charset.js
new file mode 100644
index 000000000..153863824
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_600183_charset.js
@@ -0,0 +1,59 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 INIT_URI = "data:text/html;charset=utf-8,Web Console - bug 600183 test";
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-bug-600183-charset.html";
+
+function performTest(lastFinishedRequest, console) {
+ let deferred = promise.defer();
+
+ ok(lastFinishedRequest, "charset test page was loaded and logged");
+ HUDService.lastFinishedRequest.callback = null;
+
+ executeSoon(() => {
+ console.webConsoleClient.getResponseContent(lastFinishedRequest.actor,
+ (response) => {
+ ok(!response.contentDiscarded, "response body was not discarded");
+
+ let body = response.content.text;
+ ok(body, "we have the response body");
+
+ // 的问候!
+ let chars = "\u7684\u95ee\u5019!";
+ isnot(body.indexOf("<p>" + chars + "</p>"), -1,
+ "found the chinese simplified string");
+
+ HUDService.lastFinishedRequest.callback = null;
+ executeSoon(deferred.resolve);
+ });
+ });
+
+ return deferred.promise;
+}
+
+function waitForRequest() {
+ let deferred = promise.defer();
+ HUDService.lastFinishedRequest.callback = (req, console) => {
+ performTest(req, console).then(deferred.resolve);
+ };
+ return deferred.promise;
+}
+
+add_task(function* () {
+ let { browser } = yield loadTab(INIT_URI);
+
+ yield openConsole();
+
+ let gotLastRequest = waitForRequest();
+
+ let loaded = loadBrowser(browser);
+ BrowserTestUtils.loadURI(browser, TEST_URI);
+ yield loaded;
+
+ yield gotLastRequest;
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_601177_log_levels.js b/devtools/client/webconsole/test/browser_webconsole_bug_601177_log_levels.js
new file mode 100644
index 000000000..9dd81c9fd
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_601177_log_levels.js
@@ -0,0 +1,76 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 TEST_URI = "data:text/html;charset=utf-8,Web Console test for " +
+ "bug 601177: log levels";
+const TEST_URI2 = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-bug-601177-log-levels.html";
+
+add_task(function* () {
+ Services.prefs.setBoolPref("javascript.options.strict", true);
+
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ // On e10s, the exception is triggered in child process
+ // and is ignored by test harness
+ if (!Services.appinfo.browserTabsRemoteAutostart) {
+ expectUncaughtException();
+ }
+
+ yield testLogLevels(hud);
+
+ Services.prefs.clearUserPref("javascript.options.strict");
+});
+
+function testLogLevels(hud) {
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_URI2);
+
+ info("waiting for messages");
+
+ return waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ text: "test-bug-601177-log-levels.html",
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_LOG,
+ },
+ {
+ text: "test-bug-601177-log-levels.js",
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_LOG,
+ },
+ {
+ text: "test-image.png",
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_LOG,
+ },
+ {
+ text: "foobar-known-to-fail.png",
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_ERROR,
+ },
+ {
+ text: "foobarBug601177exception",
+ category: CATEGORY_JS,
+ severity: SEVERITY_ERROR,
+ },
+ {
+ text: "undefinedPropertyBug601177",
+ category: CATEGORY_JS,
+ severity: SEVERITY_WARNING,
+ },
+ {
+ text: "foobarBug601177strictError",
+ category: CATEGORY_JS,
+ severity: SEVERITY_WARNING,
+ },
+ ],
+ });
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_601352_scroll.js b/devtools/client/webconsole/test/browser_webconsole_bug_601352_scroll.js
new file mode 100644
index 000000000..89bd83a7a
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_601352_scroll.js
@@ -0,0 +1,84 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Test that the console output scrolls to JS eval results when there are many
+// messages displayed. See bug 601352.
+
+"use strict";
+
+add_task(function* () {
+ let {tab} = yield loadTab("data:text/html;charset=utf-8,Web Console test " +
+ "for bug 601352");
+ let hud = yield openConsole(tab);
+ hud.jsterm.clearOutput();
+
+ yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ let longMessage = "";
+ for (let i = 0; i < 50; i++) {
+ longMessage += "LongNonwrappingMessage";
+ }
+
+ for (let i = 0; i < 50; i++) {
+ content.console.log("test1 message " + i);
+ }
+
+ content.console.log(longMessage);
+
+ for (let i = 0; i < 50; i++) {
+ content.console.log("test2 message " + i);
+ }
+ });
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "test1 message 0",
+ }, {
+ text: "test1 message 49",
+ }, {
+ text: "LongNonwrappingMessage",
+ }, {
+ text: "test2 message 0",
+ }, {
+ text: "test2 message 49",
+ }],
+ });
+
+ let node = yield hud.jsterm.execute("1+1");
+
+ let scrollNode = hud.ui.outputWrapper;
+ let rectNode = node.getBoundingClientRect();
+ let rectOutput = scrollNode.getBoundingClientRect();
+
+ yield ContentTask.spawn(gBrowser.selectedBrowser, {
+ rectNode,
+ rectOutput,
+ scrollHeight: scrollNode.scrollHeight,
+ scrollTop: scrollNode.scrollTop,
+ clientHeight: scrollNode.clientHeight,
+ }, function* (args) {
+ console.debug("rectNode", args.rectNode, "rectOutput", args.rectOutput);
+ console.log("scrollNode scrollHeight", args.scrollHeight,
+ "scrollTop", args.scrollTop, "clientHeight",
+ args.clientHeight);
+ });
+
+ isnot(scrollNode.scrollTop, 0, "scroll location is not at the top");
+
+ // The bounding client rect .top/left coordinates are relative to the
+ // console iframe.
+
+ // Visible scroll viewport.
+ let height = rectOutput.height;
+
+ // Top and bottom coordinates of the last message node, relative to the
+ // outputNode.
+ let top = rectNode.top - rectOutput.top;
+ let bottom = top + rectNode.height;
+ info("node top " + top + " node bottom " + bottom + " node clientHeight " +
+ node.clientHeight);
+
+ ok(top >= 0 && bottom <= height, "last message is visible");
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_601667_filter_buttons.js b/devtools/client/webconsole/test/browser_webconsole_bug_601667_filter_buttons.js
new file mode 100644
index 000000000..6dae0a7b7
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_601667_filter_buttons.js
@@ -0,0 +1,267 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests that the filter button UI logic works correctly.
+
+"use strict";
+
+const TEST_URI = "http://example.com/";
+const FILTER_PREF_DOMAIN = "devtools.webconsole.filter.";
+
+var hud, hudId, hudBox;
+var prefs = {};
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ hud = yield openConsole();
+ hudId = hud.hudId;
+ hudBox = hud.ui.rootElement;
+
+ savePrefs();
+
+ testFilterButtons();
+
+ restorePrefs();
+
+ hud = hudId = hudBox = null;
+});
+
+function savePrefs() {
+ let branch = Services.prefs.getBranch(FILTER_PREF_DOMAIN);
+ let children = branch.getChildList("");
+ for (let child of children) {
+ prefs[child] = branch.getBoolPref(child);
+ }
+}
+
+function restorePrefs() {
+ let branch = Services.prefs.getBranch(FILTER_PREF_DOMAIN);
+ for (let p in prefs) {
+ branch.setBoolPref(p, prefs[p]);
+ }
+}
+
+function testFilterButtons() {
+ testMenuFilterButton("net");
+ testMenuFilterButton("css");
+ testMenuFilterButton("js");
+ testMenuFilterButton("logging");
+ testMenuFilterButton("security");
+ testMenuFilterButton("server");
+
+ testIsolateFilterButton("net");
+ testIsolateFilterButton("css");
+ testIsolateFilterButton("js");
+ testIsolateFilterButton("logging");
+ testIsolateFilterButton("security");
+ testIsolateFilterButton("server");
+}
+
+function testMenuFilterButton(category) {
+ let selector = ".webconsole-filter-button[category=\"" + category + "\"]";
+ let button = hudBox.querySelector(selector);
+ ok(button, "we have the \"" + category + "\" button");
+
+ let firstMenuItem = button.querySelector("menuitem");
+ ok(firstMenuItem, "we have the first menu item for the \"" + category +
+ "\" button");
+
+ // Turn all the filters off, if they were on.
+ let menuItem = firstMenuItem;
+ while (menuItem != null) {
+ if (menuItem.hasAttribute("prefKey") && isChecked(menuItem)) {
+ chooseMenuItem(menuItem);
+ }
+ menuItem = menuItem.nextSibling;
+ }
+
+ // Turn all the filters on; make sure the button gets checked.
+ menuItem = firstMenuItem;
+ let prefKey;
+ while (menuItem) {
+ if (menuItem.hasAttribute("prefKey")) {
+ prefKey = menuItem.getAttribute("prefKey");
+ chooseMenuItem(menuItem);
+ ok(isChecked(menuItem), "menu item " + prefKey + " for category " +
+ category + " is checked after clicking it");
+ ok(hud.ui.filterPrefs[prefKey], prefKey + " messages are " +
+ "on after clicking the appropriate menu item");
+ }
+ menuItem = menuItem.nextSibling;
+ }
+ ok(isChecked(button), "the button for category " + category + " is " +
+ "checked after turning on all its menu items");
+
+ // Turn one filter off; make sure the button is still checked.
+ prefKey = firstMenuItem.getAttribute("prefKey");
+ chooseMenuItem(firstMenuItem);
+ ok(!isChecked(firstMenuItem), "the first menu item for category " +
+ category + " is no longer checked after clicking it");
+ ok(!hud.ui.filterPrefs[prefKey], prefKey + " messages are " +
+ "turned off after clicking the appropriate menu item");
+ ok(isChecked(button), "the button for category " + category + " is still " +
+ "checked after turning off its first menu item");
+
+ // Turn all the filters off by clicking the main part of the button.
+ let subbutton = getMainButton(button);
+ ok(subbutton, "we have the subbutton for category " + category);
+
+ clickButton(subbutton);
+ ok(!isChecked(button), "the button for category " + category + " is " +
+ "no longer checked after clicking its main part");
+
+ menuItem = firstMenuItem;
+ while (menuItem) {
+ prefKey = menuItem.getAttribute("prefKey");
+ if (prefKey) {
+ ok(!isChecked(menuItem), "menu item " + prefKey + " for category " +
+ category + " is no longer checked after clicking the button");
+ ok(!hud.ui.filterPrefs[prefKey], prefKey + " messages are " +
+ "off after clicking the button");
+ }
+ menuItem = menuItem.nextSibling;
+ }
+
+ // Turn all the filters on by clicking the main part of the button.
+ clickButton(subbutton);
+
+ ok(isChecked(button), "the button for category " + category + " is " +
+ "checked after clicking its main part");
+
+ menuItem = firstMenuItem;
+ while (menuItem) {
+ if (menuItem.hasAttribute("prefKey")) {
+ prefKey = menuItem.getAttribute("prefKey");
+ // The CSS/Log menu item should not be checked. See bug 971798.
+ if (category == "css" && prefKey == "csslog") {
+ ok(!isChecked(menuItem), "menu item " + prefKey + " for category " +
+ category + " should not be checked after clicking the button");
+ ok(!hud.ui.filterPrefs[prefKey], prefKey + " messages are " +
+ "off after clicking the button");
+ } else {
+ ok(isChecked(menuItem), "menu item " + prefKey + " for category " +
+ category + " is checked after clicking the button");
+ ok(hud.ui.filterPrefs[prefKey], prefKey + " messages are " +
+ "on after clicking the button");
+ }
+ }
+ menuItem = menuItem.nextSibling;
+ }
+
+ // Uncheck the main button by unchecking all the filters
+ menuItem = firstMenuItem;
+ while (menuItem) {
+ // The csslog menu item is already unchecked at this point.
+ // Make sure it is not selected. See bug 971798.
+ prefKey = menuItem.getAttribute("prefKey");
+ if (prefKey && prefKey != "csslog") {
+ chooseMenuItem(menuItem);
+ }
+ menuItem = menuItem.nextSibling;
+ }
+
+ ok(!isChecked(button), "the button for category " + category + " is " +
+ "unchecked after unchecking all its filters");
+
+ // Turn all the filters on again by clicking the button.
+ clickButton(subbutton);
+}
+
+function testIsolateFilterButton(category) {
+ let selector = ".webconsole-filter-button[category=\"" + category + "\"]";
+ let targetButton = hudBox.querySelector(selector);
+ ok(targetButton, "we have the \"" + category + "\" button");
+
+ // Get the main part of the filter button.
+ let subbutton = getMainButton(targetButton);
+ ok(subbutton, "we have the subbutton for category " + category);
+
+ // Turn on all the filters by alt clicking the main part of the button.
+ altClickButton(subbutton);
+ ok(isChecked(targetButton), "the button for category " + category +
+ " is checked after isolating for filter");
+
+ // Check if all the filters for the target button are on.
+ let menuItems = targetButton.querySelectorAll("menuitem");
+ Array.forEach(menuItems, (item) => {
+ let prefKey = item.getAttribute("prefKey");
+ // The CSS/Log filter should not be checked. See bug 971798.
+ if (category == "css" && prefKey == "csslog") {
+ ok(!isChecked(item), "menu item " + prefKey + " for category " +
+ category + " should not be checked after isolating for " + category);
+ ok(!hud.ui.filterPrefs[prefKey], prefKey + " messages should be " +
+ "turned off after isolating for " + category);
+ } else if (prefKey) {
+ ok(isChecked(item), "menu item " + prefKey + " for category " +
+ category + " is checked after isolating for " + category);
+ ok(hud.ui.filterPrefs[prefKey], prefKey + " messages are " +
+ "turned on after isolating for " + category);
+ }
+ });
+
+ // Ensure all other filter buttons are toggled off and their
+ // associated filters are turned off
+ let buttons = hudBox.querySelectorAll(".webconsole-filter-button[category]");
+ Array.forEach(buttons, (filterButton) => {
+ if (filterButton !== targetButton) {
+ let categoryBtn = filterButton.getAttribute("category");
+ ok(!isChecked(filterButton), "the button for category " +
+ categoryBtn + " is unchecked after isolating for " + category);
+
+ menuItems = filterButton.querySelectorAll("menuitem");
+ Array.forEach(menuItems, (item) => {
+ let prefKey = item.getAttribute("prefKey");
+ if (prefKey) {
+ ok(!isChecked(item), "menu item " + prefKey + " for category " +
+ category + " is unchecked after isolating for " + category);
+ ok(!hud.ui.filterPrefs[prefKey], prefKey + " messages are " +
+ "turned off after isolating for " + category);
+ }
+ });
+
+ // Turn all the filters on again by clicking the button.
+ let mainButton = getMainButton(filterButton);
+ clickButton(mainButton);
+ }
+ });
+}
+
+/**
+ * Return the main part of the target filter button.
+ */
+function getMainButton(targetButton) {
+ let anonymousNodes = hud.ui.document.getAnonymousNodes(targetButton);
+ let subbutton;
+
+ for (let i = 0; i < anonymousNodes.length; i++) {
+ let node = anonymousNodes[i];
+ if (node.classList.contains("toolbarbutton-menubutton-button")) {
+ subbutton = node;
+ break;
+ }
+ }
+
+ return subbutton;
+}
+
+function clickButton(node) {
+ EventUtils.sendMouseEvent({ type: "click" }, node);
+}
+
+function altClickButton(node) {
+ EventUtils.sendMouseEvent({ type: "click", altKey: true }, node);
+}
+
+function chooseMenuItem(node) {
+ let event = document.createEvent("XULCommandEvent");
+ event.initCommandEvent("command", true, true, window, 0, false, false, false,
+ false, null);
+ node.dispatchEvent(event);
+}
+
+function isChecked(node) {
+ return node.getAttribute("checked") === "true";
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_603750_websocket.js b/devtools/client/webconsole/test/browser_webconsole_bug_603750_websocket.js
new file mode 100644
index 000000000..f14530d06
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_603750_websocket.js
@@ -0,0 +1,37 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-bug-603750-websocket.html";
+const TEST_URI2 = "data:text/html;charset=utf-8,Web Console test for " +
+ "bug 603750: Web Socket errors";
+
+add_task(function* () {
+ yield loadTab(TEST_URI2);
+
+ let hud = yield openConsole();
+
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_URI);
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ text: "ws://0.0.0.0:81",
+ source: { url: "test-bug-603750-websocket.js" },
+ category: CATEGORY_JS,
+ severity: SEVERITY_ERROR,
+ },
+ {
+ text: "ws://0.0.0.0:82",
+ source: { url: "test-bug-603750-websocket.js" },
+ category: CATEGORY_JS,
+ severity: SEVERITY_ERROR,
+ },
+ ]
+ });
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_611795.js b/devtools/client/webconsole/test/browser_webconsole_bug_611795.js
new file mode 100644
index 000000000..1fa4d717e
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_611795.js
@@ -0,0 +1,67 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 TEST_URI = 'data:text/html;charset=utf-8,<div style="-moz-opacity:0;">' +
+ 'test repeated css warnings</div><p style="-moz-opacity:0">' +
+ "hi</p>";
+var hud;
+
+/**
+ * Unit test for bug 611795:
+ * Repeated CSS messages get collapsed into one.
+ */
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ hud = yield openConsole();
+ hud.jsterm.clearOutput(true);
+
+ BrowserReload();
+ yield loadBrowser(gBrowser.selectedBrowser);
+
+ yield onContentLoaded();
+ yield testConsoleLogRepeats();
+
+ hud = null;
+});
+
+function onContentLoaded() {
+ let cssWarning = "Unknown property \u2018-moz-opacity\u2019. Declaration dropped.";
+
+ return waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: cssWarning,
+ category: CATEGORY_CSS,
+ severity: SEVERITY_WARNING,
+ repeats: 2,
+ }],
+ });
+}
+
+function testConsoleLogRepeats() {
+ let jsterm = hud.jsterm;
+
+ jsterm.clearOutput();
+
+ jsterm.setInputValue("for (let i = 0; i < 10; ++i) console.log('this is a " +
+ "line of reasonably long text that I will use to " +
+ "verify that the repeated text node is of an " +
+ "appropriate size.');");
+ jsterm.execute();
+
+ return waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "this is a line of reasonably long text",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ repeats: 10,
+ }],
+ });
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_613013_console_api_iframe.js b/devtools/client/webconsole/test/browser_webconsole_bug_613013_console_api_iframe.js
new file mode 100644
index 000000000..5d0067958
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_613013_console_api_iframe.js
@@ -0,0 +1,26 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-bug-613013-console-api-iframe.html";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ BrowserReload();
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "foobarBug613013",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_613280_jsterm_copy.js b/devtools/client/webconsole/test/browser_webconsole_bug_613280_jsterm_copy.js
new file mode 100644
index 000000000..95752021d
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_613280_jsterm_copy.js
@@ -0,0 +1,64 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 TEST_URI = "data:text/html;charset=utf-8,Web Console test for bug 613280";
+
+function test() {
+ loadTab(TEST_URI).then(() => {
+ openConsole().then((HUD) => {
+ ContentTask.spawn(gBrowser.selectedBrowser, null, function*(){
+ content.console.log("foobarBazBug613280");
+ });
+ waitForMessages({
+ webconsole: HUD,
+ messages: [{
+ text: "foobarBazBug613280",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ }).then(performTest.bind(null, HUD));
+ });
+ });
+}
+
+function performTest(HUD, [result]) {
+ let msg = [...result.matched][0];
+ let input = HUD.jsterm.inputNode;
+
+ let clipboardSetup = function () {
+ goDoCommand("cmd_copy");
+ };
+
+ let clipboardCopyDone = function () {
+ finishTest();
+ };
+
+ let controller = top.document.commandDispatcher
+ .getControllerForCommand("cmd_copy");
+ is(controller.isCommandEnabled("cmd_copy"), false, "cmd_copy is disabled");
+
+ HUD.ui.output.selectMessage(msg);
+ HUD.outputNode.focus();
+
+ goUpdateCommand("cmd_copy");
+
+ controller = top.document.commandDispatcher
+ .getControllerForCommand("cmd_copy");
+ is(controller.isCommandEnabled("cmd_copy"), true, "cmd_copy is enabled");
+
+ // Remove new lines and whitespace since getSelection() includes
+ // a new line between message and line number, but the clipboard doesn't
+ // @see bug 1119503
+ let selectionText = (HUD.iframeWindow.getSelection() + "")
+ .replace(/\r?\n|\r| /g, "");
+ isnot(selectionText.indexOf("foobarBazBug613280"), -1,
+ "selection text includes 'foobarBazBug613280'");
+
+ waitForClipboard((str) => {
+ return selectionText.trim() === str.trim().replace(/ /g, "");
+ }, clipboardSetup, clipboardCopyDone, clipboardCopyDone);
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_613642_maintain_scroll.js b/devtools/client/webconsole/test/browser_webconsole_bug_613642_maintain_scroll.js
new file mode 100644
index 000000000..e24ce28e2
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_613642_maintain_scroll.js
@@ -0,0 +1,119 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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";
+
+var TEST_URI = "data:text/html;charset=utf-8,Web Console test for " +
+ "bug 613642: remember scroll location";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ hud.jsterm.clearOutput();
+ let outputNode = hud.outputNode;
+ let scrollBox = hud.ui.outputWrapper;
+
+ for (let i = 0; i < 150; i++) {
+ ContentTask.spawn(gBrowser.selectedBrowser, i, function* (num) {
+ content.console.log("test message " + num);
+ });
+ }
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "test message 149",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+
+ ok(scrollBox.scrollTop > 0, "scroll location is not at the top");
+
+ // scroll to the first node
+ outputNode.focus();
+
+ let scrolled = promise.defer();
+
+ scrollBox.onscroll = () => {
+ info("onscroll top " + scrollBox.scrollTop);
+ if (scrollBox.scrollTop != 0) {
+ // Wait for scroll to 0.
+ return;
+ }
+ scrollBox.onscroll = null;
+ is(scrollBox.scrollTop, 0, "scroll location updated (moved to top)");
+ scrolled.resolve();
+ };
+ EventUtils.synthesizeKey("VK_HOME", {}, hud.iframeWindow);
+
+ yield scrolled.promise;
+
+ // add a message and make sure scroll doesn't change
+ ContentTask.spawn(gBrowser.selectedBrowser, null,
+ "() => content.console.log('test message 150')");
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "test message 150",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+
+ scrolled = promise.defer();
+ scrollBox.onscroll = () => {
+ if (scrollBox.scrollTop != 0) {
+ // Wait for scroll to stabilize at the top.
+ return;
+ }
+ scrollBox.onscroll = null;
+ is(scrollBox.scrollTop, 0, "scroll location is still at the top");
+ scrolled.resolve();
+ };
+
+ // Make sure that scroll stabilizes at the top. executeSoon() is needed for
+ // the yield to work.
+ executeSoon(scrollBox.onscroll);
+
+ yield scrolled.promise;
+
+ // scroll back to the bottom
+ outputNode.lastChild.focus();
+
+ scrolled = promise.defer();
+ scrollBox.onscroll = () => {
+ if (scrollBox.scrollTop == 0) {
+ // Wait for scroll to bottom.
+ return;
+ }
+ scrollBox.onscroll = null;
+ isnot(scrollBox.scrollTop, 0, "scroll location updated (moved to bottom)");
+ scrolled.resolve();
+ };
+ EventUtils.synthesizeKey("VK_END", {});
+ yield scrolled.promise;
+
+ let oldScrollTop = scrollBox.scrollTop;
+
+ ContentTask.spawn(gBrowser.selectedBrowser, null,
+ "() => content.console.log('test message 151')");
+
+ scrolled = promise.defer();
+ scrollBox.onscroll = () => {
+ if (scrollBox.scrollTop == oldScrollTop) {
+ // Wait for scroll to change.
+ return;
+ }
+ scrollBox.onscroll = null;
+ isnot(scrollBox.scrollTop, oldScrollTop,
+ "scroll location updated (moved to bottom again)");
+ scrolled.resolve();
+ };
+ yield scrolled.promise;
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_613642_prune_scroll.js b/devtools/client/webconsole/test/browser_webconsole_bug_613642_prune_scroll.js
new file mode 100644
index 000000000..c53fe1683
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_613642_prune_scroll.js
@@ -0,0 +1,82 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 TEST_URI = "data:text/html;charset=utf-8,Web Console test for " +
+ "bug 613642: maintain scroll with pruning of old messages";
+
+var hud;
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ hud = yield openConsole();
+
+ hud.jsterm.clearOutput();
+
+ let outputNode = hud.outputNode;
+
+ Services.prefs.setIntPref("devtools.hud.loglimit.console", 140);
+ let scrollBoxElement = hud.ui.outputWrapper;
+
+ ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ for (let i = 0; i < 150; i++) {
+ content.console.log("test message " + i);
+ }
+ });
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "test message 149",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+
+ let oldScrollTop = scrollBoxElement.scrollTop;
+ isnot(oldScrollTop, 0, "scroll location is not at the top");
+
+ let firstNode = outputNode.firstChild;
+ ok(firstNode, "found the first message");
+
+ let msgNode = outputNode.children[80];
+ ok(msgNode, "found the 80th message");
+
+ // scroll to the middle message node
+ msgNode.scrollIntoView(false);
+
+ isnot(scrollBoxElement.scrollTop, oldScrollTop,
+ "scroll location updated (scrolled to message)");
+
+ oldScrollTop = scrollBoxElement.scrollTop;
+
+ // add a message
+ ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ content.console.log("hello world");
+ });
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "hello world",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+
+ // Scroll location needs to change, because one message is also removed, and
+ // we need to scroll a bit towards the top, to keep the current view in sync.
+ isnot(scrollBoxElement.scrollTop, oldScrollTop,
+ "scroll location updated (added a message)");
+
+ isnot(outputNode.firstChild, firstNode,
+ "first message removed");
+
+ Services.prefs.clearUserPref("devtools.hud.loglimit.console");
+
+ hud = null;
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_614793_jsterm_scroll.js b/devtools/client/webconsole/test/browser_webconsole_bug_614793_jsterm_scroll.js
new file mode 100644
index 000000000..ae61023a9
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_614793_jsterm_scroll.js
@@ -0,0 +1,54 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 TEST_URI = "data:text/html;charset=utf-8,Web Console test for " +
+ "bug 614793: jsterm result scroll";
+
+requestLongerTimeout(2);
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+
+ yield testScrollPosition(hud);
+});
+
+function* testScrollPosition(hud) {
+ hud.jsterm.clearOutput();
+
+ let scrollNode = hud.ui.outputWrapper;
+
+ for (let i = 0; i < 150; i++) {
+ yield ContentTask.spawn(gBrowser.selectedBrowser, i, function* (i) {
+ content.console.log("test message " + i);
+ });
+ }
+
+ let oldScrollTop = -1;
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "test message 149",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+
+ oldScrollTop = scrollNode.scrollTop;
+ isnot(oldScrollTop, 0, "scroll location is not at the top");
+
+ let msg = yield hud.jsterm.execute("'hello world'");
+
+ isnot(scrollNode.scrollTop, oldScrollTop, "scroll location updated");
+
+ oldScrollTop = scrollNode.scrollTop;
+
+ msg.scrollIntoView(false);
+
+ is(scrollNode.scrollTop, oldScrollTop, "scroll location is the same");
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_618078_network_exceptions.js b/devtools/client/webconsole/test/browser_webconsole_bug_618078_network_exceptions.js
new file mode 100644
index 000000000..439793b22
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_618078_network_exceptions.js
@@ -0,0 +1,36 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests that we report JS exceptions in event handlers coming from
+// network requests, like onreadystate for XHR. See bug 618078.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8,Web Console test for bug 618078";
+const TEST_URI2 = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-bug-618078-network-exceptions.html";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ // On e10s, the exception is triggered in child process
+ // and is ignored by test harness
+ if (!Services.appinfo.browserTabsRemoteAutostart) {
+ expectUncaughtException();
+ }
+
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_URI2);
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "bug618078exception",
+ category: CATEGORY_JS,
+ severity: SEVERITY_ERROR,
+ }],
+ });
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_621644_jsterm_dollar.js b/devtools/client/webconsole/test/browser_webconsole_bug_621644_jsterm_dollar.js
new file mode 100644
index 000000000..6f4248c51
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_621644_jsterm_dollar.js
@@ -0,0 +1,47 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-bug-621644-jsterm-dollar.html";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ yield test$(hud);
+ yield test$$(hud);
+});
+
+function* test$(HUD) {
+ let deferred = promise.defer();
+
+ HUD.jsterm.clearOutput();
+
+ HUD.jsterm.execute("$(document.body)", (msg) => {
+ ok(msg.textContent.indexOf("<p>") > -1,
+ "jsterm output is correct for $()");
+ deferred.resolve();
+ });
+
+ return deferred.promise;
+}
+
+function test$$(HUD) {
+ let deferred = promise.defer();
+
+ HUD.jsterm.clearOutput();
+
+ HUD.jsterm.setInputValue();
+ HUD.jsterm.execute("$$(document)", (msg) => {
+ ok(msg.textContent.indexOf("621644") > -1,
+ "jsterm output is correct for $$()");
+ deferred.resolve();
+ });
+
+ return deferred.promise;
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_622303_persistent_filters.js b/devtools/client/webconsole/test/browser_webconsole_bug_622303_persistent_filters.js
new file mode 100644
index 000000000..f4b5dca96
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_622303_persistent_filters.js
@@ -0,0 +1,149 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 prefs = {
+ "net": [
+ "network",
+ "netwarn",
+ "netxhr",
+ "networkinfo"
+ ],
+ "css": [
+ "csserror",
+ "cssparser",
+ "csslog"
+ ],
+ "js": [
+ "exception",
+ "jswarn",
+ "jslog",
+ ],
+ "logging": [
+ "error",
+ "warn",
+ "info",
+ "log",
+ "serviceworkers",
+ "sharedworkers",
+ "windowlessworkers"
+ ]
+};
+
+add_task(function* () {
+ // Set all prefs to true
+ for (let category in prefs) {
+ prefs[category].forEach(function (pref) {
+ Services.prefs.setBoolPref("devtools.webconsole.filter." + pref, true);
+ });
+ }
+
+ yield loadTab("about:blank");
+
+ let hud = yield openConsole();
+
+ let hud2 = yield onConsoleOpen(hud);
+ let hud3 = yield onConsoleReopen1(hud2);
+ yield onConsoleReopen2(hud3);
+
+ // Clear prefs
+ for (let category in prefs) {
+ prefs[category].forEach(function (pref) {
+ Services.prefs.clearUserPref("devtools.webconsole.filter." + pref);
+ });
+ }
+});
+
+function onConsoleOpen(hud) {
+ let deferred = promise.defer();
+
+ let hudBox = hud.ui.rootElement;
+
+ // Check if the filters menuitems exists and are checked
+ for (let category in prefs) {
+ let button = hudBox.querySelector(".webconsole-filter-button[category=\""
+ + category + "\"]");
+ ok(isChecked(button), "main button for " + category +
+ " category is checked");
+
+ prefs[category].forEach(function (pref) {
+ let menuitem = hudBox.querySelector("menuitem[prefKey=" + pref + "]");
+ ok(isChecked(menuitem), "menuitem for " + pref + " is checked");
+ });
+ }
+
+ // Set all prefs to false
+ for (let category in prefs) {
+ prefs[category].forEach(function (pref) {
+ hud.setFilterState(pref, false);
+ });
+ }
+
+ // Re-init the console
+ closeConsole().then(() => {
+ openConsole().then(deferred.resolve);
+ });
+
+ return deferred.promise;
+}
+
+function onConsoleReopen1(hud) {
+ info("testing after reopening once");
+ let deferred = promise.defer();
+
+ let hudBox = hud.ui.rootElement;
+
+ // Check if the filter button and menuitems are unchecked
+ for (let category in prefs) {
+ let button = hudBox.querySelector(".webconsole-filter-button[category=\""
+ + category + "\"]");
+ ok(isUnchecked(button), "main button for " + category +
+ " category is not checked");
+
+ prefs[category].forEach(function (pref) {
+ let menuitem = hudBox.querySelector("menuitem[prefKey=" + pref + "]");
+ ok(isUnchecked(menuitem), "menuitem for " + pref + " is not checked");
+ });
+ }
+
+ // Set first pref in each category to true
+ for (let category in prefs) {
+ hud.setFilterState(prefs[category][0], true);
+ }
+
+ // Re-init the console
+ closeConsole().then(() => {
+ openConsole().then(deferred.resolve);
+ });
+
+ return deferred.promise;
+}
+
+function onConsoleReopen2(hud) {
+ info("testing after reopening again");
+
+ let hudBox = hud.ui.rootElement;
+
+ // Check the main category button is checked and first menuitem is checked
+ for (let category in prefs) {
+ let button = hudBox.querySelector(".webconsole-filter-button[category=\"" +
+ category + "\"]");
+ ok(isChecked(button), category +
+ " button is checked when first pref is true");
+
+ let pref = prefs[category][0];
+ let menuitem = hudBox.querySelector("menuitem[prefKey=" + pref + "]");
+ ok(isChecked(menuitem), "first " + category + " menuitem is checked");
+ }
+}
+
+function isChecked(aNode) {
+ return aNode.getAttribute("checked") === "true";
+}
+
+function isUnchecked(aNode) {
+ return aNode.getAttribute("checked") === "false";
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_623749_ctrl_a_select_all_winnt.js b/devtools/client/webconsole/test/browser_webconsole_bug_623749_ctrl_a_select_all_winnt.js
new file mode 100644
index 000000000..3de10774d
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_623749_ctrl_a_select_all_winnt.js
@@ -0,0 +1,32 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Test for https://bugzilla.mozilla.org/show_bug.cgi?id=623749
+// Map Control + A to Select All, In the web console input, on Windows
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8,Test console for bug 623749";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ let jsterm = hud.jsterm;
+ jsterm.setInputValue("Ignore These Four Words");
+ let inputNode = jsterm.inputNode;
+
+ // Test select all with Control + A.
+ EventUtils.synthesizeKey("a", { ctrlKey: true });
+ let inputLength = inputNode.selectionEnd - inputNode.selectionStart;
+ is(inputLength, jsterm.getInputValue().length, "Select all of input");
+
+ // Test do nothing on Control + E.
+ jsterm.setInputValue("Ignore These Four Words");
+ inputNode.selectionStart = 0;
+ EventUtils.synthesizeKey("e", { ctrlKey: true });
+ is(inputNode.selectionStart, 0, "Control + E does not move to end of input");
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_630733_response_redirect_headers.js b/devtools/client/webconsole/test/browser_webconsole_bug_630733_response_redirect_headers.js
new file mode 100644
index 000000000..509749953
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_630733_response_redirect_headers.js
@@ -0,0 +1,120 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 TEST_URI = "data:text/html;charset=utf-8,<p>Web Console test for " +
+ "bug 630733";
+const TEST_URI2 = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-bug-630733-response-redirect-headers.sjs";
+
+var lastFinishedRequests = {};
+var webConsoleClient;
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ yield consoleOpened(hud);
+ yield getHeaders();
+ yield getContent();
+
+ performTest();
+});
+
+function consoleOpened(hud) {
+ let deferred = promise.defer();
+
+ webConsoleClient = hud.ui.webConsoleClient;
+ HUDService.lastFinishedRequest.callback = (aHttpRequest) => {
+ let status = aHttpRequest.response.status;
+ lastFinishedRequests[status] = aHttpRequest;
+ if ("301" in lastFinishedRequests &&
+ "404" in lastFinishedRequests) {
+ deferred.resolve();
+ }
+ };
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_URI2);
+
+ return deferred.promise;
+}
+
+function getHeaders() {
+ let deferred = promise.defer();
+
+ HUDService.lastFinishedRequest.callback = null;
+
+ ok("301" in lastFinishedRequests, "request 1: 301 Moved Permanently");
+ ok("404" in lastFinishedRequests, "request 2: 404 Not found");
+
+ webConsoleClient.getResponseHeaders(lastFinishedRequests["301"].actor,
+ function (response) {
+ lastFinishedRequests["301"].response.headers = response.headers;
+
+ webConsoleClient.getResponseHeaders(lastFinishedRequests["404"].actor,
+ function (resp) {
+ lastFinishedRequests["404"].response.headers = resp.headers;
+ executeSoon(deferred.resolve);
+ });
+ });
+ return deferred.promise;
+}
+
+function getContent() {
+ let deferred = promise.defer();
+
+ webConsoleClient.getResponseContent(lastFinishedRequests["301"].actor,
+ function (response) {
+ lastFinishedRequests["301"].response.content = response.content;
+ lastFinishedRequests["301"].discardResponseBody = response.contentDiscarded;
+
+ webConsoleClient.getResponseContent(lastFinishedRequests["404"].actor,
+ function (resp) {
+ lastFinishedRequests["404"].response.content = resp.content;
+ lastFinishedRequests["404"].discardResponseBody =
+ resp.contentDiscarded;
+
+ webConsoleClient = null;
+ executeSoon(deferred.resolve);
+ });
+ });
+ return deferred.promise;
+}
+
+function performTest() {
+ function readHeader(name) {
+ for (let header of headers) {
+ if (header.name == name) {
+ return header.value;
+ }
+ }
+ return null;
+ }
+
+ let headers = lastFinishedRequests["301"].response.headers;
+ is(readHeader("Content-Type"), "text/html",
+ "we do have the Content-Type header");
+ is(readHeader("Content-Length"), 71, "Content-Length is correct");
+ is(readHeader("Location"), "/redirect-from-bug-630733",
+ "Content-Length is correct");
+ is(readHeader("x-foobar-bug630733"), "bazbaz",
+ "X-Foobar-bug630733 is correct");
+
+ let body = lastFinishedRequests["301"].response.content;
+ ok(!body.text, "body discarded for request 1");
+ ok(lastFinishedRequests["301"].discardResponseBody,
+ "body discarded for request 1 (confirmed)");
+
+ headers = lastFinishedRequests["404"].response.headers;
+ ok(!readHeader("Location"), "no Location header");
+ ok(!readHeader("x-foobar-bug630733"), "no X-Foobar-bug630733 header");
+
+ body = lastFinishedRequests["404"].response.content.text;
+ isnot(body.indexOf("404"), -1,
+ "body is correct for request 2");
+
+ lastFinishedRequests = webConsoleClient = null;
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_632275_getters_document_width.js b/devtools/client/webconsole/test/browser_webconsole_bug_632275_getters_document_width.js
new file mode 100644
index 000000000..45d1f7102
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_632275_getters_document_width.js
@@ -0,0 +1,47 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-bug-632275-getters.html";
+
+var getterValue = null;
+
+function test() {
+ loadTab(TEST_URI).then(() => {
+ openConsole().then(consoleOpened);
+ });
+}
+
+function consoleOpened(hud) {
+ let doc = content.wrappedJSObject.document;
+ getterValue = doc.foobar._val;
+ hud.jsterm.execute("console.dir(document)");
+
+ let onOpen = onViewOpened.bind(null, hud);
+ hud.jsterm.once("variablesview-fetched", onOpen);
+}
+
+function onViewOpened(hud, event, view) {
+ let doc = content.wrappedJSObject.document;
+
+ findVariableViewProperties(view, [
+ { name: /^(width|height)$/, dontMatch: 1 },
+ { name: "foobar._val", value: getterValue },
+ { name: "foobar.val", isGetter: true },
+ ], { webconsole: hud }).then(function () {
+ is(doc.foobar._val, getterValue, "getter did not execute");
+ is(doc.foobar.val, getterValue + 1, "getter executed");
+ is(doc.foobar._val, getterValue + 1, "getter executed (recheck)");
+
+ let textContent = hud.outputNode.textContent;
+ is(textContent.indexOf("document.body.client"), -1,
+ "no document.width/height warning displayed");
+
+ getterValue = null;
+ finishTest();
+ });
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_632347_iterators_generators.js b/devtools/client/webconsole/test/browser_webconsole_bug_632347_iterators_generators.js
new file mode 100644
index 000000000..c5e672444
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_632347_iterators_generators.js
@@ -0,0 +1,84 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-bug-632347-iterators-generators.html";
+
+function test() {
+ requestLongerTimeout(6);
+
+ loadTab(TEST_URI).then(() => {
+ openConsole().then(consoleOpened);
+ });
+}
+
+function consoleOpened(HUD) {
+ let {JSPropertyProvider} = require("devtools/shared/webconsole/js-property-provider");
+
+ let tmp = Cu.import("resource://gre/modules/jsdebugger.jsm", {});
+ tmp.addDebuggerToGlobal(tmp);
+ let dbg = new tmp.Debugger();
+
+ let jsterm = HUD.jsterm;
+ let win = content.wrappedJSObject;
+ let dbgWindow = dbg.addDebuggee(content);
+ let container = win._container;
+
+ // Make sure autocomplete does not walk through iterators and generators.
+ let result = container.gen1.next();
+ let completion = JSPropertyProvider(dbgWindow, null, "_container.gen1.");
+ isnot(completion.matches.length, 0, "Got matches for gen1");
+
+ is(result + 1, container.gen1.next(), "gen1.next() did not execute");
+
+ result = container.gen2.next().value;
+
+ completion = JSPropertyProvider(dbgWindow, null, "_container.gen2.");
+ isnot(completion.matches.length, 0, "Got matches for gen2");
+
+ is((result / 2 + 1) * 2, container.gen2.next().value,
+ "gen2.next() did not execute");
+
+ result = container.iter1.next();
+ is(result[0], "foo", "iter1.next() [0] is correct");
+ is(result[1], "bar", "iter1.next() [1] is correct");
+
+ completion = JSPropertyProvider(dbgWindow, null, "_container.iter1.");
+ isnot(completion.matches.length, 0, "Got matches for iter1");
+
+ result = container.iter1.next();
+ is(result[0], "baz", "iter1.next() [0] is correct");
+ is(result[1], "baaz", "iter1.next() [1] is correct");
+
+ let dbgContent = dbg.makeGlobalObjectReference(content);
+ completion = JSPropertyProvider(dbgContent, null, "_container.iter2.");
+ isnot(completion.matches.length, 0, "Got matches for iter2");
+
+ completion = JSPropertyProvider(dbgWindow, null, "window._container.");
+ ok(completion, "matches available for window._container");
+ ok(completion.matches.length, "matches available for window (length)");
+
+ dbg.removeDebuggee(content);
+ jsterm.clearOutput();
+
+ jsterm.execute("window._container", (msg) => {
+ jsterm.once("variablesview-fetched", testVariablesView.bind(null, HUD));
+ let anchor = msg.querySelector(".message-body a");
+ EventUtils.synthesizeMouse(anchor, 2, 2, {}, HUD.iframeWindow);
+ });
+}
+
+function testVariablesView(aWebconsole, aEvent, aView) {
+ findVariableViewProperties(aView, [
+ { name: "gen1", isGenerator: true },
+ { name: "gen2", isGenerator: true },
+ { name: "iter1", isIterator: true },
+ { name: "iter2", isIterator: true },
+ ], { webconsole: aWebconsole }).then(function () {
+ executeSoon(finishTest);
+ });
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_632817.js b/devtools/client/webconsole/test/browser_webconsole_bug_632817.js
new file mode 100644
index 000000000..561e3b112
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_632817.js
@@ -0,0 +1,217 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests that Web console messages can be filtered for NET events.
+
+"use strict";
+
+const TEST_NETWORK_REQUEST_URI =
+ "https://example.com/browser/devtools/client/webconsole/test/" +
+ "test-network-request.html";
+
+const TEST_IMG = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-image.png";
+
+const TEST_DATA_JSON_CONTENT =
+ '{ id: "test JSON data", myArray: [ "foo", "bar", "baz", "biff" ] }';
+
+const TEST_URI = "data:text/html;charset=utf-8,Web Console network logging " +
+ "tests";
+
+const PAGE_REQUEST_PREDICATE =
+ ({ request }) => request.url.endsWith("test-network-request.html");
+
+const TEST_DATA_REQUEST_PREDICATE =
+ ({ request }) => request.url.endsWith("test-data.json");
+
+const XHR_WARN_REQUEST_PREDICATE =
+ ({ request }) => request.url.endsWith("sjs_cors-test-server.sjs");
+
+let hud;
+
+add_task(function*() {
+ const PREF = "devtools.webconsole.persistlog";
+ const NET_PREF = "devtools.webconsole.filter.networkinfo";
+ const NETXHR_PREF = "devtools.webconsole.filter.netxhr";
+ const MIXED_AC_PREF = "security.mixed_content.block_active_content";
+ let original = Services.prefs.getBoolPref(NET_PREF);
+ let originalXhr = Services.prefs.getBoolPref(NETXHR_PREF);
+ let originalMixedActive = Services.prefs.getBoolPref(MIXED_AC_PREF);
+ Services.prefs.setBoolPref(NET_PREF, true);
+ Services.prefs.setBoolPref(NETXHR_PREF, true);
+ Services.prefs.setBoolPref(MIXED_AC_PREF, false);
+ Services.prefs.setBoolPref(PREF, true);
+ registerCleanupFunction(() => {
+ Services.prefs.setBoolPref(NET_PREF, original);
+ Services.prefs.setBoolPref(NETXHR_PREF, originalXhr);
+ Services.prefs.setBoolPref(MIXED_AC_PREF, originalMixedActive);
+ Services.prefs.clearUserPref(PREF);
+ hud = null;
+ });
+
+ yield loadTab(TEST_URI);
+ hud = yield openConsole();
+
+ yield testPageLoad();
+ yield testXhrGet();
+ yield testXhrWarn();
+ yield testXhrPost();
+ yield testFormSubmission();
+ yield testLiveFilteringOnSearchStrings();
+});
+
+function testPageLoad() {
+
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_NETWORK_REQUEST_URI);
+ let lastRequest = yield waitForFinishedRequest(PAGE_REQUEST_PREDICATE);
+
+ // Check if page load was logged correctly.
+ ok(lastRequest, "Page load was logged");
+ is(lastRequest.request.url, TEST_NETWORK_REQUEST_URI,
+ "Logged network entry is page load");
+ is(lastRequest.request.method, "GET", "Method is correct");
+}
+
+function testXhrGet() {
+ // Start the XMLHttpRequest() GET test.
+ ContentTask.spawn(gBrowser.selectedBrowser, {}, function*() {
+ content.wrappedJSObject.testXhrGet();
+ });
+
+ let lastRequest = yield waitForFinishedRequest(TEST_DATA_REQUEST_PREDICATE);
+
+ ok(lastRequest, "testXhrGet() was logged");
+ is(lastRequest.request.method, "GET", "Method is correct");
+ ok(lastRequest.isXHR, "It's an XHR request");
+}
+
+function testXhrWarn() {
+ // Start the XMLHttpRequest() warn test.
+ ContentTask.spawn(gBrowser.selectedBrowser, {}, function*() {
+ content.wrappedJSObject.testXhrWarn();
+ });
+
+ let lastRequest = yield waitForFinishedRequest(XHR_WARN_REQUEST_PREDICATE);
+ if (lastRequest.request.method == "HEAD") {
+ // in non-e10s, we get the HEAD request that priming sends, so make sure
+ // a priming request should be sent, and then get the actual request
+ is(Services.prefs.getBoolPref("security.mixed_content.send_hsts_priming"),
+ true, "Found HSTS Priming Request");
+ lastRequest = yield waitForFinishedRequest(XHR_WARN_REQUEST_PREDICATE);
+ }
+
+ ok(lastRequest, "testXhrWarn() was logged");
+ is(lastRequest.request.method, "GET", "Method is correct");
+ ok(lastRequest.isXHR, "It's an XHR request");
+ is(lastRequest.securityInfo, "insecure", "It's an insecure request");
+}
+
+function testXhrPost() {
+ // Start the XMLHttpRequest() POST test.
+ ContentTask.spawn(gBrowser.selectedBrowser, {}, function*() {
+ content.wrappedJSObject.testXhrPost();
+ });
+
+ let lastRequest = yield waitForFinishedRequest(TEST_DATA_REQUEST_PREDICATE);
+
+ ok(lastRequest, "testXhrPost() was logged");
+ is(lastRequest.request.method, "POST", "Method is correct");
+ ok(lastRequest.isXHR, "It's an XHR request");
+}
+
+function testFormSubmission() {
+ // Start the form submission test. As the form is submitted, the page is
+ // loaded again. Bind to the load event to catch when this is done.
+ ContentTask.spawn(gBrowser.selectedBrowser, {}, function*() {
+ let form = content.document.querySelector("form");
+ ok(form, "we have the HTML form");
+ form.submit();
+ });
+
+ // The form POSTs to the page URL but over https (page over http).
+ let lastRequest = yield waitForFinishedRequest(PAGE_REQUEST_PREDICATE);
+
+ ok(lastRequest, "testFormSubmission() was logged");
+ is(lastRequest.request.method, "POST", "Method is correct");
+
+ // There should be 3 network requests pointing to the HTML file.
+ waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ text: "test-network-request.html",
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_LOG,
+ count: 3,
+ },
+ {
+ text: "test-data.json",
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_INFO,
+ isXhr: true,
+ count: 2,
+ },
+ {
+ text: "http://example.com/",
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_WARNING,
+ isXhr: true,
+ count: 1,
+ },
+ ],
+ });
+}
+
+function testLiveFilteringOnSearchStrings() {
+ setStringFilter("http");
+ isnot(countMessageNodes(), 0, "the log nodes are not hidden when the " +
+ "search string is set to \"http\"");
+
+ setStringFilter("HTTP");
+ isnot(countMessageNodes(), 0, "the log nodes are not hidden when the " +
+ "search string is set to \"HTTP\"");
+
+ setStringFilter("hxxp");
+ is(countMessageNodes(), 0, "the log nodes are hidden when the search " +
+ "string is set to \"hxxp\"");
+
+ setStringFilter("ht tp");
+ isnot(countMessageNodes(), 0, "the log nodes are not hidden when the " +
+ "search string is set to \"ht tp\"");
+
+ setStringFilter("");
+ isnot(countMessageNodes(), 0, "the log nodes are not hidden when the " +
+ "search string is removed");
+
+ setStringFilter("json");
+ is(countMessageNodes(), 2, "the log nodes show only the nodes with \"json\"");
+
+ setStringFilter("'foo'");
+ is(countMessageNodes(), 0, "the log nodes are hidden when searching for " +
+ "the string 'foo'");
+
+ setStringFilter("foo\"bar'baz\"boo'");
+ is(countMessageNodes(), 0, "the log nodes are hidden when searching for " +
+ "the string \"foo\"bar'baz\"boo'\"");
+}
+
+function countMessageNodes() {
+ let messageNodes = hud.outputNode.querySelectorAll(".message");
+ let displayedMessageNodes = 0;
+ let view = hud.iframeWindow;
+ for (let i = 0; i < messageNodes.length; i++) {
+ let computedStyle = view.getComputedStyle(messageNodes[i], null);
+ if (computedStyle.display !== "none") {
+ displayedMessageNodes++;
+ }
+ }
+
+ return displayedMessageNodes;
+}
+
+function setStringFilter(value) {
+ hud.ui.filterBox.value = value;
+ hud.ui.adjustVisibilityOnSearchStringChange();
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_642108_pruneTest.js b/devtools/client/webconsole/test/browser_webconsole_bug_642108_pruneTest.js
new file mode 100644
index 000000000..caaa73628
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_642108_pruneTest.js
@@ -0,0 +1,81 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests that the Web Console limits the number of lines displayed according to
+// the user's preferences.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8,<p>test for bug 642108.";
+const LOG_LIMIT = 20;
+
+function test() {
+ let hud;
+
+ Task.spawn(runner).then(finishTest);
+
+ function* runner() {
+ let {tab} = yield loadTab(TEST_URI);
+
+ Services.prefs.setIntPref("devtools.hud.loglimit.cssparser", LOG_LIMIT);
+ Services.prefs.setBoolPref("devtools.webconsole.filter.cssparser", true);
+
+ registerCleanupFunction(function () {
+ Services.prefs.clearUserPref("devtools.hud.loglimit.cssparser");
+ Services.prefs.clearUserPref("devtools.webconsole.filter.cssparser");
+ });
+
+ hud = yield openConsole(tab);
+
+ for (let i = 0; i < 5; i++) {
+ logCSSMessage("css log x");
+ }
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "css log x",
+ category: CATEGORY_CSS,
+ severity: SEVERITY_WARNING,
+ repeats: 5,
+ }],
+ });
+
+ for (let i = 0; i < LOG_LIMIT + 5; i++) {
+ logCSSMessage("css log " + i);
+ }
+
+ let [result] = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "css log 5",
+ category: CATEGORY_CSS,
+ severity: SEVERITY_WARNING,
+ },
+ {
+ // LOG_LIMIT + 5
+ text: "css log 24",
+ category: CATEGORY_CSS,
+ severity: SEVERITY_WARNING,
+ }],
+ });
+
+ is(hud.ui.outputNode.querySelectorAll(".message").length, LOG_LIMIT,
+ "number of messages");
+
+ is(Object.keys(hud.ui._repeatNodes).length, LOG_LIMIT,
+ "repeated nodes pruned from repeatNodes");
+
+ let msg = [...result.matched][0];
+ let repeats = msg.querySelector(".message-repeats");
+ is(repeats.getAttribute("value"), 1,
+ "repeated nodes pruned from repeatNodes (confirmed)");
+ }
+
+ function logCSSMessage(msg) {
+ let node = hud.ui.createMessageNode(CATEGORY_CSS, SEVERITY_WARNING, msg);
+ hud.ui.outputMessage(CATEGORY_CSS, node);
+ }
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_644419_log_limits.js b/devtools/client/webconsole/test/browser_webconsole_bug_644419_log_limits.js
new file mode 100644
index 000000000..93063e436
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_644419_log_limits.js
@@ -0,0 +1,235 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests that the Web Console limits the number of lines displayed according to
+// the limit set for each category.
+
+"use strict";
+
+const INIT_URI = "data:text/html;charset=utf-8,Web Console test for " +
+ "bug 644419: Console should " +
+ "have user-settable log limits for each message category";
+
+const TEST_URI = "http://example.com/browser/devtools/client/" +
+ "webconsole/test/test-bug-644419-log-limits.html";
+
+var hud, outputNode;
+
+add_task(function* () {
+ let { browser } = yield loadTab(INIT_URI);
+
+ hud = yield openConsole();
+
+ hud.jsterm.clearOutput();
+ outputNode = hud.outputNode;
+
+ let loaded = loadBrowser(browser);
+
+ // On e10s, the exception is triggered in child process
+ // and is ignored by test harness
+ if (!Services.appinfo.browserTabsRemoteAutostart) {
+ expectUncaughtException();
+ }
+
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_URI);
+ yield loaded;
+
+ yield testWebDevLimits();
+ yield testWebDevLimits2();
+ yield testJsLimits();
+ yield testJsLimits2();
+
+ yield testNetLimits();
+ yield loadImage();
+ yield testCssLimits();
+ yield testCssLimits2();
+
+ hud = outputNode = null;
+});
+
+function testWebDevLimits() {
+ Services.prefs.setIntPref("devtools.hud.loglimit.console", 10);
+
+ // Find the sentinel entry.
+ return waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "bar is not defined",
+ category: CATEGORY_JS,
+ severity: SEVERITY_ERROR,
+ }],
+ });
+}
+
+function testWebDevLimits2() {
+ // Fill the log with Web Developer errors.
+ for (let i = 0; i < 11; i++) {
+ content.console.log("test message " + i);
+ }
+
+ return waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "test message 10",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ }).then(() => {
+ testLogEntry(outputNode, "test message 0", "first message is pruned",
+ false, true);
+ findLogEntry("test message 1");
+ // Check if the sentinel entry is still there.
+ findLogEntry("bar is not defined");
+
+ Services.prefs.clearUserPref("devtools.hud.loglimit.console");
+ });
+}
+
+function testJsLimits() {
+ Services.prefs.setIntPref("devtools.hud.loglimit.exception", 10);
+
+ hud.jsterm.clearOutput();
+ content.console.log("testing JS limits");
+
+ // Find the sentinel entry.
+ return waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "testing JS limits",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+}
+
+function testJsLimits2() {
+ // Fill the log with JS errors.
+ let head = content.document.getElementsByTagName("head")[0];
+ for (let i = 0; i < 11; i++) {
+ let script = content.document.createElement("script");
+ script.text = "fubar" + i + ".bogus(6);";
+
+ if (!Services.appinfo.browserTabsRemoteAutostart) {
+ expectUncaughtException();
+ }
+ head.insertBefore(script, head.firstChild);
+ }
+
+ return waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "fubar10 is not defined",
+ category: CATEGORY_JS,
+ severity: SEVERITY_ERROR,
+ }],
+ }).then(() => {
+ testLogEntry(outputNode, "fubar0 is not defined", "first message is pruned",
+ false, true);
+ findLogEntry("fubar1 is not defined");
+ // Check if the sentinel entry is still there.
+ findLogEntry("testing JS limits");
+
+ Services.prefs.clearUserPref("devtools.hud.loglimit.exception");
+ });
+}
+
+var gCounter, gImage;
+
+function testNetLimits() {
+ Services.prefs.setIntPref("devtools.hud.loglimit.network", 10);
+
+ hud.jsterm.clearOutput();
+ content.console.log("testing Net limits");
+
+ // Find the sentinel entry.
+ return waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "testing Net limits",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ }).then(() => {
+ // Fill the log with network messages.
+ gCounter = 0;
+ });
+}
+
+function loadImage() {
+ if (gCounter < 11) {
+ let body = content.document.getElementsByTagName("body")[0];
+ gImage && gImage.removeEventListener("load", loadImage, true);
+ gImage = content.document.createElement("img");
+ gImage.src = "test-image.png?_fubar=" + gCounter;
+ body.insertBefore(gImage, body.firstChild);
+ gImage.addEventListener("load", loadImage, true);
+ gCounter++;
+ return true;
+ }
+
+ is(gCounter, 11, "loaded 11 files");
+
+ return waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "test-image.png",
+ url: "test-image.png?_fubar=10",
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_LOG,
+ }],
+ }).then(() => {
+ let msgs = outputNode.querySelectorAll(".message[category=network]");
+ is(msgs.length, 10, "number of network messages");
+ isnot(msgs[0].url.indexOf("fubar=1"), -1, "first network message");
+ isnot(msgs[1].url.indexOf("fubar=2"), -1, "second network message");
+ findLogEntry("testing Net limits");
+
+ Services.prefs.clearUserPref("devtools.hud.loglimit.network");
+ });
+}
+
+function testCssLimits() {
+ Services.prefs.setIntPref("devtools.hud.loglimit.cssparser", 10);
+
+ hud.jsterm.clearOutput();
+ content.console.log("testing CSS limits");
+
+ // Find the sentinel entry.
+ return waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "testing CSS limits",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+}
+
+function testCssLimits2() {
+ // Fill the log with CSS errors.
+ let body = content.document.getElementsByTagName("body")[0];
+ for (let i = 0; i < 11; i++) {
+ let div = content.document.createElement("div");
+ div.setAttribute("style", "-moz-foobar" + i + ": 42;");
+ body.insertBefore(div, body.firstChild);
+ }
+
+ return waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "-moz-foobar10",
+ category: CATEGORY_CSS,
+ severity: SEVERITY_WARNING,
+ }],
+ }).then(() => {
+ testLogEntry(outputNode, "Unknown property \u2018-moz-foobar0\u2019",
+ "first message is pruned", false, true);
+ findLogEntry("Unknown property \u2018-moz-foobar1\u2019");
+ // Check if the sentinel entry is still there.
+ findLogEntry("testing CSS limits");
+
+ Services.prefs.clearUserPref("devtools.hud.loglimit.cssparser");
+ });
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_646025_console_file_location.js b/devtools/client/webconsole/test/browser_webconsole_bug_646025_console_file_location.js
new file mode 100644
index 000000000..81573e56f
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_646025_console_file_location.js
@@ -0,0 +1,57 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests that console logging methods display the method location along with
+// the output in the console.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8,Web Console file location " +
+ "display test";
+const TEST_URI2 = "http://example.com/browser/devtools/client/" +
+ "webconsole/test/" +
+ "test-bug-646025-console-file-location.html";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_URI2);
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "message for level log",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ source: { url: "test-file-location.js", line: 8 },
+ },
+ {
+ text: "message for level info",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_INFO,
+ source: { url: "test-file-location.js", line: 9 },
+ },
+ {
+ text: "message for level warn",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_WARNING,
+ source: { url: "test-file-location.js", line: 10 },
+ },
+ {
+ text: "message for level error",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_ERROR,
+ source: { url: "test-file-location.js", line: 11 },
+ },
+ {
+ text: "message for level debug",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ source: { url: "test-file-location.js", line: 12 },
+ }],
+ });
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_651501_document_body_autocomplete.js b/devtools/client/webconsole/test/browser_webconsole_bug_651501_document_body_autocomplete.js
new file mode 100644
index 000000000..233643d51
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_651501_document_body_autocomplete.js
@@ -0,0 +1,102 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests that document.body autocompletes in the web console.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8,Web Console autocompletion " +
+ "bug in document.body";
+
+var gHUD;
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ gHUD = yield openConsole();
+
+ yield consoleOpened();
+ yield autocompletePopupHidden();
+ let view = yield testPropertyPanel();
+ yield onVariablesViewReady(view);
+
+ gHUD = null;
+});
+
+function consoleOpened() {
+ let deferred = promise.defer();
+
+ let jsterm = gHUD.jsterm;
+ let popup = jsterm.autocompletePopup;
+
+ ok(!popup.isOpen, "popup is not open");
+
+ popup.once("popup-opened", () => {
+ ok(popup.isOpen, "popup is open");
+
+ is(popup.itemCount, jsterm._autocompleteCache.length,
+ "popup.itemCount is correct");
+ isnot(jsterm._autocompleteCache.indexOf("addEventListener"), -1,
+ "addEventListener is in the list of suggestions");
+ isnot(jsterm._autocompleteCache.indexOf("bgColor"), -1,
+ "bgColor is in the list of suggestions");
+ isnot(jsterm._autocompleteCache.indexOf("ATTRIBUTE_NODE"), -1,
+ "ATTRIBUTE_NODE is in the list of suggestions");
+
+ popup.once("popup-closed", () => {
+ deferred.resolve();
+ });
+ EventUtils.synthesizeKey("VK_ESCAPE", {});
+ });
+
+ jsterm.setInputValue("document.body");
+ EventUtils.synthesizeKey(".", {});
+
+ return deferred.promise;
+}
+
+function autocompletePopupHidden() {
+ let deferred = promise.defer();
+
+ let jsterm = gHUD.jsterm;
+ let popup = jsterm.autocompletePopup;
+ let completeNode = jsterm.completeNode;
+
+ ok(!popup.isOpen, "popup is not open");
+
+ jsterm.once("autocomplete-updated", function () {
+ is(completeNode.value, testStr + "dy", "autocomplete shows document.body");
+ deferred.resolve();
+ });
+
+ let inputStr = "document.b";
+ jsterm.setInputValue(inputStr);
+ EventUtils.synthesizeKey("o", {});
+ let testStr = inputStr.replace(/./g, " ") + " ";
+
+ return deferred.promise;
+}
+
+function testPropertyPanel() {
+ let deferred = promise.defer();
+
+ let jsterm = gHUD.jsterm;
+ jsterm.clearOutput();
+ jsterm.execute("document", (msg) => {
+ jsterm.once("variablesview-fetched", (evt, view) => {
+ deferred.resolve(view);
+ });
+ let anchor = msg.querySelector(".message-body a");
+ EventUtils.synthesizeMouse(anchor, 2, 2, {}, gHUD.iframeWindow);
+ });
+
+ return deferred.promise;
+}
+
+function onVariablesViewReady(view) {
+ return findVariableViewProperties(view, [
+ { name: "body", value: "<body>" },
+ ], { webconsole: gHUD });
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_653531_highlighter_console_helper.js b/devtools/client/webconsole/test/browser_webconsole_bug_653531_highlighter_console_helper.js
new file mode 100644
index 000000000..217d481e2
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_653531_highlighter_console_helper.js
@@ -0,0 +1,109 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests that the $0 console helper works as intended.
+
+"use strict";
+
+var inspector, h1, outputNode;
+
+function createDocument() {
+ let doc = content.document;
+ let div = doc.createElement("div");
+ h1 = doc.createElement("h1");
+ let p1 = doc.createElement("p");
+ let p2 = doc.createElement("p");
+ let div2 = doc.createElement("div");
+ let p3 = doc.createElement("p");
+ doc.title = "Inspector Tree Selection Test";
+ h1.textContent = "Inspector Tree Selection Test";
+ p1.textContent = "This is some example text";
+ p2.textContent = "Lorem ipsum dolor sit amet, consectetur adipisicing " +
+ "elit, sed do eiusmod tempor incididunt ut labore et dolore magna " +
+ "aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco " +
+ "laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure " +
+ "dolor in reprehenderit in voluptate velit esse cillum dolore eu " +
+ "fugiat nulla pariatur. Excepteur sint occaecat cupidatat non " +
+ "proident, sunt in culpa qui officia deserunt mollit anim id est laborum.";
+ p3.textContent = "Lorem ipsum dolor sit amet, consectetur adipisicing " +
+ "elit, sed do eiusmod tempor incididunt ut labore et dolore magna " +
+ "aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco " +
+ "laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure " +
+ "dolor in reprehenderit in voluptate velit esse cillum dolore eu " +
+ "fugiat nulla pariatur. Excepteur sint occaecat cupidatat non " +
+ "proident, sunt in culpa qui officia deserunt mollit anim id est laborum.";
+ div.appendChild(h1);
+ div.appendChild(p1);
+ div.appendChild(p2);
+ div2.appendChild(p3);
+ doc.body.appendChild(div);
+ doc.body.appendChild(div2);
+ setupHighlighterTests();
+}
+
+function setupHighlighterTests() {
+ ok(h1, "we have the header node");
+ openInspector().then(runSelectionTests);
+}
+
+var runSelectionTests = Task.async(function* (aInspector) {
+ inspector = aInspector;
+
+ let onPickerStarted = inspector.toolbox.once("picker-started");
+ inspector.toolbox.highlighterUtils.startPicker();
+ yield onPickerStarted;
+
+ info("Picker mode started, now clicking on H1 to select that node");
+ h1.scrollIntoView();
+ let onPickerStopped = inspector.toolbox.once("picker-stopped");
+ let onInspectorUpdated = inspector.once("inspector-updated");
+ EventUtils.synthesizeMouseAtCenter(h1, {}, content);
+ yield onPickerStopped;
+ yield onInspectorUpdated;
+
+ info("Picker mode stopped, H1 selected, now switching to the console");
+ let hud = yield openConsole(gBrowser.selectedTab);
+
+ performWebConsoleTests(hud);
+});
+
+function performWebConsoleTests(hud) {
+ let jsterm = hud.jsterm;
+ outputNode = hud.outputNode;
+
+ jsterm.clearOutput();
+ jsterm.execute("$0", onNodeOutput);
+
+ function onNodeOutput(node) {
+ isnot(node.textContent.indexOf("<h1>"), -1, "correct output for $0");
+
+ jsterm.clearOutput();
+ jsterm.execute("$0.textContent = 'bug653531'", onNodeUpdate);
+ }
+
+ function onNodeUpdate(node) {
+ isnot(node.textContent.indexOf("bug653531"), -1,
+ "correct output for $0.textContent");
+ is(inspector.selection.node.textContent, "bug653531",
+ "node successfully updated");
+
+ inspector = h1 = outputNode = null;
+ gBrowser.removeCurrentTab();
+ finishTest();
+ }
+}
+
+function test() {
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
+ waitForFocus(createDocument, content);
+ }, true);
+
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser,
+ "data:text/html;charset=utf-8,test for highlighter helper in web console");
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_658368_time_methods.js b/devtools/client/webconsole/test/browser_webconsole_bug_658368_time_methods.js
new file mode 100644
index 000000000..2c6c933fc
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_658368_time_methods.js
@@ -0,0 +1,67 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests that the Console API implements the time() and timeEnd() methods.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-bug-658368-time-methods.html";
+
+const TEST_URI2 = "data:text/html;charset=utf-8,<script>" +
+ "console.timeEnd('bTimer');</script>";
+
+const TEST_URI3 = "data:text/html;charset=utf-8,<script>" +
+ "console.time('bTimer');</script>";
+
+const TEST_URI4 = "data:text/html;charset=utf-8," +
+ "<script>console.timeEnd('bTimer');</script>";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud1 = yield openConsole();
+
+ yield waitForMessages({
+ webconsole: hud1,
+ messages: [{
+ name: "aTimer started",
+ consoleTime: "aTimer",
+ }, {
+ name: "aTimer end",
+ consoleTimeEnd: "aTimer",
+ }],
+ });
+
+ // The next test makes sure that timers with the same name but in separate
+ // tabs, do not contain the same value.
+ let { browser } = yield loadTab(TEST_URI2);
+ let hud2 = yield openConsole();
+
+ testLogEntry(hud2.outputNode, "bTimer: timer started",
+ "bTimer was not started", false, true);
+
+ // The next test makes sure that timers with the same name but in separate
+ // pages, do not contain the same value.
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_URI3);
+
+ yield waitForMessages({
+ webconsole: hud2,
+ messages: [{
+ name: "bTimer started",
+ consoleTime: "bTimer",
+ }],
+ });
+
+ hud2.jsterm.clearOutput();
+
+ // Now the following console.timeEnd() call shouldn't display anything,
+ // if the timers in different pages are not related.
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_URI4);
+ yield loadBrowser(browser);
+
+ testLogEntry(hud2.outputNode, "bTimer: timer started",
+ "bTimer was not started", false, true);
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_659907_console_dir.js b/devtools/client/webconsole/test/browser_webconsole_bug_659907_console_dir.js
new file mode 100644
index 000000000..03741a249
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_659907_console_dir.js
@@ -0,0 +1,36 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests that console.dir works as intended.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8,Web Console test for " +
+ "bug 659907: Expand console object with a dir method";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+ hud.jsterm.clearOutput();
+
+ hud.jsterm.execute("console.dir(document)");
+
+ let varView = yield hud.jsterm.once("variablesview-fetched");
+
+ yield findVariableViewProperties(varView, [
+ {
+ name: "__proto__.__proto__.querySelectorAll",
+ value: "querySelectorAll()"
+ },
+ {
+ name: "location",
+ value: /Location \u2192 data:Web/
+ },
+ {
+ name: "__proto__.write",
+ value: "write()"
+ },
+ ], { webconsole: hud });
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_660806_history_nav.js b/devtools/client/webconsole/test/browser_webconsole_bug_660806_history_nav.js
new file mode 100644
index 000000000..5906d62d6
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_660806_history_nav.js
@@ -0,0 +1,54 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 TEST_URI = "data:text/html;charset=utf-8,<p>bug 660806 - history " +
+ "navigation must not show the autocomplete popup";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ yield consoleOpened(hud);
+});
+
+function consoleOpened(HUD) {
+ let deferred = promise.defer();
+
+ let jsterm = HUD.jsterm;
+ let popup = jsterm.autocompletePopup;
+ let onShown = function () {
+ ok(false, "popup shown");
+ };
+
+ jsterm.execute(`window.foobarBug660806 = {
+ 'location': 'value0',
+ 'locationbar': 'value1'
+ }`);
+
+ popup.on("popup-opened", onShown);
+
+ ok(!popup.isOpen, "popup is not open");
+
+ ok(!jsterm.lastInputValue, "no lastInputValue");
+ jsterm.setInputValue("window.foobarBug660806.location");
+ is(jsterm.lastInputValue, "window.foobarBug660806.location",
+ "lastInputValue is correct");
+
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ EventUtils.synthesizeKey("VK_UP", {});
+
+ is(jsterm.lastInputValue, "window.foobarBug660806.location",
+ "lastInputValue is correct, again");
+
+ executeSoon(function () {
+ ok(!popup.isOpen, "popup is not open");
+ popup.off("popup-opened", onShown);
+ executeSoon(deferred.resolve);
+ });
+ return deferred.promise;
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_664131_console_group.js b/devtools/client/webconsole/test/browser_webconsole_bug_664131_console_group.js
new file mode 100644
index 000000000..fd510240e
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_664131_console_group.js
@@ -0,0 +1,79 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests that console.group/groupEnd works as intended.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8,Web Console test for " +
+ "bug 664131: Expand console object with group methods";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+ let jsterm = hud.jsterm;
+
+ hud.jsterm.clearOutput();
+
+ yield jsterm.execute("console.group('bug664131a')");
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "bug664131a",
+ consoleGroup: 1,
+ }],
+ });
+
+ yield jsterm.execute("console.log('bug664131a-inside')");
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "bug664131a-inside",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ groupDepth: 1,
+ }],
+ });
+
+ yield jsterm.execute('console.groupEnd("bug664131a")');
+ yield jsterm.execute('console.log("bug664131-outside")');
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "bug664131-outside",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ groupDepth: 0,
+ }],
+ });
+
+ yield jsterm.execute('console.groupCollapsed("bug664131b")');
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "bug664131b",
+ consoleGroup: 1,
+ }],
+ });
+
+ // Test that clearing the console removes the indentation.
+ hud.jsterm.clearOutput();
+ yield jsterm.execute('console.log("bug664131-cleared")');
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "bug664131-cleared",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ groupDepth: 0,
+ }],
+ });
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_686937_autocomplete_JSTerm_helpers.js b/devtools/client/webconsole/test/browser_webconsole_bug_686937_autocomplete_JSTerm_helpers.js
new file mode 100644
index 000000000..2b1588ef9
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_686937_autocomplete_JSTerm_helpers.js
@@ -0,0 +1,75 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests that the autocompletion results contain the names of JSTerm helpers.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf8,<p>test JSTerm Helpers " +
+ "autocomplete";
+
+var jsterm;
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ jsterm = hud.jsterm;
+ let input = jsterm.inputNode;
+ let popup = jsterm.autocompletePopup;
+
+ // Test if 'i' gives 'inspect'
+ input.value = "i";
+ input.setSelectionRange(1, 1);
+ yield complete(jsterm.COMPLETE_HINT_ONLY);
+
+ let newItems = popup.getItems().map(function (e) {
+ return e.label;
+ });
+ ok(newItems.indexOf("inspect") > -1,
+ "autocomplete results contain helper 'inspect'");
+
+ // Test if 'window.' does not give 'inspect'.
+ input.value = "window.";
+ input.setSelectionRange(7, 7);
+ yield complete(jsterm.COMPLETE_HINT_ONLY);
+
+ newItems = popup.getItems().map(function (e) {
+ return e.label;
+ });
+ is(newItems.indexOf("inspect"), -1,
+ "autocomplete results do not contain helper 'inspect'");
+
+ // Test if 'dump(i' gives 'inspect'
+ input.value = "dump(i";
+ input.setSelectionRange(6, 6);
+ yield complete(jsterm.COMPLETE_HINT_ONLY);
+
+ newItems = popup.getItems().map(function (e) {
+ return e.label;
+ });
+ ok(newItems.indexOf("inspect") > -1,
+ "autocomplete results contain helper 'inspect'");
+
+ // Test if 'window.dump(i' gives 'inspect'
+ input.value = "window.dump(i";
+ input.setSelectionRange(13, 13);
+ yield complete(jsterm.COMPLETE_HINT_ONLY);
+
+ newItems = popup.getItems().map(function (e) {
+ return e.label;
+ });
+ ok(newItems.indexOf("inspect") > -1,
+ "autocomplete results contain helper 'inspect'");
+
+ jsterm = null;
+});
+
+function complete(type) {
+ let updated = jsterm.once("autocomplete-updated");
+ jsterm.complete(type);
+ return updated;
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_704295.js b/devtools/client/webconsole/test/browser_webconsole_bug_704295.js
new file mode 100644
index 000000000..df21232cf
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_704295.js
@@ -0,0 +1,41 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests for bug 704295
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console.html";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ testCompletion(hud);
+});
+
+function testCompletion(hud) {
+ let jsterm = hud.jsterm;
+ let input = jsterm.inputNode;
+
+ // Test typing 'var d = 5;' and press RETURN
+ jsterm.setInputValue("var d = ");
+ EventUtils.synthesizeKey("5", {});
+ EventUtils.synthesizeKey(";", {});
+ is(input.value, "var d = 5;", "var d = 5;");
+ is(jsterm.completeNode.value, "", "no completion");
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ is(jsterm.completeNode.value, "", "clear completion on execute()");
+
+ // Test typing 'var a = d' and press RETURN
+ jsterm.setInputValue("var a = ");
+ EventUtils.synthesizeKey("d", {});
+ is(input.value, "var a = d", "var a = d");
+ is(jsterm.completeNode.value, "", "no completion");
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ is(jsterm.completeNode.value, "", "clear completion on execute()");
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_734061_No_input_change_and_Tab_key_pressed.js b/devtools/client/webconsole/test/browser_webconsole_bug_734061_No_input_change_and_Tab_key_pressed.js
new file mode 100644
index 000000000..af9e172c8
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_734061_No_input_change_and_Tab_key_pressed.js
@@ -0,0 +1,35 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/browser/test-console.html";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ let jsterm = hud.jsterm;
+ let input = jsterm.inputNode;
+
+ is(input.getAttribute("focused"), "true", "input has focus");
+ EventUtils.synthesizeKey("VK_TAB", {});
+ is(input.getAttribute("focused"), "", "focus moved away");
+
+ // Test user changed something
+ input.focus();
+ EventUtils.synthesizeKey("A", {});
+ EventUtils.synthesizeKey("VK_TAB", {});
+ is(input.getAttribute("focused"), "true", "input is still focused");
+
+ // Test non empty input but not changed since last focus
+ input.blur();
+ input.focus();
+ EventUtils.synthesizeKey("VK_RIGHT", {});
+ EventUtils.synthesizeKey("VK_TAB", {});
+ is(input.getAttribute("focused"), "", "input moved away");
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_737873_mixedcontent.js b/devtools/client/webconsole/test/browser_webconsole_bug_737873_mixedcontent.js
new file mode 100644
index 000000000..4665af42a
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_737873_mixedcontent.js
@@ -0,0 +1,63 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests that the Web Console Mixed Content messages are displayed
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf8,Web Console mixed content test";
+const TEST_HTTPS_URI = "https://example.com/browser/devtools/client/" +
+ "webconsole/test/test-bug-737873-mixedcontent.html";
+const LEARN_MORE_URI = "https://developer.mozilla.org/docs/Web/Security/" +
+ "Mixed_content";
+
+registerCleanupFunction(function*() {
+ Services.prefs.clearUserPref("security.mixed_content.block_display_content");
+ Services.prefs.clearUserPref("security.mixed_content.block_active_content");
+});
+
+add_task(function* () {
+ Services.prefs.setBoolPref("security.mixed_content.block_display_content",
+ false);
+ Services.prefs.setBoolPref("security.mixed_content.block_active_content",
+ false);
+
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ yield testMixedContent(hud);
+});
+
+var testMixedContent = Task.async(function* (hud) {
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_HTTPS_URI);
+
+ let results = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "example.com",
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_WARNING,
+ }],
+ });
+
+ let msg = [...results[0].matched][0];
+ ok(msg, "page load logged");
+ ok(msg.classList.contains("mixed-content"), ".mixed-content element");
+
+ let link = msg.querySelector(".learn-more-link");
+ ok(link, "mixed content link element");
+ is(link.textContent, "[Mixed Content]", "link text is accurate");
+
+ yield simulateMessageLinkClick(link, LEARN_MORE_URI);
+
+ ok(!msg.classList.contains("filtered-by-type"), "message is not filtered");
+
+ hud.setFilterState("netwarn", false);
+
+ ok(msg.classList.contains("filtered-by-type"), "message is filtered");
+
+ hud.setFilterState("netwarn", true);
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_752559_ineffective_iframe_sandbox_warning.js b/devtools/client/webconsole/test/browser_webconsole_bug_752559_ineffective_iframe_sandbox_warning.js
new file mode 100644
index 000000000..85b99a79a
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_752559_ineffective_iframe_sandbox_warning.js
@@ -0,0 +1,83 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests that warnings about ineffective iframe sandboxing are logged to the
+// web console when necessary (and not otherwise).
+
+"use strict";
+
+requestLongerTimeout(2);
+
+const TEST_URI_WARNING = "http://example.com/browser/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning0.html";
+const TEST_URI_NOWARNING = [
+ "http://example.com/browser/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning1.html",
+ "http://example.com/browser/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning2.html",
+ "http://example.com/browser/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning3.html",
+ "http://example.com/browser/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning4.html",
+ "http://example.com/browser/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning5.html"
+];
+
+const INEFFECTIVE_IFRAME_SANDBOXING_MSG = "An iframe which has both " +
+ "allow-scripts and allow-same-origin for its sandbox attribute can remove " +
+ "its sandboxing.";
+const SENTINEL_MSG = "testing ineffective sandboxing message";
+
+add_task(function* () {
+ yield testYesWarning();
+
+ for (let id = 0; id < TEST_URI_NOWARNING.length; id++) {
+ yield testNoWarning(id);
+ }
+});
+
+function* testYesWarning() {
+ yield loadTab(TEST_URI_WARNING);
+ let hud = yield openConsole();
+
+ ContentTask.spawn(gBrowser.selectedBrowser, SENTINEL_MSG, function* (msg) {
+ content.console.log(msg);
+ });
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ name: "Ineffective iframe sandboxing warning displayed successfully",
+ text: INEFFECTIVE_IFRAME_SANDBOXING_MSG,
+ category: CATEGORY_SECURITY,
+ severity: SEVERITY_WARNING
+ },
+ {
+ text: SENTINEL_MSG,
+ severity: SEVERITY_LOG
+ }
+ ]
+ });
+
+ let msgs = hud.outputNode.querySelectorAll(".message[category=security]");
+ is(msgs.length, 1, "one security message");
+}
+
+function* testNoWarning(id) {
+ yield loadTab(TEST_URI_NOWARNING[id]);
+ let hud = yield openConsole();
+
+ ContentTask.spawn(gBrowser.selectedBrowser, SENTINEL_MSG, function* (msg) {
+ content.console.log(msg);
+ });
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ text: SENTINEL_MSG,
+ severity: SEVERITY_LOG
+ }
+ ]
+ });
+
+ let msgs = hud.outputNode.querySelectorAll(".message[category=security]");
+ is(msgs.length, 0, "no security messages (case " + id + ")");
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_762593_insecure_passwords_about_blank_web_console_warning.js b/devtools/client/webconsole/test/browser_webconsole_bug_762593_insecure_passwords_about_blank_web_console_warning.js
new file mode 100644
index 000000000..49df4d1fc
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_762593_insecure_passwords_about_blank_web_console_warning.js
@@ -0,0 +1,32 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests that errors about insecure passwords are logged to the web console.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-bug-762593-insecure-passwords-about-blank-web-console-warning.html";
+const INSECURE_PASSWORD_MSG = "Password fields present on an insecure " +
+ "(http://) iframe. This is a security risk that allows user login " +
+ "credentials to be stolen.";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ name: "Insecure password error displayed successfully",
+ text: INSECURE_PASSWORD_MSG,
+ category: CATEGORY_SECURITY,
+ severity: SEVERITY_WARNING
+ },
+ ],
+ });
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_762593_insecure_passwords_web_console_warning.js b/devtools/client/webconsole/test/browser_webconsole_bug_762593_insecure_passwords_web_console_warning.js
new file mode 100644
index 000000000..00a620fc8
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_762593_insecure_passwords_web_console_warning.js
@@ -0,0 +1,62 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+ // Tests that errors about insecure passwords are logged to the web console.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-bug-762593-insecure-passwords-web-" +
+ "console-warning.html";
+const INSECURE_PASSWORD_MSG = "Password fields present on an insecure " +
+ "(http://) page. This is a security risk that allows user " +
+ "login credentials to be stolen.";
+const INSECURE_FORM_ACTION_MSG = "Password fields present in a form with an " +
+ "insecure (http://) form action. This is a security risk " +
+ "that allows user login credentials to be stolen.";
+const INSECURE_IFRAME_MSG = "Password fields present on an insecure " +
+ "(http://) iframe. This is a security risk that allows " +
+ "user login credentials to be stolen.";
+const INSECURE_PASSWORDS_URI = "https://developer.mozilla.org/docs/Web/" +
+ "Security/Insecure_passwords" + DOCS_GA_PARAMS;
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ let result = yield waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ name: "Insecure password error displayed successfully",
+ text: INSECURE_PASSWORD_MSG,
+ category: CATEGORY_SECURITY,
+ severity: SEVERITY_WARNING
+ },
+ {
+ name: "Insecure iframe error displayed successfully",
+ text: INSECURE_IFRAME_MSG,
+ category: CATEGORY_SECURITY,
+ severity: SEVERITY_WARNING
+ },
+ {
+ name: "Insecure form action error displayed successfully",
+ text: INSECURE_FORM_ACTION_MSG,
+ category: CATEGORY_SECURITY,
+ severity: SEVERITY_WARNING
+ },
+ ],
+ });
+
+ yield testClickOpenNewTab(hud, result);
+});
+
+function testClickOpenNewTab(hud, [result]) {
+ let msg = [...result.matched][0];
+ let warningNode = msg.querySelector(".learn-more-link");
+ ok(warningNode, "learn more link");
+ return simulateMessageLinkClick(warningNode, INSECURE_PASSWORDS_URI);
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_764572_output_open_url.js b/devtools/client/webconsole/test/browser_webconsole_bug_764572_output_open_url.js
new file mode 100644
index 000000000..731e79d8b
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_764572_output_open_url.js
@@ -0,0 +1,142 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// This is a test for the Open URL context menu item
+// that is shown for network requests
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console.html";
+const COMMAND_NAME = "consoleCmd_openURL";
+const CONTEXT_MENU_ID = "#menu_openURL";
+
+var HUD = null, outputNode = null, contextMenu = null;
+
+add_task(function* () {
+ Services.prefs.setBoolPref("devtools.webconsole.filter.networkinfo", true);
+
+ yield loadTab(TEST_URI);
+ HUD = yield openConsole();
+
+ let results = yield consoleOpened();
+ yield onConsoleMessage(results);
+
+ let results2 = yield testOnNetActivity();
+ let msg = yield onNetworkMessage(results2);
+
+ yield testOnNetActivityContextMenu(msg);
+
+ Services.prefs.clearUserPref("devtools.webconsole.filter.networkinfo");
+
+ HUD = null;
+ outputNode = null;
+ contextMenu = null;
+});
+
+function consoleOpened() {
+ outputNode = HUD.outputNode;
+ contextMenu = HUD.iframeWindow.document.getElementById("output-contextmenu");
+
+ HUD.jsterm.clearOutput();
+
+ content.console.log("bug 764572");
+
+ return waitForMessages({
+ webconsole: HUD,
+ messages: [{
+ text: "bug 764572",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+}
+
+function onConsoleMessage(results) {
+ outputNode.focus();
+ outputNode.selectedItem = [...results[0].matched][0];
+
+ // Check if the command is disabled non-network messages.
+ goUpdateCommand(COMMAND_NAME);
+ let controller = top.document.commandDispatcher
+ .getControllerForCommand(COMMAND_NAME);
+
+ let isDisabled = !controller || !controller.isCommandEnabled(COMMAND_NAME);
+ ok(isDisabled, COMMAND_NAME + " should be disabled.");
+
+ return waitForContextMenu(contextMenu, outputNode.selectedItem, () => {
+ let isHidden = contextMenu.querySelector(CONTEXT_MENU_ID).hidden;
+ ok(isHidden, CONTEXT_MENU_ID + " should be hidden.");
+ });
+}
+
+function testOnNetActivity() {
+ HUD.jsterm.clearOutput();
+
+ // Reload the url to show net activity in console.
+ content.location.reload();
+
+ return waitForMessages({
+ webconsole: HUD,
+ messages: [{
+ text: "test-console.html",
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_LOG,
+ }],
+ });
+}
+
+function onNetworkMessage(results) {
+ let deferred = promise.defer();
+
+ outputNode.focus();
+ let msg = [...results[0].matched][0];
+ ok(msg, "network message");
+ HUD.ui.output.selectMessage(msg);
+
+ let currentTab = gBrowser.selectedTab;
+ let newTab = null;
+
+ gBrowser.tabContainer.addEventListener("TabOpen", function onOpen(evt) {
+ gBrowser.tabContainer.removeEventListener("TabOpen", onOpen, true);
+ newTab = evt.target;
+ newTab.linkedBrowser.addEventListener("load", onTabLoaded, true);
+ }, true);
+
+ function onTabLoaded() {
+ newTab.linkedBrowser.removeEventListener("load", onTabLoaded, true);
+ gBrowser.removeTab(newTab);
+ gBrowser.selectedTab = currentTab;
+ executeSoon(deferred.resolve.bind(null, msg));
+ }
+
+ // Check if the command is enabled for a network message.
+ goUpdateCommand(COMMAND_NAME);
+ let controller = top.document.commandDispatcher
+ .getControllerForCommand(COMMAND_NAME);
+ ok(controller.isCommandEnabled(COMMAND_NAME),
+ COMMAND_NAME + " should be enabled.");
+
+ // Try to open the URL.
+ goDoCommand(COMMAND_NAME);
+
+ return deferred.promise;
+}
+
+function testOnNetActivityContextMenu(msg) {
+ let deferred = promise.defer();
+
+ outputNode.focus();
+ HUD.ui.output.selectMessage(msg);
+
+ info("net activity context menu");
+
+ waitForContextMenu(contextMenu, msg, () => {
+ let isShown = !contextMenu.querySelector(CONTEXT_MENU_ID).hidden;
+ ok(isShown, CONTEXT_MENU_ID + " should be shown.");
+ }).then(deferred.resolve);
+
+ return deferred.promise;
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_766001_JS_Console_in_Debugger.js b/devtools/client/webconsole/test/browser_webconsole_bug_766001_JS_Console_in_Debugger.js
new file mode 100644
index 000000000..3686fba89
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_766001_JS_Console_in_Debugger.js
@@ -0,0 +1,88 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Test that message source links for js errors and console API calls open in
+// the jsdebugger when clicked.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/test" +
+ "/test-bug-766001-js-console-links.html";
+
+// Force the new debugger UI, in case this gets uplifted with the old
+// debugger still turned on
+Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", true);
+registerCleanupFunction(function* () {
+ Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend");
+});
+
+function test() {
+ let hud;
+
+ requestLongerTimeout(2);
+ Task.spawn(runner).then(finishTest);
+
+ function* runner() {
+ // On e10s, the exception is triggered in child process
+ // and is ignored by test harness
+ if (!Services.appinfo.browserTabsRemoteAutostart) {
+ expectUncaughtException();
+ }
+
+ let {tab} = yield loadTab(TEST_URI);
+ hud = yield openConsole(tab);
+
+ let [exceptionRule, consoleRule] = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "document.bar",
+ category: CATEGORY_JS,
+ severity: SEVERITY_ERROR,
+ },
+ {
+ text: "Blah Blah",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+
+ let exceptionMsg = [...exceptionRule.matched][0];
+ let consoleMsg = [...consoleRule.matched][0];
+ let nodes = [exceptionMsg.querySelector(".message-location > .frame-link"),
+ consoleMsg.querySelector(".message-location > .frame-link")];
+ ok(nodes[0], ".location node for the exception message");
+ ok(nodes[1], ".location node for the console message");
+
+ for (let i = 0; i < nodes.length; i++) {
+ yield checkClickOnNode(i, nodes[i]);
+ yield gDevTools.showToolbox(hud.target, "webconsole");
+ }
+
+ // check again the first node.
+ yield checkClickOnNode(0, nodes[0]);
+ }
+
+ function* checkClickOnNode(index, node) {
+ info("checking click on node index " + index);
+
+ let url = node.getAttribute("data-url");
+ ok(url, "source url found for index " + index);
+
+ let line = node.getAttribute("data-line");
+ ok(line, "found source line for index " + index);
+
+ executeSoon(() => {
+ EventUtils.sendMouseEvent({ type: "click" }, node.querySelector(".frame-link-filename"));
+ });
+
+ yield hud.ui.once("source-in-debugger-opened");
+
+ let toolbox = yield gDevTools.getToolbox(hud.target);
+ let dbg = toolbox.getPanel("jsdebugger");
+ is(dbg._selectors().getSelectedSource(dbg._getState()).get("url"),
+ url,
+ "expected source url");
+ }
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_770099_violation.js b/devtools/client/webconsole/test/browser_webconsole_bug_770099_violation.js
new file mode 100644
index 000000000..3a7134202
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_770099_violation.js
@@ -0,0 +1,35 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests that the Web Console CSP messages are displayed
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf8,Web Console CSP violation test";
+const TEST_VIOLATION = "https://example.com/browser/devtools/client/" +
+ "webconsole/test/test_bug_770099_violation.html";
+const CSP_VIOLATION_MSG = "Content Security Policy: The page\u2019s settings " +
+ "blocked the loading of a resource at " +
+ "http://some.example.com/test.png (\u201cdefault-src " +
+ "https://example.com\u201d).";
+
+add_task(function* () {
+ let { browser } = yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ hud.jsterm.clearOutput();
+
+ let loaded = loadBrowser(browser);
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_VIOLATION);
+ yield loaded;
+
+ yield waitForSuccess({
+ name: "CSP policy URI warning displayed successfully",
+ validator: function () {
+ return hud.outputNode.textContent.indexOf(CSP_VIOLATION_MSG) > -1;
+ }
+ });
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_782653_CSS_links_in_Style_Editor.js b/devtools/client/webconsole/test/browser_webconsole_bug_782653_CSS_links_in_Style_Editor.js
new file mode 100644
index 000000000..f2efd7922
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_782653_CSS_links_in_Style_Editor.js
@@ -0,0 +1,140 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 TEST_URI = "http://example.com/browser/devtools/client/webconsole/test" +
+ "/test-bug-782653-css-errors.html";
+
+var nodes, hud, StyleEditorUI;
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ hud = yield openConsole();
+
+ let styleEditor = yield testViewSource();
+ yield onStyleEditorReady(styleEditor);
+
+ nodes = hud = StyleEditorUI = null;
+});
+
+function testViewSource() {
+ let deferred = promise.defer();
+
+ waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "\u2018font-weight\u2019",
+ category: CATEGORY_CSS,
+ severity: SEVERITY_WARNING,
+ },
+ {
+ text: "\u2018color\u2019",
+ category: CATEGORY_CSS,
+ severity: SEVERITY_WARNING,
+ }],
+ }).then(([error1Rule, error2Rule]) => {
+ let error1Msg = [...error1Rule.matched][0];
+ let error2Msg = [...error2Rule.matched][0];
+ nodes = [error1Msg.querySelector(".message-location .frame-link"),
+ error2Msg.querySelector(".message-location .frame-link")];
+ ok(nodes[0], ".frame-link node for the first error");
+ ok(nodes[1], ".frame-link node for the second error");
+
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ let toolbox = gDevTools.getToolbox(target);
+ toolbox.once("styleeditor-selected", (event, panel) => {
+ StyleEditorUI = panel.UI;
+ deferred.resolve(panel);
+ });
+
+ EventUtils.sendMouseEvent({ type: "click" }, nodes[0].querySelector(".frame-link-filename"));
+ });
+
+ return deferred.promise;
+}
+
+function onStyleEditorReady(panel) {
+ let deferred = promise.defer();
+
+ let win = panel.panelWindow;
+ ok(win, "Style Editor Window is defined");
+ ok(StyleEditorUI, "Style Editor UI is defined");
+
+ function fireEvent(toolbox, href, line) {
+ toolbox.once("styleeditor-selected", function (evt) {
+ info(evt + " event fired");
+
+ checkStyleEditorForSheetAndLine(href, line - 1).then(deferred.resolve);
+ });
+
+ EventUtils.sendMouseEvent({ type: "click" }, nodes[1].querySelector(".frame-link-filename"));
+ }
+
+ waitForFocus(function () {
+ info("style editor window focused");
+
+ let href = nodes[0].getAttribute("data-url");
+ let line = nodes[0].getAttribute("data-line");
+ ok(line, "found source line");
+
+ checkStyleEditorForSheetAndLine(href, line - 1).then(function () {
+ info("first check done");
+
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ let toolbox = gDevTools.getToolbox(target);
+
+ href = nodes[1].getAttribute("data-url");
+ line = nodes[1].getAttribute("data-line");
+ ok(line, "found source line");
+
+ toolbox.selectTool("webconsole").then(function () {
+ info("webconsole selected");
+ fireEvent(toolbox, href, line);
+ });
+ });
+ }, win);
+
+ return deferred.promise;
+}
+
+function checkStyleEditorForSheetAndLine(href, line) {
+ let foundEditor = null;
+ for (let editor of StyleEditorUI.editors) {
+ if (editor.styleSheet.href == href) {
+ foundEditor = editor;
+ break;
+ }
+ }
+
+ ok(foundEditor, "found style editor for " + href);
+ return performLineCheck(foundEditor, line);
+}
+
+function performLineCheck(editor, line) {
+ let deferred = promise.defer();
+
+ function checkForCorrectState() {
+ is(editor.sourceEditor.getCursor().line, line,
+ "correct line is selected");
+ is(StyleEditorUI.selectedStyleSheetIndex, editor.styleSheet.styleSheetIndex,
+ "correct stylesheet is selected in the editor");
+
+ executeSoon(deferred.resolve);
+ }
+
+ info("wait for source editor to load");
+
+ // Get out of the styleeditor-selected event loop.
+ executeSoon(() => {
+ editor.getSourceEditor().then(() => {
+ // Get out of the editor's source-editor-load event loop.
+ executeSoon(checkForCorrectState);
+ });
+ });
+
+ return deferred.promise;
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_804845_ctrl_key_nav.js b/devtools/client/webconsole/test/browser_webconsole_bug_804845_ctrl_key_nav.js
new file mode 100644
index 000000000..b040e6314
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_804845_ctrl_key_nav.js
@@ -0,0 +1,227 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Test navigation of webconsole contents via ctrl-a, ctrl-e, ctrl-p, ctrl-n
+// see https://bugzilla.mozilla.org/show_bug.cgi?id=804845
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8,Web Console test for " +
+ "bug 804845 and bug 619598";
+
+var jsterm, inputNode;
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ doTests(hud);
+
+ jsterm = inputNode = null;
+});
+
+function doTests(HUD) {
+ jsterm = HUD.jsterm;
+ inputNode = jsterm.inputNode;
+ ok(!jsterm.getInputValue(), "jsterm.getInputValue() is empty");
+ is(jsterm.inputNode.selectionStart, 0);
+ is(jsterm.inputNode.selectionEnd, 0);
+
+ testSingleLineInputNavNoHistory();
+ testMultiLineInputNavNoHistory();
+ testNavWithHistory();
+}
+
+function testSingleLineInputNavNoHistory() {
+ // Single char input
+ EventUtils.synthesizeKey("1", {});
+ is(inputNode.selectionStart, 1, "caret location after single char input");
+
+ // nav to start/end with ctrl-a and ctrl-e;
+ EventUtils.synthesizeKey("a", { ctrlKey: true });
+ is(inputNode.selectionStart, 0,
+ "caret location after single char input and ctrl-a");
+
+ EventUtils.synthesizeKey("e", { ctrlKey: true });
+ is(inputNode.selectionStart, 1,
+ "caret location after single char input and ctrl-e");
+
+ // Second char input
+ EventUtils.synthesizeKey("2", {});
+ // nav to start/end with up/down keys; verify behaviour using ctrl-p/ctrl-n
+ EventUtils.synthesizeKey("VK_UP", {});
+ is(inputNode.selectionStart, 0,
+ "caret location after two char input and VK_UP");
+ EventUtils.synthesizeKey("VK_DOWN", {});
+ is(inputNode.selectionStart, 2,
+ "caret location after two char input and VK_DOWN");
+
+ EventUtils.synthesizeKey("a", { ctrlKey: true });
+ is(inputNode.selectionStart, 0,
+ "move caret to beginning of 2 char input with ctrl-a");
+ EventUtils.synthesizeKey("a", { ctrlKey: true });
+ is(inputNode.selectionStart, 0,
+ "no change of caret location on repeat ctrl-a");
+ EventUtils.synthesizeKey("p", { ctrlKey: true });
+ is(inputNode.selectionStart, 0,
+ "no change of caret location on ctrl-p from beginning of line");
+
+ EventUtils.synthesizeKey("e", { ctrlKey: true });
+ is(inputNode.selectionStart, 2,
+ "move caret to end of 2 char input with ctrl-e");
+ EventUtils.synthesizeKey("e", { ctrlKey: true });
+ is(inputNode.selectionStart, 2,
+ "no change of caret location on repeat ctrl-e");
+ EventUtils.synthesizeKey("n", { ctrlKey: true });
+ is(inputNode.selectionStart, 2,
+ "no change of caret location on ctrl-n from end of line");
+
+ EventUtils.synthesizeKey("p", { ctrlKey: true });
+ is(inputNode.selectionStart, 0, "ctrl-p moves to start of line");
+
+ EventUtils.synthesizeKey("n", { ctrlKey: true });
+ is(inputNode.selectionStart, 2, "ctrl-n moves to end of line");
+}
+
+function testMultiLineInputNavNoHistory() {
+ let lineValues = ["one", "2", "something longer", "", "", "three!"];
+ jsterm.setInputValue("");
+ // simulate shift-return
+ for (let i = 0; i < lineValues.length; i++) {
+ jsterm.setInputValue(jsterm.getInputValue() + lineValues[i]);
+ EventUtils.synthesizeKey("VK_RETURN", { shiftKey: true });
+ }
+ let inputValue = jsterm.getInputValue();
+ is(inputNode.selectionStart, inputNode.selectionEnd);
+ is(inputNode.selectionStart, inputValue.length,
+ "caret at end of multiline input");
+
+ // possibility newline is represented by one ('\r', '\n') or two
+ // ('\r\n') chars
+ let newlineString = inputValue.match(/(\r\n?|\n\r?)$/)[0];
+
+ // Ok, test navigating within the multi-line string!
+ EventUtils.synthesizeKey("VK_UP", {});
+ let expectedStringAfterCarat = lineValues[5] + newlineString;
+ is(jsterm.getInputValue().slice(inputNode.selectionStart), expectedStringAfterCarat,
+ "up arrow from end of multiline");
+
+ EventUtils.synthesizeKey("VK_DOWN", {});
+ is(jsterm.getInputValue().slice(inputNode.selectionStart), "",
+ "down arrow from within multiline");
+
+ // navigate up through input lines
+ EventUtils.synthesizeKey("p", { ctrlKey: true });
+ is(jsterm.getInputValue().slice(inputNode.selectionStart), expectedStringAfterCarat,
+ "ctrl-p from end of multiline");
+
+ for (let i = 4; i >= 0; i--) {
+ EventUtils.synthesizeKey("p", { ctrlKey: true });
+ expectedStringAfterCarat = lineValues[i] + newlineString +
+ expectedStringAfterCarat;
+ is(jsterm.getInputValue().slice(inputNode.selectionStart),
+ expectedStringAfterCarat, "ctrl-p from within line " + i +
+ " of multiline input");
+ }
+ EventUtils.synthesizeKey("p", { ctrlKey: true });
+ is(inputNode.selectionStart, 0, "reached start of input");
+ is(jsterm.getInputValue(), inputValue,
+ "no change to multiline input on ctrl-p from beginning of multiline");
+
+ // navigate to end of first line
+ EventUtils.synthesizeKey("e", { ctrlKey: true });
+ let caretPos = inputNode.selectionStart;
+ let expectedStringBeforeCarat = lineValues[0];
+ is(jsterm.getInputValue().slice(0, caretPos), expectedStringBeforeCarat,
+ "ctrl-e into multiline input");
+ EventUtils.synthesizeKey("e", { ctrlKey: true });
+ is(inputNode.selectionStart, caretPos,
+ "repeat ctrl-e doesn't change caret position in multiline input");
+
+ // navigate down one line; ctrl-a to the beginning; ctrl-e to end
+ for (let i = 1; i < lineValues.length; i++) {
+ EventUtils.synthesizeKey("n", { ctrlKey: true });
+ EventUtils.synthesizeKey("a", { ctrlKey: true });
+ caretPos = inputNode.selectionStart;
+ expectedStringBeforeCarat += newlineString;
+ is(jsterm.getInputValue().slice(0, caretPos), expectedStringBeforeCarat,
+ "ctrl-a to beginning of line " + (i + 1) + " in multiline input");
+
+ EventUtils.synthesizeKey("e", { ctrlKey: true });
+ caretPos = inputNode.selectionStart;
+ expectedStringBeforeCarat += lineValues[i];
+ is(jsterm.getInputValue().slice(0, caretPos), expectedStringBeforeCarat,
+ "ctrl-e to end of line " + (i + 1) + "in multiline input");
+ }
+}
+
+function testNavWithHistory() {
+ // NOTE: Tests does NOT currently define behaviour for ctrl-p/ctrl-n with
+ // caret placed _within_ single line input
+ let values = ['"single line input"',
+ '"a longer single-line input to check caret repositioning"',
+ ['"multi-line"', '"input"', '"here!"'].join("\n"),
+ ];
+ // submit to history
+ for (let i = 0; i < values.length; i++) {
+ jsterm.setInputValue(values[i]);
+ jsterm.execute();
+ }
+ is(inputNode.selectionStart, 0, "caret location at start of empty line");
+
+ EventUtils.synthesizeKey("p", { ctrlKey: true });
+ is(inputNode.selectionStart, values[values.length - 1].length,
+ "caret location correct at end of last history input");
+
+ // Navigate backwards history with ctrl-p
+ for (let i = values.length - 1; i > 0; i--) {
+ let match = values[i].match(/(\n)/g);
+ if (match) {
+ // multi-line inputs won't update from history unless caret at beginning
+ EventUtils.synthesizeKey("a", { ctrlKey: true });
+ for (let j = 0; j < match.length; j++) {
+ EventUtils.synthesizeKey("p", { ctrlKey: true });
+ }
+ EventUtils.synthesizeKey("p", { ctrlKey: true });
+ } else {
+ // single-line inputs will update from history from end of line
+ EventUtils.synthesizeKey("p", { ctrlKey: true });
+ }
+ is(jsterm.getInputValue(), values[i - 1],
+ "ctrl-p updates inputNode from backwards history values[" + i - 1 + "]");
+ }
+ let inputValue = jsterm.getInputValue();
+ EventUtils.synthesizeKey("p", { ctrlKey: true });
+ is(inputNode.selectionStart, 0,
+ "ctrl-p at beginning of history moves caret location to beginning " +
+ "of line");
+ is(jsterm.getInputValue(), inputValue,
+ "no change to input value on ctrl-p from beginning of line");
+
+ // Navigate forwards history with ctrl-n
+ for (let i = 1; i < values.length; i++) {
+ EventUtils.synthesizeKey("n", { ctrlKey: true });
+ is(jsterm.getInputValue(), values[i],
+ "ctrl-n updates inputNode from forwards history values[" + i + "]");
+ is(inputNode.selectionStart, values[i].length,
+ "caret location correct at end of history input for values[" + i + "]");
+ }
+ EventUtils.synthesizeKey("n", { ctrlKey: true });
+ ok(!jsterm.getInputValue(), "ctrl-n at end of history updates to empty input");
+
+ // Simulate editing multi-line
+ inputValue = "one\nlinebreak";
+ jsterm.setInputValue(inputValue);
+
+ // Attempt nav within input
+ EventUtils.synthesizeKey("p", { ctrlKey: true });
+ is(jsterm.getInputValue(), inputValue,
+ "ctrl-p from end of multi-line does not trigger history");
+
+ EventUtils.synthesizeKey("a", { ctrlKey: true });
+ EventUtils.synthesizeKey("p", { ctrlKey: true });
+ is(jsterm.getInputValue(), values[values.length - 1],
+ "ctrl-p from start of multi-line triggers history");
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_817834_add_edited_input_to_history.js b/devtools/client/webconsole/test/browser_webconsole_bug_817834_add_edited_input_to_history.js
new file mode 100644
index 000000000..ccbcb3bf3
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_817834_add_edited_input_to_history.js
@@ -0,0 +1,57 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Test that user input that is not submitted in the command line input is not
+// lost after navigating in history.
+// See https://bugzilla.mozilla.org/show_bug.cgi?id=817834
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8,Web Console test for bug 817834";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ testEditedInputHistory(hud);
+});
+
+function testEditedInputHistory(HUD) {
+ let jsterm = HUD.jsterm;
+ let inputNode = jsterm.inputNode;
+ ok(!jsterm.getInputValue(), "jsterm.getInputValue() is empty");
+ is(inputNode.selectionStart, 0);
+ is(inputNode.selectionEnd, 0);
+
+ jsterm.setInputValue('"first item"');
+ EventUtils.synthesizeKey("VK_UP", {});
+ is(jsterm.getInputValue(), '"first item"', "null test history up");
+ EventUtils.synthesizeKey("VK_DOWN", {});
+ is(jsterm.getInputValue(), '"first item"', "null test history down");
+
+ jsterm.execute();
+ is(jsterm.getInputValue(), "", "cleared input line after submit");
+
+ jsterm.setInputValue('"editing input 1"');
+ EventUtils.synthesizeKey("VK_UP", {});
+ is(jsterm.getInputValue(), '"first item"', "test history up");
+ EventUtils.synthesizeKey("VK_DOWN", {});
+ is(jsterm.getInputValue(), '"editing input 1"',
+ "test history down restores in-progress input");
+
+ jsterm.setInputValue('"second item"');
+ jsterm.execute();
+ jsterm.setInputValue('"editing input 2"');
+ EventUtils.synthesizeKey("VK_UP", {});
+ is(jsterm.getInputValue(), '"second item"', "test history up");
+ EventUtils.synthesizeKey("VK_UP", {});
+ is(jsterm.getInputValue(), '"first item"', "test history up");
+ EventUtils.synthesizeKey("VK_DOWN", {});
+ is(jsterm.getInputValue(), '"second item"', "test history down");
+ EventUtils.synthesizeKey("VK_DOWN", {});
+ is(jsterm.getInputValue(), '"editing input 2"',
+ "test history down restores new in-progress input again");
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_837351_securityerrors.js b/devtools/client/webconsole/test/browser_webconsole_bug_837351_securityerrors.js
new file mode 100644
index 000000000..0524e1c4c
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_837351_securityerrors.js
@@ -0,0 +1,42 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 TEST_URI = "https://example.com/browser/devtools/client/webconsole/" +
+ "test/test-bug-837351-security-errors.html";
+
+add_task(function* () {
+ yield pushPrefEnv();
+
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ let button = hud.ui.rootElement.querySelector(".webconsole-filter-button[category=\"security\"]");
+ ok(button, "Found security button in the web console");
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ name: "Logged blocking mixed active content",
+ text: "Blocked loading mixed active content \u201chttp://example.com/\u201d",
+ category: CATEGORY_SECURITY,
+ severity: SEVERITY_ERROR
+ },
+ ],
+ });
+});
+
+function pushPrefEnv() {
+ let deferred = promise.defer();
+ let options = {
+ set: [["security.mixed_content.block_active_content", true]]
+ };
+ SpecialPowers.pushPrefEnv(options, deferred.resolve);
+ return deferred.promise;
+}
+
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_922212_console_dirxml.js b/devtools/client/webconsole/test/browser_webconsole_bug_922212_console_dirxml.js
new file mode 100644
index 000000000..8062ffeec
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_922212_console_dirxml.js
@@ -0,0 +1,48 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests that console.dirxml works as intended.
+
+"use strict";
+
+const TEST_URI = `data:text/html;charset=utf-8,Web Console test for bug 922212:
+ Add console.dirxml`;
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+ hud.jsterm.clearOutput();
+
+ // Should work like console.log(window)
+ hud.jsterm.execute("console.dirxml(window)");
+
+ let [result] = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "console.dirxml(window) output:",
+ text: /Window \u2192/,
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+
+ hud.jsterm.clearOutput();
+
+ hud.jsterm.execute("console.dirxml(document.body)");
+
+ // Should work like console.log(document.body);
+ [result] = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "console.dirxml(document.body) output:",
+ text: "<body>",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+ let msg = [...result.matched][0];
+ yield checkLinkToInspector(true, msg);
+});
+
diff --git a/devtools/client/webconsole/test/browser_webconsole_cached_autocomplete.js b/devtools/client/webconsole/test/browser_webconsole_cached_autocomplete.js
new file mode 100644
index 000000000..fd5c4d29a
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_cached_autocomplete.js
@@ -0,0 +1,114 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests that the cached autocomplete results are used when the new
+// user input is a subset of the existing completion results.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf8,<p>test cached autocompletion " +
+ "results";
+
+var jsterm;
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ jsterm = hud.jsterm;
+ let input = jsterm.inputNode;
+ let popup = jsterm.autocompletePopup;
+
+ // Test if 'doc' gives 'document'
+ input.value = "doc";
+ input.setSelectionRange(3, 3);
+ yield complete(jsterm.COMPLETE_HINT_ONLY);
+
+ is(input.value, "doc", "'docu' completion (input.value)");
+ is(jsterm.completeNode.value, " ument", "'docu' completion (completeNode)");
+
+ // Test typing 'window.'.
+ input.value = "window.";
+ input.setSelectionRange(7, 7);
+ yield complete(jsterm.COMPLETE_HINT_ONLY);
+
+ ok(popup.getItems().length > 0, "'window.' gave a list of suggestions");
+
+ yield jsterm.execute("window.docfoobar = true");
+
+ // Test typing 'window.doc'.
+ input.value = "window.doc";
+ input.setSelectionRange(10, 10);
+ yield complete(jsterm.COMPLETE_HINT_ONLY);
+
+ let newItems = popup.getItems();
+ ok(newItems.every(function (item) {
+ return item.label != "docfoobar";
+ }), "autocomplete cached results do not contain docfoobar. list has not " +
+ "been updated");
+
+ // Test that backspace does not cause a request to the server
+ input.value = "window.do";
+ input.setSelectionRange(9, 9);
+ yield complete(jsterm.COMPLETE_HINT_ONLY);
+
+ newItems = popup.getItems();
+ ok(newItems.every(function (item) {
+ return item.label != "docfoobar";
+ }), "autocomplete cached results do not contain docfoobar. list has not " +
+ "been updated");
+
+ yield jsterm.execute("delete window.docfoobar");
+
+ // Test if 'window.getC' gives 'getComputedStyle'
+ input.value = "window.";
+ input.setSelectionRange(7, 7);
+ yield complete(jsterm.COMPLETE_HINT_ONLY);
+
+ input.value = "window.getC";
+ input.setSelectionRange(11, 11);
+ yield complete(jsterm.COMPLETE_HINT_ONLY);
+
+ newItems = popup.getItems();
+ ok(!newItems.every(function (item) {
+ return item.label != "getComputedStyle";
+ }), "autocomplete results do contain getComputedStyle");
+
+ // Test if 'dump(d' gives non-zero results
+ input.value = "dump(d";
+ input.setSelectionRange(6, 6);
+ yield complete(jsterm.COMPLETE_HINT_ONLY);
+
+ ok(popup.getItems().length > 0, "'dump(d' gives non-zero results");
+
+ // Test that 'dump(window.)' works.
+ input.value = "dump(window.)";
+ input.setSelectionRange(12, 12);
+ yield complete(jsterm.COMPLETE_HINT_ONLY);
+
+ yield jsterm.execute("window.docfoobar = true");
+
+ // Make sure 'dump(window.doc)' does not contain 'docfoobar'.
+ input.value = "dump(window.doc)";
+ input.setSelectionRange(15, 15);
+ yield complete(jsterm.COMPLETE_HINT_ONLY);
+
+ newItems = popup.getItems();
+ ok(newItems.every(function (item) {
+ return item.label != "docfoobar";
+ }), "autocomplete cached results do not contain docfoobar. list has not " +
+ "been updated");
+
+ yield jsterm.execute("delete window.docfoobar");
+
+ jsterm = null;
+});
+
+function complete(type) {
+ let updated = jsterm.once("autocomplete-updated");
+ jsterm.complete(type);
+ return updated;
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_cd_iframe.js b/devtools/client/webconsole/test/browser_webconsole_cd_iframe.js
new file mode 100644
index 000000000..480c60940
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_cd_iframe.js
@@ -0,0 +1,115 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Test that the cd() jsterm helper function works as expected. See bug 609872.
+
+"use strict";
+
+function test() {
+ let hud;
+
+ const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-bug-609872-cd-iframe-parent.html";
+
+ const parentMessages = [{
+ name: "document.title in parent iframe",
+ text: "bug 609872 - iframe parent",
+ category: CATEGORY_OUTPUT,
+ }, {
+ name: "paragraph content",
+ text: "p: test for bug 609872 - iframe parent",
+ category: CATEGORY_OUTPUT,
+ }, {
+ name: "object content",
+ text: "obj: parent!",
+ category: CATEGORY_OUTPUT,
+ }];
+
+ const childMessages = [{
+ name: "document.title in child iframe",
+ text: "bug 609872 - iframe child",
+ category: CATEGORY_OUTPUT,
+ }, {
+ name: "paragraph content",
+ text: "p: test for bug 609872 - iframe child",
+ category: CATEGORY_OUTPUT,
+ }, {
+ name: "object content",
+ text: "obj: child!",
+ category: CATEGORY_OUTPUT,
+ }];
+
+ Task.spawn(runner).then(finishTest);
+
+ function* runner() {
+ const {tab} = yield loadTab(TEST_URI);
+ hud = yield openConsole(tab);
+
+ yield executeWindowTest();
+
+ yield waitForMessages({ webconsole: hud, messages: parentMessages });
+
+ info("cd() into the iframe using a selector");
+ hud.jsterm.clearOutput();
+ yield hud.jsterm.execute("cd('iframe')");
+ yield executeWindowTest();
+
+ yield waitForMessages({ webconsole: hud, messages: childMessages });
+
+ info("cd() out of the iframe, reset to default window");
+ hud.jsterm.clearOutput();
+ yield hud.jsterm.execute("cd()");
+ yield executeWindowTest();
+
+ yield waitForMessages({ webconsole: hud, messages: parentMessages });
+
+ info("call cd() with unexpected arguments");
+ hud.jsterm.clearOutput();
+ yield hud.jsterm.execute("cd(document)");
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "Cannot cd()",
+ category: CATEGORY_OUTPUT,
+ severity: SEVERITY_ERROR,
+ }],
+ });
+
+ hud.jsterm.clearOutput();
+ yield hud.jsterm.execute("cd('p')");
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "Cannot cd()",
+ category: CATEGORY_OUTPUT,
+ severity: SEVERITY_ERROR,
+ }],
+ });
+
+ info("cd() into the iframe using an iframe DOM element");
+ hud.jsterm.clearOutput();
+ yield hud.jsterm.execute("cd($('iframe'))");
+ yield executeWindowTest();
+
+ yield waitForMessages({ webconsole: hud, messages: childMessages });
+
+ info("cd(window.parent)");
+ hud.jsterm.clearOutput();
+ yield hud.jsterm.execute("cd(window.parent)");
+ yield executeWindowTest();
+
+ yield waitForMessages({ webconsole: hud, messages: parentMessages });
+
+ yield closeConsole(tab);
+ }
+
+ function* executeWindowTest() {
+ yield hud.jsterm.execute("document.title");
+ yield hud.jsterm.execute("'p: ' + document.querySelector('p').textContent");
+ yield hud.jsterm.execute("'obj: ' + window.foobarBug609872");
+ }
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_certificate_messages.js b/devtools/client/webconsole/test/browser_webconsole_certificate_messages.js
new file mode 100644
index 000000000..ca08d1a0f
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_certificate_messages.js
@@ -0,0 +1,81 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests that the Web Console shows weak crypto warnings (SHA-1 Certificate)
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf8,Web Console weak crypto " +
+ "warnings test";
+const TEST_URI_PATH = "/browser/devtools/client/webconsole/test/" +
+ "test-certificate-messages.html";
+
+var gWebconsoleTests = [
+ {url: "https://sha1ee.example.com" + TEST_URI_PATH,
+ name: "SHA1 warning displayed successfully",
+ warning: ["SHA-1"], nowarning: ["SSL 3.0", "RC4"]},
+ {url: "https://sha256ee.example.com" + TEST_URI_PATH,
+ name: "SSL warnings appropriately not present",
+ warning: [], nowarning: ["SHA-1", "SSL 3.0", "RC4"]},
+];
+const TRIGGER_MSG = "If you haven't seen ssl warnings yet, you won't";
+
+var gHud = undefined, gContentBrowser;
+var gCurrentTest;
+
+function test() {
+ registerCleanupFunction(function () {
+ gHud = gContentBrowser = null;
+ });
+
+ loadTab(TEST_URI).then(({browser}) => {
+ gContentBrowser = browser;
+ openConsole().then(runTestLoop);
+ });
+}
+
+function runTestLoop(theHud) {
+ gCurrentTest = gWebconsoleTests.shift();
+ if (!gCurrentTest) {
+ finishTest();
+ return;
+ }
+ if (!gHud) {
+ gHud = theHud;
+ }
+ gHud.jsterm.clearOutput();
+ gContentBrowser.addEventListener("load", onLoad, true);
+ if (gCurrentTest.pref) {
+ SpecialPowers.pushPrefEnv({"set": gCurrentTest.pref},
+ function () {
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, gCurrentTest.url);
+ });
+ } else {
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, gCurrentTest.url);
+ }
+}
+
+function onLoad() {
+ gContentBrowser.removeEventListener("load", onLoad, true);
+
+ waitForSuccess({
+ name: gCurrentTest.name,
+ validator: function () {
+ if (gHud.outputNode.textContent.indexOf(TRIGGER_MSG) >= 0) {
+ for (let warning of gCurrentTest.warning) {
+ if (gHud.outputNode.textContent.indexOf(warning) < 0) {
+ return false;
+ }
+ }
+ for (let nowarning of gCurrentTest.nowarning) {
+ if (gHud.outputNode.textContent.indexOf(nowarning) >= 0) {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+ }).then(runTestLoop);
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_chrome.js b/devtools/client/webconsole/test/browser_webconsole_chrome.js
new file mode 100644
index 000000000..2513d1df5
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_chrome.js
@@ -0,0 +1,38 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests that code completion works properly in chrome tabs, like about:credits.
+
+"use strict";
+
+function test() {
+ Task.spawn(function* () {
+ const {tab} = yield loadTab("about:config");
+ ok(tab, "tab loaded");
+
+ const hud = yield openConsole(tab);
+ ok(hud, "we have a console");
+ ok(hud.iframeWindow, "we have the console UI window");
+
+ let jsterm = hud.jsterm;
+ ok(jsterm, "we have a jsterm");
+
+ let input = jsterm.inputNode;
+ ok(hud.outputNode, "we have an output node");
+
+ // Test typing 'docu'.
+ input.value = "docu";
+ input.setSelectionRange(4, 4);
+
+ let deferred = promise.defer();
+
+ jsterm.complete(jsterm.COMPLETE_HINT_ONLY, function () {
+ is(jsterm.completeNode.value, " ment", "'docu' completion");
+ deferred.resolve(null);
+ });
+
+ yield deferred.promise;
+ }).then(finishTest);
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_clear_method.js b/devtools/client/webconsole/test/browser_webconsole_clear_method.js
new file mode 100644
index 000000000..a4702980e
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_clear_method.js
@@ -0,0 +1,131 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Check that calls to console.clear from a script delete the messages
+// previously logged.
+
+"use strict";
+
+add_task(function* () {
+ const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console-clear.html";
+
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+ ok(hud, "Web Console opened");
+
+ info("Check the console.clear() done on page load has been processed.");
+ yield waitForLog("Console was cleared", hud);
+ ok(hud.outputNode.textContent.includes("Console was cleared"),
+ "console.clear() message is displayed");
+ ok(!hud.outputNode.textContent.includes("log1"), "log1 not displayed");
+ ok(!hud.outputNode.textContent.includes("log2"), "log2 not displayed");
+
+ info("Logging two messages log3, log4");
+ ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ content.wrappedJSObject.console.log("log3");
+ content.wrappedJSObject.console.log("log4");
+ });
+
+ yield waitForLog("log3", hud);
+ yield waitForLog("log4", hud);
+
+ ok(hud.outputNode.textContent.includes("Console was cleared"),
+ "console.clear() message is still displayed");
+ ok(hud.outputNode.textContent.includes("log3"), "log3 is displayed");
+ ok(hud.outputNode.textContent.includes("log4"), "log4 is displayed");
+
+ info("Open the variables view sidebar for 'objFromPage'");
+ yield openSidebar("objFromPage", { a: 1 }, hud);
+ let sidebarClosed = hud.jsterm.once("sidebar-closed");
+
+ info("Call console.clear from the page");
+ ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ content.wrappedJSObject.console.clear();
+ });
+
+ // Cannot wait for "Console was cleared" here because such a message is
+ // already present and would yield immediately.
+ info("Wait for variables view sidebar to be closed after console.clear()");
+ yield sidebarClosed;
+
+ ok(!hud.outputNode.textContent.includes("log3"), "log3 not displayed");
+ ok(!hud.outputNode.textContent.includes("log4"), "log4 not displayed");
+ ok(hud.outputNode.textContent.includes("Console was cleared"),
+ "console.clear() message is still displayed");
+ is(hud.outputNode.textContent.split("Console was cleared").length, 2,
+ "console.clear() message is only displayed once");
+
+ info("Logging one messages log5");
+ ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ content.wrappedJSObject.console.log("log5");
+ });
+ yield waitForLog("log5", hud);
+
+ info("Close and reopen the webconsole.");
+ yield closeConsole(gBrowser.selectedTab);
+ hud = yield openConsole();
+ yield waitForLog("Console was cleared", hud);
+
+ ok(hud.outputNode.textContent.includes("Console was cleared"),
+ "console.clear() message is still displayed");
+ ok(!hud.outputNode.textContent.includes("log1"), "log1 not displayed");
+ ok(!hud.outputNode.textContent.includes("log2"), "log1 not displayed");
+ ok(!hud.outputNode.textContent.includes("log3"), "log3 not displayed");
+ ok(!hud.outputNode.textContent.includes("log4"), "log4 not displayed");
+ ok(hud.outputNode.textContent.includes("log5"), "log5 still displayed");
+});
+
+/**
+ * Wait for a single message to be logged in the provided webconsole instance
+ * with the category CATEGORY_WEBDEV and the SEVERITY_LOG severity.
+ *
+ * @param {String} message
+ * The expected messaged.
+ * @param {WebConsole} webconsole
+ * WebConsole instance in which the message should be logged.
+ */
+function* waitForLog(message, webconsole, options) {
+ yield waitForMessages({
+ webconsole: webconsole,
+ messages: [{
+ text: message,
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+}
+
+/**
+ * Open the variables view sidebar for the object with the provided name objName
+ * and wait for the expected object is displayed in the variables view.
+ *
+ * @param {String} objName
+ * The name of the object to open in the sidebar.
+ * @param {Object} expectedObj
+ * The properties that should be displayed in the variables view.
+ * @param {WebConsole} webconsole
+ * WebConsole instance in which the message should be logged.
+ *
+ */
+function* openSidebar(objName, expectedObj, webconsole) {
+ let msg = yield webconsole.jsterm.execute(objName);
+ ok(msg, "output message found");
+
+ let anchor = msg.querySelector("a");
+ let body = msg.querySelector(".message-body");
+ ok(anchor, "object anchor");
+ ok(body, "message body");
+
+ yield EventUtils.synthesizeMouse(anchor, 2, 2, {}, webconsole.iframeWindow);
+
+ let vviewVar = yield webconsole.jsterm.once("variablesview-fetched");
+ let vview = vviewVar._variablesView;
+ ok(vview, "variables view object exists");
+
+ yield findVariableViewProperties(vviewVar, [
+ expectedObj,
+ ], { webconsole: webconsole });
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_clickable_urls.js b/devtools/client/webconsole/test/browser_webconsole_clickable_urls.js
new file mode 100644
index 000000000..57d81fd05
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_clickable_urls.js
@@ -0,0 +1,103 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// When strings containing URLs are entered into the webconsole, check
+// its output and ensure that the output can be clicked to open those URLs.
+
+"use strict";
+
+const inputTests = [
+
+ // 0: URL opens page when clicked.
+ {
+ input: "'http://example.com'",
+ output: "http://example.com",
+ expectedTab: "http://example.com/",
+ },
+
+ // 1: URL opens page using https when clicked.
+ {
+ input: "'https://example.com'",
+ output: "https://example.com",
+ expectedTab: "https://example.com/",
+ },
+
+ // 2: URL with port opens page when clicked.
+ {
+ input: "'https://example.com:443'",
+ output: "https://example.com:443",
+ expectedTab: "https://example.com/",
+ },
+
+ // 3: URL containing non-empty path opens page when clicked.
+ {
+ input: "'http://example.com/foo'",
+ output: "http://example.com/foo",
+ expectedTab: "http://example.com/foo",
+ },
+
+ // 4: URL opens page when clicked, even when surrounded by non-URL tokens.
+ {
+ input: "'foo http://example.com bar'",
+ output: "foo http://example.com bar",
+ expectedTab: "http://example.com/",
+ },
+
+ // 5: URL opens page when clicked, and whitespace is be preserved.
+ {
+ input: "'foo\\nhttp://example.com\\nbar'",
+ output: "foo\nhttp://example.com\nbar",
+ expectedTab: "http://example.com/",
+ },
+
+ // 6: URL opens page when clicked when multiple links are present.
+ {
+ input: "'http://example.com http://example.com'",
+ output: "http://example.com http://example.com",
+ expectedTab: "http://example.com/",
+ },
+
+ // 7: URL without scheme does not open page when clicked.
+ {
+ input: "'example.com'",
+ output: "example.com",
+ },
+
+ // 8: URL with invalid scheme does not open page when clicked.
+ {
+ input: "'foo://example.com'",
+ output: "foo://example.com",
+ },
+
+ // 9: Shortened URL in an array
+ {
+ input: "['http://example.com/abcdefghijabcdefghij some other text']",
+ output: "Array [ \"http://example.com/abcdefghijabcdef\u2026\" ]",
+ printOutput: "http://example.com/abcdefghijabcdefghij some other text",
+ expectedTab: "http://example.com/abcdefghijabcdefghij",
+ getClickableNode: (msg) => msg.querySelectorAll("a")[1],
+ },
+
+ // 10: Shortened URL in an object
+ {
+ input: "{test: 'http://example.com/abcdefghijabcdefghij some other text'}",
+ output: "Object { test: \"http://example.com/abcdefghijabcdef\u2026\" }",
+ printOutput: "[object Object]",
+ evalOutput: "http://example.com/abcdefghijabcdefghij some other text",
+ noClick: true,
+ consoleLogClick: true,
+ expectedTab: "http://example.com/abcdefghijabcdefghij",
+ getClickableNode: (msg) => msg.querySelectorAll("a")[1],
+ },
+
+];
+
+const url = "data:text/html;charset=utf8,Bug 1005909 - Clickable URLS";
+
+add_task(function* () {
+ yield BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+ let hud = yield openConsole();
+ yield checkOutputForInputs(hud, inputTests);
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_closure_inspection.js b/devtools/client/webconsole/test/browser_webconsole_closure_inspection.js
new file mode 100644
index 000000000..6a29d61aa
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_closure_inspection.js
@@ -0,0 +1,100 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Check that inspecting a closure in the variables view sidebar works when
+// execution is paused.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-closures.html";
+
+var gWebConsole, gJSTerm, gVariablesView;
+
+// Force the old debugger UI since it's directly used (see Bug 1301705)
+Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false);
+registerCleanupFunction(function* () {
+ Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend");
+});
+
+function test() {
+ registerCleanupFunction(() => {
+ gWebConsole = gJSTerm = gVariablesView = null;
+ });
+
+ function fetchScopes(hud, toolbox, panelWin, deferred) {
+ panelWin.once(panelWin.EVENTS.FETCHED_SCOPES, () => {
+ ok(true, "Scopes were fetched");
+ toolbox.selectTool("webconsole").then(() => consoleOpened(hud));
+ deferred.resolve();
+ });
+ }
+
+ loadTab(TEST_URI).then(() => {
+ openConsole().then((hud) => {
+ openDebugger().then(({ toolbox, panelWin }) => {
+ let deferred = promise.defer();
+ fetchScopes(hud, toolbox, panelWin, deferred);
+
+ ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ let button = content.document.querySelector("button");
+ ok(button, "button element found");
+ button.click();
+ });
+
+ return deferred.promise;
+ });
+ });
+ });
+}
+
+function consoleOpened(hud) {
+ gWebConsole = hud;
+ gJSTerm = hud.jsterm;
+ gJSTerm.execute("window.george.getName");
+
+ waitForMessages({
+ webconsole: gWebConsole,
+ messages: [{
+ text: "function _pfactory/<.getName()",
+ category: CATEGORY_OUTPUT,
+ objects: true,
+ }],
+ }).then(onExecuteGetName);
+}
+
+function onExecuteGetName(results) {
+ let clickable = results[0].clickableElements[0];
+ ok(clickable, "clickable object found");
+
+ gJSTerm.once("variablesview-fetched", onGetNameFetch);
+ let contextMenu =
+ gWebConsole.iframeWindow.document.getElementById("output-contextmenu");
+ waitForContextMenu(contextMenu, clickable, () => {
+ let openInVarView = contextMenu.querySelector("#menu_openInVarView");
+ ok(openInVarView.disabled === false,
+ "the \"Open In Variables View\" context menu item should be clickable");
+ // EventUtils.synthesizeMouseAtCenter seems to fail here in Mac OSX
+ openInVarView.click();
+ });
+}
+
+function onGetNameFetch(evt, view) {
+ gVariablesView = view._variablesView;
+ ok(gVariablesView, "variables view object");
+
+ findVariableViewProperties(view, [
+ { name: /_pfactory/, value: "" },
+ ], { webconsole: gWebConsole }).then(onExpandClosure);
+}
+
+function onExpandClosure(results) {
+ let prop = results[0].matchedProp;
+ ok(prop, "matched the name property in the variables view");
+
+ gVariablesView.window.focus();
+ gJSTerm.once("sidebar-closed", finishTest);
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, gVariablesView.window);
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_column_numbers.js b/devtools/client/webconsole/test/browser_webconsole_column_numbers.js
new file mode 100644
index 000000000..8407e34d5
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_column_numbers.js
@@ -0,0 +1,46 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+ // Check if console provides the right column number alongside line number
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console-column.html";
+
+var hud;
+
+function test() {
+ loadTab(TEST_URI).then(() => {
+ openConsole().then(consoleOpened);
+ });
+}
+
+function consoleOpened(aHud) {
+ hud = aHud;
+
+ waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "Error Message",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_ERROR
+ }]
+ }).then(testLocationColumn);
+}
+
+function testLocationColumn() {
+ let messages = hud.outputNode.children;
+ let expected = ["10:7", "10:39", "11:9", "12:11", "13:9", "14:7"];
+
+ for (let i = 0, len = messages.length; i < len; i++) {
+ let msg = messages[i].textContent;
+
+ is(msg.includes(expected[i]), true, "Found expected line:column of " +
+ expected[i]);
+ }
+
+ finishTest();
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_completion.js b/devtools/client/webconsole/test/browser_webconsole_completion.js
new file mode 100644
index 000000000..ee0c6809e
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_completion.js
@@ -0,0 +1,106 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests that code completion works properly.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf8,<p>test code completion";
+
+var jsterm;
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ jsterm = hud.jsterm;
+ let input = jsterm.inputNode;
+
+ // Test typing 'docu'.
+ input.value = "docu";
+ input.setSelectionRange(4, 4);
+ yield complete(jsterm.COMPLETE_HINT_ONLY);
+
+ is(input.value, "docu", "'docu' completion (input.value)");
+ is(jsterm.completeNode.value, " ment", "'docu' completion (completeNode)");
+
+ // Test typing 'docu' and press tab.
+ input.value = "docu";
+ input.setSelectionRange(4, 4);
+ yield complete(jsterm.COMPLETE_FORWARD);
+
+ is(input.value, "document", "'docu' tab completion");
+ is(input.selectionStart, 8, "start selection is alright");
+ is(input.selectionEnd, 8, "end selection is alright");
+ is(jsterm.completeNode.value.replace(/ /g, ""), "", "'docu' completed");
+
+ // Test typing 'window.Ob' and press tab. Just 'window.O' is
+ // ambiguous: could be window.Object, window.Option, etc.
+ input.value = "window.Ob";
+ input.setSelectionRange(9, 9);
+ yield complete(jsterm.COMPLETE_FORWARD);
+
+ is(input.value, "window.Object", "'window.Ob' tab completion");
+
+ // Test typing 'document.getElem'.
+ input.value = "document.getElem";
+ input.setSelectionRange(16, 16);
+ yield complete(jsterm.COMPLETE_FORWARD);
+
+ is(input.value, "document.getElem", "'document.getElem' completion");
+ is(jsterm.completeNode.value, " entsByTagNameNS",
+ "'document.getElem' completion");
+
+ // Test pressing tab another time.
+ yield jsterm.complete(jsterm.COMPLETE_FORWARD);
+
+ is(input.value, "document.getElem", "'document.getElem' completion");
+ is(jsterm.completeNode.value, " entsByTagName",
+ "'document.getElem' another tab completion");
+
+ // Test pressing shift_tab.
+ complete(jsterm.COMPLETE_BACKWARD);
+
+ is(input.value, "document.getElem", "'document.getElem' untab completion");
+ is(jsterm.completeNode.value, " entsByTagNameNS",
+ "'document.getElem' completion");
+
+ jsterm.clearOutput();
+
+ input.value = "docu";
+ yield complete(jsterm.COMPLETE_HINT_ONLY);
+
+ is(jsterm.completeNode.value, " ment", "'docu' completion");
+ yield jsterm.execute();
+ is(jsterm.completeNode.value, "", "clear completion on execute()");
+
+ // Test multi-line completion works
+ input.value = "console.log('one');\nconsol";
+ yield complete(jsterm.COMPLETE_HINT_ONLY);
+
+ is(jsterm.completeNode.value, " \n e",
+ "multi-line completion");
+
+ // Test non-object autocompletion.
+ input.value = "Object.name.sl";
+ yield complete(jsterm.COMPLETE_HINT_ONLY);
+
+ is(jsterm.completeNode.value, " ice", "non-object completion");
+
+ // Test string literal autocompletion.
+ input.value = "'Asimov'.sl";
+ yield complete(jsterm.COMPLETE_HINT_ONLY);
+
+ is(jsterm.completeNode.value, " ice", "string literal completion");
+
+ jsterm = null;
+});
+
+function complete(type) {
+ let updated = jsterm.once("autocomplete-updated");
+ jsterm.complete(type);
+ return updated;
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_console_api_stackframe.js b/devtools/client/webconsole/test/browser_webconsole_console_api_stackframe.js
new file mode 100644
index 000000000..f8f02aa15
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_console_api_stackframe.js
@@ -0,0 +1,85 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 console API messages for console.error()/exception()/assert()
+// include a stackframe. See bug 920116.
+
+function test() {
+ let hud;
+
+ const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console-api-stackframe.html";
+ const TEST_FILE = TEST_URI.substr(TEST_URI.lastIndexOf("/"));
+
+ Task.spawn(runner).then(finishTest);
+
+ function* runner() {
+ const {tab} = yield loadTab(TEST_URI);
+ hud = yield openConsole(tab);
+
+ const stack = [{
+ file: TEST_FILE,
+ fn: "thirdCall",
+ // 21,22,23
+ line: /\b2[123]\b/,
+ }, {
+ file: TEST_FILE,
+ fn: "secondCall",
+ line: 16,
+ }, {
+ file: TEST_FILE,
+ fn: "firstCall",
+ line: 12,
+ }];
+
+ let results = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "foo-log",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ collapsible: false,
+ }, {
+ text: "foo-error",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_ERROR,
+ collapsible: true,
+ stacktrace: stack,
+ }, {
+ text: "foo-exception",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_ERROR,
+ collapsible: true,
+ stacktrace: stack,
+ }, {
+ text: "foo-assert",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_ERROR,
+ collapsible: true,
+ stacktrace: stack,
+ }],
+ });
+
+ let elem = [...results[1].matched][0];
+ ok(elem, "message element");
+
+ let msg = elem._messageObject;
+ ok(msg, "message object");
+
+ ok(msg.collapsed, "message is collapsed");
+
+ msg.toggleDetails();
+
+ ok(!msg.collapsed, "message is not collapsed");
+
+ msg.toggleDetails();
+
+ ok(msg.collapsed, "message is collapsed");
+
+ yield closeConsole(tab);
+ }
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_console_custom_styles.js b/devtools/client/webconsole/test/browser_webconsole_console_custom_styles.js
new file mode 100644
index 000000000..310d4fc8b
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_console_custom_styles.js
@@ -0,0 +1,81 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 '%c' modifier works with the console API. See bug 823097.
+
+function test() {
+ let hud;
+
+ const TEST_URI = "data:text/html;charset=utf8,<p>test for " +
+ "console.log('%ccustom styles', 'color:red')";
+
+ const checks = [{
+ // check the basics work
+ style: "color:red;font-size:1.3em",
+ props: { color: true, fontSize: true },
+ sameStyleExpected: true,
+ }, {
+ // check that the url() is not allowed
+ style: "color:blue;background-image:url('http://example.com/test')",
+ props: { color: true, fontSize: false, background: false,
+ backgroundImage: false },
+ sameStyleExpected: false,
+ }, {
+ // check that some properties are not allowed
+ style: "color:pink;position:absolute;top:10px",
+ props: { color: true, position: false, top: false },
+ sameStyleExpected: false,
+ }];
+
+ Task.spawn(runner).then(finishTest);
+
+ function* runner() {
+ const {tab} = yield loadTab(TEST_URI);
+ hud = yield openConsole(tab);
+
+ for (let check of checks) {
+ yield checkStyle(check);
+ }
+
+ yield closeConsole(tab);
+ }
+
+ function* checkStyle(check) {
+ hud.jsterm.clearOutput();
+
+ info("checkStyle " + check.style);
+ hud.jsterm.execute("console.log('%cfoobar', \"" + check.style + "\")");
+
+ let [result] = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "foobar",
+ category: CATEGORY_WEBDEV,
+ }],
+ });
+
+ let msg = [...result.matched][0];
+ ok(msg, "message element");
+
+ let span = msg.querySelector(".message-body span[style]");
+ ok(span, "span element");
+
+ info("span textContent is: " + span.textContent);
+ isnot(span.textContent.indexOf("foobar"), -1, "span textContent check");
+
+ let outputStyle = span.getAttribute("style").replace(/\s+|;+$/g, "");
+ if (check.sameStyleExpected) {
+ is(outputStyle, check.style, "span style is correct");
+ } else {
+ isnot(outputStyle, check.style, "span style is not the same");
+ }
+
+ for (let prop of Object.keys(check.props)) {
+ is(!!span.style[prop], check.props[prop], "property check for " + prop);
+ }
+ }
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_console_extras.js b/devtools/client/webconsole/test/browser_webconsole_console_extras.js
new file mode 100644
index 000000000..078e33119
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_console_extras.js
@@ -0,0 +1,43 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Test that window.console functions that are not implemented yet do not
+// output anything in the web console and they do not throw any exceptions.
+// See bug 614350.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console-extras.html";
+
+function test() {
+ loadTab(TEST_URI).then(() => {
+ openConsole().then(consoleOpened);
+ });
+}
+
+function consoleOpened(hud) {
+ waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "start",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ },
+ {
+ text: "end",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ }).then(() => {
+ let nodes = hud.outputNode.querySelectorAll(".message");
+ is(nodes.length, 2, "only two messages are displayed");
+ finishTest();
+ });
+
+ let button = content.document.querySelector("button");
+ ok(button, "we have the button");
+ EventUtils.sendMouseEvent({ type: "click" }, button, content);
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_console_logging_api.js b/devtools/client/webconsole/test/browser_webconsole_console_logging_api.js
new file mode 100644
index 000000000..317337543
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_console_logging_api.js
@@ -0,0 +1,102 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests that the basic console.log()-style APIs and filtering work.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console.html";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+ hud.jsterm.clearOutput();
+
+ let outputNode = hud.outputNode;
+
+ let methods = ["log", "info", "warn", "error", "exception", "debug"];
+ for (let method of methods) {
+ yield testMethod(method, hud, outputNode);
+ }
+});
+
+function* testMethod(method, hud, outputNode) {
+ let console = content.console;
+
+ hud.jsterm.clearOutput();
+
+ console[method]("foo-bar-baz");
+ console[method]("baar-baz");
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "foo-bar-baz",
+ }, {
+ text: "baar-baz",
+ }],
+ });
+
+ setStringFilter("foo", hud);
+
+ is(outputNode.querySelectorAll(".filtered-by-string").length, 1,
+ "1 hidden " + method + " node via string filtering");
+
+ hud.jsterm.clearOutput();
+
+ // now toggle the current method off - make sure no visible message
+ // TODO: move all filtering tests into a separate test file: see bug 608135
+
+ console[method]("foo-bar-baz");
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "foo-bar-baz",
+ }],
+ });
+
+ setStringFilter("", hud);
+ let filter;
+ switch (method) {
+ case "debug":
+ filter = "log";
+ break;
+ case "exception":
+ filter = "error";
+ break;
+ default:
+ filter = method;
+ break;
+ }
+
+ hud.setFilterState(filter, false);
+
+ is(outputNode.querySelectorAll(".filtered-by-type").length, 1,
+ "1 message hidden for " + method + " (logging turned off)");
+
+ hud.setFilterState(filter, true);
+
+ is(outputNode.querySelectorAll(".message:not(.filtered-by-type)").length, 1,
+ "1 message shown for " + method + " (logging turned on)");
+
+ hud.jsterm.clearOutput();
+
+ // test for multiple arguments.
+ console[method]("foo", "bar");
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "foo bar",
+ category: CATEGORY_WEBDEV,
+ }],
+ });
+}
+
+function setStringFilter(value, hud) {
+ hud.ui.filterBox.value = value;
+ hud.ui.adjustVisibilityOnSearchStringChange();
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_console_logging_workers_api.js b/devtools/client/webconsole/test/browser_webconsole_console_logging_workers_api.js
new file mode 100644
index 000000000..9575721c3
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_console_logging_workers_api.js
@@ -0,0 +1,39 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests that the basic console.log()-style APIs and filtering work for
+// sharedWorkers
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console-workers.html";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "foo-bar-shared-worker"
+ }],
+ });
+
+ hud.setFilterState("sharedworkers", false);
+
+ is(hud.outputNode.querySelectorAll(".filtered-by-type").length, 1,
+ "1 message hidden for sharedworkers (logging turned off)");
+
+ hud.setFilterState("sharedworkers", true);
+
+ is(hud.outputNode.querySelectorAll(".filtered-by-type").length, 0,
+ "1 message shown for sharedworkers (logging turned on)");
+
+ hud.setFilterState("sharedworkers", false);
+
+ hud.jsterm.clearOutput(true);
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_console_trace_async.js b/devtools/client/webconsole/test/browser_webconsole_console_trace_async.js
new file mode 100644
index 000000000..10c3ff7a5
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_console_trace_async.js
@@ -0,0 +1,75 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 TEST_URI = "http://example.com/browser/devtools/client/" +
+ "webconsole/test/test-console-trace-async.html";
+
+add_task(function* runTest() {
+ // Async stacks aren't on by default in all builds
+ yield new Promise(resolve => {
+ SpecialPowers.pushPrefEnv({"set": [
+ ["javascript.options.asyncstack", true]
+ ]}, resolve);
+ });
+
+ let {tab} = yield loadTab("data:text/html;charset=utf8,<p>hello");
+ let hud = yield openConsole(tab);
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_URI);
+
+ let [result] = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "console.trace output",
+ consoleTrace: {
+ file: "test-console-trace-async.html",
+ fn: "inner",
+ },
+ }],
+ });
+
+ let node = [...result.matched][0];
+ ok(node, "found trace log node");
+ ok(node.textContent.includes("console.trace()"),
+ "trace log node includes console.trace()");
+ ok(node.textContent.includes("promise callback"),
+ "trace log node includes promise callback");
+ ok(node.textContent.includes("setTimeout handler"),
+ "trace log node includes setTimeout handler");
+
+ // The expected stack trace object.
+ let stacktrace = [
+ {
+ columnNumber: 3,
+ filename: TEST_URI,
+ functionName: "inner",
+ language: 2,
+ lineNumber: 9
+ },
+ {
+ asyncCause: "promise callback",
+ columnNumber: 3,
+ filename: TEST_URI,
+ functionName: "time1",
+ language: 2,
+ lineNumber: 13,
+ },
+ {
+ asyncCause: "setTimeout handler",
+ columnNumber: 1,
+ filename: TEST_URI,
+ functionName: "",
+ language: 2,
+ lineNumber: 18,
+ }
+ ];
+
+ let obj = node._messageObject;
+ ok(obj, "console.trace message object");
+ ok(obj._stacktrace, "found stacktrace object");
+ is(obj._stacktrace.toSource(), stacktrace.toSource(),
+ "stacktrace is correct");
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_console_trace_duplicates.js b/devtools/client/webconsole/test/browser_webconsole_console_trace_duplicates.js
new file mode 100644
index 000000000..e1c6f966e
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_console_trace_duplicates.js
@@ -0,0 +1,50 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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";
+
+function test() {
+ const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-bug_939783_console_trace_duplicates.html";
+
+ Task.spawn(runner).then(finishTest);
+
+ function* runner() {
+ const {tab} = yield loadTab("data:text/html;charset=utf8,<p>hello");
+ const hud = yield openConsole(tab);
+
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_URI);
+
+ // NB: Now that stack frames include a column number multiple invocations
+ // on the same line are considered unique. ie:
+ // |foo(); foo();|
+ // will generate two distinct trace entries.
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "console.trace output for foo1()",
+ text: "foo1",
+ consoleTrace: {
+ file: "test-bug_939783_console_trace_duplicates.html",
+ fn: "foo3",
+ },
+ }, {
+ name: "console.trace output for foo1()",
+ text: "foo1",
+ consoleTrace: {
+ file: "test-bug_939783_console_trace_duplicates.html",
+ fn: "foo3",
+ },
+ }, {
+ name: "console.trace output for foo1b()",
+ text: "foo1b",
+ consoleTrace: {
+ file: "test-bug_939783_console_trace_duplicates.html",
+ fn: "foo3",
+ },
+ }],
+ });
+ }
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_context_menu_open_in_var_view.js b/devtools/client/webconsole/test/browser_webconsole_context_menu_open_in_var_view.js
new file mode 100644
index 000000000..8451ec762
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_context_menu_open_in_var_view.js
@@ -0,0 +1,51 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests that the "Open in Variables View" context menu item is enabled
+// only for objects.
+
+"use strict";
+
+const TEST_URI = `data:text/html,<script>
+ console.log("foo");
+ console.log("foo", window);
+</script>`;
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+
+ let [result] = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ count: 2,
+ text: /foo/
+ }],
+ });
+
+ let [msgWithText, msgWithObj] = [...result.matched];
+ ok(msgWithText && msgWithObj, "Two messages should have appeared");
+
+ let contextMenu = hud.iframeWindow.document
+ .getElementById("output-contextmenu");
+ let openInVarViewItem = contextMenu.querySelector("#menu_openInVarView");
+ let obj = msgWithObj.querySelector(".cm-variable");
+ let text = msgWithText.querySelector(".console-string");
+
+ yield waitForContextMenu(contextMenu, obj, () => {
+ ok(openInVarViewItem.disabled === false, "The \"Open In Variables View\" " +
+ "context menu item should be available for objects");
+ }, () => {
+ ok(openInVarViewItem.disabled === true, "The \"Open In Variables View\" " +
+ "context menu item should be disabled on popup hiding");
+ });
+
+ yield waitForContextMenu(contextMenu, text, () => {
+ ok(openInVarViewItem.disabled === true, "The \"Open In Variables View\" " +
+ "context menu item should be disabled for texts");
+ });
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_context_menu_store_as_global.js b/devtools/client/webconsole/test/browser_webconsole_context_menu_store_as_global.js
new file mode 100644
index 000000000..4508101ee
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_context_menu_store_as_global.js
@@ -0,0 +1,66 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests the "Store as global variable" context menu item feature.
+// It should be work, and be enabled only for objects
+
+"use strict";
+
+const TEST_URI = `data:text/html,<script>
+ window.bar = { baz: 1 };
+ console.log("foo");
+ console.log("foo", window.bar);
+</script>`;
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+
+ let [result] = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ count: 2,
+ text: /foo/
+ }],
+ });
+
+ let [msgWithText, msgWithObj] = [...result.matched];
+ ok(msgWithText && msgWithObj, "Two messages should have appeared");
+
+ let contextMenu = hud.iframeWindow.document
+ .getElementById("output-contextmenu");
+ let storeAsGlobalItem = contextMenu.querySelector("#menu_storeAsGlobal");
+ let obj = msgWithObj.querySelector(".cm-variable");
+ let text = msgWithText.querySelector(".console-string");
+ let onceInputSet = hud.jsterm.once("set-input-value");
+
+ info("Waiting for context menu on the object");
+ yield waitForContextMenu(contextMenu, obj, () => {
+ ok(storeAsGlobalItem.disabled === false, "The \"Store as global\" " +
+ "context menu item should be available for objects");
+ storeAsGlobalItem.click();
+ }, () => {
+ ok(storeAsGlobalItem.disabled === true, "The \"Store as global\" " +
+ "context menu item should be disabled on popup hiding");
+ });
+
+ info("Waiting for context menu on the text node");
+ yield waitForContextMenu(contextMenu, text, () => {
+ ok(storeAsGlobalItem.disabled === true, "The \"Store as global\" " +
+ "context menu item should be disabled for texts");
+ });
+
+ info("Waiting for input to be set");
+ yield onceInputSet;
+
+ is(hud.jsterm.getInputValue(), "temp0", "Input was set");
+ let executedResult = yield hud.jsterm.execute();
+
+ ok(executedResult.textContent.includes("{ baz: 1 }"),
+ "Correct variable assigned into console");
+
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_count.js b/devtools/client/webconsole/test/browser_webconsole_count.js
new file mode 100644
index 000000000..abb31a08d
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_count.js
@@ -0,0 +1,77 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Test that console.count() counts as expected. See bug 922208.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console-count.html";
+
+function test() {
+ Task.spawn(runner).then(finishTest);
+
+ function* runner() {
+ const {tab} = yield loadTab(TEST_URI);
+ const hud = yield openConsole(tab);
+
+ BrowserTestUtils.synthesizeMouseAtCenter("#local", {}, gBrowser.selectedBrowser);
+ let messages = [];
+ [
+ "start",
+ "<no label>: 2",
+ "console.count() testcounter: 1",
+ "console.count() testcounter: 2",
+ "console.count() testcounter: 3",
+ "console.count() testcounter: 4",
+ "end"
+ ].forEach(function (msg) {
+ messages.push({
+ text: msg,
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG
+ });
+ });
+ messages.push({
+ name: "Three local counts with no label and count=1",
+ text: "<no label>: 1",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ count: 3
+ });
+ yield waitForMessages({
+ webconsole: hud,
+ messages: messages
+ });
+
+ hud.jsterm.clearOutput();
+
+ BrowserTestUtils.synthesizeMouseAtCenter("#external", {}, gBrowser.selectedBrowser);
+ messages = [];
+ [
+ "start",
+ "console.count() testcounter: 5",
+ "console.count() testcounter: 6",
+ "end"
+ ].forEach(function (msg) {
+ messages.push({
+ text: msg,
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG
+ });
+ });
+ messages.push({
+ name: "Two external counts with no label and count=1",
+ text: "<no label>: 1",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ count: 2
+ });
+ yield waitForMessages({
+ webconsole: hud,
+ messages: messages
+ });
+ }
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_dont_navigate_on_doubleclick.js b/devtools/client/webconsole/test/browser_webconsole_dont_navigate_on_doubleclick.js
new file mode 100644
index 000000000..61ac68208
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_dont_navigate_on_doubleclick.js
@@ -0,0 +1,56 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests that if a link in console is double clicked, the console frame doesn't
+// navigate to that destination (bug 975707).
+
+"use strict";
+
+function test() {
+ let originalNetPref = Services.prefs
+ .getBoolPref("devtools.webconsole.filter.networkinfo");
+ registerCleanupFunction(() => {
+ Services.prefs.setBoolPref("devtools.webconsole.filter.networkinfo",
+ originalNetPref);
+ });
+ Services.prefs.setBoolPref("devtools.webconsole.filter.networkinfo", true);
+ Task.spawn(runner).then(finishTest);
+
+ function* runner() {
+ const TEST_PAGE_URI = "http://example.com/browser/devtools/client/" +
+ "webconsole/test/test-console.html" + "?_uniq=" +
+ Date.now();
+
+ const {tab} = yield loadTab("data:text/html;charset=utf8,<p>hello</p>");
+ const hud = yield openConsole(tab);
+
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_PAGE_URI);
+
+ let messages = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "Network request message",
+ url: TEST_PAGE_URI,
+ category: CATEGORY_NETWORK
+ }]
+ });
+
+ let networkEventMessage = messages[0].matched.values().next().value;
+ let urlNode = networkEventMessage.querySelector(".url");
+
+ let deferred = promise.defer();
+ urlNode.addEventListener("click", function onClick(event) {
+ urlNode.removeEventListener("click", onClick);
+ ok(event.defaultPrevented, "The default action was prevented.");
+
+ deferred.resolve();
+ });
+
+ EventUtils.synthesizeMouseAtCenter(urlNode, {clickCount: 2},
+ hud.iframeWindow);
+
+ yield deferred.promise;
+ }
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_exception_stackframe.js b/devtools/client/webconsole/test/browser_webconsole_exception_stackframe.js
new file mode 100644
index 000000000..5cedfbad5
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_exception_stackframe.js
@@ -0,0 +1,104 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 console receive exceptions include a stackframe.
+// See bug 1184172.
+
+// On e10s, the exception is triggered in child process
+// and is ignored by test harness
+if (!Services.appinfo.browserTabsRemoteAutostart) {
+ SimpleTest.ignoreAllUncaughtExceptions();
+}
+
+function test() {
+ let hud;
+
+ const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-exception-stackframe.html";
+ const TEST_FILE = TEST_URI.substr(TEST_URI.lastIndexOf("/"));
+
+ Task.spawn(runner).then(finishTest);
+
+ function* runner() {
+ const {tab} = yield loadTab(TEST_URI);
+ hud = yield openConsole(tab);
+
+ const stack = [{
+ file: TEST_FILE,
+ fn: "thirdCall",
+ line: 21,
+ }, {
+ file: TEST_FILE,
+ fn: "secondCall",
+ line: 17,
+ }, {
+ file: TEST_FILE,
+ fn: "firstCall",
+ line: 12,
+ }];
+
+ let results = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "nonExistingMethodCall is not defined",
+ category: CATEGORY_JS,
+ severity: SEVERITY_ERROR,
+ collapsible: true,
+ stacktrace: stack,
+ }, {
+ text: "SyntaxError: 'buggy;selector' is not a valid selector",
+ category: CATEGORY_JS,
+ severity: SEVERITY_ERROR,
+ collapsible: true,
+ stacktrace: [{
+ file: TEST_FILE,
+ fn: "domAPI",
+ line: 25,
+ }, {
+ file: TEST_FILE,
+ fn: "onLoadDomAPI",
+ line: 33,
+ }
+ ]
+ }, {
+ text: "DOMException",
+ category: CATEGORY_JS,
+ severity: SEVERITY_ERROR,
+ collapsible: true,
+ stacktrace: [{
+ file: TEST_FILE,
+ fn: "domException",
+ line: 29,
+ }, {
+ file: TEST_FILE,
+ fn: "onLoadDomException",
+ line: 36,
+ },
+
+ ]
+ }],
+ });
+
+ let elem = [...results[0].matched][0];
+ ok(elem, "message element");
+
+ let msg = elem._messageObject;
+ ok(msg, "message object");
+
+ ok(msg.collapsed, "message is collapsed");
+
+ msg.toggleDetails();
+
+ ok(!msg.collapsed, "message is not collapsed");
+
+ msg.toggleDetails();
+
+ ok(msg.collapsed, "message is collapsed");
+
+ yield closeConsole(tab);
+ }
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_execution_scope.js b/devtools/client/webconsole/test/browser_webconsole_execution_scope.js
new file mode 100644
index 000000000..78865c9b2
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_execution_scope.js
@@ -0,0 +1,37 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests that commands run by the user are executed in content space.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console.html";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+ hud.jsterm.clearOutput();
+ hud.jsterm.execute("window.location.href;");
+
+ let [input, output] = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "window.location.href;",
+ category: CATEGORY_INPUT,
+ },
+ {
+ text: TEST_URI,
+ category: CATEGORY_OUTPUT,
+ }],
+ });
+
+ let inputNode = [...input.matched][0];
+ let outputNode = [...output.matched][0];
+ is(inputNode.getAttribute("category"), "input",
+ "input node category is correct");
+ is(outputNode.getAttribute("category"), "output",
+ "output node category is correct");
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_expandable_timestamps.js b/devtools/client/webconsole/test/browser_webconsole_expandable_timestamps.js
new file mode 100644
index 000000000..192387e8a
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_expandable_timestamps.js
@@ -0,0 +1,57 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Test for the message timestamps option: check if the preference toggles the
+// display of messages in the console output. See bug 722267.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8,Web Console test for " +
+ "bug 722267 - preference for toggling timestamps in messages";
+const PREF_MESSAGE_TIMESTAMP = "devtools.webconsole.timestampMessages";
+var hud;
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ hud = yield openConsole();
+ let panel = yield consoleOpened();
+
+ yield onOptionsPanelSelected(panel);
+ onPrefChanged();
+
+ Services.prefs.clearUserPref(PREF_MESSAGE_TIMESTAMP);
+ hud = null;
+});
+
+function consoleOpened() {
+ info("console opened");
+ let prefValue = Services.prefs.getBoolPref(PREF_MESSAGE_TIMESTAMP);
+ ok(!prefValue, "messages have no timestamp by default (pref check)");
+ ok(hud.outputNode.classList.contains("hideTimestamps"),
+ "messages have no timestamp (class name check)");
+
+ let toolbox = gDevTools.getToolbox(hud.target);
+ return toolbox.selectTool("options");
+}
+
+function onOptionsPanelSelected(panel) {
+ info("options panel opened");
+
+ let prefChanged = gDevTools.once("pref-changed", onPrefChanged);
+
+ let checkbox = panel.panelDoc.getElementById("webconsole-timestamp-messages");
+ checkbox.click();
+
+ return prefChanged;
+}
+
+function onPrefChanged() {
+ info("pref changed");
+ let prefValue = Services.prefs.getBoolPref(PREF_MESSAGE_TIMESTAMP);
+ ok(prefValue, "messages have timestamps (pref check)");
+ ok(!hud.outputNode.classList.contains("hideTimestamps"),
+ "messages have timestamps (class name check)");
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_filter_buttons_contextmenu.js b/devtools/client/webconsole/test/browser_webconsole_filter_buttons_contextmenu.js
new file mode 100644
index 000000000..e210bd81a
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_filter_buttons_contextmenu.js
@@ -0,0 +1,95 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests that the filter button context menu logic works correctly.
+
+"use strict";
+
+const TEST_URI = "http://example.com/";
+
+function test() {
+ loadTab(TEST_URI).then(() => {
+ openConsole().then(testFilterButtons);
+ });
+}
+
+function testFilterButtons(aHud) {
+ let hudBox = aHud.ui.rootElement;
+
+ testRightClick("net", hudBox, aHud)
+ .then(() => testRightClick("css", hudBox, aHud))
+ .then(() => testRightClick("js", hudBox, aHud))
+ .then(() => testRightClick("logging", hudBox, aHud))
+ .then(() => testRightClick("security", hudBox, aHud))
+ .then(finishTest);
+}
+
+function testRightClick(aCategory, hudBox, aHud) {
+ let deferred = promise.defer();
+ let selector = ".webconsole-filter-button[category=\"" + aCategory + "\"]";
+ let button = hudBox.querySelector(selector);
+ let mainButton = getMainButton(button, aHud);
+ let origCheckedState = button.getAttribute("aria-pressed");
+ let contextMenu = aHud.iframeWindow.document.getElementById(aCategory + "-contextmenu");
+
+ function verifyContextMenuIsClosed() {
+ info("verify the context menu is closed");
+ is(button.getAttribute("open"), false, "The context menu for the \"" +
+ aCategory + "\" button is closed");
+ }
+
+ function verifyOriginalCheckedState() {
+ info("verify the button has the original checked state");
+ is(button.getAttribute("aria-pressed"), origCheckedState,
+ "The button state should not have changed");
+ }
+
+ function verifyNewCheckedState() {
+ info("verify the button's checked state has changed");
+ isnot(button.getAttribute("aria-pressed"), origCheckedState,
+ "The button state should have changed");
+ }
+
+ function leftClickToClose() {
+ info("left click the button to close the contextMenu");
+ EventUtils.sendMouseEvent({type: "click"}, button);
+ executeSoon(() => {
+ verifyContextMenuIsClosed();
+ verifyOriginalCheckedState();
+ leftClickToChangeCheckedState();
+ });
+ }
+
+ function leftClickToChangeCheckedState() {
+ info("left click the mainbutton to change checked state");
+ EventUtils.sendMouseEvent({type: "click"}, mainButton);
+ executeSoon(() => {
+ verifyContextMenuIsClosed();
+ verifyNewCheckedState();
+ deferred.resolve();
+ });
+ }
+
+ verifyContextMenuIsClosed();
+ info("right click the button to open the context menu");
+ waitForContextMenu(contextMenu, mainButton, verifyOriginalCheckedState,
+ leftClickToClose);
+ return deferred.promise;
+}
+
+function getMainButton(aTargetButton, aHud) {
+ let anonymousNodes = aHud.ui.document.getAnonymousNodes(aTargetButton);
+ let subbutton;
+
+ for (let i = 0; i < anonymousNodes.length; i++) {
+ let node = anonymousNodes[i];
+ if (node.classList.contains("toolbarbutton-menubutton-button")) {
+ subbutton = node;
+ break;
+ }
+ }
+
+ return subbutton;
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_for_of.js b/devtools/client/webconsole/test/browser_webconsole_for_of.js
new file mode 100644
index 000000000..83d3aaa3d
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_for_of.js
@@ -0,0 +1,32 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// A for-of loop in Web Console code can loop over a content NodeList.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-for-of.html";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+ yield testForOf(hud);
+});
+
+function testForOf(hud) {
+ let deferred = promise.defer();
+
+ let jsterm = hud.jsterm;
+ jsterm.execute("{ let _nodes = []; for (let x of document.body.childNodes) { if (x.nodeType === 1) { _nodes.push(x.tagName); } } _nodes.join(' '); }",
+ (node) => {
+ ok(/H1 DIV H2 P/.test(node.textContent),
+ "for-of loop should find all top-level nodes");
+ deferred.resolve();
+ });
+
+ return deferred.promise;
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_history.js b/devtools/client/webconsole/test/browser_webconsole_history.js
new file mode 100644
index 000000000..5ae709a4b
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_history.js
@@ -0,0 +1,62 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests the console history feature accessed via the up and down arrow keys.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console.html";
+
+// Constants used for defining the direction of JSTerm input history navigation.
+const HISTORY_BACK = -1;
+const HISTORY_FORWARD = 1;
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+ hud.jsterm.clearOutput();
+
+ let jsterm = hud.jsterm;
+ let input = jsterm.inputNode;
+
+ let executeList = ["document", "window", "window.location"];
+
+ for (let item of executeList) {
+ input.value = item;
+ yield jsterm.execute();
+ }
+
+ for (let x = executeList.length - 1; x != -1; x--) {
+ jsterm.historyPeruse(HISTORY_BACK);
+ is(input.value, executeList[x], "check history previous idx:" + x);
+ }
+
+ jsterm.historyPeruse(HISTORY_BACK);
+ is(input.value, executeList[0], "test that item is still index 0");
+
+ jsterm.historyPeruse(HISTORY_BACK);
+ is(input.value, executeList[0], "test that item is still still index 0");
+
+ for (let i = 1; i < executeList.length; i++) {
+ jsterm.historyPeruse(HISTORY_FORWARD);
+ is(input.value, executeList[i], "check history next idx:" + i);
+ }
+
+ jsterm.historyPeruse(HISTORY_FORWARD);
+ is(input.value, "", "check input is empty again");
+
+ // Simulate pressing Arrow_Down a few times and then if Arrow_Up shows
+ // the previous item from history again.
+ jsterm.historyPeruse(HISTORY_FORWARD);
+ jsterm.historyPeruse(HISTORY_FORWARD);
+ jsterm.historyPeruse(HISTORY_FORWARD);
+
+ is(input.value, "", "check input is still empty");
+
+ let idxLast = executeList.length - 1;
+ jsterm.historyPeruse(HISTORY_BACK);
+ is(input.value, executeList[idxLast], "check history next idx:" + idxLast);
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_hpkp_invalid-headers.js b/devtools/client/webconsole/test/browser_webconsole_hpkp_invalid-headers.js
new file mode 100644
index 000000000..3ee33669d
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_hpkp_invalid-headers.js
@@ -0,0 +1,126 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests that errors about invalid HPKP security headers are logged to the web
+// console.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8,Web Console HPKP invalid " +
+ "header test";
+const SJS_URL = "https://example.com/browser/devtools/client/webconsole/" +
+ "test/test_hpkp-invalid-headers.sjs";
+const LEARN_MORE_URI = "https://developer.mozilla.org/docs/Web/Security/" +
+ "Public_Key_Pinning" + DOCS_GA_PARAMS;
+const NON_BUILTIN_ROOT_PREF = "security.cert_pinning.process_headers_from_" +
+ "non_builtin_roots";
+
+add_task(function* () {
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref(NON_BUILTIN_ROOT_PREF);
+ });
+
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ yield* checkForMessage({
+ url: SJS_URL + "?badSyntax",
+ name: "Could not parse header error displayed successfully",
+ text: "Public-Key-Pins: The site specified a header that could not be " +
+ "parsed successfully."
+ }, hud);
+
+ yield* checkForMessage({
+ url: SJS_URL + "?noMaxAge",
+ name: "No max-age error displayed successfully",
+ text: "Public-Key-Pins: The site specified a header that did not include " +
+ "a \u2018max-age\u2019 directive."
+ }, hud);
+
+ yield* checkForMessage({
+ url: SJS_URL + "?invalidIncludeSubDomains",
+ name: "Invalid includeSubDomains error displayed successfully",
+ text: "Public-Key-Pins: The site specified a header that included an " +
+ "invalid \u2018includeSubDomains\u2019 directive."
+ }, hud);
+
+ yield* checkForMessage({
+ url: SJS_URL + "?invalidMaxAge",
+ name: "Invalid max-age error displayed successfully",
+ text: "Public-Key-Pins: The site specified a header that included an " +
+ "invalid \u2018max-age\u2019 directive."
+ }, hud);
+
+ yield* checkForMessage({
+ url: SJS_URL + "?multipleIncludeSubDomains",
+ name: "Multiple includeSubDomains error displayed successfully",
+ text: "Public-Key-Pins: The site specified a header that included " +
+ "multiple \u2018includeSubDomains\u2019 directives."
+ }, hud);
+
+ yield* checkForMessage({
+ url: SJS_URL + "?multipleMaxAge",
+ name: "Multiple max-age error displayed successfully",
+ text: "Public-Key-Pins: The site specified a header that included " +
+ "multiple \u2018max-age\u2019 directives."
+ }, hud);
+
+ yield* checkForMessage({
+ url: SJS_URL + "?multipleReportURIs",
+ name: "Multiple report-uri error displayed successfully",
+ text: "Public-Key-Pins: The site specified a header that included " +
+ "multiple \u2018report-uri\u2019 directives."
+ }, hud);
+
+ // The root used for mochitests is not built-in, so set the relevant pref to
+ // true to have the PKP implementation return more specific errors.
+ Services.prefs.setBoolPref(NON_BUILTIN_ROOT_PREF, true);
+
+ yield* checkForMessage({
+ url: SJS_URL + "?pinsetDoesNotMatch",
+ name: "Non-matching pinset error displayed successfully",
+ text: "Public-Key-Pins: The site specified a header that did not include " +
+ "a matching pin."
+ }, hud);
+
+ Services.prefs.setBoolPref(NON_BUILTIN_ROOT_PREF, false);
+
+ yield* checkForMessage({
+ url: SJS_URL + "?pinsetDoesNotMatch",
+ name: "Non-built-in root error displayed successfully",
+ text: "Public-Key-Pins: The certificate used by the site was not issued " +
+ "by a certificate in the default root certificate store. To " +
+ "prevent accidental breakage, the specified header was ignored."
+ }, hud);
+});
+
+function* checkForMessage(curTest, hud) {
+ hud.jsterm.clearOutput();
+
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, curTest.url);
+
+ let results = yield waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ name: curTest.name,
+ text: curTest.text,
+ category: CATEGORY_SECURITY,
+ severity: SEVERITY_WARNING,
+ objects: true,
+ },
+ ],
+ });
+
+ yield testClickOpenNewTab(hud, results);
+}
+
+function testClickOpenNewTab(hud, results) {
+ let warningNode = results[0].clickableElements[0];
+ ok(warningNode, "link element");
+ ok(warningNode.classList.contains("learn-more-link"), "link class name");
+ return simulateMessageLinkClick(warningNode, LEARN_MORE_URI);
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_hsts_invalid-headers.js b/devtools/client/webconsole/test/browser_webconsole_hsts_invalid-headers.js
new file mode 100644
index 000000000..19cedefdb
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_hsts_invalid-headers.js
@@ -0,0 +1,92 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests that errors about invalid HSTS security headers are logged
+// to the web console.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8,Web Console HSTS invalid " +
+ "header test";
+const SJS_URL = "https://example.com/browser/devtools/client/webconsole/" +
+ "test/test_hsts-invalid-headers.sjs";
+const LEARN_MORE_URI = "https://developer.mozilla.org/docs/Web/Security/" +
+ "HTTP_strict_transport_security" + DOCS_GA_PARAMS;
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ yield* checkForMessage({
+ url: SJS_URL + "?badSyntax",
+ name: "Could not parse header error displayed successfully",
+ text: "Strict-Transport-Security: The site specified a header that could " +
+ "not be parsed successfully."
+ }, hud);
+
+ yield* checkForMessage({
+ url: SJS_URL + "?noMaxAge",
+ name: "No max-age error displayed successfully",
+ text: "Strict-Transport-Security: The site specified a header that did " +
+ "not include a \u2018max-age\u2019 directive."
+ }, hud);
+
+ yield* checkForMessage({
+ url: SJS_URL + "?invalidIncludeSubDomains",
+ name: "Invalid includeSubDomains error displayed successfully",
+ text: "Strict-Transport-Security: The site specified a header that " +
+ "included an invalid \u2018includeSubDomains\u2019 directive."
+ }, hud);
+
+ yield* checkForMessage({
+ url: SJS_URL + "?invalidMaxAge",
+ name: "Invalid max-age error displayed successfully",
+ text: "Strict-Transport-Security: The site specified a header that " +
+ "included an invalid \u2018max-age\u2019 directive."
+ }, hud);
+
+ yield* checkForMessage({
+ url: SJS_URL + "?multipleIncludeSubDomains",
+ name: "Multiple includeSubDomains error displayed successfully",
+ text: "Strict-Transport-Security: The site specified a header that " +
+ "included multiple \u2018includeSubDomains\u2019 directives."
+ }, hud);
+
+ yield* checkForMessage({
+ url: SJS_URL + "?multipleMaxAge",
+ name: "Multiple max-age error displayed successfully",
+ text: "Strict-Transport-Security: The site specified a header that " +
+ "included multiple \u2018max-age\u2019 directives."
+ }, hud);
+});
+
+function* checkForMessage(curTest, hud) {
+ hud.jsterm.clearOutput();
+
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, curTest.url);
+
+ let results = yield waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ name: curTest.name,
+ text: curTest.text,
+ category: CATEGORY_SECURITY,
+ severity: SEVERITY_WARNING,
+ objects: true,
+ },
+ ],
+ });
+
+ yield testClickOpenNewTab(hud, results);
+}
+
+function testClickOpenNewTab(hud, results) {
+ let warningNode = results[0].clickableElements[0];
+ ok(warningNode, "link element");
+ ok(warningNode.classList.contains("learn-more-link"), "link class name");
+ return simulateMessageLinkClick(warningNode, LEARN_MORE_URI);
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_input_field_focus_on_panel_select.js b/devtools/client/webconsole/test/browser_webconsole_input_field_focus_on_panel_select.js
new file mode 100644
index 000000000..2d7fda7f5
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_input_field_focus_on_panel_select.js
@@ -0,0 +1,34 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Test that the JS input field is focused when the user switches back to the
+// web console from other tools, see bug 891581.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf8,<p>hello";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+ hud.jsterm.clearOutput();
+
+ is(hud.jsterm.inputNode.hasAttribute("focused"), true,
+ "inputNode should be focused");
+
+ hud.ui.filterBox.focus();
+
+ is(hud.ui.filterBox.hasAttribute("focused"), true,
+ "filterBox should be focused");
+
+ is(hud.jsterm.inputNode.hasAttribute("focused"), false,
+ "inputNode shouldn't be focused");
+
+ yield openInspector();
+ hud = yield openConsole();
+
+ is(hud.jsterm.inputNode.hasAttribute("focused"), true,
+ "inputNode should be focused");
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_inspect-parsed-documents.js b/devtools/client/webconsole/test/browser_webconsole_inspect-parsed-documents.js
new file mode 100644
index 000000000..f79ba386f
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_inspect-parsed-documents.js
@@ -0,0 +1,35 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Test that dynamically created (HTML|XML|SVG)Documents can be inspected by
+// clicking on the object in console (bug 1035198).
+
+"use strict";
+
+const TEST_CASES = [
+ {
+ input: '(new DOMParser()).parseFromString("<a />", "text/html")',
+ output: "HTMLDocument",
+ inspectable: true,
+ },
+ {
+ input: '(new DOMParser()).parseFromString("<a />", "application/xml")',
+ output: "XMLDocument",
+ inspectable: true,
+ },
+ {
+ input: '(new DOMParser()).parseFromString("<svg></svg>", "image/svg+xml")',
+ output: "XMLDocument",
+ inspectable: true,
+ },
+];
+
+const TEST_URI = "data:text/html;charset=utf8," +
+ "browser_webconsole_inspect-parsed-documents.js";
+add_task(function* () {
+ let {tab} = yield loadTab(TEST_URI);
+ let hud = yield openConsole(tab);
+ yield checkOutputForInputs(hud, TEST_CASES);
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_js_input_expansion.js b/devtools/client/webconsole/test/browser_webconsole_js_input_expansion.js
new file mode 100644
index 000000000..7d45059fc
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_js_input_expansion.js
@@ -0,0 +1,55 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests that the input box expands as the user types long lines.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console.html";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+ hud.jsterm.clearOutput();
+
+ let input = hud.jsterm.inputNode;
+ input.focus();
+
+ is(input.getAttribute("multiline"), "true", "multiline is enabled");
+ // Tests if the inputNode expands.
+ input.value = "hello\nworld\n";
+ let length = input.value.length;
+ input.selectionEnd = length;
+ input.selectionStart = length;
+ function getHeight() {
+ return input.clientHeight;
+ }
+ let initialHeight = getHeight();
+ // Performs an "d". This will trigger/test for the input event that should
+ // change the "row" attribute of the inputNode.
+ EventUtils.synthesizeKey("d", {});
+ let newHeight = getHeight();
+ ok(initialHeight < newHeight, "Height changed: " + newHeight);
+
+ // Add some more rows. Tests for the 8 row limit.
+ input.value = "row1\nrow2\nrow3\nrow4\nrow5\nrow6\nrow7\nrow8\nrow9\nrow10\n";
+ length = input.value.length;
+ input.selectionEnd = length;
+ input.selectionStart = length;
+ EventUtils.synthesizeKey("d", {});
+ let newerHeight = getHeight();
+
+ ok(newerHeight > newHeight, "height changed: " + newerHeight);
+
+ // Test if the inputNode shrinks again.
+ input.value = "";
+ EventUtils.synthesizeKey("d", {});
+ let height = getHeight();
+ info("height: " + height);
+ info("initialHeight: " + initialHeight);
+ let finalHeightDifference = Math.abs(initialHeight - height);
+ ok(finalHeightDifference <= 1, "height shrank to original size within 1px");
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_jsterm.js b/devtools/client/webconsole/test/browser_webconsole_jsterm.js
new file mode 100644
index 000000000..221c96fa6
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_jsterm.js
@@ -0,0 +1,195 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console.html";
+
+var jsterm;
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+ jsterm = hud.jsterm;
+ yield testJSTerm(hud);
+ jsterm = null;
+});
+
+function checkResult(msg, desc) {
+ let def = promise.defer();
+ waitForMessages({
+ webconsole: jsterm.hud.owner,
+ messages: [{
+ name: desc,
+ category: CATEGORY_OUTPUT,
+ }],
+ }).then(([result]) => {
+ let node = [...result.matched][0].querySelector(".message-body");
+ if (typeof msg == "string") {
+ is(node.textContent.trim(), msg,
+ "correct message shown for " + desc);
+ } else if (typeof msg == "function") {
+ ok(msg(node), "correct message shown for " + desc);
+ }
+
+ def.resolve();
+ });
+ return def.promise;
+}
+
+function* testJSTerm(hud) {
+ const HELP_URL = "https://developer.mozilla.org/docs/Tools/" +
+ "Web_Console/Helpers";
+
+ jsterm.clearOutput();
+ yield jsterm.execute("$('#header').getAttribute('id')");
+ yield checkResult('"header"', "$() worked");
+
+ jsterm.clearOutput();
+ yield jsterm.execute("$$('h1').length");
+ yield checkResult("1", "$$() worked");
+
+ jsterm.clearOutput();
+ yield jsterm.execute("$x('.//*', document.body)[0] == $$('h1')[0]");
+ yield checkResult("true", "$x() worked");
+
+ // no jsterm.clearOutput() here as we clear the output using the clear() fn.
+ yield jsterm.execute("clear()");
+
+ yield waitForSuccess({
+ name: "clear() worked",
+ validator: function () {
+ return jsterm.outputNode.childNodes.length == 0;
+ }
+ });
+
+ jsterm.clearOutput();
+ yield jsterm.execute("keys({b:1})[0] == 'b'");
+ yield checkResult("true", "keys() worked", 1);
+
+ jsterm.clearOutput();
+ yield jsterm.execute("values({b:1})[0] == 1");
+ yield checkResult("true", "values() worked", 1);
+
+ jsterm.clearOutput();
+
+ let openedLinks = 0;
+ let oldOpenLink = hud.openLink;
+ hud.openLink = (url) => {
+ if (url == HELP_URL) {
+ openedLinks++;
+ }
+ };
+
+ yield jsterm.execute("help()");
+ yield jsterm.execute("help");
+ yield jsterm.execute("?");
+
+ let output = jsterm.outputNode.querySelector(".message[category='output']");
+ ok(!output, "no output for help() calls");
+ is(openedLinks, 3, "correct number of pages opened by the help calls");
+ hud.openLink = oldOpenLink;
+
+ jsterm.clearOutput();
+ yield jsterm.execute("pprint({b:2, a:1})");
+ yield checkResult("\" b: 2\n a: 1\"", "pprint()");
+
+ // check instanceof correctness, bug 599940
+ jsterm.clearOutput();
+ yield jsterm.execute("[] instanceof Array");
+ yield checkResult("true", "[] instanceof Array == true");
+
+ jsterm.clearOutput();
+ yield jsterm.execute("({}) instanceof Object");
+ yield checkResult("true", "({}) instanceof Object == true");
+
+ // check for occurrences of Object XRayWrapper, bug 604430
+ jsterm.clearOutput();
+ yield jsterm.execute("document");
+ yield checkResult(function (node) {
+ return node.textContent.search(/\[object xraywrapper/i) == -1;
+ }, "document - no XrayWrapper");
+
+ // check that pprint(window) and keys(window) don't throw, bug 608358
+ jsterm.clearOutput();
+ yield jsterm.execute("pprint(window)");
+ yield checkResult(null, "pprint(window)");
+
+ jsterm.clearOutput();
+ yield jsterm.execute("keys(window)");
+ yield checkResult(null, "keys(window)");
+
+ // bug 614561
+ jsterm.clearOutput();
+ yield jsterm.execute("pprint('hi')");
+ yield checkResult("\" 0: \"h\"\n 1: \"i\"\"", "pprint('hi')");
+
+ // check that pprint(function) shows function source, bug 618344
+ jsterm.clearOutput();
+ yield jsterm.execute("pprint(function() { var someCanaryValue = 42; })");
+ yield checkResult(function (node) {
+ return node.textContent.indexOf("someCanaryValue") > -1;
+ }, "pprint(function) shows source");
+
+ // check that an evaluated null produces "null", bug 650780
+ jsterm.clearOutput();
+ yield jsterm.execute("null");
+ yield checkResult("null", "null is null");
+
+ jsterm.clearOutput();
+ yield jsterm.execute("undefined");
+ yield checkResult("undefined", "undefined is printed");
+
+ // check that thrown strings produce error messages,
+ // and the message text matches that of a stringified error object
+ // bug 1099071
+ jsterm.clearOutput();
+ yield jsterm.execute("throw '';");
+ yield checkResult((node) => {
+ return node.closest(".message").getAttribute("severity") === "error" &&
+ node.textContent === new Error("").toString();
+ }, "thrown empty string generates error message");
+
+ jsterm.clearOutput();
+ yield jsterm.execute("throw 'tomatoes';");
+ yield checkResult((node) => {
+ return node.closest(".message").getAttribute("severity") === "error" &&
+ node.textContent === new Error("tomatoes").toString();
+ }, "thrown non-empty string generates error message");
+
+ jsterm.clearOutput();
+ yield jsterm.execute("throw { foo: 'bar' };");
+ yield checkResult((node) => {
+ return node.closest(".message").getAttribute("severity") === "error" &&
+ node.textContent === Object.prototype.toString();
+ }, "thrown object generates error message");
+
+ // check that errors with entires in errordocs.js display links
+ // alongside their messages.
+ const ErrorDocs = require("devtools/server/actors/errordocs");
+
+ const ErrorDocStatements = {
+ "JSMSG_BAD_RADIX": "(42).toString(0);",
+ "JSMSG_BAD_ARRAY_LENGTH": "([]).length = -1",
+ "JSMSG_NEGATIVE_REPETITION_COUNT": "'abc'.repeat(-1);",
+ "JSMSG_BAD_FORMAL": "var f = Function('x y', 'return x + y;');",
+ "JSMSG_PRECISION_RANGE": "77.1234.toExponential(-1);",
+ };
+
+ for (let errorMessageName of Object.keys(ErrorDocStatements)) {
+ let title = ErrorDocs.GetURL({ errorMessageName }).split("?")[0];
+
+ jsterm.clearOutput();
+ yield jsterm.execute(ErrorDocStatements[errorMessageName]);
+ yield checkResult((node) => {
+ return node.parentNode.getElementsByTagName("a")[0].title == title;
+ }, `error links to ${title}`);
+ }
+
+ // Ensure that dom errors, with error numbers outside of the range
+ // of valid js.msg errors, don't cause crashes (bug 1270721).
+ yield jsterm.execute("new Request('',{redirect:'foo'})");
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_live_filtering_of_message_types.js b/devtools/client/webconsole/test/browser_webconsole_live_filtering_of_message_types.js
new file mode 100644
index 000000000..1dbfa80d9
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_live_filtering_of_message_types.js
@@ -0,0 +1,56 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests that the message type filter checkboxes work.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console.html";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+ hud.jsterm.clearOutput();
+
+ ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ for (let i = 0; i < 50; i++) {
+ content.console.log("foobarz #" + i);
+ }
+ });
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "foobarz #49",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+
+ is(hud.outputNode.children.length, 50, "number of messages");
+
+ hud.setFilterState("log", false);
+ is(countMessageNodes(hud), 0, "the log nodes are hidden when the " +
+ "corresponding filter is switched off");
+
+ hud.setFilterState("log", true);
+ is(countMessageNodes(hud), 50, "the log nodes reappear when the " +
+ "corresponding filter is switched on");
+});
+
+function countMessageNodes(hud) {
+ let messageNodes = hud.outputNode.querySelectorAll(".message");
+ let displayedMessageNodes = 0;
+ let view = hud.iframeWindow;
+ for (let i = 0; i < messageNodes.length; i++) {
+ let computedStyle = view.getComputedStyle(messageNodes[i], null);
+ if (computedStyle.display !== "none") {
+ displayedMessageNodes++;
+ }
+ }
+
+ return displayedMessageNodes;
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_live_filtering_on_search_strings.js b/devtools/client/webconsole/test/browser_webconsole_live_filtering_on_search_strings.js
new file mode 100644
index 000000000..d41d5cf2e
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_live_filtering_on_search_strings.js
@@ -0,0 +1,96 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests that the text filter box works.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console.html";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+ hud.jsterm.clearOutput();
+
+ ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ for (let i = 0; i < 50; i++) {
+ content.console.log("http://www.example.com/ " + i);
+ }
+ });
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "http://www.example.com/ 49",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+
+ is(hud.outputNode.children.length, 50, "number of messages");
+
+ setStringFilter(hud, "http");
+ isnot(countMessageNodes(hud), 0, "the log nodes are not hidden when the " +
+ "search string is set to \"http\"");
+
+ setStringFilter(hud, "hxxp");
+ is(countMessageNodes(hud), 0, "the log nodes are hidden when the search " +
+ "string is set to \"hxxp\"");
+
+ setStringFilter(hud, "ht tp");
+ isnot(countMessageNodes(hud), 0, "the log nodes are not hidden when the " +
+ "search string is set to \"ht tp\"");
+
+ setStringFilter(hud, " zzzz zzzz ");
+ is(countMessageNodes(hud), 0, "the log nodes are hidden when the search " +
+ "string is set to \" zzzz zzzz \"");
+
+ setStringFilter(hud, "");
+ isnot(countMessageNodes(hud), 0, "the log nodes are not hidden when the " +
+ "search string is removed");
+
+ setStringFilter(hud, "\u9f2c");
+ is(countMessageNodes(hud), 0, "the log nodes are hidden when searching " +
+ "for weasels");
+
+ setStringFilter(hud, "\u0007");
+ is(countMessageNodes(hud), 0, "the log nodes are hidden when searching for " +
+ "the bell character");
+
+ setStringFilter(hud, '"foo"');
+ is(countMessageNodes(hud), 0, "the log nodes are hidden when searching for " +
+ 'the string "foo"');
+
+ setStringFilter(hud, "'foo'");
+ is(countMessageNodes(hud), 0, "the log nodes are hidden when searching for " +
+ "the string 'foo'");
+
+ setStringFilter(hud, "foo\"bar'baz\"boo'");
+ is(countMessageNodes(hud), 0, "the log nodes are hidden when searching for " +
+ "the string \"foo\"bar'baz\"boo'\"");
+});
+
+function countMessageNodes(hud) {
+ let outputNode = hud.outputNode;
+
+ let messageNodes = outputNode.querySelectorAll(".message");
+ let displayedMessageNodes = 0;
+ let view = hud.iframeWindow;
+ for (let i = 0; i < messageNodes.length; i++) {
+ let computedStyle = view.getComputedStyle(messageNodes[i], null);
+ if (computedStyle.display !== "none") {
+ displayedMessageNodes++;
+ }
+ }
+
+ return displayedMessageNodes;
+}
+
+function setStringFilter(hud, value) {
+ hud.ui.filterBox.value = value;
+ hud.ui.adjustVisibilityOnSearchStringChange();
+}
+
diff --git a/devtools/client/webconsole/test/browser_webconsole_log_file_filter.js b/devtools/client/webconsole/test/browser_webconsole_log_file_filter.js
new file mode 100644
index 000000000..d5059485f
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_log_file_filter.js
@@ -0,0 +1,83 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests that the text filter box works to filter based on filenames
+// where the logs were generated.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-bug_923281_console_log_filter.html";
+
+var hud;
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ hud = yield openConsole();
+ yield consoleOpened();
+
+ testLiveFilteringOnSearchStrings();
+
+ hud = null;
+});
+
+function consoleOpened() {
+ let console = content.console;
+ console.log("sentinel log");
+ return waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "sentinel log",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG
+ }],
+ });
+}
+
+function testLiveFilteringOnSearchStrings() {
+ is(hud.outputNode.children.length, 4, "number of messages");
+
+ setStringFilter("random");
+ is(countMessageNodes(), 1, "the log nodes not containing string " +
+ "\"random\" are hidden");
+
+ setStringFilter("test2.js");
+ is(countMessageNodes(), 2, "show only log nodes containing string " +
+ "\"test2.js\" or log nodes created from files with filename " +
+ "containing \"test2.js\" as substring.");
+
+ setStringFilter("test1");
+ is(countMessageNodes(), 2, "show only log nodes containing string " +
+ "\"test1\" or log nodes created from files with filename " +
+ "containing \"test1\" as substring.");
+
+ setStringFilter("");
+ is(countMessageNodes(), 4, "show all log nodes on setting filter string " +
+ "as \"\".");
+}
+
+function countMessageNodes() {
+ let outputNode = hud.outputNode;
+
+ let messageNodes = outputNode.querySelectorAll(".message");
+ content.console.log(messageNodes.length);
+ let displayedMessageNodes = 0;
+ let view = hud.iframeWindow;
+ for (let i = 0; i < messageNodes.length; i++) {
+ let computedStyle = view.getComputedStyle(messageNodes[i], null);
+ if (computedStyle.display !== "none") {
+ displayedMessageNodes++;
+ }
+ }
+
+ return displayedMessageNodes;
+}
+
+function setStringFilter(value) {
+ hud.ui.filterBox.value = value;
+ hud.ui.adjustVisibilityOnSearchStringChange();
+}
+
diff --git a/devtools/client/webconsole/test/browser_webconsole_message_node_id.js b/devtools/client/webconsole/test/browser_webconsole_message_node_id.js
new file mode 100644
index 000000000..bec657740
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_message_node_id.js
@@ -0,0 +1,28 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console.html";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+
+ hud.jsterm.execute("console.log('a log message')");
+
+ let [result] = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "a log message",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+
+ let msg = [...result.matched][0];
+ ok(msg.getAttribute("id"), "log message has an ID");
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_multiline_input.js b/devtools/client/webconsole/test/browser_webconsole_multiline_input.js
new file mode 100644
index 000000000..7285c2127
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_multiline_input.js
@@ -0,0 +1,70 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+// Tests that the console waits for more input instead of evaluating
+// when valid, but incomplete, statements are present upon pressing enter
+// -or- when the user ends a line with shift + enter.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console.html";
+
+let SHOULD_ENTER_MULTILINE = [
+ {input: "function foo() {" },
+ {input: "var a = 1," },
+ {input: "var a = 1;", shiftKey: true },
+ {input: "function foo() { }", shiftKey: true },
+ {input: "function" },
+ {input: "(x) =>" },
+ {input: "let b = {" },
+ {input: "let a = [" },
+ {input: "{" },
+ {input: "{ bob: 3343," },
+ {input: "function x(y=" },
+ {input: "Array.from(" },
+ // shift + enter creates a new line despite parse errors
+ {input: "{2,}", shiftKey: true },
+];
+let SHOULD_EXECUTE = [
+ {input: "function foo() { }" },
+ {input: "var a = 1;" },
+ {input: "function foo() { var a = 1; }" },
+ {input: '"asdf"' },
+ {input: "99 + 3" },
+ {input: "1, 2, 3" },
+ // errors
+ {input: "function f(x) { let y = 1, }" },
+ {input: "function f(x=,) {" },
+ {input: "{2,}" },
+];
+
+add_task(function* () {
+ let { tab, browser } = yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+ let inputNode = hud.jsterm.inputNode;
+
+ for (let test of SHOULD_ENTER_MULTILINE) {
+ hud.jsterm.setInputValue(test.input);
+ EventUtils.synthesizeKey("VK_RETURN", { shiftKey: test.shiftKey });
+ let inputValue = hud.jsterm.getInputValue();
+ is(inputNode.selectionStart, inputNode.selectionEnd,
+ "selection is collapsed");
+ is(inputNode.selectionStart, inputValue.length,
+ "caret at end of multiline input");
+ let inputWithNewline = test.input + "\n";
+ is(inputValue, inputWithNewline, "Input value is correct");
+ }
+
+ for (let test of SHOULD_EXECUTE) {
+ hud.jsterm.setInputValue(test.input);
+ EventUtils.synthesizeKey("VK_RETURN", { shiftKey: test.shiftKey });
+ let inputValue = hud.jsterm.getInputValue();
+ is(inputNode.selectionStart, 0, "selection starts/ends at 0");
+ is(inputNode.selectionEnd, 0, "selection starts/ends at 0");
+ is(inputValue, "", "Input value is cleared");
+ }
+
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_netlogging.js b/devtools/client/webconsole/test/browser_webconsole_netlogging.js
new file mode 100644
index 000000000..63730c9b4
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_netlogging.js
@@ -0,0 +1,139 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests response logging for different request types.
+
+"use strict";
+
+// This test runs very slowly on linux32 debug - bug 1269977
+requestLongerTimeout(2);
+
+const TEST_NETWORK_REQUEST_URI =
+ "http://example.com/browser/devtools/client/webconsole/test/" +
+ "test-network-request.html";
+
+const TEST_DATA_JSON_CONTENT =
+ '{ id: "test JSON data", myArray: [ "foo", "bar", "baz", "biff" ] }';
+
+const PAGE_REQUEST_PREDICATE =
+ ({ request }) => request.url.endsWith("test-network-request.html");
+
+const TEST_DATA_REQUEST_PREDICATE =
+ ({ request }) => request.url.endsWith("test-data.json");
+
+add_task(function* testPageLoad() {
+ // Enable logging in the UI. Not needed to pass test but makes it easier
+ // to debug interactively.
+ yield new Promise(resolve => {
+ SpecialPowers.pushPrefEnv({"set":
+ [["devtools.webconsole.filter.networkinfo", true]
+ ]}, resolve);
+ });
+
+ let finishedRequest = waitForFinishedRequest(PAGE_REQUEST_PREDICATE);
+ let hud = yield loadPageAndGetHud(TEST_NETWORK_REQUEST_URI);
+ let request = yield finishedRequest;
+
+ ok(request, "Page load was logged");
+
+ let client = hud.ui.webConsoleClient;
+ let args = [request.actor];
+ const postData = yield getPacket(client, "getRequestPostData", args);
+ const responseContent = yield getPacket(client, "getResponseContent", args);
+
+ is(request.request.url, TEST_NETWORK_REQUEST_URI,
+ "Logged network entry is page load");
+ is(request.request.method, "GET", "Method is correct");
+ ok(!postData.postData.text, "No request body was stored");
+ ok(!postData.postDataDiscarded,
+ "Request body was not discarded");
+ is(responseContent.content.text.indexOf("<!DOCTYPE HTML>"), 0,
+ "Response body's beginning is okay");
+
+ yield closeTabAndToolbox();
+});
+
+add_task(function* testXhrGet() {
+ let hud = yield loadPageAndGetHud(TEST_NETWORK_REQUEST_URI);
+
+ let finishedRequest = waitForFinishedRequest(TEST_DATA_REQUEST_PREDICATE);
+ content.wrappedJSObject.testXhrGet();
+ let request = yield finishedRequest;
+
+ ok(request, "testXhrGet() was logged");
+
+ let client = hud.ui.webConsoleClient;
+ let args = [request.actor];
+ const postData = yield getPacket(client, "getRequestPostData", args);
+ const responseContent = yield getPacket(client, "getResponseContent", args);
+
+ is(request.request.method, "GET", "Method is correct");
+ ok(!postData.postData.text, "No request body was sent");
+ ok(!postData.postDataDiscarded,
+ "Request body was not discarded");
+ is(responseContent.content.text, TEST_DATA_JSON_CONTENT,
+ "Response is correct");
+
+ yield closeTabAndToolbox();
+});
+
+add_task(function* testXhrPost() {
+ let hud = yield loadPageAndGetHud(TEST_NETWORK_REQUEST_URI);
+
+ let finishedRequest = waitForFinishedRequest(TEST_DATA_REQUEST_PREDICATE);
+ content.wrappedJSObject.testXhrPost();
+ let request = yield finishedRequest;
+
+ ok(request, "testXhrPost() was logged");
+
+ let client = hud.ui.webConsoleClient;
+ let args = [request.actor];
+ const postData = yield getPacket(client, "getRequestPostData", args);
+ const responseContent = yield getPacket(client, "getResponseContent", args);
+
+ is(request.request.method, "POST", "Method is correct");
+ is(postData.postData.text, "Hello world!", "Request body was logged");
+ is(responseContent.content.text, TEST_DATA_JSON_CONTENT,
+ "Response is correct");
+
+ yield closeTabAndToolbox();
+});
+
+add_task(function* testFormSubmission() {
+ let pageLoadRequestFinished = waitForFinishedRequest(PAGE_REQUEST_PREDICATE);
+ let hud = yield loadPageAndGetHud(TEST_NETWORK_REQUEST_URI);
+
+ info("Waiting for the page load to be finished.");
+ yield pageLoadRequestFinished;
+
+ // The form POSTs to the page URL but over https (page over http).
+ let finishedRequest = waitForFinishedRequest(PAGE_REQUEST_PREDICATE);
+ ContentTask.spawn(gBrowser.selectedBrowser, { }, `function()
+ {
+ let form = content.document.querySelector("form");
+ form.submit();
+ }`);
+ let request = yield finishedRequest;
+
+ ok(request, "testFormSubmission() was logged");
+
+ let client = hud.ui.webConsoleClient;
+ let args = [request.actor];
+ const postData = yield getPacket(client, "getRequestPostData", args);
+ const responseContent = yield getPacket(client, "getResponseContent", args);
+
+ is(request.request.method, "POST", "Method is correct");
+ isnot(postData.postData.text
+ .indexOf("Content-Type: application/x-www-form-urlencoded"), -1,
+ "Content-Type is correct");
+ isnot(postData.postData.text
+ .indexOf("Content-Length: 20"), -1, "Content-length is correct");
+ isnot(postData.postData.text
+ .indexOf("name=foo+bar&age=144"), -1, "Form data is correct");
+ is(responseContent.content.text.indexOf("<!DOCTYPE HTML>"), 0,
+ "Response body's beginning is okay");
+
+ yield closeTabAndToolbox();
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_netlogging_basic.js b/devtools/client/webconsole/test/browser_webconsole_netlogging_basic.js
new file mode 100644
index 000000000..c6fa12401
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_netlogging_basic.js
@@ -0,0 +1,44 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the page's resources are displayed in the console as they're
+// loaded
+
+"use strict";
+
+const TEST_NETWORK_URI = "http://example.com/browser/devtools/client/" +
+ "webconsole/test/test-network.html" + "?_date=" +
+ Date.now();
+
+add_task(function* () {
+ yield loadTab("data:text/html;charset=utf-8,Web Console basic network " +
+ "logging test");
+ let hud = yield openConsole();
+
+ yield BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_NETWORK_URI);
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "running network console",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ },
+ {
+ text: "test-network.html",
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_LOG,
+ },
+ {
+ text: "testscript.js",
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_LOG,
+ },
+ {
+ text: "test-image.png",
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_LOG,
+ }],
+ });
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_netlogging_panel.js b/devtools/client/webconsole/test/browser_webconsole_netlogging_panel.js
new file mode 100644
index 000000000..b44b49453
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_netlogging_panel.js
@@ -0,0 +1,30 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that network log messages bring up the network panel.
+
+"use strict";
+
+const TEST_NETWORK_REQUEST_URI =
+ "http://example.com/browser/devtools/client/webconsole/test/" +
+ "test-network-request.html";
+
+add_task(function* () {
+ let finishedRequest = waitForFinishedRequest(({ request }) => {
+ return request.url.endsWith("test-network-request.html");
+ });
+
+ const hud = yield loadPageAndGetHud(TEST_NETWORK_REQUEST_URI);
+ let request = yield finishedRequest;
+
+ yield hud.ui.openNetworkPanel(request.actor);
+ let toolbox = gDevTools.getToolbox(hud.target);
+ is(toolbox.currentToolId, "netmonitor", "Network panel was opened");
+ let panel = toolbox.getCurrentPanel();
+ let selected = panel.panelWin.NetMonitorView.RequestsMenu.selectedItem;
+ is(selected.attachment.method, request.request.method,
+ "The correct request is selected");
+ is(selected.attachment.url, request.request.url,
+ "The correct request is definitely selected");
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_netlogging_reset_filter.js b/devtools/client/webconsole/test/browser_webconsole_netlogging_reset_filter.js
new file mode 100644
index 000000000..265bc7c00
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_netlogging_reset_filter.js
@@ -0,0 +1,95 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests that network log messages bring up the network panel and select the
+// right request even if it was previously filtered off.
+
+"use strict";
+
+const TEST_FILE_URI =
+ "http://example.com/browser/devtools/client/webconsole/test/" +
+ "test-network.html";
+const TEST_URI = "data:text/html;charset=utf8,<p>test file URI";
+
+var hud;
+
+add_task(function* () {
+ let Actions = require("devtools/client/netmonitor/actions/index");
+
+ let requests = [];
+ let { browser } = yield loadTab(TEST_URI);
+
+ yield pushPrefEnv();
+ hud = yield openConsole();
+ hud.jsterm.clearOutput();
+
+ HUDService.lastFinishedRequest.callback = request => requests.push(request);
+
+ let loaded = loadBrowser(browser);
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_FILE_URI);
+ yield loaded;
+
+ yield testMessages();
+ let htmlRequest = requests.find(e => e.request.url.endsWith("html"));
+ ok(htmlRequest, "htmlRequest was a html");
+
+ yield hud.ui.openNetworkPanel(htmlRequest.actor);
+ let toolbox = gDevTools.getToolbox(hud.target);
+ is(toolbox.currentToolId, "netmonitor", "Network panel was opened");
+
+ let panel = toolbox.getCurrentPanel();
+ let selected = panel.panelWin.NetMonitorView.RequestsMenu.selectedItem;
+ is(selected.attachment.method, htmlRequest.request.method,
+ "The correct request is selected");
+ is(selected.attachment.url, htmlRequest.request.url,
+ "The correct request is definitely selected");
+
+ // Filter out the HTML request.
+ panel.panelWin.gStore.dispatch(Actions.toggleFilterType("js"));
+
+ yield toolbox.selectTool("webconsole");
+ is(toolbox.currentToolId, "webconsole", "Web console was selected");
+ yield hud.ui.openNetworkPanel(htmlRequest.actor);
+
+ panel.panelWin.NetMonitorView.RequestsMenu.selectedItem;
+ is(selected.attachment.method, htmlRequest.request.method,
+ "The correct request is selected");
+ is(selected.attachment.url, htmlRequest.request.url,
+ "The correct request is definitely selected");
+
+ // All tests are done. Shutdown.
+ HUDService.lastFinishedRequest.callback = null;
+ htmlRequest = browser = requests = hud = null;
+});
+
+function testMessages() {
+ return waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "running network console logging tests",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ },
+ {
+ text: "test-network.html",
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_LOG,
+ },
+ {
+ text: "testscript.js",
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_LOG,
+ }],
+ });
+}
+
+function pushPrefEnv() {
+ let deferred = promise.defer();
+ let options = {
+ set: [["devtools.webconsole.filter.networkinfo", true]]
+ };
+ SpecialPowers.pushPrefEnv(options, deferred.resolve);
+ return deferred.promise;
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_notifications.js b/devtools/client/webconsole/test/browser_webconsole_notifications.js
new file mode 100644
index 000000000..4bda9192f
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_notifications.js
@@ -0,0 +1,77 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 TEST_URI = "data:text/html;charset=utf-8,<p>Web Console test for " +
+ "notifications";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let consoleOpened = promise.defer();
+ let gotEvents = waitForEvents(consoleOpened.promise);
+ yield openConsole().then(() => {
+ consoleOpened.resolve();
+ });
+
+ yield gotEvents;
+});
+
+function waitForEvents(onConsoleOpened) {
+ let deferred = promise.defer();
+
+ function webConsoleCreated(id) {
+ Services.obs.removeObserver(observer, "web-console-created");
+ ok(HUDService.getHudReferenceById(id), "We have a hud reference");
+ content.wrappedJSObject.console.log("adding a log message");
+ }
+
+ function webConsoleDestroyed(id) {
+ Services.obs.removeObserver(observer, "web-console-destroyed");
+ ok(!HUDService.getHudReferenceById(id), "We do not have a hud reference");
+ executeSoon(deferred.resolve);
+ }
+
+ function webConsoleMessage(id, nodeID) {
+ Services.obs.removeObserver(observer, "web-console-message-created");
+ ok(id, "we have a console ID");
+ is(typeof nodeID, "string", "message node id is a string");
+ onConsoleOpened.then(closeConsole);
+ }
+
+ let observer = {
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
+
+ observe: function observe(subject, topic, data) {
+ subject = subject.QueryInterface(Ci.nsISupportsString);
+
+ switch (topic) {
+ case "web-console-created":
+ webConsoleCreated(subject.data);
+ break;
+ case "web-console-destroyed":
+ webConsoleDestroyed(subject.data);
+ break;
+ case "web-console-message-created":
+ webConsoleMessage(subject, data);
+ break;
+ default:
+ break;
+ }
+ },
+
+ init: function init() {
+ Services.obs.addObserver(this, "web-console-created", false);
+ Services.obs.addObserver(this, "web-console-destroyed", false);
+ Services.obs.addObserver(this, "web-console-message-created", false);
+ }
+ };
+
+ observer.init();
+
+ return deferred.promise;
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_open-links-without-callback.js b/devtools/client/webconsole/test/browser_webconsole_open-links-without-callback.js
new file mode 100644
index 000000000..ae11305de
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_open-links-without-callback.js
@@ -0,0 +1,54 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests that if a link without an onclick callback is clicked the link is
+// opened in a new tab and no exception occurs (bug 999236).
+
+"use strict";
+
+function test() {
+ function* runner() {
+ const TEST_EVAL_STRING = "document";
+ const TEST_PAGE_URI = "http://example.com/browser/devtools/client/" +
+ "webconsole/test/test-console.html";
+ const {tab} = yield loadTab(TEST_PAGE_URI);
+ const hud = yield openConsole(tab);
+
+ hud.jsterm.execute(TEST_EVAL_STRING);
+
+ const EXPECTED_OUTPUT = new RegExp("HTMLDocument \.+");
+
+ let messages = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "JS eval output",
+ text: EXPECTED_OUTPUT,
+ category: CATEGORY_OUTPUT,
+ }],
+ });
+
+ let messageNode = messages[0].matched.values().next().value;
+
+ // The correct anchor is second in the message node; the first anchor has
+ // class .cm-variable. Ignore the first one by not matching anchors that
+ // have the class .cm-variable.
+ let urlNode = messageNode.querySelector("a:not(.cm-variable)");
+
+ let linkOpened = false;
+ let oldOpenUILinkIn = window.openUILinkIn;
+ window.openUILinkIn = function (aLink) {
+ if (aLink == TEST_PAGE_URI) {
+ linkOpened = true;
+ }
+ };
+
+ EventUtils.synthesizeMouseAtCenter(urlNode, {}, hud.iframeWindow);
+
+ ok(linkOpened, "Clicking the URL opens the desired page");
+ window.openUILinkIn = oldOpenUILinkIn;
+ }
+
+ Task.spawn(runner).then(finishTest);
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_output_01.js b/devtools/client/webconsole/test/browser_webconsole_output_01.js
new file mode 100644
index 000000000..c75577ea7
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_output_01.js
@@ -0,0 +1,122 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Whitelisting this test.
+// As part of bug 1077403, the leaking uncaught rejection should be fixed.
+//
+
+"use strict";
+
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed("null");
+
+// Test the webconsole output for various types of objects.
+
+const TEST_URI = "data:text/html;charset=utf8,test for console output - 01";
+
+var {DebuggerServer} = require("devtools/server/main");
+
+var longString = (new Array(DebuggerServer.LONG_STRING_LENGTH + 4)).join("a");
+var initialString = longString.substring(0, DebuggerServer.LONG_STRING_INITIAL_LENGTH);
+
+var inputTests = [
+ // 0
+ {
+ input: "'hello \\nfrom \\rthe \\\"string world!'",
+ output: "\"hello \nfrom \rthe \"string world!\"",
+ consoleOutput: "hello \nfrom \rthe \"string world!",
+ },
+
+ // 1
+ {
+ // unicode test
+ input: "'\xFA\u1E47\u0129\xE7\xF6d\xEA \u021B\u0115\u0219\u0165'",
+ output: "\"\xFA\u1E47\u0129\xE7\xF6d\xEA \u021B\u0115\u0219\u0165\"",
+ consoleOutput: "\xFA\u1E47\u0129\xE7\xF6d\xEA \u021B\u0115\u0219\u0165",
+ },
+
+ // 2
+ {
+ input: "'" + longString + "'",
+ output: '"' + initialString + "\"[\u2026]",
+ consoleOutput: initialString + "[\u2026]",
+ printOutput: initialString,
+ },
+
+ // 3
+ {
+ input: "''",
+ output: '""',
+ consoleOutput: "",
+ printOutput: '""',
+ },
+
+ // 4
+ {
+ input: "0",
+ output: "0",
+ },
+
+ // 5
+ {
+ input: "'0'",
+ output: '"0"',
+ consoleOutput: "0",
+ },
+
+ // 6
+ {
+ input: "42",
+ output: "42",
+ },
+
+ // 7
+ {
+ input: "'42'",
+ output: '"42"',
+ consoleOutput: "42",
+ },
+
+ // 8
+ {
+ input: "/foobar/",
+ output: "/foobar/",
+ inspectable: true,
+ },
+
+ // 9
+ {
+ input: "Symbol()",
+ output: "Symbol()"
+ },
+
+ // 10
+ {
+ input: "Symbol('foo')",
+ output: "Symbol(foo)"
+ },
+
+ // 11
+ {
+ input: "Symbol.iterator",
+ output: "Symbol(Symbol.iterator)"
+ },
+];
+
+longString = initialString = null;
+
+function test() {
+ requestLongerTimeout(2);
+
+ Task.spawn(function* () {
+ let {tab} = yield loadTab(TEST_URI);
+ let hud = yield openConsole(tab);
+ return checkOutputForInputs(hud, inputTests);
+ }).then(finishUp);
+}
+
+function finishUp() {
+ longString = initialString = inputTests = null;
+ finishTest();
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_output_02.js b/devtools/client/webconsole/test/browser_webconsole_output_02.js
new file mode 100644
index 000000000..8018669a9
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_output_02.js
@@ -0,0 +1,183 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Test the webconsole output for various types of objects.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console-output-02.html";
+
+var inputTests = [
+ // 0 - native named function
+ {
+ input: "document.getElementById",
+ output: "function getElementById()",
+ printOutput: "function getElementById() {\n [native code]\n}",
+ inspectable: true,
+ variablesViewLabel: "getElementById()",
+ },
+
+ // 1 - anonymous function
+ {
+ input: "(function() { return 42; })",
+ output: "function ()",
+ printOutput: "function () { return 42; }",
+ suppressClick: true
+ },
+
+ // 2 - named function
+ {
+ input: "window.testfn1",
+ output: "function testfn1()",
+ printOutput: "function testfn1() { return 42; }",
+ suppressClick: true
+ },
+
+ // 3 - anonymous function, but spidermonkey gives us an inferred name.
+ {
+ input: "testobj1.testfn2",
+ output: "function testobj1.testfn2()",
+ printOutput: "function () { return 42; }",
+ suppressClick: true
+ },
+
+ // 4 - named function with custom display name
+ {
+ input: "window.testfn3",
+ output: "function testfn3DisplayName()",
+ printOutput: "function testfn3() { return 42; }",
+ suppressClick: true
+ },
+
+ // 5 - basic array
+ {
+ input: "window.array1",
+ output: 'Array [ 1, 2, 3, "a", "b", "c", "4", "5" ]',
+ printOutput: "1,2,3,a,b,c,4,5",
+ inspectable: true,
+ variablesViewLabel: "Array[8]",
+ },
+
+ // 6 - array with objects
+ {
+ input: "window.array2",
+ output: 'Array [ "a", HTMLDocument \u2192 test-console-output-02.html, ' +
+ "<body>, DOMStringMap[0], DOMTokenList[0] ]",
+ printOutput: '"a,[object HTMLDocument],[object HTMLBodyElement],' +
+ '[object DOMStringMap],"',
+ inspectable: true,
+ variablesViewLabel: "Array[5]",
+ },
+
+ // 7 - array with more than 10 elements
+ {
+ input: "window.array3",
+ output: "Array [ 1, Window \u2192 test-console-output-02.html, null, " +
+ '"a", "b", undefined, false, "", -Infinity, ' +
+ "testfn3DisplayName(), 3 more\u2026 ]",
+ printOutput: '"1,[object Window],,a,b,,false,,-Infinity,' +
+ 'function testfn3() { return 42; },[object Object],foo,bar"',
+ inspectable: true,
+ variablesViewLabel: "Array[13]",
+ },
+
+ // 8 - array with holes and a cyclic reference
+ {
+ input: "window.array4",
+ output: 'Array [ <5 empty slots>, "test", Array[7] ]',
+ printOutput: '",,,,,test,"',
+ inspectable: true,
+ variablesViewLabel: "Array[7]",
+ },
+
+ // 9
+ {
+ input: "window.typedarray1",
+ output: "Int32Array [ 1, 287, 8651, 40983, 8754 ]",
+ printOutput: "1,287,8651,40983,8754",
+ inspectable: true,
+ variablesViewLabel: "Int32Array[5]",
+ },
+
+ // 10 - Set with cyclic reference
+ {
+ input: "window.set1",
+ output: 'Set [ 1, 2, null, Array[13], "a", "b", undefined, <head>, ' +
+ "Set[9] ]",
+ printOutput: "[object Set]",
+ inspectable: true,
+ variablesViewLabel: "Set[9]",
+ },
+
+ // 11 - Object with cyclic reference and a getter
+ {
+ input: "window.testobj2",
+ output: 'Object { a: "b", c: "d", e: 1, f: "2", foo: Object, ' +
+ "bar: Object, getterTest: Getter }",
+ printOutput: "[object Object]",
+ inspectable: true,
+ variablesViewLabel: "Object",
+ },
+
+ // 12 - Object with more than 10 properties
+ {
+ input: "window.testobj3",
+ output: 'Object { a: "b", c: "d", e: 1, f: "2", g: true, h: null, ' +
+ 'i: undefined, j: "", k: StyleSheetList[0], l: NodeList[5], ' +
+ "2 more\u2026 }",
+ printOutput: "[object Object]",
+ inspectable: true,
+ variablesViewLabel: "Object",
+ },
+
+ // 13 - Object with a non-enumerable property that we do not show
+ {
+ input: "window.testobj4",
+ output: 'Object { a: "b", c: "d", 1 more\u2026 }',
+ printOutput: "[object Object]",
+ inspectable: true,
+ variablesViewLabel: "Object",
+ },
+
+ // 14 - Map with cyclic references
+ {
+ input: "window.map1",
+ output: 'Map { a: "b", HTMLCollection[2]: Object, Map[3]: Set[9] }',
+ printOutput: "[object Map]",
+ inspectable: true,
+ variablesViewLabel: "Map[3]",
+ },
+
+ // 15 - WeakSet
+ {
+ input: "window.weakset",
+ // Need a regexp because the order may vary.
+ output: new RegExp("WeakSet \\[ (String, <head>|<head>, String) \\]"),
+ printOutput: "[object WeakSet]",
+ inspectable: true,
+ variablesViewLabel: "WeakSet[2]",
+ },
+
+ // 16 - WeakMap
+ {
+ input: "window.weakmap",
+ // Need a regexp because the order may vary.
+ output: new RegExp("WeakMap { (String: 23, HTMLCollection\\[2\\]: Object|HTMLCollection\\[2\\]: Object, String: 23) }"),
+ printOutput: "[object WeakMap]",
+ inspectable: true,
+ variablesViewLabel: "WeakMap[2]",
+ },
+];
+
+function test() {
+ requestLongerTimeout(2);
+ Task.spawn(function* () {
+ const {tab} = yield loadTab(TEST_URI);
+ const hud = yield openConsole(tab);
+ yield checkOutputForInputs(hud, inputTests);
+ inputTests = null;
+ }).then(finishTest);
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_output_03.js b/devtools/client/webconsole/test/browser_webconsole_output_03.js
new file mode 100644
index 000000000..bd77c2a4d
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_output_03.js
@@ -0,0 +1,168 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Test the webconsole output for various types of objects.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console-output-03.html";
+
+var inputTests = [
+
+ // 0
+ {
+ input: "document",
+ output: "HTMLDocument \u2192 " + TEST_URI,
+ printOutput: "[object HTMLDocument]",
+ inspectable: true,
+ noClick: true,
+ },
+
+ // 1
+ {
+ input: "window",
+ output: "Window \u2192 " + TEST_URI,
+ printOutput: "[object Window",
+ inspectable: true,
+ noClick: true,
+ },
+
+ // 2
+ {
+ input: "document.body",
+ output: "<body>",
+ printOutput: "[object HTMLBodyElement]",
+ inspectable: true,
+ noClick: true,
+ },
+
+ // 3
+ {
+ input: "document.body.dataset",
+ output: "DOMStringMap { }",
+ printOutput: "[object DOMStringMap]",
+ inspectable: true,
+ variablesViewLabel: "DOMStringMap[0]",
+ },
+
+ // 4
+ {
+ input: "document.body.classList",
+ output: "DOMTokenList [ ]",
+ printOutput: '""',
+ inspectable: true,
+ variablesViewLabel: "DOMTokenList[0]",
+ },
+
+ // 5
+ {
+ input: "window.location.href",
+ output: '"' + TEST_URI + '"',
+ noClick: true,
+ },
+
+ // 6
+ {
+ input: "window.location",
+ output: "Location \u2192 " + TEST_URI,
+ printOutput: TEST_URI,
+ inspectable: true,
+ variablesViewLabel: "Location \u2192 test-console-output-03.html",
+ },
+
+ // 7
+ {
+ input: "document.body.attributes",
+ output: "NamedNodeMap [ ]",
+ printOutput: "[object NamedNodeMap]",
+ inspectable: true,
+ variablesViewLabel: "NamedNodeMap[0]",
+ },
+
+ // 8
+ {
+ input: "document.styleSheets",
+ output: "StyleSheetList [ ]",
+ printOutput: "[object StyleSheetList",
+ inspectable: true,
+ variablesViewLabel: "StyleSheetList[0]",
+ },
+
+ // 9
+ {
+ input: "testBodyClassName()",
+ output: '<body class="test1 tezt2">',
+ printOutput: "[object HTMLBodyElement]",
+ inspectable: true,
+ noClick: true,
+ },
+
+ // 10
+ {
+ input: "testBodyID()",
+ output: '<body class="test1 tezt2" id="foobarid">',
+ printOutput: "[object HTMLBodyElement]",
+ inspectable: true,
+ noClick: true,
+ },
+
+ // 11
+ {
+ input: "document.body.classList",
+ output: 'DOMTokenList [ "test1", "tezt2" ]',
+ printOutput: '"test1 tezt2"',
+ inspectable: true,
+ variablesViewLabel: "DOMTokenList[2]",
+ },
+
+ // 12
+ {
+ input: "testBodyDataset()",
+ output: '<body class="test1 tezt2" id="foobarid"' +
+ ' data-preview="zuzu&quot;&lt;a&gt;foo">',
+ printOutput: "[object HTMLBodyElement]",
+ inspectable: true,
+ noClick: true,
+ },
+
+ // 13
+ {
+ input: "document.body.dataset",
+ output: 'DOMStringMap { preview: "zuzu"<a>foo" }',
+ printOutput: "[object DOMStringMap]",
+ inspectable: true,
+ variablesViewLabel: "DOMStringMap[1]",
+ },
+
+ // 14
+ {
+ input: "document.body.attributes",
+ output: 'NamedNodeMap [ class="test1 tezt2", id="foobarid", ' +
+ 'data-preview="zuzu&quot;&lt;a&gt;foo" ]',
+ printOutput: "[object NamedNodeMap]",
+ inspectable: true,
+ variablesViewLabel: "NamedNodeMap[3]",
+ },
+
+ // 15
+ {
+ input: "document.body.attributes[0]",
+ output: 'class="test1 tezt2"',
+ printOutput: "[object Attr]",
+ inspectable: true,
+ variablesViewLabel: 'class="test1 tezt2"',
+ },
+];
+
+function test() {
+ requestLongerTimeout(2);
+ Task.spawn(function* () {
+ const {tab} = yield loadTab(TEST_URI);
+ const hud = yield openConsole(tab);
+ yield checkOutputForInputs(hud, inputTests);
+ inputTests = null;
+ }).then(finishTest);
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_output_04.js b/devtools/client/webconsole/test/browser_webconsole_output_04.js
new file mode 100644
index 000000000..d829594a7
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_output_04.js
@@ -0,0 +1,129 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Whitelisting this test.
+// As part of bug 1077403, the leaking uncaught rejection should be fixed.
+//
+
+"use strict";
+
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed("null");
+
+// Test the webconsole output for various types of objects.
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console-output-04.html";
+
+var inputTests = [
+ // 0
+ {
+ input: "testTextNode()",
+ output: '#text "hello world!"',
+ printOutput: "[object Text]",
+ inspectable: true,
+ noClick: true,
+ },
+
+ // 1
+ {
+ input: "testCommentNode()",
+ output: /<!--\s+- Any copyright /,
+ printOutput: "[object Comment]",
+ inspectable: true,
+ noClick: true,
+ },
+
+ // 2
+ {
+ input: "testDocumentFragment()",
+ output: "DocumentFragment [ <div#foo1.bar>, <div#foo3> ]",
+ printOutput: "[object DocumentFragment]",
+ inspectable: true,
+ variablesViewLabel: "DocumentFragment[2]",
+ },
+
+ // 3
+ {
+ input: "testError()",
+ output: "TypeError: window.foobar is not a function\n" +
+ "Stack trace:\n" +
+ "testError@" + TEST_URI + ":44",
+ printOutput: '"TypeError: window.foobar is not a function"',
+ inspectable: true,
+ variablesViewLabel: "TypeError",
+ },
+
+ // 4
+ {
+ input: "testDOMException()",
+ output: `DOMException [SyntaxError: "'foo;()bar!' is not a valid selector"`,
+ printOutput: `"SyntaxError: 'foo;()bar!' is not a valid selector"`,
+ inspectable: true,
+ variablesViewLabel: "SyntaxError",
+ },
+
+ // 5
+ {
+ input: "testCSSStyleDeclaration()",
+ output: 'CSS2Properties { color: "green", font-size: "2em" }',
+ printOutput: "[object CSS2Properties]",
+ inspectable: true,
+ noClick: true,
+ },
+
+ // 6
+ {
+ input: "testStyleSheetList()",
+ output: "StyleSheetList [ CSSStyleSheet ]",
+ printOutput: "[object StyleSheetList",
+ inspectable: true,
+ variablesViewLabel: "StyleSheetList[1]",
+ },
+
+ // 7
+ {
+ input: "document.styleSheets[0]",
+ output: "CSSStyleSheet",
+ printOutput: "[object CSSStyleSheet]",
+ inspectable: true,
+ },
+
+ // 8
+ {
+ input: "document.styleSheets[0].cssRules",
+ output: "CSSRuleList [ CSSStyleRule, CSSMediaRule ]",
+ printOutput: "[object CSSRuleList",
+ inspectable: true,
+ variablesViewLabel: "CSSRuleList[2]",
+ },
+
+ // 9
+ {
+ input: "document.styleSheets[0].cssRules[0]",
+ output: 'CSSStyleRule "p, div"',
+ printOutput: "[object CSSStyleRule",
+ inspectable: true,
+ variablesViewLabel: "CSSStyleRule",
+ },
+
+ // 10
+ {
+ input: "document.styleSheets[0].cssRules[1]",
+ output: 'CSSMediaRule "print"',
+ printOutput: "[object CSSMediaRule",
+ inspectable: true,
+ variablesViewLabel: "CSSMediaRule",
+ },
+];
+
+function test() {
+ requestLongerTimeout(2);
+ Task.spawn(function* () {
+ const {tab} = yield loadTab(TEST_URI);
+ const hud = yield openConsole(tab);
+ yield checkOutputForInputs(hud, inputTests);
+ inputTests = null;
+ }).then(finishTest);
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_output_05.js b/devtools/client/webconsole/test/browser_webconsole_output_05.js
new file mode 100644
index 000000000..53bfd768c
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_output_05.js
@@ -0,0 +1,177 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Test the webconsole output for various types of objects.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf8,test for console output - 05";
+const {ELLIPSIS} = require("devtools/shared/l10n");
+
+// March, 1960: The first implementation of Lisp. From Wikipedia:
+//
+// > Lisp was first implemented by Steve Russell on an IBM 704 computer. Russell
+// > had read McCarthy's paper, and realized (to McCarthy's surprise) that the
+// > Lisp eval function could be implemented in machine code. The result was a
+// > working Lisp interpreter which could be used to run Lisp programs, or more
+// > properly, 'evaluate Lisp expressions.'
+var testDate = -310435200000;
+
+var inputTests = [
+ // 0
+ {
+ input: "/foo?b*\\s\"ar/igym",
+ output: "/foo?b*\\s\"ar/gimy",
+ printOutput: "/foo?b*\\s\"ar/gimy",
+ inspectable: true,
+ },
+
+ // 1
+ {
+ input: "null",
+ output: "null",
+ },
+
+ // 2
+ {
+ input: "undefined",
+ output: "undefined",
+ },
+
+ // 3
+ {
+ input: "true",
+ output: "true",
+ },
+
+ // 4
+ {
+ input: "new Boolean(false)",
+ output: "Boolean { false }",
+ printOutput: "false",
+ inspectable: true,
+ variablesViewLabel: "Boolean { false }"
+ },
+
+ // 5
+ {
+ input: "new Date(" + testDate + ")",
+ output: "Date " + (new Date(testDate)).toISOString(),
+ printOutput: (new Date(testDate)).toString(),
+ inspectable: true,
+ },
+
+ // 6
+ {
+ input: "new Date('test')",
+ output: "Invalid Date",
+ printOutput: "Invalid Date",
+ inspectable: true,
+ variablesViewLabel: "Invalid Date",
+ },
+
+ // 7
+ {
+ input: "Date.prototype",
+ output: /Object \{.*\}/,
+ printOutput: "Invalid Date",
+ inspectable: true,
+ variablesViewLabel: "Object",
+ },
+
+ // 8
+ {
+ input: "new Number(43)",
+ output: "Number { 43 }",
+ printOutput: "43",
+ inspectable: true,
+ variablesViewLabel: "Number { 43 }"
+ },
+
+ // 9
+ {
+ input: "new String('hello')",
+ output: /String { "hello", 6 more.* }/,
+ printOutput: "hello",
+ inspectable: true,
+ variablesViewLabel: "String"
+ },
+
+ // 10
+ {
+ input: "(function () { var s = new String('hello'); s.whatever = 23; " +
+ " return s;})()",
+ output: /String { "hello", whatever: 23, 6 more.* }/,
+ printOutput: "hello",
+ inspectable: true,
+ variablesViewLabel: "String"
+ },
+
+ // 11
+ {
+ input: "(function () { var s = new String('hello'); s[8] = 'x'; " +
+ " return s;})()",
+ output: /String { "hello", 8: "x", 6 more.* }/,
+ printOutput: "hello",
+ inspectable: true,
+ variablesViewLabel: "String"
+ },
+
+ // 12
+ {
+ // XXX: Can't test fulfilled and rejected promises, because promises get
+ // settled on the next tick of the event loop.
+ input: "new Promise(function () {})",
+ output: 'Promise { <state>: "pending" }',
+ printOutput: "[object Promise]",
+ inspectable: true,
+ variablesViewLabel: "Promise"
+ },
+
+ // 13
+ {
+ input: "(function () { var p = new Promise(function () {}); " +
+ "p.foo = 1; return p; }())",
+ output: 'Promise { <state>: "pending", foo: 1 }',
+ printOutput: "[object Promise]",
+ inspectable: true,
+ variablesViewLabel: "Promise"
+ },
+
+ // 14
+ {
+ input: "new Object({1: 'this\\nis\\nsupposed\\nto\\nbe\\na\\nvery" +
+ "\\nlong\\nstring\\n,shown\\non\\na\\nsingle\\nline', " +
+ "2: 'a shorter string', 3: 100})",
+ output: '[ <1 empty slot>, "this is supposed to be a very long ' + ELLIPSIS +
+ '", "a shorter string", 100 ]',
+ printOutput: "[object Object]",
+ inspectable: true,
+ variablesViewLabel: "Object[4]"
+ },
+
+ // 15
+ {
+ input: "new Proxy({a:1},[1,2,3])",
+ output: 'Proxy { <target>: Object, <handler>: Array[3] }',
+ printOutput: "[object Object]",
+ inspectable: true,
+ variablesViewLabel: "Proxy"
+ }
+];
+
+function test() {
+ requestLongerTimeout(2);
+ Task.spawn(function* () {
+ let {tab} = yield loadTab(TEST_URI);
+ let hud = yield openConsole(tab);
+ return checkOutputForInputs(hud, inputTests);
+ }).then(finishUp);
+}
+
+function finishUp() {
+ inputTests = testDate = null;
+ finishTest();
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_output_06.js b/devtools/client/webconsole/test/browser_webconsole_output_06.js
new file mode 100644
index 000000000..ad69b3908
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_output_06.js
@@ -0,0 +1,283 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 webconsole output for various arrays.
+
+const TEST_URI = "data:text/html;charset=utf8,test for console output - 06";
+const {ELLIPSIS} = require("devtools/shared/l10n");
+
+const testStrIn = "SHOW\\nALL\\nOF\\nTHIS\\nON\\nA\\nSINGLE" +
+ "\\nLINE ONLY. ESCAPE ALL NEWLINE";
+const testStrOut = "SHOW ALL OF THIS ON A SINGLE LINE O" + ELLIPSIS;
+
+var inputTests = [
+ // 1 - array with empty slots only
+ {
+ input: "Array(5)",
+ output: "Array [ <5 empty slots> ]",
+ printOutput: ",,,,",
+ inspectable: true,
+ variablesViewLabel: "Array[5]",
+ },
+ // 2 - array with one empty slot at the beginning
+ {
+ input: "[,1,2,3]",
+ output: "Array [ <1 empty slot>, 1, 2, 3 ]",
+ printOutput: ",1,2,3",
+ inspectable: true,
+ variablesViewLabel: "Array[4]",
+ },
+ // 3 - array with multiple consecutive empty slots at the beginning
+ {
+ input: "[,,,3,4,5]",
+ output: "Array [ <3 empty slots>, 3, 4, 5 ]",
+ printOutput: ",,,3,4,5",
+ inspectable: true,
+ variablesViewLabel: "Array[6]",
+ },
+ // 4 - array with one empty slot at the middle
+ {
+ input: "[0,1,,3,4,5]",
+ output: "Array [ 0, 1, <1 empty slot>, 3, 4, 5 ]",
+ printOutput: "0,1,,3,4,5",
+ inspectable: true,
+ variablesViewLabel: "Array[6]",
+ },
+ // 5 - array with multiple successive empty slots at the middle
+ {
+ input: "[0,1,,,,5]",
+ output: "Array [ 0, 1, <3 empty slots>, 5 ]",
+ printOutput: "0,1,,,,5",
+ inspectable: true,
+ variablesViewLabel: "Array[6]",
+ },
+ // 6 - array with multiple non successive single empty slots
+ {
+ input: "[0,,2,,4,5]",
+ output: "Array [ 0, <1 empty slot>, 2, <1 empty slot>, 4, 5 ]",
+ printOutput: "0,,2,,4,5",
+ inspectable: true,
+ variablesViewLabel: "Array[6]",
+ },
+ // 7 - array with multiple multi-slot holes
+ {
+ input: "[0,,,3,,,,7,8]",
+ output: "Array [ 0, <2 empty slots>, 3, <3 empty slots>, 7, 8 ]",
+ printOutput: "0,,,3,,,,7,8",
+ inspectable: true,
+ variablesViewLabel: "Array[9]",
+ },
+ // 8 - array with a single slot hole at the end
+ {
+ input: "[0,1,2,3,4,,]",
+ output: "Array [ 0, 1, 2, 3, 4, <1 empty slot> ]",
+ printOutput: "0,1,2,3,4,",
+ inspectable: true,
+ variablesViewLabel: "Array[6]",
+ },
+ // 9 - array with multiple consecutive empty slots at the end
+ {
+ input: "[0,1,2,,,,]",
+ output: "Array [ 0, 1, 2, <3 empty slots> ]",
+ printOutput: "0,1,2,,,",
+ inspectable: true,
+ variablesViewLabel: "Array[6]",
+ },
+
+ // 10 - array with members explicitly set to null
+ {
+ input: "[0,null,null,3,4,5]",
+ output: "Array [ 0, null, null, 3, 4, 5 ]",
+ printOutput: "0,,,3,4,5",
+ inspectable: true,
+ variablesViewLabel: "Array[6]"
+ },
+
+ // 11 - array with members explicitly set to undefined
+ {
+ input: "[0,undefined,undefined,3,4,5]",
+ output: "Array [ 0, undefined, undefined, 3, 4, 5 ]",
+ printOutput: "0,,,3,4,5",
+ inspectable: true,
+ variablesViewLabel: "Array[6]"
+ },
+
+ // 12 - array with long strings as elements
+ {
+ input: '["' + testStrIn + '", "' + testStrIn + '", "' + testStrIn + '"]',
+ output: 'Array [ "' + testStrOut + '", "' + testStrOut + '", "' +
+ testStrOut + '" ]',
+ inspectable: true,
+ printOutput: "SHOW\nALL\nOF\nTHIS\nON\nA\nSINGLE\nLINE ONLY. ESCAPE " +
+ "ALL NEWLINE,SHOW\nALL\nOF\nTHIS\nON\nA\nSINGLE\nLINE ONLY. " +
+ "ESCAPE ALL NEWLINE,SHOW\nALL\nOF\nTHIS\nON\nA\nSINGLE\n" +
+ "LINE ONLY. ESCAPE ALL NEWLINE",
+ variablesViewLabel: "Array[3]"
+ },
+
+ // 13
+ {
+ input: '({0: "a", 1: "b"})',
+ output: 'Object [ "a", "b" ]',
+ printOutput: "[object Object]",
+ inspectable: true,
+ variablesViewLabel: "Object[2]",
+ },
+
+ // 14
+ {
+ input: '({0: "a", 42: "b"})',
+ output: '[ "a", <9 empty slots>, 33 more\u2026 ]',
+ printOutput: "[object Object]",
+ inspectable: true,
+ variablesViewLabel: "Object[43]",
+ },
+
+ // 15
+ {
+ input: '({0: "a", 1: "b", 2: "c", 3: "d", 4: "e", 5: "f", 6: "g", ' +
+ '7: "h", 8: "i", 9: "j", 10: "k", 11: "l"})',
+ output: 'Object [ "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", ' +
+ "2 more\u2026 ]",
+ printOutput: "[object Object]",
+ inspectable: true,
+ variablesViewLabel: "Object[12]",
+ },
+
+ // 16
+ {
+ input: '({0: "a", 1: "b", 2: "c", 3: "d", 4: "e", 5: "f", 6: "g", ' +
+ '7: "h", 8: "i", 9: "j", 10: "k", 11: "l", m: "n"})',
+ output: 'Object { 0: "a", 1: "b", 2: "c", 3: "d", 4: "e", 5: "f", ' +
+ '6: "g", 7: "h", 8: "i", 9: "j", 3 more\u2026 }',
+ printOutput: "[object Object]",
+ inspectable: true,
+ variablesViewLabel: "Object",
+ },
+
+ // 17
+ {
+ input: '({" ": "a"})',
+ output: 'Object { : "a" }',
+ printOutput: "[object Object]",
+ inspectable: true,
+ variablesViewLabel: "Object",
+ },
+
+ // 18
+ {
+ input: '({})',
+ output: 'Object { }',
+ printOutput: "[object Object]",
+ inspectable: true,
+ variablesViewLabel: "Object",
+ },
+
+ // 19
+ {
+ input: '({length: 0})',
+ output: 'Object [ ]',
+ printOutput: "[object Object]",
+ inspectable: true,
+ variablesViewLabel: "Object[0]",
+ },
+
+ // 20
+ {
+ input: '({length: 1})',
+ output: '[ <1 empty slot> ]',
+ printOutput: "[object Object]",
+ inspectable: true,
+ variablesViewLabel: "Object[1]",
+ },
+
+ // 21
+ {
+ input: '({0: "a", 1: "b", length: 1})',
+ output: 'Object { 0: "a", 1: "b", length: 1 }',
+ printOutput: "[object Object]",
+ inspectable: true,
+ variablesViewLabel: "Object",
+ },
+
+ // 22
+ {
+ input: '({0: "a", 1: "b", length: 2})',
+ output: 'Object [ "a", "b" ]',
+ printOutput: "[object Object]",
+ inspectable: true,
+ variablesViewLabel: "Object[2]",
+ },
+
+ // 23
+ {
+ input: '({0: "a", 1: "b", length: 3})',
+ output: '[ "a", "b", <1 empty slot> ]',
+ printOutput: "[object Object]",
+ inspectable: true,
+ variablesViewLabel: "Object[3]",
+ },
+
+ // 24
+ {
+ input: '({0: "a", 2: "b", length: 2})',
+ output: 'Object { 0: "a", 2: "b", length: 2 }',
+ printOutput: "[object Object]",
+ inspectable: true,
+ variablesViewLabel: "Object",
+ },
+
+ // 25
+ {
+ input: '({0: "a", 2: "b", length: 3})',
+ output: '[ "a", <1 empty slot>, "b" ]',
+ printOutput: "[object Object]",
+ inspectable: true,
+ variablesViewLabel: "Object[3]",
+ },
+
+ // 26
+ {
+ input: '({0: "a", b: "b", length: 1})',
+ output: 'Object { 0: "a", b: "b", length: 1 }',
+ printOutput: "[object Object]",
+ inspectable: true,
+ variablesViewLabel: "Object",
+ },
+
+ // 27
+ {
+ input: '({0: "a", b: "b", length: 2})',
+ output: 'Object { 0: "a", b: "b", length: 2 }',
+ printOutput: "[object Object]",
+ inspectable: true,
+ variablesViewLabel: "Object",
+ },
+
+ // 28
+ {
+ input: '({42: "a"})',
+ output: 'Object { 42: "a" }',
+ printOutput: "[object Object]",
+ inspectable: true,
+ variablesViewLabel: "Object",
+ },
+];
+
+function test() {
+ requestLongerTimeout(2);
+ Task.spawn(function* () {
+ let {tab} = yield loadTab(TEST_URI);
+ let hud = yield openConsole(tab);
+ return checkOutputForInputs(hud, inputTests);
+ }).then(finishUp);
+}
+
+function finishUp() {
+ inputTests = null;
+ finishTest();
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_output_copy_newlines.js b/devtools/client/webconsole/test/browser_webconsole_output_copy_newlines.js
new file mode 100644
index 000000000..22de843f9
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_output_copy_newlines.js
@@ -0,0 +1,72 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Test that multiple messages are copied into the clipboard and that they are
+// separated by new lines. See bug 916997.
+
+"use strict";
+
+add_task(function* () {
+ const TEST_URI = "data:text/html;charset=utf8,<p>hello world, bug 916997";
+ let clipboardValue = "";
+
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+ hud.jsterm.clearOutput();
+
+ let controller = top.document.commandDispatcher
+ .getControllerForCommand("cmd_copy");
+ is(controller.isCommandEnabled("cmd_copy"), false, "cmd_copy is disabled");
+
+ content.console.log("Hello world! bug916997a");
+ content.console.log("Hello world 2! bug916997b");
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "Hello world! bug916997a",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }, {
+ text: "Hello world 2! bug916997b",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+
+ hud.ui.output.selectAllMessages();
+ hud.outputNode.focus();
+
+ goUpdateCommand("cmd_copy");
+ controller = top.document.commandDispatcher
+ .getControllerForCommand("cmd_copy");
+ is(controller.isCommandEnabled("cmd_copy"), true, "cmd_copy is enabled");
+
+ let selection = hud.iframeWindow.getSelection() + "";
+ info("selection '" + selection + "'");
+
+ waitForClipboard((str) => {
+ clipboardValue = str;
+ return str.indexOf("bug916997a") > -1 && str.indexOf("bug916997b") > -1;
+ },
+ () => {
+ goDoCommand("cmd_copy");
+ },
+ () => {
+ info("clipboard value '" + clipboardValue + "'");
+ let lines = clipboardValue.trim().split("\n");
+ is(hud.outputNode.children.length, 2, "number of messages");
+ is(lines.length, hud.outputNode.children.length, "number of lines");
+ isnot(lines[0].indexOf("bug916997a"), -1,
+ "first message text includes 'bug916997a'");
+ isnot(lines[1].indexOf("bug916997b"), -1,
+ "second message text includes 'bug916997b'");
+ is(lines[0].indexOf("bug916997b"), -1,
+ "first message text does not include 'bug916997b'");
+ },
+ () => {
+ info("last clipboard value: '" + clipboardValue + "'");
+ });
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_output_dom_elements_01.js b/devtools/client/webconsole/test/browser_webconsole_output_dom_elements_01.js
new file mode 100644
index 000000000..097eb3b37
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_output_dom_elements_01.js
@@ -0,0 +1,122 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Whitelisting this test.
+// As part of bug 1077403, the leaking uncaught rejections should be fixed.
+
+"use strict";
+
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed(null);
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed(
+ "TypeError: this.toolbox is null");
+
+// Test the webconsole output for various types of DOM Nodes.
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console-output-dom-elements.html";
+
+var inputTests = [
+ {
+ input: "testBodyNode()",
+ output: '<body class="body-class" id="body-id">',
+ printOutput: "[object HTMLBodyElement]",
+ inspectable: true,
+ noClick: true,
+ inspectorIcon: true
+ },
+
+ {
+ input: "testDocumentElement()",
+ output: '<html dir="ltr" lang="en-US">',
+ printOutput: "[object HTMLHtmlElement]",
+ inspectable: true,
+ noClick: true,
+ inspectorIcon: true
+ },
+
+ {
+ input: "testDocument()",
+ output: "HTMLDocument \u2192 " + TEST_URI,
+ printOutput: "[object HTMLDocument]",
+ inspectable: true,
+ noClick: true,
+ inspectorIcon: false
+ },
+
+ {
+ input: "testNode()",
+ output: '<p some-attribute="some-value">',
+ printOutput: "[object HTMLParagraphElement]",
+ inspectable: true,
+ noClick: true,
+ inspectorIcon: true
+ },
+
+ {
+ input: "testNodeList()",
+ output: "NodeList [ <p>, <p#lots-of-attributes>, <iframe>, " +
+ "<div.some.classname.here.with.more.classnames.here>, " +
+ "<svg>, <clipPath>, <rect>, <script> ]",
+ printOutput: "[object NodeList]",
+ inspectable: true,
+ noClick: true,
+ inspectorIcon: true
+ },
+
+ {
+ input: "testNodeInIframe()",
+ output: "<p>",
+ printOutput: "[object HTMLParagraphElement]",
+ inspectable: true,
+ noClick: true,
+ inspectorIcon: true
+ },
+
+ {
+ input: "testLotsOfAttributes()",
+ output: '<p id="lots-of-attributes" a="" b="" c="" d="" e="" f="" g="" ' +
+ 'h="" i="" j="" k="" l="" m="" n="">',
+ printOutput: "[object HTMLParagraphElement]",
+ inspectable: true,
+ noClick: true,
+ inspectorIcon: true
+ },
+
+ {
+ input: "testDocumentFragment()",
+ output: "DocumentFragment [ <span.foo>, <div#fragdiv> ]",
+ printOutput: "[object DocumentFragment]",
+ inspectable: true,
+ noClick: true,
+ inspectorIcon: false
+ },
+
+ {
+ input: "testNodeInDocumentFragment()",
+ output: '<span class="foo" data-lolz="hehe">',
+ printOutput: "[object HTMLSpanElement]",
+ inspectable: true,
+ noClick: true,
+ inspectorIcon: false
+ },
+
+ {
+ input: "testUnattachedNode()",
+ output: '<p class="such-class" data-data="such-data">',
+ printOutput: "[object HTMLParagraphElement]",
+ inspectable: true,
+ noClick: true,
+ inspectorIcon: false
+ },
+];
+
+function test() {
+ requestLongerTimeout(2);
+ Task.spawn(function* () {
+ let {tab} = yield loadTab(TEST_URI);
+ let hud = yield openConsole(tab);
+ yield checkOutputForInputs(hud, inputTests);
+ }).then(finishTest);
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_output_dom_elements_02.js b/devtools/client/webconsole/test/browser_webconsole_output_dom_elements_02.js
new file mode 100644
index 000000000..51fe89e01
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_output_dom_elements_02.js
@@ -0,0 +1,66 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Test the inspector links in the webconsole output for DOM Nodes actually
+// open the inspector and select the right node.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console-output-dom-elements.html";
+
+const TEST_DATA = [
+ {
+ // The first test shouldn't be returning the body element as this is the
+ // default selected node, so re-selecting it won't fire the
+ // inspector-updated event
+ input: "testNode()",
+ output: '<p some-attribute="some-value">',
+ displayName: "p",
+ attrs: [{name: "some-attribute", value: "some-value"}]
+ },
+ {
+ input: "testBodyNode()",
+ output: '<body class="body-class" id="body-id">',
+ displayName: "body",
+ attrs: [
+ {
+ name: "class", value: "body-class"
+ },
+ {
+ name: "id", value: "body-id"
+ }
+ ]
+ },
+ {
+ input: "testNodeInIframe()",
+ output: "<p>",
+ displayName: "p",
+ attrs: []
+ },
+ {
+ input: "testDocumentElement()",
+ output: '<html dir="ltr" lang="en-US">',
+ displayName: "html",
+ attrs: [
+ {
+ name: "dir",
+ value: "ltr"
+ },
+ {
+ name: "lang",
+ value: "en-US"
+ }
+ ]
+ }
+];
+
+function test() {
+ Task.spawn(function* () {
+ let {tab} = yield loadTab(TEST_URI);
+ let hud = yield openConsole(tab);
+ yield checkDomElementHighlightingForInputs(hud, TEST_DATA);
+ }).then(finishTest);
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_output_dom_elements_03.js b/devtools/client/webconsole/test/browser_webconsole_output_dom_elements_03.js
new file mode 100644
index 000000000..b5dd125d1
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_output_dom_elements_03.js
@@ -0,0 +1,70 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Test that inspector links in webconsole outputs for DOM Nodes highlight
+// the actual DOM Nodes on hover
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console-output-dom-elements.html";
+
+function test() {
+ Task.spawn(function* () {
+ let {tab} = yield loadTab(TEST_URI);
+ let hud = yield openConsole(tab);
+ let toolbox = gDevTools.getToolbox(hud.target);
+
+ // Loading the inspector panel at first, to make it possible to listen for
+ // new node selections
+ yield toolbox.loadTool("inspector");
+ toolbox.getPanel("inspector");
+
+ info("Executing 'testNode()' in the web console to output a DOM Node");
+ let [result] = yield jsEval("testNode()", hud, {
+ text: '<p some-attribute="some-value">'
+ });
+
+ let elementNodeWidget = yield getWidget(result);
+
+ let nodeFront = yield hoverOverWidget(elementNodeWidget, toolbox);
+ let attrs = nodeFront.attributes;
+ is(nodeFront.tagName, "P", "The correct node was highlighted");
+ is(attrs[0].name, "some-attribute", "The correct node was highlighted");
+ is(attrs[0].value, "some-value", "The correct node was highlighted");
+ }).then(finishTest);
+}
+
+function jsEval(input, hud, message) {
+ hud.jsterm.execute(input);
+ return waitForMessages({
+ webconsole: hud,
+ messages: [message]
+ });
+}
+
+function* getWidget(result) {
+ info("Getting the output ElementNode widget");
+
+ let msg = [...result.matched][0];
+ let elementNodeWidget = [...msg._messageObject.widgets][0];
+ ok(elementNodeWidget, "ElementNode widget found in the output");
+
+ info("Waiting for the ElementNode widget to be linked to the inspector");
+ yield elementNodeWidget.linkToInspector();
+
+ return elementNodeWidget;
+}
+
+function* hoverOverWidget(widget, toolbox) {
+ info("Hovering over the output to highlight the node");
+
+ let onHighlight = toolbox.once("node-highlight");
+ EventUtils.sendMouseEvent({type: "mouseover"}, widget.element,
+ widget.element.ownerDocument.defaultView);
+ let nodeFront = yield onHighlight;
+ ok(true, "The highlighter was shown on a node");
+ return nodeFront;
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_output_dom_elements_04.js b/devtools/client/webconsole/test/browser_webconsole_output_dom_elements_04.js
new file mode 100644
index 000000000..c7eb94902
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_output_dom_elements_04.js
@@ -0,0 +1,113 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Test that inspector links in the webconsole output for DOM Nodes do not try
+// to highlight or select nodes once they have been detached
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console-output-dom-elements.html";
+
+const TEST_DATA = [
+ {
+ // The first test shouldn't be returning the body element as this is the
+ // default selected node, so re-selecting it won't fire the
+ // inspector-updated event
+ input: "testNode()",
+ output: '<p some-attribute="some-value">'
+ },
+ {
+ input: "testSvgNode()",
+ output: '<clipPath>'
+ },
+ {
+ input: "testBodyNode()",
+ output: '<body class="body-class" id="body-id">'
+ },
+ {
+ input: "testNodeInIframe()",
+ output: "<p>"
+ },
+ {
+ input: "testDocumentElement()",
+ output: '<html dir="ltr" lang="en-US">'
+ }
+];
+
+const PREF = "devtools.webconsole.persistlog";
+
+function test() {
+ Services.prefs.setBoolPref(PREF, true);
+ registerCleanupFunction(() => Services.prefs.clearUserPref(PREF));
+
+ Task.spawn(function* () {
+ let {tab} = yield loadTab(TEST_URI);
+ let hud = yield openConsole(tab);
+ let toolbox = gDevTools.getToolbox(hud.target);
+
+ info("Executing the test data");
+ let widgets = [];
+ for (let data of TEST_DATA) {
+ let [result] = yield jsEval(data.input, hud, {text: data.output});
+ let {widget} = yield getWidgetAndMessage(result);
+ widgets.push(widget);
+ }
+
+ info("Reloading the page");
+ yield reloadPage();
+
+ info("Iterating over the ElementNode widgets");
+ for (let widget of widgets) {
+ // Verify that openNodeInInspector rejects since the associated dom node
+ // doesn't exist anymore
+ yield widget.openNodeInInspector().then(() => {
+ ok(false, "The openNodeInInspector promise resolved");
+ }, () => {
+ ok(true, "The openNodeInInspector promise rejected as expected");
+ });
+ yield toolbox.selectTool("webconsole");
+
+ // Verify that highlightDomNode rejects too, for the same reason
+ yield widget.highlightDomNode().then(() => {
+ ok(false, "The highlightDomNode promise resolved");
+ }, () => {
+ ok(true, "The highlightDomNode promise rejected as expected");
+ });
+ }
+ }).then(finishTest);
+}
+
+function jsEval(input, hud, message) {
+ info("Executing '" + input + "' in the web console");
+ hud.jsterm.execute(input);
+ return waitForMessages({
+ webconsole: hud,
+ messages: [message]
+ });
+}
+
+function* getWidgetAndMessage(result) {
+ info("Getting the output ElementNode widget");
+
+ let msg = [...result.matched][0];
+ let widget = [...msg._messageObject.widgets][0];
+ ok(widget, "ElementNode widget found in the output");
+
+ info("Waiting for the ElementNode widget to be linked to the inspector");
+ yield widget.linkToInspector();
+
+ return {widget: widget, msg: msg};
+}
+
+function reloadPage() {
+ let def = promise.defer();
+ gBrowser.selectedBrowser.addEventListener("load", function onload() {
+ gBrowser.selectedBrowser.removeEventListener("load", onload, true);
+ def.resolve();
+ }, true);
+ content.location.reload();
+ return def.promise;
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_output_dom_elements_05.js b/devtools/client/webconsole/test/browser_webconsole_output_dom_elements_05.js
new file mode 100644
index 000000000..9d35ef984
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_output_dom_elements_05.js
@@ -0,0 +1,47 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 inspector links in the webconsole output for namespaced elements
+// actually open the inspector and select the right node.
+
+const XHTML = `
+ <!DOCTYPE html>
+ <html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:svg="http://www.w3.org/2000/svg">
+ <body>
+ <svg:svg width="100" height="100">
+ <svg:clipPath id="clip">
+ <svg:rect id="rectangle" x="0" y="0" width="10" height="5"></svg:rect>
+ </svg:clipPath>
+ <svg:circle cx="0" cy="0" r="5"></svg:circle>
+ </svg:svg>
+ </body>
+ </html>
+`;
+
+const TEST_URI = "data:application/xhtml+xml;charset=utf-8," + encodeURI(XHTML);
+
+const TEST_DATA = [
+ {
+ input: 'document.querySelector("clipPath")',
+ output: '<svg:clipPath id="clip">',
+ displayName: "svg:clipPath"
+ },
+ {
+ input: 'document.querySelector("circle")',
+ output: '<svg:circle cx="0" cy="0" r="5">',
+ displayName: "svg:circle"
+ },
+];
+
+function test() {
+ Task.spawn(function* () {
+ let {tab} = yield loadTab(TEST_URI);
+ let hud = yield openConsole(tab);
+ yield checkDomElementHighlightingForInputs(hud, TEST_DATA);
+ }).then(finishTest);
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_output_events.js b/devtools/client/webconsole/test/browser_webconsole_output_events.js
new file mode 100644
index 000000000..9bd04bfc7
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_output_events.js
@@ -0,0 +1,54 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Whitelisting this test.
+// As part of bug 1077403, the leaking uncaught rejection should be fixed.
+
+"use strict";
+
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed("null");
+
+// Test the webconsole output for DOM events.
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console-output-events.html";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ hud.jsterm.clearOutput();
+ hud.jsterm.execute("testDOMEvents()");
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "testDOMEvents() output",
+ text: "undefined",
+ category: CATEGORY_OUTPUT,
+ }],
+ });
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "console.log() output for mousemove",
+ text: /eventLogger mousemove { target: .+, buttons: 0, clientX: \d+, clientY: \d+, layerX: \d+, layerY: \d+ }/,
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "console.log() output for keypress",
+ text: /eventLogger keypress Shift { target: .+, key: .+, charCode: \d+, keyCode: \d+ }/,
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_output_order.js b/devtools/client/webconsole/test/browser_webconsole_output_order.js
new file mode 100644
index 000000000..66fa74cb0
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_output_order.js
@@ -0,0 +1,47 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests that any output created from calls to the console API comes before the
+// echoed JavaScript.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console.html";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+
+ let jsterm = hud.jsterm;
+
+ jsterm.clearOutput();
+ jsterm.execute("console.log('foo', 'bar');");
+
+ let [functionCall, consoleMessage, result] = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "console.log('foo', 'bar');",
+ category: CATEGORY_INPUT,
+ },
+ {
+ text: "foo bar",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ },
+ {
+ text: "undefined",
+ category: CATEGORY_OUTPUT,
+ }]
+ });
+
+ let fncallNode = [...functionCall.matched][0];
+ let consoleMessageNode = [...consoleMessage.matched][0];
+ let resultNode = [...result.matched][0];
+ is(fncallNode.nextElementSibling, consoleMessageNode,
+ "console.log() is followed by 'foo' 'bar'");
+ is(consoleMessageNode.nextElementSibling, resultNode,
+ "'foo' 'bar' is followed by undefined");
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_output_regexp.js b/devtools/client/webconsole/test/browser_webconsole_output_regexp.js
new file mode 100644
index 000000000..2d6e767e9
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_output_regexp.js
@@ -0,0 +1,35 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Test the webconsole output for various types of objects.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console-output-regexp.html";
+
+var inputTests = [
+ // 0
+ {
+ input: "/foo/igym",
+ output: "/foo/gimy",
+ printOutput: "Error: source called",
+ inspectable: true,
+ },
+];
+
+function test() {
+ requestLongerTimeout(2);
+ Task.spawn(function* () {
+ let {tab} = yield loadTab(TEST_URI);
+ let hud = yield openConsole(tab);
+ return checkOutputForInputs(hud, inputTests);
+ }).then(finishUp);
+}
+
+function finishUp() {
+ inputTests = null;
+ finishTest();
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_output_table.js b/devtools/client/webconsole/test/browser_webconsole_output_table.js
new file mode 100644
index 000000000..372afb28d
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_output_table.js
@@ -0,0 +1,199 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests that console.table() works as intended.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console-table.html";
+
+const TEST_DATA = [
+ {
+ command: "console.table(languages1)",
+ data: [
+ { _index: 0, name: "\"JavaScript\"", fileExtension: "Array[1]" },
+ { _index: 1, name: "Object", fileExtension: "\".ts\"" },
+ { _index: 2, name: "\"CoffeeScript\"", fileExtension: "\".coffee\"" }
+ ],
+ columns: { _index: "(index)", name: "name", fileExtension: "fileExtension" }
+ },
+ {
+ command: "console.table(languages1, 'name')",
+ data: [
+ { _index: 0, name: "\"JavaScript\"", fileExtension: "Array[1]" },
+ { _index: 1, name: "Object", fileExtension: "\".ts\"" },
+ { _index: 2, name: "\"CoffeeScript\"", fileExtension: "\".coffee\"" }
+ ],
+ columns: { _index: "(index)", name: "name" }
+ },
+ {
+ command: "console.table(languages1, ['name'])",
+ data: [
+ { _index: 0, name: "\"JavaScript\"", fileExtension: "Array[1]" },
+ { _index: 1, name: "Object", fileExtension: "\".ts\"" },
+ { _index: 2, name: "\"CoffeeScript\"", fileExtension: "\".coffee\"" }
+ ],
+ columns: { _index: "(index)", name: "name" }
+ },
+ {
+ command: "console.table(languages2)",
+ data: [
+ { _index: "csharp", name: "\"C#\"", paradigm: "\"object-oriented\"" },
+ { _index: "fsharp", name: "\"F#\"", paradigm: "\"functional\"" }
+ ],
+ columns: { _index: "(index)", name: "name", paradigm: "paradigm" }
+ },
+ {
+ command: "console.table([[1, 2], [3, 4]])",
+ data: [
+ { _index: 0, 0: "1", 1: "2" },
+ { _index: 1, 0: "3", 1: "4" }
+ ],
+ columns: { _index: "(index)", 0: "0", 1: "1" }
+ },
+ {
+ command: "console.table({a: [1, 2], b: [3, 4]})",
+ data: [
+ { _index: "a", 0: "1", 1: "2" },
+ { _index: "b", 0: "3", 1: "4" }
+ ],
+ columns: { _index: "(index)", 0: "0", 1: "1" }
+ },
+ {
+ command: "console.table(family)",
+ data: [
+ { _index: "mother", firstName: "\"Susan\"", lastName: "\"Doyle\"",
+ age: "32" },
+ { _index: "father", firstName: "\"John\"", lastName: "\"Doyle\"",
+ age: "33" },
+ { _index: "daughter", firstName: "\"Lily\"", lastName: "\"Doyle\"",
+ age: "5" },
+ { _index: "son", firstName: "\"Mike\"", lastName: "\"Doyle\"", age: "8" },
+ ],
+ columns: { _index: "(index)", firstName: "firstName", lastName: "lastName",
+ age: "age" }
+ },
+ {
+ command: "console.table(family, [])",
+ data: [
+ { _index: "mother", firstName: "\"Susan\"", lastName: "\"Doyle\"",
+ age: "32" },
+ { _index: "father", firstName: "\"John\"", lastName: "\"Doyle\"",
+ age: "33" },
+ { _index: "daughter", firstName: "\"Lily\"", lastName: "\"Doyle\"",
+ age: "5" },
+ { _index: "son", firstName: "\"Mike\"", lastName: "\"Doyle\"", age: "8" },
+ ],
+ columns: { _index: "(index)" }
+ },
+ {
+ command: "console.table(family, ['firstName', 'lastName'])",
+ data: [
+ { _index: "mother", firstName: "\"Susan\"", lastName: "\"Doyle\"",
+ age: "32" },
+ { _index: "father", firstName: "\"John\"", lastName: "\"Doyle\"",
+ age: "33" },
+ { _index: "daughter", firstName: "\"Lily\"", lastName: "\"Doyle\"",
+ age: "5" },
+ { _index: "son", firstName: "\"Mike\"", lastName: "\"Doyle\"", age: "8" },
+ ],
+ columns: { _index: "(index)", firstName: "firstName", lastName: "lastName" }
+ },
+ {
+ command: "console.table(mySet)",
+ data: [
+ { _index: 0, _value: "1" },
+ { _index: 1, _value: "5" },
+ { _index: 2, _value: "\"some text\"" },
+ { _index: 3, _value: "null" },
+ { _index: 4, _value: "undefined" }
+ ],
+ columns: { _index: "(iteration index)", _value: "Values" }
+ },
+ {
+ command: "console.table(myMap)",
+ data: [
+ { _index: 0, _key: "\"a string\"",
+ _value: "\"value associated with 'a string'\"" },
+ { _index: 1, _key: "5", _value: "\"value associated with 5\"" },
+ ],
+ columns: { _index: "(iteration index)", _key: "Key", _value: "Values" }
+ },
+ {
+ command: "console.table(weakset)",
+ data: [
+ { _value: "String" },
+ { _value: "String" },
+ ],
+ columns: { _index: "(iteration index)", _value: "Values" },
+ couldBeOutOfOrder: true,
+ },
+ {
+ command: "console.table(weakmap)",
+ data: [
+ { _key: "String", _value: "\"oh no\"" },
+ { _key: "String", _value: "23" },
+ ],
+ columns: { _index: "(iteration index)", _key: "Key", _value: "Values" },
+ couldBeOutOfOrder: true,
+ },
+];
+
+add_task(function* () {
+ const {tab} = yield loadTab(TEST_URI);
+ let hud = yield openConsole(tab);
+
+ for (let testdata of TEST_DATA) {
+ hud.jsterm.clearOutput();
+
+ info("Executing " + testdata.command);
+
+ let onTableRender = once(hud.ui, "messages-table-rendered");
+ hud.jsterm.execute(testdata.command);
+ yield onTableRender;
+
+ let [result] = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: testdata.command + " output",
+ consoleTable: true
+ }],
+ });
+
+ let node = [...result.matched][0];
+ ok(node, "found trace log node");
+
+ let obj = node._messageObject;
+ ok(obj, "console.trace message object");
+
+ ok(obj._data, "found table data object");
+
+ let data = obj._data.map(entries => {
+ let entryResult = {};
+
+ for (let key of Object.keys(entries)) {
+ // If the results can be out of order, then ignore _index.
+ if (!testdata.couldBeOutOfOrder || key !== "_index") {
+ entryResult[key] = entries[key] instanceof HTMLElement ?
+ entries[key].textContent : entries[key];
+ }
+ }
+
+ return entryResult;
+ });
+
+ if (testdata.couldBeOutOfOrder) {
+ data = data.map(e => e.toSource()).sort().join(",");
+ let expected = testdata.data.map(e => e.toSource()).sort().join(",");
+ is(data, expected, "table data is correct");
+ } else {
+ is(data.toSource(), testdata.data.toSource(), "table data is correct");
+ }
+ ok(obj._columns, "found table column object");
+ is(obj._columns.toSource(), testdata.columns.toSource(),
+ "table column is correct");
+ }
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_promise.js b/devtools/client/webconsole/test/browser_webconsole_promise.js
new file mode 100644
index 000000000..59cd287ca
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_promise.js
@@ -0,0 +1,35 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Bug 1148759 - Test the webconsole can display promises inside objects.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf8,test for console and promises";
+
+var inputTests = [
+ // 0
+ {
+ input: "({ x: Promise.resolve() })",
+ output: "Object { x: Promise }",
+ printOutput: "[object Object]",
+ inspectable: true,
+ variablesViewLabel: "Object"
+ },
+];
+
+function test() {
+ requestLongerTimeout(2);
+
+ Task.spawn(function* () {
+ let {tab} = yield loadTab(TEST_URI);
+ let hud = yield openConsole(tab);
+ return checkOutputForInputs(hud, inputTests);
+ }).then(finishUp);
+}
+
+function finishUp() {
+ finishTest();
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_property_provider.js b/devtools/client/webconsole/test/browser_webconsole_property_provider.js
new file mode 100644
index 000000000..0c9b4c4e3
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_property_provider.js
@@ -0,0 +1,46 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests the property provider, which is part of the code completion
+// infrastructure.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf8,<p>test the JS property provider";
+
+function test() {
+ loadTab(TEST_URI).then(testPropertyProvider);
+}
+
+function testPropertyProvider({browser}) {
+ browser.removeEventListener("load", testPropertyProvider, true);
+ let {JSPropertyProvider} = require("devtools/shared/webconsole/js-property-provider");
+
+ let tmp = Cu.import("resource://gre/modules/jsdebugger.jsm", {});
+ tmp.addDebuggerToGlobal(tmp);
+ let dbg = new tmp.Debugger();
+ let dbgWindow = dbg.addDebuggee(content);
+
+ let completion = JSPropertyProvider(dbgWindow, null, "thisIsNotDefined");
+ is(completion.matches.length, 0, "no match for 'thisIsNotDefined");
+
+ // This is a case the PropertyProvider can't handle. Should return null.
+ completion = JSPropertyProvider(dbgWindow, null, "window[1].acb");
+ is(completion, null, "no match for 'window[1].acb");
+
+ // A very advanced completion case.
+ let strComplete =
+ "function a() { }document;document.getElementById(window.locatio";
+ completion = JSPropertyProvider(dbgWindow, null, strComplete);
+ ok(completion.matches.length == 2, "two matches found");
+ ok(completion.matchProp == "locatio", "matching part is 'test'");
+ let matches = completion.matches;
+ matches.sort();
+ ok(matches[0] == "location", "the first match is 'location'");
+ ok(matches[1] == "locationbar", "the second match is 'locationbar'");
+
+ dbg.removeDebuggee(content);
+ finishTest();
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_reflow.js b/devtools/client/webconsole/test/browser_webconsole_reflow.js
new file mode 100644
index 000000000..86caa10e0
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_reflow.js
@@ -0,0 +1,33 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 TEST_URI = "data:text/html;charset=utf-8,Web Console test for " +
+ "reflow activity";
+
+add_task(function* () {
+ let { browser } = yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ function onReflowListenersReady() {
+ browser.contentDocument.body.style.display = "none";
+ browser.contentDocument.body.clientTop;
+ }
+
+ Services.prefs.setBoolPref("devtools.webconsole.filter.csslog", true);
+ hud.ui._updateReflowActivityListener(onReflowListenersReady);
+ Services.prefs.clearUserPref("devtools.webconsole.filter.csslog");
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: /reflow: /,
+ category: CATEGORY_CSS,
+ severity: SEVERITY_LOG,
+ }],
+ });
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_scratchpad_panel_link.js b/devtools/client/webconsole/test/browser_webconsole_scratchpad_panel_link.js
new file mode 100644
index 000000000..566af8d42
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_scratchpad_panel_link.js
@@ -0,0 +1,76 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 TEST_URI = "data:text/html;charset=utf8,<p>test Scratchpad panel " +
+ "linking</p>";
+
+var { Tools } = require("devtools/client/definitions");
+var { isTargetSupported } = Tools.scratchpad;
+
+function pushPrefEnv() {
+ let deferred = promise.defer();
+ let options = {"set":
+ [["devtools.scratchpad.enabled", true]
+ ]};
+ SpecialPowers.pushPrefEnv(options, deferred.resolve);
+ return deferred.promise;
+}
+
+add_task(function* () {
+ waitForExplicitFinish();
+
+ yield pushPrefEnv();
+
+ yield loadTab(TEST_URI);
+
+ info("Opening toolbox with Scratchpad panel");
+
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ let toolbox = yield gDevTools.showToolbox(target, "scratchpad", "window");
+
+ let scratchpadPanel = toolbox.getPanel("scratchpad");
+ let { scratchpad } = scratchpadPanel;
+ is(toolbox.getCurrentPanel(), scratchpadPanel,
+ "Scratchpad is currently selected panel");
+
+ info("Switching to webconsole panel");
+
+ let webconsolePanel = yield toolbox.selectTool("webconsole");
+ let { hud } = webconsolePanel;
+ is(toolbox.getCurrentPanel(), webconsolePanel,
+ "Webconsole is currently selected panel");
+
+ info("console.log()ing from Scratchpad");
+
+ scratchpad.setText("console.log('foobar-from-scratchpad')");
+ scratchpad.run();
+ let messages = yield waitForMessages({
+ webconsole: hud,
+ messages: [{ text: "foobar-from-scratchpad" }]
+ });
+
+ info("Clicking link to switch to and focus Scratchpad");
+
+ let [matched] = [...messages[0].matched];
+ ok(matched, "Found logged message from Scratchpad");
+ let anchor = matched.querySelector(".message-location .frame-link-filename");
+
+ toolbox.on("scratchpad-selected", function selected() {
+ toolbox.off("scratchpad-selected", selected);
+
+ is(toolbox.getCurrentPanel(), scratchpadPanel,
+ "Clicking link switches to Scratchpad panel");
+
+ is(Services.ww.activeWindow, toolbox.win.parent,
+ "Scratchpad's toolbox is focused");
+
+ Tools.scratchpad.isTargetSupported = isTargetSupported;
+ finish();
+ });
+
+ EventUtils.synthesizeMouse(anchor, 2, 2, {}, hud.iframeWindow);
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_script_errordoc_urls.js b/devtools/client/webconsole/test/browser_webconsole_script_errordoc_urls.js
new file mode 100644
index 000000000..779d80376
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_script_errordoc_urls.js
@@ -0,0 +1,67 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Ensure that [Learn More] links appear alongside any errors listed
+// in "errordocs.js". Note: this only tests script execution.
+
+"use strict";
+
+const ErrorDocs = require("devtools/server/actors/errordocs");
+
+function makeURIData(script) {
+ return `data:text/html;charset=utf8,<script>${script}</script>`;
+}
+
+const TestData = [
+ {
+ jsmsg: "JSMSG_READ_ONLY",
+ script: "'use strict'; (Object.freeze({name: 'Elsa', score: 157})).score = 0;",
+ isException: true,
+ },
+ {
+ jsmsg: "JSMSG_STMT_AFTER_RETURN",
+ script: "function a() { return; 1 + 1; };",
+ isException: false,
+ }
+];
+
+add_task(function* () {
+ yield loadTab("data:text/html;charset=utf8,errordoc tests");
+
+ let hud = yield openConsole();
+
+ for (let i = 0; i < TestData.length; i++) {
+ yield testScriptError(hud, TestData[i]);
+ }
+});
+
+function* testScriptError(hud, testData) {
+ if (testData.isException === true) {
+ expectUncaughtException();
+ }
+
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, makeURIData(testData.script));
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ category: CATEGORY_JS
+ }
+ ]
+ });
+
+ // grab the most current error doc URL
+ let url = ErrorDocs.GetURL({ errorMessageName: testData.jsmsg });
+
+ let hrefs = {};
+ for (let link of hud.jsterm.outputNode.querySelectorAll("a")) {
+ hrefs[link.href] = true;
+ }
+
+ ok(url in hrefs, `Expected a link to ${url}.`);
+
+ hud.jsterm.clearOutput();
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_show_subresource_security_errors.js b/devtools/client/webconsole/test/browser_webconsole_show_subresource_security_errors.js
new file mode 100644
index 000000000..43cb96bdc
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_show_subresource_security_errors.js
@@ -0,0 +1,39 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Ensure non-toplevel security errors are displayed
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8,Web Console subresource STS " +
+ "warning test";
+const TEST_DOC = "https://example.com/browser/devtools/client/webconsole/" +
+ "test/test_bug1092055_shouldwarn.html";
+const SAMPLE_MSG = "specified a header that could not be parsed successfully.";
+
+add_task(function* setup() {
+ yield SpecialPowers.pushPrefEnv({
+ set: [["dom.ipc.processCount", 1]]
+ });
+});
+
+add_task(function* () {
+ let { browser } = yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ hud.jsterm.clearOutput();
+
+ let loaded = loadBrowser(browser);
+ BrowserTestUtils.loadURI(browser, TEST_DOC);
+ yield loaded;
+
+ yield waitForSuccess({
+ name: "Subresource STS warning displayed successfully",
+ validator: function () {
+ return hud.outputNode.textContent.indexOf(SAMPLE_MSG) > -1;
+ }
+ });
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_shows_reqs_in_netmonitor.js b/devtools/client/webconsole/test/browser_webconsole_shows_reqs_in_netmonitor.js
new file mode 100644
index 000000000..b66d5afff
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_shows_reqs_in_netmonitor.js
@@ -0,0 +1,73 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 TEST_URI = "data:text/html;charset=utf8,Test that the web console " +
+ "displays requests that have been recorded in the " +
+ "netmonitor, even if the console hadn't opened yet.";
+
+const TEST_FILE = "test-network-request.html";
+const TEST_PATH = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/" + TEST_FILE;
+
+const NET_PREF = "devtools.webconsole.filter.networkinfo";
+Services.prefs.setBoolPref(NET_PREF, true);
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref(NET_PREF);
+});
+
+add_task(function* () {
+ let { tab, browser } = yield loadTab(TEST_URI);
+
+ let target = TargetFactory.forTab(tab);
+ let toolbox = yield gDevTools.showToolbox(target, "netmonitor");
+ info("Network panel is open.");
+
+ yield loadDocument(browser);
+ info("Document loaded.");
+
+ // Test that the request appears in the network panel.
+ testNetmonitor(toolbox);
+
+ // Test that the request appears in the console.
+ let hud = yield openConsole();
+ info("Web console is open");
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ name: "network message",
+ text: TEST_FILE,
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_LOG
+ }
+ ]
+ });
+});
+
+function loadDocument(browser) {
+ let deferred = promise.defer();
+
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ deferred.resolve();
+ }, true);
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_PATH);
+
+ return deferred.promise;
+}
+
+function testNetmonitor(toolbox) {
+ let monitor = toolbox.getCurrentPanel();
+ let { RequestsMenu } = monitor.panelWin.NetMonitorView;
+ RequestsMenu.lazyUpdate = false;
+ is(RequestsMenu.itemCount, 1, "Network request appears in the network panel");
+
+ let item = RequestsMenu.getItemAtIndex(0);
+ is(item.attachment.method, "GET", "The attached method is correct.");
+ is(item.attachment.url, TEST_PATH, "The attached url is correct.");
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_split.js b/devtools/client/webconsole/test/browser_webconsole_split.js
new file mode 100644
index 000000000..0242d94b4
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_split.js
@@ -0,0 +1,268 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 TEST_URI = "data:text/html;charset=utf-8,Web Console test for splitting";
+
+function test() {
+ waitForExplicitFinish();
+ SpecialPowers.pushPrefEnv({"set": [["dom.ipc.processCount", 1]]}, runTest);
+}
+
+function runTest() {
+ // Test is slow on Linux EC2 instances - Bug 962931
+ requestLongerTimeout(2);
+
+ let {Toolbox} = require("devtools/client/framework/toolbox");
+ let toolbox;
+
+ loadTab(TEST_URI).then(testConsoleLoadOnDifferentPanel);
+
+ function testConsoleLoadOnDifferentPanel() {
+ info("About to check console loads even when non-webconsole panel is open");
+
+ openPanel("inspector").then(() => {
+ toolbox.on("webconsole-ready", () => {
+ ok(true, "Webconsole has been triggered as loaded while another tool " +
+ "is active");
+ testKeyboardShortcuts();
+ });
+
+ // Opens split console.
+ toolbox.toggleSplitConsole();
+ });
+ }
+
+ function testKeyboardShortcuts() {
+ info("About to check that panel responds to ESCAPE keyboard shortcut");
+
+ toolbox.once("split-console", () => {
+ ok(true, "Split console has been triggered via ESCAPE keypress");
+ checkAllTools();
+ });
+
+ // Closes split console.
+ EventUtils.sendKey("ESCAPE", toolbox.win);
+ }
+
+ function checkAllTools() {
+ info("About to check split console with each panel individually.");
+
+ Task.spawn(function* () {
+ yield openAndCheckPanel("jsdebugger");
+ yield openAndCheckPanel("inspector");
+ yield openAndCheckPanel("styleeditor");
+ yield openAndCheckPanel("performance");
+ yield openAndCheckPanel("netmonitor");
+
+ yield checkWebconsolePanelOpened();
+ testBottomHost();
+ });
+ }
+
+ function getCurrentUIState() {
+ let win = toolbox.win;
+ let deck = toolbox.doc.querySelector("#toolbox-deck");
+ let webconsolePanel = toolbox.webconsolePanel;
+ let splitter = toolbox.doc.querySelector("#toolbox-console-splitter");
+
+ let containerHeight = parseFloat(win.getComputedStyle(deck.parentNode)
+ .getPropertyValue("height"));
+ let deckHeight = parseFloat(win.getComputedStyle(deck)
+ .getPropertyValue("height"));
+ let webconsoleHeight = parseFloat(win.getComputedStyle(webconsolePanel)
+ .getPropertyValue("height"));
+ let splitterVisibility = !splitter.getAttribute("hidden");
+ let openedConsolePanel = toolbox.currentToolId === "webconsole";
+ let cmdButton = toolbox.doc.querySelector("#command-button-splitconsole");
+
+ return {
+ deckHeight: deckHeight,
+ containerHeight: containerHeight,
+ webconsoleHeight: webconsoleHeight,
+ splitterVisibility: splitterVisibility,
+ openedConsolePanel: openedConsolePanel,
+ buttonSelected: cmdButton.hasAttribute("checked")
+ };
+ }
+
+ function checkWebconsolePanelOpened() {
+ info("About to check special cases when webconsole panel is open.");
+
+ let deferred = promise.defer();
+
+ // Start with console split, so we can test for transition to main panel.
+ toolbox.toggleSplitConsole();
+
+ let currentUIState = getCurrentUIState();
+
+ ok(currentUIState.splitterVisibility,
+ "Splitter is visible when console is split");
+ ok(currentUIState.deckHeight > 0,
+ "Deck has a height > 0 when console is split");
+ ok(currentUIState.webconsoleHeight > 0,
+ "Web console has a height > 0 when console is split");
+ ok(!currentUIState.openedConsolePanel,
+ "The console panel is not the current tool");
+ ok(currentUIState.buttonSelected, "The command button is selected");
+
+ openPanel("webconsole").then(() => {
+ currentUIState = getCurrentUIState();
+
+ ok(!currentUIState.splitterVisibility,
+ "Splitter is hidden when console is opened.");
+ is(currentUIState.deckHeight, 0,
+ "Deck has a height == 0 when console is opened.");
+ is(currentUIState.webconsoleHeight, currentUIState.containerHeight,
+ "Web console is full height.");
+ ok(currentUIState.openedConsolePanel,
+ "The console panel is the current tool");
+ ok(currentUIState.buttonSelected,
+ "The command button is still selected.");
+
+ // Make sure splitting console does nothing while webconsole is opened
+ toolbox.toggleSplitConsole();
+
+ currentUIState = getCurrentUIState();
+
+ ok(!currentUIState.splitterVisibility,
+ "Splitter is hidden when console is opened.");
+ is(currentUIState.deckHeight, 0,
+ "Deck has a height == 0 when console is opened.");
+ is(currentUIState.webconsoleHeight, currentUIState.containerHeight,
+ "Web console is full height.");
+ ok(currentUIState.openedConsolePanel,
+ "The console panel is the current tool");
+ ok(currentUIState.buttonSelected,
+ "The command button is still selected.");
+
+ // Make sure that split state is saved after opening another panel
+ openPanel("inspector").then(() => {
+ currentUIState = getCurrentUIState();
+ ok(currentUIState.splitterVisibility,
+ "Splitter is visible when console is split");
+ ok(currentUIState.deckHeight > 0,
+ "Deck has a height > 0 when console is split");
+ ok(currentUIState.webconsoleHeight > 0,
+ "Web console has a height > 0 when console is split");
+ ok(!currentUIState.openedConsolePanel,
+ "The console panel is not the current tool");
+ ok(currentUIState.buttonSelected,
+ "The command button is still selected.");
+
+ toolbox.toggleSplitConsole();
+ deferred.resolve();
+ });
+ });
+ return deferred.promise;
+ }
+
+ function openPanel(toolId) {
+ let deferred = promise.defer();
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ gDevTools.showToolbox(target, toolId).then(function (box) {
+ toolbox = box;
+ deferred.resolve();
+ }).then(null, console.error);
+ return deferred.promise;
+ }
+
+ function openAndCheckPanel(toolId) {
+ let deferred = promise.defer();
+ openPanel(toolId).then(() => {
+ info("Checking toolbox for " + toolId);
+ checkToolboxUI(toolbox.getCurrentPanel());
+ deferred.resolve();
+ });
+ return deferred.promise;
+ }
+
+ function checkToolboxUI() {
+ let currentUIState = getCurrentUIState();
+
+ ok(!currentUIState.splitterVisibility, "Splitter is hidden by default");
+ is(currentUIState.deckHeight, currentUIState.containerHeight,
+ "Deck has a height > 0 by default");
+ is(currentUIState.webconsoleHeight, 0,
+ "Web console is collapsed by default");
+ ok(!currentUIState.openedConsolePanel,
+ "The console panel is not the current tool");
+ ok(!currentUIState.buttonSelected, "The command button is not selected.");
+
+ toolbox.toggleSplitConsole();
+
+ currentUIState = getCurrentUIState();
+
+ ok(currentUIState.splitterVisibility,
+ "Splitter is visible when console is split");
+ ok(currentUIState.deckHeight > 0,
+ "Deck has a height > 0 when console is split");
+ ok(currentUIState.webconsoleHeight > 0,
+ "Web console has a height > 0 when console is split");
+ is(Math.round(currentUIState.deckHeight + currentUIState.webconsoleHeight),
+ currentUIState.containerHeight,
+ "Everything adds up to container height");
+ ok(!currentUIState.openedConsolePanel,
+ "The console panel is not the current tool");
+ ok(currentUIState.buttonSelected, "The command button is selected.");
+
+ toolbox.toggleSplitConsole();
+
+ currentUIState = getCurrentUIState();
+
+ ok(!currentUIState.splitterVisibility, "Splitter is hidden after toggling");
+ is(currentUIState.deckHeight, currentUIState.containerHeight,
+ "Deck has a height > 0 after toggling");
+ is(currentUIState.webconsoleHeight, 0,
+ "Web console is collapsed after toggling");
+ ok(!currentUIState.openedConsolePanel,
+ "The console panel is not the current tool");
+ ok(!currentUIState.buttonSelected, "The command button is not selected.");
+ }
+
+ function testBottomHost() {
+ checkHostType(Toolbox.HostType.BOTTOM);
+
+ checkToolboxUI();
+
+ toolbox.switchHost(Toolbox.HostType.SIDE).then(testSidebarHost);
+ }
+
+ function testSidebarHost() {
+ checkHostType(Toolbox.HostType.SIDE);
+
+ checkToolboxUI();
+
+ toolbox.switchHost(Toolbox.HostType.WINDOW).then(testWindowHost);
+ }
+
+ function testWindowHost() {
+ checkHostType(Toolbox.HostType.WINDOW);
+
+ checkToolboxUI();
+
+ toolbox.switchHost(Toolbox.HostType.BOTTOM).then(testDestroy);
+ }
+
+ function checkHostType(hostType) {
+ is(toolbox.hostType, hostType, "host type is " + hostType);
+
+ let pref = Services.prefs.getCharPref("devtools.toolbox.host");
+ is(pref, hostType, "host pref is " + hostType);
+ }
+
+ function testDestroy() {
+ toolbox.destroy().then(function () {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ gDevTools.showToolbox(target).then(finish);
+ });
+ }
+
+ function finish() {
+ toolbox = null;
+ finishTest();
+ }
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_split_escape_key.js b/devtools/client/webconsole/test/browser_webconsole_split_escape_key.js
new file mode 100644
index 000000000..f71efb99e
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_split_escape_key.js
@@ -0,0 +1,158 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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";
+
+ function test() {
+ info("Test various cases where the escape key should hide the split console.");
+
+ let toolbox;
+ let hud;
+ let jsterm;
+ let hudMessages;
+ let variablesView;
+
+ Task.spawn(runner).then(finish);
+
+ function* runner() {
+ let {tab} = yield loadTab("data:text/html;charset=utf-8,<p>Web Console " +
+ "test for splitting");
+ let target = TargetFactory.forTab(tab);
+ toolbox = yield gDevTools.showToolbox(target, "inspector");
+
+ yield testCreateSplitConsoleAfterEscape();
+
+ yield showAutoCompletePopoup();
+
+ yield testHideAutoCompletePopupAfterEscape();
+
+ yield executeJS();
+ yield clickMessageAndShowVariablesView();
+ jsterm.focus();
+
+ yield testHideVariablesViewAfterEscape();
+
+ yield clickMessageAndShowVariablesView();
+ yield startPropertyEditor();
+
+ yield testCancelPropertyEditorAfterEscape();
+ yield testHideVariablesViewAfterEscape();
+ yield testHideSplitConsoleAfterEscape();
+ }
+
+ function testCreateSplitConsoleAfterEscape() {
+ let result = toolbox.once("webconsole-ready", () => {
+ hud = toolbox.getPanel("webconsole").hud;
+ jsterm = hud.jsterm;
+ ok(toolbox.splitConsole, "Split console is created.");
+ });
+
+ let contentWindow = toolbox.win;
+ contentWindow.focus();
+ EventUtils.sendKey("ESCAPE", contentWindow);
+
+ return result;
+ }
+
+ function testHideSplitConsoleAfterEscape() {
+ let result = toolbox.once("split-console", () => {
+ ok(!toolbox.splitConsole, "Split console is hidden.");
+ });
+ EventUtils.sendKey("ESCAPE", toolbox.win);
+
+ return result;
+ }
+
+ function testHideVariablesViewAfterEscape() {
+ let result = jsterm.once("sidebar-closed", () => {
+ ok(!hud.ui.jsterm.sidebar,
+ "Variables view is hidden.");
+ ok(toolbox.splitConsole,
+ "Split console is open after hiding the variables view.");
+ });
+ EventUtils.sendKey("ESCAPE", toolbox.win);
+
+ return result;
+ }
+
+ function testHideAutoCompletePopupAfterEscape() {
+ let deferred = promise.defer();
+ let popup = jsterm.autocompletePopup;
+
+ popup.once("popup-closed", () => {
+ ok(!popup.isOpen,
+ "Auto complete popup is hidden.");
+ ok(toolbox.splitConsole,
+ "Split console is open after hiding the autocomplete popup.");
+
+ deferred.resolve();
+ });
+
+ EventUtils.sendKey("ESCAPE", toolbox.win);
+
+ return deferred.promise;
+ }
+
+ function testCancelPropertyEditorAfterEscape() {
+ EventUtils.sendKey("ESCAPE", variablesView.window);
+ ok(hud.ui.jsterm.sidebar,
+ "Variables view is open after canceling property editor.");
+ ok(toolbox.splitConsole,
+ "Split console is open after editing.");
+ }
+
+ function* executeJS() {
+ jsterm.execute("var foo = { bar: \"baz\" }; foo;");
+ hudMessages = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "Object { bar: \"baz\" }",
+ category: CATEGORY_OUTPUT,
+ objects: true
+ }],
+ });
+ }
+
+ function clickMessageAndShowVariablesView() {
+ let result = jsterm.once("variablesview-fetched", (event, vview) => {
+ variablesView = vview;
+ });
+
+ let clickable = hudMessages[0].clickableElements[0];
+ EventUtils.synthesizeMouse(clickable, 2, 2, {}, hud.iframeWindow);
+
+ return result;
+ }
+
+ function* startPropertyEditor() {
+ let results = yield findVariableViewProperties(variablesView, [
+ {name: "bar", value: "baz"}
+ ], {webconsole: hud});
+ results[0].matchedProp.focus();
+ EventUtils.synthesizeKey("VK_RETURN", variablesView.window);
+ }
+
+ function showAutoCompletePopoup() {
+ let onPopupShown = jsterm.autocompletePopup.once("popup-opened");
+
+ jsterm.focus();
+ jsterm.setInputValue("document.location.");
+ EventUtils.sendKey("TAB", hud.iframeWindow);
+
+ return onPopupShown;
+ }
+
+ function finish() {
+ toolbox.destroy().then(() => {
+ toolbox = null;
+ hud = null;
+ jsterm = null;
+ hudMessages = null;
+ variablesView = null;
+
+ finishTest();
+ });
+ }
+ }
diff --git a/devtools/client/webconsole/test/browser_webconsole_split_focus.js b/devtools/client/webconsole/test/browser_webconsole_split_focus.js
new file mode 100644
index 000000000..ff65229c9
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_split_focus.js
@@ -0,0 +1,66 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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";
+
+ function test() {
+ info("Test that the split console state is persisted");
+
+ let toolbox;
+ let TEST_URI = "data:text/html;charset=utf-8,<p>Web Console test for " +
+ "splitting</p>";
+
+ Task.spawn(runner).then(finish);
+
+ function* runner() {
+ info("Opening a tab while there is no user setting on split console pref");
+ let {tab} = yield loadTab(TEST_URI);
+ let target = TargetFactory.forTab(tab);
+ toolbox = yield gDevTools.showToolbox(target, "inspector");
+
+ ok(!toolbox.splitConsole, "Split console is hidden by default");
+
+ info("Focusing the search box before opening the split console");
+ let inspector = toolbox.getPanel("inspector");
+ inspector.searchBox.focus();
+
+ let activeElement = getActiveElement(inspector.panelDoc);
+ is(activeElement, inspector.searchBox, "Search box is focused");
+
+ yield toolbox.openSplitConsole();
+
+ ok(toolbox.splitConsole, "Split console is now visible");
+
+ // Use the binding element since jsterm.inputNode is a XUL textarea element.
+ activeElement = getActiveElement(toolbox.doc);
+ activeElement = activeElement.ownerDocument.getBindingParent(activeElement);
+ let inputNode = toolbox.getPanel("webconsole").hud.jsterm.inputNode;
+ is(activeElement, inputNode, "Split console input is focused by default");
+
+ yield toolbox.closeSplitConsole();
+
+ info("Making sure that the search box is refocused after closing the " +
+ "split console");
+ activeElement = getActiveElement(inspector.panelDoc);
+ is(activeElement, inspector.searchBox, "Search box is focused");
+
+ yield toolbox.destroy();
+ }
+
+ function getActiveElement(doc) {
+ let activeElement = doc.activeElement;
+ while (activeElement && activeElement.contentDocument) {
+ activeElement = activeElement.contentDocument.activeElement;
+ }
+ return activeElement;
+ }
+
+ function finish() {
+ toolbox = TEST_URI = null;
+ Services.prefs.clearUserPref("devtools.toolbox.splitconsoleEnabled");
+ Services.prefs.clearUserPref("devtools.toolbox.splitconsoleHeight");
+ finishTest();
+ }
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_split_persist.js b/devtools/client/webconsole/test/browser_webconsole_split_persist.js
new file mode 100644
index 000000000..e11bd4811
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_split_persist.js
@@ -0,0 +1,119 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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";
+
+ function test() {
+ info("Test that the split console state is persisted");
+
+ let toolbox;
+ let TEST_URI = "data:text/html;charset=utf-8,<p>Web Console test for " +
+ "splitting</p>";
+
+ Task.spawn(runner).then(finish);
+
+ function* runner() {
+ info("Opening a tab while there is no user setting on split console pref");
+ let {tab} = yield loadTab(TEST_URI);
+ let target = TargetFactory.forTab(tab);
+ toolbox = yield gDevTools.showToolbox(target, "inspector");
+
+ ok(!toolbox.splitConsole, "Split console is hidden by default.");
+ ok(!isCommandButtonChecked(), "Split console button is unchecked by " +
+ "default.");
+ yield toggleSplitConsoleWithEscape();
+ ok(toolbox.splitConsole, "Split console is now visible.");
+ ok(isCommandButtonChecked(), "Split console button is now checked.");
+ ok(getVisiblePrefValue(), "Visibility pref is true");
+
+ is(getHeightPrefValue(), toolbox.webconsolePanel.height,
+ "Panel height matches the pref");
+ toolbox.webconsolePanel.height = 200;
+
+ yield toolbox.destroy();
+
+ info("Opening a tab while there is a true user setting on split console " +
+ "pref");
+ ({tab} = yield loadTab(TEST_URI));
+ target = TargetFactory.forTab(tab);
+ toolbox = yield gDevTools.showToolbox(target, "inspector");
+
+ ok(toolbox.splitConsole, "Split console is visible by default.");
+ ok(isCommandButtonChecked(), "Split console button is checked by default.");
+ is(getHeightPrefValue(), 200, "Height is set based on panel height after " +
+ "closing");
+
+ // Use the binding element since jsterm.inputNode is a XUL textarea element.
+ let activeElement = getActiveElement(toolbox.doc);
+ activeElement = activeElement.ownerDocument.getBindingParent(activeElement);
+ let inputNode = toolbox.getPanel("webconsole").hud.jsterm.inputNode;
+ is(activeElement, inputNode, "Split console input is focused by default");
+
+ toolbox.webconsolePanel.height = 1;
+ ok(toolbox.webconsolePanel.clientHeight > 1,
+ "The actual height of the console is bound with a min height");
+
+ toolbox.webconsolePanel.height = 10000;
+ ok(toolbox.webconsolePanel.clientHeight < 10000,
+ "The actual height of the console is bound with a max height");
+
+ yield toggleSplitConsoleWithEscape();
+ ok(!toolbox.splitConsole, "Split console is now hidden.");
+ ok(!isCommandButtonChecked(), "Split console button is now unchecked.");
+ ok(!getVisiblePrefValue(), "Visibility pref is false");
+
+ yield toolbox.destroy();
+
+ is(getHeightPrefValue(), 10000,
+ "Height is set based on panel height after closing");
+
+ info("Opening a tab while there is a false user setting on split " +
+ "console pref");
+ ({tab} = yield loadTab(TEST_URI));
+ target = TargetFactory.forTab(tab);
+ toolbox = yield gDevTools.showToolbox(target, "inspector");
+
+ ok(!toolbox.splitConsole, "Split console is hidden by default.");
+ ok(!getVisiblePrefValue(), "Visibility pref is false");
+
+ yield toolbox.destroy();
+ }
+
+ function getActiveElement(doc) {
+ let activeElement = doc.activeElement;
+ while (activeElement && activeElement.contentDocument) {
+ activeElement = activeElement.contentDocument.activeElement;
+ }
+ return activeElement;
+ }
+
+ function getVisiblePrefValue() {
+ return Services.prefs.getBoolPref("devtools.toolbox.splitconsoleEnabled");
+ }
+
+ function getHeightPrefValue() {
+ return Services.prefs.getIntPref("devtools.toolbox.splitconsoleHeight");
+ }
+
+ function isCommandButtonChecked() {
+ return toolbox.doc.querySelector("#command-button-splitconsole")
+ .hasAttribute("checked");
+ }
+
+ function toggleSplitConsoleWithEscape() {
+ let onceSplitConsole = toolbox.once("split-console");
+ let contentWindow = toolbox.win;
+ contentWindow.focus();
+ EventUtils.sendKey("ESCAPE", contentWindow);
+ return onceSplitConsole;
+ }
+
+ function finish() {
+ toolbox = TEST_URI = null;
+ Services.prefs.clearUserPref("devtools.toolbox.splitconsoleEnabled");
+ Services.prefs.clearUserPref("devtools.toolbox.splitconsoleHeight");
+ finishTest();
+ }
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_start_netmon_first.js b/devtools/client/webconsole/test/browser_webconsole_start_netmon_first.js
new file mode 100644
index 000000000..a10acf9b2
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_start_netmon_first.js
@@ -0,0 +1,38 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Check that the webconsole works if the network monitor is first opened, then
+// the user switches to the webconsole. See bug 970914.
+
+"use strict";
+
+function test() {
+ Task.spawn(runner).then(finishTest);
+
+ function* runner() {
+ const {tab} = yield loadTab("data:text/html;charset=utf8,<p>hello");
+
+ const hud = yield openConsole(tab);
+
+ hud.jsterm.execute("console.log('foobar bug970914')");
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "console.log",
+ text: "foobar bug970914",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+
+ let text = hud.outputNode.textContent;
+ isnot(text.indexOf("foobar bug970914"), -1,
+ "console.log message confirmed");
+ ok(!/logging API|disabled by a script/i.test(text),
+ "no warning about disabled console API");
+ }
+}
+
diff --git a/devtools/client/webconsole/test/browser_webconsole_strict_mode_errors.js b/devtools/client/webconsole/test/browser_webconsole_strict_mode_errors.js
new file mode 100644
index 000000000..c8f2200f9
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_strict_mode_errors.js
@@ -0,0 +1,83 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Check that "use strict" JS errors generate errors, not warnings.
+
+"use strict";
+
+add_task(function* () {
+ // On e10s, the exception is triggered in child process
+ // and is ignored by test harness
+ if (!Services.appinfo.browserTabsRemoteAutostart) {
+ expectUncaughtException();
+ }
+ yield loadTab("data:text/html;charset=utf8,<script>'use strict';var arguments;</script>");
+
+ let hud = yield openConsole();
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ text: "SyntaxError: 'arguments' can't be defined or assigned to in strict mode code",
+ category: CATEGORY_JS,
+ severity: SEVERITY_ERROR,
+ },
+ ],
+ });
+
+ if (!Services.appinfo.browserTabsRemoteAutostart) {
+ expectUncaughtException();
+ }
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, "data:text/html;charset="
+ + "utf8,<script>'use strict';function f(a, a) {};</script>");
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ text: "SyntaxError: duplicate formal argument a",
+ category: CATEGORY_JS,
+ severity: SEVERITY_ERROR,
+ },
+ ],
+ });
+
+ if (!Services.appinfo.browserTabsRemoteAutostart) {
+ expectUncaughtException();
+ }
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, "data:text/html;charset="
+ + "utf8,<script>'use strict';var o = {get p() {}};o.p = 1;</script>");
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ text: "TypeError: setting a property that has only a getter",
+ category: CATEGORY_JS,
+ severity: SEVERITY_ERROR,
+ },
+ ],
+ });
+
+ if (!Services.appinfo.browserTabsRemoteAutostart) {
+ expectUncaughtException();
+ }
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser,
+ "data:text/html;charset=utf8,<script>'use strict';v = 1;</script>");
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ text: "ReferenceError: assignment to undeclared variable v",
+ category: CATEGORY_JS,
+ severity: SEVERITY_ERROR,
+ },
+ ],
+ });
+
+ hud = null;
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_trackingprotection_errors.js b/devtools/client/webconsole/test/browser_webconsole_trackingprotection_errors.js
new file mode 100644
index 000000000..eafeee18e
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_trackingprotection_errors.js
@@ -0,0 +1,54 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Load a page with tracking elements that get blocked and make sure that a
+// 'learn more' link shows up in the webconsole.
+
+"use strict";
+
+const TEST_URI = "http://tracking.example.org/browser/devtools/client/" +
+ "webconsole/test/test-trackingprotection-securityerrors.html";
+const LEARN_MORE_URI = "https://developer.mozilla.org/Firefox/Privacy/" +
+ "Tracking_Protection" + DOCS_GA_PARAMS;
+const PREF = "privacy.trackingprotection.enabled";
+
+const {UrlClassifierTestUtils} = Cu.import("resource://testing-common/UrlClassifierTestUtils.jsm", {});
+
+registerCleanupFunction(function () {
+ Services.prefs.clearUserPref(PREF);
+ UrlClassifierTestUtils.cleanupTestTrackers();
+});
+
+add_task(function* testMessagesAppear() {
+ yield UrlClassifierTestUtils.addTestTrackers();
+ Services.prefs.setBoolPref(PREF, true);
+
+ let { browser } = yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ let results = yield waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ name: "Was blocked because tracking protection is enabled",
+ text: "The resource at \u201chttp://tracking.example.com/\u201d was " +
+ "blocked because tracking protection is enabled",
+ category: CATEGORY_SECURITY,
+ severity: SEVERITY_WARNING,
+ objects: true,
+ },
+ ],
+ });
+
+ yield testClickOpenNewTab(hud, results[0]);
+});
+
+function testClickOpenNewTab(hud, match) {
+ let warningNode = match.clickableElements[0];
+ ok(warningNode, "link element");
+ ok(warningNode.classList.contains("learn-more-link"), "link class name");
+ return simulateMessageLinkClick(warningNode, LEARN_MORE_URI);
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_view_source.js b/devtools/client/webconsole/test/browser_webconsole_view_source.js
new file mode 100644
index 000000000..a81b58acc
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_view_source.js
@@ -0,0 +1,52 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+// Tests that source URLs in the Web Console can be clicked to display the
+// standard View Source window. As JS exceptions and console.log() messages always
+// have their locations opened in Debugger, we need to test a security message in
+// order to have it opened in the standard View Source window.
+
+"use strict";
+
+const TEST_URI = "https://example.com/browser/devtools/client/webconsole/" +
+ "test/test-mixedcontent-securityerrors.html";
+
+add_task(function* () {
+ yield actuallyTest();
+});
+
+add_task(function* () {
+ Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false);
+ yield actuallyTest();
+ Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend");
+});
+
+var actuallyTest = Task.async(function*() {
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole(null);
+ info("console opened");
+
+ let [result] = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "Blocked loading mixed active content",
+ category: CATEGORY_SECURITY,
+ severity: SEVERITY_ERROR,
+ }],
+ });
+
+ let msg = [...result.matched][0];
+ ok(msg, "error message");
+ let locationNode = msg.querySelector(".message-location .frame-link-filename");
+ ok(locationNode, "location node");
+
+ let onTabOpen = waitForTab();
+
+ EventUtils.sendMouseEvent({ type: "click" }, locationNode);
+
+ let tab = yield onTabOpen;
+ ok(true, "the view source tab was opened in response to clicking the location node");
+ gBrowser.removeTab(tab);
+});
diff --git a/devtools/client/webconsole/test/head.js b/devtools/client/webconsole/test/head.js
new file mode 100644
index 000000000..519cb78b0
--- /dev/null
+++ b/devtools/client/webconsole/test/head.js
@@ -0,0 +1,1844 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/* import-globals-from ../../framework/test/shared-head.js */
+"use strict";
+
+// shared-head.js handles imports, constants, and utility functions
+Services.scriptloader.loadSubScript("chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js", this);
+
+var {Utils: WebConsoleUtils} = require("devtools/client/webconsole/utils");
+var {Messages} = require("devtools/client/webconsole/console-output");
+const asyncStorage = require("devtools/shared/async-storage");
+const HUDService = require("devtools/client/webconsole/hudservice");
+
+// Services.prefs.setBoolPref("devtools.debugger.log", true);
+
+var gPendingOutputTest = 0;
+
+// The various categories of messages.
+const CATEGORY_NETWORK = 0;
+const CATEGORY_CSS = 1;
+const CATEGORY_JS = 2;
+const CATEGORY_WEBDEV = 3;
+const CATEGORY_INPUT = 4;
+const CATEGORY_OUTPUT = 5;
+const CATEGORY_SECURITY = 6;
+const CATEGORY_SERVER = 7;
+
+// The possible message severities.
+const SEVERITY_ERROR = 0;
+const SEVERITY_WARNING = 1;
+const SEVERITY_INFO = 2;
+const SEVERITY_LOG = 3;
+
+// The indent of a console group in pixels.
+const GROUP_INDENT = 12;
+
+const WEBCONSOLE_STRINGS_URI = "devtools/client/locales/webconsole.properties";
+var WCUL10n = new WebConsoleUtils.L10n(WEBCONSOLE_STRINGS_URI);
+
+const DOCS_GA_PARAMS = "?utm_source=mozilla" +
+ "&utm_medium=firefox-console-errors" +
+ "&utm_campaign=default";
+
+flags.testing = true;
+
+function loadTab(url) {
+ let deferred = promise.defer();
+
+ let tab = gBrowser.selectedTab = gBrowser.addTab(url);
+ let browser = gBrowser.getBrowserForTab(tab);
+
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ deferred.resolve({tab: tab, browser: browser});
+ }, true);
+
+ return deferred.promise;
+}
+
+function loadBrowser(browser) {
+ return BrowserTestUtils.browserLoaded(browser);
+}
+
+function closeTab(tab) {
+ let deferred = promise.defer();
+
+ let container = gBrowser.tabContainer;
+
+ container.addEventListener("TabClose", function onTabClose() {
+ container.removeEventListener("TabClose", onTabClose, true);
+ deferred.resolve(null);
+ }, true);
+
+ gBrowser.removeTab(tab);
+
+ return deferred.promise;
+}
+
+/**
+ * Load the page and return the associated HUD.
+ *
+ * @param string uri
+ * The URI of the page to load.
+ * @param string consoleType [optional]
+ * The console type, either "browserConsole" or "webConsole". Defaults to
+ * "webConsole".
+ * @return object
+ * The HUD associated with the console
+ */
+function* loadPageAndGetHud(uri, consoleType) {
+ let { browser } = yield loadTab("data:text/html;charset=utf-8,Loading tab for tests");
+
+ let hud;
+ if (consoleType === "browserConsole") {
+ hud = yield HUDService.openBrowserConsoleOrFocus();
+ } else {
+ hud = yield openConsole();
+ }
+
+ ok(hud, "Console was opened");
+
+ let loaded = loadBrowser(browser);
+ yield BrowserTestUtils.loadURI(gBrowser.selectedBrowser, uri);
+ yield loaded;
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: uri,
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_LOG,
+ }],
+ });
+
+ return hud;
+}
+
+function afterAllTabsLoaded(callback, win) {
+ win = win || window;
+
+ let stillToLoad = 0;
+
+ function onLoad() {
+ this.removeEventListener("load", onLoad, true);
+ stillToLoad--;
+ if (!stillToLoad) {
+ callback();
+ }
+ }
+
+ for (let a = 0; a < win.gBrowser.tabs.length; a++) {
+ let browser = win.gBrowser.tabs[a].linkedBrowser;
+ if (browser.webProgress.isLoadingDocument) {
+ stillToLoad++;
+ browser.addEventListener("load", onLoad, true);
+ }
+ }
+
+ if (!stillToLoad) {
+ callback();
+ }
+}
+
+/**
+ * Check if a log entry exists in the HUD output node.
+ *
+ * @param {Element} outputNode
+ * the HUD output node.
+ * @param {string} matchString
+ * the string you want to check if it exists in the output node.
+ * @param {string} msg
+ * the message describing the test
+ * @param {boolean} [onlyVisible=false]
+ * find only messages that are visible, not hidden by the filter.
+ * @param {boolean} [failIfFound=false]
+ * fail the test if the string is found in the output node.
+ * @param {string} cssClass [optional]
+ * find only messages with the given CSS class.
+ */
+function testLogEntry(outputNode, matchString, msg, onlyVisible,
+ failIfFound, cssClass) {
+ let selector = ".message";
+ // Skip entries that are hidden by the filter.
+ if (onlyVisible) {
+ selector += ":not(.filtered-by-type):not(.filtered-by-string)";
+ }
+ if (cssClass) {
+ selector += "." + aClass;
+ }
+
+ let msgs = outputNode.querySelectorAll(selector);
+ let found = false;
+ for (let i = 0, n = msgs.length; i < n; i++) {
+ let message = msgs[i].textContent.indexOf(matchString);
+ if (message > -1) {
+ found = true;
+ break;
+ }
+ }
+
+ is(found, !failIfFound, msg);
+}
+
+/**
+ * A convenience method to call testLogEntry().
+ *
+ * @param str string
+ * The string to find.
+ */
+function findLogEntry(str) {
+ testLogEntry(outputNode, str, "found " + str);
+}
+
+/**
+ * Open the Web Console for the given tab.
+ *
+ * @param nsIDOMElement [tab]
+ * Optional tab element for which you want open the Web Console. The
+ * default tab is taken from the global variable |tab|.
+ * @param function [callback]
+ * Optional function to invoke after the Web Console completes
+ * initialization (web-console-created).
+ * @return object
+ * A promise that is resolved once the web console is open.
+ */
+var openConsole = function (tab) {
+ let webconsoleOpened = promise.defer();
+ let target = TargetFactory.forTab(tab || gBrowser.selectedTab);
+ gDevTools.showToolbox(target, "webconsole").then(toolbox => {
+ let hud = toolbox.getCurrentPanel().hud;
+ hud.jsterm._lazyVariablesView = false;
+ webconsoleOpened.resolve(hud);
+ });
+ return webconsoleOpened.promise;
+};
+
+/**
+ * Close the Web Console for the given tab.
+ *
+ * @param nsIDOMElement [tab]
+ * Optional tab element for which you want close the Web Console. The
+ * default tab is taken from the global variable |tab|.
+ * @param function [callback]
+ * Optional function to invoke after the Web Console completes
+ * closing (web-console-destroyed).
+ * @return object
+ * A promise that is resolved once the web console is closed.
+ */
+var closeConsole = Task.async(function* (tab) {
+ let target = TargetFactory.forTab(tab || gBrowser.selectedTab);
+ let toolbox = gDevTools.getToolbox(target);
+ if (toolbox) {
+ yield toolbox.destroy();
+ }
+});
+
+/**
+ * Listen for a new tab to open and return a promise that resolves when one
+ * does and completes the load event.
+ * @return a promise that resolves to the tab object
+ */
+var waitForTab = Task.async(function* () {
+ info("Waiting for a tab to open");
+ yield once(gBrowser.tabContainer, "TabOpen");
+ let tab = gBrowser.selectedTab;
+ let browser = tab.linkedBrowser;
+ yield once(browser, "load", true);
+ info("The tab load completed");
+ return tab;
+});
+
+/**
+ * Dump the output of all open Web Consoles - used only for debugging purposes.
+ */
+function dumpConsoles() {
+ if (gPendingOutputTest) {
+ console.log("dumpConsoles start");
+ for (let [, hud] of HUDService.consoles) {
+ if (!hud.outputNode) {
+ console.debug("no output content for", hud.hudId);
+ continue;
+ }
+
+ console.debug("output content for", hud.hudId);
+ for (let elem of hud.outputNode.childNodes) {
+ dumpMessageElement(elem);
+ }
+ }
+ console.log("dumpConsoles end");
+
+ gPendingOutputTest = 0;
+ }
+}
+
+/**
+ * Dump to output debug information for the given webconsole message.
+ *
+ * @param nsIDOMNode message
+ * The message element you want to display.
+ */
+function dumpMessageElement(message) {
+ let text = message.textContent;
+ let repeats = message.querySelector(".message-repeats");
+ if (repeats) {
+ repeats = repeats.getAttribute("value");
+ }
+ console.debug("id", message.getAttribute("id"),
+ "date", message.timestamp,
+ "class", message.className,
+ "category", message.category,
+ "severity", message.severity,
+ "repeats", repeats,
+ "clipboardText", message.clipboardText,
+ "text", text);
+}
+
+var finishTest = Task.async(function* () {
+ dumpConsoles();
+
+ let browserConsole = HUDService.getBrowserConsole();
+ if (browserConsole) {
+ if (browserConsole.jsterm) {
+ browserConsole.jsterm.clearOutput(true);
+ }
+ yield HUDService.toggleBrowserConsole();
+ }
+
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ yield gDevTools.closeToolbox(target);
+
+ finish();
+});
+
+// Always use the 'old' frontend for tests that rely on it
+Services.prefs.setBoolPref("devtools.webconsole.new-frontend-enabled", false);
+registerCleanupFunction(function* () {
+ Services.prefs.clearUserPref("devtools.webconsole.new-frontend-enabled");
+});
+
+registerCleanupFunction(function* () {
+ flags.testing = false;
+
+ // Remove stored console commands in between tests
+ yield asyncStorage.removeItem("webConsoleHistory");
+
+ dumpConsoles();
+
+ let browserConsole = HUDService.getBrowserConsole();
+ if (browserConsole) {
+ if (browserConsole.jsterm) {
+ browserConsole.jsterm.clearOutput(true);
+ }
+ yield HUDService.toggleBrowserConsole();
+ }
+
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ yield gDevTools.closeToolbox(target);
+
+ while (gBrowser.tabs.length > 1) {
+ gBrowser.removeCurrentTab();
+ }
+});
+
+waitForExplicitFinish();
+
+/**
+ * Polls a given function waiting for it to become true.
+ *
+ * @param object options
+ * Options object with the following properties:
+ * - validator
+ * A validator function that returns a boolean. This is called every few
+ * milliseconds to check if the result is true. When it is true, the
+ * promise is resolved and polling stops. If validator never returns
+ * true, then polling timeouts after several tries and the promise is
+ * rejected.
+ * - name
+ * Name of test. This is used to generate the success and failure
+ * messages.
+ * - timeout
+ * Timeout for validator function, in milliseconds. Default is 5000.
+ * @return object
+ * A Promise object that is resolved based on the validator function.
+ */
+function waitForSuccess(options) {
+ let deferred = promise.defer();
+ let start = Date.now();
+ let timeout = options.timeout || 5000;
+ let {validator} = options;
+
+ function wait() {
+ if ((Date.now() - start) > timeout) {
+ // Log the failure.
+ ok(false, "Timed out while waiting for: " + options.name);
+ deferred.reject(null);
+ return;
+ }
+
+ if (validator(options)) {
+ ok(true, options.name);
+ deferred.resolve(null);
+ } else {
+ setTimeout(wait, 100);
+ }
+ }
+
+ setTimeout(wait, 100);
+
+ return deferred.promise;
+}
+
+var openInspector = Task.async(function* (tab = gBrowser.selectedTab) {
+ let target = TargetFactory.forTab(tab);
+ let toolbox = yield gDevTools.showToolbox(target, "inspector");
+ return toolbox.getCurrentPanel();
+});
+
+/**
+ * Find variables or properties in a VariablesView instance.
+ *
+ * @param object view
+ * The VariablesView instance.
+ * @param array rules
+ * The array of rules you want to match. Each rule is an object with:
+ * - name (string|regexp): property name to match.
+ * - value (string|regexp): property value to match.
+ * - isIterator (boolean): check if the property is an iterator.
+ * - isGetter (boolean): check if the property is a getter.
+ * - isGenerator (boolean): check if the property is a generator.
+ * - dontMatch (boolean): make sure the rule doesn't match any property.
+ * @param object options
+ * Options for matching:
+ * - webconsole: the WebConsole instance we work with.
+ * @return object
+ * A promise object that is resolved when all the rules complete
+ * matching. The resolved callback is given an array of all the rules
+ * you wanted to check. Each rule has a new property: |matchedProp|
+ * which holds a reference to the Property object instance from the
+ * VariablesView. If the rule did not match, then |matchedProp| is
+ * undefined.
+ */
+function findVariableViewProperties(view, rules, options) {
+ // Initialize the search.
+ function init() {
+ // Separate out the rules that require expanding properties throughout the
+ // view.
+ let expandRules = [];
+ let filterRules = rules.filter((rule) => {
+ if (typeof rule.name == "string" && rule.name.indexOf(".") > -1) {
+ expandRules.push(rule);
+ return false;
+ }
+ return true;
+ });
+
+ // Search through the view those rules that do not require any properties to
+ // be expanded. Build the array of matchers, outstanding promises to be
+ // resolved.
+ let outstanding = [];
+ finder(filterRules, view, outstanding);
+
+ // Process the rules that need to expand properties.
+ let lastStep = processExpandRules.bind(null, expandRules);
+
+ // Return the results - a promise resolved to hold the updated rules array.
+ let returnResults = onAllRulesMatched.bind(null, rules);
+
+ return promise.all(outstanding).then(lastStep).then(returnResults);
+ }
+
+ function onMatch(prop, rule, matched) {
+ if (matched && !rule.matchedProp) {
+ rule.matchedProp = prop;
+ }
+ }
+
+ function finder(rules, vars, promises) {
+ for (let [, prop] of vars) {
+ for (let rule of rules) {
+ let matcher = matchVariablesViewProperty(prop, rule, options);
+ promises.push(matcher.then(onMatch.bind(null, prop, rule)));
+ }
+ }
+ }
+
+ function processExpandRules(rules) {
+ let rule = rules.shift();
+ if (!rule) {
+ return promise.resolve(null);
+ }
+
+ let deferred = promise.defer();
+ let expandOptions = {
+ rootVariable: view,
+ expandTo: rule.name,
+ webconsole: options.webconsole,
+ };
+
+ variablesViewExpandTo(expandOptions).then(function onSuccess(prop) {
+ let name = rule.name;
+ let lastName = name.split(".").pop();
+ rule.name = lastName;
+
+ let matched = matchVariablesViewProperty(prop, rule, options);
+ return matched.then(onMatch.bind(null, prop, rule)).then(function () {
+ rule.name = name;
+ });
+ }, function onFailure() {
+ return promise.resolve(null);
+ }).then(processExpandRules.bind(null, rules)).then(function () {
+ deferred.resolve(null);
+ });
+
+ return deferred.promise;
+ }
+
+ function onAllRulesMatched(rules) {
+ for (let rule of rules) {
+ let matched = rule.matchedProp;
+ if (matched && !rule.dontMatch) {
+ ok(true, "rule " + rule.name + " matched for property " + matched.name);
+ } else if (matched && rule.dontMatch) {
+ ok(false, "rule " + rule.name + " should not match property " +
+ matched.name);
+ } else {
+ ok(rule.dontMatch, "rule " + rule.name + " did not match any property");
+ }
+ }
+ return rules;
+ }
+
+ return init();
+}
+
+/**
+ * Check if a given Property object from the variables view matches the given
+ * rule.
+ *
+ * @param object prop
+ * The variable's view Property instance.
+ * @param object rule
+ * Rules for matching the property. See findVariableViewProperties() for
+ * details.
+ * @param object options
+ * Options for matching. See findVariableViewProperties().
+ * @return object
+ * A promise that is resolved when all the checks complete. Resolution
+ * result is a boolean that tells your promise callback the match
+ * result: true or false.
+ */
+function matchVariablesViewProperty(prop, rule, options) {
+ function resolve(result) {
+ return promise.resolve(result);
+ }
+
+ if (rule.name) {
+ let match = rule.name instanceof RegExp ?
+ rule.name.test(prop.name) :
+ prop.name == rule.name;
+ if (!match) {
+ return resolve(false);
+ }
+ }
+
+ if (rule.value) {
+ let displayValue = prop.displayValue;
+ if (prop.displayValueClassName == "token-string") {
+ displayValue = displayValue.substring(1, displayValue.length - 1);
+ }
+
+ let match = rule.value instanceof RegExp ?
+ rule.value.test(displayValue) :
+ displayValue == rule.value;
+ if (!match) {
+ info("rule " + rule.name + " did not match value, expected '" +
+ rule.value + "', found '" + displayValue + "'");
+ return resolve(false);
+ }
+ }
+
+ if ("isGetter" in rule) {
+ let isGetter = !!(prop.getter && prop.get("get"));
+ if (rule.isGetter != isGetter) {
+ info("rule " + rule.name + " getter test failed");
+ return resolve(false);
+ }
+ }
+
+ if ("isGenerator" in rule) {
+ let isGenerator = prop.displayValue == "Generator";
+ if (rule.isGenerator != isGenerator) {
+ info("rule " + rule.name + " generator test failed");
+ return resolve(false);
+ }
+ }
+
+ let outstanding = [];
+
+ if ("isIterator" in rule) {
+ let isIterator = isVariableViewPropertyIterator(prop, options.webconsole);
+ outstanding.push(isIterator.then((result) => {
+ if (result != rule.isIterator) {
+ info("rule " + rule.name + " iterator test failed");
+ }
+ return result == rule.isIterator;
+ }));
+ }
+
+ outstanding.push(promise.resolve(true));
+
+ return promise.all(outstanding).then(function _onMatchDone(results) {
+ let ruleMatched = results.indexOf(false) == -1;
+ return resolve(ruleMatched);
+ });
+}
+
+/**
+ * Check if the given variables view property is an iterator.
+ *
+ * @param object prop
+ * The Property instance you want to check.
+ * @param object webConsole
+ * The WebConsole instance to work with.
+ * @return object
+ * A promise that is resolved when the check completes. The resolved
+ * callback is given a boolean: true if the property is an iterator, or
+ * false otherwise.
+ */
+function isVariableViewPropertyIterator(prop, webConsole) {
+ if (prop.displayValue == "Iterator") {
+ return promise.resolve(true);
+ }
+
+ let deferred = promise.defer();
+
+ variablesViewExpandTo({
+ rootVariable: prop,
+ expandTo: "__proto__.__iterator__",
+ webconsole: webConsole,
+ }).then(function onSuccess() {
+ deferred.resolve(true);
+ }, function onFailure() {
+ deferred.resolve(false);
+ });
+
+ return deferred.promise;
+}
+
+/**
+ * Recursively expand the variables view up to a given property.
+ *
+ * @param options
+ * Options for view expansion:
+ * - rootVariable: start from the given scope/variable/property.
+ * - expandTo: string made up of property names you want to expand.
+ * For example: "body.firstChild.nextSibling" given |rootVariable:
+ * document|.
+ * - webconsole: a WebConsole instance. If this is not provided all
+ * property expand() calls will be considered sync. Things may fail!
+ * @return object
+ * A promise that is resolved only when the last property in |expandTo|
+ * is found, and rejected otherwise. Resolution reason is always the
+ * last property - |nextSibling| in the example above. Rejection is
+ * always the last property that was found.
+ */
+function variablesViewExpandTo(options) {
+ let root = options.rootVariable;
+ let expandTo = options.expandTo.split(".");
+ let jsterm = (options.webconsole || {}).jsterm;
+ let lastDeferred = promise.defer();
+
+ function fetch(prop) {
+ if (!prop.onexpand) {
+ ok(false, "property " + prop.name + " cannot be expanded: !onexpand");
+ return promise.reject(prop);
+ }
+
+ let deferred = promise.defer();
+
+ if (prop._fetched || !jsterm) {
+ executeSoon(function () {
+ deferred.resolve(prop);
+ });
+ } else {
+ jsterm.once("variablesview-fetched", function _onFetchProp() {
+ executeSoon(() => deferred.resolve(prop));
+ });
+ }
+
+ prop.expand();
+
+ return deferred.promise;
+ }
+
+ function getNext(prop) {
+ let name = expandTo.shift();
+ let newProp = prop.get(name);
+
+ if (expandTo.length > 0) {
+ ok(newProp, "found property " + name);
+ if (newProp) {
+ fetch(newProp).then(getNext, fetchError);
+ } else {
+ lastDeferred.reject(prop);
+ }
+ } else if (newProp) {
+ lastDeferred.resolve(newProp);
+ } else {
+ lastDeferred.reject(prop);
+ }
+ }
+
+ function fetchError(prop) {
+ lastDeferred.reject(prop);
+ }
+
+ if (!root._fetched) {
+ fetch(root).then(getNext, fetchError);
+ } else {
+ getNext(root);
+ }
+
+ return lastDeferred.promise;
+}
+
+/**
+ * Update the content of a property in the variables view.
+ *
+ * @param object options
+ * Options for the property update:
+ * - property: the property you want to change.
+ * - field: string that tells what you want to change:
+ * - use "name" to change the property name,
+ * - or "value" to change the property value.
+ * - string: the new string to write into the field.
+ * - webconsole: reference to the Web Console instance we work with.
+ * @return object
+ * A Promise object that is resolved once the property is updated.
+ */
+var updateVariablesViewProperty = Task.async(function* (options) {
+ let view = options.property._variablesView;
+ view.window.focus();
+ options.property.focus();
+
+ switch (options.field) {
+ case "name":
+ EventUtils.synthesizeKey("VK_RETURN", { shiftKey: true }, view.window);
+ break;
+ case "value":
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.window);
+ break;
+ default:
+ throw new Error("options.field is incorrect");
+ }
+
+ let deferred = promise.defer();
+
+ executeSoon(() => {
+ EventUtils.synthesizeKey("A", { accelKey: true }, view.window);
+
+ for (let c of options.string) {
+ EventUtils.synthesizeKey(c, {}, view.window);
+ }
+
+ if (options.webconsole) {
+ options.webconsole.jsterm.once("variablesview-fetched")
+ .then((varView) => deferred.resolve(varView));
+ }
+
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.window);
+
+ if (!options.webconsole) {
+ executeSoon(() => {
+ deferred.resolve(null);
+ });
+ }
+ });
+
+ return deferred.promise;
+});
+
+/**
+ * Open the JavaScript debugger.
+ *
+ * @param object options
+ * Options for opening the debugger:
+ * - tab: the tab you want to open the debugger for.
+ * @return object
+ * A promise that is resolved once the debugger opens, or rejected if
+ * the open fails. The resolution callback is given one argument, an
+ * object that holds the following properties:
+ * - target: the Target object for the Tab.
+ * - toolbox: the Toolbox instance.
+ * - panel: the jsdebugger panel instance.
+ * - panelWin: the window object of the panel iframe.
+ */
+function openDebugger(options = {}) {
+ if (!options.tab) {
+ options.tab = gBrowser.selectedTab;
+ }
+
+ let deferred = promise.defer();
+
+ let target = TargetFactory.forTab(options.tab);
+ let toolbox = gDevTools.getToolbox(target);
+ let dbgPanelAlreadyOpen = toolbox && toolbox.getPanel("jsdebugger");
+
+ gDevTools.showToolbox(target, "jsdebugger").then(function onSuccess(tool) {
+ let panel = tool.getCurrentPanel();
+ let panelWin = panel.panelWin;
+
+ panel._view.Variables.lazyEmpty = false;
+
+ let resolveObject = {
+ target: target,
+ toolbox: tool,
+ panel: panel,
+ panelWin: panelWin,
+ };
+
+ if (dbgPanelAlreadyOpen) {
+ deferred.resolve(resolveObject);
+ } else {
+ panelWin.DebuggerController.waitForSourcesLoaded().then(() => {
+ deferred.resolve(resolveObject);
+ });
+ }
+ }, function onFailure(reason) {
+ console.debug("failed to open the toolbox for 'jsdebugger'", reason);
+ deferred.reject(reason);
+ });
+
+ return deferred.promise;
+}
+
+/**
+ * Returns true if the caret in the debugger editor is placed at the specified
+ * position.
+ * @param panel The debugger panel.
+ * @param {number} line The line number.
+ * @param {number} [col] The column number.
+ * @returns {boolean}
+ */
+function isDebuggerCaretPos(panel, line, col = 1) {
+ let editor = panel.panelWin.DebuggerView.editor;
+ let cursor = editor.getCursor();
+
+ // Source editor starts counting line and column numbers from 0.
+ info("Current editor caret position: " + (cursor.line + 1) + ", " +
+ (cursor.ch + 1));
+ return cursor.line == (line - 1) && cursor.ch == (col - 1);
+}
+
+/**
+ * Wait for messages in the Web Console output.
+ *
+ * @param object options
+ * Options for what you want to wait for:
+ * - webconsole: the webconsole instance you work with.
+ * - matchCondition: "any" or "all". Default: "all". The promise
+ * returned by this function resolves when all of the messages are
+ * matched, if the |matchCondition| is "all". If you set the condition to
+ * "any" then the promise is resolved by any message rule that matches,
+ * irrespective of order - waiting for messages stops whenever any rule
+ * matches.
+ * - messages: an array of objects that tells which messages to wait for.
+ * Properties:
+ * - text: string or RegExp to match the textContent of each new
+ * message.
+ * - noText: string or RegExp that must not match in the message
+ * textContent.
+ * - repeats: the number of message repeats, as displayed by the Web
+ * Console.
+ * - category: match message category. See CATEGORY_* constants at
+ * the top of this file.
+ * - severity: match message severity. See SEVERITY_* constants at
+ * the top of this file.
+ * - count: how many unique web console messages should be matched by
+ * this rule.
+ * - consoleTrace: boolean, set to |true| to match a console.trace()
+ * message. Optionally this can be an object of the form
+ * { file, fn, line } that can match the specified file, function
+ * and/or line number in the trace message.
+ * - consoleTime: string that matches a console.time() timer name.
+ * Provide this if you want to match a console.time() message.
+ * - consoleTimeEnd: same as above, but for console.timeEnd().
+ * - consoleDir: boolean, set to |true| to match a console.dir()
+ * message.
+ * - consoleGroup: boolean, set to |true| to match a console.group()
+ * message.
+ * - consoleTable: boolean, set to |true| to match a console.table()
+ * message.
+ * - longString: boolean, set to |true} to match long strings in the
+ * message.
+ * - collapsible: boolean, set to |true| to match messages that can
+ * be collapsed/expanded.
+ * - type: match messages that are instances of the given object. For
+ * example, you can point to Messages.NavigationMarker to match any
+ * such message.
+ * - objects: boolean, set to |true| if you expect inspectable
+ * objects in the message.
+ * - source: object of the shape { url, line }. This is used to
+ * match the source URL and line number of the error message or
+ * console API call.
+ * - prefix: prefix text to check for in the prefix element.
+ * - stacktrace: array of objects of the form { file, fn, line } that
+ * can match frames in the stacktrace associated with the message.
+ * - groupDepth: number used to check the depth of the message in
+ * a group.
+ * - url: URL to match for network requests.
+ * @return object
+ * A promise object is returned once the messages you want are found.
+ * The promise is resolved with the array of rule objects you give in
+ * the |messages| property. Each objects is the same as provided, with
+ * additional properties:
+ * - matched: a Set of web console messages that matched the rule.
+ * - clickableElements: a list of inspectable objects. This is available
+ * if any of the following properties are present in the rule:
+ * |consoleTrace| or |objects|.
+ * - longStrings: a list of long string ellipsis elements you can click
+ * in the message element, to expand a long string. This is available
+ * only if |longString| is present in the matching rule.
+ */
+function waitForMessages(options) {
+ info("Waiting for messages...");
+
+ gPendingOutputTest++;
+ let webconsole = options.webconsole;
+ let rules = WebConsoleUtils.cloneObject(options.messages, true);
+ let rulesMatched = 0;
+ let listenerAdded = false;
+ let deferred = promise.defer();
+ options.matchCondition = options.matchCondition || "all";
+
+ function checkText(rule, text) {
+ let result = false;
+ if (Array.isArray(rule)) {
+ result = rule.every((s) => checkText(s, text));
+ } else if (typeof rule == "string") {
+ result = text.indexOf(rule) > -1;
+ } else if (rule instanceof RegExp) {
+ result = rule.test(text);
+ } else {
+ result = rule == text;
+ }
+ return result;
+ }
+
+ function checkConsoleTable(rule, element) {
+ let elemText = element.textContent;
+
+ if (!checkText("console.table():", elemText)) {
+ return false;
+ }
+
+ rule.category = CATEGORY_WEBDEV;
+ rule.severity = SEVERITY_LOG;
+ rule.type = Messages.ConsoleTable;
+
+ return true;
+ }
+
+ function checkConsoleTrace(rule, element) {
+ let elemText = element.textContent;
+ let trace = rule.consoleTrace;
+
+ if (!checkText("console.trace():", elemText)) {
+ return false;
+ }
+
+ rule.category = CATEGORY_WEBDEV;
+ rule.severity = SEVERITY_LOG;
+ rule.type = Messages.ConsoleTrace;
+
+ if (!rule.stacktrace && typeof trace == "object" && trace !== true) {
+ if (Array.isArray(trace)) {
+ rule.stacktrace = trace;
+ } else {
+ rule.stacktrace = [trace];
+ }
+ }
+
+ return true;
+ }
+
+ function checkConsoleTime(rule, element) {
+ let elemText = element.textContent;
+ let time = rule.consoleTime;
+
+ if (!checkText(time + ": timer started", elemText)) {
+ return false;
+ }
+
+ rule.category = CATEGORY_WEBDEV;
+ rule.severity = SEVERITY_LOG;
+
+ return true;
+ }
+
+ function checkConsoleTimeEnd(rule, element) {
+ let elemText = element.textContent;
+ let time = rule.consoleTimeEnd;
+ let regex = new RegExp(time + ": -?\\d+([,.]\\d+)?ms");
+
+ if (!checkText(regex, elemText)) {
+ return false;
+ }
+
+ rule.category = CATEGORY_WEBDEV;
+ rule.severity = SEVERITY_LOG;
+
+ return true;
+ }
+
+ function checkConsoleDir(rule, element) {
+ if (!element.classList.contains("inlined-variables-view")) {
+ return false;
+ }
+
+ let elemText = element.textContent;
+ if (!checkText(rule.consoleDir, elemText)) {
+ return false;
+ }
+
+ let iframe = element.querySelector("iframe");
+ if (!iframe) {
+ ok(false, "console.dir message has no iframe");
+ return false;
+ }
+
+ return true;
+ }
+
+ function checkConsoleGroup(rule) {
+ if (!isNaN(parseInt(rule.consoleGroup, 10))) {
+ rule.groupDepth = rule.consoleGroup;
+ }
+ rule.category = CATEGORY_WEBDEV;
+ rule.severity = SEVERITY_LOG;
+
+ return true;
+ }
+
+ function checkSource(rule, element) {
+ let location = getRenderedSource(element);
+ if (!location) {
+ return false;
+ }
+
+ if (!checkText(rule.source.url, location.url)) {
+ return false;
+ }
+
+ if ("line" in rule.source && location.line != rule.source.line) {
+ return false;
+ }
+
+ return true;
+ }
+
+ function checkCollapsible(rule, element) {
+ let msg = element._messageObject;
+ if (!msg || !!msg.collapsible != rule.collapsible) {
+ return false;
+ }
+
+ return true;
+ }
+
+ function checkStacktrace(rule, element) {
+ let stack = rule.stacktrace;
+ let frames = element.querySelectorAll(".stacktrace > .stack-trace > .frame-link");
+ if (!frames.length) {
+ return false;
+ }
+
+ for (let i = 0; i < stack.length; i++) {
+ let frame = frames[i];
+ let expected = stack[i];
+ if (!frame) {
+ ok(false, "expected frame #" + i + " but didnt find it");
+ return false;
+ }
+
+ if (expected.file) {
+ let url = frame.getAttribute("data-url");
+ if (!checkText(expected.file, url)) {
+ ok(false, "frame #" + i + " does not match file name: " +
+ expected.file + " != " + url);
+ displayErrorContext(rule, element);
+ return false;
+ }
+ }
+
+ if (expected.fn) {
+ let fn = frame.querySelector(".frame-link-function-display-name").textContent;
+ if (!checkText(expected.fn, fn)) {
+ ok(false, "frame #" + i + " does not match the function name: " +
+ expected.fn + " != " + fn);
+ displayErrorContext(rule, element);
+ return false;
+ }
+ }
+
+ if (expected.line) {
+ let line = frame.getAttribute("data-line");
+ if (!checkText(expected.line, line)) {
+ ok(false, "frame #" + i + " does not match the line number: " +
+ expected.line + " != " + line);
+ displayErrorContext(rule, element);
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ function hasXhrLabel(element) {
+ let xhr = element.querySelector(".xhr");
+ if (!xhr) {
+ return false;
+ }
+ return true;
+ }
+
+ function checkMessage(rule, element) {
+ let elemText = element.textContent;
+
+ if (rule.text && !checkText(rule.text, elemText)) {
+ return false;
+ }
+
+ if (rule.noText && checkText(rule.noText, elemText)) {
+ return false;
+ }
+
+ if (rule.consoleTable && !checkConsoleTable(rule, element)) {
+ return false;
+ }
+
+ if (rule.consoleTrace && !checkConsoleTrace(rule, element)) {
+ return false;
+ }
+
+ if (rule.consoleTime && !checkConsoleTime(rule, element)) {
+ return false;
+ }
+
+ if (rule.consoleTimeEnd && !checkConsoleTimeEnd(rule, element)) {
+ return false;
+ }
+
+ if (rule.consoleDir && !checkConsoleDir(rule, element)) {
+ return false;
+ }
+
+ if (rule.consoleGroup && !checkConsoleGroup(rule, element)) {
+ return false;
+ }
+
+ if (rule.source && !checkSource(rule, element)) {
+ return false;
+ }
+
+ if ("collapsible" in rule && !checkCollapsible(rule, element)) {
+ return false;
+ }
+
+ if (rule.isXhr && !hasXhrLabel(element)) {
+ return false;
+ }
+
+ if (!rule.isXhr && hasXhrLabel(element)) {
+ return false;
+ }
+
+ let partialMatch = !!(rule.consoleTrace || rule.consoleTime ||
+ rule.consoleTimeEnd);
+
+ // The rule tries to match the newer types of messages, based on their
+ // object constructor.
+ if (rule.type) {
+ if (!element._messageObject ||
+ !(element._messageObject instanceof rule.type)) {
+ if (partialMatch) {
+ ok(false, "message type for rule: " + displayRule(rule));
+ displayErrorContext(rule, element);
+ }
+ return false;
+ }
+ partialMatch = true;
+ }
+
+ if ("category" in rule && element.category != rule.category) {
+ if (partialMatch) {
+ is(element.category, rule.category,
+ "message category for rule: " + displayRule(rule));
+ displayErrorContext(rule, element);
+ }
+ return false;
+ }
+
+ if ("severity" in rule && element.severity != rule.severity) {
+ if (partialMatch) {
+ is(element.severity, rule.severity,
+ "message severity for rule: " + displayRule(rule));
+ displayErrorContext(rule, element);
+ }
+ return false;
+ }
+
+ if (rule.text) {
+ partialMatch = true;
+ }
+
+ if (rule.stacktrace && !checkStacktrace(rule, element)) {
+ if (partialMatch) {
+ ok(false, "failed to match stacktrace for rule: " + displayRule(rule));
+ displayErrorContext(rule, element);
+ }
+ return false;
+ }
+
+ if (rule.category == CATEGORY_NETWORK && "url" in rule &&
+ !checkText(rule.url, element.url)) {
+ return false;
+ }
+
+ if ("repeats" in rule) {
+ let repeats = element.querySelector(".message-repeats");
+ if (!repeats || repeats.getAttribute("value") != rule.repeats) {
+ return false;
+ }
+ }
+
+ if ("groupDepth" in rule) {
+ let indentNode = element.querySelector(".indent");
+ let indent = (GROUP_INDENT * rule.groupDepth) + "px";
+ if (!indentNode || indentNode.style.width != indent) {
+ is(indentNode.style.width, indent,
+ "group depth check failed for message rule: " + displayRule(rule));
+ return false;
+ }
+ }
+
+ if ("longString" in rule) {
+ let longStrings = element.querySelectorAll(".longStringEllipsis");
+ if (rule.longString != !!longStrings[0]) {
+ if (partialMatch) {
+ is(!!longStrings[0], rule.longString,
+ "long string existence check failed for message rule: " +
+ displayRule(rule));
+ displayErrorContext(rule, element);
+ }
+ return false;
+ }
+ rule.longStrings = longStrings;
+ }
+
+ if ("objects" in rule) {
+ let clickables = element.querySelectorAll(".message-body a");
+ if (rule.objects != !!clickables[0]) {
+ if (partialMatch) {
+ is(!!clickables[0], rule.objects,
+ "objects existence check failed for message rule: " +
+ displayRule(rule));
+ displayErrorContext(rule, element);
+ }
+ return false;
+ }
+ rule.clickableElements = clickables;
+ }
+
+ if ("prefix" in rule) {
+ let prefixNode = element.querySelector(".prefix");
+ is(prefixNode && prefixNode.textContent, rule.prefix, "Check prefix");
+ }
+
+ let count = rule.count || 1;
+ if (!rule.matched) {
+ rule.matched = new Set();
+ }
+ rule.matched.add(element);
+
+ return rule.matched.size == count;
+ }
+
+ function onMessagesAdded(event, newMessages) {
+ for (let msg of newMessages) {
+ let elem = msg.node;
+ let location = getRenderedSource(elem);
+ if (location && location.url) {
+ let url = location.url;
+ // Prevent recursion with the browser console and any potential
+ // messages coming from head.js.
+ if (url.indexOf("devtools/client/webconsole/test/head.js") != -1) {
+ continue;
+ }
+ }
+
+ for (let rule of rules) {
+ if (rule._ruleMatched) {
+ continue;
+ }
+
+ let matched = checkMessage(rule, elem);
+ if (matched) {
+ rule._ruleMatched = true;
+ rulesMatched++;
+ ok(1, "matched rule: " + displayRule(rule));
+ if (maybeDone()) {
+ return;
+ }
+ }
+ }
+ }
+ }
+
+ function allRulesMatched() {
+ return options.matchCondition == "all" && rulesMatched == rules.length ||
+ options.matchCondition == "any" && rulesMatched > 0;
+ }
+
+ function maybeDone() {
+ if (allRulesMatched()) {
+ if (listenerAdded) {
+ webconsole.ui.off("new-messages", onMessagesAdded);
+ }
+ gPendingOutputTest--;
+ deferred.resolve(rules);
+ return true;
+ }
+ return false;
+ }
+
+ function testCleanup() {
+ if (allRulesMatched()) {
+ return;
+ }
+
+ if (webconsole.ui) {
+ webconsole.ui.off("new-messages", onMessagesAdded);
+ }
+
+ for (let rule of rules) {
+ if (!rule._ruleMatched) {
+ ok(false, "failed to match rule: " + displayRule(rule));
+ }
+ }
+ }
+
+ function displayRule(rule) {
+ return rule.name || rule.text;
+ }
+
+ function displayErrorContext(rule, element) {
+ console.log("error occured during rule " + displayRule(rule));
+ console.log("while checking the following message");
+ dumpMessageElement(element);
+ }
+
+ executeSoon(() => {
+ let messages = [];
+ for (let elem of webconsole.outputNode.childNodes) {
+ messages.push({
+ node: elem,
+ update: false,
+ });
+ }
+
+ onMessagesAdded("new-messages", messages);
+
+ if (!allRulesMatched()) {
+ listenerAdded = true;
+ registerCleanupFunction(testCleanup);
+ webconsole.ui.on("new-messages", onMessagesAdded);
+ }
+ });
+
+ return deferred.promise;
+}
+
+function whenDelayedStartupFinished(win, callback) {
+ Services.obs.addObserver(function observer(subject, topic) {
+ if (win == subject) {
+ Services.obs.removeObserver(observer, topic);
+ executeSoon(callback);
+ }
+ }, "browser-delayed-startup-finished", false);
+}
+
+/**
+ * Check the web console output for the given inputs. Each input is checked for
+ * the expected JS eval result, the result of calling print(), the result of
+ * console.log(). The JS eval result is also checked if it opens the variables
+ * view on click.
+ *
+ * @param object hud
+ * The web console instance to work with.
+ * @param array inputTests
+ * An array of input tests. An input test element is an object. Each
+ * object has the following properties:
+ * - input: string, JS input value to execute.
+ *
+ * - output: string|RegExp, expected JS eval result.
+ *
+ * - inspectable: boolean, when true, the test runner expects the JS eval
+ * result is an object that can be clicked for inspection.
+ *
+ * - noClick: boolean, when true, the test runner does not click the JS
+ * eval result. Some objects, like |window|, have a lot of properties and
+ * opening vview for them is very slow (they can cause timeouts in debug
+ * builds).
+ *
+ * - consoleOutput: string|RegExp, optional, expected consoleOutput
+ * If not provided consoleOuput = output;
+ *
+ * - printOutput: string|RegExp, optional, expected output for
+ * |print(input)|. If this is not provided, printOutput = output.
+ *
+ * - variablesViewLabel: string|RegExp, optional, the expected variables
+ * view label when the object is inspected. If this is not provided, then
+ * |output| is used.
+ *
+ * - inspectorIcon: boolean, when true, the test runner expects the
+ * result widget to contain an inspectorIcon element (className
+ * open-inspector).
+ *
+ * - expectedTab: string, optional, the full URL of the new tab which
+ * must open. If this is not provided, any new tabs that open will cause
+ * a test failure.
+ */
+function checkOutputForInputs(hud, inputTests) {
+ let container = gBrowser.tabContainer;
+
+ function* runner() {
+ for (let [i, entry] of inputTests.entries()) {
+ info("checkInput(" + i + "): " + entry.input);
+ yield checkInput(entry);
+ }
+ container = null;
+ }
+
+ function* checkInput(entry) {
+ yield checkConsoleLog(entry);
+ yield checkPrintOutput(entry);
+ yield checkJSEval(entry);
+ }
+
+ function* checkConsoleLog(entry) {
+ info("Logging");
+ hud.jsterm.clearOutput();
+ hud.jsterm.execute("console.log(" + entry.input + ")");
+
+ let consoleOutput = "consoleOutput" in entry ?
+ entry.consoleOutput : entry.output;
+
+ let [result] = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "console.log() output: " + consoleOutput,
+ text: consoleOutput,
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+
+ let msg = [...result.matched][0];
+
+ if (entry.consoleLogClick) {
+ yield checkObjectClick(entry, msg);
+ }
+
+ if (typeof entry.inspectorIcon == "boolean") {
+ info("Checking Inspector Link");
+ yield checkLinkToInspector(entry.inspectorIcon, msg);
+ }
+ }
+
+ function checkPrintOutput(entry) {
+ info("Printing");
+ hud.jsterm.clearOutput();
+ hud.jsterm.execute("print(" + entry.input + ")");
+
+ let printOutput = entry.printOutput || entry.output;
+
+ return waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "print() output: " + printOutput,
+ text: printOutput,
+ category: CATEGORY_OUTPUT,
+ }],
+ });
+ }
+
+ function* checkJSEval(entry) {
+ info("Evaluating");
+ hud.jsterm.clearOutput();
+ hud.jsterm.execute(entry.input);
+
+ let evalOutput = entry.evalOutput || entry.output;
+
+ let [result] = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "JS eval output: " + entry.evalOutput,
+ text: entry.evalOutput,
+ category: CATEGORY_OUTPUT,
+ }],
+ });
+
+ let msg = [...result.matched][0];
+ if (!entry.noClick) {
+ yield checkObjectClick(entry, msg);
+ }
+ if (typeof entry.inspectorIcon == "boolean") {
+ info("Checking Inspector Link: " + entry.input);
+ yield checkLinkToInspector(entry.inspectorIcon, msg);
+ }
+ }
+
+ function* checkObjectClick(entry, msg) {
+ info("Clicking");
+ let body;
+ if (entry.getClickableNode) {
+ body = entry.getClickableNode(msg);
+ } else {
+ body = msg.querySelector(".message-body a") ||
+ msg.querySelector(".message-body");
+ }
+ ok(body, "the message body");
+
+ let deferredVariablesView = promise.defer();
+ entry._onVariablesViewOpen = onVariablesViewOpen.bind(null, entry,
+ deferredVariablesView);
+ hud.jsterm.on("variablesview-open", entry._onVariablesViewOpen);
+
+ let deferredTab = promise.defer();
+ entry._onTabOpen = onTabOpen.bind(null, entry, deferredTab);
+ container.addEventListener("TabOpen", entry._onTabOpen, true);
+
+ body.scrollIntoView();
+
+ if (!entry.suppressClick) {
+ EventUtils.synthesizeMouse(body, 2, 2, {}, hud.iframeWindow);
+ }
+
+ if (entry.inspectable) {
+ info("message body tagName '" + body.tagName + "' className '" +
+ body.className + "'");
+ yield deferredVariablesView.promise;
+ } else {
+ hud.jsterm.off("variablesview-open", entry._onVariablesView);
+ entry._onVariablesView = null;
+ }
+
+ if (entry.expectedTab) {
+ yield deferredTab.promise;
+ } else {
+ container.removeEventListener("TabOpen", entry._onTabOpen, true);
+ entry._onTabOpen = null;
+ }
+
+ yield promise.resolve(null);
+ }
+
+ function onVariablesViewOpen(entry, {resolve, reject}, event, view, options) {
+ info("Variables view opened");
+ let label = entry.variablesViewLabel || entry.output;
+ if (typeof label == "string" && options.label != label) {
+ return;
+ }
+ if (label instanceof RegExp && !label.test(options.label)) {
+ return;
+ }
+
+ hud.jsterm.off("variablesview-open", entry._onVariablesViewOpen);
+ entry._onVariablesViewOpen = null;
+ ok(entry.inspectable, "variables view was shown");
+
+ resolve(null);
+ }
+
+ function onTabOpen(entry, {resolve, reject}, event) {
+ container.removeEventListener("TabOpen", entry._onTabOpen, true);
+ entry._onTabOpen = null;
+ let tab = event.target;
+ let browser = gBrowser.getBrowserForTab(tab);
+
+ Task.spawn(function* () {
+ yield loadBrowser(browser);
+ let uri = yield ContentTask.spawn(browser, {}, function* () {
+ return content.location.href;
+ });
+ ok(entry.expectedTab && entry.expectedTab == uri,
+ "opened tab '" + uri + "', expected tab '" + entry.expectedTab + "'");
+ yield closeTab(tab);
+ }).then(resolve, reject);
+ }
+
+ return Task.spawn(runner);
+}
+
+/**
+ * Check the web console DOM element output for the given inputs.
+ * Each input is checked for the expected JS eval result. The JS eval result is
+ * also checked if it opens the inspector with the correct node selected on
+ * inspector icon click
+ *
+ * @param object hud
+ * The web console instance to work with.
+ * @param array inputTests
+ * An array of input tests. An input test element is an object. Each
+ * object has the following properties:
+ * - input: string, JS input value to execute.
+ *
+ * - output: string, expected JS eval result.
+ *
+ * - displayName: string, expected NodeFront's displayName.
+ *
+ * - attr: Array, expected NodeFront's attributes
+ */
+function checkDomElementHighlightingForInputs(hud, inputs) {
+ function* runner() {
+ let toolbox = gDevTools.getToolbox(hud.target);
+
+ // Loading the inspector panel at first, to make it possible to listen for
+ // new node selections
+ yield toolbox.selectTool("inspector");
+ let inspector = toolbox.getCurrentPanel();
+ yield toolbox.selectTool("webconsole");
+
+ info("Iterating over the test data");
+ for (let data of inputs) {
+ let [result] = yield jsEval(data.input, {text: data.output});
+ let {msg} = yield checkWidgetAndMessage(result);
+ yield checkNodeHighlight(toolbox, inspector, msg, data);
+ }
+ }
+
+ function jsEval(input, message) {
+ info("Executing '" + input + "' in the web console");
+
+ hud.jsterm.clearOutput();
+ hud.jsterm.execute(input);
+
+ return waitForMessages({
+ webconsole: hud,
+ messages: [message]
+ });
+ }
+
+ function* checkWidgetAndMessage(result) {
+ info("Getting the output ElementNode widget");
+
+ let msg = [...result.matched][0];
+ let widget = [...msg._messageObject.widgets][0];
+ ok(widget, "ElementNode widget found in the output");
+
+ info("Waiting for the ElementNode widget to be linked to the inspector");
+ yield widget.linkToInspector();
+
+ return {widget, msg};
+ }
+
+ function* checkNodeHighlight(toolbox, inspector, msg, testData) {
+ let inspectorIcon = msg.querySelector(".open-inspector");
+ ok(inspectorIcon, "Inspector icon found in the ElementNode widget");
+
+ info("Clicking on the inspector icon and waiting for the " +
+ "inspector to be selected");
+ let onInspectorSelected = toolbox.once("inspector-selected");
+ let onInspectorUpdated = inspector.once("inspector-updated");
+ let onNewNode = toolbox.selection.once("new-node-front");
+ let onNodeHighlight = toolbox.once("node-highlight");
+
+ EventUtils.synthesizeMouseAtCenter(inspectorIcon, {},
+ inspectorIcon.ownerDocument.defaultView);
+ yield onInspectorSelected;
+ yield onInspectorUpdated;
+ yield onNodeHighlight;
+ let nodeFront = yield onNewNode;
+
+ ok(true, "Inspector selected and new node got selected");
+
+ is(nodeFront.displayName, testData.displayName,
+ "The correct node was highlighted");
+
+ if (testData.attrs) {
+ let attrs = nodeFront.attributes;
+ for (let i in testData.attrs) {
+ is(attrs[i].name, testData.attrs[i].name,
+ "Expected attribute's name is present");
+ is(attrs[i].value, testData.attrs[i].value,
+ "Expected attribute's value is present");
+ }
+ }
+
+ info("Unhighlight the node by moving away from the markup view");
+ let onNodeUnhighlight = toolbox.once("node-unhighlight");
+ let btn = inspector.toolbox.doc.querySelector(".toolbox-dock-button");
+ EventUtils.synthesizeMouseAtCenter(btn, {type: "mousemove"},
+ inspector.toolbox.win);
+ yield onNodeUnhighlight;
+
+ info("Switching back to the console");
+ yield toolbox.selectTool("webconsole");
+ }
+
+ return Task.spawn(runner);
+}
+
+/**
+ * Finish the request and resolve with the request object.
+ *
+ * @param {Function} predicate A predicate function that takes the request
+ * object as an argument and returns true if the request was the expected one,
+ * false otherwise. The returned promise is resolved ONLY if the predicate
+ * matches a request. Defaults to accepting any request.
+ * @return promise
+ * @resolves The request object.
+ */
+function waitForFinishedRequest(predicate = () => true) {
+ registerCleanupFunction(function () {
+ HUDService.lastFinishedRequest.callback = null;
+ });
+
+ return new Promise(resolve => {
+ HUDService.lastFinishedRequest.callback = request => {
+ // Check if this is the expected request
+ if (predicate(request)) {
+ // Match found. Clear the listener.
+ HUDService.lastFinishedRequest.callback = null;
+
+ resolve(request);
+ } else {
+ info(`Ignoring unexpected request ${JSON.stringify(request, null, 2)}`);
+ }
+ };
+ });
+}
+
+/**
+ * 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 + ".");
+
+ let deferred = promise.defer();
+
+ for (let [add, remove] of [
+ ["addEventListener", "removeEventListener"],
+ ["addListener", "removeListener"],
+ ["on", "off"]
+ ]) {
+ if ((add in target) && (remove in target)) {
+ target[add](eventName, function onEvent(...aArgs) {
+ target[remove](eventName, onEvent, useCapture);
+ deferred.resolve.apply(deferred, aArgs);
+ }, useCapture);
+ break;
+ }
+ }
+
+ return deferred.promise;
+}
+
+/**
+ * Checks a link to the inspector
+ *
+ * @param {boolean} hasLinkToInspector Set to true if the message should
+ * link to the inspector panel.
+ * @param {element} msg The message to test.
+ */
+function checkLinkToInspector(hasLinkToInspector, msg) {
+ let elementNodeWidget = [...msg._messageObject.widgets][0];
+ if (!elementNodeWidget) {
+ ok(!hasLinkToInspector, "The message has no ElementNode widget");
+ return true;
+ }
+
+ return elementNodeWidget.linkToInspector().then(() => {
+ // linkToInspector resolved, check for the .open-inspector element
+ if (hasLinkToInspector) {
+ ok(msg.querySelectorAll(".open-inspector").length,
+ "The ElementNode widget is linked to the inspector");
+ } else {
+ ok(!msg.querySelectorAll(".open-inspector").length,
+ "The ElementNode widget isn't linked to the inspector");
+ }
+ }, () => {
+ // linkToInspector promise rejected, node not linked to inspector
+ ok(!hasLinkToInspector,
+ "The ElementNode widget isn't linked to the inspector");
+ });
+}
+
+function getSourceActor(sources, URL) {
+ let item = sources.getItemForAttachment(a => a.source.url === URL);
+ return item && item.value;
+}
+
+/**
+ * Make a request against an actor and resolve with the packet.
+ * @param object client
+ * The client to use when making the request.
+ * @param function requestType
+ * The client request function to run.
+ * @param array args
+ * The arguments to pass into the function.
+ */
+function getPacket(client, requestType, args) {
+ return new Promise(resolve => {
+ client[requestType](...args, packet => resolve(packet));
+ });
+}
+
+/**
+ * Verify that clicking on a link from a popup notification message tries to
+ * open the expected URL.
+ */
+function simulateMessageLinkClick(element, expectedLink) {
+ let deferred = promise.defer();
+
+ // Invoke the click event and check if a new tab would
+ // open to the correct page.
+ let oldOpenUILinkIn = window.openUILinkIn;
+ window.openUILinkIn = function (link) {
+ if (link == expectedLink) {
+ ok(true, "Clicking the message link opens the desired page");
+ window.openUILinkIn = oldOpenUILinkIn;
+ deferred.resolve();
+ }
+ };
+
+ let event = new MouseEvent("click", {
+ detail: 1,
+ button: 0,
+ bubbles: true,
+ cancelable: true
+ });
+ element.dispatchEvent(event);
+
+ return deferred.promise;
+}
+
+function getRenderedSource(root) {
+ let location = root.querySelector(".message-location .frame-link");
+ return location ? {
+ url: location.getAttribute("data-url"),
+ line: location.getAttribute("data-line"),
+ column: location.getAttribute("data-column"),
+ } : null;
+}
diff --git a/devtools/client/webconsole/test/test-autocomplete-in-stackframe.html b/devtools/client/webconsole/test/test-autocomplete-in-stackframe.html
new file mode 100644
index 000000000..ba5212de3
--- /dev/null
+++ b/devtools/client/webconsole/test/test-autocomplete-in-stackframe.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html dir="ltr" lang="en">
+ <head>
+ <meta charset="utf8">
+ <!--
+ - Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/
+ -->
+ <title>Test for bug 842682 - use the debugger API for web console autocomplete</title>
+ <script>
+ var foo1 = "globalFoo";
+
+ var foo1Obj = {
+ prop1: "111",
+ prop2: {
+ prop21: "212121"
+ }
+ };
+
+ function firstCall()
+ {
+ var foo2 = "fooFirstCall";
+
+ var foo2Obj = {
+ prop1: {
+ prop11: "111111"
+ }
+ };
+
+ secondCall();
+ }
+
+ function secondCall()
+ {
+ var foo3 = "fooSecondCall";
+
+ var foo3Obj = {
+ prop1: {
+ prop11: "313131"
+ }
+ };
+
+ debugger;
+ }
+ </script>
+ </head>
+ <body>
+ <p>Hello world!</p>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-585956-console-trace.html b/devtools/client/webconsole/test/test-bug-585956-console-trace.html
new file mode 100644
index 000000000..e658ba633
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-585956-console-trace.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head><meta charset="utf-8">
+ <title>Web Console test for bug 585956 - console.trace()</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<script type="application/javascript">
+window.foobar585956c = function(a) {
+ console.trace();
+ return a+"c";
+};
+
+function foobar585956b(a) {
+ return foobar585956c(a+"b");
+}
+
+function foobar585956a(omg) {
+ return foobar585956b(omg + "a");
+}
+
+foobar585956a("omg");
+</script>
+ </head>
+ <body>
+ <p>Web Console test for bug 585956 - console.trace().</p>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-593003-iframe-wrong-hud-iframe.html b/devtools/client/webconsole/test/test-bug-593003-iframe-wrong-hud-iframe.html
new file mode 100644
index 000000000..ebf9c515f
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-593003-iframe-wrong-hud-iframe.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>WebConsole test: iframe associated to the wrong HUD</title>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ </head>
+ <body>
+ <p>WebConsole test: iframe associated to the wrong HUD.</p>
+ <p>This is the iframe!</p>
+ </body>
+ </html>
diff --git a/devtools/client/webconsole/test/test-bug-593003-iframe-wrong-hud.html b/devtools/client/webconsole/test/test-bug-593003-iframe-wrong-hud.html
new file mode 100644
index 000000000..8e47cf20f
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-593003-iframe-wrong-hud.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>WebConsole test: iframe associated to the wrong HUD</title>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ </head>
+ <body>
+ <p>WebConsole test: iframe associated to the wrong HUD.</p>
+ <iframe
+ src="http://example.com/browser/devtools/client/webconsole/test/test-bug-593003-iframe-wrong-hud-iframe.html"></iframe>
+ </body>
+ </html>
diff --git a/devtools/client/webconsole/test/test-bug-595934-canvas-css.html b/devtools/client/webconsole/test/test-bug-595934-canvas-css.html
new file mode 100644
index 000000000..3c9cf03a5
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-595934-canvas-css.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>Web Console test for bug 595934 - category: CSS Parser (with
+ Canvas)</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <script type="text/javascript"
+ src="test-bug-595934-canvas-css.js"></script>
+ </head>
+ <body>
+ <p>Web Console test for bug 595934 - category "CSS Parser" (with
+ Canvas).</p>
+ <p><canvas width="200" height="200">Canvas support is required!</canvas></p>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-595934-canvas-css.js b/devtools/client/webconsole/test/test-bug-595934-canvas-css.js
new file mode 100644
index 000000000..ee1ebd425
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-595934-canvas-css.js
@@ -0,0 +1,10 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+window.addEventListener("DOMContentLoaded", function () {
+ var canvas = document.querySelector("canvas");
+ var context = canvas.getContext("2d");
+ context.strokeStyle = "foobarCanvasCssParser";
+}, false);
diff --git a/devtools/client/webconsole/test/test-bug-595934-css-loader.css b/devtools/client/webconsole/test/test-bug-595934-css-loader.css
new file mode 100644
index 000000000..b4224430f
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-595934-css-loader.css
@@ -0,0 +1,10 @@
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+body {
+ color: #0f0;
+ font-weight: bold;
+}
+
diff --git a/devtools/client/webconsole/test/test-bug-595934-css-loader.css^headers^ b/devtools/client/webconsole/test/test-bug-595934-css-loader.css^headers^
new file mode 100644
index 000000000..e7be84a71
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-595934-css-loader.css^headers^
@@ -0,0 +1 @@
+Content-Type: image/png
diff --git a/devtools/client/webconsole/test/test-bug-595934-css-loader.html b/devtools/client/webconsole/test/test-bug-595934-css-loader.html
new file mode 100644
index 000000000..6bb0d54c5
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-595934-css-loader.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>Web Console test for bug 595934 - category: CSS Loader</title>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <link rel="stylesheet" href="test-bug-595934-css-loader.css">
+ </head>
+ <body>
+ <p>Web Console test for bug 595934 - category "CSS Loader".</p>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-595934-css-parser.css b/devtools/client/webconsole/test/test-bug-595934-css-parser.css
new file mode 100644
index 000000000..f6db82398
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-595934-css-parser.css
@@ -0,0 +1,10 @@
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+p {
+ color: #0f0;
+ foobarCssParser: failure;
+}
+
diff --git a/devtools/client/webconsole/test/test-bug-595934-css-parser.html b/devtools/client/webconsole/test/test-bug-595934-css-parser.html
new file mode 100644
index 000000000..a4ea74ba3
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-595934-css-parser.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>Web Console test for bug 595934 - category: CSS Parser</title>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <link rel="stylesheet" type="text/css"
+ href="test-bug-595934-css-parser.css">
+ </head>
+ <body>
+ <p>Web Console test for bug 595934 - category "CSS Parser".</p>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-595934-empty-getelementbyid.html b/devtools/client/webconsole/test/test-bug-595934-empty-getelementbyid.html
new file mode 100644
index 000000000..a70f9011b
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-595934-empty-getelementbyid.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>Web Console test for bug 595934 - category: DOM.
+ (empty getElementById())</title>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <script type="text/javascript"
+ src="test-bug-595934-empty-getelementbyid.js"></script>
+ </head>
+ <body>
+ <p>Web Console test for bug 595934 - category "DOM"
+ (empty getElementById()).</p>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-595934-empty-getelementbyid.js b/devtools/client/webconsole/test/test-bug-595934-empty-getelementbyid.js
new file mode 100644
index 000000000..bf9ccece9
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-595934-empty-getelementbyid.js
@@ -0,0 +1,8 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+window.addEventListener("load", function () {
+ document.getElementById("");
+}, false);
diff --git a/devtools/client/webconsole/test/test-bug-595934-html.html b/devtools/client/webconsole/test/test-bug-595934-html.html
new file mode 100644
index 000000000..fe35afef6
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-595934-html.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>Web Console test for bug 595934 - category: HTML</title>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ </head>
+ <body>
+ <p>Web Console test for bug 595934 - category "HTML".</p>
+ <form action="?" enctype="multipart/form-data">
+ <p><label>Input <input type="text" value="test value"></label></p>
+ </form>
+ </body>
+</html>
+
diff --git a/devtools/client/webconsole/test/test-bug-595934-image.html b/devtools/client/webconsole/test/test-bug-595934-image.html
new file mode 100644
index 000000000..312ecd49f
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-595934-image.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>Web Console test for bug 595934 - category: Image</title>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ </head>
+ <body>
+ <p>Web Console test for bug 595934 - category Image.</p>
+ <p><img src="test-bug-595934-image.jpg" alt="corrupted image"></p>
+ </body>
+</html>
+
+
diff --git a/devtools/client/webconsole/test/test-bug-595934-image.jpg b/devtools/client/webconsole/test/test-bug-595934-image.jpg
new file mode 100644
index 000000000..947e5f11b
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-595934-image.jpg
Binary files differ
diff --git a/devtools/client/webconsole/test/test-bug-595934-imagemap.html b/devtools/client/webconsole/test/test-bug-595934-imagemap.html
new file mode 100644
index 000000000..007c3c01b
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-595934-imagemap.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>Web Console test for bug 595934 - category: ImageMap</title>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ </head>
+ <body>
+ <p>Web Console test for bug 595934 - category "ImageMap".</p>
+ <p><img src="test-image.png" usemap="#testMap" alt="Test image"></p>
+ <map name="testMap">
+ <area shape="rect" coords="0,0,10,10,5" href="#" alt="Test area" />
+ </map>
+ </body>
+</html>
+
diff --git a/devtools/client/webconsole/test/test-bug-595934-malformedxml-external.html b/devtools/client/webconsole/test/test-bug-595934-malformedxml-external.html
new file mode 100644
index 000000000..2fd8beac5
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-595934-malformedxml-external.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>Web Console test for bug 595934 - category: malformed-xml.
+ (external file)</title>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <script type="text/javascript"><!--
+ var req = new XMLHttpRequest();
+ req.open("GET", "test-bug-595934-malformedxml-external.xml", true);
+ req.send(null);
+ // --></script>
+ </head>
+ <body>
+ <p>Web Console test for bug 595934 - category "malformed-xml"
+ (external file).</p>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-595934-malformedxml-external.xml b/devtools/client/webconsole/test/test-bug-595934-malformedxml-external.xml
new file mode 100644
index 000000000..4812786f1
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-595934-malformedxml-external.xml
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ </head>
+ <body>
+ <p>Web Console test for bug 595934 - category "malformed-xml".</p>
+ </body>
diff --git a/devtools/client/webconsole/test/test-bug-595934-malformedxml.xhtml b/devtools/client/webconsole/test/test-bug-595934-malformedxml.xhtml
new file mode 100644
index 000000000..62689c567
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-595934-malformedxml.xhtml
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+ <head>
+ <title>Web Console test for bug 595934 - category: malformed-xml</title>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ </head>
+ <body>
+ <p>Web Console test for bug 595934 - category "malformed-xml".</p>
+ </body>
diff --git a/devtools/client/webconsole/test/test-bug-595934-svg.xhtml b/devtools/client/webconsole/test/test-bug-595934-svg.xhtml
new file mode 100644
index 000000000..572382c64
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-595934-svg.xhtml
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+ <head>
+ <title>Web Console test for bug 595934 - category: SVG</title>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ </head>
+ <body>
+ <p>Web Console test for bug 595934 - category "SVG".</p>
+ <svg version="1.1" width="120" height="fooBarSVG"
+ xmlns="http://www.w3.org/2000/svg">
+ <ellipse fill="#0f0" stroke="#000" cx="50%"
+ cy="50%" rx="50%" ry="50%" />
+ </svg>
+ </body>
+</html>
+
diff --git a/devtools/client/webconsole/test/test-bug-595934-workers.html b/devtools/client/webconsole/test/test-bug-595934-workers.html
new file mode 100644
index 000000000..baf5a6215
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-595934-workers.html
@@ -0,0 +1,18 @@
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>Web Console test for bug 595934 - category: DOM Worker
+ javascript</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ </head>
+ <body>
+ <p id="foobar">Web Console test for bug 595934 - category "DOM Worker
+ javascript".</p>
+ <script type="text/javascript">
+ var myWorker = new Worker("test-bug-595934-workers.js");
+ myWorker.postMessage("hello world");
+ </script>
+ </body>
+</html>
+
diff --git a/devtools/client/webconsole/test/test-bug-595934-workers.js b/devtools/client/webconsole/test/test-bug-595934-workers.js
new file mode 100644
index 000000000..d23f080af
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-595934-workers.js
@@ -0,0 +1,14 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+/* global fooBarWorker*/
+/* eslint-disable no-unused-vars*/
+
+"use strict";
+
+var onmessage = function () {
+ fooBarWorker();
+};
+
diff --git a/devtools/client/webconsole/test/test-bug-597136-external-script-errors.html b/devtools/client/webconsole/test/test-bug-597136-external-script-errors.html
new file mode 100644
index 000000000..25bdeecc5
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-597136-external-script-errors.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+<!--
+ ***** BEGIN LICENSE BLOCK *****
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ *
+ * Contributor(s):
+ * Patrick Walton <pcwalton@mozilla.com>
+ *
+ * ***** END LICENSE BLOCK *****
+ -->
+ <title>Test for bug 597136: external script errors</title>
+ </head>
+ <body>
+ <h1>Test for bug 597136: external script errors</h1>
+ <p><button onclick="f()">Click me</button</p>
+
+ <script type="text/javascript"
+ src="test-bug-597136-external-script-errors.js"></script>
+ </body>
+</html>
+
diff --git a/devtools/client/webconsole/test/test-bug-597136-external-script-errors.js b/devtools/client/webconsole/test/test-bug-597136-external-script-errors.js
new file mode 100644
index 000000000..00821e38e
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-597136-external-script-errors.js
@@ -0,0 +1,9 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+function f() {
+ bogus.g();
+}
+
diff --git a/devtools/client/webconsole/test/test-bug-597756-reopen-closed-tab.html b/devtools/client/webconsole/test/test-bug-597756-reopen-closed-tab.html
new file mode 100644
index 000000000..68e19e677
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-597756-reopen-closed-tab.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<html dir="ltr" xml:lang="en-US" lang="en-US">
+ <head>
+ <meta charset="utf-8">
+ <title>Bug 597756: test error logging after tab close and reopen</title>
+ <!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+ -->
+ </head>
+ <body>
+ <h1>Bug 597756: test error logging after tab close and reopen.</h1>
+
+ <script type="text/javascript"><!--
+ fooBug597756_error.bar();
+ // --></script>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-599725-response-headers.sjs b/devtools/client/webconsole/test/test-bug-599725-response-headers.sjs
new file mode 100644
index 000000000..2e78d6b7b
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-599725-response-headers.sjs
@@ -0,0 +1,25 @@
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function handleRequest(request, response)
+{
+ var Etag = '"4c881ab-b03-435f0a0f9ef00"';
+ var IfNoneMatch = request.hasHeader("If-None-Match")
+ ? request.getHeader("If-None-Match")
+ : "";
+
+ var page = "<!DOCTYPE html><html><body><p>hello world!</p></body></html>";
+
+ response.setHeader("Etag", Etag, false);
+
+ if (IfNoneMatch == Etag) {
+ response.setStatusLine(request.httpVersion, "304", "Not Modified");
+ }
+ else {
+ response.setHeader("Content-Type", "text/html", false);
+ response.setHeader("Content-Length", page.length + "", false);
+ response.write(page);
+ }
+}
diff --git a/devtools/client/webconsole/test/test-bug-600183-charset.html b/devtools/client/webconsole/test/test-bug-600183-charset.html
new file mode 100644
index 000000000..040490a6b
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-600183-charset.html
@@ -0,0 +1,9 @@
+<!DOCTYPE HTML>
+<html dir="ltr" xml:lang="en-US" lang="en-US"><head>
+ <meta charset="gb2312">
+ <title>Console HTTP test page (chinese)</title>
+ </head>
+ <body>
+ <p>µÄÎʺò!</p>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-600183-charset.html^headers^ b/devtools/client/webconsole/test/test-bug-600183-charset.html^headers^
new file mode 100644
index 000000000..9f3e2302f
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-600183-charset.html^headers^
@@ -0,0 +1 @@
+Content-Type: text/html; charset=gb2312
diff --git a/devtools/client/webconsole/test/test-bug-601177-log-levels.html b/devtools/client/webconsole/test/test-bug-601177-log-levels.html
new file mode 100644
index 000000000..a59213907
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-601177-log-levels.html
@@ -0,0 +1,20 @@
+<!DOCTYPE HTML>
+<html dir="ltr" xml:lang="en-US" lang="en-US">
+ <head>
+ <meta charset="utf-8">
+ <title>Web Console test for bug 601177: log levels</title>
+ <script src="test-bug-601177-log-levels.js" type="text/javascript"></script>
+ <script type="text/javascript"><!--
+ window.undefinedPropertyBug601177;
+ // --></script>
+ <!--
+ - Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/
+ -->
+ </head>
+ <body>
+ <h1>Web Console test for bug 601177: log levels</h1>
+ <img src="test-image.png?bug601177">
+ <img src="foobar-known-to-fail.png?bug601177">
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-601177-log-levels.js b/devtools/client/webconsole/test/test-bug-601177-log-levels.js
new file mode 100644
index 000000000..afeb13ff6
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-601177-log-levels.js
@@ -0,0 +1,8 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+foobarBug601177strictError = "strict error";
+
+window.foobarBug601177exception();
diff --git a/devtools/client/webconsole/test/test-bug-603750-websocket.html b/devtools/client/webconsole/test/test-bug-603750-websocket.html
new file mode 100644
index 000000000..f0097dd77
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-603750-websocket.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>Web Console test for bug 603750 - Web Socket errors</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ </head>
+ <body>
+ <p>Web Console test for bug 595934 - Web Socket errors.</p>
+ <iframe src="data:text/html;charset=utf-8,hello world!"></iframe>
+ <script type="text/javascript" src="test-bug-603750-websocket.js"></script>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-603750-websocket.js b/devtools/client/webconsole/test/test-bug-603750-websocket.js
new file mode 100644
index 000000000..3d92c506b
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-603750-websocket.js
@@ -0,0 +1,20 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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";
+
+window.addEventListener("load", function () {
+ let ws1 = new WebSocket("ws://0.0.0.0:81");
+ ws1.onopen = function () {
+ ws1.send("test 1");
+ ws1.close();
+ };
+
+ let ws2 = new window.frames[0].WebSocket("ws://0.0.0.0:82");
+ ws2.onopen = function () {
+ ws2.send("test 2");
+ ws2.close();
+ };
+}, false);
diff --git a/devtools/client/webconsole/test/test-bug-609872-cd-iframe-child.html b/devtools/client/webconsole/test/test-bug-609872-cd-iframe-child.html
new file mode 100644
index 000000000..451eba21e
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-609872-cd-iframe-child.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>test for bug 609872 - iframe child</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ </head>
+ <body>
+ <p>test for bug 609872 - iframe child</p>
+ <script>window.foobarBug609872 = 'child!';</script>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-609872-cd-iframe-parent.html b/devtools/client/webconsole/test/test-bug-609872-cd-iframe-parent.html
new file mode 100644
index 000000000..fdb636b97
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-609872-cd-iframe-parent.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>test for bug 609872 - iframe parent</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ </head>
+ <body>
+ <p>test for bug 609872 - iframe parent</p>
+ <script>window.foobarBug609872 = 'parent!';</script>
+ <iframe src="test-bug-609872-cd-iframe-child.html"></iframe>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-613013-console-api-iframe.html b/devtools/client/webconsole/test/test-bug-613013-console-api-iframe.html
new file mode 100644
index 000000000..edf40e80e
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-613013-console-api-iframe.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>test for bug 613013</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ </head>
+ <body>
+ <p>test for bug 613013</p>
+ <script type="text/javascript"><!--
+ (function () {
+ var iframe = document.createElement('iframe');
+ iframe.src = 'data:text/html;charset=utf-8,little iframe';
+ document.body.appendChild(iframe);
+
+ console.log("foobarBug613013");
+ })();
+ // --></script>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-618078-network-exceptions.html b/devtools/client/webconsole/test/test-bug-618078-network-exceptions.html
new file mode 100644
index 000000000..ac755e1b9
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-618078-network-exceptions.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>Web Console test for bug 618078 - exception in async network request
+ callback</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <script type="text/javascript">
+ var req = new XMLHttpRequest();
+ req.open('GET', 'http://example.com', true);
+ req.onreadystatechange = function() {
+ if (req.readyState == 4) {
+ bug618078exception();
+ }
+ };
+ req.send(null);
+ </script>
+ </head>
+ <body>
+ <p>Web Console test for bug 618078 - exception in async network request
+ callback.</p>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-621644-jsterm-dollar.html b/devtools/client/webconsole/test/test-bug-621644-jsterm-dollar.html
new file mode 100644
index 000000000..09c986703
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-621644-jsterm-dollar.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html dir="ltr" xml:lang="en-US" lang="en-US">
+ <head>
+ <meta charset="utf-8">
+ <title>Web Console test for bug 621644</title>
+ <script>
+ function $(elem) {
+ return elem.innerHTML;
+ }
+ function $$(doc) {
+ return doc.title;
+ }
+ </script>
+ <!--
+ - Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/
+ -->
+ </head>
+ <body>
+ <h1>Web Console test for bug 621644</h1>
+ <p>hello world!</p>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-630733-response-redirect-headers.sjs b/devtools/client/webconsole/test/test-bug-630733-response-redirect-headers.sjs
new file mode 100644
index 000000000..f92e0fe65
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-630733-response-redirect-headers.sjs
@@ -0,0 +1,16 @@
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function handleRequest(request, response)
+{
+ var page = "<!DOCTYPE html><html><body><p>hello world! bug 630733</p></body></html>";
+
+ response.setStatusLine(request.httpVersion, "301", "Moved Permanently");
+ response.setHeader("Content-Type", "text/html", false);
+ response.setHeader("Content-Length", page.length + "", false);
+ response.setHeader("x-foobar-bug630733", "bazbaz", false);
+ response.setHeader("Location", "/redirect-from-bug-630733", false);
+ response.write(page);
+}
diff --git a/devtools/client/webconsole/test/test-bug-632275-getters.html b/devtools/client/webconsole/test/test-bug-632275-getters.html
new file mode 100644
index 000000000..349c301f3
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-632275-getters.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>Web Console test for bug 632275 - getters</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<script type="application/javascript;version=1.8">
+ document.foobar = {
+ _val: 5,
+ get val() { return ++this._val; }
+ };
+</script>
+
+ </head>
+ <body>
+ <p>Web Console test for bug 632275 - getters.</p>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-632347-iterators-generators.html b/devtools/client/webconsole/test/test-bug-632347-iterators-generators.html
new file mode 100644
index 000000000..1eddcf350
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-632347-iterators-generators.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>Web Console test for bug 632347 - iterators and generators</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<script type="application/javascript;version=1.8">
+(function(){
+function genFunc() {
+ var a = 5;
+ while (a < 10) {
+ yield a++;
+ }
+}
+
+window._container = {};
+
+_container.gen1 = genFunc();
+_container.gen1.next();
+
+var obj = { foo: "bar", baz: "baaz", hay: "stack" };
+_container.iter1 = Iterator(obj);
+
+function Range(low, high) {
+ this.low = low;
+ this.high = high;
+}
+
+function RangeIterator(range) {
+ this.range = range;
+ this.current = this.range.low;
+}
+
+RangeIterator.prototype.next = function() {
+ if (this.current > this.range.high) {
+ throw StopIteration;
+ } else {
+ return this.current++;
+ }
+}
+
+Range.prototype.__iterator__ = function() {
+ return new RangeIterator(this);
+}
+
+_container.iter2 = new Range(3, 15);
+
+_container.gen2 = (function* () { for (let i in _container.iter2) yield i * 2; })();
+})();
+</script>
+ </head>
+ <body>
+ <p>Web Console test for bug 632347 - iterators and generators.</p>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-644419-log-limits.html b/devtools/client/webconsole/test/test-bug-644419-log-limits.html
new file mode 100644
index 000000000..21d99ba14
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-644419-log-limits.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <head>
+ <meta charset="utf-8">
+ <title>Test for bug 644419: console log limits</title>
+ </head>
+ <body>
+ <h1>Test for bug 644419: Console should have user-settable log limits for
+ each message category</h1>
+
+ <script type="text/javascript">
+ function foo() {
+ bar.baz();
+ }
+ foo();
+ </script>
+ </body>
+</html>
+
diff --git a/devtools/client/webconsole/test/test-bug-646025-console-file-location.html b/devtools/client/webconsole/test/test-bug-646025-console-file-location.html
new file mode 100644
index 000000000..7c80f1446
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-646025-console-file-location.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<html dir="ltr" xml:lang="en-US" lang="en-US"><head>
+ <meta charset="utf-8">
+ <title>Console file location test</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <script src="test-file-location.js"></script>
+ </head>
+ <body>
+ <h1>Web Console File Location Test Page</h1>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-658368-time-methods.html b/devtools/client/webconsole/test/test-bug-658368-time-methods.html
new file mode 100644
index 000000000..cc50b6313
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-658368-time-methods.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <head>
+ <meta charset="utf-8">
+ <title>Test for bug 658368: Expand console object with time and timeEnd
+ methods</title>
+ </head>
+ <body>
+ <h1>Test for bug 658368: Expand console object with time and timeEnd
+ methods</h1>
+
+ <script type="text/javascript">
+ function foo() {
+ console.timeEnd("aTimer");
+ }
+ console.time("aTimer");
+ foo();
+ console.time("bTimer");
+ </script>
+ </body>
+</html>
+
diff --git a/devtools/client/webconsole/test/test-bug-737873-mixedcontent.html b/devtools/client/webconsole/test/test-bug-737873-mixedcontent.html
new file mode 100644
index 000000000..db83274f0
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-737873-mixedcontent.html
@@ -0,0 +1,15 @@
+<!DOCTYPE HTML>
+<html dir="ltr" xml:lang="en-US" lang="en-US"><head>
+ <meta charset="utf8">
+ <title>Mixed Content test - http on https</title>
+ <script src="testscript.js"></script>
+ <!--
+ - Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/
+ -->
+ </head>
+ <body>
+ <iframe src = "http://example.com"></iframe>
+ </body>
+</html>
+
diff --git a/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning-inner.html b/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning-inner.html
new file mode 100644
index 000000000..ccb363ed9
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning-inner.html
@@ -0,0 +1,13 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf8">
+ <title>Bug 752559 - print warning to error console when iframe sandbox
+ is being used ineffectively</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ </head>
+ <body>
+ <p>I am sandboxed and want to escape.</p>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning-nested1.html b/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning-nested1.html
new file mode 100644
index 000000000..b9939fe83
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning-nested1.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf8">
+ <title>Bug 752559 - print warning to error console when iframe sandbox
+ is being used ineffectively</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ </head>
+ <body>
+ <iframe
+src="http://www.example.com/browser/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning-inner.html"></iframe>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning-nested2.html b/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning-nested2.html
new file mode 100644
index 000000000..7678d15fe
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning-nested2.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf8">
+ <title>Bug 752559 - print warning to error console when iframe sandbox
+ is being used ineffectively</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ </head>
+ <body>
+ <iframe
+src="http://www.example.com/browser/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning-inner.html" sandbox="allow-scripts allow-same-origin"></iframe>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning0.html b/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning0.html
new file mode 100644
index 000000000..233a6cb70
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning0.html
@@ -0,0 +1,13 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf8">
+ <title>Bug 752559 - print warning to error console when iframe sandbox
+ is being used ineffectively (allow-scripts, allow-same-origin)</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ </head>
+ <body>
+ <iframe src="test-bug-752559-ineffective-iframe-sandbox-warning-inner.html" sandbox="allow-scripts allow-same-origin"></iframe>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning1.html b/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning1.html
new file mode 100644
index 000000000..da0d58819
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning1.html
@@ -0,0 +1,13 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf8">
+ <title>Bug 752559 - print warning to error console when iframe sandbox
+ is being used ineffectively (allow-scripts, no allow-same-origin)</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ </head>
+ <body>
+ <iframe src="test-bug-752559-ineffective-iframe-sandbox-warning-inner.html" sandbox="allow-scripts"></iframe>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning2.html b/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning2.html
new file mode 100644
index 000000000..f33f0a6dc
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning2.html
@@ -0,0 +1,13 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf8">
+ <title>Bug 752559 - print warning to error console when iframe sandbox
+ is being used ineffectively (no allow-scripts, allow-same-origin)</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ </head>
+ <body>
+ <iframe src="test-bug-752559-ineffective-iframe-sandbox-warning-inner.html" sandbox="allow-same-origin"></iframe>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning3.html b/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning3.html
new file mode 100644
index 000000000..c0ff6994a
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning3.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf8">
+ <title>Bug 752559 - print warning to error console when iframe sandbox
+ is being used ineffectively (allow-scripts, allow-same-origin)</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ </head>
+ <body>
+ <iframe
+src="http://www.example.com/browser/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning-inner.html" sandbox="allow-scripts allow-same-origin"></iframe>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning4.html b/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning4.html
new file mode 100644
index 000000000..84e0b6c72
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning4.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf8">
+ <title>Bug 752559 - print warning to error console when iframe sandbox
+ is being used ineffectively (allow-scripts, allow-same-origin, nested)</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ </head>
+ <body>
+ <iframe
+src="http://www.example.com/browser/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning-nested1.html" sandbox="allow-scripts allow-same-origin"></iframe>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning5.html b/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning5.html
new file mode 100644
index 000000000..72d86931a
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning5.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf8">
+ <title>Bug 752559 - print warning to error console when iframe sandbox
+ is being used ineffectively (nested, allow-scripts, allow-same-origin)</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ </head>
+ <body>
+ <iframe
+src="http://www.example.com/browser/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning-nested2.html"></iframe>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-762593-insecure-passwords-about-blank-web-console-warning.html b/devtools/client/webconsole/test/test-bug-762593-insecure-passwords-about-blank-web-console-warning.html
new file mode 100644
index 000000000..d7bcd45d6
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-762593-insecure-passwords-about-blank-web-console-warning.html
@@ -0,0 +1,28 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf8">
+ <title>Bug 762593 - Add warning/error Message to Web Console when the
+ page includes Insecure Password fields</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+ <!-- This test tests the scenario where a javascript adds password fields to
+ an about:blank iframe inside an insecure web page. It ensures that
+ insecure password fields like those are detected and a warning is sent to
+ the web console. -->
+ </head>
+ <body>
+ <p>This insecure page is served with an about:blank iframe. A script then adds a
+ password field to it.</p>
+ <iframe id = "myiframe" width = "300" height="300" >
+ </iframe>
+ <script>
+ var doc = window.document;
+ var myIframe = doc.getElementById("myiframe");
+ myIframe.contentDocument.open();
+ myIframe.contentDocument.write("<form><input type = 'password' name='pwd' value='test'> </form>");
+ myIframe.contentDocument.close();
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-762593-insecure-passwords-web-console-warning.html b/devtools/client/webconsole/test/test-bug-762593-insecure-passwords-web-console-warning.html
new file mode 100644
index 000000000..f473303f4
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-762593-insecure-passwords-web-console-warning.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf8">
+ <title>Bug 762593 - Add warning/error Message to Web Console when the
+ page includes Insecure Password fields</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ </head>
+ <body>
+ <p>This page is served with an iframe with insecure password field.</p>
+ <iframe src
+ ="http://example.com/browser/devtools/client/webconsole/test/test-iframe-762593-insecure-frame.html">
+ </iframe>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-766001-console-log.js b/devtools/client/webconsole/test/test-bug-766001-console-log.js
new file mode 100644
index 000000000..a4be0cb15
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-766001-console-log.js
@@ -0,0 +1,10 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+function onLoad123() {
+ console.log("Blah Blah");
+}
+
+window.addEventListener("load", onLoad123, false);
diff --git a/devtools/client/webconsole/test/test-bug-766001-js-console-links.html b/devtools/client/webconsole/test/test-bug-766001-js-console-links.html
new file mode 100644
index 000000000..6a6ac6008
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-766001-js-console-links.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>Web Console test for bug 766001 : Open JS/Console call Links in Debugger</title>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <script type="text/javascript" src="test-bug-766001-js-errors.js"></script>
+ <script type="text/javascript" src="test-bug-766001-console-log.js"></script>
+ </head>
+ <body>
+ <p>Web Console test for bug 766001 : Open JS/Console call Links in Debugger.</p>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-766001-js-errors.js b/devtools/client/webconsole/test/test-bug-766001-js-errors.js
new file mode 100644
index 000000000..85321813a
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-766001-js-errors.js
@@ -0,0 +1,8 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+window.addEventListener("load", function () {
+ document.bar();
+}, false);
diff --git a/devtools/client/webconsole/test/test-bug-782653-css-errors-1.css b/devtools/client/webconsole/test/test-bug-782653-css-errors-1.css
new file mode 100644
index 000000000..ad7fd1999
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-782653-css-errors-1.css
@@ -0,0 +1,10 @@
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+body {
+ color: #0f0;
+ font-weight: green;
+}
+
diff --git a/devtools/client/webconsole/test/test-bug-782653-css-errors-2.css b/devtools/client/webconsole/test/test-bug-782653-css-errors-2.css
new file mode 100644
index 000000000..91b14137a
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-782653-css-errors-2.css
@@ -0,0 +1,10 @@
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+body {
+ color: #0fl;
+ font-weight: bold;
+}
+
diff --git a/devtools/client/webconsole/test/test-bug-782653-css-errors.html b/devtools/client/webconsole/test/test-bug-782653-css-errors.html
new file mode 100644
index 000000000..7ca11fc34
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-782653-css-errors.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>Web Console test for bug 782653 : Open CSS Links in Style Editor</title>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <link rel="stylesheet" href="test-bug-782653-css-errors-1.css">
+ <link rel="stylesheet" href="test-bug-782653-css-errors-2.css">
+ </head>
+ <body>
+ <p>Web Console test for bug 782653 : Open CSS Links in Style Editor.</p>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-837351-security-errors.html b/devtools/client/webconsole/test/test-bug-837351-security-errors.html
new file mode 100644
index 000000000..db83274f0
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-837351-security-errors.html
@@ -0,0 +1,15 @@
+<!DOCTYPE HTML>
+<html dir="ltr" xml:lang="en-US" lang="en-US"><head>
+ <meta charset="utf8">
+ <title>Mixed Content test - http on https</title>
+ <script src="testscript.js"></script>
+ <!--
+ - Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/
+ -->
+ </head>
+ <body>
+ <iframe src = "http://example.com"></iframe>
+ </body>
+</html>
+
diff --git a/devtools/client/webconsole/test/test-bug-859170-longstring-hang.html b/devtools/client/webconsole/test/test-bug-859170-longstring-hang.html
new file mode 100644
index 000000000..51bc0de28
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-859170-longstring-hang.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head><meta charset="utf-8">
+ <title>Web Console test for bug 859170 - very long strings hang the browser</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<script type="application/javascript">
+(function() {
+var longString = "abbababazomglolztest";
+for (var i = 0; i < 10; i++) {
+ longString += longString + longString;
+}
+
+longString = "foobar" + (new Array(9000)).join("a") + "foobaz" +
+ longString + "boom!";
+console.log(longString);
+})();
+</script>
+ </head>
+ <body>
+ <p>Web Console test for bug 859170 - very long strings hang the browser.</p>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-869003-iframe.html b/devtools/client/webconsole/test/test-bug-869003-iframe.html
new file mode 100644
index 000000000..5a29728e5
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-869003-iframe.html
@@ -0,0 +1,20 @@
+<!DOCTYPE HTML>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>Web Console test for bug 869003</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <script type="text/javascript"><!--
+ window.onload = function testConsoleLogging()
+ {
+ var o = { hello: "world!", bug: 869003 };
+ console.log("foobar", o);
+ };
+ // --></script>
+ </head>
+ <body>
+ <p>Make sure users can inspect objects from cross-domain iframes.</p>
+ <p>Iframe window.</p>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-869003-top-window.html b/devtools/client/webconsole/test/test-bug-869003-top-window.html
new file mode 100644
index 000000000..a2da438f6
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-869003-top-window.html
@@ -0,0 +1,14 @@
+<!DOCTYPE HTML>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>Web Console test for bug 869003</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+ </head>
+ <body>
+ <p>Make sure users can inspect objects from cross-domain iframes.</p>
+ <p>Top window.</p>
+ <iframe src="http://example.org/browser/devtools/client/webconsole/test/test-bug-869003-iframe.html"></iframe>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-952277-highlight-nodes-in-vview.html b/devtools/client/webconsole/test/test-bug-952277-highlight-nodes-in-vview.html
new file mode 100644
index 000000000..de297d9b5
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-952277-highlight-nodes-in-vview.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>Web Console test for bug 952277 - Highlighting and selecting nodes from the variablesview</title>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ </head>
+ <body>
+ <p>Web Console test for bug 952277 - Highlighting and selecting nodes from the variablesview</p>
+ <p>Web Console test for bug 952277 - Highlighting and selecting nodes from the variablesview</p>
+ <p>Web Console test for bug 952277 - Highlighting and selecting nodes from the variablesview</p>
+ </body>
+</html>
+
diff --git a/devtools/client/webconsole/test/test-bug-989025-iframe-parent.html b/devtools/client/webconsole/test/test-bug-989025-iframe-parent.html
new file mode 100644
index 000000000..54a4e9038
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-989025-iframe-parent.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>test for bug 989025 - iframe parent</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ </head>
+ <body>
+ <p>test for bug 989025 - iframe parent</p>
+ <iframe src="http://mochi.test:8888/browser/devtools/client/webconsole/test/test-bug-609872-cd-iframe-child.html"></iframe>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug_1050691_click_function_to_source.html b/devtools/client/webconsole/test/test-bug_1050691_click_function_to_source.html
new file mode 100644
index 000000000..912e301f0
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug_1050691_click_function_to_source.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+<html dir="ltr" xml:lang="en-US" lang="en-US">
+ <head>
+ <meta charset="utf-8">
+ <title>Click on function should point to source</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <script type="text/javascript" src="test-bug_1050691_click_function_to_source.js"></script>
+ </head>
+ <body></body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug_1050691_click_function_to_source.js b/devtools/client/webconsole/test/test-bug_1050691_click_function_to_source.js
new file mode 100644
index 000000000..1eddf0d6e
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug_1050691_click_function_to_source.js
@@ -0,0 +1,10 @@
+/**
+ * this
+ * is
+ * a
+ * function
+ */
+function foo() {
+ console.log(foo);
+}
+
diff --git a/devtools/client/webconsole/test/test-bug_923281_console_log_filter.html b/devtools/client/webconsole/test/test-bug_923281_console_log_filter.html
new file mode 100644
index 000000000..f2d650a5d
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug_923281_console_log_filter.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<html dir="ltr" xml:lang="en-US" lang="en-US">
+ <head>
+ <meta charset="utf-8">
+ <title>Console test</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <script type="text/javascript" src="test-bug_923281_test1.js"></script>
+ <script type="text/javascript" src="test-bug_923281_test2.js"></script>
+ </head>
+ <body></body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug_923281_test1.js b/devtools/client/webconsole/test/test-bug_923281_test1.js
new file mode 100644
index 000000000..1c07f1155
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug_923281_test1.js
@@ -0,0 +1,7 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/ */
+
+console.log("Sample log.");
+console.log("This log should be filtered when filtered for test2.js.");
diff --git a/devtools/client/webconsole/test/test-bug_923281_test2.js b/devtools/client/webconsole/test/test-bug_923281_test2.js
new file mode 100644
index 000000000..7ac85b387
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug_923281_test2.js
@@ -0,0 +1,8 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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";
+
+console.log("This is a random text.");
diff --git a/devtools/client/webconsole/test/test-bug_939783_console_trace_duplicates.html b/devtools/client/webconsole/test/test-bug_939783_console_trace_duplicates.html
new file mode 100644
index 000000000..ab44de09f
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug_939783_console_trace_duplicates.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>Web Console test for bug 939783 - different console.trace() calls
+ wrongly filtered as duplicates</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<script type="application/javascript">
+function foo1() {
+ foo2();
+}
+
+function foo1b() {
+ foo2();
+}
+
+function foo2() {
+ foo3();
+}
+
+function foo3() {
+ console.trace();
+}
+
+foo1(); foo1();
+foo1b();
+
+</script>
+ </head>
+ <body>
+ <p>Web Console test for bug 939783 - different console.trace() calls
+ wrongly filtered as duplicates</p>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-certificate-messages.html b/devtools/client/webconsole/test/test-certificate-messages.html
new file mode 100644
index 000000000..b0419a6fc
--- /dev/null
+++ b/devtools/client/webconsole/test/test-certificate-messages.html
@@ -0,0 +1,22 @@
+<!--
+ Bug 1068949 - Log crypto warnings to the security pane in the webconsole
+-->
+
+<!DOCTYPE HTML>
+<html dir="ltr" xml:lang="en-US" lang="en-US">
+ <head>
+ <meta charset="utf8">
+ <title>Security warning test - no violations</title>
+ <!-- ensure no subresource errors so window re-use doesn't cause failures -->
+ <link rel="icon" href="data:;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC">
+ <script>
+ console.log("If you haven't seen ssl warnings yet, you won't");
+ </script>
+ <!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+ -->
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-closure-optimized-out.html b/devtools/client/webconsole/test/test-closure-optimized-out.html
new file mode 100644
index 000000000..3ad4e8fc0
--- /dev/null
+++ b/devtools/client/webconsole/test/test-closure-optimized-out.html
@@ -0,0 +1,34 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset='utf-8'/>
+ <title>Debugger Test for Inspecting Optimized-Out Variables</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <script type="text/javascript">
+ window.addEventListener("load", function onload() {
+ window.removeEventListener("load", onload);
+ function clickHandler(event) {
+ button.removeEventListener("click", clickHandler, false);
+ function outer(arg) {
+ var upvar = arg * 2;
+ // The inner lambda only aliases arg, so the frontend alias analysis decides
+ // that upvar is not aliased and is not in the CallObject.
+ return function () {
+ arg += 2;
+ };
+ }
+
+ var f = outer(42);
+ f();
+ }
+ var button = document.querySelector("button");
+ button.addEventListener("click", clickHandler, false);
+ });
+ </script>
+
+ </head>
+ <body>
+ <button>Click me!</button>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-closures.html b/devtools/client/webconsole/test/test-closures.html
new file mode 100644
index 000000000..4fadade20
--- /dev/null
+++ b/devtools/client/webconsole/test/test-closures.html
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset='utf-8'/>
+ <title>Console Test for Closure Inspection</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <script type="text/javascript">
+ function injectPerson() {
+ var PersonFactory = function _pfactory(name) {
+ var foo = 10;
+ return {
+ getName: function() { return name; },
+ getFoo: function() { foo = Date.now(); return foo; }
+ };
+ };
+ window.george = new PersonFactory("George");
+ debugger;
+ }
+ </script>
+
+ </head>
+ <body>
+ <button onclick="injectPerson()">Test</button>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-console-api-stackframe.html b/devtools/client/webconsole/test/test-console-api-stackframe.html
new file mode 100644
index 000000000..df7fef9b1
--- /dev/null
+++ b/devtools/client/webconsole/test/test-console-api-stackframe.html
@@ -0,0 +1,32 @@
+<!DOCTYPE HTML>
+<html dir="ltr" lang="en">
+ <head>
+ <meta charset="utf8">
+ <!--
+ - Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/
+ -->
+ <title>Test for bug 920116 - stacktraces for console API messages</title>
+ <script>
+ function firstCall() {
+ secondCall();
+ }
+
+ function secondCall() {
+ thirdCall();
+ }
+
+ function thirdCall() {
+ console.log("foo-log");
+ console.error("foo-error");
+ console.exception("foo-exception");
+ console.assert("red" == "blue", "foo-assert");
+ }
+
+ window.onload = firstCall;
+ </script>
+ </head>
+ <body>
+ <p>Hello world!</p>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-console-assert.html b/devtools/client/webconsole/test/test-console-assert.html
new file mode 100644
index 000000000..b104d72d4
--- /dev/null
+++ b/devtools/client/webconsole/test/test-console-assert.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html dir="ltr" xml:lang="en-US" lang="en-US">
+ <head>
+ <!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+ -->
+ <meta charset="utf-8">
+ <title>console.assert() test</title>
+ <script type="text/javascript">
+ function test() {
+ console.log("start");
+ console.assert(false, "false assert");
+ console.assert(0, "falsy assert");
+ console.assert(true, "true assert");
+ console.log("end");
+ }
+ </script>
+ </head>
+ <body>
+ <p>test console.assert()</p>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-console-clear.html b/devtools/client/webconsole/test/test-console-clear.html
new file mode 100644
index 000000000..8009db858
--- /dev/null
+++ b/devtools/client/webconsole/test/test-console-clear.html
@@ -0,0 +1,16 @@
+<!DOCTYPE HTML>
+<html dir="ltr" xml:lang="en-US" lang="en-US"><head>
+ <meta charset="utf-8">
+ <title>Console.clear() tests</title>
+ <script type="text/javascript">
+ console.log("log1");
+ console.log("log2");
+ console.clear();
+
+ window.objFromPage = { a: 1 };
+ </script>
+ </head>
+ <body>
+ <h1 id="header">Clear Demo</h1>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-console-column.html b/devtools/client/webconsole/test/test-console-column.html
new file mode 100644
index 000000000..ff9cc81e1
--- /dev/null
+++ b/devtools/client/webconsole/test/test-console-column.html
@@ -0,0 +1,17 @@
+<!DOCTYPE HTML>
+<html dir="ltr" xml:lang="en-US" lang="en-US">
+ <head>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <meta charset="utf-8">
+ <title>Console test</title>
+
+ <script type="text/javascript">
+ console.info("INLINE SCRIPT:"); console.log('Further');
+ console.warn("I'm warning you, he will eat up all yr bacon.");
+ console.error("Error Message");
+ console.log('Rainbooooww');
+ console.log('NYAN CATZ');
+ </script>
+ </head>
+</html>
diff --git a/devtools/client/webconsole/test/test-console-count-external-file.js b/devtools/client/webconsole/test/test-console-count-external-file.js
new file mode 100644
index 000000000..cca9e2f10
--- /dev/null
+++ b/devtools/client/webconsole/test/test-console-count-external-file.js
@@ -0,0 +1,11 @@
+/* eslint-disable no-unused-vars */
+
+"use strict";
+
+function counterExternalFile() {
+ console.count("console.count() testcounter");
+}
+function externalCountersWithoutLabel() {
+ console.count();
+ console.count();
+}
diff --git a/devtools/client/webconsole/test/test-console-count.html b/devtools/client/webconsole/test/test-console-count.html
new file mode 100644
index 000000000..e6db0ebb0
--- /dev/null
+++ b/devtools/client/webconsole/test/test-console-count.html
@@ -0,0 +1,56 @@
+<!DOCTYPE HTML>
+<html dir="ltr" xml:lang="en-US" lang="en-US">
+ <head>
+ <!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+ -->
+ <meta charset="utf-8">
+ <title>console.count() test</title>
+ <script src="test-console-count-external-file.js"></script>
+ <script tyoe="text/javascript">
+ function counterSeperateScriptTag() {
+ console.count("console.count() testcounter");
+ }
+ </script>
+ <script type="text/javascript">
+ function counterNoLabel() {
+ console.count();
+ }
+ function countersWithoutLabel() {
+ console.count();
+ console.count();
+ }
+ function counterWithLabel() {
+ console.count("console.count() testcounter");
+ }
+ function testLocal() {
+ console.log("start");
+ counterNoLabel();
+ counterNoLabel();
+ countersWithoutLabel();
+ counterWithLabel();
+ counterWithLabel();
+ counterSeperateScriptTag();
+ counterSeperateScriptTag();
+ console.log("end");
+ }
+ function testExternal() {
+ console.log("start");
+ counterExternalFile();
+ counterExternalFile();
+ externalCountersWithoutLabel();
+ console.log("end");
+ }
+ </script>
+ </head>
+ <body>
+ <p>test console.count()</p>
+ <button id="local" onclick="testLocal();">
+ test local console.count() calls
+ </button>
+ <button id="external" onclick="testExternal();">
+ test external console.count() calls
+ </button>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-console-extras.html b/devtools/client/webconsole/test/test-console-extras.html
new file mode 100644
index 000000000..8685b1a80
--- /dev/null
+++ b/devtools/client/webconsole/test/test-console-extras.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<html dir="ltr" xml:lang="en-US" lang="en-US"><head>
+ <meta charset="utf-8">
+ <title>Console extended API test</title>
+ <script type="text/javascript">
+ function test() {
+ console.log("start");
+ console.clear();
+ console.log("end");
+ }
+ </script>
+ </head>
+ <body>
+ <h1 id="header">Heads Up Display Demo</h1>
+ <button onclick="test();">Test Extended API</button>
+ <div id="myDiv"></div>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-console-output-02.html b/devtools/client/webconsole/test/test-console-output-02.html
new file mode 100644
index 000000000..ad90f0ebf
--- /dev/null
+++ b/devtools/client/webconsole/test/test-console-output-02.html
@@ -0,0 +1,66 @@
+<!DOCTYPE HTML>
+<html dir="ltr" lang="en-US">
+<head>
+ <meta charset="utf-8">
+ <title>Test the web console output - 02</title>
+ <!--
+ - Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/
+ -->
+</head>
+<body>
+ <p>hello world!</p>
+ <script type="text/javascript">
+function testfn1() { return 42; }
+
+var testobj1 = {
+ testfn2: function() { return 42; },
+};
+
+function testfn3() { return 42; }
+testfn3.displayName = "testfn3DisplayName";
+
+var array1 = [1, 2, 3, "a", "b", "c", "4", "5"];
+
+var array2 = ["a", document, document.body, document.body.dataset,
+ document.body.classList];
+
+var array3 = [1, window, null, "a", "b", undefined, false, "", -Infinity, testfn3, testobj1, "foo", "bar"];
+
+var array4 = new Array(5);
+array4.push("test");
+array4.push(array4);
+
+var typedarray1 = new Int32Array([1, 287, 8651, 40983, 8754]);
+
+var set1 = new Set([1, 2, null, array3, "a", "b", undefined, document.head]);
+set1.add(set1);
+
+var bunnies = new String("bunnies")
+var weakset = new WeakSet([bunnies, document.head]);
+
+var testobj2 = {a: "b", c: "d", e: 1, f: "2"};
+testobj2.foo = testobj1;
+testobj2.bar = testobj2;
+Object.defineProperty(testobj2, "getterTest", {
+ enumerable: true,
+ get: function() {
+ return 42;
+ },
+});
+
+var testobj3 = {a: "b", c: "d", e: 1, f: "2", g: true, h: null, i: undefined,
+ j: "", k: document.styleSheets, l: document.body.childNodes,
+ o: new Array(125), m: document.head};
+
+var testobj4 = {a: "b", c: "d"};
+Object.defineProperty(testobj4, "nonEnumerable", { value: "hello world" });
+
+var map1 = new Map([["a", "b"], [document.body.children, testobj2]]);
+map1.set(map1, set1);
+
+var weakmap = new WeakMap([[bunnies, 23], [document.body.children, testobj2]]);
+
+ </script>
+</body>
+</html>
diff --git a/devtools/client/webconsole/test/test-console-output-03.html b/devtools/client/webconsole/test/test-console-output-03.html
new file mode 100644
index 000000000..9dcf051a6
--- /dev/null
+++ b/devtools/client/webconsole/test/test-console-output-03.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<html dir="ltr" lang="en-US">
+<head>
+ <meta charset="utf-8">
+ <title>Test the web console output - 03</title>
+ <!--
+ - Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/
+ -->
+</head>
+<body>
+ <p>hello world!</p>
+ <script type="text/javascript">
+function testBodyClassName() {
+ document.body.className = "test1 tezt2";
+ return document.body;
+}
+
+function testBodyID() {
+ document.body.id = 'foobarid';
+ return document.body;
+}
+
+function testBodyDataset() {
+ document.body.dataset.preview = 'zuzu"<a>foo';
+ return document.body;
+}
+ </script>
+</body>
+</html>
diff --git a/devtools/client/webconsole/test/test-console-output-04.html b/devtools/client/webconsole/test/test-console-output-04.html
new file mode 100644
index 000000000..bb4345277
--- /dev/null
+++ b/devtools/client/webconsole/test/test-console-output-04.html
@@ -0,0 +1,77 @@
+<!DOCTYPE HTML>
+<html dir="ltr" lang="en-US">
+<head>
+ <meta charset="utf-8">
+ <title>Test the web console output - 04</title>
+ <!--
+ - Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/
+ -->
+</head>
+<body>
+ <p>hello world!</p>
+ <script type="text/javascript">
+function testTextNode() {
+ return document.querySelector("p").childNodes[0];
+}
+
+function testCommentNode() {
+ return document.head.childNodes[5];
+}
+
+function testDocumentFragment() {
+ var frag = document.createDocumentFragment();
+
+ var div = document.createElement("div");
+ div.id = "foo1";
+ div.className = "bar";
+ frag.appendChild(div);
+
+ var span = document.createElement("span");
+ span.id = "foo2";
+ span.textContent = "hello world";
+ div.appendChild(span);
+
+ var div2 = document.createElement("div");
+ div2.id = "foo3";
+ frag.appendChild(div2);
+
+ return frag;
+}
+
+function testError() {
+ try {
+ window.foobar("a");
+ } catch (ex) {
+ return ex;
+ }
+ return null;
+}
+
+function testDOMException() {
+ try {
+ var foo = document.querySelector("foo;()bar!");
+ } catch (ex) {
+ return ex;
+ }
+ return null;
+}
+
+function testCSSStyleDeclaration() {
+ document.body.style = 'color: green; font-size: 2em';
+ return document.body.style;
+}
+
+function testStyleSheetList() {
+ var style = document.querySelector("style");
+ if (!style) {
+ style = document.createElement("style");
+ style.textContent = "p, div { color: blue; font-weight: bold }\n" +
+ "@media print { p { background-color: yellow } }";
+ document.head.appendChild(style);
+ }
+ return document.styleSheets;
+}
+ </script>
+</body>
+</html>
diff --git a/devtools/client/webconsole/test/test-console-output-dom-elements.html b/devtools/client/webconsole/test/test-console-output-dom-elements.html
new file mode 100644
index 000000000..5acabfa3f
--- /dev/null
+++ b/devtools/client/webconsole/test/test-console-output-dom-elements.html
@@ -0,0 +1,91 @@
+<!DOCTYPE HTML>
+<html dir="ltr" lang="en-US">
+<head>
+ <meta charset="utf-8">
+ <title>Test the web console output - dom elements</title>
+ <!--
+ - Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/
+ -->
+</head>
+<body class="body-class" id="body-id">
+ <p some-attribute="some-value">hello world!</p>
+ <p id="lots-of-attributes" a b c d e f g h i j k l m n></p>
+ <!--
+ Be sure we have a charset in our iframe's data URI, otherwise we get the following extra
+ console output message:
+ "The character encoding of a framed document was not declared. The document may appear different if viewed without the document framing it."
+ This wouldn't be a big deal, but when we look for a "<p>" in our `waitForMessage` helper,
+ this extra encoding warning line contains the data URI source, returning a message
+ that was unexpected
+ -->
+ <iframe src="data:text/html;charset=US-ASCII,<p>hello from iframe</p>"></iframe>
+ <div class="some classname here with more classnames here"></div>
+ <svg>
+ <clipPath>
+ <rect x="0" y="0" width="10" height="5"></rect>
+ </clipPath>
+ </svg>
+ <script type="text/javascript">
+function testBodyNode() {
+ return document.body;
+}
+
+function testDocumentElement() {
+ return document.documentElement;
+}
+
+function testLotsOfAttributes() {
+ return document.querySelector("#lots-of-attributes");
+}
+
+function testDocument() {
+ return document;
+}
+
+function testNode() {
+ return document.querySelector("p");
+}
+
+function testSvgNode() {
+ return document.querySelector("clipPath");
+}
+
+function testNodeList() {
+ return document.querySelectorAll("body *");
+}
+
+function testNodeInIframe() {
+ return document.querySelector("iframe").contentWindow.document.querySelector("p");
+}
+
+function testDocumentFragment() {
+ var frag = document.createDocumentFragment();
+
+ var span = document.createElement("span");
+ span.className = 'foo';
+ span.dataset.lolz = 'hehe';
+
+ var div = document.createElement('div')
+ div.id = 'fragdiv';
+
+ frag.appendChild(span);
+ frag.appendChild(div);
+
+ return frag;
+}
+
+function testNodeInDocumentFragment() {
+ var frag = testDocumentFragment();
+ return frag.firstChild;
+}
+
+function testUnattachedNode() {
+ var p = document.createElement("p");
+ p.className = "such-class";
+ p.dataset.data = "such-data";
+ return p;
+}
+ </script>
+</body>
+</html>
diff --git a/devtools/client/webconsole/test/test-console-output-events.html b/devtools/client/webconsole/test/test-console-output-events.html
new file mode 100644
index 000000000..908a86fab
--- /dev/null
+++ b/devtools/client/webconsole/test/test-console-output-events.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<html dir="ltr" lang="en-US">
+<head>
+ <meta charset="utf-8">
+ <title>Test the web console output for DOM events</title>
+ <!--
+ - Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/
+ -->
+</head>
+<body>
+ <p>hello world!</p>
+
+ <script type="text/javascript">
+function testDOMEvents() {
+ function eventLogger(ev) {
+ console.log("eventLogger", ev);
+ }
+ document.addEventListener("mousemove", eventLogger);
+ document.addEventListener("keypress", eventLogger);
+
+ synthesizeMouseMove();
+ synthesizeKeyPress("a", {shiftKey: true});
+}
+
+function synthesizeMouseMove(element) {
+ var mouseEvent = document.createEvent("MouseEvent");
+ mouseEvent.initMouseEvent("mousemove", true, true, window, 0, 0, 0, 0, 0,
+ false, false, false, false, 0, null);
+
+ document.dispatchEvent(mouseEvent);
+}
+
+function synthesizeKeyPress(key, options) {
+ var keyboardEvent = document.createEvent("KeyboardEvent");
+ keyboardEvent.initKeyEvent("keypress", true, true, window, false, false,
+ options.shiftKey, false, key.charCodeAt(0), 0);
+ document.dispatchEvent(keyboardEvent);
+}
+ </script>
+</body>
+</html>
diff --git a/devtools/client/webconsole/test/test-console-output-regexp.html b/devtools/client/webconsole/test/test-console-output-regexp.html
new file mode 100644
index 000000000..e62680d90
--- /dev/null
+++ b/devtools/client/webconsole/test/test-console-output-regexp.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html dir="ltr" lang="en-US">
+<head>
+ <meta charset="utf-8">
+ <title>Test the web console output for RegExp</title>
+ <!--
+ - Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/
+ -->
+</head>
+<body>
+ <p>hello world!</p>
+
+ <script type="text/javascript">
+Object.defineProperty(RegExp.prototype, "flags", {
+ get: function() { throw Error("flags called"); }
+})
+Object.defineProperty(RegExp.prototype, "source", {
+ get: function() { throw Error("source called"); },
+})
+ </script>
+</body>
+</html>
diff --git a/devtools/client/webconsole/test/test-console-replaced-api.html b/devtools/client/webconsole/test/test-console-replaced-api.html
new file mode 100644
index 000000000..2b05d023a
--- /dev/null
+++ b/devtools/client/webconsole/test/test-console-replaced-api.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<html dir="ltr" xml:lang="en-US" lang="en-US"><head>
+ <meta charset="utf-8">
+ <title>Console test replaced API</title>
+ </head>
+ <body>
+ <h1 id="header">Web Console Replace API Test</h1>
+ <script type="text/javascript">
+ window.console = {log: function (msg){}, info: function (msg){}, warn: function (msg){}, error: function (msg){}};
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-console-server-logging-array.sjs b/devtools/client/webconsole/test/test-console-server-logging-array.sjs
new file mode 100644
index 000000000..bba394264
--- /dev/null
+++ b/devtools/client/webconsole/test/test-console-server-logging-array.sjs
@@ -0,0 +1,32 @@
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function handleRequest(request, response)
+{
+ var page = "<!DOCTYPE html><html>" +
+ "<head><meta charset='utf-8'></head>" +
+ "<body><p>hello world!</p></body>" +
+ "</html>";
+
+ var data = {
+ "version": "4.1.0",
+ "columns": ["log", "backtrace", "type"],
+ "rows":[[
+ [{ "best": "Firefox", "reckless": "Chrome", "new_ie": "Safari", "new_new_ie": "Edge"}],
+ "C:\\src\\www\\serverlogging\\test7.php:4:1",
+ ""
+ ]],
+ };
+
+ // Put log into headers.
+ var value = b64EncodeUnicode(JSON.stringify(data));
+ response.setHeader("X-ChromeLogger-Data", value, false);
+
+ response.write(page);
+}
+
+function b64EncodeUnicode(str) {
+ return btoa(unescape(encodeURIComponent(str)));
+}
diff --git a/devtools/client/webconsole/test/test-console-server-logging.sjs b/devtools/client/webconsole/test/test-console-server-logging.sjs
new file mode 100644
index 000000000..7177e7185
--- /dev/null
+++ b/devtools/client/webconsole/test/test-console-server-logging.sjs
@@ -0,0 +1,32 @@
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function handleRequest(request, response)
+{
+ var page = "<!DOCTYPE html><html>" +
+ "<head><meta charset='utf-8'></head>" +
+ "<body><p>hello world!</p></body>" +
+ "</html>";
+
+ var data = {
+ "version": "4.1.0",
+ "columns": ["log", "backtrace", "type"],
+ "rows": [[
+ ["values: %s %o %i %f %s","string",{"a":10,"___class_name":"Object"},123,1.12, "\u2713"],
+ "C:\\src\\www\\serverlogging\\test7.php:4:1",
+ ""
+ ]]
+ };
+
+ // Put log into headers.
+ var value = b64EncodeUnicode(JSON.stringify(data));
+ response.setHeader("X-ChromeLogger-Data", value, false);
+
+ response.write(page);
+}
+
+function b64EncodeUnicode(str) {
+ return btoa(unescape(encodeURIComponent(str)));
+}
diff --git a/devtools/client/webconsole/test/test-console-table.html b/devtools/client/webconsole/test/test-console-table.html
new file mode 100644
index 000000000..461dedcde
--- /dev/null
+++ b/devtools/client/webconsole/test/test-console-table.html
@@ -0,0 +1,63 @@
+<!DOCTYPE HTML>
+<html dir="ltr" lang="en">
+ <head>
+ <meta charset="utf8">
+ <!--
+ - Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/
+ -->
+ <title>Test for Bug 899753 - console.table support</title>
+ <script>
+ var languages1 = [
+ { name: "JavaScript", fileExtension: [".js"] },
+ { name: { a: "TypeScript" }, fileExtension: ".ts" },
+ { name: "CoffeeScript", fileExtension: ".coffee" }
+ ];
+
+ var languages2 = {
+ csharp: { name: "C#", paradigm: "object-oriented" },
+ fsharp: { name: "F#", paradigm: "functional" }
+ };
+
+ function Person(firstName, lastName, age)
+ {
+ this.firstName = firstName;
+ this.lastName = lastName;
+ this.age = age;
+ }
+
+ var family = {};
+ family.mother = new Person("Susan", "Doyle", 32);
+ family.father = new Person("John", "Doyle", 33);
+ family.daughter = new Person("Lily", "Doyle", 5);
+ family.son = new Person("Mike", "Doyle", 8);
+
+ var myMap = new Map();
+
+ myMap.set("a string", "value associated with 'a string'");
+ myMap.set(5, "value associated with 5");
+
+ var mySet = new Set();
+
+ mySet.add(1);
+ mySet.add(5);
+ mySet.add("some text");
+ mySet.add(null);
+ mySet.add(undefined);
+
+ // These are globals and so won't be reclaimed by the GC.
+ var bunnies = new String("bunnies");
+ var lizards = new String("lizards");
+
+ var weakmap = new WeakMap();
+ weakmap.set(bunnies, 23);
+ weakmap.set(lizards, "oh no");
+
+ var weakset = new WeakSet([bunnies, lizards]);
+
+ </script>
+ </head>
+ <body>
+ <p>Hello world!</p>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-console-trace-async.html b/devtools/client/webconsole/test/test-console-trace-async.html
new file mode 100644
index 000000000..c7b895455
--- /dev/null
+++ b/devtools/client/webconsole/test/test-console-trace-async.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head><meta charset="utf-8">
+ <title>Web Console test for bug 1200832 - console.trace() async stacks</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<script type="application/javascript">
+function inner() {
+ console.trace();
+}
+
+function time1() {
+ new Promise(function(resolve, reject) {
+ setTimeout(resolve, 10);
+ }).then(inner);
+}
+
+setTimeout(time1, 10);
+</script>
+ </head>
+ <body>
+ <p>Web Console test for bug 1200832 - console.trace() async stacks</p>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-console-workers.html b/devtools/client/webconsole/test/test-console-workers.html
new file mode 100644
index 000000000..f4b286ae5
--- /dev/null
+++ b/devtools/client/webconsole/test/test-console-workers.html
@@ -0,0 +1,13 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE HTML>
+<html dir="ltr" xml:lang="en-US" lang="en-US"><head>
+ <meta charset="utf-8">
+ <title>Console test</title>
+ </head>
+ <body>
+ <script type="text/javascript">
+var sw = new SharedWorker('data:application/javascript,console.log("foo-bar-shared-worker");');
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-console.html b/devtools/client/webconsole/test/test-console.html
new file mode 100644
index 000000000..b294a3ba1
--- /dev/null
+++ b/devtools/client/webconsole/test/test-console.html
@@ -0,0 +1,34 @@
+<!DOCTYPE HTML>
+<html dir="ltr" xml:lang="en-US" lang="en-US"><head>
+ <meta charset="utf-8">
+ <title>Console test</title>
+ <script type="text/javascript">
+ var fooObj = {
+ testProp: "testValue"
+ };
+
+ function test() {
+ var str = "Dolske Digs Bacon, Now and Forevermore."
+ for (var i=0; i < 5; i++) {
+ console.log(str);
+ }
+ }
+
+ function testTrace() {
+ console.log("bug 1100562");
+ console.trace();
+ }
+
+ console.info("INLINE SCRIPT:");
+ test();
+ console.warn("I'm warning you, he will eat up all yr bacon.");
+ console.error("Error Message");
+ </script>
+ </head>
+ <body>
+ <h1 id="header">Heads Up Display Demo</h1>
+ <button onclick="test();">Log stuff about Dolske</button>
+ <button id="testTrace" onclick="testTrace();">Log stuff with stacktrace</button>
+ <div id="myDiv"></div>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-consoleiframes.html b/devtools/client/webconsole/test/test-consoleiframes.html
new file mode 100644
index 000000000..a8176f93a
--- /dev/null
+++ b/devtools/client/webconsole/test/test-consoleiframes.html
@@ -0,0 +1,13 @@
+<html>
+<head>
+ <script>
+ console.log("main file");
+ </script>
+</head>
+<body>
+<h1>iframe console test</h1>
+<iframe src="test-iframe1.html"></iframe>
+<iframe src="test-iframe2.html"></iframe>
+<iframe src="test-iframe3.html"></iframe>
+</body>
+</html> \ No newline at end of file
diff --git a/devtools/client/webconsole/test/test-cu-reporterror.js b/devtools/client/webconsole/test/test-cu-reporterror.js
new file mode 100644
index 000000000..6e2f9d262
--- /dev/null
+++ b/devtools/client/webconsole/test/test-cu-reporterror.js
@@ -0,0 +1,4 @@
+function a() {
+ Components.utils.reportError("bug1141222");
+}
+a();
diff --git a/devtools/client/webconsole/test/test-data.json b/devtools/client/webconsole/test/test-data.json
new file mode 100644
index 000000000..471d240b5
--- /dev/null
+++ b/devtools/client/webconsole/test/test-data.json
@@ -0,0 +1 @@
+{ id: "test JSON data", myArray: [ "foo", "bar", "baz", "biff" ] } \ No newline at end of file
diff --git a/devtools/client/webconsole/test/test-data.json^headers^ b/devtools/client/webconsole/test/test-data.json^headers^
new file mode 100644
index 000000000..7b5e82d4b
--- /dev/null
+++ b/devtools/client/webconsole/test/test-data.json^headers^
@@ -0,0 +1 @@
+Content-Type: application/json
diff --git a/devtools/client/webconsole/test/test-duplicate-error.html b/devtools/client/webconsole/test/test-duplicate-error.html
new file mode 100644
index 000000000..1b2691672
--- /dev/null
+++ b/devtools/client/webconsole/test/test-duplicate-error.html
@@ -0,0 +1,21 @@
+<!DOCTYPE HTML>
+<html dir="ltr" xml:lang="en-US" lang="en-US">
+ <head>
+ <meta charset="utf-8">
+ <title>Console duplicate error test</title>
+ <!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+
+ See https://bugzilla.mozilla.org/show_bug.cgi?id=582201
+ -->
+ </head>
+ <body>
+ <h1>Heads Up Display - duplicate error test</h1>
+
+ <script type="text/javascript"><!--
+ fooDuplicateError1.bar();
+ // --></script>
+ </body>
+</html>
+
diff --git a/devtools/client/webconsole/test/test-encoding-ISO-8859-1.html b/devtools/client/webconsole/test/test-encoding-ISO-8859-1.html
new file mode 100644
index 000000000..cf19629f4
--- /dev/null
+++ b/devtools/client/webconsole/test/test-encoding-ISO-8859-1.html
@@ -0,0 +1,7 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="ISO-8859-1">
+</head>
+<body>üöä</body>
+</html> \ No newline at end of file
diff --git a/devtools/client/webconsole/test/test-error.html b/devtools/client/webconsole/test/test-error.html
new file mode 100644
index 000000000..abf62a3f1
--- /dev/null
+++ b/devtools/client/webconsole/test/test-error.html
@@ -0,0 +1,21 @@
+<!DOCTYPE HTML>
+<html dir="ltr" xml:lang="en-US" lang="en-US">
+ <head>
+ <meta charset="utf-8">
+ <title>Console error test</title>
+ </head>
+ <body>
+ <h1>Heads Up Display - error test</h1>
+ <p><button>generate error</button></p>
+
+ <script type="text/javascript"><!--
+ var button = document.getElementsByTagName("button")[0];
+
+ button.addEventListener("click", function clicker () {
+ button.removeEventListener("click", clicker, false);
+ fooBazBaz.bar();
+ }, false);
+ // --></script>
+ </body>
+</html>
+
diff --git a/devtools/client/webconsole/test/test-eval-in-stackframe.html b/devtools/client/webconsole/test/test-eval-in-stackframe.html
new file mode 100644
index 000000000..ec1bf3f30
--- /dev/null
+++ b/devtools/client/webconsole/test/test-eval-in-stackframe.html
@@ -0,0 +1,39 @@
+<!DOCTYPE HTML>
+<html dir="ltr" lang="en">
+ <head>
+ <meta charset="utf8">
+ <!--
+ - Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/
+ -->
+ <title>Test for bug 783499 - use the debugger API in the web console</title>
+ <script>
+ var foo = "globalFooBug783499";
+ var fooObj = {
+ testProp: "testValue",
+ };
+
+ function firstCall()
+ {
+ var foo = "fooFirstCall";
+ var foo3 = "foo3FirstCall";
+ secondCall();
+ }
+
+ function secondCall()
+ {
+ var foo2 = "foo2SecondCall";
+ var fooObj = {
+ testProp2: "testValue2",
+ };
+ var fooObj2 = {
+ testProp22: "testValue22",
+ };
+ debugger;
+ }
+ </script>
+ </head>
+ <body>
+ <p>Hello world!</p>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-exception-stackframe.html b/devtools/client/webconsole/test/test-exception-stackframe.html
new file mode 100644
index 000000000..0a6dea4ca
--- /dev/null
+++ b/devtools/client/webconsole/test/test-exception-stackframe.html
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<html dir="ltr" lang="en">
+ <head>
+ <meta charset="utf8">
+ <!--
+ - Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/
+ -->
+ <title>Test for bug 1184172 - stacktraces for exceptions</title>
+ <script>
+ function firstCall() {
+ secondCall();
+ }
+
+ // Check anonymous functions
+ var secondCall = function () {
+ thirdCall();
+ }
+
+ function thirdCall() {
+ nonExistingMethodCall();
+ }
+
+ function domAPI() {
+ document.querySelector("buggy;selector");
+ }
+
+ function domException() {
+ throw new DOMException("DOMException");
+ }
+ window.addEventListener("load", firstCall);
+ window.addEventListener("load", function onLoadDomAPI() {
+ domAPI();
+ });
+ window.addEventListener("load", function onLoadDomException() {
+ domException();
+ });
+ </script>
+ </head>
+ <body>
+ <p>Hello world!</p>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-file-location.js b/devtools/client/webconsole/test/test-file-location.js
new file mode 100644
index 000000000..d9879a356
--- /dev/null
+++ b/devtools/client/webconsole/test/test-file-location.js
@@ -0,0 +1,12 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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";
+
+console.log("message for level log");
+console.info("message for level info");
+console.warn("message for level warn");
+console.error("message for level error");
+console.debug("message for level debug");
diff --git a/devtools/client/webconsole/test/test-filter.html b/devtools/client/webconsole/test/test-filter.html
new file mode 100644
index 000000000..219177bb2
--- /dev/null
+++ b/devtools/client/webconsole/test/test-filter.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+<html dir="ltr" xml:lang="en-US" lang="en-US"><head>
+ <meta charset="utf-8">
+ <title>Console test</title>
+ <script type="text/javascript">
+ </script>
+ </head>
+ <body>
+ <h1>Heads Up Display Filter Test Page</h1>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-for-of.html b/devtools/client/webconsole/test/test-for-of.html
new file mode 100644
index 000000000..876010c9e
--- /dev/null
+++ b/devtools/client/webconsole/test/test-for-of.html
@@ -0,0 +1,8 @@
+<!DOCTYPE HTML>
+<html>
+<meta charset="utf-8">
+<body>
+<h1>a</h1>
+<div><p>b</p></div>
+<h2>c</h2>
+<p>d</p>
diff --git a/devtools/client/webconsole/test/test-iframe-762593-insecure-form-action.html b/devtools/client/webconsole/test/test-iframe-762593-insecure-form-action.html
new file mode 100644
index 000000000..d14b5cdd7
--- /dev/null
+++ b/devtools/client/webconsole/test/test-iframe-762593-insecure-form-action.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<html>
+ <head>
+ <meta http-equiv="Content-type" content="text/html;charset=UTF-8">
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ </head>
+ <body>
+ <h1>iframe 2</h1>
+ <p>This frame contains a password field inside a form with insecure action.</p>
+ <form action="http://test">
+ <input type="password" name="pwd">
+ </form>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-iframe-762593-insecure-frame.html b/devtools/client/webconsole/test/test-iframe-762593-insecure-frame.html
new file mode 100644
index 000000000..dde47a78e
--- /dev/null
+++ b/devtools/client/webconsole/test/test-iframe-762593-insecure-frame.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<html>
+ <head>
+ <meta http-equiv="Content-type" content="text/html;charset=UTF-8">
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ </head>
+ <body>
+ <h1>iframe 1</h1>
+ <p>This frame is served with an insecure password field.</p>
+ <iframe src=
+ "http://example.com/browser/devtools/client/webconsole/test/test-iframe-762593-insecure-form-action.html">
+ </iframe>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-iframe1.html b/devtools/client/webconsole/test/test-iframe1.html
new file mode 100644
index 000000000..4dd4eddfe
--- /dev/null
+++ b/devtools/client/webconsole/test/test-iframe1.html
@@ -0,0 +1,10 @@
+<html>
+<head>
+ <script>
+ console.log("iframe 1");
+ </script>
+</head>
+<body>
+<h1>iframe 1</h1>
+</body>
+</html> \ No newline at end of file
diff --git a/devtools/client/webconsole/test/test-iframe2.html b/devtools/client/webconsole/test/test-iframe2.html
new file mode 100644
index 000000000..c15884795
--- /dev/null
+++ b/devtools/client/webconsole/test/test-iframe2.html
@@ -0,0 +1,11 @@
+<html>
+<head>
+ <script>
+ console.log("iframe 2");
+ blah;
+ </script>
+</head>
+<body>
+<h1>iframe 2</h1>
+</body>
+</html> \ No newline at end of file
diff --git a/devtools/client/webconsole/test/test-iframe3.html b/devtools/client/webconsole/test/test-iframe3.html
new file mode 100644
index 000000000..f0df8b669
--- /dev/null
+++ b/devtools/client/webconsole/test/test-iframe3.html
@@ -0,0 +1,11 @@
+<html>
+<head>
+ <script>
+ console.log("iframe 3");
+ </script>
+</head>
+<body>
+<h1>iframe 3</h1>
+<iframe src="test-iframe1.html"></iframe>
+</body>
+</html> \ No newline at end of file
diff --git a/devtools/client/webconsole/test/test-image.png b/devtools/client/webconsole/test/test-image.png
new file mode 100644
index 000000000..769c63634
--- /dev/null
+++ b/devtools/client/webconsole/test/test-image.png
Binary files differ
diff --git a/devtools/client/webconsole/test/test-mixedcontent-securityerrors.html b/devtools/client/webconsole/test/test-mixedcontent-securityerrors.html
new file mode 100644
index 000000000..cb8cfdaaf
--- /dev/null
+++ b/devtools/client/webconsole/test/test-mixedcontent-securityerrors.html
@@ -0,0 +1,21 @@
+<!--
+ Bug 875456 - Log mixed content messages from the Mixed Content Blocker to the
+ Security Pane in the Web Console
+-->
+
+<!DOCTYPE HTML>
+<html dir="ltr" xml:lang="en-US" lang="en-US">
+ <head>
+ <meta charset="utf8">
+ <title>Mixed Content test - http on https</title>
+ <script src="testscript.js"></script>
+ <!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+ -->
+ </head>
+ <body>
+ <iframe src="http://example.com"></iframe>
+ <img src="http://example.com/tests/image/test/mochitest/blue.png"></img>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-mutation.html b/devtools/client/webconsole/test/test-mutation.html
new file mode 100644
index 000000000..e80933b06
--- /dev/null
+++ b/devtools/client/webconsole/test/test-mutation.html
@@ -0,0 +1,16 @@
+<!DOCTYPE HTML>
+<html dir="ltr" xml:lang="en-US" lang="en-US">
+ <head>
+ <meta charset="utf-8">
+ <title>Console mutation test</title>
+ <script>
+ window.onload = function (){
+ var node = document.createElement("div");
+ document.body.appendChild(node);
+ };
+ </script>
+ </head>
+ <body>
+ <h1>Heads Up Display DOM Mutation Test Page</h1>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-network-request.html b/devtools/client/webconsole/test/test-network-request.html
new file mode 100644
index 000000000..7cb736296
--- /dev/null
+++ b/devtools/client/webconsole/test/test-network-request.html
@@ -0,0 +1,40 @@
+<!DOCTYPE HTML>
+<html dir="ltr" xml:lang="en-US" lang="en-US">
+ <head>
+ <meta charset="utf-8">
+ <title>Console HTTP test page</title>
+ <script type="text/javascript"><!--
+ function makeXhr(aMethod, aUrl, aRequestBody, aCallback) {
+ var xmlhttp = new XMLHttpRequest();
+ xmlhttp.open(aMethod, aUrl, true);
+ xmlhttp.onreadystatechange = function() {
+ if (aCallback && xmlhttp.readyState == 4) {
+ aCallback();
+ }
+ };
+ xmlhttp.send(aRequestBody);
+ }
+
+ function testXhrGet(aCallback) {
+ makeXhr('get', 'test-data.json', null, aCallback);
+ }
+
+ function testXhrWarn(aCallback) {
+ makeXhr('get', 'http://example.com/browser/devtools/client/netmonitor/test/sjs_cors-test-server.sjs', null, aCallback);
+ }
+
+ function testXhrPost(aCallback) {
+ makeXhr('post', 'test-data.json', "Hello world!", aCallback);
+ }
+ // --></script>
+ </head>
+ <body>
+ <h1>Heads Up Display HTTP Logging Testpage</h1>
+ <h2>This page is used to test the HTTP logging.</h2>
+
+ <form action="https://example.com/browser/devtools/client/webconsole/test/test-network-request.html" method="post">
+ <input name="name" type="text" value="foo bar"><br>
+ <input name="age" type="text" value="144"><br>
+ </form>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-network.html b/devtools/client/webconsole/test/test-network.html
new file mode 100644
index 000000000..69d3422e3
--- /dev/null
+++ b/devtools/client/webconsole/test/test-network.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+<html dir="ltr" xml:lang="en-US" lang="en-US"><head>
+ <meta charset="utf-8">
+ <title>Console network test</title>
+ <script src="testscript.js?foo"></script>
+ </head>
+ <body>
+ <h1>Heads Up Display Network Test Page</h1>
+ <img src="test-image.png"></img>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-observe-http-ajax.html b/devtools/client/webconsole/test/test-observe-http-ajax.html
new file mode 100644
index 000000000..5abcefdad
--- /dev/null
+++ b/devtools/client/webconsole/test/test-observe-http-ajax.html
@@ -0,0 +1,17 @@
+<!DOCTYPE HTML>
+<html dir="ltr" xml:lang="en-US" lang="en-US"><head>
+ <meta charset="utf-8">
+ <title>Console HTTP test page</title>
+ <script type="text/javascript">
+ function test() {
+ var xmlhttp = new XMLHttpRequest();
+ xmlhttp.open('get', 'test-data.json', false);
+ xmlhttp.send(null);
+ }
+ </script>
+ </head>
+ <body onload="test();">
+ <h1>Heads Up Display HTTP & AJAX Test Page</h1>
+ <h2>This page fires an ajax request so we can see the http logging of the console</h2>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-own-console.html b/devtools/client/webconsole/test/test-own-console.html
new file mode 100644
index 000000000..d1d18ebc2
--- /dev/null
+++ b/devtools/client/webconsole/test/test-own-console.html
@@ -0,0 +1,24 @@
+<!DOCTYPE HTML>
+<html dir="ltr" xml:lang="en-US" lang="en-US">
+<head>
+<meta charset="utf-8">
+<script>
+ var _console = {
+ foo: "bar"
+ }
+
+ window.console = _console;
+
+ function loadIFrame() {
+ var iframe = document.body.querySelector("iframe");
+ iframe.addEventListener("load", function() {
+ iframe.removeEventListener("load", arguments.callee, true);
+ }, true);
+
+ iframe.setAttribute("src", "test-console.html");
+ }
+</script>
+</head>
+<body>
+ <iframe></iframe>
+</body>
diff --git a/devtools/client/webconsole/test/test-property-provider.html b/devtools/client/webconsole/test/test-property-provider.html
new file mode 100644
index 000000000..532b00f44
--- /dev/null
+++ b/devtools/client/webconsole/test/test-property-provider.html
@@ -0,0 +1,14 @@
+<!DOCTYPE HTML>
+<html dir="ltr" xml:lang="en-US" lang="en-US"><head>
+ <meta charset="utf-8">
+ <title>Property provider test</title>
+ <script>
+ var testObj = {
+ testProp: 'testValue'
+ };
+ </script>
+ </head>
+ <body>
+ <h1>Heads Up Property Provider Test Page</h1>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-repeated-messages.html b/devtools/client/webconsole/test/test-repeated-messages.html
new file mode 100644
index 000000000..b19c9485e
--- /dev/null
+++ b/devtools/client/webconsole/test/test-repeated-messages.html
@@ -0,0 +1,53 @@
+<!DOCTYPE HTML>
+<html dir="ltr" xml:lang="en-US" lang="en-US">
+ <head>
+ <meta charset="utf8">
+ <title>Test for bugs 720180, 800510, 865288 and 1218089</title>
+ <script>
+ function testConsole() {
+ // same line and column number
+ for(var i = 0; i < 2; i++) {
+ console.log("foo repeat");
+ }
+ console.log("foo repeat");
+ console.error("foo repeat");
+ }
+ function testConsoleObjects() {
+ for (var i = 0; i < 3; i++) {
+ var o = { id: "abba" + i };
+ console.log("abba", o);
+ }
+ }
+ function testConsoleFalsyValues(){
+ [NaN, undefined, null].forEach(function(item, index){
+ console.log(item);
+ });
+ [NaN, NaN].forEach(function(item, index){
+ console.log(item);
+ });
+ [undefined, undefined].forEach(function(item, index){
+ console.log(item);
+ });
+ [null, null].forEach(function(item, index){
+ console.log(item);
+ });
+ }
+ </script>
+ <style>
+ body {
+ background-image: foobarz;
+ }
+ p {
+ background-image: foobarz;
+ }
+ </style>
+ <!--
+ - Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/
+ -->
+ </head>
+ <body>
+ <p>Hello world!</p>
+ </body>
+</html>
+
diff --git a/devtools/client/webconsole/test/test-result-format-as-string.html b/devtools/client/webconsole/test/test-result-format-as-string.html
new file mode 100644
index 000000000..c3ab78ee7
--- /dev/null
+++ b/devtools/client/webconsole/test/test-result-format-as-string.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>Web Console test: jsterm eval format as a string</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ </head>
+ <body>
+ <p>Make sure js eval results are formatted as strings.</p>
+ <script>
+ document.querySelector("p").toSource = function() {
+ var element = document.createElement("div");
+ element.id = "foobar";
+ element.textContent = "bug772506_content";
+ element.setAttribute("onmousemove",
+ "(function () {" +
+ " gBrowser._bug772506 = 'foobar';" +
+ "})();"
+ );
+ return element;
+ };
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-trackingprotection-securityerrors.html b/devtools/client/webconsole/test/test-trackingprotection-securityerrors.html
new file mode 100644
index 000000000..17f0e459e
--- /dev/null
+++ b/devtools/client/webconsole/test/test-trackingprotection-securityerrors.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<!-- 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/. -->
+<html dir="ltr" xml:lang="en-US" lang="en-US">
+ <head>
+ <meta charset="utf8">
+ </head>
+ <body>
+ <iframe src="http://tracking.example.com/"></iframe>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-webconsole-error-observer.html b/devtools/client/webconsole/test/test-webconsole-error-observer.html
new file mode 100644
index 000000000..8466bc6f2
--- /dev/null
+++ b/devtools/client/webconsole/test/test-webconsole-error-observer.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+<html dir="ltr" xml:lang="en-US" lang="en-US">
+ <head>
+ <meta charset="utf-8">
+ <title>WebConsoleErrorObserver test - bug 611032</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <script type="text/javascript">
+ console.log("log Bazzle");
+ console.info("info Bazzle");
+ console.warn("warn Bazzle");
+ console.error("error Bazzle");
+
+ var foo = {};
+ foo.bazBug611032();
+ </script>
+ <style type="text/css">
+ .foo { color: cssColorBug611032; }
+ </style>
+ </head>
+ <body>
+ <h1>WebConsoleErrorObserver test</h1>
+ </body>
+</html>
+
diff --git a/devtools/client/webconsole/test/test_bug1045902_console_csp_ignore_reflected_xss_message.html b/devtools/client/webconsole/test/test_bug1045902_console_csp_ignore_reflected_xss_message.html
new file mode 100644
index 000000000..bf63601bf
--- /dev/null
+++ b/devtools/client/webconsole/test/test_bug1045902_console_csp_ignore_reflected_xss_message.html
@@ -0,0 +1,10 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>Bug 1045902 - CSP: Log console message for ‘reflected-xss’</title>
+</head>
+<body>
+Bug 1045902 - CSP: Log console message for ‘reflected-xss’
+</body>
+</html>
diff --git a/devtools/client/webconsole/test/test_bug1045902_console_csp_ignore_reflected_xss_message.html^headers^ b/devtools/client/webconsole/test/test_bug1045902_console_csp_ignore_reflected_xss_message.html^headers^
new file mode 100644
index 000000000..0b234f0e8
--- /dev/null
+++ b/devtools/client/webconsole/test/test_bug1045902_console_csp_ignore_reflected_xss_message.html^headers^
@@ -0,0 +1 @@
+Content-Security-Policy: reflected-xss filter;
diff --git a/devtools/client/webconsole/test/test_bug1092055_shouldwarn.html b/devtools/client/webconsole/test/test_bug1092055_shouldwarn.html
new file mode 100644
index 000000000..ebb7773cb
--- /dev/null
+++ b/devtools/client/webconsole/test/test_bug1092055_shouldwarn.html
@@ -0,0 +1,15 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>Bug 1092055 - Log console messages for non-top-level security errors</title>
+ <script src="test_bug1092055_shouldwarn.js"></script>
+ <!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+ -->
+</head>
+<body>
+Bug 1092055 - Log console messages for non-top-level security errors
+</body>
+</html>
diff --git a/devtools/client/webconsole/test/test_bug1092055_shouldwarn.js b/devtools/client/webconsole/test/test_bug1092055_shouldwarn.js
new file mode 100644
index 000000000..c7d5cec14
--- /dev/null
+++ b/devtools/client/webconsole/test/test_bug1092055_shouldwarn.js
@@ -0,0 +1,2 @@
+// It doesn't matter what this script does, but the broken HSTS header sent
+// with it should result in warnings in the webconsole
diff --git a/devtools/client/webconsole/test/test_bug1092055_shouldwarn.js^headers^ b/devtools/client/webconsole/test/test_bug1092055_shouldwarn.js^headers^
new file mode 100644
index 000000000..f99377fc6
--- /dev/null
+++ b/devtools/client/webconsole/test/test_bug1092055_shouldwarn.js^headers^
@@ -0,0 +1 @@
+Strict-Transport-Security: some complete nonsense
diff --git a/devtools/client/webconsole/test/test_bug_1010953_cspro.html b/devtools/client/webconsole/test/test_bug_1010953_cspro.html
new file mode 100644
index 000000000..83ac6391f
--- /dev/null
+++ b/devtools/client/webconsole/test/test_bug_1010953_cspro.html
@@ -0,0 +1,20 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>Test for Bug 1010953 - Verify that CSP and CSPRO log different console
+messages.</title>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1010953">Mozilla Bug 1010953</a>
+
+
+<!-- this script file allowed by the CSP header (but not by the report-only header) -->
+<script src="http://some.example.com/test_bug_1010953_cspro.js"></script>
+
+<!-- this image allowed only be the CSP report-only header. -->
+<img src="http://some.example.com/test.png">
+</body>
+</html> \ No newline at end of file
diff --git a/devtools/client/webconsole/test/test_bug_1010953_cspro.html^headers^ b/devtools/client/webconsole/test/test_bug_1010953_cspro.html^headers^
new file mode 100644
index 000000000..03056e2cb
--- /dev/null
+++ b/devtools/client/webconsole/test/test_bug_1010953_cspro.html^headers^
@@ -0,0 +1,2 @@
+Content-Security-Policy: default-src 'self'; img-src 'self'; script-src some.example.com;
+Content-Security-Policy-Report-Only: default-src 'self'; img-src some.example.com; script-src 'self'; report-uri https://example.com/ignored/; \ No newline at end of file
diff --git a/devtools/client/webconsole/test/test_bug_1247459_violation.html b/devtools/client/webconsole/test/test_bug_1247459_violation.html
new file mode 100644
index 000000000..fdda4eb26
--- /dev/null
+++ b/devtools/client/webconsole/test/test_bug_1247459_violation.html
@@ -0,0 +1,15 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta http-equiv="Content-Security-Policy" content="img-src https://example.com"></meta>
+ <meta http-equiv="Content-Security-Policy" content="img-src https://example.com"></meta>
+ <meta charset="UTF-8">
+ <title>Test for Bug 1247459 - policy violations for header and META are displayed separately</title>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1247459">Mozilla Bug 1247459</a>
+<img src="http://some.example.com/test.png">
+</body>
+</html>
diff --git a/devtools/client/webconsole/test/test_bug_770099_violation.html b/devtools/client/webconsole/test/test_bug_770099_violation.html
new file mode 100644
index 000000000..ccbded87a
--- /dev/null
+++ b/devtools/client/webconsole/test/test_bug_770099_violation.html
@@ -0,0 +1,13 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>Test for Bug 770099 - policy violation</title>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=770099">Mozilla Bug 770099</a>
+<img src="http://some.example.com/test.png">
+</body>
+</html>
diff --git a/devtools/client/webconsole/test/test_bug_770099_violation.html^headers^ b/devtools/client/webconsole/test/test_bug_770099_violation.html^headers^
new file mode 100644
index 000000000..4c6fa3c26
--- /dev/null
+++ b/devtools/client/webconsole/test/test_bug_770099_violation.html^headers^
@@ -0,0 +1 @@
+Content-Security-Policy: default-src 'self'
diff --git a/devtools/client/webconsole/test/test_hpkp-invalid-headers.sjs b/devtools/client/webconsole/test/test_hpkp-invalid-headers.sjs
new file mode 100644
index 000000000..cd0e18523
--- /dev/null
+++ b/devtools/client/webconsole/test/test_hpkp-invalid-headers.sjs
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function handleRequest(request, response)
+{
+ response.setHeader("Content-Type", "text/plain; charset=utf-8", false);
+
+ let issue;
+ switch (request.queryString) {
+ case "badSyntax":
+ response.setHeader("Public-Key-Pins", "\"");
+ issue = "is not syntactically correct.";
+ break;
+ case "noMaxAge":
+ response.setHeader("Public-Key-Pins", "max-age444");
+ issue = "does not include a max-age directive.";
+ break;
+ case "invalidIncludeSubDomains":
+ response.setHeader("Public-Key-Pins", "includeSubDomains=abc");
+ issue = "includes an invalid includeSubDomains directive.";
+ break;
+ case "invalidMaxAge":
+ response.setHeader("Public-Key-Pins", "max-age=abc");
+ issue = "includes an invalid max-age directive.";
+ break;
+ case "multipleIncludeSubDomains":
+ response.setHeader("Public-Key-Pins",
+ "includeSubDomains; includeSubDomains");
+ issue = "includes multiple includeSubDomains directives.";
+ break;
+ case "multipleMaxAge":
+ response.setHeader("Public-Key-Pins",
+ "max-age=444; max-age=999");
+ issue = "includes multiple max-age directives.";
+ break;
+ case "multipleReportURIs":
+ response.setHeader("Public-Key-Pins",
+ 'report-uri="http://example.com"; ' +
+ 'report-uri="http://example.com"');
+ issue = "includes multiple report-uri directives.";
+ break;
+ case "pinsetDoesNotMatch":
+ response.setHeader(
+ "Public-Key-Pins",
+ 'max-age=999; ' +
+ 'pin-sha256="AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; ' +
+ 'pin-sha256="BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB="');
+ issue = "does not include a matching pin.";
+ break;
+ }
+
+ response.write("This page is served with a PKP header that " + issue);
+}
diff --git a/devtools/client/webconsole/test/test_hsts-invalid-headers.sjs b/devtools/client/webconsole/test/test_hsts-invalid-headers.sjs
new file mode 100644
index 000000000..9e3ea7624
--- /dev/null
+++ b/devtools/client/webconsole/test/test_hsts-invalid-headers.sjs
@@ -0,0 +1,39 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function handleRequest(request, response)
+{
+ response.setHeader("Content-Type", "text/plain; charset=utf-8", false);
+
+ let issue;
+ switch (request.queryString) {
+ case "badSyntax":
+ response.setHeader("Strict-Transport-Security", "\"");
+ issue = "is not syntactically correct.";
+ break;
+ case "noMaxAge":
+ response.setHeader("Strict-Transport-Security", "max-age444");
+ issue = "does not include a max-age directive.";
+ break;
+ case "invalidIncludeSubDomains":
+ response.setHeader("Strict-Transport-Security", "includeSubDomains=abc");
+ issue = "includes an invalid includeSubDomains directive.";
+ break;
+ case "invalidMaxAge":
+ response.setHeader("Strict-Transport-Security", "max-age=abc");
+ issue = "includes an invalid max-age directive.";
+ break;
+ case "multipleIncludeSubDomains":
+ response.setHeader("Strict-Transport-Security",
+ "includeSubDomains; includeSubDomains");
+ issue = "includes multiple includeSubDomains directives.";
+ break;
+ case "multipleMaxAge":
+ response.setHeader("Strict-Transport-Security",
+ "max-age=444; max-age=999");
+ issue = "includes multiple max-age directives.";
+ break;
+ }
+
+ response.write("This page is served with a STS header that " + issue);
+}
diff --git a/devtools/client/webconsole/test/testscript.js b/devtools/client/webconsole/test/testscript.js
new file mode 100644
index 000000000..849b03d86
--- /dev/null
+++ b/devtools/client/webconsole/test/testscript.js
@@ -0,0 +1,2 @@
+"use strict";
+console.log("running network console logging tests");
diff --git a/devtools/client/webconsole/utils.js b/devtools/client/webconsole/utils.js
new file mode 100644
index 000000000..ae2f1809f
--- /dev/null
+++ b/devtools/client/webconsole/utils.js
@@ -0,0 +1,395 @@
+/* -*- 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 {Cc, Ci, Cu, components} = require("chrome");
+const Services = require("Services");
+const {LocalizationHelper} = require("devtools/shared/l10n");
+
+// Match the function name from the result of toString() or toSource().
+//
+// Examples:
+// (function foobar(a, b) { ...
+// function foobar2(a) { ...
+// function() { ...
+const REGEX_MATCH_FUNCTION_NAME = /^\(?function\s+([^(\s]+)\s*\(/;
+
+// Number of terminal entries for the self-xss prevention to go away
+const CONSOLE_ENTRY_THRESHOLD = 5;
+
+const CONSOLE_WORKER_IDS = exports.CONSOLE_WORKER_IDS = [
+ "SharedWorker",
+ "ServiceWorker",
+ "Worker"
+];
+
+var WebConsoleUtils = {
+
+ /**
+ * Wrap a string in an nsISupportsString object.
+ *
+ * @param string string
+ * @return nsISupportsString
+ */
+ supportsString: function (string) {
+ let str = Cc["@mozilla.org/supports-string;1"]
+ .createInstance(Ci.nsISupportsString);
+ str.data = string;
+ return str;
+ },
+
+ /**
+ * Clone an object.
+ *
+ * @param object object
+ * The object you want cloned.
+ * @param boolean recursive
+ * Tells if you want to dig deeper into the object, to clone
+ * recursively.
+ * @param function [filter]
+ * Optional, filter function, called for every property. Three
+ * arguments are passed: key, value and object. Return true if the
+ * property should be added to the cloned object. Return false to skip
+ * the property.
+ * @return object
+ * The cloned object.
+ */
+ cloneObject: function (object, recursive, filter) {
+ if (typeof object != "object") {
+ return object;
+ }
+
+ let temp;
+
+ if (Array.isArray(object)) {
+ temp = [];
+ Array.forEach(object, function (value, index) {
+ if (!filter || filter(index, value, object)) {
+ temp.push(recursive ? WebConsoleUtils.cloneObject(value) : value);
+ }
+ });
+ } else {
+ temp = {};
+ for (let key in object) {
+ let value = object[key];
+ if (object.hasOwnProperty(key) &&
+ (!filter || filter(key, value, object))) {
+ temp[key] = recursive ? WebConsoleUtils.cloneObject(value) : value;
+ }
+ }
+ }
+
+ return temp;
+ },
+
+ /**
+ * Copies certain style attributes from one element to another.
+ *
+ * @param nsIDOMNode from
+ * The target node.
+ * @param nsIDOMNode to
+ * The destination node.
+ */
+ copyTextStyles: function (from, to) {
+ let win = from.ownerDocument.defaultView;
+ let style = win.getComputedStyle(from);
+ to.style.fontFamily = style.getPropertyCSSValue("font-family").cssText;
+ to.style.fontSize = style.getPropertyCSSValue("font-size").cssText;
+ to.style.fontWeight = style.getPropertyCSSValue("font-weight").cssText;
+ to.style.fontStyle = style.getPropertyCSSValue("font-style").cssText;
+ },
+
+ /**
+ * Create a grip for the given value. If the value is an object,
+ * an object wrapper will be created.
+ *
+ * @param mixed value
+ * The value you want to create a grip for, before sending it to the
+ * client.
+ * @param function objectWrapper
+ * If the value is an object then the objectWrapper function is
+ * invoked to give us an object grip. See this.getObjectGrip().
+ * @return mixed
+ * The value grip.
+ */
+ createValueGrip: function (value, objectWrapper) {
+ switch (typeof value) {
+ case "boolean":
+ return value;
+ case "string":
+ return objectWrapper(value);
+ case "number":
+ if (value === Infinity) {
+ return { type: "Infinity" };
+ } else if (value === -Infinity) {
+ return { type: "-Infinity" };
+ } else if (Number.isNaN(value)) {
+ return { type: "NaN" };
+ } else if (!value && 1 / value === -Infinity) {
+ return { type: "-0" };
+ }
+ return value;
+ case "undefined":
+ return { type: "undefined" };
+ case "object":
+ if (value === null) {
+ return { type: "null" };
+ }
+ // Fall through.
+ case "function":
+ return objectWrapper(value);
+ default:
+ console.error("Failed to provide a grip for value of " + typeof value
+ + ": " + value);
+ return null;
+ }
+ },
+
+ /**
+ * Determine if the given request mixes HTTP with HTTPS content.
+ *
+ * @param string request
+ * Location of the requested content.
+ * @param string location
+ * Location of the current page.
+ * @return boolean
+ * True if the content is mixed, false if not.
+ */
+ isMixedHTTPSRequest: function (request, location) {
+ try {
+ let requestURI = Services.io.newURI(request, null, null);
+ let contentURI = Services.io.newURI(location, null, null);
+ return (contentURI.scheme == "https" && requestURI.scheme != "https");
+ } catch (ex) {
+ return false;
+ }
+ },
+
+ /**
+ * Helper function to deduce the name of the provided function.
+ *
+ * @param funtion function
+ * The function whose name will be returned.
+ * @return string
+ * Function name.
+ */
+ getFunctionName: function (func) {
+ let name = null;
+ if (func.name) {
+ name = func.name;
+ } else {
+ let desc;
+ try {
+ desc = func.getOwnPropertyDescriptor("displayName");
+ } catch (ex) {
+ // Ignore.
+ }
+ if (desc && typeof desc.value == "string") {
+ name = desc.value;
+ }
+ }
+ if (!name) {
+ try {
+ let str = (func.toString() || func.toSource()) + "";
+ name = (str.match(REGEX_MATCH_FUNCTION_NAME) || [])[1];
+ } catch (ex) {
+ // Ignore.
+ }
+ }
+ return name;
+ },
+
+ /**
+ * Get the object class name. For example, the |window| object has the Window
+ * class name (based on [object Window]).
+ *
+ * @param object object
+ * The object you want to get the class name for.
+ * @return string
+ * The object class name.
+ */
+ getObjectClassName: function (object) {
+ if (object === null) {
+ return "null";
+ }
+ if (object === undefined) {
+ return "undefined";
+ }
+
+ let type = typeof object;
+ if (type != "object") {
+ // Grip class names should start with an uppercase letter.
+ return type.charAt(0).toUpperCase() + type.substr(1);
+ }
+
+ let className;
+
+ try {
+ className = ((object + "").match(/^\[object (\S+)\]$/) || [])[1];
+ if (!className) {
+ className = ((object.constructor + "")
+ .match(/^\[object (\S+)\]$/) || [])[1];
+ }
+ if (!className && typeof object.constructor == "function") {
+ className = this.getFunctionName(object.constructor);
+ }
+ } catch (ex) {
+ // Ignore.
+ }
+
+ return className;
+ },
+
+ /**
+ * Check if the given value is a grip with an actor.
+ *
+ * @param mixed grip
+ * Value you want to check if it is a grip with an actor.
+ * @return boolean
+ * True if the given value is a grip with an actor.
+ */
+ isActorGrip: function (grip) {
+ return grip && typeof (grip) == "object" && grip.actor;
+ },
+
+ /**
+ * Value of devtools.selfxss.count preference
+ *
+ * @type number
+ * @private
+ */
+ _usageCount: 0,
+ get usageCount() {
+ if (WebConsoleUtils._usageCount < CONSOLE_ENTRY_THRESHOLD) {
+ WebConsoleUtils._usageCount =
+ Services.prefs.getIntPref("devtools.selfxss.count");
+ if (Services.prefs.getBoolPref("devtools.chrome.enabled")) {
+ WebConsoleUtils.usageCount = CONSOLE_ENTRY_THRESHOLD;
+ }
+ }
+ return WebConsoleUtils._usageCount;
+ },
+ set usageCount(newUC) {
+ if (newUC <= CONSOLE_ENTRY_THRESHOLD) {
+ WebConsoleUtils._usageCount = newUC;
+ Services.prefs.setIntPref("devtools.selfxss.count", newUC);
+ }
+ },
+ /**
+ * The inputNode "paste" event handler generator. Helps prevent
+ * self-xss attacks
+ *
+ * @param nsIDOMElement inputField
+ * @param nsIDOMElement notificationBox
+ * @returns A function to be added as a handler to 'paste' and
+ *'drop' events on the input field
+ */
+ pasteHandlerGen: function (inputField, notificationBox, msg, okstring) {
+ let handler = function (event) {
+ if (WebConsoleUtils.usageCount >= CONSOLE_ENTRY_THRESHOLD) {
+ inputField.removeEventListener("paste", handler);
+ inputField.removeEventListener("drop", handler);
+ return true;
+ }
+ if (notificationBox.getNotificationWithValue("selfxss-notification")) {
+ event.preventDefault();
+ event.stopPropagation();
+ return false;
+ }
+
+ let notification = notificationBox.appendNotification(msg,
+ "selfxss-notification", null,
+ notificationBox.PRIORITY_WARNING_HIGH, null,
+ function (eventType) {
+ // Cleanup function if notification is dismissed
+ if (eventType == "removed") {
+ inputField.removeEventListener("keyup", pasteKeyUpHandler);
+ }
+ });
+
+ function pasteKeyUpHandler(event2) {
+ let value = inputField.value || inputField.textContent;
+ if (value.includes(okstring)) {
+ notificationBox.removeNotification(notification);
+ inputField.removeEventListener("keyup", pasteKeyUpHandler);
+ WebConsoleUtils.usageCount = CONSOLE_ENTRY_THRESHOLD;
+ }
+ }
+ inputField.addEventListener("keyup", pasteKeyUpHandler);
+
+ event.preventDefault();
+ event.stopPropagation();
+ return false;
+ };
+ return handler;
+ },
+};
+
+exports.Utils = WebConsoleUtils;
+
+// Localization
+
+WebConsoleUtils.L10n = function (bundleURI) {
+ this._helper = new LocalizationHelper(bundleURI);
+};
+
+WebConsoleUtils.L10n.prototype = {
+ /**
+ * Generates a formatted timestamp string for displaying in console messages.
+ *
+ * @param integer [milliseconds]
+ * Optional, allows you to specify the timestamp in milliseconds since
+ * the UNIX epoch.
+ * @return string
+ * The timestamp formatted for display.
+ */
+ timestampString: function (milliseconds) {
+ let d = new Date(milliseconds ? milliseconds : null);
+ let hours = d.getHours(), minutes = d.getMinutes();
+ let seconds = d.getSeconds();
+ milliseconds = d.getMilliseconds();
+ let parameters = [hours, minutes, seconds, milliseconds];
+ return this.getFormatStr("timestampFormat", parameters);
+ },
+
+ /**
+ * Retrieve a localized string.
+ *
+ * @param string name
+ * The string name you want from the Web Console string bundle.
+ * @return string
+ * The localized string.
+ */
+ getStr: function (name) {
+ try {
+ return this._helper.getStr(name);
+ } catch (ex) {
+ console.error("Failed to get string: " + name);
+ throw ex;
+ }
+ },
+
+ /**
+ * Retrieve a localized string formatted with values coming from the given
+ * array.
+ *
+ * @param string name
+ * The string name you want from the Web Console string bundle.
+ * @param array array
+ * The array of values you want in the formatted string.
+ * @return string
+ * The formatted local string.
+ */
+ getFormatStr: function (name, array) {
+ try {
+ return this._helper.getFormatStr(name, ...array);
+ } catch (ex) {
+ console.error("Failed to format string: " + name);
+ throw ex;
+ }
+ },
+};
diff --git a/devtools/client/webconsole/webconsole.js b/devtools/client/webconsole/webconsole.js
new file mode 100644
index 000000000..bd7f90a0e
--- /dev/null
+++ b/devtools/client/webconsole/webconsole.js
@@ -0,0 +1,3658 @@
+/* -*- 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 {Cc, Ci, Cu} = require("chrome");
+
+const {Utils: WebConsoleUtils, CONSOLE_WORKER_IDS} =
+ require("devtools/client/webconsole/utils");
+const { getSourceNames } = require("devtools/client/shared/source-utils");
+const BrowserLoaderModule = {};
+Cu.import("resource://devtools/client/shared/browser-loader.js", BrowserLoaderModule);
+
+const promise = require("promise");
+const Services = require("Services");
+const ErrorDocs = require("devtools/server/actors/errordocs");
+const Telemetry = require("devtools/client/shared/telemetry");
+
+loader.lazyServiceGetter(this, "clipboardHelper",
+ "@mozilla.org/widget/clipboardhelper;1",
+ "nsIClipboardHelper");
+loader.lazyRequireGetter(this, "EventEmitter", "devtools/shared/event-emitter");
+loader.lazyRequireGetter(this, "AutocompletePopup", "devtools/client/shared/autocomplete-popup", true);
+loader.lazyRequireGetter(this, "ToolSidebar", "devtools/client/framework/sidebar", true);
+loader.lazyRequireGetter(this, "ConsoleOutput", "devtools/client/webconsole/console-output", true);
+loader.lazyRequireGetter(this, "Messages", "devtools/client/webconsole/console-output", true);
+loader.lazyRequireGetter(this, "EnvironmentClient", "devtools/shared/client/main", true);
+loader.lazyRequireGetter(this, "ObjectClient", "devtools/shared/client/main", true);
+loader.lazyRequireGetter(this, "system", "devtools/shared/system");
+loader.lazyRequireGetter(this, "JSTerm", "devtools/client/webconsole/jsterm", true);
+loader.lazyRequireGetter(this, "gSequenceId", "devtools/client/webconsole/jsterm", true);
+loader.lazyImporter(this, "VariablesView", "resource://devtools/client/shared/widgets/VariablesView.jsm");
+loader.lazyImporter(this, "VariablesViewController", "resource://devtools/client/shared/widgets/VariablesViewController.jsm");
+loader.lazyRequireGetter(this, "gDevTools", "devtools/client/framework/devtools", true);
+loader.lazyRequireGetter(this, "KeyShortcuts", "devtools/client/shared/key-shortcuts", true);
+loader.lazyRequireGetter(this, "ZoomKeys", "devtools/client/shared/zoom-keys");
+
+const {PluralForm} = require("devtools/shared/plural-form");
+const STRINGS_URI = "devtools/client/locales/webconsole.properties";
+var l10n = new WebConsoleUtils.L10n(STRINGS_URI);
+
+const XHTML_NS = "http://www.w3.org/1999/xhtml";
+
+const MIXED_CONTENT_LEARN_MORE = "https://developer.mozilla.org/docs/Web/Security/Mixed_content";
+
+const IGNORED_SOURCE_URLS = ["debugger eval code"];
+
+// The amount of time in milliseconds that we wait before performing a live
+// search.
+const SEARCH_DELAY = 200;
+
+// The number of lines that are displayed in the console output by default, for
+// each category. The user can change this number by adjusting the hidden
+// "devtools.hud.loglimit.{network,cssparser,exception,console}" preferences.
+const DEFAULT_LOG_LIMIT = 1000;
+
+// The various categories of messages. We start numbering at zero so we can
+// use these as indexes into the MESSAGE_PREFERENCE_KEYS matrix below.
+const CATEGORY_NETWORK = 0;
+const CATEGORY_CSS = 1;
+const CATEGORY_JS = 2;
+const CATEGORY_WEBDEV = 3;
+// always on
+const CATEGORY_INPUT = 4;
+// always on
+const CATEGORY_OUTPUT = 5;
+const CATEGORY_SECURITY = 6;
+const CATEGORY_SERVER = 7;
+
+// The possible message severities. As before, we start at zero so we can use
+// these as indexes into MESSAGE_PREFERENCE_KEYS.
+const SEVERITY_ERROR = 0;
+const SEVERITY_WARNING = 1;
+const SEVERITY_INFO = 2;
+const SEVERITY_LOG = 3;
+
+// The fragment of a CSS class name that identifies each category.
+const CATEGORY_CLASS_FRAGMENTS = [
+ "network",
+ "cssparser",
+ "exception",
+ "console",
+ "input",
+ "output",
+ "security",
+ "server",
+];
+
+// The fragment of a CSS class name that identifies each severity.
+const SEVERITY_CLASS_FRAGMENTS = [
+ "error",
+ "warn",
+ "info",
+ "log",
+];
+
+// The preference keys to use for each category/severity combination, indexed
+// first by category (rows) and then by severity (columns) in the following
+// order:
+//
+// [ Error, Warning, Info, Log ]
+//
+// Most of these rather idiosyncratic names are historical and predate the
+// division of message type into "category" and "severity".
+const MESSAGE_PREFERENCE_KEYS = [
+ // Network
+ [ "network", "netwarn", "netxhr", "networkinfo", ],
+ // CSS
+ [ "csserror", "cssparser", null, "csslog", ],
+ // JS
+ [ "exception", "jswarn", null, "jslog", ],
+ // Web Developer
+ [ "error", "warn", "info", "log", ],
+ // Input
+ [ null, null, null, null, ],
+ // Output
+ [ null, null, null, null, ],
+ // Security
+ [ "secerror", "secwarn", null, null, ],
+ // Server Logging
+ [ "servererror", "serverwarn", "serverinfo", "serverlog", ],
+];
+
+// A mapping from the console API log event levels to the Web Console
+// severities.
+const LEVELS = {
+ error: SEVERITY_ERROR,
+ exception: SEVERITY_ERROR,
+ assert: SEVERITY_ERROR,
+ warn: SEVERITY_WARNING,
+ info: SEVERITY_INFO,
+ log: SEVERITY_LOG,
+ clear: SEVERITY_LOG,
+ trace: SEVERITY_LOG,
+ table: SEVERITY_LOG,
+ debug: SEVERITY_LOG,
+ dir: SEVERITY_LOG,
+ dirxml: SEVERITY_LOG,
+ group: SEVERITY_LOG,
+ groupCollapsed: SEVERITY_LOG,
+ groupEnd: SEVERITY_LOG,
+ time: SEVERITY_LOG,
+ timeEnd: SEVERITY_LOG,
+ count: SEVERITY_LOG
+};
+
+// This array contains the prefKey for the workers and it must keep them in the
+// same order as CONSOLE_WORKER_IDS
+const WORKERTYPES_PREFKEYS =
+ [ "sharedworkers", "serviceworkers", "windowlessworkers" ];
+
+// The lowest HTTP response code (inclusive) that is considered an error.
+const MIN_HTTP_ERROR_CODE = 400;
+// The highest HTTP response code (inclusive) that is considered an error.
+const MAX_HTTP_ERROR_CODE = 599;
+
+// The indent of a console group in pixels.
+const GROUP_INDENT = 12;
+
+// The number of messages to display in a single display update. If we display
+// too many messages at once we slow down the Firefox UI too much.
+const MESSAGES_IN_INTERVAL = DEFAULT_LOG_LIMIT;
+
+// The delay (in milliseconds) between display updates - tells how often we
+// should *try* to push new messages to screen. This value is optimistic,
+// updates won't always happen. Keep this low so the Web Console output feels
+// live.
+const OUTPUT_INTERVAL = 20;
+
+// The maximum amount of time (in milliseconds) that can be spent doing cleanup
+// inside of the flush output callback. If things don't get cleaned up in this
+// time, then it will start again the next time it is called.
+const MAX_CLEANUP_TIME = 10;
+
+// When the output queue has more than MESSAGES_IN_INTERVAL items we throttle
+// output updates to this number of milliseconds. So during a lot of output we
+// update every N milliseconds given here.
+const THROTTLE_UPDATES = 1000;
+
+// The preference prefix for all of the Web Console filters.
+const FILTER_PREFS_PREFIX = "devtools.webconsole.filter.";
+
+// The minimum font size.
+const MIN_FONT_SIZE = 10;
+
+const PREF_CONNECTION_TIMEOUT = "devtools.debugger.remote-timeout";
+const PREF_PERSISTLOG = "devtools.webconsole.persistlog";
+const PREF_MESSAGE_TIMESTAMP = "devtools.webconsole.timestampMessages";
+const PREF_NEW_FRONTEND_ENABLED = "devtools.webconsole.new-frontend-enabled";
+
+/**
+ * A WebConsoleFrame instance is an interactive console initialized *per target*
+ * that displays console log data as well as provides an interactive terminal to
+ * manipulate the target's document content.
+ *
+ * The WebConsoleFrame is responsible for the actual Web Console UI
+ * implementation.
+ *
+ * @constructor
+ * @param object webConsoleOwner
+ * The WebConsole owner object.
+ */
+function WebConsoleFrame(webConsoleOwner) {
+ this.owner = webConsoleOwner;
+ this.hudId = this.owner.hudId;
+ this.isBrowserConsole = this.owner._browserConsole;
+
+ this.window = this.owner.iframeWindow;
+
+ this._repeatNodes = {};
+ this._outputQueue = [];
+ this._itemDestroyQueue = [];
+ this._pruneCategoriesQueue = {};
+ this.filterPrefs = {};
+
+ this.output = new ConsoleOutput(this);
+
+ this.unmountMessage = this.unmountMessage.bind(this);
+ this._toggleFilter = this._toggleFilter.bind(this);
+ this.resize = this.resize.bind(this);
+ this._onPanelSelected = this._onPanelSelected.bind(this);
+ this._flushMessageQueue = this._flushMessageQueue.bind(this);
+ this._onToolboxPrefChanged = this._onToolboxPrefChanged.bind(this);
+ this._onUpdateListeners = this._onUpdateListeners.bind(this);
+
+ this._outputTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ this._outputTimerInitialized = false;
+
+ let require = BrowserLoaderModule.BrowserLoader({
+ window: this.window,
+ useOnlyShared: true
+ }).require;
+
+ this.React = require("devtools/client/shared/vendor/react");
+ this.ReactDOM = require("devtools/client/shared/vendor/react-dom");
+ this.FrameView = this.React.createFactory(require("devtools/client/shared/components/frame"));
+ this.StackTraceView = this.React.createFactory(require("devtools/client/shared/components/stack-trace"));
+
+ this._telemetry = new Telemetry();
+
+ EventEmitter.decorate(this);
+}
+exports.WebConsoleFrame = WebConsoleFrame;
+
+WebConsoleFrame.prototype = {
+ /**
+ * The WebConsole instance that owns this frame.
+ * @see hudservice.js::WebConsole
+ * @type object
+ */
+ owner: null,
+
+ /**
+ * Proxy between the Web Console and the remote Web Console instance. This
+ * object holds methods used for connecting, listening and disconnecting from
+ * the remote server, using the remote debugging protocol.
+ *
+ * @see WebConsoleConnectionProxy
+ * @type object
+ */
+ proxy: null,
+
+ /**
+ * Getter for the xul:popupset that holds any popups we open.
+ * @type nsIDOMElement
+ */
+ get popupset() {
+ return this.owner.mainPopupSet;
+ },
+
+ /**
+ * Holds the initialization promise object.
+ * @private
+ * @type object
+ */
+ _initDefer: null,
+
+ /**
+ * Last time when we displayed any message in the output.
+ *
+ * @private
+ * @type number
+ * Timestamp in milliseconds since the Unix epoch.
+ */
+ _lastOutputFlush: 0,
+
+ /**
+ * Message nodes are stored here in a queue for later display.
+ *
+ * @private
+ * @type array
+ */
+ _outputQueue: null,
+
+ /**
+ * Keep track of the categories we need to prune from time to time.
+ *
+ * @private
+ * @type array
+ */
+ _pruneCategoriesQueue: null,
+
+ /**
+ * Function invoked whenever the output queue is emptied. This is used by some
+ * tests.
+ *
+ * @private
+ * @type function
+ */
+ _flushCallback: null,
+
+ /**
+ * Timer used for flushing the messages output queue.
+ *
+ * @private
+ * @type nsITimer
+ */
+ _outputTimer: null,
+ _outputTimerInitialized: null,
+
+ /**
+ * Store for tracking repeated nodes.
+ * @private
+ * @type object
+ */
+ _repeatNodes: null,
+
+ /**
+ * Preferences for filtering messages by type.
+ * @see this._initDefaultFilterPrefs()
+ * @type object
+ */
+ filterPrefs: null,
+
+ /**
+ * Prefix used for filter preferences.
+ * @private
+ * @type string
+ */
+ _filterPrefsPrefix: FILTER_PREFS_PREFIX,
+
+ /**
+ * The nesting depth of the currently active console group.
+ */
+ groupDepth: 0,
+
+ /**
+ * The current target location.
+ * @type string
+ */
+ contentLocation: "",
+
+ /**
+ * The JSTerm object that manage the console's input.
+ * @see JSTerm
+ * @type object
+ */
+ jsterm: null,
+
+ /**
+ * The element that holds all of the messages we display.
+ * @type nsIDOMElement
+ */
+ outputNode: null,
+
+ /**
+ * The ConsoleOutput instance that manages all output.
+ * @type object
+ */
+ output: null,
+
+ /**
+ * The input element that allows the user to filter messages by string.
+ * @type nsIDOMElement
+ */
+ filterBox: null,
+
+ /**
+ * Getter for the debugger WebConsoleClient.
+ * @type object
+ */
+ get webConsoleClient() {
+ return this.proxy ? this.proxy.webConsoleClient : null;
+ },
+
+ _destroyer: null,
+
+ _saveRequestAndResponseBodies: true,
+ _throttleData: null,
+
+ // Chevron width at the starting of Web Console's input box.
+ _chevronWidth: 0,
+ // Width of the monospace characters in Web Console's input box.
+ _inputCharWidth: 0,
+
+ /**
+ * Setter for saving of network request and response bodies.
+ *
+ * @param boolean value
+ * The new value you want to set.
+ */
+ setSaveRequestAndResponseBodies: function (value) {
+ if (!this.webConsoleClient) {
+ // Don't continue if the webconsole disconnected.
+ return promise.resolve(null);
+ }
+
+ let deferred = promise.defer();
+ let newValue = !!value;
+ let toSet = {
+ "NetworkMonitor.saveRequestAndResponseBodies": newValue,
+ };
+
+ // Make sure the web console client connection is established first.
+ this.webConsoleClient.setPreferences(toSet, response => {
+ if (!response.error) {
+ this._saveRequestAndResponseBodies = newValue;
+ deferred.resolve(response);
+ } else {
+ deferred.reject(response.error);
+ }
+ });
+
+ return deferred.promise;
+ },
+
+ /**
+ * Setter for throttling data.
+ *
+ * @param boolean value
+ * The new value you want to set; @see NetworkThrottleManager.
+ */
+ setThrottleData: function(value) {
+ if (!this.webConsoleClient) {
+ // Don't continue if the webconsole disconnected.
+ return promise.resolve(null);
+ }
+
+ let deferred = promise.defer();
+ let toSet = {
+ "NetworkMonitor.throttleData": value,
+ };
+
+ // Make sure the web console client connection is established first.
+ this.webConsoleClient.setPreferences(toSet, response => {
+ if (!response.error) {
+ this._throttleData = value;
+ deferred.resolve(response);
+ } else {
+ deferred.reject(response.error);
+ }
+ });
+
+ return deferred.promise;
+ },
+
+ /**
+ * Getter for the persistent logging preference.
+ * @type boolean
+ */
+ get persistLog() {
+ // For the browser console, we receive tab navigation
+ // when the original top level window we attached to is closed,
+ // but we don't want to reset console history and just switch to
+ // the next available window.
+ return this.isBrowserConsole ||
+ Services.prefs.getBoolPref(PREF_PERSISTLOG);
+ },
+
+ /**
+ * Initialize the WebConsoleFrame instance.
+ * @return object
+ * A promise object that resolves once the frame is ready to use.
+ */
+ init: function () {
+ this._initUI();
+ let connectionInited = this._initConnection();
+
+ // Don't reject if the history fails to load for some reason.
+ // This would be fine, the panel will just start with empty history.
+ let allReady = this.jsterm.historyLoaded.catch(() => {}).then(() => {
+ return connectionInited;
+ });
+
+ // This notification is only used in tests. Don't chain it onto
+ // the returned promise because the console panel needs to be attached
+ // to the toolbox before the web-console-created event is receieved.
+ let notifyObservers = () => {
+ let id = WebConsoleUtils.supportsString(this.hudId);
+ Services.obs.notifyObservers(id, "web-console-created", null);
+ };
+ allReady.then(notifyObservers, notifyObservers);
+
+ if (this.NEW_CONSOLE_OUTPUT_ENABLED) {
+ allReady.then(this.newConsoleOutput.init);
+ }
+
+ return allReady;
+ },
+
+ /**
+ * Connect to the server using the remote debugging protocol.
+ *
+ * @private
+ * @return object
+ * A promise object that is resolved/reject based on the connection
+ * result.
+ */
+ _initConnection: function () {
+ if (this._initDefer) {
+ return this._initDefer.promise;
+ }
+
+ this._initDefer = promise.defer();
+ this.proxy = new WebConsoleConnectionProxy(this, this.owner.target);
+
+ this.proxy.connect().then(() => {
+ // on success
+ this._initDefer.resolve(this);
+ }, (reason) => {
+ // on failure
+ let node = this.createMessageNode(CATEGORY_JS, SEVERITY_ERROR,
+ reason.error + ": " + reason.message);
+ this.outputMessage(CATEGORY_JS, node, [reason]);
+ this._initDefer.reject(reason);
+ });
+
+ return this._initDefer.promise;
+ },
+
+ /**
+ * Find the Web Console UI elements and setup event listeners as needed.
+ * @private
+ */
+ _initUI: function () {
+ this.document = this.window.document;
+ this.rootElement = this.document.documentElement;
+ this.NEW_CONSOLE_OUTPUT_ENABLED = !this.isBrowserConsole
+ && !this.owner.target.chrome
+ && Services.prefs.getBoolPref(PREF_NEW_FRONTEND_ENABLED);
+
+ this.outputNode = this.document.getElementById("output-container");
+ this.outputWrapper = this.document.getElementById("output-wrapper");
+ this.completeNode = this.document.querySelector(".jsterm-complete-node");
+ this.inputNode = this.document.querySelector(".jsterm-input-node");
+
+ // In the old frontend, the area that scrolls is outputWrapper, but in the new
+ // frontend this will be reassigned.
+ this.outputScroller = this.outputWrapper;
+
+ // Update the character width and height needed for the popup offset
+ // calculations.
+ this._updateCharSize();
+
+ this.jsterm = new JSTerm(this);
+ this.jsterm.init();
+
+ let toolbox = gDevTools.getToolbox(this.owner.target);
+
+ if (this.NEW_CONSOLE_OUTPUT_ENABLED) {
+ // @TODO Remove this once JSTerm is handled with React/Redux.
+ this.window.jsterm = this.jsterm;
+
+ // Remove context menu for now (see Bug 1307239).
+ this.outputWrapper.removeAttribute("context");
+
+ // XXX: We should actually stop output from happening on old output
+ // panel, but for now let's just hide it.
+ this.experimentalOutputNode = this.outputNode.cloneNode();
+ this.experimentalOutputNode.removeAttribute("tabindex");
+ this.outputNode.hidden = true;
+ this.outputNode.parentNode.appendChild(this.experimentalOutputNode);
+ // @TODO Once the toolbox has been converted to React, see if passing
+ // in JSTerm is still necessary.
+
+ this.newConsoleOutput = new this.window.NewConsoleOutput(
+ this.experimentalOutputNode, this.jsterm, toolbox, this.owner, this.document);
+
+ let filterToolbar = this.document.querySelector(".hud-console-filter-toolbar");
+ filterToolbar.hidden = true;
+ } else {
+ // Register the controller to handle "select all" properly.
+ this._commandController = new CommandController(this);
+ this.window.controllers.insertControllerAt(0, this._commandController);
+
+ this._contextMenuHandler = new ConsoleContextMenu(this);
+
+ this._initDefaultFilterPrefs();
+ this.filterBox = this.document.querySelector(".hud-filter-box");
+ this._setFilterTextBoxEvents();
+ this._initFilterButtons();
+ let clearButton =
+ this.document.getElementsByClassName("webconsole-clear-console-button")[0];
+ clearButton.addEventListener("command", () => {
+ this.owner._onClearButton();
+ this.jsterm.clearOutput(true);
+ });
+
+ }
+
+ this.resize();
+ this.window.addEventListener("resize", this.resize, true);
+ this.jsterm.on("sidebar-opened", this.resize);
+ this.jsterm.on("sidebar-closed", this.resize);
+
+ if (toolbox) {
+ toolbox.on("webconsole-selected", this._onPanelSelected);
+ }
+
+ /*
+ * Focus the input line whenever the output area is clicked.
+ */
+ this.outputWrapper.addEventListener("click", (event) => {
+ // Do not focus on middle/right-click or 2+ clicks.
+ if (event.detail !== 1 || event.button !== 0) {
+ return;
+ }
+
+ // Do not focus if something is selected
+ let selection = this.window.getSelection();
+ if (selection && !selection.isCollapsed) {
+ return;
+ }
+
+ // Do not focus if a link was clicked
+ if (event.target.nodeName.toLowerCase() === "a" ||
+ event.target.parentNode.nodeName.toLowerCase() === "a") {
+ return;
+ }
+
+ // Do not focus if a search input was clicked on the new frontend
+ if (this.NEW_CONSOLE_OUTPUT_ENABLED &&
+ event.target.nodeName.toLowerCase() === "input" &&
+ event.target.getAttribute("type").toLowerCase() === "search") {
+ return;
+ }
+
+ this.jsterm.focus();
+ });
+
+ // Toggle the timestamp on preference change
+ gDevTools.on("pref-changed", this._onToolboxPrefChanged);
+ this._onToolboxPrefChanged("pref-changed", {
+ pref: PREF_MESSAGE_TIMESTAMP,
+ newValue: Services.prefs.getBoolPref(PREF_MESSAGE_TIMESTAMP),
+ });
+
+ this._initShortcuts();
+
+ // focus input node
+ this.jsterm.focus();
+ },
+
+ /**
+ * Resizes the output node to fit the output wrapped.
+ * We need this because it makes the layout a lot faster than
+ * using -moz-box-flex and 100% width. See Bug 1237368.
+ */
+ resize: function () {
+ if (this.NEW_CONSOLE_OUTPUT_ENABLED) {
+ this.experimentalOutputNode.style.width =
+ this.outputWrapper.clientWidth + "px";
+ } else {
+ this.outputNode.style.width = this.outputWrapper.clientWidth + "px";
+ }
+ },
+
+ /**
+ * Sets the focus to JavaScript input field when the web console tab is
+ * selected or when there is a split console present.
+ * @private
+ */
+ _onPanelSelected: function () {
+ this.jsterm.focus();
+ },
+
+ /**
+ * Initialize the default filter preferences.
+ * @private
+ */
+ _initDefaultFilterPrefs: function () {
+ let prefs = ["network", "networkinfo", "csserror", "cssparser", "csslog",
+ "exception", "jswarn", "jslog", "error", "info", "warn", "log",
+ "secerror", "secwarn", "netwarn", "netxhr", "sharedworkers",
+ "serviceworkers", "windowlessworkers", "servererror",
+ "serverwarn", "serverinfo", "serverlog"];
+
+ for (let pref of prefs) {
+ this.filterPrefs[pref] = Services.prefs.getBoolPref(
+ this._filterPrefsPrefix + pref);
+ }
+ },
+
+ _initShortcuts: function() {
+ var shortcuts = new KeyShortcuts({
+ window: this.window
+ });
+
+ shortcuts.on(l10n.getStr("webconsole.find.key"),
+ (name, event) => {
+ this.filterBox.focus();
+ event.preventDefault();
+ });
+
+ let clearShortcut;
+ if (system.constants.platform === "macosx") {
+ clearShortcut = l10n.getStr("webconsole.clear.keyOSX");
+ } else {
+ clearShortcut = l10n.getStr("webconsole.clear.key");
+ }
+ shortcuts.on(clearShortcut,
+ () => this.jsterm.clearOutput(true));
+
+ if (this.isBrowserConsole) {
+ shortcuts.on(l10n.getStr("webconsole.close.key"),
+ this.window.close.bind(this.window));
+
+ ZoomKeys.register(this.window);
+ }
+ },
+
+ /**
+ * Attach / detach reflow listeners depending on the checked status
+ * of the `CSS > Log` menuitem.
+ *
+ * @param function [callback=null]
+ * Optional function to invoke when the listener has been
+ * added/removed.
+ */
+ _updateReflowActivityListener: function (callback) {
+ if (this.webConsoleClient) {
+ let pref = this._filterPrefsPrefix + "csslog";
+ if (Services.prefs.getBoolPref(pref)) {
+ this.webConsoleClient.startListeners(["ReflowActivity"], callback);
+ } else {
+ this.webConsoleClient.stopListeners(["ReflowActivity"], callback);
+ }
+ }
+ },
+
+ /**
+ * Attach / detach server logging listener depending on the filter
+ * preferences. If the user isn't interested in the server logs at
+ * all the listener is not registered.
+ *
+ * @param function [callback=null]
+ * Optional function to invoke when the listener has been
+ * added/removed.
+ */
+ _updateServerLoggingListener: function (callback) {
+ if (!this.webConsoleClient) {
+ return null;
+ }
+
+ let startListener = false;
+ let prefs = ["servererror", "serverwarn", "serverinfo", "serverlog"];
+ for (let i = 0; i < prefs.length; i++) {
+ if (this.filterPrefs[prefs[i]]) {
+ startListener = true;
+ break;
+ }
+ }
+
+ if (startListener) {
+ this.webConsoleClient.startListeners(["ServerLogging"], callback);
+ } else {
+ this.webConsoleClient.stopListeners(["ServerLogging"], callback);
+ }
+ },
+
+ /**
+ * Sets the events for the filter input field.
+ * @private
+ */
+ _setFilterTextBoxEvents: function () {
+ let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ let timerEvent = this.adjustVisibilityOnSearchStringChange.bind(this);
+
+ let onChange = function _onChange() {
+ // To improve responsiveness, we let the user finish typing before we
+ // perform the search.
+ timer.cancel();
+ timer.initWithCallback(timerEvent, SEARCH_DELAY,
+ Ci.nsITimer.TYPE_ONE_SHOT);
+ };
+
+ this.filterBox.addEventListener("command", onChange, false);
+ this.filterBox.addEventListener("input", onChange, false);
+ },
+
+ /**
+ * Creates one of the filter buttons on the toolbar.
+ *
+ * @private
+ * @param nsIDOMNode aParent
+ * The node to which the filter button should be appended.
+ * @param object aDescriptor
+ * A descriptor that contains info about the button. Contains "name",
+ * "category", and "prefKey" properties, and optionally a "severities"
+ * property.
+ */
+ _initFilterButtons: function () {
+ let categories = this.document
+ .querySelectorAll(".webconsole-filter-button[category]");
+ Array.forEach(categories, function (button) {
+ button.addEventListener("contextmenu", () => {
+ button.open = true;
+ }, false);
+ button.addEventListener("click", this._toggleFilter, false);
+
+ let someChecked = false;
+ let severities = button.querySelectorAll("menuitem[prefKey]");
+ Array.forEach(severities, function (menuItem) {
+ menuItem.addEventListener("command", this._toggleFilter, false);
+
+ let prefKey = menuItem.getAttribute("prefKey");
+ let checked = this.filterPrefs[prefKey];
+ menuItem.setAttribute("checked", checked);
+ someChecked = someChecked || checked;
+ }, this);
+
+ button.setAttribute("checked", someChecked);
+ button.setAttribute("aria-pressed", someChecked);
+ }, this);
+
+ if (!this.isBrowserConsole) {
+ // The Browser Console displays nsIConsoleMessages which are messages that
+ // end up in the JS category, but they are not errors or warnings, they
+ // are just log messages. The Web Console does not show such messages.
+ let jslog = this.document.querySelector("menuitem[prefKey=jslog]");
+ jslog.hidden = true;
+ }
+
+ if (Services.appinfo.OS == "Darwin") {
+ let net = this.document.querySelector("toolbarbutton[category=net]");
+ let accesskey = net.getAttribute("accesskeyMacOSX");
+ net.setAttribute("accesskey", accesskey);
+
+ let logging =
+ this.document.querySelector("toolbarbutton[category=logging]");
+ logging.removeAttribute("accesskey");
+
+ let serverLogging =
+ this.document.querySelector("toolbarbutton[category=server]");
+ serverLogging.removeAttribute("accesskey");
+ }
+ },
+
+ /**
+ * Calculates the width and height of a single character of the input box.
+ * This will be used in opening the popup at the correct offset.
+ *
+ * @private
+ */
+ _updateCharSize: function () {
+ let doc = this.document;
+ let tempLabel = doc.createElementNS(XHTML_NS, "span");
+ let style = tempLabel.style;
+ style.position = "fixed";
+ style.padding = "0";
+ style.margin = "0";
+ style.width = "auto";
+ style.color = "transparent";
+ WebConsoleUtils.copyTextStyles(this.inputNode, tempLabel);
+ tempLabel.textContent = "x";
+ doc.documentElement.appendChild(tempLabel);
+ this._inputCharWidth = tempLabel.offsetWidth;
+ tempLabel.parentNode.removeChild(tempLabel);
+ // Calculate the width of the chevron placed at the beginning of the input
+ // box. Remove 4 more pixels to accomodate the padding of the popup.
+ this._chevronWidth = +doc.defaultView.getComputedStyle(this.inputNode)
+ .paddingLeft.replace(/[^0-9.]/g, "") - 4;
+ },
+
+ /**
+ * The event handler that is called whenever a user switches a filter on or
+ * off.
+ *
+ * @private
+ * @param nsIDOMEvent event
+ * The event that triggered the filter change.
+ */
+ _toggleFilter: function (event) {
+ let target = event.target;
+ let tagName = target.tagName;
+ // Prevent toggle if generated from a contextmenu event (right click)
+ let isRightClick = (event.button === 2);
+ if (tagName != event.currentTarget.tagName || isRightClick) {
+ return;
+ }
+
+ switch (tagName) {
+ case "toolbarbutton": {
+ let originalTarget = event.originalTarget;
+ let classes = originalTarget.classList;
+
+ if (originalTarget.localName !== "toolbarbutton") {
+ // Oddly enough, the click event is sent to the menu button when
+ // selecting a menu item with the mouse. Detect this case and bail
+ // out.
+ break;
+ }
+
+ if (!classes.contains("toolbarbutton-menubutton-button") &&
+ originalTarget.getAttribute("type") === "menu-button") {
+ // This is a filter button with a drop-down. The user clicked the
+ // drop-down, so do nothing. (The menu will automatically appear
+ // without our intervention.)
+ break;
+ }
+
+ // Toggle on the targeted filter button, and if the user alt clicked,
+ // toggle off all other filter buttons and their associated filters.
+ let state = target.getAttribute("checked") !== "true";
+ if (event.getModifierState("Alt")) {
+ let buttons = this.document
+ .querySelectorAll(".webconsole-filter-button");
+ Array.forEach(buttons, (button) => {
+ if (button !== target) {
+ button.setAttribute("checked", false);
+ button.setAttribute("aria-pressed", false);
+ this._setMenuState(button, false);
+ }
+ });
+ state = true;
+ }
+ target.setAttribute("checked", state);
+ target.setAttribute("aria-pressed", state);
+
+ // This is a filter button with a drop-down, and the user clicked the
+ // main part of the button. Go through all the severities and toggle
+ // their associated filters.
+ this._setMenuState(target, state);
+
+ // CSS reflow logging can decrease web page performance.
+ // Make sure the option is always unchecked when the CSS filter button
+ // is selected. See bug 971798.
+ if (target.getAttribute("category") == "css" && state) {
+ let csslogMenuItem = target.querySelector("menuitem[prefKey=csslog]");
+ csslogMenuItem.setAttribute("checked", false);
+ this.setFilterState("csslog", false);
+ }
+
+ break;
+ }
+
+ case "menuitem": {
+ let state = target.getAttribute("checked") !== "true";
+ target.setAttribute("checked", state);
+
+ let prefKey = target.getAttribute("prefKey");
+ this.setFilterState(prefKey, state);
+
+ // Adjust the state of the button appropriately.
+ let menuPopup = target.parentNode;
+
+ let someChecked = false;
+ let menuItem = menuPopup.firstChild;
+ while (menuItem) {
+ if (menuItem.hasAttribute("prefKey") &&
+ menuItem.getAttribute("checked") === "true") {
+ someChecked = true;
+ break;
+ }
+ menuItem = menuItem.nextSibling;
+ }
+ let toolbarButton = menuPopup.parentNode;
+ toolbarButton.setAttribute("checked", someChecked);
+ toolbarButton.setAttribute("aria-pressed", someChecked);
+ break;
+ }
+ }
+ },
+
+ /**
+ * Set the menu attributes for a specific toggle button.
+ *
+ * @private
+ * @param XULElement target
+ * Button with drop down items to be toggled.
+ * @param boolean state
+ * True if the menu item is being toggled on, and false otherwise.
+ */
+ _setMenuState: function (target, state) {
+ let menuItems = target.querySelectorAll("menuitem");
+ Array.forEach(menuItems, (item) => {
+ item.setAttribute("checked", state);
+ let prefKey = item.getAttribute("prefKey");
+ this.setFilterState(prefKey, state);
+ });
+ },
+
+ /**
+ * Set the filter state for a specific toggle button.
+ *
+ * @param string toggleType
+ * @param boolean state
+ * @returns void
+ */
+ setFilterState: function (toggleType, state) {
+ this.filterPrefs[toggleType] = state;
+ this.adjustVisibilityForMessageType(toggleType, state);
+
+ Services.prefs.setBoolPref(this._filterPrefsPrefix + toggleType, state);
+
+ if (this._updateListenersTimeout) {
+ clearTimeout(this._updateListenersTimeout);
+ }
+
+ this._updateListenersTimeout = setTimeout(
+ this._onUpdateListeners, 200);
+ },
+
+ /**
+ * Get the filter state for a specific toggle button.
+ *
+ * @param string toggleType
+ * @returns boolean
+ */
+ getFilterState: function (toggleType) {
+ return this.filterPrefs[toggleType];
+ },
+
+ /**
+ * Called when a logging filter changes. Allows to stop/start
+ * listeners according to the current filter state.
+ */
+ _onUpdateListeners: function () {
+ this._updateReflowActivityListener();
+ this._updateServerLoggingListener();
+ },
+
+ /**
+ * Check that the passed string matches the filter arguments.
+ *
+ * @param String str
+ * to search for filter words in.
+ * @param String filter
+ * is a string containing all of the words to filter on.
+ * @returns boolean
+ */
+ stringMatchesFilters: function (str, filter) {
+ if (!filter || !str) {
+ return true;
+ }
+
+ let searchStr = str.toLowerCase();
+ let filterStrings = filter.toLowerCase().split(/\s+/);
+ return !filterStrings.some(function (f) {
+ return searchStr.indexOf(f) == -1;
+ });
+ },
+
+ /**
+ * Turns the display of log nodes on and off appropriately to reflect the
+ * adjustment of the message type filter named by @prefKey.
+ *
+ * @param string prefKey
+ * The preference key for the message type being filtered: one of the
+ * values in the MESSAGE_PREFERENCE_KEYS table.
+ * @param boolean state
+ * True if the filter named by @messageType is being turned on; false
+ * otherwise.
+ * @returns void
+ */
+ adjustVisibilityForMessageType: function (prefKey, state) {
+ let outputNode = this.outputNode;
+ let doc = this.document;
+
+ // Look for message nodes (".message") with the given preference key
+ // (filter="error", filter="cssparser", etc.) and add or remove the
+ // "filtered-by-type" class, which turns on or off the display.
+
+ let attribute = WORKERTYPES_PREFKEYS.indexOf(prefKey) == -1
+ ? "filter" : "workerType";
+
+ let xpath = ".//*[contains(@class, 'message') and " +
+ "@" + attribute + "='" + prefKey + "']";
+ let result = doc.evaluate(xpath, outputNode, null,
+ Ci.nsIDOMXPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
+ for (let i = 0; i < result.snapshotLength; i++) {
+ let node = result.snapshotItem(i);
+ if (state) {
+ node.classList.remove("filtered-by-type");
+ } else {
+ node.classList.add("filtered-by-type");
+ }
+ }
+ },
+
+ /**
+ * Turns the display of log nodes on and off appropriately to reflect the
+ * adjustment of the search string.
+ */
+ adjustVisibilityOnSearchStringChange: function () {
+ let nodes = this.outputNode.getElementsByClassName("message");
+ let searchString = this.filterBox.value;
+
+ for (let i = 0, n = nodes.length; i < n; ++i) {
+ let node = nodes[i];
+
+ // hide nodes that match the strings
+ let text = node.textContent;
+
+ // if the text matches the words in aSearchString...
+ if (this.stringMatchesFilters(text, searchString)) {
+ node.classList.remove("filtered-by-string");
+ } else {
+ node.classList.add("filtered-by-string");
+ }
+ }
+
+ this.resize();
+ },
+
+ /**
+ * Applies the user's filters to a newly-created message node via CSS
+ * classes.
+ *
+ * @param nsIDOMNode node
+ * The newly-created message node.
+ * @return boolean
+ * True if the message was filtered or false otherwise.
+ */
+ filterMessageNode: function (node) {
+ let isFiltered = false;
+
+ // Filter by the message type.
+ let prefKey = MESSAGE_PREFERENCE_KEYS[node.category][node.severity];
+ if (prefKey && !this.getFilterState(prefKey)) {
+ // The node is filtered by type.
+ node.classList.add("filtered-by-type");
+ isFiltered = true;
+ }
+
+ // Filter by worker type
+ if ("workerType" in node && !this.getFilterState(node.workerType)) {
+ node.classList.add("filtered-by-type");
+ isFiltered = true;
+ }
+
+ // Filter on the search string.
+ let search = this.filterBox.value;
+ let text = node.clipboardText;
+
+ // if string matches the filter text
+ if (!this.stringMatchesFilters(text, search)) {
+ node.classList.add("filtered-by-string");
+ isFiltered = true;
+ }
+
+ if (isFiltered && node.classList.contains("inlined-variables-view")) {
+ node.classList.add("hidden-message");
+ }
+
+ return isFiltered;
+ },
+
+ /**
+ * Merge the attributes of repeated nodes.
+ *
+ * @param nsIDOMNode original
+ * The Original Node. The one being merged into.
+ */
+ mergeFilteredMessageNode: function (original) {
+ let repeatNode = original.getElementsByClassName("message-repeats")[0];
+ if (!repeatNode) {
+ // no repeat node, return early.
+ return;
+ }
+
+ let occurrences = parseInt(repeatNode.getAttribute("value"), 10) + 1;
+ repeatNode.setAttribute("value", occurrences);
+ repeatNode.textContent = occurrences;
+ let str = l10n.getStr("messageRepeats.tooltip2");
+ repeatNode.title = PluralForm.get(occurrences, str)
+ .replace("#1", occurrences);
+ },
+
+ /**
+ * Filter the message node from the output if it is a repeat.
+ *
+ * @private
+ * @param nsIDOMNode node
+ * The message node to be filtered or not.
+ * @returns nsIDOMNode|null
+ * Returns the duplicate node if the message was filtered, null
+ * otherwise.
+ */
+ _filterRepeatedMessage: function (node) {
+ let repeatNode = node.getElementsByClassName("message-repeats")[0];
+ if (!repeatNode) {
+ return null;
+ }
+
+ let uid = repeatNode._uid;
+ let dupeNode = null;
+
+ if (node.category == CATEGORY_CSS ||
+ node.category == CATEGORY_SECURITY) {
+ dupeNode = this._repeatNodes[uid];
+ if (!dupeNode) {
+ this._repeatNodes[uid] = node;
+ }
+ } else if ((node.category == CATEGORY_WEBDEV ||
+ node.category == CATEGORY_JS) &&
+ node.category != CATEGORY_NETWORK &&
+ !node.classList.contains("inlined-variables-view")) {
+ let lastMessage = this.outputNode.lastChild;
+ if (!lastMessage) {
+ return null;
+ }
+
+ let lastRepeatNode =
+ lastMessage.getElementsByClassName("message-repeats")[0];
+ if (lastRepeatNode && lastRepeatNode._uid == uid) {
+ dupeNode = lastMessage;
+ }
+ }
+
+ if (dupeNode) {
+ this.mergeFilteredMessageNode(dupeNode);
+ // Even though this node was never rendered, we create the location
+ // nodes before rendering, so we still have to clean up any
+ // React components
+ this.unmountMessage(node);
+ return dupeNode;
+ }
+
+ return null;
+ },
+
+ /**
+ * Display cached messages that may have been collected before the UI is
+ * displayed.
+ *
+ * @param array remoteMessages
+ * Array of cached messages coming from the remote Web Console
+ * content instance.
+ */
+ displayCachedMessages: function (remoteMessages) {
+ if (!remoteMessages.length) {
+ return;
+ }
+
+ remoteMessages.forEach(function (message) {
+ switch (message._type) {
+ case "PageError": {
+ let category = Utils.categoryForScriptError(message);
+ this.outputMessage(category, this.reportPageError,
+ [category, message]);
+ break;
+ }
+ case "LogMessage":
+ this.handleLogMessage(message);
+ break;
+ case "ConsoleAPI":
+ this.outputMessage(CATEGORY_WEBDEV, this.logConsoleAPIMessage,
+ [message]);
+ break;
+ case "NetworkEvent":
+ this.outputMessage(CATEGORY_NETWORK, this.logNetEvent, [message]);
+ break;
+ }
+ }, this);
+ },
+
+ /**
+ * Logs a message to the Web Console that originates from the Web Console
+ * server.
+ *
+ * @param object message
+ * The message received from the server.
+ * @return nsIDOMElement|null
+ * The message element to display in the Web Console output.
+ */
+ logConsoleAPIMessage: function (message) {
+ let body = null;
+ let clipboardText = null;
+ let sourceURL = message.filename;
+ let sourceLine = message.lineNumber;
+ let level = message.level;
+ let args = message.arguments;
+ let objectActors = new Set();
+ let node = null;
+
+ // Gather the actor IDs.
+ args.forEach((value) => {
+ if (WebConsoleUtils.isActorGrip(value)) {
+ objectActors.add(value.actor);
+ }
+ });
+
+ switch (level) {
+ case "log":
+ case "info":
+ case "warn":
+ case "error":
+ case "exception":
+ case "assert":
+ case "debug": {
+ let msg = new Messages.ConsoleGeneric(message);
+ node = msg.init(this.output).render().element;
+ break;
+ }
+ case "table": {
+ let msg = new Messages.ConsoleTable(message);
+ node = msg.init(this.output).render().element;
+ break;
+ }
+ case "trace": {
+ let msg = new Messages.ConsoleTrace(message);
+ node = msg.init(this.output).render().element;
+ break;
+ }
+ case "clear": {
+ body = l10n.getStr("consoleCleared");
+ clipboardText = body;
+ break;
+ }
+ case "dir": {
+ body = { arguments: args };
+ let clipboardArray = [];
+ args.forEach((value) => {
+ clipboardArray.push(VariablesView.getString(value));
+ });
+ clipboardText = clipboardArray.join(" ");
+ break;
+ }
+ case "dirxml": {
+ // We just alias console.dirxml() with console.log().
+ message.level = "log";
+ return this.logConsoleAPIMessage(message);
+ }
+ case "group":
+ case "groupCollapsed":
+ clipboardText = body = message.groupName;
+ this.groupDepth++;
+ break;
+
+ case "groupEnd":
+ if (this.groupDepth > 0) {
+ this.groupDepth--;
+ }
+ break;
+
+ case "time": {
+ let timer = message.timer;
+ if (!timer) {
+ return null;
+ }
+ if (timer.error) {
+ console.error(new Error(l10n.getStr(timer.error)));
+ return null;
+ }
+ body = l10n.getFormatStr("timerStarted", [timer.name]);
+ clipboardText = body;
+ break;
+ }
+
+ case "timeEnd": {
+ let timer = message.timer;
+ if (!timer) {
+ return null;
+ }
+ let duration = Math.round(timer.duration * 100) / 100;
+ body = l10n.getFormatStr("timeEnd", [timer.name, duration]);
+ clipboardText = body;
+ break;
+ }
+
+ case "count": {
+ let counter = message.counter;
+ if (!counter) {
+ return null;
+ }
+ if (counter.error) {
+ console.error(l10n.getStr(counter.error));
+ return null;
+ }
+ let msg = new Messages.ConsoleGeneric(message);
+ node = msg.init(this.output).render().element;
+ break;
+ }
+
+ case "timeStamp": {
+ // console.timeStamp() doesn't need to display anything.
+ return null;
+ }
+
+ default:
+ console.error(new Error("Unknown Console API log level: " + level));
+ return null;
+ }
+
+ // Release object actors for arguments coming from console API methods that
+ // we ignore their arguments.
+ switch (level) {
+ case "group":
+ case "groupCollapsed":
+ case "groupEnd":
+ case "time":
+ case "timeEnd":
+ case "count":
+ for (let actor of objectActors) {
+ this._releaseObject(actor);
+ }
+ objectActors.clear();
+ }
+
+ if (level == "groupEnd") {
+ // no need to continue
+ return null;
+ }
+
+ if (!node) {
+ node = this.createMessageNode(CATEGORY_WEBDEV, LEVELS[level], body,
+ sourceURL, sourceLine, clipboardText,
+ level, message.timeStamp);
+ if (message.private) {
+ node.setAttribute("private", true);
+ }
+ }
+
+ if (objectActors.size > 0) {
+ node._objectActors = objectActors;
+
+ if (!node._messageObject) {
+ let repeatNode = node.getElementsByClassName("message-repeats")[0];
+ repeatNode._uid += [...objectActors].join("-");
+ }
+ }
+
+ let workerTypeID = CONSOLE_WORKER_IDS.indexOf(message.workerType);
+ if (workerTypeID != -1) {
+ node.workerType = WORKERTYPES_PREFKEYS[workerTypeID];
+ node.setAttribute("workerType", WORKERTYPES_PREFKEYS[workerTypeID]);
+ }
+
+ return node;
+ },
+
+ /**
+ * Handle ConsoleAPICall objects received from the server. This method outputs
+ * the window.console API call.
+ *
+ * @param object message
+ * The console API message received from the server.
+ */
+ handleConsoleAPICall: function (message) {
+ this.outputMessage(CATEGORY_WEBDEV, this.logConsoleAPIMessage, [message]);
+ },
+
+ /**
+ * Reports an error in the page source, either JavaScript or CSS.
+ *
+ * @param nsIScriptError scriptError
+ * The error message to report.
+ * @return nsIDOMElement|undefined
+ * The message element to display in the Web Console output.
+ */
+ reportPageError: function (category, scriptError) {
+ // Warnings and legacy strict errors become warnings; other types become
+ // errors.
+ let severity = "error";
+ if (scriptError.warning || scriptError.strict) {
+ severity = "warning";
+ } else if (scriptError.info) {
+ severity = "log";
+ }
+
+ switch (category) {
+ case CATEGORY_CSS:
+ category = "css";
+ break;
+ case CATEGORY_SECURITY:
+ category = "security";
+ break;
+ default:
+ category = "js";
+ break;
+ }
+
+ let objectActors = new Set();
+
+ // Gather the actor IDs.
+ for (let prop of ["errorMessage", "lineText"]) {
+ let grip = scriptError[prop];
+ if (WebConsoleUtils.isActorGrip(grip)) {
+ objectActors.add(grip.actor);
+ }
+ }
+
+ let errorMessage = scriptError.errorMessage;
+ if (errorMessage.type && errorMessage.type == "longString") {
+ errorMessage = errorMessage.initial;
+ }
+
+ let displayOrigin = scriptError.sourceName;
+
+ // TLS errors are related to the connection and not the resource; therefore
+ // it makes sense to only display the protcol, host and port (prePath).
+ // This also means messages are grouped for a single origin.
+ if (scriptError.category && scriptError.category == "SHA-1 Signature") {
+ let sourceURI = Services.io.newURI(scriptError.sourceName, null, null)
+ .QueryInterface(Ci.nsIURL);
+ displayOrigin = sourceURI.prePath;
+ }
+
+ // Create a new message
+ let msg = new Messages.Simple(errorMessage, {
+ location: {
+ url: displayOrigin,
+ line: scriptError.lineNumber,
+ column: scriptError.columnNumber
+ },
+ stack: scriptError.stacktrace,
+ category: category,
+ severity: severity,
+ timestamp: scriptError.timeStamp,
+ private: scriptError.private,
+ filterDuplicates: true
+ });
+
+ let node = msg.init(this.output).render().element;
+
+ // Select the body of the message node that is displayed in the console
+ let msgBody = node.getElementsByClassName("message-body")[0];
+
+ // Add the more info link node to messages that belong to certain categories
+ if (scriptError.exceptionDocURL) {
+ this.addLearnMoreWarningNode(msgBody, scriptError.exceptionDocURL);
+ }
+
+ // Collect telemetry data regarding JavaScript errors
+ this._telemetry.logKeyed("DEVTOOLS_JAVASCRIPT_ERROR_DISPLAYED",
+ scriptError.errorMessageName,
+ true);
+
+ if (objectActors.size > 0) {
+ node._objectActors = objectActors;
+ }
+
+ return node;
+ },
+
+ /**
+ * Handle PageError objects received from the server. This method outputs the
+ * given error.
+ *
+ * @param nsIScriptError pageError
+ * The error received from the server.
+ */
+ handlePageError: function (pageError) {
+ let category = Utils.categoryForScriptError(pageError);
+ this.outputMessage(category, this.reportPageError, [category, pageError]);
+ },
+
+ /**
+ * Handle log messages received from the server. This method outputs the given
+ * message.
+ *
+ * @param object packet
+ * The message packet received from the server.
+ */
+ handleLogMessage: function (packet) {
+ if (packet.message) {
+ this.outputMessage(CATEGORY_JS, this._reportLogMessage, [packet]);
+ }
+ },
+
+ /**
+ * Display log messages received from the server.
+ *
+ * @private
+ * @param object packet
+ * The message packet received from the server.
+ * @return nsIDOMElement
+ * The message element to render for the given log message.
+ */
+ _reportLogMessage: function (packet) {
+ let msg = packet.message;
+ if (msg.type && msg.type == "longString") {
+ msg = msg.initial;
+ }
+ let node = this.createMessageNode(CATEGORY_JS, SEVERITY_LOG, msg, null,
+ null, null, null, packet.timeStamp);
+ if (WebConsoleUtils.isActorGrip(packet.message)) {
+ node._objectActors = new Set([packet.message.actor]);
+ }
+ return node;
+ },
+
+ /**
+ * Log network event.
+ *
+ * @param object networkInfo
+ * The network request information to log.
+ * @return nsIDOMElement|null
+ * The message element to display in the Web Console output.
+ */
+ logNetEvent: function (networkInfo) {
+ let actorId = networkInfo.actor;
+ let request = networkInfo.request;
+ let clipboardText = request.method + " " + request.url;
+ let severity = SEVERITY_LOG;
+ if (networkInfo.isXHR) {
+ clipboardText = request.method + " XHR " + request.url;
+ severity = SEVERITY_INFO;
+ }
+ let mixedRequest =
+ WebConsoleUtils.isMixedHTTPSRequest(request.url, this.contentLocation);
+ if (mixedRequest) {
+ severity = SEVERITY_WARNING;
+ }
+
+ let methodNode = this.document.createElementNS(XHTML_NS, "span");
+ methodNode.className = "method";
+ methodNode.textContent = request.method + " ";
+
+ let messageNode = this.createMessageNode(CATEGORY_NETWORK, severity,
+ methodNode, null, null,
+ clipboardText, null,
+ networkInfo.timeStamp);
+ if (networkInfo.private) {
+ messageNode.setAttribute("private", true);
+ }
+ messageNode._connectionId = actorId;
+ messageNode.url = request.url;
+
+ let body = methodNode.parentNode;
+ body.setAttribute("aria-haspopup", true);
+
+ if (networkInfo.isXHR) {
+ let xhrNode = this.document.createElementNS(XHTML_NS, "span");
+ xhrNode.className = "xhr";
+ xhrNode.textContent = l10n.getStr("webConsoleXhrIndicator");
+ body.appendChild(xhrNode);
+ body.appendChild(this.document.createTextNode(" "));
+ }
+
+ let displayUrl = request.url;
+ let pos = displayUrl.indexOf("?");
+ if (pos > -1) {
+ displayUrl = displayUrl.substr(0, pos);
+ }
+
+ let urlNode = this.document.createElementNS(XHTML_NS, "a");
+ urlNode.className = "url";
+ urlNode.setAttribute("title", request.url);
+ urlNode.href = request.url;
+ urlNode.textContent = displayUrl;
+ urlNode.draggable = false;
+ body.appendChild(urlNode);
+ body.appendChild(this.document.createTextNode(" "));
+
+ if (mixedRequest) {
+ messageNode.classList.add("mixed-content");
+ this.makeMixedContentNode(body);
+ }
+
+ let statusNode = this.document.createElementNS(XHTML_NS, "a");
+ statusNode.className = "status";
+ body.appendChild(statusNode);
+
+ let onClick = () => this.openNetworkPanel(networkInfo.actor);
+
+ this._addMessageLinkCallback(urlNode, onClick);
+ this._addMessageLinkCallback(statusNode, onClick);
+
+ networkInfo.node = messageNode;
+
+ this._updateNetMessage(actorId);
+
+ if (this.window.NetRequest) {
+ this.window.NetRequest.onNetworkEvent({
+ consoleFrame: this,
+ response: networkInfo,
+ node: messageNode,
+ update: false
+ });
+ }
+
+ return messageNode;
+ },
+
+ /**
+ * Create a mixed content warning Node.
+ *
+ * @param linkNode
+ * Parent to the requested urlNode.
+ */
+ makeMixedContentNode: function (linkNode) {
+ let mixedContentWarning =
+ "[" + l10n.getStr("webConsoleMixedContentWarning") + "]";
+
+ // Mixed content warning message links to a Learn More page
+ let mixedContentWarningNode = this.document.createElementNS(XHTML_NS, "a");
+ mixedContentWarningNode.title = MIXED_CONTENT_LEARN_MORE;
+ mixedContentWarningNode.href = MIXED_CONTENT_LEARN_MORE;
+ mixedContentWarningNode.className = "learn-more-link";
+ mixedContentWarningNode.textContent = mixedContentWarning;
+ mixedContentWarningNode.draggable = false;
+
+ linkNode.appendChild(mixedContentWarningNode);
+
+ this._addMessageLinkCallback(mixedContentWarningNode, (event) => {
+ event.stopPropagation();
+ this.owner.openLink(MIXED_CONTENT_LEARN_MORE);
+ });
+ },
+
+ /*
+ * Appends a clickable warning node to the node passed
+ * as a parameter to the function. When a user clicks on the appended
+ * warning node, the browser navigates to the provided url.
+ *
+ * @param node
+ * The node to which we will be adding a clickable warning node.
+ * @param url
+ * The url which points to the page where the user can learn more
+ * about security issues associated with the specific message that's
+ * being logged.
+ */
+ addLearnMoreWarningNode: function (node, url) {
+ let moreInfoLabel = "[" + l10n.getStr("webConsoleMoreInfoLabel") + "]";
+
+ let warningNode = this.document.createElementNS(XHTML_NS, "a");
+ warningNode.title = url.split("?")[0];
+ warningNode.href = url;
+ warningNode.draggable = false;
+ warningNode.textContent = moreInfoLabel;
+ warningNode.className = "learn-more-link";
+
+ this._addMessageLinkCallback(warningNode, (event) => {
+ event.stopPropagation();
+ this.owner.openLink(url);
+ });
+
+ node.appendChild(warningNode);
+ },
+
+ /**
+ * Log file activity.
+ *
+ * @param string fileURI
+ * The file URI that was loaded.
+ * @return nsIDOMElement|undefined
+ * The message element to display in the Web Console output.
+ */
+ logFileActivity: function (fileURI) {
+ let urlNode = this.document.createElementNS(XHTML_NS, "a");
+ urlNode.setAttribute("title", fileURI);
+ urlNode.className = "url";
+ urlNode.textContent = fileURI;
+ urlNode.draggable = false;
+ urlNode.href = fileURI;
+
+ let outputNode = this.createMessageNode(CATEGORY_NETWORK, SEVERITY_LOG,
+ urlNode, null, null, fileURI);
+
+ this._addMessageLinkCallback(urlNode, () => {
+ this.owner.viewSource(fileURI);
+ });
+
+ return outputNode;
+ },
+
+ /**
+ * Handle the file activity messages coming from the remote Web Console.
+ *
+ * @param string fileURI
+ * The file URI that was requested.
+ */
+ handleFileActivity: function (fileURI) {
+ this.outputMessage(CATEGORY_NETWORK, this.logFileActivity, [fileURI]);
+ },
+
+ /**
+ * Handle the reflow activity messages coming from the remote Web Console.
+ *
+ * @param object msg
+ * An object holding information about a reflow batch.
+ */
+ logReflowActivity: function (message) {
+ let {start, end, sourceURL, sourceLine} = message;
+ let duration = Math.round((end - start) * 100) / 100;
+ let node = this.document.createElementNS(XHTML_NS, "span");
+ if (sourceURL) {
+ node.textContent =
+ l10n.getFormatStr("reflow.messageWithLink", [duration]);
+ let a = this.document.createElementNS(XHTML_NS, "a");
+ a.href = "#";
+ a.draggable = "false";
+ let filename = getSourceNames(sourceURL).short;
+ let functionName = message.functionName ||
+ l10n.getStr("stacktrace.anonymousFunction");
+ a.textContent = l10n.getFormatStr("reflow.messageLinkText",
+ [functionName, filename, sourceLine]);
+ this._addMessageLinkCallback(a, () => {
+ this.owner.viewSourceInDebugger(sourceURL, sourceLine);
+ });
+ node.appendChild(a);
+ } else {
+ node.textContent =
+ l10n.getFormatStr("reflow.messageWithNoLink", [duration]);
+ }
+ return this.createMessageNode(CATEGORY_CSS, SEVERITY_LOG, node);
+ },
+
+ handleReflowActivity: function (message) {
+ this.outputMessage(CATEGORY_CSS, this.logReflowActivity, [message]);
+ },
+
+ /**
+ * Inform user that the window.console API has been replaced by a script
+ * in a content page.
+ */
+ logWarningAboutReplacedAPI: function () {
+ let node = this.createMessageNode(CATEGORY_JS, SEVERITY_WARNING,
+ l10n.getStr("ConsoleAPIDisabled"));
+ this.outputMessage(CATEGORY_JS, node);
+ },
+
+ /**
+ * Handle the network events coming from the remote Web Console.
+ *
+ * @param object networkInfo
+ * The network request information.
+ */
+ handleNetworkEvent: function (networkInfo) {
+ this.outputMessage(CATEGORY_NETWORK, this.logNetEvent, [networkInfo]);
+ },
+
+ /**
+ * Handle network event updates coming from the server.
+ *
+ * @param object networkInfo
+ * The network request information.
+ * @param object packet
+ * Update details.
+ */
+ handleNetworkEventUpdate: function (networkInfo, packet) {
+ if (networkInfo.node && this._updateNetMessage(packet.from)) {
+ if (this.window.NetRequest) {
+ this.window.NetRequest.onNetworkEvent({
+ client: this.webConsoleClient,
+ response: packet,
+ node: networkInfo.node,
+ update: true
+ });
+ }
+
+ this.emit("new-messages", new Set([{
+ update: true,
+ node: networkInfo.node,
+ response: packet,
+ }]));
+ }
+
+ // For unit tests we pass the HTTP activity object to the test callback,
+ // once requests complete.
+ if (this.owner.lastFinishedRequestCallback &&
+ networkInfo.updates.indexOf("responseContent") > -1 &&
+ networkInfo.updates.indexOf("eventTimings") > -1) {
+ this.owner.lastFinishedRequestCallback(networkInfo, this);
+ }
+ },
+
+ /**
+ * Update an output message to reflect the latest state of a network request,
+ * given a network event actor ID.
+ *
+ * @private
+ * @param string actorId
+ * The network event actor ID for which you want to update the message.
+ * @return boolean
+ * |true| if the message node was updated, or |false| otherwise.
+ */
+ _updateNetMessage: function (actorId) {
+ let networkInfo = this.webConsoleClient.getNetworkRequest(actorId);
+ if (!networkInfo || !networkInfo.node) {
+ return false;
+ }
+
+ let messageNode = networkInfo.node;
+ let updates = networkInfo.updates;
+ let hasEventTimings = updates.indexOf("eventTimings") > -1;
+ let hasResponseStart = updates.indexOf("responseStart") > -1;
+ let request = networkInfo.request;
+ let methodText = (networkInfo.isXHR) ?
+ request.method + " XHR" : request.method;
+ let response = networkInfo.response;
+ let updated = false;
+
+ if (hasEventTimings || hasResponseStart) {
+ let status = [];
+ if (response.httpVersion && response.status) {
+ status = [response.httpVersion, response.status, response.statusText];
+ }
+ if (hasEventTimings) {
+ status.push(l10n.getFormatStr("NetworkPanel.durationMS",
+ [networkInfo.totalTime]));
+ }
+ let statusText = "[" + status.join(" ") + "]";
+
+ let statusNode = messageNode.getElementsByClassName("status")[0];
+ statusNode.textContent = statusText;
+
+ messageNode.clipboardText = [methodText, request.url, statusText]
+ .join(" ");
+
+ if (hasResponseStart && response.status >= MIN_HTTP_ERROR_CODE &&
+ response.status <= MAX_HTTP_ERROR_CODE) {
+ this.setMessageType(messageNode, CATEGORY_NETWORK, SEVERITY_ERROR);
+ }
+
+ updated = true;
+ }
+
+ if (messageNode._netPanel) {
+ messageNode._netPanel.update();
+ }
+
+ return updated;
+ },
+
+ /**
+ * Opens the network monitor and highlights the specified request.
+ *
+ * @param string requestId
+ * The actor ID of the network request.
+ */
+ openNetworkPanel: function (requestId) {
+ let toolbox = gDevTools.getToolbox(this.owner.target);
+ // The browser console doesn't have a toolbox.
+ if (!toolbox) {
+ return;
+ }
+ return toolbox.selectTool("netmonitor").then(panel => {
+ return panel.panelWin.NetMonitorController.inspectRequest(requestId);
+ });
+ },
+
+ /**
+ * Handler for page location changes.
+ *
+ * @param string uri
+ * New page location.
+ * @param string title
+ * New page title.
+ */
+ onLocationChange: function (uri, title) {
+ this.contentLocation = uri;
+ if (this.owner.onLocationChange) {
+ this.owner.onLocationChange(uri, title);
+ }
+ },
+
+ /**
+ * Handler for the tabNavigated notification.
+ *
+ * @param string event
+ * Event name.
+ * @param object packet
+ * Notification packet received from the server.
+ */
+ handleTabNavigated: function (event, packet) {
+ if (event == "will-navigate") {
+ if (this.persistLog) {
+ if (this.NEW_CONSOLE_OUTPUT_ENABLED) {
+ // Add a _type to hit convertCachedPacket.
+ packet._type = true;
+ this.newConsoleOutput.dispatchMessageAdd(packet);
+ } else {
+ let marker = new Messages.NavigationMarker(packet, Date.now());
+ this.output.addMessage(marker);
+ }
+ } else {
+ this.jsterm.clearOutput();
+ }
+ }
+
+ if (packet.url) {
+ this.onLocationChange(packet.url, packet.title);
+ }
+
+ if (event == "navigate" && !packet.nativeConsoleAPI) {
+ this.logWarningAboutReplacedAPI();
+ }
+ },
+
+ /**
+ * Output a message node. This filters a node appropriately, then sends it to
+ * the output, regrouping and pruning output as necessary.
+ *
+ * Note: this call is async - the given message node may not be displayed when
+ * you call this method.
+ *
+ * @param integer category
+ * The category of the message you want to output. See the CATEGORY_*
+ * constants.
+ * @param function|nsIDOMElement methodOrNode
+ * The method that creates the message element to send to the output or
+ * the actual element. If a method is given it will be bound to the HUD
+ * object and the arguments will be |args|.
+ * @param array [args]
+ * If a method is given to output the message element then the method
+ * will be invoked with the list of arguments given here. The last
+ * object in this array should be the packet received from the
+ * back end.
+ */
+ outputMessage: function (category, methodOrNode, args) {
+ if (!this._outputQueue.length) {
+ // If the queue is empty we consider that now was the last output flush.
+ // This avoid an immediate output flush when the timer executes.
+ this._lastOutputFlush = Date.now();
+ }
+
+ this._outputQueue.push([category, methodOrNode, args]);
+
+ this._initOutputTimer();
+ },
+
+ /**
+ * Try to flush the output message queue. This takes the messages in the
+ * output queue and displays them. Outputting stops at MESSAGES_IN_INTERVAL.
+ * Further output is queued to happen later - see OUTPUT_INTERVAL.
+ *
+ * @private
+ */
+ _flushMessageQueue: function () {
+ this._outputTimerInitialized = false;
+ if (!this._outputTimer) {
+ return;
+ }
+
+ let startTime = Date.now();
+ let timeSinceFlush = startTime - this._lastOutputFlush;
+ let shouldThrottle = this._outputQueue.length > MESSAGES_IN_INTERVAL &&
+ timeSinceFlush < THROTTLE_UPDATES;
+
+ // Determine how many messages we can display now.
+ let toDisplay = Math.min(this._outputQueue.length, MESSAGES_IN_INTERVAL);
+
+ // If there aren't any messages to display (because of throttling or an
+ // empty queue), then take care of some cleanup. Destroy items that were
+ // pruned from the outputQueue before being displayed.
+ if (shouldThrottle || toDisplay < 1) {
+ while (this._itemDestroyQueue.length) {
+ if ((Date.now() - startTime) > MAX_CLEANUP_TIME) {
+ break;
+ }
+ this._destroyItem(this._itemDestroyQueue.pop());
+ }
+
+ this._initOutputTimer();
+ return;
+ }
+
+ // Try to prune the message queue.
+ let shouldPrune = false;
+ if (this._outputQueue.length > toDisplay && this._pruneOutputQueue()) {
+ toDisplay = Math.min(this._outputQueue.length, toDisplay);
+ shouldPrune = true;
+ }
+
+ let batch = this._outputQueue.splice(0, toDisplay);
+ let outputNode = this.outputNode;
+ let lastVisibleNode = null;
+ let scrollNode = this.outputWrapper;
+ let hudIdSupportsString = WebConsoleUtils.supportsString(this.hudId);
+
+ // We won't bother to try to restore scroll position if this is showing
+ // a lot of messages at once (and there are still items in the queue).
+ // It is going to purge whatever you were looking at anyway.
+ let scrolledToBottom =
+ shouldPrune || Utils.isOutputScrolledToBottom(outputNode, scrollNode);
+
+ // Output the current batch of messages.
+ let messages = new Set();
+ for (let i = 0; i < batch.length; i++) {
+ let item = batch[i];
+ let result = this._outputMessageFromQueue(hudIdSupportsString, item);
+ if (result) {
+ messages.add({
+ node: result.isRepeated ? result.isRepeated : result.node,
+ response: result.message,
+ update: !!result.isRepeated,
+ });
+
+ if (result.visible && result.node == this.outputNode.lastChild) {
+ lastVisibleNode = result.node;
+ }
+ }
+ }
+
+ let oldScrollHeight = 0;
+ let removedNodes = 0;
+
+ // Prune messages from the DOM, but only if needed.
+ if (shouldPrune || !this._outputQueue.length) {
+ // Only bother measuring the scrollHeight if not scrolled to bottom,
+ // since the oldScrollHeight will not be used if it is.
+ if (!scrolledToBottom) {
+ oldScrollHeight = scrollNode.scrollHeight;
+ }
+
+ let categories = Object.keys(this._pruneCategoriesQueue);
+ categories.forEach(function _pruneOutput(category) {
+ removedNodes += this.pruneOutputIfNecessary(category);
+ }, this);
+ this._pruneCategoriesQueue = {};
+ }
+
+ let isInputOutput = lastVisibleNode &&
+ (lastVisibleNode.category == CATEGORY_INPUT ||
+ lastVisibleNode.category == CATEGORY_OUTPUT);
+
+ // Scroll to the new node if it is not filtered, and if the output node is
+ // scrolled at the bottom or if the new node is a jsterm input/output
+ // message.
+ if (lastVisibleNode && (scrolledToBottom || isInputOutput)) {
+ Utils.scrollToVisible(lastVisibleNode);
+ } else if (!scrolledToBottom && removedNodes > 0 &&
+ oldScrollHeight != scrollNode.scrollHeight) {
+ // If there were pruned messages and if scroll is not at the bottom, then
+ // we need to adjust the scroll location.
+ scrollNode.scrollTop -= oldScrollHeight - scrollNode.scrollHeight;
+ }
+
+ if (messages.size) {
+ this.emit("new-messages", messages);
+ }
+
+ // If the output queue is empty, then run _flushCallback.
+ if (this._outputQueue.length === 0 && this._flushCallback) {
+ if (this._flushCallback() === false) {
+ this._flushCallback = null;
+ }
+ }
+
+ this._initOutputTimer();
+
+ // Resize the output area in case a vertical scrollbar has been added
+ this.resize();
+
+ this._lastOutputFlush = Date.now();
+ },
+
+ /**
+ * Initialize the output timer.
+ * @private
+ */
+ _initOutputTimer: function () {
+ let panelIsDestroyed = !this._outputTimer;
+ let alreadyScheduled = this._outputTimerInitialized;
+ let nothingToDo = !this._itemDestroyQueue.length &&
+ !this._outputQueue.length;
+
+ // Don't schedule a callback in the following cases:
+ if (panelIsDestroyed || alreadyScheduled || nothingToDo) {
+ return;
+ }
+
+ this._outputTimerInitialized = true;
+ this._outputTimer.initWithCallback(this._flushMessageQueue,
+ OUTPUT_INTERVAL,
+ Ci.nsITimer.TYPE_ONE_SHOT);
+ },
+
+ /**
+ * Output a message from the queue.
+ *
+ * @private
+ * @param nsISupportsString hudIdSupportsString
+ * The HUD ID as an nsISupportsString.
+ * @param array item
+ * An item from the output queue - this item represents a message.
+ * @return object
+ * An object that holds the following properties:
+ * - node: the DOM element of the message.
+ * - isRepeated: the DOM element of the original message, if this is
+ * a repeated message, otherwise null.
+ * - visible: boolean that tells if the message is visible.
+ */
+ _outputMessageFromQueue: function (hudIdSupportsString, item) {
+ let [, methodOrNode, args] = item;
+
+ // The last object in the args array should be message
+ // object or response packet received from the server.
+ let message = (args && args.length) ? args[args.length - 1] : null;
+
+ let node = typeof methodOrNode == "function" ?
+ methodOrNode.apply(this, args || []) :
+ methodOrNode;
+ if (!node) {
+ return null;
+ }
+
+ let isFiltered = this.filterMessageNode(node);
+
+ let isRepeated = this._filterRepeatedMessage(node);
+
+ // If a clear message is processed while the webconsole is opened, the UI
+ // should be cleared.
+ // Do not clear the output if the current frame is owned by a Browser Console.
+ if (message && message.level == "clear" && !this.isBrowserConsole) {
+ // Do not clear the consoleStorage here as it has been cleared already
+ // by the clear method, only clear the UI.
+ this.jsterm.clearOutput(false);
+ }
+
+ let visible = !isRepeated && !isFiltered;
+ if (!isRepeated) {
+ this.outputNode.appendChild(node);
+ this._pruneCategoriesQueue[node.category] = true;
+
+ let nodeID = node.getAttribute("id");
+ Services.obs.notifyObservers(hudIdSupportsString,
+ "web-console-message-created", nodeID);
+ }
+
+ if (node._onOutput) {
+ node._onOutput();
+ delete node._onOutput;
+ }
+
+ return {
+ visible: visible,
+ node: node,
+ isRepeated: isRepeated,
+ message: message
+ };
+ },
+
+ /**
+ * Prune the queue of messages to display. This avoids displaying messages
+ * that will be removed at the end of the queue anyway.
+ * @private
+ */
+ _pruneOutputQueue: function () {
+ let nodes = {};
+
+ // Group the messages per category.
+ this._outputQueue.forEach(function (item, index) {
+ let [category] = item;
+ if (!(category in nodes)) {
+ nodes[category] = [];
+ }
+ nodes[category].push(index);
+ }, this);
+
+ let pruned = 0;
+
+ // Loop through the categories we found and prune if needed.
+ for (let category in nodes) {
+ let limit = Utils.logLimitForCategory(category);
+ let indexes = nodes[category];
+ if (indexes.length > limit) {
+ let n = Math.max(0, indexes.length - limit);
+ pruned += n;
+ for (let i = n - 1; i >= 0; i--) {
+ this._itemDestroyQueue.push(this._outputQueue[indexes[i]]);
+ this._outputQueue.splice(indexes[i], 1);
+ }
+ }
+ }
+
+ return pruned;
+ },
+
+ /**
+ * Destroy an item that was once in the outputQueue but isn't needed
+ * after all.
+ *
+ * @private
+ * @param array item
+ * The item you want to destroy. Does not remove it from the output
+ * queue.
+ */
+ _destroyItem: function (item) {
+ // TODO: handle object releasing in a more elegant way once all console
+ // messages use the new API - bug 778766.
+ let [category, methodOrNode, args] = item;
+ if (typeof methodOrNode != "function" && methodOrNode._objectActors) {
+ for (let actor of methodOrNode._objectActors) {
+ this._releaseObject(actor);
+ }
+ methodOrNode._objectActors.clear();
+ }
+
+ if (methodOrNode == this.output._flushMessageQueue &&
+ args[0]._objectActors) {
+ for (let arg of args) {
+ if (!arg._objectActors) {
+ continue;
+ }
+ for (let actor of arg._objectActors) {
+ this._releaseObject(actor);
+ }
+ arg._objectActors.clear();
+ }
+ }
+
+ if (category == CATEGORY_NETWORK) {
+ let connectionId = null;
+ if (methodOrNode == this.logNetEvent) {
+ connectionId = args[0].actor;
+ } else if (typeof methodOrNode != "function") {
+ connectionId = methodOrNode._connectionId;
+ }
+ if (connectionId &&
+ this.webConsoleClient.hasNetworkRequest(connectionId)) {
+ this.webConsoleClient.removeNetworkRequest(connectionId);
+ this._releaseObject(connectionId);
+ }
+ } else if (category == CATEGORY_WEBDEV &&
+ methodOrNode == this.logConsoleAPIMessage) {
+ args[0].arguments.forEach((value) => {
+ if (WebConsoleUtils.isActorGrip(value)) {
+ this._releaseObject(value.actor);
+ }
+ });
+ } else if (category == CATEGORY_JS &&
+ methodOrNode == this.reportPageError) {
+ let pageError = args[1];
+ for (let prop of ["errorMessage", "lineText"]) {
+ let grip = pageError[prop];
+ if (WebConsoleUtils.isActorGrip(grip)) {
+ this._releaseObject(grip.actor);
+ }
+ }
+ } else if (category == CATEGORY_JS &&
+ methodOrNode == this._reportLogMessage) {
+ if (WebConsoleUtils.isActorGrip(args[0].message)) {
+ this._releaseObject(args[0].message.actor);
+ }
+ }
+ },
+
+ /**
+ * Cleans up a message via a node that may or may not
+ * have actually been rendered in the DOM. Currently, only
+ * cleans up React components.
+ *
+ * @param nsIDOMNode node
+ * The message node you want to clean up.
+ */
+ unmountMessage(node) {
+ // Unmount the Frame component with the message location
+ let locationNode = node.querySelector(".message-location");
+ if (locationNode) {
+ this.ReactDOM.unmountComponentAtNode(locationNode);
+ }
+
+ // Unmount the StackTrace component if present in the message
+ let stacktraceNode = node.querySelector(".stacktrace");
+ if (stacktraceNode) {
+ this.ReactDOM.unmountComponentAtNode(stacktraceNode);
+ }
+ },
+
+ /**
+ * Ensures that the number of message nodes of type category don't exceed that
+ * category's line limit by removing old messages as needed.
+ *
+ * @param integer category
+ * The category of message nodes to prune if needed.
+ * @return number
+ * The number of removed nodes.
+ */
+ pruneOutputIfNecessary: function (category) {
+ let logLimit = Utils.logLimitForCategory(category);
+ let messageNodes = this.outputNode.querySelectorAll(".message[category=" +
+ CATEGORY_CLASS_FRAGMENTS[category] + "]");
+ let n = Math.max(0, messageNodes.length - logLimit);
+ [...messageNodes].slice(0, n).forEach(this.removeOutputMessage, this);
+ return n;
+ },
+
+ /**
+ * Remove a given message from the output.
+ *
+ * @param nsIDOMNode node
+ * The message node you want to remove.
+ */
+ removeOutputMessage: function (node) {
+ if (node._messageObject) {
+ node._messageObject.destroy();
+ }
+
+ if (node._objectActors) {
+ for (let actor of node._objectActors) {
+ this._releaseObject(actor);
+ }
+ node._objectActors.clear();
+ }
+
+ if (node.category == CATEGORY_CSS ||
+ node.category == CATEGORY_SECURITY) {
+ let repeatNode = node.getElementsByClassName("message-repeats")[0];
+ if (repeatNode && repeatNode._uid) {
+ delete this._repeatNodes[repeatNode._uid];
+ }
+ } else if (node._connectionId &&
+ node.category == CATEGORY_NETWORK) {
+ this.webConsoleClient.removeNetworkRequest(node._connectionId);
+ this._releaseObject(node._connectionId);
+ } else if (node.classList.contains("inlined-variables-view")) {
+ let view = node._variablesView;
+ if (view) {
+ view.controller.releaseActors();
+ }
+ node._variablesView = null;
+ }
+
+ this.unmountMessage(node);
+
+ node.remove();
+ },
+
+ /**
+ * Given a category and message body, creates a DOM node to represent an
+ * incoming message. The timestamp is automatically added.
+ *
+ * @param number category
+ * The category of the message: one of the CATEGORY_* constants.
+ * @param number severity
+ * The severity of the message: one of the SEVERITY_* constants;
+ * @param string|nsIDOMNode body
+ * The body of the message, either a simple string or a DOM node.
+ * @param string sourceURL [optional]
+ * The URL of the source file that emitted the error.
+ * @param number sourceLine [optional]
+ * The line number on which the error occurred. If zero or omitted,
+ * there is no line number associated with this message.
+ * @param string clipboardText [optional]
+ * The text that should be copied to the clipboard when this node is
+ * copied. If omitted, defaults to the body text. If `body` is not
+ * a string, then the clipboard text must be supplied.
+ * @param number level [optional]
+ * The level of the console API message.
+ * @param number timestamp [optional]
+ * The timestamp to use for this message node. If omitted, the current
+ * date and time is used.
+ * @return nsIDOMNode
+ * The message node: a DIV ready to be inserted into the Web Console
+ * output node.
+ */
+ createMessageNode: function (category, severity, body, sourceURL, sourceLine,
+ clipboardText, level, timestamp) {
+ if (typeof body != "string" && clipboardText == null && body.innerText) {
+ clipboardText = body.innerText;
+ }
+
+ let indentNode = this.document.createElementNS(XHTML_NS, "span");
+ indentNode.className = "indent";
+
+ // Apply the current group by indenting appropriately.
+ let indent = this.groupDepth * GROUP_INDENT;
+ indentNode.style.width = indent + "px";
+
+ // Make the icon container, which is a vertical box. Its purpose is to
+ // ensure that the icon stays anchored at the top of the message even for
+ // long multi-line messages.
+ let iconContainer = this.document.createElementNS(XHTML_NS, "span");
+ iconContainer.className = "icon";
+
+ // Create the message body, which contains the actual text of the message.
+ let bodyNode = this.document.createElementNS(XHTML_NS, "span");
+ bodyNode.className = "message-body-wrapper message-body devtools-monospace";
+
+ // Store the body text, since it is needed later for the variables view.
+ let storedBody = body;
+ // If a string was supplied for the body, turn it into a DOM node and an
+ // associated clipboard string now.
+ clipboardText = clipboardText ||
+ (body + (sourceURL ? " @ " + sourceURL : "") +
+ (sourceLine ? ":" + sourceLine : ""));
+
+ timestamp = timestamp || Date.now();
+
+ // Create the containing node and append all its elements to it.
+ let node = this.document.createElementNS(XHTML_NS, "div");
+ node.id = "console-msg-" + gSequenceId();
+ node.className = "message";
+ node.clipboardText = clipboardText;
+ node.timestamp = timestamp;
+ this.setMessageType(node, category, severity);
+
+ if (body instanceof Ci.nsIDOMNode) {
+ bodyNode.appendChild(body);
+ } else {
+ let str = undefined;
+ if (level == "dir") {
+ str = VariablesView.getString(body.arguments[0]);
+ } else {
+ str = body;
+ }
+
+ if (str !== undefined) {
+ body = this.document.createTextNode(str);
+ bodyNode.appendChild(body);
+ }
+ }
+
+ // Add the message repeats node only when needed.
+ let repeatNode = null;
+ if (category != CATEGORY_INPUT &&
+ category != CATEGORY_OUTPUT &&
+ category != CATEGORY_NETWORK &&
+ !(category == CATEGORY_CSS && severity == SEVERITY_LOG)) {
+ repeatNode = this.document.createElementNS(XHTML_NS, "span");
+ repeatNode.setAttribute("value", "1");
+ repeatNode.className = "message-repeats";
+ repeatNode.textContent = 1;
+ repeatNode._uid = [bodyNode.textContent, category, severity, level,
+ sourceURL, sourceLine].join(":");
+ }
+
+ // Create the timestamp.
+ let timestampNode = this.document.createElementNS(XHTML_NS, "span");
+ timestampNode.className = "timestamp devtools-monospace";
+
+ let timestampString = l10n.timestampString(timestamp);
+ timestampNode.textContent = timestampString + " ";
+
+ // Create the source location (e.g. www.example.com:6) that sits on the
+ // right side of the message, if applicable.
+ let locationNode;
+ if (sourceURL && IGNORED_SOURCE_URLS.indexOf(sourceURL) == -1) {
+ locationNode = this.createLocationNode({url: sourceURL,
+ line: sourceLine});
+ }
+
+ node.appendChild(timestampNode);
+ node.appendChild(indentNode);
+ node.appendChild(iconContainer);
+
+ // Display the variables view after the message node.
+ if (level == "dir") {
+ let options = {
+ objectActor: storedBody.arguments[0],
+ targetElement: bodyNode,
+ hideFilterInput: true,
+ };
+ this.jsterm.openVariablesView(options).then((view) => {
+ node._variablesView = view;
+ if (node.classList.contains("hidden-message")) {
+ node.classList.remove("hidden-message");
+ }
+ });
+
+ node.classList.add("inlined-variables-view");
+ }
+
+ node.appendChild(bodyNode);
+ if (repeatNode) {
+ node.appendChild(repeatNode);
+ }
+ if (locationNode) {
+ node.appendChild(locationNode);
+ }
+ node.appendChild(this.document.createTextNode("\n"));
+
+ return node;
+ },
+
+ /**
+ * Creates the anchor that displays the textual location of an incoming
+ * message.
+ *
+ * @param {Object} location
+ * An object containing url, line and column number of the message source.
+ * @return {Element}
+ * The new anchor element, ready to be added to the message node.
+ */
+ createLocationNode: function (location) {
+ let locationNode = this.document.createElementNS(XHTML_NS, "div");
+ locationNode.className = "message-location devtools-monospace";
+
+ // Make the location clickable.
+ let onClick = ({ url, line }) => {
+ let category = locationNode.closest(".message").category;
+ let target = null;
+
+ if (/^Scratchpad\/\d+$/.test(url)) {
+ target = "scratchpad";
+ } else if (category === CATEGORY_CSS) {
+ target = "styleeditor";
+ } else if (category === CATEGORY_JS || category === CATEGORY_WEBDEV) {
+ target = "jsdebugger";
+ } else if (/\.js$/.test(url)) {
+ // If it ends in .js, let's attempt to open in debugger
+ // anyway, as this falls back to normal view-source.
+ target = "jsdebugger";
+ } else {
+ // Point everything else to debugger, if source not available,
+ // it will fall back to view-source.
+ target = "jsdebugger";
+ }
+
+ switch (target) {
+ case "scratchpad":
+ this.owner.viewSourceInScratchpad(url, line);
+ return;
+ case "jsdebugger":
+ this.owner.viewSourceInDebugger(url, line);
+ return;
+ case "styleeditor":
+ this.owner.viewSourceInStyleEditor(url, line);
+ return;
+ }
+ // No matching tool found; use old school view-source
+ this.owner.viewSource(url, line);
+ };
+
+ const toolbox = gDevTools.getToolbox(this.owner.target);
+
+ let { url, line, column } = location;
+ let source = url ? url.split(" -> ").pop() : "";
+
+ this.ReactDOM.render(this.FrameView({
+ frame: { source, line, column },
+ showEmptyPathAsHost: true,
+ onClick,
+ sourceMapService: toolbox ? toolbox._sourceMapService : null,
+ }), locationNode);
+
+ return locationNode;
+ },
+
+ /**
+ * Adjusts the category and severity of the given message.
+ *
+ * @param nsIDOMNode messageNode
+ * The message node to alter.
+ * @param number category
+ * The category for the message; one of the CATEGORY_ constants.
+ * @param number severity
+ * The severity for the message; one of the SEVERITY_ constants.
+ * @return void
+ */
+ setMessageType: function (messageNode, category, severity) {
+ messageNode.category = category;
+ messageNode.severity = severity;
+ messageNode.setAttribute("category", CATEGORY_CLASS_FRAGMENTS[category]);
+ messageNode.setAttribute("severity", SEVERITY_CLASS_FRAGMENTS[severity]);
+ messageNode.setAttribute("filter",
+ MESSAGE_PREFERENCE_KEYS[category][severity]);
+ },
+
+ /**
+ * Add the mouse event handlers needed to make a link.
+ *
+ * @private
+ * @param nsIDOMNode node
+ * The node for which you want to add the event handlers.
+ * @param function callback
+ * The function you want to invoke on click.
+ */
+ _addMessageLinkCallback: function (node, callback) {
+ node.addEventListener("mousedown", (event) => {
+ this._mousedown = true;
+ this._startX = event.clientX;
+ this._startY = event.clientY;
+ }, false);
+
+ node.addEventListener("click", (event) => {
+ let mousedown = this._mousedown;
+ this._mousedown = false;
+
+ event.preventDefault();
+
+ // Do not allow middle/right-click or 2+ clicks.
+ if (event.detail != 1 || event.button != 0) {
+ return;
+ }
+
+ // If this event started with a mousedown event and it ends at a different
+ // location, we consider this text selection.
+ if (mousedown &&
+ (this._startX != event.clientX) &&
+ (this._startY != event.clientY)) {
+ this._startX = this._startY = undefined;
+ return;
+ }
+
+ this._startX = this._startY = undefined;
+
+ callback.call(this, event);
+ }, false);
+ },
+
+ /**
+ * Handler for the pref-changed event coming from the toolbox.
+ * Currently this function only handles the timestamps preferences.
+ *
+ * @private
+ * @param object event
+ * This parameter is a string that holds the event name
+ * pref-changed in this case.
+ * @param object data
+ * This is the pref-changed data object.
+ */
+ _onToolboxPrefChanged: function (event, data) {
+ if (data.pref == PREF_MESSAGE_TIMESTAMP) {
+ if (data.newValue) {
+ this.outputNode.classList.remove("hideTimestamps");
+ } else {
+ this.outputNode.classList.add("hideTimestamps");
+ }
+ }
+ },
+
+ /**
+ * Copies the selected items to the system clipboard.
+ *
+ * @param object options
+ * - linkOnly:
+ * An optional flag to copy only URL without other meta-information.
+ * Default is false.
+ * - contextmenu:
+ * An optional flag to copy the last clicked item which brought
+ * up the context menu if nothing is selected. Default is false.
+ */
+ copySelectedItems: function (options) {
+ options = options || { linkOnly: false, contextmenu: false };
+
+ // Gather up the selected items and concatenate their clipboard text.
+ let strings = [];
+
+ let children = this.output.getSelectedMessages();
+ if (!children.length && options.contextmenu) {
+ children = [this._contextMenuHandler.lastClickedMessage];
+ }
+
+ for (let item of children) {
+ // Ensure the selected item hasn't been filtered by type or string.
+ if (!item.classList.contains("filtered-by-type") &&
+ !item.classList.contains("filtered-by-string")) {
+ if (options.linkOnly) {
+ strings.push(item.url);
+ } else {
+ strings.push(item.clipboardText);
+ }
+ }
+ }
+
+ clipboardHelper.copyString(strings.join("\n"));
+ },
+
+ /**
+ * Object properties provider. This function gives you the properties of the
+ * remote object you want.
+ *
+ * @param string actor
+ * The object actor ID from which you want the properties.
+ * @param function callback
+ * Function you want invoked once the properties are received.
+ */
+ objectPropertiesProvider: function (actor, callback) {
+ this.webConsoleClient.inspectObjectProperties(actor,
+ function (response) {
+ if (response.error) {
+ console.error("Failed to retrieve the object properties from the " +
+ "server. Error: " + response.error);
+ return;
+ }
+ callback(response.properties);
+ });
+ },
+
+ /**
+ * Release an actor.
+ *
+ * @private
+ * @param string actor
+ * The actor ID you want to release.
+ */
+ _releaseObject: function (actor) {
+ if (this.proxy) {
+ this.proxy.releaseActor(actor);
+ }
+ },
+
+ /**
+ * Open the selected item's URL in a new tab.
+ */
+ openSelectedItemInTab: function () {
+ let item = this.output.getSelectedMessages(1)[0] ||
+ this._contextMenuHandler.lastClickedMessage;
+
+ if (!item || !item.url) {
+ return;
+ }
+
+ this.owner.openLink(item.url);
+ },
+
+ /**
+ * Destroy the WebConsoleFrame object. Call this method to avoid memory leaks
+ * when the Web Console is closed.
+ *
+ * @return object
+ * A promise that is resolved when the WebConsoleFrame instance is
+ * destroyed.
+ */
+ destroy: function () {
+ if (this._destroyer) {
+ return this._destroyer.promise;
+ }
+
+ this._destroyer = promise.defer();
+
+ let toolbox = gDevTools.getToolbox(this.owner.target);
+ if (toolbox) {
+ toolbox.off("webconsole-selected", this._onPanelSelected);
+ }
+
+ gDevTools.off("pref-changed", this._onToolboxPrefChanged);
+ this.window.removeEventListener("resize", this.resize, true);
+
+ this._repeatNodes = {};
+ this._outputQueue.forEach(this._destroyItem, this);
+ this._outputQueue = [];
+ this._itemDestroyQueue.forEach(this._destroyItem, this);
+ this._itemDestroyQueue = [];
+ this._pruneCategoriesQueue = {};
+ this.webConsoleClient.clearNetworkRequests();
+
+ // Unmount any currently living frame components in DOM, since
+ // currently we only clean up messages in `this.removeOutputMessage`,
+ // via `this.pruneOutputIfNecessary`.
+ let liveMessages = this.outputNode.querySelectorAll(".message");
+ Array.prototype.forEach.call(liveMessages, this.unmountMessage);
+
+ if (this._outputTimerInitialized) {
+ this._outputTimerInitialized = false;
+ this._outputTimer.cancel();
+ }
+ this._outputTimer = null;
+ if (this.jsterm) {
+ this.jsterm.off("sidebar-opened", this.resize);
+ this.jsterm.off("sidebar-closed", this.resize);
+ this.jsterm.destroy();
+ this.jsterm = null;
+ }
+ this.output.destroy();
+ this.output = null;
+
+ this.React = this.ReactDOM = this.FrameView = null;
+
+ if (this._contextMenuHandler) {
+ this._contextMenuHandler.destroy();
+ this._contextMenuHandler = null;
+ }
+
+ this._commandController = null;
+
+ let onDestroy = () => {
+ this._destroyer.resolve(null);
+ };
+
+ if (this.proxy) {
+ this.proxy.disconnect().then(onDestroy);
+ this.proxy = null;
+ } else {
+ onDestroy();
+ }
+
+ return this._destroyer.promise;
+ },
+};
+
+/**
+ * Utils: a collection of globally used functions.
+ */
+var Utils = {
+ /**
+ * Scrolls a node so that it's visible in its containing element.
+ *
+ * @param nsIDOMNode node
+ * The node to make visible.
+ * @returns void
+ */
+ scrollToVisible: function (node) {
+ node.scrollIntoView(false);
+ },
+
+ /**
+ * Check if the given output node is scrolled to the bottom.
+ *
+ * @param nsIDOMNode outputNode
+ * @param nsIDOMNode scrollNode
+ * @return boolean
+ * True if the output node is scrolled to the bottom, or false
+ * otherwise.
+ */
+ isOutputScrolledToBottom: function (outputNode, scrollNode) {
+ let lastNodeHeight = outputNode.lastChild ?
+ outputNode.lastChild.clientHeight : 0;
+ return scrollNode.scrollTop + scrollNode.clientHeight >=
+ scrollNode.scrollHeight - lastNodeHeight / 2;
+ },
+
+ /**
+ * Determine the category of a given nsIScriptError.
+ *
+ * @param nsIScriptError scriptError
+ * The script error you want to determine the category for.
+ * @return CATEGORY_JS|CATEGORY_CSS|CATEGORY_SECURITY
+ * Depending on the script error CATEGORY_JS, CATEGORY_CSS, or
+ * CATEGORY_SECURITY can be returned.
+ */
+ categoryForScriptError: function (scriptError) {
+ let category = scriptError.category;
+
+ if (/^(?:CSS|Layout)\b/.test(category)) {
+ return CATEGORY_CSS;
+ }
+
+ switch (category) {
+ case "Mixed Content Blocker":
+ case "Mixed Content Message":
+ case "CSP":
+ case "Invalid HSTS Headers":
+ case "Invalid HPKP Headers":
+ case "SHA-1 Signature":
+ case "Insecure Password Field":
+ case "SSL":
+ case "CORS":
+ case "Iframe Sandbox":
+ case "Tracking Protection":
+ case "Sub-resource Integrity":
+ return CATEGORY_SECURITY;
+
+ default:
+ return CATEGORY_JS;
+ }
+ },
+
+ /**
+ * Retrieve the limit of messages for a specific category.
+ *
+ * @param number category
+ * The category of messages you want to retrieve the limit for. See the
+ * CATEGORY_* constants.
+ * @return number
+ * The number of messages allowed for the specific category.
+ */
+ logLimitForCategory: function (category) {
+ let logLimit = DEFAULT_LOG_LIMIT;
+
+ try {
+ let prefName = CATEGORY_CLASS_FRAGMENTS[category];
+ logLimit = Services.prefs.getIntPref("devtools.hud.loglimit." + prefName);
+ logLimit = Math.max(logLimit, 1);
+ } catch (e) {
+ // Ignore any exceptions
+ }
+
+ return logLimit;
+ },
+};
+
+// CommandController
+
+/**
+ * A controller (an instance of nsIController) that makes editing actions
+ * behave appropriately in the context of the Web Console.
+ */
+function CommandController(webConsole) {
+ this.owner = webConsole;
+}
+
+CommandController.prototype = {
+ /**
+ * Selects all the text in the HUD output.
+ */
+ selectAll: function () {
+ this.owner.output.selectAllMessages();
+ },
+
+ /**
+ * Open the URL of the selected message in a new tab.
+ */
+ openURL: function () {
+ this.owner.openSelectedItemInTab();
+ },
+
+ copyURL: function () {
+ this.owner.copySelectedItems({ linkOnly: true, contextmenu: true });
+ },
+
+ /**
+ * Copies the last clicked message.
+ */
+ copyLastClicked: function () {
+ this.owner.copySelectedItems({ linkOnly: false, contextmenu: true });
+ },
+
+ supportsCommand: function (command) {
+ if (!this.owner || !this.owner.output) {
+ return false;
+ }
+ return this.isCommandEnabled(command);
+ },
+
+ isCommandEnabled: function (command) {
+ switch (command) {
+ case "consoleCmd_openURL":
+ case "consoleCmd_copyURL": {
+ // Only enable URL-related actions if node is Net Activity.
+ let selectedItem = this.owner.output.getSelectedMessages(1)[0] ||
+ this.owner._contextMenuHandler.lastClickedMessage;
+ return selectedItem && "url" in selectedItem;
+ }
+ case "cmd_copy": {
+ // Only copy if we right-clicked the console and there's no selected
+ // text. With text selected, we want to fall back onto the default
+ // copy behavior.
+ return this.owner._contextMenuHandler.lastClickedMessage &&
+ !this.owner.output.getSelectedMessages(1)[0];
+ }
+ case "cmd_selectAll":
+ return true;
+ }
+ return false;
+ },
+
+ doCommand: function (command) {
+ switch (command) {
+ case "consoleCmd_openURL":
+ this.openURL();
+ break;
+ case "consoleCmd_copyURL":
+ this.copyURL();
+ break;
+ case "cmd_copy":
+ this.copyLastClicked();
+ break;
+ case "cmd_selectAll":
+ this.selectAll();
+ break;
+ }
+ }
+};
+
+// Web Console connection proxy
+
+/**
+ * The WebConsoleConnectionProxy handles the connection between the Web Console
+ * and the application we connect to through the remote debug protocol.
+ *
+ * @constructor
+ * @param object webConsoleFrame
+ * The WebConsoleFrame object that owns this connection proxy.
+ * @param RemoteTarget target
+ * The target that the console will connect to.
+ */
+function WebConsoleConnectionProxy(webConsoleFrame, target) {
+ this.webConsoleFrame = webConsoleFrame;
+ this.target = target;
+
+ this._onPageError = this._onPageError.bind(this);
+ this._onLogMessage = this._onLogMessage.bind(this);
+ this._onConsoleAPICall = this._onConsoleAPICall.bind(this);
+ this._onNetworkEvent = this._onNetworkEvent.bind(this);
+ this._onNetworkEventUpdate = this._onNetworkEventUpdate.bind(this);
+ this._onFileActivity = this._onFileActivity.bind(this);
+ this._onReflowActivity = this._onReflowActivity.bind(this);
+ this._onServerLogCall = this._onServerLogCall.bind(this);
+ this._onTabNavigated = this._onTabNavigated.bind(this);
+ this._onAttachConsole = this._onAttachConsole.bind(this);
+ this._onCachedMessages = this._onCachedMessages.bind(this);
+ this._connectionTimeout = this._connectionTimeout.bind(this);
+ this._onLastPrivateContextExited =
+ this._onLastPrivateContextExited.bind(this);
+}
+
+WebConsoleConnectionProxy.prototype = {
+ /**
+ * The owning Web Console Frame instance.
+ *
+ * @see WebConsoleFrame
+ * @type object
+ */
+ webConsoleFrame: null,
+
+ /**
+ * The target that the console connects to.
+ * @type RemoteTarget
+ */
+ target: null,
+
+ /**
+ * The DebuggerClient object.
+ *
+ * @see DebuggerClient
+ * @type object
+ */
+ client: null,
+
+ /**
+ * The WebConsoleClient object.
+ *
+ * @see WebConsoleClient
+ * @type object
+ */
+ webConsoleClient: null,
+
+ /**
+ * Tells if the connection is established.
+ * @type boolean
+ */
+ connected: false,
+
+ /**
+ * Timer used for the connection.
+ * @private
+ * @type object
+ */
+ _connectTimer: null,
+
+ _connectDefer: null,
+ _disconnecter: null,
+
+ /**
+ * The WebConsoleActor ID.
+ *
+ * @private
+ * @type string
+ */
+ _consoleActor: null,
+
+ /**
+ * Tells if the window.console object of the remote web page is the native
+ * object or not.
+ * @private
+ * @type boolean
+ */
+ _hasNativeConsoleAPI: false,
+
+ /**
+ * Initialize a debugger client and connect it to the debugger server.
+ *
+ * @return object
+ * A promise object that is resolved/rejected based on the success of
+ * the connection initialization.
+ */
+ connect: function () {
+ if (this._connectDefer) {
+ return this._connectDefer.promise;
+ }
+
+ this._connectDefer = promise.defer();
+
+ let timeout = Services.prefs.getIntPref(PREF_CONNECTION_TIMEOUT);
+ this._connectTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ this._connectTimer.initWithCallback(this._connectionTimeout,
+ timeout, Ci.nsITimer.TYPE_ONE_SHOT);
+
+ let connPromise = this._connectDefer.promise;
+ connPromise.then(() => {
+ this._connectTimer.cancel();
+ this._connectTimer = null;
+ }, () => {
+ this._connectTimer = null;
+ });
+
+ let client = this.client = this.target.client;
+
+ if (this.target.isWorkerTarget) {
+ // XXXworkers: Not Console API yet inside of workers (Bug 1209353).
+ } else {
+ client.addListener("logMessage", this._onLogMessage);
+ client.addListener("pageError", this._onPageError);
+ client.addListener("consoleAPICall", this._onConsoleAPICall);
+ client.addListener("fileActivity", this._onFileActivity);
+ client.addListener("reflowActivity", this._onReflowActivity);
+ client.addListener("serverLogCall", this._onServerLogCall);
+ client.addListener("lastPrivateContextExited",
+ this._onLastPrivateContextExited);
+ }
+ this.target.on("will-navigate", this._onTabNavigated);
+ this.target.on("navigate", this._onTabNavigated);
+
+ this._consoleActor = this.target.form.consoleActor;
+ if (this.target.isTabActor) {
+ let tab = this.target.form;
+ this.webConsoleFrame.onLocationChange(tab.url, tab.title);
+ }
+ this._attachConsole();
+
+ return connPromise;
+ },
+
+ /**
+ * Connection timeout handler.
+ * @private
+ */
+ _connectionTimeout: function () {
+ let error = {
+ error: "timeout",
+ message: l10n.getStr("connectionTimeout"),
+ };
+
+ this._connectDefer.reject(error);
+ },
+
+ /**
+ * Attach to the Web Console actor.
+ * @private
+ */
+ _attachConsole: function () {
+ let listeners = ["PageError", "ConsoleAPI", "NetworkActivity",
+ "FileActivity"];
+ this.client.attachConsole(this._consoleActor, listeners,
+ this._onAttachConsole);
+ },
+
+ /**
+ * The "attachConsole" response handler.
+ *
+ * @private
+ * @param object response
+ * The JSON response object received from the server.
+ * @param object webConsoleClient
+ * The WebConsoleClient instance for the attached console, for the
+ * specific tab we work with.
+ */
+ _onAttachConsole: function (response, webConsoleClient) {
+ if (response.error) {
+ console.error("attachConsole failed: " + response.error + " " +
+ response.message);
+ this._connectDefer.reject(response);
+ return;
+ }
+
+ this.webConsoleClient = webConsoleClient;
+ this._hasNativeConsoleAPI = response.nativeConsoleAPI;
+
+ // There is no way to view response bodies from the Browser Console, so do
+ // not waste the memory.
+ let saveBodies = !this.webConsoleFrame.isBrowserConsole;
+ this.webConsoleFrame.setSaveRequestAndResponseBodies(saveBodies);
+
+ this.webConsoleClient.on("networkEvent", this._onNetworkEvent);
+ this.webConsoleClient.on("networkEventUpdate", this._onNetworkEventUpdate);
+
+ let msgs = ["PageError", "ConsoleAPI"];
+ this.webConsoleClient.getCachedMessages(msgs, this._onCachedMessages);
+
+ this.webConsoleFrame._onUpdateListeners();
+ },
+
+ /**
+ * Dispatch a message add on the new frontend and emit an event for tests.
+ */
+ dispatchMessageAdd: function(packet) {
+ this.webConsoleFrame.newConsoleOutput.dispatchMessageAdd(packet);
+ },
+
+ /**
+ * Batched dispatch of messages.
+ */
+ dispatchMessagesAdd: function(packets) {
+ this.webConsoleFrame.newConsoleOutput.dispatchMessagesAdd(packets);
+ },
+
+ /**
+ * The "cachedMessages" response handler.
+ *
+ * @private
+ * @param object response
+ * The JSON response object received from the server.
+ */
+ _onCachedMessages: function (response) {
+ if (response.error) {
+ console.error("Web Console getCachedMessages error: " + response.error +
+ " " + response.message);
+ this._connectDefer.reject(response);
+ return;
+ }
+
+ if (!this._connectTimer) {
+ // This happens if the promise is rejected (eg. a timeout), but the
+ // connection attempt is successful, nonetheless.
+ console.error("Web Console getCachedMessages error: invalid state.");
+ }
+
+ let messages =
+ response.messages.concat(...this.webConsoleClient.getNetworkEvents());
+ messages.sort((a, b) => a.timeStamp - b.timeStamp);
+
+ if (this.webConsoleFrame.NEW_CONSOLE_OUTPUT_ENABLED) {
+ // Filter out CSS page errors.
+ messages = messages.filter(message => !(message._type == "PageError"
+ && Utils.categoryForScriptError(message) === CATEGORY_CSS));
+ this.dispatchMessagesAdd(messages);
+ } else {
+ this.webConsoleFrame.displayCachedMessages(messages);
+ if (!this._hasNativeConsoleAPI) {
+ this.webConsoleFrame.logWarningAboutReplacedAPI();
+ }
+ }
+
+ this.connected = true;
+ this._connectDefer.resolve(this);
+ },
+
+ /**
+ * The "pageError" message type handler. We redirect any page errors to the UI
+ * for displaying.
+ *
+ * @private
+ * @param string type
+ * Message type.
+ * @param object packet
+ * The message received from the server.
+ */
+ _onPageError: function (type, packet) {
+ if (this.webConsoleFrame && packet.from == this._consoleActor) {
+ if (this.webConsoleFrame.NEW_CONSOLE_OUTPUT_ENABLED) {
+ let category = Utils.categoryForScriptError(packet.pageError);
+ if (category !== CATEGORY_CSS) {
+ this.dispatchMessageAdd(packet);
+ }
+ return;
+ }
+ this.webConsoleFrame.handlePageError(packet.pageError);
+ }
+ },
+
+ /**
+ * The "logMessage" message type handler. We redirect any message to the UI
+ * for displaying.
+ *
+ * @private
+ * @param string type
+ * Message type.
+ * @param object packet
+ * The message received from the server.
+ */
+ _onLogMessage: function (type, packet) {
+ if (this.webConsoleFrame && packet.from == this._consoleActor) {
+ this.webConsoleFrame.handleLogMessage(packet);
+ }
+ },
+
+ /**
+ * The "consoleAPICall" message type handler. We redirect any message to
+ * the UI for displaying.
+ *
+ * @private
+ * @param string type
+ * Message type.
+ * @param object packet
+ * The message received from the server.
+ */
+ _onConsoleAPICall: function (type, packet) {
+ if (this.webConsoleFrame && packet.from == this._consoleActor) {
+ if (this.webConsoleFrame.NEW_CONSOLE_OUTPUT_ENABLED) {
+ this.dispatchMessageAdd(packet);
+ } else {
+ this.webConsoleFrame.handleConsoleAPICall(packet.message);
+ }
+ }
+ },
+
+ /**
+ * The "networkEvent" message type handler. We redirect any message to
+ * the UI for displaying.
+ *
+ * @private
+ * @param string type
+ * Message type.
+ * @param object networkInfo
+ * The network request information.
+ */
+ _onNetworkEvent: function (type, networkInfo) {
+ if (this.webConsoleFrame) {
+ if (this.webConsoleFrame.NEW_CONSOLE_OUTPUT_ENABLED) {
+ this.dispatchMessageAdd(networkInfo);
+ } else {
+ this.webConsoleFrame.handleNetworkEvent(networkInfo);
+ }
+ }
+ },
+
+ /**
+ * The "networkEventUpdate" message type handler. We redirect any message to
+ * the UI for displaying.
+ *
+ * @private
+ * @param string type
+ * Message type.
+ * @param object packet
+ * The message received from the server.
+ * @param object networkInfo
+ * The network request information.
+ */
+ _onNetworkEventUpdate: function (type, { packet, networkInfo }) {
+ if (this.webConsoleFrame) {
+ this.webConsoleFrame.handleNetworkEventUpdate(networkInfo, packet);
+ }
+ },
+
+ /**
+ * The "fileActivity" message type handler. We redirect any message to
+ * the UI for displaying.
+ *
+ * @private
+ * @param string type
+ * Message type.
+ * @param object packet
+ * The message received from the server.
+ */
+ _onFileActivity: function (type, packet) {
+ if (this.webConsoleFrame && packet.from == this._consoleActor) {
+ this.webConsoleFrame.handleFileActivity(packet.uri);
+ }
+ },
+
+ _onReflowActivity: function (type, packet) {
+ if (this.webConsoleFrame && packet.from == this._consoleActor) {
+ this.webConsoleFrame.handleReflowActivity(packet);
+ }
+ },
+
+ /**
+ * The "serverLogCall" message type handler. We redirect any message to
+ * the UI for displaying.
+ *
+ * @private
+ * @param string type
+ * Message type.
+ * @param object packet
+ * The message received from the server.
+ */
+ _onServerLogCall: function (type, packet) {
+ if (this.webConsoleFrame && packet.from == this._consoleActor) {
+ this.webConsoleFrame.handleConsoleAPICall(packet.message);
+ }
+ },
+
+ /**
+ * The "lastPrivateContextExited" message type handler. When this message is
+ * received the Web Console UI is cleared.
+ *
+ * @private
+ * @param string type
+ * Message type.
+ * @param object packet
+ * The message received from the server.
+ */
+ _onLastPrivateContextExited: function (type, packet) {
+ if (this.webConsoleFrame && packet.from == this._consoleActor) {
+ this.webConsoleFrame.jsterm.clearPrivateMessages();
+ }
+ },
+
+ /**
+ * The "will-navigate" and "navigate" event handlers. We redirect any message
+ * to the UI for displaying.
+ *
+ * @private
+ * @param string event
+ * Event type.
+ * @param object packet
+ * The message received from the server.
+ */
+ _onTabNavigated: function (event, packet) {
+ if (!this.webConsoleFrame) {
+ return;
+ }
+
+ this.webConsoleFrame.handleTabNavigated(event, packet);
+ },
+
+ /**
+ * Release an object actor.
+ *
+ * @param string actor
+ * The actor ID to send the request to.
+ */
+ releaseActor: function (actor) {
+ if (this.client) {
+ this.client.release(actor);
+ }
+ },
+
+ /**
+ * Disconnect the Web Console from the remote server.
+ *
+ * @return object
+ * A promise object that is resolved when disconnect completes.
+ */
+ disconnect: function () {
+ if (this._disconnecter) {
+ return this._disconnecter.promise;
+ }
+
+ this._disconnecter = promise.defer();
+
+ if (!this.client) {
+ this._disconnecter.resolve(null);
+ return this._disconnecter.promise;
+ }
+
+ this.client.removeListener("logMessage", this._onLogMessage);
+ this.client.removeListener("pageError", this._onPageError);
+ this.client.removeListener("consoleAPICall", this._onConsoleAPICall);
+ this.client.removeListener("fileActivity", this._onFileActivity);
+ this.client.removeListener("reflowActivity", this._onReflowActivity);
+ this.client.removeListener("serverLogCall", this._onServerLogCall);
+ this.client.removeListener("lastPrivateContextExited",
+ this._onLastPrivateContextExited);
+ this.webConsoleClient.off("networkEvent", this._onNetworkEvent);
+ this.webConsoleClient.off("networkEventUpdate", this._onNetworkEventUpdate);
+ this.target.off("will-navigate", this._onTabNavigated);
+ this.target.off("navigate", this._onTabNavigated);
+
+ this.client = null;
+ this.webConsoleClient = null;
+ this.target = null;
+ this.connected = false;
+ this.webConsoleFrame = null;
+ this._disconnecter.resolve(null);
+
+ return this._disconnecter.promise;
+ },
+};
+
+// Context Menu
+
+/*
+ * ConsoleContextMenu this used to handle the visibility of context menu items.
+ *
+ * @constructor
+ * @param object owner
+ * The WebConsoleFrame instance that owns this object.
+ */
+function ConsoleContextMenu(owner) {
+ this.owner = owner;
+ this.popup = this.owner.document.getElementById("output-contextmenu");
+ this.build = this.build.bind(this);
+ this.popup.addEventListener("popupshowing", this.build);
+}
+
+ConsoleContextMenu.prototype = {
+ lastClickedMessage: null,
+
+ /*
+ * Handle to show/hide context menu item.
+ */
+ build: function (event) {
+ let metadata = this.getSelectionMetadata(event.rangeParent);
+ for (let element of this.popup.children) {
+ element.hidden = this.shouldHideMenuItem(element, metadata);
+ }
+ },
+
+ /*
+ * Get selection information from the view.
+ *
+ * @param nsIDOMElement clickElement
+ * The DOM element the user clicked on.
+ * @return object
+ * Selection metadata.
+ */
+ getSelectionMetadata: function (clickElement) {
+ let metadata = {
+ selectionType: "",
+ selection: new Set(),
+ };
+ let selectedItems = this.owner.output.getSelectedMessages();
+ if (!selectedItems.length) {
+ let clickedItem = this.owner.output.getMessageForElement(clickElement);
+ if (clickedItem) {
+ this.lastClickedMessage = clickedItem;
+ selectedItems = [clickedItem];
+ }
+ }
+
+ metadata.selectionType = selectedItems.length > 1 ? "multiple" : "single";
+
+ let selection = metadata.selection;
+ for (let item of selectedItems) {
+ switch (item.category) {
+ case CATEGORY_NETWORK:
+ selection.add("network");
+ break;
+ case CATEGORY_CSS:
+ selection.add("css");
+ break;
+ case CATEGORY_JS:
+ selection.add("js");
+ break;
+ case CATEGORY_WEBDEV:
+ selection.add("webdev");
+ break;
+ case CATEGORY_SERVER:
+ selection.add("server");
+ break;
+ }
+ }
+
+ return metadata;
+ },
+
+ /*
+ * Determine if an item should be hidden.
+ *
+ * @param nsIDOMElement menuItem
+ * @param object metadata
+ * @return boolean
+ * Whether the given item should be hidden or not.
+ */
+ shouldHideMenuItem: function (menuItem, metadata) {
+ let selectionType = menuItem.getAttribute("selectiontype");
+ if (selectionType && !metadata.selectionType == selectionType) {
+ return true;
+ }
+
+ let selection = menuItem.getAttribute("selection");
+ if (!selection) {
+ return false;
+ }
+
+ let shouldHide = true;
+ let itemData = selection.split("|");
+ for (let type of metadata.selection) {
+ // check whether this menu item should show or not.
+ if (itemData.indexOf(type) !== -1) {
+ shouldHide = false;
+ break;
+ }
+ }
+
+ return shouldHide;
+ },
+
+ /**
+ * Destroy the ConsoleContextMenu object instance.
+ */
+ destroy: function () {
+ this.popup.removeEventListener("popupshowing", this.build);
+ this.popup = null;
+ this.owner = null;
+ this.lastClickedMessage = null;
+ },
+};
diff --git a/devtools/client/webconsole/webconsole.xul b/devtools/client/webconsole/webconsole.xul
new file mode 100644
index 000000000..cd3e44d82
--- /dev/null
+++ b/devtools/client/webconsole/webconsole.xul
@@ -0,0 +1,214 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+<!DOCTYPE window [
+<!ENTITY % webConsoleDTD SYSTEM "chrome://devtools/locale/webConsole.dtd">
+%webConsoleDTD;
+]>
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/skin/widgets.css"
+ type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/skin/webconsole.css"
+ type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/skin/components-frame.css"
+ type="text/css"?>
+<?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ id="devtools-webconsole"
+ macanimationtype="document"
+ fullscreenbutton="true"
+ title="&window.title;"
+ browserConsoleTitle="&browserConsole.title;"
+ windowtype="devtools:webconsole"
+ width="900" height="350"
+ persist="screenX screenY width height sizemode">
+
+ <script type="application/javascript;version=1.8"
+ src="chrome://devtools/content/shared/theme-switching.js"/>
+ <script type="application/javascript;version=1.8"
+ src="resource://devtools/client/webconsole/new-console-output/main.js"/>
+ <script type="text/javascript" src="chrome://global/content/globalOverlay.js"/>
+ <script type="text/javascript" src="resource://devtools/client/webconsole/net/main.js"/>
+ <script type="text/javascript"><![CDATA[
+function goUpdateConsoleCommands() {
+ goUpdateCommand("consoleCmd_openURL");
+ goUpdateCommand("consoleCmd_copyURL");
+}
+ // ]]></script>
+
+ <commandset id="editMenuCommands"/>
+
+ <commandset id="consoleCommands"
+ commandupdater="true"
+ events="focus,select"
+ oncommandupdate="goUpdateConsoleCommands();">
+ <command id="consoleCmd_openURL"
+ oncommand="goDoCommand('consoleCmd_openURL');"/>
+ <command id="consoleCmd_copyURL"
+ oncommand="goDoCommand('consoleCmd_copyURL');"/>
+ </commandset>
+ <keyset id="consoleKeys">
+ </keyset>
+ <keyset id="editMenuKeys"/>
+
+ <popupset id="mainPopupSet">
+ <menupopup id="output-contextmenu" onpopupshowing="goUpdateGlobalEditMenuItems()">
+ <menuitem id="menu_openURL" label="&openURL.label;"
+ accesskey="&openURL.accesskey;" command="consoleCmd_openURL"
+ selection="network" selectionType="single"/>
+ <menuitem id="menu_copyURL" label="&copyURLCmd.label;"
+ accesskey="&copyURLCmd.accesskey;" command="consoleCmd_copyURL"
+ selection="network" selectionType="single"/>
+ <menuitem id="menu_openInVarView" label="&openInVarViewCmd.label;"
+ accesskey="&openInVarViewCmd.accesskey;" disabled="true"/>
+ <menuitem id="menu_storeAsGlobal" label="&storeAsGlobalVar.label;"
+ accesskey="&storeAsGlobalVar.accesskey;"/>
+ <menuitem id="cMenu_copy"/>
+ <menuitem id="cMenu_selectAll"/>
+ </menupopup>
+ </popupset>
+
+ <tooltip id="aHTMLTooltip" page="true"/>
+
+ <box class="hud-outer-wrapper devtools-responsive-container theme-body" flex="1">
+ <vbox class="hud-console-wrapper devtools-main-content" flex="1">
+ <toolbar class="hud-console-filter-toolbar devtools-toolbar" mode="full">
+ <toolbarbutton class="webconsole-clear-console-button devtools-toolbarbutton devtools-clear-icon"
+ tooltiptext="&btnClear.tooltip;"
+ accesskey="&btnClear.accesskey;"
+ tabindex="3"/>
+ <hbox class="devtools-toolbarbutton-group">
+ <toolbarbutton label="&btnPageNet.label;" type="menu-button"
+ category="net" class="devtools-toolbarbutton webconsole-filter-button"
+ tooltiptext="&btnPageNet.tooltip;"
+ accesskeyMacOSX="&btnPageNet.accesskeyMacOSX;"
+ accesskey="&btnPageNet.accesskey;"
+ tabindex="4">
+ <menupopup id="net-contextmenu">
+ <menuitem label="&btnConsoleErrors;" type="checkbox" autocheck="false"
+ prefKey="network"/>
+ <menuitem label="&btnConsoleWarnings;" type="checkbox" autocheck="false"
+ prefKey="netwarn"/>
+ <menuitem label="&btnConsoleXhr;" type="checkbox" autocheck="false"
+ prefKey="netxhr"/>
+ <menuitem label="&btnConsoleLog;" type="checkbox" autocheck="false"
+ prefKey="networkinfo"/>
+ </menupopup>
+ </toolbarbutton>
+ <toolbarbutton label="&btnPageCSS.label;" type="menu-button"
+ category="css" class="devtools-toolbarbutton webconsole-filter-button"
+ tooltiptext="&btnPageCSS.tooltip2;"
+ accesskey="&btnPageCSS.accesskey;"
+ tabindex="5">
+ <menupopup id="css-contextmenu">
+ <menuitem label="&btnConsoleErrors;" type="checkbox" autocheck="false"
+ prefKey="csserror"/>
+ <menuitem label="&btnConsoleWarnings;" type="checkbox"
+ autocheck="false" prefKey="cssparser"/>
+ <menuitem label="&btnConsoleReflows;" type="checkbox"
+ autocheck="false" prefKey="csslog"/>
+ </menupopup>
+ </toolbarbutton>
+ <toolbarbutton label="&btnPageJS.label;" type="menu-button"
+ category="js" class="devtools-toolbarbutton webconsole-filter-button"
+ tooltiptext="&btnPageJS.tooltip;"
+ accesskey="&btnPageJS.accesskey;"
+ tabindex="6">
+ <menupopup id="js-contextmenu">
+ <menuitem label="&btnConsoleErrors;" type="checkbox"
+ autocheck="false" prefKey="exception"/>
+ <menuitem label="&btnConsoleWarnings;" type="checkbox"
+ autocheck="false" prefKey="jswarn"/>
+ <menuitem label="&btnConsoleLog;" type="checkbox"
+ autocheck="false" prefKey="jslog"/>
+ </menupopup>
+ </toolbarbutton>
+ <toolbarbutton label="&btnPageSecurity.label;" type="menu-button"
+ category="security" class="devtools-toolbarbutton webconsole-filter-button"
+ tooltiptext="&btnPageSecurity.tooltip;"
+ accesskey="&btnPageSecurity.accesskey;"
+ tabindex="7">
+ <menupopup id="security-contextmenu">
+ <menuitem label="&btnConsoleErrors;" type="checkbox"
+ autocheck="false" prefKey="secerror"/>
+ <menuitem label="&btnConsoleWarnings;" type="checkbox"
+ autocheck="false" prefKey="secwarn"/>
+ </menupopup>
+ </toolbarbutton>
+ <toolbarbutton label="&btnPageLogging.label;" type="menu-button"
+ category="logging" class="devtools-toolbarbutton webconsole-filter-button"
+ tooltiptext="&btnPageLogging.tooltip;"
+ accesskey="&btnPageLogging.accesskey3;"
+ tabindex="8">
+ <menupopup id="logging-contextmenu">
+ <menuitem label="&btnConsoleErrors;" type="checkbox"
+ autocheck="false" prefKey="error"/>
+ <menuitem label="&btnConsoleWarnings;" type="checkbox"
+ autocheck="false" prefKey="warn"/>
+ <menuitem label="&btnConsoleInfo;" type="checkbox" autocheck="false"
+ prefKey="info"/>
+ <menuitem label="&btnConsoleLog;" type="checkbox" autocheck="false"
+ prefKey="log"/>
+ <menuseparator />
+ <menuitem label="&btnConsoleSharedWorkers;" type="checkbox"
+ autocheck="false" prefKey="sharedworkers"/>
+ <menuitem label="&btnConsoleServiceWorkers;" type="checkbox"
+ autocheck="false" prefKey="serviceworkers"/>
+ <menuitem label="&btnConsoleWindowlessWorkers;" type="checkbox"
+ autocheck="false" prefKey="windowlessworkers"/>
+ </menupopup>
+ </toolbarbutton>
+ <toolbarbutton label="&btnServerLogging.label;" type="menu-button"
+ category="server" class="devtools-toolbarbutton webconsole-filter-button"
+ tooltiptext="&btnServerLogging.tooltip;"
+ accesskey="&btnServerLogging.accesskey;"
+ tabindex="9">
+ <menupopup id="server-logging-contextmenu">
+ <menuitem label="&btnServerErrors;" type="checkbox"
+ autocheck="false" prefKey="servererror"/>
+ <menuitem label="&btnServerWarnings;" type="checkbox"
+ autocheck="false" prefKey="serverwarn"/>
+ <menuitem label="&btnServerInfo;" type="checkbox" autocheck="false"
+ prefKey="serverinfo"/>
+ <menuitem label="&btnServerLog;" type="checkbox" autocheck="false"
+ prefKey="serverlog"/>
+ </menupopup>
+ </toolbarbutton>
+ </hbox>
+
+ <spacer flex="1"/>
+
+ <textbox class="compact hud-filter-box devtools-filterinput" type="search"
+ placeholder="&filterOutput.placeholder;" tabindex="2"/>
+ </toolbar>
+
+ <hbox id="output-wrapper" flex="1" context="output-contextmenu" tooltip="aHTMLTooltip">
+ <!-- Wrapper element to make scrolling in output-container much faster.
+ See Bug 1237368 -->
+ <div xmlns="http://www.w3.org/1999/xhtml">
+ <div xmlns="http://www.w3.org/1999/xhtml" id="output-container"
+ tabindex="0" role="document" aria-live="polite" />
+ </div>
+ </hbox>
+ <notificationbox id="webconsole-notificationbox">
+ <hbox class="jsterm-input-container" style="direction:ltr">
+ <stack class="jsterm-stack-node" flex="1">
+ <textbox class="jsterm-complete-node devtools-monospace"
+ multiline="true" rows="1" tabindex="-1"/>
+ <textbox class="jsterm-input-node devtools-monospace"
+ multiline="true" rows="1" tabindex="0"
+ aria-autocomplete="list"/>
+ </stack>
+ </hbox>
+ </notificationbox>
+ </vbox>
+
+ <splitter class="devtools-side-splitter"/>
+
+ <tabbox id="webconsole-sidebar" class="devtools-sidebar-tabs" hidden="true" width="300">
+ <tabs/>
+ <tabpanels flex="1"/>
+ </tabbox>
+ </box>
+</window>
diff --git a/devtools/client/webide/components/moz.build b/devtools/client/webide/components/moz.build
new file mode 100644
index 000000000..d4047c295
--- /dev/null
+++ b/devtools/client/webide/components/moz.build
@@ -0,0 +1,10 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+EXTRA_COMPONENTS += [
+ 'webideCli.js',
+ 'webideComponents.manifest',
+]
diff --git a/devtools/client/webide/components/webideCli.js b/devtools/client/webide/components/webideCli.js
new file mode 100644
index 000000000..0f75da2c4
--- /dev/null
+++ b/devtools/client/webide/components/webideCli.js
@@ -0,0 +1,58 @@
+/* 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 = Components.interfaces;
+const Cu = Components.utils;
+
+const { XPCOMUtils } = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {});
+
+XPCOMUtils.defineLazyModuleGetter(this, "Services", "resource://gre/modules/Services.jsm");
+
+/**
+ * Handles --webide command line option.
+ */
+
+function webideCli() { }
+
+webideCli.prototype = {
+ handle: function (cmdLine) {
+ if (!cmdLine.handleFlag("webide", false)) {
+ return;
+ }
+
+ // If --webide is used remotely, we don't want to open
+ // a new tab.
+ //
+ // If --webide is used for a new Firefox instance, we
+ // want to open webide only.
+ cmdLine.preventDefault = true;
+
+ let win = Services.wm.getMostRecentWindow("devtools:webide");
+ if (win) {
+ win.focus();
+ } else {
+ win = Services.ww.openWindow(null,
+ "chrome://webide/content/",
+ "webide",
+ "chrome,centerscreen,resizable,dialog=no",
+ null);
+ }
+
+ if (cmdLine.state == Ci.nsICommandLine.STATE_INITIAL_LAUNCH) {
+ // If this is a new Firefox instance, and because we will only start
+ // webide, we need to notify "sessionstore-windows-restored" to trigger
+ // addons registration (for simulators and adb helper).
+ Services.obs.notifyObservers(null, "sessionstore-windows-restored", "");
+ }
+ },
+
+ helpInfo: "",
+
+ classID: Components.ID("{79b7b44e-de5e-4e4c-b7a2-044003c615d9}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsICommandLineHandler]),
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([webideCli]);
diff --git a/devtools/client/webide/components/webideComponents.manifest b/devtools/client/webide/components/webideComponents.manifest
new file mode 100644
index 000000000..03af9758c
--- /dev/null
+++ b/devtools/client/webide/components/webideComponents.manifest
@@ -0,0 +1,4 @@
+# webide components
+component {79b7b44e-de5e-4e4c-b7a2-044003c615d9} webideCli.js
+contract @mozilla.org/browser/webide-clh;1 {79b7b44e-de5e-4e4c-b7a2-044003c615d9}
+category command-line-handler a-webide @mozilla.org/browser/webide-clh;1
diff --git a/devtools/client/webide/content/addons.js b/devtools/client/webide/content/addons.js
new file mode 100644
index 000000000..3948b040f
--- /dev/null
+++ b/devtools/client/webide/content/addons.js
@@ -0,0 +1,135 @@
+/* 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 Cu = Components.utils;
+const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const Services = require("Services");
+const {gDevTools} = require("devtools/client/framework/devtools");
+const {GetAvailableAddons, ForgetAddonsList} = require("devtools/client/webide/modules/addons");
+const Strings = Services.strings.createBundle("chrome://devtools/locale/webide.properties");
+
+window.addEventListener("load", function onLoad() {
+ window.removeEventListener("load", onLoad);
+ document.querySelector("#aboutaddons").onclick = function () {
+ let browserWin = Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
+ if (browserWin && browserWin.BrowserOpenAddonsMgr) {
+ browserWin.BrowserOpenAddonsMgr("addons://list/extension");
+ }
+ };
+ document.querySelector("#close").onclick = CloseUI;
+ GetAvailableAddons().then(BuildUI, (e) => {
+ console.error(e);
+ window.alert(Strings.formatStringFromName("error_cantFetchAddonsJSON", [e], 1));
+ });
+}, true);
+
+window.addEventListener("unload", function onUnload() {
+ window.removeEventListener("unload", onUnload);
+ ForgetAddonsList();
+}, true);
+
+function CloseUI() {
+ window.parent.UI.openProject();
+}
+
+function BuildUI(addons) {
+ BuildItem(addons.adb, "adb");
+ BuildItem(addons.adapters, "adapters");
+ for (let addon of addons.simulators) {
+ BuildItem(addon, "simulator");
+ }
+}
+
+function BuildItem(addon, type) {
+
+ function onAddonUpdate(event, arg) {
+ switch (event) {
+ case "update":
+ progress.removeAttribute("value");
+ li.setAttribute("status", addon.status);
+ status.textContent = Strings.GetStringFromName("addons_status_" + addon.status);
+ break;
+ case "failure":
+ window.parent.UI.reportError("error_operationFail", arg);
+ break;
+ case "progress":
+ if (arg == -1) {
+ progress.removeAttribute("value");
+ } else {
+ progress.value = arg;
+ }
+ break;
+ }
+ }
+
+ let events = ["update", "failure", "progress"];
+ for (let e of events) {
+ addon.on(e, onAddonUpdate);
+ }
+ window.addEventListener("unload", function onUnload() {
+ window.removeEventListener("unload", onUnload);
+ for (let e of events) {
+ addon.off(e, onAddonUpdate);
+ }
+ });
+
+ let li = document.createElement("li");
+ li.setAttribute("status", addon.status);
+
+ let name = document.createElement("span");
+ name.className = "name";
+
+ switch (type) {
+ case "adb":
+ li.setAttribute("addon", type);
+ name.textContent = Strings.GetStringFromName("addons_adb_label");
+ break;
+ case "adapters":
+ li.setAttribute("addon", type);
+ try {
+ name.textContent = Strings.GetStringFromName("addons_adapters_label");
+ } catch (e) {
+ // This code (bug 1081093) will be backported to Aurora, which doesn't
+ // contain this string.
+ name.textContent = "Tools Adapters Add-on";
+ }
+ break;
+ case "simulator":
+ li.setAttribute("addon", "simulator-" + addon.version);
+ let stability = Strings.GetStringFromName("addons_" + addon.stability);
+ name.textContent = Strings.formatStringFromName("addons_simulator_label", [addon.version, stability], 2);
+ break;
+ }
+
+ li.appendChild(name);
+
+ let status = document.createElement("span");
+ status.className = "status";
+ status.textContent = Strings.GetStringFromName("addons_status_" + addon.status);
+ li.appendChild(status);
+
+ let installButton = document.createElement("button");
+ installButton.className = "install-button";
+ installButton.onclick = () => addon.install();
+ installButton.textContent = Strings.GetStringFromName("addons_install_button");
+ li.appendChild(installButton);
+
+ let uninstallButton = document.createElement("button");
+ uninstallButton.className = "uninstall-button";
+ uninstallButton.onclick = () => addon.uninstall();
+ uninstallButton.textContent = Strings.GetStringFromName("addons_uninstall_button");
+ li.appendChild(uninstallButton);
+
+ let progress = document.createElement("progress");
+ li.appendChild(progress);
+
+ if (type == "adb") {
+ let warning = document.createElement("p");
+ warning.textContent = Strings.GetStringFromName("addons_adb_warning");
+ warning.className = "warning";
+ li.appendChild(warning);
+ }
+
+ document.querySelector("ul").appendChild(li);
+}
diff --git a/devtools/client/webide/content/addons.xhtml b/devtools/client/webide/content/addons.xhtml
new file mode 100644
index 000000000..6f3bc1e7c
--- /dev/null
+++ b/devtools/client/webide/content/addons.xhtml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!-- 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/. -->
+
+<!DOCTYPE html [
+ <!ENTITY % webideDTD SYSTEM "chrome://devtools/locale/webide.dtd" >
+ %webideDTD;
+]>
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta charset="utf8"/>
+ <link rel="stylesheet" href="chrome://webide/skin/deck.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://webide/skin/addons.css" type="text/css"/>
+ <script type="application/javascript;version=1.8" src="chrome://webide/content/addons.js"></script>
+ </head>
+ <body>
+
+ <div id="controls">
+ <a id="aboutaddons">&addons_aboutaddons;</a>
+ <a id="close">&deck_close;</a>
+ </div>
+
+ <h1>&addons_title;</h1>
+
+ <ul></ul>
+
+ </body>
+</html>
diff --git a/devtools/client/webide/content/details.js b/devtools/client/webide/content/details.js
new file mode 100644
index 000000000..9097cd8c5
--- /dev/null
+++ b/devtools/client/webide/content/details.js
@@ -0,0 +1,139 @@
+/* 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 Cu = Components.utils;
+const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const Services = require("Services");
+const {AppManager} = require("devtools/client/webide/modules/app-manager");
+const {ProjectBuilding} = require("devtools/client/webide/modules/build");
+
+window.addEventListener("load", function onLoad() {
+ window.removeEventListener("load", onLoad);
+ document.addEventListener("visibilitychange", updateUI, true);
+ AppManager.on("app-manager-update", onAppManagerUpdate);
+ updateUI();
+}, true);
+
+window.addEventListener("unload", function onUnload() {
+ window.removeEventListener("unload", onUnload);
+ AppManager.off("app-manager-update", onAppManagerUpdate);
+}, true);
+
+function onAppManagerUpdate(event, what, details) {
+ if (what == "project" ||
+ what == "project-validated") {
+ updateUI();
+ }
+}
+
+function resetUI() {
+ document.querySelector("#toolbar").classList.add("hidden");
+ document.querySelector("#type").classList.add("hidden");
+ document.querySelector("#descriptionHeader").classList.add("hidden");
+ document.querySelector("#manifestURLHeader").classList.add("hidden");
+ document.querySelector("#locationHeader").classList.add("hidden");
+
+ document.body.className = "";
+ document.querySelector("#icon").src = "";
+ document.querySelector("h1").textContent = "";
+ document.querySelector("#description").textContent = "";
+ document.querySelector("#type").textContent = "";
+ document.querySelector("#manifestURL").textContent = "";
+ document.querySelector("#location").textContent = "";
+
+ document.querySelector("#prePackageLog").hidden = true;
+
+ document.querySelector("#errorslist").innerHTML = "";
+ document.querySelector("#warningslist").innerHTML = "";
+
+}
+
+function updateUI() {
+ resetUI();
+
+ let project = AppManager.selectedProject;
+ if (!project) {
+ return;
+ }
+
+ if (project.type != "runtimeApp" && project.type != "mainProcess") {
+ document.querySelector("#toolbar").classList.remove("hidden");
+ document.querySelector("#locationHeader").classList.remove("hidden");
+ document.querySelector("#location").textContent = project.location;
+ }
+
+ document.body.className = project.validationStatus;
+ document.querySelector("#icon").src = project.icon;
+ document.querySelector("h1").textContent = project.name;
+
+ let manifest;
+ if (project.type == "runtimeApp") {
+ manifest = project.app.manifest;
+ } else {
+ manifest = project.manifest;
+ }
+
+ if (manifest) {
+ if (manifest.description) {
+ document.querySelector("#descriptionHeader").classList.remove("hidden");
+ document.querySelector("#description").textContent = manifest.description;
+ }
+
+ document.querySelector("#type").classList.remove("hidden");
+
+ if (project.type == "runtimeApp") {
+ let manifestURL = AppManager.getProjectManifestURL(project);
+ document.querySelector("#type").textContent = manifest.type || "web";
+ document.querySelector("#manifestURLHeader").classList.remove("hidden");
+ document.querySelector("#manifestURL").textContent = manifestURL;
+ } else if (project.type == "mainProcess") {
+ document.querySelector("#type").textContent = project.name;
+ } else {
+ document.querySelector("#type").textContent = project.type + " " + (manifest.type || "web");
+ }
+
+ if (project.type == "packaged") {
+ let manifestURL = AppManager.getProjectManifestURL(project);
+ if (manifestURL) {
+ document.querySelector("#manifestURLHeader").classList.remove("hidden");
+ document.querySelector("#manifestURL").textContent = manifestURL;
+ }
+ }
+ }
+
+ if (project.type != "runtimeApp" && project.type != "mainProcess") {
+ ProjectBuilding.hasPrepackage(project).then(hasPrepackage => {
+ document.querySelector("#prePackageLog").hidden = !hasPrepackage;
+ });
+ }
+
+ let errorsNode = document.querySelector("#errorslist");
+ let warningsNode = document.querySelector("#warningslist");
+
+ if (project.errors) {
+ for (let e of project.errors) {
+ let li = document.createElement("li");
+ li.textContent = e;
+ errorsNode.appendChild(li);
+ }
+ }
+
+ if (project.warnings) {
+ for (let w of project.warnings) {
+ let li = document.createElement("li");
+ li.textContent = w;
+ warningsNode.appendChild(li);
+ }
+ }
+
+ AppManager.update("details");
+}
+
+function showPrepackageLog() {
+ window.top.UI.selectDeckPanel("logs");
+}
+
+function removeProject() {
+ AppManager.removeSelectedProject();
+}
diff --git a/devtools/client/webide/content/details.xhtml b/devtools/client/webide/content/details.xhtml
new file mode 100644
index 000000000..a04c37b0c
--- /dev/null
+++ b/devtools/client/webide/content/details.xhtml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!-- 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/. -->
+
+<!DOCTYPE html [
+ <!ENTITY % webideDTD SYSTEM "chrome://devtools/locale/webide.dtd" >
+ %webideDTD;
+]>
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta charset="utf8"/>
+ <link rel="stylesheet" href="chrome://webide/skin/details.css" type="text/css"/>
+ <script type="application/javascript;version=1.8" src="chrome://webide/content/details.js"></script>
+ </head>
+ <body>
+
+ <div id="toolbar">
+ <button onclick="removeProject()">&details_removeProject_button;</button>
+ <p id="validation_status">
+ <span class="valid">&details_valid_header;</span>
+ <span class="warning">&details_warning_header;</span>
+ <span class="error">&details_error_header;</span>
+ </p>
+ </div>
+
+ <header>
+ <img id="icon"></img>
+ <div>
+ <h1></h1>
+ <p id="type"></p>
+ </div>
+ </header>
+
+ <main>
+ <h3 id="descriptionHeader">&details_description;</h3>
+ <p id="description"></p>
+
+ <h3 id="locationHeader">&details_location;</h3>
+ <p id="location"></p>
+
+ <h3 id="manifestURLHeader">&details_manifestURL;</h3>
+ <p id="manifestURL"></p>
+
+ <button id="prePackageLog" onclick="showPrepackageLog()" hidden="true">&details_showPrepackageLog_button;</button>
+ </main>
+
+ <ul class="validation_messages" id="errorslist"></ul>
+ <ul class="validation_messages" id="warningslist"></ul>
+
+ </body>
+</html>
diff --git a/devtools/client/webide/content/devicepreferences.js b/devtools/client/webide/content/devicepreferences.js
new file mode 100644
index 000000000..14c020f12
--- /dev/null
+++ b/devtools/client/webide/content/devicepreferences.js
@@ -0,0 +1,81 @@
+/* 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 Cu = Components.utils;
+const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const {AppManager} = require("devtools/client/webide/modules/app-manager");
+const {Connection} = require("devtools/shared/client/connection-manager");
+const ConfigView = require("devtools/client/webide/modules/config-view");
+
+var configView = new ConfigView(window);
+
+window.addEventListener("load", function onLoad() {
+ window.removeEventListener("load", onLoad);
+ AppManager.on("app-manager-update", OnAppManagerUpdate);
+ document.getElementById("close").onclick = CloseUI;
+ document.getElementById("device-fields").onchange = UpdateField;
+ document.getElementById("device-fields").onclick = CheckReset;
+ document.getElementById("search-bar").onkeyup = document.getElementById("search-bar").onclick = SearchField;
+ document.getElementById("custom-value").onclick = UpdateNewField;
+ document.getElementById("custom-value-type").onchange = ClearNewFields;
+ document.getElementById("add-custom-field").onkeyup = CheckNewFieldSubmit;
+ BuildUI();
+}, true);
+
+window.addEventListener("unload", function onUnload() {
+ window.removeEventListener("unload", onUnload);
+ AppManager.off("app-manager-update", OnAppManagerUpdate);
+});
+
+function CloseUI() {
+ window.parent.UI.openProject();
+}
+
+function OnAppManagerUpdate(event, what) {
+ if (what == "connection" || what == "runtime-global-actors") {
+ BuildUI();
+ }
+}
+
+function CheckNewFieldSubmit(event) {
+ configView.checkNewFieldSubmit(event);
+}
+
+function UpdateNewField() {
+ configView.updateNewField();
+}
+
+function ClearNewFields() {
+ configView.clearNewFields();
+}
+
+function CheckReset(event) {
+ configView.checkReset(event);
+}
+
+function UpdateField(event) {
+ configView.updateField(event);
+}
+
+function SearchField(event) {
+ configView.search(event);
+}
+
+var getAllPrefs; // Used by tests
+function BuildUI() {
+ configView.resetTable();
+
+ if (AppManager.connection &&
+ AppManager.connection.status == Connection.Status.CONNECTED &&
+ AppManager.preferenceFront) {
+ configView.front = AppManager.preferenceFront;
+ configView.kind = "Pref";
+ configView.includeTypeName = true;
+
+ getAllPrefs = AppManager.preferenceFront.getAllPrefs()
+ .then(json => configView.generateDisplay(json));
+ } else {
+ CloseUI();
+ }
+}
diff --git a/devtools/client/webide/content/devicepreferences.xhtml b/devtools/client/webide/content/devicepreferences.xhtml
new file mode 100644
index 000000000..dafb6f15f
--- /dev/null
+++ b/devtools/client/webide/content/devicepreferences.xhtml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!-- 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/. -->
+
+<!DOCTYPE html [
+ <!ENTITY % webideDTD SYSTEM "chrome://devtools/locale/webide.dtd" >
+ %webideDTD;
+]>
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta charset="utf8"/>
+ <link rel="stylesheet" href="chrome://webide/skin/deck.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://webide/skin/config-view.css" type="text/css"/>
+ <script type="application/javascript;version=1.8" src="chrome://webide/content/devicepreferences.js"></script>
+ </head>
+ <body>
+ <header>
+ <div id="controls">
+ <a id="close">&deck_close;</a>
+ </div>
+ <h1>&devicepreference_title;</h1>
+ <div id="search">
+ <input type="text" id="search-bar" placeholder="&devicepreference_search;"/>
+ </div>
+ </header>
+ <table id="device-fields">
+ <tr id="add-custom-field">
+ <td>
+ <select id="custom-value-type">
+ <option value="" selected="selected">&device_typenone;</option>
+ <option value="boolean">&device_typeboolean;</option>
+ <option value="number">&device_typenumber;</option>
+ <option value="string">&device_typestring;</option>
+ </select>
+ <input type="text" id="custom-value-name" placeholder="&devicepreference_newname;"/>
+ </td>
+ <td class="custom-input">
+ <input type="text" id="custom-value-text" placeholder="&devicepreference_newtext;"/>
+ </td>
+ <td>
+ <button id="custom-value" class="new-editable">&devicepreference_addnew;</button>
+ </td>
+ </tr>
+ </table>
+ </body>
+</html>
diff --git a/devtools/client/webide/content/devicesettings.js b/devtools/client/webide/content/devicesettings.js
new file mode 100644
index 000000000..987df5995
--- /dev/null
+++ b/devtools/client/webide/content/devicesettings.js
@@ -0,0 +1,81 @@
+/* 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 Cu = Components.utils;
+const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const {AppManager} = require("devtools/client/webide/modules/app-manager");
+const {Connection} = require("devtools/shared/client/connection-manager");
+const ConfigView = require("devtools/client/webide/modules/config-view");
+
+var configView = new ConfigView(window);
+
+window.addEventListener("load", function onLoad() {
+ window.removeEventListener("load", onLoad);
+ AppManager.on("app-manager-update", OnAppManagerUpdate);
+ document.getElementById("close").onclick = CloseUI;
+ document.getElementById("device-fields").onchange = UpdateField;
+ document.getElementById("device-fields").onclick = CheckReset;
+ document.getElementById("search-bar").onkeyup = document.getElementById("search-bar").onclick = SearchField;
+ document.getElementById("custom-value").onclick = UpdateNewField;
+ document.getElementById("custom-value-type").onchange = ClearNewFields;
+ document.getElementById("add-custom-field").onkeyup = CheckNewFieldSubmit;
+ BuildUI();
+}, true);
+
+window.addEventListener("unload", function onUnload() {
+ window.removeEventListener("unload", onUnload);
+ AppManager.off("app-manager-update", OnAppManagerUpdate);
+});
+
+function CloseUI() {
+ window.parent.UI.openProject();
+}
+
+function OnAppManagerUpdate(event, what) {
+ if (what == "connection" || what == "runtime-global-actors") {
+ BuildUI();
+ }
+}
+
+function CheckNewFieldSubmit(event) {
+ configView.checkNewFieldSubmit(event);
+}
+
+function UpdateNewField() {
+ configView.updateNewField();
+}
+
+function ClearNewFields() {
+ configView.clearNewFields();
+}
+
+function CheckReset(event) {
+ configView.checkReset(event);
+}
+
+function UpdateField(event) {
+ configView.updateField(event);
+}
+
+function SearchField(event) {
+ configView.search(event);
+}
+
+var getAllSettings; // Used by tests
+function BuildUI() {
+ configView.resetTable();
+
+ if (AppManager.connection &&
+ AppManager.connection.status == Connection.Status.CONNECTED &&
+ AppManager.settingsFront) {
+ configView.front = AppManager.settingsFront;
+ configView.kind = "Setting";
+ configView.includeTypeName = false;
+
+ getAllSettings = AppManager.settingsFront.getAllSettings()
+ .then(json => configView.generateDisplay(json));
+ } else {
+ CloseUI();
+ }
+}
diff --git a/devtools/client/webide/content/devicesettings.xhtml b/devtools/client/webide/content/devicesettings.xhtml
new file mode 100644
index 000000000..0406c6f07
--- /dev/null
+++ b/devtools/client/webide/content/devicesettings.xhtml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!-- 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/. -->
+
+<!DOCTYPE html [
+ <!ENTITY % webideDTD SYSTEM "chrome://devtools/locale/webide.dtd" >
+ %webideDTD;
+]>
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta charset="utf8"/>
+ <link rel="stylesheet" href="chrome://webide/skin/deck.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://webide/skin/config-view.css" type="text/css"/>
+ <script type="application/javascript;version=1.8" src="chrome://webide/content/devicesettings.js"></script>
+ </head>
+ <body>
+ <header>
+ <div id="controls">
+ <a id="close">&deck_close;</a>
+ </div>
+ <h1>&devicesetting_title;</h1>
+ <div id="search">
+ <input type="text" id="search-bar" placeholder="&devicesetting_search;"/>
+ </div>
+ </header>
+ <table id="device-fields">
+ <tr id="add-custom-field">
+ <td>
+ <select id="custom-value-type">
+ <option value="" selected="selected">&device_typenone;</option>
+ <option value="boolean">&device_typeboolean;</option>
+ <option value="number">&device_typenumber;</option>
+ <option value="string">&device_typestring;</option>
+ <option value="object">&device_typeobject;</option>
+ </select>
+ <input type="text" id="custom-value-name" placeholder="&devicesetting_newname;"/>
+ </td>
+ <td class="custom-input">
+ <input type="text" id="custom-value-text" placeholder="&devicesetting_newtext;"/>
+ </td>
+ <td>
+ <button id="custom-value" class="new-editable">&devicesetting_addnew;</button>
+ </td>
+ </tr>
+ </table>
+ </body>
+</html>
diff --git a/devtools/client/webide/content/jar.mn b/devtools/client/webide/content/jar.mn
new file mode 100644
index 000000000..db79fdb51
--- /dev/null
+++ b/devtools/client/webide/content/jar.mn
@@ -0,0 +1,38 @@
+# 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/.
+
+webide.jar:
+% content webide %content/
+ content/webide.xul (webide.xul)
+ content/webide.js (webide.js)
+ content/newapp.xul (newapp.xul)
+ content/newapp.js (newapp.js)
+ content/details.xhtml (details.xhtml)
+ content/details.js (details.js)
+ content/addons.js (addons.js)
+ content/addons.xhtml (addons.xhtml)
+ content/permissionstable.js (permissionstable.js)
+ content/permissionstable.xhtml (permissionstable.xhtml)
+ content/runtimedetails.js (runtimedetails.js)
+ content/runtimedetails.xhtml (runtimedetails.xhtml)
+ content/prefs.js (prefs.js)
+ content/prefs.xhtml (prefs.xhtml)
+ content/monitor.xhtml (monitor.xhtml)
+ content/monitor.js (monitor.js)
+ content/devicepreferences.js (devicepreferences.js)
+ content/devicepreferences.xhtml (devicepreferences.xhtml)
+ content/devicesettings.js (devicesettings.js)
+ content/devicesettings.xhtml (devicesettings.xhtml)
+ content/wifi-auth.js (wifi-auth.js)
+ content/wifi-auth.xhtml (wifi-auth.xhtml)
+ content/logs.xhtml (logs.xhtml)
+ content/logs.js (logs.js)
+ content/project-listing.xhtml (project-listing.xhtml)
+ content/project-listing.js (project-listing.js)
+ content/project-panel.js (project-panel.js)
+ content/runtime-panel.js (runtime-panel.js)
+ content/runtime-listing.xhtml (runtime-listing.xhtml)
+ content/runtime-listing.js (runtime-listing.js)
+ content/simulator.js (simulator.js)
+ content/simulator.xhtml (simulator.xhtml)
diff --git a/devtools/client/webide/content/logs.js b/devtools/client/webide/content/logs.js
new file mode 100644
index 000000000..157d83b67
--- /dev/null
+++ b/devtools/client/webide/content/logs.js
@@ -0,0 +1,70 @@
+/* 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 Cu = Components.utils;
+const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const {AppManager} = require("devtools/client/webide/modules/app-manager");
+
+window.addEventListener("load", function onLoad() {
+ window.removeEventListener("load", onLoad);
+
+ Logs.init();
+});
+
+window.addEventListener("unload", function onUnload() {
+ window.removeEventListener("unload", onUnload);
+
+ Logs.uninit();
+});
+
+const Logs = {
+ init: function () {
+ this.list = document.getElementById("logs");
+
+ Logs.onAppManagerUpdate = Logs.onAppManagerUpdate.bind(this);
+ AppManager.on("app-manager-update", Logs.onAppManagerUpdate);
+
+ document.getElementById("close").onclick = Logs.close.bind(this);
+ },
+
+ uninit: function () {
+ AppManager.off("app-manager-update", Logs.onAppManagerUpdate);
+ },
+
+ onAppManagerUpdate: function (event, what, details) {
+ switch (what) {
+ case "pre-package":
+ this.prePackageLog(details);
+ break;
+ }
+ },
+
+ close: function () {
+ window.parent.UI.openProject();
+ },
+
+ prePackageLog: function (msg, details) {
+ if (msg == "start") {
+ this.clear();
+ } else if (msg == "succeed") {
+ setTimeout(function () {
+ Logs.close();
+ }, 1000);
+ } else if (msg == "failed") {
+ this.log(details);
+ } else {
+ this.log(msg);
+ }
+ },
+
+ clear: function () {
+ this.list.innerHTML = "";
+ },
+
+ log: function (msg) {
+ let line = document.createElement("li");
+ line.textContent = msg;
+ this.list.appendChild(line);
+ }
+};
diff --git a/devtools/client/webide/content/logs.xhtml b/devtools/client/webide/content/logs.xhtml
new file mode 100644
index 000000000..8d003e509
--- /dev/null
+++ b/devtools/client/webide/content/logs.xhtml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!-- 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/. -->
+
+<!DOCTYPE html [
+ <!ENTITY % webideDTD SYSTEM "chrome://devtools/locale/webide.dtd" >
+ %webideDTD;
+]>
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta charset="utf8"/>
+ <link rel="stylesheet" href="chrome://webide/skin/deck.css" type="text/css"/>
+ <link rel="stylesheet" href="resource://devtools/client/themes/common.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://webide/skin/logs.css" type="text/css"/>
+ <script type="application/javascript;version=1.8" src="chrome://devtools/content/shared/theme-switching.js"></script>
+ <script type="application/javascript;version=1.8" src="logs.js"></script>
+ </head>
+ <body>
+
+ <div id="controls">
+ <a id="close">&deck_close;</a>
+ </div>
+
+ <h1>&logs_title;</h1>
+
+ <ul id="logs" class="devtools-monospace">
+ </ul>
+
+ </body>
+</html>
diff --git a/devtools/client/webide/content/monitor.js b/devtools/client/webide/content/monitor.js
new file mode 100644
index 000000000..a5d80d460
--- /dev/null
+++ b/devtools/client/webide/content/monitor.js
@@ -0,0 +1,741 @@
+/* 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 Cu = Components.utils;
+const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const Services = require("Services");
+const {AppManager} = require("devtools/client/webide/modules/app-manager");
+const {AppActorFront} = require("devtools/shared/apps/app-actor-front");
+const {Connection} = require("devtools/shared/client/connection-manager");
+const EventEmitter = require("devtools/shared/event-emitter");
+
+window.addEventListener("load", function onLoad() {
+ window.removeEventListener("load", onLoad);
+ window.addEventListener("resize", Monitor.resize);
+ window.addEventListener("unload", Monitor.unload);
+
+ document.querySelector("#close").onclick = () => {
+ window.parent.UI.openProject();
+ };
+
+ Monitor.load();
+});
+
+
+/**
+ * The Monitor is a WebIDE tool used to display any kind of time-based data in
+ * the form of graphs.
+ *
+ * The data can come from a Firefox OS device, simulator, or from a WebSockets
+ * server running locally.
+ *
+ * The format of a data update is typically an object like:
+ *
+ * { graph: 'mygraph', curve: 'mycurve', value: 42, time: 1234 }
+ *
+ * or an array of such objects. For more details on the data format, see the
+ * `Graph.update(data)` method.
+ */
+var Monitor = {
+
+ apps: new Map(),
+ graphs: new Map(),
+ front: null,
+ socket: null,
+ wstimeout: null,
+ b2ginfo: false,
+ b2gtimeout: null,
+
+ /**
+ * Add new data to the graphs, create a new graph if necessary.
+ */
+ update: function (data, fallback) {
+ if (Array.isArray(data)) {
+ data.forEach(d => Monitor.update(d, fallback));
+ return;
+ }
+
+ if (Monitor.b2ginfo && data.graph === "USS") {
+ // If we're polling b2g-info, ignore USS updates from the device's
+ // USSAgents (see Monitor.pollB2GInfo()).
+ return;
+ }
+
+ if (fallback) {
+ for (let key in fallback) {
+ if (!data[key]) {
+ data[key] = fallback[key];
+ }
+ }
+ }
+
+ let graph = Monitor.graphs.get(data.graph);
+ if (!graph) {
+ let element = document.createElement("div");
+ element.classList.add("graph");
+ document.body.appendChild(element);
+
+ graph = new Graph(data.graph, element);
+ Monitor.resize(); // a scrollbar might have dis/reappeared
+ Monitor.graphs.set(data.graph, graph);
+ }
+ graph.update(data);
+ },
+
+ /**
+ * Initialize the Monitor.
+ */
+ load: function () {
+ AppManager.on("app-manager-update", Monitor.onAppManagerUpdate);
+ Monitor.connectToRuntime();
+ Monitor.connectToWebSocket();
+ },
+
+ /**
+ * Clean up the Monitor.
+ */
+ unload: function () {
+ AppManager.off("app-manager-update", Monitor.onAppManagerUpdate);
+ Monitor.disconnectFromRuntime();
+ Monitor.disconnectFromWebSocket();
+ },
+
+ /**
+ * Resize all the graphs.
+ */
+ resize: function () {
+ for (let graph of Monitor.graphs.values()) {
+ graph.resize();
+ }
+ },
+
+ /**
+ * When WebIDE connects to a new runtime, start its data forwarders.
+ */
+ onAppManagerUpdate: function (event, what, details) {
+ switch (what) {
+ case "runtime-global-actors":
+ Monitor.connectToRuntime();
+ break;
+ case "connection":
+ if (AppManager.connection.status == Connection.Status.DISCONNECTED) {
+ Monitor.disconnectFromRuntime();
+ }
+ break;
+ }
+ },
+
+ /**
+ * Use an AppActorFront on a runtime to watch track its apps.
+ */
+ connectToRuntime: function () {
+ Monitor.pollB2GInfo();
+ let client = AppManager.connection && AppManager.connection.client;
+ let resp = AppManager._listTabsResponse;
+ if (client && resp && !Monitor.front) {
+ Monitor.front = new AppActorFront(client, resp);
+ Monitor.front.watchApps(Monitor.onRuntimeAppEvent);
+ }
+ },
+
+ /**
+ * Destroy our AppActorFront.
+ */
+ disconnectFromRuntime: function () {
+ Monitor.unpollB2GInfo();
+ if (Monitor.front) {
+ Monitor.front.unwatchApps(Monitor.onRuntimeAppEvent);
+ Monitor.front = null;
+ }
+ },
+
+ /**
+ * Try connecting to a local websockets server and accept updates from it.
+ */
+ connectToWebSocket: function () {
+ let webSocketURL = Services.prefs.getCharPref("devtools.webide.monitorWebSocketURL");
+ try {
+ Monitor.socket = new WebSocket(webSocketURL);
+ Monitor.socket.onmessage = function (event) {
+ Monitor.update(JSON.parse(event.data));
+ };
+ Monitor.socket.onclose = function () {
+ Monitor.wstimeout = setTimeout(Monitor.connectToWebsocket, 1000);
+ };
+ } catch (e) {
+ Monitor.wstimeout = setTimeout(Monitor.connectToWebsocket, 1000);
+ }
+ },
+
+ /**
+ * Used when cleaning up.
+ */
+ disconnectFromWebSocket: function () {
+ clearTimeout(Monitor.wstimeout);
+ if (Monitor.socket) {
+ Monitor.socket.onclose = () => {};
+ Monitor.socket.close();
+ }
+ },
+
+ /**
+ * When an app starts on the runtime, start a monitor actor for its process.
+ */
+ onRuntimeAppEvent: function (type, app) {
+ if (type !== "appOpen" && type !== "appClose") {
+ return;
+ }
+
+ let client = AppManager.connection.client;
+ app.getForm().then(form => {
+ if (type === "appOpen") {
+ app.monitorClient = new MonitorClient(client, form);
+ app.monitorClient.start();
+ app.monitorClient.on("update", Monitor.onRuntimeUpdate);
+ Monitor.apps.set(form.monitorActor, app);
+ } else {
+ let app = Monitor.apps.get(form.monitorActor);
+ if (app) {
+ app.monitorClient.stop(() => app.monitorClient.destroy());
+ Monitor.apps.delete(form.monitorActor);
+ }
+ }
+ });
+ },
+
+ /**
+ * Accept data updates from the monitor actors of a runtime.
+ */
+ onRuntimeUpdate: function (type, packet) {
+ let fallback = {}, app = Monitor.apps.get(packet.from);
+ if (app) {
+ fallback.curve = app.manifest.name;
+ }
+ Monitor.update(packet.data, fallback);
+ },
+
+ /**
+ * Bug 1047355: If possible, parsing the output of `b2g-info` has several
+ * benefits over bug 1037465's multi-process USSAgent approach, notably:
+ * - Works for older Firefox OS devices (pre-2.1),
+ * - Doesn't need certified-apps debugging,
+ * - Polling time is synchronized for all processes.
+ * TODO: After bug 1043324 lands, consider removing this hack.
+ */
+ pollB2GInfo: function () {
+ if (AppManager.selectedRuntime) {
+ let device = AppManager.selectedRuntime.device;
+ if (device && device.shell) {
+ device.shell("b2g-info").then(s => {
+ let lines = s.split("\n");
+ let line = "";
+
+ // Find the header row to locate NAME and USS, looks like:
+ // ' NAME PID NICE USS PSS RSS VSIZE OOM_ADJ USER '.
+ while (line.indexOf("NAME") < 0) {
+ if (lines.length < 1) {
+ // Something is wrong with this output, don't trust b2g-info.
+ Monitor.unpollB2GInfo();
+ return;
+ }
+ line = lines.shift();
+ }
+ let namelength = line.indexOf("NAME") + "NAME".length;
+ let ussindex = line.slice(namelength).split(/\s+/).indexOf("USS");
+
+ // Get the NAME and USS in each following line, looks like:
+ // 'Homescreen 375 18 12.6 16.3 27.1 67.8 4 app_375'.
+ while (lines.length > 0 && lines[0].length > namelength) {
+ line = lines.shift();
+ let name = line.slice(0, namelength);
+ let uss = line.slice(namelength).split(/\s+/)[ussindex];
+ Monitor.update({
+ curve: name.trim(),
+ value: 1024 * 1024 * parseFloat(uss) // Convert MB to bytes.
+ }, {
+ // Note: We use the fallback object to set the graph name to 'USS'
+ // so that Monitor.update() can ignore USSAgent updates.
+ graph: "USS"
+ });
+ }
+ });
+ }
+ }
+ Monitor.b2ginfo = true;
+ Monitor.b2gtimeout = setTimeout(Monitor.pollB2GInfo, 350);
+ },
+
+ /**
+ * Polling b2g-info doesn't work or is no longer needed.
+ */
+ unpollB2GInfo: function () {
+ clearTimeout(Monitor.b2gtimeout);
+ Monitor.b2ginfo = false;
+ }
+
+};
+
+
+/**
+ * A MonitorClient is used as an actor client of a runtime's monitor actors,
+ * receiving its updates.
+ */
+function MonitorClient(client, form) {
+ this.client = client;
+ this.actor = form.monitorActor;
+ this.events = ["update"];
+
+ EventEmitter.decorate(this);
+ this.client.registerClient(this);
+}
+MonitorClient.prototype.destroy = function () {
+ this.client.unregisterClient(this);
+};
+MonitorClient.prototype.start = function () {
+ this.client.request({
+ to: this.actor,
+ type: "start"
+ });
+};
+MonitorClient.prototype.stop = function (callback) {
+ this.client.request({
+ to: this.actor,
+ type: "stop"
+ }, callback);
+};
+
+
+/**
+ * A Graph populates a container DOM element with an SVG graph and a legend.
+ */
+function Graph(name, element) {
+ this.name = name;
+ this.element = element;
+ this.curves = new Map();
+ this.events = new Map();
+ this.ignored = new Set();
+ this.enabled = true;
+ this.request = null;
+
+ this.x = d3.time.scale();
+ this.y = d3.scale.linear();
+
+ this.xaxis = d3.svg.axis().scale(this.x).orient("bottom");
+ this.yaxis = d3.svg.axis().scale(this.y).orient("left");
+
+ this.xformat = d3.time.format("%I:%M:%S");
+ this.yformat = this.formatter(1);
+ this.yaxis.tickFormat(this.formatter(0));
+
+ this.line = d3.svg.line().interpolate("linear")
+ .x(function (d) { return this.x(d.time); })
+ .y(function (d) { return this.y(d.value); });
+
+ this.color = d3.scale.category10();
+
+ this.svg = d3.select(element).append("svg").append("g")
+ .attr("transform", "translate(" + this.margin.left + "," + this.margin.top + ")");
+
+ this.xelement = this.svg.append("g").attr("class", "x axis").call(this.xaxis);
+ this.yelement = this.svg.append("g").attr("class", "y axis").call(this.yaxis);
+
+ // RULERS on axes
+ let xruler = this.xruler = this.svg.select(".x.axis").append("g").attr("class", "x ruler");
+ xruler.append("line").attr("y2", 6);
+ xruler.append("line").attr("stroke-dasharray", "1,1");
+ xruler.append("text").attr("y", 9).attr("dy", ".71em");
+
+ let yruler = this.yruler = this.svg.select(".y.axis").append("g").attr("class", "y ruler");
+ yruler.append("line").attr("x2", -6);
+ yruler.append("line").attr("stroke-dasharray", "1,1");
+ yruler.append("text").attr("x", -9).attr("dy", ".32em");
+
+ let self = this;
+
+ d3.select(element).select("svg")
+ .on("mousemove", function () {
+ let mouse = d3.mouse(this);
+ self.mousex = mouse[0] - self.margin.left,
+ self.mousey = mouse[1] - self.margin.top;
+
+ xruler.attr("transform", "translate(" + self.mousex + ",0)");
+ yruler.attr("transform", "translate(0," + self.mousey + ")");
+ });
+ /* .on('mouseout', function() {
+ self.xruler.attr('transform', 'translate(-500,0)');
+ self.yruler.attr('transform', 'translate(0,-500)');
+ });*/
+ this.mousex = this.mousey = -500;
+
+ let sidebar = d3.select(this.element).append("div").attr("class", "sidebar");
+ let title = sidebar.append("label").attr("class", "graph-title");
+
+ title.append("input")
+ .attr("type", "checkbox")
+ .attr("checked", "true")
+ .on("click", function () { self.toggle(); });
+ title.append("span").text(this.name);
+
+ this.legend = sidebar.append("div").attr("class", "legend");
+
+ this.resize = this.resize.bind(this);
+ this.render = this.render.bind(this);
+ this.averages = this.averages.bind(this);
+
+ setInterval(this.averages, 1000);
+
+ this.resize();
+}
+
+Graph.prototype = {
+
+ /**
+ * These margin are used to properly position the SVG graph items inside the
+ * container element.
+ */
+ margin: {
+ top: 10,
+ right: 150,
+ bottom: 20,
+ left: 50
+ },
+
+ /**
+ * A Graph can be collapsed by the user.
+ */
+ toggle: function () {
+ if (this.enabled) {
+ this.element.classList.add("disabled");
+ this.enabled = false;
+ } else {
+ this.element.classList.remove("disabled");
+ this.enabled = true;
+ }
+ Monitor.resize();
+ },
+
+ /**
+ * If the container element is resized (e.g. because the window was resized or
+ * a scrollbar dis/appeared), the graph needs to be resized as well.
+ */
+ resize: function () {
+ let style = getComputedStyle(this.element),
+ height = parseFloat(style.height) - this.margin.top - this.margin.bottom,
+ width = parseFloat(style.width) - this.margin.left - this.margin.right;
+
+ d3.select(this.element).select("svg")
+ .attr("width", width + this.margin.left)
+ .attr("height", height + this.margin.top + this.margin.bottom);
+
+ this.x.range([0, width]);
+ this.y.range([height, 0]);
+
+ this.xelement.attr("transform", "translate(0," + height + ")");
+ this.xruler.select("line[stroke-dasharray]").attr("y2", -height);
+ this.yruler.select("line[stroke-dasharray]").attr("x2", width);
+ },
+
+ /**
+ * If the domain of the Graph's data changes (on the time axis and/or on the
+ * value axis), the axes' domains need to be updated and the graph items need
+ * to be rescaled in order to represent all the data.
+ */
+ rescale: function () {
+ let gettime = v => { return v.time; },
+ getvalue = v => { return v.value; },
+ ignored = c => { return this.ignored.has(c.id); };
+
+ let xmin = null, xmax = null, ymin = null, ymax = null;
+ for (let curve of this.curves.values()) {
+ if (ignored(curve)) {
+ continue;
+ }
+ if (xmax == null || curve.xmax > xmax) {
+ xmax = curve.xmax;
+ }
+ if (xmin == null || curve.xmin < xmin) {
+ xmin = curve.xmin;
+ }
+ if (ymax == null || curve.ymax > ymax) {
+ ymax = curve.ymax;
+ }
+ if (ymin == null || curve.ymin < ymin) {
+ ymin = curve.ymin;
+ }
+ }
+ for (let event of this.events.values()) {
+ if (ignored(event)) {
+ continue;
+ }
+ if (xmax == null || event.xmax > xmax) {
+ xmax = event.xmax;
+ }
+ if (xmin == null || event.xmin < xmin) {
+ xmin = event.xmin;
+ }
+ }
+
+ let oldxdomain = this.x.domain();
+ if (xmin != null && xmax != null) {
+ this.x.domain([xmin, xmax]);
+ let newxdomain = this.x.domain();
+ if (newxdomain[0] !== oldxdomain[0] || newxdomain[1] !== oldxdomain[1]) {
+ this.xelement.call(this.xaxis);
+ }
+ }
+
+ let oldydomain = this.y.domain();
+ if (ymin != null && ymax != null) {
+ this.y.domain([ymin, ymax]).nice();
+ let newydomain = this.y.domain();
+ if (newydomain[0] !== oldydomain[0] || newydomain[1] !== oldydomain[1]) {
+ this.yelement.call(this.yaxis);
+ }
+ }
+ },
+
+ /**
+ * Add new values to the graph.
+ */
+ update: function (data) {
+ delete data.graph;
+
+ let time = data.time || Date.now();
+ delete data.time;
+
+ let curve = data.curve;
+ delete data.curve;
+
+ // Single curve value, e.g. { curve: 'memory', value: 42, time: 1234 }.
+ if ("value" in data) {
+ this.push(this.curves, curve, [{time: time, value: data.value}]);
+ delete data.value;
+ }
+
+ // Several curve values, e.g. { curve: 'memory', values: [{value: 42, time: 1234}] }.
+ if ("values" in data) {
+ this.push(this.curves, curve, data.values);
+ delete data.values;
+ }
+
+ // Punctual event, e.g. { event: 'gc', time: 1234 },
+ // event with duration, e.g. { event: 'jank', duration: 425, time: 1234 }.
+ if ("event" in data) {
+ this.push(this.events, data.event, [{time: time, value: data.duration}]);
+ delete data.event;
+ delete data.duration;
+ }
+
+ // Remaining keys are curves, e.g. { time: 1234, memory: 42, battery: 13, temperature: 45 }.
+ for (let key in data) {
+ this.push(this.curves, key, [{time: time, value: data[key]}]);
+ }
+
+ // If no render is currently pending, request one.
+ if (this.enabled && !this.request) {
+ this.request = requestAnimationFrame(this.render);
+ }
+ },
+
+ /**
+ * Insert new data into the graph's data structures.
+ */
+ push: function (collection, id, values) {
+
+ // Note: collection is either `this.curves` or `this.events`.
+ let item = collection.get(id);
+ if (!item) {
+ item = { id: id, values: [], xmin: null, xmax: null, ymin: 0, ymax: null, average: 0 };
+ collection.set(id, item);
+ }
+
+ for (let v of values) {
+ let time = new Date(v.time), value = +v.value;
+ // Update the curve/event's domain values.
+ if (item.xmax == null || time > item.xmax) {
+ item.xmax = time;
+ }
+ if (item.xmin == null || time < item.xmin) {
+ item.xmin = time;
+ }
+ if (item.ymax == null || value > item.ymax) {
+ item.ymax = value;
+ }
+ if (item.ymin == null || value < item.ymin) {
+ item.ymin = value;
+ }
+ // Note: A curve's average is not computed here. Call `graph.averages()`.
+ item.values.push({ time: time, value: value });
+ }
+ },
+
+ /**
+ * Render the SVG graph with curves, events, crosshair and legend.
+ */
+ render: function () {
+ this.request = null;
+ this.rescale();
+
+
+ // DATA
+
+ let self = this,
+ getid = d => { return d.id; },
+ gettime = d => { return d.time.getTime(); },
+ getline = d => { return self.line(d.values); },
+ getcolor = d => { return self.color(d.id); },
+ getvalues = d => { return d.values; },
+ ignored = d => { return self.ignored.has(d.id); };
+
+ // Convert our maps to arrays for d3.
+ let curvedata = [...this.curves.values()],
+ eventdata = [...this.events.values()],
+ data = curvedata.concat(eventdata);
+
+
+ // CURVES
+
+ // Map curve data to curve elements.
+ let curves = this.svg.selectAll(".curve").data(curvedata, getid);
+
+ // Create new curves (no element corresponding to the data).
+ curves.enter().append("g").attr("class", "curve").append("path")
+ .style("stroke", getcolor);
+
+ // Delete old curves (elements corresponding to data not present anymore).
+ curves.exit().remove();
+
+ // Update all curves from data.
+ this.svg.selectAll(".curve").select("path")
+ .attr("d", d => { return ignored(d) ? "" : getline(d); });
+
+ let height = parseFloat(getComputedStyle(this.element).height) - this.margin.top - this.margin.bottom;
+
+
+ // EVENTS
+
+ // Map event data to event elements.
+ let events = this.svg.selectAll(".event-slot").data(eventdata, getid);
+
+ // Create new events.
+ events.enter().append("g").attr("class", "event-slot");
+
+ // Remove old events.
+ events.exit().remove();
+
+ // Get all occurences of an event, and map its data to them.
+ let lines = this.svg.selectAll(".event-slot")
+ .style("stroke", d => { return ignored(d) ? "none" : getcolor(d); })
+ .selectAll(".event")
+ .data(getvalues, gettime);
+
+ // Create new event occurrence.
+ lines.enter().append("line").attr("class", "event").attr("y2", height);
+
+ // Delete old event occurrence.
+ lines.exit().remove();
+
+ // Update all event occurrences from data.
+ this.svg.selectAll(".event")
+ .attr("transform", d => { return "translate(" + self.x(d.time) + ",0)"; });
+
+
+ // CROSSHAIR
+
+ // TODO select curves and events, intersect with curves and show values/hovers
+ // e.g. look like http://code.shutterstock.com/rickshaw/examples/lines.html
+
+ // Update crosshair labels on each axis.
+ this.xruler.select("text").text(self.xformat(self.x.invert(self.mousex)));
+ this.yruler.select("text").text(self.yformat(self.y.invert(self.mousey)));
+
+
+ // LEGEND
+
+ // Map data to legend elements.
+ let legends = this.legend.selectAll("label").data(data, getid);
+
+ // Update averages.
+ legends.attr("title", c => { return "Average: " + self.yformat(c.average); });
+
+ // Create new legends.
+ let newlegend = legends.enter().append("label");
+ newlegend.append("input").attr("type", "checkbox").attr("checked", "true").on("click", function (c) {
+ if (ignored(c)) {
+ this.parentElement.classList.remove("disabled");
+ self.ignored.delete(c.id);
+ } else {
+ this.parentElement.classList.add("disabled");
+ self.ignored.add(c.id);
+ }
+ self.update({}); // if no re-render is pending, request one.
+ });
+ newlegend.append("span").attr("class", "legend-color").style("background-color", getcolor);
+ newlegend.append("span").attr("class", "legend-id").text(getid);
+
+ // Delete old legends.
+ legends.exit().remove();
+ },
+
+ /**
+ * Returns a SI value formatter with a given precision.
+ */
+ formatter: function (decimals) {
+ return value => {
+ // Don't use sub-unit SI prefixes (milli, micro, etc.).
+ if (Math.abs(value) < 1) return value.toFixed(decimals);
+ // SI prefix, e.g. 1234567 will give '1.2M' at precision 1.
+ let prefix = d3.formatPrefix(value);
+ return prefix.scale(value).toFixed(decimals) + prefix.symbol;
+ };
+ },
+
+ /**
+ * Compute the average of each time series.
+ */
+ averages: function () {
+ for (let c of this.curves.values()) {
+ let length = c.values.length;
+ if (length > 0) {
+ let total = 0;
+ c.values.forEach(v => total += v.value);
+ c.average = (total / length);
+ }
+ }
+ },
+
+ /**
+ * Bisect a time serie to find the data point immediately left of `time`.
+ */
+ bisectTime: d3.bisector(d => d.time).left,
+
+ /**
+ * Get all curve values at a given time.
+ */
+ valuesAt: function (time) {
+ let values = { time: time };
+
+ for (let id of this.curves.keys()) {
+ let curve = this.curves.get(id);
+
+ // Find the closest value just before `time`.
+ let i = this.bisectTime(curve.values, time);
+ if (i < 0) {
+ // Curve starts after `time`, use first value.
+ values[id] = curve.values[0].value;
+ } else if (i > curve.values.length - 2) {
+ // Curve ends before `time`, use last value.
+ values[id] = curve.values[curve.values.length - 1].value;
+ } else {
+ // Curve has two values around `time`, interpolate.
+ let v1 = curve.values[i],
+ v2 = curve.values[i + 1],
+ delta = (time - v1.time) / (v2.time - v1.time);
+ values[id] = v1.value + (v2.value - v1.time) * delta;
+ }
+ }
+ return values;
+ }
+
+};
diff --git a/devtools/client/webide/content/monitor.xhtml b/devtools/client/webide/content/monitor.xhtml
new file mode 100644
index 000000000..552f3826c
--- /dev/null
+++ b/devtools/client/webide/content/monitor.xhtml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!-- 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/. -->
+
+<!DOCTYPE html [
+ <!ENTITY % webideDTD SYSTEM "chrome://devtools/locale/webide.dtd" >
+ %webideDTD;
+]>
+
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta charset="utf8"/>
+ <link rel="stylesheet" href="chrome://webide/skin/deck.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://webide/skin/monitor.css" type="text/css"/>
+ <script src="chrome://devtools/content/shared/vendor/d3.js"></script>
+ <script type="application/javascript;version=1.8" src="monitor.js"></script>
+ </head>
+ <body>
+
+ <div id="controls">
+ <a href="https://developer.mozilla.org/docs/Tools/WebIDE/Monitor" target="_blank">&monitor_help;</a>
+ <a id="close">&deck_close;</a>
+ </div>
+
+ <h1>&monitor_title;</h1>
+
+ </body>
+</html>
diff --git a/devtools/client/webide/content/moz.build b/devtools/client/webide/content/moz.build
new file mode 100644
index 000000000..aac3a838c
--- /dev/null
+++ b/devtools/client/webide/content/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+JAR_MANIFESTS += ['jar.mn']
diff --git a/devtools/client/webide/content/newapp.js b/devtools/client/webide/content/newapp.js
new file mode 100644
index 000000000..d47bfabec
--- /dev/null
+++ b/devtools/client/webide/content/newapp.js
@@ -0,0 +1,175 @@
+/* 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 Cc = Components.classes;
+var Cu = Components.utils;
+var Ci = Components.interfaces;
+
+const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const {XPCOMUtils} = require("resource://gre/modules/XPCOMUtils.jsm");
+const Services = require("Services");
+const {FileUtils} = require("resource://gre/modules/FileUtils.jsm");
+const {AppProjects} = require("devtools/client/webide/modules/app-projects");
+const {AppManager} = require("devtools/client/webide/modules/app-manager");
+const {getJSON} = require("devtools/client/shared/getjson");
+
+XPCOMUtils.defineLazyModuleGetter(this, "ZipUtils", "resource://gre/modules/ZipUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Downloads", "resource://gre/modules/Downloads.jsm");
+
+const TEMPLATES_URL = "devtools.webide.templatesURL";
+
+var gTemplateList = null;
+
+// See bug 989619
+console.log = console.log.bind(console);
+console.warn = console.warn.bind(console);
+console.error = console.error.bind(console);
+
+window.addEventListener("load", function onLoad() {
+ window.removeEventListener("load", onLoad);
+ let projectNameNode = document.querySelector("#project-name");
+ projectNameNode.addEventListener("input", canValidate, true);
+ getTemplatesJSON();
+}, true);
+
+function getTemplatesJSON() {
+ getJSON(TEMPLATES_URL).then(list => {
+ if (!Array.isArray(list)) {
+ throw new Error("JSON response not an array");
+ }
+ if (list.length == 0) {
+ throw new Error("JSON response is an empty array");
+ }
+ gTemplateList = list;
+ let templatelistNode = document.querySelector("#templatelist");
+ templatelistNode.innerHTML = "";
+ for (let template of list) {
+ let richlistitemNode = document.createElement("richlistitem");
+ let imageNode = document.createElement("image");
+ imageNode.setAttribute("src", template.icon);
+ let labelNode = document.createElement("label");
+ labelNode.setAttribute("value", template.name);
+ let descriptionNode = document.createElement("description");
+ descriptionNode.textContent = template.description;
+ let vboxNode = document.createElement("vbox");
+ vboxNode.setAttribute("flex", "1");
+ richlistitemNode.appendChild(imageNode);
+ vboxNode.appendChild(labelNode);
+ vboxNode.appendChild(descriptionNode);
+ richlistitemNode.appendChild(vboxNode);
+ templatelistNode.appendChild(richlistitemNode);
+ }
+ templatelistNode.selectedIndex = 0;
+
+ /* Chrome mochitest support */
+ let testOptions = window.arguments[0].testOptions;
+ if (testOptions) {
+ templatelistNode.selectedIndex = testOptions.index;
+ document.querySelector("#project-name").value = testOptions.name;
+ doOK();
+ }
+ }, (e) => {
+ failAndBail("Can't download app templates: " + e);
+ });
+}
+
+function failAndBail(msg) {
+ let promptService = Cc["@mozilla.org/embedcomp/prompt-service;1"].getService(Ci.nsIPromptService);
+ promptService.alert(window, "error", msg);
+ window.close();
+}
+
+function canValidate() {
+ let projectNameNode = document.querySelector("#project-name");
+ let dialogNode = document.querySelector("dialog");
+ if (projectNameNode.value.length > 0) {
+ dialogNode.removeAttribute("buttondisabledaccept");
+ } else {
+ dialogNode.setAttribute("buttondisabledaccept", "true");
+ }
+}
+
+function doOK() {
+ let projectName = document.querySelector("#project-name").value;
+
+ if (!projectName) {
+ console.error("No project name");
+ return false;
+ }
+
+ if (!gTemplateList) {
+ console.error("No template index");
+ return false;
+ }
+
+ let templatelistNode = document.querySelector("#templatelist");
+ if (templatelistNode.selectedIndex < 0) {
+ console.error("No template selected");
+ return false;
+ }
+
+ let folder;
+
+ /* Chrome mochitest support */
+ let testOptions = window.arguments[0].testOptions;
+ if (testOptions) {
+ folder = testOptions.folder;
+ } else {
+ let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ fp.init(window, "Select directory where to create app directory", Ci.nsIFilePicker.modeGetFolder);
+ let res = fp.show();
+ if (res == Ci.nsIFilePicker.returnCancel) {
+ console.error("No directory selected");
+ return false;
+ }
+ folder = fp.file;
+ }
+
+ // Create subfolder with fs-friendly name of project
+ let subfolder = projectName.replace(/[\\/:*?"<>|]/g, "").toLowerCase();
+ let win = Services.wm.getMostRecentWindow("devtools:webide");
+ folder.append(subfolder);
+
+ try {
+ folder.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+ } catch (e) {
+ win.UI.reportError("error_folderCreationFailed");
+ window.close();
+ return false;
+ }
+
+ // Download boilerplate zip
+ let template = gTemplateList[templatelistNode.selectedIndex];
+ let source = template.file;
+ let target = folder.clone();
+ target.append(subfolder + ".zip");
+
+ let bail = (e) => {
+ console.error(e);
+ window.close();
+ };
+
+ Downloads.fetch(source, target).then(() => {
+ ZipUtils.extractFiles(target, folder);
+ target.remove(false);
+ AppProjects.addPackaged(folder).then((project) => {
+ window.arguments[0].location = project.location;
+ AppManager.validateAndUpdateProject(project).then(() => {
+ if (project.manifest) {
+ project.manifest.name = projectName;
+ AppManager.writeManifest(project).then(() => {
+ AppManager.validateAndUpdateProject(project).then(
+ () => {window.close();}, bail);
+ }, bail);
+ } else {
+ bail("Manifest not found");
+ }
+ }, bail);
+ }, bail);
+ }, bail);
+
+ return false;
+}
diff --git a/devtools/client/webide/content/newapp.xul b/devtools/client/webide/content/newapp.xul
new file mode 100644
index 000000000..7ff083519
--- /dev/null
+++ b/devtools/client/webide/content/newapp.xul
@@ -0,0 +1,33 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<!DOCTYPE window [
+ <!ENTITY % webideDTD SYSTEM "chrome://devtools/locale/webide.dtd" >
+ %webideDTD;
+]>
+
+<?xml-stylesheet href="chrome://global/skin/global.css"?>
+<?xml-stylesheet href="chrome://webide/skin/newapp.css"?>
+
+<dialog id="webide:newapp" title="&newAppWindowTitle;"
+ width="600" height="400"
+ buttons="accept,cancel"
+ ondialogaccept="return doOK();"
+ buttondisabledaccept="true"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script type="application/javascript" src="newapp.js"></script>
+ <label class="header-name" value="&newAppHeader;"/>
+
+ <richlistbox id="templatelist" flex="1">
+ <description>&newAppLoadingTemplate;</description>
+ </richlistbox>
+ <vbox>
+ <label class="header-name" control="project-name" value="&newAppProjectName;"/>
+ <textbox id="project-name"/>
+ </vbox>
+
+</dialog>
diff --git a/devtools/client/webide/content/permissionstable.js b/devtools/client/webide/content/permissionstable.js
new file mode 100644
index 000000000..22c74bd0d
--- /dev/null
+++ b/devtools/client/webide/content/permissionstable.js
@@ -0,0 +1,78 @@
+/* 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 Cu = Components.utils;
+const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const Services = require("Services");
+const {AppManager} = require("devtools/client/webide/modules/app-manager");
+const {Connection} = require("devtools/shared/client/connection-manager");
+
+window.addEventListener("load", function onLoad() {
+ window.removeEventListener("load", onLoad);
+ document.querySelector("#close").onclick = CloseUI;
+ AppManager.on("app-manager-update", OnAppManagerUpdate);
+ BuildUI();
+}, true);
+
+window.addEventListener("unload", function onUnload() {
+ window.removeEventListener("unload", onUnload);
+ AppManager.off("app-manager-update", OnAppManagerUpdate);
+});
+
+function CloseUI() {
+ window.parent.UI.openProject();
+}
+
+function OnAppManagerUpdate(event, what) {
+ if (what == "connection" || what == "runtime-global-actors") {
+ BuildUI();
+ }
+}
+
+function generateFields(json) {
+ let table = document.querySelector("table");
+ let permissionsTable = json.rawPermissionsTable;
+ for (let name in permissionsTable) {
+ let tr = document.createElement("tr");
+ tr.className = "line";
+ let td = document.createElement("td");
+ td.textContent = name;
+ tr.appendChild(td);
+ for (let type of ["app", "privileged", "certified"]) {
+ let td = document.createElement("td");
+ if (permissionsTable[name][type] == json.ALLOW_ACTION) {
+ td.textContent = "✓";
+ td.className = "permallow";
+ }
+ if (permissionsTable[name][type] == json.PROMPT_ACTION) {
+ td.textContent = "!";
+ td.className = "permprompt";
+ }
+ if (permissionsTable[name][type] == json.DENY_ACTION) {
+ td.textContent = "✕";
+ td.className = "permdeny";
+ }
+ tr.appendChild(td);
+ }
+ table.appendChild(tr);
+ }
+}
+
+var getRawPermissionsTablePromise; // Used by tests
+function BuildUI() {
+ let table = document.querySelector("table");
+ let lines = table.querySelectorAll(".line");
+ for (let line of lines) {
+ line.remove();
+ }
+
+ if (AppManager.connection &&
+ AppManager.connection.status == Connection.Status.CONNECTED &&
+ AppManager.deviceFront) {
+ getRawPermissionsTablePromise = AppManager.deviceFront.getRawPermissionsTable()
+ .then(json => generateFields(json));
+ } else {
+ CloseUI();
+ }
+}
diff --git a/devtools/client/webide/content/permissionstable.xhtml b/devtools/client/webide/content/permissionstable.xhtml
new file mode 100644
index 000000000..361cfece8
--- /dev/null
+++ b/devtools/client/webide/content/permissionstable.xhtml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!-- 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/. -->
+
+<!DOCTYPE html [
+ <!ENTITY % webideDTD SYSTEM "chrome://devtools/locale/webide.dtd" >
+ %webideDTD;
+]>
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta charset="utf8"/>
+ <link rel="stylesheet" href="chrome://webide/skin/deck.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://webide/skin/permissionstable.css" type="text/css"/>
+ <script type="application/javascript;version=1.8" src="chrome://webide/content/permissionstable.js"></script>
+ </head>
+ <body>
+
+ <div id="controls">
+ <a id="close">&deck_close;</a>
+ </div>
+
+ <h1>&permissionstable_title;</h1>
+
+ <table class="permissionstable">
+ <tr>
+ <th>&permissionstable_name_header;</th>
+ <th>type:web</th>
+ <th>type:privileged</th>
+ <th>type:certified</th>
+ </tr>
+ </table>
+ </body>
+</html>
diff --git a/devtools/client/webide/content/prefs.js b/devtools/client/webide/content/prefs.js
new file mode 100644
index 000000000..75f6233ba
--- /dev/null
+++ b/devtools/client/webide/content/prefs.js
@@ -0,0 +1,108 @@
+/* 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 Cu = Components.utils;
+const {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
+
+window.addEventListener("load", function onLoad() {
+ window.removeEventListener("load", onLoad);
+
+ // Listen to preference changes
+ let inputs = document.querySelectorAll("[data-pref]");
+ for (let i of inputs) {
+ let pref = i.dataset.pref;
+ Services.prefs.addObserver(pref, FillForm, false);
+ i.addEventListener("change", SaveForm, false);
+ }
+
+ // Buttons
+ document.querySelector("#close").onclick = CloseUI;
+ document.querySelector("#restore").onclick = RestoreDefaults;
+ document.querySelector("#manageComponents").onclick = ShowAddons;
+
+ // Initialize the controls
+ FillForm();
+
+}, true);
+
+window.addEventListener("unload", function onUnload() {
+ window.removeEventListener("unload", onUnload);
+ let inputs = document.querySelectorAll("[data-pref]");
+ for (let i of inputs) {
+ let pref = i.dataset.pref;
+ i.removeEventListener("change", SaveForm, false);
+ Services.prefs.removeObserver(pref, FillForm, false);
+ }
+}, true);
+
+function CloseUI() {
+ window.parent.UI.openProject();
+}
+
+function ShowAddons() {
+ window.parent.Cmds.showAddons();
+}
+
+function FillForm() {
+ let inputs = document.querySelectorAll("[data-pref]");
+ for (let i of inputs) {
+ let pref = i.dataset.pref;
+ let val = GetPref(pref);
+ if (i.type == "checkbox") {
+ i.checked = val;
+ } else {
+ i.value = val;
+ }
+ }
+}
+
+function SaveForm(e) {
+ let inputs = document.querySelectorAll("[data-pref]");
+ for (let i of inputs) {
+ let pref = i.dataset.pref;
+ if (i.type == "checkbox") {
+ SetPref(pref, i.checked);
+ } else {
+ SetPref(pref, i.value);
+ }
+ }
+}
+
+function GetPref(name) {
+ let type = Services.prefs.getPrefType(name);
+ switch (type) {
+ case Services.prefs.PREF_STRING:
+ return Services.prefs.getCharPref(name);
+ case Services.prefs.PREF_INT:
+ return Services.prefs.getIntPref(name);
+ case Services.prefs.PREF_BOOL:
+ return Services.prefs.getBoolPref(name);
+ default:
+ throw new Error("Unknown type");
+ }
+}
+
+function SetPref(name, value) {
+ let type = Services.prefs.getPrefType(name);
+ switch (type) {
+ case Services.prefs.PREF_STRING:
+ return Services.prefs.setCharPref(name, value);
+ case Services.prefs.PREF_INT:
+ return Services.prefs.setIntPref(name, value);
+ case Services.prefs.PREF_BOOL:
+ return Services.prefs.setBoolPref(name, value);
+ default:
+ throw new Error("Unknown type");
+ }
+}
+
+function RestoreDefaults() {
+ let inputs = document.querySelectorAll("[data-pref]");
+ for (let i of inputs) {
+ let pref = i.dataset.pref;
+ Services.prefs.clearUserPref(pref);
+ }
+}
diff --git a/devtools/client/webide/content/prefs.xhtml b/devtools/client/webide/content/prefs.xhtml
new file mode 100644
index 000000000..726ca772c
--- /dev/null
+++ b/devtools/client/webide/content/prefs.xhtml
@@ -0,0 +1,112 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!-- 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/. -->
+
+<!DOCTYPE html [
+ <!ENTITY % webideDTD SYSTEM "chrome://devtools/locale/webide.dtd" >
+ %webideDTD;
+]>
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta charset="utf8"/>
+ <link rel="stylesheet" href="chrome://webide/skin/deck.css" type="text/css"/>
+ <script type="application/javascript;version=1.8" src="chrome://webide/content/prefs.js"></script>
+ </head>
+ <body>
+
+ <div id="controls">
+ <a id="restore">&prefs_restore;</a>
+ <a id="manageComponents">&prefs_manage_components;</a>
+ <a id="close">&deck_close;</a>
+ </div>
+
+ <h1>&prefs_title;</h1>
+
+ <h2>&prefs_general_title;</h2>
+
+ <ul>
+ <li>
+ <label title="&prefs_options_showeditor_tooltip;">
+ <input type="checkbox" data-pref="devtools.webide.showProjectEditor"/>
+ <span>&prefs_options_showeditor;</span>
+ </label>
+ </li>
+ <li>
+ <label title="&prefs_options_rememberlastproject_tooltip;">
+ <input type="checkbox" data-pref="devtools.webide.restoreLastProject"/>
+ <span>&prefs_options_rememberlastproject;</span>
+ </label>
+ </li>
+ <li>
+ <label title="&prefs_options_autoconnectruntime_tooltip;">
+ <input type="checkbox" data-pref="devtools.webide.autoConnectRuntime"/>
+ <span>&prefs_options_autoconnectruntime;</span>
+ </label>
+ </li>
+ <li>
+ <label class="text-input" title="&prefs_options_templatesurl_tooltip;">
+ <span>&prefs_options_templatesurl;</span>
+ <input data-pref="devtools.webide.templatesURL"/>
+ </label>
+ </li>
+ </ul>
+
+ <h2>&prefs_editor_title;</h2>
+
+ <ul>
+ <li>
+ <label><span>&prefs_options_tabsize;</span>
+ <select data-pref="devtools.editor.tabsize">
+ <option value="2">2</option>
+ <option value="4">4</option>
+ <option value="8">8</option>
+ </select>
+ </label>
+ </li>
+ <li>
+ <label title="&prefs_options_expandtab_tooltip;">
+ <input type="checkbox" data-pref="devtools.editor.expandtab"/>
+ <span>&prefs_options_expandtab;</span>
+ </label>
+ </li>
+ <li>
+ <label title="&prefs_options_detectindentation_tooltip;">
+ <input type="checkbox" data-pref="devtools.editor.detectindentation"/>
+ <span>&prefs_options_detectindentation;</span>
+ </label>
+ </li>
+ <li>
+ <label title="&prefs_options_autocomplete_tooltip;">
+ <input type="checkbox" data-pref="devtools.editor.autocomplete"/>
+ <span>&prefs_options_autocomplete;</span>
+ </label>
+ </li>
+ <li>
+ <label title="&prefs_options_autoclosebrackets_tooltip;">
+ <input type="checkbox" data-pref="devtools.editor.autoclosebrackets"/>
+ <span>&prefs_options_autoclosebrackets;</span>
+ </label>
+ </li>
+ <li>
+ <label title="&prefs_options_autosavefiles_tooltip;">
+ <input type="checkbox" data-pref="devtools.webide.autosaveFiles"/>
+ <span>&prefs_options_autosavefiles;</span>
+ </label>
+ </li>
+ <li>
+ <label><span>&prefs_options_keybindings;</span>
+ <select data-pref="devtools.editor.keymap">
+ <option value="default">&prefs_options_keybindings_default;</option>
+ <option value="vim">Vim</option>
+ <option value="emacs">Emacs</option>
+ <option value="sublime">Sublime</option>
+ </select>
+ </label>
+ </li>
+ </ul>
+
+ </body>
+</html>
diff --git a/devtools/client/webide/content/project-listing.js b/devtools/client/webide/content/project-listing.js
new file mode 100644
index 000000000..5641f6c0c
--- /dev/null
+++ b/devtools/client/webide/content/project-listing.js
@@ -0,0 +1,42 @@
+/* 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/. */
+
+/* eslint-env browser */
+
+var Cu = Components.utils;
+const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const ProjectList = require("devtools/client/webide/modules/project-list");
+
+var projectList = new ProjectList(window, window.parent);
+
+window.addEventListener("load", function onLoad() {
+ window.removeEventListener("load", onLoad, true);
+ document.getElementById("new-app").onclick = CreateNewApp;
+ document.getElementById("hosted-app").onclick = ImportHostedApp;
+ document.getElementById("packaged-app").onclick = ImportPackagedApp;
+ document.getElementById("refresh-tabs").onclick = RefreshTabs;
+ projectList.update();
+ projectList.updateCommands();
+}, true);
+
+window.addEventListener("unload", function onUnload() {
+ window.removeEventListener("unload", onUnload);
+ projectList.destroy();
+});
+
+function RefreshTabs() {
+ projectList.refreshTabs();
+}
+
+function CreateNewApp() {
+ projectList.newApp();
+}
+
+function ImportHostedApp() {
+ projectList.importHostedApp();
+}
+
+function ImportPackagedApp() {
+ projectList.importPackagedApp();
+}
diff --git a/devtools/client/webide/content/project-listing.xhtml b/devtools/client/webide/content/project-listing.xhtml
new file mode 100644
index 000000000..337befe5d
--- /dev/null
+++ b/devtools/client/webide/content/project-listing.xhtml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!-- 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/. -->
+
+<!DOCTYPE html [
+ <!ENTITY % webideDTD SYSTEM "chrome://devtools/locale/webide.dtd" >
+ %webideDTD;
+]>
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta charset="utf8"/>
+ <link rel="stylesheet" href="chrome://webide/skin/panel-listing.css" type="text/css"/>
+ <script type="application/javascript;version=1.8" src="chrome://webide/content/project-listing.js"></script>
+ </head>
+ <body>
+ <div id="project-panel">
+ <div id="project-panel-box">
+ <button class="panel-item project-panel-item-newapp" id="new-app">&projectMenu_newApp_label;</button>
+ <button class="panel-item project-panel-item-openpackaged" id="packaged-app">&projectMenu_importPackagedApp_label;</button>
+ <button class="panel-item project-panel-item-openhosted" id="hosted-app">&projectMenu_importHostedApp_label;</button>
+ <label class="panel-header">&projectPanel_myProjects;</label>
+ <div id="project-panel-projects"></div>
+ <label class="panel-header" id="panel-header-runtimeapps" hidden="true">&projectPanel_runtimeApps;</label>
+ <div id="project-panel-runtimeapps"/>
+ <label class="panel-header" id="panel-header-tabs" hidden="true">&projectPanel_tabs;
+ <button class="project-panel-item-refreshtabs refresh-icon" id="refresh-tabs" title="&projectMenu_refreshTabs_label;"></button>
+ </label>
+ <div id="project-panel-tabs"/>
+ </div>
+ </div>
+ </body>
+</html>
diff --git a/devtools/client/webide/content/project-panel.js b/devtools/client/webide/content/project-panel.js
new file mode 100644
index 000000000..54eab8251
--- /dev/null
+++ b/devtools/client/webide/content/project-panel.js
@@ -0,0 +1,11 @@
+/* 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 ProjectPanel = {
+ // TODO: Expand function to save toggle state.
+ toggleSidebar: function () {
+ document.querySelector("#project-listing-panel").setAttribute("sidebar-displayed", true);
+ document.querySelector("#project-listing-splitter").setAttribute("sidebar-displayed", true);
+ }
+};
diff --git a/devtools/client/webide/content/runtime-listing.js b/devtools/client/webide/content/runtime-listing.js
new file mode 100644
index 000000000..0a1a40a2a
--- /dev/null
+++ b/devtools/client/webide/content/runtime-listing.js
@@ -0,0 +1,66 @@
+/* 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 Cu = Components.utils;
+const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const RuntimeList = require("devtools/client/webide/modules/runtime-list");
+
+var runtimeList = new RuntimeList(window, window.parent);
+
+window.addEventListener("load", function onLoad() {
+ window.removeEventListener("load", onLoad, true);
+ document.getElementById("runtime-screenshot").onclick = TakeScreenshot;
+ document.getElementById("runtime-permissions").onclick = ShowPermissionsTable;
+ document.getElementById("runtime-details").onclick = ShowRuntimeDetails;
+ document.getElementById("runtime-disconnect").onclick = DisconnectRuntime;
+ document.getElementById("runtime-preferences").onclick = ShowDevicePreferences;
+ document.getElementById("runtime-settings").onclick = ShowSettings;
+ document.getElementById("runtime-panel-installsimulator").onclick = ShowAddons;
+ document.getElementById("runtime-panel-noadbhelper").onclick = ShowAddons;
+ document.getElementById("runtime-panel-nousbdevice").onclick = ShowTroubleShooting;
+ document.getElementById("refresh-devices").onclick = RefreshScanners;
+ runtimeList.update();
+ runtimeList.updateCommands();
+}, true);
+
+window.addEventListener("unload", function onUnload() {
+ window.removeEventListener("unload", onUnload);
+ runtimeList.destroy();
+});
+
+function TakeScreenshot() {
+ runtimeList.takeScreenshot();
+}
+
+function ShowRuntimeDetails() {
+ runtimeList.showRuntimeDetails();
+}
+
+function ShowPermissionsTable() {
+ runtimeList.showPermissionsTable();
+}
+
+function ShowDevicePreferences() {
+ runtimeList.showDevicePreferences();
+}
+
+function ShowSettings() {
+ runtimeList.showSettings();
+}
+
+function RefreshScanners() {
+ runtimeList.refreshScanners();
+}
+
+function DisconnectRuntime() {
+ window.parent.Cmds.disconnectRuntime();
+}
+
+function ShowAddons() {
+ runtimeList.showAddons();
+}
+
+function ShowTroubleShooting() {
+ runtimeList.showTroubleShooting();
+}
diff --git a/devtools/client/webide/content/runtime-listing.xhtml b/devtools/client/webide/content/runtime-listing.xhtml
new file mode 100644
index 000000000..f648fac12
--- /dev/null
+++ b/devtools/client/webide/content/runtime-listing.xhtml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!-- 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/. -->
+
+<!DOCTYPE html [
+ <!ENTITY % webideDTD SYSTEM "chrome://devtools/locale/webide.dtd" >
+ %webideDTD;
+]>
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta charset="utf8"/>
+ <link rel="stylesheet" href="chrome://webide/skin/panel-listing.css" type="text/css"/>
+ <script type="application/javascript;version=1.8" src="chrome://webide/content/runtime-listing.js"></script>
+ </head>
+ <body>
+ <div id="runtime-panel">
+ <div id="runtime-panel-box">
+ <label class="panel-header">&runtimePanel_usb;
+ <button class="runtime-panel-item-refreshdevices refresh-icon" id="refresh-devices" title="&runtimePanel_refreshDevices_label;"></button>
+ </label>
+ <button class="panel-item" id="runtime-panel-nousbdevice">&runtimePanel_nousbdevice;</button>
+ <button class="panel-item" id="runtime-panel-noadbhelper">&runtimePanel_noadbhelper;</button>
+ <div id="runtime-panel-usb"></div>
+ <label class="panel-header" id="runtime-header-wifi">&runtimePanel_wifi;</label>
+ <div id="runtime-panel-wifi"></div>
+ <label class="panel-header">&runtimePanel_simulator;</label>
+ <div id="runtime-panel-simulator"></div>
+ <button class="panel-item" id="runtime-panel-installsimulator">&runtimePanel_installsimulator;</button>
+ <label class="panel-header">&runtimePanel_other;</label>
+ <div id="runtime-panel-other"></div>
+ <div id="runtime-actions">
+ <button class="panel-item" id="runtime-details">&runtimeMenu_showDetails_label;</button>
+ <button class="panel-item" id="runtime-permissions">&runtimeMenu_showPermissionTable_label;</button>
+ <button class="panel-item" id="runtime-preferences">&runtimeMenu_showDevicePrefs_label;</button>
+ <button class="panel-item" id="runtime-settings">&runtimeMenu_showSettings_label;</button>
+ <button class="panel-item" id="runtime-screenshot">&runtimeMenu_takeScreenshot_label;</button>
+ <button class="panel-item" id="runtime-disconnect">&runtimeMenu_disconnect_label;</button>
+ </div>
+ </div>
+ </div>
+ </body>
+</html>
diff --git a/devtools/client/webide/content/runtime-panel.js b/devtools/client/webide/content/runtime-panel.js
new file mode 100644
index 000000000..3646fa15c
--- /dev/null
+++ b/devtools/client/webide/content/runtime-panel.js
@@ -0,0 +1,11 @@
+/* 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 RuntimePanel = {
+ // TODO: Expand function to save toggle state.
+ toggleSidebar: function () {
+ document.querySelector("#runtime-listing-panel").setAttribute("sidebar-displayed", true);
+ document.querySelector("#runtime-listing-splitter").setAttribute("sidebar-displayed", true);
+ }
+};
diff --git a/devtools/client/webide/content/runtimedetails.js b/devtools/client/webide/content/runtimedetails.js
new file mode 100644
index 000000000..dea423e81
--- /dev/null
+++ b/devtools/client/webide/content/runtimedetails.js
@@ -0,0 +1,153 @@
+/* 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 Cu = Components.utils;
+const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const Services = require("Services");
+const {AppManager} = require("devtools/client/webide/modules/app-manager");
+const {Connection} = require("devtools/shared/client/connection-manager");
+const {RuntimeTypes} = require("devtools/client/webide/modules/runtimes");
+const Strings = Services.strings.createBundle("chrome://devtools/locale/webide.properties");
+
+const UNRESTRICTED_HELP_URL = "https://developer.mozilla.org/docs/Tools/WebIDE/Running_and_debugging_apps#Unrestricted_app_debugging_%28including_certified_apps_main_process_etc.%29";
+
+window.addEventListener("load", function onLoad() {
+ window.removeEventListener("load", onLoad);
+ document.querySelector("#close").onclick = CloseUI;
+ document.querySelector("#devtools-check button").onclick = EnableCertApps;
+ document.querySelector("#adb-check button").onclick = RootADB;
+ document.querySelector("#unrestricted-privileges").onclick = function () {
+ window.parent.UI.openInBrowser(UNRESTRICTED_HELP_URL);
+ };
+ AppManager.on("app-manager-update", OnAppManagerUpdate);
+ BuildUI();
+ CheckLockState();
+}, true);
+
+window.addEventListener("unload", function onUnload() {
+ window.removeEventListener("unload", onUnload);
+ AppManager.off("app-manager-update", OnAppManagerUpdate);
+});
+
+function CloseUI() {
+ window.parent.UI.openProject();
+}
+
+function OnAppManagerUpdate(event, what) {
+ if (what == "connection" || what == "runtime-global-actors") {
+ BuildUI();
+ CheckLockState();
+ }
+}
+
+function generateFields(json) {
+ let table = document.querySelector("table");
+ for (let name in json) {
+ let tr = document.createElement("tr");
+ let td = document.createElement("td");
+ td.textContent = name;
+ tr.appendChild(td);
+ td = document.createElement("td");
+ td.textContent = json[name];
+ tr.appendChild(td);
+ table.appendChild(tr);
+ }
+}
+
+var getDescriptionPromise; // Used by tests
+function BuildUI() {
+ let table = document.querySelector("table");
+ table.innerHTML = "";
+ if (AppManager.connection &&
+ AppManager.connection.status == Connection.Status.CONNECTED &&
+ AppManager.deviceFront) {
+ getDescriptionPromise = AppManager.deviceFront.getDescription()
+ .then(json => generateFields(json));
+ } else {
+ CloseUI();
+ }
+}
+
+function CheckLockState() {
+ let adbCheckResult = document.querySelector("#adb-check > .yesno");
+ let devtoolsCheckResult = document.querySelector("#devtools-check > .yesno");
+ let flipCertPerfButton = document.querySelector("#devtools-check button");
+ let adbRootButton = document.querySelector("#adb-check button");
+ let flipCertPerfAction = document.querySelector("#devtools-check > .action");
+ let adbRootAction = document.querySelector("#adb-check > .action");
+
+ let sYes = Strings.GetStringFromName("runtimedetails_checkyes");
+ let sNo = Strings.GetStringFromName("runtimedetails_checkno");
+ let sUnknown = Strings.GetStringFromName("runtimedetails_checkunknown");
+ let sNotUSB = Strings.GetStringFromName("runtimedetails_notUSBDevice");
+
+ flipCertPerfButton.setAttribute("disabled", "true");
+ flipCertPerfAction.setAttribute("hidden", "true");
+ adbRootAction.setAttribute("hidden", "true");
+
+ adbCheckResult.textContent = sUnknown;
+ devtoolsCheckResult.textContent = sUnknown;
+
+ if (AppManager.connection &&
+ AppManager.connection.status == Connection.Status.CONNECTED) {
+
+ // ADB check
+ if (AppManager.selectedRuntime.type === RuntimeTypes.USB) {
+ let device = AppManager.selectedRuntime.device;
+ if (device && device.summonRoot) {
+ device.isRoot().then(isRoot => {
+ if (isRoot) {
+ adbCheckResult.textContent = sYes;
+ flipCertPerfButton.removeAttribute("disabled");
+ } else {
+ adbCheckResult.textContent = sNo;
+ adbRootAction.removeAttribute("hidden");
+ }
+ }, e => console.error(e));
+ } else {
+ adbCheckResult.textContent = sUnknown;
+ }
+ } else {
+ adbCheckResult.textContent = sNotUSB;
+ }
+
+ // forbid-certified-apps check
+ try {
+ let prefFront = AppManager.preferenceFront;
+ prefFront.getBoolPref("devtools.debugger.forbid-certified-apps").then(isForbidden => {
+ if (isForbidden) {
+ devtoolsCheckResult.textContent = sNo;
+ flipCertPerfAction.removeAttribute("hidden");
+ } else {
+ devtoolsCheckResult.textContent = sYes;
+ }
+ }, e => console.error(e));
+ } catch (e) {
+ // Exception. pref actor is only accessible if forbird-certified-apps is false
+ devtoolsCheckResult.textContent = sNo;
+ flipCertPerfAction.removeAttribute("hidden");
+ }
+
+ }
+
+}
+
+function EnableCertApps() {
+ let device = AppManager.selectedRuntime.device;
+ // TODO: Remove `network.disable.ipc.security` once bug 1125916 is fixed.
+ device.shell(
+ "stop b2g && " +
+ "cd /data/b2g/mozilla/*.default/ && " +
+ "echo 'user_pref(\"devtools.debugger.forbid-certified-apps\", false);' >> prefs.js && " +
+ "echo 'user_pref(\"dom.apps.developer_mode\", true);' >> prefs.js && " +
+ "echo 'user_pref(\"network.disable.ipc.security\", true);' >> prefs.js && " +
+ "echo 'user_pref(\"dom.webcomponents.enabled\", true);' >> prefs.js && " +
+ "start b2g"
+ );
+}
+
+function RootADB() {
+ let device = AppManager.selectedRuntime.device;
+ device.summonRoot().then(CheckLockState, (e) => console.error(e));
+}
diff --git a/devtools/client/webide/content/runtimedetails.xhtml b/devtools/client/webide/content/runtimedetails.xhtml
new file mode 100644
index 000000000..b2f74728a
--- /dev/null
+++ b/devtools/client/webide/content/runtimedetails.xhtml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!-- 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/. -->
+
+<!DOCTYPE html [
+ <!ENTITY % webideDTD SYSTEM "chrome://devtools/locale/webide.dtd" >
+ %webideDTD;
+]>
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta charset="utf8"/>
+ <link rel="stylesheet" href="chrome://webide/skin/deck.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://webide/skin/runtimedetails.css" type="text/css"/>
+ <script type="application/javascript;version=1.8" src="chrome://webide/content/runtimedetails.js"></script>
+ </head>
+ <body>
+
+ <div id="controls">
+ <a id="close">&deck_close;</a>
+ </div>
+
+ <h1>&runtimedetails_title;</h1>
+
+ <div id="devicePrivileges">
+ <p id="adb-check">
+ &runtimedetails_adbIsRoot;<span class="yesno"></span>
+ <div class="action">
+ <button>&runtimedetails_summonADBRoot;</button>
+ <em>&runtimedetails_ADBRootWarning;</em>
+ </div>
+ </p>
+ <p id="devtools-check">
+ <a id="unrestricted-privileges">&runtimedetails_unrestrictedPrivileges;</a><span class="yesno"></span>
+ <div class="action">
+ <button>&runtimedetails_requestPrivileges;</button>
+ <em>&runtimedetails_privilegesWarning;</em>
+ </div>
+ </p>
+ </div>
+
+ <table></table>
+ </body>
+</html>
diff --git a/devtools/client/webide/content/simulator.js b/devtools/client/webide/content/simulator.js
new file mode 100644
index 000000000..ddc1cbed1
--- /dev/null
+++ b/devtools/client/webide/content/simulator.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/. */
+
+var Cu = Components.utils;
+var Ci = Components.interfaces;
+
+const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const { getDevices, getDeviceString } = require("devtools/client/shared/devices");
+const { Simulators, Simulator } = require("devtools/client/webide/modules/simulators");
+const Services = require("Services");
+const EventEmitter = require("devtools/shared/event-emitter");
+const promise = require("promise");
+const utils = require("devtools/client/webide/modules/utils");
+
+const Strings = Services.strings.createBundle("chrome://devtools/locale/webide.properties");
+
+var SimulatorEditor = {
+
+ // Available Firefox OS Simulator addons (key: `addon.id`).
+ _addons: {},
+
+ // Available device simulation profiles (key: `device.name`).
+ _devices: {},
+
+ // The names of supported simulation options.
+ _deviceOptions: [],
+
+ // The <form> element used to edit Simulator options.
+ _form: null,
+
+ // The Simulator object being edited.
+ _simulator: null,
+
+ // Generate the dynamic form elements.
+ init() {
+ let promises = [];
+
+ // Grab the <form> element.
+ let form = this._form;
+ if (!form) {
+ // This is the first time we run `init()`, bootstrap some things.
+ form = this._form = document.querySelector("#simulator-editor");
+ form.addEventListener("change", this.update.bind(this));
+ Simulators.on("configure", (e, simulator) => { this.edit(simulator); });
+ // Extract the list of device simulation options we'll support.
+ let deviceFields = form.querySelectorAll("*[data-device]");
+ this._deviceOptions = Array.map(deviceFields, field => field.name);
+ }
+
+ // Append a new <option> to a <select> (or <optgroup>) element.
+ function opt(select, value, text) {
+ let option = document.createElement("option");
+ option.value = value;
+ option.textContent = text;
+ select.appendChild(option);
+ }
+
+ // Generate B2G version selector.
+ promises.push(Simulators.findSimulatorAddons().then(addons => {
+ this._addons = {};
+ form.version.innerHTML = "";
+ form.version.classList.remove("custom");
+ addons.forEach(addon => {
+ this._addons[addon.id] = addon;
+ opt(form.version, addon.id, addon.name);
+ });
+ opt(form.version, "custom", "");
+ opt(form.version, "pick", Strings.GetStringFromName("simulator_custom_binary"));
+ }));
+
+ // Generate profile selector.
+ form.profile.innerHTML = "";
+ form.profile.classList.remove("custom");
+ opt(form.profile, "default", Strings.GetStringFromName("simulator_default_profile"));
+ opt(form.profile, "custom", "");
+ opt(form.profile, "pick", Strings.GetStringFromName("simulator_custom_profile"));
+
+ // Generate example devices list.
+ form.device.innerHTML = "";
+ form.device.classList.remove("custom");
+ opt(form.device, "custom", Strings.GetStringFromName("simulator_custom_device"));
+ promises.push(getDevices().then(devices => {
+ devices.TYPES.forEach(type => {
+ let b2gDevices = devices[type].filter(d => d.firefoxOS);
+ if (b2gDevices.length < 1) {
+ return;
+ }
+ let optgroup = document.createElement("optgroup");
+ optgroup.label = getDeviceString(type);
+ b2gDevices.forEach(device => {
+ this._devices[device.name] = device;
+ opt(optgroup, device.name, device.name);
+ });
+ form.device.appendChild(optgroup);
+ });
+ }));
+
+ return promise.all(promises);
+ },
+
+ // Edit the configuration of an existing Simulator, or create a new one.
+ edit(simulator) {
+ // If no Simulator was given to edit, we're creating a new one.
+ if (!simulator) {
+ simulator = new Simulator(); // Default options.
+ Simulators.add(simulator);
+ }
+
+ this._simulator = null;
+
+ return this.init().then(() => {
+ this._simulator = simulator;
+
+ // Update the form fields.
+ this._form.name.value = simulator.name;
+
+ this.updateVersionSelector();
+ this.updateProfileSelector();
+ this.updateDeviceSelector();
+ this.updateDeviceFields();
+
+ // Change visibility of 'TV Simulator Menu'.
+ let tvSimMenu = document.querySelector("#tv_simulator_menu");
+ tvSimMenu.style.visibility = (this._simulator.type === "television") ?
+ "visible" : "hidden";
+
+ // Trigger any listener waiting for this update
+ let change = document.createEvent("HTMLEvents");
+ change.initEvent("change", true, true);
+ this._form.dispatchEvent(change);
+ });
+ },
+
+ // Open the directory of TV Simulator config.
+ showTVConfigDirectory() {
+ let profD = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ profD.append("extensions");
+ profD.append(this._simulator.addon.id);
+ profD.append("profile");
+ profD.append("dummy");
+ let profileDir = profD.path;
+
+ // Show the profile directory.
+ let nsLocalFile = Components.Constructor("@mozilla.org/file/local;1",
+ "nsILocalFile", "initWithPath");
+ new nsLocalFile(profileDir).reveal();
+ },
+
+ // Close the configuration panel.
+ close() {
+ this._simulator = null;
+ window.parent.UI.openProject();
+ },
+
+ // Restore the simulator to its default configuration.
+ restoreDefaults() {
+ let simulator = this._simulator;
+ this.version = simulator.addon.id;
+ this.profile = "default";
+ simulator.restoreDefaults();
+ Simulators.emitUpdated();
+ return this.edit(simulator);
+ },
+
+ // Delete this simulator.
+ deleteSimulator() {
+ Simulators.remove(this._simulator);
+ this.close();
+ },
+
+ // Select an available option, or set the "custom" option.
+ updateSelector(selector, value) {
+ selector.value = value;
+ if (selector.selectedIndex == -1) {
+ selector.value = "custom";
+ selector.classList.add("custom");
+ selector[selector.selectedIndex].textContent = value;
+ }
+ },
+
+ // VERSION: Can be an installed `addon.id` or a custom binary path.
+
+ get version() {
+ return this._simulator.options.b2gBinary || this._simulator.addon.id;
+ },
+
+ set version(value) {
+ let form = this._form;
+ let simulator = this._simulator;
+ let oldVer = simulator.version;
+ if (this._addons[value]) {
+ // `value` is a simulator addon ID.
+ simulator.addon = this._addons[value];
+ simulator.options.b2gBinary = null;
+ } else {
+ // `value` is a custom binary path.
+ simulator.options.b2gBinary = value;
+ // TODO (Bug 1146531) Indicate that a custom profile is now required.
+ }
+ // If `form.name` contains the old version, update its last occurrence.
+ if (form.name.value.includes(oldVer) && simulator.version !== oldVer) {
+ let regex = new RegExp("(.*)" + oldVer);
+ let name = form.name.value.replace(regex, "$1" + simulator.version);
+ simulator.options.name = form.name.value = Simulators.uniqueName(name);
+ }
+ },
+
+ updateVersionSelector() {
+ this.updateSelector(this._form.version, this.version);
+ },
+
+ // PROFILE. Can be "default" or a custom profile directory path.
+
+ get profile() {
+ return this._simulator.options.gaiaProfile || "default";
+ },
+
+ set profile(value) {
+ this._simulator.options.gaiaProfile = (value == "default" ? null : value);
+ },
+
+ updateProfileSelector() {
+ this.updateSelector(this._form.profile, this.profile);
+ },
+
+ // DEVICE. Can be an existing `device.name` or "custom".
+
+ get device() {
+ let devices = this._devices;
+ let simulator = this._simulator;
+
+ // Search for the name of a device matching current simulator options.
+ for (let name in devices) {
+ let match = true;
+ for (let option of this._deviceOptions) {
+ if (simulator.options[option] === devices[name][option]) {
+ continue;
+ }
+ match = false;
+ break;
+ }
+ if (match) {
+ return name;
+ }
+ }
+ return "custom";
+ },
+
+ set device(name) {
+ let device = this._devices[name];
+ if (!device) {
+ return;
+ }
+ let form = this._form;
+ let simulator = this._simulator;
+ this._deviceOptions.forEach(option => {
+ simulator.options[option] = form[option].value = device[option] || null;
+ });
+ // TODO (Bug 1146531) Indicate when a custom profile is required (e.g. for
+ // tablet, TV…).
+ },
+
+ updateDeviceSelector() {
+ this.updateSelector(this._form.device, this.device);
+ },
+
+ // Erase any current values, trust only the `simulator.options`.
+ updateDeviceFields() {
+ let form = this._form;
+ let simulator = this._simulator;
+ this._deviceOptions.forEach(option => {
+ form[option].value = simulator.options[option];
+ });
+ },
+
+ // Handle a change in our form's fields.
+ update(event) {
+ let simulator = this._simulator;
+ if (!simulator) {
+ return;
+ }
+ let form = this._form;
+ let input = event.target;
+ switch (input.name) {
+ case "name":
+ simulator.options.name = input.value;
+ break;
+ case "version":
+ switch (input.value) {
+ case "pick":
+ let file = utils.getCustomBinary(window);
+ if (file) {
+ this.version = file.path;
+ }
+ // Whatever happens, don't stay on the "pick" option.
+ this.updateVersionSelector();
+ break;
+ case "custom":
+ this.version = input[input.selectedIndex].textContent;
+ break;
+ default:
+ this.version = input.value;
+ }
+ break;
+ case "profile":
+ switch (input.value) {
+ case "pick":
+ let directory = utils.getCustomProfile(window);
+ if (directory) {
+ this.profile = directory.path;
+ }
+ // Whatever happens, don't stay on the "pick" option.
+ this.updateProfileSelector();
+ break;
+ case "custom":
+ this.profile = input[input.selectedIndex].textContent;
+ break;
+ default:
+ this.profile = input.value;
+ }
+ break;
+ case "device":
+ this.device = input.value;
+ break;
+ default:
+ simulator.options[input.name] = input.value || null;
+ this.updateDeviceSelector();
+ }
+ Simulators.emitUpdated();
+ },
+};
+
+window.addEventListener("load", function onLoad() {
+ document.querySelector("#close").onclick = e => {
+ SimulatorEditor.close();
+ };
+ document.querySelector("#reset").onclick = e => {
+ SimulatorEditor.restoreDefaults();
+ };
+ document.querySelector("#remove").onclick = e => {
+ SimulatorEditor.deleteSimulator();
+ };
+
+ // We just loaded, so we probably missed the first configure request.
+ SimulatorEditor.edit(Simulators._lastConfiguredSimulator);
+
+ document.querySelector("#open-tv-dummy-directory").onclick = e => {
+ SimulatorEditor.showTVConfigDirectory();
+ e.preventDefault();
+ };
+});
diff --git a/devtools/client/webide/content/simulator.xhtml b/devtools/client/webide/content/simulator.xhtml
new file mode 100644
index 000000000..3ab916248
--- /dev/null
+++ b/devtools/client/webide/content/simulator.xhtml
@@ -0,0 +1,99 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!-- 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/. -->
+
+<!DOCTYPE html [
+ <!ENTITY % webideDTD SYSTEM "chrome://devtools/locale/webide.dtd" >
+ %webideDTD;
+]>
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta charset="utf8"/>
+ <link rel="stylesheet" href="chrome://webide/skin/deck.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://webide/skin/simulator.css" type="text/css"/>
+ <script type="application/javascript;version=1.8" src="chrome://webide/content/simulator.js"></script>
+ </head>
+ <body>
+
+ <div id="controls">
+ <a id="remove" class="hidden">&simulator_remove;</a>
+ <a id="reset">&simulator_reset;</a>
+ <a id="close">&deck_close;</a>
+ </div>
+
+ <form id="simulator-editor">
+
+ <h1>&simulator_title;</h1>
+
+ <h2>&simulator_software;</h2>
+
+ <ul>
+ <li>
+ <label>
+ <span class="label">&simulator_name;</span>
+ <input type="text" name="name"/>
+ </label>
+ </li>
+ <li>
+ <label>
+ <span class="label">&simulator_version;</span>
+ <select name="version"/>
+ </label>
+ </li>
+ <li>
+ <label>
+ <span class="label">&simulator_profile;</span>
+ <select name="profile"/>
+ </label>
+ </li>
+ </ul>
+
+ <h2>&simulator_hardware;</h2>
+
+ <ul>
+ <li>
+ <label>
+ <span class="label">&simulator_device;</span>
+ <select name="device"/>
+ </label>
+ </li>
+ <li>
+ <label>
+ <span class="label">&simulator_screenSize;</span>
+ <input name="width" data-device="" type="number"/>
+ <span>×</span>
+ <input name="height" data-device="" type="number"/>
+ </label>
+ </li>
+ <li class="hidden">
+ <label>
+ <span class="label">&simulator_pixelRatio;</span>
+ <input name="pixelRatio" data-device="" type="number" step="0.05"/>
+ </label>
+ </li>
+ </ul>
+
+ <!-- This menu is shown when simulator type is television-->
+ <p id="tv_simulator_menu" style="visibility:hidden;">
+ <h2>&simulator_tv_data;</h2>
+
+ <ul>
+ <li>
+ <label>
+ <span class="label">&simulator_tv_data_open;</span>
+ <button id="open-tv-dummy-directory">
+ &simulator_tv_data_open_button;
+ </button>
+ </label>
+ </li>
+ </ul>
+
+ </p>
+
+ </form>
+
+ </body>
+</html>
diff --git a/devtools/client/webide/content/webide.js b/devtools/client/webide/content/webide.js
new file mode 100644
index 000000000..c222332e3
--- /dev/null
+++ b/devtools/client/webide/content/webide.js
@@ -0,0 +1,1157 @@
+/* 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 Cu = Components.utils;
+var Ci = Components.interfaces;
+
+const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const {gDevTools} = require("devtools/client/framework/devtools");
+const {gDevToolsBrowser} = require("devtools/client/framework/devtools-browser");
+const {Toolbox} = require("devtools/client/framework/toolbox");
+const Services = require("Services");
+const {AppProjects} = require("devtools/client/webide/modules/app-projects");
+const {Connection} = require("devtools/shared/client/connection-manager");
+const {AppManager} = require("devtools/client/webide/modules/app-manager");
+const EventEmitter = require("devtools/shared/event-emitter");
+const promise = require("promise");
+const ProjectEditor = require("devtools/client/projecteditor/lib/projecteditor");
+const {GetAvailableAddons} = require("devtools/client/webide/modules/addons");
+const {getJSON} = require("devtools/client/shared/getjson");
+const utils = require("devtools/client/webide/modules/utils");
+const Telemetry = require("devtools/client/shared/telemetry");
+const {RuntimeScanners} = require("devtools/client/webide/modules/runtimes");
+const {showDoorhanger} = require("devtools/client/shared/doorhanger");
+const {Simulators} = require("devtools/client/webide/modules/simulators");
+const {Task} = require("devtools/shared/task");
+
+const Strings = Services.strings.createBundle("chrome://devtools/locale/webide.properties");
+
+const HTML = "http://www.w3.org/1999/xhtml";
+const HELP_URL = "https://developer.mozilla.org/docs/Tools/WebIDE/Troubleshooting";
+
+const MAX_ZOOM = 1.4;
+const MIN_ZOOM = 0.6;
+
+const MS_PER_DAY = 86400000;
+
+[["AppManager", AppManager],
+ ["AppProjects", AppProjects],
+ ["Connection", Connection]].forEach(([key, value]) => {
+ Object.defineProperty(this, key, {
+ value: value,
+ enumerable: true,
+ writable: false
+ });
+ });
+
+// Download remote resources early
+getJSON("devtools.webide.addonsURL");
+getJSON("devtools.webide.templatesURL");
+getJSON("devtools.devices.url");
+
+// See bug 989619
+console.log = console.log.bind(console);
+console.warn = console.warn.bind(console);
+console.error = console.error.bind(console);
+
+window.addEventListener("load", function onLoad() {
+ window.removeEventListener("load", onLoad);
+ UI.init();
+});
+
+window.addEventListener("unload", function onUnload() {
+ window.removeEventListener("unload", onUnload);
+ UI.destroy();
+});
+
+var UI = {
+ init: function () {
+ this._telemetry = new Telemetry();
+ this._telemetry.toolOpened("webide");
+
+ AppManager.init();
+
+ this.appManagerUpdate = this.appManagerUpdate.bind(this);
+ AppManager.on("app-manager-update", this.appManagerUpdate);
+
+ Cmds.showProjectPanel();
+ Cmds.showRuntimePanel();
+
+ this.updateCommands();
+
+ this.onfocus = this.onfocus.bind(this);
+ window.addEventListener("focus", this.onfocus, true);
+
+ AppProjects.load().then(() => {
+ this.autoSelectProject();
+ }, e => {
+ console.error(e);
+ this.reportError("error_appProjectsLoadFailed");
+ });
+
+ // Auto install the ADB Addon Helper and Tools Adapters. Only once.
+ // If the user decides to uninstall any of this addon, we won't install it again.
+ let autoinstallADBHelper = Services.prefs.getBoolPref("devtools.webide.autoinstallADBHelper");
+ let autoinstallFxdtAdapters = Services.prefs.getBoolPref("devtools.webide.autoinstallFxdtAdapters");
+ if (autoinstallADBHelper) {
+ GetAvailableAddons().then(addons => {
+ addons.adb.install();
+ }, console.error);
+ }
+ if (autoinstallFxdtAdapters) {
+ GetAvailableAddons().then(addons => {
+ addons.adapters.install();
+ }, console.error);
+ }
+ Services.prefs.setBoolPref("devtools.webide.autoinstallADBHelper", false);
+ Services.prefs.setBoolPref("devtools.webide.autoinstallFxdtAdapters", false);
+
+ if (Services.prefs.getBoolPref("devtools.webide.widget.autoinstall") &&
+ !Services.prefs.getBoolPref("devtools.webide.widget.enabled")) {
+ Services.prefs.setBoolPref("devtools.webide.widget.enabled", true);
+ gDevToolsBrowser.moveWebIDEWidgetInNavbar();
+ }
+
+ this.setupDeck();
+
+ this.contentViewer = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShell)
+ .contentViewer;
+ this.contentViewer.fullZoom = Services.prefs.getCharPref("devtools.webide.zoom");
+
+ gDevToolsBrowser.isWebIDEInitialized.resolve();
+
+ this.configureSimulator = this.configureSimulator.bind(this);
+ Simulators.on("configure", this.configureSimulator);
+ },
+
+ destroy: function () {
+ window.removeEventListener("focus", this.onfocus, true);
+ AppManager.off("app-manager-update", this.appManagerUpdate);
+ AppManager.destroy();
+ Simulators.off("configure", this.configureSimulator);
+ this.updateConnectionTelemetry();
+ this._telemetry.toolClosed("webide");
+ this._telemetry.toolClosed("webideProjectEditor");
+ this._telemetry.destroy();
+ },
+
+ canCloseProject: function () {
+ if (this.projecteditor) {
+ return this.projecteditor.confirmUnsaved();
+ }
+ return true;
+ },
+
+ onfocus: function () {
+ // Because we can't track the activity in the folder project,
+ // we need to validate the project regularly. Let's assume that
+ // if a modification happened, it happened when the window was
+ // not focused.
+ if (AppManager.selectedProject &&
+ AppManager.selectedProject.type != "mainProcess" &&
+ AppManager.selectedProject.type != "runtimeApp" &&
+ AppManager.selectedProject.type != "tab") {
+ AppManager.validateAndUpdateProject(AppManager.selectedProject);
+ }
+
+ // Hook to display promotional Developer Edition doorhanger. Only displayed once.
+ // Hooked into the `onfocus` event because sometimes does not work
+ // when run at the end of `init`. ¯\(°_o)/¯
+ showDoorhanger({ window, type: "deveditionpromo", anchor: document.querySelector("#deck") });
+ },
+
+ appManagerUpdate: function (event, what, details) {
+ // Got a message from app-manager.js
+ // See AppManager.update() for descriptions of what these events mean.
+ switch (what) {
+ case "runtime-list":
+ this.autoConnectRuntime();
+ break;
+ case "connection":
+ this.updateRuntimeButton();
+ this.updateCommands();
+ this.updateConnectionTelemetry();
+ break;
+ case "before-project":
+ if (!this.canCloseProject()) {
+ details.cancel();
+ }
+ break;
+ case "project":
+ this._updatePromise = Task.spawn(function* () {
+ UI.updateTitle();
+ yield UI.destroyToolbox();
+ UI.updateCommands();
+ UI.openProject();
+ yield UI.autoStartProject();
+ UI.autoOpenToolbox();
+ UI.saveLastSelectedProject();
+ UI.updateRemoveProjectButton();
+ });
+ return;
+ case "project-started":
+ this.updateCommands();
+ UI.autoOpenToolbox();
+ break;
+ case "project-stopped":
+ UI.destroyToolbox();
+ this.updateCommands();
+ break;
+ case "runtime-global-actors":
+ // Check runtime version only on runtime-global-actors,
+ // as we expect to use device actor
+ this.checkRuntimeVersion();
+ this.updateCommands();
+ break;
+ case "runtime-details":
+ this.updateRuntimeButton();
+ break;
+ case "runtime":
+ this.updateRuntimeButton();
+ this.saveLastConnectedRuntime();
+ break;
+ case "project-validated":
+ this.updateTitle();
+ this.updateCommands();
+ this.updateProjectEditorHeader();
+ break;
+ case "install-progress":
+ this.updateProgress(Math.round(100 * details.bytesSent / details.totalBytes));
+ break;
+ case "runtime-targets":
+ this.autoSelectProject();
+ break;
+ case "pre-package":
+ this.prePackageLog(details);
+ break;
+ }
+ this._updatePromise = promise.resolve();
+ },
+
+ configureSimulator: function (event, simulator) {
+ UI.selectDeckPanel("simulator");
+ },
+
+ openInBrowser: function (url) {
+ // Open a URL in a Firefox window
+ let mainWindow = Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
+ if (mainWindow) {
+ mainWindow.openUILinkIn(url, "tab");
+ mainWindow.focus()
+ } else {
+ window.open(url);
+ }
+ },
+
+ updateTitle: function () {
+ let project = AppManager.selectedProject;
+ if (project) {
+ window.document.title = Strings.formatStringFromName("title_app", [project.name], 1);
+ } else {
+ window.document.title = Strings.GetStringFromName("title_noApp");
+ }
+ },
+
+ /** ******** BUSY UI **********/
+
+ _busyTimeout: null,
+ _busyOperationDescription: null,
+ _busyPromise: null,
+
+ updateProgress: function (percent) {
+ let progress = document.querySelector("#action-busy-determined");
+ progress.mode = "determined";
+ progress.value = percent;
+ this.setupBusyTimeout();
+ },
+
+ busy: function () {
+ let win = document.querySelector("window");
+ win.classList.add("busy");
+ win.classList.add("busy-undetermined");
+ this.updateCommands();
+ this.update("busy");
+ },
+
+ unbusy: function () {
+ let win = document.querySelector("window");
+ win.classList.remove("busy");
+ win.classList.remove("busy-determined");
+ win.classList.remove("busy-undetermined");
+ this.updateCommands();
+ this.update("unbusy");
+ this._busyPromise = null;
+ },
+
+ setupBusyTimeout: function () {
+ this.cancelBusyTimeout();
+ this._busyTimeout = setTimeout(() => {
+ this.unbusy();
+ UI.reportError("error_operationTimeout", this._busyOperationDescription);
+ }, Services.prefs.getIntPref("devtools.webide.busyTimeout"));
+ },
+
+ cancelBusyTimeout: function () {
+ clearTimeout(this._busyTimeout);
+ },
+
+ busyWithProgressUntil: function (promise, operationDescription) {
+ let busy = this.busyUntil(promise, operationDescription);
+ let win = document.querySelector("window");
+ let progress = document.querySelector("#action-busy-determined");
+ progress.mode = "undetermined";
+ win.classList.add("busy-determined");
+ win.classList.remove("busy-undetermined");
+ return busy;
+ },
+
+ busyUntil: function (promise, operationDescription) {
+ // Freeze the UI until the promise is resolved. A timeout will unfreeze the
+ // UI, just in case the promise never gets resolved.
+ this._busyPromise = promise;
+ this._busyOperationDescription = operationDescription;
+ this.setupBusyTimeout();
+ this.busy();
+ promise.then(() => {
+ this.cancelBusyTimeout();
+ this.unbusy();
+ }, (e) => {
+ let message;
+ if (e && e.error && e.message) {
+ // Some errors come from fronts that are not based on protocol.js.
+ // Errors are not translated to strings.
+ message = operationDescription + " (" + e.error + "): " + e.message;
+ } else {
+ message = operationDescription + (e ? (": " + e) : "");
+ }
+ this.cancelBusyTimeout();
+ let operationCanceled = e && e.canceled;
+ if (!operationCanceled) {
+ UI.reportError("error_operationFail", message);
+ if (e) {
+ console.error(e);
+ }
+ }
+ this.unbusy();
+ });
+ return promise;
+ },
+
+ reportError: function (l10nProperty, ...l10nArgs) {
+ let text;
+
+ if (l10nArgs.length > 0) {
+ text = Strings.formatStringFromName(l10nProperty, l10nArgs, l10nArgs.length);
+ } else {
+ text = Strings.GetStringFromName(l10nProperty);
+ }
+
+ console.error(text);
+
+ let buttons = [{
+ label: Strings.GetStringFromName("notification_showTroubleShooting_label"),
+ accessKey: Strings.GetStringFromName("notification_showTroubleShooting_accesskey"),
+ callback: function () {
+ Cmds.showTroubleShooting();
+ }
+ }];
+
+ let nbox = document.querySelector("#notificationbox");
+ nbox.removeAllNotifications(true);
+ nbox.appendNotification(text, "webide:errornotification", null,
+ nbox.PRIORITY_WARNING_LOW, buttons);
+ },
+
+ dismissErrorNotification: function () {
+ let nbox = document.querySelector("#notificationbox");
+ nbox.removeAllNotifications(true);
+ },
+
+ /** ******** COMMANDS **********/
+
+ /**
+ * This module emits various events when state changes occur.
+ *
+ * The events this module may emit include:
+ * busy:
+ * The window is currently busy and certain UI functions may be disabled.
+ * unbusy:
+ * The window is not busy and certain UI functions may be re-enabled.
+ */
+ update: function (what, details) {
+ this.emit("webide-update", what, details);
+ },
+
+ updateCommands: function () {
+ // Action commands
+ let playCmd = document.querySelector("#cmd_play");
+ let stopCmd = document.querySelector("#cmd_stop");
+ let debugCmd = document.querySelector("#cmd_toggleToolbox");
+ let playButton = document.querySelector("#action-button-play");
+ let projectPanelCmd = document.querySelector("#cmd_showProjectPanel");
+
+ if (document.querySelector("window").classList.contains("busy")) {
+ playCmd.setAttribute("disabled", "true");
+ stopCmd.setAttribute("disabled", "true");
+ debugCmd.setAttribute("disabled", "true");
+ projectPanelCmd.setAttribute("disabled", "true");
+ return;
+ }
+
+ if (!AppManager.selectedProject || !AppManager.connected) {
+ playCmd.setAttribute("disabled", "true");
+ stopCmd.setAttribute("disabled", "true");
+ debugCmd.setAttribute("disabled", "true");
+ } else {
+ let isProjectRunning = AppManager.isProjectRunning();
+ if (isProjectRunning) {
+ playButton.classList.add("reload");
+ stopCmd.removeAttribute("disabled");
+ debugCmd.removeAttribute("disabled");
+ } else {
+ playButton.classList.remove("reload");
+ stopCmd.setAttribute("disabled", "true");
+ debugCmd.setAttribute("disabled", "true");
+ }
+
+ // If connected and a project is selected
+ if (AppManager.selectedProject.type == "runtimeApp") {
+ playCmd.removeAttribute("disabled");
+ } else if (AppManager.selectedProject.type == "tab") {
+ playCmd.removeAttribute("disabled");
+ stopCmd.setAttribute("disabled", "true");
+ } else if (AppManager.selectedProject.type == "mainProcess") {
+ playCmd.setAttribute("disabled", "true");
+ stopCmd.setAttribute("disabled", "true");
+ } else {
+ if (AppManager.selectedProject.errorsCount == 0 &&
+ AppManager.runtimeCanHandleApps()) {
+ playCmd.removeAttribute("disabled");
+ } else {
+ playCmd.setAttribute("disabled", "true");
+ }
+ }
+ }
+
+ // Runtime commands
+ let monitorCmd = document.querySelector("#cmd_showMonitor");
+ let screenshotCmd = document.querySelector("#cmd_takeScreenshot");
+ let permissionsCmd = document.querySelector("#cmd_showPermissionsTable");
+ let detailsCmd = document.querySelector("#cmd_showRuntimeDetails");
+ let disconnectCmd = document.querySelector("#cmd_disconnectRuntime");
+ let devicePrefsCmd = document.querySelector("#cmd_showDevicePrefs");
+ let settingsCmd = document.querySelector("#cmd_showSettings");
+
+ if (AppManager.connected) {
+ if (AppManager.deviceFront) {
+ monitorCmd.removeAttribute("disabled");
+ detailsCmd.removeAttribute("disabled");
+ permissionsCmd.removeAttribute("disabled");
+ screenshotCmd.removeAttribute("disabled");
+ }
+ if (AppManager.preferenceFront) {
+ devicePrefsCmd.removeAttribute("disabled");
+ }
+ if (AppManager.settingsFront) {
+ settingsCmd.removeAttribute("disabled");
+ }
+ disconnectCmd.removeAttribute("disabled");
+ } else {
+ monitorCmd.setAttribute("disabled", "true");
+ detailsCmd.setAttribute("disabled", "true");
+ permissionsCmd.setAttribute("disabled", "true");
+ screenshotCmd.setAttribute("disabled", "true");
+ disconnectCmd.setAttribute("disabled", "true");
+ devicePrefsCmd.setAttribute("disabled", "true");
+ settingsCmd.setAttribute("disabled", "true");
+ }
+
+ let runtimePanelButton = document.querySelector("#runtime-panel-button");
+
+ if (AppManager.connected) {
+ runtimePanelButton.setAttribute("active", "true");
+ runtimePanelButton.removeAttribute("hidden");
+ } else {
+ runtimePanelButton.removeAttribute("active");
+ runtimePanelButton.setAttribute("hidden", "true");
+ }
+
+ projectPanelCmd.removeAttribute("disabled");
+ },
+
+ updateRemoveProjectButton: function () {
+ // Remove command
+ let removeCmdNode = document.querySelector("#cmd_removeProject");
+ if (AppManager.selectedProject) {
+ removeCmdNode.removeAttribute("disabled");
+ } else {
+ removeCmdNode.setAttribute("disabled", "true");
+ }
+ },
+
+ /** ******** RUNTIME **********/
+
+ get lastConnectedRuntime() {
+ return Services.prefs.getCharPref("devtools.webide.lastConnectedRuntime");
+ },
+
+ set lastConnectedRuntime(runtime) {
+ Services.prefs.setCharPref("devtools.webide.lastConnectedRuntime", runtime);
+ },
+
+ autoConnectRuntime: function () {
+ // Automatically reconnect to the previously selected runtime,
+ // if available and has an ID and feature is enabled
+ if (AppManager.selectedRuntime ||
+ !Services.prefs.getBoolPref("devtools.webide.autoConnectRuntime") ||
+ !this.lastConnectedRuntime) {
+ return;
+ }
+ let [_, type, id] = this.lastConnectedRuntime.match(/^(\w+):(.+)$/);
+
+ type = type.toLowerCase();
+
+ // Local connection is mapped to AppManager.runtimeList.other array
+ if (type == "local") {
+ type = "other";
+ }
+
+ // We support most runtimes except simulator, that needs to be manually
+ // launched
+ if (type == "usb" || type == "wifi" || type == "other") {
+ for (let runtime of AppManager.runtimeList[type]) {
+ // Some runtimes do not expose an id and don't support autoconnect (like
+ // remote connection)
+ if (runtime.id == id) {
+ // Only want one auto-connect attempt, so clear last runtime value
+ this.lastConnectedRuntime = "";
+ this.connectToRuntime(runtime);
+ }
+ }
+ }
+ },
+
+ connectToRuntime: function (runtime) {
+ let name = runtime.name;
+ let promise = AppManager.connectToRuntime(runtime);
+ promise.then(() => this.initConnectionTelemetry())
+ .catch(() => {
+ // Empty rejection handler to silence uncaught rejection warnings
+ // |busyUntil| will listen for rejections.
+ // Bug 1121100 may find a better way to silence these.
+ });
+ promise = this.busyUntil(promise, "Connecting to " + name);
+ // Stop busy timeout for runtimes that take unknown or long amounts of time
+ // to connect.
+ if (runtime.prolongedConnection) {
+ this.cancelBusyTimeout();
+ }
+ return promise;
+ },
+
+ updateRuntimeButton: function () {
+ let labelNode = document.querySelector("#runtime-panel-button > .panel-button-label");
+ if (!AppManager.selectedRuntime) {
+ labelNode.setAttribute("value", Strings.GetStringFromName("runtimeButton_label"));
+ } else {
+ let name = AppManager.selectedRuntime.name;
+ labelNode.setAttribute("value", name);
+ }
+ },
+
+ saveLastConnectedRuntime: function () {
+ if (AppManager.selectedRuntime &&
+ AppManager.selectedRuntime.id !== undefined) {
+ this.lastConnectedRuntime = AppManager.selectedRuntime.type + ":" +
+ AppManager.selectedRuntime.id;
+ } else {
+ this.lastConnectedRuntime = "";
+ }
+ },
+
+ /** ******** ACTIONS **********/
+
+ _actionsToLog: new Set(),
+
+ /**
+ * For each new connection, track whether play and debug were ever used. Only
+ * one value is collected for each button, even if they are used multiple
+ * times during a connection.
+ */
+ initConnectionTelemetry: function () {
+ this._actionsToLog.add("play");
+ this._actionsToLog.add("debug");
+ },
+
+ /**
+ * Action occurred. Log that it happened, and remove it from the loggable
+ * set.
+ */
+ onAction: function (action) {
+ if (!this._actionsToLog.has(action)) {
+ return;
+ }
+ this.logActionState(action, true);
+ this._actionsToLog.delete(action);
+ },
+
+ /**
+ * Connection status changed or we are shutting down. Record any loggable
+ * actions as having not occurred.
+ */
+ updateConnectionTelemetry: function () {
+ for (let action of this._actionsToLog.values()) {
+ this.logActionState(action, false);
+ }
+ this._actionsToLog.clear();
+ },
+
+ logActionState: function (action, state) {
+ let histogramId = "DEVTOOLS_WEBIDE_CONNECTION_" +
+ action.toUpperCase() + "_USED";
+ this._telemetry.log(histogramId, state);
+ },
+
+ /** ******** PROJECTS **********/
+
+ // ProjectEditor & details screen
+
+ destroyProjectEditor: function () {
+ if (this.projecteditor) {
+ this.projecteditor.destroy();
+ this.projecteditor = null;
+ }
+ },
+
+ /**
+ * Called when selecting or deselecting the project editor panel.
+ */
+ onChangeProjectEditorSelected: function () {
+ if (this.projecteditor) {
+ let panel = document.querySelector("#deck").selectedPanel;
+ if (panel && panel.id == "deck-panel-projecteditor") {
+ this.projecteditor.menuEnabled = true;
+ this._telemetry.toolOpened("webideProjectEditor");
+ } else {
+ this.projecteditor.menuEnabled = false;
+ this._telemetry.toolClosed("webideProjectEditor");
+ }
+ }
+ },
+
+ getProjectEditor: function () {
+ if (this.projecteditor) {
+ return this.projecteditor.loaded;
+ }
+
+ let projecteditorIframe = document.querySelector("#deck-panel-projecteditor");
+ this.projecteditor = ProjectEditor.ProjectEditor(projecteditorIframe, {
+ menubar: document.querySelector("#main-menubar"),
+ menuindex: 1
+ });
+ this.projecteditor.on("onEditorSave", () => {
+ AppManager.validateAndUpdateProject(AppManager.selectedProject);
+ this._telemetry.actionOccurred("webideProjectEditorSave");
+ });
+ return this.projecteditor.loaded;
+ },
+
+ updateProjectEditorHeader: function () {
+ let project = AppManager.selectedProject;
+ if (!project || !this.projecteditor) {
+ return;
+ }
+ let status = project.validationStatus || "unknown";
+ if (status == "error warning") {
+ status = "error";
+ }
+ this.getProjectEditor().then((projecteditor) => {
+ projecteditor.setProjectToAppPath(project.location, {
+ name: project.name,
+ iconUrl: project.icon,
+ projectOverviewURL: "chrome://webide/content/details.xhtml",
+ validationStatus: status
+ }).then(null, console.error);
+ }, console.error);
+ },
+
+ isProjectEditorEnabled: function () {
+ return Services.prefs.getBoolPref("devtools.webide.showProjectEditor");
+ },
+
+ openProject: function () {
+ let project = AppManager.selectedProject;
+
+ // Nothing to show
+
+ if (!project) {
+ this.resetDeck();
+ return;
+ }
+
+ // Make sure the directory exist before we show Project Editor
+
+ let forceDetailsOnly = false;
+ if (project.type == "packaged") {
+ forceDetailsOnly = !utils.doesFileExist(project.location);
+ }
+
+ // Show only the details screen
+
+ if (project.type != "packaged" ||
+ !this.isProjectEditorEnabled() ||
+ forceDetailsOnly) {
+ this.selectDeckPanel("details");
+ return;
+ }
+
+ // Show ProjectEditor
+
+ this.getProjectEditor().then(() => {
+ this.updateProjectEditorHeader();
+ }, console.error);
+
+ this.selectDeckPanel("projecteditor");
+ },
+
+ autoStartProject: Task.async(function* () {
+ let project = AppManager.selectedProject;
+
+ if (!project) {
+ return;
+ }
+ if (!(project.type == "runtimeApp" ||
+ project.type == "mainProcess" ||
+ project.type == "tab")) {
+ return; // For something that is not an editable app, we're done.
+ }
+
+ // Do not force opening apps that are already running, as they may have
+ // some activity being opened and don't want to dismiss them.
+ if (project.type == "runtimeApp" && !AppManager.isProjectRunning()) {
+ yield UI.busyUntil(AppManager.launchRuntimeApp(), "running app");
+ }
+ }),
+
+ autoOpenToolbox: Task.async(function* () {
+ let project = AppManager.selectedProject;
+
+ if (!project) {
+ return;
+ }
+ if (!(project.type == "runtimeApp" ||
+ project.type == "mainProcess" ||
+ project.type == "tab")) {
+ return; // For something that is not an editable app, we're done.
+ }
+
+ yield UI.createToolbox();
+ }),
+
+ importAndSelectApp: Task.async(function* (source) {
+ let isPackaged = !!source.path;
+ let project;
+ try {
+ project = yield AppProjects[isPackaged ? "addPackaged" : "addHosted"](source);
+ } catch (e) {
+ if (e === "Already added") {
+ // Select project that's already been added,
+ // and allow it to be revalidated and selected
+ project = AppProjects.get(isPackaged ? source.path : source);
+ } else {
+ throw e;
+ }
+ }
+
+ // Select project
+ AppManager.selectedProject = project;
+
+ this._telemetry.actionOccurred("webideImportProject");
+ }),
+
+ // Remember the last selected project on the runtime
+ saveLastSelectedProject: function () {
+ let shouldRestore = Services.prefs.getBoolPref("devtools.webide.restoreLastProject");
+ if (!shouldRestore) {
+ return;
+ }
+
+ // Ignore unselection of project on runtime disconnection
+ if (!AppManager.connected) {
+ return;
+ }
+
+ let project = "", type = "";
+ let selected = AppManager.selectedProject;
+ if (selected) {
+ if (selected.type == "runtimeApp") {
+ type = "runtimeApp";
+ project = selected.app.manifestURL;
+ } else if (selected.type == "mainProcess") {
+ type = "mainProcess";
+ } else if (selected.type == "packaged" ||
+ selected.type == "hosted") {
+ type = "local";
+ project = selected.location;
+ }
+ }
+ if (type) {
+ Services.prefs.setCharPref("devtools.webide.lastSelectedProject",
+ type + ":" + project);
+ } else {
+ Services.prefs.clearUserPref("devtools.webide.lastSelectedProject");
+ }
+ },
+
+ autoSelectProject: function () {
+ if (AppManager.selectedProject) {
+ return;
+ }
+ let shouldRestore = Services.prefs.getBoolPref("devtools.webide.restoreLastProject");
+ if (!shouldRestore) {
+ return;
+ }
+ let pref = Services.prefs.getCharPref("devtools.webide.lastSelectedProject");
+ if (!pref) {
+ return;
+ }
+ let m = pref.match(/^(\w+):(.*)$/);
+ if (!m) {
+ return;
+ }
+ let [_, type, project] = m;
+
+ if (type == "local") {
+ let lastProject = AppProjects.get(project);
+ if (lastProject) {
+ AppManager.selectedProject = lastProject;
+ }
+ }
+
+ // For other project types, we need to be connected to the runtime
+ if (!AppManager.connected) {
+ return;
+ }
+
+ if (type == "mainProcess" && AppManager.isMainProcessDebuggable()) {
+ AppManager.selectedProject = {
+ type: "mainProcess",
+ name: Strings.GetStringFromName("mainProcess_label"),
+ icon: AppManager.DEFAULT_PROJECT_ICON
+ };
+ } else if (type == "runtimeApp") {
+ let app = AppManager.apps.get(project);
+ if (app) {
+ AppManager.selectedProject = {
+ type: "runtimeApp",
+ app: app.manifest,
+ icon: app.iconURL,
+ name: app.manifest.name
+ };
+ }
+ }
+ },
+
+ /** ******** DECK **********/
+
+ setupDeck: function () {
+ let iframes = document.querySelectorAll("#deck > iframe");
+ for (let iframe of iframes) {
+ iframe.tooltip = "aHTMLTooltip";
+ }
+ },
+
+ resetFocus: function () {
+ document.commandDispatcher.focusedElement = document.documentElement;
+ },
+
+ selectDeckPanel: function (id) {
+ let deck = document.querySelector("#deck");
+ if (deck.selectedPanel && deck.selectedPanel.id === "deck-panel-" + id) {
+ // This panel is already displayed.
+ return;
+ }
+ this.resetFocus();
+ let panel = deck.querySelector("#deck-panel-" + id);
+ let lazysrc = panel.getAttribute("lazysrc");
+ if (lazysrc) {
+ panel.removeAttribute("lazysrc");
+ panel.setAttribute("src", lazysrc);
+ }
+ deck.selectedPanel = panel;
+ this.onChangeProjectEditorSelected();
+ },
+
+ resetDeck: function () {
+ this.resetFocus();
+ let deck = document.querySelector("#deck");
+ deck.selectedPanel = null;
+ this.onChangeProjectEditorSelected();
+ },
+
+ buildIDToDate(buildID) {
+ let fields = buildID.match(/(\d{4})(\d{2})(\d{2})/);
+ // Date expects 0 - 11 for months
+ return new Date(fields[1], Number.parseInt(fields[2]) - 1, fields[3]);
+ },
+
+ checkRuntimeVersion: Task.async(function* () {
+ if (AppManager.connected && AppManager.deviceFront) {
+ let desc = yield AppManager.deviceFront.getDescription();
+ // Compare device and firefox build IDs
+ // and only compare by day (strip hours/minutes) to prevent
+ // warning against builds of the same day.
+ let deviceID = desc.appbuildid.substr(0, 8);
+ let localID = Services.appinfo.appBuildID.substr(0, 8);
+ let deviceDate = this.buildIDToDate(deviceID);
+ let localDate = this.buildIDToDate(localID);
+ // Allow device to be newer by up to a week. This accommodates those with
+ // local device builds, since their devices will almost always be newer
+ // than the client.
+ if (deviceDate - localDate > 7 * MS_PER_DAY) {
+ this.reportError("error_runtimeVersionTooRecent", deviceID, localID);
+ }
+ }
+ }),
+
+ /** ******** TOOLBOX **********/
+
+ /**
+ * There are many ways to close a toolbox:
+ * * Close button inside the toolbox
+ * * Toggle toolbox wrench in WebIDE
+ * * Disconnect the current runtime gracefully
+ * * Yank cord out of device
+ * * Close or crash the app/tab
+ * We can't know for sure which one was used here, so reset the
+ * |toolboxPromise| since someone must be destroying it to reach here,
+ * and call our own close method.
+ */
+ _onToolboxClosed: function (promise, iframe) {
+ // Only save toolbox size, disable wrench button, workaround focus issue...
+ // if we are closing the last toolbox:
+ // - toolboxPromise is nullified by destroyToolbox and is still null here
+ // if no other toolbox has been opened in between,
+ // - having two distinct promise means we are receiving closed event
+ // for a previous, non-current, toolbox.
+ if (!this.toolboxPromise || this.toolboxPromise === promise) {
+ this.toolboxPromise = null;
+ this.resetFocus();
+ Services.prefs.setIntPref("devtools.toolbox.footer.height", iframe.height);
+
+ let splitter = document.querySelector(".devtools-horizontal-splitter");
+ splitter.setAttribute("hidden", "true");
+ document.querySelector("#action-button-debug").removeAttribute("active");
+ }
+ // We have to destroy the iframe, otherwise, the keybindings of webide don't work
+ // properly anymore.
+ iframe.remove();
+ },
+
+ destroyToolbox: function () {
+ // Only have a live toolbox if |this.toolboxPromise| exists
+ if (this.toolboxPromise) {
+ let toolboxPromise = this.toolboxPromise;
+ this.toolboxPromise = null;
+ return toolboxPromise.then(toolbox => toolbox.destroy());
+ }
+ return promise.resolve();
+ },
+
+ createToolbox: function () {
+ // If |this.toolboxPromise| exists, there is already a live toolbox
+ if (this.toolboxPromise) {
+ return this.toolboxPromise;
+ }
+
+ let iframe = document.createElement("iframe");
+ iframe.id = "toolbox";
+
+ // Compute a uid on the iframe in order to identify toolbox iframe
+ // when receiving toolbox-close event
+ iframe.uid = new Date().getTime();
+
+ let height = Services.prefs.getIntPref("devtools.toolbox.footer.height");
+ iframe.height = height;
+
+ let promise = this.toolboxPromise = AppManager.getTarget().then(target => {
+ return this._showToolbox(target, iframe);
+ }).then(toolbox => {
+ // Destroy the toolbox on WebIDE side before
+ // toolbox.destroy's promise resolves.
+ toolbox.once("destroyed", this._onToolboxClosed.bind(this, promise, iframe));
+ return toolbox;
+ }, console.error);
+
+ return this.busyUntil(this.toolboxPromise, "opening toolbox");
+ },
+
+ _showToolbox: function (target, iframe) {
+ let splitter = document.querySelector(".devtools-horizontal-splitter");
+ splitter.removeAttribute("hidden");
+
+ document.querySelector("notificationbox").insertBefore(iframe, splitter.nextSibling);
+ let host = Toolbox.HostType.CUSTOM;
+ let options = { customIframe: iframe, zoom: false, uid: iframe.uid };
+
+ document.querySelector("#action-button-debug").setAttribute("active", "true");
+
+ return gDevTools.showToolbox(target, null, host, options);
+ },
+
+ prePackageLog: function (msg) {
+ if (msg == "start") {
+ UI.selectDeckPanel("logs");
+ }
+ }
+};
+
+EventEmitter.decorate(UI);
+
+var Cmds = {
+ quit: function () {
+ if (UI.canCloseProject()) {
+ window.close();
+ }
+ },
+
+ showProjectPanel: function () {
+ ProjectPanel.toggleSidebar();
+ return promise.resolve();
+ },
+
+ showRuntimePanel: function () {
+ RuntimeScanners.scan();
+ RuntimePanel.toggleSidebar();
+ },
+
+ disconnectRuntime: function () {
+ let disconnecting = Task.spawn(function* () {
+ yield UI.destroyToolbox();
+ yield AppManager.disconnectRuntime();
+ });
+ return UI.busyUntil(disconnecting, "disconnecting from runtime");
+ },
+
+ takeScreenshot: function () {
+ let url = AppManager.deviceFront.screenshotToDataURL();
+ return UI.busyUntil(url.then(longstr => {
+ return longstr.string().then(dataURL => {
+ longstr.release().then(null, console.error);
+ UI.openInBrowser(dataURL);
+ });
+ }), "taking screenshot");
+ },
+
+ showPermissionsTable: function () {
+ UI.selectDeckPanel("permissionstable");
+ },
+
+ showRuntimeDetails: function () {
+ UI.selectDeckPanel("runtimedetails");
+ },
+
+ showDevicePrefs: function () {
+ UI.selectDeckPanel("devicepreferences");
+ },
+
+ showSettings: function () {
+ UI.selectDeckPanel("devicesettings");
+ },
+
+ showMonitor: function () {
+ UI.selectDeckPanel("monitor");
+ },
+
+ play: Task.async(function* () {
+ let busy;
+ switch (AppManager.selectedProject.type) {
+ case "packaged":
+ let autosave =
+ Services.prefs.getBoolPref("devtools.webide.autosaveFiles");
+ if (autosave && UI.projecteditor) {
+ yield UI.projecteditor.saveAllFiles();
+ }
+ busy = UI.busyWithProgressUntil(AppManager.installAndRunProject(),
+ "installing and running app");
+ break;
+ case "hosted":
+ busy = UI.busyUntil(AppManager.installAndRunProject(),
+ "installing and running app");
+ break;
+ case "runtimeApp":
+ busy = UI.busyUntil(AppManager.launchOrReloadRuntimeApp(), "launching / reloading app");
+ break;
+ case "tab":
+ busy = UI.busyUntil(AppManager.reloadTab(), "reloading tab");
+ break;
+ }
+ if (!busy) {
+ return promise.reject();
+ }
+ UI.onAction("play");
+ return busy;
+ }),
+
+ stop: function () {
+ return UI.busyUntil(AppManager.stopRunningApp(), "stopping app");
+ },
+
+ toggleToolbox: function () {
+ UI.onAction("debug");
+ if (UI.toolboxPromise) {
+ UI.destroyToolbox();
+ return promise.resolve();
+ } else {
+ return UI.createToolbox();
+ }
+ },
+
+ removeProject: function () {
+ AppManager.removeSelectedProject();
+ },
+
+ toggleEditors: function () {
+ let isNowEnabled = !UI.isProjectEditorEnabled();
+ Services.prefs.setBoolPref("devtools.webide.showProjectEditor", isNowEnabled);
+ if (!isNowEnabled) {
+ UI.destroyProjectEditor();
+ }
+ UI.openProject();
+ },
+
+ showTroubleShooting: function () {
+ UI.openInBrowser(HELP_URL);
+ },
+
+ showAddons: function () {
+ UI.selectDeckPanel("addons");
+ },
+
+ showPrefs: function () {
+ UI.selectDeckPanel("prefs");
+ },
+
+ zoomIn: function () {
+ if (UI.contentViewer.fullZoom < MAX_ZOOM) {
+ UI.contentViewer.fullZoom += 0.1;
+ Services.prefs.setCharPref("devtools.webide.zoom", UI.contentViewer.fullZoom);
+ }
+ },
+
+ zoomOut: function () {
+ if (UI.contentViewer.fullZoom > MIN_ZOOM) {
+ UI.contentViewer.fullZoom -= 0.1;
+ Services.prefs.setCharPref("devtools.webide.zoom", UI.contentViewer.fullZoom);
+ }
+ },
+
+ resetZoom: function () {
+ UI.contentViewer.fullZoom = 1;
+ Services.prefs.setCharPref("devtools.webide.zoom", 1);
+ }
+};
diff --git a/devtools/client/webide/content/webide.xul b/devtools/client/webide/content/webide.xul
new file mode 100644
index 000000000..a3e4355b9
--- /dev/null
+++ b/devtools/client/webide/content/webide.xul
@@ -0,0 +1,178 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<!DOCTYPE window [
+ <!ENTITY % webideDTD SYSTEM "chrome://devtools/locale/webide.dtd" >
+ %webideDTD;
+]>
+
+<?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?>
+
+<?xml-stylesheet href="chrome://global/skin/global.css"?>
+<?xml-stylesheet href="resource://devtools/client/themes/common.css"?>
+<?xml-stylesheet href="chrome://webide/skin/webide.css"?>
+
+<window id="webide" onclose="return UI.canCloseProject();"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="&windowTitle;"
+ windowtype="devtools:webide"
+ macanimationtype="document"
+ fullscreenbutton="true"
+ screenX="4" screenY="4"
+ width="800" height="600"
+ persist="screenX screenY width height sizemode">
+
+ <script type="application/javascript" src="chrome://global/content/globalOverlay.js"></script>
+ <script type="application/javascript" src="project-panel.js"></script>
+ <script type="application/javascript" src="runtime-panel.js"></script>
+ <script type="application/javascript" src="webide.js"></script>
+
+ <commandset id="mainCommandSet">
+ <commandset id="editMenuCommands"/>
+ <commandset id="webideCommands">
+ <command id="cmd_quit" oncommand="Cmds.quit()"/>
+ <command id="cmd_newApp" oncommand="Cmds.newApp()" label="&projectMenu_newApp_label;"/>
+ <command id="cmd_importPackagedApp" oncommand="Cmds.importPackagedApp()" label="&projectMenu_importPackagedApp_label;"/>
+ <command id="cmd_importHostedApp" oncommand="Cmds.importHostedApp()" label="&projectMenu_importHostedApp_label;"/>
+ <command id="cmd_showDevicePrefs" label="&runtimeMenu_showDevicePrefs_label;" oncommand="Cmds.showDevicePrefs()"/>
+ <command id="cmd_showSettings" label="&runtimeMenu_showSettings_label;" oncommand="Cmds.showSettings()"/>
+ <command id="cmd_removeProject" oncommand="Cmds.removeProject()" label="&projectMenu_remove_label;"/>
+ <command id="cmd_showProjectPanel" oncommand="Cmds.showProjectPanel()"/>
+ <command id="cmd_showRuntimePanel" oncommand="Cmds.showRuntimePanel()"/>
+ <command id="cmd_disconnectRuntime" oncommand="Cmds.disconnectRuntime()" label="&runtimeMenu_disconnect_label;"/>
+ <command id="cmd_showMonitor" oncommand="Cmds.showMonitor()" label="&runtimeMenu_showMonitor_label;"/>
+ <command id="cmd_showPermissionsTable" oncommand="Cmds.showPermissionsTable()" label="&runtimeMenu_showPermissionTable_label;"/>
+ <command id="cmd_showRuntimeDetails" oncommand="Cmds.showRuntimeDetails()" label="&runtimeMenu_showDetails_label;"/>
+ <command id="cmd_takeScreenshot" oncommand="Cmds.takeScreenshot()" label="&runtimeMenu_takeScreenshot_label;"/>
+ <command id="cmd_toggleEditor" oncommand="Cmds.toggleEditors()" label="&viewMenu_toggleEditor_label;"/>
+ <command id="cmd_showAddons" oncommand="Cmds.showAddons()"/>
+ <command id="cmd_showPrefs" oncommand="Cmds.showPrefs()"/>
+ <command id="cmd_showTroubleShooting" oncommand="Cmds.showTroubleShooting()"/>
+ <command id="cmd_play" oncommand="Cmds.play()"/>
+ <command id="cmd_stop" oncommand="Cmds.stop()" label="&projectMenu_stop_label;"/>
+ <command id="cmd_toggleToolbox" oncommand="Cmds.toggleToolbox()"/>
+ <command id="cmd_zoomin" label="&viewMenu_zoomin_label;" oncommand="Cmds.zoomIn()"/>
+ <command id="cmd_zoomout" label="&viewMenu_zoomout_label;" oncommand="Cmds.zoomOut()"/>
+ <command id="cmd_resetzoom" label="&viewMenu_resetzoom_label;" oncommand="Cmds.resetZoom()"/>
+ </commandset>
+ </commandset>
+
+ <menubar id="main-menubar">
+ <menu id="menu-project" label="&projectMenu_label;" accesskey="&projectMenu_accesskey;">
+ <menupopup id="menu-project-popup">
+ <menuitem command="cmd_newApp" accesskey="&projectMenu_newApp_accesskey;"/>
+ <menuitem command="cmd_importPackagedApp" accesskey="&projectMenu_importPackagedApp_accesskey;"/>
+ <menuitem command="cmd_importHostedApp" accesskey="&projectMenu_importHostedApp_accesskey;"/>
+ <menuitem id="menuitem-show_projectPanel" command="cmd_showProjectPanel" key="key_showProjectPanel" label="&projectMenu_selectApp_label;" accesskey="&projectMenu_selectApp_accesskey;"/>
+ <menuseparator/>
+ <menuitem command="cmd_play" key="key_play" label="&projectMenu_play_label;" accesskey="&projectMenu_play_accesskey;"/>
+ <menuitem command="cmd_stop" accesskey="&projectMenu_stop_accesskey;"/>
+ <menuitem command="cmd_toggleToolbox" key="key_toggleToolbox" label="&projectMenu_debug_label;" accesskey="&projectMenu_debug_accesskey;"/>
+ <menuseparator/>
+ <menuitem command="cmd_removeProject" accesskey="&projectMenu_remove_accesskey;"/>
+ <menuseparator/>
+ <menuitem command="cmd_showPrefs" label="&projectMenu_showPrefs_label;" accesskey="&projectMenu_showPrefs_accesskey;"/>
+ <menuitem command="cmd_showAddons" label="&projectMenu_manageComponents_label;" accesskey="&projectMenu_manageComponents_accesskey;"/>
+ </menupopup>
+ </menu>
+
+ <menu id="menu-runtime" label="&runtimeMenu_label;" accesskey="&runtimeMenu_accesskey;">
+ <menupopup id="menu-runtime-popup">
+ <menuitem command="cmd_showMonitor" accesskey="&runtimeMenu_showMonitor_accesskey;"/>
+ <menuitem command="cmd_takeScreenshot" accesskey="&runtimeMenu_takeScreenshot_accesskey;"/>
+ <menuitem command="cmd_showPermissionsTable" accesskey="&runtimeMenu_showPermissionTable_accesskey;"/>
+ <menuitem command="cmd_showRuntimeDetails" accesskey="&runtimeMenu_showDetails_accesskey;"/>
+ <menuitem command="cmd_showDevicePrefs" accesskey="&runtimeMenu_showDevicePrefs_accesskey;"/>
+ <menuitem command="cmd_showSettings" accesskey="&runtimeMenu_showSettings_accesskey;"/>
+ <menuseparator/>
+ <menuitem command="cmd_disconnectRuntime" accesskey="&runtimeMenu_disconnect_accesskey;"/>
+ </menupopup>
+ </menu>
+
+ <menu id="menu-view" label="&viewMenu_label;" accesskey="&viewMenu_accesskey;">
+ <menupopup id="menu-ViewPopup">
+ <menuitem command="cmd_toggleEditor" key="key_toggleEditor" accesskey="&viewMenu_toggleEditor_accesskey;"/>
+ <menuseparator/>
+ <menuitem command="cmd_zoomin" key="key_zoomin" accesskey="&viewMenu_zoomin_accesskey;"/>
+ <menuitem command="cmd_zoomout" key="key_zoomout" accesskey="&viewMenu_zoomout_accesskey;"/>
+ <menuitem command="cmd_resetzoom" key="key_resetzoom" accesskey="&viewMenu_resetzoom_accesskey;"/>
+ </menupopup>
+ </menu>
+
+ </menubar>
+
+ <keyset id="mainKeyset">
+ <key key="&key_quit;" id="key_quit" command="cmd_quit" modifiers="accel"/>
+ <key key="&key_showProjectPanel;" id="key_showProjectPanel" command="cmd_showProjectPanel" modifiers="accel"/>
+ <key key="&key_play;" id="key_play" command="cmd_play" modifiers="accel"/>
+ <key key="&key_toggleEditor;" id="key_toggleEditor" command="cmd_toggleEditor" modifiers="accel"/>
+ <key keycode="&key_toggleToolbox;" id="key_toggleToolbox" command="cmd_toggleToolbox"/>
+ <key key="&key_zoomin;" id="key_zoomin" command="cmd_zoomin" modifiers="accel"/>
+ <key key="&key_zoomin2;" id="key_zoomin2" command="cmd_zoomin" modifiers="accel"/>
+ <key key="&key_zoomout;" id="key_zoomout" command="cmd_zoomout" modifiers="accel"/>
+ <key key="&key_resetzoom;" id="key_resetzoom" command="cmd_resetzoom" modifiers="accel"/>
+ </keyset>
+
+ <tooltip id="aHTMLTooltip" page="true"/>
+
+ <toolbar id="main-toolbar">
+
+ <vbox flex="1">
+ <hbox id="action-buttons-container" class="busy">
+ <toolbarbutton id="action-button-play" class="action-button" command="cmd_play" tooltiptext="&projectMenu_play_label;"/>
+ <toolbarbutton id="action-button-stop" class="action-button" command="cmd_stop" tooltiptext="&projectMenu_stop_label;"/>
+ <toolbarbutton id="action-button-debug" class="action-button" command="cmd_toggleToolbox" tooltiptext="&projectMenu_debug_label;"/>
+ <hbox id="action-busy" align="center">
+ <html:img id="action-busy-undetermined" src="chrome://webide/skin/throbber.svg"/>
+ <progressmeter id="action-busy-determined"/>
+ </hbox>
+ </hbox>
+
+ <hbox id="panel-buttons-container">
+ <spacer flex="1"/>
+ <toolbarbutton id="runtime-panel-button" class="panel-button">
+ <image class="panel-button-image"/>
+ <label class="panel-button-label" value="&runtimeButton_label;"/>
+ </toolbarbutton>
+ </hbox>
+
+ </vbox>
+ </toolbar>
+
+ <notificationbox flex="1" id="notificationbox">
+ <div flex="1" id="deck-panels">
+ <vbox id="project-listing-panel" class="project-listing panel-list" flex="1">
+ <div id="project-listing-wrapper" class="panel-list-wrapper">
+ <iframe id="project-listing-panel-details" flex="1" src="project-listing.xhtml" tooltip="aHTMLTooltip"/>
+ </div>
+ </vbox>
+ <splitter class="devtools-side-splitter" id="project-listing-splitter"/>
+ <deck flex="1" id="deck" selectedIndex="-1">
+ <iframe id="deck-panel-details" flex="1" src="details.xhtml"/>
+ <iframe id="deck-panel-projecteditor" flex="1"/>
+ <iframe id="deck-panel-addons" flex="1" src="addons.xhtml"/>
+ <iframe id="deck-panel-prefs" flex="1" src="prefs.xhtml"/>
+ <iframe id="deck-panel-permissionstable" flex="1" lazysrc="permissionstable.xhtml"/>
+ <iframe id="deck-panel-runtimedetails" flex="1" lazysrc="runtimedetails.xhtml"/>
+ <iframe id="deck-panel-monitor" flex="1" lazysrc="monitor.xhtml"/>
+ <iframe id="deck-panel-devicepreferences" flex="1" lazysrc="devicepreferences.xhtml"/>
+ <iframe id="deck-panel-devicesettings" flex="1" lazysrc="devicesettings.xhtml"/>
+ <iframe id="deck-panel-logs" flex="1" src="logs.xhtml"/>
+ <iframe id="deck-panel-simulator" flex="1" lazysrc="simulator.xhtml"/>
+ </deck>
+ <splitter class="devtools-side-splitter" id="runtime-listing-splitter"/>
+ <vbox id="runtime-listing-panel" class="runtime-listing panel-list" flex="1">
+ <div id="runtime-listing-wrapper" class="panel-list-wrapper">
+ <iframe id="runtime-listing-panel-details" flex="1" src="runtime-listing.xhtml" tooltip="aHTMLTooltip"/>
+ </div>
+ </vbox>
+ </div>
+ <splitter hidden="true" class="devtools-horizontal-splitter" orient="vertical"/>
+ <!-- toolbox iframe will be inserted here -->
+ </notificationbox>
+
+</window>
diff --git a/devtools/client/webide/content/wifi-auth.js b/devtools/client/webide/content/wifi-auth.js
new file mode 100644
index 000000000..5ae5d824c
--- /dev/null
+++ b/devtools/client/webide/content/wifi-auth.js
@@ -0,0 +1,44 @@
+/* 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 Cu = Components.utils;
+const { require } =
+ Cu.import("resource://devtools/shared/Loader.jsm", {});
+const Services = require("Services");
+const QR = require("devtools/shared/qrcode/index");
+
+window.addEventListener("load", function onLoad() {
+ window.removeEventListener("load", onLoad);
+ document.getElementById("close").onclick = () => window.close();
+ document.getElementById("no-scanner").onclick = showToken;
+ document.getElementById("yes-scanner").onclick = hideToken;
+ buildUI();
+});
+
+function buildUI() {
+ let { oob } = window.arguments[0];
+ createQR(oob);
+ createToken(oob);
+}
+
+function createQR(oob) {
+ let oobData = JSON.stringify(oob);
+ let imgData = QR.encodeToDataURI(oobData, "L" /* low quality */);
+ document.querySelector("#qr-code img").src = imgData.src;
+}
+
+function createToken(oob) {
+ let token = oob.sha256.replace(/:/g, "").toLowerCase() + oob.k;
+ document.querySelector("#token pre").textContent = token;
+}
+
+function showToken() {
+ document.querySelector("body").setAttribute("token", "true");
+}
+
+function hideToken() {
+ document.querySelector("body").removeAttribute("token");
+}
diff --git a/devtools/client/webide/content/wifi-auth.xhtml b/devtools/client/webide/content/wifi-auth.xhtml
new file mode 100644
index 000000000..cfeec3c96
--- /dev/null
+++ b/devtools/client/webide/content/wifi-auth.xhtml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!-- 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/. -->
+
+<!DOCTYPE html [
+ <!ENTITY % webideDTD SYSTEM "chrome://devtools/locale/webide.dtd" >
+ %webideDTD;
+]>
+
+<html id="devtools:wifi-auth" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta charset="utf8"/>
+ <link rel="stylesheet" href="chrome://webide/skin/deck.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://webide/skin/wifi-auth.css" type="text/css"/>
+ <script type="application/javascript;version=1.8" src="chrome://webide/content/wifi-auth.js"></script>
+ </head>
+ <body>
+
+ <div id="controls">
+ <a id="close">&deck_close;</a>
+ </div>
+
+ <h3 id="header">&wifi_auth_header;</h3>
+ <div id="scan-request">&wifi_auth_scan_request;</div>
+
+ <div id="qr-code">
+ <div id="qr-code-wrapper">
+ <img/>
+ </div>
+ <a id="no-scanner" class="toggle-scanner">&wifi_auth_no_scanner;</a>
+ <div id="qr-size-note">
+ <h5>&wifi_auth_qr_size_note;</h5>
+ </div>
+ </div>
+
+ <div id="token">
+ <div>&wifi_auth_token_request;</div>
+ <pre id="token-value"/>
+ <a id="yes-scanner" class="toggle-scanner">&wifi_auth_yes_scanner;</a>
+ </div>
+
+ </body>
+</html>
diff --git a/devtools/client/webide/modules/addons.js b/devtools/client/webide/modules/addons.js
new file mode 100644
index 000000000..4dc09f1ca
--- /dev/null
+++ b/devtools/client/webide/modules/addons.js
@@ -0,0 +1,197 @@
+/* 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 {AddonManager} = require("resource://gre/modules/AddonManager.jsm");
+const Services = require("Services");
+const {getJSON} = require("devtools/client/shared/getjson");
+const EventEmitter = require("devtools/shared/event-emitter");
+
+const ADDONS_URL = "devtools.webide.addonsURL";
+
+var SIMULATOR_LINK = Services.prefs.getCharPref("devtools.webide.simulatorAddonsURL");
+var ADB_LINK = Services.prefs.getCharPref("devtools.webide.adbAddonURL");
+var ADAPTERS_LINK = Services.prefs.getCharPref("devtools.webide.adaptersAddonURL");
+var SIMULATOR_ADDON_ID = Services.prefs.getCharPref("devtools.webide.simulatorAddonID");
+var ADB_ADDON_ID = Services.prefs.getCharPref("devtools.webide.adbAddonID");
+var ADAPTERS_ADDON_ID = Services.prefs.getCharPref("devtools.webide.adaptersAddonID");
+
+var platform = Services.appShell.hiddenDOMWindow.navigator.platform;
+var OS = "";
+if (platform.indexOf("Win") != -1) {
+ OS = "win32";
+} else if (platform.indexOf("Mac") != -1) {
+ OS = "mac64";
+} else if (platform.indexOf("Linux") != -1) {
+ if (platform.indexOf("x86_64") != -1) {
+ OS = "linux64";
+ } else {
+ OS = "linux32";
+ }
+}
+
+var addonsListener = {};
+addonsListener.onEnabled =
+addonsListener.onDisabled =
+addonsListener.onInstalled =
+addonsListener.onUninstalled = (updatedAddon) => {
+ GetAvailableAddons().then(addons => {
+ for (let a of [...addons.simulators, addons.adb, addons.adapters]) {
+ if (a.addonID == updatedAddon.id) {
+ a.updateInstallStatus();
+ }
+ }
+ });
+};
+AddonManager.addAddonListener(addonsListener);
+
+var GetAvailableAddons_promise = null;
+var GetAvailableAddons = exports.GetAvailableAddons = function () {
+ if (!GetAvailableAddons_promise) {
+ let deferred = promise.defer();
+ GetAvailableAddons_promise = deferred.promise;
+ let addons = {
+ simulators: [],
+ adb: null
+ };
+ getJSON(ADDONS_URL).then(json => {
+ for (let stability in json) {
+ for (let version of json[stability]) {
+ addons.simulators.push(new SimulatorAddon(stability, version));
+ }
+ }
+ addons.adb = new ADBAddon();
+ addons.adapters = new AdaptersAddon();
+ deferred.resolve(addons);
+ }, e => {
+ GetAvailableAddons_promise = null;
+ deferred.reject(e);
+ });
+ }
+ return GetAvailableAddons_promise;
+};
+
+exports.ForgetAddonsList = function () {
+ GetAvailableAddons_promise = null;
+};
+
+function Addon() {}
+Addon.prototype = {
+ _status: "unknown",
+ set status(value) {
+ if (this._status != value) {
+ this._status = value;
+ this.emit("update");
+ }
+ },
+ get status() {
+ return this._status;
+ },
+
+ updateInstallStatus: function () {
+ AddonManager.getAddonByID(this.addonID, (addon) => {
+ if (addon && !addon.userDisabled) {
+ this.status = "installed";
+ } else {
+ this.status = "uninstalled";
+ }
+ });
+ },
+
+ install: function () {
+ AddonManager.getAddonByID(this.addonID, (addon) => {
+ if (addon && !addon.userDisabled) {
+ this.status = "installed";
+ return;
+ }
+ this.status = "preparing";
+ if (addon && addon.userDisabled) {
+ addon.userDisabled = false;
+ } else {
+ AddonManager.getInstallForURL(this.xpiLink, (install) => {
+ install.addListener(this);
+ install.install();
+ }, "application/x-xpinstall");
+ }
+ });
+ },
+
+ uninstall: function () {
+ AddonManager.getAddonByID(this.addonID, (addon) => {
+ addon.uninstall();
+ });
+ },
+
+ installFailureHandler: function (install, message) {
+ this.status = "uninstalled";
+ this.emit("failure", message);
+ },
+
+ onDownloadStarted: function () {
+ this.status = "downloading";
+ },
+
+ onInstallStarted: function () {
+ this.status = "installing";
+ },
+
+ onDownloadProgress: function (install) {
+ if (install.maxProgress == -1) {
+ this.emit("progress", -1);
+ } else {
+ this.emit("progress", install.progress / install.maxProgress);
+ }
+ },
+
+ onInstallEnded: function ({addon}) {
+ addon.userDisabled = false;
+ },
+
+ onDownloadCancelled: function (install) {
+ this.installFailureHandler(install, "Download cancelled");
+ },
+ onDownloadFailed: function (install) {
+ this.installFailureHandler(install, "Download failed");
+ },
+ onInstallCancelled: function (install) {
+ this.installFailureHandler(install, "Install cancelled");
+ },
+ onInstallFailed: function (install) {
+ this.installFailureHandler(install, "Install failed");
+ },
+};
+
+function SimulatorAddon(stability, version) {
+ EventEmitter.decorate(this);
+ this.stability = stability;
+ this.version = version;
+ // This addon uses the string "linux" for "linux32"
+ let fixedOS = OS == "linux32" ? "linux" : OS;
+ this.xpiLink = SIMULATOR_LINK.replace(/#OS#/g, fixedOS)
+ .replace(/#VERSION#/g, version)
+ .replace(/#SLASHED_VERSION#/g, version.replace(/\./g, "_"));
+ this.addonID = SIMULATOR_ADDON_ID.replace(/#SLASHED_VERSION#/g, version.replace(/\./g, "_"));
+ this.updateInstallStatus();
+}
+SimulatorAddon.prototype = Object.create(Addon.prototype);
+
+function ADBAddon() {
+ EventEmitter.decorate(this);
+ // This addon uses the string "linux" for "linux32"
+ let fixedOS = OS == "linux32" ? "linux" : OS;
+ this.xpiLink = ADB_LINK.replace(/#OS#/g, fixedOS);
+ this.addonID = ADB_ADDON_ID;
+ this.updateInstallStatus();
+}
+ADBAddon.prototype = Object.create(Addon.prototype);
+
+function AdaptersAddon() {
+ EventEmitter.decorate(this);
+ this.xpiLink = ADAPTERS_LINK.replace(/#OS#/g, OS);
+ this.addonID = ADAPTERS_ADDON_ID;
+ this.updateInstallStatus();
+}
+AdaptersAddon.prototype = Object.create(Addon.prototype);
diff --git a/devtools/client/webide/modules/app-manager.js b/devtools/client/webide/modules/app-manager.js
new file mode 100644
index 000000000..88dfcdd44
--- /dev/null
+++ b/devtools/client/webide/modules/app-manager.js
@@ -0,0 +1,850 @@
+/* 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/. */
+
+const {Cu} = require("chrome");
+
+const promise = require("promise");
+const {TargetFactory} = require("devtools/client/framework/target");
+const Services = require("Services");
+const {FileUtils} = Cu.import("resource://gre/modules/FileUtils.jsm", {});
+const EventEmitter = require("devtools/shared/event-emitter");
+const {TextEncoder, OS} = Cu.import("resource://gre/modules/osfile.jsm", {});
+const {AppProjects} = require("devtools/client/webide/modules/app-projects");
+const TabStore = require("devtools/client/webide/modules/tab-store");
+const {AppValidator} = require("devtools/client/webide/modules/app-validator");
+const {ConnectionManager, Connection} = require("devtools/shared/client/connection-manager");
+const {AppActorFront} = require("devtools/shared/apps/app-actor-front");
+const {getDeviceFront} = require("devtools/shared/fronts/device");
+const {getPreferenceFront} = require("devtools/shared/fronts/preference");
+const {getSettingsFront} = require("devtools/shared/fronts/settings");
+const {Task} = require("devtools/shared/task");
+const {RuntimeScanners, RuntimeTypes} = require("devtools/client/webide/modules/runtimes");
+const {NetUtil} = Cu.import("resource://gre/modules/NetUtil.jsm", {});
+const Telemetry = require("devtools/client/shared/telemetry");
+const {ProjectBuilding} = require("./build");
+
+const Strings = Services.strings.createBundle("chrome://devtools/locale/webide.properties");
+
+var AppManager = exports.AppManager = {
+
+ DEFAULT_PROJECT_ICON: "chrome://webide/skin/default-app-icon.png",
+ DEFAULT_PROJECT_NAME: "--",
+
+ _initialized: false,
+
+ init: function () {
+ if (this._initialized) {
+ return;
+ }
+ this._initialized = true;
+
+ let port = Services.prefs.getIntPref("devtools.debugger.remote-port");
+ this.connection = ConnectionManager.createConnection("localhost", port);
+ this.onConnectionChanged = this.onConnectionChanged.bind(this);
+ this.connection.on(Connection.Events.STATUS_CHANGED, this.onConnectionChanged);
+
+ this.tabStore = new TabStore(this.connection);
+ this.onTabList = this.onTabList.bind(this);
+ this.onTabNavigate = this.onTabNavigate.bind(this);
+ this.onTabClosed = this.onTabClosed.bind(this);
+ this.tabStore.on("tab-list", this.onTabList);
+ this.tabStore.on("navigate", this.onTabNavigate);
+ this.tabStore.on("closed", this.onTabClosed);
+
+ this._clearRuntimeList();
+ this._rebuildRuntimeList = this._rebuildRuntimeList.bind(this);
+ RuntimeScanners.on("runtime-list-updated", this._rebuildRuntimeList);
+ RuntimeScanners.enable();
+ this._rebuildRuntimeList();
+
+ this.onInstallProgress = this.onInstallProgress.bind(this);
+
+ this._telemetry = new Telemetry();
+ },
+
+ destroy: function () {
+ if (!this._initialized) {
+ return;
+ }
+ this._initialized = false;
+
+ this.selectedProject = null;
+ this.selectedRuntime = null;
+ RuntimeScanners.off("runtime-list-updated", this._rebuildRuntimeList);
+ RuntimeScanners.disable();
+ this.runtimeList = null;
+ this.tabStore.off("tab-list", this.onTabList);
+ this.tabStore.off("navigate", this.onTabNavigate);
+ this.tabStore.off("closed", this.onTabClosed);
+ this.tabStore.destroy();
+ this.tabStore = null;
+ this.connection.off(Connection.Events.STATUS_CHANGED, this.onConnectionChanged);
+ this._listTabsResponse = null;
+ this.connection.disconnect();
+ this.connection = null;
+ },
+
+ /**
+ * This module emits various events when state changes occur. The basic event
+ * naming scheme is that event "X" means "X has changed" or "X is available".
+ * Some names are more detailed to clarify their precise meaning.
+ *
+ * The events this module may emit include:
+ * before-project:
+ * The selected project is about to change. The event includes a special
+ * |cancel| callback that will abort the project change if desired.
+ * connection:
+ * The connection status has changed (connected, disconnected, etc.)
+ * install-progress:
+ * A project being installed to a runtime has made further progress. This
+ * event contains additional details about exactly how far the process is
+ * when such information is available.
+ * project:
+ * The selected project has changed.
+ * project-started:
+ * The selected project started running on the connected runtime.
+ * project-stopped:
+ * The selected project stopped running on the connected runtime.
+ * project-removed:
+ * The selected project was removed from the project list.
+ * project-validated:
+ * The selected project just completed validation. As part of validation,
+ * many pieces of metadata about the project are refreshed, including its
+ * name, manifest details, etc.
+ * runtime:
+ * The selected runtime has changed.
+ * runtime-apps-icons:
+ * The list of URLs for the runtime app icons are available.
+ * runtime-global-actors:
+ * The list of global actors for the entire runtime (but not actors for a
+ * specific tab or app) are now available, so we can test for features
+ * like preferences and settings.
+ * runtime-details:
+ * The selected runtime's details have changed, such as its user-visible
+ * name.
+ * runtime-list:
+ * The list of available runtimes has changed, or any of the user-visible
+ * details (like names) for the non-selected runtimes has changed.
+ * runtime-telemetry:
+ * Detailed runtime telemetry has been recorded. Used by tests.
+ * runtime-targets:
+ * The list of remote runtime targets available from the currently
+ * connected runtime (such as tabs or apps) has changed, or any of the
+ * user-visible details (like names) for the non-selected runtime targets
+ * has changed. This event includes |type| in the details, to distinguish
+ * "apps" and "tabs".
+ */
+ update: function (what, details) {
+ // Anything we want to forward to the UI
+ this.emit("app-manager-update", what, details);
+ },
+
+ reportError: function (l10nProperty, ...l10nArgs) {
+ let win = Services.wm.getMostRecentWindow("devtools:webide");
+ if (win) {
+ win.UI.reportError(l10nProperty, ...l10nArgs);
+ } else {
+ let text;
+ if (l10nArgs.length > 0) {
+ text = Strings.formatStringFromName(l10nProperty, l10nArgs, l10nArgs.length);
+ } else {
+ text = Strings.GetStringFromName(l10nProperty);
+ }
+ console.error(text);
+ }
+ },
+
+ onConnectionChanged: function () {
+ console.log("Connection status changed: " + this.connection.status);
+
+ if (this.connection.status == Connection.Status.DISCONNECTED) {
+ this.selectedRuntime = null;
+ }
+
+ if (!this.connected) {
+ if (this._appsFront) {
+ this._appsFront.off("install-progress", this.onInstallProgress);
+ this._appsFront.unwatchApps();
+ this._appsFront = null;
+ }
+ this._listTabsResponse = null;
+ } else {
+ this.connection.client.listTabs((response) => {
+ if (response.webappsActor) {
+ let front = new AppActorFront(this.connection.client,
+ response);
+ front.on("install-progress", this.onInstallProgress);
+ front.watchApps(() => this.checkIfProjectIsRunning())
+ .then(() => {
+ // This can't be done earlier as many operations
+ // in the apps actor require watchApps to be called
+ // first.
+ this._appsFront = front;
+ this._listTabsResponse = response;
+ this._recordRuntimeInfo();
+ this.update("runtime-global-actors");
+ })
+ .then(() => {
+ this.checkIfProjectIsRunning();
+ this.update("runtime-targets", { type: "apps" });
+ front.fetchIcons().then(() => this.update("runtime-apps-icons"));
+ });
+ } else {
+ this._listTabsResponse = response;
+ this._recordRuntimeInfo();
+ this.update("runtime-global-actors");
+ }
+ });
+ }
+
+ this.update("connection");
+ },
+
+ get connected() {
+ return this.connection &&
+ this.connection.status == Connection.Status.CONNECTED;
+ },
+
+ get apps() {
+ if (this._appsFront) {
+ return this._appsFront.apps;
+ } else {
+ return new Map();
+ }
+ },
+
+ onInstallProgress: function (event, details) {
+ this.update("install-progress", details);
+ },
+
+ isProjectRunning: function () {
+ if (this.selectedProject.type == "mainProcess" ||
+ this.selectedProject.type == "tab") {
+ return true;
+ }
+
+ let app = this._getProjectFront(this.selectedProject);
+ return app && app.running;
+ },
+
+ checkIfProjectIsRunning: function () {
+ if (this.selectedProject) {
+ if (this.isProjectRunning()) {
+ this.update("project-started");
+ } else {
+ this.update("project-stopped");
+ }
+ }
+ },
+
+ listTabs: function () {
+ return this.tabStore.listTabs();
+ },
+
+ onTabList: function () {
+ this.update("runtime-targets", { type: "tabs" });
+ },
+
+ // TODO: Merge this into TabProject as part of project-agnostic work
+ onTabNavigate: function () {
+ this.update("runtime-targets", { type: "tabs" });
+ if (this.selectedProject.type !== "tab") {
+ return;
+ }
+ let tab = this.selectedProject.app = this.tabStore.selectedTab;
+ let uri = NetUtil.newURI(tab.url);
+ // Wanted to use nsIFaviconService here, but it only works for visited
+ // tabs, so that's no help for any remote tabs. Maybe some favicon wizard
+ // knows how to get high-res favicons easily, or we could offer actor
+ // support for this (bug 1061654).
+ tab.favicon = uri.prePath + "/favicon.ico";
+ tab.name = tab.title || Strings.GetStringFromName("project_tab_loading");
+ if (uri.scheme.startsWith("http")) {
+ tab.name = uri.host + ": " + tab.name;
+ }
+ this.selectedProject.location = tab.url;
+ this.selectedProject.name = tab.name;
+ this.selectedProject.icon = tab.favicon;
+ this.update("project-validated");
+ },
+
+ onTabClosed: function () {
+ if (this.selectedProject.type !== "tab") {
+ return;
+ }
+ this.selectedProject = null;
+ },
+
+ reloadTab: function () {
+ if (this.selectedProject && this.selectedProject.type != "tab") {
+ return promise.reject("tried to reload non-tab project");
+ }
+ return this.getTarget().then(target => {
+ target.activeTab.reload();
+ }, console.error.bind(console));
+ },
+
+ getTarget: function () {
+ if (this.selectedProject.type == "mainProcess") {
+ // Fx >=39 exposes a ChromeActor to debug the main process
+ if (this.connection.client.mainRoot.traits.allowChromeProcess) {
+ return this.connection.client.getProcess()
+ .then(aResponse => {
+ return TargetFactory.forRemoteTab({
+ form: aResponse.form,
+ client: this.connection.client,
+ chrome: true
+ });
+ });
+ } else {
+ // Fx <39 exposes tab actors on the root actor
+ return TargetFactory.forRemoteTab({
+ form: this._listTabsResponse,
+ client: this.connection.client,
+ chrome: true,
+ isTabActor: false
+ });
+ }
+ }
+
+ if (this.selectedProject.type == "tab") {
+ return this.tabStore.getTargetForTab();
+ }
+
+ let app = this._getProjectFront(this.selectedProject);
+ if (!app) {
+ return promise.reject("Can't find app front for selected project");
+ }
+
+ return Task.spawn(function* () {
+ // Once we asked the app to launch, the app isn't necessary completely loaded.
+ // launch request only ask the app to launch and immediatly returns.
+ // We have to keep trying to get app tab actors required to create its target.
+
+ for (let i = 0; i < 10; i++) {
+ try {
+ return yield app.getTarget();
+ } catch (e) {}
+ let deferred = promise.defer();
+ setTimeout(deferred.resolve, 500);
+ yield deferred.promise;
+ }
+
+ AppManager.reportError("error_cantConnectToApp", app.manifest.manifestURL);
+ throw new Error("can't connect to app");
+ });
+ },
+
+ getProjectManifestURL: function (project) {
+ let manifest = null;
+ if (project.type == "runtimeApp") {
+ manifest = project.app.manifestURL;
+ }
+
+ if (project.type == "hosted") {
+ manifest = project.location;
+ }
+
+ if (project.type == "packaged" && project.packagedAppOrigin) {
+ manifest = "app://" + project.packagedAppOrigin + "/manifest.webapp";
+ }
+
+ return manifest;
+ },
+
+ _getProjectFront: function (project) {
+ let manifest = this.getProjectManifestURL(project);
+ if (manifest && this._appsFront) {
+ return this._appsFront.apps.get(manifest);
+ }
+ return null;
+ },
+
+ _selectedProject: null,
+ set selectedProject(project) {
+ // A regular comparison doesn't work as we recreate a new object every time
+ let prev = this._selectedProject;
+ if (!prev && !project) {
+ return;
+ } else if (prev && project && prev.type === project.type) {
+ let type = project.type;
+ if (type === "runtimeApp") {
+ if (prev.app.manifestURL === project.app.manifestURL) {
+ return;
+ }
+ } else if (type === "tab") {
+ if (prev.app.actor === project.app.actor) {
+ return;
+ }
+ } else if (type === "packaged" || type === "hosted") {
+ if (prev.location === project.location) {
+ return;
+ }
+ } else if (type === "mainProcess") {
+ return;
+ } else {
+ throw new Error("Unsupported project type: " + type);
+ }
+ }
+
+ let cancelled = false;
+ this.update("before-project", { cancel: () => { cancelled = true; } });
+ if (cancelled) {
+ return;
+ }
+
+ this._selectedProject = project;
+
+ // Clear out tab store's selected state, if any
+ this.tabStore.selectedTab = null;
+
+ if (project) {
+ if (project.type == "packaged" ||
+ project.type == "hosted") {
+ this.validateAndUpdateProject(project);
+ }
+ if (project.type == "tab") {
+ this.tabStore.selectedTab = project.app;
+ }
+ }
+
+ this.update("project");
+ this.checkIfProjectIsRunning();
+ },
+ get selectedProject() {
+ return this._selectedProject;
+ },
+
+ removeSelectedProject: Task.async(function* () {
+ let location = this.selectedProject.location;
+ AppManager.selectedProject = null;
+ // If the user cancels the removeProject operation, don't remove the project
+ if (AppManager.selectedProject != null) {
+ return;
+ }
+
+ yield AppProjects.remove(location);
+ AppManager.update("project-removed");
+ }),
+
+ packageProject: Task.async(function* (project) {
+ if (!project) {
+ return;
+ }
+ if (project.type == "packaged" ||
+ project.type == "hosted") {
+ yield ProjectBuilding.build({
+ project: project,
+ logger: this.update.bind(this, "pre-package")
+ });
+ }
+ }),
+
+ _selectedRuntime: null,
+ set selectedRuntime(value) {
+ this._selectedRuntime = value;
+ if (!value && this.selectedProject &&
+ (this.selectedProject.type == "mainProcess" ||
+ this.selectedProject.type == "runtimeApp" ||
+ this.selectedProject.type == "tab")) {
+ this.selectedProject = null;
+ }
+ this.update("runtime");
+ },
+
+ get selectedRuntime() {
+ return this._selectedRuntime;
+ },
+
+ connectToRuntime: function (runtime) {
+
+ if (this.connected && this.selectedRuntime === runtime) {
+ // Already connected
+ return promise.resolve();
+ }
+
+ let deferred = promise.defer();
+
+ this.disconnectRuntime().then(() => {
+ this.selectedRuntime = runtime;
+
+ let onConnectedOrDisconnected = () => {
+ this.connection.off(Connection.Events.CONNECTED, onConnectedOrDisconnected);
+ this.connection.off(Connection.Events.DISCONNECTED, onConnectedOrDisconnected);
+ if (this.connected) {
+ deferred.resolve();
+ } else {
+ deferred.reject();
+ }
+ };
+ this.connection.on(Connection.Events.CONNECTED, onConnectedOrDisconnected);
+ this.connection.on(Connection.Events.DISCONNECTED, onConnectedOrDisconnected);
+ try {
+ // Reset the connection's state to defaults
+ this.connection.resetOptions();
+ // Only watch for errors here. Final resolution occurs above, once
+ // we've reached the CONNECTED state.
+ this.selectedRuntime.connect(this.connection)
+ .then(null, e => deferred.reject(e));
+ } catch (e) {
+ deferred.reject(e);
+ }
+ }, deferred.reject);
+
+ // Record connection result in telemetry
+ let logResult = result => {
+ this._telemetry.log("DEVTOOLS_WEBIDE_CONNECTION_RESULT", result);
+ if (runtime.type) {
+ this._telemetry.log("DEVTOOLS_WEBIDE_" + runtime.type +
+ "_CONNECTION_RESULT", result);
+ }
+ };
+ deferred.promise.then(() => logResult(true), () => logResult(false));
+
+ // If successful, record connection time in telemetry
+ deferred.promise.then(() => {
+ const timerId = "DEVTOOLS_WEBIDE_CONNECTION_TIME_SECONDS";
+ this._telemetry.startTimer(timerId);
+ this.connection.once(Connection.Events.STATUS_CHANGED, () => {
+ this._telemetry.stopTimer(timerId);
+ });
+ }).catch(() => {
+ // Empty rejection handler to silence uncaught rejection warnings
+ // |connectToRuntime| caller should listen for rejections.
+ // Bug 1121100 may find a better way to silence these.
+ });
+
+ return deferred.promise;
+ },
+
+ _recordRuntimeInfo: Task.async(function* () {
+ if (!this.connected) {
+ return;
+ }
+ let runtime = this.selectedRuntime;
+ this._telemetry.logKeyed("DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_TYPE",
+ runtime.type || "UNKNOWN", true);
+ this._telemetry.logKeyed("DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_ID",
+ runtime.id || "unknown", true);
+ if (!this.deviceFront) {
+ this.update("runtime-telemetry");
+ return;
+ }
+ let d = yield this.deviceFront.getDescription();
+ this._telemetry.logKeyed("DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_PROCESSOR",
+ d.processor, true);
+ this._telemetry.logKeyed("DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_OS",
+ d.os, true);
+ this._telemetry.logKeyed("DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_PLATFORM_VERSION",
+ d.platformversion, true);
+ this._telemetry.logKeyed("DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_APP_TYPE",
+ d.apptype, true);
+ this._telemetry.logKeyed("DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_VERSION",
+ d.version, true);
+ this.update("runtime-telemetry");
+ }),
+
+ isMainProcessDebuggable: function () {
+ // Fx <39 exposes chrome tab actors on RootActor
+ // Fx >=39 exposes a dedicated actor via getProcess request
+ return this.connection.client &&
+ this.connection.client.mainRoot &&
+ this.connection.client.mainRoot.traits.allowChromeProcess ||
+ (this._listTabsResponse &&
+ this._listTabsResponse.consoleActor);
+ },
+
+ get deviceFront() {
+ if (!this._listTabsResponse) {
+ return null;
+ }
+ return getDeviceFront(this.connection.client, this._listTabsResponse);
+ },
+
+ get preferenceFront() {
+ if (!this._listTabsResponse) {
+ return null;
+ }
+ return getPreferenceFront(this.connection.client, this._listTabsResponse);
+ },
+
+ get settingsFront() {
+ if (!this._listTabsResponse) {
+ return null;
+ }
+ return getSettingsFront(this.connection.client, this._listTabsResponse);
+ },
+
+ disconnectRuntime: function () {
+ if (!this.connected) {
+ return promise.resolve();
+ }
+ let deferred = promise.defer();
+ this.connection.once(Connection.Events.DISCONNECTED, () => deferred.resolve());
+ this.connection.disconnect();
+ return deferred.promise;
+ },
+
+ launchRuntimeApp: function () {
+ if (this.selectedProject && this.selectedProject.type != "runtimeApp") {
+ return promise.reject("attempting to launch a non-runtime app");
+ }
+ let app = this._getProjectFront(this.selectedProject);
+ return app.launch();
+ },
+
+ launchOrReloadRuntimeApp: function () {
+ if (this.selectedProject && this.selectedProject.type != "runtimeApp") {
+ return promise.reject("attempting to launch / reload a non-runtime app");
+ }
+ let app = this._getProjectFront(this.selectedProject);
+ if (!app.running) {
+ return app.launch();
+ } else {
+ return app.reload();
+ }
+ },
+
+ runtimeCanHandleApps: function () {
+ return !!this._appsFront;
+ },
+
+ installAndRunProject: function () {
+ let project = this.selectedProject;
+
+ if (!project || (project.type != "packaged" && project.type != "hosted")) {
+ console.error("Can't install project. Unknown type of project.");
+ return promise.reject("Can't install");
+ }
+
+ if (!this._listTabsResponse) {
+ this.reportError("error_cantInstallNotFullyConnected");
+ return promise.reject("Can't install");
+ }
+
+ if (!this._appsFront) {
+ console.error("Runtime doesn't have a webappsActor");
+ return promise.reject("Can't install");
+ }
+
+ return Task.spawn(function* () {
+ let self = AppManager;
+
+ // Package and validate project
+ yield self.packageProject(project);
+ yield self.validateAndUpdateProject(project);
+
+ if (project.errorsCount > 0) {
+ self.reportError("error_cantInstallValidationErrors");
+ return;
+ }
+
+ let installPromise;
+
+ if (project.type != "packaged" && project.type != "hosted") {
+ return promise.reject("Don't know how to install project");
+ }
+
+ let response;
+ if (project.type == "packaged") {
+ let packageDir = yield ProjectBuilding.getPackageDir(project);
+ console.log("Installing app from " + packageDir);
+
+ response = yield self._appsFront.installPackaged(packageDir,
+ project.packagedAppOrigin);
+
+ // If the packaged app specified a custom origin override,
+ // we need to update the local project origin
+ project.packagedAppOrigin = response.appId;
+ // And ensure the indexed db on disk is also updated
+ AppProjects.update(project);
+ }
+
+ if (project.type == "hosted") {
+ let manifestURLObject = Services.io.newURI(project.location, null, null);
+ let origin = Services.io.newURI(manifestURLObject.prePath, null, null);
+ let appId = origin.host;
+ let metadata = {
+ origin: origin.spec,
+ manifestURL: project.location
+ };
+ response = yield self._appsFront.installHosted(appId,
+ metadata,
+ project.manifest);
+ }
+
+ // Addons don't have any document to load (yet?)
+ // So that there is no need to run them, installing is enough
+ if (project.manifest.manifest_version || project.manifest.role === "addon") {
+ return;
+ }
+
+ let {app} = response;
+ if (!app.running) {
+ let deferred = promise.defer();
+ self.on("app-manager-update", function onUpdate(event, what) {
+ if (what == "project-started") {
+ self.off("app-manager-update", onUpdate);
+ deferred.resolve();
+ }
+ });
+ yield app.launch();
+ yield deferred.promise;
+ } else {
+ yield app.reload();
+ }
+ });
+ },
+
+ stopRunningApp: function () {
+ let app = this._getProjectFront(this.selectedProject);
+ return app.close();
+ },
+
+ /* PROJECT VALIDATION */
+
+ validateAndUpdateProject: function (project) {
+ if (!project) {
+ return promise.reject();
+ }
+
+ return Task.spawn(function* () {
+
+ let packageDir = yield ProjectBuilding.getPackageDir(project);
+ let validation = new AppValidator({
+ type: project.type,
+ // Build process may place the manifest in a non-root directory
+ location: packageDir
+ });
+
+ yield validation.validate();
+
+ if (validation.manifest) {
+ let manifest = validation.manifest;
+ let iconPath;
+ if (manifest.icons) {
+ let size = Object.keys(manifest.icons).sort((a, b) => b - a)[0];
+ if (size) {
+ iconPath = manifest.icons[size];
+ }
+ }
+ if (!iconPath) {
+ project.icon = AppManager.DEFAULT_PROJECT_ICON;
+ } else {
+ if (project.type == "hosted") {
+ let manifestURL = Services.io.newURI(project.location, null, null);
+ let origin = Services.io.newURI(manifestURL.prePath, null, null);
+ project.icon = Services.io.newURI(iconPath, null, origin).spec;
+ } else if (project.type == "packaged") {
+ let projectFolder = FileUtils.File(packageDir);
+ let folderURI = Services.io.newFileURI(projectFolder).spec;
+ project.icon = folderURI + iconPath.replace(/^\/|\\/, "");
+ }
+ }
+ project.manifest = validation.manifest;
+
+ if ("name" in project.manifest) {
+ project.name = project.manifest.name;
+ } else {
+ project.name = AppManager.DEFAULT_PROJECT_NAME;
+ }
+ } else {
+ project.manifest = null;
+ project.icon = AppManager.DEFAULT_PROJECT_ICON;
+ project.name = AppManager.DEFAULT_PROJECT_NAME;
+ }
+
+ project.validationStatus = "valid";
+
+ if (validation.warnings.length > 0) {
+ project.warningsCount = validation.warnings.length;
+ project.warnings = validation.warnings;
+ project.validationStatus = "warning";
+ } else {
+ project.warnings = "";
+ project.warningsCount = 0;
+ }
+
+ if (validation.errors.length > 0) {
+ project.errorsCount = validation.errors.length;
+ project.errors = validation.errors;
+ project.validationStatus = "error";
+ } else {
+ project.errors = "";
+ project.errorsCount = 0;
+ }
+
+ if (project.warningsCount && project.errorsCount) {
+ project.validationStatus = "error warning";
+ }
+
+ if (project.type === "hosted" && project.location !== validation.manifestURL) {
+ yield AppProjects.updateLocation(project, validation.manifestURL);
+ } else if (AppProjects.get(project.location)) {
+ yield AppProjects.update(project);
+ }
+
+ if (AppManager.selectedProject === project) {
+ AppManager.update("project-validated");
+ }
+ });
+ },
+
+ /* RUNTIME LIST */
+
+ _clearRuntimeList: function () {
+ this.runtimeList = {
+ usb: [],
+ wifi: [],
+ simulator: [],
+ other: []
+ };
+ },
+
+ _rebuildRuntimeList: function () {
+ let runtimes = RuntimeScanners.listRuntimes();
+ this._clearRuntimeList();
+
+ // Reorganize runtimes by type
+ for (let runtime of runtimes) {
+ switch (runtime.type) {
+ case RuntimeTypes.USB:
+ this.runtimeList.usb.push(runtime);
+ break;
+ case RuntimeTypes.WIFI:
+ this.runtimeList.wifi.push(runtime);
+ break;
+ case RuntimeTypes.SIMULATOR:
+ this.runtimeList.simulator.push(runtime);
+ break;
+ default:
+ this.runtimeList.other.push(runtime);
+ }
+ }
+
+ this.update("runtime-details");
+ this.update("runtime-list");
+ },
+
+ /* MANIFEST UTILS */
+
+ writeManifest: function (project) {
+ if (project.type != "packaged") {
+ return promise.reject("Not a packaged app");
+ }
+
+ if (!project.manifest) {
+ project.manifest = {};
+ }
+
+ let folder = project.location;
+ let manifestPath = OS.Path.join(folder, "manifest.webapp");
+ let text = JSON.stringify(project.manifest, null, 2);
+ let encoder = new TextEncoder();
+ let array = encoder.encode(text);
+ return OS.File.writeAtomic(manifestPath, array, {tmpPath: manifestPath + ".tmp"});
+ },
+};
+
+EventEmitter.decorate(AppManager);
diff --git a/devtools/client/webide/modules/app-projects.js b/devtools/client/webide/modules/app-projects.js
new file mode 100644
index 000000000..691d09064
--- /dev/null
+++ b/devtools/client/webide/modules/app-projects.js
@@ -0,0 +1,235 @@
+/* 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/. */
+
+const {Cc, Ci, Cu, Cr} = require("chrome");
+const promise = require("promise");
+
+const EventEmitter = require("devtools/shared/event-emitter");
+const {generateUUID} = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
+const {FileUtils} = Cu.import("resource://gre/modules/FileUtils.jsm", {});
+
+/**
+ * IndexedDB wrapper that just save project objects
+ *
+ * The only constraint is that project objects have to have
+ * a unique `location` object.
+ */
+
+const IDB = {
+ _db: null,
+ databaseName: "AppProjects",
+
+ open: function () {
+ let deferred = promise.defer();
+
+ let request = indexedDB.open(IDB.databaseName, 5);
+ request.onerror = function (event) {
+ deferred.reject("Unable to open AppProjects indexedDB: " +
+ this.error.name + " - " + this.error.message);
+ };
+ request.onupgradeneeded = function (event) {
+ let db = event.target.result;
+ db.createObjectStore("projects", { keyPath: "location" });
+ };
+
+ request.onsuccess = function () {
+ let db = IDB._db = request.result;
+ let objectStore = db.transaction("projects").objectStore("projects");
+ let projects = [];
+ let toRemove = [];
+ objectStore.openCursor().onsuccess = function (event) {
+ let cursor = event.target.result;
+ if (cursor) {
+ if (cursor.value.location) {
+
+ // We need to make sure this object has a `.location` property.
+ // The UI depends on this property.
+ // This should not be needed as we make sure to register valid
+ // projects, but in the past (before bug 924568), we might have
+ // registered invalid objects.
+
+
+ // We also want to make sure the location is valid.
+ // If the location doesn't exist, we remove the project.
+
+ try {
+ let file = FileUtils.File(cursor.value.location);
+ if (file.exists()) {
+ projects.push(cursor.value);
+ } else {
+ toRemove.push(cursor.value.location);
+ }
+ } catch (e) {
+ if (e.result == Cr.NS_ERROR_FILE_UNRECOGNIZED_PATH) {
+ // A URL
+ projects.push(cursor.value);
+ }
+ }
+ }
+ cursor.continue();
+ } else {
+ let removePromises = [];
+ for (let location of toRemove) {
+ removePromises.push(IDB.remove(location));
+ }
+ promise.all(removePromises).then(() => {
+ deferred.resolve(projects);
+ });
+ }
+ };
+ };
+
+ return deferred.promise;
+ },
+
+ add: function (project) {
+ let deferred = promise.defer();
+
+ if (!project.location) {
+ // We need to make sure this object has a `.location` property.
+ deferred.reject("Missing location property on project object.");
+ } else {
+ let transaction = IDB._db.transaction(["projects"], "readwrite");
+ let objectStore = transaction.objectStore("projects");
+ let request = objectStore.add(project);
+ request.onerror = function (event) {
+ deferred.reject("Unable to add project to the AppProjects indexedDB: " +
+ this.error.name + " - " + this.error.message);
+ };
+ request.onsuccess = function () {
+ deferred.resolve();
+ };
+ }
+
+ return deferred.promise;
+ },
+
+ update: function (project) {
+ let deferred = promise.defer();
+
+ var transaction = IDB._db.transaction(["projects"], "readwrite");
+ var objectStore = transaction.objectStore("projects");
+ var request = objectStore.put(project);
+ request.onerror = function (event) {
+ deferred.reject("Unable to update project to the AppProjects indexedDB: " +
+ this.error.name + " - " + this.error.message);
+ };
+ request.onsuccess = function () {
+ deferred.resolve();
+ };
+
+ return deferred.promise;
+ },
+
+ remove: function (location) {
+ let deferred = promise.defer();
+
+ let request = IDB._db.transaction(["projects"], "readwrite")
+ .objectStore("projects")
+ .delete(location);
+ request.onsuccess = function (event) {
+ deferred.resolve();
+ };
+ request.onerror = function () {
+ deferred.reject("Unable to delete project to the AppProjects indexedDB: " +
+ this.error.name + " - " + this.error.message);
+ };
+
+ return deferred.promise;
+ }
+};
+
+var loadDeferred = promise.defer();
+
+loadDeferred.resolve(IDB.open().then(function (projects) {
+ AppProjects.projects = projects;
+ AppProjects.emit("ready", projects);
+}));
+
+const AppProjects = {
+ load: function () {
+ return loadDeferred.promise;
+ },
+
+ addPackaged: function (folder) {
+ let file = FileUtils.File(folder.path);
+ if (!file.exists()) {
+ return promise.reject("path doesn't exist");
+ }
+ let existingProject = this.get(folder.path);
+ if (existingProject) {
+ return promise.reject("Already added");
+ }
+ let project = {
+ type: "packaged",
+ location: folder.path,
+ // We need a unique id, that is the app origin,
+ // in order to identify the app when being installed on the device.
+ // The packaged app local path is a valid id, but only on the client.
+ // This origin will be used to generate the true id of an app:
+ // its manifest URL.
+ // If the app ends up specifying an explicit origin in its manifest,
+ // we will override this random UUID on app install.
+ packagedAppOrigin: generateUUID().toString().slice(1, -1)
+ };
+ return IDB.add(project).then(() => {
+ this.projects.push(project);
+ return project;
+ });
+ },
+
+ addHosted: function (manifestURL) {
+ let existingProject = this.get(manifestURL);
+ if (existingProject) {
+ return promise.reject("Already added");
+ }
+ let project = {
+ type: "hosted",
+ location: manifestURL
+ };
+ return IDB.add(project).then(() => {
+ this.projects.push(project);
+ return project;
+ });
+ },
+
+ update: function (project) {
+ return IDB.update(project);
+ },
+
+ updateLocation: function (project, newLocation) {
+ return IDB.remove(project.location)
+ .then(() => {
+ project.location = newLocation;
+ return IDB.add(project);
+ });
+ },
+
+ remove: function (location) {
+ return IDB.remove(location).then(() => {
+ for (let i = 0; i < this.projects.length; i++) {
+ if (this.projects[i].location == location) {
+ this.projects.splice(i, 1);
+ return;
+ }
+ }
+ throw new Error("Unable to find project in AppProjects store");
+ });
+ },
+
+ get: function (location) {
+ for (let i = 0; i < this.projects.length; i++) {
+ if (this.projects[i].location == location) {
+ return this.projects[i];
+ }
+ }
+ return null;
+ },
+
+ projects: []
+};
+
+EventEmitter.decorate(AppProjects);
+
+exports.AppProjects = AppProjects;
diff --git a/devtools/client/webide/modules/app-validator.js b/devtools/client/webide/modules/app-validator.js
new file mode 100644
index 000000000..750720110
--- /dev/null
+++ b/devtools/client/webide/modules/app-validator.js
@@ -0,0 +1,292 @@
+/* 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, CC} = require("chrome");
+const promise = require("promise");
+
+const {FileUtils} = Cu.import("resource://gre/modules/FileUtils.jsm", {});
+const Services = require("Services");
+const {Task} = require("devtools/shared/task");
+var XMLHttpRequest = CC("@mozilla.org/xmlextras/xmlhttprequest;1");
+var strings = Services.strings.createBundle("chrome://devtools/locale/app-manager.properties");
+
+function AppValidator({ type, location }) {
+ this.type = type;
+ this.location = location;
+ this.errors = [];
+ this.warnings = [];
+}
+
+AppValidator.prototype.error = function (message) {
+ this.errors.push(message);
+};
+
+AppValidator.prototype.warning = function (message) {
+ this.warnings.push(message);
+};
+
+AppValidator.prototype._getPackagedManifestFile = function () {
+ let manifestFile = FileUtils.File(this.location);
+ if (!manifestFile.exists()) {
+ this.error(strings.GetStringFromName("validator.nonExistingFolder"));
+ return null;
+ }
+ if (!manifestFile.isDirectory()) {
+ this.error(strings.GetStringFromName("validator.expectProjectFolder"));
+ return null;
+ }
+
+ let appManifestFile = manifestFile.clone();
+ appManifestFile.append("manifest.webapp");
+
+ let jsonManifestFile = manifestFile.clone();
+ jsonManifestFile.append("manifest.json");
+
+ let hasAppManifest = appManifestFile.exists() && appManifestFile.isFile();
+ let hasJsonManifest = jsonManifestFile.exists() && jsonManifestFile.isFile();
+
+ if (!hasAppManifest && !hasJsonManifest) {
+ this.error(strings.GetStringFromName("validator.noManifestFile"));
+ return null;
+ }
+
+ return hasAppManifest ? appManifestFile : jsonManifestFile;
+};
+
+AppValidator.prototype._getPackagedManifestURL = function () {
+ let manifestFile = this._getPackagedManifestFile();
+ if (!manifestFile) {
+ return null;
+ }
+ return Services.io.newFileURI(manifestFile).spec;
+};
+
+AppValidator.checkManifest = function (manifestURL) {
+ let deferred = promise.defer();
+ let error;
+
+ let req = new XMLHttpRequest();
+ req.overrideMimeType("text/plain");
+
+ try {
+ req.open("GET", manifestURL, true);
+ req.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE | Ci.nsIRequest.INHIBIT_CACHING;
+ } catch (e) {
+ error = strings.formatStringFromName("validator.invalidManifestURL", [manifestURL], 1);
+ deferred.reject(error);
+ return deferred.promise;
+ }
+
+ req.onload = function () {
+ let manifest = null;
+ try {
+ manifest = JSON.parse(req.responseText);
+ } catch (e) {
+ error = strings.formatStringFromName("validator.invalidManifestJSON", [e, manifestURL], 2);
+ deferred.reject(error);
+ }
+
+ deferred.resolve({manifest, manifestURL});
+ };
+
+ req.onerror = function () {
+ error = strings.formatStringFromName("validator.noAccessManifestURL", [req.statusText, manifestURL], 2);
+ deferred.reject(error);
+ };
+
+ try {
+ req.send(null);
+ } catch (e) {
+ error = strings.formatStringFromName("validator.noAccessManifestURL", [e, manifestURL], 2);
+ deferred.reject(error);
+ }
+
+ return deferred.promise;
+};
+
+AppValidator.findManifestAtOrigin = function (manifestURL) {
+ let fixedManifest = Services.io.newURI(manifestURL, null, null).prePath + "/manifest.webapp";
+ return AppValidator.checkManifest(fixedManifest);
+};
+
+AppValidator.findManifestPath = function (manifestURL) {
+ let deferred = promise.defer();
+
+ if (manifestURL.endsWith("manifest.webapp")) {
+ deferred.reject();
+ } else {
+ let fixedManifest = manifestURL + "/manifest.webapp";
+ deferred.resolve(AppValidator.checkManifest(fixedManifest));
+ }
+
+ return deferred.promise;
+};
+
+AppValidator.checkAlternateManifest = function (manifestURL) {
+ return Task.spawn(function* () {
+ let result;
+ try {
+ result = yield AppValidator.findManifestPath(manifestURL);
+ } catch (e) {
+ result = yield AppValidator.findManifestAtOrigin(manifestURL);
+ }
+
+ return result;
+ });
+};
+
+AppValidator.prototype._fetchManifest = function (manifestURL) {
+ let deferred = promise.defer();
+ this.manifestURL = manifestURL;
+
+ AppValidator.checkManifest(manifestURL)
+ .then(({manifest, manifestURL}) => {
+ deferred.resolve(manifest);
+ }, error => {
+ AppValidator.checkAlternateManifest(manifestURL)
+ .then(({manifest, manifestURL}) => {
+ this.manifestURL = manifestURL;
+ deferred.resolve(manifest);
+ }, () => {
+ this.error(error);
+ deferred.resolve(null);
+ });
+ });
+
+ return deferred.promise;
+};
+
+AppValidator.prototype._getManifest = function () {
+ let manifestURL;
+ if (this.type == "packaged") {
+ manifestURL = this._getPackagedManifestURL();
+ if (!manifestURL)
+ return promise.resolve(null);
+ } else if (this.type == "hosted") {
+ manifestURL = this.location;
+ try {
+ Services.io.newURI(manifestURL, null, null);
+ } catch (e) {
+ this.error(strings.formatStringFromName("validator.invalidHostedManifestURL", [manifestURL, e.message], 2));
+ return promise.resolve(null);
+ }
+ } else {
+ this.error(strings.formatStringFromName("validator.invalidProjectType", [this.type], 1));
+ return promise.resolve(null);
+ }
+ return this._fetchManifest(manifestURL);
+};
+
+AppValidator.prototype.validateManifest = function (manifest) {
+ if (!manifest.name) {
+ this.error(strings.GetStringFromName("validator.missNameManifestProperty"));
+ }
+
+ if (!manifest.icons || Object.keys(manifest.icons).length === 0) {
+ this.warning(strings.GetStringFromName("validator.missIconsManifestProperty"));
+ } else if (!manifest.icons["128"]) {
+ this.warning(strings.GetStringFromName("validator.missIconMarketplace2"));
+ }
+};
+
+AppValidator.prototype._getOriginURL = function () {
+ if (this.type == "packaged") {
+ let manifestURL = Services.io.newURI(this.manifestURL, null, null);
+ return Services.io.newURI(".", null, manifestURL).spec;
+ } else if (this.type == "hosted") {
+ return Services.io.newURI(this.location, null, null).prePath;
+ }
+};
+
+AppValidator.prototype.validateLaunchPath = function (manifest) {
+ let deferred = promise.defer();
+ // The launch_path field has to start with a `/`
+ if (manifest.launch_path && manifest.launch_path[0] !== "/") {
+ this.error(strings.formatStringFromName("validator.nonAbsoluteLaunchPath", [manifest.launch_path], 1));
+ deferred.resolve();
+ return deferred.promise;
+ }
+ let origin = this._getOriginURL();
+ let path;
+ if (this.type == "packaged") {
+ path = "." + (manifest.launch_path || "/index.html");
+ } else if (this.type == "hosted") {
+ path = manifest.launch_path || "/";
+ }
+ let indexURL;
+ try {
+ indexURL = Services.io.newURI(path, null, Services.io.newURI(origin, null, null)).spec;
+ } catch (e) {
+ this.error(strings.formatStringFromName("validator.accessFailedLaunchPath", [origin + path], 1));
+ deferred.resolve();
+ return deferred.promise;
+ }
+
+ let req = new XMLHttpRequest();
+ req.overrideMimeType("text/plain");
+ try {
+ req.open("HEAD", indexURL, true);
+ req.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE | Ci.nsIRequest.INHIBIT_CACHING;
+ } catch (e) {
+ this.error(strings.formatStringFromName("validator.accessFailedLaunchPath", [indexURL], 1));
+ deferred.resolve();
+ return deferred.promise;
+ }
+ req.onload = () => {
+ if (req.status >= 400)
+ this.error(strings.formatStringFromName("validator.accessFailedLaunchPathBadHttpCode", [indexURL, req.status], 2));
+ deferred.resolve();
+ };
+ req.onerror = () => {
+ this.error(strings.formatStringFromName("validator.accessFailedLaunchPath", [indexURL], 1));
+ deferred.resolve();
+ };
+
+ try {
+ req.send(null);
+ } catch (e) {
+ this.error(strings.formatStringFromName("validator.accessFailedLaunchPath", [indexURL], 1));
+ deferred.resolve();
+ }
+
+ return deferred.promise;
+};
+
+AppValidator.prototype.validateType = function (manifest) {
+ let appType = manifest.type || "web";
+ if (["web", "privileged", "certified"].indexOf(appType) === -1) {
+ this.error(strings.formatStringFromName("validator.invalidAppType", [appType], 1));
+ } else if (this.type == "hosted" &&
+ ["certified", "privileged"].indexOf(appType) !== -1) {
+ this.error(strings.formatStringFromName("validator.invalidHostedPriviledges", [appType], 1));
+ }
+
+ // certified app are not fully supported on the simulator
+ if (appType === "certified") {
+ this.warning(strings.GetStringFromName("validator.noCertifiedSupport"));
+ }
+};
+
+AppValidator.prototype.validate = function () {
+ this.errors = [];
+ this.warnings = [];
+ return this._getManifest().
+ then((manifest) => {
+ if (manifest) {
+ this.manifest = manifest;
+
+ // Skip validations for add-ons
+ if (manifest.role === "addon" || manifest.manifest_version) {
+ return promise.resolve();
+ }
+
+ this.validateManifest(manifest);
+ this.validateType(manifest);
+ return this.validateLaunchPath(manifest);
+ }
+ });
+};
+
+exports.AppValidator = AppValidator;
diff --git a/devtools/client/webide/modules/build.js b/devtools/client/webide/modules/build.js
new file mode 100644
index 000000000..34cbcc0b7
--- /dev/null
+++ b/devtools/client/webide/modules/build.js
@@ -0,0 +1,199 @@
+/* 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/. */
+
+const {Cu, Cc, Ci} = require("chrome");
+
+const promise = require("promise");
+const { Task } = require("devtools/shared/task");
+const { TextDecoder, OS } = Cu.import("resource://gre/modules/osfile.jsm", {});
+const Subprocess = require("sdk/system/child_process/subprocess");
+
+const ProjectBuilding = exports.ProjectBuilding = {
+ fetchPackageManifest: Task.async(function* (project) {
+ let manifestPath = OS.Path.join(project.location, "package.json");
+ let exists = yield OS.File.exists(manifestPath);
+ if (!exists) {
+ // No explicit manifest, try to generate one if possible
+ return this.generatePackageManifest(project);
+ }
+
+ let data = yield OS.File.read(manifestPath);
+ data = new TextDecoder().decode(data);
+ let manifest;
+ try {
+ manifest = JSON.parse(data);
+ } catch (e) {
+ throw new Error("Error while reading WebIDE manifest at: '" + manifestPath +
+ "', invalid JSON: " + e.message);
+ }
+ return manifest;
+ }),
+
+ /**
+ * For common frameworks in the community, attempt to detect the build
+ * settings if none are defined. This makes it much easier to get started
+ * with WebIDE. Later on, perhaps an add-on could define such things for
+ * different frameworks.
+ */
+ generatePackageManifest: Task.async(function* (project) {
+ // Cordova
+ let cordovaConfigPath = OS.Path.join(project.location, "config.xml");
+ let exists = yield OS.File.exists(cordovaConfigPath);
+ if (!exists) {
+ return;
+ }
+ let data = yield OS.File.read(cordovaConfigPath);
+ data = new TextDecoder().decode(data);
+ if (data.contains("cordova.apache.org")) {
+ return {
+ "webide": {
+ "prepackage": "cordova prepare",
+ "packageDir": "./platforms/firefoxos/www"
+ }
+ };
+ }
+ }),
+
+ hasPrepackage: Task.async(function* (project) {
+ let manifest = yield ProjectBuilding.fetchPackageManifest(project);
+ return manifest && manifest.webide && "prepackage" in manifest.webide;
+ }),
+
+ // If the app depends on some build step, run it before pushing the app
+ build: Task.async(function* ({ project, logger }) {
+ if (!(yield this.hasPrepackage(project))) {
+ return;
+ }
+
+ let manifest = yield ProjectBuilding.fetchPackageManifest(project);
+
+ logger("start");
+ try {
+ yield this._build(project, manifest, logger);
+ logger("succeed");
+ } catch (e) {
+ logger("failed", e);
+ }
+ }),
+
+ _build: Task.async(function* (project, manifest, logger) {
+ // Look for `webide` property
+ manifest = manifest.webide;
+
+ let command, cwd, args = [], env = [];
+
+ // Copy frequently used env vars
+ let envService = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment);
+ ["HOME", "PATH"].forEach(key => {
+ let value = envService.get(key);
+ if (value) {
+ env.push(key + "=" + value);
+ }
+ });
+
+ if (typeof (manifest.prepackage) === "string") {
+ command = manifest.prepackage.replace(/%project%/g, project.location);
+ } else if (manifest.prepackage.command) {
+ command = manifest.prepackage.command;
+
+ args = manifest.prepackage.args || [];
+ args = args.map(a => a.replace(/%project%/g, project.location));
+
+ env = env.concat(manifest.prepackage.env || []);
+ env = env.map(a => a.replace(/%project%/g, project.location));
+
+ if (manifest.prepackage.cwd) {
+ // Normalize path for Windows support (converts / to \)
+ let path = OS.Path.normalize(manifest.prepackage.cwd);
+ // Note that Path.join also support absolute path and argument.
+ // So that if cwd is absolute, it will return cwd.
+ let rel = OS.Path.join(project.location, path);
+ let exists = yield OS.File.exists(rel);
+ if (exists) {
+ cwd = rel;
+ }
+ }
+ } else {
+ throw new Error("pre-package manifest is invalid, missing or invalid " +
+ "`prepackage` attribute");
+ }
+
+ if (!cwd) {
+ cwd = project.location;
+ }
+
+ logger("Running pre-package hook '" + command + "' " +
+ args.join(" ") +
+ " with ENV=[" + env.join(", ") + "]" +
+ " at " + cwd);
+
+ // Run the command through a shell command in order to support non absolute
+ // paths.
+ // On Windows `ComSpec` env variable is going to refer to cmd.exe,
+ // Otherwise, on Linux and Mac, SHELL env variable should refer to
+ // the user chosen shell program.
+ // (We do not check for OS, as on windows, with cygwin, ComSpec isn't set)
+ let shell = envService.get("ComSpec") || envService.get("SHELL");
+ args.unshift(command);
+
+ // For cmd.exe, we have to pass the `/C` option,
+ // but for unix shells we need -c.
+ // That to interpret next argument as a shell command.
+ if (envService.exists("ComSpec")) {
+ args.unshift("/C");
+ } else {
+ args.unshift("-c");
+ }
+
+ // Subprocess changes CWD, we have to save and restore it.
+ let originalCwd = yield OS.File.getCurrentDirectory();
+ try {
+ let defer = promise.defer();
+ Subprocess.call({
+ command: shell,
+ arguments: args,
+ environment: env,
+ workdir: cwd,
+
+ stdout: data =>
+ logger(data),
+ stderr: data =>
+ logger(data),
+
+ done: result => {
+ logger("Terminated with error code: " + result.exitCode);
+ if (result.exitCode == 0) {
+ defer.resolve();
+ } else {
+ defer.reject("pre-package command failed with error code " + result.exitCode);
+ }
+ }
+ });
+ defer.promise.then(() => {
+ OS.File.setCurrentDirectory(originalCwd);
+ });
+ yield defer.promise;
+ } catch (e) {
+ throw new Error("Unable to run pre-package command '" + command + "' " +
+ args.join(" ") + ":\n" + (e.message || e));
+ }
+ }),
+
+ getPackageDir: Task.async(function* (project) {
+ let manifest = yield ProjectBuilding.fetchPackageManifest(project);
+ if (!manifest || !manifest.webide || !manifest.webide.packageDir) {
+ return project.location;
+ }
+ manifest = manifest.webide;
+
+ let packageDir = OS.Path.join(project.location, manifest.packageDir);
+ // On Windows, replace / by \\
+ packageDir = OS.Path.normalize(packageDir);
+ let exists = yield OS.File.exists(packageDir);
+ if (exists) {
+ return packageDir;
+ }
+ throw new Error("Unable to resolve application package directory: '" + manifest.packageDir + "'");
+ })
+};
diff --git a/devtools/client/webide/modules/config-view.js b/devtools/client/webide/modules/config-view.js
new file mode 100644
index 000000000..5fb07e235
--- /dev/null
+++ b/devtools/client/webide/modules/config-view.js
@@ -0,0 +1,373 @@
+/* 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/. */
+
+const {Cu} = require("chrome");
+
+const EventEmitter = require("devtools/shared/event-emitter");
+const Services = require("Services");
+const Strings = Services.strings.createBundle("chrome://devtools/locale/webide.properties");
+
+var ConfigView;
+
+module.exports = ConfigView = function (window) {
+ EventEmitter.decorate(this);
+ this._doc = window.document;
+ this._keys = [];
+ return this;
+};
+
+ConfigView.prototype = {
+ _renderByType: function (input, name, value, customType) {
+ value = customType || typeof value;
+
+ switch (value) {
+ case "boolean":
+ input.setAttribute("data-type", "boolean");
+ input.setAttribute("type", "checkbox");
+ break;
+ case "number":
+ input.setAttribute("data-type", "number");
+ input.setAttribute("type", "number");
+ break;
+ case "object":
+ input.setAttribute("data-type", "object");
+ input.setAttribute("type", "text");
+ break;
+ default:
+ input.setAttribute("data-type", "string");
+ input.setAttribute("type", "text");
+ break;
+ }
+ return input;
+ },
+
+ set front(front) {
+ this._front = front;
+ },
+
+ set keys(keys) {
+ this._keys = keys;
+ },
+
+ get keys() {
+ return this._keys;
+ },
+
+ set kind(kind) {
+ this._kind = kind;
+ },
+
+ set includeTypeName(include) {
+ this._includeTypeName = include;
+ },
+
+ search: function (event) {
+ if (event.target.value.length) {
+ let stringMatch = new RegExp(event.target.value, "i");
+
+ for (let i = 0; i < this._keys.length; i++) {
+ let key = this._keys[i];
+ let row = this._doc.getElementById("row-" + key);
+ if (key.match(stringMatch)) {
+ row.classList.remove("hide");
+ } else if (row) {
+ row.classList.add("hide");
+ }
+ }
+ } else {
+ var trs = this._doc.getElementById("device-fields").querySelectorAll("tr");
+
+ for (let i = 0; i < trs.length; i++) {
+ trs[i].classList.remove("hide");
+ }
+ }
+ },
+
+ generateDisplay: function (json) {
+ let deviceItems = Object.keys(json);
+ deviceItems.sort();
+ this.keys = deviceItems;
+ for (let i = 0; i < this.keys.length; i++) {
+ let key = this.keys[i];
+ this.generateField(key, json[key].value, json[key].hasUserValue);
+ }
+ },
+
+ generateField: function (name, value, hasUserValue, customType, newRow) {
+ let table = this._doc.querySelector("table");
+ let sResetDefault = Strings.GetStringFromName("device_reset_default");
+
+ if (this._keys.indexOf(name) === -1) {
+ this._keys.push(name);
+ }
+
+ let input = this._doc.createElement("input");
+ let tr = this._doc.createElement("tr");
+ tr.setAttribute("id", "row-" + name);
+ tr.classList.add("edit-row");
+ let td = this._doc.createElement("td");
+ td.classList.add("field-name");
+ td.textContent = name;
+ tr.appendChild(td);
+ td = this._doc.createElement("td");
+ input.classList.add("editable");
+ input.setAttribute("id", name);
+ input = this._renderByType(input, name, value, customType);
+
+ if (customType === "boolean" || input.type === "checkbox") {
+ input.checked = value;
+ } else {
+ if (typeof value === "object") {
+ value = JSON.stringify(value);
+ }
+ input.value = value;
+ }
+
+ if (!(this._includeTypeName || isNaN(parseInt(value, 10)))) {
+ input.type = "number";
+ }
+
+ td.appendChild(input);
+ tr.appendChild(td);
+ td = this._doc.createElement("td");
+ td.setAttribute("id", "td-" + name);
+
+ let button = this._doc.createElement("button");
+ button.setAttribute("data-id", name);
+ button.setAttribute("id", "btn-" + name);
+ button.classList.add("reset");
+ button.textContent = sResetDefault;
+ td.appendChild(button);
+
+ if (!hasUserValue) {
+ button.classList.add("hide");
+ }
+
+ tr.appendChild(td);
+
+ // If this is a new field, add it to the top of the table.
+ if (newRow) {
+ let existing = table.querySelector("#" + name);
+
+ if (!existing) {
+ table.insertBefore(tr, newRow);
+ } else {
+ existing.value = value;
+ }
+ } else {
+ table.appendChild(tr);
+ }
+ },
+
+ resetTable: function () {
+ let table = this._doc.querySelector("table");
+ let trs = table.querySelectorAll("tr:not(#add-custom-field)");
+
+ for (var i = 0; i < trs.length; i++) {
+ table.removeChild(trs[i]);
+ }
+
+ return table;
+ },
+
+ _getCallType: function (type, name) {
+ let frontName = "get";
+
+ if (this._includeTypeName) {
+ frontName += type;
+ }
+
+ return this._front[frontName + this._kind](name);
+ },
+
+ _setCallType: function (type, name, value) {
+ let frontName = "set";
+
+ if (this._includeTypeName) {
+ frontName += type;
+ }
+
+ return this._front[frontName + this._kind](name, value);
+ },
+
+ _saveByType: function (options) {
+ let fieldName = options.id;
+ let inputType = options.type;
+ let value = options.value;
+ let input = this._doc.getElementById(fieldName);
+
+ switch (inputType) {
+ case "boolean":
+ this._setCallType("Bool", fieldName, input.checked);
+ break;
+ case "number":
+ this._setCallType("Int", fieldName, value);
+ break;
+ case "object":
+ try {
+ value = JSON.parse(value);
+ } catch (e) {}
+ this._setCallType("Object", fieldName, value);
+ break;
+ default:
+ this._setCallType("Char", fieldName, value);
+ break;
+ }
+ },
+
+ updateField: function (event) {
+ if (event.target) {
+ let inputType = event.target.getAttribute("data-type");
+ let inputValue = event.target.checked || event.target.value;
+
+ if (event.target.nodeName == "input" &&
+ event.target.validity.valid &&
+ event.target.classList.contains("editable")) {
+ let id = event.target.id;
+ if (inputType === "boolean") {
+ if (event.target.checked) {
+ inputValue = true;
+ } else {
+ inputValue = false;
+ }
+ }
+
+ this._saveByType({
+ id: id,
+ type: inputType,
+ value: inputValue
+ });
+ this._doc.getElementById("btn-" + id).classList.remove("hide");
+ }
+ }
+ },
+
+ _resetToDefault: function (name, input, button) {
+ this._front["clearUser" + this._kind](name);
+ let dataType = input.getAttribute("data-type");
+ let tr = this._doc.getElementById("row-" + name);
+
+ switch (dataType) {
+ case "boolean":
+ this._defaultField = this._getCallType("Bool", name);
+ this._defaultField.then(boolean => {
+ input.checked = boolean;
+ }, () => {
+ input.checked = false;
+ tr.parentNode.removeChild(tr);
+ });
+ break;
+ case "number":
+ this._defaultField = this._getCallType("Int", name);
+ this._defaultField.then(number => {
+ input.value = number;
+ }, () => {
+ tr.parentNode.removeChild(tr);
+ });
+ break;
+ case "object":
+ this._defaultField = this._getCallType("Object", name);
+ this._defaultField.then(object => {
+ input.value = JSON.stringify(object);
+ }, () => {
+ tr.parentNode.removeChild(tr);
+ });
+ break;
+ default:
+ this._defaultField = this._getCallType("Char", name);
+ this._defaultField.then(string => {
+ input.value = string;
+ }, () => {
+ tr.parentNode.removeChild(tr);
+ });
+ break;
+ }
+
+ button.classList.add("hide");
+ },
+
+ checkReset: function (event) {
+ if (event.target.classList.contains("reset")) {
+ let btnId = event.target.getAttribute("data-id");
+ let input = this._doc.getElementById(btnId);
+ this._resetToDefault(btnId, input, event.target);
+ }
+ },
+
+ updateFieldType: function () {
+ let table = this._doc.querySelector("table");
+ let customValueType = table.querySelector("#custom-value-type").value;
+ let customTextEl = table.querySelector("#custom-value-text");
+ let customText = customTextEl.value;
+
+ if (customValueType.length === 0) {
+ return false;
+ }
+
+ switch (customValueType) {
+ case "boolean":
+ customTextEl.type = "checkbox";
+ customText = customTextEl.checked;
+ break;
+ case "number":
+ customText = parseInt(customText, 10) || 0;
+ customTextEl.type = "number";
+ break;
+ default:
+ customTextEl.type = "text";
+ break;
+ }
+
+ return customValueType;
+ },
+
+ clearNewFields: function () {
+ let table = this._doc.querySelector("table");
+ let customTextEl = table.querySelector("#custom-value-text");
+ if (customTextEl.checked) {
+ customTextEl.checked = false;
+ } else {
+ customTextEl.value = "";
+ }
+
+ this.updateFieldType();
+ },
+
+ updateNewField: function () {
+ let table = this._doc.querySelector("table");
+ let customValueType = this.updateFieldType();
+
+ if (!customValueType) {
+ return;
+ }
+
+ let customRow = table.querySelector("tr:nth-of-type(2)");
+ let customTextEl = table.querySelector("#custom-value-text");
+ let customTextNameEl = table.querySelector("#custom-value-name");
+
+ if (customTextEl.validity.valid) {
+ let customText = customTextEl.value;
+
+ if (customValueType === "boolean") {
+ customText = customTextEl.checked;
+ }
+
+ let customTextName = customTextNameEl.value.replace(/[^A-Za-z0-9\.\-_]/gi, "");
+ this.generateField(customTextName, customText, true, customValueType, customRow);
+ this._saveByType({
+ id: customTextName,
+ type: customValueType,
+ value: customText
+ });
+ customTextNameEl.value = "";
+ this.clearNewFields();
+ }
+ },
+
+ checkNewFieldSubmit: function (event) {
+ if (event.keyCode === 13) {
+ this._doc.getElementById("custom-value").click();
+ }
+ }
+};
diff --git a/devtools/client/webide/modules/moz.build b/devtools/client/webide/modules/moz.build
new file mode 100644
index 000000000..c4072b703
--- /dev/null
+++ b/devtools/client/webide/modules/moz.build
@@ -0,0 +1,21 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'addons.js',
+ 'app-manager.js',
+ 'app-projects.js',
+ 'app-validator.js',
+ 'build.js',
+ 'config-view.js',
+ 'project-list.js',
+ 'runtime-list.js',
+ 'runtimes.js',
+ 'simulator-process.js',
+ 'simulators.js',
+ 'tab-store.js',
+ 'utils.js'
+)
diff --git a/devtools/client/webide/modules/project-list.js b/devtools/client/webide/modules/project-list.js
new file mode 100644
index 000000000..10766dd4f
--- /dev/null
+++ b/devtools/client/webide/modules/project-list.js
@@ -0,0 +1,375 @@
+/* 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/. */
+
+const {Cu} = require("chrome");
+
+const Services = require("Services");
+const {AppProjects} = require("devtools/client/webide/modules/app-projects");
+const {AppManager} = require("devtools/client/webide/modules/app-manager");
+const promise = require("promise");
+const EventEmitter = require("devtools/shared/event-emitter");
+const {Task} = require("devtools/shared/task");
+const utils = require("devtools/client/webide/modules/utils");
+const Telemetry = require("devtools/client/shared/telemetry");
+
+const Strings = Services.strings.createBundle("chrome://devtools/locale/webide.properties");
+
+var ProjectList;
+
+module.exports = ProjectList = function (win, parentWindow) {
+ EventEmitter.decorate(this);
+ this._doc = win.document;
+ this._UI = parentWindow.UI;
+ this._parentWindow = parentWindow;
+ this._telemetry = new Telemetry();
+ this._panelNodeEl = "div";
+
+ this.onWebIDEUpdate = this.onWebIDEUpdate.bind(this);
+ this._UI.on("webide-update", this.onWebIDEUpdate);
+
+ AppManager.init();
+ this.appManagerUpdate = this.appManagerUpdate.bind(this);
+ AppManager.on("app-manager-update", this.appManagerUpdate);
+};
+
+ProjectList.prototype = {
+ get doc() {
+ return this._doc;
+ },
+
+ appManagerUpdate: function (event, what, details) {
+ // Got a message from app-manager.js
+ // See AppManager.update() for descriptions of what these events mean.
+ switch (what) {
+ case "project-removed":
+ case "runtime-apps-icons":
+ case "runtime-targets":
+ case "connection":
+ this.update(details);
+ break;
+ case "project":
+ this.updateCommands();
+ this.update(details);
+ break;
+ }
+ },
+
+ onWebIDEUpdate: function (event, what, details) {
+ if (what == "busy" || what == "unbusy") {
+ this.updateCommands();
+ }
+ },
+
+ /**
+ * testOptions: { chrome mochitest support
+ * folder: nsIFile, where to store the app
+ * index: Number, index of the app in the template list
+ * name: String name of the app
+ * }
+ */
+ newApp: function (testOptions) {
+ let parentWindow = this._parentWindow;
+ let self = this;
+ return this._UI.busyUntil(Task.spawn(function* () {
+ // Open newapp.xul, which will feed ret.location
+ let ret = {location: null, testOptions: testOptions};
+ parentWindow.openDialog("chrome://webide/content/newapp.xul", "newapp", "chrome,modal", ret);
+ if (!ret.location)
+ return;
+
+ // Retrieve added project
+ let project = AppProjects.get(ret.location);
+
+ // Select project
+ AppManager.selectedProject = project;
+
+ self._telemetry.actionOccurred("webideNewProject");
+ }), "creating new app");
+ },
+
+ importPackagedApp: function (location) {
+ let parentWindow = this._parentWindow;
+ let UI = this._UI;
+ return UI.busyUntil(Task.spawn(function* () {
+ let directory = utils.getPackagedDirectory(parentWindow, location);
+
+ if (!directory) {
+ // User cancelled directory selection
+ return;
+ }
+
+ yield UI.importAndSelectApp(directory);
+ }), "importing packaged app");
+ },
+
+ importHostedApp: function (location) {
+ let parentWindow = this._parentWindow;
+ let UI = this._UI;
+ return UI.busyUntil(Task.spawn(function* () {
+ let url = utils.getHostedURL(parentWindow, location);
+
+ if (!url) {
+ return;
+ }
+
+ yield UI.importAndSelectApp(url);
+ }), "importing hosted app");
+ },
+
+ /**
+ * opts: {
+ * panel: Object, currenl project panel node
+ * name: String, name of the project
+ * icon: String path of the project icon
+ * }
+ */
+ _renderProjectItem: function (opts) {
+ let span = opts.panel.querySelector("span") || this._doc.createElement("span");
+ span.textContent = opts.name;
+ let icon = opts.panel.querySelector("img") || this._doc.createElement("img");
+ icon.className = "project-image";
+ icon.setAttribute("src", opts.icon);
+ opts.panel.appendChild(icon);
+ opts.panel.appendChild(span);
+ opts.panel.setAttribute("title", opts.name);
+ },
+
+ refreshTabs: function () {
+ if (AppManager.connected) {
+ return AppManager.listTabs().then(() => {
+ this.updateTabs();
+ }).catch(console.error);
+ }
+ },
+
+ updateTabs: function () {
+ let tabsHeaderNode = this._doc.querySelector("#panel-header-tabs");
+ let tabsNode = this._doc.querySelector("#project-panel-tabs");
+
+ while (tabsNode.hasChildNodes()) {
+ tabsNode.firstChild.remove();
+ }
+
+ if (!AppManager.connected) {
+ tabsHeaderNode.setAttribute("hidden", "true");
+ return;
+ }
+
+ let tabs = AppManager.tabStore.tabs;
+
+ tabsHeaderNode.removeAttribute("hidden");
+
+ for (let i = 0; i < tabs.length; i++) {
+ let tab = tabs[i];
+ let URL = this._parentWindow.URL;
+ let url;
+ try {
+ url = new URL(tab.url);
+ } catch (e) {
+ // Don't try to handle invalid URLs, especially from Valence.
+ continue;
+ }
+ // Wanted to use nsIFaviconService here, but it only works for visited
+ // tabs, so that's no help for any remote tabs. Maybe some favicon wizard
+ // knows how to get high-res favicons easily, or we could offer actor
+ // support for this (bug 1061654).
+ if (url.origin) {
+ tab.favicon = url.origin + "/favicon.ico";
+ }
+ tab.name = tab.title || Strings.GetStringFromName("project_tab_loading");
+ if (url.protocol.startsWith("http")) {
+ tab.name = url.hostname + ": " + tab.name;
+ }
+ let panelItemNode = this._doc.createElement(this._panelNodeEl);
+ panelItemNode.className = "panel-item";
+ tabsNode.appendChild(panelItemNode);
+ this._renderProjectItem({
+ panel: panelItemNode,
+ name: tab.name,
+ icon: tab.favicon || AppManager.DEFAULT_PROJECT_ICON
+ });
+ panelItemNode.addEventListener("click", () => {
+ AppManager.selectedProject = {
+ type: "tab",
+ app: tab,
+ icon: tab.favicon || AppManager.DEFAULT_PROJECT_ICON,
+ location: tab.url,
+ name: tab.name
+ };
+ }, true);
+ }
+
+ return promise.resolve();
+ },
+
+ updateApps: function () {
+ let doc = this._doc;
+ let runtimeappsHeaderNode = doc.querySelector("#panel-header-runtimeapps");
+ let sortedApps = [];
+ for (let [manifestURL, app] of AppManager.apps) {
+ sortedApps.push(app);
+ }
+ sortedApps = sortedApps.sort((a, b) => {
+ return a.manifest.name > b.manifest.name;
+ });
+ let mainProcess = AppManager.isMainProcessDebuggable();
+ if (AppManager.connected && (sortedApps.length > 0 || mainProcess)) {
+ runtimeappsHeaderNode.removeAttribute("hidden");
+ } else {
+ runtimeappsHeaderNode.setAttribute("hidden", "true");
+ }
+
+ let runtimeAppsNode = doc.querySelector("#project-panel-runtimeapps");
+ while (runtimeAppsNode.hasChildNodes()) {
+ runtimeAppsNode.firstChild.remove();
+ }
+
+ if (mainProcess) {
+ let panelItemNode = doc.createElement(this._panelNodeEl);
+ panelItemNode.className = "panel-item";
+ this._renderProjectItem({
+ panel: panelItemNode,
+ name: Strings.GetStringFromName("mainProcess_label"),
+ icon: AppManager.DEFAULT_PROJECT_ICON
+ });
+ runtimeAppsNode.appendChild(panelItemNode);
+ panelItemNode.addEventListener("click", () => {
+ AppManager.selectedProject = {
+ type: "mainProcess",
+ name: Strings.GetStringFromName("mainProcess_label"),
+ icon: AppManager.DEFAULT_PROJECT_ICON
+ };
+ }, true);
+ }
+
+ for (let i = 0; i < sortedApps.length; i++) {
+ let app = sortedApps[i];
+ let panelItemNode = doc.createElement(this._panelNodeEl);
+ panelItemNode.className = "panel-item";
+ this._renderProjectItem({
+ panel: panelItemNode,
+ name: app.manifest.name,
+ icon: app.iconURL || AppManager.DEFAULT_PROJECT_ICON
+ });
+ runtimeAppsNode.appendChild(panelItemNode);
+ panelItemNode.addEventListener("click", () => {
+ AppManager.selectedProject = {
+ type: "runtimeApp",
+ app: app.manifest,
+ icon: app.iconURL || AppManager.DEFAULT_PROJECT_ICON,
+ name: app.manifest.name
+ };
+ }, true);
+ }
+
+ return promise.resolve();
+ },
+
+ updateCommands: function () {
+ let doc = this._doc;
+ let newAppCmd;
+ let packagedAppCmd;
+ let hostedAppCmd;
+
+ newAppCmd = doc.querySelector("#new-app");
+ packagedAppCmd = doc.querySelector("#packaged-app");
+ hostedAppCmd = doc.querySelector("#hosted-app");
+
+ if (!newAppCmd || !packagedAppCmd || !hostedAppCmd) {
+ return;
+ }
+
+ if (this._parentWindow.document.querySelector("window").classList.contains("busy")) {
+ newAppCmd.setAttribute("disabled", "true");
+ packagedAppCmd.setAttribute("disabled", "true");
+ hostedAppCmd.setAttribute("disabled", "true");
+ return;
+ }
+
+ newAppCmd.removeAttribute("disabled");
+ packagedAppCmd.removeAttribute("disabled");
+ hostedAppCmd.removeAttribute("disabled");
+ },
+
+ /**
+ * Trigger an update of the project and remote runtime list.
+ * @param options object (optional)
+ * An |options| object containing a type of |apps| or |tabs| will limit
+ * what is updated to only those sections.
+ */
+ update: function (options) {
+ let deferred = promise.defer();
+
+ if (options && options.type === "apps") {
+ return this.updateApps();
+ } else if (options && options.type === "tabs") {
+ return this.updateTabs();
+ }
+
+ let doc = this._doc;
+ let projectsNode = doc.querySelector("#project-panel-projects");
+
+ while (projectsNode.hasChildNodes()) {
+ projectsNode.firstChild.remove();
+ }
+
+ AppProjects.load().then(() => {
+ let projects = AppProjects.projects;
+ for (let i = 0; i < projects.length; i++) {
+ let project = projects[i];
+ let panelItemNode = doc.createElement(this._panelNodeEl);
+ panelItemNode.className = "panel-item";
+ projectsNode.appendChild(panelItemNode);
+ if (!project.validationStatus) {
+ // The result of the validation process (storing names, icons, …) is not stored in
+ // the IndexedDB database when App Manager v1 is used.
+ // We need to run the validation again and update the name and icon of the app.
+ AppManager.validateAndUpdateProject(project).then(() => {
+ this._renderProjectItem({
+ panel: panelItemNode,
+ name: project.name,
+ icon: project.icon
+ });
+ });
+ } else {
+ this._renderProjectItem({
+ panel: panelItemNode,
+ name: project.name || AppManager.DEFAULT_PROJECT_NAME,
+ icon: project.icon || AppManager.DEFAULT_PROJECT_ICON
+ });
+ }
+ panelItemNode.addEventListener("click", () => {
+ AppManager.selectedProject = project;
+ }, true);
+ }
+
+ deferred.resolve();
+ }, deferred.reject);
+
+ // List remote apps and the main process, if they exist
+ this.updateApps();
+
+ // Build the tab list right now, so it's fast...
+ this.updateTabs();
+
+ // But re-list them and rebuild, in case any tabs navigated since the last
+ // time they were listed.
+ if (AppManager.connected) {
+ AppManager.listTabs().then(() => {
+ this.updateTabs();
+ }).catch(console.error);
+ }
+
+ return deferred.promise;
+ },
+
+ destroy: function () {
+ this._doc = null;
+ AppManager.off("app-manager-update", this.appManagerUpdate);
+ this._UI.off("webide-update", this.onWebIDEUpdate);
+ this._UI = null;
+ this._parentWindow = null;
+ this._panelNodeEl = null;
+ }
+};
diff --git a/devtools/client/webide/modules/runtime-list.js b/devtools/client/webide/modules/runtime-list.js
new file mode 100644
index 000000000..295dd1705
--- /dev/null
+++ b/devtools/client/webide/modules/runtime-list.js
@@ -0,0 +1,207 @@
+/* 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 Services = require("Services");
+const {AppManager} = require("devtools/client/webide/modules/app-manager");
+const EventEmitter = require("devtools/shared/event-emitter");
+const {RuntimeScanners, WiFiScanner} = require("devtools/client/webide/modules/runtimes");
+const {Devices} = require("resource://devtools/shared/apps/Devices.jsm");
+const {Task} = require("devtools/shared/task");
+const utils = require("devtools/client/webide/modules/utils");
+
+const Strings = Services.strings.createBundle("chrome://devtools/locale/webide.properties");
+
+var RuntimeList;
+
+module.exports = RuntimeList = function (window, parentWindow) {
+ EventEmitter.decorate(this);
+ this._doc = window.document;
+ this._UI = parentWindow.UI;
+ this._Cmds = parentWindow.Cmds;
+ this._parentWindow = parentWindow;
+ this._panelNodeEl = "button";
+ this._panelBoxEl = "div";
+
+ this.onWebIDEUpdate = this.onWebIDEUpdate.bind(this);
+ this._UI.on("webide-update", this.onWebIDEUpdate);
+
+ AppManager.init();
+ this.appManagerUpdate = this.appManagerUpdate.bind(this);
+ AppManager.on("app-manager-update", this.appManagerUpdate);
+};
+
+RuntimeList.prototype = {
+ get doc() {
+ return this._doc;
+ },
+
+ appManagerUpdate: function (event, what, details) {
+ // Got a message from app-manager.js
+ // See AppManager.update() for descriptions of what these events mean.
+ switch (what) {
+ case "runtime-list":
+ this.update();
+ break;
+ case "connection":
+ case "runtime-global-actors":
+ this.updateCommands();
+ break;
+ }
+ },
+
+ onWebIDEUpdate: function (event, what, details) {
+ if (what == "busy" || what == "unbusy") {
+ this.updateCommands();
+ }
+ },
+
+ takeScreenshot: function () {
+ this._Cmds.takeScreenshot();
+ },
+
+ showRuntimeDetails: function () {
+ this._Cmds.showRuntimeDetails();
+ },
+
+ showPermissionsTable: function () {
+ this._Cmds.showPermissionsTable();
+ },
+
+ showDevicePreferences: function () {
+ this._Cmds.showDevicePrefs();
+ },
+
+ showSettings: function () {
+ this._Cmds.showSettings();
+ },
+
+ showTroubleShooting: function () {
+ this._Cmds.showTroubleShooting();
+ },
+
+ showAddons: function () {
+ this._Cmds.showAddons();
+ },
+
+ refreshScanners: function () {
+ RuntimeScanners.scan();
+ },
+
+ updateCommands: function () {
+ let doc = this._doc;
+
+ // Runtime commands
+ let screenshotCmd = doc.querySelector("#runtime-screenshot");
+ let permissionsCmd = doc.querySelector("#runtime-permissions");
+ let detailsCmd = doc.querySelector("#runtime-details");
+ let disconnectCmd = doc.querySelector("#runtime-disconnect");
+ let devicePrefsCmd = doc.querySelector("#runtime-preferences");
+ let settingsCmd = doc.querySelector("#runtime-settings");
+
+ if (AppManager.connected) {
+ if (AppManager.deviceFront) {
+ detailsCmd.removeAttribute("disabled");
+ permissionsCmd.removeAttribute("disabled");
+ screenshotCmd.removeAttribute("disabled");
+ }
+ if (AppManager.preferenceFront) {
+ devicePrefsCmd.removeAttribute("disabled");
+ }
+ if (AppManager.settingsFront) {
+ settingsCmd.removeAttribute("disabled");
+ }
+ disconnectCmd.removeAttribute("disabled");
+ } else {
+ detailsCmd.setAttribute("disabled", "true");
+ permissionsCmd.setAttribute("disabled", "true");
+ screenshotCmd.setAttribute("disabled", "true");
+ disconnectCmd.setAttribute("disabled", "true");
+ devicePrefsCmd.setAttribute("disabled", "true");
+ settingsCmd.setAttribute("disabled", "true");
+ }
+ },
+
+ update: function () {
+ let doc = this._doc;
+ let wifiHeaderNode = doc.querySelector("#runtime-header-wifi");
+
+ if (WiFiScanner.allowed) {
+ wifiHeaderNode.removeAttribute("hidden");
+ } else {
+ wifiHeaderNode.setAttribute("hidden", "true");
+ }
+
+ let usbListNode = doc.querySelector("#runtime-panel-usb");
+ let wifiListNode = doc.querySelector("#runtime-panel-wifi");
+ let simulatorListNode = doc.querySelector("#runtime-panel-simulator");
+ let otherListNode = doc.querySelector("#runtime-panel-other");
+ let noHelperNode = doc.querySelector("#runtime-panel-noadbhelper");
+ let noUSBNode = doc.querySelector("#runtime-panel-nousbdevice");
+
+ if (Devices.helperAddonInstalled) {
+ noHelperNode.setAttribute("hidden", "true");
+ } else {
+ noHelperNode.removeAttribute("hidden");
+ }
+
+ let runtimeList = AppManager.runtimeList;
+
+ if (!runtimeList) {
+ return;
+ }
+
+ if (runtimeList.usb.length === 0 && Devices.helperAddonInstalled) {
+ noUSBNode.removeAttribute("hidden");
+ } else {
+ noUSBNode.setAttribute("hidden", "true");
+ }
+
+ for (let [type, parent] of [
+ ["usb", usbListNode],
+ ["wifi", wifiListNode],
+ ["simulator", simulatorListNode],
+ ["other", otherListNode],
+ ]) {
+ while (parent.hasChildNodes()) {
+ parent.firstChild.remove();
+ }
+ for (let runtime of runtimeList[type]) {
+ let r = runtime;
+ let panelItemNode = doc.createElement(this._panelBoxEl);
+ panelItemNode.className = "panel-item-complex";
+
+ let connectButton = doc.createElement(this._panelNodeEl);
+ connectButton.className = "panel-item runtime-panel-item-" + type;
+ connectButton.textContent = r.name;
+
+ connectButton.addEventListener("click", () => {
+ this._UI.dismissErrorNotification();
+ this._UI.connectToRuntime(r);
+ }, true);
+ panelItemNode.appendChild(connectButton);
+
+ if (r.configure) {
+ let configButton = doc.createElement(this._panelNodeEl);
+ configButton.className = "configure-button";
+ configButton.addEventListener("click", r.configure.bind(r), true);
+ panelItemNode.appendChild(configButton);
+ }
+
+ parent.appendChild(panelItemNode);
+ }
+ }
+ },
+
+ destroy: function () {
+ this._doc = null;
+ AppManager.off("app-manager-update", this.appManagerUpdate);
+ this._UI.off("webide-update", this.onWebIDEUpdate);
+ this._UI = null;
+ this._Cmds = null;
+ this._parentWindow = null;
+ this._panelNodeEl = null;
+ }
+};
diff --git a/devtools/client/webide/modules/runtimes.js b/devtools/client/webide/modules/runtimes.js
new file mode 100644
index 000000000..a23337359
--- /dev/null
+++ b/devtools/client/webide/modules/runtimes.js
@@ -0,0 +1,673 @@
+/* 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 {Devices} = require("resource://devtools/shared/apps/Devices.jsm");
+const {Connection} = require("devtools/shared/client/connection-manager");
+const {DebuggerServer} = require("devtools/server/main");
+const {Simulators} = require("devtools/client/webide/modules/simulators");
+const discovery = require("devtools/shared/discovery/discovery");
+const EventEmitter = require("devtools/shared/event-emitter");
+const promise = require("promise");
+loader.lazyRequireGetter(this, "AuthenticationResult",
+ "devtools/shared/security/auth", true);
+loader.lazyRequireGetter(this, "DevToolsUtils",
+ "devtools/shared/DevToolsUtils");
+
+const Strings = Services.strings.createBundle("chrome://devtools/locale/webide.properties");
+
+/**
+ * Runtime and Scanner API
+ *
+ * |RuntimeScanners| maintains a set of |Scanner| objects that produce one or
+ * more |Runtime|s to connect to. Add-ons can extend the set of known runtimes
+ * by registering additional |Scanner|s that emit them.
+ *
+ * Each |Scanner| must support the following API:
+ *
+ * enable()
+ * Bind any event handlers and start any background work the scanner needs to
+ * maintain an updated set of |Runtime|s.
+ * Called when the first consumer (such as WebIDE) actively interested in
+ * maintaining the |Runtime| list enables the registry.
+ * disable()
+ * Unbind any event handlers and stop any background work the scanner needs to
+ * maintain an updated set of |Runtime|s.
+ * Called when the last consumer (such as WebIDE) actively interested in
+ * maintaining the |Runtime| list disables the registry.
+ * emits "runtime-list-updated"
+ * If the set of runtimes a |Scanner| manages has changed, it must emit this
+ * event to notify consumers of changes.
+ * scan()
+ * Actively refreshes the list of runtimes the scanner knows about. If your
+ * scanner uses an active scanning approach (as opposed to listening for
+ * events when changes occur), the bulk of the work would be done here.
+ * @return Promise
+ * Should be resolved when scanning is complete. If scanning has no
+ * well-defined end point, you can resolve immediately, as long as
+ * update event is emitted later when changes are noticed.
+ * listRuntimes()
+ * Return the current list of runtimes known to the |Scanner| instance.
+ * @return Iterable
+ *
+ * Each |Runtime| must support the following API:
+ *
+ * |type| field
+ * The |type| must be one of the values from the |RuntimeTypes| object. This
+ * is used for Telemetry and to support displaying sets of |Runtime|s
+ * categorized by type.
+ * |id| field
+ * An identifier that is unique in the set of all runtimes with the same
+ * |type|. WebIDE tries to save the last used runtime via type + id, and
+ * tries to locate it again in the next session, so this value should attempt
+ * to be stable across Firefox sessions.
+ * |name| field
+ * A user-visible label to identify the runtime that will be displayed in a
+ * runtime list.
+ * |prolongedConnection| field
+ * A boolean value which should be |true| if the connection process is
+ * expected to take a unknown or large amount of time. A UI may use this as a
+ * hint to skip timeouts or other time-based code paths.
+ * connect()
+ * Configure the passed |connection| object with any settings need to
+ * successfully connect to the runtime, and call the |connection|'s connect()
+ * method.
+ * @param Connection connection
+ * A |Connection| object from the DevTools |ConnectionManager|.
+ * @return Promise
+ * Resolved once you've called the |connection|'s connect() method.
+ * configure() OPTIONAL
+ * Show a configuration screen if the runtime is configurable.
+ */
+
+/* SCANNER REGISTRY */
+
+var RuntimeScanners = {
+
+ _enabledCount: 0,
+ _scanners: new Set(),
+
+ get enabled() {
+ return !!this._enabledCount;
+ },
+
+ add(scanner) {
+ if (this.enabled) {
+ // Enable any scanner added while globally enabled
+ this._enableScanner(scanner);
+ }
+ this._scanners.add(scanner);
+ this._emitUpdated();
+ },
+
+ remove(scanner) {
+ this._scanners.delete(scanner);
+ if (this.enabled) {
+ // Disable any scanner removed while globally enabled
+ this._disableScanner(scanner);
+ }
+ this._emitUpdated();
+ },
+
+ has(scanner) {
+ return this._scanners.has(scanner);
+ },
+
+ scan() {
+ if (!this.enabled) {
+ return promise.resolve();
+ }
+
+ if (this._scanPromise) {
+ return this._scanPromise;
+ }
+
+ let promises = [];
+
+ for (let scanner of this._scanners) {
+ promises.push(scanner.scan());
+ }
+
+ this._scanPromise = promise.all(promises);
+
+ // Reset pending promise
+ this._scanPromise.then(() => {
+ this._scanPromise = null;
+ }, () => {
+ this._scanPromise = null;
+ });
+
+ return this._scanPromise;
+ },
+
+ listRuntimes: function* () {
+ for (let scanner of this._scanners) {
+ for (let runtime of scanner.listRuntimes()) {
+ yield runtime;
+ }
+ }
+ },
+
+ _emitUpdated() {
+ this.emit("runtime-list-updated");
+ },
+
+ enable() {
+ if (this._enabledCount++ !== 0) {
+ // Already enabled scanners during a previous call
+ return;
+ }
+ this._emitUpdated = this._emitUpdated.bind(this);
+ for (let scanner of this._scanners) {
+ this._enableScanner(scanner);
+ }
+ },
+
+ _enableScanner(scanner) {
+ scanner.enable();
+ scanner.on("runtime-list-updated", this._emitUpdated);
+ },
+
+ disable() {
+ if (--this._enabledCount !== 0) {
+ // Already disabled scanners during a previous call
+ return;
+ }
+ for (let scanner of this._scanners) {
+ this._disableScanner(scanner);
+ }
+ },
+
+ _disableScanner(scanner) {
+ scanner.off("runtime-list-updated", this._emitUpdated);
+ scanner.disable();
+ },
+
+};
+
+EventEmitter.decorate(RuntimeScanners);
+
+exports.RuntimeScanners = RuntimeScanners;
+
+/* SCANNERS */
+
+var SimulatorScanner = {
+
+ _runtimes: [],
+
+ enable() {
+ this._updateRuntimes = this._updateRuntimes.bind(this);
+ Simulators.on("updated", this._updateRuntimes);
+ this._updateRuntimes();
+ },
+
+ disable() {
+ Simulators.off("updated", this._updateRuntimes);
+ },
+
+ _emitUpdated() {
+ this.emit("runtime-list-updated");
+ },
+
+ _updateRuntimes() {
+ Simulators.findSimulators().then(simulators => {
+ this._runtimes = [];
+ for (let simulator of simulators) {
+ this._runtimes.push(new SimulatorRuntime(simulator));
+ }
+ this._emitUpdated();
+ });
+ },
+
+ scan() {
+ return promise.resolve();
+ },
+
+ listRuntimes: function () {
+ return this._runtimes;
+ }
+
+};
+
+EventEmitter.decorate(SimulatorScanner);
+RuntimeScanners.add(SimulatorScanner);
+
+/**
+ * TODO: Remove this comaptibility layer in the future (bug 1085393)
+ * This runtime exists to support the ADB Helper add-on below version 0.7.0.
+ *
+ * This scanner will list all ADB devices as runtimes, even if they may or may
+ * not actually connect (since the |DeprecatedUSBRuntime| assumes a Firefox OS
+ * device).
+ */
+var DeprecatedAdbScanner = {
+
+ _runtimes: [],
+
+ enable() {
+ this._updateRuntimes = this._updateRuntimes.bind(this);
+ Devices.on("register", this._updateRuntimes);
+ Devices.on("unregister", this._updateRuntimes);
+ Devices.on("addon-status-updated", this._updateRuntimes);
+ this._updateRuntimes();
+ },
+
+ disable() {
+ Devices.off("register", this._updateRuntimes);
+ Devices.off("unregister", this._updateRuntimes);
+ Devices.off("addon-status-updated", this._updateRuntimes);
+ },
+
+ _emitUpdated() {
+ this.emit("runtime-list-updated");
+ },
+
+ _updateRuntimes() {
+ this._runtimes = [];
+ for (let id of Devices.available()) {
+ let runtime = new DeprecatedUSBRuntime(id);
+ this._runtimes.push(runtime);
+ runtime.updateNameFromADB().then(() => {
+ this._emitUpdated();
+ }, () => {});
+ }
+ this._emitUpdated();
+ },
+
+ scan() {
+ return promise.resolve();
+ },
+
+ listRuntimes: function () {
+ return this._runtimes;
+ }
+
+};
+
+EventEmitter.decorate(DeprecatedAdbScanner);
+RuntimeScanners.add(DeprecatedAdbScanner);
+
+// ADB Helper 0.7.0 and later will replace this scanner on startup
+exports.DeprecatedAdbScanner = DeprecatedAdbScanner;
+
+/**
+ * This is a lazy ADB scanner shim which only tells the ADB Helper to start and
+ * stop as needed. The real scanner that lists devices lives in ADB Helper.
+ * ADB Helper 0.8.0 and later wait until these signals are received before
+ * starting ADB polling. For earlier versions, they have no effect.
+ */
+var LazyAdbScanner = {
+
+ enable() {
+ Devices.emit("adb-start-polling");
+ },
+
+ disable() {
+ Devices.emit("adb-stop-polling");
+ },
+
+ scan() {
+ return promise.resolve();
+ },
+
+ listRuntimes: function () {
+ return [];
+ }
+
+};
+
+EventEmitter.decorate(LazyAdbScanner);
+RuntimeScanners.add(LazyAdbScanner);
+
+var WiFiScanner = {
+
+ _runtimes: [],
+
+ init() {
+ this.updateRegistration();
+ Services.prefs.addObserver(this.ALLOWED_PREF, this, false);
+ },
+
+ enable() {
+ this._updateRuntimes = this._updateRuntimes.bind(this);
+ discovery.on("devtools-device-added", this._updateRuntimes);
+ discovery.on("devtools-device-updated", this._updateRuntimes);
+ discovery.on("devtools-device-removed", this._updateRuntimes);
+ this._updateRuntimes();
+ },
+
+ disable() {
+ discovery.off("devtools-device-added", this._updateRuntimes);
+ discovery.off("devtools-device-updated", this._updateRuntimes);
+ discovery.off("devtools-device-removed", this._updateRuntimes);
+ },
+
+ _emitUpdated() {
+ this.emit("runtime-list-updated");
+ },
+
+ _updateRuntimes() {
+ this._runtimes = [];
+ for (let device of discovery.getRemoteDevicesWithService("devtools")) {
+ this._runtimes.push(new WiFiRuntime(device));
+ }
+ this._emitUpdated();
+ },
+
+ scan() {
+ discovery.scan();
+ return promise.resolve();
+ },
+
+ listRuntimes: function () {
+ return this._runtimes;
+ },
+
+ ALLOWED_PREF: "devtools.remote.wifi.scan",
+
+ get allowed() {
+ return Services.prefs.getBoolPref(this.ALLOWED_PREF);
+ },
+
+ updateRegistration() {
+ if (this.allowed) {
+ RuntimeScanners.add(WiFiScanner);
+ } else {
+ RuntimeScanners.remove(WiFiScanner);
+ }
+ this._emitUpdated();
+ },
+
+ observe(subject, topic, data) {
+ if (data !== WiFiScanner.ALLOWED_PREF) {
+ return;
+ }
+ WiFiScanner.updateRegistration();
+ }
+
+};
+
+EventEmitter.decorate(WiFiScanner);
+WiFiScanner.init();
+
+exports.WiFiScanner = WiFiScanner;
+
+var StaticScanner = {
+ enable() {},
+ disable() {},
+ scan() { return promise.resolve(); },
+ listRuntimes() {
+ let runtimes = [gRemoteRuntime];
+ if (Services.prefs.getBoolPref("devtools.webide.enableLocalRuntime")) {
+ runtimes.push(gLocalRuntime);
+ }
+ return runtimes;
+ }
+};
+
+EventEmitter.decorate(StaticScanner);
+RuntimeScanners.add(StaticScanner);
+
+/* RUNTIMES */
+
+// These type strings are used for logging events to Telemetry.
+// You must update Histograms.json if new types are added.
+var RuntimeTypes = exports.RuntimeTypes = {
+ USB: "USB",
+ WIFI: "WIFI",
+ SIMULATOR: "SIMULATOR",
+ REMOTE: "REMOTE",
+ LOCAL: "LOCAL",
+ OTHER: "OTHER"
+};
+
+/**
+ * TODO: Remove this comaptibility layer in the future (bug 1085393)
+ * This runtime exists to support the ADB Helper add-on below version 0.7.0.
+ *
+ * This runtime assumes it is connecting to a Firefox OS device.
+ */
+function DeprecatedUSBRuntime(id) {
+ this._id = id;
+}
+
+DeprecatedUSBRuntime.prototype = {
+ type: RuntimeTypes.USB,
+ get device() {
+ return Devices.getByName(this._id);
+ },
+ connect: function (connection) {
+ if (!this.device) {
+ return promise.reject(new Error("Can't find device: " + this.name));
+ }
+ return this.device.connect().then((port) => {
+ connection.host = "localhost";
+ connection.port = port;
+ connection.connect();
+ });
+ },
+ get id() {
+ return this._id;
+ },
+ get name() {
+ return this._productModel || this._id;
+ },
+ updateNameFromADB: function () {
+ if (this._productModel) {
+ return promise.reject();
+ }
+ let deferred = promise.defer();
+ if (this.device && this.device.shell) {
+ this.device.shell("getprop ro.product.model").then(stdout => {
+ this._productModel = stdout;
+ deferred.resolve();
+ }, () => {});
+ } else {
+ this._productModel = null;
+ deferred.reject();
+ }
+ return deferred.promise;
+ },
+};
+
+// For testing use only
+exports._DeprecatedUSBRuntime = DeprecatedUSBRuntime;
+
+function WiFiRuntime(deviceName) {
+ this.deviceName = deviceName;
+}
+
+WiFiRuntime.prototype = {
+ type: RuntimeTypes.WIFI,
+ // Mark runtime as taking a long time to connect
+ prolongedConnection: true,
+ connect: function (connection) {
+ let service = discovery.getRemoteService("devtools", this.deviceName);
+ if (!service) {
+ return promise.reject(new Error("Can't find device: " + this.name));
+ }
+ connection.advertisement = service;
+ connection.authenticator.sendOOB = this.sendOOB;
+ // Disable the default connection timeout, since QR scanning can take an
+ // unknown amount of time. This prevents spurious errors (even after
+ // eventual success) from being shown.
+ connection.timeoutDelay = 0;
+ connection.connect();
+ return promise.resolve();
+ },
+ get id() {
+ return this.deviceName;
+ },
+ get name() {
+ return this.deviceName;
+ },
+
+ /**
+ * During OOB_CERT authentication, a notification dialog like this is used to
+ * to display a token which the user must transfer through some mechanism to the
+ * server to authenticate the devices.
+ *
+ * This implementation presents the token as text for the user to transfer
+ * manually. For a mobile device, you should override this implementation with
+ * something more convenient, such as displaying a QR code.
+ *
+ * This method receives an object containing:
+ * @param host string
+ * The host name or IP address of the debugger server.
+ * @param port number
+ * The port number of the debugger server.
+ * @param cert object (optional)
+ * The server's cert details.
+ * @param authResult AuthenticationResult
+ * Authentication result sent from the server.
+ * @param oob object (optional)
+ * The token data to be transferred during OOB_CERT step 8:
+ * * sha256: hash(ClientCert)
+ * * k : K(random 128-bit number)
+ * @return object containing:
+ * * close: Function to hide the notification
+ */
+ sendOOB(session) {
+ const WINDOW_ID = "devtools:wifi-auth";
+ let { authResult } = session;
+ // Only show in the PENDING state
+ if (authResult != AuthenticationResult.PENDING) {
+ throw new Error("Expected PENDING result, got " + authResult);
+ }
+
+ // Listen for the window our prompt opens, so we can close it programatically
+ let promptWindow;
+ let windowListener = {
+ onOpenWindow(xulWindow) {
+ let win = xulWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow);
+ win.addEventListener("load", function listener() {
+ win.removeEventListener("load", listener, false);
+ if (win.document.documentElement.getAttribute("id") != WINDOW_ID) {
+ return;
+ }
+ // Found the window
+ promptWindow = win;
+ Services.wm.removeListener(windowListener);
+ }, false);
+ },
+ onCloseWindow() {},
+ onWindowTitleChange() {}
+ };
+ Services.wm.addListener(windowListener);
+
+ // |openDialog| is typically a blocking API, so |executeSoon| to get around this
+ DevToolsUtils.executeSoon(() => {
+ // Height determines the size of the QR code. Force a minimum size to
+ // improve scanability.
+ const MIN_HEIGHT = 600;
+ let win = Services.wm.getMostRecentWindow("devtools:webide");
+ let width = win.outerWidth * 0.8;
+ let height = Math.max(win.outerHeight * 0.5, MIN_HEIGHT);
+ win.openDialog("chrome://webide/content/wifi-auth.xhtml",
+ WINDOW_ID,
+ "modal=yes,width=" + width + ",height=" + height, session);
+ });
+
+ return {
+ close() {
+ if (!promptWindow) {
+ return;
+ }
+ promptWindow.close();
+ promptWindow = null;
+ }
+ };
+ }
+};
+
+// For testing use only
+exports._WiFiRuntime = WiFiRuntime;
+
+function SimulatorRuntime(simulator) {
+ this.simulator = simulator;
+}
+
+SimulatorRuntime.prototype = {
+ type: RuntimeTypes.SIMULATOR,
+ connect: function (connection) {
+ return this.simulator.launch().then(port => {
+ connection.host = "localhost";
+ connection.port = port;
+ connection.keepConnecting = true;
+ connection.once(Connection.Events.DISCONNECTED, e => this.simulator.kill());
+ connection.connect();
+ });
+ },
+ configure() {
+ Simulators.emit("configure", this.simulator);
+ },
+ get id() {
+ return this.simulator.id;
+ },
+ get name() {
+ return this.simulator.name;
+ },
+};
+
+// For testing use only
+exports._SimulatorRuntime = SimulatorRuntime;
+
+var gLocalRuntime = {
+ type: RuntimeTypes.LOCAL,
+ connect: function (connection) {
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+ DebuggerServer.allowChromeProcess = true;
+ connection.host = null; // Force Pipe transport
+ connection.port = null;
+ connection.connect();
+ return promise.resolve();
+ },
+ get id() {
+ return "local";
+ },
+ get name() {
+ return Strings.GetStringFromName("local_runtime");
+ },
+};
+
+// For testing use only
+exports._gLocalRuntime = gLocalRuntime;
+
+var gRemoteRuntime = {
+ type: RuntimeTypes.REMOTE,
+ connect: function (connection) {
+ let win = Services.wm.getMostRecentWindow("devtools:webide");
+ if (!win) {
+ return promise.reject(new Error("No WebIDE window found"));
+ }
+ let ret = {value: connection.host + ":" + connection.port};
+ let title = Strings.GetStringFromName("remote_runtime_promptTitle");
+ let message = Strings.GetStringFromName("remote_runtime_promptMessage");
+ let ok = Services.prompt.prompt(win, title, message, ret, null, {});
+ let [host, port] = ret.value.split(":");
+ if (!ok) {
+ return promise.reject({canceled: true});
+ }
+ if (!host || !port) {
+ return promise.reject(new Error("Invalid host or port"));
+ }
+ connection.host = host;
+ connection.port = port;
+ connection.connect();
+ return promise.resolve();
+ },
+ get name() {
+ return Strings.GetStringFromName("remote_runtime");
+ },
+};
+
+// For testing use only
+exports._gRemoteRuntime = gRemoteRuntime;
diff --git a/devtools/client/webide/modules/simulator-process.js b/devtools/client/webide/modules/simulator-process.js
new file mode 100644
index 000000000..7d0b57cc6
--- /dev/null
+++ b/devtools/client/webide/modules/simulator-process.js
@@ -0,0 +1,325 @@
+/* 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 Environment = require("sdk/system/environment").env;
+const EventEmitter = require("devtools/shared/event-emitter");
+const promise = require("promise");
+const Subprocess = require("sdk/system/child_process/subprocess");
+const Services = require("Services");
+
+loader.lazyGetter(this, "OS", () => {
+ const Runtime = require("sdk/system/runtime");
+ switch (Runtime.OS) {
+ case "Darwin":
+ return "mac64";
+ case "Linux":
+ if (Runtime.XPCOMABI.indexOf("x86_64") === 0) {
+ return "linux64";
+ } else {
+ return "linux32";
+ }
+ case "WINNT":
+ return "win32";
+ default:
+ return "";
+ }
+});
+
+function SimulatorProcess() {}
+SimulatorProcess.prototype = {
+
+ // Check if B2G is running.
+ get isRunning() {
+ return !!this.process;
+ },
+
+ // Start the process and connect the debugger client.
+ run() {
+
+ // Resolve B2G binary.
+ let b2g = this.b2gBinary;
+ if (!b2g || !b2g.exists()) {
+ throw Error("B2G executable not found.");
+ }
+
+ // Ensure Gaia profile exists.
+ let gaia = this.gaiaProfile;
+ if (!gaia || !gaia.exists()) {
+ throw Error("Gaia profile directory not found.");
+ }
+
+ this.once("stdout", function () {
+ if (OS == "mac64") {
+ console.debug("WORKAROUND run osascript to show b2g-desktop window on OS=='mac64'");
+ // Escape double quotes and escape characters for use in AppleScript.
+ let path = b2g.path.replace(/\\/g, "\\\\").replace(/\"/g, '\\"');
+
+ Subprocess.call({
+ command: "/usr/bin/osascript",
+ arguments: ["-e", 'tell application "' + path + '" to activate'],
+ });
+ }
+ });
+
+ let logHandler = (e, data) => this.log(e, data.trim());
+ this.on("stdout", logHandler);
+ this.on("stderr", logHandler);
+ this.once("exit", () => {
+ this.off("stdout", logHandler);
+ this.off("stderr", logHandler);
+ });
+
+ let environment;
+ if (OS.indexOf("linux") > -1) {
+ environment = ["TMPDIR=" + Services.dirsvc.get("TmpD", Ci.nsIFile).path];
+ ["DISPLAY", "XAUTHORITY"].forEach(key => {
+ if (key in Environment) {
+ environment.push(key + "=" + Environment[key]);
+ }
+ });
+ }
+
+ // Spawn a B2G instance.
+ this.process = Subprocess.call({
+ command: b2g,
+ arguments: this.args,
+ environment: environment,
+ stdout: data => this.emit("stdout", data),
+ stderr: data => this.emit("stderr", data),
+ // On B2G instance exit, reset tracked process, remote debugger port and
+ // shuttingDown flag, then finally emit an exit event.
+ done: result => {
+ console.log("B2G terminated with " + result.exitCode);
+ this.process = null;
+ this.emit("exit", result.exitCode);
+ }
+ });
+ },
+
+ // Request a B2G instance kill.
+ kill() {
+ let deferred = promise.defer();
+ if (this.process) {
+ this.once("exit", (e, exitCode) => {
+ this.shuttingDown = false;
+ deferred.resolve(exitCode);
+ });
+ if (!this.shuttingDown) {
+ this.shuttingDown = true;
+ this.emit("kill", null);
+ this.process.kill();
+ }
+ return deferred.promise;
+ } else {
+ return promise.resolve(undefined);
+ }
+ },
+
+ // Maybe log output messages.
+ log(level, message) {
+ if (!Services.prefs.getBoolPref("devtools.webide.logSimulatorOutput")) {
+ return;
+ }
+ if (level === "stderr" || level === "error") {
+ console.error(message);
+ return;
+ }
+ console.log(message);
+ },
+
+ // Compute B2G CLI arguments.
+ get args() {
+ let args = [];
+
+ // Gaia profile.
+ args.push("-profile", this.gaiaProfile.path);
+
+ // Debugger server port.
+ let port = parseInt(this.options.port);
+ args.push("-start-debugger-server", "" + port);
+
+ // Screen size.
+ let width = parseInt(this.options.width);
+ let height = parseInt(this.options.height);
+ if (width && height) {
+ args.push("-screen", width + "x" + height);
+ }
+
+ // Ignore eventual zombie instances of b2g that are left over.
+ args.push("-no-remote");
+
+ // If we are running a simulator based on Mulet,
+ // we have to override the default chrome URL
+ // in order to prevent the Browser UI to appear.
+ if (this.b2gBinary.leafName.includes("firefox")) {
+ args.push("-chrome", "chrome://b2g/content/shell.html");
+ }
+
+ return args;
+ },
+};
+
+EventEmitter.decorate(SimulatorProcess.prototype);
+
+
+function CustomSimulatorProcess(options) {
+ this.options = options;
+}
+
+var CSPp = CustomSimulatorProcess.prototype = Object.create(SimulatorProcess.prototype);
+
+// Compute B2G binary file handle.
+Object.defineProperty(CSPp, "b2gBinary", {
+ get: function () {
+ let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
+ file.initWithPath(this.options.b2gBinary);
+ return file;
+ }
+});
+
+// Compute Gaia profile file handle.
+Object.defineProperty(CSPp, "gaiaProfile", {
+ get: function () {
+ let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
+ file.initWithPath(this.options.gaiaProfile);
+ return file;
+ }
+});
+
+exports.CustomSimulatorProcess = CustomSimulatorProcess;
+
+
+function AddonSimulatorProcess(addon, options) {
+ this.addon = addon;
+ this.options = options;
+}
+
+var ASPp = AddonSimulatorProcess.prototype = Object.create(SimulatorProcess.prototype);
+
+// Compute B2G binary file handle.
+Object.defineProperty(ASPp, "b2gBinary", {
+ get: function () {
+ let file;
+ try {
+ let pref = "extensions." + this.addon.id + ".customRuntime";
+ file = Services.prefs.getComplexValue(pref, Ci.nsIFile);
+ } catch (e) {}
+
+ if (!file) {
+ file = this.addon.getResourceURI().QueryInterface(Ci.nsIFileURL).file;
+ file.append("b2g");
+ let binaries = {
+ win32: "b2g-bin.exe",
+ mac64: "B2G.app/Contents/MacOS/b2g-bin",
+ linux32: "b2g-bin",
+ linux64: "b2g-bin",
+ };
+ binaries[OS].split("/").forEach(node => file.append(node));
+ }
+ // If the binary doesn't exists, it may be because of a simulator
+ // based on mulet, which has a different binary name.
+ if (!file.exists()) {
+ file = this.addon.getResourceURI().QueryInterface(Ci.nsIFileURL).file;
+ file.append("firefox");
+ let binaries = {
+ win32: "firefox.exe",
+ mac64: "FirefoxNightly.app/Contents/MacOS/firefox-bin",
+ linux32: "firefox-bin",
+ linux64: "firefox-bin",
+ };
+ binaries[OS].split("/").forEach(node => file.append(node));
+ }
+ return file;
+ }
+});
+
+// Compute Gaia profile file handle.
+Object.defineProperty(ASPp, "gaiaProfile", {
+ get: function () {
+ let file;
+
+ // Custom profile from simulator configuration.
+ if (this.options.gaiaProfile) {
+ file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
+ file.initWithPath(this.options.gaiaProfile);
+ return file;
+ }
+
+ // Custom profile from addon prefs.
+ try {
+ let pref = "extensions." + this.addon.id + ".gaiaProfile";
+ file = Services.prefs.getComplexValue(pref, Ci.nsIFile);
+ return file;
+ } catch (e) {}
+
+ // Default profile from addon.
+ file = this.addon.getResourceURI().QueryInterface(Ci.nsIFileURL).file;
+ file.append("profile");
+ return file;
+ }
+});
+
+exports.AddonSimulatorProcess = AddonSimulatorProcess;
+
+
+function OldAddonSimulatorProcess(addon, options) {
+ this.addon = addon;
+ this.options = options;
+}
+
+var OASPp = OldAddonSimulatorProcess.prototype = Object.create(AddonSimulatorProcess.prototype);
+
+// Compute B2G binary file handle.
+Object.defineProperty(OASPp, "b2gBinary", {
+ get: function () {
+ let file;
+ try {
+ let pref = "extensions." + this.addon.id + ".customRuntime";
+ file = Services.prefs.getComplexValue(pref, Ci.nsIFile);
+ } catch (e) {}
+
+ if (!file) {
+ file = this.addon.getResourceURI().QueryInterface(Ci.nsIFileURL).file;
+ let version = this.addon.name.match(/\d+\.\d+/)[0].replace(/\./, "_");
+ file.append("resources");
+ file.append("fxos_" + version + "_simulator");
+ file.append("data");
+ file.append(OS == "linux32" ? "linux" : OS);
+ let binaries = {
+ win32: "b2g/b2g-bin.exe",
+ mac64: "B2G.app/Contents/MacOS/b2g-bin",
+ linux32: "b2g/b2g-bin",
+ linux64: "b2g/b2g-bin",
+ };
+ binaries[OS].split("/").forEach(node => file.append(node));
+ }
+ return file;
+ }
+});
+
+// Compute B2G CLI arguments.
+Object.defineProperty(OASPp, "args", {
+ get: function () {
+ let args = [];
+
+ // Gaia profile.
+ args.push("-profile", this.gaiaProfile.path);
+
+ // Debugger server port.
+ let port = parseInt(this.options.port);
+ args.push("-dbgport", "" + port);
+
+ // Ignore eventual zombie instances of b2g that are left over.
+ args.push("-no-remote");
+
+ return args;
+ }
+});
+
+exports.OldAddonSimulatorProcess = OldAddonSimulatorProcess;
diff --git a/devtools/client/webide/modules/simulators.js b/devtools/client/webide/modules/simulators.js
new file mode 100644
index 000000000..f09df9e05
--- /dev/null
+++ b/devtools/client/webide/modules/simulators.js
@@ -0,0 +1,368 @@
+/* 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 { Task } = require("devtools/shared/task");
+loader.lazyRequireGetter(this, "ConnectionManager", "devtools/shared/client/connection-manager", true);
+loader.lazyRequireGetter(this, "AddonSimulatorProcess", "devtools/client/webide/modules/simulator-process", true);
+loader.lazyRequireGetter(this, "OldAddonSimulatorProcess", "devtools/client/webide/modules/simulator-process", true);
+loader.lazyRequireGetter(this, "CustomSimulatorProcess", "devtools/client/webide/modules/simulator-process", true);
+const asyncStorage = require("devtools/shared/async-storage");
+const EventEmitter = require("devtools/shared/event-emitter");
+const promise = require("promise");
+const Services = require("Services");
+
+const SimulatorRegExp = new RegExp(Services.prefs.getCharPref("devtools.webide.simulatorAddonRegExp"));
+const LocaleCompare = (a, b) => {
+ return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
+};
+
+var Simulators = {
+
+ // The list of simulator configurations.
+ _simulators: [],
+
+ /**
+ * Load a previously saved list of configurations (only once).
+ *
+ * @return Promise.
+ */
+ _load() {
+ if (this._loadingPromise) {
+ return this._loadingPromise;
+ }
+
+ this._loadingPromise = Task.spawn(function* () {
+ let jobs = [];
+
+ let value = yield asyncStorage.getItem("simulators");
+ if (Array.isArray(value)) {
+ value.forEach(options => {
+ let simulator = new Simulator(options);
+ Simulators.add(simulator, true);
+
+ // If the simulator had a reference to an addon, fix it.
+ if (options.addonID) {
+ let deferred = promise.defer();
+ AddonManager.getAddonByID(options.addonID, addon => {
+ simulator.addon = addon;
+ delete simulator.options.addonID;
+ deferred.resolve();
+ });
+ jobs.push(deferred.promise);
+ }
+ });
+ }
+
+ yield promise.all(jobs);
+ yield Simulators._addUnusedAddons();
+ Simulators.emitUpdated();
+ return Simulators._simulators;
+ });
+
+ return this._loadingPromise;
+ },
+
+ /**
+ * Add default simulators to the list for each new (unused) addon.
+ *
+ * @return Promise.
+ */
+ _addUnusedAddons: Task.async(function* () {
+ let jobs = [];
+
+ let addons = yield Simulators.findSimulatorAddons();
+ addons.forEach(addon => {
+ jobs.push(Simulators.addIfUnusedAddon(addon, true));
+ });
+
+ yield promise.all(jobs);
+ }),
+
+ /**
+ * Save the current list of configurations.
+ *
+ * @return Promise.
+ */
+ _save: Task.async(function* () {
+ yield this._load();
+
+ let value = Simulators._simulators.map(simulator => {
+ let options = JSON.parse(JSON.stringify(simulator.options));
+ if (simulator.addon != null) {
+ options.addonID = simulator.addon.id;
+ }
+ return options;
+ });
+
+ yield asyncStorage.setItem("simulators", value);
+ }),
+
+ /**
+ * List all available simulators.
+ *
+ * @return Promised simulator list.
+ */
+ findSimulators: Task.async(function* () {
+ yield this._load();
+ return Simulators._simulators;
+ }),
+
+ /**
+ * List all installed simulator addons.
+ *
+ * @return Promised addon list.
+ */
+ findSimulatorAddons() {
+ let deferred = promise.defer();
+ AddonManager.getAllAddons(all => {
+ let addons = [];
+ for (let addon of all) {
+ if (Simulators.isSimulatorAddon(addon)) {
+ addons.push(addon);
+ }
+ }
+ // Sort simulator addons by name.
+ addons.sort(LocaleCompare);
+ deferred.resolve(addons);
+ });
+ return deferred.promise;
+ },
+
+ /**
+ * Add a new simulator for `addon` if no other simulator uses it.
+ */
+ addIfUnusedAddon(addon, silently = false) {
+ let simulators = this._simulators;
+ let matching = simulators.filter(s => s.addon && s.addon.id == addon.id);
+ if (matching.length > 0) {
+ return promise.resolve();
+ }
+ let options = {};
+ options.name = addon.name.replace(" Simulator", "");
+ // Some addons specify a simulator type at the end of their version string,
+ // e.g. "2_5_tv".
+ let type = this.simulatorAddonVersion(addon).split("_")[2];
+ if (type) {
+ // "tv" is shorthand for type "television".
+ options.type = (type === "tv" ? "television" : type);
+ }
+ return this.add(new Simulator(options, addon), silently);
+ },
+
+ // TODO (Bug 1146521) Maybe find a better way to deal with removed addons?
+ removeIfUsingAddon(addon) {
+ let simulators = this._simulators;
+ let remaining = simulators.filter(s => !s.addon || s.addon.id != addon.id);
+ this._simulators = remaining;
+ if (remaining.length !== simulators.length) {
+ this.emitUpdated();
+ }
+ },
+
+ /**
+ * Add a new simulator to the list. Caution: `simulator.name` may be modified.
+ *
+ * @return Promise to added simulator.
+ */
+ add(simulator, silently = false) {
+ let simulators = this._simulators;
+ let uniqueName = this.uniqueName(simulator.options.name);
+ simulator.options.name = uniqueName;
+ simulators.push(simulator);
+ if (!silently) {
+ this.emitUpdated();
+ }
+ return promise.resolve(simulator);
+ },
+
+ /**
+ * Remove a simulator from the list.
+ */
+ remove(simulator) {
+ let simulators = this._simulators;
+ let remaining = simulators.filter(s => s !== simulator);
+ this._simulators = remaining;
+ if (remaining.length !== simulators.length) {
+ this.emitUpdated();
+ }
+ },
+
+ /**
+ * Get a unique name for a simulator (may add a suffix, e.g. "MyName (1)").
+ */
+ uniqueName(name) {
+ let simulators = this._simulators;
+
+ let names = {};
+ simulators.forEach(simulator => names[simulator.name] = true);
+
+ // Strip any previous suffix, add a new suffix if necessary.
+ let stripped = name.replace(/ \(\d+\)$/, "");
+ let unique = stripped;
+ for (let i = 1; names[unique]; i++) {
+ unique = stripped + " (" + i + ")";
+ }
+ return unique;
+ },
+
+ /**
+ * Compare an addon's ID against the expected form of a simulator addon ID,
+ * and try to extract its version if there is a match.
+ *
+ * Note: If a simulator addon is recognized, but no version can be extracted
+ * (e.g. custom RegExp pref value), we return "Unknown" to keep the returned
+ * value 'truthy'.
+ */
+ simulatorAddonVersion(addon) {
+ let match = SimulatorRegExp.exec(addon.id);
+ if (!match) {
+ return null;
+ }
+ let version = match[1];
+ return version || "Unknown";
+ },
+
+ /**
+ * Detect simulator addons, including "unofficial" ones.
+ */
+ isSimulatorAddon(addon) {
+ return !!this.simulatorAddonVersion(addon);
+ },
+
+ emitUpdated() {
+ this.emit("updated", { length: this._simulators.length });
+ this._simulators.sort(LocaleCompare);
+ this._save();
+ },
+
+ onConfigure(e, simulator) {
+ this._lastConfiguredSimulator = simulator;
+ },
+
+ onInstalled(addon) {
+ if (this.isSimulatorAddon(addon)) {
+ this.addIfUnusedAddon(addon);
+ }
+ },
+
+ onEnabled(addon) {
+ if (this.isSimulatorAddon(addon)) {
+ this.addIfUnusedAddon(addon);
+ }
+ },
+
+ onDisabled(addon) {
+ if (this.isSimulatorAddon(addon)) {
+ this.removeIfUsingAddon(addon);
+ }
+ },
+
+ onUninstalled(addon) {
+ if (this.isSimulatorAddon(addon)) {
+ this.removeIfUsingAddon(addon);
+ }
+ },
+};
+exports.Simulators = Simulators;
+AddonManager.addAddonListener(Simulators);
+EventEmitter.decorate(Simulators);
+Simulators.on("configure", Simulators.onConfigure.bind(Simulators));
+
+function Simulator(options = {}, addon = null) {
+ this.addon = addon;
+ this.options = options;
+
+ // Fill `this.options` with default values where needed.
+ let defaults = this.defaults;
+ for (let option in defaults) {
+ if (this.options[option] == null) {
+ this.options[option] = defaults[option];
+ }
+ }
+}
+Simulator.prototype = {
+
+ // Default simulation options.
+ _defaults: {
+ // Based on the Firefox OS Flame.
+ phone: {
+ width: 320,
+ height: 570,
+ pixelRatio: 1.5
+ },
+ // Based on a 720p HD TV.
+ television: {
+ width: 1280,
+ height: 720,
+ pixelRatio: 1,
+ }
+ },
+ _defaultType: "phone",
+
+ restoreDefaults() {
+ let defaults = this.defaults;
+ let options = this.options;
+ for (let option in defaults) {
+ options[option] = defaults[option];
+ }
+ },
+
+ launch() {
+ // Close already opened simulation.
+ if (this.process) {
+ return this.kill().then(this.launch.bind(this));
+ }
+
+ this.options.port = ConnectionManager.getFreeTCPPort();
+
+ // Choose simulator process type.
+ if (this.options.b2gBinary) {
+ // Custom binary.
+ this.process = new CustomSimulatorProcess(this.options);
+ } else if (this.version > "1.3") {
+ // Recent simulator addon.
+ this.process = new AddonSimulatorProcess(this.addon, this.options);
+ } else {
+ // Old simulator addon.
+ this.process = new OldAddonSimulatorProcess(this.addon, this.options);
+ }
+ this.process.run();
+
+ return promise.resolve(this.options.port);
+ },
+
+ kill() {
+ let process = this.process;
+ if (!process) {
+ return promise.resolve();
+ }
+ this.process = null;
+ return process.kill();
+ },
+
+ get defaults() {
+ let defaults = this._defaults;
+ return defaults[this.type] || defaults[this._defaultType];
+ },
+
+ get id() {
+ return this.name;
+ },
+
+ get name() {
+ return this.options.name;
+ },
+
+ get type() {
+ return this.options.type || this._defaultType;
+ },
+
+ get version() {
+ return this.options.b2gBinary ? "Custom" : this.addon.name.match(/\d+\.\d+/)[0];
+ },
+};
+exports.Simulator = Simulator;
diff --git a/devtools/client/webide/modules/tab-store.js b/devtools/client/webide/modules/tab-store.js
new file mode 100644
index 000000000..0fed366cc
--- /dev/null
+++ b/devtools/client/webide/modules/tab-store.js
@@ -0,0 +1,178 @@
+/* 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/. */
+
+const { Cu } = require("chrome");
+
+const { TargetFactory } = require("devtools/client/framework/target");
+const EventEmitter = require("devtools/shared/event-emitter");
+const { Connection } = require("devtools/shared/client/connection-manager");
+const promise = require("promise");
+const { Task } = require("devtools/shared/task");
+
+const _knownTabStores = new WeakMap();
+
+var TabStore;
+
+module.exports = TabStore = function (connection) {
+ // If we already know about this connection,
+ // let's re-use the existing store.
+ if (_knownTabStores.has(connection)) {
+ return _knownTabStores.get(connection);
+ }
+
+ _knownTabStores.set(connection, this);
+
+ EventEmitter.decorate(this);
+
+ this._resetStore();
+
+ this.destroy = this.destroy.bind(this);
+ this._onStatusChanged = this._onStatusChanged.bind(this);
+
+ this._connection = connection;
+ this._connection.once(Connection.Events.DESTROYED, this.destroy);
+ this._connection.on(Connection.Events.STATUS_CHANGED, this._onStatusChanged);
+ this._onTabListChanged = this._onTabListChanged.bind(this);
+ this._onTabNavigated = this._onTabNavigated.bind(this);
+ this._onStatusChanged();
+ return this;
+};
+
+TabStore.prototype = {
+
+ destroy: function () {
+ if (this._connection) {
+ // While this.destroy is bound using .once() above, that event may not
+ // have occurred when the TabStore client calls destroy, so we
+ // manually remove it here.
+ this._connection.off(Connection.Events.DESTROYED, this.destroy);
+ this._connection.off(Connection.Events.STATUS_CHANGED, this._onStatusChanged);
+ _knownTabStores.delete(this._connection);
+ this._connection = null;
+ }
+ },
+
+ _resetStore: function () {
+ this.response = null;
+ this.tabs = [];
+ this._selectedTab = null;
+ this._selectedTabTargetPromise = null;
+ },
+
+ _onStatusChanged: function () {
+ if (this._connection.status == Connection.Status.CONNECTED) {
+ // Watch for changes to remote browser tabs
+ this._connection.client.addListener("tabListChanged",
+ this._onTabListChanged);
+ this._connection.client.addListener("tabNavigated",
+ this._onTabNavigated);
+ this.listTabs();
+ } else {
+ if (this._connection.client) {
+ this._connection.client.removeListener("tabListChanged",
+ this._onTabListChanged);
+ this._connection.client.removeListener("tabNavigated",
+ this._onTabNavigated);
+ }
+ this._resetStore();
+ }
+ },
+
+ _onTabListChanged: function () {
+ this.listTabs().then(() => this.emit("tab-list"))
+ .catch(console.error);
+ },
+
+ _onTabNavigated: function (e, { from, title, url }) {
+ if (!this._selectedTab || from !== this._selectedTab.actor) {
+ return;
+ }
+ this._selectedTab.url = url;
+ this._selectedTab.title = title;
+ this.emit("navigate");
+ },
+
+ listTabs: function () {
+ if (!this._connection || !this._connection.client) {
+ return promise.reject(new Error("Can't listTabs, not connected."));
+ }
+ let deferred = promise.defer();
+ this._connection.client.listTabs(response => {
+ if (response.error) {
+ this._connection.disconnect();
+ deferred.reject(response.error);
+ return;
+ }
+ let tabsChanged = JSON.stringify(this.tabs) !== JSON.stringify(response.tabs);
+ this.response = response;
+ this.tabs = response.tabs;
+ this._checkSelectedTab();
+ if (tabsChanged) {
+ this.emit("tab-list");
+ }
+ deferred.resolve(response);
+ });
+ return deferred.promise;
+ },
+
+ // TODO: Tab "selection" should really take place by creating a TabProject
+ // which is the selected project. This should be done as part of the
+ // project-agnostic work.
+ _selectedTab: null,
+ _selectedTabTargetPromise: null,
+ get selectedTab() {
+ return this._selectedTab;
+ },
+ set selectedTab(tab) {
+ if (this._selectedTab === tab) {
+ return;
+ }
+ this._selectedTab = tab;
+ this._selectedTabTargetPromise = null;
+ // Attach to the tab to follow navigation events
+ if (this._selectedTab) {
+ this.getTargetForTab();
+ }
+ },
+
+ _checkSelectedTab: function () {
+ if (!this._selectedTab) {
+ return;
+ }
+ let alive = this.tabs.some(tab => {
+ return tab.actor === this._selectedTab.actor;
+ });
+ if (!alive) {
+ this._selectedTab = null;
+ this._selectedTabTargetPromise = null;
+ this.emit("closed");
+ }
+ },
+
+ getTargetForTab: function () {
+ if (this._selectedTabTargetPromise) {
+ return this._selectedTabTargetPromise;
+ }
+ let store = this;
+ this._selectedTabTargetPromise = Task.spawn(function* () {
+ // If you connect to a tab, then detach from it, the root actor may have
+ // de-listed the actors that belong to the tab. This breaks the toolbox
+ // if you try to connect to the same tab again. To work around this
+ // issue, we force a "listTabs" request before connecting to a tab.
+ yield store.listTabs();
+ return TargetFactory.forRemoteTab({
+ form: store._selectedTab,
+ client: store._connection.client,
+ chrome: false
+ });
+ });
+ this._selectedTabTargetPromise.then(target => {
+ target.once("close", () => {
+ this._selectedTabTargetPromise = null;
+ });
+ });
+ return this._selectedTabTargetPromise;
+ },
+
+};
diff --git a/devtools/client/webide/modules/utils.js b/devtools/client/webide/modules/utils.js
new file mode 100644
index 000000000..7a19c7044
--- /dev/null
+++ b/devtools/client/webide/modules/utils.js
@@ -0,0 +1,68 @@
+/* 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/. */
+
+const { Cc, Cu, Ci } = require("chrome");
+const { FileUtils } = Cu.import("resource://gre/modules/FileUtils.jsm", {});
+const Services = require("Services");
+const Strings = Services.strings.createBundle("chrome://devtools/locale/webide.properties");
+
+function doesFileExist(location) {
+ let file = new FileUtils.File(location);
+ return file.exists();
+}
+exports.doesFileExist = doesFileExist;
+
+function _getFile(location, ...pickerParams) {
+ if (location) {
+ return new FileUtils.File(location);
+ }
+ let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ fp.init(...pickerParams);
+ let res = fp.show();
+ if (res == Ci.nsIFilePicker.returnCancel) {
+ return null;
+ }
+ return fp.file;
+}
+
+function getCustomBinary(window, location) {
+ return _getFile(location, window, Strings.GetStringFromName("selectCustomBinary_title"), Ci.nsIFilePicker.modeOpen);
+}
+exports.getCustomBinary = getCustomBinary;
+
+function getCustomProfile(window, location) {
+ return _getFile(location, window, Strings.GetStringFromName("selectCustomProfile_title"), Ci.nsIFilePicker.modeGetFolder);
+}
+exports.getCustomProfile = getCustomProfile;
+
+function getPackagedDirectory(window, location) {
+ return _getFile(location, window, Strings.GetStringFromName("importPackagedApp_title"), Ci.nsIFilePicker.modeGetFolder);
+}
+exports.getPackagedDirectory = getPackagedDirectory;
+
+function getHostedURL(window, location) {
+ let ret = { value: null };
+
+ if (!location) {
+ Services.prompt.prompt(window,
+ Strings.GetStringFromName("importHostedApp_title"),
+ Strings.GetStringFromName("importHostedApp_header"),
+ ret, null, {});
+ location = ret.value;
+ }
+
+ if (!location) {
+ return null;
+ }
+
+ // Clean location string and add "http://" if missing
+ location = location.trim();
+ try { // Will fail if no scheme
+ Services.io.extractScheme(location);
+ } catch (e) {
+ location = "http://" + location;
+ }
+ return location;
+}
+exports.getHostedURL = getHostedURL;
diff --git a/devtools/client/webide/moz.build b/devtools/client/webide/moz.build
new file mode 100644
index 000000000..c5dcb07a9
--- /dev/null
+++ b/devtools/client/webide/moz.build
@@ -0,0 +1,23 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DIRS += [
+ 'content',
+ 'components',
+ 'modules',
+ 'themes',
+]
+
+BROWSER_CHROME_MANIFESTS += [
+ 'test/browser.ini'
+]
+MOCHITEST_CHROME_MANIFESTS += [
+ 'test/chrome.ini'
+]
+
+JS_PREFERENCE_PP_FILES += [
+ 'webide-prefs.js',
+]
diff --git a/devtools/client/webide/test/.eslintrc.js b/devtools/client/webide/test/.eslintrc.js
new file mode 100644
index 000000000..8d15a76d9
--- /dev/null
+++ b/devtools/client/webide/test/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../../.eslintrc.mochitests.js"
+};
diff --git a/devtools/client/webide/test/addons/adbhelper-linux.xpi b/devtools/client/webide/test/addons/adbhelper-linux.xpi
new file mode 100644
index 000000000..b56cc03e3
--- /dev/null
+++ b/devtools/client/webide/test/addons/adbhelper-linux.xpi
Binary files differ
diff --git a/devtools/client/webide/test/addons/adbhelper-linux64.xpi b/devtools/client/webide/test/addons/adbhelper-linux64.xpi
new file mode 100644
index 000000000..b56cc03e3
--- /dev/null
+++ b/devtools/client/webide/test/addons/adbhelper-linux64.xpi
Binary files differ
diff --git a/devtools/client/webide/test/addons/adbhelper-mac64.xpi b/devtools/client/webide/test/addons/adbhelper-mac64.xpi
new file mode 100644
index 000000000..b56cc03e3
--- /dev/null
+++ b/devtools/client/webide/test/addons/adbhelper-mac64.xpi
Binary files differ
diff --git a/devtools/client/webide/test/addons/adbhelper-win32.xpi b/devtools/client/webide/test/addons/adbhelper-win32.xpi
new file mode 100644
index 000000000..b56cc03e3
--- /dev/null
+++ b/devtools/client/webide/test/addons/adbhelper-win32.xpi
Binary files differ
diff --git a/devtools/client/webide/test/addons/fxdt-adapters-linux32.xpi b/devtools/client/webide/test/addons/fxdt-adapters-linux32.xpi
new file mode 100644
index 000000000..5a512ae3d
--- /dev/null
+++ b/devtools/client/webide/test/addons/fxdt-adapters-linux32.xpi
Binary files differ
diff --git a/devtools/client/webide/test/addons/fxdt-adapters-linux64.xpi b/devtools/client/webide/test/addons/fxdt-adapters-linux64.xpi
new file mode 100644
index 000000000..5a512ae3d
--- /dev/null
+++ b/devtools/client/webide/test/addons/fxdt-adapters-linux64.xpi
Binary files differ
diff --git a/devtools/client/webide/test/addons/fxdt-adapters-mac64.xpi b/devtools/client/webide/test/addons/fxdt-adapters-mac64.xpi
new file mode 100644
index 000000000..5a512ae3d
--- /dev/null
+++ b/devtools/client/webide/test/addons/fxdt-adapters-mac64.xpi
Binary files differ
diff --git a/devtools/client/webide/test/addons/fxdt-adapters-win32.xpi b/devtools/client/webide/test/addons/fxdt-adapters-win32.xpi
new file mode 100644
index 000000000..5a512ae3d
--- /dev/null
+++ b/devtools/client/webide/test/addons/fxdt-adapters-win32.xpi
Binary files differ
diff --git a/devtools/client/webide/test/addons/fxos_1_0_simulator-linux.xpi b/devtools/client/webide/test/addons/fxos_1_0_simulator-linux.xpi
new file mode 100644
index 000000000..238c97562
--- /dev/null
+++ b/devtools/client/webide/test/addons/fxos_1_0_simulator-linux.xpi
Binary files differ
diff --git a/devtools/client/webide/test/addons/fxos_1_0_simulator-linux64.xpi b/devtools/client/webide/test/addons/fxos_1_0_simulator-linux64.xpi
new file mode 100644
index 000000000..2f86c4d4d
--- /dev/null
+++ b/devtools/client/webide/test/addons/fxos_1_0_simulator-linux64.xpi
Binary files differ
diff --git a/devtools/client/webide/test/addons/fxos_1_0_simulator-mac64.xpi b/devtools/client/webide/test/addons/fxos_1_0_simulator-mac64.xpi
new file mode 100644
index 000000000..6da2fcbad
--- /dev/null
+++ b/devtools/client/webide/test/addons/fxos_1_0_simulator-mac64.xpi
Binary files differ
diff --git a/devtools/client/webide/test/addons/fxos_1_0_simulator-win32.xpi b/devtools/client/webide/test/addons/fxos_1_0_simulator-win32.xpi
new file mode 100644
index 000000000..546deacaf
--- /dev/null
+++ b/devtools/client/webide/test/addons/fxos_1_0_simulator-win32.xpi
Binary files differ
diff --git a/devtools/client/webide/test/addons/fxos_2_0_simulator-linux.xpi b/devtools/client/webide/test/addons/fxos_2_0_simulator-linux.xpi
new file mode 100644
index 000000000..e2335e3a0
--- /dev/null
+++ b/devtools/client/webide/test/addons/fxos_2_0_simulator-linux.xpi
Binary files differ
diff --git a/devtools/client/webide/test/addons/fxos_2_0_simulator-linux64.xpi b/devtools/client/webide/test/addons/fxos_2_0_simulator-linux64.xpi
new file mode 100644
index 000000000..75fe209ea
--- /dev/null
+++ b/devtools/client/webide/test/addons/fxos_2_0_simulator-linux64.xpi
Binary files differ
diff --git a/devtools/client/webide/test/addons/fxos_2_0_simulator-mac64.xpi b/devtools/client/webide/test/addons/fxos_2_0_simulator-mac64.xpi
new file mode 100644
index 000000000..58749f724
--- /dev/null
+++ b/devtools/client/webide/test/addons/fxos_2_0_simulator-mac64.xpi
Binary files differ
diff --git a/devtools/client/webide/test/addons/fxos_2_0_simulator-win32.xpi b/devtools/client/webide/test/addons/fxos_2_0_simulator-win32.xpi
new file mode 100644
index 000000000..60cffd46e
--- /dev/null
+++ b/devtools/client/webide/test/addons/fxos_2_0_simulator-win32.xpi
Binary files differ
diff --git a/devtools/client/webide/test/addons/fxos_3_0_simulator-linux.xpi b/devtools/client/webide/test/addons/fxos_3_0_simulator-linux.xpi
new file mode 100644
index 000000000..c54cae3aa
--- /dev/null
+++ b/devtools/client/webide/test/addons/fxos_3_0_simulator-linux.xpi
Binary files differ
diff --git a/devtools/client/webide/test/addons/fxos_3_0_simulator-linux64.xpi b/devtools/client/webide/test/addons/fxos_3_0_simulator-linux64.xpi
new file mode 100644
index 000000000..9a650a888
--- /dev/null
+++ b/devtools/client/webide/test/addons/fxos_3_0_simulator-linux64.xpi
Binary files differ
diff --git a/devtools/client/webide/test/addons/fxos_3_0_simulator-mac64.xpi b/devtools/client/webide/test/addons/fxos_3_0_simulator-mac64.xpi
new file mode 100644
index 000000000..d13dd78de
--- /dev/null
+++ b/devtools/client/webide/test/addons/fxos_3_0_simulator-mac64.xpi
Binary files differ
diff --git a/devtools/client/webide/test/addons/fxos_3_0_simulator-win32.xpi b/devtools/client/webide/test/addons/fxos_3_0_simulator-win32.xpi
new file mode 100644
index 000000000..92d5cc394
--- /dev/null
+++ b/devtools/client/webide/test/addons/fxos_3_0_simulator-win32.xpi
Binary files differ
diff --git a/devtools/client/webide/test/addons/fxos_3_0_tv_simulator-linux.xpi b/devtools/client/webide/test/addons/fxos_3_0_tv_simulator-linux.xpi
new file mode 100644
index 000000000..7a2a432ff
--- /dev/null
+++ b/devtools/client/webide/test/addons/fxos_3_0_tv_simulator-linux.xpi
Binary files differ
diff --git a/devtools/client/webide/test/addons/fxos_3_0_tv_simulator-linux64.xpi b/devtools/client/webide/test/addons/fxos_3_0_tv_simulator-linux64.xpi
new file mode 100644
index 000000000..d38932195
--- /dev/null
+++ b/devtools/client/webide/test/addons/fxos_3_0_tv_simulator-linux64.xpi
Binary files differ
diff --git a/devtools/client/webide/test/addons/fxos_3_0_tv_simulator-mac64.xpi b/devtools/client/webide/test/addons/fxos_3_0_tv_simulator-mac64.xpi
new file mode 100644
index 000000000..48e271d54
--- /dev/null
+++ b/devtools/client/webide/test/addons/fxos_3_0_tv_simulator-mac64.xpi
Binary files differ
diff --git a/devtools/client/webide/test/addons/fxos_3_0_tv_simulator-win32.xpi b/devtools/client/webide/test/addons/fxos_3_0_tv_simulator-win32.xpi
new file mode 100644
index 000000000..4c8bb2f10
--- /dev/null
+++ b/devtools/client/webide/test/addons/fxos_3_0_tv_simulator-win32.xpi
Binary files differ
diff --git a/devtools/client/webide/test/addons/simulators.json b/devtools/client/webide/test/addons/simulators.json
new file mode 100644
index 000000000..31d71b4da
--- /dev/null
+++ b/devtools/client/webide/test/addons/simulators.json
@@ -0,0 +1,4 @@
+{
+ "stable": ["1.0", "2.0"],
+ "unstable": ["3.0", "3.0_tv"]
+}
diff --git a/devtools/client/webide/test/app.zip b/devtools/client/webide/test/app.zip
new file mode 100644
index 000000000..8a706a3c9
--- /dev/null
+++ b/devtools/client/webide/test/app.zip
Binary files differ
diff --git a/devtools/client/webide/test/app/index.html b/devtools/client/webide/test/app/index.html
new file mode 100644
index 000000000..3ef4a25e2
--- /dev/null
+++ b/devtools/client/webide/test/app/index.html
@@ -0,0 +1,6 @@
+<!doctype html>
+<html>
+<head><title></title></head>
+<body>
+</body>
+</html>
diff --git a/devtools/client/webide/test/app/manifest.webapp b/devtools/client/webide/test/app/manifest.webapp
new file mode 100644
index 000000000..4a198b1ca
--- /dev/null
+++ b/devtools/client/webide/test/app/manifest.webapp
@@ -0,0 +1,5 @@
+{
+ "name": "A name (in app directory)",
+ "description": "desc",
+ "launch_path": "/index.html"
+}
diff --git a/devtools/client/webide/test/browser.ini b/devtools/client/webide/test/browser.ini
new file mode 100644
index 000000000..7d6e2de72
--- /dev/null
+++ b/devtools/client/webide/test/browser.ini
@@ -0,0 +1,12 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ addons/simulators.json
+ doc_tabs.html
+ head.js
+ templates.json
+
+[browser_tabs.js]
+skip-if = e10s # Bug 1072167 - browser_tabs.js test fails under e10s
+[browser_widget.js]
diff --git a/devtools/client/webide/test/browser_tabs.js b/devtools/client/webide/test/browser_tabs.js
new file mode 100644
index 000000000..541c6b363
--- /dev/null
+++ b/devtools/client/webide/test/browser_tabs.js
@@ -0,0 +1,84 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webide/test/doc_tabs.html";
+
+function test() {
+ waitForExplicitFinish();
+ requestCompleteLog();
+
+ Task.spawn(function* () {
+ // Since we test the connections set below, destroy the server in case it
+ // was left open.
+ DebuggerServer.destroy();
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+
+ let tab = yield addTab(TEST_URI);
+
+ let win = yield openWebIDE();
+ let docProject = getProjectDocument(win);
+ let docRuntime = getRuntimeDocument(win);
+
+ yield connectToLocal(win, docRuntime);
+
+ is(Object.keys(DebuggerServer._connections).length, 1, "Locally connected");
+
+ yield selectTabProject(win, docProject);
+
+ ok(win.UI.toolboxPromise, "Toolbox promise exists");
+ yield win.UI.toolboxPromise;
+
+ let project = win.AppManager.selectedProject;
+ is(project.location, TEST_URI, "Location is correct");
+ is(project.name, "example.com: Test Tab", "Name is correct");
+
+ // Ensure tab list changes are noticed
+ let tabsNode = docProject.querySelector("#project-panel-tabs");
+ is(tabsNode.querySelectorAll(".panel-item").length, 2, "2 tabs available");
+ yield removeTab(tab);
+ yield waitForUpdate(win, "project");
+ yield waitForUpdate(win, "runtime-targets");
+ is(tabsNode.querySelectorAll(".panel-item").length, 1, "1 tab available");
+
+ tab = yield addTab(TEST_URI);
+
+ is(tabsNode.querySelectorAll(".panel-item").length, 2, "2 tabs available");
+
+ yield removeTab(tab);
+
+ is(tabsNode.querySelectorAll(".panel-item").length, 2, "2 tabs available");
+
+ docProject.querySelector("#refresh-tabs").click();
+
+ yield waitForUpdate(win, "runtime-targets");
+
+ is(tabsNode.querySelectorAll(".panel-item").length, 1, "1 tab available");
+
+ yield win.Cmds.disconnectRuntime();
+ yield closeWebIDE(win);
+
+ DebuggerServer.destroy();
+ }).then(finish, handleError);
+}
+
+function connectToLocal(win, docRuntime) {
+ let deferred = promise.defer();
+ win.AppManager.connection.once(
+ win.Connection.Events.CONNECTED,
+ () => deferred.resolve());
+ docRuntime.querySelectorAll(".runtime-panel-item-other")[1].click();
+ return deferred.promise;
+}
+
+function selectTabProject(win, docProject) {
+ return Task.spawn(function* () {
+ yield waitForUpdate(win, "runtime-targets");
+ let tabsNode = docProject.querySelector("#project-panel-tabs");
+ let tabNode = tabsNode.querySelectorAll(".panel-item")[1];
+ let project = waitForUpdate(win, "project");
+ tabNode.click();
+ yield project;
+ });
+}
diff --git a/devtools/client/webide/test/browser_widget.js b/devtools/client/webide/test/browser_widget.js
new file mode 100644
index 000000000..7cfb2782b
--- /dev/null
+++ b/devtools/client/webide/test/browser_widget.js
@@ -0,0 +1,15 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+function test() {
+ waitForExplicitFinish();
+ Task.spawn(function* () {
+ let win = yield openWebIDE();
+ ok(document.querySelector("#webide-button"), "Found WebIDE button");
+ Services.prefs.setBoolPref("devtools.webide.widget.enabled", false);
+ ok(!document.querySelector("#webide-button"), "WebIDE button uninstalled");
+ yield closeWebIDE(win);
+ Services.prefs.clearUserPref("devtools.webide.widget.enabled");
+ }).then(finish, handleError);
+}
diff --git a/devtools/client/webide/test/build_app1/package.json b/devtools/client/webide/test/build_app1/package.json
new file mode 100644
index 000000000..c6ae833e1
--- /dev/null
+++ b/devtools/client/webide/test/build_app1/package.json
@@ -0,0 +1,5 @@
+{
+ "webide": {
+ "prepackage": "echo \"{\\\"name\\\":\\\"hello\\\"}\" > manifest.webapp"
+ }
+}
diff --git a/devtools/client/webide/test/build_app2/manifest.webapp b/devtools/client/webide/test/build_app2/manifest.webapp
new file mode 100644
index 000000000..0967ef424
--- /dev/null
+++ b/devtools/client/webide/test/build_app2/manifest.webapp
@@ -0,0 +1 @@
+{}
diff --git a/devtools/client/webide/test/build_app2/package.json b/devtools/client/webide/test/build_app2/package.json
new file mode 100644
index 000000000..5b7101620
--- /dev/null
+++ b/devtools/client/webide/test/build_app2/package.json
@@ -0,0 +1,10 @@
+{
+ "webide": {
+ "prepackage": {
+ "command": "echo \"{\\\"name\\\":\\\"$NAME\\\"}\" > manifest.webapp",
+ "cwd": "./stage",
+ "env": ["NAME=world"]
+ },
+ "packageDir": "./stage"
+ }
+}
diff --git a/devtools/client/webide/test/build_app2/stage/empty-directory b/devtools/client/webide/test/build_app2/stage/empty-directory
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/devtools/client/webide/test/build_app2/stage/empty-directory
diff --git a/devtools/client/webide/test/build_app_windows1/package.json b/devtools/client/webide/test/build_app_windows1/package.json
new file mode 100644
index 000000000..036d2d767
--- /dev/null
+++ b/devtools/client/webide/test/build_app_windows1/package.json
@@ -0,0 +1,5 @@
+{
+ "webide": {
+ "prepackage": "echo {\"name\":\"hello\"} > manifest.webapp"
+ }
+}
diff --git a/devtools/client/webide/test/build_app_windows2/manifest.webapp b/devtools/client/webide/test/build_app_windows2/manifest.webapp
new file mode 100644
index 000000000..0967ef424
--- /dev/null
+++ b/devtools/client/webide/test/build_app_windows2/manifest.webapp
@@ -0,0 +1 @@
+{}
diff --git a/devtools/client/webide/test/build_app_windows2/package.json b/devtools/client/webide/test/build_app_windows2/package.json
new file mode 100644
index 000000000..83caf82ab
--- /dev/null
+++ b/devtools/client/webide/test/build_app_windows2/package.json
@@ -0,0 +1,10 @@
+{
+ "webide": {
+ "prepackage": {
+ "command": "echo {\"name\":\"%NAME%\"} > manifest.webapp",
+ "cwd": "./stage",
+ "env": ["NAME=world"]
+ },
+ "packageDir": "./stage"
+ }
+}
diff --git a/devtools/client/webide/test/build_app_windows2/stage/empty-directory b/devtools/client/webide/test/build_app_windows2/stage/empty-directory
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/devtools/client/webide/test/build_app_windows2/stage/empty-directory
diff --git a/devtools/client/webide/test/chrome.ini b/devtools/client/webide/test/chrome.ini
new file mode 100644
index 000000000..b492ccd9b
--- /dev/null
+++ b/devtools/client/webide/test/chrome.ini
@@ -0,0 +1,71 @@
+[DEFAULT]
+tags = devtools
+support-files =
+ app/index.html
+ app/manifest.webapp
+ app.zip
+ addons/simulators.json
+ addons/fxos_1_0_simulator-linux.xpi
+ addons/fxos_1_0_simulator-linux64.xpi
+ addons/fxos_1_0_simulator-win32.xpi
+ addons/fxos_1_0_simulator-mac64.xpi
+ addons/fxos_2_0_simulator-linux.xpi
+ addons/fxos_2_0_simulator-linux64.xpi
+ addons/fxos_2_0_simulator-win32.xpi
+ addons/fxos_2_0_simulator-mac64.xpi
+ addons/fxos_3_0_simulator-linux.xpi
+ addons/fxos_3_0_simulator-linux64.xpi
+ addons/fxos_3_0_simulator-win32.xpi
+ addons/fxos_3_0_simulator-mac64.xpi
+ addons/fxos_3_0_tv_simulator-linux.xpi
+ addons/fxos_3_0_tv_simulator-linux64.xpi
+ addons/fxos_3_0_tv_simulator-win32.xpi
+ addons/fxos_3_0_tv_simulator-mac64.xpi
+ addons/adbhelper-linux.xpi
+ addons/adbhelper-linux64.xpi
+ addons/adbhelper-win32.xpi
+ addons/adbhelper-mac64.xpi
+ addons/fxdt-adapters-linux32.xpi
+ addons/fxdt-adapters-linux64.xpi
+ addons/fxdt-adapters-win32.xpi
+ addons/fxdt-adapters-mac64.xpi
+ build_app1/package.json
+ build_app2/manifest.webapp
+ build_app2/package.json
+ build_app2/stage/empty-directory
+ build_app_windows1/package.json
+ build_app_windows2/manifest.webapp
+ build_app_windows2/package.json
+ build_app_windows2/stage/empty-directory
+ device_front_shared.js
+ head.js
+ hosted_app.manifest
+ templates.json
+ ../../shared/test/browser_devices.json
+ validator/*
+
+[test_basic.html]
+[test_newapp.html]
+skip-if = (os == "win" && os_version == "10.0") # Bug 1197053
+[test_import.html]
+skip-if = (os == "linux") # Bug 1024734
+[test_duplicate_import.html]
+[test_runtime.html]
+[test_manifestUpdate.html]
+[test_addons.html]
+skip-if = true # Bug 1201392 - Update add-ons after migration
+[test_device_runtime.html]
+[test_device_permissions.html]
+[test_autoconnect_runtime.html]
+[test_autoselect_project.html]
+[test_telemetry.html]
+skip-if = true # Bug 1201392 - Update add-ons after migration
+[test_device_preferences.html]
+[test_device_settings.html]
+[test_fullscreenToolbox.html]
+[test_zoom.html]
+[test_build.html]
+[test_simulators.html]
+skip-if = true # Bug 1281138 - intermittent failures
+[test_toolbox.html]
+[test_app_validator.html]
diff --git a/devtools/client/webide/test/device_front_shared.js b/devtools/client/webide/test/device_front_shared.js
new file mode 100644
index 000000000..0ddb5df21
--- /dev/null
+++ b/devtools/client/webide/test/device_front_shared.js
@@ -0,0 +1,219 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var customName;
+var customValue;
+var customValueType;
+var customBtn;
+var newField;
+var change;
+var doc;
+var iframe;
+var resetBtn;
+var found = false;
+
+function setDocument(frame) {
+ iframe = frame;
+ doc = iframe.contentWindow.document;
+}
+
+function fieldChange(fields, id) {
+ // Trigger existing field change
+ for (let field of fields) {
+ if (field.id == id) {
+ let button = doc.getElementById("btn-" + id);
+ found = true;
+ ok(button.classList.contains("hide"), "Default field detected");
+ field.value = "custom";
+ field.click();
+ ok(!button.classList.contains("hide"), "Custom field detected");
+ break;
+ }
+ }
+ ok(found, "Found " + id + " line");
+}
+
+function addNewField() {
+ found = false;
+ customName = doc.querySelector("#custom-value-name");
+ customValue = doc.querySelector("#custom-value-text");
+ customValueType = doc.querySelector("#custom-value-type");
+ customBtn = doc.querySelector("#custom-value");
+ change = doc.createEvent("HTMLEvents");
+ change.initEvent("change", false, true);
+
+ // Add a new custom string
+ customValueType.value = "string";
+ customValueType.dispatchEvent(change);
+ customName.value = "new-string-field!";
+ customValue.value = "test";
+ customBtn.click();
+ let newField = doc.querySelector("#new-string-field");
+ if (newField) {
+ found = true;
+ is(newField.type, "text", "Custom type is a string");
+ is(newField.value, "test", "Custom string new value is correct");
+ }
+ ok(found, "Found new string field line");
+ is(customName.value, "", "Custom string name reset");
+ is(customValue.value, "", "Custom string value reset");
+}
+
+function addNewFieldWithEnter() {
+ // Add a new custom value with the <enter> key
+ found = false;
+ customName.value = "new-string-field-two";
+ customValue.value = "test";
+ let newAddField = doc.querySelector("#add-custom-field");
+ let enter = doc.createEvent("KeyboardEvent");
+ enter.initKeyEvent(
+ "keyup", true, true, null, false, false, false, false, 13, 0);
+ newAddField.dispatchEvent(enter);
+ newField = doc.querySelector("#new-string-field-two");
+ if (newField) {
+ found = true;
+ is(newField.type, "text", "Custom type is a string");
+ is(newField.value, "test", "Custom string new value is correct");
+ }
+ ok(found, "Found new string field line");
+ is(customName.value, "", "Custom string name reset");
+ is(customValue.value, "", "Custom string value reset");
+}
+
+function editExistingField() {
+ // Edit existing custom string preference
+ newField.value = "test2";
+ newField.click();
+ is(newField.value, "test2", "Custom string existing value is correct");
+}
+
+function addNewFieldInteger() {
+ // Add a new custom integer preference with a valid integer
+ customValueType.value = "number";
+ customValueType.dispatchEvent(change);
+ customName.value = "new-integer-field";
+ customValue.value = 1;
+ found = false;
+
+ customBtn.click();
+ newField = doc.querySelector("#new-integer-field");
+ if (newField) {
+ found = true;
+ is(newField.type, "number", "Custom type is a number");
+ is(newField.value, "1", "Custom integer value is correct");
+ }
+ ok(found, "Found new integer field line");
+ is(customName.value, "", "Custom integer name reset");
+ is(customValue.value, "", "Custom integer value reset");
+}
+
+var editFieldInteger = Task.async(function* () {
+ // Edit existing custom integer preference
+ newField.value = 3;
+ newField.click();
+ is(newField.value, "3", "Custom integer existing value is correct");
+
+ // Reset a custom field
+ let resetBtn = doc.querySelector("#btn-new-integer-field");
+ resetBtn.click();
+
+ try {
+ yield iframe.contentWindow.configView._defaultField;
+ } catch (err) {
+ let fieldRow = doc.querySelector("#row-new-integer-field");
+ if (!fieldRow) {
+ found = false;
+ }
+ ok(!found, "Custom field removed");
+ }
+});
+
+var resetExistingField = Task.async(function* (id) {
+ let existing = doc.getElementById(id);
+ existing.click();
+ is(existing.checked, true, "Existing boolean value is correct");
+ resetBtn = doc.getElementById("btn-" + id);
+ resetBtn.click();
+
+ yield iframe.contentWindow.configView._defaultField;
+
+ ok(resetBtn.classList.contains("hide"), true, "Reset button hidden");
+ is(existing.checked, true, "Existing field reset");
+});
+
+var resetNewField = Task.async(function* (id) {
+ let custom = doc.getElementById(id);
+ custom.click();
+ is(custom.value, "test", "New string value is correct");
+ resetBtn = doc.getElementById("btn-" + id);
+ resetBtn.click();
+
+ yield iframe.contentWindow.configView._defaultField;
+
+ ok(resetBtn.classList.contains("hide"), true, "Reset button hidden");
+});
+
+function addNewFieldBoolean() {
+ customValueType.value = "boolean";
+ customValueType.dispatchEvent(change);
+ customName.value = "new-boolean-field";
+ customValue.checked = true;
+ found = false;
+ customBtn.click();
+ newField = doc.querySelector("#new-boolean-field");
+ if (newField) {
+ found = true;
+ is(newField.type, "checkbox", "Custom type is a checkbox");
+ is(newField.checked, true, "Custom boolean value is correctly true");
+ }
+ ok(found, "Found new boolean field line");
+
+ // Mouse event trigger
+ var mouseClick = new MouseEvent("click", {
+ canBubble: true,
+ cancelable: true,
+ view: doc.parent,
+ });
+
+ found = false;
+ customValueType.value = "boolean";
+ customValueType.dispatchEvent(change);
+ customName.value = "new-boolean-field2";
+ customValue.dispatchEvent(mouseClick);
+ customBtn.dispatchEvent(mouseClick);
+ newField = doc.querySelector("#new-boolean-field2");
+ if (newField) {
+ found = true;
+ is(newField.checked, true, "Custom boolean value is correctly false");
+ }
+ ok(found, "Found new second boolean field line");
+
+ is(customName.value, "", "Custom boolean name reset");
+ is(customValue.checked, false, "Custom boolean value reset");
+
+ newField.click();
+ is(newField.checked, false, "Custom boolean existing value is correct");
+}
+
+function searchFields(deck, keyword) {
+ // Search for a non-existent field
+ let searchField = doc.querySelector("#search-bar");
+ searchField.value = "![o_O]!";
+ searchField.click();
+
+ let fieldsTotal = doc.querySelectorAll("tr.edit-row").length;
+ let hiddenFields = doc.querySelectorAll("tr.hide");
+ is(hiddenFields.length, fieldsTotal, "Search keyword not found");
+
+ // Search for existing fields
+ searchField.value = keyword;
+ searchField.click();
+ hiddenFields = doc.querySelectorAll("tr.hide");
+ isnot(hiddenFields.length, fieldsTotal, "Search keyword found");
+
+ doc.querySelector("#close").click();
+
+ ok(!deck.selectedPanel, "No panel selected");
+}
diff --git a/devtools/client/webide/test/doc_tabs.html b/devtools/client/webide/test/doc_tabs.html
new file mode 100644
index 000000000..4901289fc
--- /dev/null
+++ b/devtools/client/webide/test/doc_tabs.html
@@ -0,0 +1,15 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Test Tab</title>
+ </head>
+
+ <body>
+ Test Tab
+ </body>
+
+</html>
diff --git a/devtools/client/webide/test/head.js b/devtools/client/webide/test/head.js
new file mode 100644
index 000000000..c0171c730
--- /dev/null
+++ b/devtools/client/webide/test/head.js
@@ -0,0 +1,248 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var {utils: Cu, classes: Cc, interfaces: Ci} = Components;
+
+const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const { FileUtils } = require("resource://gre/modules/FileUtils.jsm");
+const { gDevTools } = require("devtools/client/framework/devtools");
+const promise = require("promise");
+const Services = require("Services");
+const { Task } = require("devtools/shared/task");
+const { AppProjects } = require("devtools/client/webide/modules/app-projects");
+const DevToolsUtils = require("devtools/shared/DevToolsUtils");
+const { DebuggerServer } = require("devtools/server/main");
+const flags = require("devtools/shared/flags");
+flags.testing = true;
+
+var TEST_BASE;
+if (window.location === "chrome://browser/content/browser.xul") {
+ TEST_BASE = "chrome://mochitests/content/browser/devtools/client/webide/test/";
+} else {
+ TEST_BASE = "chrome://mochitests/content/chrome/devtools/client/webide/test/";
+}
+
+Services.prefs.setBoolPref("devtools.webide.enabled", true);
+Services.prefs.setBoolPref("devtools.webide.enableLocalRuntime", true);
+
+Services.prefs.setCharPref("devtools.webide.addonsURL", TEST_BASE + "addons/simulators.json");
+Services.prefs.setCharPref("devtools.webide.simulatorAddonsURL", TEST_BASE + "addons/fxos_#SLASHED_VERSION#_simulator-#OS#.xpi");
+Services.prefs.setCharPref("devtools.webide.adbAddonURL", TEST_BASE + "addons/adbhelper-#OS#.xpi");
+Services.prefs.setCharPref("devtools.webide.adaptersAddonURL", TEST_BASE + "addons/fxdt-adapters-#OS#.xpi");
+Services.prefs.setCharPref("devtools.webide.templatesURL", TEST_BASE + "templates.json");
+Services.prefs.setCharPref("devtools.devices.url", TEST_BASE + "browser_devices.json");
+
+var registerCleanupFunction = registerCleanupFunction ||
+ SimpleTest.registerCleanupFunction;
+registerCleanupFunction(() => {
+ flags.testing = false;
+ Services.prefs.clearUserPref("devtools.webide.enabled");
+ Services.prefs.clearUserPref("devtools.webide.enableLocalRuntime");
+ Services.prefs.clearUserPref("devtools.webide.autoinstallADBHelper");
+ Services.prefs.clearUserPref("devtools.webide.autoinstallFxdtAdapters");
+ Services.prefs.clearUserPref("devtools.webide.busyTimeout");
+ Services.prefs.clearUserPref("devtools.webide.lastSelectedProject");
+ Services.prefs.clearUserPref("devtools.webide.lastConnectedRuntime");
+});
+
+var openWebIDE = Task.async(function* (autoInstallAddons) {
+ info("opening WebIDE");
+
+ Services.prefs.setBoolPref("devtools.webide.autoinstallADBHelper", !!autoInstallAddons);
+ Services.prefs.setBoolPref("devtools.webide.autoinstallFxdtAdapters", !!autoInstallAddons);
+
+ let ww = Cc["@mozilla.org/embedcomp/window-watcher;1"].getService(Ci.nsIWindowWatcher);
+ let win = ww.openWindow(null, "chrome://webide/content/", "webide", "chrome,centerscreen,resizable", null);
+
+ yield new Promise(resolve => {
+ win.addEventListener("load", function onLoad() {
+ win.removeEventListener("load", onLoad);
+ SimpleTest.requestCompleteLog();
+ SimpleTest.executeSoon(resolve);
+ });
+ });
+
+ info("WebIDE open");
+
+ return win;
+});
+
+function closeWebIDE(win) {
+ info("Closing WebIDE");
+
+ let deferred = promise.defer();
+
+ Services.prefs.clearUserPref("devtools.webide.widget.enabled");
+
+ win.addEventListener("unload", function onUnload() {
+ win.removeEventListener("unload", onUnload);
+ info("WebIDE closed");
+ SimpleTest.executeSoon(() => {
+ deferred.resolve();
+ });
+ });
+
+ win.close();
+
+ return deferred.promise;
+}
+
+function removeAllProjects() {
+ return Task.spawn(function* () {
+ yield AppProjects.load();
+ // use a new array so we're not iterating over the same
+ // underlying array that's being modified by AppProjects
+ let projects = AppProjects.projects.map(p => p.location);
+ for (let i = 0; i < projects.length; i++) {
+ yield AppProjects.remove(projects[i]);
+ }
+ });
+}
+
+function nextTick() {
+ let deferred = promise.defer();
+ SimpleTest.executeSoon(() => {
+ deferred.resolve();
+ });
+
+ return deferred.promise;
+}
+
+function waitForUpdate(win, update) {
+ info("Wait: " + update);
+ let deferred = promise.defer();
+ win.AppManager.on("app-manager-update", function onUpdate(e, what) {
+ info("Got: " + what);
+ if (what !== update) {
+ return;
+ }
+ win.AppManager.off("app-manager-update", onUpdate);
+ deferred.resolve(win.UI._updatePromise);
+ });
+ return deferred.promise;
+}
+
+function waitForTime(time) {
+ let deferred = promise.defer();
+ setTimeout(() => {
+ deferred.resolve();
+ }, time);
+ return deferred.promise;
+}
+
+function documentIsLoaded(doc) {
+ let deferred = promise.defer();
+ if (doc.readyState == "complete") {
+ deferred.resolve();
+ } else {
+ doc.addEventListener("readystatechange", function onChange() {
+ if (doc.readyState == "complete") {
+ doc.removeEventListener("readystatechange", onChange);
+ deferred.resolve();
+ }
+ });
+ }
+ return deferred.promise;
+}
+
+function lazyIframeIsLoaded(iframe) {
+ let deferred = promise.defer();
+ iframe.addEventListener("load", function onLoad() {
+ iframe.removeEventListener("load", onLoad, true);
+ deferred.resolve(nextTick());
+ }, true);
+ return deferred.promise;
+}
+
+function addTab(aUrl, aWindow) {
+ info("Adding tab: " + aUrl);
+
+ let deferred = promise.defer();
+ let targetWindow = aWindow || window;
+ let targetBrowser = targetWindow.gBrowser;
+
+ targetWindow.focus();
+ let tab = targetBrowser.selectedTab = targetBrowser.addTab(aUrl);
+ let linkedBrowser = tab.linkedBrowser;
+
+ BrowserTestUtils.browserLoaded(linkedBrowser).then(function () {
+ info("Tab added and finished loading: " + aUrl);
+ deferred.resolve(tab);
+ });
+
+ return deferred.promise;
+}
+
+function removeTab(aTab, aWindow) {
+ info("Removing tab.");
+
+ let deferred = promise.defer();
+ let targetWindow = aWindow || window;
+ let targetBrowser = targetWindow.gBrowser;
+ let tabContainer = targetBrowser.tabContainer;
+
+ tabContainer.addEventListener("TabClose", function onClose(aEvent) {
+ tabContainer.removeEventListener("TabClose", onClose, false);
+ info("Tab removed and finished closing.");
+ deferred.resolve();
+ }, false);
+
+ targetBrowser.removeTab(aTab);
+ return deferred.promise;
+}
+
+function getRuntimeDocument(win) {
+ return win.document.querySelector("#runtime-listing-panel-details").contentDocument;
+}
+
+function getProjectDocument(win) {
+ return win.document.querySelector("#project-listing-panel-details").contentDocument;
+}
+
+function getRuntimeWindow(win) {
+ return win.document.querySelector("#runtime-listing-panel-details").contentWindow;
+}
+
+function getProjectWindow(win) {
+ return win.document.querySelector("#project-listing-panel-details").contentWindow;
+}
+
+function connectToLocalRuntime(win) {
+ info("Loading local runtime.");
+
+ let panelNode;
+ let runtimePanel;
+
+ runtimePanel = getRuntimeDocument(win);
+
+ panelNode = runtimePanel.querySelector("#runtime-panel");
+ let items = panelNode.querySelectorAll(".runtime-panel-item-other");
+ is(items.length, 2, "Found 2 custom runtime buttons");
+
+ let updated = waitForUpdate(win, "runtime-global-actors");
+ items[1].click();
+ return updated;
+}
+
+function handleError(aError) {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ finish();
+}
+
+function waitForConnectionChange(expectedState, count = 1) {
+ return new Promise(resolve => {
+ let onConnectionChange = (_, state) => {
+ if (state != expectedState) {
+ return;
+ }
+ if (--count != 0) {
+ return;
+ }
+ DebuggerServer.off("connectionchange", onConnectionChange);
+ resolve();
+ };
+ DebuggerServer.on("connectionchange", onConnectionChange);
+ });
+}
diff --git a/devtools/client/webide/test/hosted_app.manifest b/devtools/client/webide/test/hosted_app.manifest
new file mode 100644
index 000000000..ab5069978
--- /dev/null
+++ b/devtools/client/webide/test/hosted_app.manifest
@@ -0,0 +1,3 @@
+{
+ "name": "hosted manifest name property"
+}
diff --git a/devtools/client/webide/test/templates.json b/devtools/client/webide/test/templates.json
new file mode 100644
index 000000000..e6ffa3efe
--- /dev/null
+++ b/devtools/client/webide/test/templates.json
@@ -0,0 +1,14 @@
+[
+ {
+ "file": "chrome://mochitests/content/chrome/devtools/client/webide/test/app.zip?1",
+ "icon": "ximgx1",
+ "name": "app name 1",
+ "description": "app description 1"
+ },
+ {
+ "file": "chrome://mochitests/content/chrome/devtools/client/webide/test/app.zip?2",
+ "icon": "ximgx2",
+ "name": "app name 2",
+ "description": "app description 2"
+ }
+]
diff --git a/devtools/client/webide/test/test_addons.html b/devtools/client/webide/test/test_addons.html
new file mode 100644
index 000000000..5a1bc7504
--- /dev/null
+++ b/devtools/client/webide/test/test_addons.html
@@ -0,0 +1,176 @@
+<!DOCTYPE html>
+
+<html>
+
+ <head>
+ <meta charset="utf8">
+ <title></title>
+
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js"></script>
+ <script type="application/javascript;version=1.8" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ </head>
+
+ <body>
+
+ <script type="application/javascript;version=1.8">
+ window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+
+ const {GetAvailableAddons} = require("devtools/client/webide/modules/addons");
+ const {Devices} = Cu.import("resource://devtools/shared/apps/Devices.jsm");
+ const {Simulators} = require("devtools/client/webide/modules/simulators");
+
+ let adbAddonsInstalled = promise.defer();
+ Devices.on("addon-status-updated", function onUpdate1() {
+ Devices.off("addon-status-updated", onUpdate1);
+ adbAddonsInstalled.resolve();
+ });
+
+ function getVersion(name) {
+ return name.match(/(\d+\.\d+)/)[0];
+ }
+
+ function onSimulatorInstalled(name) {
+ let deferred = promise.defer();
+ Simulators.on("updated", function onUpdate() {
+ Simulators.findSimulatorAddons().then(addons => {
+ for (let addon of addons) {
+ if (name == addon.name.replace(" Simulator", "")) {
+ Simulators.off("updated", onUpdate);
+ nextTick().then(deferred.resolve);
+ return;
+ }
+ }
+ });
+ });
+ return deferred.promise;
+ }
+
+ function installSimulatorFromUI(doc, name) {
+ let li = doc.querySelector('[addon="simulator-' + getVersion(name) + '"]');
+ li.querySelector(".install-button").click();
+ return onSimulatorInstalled(name);
+ }
+
+ function uninstallSimulatorFromUI(doc, name) {
+ let deferred = promise.defer();
+ Simulators.on("updated", function onUpdate() {
+ nextTick().then(() => {
+ let li = doc.querySelector('[status="uninstalled"][addon="simulator-' + getVersion(name) + '"]');
+ if (li) {
+ Simulators.off("updated", onUpdate);
+ deferred.resolve();
+ } else {
+ deferred.reject("Can't find item");
+ }
+ });
+ });
+ let li = doc.querySelector('[status="installed"][addon="simulator-' + getVersion(name) + '"]');
+ li.querySelector(".uninstall-button").click();
+ return deferred.promise;
+ }
+
+ function uninstallADBFromUI(doc) {
+ let deferred = promise.defer();
+ Devices.on("addon-status-updated", function onUpdate() {
+ nextTick().then(() => {
+ let li = doc.querySelector('[status="uninstalled"][addon="adb"]');
+ if (li) {
+ Devices.off("addon-status-updated", onUpdate);
+ deferred.resolve();
+ } else {
+ deferred.reject("Can't find item");
+ }
+ })
+ });
+ let li = doc.querySelector('[status="installed"][addon="adb"]');
+ li.querySelector(".uninstall-button").click();
+ return deferred.promise;
+ }
+
+ Task.spawn(function*() {
+
+ ok(!Devices.helperAddonInstalled, "Helper not installed");
+
+ let win = yield openWebIDE(true);
+ let docRuntime = getRuntimeDocument(win);
+
+ yield adbAddonsInstalled.promise;
+
+ ok(Devices.helperAddonInstalled, "Helper has been auto-installed");
+
+ yield nextTick();
+
+ let addons = yield GetAvailableAddons();
+
+ is(addons.simulators.length, 3, "3 simulator addons to install");
+
+ let sim10 = addons.simulators.filter(a => a.version == "1.0")[0];
+ sim10.install();
+
+ yield onSimulatorInstalled("Firefox OS 1.0");
+
+ win.Cmds.showAddons();
+
+ let frame = win.document.querySelector("#deck-panel-addons");
+ let addonDoc = frame.contentWindow.document;
+ let lis;
+
+ lis = addonDoc.querySelectorAll("li");
+ is(lis.length, 5, "5 addons listed");
+
+ lis = addonDoc.querySelectorAll('li[status="installed"]');
+ is(lis.length, 3, "3 addons installed");
+
+ lis = addonDoc.querySelectorAll('li[status="uninstalled"]');
+ is(lis.length, 2, "2 addons uninstalled");
+
+ info("Uninstalling Simulator 2.0");
+
+ yield installSimulatorFromUI(addonDoc, "Firefox OS 2.0");
+
+ info("Uninstalling Simulator 3.0");
+
+ yield installSimulatorFromUI(addonDoc, "Firefox OS 3.0");
+
+ yield nextTick();
+
+ let panelNode = docRuntime.querySelector("#runtime-panel");
+ let items;
+
+ items = panelNode.querySelectorAll(".runtime-panel-item-usb");
+ is(items.length, 1, "Found one runtime button");
+
+ items = panelNode.querySelectorAll(".runtime-panel-item-simulator");
+ is(items.length, 3, "Found 3 simulators button");
+
+ yield uninstallSimulatorFromUI(addonDoc, "Firefox OS 1.0");
+ yield uninstallSimulatorFromUI(addonDoc, "Firefox OS 2.0");
+ yield uninstallSimulatorFromUI(addonDoc, "Firefox OS 3.0");
+
+ items = panelNode.querySelectorAll(".runtime-panel-item-simulator");
+ is(items.length, 0, "No simulator listed");
+
+ let w = addonDoc.querySelector(".warning");
+ let display = addonDoc.defaultView.getComputedStyle(w).display
+ is(display, "none", "Warning about missing ADB hidden");
+
+ yield uninstallADBFromUI(addonDoc, "adb");
+
+ items = panelNode.querySelectorAll(".runtime-panel-item-usb");
+ is(items.length, 0, "No usb runtime listed");
+
+ display = addonDoc.defaultView.getComputedStyle(w).display
+ is(display, "block", "Warning about missing ADB present");
+
+ yield closeWebIDE(win);
+
+ SimpleTest.finish();
+
+ });
+ }
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/webide/test/test_app_validator.html b/devtools/client/webide/test/test_app_validator.html
new file mode 100644
index 000000000..60ed29aac
--- /dev/null
+++ b/devtools/client/webide/test/test_app_validator.html
@@ -0,0 +1,205 @@
+<!DOCTYPE html>
+
+<html>
+
+ <head>
+ <meta charset="utf8">
+ <title></title>
+
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ </head>
+
+ <body>
+
+ <script type="application/javascript;version=1.8">
+ const Cu = Components.utils;
+ const Cc = Components.classes;
+ const Ci = Components.interfaces;
+ Cu.import("resource://testing-common/httpd.js");
+ const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+
+ const {AppValidator} = require("devtools/client/webide/modules/app-validator");
+ const Services = require("Services");
+ const nsFile = Components.Constructor("@mozilla.org/file/local;1",
+ "nsILocalFile", "initWithPath");
+ const cr = Cc["@mozilla.org/chrome/chrome-registry;1"]
+ .getService(Ci.nsIChromeRegistry);
+ const strings = Services.strings.createBundle("chrome://devtools/locale/app-manager.properties");
+ let httpserver, origin;
+
+ window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+
+ httpserver = new HttpServer();
+ httpserver.start(-1);
+ origin = "http://localhost:" + httpserver.identity.primaryPort + "/";
+
+ next();
+ }
+
+ function createHosted(path, manifestFile="/manifest.webapp") {
+ let dirPath = getTestFilePath("validator/" + path);
+ httpserver.registerDirectory("/", nsFile(dirPath));
+ return new AppValidator({
+ type: "hosted",
+ location: origin + manifestFile
+ });
+ }
+
+ function createPackaged(path) {
+ let dirPath = getTestFilePath("validator/" + path);
+ return new AppValidator({
+ type: "packaged",
+ location: dirPath
+ });
+ }
+
+ function next() {
+ let test = tests.shift();
+ if (test) {
+ try {
+ test();
+ } catch(e) {
+ console.error("exception", String(e), e, e.stack);
+ }
+ } else {
+ httpserver.stop(function() {
+ SimpleTest.finish();
+ });
+ }
+ }
+
+ let tests = [
+ // Test a 100% valid example
+ function () {
+ let validator = createHosted("valid");
+ validator.validate().then(() => {
+ is(validator.errors.length, 0, "valid app got no error");
+ is(validator.warnings.length, 0, "valid app got no warning");
+
+ next();
+ });
+ },
+
+ function () {
+ let validator = createPackaged("valid");
+ validator.validate().then(() => {
+ is(validator.errors.length, 0, "valid packaged app got no error");
+ is(validator.warnings.length, 0, "valid packaged app got no warning");
+
+ next();
+ });
+ },
+
+ // Test a launch path that returns a 404
+ function () {
+ let validator = createHosted("wrong-launch-path");
+ validator.validate().then(() => {
+ is(validator.errors.length, 1, "app with non-existant launch path got an error");
+ is(validator.errors[0], strings.formatStringFromName("validator.accessFailedLaunchPathBadHttpCode", [origin + "wrong-path.html", 404], 2),
+ "with the right error message");
+ is(validator.warnings.length, 0, "but no warning");
+ next();
+ });
+ },
+ function () {
+ let validator = createPackaged("wrong-launch-path");
+ validator.validate().then(() => {
+ is(validator.errors.length, 1, "app with wrong path got an error");
+ let file = nsFile(validator.location);
+ file.append("wrong-path.html");
+ let url = Services.io.newFileURI(file);
+ is(validator.errors[0], strings.formatStringFromName("validator.accessFailedLaunchPath", [url.spec], 1),
+ "with the expected message");
+ is(validator.warnings.length, 0, "but no warning");
+
+ next();
+ });
+ },
+
+ // Test when using a non-absolute path for launch_path
+ function () {
+ let validator = createHosted("non-absolute-path");
+ validator.validate().then(() => {
+ is(validator.errors.length, 1, "app with non absolute path got an error");
+ is(validator.errors[0], strings.formatStringFromName("validator.nonAbsoluteLaunchPath", ["non-absolute.html"], 1),
+ "with expected message");
+ is(validator.warnings.length, 0, "but no warning");
+ next();
+ });
+ },
+ function () {
+ let validator = createPackaged("non-absolute-path");
+ validator.validate().then(() => {
+ is(validator.errors.length, 1, "app with non absolute path got an error");
+ is(validator.errors[0], strings.formatStringFromName("validator.nonAbsoluteLaunchPath", ["non-absolute.html"], 1),
+ "with expected message");
+ is(validator.warnings.length, 0, "but no warning");
+ next();
+ });
+ },
+
+ // Test multiple failures (missing name [error] and icon [warning])
+ function () {
+ let validator = createHosted("no-name-or-icon");
+ validator.validate().then(() => {
+ checkNoNameOrIcon(validator);
+ });
+ },
+ function () {
+ let validator = createPackaged("no-name-or-icon");
+ validator.validate().then(() => {
+ checkNoNameOrIcon(validator);
+ });
+ },
+
+ // Test a regular URL instead of a direct link to the manifest
+ function () {
+ let validator = createHosted("valid", "/");
+ validator.validate().then(() => {
+ is(validator.warnings.length, 0, "manifest found got no warning");
+ is(validator.errors.length, 0, "manifest found got no error");
+
+ next();
+ });
+ },
+
+ // Test finding a manifest at origin's root
+ function () {
+ let validator = createHosted("valid", "/unexisting-dir");
+ validator.validate().then(() => {
+ is(validator.warnings.length, 0, "manifest found at origin root got no warning");
+ is(validator.errors.length, 0, "manifest found at origin root got no error");
+
+ next();
+ });
+ },
+
+ // Test priorization of manifest.webapp at provided location instead of a manifest located at origin's root
+ function() {
+ let validator = createHosted("valid", "/alsoValid");
+ validator.validate().then(() => {
+ is(validator.manifest.name, "valid at subfolder", "manifest at subfolder was used");
+
+ next();
+ });
+ }
+ ];
+
+ function checkNoNameOrIcon(validator) {
+ is(validator.errors.length, 1, "app with no name has an error");
+ is(validator.errors[0],
+ strings.GetStringFromName("validator.missNameManifestProperty"),
+ "with expected message");
+ is(validator.warnings.length, 1, "app with no icon has a warning");
+ is(validator.warnings[0],
+ strings.GetStringFromName("validator.missIconsManifestProperty"),
+ "with expected message");
+ next();
+ }
+
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/webide/test/test_autoconnect_runtime.html b/devtools/client/webide/test/test_autoconnect_runtime.html
new file mode 100644
index 000000000..3de00473a
--- /dev/null
+++ b/devtools/client/webide/test/test_autoconnect_runtime.html
@@ -0,0 +1,94 @@
+<!DOCTYPE html>
+
+<html>
+
+ <head>
+ <meta charset="utf8">
+ <title></title>
+
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js"></script>
+ <script type="application/javascript;version=1.8" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ </head>
+
+ <body>
+
+ <script type="application/javascript;version=1.8">
+ window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+
+ Task.spawn(function*() {
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+
+ let win = yield openWebIDE();
+ let docRuntime = getRuntimeDocument(win);
+
+ let fakeRuntime = {
+ type: "USB",
+ connect: function(connection) {
+ is(connection, win.AppManager.connection, "connection is valid");
+ connection.host = null; // force connectPipe
+ connection.connect();
+ return promise.resolve();
+ },
+
+ get id() {
+ return "fakeRuntime";
+ },
+
+ get name() {
+ return "fakeRuntime";
+ }
+ };
+ win.AppManager.runtimeList.usb.push(fakeRuntime);
+ win.AppManager.update("runtime-list");
+
+ let panelNode = docRuntime.querySelector("#runtime-panel");
+ let items = panelNode.querySelectorAll(".runtime-panel-item-usb");
+ is(items.length, 1, "Found one runtime button");
+
+ let connectionsChanged = waitForConnectionChange("opened", 2);
+ items[0].click();
+
+ ok(win.document.querySelector("window").className, "busy", "UI is busy");
+ yield win.UI._busyPromise;
+
+ yield connectionsChanged;
+ is(Object.keys(DebuggerServer._connections).length, 2, "Connected");
+
+ connectionsChanged = waitForConnectionChange("closed", 2);
+
+ yield nextTick();
+ yield closeWebIDE(win);
+
+ yield connectionsChanged;
+ is(Object.keys(DebuggerServer._connections).length, 0, "Disconnected");
+
+ connectionsChanged = waitForConnectionChange("opened", 2);
+
+ win = yield openWebIDE();
+
+ win.AppManager.runtimeList.usb.push(fakeRuntime);
+ win.AppManager.update("runtime-list");
+
+ yield waitForUpdate(win, "runtime-targets");
+
+ yield connectionsChanged;
+ is(Object.keys(DebuggerServer._connections).length, 2, "Automatically reconnected");
+
+ yield win.Cmds.disconnectRuntime();
+
+ yield closeWebIDE(win);
+
+ DebuggerServer.destroy();
+
+ SimpleTest.finish();
+ });
+ }
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/webide/test/test_autoselect_project.html b/devtools/client/webide/test/test_autoselect_project.html
new file mode 100644
index 000000000..cd5793559
--- /dev/null
+++ b/devtools/client/webide/test/test_autoselect_project.html
@@ -0,0 +1,110 @@
+<!DOCTYPE html>
+
+<html>
+
+ <head>
+ <meta charset="utf8">
+ <title></title>
+
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js"></script>
+ <script type="application/javascript;version=1.8" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ </head>
+
+ <body>
+
+ <script type="application/javascript;version=1.8">
+ window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+
+ Task.spawn(function* () {
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+
+ let win = yield openWebIDE();
+ let docRuntime = getRuntimeDocument(win);
+ let docProject = getProjectDocument(win);
+
+ let panelNode = docRuntime.querySelector("#runtime-panel");
+ let items = panelNode.querySelectorAll(".runtime-panel-item-other");
+ is(items.length, 2, "Found 2 runtime buttons");
+
+ // Connect to local runtime
+ let connectionsChanged = waitForConnectionChange("opened", 2);
+ items[1].click();
+
+ yield waitForUpdate(win, "runtime-targets");
+
+ yield connectionsChanged;
+ is(Object.keys(DebuggerServer._connections).length, 2, "Locally connected");
+
+ ok(win.AppManager.isMainProcessDebuggable(), "Main process available");
+
+ // Select main process
+ yield win.Cmds.showProjectPanel();
+ yield waitForUpdate(win, "runtime-targets");
+ SimpleTest.executeSoon(() => {
+ docProject.querySelectorAll("#project-panel-runtimeapps .panel-item")[0].click();
+ });
+
+ yield waitForUpdate(win, "project");
+
+ let lastProject = Services.prefs.getCharPref("devtools.webide.lastSelectedProject");
+ is(lastProject, "mainProcess:", "Last project is main process");
+
+ connectionsChanged = waitForConnectionChange("closed", 2);
+
+ yield nextTick();
+ yield closeWebIDE(win);
+
+ yield connectionsChanged;
+ is(Object.keys(DebuggerServer._connections).length, 0, "Disconnected");
+
+ connectionsChanged = waitForConnectionChange("opened", 2);
+
+ // Re-open, should reselect main process after connection
+ win = yield openWebIDE();
+
+ docRuntime = getRuntimeDocument(win);
+
+ panelNode = docRuntime.querySelector("#runtime-panel");
+ items = panelNode.querySelectorAll(".runtime-panel-item-other");
+ is(items.length, 2, "Found 2 runtime buttons");
+
+ // Connect to local runtime
+ items[1].click();
+
+ yield waitForUpdate(win, "runtime-targets");
+
+ yield connectionsChanged;
+ is(Object.keys(DebuggerServer._connections).length, 2, "Locally connected");
+ ok(win.AppManager.isMainProcessDebuggable(), "Main process available");
+ is(win.AppManager.selectedProject.type, "mainProcess", "Main process reselected");
+
+ // Wait for the toolbox to be fully loaded
+ yield win.UI.toolboxPromise;
+
+ // If we happen to pass a project object targeting the same context,
+ // here, the main process, the `selectedProject` attribute shouldn't be updated
+ // so that no `project` event would fire.
+ let oldProject = win.AppManager.selectedProject;
+ win.AppManager.selectedProject = {
+ type: "mainProcess"
+ };
+ is(win.AppManager.selectedProject, oldProject, "AppManager.selectedProject shouldn't be updated if we selected the same project");
+
+ yield win.Cmds.disconnectRuntime();
+
+ yield closeWebIDE(win);
+
+ DebuggerServer.destroy();
+
+ SimpleTest.finish();
+ });
+ }
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/webide/test/test_basic.html b/devtools/client/webide/test/test_basic.html
new file mode 100644
index 000000000..e619a0f06
--- /dev/null
+++ b/devtools/client/webide/test/test_basic.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+
+<html>
+
+ <head>
+ <meta charset="utf8">
+ <title></title>
+
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js"></script>
+ <script type="application/javascript;version=1.8" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ </head>
+
+ <body>
+
+ <script type="application/javascript;version=1.8">
+ window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+
+ Task.spawn(function* () {
+ let win = yield openWebIDE();
+
+ const {gDevToolsBrowser} = require("devtools/client/framework/devtools-browser");
+ yield gDevToolsBrowser.isWebIDEInitialized.promise;
+ ok(true, "WebIDE was initialized");
+
+ ok(win, "Found a window");
+ ok(win.AppManager, "App Manager accessible");
+ let appmgr = win.AppManager;
+ ok(appmgr.connection, "App Manager connection ready");
+ ok(appmgr.runtimeList, "Runtime list ready");
+
+ // test error reporting
+ let nbox = win.document.querySelector("#notificationbox");
+ let notification = nbox.getNotificationWithValue("webide:errornotification");
+ ok(!notification, "No notification yet");
+ let deferred = promise.defer();
+ nextTick().then(() => {
+ deferred.reject("BOOM!");
+ });
+ try {
+ yield win.UI.busyUntil(deferred.promise, "xx");
+ } catch(e) {/* This *will* fail */}
+ notification = nbox.getNotificationWithValue("webide:errornotification");
+ ok(notification, "Error has been reported");
+
+ yield closeWebIDE(win);
+
+ SimpleTest.finish();
+ });
+ }
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/webide/test/test_build.html b/devtools/client/webide/test/test_build.html
new file mode 100644
index 000000000..ffb01998c
--- /dev/null
+++ b/devtools/client/webide/test/test_build.html
@@ -0,0 +1,128 @@
+<!DOCTYPE html>
+
+<html>
+
+ <head>
+ <meta charset="utf8">
+ <title></title>
+
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js"></script>
+ <script type="application/javascript;version=1.8" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ </head>
+
+ <body>
+
+ <script type="application/javascript;version=1.8">
+ window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+
+ let {TextDecoder, OS} = Cu.import("resource://gre/modules/osfile.jsm", {});
+ let {ProjectBuilding} = require("devtools/client/webide/modules/build");
+
+ Task.spawn(function* () {
+ let win = yield openWebIDE();
+ let winProject = getProjectWindow(win);
+ let AppManager = win.AppManager;
+
+ function isProjectMarkedAsValid() {
+ let details = win.frames[0];
+ return !details.document.body.classList.contains("error");
+ }
+
+ // # Test first package.json like this: `{webide: {prepackage: "command line string"}}`
+ let platform = Services.appShell.hiddenDOMWindow.navigator.platform;
+ let testSuffix = "";
+ if (platform.indexOf("Win") != -1) {
+ testSuffix = "_windows";
+ }
+
+ let packagedAppLocation = getTestFilePath("build_app" + testSuffix + "1");
+
+ let onValidated = waitForUpdate(win, "project-validated");
+ let onDetails = waitForUpdate(win, "details");
+ yield winProject.projectList.importPackagedApp(packagedAppLocation);
+ yield onValidated;
+ yield onDetails;
+
+ let project = win.AppManager.selectedProject;
+
+ ok(!project.manifest, "manifest includes name");
+ is(project.name, "--", "Display name uses manifest name");
+
+ let loggedMessages = [];
+ let logger = function (msg) {
+ loggedMessages.push(msg);
+ }
+
+ yield ProjectBuilding.build({
+ project,
+ logger
+ });
+ let packageDir = yield ProjectBuilding.getPackageDir(project);
+ is(packageDir, packagedAppLocation, "no custom packagedir");
+ is(loggedMessages[0], "start", "log messages are correct");
+ ok(loggedMessages[1].indexOf("Running pre-package hook") != -1, "log messages are correct");
+ is(loggedMessages[2], "Terminated with error code: 0", "log messages are correct");
+ is(loggedMessages[3], "succeed", "log messages are correct");
+
+ // Trigger validation
+ yield AppManager.validateAndUpdateProject(AppManager.selectedProject);
+ yield nextTick();
+
+ ok("name" in project.manifest, "manifest includes name");
+ is(project.name, "hello", "Display name uses manifest name");
+ is(project.manifest.name, project.name, "Display name uses manifest name");
+
+ yield OS.File.remove(OS.Path.join(packagedAppLocation, "manifest.webapp"));
+
+ // # Now test a full featured package.json
+ packagedAppLocation = getTestFilePath("build_app" + testSuffix + "2");
+
+ onValidated = waitForUpdate(win, "project-validated");
+ onDetails = waitForUpdate(win, "details");
+ yield winProject.projectList.importPackagedApp(packagedAppLocation);
+ yield onValidated;
+ yield onDetails;
+
+ project = win.AppManager.selectedProject;
+
+ loggedMessages = [];
+ yield ProjectBuilding.build({
+ project,
+ logger
+ });
+ packageDir = yield ProjectBuilding.getPackageDir(project);
+ is(OS.Path.normalize(packageDir),
+ OS.Path.join(packagedAppLocation, "stage"), "custom packagedir");
+ is(loggedMessages[0], "start", "log messages are correct");
+ ok(loggedMessages[1].indexOf("Running pre-package hook") != -1, "log messages are correct");
+ is(loggedMessages[2], "Terminated with error code: 0", "log messages are correct");
+ is(loggedMessages[3], "succeed", "log messages are correct");
+
+ // Switch to the package dir in order to verify the generated webapp.manifest
+ onValidated = waitForUpdate(win, "project-validated");
+ onDetails = waitForUpdate(win, "details");
+ yield winProject.projectList.importPackagedApp(packageDir);
+ yield onValidated;
+ yield onDetails;
+
+ project = win.AppManager.selectedProject;
+
+ ok("name" in project.manifest, "manifest includes name");
+ is(project.name, "world", "Display name uses manifest name");
+ is(project.manifest.name, project.name, "Display name uses manifest name");
+
+ yield closeWebIDE(win);
+
+ yield removeAllProjects();
+
+ SimpleTest.finish();
+ });
+ }
+
+
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/webide/test/test_device_permissions.html b/devtools/client/webide/test/test_device_permissions.html
new file mode 100644
index 000000000..eadd9f595
--- /dev/null
+++ b/devtools/client/webide/test/test_device_permissions.html
@@ -0,0 +1,81 @@
+<!DOCTYPE html>
+
+<html>
+
+ <head>
+ <meta charset="utf8">
+ <title></title>
+
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js"></script>
+ <script type="application/javascript;version=1.8" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ </head>
+
+ <body>
+
+ <script type="application/javascript;version=1.8">
+ window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+
+ Task.spawn(function* () {
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+
+ let win = yield openWebIDE();
+
+ let permIframe = win.document.querySelector("#deck-panel-permissionstable");
+ let docRuntime = getRuntimeDocument(win);
+ let winRuntime = getRuntimeWindow(win);
+
+ yield connectToLocalRuntime(win);
+
+ let perm = docRuntime.querySelector("#runtime-permissions");
+
+ ok(!perm.hasAttribute("disabled"), "perm cmd enabled");
+
+ let deck = win.document.querySelector("#deck");
+
+ winRuntime.runtimeList.showPermissionsTable();
+ is(deck.selectedPanel, permIframe, "permission iframe selected");
+
+ yield nextTick();
+
+ yield lazyIframeIsLoaded(permIframe);
+
+ yield permIframe.contentWindow.getRawPermissionsTablePromise;
+
+ doc = permIframe.contentWindow.document;
+ trs = doc.querySelectorAll(".line");
+ found = false;
+ for (let tr of trs) {
+ let [name,v1,v2,v3] = tr.querySelectorAll("td");
+ if (name.textContent == "geolocation") {
+ found = true;
+ is(v1.className, "permprompt", "geolocation perm is valid");
+ is(v2.className, "permprompt", "geolocation perm is valid");
+ is(v3.className, "permprompt", "geolocation perm is valid");
+ break;
+ }
+ }
+ ok(found, "Found geolocation line");
+
+ doc.querySelector("#close").click();
+
+ ok(!deck.selectedPanel, "No panel selected");
+
+ DebuggerServer.destroy();
+
+ yield closeWebIDE(win);
+
+ SimpleTest.finish();
+ }).then(null, e => {
+ ok(false, "Exception: " + e);
+ SimpleTest.finish();
+ });
+ }
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/webide/test/test_device_preferences.html b/devtools/client/webide/test/test_device_preferences.html
new file mode 100644
index 000000000..c79db7f79
--- /dev/null
+++ b/devtools/client/webide/test/test_device_preferences.html
@@ -0,0 +1,87 @@
+<!DOCTYPE html>
+
+<html>
+
+ <head>
+ <meta charset="utf8">
+ <title></title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js"></script>
+ <script type="application/javascript;version=1.8" src="head.js"></script>
+ <script type="application/javascript;version=1.8" src="device_front_shared.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ </head>
+
+ <body>
+
+ <script type="application/javascript;version=1.8">
+ window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+
+ Task.spawn(function* () {
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+
+ let win = yield openWebIDE();
+
+ let prefIframe = win.document.querySelector("#deck-panel-devicepreferences");
+ let docRuntime = getRuntimeDocument(win);
+
+ win.AppManager.update("runtime-list");
+
+ yield connectToLocalRuntime(win);
+
+ let prefs = docRuntime.querySelector("#runtime-preferences");
+
+ ok(!prefs.hasAttribute("disabled"), "device prefs cmd enabled");
+
+ let deck = win.document.querySelector("#deck");
+
+ win.Cmds.showDevicePrefs();
+ is(deck.selectedPanel, prefIframe, "device preferences iframe selected");
+
+ yield nextTick();
+
+ yield lazyIframeIsLoaded(prefIframe);
+
+ yield prefIframe.contentWindow.getAllPrefs;
+
+ setDocument(prefIframe);
+
+ let fields = doc.querySelectorAll(".editable");
+
+ addNewField();
+
+ let preference = "accessibility.accesskeycausesactivation";
+
+ fieldChange(fields, preference);
+
+ addNewFieldWithEnter();
+
+ editExistingField();
+
+ addNewFieldInteger();
+
+ yield editFieldInteger();
+
+ yield resetExistingField("accessibility.accesskeycausesactivation");
+
+ addNewFieldBoolean();
+
+ searchFields(deck, "debugger");
+
+ DebuggerServer.destroy();
+
+ yield closeWebIDE(win);
+
+ SimpleTest.finish();
+ }).then(null, e => {
+ ok(false, "Exception: " + e);
+ SimpleTest.finish();
+ });
+ }
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/webide/test/test_device_runtime.html b/devtools/client/webide/test/test_device_runtime.html
new file mode 100644
index 000000000..0ac42b472
--- /dev/null
+++ b/devtools/client/webide/test/test_device_runtime.html
@@ -0,0 +1,81 @@
+<!DOCTYPE html>
+
+<html>
+
+ <head>
+ <meta charset="utf8">
+ <title></title>
+
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js"></script>
+ <script type="application/javascript;version=1.8" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ </head>
+
+ <body>
+
+ <script type="application/javascript;version=1.8">
+ window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+
+ Task.spawn(function* () {
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+
+ let win = yield openWebIDE();
+
+ let detailsIframe = win.document.querySelector("#deck-panel-runtimedetails");
+
+ yield connectToLocalRuntime(win);
+
+ let details = win.document.querySelector("#cmd_showRuntimeDetails");
+
+ ok(!details.hasAttribute("disabled"), "info cmd enabled");
+
+ let deck = win.document.querySelector("#deck");
+
+ win.Cmds.showRuntimeDetails();
+ is(deck.selectedPanel, detailsIframe, "info iframe selected");
+
+ yield nextTick();
+
+ yield lazyIframeIsLoaded(detailsIframe);
+
+ yield detailsIframe.contentWindow.getDescriptionPromise;
+
+ // device info and permissions content is checked in other tests
+ // We just test one value to make sure we get something
+
+ let doc = detailsIframe.contentWindow.document;
+ let trs = doc.querySelectorAll("tr");
+ let found = false;
+
+ for (let tr of trs) {
+ let [name,val] = tr.querySelectorAll("td");
+ if (name.textContent == "appid") {
+ found = true;
+ is(val.textContent, Services.appinfo.ID, "appid has the right value");
+ break;
+ }
+ }
+ ok(found, "Found appid line");
+
+ doc.querySelector("#close").click();
+
+ ok(!deck.selectedPanel, "No panel selected");
+
+ DebuggerServer.destroy();
+
+ yield closeWebIDE(win);
+
+ SimpleTest.finish();
+ }).then(null, e => {
+ ok(false, "Exception: " + e);
+ SimpleTest.finish();
+ });
+ }
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/webide/test/test_device_settings.html b/devtools/client/webide/test/test_device_settings.html
new file mode 100644
index 000000000..ec8e7943b
--- /dev/null
+++ b/devtools/client/webide/test/test_device_settings.html
@@ -0,0 +1,87 @@
+<!DOCTYPE html>
+
+<html>
+
+ <head>
+ <meta charset="utf8">
+ <title></title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js"></script>
+ <script type="application/javascript;version=1.8" src="head.js"></script>
+ <script type="application/javascript;version=1.8" src="device_front_shared.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ </head>
+
+ <body>
+
+ <script type="application/javascript;version=1.8">
+ window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+
+ Task.spawn(function*() {
+ if (SpecialPowers.isMainProcess()) {
+ Cu.import("resource://gre/modules/SettingsRequestManager.jsm");
+ }
+
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+
+ let win = yield openWebIDE();
+
+ let settingIframe = win.document.querySelector("#deck-panel-devicesettings");
+ let docRuntime = getRuntimeDocument(win);
+
+ win.AppManager.update("runtime-list");
+
+ yield connectToLocalRuntime(win);
+
+ let settings = docRuntime.querySelector("#runtime-settings");
+
+ ok(!settings.hasAttribute("disabled"), "device settings cmd enabled");
+
+ let deck = win.document.querySelector("#deck");
+
+ win.Cmds.showSettings();
+ is(deck.selectedPanel, settingIframe, "device settings iframe selected");
+
+ yield nextTick();
+
+ yield lazyIframeIsLoaded(settingIframe);
+
+ yield settingIframe.contentWindow.getAllSettings;
+
+ setDocument(settingIframe);
+
+ let fields = doc.querySelectorAll(".editable");
+
+ addNewField();
+
+ addNewFieldWithEnter();
+
+ editExistingField();
+
+ addNewFieldInteger();
+
+ yield editFieldInteger();
+
+ yield resetNewField("new-string-field");
+
+ addNewFieldBoolean();
+
+ searchFields(deck, "new-boolean-field2");
+
+ DebuggerServer.destroy();
+
+ yield closeWebIDE(win);
+
+ SimpleTest.finish();
+ }).then(null, e => {
+ ok(false, "Exception: " + e);
+ SimpleTest.finish();
+ });
+ }
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/webide/test/test_duplicate_import.html b/devtools/client/webide/test/test_duplicate_import.html
new file mode 100644
index 000000000..ef01e23e4
--- /dev/null
+++ b/devtools/client/webide/test/test_duplicate_import.html
@@ -0,0 +1,77 @@
+<!DOCTYPE html>
+
+<html>
+
+ <head>
+ <meta charset="utf8">
+ <title></title>
+
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js"></script>
+ <script type="application/javascript;version=1.8" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ </head>
+
+ <body>
+ <script type="application/javascript;version=1.8">
+ window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+
+ Task.spawn(function*() {
+ let win = yield openWebIDE();
+ let docProject = getProjectDocument(win);
+ let winProject = getProjectWindow(win);
+ let packagedAppLocation = getTestFilePath("app");
+ let hostedAppManifest = TEST_BASE + "hosted_app.manifest";
+
+ yield win.AppProjects.load();
+ is(win.AppProjects.projects.length, 0, "IDB is empty");
+
+ let onValidated = waitForUpdate(win, "project-validated");
+ let onDetails = waitForUpdate(win, "details");
+ yield winProject.projectList.importPackagedApp(packagedAppLocation);
+ yield onValidated;
+ yield onDetails;
+
+ yield winProject.projectList.importHostedApp(hostedAppManifest);
+ yield waitForUpdate(win, "project-validated");
+ yield nextTick();
+
+ onValidated = waitForUpdate(win, "project-validated");
+ onDetails = waitForUpdate(win, "details");
+ yield winProject.projectList.importPackagedApp(packagedAppLocation);
+ yield onValidated;
+ yield onDetails;
+
+ let project = win.AppManager.selectedProject;
+ is(project.location, packagedAppLocation, "Correctly reselected existing packaged app.");
+ yield nextTick();
+
+ info("to call importHostedApp(" + hostedAppManifest + ") again");
+ yield winProject.projectList.importHostedApp(hostedAppManifest);
+ yield waitForUpdate(win, "project-validated");
+ project = win.AppManager.selectedProject;
+ is(project.location, hostedAppManifest, "Correctly reselected existing hosted app.");
+ yield nextTick();
+
+ let panelNode = docProject.querySelector("#project-panel");
+ let items = panelNode.querySelectorAll(".panel-item");
+ // 3 controls, + 2 projects
+ is(items.length, 5, "5 projects in panel");
+ is(items[3].querySelector("span").textContent, "A name (in app directory)", "Panel text is correct");
+ is(items[4].querySelector("span").textContent, "hosted manifest name property", "Panel text is correct");
+
+ yield closeWebIDE(win);
+
+ yield removeAllProjects();
+
+ SimpleTest.finish();
+ }).then(null, e => {
+ ok(false, "Exception: " + e);
+ SimpleTest.finish();
+ });
+ }
+ </script>
+ </body>
+</html>
+
diff --git a/devtools/client/webide/test/test_fullscreenToolbox.html b/devtools/client/webide/test/test_fullscreenToolbox.html
new file mode 100644
index 000000000..6ae0c4446
--- /dev/null
+++ b/devtools/client/webide/test/test_fullscreenToolbox.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+
+<html>
+
+ <head>
+ <meta charset="utf8">
+ <title></title>
+
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js"></script>
+ <script type="application/javascript;version=1.8" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ </head>
+
+ <body>
+
+ <script type="application/javascript;version=1.8">
+ function connectToLocal(win, docRuntime) {
+ let deferred = promise.defer();
+ win.AppManager.connection.once(
+ win.Connection.Events.CONNECTED,
+ () => deferred.resolve());
+ docRuntime.querySelectorAll(".runtime-panel-item-other")[1].click();
+ return deferred.promise;
+ }
+
+ window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+
+ Task.spawn(function* () {
+ let win = yield openWebIDE();
+ let docProject = getProjectDocument(win);
+ let docRuntime = getRuntimeDocument(win);
+ win.AppManager.update("runtime-list");
+
+ yield connectToLocal(win, docRuntime);
+
+ // Select main process
+ yield waitForUpdate(win, "runtime-targets");
+ SimpleTest.executeSoon(() => {
+ docProject.querySelectorAll("#project-panel-runtimeapps .panel-item")[0].click();
+ });
+
+ yield waitForUpdate(win, "project");
+
+ ok(win.UI.toolboxPromise, "Toolbox promise exists");
+ yield win.UI.toolboxPromise;
+
+ let nbox = win.document.querySelector("#notificationbox");
+ ok(!nbox.hasAttribute("toolboxfullscreen"), "Toolbox is not fullscreen");
+
+ win.Cmds.showRuntimeDetails();
+
+ ok(!nbox.hasAttribute("toolboxfullscreen"), "Toolbox is not fullscreen");
+
+ yield win.Cmds.disconnectRuntime();
+
+ yield closeWebIDE(win);
+
+ DebuggerServer.destroy();
+
+ SimpleTest.finish();
+ });
+ }
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/webide/test/test_import.html b/devtools/client/webide/test/test_import.html
new file mode 100644
index 000000000..830198cca
--- /dev/null
+++ b/devtools/client/webide/test/test_import.html
@@ -0,0 +1,82 @@
+<!DOCTYPE html>
+
+<html>
+
+ <head>
+ <meta charset="utf8">
+ <title></title>
+
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js"></script>
+ <script type="application/javascript;version=1.8" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ </head>
+
+ <body>
+ <script type="application/javascript;version=1.8">
+ window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+
+ Task.spawn(function*() {
+ let win = yield openWebIDE();
+ let docProject = getProjectDocument(win);
+ let winProject = getProjectWindow(win);
+ let packagedAppLocation = getTestFilePath("app");
+
+ yield win.AppProjects.load();
+ is(win.AppProjects.projects.length, 0, "IDB is empty");
+
+ info("to call importPackagedApp(" + packagedAppLocation + ")");
+ ok(!win.UI._busyPromise, "UI is not busy");
+
+ let onValidated = waitForUpdate(win, "project-validated");
+ let onDetails = waitForUpdate(win, "details");
+ yield winProject.projectList.importPackagedApp(packagedAppLocation);
+ yield onValidated;
+ yield onDetails;
+
+ let project = win.AppManager.selectedProject;
+ is(project.location, packagedAppLocation, "Location is valid");
+ is(project.name, "A name (in app directory)", "name field has been updated");
+ is(project.manifest.launch_path, "/index.html", "manifest found. launch_path valid.");
+ is(project.manifest.description, "desc", "manifest found. description valid");
+
+ yield nextTick();
+
+ let hostedAppManifest = TEST_BASE + "hosted_app.manifest";
+ yield winProject.projectList.importHostedApp(hostedAppManifest);
+ yield waitForUpdate(win, "project-validated");
+
+ project = win.AppManager.selectedProject;
+ is(project.location, hostedAppManifest, "Location is valid");
+ is(project.name, "hosted manifest name property", "name field has been updated");
+
+ yield nextTick();
+
+ hostedAppManifest = TEST_BASE + "/app";
+ yield winProject.projectList.importHostedApp(hostedAppManifest);
+ yield waitForUpdate(win, "project-validated");
+
+ project = win.AppManager.selectedProject;
+ ok(project.location.endsWith('manifest.webapp'), "The manifest was found and the project was updated");
+
+ let panelNode = docProject.querySelector("#project-panel");
+ let items = panelNode.querySelectorAll(".panel-item");
+ // 4 controls, + 2 projects
+ is(items.length, 6, "6 projects in panel");
+ is(items[3].querySelector("span").textContent, "A name (in app directory)", "Panel text is correct");
+ is(items[4].querySelector("span").textContent, "hosted manifest name property", "Panel text is correct");
+
+ yield closeWebIDE(win);
+
+ yield removeAllProjects();
+
+ SimpleTest.finish();
+ }).then(null, e => {
+ ok(false, "Exception: " + e);
+ SimpleTest.finish();
+ });
+ }
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/webide/test/test_manifestUpdate.html b/devtools/client/webide/test/test_manifestUpdate.html
new file mode 100644
index 000000000..66f9affd0
--- /dev/null
+++ b/devtools/client/webide/test/test_manifestUpdate.html
@@ -0,0 +1,98 @@
+<!DOCTYPE html>
+
+<html>
+
+ <head>
+ <meta charset="utf8">
+ <title></title>
+
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js"></script>
+ <script type="application/javascript;version=1.8" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ </head>
+
+ <body>
+
+ <script type="application/javascript;version=1.8">
+ window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+
+ let {TextDecoder, OS} = Cu.import("resource://gre/modules/osfile.jsm", {});
+
+ Task.spawn(function* () {
+ let win = yield openWebIDE();
+ let winProject = getProjectWindow(win);
+ let AppManager = win.AppManager;
+
+ function isProjectMarkedAsValid() {
+ let details = win.frames[1];
+ return !details.document.body.classList.contains("error");
+ }
+
+ let packagedAppLocation = getTestFilePath("app");
+
+ let onValidated = waitForUpdate(win, "project-validated");
+ let onDetails = waitForUpdate(win, "details");
+ yield winProject.projectList.importPackagedApp(packagedAppLocation);
+ yield onValidated;
+ yield onDetails;
+
+ let project = win.AppManager.selectedProject;
+
+ ok("name" in project.manifest, "manifest includes name");
+ is(project.name, project.manifest.name, "Display name uses manifest name");
+ ok(isProjectMarkedAsValid(), "project is marked as valid");
+
+ // Change the name
+ let originalName = project.manifest.name;
+
+ project.manifest.name = "xxx";
+
+ // Write to disk
+ yield AppManager.writeManifest(project);
+
+ // Read file
+ let manifestPath = OS.Path.join(packagedAppLocation, "manifest.webapp");
+ let Decoder = new TextDecoder();
+ let data = yield OS.File.read(manifestPath);
+ data = new TextDecoder().decode(data);
+ let json = JSON.parse(data);
+ is(json.name, "xxx", "manifest written on disc");
+
+ // Make the manifest invalid on disk
+ delete json.name;
+ let Encoder = new TextEncoder();
+ data = Encoder.encode(JSON.stringify(json));
+ yield OS.File.writeAtomic(manifestPath, data , {tmpPath: manifestPath + ".tmp"});
+
+ // Trigger validation
+ yield AppManager.validateAndUpdateProject(AppManager.selectedProject);
+ yield nextTick();
+
+ ok(!("name" in project.manifest), "manifest has been updated");
+ is(project.name, "--", "Placeholder is used for display name");
+ ok(!isProjectMarkedAsValid(), "project is marked as invalid");
+
+ // Make the manifest valid on disk
+ project.manifest.name = originalName;
+ yield AppManager.writeManifest(project);
+
+ // Trigger validation
+ yield AppManager.validateAndUpdateProject(AppManager.selectedProject);
+ yield nextTick();
+
+ ok("name" in project.manifest, "manifest includes name");
+ is(project.name, originalName, "Display name uses original manifest name");
+ ok(isProjectMarkedAsValid(), "project is marked as valid");
+
+ yield closeWebIDE(win);
+
+ yield removeAllProjects();
+
+ SimpleTest.finish();
+ });
+ }
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/webide/test/test_newapp.html b/devtools/client/webide/test/test_newapp.html
new file mode 100644
index 000000000..45374f268
--- /dev/null
+++ b/devtools/client/webide/test/test_newapp.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+
+<html>
+
+ <head>
+ <meta charset="utf8">
+ <title></title>
+
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js"></script>
+ <script type="application/javascript;version=1.8" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ </head>
+
+ <body>
+
+ <script type="application/javascript;version=1.8">
+ window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+
+ Task.spawn(function* () {
+ let win = yield openWebIDE();
+ let winProject = getProjectWindow(win);
+ let tmpDir = FileUtils.getDir("TmpD", []);
+ yield winProject.projectList.newApp({
+ index: 0,
+ name: "webideTmpApp",
+ folder: tmpDir
+ });
+
+ let project = win.AppManager.selectedProject;
+ tmpDir = FileUtils.getDir("TmpD", ["webidetmpapp"]);
+ ok(tmpDir.isDirectory(), "Directory created");
+ is(project.location, tmpDir.path, "Location is valid (and lowercase)");
+ is(project.name, "webideTmpApp", "name field has been updated");
+
+ // Clean up
+ tmpDir.remove(true);
+ yield closeWebIDE(win);
+ yield removeAllProjects();
+ SimpleTest.finish();
+ });
+ }
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/webide/test/test_runtime.html b/devtools/client/webide/test/test_runtime.html
new file mode 100644
index 000000000..9b16ef82d
--- /dev/null
+++ b/devtools/client/webide/test/test_runtime.html
@@ -0,0 +1,203 @@
+<!DOCTYPE html>
+
+<html>
+
+ <head>
+ <meta charset="utf8">
+ <title></title>
+
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js"></script>
+ <script type="application/javascript;version=1.8" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ </head>
+
+ <body>
+
+ <script type="application/javascript;version=1.8">
+ window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+
+ let win;
+
+ SimpleTest.registerCleanupFunction(() => {
+ Task.spawn(function*() {
+ if (win) {
+ yield closeWebIDE(win);
+ }
+ DebuggerServer.destroy();
+ yield removeAllProjects();
+ });
+ });
+
+ Task.spawn(function*() {
+ function isPlayActive() {
+ return !win.document.querySelector("#cmd_play").hasAttribute("disabled");
+ }
+
+ function isStopActive() {
+ return !win.document.querySelector("#cmd_stop").hasAttribute("disabled");
+ }
+
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+
+ win = yield openWebIDE();
+ let docRuntime = getRuntimeDocument(win);
+ let docProject = getProjectDocument(win);
+ let winProject = getProjectWindow(win);
+
+ let packagedAppLocation = getTestFilePath("app");
+
+ let onValidated = waitForUpdate(win, "project-validated");
+ let onDetails = waitForUpdate(win, "details");
+ yield winProject.projectList.importPackagedApp(packagedAppLocation);
+ yield onValidated;
+ yield onDetails;
+
+ win.AppManager.runtimeList.usb.push({
+ connect: function(connection) {
+ is(connection, win.AppManager.connection, "connection is valid");
+ connection.host = null; // force connectPipe
+ connection.connect();
+ return promise.resolve();
+ },
+
+ get name() {
+ return "fakeRuntime";
+ }
+ });
+
+ win.AppManager.runtimeList.usb.push({
+ connect: function(connection) {
+ let deferred = promise.defer();
+ return deferred.promise;
+ },
+
+ get name() {
+ return "infiniteRuntime";
+ }
+ });
+
+ win.AppManager.runtimeList.usb.push({
+ connect: function(connection) {
+ let deferred = promise.defer();
+ return deferred.promise;
+ },
+
+ prolongedConnection: true,
+
+ get name() {
+ return "prolongedRuntime";
+ }
+ });
+
+ win.AppManager.update("runtime-list");
+
+ let panelNode = docRuntime.querySelector("#runtime-panel");
+ let items = panelNode.querySelectorAll(".runtime-panel-item-usb");
+ is(items.length, 3, "Found 3 runtime buttons");
+
+ let connectionsChanged = waitForConnectionChange("opened", 2);
+ items[0].click();
+
+ ok(win.document.querySelector("window").className, "busy", "UI is busy");
+ yield win.UI._busyPromise;
+
+ yield connectionsChanged;
+ is(Object.keys(DebuggerServer._connections).length, 2, "Connected");
+
+ yield waitForUpdate(win, "runtime-global-actors");
+
+ // Play button always disabled now, webapps actor removed
+ ok(!isPlayActive(), "play button is disabled");
+ ok(!isStopActive(), "stop button is disabled");
+ let oldProject = win.AppManager.selectedProject;
+ win.AppManager.selectedProject = null;
+
+ yield nextTick();
+
+ ok(!isPlayActive(), "play button is disabled");
+ ok(!isStopActive(), "stop button is disabled");
+ win.AppManager._selectedProject = oldProject;
+ win.UI.updateCommands();
+
+ yield nextTick();
+
+ ok(!isPlayActive(), "play button is enabled");
+ ok(!isStopActive(), "stop button is disabled");
+
+ connectionsChanged = waitForConnectionChange("closed", 2);
+ yield win.Cmds.disconnectRuntime();
+
+ yield connectionsChanged;
+ is(Object.keys(DebuggerServer._connections).length, 0, "Disconnected");
+
+ ok(win.AppManager.selectedProject, "A project is still selected");
+ ok(!isPlayActive(), "play button is disabled");
+ ok(!isStopActive(), "stop button is disabled");
+
+ connectionsChanged = waitForConnectionChange("opened", 2);
+ docRuntime.querySelectorAll(".runtime-panel-item-other")[1].click();
+
+ yield waitForUpdate(win, "runtime-targets");
+
+ yield connectionsChanged;
+ is(Object.keys(DebuggerServer._connections).length, 2, "Locally connected");
+
+ ok(win.AppManager.isMainProcessDebuggable(), "Main process available");
+
+ // Select main process
+ SimpleTest.executeSoon(() => {
+ docProject.querySelectorAll("#project-panel-runtimeapps .panel-item")[0].click();
+ });
+
+ yield waitForUpdate(win, "project");
+
+ // Toolbox opens automatically for main process / runtime apps
+ ok(win.UI.toolboxPromise, "Toolbox promise exists");
+ yield win.UI.toolboxPromise;
+
+ yield win.Cmds.disconnectRuntime();
+
+ Services.prefs.setIntPref("devtools.webide.busyTimeout", 100);
+
+ // Wait for error message since connection never completes
+ let errorDeferred = promise.defer();
+ win.UI.reportError = errorName => {
+ if (errorName === "error_operationTimeout") {
+ errorDeferred.resolve();
+ }
+ };
+
+ // Click the infinite runtime
+ items[1].click();
+ ok(win.document.querySelector("window").className, "busy", "UI is busy");
+ yield errorDeferred.promise;
+
+ // Check for unexpected error message since this is prolonged
+ let noErrorDeferred = promise.defer();
+ win.UI.reportError = errorName => {
+ if (errorName === "error_operationTimeout") {
+ noErrorDeferred.reject();
+ }
+ };
+
+ // Click the prolonged runtime
+ items[2].click();
+ ok(win.document.querySelector("window").className, "busy", "UI is busy");
+
+ setTimeout(() => {
+ noErrorDeferred.resolve();
+ }, 1000);
+
+ yield noErrorDeferred.promise;
+
+ SimpleTest.finish();
+ });
+ }
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/webide/test/test_simulators.html b/devtools/client/webide/test/test_simulators.html
new file mode 100644
index 000000000..204881512
--- /dev/null
+++ b/devtools/client/webide/test/test_simulators.html
@@ -0,0 +1,426 @@
+<!DOCTYPE html>
+
+<html>
+
+ <head>
+ <meta charset="utf8">
+ <title></title>
+
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js"></script>
+ <script type="application/javascript;version=1.8" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ </head>
+
+ <body>
+
+ <script type="application/javascript;version=1.8">
+ window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+
+ const asyncStorage = require("devtools/shared/async-storage");
+ const EventEmitter = require("devtools/shared/event-emitter");
+ const { GetAvailableAddons } = require("devtools/client/webide/modules/addons");
+ const { getDevices } = require("devtools/client/shared/devices");
+ const { Simulator, Simulators } = require("devtools/client/webide/modules/simulators");
+ const { AddonSimulatorProcess,
+ OldAddonSimulatorProcess,
+ CustomSimulatorProcess } = require("devtools/client/webide/modules/simulator-process");
+
+ function addonStatus(addon, status) {
+ if (addon.status == status) {
+ return promise.resolve();
+ }
+ let deferred = promise.defer();
+ addon.on("update", function onUpdate() {
+ if (addon.status == status) {
+ addon.off("update", onUpdate);
+ nextTick().then(() => deferred.resolve());
+ }
+ });
+ return deferred.promise;
+ }
+
+ function waitForUpdate(length) {
+ info(`Wait for update with length ${length}`);
+ let deferred = promise.defer();
+ let handler = (_, data) => {
+ if (data.length != length) {
+ return;
+ }
+ info(`Got update with length ${length}`);
+ Simulators.off("updated", handler);
+ deferred.resolve();
+ };
+ Simulators.on("updated", handler);
+ return deferred.promise;
+ }
+
+ Task.spawn(function* () {
+ let win = yield openWebIDE(false);
+
+ yield Simulators._load();
+
+ let docRuntime = getRuntimeDocument(win);
+ let find = win.document.querySelector.bind(docRuntime);
+ let findAll = win.document.querySelectorAll.bind(docRuntime);
+
+ let simulatorList = find("#runtime-panel-simulator");
+ let simulatorPanel = win.document.querySelector("#deck-panel-simulator");
+
+ // Hack SimulatorProcesses to spy on simulation parameters.
+
+ let runPromise;
+ function fakeRun() {
+ runPromise.resolve({
+ path: this.b2gBinary.path,
+ args: this.args
+ });
+ // Don't actually try to connect to the fake simulator.
+ throw new Error("Aborting on purpose before connection.");
+ }
+
+ AddonSimulatorProcess.prototype.run = fakeRun;
+ OldAddonSimulatorProcess.prototype.run = fakeRun;
+ CustomSimulatorProcess.prototype.run = fakeRun;
+
+ function runSimulator(i) {
+ runPromise = promise.defer();
+ findAll(".runtime-panel-item-simulator")[i].click();
+ return runPromise.promise;
+ }
+
+ // Install fake "Firefox OS 1.0" simulator addon.
+
+ let addons = yield GetAvailableAddons();
+
+ let sim10 = addons.simulators.filter(a => a.version == "1.0")[0];
+
+ sim10.install();
+
+ let updated = waitForUpdate(1);
+ yield addonStatus(sim10, "installed");
+ yield updated;
+ // Wait for next tick to ensure UI elements are updated
+ yield nextTick();
+
+ is(findAll(".runtime-panel-item-simulator").length, 1, "One simulator in runtime panel");
+
+ // Install fake "Firefox OS 2.0" simulator addon.
+
+ let sim20 = addons.simulators.filter(a => a.version == "2.0")[0];
+
+ sim20.install();
+
+ updated = waitForUpdate(2);
+ yield addonStatus(sim20, "installed");
+ yield updated;
+ // Wait for next tick to ensure UI elements are updated
+ yield nextTick();
+
+ is(findAll(".runtime-panel-item-simulator").length, 2, "Two simulators in runtime panel");
+
+ // Dry run a simulator to verify that its parameters look right.
+
+ let params = yield runSimulator(0);
+
+ ok(params.path.includes(sim10.addonID) && params.path.includes("b2g-bin"), "Simulator binary path looks right");
+
+ let pid = params.args.indexOf("-profile");
+ ok(pid > -1, "Simulator process arguments have --profile");
+
+ let profilePath = params.args[pid + 1];
+ ok(profilePath.includes(sim10.addonID) && profilePath.includes("profile"), "Simulator profile path looks right");
+
+ ok(params.args.indexOf("-dbgport") > -1 || params.args.indexOf("-start-debugger-server") > -1, "Simulator process arguments have a debugger port");
+
+ ok(params.args.indexOf("-no-remote") > -1, "Simulator process arguments have --no-remote");
+
+ // Wait for next tick to ensure UI elements are updated
+ yield nextTick();
+
+ // Configure the fake 1.0 simulator.
+
+ simulatorList.querySelectorAll(".configure-button")[0].click();
+ is(win.document.querySelector("#deck").selectedPanel, simulatorPanel, "Simulator deck panel is selected");
+
+ yield lazyIframeIsLoaded(simulatorPanel);
+
+ let doc = simulatorPanel.contentWindow.document;
+ let form = doc.querySelector("#simulator-editor");
+
+ let formReady = new Promise((resolve, reject) => {
+ form.addEventListener("change", () => {
+ resolve();
+ });
+ });
+
+ let change = doc.createEvent("HTMLEvents");
+ change.initEvent("change", true, true);
+
+ function set(input, value) {
+ input.value = value;
+ input.dispatchEvent(change);
+ return nextTick();
+ }
+
+ let MockFilePicker = SpecialPowers.MockFilePicker;
+ MockFilePicker.init(simulatorPanel.contentWindow);
+
+ yield formReady;
+
+ // Test `name`.
+
+ is(form.name.value, find(".runtime-panel-item-simulator").textContent, "Original simulator name");
+
+ let customName = "CustomFox ";
+ yield set(form.name, customName + "1.0");
+
+ is(find(".runtime-panel-item-simulator").textContent, form.name.value, "Updated simulator name");
+
+ // Test `version`.
+
+ is(form.version.value, sim10.addonID, "Original simulator version");
+ ok(!form.version.classList.contains("custom"), "Version selector is not customized");
+
+ yield set(form.version, sim20.addonID);
+
+ ok(!form.version.classList.contains("custom"), "Version selector is not customized after addon change");
+ is(form.name.value, customName + "2.0", "Simulator name was updated to new version");
+
+ // Pick custom binary, but act like the user aborted the file picker.
+
+ MockFilePicker.returnFiles = [];
+ yield set(form.version, "pick");
+
+ is(form.version.value, sim20.addonID, "Version selector reverted to last valid choice after customization abort");
+ ok(!form.version.classList.contains("custom"), "Version selector is not customized after customization abort");
+
+ // Pick custom binary, and actually follow through. (success, verify value = "custom" and textContent = custom path)
+
+ MockFilePicker.useAnyFile();
+ yield set(form.version, "pick");
+
+ let fakeBinary = MockFilePicker.returnFiles[0];
+
+ ok(form.version.value == "custom", "Version selector was set to a new custom binary");
+ ok(form.version.classList.contains("custom"), "Version selector is now customized");
+ is(form.version.selectedOptions[0].textContent, fakeBinary.path, "Custom option textContent is correct");
+
+ yield set(form.version, sim10.addonID);
+
+ ok(form.version.classList.contains("custom"), "Version selector remains customized after change back to addon");
+ is(form.name.value, customName + "1.0", "Simulator name was updated to new version");
+
+ yield set(form.version, "custom");
+
+ ok(form.version.value == "custom", "Version selector is back to custom");
+
+ // Test `profile`.
+
+ is(form.profile.value, "default", "Default simulator profile");
+ ok(!form.profile.classList.contains("custom"), "Profile selector is not customized");
+
+ MockFilePicker.returnFiles = [];
+ yield set(form.profile, "pick");
+
+ is(form.profile.value, "default", "Profile selector reverted to last valid choice after customization abort");
+ ok(!form.profile.classList.contains("custom"), "Profile selector is not customized after customization abort");
+
+ let fakeProfile = FileUtils.getDir("TmpD", []);
+
+ MockFilePicker.returnFiles = [ fakeProfile ];
+ yield set(form.profile, "pick");
+
+ ok(form.profile.value == "custom", "Profile selector was set to a new custom directory");
+ ok(form.profile.classList.contains("custom"), "Profile selector is now customized");
+ is(form.profile.selectedOptions[0].textContent, fakeProfile.path, "Custom option textContent is correct");
+
+ yield set(form.profile, "default");
+
+ is(form.profile.value, "default", "Profile selector back to default");
+ ok(form.profile.classList.contains("custom"), "Profile selector remains customized after change back to default");
+
+ yield set(form.profile, "custom");
+
+ is(form.profile.value, "custom", "Profile selector back to custom");
+
+ params = yield runSimulator(0);
+
+ is(params.path, fakeBinary.path, "Simulator process uses custom binary path");
+
+ pid = params.args.indexOf("-profile");
+ is(params.args[pid + 1], fakeProfile.path, "Simulator process uses custom profile directory");
+
+ yield set(form.version, sim10.addonID);
+
+ is(form.name.value, customName + "1.0", "Simulator restored to 1.0");
+
+ params = yield runSimulator(0);
+
+ pid = params.args.indexOf("-profile");
+ is(params.args[pid + 1], fakeProfile.path, "Simulator process still uses custom profile directory");
+
+ yield set(form.version, "custom");
+
+ // Test `device`.
+
+ let defaults = Simulator.prototype._defaults;
+
+ for (let param in defaults.phone) {
+ is(form[param].value, String(defaults.phone[param]), "Default phone value for device " + param);
+ }
+
+ let width = 5000, height = 4000;
+ yield set(form.width, width);
+ yield set(form.height, height);
+
+ is(form.device.value, "custom", "Device selector is custom");
+
+ params = yield runSimulator(0);
+
+ let sid = params.args.indexOf("-screen");
+ ok(sid > -1, "Simulator process arguments have --screen");
+ ok(params.args[sid + 1].includes(width + "x" + height), "Simulator screen resolution looks right");
+
+ yield set(form.version, sim10.addonID);
+
+ // Configure the fake 2.0 simulator.
+
+ simulatorList.querySelectorAll(".configure-button")[1].click();
+ // Wait for next tick to ensure UI elements are updated
+ yield nextTick();
+
+ // Test `name`.
+
+ is(form.name.value, findAll(".runtime-panel-item-simulator")[1].textContent, "Original simulator name");
+
+ yield set(form.name, customName + "2.0");
+
+ is(findAll(".runtime-panel-item-simulator")[1].textContent, form.name.value, "Updated simulator name");
+
+ yield set(form.version, sim10.addonID);
+
+ ok(form.name.value !== customName + "1.0", "Conflicting simulator name was deduplicated");
+
+ is(form.name.value, findAll(".runtime-panel-item-simulator")[1].textContent, "Deduplicated simulator name stayed consistent");
+
+ yield set(form.version, sim20.addonID);
+
+ is(form.name.value, customName + "2.0", "Name deduplication was undone when possible");
+
+ // Test `device`.
+
+ for (let param in defaults.phone) {
+ is(form[param].value, String(defaults.phone[param]), "Default phone value for device " + param);
+ }
+
+ let devices = yield getDevices();
+ devices = devices[devices.TYPES[0]];
+ let device = devices[devices.length - 1];
+
+ yield set(form.device, device.name);
+
+ is(form.device.value, device.name, "Device selector was changed");
+ is(form.width.value, String(device.width), "New device width is correct");
+ is(form.height.value, String(device.height), "New device height is correct");
+
+ params = yield runSimulator(1);
+
+ sid = params.args.indexOf("-screen");
+ ok(params.args[sid + 1].includes(device.width + "x" + device.height), "Simulator screen resolution looks right");
+
+ // Test Simulator Menu.
+ is(doc.querySelector("#tv_simulator_menu").style.visibility, "hidden", "OpenTVDummyDirectory Button is not hidden");
+
+ // Restore default simulator options.
+
+ doc.querySelector("#reset").click();
+ // Wait for next tick to ensure UI elements are updated
+ yield nextTick();
+
+ for (let param in defaults.phone) {
+ is(form[param].value, String(defaults.phone[param]), "Default phone value for device " + param);
+ }
+
+ // Install and configure the fake "Firefox OS 3.0 TV" simulator addon.
+
+ let sim30tv = addons.simulators.filter(a => a.version == "3.0_tv")[0];
+
+ sim30tv.install();
+
+ updated = waitForUpdate(3);
+ yield addonStatus(sim30tv, "installed");
+ yield updated;
+ // Wait for next tick to ensure UI elements are updated
+ yield nextTick();
+
+ is(findAll(".runtime-panel-item-simulator").length, 3, "Three simulators in runtime panel");
+
+ simulatorList.querySelectorAll(".configure-button")[2].click();
+ // Wait for next tick to ensure UI elements are updated
+ yield nextTick();
+
+ for (let param in defaults.television) {
+ is(form[param].value, String(defaults.television[param]), "Default TV value for device " + param);
+ }
+
+ // Test Simulator Menu
+ is(doc.querySelector("#tv_simulator_menu").style.visibility, "visible", "OpenTVDummyDirectory Button is not visible");
+
+ // Force reload the list of simulators.
+
+ Simulators._loadingPromise = null;
+ Simulators._simulators = [];
+ yield Simulators._load();
+ // Wait for next tick to ensure UI elements are updated
+ yield nextTick();
+
+ is(findAll(".runtime-panel-item-simulator").length, 3, "Three simulators saved and reloaded " + Simulators._simulators.map(s => s.name).join(','));
+
+ // Uninstall the 3.0 TV and 2.0 addons, and watch their Simulator objects disappear.
+
+ sim30tv.uninstall();
+
+ yield addonStatus(sim30tv, "uninstalled");
+
+ is(findAll(".runtime-panel-item-simulator").length, 2, "Two simulators left in runtime panel");
+
+ sim20.uninstall();
+
+ yield addonStatus(sim20, "uninstalled");
+
+ is(findAll(".runtime-panel-item-simulator").length, 1, "One simulator left in runtime panel");
+
+ // Remove 1.0 simulator.
+
+ simulatorList.querySelectorAll(".configure-button")[0].click();
+ // Wait for next tick to ensure UI elements are updated
+ yield nextTick();
+
+ doc.querySelector("#remove").click();
+ // Wait for next tick to ensure UI elements are updated
+ yield nextTick();
+
+ is(findAll(".runtime-panel-item-simulator").length, 0, "Last simulator was removed");
+
+ yield asyncStorage.removeItem("simulators");
+
+ sim10.uninstall();
+
+ MockFilePicker.cleanup();
+
+ doc.querySelector("#close").click();
+
+ ok(!win.document.querySelector("#deck").selectedPanel, "No panel selected");
+
+ yield closeWebIDE(win);
+
+ SimpleTest.finish();
+
+ });
+ }
+
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/webide/test/test_telemetry.html b/devtools/client/webide/test/test_telemetry.html
new file mode 100644
index 000000000..225ddb89b
--- /dev/null
+++ b/devtools/client/webide/test/test_telemetry.html
@@ -0,0 +1,325 @@
+<!DOCTYPE html>
+
+<html>
+
+ <head>
+ <meta charset="utf8">
+ <title></title>
+
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js"></script>
+ <script type="application/javascript;version=1.8" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ </head>
+
+ <body>
+
+ <script type="application/javascript;version=1.8">
+ const Telemetry = require("devtools/client/shared/telemetry");
+ const { _DeprecatedUSBRuntime, _WiFiRuntime, _SimulatorRuntime,
+ _gRemoteRuntime, _gLocalRuntime, RuntimeTypes }
+ = require("devtools/client/webide/modules/runtimes");
+
+ // Because we need to gather stats for the period of time that a tool has
+ // been opened we make use of setTimeout() to create tool active times.
+ const TOOL_DELAY = 200;
+
+ function patchTelemetry() {
+ Telemetry.prototype.telemetryInfo = {};
+ Telemetry.prototype._oldlog = Telemetry.prototype.log;
+ Telemetry.prototype.log = function(histogramId, value) {
+ if (histogramId) {
+ if (!this.telemetryInfo[histogramId]) {
+ this.telemetryInfo[histogramId] = [];
+ }
+ this.telemetryInfo[histogramId].push(value);
+ }
+ }
+ Telemetry.prototype._oldlogKeyed = Telemetry.prototype.logKeyed;
+ Telemetry.prototype.logKeyed = function(histogramId, key, value) {
+ // This simple reduction is enough to test WebIDE's usage
+ this.log(`${histogramId}|${key}`, value);
+ }
+ }
+
+ function resetTelemetry() {
+ Telemetry.prototype.log = Telemetry.prototype._oldlog;
+ Telemetry.prototype.logKeyed = Telemetry.prototype._oldlogKeyed;
+ delete Telemetry.prototype._oldlog;
+ delete Telemetry.prototype._oldlogKeyed;
+ delete Telemetry.prototype.telemetryInfo;
+ }
+
+ function cycleWebIDE() {
+ return Task.spawn(function*() {
+ let win = yield openWebIDE();
+ // Wait a bit, so we're open for a non-zero time
+ yield waitForTime(TOOL_DELAY);
+ yield closeWebIDE(win);
+ });
+ }
+
+ function addFakeRuntimes(win) {
+ // We use the real runtimes here (and switch out some functionality)
+ // so we can ensure that logging happens as it would in real use.
+
+ let usb = new _DeprecatedUSBRuntime("fakeUSB");
+ // Use local pipe instead
+ usb.connect = function(connection) {
+ ok(connection, win.AppManager.connection, "connection is valid");
+ connection.host = null; // force connectPipe
+ connection.connect();
+ return promise.resolve();
+ };
+ win.AppManager.runtimeList.usb.push(usb);
+
+ let wifi = new _WiFiRuntime("fakeWiFi");
+ // Use local pipe instead
+ wifi.connect = function(connection) {
+ ok(connection, win.AppManager.connection, "connection is valid");
+ connection.host = null; // force connectPipe
+ connection.connect();
+ return promise.resolve();
+ };
+ win.AppManager.runtimeList.wifi.push(wifi);
+
+ let sim = new _SimulatorRuntime({ id: "fakeSimulator" });
+ // Use local pipe instead
+ sim.connect = function(connection) {
+ ok(connection, win.AppManager.connection, "connection is valid");
+ connection.host = null; // force connectPipe
+ connection.connect();
+ return promise.resolve();
+ };
+ Object.defineProperty(sim, "name", {
+ get() {
+ return this.version;
+ }
+ });
+ win.AppManager.runtimeList.simulator.push(sim);
+
+ let remote = _gRemoteRuntime;
+ // Use local pipe instead
+ remote.connect = function(connection) {
+ ok(connection, win.AppManager.connection, "connection is valid");
+ connection.host = null; // force connectPipe
+ connection.connect();
+ return promise.resolve();
+ };
+ let local = _gLocalRuntime;
+
+ let other = Object.create(_gLocalRuntime);
+ other.type = RuntimeTypes.OTHER;
+
+ win.AppManager.runtimeList.other = [remote, local, other];
+
+ win.AppManager.update("runtime-list");
+ }
+
+ function addTestApp(win) {
+ return Task.spawn(function*() {
+ let packagedAppLocation = getTestFilePath("../app");
+ let winProject = getProjectWindow(win);
+ let onValidated = waitForUpdate(win, "project-validated");
+ let onDetails = waitForUpdate(win, "details");
+ yield winProject.projectList.importPackagedApp(packagedAppLocation);
+ yield onValidated;
+ yield onDetails;
+ });
+ }
+
+ function startConnection(win, docRuntime, type, index) {
+ let panelNode = docRuntime.querySelector("#runtime-panel");
+ let items = panelNode.querySelectorAll(".runtime-panel-item-" + type);
+ if (index === undefined) {
+ is(items.length, 1, "Found one runtime button");
+ }
+
+ let deferred = promise.defer();
+ win.AppManager.connection.once(
+ win.Connection.Events.CONNECTED,
+ () => deferred.resolve());
+
+ items[index || 0].click();
+
+ return deferred.promise;
+ }
+
+ function waitUntilConnected(win) {
+ return Task.spawn(function*() {
+ ok(win.document.querySelector("window").className, "busy", "UI is busy");
+ yield win.UI._busyPromise;
+ is(Object.keys(DebuggerServer._connections).length, 1, "Connected");
+ // Logging runtime info needs to use the device actor
+ yield waitForUpdate(win, "runtime-global-actors");
+ // Ensure detailed telemetry is recorded
+ yield waitForUpdate(win, "runtime-telemetry");
+ });
+ }
+
+ function connectToRuntime(win, docRuntime, type, index) {
+ return Task.spawn(function*() {
+ startConnection(win, docRuntime, type, index);
+ yield waitUntilConnected(win);
+ });
+ }
+
+ function checkResults() {
+ let result = Telemetry.prototype.telemetryInfo;
+ for (let [histId, value] of Iterator(result)) {
+ if (histId === "DEVTOOLS_WEBIDE_IMPORT_PROJECT_BOOLEAN") {
+ ok(value.length === 1 && !!value[0],
+ histId + " has 1 successful entry");
+ } else if (histId ===
+ "DEVTOOLS_WEBIDE_PROJECT_EDITOR_OPENED_COUNT") {
+ ok(value.length === 1 && !!value[0],
+ histId + " has 1 successful entry");
+ } else if (histId === "DEVTOOLS_WEBIDE_OPENED_COUNT") {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function(element) {
+ return !!element;
+ });
+
+ ok(okay, "All " + histId + " entries are true");
+ } else if (histId.endsWith("WEBIDE_TIME_ACTIVE_SECONDS")) {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function(element) {
+ return element > 0;
+ });
+
+ ok(okay, "All " + histId + " entries have time > 0");
+ } else if (histId.endsWith("EDITOR_TIME_ACTIVE_SECONDS")) {
+ ok(value.length === 1 && value[0] > 0,
+ histId + " has 1 entry with time > 0");
+ } else if (histId === "DEVTOOLS_WEBIDE_CONNECTION_RESULT") {
+ ok(value.length === 6, histId + " has 6 connection results");
+
+ let okay = value.every(function(element) {
+ return !!element;
+ });
+
+ ok(okay, "All " + histId + " connections succeeded");
+ } else if (histId.endsWith("CONNECTION_RESULT")) {
+ ok(value.length === 1 && !!value[0],
+ histId + " has 1 successful connection");
+ } else if (histId === "DEVTOOLS_WEBIDE_CONNECTION_TIME_SECONDS") {
+ ok(value.length === 6, histId + " has 6 connection results");
+
+ let okay = value.every(function(element) {
+ return element > 0;
+ });
+
+ ok(okay, "All " + histId + " connections have time > 0");
+ } else if (histId.endsWith("USED")) {
+ ok(value.length === 6, histId + " has 6 connection actions");
+
+ let okay = value.every(function(element) {
+ return !element;
+ });
+
+ ok(okay, "All " + histId + " actions were skipped");
+ } else if (histId === "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_TYPE|USB") {
+ is(value.length, 1, histId + " has 1 connection results");
+ } else if (histId === "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_TYPE|WIFI") {
+ is(value.length, 1, histId + " has 1 connection results");
+ } else if (histId === "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_TYPE|SIMULATOR") {
+ is(value.length, 1, histId + " has 1 connection results");
+ } else if (histId === "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_TYPE|REMOTE") {
+ is(value.length, 1, histId + " has 1 connection results");
+ } else if (histId === "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_TYPE|LOCAL") {
+ is(value.length, 1, histId + " has 1 connection results");
+ } else if (histId === "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_TYPE|OTHER") {
+ is(value.length, 1, histId + " has 1 connection results");
+ } else if (histId === "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_ID|fakeUSB") {
+ is(value.length, 1, histId + " has 1 connection results");
+ } else if (histId === "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_ID|fakeWiFi") {
+ is(value.length, 1, histId + " has 1 connection results");
+ } else if (histId === "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_ID|fakeSimulator") {
+ is(value.length, 1, histId + " has 1 connection results");
+ } else if (histId === "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_ID|unknown") {
+ is(value.length, 1, histId + " has 1 connection results");
+ } else if (histId === "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_ID|local") {
+ is(value.length, 2, histId + " has 2 connection results");
+ } else if (histId.startsWith("DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_PROCESSOR")) {
+ let processor = histId.split("|")[1];
+ is(processor, Services.appinfo.XPCOMABI.split("-")[0], "Found runtime processor");
+ is(value.length, 6, histId + " has 6 connection results");
+ } else if (histId.startsWith("DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_OS")) {
+ let os = histId.split("|")[1];
+ is(os, Services.appinfo.OS, "Found runtime OS");
+ is(value.length, 6, histId + " has 6 connection results");
+ } else if (histId.startsWith("DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_PLATFORM_VERSION")) {
+ let platformversion = histId.split("|")[1];
+ is(platformversion, Services.appinfo.platformVersion, "Found runtime platform version");
+ is(value.length, 6, histId + " has 6 connection results");
+ } else if (histId.startsWith("DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_APP_TYPE")) {
+ let apptype = histId.split("|")[1];
+ is(apptype, "firefox", "Found runtime app type");
+ is(value.length, 6, histId + " has 6 connection results");
+ } else if (histId.startsWith("DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_VERSION")) {
+ let version = histId.split("|")[1];
+ is(version, Services.appinfo.version, "Found runtime version");
+ is(value.length, 6, histId + " has 6 connection results");
+ } else {
+ ok(false, "Unexpected " + histId + " was logged");
+ }
+ }
+ }
+
+ window.onload = function() {
+ SimpleTest.testInChaosMode();
+ SimpleTest.waitForExplicitFinish();
+
+ let win;
+
+ SimpleTest.registerCleanupFunction(() => {
+ return Task.spawn(function*() {
+ if (win) {
+ yield closeWebIDE(win);
+ }
+ DebuggerServer.destroy();
+ yield removeAllProjects();
+ resetTelemetry();
+ });
+ });
+
+ Task.spawn(function*() {
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+
+ patchTelemetry();
+
+ // Cycle once, so we can test for multiple opens
+ yield cycleWebIDE();
+
+ win = yield openWebIDE();
+ let docRuntime = getRuntimeDocument(win);
+
+ // Wait a bit, so we're open for a non-zero time
+ yield waitForTime(TOOL_DELAY);
+ addFakeRuntimes(win);
+ yield addTestApp(win);
+
+ // Each one should log a connection result and non-zero connection
+ // time
+ yield connectToRuntime(win, docRuntime, "usb");
+ yield connectToRuntime(win, docRuntime, "wifi");
+ yield connectToRuntime(win, docRuntime, "simulator");
+ yield connectToRuntime(win, docRuntime, "other", 0 /* remote */);
+ yield connectToRuntime(win, docRuntime, "other", 1 /* local */);
+ yield connectToRuntime(win, docRuntime, "other", 2 /* other */);
+ yield closeWebIDE(win);
+ win = null;
+
+ checkResults();
+
+ SimpleTest.finish();
+ });
+ }
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/webide/test/test_toolbox.html b/devtools/client/webide/test/test_toolbox.html
new file mode 100644
index 000000000..71ac2706c
--- /dev/null
+++ b/devtools/client/webide/test/test_toolbox.html
@@ -0,0 +1,93 @@
+<!DOCTYPE html>
+
+<html>
+
+ <head>
+ <meta charset="utf8">
+ <title></title>
+
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js"></script>
+ <script type="application/javascript;version=1.8" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ </head>
+
+ <body>
+
+ <script type="application/javascript;version=1.8">
+ window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+
+ let win;
+
+ SimpleTest.registerCleanupFunction(() => {
+ Task.spawn(function*() {
+ if (win) {
+ yield closeWebIDE(win);
+ }
+ DebuggerServer.destroy();
+ yield removeAllProjects();
+ });
+ });
+
+ Task.spawn(function*() {
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+
+ win = yield openWebIDE();
+ let docRuntime = getRuntimeDocument(win);
+ let docProject = getProjectDocument(win);
+
+ win.AppManager.update("runtime-list");
+
+ let deferred = promise.defer();
+ win.AppManager.connection.once(
+ win.Connection.Events.CONNECTED,
+ () => deferred.resolve());
+
+ docRuntime.querySelectorAll(".runtime-panel-item-other")[1].click();
+
+ ok(win.document.querySelector("window").className, "busy", "UI is busy");
+ yield win.UI._busyPromise;
+
+ is(Object.keys(DebuggerServer._connections).length, 1, "Connected");
+
+ yield waitForUpdate(win, "runtime-global-actors");
+
+ ok(win.AppManager.isMainProcessDebuggable(), "Main process available");
+
+ // Select main process
+ SimpleTest.executeSoon(() => {
+ docProject.querySelectorAll("#project-panel-runtimeapps .panel-item")[0].click();
+ });
+
+ yield waitForUpdate(win, "project");
+
+ // Toolbox opens automatically for main process / runtime apps
+ ok(win.UI.toolboxPromise, "Toolbox promise exists");
+ let toolbox = yield win.UI.toolboxPromise;
+
+ yield toolbox.destroy();
+
+ ok(!win.UI.toolboxPromise, "Toolbox promise should be unset once toolbox.destroy()'s promise resolves");
+
+ // Reopen the toolbox right after to check races and also
+ // opening a toolbox more than just once against the same target
+ yield win.Cmds.toggleToolbox();
+
+ ok(win.UI.toolboxPromise, "Toolbox promise exists");
+
+ yield win.UI.destroyToolbox();
+
+ ok(!win.UI.toolboxPromise, "Toolbox promise is also nullified the second times");
+
+ yield win.Cmds.disconnectRuntime();
+
+ SimpleTest.finish();
+ });
+ }
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/webide/test/test_zoom.html b/devtools/client/webide/test/test_zoom.html
new file mode 100644
index 000000000..4ad3885d2
--- /dev/null
+++ b/devtools/client/webide/test/test_zoom.html
@@ -0,0 +1,77 @@
+<!DOCTYPE html>
+
+<html>
+
+ <head>
+ <meta charset="utf8">
+ <title></title>
+
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js"></script>
+ <script type="application/javascript;version=1.8" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ </head>
+
+ <body>
+
+ <script type="application/javascript;version=1.8">
+ window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+
+ Task.spawn(function* () {
+ let win = yield openWebIDE();
+ let viewer = win.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShell)
+ .contentViewer;
+
+ win.Cmds.zoomOut();
+ win.Cmds.zoomOut();
+ win.Cmds.zoomOut();
+ win.Cmds.zoomOut();
+ win.Cmds.zoomOut();
+ win.Cmds.zoomOut();
+ win.Cmds.zoomOut();
+
+ let roundZoom = Math.round(10 * viewer.fullZoom) / 10;
+ is(roundZoom, 0.6, "Reach min zoom");
+
+ win.Cmds.zoomIn();
+ win.Cmds.zoomIn();
+ win.Cmds.zoomIn();
+ win.Cmds.zoomIn();
+ win.Cmds.zoomIn();
+ win.Cmds.zoomIn();
+ win.Cmds.zoomIn();
+ win.Cmds.zoomIn();
+ win.Cmds.zoomIn();
+ win.Cmds.zoomIn();
+ win.Cmds.zoomIn();
+ win.Cmds.zoomIn();
+
+ roundZoom = Math.round(10 * viewer.fullZoom) / 10;
+ is(roundZoom, 1.4, "Reach max zoom");
+
+ yield closeWebIDE(win);
+
+ win = yield openWebIDE();
+ viewer = win.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShell)
+ .contentViewer;
+
+ roundZoom = Math.round(10 * viewer.fullZoom) / 10;
+ is(roundZoom, 1.4, "Zoom restored");
+
+ win.Cmds.resetZoom();
+
+ is(viewer.fullZoom, 1, "Zoom reset");
+
+ yield closeWebIDE(win);
+
+ SimpleTest.finish();
+ });
+ }
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/webide/test/validator/no-name-or-icon/home.html b/devtools/client/webide/test/validator/no-name-or-icon/home.html
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/devtools/client/webide/test/validator/no-name-or-icon/home.html
diff --git a/devtools/client/webide/test/validator/no-name-or-icon/manifest.webapp b/devtools/client/webide/test/validator/no-name-or-icon/manifest.webapp
new file mode 100644
index 000000000..149e3fb79
--- /dev/null
+++ b/devtools/client/webide/test/validator/no-name-or-icon/manifest.webapp
@@ -0,0 +1,3 @@
+{
+ "launch_path": "/home.html"
+}
diff --git a/devtools/client/webide/test/validator/non-absolute-path/manifest.webapp b/devtools/client/webide/test/validator/non-absolute-path/manifest.webapp
new file mode 100644
index 000000000..64744067f
--- /dev/null
+++ b/devtools/client/webide/test/validator/non-absolute-path/manifest.webapp
@@ -0,0 +1,7 @@
+{
+ "name": "non-absolute path",
+ "icons": {
+ "128": "/icon.png"
+ },
+ "launch_path": "non-absolute.html"
+}
diff --git a/devtools/client/webide/test/validator/valid/alsoValid/manifest.webapp b/devtools/client/webide/test/validator/valid/alsoValid/manifest.webapp
new file mode 100644
index 000000000..20bd97bba
--- /dev/null
+++ b/devtools/client/webide/test/validator/valid/alsoValid/manifest.webapp
@@ -0,0 +1,7 @@
+{
+ "name": "valid at subfolder",
+ "launch_path": "/home.html",
+ "icons": {
+ "128": "/icon.png"
+ }
+}
diff --git a/devtools/client/webide/test/validator/valid/home.html b/devtools/client/webide/test/validator/valid/home.html
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/devtools/client/webide/test/validator/valid/home.html
diff --git a/devtools/client/webide/test/validator/valid/icon.png b/devtools/client/webide/test/validator/valid/icon.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/devtools/client/webide/test/validator/valid/icon.png
diff --git a/devtools/client/webide/test/validator/valid/manifest.webapp b/devtools/client/webide/test/validator/valid/manifest.webapp
new file mode 100644
index 000000000..2c22a1567
--- /dev/null
+++ b/devtools/client/webide/test/validator/valid/manifest.webapp
@@ -0,0 +1,7 @@
+{
+ "name": "valid",
+ "launch_path": "/home.html",
+ "icons": {
+ "128": "/icon.png"
+ }
+}
diff --git a/devtools/client/webide/test/validator/wrong-launch-path/icon.png b/devtools/client/webide/test/validator/wrong-launch-path/icon.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/devtools/client/webide/test/validator/wrong-launch-path/icon.png
diff --git a/devtools/client/webide/test/validator/wrong-launch-path/manifest.webapp b/devtools/client/webide/test/validator/wrong-launch-path/manifest.webapp
new file mode 100644
index 000000000..08057bae1
--- /dev/null
+++ b/devtools/client/webide/test/validator/wrong-launch-path/manifest.webapp
@@ -0,0 +1,7 @@
+{
+ "name": "valid",
+ "launch_path": "/wrong-path.html",
+ "icons": {
+ "128": "/icon.png"
+ }
+}
diff --git a/devtools/client/webide/themes/addons.css b/devtools/client/webide/themes/addons.css
new file mode 100644
index 000000000..1ae41f2d9
--- /dev/null
+++ b/devtools/client/webide/themes/addons.css
@@ -0,0 +1,79 @@
+/* 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/. */
+
+button {
+ line-height: 20px;
+ font-size: 1em;
+ height: 30px;
+ max-height: 30px;
+ min-width: 120px;
+ padding: 3px;
+ color: #737980;
+ border: 1px solid rgba(23,50,77,.4);
+ border-radius: 5px;
+ background-color: #f1f1f1;
+ background-image: linear-gradient(#fff, rgba(255,255,255,.1));
+ box-shadow: 0 1px 1px 0 #fff, inset 0 2px 2px 0 #fff;
+ text-shadow: 0 1px 1px #fefffe;
+ -moz-appearance: none;
+ -moz-border-top-colors: none !important;
+ -moz-border-right-colors: none !important;
+ -moz-border-bottom-colors: none !important;
+ -moz-border-left-colors: none !important;
+}
+
+button:hover {
+ background-image: linear-gradient(#fff, rgba(255,255,255,.6));
+ cursor: pointer;
+}
+
+button:hover:active {
+ background-image: linear-gradient(rgba(255,255,255,.1), rgba(255,255,255,.6));
+}
+
+progress {
+ height: 30px;
+ vertical-align: middle;
+ padding: 0;
+ width: 120px;
+}
+
+li {
+ margin: 20px 0;
+}
+
+.name {
+ display: inline-block;
+ min-width: 280px;
+}
+
+.status {
+ display: inline-block;
+ min-width: 120px;
+}
+
+.warning {
+ color: #F06;
+ margin: 0;
+ font-size: 0.9em;
+}
+
+li[status="unknown"],
+li > .uninstall-button,
+li > .install-button,
+li > progress {
+ display: none;
+}
+
+li[status="installed"] > .uninstall-button,
+li[status="uninstalled"] > .install-button,
+li[status="preparing"] > progress,
+li[status="downloading"] > progress,
+li[status="installing"] > progress {
+ display: inline;
+}
+
+li:not([status="uninstalled"]) > .warning {
+ display: none;
+}
diff --git a/devtools/client/webide/themes/config-view.css b/devtools/client/webide/themes/config-view.css
new file mode 100644
index 000000000..019e735df
--- /dev/null
+++ b/devtools/client/webide/themes/config-view.css
@@ -0,0 +1,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/. */
+
+html, body {
+ background: white;
+}
+
+.action {
+ display: inline;
+}
+
+.action[hidden] {
+ display: none;
+}
+
+#device-fields {
+ font-family: sans-serif;
+ padding-left: 6px;
+ width: 100%;
+ table-layout: auto;
+ margin-top: 110px;
+}
+
+#custom-value-name {
+ width: 50%;
+}
+
+header {
+ background-color: rgba(255, 255, 255, 0.8);
+ border-bottom: 1px solid #EEE;
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 90px;
+ padding: 10px 20px;
+}
+
+#device-fields td {
+ background-color: #F9F9F9;
+ border-bottom: 1px solid #CCC;
+ border-right: 1px solid #FFF;
+ font-size: 0.75em;
+}
+
+#device-fields td:first-child {
+ max-width: 250px;
+ min-width: 150px;
+}
+
+#device-fields td.preference-name, #device-fields td.setting-name {
+ width: 50%;
+ min-width: 400px;
+ word-break: break-all;
+}
+
+#device-fields button {
+ display: inline-block;
+ font-family: sans-serif;
+ font-size: 0.7rem;
+ white-space: nowrap;
+}
+
+#device-fields tr.hide, #device-fields button.hide {
+ display: none;
+}
+
+#device-fields .custom-input {
+ width: 130px;
+}
+
+#search {
+ margin-bottom: 20px;
+ width: 100%;
+}
+
+#search-bar {
+ width: 80%;
+}
diff --git a/devtools/client/webide/themes/deck.css b/devtools/client/webide/themes/deck.css
new file mode 100644
index 000000000..30537f612
--- /dev/null
+++ b/devtools/client/webide/themes/deck.css
@@ -0,0 +1,91 @@
+/* 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/. */
+
+html {
+ font: message-box;
+ font-size: 0.9em;
+ font-weight: normal;
+ margin: 0;
+ height: 100%;
+ color: #737980;
+ background-color: #ededed;
+}
+
+body {
+ margin: 0;
+ padding: 20px;
+ background-image: linear-gradient(#fff, #ededed 100px);
+}
+
+.text-input {
+ display: flex;
+}
+
+.text-input input {
+ flex: 0.5;
+ margin-left: 5px;
+}
+
+h1 {
+ font-size: 2em;
+ font-weight: lighter;
+ line-height: 1.2;
+ margin: 0;
+ margin-bottom: .5em;
+}
+
+#controls {
+ float: right;
+ position: relative;
+ top: -10px;
+ right: -10px;
+}
+
+#controls > a {
+ color: #4C9ED9;
+ font-size: small;
+ cursor: pointer;
+ border-bottom: 1px dotted;
+ margin-left: 10px;
+}
+
+table {
+ font-family: monospace;
+ border-collapse: collapse;
+}
+
+th, td {
+ padding: 5px;
+ border: 1px solid #eee;
+}
+
+th {
+ min-width: 100px;
+}
+
+th:first-of-type, td:first-of-type {
+ text-align: left;
+}
+
+li {
+ list-style: none;
+ padding: 2px;
+}
+
+li > label:hover {
+ background-color: rgba(0,0,0,0.02);
+}
+
+li > label > span {
+ display: inline-block;
+}
+
+input, select {
+ box-sizing: border-box;
+}
+
+select {
+ padding-top: 2px;
+ padding-bottom: 2px;
+}
diff --git a/devtools/client/webide/themes/default-app-icon.png b/devtools/client/webide/themes/default-app-icon.png
new file mode 100644
index 000000000..f186d9c62
--- /dev/null
+++ b/devtools/client/webide/themes/default-app-icon.png
Binary files differ
diff --git a/devtools/client/webide/themes/details.css b/devtools/client/webide/themes/details.css
new file mode 100644
index 000000000..dc73d5357
--- /dev/null
+++ b/devtools/client/webide/themes/details.css
@@ -0,0 +1,138 @@
+/* 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/. */
+
+body {
+ margin: 0;
+ background-color: white;
+ font: message-box;
+}
+
+.hidden {
+ display: none;
+}
+
+h1, h3, p {
+ margin: 0;
+}
+
+#toolbar {
+ background-color: #D8D8D8;
+ border-bottom: 1px solid #AAA;
+}
+
+#toolbar > button {
+ -moz-appearance: none;
+ background-color: transparent;
+ border-width: 0 1px 0 0;
+ border-color: #AAA;
+ border-style: solid;
+ margin: 0;
+ padding: 0 12px;
+ font-family: inherit;
+ font-weight: bold;
+ height: 24px;
+}
+
+#toolbar > button:hover {
+ background-color: #CCC;
+ cursor: pointer;
+}
+
+#validation_status {
+ float: right;
+ text-transform: uppercase;
+ font-size: 10px;
+ line-height: 24px;
+ padding: 0 12px;
+ color: white;
+}
+
+
+header {
+ padding: 20px 0;
+}
+
+header > div {
+ vertical-align: top;
+ display: flex;
+ flex-direction: column;
+}
+
+#icon {
+ height: 48px;
+ width: 48px;
+ float: left;
+ margin: 0 20px;
+}
+
+h1, #type {
+ line-height: 24px;
+ height: 24px; /* avoid collapsing if empty */
+ display: block;
+}
+
+h1 {
+ font-size: 20px;
+ overflow-x: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+}
+
+#type {
+ font-size: 10px;
+ text-transform: uppercase;
+ color: #777;
+}
+
+main {
+ padding-left: 88px;
+}
+
+h3 {
+ color: #999;
+ font-size: 10px;
+ font-weight: normal;
+}
+
+main > p {
+ margin-bottom: 20px;
+}
+
+.validation_messages {
+ margin-left: 74px;
+ list-style: none;
+ border-left: 4px solid transparent;
+ padding: 0 10px;;
+}
+
+
+body.valid #validation_status {
+ background-color: #81D135;
+}
+
+body.warning #validation_status {
+ background-color: #FFAC00;
+}
+
+body.error #validation_status {
+ background-color: #ED4C62;
+}
+
+#warningslist {
+ border-color: #FFAC00
+}
+
+#errorslist {
+ border-color: #ED4C62;
+}
+
+#validation_status > span {
+ display: none;
+}
+
+body.valid #validation_status > .valid,
+body.warning #validation_status > .warning,
+body.error #validation_status > .error {
+ display: inline;
+}
diff --git a/devtools/client/webide/themes/icons.png b/devtools/client/webide/themes/icons.png
new file mode 100644
index 000000000..5e1dd5c64
--- /dev/null
+++ b/devtools/client/webide/themes/icons.png
Binary files differ
diff --git a/devtools/client/webide/themes/jar.mn b/devtools/client/webide/themes/jar.mn
new file mode 100644
index 000000000..4235278da
--- /dev/null
+++ b/devtools/client/webide/themes/jar.mn
@@ -0,0 +1,24 @@
+# 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/.
+
+webide.jar:
+% skin webide classic/1.0 %skin/
+* skin/webide.css (webide.css)
+ skin/icons.png (icons.png)
+ skin/details.css (details.css)
+ skin/newapp.css (newapp.css)
+ skin/throbber.svg (throbber.svg)
+ skin/deck.css (deck.css)
+ skin/addons.css (addons.css)
+ skin/runtimedetails.css (runtimedetails.css)
+ skin/permissionstable.css (permissionstable.css)
+ skin/monitor.css (monitor.css)
+ skin/config-view.css (config-view.css)
+ skin/wifi-auth.css (wifi-auth.css)
+ skin/logs.css (logs.css)
+ skin/panel-listing.css (panel-listing.css)
+ skin/simulator.css (simulator.css)
+ skin/rocket.svg (rocket.svg)
+ skin/noise.png (noise.png)
+ skin/default-app-icon.png (default-app-icon.png)
diff --git a/devtools/client/webide/themes/logs.css b/devtools/client/webide/themes/logs.css
new file mode 100644
index 000000000..446b6e41c
--- /dev/null
+++ b/devtools/client/webide/themes/logs.css
@@ -0,0 +1,18 @@
+html, body {
+ background: var(--theme-body-background);
+ color: var(--theme-body-color);
+}
+
+h1 {
+ font-size: 1.2em;
+}
+
+ul {
+ padding: 0;
+ font-size: 1em;
+}
+
+li {
+ list-style: none;
+ margin: 0;
+}
diff --git a/devtools/client/webide/themes/monitor.css b/devtools/client/webide/themes/monitor.css
new file mode 100644
index 000000000..ba4b298ed
--- /dev/null
+++ b/devtools/client/webide/themes/monitor.css
@@ -0,0 +1,86 @@
+/* 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/. */
+
+/* Graph */
+.graph {
+ height: 500px;
+ width: 100%;
+ padding-top: 20px;
+ padding-bottom: 20px;
+ margin-bottom: 30px;
+ background-color: white;
+}
+.graph > svg, .sidebar {
+ display: inline-block;
+ vertical-align: top;
+}
+.disabled {
+ opacity: 0.5;
+}
+.graph.disabled {
+ height: 30px;
+}
+.graph.disabled > svg {
+ visibility: hidden;
+}
+.curve path, .event-slot line {
+ fill: none;
+ stroke-width: 1.5px;
+}
+.axis line {
+ fill: none;
+ stroke: #000;
+ shape-rendering: crispEdges;
+}
+.axis path {
+ fill: none;
+ stroke: black;
+ stroke-width: 1px;
+ shape-rendering: crispEdges;
+}
+.tick text, .x.ruler text, .y.ruler text {
+ font-size: 0.9em;
+}
+.x.ruler text {
+ text-anchor: middle;
+}
+.y.ruler text {
+ text-anchor: end;
+}
+
+/* Sidebar */
+.sidebar {
+ width: 150px;
+ overflow-x: hidden;
+}
+.sidebar label {
+ cursor: pointer;
+ display: block;
+}
+.sidebar span:not(.color) {
+ vertical-align: 13%;
+}
+.sidebar input {
+ visibility: hidden;
+}
+.sidebar input:hover {
+ visibility: visible;
+}
+.graph-title {
+ margin-top: 5px;
+ font-size: 1.2em;
+}
+.legend-color {
+ display: inline-block;
+ height: 10px;
+ width: 10px;
+ margin-left: 1px;
+ margin-right: 3px;
+}
+.legend-id {
+ font-size: .9em;
+}
+.graph.disabled > .sidebar > .legend {
+ display: none;
+}
diff --git a/devtools/client/webide/themes/moz.build b/devtools/client/webide/themes/moz.build
new file mode 100644
index 000000000..aac3a838c
--- /dev/null
+++ b/devtools/client/webide/themes/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+JAR_MANIFESTS += ['jar.mn']
diff --git a/devtools/client/webide/themes/newapp.css b/devtools/client/webide/themes/newapp.css
new file mode 100644
index 000000000..0b351a40a
--- /dev/null
+++ b/devtools/client/webide/themes/newapp.css
@@ -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/. */
+
+dialog {
+ -moz-appearance: none;
+ background-image: linear-gradient(rgb(255, 255, 255), rgb(237, 237, 237) 100px);
+ font-family: "Clear Sans", sans-serif;
+ color: #424E5A;
+ overflow-y: scroll;
+}
+
+.header-name {
+ font-size: 1.5rem;
+ font-weight: normal;
+ margin: 15px 0;
+}
+
+richlistbox {
+ -moz-appearance: none;
+ overflow-y: auto;
+ border: 1px solid #424E5A;
+}
+
+richlistitem {
+ padding: 6px 0;
+}
+
+richlistitem:not([selected="true"]):hover {
+ background-color: rgba(0,0,0,0.04);
+}
+
+richlistitem > vbox > label {
+ margin: 0;
+ font-size: 1.1em;
+}
+
+richlistbox > description {
+ margin: 8px;
+}
+
+richlistitem {
+ -moz-box-align: start;
+}
+
+richlistitem > image {
+ height: 24px;
+ width: 24px;
+ margin: 0 6px;
+}
+
+textbox {
+ font-size: 1.2rem;
+}
diff --git a/devtools/client/webide/themes/noise.png b/devtools/client/webide/themes/noise.png
new file mode 100644
index 000000000..b3c42acae
--- /dev/null
+++ b/devtools/client/webide/themes/noise.png
Binary files differ
diff --git a/devtools/client/webide/themes/panel-listing.css b/devtools/client/webide/themes/panel-listing.css
new file mode 100644
index 000000000..06e51211c
--- /dev/null
+++ b/devtools/client/webide/themes/panel-listing.css
@@ -0,0 +1,150 @@
+/* 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/. */
+
+html {
+ font: message-box;
+ font-size: 11px;
+ font-weight: 400;
+}
+
+label,
+.panel-item,
+#project-panel-projects,
+#runtime-panel-projects {
+ display: block;
+ float: left;
+ width: 100%;
+ text-align: left;
+}
+
+.project-image,
+.panel-item span {
+ display: inline-block;
+ float: left;
+ line-height: 20px;
+}
+
+.project-image {
+ margin-right: 10px;
+ max-height: 20px;
+}
+
+.panel-header {
+ color: #ACACAC;
+ text-transform: uppercase;
+ line-height: 200%;
+ margin: 5px 5px 0 5px;
+ font-weight: 700;
+ width: 100%;
+}
+
+.panel-header:first-child {
+ margin-top: 0;
+}
+
+.panel-header[hidden], .panel-item[hidden] {
+ display: none;
+}
+
+#runtime-panel-simulator,
+.panel-item-complex {
+ clear: both;
+ position: relative;
+}
+
+.panel-item span {
+ display: block;
+ float: left;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ width: 75%;
+ white-space: nowrap;
+}
+
+.panel-item {
+ -moz-appearance: none;
+ -moz-box-align: center;
+ padding: 3%;
+ display: block;
+ width: 94%;
+ cursor: pointer;
+ border-top: 1px solid transparent;
+ border-left: 0;
+ border-bottom: 1px solid #CCC;
+ border-right: 0;
+ background-color: transparent;
+}
+
+button.panel-item {
+ background-position: 5px 5px;
+ background-repeat: no-repeat;
+ background-size: 14px 14px;
+ padding-left: 25px;
+ width: 100%;
+}
+
+.panel-item:disabled {
+ background-color: #FFF;
+ color: #5A5A5A;
+ opacity: 0.5;
+ cursor: default;
+}
+
+.refresh-icon {
+ background-image: url("chrome://devtools/skin/images/reload.svg");
+ height: 14px;
+ width: 14px;
+ border: 0;
+ opacity: 0.6;
+ display: inline-block;
+ margin: 3px;
+ float: right;
+}
+
+.panel-item:not(:disabled):hover,
+button.panel-item:not(:disabled):hover {
+ background-color: #CCF0FD;
+ border-top: 1px solid #EDEDED;
+}
+
+.configure-button {
+ display: inline-block;
+ height: 30px;
+ width: 30px;
+ background-color: transparent;
+ background-image: -moz-image-rect(url("icons.png"), 104, 462, 129, 438);
+ background-position: center center;
+ background-repeat: no-repeat;
+ background-size: 14px 14px;
+ position: absolute;
+ top: -2px;
+ right: 0;
+ border: 0;
+}
+
+.configure-button:hover {
+ cursor: pointer;
+}
+
+.project-panel-item-openpackaged { background-image: -moz-image-rect(url("icons.png"), 260, 438, 286, 412); }
+.runtime-panel-item-simulator { background-image: -moz-image-rect(url("icons.png"), 0, 438, 26, 412); }
+.runtime-panel-item-other { background-image: -moz-image-rect(url("icons.png"), 26, 438, 52, 412); }
+#runtime-permissions { background-image: -moz-image-rect(url("icons.png"), 105, 438, 131, 412); }
+#runtime-screenshot { background-image: -moz-image-rect(url("icons.png"), 131, 438, 156, 412); }
+
+#runtime-preferences,
+#runtime-settings { background-image: -moz-image-rect(url("icons.png"), 105, 464, 131, 438); }
+
+#runtime-panel-nousbdevice,
+#runtime-details { background-image: -moz-image-rect(url("icons.png"), 156, 438, 182, 412); }
+
+.runtime-panel-item-usb,
+#runtime-disconnect { background-image: -moz-image-rect(url("icons.png"), 52, 438, 78, 412); }
+
+.runtime-panel-item-wifi,
+.project-panel-item-openhosted { background-image: -moz-image-rect(url("icons.png"), 208, 438, 234, 412); }
+
+.project-panel-item-newapp,
+#runtime-panel-noadbhelper,
+#runtime-panel-installsimulator { background-image: -moz-image-rect(url("icons.png"), 234, 438, 260, 412); }
diff --git a/devtools/client/webide/themes/permissionstable.css b/devtools/client/webide/themes/permissionstable.css
new file mode 100644
index 000000000..3a45e0d74
--- /dev/null
+++ b/devtools/client/webide/themes/permissionstable.css
@@ -0,0 +1,23 @@
+/* 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/. */
+
+html, body {
+ background: white;
+}
+
+.permissionstable td {
+ text-align: center;
+}
+
+.permallow {
+ color: rgb(152,207,57);
+}
+
+.permprompt {
+ color: rgb(0,158,237);
+}
+
+.permdeny {
+ color: rgb(204,73,8);
+}
diff --git a/devtools/client/webide/themes/rocket.svg b/devtools/client/webide/themes/rocket.svg
new file mode 100644
index 000000000..a0cca5c21
--- /dev/null
+++ b/devtools/client/webide/themes/rocket.svg
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewBox="0 0 24 24">
+ <g opacity="0.1">
+ <path fill="#fff" d="M12,2.3c-1.127,0-3.333,3.721-4.084,7.411l-2.535,2.535v6.619l1.767,0l2.464-2.464 c0.252,0.264,0.529,0.486,0.827,0.662h3.118c0.299-0.175,0.579-0.397,0.831-0.662l2.464,2.464l1.767,0v-6.619l-2.535-2.535 C15.333,6.021,13.127,2.3,12,2.3z M12.003,6.181c0.393,0,1.084,1.103,1.515,2.423c-0.466-0.087-0.963-0.135-1.481-0.135 c-0.545,0-1.066,0.054-1.553,0.15C10.914,7.292,11.608,6.181,12.003,6.181z"/>
+ <path fill="#fff" d="M12.792,18.755c0,0.778-0.603,1.408-0.805,1.408c-0.201,0-0.805-0.631-0.805-1.408 c0-0.301,0.055-0.579,0.147-0.809h-0.932c-0.109,0.403-0.171,0.854-0.171,1.33c0,1.714,1.33,3.104,1.774,3.104 s1.774-1.389,1.774-3.103c0-0.477-0.062-0.927-0.171-1.331l-0.957,0C12.738,18.175,12.792,18.453,12.792,18.755z"/>
+ <path fill="#414042" d="M12,2c-1.127,0-3.333,3.721-4.084,7.411l-2.535,2.535v6.619l1.767,0l2.464-2.464 c0.252,0.264,0.529,0.486,0.827,0.662h3.118c0.299-0.175,0.579-0.397,0.831-0.662l2.464,2.464l1.767,0v-6.619l-2.535-2.535 C15.333,5.721,13.127,2,12,2z M12.003,5.881c0.393,0,1.084,1.103,1.515,2.423c-0.466-0.087-0.963-0.135-1.481-0.135 c-0.545,0-1.066,0.054-1.553,0.15C10.914,6.992,11.608,5.881,12.003,5.881z"/>
+ <path fill="#414042" d="M12.792,18.455c0,0.778-0.603,1.408-0.805,1.408c-0.201,0-0.805-0.631-0.805-1.408 c0-0.301,0.055-0.579,0.147-0.809h-0.932c-0.109,0.403-0.171,0.854-0.171,1.33c0,1.714,1.33,3.104,1.774,3.104 s1.774-1.389,1.774-3.103c0-0.477-0.062-0.927-0.171-1.331l-0.957,0C12.738,17.875,12.792,18.153,12.792,18.455z"/>
+ </g>
+</svg>
diff --git a/devtools/client/webide/themes/runtimedetails.css b/devtools/client/webide/themes/runtimedetails.css
new file mode 100644
index 000000000..91ced5bff
--- /dev/null
+++ b/devtools/client/webide/themes/runtimedetails.css
@@ -0,0 +1,25 @@
+/* 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/. */
+
+html, body {
+ background: white;
+}
+
+#devicePrivileges {
+ font-family: monospace;
+ padding-left: 6px;
+}
+
+#devtools-check > a {
+ color: #4C9ED9;
+ cursor: pointer;
+}
+
+.action {
+ display: inline;
+}
+
+.action[hidden] {
+ display: none;
+}
diff --git a/devtools/client/webide/themes/simulator.css b/devtools/client/webide/themes/simulator.css
new file mode 100644
index 000000000..036cfcdb4
--- /dev/null
+++ b/devtools/client/webide/themes/simulator.css
@@ -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/. */
+
+select:not(.custom) > option[value="custom"] {
+ display: none;
+}
+
+select, input[type="text"] {
+ width: 13rem;
+}
+
+input[name="name"] {
+ height: 1.8rem;
+}
+
+input[type="number"] {
+ width: 6rem;
+}
+
+input[type="text"], input[type="number"] {
+ padding-left: 0.2rem;
+}
+
+li > label:hover {
+ background-color: transparent;
+}
+
+ul {
+ padding-left: 0;
+}
+
+.label {
+ width: 6rem;
+ padding: 0.2rem;
+ text-align: right;
+}
+
+.hidden {
+ display: none;
+}
diff --git a/devtools/client/webide/themes/throbber.svg b/devtools/client/webide/themes/throbber.svg
new file mode 100644
index 000000000..d89fb3851
--- /dev/null
+++ b/devtools/client/webide/themes/throbber.svg
@@ -0,0 +1,22 @@
+<!-- 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/. -->
+
+<svg xmlns="http://www.w3.org/2000/svg"
+ width="24" height="24" viewBox="0 0 64 64">
+ <g>
+ <rect x="30" y="4" width="4" height="15" transform="rotate(0, 32, 32)" fill="#BBB"/>
+ <rect x="30" y="4" width="4" height="15" transform="rotate(30, 32, 32)" fill="#AAA"/>
+ <rect x="30" y="4" width="4" height="15" transform="rotate(60, 32, 32)" fill="#999"/>
+ <rect x="30" y="4" width="4" height="15" transform="rotate(90, 32, 32)" fill="#888"/>
+ <rect x="30" y="4" width="4" height="15" transform="rotate(120, 32, 32)" fill="#777"/>
+ <rect x="30" y="4" width="4" height="15" transform="rotate(150, 32, 32)" fill="#666"/>
+ <rect x="30" y="4" width="4" height="15" transform="rotate(180, 32, 32)" fill="#555"/>
+ <rect x="30" y="4" width="4" height="15" transform="rotate(210, 32, 32)" fill="#444"/>
+ <rect x="30" y="4" width="4" height="15" transform="rotate(240, 32, 32)" fill="#333"/>
+ <rect x="30" y="4" width="4" height="15" transform="rotate(270, 32, 32)" fill="#222"/>
+ <rect x="30" y="4" width="4" height="15" transform="rotate(300, 32, 32)" fill="#111"/>
+ <rect x="30" y="4" width="4" height="15" transform="rotate(330, 32, 32)" fill="#000"/>
+ <animateTransform attributeName="transform" type="rotate" calcMode="discrete" values="0 32 32;30 32 32;60 32 32;90 32 32;120 32 32;150 32 32;180 32 32;210 32 32;240 32 32;270 32 32;300 32 32;330 32 32" dur="0.8s" repeatCount="indefinite"/>
+ </g>
+</svg>
diff --git a/devtools/client/webide/themes/webide.css b/devtools/client/webide/themes/webide.css
new file mode 100644
index 000000000..0dea91a5f
--- /dev/null
+++ b/devtools/client/webide/themes/webide.css
@@ -0,0 +1,149 @@
+/* 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/. */
+
+/*
+ *
+ * Icons.png:
+ *
+ * actions icons: 100x100. Starts at 0x0.
+ * menu icons: 26x26. Starts at 312x0.
+ * anchors icons: 27x16. Starts at 364x0.
+ *
+ */
+
+#main-toolbar {
+ padding: 0 12px;
+}
+
+#action-buttons-container {
+ -moz-box-pack: center;
+ height: 50px;
+}
+
+#panel-buttons-container {
+ height: 50px;
+ margin-top: -50px;
+ pointer-events: none;
+}
+
+#panel-buttons-container > .panel-button {
+ pointer-events: auto;
+}
+
+#action-busy-undetermined {
+ height: 24px;
+ width: 24px;
+}
+
+window.busy .action-button,
+window:not(.busy) #action-busy,
+window.busy-undetermined #action-busy-determined,
+window.busy-determined #action-busy-undetermined {
+ display: none;
+}
+
+/* Panel buttons - runtime */
+
+#runtime-panel-button > .panel-button-image {
+ list-style-image: url('icons.png');
+ -moz-image-region: rect(78px,438px,104px,412px);
+ width: 13px;
+ height: 13px;
+}
+
+#runtime-panel-button[active="true"] > .panel-button-image {
+ -moz-image-region: rect(78px,464px,104px,438px);
+}
+
+/* Action buttons */
+
+.action-button {
+ -moz-appearance: none;
+ border-width: 0;
+ margin: 0;
+ padding: 0;
+ list-style-image: url('icons.png');
+}
+
+.action-button[disabled="true"] {
+ opacity: 0.4;
+}
+
+.action-button > .toolbarbutton-icon {
+ width: 40px;
+ height: 40px;
+}
+
+.action-button > .toolbarbutton-text {
+ display: none;
+}
+
+#action-button-play { -moz-image-region: rect(0,100px,100px,0) }
+#action-button-stop { -moz-image-region: rect(0,200px,100px,100px) }
+#action-button-debug { -moz-image-region: rect(0,300px,100px,200px) }
+
+#action-button-play:not([disabled="true"]):hover { -moz-image-region: rect(200px,100px,300px,0) }
+#action-button-stop:not([disabled="true"]):hover { -moz-image-region: rect(200px,200px,300px,100px) }
+#action-button-debug:not([disabled="true"]):not([active="true"]):hover { -moz-image-region: rect(200px,300px,300px,200px) }
+
+#action-button-play.reload { -moz-image-region: rect(0,400px,100px,303px) }
+#action-button-play.reload:hover { -moz-image-region: rect(200px,400px,300px,303px) }
+
+#action-button-debug[active="true"] { -moz-image-region: rect(100px,300px,200px,200px) }
+
+/* Panels */
+
+.panel-list {
+ display: none;
+ position: relative;
+ max-width: 190px;
+ overflow: hidden;
+}
+
+#project-listing-panel {
+ max-width: 165px;
+}
+
+.panel-list-wrapper {
+ height: 100%;
+ width: 100%;
+ min-width: 100px;
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ right: 0;
+ left: 0;
+}
+
+.panel-list-wrapper > iframe {
+ height: inherit;
+ width: 100%;
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ right: 0;
+ left: 0;
+}
+
+[sidebar-displayed] {
+ display: block;
+}
+
+/* Main view */
+
+#deck {
+ background-color: rgb(225, 225, 225);
+ background-image: url('rocket.svg'), url('noise.png');
+ background-repeat: no-repeat, repeat;
+ background-size: 35%, auto;
+ background-position: center center, top left;
+%ifndef XP_MACOSX
+ border-top: 1px solid #AAA;
+%endif
+}
+
+.devtools-horizontal-splitter {
+ position: relative;
+ border-bottom: 1px solid #aaa;
+}
diff --git a/devtools/client/webide/themes/wifi-auth.css b/devtools/client/webide/themes/wifi-auth.css
new file mode 100644
index 000000000..de6afc94e
--- /dev/null
+++ b/devtools/client/webide/themes/wifi-auth.css
@@ -0,0 +1,64 @@
+/* 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/. */
+
+html, body {
+ background: white;
+}
+
+body {
+ display: flex;
+ flex-direction: column;
+ height: 90%;
+}
+
+div {
+ margin-bottom: 1em;
+}
+
+#qr-code {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+#qr-code-wrapper {
+ flex: 1;
+ width: 100%;
+ margin: 2em 0;
+ text-align: center;
+}
+
+#qr-code img {
+ height: 100%;
+}
+
+.toggle-scanner {
+ color: #4C9ED9;
+ font-size: small;
+ cursor: pointer;
+ border-bottom: 1px dotted;
+}
+
+#token {
+ display: none;
+}
+
+body[token] > #token {
+ display: flex;
+ flex-direction: column;
+}
+
+body[token] > #qr-code {
+ display: none;
+}
+
+#token pre,
+#token a {
+ align-self: center;
+}
+
+#qr-size-note {
+ text-align: center
+}
diff --git a/devtools/client/webide/webide-prefs.js b/devtools/client/webide/webide-prefs.js
new file mode 100644
index 000000000..94871171d
--- /dev/null
+++ b/devtools/client/webide/webide-prefs.js
@@ -0,0 +1,35 @@
+# -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+# 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/.
+
+pref("devtools.webide.showProjectEditor", true);
+pref("devtools.webide.templatesURL", "https://code.cdn.mozilla.net/templates/list.json");
+pref("devtools.webide.autoinstallADBHelper", true);
+pref("devtools.webide.autoinstallFxdtAdapters", true);
+pref("devtools.webide.autoConnectRuntime", true);
+pref("devtools.webide.restoreLastProject", true);
+pref("devtools.webide.enableLocalRuntime", false);
+pref("devtools.webide.addonsURL", "https://ftp.mozilla.org/pub/mozilla.org/labs/fxos-simulator/index.json");
+pref("devtools.webide.simulatorAddonsURL", "https://ftp.mozilla.org/pub/mozilla.org/labs/fxos-simulator/#VERSION#/#OS#/fxos_#SLASHED_VERSION#_simulator-#OS#-latest.xpi");
+pref("devtools.webide.simulatorAddonID", "fxos_#SLASHED_VERSION#_simulator@mozilla.org");
+pref("devtools.webide.simulatorAddonRegExp", "fxos_(.*)_simulator@mozilla\\.org$");
+pref("devtools.webide.adbAddonURL", "https://ftp.mozilla.org/pub/mozilla.org/labs/fxos-simulator/adb-helper/#OS#/adbhelper-#OS#-latest.xpi");
+pref("devtools.webide.adbAddonID", "adbhelper@mozilla.org");
+pref("devtools.webide.adaptersAddonURL", "https://ftp.mozilla.org/pub/mozilla.org/labs/valence/#OS#/valence-#OS#-latest.xpi");
+pref("devtools.webide.adaptersAddonID", "fxdevtools-adapters@mozilla.org");
+pref("devtools.webide.monitorWebSocketURL", "ws://localhost:9000");
+pref("devtools.webide.lastConnectedRuntime", "");
+pref("devtools.webide.lastSelectedProject", "");
+pref("devtools.webide.logSimulatorOutput", false);
+pref("devtools.webide.widget.autoinstall", true);
+#ifdef MOZ_DEV_EDITION
+pref("devtools.webide.widget.enabled", true);
+pref("devtools.webide.widget.inNavbarByDefault", true);
+#else
+pref("devtools.webide.widget.enabled", false);
+pref("devtools.webide.widget.inNavbarByDefault", false);
+#endif
+pref("devtools.webide.zoom", "1");
+pref("devtools.webide.busyTimeout", 10000);
+pref("devtools.webide.autosaveFiles", true);
diff --git a/devtools/client/webpack.config.js b/devtools/client/webpack.config.js
new file mode 100644
index 000000000..ae8c35e24
--- /dev/null
+++ b/devtools/client/webpack.config.js
@@ -0,0 +1,39 @@
+/* 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";
+
+module.exports = [{
+ bail: true,
+ entry: [
+ "./sourceeditor/codemirror/addon/dialog/dialog.js",
+ "./sourceeditor/codemirror/addon/search/searchcursor.js",
+ "./sourceeditor/codemirror/addon/search/search.js",
+ "./sourceeditor/codemirror/addon/edit/matchbrackets.js",
+ "./sourceeditor/codemirror/addon/edit/closebrackets.js",
+ "./sourceeditor/codemirror/addon/comment/comment.js",
+ "./sourceeditor/codemirror/mode/javascript/javascript.js",
+ "./sourceeditor/codemirror/mode/xml/xml.js",
+ "./sourceeditor/codemirror/mode/css/css.js",
+ "./sourceeditor/codemirror/mode/htmlmixed/htmlmixed.js",
+ "./sourceeditor/codemirror/mode/clike/clike.js",
+ "./sourceeditor/codemirror/mode/wasm/wasm.js",
+ "./sourceeditor/codemirror/addon/selection/active-line.js",
+ "./sourceeditor/codemirror/addon/edit/trailingspace.js",
+ "./sourceeditor/codemirror/keymap/emacs.js",
+ "./sourceeditor/codemirror/keymap/vim.js",
+ "./sourceeditor/codemirror/keymap/sublime.js",
+ "./sourceeditor/codemirror/addon/fold/foldcode.js",
+ "./sourceeditor/codemirror/addon/fold/brace-fold.js",
+ "./sourceeditor/codemirror/addon/fold/comment-fold.js",
+ "./sourceeditor/codemirror/addon/fold/xml-fold.js",
+ "./sourceeditor/codemirror/addon/fold/foldgutter.js",
+ "./sourceeditor/codemirror/lib/codemirror.js",
+ ],
+ output: {
+ filename: "./sourceeditor/codemirror/codemirror.bundle.js",
+ libraryTarget: "var",
+ library: "CodeMirror",
+ },
+}];